summaryrefslogtreecommitdiffstats
path: root/comm/mailnews
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews')
-rw-r--r--comm/mailnews/addrbook/.eslintrc.js5
-rw-r--r--comm/mailnews/addrbook/content/abAddressBookNameDialog.js103
-rw-r--r--comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml51
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVDialog.js229
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVDialog.xhtml96
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVProperties.js140
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVProperties.xhtml93
-rw-r--r--comm/mailnews/addrbook/content/abDragDrop.js124
-rw-r--r--comm/mailnews/addrbook/content/abMailListDialog.js961
-rw-r--r--comm/mailnews/addrbook/content/abResultsPane.js482
-rw-r--r--comm/mailnews/addrbook/content/abView.js539
-rw-r--r--comm/mailnews/addrbook/content/map-list.js217
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookCard.jsm481
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookDirectory.jsm817
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookMailingList.jsm420
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookManager.jsm608
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookUtils.jsm522
-rw-r--r--comm/mailnews/addrbook/modules/CardDAVDirectory.jsm925
-rw-r--r--comm/mailnews/addrbook/modules/CardDAVUtils.jsm718
-rw-r--r--comm/mailnews/addrbook/modules/LDAPClient.jsm285
-rw-r--r--comm/mailnews/addrbook/modules/LDAPConnection.jsm53
-rw-r--r--comm/mailnews/addrbook/modules/LDAPDirectory.jsm230
-rw-r--r--comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm218
-rw-r--r--comm/mailnews/addrbook/modules/LDAPListenerBase.jsm117
-rw-r--r--comm/mailnews/addrbook/modules/LDAPMessage.jsm632
-rw-r--r--comm/mailnews/addrbook/modules/LDAPOperation.jsm198
-rw-r--r--comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm41
-rw-r--r--comm/mailnews/addrbook/modules/LDAPReplicationService.jsm233
-rw-r--r--comm/mailnews/addrbook/modules/LDAPService.jsm66
-rw-r--r--comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm112
-rw-r--r--comm/mailnews/addrbook/modules/LDAPURLParser.jsm42
-rw-r--r--comm/mailnews/addrbook/modules/QueryStringToExpression.jsm186
-rw-r--r--comm/mailnews/addrbook/modules/SQLiteDirectory.jsm474
-rw-r--r--comm/mailnews/addrbook/modules/VCardUtils.jsm973
-rw-r--r--comm/mailnews/addrbook/modules/components.conf136
-rw-r--r--comm/mailnews/addrbook/modules/moz.build34
-rw-r--r--comm/mailnews/addrbook/moz.build12
-rw-r--r--comm/mailnews/addrbook/prefs/content/pref-directory-add.js454
-rw-r--r--comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml190
-rw-r--r--comm/mailnews/addrbook/prefs/content/pref-editdirectories.js188
-rw-r--r--comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml77
-rw-r--r--comm/mailnews/addrbook/public/moz.build48
-rw-r--r--comm/mailnews/addrbook/public/nsIAbAddressCollector.idl46
-rw-r--r--comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl51
-rw-r--r--comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl120
-rw-r--r--comm/mailnews/addrbook/public/nsIAbCard.idl402
-rw-r--r--comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl40
-rw-r--r--comm/mailnews/addrbook/public/nsIAbDirectory.idl371
-rw-r--r--comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl133
-rw-r--r--comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl13
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl192
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl97
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl54
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl63
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl31
-rw-r--r--comm/mailnews/addrbook/public/nsIAbLDIFService.idl43
-rw-r--r--comm/mailnews/addrbook/public/nsIAbManager.idl132
-rw-r--r--comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl12
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPBERElement.idl122
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPBERValue.idl41
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPConnection.idl77
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPControl.idl45
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPErrors.idl447
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPMessage.idl167
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPMessageListener.idl48
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPModification.idl57
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPOperation.idl278
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPService.idl43
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl27
-rw-r--r--comm/mailnews/addrbook/public/nsILDAPURL.idl170
-rw-r--r--comm/mailnews/addrbook/public/nsIMsgVCardService.idl36
-rw-r--r--comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm69
-rw-r--r--comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm608
-rw-r--r--comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm219
-rw-r--r--comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm364
-rw-r--r--comm/mailnews/addrbook/src/components.conf129
-rw-r--r--comm/mailnews/addrbook/src/moz.build49
-rw-r--r--comm/mailnews/addrbook/src/nsAbAddressCollector.cpp281
-rw-r--r--comm/mailnews/addrbook/src/nsAbAddressCollector.h42
-rw-r--r--comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp98
-rw-r--r--comm/mailnews/addrbook/src/nsAbBooleanExpression.h41
-rw-r--r--comm/mailnews/addrbook/src/nsAbCardProperty.cpp1004
-rw-r--r--comm/mailnews/addrbook/src/nsAbCardProperty.h63
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirProperty.cpp573
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirProperty.h62
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp421
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirectoryQuery.h96
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp25
-rw-r--r--comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h26
-rw-r--r--comm/mailnews/addrbook/src/nsAbLDIFService.cpp787
-rw-r--r--comm/mailnews/addrbook/src/nsAbLDIFService.h37
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXCard.h51
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXCard.mm353
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXDirectory.h119
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXDirectory.mm911
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXUtils.h30
-rw-r--r--comm/mailnews/addrbook/src/nsAbOSXUtils.mm107
-rw-r--r--comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp1418
-rw-r--r--comm/mailnews/addrbook/src/nsAbOutlookDirectory.h181
-rw-r--r--comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp38
-rw-r--r--comm/mailnews/addrbook/src/nsAbOutlookInterface.h21
-rw-r--r--comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp293
-rw-r--r--comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h38
-rw-r--r--comm/mailnews/addrbook/src/nsAbWinHelper.cpp1491
-rw-r--r--comm/mailnews/addrbook/src/nsAbWinHelper.h183
-rw-r--r--comm/mailnews/addrbook/src/nsLDAPURL.cpp591
-rw-r--r--comm/mailnews/addrbook/src/nsLDAPURL.h95
-rw-r--r--comm/mailnews/addrbook/src/nsMapiAddressBook.cpp163
-rw-r--r--comm/mailnews/addrbook/src/nsMapiAddressBook.h52
-rw-r--r--comm/mailnews/addrbook/test/CardDAVServer.jsm634
-rw-r--r--comm/mailnews/addrbook/test/LDAPServer.jsm324
-rw-r--r--comm/mailnews/addrbook/test/moz.build14
-rw-r--r--comm/mailnews/addrbook/test/unit/data/bug534822prefs.js7
-rw-r--r--comm/mailnews/addrbook/test/unit/data/cardForEmail.sql95
-rw-r--r--comm/mailnews/addrbook/test/unit/data/collect.sql21
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.csv4
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.ldif36
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.txt4
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.vcf20
-rw-r--r--comm/mailnews/addrbook/test/unit/data/ldap_contacts.json104
-rw-r--r--comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat17
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf101
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/head.js66
-rw-r--r--comm/mailnews/addrbook/test/unit/head_cardDAV.js149
-rw-r--r--comm/mailnews/addrbook/test/unit/test_LDAPMessage.js101
-rw-r--r--comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js66
-rw-r--r--comm/mailnews/addrbook/test/unit/test_abCardProperty.js178
-rw-r--r--comm/mailnews/addrbook/test/unit/test_addrBookCard.js260
-rw-r--r--comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js125
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug1522453.js72
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug1769889.js95
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug387403.js16
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug448165.js18
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug534822.js38
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js148
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js550
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js68
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js282
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js408
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardForEmail.js111
-rw-r--r--comm/mailnews/addrbook/test/unit/test_collection.js404
-rw-r--r--comm/mailnews/addrbook/test/unit/test_collection_2.js42
-rw-r--r--comm/mailnews/addrbook/test/unit/test_convertOnSave.js329
-rw-r--r--comm/mailnews/addrbook/test/unit/test_db_enumerator.js89
-rw-r--r--comm/mailnews/addrbook/test/unit/test_delete_book.js82
-rw-r--r--comm/mailnews/addrbook/test/unit/test_export.js156
-rw-r--r--comm/mailnews/addrbook/test/unit/test_jsaddrbook.js420
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldap1.js205
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldap2.js41
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapOffline.js47
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapReplication.js159
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapquery.js181
-rw-r--r--comm/mailnews/addrbook/test/unit/test_mailList1.js65
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js128
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js468
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js194
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js164
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js258
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js120
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js248
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js162
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager2.js83
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager3.js42
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager4.js75
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager5.js43
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager6.js27
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js40
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js428
-rw-r--r--comm/mailnews/addrbook/test/unit/test_photoURL.js35
-rw-r--r--comm/mailnews/addrbook/test/unit/test_preferDisplayName.js79
-rw-r--r--comm/mailnews/addrbook/test/unit/test_search.js65
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCard.js474
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCard21.js190
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCardProperties.js899
-rw-r--r--comm/mailnews/addrbook/test/unit/xpcshell.ini60
-rw-r--r--comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini12
-rw-r--r--comm/mailnews/base/.eslintrc.js5
-rw-r--r--comm/mailnews/base/content/.eslintrc.js5
-rw-r--r--comm/mailnews/base/content/dateFormat.js227
-rw-r--r--comm/mailnews/base/content/folder-menupopup.js1238
-rw-r--r--comm/mailnews/base/content/folderProps.js480
-rw-r--r--comm/mailnews/base/content/folderProps.xhtml338
-rw-r--r--comm/mailnews/base/content/jsTreeView.js239
-rw-r--r--comm/mailnews/base/content/junkCommands.js449
-rw-r--r--comm/mailnews/base/content/junkLog.js48
-rw-r--r--comm/mailnews/base/content/junkLog.xhtml57
-rw-r--r--comm/mailnews/base/content/markByDate.js120
-rw-r--r--comm/mailnews/base/content/markByDate.xhtml79
-rw-r--r--comm/mailnews/base/content/menulist-charsetpicker.js86
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.js238
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.xhtml309
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.js189
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.xhtml89
-rw-r--r--comm/mailnews/base/content/msgSynchronize.js192
-rw-r--r--comm/mailnews/base/content/msgSynchronize.xhtml76
-rw-r--r--comm/mailnews/base/content/newFolderDialog.js82
-rw-r--r--comm/mailnews/base/content/newFolderDialog.xhtml107
-rw-r--r--comm/mailnews/base/content/newmailalert.js109
-rw-r--r--comm/mailnews/base/content/newmailalert.xhtml55
-rw-r--r--comm/mailnews/base/content/newsError.js48
-rw-r--r--comm/mailnews/base/content/newsError.xhtml57
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.js43
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.xhtml63
-rw-r--r--comm/mailnews/base/content/retention.js52
-rw-r--r--comm/mailnews/base/content/shutdownWindow.js97
-rw-r--r--comm/mailnews/base/content/shutdownWindow.xhtml53
-rw-r--r--comm/mailnews/base/content/subscribe.js496
-rw-r--r--comm/mailnews/base/content/subscribe.xhtml235
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.js206
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.xhtml84
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.js383
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.xhtml110
-rw-r--r--comm/mailnews/base/moz.build11
-rw-r--r--comm/mailnews/base/prefs/.eslintrc.js5
-rw-r--r--comm/mailnews/base/prefs/content/AccountManager.js1949
-rw-r--r--comm/mailnews/base/prefs/content/AccountManager.xhtml171
-rw-r--r--comm/mailnews/base/prefs/content/AccountWizard.js605
-rw-r--r--comm/mailnews/base/prefs/content/AccountWizard.xhtml161
-rw-r--r--comm/mailnews/base/prefs/content/SmtpServerEdit.js241
-rw-r--r--comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml208
-rw-r--r--comm/mailnews/base/prefs/content/accountUtils.js369
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.inc.xhtml125
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.js68
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.xhtml32
-rw-r--r--comm/mailnews/base/prefs/content/am-archiveoptions.js74
-rw-r--r--comm/mailnews/base/prefs/content/am-archiveoptions.xhtml136
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.inc.xhtml308
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.js555
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.xhtml35
-rw-r--r--comm/mailnews/base/prefs/content/am-identities-list.js206
-rw-r--r--comm/mailnews/base/prefs/content/am-identities-list.xhtml101
-rw-r--r--comm/mailnews/base/prefs/content/am-identity-edit.js577
-rw-r--r--comm/mailnews/base/prefs/content/am-identity-edit.xhtml239
-rw-r--r--comm/mailnews/base/prefs/content/am-junk.js335
-rw-r--r--comm/mailnews/base/prefs/content/am-junk.xhtml293
-rw-r--r--comm/mailnews/base/prefs/content/am-main.js110
-rw-r--r--comm/mailnews/base/prefs/content/am-main.xhtml344
-rw-r--r--comm/mailnews/base/prefs/content/am-offline.js435
-rw-r--r--comm/mailnews/base/prefs/content/am-offline.xhtml350
-rw-r--r--comm/mailnews/base/prefs/content/am-prefs.js142
-rw-r--r--comm/mailnews/base/prefs/content/am-server-advanced.js151
-rw-r--r--comm/mailnews/base/prefs/content/am-server-advanced.xhtml222
-rw-r--r--comm/mailnews/base/prefs/content/am-server.js615
-rw-r--r--comm/mailnews/base/prefs/content/am-server.xhtml674
-rw-r--r--comm/mailnews/base/prefs/content/am-serverwithnoidentities.js94
-rw-r--r--comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml152
-rw-r--r--comm/mailnews/base/prefs/content/am-smtp.js277
-rw-r--r--comm/mailnews/base/prefs/content/am-smtp.xhtml121
-rw-r--r--comm/mailnews/base/prefs/content/amUtils.js275
-rw-r--r--comm/mailnews/base/prefs/content/aw-accname.js28
-rw-r--r--comm/mailnews/base/prefs/content/aw-done.js39
-rw-r--r--comm/mailnews/base/prefs/content/aw-identity.js77
-rw-r--r--comm/mailnews/base/prefs/content/aw-incoming.js42
-rw-r--r--comm/mailnews/base/prefs/content/converterDialog.js385
-rw-r--r--comm/mailnews/base/prefs/content/converterDialog.xhtml53
-rw-r--r--comm/mailnews/base/prefs/content/removeAccount.js168
-rw-r--r--comm/mailnews/base/prefs/content/removeAccount.xhtml84
-rw-r--r--comm/mailnews/base/public/MailNewsTypes.h40
-rw-r--r--comm/mailnews/base/public/MailNewsTypes2.idl93
-rw-r--r--comm/mailnews/base/public/moz.build80
-rw-r--r--comm/mailnews/base/public/mozINewMailListener.idl22
-rw-r--r--comm/mailnews/base/public/mozINewMailNotificationService.idl58
-rw-r--r--comm/mailnews/base/public/msgCore.h210
-rw-r--r--comm/mailnews/base/public/msgIOAuth2Module.idl56
-rw-r--r--comm/mailnews/base/public/nsICopyMessageListener.idl24
-rw-r--r--comm/mailnews/base/public/nsICopyMessageStreamListener.idl20
-rw-r--r--comm/mailnews/base/public/nsIFolderListener.idl70
-rw-r--r--comm/mailnews/base/public/nsIFolderLookupService.idl52
-rw-r--r--comm/mailnews/base/public/nsIIncomingServerListener.idl32
-rw-r--r--comm/mailnews/base/public/nsIMailAuthModule.idl40
-rw-r--r--comm/mailnews/base/public/nsIMailChannel.idl109
-rw-r--r--comm/mailnews/base/public/nsIMapiRegistry.idl50
-rw-r--r--comm/mailnews/base/public/nsIMessenger.idl127
-rw-r--r--comm/mailnews/base/public/nsIMessengerMigrator.idl14
-rw-r--r--comm/mailnews/base/public/nsIMessengerOSIntegration.idl26
-rw-r--r--comm/mailnews/base/public/nsIMessengerWindowService.idl17
-rw-r--r--comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl16
-rw-r--r--comm/mailnews/base/public/nsIMsgAccount.idl86
-rw-r--r--comm/mailnews/base/public/nsIMsgAccountManager.idl256
-rw-r--r--comm/mailnews/base/public/nsIMsgAsyncPrompter.idl79
-rw-r--r--comm/mailnews/base/public/nsIMsgBiffManager.idl18
-rw-r--r--comm/mailnews/base/public/nsIMsgContentPolicy.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgCopyService.idl116
-rw-r--r--comm/mailnews/base/public/nsIMsgCopyServiceListener.idl55
-rw-r--r--comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl40
-rw-r--r--comm/mailnews/base/public/nsIMsgDBView.idl558
-rw-r--r--comm/mailnews/base/public/nsIMsgEnumerator.idl94
-rw-r--r--comm/mailnews/base/public/nsIMsgFolder.idl877
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCache.idl48
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCacheElement.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCompactor.idl34
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderListener.idl227
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderNotificationService.idl119
-rw-r--r--comm/mailnews/base/public/nsIMsgHdr.idl109
-rw-r--r--comm/mailnews/base/public/nsIMsgIdentity.idl311
-rw-r--r--comm/mailnews/base/public/nsIMsgIncomingServer.idl596
-rw-r--r--comm/mailnews/base/public/nsIMsgMailNewsUrl.idl211
-rw-r--r--comm/mailnews/base/public/nsIMsgMailSession.idl78
-rw-r--r--comm/mailnews/base/public/nsIMsgMdnGenerator.idl70
-rw-r--r--comm/mailnews/base/public/nsIMsgMessageService.idl226
-rw-r--r--comm/mailnews/base/public/nsIMsgOfflineManager.idl22
-rw-r--r--comm/mailnews/base/public/nsIMsgPluggableStore.idl335
-rw-r--r--comm/mailnews/base/public/nsIMsgProgress.idl38
-rw-r--r--comm/mailnews/base/public/nsIMsgProtocolHandler.idl13
-rw-r--r--comm/mailnews/base/public/nsIMsgProtocolInfo.idl97
-rw-r--r--comm/mailnews/base/public/nsIMsgPurgeService.idl13
-rw-r--r--comm/mailnews/base/public/nsIMsgShutdown.idl67
-rw-r--r--comm/mailnews/base/public/nsIMsgStatusFeedback.idl18
-rw-r--r--comm/mailnews/base/public/nsIMsgTagService.idl67
-rw-r--r--comm/mailnews/base/public/nsIMsgThread.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl28
-rw-r--r--comm/mailnews/base/public/nsIMsgWindow.idl64
-rw-r--r--comm/mailnews/base/public/nsISpamSettings.idl97
-rw-r--r--comm/mailnews/base/public/nsIStatusBarBiffManager.idl13
-rw-r--r--comm/mailnews/base/public/nsIStopwatch.idl44
-rw-r--r--comm/mailnews/base/public/nsISubscribableServer.idl74
-rw-r--r--comm/mailnews/base/public/nsIUrlListener.idl32
-rw-r--r--comm/mailnews/base/public/nsIUserInfo.idl32
-rw-r--r--comm/mailnews/base/public/nsMsgFolderFlags.idl117
-rw-r--r--comm/mailnews/base/public/nsMsgGroupnameFlags.h48
-rw-r--r--comm/mailnews/base/public/nsMsgHeaderMasks.h53
-rw-r--r--comm/mailnews/base/public/nsMsgLocalFolderHdrs.h47
-rw-r--r--comm/mailnews/base/public/nsMsgMessageFlags.idl183
-rw-r--r--comm/mailnews/base/src/ABQueryUtils.jsm159
-rw-r--r--comm/mailnews/base/src/FolderLookupService.jsm163
-rw-r--r--comm/mailnews/base/src/FolderUtils.jsm364
-rw-r--r--comm/mailnews/base/src/HeaderReader.h305
-rw-r--r--comm/mailnews/base/src/JXON.jsm159
-rw-r--r--comm/mailnews/base/src/LineReader.h188
-rw-r--r--comm/mailnews/base/src/LineReader.jsm68
-rw-r--r--comm/mailnews/base/src/MailAuthenticator.jsm468
-rw-r--r--comm/mailnews/base/src/MailChannel.sys.mjs71
-rw-r--r--comm/mailnews/base/src/MailCryptoUtils.jsm76
-rw-r--r--comm/mailnews/base/src/MailNewsDLF.cpp84
-rw-r--r--comm/mailnews/base/src/MailNewsDLF.h37
-rw-r--r--comm/mailnews/base/src/MailNotificationManager.jsm478
-rw-r--r--comm/mailnews/base/src/MailNotificationService.jsm375
-rw-r--r--comm/mailnews/base/src/MailServices.jsm169
-rw-r--r--comm/mailnews/base/src/MailStringUtils.jsm102
-rw-r--r--comm/mailnews/base/src/MailnewsLoadContextInfo.cpp51
-rw-r--r--comm/mailnews/base/src/MailnewsLoadContextInfo.h32
-rw-r--r--comm/mailnews/base/src/MailnewsMigrator.jsm352
-rw-r--r--comm/mailnews/base/src/MsgAsyncPrompter.jsm621
-rw-r--r--comm/mailnews/base/src/MsgDBCacheManager.jsm185
-rw-r--r--comm/mailnews/base/src/MsgIncomingServer.jsm1268
-rw-r--r--comm/mailnews/base/src/MsgKeySet.jsm132
-rw-r--r--comm/mailnews/base/src/MsgProtocolInfo.sys.mjs53
-rw-r--r--comm/mailnews/base/src/OAuth2.jsm364
-rw-r--r--comm/mailnews/base/src/OAuth2Module.jsm203
-rw-r--r--comm/mailnews/base/src/OAuth2Providers.jsm259
-rw-r--r--comm/mailnews/base/src/TemplateUtils.jsm90
-rw-r--r--comm/mailnews/base/src/UrlListener.cpp22
-rw-r--r--comm/mailnews/base/src/UrlListener.h72
-rw-r--r--comm/mailnews/base/src/VirtualFolderWrapper.jsm257
-rw-r--r--comm/mailnews/base/src/WinUnreadBadge.jsm246
-rw-r--r--comm/mailnews/base/src/components.conf359
-rw-r--r--comm/mailnews/base/src/converterWorker.js533
-rw-r--r--comm/mailnews/base/src/hostnameUtils.jsm366
-rw-r--r--comm/mailnews/base/src/mailstoreConverter.jsm339
-rw-r--r--comm/mailnews/base/src/moz.build154
-rw-r--r--comm/mailnews/base/src/nsCidProtocolHandler.cpp49
-rw-r--r--comm/mailnews/base/src/nsCidProtocolHandler.h25
-rw-r--r--comm/mailnews/base/src/nsCopyMessageStreamListener.cpp106
-rw-r--r--comm/mailnews/base/src/nsCopyMessageStreamListener.h29
-rw-r--r--comm/mailnews/base/src/nsImapMoveCoalescer.cpp198
-rw-r--r--comm/mailnews/base/src/nsImapMoveCoalescer.h71
-rw-r--r--comm/mailnews/base/src/nsMailAuthModule.cpp85
-rw-r--r--comm/mailnews/base/src/nsMailAuthModule.h27
-rw-r--r--comm/mailnews/base/src/nsMailChannel.cpp139
-rw-r--r--comm/mailnews/base/src/nsMailChannel.h30
-rw-r--r--comm/mailnews/base/src/nsMailDirProvider.cpp160
-rw-r--r--comm/mailnews/base/src/nsMailDirProvider.h42
-rw-r--r--comm/mailnews/base/src/nsMailDirServiceDefs.h31
-rw-r--r--comm/mailnews/base/src/nsMessenger.cpp2446
-rw-r--r--comm/mailnews/base/src/nsMessenger.h118
-rw-r--r--comm/mailnews/base/src/nsMessengerBootstrap.cpp84
-rw-r--r--comm/mailnews/base/src/nsMessengerBootstrap.h30
-rw-r--r--comm/mailnews/base/src/nsMessengerOSXIntegration.h24
-rw-r--r--comm/mailnews/base/src/nsMessengerOSXIntegration.mm63
-rw-r--r--comm/mailnews/base/src/nsMessengerUnixIntegration.cpp24
-rw-r--r--comm/mailnews/base/src/nsMessengerUnixIntegration.h22
-rw-r--r--comm/mailnews/base/src/nsMessengerWinIntegration.cpp379
-rw-r--r--comm/mailnews/base/src/nsMessengerWinIntegration.h39
-rw-r--r--comm/mailnews/base/src/nsMsgAccount.cpp413
-rw-r--r--comm/mailnews/base/src/nsMsgAccount.h34
-rw-r--r--comm/mailnews/base/src/nsMsgAccountManager.cpp3546
-rw-r--r--comm/mailnews/base/src/nsMsgAccountManager.h211
-rw-r--r--comm/mailnews/base/src/nsMsgBiffManager.cpp341
-rw-r--r--comm/mailnews/base/src/nsMsgBiffManager.h52
-rw-r--r--comm/mailnews/base/src/nsMsgCompressIStream.cpp203
-rw-r--r--comm/mailnews/base/src/nsMsgCompressIStream.h33
-rw-r--r--comm/mailnews/base/src/nsMsgCompressOStream.cpp128
-rw-r--r--comm/mailnews/base/src/nsMsgCompressOStream.h26
-rw-r--r--comm/mailnews/base/src/nsMsgContentPolicy.cpp928
-rw-r--r--comm/mailnews/base/src/nsMsgContentPolicy.h95
-rw-r--r--comm/mailnews/base/src/nsMsgCopyService.cpp587
-rw-r--r--comm/mailnews/base/src/nsMsgCopyService.h91
-rw-r--r--comm/mailnews/base/src/nsMsgDBFolder.cpp5573
-rw-r--r--comm/mailnews/base/src/nsMsgDBFolder.h366
-rw-r--r--comm/mailnews/base/src/nsMsgDBView.cpp7411
-rw-r--r--comm/mailnews/base/src/nsMsgDBView.h558
-rw-r--r--comm/mailnews/base/src/nsMsgEnumerator.cpp138
-rw-r--r--comm/mailnews/base/src/nsMsgEnumerator.h45
-rw-r--r--comm/mailnews/base/src/nsMsgFileStream.cpp190
-rw-r--r--comm/mailnews/base/src/nsMsgFileStream.h35
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCache.cpp570
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCache.h60
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCompactor.cpp1391
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCompactor.h48
-rw-r--r--comm/mailnews/base/src/nsMsgFolderNotificationService.cpp174
-rw-r--r--comm/mailnews/base/src/nsMsgFolderNotificationService.h46
-rw-r--r--comm/mailnews/base/src/nsMsgGroupThread.cpp731
-rw-r--r--comm/mailnews/base/src/nsMsgGroupThread.h88
-rw-r--r--comm/mailnews/base/src/nsMsgGroupView.cpp941
-rw-r--r--comm/mailnews/base/src/nsMsgGroupView.h78
-rw-r--r--comm/mailnews/base/src/nsMsgI18N.cpp403
-rw-r--r--comm/mailnews/base/src/nsMsgI18N.h138
-rw-r--r--comm/mailnews/base/src/nsMsgIdentity.cpp645
-rw-r--r--comm/mailnews/base/src/nsMsgIdentity.h87
-rw-r--r--comm/mailnews/base/src/nsMsgIncomingServer.cpp2142
-rw-r--r--comm/mailnews/base/src/nsMsgIncomingServer.h103
-rw-r--r--comm/mailnews/base/src/nsMsgKeySet.cpp1412
-rw-r--r--comm/mailnews/base/src/nsMsgKeySet.h108
-rw-r--r--comm/mailnews/base/src/nsMsgLineBuffer.cpp351
-rw-r--r--comm/mailnews/base/src/nsMsgLineBuffer.h123
-rw-r--r--comm/mailnews/base/src/nsMsgMailNewsUrl.cpp1070
-rw-r--r--comm/mailnews/base/src/nsMsgMailNewsUrl.h142
-rw-r--r--comm/mailnews/base/src/nsMsgMailSession.cpp671
-rw-r--r--comm/mailnews/base/src/nsMsgMailSession.h110
-rw-r--r--comm/mailnews/base/src/nsMsgOfflineManager.cpp352
-rw-r--r--comm/mailnews/base/src/nsMsgOfflineManager.h79
-rw-r--r--comm/mailnews/base/src/nsMsgProgress.cpp250
-rw-r--r--comm/mailnews/base/src/nsMsgProgress.h45
-rw-r--r--comm/mailnews/base/src/nsMsgProtocol.cpp1512
-rw-r--r--comm/mailnews/base/src/nsMsgProtocol.h263
-rw-r--r--comm/mailnews/base/src/nsMsgPurgeService.cpp496
-rw-r--r--comm/mailnews/base/src/nsMsgPurgeService.h51
-rw-r--r--comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp806
-rw-r--r--comm/mailnews/base/src/nsMsgQuickSearchDBView.h99
-rw-r--r--comm/mailnews/base/src/nsMsgReadStateTxn.cpp43
-rw-r--r--comm/mailnews/base/src/nsMsgReadStateTxn.h44
-rw-r--r--comm/mailnews/base/src/nsMsgSearchDBView.cpp1344
-rw-r--r--comm/mailnews/base/src/nsMsgSearchDBView.h182
-rw-r--r--comm/mailnews/base/src/nsMsgSpecialViews.cpp163
-rw-r--r--comm/mailnews/base/src/nsMsgSpecialViews.h82
-rw-r--r--comm/mailnews/base/src/nsMsgStatusFeedback.cpp261
-rw-r--r--comm/mailnews/base/src/nsMsgStatusFeedback.h48
-rw-r--r--comm/mailnews/base/src/nsMsgTagService.cpp458
-rw-r--r--comm/mailnews/base/src/nsMsgTagService.h50
-rw-r--r--comm/mailnews/base/src/nsMsgThreadedDBView.cpp899
-rw-r--r--comm/mailnews/base/src/nsMsgThreadedDBView.h62
-rw-r--r--comm/mailnews/base/src/nsMsgTxn.cpp247
-rw-r--r--comm/mailnews/base/src/nsMsgTxn.h77
-rw-r--r--comm/mailnews/base/src/nsMsgUtils.cpp1926
-rw-r--r--comm/mailnews/base/src/nsMsgUtils.h462
-rw-r--r--comm/mailnews/base/src/nsMsgWindow.cpp327
-rw-r--r--comm/mailnews/base/src/nsMsgWindow.h52
-rw-r--r--comm/mailnews/base/src/nsMsgXFViewThread.cpp444
-rw-r--r--comm/mailnews/base/src/nsMsgXFViewThread.h51
-rw-r--r--comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp514
-rw-r--r--comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h70
-rw-r--r--comm/mailnews/base/src/nsNewMailnewsURI.cpp155
-rw-r--r--comm/mailnews/base/src/nsNewMailnewsURI.h15
-rw-r--r--comm/mailnews/base/src/nsQuarantinedOutputStream.cpp234
-rw-r--r--comm/mailnews/base/src/nsQuarantinedOutputStream.h72
-rw-r--r--comm/mailnews/base/src/nsSpamSettings.cpp806
-rw-r--r--comm/mailnews/base/src/nsSpamSettings.h68
-rw-r--r--comm/mailnews/base/src/nsStatusBarBiffManager.cpp254
-rw-r--r--comm/mailnews/base/src/nsStatusBarBiffManager.h37
-rw-r--r--comm/mailnews/base/src/nsStopwatch.cpp164
-rw-r--r--comm/mailnews/base/src/nsStopwatch.h51
-rw-r--r--comm/mailnews/base/src/nsSubscribableServer.cpp867
-rw-r--r--comm/mailnews/base/src/nsSubscribableServer.h59
-rw-r--r--comm/mailnews/base/src/nsUserInfo.h23
-rw-r--r--comm/mailnews/base/src/nsUserInfoMac.mm70
-rw-r--r--comm/mailnews/base/src/nsUserInfoUnix.cpp124
-rw-r--r--comm/mailnews/base/src/nsUserInfoWin.cpp99
-rw-r--r--comm/mailnews/base/test/.eslintrc.js5
-rw-r--r--comm/mailnews/base/test/TestMsgStripRE.cpp91
-rw-r--r--comm/mailnews/base/test/gtest/TestHeaderReader.cpp205
-rw-r--r--comm/mailnews/base/test/gtest/TestLineReader.cpp200
-rw-r--r--comm/mailnews/base/test/gtest/moz.build15
-rw-r--r--comm/mailnews/base/test/moz.build19
-rw-r--r--comm/mailnews/base/test/unit/data/folderCache.json206
-rw-r--r--comm/mailnews/base/test/unit/data/panacea.dat70
-rw-r--r--comm/mailnews/base/test/unit/data/panacea_empty.dat1
-rw-r--r--comm/mailnews/base/test/unit/data/remoteContent.sql41
-rw-r--r--comm/mailnews/base/test/unit/head_mailbase.js23
-rw-r--r--comm/mailnews/base/test/unit/nodelist_test.xml7
-rw-r--r--comm/mailnews/base/test/unit/test_MsgIncomingServer.js224
-rw-r--r--comm/mailnews/base/test/unit/test_MsgKeySet.js85
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgr.js102
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgr2.js199
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js94
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js43
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js55
-rw-r--r--comm/mailnews/base/test/unit/test_accountMigration.js184
-rw-r--r--comm/mailnews/base/test/unit/test_acctRepair.js56
-rw-r--r--comm/mailnews/base/test/unit/test_bccInDatabase.js54
-rw-r--r--comm/mailnews/base/test/unit/test_bug428427.js249
-rw-r--r--comm/mailnews/base/test/unit/test_bug434810.js27
-rw-r--r--comm/mailnews/base/test/unit/test_bug471682.js118
-rw-r--r--comm/mailnews/base/test/unit/test_bug514945.js40
-rw-r--r--comm/mailnews/base/test/unit/test_closedDB.js104
-rw-r--r--comm/mailnews/base/test/unit/test_compactFailure.js134
-rw-r--r--comm/mailnews/base/test/unit/test_converterDeferredAccount.js206
-rw-r--r--comm/mailnews/base/test/unit/test_copyChaining.js109
-rw-r--r--comm/mailnews/base/test/unit/test_copyToInvalidDB.js106
-rw-r--r--comm/mailnews/base/test/unit/test_detachToFile.js159
-rw-r--r--comm/mailnews/base/test/unit/test_emptyTrash.js174
-rw-r--r--comm/mailnews/base/test/unit/test_fix_deferred_accounts.js59
-rw-r--r--comm/mailnews/base/test/unit/test_folderCompact.js335
-rw-r--r--comm/mailnews/base/test/unit/test_folderCompact2.js300
-rw-r--r--comm/mailnews/base/test/unit/test_folderLookupService.js83
-rw-r--r--comm/mailnews/base/test/unit/test_folderStringProperties.js41
-rw-r--r--comm/mailnews/base/test/unit/test_formatFileSize.js144
-rw-r--r--comm/mailnews/base/test/unit/test_getMsgTextFromStream.js88
-rw-r--r--comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js58
-rw-r--r--comm/mailnews/base/test/unit/test_hostnameUtils.js276
-rw-r--r--comm/mailnews/base/test/unit/test_identity.js69
-rw-r--r--comm/mailnews/base/test/unit/test_imapPump.js81
-rw-r--r--comm/mailnews/base/test/unit/test_incomingServer.js99
-rw-r--r--comm/mailnews/base/test/unit/test_inheritedFolderProperties.js183
-rw-r--r--comm/mailnews/base/test/unit/test_junkingWhenDisabled.js176
-rw-r--r--comm/mailnews/base/test/unit/test_loadVirtualFolders.js64
-rw-r--r--comm/mailnews/base/test/unit/test_mailServices.js67
-rw-r--r--comm/mailnews/base/test/unit/test_mailstoreConverter.js376
-rw-r--r--comm/mailnews/base/test/unit/test_mimemaltdetach.js160
-rw-r--r--comm/mailnews/base/test/unit/test_newMailNotification.js203
-rw-r--r--comm/mailnews/base/test/unit/test_nsIFolderListener.js45
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js68
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolder.js102
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js228
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js214
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js444
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgTagService.js113
-rw-r--r--comm/mailnews/base/test/unit/test_nsMailDirProvider.js23
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgDBView.js1212
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js110
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js217
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js161
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgTraitService.js130
-rw-r--r--comm/mailnews/base/test/unit/test_postPluginFilters.js223
-rw-r--r--comm/mailnews/base/test/unit/test_retention.js66
-rw-r--r--comm/mailnews/base/test/unit/test_saveAs.js172
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_base64.js22
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js58
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js92
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js150
-rw-r--r--comm/mailnews/base/test/unit/test_viewSortByAddresses.js144
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders1.js205
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders2.js90
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders3.js229
-rw-r--r--comm/mailnews/base/test/unit/xpcshell-imap.ini8
-rw-r--r--comm/mailnews/base/test/unit/xpcshell.ini77
-rw-r--r--comm/mailnews/compose/.eslintrc.js5
-rw-r--r--comm/mailnews/compose/content/sendProgress.js174
-rw-r--r--comm/mailnews/compose/content/sendProgress.xhtml65
-rw-r--r--comm/mailnews/compose/moz.build11
-rw-r--r--comm/mailnews/compose/public/moz.build30
-rw-r--r--comm/mailnews/compose/public/nsIMsgAttachment.idl144
-rw-r--r--comm/mailnews/compose/public/nsIMsgCompFields.idl104
-rw-r--r--comm/mailnews/compose/public/nsIMsgCompUtils.idl43
-rw-r--r--comm/mailnews/compose/public/nsIMsgCompose.idl305
-rw-r--r--comm/mailnews/compose/public/nsIMsgComposeParams.idl87
-rw-r--r--comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl16
-rw-r--r--comm/mailnews/compose/public/nsIMsgComposeSecure.idl145
-rw-r--r--comm/mailnews/compose/public/nsIMsgComposeService.idl169
-rw-r--r--comm/mailnews/compose/public/nsIMsgCopy.idl38
-rw-r--r--comm/mailnews/compose/public/nsIMsgQuote.idl35
-rw-r--r--comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl16
-rw-r--r--comm/mailnews/compose/public/nsIMsgSend.idl374
-rw-r--r--comm/mailnews/compose/public/nsIMsgSendLater.idl66
-rw-r--r--comm/mailnews/compose/public/nsIMsgSendLaterListener.idl86
-rw-r--r--comm/mailnews/compose/public/nsIMsgSendListener.idl79
-rw-r--r--comm/mailnews/compose/public/nsIMsgSendReport.idl47
-rw-r--r--comm/mailnews/compose/public/nsISmtpServer.idl151
-rw-r--r--comm/mailnews/compose/public/nsISmtpService.idl134
-rw-r--r--comm/mailnews/compose/public/nsISmtpUrl.idl115
-rw-r--r--comm/mailnews/compose/src/MailtoProtocolHandler.jsm38
-rw-r--r--comm/mailnews/compose/src/MessageSend.jsm1434
-rw-r--r--comm/mailnews/compose/src/MimeEncoder.jsm430
-rw-r--r--comm/mailnews/compose/src/MimeMessage.jsm625
-rw-r--r--comm/mailnews/compose/src/MimeMessageUtils.jsm1058
-rw-r--r--comm/mailnews/compose/src/MimePart.jsm378
-rw-r--r--comm/mailnews/compose/src/SMTPProtocolHandler.jsm39
-rw-r--r--comm/mailnews/compose/src/SmtpClient.jsm1344
-rw-r--r--comm/mailnews/compose/src/SmtpServer.jsm519
-rw-r--r--comm/mailnews/compose/src/SmtpService.jsm350
-rw-r--r--comm/mailnews/compose/src/components.conf197
-rw-r--r--comm/mailnews/compose/src/moz.build60
-rw-r--r--comm/mailnews/compose/src/nsComposeStrings.cpp106
-rw-r--r--comm/mailnews/compose/src/nsComposeStrings.h76
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachedFile.cpp179
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachedFile.h13
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachment.cpp263
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachment.h42
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachmentData.cpp103
-rw-r--r--comm/mailnews/compose/src/nsMsgAttachmentData.h131
-rw-r--r--comm/mailnews/compose/src/nsMsgCompFields.cpp559
-rw-r--r--comm/mailnews/compose/src/nsMsgCompFields.h212
-rw-r--r--comm/mailnews/compose/src/nsMsgCompUtils.cpp1164
-rw-r--r--comm/mailnews/compose/src/nsMsgCompUtils.h116
-rw-r--r--comm/mailnews/compose/src/nsMsgCompose.cpp5094
-rw-r--r--comm/mailnews/compose/src/nsMsgCompose.h245
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp119
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeContentHandler.h19
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeParams.cpp157
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeParams.h30
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp40
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeProgressParams.h19
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeService.cpp1411
-rw-r--r--comm/mailnews/compose/src/nsMsgComposeService.h68
-rw-r--r--comm/mailnews/compose/src/nsMsgCopy.cpp471
-rw-r--r--comm/mailnews/compose/src/nsMsgCopy.h108
-rw-r--r--comm/mailnews/compose/src/nsMsgPrompts.cpp94
-rw-r--r--comm/mailnews/compose/src/nsMsgPrompts.h28
-rw-r--r--comm/mailnews/compose/src/nsMsgQuote.cpp188
-rw-r--r--comm/mailnews/compose/src/nsMsgQuote.h51
-rw-r--r--comm/mailnews/compose/src/nsMsgSendLater.cpp1406
-rw-r--r--comm/mailnews/compose/src/nsMsgSendLater.h142
-rw-r--r--comm/mailnews/compose/src/nsMsgSendReport.cpp385
-rw-r--r--comm/mailnews/compose/src/nsMsgSendReport.h45
-rw-r--r--comm/mailnews/compose/src/nsSmtpUrl.cpp755
-rw-r--r--comm/mailnews/compose/src/nsSmtpUrl.h144
-rw-r--r--comm/mailnews/compose/test/moz.build8
-rw-r--r--comm/mailnews/compose/test/unit/data/429891_testcase.eml384
-rw-r--r--comm/mailnews/compose/test/unit/data/binary-after-plain.txtbin0 -> 1527 bytes
-rw-r--r--comm/mailnews/compose/test/unit/data/listexpansion.sql126
-rw-r--r--comm/mailnews/compose/test/unit/data/message1.eml7
-rw-r--r--comm/mailnews/compose/test/unit/data/shift-jis.eml13
-rw-r--r--comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt1
-rw-r--r--comm/mailnews/compose/test/unit/data/test-KOI8-R.txt2
-rw-r--r--comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt1
-rw-r--r--comm/mailnews/compose/test/unit/data/test-UTF-16BE.txtbin0 -> 60 bytes
-rw-r--r--comm/mailnews/compose/test/unit/data/test-UTF-16LE.txtbin0 -> 60 bytes
-rw-r--r--comm/mailnews/compose/test/unit/data/test-UTF-8.txt1
-rw-r--r--comm/mailnews/compose/test/unit/data/test-windows-1252.txt2
-rw-r--r--comm/mailnews/compose/test/unit/head_compose.js280
-rw-r--r--comm/mailnews/compose/test/unit/test_accountKey.js80
-rw-r--r--comm/mailnews/compose/test/unit/test_attachment.js171
-rw-r--r--comm/mailnews/compose/test/unit/test_attachment_intl.js42
-rw-r--r--comm/mailnews/compose/test/unit/test_autoReply.js254
-rw-r--r--comm/mailnews/compose/test/unit/test_bcc.js330
-rw-r--r--comm/mailnews/compose/test/unit/test_bug155172.js140
-rw-r--r--comm/mailnews/compose/test/unit/test_bug474774.js253
-rw-r--r--comm/mailnews/compose/test/unit/test_createAndSendMessage.js170
-rw-r--r--comm/mailnews/compose/test/unit/test_createRFC822Message.js68
-rw-r--r--comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js79
-rw-r--r--comm/mailnews/compose/test/unit/test_expandMailingLists.js115
-rw-r--r--comm/mailnews/compose/test/unit/test_fcc2.js47
-rw-r--r--comm/mailnews/compose/test/unit/test_fccReply.js140
-rw-r--r--comm/mailnews/compose/test/unit/test_longLines.js232
-rw-r--r--comm/mailnews/compose/test/unit/test_mailTelemetry.js79
-rw-r--r--comm/mailnews/compose/test/unit/test_mailtoURL.js810
-rw-r--r--comm/mailnews/compose/test/unit/test_messageBody.js206
-rw-r--r--comm/mailnews/compose/test/unit/test_messageHeaders.js812
-rw-r--r--comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js62
-rw-r--r--comm/mailnews/compose/test/unit/test_nsMsgCompose1.js137
-rw-r--r--comm/mailnews/compose/test/unit/test_nsMsgCompose2.js132
-rw-r--r--comm/mailnews/compose/test/unit/test_nsMsgCompose3.js92
-rw-r--r--comm/mailnews/compose/test/unit/test_nsSmtpService1.js127
-rw-r--r--comm/mailnews/compose/test/unit/test_saveDraft.js15
-rw-r--r--comm/mailnews/compose/test/unit/test_sendBackground.js223
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js231
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMailMessage.js189
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMessageFile.js172
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMessageLater.js261
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMessageLater2.js301
-rw-r--r--comm/mailnews/compose/test/unit/test_sendMessageLater3.js188
-rw-r--r--comm/mailnews/compose/test/unit/test_sendObserver.js52
-rw-r--r--comm/mailnews/compose/test/unit/test_smtp8bitMime.js105
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpAuthMethods.js166
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpClient.js136
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpPassword.js97
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpPassword2.js59
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js151
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js178
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js154
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpProtocols.js63
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpProxy.js49
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpServer.js104
-rw-r--r--comm/mailnews/compose/test/unit/test_smtpURL.js30
-rw-r--r--comm/mailnews/compose/test/unit/test_splitRecipients.js163
-rw-r--r--comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js57
-rw-r--r--comm/mailnews/compose/test/unit/test_telemetry_compose.js109
-rw-r--r--comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js123
-rw-r--r--comm/mailnews/compose/test/unit/xpcshell.ini54
-rw-r--r--comm/mailnews/db/gloda/.project11
-rw-r--r--comm/mailnews/db/gloda/components/GlodaAutoComplete.jsm576
-rw-r--r--comm/mailnews/db/gloda/components/MimeMessageEmitter.jsm501
-rw-r--r--comm/mailnews/db/gloda/components/components.conf25
-rw-r--r--comm/mailnews/db/gloda/components/moz.build13
-rw-r--r--comm/mailnews/db/gloda/content/autocomplete-richlistitem.js644
-rw-r--r--comm/mailnews/db/gloda/content/glodacomplete.js466
-rw-r--r--comm/mailnews/db/gloda/jar.mn8
-rw-r--r--comm/mailnews/db/gloda/modules/Collection.jsm834
-rw-r--r--comm/mailnews/db/gloda/modules/Everybody.jsm23
-rw-r--r--comm/mailnews/db/gloda/modules/Facet.jsm599
-rw-r--r--comm/mailnews/db/gloda/modules/Gloda.jsm2275
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaConstants.jsm250
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaContent.jsm285
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaDataModel.jsm1020
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaDatabind.jsm210
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaDatastore.jsm4402
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaExplicitAttr.jsm188
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaFundAttr.jsm947
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaIndexer.jsm1491
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaMsgIndexer.jsm310
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaMsgSearcher.jsm361
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaPublic.jsm45
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaQueryClassFactory.jsm642
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaSyntheticView.jsm175
-rw-r--r--comm/mailnews/db/gloda/modules/GlodaUtils.jsm84
-rw-r--r--comm/mailnews/db/gloda/modules/IndexMsg.jsm3464
-rw-r--r--comm/mailnews/db/gloda/modules/MimeMessage.jsm821
-rw-r--r--comm/mailnews/db/gloda/modules/NounFreetag.jsm91
-rw-r--r--comm/mailnews/db/gloda/modules/NounMimetype.jsm582
-rw-r--r--comm/mailnews/db/gloda/modules/NounTag.jsm97
-rw-r--r--comm/mailnews/db/gloda/modules/SuffixTree.jsm381
-rw-r--r--comm/mailnews/db/gloda/modules/moz.build31
-rw-r--r--comm/mailnews/db/gloda/moz.build13
-rw-r--r--comm/mailnews/db/gloda/test/moz.build12
-rw-r--r--comm/mailnews/db/gloda/test/unit/base_gloda_content.js226
-rw-r--r--comm/mailnews/db/gloda/test/unit/base_index_junk.js217
-rw-r--r--comm/mailnews/db/gloda/test/unit/base_index_messages.js1461
-rw-r--r--comm/mailnews/db/gloda/test/unit/base_query_messages.js729
-rw-r--r--comm/mailnews/db/gloda/test/unit/head_gloda.js19
-rw-r--r--comm/mailnews/db/gloda/test/unit/resources/GlodaQueryHelper.jsm431
-rw-r--r--comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelper.jsm847
-rw-r--r--comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm293
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_corrupt_database.js86
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_folder_logic.js60
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_fts3_tokenizer.js299
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_gloda_content_imap_offline.js34
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_gloda_content_local.js31
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_addressbook.js139
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_bad_messages.js210
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_compaction.js395
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_junk_imap_offline.js49
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_junk_imap_online.js36
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_junk_local.js33
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_messages_imap_offline.js38
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online.js36
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online_to_offline.js42
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_messages_local.js133
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_index_sweep_folder.js265
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_intl.js355
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_migration.js151
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_mime_attachments_size.js445
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_mime_emitter.js746
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_msg_search.js155
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_noun_mimetype.js144
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_nuke_migration.js62
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_nuke_migration_from_future.js12
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_query_core.js658
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_query_messages_imap_offline.js37
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online.js38
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online_to_offline.js40
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_query_messages_local.js33
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_smime_mimemsg_representation.js894
-rw-r--r--comm/mailnews/db/gloda/test/unit/test_startup_offline.js53
-rw-r--r--comm/mailnews/db/gloda/test/unit/xpcshell.ini38
-rw-r--r--comm/mailnews/db/mork/components.conf12
-rw-r--r--comm/mailnews/db/mork/mdb.h2550
-rw-r--r--comm/mailnews/db/mork/mork.h255
-rw-r--r--comm/mailnews/db/mork/morkArray.cpp250
-rw-r--r--comm/mailnews/db/mork/morkArray.h97
-rw-r--r--comm/mailnews/db/mork/morkAtom.cpp432
-rw-r--r--comm/mailnews/db/mork/morkAtom.h362
-rw-r--r--comm/mailnews/db/mork/morkAtomMap.cpp378
-rw-r--r--comm/mailnews/db/mork/morkAtomMap.h394
-rw-r--r--comm/mailnews/db/mork/morkAtomSpace.cpp233
-rw-r--r--comm/mailnews/db/mork/morkAtomSpace.h227
-rw-r--r--comm/mailnews/db/mork/morkBead.cpp361
-rw-r--r--comm/mailnews/db/mork/morkBead.h244
-rw-r--r--comm/mailnews/db/mork/morkBlob.cpp96
-rw-r--r--comm/mailnews/db/mork/morkBlob.h140
-rw-r--r--comm/mailnews/db/mork/morkBuilder.cpp892
-rw-r--r--comm/mailnews/db/mork/morkBuilder.h303
-rw-r--r--comm/mailnews/db/mork/morkCell.cpp99
-rw-r--r--comm/mailnews/db/mork/morkCell.h91
-rw-r--r--comm/mailnews/db/mork/morkCellObject.cpp453
-rw-r--r--comm/mailnews/db/mork/morkCellObject.h180
-rw-r--r--comm/mailnews/db/mork/morkCh.cpp344
-rw-r--r--comm/mailnews/db/mork/morkCh.h125
-rw-r--r--comm/mailnews/db/mork/morkConfig.cpp173
-rw-r--r--comm/mailnews/db/mork/morkConfig.h170
-rw-r--r--comm/mailnews/db/mork/morkCursor.cpp173
-rw-r--r--comm/mailnews/db/mork/morkCursor.h134
-rw-r--r--comm/mailnews/db/mork/morkDeque.cpp246
-rw-r--r--comm/mailnews/db/mork/morkDeque.h244
-rw-r--r--comm/mailnews/db/mork/morkEnv.cpp519
-rw-r--r--comm/mailnews/db/mork/morkEnv.h221
-rw-r--r--comm/mailnews/db/mork/morkFactory.cpp521
-rw-r--r--comm/mailnews/db/mork/morkFactory.h214
-rw-r--r--comm/mailnews/db/mork/morkFile.cpp738
-rw-r--r--comm/mailnews/db/mork/morkFile.h360
-rw-r--r--comm/mailnews/db/mork/morkHandle.cpp357
-rw-r--r--comm/mailnews/db/mork/morkHandle.h183
-rw-r--r--comm/mailnews/db/mork/morkIntMap.cpp212
-rw-r--r--comm/mailnews/db/mork/morkIntMap.h144
-rw-r--r--comm/mailnews/db/mork/morkMap.cpp852
-rw-r--r--comm/mailnews/db/mork/morkMap.h379
-rw-r--r--comm/mailnews/db/mork/morkNode.cpp550
-rw-r--r--comm/mailnews/db/mork/morkNode.h290
-rw-r--r--comm/mailnews/db/mork/morkNodeMap.cpp139
-rw-r--r--comm/mailnews/db/mork/morkNodeMap.h101
-rw-r--r--comm/mailnews/db/mork/morkObject.cpp176
-rw-r--r--comm/mailnews/db/mork/morkObject.h146
-rw-r--r--comm/mailnews/db/mork/morkParser.cpp1331
-rw-r--r--comm/mailnews/db/mork/morkParser.h547
-rw-r--r--comm/mailnews/db/mork/morkPool.cpp483
-rw-r--r--comm/mailnews/db/mork/morkPool.h162
-rw-r--r--comm/mailnews/db/mork/morkPortTableCursor.cpp381
-rw-r--r--comm/mailnews/db/mork/morkPortTableCursor.h142
-rw-r--r--comm/mailnews/db/mork/morkProbeMap.cpp1107
-rw-r--r--comm/mailnews/db/mork/morkProbeMap.h423
-rw-r--r--comm/mailnews/db/mork/morkQuickSort.cpp182
-rw-r--r--comm/mailnews/db/mork/morkQuickSort.h24
-rw-r--r--comm/mailnews/db/mork/morkRow.cpp769
-rw-r--r--comm/mailnews/db/mork/morkRow.h208
-rw-r--r--comm/mailnews/db/mork/morkRowCellCursor.cpp220
-rw-r--r--comm/mailnews/db/mork/morkRowCellCursor.h118
-rw-r--r--comm/mailnews/db/mork/morkRowMap.cpp250
-rw-r--r--comm/mailnews/db/mork/morkRowMap.h228
-rw-r--r--comm/mailnews/db/mork/morkRowObject.cpp530
-rw-r--r--comm/mailnews/db/mork/morkRowObject.h204
-rw-r--r--comm/mailnews/db/mork/morkRowSpace.cpp540
-rw-r--r--comm/mailnews/db/mork/morkRowSpace.h243
-rw-r--r--comm/mailnews/db/mork/morkSearchRowCursor.cpp153
-rw-r--r--comm/mailnews/db/mork/morkSearchRowCursor.h100
-rw-r--r--comm/mailnews/db/mork/morkSink.cpp247
-rw-r--r--comm/mailnews/db/mork/morkSink.h155
-rw-r--r--comm/mailnews/db/mork/morkSpace.cpp136
-rw-r--r--comm/mailnews/db/mork/morkSpace.h108
-rw-r--r--comm/mailnews/db/mork/morkStore.cpp1981
-rw-r--r--comm/mailnews/db/mork/morkStore.h770
-rw-r--r--comm/mailnews/db/mork/morkStream.cpp790
-rw-r--r--comm/mailnews/db/mork/morkStream.h258
-rw-r--r--comm/mailnews/db/mork/morkTable.cpp1415
-rw-r--r--comm/mailnews/db/mork/morkTable.h742
-rw-r--r--comm/mailnews/db/mork/morkTableRowCursor.cpp410
-rw-r--r--comm/mailnews/db/mork/morkTableRowCursor.h150
-rw-r--r--comm/mailnews/db/mork/morkThumb.cpp455
-rw-r--r--comm/mailnews/db/mork/morkThumb.h176
-rw-r--r--comm/mailnews/db/mork/morkUniqRowCursor.h89
-rw-r--r--comm/mailnews/db/mork/morkWriter.cpp1936
-rw-r--r--comm/mailnews/db/mork/morkWriter.h340
-rw-r--r--comm/mailnews/db/mork/morkYarn.cpp70
-rw-r--r--comm/mailnews/db/mork/morkYarn.h75
-rw-r--r--comm/mailnews/db/mork/morkZone.cpp487
-rw-r--r--comm/mailnews/db/mork/morkZone.h313
-rw-r--r--comm/mailnews/db/mork/moz.build68
-rw-r--r--comm/mailnews/db/mork/nsIMdbFactoryFactory.h33
-rw-r--r--comm/mailnews/db/mork/nsMorkFactory.cpp14
-rw-r--r--comm/mailnews/db/mork/nsMorkFactory.h27
-rw-r--r--comm/mailnews/db/mork/orkinHeap.cpp72
-rw-r--r--comm/mailnews/db/mork/orkinHeap.h50
-rw-r--r--comm/mailnews/db/moz.build9
-rw-r--r--comm/mailnews/db/msgdb/.eslintrc.js5
-rw-r--r--comm/mailnews/db/msgdb/moz.build11
-rw-r--r--comm/mailnews/db/msgdb/public/moz.build25
-rw-r--r--comm/mailnews/db/msgdb/public/nsDBFolderInfo.h151
-rw-r--r--comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl42
-rw-r--r--comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl117
-rw-r--r--comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl94
-rw-r--r--comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl506
-rw-r--r--comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl50
-rw-r--r--comm/mailnews/db/msgdb/public/nsINewsDatabase.idl18
-rw-r--r--comm/mailnews/db/msgdb/public/nsImapMailDatabase.h46
-rw-r--r--comm/mailnews/db/msgdb/public/nsMailDatabase.h62
-rw-r--r--comm/mailnews/db/msgdb/public/nsMsgDatabase.h447
-rw-r--r--comm/mailnews/db/msgdb/public/nsMsgHdr.h92
-rw-r--r--comm/mailnews/db/msgdb/public/nsMsgThread.h65
-rw-r--r--comm/mailnews/db/msgdb/public/nsNewsDatabase.h57
-rw-r--r--comm/mailnews/db/msgdb/src/components.conf44
-rw-r--r--comm/mailnews/db/msgdb/src/moz.build22
-rw-r--r--comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp749
-rw-r--r--comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp217
-rw-r--r--comm/mailnews/db/msgdb/src/nsMailDatabase.cpp380
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp4730
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp317
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h133
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgHdr.cpp936
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp385
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h52
-rw-r--r--comm/mailnews/db/msgdb/src/nsMsgThread.cpp1050
-rw-r--r--comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp307
-rw-r--r--comm/mailnews/db/msgdb/test/moz.build6
-rw-r--r--comm/mailnews/db/msgdb/test/unit/head_maildb.js21
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js56
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js100
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js38
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_maildb.js67
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js66
-rw-r--r--comm/mailnews/db/msgdb/test/unit/test_references_parsing.js124
-rw-r--r--comm/mailnews/db/msgdb/test/unit/xpcshell.ini10
-rw-r--r--comm/mailnews/export/content/exportDialog.js154
-rw-r--r--comm/mailnews/export/content/exportDialog.xhtml49
-rw-r--r--comm/mailnews/export/modules/ProfileExporter.jsm114
-rw-r--r--comm/mailnews/export/modules/moz.build8
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/components.conf15
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/moz.build16
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp2548
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.h397
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/nsIncompleteGamma.h239
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/head_bayes.js28
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases.datbin0 -> 446 bytes
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases1.eml6
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases2.eml6
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases3.eml6
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham1.eml7
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham2.eml8
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/msgCorpus.datbin0 -> 2447 bytes
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam1.eml7
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam2.eml8
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam3.eml7
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam4.eml8
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/tokenTest.eml14
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/trainingfile.js108
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_bug228675.js136
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_customTokenization.js197
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_junkAsTraits.js574
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_msgCorpus.js144
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traitAliases.js172
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traits.js287
-rw-r--r--comm/mailnews/extensions/bayesian-spam-filter/test/unit/xpcshell.ini11
-rw-r--r--comm/mailnews/extensions/fts3/Normalize.c2694
-rw-r--r--comm/mailnews/extensions/fts3/README.mozilla3
-rw-r--r--comm/mailnews/extensions/fts3/components.conf12
-rw-r--r--comm/mailnews/extensions/fts3/data/README5
-rw-r--r--comm/mailnews/extensions/fts3/data/generate_table.py269
-rw-r--r--comm/mailnews/extensions/fts3/data/nfkc.txt5786
-rw-r--r--comm/mailnews/extensions/fts3/data/nfkc_cf.txt5376
-rw-r--r--comm/mailnews/extensions/fts3/fts3_porter.c1140
-rw-r--r--comm/mailnews/extensions/fts3/fts3_tokenizer.h146
-rw-r--r--comm/mailnews/extensions/fts3/moz.build25
-rw-r--r--comm/mailnews/extensions/fts3/nsFts3Tokenizer.cpp59
-rw-r--r--comm/mailnews/extensions/fts3/nsFts3Tokenizer.h26
-rw-r--r--comm/mailnews/extensions/fts3/nsGlodaRankerFunction.cpp106
-rw-r--r--comm/mailnews/extensions/fts3/nsGlodaRankerFunction.h25
-rw-r--r--comm/mailnews/extensions/fts3/nsIFts3Tokenizer.idl15
-rw-r--r--comm/mailnews/extensions/mailviews/components.conf12
-rw-r--r--comm/mailnews/extensions/mailviews/mailViews.dat22
-rw-r--r--comm/mailnews/extensions/mailviews/moz.build25
-rw-r--r--comm/mailnews/extensions/mailviews/nsIMsgMailView.idl24
-rw-r--r--comm/mailnews/extensions/mailviews/nsIMsgMailViewList.idl28
-rw-r--r--comm/mailnews/extensions/mailviews/nsMsgMailViewList.cpp281
-rw-r--r--comm/mailnews/extensions/mailviews/nsMsgMailViewList.h51
-rw-r--r--comm/mailnews/extensions/mdn/MDNService.jsm24
-rw-r--r--comm/mailnews/extensions/mdn/am-mdn.js161
-rw-r--r--comm/mailnews/extensions/mdn/am-mdn.xhtml231
-rw-r--r--comm/mailnews/extensions/mdn/components.conf23
-rw-r--r--comm/mailnews/extensions/mdn/jar.mn7
-rw-r--r--comm/mailnews/extensions/mdn/mdn.js25
-rw-r--r--comm/mailnews/extensions/mdn/moz.build26
-rw-r--r--comm/mailnews/extensions/mdn/nsMsgMdnGenerator.cpp1040
-rw-r--r--comm/mailnews/extensions/mdn/nsMsgMdnGenerator.h86
-rw-r--r--comm/mailnews/extensions/mdn/test/unit/head_mdn.js18
-rw-r--r--comm/mailnews/extensions/mdn/test/unit/test_askuser.js67
-rw-r--r--comm/mailnews/extensions/mdn/test/unit/test_mdnFlags.js60
-rw-r--r--comm/mailnews/extensions/mdn/test/unit/xpcshell.ini6
-rw-r--r--comm/mailnews/extensions/moz.build15
-rw-r--r--comm/mailnews/extensions/newsblog/.eslintrc.js18
-rw-r--r--comm/mailnews/extensions/newsblog/Feed.jsm700
-rw-r--r--comm/mailnews/extensions/newsblog/FeedItem.jsm490
-rw-r--r--comm/mailnews/extensions/newsblog/FeedParser.jsm1496
-rw-r--r--comm/mailnews/extensions/newsblog/FeedUtils.jsm2136
-rw-r--r--comm/mailnews/extensions/newsblog/NewsBlog.jsm28
-rw-r--r--comm/mailnews/extensions/newsblog/am-newsblog.js128
-rw-r--r--comm/mailnews/extensions/newsblog/am-newsblog.xhtml233
-rw-r--r--comm/mailnews/extensions/newsblog/components.conf21
-rw-r--r--comm/mailnews/extensions/newsblog/feed-subscriptions.js3120
-rw-r--r--comm/mailnews/extensions/newsblog/feed-subscriptions.xhtml373
-rw-r--r--comm/mailnews/extensions/newsblog/feedAccountWizard.js56
-rw-r--r--comm/mailnews/extensions/newsblog/feedAccountWizard.xhtml95
-rw-r--r--comm/mailnews/extensions/newsblog/jar.mn13
-rw-r--r--comm/mailnews/extensions/newsblog/moz.build25
-rw-r--r--comm/mailnews/extensions/newsblog/newsblogOverlay.js416
-rw-r--r--comm/mailnews/extensions/newsblog/test/browser/browser.ini20
-rw-r--r--comm/mailnews/extensions/newsblog/test/browser/browser_feedDisplay.js228
-rw-r--r--comm/mailnews/extensions/newsblog/test/browser/data/article.html17
-rw-r--r--comm/mailnews/extensions/newsblog/test/browser/data/rss.xml27
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/head_feeds.js35
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/README.md24
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.json1
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.rdf6
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.json1
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.rdf17
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.json1
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.rdf6
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.json1
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.rdf12
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.json1
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.rdf6
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.json23
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.rdf21
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.json122
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.rdf126
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.json46
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.rdf32
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/rss2_example.xml25
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/rss2_guid.xml42
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1.rdf66
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1_BORKED.rdf66
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/test_feedparser.js146
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/test_rdfmigration.js61
-rw-r--r--comm/mailnews/extensions/newsblog/test/unit/xpcshell.ini8
-rw-r--r--comm/mailnews/extensions/offline-startup/OfflineStartup.jsm126
-rw-r--r--comm/mailnews/extensions/offline-startup/components.conf15
-rw-r--r--comm/mailnews/extensions/offline-startup/moz.build12
-rw-r--r--comm/mailnews/extensions/smime/certFetchingStatus.js255
-rw-r--r--comm/mailnews/extensions/smime/certFetchingStatus.xhtml40
-rw-r--r--comm/mailnews/extensions/smime/certpicker.js69
-rw-r--r--comm/mailnews/extensions/smime/certpicker.xhtml54
-rw-r--r--comm/mailnews/extensions/smime/components.conf63
-rw-r--r--comm/mailnews/extensions/smime/moz.build37
-rw-r--r--comm/mailnews/extensions/smime/msgCompSecurityInfo.js122
-rw-r--r--comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml69
-rw-r--r--comm/mailnews/extensions/smime/msgReadSMIMEOverlay.js251
-rw-r--r--comm/mailnews/extensions/smime/nsCMS.cpp1187
-rw-r--r--comm/mailnews/extensions/smime/nsCMS.h123
-rw-r--r--comm/mailnews/extensions/smime/nsCMSSecureMessage.cpp92
-rw-r--r--comm/mailnews/extensions/smime/nsCMSSecureMessage.h39
-rw-r--r--comm/mailnews/extensions/smime/nsCertPicker.cpp410
-rw-r--r--comm/mailnews/extensions/smime/nsCertPicker.h32
-rw-r--r--comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.cpp32
-rw-r--r--comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.h24
-rw-r--r--comm/mailnews/extensions/smime/nsICMSDecoder.idl29
-rw-r--r--comm/mailnews/extensions/smime/nsICMSDecoderJS.idl24
-rw-r--r--comm/mailnews/extensions/smime/nsICMSEncoder.idl29
-rw-r--r--comm/mailnews/extensions/smime/nsICMSMessage.idl96
-rw-r--r--comm/mailnews/extensions/smime/nsICMSMessageErrors.idl36
-rw-r--r--comm/mailnews/extensions/smime/nsICMSSecureMessage.idl30
-rw-r--r--comm/mailnews/extensions/smime/nsICertPickDialogs.idl27
-rw-r--r--comm/mailnews/extensions/smime/nsIEncryptedSMIMEURIsSrvc.idl24
-rw-r--r--comm/mailnews/extensions/smime/nsIMsgSMIMEHeaderSink.idl30
-rw-r--r--comm/mailnews/extensions/smime/nsIUserCertPicker.idl28
-rw-r--r--comm/mailnews/extensions/smime/nsMsgComposeSecure.cpp1277
-rw-r--r--comm/mailnews/extensions/smime/nsMsgComposeSecure.h103
-rw-r--r--comm/mailnews/imap/public/moz.build29
-rw-r--r--comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl23
-rw-r--r--comm/mailnews/imap/public/nsIAutoSyncManager.idl192
-rw-r--r--comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl35
-rw-r--r--comm/mailnews/imap/public/nsIAutoSyncState.idl157
-rw-r--r--comm/mailnews/imap/public/nsIImapFlagAndUidState.idl86
-rw-r--r--comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl22
-rw-r--r--comm/mailnews/imap/public/nsIImapHostSessionList.h129
-rw-r--r--comm/mailnews/imap/public/nsIImapIncomingServer.idl114
-rw-r--r--comm/mailnews/imap/public/nsIImapMailFolderSink.idl119
-rw-r--r--comm/mailnews/imap/public/nsIImapMessageSink.idl89
-rw-r--r--comm/mailnews/imap/public/nsIImapMockChannel.idl49
-rw-r--r--comm/mailnews/imap/public/nsIImapOfflineSync.idl19
-rw-r--r--comm/mailnews/imap/public/nsIImapProtocol.idl90
-rw-r--r--comm/mailnews/imap/public/nsIImapProtocolSink.idl33
-rw-r--r--comm/mailnews/imap/public/nsIImapServerSink.idl178
-rw-r--r--comm/mailnews/imap/public/nsIImapService.idl253
-rw-r--r--comm/mailnews/imap/public/nsIImapUrl.idl210
-rw-r--r--comm/mailnews/imap/public/nsIMailboxSpec.idl47
-rw-r--r--comm/mailnews/imap/public/nsIMsgImapMailFolder.idl253
-rw-r--r--comm/mailnews/imap/src/ImapChannel.jsm318
-rw-r--r--comm/mailnews/imap/src/ImapClient.jsm1895
-rw-r--r--comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs71
-rw-r--r--comm/mailnews/imap/src/ImapIncomingServer.jsm783
-rw-r--r--comm/mailnews/imap/src/ImapMessageService.jsm292
-rw-r--r--comm/mailnews/imap/src/ImapModuleLoader.jsm131
-rw-r--r--comm/mailnews/imap/src/ImapProtocolHandler.jsm40
-rw-r--r--comm/mailnews/imap/src/ImapProtocolInfo.jsm44
-rw-r--r--comm/mailnews/imap/src/ImapResponse.jsm479
-rw-r--r--comm/mailnews/imap/src/ImapService.jsm518
-rw-r--r--comm/mailnews/imap/src/ImapUtils.jsm197
-rw-r--r--comm/mailnews/imap/src/components.conf76
-rw-r--r--comm/mailnews/imap/src/moz.build56
-rw-r--r--comm/mailnews/imap/src/nsAutoSyncManager.cpp1372
-rw-r--r--comm/mailnews/imap/src/nsAutoSyncManager.h265
-rw-r--r--comm/mailnews/imap/src/nsAutoSyncState.cpp782
-rw-r--r--comm/mailnews/imap/src/nsAutoSyncState.h106
-rw-r--r--comm/mailnews/imap/src/nsImapBodyShell.cpp1060
-rw-r--r--comm/mailnews/imap/src/nsImapBodyShell.h357
-rw-r--r--comm/mailnews/imap/src/nsImapCore.h191
-rw-r--r--comm/mailnews/imap/src/nsImapFlagAndUidState.cpp315
-rw-r--r--comm/mailnews/imap/src/nsImapFlagAndUidState.h56
-rw-r--r--comm/mailnews/imap/src/nsImapGenericParser.cpp407
-rw-r--r--comm/mailnews/imap/src/nsImapGenericParser.h74
-rw-r--r--comm/mailnews/imap/src/nsImapHostSessionList.cpp595
-rw-r--r--comm/mailnews/imap/src/nsImapHostSessionList.h169
-rw-r--r--comm/mailnews/imap/src/nsImapIncomingServer.cpp3032
-rw-r--r--comm/mailnews/imap/src/nsImapIncomingServer.h150
-rw-r--r--comm/mailnews/imap/src/nsImapMailFolder.cpp9095
-rw-r--r--comm/mailnews/imap/src/nsImapMailFolder.h599
-rw-r--r--comm/mailnews/imap/src/nsImapNamespace.cpp513
-rw-r--r--comm/mailnews/imap/src/nsImapNamespace.h87
-rw-r--r--comm/mailnews/imap/src/nsImapOfflineSync.cpp1175
-rw-r--r--comm/mailnews/imap/src/nsImapOfflineSync.h95
-rw-r--r--comm/mailnews/imap/src/nsImapProtocol.cpp9915
-rw-r--r--comm/mailnews/imap/src/nsImapProtocol.h848
-rw-r--r--comm/mailnews/imap/src/nsImapSearchResults.cpp72
-rw-r--r--comm/mailnews/imap/src/nsImapSearchResults.h40
-rw-r--r--comm/mailnews/imap/src/nsImapServerResponseParser.cpp2640
-rw-r--r--comm/mailnews/imap/src/nsImapServerResponseParser.h275
-rw-r--r--comm/mailnews/imap/src/nsImapService.cpp3091
-rw-r--r--comm/mailnews/imap/src/nsImapService.h122
-rw-r--r--comm/mailnews/imap/src/nsImapStringBundle.cpp37
-rw-r--r--comm/mailnews/imap/src/nsImapStringBundle.h17
-rw-r--r--comm/mailnews/imap/src/nsImapUndoTxn.cpp647
-rw-r--r--comm/mailnews/imap/src/nsImapUndoTxn.h87
-rw-r--r--comm/mailnews/imap/src/nsImapUrl.cpp1276
-rw-r--r--comm/mailnews/imap/src/nsImapUrl.h132
-rw-r--r--comm/mailnews/imap/src/nsImapUtils.cpp336
-rw-r--r--comm/mailnews/imap/src/nsImapUtils.h77
-rw-r--r--comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp596
-rw-r--r--comm/mailnews/imap/src/nsSyncRunnableHelpers.h149
-rw-r--r--comm/mailnews/imap/test/TestImapFlagAndUidState.cpp166
-rw-r--r--comm/mailnews/imap/test/TestImapHdrXferInfo.cpp101
-rw-r--r--comm/mailnews/imap/test/moz.build27
-rw-r--r--comm/mailnews/imap/test/unit/head_imap_maildir.js9
-rw-r--r--comm/mailnews/imap/test/unit/head_server.js201
-rw-r--r--comm/mailnews/imap/test/unit/test_ImapResponse.js288
-rw-r--r--comm/mailnews/imap/test/unit/test_autosync_date_constraints.js88
-rw-r--r--comm/mailnews/imap/test/unit/test_bccProperty.js52
-rw-r--r--comm/mailnews/imap/test/unit/test_bug460636.js82
-rw-r--r--comm/mailnews/imap/test/unit/test_chunkLastLF.js108
-rw-r--r--comm/mailnews/imap/test/unit/test_compactOfflineStore.js194
-rw-r--r--comm/mailnews/imap/test/unit/test_converterImap.js110
-rw-r--r--comm/mailnews/imap/test/unit/test_copyThenMove.js200
-rw-r--r--comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js134
-rw-r--r--comm/mailnews/imap/test/unit/test_dontStatNoSelect.js149
-rw-r--r--comm/mailnews/imap/test/unit/test_downloadOffline.js75
-rw-r--r--comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js105
-rw-r--r--comm/mailnews/imap/test/unit/test_filterCustomHeaders.js66
-rw-r--r--comm/mailnews/imap/test/unit/test_filterNeedsBody.js113
-rw-r--r--comm/mailnews/imap/test/unit/test_folderOfflineFlags.js108
-rw-r--r--comm/mailnews/imap/test/unit/test_gmailAttributes.js91
-rw-r--r--comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js229
-rw-r--r--comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js199
-rw-r--r--comm/mailnews/imap/test/unit/test_imapAuthMethods.js165
-rw-r--r--comm/mailnews/imap/test/unit/test_imapAutoSync.js239
-rw-r--r--comm/mailnews/imap/test/unit/test_imapChunks.js114
-rw-r--r--comm/mailnews/imap/test/unit/test_imapClientid.js64
-rw-r--r--comm/mailnews/imap/test/unit/test_imapContentLength.js98
-rw-r--r--comm/mailnews/imap/test/unit/test_imapCopyTimeout.js120
-rw-r--r--comm/mailnews/imap/test/unit/test_imapFilterActions.js597
-rw-r--r--comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js428
-rw-r--r--comm/mailnews/imap/test/unit/test_imapFlagChange.js216
-rw-r--r--comm/mailnews/imap/test/unit/test_imapFolderCopy.js137
-rw-r--r--comm/mailnews/imap/test/unit/test_imapHdrChunking.js168
-rw-r--r--comm/mailnews/imap/test/unit/test_imapHdrStreaming.js74
-rw-r--r--comm/mailnews/imap/test/unit/test_imapHighWater.js194
-rw-r--r--comm/mailnews/imap/test/unit/test_imapID.js40
-rw-r--r--comm/mailnews/imap/test/unit/test_imapMove.js88
-rw-r--r--comm/mailnews/imap/test/unit/test_imapPasswordFailure.js179
-rw-r--r--comm/mailnews/imap/test/unit/test_imapProtocols.js59
-rw-r--r--comm/mailnews/imap/test/unit/test_imapProxy.js68
-rw-r--r--comm/mailnews/imap/test/unit/test_imapRename.js43
-rw-r--r--comm/mailnews/imap/test/unit/test_imapSearch.js348
-rw-r--r--comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js49
-rw-r--r--comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js221
-rw-r--r--comm/mailnews/imap/test/unit/test_imapUndo.js160
-rw-r--r--comm/mailnews/imap/test/unit/test_imapUrls.js31
-rw-r--r--comm/mailnews/imap/test/unit/test_largeOfflineStore.js141
-rw-r--r--comm/mailnews/imap/test/unit/test_listClosesDB.js58
-rw-r--r--comm/mailnews/imap/test/unit/test_listSubscribed.js123
-rw-r--r--comm/mailnews/imap/test/unit/test_localToImapFilter.js159
-rw-r--r--comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js121
-rw-r--r--comm/mailnews/imap/test/unit/test_lsub.js76
-rw-r--r--comm/mailnews/imap/test/unit/test_mailboxes.js80
-rw-r--r--comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js363
-rw-r--r--comm/mailnews/imap/test/unit/test_offlineCopy.js271
-rw-r--r--comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js152
-rw-r--r--comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js125
-rw-r--r--comm/mailnews/imap/test/unit/test_offlinePlayback.js187
-rw-r--r--comm/mailnews/imap/test/unit/test_offlineStoreLocking.js258
-rw-r--r--comm/mailnews/imap/test/unit/test_preserveDataOnMove.js90
-rw-r--r--comm/mailnews/imap/test/unit/test_saveImapDraft.js119
-rw-r--r--comm/mailnews/imap/test/unit/test_saveTemplate.js96
-rw-r--r--comm/mailnews/imap/test/unit/test_starttlsFailure.js79
-rw-r--r--comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js95
-rw-r--r--comm/mailnews/imap/test/unit/test_subfolderLocation.js83
-rw-r--r--comm/mailnews/imap/test/unit/test_syncChanges.js80
-rw-r--r--comm/mailnews/imap/test/unit/test_trustSpamAssassin.js146
-rw-r--r--comm/mailnews/imap/test/unit/xpcshell-cpp.ini14
-rw-r--r--comm/mailnews/imap/test/unit/xpcshell-shared.ini58
-rw-r--r--comm/mailnews/imap/test/unit/xpcshell.ini12
-rw-r--r--comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini14
-rw-r--r--comm/mailnews/imap/test/unit/xpcshell_maildir.ini9
-rw-r--r--comm/mailnews/import/build/moz.build28
-rw-r--r--comm/mailnews/import/content/aboutImport.js1511
-rw-r--r--comm/mailnews/import/content/aboutImport.xhtml477
-rw-r--r--comm/mailnews/import/content/csv-field-map.js280
-rw-r--r--comm/mailnews/import/content/fieldMapImport.js259
-rw-r--r--comm/mailnews/import/content/fieldMapImport.xhtml104
-rw-r--r--comm/mailnews/import/content/importDialog.js1184
-rw-r--r--comm/mailnews/import/content/importDialog.xhtml225
-rw-r--r--comm/mailnews/import/modules/AddrBookFileImporter.jsm356
-rw-r--r--comm/mailnews/import/modules/AppleMailProfileImporter.jsm92
-rw-r--r--comm/mailnews/import/modules/BaseProfileImporter.jsm100
-rw-r--r--comm/mailnews/import/modules/BeckyProfileImporter.jsm125
-rw-r--r--comm/mailnews/import/modules/CalendarFileImporter.jsm127
-rw-r--r--comm/mailnews/import/modules/OutlookProfileImporter.jsm143
-rw-r--r--comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm75
-rw-r--r--comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm1024
-rw-r--r--comm/mailnews/import/modules/moz.build22
-rw-r--r--comm/mailnews/import/public/moz.build20
-rw-r--r--comm/mailnews/import/public/nsIImportABDescriptor.idl70
-rw-r--r--comm/mailnews/import/public/nsIImportABFile.idl24
-rw-r--r--comm/mailnews/import/public/nsIImportAddressBooks.idl142
-rw-r--r--comm/mailnews/import/public/nsIImportFieldMap.idl71
-rw-r--r--comm/mailnews/import/public/nsIImportFilters.idl25
-rw-r--r--comm/mailnews/import/public/nsIImportGeneric.idl81
-rw-r--r--comm/mailnews/import/public/nsIImportMail.idl94
-rw-r--r--comm/mailnews/import/public/nsIImportMailboxDescriptor.idl44
-rw-r--r--comm/mailnews/import/public/nsIImportModule.idl30
-rw-r--r--comm/mailnews/import/public/nsIImportService.idl59
-rw-r--r--comm/mailnews/import/public/nsIImportSettings.idl33
-rw-r--r--comm/mailnews/import/src/ImportCharSet.cpp58
-rw-r--r--comm/mailnews/import/src/ImportCharSet.h201
-rw-r--r--comm/mailnews/import/src/ImportDebug.h25
-rw-r--r--comm/mailnews/import/src/ImportOutFile.cpp257
-rw-r--r--comm/mailnews/import/src/ImportOutFile.h94
-rw-r--r--comm/mailnews/import/src/ImportTranslate.cpp100
-rw-r--r--comm/mailnews/import/src/ImportTranslate.h23
-rw-r--r--comm/mailnews/import/src/MapiApi.cpp1842
-rw-r--r--comm/mailnews/import/src/MapiApi.h284
-rw-r--r--comm/mailnews/import/src/MapiDbgLog.h36
-rw-r--r--comm/mailnews/import/src/MapiMessage.cpp1383
-rw-r--r--comm/mailnews/import/src/MapiMessage.h290
-rw-r--r--comm/mailnews/import/src/MapiMimeTypes.cpp81
-rw-r--r--comm/mailnews/import/src/MapiMimeTypes.h27
-rw-r--r--comm/mailnews/import/src/MapiTagStrs.cpp1473
-rw-r--r--comm/mailnews/import/src/MorkImport.cpp343
-rw-r--r--comm/mailnews/import/src/MorkImport.h50
-rw-r--r--comm/mailnews/import/src/SeamonkeyImport.jsm253
-rw-r--r--comm/mailnews/import/src/ThunderbirdImport.jsm145
-rw-r--r--comm/mailnews/import/src/components.conf104
-rw-r--r--comm/mailnews/import/src/moz.build86
-rw-r--r--comm/mailnews/import/src/nsAddrDatabase.cpp864
-rw-r--r--comm/mailnews/import/src/nsAddrDatabase.h158
-rw-r--r--comm/mailnews/import/src/nsAppleMailImport.cpp609
-rw-r--r--comm/mailnews/import/src/nsAppleMailImport.h86
-rw-r--r--comm/mailnews/import/src/nsBeckyAddressBooks.cpp311
-rw-r--r--comm/mailnews/import/src/nsBeckyAddressBooks.h36
-rw-r--r--comm/mailnews/import/src/nsBeckyFilters.cpp711
-rw-r--r--comm/mailnews/import/src/nsBeckyFilters.h73
-rw-r--r--comm/mailnews/import/src/nsBeckyImport.cpp143
-rw-r--r--comm/mailnews/import/src/nsBeckyImport.h38
-rw-r--r--comm/mailnews/import/src/nsBeckyMail.cpp566
-rw-r--r--comm/mailnews/import/src/nsBeckyMail.h41
-rw-r--r--comm/mailnews/import/src/nsBeckySettings.cpp379
-rw-r--r--comm/mailnews/import/src/nsBeckySettings.h50
-rw-r--r--comm/mailnews/import/src/nsBeckyStringBundle.cpp55
-rw-r--r--comm/mailnews/import/src/nsBeckyStringBundle.h32
-rw-r--r--comm/mailnews/import/src/nsBeckyUtils.cpp302
-rw-r--r--comm/mailnews/import/src/nsBeckyUtils.h35
-rw-r--r--comm/mailnews/import/src/nsEmlxHelperUtils.h61
-rw-r--r--comm/mailnews/import/src/nsEmlxHelperUtils.mm230
-rw-r--r--comm/mailnews/import/src/nsImportABDescriptor.cpp19
-rw-r--r--comm/mailnews/import/src/nsImportABDescriptor.h100
-rw-r--r--comm/mailnews/import/src/nsImportAddressBooks.cpp571
-rw-r--r--comm/mailnews/import/src/nsImportAddressBooks.h81
-rw-r--r--comm/mailnews/import/src/nsImportEmbeddedImageData.cpp52
-rw-r--r--comm/mailnews/import/src/nsImportEmbeddedImageData.h31
-rw-r--r--comm/mailnews/import/src/nsImportEncodeScan.cpp334
-rw-r--r--comm/mailnews/import/src/nsImportEncodeScan.h39
-rw-r--r--comm/mailnews/import/src/nsImportFieldMap.cpp325
-rw-r--r--comm/mailnews/import/src/nsImportFieldMap.h44
-rw-r--r--comm/mailnews/import/src/nsImportMail.cpp1007
-rw-r--r--comm/mailnews/import/src/nsImportMail.h86
-rw-r--r--comm/mailnews/import/src/nsImportMailboxDescriptor.cpp25
-rw-r--r--comm/mailnews/import/src/nsImportMailboxDescriptor.h94
-rw-r--r--comm/mailnews/import/src/nsImportScanFile.cpp154
-rw-r--r--comm/mailnews/import/src/nsImportScanFile.h56
-rw-r--r--comm/mailnews/import/src/nsImportService.cpp293
-rw-r--r--comm/mailnews/import/src/nsImportService.h54
-rw-r--r--comm/mailnews/import/src/nsImportStringBundle.cpp67
-rw-r--r--comm/mailnews/import/src/nsImportStringBundle.h43
-rw-r--r--comm/mailnews/import/src/nsImportTranslator.cpp308
-rw-r--r--comm/mailnews/import/src/nsImportTranslator.h87
-rw-r--r--comm/mailnews/import/src/nsOutlookCompose.cpp669
-rw-r--r--comm/mailnews/import/src/nsOutlookCompose.h63
-rw-r--r--comm/mailnews/import/src/nsOutlookImport.cpp522
-rw-r--r--comm/mailnews/import/src/nsOutlookImport.h38
-rw-r--r--comm/mailnews/import/src/nsOutlookMail.cpp830
-rw-r--r--comm/mailnews/import/src/nsOutlookMail.h84
-rw-r--r--comm/mailnews/import/src/nsOutlookSettings.cpp500
-rw-r--r--comm/mailnews/import/src/nsOutlookSettings.h27
-rw-r--r--comm/mailnews/import/src/nsOutlookStringBundle.cpp53
-rw-r--r--comm/mailnews/import/src/nsOutlookStringBundle.h36
-rw-r--r--comm/mailnews/import/src/nsTextAddress.cpp426
-rw-r--r--comm/mailnews/import/src/nsTextAddress.h60
-rw-r--r--comm/mailnews/import/src/nsTextImport.cpp646
-rw-r--r--comm/mailnews/import/src/nsTextImport.h40
-rw-r--r--comm/mailnews/import/src/nsVCardAddress.cpp151
-rw-r--r--comm/mailnews/import/src/nsVCardAddress.h28
-rw-r--r--comm/mailnews/import/src/nsVCardImport.cpp352
-rw-r--r--comm/mailnews/import/src/nsVCardImport.h39
-rw-r--r--comm/mailnews/import/src/nsWMImport.cpp199
-rw-r--r--comm/mailnews/import/src/nsWMImport.h38
-rw-r--r--comm/mailnews/import/src/nsWMSettings.cpp679
-rw-r--r--comm/mailnews/import/src/nsWMSettings.h22
-rw-r--r--comm/mailnews/import/src/nsWMStringBundle.cpp52
-rw-r--r--comm/mailnews/import/src/nsWMStringBundle.h36
-rw-r--r--comm/mailnews/import/src/nsWMUtils.cpp153
-rw-r--r--comm/mailnews/import/src/nsWMUtils.h23
-rw-r--r--comm/mailnews/import/src/rtfDecoder.cpp561
-rw-r--r--comm/mailnews/import/src/rtfDecoder.h21
-rw-r--r--comm/mailnews/import/src/rtfMailDecoder.cpp71
-rw-r--r--comm/mailnews/import/src/rtfMailDecoder.h44
-rw-r--r--comm/mailnews/import/test/moz.build6
-rw-r--r--comm/mailnews/import/test/unit/head_import.js23
-rw-r--r--comm/mailnews/import/test/unit/resources/AB_README39
-rw-r--r--comm/mailnews/import/test/unit/resources/WindowsLiveMail/MicrosoftCommunities/account{2E23}.oeaccountbin0 -> 698 bytes
-rw-r--r--comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallimap/donhallimap{testimap}.oeaccountbin0 -> 4454 bytes
-rw-r--r--comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallnntp/donhallnntp{testnntp}.oeaccountbin0 -> 2728 bytes
-rw-r--r--comm/mailnews/import/test/unit/resources/WindowsLiveMail/news.mozilla.org/account{B3B3}.oeaccountbin0 -> 960 bytes
-rw-r--r--comm/mailnews/import/test/unit/resources/WindowsLiveMail/pop3.test.test/account{D244}.oeaccountbin0 -> 1478 bytes
-rw-r--r--comm/mailnews/import/test/unit/resources/addressbook.json170
-rw-r--r--comm/mailnews/import/test/unit/resources/basic_addressbook.csv2
-rw-r--r--comm/mailnews/import/test/unit/resources/basic_csv_addressbook.csv3
-rw-r--r--comm/mailnews/import/test/unit/resources/basic_ldif_addressbook.ldif45
-rw-r--r--comm/mailnews/import/test/unit/resources/basic_vcard_addressbook.vcf12
-rw-r--r--comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186e.bab26
-rw-r--r--comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186f.bab13
-rw-r--r--comm/mailnews/import/test/unit/resources/becky/addressbooks/do_not_import_this.nobab12
-rw-r--r--comm/mailnews/import/test/unit/resources/becky/filters/IFilter.def23
-rw-r--r--comm/mailnews/import/test/unit/resources/becky/filters/OFilter.def22
-rw-r--r--comm/mailnews/import/test/unit/resources/bug_263304.ldif7
-rw-r--r--comm/mailnews/import/test/unit/resources/csv_no_header.csv2
-rw-r--r--comm/mailnews/import/test/unit/resources/csv_semicolon.csv3
-rw-r--r--comm/mailnews/import/test/unit/resources/dos_vcard_addressbook.vcf6
-rw-r--r--comm/mailnews/import/test/unit/resources/emptylines_vcard_addressbook.vcf18
-rw-r--r--comm/mailnews/import/test/unit/resources/import_helper.js665
-rw-r--r--comm/mailnews/import/test/unit/resources/mock_windows_reg_factory.js84
-rw-r--r--comm/mailnews/import/test/unit/resources/quote.csv2
-rw-r--r--comm/mailnews/import/test/unit/resources/shiftjis_addressbook.csv2
-rw-r--r--comm/mailnews/import/test/unit/resources/tab_comma_mixed.csv4
-rw-r--r--comm/mailnews/import/test/unit/resources/utf16_addressbook.csvbin0 -> 1006 bytes
-rw-r--r--comm/mailnews/import/test/unit/test_AddrBookFileImporter.js130
-rw-r--r--comm/mailnews/import/test/unit/test_ThunderbirdProfileImporter.js288
-rw-r--r--comm/mailnews/import/test/unit/test_becky_addressbook.js59
-rw-r--r--comm/mailnews/import/test/unit/test_becky_filters.js41
-rw-r--r--comm/mailnews/import/test/unit/test_bug_263304.js25
-rw-r--r--comm/mailnews/import/test/unit/test_bug_437556.js23
-rw-r--r--comm/mailnews/import/test/unit/test_csv_GetSample.js15
-rw-r--r--comm/mailnews/import/test/unit/test_csv_import.js23
-rw-r--r--comm/mailnews/import/test/unit/test_csv_import_quote.js11
-rw-r--r--comm/mailnews/import/test/unit/test_ldif_import.js27
-rw-r--r--comm/mailnews/import/test/unit/test_outlook_settings.js158
-rw-r--r--comm/mailnews/import/test/unit/test_shiftjis_csv.js20
-rw-r--r--comm/mailnews/import/test/unit/test_utf16_csv.js20
-rw-r--r--comm/mailnews/import/test/unit/test_vcard_import.js34
-rw-r--r--comm/mailnews/import/test/unit/test_winmail.js178
-rw-r--r--comm/mailnews/import/test/unit/xpcshell.ini26
-rw-r--r--comm/mailnews/intl/charsetData.properties104
-rw-r--r--comm/mailnews/intl/charsetalias.properties151
-rw-r--r--comm/mailnews/intl/components.conf12
-rw-r--r--comm/mailnews/intl/jar.mn6
-rw-r--r--comm/mailnews/intl/moz.build42
-rw-r--r--comm/mailnews/intl/nsCharsetAlias.cpp86
-rw-r--r--comm/mailnews/intl/nsCharsetAlias.h27
-rw-r--r--comm/mailnews/intl/nsCharsetConverterManager.cpp184
-rw-r--r--comm/mailnews/intl/nsCharsetConverterManager.h27
-rw-r--r--comm/mailnews/intl/nsICharsetConverterManager.idl71
-rw-r--r--comm/mailnews/intl/nsMUTF7ToUnicode.cpp11
-rw-r--r--comm/mailnews/intl/nsMUTF7ToUnicode.h28
-rw-r--r--comm/mailnews/intl/nsUTF7ToUnicode.cpp217
-rw-r--r--comm/mailnews/intl/nsUTF7ToUnicode.h64
-rw-r--r--comm/mailnews/intl/nsUnicodeToMUTF7.cpp11
-rw-r--r--comm/mailnews/intl/nsUnicodeToMUTF7.h28
-rw-r--r--comm/mailnews/intl/nsUnicodeToUTF7.cpp302
-rw-r--r--comm/mailnews/intl/nsUnicodeToUTF7.h69
-rw-r--r--comm/mailnews/intl/test/moz.build6
-rw-r--r--comm/mailnews/intl/test/unit/head_CharsetConversionTests.js46
-rw-r--r--comm/mailnews/intl/test/unit/test_decode_utf-7.js23
-rw-r--r--comm/mailnews/intl/test/unit/test_decode_utf-7_internal.js30
-rw-r--r--comm/mailnews/intl/test/unit/test_encode_utf-7.js22
-rw-r--r--comm/mailnews/intl/test/unit/test_encode_utf-7_internal.js24
-rw-r--r--comm/mailnews/intl/test/unit/xpcshell.ini10
-rw-r--r--comm/mailnews/jar.mn120
-rw-r--r--comm/mailnews/jsaccount/modules/JSAccountUtils.jsm264
-rw-r--r--comm/mailnews/jsaccount/modules/JaBaseUrl.jsm83
-rw-r--r--comm/mailnews/jsaccount/moz.build20
-rw-r--r--comm/mailnews/jsaccount/public/moz.build15
-rw-r--r--comm/mailnews/jsaccount/public/msgIDelegateList.idl19
-rw-r--r--comm/mailnews/jsaccount/public/msgIJaUrl.idl27
-rw-r--r--comm/mailnews/jsaccount/public/msgIOverride.idl42
-rw-r--r--comm/mailnews/jsaccount/readme.html67
-rw-r--r--comm/mailnews/jsaccount/src/DelegateList.cpp24
-rw-r--r--comm/mailnews/jsaccount/src/DelegateList.h48
-rw-r--r--comm/mailnews/jsaccount/src/JaAbDirectory.cpp81
-rw-r--r--comm/mailnews/jsaccount/src/JaAbDirectory.h84
-rw-r--r--comm/mailnews/jsaccount/src/JaCompose.cpp89
-rw-r--r--comm/mailnews/jsaccount/src/JaCompose.h89
-rw-r--r--comm/mailnews/jsaccount/src/JaIncomingServer.cpp96
-rw-r--r--comm/mailnews/jsaccount/src/JaIncomingServer.h95
-rw-r--r--comm/mailnews/jsaccount/src/JaMsgFolder.cpp176
-rw-r--r--comm/mailnews/jsaccount/src/JaMsgFolder.h137
-rw-r--r--comm/mailnews/jsaccount/src/JaUrl.cpp205
-rw-r--r--comm/mailnews/jsaccount/src/JaUrl.h138
-rw-r--r--comm/mailnews/jsaccount/src/components.conf37
-rw-r--r--comm/mailnews/jsaccount/src/moz.build30
-rw-r--r--comm/mailnews/jsaccount/test/components.conf14
-rw-r--r--comm/mailnews/jsaccount/test/idl/moz.build15
-rw-r--r--comm/mailnews/jsaccount/test/idl/msgIFooUrl.idl17
-rw-r--r--comm/mailnews/jsaccount/test/moz.build15
-rw-r--r--comm/mailnews/jsaccount/test/unit/head_jsaccount.js60
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/TestJaMsgProtocolInfoComponent.jsm75
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/readme.html62
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testComponents.manifest16
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServer.jsm74
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServerComponent.js20
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolder.jsm70
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolderComponent.js19
-rw-r--r--comm/mailnews/jsaccount/test/unit/resources/testJaFooUrlComponent.js88
-rw-r--r--comm/mailnews/jsaccount/test/unit/test_componentsExist.js90
-rw-r--r--comm/mailnews/jsaccount/test/unit/test_fooUrl.js93
-rw-r--r--comm/mailnews/jsaccount/test/unit/test_jaMsgFolder.js56
-rw-r--r--comm/mailnews/jsaccount/test/unit/xpcshell.ini10
-rw-r--r--comm/mailnews/local/public/moz.build26
-rw-r--r--comm/mailnews/local/public/nsILocalMailIncomingServer.idl23
-rw-r--r--comm/mailnews/local/public/nsIMailboxService.idl34
-rw-r--r--comm/mailnews/local/public/nsIMailboxUrl.idl58
-rw-r--r--comm/mailnews/local/public/nsIMsgLocalMailFolder.idl134
-rw-r--r--comm/mailnews/local/public/nsIMsgParseMailMsgState.idl46
-rw-r--r--comm/mailnews/local/public/nsINewsBlogFeedDownloader.idl32
-rw-r--r--comm/mailnews/local/public/nsINoIncomingServer.idl16
-rw-r--r--comm/mailnews/local/public/nsINoneService.idl11
-rw-r--r--comm/mailnews/local/public/nsIPop3IncomingServer.idl33
-rw-r--r--comm/mailnews/local/public/nsIPop3Protocol.idl23
-rw-r--r--comm/mailnews/local/public/nsIPop3Service.idl128
-rw-r--r--comm/mailnews/local/public/nsIPop3Sink.idl42
-rw-r--r--comm/mailnews/local/public/nsIPop3URL.idl19
-rw-r--r--comm/mailnews/local/public/nsIRssIncomingServer.idl16
-rw-r--r--comm/mailnews/local/public/nsIRssService.idl10
-rw-r--r--comm/mailnews/local/src/Pop3Channel.jsm105
-rw-r--r--comm/mailnews/local/src/Pop3Client.jsm1570
-rw-r--r--comm/mailnews/local/src/Pop3IncomingServer.jsm308
-rw-r--r--comm/mailnews/local/src/Pop3ProtocolHandler.jsm40
-rw-r--r--comm/mailnews/local/src/Pop3ProtocolInfo.jsm44
-rw-r--r--comm/mailnews/local/src/Pop3Service.jsm77
-rw-r--r--comm/mailnews/local/src/components.conf150
-rw-r--r--comm/mailnews/local/src/moz.build40
-rw-r--r--comm/mailnews/local/src/nsLocalMailFolder.cpp3468
-rw-r--r--comm/mailnews/local/src/nsLocalMailFolder.h285
-rw-r--r--comm/mailnews/local/src/nsLocalUndoTxn.cpp495
-rw-r--r--comm/mailnews/local/src/nsLocalUndoTxn.h79
-rw-r--r--comm/mailnews/local/src/nsLocalUtils.cpp208
-rw-r--r--comm/mailnews/local/src/nsLocalUtils.h29
-rw-r--r--comm/mailnews/local/src/nsMailboxProtocol.cpp657
-rw-r--r--comm/mailnews/local/src/nsMailboxProtocol.h113
-rw-r--r--comm/mailnews/local/src/nsMailboxServer.cpp28
-rw-r--r--comm/mailnews/local/src/nsMailboxServer.h22
-rw-r--r--comm/mailnews/local/src/nsMailboxService.cpp559
-rw-r--r--comm/mailnews/local/src/nsMailboxService.h59
-rw-r--r--comm/mailnews/local/src/nsMailboxUrl.cpp474
-rw-r--r--comm/mailnews/local/src/nsMailboxUrl.h106
-rw-r--r--comm/mailnews/local/src/nsMsgBrkMBoxStore.cpp1033
-rw-r--r--comm/mailnews/local/src/nsMsgBrkMBoxStore.h57
-rw-r--r--comm/mailnews/local/src/nsMsgFileHdr.cpp389
-rw-r--r--comm/mailnews/local/src/nsMsgFileHdr.h41
-rw-r--r--comm/mailnews/local/src/nsMsgLocalStoreUtils.cpp380
-rw-r--r--comm/mailnews/local/src/nsMsgLocalStoreUtils.h39
-rw-r--r--comm/mailnews/local/src/nsMsgMaildirStore.cpp1380
-rw-r--r--comm/mailnews/local/src/nsMsgMaildirStore.h39
-rw-r--r--comm/mailnews/local/src/nsNoIncomingServer.cpp189
-rw-r--r--comm/mailnews/local/src/nsNoIncomingServer.h41
-rw-r--r--comm/mailnews/local/src/nsNoneService.cpp147
-rw-r--r--comm/mailnews/local/src/nsNoneService.h26
-rw-r--r--comm/mailnews/local/src/nsParseMailbox.cpp2354
-rw-r--r--comm/mailnews/local/src/nsParseMailbox.h256
-rw-r--r--comm/mailnews/local/src/nsPop3Sink.cpp740
-rw-r--r--comm/mailnews/local/src/nsPop3Sink.h70
-rw-r--r--comm/mailnews/local/src/nsPop3URL.cpp202
-rw-r--r--comm/mailnews/local/src/nsPop3URL.h36
-rw-r--r--comm/mailnews/local/src/nsRssIncomingServer.cpp248
-rw-r--r--comm/mailnews/local/src/nsRssIncomingServer.h47
-rw-r--r--comm/mailnews/local/src/nsRssService.cpp113
-rw-r--r--comm/mailnews/local/src/nsRssService.h23
-rw-r--r--comm/mailnews/local/test/moz.build6
-rw-r--r--comm/mailnews/local/test/unit/data/dot10
-rw-r--r--comm/mailnews/local/test/unit/data/invalid_mozilla_keys.eml5
-rw-r--r--comm/mailnews/local/test/unit/data/mailformed_recipients.eml66
-rw-r--r--comm/mailnews/local/test/unit/data/mailformed_subject.eml1934
-rw-r--r--comm/mailnews/local/test/unit/data/message1.eml7
-rw-r--r--comm/mailnews/local/test/unit/data/message2.eml9
-rw-r--r--comm/mailnews/local/test/unit/data/message3.eml7
-rw-r--r--comm/mailnews/local/test/unit/data/movemailspool169
-rw-r--r--comm/mailnews/local/test/unit/head_maillocal.js214
-rw-r--r--comm/mailnews/local/test/unit/test_Pop3Channel.js75
-rw-r--r--comm/mailnews/local/test/unit/test_bug457168.js165
-rw-r--r--comm/mailnews/local/test/unit/test_duplicateKey.js81
-rw-r--r--comm/mailnews/local/test/unit/test_fileName.js112
-rw-r--r--comm/mailnews/local/test/unit/test_folderLoaded.js89
-rw-r--r--comm/mailnews/local/test/unit/test_localFolder.js164
-rw-r--r--comm/mailnews/local/test/unit/test_mailboxContentLength.js62
-rw-r--r--comm/mailnews/local/test/unit/test_mailboxProtocol.js52
-rw-r--r--comm/mailnews/local/test/unit/test_mailboxURL.js82
-rw-r--r--comm/mailnews/local/test/unit/test_msgCopy.js27
-rw-r--r--comm/mailnews/local/test/unit/test_msgIDParsing.js24
-rw-r--r--comm/mailnews/local/test/unit/test_noTop.js64
-rw-r--r--comm/mailnews/local/test/unit/test_noUidl.js89
-rw-r--r--comm/mailnews/local/test/unit/test_nsIMsgLocalMailFolder.js321
-rw-r--r--comm/mailnews/local/test/unit/test_nsIMsgParseMailMsgState.js42
-rw-r--r--comm/mailnews/local/test/unit/test_nsIMsgPluggableStore.js52
-rw-r--r--comm/mailnews/local/test/unit/test_over2GBMailboxes.js129
-rw-r--r--comm/mailnews/local/test/unit/test_over4GBMailboxes.js640
-rw-r--r--comm/mailnews/local/test/unit/test_pop3AuthMethods.js201
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Client.js145
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Download.js81
-rw-r--r--comm/mailnews/local/test/unit/test_pop3DownloadTempFileHandling.js62
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Duplicates.js40
-rw-r--r--comm/mailnews/local/test/unit/test_pop3FilterActions.js143
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Filters.js114
-rw-r--r--comm/mailnews/local/test/unit/test_pop3GSSAPIFail.js222
-rw-r--r--comm/mailnews/local/test/unit/test_pop3GetNewMail.js147
-rw-r--r--comm/mailnews/local/test/unit/test_pop3MoveFilter.js137
-rw-r--r--comm/mailnews/local/test/unit/test_pop3MoveFilter2.js108
-rw-r--r--comm/mailnews/local/test/unit/test_pop3MultiCopy.js97
-rw-r--r--comm/mailnews/local/test/unit/test_pop3MultiCopy2.js179
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Password.js162
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Password2.js213
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Password3.js75
-rw-r--r--comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc1939.js216
-rw-r--r--comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc2449.js215
-rw-r--r--comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc5034.js217
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Proxy.js60
-rw-r--r--comm/mailnews/local/test/unit/test_pop3Pump.js32
-rw-r--r--comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMDisconnect.js127
-rw-r--r--comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMFail.js124
-rw-r--r--comm/mailnews/local/test/unit/test_preview.js40
-rw-r--r--comm/mailnews/local/test/unit/test_saveMessage.js69
-rw-r--r--comm/mailnews/local/test/unit/test_streamHeaders.js90
-rw-r--r--comm/mailnews/local/test/unit/test_undoDelete.js80
-rw-r--r--comm/mailnews/local/test/unit/test_verifyLogon.js93
-rw-r--r--comm/mailnews/local/test/unit/xpcshell.ini57
-rw-r--r--comm/mailnews/mailnews.js1150
-rw-r--r--comm/mailnews/mapi/include/README.md33
-rw-r--r--comm/mailnews/mapi/include/mapiaux.h235
-rw-r--r--comm/mailnews/mapi/include/mapicode.h218
-rw-r--r--comm/mailnews/mapi/include/mapidbg.h492
-rw-r--r--comm/mailnews/mapi/include/mapidefs.h2736
-rw-r--r--comm/mailnews/mapi/include/mapiform.h632
-rw-r--r--comm/mailnews/mapi/include/mapiguid.h353
-rw-r--r--comm/mailnews/mapi/include/mapihook.h83
-rw-r--r--comm/mailnews/mapi/include/mapinls.h196
-rw-r--r--comm/mailnews/mapi/include/mapioid.h106
-rw-r--r--comm/mailnews/mapi/include/mapispi.h929
-rw-r--r--comm/mailnews/mapi/include/mapitags.h1036
-rw-r--r--comm/mailnews/mapi/include/mapiutil.h889
-rw-r--r--comm/mailnews/mapi/include/mapival.h1975
-rw-r--r--comm/mailnews/mapi/include/mapiwin.h274
-rw-r--r--comm/mailnews/mapi/include/mapiwz.h73
-rw-r--r--comm/mailnews/mapi/include/mapix.h545
-rw-r--r--comm/mailnews/mapi/include/mspst.h99
-rw-r--r--comm/mailnews/mapi/mapiDll/Makefile.in6
-rw-r--r--comm/mailnews/mapi/mapiDll/Mapi32.def22
-rw-r--r--comm/mailnews/mapi/mapiDll/MapiDll.cpp577
-rw-r--r--comm/mailnews/mapi/mapiDll/module.ver7
-rw-r--r--comm/mailnews/mapi/mapiDll/moz.build24
-rw-r--r--comm/mailnews/mapi/mapihook/build/MapiProxy.def13
-rw-r--r--comm/mailnews/mapi/mapihook/build/module.ver6
-rw-r--r--comm/mailnews/mapi/mapihook/build/moz.build56
-rw-r--r--comm/mailnews/mapi/mapihook/build/msgMapi.idl125
-rw-r--r--comm/mailnews/mapi/mapihook/moz.build10
-rw-r--r--comm/mailnews/mapi/mapihook/public/moz.build10
-rw-r--r--comm/mailnews/mapi/mapihook/public/nsIMapiSupport.idl42
-rw-r--r--comm/mailnews/mapi/mapihook/src/Registry.cpp249
-rw-r--r--comm/mailnews/mapi/mapihook/src/Registry.h20
-rw-r--r--comm/mailnews/mapi/mapihook/src/components.conf18
-rw-r--r--comm/mailnews/mapi/mapihook/src/moz.build33
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiFactory.cpp64
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiFactory.h35
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiHook.cpp934
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiHook.h39
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiImp.cpp801
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiImp.h79
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiMain.cpp248
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiMain.h80
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiSupport.cpp101
-rw-r--r--comm/mailnews/mapi/mapihook/src/msgMapiSupport.h35
-rw-r--r--comm/mailnews/mapi/test/moz.build6
-rw-r--r--comm/mailnews/mapi/test/unit/head_mapi.js243
-rw-r--r--comm/mailnews/mapi/test/unit/tail_mapi.js1
-rw-r--r--comm/mailnews/mapi/test/unit/test_mapisendmail.js97
-rw-r--r--comm/mailnews/mapi/test/unit/xpcshell.ini7
-rw-r--r--comm/mailnews/mime/cthandlers/glue/mimexpcom.cpp119
-rw-r--r--comm/mailnews/mime/cthandlers/glue/mimexpcom.h92
-rw-r--r--comm/mailnews/mime/cthandlers/glue/moz.build17
-rw-r--r--comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.cpp55
-rw-r--r--comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.h46
-rw-r--r--comm/mailnews/mime/cthandlers/moz.build10
-rw-r--r--comm/mailnews/mime/cthandlers/pgpmime/components.conf21
-rw-r--r--comm/mailnews/mime/cthandlers/pgpmime/moz.build24
-rw-r--r--comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeMimeContentTypeHandler.h22
-rw-r--r--comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.cpp672
-rw-r--r--comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.h73
-rw-r--r--comm/mailnews/mime/emitters/moz.build18
-rw-r--r--comm/mailnews/mime/emitters/nsEmitterUtils.cpp45
-rw-r--r--comm/mailnews/mime/emitters/nsEmitterUtils.h14
-rw-r--r--comm/mailnews/mime/emitters/nsMimeBaseEmitter.cpp978
-rw-r--r--comm/mailnews/mime/emitters/nsMimeBaseEmitter.h140
-rw-r--r--comm/mailnews/mime/emitters/nsMimeHtmlEmitter.cpp492
-rw-r--r--comm/mailnews/mime/emitters/nsMimeHtmlEmitter.h50
-rw-r--r--comm/mailnews/mime/emitters/nsMimePlainEmitter.cpp50
-rw-r--r--comm/mailnews/mime/emitters/nsMimePlainEmitter.h26
-rw-r--r--comm/mailnews/mime/emitters/nsMimeRawEmitter.cpp24
-rw-r--r--comm/mailnews/mime/emitters/nsMimeRawEmitter.h22
-rw-r--r--comm/mailnews/mime/emitters/nsMimeRebuffer.cpp33
-rw-r--r--comm/mailnews/mime/emitters/nsMimeRebuffer.h29
-rw-r--r--comm/mailnews/mime/emitters/nsMimeXmlEmitter.cpp152
-rw-r--r--comm/mailnews/mime/emitters/nsMimeXmlEmitter.h41
-rw-r--r--comm/mailnews/mime/jsmime/LICENSE19
-rw-r--r--comm/mailnews/mime/jsmime/README.md59
-rw-r--r--comm/mailnews/mime/jsmime/jsmime.js3682
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/base64-17
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/base64-26
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/basic13
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/bug50522175
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/bugmail1147
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/charsetsbin0 -> 1979 bytes
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/message-encoded24
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/mime-torture25875
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-111
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-212
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-310
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart-complex135
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart-complex262
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart112
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart213
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart329
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipart47
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/multipartmalt-detach54
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/data/shift-jis-image21
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/head_xpcshell_glue.js176
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/mock_date.js86
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_custom_headers.js43
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_header.js1224
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_header_emitter.js519
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_mime_tree.js838
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_structured_header_emitters.js133
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/test_structured_headers.js254
-rw-r--r--comm/mailnews/mime/jsmime/test/unit/xpcshell.ini11
-rw-r--r--comm/mailnews/mime/moz.build23
-rw-r--r--comm/mailnews/mime/public/MimeEncoder.h44
-rw-r--r--comm/mailnews/mime/public/MimeHeaderParser.h178
-rw-r--r--comm/mailnews/mime/public/moz.build28
-rw-r--r--comm/mailnews/mime/public/msgIStructuredHeaders.idl175
-rw-r--r--comm/mailnews/mime/public/nsIMimeContentTypeHandler.idl39
-rw-r--r--comm/mailnews/mime/public/nsIMimeConverter.idl69
-rw-r--r--comm/mailnews/mime/public/nsIMimeEmitter.idl81
-rw-r--r--comm/mailnews/mime/public/nsIMimeHeaders.idl41
-rw-r--r--comm/mailnews/mime/public/nsIMimeObjectClassAccess.h53
-rw-r--r--comm/mailnews/mime/public/nsIMimeStreamConverter.idl93
-rw-r--r--comm/mailnews/mime/public/nsIMsgHeaderParser.idl200
-rw-r--r--comm/mailnews/mime/public/nsIPgpMimeProxy.idl90
-rw-r--r--comm/mailnews/mime/public/nsISimpleMimeConverter.idl51
-rw-r--r--comm/mailnews/mime/public/nsMailHeaders.h90
-rw-r--r--comm/mailnews/mime/src/MimeHeaderParser.cpp232
-rw-r--r--comm/mailnews/mime/src/MimeJSComponents.jsm547
-rw-r--r--comm/mailnews/mime/src/comi18n.cpp49
-rw-r--r--comm/mailnews/mime/src/comi18n.h39
-rw-r--r--comm/mailnews/mime/src/components.conf87
-rw-r--r--comm/mailnews/mime/src/extraMimeParsers.jsm31
-rw-r--r--comm/mailnews/mime/src/jsmime.jsm72
-rw-r--r--comm/mailnews/mime/src/mime.def7
-rw-r--r--comm/mailnews/mime/src/mimeParser.jsm546
-rw-r--r--comm/mailnews/mime/src/mimeTextHTMLParsed.cpp171
-rw-r--r--comm/mailnews/mime/src/mimeTextHTMLParsed.h28
-rw-r--r--comm/mailnews/mime/src/mimebuf.cpp214
-rw-r--r--comm/mailnews/mime/src/mimebuf.h35
-rw-r--r--comm/mailnews/mime/src/mimecms.cpp772
-rw-r--r--comm/mailnews/mime/src/mimecms.h36
-rw-r--r--comm/mailnews/mime/src/mimecom.cpp53
-rw-r--r--comm/mailnews/mime/src/mimecom.h37
-rw-r--r--comm/mailnews/mime/src/mimecont.cpp209
-rw-r--r--comm/mailnews/mime/src/mimecont.h43
-rw-r--r--comm/mailnews/mime/src/mimecryp.cpp507
-rw-r--r--comm/mailnews/mime/src/mimecryp.h139
-rw-r--r--comm/mailnews/mime/src/mimecth.cpp33
-rw-r--r--comm/mailnews/mime/src/mimecth.h131
-rw-r--r--comm/mailnews/mime/src/mimedrft.cpp2135
-rw-r--r--comm/mailnews/mime/src/mimeebod.cpp441
-rw-r--r--comm/mailnews/mime/src/mimeebod.h37
-rw-r--r--comm/mailnews/mime/src/mimeenc.cpp999
-rw-r--r--comm/mailnews/mime/src/mimeeobj.cpp215
-rw-r--r--comm/mailnews/mime/src/mimeeobj.h50
-rw-r--r--comm/mailnews/mime/src/mimefilt.cpp349
-rw-r--r--comm/mailnews/mime/src/mimehdrs.cpp785
-rw-r--r--comm/mailnews/mime/src/mimehdrs.h85
-rw-r--r--comm/mailnews/mime/src/mimei.cpp1716
-rw-r--r--comm/mailnews/mime/src/mimei.h405
-rw-r--r--comm/mailnews/mime/src/mimeiimg.cpp220
-rw-r--r--comm/mailnews/mime/src/mimeiimg.h35
-rw-r--r--comm/mailnews/mime/src/mimeleaf.cpp187
-rw-r--r--comm/mailnews/mime/src/mimeleaf.h60
-rw-r--r--comm/mailnews/mime/src/mimemalt.cpp549
-rw-r--r--comm/mailnews/mime/src/mimemalt.h50
-rw-r--r--comm/mailnews/mime/src/mimemapl.cpp173
-rw-r--r--comm/mailnews/mime/src/mimemapl.h32
-rw-r--r--comm/mailnews/mime/src/mimemcms.cpp501
-rw-r--r--comm/mailnews/mime/src/mimemcms.h35
-rw-r--r--comm/mailnews/mime/src/mimemdig.cpp22
-rw-r--r--comm/mailnews/mime/src/mimemdig.h33
-rw-r--r--comm/mailnews/mime/src/mimemmix.cpp19
-rw-r--r--comm/mailnews/mime/src/mimemmix.h32
-rw-r--r--comm/mailnews/mime/src/mimemoz2.cpp1862
-rw-r--r--comm/mailnews/mime/src/mimemoz2.h207
-rw-r--r--comm/mailnews/mime/src/mimempar.cpp20
-rw-r--r--comm/mailnews/mime/src/mimempar.h32
-rw-r--r--comm/mailnews/mime/src/mimemrel.cpp1113
-rw-r--r--comm/mailnews/mime/src/mimemrel.h61
-rw-r--r--comm/mailnews/mime/src/mimemsg.cpp847
-rw-r--r--comm/mailnews/mime/src/mimemsg.h38
-rw-r--r--comm/mailnews/mime/src/mimemsig.cpp716
-rw-r--r--comm/mailnews/mime/src/mimemsig.h136
-rw-r--r--comm/mailnews/mime/src/mimemult.cpp676
-rw-r--r--comm/mailnews/mime/src/mimemult.h101
-rw-r--r--comm/mailnews/mime/src/mimeobj.cpp301
-rw-r--r--comm/mailnews/mime/src/mimeobj.h182
-rw-r--r--comm/mailnews/mime/src/mimepbuf.cpp251
-rw-r--r--comm/mailnews/mime/src/mimepbuf.h63
-rw-r--r--comm/mailnews/mime/src/mimesun.cpp313
-rw-r--r--comm/mailnews/mime/src/mimesun.h59
-rw-r--r--comm/mailnews/mime/src/mimetenr.cpp27
-rw-r--r--comm/mailnews/mime/src/mimetenr.h32
-rw-r--r--comm/mailnews/mime/src/mimetext.cpp442
-rw-r--r--comm/mailnews/mime/src/mimetext.h79
-rw-r--r--comm/mailnews/mime/src/mimethpl.cpp151
-rw-r--r--comm/mailnews/mime/src/mimethpl.h36
-rw-r--r--comm/mailnews/mime/src/mimethsa.cpp127
-rw-r--r--comm/mailnews/mime/src/mimethsa.h30
-rw-r--r--comm/mailnews/mime/src/mimethtm.cpp229
-rw-r--r--comm/mailnews/mime/src/mimethtm.h34
-rw-r--r--comm/mailnews/mime/src/mimetpfl.cpp613
-rw-r--r--comm/mailnews/mime/src/mimetpfl.h51
-rw-r--r--comm/mailnews/mime/src/mimetpla.cpp409
-rw-r--r--comm/mailnews/mime/src/mimetpla.h39
-rw-r--r--comm/mailnews/mime/src/mimetric.cpp356
-rw-r--r--comm/mailnews/mime/src/mimetric.h33
-rw-r--r--comm/mailnews/mime/src/mimeunty.cpp523
-rw-r--r--comm/mailnews/mime/src/mimeunty.h70
-rw-r--r--comm/mailnews/mime/src/modlmime.h388
-rw-r--r--comm/mailnews/mime/src/modmimee.h54
-rw-r--r--comm/mailnews/mime/src/moz.build88
-rw-r--r--comm/mailnews/mime/src/msgMime.manifest1
-rw-r--r--comm/mailnews/mime/src/nsMimeObjectClassAccess.cpp75
-rw-r--r--comm/mailnews/mime/src/nsMimeObjectClassAccess.h50
-rw-r--r--comm/mailnews/mime/src/nsMimeStringResources.h40
-rw-r--r--comm/mailnews/mime/src/nsSimpleMimeConverterStub.cpp191
-rw-r--r--comm/mailnews/mime/src/nsSimpleMimeConverterStub.h12
-rw-r--r--comm/mailnews/mime/src/nsStreamConverter.cpp981
-rw-r--r--comm/mailnews/mime/src/nsStreamConverter.h94
-rw-r--r--comm/mailnews/mime/test/TestMimeCrash.cpp63
-rw-r--r--comm/mailnews/mime/test/moz.build6
-rw-r--r--comm/mailnews/mime/test/unit/custom_header.js11
-rw-r--r--comm/mailnews/mime/test/unit/head_mime.js62
-rw-r--r--comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js39
-rw-r--r--comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js58
-rw-r--r--comm/mailnews/mime/test/unit/test_attachment_size.js322
-rw-r--r--comm/mailnews/mime/test/unit/test_badContentType.js115
-rw-r--r--comm/mailnews/mime/test/unit/test_bug493544.js106
-rw-r--r--comm/mailnews/mime/test/unit/test_handlerRegistration.js64
-rw-r--r--comm/mailnews/mime/test/unit/test_hidden_attachments.js221
-rw-r--r--comm/mailnews/mime/test/unit/test_jsmime_charset.js43
-rw-r--r--comm/mailnews/mime/test/unit/test_message_attachment.js168
-rw-r--r--comm/mailnews/mime/test/unit/test_mimeContentType.js82
-rw-r--r--comm/mailnews/mime/test/unit/test_mimeStreaming.js88
-rw-r--r--comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js52
-rw-r--r--comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js86
-rw-r--r--comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js115
-rw-r--r--comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js199
-rw-r--r--comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js104
-rw-r--r--comm/mailnews/mime/test/unit/test_openpgp_decrypt.js415
-rw-r--r--comm/mailnews/mime/test/unit/test_parser.js322
-rw-r--r--comm/mailnews/mime/test/unit/test_rfc822_body.js94
-rw-r--r--comm/mailnews/mime/test/unit/test_smime_decrypt.js701
-rw-r--r--comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js717
-rw-r--r--comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js274
-rw-r--r--comm/mailnews/mime/test/unit/test_structured_headers.js249
-rw-r--r--comm/mailnews/mime/test/unit/test_text_attachment.js89
-rw-r--r--comm/mailnews/mime/test/unit/xpcshell.ini35
-rw-r--r--comm/mailnews/moz.build90
-rw-r--r--comm/mailnews/moz.configure26
-rw-r--r--comm/mailnews/news/content/downloadheaders.js92
-rw-r--r--comm/mailnews/news/content/downloadheaders.xhtml82
-rw-r--r--comm/mailnews/news/moz.build11
-rw-r--r--comm/mailnews/news/public/moz.build18
-rw-r--r--comm/mailnews/news/public/nsIMsgNewsFolder.idl151
-rw-r--r--comm/mailnews/news/public/nsIMsgOfflineNewsState.idl22
-rw-r--r--comm/mailnews/news/public/nsINNTPNewsgroupPost.idl51
-rw-r--r--comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl19
-rw-r--r--comm/mailnews/news/public/nsINntpIncomingServer.idl136
-rw-r--r--comm/mailnews/news/public/nsINntpService.idl50
-rw-r--r--comm/mailnews/news/public/nsINntpUrl.idl102
-rw-r--r--comm/mailnews/news/src/NewsAutoCompleteSearch.jsm134
-rw-r--r--comm/mailnews/news/src/NewsDownloader.sys.mjs158
-rw-r--r--comm/mailnews/news/src/NntpChannel.jsm402
-rw-r--r--comm/mailnews/news/src/NntpClient.jsm981
-rw-r--r--comm/mailnews/news/src/NntpIncomingServer.jsm624
-rw-r--r--comm/mailnews/news/src/NntpMessageService.jsm272
-rw-r--r--comm/mailnews/news/src/NntpNewsGroup.jsm420
-rw-r--r--comm/mailnews/news/src/NntpProtocolHandler.jsm46
-rw-r--r--comm/mailnews/news/src/NntpProtocolInfo.jsm44
-rw-r--r--comm/mailnews/news/src/NntpService.jsm250
-rw-r--r--comm/mailnews/news/src/NntpUtils.jsm63
-rw-r--r--comm/mailnews/news/src/components.conf98
-rw-r--r--comm/mailnews/news/src/moz.build32
-rw-r--r--comm/mailnews/news/src/nntpCore.h165
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp79
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.h29
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.cpp507
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.h136
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.cpp1645
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.h144
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.cpp57
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.h30
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.cpp476
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.h68
-rw-r--r--comm/mailnews/news/test/moz.build6
-rw-r--r--comm/mailnews/news/test/unit/head_server_setup.js244
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post1.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post2.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post3.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post4.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post5.eml50
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post6.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post7.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post8.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/bug403242.eml25
-rw-r--r--comm/mailnews/news/test/unit/postings/bug670935.eml7
-rw-r--r--comm/mailnews/news/test/unit/postings/post1.eml7
-rw-r--r--comm/mailnews/news/test/unit/postings/post2.eml6
-rw-r--r--comm/mailnews/news/test/unit/postings/post3.eml10
-rw-r--r--comm/mailnews/news/test/unit/test_NntpChannel.js74
-rw-r--r--comm/mailnews/news/test/unit/test_biff.js57
-rw-r--r--comm/mailnews/news/test/unit/test_bug170727.js61
-rw-r--r--comm/mailnews/news/test/unit/test_bug37465.js49
-rw-r--r--comm/mailnews/news/test/unit/test_bug403242.js55
-rw-r--r--comm/mailnews/news/test/unit/test_bug540288.js114
-rw-r--r--comm/mailnews/news/test/unit/test_bug695309.js121
-rw-r--r--comm/mailnews/news/test/unit/test_cancelPasswordDialog.js59
-rw-r--r--comm/mailnews/news/test/unit/test_filter.js179
-rw-r--r--comm/mailnews/news/test/unit/test_getNewsMessage.js101
-rw-r--r--comm/mailnews/news/test/unit/test_internalUris.js305
-rw-r--r--comm/mailnews/news/test/unit/test_newsAutocomplete.js107
-rw-r--r--comm/mailnews/news/test/unit/test_nntpContentLength.js80
-rw-r--r--comm/mailnews/news/test/unit/test_nntpGroupPassword.js99
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword.js54
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword2.js106
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword3.js46
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPasswordFailure.js196
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPost.js37
-rw-r--r--comm/mailnews/news/test/unit/test_nntpProtocols.js55
-rw-r--r--comm/mailnews/news/test/unit/test_nntpProxy.js38
-rw-r--r--comm/mailnews/news/test/unit/test_nntpUrl.js30
-rw-r--r--comm/mailnews/news/test/unit/test_server.js179
-rw-r--r--comm/mailnews/news/test/unit/test_uriParser.js221
-rw-r--r--comm/mailnews/news/test/unit/test_xover.js42
-rw-r--r--comm/mailnews/news/test/unit/xpcshell.ini35
-rw-r--r--comm/mailnews/nss-extra.symbols58
-rw-r--r--comm/mailnews/search/content/CustomHeaders.js194
-rw-r--r--comm/mailnews/search/content/CustomHeaders.xhtml61
-rw-r--r--comm/mailnews/search/content/FilterEditor.js809
-rw-r--r--comm/mailnews/search/content/FilterEditor.xhtml136
-rw-r--r--comm/mailnews/search/content/searchTerm.inc.xhtml27
-rw-r--r--comm/mailnews/search/content/searchTerm.js568
-rw-r--r--comm/mailnews/search/content/searchWidgets.js1779
-rw-r--r--comm/mailnews/search/content/viewLog.js38
-rw-r--r--comm/mailnews/search/content/viewLog.xhtml65
-rw-r--r--comm/mailnews/search/public/moz.build37
-rw-r--r--comm/mailnews/search/public/nsIMsgFilter.idl124
-rw-r--r--comm/mailnews/search/public/nsIMsgFilterCustomAction.idl88
-rw-r--r--comm/mailnews/search/public/nsIMsgFilterHitNotify.idl26
-rw-r--r--comm/mailnews/search/public/nsIMsgFilterList.idl115
-rw-r--r--comm/mailnews/search/public/nsIMsgFilterPlugin.idl331
-rw-r--r--comm/mailnews/search/public/nsIMsgFilterService.idl102
-rw-r--r--comm/mailnews/search/public/nsIMsgOperationListener.idl17
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchAdapter.idl41
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl75
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchNotify.idl30
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl19
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchSession.idl130
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchTerm.idl153
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchValidityManager.idl26
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchValidityTable.idl32
-rw-r--r--comm/mailnews/search/public/nsIMsgSearchValue.idl35
-rw-r--r--comm/mailnews/search/public/nsIMsgTraitService.idl182
-rw-r--r--comm/mailnews/search/public/nsMsgBodyHandler.h110
-rw-r--r--comm/mailnews/search/public/nsMsgFilterCore.idl62
-rw-r--r--comm/mailnews/search/public/nsMsgResultElement.h40
-rw-r--r--comm/mailnews/search/public/nsMsgSearchAdapter.h242
-rw-r--r--comm/mailnews/search/public/nsMsgSearchBoolExpression.h110
-rw-r--r--comm/mailnews/search/public/nsMsgSearchCore.idl206
-rw-r--r--comm/mailnews/search/public/nsMsgSearchScopeTerm.h44
-rw-r--r--comm/mailnews/search/public/nsMsgSearchTerm.h89
-rw-r--r--comm/mailnews/search/src/Bogofilter.sfd14
-rw-r--r--comm/mailnews/search/src/DSPAM.sfd14
-rw-r--r--comm/mailnews/search/src/Habeas.sfd8
-rw-r--r--comm/mailnews/search/src/MsgTraitService.jsm199
-rw-r--r--comm/mailnews/search/src/POPFile.sfd14
-rw-r--r--comm/mailnews/search/src/PeriodicFilterManager.jsm202
-rw-r--r--comm/mailnews/search/src/SpamAssassin.sfd14
-rw-r--r--comm/mailnews/search/src/SpamCatcher.sfd14
-rw-r--r--comm/mailnews/search/src/SpamPal.sfd14
-rw-r--r--comm/mailnews/search/src/components.conf40
-rw-r--r--comm/mailnews/search/src/moz.build37
-rw-r--r--comm/mailnews/search/src/nsMsgBodyHandler.cpp464
-rw-r--r--comm/mailnews/search/src/nsMsgFilter.cpp864
-rw-r--r--comm/mailnews/search/src/nsMsgFilter.h100
-rw-r--r--comm/mailnews/search/src/nsMsgFilterList.cpp1207
-rw-r--r--comm/mailnews/search/src/nsMsgFilterList.h74
-rw-r--r--comm/mailnews/search/src/nsMsgFilterService.cpp1374
-rw-r--r--comm/mailnews/search/src/nsMsgFilterService.h44
-rw-r--r--comm/mailnews/search/src/nsMsgImapSearch.cpp991
-rw-r--r--comm/mailnews/search/src/nsMsgLocalSearch.cpp919
-rw-r--r--comm/mailnews/search/src/nsMsgLocalSearch.h90
-rw-r--r--comm/mailnews/search/src/nsMsgSearchAdapter.cpp1109
-rw-r--r--comm/mailnews/search/src/nsMsgSearchImap.h34
-rw-r--r--comm/mailnews/search/src/nsMsgSearchNews.cpp452
-rw-r--r--comm/mailnews/search/src/nsMsgSearchNews.h54
-rw-r--r--comm/mailnews/search/src/nsMsgSearchSession.cpp576
-rw-r--r--comm/mailnews/search/src/nsMsgSearchSession.h87
-rw-r--r--comm/mailnews/search/src/nsMsgSearchTerm.cpp1797
-rw-r--r--comm/mailnews/search/src/nsMsgSearchValue.cpp96
-rw-r--r--comm/mailnews/search/src/nsMsgSearchValue.h25
-rw-r--r--comm/mailnews/search/test/moz.build6
-rw-r--r--comm/mailnews/search/test/unit/head_mailbase.js23
-rw-r--r--comm/mailnews/search/test/unit/test_base64_decoding.js94
-rw-r--r--comm/mailnews/search/test/unit/test_bug366491.js110
-rw-r--r--comm/mailnews/search/test/unit/test_bug404489.js202
-rw-r--r--comm/mailnews/search/test/unit/test_copyThenMoveManual.js116
-rw-r--r--comm/mailnews/search/test/unit/test_junkWhitelisting.js204
-rw-r--r--comm/mailnews/search/test/unit/test_quarantineFilterMove.js181
-rw-r--r--comm/mailnews/search/test/unit/test_search.js623
-rw-r--r--comm/mailnews/search/test/unit/test_searchAddressInAb.js337
-rw-r--r--comm/mailnews/search/test/unit/test_searchBody.js294
-rw-r--r--comm/mailnews/search/test/unit/test_searchBoolean.js239
-rw-r--r--comm/mailnews/search/test/unit/test_searchChaining.js90
-rw-r--r--comm/mailnews/search/test/unit/test_searchCustomTerm.js112
-rw-r--r--comm/mailnews/search/test/unit/test_searchJunk.js322
-rw-r--r--comm/mailnews/search/test/unit/test_searchLocalizationStrings.js61
-rw-r--r--comm/mailnews/search/test/unit/test_searchTag.js490
-rw-r--r--comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js141
-rw-r--r--comm/mailnews/search/test/unit/xpcshell.ini20
-rw-r--r--comm/mailnews/test/data/01-plaintext.eml14
-rw-r--r--comm/mailnews/test/data/02-plaintext+attachment.eml32
-rw-r--r--comm/mailnews/test/data/03-HTML.eml14
-rw-r--r--comm/mailnews/test/data/04-HTML+attachment.eml32
-rw-r--r--comm/mailnews/test/data/05-HTML+embedded-image.eml38
-rw-r--r--comm/mailnews/test/data/06-plaintext+HMTL.eml27
-rw-r--r--comm/mailnews/test/data/07-plaintext+(HTML+embedded-image).eml52
-rw-r--r--comm/mailnews/test/data/08-plaintext+HTML+attachment.eml46
-rw-r--r--comm/mailnews/test/data/09-(HTML+embedded-image)+attachment.eml55
-rw-r--r--comm/mailnews/test/data/10-plaintext+(HTML+embedded-image)+attachment.eml69
-rw-r--r--comm/mailnews/test/data/11-plaintext.eml14
-rw-r--r--comm/mailnews/test/data/12-plaintext+attachment.eml32
-rw-r--r--comm/mailnews/test/data/13-HTML.eml14
-rw-r--r--comm/mailnews/test/data/14-HTML+attachment.eml32
-rw-r--r--comm/mailnews/test/data/15-HTML+embedded-image.eml42
-rw-r--r--comm/mailnews/test/data/16-plaintext+HMTL.eml27
-rw-r--r--comm/mailnews/test/data/17-plaintext+(HTML+embedded-image).eml52
-rw-r--r--comm/mailnews/test/data/18-plaintext+HTML+attachment.eml46
-rw-r--r--comm/mailnews/test/data/19-(HTML+embedded-image)+attachment.eml59
-rw-r--r--comm/mailnews/test/data/20-plaintext+(HTML+embedded-image)+attachment.eml73
-rw-r--r--comm/mailnews/test/data/21-plaintext.eml16
-rw-r--r--comm/mailnews/test/data/22-plaintext+attachment.eml32
-rw-r--r--comm/mailnews/test/data/23-HTML.eml18
-rw-r--r--comm/mailnews/test/data/24-HTML+attachment.eml35
-rw-r--r--comm/mailnews/test/data/25-HTML+embedded-image.eml42
-rw-r--r--comm/mailnews/test/data/26-plaintext+HMTL.eml27
-rw-r--r--comm/mailnews/test/data/27-plaintext+(HTML+embedded-image).eml56
-rw-r--r--comm/mailnews/test/data/28-plaintext+HTML+attachment.eml46
-rw-r--r--comm/mailnews/test/data/29-(HTML+embedded-image)+attachment.eml59
-rw-r--r--comm/mailnews/test/data/30-plaintext+(HTML+embedded-image)+attachment.eml73
-rw-r--r--comm/mailnews/test/data/HTML-with-split-tag1.eml26
-rw-r--r--comm/mailnews/test/data/HTML-with-split-tag2.eml41
-rw-r--r--comm/mailnews/test/data/SenderHeader72
-rw-r--r--comm/mailnews/test/data/SpamAssassinYes12
-rw-r--r--comm/mailnews/test/data/abLists1.sql62
-rw-r--r--comm/mailnews/test/data/abLists2.sql62
-rw-r--r--comm/mailnews/test/data/alias-1.json12
-rw-r--r--comm/mailnews/test/data/alias-10.json8
-rw-r--r--comm/mailnews/test/data/alias-11.json9
-rw-r--r--comm/mailnews/test/data/alias-12.json9
-rw-r--r--comm/mailnews/test/data/alias-13.json9
-rw-r--r--comm/mailnews/test/data/alias-14.json9
-rw-r--r--comm/mailnews/test/data/alias-15.json9
-rw-r--r--comm/mailnews/test/data/alias-2.json9
-rw-r--r--comm/mailnews/test/data/alias-3.json9
-rw-r--r--comm/mailnews/test/data/alias-4.json13
-rw-r--r--comm/mailnews/test/data/alias-5.json14
-rw-r--r--comm/mailnews/test/data/alias-6.json9
-rw-r--r--comm/mailnews/test/data/alias-7.json20
-rw-r--r--comm/mailnews/test/data/alias-8.json19
-rw-r--r--comm/mailnews/test/data/alias-9.json7
-rw-r--r--comm/mailnews/test/data/bad-charset.eml29
-rw-r--r--comm/mailnews/test/data/badly-folded-headers.eml16
-rw-r--r--comm/mailnews/test/data/base64-17
-rw-r--r--comm/mailnews/test/data/base64-1.out3
-rw-r--r--comm/mailnews/test/data/base64-26
-rw-r--r--comm/mailnews/test/data/base64-2.out2
-rw-r--r--comm/mailnews/test/data/base64-34
-rw-r--r--comm/mailnews/test/data/base64-3.out1
-rw-r--r--comm/mailnews/test/data/base64-with-whitespace.eml46
-rw-r--r--comm/mailnews/test/data/basic13
-rw-r--r--comm/mailnews/test/data/basic1.out1
-rw-r--r--comm/mailnews/test/data/basic23
-rw-r--r--comm/mailnews/test/data/basic2.out1
-rw-r--r--comm/mailnews/test/data/basic33
-rw-r--r--comm/mailnews/test/data/basic3.out1
-rw-r--r--comm/mailnews/test/data/basic42
-rw-r--r--comm/mailnews/test/data/basic4.out1
-rw-r--r--comm/mailnews/test/data/basic54
-rw-r--r--comm/mailnews/test/data/basic5.out3
-rw-r--r--comm/mailnews/test/data/bodySearchCrash52
-rw-r--r--comm/mailnews/test/data/bodystructurebug24474172
-rw-r--r--comm/mailnews/test/data/bodystructurebug24641530
-rw-r--r--comm/mailnews/test/data/bodystructuretest157
-rw-r--r--comm/mailnews/test/data/bodystructuretest231
-rw-r--r--comm/mailnews/test/data/bodystructuretest356
-rw-r--r--comm/mailnews/test/data/bug13234026
-rw-r--r--comm/mailnews/test/data/bug460636126
-rw-r--r--comm/mailnews/test/data/bug50522175
-rw-r--r--comm/mailnews/test/data/bug51354358
-rw-r--r--comm/mailnews/test/data/bug92111103
-rw-r--r--comm/mailnews/test/data/bug92111b44
-rw-r--r--comm/mailnews/test/data/bugmail-172
-rw-r--r--comm/mailnews/test/data/bugmail-1.msf1
-rw-r--r--comm/mailnews/test/data/bugmail175
-rw-r--r--comm/mailnews/test/data/bugmail1061
-rw-r--r--comm/mailnews/test/data/bugmail1147
-rw-r--r--comm/mailnews/test/data/bugmail12101
-rw-r--r--comm/mailnews/test/data/bugmail1961
-rw-r--r--comm/mailnews/test/data/bugmail272
-rw-r--r--comm/mailnews/test/data/bugmail372
-rw-r--r--comm/mailnews/test/data/bugmail472
-rw-r--r--comm/mailnews/test/data/bugmail572
-rw-r--r--comm/mailnews/test/data/bugmail672
-rw-r--r--comm/mailnews/test/data/bugmail773
-rw-r--r--comm/mailnews/test/data/bugmail872
-rw-r--r--comm/mailnews/test/data/db-tinderbox-invalid/cert9.dbbin0 -> 36864 bytes
-rw-r--r--comm/mailnews/test/data/db-tinderbox-invalid/key4.dbbin0 -> 294912 bytes
-rw-r--r--comm/mailnews/test/data/draft136
-rw-r--r--comm/mailnews/test/data/external-attach-test63
-rw-r--r--comm/mailnews/test/data/image-attach-test62
-rw-r--r--comm/mailnews/test/data/iso-2022-jp-not-qp.eml14
-rw-r--r--comm/mailnews/test/data/iso-2022-jp-qp.eml14
-rw-r--r--comm/mailnews/test/data/key4.dbbin0 -> 294912 bytes
-rw-r--r--comm/mailnews/test/data/mail-without-from8
-rw-r--r--comm/mailnews/test/data/mbox_mboxrd15
-rw-r--r--comm/mailnews/test/data/mbox_modern20
-rw-r--r--comm/mailnews/test/data/mbox_unquoted18
-rw-r--r--comm/mailnews/test/data/mime-torture25875
-rw-r--r--comm/mailnews/test/data/multipart-base64-111
-rw-r--r--comm/mailnews/test/data/multipart-base64-1.out1
-rw-r--r--comm/mailnews/test/data/multipart-base64-212
-rw-r--r--comm/mailnews/test/data/multipart-base64-2.out1
-rw-r--r--comm/mailnews/test/data/multipart-base64-310
-rw-r--r--comm/mailnews/test/data/multipart-base64-3.out1
-rw-r--r--comm/mailnews/test/data/multipart-complex135
-rw-r--r--comm/mailnews/test/data/multipart-complex1.out1
-rw-r--r--comm/mailnews/test/data/multipart-complex262
-rw-r--r--comm/mailnews/test/data/multipart-complex2.out1
-rw-r--r--comm/mailnews/test/data/multipart-message-1.eml42
-rw-r--r--comm/mailnews/test/data/multipart-message-2.eml42
-rw-r--r--comm/mailnews/test/data/multipart-message-3.eml57
-rw-r--r--comm/mailnews/test/data/multipart-message-4.eml57
-rw-r--r--comm/mailnews/test/data/multipart112
-rw-r--r--comm/mailnews/test/data/multipart1.out1
-rw-r--r--comm/mailnews/test/data/multipart213
-rw-r--r--comm/mailnews/test/data/multipart2.out2
-rw-r--r--comm/mailnews/test/data/multipart329
-rw-r--r--comm/mailnews/test/data/multipart3.out1
-rw-r--r--comm/mailnews/test/data/multipart47
-rw-r--r--comm/mailnews/test/data/multipart4.out1
-rw-r--r--comm/mailnews/test/data/multipartmalt-detach49
-rw-r--r--comm/mailnews/test/data/readme.txt103
-rw-r--r--comm/mailnews/test/data/reply-filter-testmail22
-rw-r--r--comm/mailnews/test/data/signons-mailnews1.8-alt.json39
-rw-r--r--comm/mailnews/test/data/signons-mailnews1.8-imap.json23
-rw-r--r--comm/mailnews/test/data/signons-mailnews1.8-multiple.json71
-rw-r--r--comm/mailnews/test/data/signons-mailnews1.8.json55
-rw-r--r--comm/mailnews/test/data/signons-smtp.json39
-rw-r--r--comm/mailnews/test/data/smime/Alice.p12bin0 -> 3670 bytes
-rw-r--r--comm/mailnews/test/data/smime/Bob.p12bin0 -> 3666 bytes
-rw-r--r--comm/mailnews/test/data/smime/Dave.p12bin0 -> 3668 bytes
-rw-r--r--comm/mailnews/test/data/smime/Eve.p12bin0 -> 3730 bytes
-rw-r--r--comm/mailnews/test/data/smime/README.md23
-rw-r--r--comm/mailnews/test/data/smime/TestCA.pem21
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart54
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.bad.eml59
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.eml59
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.envbin0 -> 3387 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env.eml84
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent55
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent.eml61
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart54
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.bad.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.envbin0 -> 3419 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env.eml85
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent56
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent.eml61
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart54
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.bad.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.envbin0 -> 3451 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env.eml85
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent56
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent.eml62
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart55
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.bad.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.envbin0 -> 3467 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env.eml86
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent56
-rw-r--r--comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent.eml62
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml128
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.eml75
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml129
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.eml76
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml130
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.eml76
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml131
-rw-r--r--comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.eml76
-rw-r--r--comm/mailnews/test/data/smime/alice.env.eml26
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml131
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.eml70
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml132
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.eml71
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml132
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.eml71
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml133
-rw-r--r--comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.eml72
-rw-r--r--comm/mailnews/test/data/smime/alice.future.dsig.SHA256.multipart.eml60
-rw-r--r--comm/mailnews/test/data/smime/alice.mime5
-rw-r--r--comm/mailnews/test/data/smime/alice.mime.dsig.SHA1bin0 -> 1641 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.mime.dsig.SHA256bin0 -> 1661 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.mime.dsig.SHA384bin0 -> 1677 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.mime.dsig.SHA512bin0 -> 1693 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.mime.envbin0 -> 650 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml108
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml107
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml110
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml108
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml110
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml109
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml110
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml110
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml99
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml102
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml100
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml103
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml101
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml103
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml101
-rw-r--r--comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml104
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA1.opaque42
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.eml49
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.envbin0 -> 3035 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env.eml77
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA256.opaque43
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.eml49
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.envbin0 -> 3067 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env.eml77
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA384.opaque43
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.eml50
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.envbin0 -> 3083 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env.eml78
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA512.opaque43
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.eml50
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.envbin0 -> 3115 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env.eml78
-rw-r--r--comm/mailnews/test/data/smime/alice.textplain3
-rw-r--r--comm/mailnews/test/data/smime/alice.textplain.sig.SHA1bin0 -> 1722 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.textplain.sig.SHA256bin0 -> 1742 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.textplain.sig.SHA384bin0 -> 1758 bytes
-rw-r--r--comm/mailnews/test/data/smime/alice.textplain.sig.SHA512bin0 -> 1774 bytes
-rw-r--r--comm/mailnews/test/data/smime/expiration.txt1
-rwxr-xr-xcomm/mailnews/test/data/smime/generate.sh79
-rwxr-xr-xcomm/mailnews/test/data/smime/local-gen.sh97
-rw-r--r--comm/mailnews/test/data/tb2hexpopularity.sql76
-rw-r--r--comm/mailnews/test/data/template-latin130
-rw-r--r--comm/mailnews/test/data/template-utf833
-rw-r--r--comm/mailnews/test/data/test_virtualFolders.dat14
-rw-r--r--comm/mailnews/test/fakeserver/Auth.jsm209
-rw-r--r--comm/mailnews/test/fakeserver/Binaryd.jsm250
-rw-r--r--comm/mailnews/test/fakeserver/Imapd.jsm2544
-rw-r--r--comm/mailnews/test/fakeserver/Ldapd.jsm665
-rw-r--r--comm/mailnews/test/fakeserver/Maild.jsm566
-rw-r--r--comm/mailnews/test/fakeserver/Nntpd.jsm631
-rw-r--r--comm/mailnews/test/fakeserver/Pop3d.jsm454
-rw-r--r--comm/mailnews/test/fakeserver/Smtpd.jsm274
-rw-r--r--comm/mailnews/test/resources/.eslintrc.js5
-rw-r--r--comm/mailnews/test/resources/IMAPpump.jsm143
-rw-r--r--comm/mailnews/test/resources/LocalAccountUtils.jsm195
-rw-r--r--comm/mailnews/test/resources/MailTestUtils.jsm611
-rw-r--r--comm/mailnews/test/resources/MessageGenerator.jsm1651
-rw-r--r--comm/mailnews/test/resources/MessageInjection.jsm987
-rw-r--r--comm/mailnews/test/resources/NetworkTestUtils.jsm294
-rw-r--r--comm/mailnews/test/resources/POP3pump.js269
-rw-r--r--comm/mailnews/test/resources/PromiseTestUtils.jsm316
-rw-r--r--comm/mailnews/test/resources/abSetup.js80
-rw-r--r--comm/mailnews/test/resources/alertTestUtils.js487
-rw-r--r--comm/mailnews/test/resources/filterTestUtils.js89
-rw-r--r--comm/mailnews/test/resources/folderEventLogHelper.js0
-rw-r--r--comm/mailnews/test/resources/logHelper.js567
-rw-r--r--comm/mailnews/test/resources/mailShutdown.js51
-rw-r--r--comm/mailnews/test/resources/msgFolderListenerSetup.js429
-rw-r--r--comm/mailnews/test/resources/passwordStorage.js23
-rw-r--r--comm/mailnews/test/resources/searchTestUtils.js134
-rw-r--r--comm/mailnews/test/resources/smimeUtils.jsm71
2219 files changed, 568173 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/.eslintrc.js b/comm/mailnews/addrbook/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/addrbook/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.js b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js
new file mode 100644
index 0000000000..6c921d9285
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js
@@ -0,0 +1,103 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gOkButton;
+var gNameInput;
+var gDirectory = null;
+
+var kPersonalAddressbookURI = "jsaddrbook://abook.sqlite";
+var kCollectedAddressbookURI = "jsaddrbook://history.sqlite";
+var kAllDirectoryRoot = "moz-abdirectory://";
+
+window.addEventListener("DOMContentLoaded", abNameOnLoad);
+
+function abNameOnLoad() {
+ // Get the document elements.
+ gOkButton = document.querySelector("dialog").getButton("accept");
+ gNameInput = document.getElementById("name");
+
+ // look in arguments[0] for parameters to see if we have a directory or not
+ if (
+ "arguments" in window &&
+ window.arguments[0] &&
+ "selectedDirectory" in window.arguments[0]
+ ) {
+ gDirectory = window.arguments[0].selectedDirectory;
+ gNameInput.value = gDirectory.dirName;
+ }
+
+ // Work out the window title (if we have a directory specified, then it's a
+ // rename).
+ var bundle = document.getElementById("bundle_addressBook");
+
+ if (gDirectory) {
+ let oldListName = gDirectory.dirName;
+ document.title = bundle.getFormattedString("addressBookTitleEdit", [
+ oldListName,
+ ]);
+ } else {
+ document.title = bundle.getString("addressBookTitleNew");
+ }
+
+ if (
+ gDirectory &&
+ (gDirectory.URI == kCollectedAddressbookURI ||
+ gDirectory.URI == kPersonalAddressbookURI ||
+ gDirectory.URI == kAllDirectoryRoot + "?")
+ ) {
+ // Address book name is not editable, therefore disable the field and
+ // only have an ok button that doesn't do anything.
+ gNameInput.readOnly = true;
+ document.querySelector("dialog").buttons = "accept";
+ } else {
+ document.addEventListener("dialogaccept", abNameOKButton);
+ gNameInput.focus();
+ abNameDoOkEnabling();
+ }
+}
+
+function abNameOKButton(event) {
+ let newDirName = gNameInput.value.trim();
+
+ // Do not allow an already existing name.
+ if (
+ MailServices.ab.directoryNameExists(newDirName) &&
+ (!gDirectory || newDirName != gDirectory.dirName)
+ ) {
+ const kAlertTitle = document
+ .getElementById("bundle_addressBook")
+ .getString("duplicateNameTitle");
+ const kAlertText = document
+ .getElementById("bundle_addressBook")
+ .getFormattedString("duplicateNameText", [newDirName]);
+ Services.prompt.alert(window, kAlertTitle, kAlertText);
+ event.preventDefault();
+ return;
+ }
+
+ // Either create a new directory or update an existing one depending on what
+ // we were given when we started.
+ if (gDirectory) {
+ gDirectory.dirName = newDirName;
+ } else {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ newDirName,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ window.arguments[0].newDirectoryUID = directory.UID;
+ if ("onNewDirectory" in window.arguments[0]) {
+ window.arguments[0].onNewDirectory(directory);
+ }
+ }
+}
+
+function abNameDoOkEnabling() {
+ gOkButton.disabled = gNameInput.value.trim() == "";
+}
diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml
new file mode 100644
index 0000000000..3fc0d7d9e1
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="min-width: 540px; min-height: 210px"
+>
+ <head>
+ <title><!-- addressBookTitleEdit --></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abAddressBookNameDialog.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog>
+ <xul:stringbundle
+ id="bundle_addressBook"
+ src="chrome://messenger/locale/addressbook/addressBook.properties"
+ />
+
+ <div class="input-container">
+ <label id="nameLabel" for="name">&name.label;</label>
+ <input
+ id="name"
+ type="text"
+ class="input-inline"
+ oninput="abNameDoOkEnabling();"
+ />
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.js b/comm/mailnews/addrbook/content/abCardDAVDialog.js
new file mode 100644
index 0000000000..8ccce89680
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVDialog.js
@@ -0,0 +1,229 @@
+/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CardDAVUtils: "resource:///modules/CardDAVUtils.jsm",
+ MailServices: "resource:///modules/MailServices.jsm",
+});
+
+var log = console.createInstance({
+ prefix: "carddav.setup",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "carddav.setup.loglevel",
+});
+
+var oAuth = null;
+var callbacks = null;
+var uiElements = {};
+var userContextId;
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ for (let id of [
+ "username",
+ "location",
+ "statusArea",
+ "statusImage",
+ "statusMessage",
+ "resultsArea",
+ "availableBooks",
+ ]) {
+ uiElements[id] = document.getElementById("carddav-" + id);
+ }
+ },
+ { once: true }
+);
+window.addEventListener(
+ "DOMContentLoaded",
+ async () => {
+ await document.l10n.translateRoots();
+ fillLocationPlaceholder();
+ setStatus();
+ },
+ { once: true }
+);
+
+/**
+ * Update the placeholder text for the network location field. If the username
+ * is a valid email address use the domain part of the username, otherwise use
+ * the default placeholder.
+ */
+function fillLocationPlaceholder() {
+ let parts = uiElements.username.value.split("@");
+ let domain = parts.length == 2 && parts[1] ? parts[1] : null;
+
+ if (domain) {
+ uiElements.location.setAttribute("placeholder", domain);
+ } else {
+ uiElements.location.setAttribute(
+ "placeholder",
+ uiElements.location.getAttribute("default-placeholder")
+ );
+ }
+}
+
+function handleCardDAVURLInput(event) {
+ changeCardDAVURL();
+}
+
+function changeCardDAVURL() {
+ uiElements.resultsArea.hidden = true;
+ setStatus();
+}
+
+function handleCardDAVURLBlur(event) {
+ if (
+ uiElements.location.validity.typeMismatch &&
+ !uiElements.location.value.match(/^https?:\/\//)
+ ) {
+ uiElements.location.value = `https://${uiElements.location.value}`;
+ }
+}
+
+async function check() {
+ // We might be accepting the dialog by pressing Enter in the URL input.
+ handleCardDAVURLBlur();
+
+ let username = uiElements.username.value;
+
+ if (!uiElements.location.validity.valid && !username.split("@")[1]) {
+ log.error(`Invalid URL: "${uiElements.location.value}"`);
+ return;
+ }
+
+ let url = uiElements.location.value || username.split("@")[1];
+ if (!url.match(/^https?:\/\//)) {
+ url = "https://" + url;
+ }
+
+ setStatus("loading", "carddav-loading");
+ while (uiElements.availableBooks.lastChild) {
+ uiElements.availableBooks.lastChild.remove();
+ }
+
+ let foundBooks;
+ try {
+ foundBooks = await CardDAVUtils.detectAddressBooks(
+ username,
+ undefined,
+ url,
+ true
+ );
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ setStatus("error", "carddav-known-incompatible", {
+ url: new URL(url).hostname,
+ });
+ } else {
+ log.error(ex);
+ setStatus("error", "carddav-connection-error");
+ }
+ return;
+ }
+
+ // Create a list of CardDAV directories that already exist.
+ let existing = [];
+ for (let d of MailServices.ab.directories) {
+ if (d.dirType == Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE) {
+ existing.push(d.getStringValue("carddav.url", ""));
+ }
+ }
+
+ // Display a checkbox for each directory that doesn't already exist.
+ let alreadyAdded = 0;
+ for (let book of foundBooks) {
+ if (existing.includes(book.url.href)) {
+ alreadyAdded++;
+ continue;
+ }
+ let checkbox = uiElements.availableBooks.appendChild(
+ document.createXULElement("checkbox")
+ );
+ checkbox.setAttribute("label", book.name);
+ checkbox.checked = true;
+ checkbox.value = book.url.href;
+ checkbox._book = book;
+ }
+
+ if (uiElements.availableBooks.childElementCount == 0) {
+ if (alreadyAdded > 0) {
+ setStatus("error", "carddav-already-added");
+ } else {
+ setStatus("error", "carddav-none-found");
+ }
+ } else {
+ uiElements.resultsArea.hidden = false;
+ setStatus();
+ }
+}
+
+function setStatus(status, message, args) {
+ uiElements.username.disabled = status == "loading";
+ uiElements.location.disabled = status == "loading";
+
+ switch (status) {
+ case "loading":
+ uiElements.statusImage.setAttribute(
+ "src",
+ "chrome://global/skin/icons/loading.png"
+ );
+ uiElements.statusImage.setAttribute(
+ "srcset",
+ "chrome://global/skin/icons/loading@2x.png 2x"
+ );
+ break;
+ case "error":
+ uiElements.statusImage.setAttribute(
+ "src",
+ "chrome://global/skin/icons/warning.svg"
+ );
+ uiElements.statusImage.removeAttribute("srcset");
+ break;
+ default:
+ uiElements.statusImage.removeAttribute("src");
+ uiElements.statusImage.removeAttribute("srcset");
+ break;
+ }
+
+ if (status) {
+ uiElements.statusArea.setAttribute("status", status);
+ document.l10n.setAttributes(uiElements.statusMessage, message, args);
+ } else {
+ uiElements.statusArea.removeAttribute("status");
+ uiElements.statusMessage.removeAttribute("data-l10n-id");
+ uiElements.statusMessage.textContent = "";
+ }
+
+ // Grow to fit the list of books. Uses `resizeBy` because it has special
+ // handling in SubDialog.jsm that the other resize functions don't have.
+ window.resizeBy(0, Math.min(250, uiElements.availableBooks.scrollHeight));
+ window.dispatchEvent(new CustomEvent("status-changed"));
+}
+
+window.addEventListener("dialogaccept", event => {
+ if (uiElements.resultsArea.hidden) {
+ event.preventDefault();
+ check();
+ return;
+ }
+
+ if (uiElements.availableBooks.childElementCount == 0) {
+ return;
+ }
+
+ for (let checkbox of uiElements.availableBooks.children) {
+ if (checkbox.checked) {
+ let book = checkbox._book.create();
+ if (window.arguments[0]) {
+ // Pass the UID of the book back to the opening window.
+ window.arguments[0].newDirectoryUID = book.UID;
+ }
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml
new file mode 100644
index 0000000000..9e0fc5e1e4
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="min-width: 540px; min-height: 210px"
+>
+ <head>
+ <title data-l10n-id="carddav-window-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/addressbook/abCardDAVDialog.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abCardDAVDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ id="carddav-dialog"
+ data-l10n-id="carddav-dialog"
+ data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept"
+ >
+ <html:div id="carddav-properties-table">
+ <html:div>
+ <label
+ control="carddav-username"
+ data-l10n-id="carddav-username-label"
+ data-l10n-attrs="value,accesskey"
+ />
+ </html:div>
+ <html:div class="input-container">
+ <html:input
+ id="carddav-username"
+ type="text"
+ oninput="fillLocationPlaceholder();"
+ />
+ </html:div>
+ <html:div>
+ <label
+ control="carddav-location"
+ data-l10n-id="carddav-location-label"
+ data-l10n-attrs="value,accesskey"
+ />
+ </html:div>
+ <html:div class="input-container">
+ <html:input
+ id="carddav-location"
+ type="url"
+ data-l10n-id="carddav-location"
+ data-l10n-attrs="default-placeholder"
+ required="required"
+ onblur="handleCardDAVURLBlur(event);"
+ oninput="handleCardDAVURLInput(event);"
+ />
+ </html:div>
+ </html:div>
+
+ <html:div id="carddav-statusArea">
+ <html:div id="carddav-statusContainer">
+ <html:img id="carddav-statusImage" alt="" />
+ <html:span id="carddav-statusMessage">&#160;</html:span>
+ <!-- Include 160 = nbsp, to make the element occupy the
+ full height, for at least one line. With a normal space,
+ it does not have sufficient height. -->
+ </html:div>
+ </html:div>
+
+ <vbox id="carddav-resultsArea" hidden="true" flex="1">
+ <label
+ id="carddav-availableBooksHeader"
+ data-l10n-id="carddav-available-books"
+ />
+ <vbox id="carddav-availableBooks" class="indent"></vbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.js b/comm/mailnews/addrbook/content/abCardDAVProperties.js
new file mode 100644
index 0000000000..3b2bddf2c4
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVProperties.js
@@ -0,0 +1,140 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDirectory = window.arguments[0].selectedDirectory;
+var gStringBundle,
+ gNameInput,
+ gURLInput,
+ gRefreshActiveInput,
+ gRefreshMenulist,
+ gReadOnlyInput,
+ gAcceptButton;
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ document.title = gStringBundle.formatStringFromName(
+ "addressBookTitleEdit",
+ [gDirectory.dirName]
+ );
+
+ gNameInput = document.getElementById("carddav-name");
+ gNameInput.value = gDirectory.dirName;
+ gNameInput.addEventListener("input", () => {
+ gAcceptButton.disabled = gNameInput.value.trim() == "";
+ });
+
+ gURLInput = document.getElementById("carddav-url");
+ gURLInput.value = gDirectory.getStringValue("carddav.url", "");
+
+ gRefreshActiveInput = document.getElementById("carddav-refreshActive");
+ gRefreshActiveInput.addEventListener(
+ "command",
+ () => (gRefreshMenulist.disabled = !gRefreshActiveInput.checked)
+ );
+
+ gRefreshMenulist = document.getElementById("carddav-refreshInterval");
+ initRefreshInterval();
+
+ gReadOnlyInput = document.getElementById("carddav-readOnly");
+ gReadOnlyInput.checked = gDirectory.readOnly;
+
+ gAcceptButton = document.querySelector("dialog").getButton("accept");
+ },
+ { once: true }
+);
+
+window.addEventListener("dialogaccept", event => {
+ let newDirName = gNameInput.value.trim();
+ let newSyncInterval = gRefreshActiveInput.checked
+ ? gRefreshMenulist.value
+ : 0;
+
+ if (newDirName != gDirectory.dirName) {
+ // Do not allow an already existing name.
+ if (MailServices.ab.directoryNameExists(newDirName)) {
+ let alertTitle = gStringBundle.GetStringFromName("duplicateNameTitle");
+ let alertText = gStringBundle.formatStringFromName("duplicateNameText", [
+ newDirName,
+ ]);
+ Services.prompt.alert(window, alertTitle, alertText);
+ event.preventDefault();
+ return;
+ }
+
+ gDirectory.dirName = newDirName;
+ }
+
+ if (newSyncInterval != gDirectory.getIntValue("carddav.syncinterval", -1)) {
+ gDirectory.setIntValue("carddav.syncinterval", newSyncInterval);
+ }
+
+ if (gReadOnlyInput.checked != gDirectory.readOnly) {
+ gDirectory.setBoolValue("readOnly", gReadOnlyInput.checked);
+ }
+});
+
+function initRefreshInterval() {
+ function createMenuItem(minutes) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("value", minutes);
+ menuitem.setAttribute("data-l10n-attrs", "label");
+ if (minutes < 60) {
+ document.l10n.setAttributes(
+ menuitem,
+ "carddav-refreshinterval-minutes-value",
+ {
+ minutes,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ menuitem,
+ "carddav-refreshinterval-hours-value",
+ {
+ hours: minutes / 60,
+ }
+ );
+ }
+
+ gRefreshMenulist.menupopup.appendChild(menuitem);
+ if (refreshInterval == minutes) {
+ gRefreshMenulist.value = minutes;
+ foundValue = true;
+ }
+
+ return menuitem;
+ }
+
+ let refreshInterval = gDirectory.getIntValue("carddav.syncinterval", 30);
+ if (refreshInterval === null) {
+ refreshInterval = 30;
+ }
+
+ let foundValue = false;
+
+ for (let min of [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440]) {
+ createMenuItem(min);
+ }
+
+ if (refreshInterval == 0) {
+ gRefreshMenulist.value = 30; // The default.
+ gRefreshMenulist.disabled = true;
+ foundValue = true;
+ } else {
+ gRefreshActiveInput.checked = true;
+ }
+
+ if (!foundValue) {
+ // Special menuitem in case the user changed the value in the config editor.
+ createMenuItem(refreshInterval);
+ }
+}
diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml
new file mode 100644
index 0000000000..b611cbbf43
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="540"
+ height="210"
+ scrolling="false"
+>
+ <head>
+ <title><!-- addressBookTitleEdit --></title>
+ <link
+ rel="localization"
+ href="messenger/addressbook/abCardDAVProperties.ftl"
+ />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abCardDAVProperties.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog id="carddav-properties-dialog">
+ <div id="carddav-properties-table">
+ <div>
+ <xul:label
+ control="carddav-name"
+ value="&name.label;"
+ accesskey="&name.accesskey;"
+ />
+ </div>
+ <div class="input-container">
+ <input id="carddav-name" type="text" class="input-inline" />
+ </div>
+ <div>
+ <xul:label
+ data-l10n-id="carddav-url-label"
+ data-l10n-attrs="value,accesskey"
+ control="carddav-url"
+ />
+ </div>
+ <div class="input-container">
+ <input
+ id="carddav-url"
+ type="url"
+ class="input-inline"
+ readonly="readonly"
+ />
+ </div>
+ <div id="carddav-refreshActive-cell">
+ <xul:checkbox
+ id="carddav-refreshActive"
+ data-l10n-id="carddav-refreshinterval-label"
+ data-l10n-attrs="label,accesskey"
+ control="carddav-refreshInterval"
+ />
+ </div>
+ <div id="carddav-refreshInterval-cell">
+ <xul:menulist id="carddav-refreshInterval">
+ <xul:menupopup>
+ <!-- This will be filled programmatically to reduce the number of needed strings -->
+ </xul:menupopup>
+ </xul:menulist>
+ </div>
+ <div></div>
+ <div>
+ <xul:checkbox
+ id="carddav-readOnly"
+ data-l10n-id="carddav-readonly-label"
+ data-l10n-attrs="label,accesskey"
+ />
+ </div>
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abDragDrop.js b/comm/mailnews/addrbook/content/abDragDrop.js
new file mode 100644
index 0000000000..56cbee0e08
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abDragDrop.js
@@ -0,0 +1,124 @@
+/* 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/. */
+
+/* import-globals-from abResultsPane.js */
+
+// Returns the load context for the current window
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+var abFlavorDataProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]),
+
+ getFlavorData(aTransferable, aFlavor, aData) {
+ if (aFlavor == "application/x-moz-file-promise") {
+ var primitive = {};
+ aTransferable.getTransferData("text/vcard", primitive);
+ var vCard = primitive.value.QueryInterface(Ci.nsISupportsString).data;
+ aTransferable.getTransferData(
+ "application/x-moz-file-promise-dest-filename",
+ primitive
+ );
+ var leafName = primitive.value.QueryInterface(Ci.nsISupportsString).data;
+ aTransferable.getTransferData(
+ "application/x-moz-file-promise-dir",
+ primitive
+ );
+ var localFile = primitive.value.QueryInterface(Ci.nsIFile).clone();
+ localFile.append(leafName);
+
+ var ofStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ofStream.init(localFile, -1, -1, 0);
+ var converter = Cc[
+ "@mozilla.org/intl/converter-output-stream;1"
+ ].createInstance(Ci.nsIConverterOutputStream);
+ converter.init(ofStream, null);
+ converter.writeString(vCard);
+ converter.close();
+
+ aData.value = localFile;
+ }
+ },
+};
+
+let abResultsPaneObserver = {
+ onDragStart(event) {
+ let selectedRows = GetSelectedRows();
+
+ if (!selectedRows) {
+ return;
+ }
+
+ let selectedAddresses = GetSelectedAddresses();
+
+ event.dataTransfer.setData("moz/abcard", selectedRows);
+ event.dataTransfer.setData("moz/abcard", selectedRows);
+ event.dataTransfer.setData("text/x-moz-address", selectedAddresses);
+ event.dataTransfer.setData("text/plain", selectedAddresses);
+
+ let card = GetSelectedCard();
+ if (card && card.displayName && !card.isMailList) {
+ try {
+ // A card implementation may throw NS_ERROR_NOT_IMPLEMENTED.
+ // Don't break drag-and-drop if that happens.
+ let vCard = card.translateTo("vcard");
+ event.dataTransfer.setData("text/vcard", decodeURIComponent(vCard));
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise-dest-filename",
+ `${card.displayName}.vcf`.replace(/(.{74}).*(.{10})$/u, "$1...$2")
+ );
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise-url",
+ "data:text/vcard," + vCard
+ );
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise",
+ abFlavorDataProvider
+ );
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ event.dataTransfer.effectAllowed = "copyMove";
+ // a drag targeted at a tree should instead use the treechildren so that
+ // the current selection is used as the drag feedback
+ event.dataTransfer.addElement(event.target);
+ event.stopPropagation();
+ },
+};
+
+function DragAddressOverTargetControl(event) {
+ var dragSession = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession();
+
+ if (!dragSession.isDataFlavorSupported("text/x-moz-address")) {
+ return;
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+
+ var canDrop = true;
+
+ for (var i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ var dataObj = {};
+ var bestFlavor = {};
+ try {
+ trans.getAnyTransferData(bestFlavor, dataObj);
+ } catch (ex) {
+ canDrop = false;
+ break;
+ }
+ }
+ dragSession.canDrop = canDrop;
+}
diff --git a/comm/mailnews/addrbook/content/abMailListDialog.js b/comm/mailnews/addrbook/content/abMailListDialog.js
new file mode 100644
index 0000000000..6d512d3014
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abMailListDialog.js
@@ -0,0 +1,961 @@
+/* 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/. */
+
+/* import-globals-from ../../../mail/components/addrbook/content/abCommon.js */
+/* import-globals-from ../../../mail/components/compose/content/addressingWidgetOverlay.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+top.MAX_RECIPIENTS = 1;
+
+var gListCard;
+var gEditList;
+var gOldListName = "";
+
+var gAWContentHeight = 0;
+var gAWRowHeight = 0;
+var gNumberOfCols = 0;
+
+var test_addresses_sequence = false;
+
+if (
+ Services.prefs.getPrefType("mail.debug.test_addresses_sequence") ==
+ Ci.nsIPrefBranch.PREF_BOOL
+) {
+ test_addresses_sequence = Services.prefs.getBoolPref(
+ "mail.debug.test_addresses_sequence"
+ );
+}
+
+try {
+ var gDragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+} catch (e) {}
+
+// Returns the load context for the current window
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function mailingListExists(listname) {
+ if (MailServices.ab.mailListNameExists(listname)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ Services.prompt.alert(
+ window,
+ bundle.GetStringFromName("mailListNameExistsTitle"),
+ bundle.GetStringFromName("mailListNameExistsMessage")
+ );
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Get the new inputs from the create/edit mailing list dialog and use them to
+ * update the mailing list that was passed in as an argument.
+ *
+ * @param {nsIAbDirectory} mailList - The mailing list object to update. When
+ * creating a new list it will be newly created and empty.
+ * @param {boolean} isNewList - Whether we are populating a new list.
+ * @returns {boolean} - Whether the operation succeeded or not.
+ */
+function updateMailList(mailList, isNewList) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ let listname = document.getElementById("ListName").value.trim();
+
+ if (listname.length == 0) {
+ alert(bundle.GetStringFromName("emptyListName"));
+ return false;
+ }
+
+ if (listname.match(" ")) {
+ alert(bundle.GetStringFromName("badListNameSpaces"));
+ return false;
+ }
+
+ for (let char of ',;"<>') {
+ if (listname.includes(char)) {
+ alert(bundle.GetStringFromName("badListNameCharacters"));
+ return false;
+ }
+ }
+
+ let canonicalNewListName = listname.toLowerCase();
+ let canonicalOldListName = gOldListName.toLowerCase();
+ if (isNewList || canonicalOldListName != canonicalNewListName) {
+ if (mailingListExists(listname)) {
+ // After showing the "Mailing List Already Exists" error alert,
+ // focus ListName input field for user to choose a different name.
+ document.getElementById("ListName").focus();
+ return false;
+ }
+ }
+
+ mailList.isMailList = true;
+ mailList.dirName = listname;
+ mailList.listNickName = document.getElementById("ListNickName").value;
+ mailList.description = document.getElementById("ListDescription").value;
+
+ return true;
+}
+
+/**
+ * Updates the members of the mailing list.
+ *
+ * @param {nsIAbDirectory} mailList - The mailing list object to
+ * update. When creating a new list it will be newly created and empty.
+ * @param {nsIAbDirectory} parentDirectory - The address book containing the
+ * mailing list.
+ */
+function updateMailListMembers(mailList, parentDirectory) {
+ // Gather email address inputs into a single string (comma-separated).
+ let addresses = Array.from(
+ document.querySelectorAll(".textbox-addressingWidget"),
+ element => element.value
+ )
+ .filter(value => value.trim())
+ .join();
+
+ // Convert the addresses string into address objects.
+ let addressObjects =
+ MailServices.headerParser.makeFromDisplayAddress(addresses);
+ let existingCards = mailList.childCards;
+
+ // Work out which addresses need to be added...
+ let existingCardAddresses = existingCards.map(card => card.primaryEmail);
+ let addressObjectsToAdd = addressObjects.filter(
+ aObj => !existingCardAddresses.includes(aObj.email)
+ );
+
+ // ... and which need to be removed.
+ let addressObjectAddresses = addressObjects.map(aObj => aObj.email);
+ let cardsToRemove = existingCards.filter(
+ card => !addressObjectAddresses.includes(card.primaryEmail)
+ );
+
+ for (let { email, name } of addressObjectsToAdd) {
+ let card = parentDirectory.cardForEmailAddress(email);
+ if (!card) {
+ card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.primaryEmail = email;
+ card.displayName = name || email;
+ }
+ mailList.addCard(card);
+ }
+
+ if (cardsToRemove.length > 0) {
+ mailList.deleteCards(cardsToRemove);
+ }
+}
+
+function MailListOKButton(event) {
+ var popup = document.getElementById("abPopup");
+ if (popup) {
+ var uri = popup.getAttribute("value");
+
+ // FIX ME - hack to avoid crashing if no ab selected because of blank option bug from template
+ // should be able to just remove this if we are not seeing blank lines in the ab popup
+ if (!uri) {
+ event.preventDefault();
+ return; // don't close window
+ }
+ // -----
+
+ // Add mailing list to database
+ var mailList =
+ Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance();
+ mailList = mailList.QueryInterface(Ci.nsIAbDirectory);
+
+ if (updateMailList(mailList, true)) {
+ var parentDirectory = GetDirectoryFromURI(uri);
+ mailList = parentDirectory.addMailList(mailList);
+ updateMailListMembers(mailList, parentDirectory);
+ window.arguments[0].newListUID = mailList.UID;
+ window.arguments[0].newListURI = mailList.URI;
+ } else {
+ event.preventDefault();
+ }
+ }
+}
+
+function OnLoadNewMailList() {
+ var selectedAB = null;
+
+ if ("arguments" in window && window.arguments[0]) {
+ var abURI = window.arguments[0].selectedAB;
+ if (abURI && abURI != kAllDirectoryRoot + "?") {
+ var directory = GetDirectoryFromURI(abURI);
+ if (directory.isMailList) {
+ var parentURI = GetParentDirectoryFromMailingListURI(abURI);
+ if (parentURI) {
+ selectedAB = parentURI;
+ }
+ } else if (directory.readOnly) {
+ selectedAB = kPersonalAddressbookURI;
+ } else {
+ selectedAB = abURI;
+ }
+ }
+
+ let cards = window.arguments[0].cards;
+ if (cards && cards.length > 0) {
+ let listbox = document.getElementById("addressingWidget");
+ let newListBoxNode = listbox.cloneNode(false);
+ let templateNode = listbox.querySelector("richlistitem");
+
+ top.MAX_RECIPIENTS = 0;
+ for (let card of cards) {
+ let address = MailServices.headerParser
+ .makeMailboxObject(card.displayName, card.primaryEmail)
+ .toString();
+ SetInputValue(address, newListBoxNode, templateNode);
+ }
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+ }
+ }
+
+ if (!selectedAB) {
+ selectedAB = kPersonalAddressbookURI;
+ }
+
+ // set popup with address book names
+ var abPopup = document.getElementById("abPopup");
+ abPopup.value = selectedAB;
+
+ AppendNewRowAndSetFocus();
+ awFitDummyRows(1);
+
+ if (AppConstants.MOZ_APP_NAME == "seamonkey") {
+ /* global awDocumentKeyPress */
+ document.addEventListener("keypress", awDocumentKeyPress, true);
+ }
+
+ // focus on first name
+ var listName = document.getElementById("ListName");
+ if (listName) {
+ setTimeout(
+ function (firstTextBox) {
+ firstTextBox.focus();
+ },
+ 0,
+ listName
+ );
+ }
+
+ let input = document.getElementById("addressCol1#1");
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+
+ document.addEventListener("dialogaccept", MailListOKButton);
+}
+
+function EditListOKButton(event) {
+ // edit mailing list in database
+ if (updateMailList(gEditList, false)) {
+ let parentURI = GetParentDirectoryFromMailingListURI(gEditList.URI);
+ let parentDirectory = GetDirectoryFromURI(parentURI);
+ updateMailListMembers(gEditList, parentDirectory);
+ if (gListCard) {
+ // modify the list card (for the results pane) from the mailing list
+ gListCard.displayName = gEditList.dirName;
+ gListCard.lastName = gEditList.dirName;
+ gListCard.setProperty("NickName", gEditList.listNickName);
+ gListCard.setProperty("Notes", gEditList.description);
+ }
+
+ gEditList.editMailListToDatabase(gListCard);
+
+ window.arguments[0].refresh = true;
+ return; // close the window
+ }
+ event.preventDefault();
+}
+
+function OnLoadEditList() {
+ gListCard = window.arguments[0].abCard;
+ var listUri = window.arguments[0].listURI;
+
+ gEditList = GetDirectoryFromURI(listUri);
+
+ document.getElementById("ListName").value = gEditList.dirName;
+ document.getElementById("ListNickName").value = gEditList.listNickName;
+ document.getElementById("ListDescription").value = gEditList.description;
+ gOldListName = gEditList.dirName;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ document.title = bundle.formatStringFromName("mailingListTitleEdit", [
+ gOldListName,
+ ]);
+
+ let cards = gEditList.childCards;
+ if (cards.length > 0) {
+ let listbox = document.getElementById("addressingWidget");
+ let newListBoxNode = listbox.cloneNode(false);
+ let templateNode = listbox.querySelector("richlistitem");
+
+ top.MAX_RECIPIENTS = 0;
+ for (let card of cards) {
+ let address = MailServices.headerParser
+ .makeMailboxObject(card.displayName, card.primaryEmail)
+ .toString();
+ SetInputValue(address, newListBoxNode, templateNode);
+ }
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+ }
+
+ // Is this directory read-only? If so, we now need to set all the fields to
+ // read-only.
+ if (gEditList.readOnly) {
+ const kMailListFields = ["ListName", "ListNickName", "ListDescription"];
+
+ for (let i = 0; i < kMailListFields.length; ++i) {
+ document.getElementById(kMailListFields[i]).readOnly = true;
+ }
+
+ document.querySelector("dialog").buttons = "accept";
+
+ // Getting a sane read-only implementation for the addressing widget would
+ // basically need a separate dialog. Given I'm not sure about the future of
+ // the mailing list dialog in its current state, let's just disable it
+ // completely.
+ document.getElementById("addressingWidget").disabled = true;
+ } else {
+ document.addEventListener("dialogaccept", EditListOKButton);
+ }
+
+ if (AppConstants.MOZ_APP_NAME == "seamonkey") {
+ document.addEventListener("keypress", awDocumentKeyPress, true);
+ }
+
+ // workaround for bug 118337 - for mailing lists that have more rows than fits inside
+ // the display, the value of the textbox inside the new row isn't inherited into the input -
+ // the first row then appears to be duplicated at the end although it is actually empty.
+ // see awAppendNewRow which copies first row and clears it
+ setTimeout(AppendLastRow, 0);
+
+ document.querySelectorAll(`input[is="autocomplete-input"]`).forEach(input => {
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ });
+}
+
+function AppendLastRow() {
+ AppendNewRowAndSetFocus();
+ awFitDummyRows(1);
+
+ // focus on first name
+ let listName = document.getElementById("ListName");
+ if (listName) {
+ listName.focus();
+ }
+}
+
+function AppendNewRowAndSetFocus() {
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+ if (lastInput && lastInput.value) {
+ awAppendNewRow(true);
+ } else {
+ awSetFocusTo(lastInput);
+ }
+}
+
+function SetInputValue(inputValue, parentNode, templateNode) {
+ top.MAX_RECIPIENTS++;
+
+ var newNode = templateNode.cloneNode(true);
+ parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element!
+
+ var input = newNode.querySelector(`input[is="autocomplete-input"]`);
+ let label = newNode.querySelector(`label.person-icon`);
+ if (input) {
+ input.value = inputValue;
+ input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS);
+ label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS);
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ }
+}
+
+function awClickEmptySpace(target, setFocus) {
+ if (target == null || target.localName != "hbox") {
+ return;
+ }
+
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+
+ if (lastInput && lastInput.value) {
+ awAppendNewRow(setFocus);
+ } else if (setFocus) {
+ awSetFocusTo(lastInput);
+ }
+}
+
+function awReturnHit(inputElement) {
+ let row = awGetRowByInputElement(inputElement);
+ if (inputElement.value) {
+ let nextInput = awGetInputElement(row + 1);
+ if (!nextInput) {
+ awAppendNewRow(true);
+ } else {
+ awSetFocusTo(nextInput);
+ }
+ }
+}
+
+function awDeleteRow(rowToDelete) {
+ /* When we delete a row, we must reset the id of others row in order to not break the sequence */
+ var maxRecipients = top.MAX_RECIPIENTS;
+ awRemoveRow(rowToDelete);
+
+ var numberOfCols = awGetNumberOfCols();
+ for (var row = rowToDelete + 1; row <= maxRecipients; row++) {
+ for (var col = 1; col <= numberOfCols; col++) {
+ awGetElementByCol(row, col).setAttribute(
+ "id",
+ "addressCol" + col + "#" + (row - 1)
+ );
+ }
+ }
+
+ awTestRowSequence();
+}
+
+/**
+ * Append a new row.
+ *
+ * @param {boolean} setFocus - Whether to set the focus on the new row.
+ * @returns {Element?} The input element from the new row.
+ */
+function awAppendNewRow(setFocus) {
+ let body = document.getElementById("addressingWidget");
+ let listitem1 = awGetListItem(1);
+ let input;
+ let label;
+
+ if (body && listitem1) {
+ let nextDummy = awGetNextDummyRow();
+ let newNode = listitem1.cloneNode(true);
+ if (nextDummy) {
+ body.replaceChild(newNode, nextDummy);
+ } else {
+ body.appendChild(newNode);
+ }
+
+ top.MAX_RECIPIENTS++;
+
+ input = newNode.querySelector(`input[is="autocomplete-input"]`);
+ label = newNode.querySelector(`label.person-icon`);
+ if (input) {
+ input.value = "";
+ input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS);
+ label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS);
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ }
+ // Focus the new input widget.
+ if (setFocus && input) {
+ awSetFocusTo(input);
+ }
+ }
+ return input;
+}
+
+// functions for accessing the elements in the addressing widget
+
+/**
+ * Returns the recipient inputbox for a row.
+ *
+ * @param {integer} row - Index of the recipient row to return. Starts at 1.
+ * @returns {Element} This returns the input element.
+ */
+function awGetInputElement(row) {
+ return document.getElementById("addressCol1#" + row);
+}
+
+function awGetElementByCol(row, col) {
+ var colID = "addressCol" + col + "#" + row;
+ return document.getElementById(colID);
+}
+
+function awGetListItem(row) {
+ var listbox = document.getElementById("addressingWidget");
+ if (listbox && row > 0) {
+ return listbox.getItemAtIndex(row - 1);
+ }
+
+ return null;
+}
+
+/**
+ * @param {Element} inputElement - The recipient input element.
+ * @returns {integer} The row index (starting from 1) where the input element
+ * is found. 0 if the element is not found.
+ */
+function awGetRowByInputElement(inputElement) {
+ if (!inputElement) {
+ return 0;
+ }
+
+ var listitem = inputElement.parentNode.parentNode;
+ return (
+ document.getElementById("addressingWidget").getIndexOfItem(listitem) + 1
+ );
+}
+
+function DragOverAddressListTree(event) {
+ var dragSession = gDragService.getCurrentSession();
+
+ // XXX add support for other flavors here
+ if (dragSession.isDataFlavorSupported("text/x-moz-address")) {
+ dragSession.canDrop = true;
+ }
+}
+
+function DropOnAddressListTree(event) {
+ let dragSession = gDragService.getCurrentSession();
+ let trans;
+
+ try {
+ trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+ } catch (ex) {
+ return;
+ }
+
+ for (let i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ let dataObj = {};
+ let bestFlavor = {};
+ trans.getAnyTransferData(bestFlavor, dataObj);
+ if (dataObj) {
+ dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString);
+ }
+ if (!dataObj) {
+ continue;
+ }
+
+ // pull the URL out of the data object
+ let address = dataObj.data.substring(0, dataObj.length);
+ if (!address) {
+ continue;
+ }
+
+ DropListAddress(event.target, address);
+ }
+}
+
+function DropListAddress(target, address) {
+ // Set focus on a new available, visible row.
+ awClickEmptySpace(target, true);
+ if (top.MAX_RECIPIENTS == 0) {
+ top.MAX_RECIPIENTS = 1;
+ }
+
+ // Break apart the MIME-ready header address into individual addressees to
+ // add to the dialog.
+ let addresses = MailServices.headerParser.parseEncodedHeader(address);
+ for (let addr of addresses) {
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+ lastInput.value = addr.toString();
+ awAppendNewRow(true);
+ }
+}
+
+/**
+ * Handles keypress events for the email address inputs (that auto-fill)
+ * in the Address Book Mailing List dialogs. When a comma-separated list of
+ * addresses is entered on one row, split them into one address per row. Only
+ * add a new blank row on "Enter" key. On "Tab" key focus moves to the "Cancel"
+ * button.
+ *
+ * @param {KeyboardEvent} event - The DOM keypress event.
+ * @param {Element} element - The element that triggered the keypress event.
+ */
+function awAbRecipientKeyPress(event, element) {
+ if (event.key != "Enter" && event.key != "Tab") {
+ return;
+ }
+
+ if (!element.value) {
+ if (event.key == "Enter") {
+ awReturnHit(element);
+ }
+ } else {
+ let inputElement = element;
+ let originalRow = awGetRowByInputElement(element);
+ let row;
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(
+ element.value
+ );
+
+ if (addresses.length > 1) {
+ // Collect any existing addresses from the following rows so we don't
+ // simply overwrite them.
+ row = originalRow + 1;
+ inputElement = awGetInputElement(row);
+
+ while (inputElement) {
+ if (inputElement.value) {
+ addresses.push(inputElement.value);
+ inputElement.value = "";
+ }
+ row += 1;
+ inputElement = awGetInputElement(row);
+ }
+ }
+
+ // Insert the addresses, adding new rows if needed.
+ row = originalRow;
+ let needNewRows = false;
+
+ for (let address of addresses) {
+ if (needNewRows) {
+ inputElement = awAppendNewRow(false);
+ } else {
+ inputElement = awGetInputElement(row);
+ if (!inputElement) {
+ needNewRows = true;
+ inputElement = awAppendNewRow(false);
+ }
+ }
+
+ if (inputElement) {
+ inputElement.value = address;
+ }
+ row += 1;
+ }
+
+ if (event.key == "Enter") {
+ // Prevent the dialog from closing. "Enter" inserted a new row instead.
+ event.preventDefault();
+ awReturnHit(inputElement);
+ } else if (event.key == "Tab") {
+ // Focus the last row to let "Tab" move focus to the "Cancel" button.
+ let lastRow = row - 1;
+ awGetInputElement(lastRow).focus();
+ }
+ }
+}
+
+/**
+ * Handle keydown event on a recipient input.
+ * Enables recipient row deletion with DEL or BACKSPACE and
+ * recipient list navigation with cursor up/down.
+ *
+ * Note that the keydown event fires for ALL keys, so this may affect
+ * autocomplete as user enters a recipient text.
+ *
+ * @param {KeyboardEvent} event - The keydown event fired on a recipient input.
+ * @param {HTMLInputElement} inputElement - The recipient input element
+ * on which the event fired (textbox-addressingWidget).
+ */
+function awRecipientKeyDown(event, inputElement) {
+ switch (event.key) {
+ // Enable deletion of empty recipient rows.
+ case "Delete":
+ case "Backspace":
+ if (inputElement.value.length == 1 && event.repeat) {
+ // User is holding down Delete or Backspace to delete recipient text
+ // inline and is now deleting the last character: Set flag to
+ // temporarily block row deletion.
+ top.awRecipientInlineDelete = true;
+ }
+ if (!inputElement.value && !event.altKey) {
+ // When user presses DEL or BACKSPACE on an empty row, and it's not an
+ // ongoing inline deletion, and not ALT+BACKSPACE for input undo,
+ // we delete the row.
+ if (top.awRecipientInlineDelete && !event.repeat) {
+ // User has released and re-pressed Delete or Backspace key
+ // after holding them down to delete recipient text inline:
+ // unblock row deletion.
+ top.awRecipientInlineDelete = false;
+ }
+ if (!top.awRecipientInlineDelete) {
+ let deleteForward = event.key == "Delete";
+ awDeleteHit(inputElement, deleteForward);
+ }
+ }
+ break;
+
+ // Enable browsing the list of recipients up and down with cursor keys.
+ case "ArrowDown":
+ case "ArrowUp":
+ // Only browse recipients if the autocomplete popup is not open.
+ if (!inputElement.popupOpen) {
+ let row = awGetRowByInputElement(inputElement);
+ let down = event.key == "ArrowDown";
+ let noEdgeRow = down ? row < top.MAX_RECIPIENTS : row > 1;
+ if (noEdgeRow) {
+ let targetRow = down ? row + 1 : row - 1;
+ awSetFocusTo(awGetInputElement(targetRow));
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Delete recipient row (addressingWidgetItem) from UI.
+ *
+ * @param {HTMLInputElement} inputElement - The recipient input element.
+ * textbox-addressingWidget) whose parent row (addressingWidgetItem) will be
+ * deleted.
+ * @param {boolean} deleteForward - true: focus next row after deleting the row
+ * false: focus previous row after deleting the row
+ */
+function awDeleteHit(inputElement, deleteForward = false) {
+ let row = awGetRowByInputElement(inputElement);
+
+ // Don't delete the row if it's the last one remaining; just reset it.
+ if (top.MAX_RECIPIENTS <= 1) {
+ inputElement.value = "";
+ return;
+ }
+
+ // Set the focus to the input field of the next/previous row according to
+ // the direction of deleting if possible.
+ // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal.
+ if (
+ (!deleteForward && row > 1) ||
+ (deleteForward && row == top.MAX_RECIPIENTS)
+ ) {
+ // We're deleting backwards, but not the first row,
+ // or forwards on the last row: Focus previous row.
+ awSetFocusTo(awGetInputElement(row - 1));
+ } else {
+ // We're deleting forwards, but not the last row,
+ // or backwards on the first row: Focus next row.
+ awSetFocusTo(awGetInputElement(row + 1));
+ }
+
+ // Delete the row.
+ awDeleteRow(row);
+}
+
+function awTestRowSequence() {
+ /*
+ This function is for debug and testing purpose only, normal user should not run it!
+
+ Every time we insert or delete a row, we must be sure we didn't break the ID sequence of
+ the addressing widget rows. This function will run a quick test to see if the sequence still ok
+
+ You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it
+ */
+
+ if (!test_addresses_sequence) {
+ return true;
+ }
+
+ // Debug code to verify the sequence is still good.
+
+ let listbox = document.getElementById("addressingWidget");
+ let listitems = listbox.itemChildren;
+ if (listitems.length >= top.MAX_RECIPIENTS) {
+ for (let i = 1; i <= listitems.length; i++) {
+ let item = listitems[i - 1];
+ let inputID = item
+ .querySelector(`input[is="autocomplete-input"]`)
+ .id.split("#")[1];
+ let menulist = item.querySelector("menulist");
+ // In some places like the mailing list dialog there is no menulist,
+ // and so no popupID that needs to be kept in sequence.
+ let popupID = menulist && menulist.id.split("#")[1];
+ if (inputID != i || (popupID && popupID != i)) {
+ dump(
+ `#ERROR: sequence broken at row ${i}, ` +
+ `inputID=${inputID}, popupID=${popupID}\n`
+ );
+ return false;
+ }
+ dump("---SEQUENCE OK---\n");
+ return true;
+ }
+ } else {
+ dump(
+ `#ERROR: listitems.length(${listitems.length}) < ` +
+ `top.MAX_RECIPIENTS(${top.MAX_RECIPIENTS})\n`
+ );
+ }
+
+ return false;
+}
+
+function awRemoveRow(row) {
+ awGetListItem(row).remove();
+ awFitDummyRows();
+
+ top.MAX_RECIPIENTS--;
+}
+
+function awGetNumberOfCols() {
+ if (gNumberOfCols == 0) {
+ var listbox = document.getElementById("addressingWidget");
+ var listCols = listbox.getElementsByTagName("treecol");
+ gNumberOfCols = listCols.length;
+ if (!gNumberOfCols) {
+ // If no cols defined, that means we have only one!
+ gNumberOfCols = 1;
+ }
+ }
+
+ return gNumberOfCols;
+}
+
+function awCreateDummyItem(aParent) {
+ var listbox = document.getElementById("addressingWidget");
+ var item = listbox.getItemAtIndex(0);
+
+ var titem = document.createXULElement("richlistitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+ titem.style.height = item.getBoundingClientRect().height + "px";
+
+ for (let i = 0; i < awGetNumberOfCols(); i++) {
+ let cell = awCreateDummyCell(titem);
+ if (item.children[i].hasAttribute("style")) {
+ cell.setAttribute("style", item.children[i].getAttribute("style"));
+ }
+ if (item.children[i].hasAttribute("flex")) {
+ cell.setAttribute("flex", item.children[i].getAttribute("flex"));
+ }
+ }
+
+ if (aParent) {
+ aParent.appendChild(titem);
+ }
+
+ return titem;
+}
+
+function awFitDummyRows() {
+ awCalcContentHeight();
+ awCreateOrRemoveDummyRows();
+}
+
+function awCreateOrRemoveDummyRows() {
+ let listbox = document.getElementById("addressingWidget");
+ let listboxHeight = listbox.getBoundingClientRect().height;
+
+ // remove rows to remove scrollbar
+ let kids = listbox.querySelectorAll("[_isDummyRow]");
+ for (
+ let i = kids.length - 1;
+ gAWContentHeight > listboxHeight && i >= 0;
+ --i
+ ) {
+ gAWContentHeight -= gAWRowHeight;
+ kids[i].remove();
+ }
+
+ // add rows to fill space
+ if (gAWRowHeight) {
+ while (gAWContentHeight + gAWRowHeight < listboxHeight) {
+ awCreateDummyItem(listbox);
+ gAWContentHeight += gAWRowHeight;
+ }
+ }
+}
+
+function awCalcContentHeight() {
+ var listbox = document.getElementById("addressingWidget");
+ var items = listbox.itemChildren;
+
+ gAWContentHeight = 0;
+ if (items.length > 0) {
+ // all rows are forced to a uniform height in xul listboxes, so
+ // find the first listitem with a boxObject and use it as precedent
+ var i = 0;
+ do {
+ gAWRowHeight = items[i].getBoundingClientRect().height;
+ ++i;
+ } while (i < items.length && !gAWRowHeight);
+ gAWContentHeight = gAWRowHeight * items.length;
+ }
+}
+
+/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */
+
+function awCreateDummyCell(aParent) {
+ var cell = document.createXULElement("hbox");
+ cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+ if (aParent) {
+ aParent.appendChild(cell);
+ }
+
+ return cell;
+}
+
+function awGetNextDummyRow() {
+ // gets the next row from the top down
+ return document.querySelector("#addressingWidget > [_isDummyRow]");
+}
+
+/**
+ * Set focus to the specified element, typically a recipient input element.
+ * We do this asynchronously to allow other processes like adding or removing rows
+ * to complete before shifting focus.
+ *
+ * @param {Element} element - The element to receive focus asynchronously.
+ */
+function awSetFocusTo(element) {
+ // Remember the (input) element to focus for asynchronous focusing, so that we
+ // play safe if this gets called again and the original element gets removed
+ // before we can focus it.
+ top.awInputToFocus = element;
+ setTimeout(_awSetFocusTo, 0);
+}
+
+function _awSetFocusTo() {
+ top.awInputToFocus.focus();
+}
+
+// returns null if abURI is not a mailing list URI
+function GetParentDirectoryFromMailingListURI(abURI) {
+ var abURIArr = abURI.split("/");
+ /*
+ Turn "jsaddrbook://abook.sqlite/MailList6"
+ into ["jsaddrbook:","","abook.sqlite","MailList6"],
+ then into "jsaddrbook://abook.sqlite".
+
+ Turn "moz-aboutlookdirectory:///<top dir ID>/<ML dir ID>"
+ into ["moz-aboutlookdirectory:","","","<top dir ID>","<ML dir ID>"],
+ and then into: "moz-aboutlookdirectory:///<top dir ID>".
+ */
+ if (
+ abURIArr.length == 4 &&
+ ["jsaddrbook:", "moz-abmdbdirectory:"].includes(abURIArr[0]) &&
+ abURIArr[3] != ""
+ ) {
+ return abURIArr[0] + "//" + abURIArr[2];
+ } else if (
+ abURIArr.length == 5 &&
+ abURIArr[0] == "moz-aboutlookdirectory:" &&
+ abURIArr[4] != ""
+ ) {
+ return abURIArr[0] + "///" + abURIArr[3];
+ }
+
+ return null;
+}
diff --git a/comm/mailnews/addrbook/content/abResultsPane.js b/comm/mailnews/addrbook/content/abResultsPane.js
new file mode 100644
index 0000000000..2a4bd99f3b
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abResultsPane.js
@@ -0,0 +1,482 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ; js-indent-level: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/* import-globals-from ../../../mail/base/content/utilityOverlay.js */
+/* import-globals-from ../../../mail/components/addrbook/content/abCommon.js */
+/* import-globals-from abView.js */
+
+/**
+ * Use of items in this file require:
+ *
+ * AbResultsPaneDoubleClick(card)
+ * Is called when the results pane is double-clicked, with the clicked card.
+ * GetAbViewListener()
+ * Called when creating a new view
+ */
+/* globals AbResultsPaneDoubleClick, GetAbViewListener */ // abContactsPane.js or abSearchDialog.js
+
+var kDefaultSortColumn = "GeneratedName";
+
+// List/card selections in the results pane.
+var kNothingSelected = 0;
+var kListsAndCards = 1;
+var kMultipleListsOnly = 2;
+var kSingleListOnly = 3;
+var kCardsOnly = 4;
+
+// Global Variables
+
+// Holds a reference to the "abResultsTree" document element. Initially
+// set up by SetAbView.
+var gAbResultsTree = null;
+// gAbView is the current value of gAbResultsTree.view, without passing
+// through XPCOM, so we can access extra functions if necessary.
+var gAbView = null;
+
+function SetAbView(aURI, aSearchQuery, aSearchString) {
+ // If we don't have a URI, just clear the view and leave everything else
+ // alone.
+ if (!aURI) {
+ if (gAbView) {
+ CloseAbView();
+ }
+ return;
+ }
+
+ // If we do have a URI, we want to allow updating the review even if the
+ // URI is the same, as the search results may be different.
+
+ var sortColumn = kDefaultSortColumn;
+ var sortDirection = kDefaultAscending;
+
+ if (!gAbResultsTree) {
+ gAbResultsTree = document.getElementById("abResultsTree");
+ gAbResultsTree.controllers.appendController(ResultsPaneController);
+ }
+
+ if (gAbView) {
+ sortColumn = gAbView.sortColumn;
+ sortDirection = gAbView.sortDirection;
+ } else {
+ if (gAbResultsTree.hasAttribute("sortCol")) {
+ sortColumn = gAbResultsTree.getAttribute("sortCol");
+ }
+ var sortColumnNode = document.getElementById(sortColumn);
+ if (sortColumnNode && sortColumnNode.hasAttribute("sortDirection")) {
+ sortDirection = sortColumnNode.getAttribute("sortDirection");
+ }
+ }
+
+ gAbView = gAbResultsTree.view = new ABView(
+ GetDirectoryFromURI(aURI),
+ aSearchQuery,
+ aSearchString,
+ GetAbViewListener(),
+ sortColumn,
+ sortDirection
+ ).QueryInterface(Ci.nsITreeView);
+ window.dispatchEvent(new CustomEvent("viewchange"));
+
+ UpdateSortIndicators(sortColumn, sortDirection);
+
+ // If the selected address book is LDAP and the search box is empty,
+ // inform the user of the empty results pane.
+ let abResultsTree = document.getElementById("abResultsTree");
+ let cardViewOuterBox = document.getElementById("CardViewOuterBox");
+ let blankResultsPaneMessageBox = document.getElementById(
+ "blankResultsPaneMessageBox"
+ );
+ if (aURI.startsWith("moz-abldapdirectory://") && !aSearchQuery) {
+ if (abResultsTree) {
+ abResultsTree.hidden = true;
+ }
+ if (cardViewOuterBox) {
+ cardViewOuterBox.hidden = true;
+ }
+ if (blankResultsPaneMessageBox) {
+ blankResultsPaneMessageBox.hidden = false;
+ }
+ } else {
+ if (abResultsTree) {
+ abResultsTree.hidden = false;
+ }
+ if (cardViewOuterBox) {
+ cardViewOuterBox.hidden = false;
+ }
+ if (blankResultsPaneMessageBox) {
+ blankResultsPaneMessageBox.hidden = true;
+ }
+ }
+}
+
+function CloseAbView() {
+ gAbView = null;
+ if (gAbResultsTree) {
+ gAbResultsTree.view = null;
+ }
+}
+
+function GetSelectedAddresses() {
+ return GetAddressesForCards(GetSelectedAbCards());
+}
+
+function GetNumSelectedCards() {
+ try {
+ return gAbView.selection.count;
+ } catch (ex) {}
+
+ // if something went wrong, return 0 for the count.
+ return 0;
+}
+
+function GetSelectedCardTypes() {
+ var cards = GetSelectedAbCards();
+ if (!cards) {
+ console.error("ERROR: GetSelectedCardTypes: |cards| is null.");
+ return kNothingSelected; // no view
+ }
+ var count = cards.length;
+ if (count == 0) {
+ // Nothing selected.
+ return kNothingSelected;
+ }
+
+ var mailingListCnt = 0;
+ var cardCnt = 0;
+ for (let i = 0; i < count; i++) {
+ // We can assume no values from GetSelectedAbCards will be null.
+ if (cards[i].isMailList) {
+ mailingListCnt++;
+ } else {
+ cardCnt++;
+ }
+ }
+
+ if (mailingListCnt == 0) {
+ return kCardsOnly;
+ }
+ if (cardCnt > 0) {
+ return kListsAndCards;
+ }
+ if (mailingListCnt == 1) {
+ return kSingleListOnly;
+ }
+ return kMultipleListsOnly;
+}
+
+// NOTE, will return -1 if more than one card selected, or no cards selected.
+function GetSelectedCardIndex() {
+ if (!gAbView) {
+ return -1;
+ }
+
+ var treeSelection = gAbView.selection;
+ if (treeSelection.getRangeCount() == 1) {
+ var start = {};
+ var end = {};
+ treeSelection.getRangeAt(0, start, end);
+ if (start.value == end.value) {
+ return start.value;
+ }
+ }
+
+ return -1;
+}
+
+// NOTE, returns the card if exactly one card is selected, null otherwise
+function GetSelectedCard() {
+ var index = GetSelectedCardIndex();
+ return index == -1 ? null : gAbView.getCardFromRow(index);
+}
+
+/**
+ * Return a (possibly empty) list of cards
+ *
+ * It pushes only non-null/empty element, if any, into the returned list.
+ */
+function GetSelectedAbCards() {
+ var abView = gAbView;
+
+ if (!abView?.selection) {
+ return [];
+ }
+
+ let cards = [];
+ var count = abView.selection.getRangeCount();
+ for (let i = 0; i < count; ++i) {
+ let start = {};
+ let end = {};
+
+ abView.selection.getRangeAt(i, start, end);
+
+ for (let j = start.value; j <= end.value; ++j) {
+ // avoid inserting null element into the list. GetRangeAt() may be buggy.
+ let tmp = abView.getCardFromRow(j);
+ if (tmp) {
+ cards.push(tmp);
+ }
+ }
+ }
+ return cards;
+}
+
+// XXX todo
+// an optimization might be to make this return
+// the selected ranges, which would be faster
+// when the user does large selections, but for now, let's keep it simple.
+function GetSelectedRows() {
+ var selectedRows = "";
+
+ if (!gAbView) {
+ return selectedRows;
+ }
+
+ var rangeCount = gAbView.selection.getRangeCount();
+ for (let i = 0; i < rangeCount; ++i) {
+ var start = {};
+ var end = {};
+ gAbView.selection.getRangeAt(i, start, end);
+ for (let j = start.value; j <= end.value; ++j) {
+ if (selectedRows) {
+ selectedRows += ",";
+ }
+ selectedRows += j;
+ }
+ }
+
+ return selectedRows;
+}
+
+function AbResultsPaneOnClick(event) {
+ // we only care about button 0 (left click) events
+ if (event.button != 0) {
+ return;
+ }
+
+ // all we need to worry about here is double clicks
+ // and column header clicks.
+ //
+ // we get in here for clicks on the "treecol" (headers)
+ // and the "scrollbarbutton" (scrollbar buttons)
+ // we don't want those events to cause a "double click"
+
+ var t = event.target;
+
+ if (t.localName == "treecol") {
+ var sortDirection;
+ var currentDirection = t.getAttribute("sortDirection");
+
+ // Revert the sort order. If none is set, use Ascending.
+ sortDirection =
+ currentDirection == kDefaultAscending
+ ? kDefaultDescending
+ : kDefaultAscending;
+
+ SortAndUpdateIndicators(t.id, sortDirection);
+ } else if (t.localName == "treechildren") {
+ // figure out what row the click was in
+ var row = gAbResultsTree.getRowAt(event.clientX, event.clientY);
+ if (row == -1) {
+ return;
+ }
+
+ if (event.detail == 2) {
+ AbResultsPaneDoubleClick(gAbView.getCardFromRow(row));
+ }
+ }
+}
+
+function SortAndUpdateIndicators(sortColumn, sortDirection) {
+ UpdateSortIndicators(sortColumn, sortDirection);
+
+ if (gAbView) {
+ gAbView.sortBy(sortColumn, sortDirection);
+ }
+}
+
+function UpdateSortIndicators(colID, sortDirection) {
+ var sortedColumn = null;
+
+ // set the sort indicator on the column we are sorted by
+ if (colID) {
+ sortedColumn = document.getElementById(colID);
+ if (sortedColumn) {
+ sortedColumn.setAttribute("sortDirection", sortDirection);
+ gAbResultsTree.setAttribute("sortCol", colID);
+ }
+ }
+
+ // remove the sort indicator from all the columns
+ // except the one we are sorted by
+ var currCol = gAbResultsTree.firstElementChild.firstElementChild;
+ while (currCol) {
+ if (currCol != sortedColumn && currCol.localName == "treecol") {
+ currCol.removeAttribute("sortDirection");
+ }
+ currCol = currCol.nextElementSibling;
+ }
+}
+
+// Controller object for Results Pane
+var ResultsPaneController = {
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ case "cmd_delete":
+ case "button_delete":
+ case "cmd_print":
+ case "cmd_printcard":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ return true;
+ case "cmd_delete":
+ case "button_delete": {
+ let numSelected;
+ let enabled = false;
+ if (gAbView && gAbView.selection) {
+ if (gAbView.directory) {
+ enabled = !gAbView.directory.readOnly;
+ } else {
+ enabled = true;
+ }
+ numSelected = gAbView.selection.count;
+ } else {
+ numSelected = 0;
+ }
+ enabled = enabled && numSelected > 0;
+
+ if (enabled && !gAbView?.directory) {
+ // Undefined gAbView.directory means "All Address Books" is selected.
+ // Disable the menu/button if any selected card is from a read only
+ // directory.
+ enabled = !GetSelectedAbCards().some(
+ card =>
+ MailServices.ab.getDirectoryFromUID(card.directoryUID).readOnly
+ );
+ }
+
+ if (command == "cmd_delete") {
+ switch (GetSelectedCardTypes()) {
+ case kSingleListOnly:
+ updateDeleteControls("valueList");
+ break;
+ case kMultipleListsOnly:
+ updateDeleteControls("valueLists");
+ break;
+ case kListsAndCards:
+ updateDeleteControls("valueItems");
+ break;
+ case kCardsOnly:
+ default:
+ updateDeleteControls(
+ numSelected < 2 ? "valueCard" : "valueCards"
+ );
+ }
+ }
+ return enabled;
+ }
+ case "cmd_print":
+ // cmd_print is currently only used in SeaMonkey.
+ // Prevent printing when we don't have an opener (browserDOMWindow is
+ // null).
+ let enabled = window.browserDOMWindow && GetNumSelectedCards() > 0;
+ document.querySelectorAll("[command=cmd_print]").forEach(e => {
+ e.disabled = !enabled;
+ });
+ return enabled;
+ case "cmd_printcard":
+ // Prevent printing when we don't have an opener (browserDOMWindow is
+ // null).
+ return window.browserDOMWindow && GetNumSelectedCards() > 0;
+ default:
+ return false;
+ }
+ },
+
+ doCommand(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ if (gAbView) {
+ gAbView.selection.selectAll();
+ }
+ break;
+ case "cmd_delete":
+ case "button_delete":
+ AbDelete();
+ break;
+ }
+ },
+};
+
+function updateDeleteControls(
+ labelAttribute,
+ accessKeyAttribute = "accesskeyDefault"
+) {
+ goSetMenuValue("cmd_delete", labelAttribute);
+ goSetAccessKey("cmd_delete", accessKeyAttribute);
+
+ // The toolbar button doesn't update itself from the command. Do that now.
+ let button = document.getElementById("button-abdelete");
+ if (!button) {
+ return;
+ }
+
+ let command = document.getElementById("cmd_delete");
+ button.label = command.getAttribute("label");
+ button.setAttribute(
+ "tooltiptext",
+ button.getAttribute(
+ labelAttribute == "valueCardDAV" ? "tooltipCardDAV" : "tooltipDefault"
+ )
+ );
+}
+
+/**
+ * Generate a comma separated list of addresses from the given cards.
+ *
+ * @param {nsIAbCard[]} cards - The cards to get addresses for.
+ * @returns {string} A string of comma separated mailboxes.
+ */
+function GetAddressesForCards(cards) {
+ if (!cards) {
+ return "";
+ }
+
+ return cards
+ .map(makeMimeAddressFromCard)
+ .filter(addr => addr)
+ .join(",");
+}
+
+/**
+ * Make a MIME encoded string output of the card. This will make a difference
+ * e.g. in scenarios where non-ASCII is used in the mailbox, or when then
+ * display name include special characters such as comma.
+ *
+ * @param {nsIAbCard} card - The card to use.
+ * @returns {string} A MIME encoded mailbox representation of the card.
+ */
+function makeMimeAddressFromCard(card) {
+ if (!card) {
+ return "";
+ }
+
+ let email;
+ if (card.isMailList) {
+ let directory = GetDirectoryFromURI(card.mailListURI);
+ email = directory.description || card.displayName;
+ } else {
+ email = card.emailAddresses[0];
+ }
+ return MailServices.headerParser.makeMimeAddress(card.displayName, email);
+}
diff --git a/comm/mailnews/addrbook/content/abView.js b/comm/mailnews/addrbook/content/abView.js
new file mode 100644
index 0000000000..f5acb7d11c
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abView.js
@@ -0,0 +1,539 @@
+/* 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/. */
+
+/* globals MailServices, PROTO_TREE_VIEW, Services */
+
+function ABView(
+ directory,
+ searchQuery,
+ searchString,
+ listener,
+ sortColumn,
+ sortDirection
+) {
+ this.__proto__.__proto__ = new PROTO_TREE_VIEW();
+ this.directory = directory;
+ this.listener = listener;
+
+ let directories = directory ? [directory] : MailServices.ab.directories;
+ if (searchQuery) {
+ searchQuery = searchQuery.replace(/^\?+/, "");
+ for (let dir of directories) {
+ dir.search(searchQuery, searchString, this);
+ }
+ } else {
+ for (let dir of directories) {
+ for (let card of dir.childCards) {
+ this._rowMap.push(new abViewCard(card, dir));
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ }
+ this.sortBy(sortColumn, sortDirection);
+}
+ABView.nameFormat = Services.prefs.getIntPref(
+ "mail.addr_book.lastnamefirst",
+ 0
+);
+ABView.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsITreeView",
+ "nsIAbDirSearchListener",
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ directory: null,
+ listener: null,
+ _notifications: [
+ "addrbook-directory-deleted",
+ "addrbook-directory-invalidated",
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-deleted",
+ "addrbook-list-created",
+ "addrbook-list-updated",
+ "addrbook-list-deleted",
+ "addrbook-list-member-added",
+ "addrbook-list-member-removed",
+ ],
+
+ sortColumn: "",
+ sortDirection: "",
+ collator: new Intl.Collator(undefined, { numeric: true }),
+
+ deleteSelectedCards() {
+ let directoryMap = new Map();
+ for (let i = 0; i < this.selection.getRangeCount(); i++) {
+ let start = {};
+ let finish = {};
+ this.selection.getRangeAt(i, start, finish);
+ for (let j = start.value; j <= finish.value; j++) {
+ let card = this.getCardFromRow(j);
+ let cardSet = directoryMap.get(card.directoryUID);
+ if (!cardSet) {
+ cardSet = new Set();
+ directoryMap.set(card.directoryUID, cardSet);
+ }
+ cardSet.add(card);
+ }
+ }
+
+ for (let [directoryUID, cardSet] of directoryMap) {
+ let directory;
+ if (this.directory && this.directory.isMailList) {
+ // Removes cards from the list instead of deleting them.
+ directory = this.directory;
+ } else {
+ directory = MailServices.ab.getDirectoryFromUID(directoryUID);
+ }
+
+ cardSet = [...cardSet];
+ directory.deleteCards(cardSet.filter(card => !card.isMailList));
+ for (let card of cardSet.filter(card => card.isMailList)) {
+ MailServices.ab.deleteAddressBook(card.mailListURI);
+ }
+ }
+ },
+ getCardFromRow(row) {
+ return this._rowMap[row] ? this._rowMap[row].card : null;
+ },
+ getDirectoryFromRow(row) {
+ return this._rowMap[row] ? this._rowMap[row].directory : null;
+ },
+ sortBy(sortColumn, sortDirection, resort) {
+ // Remember what was selected.
+ let selection = this.selection;
+ if (selection) {
+ for (let i = 0; i < this._rowMap.length; i++) {
+ this._rowMap[i].wasSelected = selection.isSelected(i);
+ this._rowMap[i].wasCurrent = selection.currentIndex == i;
+ }
+ }
+
+ // Do the sort.
+ if (sortColumn == this.sortColumn && !resort) {
+ if (sortDirection == this.sortDirection) {
+ return;
+ }
+ this._rowMap.reverse();
+ } else {
+ this._rowMap.sort((a, b) => {
+ let aText = a.getText(sortColumn);
+ let bText = b.getText(sortColumn);
+ if (sortDirection == "descending") {
+ return this.collator.compare(bText, aText);
+ }
+ return this.collator.compare(aText, bText);
+ });
+ }
+
+ // Restore what was selected.
+ if (selection) {
+ selection.selectEventsSuppressed = true;
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].wasSelected != selection.isSelected(i)) {
+ selection.toggleSelect(i);
+ }
+ }
+ // Can't do this until updating the selection is finished.
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].wasCurrent) {
+ selection.currentIndex = i;
+ break;
+ }
+ }
+ this.selectionChanged();
+ selection.selectEventsSuppressed = false;
+ }
+
+ if (this.tree) {
+ this.tree.invalidate();
+ }
+ this.sortColumn = sortColumn;
+ this.sortDirection = sortDirection;
+ },
+
+ // nsITreeView
+
+ selectionChanged() {
+ if (this.listener) {
+ this.listener.onSelectionChanged();
+ }
+ },
+ setTree(tree) {
+ this.tree = tree;
+ for (let topic of this._notifications) {
+ if (tree) {
+ Services.obs.addObserver(this, topic, true);
+ } else {
+ try {
+ Services.obs.removeObserver(this, topic);
+ } catch (ex) {
+ // `this` might not be a valid observer.
+ }
+ }
+ }
+ Services.prefs.addObserver("mail.addr_book.lastnamefirst", this, true);
+ },
+
+ // nsIAbDirSearchListener
+
+ onSearchFoundCard(card) {
+ // Instead of duplicating the insertion code below, just call it.
+ this.observe(card, "addrbook-contact-created", this.directory?.UID);
+ },
+ onSearchFinished(status, complete, secInfo, location) {
+ // Special handling for Bad Cert errors.
+ let offerCertException = false;
+ try {
+ // If code is not an NSS error, getErrorClass() will fail.
+ let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ let errorClass = nssErrorsService.getErrorClass(status);
+ if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ offerCertException = true;
+ }
+ } catch (ex) {}
+
+ if (offerCertException) {
+ // Give the user the option of adding an exception for the bad cert.
+ let params = {
+ exceptionAdded: false,
+ securityInfo: secInfo,
+ prefetchCert: true,
+ location,
+ };
+ window.openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "",
+ "chrome,centerscreen,modal",
+ params
+ );
+ // params.exceptionAdded will be set if the user added an exception.
+ }
+ },
+
+ // nsIObserver
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ ABView.nameFormat = Services.prefs.getIntPref(
+ "mail.addr_book.lastnamefirst",
+ 0
+ );
+ for (let card of this._rowMap) {
+ delete card._getTextCache.GeneratedName;
+ }
+ if (this.tree) {
+ if (this.sortColumn == "GeneratedName") {
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ } else {
+ this.tree.invalidate(this.tree.columns.GeneratedName);
+ }
+ }
+ return;
+ }
+
+ if (this.directory && data && this.directory.UID != data) {
+ return;
+ }
+
+ // If we make it here, we're in the root directory, or the right directory.
+
+ switch (topic) {
+ case "addrbook-directory-deleted": {
+ if (this.directory) {
+ break;
+ }
+
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (this._rowMap[i].directory.UID == subject.UID) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ case "addrbook-directory-invalidated":
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ if (subject == this.directory) {
+ this._rowMap.length = 0;
+ for (let card of this.directory.childCards) {
+ this._rowMap.push(new abViewCard(card, this.directory));
+ }
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ }
+ break;
+ case "addrbook-list-created": {
+ let parentDir = MailServices.ab.getDirectoryFromUID(data);
+ // `subject` is an nsIAbDirectory, make it the matching card instead.
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ for (let card of parentDir.childCards) {
+ if (card.UID == subject.UID) {
+ subject = card;
+ break;
+ }
+ }
+ }
+ // Falls through.
+ case "addrbook-list-member-added":
+ case "addrbook-contact-created":
+ if (topic == "addrbook-list-member-added" && !this.directory) {
+ break;
+ }
+
+ subject.QueryInterface(Ci.nsIAbCard);
+ let viewCard = new abViewCard(subject);
+ let sortText = viewCard.getText(this.sortColumn);
+ let addIndex = null;
+ for (let i = 0; addIndex === null && i < this._rowMap.length; i++) {
+ let comparison = this.collator.compare(
+ sortText,
+ this._rowMap[i].getText(this.sortColumn)
+ );
+ if (
+ (comparison < 0 && this.sortDirection == "ascending") ||
+ (comparison >= 0 && this.sortDirection == "descending")
+ ) {
+ addIndex = i;
+ }
+ }
+ if (addIndex === null) {
+ addIndex = this._rowMap.length;
+ }
+ this._rowMap.splice(addIndex, 0, viewCard);
+ if (this.tree) {
+ this.tree.rowCountChanged(addIndex, 1);
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ break;
+
+ case "addrbook-list-updated": {
+ let parentDir = this.directory;
+ if (!parentDir) {
+ parentDir = MailServices.ab.getDirectoryFromUID(data);
+ }
+ // `subject` is an nsIAbDirectory, make it the matching card instead.
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ for (let card of parentDir.childCards) {
+ if (card.UID == subject.UID) {
+ subject = card;
+ break;
+ }
+ }
+ }
+ // Falls through.
+ case "addrbook-contact-updated": {
+ subject.QueryInterface(Ci.nsIAbCard);
+ let needsSort = false;
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (
+ this._rowMap[i].card.equals(subject) &&
+ this._rowMap[i].card.directoryUID == subject.directoryUID
+ ) {
+ this._rowMap.splice(i, 1, new abViewCard(subject));
+ needsSort = true;
+ }
+ }
+ if (needsSort) {
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ }
+ break;
+ }
+
+ case "addrbook-list-deleted": {
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (this._rowMap[i].card.UID == subject.UID) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ case "addrbook-list-member-removed":
+ if (!this.directory) {
+ break;
+ }
+ // Falls through.
+ case "addrbook-contact-deleted": {
+ subject.QueryInterface(Ci.nsIAbCard);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (
+ this._rowMap[i].card.equals(subject) &&
+ this._rowMap[i].card.directoryUID == subject.directoryUID
+ ) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ }
+ },
+};
+
+/**
+ * Representation of a card, used as a table row in ABView.
+ *
+ * @param {nsIAbCard} card - contact or mailing list card for this row.
+ * @param {nsIAbDirectory} [directoryHint] - the directory containing card,
+ * if available (this is a performance optimization only).
+ */
+function abViewCard(card, directoryHint) {
+ this.card = card;
+ this._getTextCache = {};
+ if (directoryHint) {
+ this._directory = directoryHint;
+ } else {
+ this._directory = MailServices.ab.getDirectoryFromUID(
+ this.card.directoryUID
+ );
+ }
+}
+abViewCard.listFormatter = new Services.intl.ListFormat(
+ Services.appinfo.name == "xpcshell" ? "en-US" : undefined,
+ { type: "unit" }
+);
+abViewCard.prototype = {
+ _getText(columnID) {
+ try {
+ let { getProperty, supportsVCard, vCardProperties } = this.card;
+
+ if (this.card.isMailList) {
+ if (columnID == "GeneratedName") {
+ return this.card.displayName;
+ }
+ if (["NickName", "Notes"].includes(columnID)) {
+ return getProperty(columnID, "");
+ }
+ return "";
+ }
+
+ switch (columnID) {
+ case "addrbook":
+ case "Addrbook":
+ return this._directory.dirName;
+ case "GeneratedName":
+ return this.card.generateName(ABView.nameFormat);
+ case "_PhoneticName":
+ return this.card.generatePhoneticName(true);
+ case "ChatName":
+ return this.card.isMailList ? "" : this.card.generateChatName();
+ case "EmailAddresses":
+ return abViewCard.listFormatter.format(this.card.emailAddresses);
+ case "PhoneNumbers": {
+ let phoneNumbers;
+ if (supportsVCard) {
+ phoneNumbers = vCardProperties.getAllValues("tel");
+ } else {
+ phoneNumbers = [
+ getProperty("WorkPhone", ""),
+ getProperty("HomePhone", ""),
+ getProperty("CellularNumber", ""),
+ getProperty("FaxNumber", ""),
+ getProperty("PagerNumber", ""),
+ ];
+ }
+ return abViewCard.listFormatter.format(phoneNumbers.filter(Boolean));
+ }
+ case "JobTitle":
+ case "Title":
+ if (supportsVCard) {
+ return vCardProperties.getFirstValue("title");
+ }
+ return getProperty("JobTitle", "");
+ case "Department":
+ if (supportsVCard) {
+ let vCardValue = vCardProperties.getFirstValue("org");
+ if (Array.isArray(vCardValue)) {
+ return vCardValue[1] || "";
+ }
+ return "";
+ }
+ return getProperty(columnID, "");
+ case "Company":
+ case "Organization":
+ if (supportsVCard) {
+ let vCardValue = vCardProperties.getFirstValue("org");
+ if (Array.isArray(vCardValue)) {
+ return vCardValue[0] || "";
+ }
+ return vCardValue;
+ }
+ return getProperty("Company", "");
+ case "NickName":
+ if (supportsVCard) {
+ return vCardProperties.getFirstValue("nickname");
+ }
+ return getProperty(columnID, "");
+ default:
+ return getProperty(columnID, "");
+ }
+ } catch (ex) {
+ return "";
+ }
+ },
+ getText(columnID) {
+ if (!(columnID in this._getTextCache)) {
+ this._getTextCache[columnID] = this._getText(columnID)?.trim() ?? "";
+ }
+ return this._getTextCache[columnID];
+ },
+ get id() {
+ return this.card.UID;
+ },
+ get open() {
+ return false;
+ },
+ get level() {
+ return 0;
+ },
+ get children() {
+ return [];
+ },
+ getProperties() {
+ return this.card.isMailList ? "MailList" : "";
+ },
+ get directory() {
+ return this._directory;
+ },
+};
diff --git a/comm/mailnews/addrbook/content/map-list.js b/comm/mailnews/addrbook/content/map-list.js
new file mode 100644
index 0000000000..102cb09522
--- /dev/null
+++ b/comm/mailnews/addrbook/content/map-list.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+"use strict";
+
+/* global MozElements */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ /**
+ * The MozMapList widget behaves as a popup menu showing available map options
+ * for an address. It is a part of the card view in the addressbook.
+ *
+ * @augments {MozElements.MozMenuPopup}
+ */
+ class MozMapList extends MozElements.MozMenuPopup {
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasConnected) {
+ return;
+ }
+ this.setAttribute("is", "map-list");
+
+ this.addEventListener("command", event => {
+ this._chooseMapService(event.target);
+ event.stopPropagation();
+ });
+
+ this.addEventListener("popupshowing", event => {
+ this._listMapServices();
+ });
+
+ this._setWidgetDisabled(true);
+ }
+
+ get mapURL() {
+ return this._createMapItURL();
+ }
+
+ /**
+ * Initializes the necessary address data from an addressbook card.
+ *
+ * @param {nsIAbCard} card - The card to get the address data from.
+ * @param {string} addrPrefix - Card property prefix: "Home" or "Work",
+ * to make the map use either HomeAddress or WorkAddress.
+ */
+ initMapAddressFromCard(card, addrPrefix) {
+ let mapItURLFormat = this._getMapURLPref();
+ let doNotShowMap = !mapItURLFormat || !addrPrefix || !card;
+ this._setWidgetDisabled(doNotShowMap);
+ if (doNotShowMap) {
+ return;
+ }
+
+ this.address1 = card.getProperty(addrPrefix + "Address");
+ this.address2 = card.getProperty(addrPrefix + "Address2");
+ this.city = card.getProperty(addrPrefix + "City");
+ this._state = card.getProperty(addrPrefix + "State");
+ this.zip = card.getProperty(addrPrefix + "ZipCode");
+ this.country = card.getProperty(addrPrefix + "Country");
+ }
+
+ /**
+ * Sets the disabled/enabled state of the parent widget (e.g. a button).
+ */
+ _setWidgetDisabled(disabled) {
+ this.parentNode.disabled = disabled;
+ }
+
+ /**
+ * Returns the Map service URL from localized pref. Returns null if there
+ * is none at the given index.
+ *
+ * @param {integer} [index=0] - The index of the service to return.
+ * 0 is the default service.
+ */
+ _getMapURLPref(index = 0) {
+ let url = null;
+ if (!index) {
+ url = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url.format",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } else {
+ try {
+ url = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url." + index + ".format",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+ }
+
+ return url;
+ }
+
+ /**
+ * Builds menuitem elements representing map services defined in prefs
+ * and attaches them to the specified button.
+ */
+ _listMapServices() {
+ let index = 1;
+ let itemFound = true;
+ let defaultFound = false;
+ const kUserIndex = 100;
+ let mapList = this;
+ while (mapList.hasChildNodes()) {
+ mapList.lastChild.remove();
+ }
+
+ let defaultUrl = this._getMapURLPref();
+
+ // Creates the menuitem with supplied data.
+ function addMapService(url, name) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("url", url);
+ item.setAttribute("label", name);
+ item.setAttribute("type", "radio");
+ item.setAttribute("name", "mapit_service");
+ if (url == defaultUrl) {
+ item.setAttribute("checked", "true");
+ }
+ mapList.appendChild(item);
+ }
+
+ // Generates a useful generic name by cutting out only the host address.
+ function generateName(url) {
+ return new URL(url).hostname;
+ }
+
+ // Add all defined map services as menuitems.
+ while (itemFound) {
+ let urlName;
+ let urlTemplate = this._getMapURLPref(index);
+ if (!urlTemplate) {
+ itemFound = false;
+ } else {
+ // Name is not mandatory, generate one if not found.
+ try {
+ urlName = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url." + index + ".name",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {
+ urlName = generateName(urlTemplate);
+ }
+ }
+ if (itemFound) {
+ addMapService(urlTemplate, urlName);
+ index++;
+ if (urlTemplate == defaultUrl) {
+ defaultFound = true;
+ }
+ } else if (index < kUserIndex) {
+ // After iterating the base region provided urls, check for user defined ones.
+ index = kUserIndex;
+ itemFound = true;
+ }
+ }
+ if (!defaultFound) {
+ // If user had put a customized map URL into mail.addr_book.mapit_url.format
+ // preserve it as a new map service named with the URL.
+ // 'index' now points to the first unused entry in prefs.
+ let defaultName = generateName(defaultUrl);
+ addMapService(defaultUrl, defaultName);
+ Services.prefs.setCharPref(
+ "mail.addr_book.mapit_url." + index + ".format",
+ defaultUrl
+ );
+ Services.prefs.setCharPref(
+ "mail.addr_book.mapit_url." + index + ".name",
+ defaultName
+ );
+ }
+ }
+
+ /**
+ * Save user selected mapping service.
+ *
+ * @param {Element} item - The chosen menuitem with map service.
+ */
+ _chooseMapService(item) {
+ // Save selected URL as the default.
+ let defaultUrl = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ defaultUrl.data = item.getAttribute("url");
+ Services.prefs.setComplexValue(
+ "mail.addr_book.mapit_url.format",
+ Ci.nsIPrefLocalizedString,
+ defaultUrl
+ );
+ }
+
+ /**
+ * Generate the map URL used to open the link on clicking the menulist button.
+ *
+ * @returns {urlFormat} - the map url generated from the address.
+ */
+ _createMapItURL() {
+ let urlFormat = this._getMapURLPref();
+ if (!urlFormat) {
+ return null;
+ }
+
+ urlFormat = urlFormat.replace("@A1", encodeURIComponent(this.address1));
+ urlFormat = urlFormat.replace("@A2", encodeURIComponent(this.address2));
+ urlFormat = urlFormat.replace("@CI", encodeURIComponent(this.city));
+ urlFormat = urlFormat.replace("@ST", encodeURIComponent(this._state));
+ urlFormat = urlFormat.replace("@ZI", encodeURIComponent(this.zip));
+ urlFormat = urlFormat.replace("@CO", encodeURIComponent(this.country));
+
+ return urlFormat;
+ }
+ }
+
+ customElements.define("map-list", MozMapList, { extends: "menupopup" });
+}
diff --git a/comm/mailnews/addrbook/modules/AddrBookCard.jsm b/comm/mailnews/addrbook/modules/AddrBookCard.jsm
new file mode 100644
index 0000000000..23f387f921
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/AddrBookCard.jsm
@@ -0,0 +1,481 @@
+/* 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 = ["AddrBookCard"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm",
+ newUID: "resource:///modules/AddrBookUtils.jsm",
+ VCardProperties: "resource:///modules/VCardUtils.jsm",
+ VCardPropertyEntry: "resource:///modules/VCardUtils.jsm",
+});
+
+/**
+ * Prototype for nsIAbCard objects that are not mailing lists.
+ *
+ * @implements {nsIAbCard}
+ */
+function AddrBookCard() {
+ this._directoryUID = "";
+ this._properties = new Map([
+ ["PopularityIndex", 0],
+ ["LastModifiedDate", 0],
+ ]);
+
+ this._hasVCard = false;
+ XPCOMUtils.defineLazyGetter(this, "_vCardProperties", () => {
+ // Lazy creation of the VCardProperties object. Change the `_properties`
+ // object as much as you like (e.g. loading in properties from a database)
+ // before running this code. After it runs, the `_vCardProperties` object
+ // takes over and anything in `_properties` which could be stored in the
+ // vCard will be ignored!
+
+ this._hasVCard = true;
+
+ let vCard = this.getProperty("_vCard", "");
+ try {
+ if (vCard) {
+ let vCardProperties = lazy.VCardProperties.fromVCard(vCard, {
+ isGoogleCardDAV: this._isGoogleCardDAV,
+ });
+ // Custom1..4 properties could still exist as nsIAbCard properties.
+ // Migrate them now.
+ for (let key of ["Custom1", "Custom2", "Custom3", "Custom4"]) {
+ let value = this.getProperty(key, "");
+ if (
+ value &&
+ vCardProperties.getFirstEntry(`x-${key.toLowerCase()}`) === null
+ ) {
+ vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry(
+ `x-${key.toLowerCase()}`,
+ {},
+ "text",
+ value
+ )
+ );
+ }
+ this.deleteProperty(key);
+ }
+ return vCardProperties;
+ }
+ return lazy.VCardProperties.fromPropertyMap(this._properties);
+ } catch (error) {
+ console.error("Error creating vCard properties", error);
+ // Return an empty VCardProperties object if parsing failed
+ // catastrophically.
+ return new lazy.VCardProperties("4.0");
+ }
+ });
+}
+
+AddrBookCard.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAbCard"]),
+ classID: Components.ID("{1143991d-31cd-4ea6-9c97-c587d990d724}"),
+
+ /* nsIAbCard */
+
+ generateName(generateFormat, bundle) {
+ let result = "";
+ switch (generateFormat) {
+ case Ci.nsIAbCard.GENERATE_DISPLAY_NAME:
+ result = this.displayName;
+ break;
+
+ case Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER:
+ if (this.lastName) {
+ let otherNames = [
+ this.prefixName,
+ this.firstName,
+ this.middleName,
+ this.suffixName,
+ ]
+ .filter(Boolean)
+ .join(" ");
+ if (!otherNames) {
+ // Only use the lastName if we don't have anything to add after the
+ // comma, in order to avoid for the string to finish with ", ".
+ result = this.lastName;
+ } else {
+ result =
+ bundle?.formatStringFromName("lastFirstFormat", [
+ this.lastName,
+ otherNames,
+ ]) ?? `${this.lastName}, ${otherNames}`;
+ }
+ }
+ break;
+
+ default:
+ let startNames = [this.prefixName, this.firstName, this.middleName]
+ .filter(Boolean)
+ .join(" ");
+ let endNames = [this.lastName, this.suffixName]
+ .filter(Boolean)
+ .join(" ");
+ result =
+ bundle?.formatStringFromName("firstLastFormat", [
+ startNames,
+ endNames,
+ ]) ?? `${startNames} ${endNames}`;
+ break;
+ }
+
+ // Remove any leftover blank spaces.
+ result = result.trim();
+
+ if (result == "" || result == ",") {
+ result =
+ this.displayName ||
+ [
+ this.prefixName,
+ this.firstName,
+ this.middleName,
+ this.lastName,
+ this.suffixName,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .trim();
+
+ if (!result) {
+ // So far we don't have anything to show as a contact name.
+
+ if (this.primaryEmail) {
+ // Let's use the primary email localpart.
+ result = this.primaryEmail.split("@", 1)[0];
+ } else {
+ // We don't have a primary email either, let's try with the
+ // organization name.
+ result = !this._hasVCard
+ ? this.getProperty("Company", "")
+ : this._vCardProperties.getFirstValue("org");
+ }
+ }
+ }
+ return result || "";
+ },
+ get directoryUID() {
+ return this._directoryUID;
+ },
+ set directoryUID(value) {
+ this._directoryUID = value;
+ },
+ get UID() {
+ if (!this._uid) {
+ this._uid = lazy.newUID();
+ }
+ return this._uid;
+ },
+ set UID(value) {
+ if (this._uid && value != this._uid) {
+ throw Components.Exception(
+ `Bad UID: got ${value} != ${this.uid}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ this._uid = value;
+ },
+ get properties() {
+ let props = [];
+ for (const [name, value] of this._properties) {
+ props.push({
+ get name() {
+ return name;
+ },
+ get value() {
+ return value;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIProperty"]),
+ });
+ }
+ return props;
+ },
+ get supportsVCard() {
+ return true;
+ },
+ get vCardProperties() {
+ return this._vCardProperties;
+ },
+ get firstName() {
+ if (!this._hasVCard) {
+ return this.getProperty("FirstName", "");
+ }
+ let name = this._vCardProperties.getFirstValue("n");
+ if (!Array.isArray(name)) {
+ return "";
+ }
+ name = name[1];
+ if (Array.isArray(name)) {
+ name = name.join(" ");
+ }
+ return name;
+ },
+ set firstName(value) {
+ let n = this._vCardProperties.getFirstEntry("n");
+ if (n) {
+ n.value[1] = value;
+ } else {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry("n", {}, "text", ["", value, "", "", ""])
+ );
+ }
+ },
+ get lastName() {
+ if (!this._hasVCard) {
+ return this.getProperty("LastName", "");
+ }
+ let name = this._vCardProperties.getFirstValue("n");
+ if (!Array.isArray(name)) {
+ return "";
+ }
+ name = name[0];
+ if (Array.isArray(name)) {
+ name = name.join(" ");
+ }
+ return name;
+ },
+ set lastName(value) {
+ let n = this._vCardProperties.getFirstEntry("n");
+ if (n) {
+ n.value[0] = value;
+ } else {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry("n", {}, "text", [value, "", "", "", ""])
+ );
+ }
+ },
+ get displayName() {
+ if (!this._hasVCard) {
+ return this.getProperty("DisplayName", "");
+ }
+ return this._vCardProperties.getFirstValue("fn") || "";
+ },
+ set displayName(value) {
+ let fn = this._vCardProperties.getFirstEntry("fn");
+ if (fn) {
+ fn.value = value;
+ } else {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry("fn", {}, "text", value)
+ );
+ }
+ },
+ get primaryEmail() {
+ if (!this._hasVCard) {
+ return this.getProperty("PrimaryEmail", "");
+ }
+ return this._vCardProperties.getAllValuesSorted("email")[0] ?? "";
+ },
+ set primaryEmail(value) {
+ let entries = this._vCardProperties.getAllEntriesSorted("email");
+ if (entries.length && entries[0].value != value) {
+ this._vCardProperties.removeEntry(entries[0]);
+ entries.shift();
+ }
+
+ if (value) {
+ let existing = entries.find(e => e.value == value);
+ if (existing) {
+ existing.params.pref = "1";
+ } else {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry("email", { pref: "1" }, "text", value)
+ );
+ }
+ } else if (entries.length) {
+ entries[0].params.pref = "1";
+ }
+ },
+ get isMailList() {
+ return false;
+ },
+ get mailListURI() {
+ return "";
+ },
+ get emailAddresses() {
+ return this._vCardProperties.getAllValuesSorted("email");
+ },
+ get photoURL() {
+ let photoEntry = this.vCardProperties.getFirstEntry("photo");
+ if (photoEntry?.value) {
+ if (photoEntry.value?.startsWith("data:image/")) {
+ // This is a version 4.0 card
+ // OR a version 3.0 card with the URI type set (uncommon)
+ // OR a version 3.0 card that is lying about its type.
+ return photoEntry.value;
+ }
+ if (photoEntry.type == "binary" && photoEntry.value.startsWith("iVBO")) {
+ // This is a version 3.0 card.
+ // The first 3 bytes say this image is PNG.
+ return `data:image/png;base64,${photoEntry.value}`;
+ }
+ if (photoEntry.type == "binary" && photoEntry.value.startsWith("/9j/")) {
+ // This is a version 3.0 card.
+ // The first 3 bytes say this image is JPEG.
+ return `data:image/jpeg;base64,${photoEntry.value}`;
+ }
+ if (photoEntry.type == "uri" && /^https?:\/\//.test(photoEntry.value)) {
+ // A remote URI.
+ return photoEntry.value;
+ }
+ }
+
+ let photoName = this.getProperty("PhotoName", "");
+ if (photoName) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("Photos");
+ file.append(photoName);
+ return Services.io.newFileURI(file).spec;
+ }
+
+ return "";
+ },
+
+ getProperty(name, defaultValue) {
+ if (this._properties.has(name)) {
+ return this._properties.get(name);
+ }
+ return defaultValue;
+ },
+ getPropertyAsAString(name) {
+ if (!this._properties.has(name)) {
+ return "";
+ }
+ return this.getProperty(name);
+ },
+ getPropertyAsAUTF8String(name) {
+ if (!this._properties.has(name)) {
+ throw Components.Exception(`${name} N/A`, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ return this.getProperty(name);
+ },
+ getPropertyAsUint32(name) {
+ let value = this.getProperty(name);
+ if (!isNaN(parseInt(value, 10))) {
+ return parseInt(value, 10);
+ }
+ if (!isNaN(parseInt(value, 16))) {
+ return parseInt(value, 16);
+ }
+ throw Components.Exception(
+ `${name}: ${value} - not an int`,
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ },
+ getPropertyAsBool(name, defaultValue) {
+ let value = this.getProperty(name);
+ switch (value) {
+ case false:
+ case 0:
+ case "0":
+ return false;
+ case true:
+ case 1:
+ case "1":
+ return true;
+ case undefined:
+ return defaultValue;
+ }
+ throw Components.Exception(
+ `${name}: ${value} - not a boolean`,
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ },
+ setProperty(name, value) {
+ if (lazy.BANISHED_PROPERTIES.includes(name)) {
+ throw new Components.Exception(
+ `Unable to set ${name} as a property, use vCardProperties`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ if ([null, undefined, ""].includes(value)) {
+ this._properties.delete(name);
+ return;
+ }
+ if (typeof value == "boolean") {
+ value = value ? "1" : "0";
+ }
+ this._properties.set(name, "" + value);
+ },
+ setPropertyAsAString(name, value) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ setPropertyAsAUTF8String(name, value) {
+ this.setProperty(name, value);
+ },
+ setPropertyAsUint32(name, value) {
+ this.setProperty(name, value);
+ },
+ setPropertyAsBool(name, value) {
+ this.setProperty(name, value ? "1" : "0");
+ },
+ deleteProperty(name) {
+ this._properties.delete(name);
+ },
+ hasEmailAddress(emailAddress) {
+ emailAddress = emailAddress.toLowerCase();
+ return this.emailAddresses.some(e => e.toLowerCase() == emailAddress);
+ },
+ translateTo(type) {
+ if (type == "vcard") {
+ if (!this._vCardProperties.getFirstValue("uid")) {
+ this._vCardProperties.addValue("uid", this.UID);
+ }
+ return encodeURIComponent(this._vCardProperties.toVCard());
+ }
+ // Get nsAbCardProperty to do the work, the code is in C++ anyway.
+ let cardCopy = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ cardCopy.UID = this.UID;
+ cardCopy.copy(this);
+ return cardCopy.translateTo(type);
+ },
+ generatePhoneticName(lastNameFirst) {
+ if (lastNameFirst) {
+ return (
+ this.getProperty("PhoneticLastName", "") +
+ this.getProperty("PhoneticFirstName", "")
+ );
+ }
+ return (
+ this.getProperty("PhoneticFirstName", "") +
+ this.getProperty("PhoneticLastName", "")
+ );
+ },
+ generateChatName() {
+ for (let name of [
+ "_GoogleTalk",
+ "_AimScreenName",
+ "_Yahoo",
+ "_Skype",
+ "_QQ",
+ "_MSN",
+ "_ICQ",
+ "_JabberId",
+ "_IRC",
+ ]) {
+ if (this._properties.has(name)) {
+ return this._properties.get(name);
+ }
+ }
+ return "";
+ },
+ copy(srcCard) {
+ throw Components.Exception(
+ "nsIAbCard.copy() not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ },
+ equals(card) {
+ return this.UID == card.UID;
+ },
+};
diff --git a/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm b/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm
new file mode 100644
index 0000000000..b35f35b147
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm
@@ -0,0 +1,817 @@
+/* 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 = ["AddrBookDirectory"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm",
+ AddrBookMailingList: "resource:///modules/AddrBookMailingList.jsm",
+ BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm",
+ compareAddressBooks: "resource:///modules/AddrBookUtils.jsm",
+ newUID: "resource:///modules/AddrBookUtils.jsm",
+ VCardProperties: "resource:///modules/VCardUtils.jsm",
+});
+
+/**
+ * Abstract base class implementing nsIAbDirectory.
+ *
+ * @abstract
+ * @implements {nsIAbDirectory}
+ */
+class AddrBookDirectory {
+ QueryInterface = ChromeUtils.generateQI(["nsIAbDirectory"]);
+
+ constructor() {
+ this._uid = null;
+ this._dirName = null;
+ }
+
+ _initialized = false;
+ init(uri) {
+ if (this._initialized) {
+ throw new Components.Exception(
+ `Directory already initialized: ${uri}`,
+ Cr.NS_ERROR_ALREADY_INITIALIZED
+ );
+ }
+
+ // If this._readOnly is true, the user is prevented from making changes to
+ // the contacts. Subclasses may override this (for example to sync with a
+ // server) by setting this._overrideReadOnly to true, but must clear it
+ // before yielding to another thread (e.g. awaiting a Promise).
+
+ if (this._dirPrefId) {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_readOnly",
+ `${this.dirPrefId}.readOnly`,
+ false
+ );
+ }
+
+ this._initialized = true;
+ }
+ async cleanUp() {
+ if (!this._initialized) {
+ throw new Components.Exception(
+ "Directory not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+ }
+
+ get _prefBranch() {
+ if (this.__prefBranch) {
+ return this.__prefBranch;
+ }
+ if (!this._dirPrefId) {
+ throw Components.Exception("No dirPrefId!", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ return (this.__prefBranch = Services.prefs.getBranch(
+ `${this._dirPrefId}.`
+ ));
+ }
+ /** @abstract */
+ get lists() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement lists getter.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ get cards() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement cards getter.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ getCard(uid) {
+ let card = new lazy.AddrBookCard();
+ card.directoryUID = this.UID;
+ card._uid = uid;
+ card._properties = this.loadCardProperties(uid);
+ card._isGoogleCardDAV = this._isGoogleCardDAV;
+ return card.QueryInterface(Ci.nsIAbCard);
+ }
+ /** @abstract */
+ loadCardProperties(uid) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement loadCardProperties.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ saveCardProperties(uid, properties) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement saveCardProperties.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ deleteCard(uid) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement deleteCard.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ saveList(list) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement saveList.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ deleteList(uid) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement deleteList.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /**
+ * Create a Map of the properties to record when saving `card`, including
+ * any changes we want to make just before saving.
+ *
+ * @param {nsIAbCard} card
+ * @param {?string} uid
+ * @returns {Map<string, string>}
+ */
+ prepareToSaveCard(card, uid) {
+ let propertyMap = new Map(
+ Array.from(card.properties, p => [p.name, p.value])
+ );
+ let newProperties = new Map();
+
+ // Get a VCardProperties object for the card.
+ let vCardProperties;
+ if (card.supportsVCard) {
+ vCardProperties = card.vCardProperties;
+ } else {
+ vCardProperties = lazy.VCardProperties.fromPropertyMap(propertyMap);
+ }
+
+ if (uid) {
+ // Force the UID to be as passed.
+ vCardProperties.clearValues("uid");
+ vCardProperties.addValue("uid", uid);
+ } else if (vCardProperties.getFirstValue("uid") != card.UID) {
+ vCardProperties.clearValues("uid");
+ vCardProperties.addValue("uid", card.UID);
+ }
+
+ // Collect only the properties we intend to keep.
+ for (let [name, value] of propertyMap) {
+ if (lazy.BANISHED_PROPERTIES.includes(name)) {
+ continue;
+ }
+ if (value !== null && value !== undefined && value !== "") {
+ newProperties.set(name, value);
+ }
+ }
+
+ // Add the vCard and the properties from it we want to cache.
+ newProperties.set("_vCard", vCardProperties.toVCard());
+
+ let displayName = vCardProperties.getFirstValue("fn");
+ newProperties.set("DisplayName", displayName || "");
+
+ let flatten = value => {
+ if (Array.isArray(value)) {
+ return value.join(" ");
+ }
+ return value;
+ };
+
+ let name = vCardProperties.getFirstValue("n");
+ if (Array.isArray(name)) {
+ newProperties.set("FirstName", flatten(name[1]));
+ newProperties.set("LastName", flatten(name[0]));
+ }
+
+ let email = vCardProperties.getAllValuesSorted("email");
+ if (email[0]) {
+ newProperties.set("PrimaryEmail", email[0]);
+ }
+ if (email[1]) {
+ newProperties.set("SecondEmail", email[1]);
+ }
+
+ let nickname = vCardProperties.getFirstValue("nickname");
+ if (nickname) {
+ newProperties.set("NickName", flatten(nickname));
+ }
+
+ // Always set the last modified date.
+ newProperties.set("LastModifiedDate", "" + Math.floor(Date.now() / 1000));
+ return newProperties;
+ }
+
+ /* nsIAbDirectory */
+
+ get readOnly() {
+ return this._readOnly;
+ }
+ get isRemote() {
+ return false;
+ }
+ get isSecure() {
+ return false;
+ }
+ get propertiesChromeURI() {
+ return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml";
+ }
+ get dirPrefId() {
+ return this._dirPrefId;
+ }
+ get dirName() {
+ if (this._dirName === null) {
+ this._dirName = this.getLocalizedStringValue("description", "");
+ }
+ return this._dirName;
+ }
+ set dirName(value) {
+ this.setLocalizedStringValue("description", value);
+ this._dirName = value;
+ Services.obs.notifyObservers(this, "addrbook-directory-updated", "DirName");
+ }
+ get dirType() {
+ return Ci.nsIAbManager.JS_DIRECTORY_TYPE;
+ }
+ get fileName() {
+ return this._fileName;
+ }
+ get UID() {
+ if (!this._uid) {
+ if (this._prefBranch.getPrefType("uid") == Services.prefs.PREF_STRING) {
+ this._uid = this._prefBranch.getStringPref("uid");
+ } else {
+ this._uid = lazy.newUID();
+ this._prefBranch.setStringPref("uid", this._uid);
+ }
+ }
+ return this._uid;
+ }
+ get URI() {
+ return this._uri;
+ }
+ get position() {
+ return this._prefBranch.getIntPref("position", 1);
+ }
+ get childNodes() {
+ let lists = Array.from(
+ this.lists.values(),
+ list =>
+ new lazy.AddrBookMailingList(
+ list.uid,
+ this,
+ list.name,
+ list.nickName,
+ list.description
+ ).asDirectory
+ );
+ lists.sort(lazy.compareAddressBooks);
+ return lists;
+ }
+ /** @abstract */
+ get childCardCount() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement childCardCount getter.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ get childCards() {
+ let results = Array.from(
+ this.lists.values(),
+ list =>
+ new lazy.AddrBookMailingList(
+ list.uid,
+ this,
+ list.name,
+ list.nickName,
+ list.description
+ ).asCard
+ ).concat(Array.from(this.cards.keys(), this.getCard, this));
+
+ return results;
+ }
+ get supportsMailingLists() {
+ return true;
+ }
+
+ search(query, string, listener) {
+ if (!listener) {
+ return;
+ }
+ if (!query) {
+ listener.onSearchFinished(Cr.NS_ERROR_FAILURE, true, null, "");
+ return;
+ }
+ if (query[0] == "?") {
+ query = query.substring(1);
+ }
+
+ let results = Array.from(
+ this.lists.values(),
+ list =>
+ new lazy.AddrBookMailingList(
+ list.uid,
+ this,
+ list.name,
+ list.nickName,
+ list.description
+ ).asCard
+ ).concat(Array.from(this.cards.keys(), this.getCard, this));
+
+ // Process the query string into a tree of conditions to match.
+ let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/;
+ let index = 0;
+ let rootQuery = { children: [], op: "or" };
+ let currentQuery = rootQuery;
+
+ while (true) {
+ let match = lispRegexp.exec(query.substring(index));
+ if (!match) {
+ break;
+ }
+ index += match[0].length;
+
+ if (["and", "or", "not"].includes(match[1])) {
+ // For the opening bracket, step down a level.
+ let child = {
+ parent: currentQuery,
+ children: [],
+ op: match[1],
+ };
+ currentQuery.children.push(child);
+ currentQuery = child;
+ } else {
+ let [name, condition, value] = match[2].split(",");
+ currentQuery.children.push({
+ name,
+ condition,
+ value: decodeURIComponent(value).toLowerCase(),
+ });
+
+ // For each closing bracket except the first, step up a level.
+ for (let i = match[3].length - 1; i > 0; i--) {
+ currentQuery = currentQuery.parent;
+ }
+ }
+ }
+
+ results = results.filter(card => {
+ let properties;
+ if (card.isMailList) {
+ properties = new Map([
+ ["DisplayName", card.displayName],
+ ["NickName", card.getProperty("NickName", "")],
+ ["Notes", card.getProperty("Notes", "")],
+ ]);
+ } else if (card._properties.has("_vCard")) {
+ try {
+ properties = card.vCardProperties.toPropertyMap();
+ } catch (ex) {
+ // Parsing failed. Skip the vCard and just use the other properties.
+ console.error(ex);
+ properties = new Map();
+ }
+ for (let [key, value] of card._properties) {
+ if (!properties.has(key)) {
+ properties.set(key, value);
+ }
+ }
+ } else {
+ properties = card._properties;
+ }
+ let matches = b => {
+ if ("condition" in b) {
+ let { name, condition, value } = b;
+ if (name == "IsMailList" && condition == "=") {
+ return card.isMailList == (value == "true");
+ }
+ let cardValue = properties.get(name);
+ if (!cardValue) {
+ return condition == "!ex";
+ }
+ if (condition == "ex") {
+ return true;
+ }
+
+ cardValue = cardValue.toLowerCase();
+ switch (condition) {
+ case "=":
+ return cardValue == value;
+ case "!=":
+ return cardValue != value;
+ case "lt":
+ return cardValue < value;
+ case "gt":
+ return cardValue > value;
+ case "bw":
+ return cardValue.startsWith(value);
+ case "ew":
+ return cardValue.endsWith(value);
+ case "c":
+ return cardValue.includes(value);
+ case "!c":
+ return !cardValue.includes(value);
+ case "~=":
+ case "regex":
+ default:
+ return false;
+ }
+ }
+ if (b.op == "or") {
+ return b.children.some(bb => matches(bb));
+ }
+ if (b.op == "and") {
+ return b.children.every(bb => matches(bb));
+ }
+ if (b.op == "not") {
+ return !matches(b.children[0]);
+ }
+ return false;
+ };
+
+ return matches(rootQuery);
+ }, this);
+
+ for (let card of results) {
+ listener.onSearchFoundCard(card);
+ }
+ listener.onSearchFinished(Cr.NS_OK, true, null, "");
+ }
+ generateName(generateFormat, bundle) {
+ return this.dirName;
+ }
+ cardForEmailAddress(emailAddress) {
+ if (!emailAddress) {
+ return null;
+ }
+
+ // Check the properties. We copy the first two addresses to properties for
+ // this purpose, so it should be fast.
+ let card = this.getCardFromProperty("PrimaryEmail", emailAddress, false);
+ if (card) {
+ return card;
+ }
+ card = this.getCardFromProperty("SecondEmail", emailAddress, false);
+ if (card) {
+ return card;
+ }
+
+ // Nothing so far? Go through all the cards checking all of the addresses.
+ // This could be slow.
+ emailAddress = emailAddress.toLowerCase();
+ for (let [uid, properties] of this.cards) {
+ let vCard = properties.get("_vCard");
+ // If the vCard string doesn't include the email address, the parsed
+ // vCard won't include it either, so don't waste time parsing it.
+ if (!vCard?.toLowerCase().includes(emailAddress)) {
+ continue;
+ }
+ card = this.getCard(uid);
+ if (card.emailAddresses.some(e => e.toLowerCase() == emailAddress)) {
+ return card;
+ }
+ }
+
+ return null;
+ }
+ /** @abstract */
+ getCardFromProperty(property, value, caseSensitive) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement getCardFromProperty.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ /** @abstract */
+ getCardsFromProperty(property, value, caseSensitive) {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement getCardsFromProperty.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ getMailListFromName(name) {
+ for (let list of this.lists.values()) {
+ if (list.name.toLowerCase() == name.toLowerCase()) {
+ return new lazy.AddrBookMailingList(
+ list.uid,
+ this,
+ list.name,
+ list.nickName,
+ list.description
+ ).asDirectory;
+ }
+ }
+ return null;
+ }
+ deleteDirectory(directory) {
+ if (this._readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let list = this.lists.get(directory.UID);
+ list = new lazy.AddrBookMailingList(
+ list.uid,
+ this,
+ list.name,
+ list.nickName,
+ list.description
+ );
+
+ this.deleteList(directory.UID);
+
+ Services.obs.notifyObservers(
+ list.asDirectory,
+ "addrbook-list-deleted",
+ this.UID
+ );
+ }
+ hasCard(card) {
+ return this.lists.has(card.UID) || this.cards.has(card.UID);
+ }
+ hasDirectory(dir) {
+ return this.lists.has(dir.UID);
+ }
+ hasMailListWithName(name) {
+ return this.getMailListFromName(name) != null;
+ }
+ addCard(card) {
+ return this.dropCard(card, false);
+ }
+ modifyCard(card) {
+ if (this._readOnly && !this._overrideReadOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let oldProperties = this.loadCardProperties(card.UID);
+ let newProperties = this.prepareToSaveCard(card);
+
+ let allProperties = new Set(oldProperties.keys());
+ for (let key of newProperties.keys()) {
+ allProperties.add(key);
+ }
+
+ if (this.hasOwnProperty("cards")) {
+ this.cards.set(card.UID, newProperties);
+ }
+ this.saveCardProperties(card.UID, newProperties);
+
+ let changeData = {};
+ for (let name of allProperties) {
+ if (name == "LastModifiedDate") {
+ continue;
+ }
+
+ let oldValue = oldProperties.get(name) || null;
+ let newValue = newProperties.get(name) || null;
+ if (oldValue != newValue) {
+ changeData[name] = { oldValue, newValue };
+ }
+ }
+
+ // Increment this preference if one or both of these properties change.
+ // This will cause the UI to throw away cached values.
+ if ("DisplayName" in changeData || "PreferDisplayName" in changeData) {
+ Services.prefs.setIntPref(
+ "mail.displayname.version",
+ Services.prefs.getIntPref("mail.displayname.version", 0) + 1
+ );
+ }
+
+ // Send the card as it is in this directory, not as passed to this function.
+ let newCard = this.getCard(card.UID);
+ Services.obs.notifyObservers(newCard, "addrbook-contact-updated", this.UID);
+
+ Services.obs.notifyObservers(
+ newCard,
+ "addrbook-contact-properties-updated",
+ JSON.stringify(changeData)
+ );
+
+ // Return the card, even though the interface says not to, because
+ // subclasses may want it.
+ return newCard;
+ }
+ deleteCards(cards) {
+ if (this._readOnly && !this._overrideReadOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (cards === null) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_POINTER);
+ }
+
+ let updateDisplayNameVersion = false;
+ for (let card of cards) {
+ updateDisplayNameVersion = updateDisplayNameVersion || card.displayName;
+ // TODO: delete photo if there is one
+ this.deleteCard(card.UID);
+ if (this.hasOwnProperty("cards")) {
+ this.cards.delete(card.UID);
+ }
+ }
+
+ // Increment this preference if one or more cards has a display name.
+ // This will cause the UI to throw away cached values.
+ if (updateDisplayNameVersion) {
+ Services.prefs.setIntPref(
+ "mail.displayname.version",
+ Services.prefs.getIntPref("mail.displayname.version", 0) + 1
+ );
+ }
+
+ for (let card of cards) {
+ Services.obs.notifyObservers(card, "addrbook-contact-deleted", this.UID);
+ card.directoryUID = null;
+ }
+
+ // We could just delete all non-existent cards from list_cards, but a
+ // notification should be fired for each one. Let the list handle that.
+ for (let list of this.childNodes) {
+ list.deleteCards(cards);
+ }
+ }
+ dropCard(card, needToCopyCard) {
+ if (this._readOnly && !this._overrideReadOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (!card.UID) {
+ throw new Error("Card must have a UID to be added to this directory.");
+ }
+
+ let uid = needToCopyCard ? lazy.newUID() : card.UID;
+ let newProperties = this.prepareToSaveCard(card, uid);
+ if (card.directoryUID && card.directoryUID != this._uid) {
+ // These properties belong to a different directory. Don't keep them.
+ newProperties.delete("_etag");
+ newProperties.delete("_href");
+ }
+
+ if (this.hasOwnProperty("cards")) {
+ this.cards.set(uid, newProperties);
+ }
+ this.saveCardProperties(uid, newProperties);
+
+ // Increment this preference if the card has a display name.
+ // This will cause the UI to throw away cached values.
+ if (card.displayName) {
+ Services.prefs.setIntPref(
+ "mail.displayname.version",
+ Services.prefs.getIntPref("mail.displayname.version", 0) + 1
+ );
+ }
+
+ let newCard = this.getCard(uid);
+ Services.obs.notifyObservers(newCard, "addrbook-contact-created", this.UID);
+ return newCard;
+ }
+ useForAutocomplete(identityKey) {
+ return (
+ Services.prefs.getBoolPref("mail.enable_autocomplete") &&
+ this.getBoolValue("enable_autocomplete", true)
+ );
+ }
+ addMailList(list) {
+ if (this._readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (!list.isMailList) {
+ throw Components.Exception(
+ "Can't add; not a mail list",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ // Check if the new name is empty.
+ if (!list.dirName) {
+ throw new Components.Exception(
+ `Mail list name must be set; list.dirName=${list.dirName}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ // Check if the new name contains 2 spaces.
+ if (list.dirName.match(" ")) {
+ throw new Components.Exception(
+ `Invalid mail list name: ${list.dirName}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ // Check if the new name contains the following special characters.
+ for (let char of ',;"<>') {
+ if (list.dirName.includes(char)) {
+ throw new Components.Exception(
+ `Invalid mail list name: ${list.dirName}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ }
+
+ let newList = new lazy.AddrBookMailingList(
+ lazy.newUID(),
+ this,
+ list.dirName || "",
+ list.listNickName || "",
+ list.description || ""
+ );
+ this.saveList(newList);
+
+ let newListDirectory = newList.asDirectory;
+ Services.obs.notifyObservers(
+ newListDirectory,
+ "addrbook-list-created",
+ this.UID
+ );
+ return newListDirectory;
+ }
+ editMailListToDatabase(listCard) {
+ // Deliberately not implemented, this isn't a mailing list.
+ throw Components.Exception(
+ "editMailListToDatabase not relevant here",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ copyMailList(srcList) {
+ // Deliberately not implemented, this isn't a mailing list.
+ throw Components.Exception(
+ "copyMailList not relevant here",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ getIntValue(name, defaultValue) {
+ return this._prefBranch
+ ? this._prefBranch.getIntPref(name, defaultValue)
+ : defaultValue;
+ }
+ getBoolValue(name, defaultValue) {
+ return this._prefBranch
+ ? this._prefBranch.getBoolPref(name, defaultValue)
+ : defaultValue;
+ }
+ getStringValue(name, defaultValue) {
+ return this._prefBranch
+ ? this._prefBranch.getStringPref(name, defaultValue)
+ : defaultValue;
+ }
+ getLocalizedStringValue(name, defaultValue) {
+ if (!this._prefBranch) {
+ return defaultValue;
+ }
+ if (this._prefBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) {
+ return defaultValue;
+ }
+ try {
+ return this._prefBranch.getComplexValue(name, Ci.nsIPrefLocalizedString)
+ .data;
+ } catch (e) {
+ // getComplexValue doesn't work with autoconfig.
+ return this._prefBranch.getStringPref(name);
+ }
+ }
+ setIntValue(name, value) {
+ this._prefBranch.setIntPref(name, value);
+ }
+ setBoolValue(name, value) {
+ this._prefBranch.setBoolPref(name, value);
+ }
+ setStringValue(name, value) {
+ this._prefBranch.setStringPref(name, value);
+ }
+ setLocalizedStringValue(name, value) {
+ let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ valueLocal.data = value;
+ this._prefBranch.setComplexValue(
+ name,
+ Ci.nsIPrefLocalizedString,
+ valueLocal
+ );
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm b/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm
new file mode 100644
index 0000000000..31d16e93aa
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm
@@ -0,0 +1,420 @@
+/* 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 = ["AddrBookMailingList"];
+
+/* Prototype for mailing lists. A mailing list can appear as nsIAbDirectory
+ * or as nsIAbCard. Here we keep all relevant information in the class itself
+ * and fulfill each interface on demand. This will make more sense and be
+ * a lot neater once we stop using two XPCOM interfaces for one job. */
+
+function AddrBookMailingList(uid, parent, name, nickName, description) {
+ this._uid = uid;
+ this._parent = parent;
+ this._name = name;
+ this._nickName = nickName;
+ this._description = description;
+}
+AddrBookMailingList.prototype = {
+ get asDirectory() {
+ let self = this;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsIAbDirectory"]),
+ classID: Components.ID("{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"),
+
+ get readOnly() {
+ return self._parent._readOnly;
+ },
+ get isRemote() {
+ return self._parent.isRemote;
+ },
+ get isSecure() {
+ return self._parent.isSecure;
+ },
+ get propertiesChromeURI() {
+ return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml";
+ },
+ get UID() {
+ return self._uid;
+ },
+ get URI() {
+ return `${self._parent.URI}/${self._uid}`;
+ },
+ get dirName() {
+ return self._name;
+ },
+ set dirName(value) {
+ self._name = value;
+ },
+ get listNickName() {
+ return self._nickName;
+ },
+ set listNickName(value) {
+ self._nickName = value;
+ },
+ get description() {
+ return self._description;
+ },
+ set description(value) {
+ self._description = value;
+ },
+ get isMailList() {
+ return true;
+ },
+ get childNodes() {
+ return [];
+ },
+ get childCards() {
+ let selectStatement = self._parent._dbConnection.createStatement(
+ "SELECT card FROM list_cards WHERE list = :list ORDER BY oid"
+ );
+ selectStatement.params.list = self._uid;
+ let results = [];
+ while (selectStatement.executeStep()) {
+ results.push(self._parent.getCard(selectStatement.row.card));
+ }
+ selectStatement.finalize();
+ return results;
+ },
+ get supportsMailingLists() {
+ return false;
+ },
+
+ search(query, string, listener) {
+ if (!listener) {
+ return;
+ }
+ if (!query) {
+ listener.onSearchFinished(Cr.NS_ERROR_FAILURE, true, null, "");
+ return;
+ }
+ if (query[0] == "?") {
+ query = query.substring(1);
+ }
+
+ let results = this.childCards;
+
+ // Process the query string into a tree of conditions to match.
+ let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/;
+ let index = 0;
+ let rootQuery = { children: [], op: "or" };
+ let currentQuery = rootQuery;
+
+ while (true) {
+ let match = lispRegexp.exec(query.substring(index));
+ if (!match) {
+ break;
+ }
+ index += match[0].length;
+
+ if (["and", "or", "not"].includes(match[1])) {
+ // For the opening bracket, step down a level.
+ let child = {
+ parent: currentQuery,
+ children: [],
+ op: match[1],
+ };
+ currentQuery.children.push(child);
+ currentQuery = child;
+ } else {
+ let [name, condition, value] = match[2].split(",");
+ currentQuery.children.push({
+ name,
+ condition,
+ value: decodeURIComponent(value).toLowerCase(),
+ });
+
+ // For each closing bracket except the first, step up a level.
+ for (let i = match[3].length - 1; i > 0; i--) {
+ currentQuery = currentQuery.parent;
+ }
+ }
+ }
+
+ results = results.filter(card => {
+ let properties = card._properties;
+ let matches = b => {
+ if ("condition" in b) {
+ let { name, condition, value } = b;
+ if (name == "IsMailList" && condition == "=") {
+ return value == "true";
+ }
+
+ if (!properties.has(name)) {
+ return condition == "!ex";
+ }
+ if (condition == "ex") {
+ return true;
+ }
+
+ let cardValue = properties.get(name).toLowerCase();
+ switch (condition) {
+ case "=":
+ return cardValue == value;
+ case "!=":
+ return cardValue != value;
+ case "lt":
+ return cardValue < value;
+ case "gt":
+ return cardValue > value;
+ case "bw":
+ return cardValue.startsWith(value);
+ case "ew":
+ return cardValue.endsWith(value);
+ case "c":
+ return cardValue.includes(value);
+ case "!c":
+ return !cardValue.includes(value);
+ case "~=":
+ case "regex":
+ default:
+ return false;
+ }
+ }
+ if (b.op == "or") {
+ return b.children.some(bb => matches(bb));
+ }
+ if (b.op == "and") {
+ return b.children.every(bb => matches(bb));
+ }
+ if (b.op == "not") {
+ return !matches(b.children[0]);
+ }
+ return false;
+ };
+
+ return matches(rootQuery);
+ }, this);
+
+ for (let card of results) {
+ listener.onSearchFoundCard(card);
+ }
+ listener.onSearchFinished(Cr.NS_OK, true, null, "");
+ },
+ addCard(card) {
+ if (this.readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (!card.primaryEmail) {
+ return card;
+ }
+ if (!self._parent.hasCard(card)) {
+ card = self._parent.addCard(card);
+ }
+ let insertStatement = self._parent._dbConnection.createStatement(
+ "REPLACE INTO list_cards (list, card) VALUES (:list, :card)"
+ );
+ insertStatement.params.list = self._uid;
+ insertStatement.params.card = card.UID;
+ insertStatement.execute();
+ Services.obs.notifyObservers(
+ card,
+ "addrbook-list-member-added",
+ self._uid
+ );
+ insertStatement.finalize();
+ return card;
+ },
+ deleteCards(cards) {
+ if (this.readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let deleteCardStatement = self._parent._dbConnection.createStatement(
+ "DELETE FROM list_cards WHERE list = :list AND card = :card"
+ );
+ for (let card of cards) {
+ deleteCardStatement.params.list = self._uid;
+ deleteCardStatement.params.card = card.UID;
+ deleteCardStatement.execute();
+ if (self._parent._dbConnection.affectedRows) {
+ Services.obs.notifyObservers(
+ card,
+ "addrbook-list-member-removed",
+ self._uid
+ );
+ }
+ deleteCardStatement.reset();
+ }
+ deleteCardStatement.finalize();
+ },
+ dropCard(card, needToCopyCard) {
+ if (this.readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (needToCopyCard) {
+ card = self._parent.dropCard(card, true);
+ }
+ this.addCard(card);
+ Services.obs.notifyObservers(
+ card,
+ "addrbook-list-member-added",
+ self._uid
+ );
+ },
+ editMailListToDatabase(listCard) {
+ if (this.readOnly) {
+ throw new Components.Exception(
+ "Directory is read-only",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ // Check if the new name is empty.
+ if (!self._name) {
+ throw new Components.Exception(
+ "Invalid mailing list name",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ // Check if the new name contains 2 spaces.
+ if (self._name.match(" ")) {
+ throw new Components.Exception(
+ "Invalid mailing list name",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ // Check if the new name contains the following special characters.
+ for (let char of ',;"<>') {
+ if (self._name.includes(char)) {
+ throw new Components.Exception(
+ "Invalid mailing list name",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ }
+
+ self._parent.saveList(self);
+ Services.obs.notifyObservers(
+ this,
+ "addrbook-list-updated",
+ self._parent.UID
+ );
+ },
+ hasMailListWithName(name) {
+ return false;
+ },
+ getMailListFromName(name) {
+ return null;
+ },
+ };
+ },
+ get asCard() {
+ let self = this;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsIAbCard"]),
+ classID: Components.ID("{1143991d-31cd-4ea6-9c97-c587d990d724}"),
+
+ get UID() {
+ return self._uid;
+ },
+ get isMailList() {
+ return true;
+ },
+ get mailListURI() {
+ return `${self._parent.URI}/${self._uid}`;
+ },
+
+ get directoryUID() {
+ return self._parent.UID;
+ },
+ get firstName() {
+ return "";
+ },
+ get lastName() {
+ return self._name;
+ },
+ get displayName() {
+ return self._name;
+ },
+ set displayName(value) {
+ self._name = value;
+ },
+ get primaryEmail() {
+ return "";
+ },
+ get emailAddresses() {
+ // NOT the members of this list.
+ return [];
+ },
+
+ generateName(generateFormat) {
+ return self._name;
+ },
+ getProperty(name, defaultValue) {
+ switch (name) {
+ case "NickName":
+ return self._nickName;
+ case "Notes":
+ return self._description;
+ }
+ return defaultValue;
+ },
+ setProperty(name, value) {
+ switch (name) {
+ case "NickName":
+ self._nickName = value;
+ break;
+ case "Notes":
+ self._description = value;
+ break;
+ }
+ },
+ equals(card) {
+ return self._uid == card.UID;
+ },
+ hasEmailAddress(emailAddress) {
+ return false;
+ },
+ get properties() {
+ const entries = [
+ ["DisplayName", this.displayName],
+ ["NickName", this.getProperty("NickName", "")],
+ ["Notes", this.getProperty("Notes", "")],
+ ];
+ let props = [];
+ for (const [name, value] of entries) {
+ props.push({
+ get name() {
+ return name;
+ },
+ get value() {
+ return value;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIProperty"]),
+ });
+ }
+ return props;
+ },
+ get supportsVCard() {
+ return false;
+ },
+ get vCardProperties() {
+ return null;
+ },
+ translateTo(type) {
+ // Get nsAbCardProperty to do the work, the code is in C++ anyway.
+ let cardCopy = Cc[
+ "@mozilla.org/addressbook/cardproperty;1"
+ ].createInstance(Ci.nsIAbCard);
+ cardCopy.UID = this.UID;
+ cardCopy.copy(this);
+ return cardCopy.translateTo(type);
+ },
+ };
+ },
+};
diff --git a/comm/mailnews/addrbook/modules/AddrBookManager.jsm b/comm/mailnews/addrbook/modules/AddrBookManager.jsm
new file mode 100644
index 0000000000..6e15a4c971
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/AddrBookManager.jsm
@@ -0,0 +1,608 @@
+/* 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 = ["AddrBookManager"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ compareAddressBooks: "resource:///modules/AddrBookUtils.jsm",
+ MailGlue: "resource:///modules/MailGlue.jsm",
+});
+
+/** Test for valid directory URIs. */
+const URI_REGEXP = /^([\w-]+):\/\/([\w\.-]*)([/:].*|$)/;
+
+/**
+ * When initialized, a map of nsIAbDirectory objects. Keys to this map are
+ * the directories' URIs.
+ */
+let store = null;
+
+/** Valid address book types. This differs by operating system. */
+let types = ["jsaddrbook", "jscarddav", "moz-abldapdirectory"];
+if (AppConstants.platform == "macosx") {
+ types.push("moz-abosxdirectory");
+} else if (AppConstants.platform == "win") {
+ types.push("moz-aboutlookdirectory");
+}
+
+/**
+ * A pre-sorted list of directories in the right order, to be returned by
+ * AddrBookManager.directories. That function is called a lot, and there's
+ * no need to sort the list every time.
+ *
+ * Call updateSortedDirectoryList after `store` changes and before any
+ * notifications happen.
+ */
+let sortedDirectoryList = [];
+function updateSortedDirectoryList() {
+ sortedDirectoryList = [...store.values()];
+ sortedDirectoryList.sort(lazy.compareAddressBooks);
+}
+
+/**
+ * Initialise an address book directory by URI.
+ *
+ * @param {string} uri - URI for the directory.
+ * @param {boolean} shouldStore - Whether to keep a reference to this address
+ * book in the store.
+ * @returns {nsIAbDirectory}
+ */
+function createDirectoryObject(uri, shouldStore = false) {
+ let uriParts = URI_REGEXP.exec(uri);
+ if (!uriParts) {
+ throw Components.Exception(
+ `Unexpected uri: ${uri}`,
+ Cr.NS_ERROR_MALFORMED_URI
+ );
+ }
+
+ let [, scheme] = uriParts;
+ let dir = Cc[
+ `@mozilla.org/addressbook/directory;1?type=${scheme}`
+ ].createInstance(Ci.nsIAbDirectory);
+
+ try {
+ if (shouldStore) {
+ // This must happen before .init is called, or the OS X provider breaks
+ // in some circumstances. If .init fails, we'll remove it again.
+ // The Outlook provider also needs this since during the initialisation
+ // of the top-most directory, contained mailing lists already need
+ // to loop that directory.
+ store.set(uri, dir);
+ }
+ dir.init(uri);
+ } catch (ex) {
+ if (shouldStore) {
+ store.delete(uri);
+ }
+ throw ex;
+ }
+
+ return dir;
+}
+
+/**
+ * Read the preferences and create any address books defined there.
+ */
+function ensureInitialized() {
+ if (store !== null) {
+ return;
+ }
+ if (lazy.MailGlue.isToolboxProcess) {
+ throw new Components.Exception(
+ "AddrBookManager tried to start in the Developer Tools process!",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ store = new Map();
+
+ for (let pref of Services.prefs.getChildList("ldap_2.servers.")) {
+ try {
+ if (pref.endsWith(".uri")) {
+ let uri = Services.prefs.getStringPref(pref);
+ if (uri.startsWith("ldap://") || uri.startsWith("ldaps://")) {
+ let prefName = pref.substring(0, pref.length - 4);
+
+ uri = `moz-abldapdirectory://${prefName}`;
+ createDirectoryObject(uri, true);
+ }
+ } else if (pref.endsWith(".dirType")) {
+ let prefName = pref.substring(0, pref.length - 8);
+ let dirType = Services.prefs.getIntPref(pref);
+ let fileName = Services.prefs.getStringPref(`${prefName}.filename`, "");
+ let uri = Services.prefs.getStringPref(`${prefName}.uri`, "");
+
+ switch (dirType) {
+ case Ci.nsIAbManager.MAPI_DIRECTORY_TYPE:
+ if (
+ Cu.isInAutomation ||
+ Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")
+ ) {
+ // Don't load the OS Address Book in tests.
+ break;
+ }
+ if (Services.prefs.getIntPref(`${prefName}.position`, 1) < 1) {
+ // Migration: the previous address book manager set the position
+ // value to 0 to indicate the removal of an address book.
+ Services.prefs.clearUserPref(`${prefName}.position`);
+ Services.prefs.setIntPref(pref, -1);
+ break;
+ }
+ if (AppConstants.platform == "macosx") {
+ createDirectoryObject(uri, true);
+ } else if (AppConstants.platform == "win") {
+ let outlookInterface = Cc[
+ "@mozilla.org/addressbook/outlookinterface;1"
+ ].getService(Ci.nsIAbOutlookInterface);
+ for (let folderURI of outlookInterface.getFolderURIs(uri)) {
+ createDirectoryObject(folderURI, true);
+ }
+ }
+ break;
+ case Ci.nsIAbManager.JS_DIRECTORY_TYPE:
+ if (fileName) {
+ let uri = `jsaddrbook://${fileName}`;
+ createDirectoryObject(uri, true);
+ }
+ break;
+ case Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE:
+ if (fileName) {
+ let uri = `jscarddav://${fileName}`;
+ createDirectoryObject(uri, true);
+ }
+ break;
+ }
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ updateSortedDirectoryList();
+}
+
+// Force the manager to shut down. For tests only.
+Services.obs.addObserver(async () => {
+ // Allow directories to tidy up.
+ for (let directory of store.values()) {
+ await directory.cleanUp();
+ }
+ // Clear the store. The next call to ensureInitialized will recreate it.
+ store = null;
+ Services.obs.notifyObservers(null, "addrbook-reloaded");
+}, "addrbook-reload");
+
+/** Cache for the cardForEmailAddress function, and timer to clear it. */
+let addressCache = new Map();
+let addressCacheTimer = null;
+
+// Throw away cached cards if the display name properties change, so we can
+// get the updated version of the card that changed.
+Services.prefs.addObserver("mail.displayname.version", () => {
+ addressCache.clear();
+ Services.obs.notifyObservers(null, "addrbook-displayname-changed");
+});
+
+// When this prefence has been updated, we need to update the
+// mail.displayname.version, which notifies it's preference observer (above).
+// This will then notify the addrbook-displayname-changed observer, and change
+// the displayname in the thread tree and message header.
+Services.prefs.addObserver("mail.showCondensedAddresses", () => {
+ Services.prefs.setIntPref(
+ "mail.displayname.version",
+ Services.prefs.getIntPref("mail.displayname.version") + 1
+ );
+});
+
+/**
+ * @implements {nsIAbManager}
+ * @implements {nsICommandLineHandler}
+ */
+function AddrBookManager() {}
+AddrBookManager.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAbManager",
+ "nsICommandLineHandler",
+ ]),
+ classID: Components.ID("{224d3ef9-d81c-4d94-8826-a79a5835af93}"),
+
+ /* nsIAbManager */
+
+ get directories() {
+ ensureInitialized();
+ return sortedDirectoryList.slice();
+ },
+ getDirectory(uri) {
+ if (uri.startsWith("moz-abdirectory://")) {
+ throw new Components.Exception(
+ "The root address book no longer exists",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ ensureInitialized();
+ if (store.has(uri)) {
+ return store.get(uri);
+ }
+
+ let uriParts = URI_REGEXP.exec(uri);
+ if (!uriParts) {
+ throw Components.Exception(
+ `Unexpected uri: ${uri}`,
+ Cr.NS_ERROR_MALFORMED_URI
+ );
+ }
+ let [, scheme, fileName, tail] = uriParts;
+ if (tail && types.includes(scheme)) {
+ if (
+ (scheme == "jsaddrbook" && tail.startsWith("/")) ||
+ scheme == "moz-aboutlookdirectory"
+ ) {
+ let parent;
+ if (scheme == "jsaddrbook") {
+ parent = this.getDirectory(`${scheme}://${fileName}`);
+ } else {
+ parent = this.getDirectory(`${scheme}:///${tail.split("/")[1]}`);
+ }
+ for (let list of parent.childNodes) {
+ list.QueryInterface(Ci.nsIAbDirectory);
+ if (list.URI == uri) {
+ return list;
+ }
+ }
+ throw Components.Exception(
+ `No ${scheme} directory for uri=${uri}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ } else if (scheme == "jscarddav") {
+ throw Components.Exception(
+ `No ${scheme} directory for uri=${uri}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ // `tail` could point to a mailing list.
+ return createDirectoryObject(uri);
+ }
+ throw Components.Exception(
+ `No directory for uri=${uri}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ },
+ getDirectoryFromId(dirPrefId) {
+ ensureInitialized();
+ for (let dir of store.values()) {
+ if (dir.dirPrefId == dirPrefId) {
+ return dir;
+ }
+ }
+ return null;
+ },
+ getDirectoryFromUID(uid) {
+ ensureInitialized();
+ for (let dir of store.values()) {
+ if (dir.UID == uid) {
+ return dir;
+ }
+ }
+ return null;
+ },
+ getMailListFromName(name) {
+ ensureInitialized();
+ for (let dir of store.values()) {
+ let hit = dir.getMailListFromName(name);
+ if (hit) {
+ return hit;
+ }
+ }
+ return null;
+ },
+ newAddressBook(dirName, uri, type, uid) {
+ function ensureUniquePrefName() {
+ let leafName = dirName.replace(/\W/g, "");
+ if (!leafName) {
+ leafName = "_nonascii";
+ }
+
+ let existingNames = Array.from(store.values(), dir => dir.dirPrefId);
+ let uniqueCount = 0;
+ prefName = `ldap_2.servers.${leafName}`;
+ while (existingNames.includes(prefName)) {
+ prefName = `ldap_2.servers.${leafName}_${++uniqueCount}`;
+ }
+ }
+
+ if (!dirName) {
+ throw new Components.Exception(
+ "dirName must be specified",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+ if (uid && this.getDirectoryFromUID(uid)) {
+ throw new Components.Exception(
+ `An address book with the UID ${uid} already exists`,
+ Cr.NS_ERROR_ABORT
+ );
+ }
+
+ let prefName;
+ ensureInitialized();
+
+ switch (type) {
+ case Ci.nsIAbManager.LDAP_DIRECTORY_TYPE: {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("ldap.sqlite");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ ensureUniquePrefName();
+ Services.prefs.setStringPref(`${prefName}.description`, dirName);
+ Services.prefs.setStringPref(`${prefName}.filename`, file.leafName);
+ Services.prefs.setStringPref(`${prefName}.uri`, uri);
+ if (uid) {
+ Services.prefs.setStringPref(`${prefName}.uid`, uid);
+ }
+
+ uri = `moz-abldapdirectory://${prefName}`;
+ let dir = createDirectoryObject(uri, true);
+ updateSortedDirectoryList();
+ Services.obs.notifyObservers(dir, "addrbook-directory-created");
+ break;
+ }
+ case Ci.nsIAbManager.MAPI_DIRECTORY_TYPE: {
+ if (AppConstants.platform == "macosx") {
+ uri = "moz-abosxdirectory:///";
+ if (store.has(uri)) {
+ throw Components.Exception(
+ `Can't create new ab of type=${type} - already exists`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ prefName = "ldap_2.servers.osx";
+ } else if (AppConstants.platform == "win") {
+ uri = "moz-aboutlookdirectory:///";
+ if (store.has(uri)) {
+ throw Components.Exception(
+ `Can't create new ab of type=${type} - already exists`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ prefName = "ldap_2.servers.outlook";
+ } else {
+ throw Components.Exception(
+ "Can't create new ab of type=MAPI_DIRECTORY_TYPE",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ Services.prefs.setIntPref(
+ `${prefName}.dirType`,
+ Ci.nsIAbManager.MAPI_DIRECTORY_TYPE
+ );
+ Services.prefs.setStringPref(
+ `${prefName}.description`,
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ Services.prefs.setStringPref(`${prefName}.uri`, uri);
+ if (uid) {
+ Services.prefs.setStringPref(`${prefName}.uid`, uid);
+ }
+
+ if (AppConstants.platform == "macosx") {
+ let dir = createDirectoryObject(uri, true);
+ updateSortedDirectoryList();
+ Services.obs.notifyObservers(dir, "addrbook-directory-created");
+ } else if (AppConstants.platform == "win") {
+ let outlookInterface = Cc[
+ "@mozilla.org/addressbook/outlookinterface;1"
+ ].getService(Ci.nsIAbOutlookInterface);
+ for (let folderURI of outlookInterface.getFolderURIs(uri)) {
+ let dir = createDirectoryObject(folderURI, true);
+ updateSortedDirectoryList();
+ Services.obs.notifyObservers(dir, "addrbook-directory-created");
+ }
+ }
+ break;
+ }
+ case Ci.nsIAbManager.JS_DIRECTORY_TYPE:
+ case Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE: {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("abook.sqlite");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ ensureUniquePrefName();
+ Services.prefs.setStringPref(`${prefName}.description`, dirName);
+ Services.prefs.setIntPref(`${prefName}.dirType`, type);
+ Services.prefs.setStringPref(`${prefName}.filename`, file.leafName);
+ if (uid) {
+ Services.prefs.setStringPref(`${prefName}.uid`, uid);
+ }
+
+ let scheme =
+ type == Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ ? "jsaddrbook"
+ : "jscarddav";
+ uri = `${scheme}://${file.leafName}`;
+ let dir = createDirectoryObject(uri, true);
+ updateSortedDirectoryList();
+ Services.obs.notifyObservers(dir, "addrbook-directory-created");
+ break;
+ }
+ default:
+ throw Components.Exception(
+ `Unexpected directory type: ${type}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ return prefName;
+ },
+ addAddressBook(dir) {
+ if (
+ !dir.URI ||
+ !dir.dirName ||
+ !dir.UID ||
+ dir.isMailList ||
+ dir.isQuery ||
+ dir.dirPrefId
+ ) {
+ throw new Components.Exception(
+ "Invalid directory",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ ensureInitialized();
+ if (store.has(dir.URI)) {
+ throw new Components.Exception(
+ "Directory already exists",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ store.set(dir.URI, dir);
+ updateSortedDirectoryList();
+ Services.obs.notifyObservers(dir, "addrbook-directory-created");
+ },
+ deleteAddressBook(uri) {
+ let uriParts = URI_REGEXP.exec(uri);
+ if (!uriParts) {
+ throw Components.Exception("", Cr.NS_ERROR_MALFORMED_URI);
+ }
+
+ let [, scheme, fileName, tail] = uriParts;
+ if (tail && tail.startsWith("/")) {
+ let dir;
+ if (scheme == "jsaddrbook") {
+ dir = store.get(`${scheme}://${fileName}`);
+ } else if (scheme == "moz-aboutlookdirectory") {
+ dir = store.get(`${scheme}:///${tail.split("/")[1]}`);
+ }
+ let list = this.getDirectory(uri);
+ if (dir && list) {
+ dir.deleteDirectory(list);
+ return;
+ }
+ }
+
+ let dir = store.get(uri);
+ if (!dir) {
+ throw new Components.Exception(
+ `Address book not found: ${uri}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ let prefName = dir.dirPrefId;
+ if (prefName) {
+ let dirType = Services.prefs.getIntPref(`${prefName}.dirType`, 0);
+ fileName = dir.fileName;
+
+ // Deleting the built-in address books is very bad.
+ if (["ldap_2.servers.pab", "ldap_2.servers.history"].includes(prefName)) {
+ throw new Components.Exception(
+ "Refusing to delete a built-in address book",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ for (let name of Services.prefs.getChildList(`${prefName}.`)) {
+ Services.prefs.clearUserPref(name);
+ }
+ if (dirType == Ci.nsIAbManager.MAPI_DIRECTORY_TYPE) {
+ // The prefs for this directory type are defaults. Setting the dirType
+ // to -1 ensures the directory is ignored.
+ Services.prefs.setIntPref(`${prefName}.dirType`, -1);
+ }
+ }
+
+ store.delete(uri);
+ updateSortedDirectoryList();
+
+ // Clear this reference to the deleted address book.
+ if (Services.prefs.getStringPref("mail.collect_addressbook") == uri) {
+ Services.prefs.clearUserPref("mail.collect_addressbook");
+ }
+
+ dir.cleanUp().then(() => {
+ if (fileName) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(fileName);
+ if (file.exists()) {
+ file.remove(false);
+ }
+ }
+
+ Services.obs.notifyObservers(dir, "addrbook-directory-deleted");
+ });
+ },
+ mailListNameExists(name) {
+ ensureInitialized();
+ for (let dir of store.values()) {
+ if (dir.hasMailListWithName(name)) {
+ return true;
+ }
+ }
+ return false;
+ },
+ /**
+ * Finds out if the directory name already exists.
+ *
+ * @param {string} name - The name of a directory to check for.
+ */
+ directoryNameExists(name) {
+ ensureInitialized();
+ for (let dir of store.values()) {
+ if (dir.dirName.toLowerCase() === name.toLowerCase()) {
+ return true;
+ }
+ }
+ return false;
+ },
+ cardForEmailAddress(emailAddress) {
+ if (!emailAddress) {
+ return null;
+ }
+
+ if (addressCacheTimer) {
+ lazy.clearTimeout(addressCacheTimer);
+ }
+ addressCacheTimer = lazy.setTimeout(() => {
+ addressCacheTimer = null;
+ addressCache.clear();
+ }, 60000);
+
+ if (addressCache.has(emailAddress)) {
+ return addressCache.get(emailAddress);
+ }
+
+ for (let directory of sortedDirectoryList) {
+ try {
+ let card = directory.cardForEmailAddress(emailAddress);
+ if (card) {
+ addressCache.set(emailAddress, card);
+ return card;
+ }
+ } catch (ex) {
+ // Directories can throw, that's okay.
+ }
+ }
+
+ addressCache.set(emailAddress, null);
+ return null;
+ },
+};
diff --git a/comm/mailnews/addrbook/modules/AddrBookUtils.jsm b/comm/mailnews/addrbook/modules/AddrBookUtils.jsm
new file mode 100644
index 0000000000..aea0c152ad
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/AddrBookUtils.jsm
@@ -0,0 +1,522 @@
+/* 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 = [
+ "exportAttributes",
+ "AddrBookUtils",
+ "compareAddressBooks",
+ "newUID",
+ "SimpleEnumerator",
+];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ attrMapService: [
+ "@mozilla.org/addressbook/ldap-attribute-map-service;1",
+ "nsIAbLDAPAttributeMapService",
+ ],
+});
+
+function SimpleEnumerator(elements) {
+ this._elements = elements;
+ this._position = 0;
+}
+SimpleEnumerator.prototype = {
+ hasMoreElements() {
+ return this._position < this._elements.length;
+ },
+ getNext() {
+ if (this.hasMoreElements()) {
+ return this._elements[this._position++];
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsISimpleEnumerator"]),
+ *[Symbol.iterator]() {
+ while (this.hasMoreElements()) {
+ yield this.getNext();
+ }
+ },
+};
+
+function newUID() {
+ return Services.uuid.generateUUID().toString().substring(1, 37);
+}
+
+let abSortOrder = {
+ [Ci.nsIAbManager.JS_DIRECTORY_TYPE]: 1,
+ [Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE]: 2,
+ [Ci.nsIAbManager.LDAP_DIRECTORY_TYPE]: 3,
+ [Ci.nsIAbManager.ASYNC_DIRECTORY_TYPE]: 3,
+ [Ci.nsIAbManager.MAPI_DIRECTORY_TYPE]: 4,
+};
+let abNameComparer = new Intl.Collator(undefined, { numeric: true });
+
+/**
+ * Comparator for address books. Any UI that lists address books should use
+ * this order, although generally speaking, using nsIAbManager.directories is
+ * all that is required to get the order.
+ *
+ * Note that directories should not be compared with mailing lists in this way,
+ * however two mailing lists with the same parent can be safely compared.
+ *
+ * @param {nsIAbDirectory} a
+ * @param {nsIAbDirectory} b
+ * @returns {integer}
+ */
+function compareAddressBooks(a, b) {
+ if (a.isMailList != b.isMailList) {
+ throw Components.Exception(
+ "Tried to compare a mailing list with a directory",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ // Only compare the names of mailing lists.
+ if (a.isMailList) {
+ return abNameComparer.compare(a.dirName, b.dirName);
+ }
+
+ // The Personal Address Book is first and Collected Addresses last.
+ let aPrefId = a.dirPrefId;
+ let bPrefId = b.dirPrefId;
+
+ if (aPrefId == "ldap_2.servers.pab" || bPrefId == "ldap_2.servers.history") {
+ return -1;
+ }
+ if (bPrefId == "ldap_2.servers.pab" || aPrefId == "ldap_2.servers.history") {
+ return 1;
+ }
+
+ // Order remaining directories by type.
+ let aType = a.dirType;
+ let bType = b.dirType;
+
+ if (aType != bType) {
+ return abSortOrder[aType] - abSortOrder[bType];
+ }
+
+ // Order directories of the same type by name, case-insensitively.
+ return abNameComparer.compare(a.dirName, b.dirName);
+}
+
+const exportAttributes = [
+ ["FirstName", 2100],
+ ["LastName", 2101],
+ ["DisplayName", 2102],
+ ["NickName", 2103],
+ ["PrimaryEmail", 2104],
+ ["SecondEmail", 2105],
+ ["_AimScreenName", 2136],
+ ["LastModifiedDate", 0],
+ ["WorkPhone", 2106],
+ ["WorkPhoneType", 0],
+ ["HomePhone", 2107],
+ ["HomePhoneType", 0],
+ ["FaxNumber", 2108],
+ ["FaxNumberType", 0],
+ ["PagerNumber", 2109],
+ ["PagerNumberType", 0],
+ ["CellularNumber", 2110],
+ ["CellularNumberType", 0],
+ ["HomeAddress", 2111],
+ ["HomeAddress2", 2112],
+ ["HomeCity", 2113],
+ ["HomeState", 2114],
+ ["HomeZipCode", 2115],
+ ["HomeCountry", 2116],
+ ["WorkAddress", 2117],
+ ["WorkAddress2", 2118],
+ ["WorkCity", 2119],
+ ["WorkState", 2120],
+ ["WorkZipCode", 2121],
+ ["WorkCountry", 2122],
+ ["JobTitle", 2123],
+ ["Department", 2124],
+ ["Company", 2125],
+ ["WebPage1", 2126],
+ ["WebPage2", 2127],
+ ["BirthYear", 2128],
+ ["BirthMonth", 2129],
+ ["BirthDay", 2130],
+ ["Custom1", 2131],
+ ["Custom2", 2132],
+ ["Custom3", 2133],
+ ["Custom4", 2134],
+ ["Notes", 2135],
+ ["AnniversaryYear", 0],
+ ["AnniversaryMonth", 0],
+ ["AnniversaryDay", 0],
+ ["SpouseName", 0],
+ ["FamilyName", 0],
+];
+const LINEBREAK = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+var AddrBookUtils = {
+ compareAddressBooks,
+ async exportDirectory(directory) {
+ let systemCharset = "utf-8";
+ if (AppConstants.platform == "win") {
+ // Some Windows applications (notably Outlook) still don't understand
+ // UTF-8 encoding when importing address books and instead use the current
+ // operating system encoding. We can get that encoding from the registry.
+ let registryKey = Cc[
+ "@mozilla.org/windows-registry-key;1"
+ ].createInstance(Ci.nsIWindowsRegKey);
+ registryKey.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage",
+ Ci.nsIWindowsRegKey.ACCESS_READ
+ );
+ let acpValue = registryKey.readStringValue("ACP");
+
+ // This data converts the registry key value into encodings that
+ // nsIConverterOutputStream understands. It is from
+ // https://github.com/hsivonen/encoding_rs/blob/c3eb642cdf3f17003b8dac95c8fff478568e46da/generate-encoding-data.py#L188
+ systemCharset =
+ {
+ 866: "IBM866",
+ 874: "windows-874",
+ 932: "Shift_JIS",
+ 936: "GBK",
+ 949: "EUC-KR",
+ 950: "Big5",
+ 1200: "UTF-16LE",
+ 1201: "UTF-16BE",
+ 1250: "windows-1250",
+ 1251: "windows-1251",
+ 1252: "windows-1252",
+ 1253: "windows-1253",
+ 1254: "windows-1254",
+ 1255: "windows-1255",
+ 1256: "windows-1256",
+ 1257: "windows-1257",
+ 1258: "windows-1258",
+ 10000: "macintosh",
+ 10017: "x-mac-cyrillic",
+ 20866: "KOI8-R",
+ 20932: "EUC-JP",
+ 21866: "KOI8-U",
+ 28592: "ISO-8859-2",
+ 28593: "ISO-8859-3",
+ 28594: "ISO-8859-4",
+ 28595: "ISO-8859-5",
+ 28596: "ISO-8859-6",
+ 28597: "ISO-8859-7",
+ 28598: "ISO-8859-8",
+ 28600: "ISO-8859-10",
+ 28603: "ISO-8859-13",
+ 28604: "ISO-8859-14",
+ 28605: "ISO-8859-15",
+ 28606: "ISO-8859-16",
+ 38598: "ISO-8859-8-I",
+ 50221: "ISO-2022-JP",
+ 54936: "gb18030",
+ }[acpValue] || systemCharset;
+ }
+
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+
+ let title = bundle.formatStringFromName("ExportAddressBookNameTitle", [
+ directory.dirName,
+ ]);
+ filePicker.init(Services.ww.activeWindow, title, Ci.nsIFilePicker.modeSave);
+ filePicker.defaultString = directory.dirName;
+
+ let filterString;
+ // Since the list of file picker filters isn't fixed, keep track of which
+ // ones are added, so we can use them in the switch block below.
+ let activeFilters = [];
+
+ // CSV
+ if (systemCharset != "utf-8") {
+ filterString = bundle.GetStringFromName("CSVFilesSysCharset");
+ filePicker.appendFilter(filterString, "*.csv");
+ activeFilters.push("CSVFilesSysCharset");
+ }
+ filterString = bundle.GetStringFromName("CSVFilesUTF8");
+ filePicker.appendFilter(filterString, "*.csv");
+ activeFilters.push("CSVFilesUTF8");
+
+ // Tab separated
+ if (systemCharset != "utf-8") {
+ filterString = bundle.GetStringFromName("TABFilesSysCharset");
+ filePicker.appendFilter(filterString, "*.tab; *.txt");
+ activeFilters.push("TABFilesSysCharset");
+ }
+ filterString = bundle.GetStringFromName("TABFilesUTF8");
+ filePicker.appendFilter(filterString, "*.tab; *.txt");
+ activeFilters.push("TABFilesUTF8");
+
+ // vCard
+ filterString = bundle.GetStringFromName("VCFFiles");
+ filePicker.appendFilter(filterString, "*.vcf");
+ activeFilters.push("VCFFiles");
+
+ // LDIF
+ filterString = bundle.GetStringFromName("LDIFFiles");
+ filePicker.appendFilter(filterString, "*.ldi; *.ldif");
+ activeFilters.push("LDIFFiles");
+
+ let rv = await new Promise(resolve => filePicker.open(resolve));
+ if (
+ rv == Ci.nsIFilePicker.returnCancel ||
+ !filePicker.file ||
+ !filePicker.file.path
+ ) {
+ return;
+ }
+
+ if (rv == Ci.nsIFilePicker.returnReplace) {
+ if (filePicker.file.isFile()) {
+ filePicker.file.remove(false);
+ }
+ }
+
+ let exportFile = filePicker.file.clone();
+ let leafName = exportFile.leafName;
+ let output = "";
+ let charset = "utf-8";
+
+ switch (activeFilters[filePicker.filterIndex]) {
+ case "CSVFilesSysCharset":
+ charset = systemCharset;
+ // Falls through.
+ case "CSVFilesUTF8":
+ if (!leafName.endsWith(".csv")) {
+ exportFile.leafName += ".csv";
+ }
+ output = AddrBookUtils.exportDirectoryToDelimitedText(directory, ",");
+ break;
+ case "TABFilesSysCharset":
+ charset = systemCharset;
+ // Falls through.
+ case "TABFilesUTF8":
+ if (!leafName.endsWith(".txt") && !leafName.endsWith(".tab")) {
+ exportFile.leafName += ".txt";
+ }
+ output = AddrBookUtils.exportDirectoryToDelimitedText(directory, "\t");
+ break;
+ case "VCFFiles":
+ if (!leafName.endsWith(".vcf")) {
+ exportFile.leafName += ".vcf";
+ }
+ output = AddrBookUtils.exportDirectoryToVCard(directory);
+ break;
+ case "LDIFFiles":
+ if (!leafName.endsWith(".ldi") && !leafName.endsWith(".ldif")) {
+ exportFile.leafName += ".ldif";
+ }
+ output = AddrBookUtils.exportDirectoryToLDIF(directory);
+ break;
+ }
+
+ if (charset == "utf-8") {
+ await IOUtils.writeUTF8(exportFile.path, output);
+ return;
+ }
+
+ let outputFileStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outputFileStream.init(exportFile, -1, -1, 0);
+ let outputStream = Cc[
+ "@mozilla.org/intl/converter-output-stream;1"
+ ].createInstance(Ci.nsIConverterOutputStream);
+ outputStream.init(outputFileStream, charset);
+ outputStream.writeString(output);
+ outputStream.close();
+ },
+ exportDirectoryToDelimitedText(directory, delimiter) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+ let output = "";
+ for (let i = 0; i < exportAttributes.length; i++) {
+ let [, plainTextStringID] = exportAttributes[i];
+ if (plainTextStringID != 0) {
+ if (i != 0) {
+ output += delimiter;
+ }
+ output += bundle.GetStringFromID(plainTextStringID);
+ }
+ }
+ output += LINEBREAK;
+ for (let card of directory.childCards) {
+ if (card.isMailList) {
+ // .tab, .txt and .csv aren't able to export mailing lists.
+ // Use LDIF for that.
+ continue;
+ }
+ let propertyMap = card.supportsVCard
+ ? card.vCardProperties.toPropertyMap()
+ : null;
+ for (let i = 0; i < exportAttributes.length; i++) {
+ let [abPropertyName, plainTextStringID] = exportAttributes[i];
+ if (plainTextStringID == 0) {
+ continue;
+ }
+ if (i != 0) {
+ output += delimiter;
+ }
+ let value;
+ if (propertyMap) {
+ value = propertyMap.get(abPropertyName);
+ }
+ if (!value) {
+ value = card.getProperty(abPropertyName, "");
+ }
+
+ // If a string contains at least one comma, tab, double quote or line
+ // break then we need to quote the entire string. Also if double quote
+ // is part of the string we need to quote the double quote(s) as well.
+ let needsQuotes = false;
+ if (value.includes('"')) {
+ needsQuotes = true;
+ value = value.replace(/"/g, '""');
+ } else if (/[,\t\r\n]/.test(value)) {
+ needsQuotes = true;
+ }
+ if (needsQuotes) {
+ value = `"${value}"`;
+ }
+
+ output += value;
+ }
+ output += LINEBREAK;
+ }
+
+ return output;
+ },
+ exportDirectoryToLDIF(directory) {
+ function appendProperty(name, value) {
+ if (!value) {
+ return;
+ }
+ // Follow RFC 2849 to determine if something is safe "as is" for LDIF.
+ // If not, base 64 encode it as UTF-8.
+ if (
+ value[0] == " " ||
+ value[0] == ":" ||
+ value[0] == "<" ||
+ /[\0\r\n\u0080-\uffff]/.test(value)
+ ) {
+ // Convert 16bit JavaScript string to a byteString, to make it work with
+ // btoa().
+ let byteString = MailStringUtils.stringToByteString(value);
+ output += name + ":: " + btoa(byteString) + LINEBREAK;
+ } else {
+ output += name + ": " + value + LINEBREAK;
+ }
+ }
+
+ function appendDNForCard(property, card, attrMap) {
+ let value = "";
+ if (card.displayName) {
+ value +=
+ attrMap.getFirstAttribute("DisplayName") + "=" + card.displayName;
+ }
+ if (card.primaryEmail) {
+ if (card.displayName) {
+ value += ",";
+ }
+ value +=
+ attrMap.getFirstAttribute("PrimaryEmail") + "=" + card.primaryEmail;
+ }
+ appendProperty(property, value);
+ }
+
+ let output = "";
+ let attrMap = lazy.attrMapService.getMapForPrefBranch(
+ "ldap_2.servers.default.attrmap"
+ );
+
+ for (let card of directory.childCards) {
+ if (card.isMailList) {
+ appendDNForCard("dn", card, attrMap);
+ appendProperty("objectclass", "top");
+ appendProperty("objectclass", "groupOfNames");
+ appendProperty(
+ attrMap.getFirstAttribute("DisplayName"),
+ card.displayName
+ );
+ if (card.getProperty("NickName", "")) {
+ appendProperty(
+ attrMap.getFirstAttribute("NickName"),
+ card.getProperty("NickName", "")
+ );
+ }
+ if (card.getProperty("Notes", "")) {
+ appendProperty(
+ attrMap.getFirstAttribute("Notes"),
+ card.getProperty("Notes", "")
+ );
+ }
+ let listAsDirectory = MailServices.ab.getDirectory(card.mailListURI);
+ for (let childCard of listAsDirectory.childCards) {
+ appendDNForCard("member", childCard, attrMap);
+ }
+ } else {
+ appendDNForCard("dn", card, attrMap);
+ appendProperty("objectclass", "top");
+ appendProperty("objectclass", "person");
+ appendProperty("objectclass", "organizationalPerson");
+ appendProperty("objectclass", "inetOrgPerson");
+ appendProperty("objectclass", "mozillaAbPersonAlpha");
+
+ let propertyMap = card.supportsVCard
+ ? card.vCardProperties.toPropertyMap()
+ : null;
+ for (let [abPropertyName] of exportAttributes) {
+ let attrName = attrMap.getFirstAttribute(abPropertyName);
+ if (attrName) {
+ let attrValue;
+ if (propertyMap) {
+ attrValue = propertyMap.get(abPropertyName);
+ }
+ if (!attrValue) {
+ attrValue = card.getProperty(abPropertyName, "");
+ }
+ appendProperty(attrName, attrValue);
+ }
+ }
+ }
+ output += LINEBREAK;
+ }
+
+ return output;
+ },
+ exportDirectoryToVCard(directory) {
+ let output = "";
+ for (let card of directory.childCards) {
+ if (!card.isMailList) {
+ // We don't know how to export mailing lists to vcf.
+ // Use LDIF for that.
+ output += decodeURIComponent(card.translateTo("vcard"));
+ }
+ }
+ return output;
+ },
+ newUID,
+ SimpleEnumerator,
+};
diff --git a/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm b/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm
new file mode 100644
index 0000000000..e4361d53bb
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm
@@ -0,0 +1,925 @@
+/* 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 = ["CardDAVDirectory"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+const { SQLiteDirectory } = ChromeUtils.import(
+ "resource:///modules/SQLiteDirectory.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CardDAVUtils: "resource:///modules/CardDAVUtils.jsm",
+ NotificationCallbacks: "resource:///modules/CardDAVUtils.jsm",
+ OAuth2Module: "resource:///modules/OAuth2Module.jsm",
+ OAuth2Providers: "resource:///modules/OAuth2Providers.jsm",
+ VCardProperties: "resource:///modules/VCardUtils.jsm",
+ VCardUtils: "resource:///modules/VCardUtils.jsm",
+});
+
+const PREFIX_BINDINGS = {
+ card: "urn:ietf:params:xml:ns:carddav",
+ cs: "http://calendarserver.org/ns/",
+ d: "DAV:",
+};
+const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS)
+ .map(([prefix, url]) => `xmlns:${prefix}="${url}"`)
+ .join(" ");
+
+const log = console.createInstance({
+ prefix: "carddav.sync",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "carddav.sync.loglevel",
+});
+
+/**
+ * Adds CardDAV sync to SQLiteDirectory.
+ */
+class CardDAVDirectory extends SQLiteDirectory {
+ /** nsIAbDirectory */
+
+ init(uri) {
+ super.init(uri);
+
+ let serverURL = this._serverURL;
+ if (serverURL) {
+ // Google's server enforces some vCard 3.0-isms (or just fails badly if
+ // you don't provide exactly what it wants) so we use this property to
+ // determine when to do things differently. Cards from this directory
+ // inherit the same property.
+ if (this.getBoolValue("carddav.vcard3")) {
+ this._isGoogleCardDAV = true;
+ } else {
+ this._isGoogleCardDAV = serverURL.startsWith(
+ "https://www.googleapis.com/"
+ );
+ if (this._isGoogleCardDAV) {
+ this.setBoolValue("carddav.vcard3", true);
+ }
+ }
+
+ // If this directory is configured, start sync'ing with the server in 30s.
+ // Don't do this immediately, as this code runs at start-up and could
+ // impact performance if there are lots of changes to process.
+ if (this.getIntValue("carddav.syncinterval", 30) > 0) {
+ this._syncTimer = lazy.setTimeout(() => this.syncWithServer(), 30000);
+ }
+ }
+
+ let uidsToSync = this.getStringValue("carddav.uidsToSync", "");
+ if (uidsToSync) {
+ this._uidsToSync = new Set(uidsToSync.split(" ").filter(Boolean));
+ this.setStringValue("carddav.uidsToSync", "");
+ log.debug(`Retrieved list of cards to sync: ${uidsToSync}`);
+ } else {
+ this._uidsToSync = new Set();
+ }
+
+ let hrefsToRemove = this.getStringValue("carddav.hrefsToRemove", "");
+ if (hrefsToRemove) {
+ this._hrefsToRemove = new Set(hrefsToRemove.split(" ").filter(Boolean));
+ this.setStringValue("carddav.hrefsToRemove", "");
+ log.debug(`Retrieved list of cards to remove: ${hrefsToRemove}`);
+ } else {
+ this._hrefsToRemove = new Set();
+ }
+ }
+ async cleanUp() {
+ await super.cleanUp();
+
+ if (this._syncTimer) {
+ lazy.clearInterval(this._syncTimer);
+ this._syncTimer = null;
+ }
+
+ if (this._uidsToSync.size) {
+ let uidsToSync = [...this._uidsToSync].join(" ");
+ this.setStringValue("carddav.uidsToSync", uidsToSync);
+ log.debug(`Stored list of cards to sync: ${uidsToSync}`);
+ }
+ if (this._hrefsToRemove.size) {
+ let hrefsToRemove = [...this._hrefsToRemove].join(" ");
+ this.setStringValue("carddav.hrefsToRemove", hrefsToRemove);
+ log.debug(`Stored list of cards to remove: ${hrefsToRemove}`);
+ }
+ }
+
+ get propertiesChromeURI() {
+ return "chrome://messenger/content/addressbook/abCardDAVProperties.xhtml";
+ }
+ get dirType() {
+ return Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE;
+ }
+ get supportsMailingLists() {
+ return false;
+ }
+
+ modifyCard(card) {
+ // Well this is awkward. Because it's defined in nsIAbDirectory,
+ // modifyCard must not be async, but we need to do async operations.
+ let newCard = super.modifyCard(card);
+ this._modifyCard(newCard);
+ }
+ async _modifyCard(card) {
+ try {
+ await this._sendCardToServer(card);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ deleteCards(cards) {
+ super.deleteCards(cards);
+ this._deleteCards(cards);
+ }
+ async _deleteCards(cards) {
+ for (let card of cards) {
+ try {
+ await this._deleteCardFromServer(card);
+ } catch (ex) {
+ console.error(ex);
+ break;
+ }
+ }
+
+ for (let card of cards) {
+ this._uidsToSync.delete(card.UID);
+ }
+ }
+ dropCard(card, needToCopyCard) {
+ // Ideally, we'd not add the card until it was on the server, but we have
+ // to return newCard synchronously.
+ let newCard = super.dropCard(card, needToCopyCard);
+ this._sendCardToServer(newCard).catch(console.error);
+ return newCard;
+ }
+ addMailList() {
+ throw Components.Exception(
+ "CardDAVDirectory does not implement addMailList",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ setIntValue(name, value) {
+ super.setIntValue(name, value);
+
+ // Capture changes to the sync interval from the UI.
+ if (name == "carddav.syncinterval") {
+ this._scheduleNextSync();
+ }
+ }
+
+ /** CardDAV specific */
+ _syncInProgress = false;
+ _syncTimer = null;
+
+ get _serverURL() {
+ return this.getStringValue("carddav.url", "");
+ }
+ get _syncToken() {
+ return this.getStringValue("carddav.token", "");
+ }
+ set _syncToken(value) {
+ this.setStringValue("carddav.token", value);
+ }
+
+ /**
+ * Wraps CardDAVUtils.makeRequest, resolving path this directory's server
+ * URL, and providing a mechanism to give a username and password specific
+ * to this directory.
+ *
+ * @param {string} path - A path relative to the server URL.
+ * @param {object} details - See CardDAVUtils.makeRequest.
+ * @returns {Promise<object>} - See CardDAVUtils.makeRequest.
+ */
+ async _makeRequest(path, details = {}) {
+ let serverURI = Services.io.newURI(this._serverURL);
+ let uri = serverURI.resolve(path);
+
+ if (!("_oAuth" in this)) {
+ if (lazy.OAuth2Providers.getHostnameDetails(serverURI.host)) {
+ this._oAuth = new lazy.OAuth2Module();
+ this._oAuth.initFromABDirectory(this, serverURI.host);
+ } else {
+ this._oAuth = null;
+ }
+ }
+ details.oAuth = this._oAuth;
+
+ let username = this.getStringValue("carddav.username", "");
+ let callbacks = new lazy.NotificationCallbacks(username);
+ details.callbacks = callbacks;
+
+ details.userContextId =
+ this._userContextId ?? lazy.CardDAVUtils.contextForUsername(username);
+
+ let response;
+ try {
+ Services.obs.notifyObservers(
+ this,
+ "addrbook-directory-request-start",
+ this.UID
+ );
+ response = await lazy.CardDAVUtils.makeRequest(uri, details);
+ } finally {
+ Services.obs.notifyObservers(
+ this,
+ "addrbook-directory-request-end",
+ this.UID
+ );
+ }
+ if (
+ details.expectedStatuses &&
+ !details.expectedStatuses.includes(response.status)
+ ) {
+ throw Components.Exception(
+ `Incorrect response from server: ${response.status} ${response.statusText}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (callbacks.shouldSaveAuth) {
+ // The user was prompted for a username and password. Save the response.
+ this.setStringValue("carddav.username", callbacks.authInfo?.username);
+ callbacks.saveAuth();
+ }
+ return response;
+ }
+
+ /**
+ * Gets or creates the path for storing this card on the server. Cards that
+ * already exist on the server have this value in the _href property.
+ *
+ * @param {nsIAbCard} card
+ * @returns {string}
+ */
+ _getCardHref(card) {
+ let href = card.getProperty("_href", "");
+ if (href) {
+ return href;
+ }
+ href = Services.io.newURI(this._serverURL).resolve(`${card.UID}.vcf`);
+ return new URL(href).pathname;
+ }
+
+ _multigetRequest(hrefsToFetch) {
+ hrefsToFetch = hrefsToFetch.map(
+ href => ` <d:href>${xmlEncode(href)}</d:href>`
+ );
+ let data = `<card:addressbook-multiget ${NAMESPACE_STRING}>
+ <d:prop>
+ <d:getetag/>
+ <card:address-data/>
+ </d:prop>
+ ${hrefsToFetch.join("\n")}
+ </card:addressbook-multiget>`;
+
+ return this._makeRequest("", {
+ method: "REPORT",
+ body: data,
+ headers: {
+ Depth: 1,
+ },
+ expectedStatuses: [207],
+ });
+ }
+
+ /**
+ * Performs a multiget request for the provided hrefs, and adds each response
+ * to the directory, adding or modifying as necessary.
+ *
+ * @param {string[]} hrefsToFetch - The href of each card to be requested.
+ */
+ async _fetchAndStore(hrefsToFetch) {
+ if (hrefsToFetch.length == 0) {
+ return;
+ }
+
+ let response = await this._multigetRequest(hrefsToFetch);
+
+ // If this directory is set to read-only, the following operations would
+ // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only
+ // directory, so set this._overrideReadOnly to avoid the exception.
+ //
+ // Do not use await while it is set, and use a try/finally block to ensure
+ // it is cleared.
+
+ try {
+ this._overrideReadOnly = true;
+ for (let { href, properties } of this._readResponse(response.dom)) {
+ if (!properties) {
+ continue;
+ }
+
+ let etag = properties.querySelector("getetag")?.textContent;
+ let vCard = normalizeLineEndings(
+ properties.querySelector("address-data")?.textContent
+ );
+
+ let abCard = lazy.VCardUtils.vCardToAbCard(vCard);
+ abCard.setProperty("_etag", etag);
+ abCard.setProperty("_href", href);
+
+ if (!this.cards.has(abCard.UID)) {
+ super.dropCard(abCard, false);
+ } else if (this.loadCardProperties(abCard.UID).get("_etag") != etag) {
+ super.modifyCard(abCard);
+ }
+ }
+ } finally {
+ this._overrideReadOnly = false;
+ }
+ }
+
+ /**
+ * Reads a multistatus response, yielding once for each response element.
+ *
+ * @param {Document} dom - as returned by CardDAVUtils.makeRequest.
+ * @yields {object} - An object representing a single <response> element
+ * from the document:
+ * - href, the href of the object represented
+ * - notFound, if a 404 status applies to this response
+ * - properties, the <prop> element, if any, containing properties
+ * of the object represented
+ */
+ _readResponse = function* (dom) {
+ if (!dom || dom.documentElement.localName != "multistatus") {
+ throw Components.Exception(
+ `Expected a multistatus response, but didn't get one`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ for (let r of dom.querySelectorAll("response")) {
+ let response = {
+ href: r.querySelector("href")?.textContent,
+ };
+
+ let responseStatus = r.querySelector("response > status");
+ if (responseStatus?.textContent.startsWith("HTTP/1.1 404")) {
+ response.notFound = true;
+ yield response;
+ continue;
+ }
+
+ for (let p of r.querySelectorAll("response > propstat")) {
+ let status = p.querySelector("propstat > status").textContent;
+ if (status == "HTTP/1.1 200 OK") {
+ response.properties = p.querySelector("propstat > prop");
+ }
+ }
+
+ yield response;
+ }
+ };
+
+ /**
+ * Converts the card to a vCard and performs a PUT request to store it on the
+ * server. Then immediately performs a GET request ensuring the local copy
+ * matches the server copy. Stores the card in the database on success.
+ *
+ * @param {nsIAbCard} card
+ * @returns {boolean} true if the PUT request succeeded without conflict,
+ * false if there was a conflict.
+ * @throws if the server responded with anything other than a success or
+ * conflict status code.
+ */
+ async _sendCardToServer(card) {
+ let href = this._getCardHref(card);
+ let requestDetails = {
+ method: "PUT",
+ contentType: "text/vcard",
+ };
+
+ let vCard = card.getProperty("_vCard", "");
+ if (this._isGoogleCardDAV) {
+ // There must be an `N` property, even if empty.
+ let vCardProperties = lazy.VCardProperties.fromVCard(vCard);
+ if (!vCardProperties.getFirstEntry("n")) {
+ vCardProperties.addValue("n", ["", "", "", "", ""]);
+ }
+ requestDetails.body = vCardProperties.toVCard();
+ } else {
+ requestDetails.body = vCard;
+ }
+
+ let response;
+ try {
+ log.debug(`Sending ${href} to server.`);
+ response = await this._makeRequest(href, requestDetails);
+ } catch (ex) {
+ Services.obs.notifyObservers(this, "addrbook-directory-sync-failed");
+ this._uidsToSync.add(card.UID);
+ throw ex;
+ }
+
+ if (response.status >= 400) {
+ throw Components.Exception(
+ `Sending card to the server failed, response was ${response.status} ${response.statusText}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ // At this point we *should* be able to make a simple GET request and
+ // store the response. But Google moves the data (fair enough) without
+ // telling us where it went (c'mon, really?). Fortunately a multiget
+ // request at the original location works.
+
+ response = await this._multigetRequest([href]);
+
+ for (let { href, properties } of this._readResponse(response.dom)) {
+ if (!properties) {
+ continue;
+ }
+
+ let etag = properties.querySelector("getetag")?.textContent;
+ let vCard = normalizeLineEndings(
+ properties.querySelector("address-data")?.textContent
+ );
+
+ let abCard = lazy.VCardUtils.vCardToAbCard(vCard);
+ abCard.setProperty("_etag", etag);
+ abCard.setProperty("_href", href);
+
+ if (abCard.UID == card.UID) {
+ super.modifyCard(abCard);
+ } else {
+ // Add a property so the UI can work out if it's still displaying the
+ // old card and respond appropriately.
+ abCard.setProperty("_originalUID", card.UID);
+ super.dropCard(abCard, false);
+ super.deleteCards([card]);
+ }
+ }
+ }
+
+ /**
+ * Deletes card from the server.
+ *
+ * @param {nsIAbCard|string} cardOrHRef
+ */
+ async _deleteCardFromServer(cardOrHRef) {
+ let href;
+ if (typeof cardOrHRef == "string") {
+ href = cardOrHRef;
+ } else {
+ href = cardOrHRef.getProperty("_href", "");
+ }
+ if (!href) {
+ return;
+ }
+
+ try {
+ log.debug(`Removing ${href} from server.`);
+ await this._makeRequest(href, { method: "DELETE" });
+ } catch (ex) {
+ Services.obs.notifyObservers(this, "addrbook-directory-sync-failed");
+ this._hrefsToRemove.add(href);
+ throw ex;
+ }
+ }
+
+ /**
+ * Set up a repeating timer for synchronisation with the server. The timer's
+ * interval is defined by pref, set it to 0 to disable sync'ing altogether.
+ */
+ _scheduleNextSync() {
+ if (this._syncTimer) {
+ lazy.clearInterval(this._syncTimer);
+ this._syncTimer = null;
+ }
+
+ let interval = this.getIntValue("carddav.syncinterval", 30);
+ if (interval <= 0) {
+ return;
+ }
+
+ this._syncTimer = lazy.setInterval(
+ () => this.syncWithServer(false),
+ interval * 60000
+ );
+ }
+
+ /**
+ * Get all cards on the server and add them to this directory.
+ *
+ * This is usually used for the initial population of a directory, but it
+ * can also be used for a complete re-sync.
+ */
+ async fetchAllFromServer() {
+ log.log("Fetching all cards from the server.");
+ this._syncInProgress = true;
+
+ let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <prop>
+ <resourcetype/>
+ <getetag/>
+ <cs:getctag/>
+ </prop>
+ </propfind>`;
+
+ let response = await this._makeRequest("", {
+ method: "PROPFIND",
+ body: data,
+ headers: {
+ Depth: 1,
+ },
+ expectedStatuses: [207],
+ });
+
+ // A map of all existing hrefs and etags. If the etag for an href matches
+ // what we already have, we won't fetch it.
+ let currentHrefs = new Map(
+ Array.from(this.cards.values(), c => [c.get("_href"), c.get("_etag")])
+ );
+
+ let hrefsToFetch = [];
+ for (let { href, properties } of this._readResponse(response.dom)) {
+ if (!properties || properties.querySelector("resourcetype collection")) {
+ continue;
+ }
+
+ let currentEtag = currentHrefs.get(href);
+ currentHrefs.delete(href);
+
+ let etag = properties.querySelector("getetag")?.textContent;
+ if (etag && currentEtag == etag) {
+ continue;
+ }
+
+ hrefsToFetch.push(href);
+ }
+
+ // Delete any existing cards we didn't see. They're not on the server so
+ // they shouldn't be on the client.
+ let cardsToDelete = [];
+ for (let href of currentHrefs.keys()) {
+ cardsToDelete.push(this.getCardFromProperty("_href", href, true));
+ }
+ if (cardsToDelete.length > 0) {
+ super.deleteCards(cardsToDelete);
+ }
+
+ // Fetch any cards we don't already have, or that have changed.
+ if (hrefsToFetch.length > 0) {
+ response = await this._multigetRequest(hrefsToFetch);
+
+ let abCards = [];
+
+ for (let { href, properties } of this._readResponse(response.dom)) {
+ if (!properties) {
+ continue;
+ }
+
+ let etag = properties.querySelector("getetag")?.textContent;
+ let vCard = normalizeLineEndings(
+ properties.querySelector("address-data")?.textContent
+ );
+
+ try {
+ let abCard = lazy.VCardUtils.vCardToAbCard(vCard);
+ abCard.setProperty("_etag", etag);
+ abCard.setProperty("_href", href);
+ abCards.push(abCard);
+ } catch (ex) {
+ log.error(`Error parsing: ${vCard}`);
+ console.error(ex);
+ }
+ }
+
+ await this.bulkAddCards(abCards);
+ }
+
+ await this._getSyncToken();
+
+ log.log("Sync with server completed successfully.");
+ Services.obs.notifyObservers(this, "addrbook-directory-synced");
+
+ this._scheduleNextSync();
+ this._syncInProgress = false;
+ }
+
+ /**
+ * Begin a sync operation. This function will decide which sync protocol to
+ * use based on the directory's configuration. It will also (re)start the
+ * timer for the next synchronisation unless told not to.
+ *
+ * @param {boolean} shouldResetTimer
+ */
+ async syncWithServer(shouldResetTimer = true) {
+ if (this._syncInProgress || !this._serverURL) {
+ return;
+ }
+
+ log.log("Performing sync with server.");
+ this._syncInProgress = true;
+
+ try {
+ // First perform all pending removals. We don't want to have deleted cards
+ // reappearing when we sync.
+ for (let href of this._hrefsToRemove) {
+ await this._deleteCardFromServer(href);
+ }
+ this._hrefsToRemove.clear();
+
+ // Now update any cards that were modified while not connected to the server.
+ for (let uid of this._uidsToSync) {
+ let card = this.getCard(uid);
+ // The card may no longer exist. It shouldn't still be listed to send,
+ // but it might be.
+ if (card) {
+ await this._sendCardToServer(card);
+ }
+ }
+ this._uidsToSync.clear();
+
+ if (this._syncToken) {
+ await this.updateAllFromServerV2();
+ } else {
+ await this.updateAllFromServerV1();
+ }
+ } catch (ex) {
+ log.error("Sync with server failed.");
+ throw ex;
+ } finally {
+ if (shouldResetTimer) {
+ this._scheduleNextSync();
+ }
+ this._syncInProgress = false;
+ }
+ }
+
+ /**
+ * Compares cards in the directory with cards on the server, and updates the
+ * directory to match what is on the server.
+ */
+ async updateAllFromServerV1() {
+ let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <prop>
+ <resourcetype/>
+ <getetag/>
+ <cs:getctag/>
+ </prop>
+ </propfind>`;
+
+ let response = await this._makeRequest("", {
+ method: "PROPFIND",
+ body: data,
+ headers: {
+ Depth: 1,
+ },
+ expectedStatuses: [207],
+ });
+
+ let hrefMap = new Map();
+ for (let { href, properties } of this._readResponse(response.dom)) {
+ if (
+ !properties ||
+ !properties.querySelector("resourcetype") ||
+ properties.querySelector("resourcetype collection")
+ ) {
+ continue;
+ }
+
+ let etag = properties.querySelector("getetag").textContent;
+ hrefMap.set(href, etag);
+ }
+
+ let cardMap = new Map();
+ let hrefsToFetch = [];
+ let cardsToDelete = [];
+ for (let card of this.childCards) {
+ let href = card.getProperty("_href", "");
+ let etag = card.getProperty("_etag", "");
+
+ if (!href || !etag) {
+ // Not sure how we got here. Ignore it.
+ continue;
+ }
+ cardMap.set(href, card);
+ if (hrefMap.has(href)) {
+ if (hrefMap.get(href) != etag) {
+ // The card was updated on server.
+ hrefsToFetch.push(href);
+ }
+ } else {
+ // The card doesn't exist on the server.
+ cardsToDelete.push(card);
+ }
+ }
+
+ for (let href of hrefMap.keys()) {
+ if (!cardMap.has(href)) {
+ // The card is new on the server.
+ hrefsToFetch.push(href);
+ }
+ }
+
+ // If this directory is set to read-only, the following operations would
+ // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only
+ // directory, so set this._overrideReadOnly to avoid the exception.
+ //
+ // Do not use await while it is set, and use a try/finally block to ensure
+ // it is cleared.
+
+ if (cardsToDelete.length > 0) {
+ this._overrideReadOnly = true;
+ try {
+ super.deleteCards(cardsToDelete);
+ } finally {
+ this._overrideReadOnly = false;
+ }
+ }
+
+ await this._fetchAndStore(hrefsToFetch);
+
+ log.log("Sync with server completed successfully.");
+ Services.obs.notifyObservers(this, "addrbook-directory-synced");
+ }
+
+ /**
+ * Retrieves the current sync token from the server.
+ *
+ * @see RFC 6578
+ */
+ async _getSyncToken() {
+ log.log("Fetching new sync token");
+
+ let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <prop>
+ <displayname/>
+ <cs:getctag/>
+ <sync-token/>
+ </prop>
+ </propfind>`;
+
+ let response = await this._makeRequest("", {
+ method: "PROPFIND",
+ body: data,
+ headers: {
+ Depth: 0,
+ },
+ });
+
+ if (response.status == 207) {
+ for (let { properties } of this._readResponse(response.dom)) {
+ let token = properties?.querySelector("prop sync-token");
+ if (token) {
+ this._syncToken = token.textContent;
+ return;
+ }
+ }
+ }
+
+ this._syncToken = "";
+ }
+
+ /**
+ * Gets a list of changes on the server since the last call to getSyncToken
+ * or updateAllFromServerV2, and updates the directory to match what is on
+ * the server.
+ *
+ * @see RFC 6578
+ */
+ async updateAllFromServerV2() {
+ let syncToken = this._syncToken;
+ if (!syncToken) {
+ throw new Components.Exception("No sync token", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let data = `<sync-collection xmlns="${
+ PREFIX_BINDINGS.d
+ }" ${NAMESPACE_STRING}>
+ <sync-token>${xmlEncode(syncToken)}</sync-token>
+ <sync-level>1</sync-level>
+ <prop>
+ <getetag/>
+ <card:address-data/>
+ </prop>
+ </sync-collection>`;
+
+ let response = await this._makeRequest("", {
+ method: "REPORT",
+ body: data,
+ headers: {
+ Depth: 1, // Only Google seems to need this.
+ },
+ expectedStatuses: [207, 400],
+ });
+
+ if (response.status == 400) {
+ log.warn(
+ `Server responded with: ${response.status} ${response.statusText}`
+ );
+ await this.fetchAllFromServer();
+ return;
+ }
+
+ let dom = response.dom;
+
+ // If this directory is set to read-only, the following operations would
+ // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only
+ // directory, so set this._overrideReadOnly to avoid the exception.
+ //
+ // Do not use await while it is set, and use a try/finally block to ensure
+ // it is cleared.
+
+ let hrefsToFetch = [];
+ try {
+ this._overrideReadOnly = true;
+ let cardsToDelete = [];
+ for (let { href, notFound, properties } of this._readResponse(dom)) {
+ let card = this.getCardFromProperty("_href", href, true);
+ if (notFound) {
+ if (card) {
+ cardsToDelete.push(card);
+ }
+ continue;
+ }
+ if (!properties) {
+ continue;
+ }
+
+ let etag = properties.querySelector("getetag")?.textContent;
+ if (!etag) {
+ continue;
+ }
+ let vCard = properties.querySelector("address-data")?.textContent;
+ if (!vCard) {
+ hrefsToFetch.push(href);
+ continue;
+ }
+ vCard = normalizeLineEndings(vCard);
+
+ let abCard = lazy.VCardUtils.vCardToAbCard(vCard);
+ abCard.setProperty("_etag", etag);
+ abCard.setProperty("_href", href);
+
+ if (card) {
+ if (card.getProperty("_etag", "") != etag) {
+ super.modifyCard(abCard);
+ }
+ } else {
+ super.dropCard(abCard, false);
+ }
+ }
+
+ if (cardsToDelete.length > 0) {
+ super.deleteCards(cardsToDelete);
+ }
+ } finally {
+ this._overrideReadOnly = false;
+ }
+
+ await this._fetchAndStore(hrefsToFetch);
+
+ this._syncToken = dom.querySelector("sync-token").textContent;
+
+ log.log("Sync with server completed successfully.");
+ Services.obs.notifyObservers(this, "addrbook-directory-synced");
+ }
+
+ static forFile(fileName) {
+ let directory = super.forFile(fileName);
+ if (directory instanceof CardDAVDirectory) {
+ return directory;
+ }
+ return undefined;
+ }
+}
+CardDAVDirectory.prototype.classID = Components.ID(
+ "{1fa9941a-07d5-4a6f-9673-15327fc2b9ab}"
+);
+
+/**
+ * Ensure that `string` always has Windows line-endings. Some functions,
+ * notably DOMParser.parseFromString, strip \r, but we want it because \r\n
+ * is a part of the vCard specification.
+ */
+function normalizeLineEndings(string) {
+ if (string.includes("\r\n")) {
+ return string;
+ }
+ return string.replace(/\n/g, "\r\n");
+}
+
+/**
+ * Encode special characters safely for XML.
+ */
+function xmlEncode(string) {
+ return string
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
diff --git a/comm/mailnews/addrbook/modules/CardDAVUtils.jsm b/comm/mailnews/addrbook/modules/CardDAVUtils.jsm
new file mode 100644
index 0000000000..d45b5a9b42
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/CardDAVUtils.jsm
@@ -0,0 +1,718 @@
+/* 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 = ["CardDAVUtils", "NotificationCallbacks"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm");
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CardDAVDirectory: "resource:///modules/CardDAVDirectory.jsm",
+ MsgAuthPrompt: "resource:///modules/MsgAsyncPrompter.jsm",
+ OAuth2: "resource:///modules/OAuth2.jsm",
+ OAuth2Providers: "resource:///modules/OAuth2Providers.jsm",
+});
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "nssErrorsService",
+ "@mozilla.org/nss_errors_service;1",
+ "nsINSSErrorsService"
+);
+
+// Use presets only where DNS discovery fails. Set to null to prevent
+// auto-fill completely for a domain.
+const PRESETS = {
+ // For testing purposes.
+ "bad.invalid": null,
+ // Google responds correctly but the provided address returns 404.
+ "gmail.com": "https://www.googleapis.com",
+ "googlemail.com": "https://www.googleapis.com",
+ // For testing purposes.
+ "test.invalid": "http://localhost:9999",
+ // Yahoo! OAuth is not working yet.
+ "yahoo.com": null,
+};
+
+// At least one of these ACL privileges must be present to consider an address
+// book writable.
+const writePrivs = ["write", "write-properties", "write-content", "all"];
+
+// At least one of these ACL privileges must be present to consider an address
+// book readable.
+const readPrivs = ["read", "all"];
+
+var CardDAVUtils = {
+ _contextMap: new Map(),
+
+ /**
+ * Returns the id of a unique private context for each username. When the
+ * userContextId is set on a principal, this allows the use of multiple
+ * usernames on the same server without the networking code causing issues.
+ *
+ * @param {string} username
+ * @returns {integer}
+ */
+ contextForUsername(username) {
+ if (username && CardDAVUtils._contextMap.has(username)) {
+ return CardDAVUtils._contextMap.get(username);
+ }
+
+ // This could be any 32-bit integer, as long as it isn't already in use.
+ let nextId = 25000 + CardDAVUtils._contextMap.size;
+ lazy.ContextualIdentityService.remove(nextId);
+ CardDAVUtils._contextMap.set(username, nextId);
+ return nextId;
+ },
+
+ /**
+ * Make an HTTP request. If the request needs a username and password, the
+ * given authPrompt is called.
+ *
+ * @param {string} uri
+ * @param {object} details
+ * @param {string} [details.method]
+ * @param {object} [details.headers]
+ * @param {string} [details.body]
+ * @param {string} [details.contentType]
+ * @param {msgIOAuth2Module} [details.oAuth] - If this is present the
+ * request will use OAuth2 authorization.
+ * @param {NotificationCallbacks} [details.callbacks] - Handles usernames
+ * and passwords for this request.
+ * @param {integer} [details.userContextId] - See _contextForUsername.
+ *
+ * @returns {Promise<object>} - Resolves to an object with getters for:
+ * - status, the HTTP response code
+ * - statusText, the HTTP response message
+ * - text, the returned data as a String
+ * - dom, the returned data parsed into a Document
+ */
+ async makeRequest(uri, details) {
+ if (typeof uri == "string") {
+ uri = Services.io.newURI(uri);
+ }
+ let {
+ method = "GET",
+ headers = {},
+ body = null,
+ contentType = "text/xml",
+ oAuth = null,
+ callbacks = new NotificationCallbacks(),
+ userContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID,
+ } = details;
+ headers["Content-Type"] = contentType;
+ if (oAuth) {
+ headers.Authorization = await new Promise((resolve, reject) => {
+ oAuth.connect(true, {
+ onSuccess(token) {
+ resolve(
+ // `token` is a base64-encoded string for SASL XOAUTH2. That is
+ // not what we want, extract just the Bearer token part.
+ // (See OAuth2Module.connect.)
+ atob(token).split("\x01")[1].slice(5)
+ );
+ },
+ onFailure: reject,
+ });
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ { userContextId }
+ );
+
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ principal,
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ for (let [name, value] of Object.entries(headers)) {
+ channel.setRequestHeader(name, value, false);
+ }
+ if (body !== null) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.setUTF8Data(body, body.length);
+
+ channel.QueryInterface(Ci.nsIUploadChannel);
+ channel.setUploadStream(stream, contentType, -1);
+ }
+ channel.requestMethod = method; // Must go after setUploadStream.
+ channel.notificationCallbacks = callbacks;
+
+ let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
+ Ci.nsIStreamLoader
+ );
+ listener.init({
+ onStreamComplete(loader, context, status, resultLength, result) {
+ let finalChannel = loader.request.QueryInterface(Ci.nsIHttpChannel);
+ if (!Components.isSuccessCode(status)) {
+ let isCertError = false;
+ try {
+ let errorType = lazy.nssErrorsService.getErrorClass(status);
+ if (errorType == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ isCertError = true;
+ }
+ } catch (ex) {
+ // nsINSSErrorsService.getErrorClass throws if given a non-TLS,
+ // non-cert error, so ignore this.
+ }
+
+ if (isCertError && finalChannel.securityInfo) {
+ let secInfo = finalChannel.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ );
+ let params = {
+ exceptionAdded: false,
+ securityInfo: secInfo,
+ prefetchCert: true,
+ location: finalChannel.originalURI.displayHostPort,
+ };
+ Services.wm
+ .getMostRecentWindow("")
+ .openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "",
+ "chrome,centerscreen,modal",
+ params
+ );
+
+ if (params.exceptionAdded) {
+ // Try again now that an exception has been added.
+ CardDAVUtils.makeRequest(uri, details).then(resolve, reject);
+ return;
+ }
+ }
+
+ reject(new Components.Exception("Connection failure", status));
+ return;
+ }
+ if (finalChannel.responseStatus == 401) {
+ // We tried to authenticate, but failed.
+ reject(
+ new Components.Exception(
+ "Authorization failure",
+ Cr.NS_ERROR_FAILURE
+ )
+ );
+ return;
+ }
+ resolve({
+ get status() {
+ return finalChannel.responseStatus;
+ },
+ get statusText() {
+ return finalChannel.responseStatusText;
+ },
+ get text() {
+ return new TextDecoder().decode(Uint8Array.from(result));
+ },
+ get dom() {
+ if (this._dom === undefined) {
+ try {
+ this._dom = new DOMParser().parseFromString(
+ this.text,
+ "text/xml"
+ );
+ } catch (ex) {
+ this._dom = null;
+ }
+ }
+ return this._dom;
+ },
+ });
+ },
+ });
+ channel.asyncOpen(listener, channel);
+ });
+ },
+
+ /**
+ * @typedef foundBook
+ * @property {URL} url - The address for this address book.
+ * @param {string} name - The name of this address book on the server.
+ * @param {Function} create - A callback to add this address book locally.
+ */
+
+ /**
+ * Uses DNS look-ups and magic URLs to detects CardDAV address books.
+ *
+ * @param {string} username - Username for the server at `location`.
+ * @param {string} [password] - If not given, the user will be prompted.
+ * @param {string} location - The URL of a server to query.
+ * @param {boolean} [forcePrompt=false] - If true, the user will be shown a
+ * login prompt even if `password` is specified. If false, the user will
+ * be shown a prompt only if `password` is not specified and no saved
+ * password matches `username` and `location`.
+ * @returns {foundBook[]} - An array of found address books.
+ */
+ async detectAddressBooks(username, password, location, forcePrompt = false) {
+ let log = console.createInstance({
+ prefix: "carddav.setup",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "carddav.setup.loglevel",
+ });
+
+ // Use a unique context for each attempt, so a prompt is always shown.
+ let userContextId = Math.floor(Date.now() / 1000);
+
+ let url = new URL(location);
+
+ if (url.hostname in PRESETS) {
+ if (PRESETS[url.hostname] === null) {
+ throw new Components.Exception(
+ `${url} is known to be incompatible`,
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+ log.log(`Using preset URL for ${url}`);
+ url = new URL(PRESETS[url.hostname]);
+ }
+
+ if (url.pathname == "/" && !(url.hostname in PRESETS)) {
+ log.log(`Looking up DNS record for ${url.hostname}`);
+ let domain = `_carddavs._tcp.${url.hostname}`;
+ let srvRecords = await DNS.srv(domain);
+ srvRecords.sort((a, b) => a.prio - b.prio || b.weight - a.weight);
+
+ if (srvRecords[0]) {
+ url = new URL(`https://${srvRecords[0].host}:${srvRecords[0].port}`);
+ log.log(`Found a DNS SRV record pointing to ${url.host}`);
+
+ let txtRecords = await DNS.txt(domain);
+ txtRecords.sort((a, b) => a.prio - b.prio || b.weight - a.weight);
+ txtRecords = txtRecords.filter(result =>
+ result.data.startsWith("path=")
+ );
+
+ if (txtRecords[0]) {
+ url.pathname = txtRecords[0].data.substr(5);
+ log.log(`Found a DNS TXT record pointing to ${url.href}`);
+ }
+ } else {
+ let mxRecords = await DNS.mx(url.hostname);
+ if (mxRecords.some(r => /\bgoogle\.com$/.test(r.host))) {
+ log.log(
+ `Found a DNS MX record for Google, using preset URL for ${url}`
+ );
+ url = new URL(PRESETS["gmail.com"]);
+ }
+ }
+ }
+
+ let oAuth = null;
+ let callbacks = new NotificationCallbacks(username, password, forcePrompt);
+
+ let requestParams = {
+ method: "PROPFIND",
+ callbacks,
+ userContextId,
+ headers: {
+ Depth: 0,
+ },
+ body: `<propfind xmlns="DAV:">
+ <prop>
+ <resourcetype/>
+ <displayname/>
+ <current-user-principal/>
+ <current-user-privilege-set/>
+ </prop>
+ </propfind>`,
+ };
+
+ let details = lazy.OAuth2Providers.getHostnameDetails(url.host);
+ if (details) {
+ let [issuer, scope] = details;
+ let issuerDetails = lazy.OAuth2Providers.getIssuerDetails(issuer);
+
+ oAuth = new lazy.OAuth2(scope, issuerDetails);
+ oAuth._isNew = true;
+ oAuth._loginOrigin = `oauth://${issuer}`;
+ oAuth._scope = scope;
+ for (let login of Services.logins.findLogins(
+ oAuth._loginOrigin,
+ null,
+ ""
+ )) {
+ if (
+ login.username == username &&
+ (login.httpRealm == scope ||
+ login.httpRealm.split(" ").includes(scope))
+ ) {
+ oAuth.refreshToken = login.password;
+ oAuth._isNew = false;
+ break;
+ }
+ }
+
+ if (username) {
+ oAuth.extraAuthParams = [["login_hint", username]];
+ }
+
+ // Implement msgIOAuth2Module.connect, which CardDAVUtils.makeRequest expects.
+ requestParams.oAuth = {
+ QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]),
+ connect(withUI, listener) {
+ oAuth.connect(
+ () =>
+ listener.onSuccess(
+ // String format based on what OAuth2Module has.
+ btoa(`\x01auth=Bearer ${oAuth.accessToken}`)
+ ),
+ () => listener.onFailure(Cr.NS_ERROR_ABORT),
+ withUI,
+ false
+ );
+ },
+ };
+ }
+
+ let response;
+ let triedURLs = new Set();
+ async function tryURL(url) {
+ if (triedURLs.has(url)) {
+ return;
+ }
+ triedURLs.add(url);
+
+ log.log(`Attempting to connect to ${url}`);
+ response = await CardDAVUtils.makeRequest(url, requestParams);
+ if (response.status == 207 && response.dom) {
+ log.log(`${url} ... success`);
+ } else {
+ log.log(
+ `${url} ... response was "${response.status} ${response.statusText}"`
+ );
+ response = null;
+ }
+ }
+
+ if (url.pathname != "/") {
+ // This might be the full URL of an address book.
+ await tryURL(url.href);
+ if (
+ !response?.dom?.querySelector("resourcetype addressbook") &&
+ !response?.dom?.querySelector("current-user-principal href")
+ ) {
+ response = null;
+ }
+ }
+ if (!response || !response.dom) {
+ // Auto-discovery using a magic URL.
+ requestParams.body = `<propfind xmlns="DAV:">
+ <prop>
+ <current-user-principal/>
+ </prop>
+ </propfind>`;
+ await tryURL(`${url.origin}/.well-known/carddav`);
+ }
+ if (!response) {
+ // Auto-discovery at the root of the domain.
+ await tryURL(`${url.origin}/`);
+ }
+ if (!response) {
+ // We've run out of ideas.
+ throw new Components.Exception(
+ "Address book discovery failed",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (!response.dom.querySelector("resourcetype addressbook")) {
+ let userPrincipal = response.dom.querySelector(
+ "current-user-principal href"
+ );
+ if (!userPrincipal) {
+ // We've run out of ideas.
+ throw new Components.Exception(
+ "Address book discovery failed",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ // Steps two and three of auto-discovery. If the entered URL did point
+ // to an address book, we won't get here.
+ url = new URL(userPrincipal.textContent, url);
+ requestParams.body = `<propfind xmlns="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
+ <prop>
+ <card:addressbook-home-set/>
+ </prop>
+ </propfind>`;
+ await tryURL(url.href);
+
+ url = new URL(
+ response.dom.querySelector("addressbook-home-set href").textContent,
+ url
+ );
+ requestParams.headers.Depth = 1;
+ requestParams.body = `<propfind xmlns="DAV:">
+ <prop>
+ <resourcetype/>
+ <displayname/>
+ <current-user-privilege-set/>
+ </prop>
+ </propfind>`;
+ await tryURL(url.href);
+ }
+
+ // Find any directories in the response.
+
+ let foundBooks = [];
+ for (let r of response.dom.querySelectorAll("response")) {
+ if (r.querySelector("status")?.textContent != "HTTP/1.1 200 OK") {
+ continue;
+ }
+ if (!r.querySelector("resourcetype addressbook")) {
+ continue;
+ }
+
+ // If the server provided ACL information, skip address books that we do
+ // not have read privileges to.
+ let privNode = r.querySelector("current-user-privilege-set");
+ let isWritable = false;
+ let isReadable = false;
+ if (privNode) {
+ let privs = Array.from(privNode.querySelectorAll("privilege > *")).map(
+ node => node.localName
+ );
+
+ isWritable = writePrivs.some(priv => privs.includes(priv));
+ isReadable = readPrivs.some(priv => privs.includes(priv));
+
+ if (!isWritable && !isReadable) {
+ continue;
+ }
+ }
+
+ url = new URL(r.querySelector("href").textContent, url);
+ let name = r.querySelector("displayname")?.textContent;
+ if (!name) {
+ // The server didn't give a name, let's make one from the path.
+ name = url.pathname.replace(/\/$/, "").split("/").slice(-1)[0];
+ }
+ if (!name) {
+ // That didn't work either, use the hostname.
+ name = url.hostname;
+ }
+ foundBooks.push({
+ url,
+ name,
+ create() {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ this.name,
+ null,
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE,
+ null
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ book.setStringValue("carddav.url", this.url);
+
+ if (!isWritable && isReadable) {
+ book.setBoolValue("readOnly", true);
+ }
+
+ if (oAuth) {
+ if (oAuth._isNew) {
+ log.log(`Saving refresh token for ${username}`);
+ let newLoginInfo = Cc[
+ "@mozilla.org/login-manager/loginInfo;1"
+ ].createInstance(Ci.nsILoginInfo);
+ newLoginInfo.init(
+ oAuth._loginOrigin,
+ null,
+ oAuth._scope,
+ username,
+ oAuth.refreshToken,
+ "",
+ ""
+ );
+ try {
+ Services.logins.addLogin(newLoginInfo);
+ } catch (ex) {
+ console.error(ex);
+ }
+ oAuth._isNew = false;
+ }
+ book.setStringValue("carddav.username", username);
+ } else if (callbacks.authInfo?.username) {
+ log.log(`Saving login info for ${callbacks.authInfo.username}`);
+ book.setStringValue(
+ "carddav.username",
+ callbacks.authInfo.username
+ );
+ callbacks.saveAuth();
+ }
+
+ let dir = lazy.CardDAVDirectory.forFile(book.fileName);
+ // Pass the context to the created address book. This prevents asking
+ // for a username/password again in the case that we didn't save it.
+ // The user won't be prompted again until Thunderbird is restarted.
+ dir._userContextId = userContextId;
+ dir.fetchAllFromServer();
+
+ return dir;
+ },
+ });
+ }
+ return foundBooks;
+ },
+};
+
+/**
+ * Passed to nsIChannel.notificationCallbacks in CardDAVDirectory.makeRequest.
+ * This handles HTTP authentication, prompting the user if necessary. It also
+ * ensures important headers are copied from one channel to another if a
+ * redirection occurs.
+ *
+ * @implements {nsIInterfaceRequestor}
+ * @implements {nsIAuthPrompt2}
+ * @implements {nsIChannelEventSink}
+ */
+class NotificationCallbacks {
+ /**
+ * @param {string} [username] - Used to pre-fill any auth dialogs.
+ * @param {string} [password] - Used to pre-fill any auth dialogs.
+ * @param {boolean} [forcePrompt] - Skips checking the password manager for
+ * a password, even if username is given. The user will be prompted.
+ */
+ constructor(username, password, forcePrompt) {
+ this.username = username;
+ this.password = password;
+ this.forcePrompt = forcePrompt;
+ }
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPrompt2",
+ "nsIChannelEventSink",
+ ]);
+ getInterface = ChromeUtils.generateQI([
+ "nsIAuthPrompt2",
+ "nsIChannelEventSink",
+ ]);
+ promptAuth(channel, level, authInfo) {
+ if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) {
+ return false;
+ }
+
+ this.origin = channel.URI.prePath;
+ this.authInfo = authInfo;
+
+ if (!this.forcePrompt) {
+ if (this.username && this.password) {
+ authInfo.username = this.username;
+ authInfo.password = this.password;
+ this.shouldSaveAuth = true;
+ return true;
+ }
+
+ let logins = Services.logins.findLogins(channel.URI.prePath, null, "");
+ for (let l of logins) {
+ if (l.username == this.username) {
+ authInfo.username = l.username;
+ authInfo.password = l.password;
+ return true;
+ }
+ }
+ }
+
+ authInfo.username = this.username;
+ authInfo.password = this.password;
+
+ let savePasswordLabel = null;
+ let savePassword = {};
+ if (Services.prefs.getBoolPref("signon.rememberSignons", true)) {
+ savePasswordLabel = Services.strings
+ .createBundle("chrome://passwordmgr/locale/passwordmgr.properties")
+ .GetStringFromName("rememberPassword");
+ savePassword.value = true;
+ }
+
+ let returnValue = new lazy.MsgAuthPrompt().promptAuth(
+ channel,
+ level,
+ authInfo,
+ savePasswordLabel,
+ savePassword
+ );
+ if (returnValue) {
+ this.shouldSaveAuth = savePassword.value;
+ }
+ return returnValue;
+ }
+ saveAuth() {
+ if (this.shouldSaveAuth) {
+ let newLoginInfo = Cc[
+ "@mozilla.org/login-manager/loginInfo;1"
+ ].createInstance(Ci.nsILoginInfo);
+ newLoginInfo.init(
+ this.origin,
+ null,
+ this.authInfo.realm,
+ this.authInfo.username,
+ this.authInfo.password,
+ "",
+ ""
+ );
+ try {
+ Services.logins.addLogin(newLoginInfo);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ }
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ /**
+ * Copy the given header from the old channel to the new one, ignoring missing headers
+ *
+ * @param {string} header - The header to copy
+ */
+ function copyHeader(header) {
+ try {
+ let headerValue = oldChannel.getRequestHeader(header);
+ if (headerValue) {
+ newChannel.setRequestHeader(header, headerValue, false);
+ }
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ // The header could possibly not be available, ignore that
+ // case but throw otherwise
+ throw e;
+ }
+ }
+ }
+
+ // Make sure we can get/set headers on both channels.
+ newChannel.QueryInterface(Ci.nsIHttpChannel);
+ oldChannel.QueryInterface(Ci.nsIHttpChannel);
+
+ // If any other header is used, it should be added here. We might want
+ // to just copy all headers over to the new channel.
+ copyHeader("Authorization");
+ copyHeader("Depth");
+ copyHeader("Originator");
+ copyHeader("Recipient");
+ copyHeader("If-None-Match");
+ copyHeader("If-Match");
+
+ newChannel.requestMethod = oldChannel.requestMethod;
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/LDAPClient.jsm b/comm/mailnews/addrbook/modules/LDAPClient.jsm
new file mode 100644
index 0000000000..e26b7b5fce
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPClient.jsm
@@ -0,0 +1,285 @@
+/* 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 = ["LDAPClient"];
+
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+var {
+ AbandonRequest,
+ BindRequest,
+ UnbindRequest,
+ SearchRequest,
+ LDAPResponse,
+} = ChromeUtils.import("resource:///modules/LDAPMessage.jsm");
+
+class LDAPClient {
+ /**
+ * @param {string} host - The LDAP server host.
+ * @param {number} port - The LDAP server port.
+ * @param {boolean} useSecureTransport - Whether to use TLS connection.
+ */
+ constructor(host, port, useSecureTransport) {
+ this.onOpen = () => {};
+ this.onError = () => {};
+
+ this._host = host;
+ this._port = port;
+ this._useSecureTransport = useSecureTransport;
+
+ this._messageId = 1;
+ this._callbackMap = new Map();
+
+ this._logger = console.createInstance({
+ prefix: "mailnews.ldap",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.ldap.loglevel",
+ });
+
+ this._dataEventsQueue = [];
+ }
+
+ connect() {
+ let hostname = this._host.toLowerCase();
+ this._logger.debug(
+ `Connecting to ${
+ this._useSecureTransport ? "ldaps" : "ldap"
+ }://${hostname}:${this._port}`
+ );
+ this._socket = new TCPSocket(hostname, this._port, {
+ binaryType: "arraybuffer",
+ useSecureTransport: this._useSecureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+ }
+
+ /**
+ * Send a simple bind request to the server.
+ *
+ * @param {string} dn - The name to bind.
+ * @param {string} password - The password.
+ * @param {Function} callback - Callback function when receiving BindResponse.
+ * @returns {number} The id of the sent request.
+ */
+ bind(dn, password, callback) {
+ this._logger.debug(`Binding ${dn}`);
+ let req = new BindRequest(dn || "", password || "");
+ return this._send(req, callback);
+ }
+
+ /**
+ * Send a SASL bind request to the server.
+ *
+ * @param {string} service - The service host name to bind.
+ * @param {string} mechanism - The SASL mechanism to use, e.g. GSSAPI.
+ * @param {string} authModuleType - The auth module type, @see nsIMailAuthModule.
+ * @param {ArrayBuffer} serverCredentials - The challenge token returned from
+ * the server, which must be used to generate a new request token. Or
+ * undefined for the first request.
+ * @param {Function} callback - Callback function when receiving BindResponse.
+ * @returns {number} The id of the sent request.
+ */
+ saslBind(service, mechanism, authModuleType, serverCredentials, callback) {
+ this._logger.debug(`Binding ${service} using ${mechanism}`);
+ if (!this._authModule || this._authModuleType != authModuleType) {
+ this._authModuleType = authModuleType;
+ this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance(
+ Ci.nsIMailAuthModule
+ );
+ this._authModule.init(
+ authModuleType,
+ service,
+ 0, // nsIAuthModule::REQ_DEFAULT
+ null, // domain
+ null, // username
+ null // password
+ );
+ }
+ // getNextToken expects a base64 string.
+ let token = this._authModule.getNextToken(
+ serverCredentials
+ ? btoa(
+ CommonUtils.arrayBufferToByteString(
+ new Uint8Array(serverCredentials)
+ )
+ )
+ : ""
+ );
+ // token is a base64 string, convert it to Uint8Array.
+ let credentials = CommonUtils.byteStringToArrayBuffer(atob(token));
+ let req = new BindRequest("", "", { mechanism, credentials });
+ return this._send(req, callback);
+ }
+
+ /**
+ * Send an unbind request to the server.
+ */
+ unbind() {
+ return this._send(new UnbindRequest(), () => this._socket.close());
+ }
+
+ /**
+ * Send a search request to the server.
+ *
+ * @param {string} dn - The name to search.
+ * @param {number} scope - The scope to search.
+ * @param {string} filter - The filter string.
+ * @param {string} attributes - Attributes to include in the search result.
+ * @param {number} timeout - The seconds to wait.
+ * @param {number} limit - Maximum number of entries to return.
+ * @param {Function} callback - Callback function when receiving search responses.
+ * @returns {number} The id of the sent request.
+ */
+ search(dn, scope, filter, attributes, timeout, limit, callback) {
+ this._logger.debug(`Searching dn="${dn}" filter="${filter}"`);
+ let req = new SearchRequest(dn, scope, filter, attributes, timeout, limit);
+ return this._send(req, callback);
+ }
+
+ /**
+ * Send an abandon request to the server.
+ *
+ * @param {number} messageId - The id of the message to abandon.
+ */
+ abandon(messageId) {
+ this._logger.debug(`Abandoning ${messageId}`);
+ this._callbackMap.delete(messageId);
+ let req = new AbandonRequest(messageId);
+ this._send(req);
+ }
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this.onOpen();
+ };
+
+ /**
+ * The data event handler. Server may send multiple data events after a
+ * search, we want to handle them asynchonosly and in sequence.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = async event => {
+ if (this._processingData) {
+ this._dataEventsQueue.push(event);
+ return;
+ }
+ this._processingData = true;
+ let data = event.data;
+ if (this._buffer) {
+ // Concatenate left over data from the last event with the new data.
+ let arr = new Uint8Array(this._buffer.byteLength + data.byteLength);
+ arr.set(new Uint8Array(this._buffer));
+ arr.set(new Uint8Array(data), this._buffer.byteLength);
+ data = arr.buffer;
+ this._buffer = null;
+ }
+ let i = 0;
+ // The payload can contain multiple messages, parse it to the end.
+ while (data.byteLength) {
+ i++;
+ let res;
+ try {
+ res = LDAPResponse.fromBER(data);
+ if (typeof res == "number") {
+ data = data.slice(res);
+ continue;
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_CANNOT_CONVERT_DATA) {
+ // The remaining data doesn't form a valid LDAP message, save it for
+ // the next round.
+ this._buffer = data;
+ this._handleNextDataEvent();
+ return;
+ }
+ throw e;
+ }
+ this._logger.debug(
+ `S: [${res.messageId}] ${res.constructor.name}`,
+ res.result.resultCode >= 0
+ ? `resultCode=${res.result.resultCode} message="${res.result.diagnosticMessage}"`
+ : ""
+ );
+ if (res.constructor.name == "SearchResultReference") {
+ this._logger.debug("References=", res.result);
+ }
+ let callback = this._callbackMap.get(res.messageId);
+ if (callback) {
+ callback(res);
+ if (
+ !["SearchResultEntry", "SearchResultReference"].includes(
+ res.constructor.name
+ )
+ ) {
+ this._callbackMap.delete(res.messageId);
+ }
+ }
+ data = data.slice(res.byteLength);
+ if (i % 10 == 0) {
+ // Prevent blocking the main thread for too long.
+ await new Promise(resolve => setTimeout(resolve));
+ }
+ }
+ this._handleNextDataEvent();
+ };
+
+ /**
+ * Process a queued data event, if there is any.
+ */
+ _handleNextDataEvent() {
+ this._processingData = false;
+ let next = this._dataEventsQueue.shift();
+ if (next) {
+ this._onData(next);
+ }
+ }
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed");
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = async event => {
+ this._logger.error(event);
+ this._socket.close();
+ this.onError(
+ event.errorCode,
+ await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo()
+ );
+ };
+
+ /**
+ * Send a message to the server.
+ *
+ * @param {LDAPMessage} msg - The message to send.
+ * @param {Function} callback - Callback function when receiving server responses.
+ * @returns {number} The id of the sent message.
+ */
+ _send(msg, callback) {
+ if (callback) {
+ this._callbackMap.set(this._messageId, callback);
+ }
+ this._logger.debug(`C: [${this._messageId}] ${msg.constructor.name}`);
+ this._socket.send(msg.toBER(this._messageId));
+ return this._messageId++;
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/LDAPConnection.jsm b/comm/mailnews/addrbook/modules/LDAPConnection.jsm
new file mode 100644
index 0000000000..f9a66f47a7
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPConnection.jsm
@@ -0,0 +1,53 @@
+/* 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 = ["LDAPConnection"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "LDAPClient",
+ "resource:///modules/LDAPClient.jsm"
+);
+
+/**
+ * A module to manage LDAP connection.
+ *
+ * @implements {nsILDAPConnection}
+ */
+class LDAPConnection {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPConnection"]);
+
+ get bindName() {
+ return this._bindName;
+ }
+
+ init(url, bindName, listener, closure, version) {
+ let useSecureTransport = url.scheme == "ldaps";
+ let port = url.port;
+ if (port == -1) {
+ // -1 corresponds to the protocol's default port.
+ port = useSecureTransport ? 636 : 389;
+ }
+ this.client = new lazy.LDAPClient(url.host, port, useSecureTransport);
+ this._url = url;
+ this._bindName = bindName;
+ this.client.onOpen = () => {
+ listener.onLDAPInit();
+ };
+ this.client.onError = (status, secInfo) => {
+ listener.onLDAPError(status, secInfo, `${url.host}:${port}`);
+ };
+ this.client.connect();
+ }
+
+ get wrappedJSObject() {
+ return this;
+ }
+}
+
+LDAPConnection.prototype.classID = Components.ID(
+ "{f87b71b5-2a0f-4b37-8e4f-3c899f6b8432}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPDirectory.jsm b/comm/mailnews/addrbook/modules/LDAPDirectory.jsm
new file mode 100644
index 0000000000..758cde2ed9
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPDirectory.jsm
@@ -0,0 +1,230 @@
+/* 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 = ["LDAPDirectory"];
+
+const { AddrBookDirectory } = ChromeUtils.import(
+ "resource:///modules/AddrBookDirectory.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ QueryStringToExpression: "resource:///modules/QueryStringToExpression.jsm",
+});
+
+/**
+ * @implements {nsIAbLDAPDirectory}
+ * @implements {nsIAbDirectory}
+ */
+
+class LDAPDirectory extends AddrBookDirectory {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIAbLDAPDirectory",
+ "nsIAbDirectory",
+ ]);
+
+ init(uri) {
+ this._uri = uri;
+
+ let searchIndex = uri.indexOf("?");
+ this._dirPrefId = uri.substr(
+ "moz-abldapdirectory://".length,
+ searchIndex == -1 ? undefined : searchIndex
+ );
+
+ super.init(uri);
+ }
+
+ get readOnly() {
+ return true;
+ }
+
+ get isRemote() {
+ return true;
+ }
+
+ get isSecure() {
+ return this.lDAPURL.scheme == "ldaps";
+ }
+
+ get propertiesChromeURI() {
+ return "chrome://messenger/content/addressbook/pref-directory-add.xhtml";
+ }
+
+ get dirType() {
+ return Ci.nsIAbManager.LDAP_DIRECTORY_TYPE;
+ }
+
+ get replicationFileName() {
+ return this.getStringValue("filename");
+ }
+
+ set replicationFileName(value) {
+ this.setStringValue("filename", value);
+ }
+
+ get replicationFile() {
+ return lazy.FileUtils.getFile("ProfD", [this.replicationFileName]);
+ }
+
+ get protocolVersion() {
+ return this.getStringValue("protocolVersion", "3") == "3"
+ ? Ci.nsILDAPConnection.VERSION3
+ : Ci.nsILDAPConnection.VERSION2;
+ }
+
+ set protocolVersion(value) {
+ this.setStringValue(
+ "protocolVersion",
+ value == Ci.nsILDAPConnection.VERSION3 ? "3" : "2"
+ );
+ }
+
+ get saslMechanism() {
+ return this.getStringValue("auth.saslmech");
+ }
+
+ set saslMechanism(value) {
+ this.setStringValue("auth.saslmech", value);
+ }
+
+ get authDn() {
+ return this.getStringValue("auth.dn");
+ }
+
+ set authDn(value) {
+ this.setStringValue("auth.dn", value);
+ }
+
+ get maxHits() {
+ return this.getIntValue("maxHits", 100);
+ }
+
+ set maxHits(value) {
+ this.setIntValue("maxHits", value);
+ }
+
+ get attributeMap() {
+ let mapSvc = Cc[
+ "@mozilla.org/addressbook/ldap-attribute-map-service;1"
+ ].createInstance(Ci.nsIAbLDAPAttributeMapService);
+ return mapSvc.getMapForPrefBranch(this._dirPrefId);
+ }
+
+ get lDAPURL() {
+ let uri = this.getStringValue("uri") || `ldap://${this._uri.slice(22)}`;
+ return Services.io.newURI(uri).QueryInterface(Ci.nsILDAPURL);
+ }
+
+ set lDAPURL(uri) {
+ this.setStringValue("uri", uri.spec);
+ }
+
+ get childCardCount() {
+ return 0;
+ }
+
+ get childCards() {
+ if (Services.io.offline) {
+ return this.replicationDB.childCards;
+ }
+ return super.childCards;
+ }
+
+ /**
+ * @see {AddrBookDirectory}
+ */
+ get cards() {
+ return new Map();
+ }
+
+ /**
+ * @see {AddrBookDirectory}
+ */
+ get lists() {
+ return new Map();
+ }
+
+ get replicationDB() {
+ this._replicationDB?.cleanUp();
+ this._replicationDB = Cc[
+ "@mozilla.org/addressbook/directory;1?type=jsaddrbook"
+ ].createInstance(Ci.nsIAbDirectory);
+ this._replicationDB.init(`jsaddrbook://${this.replicationFileName}`);
+ return this._replicationDB;
+ }
+
+ getCardFromProperty(property, value, caseSensitive) {
+ return null;
+ }
+
+ search(queryString, searchString, listener) {
+ if (Services.io.offline) {
+ this.replicationDB.search(queryString, searchString, listener);
+ return;
+ }
+ this._query = Cc[
+ "@mozilla.org/addressbook/ldap-directory-query;1"
+ ].createInstance(Ci.nsIAbDirectoryQuery);
+
+ let args = Cc[
+ "@mozilla.org/addressbook/directory/query-arguments;1"
+ ].createInstance(Ci.nsIAbDirectoryQueryArguments);
+ args.expression = lazy.QueryStringToExpression.convert(queryString);
+ args.querySubDirectories = true;
+ args.typeSpecificArg = this.attributeMap;
+
+ this._query.doQuery(this, args, listener, this.maxHits, 0);
+ }
+
+ useForAutocomplete(identityKey) {
+ // If we're online, then don't allow search during local autocomplete - must
+ // use the separate LDAP autocomplete session due to the current interfaces
+ let useDirectory = Services.prefs.getBoolPref(
+ "ldap_2.autoComplete.useDirectory",
+ false
+ );
+ if (!Services.io.offline || (!useDirectory && !identityKey)) {
+ return false;
+ }
+
+ let prefName = "";
+ if (identityKey) {
+ // If we have an identity string, try and find out the required directory
+ // server.
+ let identity = MailServices.accounts.getIdentity(identityKey);
+ if (identity.overrideGlobalPref) {
+ prefName = identity.directoryServer;
+ }
+ if (!prefName && !useDirectory) {
+ return false;
+ }
+ }
+ if (!prefName) {
+ prefName = Services.prefs.getCharPref(
+ "ldap_2.autoComplete.directoryServer"
+ );
+ }
+ if (prefName == this.dirPrefId) {
+ return this.replicationFile.exists();
+ }
+
+ return false;
+ }
+}
+
+LDAPDirectory.prototype.classID = Components.ID(
+ "{8683e821-f1b0-476d-ac15-07771c79bb11}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm b/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm
new file mode 100644
index 0000000000..88291cbaed
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm
@@ -0,0 +1,218 @@
+/* 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 = ["LDAPDirectoryQuery"];
+
+const { LDAPListenerBase } = ChromeUtils.import(
+ "resource:///modules/LDAPListenerBase.jsm"
+);
+
+/**
+ * Convert a nsIAbBooleanExpression to a filter string.
+ *
+ * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between address book
+ * properties and ldap attributes.
+ * @param {nsIAbBooleanExpression} exp - The expression to convert.
+ * @returns {string}
+ */
+function boolExpressionToFilter(attrMap, exp) {
+ let filter = "(";
+ filter +=
+ {
+ [Ci.nsIAbBooleanOperationTypes.AND]: "&",
+ [Ci.nsIAbBooleanOperationTypes.OR]: "|",
+ [Ci.nsIAbBooleanOperationTypes.NOT]: "!",
+ }[exp.operation] || "";
+
+ if (exp.expressions) {
+ for (let childExp of exp.expressions) {
+ if (childExp instanceof Ci.nsIAbBooleanExpression) {
+ filter += boolExpressionToFilter(attrMap, childExp);
+ } else if (childExp instanceof Ci.nsIAbBooleanConditionString) {
+ filter += boolConditionToFilter(attrMap, childExp);
+ }
+ }
+ }
+
+ filter += ")";
+ return filter;
+}
+
+/**
+ * Convert a nsIAbBooleanConditionString to a filter string.
+ *
+ * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between addressbook
+ * properties and ldap attributes.
+ * @param {nsIAbBooleanConditionString} exp - The expression to convert.
+ * @returns {string}
+ */
+function boolConditionToFilter(attrMap, exp) {
+ let attr = attrMap.getFirstAttribute(exp.name);
+ if (!attr) {
+ return "";
+ }
+ switch (exp.condition) {
+ case Ci.nsIAbBooleanConditionTypes.DoesNotExist:
+ return `(!(${attr}=*))`;
+ case Ci.nsIAbBooleanConditionTypes.Exists:
+ return `(${attr}=*)`;
+ case Ci.nsIAbBooleanConditionTypes.Contains:
+ return `(${attr}=*${exp.value}*)`;
+ case Ci.nsIAbBooleanConditionTypes.DoesNotContain:
+ return `(!(${attr}=*${exp.value}*))`;
+ case Ci.nsIAbBooleanConditionTypes.Is:
+ return `(${attr}=${exp.value})`;
+ case Ci.nsIAbBooleanConditionTypes.IsNot:
+ return `(!(${attr}=${exp.value}))`;
+ case Ci.nsIAbBooleanConditionTypes.BeginsWith:
+ return `(${attr}=${exp.value}*)`;
+ case Ci.nsIAbBooleanConditionTypes.EndsWith:
+ return `(${attr}=*${exp.value})`;
+ case Ci.nsIAbBooleanConditionTypes.LessThan:
+ return `(${attr}<=${exp.value})`;
+ case Ci.nsIAbBooleanConditionTypes.GreaterThan:
+ return `(${attr}>=${exp.value})`;
+ case Ci.nsIAbBooleanConditionTypes.SoundsLike:
+ return `(${attr}~=${exp.value})`;
+ default:
+ return "";
+ }
+}
+
+/**
+ * @implements {nsIAbDirectoryQuery}
+ * @implements {nsILDAPMessageListener}
+ */
+class LDAPDirectoryQuery extends LDAPListenerBase {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIAbDirectoryQuery",
+ "nsILDAPMessageListener",
+ ]);
+
+ i = 0;
+
+ doQuery(directory, args, listener, limit, timeout) {
+ this._directory = directory.QueryInterface(Ci.nsIAbLDAPDirectory);
+ this._listener = listener;
+ this._attrMap = args.typeSpecificArg;
+ this._filter =
+ args.filter || boolExpressionToFilter(this._attrMap, args.expression);
+ this._limit = limit;
+ this._timeout = timeout;
+
+ let urlFilter = this._directory.lDAPURL.filter;
+ // If urlFilter is empty or the default "(objectclass=*)", do nothing.
+ if (urlFilter && urlFilter != "(objectclass=*)") {
+ if (!urlFilter.startsWith("(")) {
+ urlFilter = `(${urlFilter})`;
+ }
+ this._filter = `(&${urlFilter}${this._filter})`;
+ }
+
+ this._connection = Cc[
+ "@mozilla.org/network/ldap-connection;1"
+ ].createInstance(Ci.nsILDAPConnection);
+ this._operation = Cc[
+ "@mozilla.org/network/ldap-operation;1"
+ ].createInstance(Ci.nsILDAPOperation);
+
+ this._connection.init(
+ directory.lDAPURL,
+ directory.authDn,
+ this,
+ null,
+ directory.protocolVersion
+ );
+ return this.i++;
+ }
+
+ stopQuery(contextId) {
+ this._operation?.abandonExt();
+ }
+
+ /**
+ * @see nsILDAPMessageListener
+ */
+ onLDAPMessage(msg) {
+ switch (msg.type) {
+ case Ci.nsILDAPMessage.RES_BIND:
+ this._onLDAPBind(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_ENTRY:
+ this._onLDAPSearchEntry(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_RESULT:
+ this._onLDAPSearchResult(msg);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * @see nsILDAPMessageListener
+ */
+ onLDAPError(status, secInfo, location) {
+ this._onSearchFinished(status, secInfo, location);
+ }
+
+ /**
+ * @see LDAPListenerBase
+ */
+ _actionOnBindSuccess() {
+ let ldapUrl = this._directory.lDAPURL;
+ this._operation.searchExt(
+ ldapUrl.dn,
+ ldapUrl.scope,
+ this._filter,
+ ldapUrl.attributes,
+ this._timeout,
+ this._limit
+ );
+ }
+
+ /**
+ * @see LDAPListenerBase
+ */
+ _actionOnBindFailure() {
+ this._onSearchFinished(Cr.NS_ERROR_FAILURE);
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPSearchEntry(msg) {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ this._attrMap.setCardPropertiesFromLDAPMessage(msg, newCard);
+ newCard.directoryUID = this._directory.UID;
+ this._listener.onSearchFoundCard(newCard);
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_RESULT message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPSearchResult(msg) {
+ this._onSearchFinished(
+ [Ci.nsILDAPErrors.SUCCESS, Ci.nsILDAPErrors.SIZELIMIT_EXCEEDED].includes(
+ msg.errorCode
+ )
+ ? Cr.NS_OK
+ : Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ _onSearchFinished(status, secInfo, location) {
+ this._listener.onSearchFinished(status, false, secInfo, location);
+ }
+}
+
+LDAPDirectoryQuery.prototype.classID = Components.ID(
+ "{5ad5d311-1a50-43db-a03c-63d45f443903}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm b/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm
new file mode 100644
index 0000000000..486dcaffbe
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm
@@ -0,0 +1,117 @@
+/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 = ["LDAPListenerBase"];
+
+/**
+ * @implements {nsILDAPMessageListener}
+ */
+class LDAPListenerBase {
+ /**
+ * @see nsILDAPMessageListener
+ */
+ async onLDAPInit() {
+ let outPassword = {};
+ if (this._directory.authDn && this._directory.saslMechanism != "GSSAPI") {
+ // If authDn is set, we're expected to use it to get a password.
+ let bundle = Services.strings.createBundle(
+ "chrome://mozldap/locale/ldap.properties"
+ );
+
+ let authPrompt = Services.ww.getNewAuthPrompter(
+ Services.wm.getMostRecentWindow(null)
+ );
+ await authPrompt.asyncPromptPassword(
+ bundle.GetStringFromName("authPromptTitle"),
+ bundle.formatStringFromName("authPromptText", [
+ this._directory.lDAPURL.host,
+ ]),
+ this._directory.lDAPURL.spec,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outPassword
+ );
+ }
+ this._operation.init(this._connection, this, null);
+
+ if (this._directory.saslMechanism != "GSSAPI") {
+ this._operation.simpleBind(outPassword.value);
+ return;
+ }
+
+ // Handle GSSAPI now.
+ this._operation.saslBind(
+ `ldap@${this._directory.lDAPURL.host}`,
+ "GSSAPI",
+ "sasl-gssapi"
+ );
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_BIND message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPBind(msg) {
+ let errCode = msg.errorCode;
+ if (
+ errCode == Ci.nsILDAPErrors.INAPPROPRIATE_AUTH ||
+ errCode == Ci.nsILDAPErrors.INVALID_CREDENTIALS
+ ) {
+ // Login failed, remove any existing login(s).
+ let ldapUrl = this._directory.lDAPURL;
+ let logins = Services.logins.findLogins(
+ ldapUrl.prePath,
+ "",
+ ldapUrl.spec
+ );
+ for (let login of logins) {
+ Services.logins.removeLogin(login);
+ }
+ // Trigger the auth prompt.
+ this.onLDAPInit();
+ return;
+ }
+ if (errCode != Ci.nsILDAPErrors.SUCCESS) {
+ this._actionOnBindFailure();
+ return;
+ }
+ this._actionOnBindSuccess();
+ }
+
+ /**
+ * @see nsILDAPMessageListener
+ * @abstract
+ */
+ onLDAPMessage() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement onLDAPMessage.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Callback when BindResponse succeeded.
+ *
+ * @abstract
+ */
+ _actionOnBindSuccess() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement _actionOnBindSuccess.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Callback when BindResponse failed.
+ *
+ * @abstract
+ */
+ _actionOnBindFailure() {
+ throw new Components.Exception(
+ `${this.constructor.name} does not implement _actionOnBindFailure.`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/LDAPMessage.jsm b/comm/mailnews/addrbook/modules/LDAPMessage.jsm
new file mode 100644
index 0000000000..6ee7574605
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPMessage.jsm
@@ -0,0 +1,632 @@
+/* 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 = [
+ "AbandonRequest",
+ "BindRequest",
+ "UnbindRequest",
+ "SearchRequest",
+ "LDAPResponse",
+];
+
+var { asn1js } = ChromeUtils.importESModule("resource:///modules/asn1js.mjs");
+
+/**
+ * A base class for all LDAP request and response messages, see
+ * rfc4511#section-4.1.1.
+ *
+ * @property {number} messageId - The message id.
+ * @property {LocalBaseBlock} protocolOp - The message content, in a data
+ * structure provided by asn1js.
+ */
+class LDAPMessage {
+ /**
+ * Encode the current message by Basic Encoding Rules (BER).
+ *
+ * @param {number} messageId - The id of the current message.
+ * @returns {ArrayBuffer} BER encoded message.
+ */
+ toBER(messageId = this.messageId) {
+ let msg = new asn1js.Sequence({
+ value: [new asn1js.Integer({ value: messageId }), this.protocolOp],
+ });
+ return msg.toBER();
+ }
+
+ static TAG_CLASS_APPLICATION = 2;
+ static TAG_CLASS_CONTEXT = 3;
+
+ /**
+ * Get the idBlock of [APPLICATION n].
+ *
+ * @param {number} tagNumber - The tag number of this block.
+ */
+ _getApplicationId(tagNumber) {
+ return {
+ tagClass: LDAPMessage.TAG_CLASS_APPLICATION,
+ tagNumber,
+ };
+ }
+
+ /**
+ * Get the idBlock of context-specific [n].
+ *
+ * @param {number} tagNumber - The tag number of this block.
+ */
+ _getContextId(tagNumber) {
+ return {
+ tagClass: LDAPMessage.TAG_CLASS_CONTEXT,
+ tagNumber,
+ };
+ }
+
+ /**
+ * Create a string block with context-specific [n].
+ *
+ * @param {number} tagNumber - The tag number of this block.
+ * @param {string} value - The string value of this block.
+ * @returns {LocalBaseBlock}
+ */
+ _contextStringBlock(tagNumber, value) {
+ return new asn1js.Primitive({
+ idBlock: this._getContextId(tagNumber),
+ valueHex: new TextEncoder().encode(value),
+ });
+ }
+}
+
+class BindRequest extends LDAPMessage {
+ static APPLICATION = 0;
+
+ AUTH_SIMPLE = 0;
+ AUTH_SASL = 3;
+
+ /**
+ * @param {string} dn - The name to bind.
+ * @param {string} password - The password.
+ * @param {object} sasl - The SASL configs.
+ * @param {string} sasl.mechanism - The SASL mechanism e.g. sasl-gssapi.
+ * @param {Uint8Array} sasl.credentials - The credential token for the request.
+ */
+ constructor(dn, password, sasl) {
+ super();
+ let authBlock;
+ if (sasl) {
+ authBlock = new asn1js.Constructed({
+ idBlock: this._getContextId(this.AUTH_SASL),
+ value: [
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(sasl.mechanism),
+ }),
+ new asn1js.OctetString({
+ valueHex: sasl.credentials,
+ }),
+ ],
+ });
+ } else {
+ authBlock = new asn1js.Primitive({
+ idBlock: this._getContextId(this.AUTH_SIMPLE),
+ valueHex: new TextEncoder().encode(password),
+ });
+ }
+ this.protocolOp = new asn1js.Constructed({
+ // [APPLICATION 0]
+ idBlock: this._getApplicationId(BindRequest.APPLICATION),
+ value: [
+ // version
+ new asn1js.Integer({ value: 3 }),
+ // name
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(dn),
+ }),
+ // authentication
+ authBlock,
+ ],
+ });
+ }
+}
+
+class UnbindRequest extends LDAPMessage {
+ static APPLICATION = 2;
+
+ protocolOp = new asn1js.Primitive({
+ // [APPLICATION 2]
+ idBlock: this._getApplicationId(UnbindRequest.APPLICATION),
+ });
+}
+
+class SearchRequest extends LDAPMessage {
+ static APPLICATION = 3;
+
+ // Filter CHOICE.
+ FILTER_AND = 0;
+ FILTER_OR = 1;
+ FILTER_NOT = 2;
+ FILTER_EQUALITY_MATCH = 3;
+ FILTER_SUBSTRINGS = 4;
+ FILTER_GREATER_OR_EQUAL = 5;
+ FILTER_LESS_OR_EQUAL = 6;
+ FILTER_PRESENT = 7;
+ FILTER_APPROX_MATCH = 8;
+ FILTER_EXTENSIBLE_MATCH = 9;
+
+ // SubstringFilter SEQUENCE.
+ SUBSTRINGS_INITIAL = 0;
+ SUBSTRINGS_ANY = 1;
+ SUBSTRINGS_FINAL = 2;
+
+ // MatchingRuleAssertion SEQUENCE.
+ MATCHING_RULE = 1; // optional
+ MATCHING_TYPE = 2; // optional
+ MATCHING_VALUE = 3;
+ MATCHING_DN = 4; // default to FALSE
+
+ /**
+ * @param {string} dn - The name to search.
+ * @param {number} scope - The scope to search.
+ * @param {string} filter - The filter string, e.g. "(&(|(k1=v1)(k2=v2)))".
+ * @param {string} attributes - Attributes to include in the search result.
+ * @param {number} timeout - The seconds to wait.
+ * @param {number} limit - Maximum number of entries to return.
+ */
+ constructor(dn, scope, filter, attributes, timeout, limit) {
+ super();
+ this.protocolOp = new asn1js.Constructed({
+ // [APPLICATION 3]
+ idBlock: this._getApplicationId(SearchRequest.APPLICATION),
+ value: [
+ // base DN
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(dn),
+ }),
+ // scope
+ new asn1js.Enumerated({
+ value: scope,
+ }),
+ // derefAliases
+ new asn1js.Enumerated({
+ value: 0,
+ }),
+ // sizeLimit
+ new asn1js.Integer({ value: limit }),
+ // timeLimit
+ new asn1js.Integer({ value: timeout }),
+ // typesOnly
+ new asn1js.Boolean({ value: false }),
+ // filter
+ this._convertFilterToBlock(filter),
+ // attributes
+ new asn1js.Sequence({
+ value: attributes
+ .split(",")
+ .filter(Boolean)
+ .map(
+ attr =>
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(attr),
+ })
+ ),
+ }),
+ ],
+ });
+ }
+
+ /**
+ * Parse a single filter value "key=value" to [filterId, key, value].
+ *
+ * @param {string} filter - A single filter value without parentheses.
+ * @returns {(number|string)[]} An array [filterId, key, value] as
+ * [number, string, string]
+ */
+ _parseFilterValue(filter) {
+ for (let cond of [">=", "<=", "~=", ":=", "="]) {
+ let index = filter.indexOf(cond);
+ if (index > 0) {
+ let k = filter.slice(0, index);
+ let v = filter.slice(index + cond.length);
+ let filterId = {
+ ">=": this.FILTER_GREATER_OR_EQUAL,
+ "<=": this.FILTER_LESS_OR_EQUAL,
+ "~=": this.FILTER_APPROX_MATCH,
+ ":=": this.FILTER_EXTENSIBLE_MATCH,
+ }[cond];
+ if (!filterId) {
+ if (v == "*") {
+ filterId = this.FILTER_PRESENT;
+ } else if (!v.includes("*")) {
+ filterId = this.FILTER_EQUALITY_MATCH;
+ } else {
+ filterId = this.FILTER_SUBSTRINGS;
+ v = v.split("*");
+ }
+ }
+ return [filterId, k, v];
+ }
+ }
+ throw Components.Exception(
+ `Invalid filter: ${filter}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ /**
+ * Parse a full filter string to an array of tokens.
+ *
+ * @param {string} filter - The full filter string to parse.
+ * @param {number} depth - The depth of a token.
+ * @param {object[]} tokens - The tokens to return.
+ * @param {"op"|"field"} tokens[].type - The token type.
+ * @param {number} tokens[].depth - The token depth.
+ * @param {string|string[]} tokens[].value - The token value.
+ */
+ _parseFilter(filter, depth = 0, tokens = []) {
+ while (filter[0] == ")" && depth > 0) {
+ depth--;
+ filter = filter.slice(1);
+ }
+ if (filter.length == 0) {
+ // End of input.
+ return tokens;
+ }
+ if (filter[0] != "(") {
+ throw Components.Exception(
+ `Invalid filter: ${filter}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ filter = filter.slice(1);
+ let nextOpen = filter.indexOf("(");
+ let nextClose = filter.indexOf(")");
+
+ if (nextOpen != -1 && nextOpen < nextClose) {
+ // Case: "OP("
+ depth++;
+ tokens.push({
+ type: "op",
+ depth,
+ value: {
+ "&": this.FILTER_AND,
+ "|": this.FILTER_OR,
+ "!": this.FILTER_NOT,
+ }[filter.slice(0, nextOpen)],
+ });
+ this._parseFilter(filter.slice(nextOpen), depth, tokens);
+ } else if (nextClose != -1) {
+ // Case: "key=value)"
+ tokens.push({
+ type: "field",
+ depth,
+ value: this._parseFilterValue(filter.slice(0, nextClose)),
+ });
+ this._parseFilter(filter.slice(nextClose + 1), depth, tokens);
+ }
+ return tokens;
+ }
+
+ /**
+ * Parse a filter string to a LocalBaseBlock.
+ *
+ * @param {string} filter - The filter string to parse.
+ * @returns {LocalBaseBlock}
+ */
+ _convertFilterToBlock(filter) {
+ if (!filter.startsWith("(")) {
+ // Make sure filter is wrapped in parens, see rfc2254#section-4.
+ filter = `(${filter})`;
+ }
+ let tokens = this._parseFilter(filter);
+ let stack = [];
+ for (let { type, depth, value } of tokens) {
+ while (depth < stack.length) {
+ // We are done with the current block, go one level up.
+ stack.pop();
+ }
+ if (type == "op") {
+ if (depth == stack.length) {
+ // We are done with the current block, go one level up.
+ stack.pop();
+ }
+ // Found a new block, go one level down.
+ let parent = stack.slice(-1)[0];
+ let curBlock = new asn1js.Constructed({
+ idBlock: this._getContextId(value),
+ });
+ stack.push(curBlock);
+ if (parent) {
+ parent.valueBlock.value.push(curBlock);
+ }
+ } else if (type == "field") {
+ let [tagNumber, field, fieldValue] = value;
+ let block;
+ let idBlock = this._getContextId(tagNumber);
+ if (tagNumber == this.FILTER_PRESENT) {
+ // A present filter.
+ block = new asn1js.Primitive({
+ idBlock,
+ valueHex: new TextEncoder().encode(field),
+ });
+ } else if (tagNumber == this.FILTER_EXTENSIBLE_MATCH) {
+ // An extensibleMatch filter is in the form of
+ // <type>:dn:<rule>:=<value>. We need to further parse the field.
+ let parts = field.split(":");
+ let value = [];
+ if (parts.length == 3) {
+ // field is <type>:dn:<rule>.
+ if (parts[2]) {
+ value.push(
+ this._contextStringBlock(this.MATCHING_RULE, parts[2])
+ );
+ }
+ if (parts[0]) {
+ value.push(
+ this._contextStringBlock(this.MATCHING_TYPE, parts[0])
+ );
+ }
+ value.push(
+ this._contextStringBlock(this.MATCHING_VALUE, fieldValue)
+ );
+ if (parts[1] == "dn") {
+ let dn = new asn1js.Boolean({
+ value: true,
+ });
+ dn.idBlock.tagClass = LDAPMessage.TAG_CLASS_CONTEXT;
+ dn.idBlock.tagNumber = this.MATCHING_DN;
+ value.push(dn);
+ }
+ } else if (parts.length == 2) {
+ // field is <type>:<rule>.
+ if (parts[1]) {
+ value.push(
+ this._contextStringBlock(this.MATCHING_RULE, parts[1])
+ );
+ }
+
+ if (parts[0]) {
+ value.push(
+ this._contextStringBlock(this.MATCHING_TYPE, parts[0])
+ );
+ }
+ value.push(
+ this._contextStringBlock(this.MATCHING_VALUE, fieldValue)
+ );
+ } else {
+ // field is <type>.
+ value = [
+ this._contextStringBlock(this.MATCHING_TYPE, field),
+ this._contextStringBlock(this.MATCHING_VALUE, fieldValue),
+ ];
+ }
+ block = new asn1js.Constructed({
+ idBlock,
+ value,
+ });
+ } else if (tagNumber != this.FILTER_SUBSTRINGS) {
+ // A filter that is not substrings filter.
+ block = new asn1js.Constructed({
+ idBlock,
+ value: [
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(field),
+ }),
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(fieldValue),
+ }),
+ ],
+ });
+ } else {
+ // A substrings filter.
+ let substringsSeq = new asn1js.Sequence();
+ block = new asn1js.Constructed({
+ idBlock,
+ value: [
+ new asn1js.OctetString({
+ valueHex: new TextEncoder().encode(field),
+ }),
+ substringsSeq,
+ ],
+ });
+ for (let i = 0; i < fieldValue.length; i++) {
+ let v = fieldValue[i];
+ if (!v.length) {
+ // Case: *
+ continue;
+ } else if (i < fieldValue.length - 1) {
+ // Case: abc*
+ substringsSeq.valueBlock.value.push(
+ new asn1js.Primitive({
+ idBlock: this._getContextId(
+ i == 0 ? this.SUBSTRINGS_INITIAL : this.SUBSTRINGS_ANY
+ ),
+ valueHex: new TextEncoder().encode(v),
+ })
+ );
+ } else {
+ // Case: *abc
+ substringsSeq.valueBlock.value.push(
+ new asn1js.Primitive({
+ idBlock: this._getContextId(this.SUBSTRINGS_FINAL),
+ valueHex: new TextEncoder().encode(v),
+ })
+ );
+ }
+ }
+ }
+ let curBlock = stack.slice(-1)[0];
+ if (curBlock) {
+ curBlock.valueBlock.value.push(block);
+ } else {
+ stack.push(block);
+ }
+ }
+ }
+
+ return stack[0];
+ }
+}
+
+class AbandonRequest extends LDAPMessage {
+ static APPLICATION = 16;
+
+ /**
+ * @param {string} messageId - The messageId to abandon.
+ */
+ constructor(messageId) {
+ super();
+ this.protocolOp = new asn1js.Integer({ value: messageId });
+ // [APPLICATION 16]
+ this.protocolOp.idBlock.tagClass = LDAPMessage.TAG_CLASS_APPLICATION;
+ this.protocolOp.idBlock.tagNumber = AbandonRequest.APPLICATION;
+ }
+}
+
+class LDAPResult {
+ /**
+ * @param {number} resultCode - The result code.
+ * @param {string} matchedDN - For certain result codes, matchedDN is the last entry used.
+ * @param {string} diagnosticMessage - A diagnostic message returned by the server.
+ */
+ constructor(resultCode, matchedDN, diagnosticMessage) {
+ this.resultCode = resultCode;
+ this.matchedDN = matchedDN;
+ this.diagnosticMessage = diagnosticMessage;
+ }
+}
+
+/**
+ * A base class for all LDAP response messages.
+ *
+ * @property {LDAPResult} result - The result of a response.
+ */
+class LDAPResponse extends LDAPMessage {
+ /**
+ * @param {number} messageId - The message id.
+ * @param {LocalBaseBlock} protocolOp - The message content.
+ * @param {number} byteLength - The byte size of this message in raw BER form.
+ */
+ constructor(messageId, protocolOp, byteLength) {
+ super();
+ this.messageId = messageId;
+ this.protocolOp = protocolOp;
+ this.byteLength = byteLength;
+ }
+
+ /**
+ * Find the corresponding response class name from a tag number.
+ *
+ * @param {number} tagNumber - The tag number of a block.
+ * @returns {LDAPResponse}
+ */
+ static _getResponseClassFromTagNumber(tagNumber) {
+ return [
+ SearchResultEntry,
+ SearchResultDone,
+ SearchResultReference,
+ BindResponse,
+ ExtendedResponse,
+ ].find(x => x.APPLICATION == tagNumber);
+ }
+
+ /**
+ * Decode a raw server response to LDAPResponse instance.
+ *
+ * @param {ArrayBuffer} buffer - The raw message received from the server.
+ * @returns {LDAPResponse} A concrete instance of LDAPResponse subclass.
+ */
+ static fromBER(buffer) {
+ let decoded = asn1js.fromBER(buffer);
+ if (decoded.offset == -1 || decoded.result.error) {
+ throw Components.Exception(
+ decoded.result.error,
+ Cr.NS_ERROR_CANNOT_CONVERT_DATA
+ );
+ }
+ let value = decoded.result.valueBlock.value;
+ let protocolOp = value[1];
+ if (protocolOp.idBlock.tagClass != this.TAG_CLASS_APPLICATION) {
+ throw Components.Exception(
+ `Unexpected tagClass ${protocolOp.idBlock.tagClass}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ let ProtocolOp = this._getResponseClassFromTagNumber(
+ protocolOp.idBlock.tagNumber
+ );
+ if (!ProtocolOp) {
+ throw Components.Exception(
+ `Unexpected tagNumber ${protocolOp.idBlock.tagNumber}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ let op = new ProtocolOp(
+ value[0].valueBlock.valueDec,
+ protocolOp,
+ decoded.offset
+ );
+ op.parse();
+ return op;
+ }
+
+ /**
+ * Parse the protocolOp part of a LDAPMessage to LDAPResult. For LDAP
+ * responses that are simply LDAPResult, reuse this function. Other responses
+ * need to implement this function.
+ */
+ parse() {
+ let value = this.protocolOp.valueBlock.value;
+ let resultCode = value[0].valueBlock.valueDec;
+ let matchedDN = new TextDecoder().decode(value[1].valueBlock.valueHex);
+ let diagnosticMessage = new TextDecoder().decode(
+ value[2].valueBlock.valueHex
+ );
+ this.result = new LDAPResult(resultCode, matchedDN, diagnosticMessage);
+ }
+}
+
+class BindResponse extends LDAPResponse {
+ static APPLICATION = 1;
+
+ parse() {
+ super.parse();
+ let serverSaslCredsBlock = this.protocolOp.valueBlock.value[3];
+ if (serverSaslCredsBlock) {
+ this.result.serverSaslCreds = serverSaslCredsBlock.valueBlock.valueHex;
+ }
+ }
+}
+
+class SearchResultEntry extends LDAPResponse {
+ static APPLICATION = 4;
+
+ parse() {
+ let value = this.protocolOp.valueBlock.value;
+ let objectName = new TextDecoder().decode(value[0].valueBlock.valueHex);
+ let attributes = {};
+ for (let attr of value[1].valueBlock.value) {
+ let attrValue = attr.valueBlock.value;
+ let type = new TextDecoder().decode(attrValue[0].valueBlock.valueHex);
+ let vals = attrValue[1].valueBlock.value.map(v => v.valueBlock.valueHex);
+ attributes[type] = vals;
+ }
+ this.result = { objectName, attributes };
+ }
+}
+
+class SearchResultDone extends LDAPResponse {
+ static APPLICATION = 5;
+}
+
+class SearchResultReference extends LDAPResponse {
+ static APPLICATION = 19;
+
+ parse() {
+ let value = this.protocolOp.valueBlock.value;
+ this.result = value.map(block =>
+ new TextDecoder().decode(block.valueBlock.valueHex)
+ );
+ }
+}
+
+class ExtendedResponse extends LDAPResponse {
+ static APPLICATION = 24;
+}
diff --git a/comm/mailnews/addrbook/modules/LDAPOperation.jsm b/comm/mailnews/addrbook/modules/LDAPOperation.jsm
new file mode 100644
index 0000000000..d0e2d64a54
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPOperation.jsm
@@ -0,0 +1,198 @@
+/* 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 = ["LDAPOperation"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "LDAPClient",
+ "resource:///modules/LDAPClient.jsm"
+);
+
+/**
+ * A module to manage LDAP operation.
+ *
+ * @implements {nsILDAPOperation}
+ */
+class LDAPOperation {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPOperation"]);
+
+ init(connection, listener, closure) {
+ this._listener = listener;
+ this._connection = connection;
+ this._client = connection.wrappedJSObject.client;
+
+ this._referenceUrls = [];
+
+ // Cache request arguments to use when searching references.
+ this._simpleBindPassword = null;
+ this._saslBindArgs = null;
+ this._searchArgs = null;
+ }
+
+ simpleBind(password) {
+ this._password = password;
+ try {
+ this._messageId = this._client.bind(
+ this._connection.bindName,
+ password,
+ res => this._onBindSuccess(res.result.resultCode)
+ );
+ } catch (e) {
+ this._listener.onLDAPError(e.result, null, "");
+ }
+ }
+
+ saslBind(service, mechanism, authModuleType, serverCredentials) {
+ this._saslBindArgs = [service, mechanism, authModuleType];
+ try {
+ this._client.saslBind(
+ service,
+ mechanism,
+ authModuleType,
+ serverCredentials,
+ res => {
+ if (res.result.resultCode == Ci.nsILDAPErrors.SASL_BIND_IN_PROGRESS) {
+ this.saslBind(
+ service,
+ mechanism,
+ authModuleType,
+ res.result.serverSaslCreds
+ );
+ } else if (res.result.resultCode == Ci.nsILDAPErrors.SUCCESS) {
+ this._onBindSuccess(res.result.resultCode);
+ }
+ }
+ );
+ } catch (e) {
+ this._listener.onLDAPError(e.result, null, "");
+ }
+ }
+
+ searchExt(baseDN, scope, filter, attributes, timeout, limit) {
+ this._searchArgs = [baseDN, scope, filter, attributes, timeout, limit];
+ try {
+ this._messageId = this._client.search(
+ baseDN,
+ scope,
+ filter,
+ attributes,
+ timeout,
+ limit,
+ res => {
+ if (res.constructor.name == "SearchResultEntry") {
+ this._listener.onLDAPMessage({
+ QueryInterface: ChromeUtils.generateQI(["nsILDAPMessage"]),
+ errorCode: 0,
+ type: Ci.nsILDAPMessage.RES_SEARCH_ENTRY,
+ getAttributes() {
+ return Object.keys(res.result.attributes);
+ },
+ // Find the matching attribute name while ignoring the case.
+ _getAttribute(attr) {
+ attr = attr.toLowerCase();
+ return this.getAttributes().find(x => x.toLowerCase() == attr);
+ },
+ getValues(attr) {
+ attr = this._getAttribute(attr);
+ return res.result.attributes[attr]?.map(v =>
+ new TextDecoder().decode(v)
+ );
+ },
+ getBinaryValues(attr) {
+ attr = this._getAttribute(attr);
+ return res.result.attributes[attr]?.map(v => ({
+ // @see nsILDAPBERValue
+ get: () => new Uint8Array(v),
+ }));
+ },
+ });
+ } else if (res.constructor.name == "SearchResultReference") {
+ this._referenceUrls.push(...res.result);
+ } else if (res.constructor.name == "SearchResultDone") {
+ // NOTE: we create a new connection for every search, can be changed
+ // to reuse connections.
+ this._client.onError = () => {};
+ this._client.unbind();
+ this._messageId = null;
+ if (this._referenceUrls.length) {
+ this._searchReference(this._referenceUrls.shift());
+ } else {
+ this._listener.onLDAPMessage({
+ errorCode: res.result.resultCode,
+ type: Ci.nsILDAPMessage.RES_SEARCH_RESULT,
+ });
+ }
+ }
+ }
+ );
+ } catch (e) {
+ this._listener.onLDAPError(e.result, null, "");
+ }
+ }
+
+ abandonExt() {
+ if (this._messageId) {
+ this._client.abandon(this._messageId);
+ }
+ }
+
+ /**
+ * Decide what to do on bind success. When searching a reference url, trigger
+ * a new search. Otherwise, emit a message to this._listener.
+ *
+ * @param {number} errorCode - The result code of BindResponse.
+ */
+ _onBindSuccess(errorCode) {
+ if (this._searchingReference) {
+ this.searchExt(...this._searchArgs);
+ } else {
+ this._listener.onLDAPMessage({
+ errorCode,
+ type: Ci.nsILDAPMessage.RES_BIND,
+ });
+ }
+ }
+
+ /**
+ * Connect to a reference url and continue the search.
+ *
+ * @param {string} urlStr - A url string we get from SearchResultReference.
+ */
+ _searchReference(urlStr) {
+ this._searchingReference = true;
+ let urlParser = Cc["@mozilla.org/network/ldap-url-parser;1"].createInstance(
+ Ci.nsILDAPURLParser
+ );
+ let url;
+ try {
+ url = urlParser.parse(urlStr);
+ } catch (e) {
+ console.error(e);
+ return;
+ }
+ this._client = new lazy.LDAPClient(
+ url.host,
+ url.port,
+ url.options & Ci.nsILDAPURL.OPT_SECURE
+ );
+ this._client.onOpen = () => {
+ if (this._password) {
+ this.simpleBind(this._password);
+ } else {
+ this.saslBind(...this._saslBindData);
+ }
+ };
+ this._client.onError = (status, secInfo) => {
+ this._listener.onLDAPError(status, secInfo, `${url.host}:${url.port}`);
+ };
+ this._client.connect();
+ }
+}
+
+LDAPOperation.prototype.classID = Components.ID(
+ "{a6f94ca4-cd2d-4983-bcf2-fe936190955c}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm b/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm
new file mode 100644
index 0000000000..c9d84f2d99
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm
@@ -0,0 +1,41 @@
+/* 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 EXPORTED_SYMBOLS = ["LDAPProtocolHandler", "LDAPSProtocolHandler"];
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class LDAPProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "ldap";
+
+ newChannel(aURI, aLoadInfo) {
+ let channel = Cc["@mozilla.org/network/ldap-channel;1"].createInstance(
+ Ci.nsIChannel
+ );
+ channel.init(aURI);
+ channel.loadInfo = aLoadInfo;
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return port == 389;
+ }
+}
+LDAPProtocolHandler.prototype.classID = Components.ID(
+ "{b3de9249-b0e5-4c12-8d91-c9a434fd80f5}"
+);
+
+class LDAPSProtocolHandler extends LDAPProtocolHandler {
+ scheme = "ldaps";
+
+ allowPort(port, scheme) {
+ return port == 636;
+ }
+}
+LDAPSProtocolHandler.prototype.classID = Components.ID(
+ "{c85a5ef2-9c56-445f-b029-76889f2dd29b}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm b/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm
new file mode 100644
index 0000000000..2a11d15eee
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm
@@ -0,0 +1,233 @@
+/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 = ["LDAPReplicationService"];
+
+const { LDAPListenerBase } = ChromeUtils.import(
+ "resource:///modules/LDAPListenerBase.jsm"
+);
+var { SQLiteDirectory } = ChromeUtils.import(
+ "resource:///modules/SQLiteDirectory.jsm"
+);
+
+/**
+ * A service to replicate a LDAP directory to a local SQLite db.
+ *
+ * @implements {nsIAbLDAPReplicationService}
+ * @implements {nsILDAPMessageListener}
+ */
+class LDAPReplicationService extends LDAPListenerBase {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIAbLDAPReplicationService",
+ "nsILDAPMessageListener",
+ ]);
+
+ /**
+ * @see nsIAbLDAPReplicationService
+ */
+ startReplication(directory, progressListener) {
+ this._directory = directory;
+ this._listener = progressListener;
+ this._attrMap = directory.attributeMap;
+ this._count = 0;
+ this._cards = [];
+ this._connection = Cc[
+ "@mozilla.org/network/ldap-connection;1"
+ ].createInstance(Ci.nsILDAPConnection);
+ this._operation = Cc[
+ "@mozilla.org/network/ldap-operation;1"
+ ].createInstance(Ci.nsILDAPOperation);
+
+ this._connection.init(
+ directory.lDAPURL,
+ directory.authDn,
+ this,
+ null,
+ directory.protocolVersion
+ );
+ }
+
+ /**
+ * @see nsIAbLDAPReplicationService
+ */
+ cancelReplication(directory) {
+ this._operation.abandonExt();
+ this.done(false);
+ }
+
+ /**
+ * @see nsIAbLDAPReplicationService
+ */
+ done(success) {
+ this._done(success);
+ }
+
+ /**
+ * @see nsILDAPMessageListener
+ */
+ onLDAPMessage(msg) {
+ switch (msg.type) {
+ case Ci.nsILDAPMessage.RES_BIND:
+ this._onLDAPBind(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_ENTRY:
+ this._onLDAPSearchEntry(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_RESULT:
+ this._onLDAPSearchResult(msg);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * @see nsILDAPMessageListener
+ */
+ onLDAPError(status, secInfo, location) {
+ this.done(false);
+ }
+
+ /**
+ * @see LDAPListenerBase
+ */
+ _actionOnBindSuccess() {
+ this._openABForReplicationDir();
+ let ldapUrl = this._directory.lDAPURL;
+ this._operation.init(this._connection, this, null);
+ this._listener.onStateChange(
+ null,
+ null,
+ Ci.nsIWebProgressListener.STATE_START,
+ Cr.NS_OK
+ );
+ this._operation.searchExt(
+ ldapUrl.dn,
+ ldapUrl.scope,
+ ldapUrl.filter,
+ ldapUrl.attributes,
+ 0,
+ 0
+ );
+ }
+
+ /**
+ * @see LDAPListenerBase
+ */
+ _actionOnBindFailure() {
+ this._done(false);
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ async _onLDAPSearchEntry(msg) {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ this._attrMap.setCardPropertiesFromLDAPMessage(msg, newCard);
+ this._cards.push(newCard);
+ this._count++;
+ if (this._count % 10 == 0) {
+ // inform the listener every 10 entries
+ this._listener.onProgressChange(
+ null,
+ null,
+ this._count,
+ -1,
+ this._count,
+ -1
+ );
+ }
+ if (this._count % 100 == 0 && !this._writePromise) {
+ // Write to the db to release some memory.
+ this._writePromise = this._replicationDB.bulkAddCards(this._cards);
+ this._cards = [];
+ await this._writePromise;
+ this._writePromise = null;
+ }
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_RESULT message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ async _onLDAPSearchResult(msg) {
+ if (
+ msg.errorCode == Ci.nsILDAPErrors.SUCCESS ||
+ msg.errorCode == Ci.nsILDAPErrors.SIZELIMIT_EXCEEDED
+ ) {
+ if (this._writePromise) {
+ await this._writePromise;
+ }
+ await this._replicationDB.bulkAddCards(this._cards);
+ this.done(true);
+ return;
+ }
+ this.done(false);
+ }
+
+ /**
+ * Init a jsaddrbook from the replicationFileName of the current LDAP directory.
+ */
+ _openABForReplicationDir() {
+ this._oldReplicationFileName = this._directory.replicationFileName;
+ this._replicationFile = this._directory.replicationFile;
+ if (this._replicationFile.exists()) {
+ // If the database file already exists, create a new one here, and replace
+ // the old file in _done when success.
+ this._replicationFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ // What we need is the unique file name, _replicationDB will create an
+ // empty database file.
+ this._replicationFile.remove(false);
+ // Set replicationFileName to the new db file name, so that _replicationDB
+ // works correctly.
+ this._directory.replicationFileName = this._replicationFile.leafName;
+ }
+
+ this._replicationDB = new SQLiteDirectory();
+ this._replicationDB.init(`jsaddrbook://${this._replicationFile.leafName}`);
+ }
+
+ /**
+ * Clean up depending on whether replication succeeded or failed, emit
+ * STATE_STOP event.
+ *
+ * @param {bool} success - Replication succeeded or failed.
+ */
+ async _done(success) {
+ this._cards = [];
+ if (this._replicationDB) {
+ // Close the db.
+ await this._replicationDB.cleanUp();
+ }
+ if (success) {
+ // Replace the old db file with new db file.
+ this._replicationFile.moveTo(null, this._oldReplicationFileName);
+ } else if (
+ this._replicationFile &&
+ this._replicationFile.path != this._oldReplicationFileName
+ ) {
+ this._replicationFile.remove(false);
+ }
+ if (this._oldReplicationFileName) {
+ // Reset replicationFileName to the old db file name.
+ this._directory.replicationFileName = this._oldReplicationFileName;
+ }
+ this._listener.onStateChange(
+ null,
+ null,
+ Ci.nsIWebProgressListener.STATE_STOP,
+ success ? Cr.NS_OK : Cr.NS_ERROR_FAILURE
+ );
+ }
+}
+
+LDAPReplicationService.prototype.classID = Components.ID(
+ "{dbe204e8-ae09-11eb-b4c8-a7e4b3e6e82e}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPService.jsm b/comm/mailnews/addrbook/modules/LDAPService.jsm
new file mode 100644
index 0000000000..d1def67afc
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPService.jsm
@@ -0,0 +1,66 @@
+/* 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 = ["LDAPService"];
+
+/**
+ * @implements {nsILDAPService}
+ */
+class LDAPService {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPService"]);
+
+ createFilter(maxSize, pattern, prefix, suffix, attr, value) {
+ let words = value.split(" ");
+ // Get the Mth to Nth words.
+ function getMtoN(m, n) {
+ n = n || m;
+ return words.slice(m - 1, n).join(" ");
+ }
+
+ let filter = prefix;
+ pattern.replaceAll("%a", attr);
+ while (pattern) {
+ let index = pattern.indexOf("%v");
+ if (index == -1) {
+ filter += pattern;
+ pattern = "";
+ } else {
+ filter += pattern.slice(0, index);
+ // Get the three characters after %v.
+ let [c1, c2, c3] = pattern.slice(index + 2, index + 5);
+ if (c1 >= "1" && c1 <= "9") {
+ if (c2 == "$") {
+ // %v$: means the last word
+ filter += getMtoN(words.length);
+ pattern = pattern.slice(index + 3);
+ } else if (c2 == "-") {
+ if (c3 >= "1" && c3 <= "9") {
+ // %vM-N: means from the Mth to the Nth word
+ filter += getMtoN(c1, c3);
+ pattern = pattern.slice(index + 5);
+ } else {
+ // %vN-: means from the Nth to the last word
+ filter += getMtoN(c1, words.length);
+ pattern = pattern.slice(index + 4);
+ }
+ } else {
+ // %vN: means the Nth word
+ filter += getMtoN(c1);
+ pattern = pattern.slice(index + 3);
+ }
+ } else {
+ // %v: means the entire search value
+ filter += value;
+ pattern = pattern.slice(index + 2);
+ }
+ }
+ }
+ filter += suffix;
+ return filter.length > maxSize ? "" : filter;
+ }
+}
+
+LDAPService.prototype.classID = Components.ID(
+ "{e8b59b32-f83f-4d5f-8eb5-e3c1e5de0d47}"
+);
diff --git a/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm b/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm
new file mode 100644
index 0000000000..d92fea191f
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm
@@ -0,0 +1,112 @@
+/* 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 = ["LDAPSyncQuery"];
+
+/**
+ * @implements {nsILDAPMessageListener}
+ * @implements {nsILDAPSyncQuery}
+ */
+class LDAPSyncQuery {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsILDAPMessageListener",
+ "nsILDAPSyncQuery",
+ ]);
+
+ /** @see nsILDAPMessageListener */
+ onLDAPInit() {
+ this._operation = Cc[
+ "@mozilla.org/network/ldap-operation;1"
+ ].createInstance(Ci.nsILDAPOperation);
+ this._operation.init(this._connection, this, null);
+ this._operation.simpleBind("");
+ }
+
+ onLDAPMessage(msg) {
+ switch (msg.type) {
+ case Ci.nsILDAPMessage.RES_BIND:
+ this._onLDAPBind(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_ENTRY:
+ this._onLDAPSearchEntry(msg);
+ break;
+ case Ci.nsILDAPMessage.RES_SEARCH_RESULT:
+ this._onLDAPSearchResult(msg);
+ break;
+ default:
+ break;
+ }
+ }
+
+ onLDAPError(status, secInfo, location) {
+ this._statusCode = status;
+ this._finished = true;
+ }
+
+ /** @see nsILDAPSyncQuery */
+ getQueryResults(ldapUrl, protocolVersion) {
+ this._ldapUrl = ldapUrl;
+ this._connection = Cc[
+ "@mozilla.org/network/ldap-connection;1"
+ ].createInstance(Ci.nsILDAPConnection);
+ this._connection.init(ldapUrl, "", this, null, protocolVersion);
+
+ this._statusCode = 0;
+ this._result = "";
+ this._finished = false;
+
+ Services.tm.spinEventLoopUntil(
+ "getQueryResults is a sync function",
+ () => this._finished
+ );
+ if (this._statusCode) {
+ throw Components.Exception("getQueryResults failed", this._statusCode);
+ }
+ return this._result;
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_BIND message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPBind(msg) {
+ if (msg.errorCode != Ci.nsILDAPErrors.SUCCESS) {
+ this._statusCode = msg.errorCode;
+ this._finished = true;
+ return;
+ }
+ this._operation.init(this._connection, this, null);
+ this._operation.searchExt(
+ this._ldapUrl.dn,
+ this._ldapUrl.scope,
+ this._ldapUrl.filter,
+ this._ldapUrl.attributes,
+ 0,
+ 0
+ );
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPSearchEntry(msg) {
+ for (let attr of msg.getAttributes()) {
+ for (let value of msg.getValues(attr)) {
+ this._result += `\n${attr}=${value}`;
+ }
+ }
+ }
+
+ /**
+ * Handler of nsILDAPMessage.RES_SEARCH_RESULT message.
+ *
+ * @param {nsILDAPMessage} msg - The received LDAP message.
+ */
+ _onLDAPSearchResult(msg) {
+ this._finished = true;
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/LDAPURLParser.jsm b/comm/mailnews/addrbook/modules/LDAPURLParser.jsm
new file mode 100644
index 0000000000..2c19be1386
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/LDAPURLParser.jsm
@@ -0,0 +1,42 @@
+/* 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 = ["LDAPURLParser"];
+
+/**
+ * @implements {nsILDAPURLParser}
+ */
+class LDAPURLParser {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPURLParser"]);
+
+ parse(spec) {
+ // The url is in the form of scheme://hostport/dn?attributes?scope?filter,
+ // see RFC2255.
+ let matches =
+ /^(ldaps?):\/\/\[?([^\s\]/]+)\]?:?(\d*)\/([^\s?]*)\??(.*)$/.exec(spec);
+ if (!matches) {
+ throw Components.Exception(
+ `Invalid LDAP URL: ${spec}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ let [, scheme, host, port, dn, query] = matches;
+ let [attributes, scopeString, filter] = query.split("?");
+ let scope =
+ {
+ one: Ci.nsILDAPURL.SCOPE_ONELEVEL,
+ sub: Ci.nsILDAPURL.SCOPE_SUBTREE,
+ }[scopeString] || Ci.nsILDAPURL.SCOPE_BASE;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsILDAPURLParserResult"]),
+ host,
+ port,
+ dn: decodeURIComponent(dn),
+ attributes,
+ scope,
+ filter: filter ? decodeURIComponent(filter) : "(objectclass=*)",
+ options: scheme == "ldaps" ? Ci.nsILDAPURL.OPT_SECURE : 0,
+ };
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm b/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm
new file mode 100644
index 0000000000..0129d2e3d3
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm
@@ -0,0 +1,186 @@
+/* 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 = ["QueryStringToExpression"];
+
+/**
+ * A module to parse a query string to a nsIAbBooleanExpression. A valid query
+ * string is in this form:
+ *
+ * (OP1(FIELD1,COND1,VALUE1)..(FIELDn,CONDn,VALUEn)(BOOL2(FIELD1,COND1,VALUE1)..)..)
+ *
+ * OPn A boolean operator joining subsequent terms delimited by ().
+ *
+ * @see {nsIAbBooleanOperationTypes}.
+ * FIELDn An addressbook card data field.
+ * CONDn A condition to compare FIELDn with VALUEn.
+ * @see {nsIAbBooleanConditionTypes}.
+ * VALUEn The value to be matched in the FIELDn via the CONDn.
+ * The value must be URL encoded by the caller, if it contains any
+ * special characters including '(' and ')'.
+ */
+var QueryStringToExpression = {
+ /**
+ * Convert a query string to a nsIAbBooleanExpression.
+ *
+ * @param {string} qs - The query string to convert.
+ * @returns {nsIAbBooleanExpression}
+ */
+ convert(qs) {
+ let tokens = this.parse(qs);
+
+ // An array of nsIAbBooleanExpression, the first element is the root exp,
+ // the last element is the current operating exp.
+ let stack = [];
+ for (let { type, depth, value } of tokens) {
+ while (depth < stack.length) {
+ // We are done with the current exp, go one level up.
+ stack.pop();
+ }
+ if (type == "op") {
+ if (depth == stack.length) {
+ // We are done with the current exp, go one level up.
+ stack.pop();
+ }
+ // Found a new exp, go one level down.
+ let parent = stack.slice(-1)[0];
+ let exp = this.createBooleanExpression(value);
+ stack.push(exp);
+ if (parent) {
+ parent.expressions = [...parent.expressions, exp];
+ }
+ } else if (type == "field") {
+ // Add a new nsIAbBooleanConditionString to the current exp.
+ let condition = this.createBooleanConditionString(...value);
+ let exp = stack.slice(-1)[0];
+ exp.expressions = [...exp.expressions, condition];
+ }
+ }
+
+ return stack[0];
+ },
+
+ /**
+ * Parse a query string to an array of tokens.
+ *
+ * @param {string} qs - The query string to parse.
+ * @param {number} depth - The depth of a token.
+ * @param {object[]} tokens - The tokens to return.
+ * @param {"op"|"field"} tokens[].type - The token type.
+ * @param {number} tokens[].depth - The token depth.
+ * @param {string|string[]} tokens[].value - The token value.
+ */
+ parse(qs, depth = 0, tokens = []) {
+ if (qs[0] == "?") {
+ qs = qs.slice(1);
+ }
+ while (qs[0] == ")" && depth > 0) {
+ depth--;
+ qs = qs.slice(1);
+ }
+ if (qs.length == 0) {
+ // End of input.
+ return tokens;
+ }
+ if (qs[0] != "(") {
+ throw Components.Exception(
+ `Invalid query string: ${qs}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ qs = qs.slice(1);
+ let nextOpen = qs.indexOf("(");
+ let nextClose = qs.indexOf(")");
+
+ if (nextOpen != -1 && nextOpen < nextClose) {
+ // Case: "OP("
+ depth++;
+ tokens.push({
+ type: "op",
+ depth,
+ value: qs.slice(0, nextOpen),
+ });
+ this.parse(qs.slice(nextOpen), depth, tokens);
+ } else if (nextClose != -1) {
+ // Case: "FIELD, COND, VALUE)"
+ tokens.push({
+ type: "field",
+ depth,
+ value: qs.slice(0, nextClose).split(","),
+ });
+ this.parse(qs.slice(nextClose + 1), depth, tokens);
+ }
+ return tokens;
+ },
+
+ /**
+ * Create a nsIAbBooleanExpression from a string.
+ *
+ * @param {string} operation - The operation string.
+ * @returns {nsIAbBooleanExpression}
+ */
+ createBooleanExpression(operation) {
+ let op = {
+ and: Ci.nsIAbBooleanOperationTypes.AND,
+ or: Ci.nsIAbBooleanOperationTypes.OR,
+ not: Ci.nsIAbBooleanOperationTypes.NOT,
+ }[operation];
+ if (op == undefined) {
+ throw Components.Exception(
+ `Invalid operation: ${operation}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ let exp = Cc["@mozilla.org/boolean-expression/n-peer;1"].createInstance(
+ Ci.nsIAbBooleanExpression
+ );
+ exp.operation = op;
+ return exp;
+ },
+
+ /**
+ * Create a nsIAbBooleanConditionString.
+ *
+ * @param {string} name - The field name.
+ * @param {nsIAbBooleanConditionTypes} condition - The condition.
+ * @param {string} value - The value string.
+ * @returns {nsIAbBooleanConditionString}
+ */
+ createBooleanConditionString(name, condition, value) {
+ value = decodeURIComponent(value);
+ let cond = {
+ "=": Ci.nsIAbBooleanConditionTypes.Is,
+ "!=": Ci.nsIAbBooleanConditionTypes.IsNot,
+ lt: Ci.nsIAbBooleanConditionTypes.LessThan,
+ gt: Ci.nsIAbBooleanConditionTypes.GreaterThan,
+ bw: Ci.nsIAbBooleanConditionTypes.BeginsWith,
+ ew: Ci.nsIAbBooleanConditionTypes.EndsWith,
+ c: Ci.nsIAbBooleanConditionTypes.Contains,
+ "!c": Ci.nsIAbBooleanConditionTypes.DoesNotContain,
+ "~=": Ci.nsIAbBooleanConditionTypes.SoundsLike,
+ regex: Ci.nsIAbBooleanConditionTypes.RegExp,
+ ex: Ci.nsIAbBooleanConditionTypes.Exists,
+ "!ex": Ci.nsIAbBooleanConditionTypes.DoesNotExist,
+ }[condition];
+ if (name == "" || condition == "" || value == "" || cond == undefined) {
+ throw Components.Exception(
+ `Failed to create condition string from name=${name}, condition=${condition}, value=${value}, cond=${cond}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ let cs = Cc[
+ "@mozilla.org/boolean-expression/condition-string;1"
+ ].createInstance(Ci.nsIAbBooleanConditionString);
+ cs.condition = cond;
+
+ try {
+ cs.name = Services.textToSubURI.unEscapeAndConvert("UTF-8", name);
+ cs.value = Services.textToSubURI.unEscapeAndConvert("UTF-8", value);
+ } catch (e) {
+ cs.name = name;
+ cs.value = value;
+ }
+ return cs;
+ },
+};
diff --git a/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm b/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm
new file mode 100644
index 0000000000..a89f2880d7
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm
@@ -0,0 +1,474 @@
+/* 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 = ["SQLiteDirectory"];
+
+const { AddrBookDirectory } = ChromeUtils.import(
+ "resource:///modules/AddrBookDirectory.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+const { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ newUID: "resource:///modules/AddrBookUtils.jsm",
+});
+
+var log = console.createInstance({
+ prefix: "mail.addr_book",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.addr_book.loglevel",
+});
+
+// Track all directories by filename, for SQLiteDirectory.forFile.
+var directories = new Map();
+
+// Keep track of all database connections, and close them at shutdown, since
+// nothing else ever tells us to close them.
+var connections = new Map();
+
+/**
+ * Opens an SQLite connection to `file`, caches the connection, and upgrades
+ * the database schema if necessary.
+ */
+function openConnectionTo(file) {
+ const CURRENT_VERSION = 4;
+
+ let connection = connections.get(file.path);
+ if (!connection) {
+ connection = Services.storage.openDatabase(file);
+ let fileVersion = connection.schemaVersion;
+
+ // If we're upgrading the version, first create a backup.
+ if (fileVersion > 0 && fileVersion < CURRENT_VERSION) {
+ let backupFile = file.clone();
+ backupFile.leafName = backupFile.leafName.replace(
+ /\.sqlite$/,
+ `.v${fileVersion}.sqlite`
+ );
+ backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ log.warn(`Backing up ${file.leafName} to ${backupFile.leafName}`);
+ file.copyTo(null, backupFile.leafName);
+ }
+
+ switch (fileVersion) {
+ case 0:
+ connection.executeSimpleSQL("PRAGMA journal_mode=WAL");
+ connection.executeSimpleSQL(
+ "CREATE TABLE properties (card TEXT, name TEXT, value TEXT)"
+ );
+ connection.executeSimpleSQL(
+ "CREATE TABLE lists (uid TEXT PRIMARY KEY, name TEXT, nickName TEXT, description TEXT)"
+ );
+ connection.executeSimpleSQL(
+ "CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card))"
+ );
+ // Falls through.
+ case 1:
+ connection.executeSimpleSQL(
+ "CREATE INDEX properties_card ON properties(card)"
+ );
+ connection.executeSimpleSQL(
+ "CREATE INDEX properties_name ON properties(name)"
+ );
+ // Falls through.
+ case 2:
+ connection.executeSimpleSQL("DROP TABLE IF EXISTS cards");
+ // The lists table may have a localId column we no longer use, but
+ // since SQLite can't drop columns it's not worth effort to remove it.
+ // Falls through.
+ case 3:
+ // This version exists only to create an automatic backup before cards
+ // are transitioned to vCard.
+ connection.schemaVersion = CURRENT_VERSION;
+ break;
+ }
+ connections.set(file.path, connection);
+ }
+ return connection;
+}
+
+/**
+ * Closes the SQLite connection to `file` and removes it from the cache.
+ */
+function closeConnectionTo(file) {
+ let connection = connections.get(file.path);
+ if (connection) {
+ return new Promise(resolve => {
+ connection.asyncClose({
+ complete() {
+ resolve();
+ },
+ });
+ connections.delete(file.path);
+ });
+ }
+ return Promise.resolve();
+}
+
+// Close all open connections at shut down time.
+AsyncShutdown.profileBeforeChange.addBlocker(
+ "Address Book: closing databases",
+ async () => {
+ let promises = [];
+ for (let directory of directories.values()) {
+ promises.push(directory.cleanUp());
+ }
+ await Promise.allSettled(promises);
+ }
+);
+
+// Close a connection on demand. This serves as an escape hatch from C++ code.
+Services.obs.addObserver(async file => {
+ file.QueryInterface(Ci.nsIFile);
+ await closeConnectionTo(file);
+ Services.obs.notifyObservers(file, "addrbook-close-ab-complete");
+}, "addrbook-close-ab");
+
+/**
+ * Adds SQLite storage to AddrBookDirectory.
+ */
+class SQLiteDirectory extends AddrBookDirectory {
+ init(uri) {
+ let uriParts = /^[\w-]+:\/\/([\w\.-]+\.\w+)$/.exec(uri);
+ if (!uriParts) {
+ throw new Components.Exception(
+ `Unexpected uri: ${uri}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ this._uri = uri;
+ let fileName = uriParts[1];
+ if (fileName.includes("/")) {
+ fileName = fileName.substring(0, fileName.indexOf("/"));
+ }
+
+ for (let child of Services.prefs.getChildList("ldap_2.servers.")) {
+ if (
+ child.endsWith(".filename") &&
+ Services.prefs.getStringPref(child) == fileName
+ ) {
+ this._dirPrefId = child.substring(0, child.length - ".filename".length);
+ break;
+ }
+ }
+ if (!this._dirPrefId) {
+ throw Components.Exception(
+ `Couldn't grab dirPrefId for uri=${uri}, fileName=${fileName}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ // Make sure we always have a file. If a file is not created, the
+ // filename may be accidentally reused.
+ let file = lazy.FileUtils.getFile("ProfD", [fileName]);
+ if (!file.exists()) {
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ }
+
+ this._fileName = fileName;
+
+ super.init(uri);
+
+ directories.set(fileName, this);
+ // Create the DB connection here already, to let init() throw on corrupt SQLite files.
+ this._dbConnection;
+ }
+ async cleanUp() {
+ await super.cleanUp();
+
+ if (this.hasOwnProperty("_file")) {
+ await closeConnectionTo(this._file);
+ delete this._file;
+ }
+
+ directories.delete(this._fileName);
+ }
+
+ get _dbConnection() {
+ this._file = lazy.FileUtils.getFile("ProfD", [this.fileName]);
+ let connection = openConnectionTo(this._file);
+
+ // SQLite cache size can be set by the cacheSize preference, in KiB.
+ // The default is 5 MiB but this can be lowered to 1 MiB if wanted.
+ // There is no maximum size.
+ let cacheSize = this.getIntValue("cacheSize", 5120); // 5 MiB
+ cacheSize = Math.max(cacheSize, 1024); // 1 MiB
+ connection.executeSimpleSQL(`PRAGMA cache_size=-${cacheSize}`);
+
+ Object.defineProperty(this, "_dbConnection", {
+ enumerable: true,
+ value: connection,
+ writable: false,
+ });
+ return connection;
+ }
+ get lists() {
+ let listCache = new Map();
+ let selectStatement = this._dbConnection.createStatement(
+ "SELECT uid, name, nickName, description FROM lists"
+ );
+ while (selectStatement.executeStep()) {
+ listCache.set(selectStatement.row.uid, {
+ uid: selectStatement.row.uid,
+ name: selectStatement.row.name,
+ nickName: selectStatement.row.nickName,
+ description: selectStatement.row.description,
+ });
+ }
+ selectStatement.finalize();
+
+ Object.defineProperty(this, "lists", {
+ enumerable: true,
+ value: listCache,
+ writable: false,
+ });
+ return listCache;
+ }
+ get cards() {
+ let cardCache = new Map();
+ let propertiesStatement = this._dbConnection.createStatement(
+ "SELECT card, name, value FROM properties"
+ );
+ while (propertiesStatement.executeStep()) {
+ let uid = propertiesStatement.row.card;
+ if (!cardCache.has(uid)) {
+ cardCache.set(uid, new Map());
+ }
+ let card = cardCache.get(uid);
+ if (card) {
+ card.set(propertiesStatement.row.name, propertiesStatement.row.value);
+ }
+ }
+ propertiesStatement.finalize();
+
+ Object.defineProperty(this, "cards", {
+ enumerable: true,
+ value: cardCache,
+ writable: false,
+ });
+ return cardCache;
+ }
+
+ loadCardProperties(uid) {
+ if (this.hasOwnProperty("cards")) {
+ let cachedCard = this.cards.get(uid);
+ if (cachedCard) {
+ return new Map(cachedCard);
+ }
+ }
+ let properties = new Map();
+ let propertyStatement = this._dbConnection.createStatement(
+ "SELECT name, value FROM properties WHERE card = :card"
+ );
+ propertyStatement.params.card = uid;
+ while (propertyStatement.executeStep()) {
+ properties.set(propertyStatement.row.name, propertyStatement.row.value);
+ }
+ propertyStatement.finalize();
+ return properties;
+ }
+ saveCardProperties(uid, properties) {
+ try {
+ this._dbConnection.beginTransaction();
+ let deleteStatement = this._dbConnection.createStatement(
+ "DELETE FROM properties WHERE card = :card"
+ );
+ deleteStatement.params.card = uid;
+ deleteStatement.execute();
+ let insertStatement = this._dbConnection.createStatement(
+ "INSERT INTO properties VALUES (:card, :name, :value)"
+ );
+
+ for (let [name, value] of properties) {
+ if (value !== null && value !== undefined && value !== "") {
+ insertStatement.params.card = uid;
+ insertStatement.params.name = name;
+ insertStatement.params.value = value;
+ insertStatement.execute();
+ insertStatement.reset();
+ }
+ }
+
+ this._dbConnection.commitTransaction();
+ deleteStatement.finalize();
+ insertStatement.finalize();
+ } catch (ex) {
+ this._dbConnection.rollbackTransaction();
+ throw ex;
+ }
+ }
+ deleteCard(uid) {
+ let deleteStatement = this._dbConnection.createStatement(
+ "DELETE FROM properties WHERE card = :cardUID"
+ );
+ deleteStatement.params.cardUID = uid;
+ deleteStatement.execute();
+ deleteStatement.finalize();
+ }
+ saveList(list) {
+ // Ensure list cache exists.
+ this.lists;
+
+ let replaceStatement = this._dbConnection.createStatement(
+ "REPLACE INTO lists (uid, name, nickName, description) " +
+ "VALUES (:uid, :name, :nickName, :description)"
+ );
+ replaceStatement.params.uid = list._uid;
+ replaceStatement.params.name = list._name;
+ replaceStatement.params.nickName = list._nickName;
+ replaceStatement.params.description = list._description;
+ replaceStatement.execute();
+ replaceStatement.finalize();
+
+ this.lists.set(list._uid, {
+ uid: list._uid,
+ name: list._name,
+ nickName: list._nickName,
+ description: list._description,
+ });
+ }
+ deleteList(uid) {
+ let deleteListStatement = this._dbConnection.createStatement(
+ "DELETE FROM lists WHERE uid = :uid"
+ );
+ deleteListStatement.params.uid = uid;
+ deleteListStatement.execute();
+ deleteListStatement.finalize();
+
+ if (this.hasOwnProperty("lists")) {
+ this.lists.delete(uid);
+ }
+
+ this._dbConnection.executeSimpleSQL(
+ "DELETE FROM list_cards WHERE list NOT IN (SELECT DISTINCT uid FROM lists)"
+ );
+ }
+ async bulkAddCards(cards) {
+ if (cards.length == 0) {
+ return;
+ }
+
+ let usedUIDs = new Set();
+ let propertiesStatement = this._dbConnection.createStatement(
+ "INSERT INTO properties VALUES (:card, :name, :value)"
+ );
+ let propertiesArray = propertiesStatement.newBindingParamsArray();
+ for (let card of cards) {
+ let uid = card.UID;
+ if (!uid || usedUIDs.has(uid)) {
+ // A card cannot have the same UID as one that already exists.
+ // Assign a new UID to avoid losing data.
+ uid = lazy.newUID();
+ }
+ usedUIDs.add(uid);
+
+ let cachedCard;
+ if (this.hasOwnProperty("cards")) {
+ cachedCard = new Map();
+ this.cards.set(uid, cachedCard);
+ }
+
+ for (let [name, value] of this.prepareToSaveCard(card)) {
+ let propertiesParams = propertiesArray.newBindingParams();
+ propertiesParams.bindByName("card", uid);
+ propertiesParams.bindByName("name", name);
+ propertiesParams.bindByName("value", value);
+ propertiesArray.addParams(propertiesParams);
+
+ if (cachedCard) {
+ cachedCard.set(name, value);
+ }
+ }
+ }
+ try {
+ this._dbConnection.beginTransaction();
+ if (propertiesArray.length > 0) {
+ propertiesStatement.bindParameters(propertiesArray);
+ await new Promise((resolve, reject) => {
+ propertiesStatement.executeAsync({
+ handleError(error) {
+ this._error = error;
+ },
+ handleCompletion(status) {
+ if (status == Ci.mozIStorageStatementCallback.REASON_ERROR) {
+ reject(
+ Components.Exception(this._error.message, Cr.NS_ERROR_FAILURE)
+ );
+ } else {
+ resolve();
+ }
+ },
+ });
+ });
+ propertiesStatement.finalize();
+ }
+ this._dbConnection.commitTransaction();
+
+ Services.obs.notifyObservers(this, "addrbook-directory-invalidated");
+ } catch (ex) {
+ this._dbConnection.rollbackTransaction();
+ throw ex;
+ }
+ }
+
+ /* nsIAbDirectory */
+
+ get childCardCount() {
+ let countStatement = this._dbConnection.createStatement(
+ "SELECT COUNT(DISTINCT card) AS card_count FROM properties"
+ );
+ countStatement.executeStep();
+ let count = countStatement.row.card_count;
+ countStatement.finalize();
+ return count;
+ }
+ getCardFromProperty(property, value, caseSensitive) {
+ let sql = caseSensitive
+ ? "SELECT card FROM properties WHERE name = :name AND value = :value LIMIT 1"
+ : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value) LIMIT 1";
+ let selectStatement = this._dbConnection.createStatement(sql);
+ selectStatement.params.name = property;
+ selectStatement.params.value = value;
+ let result = null;
+ if (selectStatement.executeStep()) {
+ result = this.getCard(selectStatement.row.card);
+ }
+ selectStatement.finalize();
+ return result;
+ }
+ getCardsFromProperty(property, value, caseSensitive) {
+ let sql = caseSensitive
+ ? "SELECT card FROM properties WHERE name = :name AND value = :value"
+ : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value)";
+ let selectStatement = this._dbConnection.createStatement(sql);
+ selectStatement.params.name = property;
+ selectStatement.params.value = value;
+ let results = [];
+ while (selectStatement.executeStep()) {
+ results.push(this.getCard(selectStatement.row.card));
+ }
+ selectStatement.finalize();
+ return results;
+ }
+
+ static forFile(fileName) {
+ return directories.get(fileName);
+ }
+}
+SQLiteDirectory.prototype.classID = Components.ID(
+ "{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"
+);
diff --git a/comm/mailnews/addrbook/modules/VCardUtils.jsm b/comm/mailnews/addrbook/modules/VCardUtils.jsm
new file mode 100644
index 0000000000..a3ff0f5e14
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/VCardUtils.jsm
@@ -0,0 +1,973 @@
+/* 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 = [
+ "VCardService",
+ "VCardMimeConverter",
+ "VCardProperties",
+ "VCardPropertyEntry",
+ "VCardUtils",
+ "BANISHED_PROPERTIES",
+];
+
+const { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm",
+});
+
+/**
+ * Utilities for working with vCard data. This file uses ICAL.js as parser and
+ * formatter to avoid reinventing the wheel.
+ *
+ * @see RFC 6350.
+ */
+
+var VCardUtils = {
+ _decodeQuotedPrintable(value) {
+ let bytes = [];
+ for (let b = 0; b < value.length; b++) {
+ if (value[b] == "=") {
+ bytes.push(parseInt(value.substr(b + 1, 2), 16));
+ b += 2;
+ } else {
+ bytes.push(value.charCodeAt(b));
+ }
+ }
+ return new TextDecoder().decode(new Uint8Array(bytes));
+ },
+ _parse(vProps) {
+ let vPropMap = new Map();
+ for (let index = 0; index < vProps.length; index++) {
+ let { name, params, value } = vProps[index];
+
+ // Work out which type in typeMap, if any, this property belongs to.
+
+ // To make the next piece easier, the type param must always be an array
+ // of lower-case strings.
+ let type = params.type || [];
+ if (type) {
+ if (Array.isArray(type)) {
+ type = type.map(t => t.toLowerCase());
+ } else {
+ type = [type.toLowerCase()];
+ }
+ }
+
+ // Special cases for address and telephone types.
+ if (name == "adr") {
+ name = type.includes("home") ? "adr.home" : "adr.work";
+ }
+ if (name == "tel") {
+ name = "tel.work";
+ for (let t of type) {
+ if (["home", "work", "cell", "pager", "fax"].includes(t)) {
+ name = `tel.${t}`;
+ break;
+ }
+ }
+ }
+ // Preserve URL if no URL with type work is given take for `url.work` the URL without any type.
+ if (name == "url") {
+ name = type.includes("home") ? "url.home" : name;
+ name = type.includes("work") ? "url.work" : name;
+ }
+
+ // Special treatment for `url`, which is not in the typeMap.
+ if (!(name in typeMap) && name != "url") {
+ continue;
+ }
+
+ // The preference param is 1-100, lower numbers indicate higher
+ // preference. If not specified, the value is least preferred.
+ let pref = parseInt(params.pref, 10) || 101;
+
+ if (!vPropMap.has(name)) {
+ vPropMap.set(name, []);
+ }
+ vPropMap.get(name).push({ index, pref, value });
+ }
+
+ // If no URL with type is specified assume its the Work Web Page (WebPage 1).
+ if (vPropMap.has("url") && !vPropMap.has("url.work")) {
+ vPropMap.set("url.work", vPropMap.get("url"));
+ }
+ // AbCard only supports Work Web Page or Home Web Page. Get rid of the URL without type.
+ vPropMap.delete("url");
+
+ for (let props of vPropMap.values()) {
+ // Sort the properties by preference, or by the order they appeared.
+ props.sort((a, b) => {
+ if (a.pref == b.pref) {
+ return a.index - b.index;
+ }
+ return a.pref - b.pref;
+ });
+ }
+ return vPropMap;
+ },
+ /**
+ * ICAL.js's parser only supports vCard 3.0 and 4.0. To maintain
+ * interoperability with other applications, here we convert vCard 2.1
+ * cards into a "good-enough" mimic of vCard 4.0 so that the parser will
+ * read it without throwing an error.
+ *
+ * @param {string} vCard
+ * @returns {string}
+ */
+ translateVCard21(vCard) {
+ if (!/\bVERSION:2.1\b/i.test(vCard)) {
+ return vCard;
+ }
+
+ // Convert known type parameters to valid vCard 4.0, ignore unknown ones.
+ vCard = vCard.replace(/\n(([A-Z]+)(;[\w-]*)+):/gi, (match, key) => {
+ let parts = key.split(";");
+ let newParts = [parts[0]];
+ for (let i = 1; i < parts.length; i++) {
+ if (parts[i] == "") {
+ continue;
+ }
+ if (
+ ["HOME", "WORK", "FAX", "PAGER", "CELL"].includes(
+ parts[i].toUpperCase()
+ )
+ ) {
+ newParts.push(`TYPE=${parts[i]}`);
+ } else if (parts[i].toUpperCase() == "PREF") {
+ newParts.push("PREF=1");
+ } else if (parts[i].toUpperCase() == "QUOTED-PRINTABLE") {
+ newParts.push("ENCODING=QUOTED-PRINTABLE");
+ }
+ }
+ return "\n" + newParts.join(";") + ":";
+ });
+
+ // Join quoted-printable wrapped lines together. This regular expression
+ // only matches lines that are quoted-printable and end with `=`.
+ let quotedNewLineRegExp = /(;ENCODING=QUOTED-PRINTABLE[;:][^\r\n]*)=\r?\n/i;
+ while (vCard.match(quotedNewLineRegExp)) {
+ vCard = vCard.replace(quotedNewLineRegExp, "$1");
+ }
+
+ // Strip the version.
+ return vCard.replace(/(\r?\n)VERSION:2.1\r?\n/i, "$1");
+ },
+ /**
+ * Return a new AddrBookCard from the provided vCard string.
+ *
+ * @param {string} vCard - The vCard string.
+ * @param {string} [uid] - An optional UID to be used for the new card,
+ * overriding any UID specified in the vCard string.
+ * @returns {AddrBookCard}
+ */
+ vCardToAbCard(vCard, uid) {
+ vCard = this.translateVCard21(vCard);
+
+ let abCard = new lazy.AddrBookCard();
+ abCard.setProperty("_vCard", vCard);
+
+ let vCardUID = abCard.vCardProperties.getFirstValue("uid");
+ if (uid || vCardUID) {
+ abCard.UID = uid || vCardUID;
+ if (abCard.UID != vCardUID) {
+ abCard.vCardProperties.clearValues("uid");
+ abCard.vCardProperties.addValue("uid", abCard.UID);
+ }
+ }
+
+ return abCard;
+ },
+ abCardToVCard(abCard, version) {
+ if (abCard.supportsVCard && abCard.getProperty("_vCard")) {
+ return abCard.vCardProperties.toVCard();
+ }
+
+ // Collect all of the AB card properties into a Map.
+ let abProps = new Map(
+ Array.from(abCard.properties, p => [p.name, p.value])
+ );
+ abProps.set("UID", abCard.UID);
+
+ return this.propertyMapToVCard(abProps, version);
+ },
+ propertyMapToVCard(abProps, version = "4.0") {
+ let vProps = [["version", {}, "text", version]];
+
+ // Add the properties to the vCard.
+ for (let vPropName of Object.keys(typeMap)) {
+ for (let vProp of typeMap[vPropName].fromAbCard(abProps, vPropName)) {
+ if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") {
+ vProps.push(vProp);
+ }
+ }
+ }
+
+ // If there's only one address or telephone number, don't specify type.
+ let adrProps = vProps.filter(p => p[0] == "adr");
+ if (adrProps.length == 1) {
+ delete adrProps[0][1].type;
+ }
+ let telProps = vProps.filter(p => p[0] == "tel");
+ if (telProps.length == 1) {
+ delete telProps[0][1].type;
+ }
+
+ if (abProps.has("UID")) {
+ vProps.push(["uid", {}, "text", abProps.get("UID")]);
+ }
+ return ICAL.stringify(["vcard", vProps]);
+ },
+};
+
+function VCardService() {}
+VCardService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgVCardService"]),
+ classID: Components.ID("{e2e0f615-bc5a-4441-a16b-a26e75949376}"),
+
+ vCardToAbCard(vCard) {
+ return vCard ? VCardUtils.vCardToAbCard(vCard) : null;
+ },
+ escapedVCardToAbCard(vCard) {
+ return vCard ? VCardUtils.vCardToAbCard(decodeURIComponent(vCard)) : null;
+ },
+ abCardToEscapedVCard(abCard) {
+ return abCard ? encodeURIComponent(VCardUtils.abCardToVCard(abCard)) : null;
+ },
+};
+
+function VCardMimeConverter() {}
+VCardMimeConverter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsISimpleMimeConverter"]),
+ classID: Components.ID("{dafab386-bd4c-4238-bb48-228fbc98ba29}"),
+
+ mailChannel: null,
+ uri: null,
+ convertToHTML(contentType, data) {
+ function escapeHTML(template, ...parts) {
+ let arr = [];
+ for (let i = 0; i < parts.length; i++) {
+ arr.push(template[i]);
+ arr.push(
+ parts[i]
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ );
+ }
+ arr.push(template[template.length - 1]);
+ return arr.join("");
+ }
+
+ let abCard;
+ try {
+ abCard = VCardUtils.vCardToAbCard(data);
+ } catch (e) {
+ // We were given invalid vcard data.
+ return "";
+ }
+
+ let escapedVCard = encodeURIComponent(data);
+
+ let propertiesTable = `<table class="moz-vcard-properties-table">`;
+ propertiesTable += escapeHTML`<tr><td class="moz-vcard-title-property">${abCard.displayName}`;
+ if (abCard.primaryEmail) {
+ propertiesTable += escapeHTML`&nbsp;&lt;<a href="mailto:${abCard.primaryEmail}" private>${abCard.primaryEmail}</a>&gt;`;
+ }
+ propertiesTable += `</td></tr>`;
+ for (let propName of ["JobTitle", "Department", "Company"]) {
+ let propValue = abCard.getProperty(propName, "");
+ if (propValue) {
+ propertiesTable += escapeHTML`<tr><td class="moz-vcard-property">${propValue}</td></tr>`;
+ }
+ }
+ propertiesTable += `</table>`;
+
+ // VCardChild.jsm and VCardParent.jsm handle clicking on this link.
+ return `<html>
+ <body>
+ <table class="moz-vcard-table">
+ <tr>
+ <td valign="top"><a class="moz-vcard-badge" href="data:text/vcard,${escapedVCard}"></a></td>
+ <td>
+ ${propertiesTable}
+ </td>
+ </tr>
+ </table>
+ </body>
+ </html>`;
+ },
+};
+
+const BANISHED_PROPERTIES = [
+ "UID",
+ "PrimaryEmail",
+ "SecondEmail",
+ "DisplayName",
+ "NickName",
+ "Notes",
+ "Company",
+ "Department",
+ "JobTitle",
+ "BirthDay",
+ "BirthMonth",
+ "BirthYear",
+ "AnniversaryDay",
+ "AnniversaryMonth",
+ "AnniversaryYear",
+ "LastName",
+ "FirstName",
+ "AdditionalNames",
+ "NamePrefix",
+ "NameSuffix",
+ "HomePOBox",
+ "HomeAddress2",
+ "HomeAddress",
+ "HomeCity",
+ "HomeState",
+ "HomeZipCode",
+ "HomeCountry",
+ "WorkPOBox",
+ "WorkAddress2",
+ "WorkAddress",
+ "WorkCity",
+ "WorkState",
+ "WorkZipCode",
+ "WorkCountry",
+ "HomePhone",
+ "WorkPhone",
+ "FaxNumber",
+ "PagerNumber",
+ "CellularNumber",
+ "WebPage1",
+ "WebPage2",
+ "Custom1",
+ "Custom2",
+ "Custom3",
+ "Custom4",
+];
+
+/** Helper functions for typeMap. */
+
+function singleTextProperty(
+ abPropName,
+ vPropName,
+ vPropParams = {},
+ vPropType = "text"
+) {
+ return {
+ /**
+ * Formats nsIAbCard properties into an array for use by ICAL.js.
+ *
+ * @param {Map} map - A map of address book properties to map.
+ * @yields {Array} - Values in a jCard array for use with ICAL.js.
+ */
+ *fromAbCard(map) {
+ yield [vPropName, { ...vPropParams }, vPropType, map.get(abPropName)];
+ },
+ /**
+ * Parses a vCard value into properties usable by nsIAbCard.
+ *
+ * @param {string} value - vCard string to map to an address book card property.
+ * @yields {string[]} - Any number of key, value pairs to set on the nsIAbCard.
+ */
+ *toAbCard(value) {
+ if (typeof value != "string") {
+ console.warn(`Unexpected value for ${vPropName}: ${value}`);
+ return;
+ }
+ yield [abPropName, value];
+ },
+ };
+}
+function dateProperty(abCardPrefix, vPropName) {
+ return {
+ *fromAbCard(map) {
+ let year = map.get(`${abCardPrefix}Year`);
+ let month = map.get(`${abCardPrefix}Month`);
+ let day = map.get(`${abCardPrefix}Day`);
+
+ if (!year && !month && !day) {
+ return;
+ }
+
+ let dateValue = new ICAL.VCardTime({}, null, "date");
+ // Set the properties directly instead of using the VCardTime
+ // constructor argument, which causes null values to become 0.
+ dateValue.year = year ? Number(year) : null;
+ dateValue.month = month ? Number(month) : null;
+ dateValue.day = day ? Number(day) : null;
+
+ yield [vPropName, {}, "date", dateValue.toString()];
+ },
+ *toAbCard(value) {
+ try {
+ let dateValue = ICAL.VCardTime.fromDateAndOrTimeString(value);
+ yield [`${abCardPrefix}Year`, String(dateValue.year ?? "")];
+ yield [`${abCardPrefix}Month`, String(dateValue.month ?? "")];
+ yield [`${abCardPrefix}Day`, String(dateValue.day ?? "")];
+ } catch (ex) {
+ console.error(ex);
+ }
+ },
+ };
+}
+function multiTextProperty(abPropNames, vPropName, vPropParams = {}) {
+ return {
+ *fromAbCard(map) {
+ if (abPropNames.every(name => !map.has(name))) {
+ return;
+ }
+ let vPropValues = abPropNames.map(name => map.get(name) || "");
+ if (vPropValues.some(Boolean)) {
+ yield [vPropName, { ...vPropParams }, "text", vPropValues];
+ }
+ },
+ *toAbCard(value) {
+ if (Array.isArray(value)) {
+ for (let abPropName of abPropNames) {
+ let valuePart = value.shift();
+ if (abPropName && valuePart) {
+ yield [
+ abPropName,
+ Array.isArray(valuePart) ? valuePart.join(" ") : valuePart,
+ ];
+ }
+ }
+ } else if (typeof value == "string") {
+ // Only one value was given.
+ yield [abPropNames[0], value];
+ } else {
+ console.warn(`Unexpected value for ${vPropName}: ${value}`);
+ }
+ },
+ };
+}
+
+/**
+ * Properties we support for conversion between nsIAbCard and vCard.
+ *
+ * Keys correspond to vCard property keys, with the type appended where more
+ * than one type is supported (e.g. work and home).
+ *
+ * Values are objects with toAbCard and fromAbCard functions which convert
+ * property values in each direction. See the docs on the object returned by
+ * singleTextProperty.
+ */
+var typeMap = {
+ fn: singleTextProperty("DisplayName", "fn"),
+ email: {
+ *fromAbCard(map) {
+ yield ["email", { pref: "1" }, "text", map.get("PrimaryEmail")];
+ yield ["email", {}, "text", map.get("SecondEmail")];
+ },
+ toAbCard: singleTextProperty("PrimaryEmail", "email", { pref: "1" })
+ .toAbCard,
+ },
+ nickname: singleTextProperty("NickName", "nickname"),
+ note: singleTextProperty("Notes", "note"),
+ org: multiTextProperty(["Company", "Department"], "org"),
+ title: singleTextProperty("JobTitle", "title"),
+ bday: dateProperty("Birth", "bday"),
+ anniversary: dateProperty("Anniversary", "anniversary"),
+ n: multiTextProperty(
+ ["LastName", "FirstName", "AdditionalNames", "NamePrefix", "NameSuffix"],
+ "n"
+ ),
+ "adr.home": multiTextProperty(
+ [
+ "HomePOBox",
+ "HomeAddress2",
+ "HomeAddress",
+ "HomeCity",
+ "HomeState",
+ "HomeZipCode",
+ "HomeCountry",
+ ],
+ "adr",
+ { type: "home" }
+ ),
+ "adr.work": multiTextProperty(
+ [
+ "WorkPOBox",
+ "WorkAddress2",
+ "WorkAddress",
+ "WorkCity",
+ "WorkState",
+ "WorkZipCode",
+ "WorkCountry",
+ ],
+ "adr",
+ { type: "work" }
+ ),
+ "tel.home": singleTextProperty("HomePhone", "tel", { type: "home" }),
+ "tel.work": singleTextProperty("WorkPhone", "tel", { type: "work" }),
+ "tel.fax": singleTextProperty("FaxNumber", "tel", { type: "fax" }),
+ "tel.pager": singleTextProperty("PagerNumber", "tel", { type: "pager" }),
+ "tel.cell": singleTextProperty("CellularNumber", "tel", { type: "cell" }),
+ "url.work": singleTextProperty("WebPage1", "url", { type: "work" }, "url"),
+ "url.home": singleTextProperty("WebPage2", "url", { type: "home" }, "url"),
+ "x-custom1": singleTextProperty("Custom1", "x-custom1"),
+ "x-custom2": singleTextProperty("Custom2", "x-custom2"),
+ "x-custom3": singleTextProperty("Custom3", "x-custom3"),
+ "x-custom4": singleTextProperty("Custom4", "x-custom4"),
+};
+
+/**
+ * Any value that can be represented in a vCard. A value can be a boolean,
+ * number, string, or an array, depending on the data. A top-level array might
+ * contain primitives and/or second-level arrays of primitives.
+ *
+ * @see ICAL.design
+ * @see RFC6350
+ *
+ * @typedef {boolean|number|string|vCardValue[]} vCardValue
+ */
+
+/**
+ * Represents a single entry in a vCard ("contentline" in RFC6350 terms).
+ * The name, params, type and value are as returned by ICAL.
+ */
+class VCardPropertyEntry {
+ #name = null;
+ #params = null;
+ #type = null;
+ #value = null;
+ _original = null;
+
+ /**
+ * @param {string} name
+ * @param {object} params
+ * @param {string} type
+ * @param {vCardValue} value
+ */
+ constructor(name, params, type, value) {
+ this.#name = name;
+ this.#params = params;
+ this.#type = type;
+ if (params.encoding?.toUpperCase() == "QUOTED-PRINTABLE") {
+ if (Array.isArray(value)) {
+ value = value.map(VCardUtils._decodeQuotedPrintable);
+ } else {
+ value = VCardUtils._decodeQuotedPrintable(value);
+ }
+ delete params.encoding;
+ delete params.charset;
+ }
+ this.#value = value;
+ this._original = this;
+ }
+
+ /**
+ * @type {string}
+ */
+ get name() {
+ return this.#name;
+ }
+
+ /**
+ * @type {object}
+ */
+ get params() {
+ return this.#params;
+ }
+
+ /**
+ * @type {string}
+ */
+ get type() {
+ return this.#type;
+ }
+ set type(type) {
+ this.#type = type;
+ }
+
+ /**
+ * @type {vCardValue}
+ */
+ get value() {
+ return this.#value;
+ }
+ set value(value) {
+ this.#value = value;
+ }
+
+ /**
+ * Clone this object.
+ *
+ * @returns {VCardPropertyEntry}
+ */
+ clone() {
+ let cloneValue;
+ if (Array.isArray(this.#value)) {
+ cloneValue = this.#value.map(v => (Array.isArray(v) ? v.slice() : v));
+ } else {
+ cloneValue = this.#value;
+ }
+
+ let clone = new VCardPropertyEntry(
+ this.#name,
+ { ...this.#params },
+ this.#type,
+ cloneValue
+ );
+ clone._original = this;
+ return clone;
+ }
+
+ /**
+ * @param {VCardPropertyEntry} other
+ */
+ equals(other) {
+ if (other.constructor.name != "VCardPropertyEntry") {
+ return false;
+ }
+ return this._original == other._original;
+ }
+}
+
+/**
+ * Represents an entire vCard as a collection of `VCardPropertyEntry` objects.
+ */
+class VCardProperties {
+ /**
+ * All of the vCard entries in this object.
+ *
+ * @type {VCardPropertyEntry[]}
+ */
+ entries = [];
+
+ /**
+ * @param {?string} version - The version of vCard to use. Valid values are
+ * "3.0" and "4.0". If unspecified, vCard 3.0 will be used.
+ */
+ constructor(version) {
+ if (version) {
+ if (!["3.0", "4.0"].includes(version)) {
+ throw new Error(`Unsupported vCard version: ${version}`);
+ }
+ this.addEntry(new VCardPropertyEntry("version", {}, "text", version));
+ }
+ }
+
+ /**
+ * Parse a vCard into a VCardProperties object.
+ *
+ * @param {string} vCard
+ * @returns {VCardProperties}
+ */
+ static fromVCard(vCard, { isGoogleCardDAV = false } = {}) {
+ vCard = VCardUtils.translateVCard21(vCard);
+
+ let rv = new VCardProperties();
+ let [, properties] = ICAL.parse(vCard);
+ for (let property of properties) {
+ let [name, params, type, value] = property;
+ if (property.length > 4) {
+ // The jCal format stores multiple values as the 4th...nth items.
+ // VCardPropertyEntry has only one place for a value, so store an
+ // array instead. This applies to CATEGORIES and NICKNAME types in
+ // vCard 4 and also NOTE in vCard 3.
+ value = property.slice(3);
+ }
+ if (isGoogleCardDAV) {
+ // Google escapes the characters \r : , ; and \ unnecessarily, in
+ // violation of RFC6350. Removing the escaping at this point means no
+ // other code requires a special case for it.
+ if (Array.isArray(value)) {
+ value = value.map(v => v.replace(/\\r/g, "\r").replace(/\\:/g, ":"));
+ } else {
+ value = value.replace(/\\r/g, "\r").replace(/\\:/g, ":");
+ if (["phone-number", "uri"].includes(type)) {
+ value = value.replace(/\\([,;\\])/g, "$1");
+ }
+ }
+ }
+ rv.addEntry(new VCardPropertyEntry(name, params, type, value));
+ }
+ return rv;
+ }
+
+ /**
+ * Parse a Map of Address Book properties into a VCardProperties object.
+ *
+ * @param {Map<string, string>} propertyMap
+ * @param {string} [version="4.0"]
+ * @returns {VCardProperties}
+ */
+ static fromPropertyMap(propertyMap, version = "4.0") {
+ let rv = new VCardProperties(version);
+
+ for (let vPropName of Object.keys(typeMap)) {
+ for (let vProp of typeMap[vPropName].fromAbCard(propertyMap, vPropName)) {
+ if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") {
+ rv.addEntry(new VCardPropertyEntry(...vProp));
+ }
+ }
+ }
+
+ return rv;
+ }
+
+ /**
+ * Used to determine the default value type when adding values.
+ * Either `ICAL.design.vcard` for (vCard 4.0) or `ICAL.design.vcard3` (3.0).
+ *
+ * @type {ICAL.design.designSet}
+ */
+ designSet = ICAL.design.vcard3;
+
+ /**
+ * Add an entry to this object.
+ *
+ * @param {VCardPropertyEntry} entry - The entry to add.
+ * @returns {boolean} - If the entry was added.
+ */
+ addEntry(entry) {
+ if (entry.constructor.name != "VCardPropertyEntry") {
+ throw new Error("Not a VCardPropertyEntry");
+ }
+
+ if (this.entries.find(e => e.equals(entry))) {
+ return false;
+ }
+
+ if (entry.name == "version") {
+ if (entry.value == "3.0") {
+ this.designSet = ICAL.design.vcard3;
+ } else if (entry.value == "4.0") {
+ this.designSet = ICAL.design.vcard;
+ } else {
+ throw new Error(`Unsupported vCard version: ${entry.value}`);
+ }
+ // Version must be the first entry, so clear out any existing values
+ // and add it to the start of the collection.
+ this.clearValues("version");
+ this.entries.unshift(entry);
+ return true;
+ }
+
+ this.entries.push(entry);
+ return true;
+ }
+
+ /**
+ * Add an entry to this object by name and value.
+ *
+ * @param {string} name
+ * @param {string} value
+ * @returns {VCardPropertyEntry}
+ */
+ addValue(name, value) {
+ for (let entry of this.getAllEntries(name)) {
+ if (entry.value == value) {
+ return entry;
+ }
+ }
+
+ let newEntry = new VCardPropertyEntry(
+ name,
+ {},
+ this.designSet.property[name].defaultType,
+ value
+ );
+ this.entries.push(newEntry);
+ return newEntry;
+ }
+
+ /**
+ * Remove an entry from this object.
+ *
+ * @param {VCardPropertyEntry} entry - The entry to remove.
+ * @returns {boolean} - If an entry was found and removed.
+ */
+ removeEntry(entry) {
+ if (entry.constructor.name != "VCardPropertyEntry") {
+ throw new Error("Not a VCardPropertyEntry");
+ }
+
+ let index = this.entries.findIndex(e => e.equals(entry));
+ if (index >= 0) {
+ this.entries.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Remove entries from this object by name and value. All entries matching
+ * the name and value will be removed.
+ *
+ * @param {string} name
+ * @param {string} value
+ */
+ removeValue(name, value) {
+ for (let entry of this.getAllEntries(name)) {
+ if (entry.value == value) {
+ this.removeEntry(entry);
+ }
+ }
+ }
+
+ /**
+ * Remove entries from this object by name. All entries matching the name
+ * will be removed.
+ *
+ * @param {string} name
+ */
+ clearValues(name) {
+ for (let entry of this.getAllEntries(name)) {
+ this.removeEntry(entry);
+ }
+ }
+
+ /**
+ * Get the first value matching the given name, or null if no entry matches.
+ *
+ * @param {string} name
+ * @returns {?vCardValue}
+ */
+ getFirstValue(name) {
+ let entry = this.entries.find(e => e.name == name);
+ if (entry) {
+ return entry.value;
+ }
+ return null;
+ }
+
+ /**
+ * Get all values matching the given name.
+ *
+ * @param {string} name
+ * @returns {vCardValue[]}
+ */
+ getAllValues(name) {
+ return this.getAllEntries(name).map(e => e.value);
+ }
+
+ /**
+ * Get all values matching the given name, sorted in order of preference.
+ * Preference is determined by the `pref` parameter if it exists, then by
+ * the position in `entries`.
+ *
+ * @param {string} name
+ * @returns {vCardValue[]}
+ */
+ getAllValuesSorted(name) {
+ return this.getAllEntriesSorted(name).map(e => e.value);
+ }
+
+ /**
+ * Get the first entry matching the given name, or null if no entry matches.
+ *
+ * @param {string} name
+ * @returns {?VCardPropertyEntry}
+ */
+ getFirstEntry(name) {
+ return this.entries.find(e => e.name == name) ?? null;
+ }
+
+ /**
+ * Get all entries matching the given name.
+ *
+ * @param {string} name
+ * @returns {VCardPropertyEntry[]}
+ */
+ getAllEntries(name) {
+ return this.entries.filter(e => e.name == name);
+ }
+
+ /**
+ * Get all entries matching the given name, sorted in order of preference.
+ * Preference is determined by the `pref` parameter if it exists, then by
+ * the position in `entries`.
+ *
+ * @param {string} name
+ * @returns {VCardPropertyEntry[]}
+ */
+ getAllEntriesSorted(name) {
+ let nextPref = 101;
+ let entries = this.getAllEntries(name).map(e => {
+ return { entry: e, pref: e.params.pref || nextPref++ };
+ });
+ entries.sort((a, b) => a.pref - b.pref);
+ return entries.map(e => e.entry);
+ }
+
+ /**
+ * Get all entries matching the given group.
+ *
+ * @param {string} group
+ * @returns {VCardPropertyEntry[]}
+ */
+ getGroupedEntries(group) {
+ return this.entries.filter(e => e.params.group == group);
+ }
+
+ /**
+ * Clone this object.
+ *
+ * @returns {VCardProperties}
+ */
+ clone() {
+ let copy = new VCardProperties();
+ copy.entries = this.entries.map(e => e.clone());
+ return copy;
+ }
+
+ /**
+ * Get a Map of Address Book properties from this object.
+ *
+ * @returns {Map<string, string>} propertyMap
+ */
+ toPropertyMap() {
+ let vPropMap = VCardUtils._parse(this.entries.map(e => e.clone()));
+ let propertyMap = new Map();
+
+ for (let [name, props] of vPropMap) {
+ // Store the value(s) on the abCard.
+ for (let [abPropName, abPropValue] of typeMap[name].toAbCard(
+ props[0].value
+ )) {
+ if (abPropValue) {
+ propertyMap.set(abPropName, abPropValue);
+ }
+ }
+ // Special case for email, which can also have a second preference.
+ if (name == "email" && props.length > 1) {
+ propertyMap.set("SecondEmail", props[1].value);
+ }
+ }
+
+ return propertyMap;
+ }
+
+ /**
+ * Serialize this object into a vCard.
+ *
+ * @returns {string} vCard
+ */
+ toVCard() {
+ let jCal = this.entries.map(e => {
+ if (Array.isArray(e.value)) {
+ let design = this.designSet.property[e.name];
+ if (design.multiValue == "," && !design.structuredValue) {
+ // The jCal format stores multiple values as the 4th...nth items,
+ // but VCardPropertyEntry stores them as an array. This applies to
+ // CATEGORIES and NICKNAME types in vCard 4 and also NOTE in vCard 3.
+ return [e.name, e.params, e.type, ...e.value];
+ }
+ }
+ return [e.name, e.params, e.type, e.value];
+ });
+ return ICAL.stringify(["vcard", jCal]);
+ }
+}
diff --git a/comm/mailnews/addrbook/modules/components.conf b/comm/mailnews/addrbook/modules/components.conf
new file mode 100644
index 0000000000..137150d06f
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/components.conf
@@ -0,0 +1,136 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{e96ee804-0bd3-472f-81a6-8a9d65277ad3}",
+ "contract_ids": ["@mozilla.org/addressbook/directory;1?type=jsaddrbook"],
+ "jsm": "resource:///modules/SQLiteDirectory.jsm",
+ "constructor": "SQLiteDirectory",
+ },
+ {
+ "cid": "{1143991d-31cd-4ea6-9c97-c587d990d724}",
+ "contract_ids": ["@mozilla.org/addressbook/jsaddrbookcard;1"],
+ "jsm": "resource:///modules/AddrBookCard.jsm",
+ "constructor": "AddrBookCard",
+ },
+ {
+ "cid": "{224d3ef9-d81c-4d94-8826-a79a5835af93}",
+ "contract_ids": ["@mozilla.org/abmanager;1"],
+ "jsm": "resource:///modules/AddrBookManager.jsm",
+ "constructor": "AddrBookManager",
+ "name": "AbManager",
+ "interfaces": ["nsIAbManager"],
+ },
+ {
+ "cid": "{1fa9941a-07d5-4a6f-9673-15327fc2b9ab}",
+ "contract_ids": ["@mozilla.org/addressbook/directory;1?type=jscarddav"],
+ "jsm": "resource:///modules/CardDAVDirectory.jsm",
+ "constructor": "CardDAVDirectory",
+ },
+ {
+ "cid": "{e2e0f615-bc5a-4441-a16b-a26e75949376}",
+ "contract_ids": ["@mozilla.org/addressbook/msgvcardservice;1"],
+ "jsm": "resource:///modules/VCardUtils.jsm",
+ "constructor": "VCardService",
+ },
+ {
+ "cid": "{e9fb36ec-c980-4a77-9f68-0eb10491eda8}",
+ "contract_ids": ["@mozilla.org/mimecth;1?type=text/vcard"],
+ "jsm": "resource:///modules/VCardUtils.jsm",
+ "constructor": "VCardMimeConverter",
+ "categories": {"simple-mime-converters": "text/vcard"},
+ },
+ {
+ "cid": "{dafab386-bd4c-4238-bb48-228fbc98ba29}",
+ "contract_ids": ["@mozilla.org/mimecth;1?type=text/x-vcard"],
+ "jsm": "resource:///modules/VCardUtils.jsm",
+ "constructor": "VCardMimeConverter",
+ "categories": {"simple-mime-converters": "text/x-vcard"},
+ },
+ {
+ "cid": "{f87b71b5-2a0f-4b37-8e4f-3c899f6b8432}",
+ "contract_ids": ["@mozilla.org/network/ldap-connection;1"],
+ "jsm": "resource:///modules/LDAPConnection.jsm",
+ "constructor": "LDAPConnection",
+ },
+ {
+ "cid": "{a6f94ca4-cd2d-4983-bcf2-fe936190955c}",
+ "contract_ids": ["@mozilla.org/network/ldap-operation;1"],
+ "jsm": "resource:///modules/LDAPOperation.jsm",
+ "constructor": "LDAPOperation",
+ },
+ {
+ "cid": "{8683e821-f1b0-476d-ac15-07771c79bb11}",
+ "contract_ids": [
+ "@mozilla.org/addressbook/directory;1?type=moz-abldapdirectory"
+ ],
+ "jsm": "resource:///modules/LDAPDirectory.jsm",
+ "constructor": "LDAPDirectory",
+ },
+ {
+ "cid": "{5ad5d311-1a50-43db-a03c-63d45f443903}",
+ "contract_ids": ["@mozilla.org/addressbook/ldap-directory-query;1"],
+ "jsm": "resource:///modules/LDAPDirectoryQuery.jsm",
+ "constructor": "LDAPDirectoryQuery",
+ },
+ {
+ "cid": "{dbe204e8-ae09-11eb-b4c8-a7e4b3e6e82e}",
+ "contract_ids": ["@mozilla.org/addressbook/ldap-replication-service;1"],
+ "jsm": "resource:///modules/LDAPReplicationService.jsm",
+ "constructor": "LDAPReplicationService",
+ },
+ {
+ "cid": "{e8b59b32-f83f-4d5f-8eb5-e3c1e5de0d47}",
+ "contract_ids": ["@mozilla.org/network/ldap-service;1"],
+ "jsm": "resource:///modules/LDAPService.jsm",
+ "constructor": "LDAPService",
+ },
+ {
+ "cid": "{50ca73fa-7deb-42b9-9eec-e219e31e6d4b}",
+ "contract_ids": ["@mozilla.org/network/ldap-url-parser;1"],
+ "jsm": "resource:///modules/LDAPURLParser.jsm",
+ "constructor": "LDAPURLParser",
+ },
+ {
+ "cid": "{b3de9249-b0e5-4c12-8d91-c9a434fd80f5}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=ldap"],
+ "jsm": "resource:///modules/LDAPProtocolHandler.jsm",
+ "constructor": "LDAPProtocolHandler",
+ "protocol_config": {
+ "scheme": "ldap",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_DANGEROUS_TO_LOAD",
+ "ALLOWS_PROXY",
+ ],
+ "default_port": 389,
+ },
+ },
+ {
+ "cid": "{c85a5ef2-9c56-445f-b029-76889f2dd29b}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=ldaps"],
+ "jsm": "resource:///modules/LDAPProtocolHandler.jsm",
+ "constructor": "LDAPSProtocolHandler",
+ "protocol_config": {
+ "scheme": "ldaps",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_DANGEROUS_TO_LOAD",
+ "ALLOWS_PROXY",
+ ],
+ "default_port": 636,
+ },
+ },
+]
+
+if buildconfig.substs["MOZ_PREF_EXTENSIONS"]:
+ Classes += [
+ {
+ "cid": "{53d16809-1114-44e2-b585-41a2abb18f66}",
+ "contract_ids": ["@mozilla.org/ldapsyncquery;1"],
+ "jsm": "resource:///modules/LDAPSyncQuery.jsm",
+ "constructor": "LDAPSyncQuery",
+ },
+ ]
diff --git a/comm/mailnews/addrbook/modules/moz.build b/comm/mailnews/addrbook/modules/moz.build
new file mode 100644
index 0000000000..8b86aa9555
--- /dev/null
+++ b/comm/mailnews/addrbook/modules/moz.build
@@ -0,0 +1,34 @@
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "AddrBookCard.jsm",
+ "AddrBookDirectory.jsm",
+ "AddrBookMailingList.jsm",
+ "AddrBookManager.jsm",
+ "AddrBookUtils.jsm",
+ "CardDAVDirectory.jsm",
+ "CardDAVUtils.jsm",
+ "LDAPClient.jsm",
+ "LDAPConnection.jsm",
+ "LDAPDirectory.jsm",
+ "LDAPDirectoryQuery.jsm",
+ "LDAPListenerBase.jsm",
+ "LDAPMessage.jsm",
+ "LDAPOperation.jsm",
+ "LDAPProtocolHandler.jsm",
+ "LDAPReplicationService.jsm",
+ "LDAPService.jsm",
+ "LDAPURLParser.jsm",
+ "QueryStringToExpression.jsm",
+ "SQLiteDirectory.jsm",
+ "VCardUtils.jsm",
+]
+
+if CONFIG["MOZ_PREF_EXTENSIONS"]:
+ EXTRA_JS_MODULES += ["LDAPSyncQuery.jsm"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/addrbook/moz.build b/comm/mailnews/addrbook/moz.build
new file mode 100644
index 0000000000..ecf2c5590b
--- /dev/null
+++ b/comm/mailnews/addrbook/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "modules",
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/addrbook/prefs/content/pref-directory-add.js b/comm/mailnews/addrbook/prefs/content/pref-directory-add.js
new file mode 100644
index 0000000000..23bee12a0e
--- /dev/null
+++ b/comm/mailnews/addrbook/prefs/content/pref-directory-add.js
@@ -0,0 +1,454 @@
+/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { isLegalHostNameOrIP, cleanUpHostName } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+
+var gCurrentDirectory = null;
+var gReplicationBundle = null;
+var gReplicationService = Cc[
+ "@mozilla.org/addressbook/ldap-replication-service;1"
+].getService(Ci.nsIAbLDAPReplicationService);
+var gReplicationCancelled = false;
+var gProgressText;
+var gProgressMeter;
+var gDownloadInProgress = false;
+
+var kDefaultLDAPPort = 389;
+var kDefaultSecureLDAPPort = 636;
+
+window.addEventListener("DOMContentLoaded", Startup);
+window.addEventListener("unload", onUnload);
+document.addEventListener("dialogaccept", onAccept);
+document.addEventListener("dialogcancel", onCancel);
+
+var ldapOfflineObserver = {
+ observe(subject, topic, state) {
+ // sanity checks
+ if (topic != "network:offline-status-changed") {
+ return;
+ }
+ setDownloadOfflineOnlineState(state == "offline");
+ },
+};
+
+function Startup() {
+ gReplicationBundle = document.getElementById("bundle_replication");
+
+ document.getElementById("download").label =
+ gReplicationBundle.getString("downloadButton");
+ document.getElementById("download").accessKey = gReplicationBundle.getString(
+ "downloadButton.accesskey"
+ );
+
+ if (
+ "arguments" in window &&
+ window.arguments[0] &&
+ window.arguments[0].selectedDirectory
+ ) {
+ gCurrentDirectory = window.arguments[0].selectedDirectory;
+ try {
+ fillSettings();
+ } catch (ex) {
+ dump(
+ "pref-directory-add.js:Startup(): fillSettings() exception: " +
+ ex +
+ "\n"
+ );
+ }
+
+ let oldListName = gCurrentDirectory.dirName;
+ document.title = gReplicationBundle.getFormattedString(
+ "directoryTitleEdit",
+ [oldListName]
+ );
+
+ // Only set up the download button for online/offline status toggling
+ // if the pref isn't locked to disable the button.
+ if (
+ !Services.prefs.prefIsLocked(
+ gCurrentDirectory.dirPrefId + ".disable_button_download"
+ )
+ ) {
+ // Now connect to the offline/online observer
+ Services.obs.addObserver(
+ ldapOfflineObserver,
+ "network:offline-status-changed"
+ );
+
+ // Now set the initial offline/online state and update the state
+ setDownloadOfflineOnlineState(Services.io.offline);
+ }
+ } else {
+ document.title = gReplicationBundle.getString("directoryTitleNew");
+ fillDefaultSettings();
+ // Don't add observer here as it doesn't make any sense.
+ }
+}
+
+function onUnload() {
+ if (
+ "arguments" in window &&
+ window.arguments[0] &&
+ window.arguments[0].selectedDirectory &&
+ !Services.prefs.prefIsLocked(
+ gCurrentDirectory.dirPrefId + ".disable_button_download"
+ )
+ ) {
+ // Remove the observer that we put in on dialog startup
+ Services.obs.removeObserver(
+ ldapOfflineObserver,
+ "network:offline-status-changed"
+ );
+ }
+}
+
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ // start the spinning
+ gProgressMeter.removeAttribute("value");
+ gProgressText.value = gReplicationBundle.getString(
+ aStatus ? "replicationStarted" : "changesStarted"
+ );
+ gDownloadInProgress = true;
+ document.getElementById("download").label = gReplicationBundle.getString(
+ "cancelDownloadButton"
+ );
+ document.getElementById("download").accessKey =
+ gReplicationBundle.getString("cancelDownloadButton.accesskey");
+ }
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ EndDownload(aStatus);
+ }
+ },
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ gProgressText.value = gReplicationBundle.getFormattedString(
+ "currentCount",
+ [aCurSelfProgress]
+ );
+ },
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function DownloadNow() {
+ if (!gDownloadInProgress) {
+ gProgressText = document.getElementById("replicationProgressText");
+ gProgressMeter = document.getElementById("replicationProgressMeter");
+
+ gProgressText.hidden = false;
+ gProgressMeter.hidden = false;
+ gReplicationCancelled = false;
+
+ try {
+ if (gCurrentDirectory instanceof Ci.nsIAbLDAPDirectory) {
+ gReplicationService.startReplication(
+ gCurrentDirectory,
+ progressListener
+ );
+ } else {
+ EndDownload(Cr.NS_ERROR_FAILURE);
+ }
+ } catch (ex) {
+ EndDownload(Cr.NS_ERROR_FAILURE);
+ }
+ } else {
+ gReplicationCancelled = true;
+ try {
+ gReplicationService.cancelReplication(gCurrentDirectory);
+ } catch (ex) {
+ // XXX todo
+ // perhaps replication hasn't started yet? This can happen if you hit cancel after attempting to replication when offline
+ dump("unexpected failure while cancelling. ex=" + ex + "\n");
+ }
+ }
+}
+
+function EndDownload(aStatus) {
+ document.getElementById("download").label =
+ gReplicationBundle.getString("downloadButton");
+ document.getElementById("download").accessKey = gReplicationBundle.getString(
+ "downloadButton.accesskey"
+ );
+
+ // stop the spinning
+ gProgressMeter.value = 100;
+ gProgressMeter.hidden = true;
+
+ gDownloadInProgress = false;
+ if (Components.isSuccessCode(aStatus)) {
+ gProgressText.value = gReplicationBundle.getString("replicationSucceeded");
+ } else if (gReplicationCancelled) {
+ gProgressText.value = gReplicationBundle.getString("replicationCancelled");
+ } else {
+ gProgressText.value = gReplicationBundle.getString("replicationFailed");
+ }
+}
+
+// fill the settings panel with the data from the preferences.
+//
+function fillSettings() {
+ document.getElementById("description").value = gCurrentDirectory.dirName;
+
+ if (gCurrentDirectory instanceof Ci.nsIAbLDAPDirectory) {
+ var ldapUrl = gCurrentDirectory.lDAPURL;
+
+ document.getElementById("results").value = gCurrentDirectory.maxHits;
+ document.getElementById("login").value = gCurrentDirectory.authDn;
+ document.getElementById("hostname").value = ldapUrl.host;
+ document.getElementById("basedn").value = ldapUrl.dn;
+ document.getElementById("search").value = ldapUrl.filter;
+
+ var sub = document.getElementById("sub");
+ switch (ldapUrl.scope) {
+ case Ci.nsILDAPURL.SCOPE_ONELEVEL:
+ sub.radioGroup.selectedItem = document.getElementById("one");
+ break;
+ default:
+ sub.radioGroup.selectedItem = sub;
+ break;
+ }
+
+ var sasl = document.getElementById("saslMechanism");
+ switch (gCurrentDirectory.saslMechanism) {
+ case "GSSAPI":
+ sasl.selectedItem = document.getElementById("GSSAPI");
+ break;
+ default:
+ sasl.selectedItem = document.getElementById("Simple");
+ break;
+ }
+
+ var secure = ldapUrl.options & ldapUrl.OPT_SECURE;
+ if (secure) {
+ document.getElementById("secure").setAttribute("checked", "true");
+ }
+
+ if (ldapUrl.port == -1) {
+ document.getElementById("port").value = secure
+ ? kDefaultSecureLDAPPort
+ : kDefaultLDAPPort;
+ } else {
+ document.getElementById("port").value = ldapUrl.port;
+ }
+ }
+
+ // check if any of the preferences for this server are locked.
+ // If they are locked disable them
+ DisableUriFields(gCurrentDirectory.dirPrefId + ".uri");
+ DisableElementIfPrefIsLocked(
+ gCurrentDirectory.dirPrefId + ".description",
+ "description"
+ );
+ DisableElementIfPrefIsLocked(
+ gCurrentDirectory.dirPrefId + ".disable_button_download",
+ "download"
+ );
+ DisableElementIfPrefIsLocked(
+ gCurrentDirectory.dirPrefId + ".maxHits",
+ "results"
+ );
+ DisableElementIfPrefIsLocked(
+ gCurrentDirectory.dirPrefId + ".auth.dn",
+ "login"
+ );
+}
+
+function DisableElementIfPrefIsLocked(aPrefName, aElementId) {
+ if (Services.prefs.prefIsLocked(aPrefName)) {
+ document.getElementById(aElementId).setAttribute("disabled", true);
+ }
+}
+
+// disables all the text fields corresponding to the .uri pref.
+function DisableUriFields(aPrefName) {
+ if (Services.prefs.prefIsLocked(aPrefName)) {
+ let lockedElements = document.querySelectorAll('[disableiflocked="true"]');
+ for (let i = 0; i < lockedElements.length; i++) {
+ lockedElements[i].setAttribute("disabled", "true");
+ }
+ }
+}
+
+function onSecure() {
+ document.getElementById("port").value = document.getElementById("secure")
+ .checked
+ ? kDefaultSecureLDAPPort
+ : kDefaultLDAPPort;
+}
+
+function fillDefaultSettings() {
+ document.getElementById("port").value = kDefaultLDAPPort;
+ var sub = document.getElementById("sub");
+ sub.radioGroup.selectedItem = sub;
+
+ // Disable the download button and add some text indicating why.
+ document.getElementById("download").disabled = true;
+ document.getElementById("downloadWarningMsg").hidden = false;
+ document.getElementById("downloadWarningMsg").textContent = document
+ .getElementById("bundle_addressBook")
+ .getString("abReplicationSaveSettings");
+}
+
+function hasCharacters(number) {
+ var re = /[0-9]/g;
+ var num = number.match(re);
+ if (num && num.length == number.length) {
+ return false;
+ }
+ return true;
+}
+
+function onAccept(event) {
+ try {
+ let description = document.getElementById("description").value.trim();
+ let hostname = cleanUpHostName(document.getElementById("hostname").value);
+ let port = document.getElementById("port").value;
+ let secure = document.getElementById("secure");
+ let results = document.getElementById("results").value;
+ let errorValue = null;
+ let errorArg = null;
+ let saslMechanism = "";
+
+ let findDupeName = function (newName) {
+ // Do not allow an already existing name.
+ for (let ab of MailServices.ab.directories) {
+ if (
+ ab.dirName.toLowerCase() == newName.toLowerCase() &&
+ (!gCurrentDirectory || ab.URI != gCurrentDirectory.URI)
+ ) {
+ return ab.dirName;
+ }
+ }
+ return null;
+ };
+
+ if (!description) {
+ errorValue = "invalidName";
+ } else if ((errorArg = findDupeName(description))) {
+ errorValue = "duplicateNameText";
+ } else if (!isLegalHostNameOrIP(hostname)) {
+ errorValue = "invalidHostname";
+ } else if (port && hasCharacters(port)) {
+ // XXX write isValidDn and call it on the dn string here?
+ errorValue = "invalidPortNumber";
+ } else if (results && hasCharacters(results)) {
+ errorValue = "invalidResults";
+ }
+
+ if (!errorValue) {
+ if (!port) {
+ port = secure.checked ? kDefaultSecureLDAPPort : kDefaultLDAPPort;
+ }
+ if (hostname.includes(":")) {
+ // Wrap IPv6 address in [].
+ hostname = `[${hostname}]`;
+ }
+ let ldapUrl = Services.io
+ .newURI(`${secure.checked ? "ldaps" : "ldap"}://${hostname}:${port}`)
+ .QueryInterface(Ci.nsILDAPURL);
+
+ ldapUrl.dn = document.getElementById("basedn").value;
+ ldapUrl.scope = document.getElementById("one").selected
+ ? Ci.nsILDAPURL.SCOPE_ONELEVEL
+ : Ci.nsILDAPURL.SCOPE_SUBTREE;
+
+ ldapUrl.filter = document.getElementById("search").value;
+ if (document.getElementById("GSSAPI").selected) {
+ saslMechanism = "GSSAPI";
+ }
+
+ // check if we are modifying an existing directory or adding a new directory
+ if (gCurrentDirectory) {
+ gCurrentDirectory.dirName = description;
+ gCurrentDirectory.lDAPURL = ldapUrl;
+ window.opener.gNewServerString = gCurrentDirectory.dirPrefId;
+ } else {
+ // adding a new directory
+ window.opener.gNewServerString = MailServices.ab.newAddressBook(
+ description,
+ ldapUrl.spec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+ }
+
+ // XXX This is really annoying - both new/modify Address Book don't
+ // give us back the new directory we just created - so go find it from
+ // rdf so we can set a few final things up on it.
+ var targetURI = "moz-abldapdirectory://" + window.opener.gNewServerString;
+ var theDirectory = MailServices.ab
+ .getDirectory(targetURI)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ theDirectory.maxHits = results;
+ theDirectory.authDn = document.getElementById("login").value;
+ theDirectory.saslMechanism = saslMechanism;
+
+ window.opener.gNewServer = description;
+ // set window.opener.gUpdate to true so that LDAP Directory Servers
+ // dialog gets updated
+ window.opener.gUpdate = true;
+ window.arguments[0].newDirectoryUID = theDirectory.UID;
+ if ("onNewDirectory" in window.arguments[0]) {
+ window.arguments[0].onNewDirectory(theDirectory);
+ }
+ } else {
+ let addressBookBundle = document.getElementById("bundle_addressBook");
+
+ let errorText;
+ if (errorArg) {
+ errorText = addressBookBundle.getFormattedString(errorValue, [
+ errorArg,
+ ]);
+ } else {
+ errorText = addressBookBundle.getString(errorValue);
+ }
+
+ Services.prompt.alert(window, document.title, errorText);
+ event.preventDefault();
+ return;
+ }
+ } catch (outer) {
+ console.error(
+ "Internal error in pref-directory-add.js:onAccept() " + outer
+ );
+ }
+}
+
+function onCancel() {
+ window.opener.gUpdate = false;
+}
+
+// Sets the download button state for offline or online.
+// This function should only be called for ldap edit dialogs.
+function setDownloadOfflineOnlineState(isOffline) {
+ if (isOffline) {
+ // Disable the download button and add some text indicating why.
+ document.getElementById("downloadWarningMsg").textContent = document
+ .getElementById("bundle_addressBook")
+ .getString("abReplicationOfflineWarning");
+ }
+ document.getElementById("downloadWarningMsg").hidden = !isOffline;
+ document.getElementById("download").disabled = isOffline;
+}
diff --git a/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml b/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml
new file mode 100644
index 0000000000..ff2e40df1e
--- /dev/null
+++ b/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml
@@ -0,0 +1,190 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/pref-directory-add.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false">
+<head>
+ <title><!-- directoryTitleEdit --></title>
+ <style>
+ #directoryTabPanels radiogroup {
+ margin-inline-start: 4px;
+ }
+ #directoryTabPanels textarea {
+ width: calc(100% - 22px);
+ }
+ #directoryTabPanels menulist {
+ width: calc(100% - 4px);
+ margin-inline-start: 4px;
+ }
+ </style>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/addressbook/pref-directory-add.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="addDirectory" buttons="accept,cancel" style="width:100vw; min-width:&newDirectoryWidth;">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ <stringbundle id="bundle_replication" src="chrome://messenger/locale/addressbook/replicationProgress.properties"/>
+
+ <vbox id="editDirectory">
+
+ <tabbox style="margin:5px">
+ <tabs id="directoryTabBox">
+ <tab label="&General.tab;"/>
+ <tab label="&Offline.tab;"/>
+ <tab label="&Advanced.tab;"/>
+ </tabs>
+
+ <tabpanels id="directoryTabPanels" flex="1">
+ <vbox>
+ <div xmlns="http://www.w3.org/1999/xhtml" class="grid-three-column">
+ <div class="flex-items-center">
+ <xul:label id="descriptionLabel" value="&directoryName.label;"
+ accesskey="&directoryName.accesskey;"
+ control="description"/>
+ </div>
+ <div>
+ <input id="description" type="text" class="input-inline"
+ aria-labelledby="descriptionLabel"/>
+ </div>
+ <div></div>
+ <div class="flex-items-center">
+ <xul:label id="hostnameLabel"
+ value="&directoryHostname.label;"
+ accesskey="&directoryHostname.accesskey;"
+ control="hostname"/>
+ </div>
+ <div>
+ <input id="hostname" type="text"
+ class="uri-element input-inline"
+ aria-labelledby="descriptionLabel"
+ disableiflocked="true"/>
+ </div>
+ <div></div>
+ <div class="flex-items-center">
+ <xul:label id="basednLabel"
+ value="&directoryBaseDN.label;"
+ accesskey="&directoryBaseDN.accesskey;"
+ control="basedn"/>
+ </div>
+ <div>
+ <input id="basedn" type="text"
+ class="uri-element input-inline"
+ aria-labelledby="basednLabel"
+ disableiflocked="true"/>
+ </div>
+ <div class="flex-items-center flex-content-center">
+ <xul:button label="&findButton.label;"
+ accesskey="&findButton.accesskey;" disabled="true"/>
+ </div>
+ <div class="flex-items-center">
+ <xul:label id="portLabel" value="&portNumber.label;"
+ accesskey="&portNumber.accesskey;"
+ control="port"/>
+ </div>
+ <div>
+ <input id="port" type="number"
+ class="size5 input-inline"
+ min="1" max="65535"
+ aria-labelledby="portLabel"
+ disableiflocked="true"/>
+ </div>
+ <div></div>
+ <div class="flex-items-center">
+ <xul:label id="loginLabel" value="&directoryLogin.label;"
+ accesskey="&directoryLogin.accesskey;"
+ control="login"/>
+ </div>
+ <div>
+ <input id="login" type="text" class="uri-element input-inline"
+ aria-labelledby="loginLabel"/>
+ </div>
+ <div></div>
+ </div>
+ <separator/>
+ <checkbox id="secure" label="&directorySecure.label;"
+ accesskey="&directorySecure.accesskey;"
+ oncommand="onSecure();" disableiflocked="true"/>
+ </vbox>
+ <vbox>
+ <description>&offlineText.label;</description>
+ <separator/>
+ <hbox>
+ <button id="download" oncommand="DownloadNow();"/>
+ <spacer flex="1"/>
+ </hbox>
+ <description id="downloadWarningMsg" hidden="true" class="error"/>
+ <description id="replicationProgressText" hidden="true"/>
+
+ <html:progress id="replicationProgressMeter" value="0" max="100" hidden="hidden"/>
+ </vbox>
+ <vbox>
+ <div xmlns="http://www.w3.org/1999/xhtml" class="grid-two-column">
+ <div class="flex-items-center">
+ <xul:label id="returnMaxLabel" value="&return.label;"
+ accesskey="&return.accesskey;"
+ control="results"/>
+ </div>
+ <div class="flex-items-center">
+ <input id="results" type="number"
+ class="size5 input-inline"
+ min="1" max="2147483647" value="100"
+ aria-labelledby="returnMaxLabel"/>
+ <xul:label value="&results.label;"/>
+ </div>
+ <div class="flex-items-center">
+ <xul:label value="&scope.label;" control="scope"
+ accesskey="&scope.accesskey;"/>
+ </div>
+ <div>
+ <xul:radiogroup id="scope"
+ orient="horizontal">
+ <xul:radio id="one" value="1" label="&scopeOneLevel.label;"
+ disableiflocked="true" accesskey="&scopeOneLevel.accesskey;"/>
+ <xul:radio id="sub" value="2" label="&scopeSubtree.label;"
+ disableiflocked="true" accesskey="&scopeSubtree.accesskey;"/>
+ </xul:radiogroup>
+ </div>
+ <div class="flex-items-center">
+ <xul:label value="&searchFilter.label;"
+ accesskey="&searchFilter.accesskey;"
+ control="search"/>
+ </div>
+ <div>
+ <textarea id="search" disableiflocked="true"></textarea>
+ </div>
+ <div class="flex-items-center">
+ <xul:label value="&saslMechanism.label;" control="saslMechanism"
+ accesskey="&saslMechanism.accesskey;"/>
+ </div>
+ <div>
+ <xul:menulist id="saslMechanism">
+ <xul:menupopup>
+ <xul:menuitem id="Simple" value="" label="&saslOff.label;"
+ accesskey="&saslOff.accesskey;"/>
+ <xul:menuitem id="GSSAPI" value="GSSAPI" label="&saslGSSAPI.label;"
+ accesskey="&saslGSSAPI.accesskey;"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </div>
+ </div>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js
new file mode 100644
index 0000000000..2ba3421c5a
--- /dev/null
+++ b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js
@@ -0,0 +1,188 @@
+/* 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/. */
+
+/* import-globals-from ../../../../mail/components/addrbook/content/abCommon.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+window.addEventListener("DOMContentLoaded", onInitEditDirectories);
+
+// Listener to refresh the list items if something changes. In all these
+// cases we just rebuild the list as it is easier than searching/adding in the
+// correct places an would be an infrequent operation.
+var gAddressBookAbListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ init() {
+ for (let topic of [
+ "addrbook-directory-created",
+ "addrbook-directory-updated",
+ "addrbook-directory-deleted",
+ ]) {
+ Services.obs.addObserver(this, topic, true);
+ }
+ },
+
+ observe(subject, topic, data) {
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ fillDirectoryList(subject);
+ },
+};
+
+function onInitEditDirectories() {
+ // If the pref is locked disable the "Add" button
+ if (Services.prefs.prefIsLocked("ldap_2.disable_button_add")) {
+ document.getElementById("addButton").setAttribute("disabled", true);
+ }
+
+ // Fill out the directory list
+ fillDirectoryList();
+
+ // Add a listener so we can update correctly if the list should change
+ gAddressBookAbListener.init();
+}
+
+function fillDirectoryList(aItem = null) {
+ var abList = document.getElementById("directoriesList");
+
+ // Empty out anything in the list
+ while (abList.hasChildNodes()) {
+ abList.lastChild.remove();
+ }
+
+ // Init the address book list
+ let holdingArray = [];
+ for (let ab of MailServices.ab.directories) {
+ if (ab.isRemote) {
+ holdingArray.push(ab);
+ }
+ }
+
+ holdingArray.sort(function (a, b) {
+ return a.dirName.localeCompare(b.dirName);
+ });
+
+ holdingArray.forEach(function (ab) {
+ let item = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ label.setAttribute("value", ab.dirName);
+ item.appendChild(label);
+ item.setAttribute("value", ab.URI);
+
+ abList.appendChild(item);
+ });
+
+ // Forces the focus back on the list and on the first item.
+ // We also select an edited or recently added item.
+ abList.focus();
+ if (aItem) {
+ abList.selectedIndex = holdingArray.findIndex(d => {
+ return d && d.URI == aItem.URI;
+ });
+ }
+}
+
+function selectDirectory() {
+ var abList = document.getElementById("directoriesList");
+ var editButton = document.getElementById("editButton");
+ var removeButton = document.getElementById("removeButton");
+
+ if (abList && abList.selectedItem) {
+ editButton.removeAttribute("disabled");
+
+ // If the disable delete button pref for the selected directory is set,
+ // disable the delete button for that directory.
+ let ab = MailServices.ab.getDirectory(abList.value);
+ let disable = Services.prefs.getBoolPref(
+ ab.dirPrefId + ".disable_delete",
+ false
+ );
+ if (disable) {
+ removeButton.setAttribute("disabled", true);
+ } else {
+ removeButton.removeAttribute("disabled");
+ }
+ } else {
+ editButton.setAttribute("disabled", true);
+ removeButton.setAttribute("disabled", true);
+ }
+}
+
+function dblClickDirectory(event) {
+ // We only care about left click events.
+ if (event.button != 0) {
+ return;
+ }
+
+ editDirectory();
+}
+
+function addDirectory() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/addressbook/pref-directory-add.xhtml",
+ { features: "resizable=no" }
+ );
+}
+
+function editDirectory() {
+ var abList = document.getElementById("directoriesList");
+
+ if (abList && abList.selectedItem) {
+ let abURI = abList.value;
+ let ab = MailServices.ab.getDirectory(abURI);
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/addressbook/pref-directory-add.xhtml",
+ { features: "resizable=no" },
+ { selectedDirectory: ab }
+ );
+ }
+}
+
+async function removeDirectory() {
+ let abList = document.getElementById("directoriesList");
+
+ if (!abList.selectedItem) {
+ return;
+ }
+
+ let directory = GetDirectoryFromURI(abList.value);
+ if (
+ !directory ||
+ ["ldap_2.servers.history", "ldap_2.servers.pab"].includes(
+ directory.dirPrefId
+ )
+ ) {
+ return;
+ }
+
+ let action = "delete-book";
+ if (directory.isMailList) {
+ action = "delete-lists";
+ } else if (
+ [
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE,
+ ].includes(directory.dirType)
+ ) {
+ action = "remove-remote-book";
+ }
+
+ let [title, message] = await document.l10n.formatValues([
+ { id: `about-addressbook-confirm-${action}-title`, args: { count: 1 } },
+ {
+ id: `about-addressbook-confirm-${action}`,
+ args: { name: directory.dirName, count: 1 },
+ },
+ ]);
+
+ if (Services.prompt.confirm(window, title, message)) {
+ MailServices.ab.deleteAddressBook(directory.URI);
+ }
+}
diff --git a/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml
new file mode 100644
index 0000000000..381aee3050
--- /dev/null
+++ b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/pref-directory.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&pref.ldap.window.title;</title>
+ <link
+ rel="localization"
+ href="messenger/addressbook/aboutAddressBook.ftl"
+ />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abCommon.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/pref-editdirectories.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="editDirectories" buttons="accept">
+ <stringbundle
+ id="bundle_addressBook"
+ src="chrome://messenger/locale/addressbook/addressBook.properties"
+ />
+
+ <label
+ value="&directoriesText.label;"
+ accesskey="&directoriesText.accesskey;"
+ control="directoriesList"
+ />
+ <hbox flex="1">
+ <richlistbox
+ id="directoriesList"
+ flex="1"
+ onselect="selectDirectory();"
+ ondblclick="dblClickDirectory(event);"
+ />
+ <vbox>
+ <button
+ id="addButton"
+ label="&addDirectory.label;"
+ accesskey="&addDirectory.accesskey;"
+ oncommand="addDirectory();"
+ />
+ <button
+ id="editButton"
+ label="&editDirectory.label;"
+ accesskey="&editDirectory.accesskey;"
+ disabled="true"
+ oncommand="editDirectory();"
+ />
+ <button
+ id="removeButton"
+ label="&deleteDirectory.label;"
+ accesskey="&deleteDirectory.accesskey;"
+ disabled="true"
+ oncommand="removeDirectory();"
+ />
+ </vbox>
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/addrbook/public/moz.build b/comm/mailnews/addrbook/public/moz.build
new file mode 100644
index 0000000000..ece3c8d37b
--- /dev/null
+++ b/comm/mailnews/addrbook/public/moz.build
@@ -0,0 +1,48 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIAbAddressCollector.idl",
+ "nsIAbAutoCompleteResult.idl",
+ "nsIAbBooleanExpression.idl",
+ "nsIAbCard.idl",
+ "nsIAbDirectory.idl",
+ "nsIAbDirectoryQuery.idl",
+ "nsIAbDirectoryQueryProxy.idl",
+ "nsIAbDirSearchListener.idl",
+ "nsIAbLDAPAttributeMap.idl",
+ "nsIAbLDAPDirectory.idl",
+ "nsIAbLDAPReplicationData.idl",
+ "nsIAbLDAPReplicationQuery.idl",
+ "nsIAbLDAPReplicationService.idl",
+ "nsIAbLDIFService.idl",
+ "nsIAbManager.idl",
+ "nsILDAPBERElement.idl",
+ "nsILDAPBERValue.idl",
+ "nsILDAPConnection.idl",
+ "nsILDAPControl.idl",
+ "nsILDAPErrors.idl",
+ "nsILDAPMessage.idl",
+ "nsILDAPMessageListener.idl",
+ "nsILDAPModification.idl",
+ "nsILDAPOperation.idl",
+ "nsILDAPService.idl",
+ "nsILDAPURL.idl",
+ "nsIMsgVCardService.idl",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_MAPI_SUPPORT"]:
+ XPIDL_SOURCES += [
+ "nsIAbOutlookInterface.idl",
+ ]
+
+if CONFIG["MOZ_PREF_EXTENSIONS"]:
+ XPIDL_SOURCES += [
+ "nsILDAPSyncQuery.idl",
+ ]
+
+XPIDL_MODULE = "addrbook"
+
+EXPORTS += []
diff --git a/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl b/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl
new file mode 100644
index 0000000000..d1c90ca35d
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl
@@ -0,0 +1,46 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsIAbAddressCollector is the interface to the address collecter service.
+ * It will save and update the supplied addresses into the address book
+ * specified by the "mail.collect_addressbook" pref.
+ */
+[scriptable, uuid(069d3fba-37d4-4158-b401-a8efaeea0b66)]
+interface nsIAbAddressCollector : nsISupports {
+ /**
+ * Collects email addresses into the address book.
+ * If a card already exists for the email, the first/last/display names
+ * will be updated if they are supplied alongside the address.
+ * If a card does not exist for the email it will be created if aCreateCard
+ * is true.
+ *
+ * @param aAddresses The list of emails (in standard header format)
+ * to collect into the address book.
+ * @param aCreateCard Set to true if a card should be created if the
+ * email address doesn't exist.
+ */
+ void collectAddress(in AUTF8String aAddresses, in boolean aCreateCard);
+
+ /**
+ * Collects a single name and email address into the address book.
+ * By default, it saves the address without checking for an existing one.
+ * See collectAddress for the general implementation.
+ *
+ * @param aEmail The email address to collect.
+ * @param aDisplayName The display name associated with the email address.
+ * @param aCreateCard Set to true if a card should be created if the
+ * email address doesn't exist (ignored if
+ * aSkipCheckExisting is true).
+ * @param aSkipCheckExisting Optional parameter, if this is set then the
+ * implementation will skip checking for an
+ * existing card, and just create a new card.
+ */
+ void collectSingleAddress(in AUTF8String aEmail, in AUTF8String aDisplayName,
+ in boolean aCreateCard,
+ [optional] in boolean aSkipCheckExisting);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl b/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl
new file mode 100644
index 0000000000..ceeeeefe67
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIAutoCompleteResult.idl"
+
+interface nsIAbCard;
+interface nsIAbDirectory;
+
+/**
+ * This interface is used to extend the nsIAutoCompleteResult interface to
+ * provide extra facilities for obtaining more details of the results of
+ * an address book search.
+ */
+[scriptable, uuid(c0d35623-f719-4e43-ae24-573e393f87f9)]
+interface nsIAbAutoCompleteResult : nsIAutoCompleteResult {
+ /**
+ * Get the card from the result at the given index
+ */
+ nsIAbCard getCardAt(in long index);
+
+ /**
+ * Gets the email to use for the card within the result at the given index.
+ * This is the email that was matched against for the card where there are
+ * multiple email addresses on a card.
+ *
+ * @param index Index of the autocomplete result to return the value for.
+ * @result The email address to use from the card.
+ */
+ AString getEmailToUse(in long index);
+
+ /**
+ * Indicates whether the source that returned this result returned a
+ * complete result for the query. If true, refining the search will not
+ * trigger a new query, instead simply filtering the previous results.
+ * If false, the directory will be present in asyncDirectories.
+ */
+ bool isCompleteResult(in long index);
+
+ /**
+ * The template used to build the query for this search. Optional.
+ */
+ attribute AString modelQuery;
+
+ /**
+ * Asynchronous address books that were unable to return full results.
+ * This means that they need to be required rather than simply filtered.
+ */
+ attribute Array<nsIAbDirectory> asyncDirectories;
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl b/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl
new file mode 100644
index 0000000000..37ab350bbc
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+typedef long nsAbBooleanConditionType;
+
+/**
+ * Condition types
+ *
+ * Constants defining the types of condition
+ * to obtain a boolean result of TRUE or FALSE
+ *
+ */
+[scriptable, uuid(F51387B1-5AEF-4A1C-830E-7CD3B02366CE)]
+interface nsIAbBooleanConditionTypes : nsISupports
+{
+ const long Exists = 0;
+ const long DoesNotExist = 1;
+ const long Contains = 2;
+ const long DoesNotContain = 3;
+ const long Is = 4;
+ const long IsNot = 5;
+ const long BeginsWith = 6;
+ const long EndsWith = 7;
+ const long LessThan = 8;
+ const long GreaterThan = 9;
+ const long SoundsLike = 10;
+ const long RegExp = 11;
+};
+
+
+typedef long nsAbBooleanOperationType;
+
+/*
+ * Operation types
+ *
+ * Constants defining the boolean operation that
+ * should be performed between two boolean expressions
+ *
+ */
+[scriptable, uuid(9bdd2e51-2be4-49a4-a558-36d1a812231a)]
+interface nsIAbBooleanOperationTypes : nsISupports
+{
+ const long AND = 0;
+ const long OR = 1;
+ const long NOT = 2;
+};
+
+
+/**
+ * String condition
+ *
+ * A string condition represents a leaf node in a
+ * boolean expression tree and represents
+ * test which will return TRUE or FALSE
+ *
+ * Condition is an expression which is a
+ * leaf node in a boolean expression tree
+ *
+ */
+[scriptable, uuid(C3869D72-CFD0-45F0-A0EC-3F67D83C7110)]
+interface nsIAbBooleanConditionString : nsISupports
+{
+ /**
+ * The condition for how the a value
+ * should be compared
+ *
+ */
+ attribute nsAbBooleanConditionType condition;
+
+ /**
+ * The lhs of the condition
+ *
+ * Represents a property name which
+ * should be evaluated to obtain the
+ * lhs.
+ *
+ */
+ attribute string name;
+
+ /**
+ * The rhs of the condition
+ *
+ * <name> [condition] value
+ *
+ */
+ attribute wstring value;
+};
+
+/**
+ * N Boolean expression type
+ *
+ * Supports Unary Binary and N boolean expressions
+ *
+ * An operation represents a node in a boolean
+ * expression tree which may contain one or more
+ * child conditions or expressions
+ *
+ */
+[scriptable, uuid(223a9462-1aeb-4c1f-b069-5fc6278989b2)]
+interface nsIAbBooleanExpression: nsISupports
+{
+ /**
+ * The boolean operation to be applied to
+ * results of all evaluated expressions
+ *
+ */
+ attribute nsAbBooleanOperationType operation;
+
+ /**
+ * List of peer expressions
+ *
+ * e1 [op] e2 [op] .... en
+ *
+ */
+ attribute Array<nsISupports> expressions;
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbCard.idl b/comm/mailnews/addrbook/public/nsIAbCard.idl
new file mode 100644
index 0000000000..69acf7fa95
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbCard.idl
@@ -0,0 +1,402 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIProperty;
+interface nsIStringBundle;
+interface nsIVariant;
+
+[scriptable, uuid(97448252-F189-11d4-A422-001083003D0C)]
+interface nsIAbPreferMailFormat : nsISupports {
+ const unsigned long unknown = 0;
+ const unsigned long plaintext = 1;
+ const unsigned long html = 2;
+};
+
+/**
+ * An interface representing an address book card.
+ *
+ * None of these IDs will be reflected in the property collection. Neither
+ * nsIAbCard::properties, nsIAbCard::deleteProperty, nor any of the property
+ * getters and setters are able to interact with these properties.
+ *
+ * Fundamentally, a card is a collection of properties. Modifying a property in
+ * some way on a card does not change the backend used to store the card; the
+ * directory is required to do make the changes here.
+ *
+ * The following are the core properties that are used:
+ * - Names:
+ * - FirstName, LastName
+ * - PhoneticFirstName, PhoneticLastName
+ * - DisplayName, NickName
+ * - SpouseName, FamilyName
+ * - PrimaryEmail, SecondEmail
+ * - Home Contact:
+ * - HomeAddress, HomeAddress2, HomeCity, HomeState, HomeZipCode, HomeCountry
+ * - HomePhone, HomePhoneType
+ * - Work contact. Same as home, but with `Work' instead of `Home'
+ * - Other Contact:
+ * - FaxNumber, FaxNumberType
+ * - PagerNumber, PagerNumberType
+ * - CellularNumber, CellularNumberType
+ * - JobTitle, Department, Company
+ * - _AimScreenName
+ * - Dates:
+ * - AnniversaryYear, AnniversaryMonth, AnniversaryDay
+ * - BirthYear, BirthMonth, BirthDay
+ * - WebPage1 (work), WebPage2 (home)
+ * - Custom1, Custom2, Custom3, Custom4
+ * - Notes
+ * - Integral properties:
+ * - LastModifiedDate
+ * - PopularityIndex
+ * - Photo properties:
+ * - PhotoName
+ * - PhotoType
+ * - PhotoURI
+ *
+ * The contract id for the standard implementation is
+ * <tt>\@mozilla.org/addressbook/cardproperty;1</tt>.
+ */
+[scriptable, uuid(9bddf024-5178-4097-894e-d84b4ddde101)]
+interface nsIAbCard : nsISupports {
+ /**
+ * @{
+ * These constants reflect the possible values of the
+ * mail.addr_book.lastnamefirst preferences. They are intended to be used in
+ * generateName, defined below.
+ */
+ const unsigned long GENERATE_DISPLAY_NAME = 0;
+ const unsigned long GENERATE_LAST_FIRST_ORDER = 1;
+ const unsigned long GENERATE_FIRST_LAST_ORDER = 2;
+ /** @} */
+
+ /**
+ * Generate a name from the item for display purposes.
+ *
+ * If this item is an nsIAbCard, then it will use the aGenerateFormat option
+ * to determine the string to return.
+ * If this item is not an nsIAbCard, then the aGenerateFormat option may be
+ * ignored, and the displayName of the item returned.
+ *
+ * @param aGenerateFormat The format to generate as per the GENERATE_*
+ * constants above.
+ * @param aBundle An optional parameter that is a pointer to a string
+ * bundle that holds:
+ * chrome://messenger/locale/addressbook/addressBook.properties
+ * If this bundle is not supplied, then the function
+ * will obtain the bundle itself. If cached by the
+ * caller and supplied to this function, then
+ * performance will be improved over many calls.
+ * @return A string containing the generated name.
+ */
+ AString generateName(in long aGenerateFormat,
+ [optional] in nsIStringBundle aBundle);
+
+ /**
+ * The UID for the nsIAbDirectory containing this card.
+ *
+ * The directory considered to contain this card is the directory which
+ * produced this card (e.g., through nsIAbDirectory::getCardForProperty) or
+ * the last directory to modify this card, if another directory did so. If the
+ * last directory to modify this card deleted it, then this card is considered
+ * unassociated.
+ *
+ * If this card is not associated with a directory, this string will be empty.
+ *
+ * There is no standardized way to associate a card with multiple directories.
+ *
+ * Consumers of this interface outside of directory implementations SHOULD
+ * NOT, in general, modify this property.
+ */
+ attribute AUTF8String directoryUID;
+
+ /**
+ * A 128-bit unique identifier for this card. This can only be set if it is not
+ * already set. The getter sets a value if there is not one.
+ */
+ attribute AUTF8String UID;
+
+ /**
+ * A list of all the properties that this card has as an enumerator, whose
+ * members are all nsIProperty objects.
+ */
+ readonly attribute Array<nsIProperty> properties;
+
+ /**
+ * Returns a property for the given name.
+ *
+ * @param name The case-sensitive name of the property to get.
+ * @param defaultValue The value to return if the property does not exist.
+ * @exception NS_ERROR_NOT_AVAILABLE if the named property does not exist.
+ * @exception NS_ERROR_CANNOT_CONVERT_DATA if the property cannot be converted
+ * to the desired type.
+ */
+ nsIVariant getProperty(in AUTF8String name, in nsIVariant defaultValue);
+ /**
+ * @{
+ * Returns a property for the given name. Javascript callers should NOT use these,
+ * but use getProperty instead. XPConnect will do the type conversion automagically.
+ *
+ * These functions convert values in the same manner as the default
+ * implementation of nsIVariant. Of particular note is that boolean variables
+ * are converted to integers as in C/C++ (true is a non-zero value), so that
+ * false will be converted to a string of "0" and not "false."
+ *
+ *
+ * @param name The case-sensitive name of the property to get.
+ * @exception NS_ERROR_NOT_AVAILABLE if the named property does not exist.
+ * @exception NS_ERROR_CANNOT_CONVERT_DATA if the property cannot be converted
+ * to the desired type.
+ */
+ AString getPropertyAsAString(in string name);
+ AUTF8String getPropertyAsAUTF8String(in string name);
+ unsigned long getPropertyAsUint32(in string name);
+
+ /**
+ * Returns a property for the given name.
+ *
+ * @param name The case-sensitive name of the property to get.
+ * @param defaultValue The value to return if the property does not exist.
+ */
+ boolean getPropertyAsBool(in string name, in boolean defaultValue);
+
+ /** @} */
+
+ /**
+ * Assigns the given to value to the property of the given name.
+ *
+ * Should the property exist, its value will be overwritten. An
+ * implementation may impose additional semantic constraints for certain
+ * properties. However, such constraints might not be checked by this method.
+ *
+ * @warning A value MUST be convertible to a string; if this convention is not
+ * followed, consumers of cards may fail unpredictably or return incorrect
+ * results.
+ *
+ * @param name The case-sensitive name of the property to set.
+ * @param value The new value of the property.
+ */
+ void setProperty(in AUTF8String name, in nsIVariant value);
+
+ /**
+ * @{
+ * Sets a property for the given name. Javascript callers should NOT use these,
+ * but use setProperty instead. XPConnect will do the type conversion automagically.
+ *
+ * These functions convert values in the same manner as the default
+ * implementation of nsIVariant.
+ */
+ void setPropertyAsAString(in string name, in AString value);
+ void setPropertyAsAUTF8String(in string name, in AUTF8String value);
+ void setPropertyAsUint32(in string name, in unsigned long value);
+ void setPropertyAsBool(in string name, in boolean value);
+
+ /** @} */
+
+ /**
+ * Deletes the property with the given name.
+ *
+ * Some properties may not be deleted. However, the implementation will not
+ * check this constraint at this method. If such a property is deleted, an
+ * error may be thrown when the card is modified at the database level.
+ *
+ * @param name The case-sensitive name of the property to set.
+ */
+ void deleteProperty(in AUTF8String name);
+
+ /**
+ * Whether this card supports vCard properties. Currently only AddrBookCard
+ * supports vCard properties.
+ */
+ readonly attribute boolean supportsVCard;
+
+ /**
+ * A `VCardProperties` object for this card, or null. If `supportsVCard` is
+ * true, this attribute MUST be a `VCardProperties` object, otherwise it
+ * MUST be null.
+ *
+ * @see VCardProperties in VCardUtils.jsm
+ */
+ readonly attribute jsval vCardProperties;
+
+ /**
+ * @{
+ * These properties are shorthand for getProperty and setProperty.
+ */
+ attribute AString firstName;
+ attribute AString lastName;
+ attribute AString displayName;
+ attribute AString primaryEmail;
+ /** @} */
+
+ /**
+ * All email addresses associated with this card, in order of preference.
+ */
+ readonly attribute Array<AString> emailAddresses;
+
+ /**
+ * Determines whether or not a card has the supplied email address in either
+ * of its PrimaryEmail or SecondEmail attributes.
+ *
+ * Note: This function is likely to be temporary whilst we work out proper
+ * APIs for multi-valued attributes in bug 118665.
+ *
+ * @param aEmailAddress The email address to attempt to match against.
+ * @return True if aEmailAddress matches any of the email
+ * addresses stored in the card.
+ */
+ boolean hasEmailAddress(in AUTF8String aEmailAddress);
+
+ /**
+ * A URL to a photo for this card, or an empty string if there isn't one.
+ * This is probably a file: or data: URL but other schemes are possible.
+ */
+ readonly attribute AString photoURL;
+
+ /**
+ * Translates a card into a specific format.
+ * The following types are supported:
+ * - base64xml
+ * - xml
+ * - vcard
+ *
+ * @param aType The type of item to translate the card into.
+ * @return A string containing the translated card.
+ * @exception NS_ERROR_ILLEGAL_VALUE if we do not recognize the type.
+ */
+ AUTF8String translateTo(in AUTF8String aType);
+
+ /**
+ * Translates a card from the specified format
+ */
+ //void translateFrom(in AUTF8String aType, in AUTF8String aData);
+
+ /**
+ * Generate a phonetic name from the card, using the firstName and lastName
+ * values.
+ *
+ * @param aLastNameFirst Set to True to put the last name before the first.
+ * @return A string containing the generated phonetic name.
+ */
+ AString generatePhoneticName(in boolean aLastNameFirst);
+
+ /**
+ * Generate a chat name from the card, containing the value of the
+ * first non-empty chat field.
+ *
+ * @return A string containing the generated chat name.
+ */
+ AString generateChatName();
+
+ /**
+ * This function will copy all values from one card to another.
+ *
+ * @param srcCard The source card to copy values from.
+ */
+ void copy(in nsIAbCard aSrcCard);
+
+ /**
+ * Returns true if this card is equal to the other card.
+ *
+ * The default implementation defines equal as this card pointing to the
+ * same object as @arg aCard; another implementation defines it as equality of
+ * properties and values.
+ *
+ * @warning The exact nature of equality is still undefined, and actual
+ * results may not match theoretical results. Most notably, the code
+ * <tt>a.equals(b) == b.equals(a)</tt> might not return true. In
+ * particular, calling equals on cards from different address books
+ * may return inaccurate results.
+ *
+ *
+ * @return Equality, as defined above.
+ * @param aCard The card to compare against.
+ */
+ boolean equals(in nsIAbCard aCard);
+
+ // PROPERTIES TO BE DELETED AS PART OF REWRITE
+
+ attribute boolean isMailList;
+ /**
+ * If isMailList is true then mailListURI
+ * will contain the URI of the associated
+ * mail list
+ */
+ attribute string mailListURI;
+};
+
+%{C++
+// A nice list of properties for the benefit of C++ clients
+#define kUIDProperty "UID"
+#define kFirstNameProperty "FirstName"
+#define kLastNameProperty "LastName"
+#define kDisplayNameProperty "DisplayName"
+#define kNicknameProperty "NickName"
+#define kPriEmailProperty "PrimaryEmail"
+#define kLastModifiedDateProperty "LastModifiedDate"
+#define kPopularityIndexProperty "PopularityIndex"
+
+#define kPhoneticFirstNameProperty "PhoneticFirstName"
+#define kPhoneticLastNameProperty "PhoneticLastName"
+#define kSpouseNameProperty "SpouseName"
+#define kFamilyNameProperty "FamilyName"
+#define k2ndEmailProperty "SecondEmail"
+
+#define kHomeAddressProperty "HomeAddress"
+#define kHomeAddress2Property "HomeAddress2"
+#define kHomeCityProperty "HomeCity"
+#define kHomeStateProperty "HomeState"
+#define kHomeZipCodeProperty "HomeZipCode"
+#define kHomeCountryProperty "HomeCountry"
+#define kHomeWebPageProperty "WebPage2"
+
+#define kWorkAddressProperty "WorkAddress"
+#define kWorkAddress2Property "WorkAddress2"
+#define kWorkCityProperty "WorkCity"
+#define kWorkStateProperty "WorkState"
+#define kWorkZipCodeProperty "WorkZipCode"
+#define kWorkCountryProperty "WorkCountry"
+#define kWorkWebPageProperty "WebPage1"
+
+#define kHomePhoneProperty "HomePhone"
+#define kHomePhoneTypeProperty "HomePhoneType"
+#define kWorkPhoneProperty "WorkPhone"
+#define kWorkPhoneTypeProperty "WorkPhoneType"
+#define kFaxProperty "FaxNumber"
+#define kFaxTypeProperty "FaxNumberType"
+#define kPagerTypeProperty "PagerNumberType"
+#define kPagerProperty "PagerNumber"
+#define kCellularProperty "CellularNumber"
+#define kCellularTypeProperty "CellularNumberType"
+
+#define kJobTitleProperty "JobTitle"
+#define kDepartmentProperty "Department"
+#define kCompanyProperty "Company"
+#define kScreenNameProperty "_AimScreenName"
+#define kCustom1Property "Custom1"
+#define kCustom2Property "Custom2"
+#define kCustom3Property "Custom3"
+#define kCustom4Property "Custom4"
+#define kNotesProperty "Notes"
+
+#define kGtalkProperty "_GoogleTalk"
+#define kAIMProperty "_AimScreenName"
+#define kYahooProperty "_Yahoo"
+#define kSkypeProperty "_Skype"
+#define kQQProperty "_QQ"
+#define kMSNProperty "_MSN"
+#define kICQProperty "_ICQ"
+#define kXMPPProperty "_JabberId"
+#define kIRCProperty "_IRC"
+
+#define kAnniversaryYearProperty "AnniversaryYear"
+#define kAnniversaryMonthProperty "AnniversaryMonth"
+#define kAnniversaryDayProperty "AnniversaryDay"
+#define kBirthYearProperty "BirthYear"
+#define kBirthMonthProperty "BirthMonth"
+#define kBirthDayProperty "BirthDay"
+%}
diff --git a/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl b/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl
new file mode 100644
index 0000000000..cc78f1137f
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAbCard;
+interface nsITransportSecurityInfo;
+
+/**
+ * Listener callbacks for addressbook nsIAbDirectory searches and queries.
+ */
+[scriptable, uuid(eafe2488-4efb-4ac8-a6b4-7756eb1650a3)]
+interface nsIAbDirSearchListener : nsISupports {
+ /**
+ * Invoked for each matching result found by the search.
+ *
+ * @param aCard A matching addressbook card.
+ */
+ void onSearchFoundCard(in nsIAbCard aCard);
+
+ /**
+ * Invoked when the search finishes.
+ *
+ * @param status The result of the search. NS_OK means the search
+ * completed without error. NS_ERROR_ABORT means the user
+ * stopped the search. But there are many other error codes
+ * which may be seen here (LDAP or NSS errors, for example).
+ * @param complete Whether this search returned all possible results.
+ * @param secInfo If status is an NSS error code, the securityInfo of the
+ * failing operation is passed out here. This can be used
+ * to obtain a failing certificate, to present the user an
+ * option to add it as a security exception (handy for
+ * LDAP servers with self-signed certs).
+ * @param location If status is an NSS error code, this holds the location
+ * of the failed operation ("<host>:<port>").
+ */
+ void onSearchFinished(in nsresult status, in bool complete, in nsITransportSecurityInfo secInfo, in ACString location);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbDirectory.idl b/comm/mailnews/addrbook/public/nsIAbDirectory.idl
new file mode 100644
index 0000000000..07eab7aa81
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbDirectory.idl
@@ -0,0 +1,371 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAbCard;
+interface nsIAbDirSearchListener;
+
+/* moz-abdirectory:// is the URI to access nsAbBSDirectory,
+ * which is the root directory for all types of address books
+ * this is used to get all address book directories. */
+
+%{C++
+#define kAllDirectoryRoot "moz-abdirectory://"
+
+#define kPersonalAddressbook "abook.sqlite"
+#define kPersonalAddressbookUri "jsaddrbook://abook.sqlite"
+#define kCollectedAddressbook "history.sqlite"
+#define kCollectedAddressbookUri "jsaddrbook://history.sqlite"
+
+#define kABFileName_PreviousSuffix ".na2" /* final v2 address book format */
+#define kABFileName_PreviousSuffixLen 4
+#define kABFileName_CurrentSuffix ".mab" /* v3 address book extension */
+
+#define kJSDirectoryRoot "jsaddrbook://"
+#define kJSAddressBook "abook.sqlite"
+%}
+
+/**
+ * A top-level address book directory.
+ *
+ * Please note that in order to be properly instantiated by nsIAbManager, every
+ * type of nsIAbDirectory must have a contract ID of the form:
+ *
+ * @mozilla.org/addressbook/directory;1?type=<AB URI Scheme>
+ *
+ * Where AB URI Scheme does not include the ://. For example, for the
+ * SQLite-based address book, the scheme is "jsaddrbook", so the contract ID for
+ * the SQLite-based address book type is:
+ *
+ * @mozilla.org/addressbook/directory;1?type=jsaddrbook
+ */
+[scriptable, uuid(72dc868b-db5b-4daa-b6c6-071be4a05d02)]
+interface nsIAbDirectory : nsISupports {
+ /**
+ * Returns true if this collection is read-only.
+ */
+ readonly attribute boolean readOnly;
+
+ /**
+ * Returns true if this collection is accessed over a network connection.
+ */
+ readonly attribute boolean isRemote;
+
+ /**
+ * Returns true if this collection is accessed over a secure connection.
+ *
+ * If isRemote returns false, then this value MUST be false as well.
+ */
+ readonly attribute boolean isSecure;
+
+ /**
+ * Returns an address book card for the specified email address if found.
+ *
+ * If there are multiple cards with the given email address, this method will
+ * return one of these cards in an implementation-defined manner.
+ *
+ * Matching is performed in a case-insensitive manner.
+ *
+ * This method performs a synchronous operation. If the collection cannot do
+ * the search in such a manner, then it should throw NS_ERROR_NOT_IMPLEMENTED.
+ *
+ * @param emailAddress The email address to find in any of the email address
+ * fields. If emailAddress is empty, the database won't
+ * be searched and the function will return as if no card
+ * was found.
+ * @return An nsIAbCard if one was found, else returns NULL.
+ * @exception NS_ERROR_NOT_IMPLEMENTED If the collection cannot do this.
+ */
+ nsIAbCard cardForEmailAddress(in AUTF8String emailAddress);
+
+ /**
+ * Returns an address book card for the specified property if found.
+ *
+ * If there are multiple cards with the given value for the property, this
+ * method will return one of these cards in an implementation-defined manner.
+ *
+ * This method performs a synchronous operation. If the collection cannot do
+ * the search in such a manner, then it should throw NS_ERROR_NOT_IMPLEMENTED.
+ *
+ * If the property is not natively a string, it can still be searched for
+ * using the string-encoded value of the property, e.g. "0". See
+ * nsIAbCard::getPropertyAsAUTF8String for more information. Empty values will
+ * return no match, to prevent spurious results.
+ *
+ * @param aProperty The property to look for.
+ * @param aValue The value to search for.
+ * @param aCaseSensitive True if matching should be done case-sensitively.
+ * @result An nsIAbCard if one was found, else returns NULL.
+ * @exception NS_ERROR_NOT_IMPLEMENTED If the collection cannot do this.
+ */
+ nsIAbCard getCardFromProperty(in string aProperty, in AUTF8String aValue,
+ in boolean aCaseSensitive);
+
+ /**
+ * Returns all address book cards with a specific property matching value
+ *
+ * This function is almost identical to getCardFromProperty, with the
+ * exception of returning all cards rather than just the first.
+ *
+ * @param aProperty The property to look for.
+ * @param aValue The value to search for.
+ * @param aCaseSensitive True if matching should be done case-sensitively.
+ * @result The matching nsIAbCard instances.
+ */
+ Array<nsIAbCard> getCardsFromProperty(in string aProperty,
+ in AUTF8String aValue,
+ in boolean aCaseSensitive);
+
+ /**
+ * Returns the nsIAbDirectory for a mailing list with the specified name.
+ */
+ nsIAbDirectory getMailListFromName(in AString aName);
+
+ /**
+ * The chrome URI to use for bringing up a dialog to edit this directory.
+ * When opening the dialog, use a JS argument of
+ * {selectedDirectory: thisdir} where thisdir is this directory that you just
+ * got the chrome URI from.
+ */
+ readonly attribute ACString propertiesChromeURI;
+
+ /**
+ * The description of the directory. If this directory is not a mailing list,
+ * then setting this attribute will send round a "DirName" update via
+ * nsIAddrBookSession.
+ */
+ attribute AString dirName;
+
+ // XXX This should really be replaced by a QI or something better
+ readonly attribute long dirType;
+
+ // The filename for address books within this directory.
+ readonly attribute ACString fileName;
+
+ /**
+ * A 128-bit unique identifier for this directory.
+ */
+ readonly attribute AUTF8String UID;
+ [noscript] void setUID(in AUTF8String aUID);
+
+ // The URI of the address book
+ readonly attribute ACString URI;
+
+ // The position of the directory on the display.
+ readonly attribute long position;
+
+ // will be used for LDAP replication
+ attribute unsigned long lastModifiedDate;
+
+ // Defines whether this directory is a mail
+ // list or not
+ attribute boolean isMailList;
+
+ // Get the children directories
+ readonly attribute Array<nsIAbDirectory> childNodes;
+
+ /**
+ * Get the count of cards associated with the directory. This includes the
+ * cards associated with the mailing lists too.
+ */
+ readonly attribute unsigned long childCardCount;
+
+ /**
+ * Get the cards associated with the directory. This will return the cards
+ * associated with the mailing lists too.
+ */
+ readonly attribute Array<nsIAbCard> childCards;
+
+ /**
+ * Searches the directory for cards matching query.
+ *
+ * The query takes the form:
+ * (BOOL1(FIELD1,OP1,VALUE1)..(FIELDn,OPn,VALUEn)(BOOL2(FIELD1,OP1,VALUE1)...)...)
+ *
+ * BOOLn A boolean operator joining subsequent terms delimited by ().
+ * For possible values see CreateBooleanExpression().
+ * FIELDn An addressbook card data field.
+ * OPn An operator for the search term.
+ * For possible values see CreateBooleanConditionString().
+ * VALUEn The value to be matched in the FIELDn via the OPn operator.
+ * The value must be URL encoded by the caller, if it contains any
+ * special characters including '(' and ')'.
+ */
+ void search(in AString query, in AString searchString, in nsIAbDirSearchListener listener);
+
+ /**
+ * Initializes a directory, pointing to a particular URI.
+ */
+ void init(in string aURI);
+
+ /**
+ * Clean up any database connections or open file handles.
+ * Called at shutdown or if the directory is about to be deleted.
+ */
+ [implicit_jscontext]
+ Promise cleanUp();
+
+ // Deletes either a mailing list or a top
+ // level directory, which also updates the
+ // preferences
+ void deleteDirectory(in nsIAbDirectory directory);
+
+ // Check if directory contains card
+ // If the implementation is asynchronous the card
+ // may not yet have arrived. If it is in the process
+ // of obtaining cards the method will throw an
+ // NS_ERROR_NOT_AVAILABLE exception if the card
+ // cannot be found.
+ boolean hasCard(in nsIAbCard cards);
+
+ // Check if directory contains directory
+ boolean hasDirectory(in nsIAbDirectory dir);
+
+ // Check if directory contains a mailinglist by name
+ boolean hasMailListWithName(in AString aName);
+
+ /**
+ * Adds a card to the database.
+ *
+ * This card does not need to be of the same type as the database, e.g., one
+ * can add an nsIAbLDAPCard to an nsIAbMDBDirectory.
+ *
+ * @return "Real" card (eg nsIAbLDAPCard) that can be used for some
+ * extra functions.
+ */
+ nsIAbCard addCard(in nsIAbCard card);
+
+ /**
+ * Modifies a card in the database to match that supplied.
+ */
+ void modifyCard(in nsIAbCard modifiedCard);
+
+ /**
+ * Deletes the array of cards from the database.
+ *
+ * @param aCards The cards to delete from the database.
+ */
+ void deleteCards(in Array<nsIAbCard> aCards);
+
+ void dropCard(in nsIAbCard card, in boolean needToCopyCard);
+
+ /**
+ * Whether or not the directory should be searched when doing autocomplete,
+ * (currently by using GetChildCards); LDAP does not support this in online
+ * mode, so that should return false; additionally any other directory types
+ * that also do not support GetChildCards should return false.
+ *
+ * @param aIdentity An optional parameter detailing the identity key (see
+ * nsIMsgAccountManager) that this autocomplete is being
+ * run against.
+ * @return True if this directory should/can be used during
+ * local autocomplete.
+ */
+ boolean useForAutocomplete(in ACString aIdentityKey);
+
+ /**
+ * Does this directory support mailing lists? Note that in the case
+ * this directory is a mailing list and nested mailing lists are not
+ * supported, this will return false rather than true which the parent
+ * directory might.
+ */
+ readonly attribute boolean supportsMailingLists;
+
+ // Specific to a directory which stores mail lists
+
+ /**
+ * Creates a new mailing list in the directory. Currently only supported
+ * for top-level directories.
+ *
+ * @param list The new mailing list to add.
+ * @return The mailing list directory added, which may have been modified.
+ */
+ nsIAbDirectory addMailList(in nsIAbDirectory list);
+
+ /**
+ * Nick Name of the mailing list. This attribute is only really used when
+ * the nsIAbDirectory represents a mailing list.
+ */
+ attribute AString listNickName;
+
+ /**
+ * Description of the mailing list. This attribute is only really used when
+ * the nsIAbDirectory represents a mailing list.
+ */
+ attribute AString description;
+
+ /**
+ * Edits an existing mailing list (specified as listCard) into its parent
+ * directory. You should call this function on the resource with the same
+ * uri as the listCard.
+ *
+ * @param listCard A nsIAbCard version of the mailing list with the new
+ * values.
+ */
+ void editMailListToDatabase(in nsIAbCard listCard);
+
+ // Copies mail list properties from the srcList
+ void copyMailList(in nsIAbDirectory srcList);
+
+ /**
+ * The id of the directory used in prefs e.g. "ldap_2.servers.pab"
+ */
+ readonly attribute ACString dirPrefId;
+
+ /**
+ * @name getXXXValue
+ *
+ * Helper functions to get different types of pref, but return a default
+ * value if a pref value was not obtained.
+ *
+ * @param aName The name of the pref within the branch dirPrefId to
+ * get a value from.
+ *
+ * @param aDefaultValue The default value to return if getting the pref fails
+ * or the pref is not present.
+ *
+ * @return The value of the pref or the default value.
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED if the pref branch couldn't
+ * be obtained (e.g. dirPrefId isn't set).
+ */
+ //@{
+ long getIntValue(in string aName, in long aDefaultValue);
+ boolean getBoolValue(in string aName, in boolean aDefaultValue);
+ ACString getStringValue(in string aName, in ACString aDefaultValue);
+ AUTF8String getLocalizedStringValue(in string aName, in AUTF8String aDefaultValue);
+ //@}
+
+ /**
+ * The following attributes are read from an nsIAbDirectory via the above methods:
+ *
+ * HidesRecipients (Boolean)
+ * If true, and this nsIAbDirectory is a mailing list, then when sending mail to
+ * this list, recipients addresses will be hidden from one another by sending
+ * via BCC.
+ */
+
+ /**
+ * @name setXXXValue
+ *
+ * Helper functions to set different types of pref values.
+ *
+ * @param aName The name of the pref within the branch dirPrefId to
+ * get a value from.
+ *
+ * @param aValue The value to set the pref to.
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED if the pref branch couldn't
+ * be obtained (e.g. dirPrefId isn't set).
+ */
+ //@{
+ void setIntValue(in string aName, in long aValue);
+ void setBoolValue(in string aName, in boolean aValue);
+ void setStringValue(in string aName, in ACString aValue);
+ void setLocalizedStringValue(in string aName, in AUTF8String aValue);
+ //@}
+
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl b/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl
new file mode 100644
index 0000000000..e21bca5c05
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIAbDirSearchListener;
+interface nsIAbCard;
+interface nsIAbDirectory;
+
+/**
+ * The arguments for a query.
+ *
+ * Contains an expression for perform matches
+ * and an array of properties which should be
+ * returned if a match is found from the expression
+ *
+ */
+[scriptable, uuid(03af3018-2590-4f4c-a88c-1fff6595ef05)]
+interface nsIAbDirectoryQueryArguments : nsISupports
+{
+ /**
+ * Defines the boolean expression for
+ * the matching of cards
+ *
+ */
+ attribute nsISupports expression;
+
+ /**
+ * Defines if sub directories should be
+ * queried
+ *
+ */
+ attribute boolean querySubDirectories;
+
+ /**
+ * A parameter which can be used to pass in data specific to a particular
+ * type of addressbook.
+ */
+ attribute nsISupports typeSpecificArg;
+
+ /**
+ * A custom search filter which user wants to use in LDAP query.
+ */
+ attribute AUTF8String filter;
+};
+
+
+[scriptable, uuid(3A6E0C0C-1DD2-11B2-B23D-EA3A8CCB333C)]
+interface nsIAbDirectoryQueryPropertyValue : nsISupports
+{
+ /**
+ * The property which should be matched
+ *
+ * For example 'primaryEmail' or 'homePhone'
+ * for card properties.
+ *
+ * Two further properties are defined that
+ * do not exist as properties on a card.
+ *
+ * 'card:nsIAbCard' which represents the interface
+ * of a card component
+ *
+ */
+ readonly attribute string name;
+
+ /**
+ * The value of the property
+ *
+ */
+ readonly attribute wstring value;
+
+ /**
+ * The value of the property
+ * as an interface
+ *
+ * Only valid if the corresponding
+ * property name is related to an
+ * interface instead of a wstring
+ *
+ */
+ readonly attribute nsISupports valueISupports;
+};
+
+[scriptable, uuid(60b5961c-ce61-47b3-aa99-6d865f734dee)]
+interface nsIAbDirectoryQuery : nsISupports
+{
+ /**
+ * Initiates a query on a directory and sub-directories for properties
+ * on cards
+ *
+ * @param aDirectory A directory that the query may get extra details
+ * from.
+ *
+ * @param aArguments The properties and values to match value could of
+ * type nsIAbDirectoryQueryMatchItem for matches other
+ * than ?contains?
+ *
+ * @param aListener The listener which will obtain individual query
+ * results.
+ *
+ * @param aResultLimit Limits the number of results returned to a maximum
+ * value.
+ *
+ * @param aTimeOut The maximum length of time for the query
+ *
+ * @return A context id for the query
+ */
+ long doQuery(in nsIAbDirectory aDirectory,
+ in nsIAbDirectoryQueryArguments aArguments,
+ in nsIAbDirSearchListener aListener,
+ in long aResultLimit,
+ in long aTimeOut);
+
+ /**
+ * Stops an existing query operation if
+ * query operation is asynchronous
+ *
+ * The nsIAbDirSearchListener will
+ * be notified when query has stopped
+ *
+ * It is implementation specific if notification
+ * synchronous or asynchronous
+ *
+ * @param contextID
+ * The unique number returned from
+ * the doQuery methods
+ *
+ */
+ void stopQuery(in long contextID);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl b/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl
new file mode 100644
index 0000000000..ced61ff8f4
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+
+#include "nsIAbDirectoryQuery.idl"
+
+[scriptable, uuid(b8034849-1e98-4d39-819c-15ba61a7434f)]
+interface nsIAbDirectoryQueryProxy : nsIAbDirectoryQuery
+{
+ void initiate();
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl b/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl
new file mode 100644
index 0000000000..c5bf6c565c
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl
@@ -0,0 +1,192 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPMessage;
+interface nsIAbCard;
+
+/**
+ * A mapping between addressbook properties and ldap attributes.
+ *
+ * Each addressbook property can map to one or more attributes. If
+ * there is no entry in preferences for a field, the getters generally
+ * return null; empty strings are passed through as usual. The intent is
+ * that properties with a non-zero number of attributes can be overridden for
+ * a specific server by supplying a zero-length string. For this to work,
+ * most callers are likely to want to check for both success and a
+ * non-empty string.
+ *
+ * Note that the one exception to this pattern is getAttributes, which
+ * throws NS_ERROR_FAILURE for non-existent property entries, since
+ * XPConnect doesn't like returning null arrays.
+ *
+ * Note that each LDAP attribute can map to at most one addressbook
+ * property. The checkState method is a useful tool in enforcing
+ * this. Failure to enforce it may make it impossible to guarantee
+ * that getProperty will do something consistent and reasonable.
+ *
+ * Maybe someday once we support ldap autoconfig stuff (ie
+ * draft-joslin-config-schema-11.txt), we can simplify this and other
+ * code and only allow a property to map to a single attribute.
+ */
+[scriptable, uuid(fa019fd1-7f3d-417a-8957-154cca0240be)]
+interface nsIAbLDAPAttributeMap : nsISupports
+{
+ /**
+ * Get all the LDAP attributes associated with a given property
+ * name, in order of precedence (highest to lowest).
+ *
+ * @param aProperty the address book property to return attrs for
+ *
+ * @return a comma-separated list of attributes, null if no entry is
+ * present
+ */
+ ACString getAttributeList(in ACString aProperty);
+
+ /**
+ * Get all the LDAP attributes associated with a given property name, in
+ * order of precedence (highest to lowest).
+ *
+ * @param aProperty the address book property to return attrs for
+ *
+ * @return an array of attributes
+ *
+ * @exception NS_ERROR_FAILURE if there is no entry for this property
+ */
+ Array<ACString> getAttributes(in ACString aProperty);
+
+ /**
+ * Get the first (canonical) LDAP attribute associated with a given property
+ * name
+ *
+ * @param aProperty the address book property to return attrs for
+ *
+ * @return the first attribute associated with a given property,
+ * null if there is no entry for this property
+ */
+ ACString getFirstAttribute(in ACString aProperty);
+
+ /**
+ * Set an existing mapping to the comma-separated list of attributes.
+ *
+ * @param aProperty the mozilla addressbook property name
+ *
+ * @param aAttributeList a comma-separated list of attributes in
+ * order of precedence from high to low
+ *
+ * @param aAllowInconsistencies allow changes that would result in
+ * a map with an LDAP attribute associated
+ * with more than one property. Useful for
+ * doing a bunch of sets at once, and
+ * calling checkState at the end.
+ *
+ * @exception NS_ERROR_FAILURE making this change would result in a map
+ * with an LDAP attribute pointing to more
+ * than one property
+ */
+ void setAttributeList(in ACString aProperty, in ACString aAttributeList,
+ in boolean allowInconsistencies);
+
+ /**
+ * Find the Mozilla addressbook property name that this attribute should
+ * map to.
+ *
+ * @return the addressbook property name, null if it's not used in the map
+ */
+ ACString getProperty(in ACString aAttribute);
+
+ /**
+ * Get all attributes that may be used in an addressbook card via this
+ * property map (used for passing to to an LDAP search when you want
+ * everything that could be in a card returned).
+ *
+ * @return a comma-separated list of attribute names
+ *
+ * @exception NS_ERROR_FAILURE there are no attributes in this property map
+ */
+ ACString getAllCardAttributes();
+
+ /**
+ * Get all properties that may be used in an addressbook card via this
+ * property map.
+ *
+ * @return an array of properties
+ *
+ * @exception NS_ERROR_FAILURE there are no attributes in this property map
+ */
+ Array<ACString> getAllCardProperties();
+
+ /**
+ * Check that no LDAP attributes are listed in more than one property.
+ *
+ * @exception NS_ERROR_FAILURE one or more LDAP attributes are listed
+ * multiple times. The object is now in an
+ * inconsistent state, and should be either
+ * manually repaired or discarded.
+ */
+ void checkState();
+
+ /* These last two methods are really just for the convenience of the caller
+ * and to avoid tons of unnecessary crossing of the XPConnect boundary.
+ */
+
+ /**
+ * Set any attributes specified in the given prefbranch on this object.
+ *
+ * @param aPrefBranchName the pref branch containing all the
+ * property names
+ *
+ * @exception NS_ERROR_FAILURE one or more LDAP attributes are listed
+ * multiple times. The object is now in an
+ * inconsistent state, and should be either
+ * manually repaired or discarded.
+ */
+ void setFromPrefs(in ACString aPrefBranchName);
+
+ /**
+ * Set the properties on an addressbook card from the given LDAP message
+ * using the map in this object.
+ *
+ * @param aCard is the card object whose values are to be set
+ * @param aMessage is the LDAP message to get the values from
+ *
+ * @exception NS_ERROR_FAILURE is thrown if no addressbook properties
+ * are found in the message
+ */
+ void setCardPropertiesFromLDAPMessage(in nsILDAPMessage aMessage,
+ in nsIAbCard aCard);
+};
+
+/**
+ * The nsIAbLDAPAttributeMapService is used to build and hold a cache
+ * of maps.
+ */
+[scriptable, uuid(12e2d589-3c2a-48e4-8c82-b1e6464a0dfd)]
+interface nsIAbLDAPAttributeMapService : nsISupports
+{
+ /**
+ * Accessor to construct or return a cached copy of the attribute
+ * map for a given preference branch. The map is constructed by
+ * first taking the default map (as specified by the
+ * "ldap_2.servers.default.attrmap" prefbranch), and then having any
+ * preferences specified by aPrefBranchName override the defaults.
+ * LDIF import and export code should use the default map.
+ *
+ * @return the requested map
+ *
+ * @exception NS_ERROR_FAILURE error constructing the map;
+ * possibly because of a failure
+ * from checkState()
+ */
+ nsIAbLDAPAttributeMap getMapForPrefBranch(in ACString aPrefBranchName);
+};
+
+
+%{C++
+// test whether one of the getters has actually found an attribute
+#define ATTRMAP_FOUND_ATTR(rv, str) (NS_SUCCEEDED(rv) && !(str).IsEmpty())
+%}
diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl b/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl
new file mode 100644
index 0000000000..8d84bf688e
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIAbLDAPAttributeMap;
+interface nsILDAPURL;
+
+%{C++
+#define kLDAPDirectoryRoot "moz-abldapdirectory://"
+#define kLDAPDirectoryRootLen 22
+%}
+
+/**
+ * XXX This should really inherit from nsIAbDirectory, and some day it will.
+ * But for now, doing that complicates implementation.
+ */
+[scriptable, uuid(90dde295-e354-4d58-Add8-f9b29a95942d)]
+interface nsIAbLDAPDirectory : nsISupports
+{
+ /**
+ * The Replication File Name to use.
+ */
+ attribute ACString replicationFileName;
+
+ /**
+ * The version of LDAP protocol in use.
+ */
+ attribute unsigned long protocolVersion;
+
+ /**
+ * The SASL mechanism to use to authenticate to the LDAP server
+ * If this is an empty string, then a simple bind will be performed
+ * A non-zero string is assumed to be the name of the SASL mechanism.
+ * Currently the only supported mechanism is GSSAPI
+ */
+ attribute ACString saslMechanism;
+
+ /**
+ * The AuthDN to use to access the server.
+ */
+ attribute AUTF8String authDn;
+
+ /**
+ * The maximum number of matches that the server will return per a search.
+ */
+ attribute long maxHits;
+
+ /**
+ * The Last Change Number used for replication.
+ */
+ attribute long lastChangeNumber;
+
+ /**
+ * The LDAP server's scoping of the lastChangeNumber.
+ */
+ attribute ACString dataVersion;
+
+ /**
+ * The attribute map that is associated with this directory's server.
+ */
+ readonly attribute nsIAbLDAPAttributeMap attributeMap;
+
+ /**
+ * The LDAP URL for this directory. Note that this differs from
+ * nsIAbDirectory::URI. This attribute will give you a true ldap
+ * url, e.g. ldap://localhost:389/ whereas the uri will give you the
+ * directories rdf uri, e.g. moz-abldapdirectory://<pref base name>/.
+ */
+ attribute nsILDAPURL lDAPURL;
+
+ /**
+ * The replication (offline) file that this database uses.
+ */
+ readonly attribute nsIFile replicationFile;
+
+ /**
+ * The LDAP attributes used to build the Relative Distinguished Name
+ * of new cards, in the form of a comma separated list.
+ *
+ * The default is to use the common name (cn) attribute.
+ */
+ attribute ACString rdnAttributes;
+
+ /**
+ * The LDAP objectClass values added to cards when they are created/added,
+ * in the form of a comma separated list.
+ *
+ * The default is to use the following classes:
+ * top,person,organizationalPerson,inetOrgPerson,mozillaAbPersonAlpha
+ */
+ attribute ACString objectClasses;
+
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl
new file mode 100644
index 0000000000..c42ffc9f4d
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAbLDAPDirectory;
+interface nsILDAPConnection;
+interface nsILDAPURL;
+interface nsIAbLDAPReplicationQuery;
+interface nsIWebProgressListener;
+
+/**
+ * this service does replication of an LDAP directory to a local AB Database.
+ */
+[scriptable, uuid(e628bbc9-8793-4f0b-bce4-990d399b1fca)]
+interface nsIAbLDAPProcessReplicationData : nsISupports
+{
+ /**
+ * readonly attribute giving the current replication state
+ */
+ readonly attribute int32_t replicationState;
+
+ /**
+ * replication states
+ */
+ const long kIdle = 0;
+ const long kAnonymousBinding = 1;
+ const long kAuthenticatedBinding = 2;
+ const long kSyncServerBinding = 3;
+ const long kSearchingAuthDN = 4;
+ const long kDecidingProtocol = 5;
+ const long kAuthenticating = 6;
+ const long kReplicatingAll = 7;
+ const long kSearchingRootDSE = 8;
+ const long kFindingChanges = 9;
+ const long kReplicatingChanges = 10;
+ const long kReplicationDone = 11;
+
+ /**
+ * this method initializes the implementation
+ */
+ void init(in nsIAbLDAPDirectory directory,
+ in nsILDAPConnection connection,
+ in nsILDAPURL url,
+ in nsIAbLDAPReplicationQuery query,
+ in nsIWebProgressListener progressListener);
+
+ /**
+ * this method a aborts the ongoing processing
+ */
+ void abort();
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl
new file mode 100644
index 0000000000..aa9db9ab61
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIWebProgressListener;
+interface nsIAbLDAPDirectory;
+
+/**
+ * this interface provides methods to perform LDAP Replication Queries
+ */
+[scriptable, uuid(460a739c-a8c1-4f24-b705-c89d136ab9f5)]
+interface nsIAbLDAPReplicationQuery : nsISupports
+{
+ /**
+ * initialize for the query
+ */
+ void init(in nsIAbLDAPDirectory aDirectory,
+ in nsIWebProgressListener aProgressListener);
+
+ /**
+ * Starts an LDAP query to do replication as needed
+ */
+ void doReplicationQuery();
+
+ /**
+ * Cancels the currently executing query
+ */
+ void cancelQuery();
+
+ /**
+ * this method is the callback when query is done, failed or successful
+ */
+ void done(in boolean aSuccess);
+};
+
+// XXX This interface currently isn't implemented as it didn't work.
+// Bug 311632 should fix it
+[scriptable, uuid(126202D1-4460-11d6-B7C2-00B0D06E5F27)]
+interface nsIAbLDAPChangeLogQuery : nsISupports
+{
+ /**
+ * Starts an LDAP query to find auth DN
+ */
+ void queryAuthDN(in AUTF8String aValueUsedToFindDn);
+
+ /**
+ * Starts an LDAP query to search server's Root DSE
+ */
+ void queryRootDSE();
+
+ /**
+ * Starts an LDAP ChangeLog query to find changelog entries
+ */
+ void queryChangeLog(in AUTF8String aChangeLogDN, in int32_t aLastChangeNo);
+
+ /**
+ * Starts an LDAP query to find changed entries
+ */
+ void queryChangedEntries(in AUTF8String aChangedEntryDN);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl
new file mode 100644
index 0000000000..010c73d956
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl
@@ -0,0 +1,31 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIWebProgressListener;
+interface nsIAbLDAPDirectory;
+
+/**
+ * this service does replication of an LDAP directory to a local AB Database.
+ */
+[scriptable, uuid(3f499c70-5ceb-4b91-8b7f-62c366859383)]
+interface nsIAbLDAPReplicationService: nsISupports {
+
+ /**
+ * Start Replication of given LDAP directory represented by the URI
+ */
+ void startReplication(in nsIAbLDAPDirectory aDirectory,
+ in nsIWebProgressListener progressListener);
+
+ /**
+ * Cancel Replication of given LDAP directory represented by the URI
+ */
+ void cancelReplication(in nsIAbLDAPDirectory aDirectory);
+
+ /**
+ * callback when replication is done, failure or success
+ */
+ void done(in boolean aSuccess);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbLDIFService.idl b/comm/mailnews/addrbook/public/nsIAbLDIFService.idl
new file mode 100644
index 0000000000..6a32c458da
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbLDIFService.idl
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIAbDirectory;
+
+[scriptable, uuid(7afaa95f-0b1c-4d8a-a65f-bb5073ed6d39)]
+interface nsIAbLDIFService : nsISupports {
+
+ /**
+ * Determine if a file is likely to be an LDIF file based on field
+ * names that commonly appear in LDIF files.
+ *
+ * @param aSrc The file to examine
+ *
+ * @return true if the file appears to be of LDIF type,
+ * false otherwise
+ */
+ boolean isLDIFFile(in nsIFile aSrc);
+
+ /**
+ * Imports a file into the specified address book.
+ *
+ * @param aDirectory The address book to import addresses into.
+ *
+ * @param aSrc The file to import addresses from.
+ *
+ * @param aStoreLocAsHome Stores the address as a home rather than work
+ * address.
+ *
+ * @param aProgress May be null, but if a pointer is supplied,
+ * then it will be updated regularly with the
+ * current position of reading from the file.
+ *
+ */
+ void importLDIFFile(in nsIAbDirectory aDirectory,
+ in nsIFile aSrc,
+ in boolean aStoreLocAsHome,
+ inout unsigned long aProgress);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbManager.idl b/comm/mailnews/addrbook/public/nsIAbManager.idl
new file mode 100644
index 0000000000..366866d05b
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbManager.idl
@@ -0,0 +1,132 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIAbDirectory;
+interface nsIAbCard;
+interface nsIFile;
+interface nsIAbBooleanExpression;
+
+/**
+ * nsIAbManager is an interface to the main address book manager
+ * via the contract id "@mozilla.org/abmanager;1"
+ *
+ * It contains the main functions to create and delete address books as well
+ * as some helper functions.
+ */
+[scriptable, uuid(ea0d8b3d-a549-4874-82d8-3a82cee2a3f1)]
+interface nsIAbManager : nsISupports
+{
+ const unsigned long LDAP_DIRECTORY_TYPE = 0;
+ const unsigned long MAPI_DIRECTORY_TYPE = 3;
+ const unsigned long JS_DIRECTORY_TYPE = 101;
+ const unsigned long CARDDAV_DIRECTORY_TYPE = 102;
+ const unsigned long ASYNC_DIRECTORY_TYPE = 103;
+
+ /**
+ * Returns an array containing all the top-level directories.
+ */
+ readonly attribute Array<nsIAbDirectory> directories;
+
+ /**
+ * Returns the directory that represents the supplied URI.
+ *
+ * @param aURI The URI of the address book to find.
+ * @return The found address book.
+ */
+ nsIAbDirectory getDirectory(in ACString aURI);
+
+ /**
+ * Returns the directory that has the supplied dirPrefId.
+ *
+ * @param aDirPrefId The dirPrefId of the directory.
+ * @return The found AB directory.
+ */
+ nsIAbDirectory getDirectoryFromId(in ACString aDirPrefId);
+
+ /**
+ * Returns the directory that has the supplied UID.
+ *
+ * @param aUID The UID of the directory.
+ * @return The found AB directory.
+ */
+ nsIAbDirectory getDirectoryFromUID(in ACString aUID);
+
+ /**
+ * Creates a new address book.
+ *
+ * @param aDirName The description of the address book.
+ * @param aURI The URI for the address book. This is specific to each
+ * type of address book.
+ * @param aType One of the *_DIRECTORY_TYPE constants.
+ * @param aUID Sets the UID of the new Address Book.
+ */
+ ACString newAddressBook(in AString aDirName, in ACString aURI,
+ in unsigned long aType,
+ [optional] in AUTF8String aUID);
+
+ /**
+ * Adds a previously created address book object. If it has not been removed
+ * (using `deleteAddressBook`) it will be removed at the end of the session.
+ *
+ * @param aDir The address book object.
+ */
+ void addAddressBook(in nsIAbDirectory aDir);
+
+ /**
+ * Deletes an address book.
+ *
+ * @param aURI The URI for the address book. This is specific to each
+ * type of address book.
+ */
+ void deleteAddressBook(in ACString aURI);
+
+ /**
+ * Finds out if the mailing list name exists in any address book.
+ *
+ * @param aName The name of the list to try and find.
+ *
+ * @return True if the name exists.
+ */
+ boolean mailListNameExists(in AString name);
+
+ /**
+ * Finds out if the directory name already exists.
+ *
+ * @param aName The name of a directory to check for.
+ *
+ * @return True if a directory called name already exists.
+ */
+ boolean directoryNameExists(in AString name);
+
+ /**
+ * Returns an address book card for the specified email address if found, in
+ * any directory. The first matching card found is returned.
+ *
+ * *** Results of this function are cached! ***
+ * This function is for where speed is more important than accuracy. Results
+ * are stored in a cache until 60s passes without this function being called.
+ * The address book *could* change in this time, in a way that produces a
+ * different result, but probably won't.
+ *
+ * @see nsIAbCard.cardForEmailAddress
+ * @param emailAddress The email address to find in any of the email address
+ * fields. If emailAddress is empty, the directories
+ * won't be searched and the function will return as if
+ * no card was found.
+ * @return An nsIAbCard if one was found, else returns NULL.
+ */
+ nsIAbCard cardForEmailAddress(in AUTF8String emailAddress);
+
+ /**
+ * Returns the mailing lists that has the supplied name.
+ *
+ * @param aName The name of the list.
+ * @return The found AB directory.
+ */
+ nsIAbDirectory getMailListFromName(in AString aName);
+};
diff --git a/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl b/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl
new file mode 100644
index 0000000000..5ab66ac095
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl
@@ -0,0 +1,12 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(088d3dea-4a6a-41ce-b974-5043d00f1798)]
+interface nsIAbOutlookInterface : nsISupports
+{
+ Array<ACString> getFolderURIs(in AUTF8String aURI);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPBERElement.idl b/comm/mailnews/addrbook/public/nsILDAPBERElement.idl
new file mode 100644
index 0000000000..087fd1b2d2
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPBERElement.idl
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPBERValue;
+
+
+/**
+ * nsILDAPBERElement is a wrapper interface for a C-SDK BerElement object.
+ * Typically, this is used as an intermediate object to aid in the manual
+ * construction of a BER value. Once the construction is completed by calling
+ * methods on this object, an nsILDAPBERValue can be retrieved from the
+ * asValue attribute on this interface.
+ *
+ * <http://www.mozilla.org/directory/ietf-docs/draft-ietf-ldapext-ldap-c-api-05.txt>
+ * contains some documentation that mostly (but not exactly) matches
+ * the code that this wraps in section 17.
+ */
+
+[scriptable, uuid(409f5b31-c062-4d11-a35b-0a09e7967bf2)]
+interface nsILDAPBERElement : nsISupports
+{
+ /**
+ * Initialize this object. Must be called before calling any other method
+ * on this interface.
+ *
+ * @param aValue value to preinitialize with; 0 for a new empty object
+ *
+ * @exception NS_ERROR_NOT_IMPLEMENTED preinitialization is currently
+ * not implemented
+ * @exception NS_ERROR_OUT_OF_MEMORY unable to allocate the internal
+ * BerElement
+ */
+ void init(in nsILDAPBERValue aValue);
+
+ /**
+ * Most TAG_* constants can be used in the construction or passing in of
+ * values to the aTag arguments to most of the methods in this interface.
+ */
+
+ /**
+ * When returned from a parsing method, 0xffffffff is referred to
+ * has the parse-error semantic (ie TAG_LBER_ERROR); when passing it to
+ * a construction method, it is used to mean "pick the default tag for
+ * this type" (ie TAG_LBER_DEFAULT).
+ */
+ const unsigned long TAG_LBER_ERROR = 0xffffffff;
+ const unsigned long TAG_LBER_DEFAULT = 0xffffffff;
+ const unsigned long TAG_LBER_END_OF_SEQORSET = 0xfffffffe;
+
+ /**
+ * BER encoding types and masks
+ */
+ const unsigned long TAG_LBER_PRIMITIVE = 0x00;
+
+ /**
+ * The following two tags are carried over from the LDAP C SDK; their
+ * exact purpose there is not well documented. They both have
+ * the same value there as well.
+ */
+ const unsigned long TAG_LBER_CONSTRUCTED = 0x20;
+ const unsigned long TAG_LBER_ENCODING_MASK = 0x20;
+
+ const unsigned long TAG_LBER_BIG_TAG_MASK = 0x1f;
+ const unsigned long TAG_LBER_MORE_TAG_MASK = 0x80;
+
+ /**
+ * general BER types we know about
+ */
+ const unsigned long TAG_LBER_BOOLEAN = 0x01;
+ const unsigned long TAG_LBER_INTEGER = 0x02;
+ const unsigned long TAG_LBER_BITSTRING = 0x03;
+ const unsigned long TAG_LBER_OCTETSTRING = 0x04;
+ const unsigned long TAG_LBER_NULL = 0x05;
+ const unsigned long TAG_LBER_ENUMERATED = 0x0a;
+ const unsigned long TAG_LBER_SEQUENCE = 0x30;
+ const unsigned long TAG_LBER_SET = 0x31;
+
+ /**
+ * Write a string to this element.
+ *
+ * @param aString string to write
+ * @param aTag tag for this string (if TAG_LBER_DEFAULT is used,
+ * TAG_LBER_OCTETSTRING will be written).
+ *
+ * @return number of bytes written
+ *
+ * @exception NS_ERROR_FAILUE C-SDK returned error
+ */
+ unsigned long putString(in AUTF8String aString, in unsigned long aTag);
+
+ /**
+ * Start a set. Sets may be nested.
+ *
+ * @param aTag tag for this set (if TAG_LBER_DEFAULT is used,
+ * TAG_LBER_SET will be written).
+ *
+ * @exception NS_ERROR_FAILUE C-SDK returned an error
+ */
+ void startSet(in unsigned long aTag);
+
+ /**
+ * Cause the entire set started by the last startSet() call to be written.
+ *
+ * @exception NS_ERROR_FAILUE C-SDK returned an error
+ *
+ * @return number of bytes written
+ */
+ unsigned long putSet();
+
+ /**
+ * an nsILDAPBERValue version of this element. Calls ber_flatten() under
+ * the hood.
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY
+ */
+ readonly attribute nsILDAPBERValue asValue;
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPBERValue.idl b/comm/mailnews/addrbook/public/nsILDAPBERValue.idl
new file mode 100644
index 0000000000..e4a5651476
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPBERValue.idl
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Representation of a BER value as an interface containing an array of
+ * bytes. Someday this should perhaps be obsoleted by a better, more
+ * generalized version of nsIByteBuffer, but that's currently not even
+ * scriptable (see bug 125596).
+ */
+[scriptable, uuid(c817c5fe-1dd1-11b2-a10b-ae9885762ea9)]
+interface nsILDAPBERValue : nsISupports
+{
+ /**
+ * Set the BER value from an array of bytes (copies).
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to
+ */
+ void set(in Array<octet> aValue);
+
+ /**
+ * Set the BER value from a UTF8 string (copies).
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to
+ */
+ void setFromUTF8(in AUTF8String aValue);
+
+ /**
+ * Get the BER value as an array of bytes. Note that if this value is
+ * zero-length, aCount and aRetVal will both be 0. This means that
+ * (in C++ anyway) the caller MUST test either aCount or aRetval before
+ * dereferencing aRetVal.
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to
+ */
+ Array<octet> get();
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPConnection.idl b/comm/mailnews/addrbook/public/nsILDAPConnection.idl
new file mode 100644
index 0000000000..5fb44dd67c
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPConnection.idl
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPOperation;
+interface nsILDAPMessageListener;
+interface nsILDAPURL;
+
+%{C++
+#define NS_LDAPCONNECTION_CONTRACTID "@mozilla.org/network/ldap-connection;1"
+%}
+
+[scriptable, uuid(360c1ff7-15e3-4ffe-b4b8-0eda72ebc096)]
+interface nsILDAPConnection : nsISupports
+{
+ /**
+ * the string version of lderrno
+ */
+ readonly attribute wstring errorString;
+
+ /**
+ * DN to bind as. use the init() method to set this.
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY
+ */
+ readonly attribute AUTF8String bindName;
+
+ /**
+ * private parameter (anything caller desires)
+ */
+ attribute nsISupports closure;
+
+ /**
+ * Set up the connection. Note that init() must be called on a thread
+ * that already has an nsIEventQueue.
+ *
+ * @param aUrl A URL for the ldap server. The host, port and
+ * ssl connection type will be extracted from this
+ * @param aBindName DN to bind as
+ * @param aMessageListener Callback for DNS resolution completion
+ * @param aClosure private parameter (anything caller desires)
+ * @param aVersion LDAP version to use (currently VERSION2 or
+ * VERSION3)
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE null pointer or invalid version
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_OFFLINE we are in off-line mode
+ * @exception NS_ERROR_FAILURE
+ * @exception NS_ERROR_UNEXPECTED internal error
+ */
+ void init(in nsILDAPURL aUrl,
+ in AUTF8String aBindName,
+ in nsILDAPMessageListener aMessageListener,
+ in nsISupports aClosure, in unsigned long aVersion);
+
+ const unsigned long VERSION2 = 2;
+ const unsigned long VERSION3 = 3;
+
+ /**
+ * Get information about the last error that occurred on this connection.
+ *
+ * @param matched if the server is returning LDAP_NO_SUCH_OBJECT,
+ * LDAP_ALIAS_PROBLEM, LDAP_INVALID_DN_SYNTAX,
+ * or LDAP_ALIAS_DEREF_PROBLEM, this will contain
+ * the portion of DN that matches the entry that is
+ * closest to the requested entry
+ *
+ * @param s additional error information from the server
+ *
+ * @return the error code, as defined in nsILDAPErrors.idl
+ */
+ long getLdErrno(out AUTF8String matched, out AUTF8String s);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPControl.idl b/comm/mailnews/addrbook/public/nsILDAPControl.idl
new file mode 100644
index 0000000000..97a70a4d93
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPControl.idl
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPBERValue;
+
+/**
+ * XPCOM representation of the C SDK LDAPControl structure.
+ */
+[scriptable, uuid(3a7ceb8e-482a-4a4f-9aa4-26b9a69a3595)]
+interface nsILDAPControl : nsISupports
+{
+ /**
+ * Control type, represented as a string.
+ *
+ * @exceptions none
+ */
+ attribute ACString oid;
+
+ /**
+ * The data associated with a control, if any. To specify that no data
+ * is to be associated with the control, don't set this at all (which
+ * is equivalent to setting it to null).
+ *
+ * @note Specifying a zero-length value is not currently supported. At some
+ * date, setting this to an nsILDAPBERValue which has not had any of the
+ * set methods called will be the appropriate way to do that.
+ *
+ * @exceptions none
+ */
+ attribute nsILDAPBERValue value;
+
+ /**
+ * Should the client or server abort if the control is not understood?
+ * Should be set to false for server controls used in abandon and unbind
+ * operations, since those have no server response.
+ *
+ * @exceptions none
+ */
+ attribute boolean isCritical;
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPErrors.idl b/comm/mailnews/addrbook/public/nsILDAPErrors.idl
new file mode 100644
index 0000000000..0251f63293
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPErrors.idl
@@ -0,0 +1,447 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Error codes used in the LDAP XPCOM SDK.
+ *
+ * Taken from the Mozilla C SDK's ldap.h include file, these should be
+ * the same as those specified in the draft-ietf-ldapext-ldap-c-api-04.txt
+ * Internet Draft.
+ *
+ * The only good documentation I'm aware of for these error codes is
+ * at <http://docs.iplanet.com/docs/manuals/directory.html#SDKC>.
+ * Unfortunately, this does not currently seem to be available under any
+ * open source license, so I can't include that documentation here as
+ * doxygen comments.
+ *
+ */
+[scriptable, uuid(f9ac10fa-1dd1-11b2-9798-8d5cbda95d74)]
+interface nsILDAPErrors : nsISupports
+{
+
+ const long SUCCESS = 0x00;
+
+ const long OPERATIONS_ERROR = 0x01;
+
+ const long PROTOCOL_ERROR = 0x02;
+
+ const long TIMELIMIT_EXCEEDED = 0x03;
+
+ const long SIZELIMIT_EXCEEDED = 0x04;
+
+ const long COMPARE_FALSE = 0x05;
+
+ const long COMPARE_TRUE = 0x06;
+
+ const long STRONG_AUTH_NOT_SUPPORTED = 0x07;
+
+ const long STRONG_AUTH_REQUIRED = 0x08;
+
+
+ /**
+ * UMich LDAPv2 extension
+ */
+ const long PARTIAL_RESULTS = 0x09;
+
+ /**
+ * new in LDAPv3
+ */
+ const long REFERRAL = 0x0a;
+
+ /**
+ * new in LDAPv3
+ */
+ const long ADMINLIMIT_EXCEEDED = 0x0b;
+
+ /**
+ * new in LDAPv3
+ */
+ const long UNAVAILABLE_CRITICAL_EXTENSION = 0x0c;
+
+ /**
+ * new in LDAPv3
+ */
+ const long CONFIDENTIALITY_REQUIRED = 0x0d;
+
+ /**
+ * new in LDAPv3
+ */
+ const long SASL_BIND_IN_PROGRESS = 0x0e;
+
+ const long NO_SUCH_ATTRIBUTE = 0x10;
+
+ const long UNDEFINED_TYPE = 0x11;
+
+ const long INAPPROPRIATE_MATCHING = 0x12;
+
+ const long CONSTRAINT_VIOLATION = 0x13;
+
+ const long TYPE_OR_VALUE_EXISTS = 0x14;
+
+ const long INVALID_SYNTAX = 0x15;
+
+ const long NO_SUCH_OBJECT = 0x20;
+
+ const long ALIAS_PROBLEM = 0x21;
+
+ const long INVALID_DN_SYNTAX = 0x22;
+
+ /**
+ * not used in LDAPv3
+ */
+ const long IS_LEAF = 0x23;
+
+ const long ALIAS_DEREF_PROBLEM = 0x24;
+
+ const long INAPPROPRIATE_AUTH = 0x30;
+
+ const long INVALID_CREDENTIALS = 0x31;
+
+ const long INSUFFICIENT_ACCESS = 0x32;
+
+ const long BUSY = 0x33;
+
+ const long UNAVAILABLE = 0x34;
+
+ const long UNWILLING_TO_PERFORM = 0x35;
+
+ const long LOOP_DETECT = 0x36;
+
+ /**
+ * server side sort extension
+ */
+ const long SORT_CONTROL_MISSING = 0x3C;
+
+ /**
+ * VLV extension
+ */
+ const long INDEX_RANGE_ERROR = 0x3D;
+
+ const long NAMING_VIOLATION = 0x40;
+
+ const long OBJECT_CLASS_VIOLATION = 0x41;
+
+ const long NOT_ALLOWED_ON_NONLEAF = 0x42;
+
+ const long NOT_ALLOWED_ON_RDN = 0x43;
+
+ const long ALREADY_EXISTS = 0x44;
+
+ const long NO_OBJECT_CLASS_MODS = 0x45;
+
+ /**
+ * reserved CLDAP
+ */
+ const long RESULTS_TOO_LARGE = 0x46;
+
+ /**
+ * new in LDAPv3
+ */
+ const long AFFECTS_MULTIPLE_DSAS = 0x47;
+
+ const long OTHER = 0x50;
+
+ const long SERVER_DOWN = 0x51;
+
+ const long LOCAL_ERROR = 0x52;
+
+ const long ENCODING_ERROR = 0x53;
+
+ const long DECODING_ERROR = 0x54;
+
+ const long TIMEOUT = 0x55;
+
+ const long AUTH_UNKNOWN = 0x56;
+
+ const long FILTER_ERROR = 0x57;
+
+ const long USER_CANCELLED = 0x58;
+
+ const long PARAM_ERROR = 0x59;
+
+ const long NO_MEMORY = 0x5a;
+
+ const long CONNECT_ERROR = 0x5b;
+
+ /**
+ * new in LDAPv3
+ */
+ const long NOT_SUPPORTED = 0x5c;
+
+ /**
+ * new in LDAPv3
+ */
+ const long CONTROL_NOT_FOUND = 0x5d;
+
+ /**
+ * new in LDAPv3
+ */
+ const long NO_RESULTS_RETURNED = 0x5e;
+
+ /**
+ * new in LDAPv3
+ */
+ const long MORE_RESULTS_TO_RETURN = 0x5f;
+
+ /**
+ * new in LDAPv3
+ */
+ const long CLIENT_LOOP = 0x60;
+
+ /**
+ * new in LDAPv3
+ */
+ const long REFERRAL_LIMIT_EXCEEDED = 0x61;
+};
+
+/*
+ * Map these errors codes into the nsresult namespace in C++
+ */
+%{C++
+
+#define NS_ERROR_LDAP_OPERATIONS_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::OPERATIONS_ERROR)
+
+#define NS_ERROR_LDAP_PROTOCOL_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::PROTOCOL_ERROR)
+
+#define NS_ERROR_LDAP_TIMELIMIT_EXCEEDED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::TIMELIMIT_EXCEEDED)
+
+#define NS_ERROR_LDAP_SIZELIMIT_EXCEEDED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::SIZELIMIT_EXCEEDED)
+
+#define NS_ERROR_LDAP_COMPARE_FALSE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::COMPARE_FALSE)
+
+#define NS_ERROR_LDAP_COMPARE_TRUE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::COMPARE_TRUE)
+
+#define NS_ERROR_LDAP_STRONG_AUTH_NOT_SUPPORTED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::STRONG_AUTH_NOT_SUPPORTED)
+
+#define NS_ERROR_LDAP_STRONG_AUTH_REQUIRED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::STRONG_AUTH_REQUIRED)
+
+#define NS_ERROR_LDAP_PARTIAL_RESULTS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::PARTIAL_RESULTS)
+
+#define NS_ERROR_LDAP_REFERRAL \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::REFERRAL)
+
+#define NS_ERROR_LDAP_ADMINLIMIT_EXCEEDED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::ADMINLIMIT_EXCEEDED)
+
+#define NS_ERROR_LDAP_UNAVAILABLE_CRITICAL_EXTENSION \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::UNAVAILABLE_CRITICAL_EXTENSION)
+
+#define NS_ERROR_LDAP_CONFIDENTIALITY_REQUIRED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::CONFIDENTIALITY_REQUIRED)
+
+#define NS_ERROR_LDAP_SASL_BIND_IN_PROGRESS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::SASL_BIND_IN_PROGRESS)
+
+#define NS_ERROR_LDAP_NO_SUCH_ATTRIBUTE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NO_SUCH_ATTRIBUTE)
+
+#define NS_ERROR_LDAP_UNDEFINED_TYPE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::UNDEFINED_TYPE)
+
+#define NS_ERROR_LDAP_INAPPROPRIATE_MATCHING \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INAPPROPRIATE_MATCHING)
+
+#define NS_ERROR_LDAP_CONSTRAINT_VIOLATION \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::CONSTRAINT_VIOLATION)
+
+#define NS_ERROR_LDAP_TYPE_OR_VALUE_EXISTS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::TYPE_OR_VALUE_EXISTS)
+
+#define NS_ERROR_LDAP_INVALID_SYNTAX \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INVALID_SYNTAX)
+
+#define NS_ERROR_LDAP_NO_SUCH_OBJECT \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NO_SUCH_OBJECT)
+
+#define NS_ERROR_LDAP_ALIAS_PROBLEM \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::ALIAS_PROBLEM)
+
+#define NS_ERROR_LDAP_INVALID_DN_SYNTAX \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INVALID_DN_SYNTAX)
+
+#define NS_ERROR_LDAP_IS_LEAF \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::IS_LEAF)
+
+#define NS_ERROR_LDAP_ALIAS_DEREF_PROBLEM \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::ALIAS_DEREF_PROBLEM)
+
+#define NS_ERROR_LDAP_INAPPROPRIATE_AUTH \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INAPPROPRIATE_AUTH)
+
+#define NS_ERROR_LDAP_INVALID_CREDENTIALS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INVALID_CREDENTIALS)
+
+#define NS_ERROR_LDAP_INSUFFICIENT_ACCESS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INSUFFICIENT_ACCESS)
+
+#define NS_ERROR_LDAP_BUSY \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::BUSY)
+
+#define NS_ERROR_LDAP_UNAVAILABLE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::UNAVAILABLE)
+
+#define NS_ERROR_LDAP_UNWILLING_TO_PERFORM \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::UNWILLING_TO_PERFORM)
+
+#define NS_ERROR_LDAP_LOOP_DETECT \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::LOOP_DETECT)
+
+#define NS_ERROR_LDAP_SORT_CONTROL_MISSING \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::SORT_CONTROL_MISSING)
+
+#define NS_ERROR_LDAP_INDEX_RANGE_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::INDEX_RANGE_ERROR)
+
+#define NS_ERROR_LDAP_NAMING_VIOLATION \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NAMING_VIOLATION)
+
+#define NS_ERROR_LDAP_OBJECT_CLASS_VIOLATION \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::OBJECT_CLASS_VIOLATION)
+
+#define NS_ERROR_LDAP_NOT_ALLOWED_ON_NONLEAF \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NOT_ALLOWED_ON_NONLEAF)
+
+#define NS_ERROR_LDAP_NOT_ALLOWED_ON_RDN \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NOT_ALLOWED_ON_RDN)
+
+#define NS_ERROR_LDAP_ALREADY_EXISTS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::ALREADY_EXISTS)
+
+#define NS_ERROR_LDAP_NO_OBJECT_CLASS_MODS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NO_OBJECT_CLASS_MODS)
+
+#define NS_ERROR_LDAP_RESULTS_TOO_LARGE \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::RESULTS_TOO_LARGE)
+
+#define NS_ERROR_LDAP_AFFECTS_MULTIPLE_DSAS \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::AFFECTS_MULTIPLE_DSAS)
+
+#define NS_ERROR_LDAP_OTHER \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::OTHER)
+
+#define NS_ERROR_LDAP_SERVER_DOWN \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::SERVER_DOWN)
+
+#define NS_ERROR_LDAP_LOCAL_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::LOCAL_ERROR)
+
+#define NS_ERROR_LDAP_ENCODING_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::ENCODING_ERROR)
+
+#define NS_ERROR_LDAP_DECODING_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::DECODING_ERROR)
+
+#define NS_ERROR_LDAP_TIMEOUT \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::TIMEOUT)
+
+#define NS_ERROR_LDAP_AUTH_UNKNOWN \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::AUTH_UNKNOWN)
+
+#define NS_ERROR_LDAP_FILTER_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::FILTER_ERROR)
+
+#define NS_ERROR_LDAP_USER_CANCELLED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::USER_CANCELLED)
+
+#define NS_ERROR_LDAP_PARAM_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::PARAM_ERROR)
+
+#define NS_ERROR_LDAP_NO_MEMORY \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NO_MEMORY)
+
+#define NS_ERROR_LDAP_CONNECT_ERROR \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::CONNECT_ERROR)
+
+#define NS_ERROR_LDAP_NOT_SUPPORTED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NOT_SUPPORTED)
+
+#define NS_ERROR_LDAP_CONTROL_NOT_FOUND \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::CONTROL_NOT_FOUND)
+
+#define NS_ERROR_LDAP_NO_RESULTS_RETURNED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::NO_RESULTS_RETURNED)
+
+#define NS_ERROR_LDAP_MORE_RESULTS_TO_RETURN \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::MORE_RESULTS_TO_RETURN)
+
+#define NS_ERROR_LDAP_CLIENT_LOOP \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::CLIENT_LOOP)
+
+#define NS_ERROR_LDAP_REFERRAL_LIMIT_EXCEEDED \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \
+ nsILDAPErrors::REFERRAL_LIMIT_EXCEEDED)
+
+%}
diff --git a/comm/mailnews/addrbook/public/nsILDAPMessage.idl b/comm/mailnews/addrbook/public/nsILDAPMessage.idl
new file mode 100644
index 0000000000..f6dd94efc2
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPMessage.idl
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPBERValue;
+interface nsILDAPOperation;
+
+%{C++
+#define NS_LDAPMESSAGE_CONTRACTID "@mozilla.org/network/ldap-message;1"
+%}
+
+[scriptable, uuid(973ff50f-2002-4f0c-b57d-2242156139a2)]
+interface nsILDAPMessage : nsISupports
+{
+ /**
+ * The Distinguished Name of the entry associated with this message.
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in
+ * @exception NS_ERROR_LDAP_DECODING_ERROR problem during BER-decoding
+ * @exception NS_ERROR_UNEXPECTED bug or memory corruption
+ */
+ readonly attribute AUTF8String dn;
+
+ /**
+ * Get all the attributes in this message.
+ *
+ * @exception NS_ERROR_OUT_OF_MEMORY
+ * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in
+ * @exception NS_ERROR_UNEXPECTED bug or memory corruption
+ * @exception NS_ERROR_LDAP_DECODING_ERROR problem during BER decoding
+ *
+ * @return array of all attributes in the current message
+ */
+ Array<AUTF8String> getAttributes();
+
+ /**
+ * Get an array of all the attribute values in this message.
+ *
+ * @param attr The attribute whose values are to be returned
+ *
+ * @exception NS_ERROR_UNEXPECTED Bug or memory corruption
+ * @exception NS_ERROR_LDAP_DECODING_ERROR Attribute not found or other
+ * decoding error.
+ * @exception NS_ERROR_OUT_OF_MEMORY
+ *
+ * @return Array of values for attr.
+ */
+ Array<AString> getValues(in string attr);
+
+ /**
+ * The operation this message originated from
+ *
+ * @exception NS_ERROR_NULL_POINTER NULL pointer to getter
+ */
+ readonly attribute nsILDAPOperation operation;
+
+ /**
+ * The result code (aka lderrno) for this message.
+ *
+ * IDL definitions for these constants live in nsILDAPErrors.idl.
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in
+ */
+ readonly attribute long errorCode;
+
+ /**
+ * The result type of this message. Possible types listed below, the
+ * values chosen are taken from the draft-ietf-ldapext-ldap-c-api-04.txt
+ * and are the same ones used in the ldap.h include file from the Mozilla
+ * LDAP C SDK.
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in
+ * @exception NS_ERROR_UNEXPECTED internal error (possible memory
+ * corruption)
+ */
+ readonly attribute long type;
+
+ /**
+ * Result of a bind operation
+ */
+ const long RES_BIND = 0x61;
+
+ /**
+ * An entry found in an search operation.
+ */
+ const long RES_SEARCH_ENTRY = 0x64;
+
+ /**
+ * An LDAPv3 search reference (a referral to another server)
+ */
+ const long RES_SEARCH_REFERENCE = 0x73;
+
+ /**
+ * The result of a search operation (i.e. the search is done; no more
+ * entries to follow).
+ */
+ const long RES_SEARCH_RESULT = 0x65;
+
+ /**
+ * The result of a modify operation.
+ */
+ const long RES_MODIFY = 0x67;
+
+ /**
+ * The result of an add operation
+ */
+ const long RES_ADD = 0x69;
+
+ /**
+ * The result of a delete operation
+ */
+ const long RES_DELETE = 0x6B;
+
+ /**
+ * The result of an modify DN operation
+ */
+ const long RES_MODDN = 0x6D;
+
+ /**
+ * The result of a compare operation
+ */
+ const long RES_COMPARE = 0x6F;
+
+ /**
+ * The result of an LDAPv3 extended operation
+ */
+ const long RES_EXTENDED = 0x78;
+
+ /**
+ * get an LDIF-like string representation of this message
+ *
+ * @return unicode encoded string representation.
+ */
+ wstring toUnicode();
+
+ /**
+ * Additional error information optionally sent by the server.
+ */
+ readonly attribute AUTF8String errorMessage;
+
+ /**
+ * In LDAPv3, when the server returns any of the following errors:
+ * NO_SUCH_OBJECT, ALIAS_PROBLEM, INVALID_DN_SYNTAX, ALIAS_DEREF_PROBLEM,
+ * it also returns the closest existing DN to the entry requested.
+ */
+ readonly attribute AUTF8String matchedDn;
+
+ /**
+ * Get an array of all the attribute values in this message (a wrapper
+ * around the LDAP C SDK's get_values_len()).
+ *
+ * @param attr The attribute whose values are to be returned
+ *
+ * @exception NS_ERROR_UNEXPECTED Bug or memory corruption
+ * @exception NS_ERROR_LDAP_DECODING_ERROR Attribute not found or other
+ * decoding error.
+ * @exception NS_ERROR_OUT_OF_MEMORY
+ *
+ * @return Array of nsILDAPBERValue objects.
+ */
+ Array<nsILDAPBERValue> getBinaryValues(in string attr);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl b/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl
new file mode 100644
index 0000000000..2907840df5
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPMessage;
+interface nsILDAPConnection;
+interface nsITransportSecurityInfo;
+
+/**
+ * A callback interface to be implemented by any objects that want to
+ * receive results from an nsILDAPOperation (ie nsILDAPMessages) as they
+ * come in.
+ */
+[scriptable, uuid(dc721d4b-3ff2-4387-a80c-5e29545f774a)]
+interface nsILDAPMessageListener : nsISupports
+{
+ /**
+ * Invoked when Init has completed successfully LDAP operations can
+ * proceed.
+ */
+ void onLDAPInit();
+
+ /**
+ * Messages from LDAP operations are passed back via this function.
+ *
+ * @param aMessage The message that was returned, NULL if none was.
+ *
+ * XXX semantics of NULL?
+ */
+ void onLDAPMessage(in nsILDAPMessage aMessage);
+
+
+ /**
+ * Indicates that an error has occurred - either during init, or due to
+ * an LDAP operation.
+ *
+ * @param status The error code.
+ * @param secInfo The securityInfo object for the connection, if status
+ * is a security (NSS) error. Null otherwise.
+ * @param location If status is an NSS error code, this holds the location
+ * of the failed operation ("<host>:<port>").
+ */
+ void onLDAPError(in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPModification.idl b/comm/mailnews/addrbook/public/nsILDAPModification.idl
new file mode 100644
index 0000000000..453efc4aaa
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPModification.idl
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsILDAPBERValue;
+
+[scriptable, uuid(f64ef501-0623-11d6-a7f2-b65476fc49dc)]
+interface nsILDAPModification : nsISupports
+{
+ /**
+ * The operation to perform.
+ */
+ attribute long operation;
+
+ /**
+ * Add operation
+ */
+ const long MOD_ADD = 0x00;
+
+ /**
+ * Delete operation
+ */
+ const long MOD_DELETE = 0x01;
+
+ /**
+ * Replace operation
+ */
+ const long MOD_REPLACE = 0x02;
+
+ /**
+ * Values are BER encoded
+ */
+ const long MOD_BVALUES = 0x80;
+
+ /**
+ * The attribute to modify.
+ */
+ attribute ACString type;
+
+ /**
+ * The array of values this modification sets for the attribute
+ */
+ attribute Array<nsILDAPBERValue> values;
+
+ /**
+ * Function that allows all the attributes to be set at the same
+ * time to avoid multiple function calls.
+ */
+ void setUpModification(in long aOperation, in ACString aType,
+ in Array<nsILDAPBERValue> aValues);
+
+ void setUpModificationOneValue(in long aOperation, in ACString aType,
+ in nsILDAPBERValue aValue);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPOperation.idl b/comm/mailnews/addrbook/public/nsILDAPOperation.idl
new file mode 100644
index 0000000000..3f3c9562c4
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPOperation.idl
@@ -0,0 +1,278 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+#include "nsILDAPConnection.idl"
+#include "nsIAuthModule.idl"
+
+interface nsILDAPMessage;
+interface nsILDAPMessageListener;
+interface nsILDAPModification;
+interface nsILDAPControl;
+
+%{C++
+#define NS_LDAPOPERATION_CONTRACTID "@mozilla.org/network/ldap-operation;1"
+%}
+
+// XXXdmose check to make sure ctl-related err codes documented
+
+typedef uint32_t PRIntervalTime;
+
+[scriptable, uuid(4dfb1b19-fc8f-4525-92e7-f97b78a9747a)]
+interface nsILDAPOperation : nsISupports
+{
+ /**
+ * The connection this operation is on.
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in
+ */
+ readonly attribute nsILDAPConnection connection;
+
+ /**
+ * Callback for individual result messages related to this operation (set
+ * by the init() method). This is actually an nsISupports proxy object,
+ * as the callback will happen from another thread.
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in
+ */
+ readonly attribute nsILDAPMessageListener messageListener;
+
+ /**
+ * The message-id associated with this operation.
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in
+ */
+ readonly attribute long messageID;
+
+ /**
+ * private parameter (anything caller desires)
+ */
+ attribute nsISupports closure;
+ /**
+ * number of the request for compare that the request is still valid.
+ */
+ attribute unsigned long requestNum;
+
+ /**
+ * No time and/or size limit specified
+ */
+ const long NO_LIMIT = 0;
+
+ /**
+ * If specified, these arrays of nsILDAPControls are passed into the LDAP
+ * C SDK for any extended operations (ie method calls on this interface
+ * ending in "Ext").
+ */
+ attribute Array<nsILDAPControl> serverControls;
+ attribute Array<nsILDAPControl> clientControls;
+
+ /**
+ * Initializes this operation. Must be called prior to initiating
+ * any actual operations. Note that by default, the aMessageListener
+ * callbacks happen on the LDAP connection thread. If you need them
+ * to happen on the main thread (or any other thread), then you should
+ * created an nsISupports proxy object and pass that in.
+ *
+ * @param aConnection connection this operation should use
+ * @param aMessageListener interface used to call back the results.
+ * @param aClosure private parameter (anything caller desires)
+ *
+ * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in
+ * @exception NS_ERROR_UNEXPECTED failed to get connection handle
+ */
+ void init(in nsILDAPConnection aConnection,
+ in nsILDAPMessageListener aMessageListener,
+ in nsISupports aClosure);
+
+ /**
+ * Asynchronously authenticate to the LDAP server.
+ *
+ * @param passwd the password used for binding; NULL for anon-binds
+ *
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR problem encoding bind request
+ * @exception NS_ERROR_LDAP_SERVER_DOWN server down (XXX rebinds?)
+ * @exception NS_ERROR_LDAP_CONNECT_ERROR connection failed or lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_UNEXPECTED internal error
+ */
+ void simpleBind(in AUTF8String passwd);
+
+ /**
+ * Asynchronously perform a SASL bind against the LDAP server
+ *
+ * @param service the host name of the service being connected to
+ * @param mechanism the name of the SASL mechanism in use
+ * @param authModuleType the type of auth module to be used to perform the operation
+ *
+ */
+ void saslBind(in ACString service, in ACString mechanism,
+ in ACString authModuleType);
+
+ /**
+ * Continue a SASL bind operation
+ *
+ * @param token the next SASL token to send to the server
+ * @param tokenLen the length of the token to send
+ *
+ */
+ void saslStep(in string token, in unsigned long tokenLen);
+
+ /**
+ * Kicks off an asynchronous add request. The "ext" stands for
+ * "extensions", and is intended to convey that this method will
+ * eventually support the extensions described in the
+ * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft.
+ *
+ * @param aBaseDn Base DN to add
+ * @param aModCount Number of modifications
+ * @param aMods Array of modifications
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version
+ * of the LDAP protocol that the
+ * client is using
+ * @exception NS_ERROR_UNEXPECTED an unexpected error has
+ * occurred
+ *
+ * XXX doesn't currently handle LDAPControl params
+ */
+ void addExt(in AUTF8String aBaseDn, in Array<nsILDAPModification> aMods);
+
+ /**
+ * Kicks off an asynchronous delete request. The "ext" stands for
+ * "extensions", and is intended to convey that this method will
+ * eventually support the extensions described in the
+ * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft.
+ *
+ * @param aBaseDn Base DN to delete
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version
+ * of the LDAP protocol that the
+ * client is using
+ * @exception NS_ERROR_UNEXPECTED an unexpected error has
+ * occurred
+ *
+ * XXX doesn't currently handle LDAPControl params
+ */
+ void deleteExt(in AUTF8String aBaseDn);
+
+ /**
+ * Kicks off an asynchronous modify request. The "ext" stands for
+ * "extensions", and is intended to convey that this method will
+ * eventually support the extensions described in the
+ * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft.
+ *
+ * @param aBaseDn Base DN to modify
+ * @param aModCount Number of modifications
+ * @param aMods Array of modifications
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version
+ * of the LDAP protocol that the
+ * client is using
+ * @exception NS_ERROR_UNEXPECTED an unexpected error has
+ * occurred
+ *
+ * XXX doesn't currently handle LDAPControl params
+ */
+ void modifyExt(in AUTF8String aBaseDn, in Array<nsILDAPModification> aMods);
+
+ /**
+ * Kicks off an asynchronous rename request.
+ *
+ * @param aBaseDn Base DN to rename
+ * @param aNewRDn New relative DN
+ * @param aNewParent DN of the new parent under which to move the
+ * entry
+ * @param aDeleteOldRDn Indicates whether to remove the old relative
+ * DN as a value in the entry or not
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version
+ * of the LDAP protocol that the
+ * client is using
+ * @exception NS_ERROR_UNEXPECTED an unexpected error has
+ * occurred
+ *
+ * XXX doesn't currently handle LDAPControl params
+ */
+ void rename(in AUTF8String aBaseDn, in AUTF8String aNewRDn,
+ in AUTF8String aNewParent, in boolean aDeleteOldRDn);
+
+ /**
+ * Kicks off an asynchronous search request. The "ext" stands for
+ * "extensions", and is intended to convey that this method will
+ * eventually support the extensions described in the
+ * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft.
+ *
+ * @param aBaseDn Base DN to search
+ * @param aScope One of SCOPE_{BASE,ONELEVEL,SUBTREE}
+ * @param aFilter Search filter
+ * @param aAttributes Comma separated list of values, holding the
+ * attributes we need
+ * @param aTimeOut How long to wait
+ * @param aSizeLimit Maximum number of entries to return.
+ *
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version
+ * of the LDAP protocol that the
+ * client is using
+ * @exception NS_ERROR_LDAP_FILTER_ERROR
+ * @exception NS_ERROR_UNEXPECTED
+ */
+ void searchExt(in AUTF8String aBaseDn, in int32_t aScope,
+ in AUTF8String aFilter, in ACString aAttributes,
+ in PRIntervalTime aTimeOut, in int32_t aSizeLimit);
+
+ /**
+ * Cancels an async operation that is in progress.
+ *
+ * XXX controls not supported yet
+ *
+ * @exception NS_ERROR_NOT_IMPLEMENTED server or client controls
+ * were set on this object
+ * @exception NS_ERROR_NOT_INITIALIZED operation not initialized
+ * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding
+ * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not
+ * receive the request or the
+ * connection was lost
+ * @exception NS_ERROR_OUT_OF_MEMORY out of memory
+ * @exception NS_ERROR_INVALID_ARG invalid argument
+ * @exception NS_ERROR_UNEXPECTED internal error
+ */
+ void abandonExt();
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPService.idl b/comm/mailnews/addrbook/public/nsILDAPService.idl
new file mode 100644
index 0000000000..6396dafc21
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPService.idl
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This interface is a catch-all for any LDAP functionality that is needed
+ * in XPCOM and not provided elsewhere.
+ */
+[scriptable, uuid(69de6fbc-2e8c-4482-bf14-358d68b785d1)]
+interface nsILDAPService : nsISupports {
+
+ /**
+ * Generates and returns an LDAP search filter by substituting
+ * aValue, aAttr, aPrefix, and aSuffix into aPattern.
+ *
+ * Exposes the functionality of ldap_create_filter() via XPCOM.
+ *
+ * There is some documentation on the filter template format
+ * (passed in via aPattern) here:
+ * https://docs.oracle.com/cd/E19957-01/817-6707/filter.html
+ *
+ * @param aMaxSize maximum size (in char) of string to be
+ * created and returned (including final \0)
+ * @param aPattern pattern to be used for the filter
+ * @param aPrefix prefix to prepend to the filter
+ * @param aSuffix suffix to be appended to the filer
+ * @param aAttr replacement for %a in the pattern
+ * @param aValue replacement for %v in the pattern
+ *
+ * @exception NS_ERROR_INVALID_ARG invalid parameter passed in
+ * @exception NS_ERROR_OUT_OF_MEMORY allocation failed
+ * @exception NS_ERROR_NOT_AVAILABLE filter longer than maxsiz chars
+ * @exception NS_ERROR_UNEXPECTED ldap_create_filter returned
+ * unexpected error code
+ */
+ AUTF8String createFilter(in unsigned long aMaxSize, in AUTF8String aPattern,
+ in AUTF8String aPrefix, in AUTF8String aSuffix,
+ in AUTF8String aAttr, in AUTF8String aValue);
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl b/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl
new file mode 100644
index 0000000000..d2709ed2a2
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl
@@ -0,0 +1,27 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+/* 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/. */
+
+#include "nsISupports.idl"
+interface nsILDAPURL;
+
+
+[scriptable, uuid (0308fb36-1dd2-11b2-b16f-8510e8c5311a)]
+interface nsILDAPSyncQuery : nsISupports {
+
+ /**
+ * getQueryResults
+ *
+ * Create a new LDAP connection do a synchronous LDAP search and return
+ * the results.
+ * @param aServerURL - LDAP URL with parameters to a LDAP search
+ * ("ldap://host/base?attributes?one/sub?filter")
+ * @param aProtocolVersion - LDAP protocol version to use for connection
+ * (nsILDAPConnection.idl has symbolic constants)
+ * @return results
+ */
+ wstring getQueryResults (in nsILDAPURL aServerURL,
+ in unsigned long aProtocolVersion);
+
+};
diff --git a/comm/mailnews/addrbook/public/nsILDAPURL.idl b/comm/mailnews/addrbook/public/nsILDAPURL.idl
new file mode 100644
index 0000000000..fd0c3e28ec
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsILDAPURL.idl
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsIURI.idl"
+
+%{C++
+#define NS_LDAPURL_CONTRACTID "@mozilla.org/network/ldap-url;1"
+%}
+
+/**
+ * Strings in methods inherited from nsIURI, which are using XPIDL
+ * |string| types, are expected to be UTF8 encoded. All such strings
+ * in this interface, except attribute types (e.g. "cn"), should in fact
+ * be UTF8. It's important to remember that attributes can not be UTF8,
+ * they can only be of a limited subset of ASCII (see RFC 2251).
+ */
+
+[scriptable, builtinclass, uuid(8e3a6d33-2e68-40ba-8f94-6ac03f69066e)]
+interface nsILDAPURL : nsIURI {
+ /**
+ * Initialize an LDAP URL
+ *
+ * @param aUrlType - one of the URLTYPE_ flags @seealso nsIStandardURL
+ * @param aDefaultPort - if the port parsed from the URL string matches
+ * this port, then the port will be removed from the
+ * canonical form of the URL.
+ * @param aSpec - URL string.
+ * @param aOriginCharset - the charset from which this URI string
+ * originated. this corresponds to the charset
+ * that should be used when communicating this
+ * URI to an origin server, for example. if
+ * null, then provide aBaseURI implements this
+ * interface, the origin charset of aBaseURI will
+ * be assumed, otherwise defaulting to UTF-8 (i.e.,
+ * no charset transformation from aSpec).
+ * @param aBaseURI - if null, aSpec must specify an absolute URI.
+ * otherwise, aSpec will be resolved relative
+ * to aBaseURI.
+ */
+ void init(in unsigned long aUrlType,
+ in long aDefaultPort,
+ in AUTF8String aSpec,
+ in string aOriginCharset,
+ in nsIURI aBaseURI);
+
+ /**
+ * The distinguished name of the URL (ie the base DN for the search).
+ * This string is expected to be a valid UTF8 string.
+ *
+ * for the getter:
+ *
+ * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method
+ * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory
+ */
+ attribute AUTF8String dn;
+
+ /**
+ * The attributes to get for this URL, in comma-separated format. If the
+ * list is empty, all attributes are requested.
+ */
+ attribute ACString attributes;
+
+ /**
+ * Add one attribute to the array of attributes to request. If the
+ * attribute is already in our array, this becomes a noop.
+ *
+ * @param aAttribute An LDAP attribute (e.g. "cn")
+ */
+ void addAttribute(in ACString aAttribute);
+
+ /**
+ * Remove one attribute from the array of attributes to request. If
+ * the attribute didn't exist in the array, this becomes a noop.
+ *
+ * @param aAttribute An LDAP attribute (e.g. "cn")
+ * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory
+ */
+ void removeAttribute(in ACString aAttribute);
+
+ /**
+ * Test if an attribute is in our list of attributes already
+ *
+ * @param aAttribute An LDAP attribute (e.g. "cn")
+ * @return boolean Truth value
+ * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method
+ */
+ boolean hasAttribute(in ACString aAttribute);
+
+ /**
+ * The scope of the search. defaults to SCOPE_BASE.
+ *
+ * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method
+ * @exception NS_ERROR_MALFORMED_URI Illegal base to SET method
+ */
+ attribute long scope;
+
+ /**
+ * Search just the base object
+ */
+ const long SCOPE_BASE = 0;
+
+ /**
+ * Search only the children of the base object
+ */
+ const long SCOPE_ONELEVEL = 1;
+
+ /**
+ * Search the entire subtree under and including the base object
+ */
+ const long SCOPE_SUBTREE = 2;
+
+ /**
+ * The search filter. "(objectClass=*)" is the default.
+ */
+ attribute AUTF8String filter;
+
+ /**
+ * Any options defined for this URL (check options using a bitwise and)
+ *
+ * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method
+ * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory
+ */
+ attribute unsigned long options;
+
+ /**
+ * If this is set/true, this is an ldaps: URL, not an ldap: URL
+ */
+ const unsigned long OPT_SECURE = 0x01;
+};
+
+/**
+ * A structure to represent an LDAP URL.
+ */
+[scriptable, uuid(c0376fe9-2c7c-4f7b-a991-db9c3d95c1bb)]
+interface nsILDAPURLParserResult : nsISupports {
+ /** The host name of the URL. */
+ readonly attribute AUTF8String host;
+
+ /** The port number of the URL. */
+ readonly attribute long port;
+
+ /** The distinguished name of the URL. */
+ readonly attribute AUTF8String dn;
+
+ /** The attributes to request when searching. */
+ readonly attribute ACString attributes;
+
+ /** The scope to use when searching. */
+ readonly attribute long scope;
+
+ /** The filter to use when searching. */
+ readonly attribute AUTF8String filter;
+
+ /** The options of the URL. */
+ readonly attribute unsigned long options;
+};
+
+/**
+ * A helper module to parse a string to an LDAP URL.
+ */
+[scriptable, uuid(340098c0-a881-49ab-a5e8-f79d04e6651c)]
+interface nsILDAPURLParser : nsISupports {
+ /**
+ * Parse a string to an LDAP URL.
+ */
+ nsILDAPURLParserResult parse(in AUTF8String aSpec);
+};
diff --git a/comm/mailnews/addrbook/public/nsIMsgVCardService.idl b/comm/mailnews/addrbook/public/nsIMsgVCardService.idl
new file mode 100644
index 0000000000..b2d542f14c
--- /dev/null
+++ b/comm/mailnews/addrbook/public/nsIMsgVCardService.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAbCard;
+
+[scriptable, uuid(8b6ae917-676d-4f1f-bbad-2ecc9be0d9b1)]
+interface nsIMsgVCardService : nsISupports {
+
+ /**
+ * Translates a vCard string into a nsIAbCard.
+ *
+ * @param vCardStr - The string containing the vCard data.
+ * @return - A card containing the translated vCard data.
+ */
+ nsIAbCard vCardToAbCard(in AString vCardStr);
+
+ /**
+ * Translates an URL-encoded vCard string into a nsIAbCard.
+ *
+ * @param escapedVCardStr - The string containing the vCard data.
+ * @return - A card containing the translated vCard data.
+ */
+ nsIAbCard escapedVCardToAbCard(in AString escapedVCardStr);
+
+ /**
+ * Translates a nsIAbCard into an URL-encoded vCard.
+ *
+ * @param abCard - A card to be translated.
+ * @return - The string containing the vCard data.
+ */
+ AString abCardToEscapedVCard(in nsIAbCard abCard);
+};
diff --git a/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm b/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm
new file mode 100644
index 0000000000..08a2654d03
--- /dev/null
+++ b/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm
@@ -0,0 +1,69 @@
+/* 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 EXPORTED_SYMBOLS = ["AbAutoCompleteMyDomain"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function AbAutoCompleteMyDomain() {}
+
+AbAutoCompleteMyDomain.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+
+ cachedIdKey: "",
+ cachedIdentity: null,
+
+ applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]),
+
+ startSearch(aString, aSearchParam, aResult, aListener) {
+ let params = aSearchParam ? JSON.parse(aSearchParam) : {};
+ let applicable =
+ "type" in params && this.applicableHeaders.has(params.type);
+ const ACR = Ci.nsIAutoCompleteResult;
+ var address = null;
+ if (applicable && aString && !aString.includes(",")) {
+ if ("idKey" in params && params.idKey != this.cachedIdKey) {
+ this.cachedIdentity = MailServices.accounts.getIdentity(params.idKey);
+ this.cachedIdKey = params.idKey;
+ }
+ if (this.cachedIdentity.autocompleteToMyDomain) {
+ address = aString.includes("@")
+ ? aString
+ : this.cachedIdentity.email.replace(/[^@]*/, aString);
+ }
+ }
+
+ var result = {
+ searchString: aString,
+ searchResult: address ? ACR.RESULT_SUCCESS : ACR.RESULT_FAILURE,
+ defaultIndex: -1,
+ errorDescription: null,
+ matchCount: address ? 1 : 0,
+ getValueAt() {
+ return address;
+ },
+ getLabelAt() {
+ return this.getValueAt();
+ },
+ getCommentAt() {
+ return null;
+ },
+ getStyleAt() {
+ return "default-match";
+ },
+ getImageAt() {
+ return null;
+ },
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+ removeValueAt() {},
+ };
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch() {},
+};
diff --git a/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm b/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..8beff4f670
--- /dev/null
+++ b/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
@@ -0,0 +1,608 @@
+/* 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 EXPORTED_SYMBOLS = ["AbAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var {
+ getSearchTokens,
+ getModelQuery,
+ modelQueryHasUserValue,
+ generateQueryURI,
+} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm");
+
+var ACR = Ci.nsIAutoCompleteResult;
+var nsIAbAutoCompleteResult = Ci.nsIAbAutoCompleteResult;
+
+var MAX_ASYNC_RESULTS = 100;
+
+function nsAbAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this.asyncDirectories = [];
+ this._searchResults = []; // final results
+ this.searchString = aSearchString;
+ this._collectedValues = new Map(); // temporary unsorted results
+ // Get model query from pref; this will return mail.addr_book.autocompletequery.format.phonetic
+ // if mail.addr_book.show_phonetic_fields == true
+ this.modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+ // check if the currently active model query has been modified by user
+ this._modelQueryHasUserValue = modelQueryHasUserValue(
+ "mail.addr_book.autocompletequery.format"
+ );
+}
+
+nsAbAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ searchString: null,
+ searchResult: ACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt(aIndex) {
+ return "local-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsIAbAutoCompleteResult
+
+ getCardAt(aIndex) {
+ return this._searchResults[aIndex].card;
+ },
+
+ getEmailToUse(aIndex) {
+ return this._searchResults[aIndex].emailToUse;
+ },
+
+ isCompleteResult(aIndex) {
+ return this._searchResults[aIndex].isCompleteResult;
+ },
+
+ modelQuery: null,
+ asyncDirectories: null,
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAutoCompleteResult",
+ "nsIAbAutoCompleteResult",
+ ]),
+};
+
+function AbAutoCompleteSearch() {}
+
+AbAutoCompleteSearch.prototype = {
+ // This is set from a preference,
+ // 0 = no comment column, 1 = name of address book this card came from
+ // Other numbers currently unused (hence default to zero)
+ _commentColumn: 0,
+ _parser: MailServices.headerParser,
+ _abManager: MailServices.ab,
+ applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]),
+ _result: null,
+
+ // Private methods
+
+ /**
+ * Returns the popularity index for a given card. This takes account of a
+ * translation bug whereby Thunderbird 2 stores its values in mork as
+ * hexadecimal, and Thunderbird 3 stores as decimal.
+ *
+ * @param {nsIAbDirectory} aDirectory - The directory that the card is in.
+ * @param {nsIAbCard} aCard - The card to return the popularity index for.
+ */
+ _getPopularityIndex(aDirectory, aCard) {
+ let popularityValue = aCard.getProperty("PopularityIndex", "0");
+ let popularityIndex = parseInt(popularityValue);
+
+ // If we haven't parsed it the first time round, parse it as hexadecimal
+ // and repair so that we don't have to keep repairing.
+ if (isNaN(popularityIndex)) {
+ popularityIndex = parseInt(popularityValue, 16);
+
+ // If its still NaN, just give up, we shouldn't ever get here.
+ if (isNaN(popularityIndex)) {
+ popularityIndex = 0;
+ }
+
+ // Now store this change so that we're not changing it each time around.
+ if (!aDirectory.readOnly) {
+ aCard.setProperty("PopularityIndex", popularityIndex);
+ try {
+ aDirectory.modifyCard(aCard);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ }
+ return popularityIndex;
+ },
+
+ /**
+ * Gets the score of the (full) address, given the search input. We want
+ * results that match the beginning of a "word" in the result to score better
+ * than a result that matches only in the middle of the word.
+ *
+ * @param {nsIAbCard} aCard - The card whose score is being decided.
+ * @param {string} aAddress - Full lower-cased address, including display
+ * name and address.
+ * @param {string} aSearchString - Search string provided by user.
+ * @returns {integer} a score; a higher score is better than a lower one.
+ */
+ _getScore(aCard, aAddress, aSearchString) {
+ const BEST = 100;
+
+ // We will firstly check if the search term provided by the user
+ // is the nick name for the card or at least in the beginning of it.
+ let nick = aCard.getProperty("NickName", "").toLocaleLowerCase();
+ aSearchString = aSearchString.toLocaleLowerCase();
+ if (nick == aSearchString) {
+ return BEST + 1;
+ }
+ if (nick.indexOf(aSearchString) == 0) {
+ return BEST;
+ }
+
+ // We'll do this case-insensitively and ignore the domain.
+ let atIdx = aAddress.lastIndexOf("@");
+ if (atIdx != -1) {
+ // mail lists don't have an @
+ aAddress = aAddress.substr(0, atIdx);
+ }
+ let idx = aAddress.indexOf(aSearchString);
+ if (idx == 0) {
+ return BEST;
+ }
+ if (idx == -1) {
+ return 0;
+ }
+
+ // We want to treat firstname, lastname and word boundary(ish) parts of
+ // the email address the same. E.g. for "John Doe (:xx) <jd.who@example.com>"
+ // all of these should score the same: "John", "Doe", "xx",
+ // ":xx", "jd", "who".
+ let prevCh = aAddress.charAt(idx - 1);
+ if (/[ :."'(\-_<&]/.test(prevCh)) {
+ return BEST;
+ }
+
+ // The match was inside a word -> we don't care about the position.
+ return 0;
+ },
+
+ /**
+ * Searches cards in the given directory. If a card is matched (and isn't
+ * a mailing list) then the function will add a result for each email address
+ * that exists.
+ *
+ * @param {string} searchQuery - The boolean search query to use.
+ * @param {string} searchString - The original search string.
+ * @param {nsIAbDirectory} directory - An nsIAbDirectory to search.
+ * @param {nsIAbAutoCompleteResult} result - The result element to append
+ * results to.
+ */
+ _searchCards(searchQuery, searchString, directory, result) {
+ // Cache this values to save going through xpconnect each time
+ let commentColumn = this._commentColumn == 1 ? directory.dirName : "";
+
+ if (searchQuery[0] == "?") {
+ searchQuery = searchQuery.substring(1);
+ }
+ return new Promise(resolve => {
+ directory.search(searchQuery, searchString, {
+ onSearchFoundCard: card => {
+ if (card.isMailList) {
+ this._addToResult(commentColumn, directory, card, "", true, result);
+ } else {
+ let first = true;
+ for (let emailAddress of card.emailAddresses) {
+ this._addToResult(
+ commentColumn,
+ directory,
+ card,
+ emailAddress,
+ first,
+ result
+ );
+ first = false;
+ }
+ }
+ },
+ onSearchFinished(status, complete, secInfo, location) {
+ resolve();
+ },
+ });
+ });
+ },
+
+ /**
+ * Checks the parent card and email address of an autocomplete results entry
+ * from a previous result against the search parameters to see if that entry
+ * should still be included in the narrowed-down result.
+ *
+ * @param {nsIAbCard} aCard - The card to check.
+ * @param {string} aEmailToUse - The email address to check against.
+ * @param {string[]} aSearchWords - Words in the multi word search string.
+ * @returns {boolean} True if the card matches the search parameters,
+ * false otherwise.
+ */
+ _checkEntry(aCard, aEmailToUse, aSearchWords) {
+ // Joining values of many fields in a single string so that a single
+ // search query can be fired on all of them at once. Separating them
+ // using spaces so that field1=> "abc" and field2=> "def" on joining
+ // shouldn't return true on search for "bcd".
+ // Note: This should be constructed from model query pref using
+ // getModelQuery("mail.addr_book.autocompletequery.format")
+ // but for now we hard-code the default value equivalent of the pref here
+ // or else bail out before and reconstruct the full c++ query if the pref
+ // has been customized (modelQueryHasUserValue), so that we won't get here.
+ let cumulativeFieldText =
+ aCard.displayName +
+ " " +
+ aCard.firstName +
+ " " +
+ aCard.lastName +
+ " " +
+ aEmailToUse +
+ " " +
+ aCard.getProperty("NickName", "");
+ if (aCard.isMailList) {
+ cumulativeFieldText += " " + aCard.getProperty("Notes", "");
+ }
+ cumulativeFieldText = cumulativeFieldText.toLocaleLowerCase();
+
+ return aSearchWords.every(String.prototype.includes, cumulativeFieldText);
+ },
+
+ /**
+ * Checks to see if an emailAddress (name/address) is a duplicate of an
+ * existing entry already in the results. If the emailAddress is found, it
+ * will remove the existing element if the popularity of the new card is
+ * higher than the previous card.
+ *
+ * @param {nsIAbDirectory} directory - The directory that the card is in.
+ * @param {nsIAbCard} card - The card that could be a duplicate.
+ * @param {string} lcEmailAddress - The emailAddress (name/address
+ * combination) to check for duplicates against. Lowercased.
+ * @param {nsIAbAutoCompleteResult} currentResults - The current results list.
+ */
+ _checkDuplicate(directory, card, lcEmailAddress, currentResults) {
+ let existingResult = currentResults._collectedValues.get(lcEmailAddress);
+ if (!existingResult) {
+ return false;
+ }
+
+ let popIndex = this._getPopularityIndex(directory, card);
+ // It's a duplicate, is the new one more popular?
+ if (popIndex > existingResult.popularity) {
+ // Yes it is, so delete this element, return false and allow
+ // _addToResult to sort the new element into the correct place.
+ currentResults._collectedValues.delete(lcEmailAddress);
+ return false;
+ }
+ // Not more popular, but still a duplicate. Return true and _addToResult
+ // will just forget about it.
+ return true;
+ },
+
+ /**
+ * Adds a card to the results list if it isn't a duplicate. The function will
+ * order the results by popularity.
+ *
+ * @param {string} commentColumn - The text to be displayed in the comment
+ * column (if any).
+ * @param {nsIAbDirectory} directory - The directory that the card is in.
+ * @param {nsIAbCard} card - The card being added to the results.
+ * @param {string} emailToUse - The email address from the card that should
+ * be used for this result.
+ * @param {boolean} isPrimaryEmail - Is the emailToUse the primary email?
+ * Set to true if it is the case. For mailing lists set it to true.
+ * @param {nsIAbAutoCompleteResult} result - The result to add the new entry to.
+ */
+ _addToResult(
+ commentColumn,
+ directory,
+ card,
+ emailToUse,
+ isPrimaryEmail,
+ result
+ ) {
+ let mbox = this._parser.makeMailboxObject(
+ card.displayName,
+ card.isMailList
+ ? card.getProperty("Notes", "") || card.displayName
+ : emailToUse
+ );
+ if (!mbox.email) {
+ return;
+ }
+
+ let emailAddress = mbox.toString();
+ let lcEmailAddress = emailAddress.toLocaleLowerCase();
+
+ // If it is a duplicate, then just return and don't add it. The
+ // _checkDuplicate function deals with it all for us.
+ if (this._checkDuplicate(directory, card, lcEmailAddress, result)) {
+ return;
+ }
+
+ result._collectedValues.set(lcEmailAddress, {
+ value: emailAddress,
+ comment: commentColumn,
+ card,
+ isPrimaryEmail,
+ emailToUse,
+ isCompleteResult: true,
+ popularity: this._getPopularityIndex(directory, card),
+ score: this._getScore(card, lcEmailAddress, result.searchString),
+ });
+ },
+
+ // nsIAutoCompleteSearch
+
+ /**
+ * Starts a search based on the given parameters.
+ *
+ * @see nsIAutoCompleteSearch for parameter details.
+ *
+ * It is expected that aSearchParam contains the identity (if any) to use
+ * for determining if an address book should be autocompleted against.
+ */
+ async startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+ let params = aSearchParam ? JSON.parse(aSearchParam) : {};
+ var result = new nsAbAutoCompleteResult(aSearchString);
+ if ("type" in params && !this.applicableHeaders.has(params.type)) {
+ result.searchResult = ACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ let fullString = aSearchString && aSearchString.trim().toLocaleLowerCase();
+
+ // If the search string is empty, or the user hasn't enabled autocomplete,
+ // then just return no matches or the result ignored.
+ if (!fullString) {
+ result.searchResult = ACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ // Array of all the terms from the fullString search query
+ // (separated on the basis of spaces or exact terms on the
+ // basis of quotes).
+ let searchWords = getSearchTokens(fullString);
+
+ // Find out about the comment column
+ this._commentColumn = Services.prefs.getIntPref(
+ "mail.autoComplete.commentColumn",
+ 0
+ );
+
+ let asyncDirectories = [];
+
+ if (
+ aPreviousResult instanceof nsIAbAutoCompleteResult &&
+ aSearchString.startsWith(aPreviousResult.searchString) &&
+ aPreviousResult.searchResult == ACR.RESULT_SUCCESS &&
+ !result._modelQueryHasUserValue &&
+ result.modelQuery == aPreviousResult.modelQuery
+ ) {
+ // We have successful previous matches, and model query has not changed since
+ // previous search, therefore just iterate through the list of previous result
+ // entries and reduce as appropriate (via _checkEntry function).
+ // Test for model query change is required: when reverting back from custom to
+ // default query, result._modelQueryHasUserValue==false, but we must bail out.
+ // Todo: However, if autocomplete model query has been customized, we fall
+ // back to using the full query again instead of reducing result list in js;
+ // The full query might be less performant as it's fired against entire AB,
+ // so we should try morphing the query for js. We can't use the _checkEntry
+ // js query yet because it is hardcoded (mimic default model query).
+ // At least we now allow users to customize their autocomplete model query...
+ for (let i = 0; i < aPreviousResult.matchCount; ++i) {
+ if (aPreviousResult.isCompleteResult(i)) {
+ let card = aPreviousResult.getCardAt(i);
+ let email = aPreviousResult.getEmailToUse(i);
+ if (this._checkEntry(card, email, searchWords)) {
+ // Add matches into the results array. We re-sort as needed later.
+ result._searchResults.push({
+ value: aPreviousResult.getValueAt(i),
+ comment: aPreviousResult.getCommentAt(i),
+ card,
+ isPrimaryEmail: card.primaryEmail == email,
+ emailToUse: email,
+ isCompleteResult: true,
+ popularity: parseInt(card.getProperty("PopularityIndex", "0")),
+ score: this._getScore(
+ card,
+ aPreviousResult.getValueAt(i).toLocaleLowerCase(),
+ fullString
+ ),
+ });
+ }
+ }
+ }
+
+ asyncDirectories = aPreviousResult.asyncDirectories;
+ } else {
+ // Construct the search query from pref; using a query means we can
+ // optimise on running the search through c++ which is better for string
+ // comparisons (_checkEntry is relatively slow).
+ // When user's fullstring search expression is a multiword query, search
+ // for each word separately so that each result contains all the words
+ // from the fullstring in the fields of the addressbook card
+ // (see bug 558931 for explanations).
+ // Use helper method to split up search query to multi-word search
+ // query against multiple fields.
+ let searchWords = getSearchTokens(fullString);
+ let searchQuery = generateQueryURI(result.modelQuery, searchWords);
+
+ // Now do the searching
+ // We're not going to bother searching sub-directories, currently the
+ // architecture forces all cards that are in mailing lists to be in ABs as
+ // well, therefore by searching sub-directories (aka mailing lists) we're
+ // just going to find duplicates.
+ for (let dir of this._abManager.directories) {
+ // A failure in one address book should no break the whole search.
+ try {
+ if (dir.useForAutocomplete("idKey" in params ? params.idKey : null)) {
+ await this._searchCards(searchQuery, aSearchString, dir, result);
+ } else if (dir.dirType == Ci.nsIAbManager.ASYNC_DIRECTORY_TYPE) {
+ asyncDirectories.push(dir);
+ }
+ } catch (ex) {
+ console.error(
+ new Components.Exception(
+ `Exception thrown by ${dir.URI}: ${ex.message}`,
+ ex
+ )
+ );
+ }
+ }
+
+ result._searchResults = [...result._collectedValues.values()];
+ // Make sure a result with direct email match will be the one used.
+ for (let sr of result._searchResults) {
+ if (sr.emailToUse == fullString.replace(/.*<(.+@.+)>$/, "$1")) {
+ sr.score = 100;
+ }
+ }
+ }
+
+ // Sort the results. Scoring may have changed so do it even if this is
+ // just filtered previous results. Only local results are sorted,
+ // because the autocomplete widget doesn't let us alter the order of
+ // results that have already been notified.
+ result._searchResults.sort(function (a, b) {
+ // Order by 1) descending score, then 2) descending popularity,
+ // then 3) any emails that actually match the search string,
+ // 4) primary email before secondary for the same card, then
+ // 5) by emails sorted alphabetically.
+ return (
+ b.score - a.score ||
+ b.popularity - a.popularity ||
+ (b.emailToUse.includes(aSearchString) &&
+ !a.emailToUse.includes(aSearchString)
+ ? 1
+ : 0) ||
+ (a.card == b.card && a.isPrimaryEmail ? -1 : 0) ||
+ a.value.localeCompare(b.value)
+ );
+ });
+
+ if (result.matchCount) {
+ result.searchResult = ACR.RESULT_SUCCESS;
+ result.defaultIndex = 0;
+ }
+
+ if (!asyncDirectories.length) {
+ // We're done. Just return our result immediately.
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ // Let the widget know the sync results we have so far.
+ result.searchResult = result.matchCount
+ ? ACR.RESULT_SUCCESS_ONGOING
+ : ACR.RESULT_NOMATCH_ONGOING;
+ aListener.onSearchResult(this, result);
+
+ // Start searching our asynchronous autocomplete directories.
+ this._result = result;
+ let searches = new Set();
+ for (let dir of asyncDirectories) {
+ let comment = this._commentColumn == 1 ? dir.dirName : "";
+ let cards = [];
+ let searchListener = {
+ onSearchFoundCard: card => {
+ cards.push(card);
+ },
+ onSearchFinished: (status, isCompleteResult, secInfo, location) => {
+ if (this._result != result) {
+ // The search was aborted, so give up.
+ return;
+ }
+ searches.delete(searchListener);
+ if (cards.length) {
+ // Avoid overwhelming the UI with excessive results.
+ if (cards.length > MAX_ASYNC_RESULTS) {
+ cards.length = MAX_ASYNC_RESULTS;
+ isCompleteResult = false;
+ }
+ // We can't guarantee to score the extension's results accurately so
+ // we assume that the extension has sorted the results appropriately
+ for (let card of cards) {
+ let emailToUse = card.primaryEmail;
+ let value = MailServices.headerParser
+ .makeMailboxObject(card.displayName, emailToUse)
+ .toString();
+ result._searchResults.push({
+ value,
+ comment,
+ card,
+ emailToUse,
+ isCompleteResult,
+ });
+ }
+ if (!isCompleteResult) {
+ // Next time perform a full search again to get better results.
+ result.asyncDirectories.push(dir);
+ }
+ }
+ if (result._searchResults.length) {
+ result.searchResult = searches.size
+ ? ACR.RESULT_SUCCESS_ONGOING
+ : ACR.RESULT_SUCCESS;
+ result.defaultIndex = 0;
+ } else {
+ result.searchResult = searches.size
+ ? ACR.RESULT_NOMATCH_ONGOING
+ : ACR.RESULT_NOMATCH;
+ }
+ aListener.onSearchResult(this, result);
+ },
+ };
+ // Keep track of the pending searches so that we know when we've finished.
+ searches.add(searchListener);
+ dir.search(null, aSearchString, searchListener);
+ }
+ },
+
+ stopSearch() {
+ this._result = null;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+};
diff --git a/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm b/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm
new file mode 100644
index 0000000000..dae7b97630
--- /dev/null
+++ b/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm
@@ -0,0 +1,219 @@
+/* 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 EXPORTED_SYMBOLS = ["AbLDAPAttributeMap", "AbLDAPAttributeMapService"];
+
+function AbLDAPAttributeMap() {
+ this.mPropertyMap = {};
+ this.mAttrMap = {};
+}
+
+AbLDAPAttributeMap.prototype = {
+ getAttributeList(aProperty) {
+ if (!(aProperty in this.mPropertyMap)) {
+ return null;
+ }
+
+ // return the joined list
+ return this.mPropertyMap[aProperty].join(",");
+ },
+
+ getAttributes(aProperty) {
+ // fail if no entry for this
+ if (!(aProperty in this.mPropertyMap)) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ return this.mPropertyMap[aProperty];
+ },
+
+ getFirstAttribute(aProperty) {
+ // fail if no entry for this
+ if (!(aProperty in this.mPropertyMap)) {
+ return null;
+ }
+
+ return this.mPropertyMap[aProperty][0]?.replace(/\[(\d+)\]$/, "");
+ },
+
+ setAttributeList(aProperty, aAttributeList, aAllowInconsistencies) {
+ var attrs = aAttributeList.split(",");
+
+ // check to make sure this call won't allow multiple mappings to be
+ // created, if requested
+ if (!aAllowInconsistencies) {
+ for (var attr of attrs) {
+ if (attr in this.mAttrMap && this.mAttrMap[attr] != aProperty) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ }
+ }
+
+ // delete any attr mappings created by the existing property map entry
+ if (aProperty in this.mPropertyMap) {
+ for (attr of this.mPropertyMap[aProperty]) {
+ delete this.mAttrMap[attr];
+ }
+ }
+
+ // add these attrs to the attrmap
+ for (attr of attrs) {
+ this.mAttrMap[attr] = aProperty;
+ }
+
+ // add them to the property map
+ this.mPropertyMap[aProperty] = attrs;
+ },
+
+ getProperty(aAttribute) {
+ if (!(aAttribute in this.mAttrMap)) {
+ return null;
+ }
+
+ return this.mAttrMap[aAttribute];
+ },
+
+ getAllCardAttributes() {
+ var attrs = [];
+ for (let attrArray of Object.entries(this.mPropertyMap)) {
+ for (let attrName of attrArray) {
+ attrName = attrName.toString().replace(/\[(\d+)\]$/, "");
+ if (attrs.includes(attrName)) {
+ continue;
+ }
+ attrs.push(attrName);
+ }
+ }
+
+ if (!attrs.length) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ return attrs.join(",");
+ },
+
+ getAllCardProperties() {
+ var props = [];
+ for (var prop in this.mPropertyMap) {
+ props.push(prop);
+ }
+ return props;
+ },
+
+ setFromPrefs(aPrefBranchName) {
+ // get the right pref branch
+ let branch = Services.prefs.getBranch(aPrefBranchName + ".");
+
+ // get the list of children
+ var children = branch.getChildList("");
+
+ // do the actual sets
+ for (var child of children) {
+ this.setAttributeList(child, branch.getCharPref(child), true);
+ }
+
+ // ensure that everything is kosher
+ this.checkState();
+ },
+
+ setCardPropertiesFromLDAPMessage(aMessage, aCard) {
+ var cardValueWasSet = false;
+
+ var msgAttrs = aMessage.getAttributes();
+
+ // downcase the array for comparison
+ function toLower(a) {
+ return a.toLowerCase();
+ }
+ msgAttrs = msgAttrs.map(toLower);
+
+ // deal with each addressbook property
+ for (var prop in this.mPropertyMap) {
+ // go through the list of possible attrs in precedence order
+ for (var attr of this.mPropertyMap[prop]) {
+ attr = attr.toLowerCase();
+ // allow an index in attr
+ let valueIndex = 0;
+ const valueIndexMatch = /^(.+)\[(\d+)\]$/.exec(attr);
+ if (valueIndexMatch !== null) {
+ attr = valueIndexMatch[1];
+ valueIndex = parseInt(valueIndexMatch[2]);
+ }
+
+ // find the first attr that exists in this message
+ if (msgAttrs.includes(attr)) {
+ try {
+ var values = aMessage.getValues(attr);
+ // strip out the optional label from the labeledURI
+ if (attr == "labeleduri" && values[valueIndex]) {
+ var index = values[valueIndex].indexOf(" ");
+ if (index != -1) {
+ values[valueIndex] = values[valueIndex].substring(0, index);
+ }
+ }
+ aCard.setProperty(prop, values[valueIndex]);
+
+ cardValueWasSet = true;
+ break;
+ } catch (ex) {
+ // ignore any errors getting message values or setting card values
+ }
+ }
+ }
+ }
+
+ if (!cardValueWasSet) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ },
+
+ checkState() {
+ var attrsSeen = [];
+
+ for (var prop in this.mPropertyMap) {
+ let attrArray = this.mPropertyMap[prop];
+ for (var attr of attrArray) {
+ // multiple attributes that mapped to the empty string are permitted
+ if (!attr.length) {
+ continue;
+ }
+
+ // if we've seen this before, there's a problem
+ if (attrsSeen.includes(attr)) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ // remember that we've seen it now
+ attrsSeen.push(attr);
+ }
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAbLDAPAttributeMap"]),
+};
+
+function AbLDAPAttributeMapService() {}
+
+AbLDAPAttributeMapService.prototype = {
+ mAttrMaps: {},
+
+ getMapForPrefBranch(aPrefBranchName) {
+ // if we've already got this map, return it
+ if (aPrefBranchName in this.mAttrMaps) {
+ return this.mAttrMaps[aPrefBranchName];
+ }
+
+ // otherwise, try and create it
+ var attrMap = new AbLDAPAttributeMap();
+ attrMap.setFromPrefs("ldap_2.servers.default.attrmap");
+ attrMap.setFromPrefs(aPrefBranchName + ".attrmap");
+
+ // cache
+ this.mAttrMaps[aPrefBranchName] = attrMap;
+
+ // and return
+ return attrMap;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAbLDAPAttributeMapService"]),
+};
diff --git a/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..8df1c0f71e
--- /dev/null
+++ b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm
@@ -0,0 +1,364 @@
+/* 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 EXPORTED_SYMBOLS = ["AbLDAPAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// nsAbLDAPAutoCompleteResult
+// Derived from nsIAbAutoCompleteResult, provides a LDAP specific result
+// implementation.
+
+function nsAbLDAPAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+}
+
+nsAbLDAPAutoCompleteResult.prototype = {
+ _searchResults: null,
+ _commentColumn: "",
+
+ // nsIAutoCompleteResult
+
+ searchString: null,
+ searchResult: ACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getCommentAt(aIndex) {
+ return this._commentColumn;
+ },
+
+ getStyleAt(aIndex) {
+ return this.searchResult == ACR.RESULT_FAILURE
+ ? "remote-err"
+ : "remote-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsIAbAutoCompleteResult
+
+ getCardAt(aIndex) {
+ return this._searchResults[aIndex].card;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAutoCompleteResult",
+ "nsIAbAutoCompleteResult",
+ ]),
+};
+
+function AbLDAPAutoCompleteSearch() {
+ Services.obs.addObserver(this, "quit-application");
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+}
+
+AbLDAPAutoCompleteSearch.prototype = {
+ // A short-lived LDAP directory cache.
+ // To avoid recreating components as the user completes, we maintain the most
+ // recently used address book, nsAbLDAPDirectoryQuery and search context.
+ // However the cache is discarded if it has not been used for a minute.
+ // This is done to avoid problems with LDAP sessions timing out and hanging.
+ _query: null,
+ _book: null,
+ _attributes: null,
+ _context: -1,
+ _timer: null,
+
+ // The current search result.
+ _result: null,
+ // The listener to pass back results to.
+ _listener: null,
+
+ _parser: MailServices.headerParser,
+
+ applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]),
+
+ // Private methods
+
+ _checkDuplicate(card, emailAddress) {
+ var lcEmailAddress = emailAddress.toLocaleLowerCase();
+
+ return this._result._searchResults.some(function (result) {
+ return result.value.toLocaleLowerCase() == lcEmailAddress;
+ });
+ },
+
+ _addToResult(card, address) {
+ let mbox = this._parser.makeMailboxObject(
+ card.displayName,
+ card.isMailList
+ ? card.getProperty("Notes", "") || card.displayName
+ : address
+ );
+ if (!mbox.email) {
+ return;
+ }
+
+ let emailAddress = mbox.toString();
+
+ // If it is a duplicate, then just return and don't add it. The
+ // _checkDuplicate function deals with it all for us.
+ if (this._checkDuplicate(card, emailAddress)) {
+ return;
+ }
+
+ // Find out where to insert the card.
+ var insertPosition = 0;
+
+ // Next sort on full address
+ while (
+ insertPosition < this._result._searchResults.length &&
+ emailAddress > this._result._searchResults[insertPosition].value
+ ) {
+ ++insertPosition;
+ }
+
+ this._result._searchResults.splice(insertPosition, 0, {
+ value: emailAddress,
+ card,
+ });
+ },
+
+ // nsIObserver
+
+ observe(subject, topic, data) {
+ if (topic == "quit-application") {
+ Services.obs.removeObserver(this, "quit-application");
+ } else if (topic != "timer-callback") {
+ return;
+ }
+
+ // Force the individual query items to null, so that the memory
+ // gets collected straight away.
+ this.stopSearch();
+ this._book = null;
+ this._context = -1;
+ this._query = null;
+ this._attributes = null;
+ },
+
+ // nsIAutoCompleteSearch
+
+ startSearch(aSearchString, aParam, aPreviousResult, aListener) {
+ let params = JSON.parse(aParam) || {};
+ let applicable =
+ !("type" in params) || this.applicableHeaders.has(params.type);
+
+ this._result = new nsAbLDAPAutoCompleteResult(aSearchString);
+ aSearchString = aSearchString.toLocaleLowerCase();
+
+ // If the search string isn't value, or contains a comma, or the user
+ // hasn't enabled autocomplete, then just return no matches / or the
+ // result ignored.
+ // The comma check is so that we don't autocomplete against the user
+ // entering multiple addresses.
+ if (!applicable || !aSearchString || aSearchString.includes(",")) {
+ this._result.searchResult = ACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, this._result);
+ return;
+ }
+
+ // The rules here: If the current identity has a directoryServer set, then
+ // use that, otherwise, try the global preference instead.
+ var acDirURI = null;
+ var identity;
+
+ if ("idKey" in params) {
+ try {
+ identity = MailServices.accounts.getIdentity(params.idKey);
+ } catch (ex) {
+ console.error(
+ "Couldn't get specified identity, " +
+ "falling back to global settings"
+ );
+ }
+ }
+
+ // Does the current identity override the global preference?
+ if (identity && identity.overrideGlobalPref) {
+ acDirURI = identity.directoryServer;
+ } else if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) {
+ // Try the global one
+ acDirURI = Services.prefs.getCharPref(
+ "ldap_2.autoComplete.directoryServer"
+ );
+ }
+
+ if (!acDirURI || Services.io.offline) {
+ // No directory to search or we are offline, send a no match and return.
+ aListener.onSearchResult(this, this._result);
+ return;
+ }
+
+ this.stopSearch();
+
+ // If we don't already have a cached query for this URI, build a new one.
+ acDirURI = "moz-abldapdirectory://" + acDirURI;
+ if (!this._book || this._book.URI != acDirURI) {
+ this._query = Cc[
+ "@mozilla.org/addressbook/ldap-directory-query;1"
+ ].createInstance(Ci.nsIAbDirectoryQuery);
+ this._book = MailServices.ab
+ .getDirectory(acDirURI)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Create a minimal map just for the display name and primary email.
+ this._attributes = Cc[
+ "@mozilla.org/addressbook/ldap-attribute-map;1"
+ ].createInstance(Ci.nsIAbLDAPAttributeMap);
+ this._attributes.setAttributeList(
+ "DisplayName",
+ this._book.attributeMap.getAttributeList("DisplayName", {}),
+ true
+ );
+ this._attributes.setAttributeList(
+ "PrimaryEmail",
+ this._book.attributeMap.getAttributeList("PrimaryEmail", {}),
+ true
+ );
+ this._attributes.setAttributeList(
+ "SecondEmail",
+ this._book.attributeMap.getAttributeList("SecondEmail", {}),
+ true
+ );
+ }
+
+ this._result._commentColumn = this._book.dirName;
+ this._listener = aListener;
+ this._timer.init(this, 60000, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ var args = Cc[
+ "@mozilla.org/addressbook/directory/query-arguments;1"
+ ].createInstance(Ci.nsIAbDirectoryQueryArguments);
+
+ var filterTemplate = this._book.getStringValue(
+ "autoComplete.filterTemplate",
+ ""
+ );
+
+ // Use default value when preference is not set or it contains empty string
+ if (!filterTemplate) {
+ filterTemplate =
+ "(|(cn=*%v1*%v2-*)(mail=*%v*)(givenName=*%v1*)(sn=*%v*))";
+ }
+
+ // Create filter from filter template and search string
+ var ldapSvc = Cc["@mozilla.org/network/ldap-service;1"].getService(
+ Ci.nsILDAPService
+ );
+ var filter = ldapSvc.createFilter(
+ 1024,
+ filterTemplate,
+ "",
+ "",
+ "",
+ aSearchString
+ );
+ if (!filter) {
+ throw new Error(
+ "Filter string is empty, check if filterTemplate variable is valid in prefs.js."
+ );
+ }
+ args.typeSpecificArg = this._attributes;
+ args.querySubDirectories = true;
+ args.filter = filter;
+
+ // Start the actual search
+ this._context = this._query.doQuery(
+ this._book,
+ args,
+ this,
+ this._book.maxHits,
+ 0
+ );
+ },
+
+ stopSearch() {
+ if (this._listener) {
+ this._query.stopQuery(this._context);
+ this._listener = null;
+ }
+ },
+
+ // nsIAbDirSearchListener
+
+ onSearchFinished(status, complete, secInfo, location) {
+ if (!this._listener) {
+ return;
+ }
+
+ if (status == Cr.NS_OK) {
+ if (this._result.matchCount) {
+ this._result.searchResult = ACR.RESULT_SUCCESS;
+ this._result.defaultIndex = 0;
+ } else {
+ this._result.searchResult = ACR.RESULT_NOMATCH;
+ }
+ } else {
+ this._result.searchResult = ACR.RESULT_FAILURE;
+ this._result.defaultIndex = 0;
+ }
+ // const long queryResultStopped = 2;
+ // const long queryResultError = 3;
+ this._listener.onSearchResult(this, this._result);
+ this._listener = null;
+ },
+
+ onSearchFoundCard(aCard) {
+ if (!this._listener) {
+ return;
+ }
+
+ for (let emailAddress of aCard.emailAddresses) {
+ this._addToResult(aCard, emailAddress);
+ }
+
+ /* XXX autocomplete doesn't expect you to rearrange while searching
+ if (this._result.matchCount) {
+ this._result.searchResult = ACR.RESULT_SUCCESS_ONGOING;
+ } else {
+ this._result.searchResult = ACR.RESULT_NOMATCH_ONGOING;
+ }
+ this._listener.onSearchResult(this, this._result);
+ */
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIAutoCompleteSearch",
+ "nsIAbDirSearchListener",
+ ]),
+};
diff --git a/comm/mailnews/addrbook/src/components.conf b/comm/mailnews/addrbook/src/components.conf
new file mode 100644
index 0000000000..622fba5951
--- /dev/null
+++ b/comm/mailnews/addrbook/src/components.conf
@@ -0,0 +1,129 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{5b259db2-e451-4de9-8a6f-cfba91402973}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=mydomain"],
+ "jsm": "resource:///modules/AbAutoCompleteMyDomain.jsm",
+ "constructor": "AbAutoCompleteMyDomain",
+ },
+ {
+ "cid": "{2f946df9-114c-41fe-8899-81f10daf4f0c}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=addrbook"],
+ "jsm": "resource:///modules/AbAutoCompleteSearch.jsm",
+ "constructor": "AbAutoCompleteSearch",
+ },
+ {
+ "cid": "{127b341a-bdda-4270-85e1-edff569a9b85}",
+ "contract_ids": ["@mozilla.org/addressbook/ldap-attribute-map;1"],
+ "jsm": "resource:///modules/AbLDAPAttributeMap.jsm",
+ "constructor": "AbLDAPAttributeMap",
+ },
+ {
+ "cid": "{4ed7d5e1-8800-40da-9e78-c4f509d7ac5e}",
+ "contract_ids": ["@mozilla.org/addressbook/ldap-attribute-map-service;1"],
+ "jsm": "resource:///modules/AbLDAPAttributeMap.jsm",
+ "constructor": "AbLDAPAttributeMapService",
+ },
+ {
+ "cid": "{227e6482-fe9f-441f-9b7d-7b60375e7449}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=ldap"],
+ "jsm": "resource:///modules/AbLDAPAutoCompleteSearch.jsm",
+ "constructor": "AbLDAPAutoCompleteSearch",
+ },
+ {
+ "cid": "{cb7c67f8-0053-4072-89e9-501cbd1b35ab}",
+ "contract_ids": ["@mozilla.org/network/ldap-url;1"],
+ "type": "nsLDAPURL",
+ "headers": ["/comm/mailnews/addrbook/src/nsLDAPURL.h"],
+ },
+ {
+ "cid": "{2b722171-2cea-11d3-9e0b-00a0c92b5f0d}",
+ "contract_ids": ["@mozilla.org/addressbook/cardproperty;1"],
+ "type": "nsAbCardProperty",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbCardProperty.h"],
+ },
+ {
+ "cid": "{6fd8ec67-3965-11d3-a316-001083003d0c}",
+ "contract_ids": ["@mozilla.org/addressbook/directoryproperty;1"],
+ "type": "nsAbDirProperty",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbDirProperty.h"],
+ },
+ {
+ "cid": "{e7702d5a-99d8-4648-bab7-919ea29f30b6}",
+ "contract_ids": ["@mozilla.org/addressbook/services/addressCollector;1"],
+ "type": "nsAbAddressCollector",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbAddressCollector.h"],
+ },
+ {
+ "cid": "{f7dc2aeb-8e62-4750-965c-24b9e09ed8d2}",
+ "contract_ids": ["@mozilla.org/addressbook/directory/query-arguments;1"],
+ "type": "nsAbDirectoryQueryArguments",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h"],
+ },
+ {
+ "cid": "{ca1944a9-527e-4c77-895d-d0466dd41cf5}",
+ "contract_ids": ["@mozilla.org/boolean-expression/condition-string;1"],
+ "type": "nsAbBooleanConditionString",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbBooleanExpression.h"],
+ },
+ {
+ "cid": "{2c2e75c8-6f56-4a50-af1c-72af5d0e8d41}",
+ "contract_ids": ["@mozilla.org/boolean-expression/n-peer;1"],
+ "type": "nsAbBooleanExpression",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbBooleanExpression.h"],
+ },
+ {
+ "cid": "{e162e335-541b-43b4-aaea-fe591e240caf}",
+ "contract_ids": ["@mozilla.org/addressbook/directory-query/proxy;1"],
+ "type": "nsAbDirectoryQueryProxy",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h"],
+ },
+ {
+ "cid": "{db6f46da-8de3-478d-b539-801398656cf6}",
+ "contract_ids": ["@mozilla.org/addressbook/abldifservice;1"],
+ "type": "nsAbLDIFService",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbLDIFService.h"],
+ },
+]
+
+if buildconfig.substs["OS_ARCH"] == "Darwin":
+ Classes += [
+ {
+ "cid": "{83781cc6-c682-11d6-bdeb-0005024967b8}",
+ "contract_ids": [
+ "@mozilla.org/addressbook/directory;1?type=moz-abosxdirectory"
+ ],
+ "type": "nsAbOSXDirectory",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbOSXDirectory.h"],
+ },
+ {
+ "cid": "{89bbf582-c682-11d6-bc9d-0005024967b8}",
+ "contract_ids": ["@mozilla.org/addressbook/directory;1?type=moz-abosxcard"],
+ "type": "nsAbOSXCard",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbOSXCard.h"],
+ },
+ ]
+
+if buildconfig.substs["OS_ARCH"] == "WINNT" and buildconfig.substs["MOZ_MAPI_SUPPORT"]:
+ Classes += [
+ {
+ "cid": "{9cc57822-0599-4c47-a399-1c6fa185a05c}",
+ "contract_ids": [
+ "@mozilla.org/addressbook/directory;1?type=moz-aboutlookdirectory"
+ ],
+ "type": "nsAbOutlookDirectory",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h"],
+ },
+ {
+ "cid": "{558ccc0f-2681-4dac-a066-debd8d26faf6}",
+ "contract_ids": ["@mozilla.org/addressbook/outlookinterface;1"],
+ "type": "nsAbOutlookInterface",
+ "headers": ["/comm/mailnews/addrbook/src/nsAbOutlookInterface.h"],
+ },
+ ]
diff --git a/comm/mailnews/addrbook/src/moz.build b/comm/mailnews/addrbook/src/moz.build
new file mode 100644
index 0000000000..f7fbe7c58f
--- /dev/null
+++ b/comm/mailnews/addrbook/src/moz.build
@@ -0,0 +1,49 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "nsAbDirProperty.h",
+]
+
+SOURCES += [
+ "nsAbAddressCollector.cpp",
+ "nsAbBooleanExpression.cpp",
+ "nsAbCardProperty.cpp",
+ "nsAbDirectoryQuery.cpp",
+ "nsAbDirectoryQueryProxy.cpp",
+ "nsAbDirProperty.cpp",
+ "nsAbLDIFService.cpp",
+ "nsAbQueryStringToExpression.cpp",
+ "nsLDAPURL.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_MAPI_SUPPORT"]:
+ SOURCES += [
+ "nsAbOutlookDirectory.cpp",
+ "nsAbOutlookInterface.cpp",
+ "nsAbWinHelper.cpp",
+ "nsMapiAddressBook.cpp",
+ ]
+ LOCAL_INCLUDES += ["/comm/mailnews/mapi/include"]
+
+if CONFIG["OS_ARCH"] == "Darwin":
+ SOURCES += [
+ "nsAbOSXCard.mm",
+ "nsAbOSXDirectory.mm",
+ "nsAbOSXUtils.mm",
+ ]
+
+EXTRA_JS_MODULES += [
+ "AbAutoCompleteMyDomain.jsm",
+ "AbAutoCompleteSearch.jsm",
+ "AbLDAPAttributeMap.jsm",
+ "AbLDAPAutoCompleteSearch.jsm",
+]
+
+FINAL_LIBRARY = "mail"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp b/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp
new file mode 100644
index 0000000000..09018daf96
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsISimpleEnumerator.h"
+
+#include "nsIAbCard.h"
+#include "nsAbAddressCollector.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsString.h"
+#include "prmem.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIAbManager.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+
+using namespace mozilla::mailnews;
+
+NS_IMPL_ISUPPORTS(nsAbAddressCollector, nsIAbAddressCollector, nsIObserver)
+
+#define PREF_MAIL_COLLECT_ADDRESSBOOK "mail.collect_addressbook"
+
+nsAbAddressCollector::nsAbAddressCollector() {}
+
+nsAbAddressCollector::~nsAbAddressCollector() {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranchInt(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv))
+ pPrefBranchInt->RemoveObserver(PREF_MAIL_COLLECT_ADDRESSBOOK, this);
+}
+
+/**
+ * Returns the first card found with the specified email address. This
+ * returns an already addrefed pointer to the card if the card is found.
+ */
+already_AddRefed<nsIAbCard> nsAbAddressCollector::GetCardForAddress(
+ const char* aProperty, const nsACString& aEmailAddress,
+ nsIAbDirectory** aDirectory) {
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsTArray<RefPtr<nsIAbDirectory>> directories;
+ rv = abManager->GetDirectories(directories);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIAbCard> result;
+ uint32_t count = directories.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ // Some implementations may return NS_ERROR_NOT_IMPLEMENTED here,
+ // so just catch the value and continue.
+ if (NS_FAILED(directories[i]->GetCardFromProperty(
+ aProperty, aEmailAddress, false, getter_AddRefs(result)))) {
+ continue;
+ }
+
+ if (result) {
+ if (aDirectory) directories[i].forget(aDirectory);
+ return result.forget();
+ }
+ }
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsAbAddressCollector::CollectAddress(const nsACString& aAddresses,
+ bool aCreateCard) {
+ // If we've not got a valid directory, no point in going any further
+ if (!mDirectory) return NS_OK;
+
+ // note that we're now setting the whole recipient list,
+ // not just the pretty name of the first recipient.
+ nsTArray<nsCString> names;
+ nsTArray<nsCString> addresses;
+ ExtractAllAddresses(EncodedHeader(aAddresses), UTF16ArrayAdapter<>(names),
+ UTF16ArrayAdapter<>(addresses));
+ uint32_t numAddresses = names.Length();
+
+ for (uint32_t i = 0; i < numAddresses; i++) {
+ // Don't allow collection of addresses with no email address, it makes
+ // no sense. Whilst we should never get here in most normal cases, we
+ // should still be careful.
+ if (addresses[i].IsEmpty()) continue;
+
+ CollectSingleAddress(addresses[i], names[i], aCreateCard, false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbAddressCollector::CollectSingleAddress(const nsACString& aEmail,
+ const nsACString& aDisplayName,
+ bool aCreateCard,
+ bool aSkipCheckExisting) {
+ if (!mDirectory) return NS_OK;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIAbDirectory> originDirectory;
+ nsCOMPtr<nsIAbCard> card;
+ if (!aSkipCheckExisting) {
+ card = GetCardForAddress(kPriEmailProperty, aEmail,
+ getter_AddRefs(originDirectory));
+
+ // If a card has aEmail, but it's the secondary address, we don't want to
+ // update any properties, so just return.
+ if (!card) {
+ card = GetCardForAddress(k2ndEmailProperty, aEmail,
+ getter_AddRefs(originDirectory));
+ if (card) return NS_OK;
+ }
+ }
+
+ if (!card && (aCreateCard || aSkipCheckExisting)) {
+ card = do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ if (NS_SUCCEEDED(rv) && card) {
+ // Set up the fields for the new card.
+ SetNamesForCard(card, aDisplayName);
+ AutoCollectScreenName(card, aEmail);
+
+ if (NS_SUCCEEDED(card->SetPrimaryEmail(NS_ConvertUTF8toUTF16(aEmail)))) {
+ nsCOMPtr<nsIAbCard> addedCard;
+ rv = mDirectory->AddCard(card, getter_AddRefs(addedCard));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to add card");
+ }
+ }
+ } else if (card && originDirectory) {
+ // It could be that the origin directory is read-only, so don't try and
+ // write to it if it is.
+ bool readOnly;
+ rv = originDirectory->GetReadOnly(&readOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (readOnly) return NS_OK;
+
+ // address is already in the AB, so update the names
+ bool modifiedCard = false;
+
+ nsString displayName;
+ card->GetDisplayName(displayName);
+ // If we already have a display name, don't set the names on the card.
+ if (displayName.IsEmpty() && !aDisplayName.IsEmpty())
+ modifiedCard = SetNamesForCard(card, aDisplayName);
+
+ if (modifiedCard) originDirectory->ModifyCard(card);
+ }
+
+ return NS_OK;
+}
+
+// Works out the screen name to put on the card for some well-known addresses
+void nsAbAddressCollector::AutoCollectScreenName(nsIAbCard* aCard,
+ const nsACString& aEmail) {
+ if (!aCard) return;
+
+ int32_t atPos = aEmail.FindChar('@');
+ if (atPos == -1) return;
+
+ const nsACString& domain = Substring(aEmail, atPos + 1);
+
+ if (domain.IsEmpty()) return;
+ // username in
+ // username@aol.com (America Online)
+ // username@cs.com (Compuserve)
+ // username@netscape.net (Netscape webmail)
+ // are all AIM screennames. autocollect that info.
+ if (domain.EqualsLiteral("aol.com") || domain.EqualsLiteral("cs.com") ||
+ domain.EqualsLiteral("netscape.net"))
+ aCard->SetPropertyAsAUTF8String(kScreenNameProperty,
+ Substring(aEmail, 0, atPos));
+ else if (domain.EqualsLiteral("gmail.com") ||
+ domain.EqualsLiteral("googlemail.com"))
+ aCard->SetPropertyAsAUTF8String(kGtalkProperty,
+ Substring(aEmail, 0, atPos));
+}
+
+// Returns true if the card was modified successfully.
+bool nsAbAddressCollector::SetNamesForCard(nsIAbCard* aSenderCard,
+ const nsACString& aFullName) {
+ nsCString firstName;
+ nsCString lastName;
+ bool modifiedCard = false;
+
+ if (NS_SUCCEEDED(
+ aSenderCard->SetDisplayName(NS_ConvertUTF8toUTF16(aFullName))))
+ modifiedCard = true;
+
+ // Now split up the full name.
+ SplitFullName(nsCString(aFullName), firstName, lastName);
+
+ if (!firstName.IsEmpty() &&
+ NS_SUCCEEDED(aSenderCard->SetFirstName(NS_ConvertUTF8toUTF16(firstName))))
+ modifiedCard = true;
+
+ if (!lastName.IsEmpty() &&
+ NS_SUCCEEDED(aSenderCard->SetLastName(NS_ConvertUTF8toUTF16(lastName))))
+ modifiedCard = true;
+
+ if (modifiedCard) aSenderCard->SetPropertyAsBool("PreferDisplayName", false);
+
+ return modifiedCard;
+}
+
+// Splits the first and last name based on the space between them.
+void nsAbAddressCollector::SplitFullName(const nsCString& aFullName,
+ nsCString& aFirstName,
+ nsCString& aLastName) {
+ int index = aFullName.RFindChar(' ');
+ if (index != -1) {
+ aLastName = Substring(aFullName, index + 1);
+ aFirstName = Substring(aFullName, 0, index);
+ }
+}
+
+// Observes the collected address book pref in case it changes.
+NS_IMETHODIMP
+nsAbAddressCollector::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
+ if (!prefBranch) {
+ NS_ASSERTION(prefBranch, "failed to get prefs");
+ return NS_OK;
+ }
+
+ SetUpAbFromPrefs(prefBranch);
+ return NS_OK;
+}
+
+// Initialises the collector with the required items.
+nsresult nsAbAddressCollector::Init(void) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->AddObserver(PREF_MAIL_COLLECT_ADDRESSBOOK, this, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetUpAbFromPrefs(prefBranch);
+ return NS_OK;
+}
+
+// Performs the necessary changes to set up the collector for the specified
+// collected address book.
+void nsAbAddressCollector::SetUpAbFromPrefs(nsIPrefBranch* aPrefBranch) {
+ nsCString abURI;
+ aPrefBranch->GetCharPref(PREF_MAIL_COLLECT_ADDRESSBOOK, abURI);
+
+ if (abURI.IsEmpty()) abURI.AssignLiteral(kPersonalAddressbookUri);
+
+ if (abURI == mABURI) return;
+
+ mDirectory = nullptr;
+ mABURI = abURI;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = abManager->GetDirectory(mABURI, getter_AddRefs(mDirectory));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ bool readOnly;
+ rv = mDirectory->GetReadOnly(&readOnly);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // If the directory is read-only, we can't write to it, so just blank it out
+ // here, and warn because we shouldn't hit this (UI is wrong).
+ if (readOnly) {
+ NS_ERROR(
+ "Address Collection book preferences is set to a read-only book. "
+ "Address collection will not take place.");
+ mDirectory = nullptr;
+ }
+}
diff --git a/comm/mailnews/addrbook/src/nsAbAddressCollector.h b/comm/mailnews/addrbook/src/nsAbAddressCollector.h
new file mode 100644
index 0000000000..a2bfcaf802
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbAddressCollector.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsAbAddressCollector_H_
+#define _nsAbAddressCollector_H_
+
+#include "nsIAbAddressCollector.h"
+#include "nsCOMPtr.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "nsIObserver.h"
+#include "nsString.h"
+
+class nsIPrefBranch;
+
+class nsAbAddressCollector : public nsIAbAddressCollector, public nsIObserver {
+ public:
+ nsAbAddressCollector();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABADDRESSCOLLECTOR
+ NS_DECL_NSIOBSERVER
+
+ nsresult Init();
+
+ private:
+ virtual ~nsAbAddressCollector();
+ already_AddRefed<nsIAbCard> GetCardForAddress(const char* aProperty,
+ const nsACString& aEmailAddress,
+ nsIAbDirectory** aDirectory);
+ void AutoCollectScreenName(nsIAbCard* aCard, const nsACString& aEmail);
+ bool SetNamesForCard(nsIAbCard* aSenderCard, const nsACString& aFullName);
+ void SplitFullName(const nsCString& aFullName, nsCString& aFirstName,
+ nsCString& aLastName);
+ void SetUpAbFromPrefs(nsIPrefBranch* aPrefBranch);
+ nsCOMPtr<nsIAbDirectory> mDirectory;
+ nsCString mABURI;
+};
+
+#endif // _nsAbAddressCollector_H_
diff --git a/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp b/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp
new file mode 100644
index 0000000000..434c8756e6
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbBooleanExpression.h"
+#include "nsComponentManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsAbBooleanConditionString, nsIAbBooleanConditionString)
+
+nsAbBooleanConditionString::nsAbBooleanConditionString()
+ : mCondition(nsIAbBooleanConditionTypes::Exists) {}
+
+nsAbBooleanConditionString::~nsAbBooleanConditionString() {}
+
+/* attribute nsAbBooleanConditionType condition; */
+NS_IMETHODIMP nsAbBooleanConditionString::GetCondition(
+ nsAbBooleanConditionType* aCondition) {
+ if (!aCondition) return NS_ERROR_NULL_POINTER;
+
+ *aCondition = mCondition;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsAbBooleanConditionString::SetCondition(
+ nsAbBooleanConditionType aCondition) {
+ mCondition = aCondition;
+
+ return NS_OK;
+}
+
+/* attribute string name; */
+NS_IMETHODIMP nsAbBooleanConditionString::GetName(char** aName) {
+ if (!aName) return NS_ERROR_NULL_POINTER;
+
+ *aName = mName.IsEmpty() ? 0 : ToNewCString(mName);
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsAbBooleanConditionString::SetName(const char* aName) {
+ if (!aName) return NS_ERROR_NULL_POINTER;
+
+ mName = aName;
+
+ return NS_OK;
+}
+
+/* attribute wstring value; */
+NS_IMETHODIMP nsAbBooleanConditionString::GetValue(char16_t** aValue) {
+ if (!aValue) return NS_ERROR_NULL_POINTER;
+
+ *aValue = ToNewUnicode(mValue);
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsAbBooleanConditionString::SetValue(const char16_t* aValue) {
+ if (!aValue) return NS_ERROR_NULL_POINTER;
+
+ mValue = aValue;
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsAbBooleanExpression, nsIAbBooleanExpression)
+
+nsAbBooleanExpression::nsAbBooleanExpression()
+ : mOperation(nsIAbBooleanOperationTypes::AND) {}
+
+nsAbBooleanExpression::~nsAbBooleanExpression() {}
+
+/* attribute nsAbBooleanOperationType operation; */
+NS_IMETHODIMP nsAbBooleanExpression::GetOperation(
+ nsAbBooleanOperationType* aOperation) {
+ if (!aOperation) return NS_ERROR_NULL_POINTER;
+
+ *aOperation = mOperation;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsAbBooleanExpression::SetOperation(
+ nsAbBooleanOperationType aOperation) {
+ mOperation = aOperation;
+
+ return NS_OK;
+}
+
+/* attribute Array<nsISupports> expressions; */
+NS_IMETHODIMP nsAbBooleanExpression::GetExpressions(
+ nsTArray<RefPtr<nsISupports>>& aExpressions) {
+ aExpressions = mExpressions.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbBooleanExpression::SetExpressions(
+ const nsTArray<RefPtr<nsISupports>>& aExpressions) {
+ mExpressions = aExpressions.Clone();
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbBooleanExpression.h b/comm/mailnews/addrbook/src/nsAbBooleanExpression.h
new file mode 100644
index 0000000000..c739e0c2df
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbBooleanExpression.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsAbBooleanExpression_h__
+#define nsAbBooleanExpression_h__
+
+#include "nsIAbBooleanExpression.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIArray.h"
+
+class nsAbBooleanConditionString : public nsIAbBooleanConditionString {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABBOOLEANCONDITIONSTRING
+
+ nsAbBooleanConditionString();
+
+ protected:
+ virtual ~nsAbBooleanConditionString();
+ nsAbBooleanConditionType mCondition;
+ nsCString mName;
+ nsString mValue;
+};
+
+class nsAbBooleanExpression : public nsIAbBooleanExpression {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABBOOLEANEXPRESSION
+
+ nsAbBooleanExpression();
+
+ protected:
+ virtual ~nsAbBooleanExpression();
+ nsAbBooleanOperationType mOperation;
+ nsTArray<RefPtr<nsISupports>> mExpressions;
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbCardProperty.cpp b/comm/mailnews/addrbook/src/nsAbCardProperty.cpp
new file mode 100644
index 0000000000..cc34655afd
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbCardProperty.cpp
@@ -0,0 +1,1004 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbCardProperty.h"
+#include "nsIPrefService.h"
+#include "nsIAbDirectory.h"
+#include "plbase64.h"
+#include "nsIStringBundle.h"
+#include "plstr.h"
+#include "nsMsgUtils.h"
+#include "nsINetUtil.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsIAbManager.h"
+#include "nsIUUIDGenerator.h"
+#include "nsIMsgVCardService.h"
+#include "nsVariant.h"
+#include "nsIProperty.h"
+#include "nsCOMArray.h"
+#include "prmem.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Components.h"
+using namespace mozilla;
+
+#define PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST "mail.addr_book.lastnamefirst"
+
+const char sAddrbookProperties[] =
+ "chrome://messenger/locale/addressbook/addressBook.properties";
+
+enum EAppendType {
+ eAppendLine,
+ eAppendLabel,
+ eAppendCityStateZip,
+ eAppendUndefined
+};
+
+struct AppendItem {
+ const char* mColumn;
+ const char* mLabel;
+ EAppendType mAppendType;
+};
+
+static const AppendItem NAME_ATTRS_ARRAY[] = {
+ {kDisplayNameProperty, "propertyDisplayName", eAppendLabel},
+ {kNicknameProperty, "propertyNickname", eAppendLabel},
+ {kPriEmailProperty, "", eAppendLine},
+ {k2ndEmailProperty, "", eAppendLine}};
+
+static const AppendItem PHONE_ATTRS_ARRAY[] = {
+ {kWorkPhoneProperty, "propertyWork", eAppendLabel},
+ {kHomePhoneProperty, "propertyHome", eAppendLabel},
+ {kFaxProperty, "propertyFax", eAppendLabel},
+ {kPagerProperty, "propertyPager", eAppendLabel},
+ {kCellularProperty, "propertyCellular", eAppendLabel}};
+
+static const AppendItem HOME_ATTRS_ARRAY[] = {
+ {kHomeAddressProperty, "", eAppendLine},
+ {kHomeAddress2Property, "", eAppendLine},
+ {kHomeCityProperty, "", eAppendCityStateZip},
+ {kHomeCountryProperty, "", eAppendLine},
+ {kHomeWebPageProperty, "", eAppendLine}};
+
+static const AppendItem WORK_ATTRS_ARRAY[] = {
+ {kJobTitleProperty, "", eAppendLine},
+ {kDepartmentProperty, "", eAppendLine},
+ {kCompanyProperty, "", eAppendLine},
+ {kWorkAddressProperty, "", eAppendLine},
+ {kWorkAddress2Property, "", eAppendLine},
+ {kWorkCityProperty, "", eAppendCityStateZip},
+ {kWorkCountryProperty, "", eAppendLine},
+ {kWorkWebPageProperty, "", eAppendLine}};
+
+static const AppendItem CUSTOM_ATTRS_ARRAY[] = {
+ {kCustom1Property, "propertyCustom1", eAppendLabel},
+ {kCustom2Property, "propertyCustom2", eAppendLabel},
+ {kCustom3Property, "propertyCustom3", eAppendLabel},
+ {kCustom4Property, "propertyCustom4", eAppendLabel},
+ {kNotesProperty, "", eAppendLine}};
+
+static const AppendItem CHAT_ATTRS_ARRAY[] = {
+ {kGtalkProperty, "propertyGtalk", eAppendLabel},
+ {kAIMProperty, "propertyAIM", eAppendLabel},
+ {kYahooProperty, "propertyYahoo", eAppendLabel},
+ {kSkypeProperty, "propertySkype", eAppendLabel},
+ {kQQProperty, "propertyQQ", eAppendLabel},
+ {kMSNProperty, "propertyMSN", eAppendLabel},
+ {kICQProperty, "propertyICQ", eAppendLabel},
+ {kXMPPProperty, "propertyXMPP", eAppendLabel},
+ {kIRCProperty, "propertyIRC", eAppendLabel}};
+
+nsAbCardProperty::nsAbCardProperty() : m_IsMailList(false) {
+ // Initialize some default properties
+ SetPropertyAsUint32(kPopularityIndexProperty, 0);
+ // Uninitialized...
+ SetPropertyAsUint32(kLastModifiedDateProperty, 0);
+}
+
+nsAbCardProperty::~nsAbCardProperty(void) {}
+
+NS_IMPL_ISUPPORTS(nsAbCardProperty, nsIAbCard)
+
+NS_IMETHODIMP nsAbCardProperty::GetDirectoryUID(nsACString& dirUID) {
+ dirUID = m_directoryUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetDirectoryUID(const nsACString& aDirUID) {
+ m_directoryUID = aDirUID;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsAbCardProperty::GetIsMailList(bool* aIsMailList) {
+ *aIsMailList = m_IsMailList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetIsMailList(bool aIsMailList) {
+ m_IsMailList = aIsMailList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetMailListURI(char** aMailListURI) {
+ if (aMailListURI) {
+ *aMailListURI = ToNewCString(m_MailListURI);
+ return (*aMailListURI) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+ } else
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetMailListURI(const char* aMailListURI) {
+ if (aMailListURI) {
+ m_MailListURI = aMailListURI;
+ return NS_OK;
+ } else
+ return NS_ERROR_NULL_POINTER;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Property bag portion of nsAbCardProperty
+///////////////////////////////////////////////////////////////////////////////
+
+class nsAbSimpleProperty final : public nsIProperty {
+ public:
+ nsAbSimpleProperty(const nsACString& aName, nsIVariant* aValue)
+ : mName(aName), mValue(aValue) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROPERTY
+ protected:
+ ~nsAbSimpleProperty() {}
+ nsCString mName;
+ nsCOMPtr<nsIVariant> mValue;
+};
+
+NS_IMPL_ISUPPORTS(nsAbSimpleProperty, nsIProperty)
+
+NS_IMETHODIMP
+nsAbSimpleProperty::GetName(nsAString& aName) {
+ aName.Assign(NS_ConvertUTF8toUTF16(mName));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbSimpleProperty::GetValue(nsIVariant** aValue) {
+ NS_IF_ADDREF(*aValue = mValue);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetProperties(
+ nsTArray<RefPtr<nsIProperty>>& props) {
+ props.ClearAndRetainStorage();
+ props.SetCapacity(m_properties.Count());
+ for (auto iter = m_properties.Iter(); !iter.Done(); iter.Next()) {
+ props.AppendElement(new nsAbSimpleProperty(iter.Key(), iter.UserData()));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetProperty(const nsACString& name,
+ nsIVariant* defaultValue,
+ nsIVariant** value) {
+ if (!m_properties.Get(name, value)) NS_ADDREF(*value = defaultValue);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPropertyAsAString(const char* name,
+ nsAString& value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIVariant> variant;
+ return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant))
+ ? variant->GetAsAString(value)
+ : NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPropertyAsAUTF8String(const char* name,
+ nsACString& value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIVariant> variant;
+ return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant))
+ ? variant->GetAsAUTF8String(value)
+ : NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPropertyAsUint32(const char* name,
+ uint32_t* value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIVariant> variant;
+ return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant))
+ ? variant->GetAsUint32(value)
+ : NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPropertyAsBool(const char* name,
+ bool defaultValue,
+ bool* value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ *value = defaultValue;
+
+ nsCOMPtr<nsIVariant> variant;
+ return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant))
+ ? variant->GetAsBool(value)
+ : NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetProperty(const nsACString& name,
+ nsIVariant* value) {
+ m_properties.InsertOrUpdate(name, value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetPropertyAsAString(const char* name,
+ const nsAString& value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIWritableVariant> variant = new nsVariant();
+ variant->SetAsAString(value);
+ m_properties.InsertOrUpdate(nsDependentCString(name), variant);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetPropertyAsAUTF8String(
+ const char* name, const nsACString& value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIWritableVariant> variant = new nsVariant();
+ variant->SetAsAUTF8String(value);
+ m_properties.InsertOrUpdate(nsDependentCString(name), variant);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetPropertyAsUint32(const char* name,
+ uint32_t value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIWritableVariant> variant = new nsVariant();
+ variant->SetAsUint32(value);
+ m_properties.InsertOrUpdate(nsDependentCString(name), variant);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetPropertyAsBool(const char* name,
+ bool value) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ nsCOMPtr<nsIWritableVariant> variant = new nsVariant();
+ variant->SetAsBool(value);
+ m_properties.InsertOrUpdate(nsDependentCString(name), variant);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::DeleteProperty(const nsACString& name) {
+ m_properties.Remove(name);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetSupportsVCard(bool* aSupportsVCard) {
+ *aSupportsVCard = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetVCardProperties(
+ JS::MutableHandle<JS::Value> properties) {
+ properties.setNull();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetUID(nsACString& uid) {
+ nsAutoString aString;
+ nsresult rv = GetPropertyAsAString(kUIDProperty, aString);
+ if (NS_SUCCEEDED(rv)) {
+ uid = NS_ConvertUTF16toUTF8(aString);
+ return rv;
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(uid);
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetUID(const nsACString& aUID) {
+ nsAutoString aString;
+ nsresult rv = GetPropertyAsAString(kUIDProperty, aString);
+ if (NS_SUCCEEDED(rv)) {
+ if (!aString.Equals(NS_ConvertUTF8toUTF16(aUID))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ rv = SetPropertyAsAString(kUIDProperty, NS_ConvertUTF8toUTF16(aUID));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_directoryUID.IsEmpty()) {
+ // This card's not in a directory.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> directory = nullptr;
+ rv =
+ abManager->GetDirectoryFromUID(m_directoryUID, getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!directory) {
+ // This card claims to be in a directory, but we can't find it.
+ return NS_OK;
+ }
+
+ bool readOnly;
+ rv = directory->GetReadOnly(&readOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (readOnly) {
+ // The directory is read-only.
+ return NS_OK;
+ }
+
+ // Save the new UID so we can use it again in the future.
+ return directory->ModifyCard(this);
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetFirstName(nsAString& aString) {
+ nsresult rv = GetPropertyAsAString(kFirstNameProperty, aString);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ aString.Truncate();
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetFirstName(const nsAString& aString) {
+ return SetPropertyAsAString(kFirstNameProperty, aString);
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetLastName(nsAString& aString) {
+ nsresult rv = GetPropertyAsAString(kLastNameProperty, aString);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ aString.Truncate();
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetLastName(const nsAString& aString) {
+ return SetPropertyAsAString(kLastNameProperty, aString);
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetDisplayName(nsAString& aString) {
+ nsresult rv = GetPropertyAsAString(kDisplayNameProperty, aString);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ aString.Truncate();
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetDisplayName(const nsAString& aString) {
+ return SetPropertyAsAString(kDisplayNameProperty, aString);
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPrimaryEmail(nsAString& aString) {
+ nsresult rv = GetPropertyAsAString(kPriEmailProperty, aString);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ aString.Truncate();
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsAbCardProperty::SetPrimaryEmail(const nsAString& aString) {
+ return SetPropertyAsAString(kPriEmailProperty, aString);
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetEmailAddresses(
+ nsTArray<nsString>& aEmailAddresses) {
+ aEmailAddresses.Clear();
+
+ nsresult rv;
+ nsString emailAddress;
+
+ rv = GetPropertyAsAString(kPriEmailProperty, emailAddress);
+ if (rv != NS_ERROR_NOT_AVAILABLE && !emailAddress.IsEmpty()) {
+ aEmailAddresses.AppendElement(emailAddress);
+ }
+
+ rv = GetPropertyAsAString(k2ndEmailProperty, emailAddress);
+ if (rv != NS_ERROR_NOT_AVAILABLE && !emailAddress.IsEmpty()) {
+ aEmailAddresses.AppendElement(emailAddress);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::HasEmailAddress(const nsACString& aEmailAddress,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+
+ nsCString emailAddress;
+ nsresult rv = GetPropertyAsAUTF8String(kPriEmailProperty, emailAddress);
+ if (rv != NS_ERROR_NOT_AVAILABLE &&
+ emailAddress.Equals(aEmailAddress, nsCaseInsensitiveCStringComparator)) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ rv = GetPropertyAsAUTF8String(k2ndEmailProperty, emailAddress);
+ if (rv != NS_ERROR_NOT_AVAILABLE &&
+ emailAddress.Equals(aEmailAddress, nsCaseInsensitiveCStringComparator))
+ *aResult = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GetPhotoURL(nsAString& aPhotoURL) {
+ aPhotoURL.Truncate();
+ return NS_OK;
+}
+
+// This function may be overridden by derived classes for
+// nsAb*Card specific implementations.
+NS_IMETHODIMP nsAbCardProperty::Copy(nsIAbCard* srcCard) {
+ NS_ENSURE_ARG_POINTER(srcCard);
+
+ nsTArray<RefPtr<nsIProperty>> properties;
+ nsresult rv = srcCard->GetProperties(properties);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIProperty* property : properties) {
+ nsAutoString name;
+ property->GetName(name);
+ nsCOMPtr<nsIVariant> value;
+ property->GetValue(getter_AddRefs(value));
+
+ SetProperty(NS_ConvertUTF16toUTF8(name), value);
+ }
+
+ bool isMailList;
+ srcCard->GetIsMailList(&isMailList);
+ SetIsMailList(isMailList);
+
+ nsCString mailListURI;
+ srcCard->GetMailListURI(getter_Copies(mailListURI));
+ SetMailListURI(mailListURI.get());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::Equals(nsIAbCard* card, bool* result) {
+ *result = (card == this);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// The following methods are other views of a card
+////////////////////////////////////////////////////////////////////////////////
+
+// XXX: Use the category manager instead of this file to implement these
+NS_IMETHODIMP nsAbCardProperty::TranslateTo(const nsACString& type,
+ nsACString& result) {
+ if (type.EqualsLiteral("base64xml")) {
+ return ConvertToBase64EncodedXML(result);
+ } else if (type.EqualsLiteral("xml")) {
+ nsString utf16String;
+ nsresult rv = ConvertToXMLPrintData(utf16String);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result = NS_ConvertUTF16toUTF8(utf16String);
+ return NS_OK;
+ } else if (type.EqualsLiteral("vcard")) {
+ return ConvertToEscapedVCard(result);
+ }
+
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+nsresult nsAbCardProperty::ConvertToEscapedVCard(nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgVCardService> vCardService =
+ do_GetService("@mozilla.org/addressbook/msgvcardservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString result;
+ rv = vCardService->AbCardToEscapedVCard(this, result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aResult = NS_ConvertUTF16toUTF8(result);
+ return NS_OK;
+}
+
+nsresult nsAbCardProperty::ConvertToBase64EncodedXML(nsACString& result) {
+ nsresult rv;
+ nsString xmlStr;
+
+ xmlStr.AppendLiteral(
+ "<?xml version=\"1.0\"?>\n"
+ "<?xml-stylesheet type=\"text/css\" "
+ "href=\"chrome://messagebody/skin/abPrint.css\"?>\n"
+ "<directory>\n");
+
+ // Get Address Book string and set it as title of XML document
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ if (stringBundleService) {
+ rv = stringBundleService->CreateBundle(sAddrbookProperties,
+ getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsString addrBook;
+ rv = bundle->GetStringFromName("addressBook", addrBook);
+ if (NS_SUCCEEDED(rv)) {
+ xmlStr.AppendLiteral("<title xmlns=\"http://www.w3.org/1999/xhtml\">");
+ xmlStr.Append(addrBook);
+ xmlStr.AppendLiteral("</title>\n");
+ }
+ }
+ }
+
+ nsString xmlSubstr;
+ rv = ConvertToXMLPrintData(xmlSubstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ xmlStr.Append(xmlSubstr);
+ xmlStr.AppendLiteral("</directory>\n");
+
+ char* tmpRes =
+ PL_Base64Encode(NS_ConvertUTF16toUTF8(xmlStr).get(), 0, nullptr);
+ result.Assign(tmpRes);
+ PR_Free(tmpRes);
+ return NS_OK;
+}
+
+nsresult nsAbCardProperty::ConvertToXMLPrintData(nsAString& aXMLSubstr) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t generatedNameFormat;
+ rv = prefBranch->GetIntPref(PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST,
+ &generatedNameFormat);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(stringBundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = stringBundleService->CreateBundle(sAddrbookProperties,
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString generatedName;
+ rv = GenerateName(generatedNameFormat, bundle, generatedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozITXTToHTMLConv> conv =
+ do_CreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString xmlStr;
+ xmlStr.SetLength(
+ 4096); // to reduce allocations. should be enough for most cards
+ xmlStr.AssignLiteral("<GeneratedName>\n");
+
+ // use ScanTXT to convert < > & to safe values.
+ nsString safeText;
+ if (!generatedName.IsEmpty()) {
+ rv = conv->ScanTXT(generatedName, mozITXTToHTMLConv::kEntities, safeText);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (safeText.IsEmpty()) {
+ nsAutoString primaryEmail;
+ GetPrimaryEmail(primaryEmail);
+
+ // use ScanTXT to convert < > & to safe values.
+ rv = conv->ScanTXT(primaryEmail, mozITXTToHTMLConv::kEntities, safeText);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ xmlStr.Append(safeText);
+
+ xmlStr.AppendLiteral(
+ "</GeneratedName>\n"
+ "<table><tr><td>");
+
+ rv = AppendSection(NAME_ATTRS_ARRAY,
+ sizeof(NAME_ATTRS_ARRAY) / sizeof(AppendItem),
+ EmptyString(), bundle, conv, xmlStr);
+
+ xmlStr.AppendLiteral("</td></tr><tr><td>");
+
+ rv = AppendSection(PHONE_ATTRS_ARRAY,
+ sizeof(PHONE_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingPhone"_ns, bundle, conv, xmlStr);
+
+ if (!m_IsMailList) {
+ rv = AppendSection(CUSTOM_ATTRS_ARRAY,
+ sizeof(CUSTOM_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingOther"_ns, bundle, conv, xmlStr);
+ rv = AppendSection(CHAT_ATTRS_ARRAY,
+ sizeof(CHAT_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingChat"_ns, bundle, conv, xmlStr);
+ } else {
+ rv = AppendSection(CUSTOM_ATTRS_ARRAY,
+ sizeof(CUSTOM_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingDescription"_ns, bundle, conv, xmlStr);
+
+ xmlStr.AppendLiteral("<section><sectiontitle>");
+
+ nsString headingAddresses;
+ rv = bundle->GetStringFromName("headingAddresses", headingAddresses);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ xmlStr.Append(headingAddresses);
+ xmlStr.AppendLiteral("</sectiontitle>");
+
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> mailList = nullptr;
+ rv = abManager->GetDirectory(m_MailListURI, getter_AddRefs(mailList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIAbCard>> mailListAddresses;
+ rv = mailList->GetChildCards(mailListAddresses);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIAbCard* listCard : mailListAddresses) {
+ xmlStr.AppendLiteral("<PrimaryEmail>\n");
+
+ nsAutoString displayName;
+ rv = listCard->GetDisplayName(displayName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // use ScanTXT to convert < > & to safe values.
+ nsString safeText;
+ rv = conv->ScanTXT(displayName, mozITXTToHTMLConv::kEntities, safeText);
+ NS_ENSURE_SUCCESS(rv, rv);
+ xmlStr.Append(safeText);
+
+ xmlStr.AppendLiteral(" &lt;");
+
+ nsAutoString primaryEmail;
+ listCard->GetPrimaryEmail(primaryEmail);
+
+ // use ScanTXT to convert < > & to safe values.
+ nsString safeText2;
+ rv = conv->ScanTXT(primaryEmail, mozITXTToHTMLConv::kEntities, safeText2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ xmlStr.Append(safeText2);
+
+ xmlStr.AppendLiteral("&gt;</PrimaryEmail>\n");
+ }
+ xmlStr.AppendLiteral("</section>");
+ }
+
+ xmlStr.AppendLiteral("</td><td>");
+
+ rv = AppendSection(HOME_ATTRS_ARRAY,
+ sizeof(HOME_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingHome"_ns, bundle, conv, xmlStr);
+ rv = AppendSection(WORK_ATTRS_ARRAY,
+ sizeof(WORK_ATTRS_ARRAY) / sizeof(AppendItem),
+ u"headingWork"_ns, bundle, conv, xmlStr);
+
+ xmlStr.AppendLiteral("</td></tr></table>");
+
+ aXMLSubstr = xmlStr;
+
+ return NS_OK;
+}
+
+nsresult nsAbCardProperty::AppendSection(
+ const AppendItem* aArray, int16_t aCount, const nsString& aHeading,
+ nsIStringBundle* aBundle, mozITXTToHTMLConv* aConv, nsString& aResult) {
+ nsresult rv = NS_OK;
+
+ aResult.AppendLiteral("<section>");
+
+ nsString attrValue;
+ bool sectionIsEmpty = true;
+
+ int16_t i = 0;
+ for (i = 0; i < aCount; i++) {
+ rv = GetPropertyAsAString(aArray[i].mColumn, attrValue);
+ if (NS_SUCCEEDED(rv) && !attrValue.IsEmpty()) sectionIsEmpty = false;
+ }
+
+ if (!sectionIsEmpty && !aHeading.IsEmpty()) {
+ nsString heading;
+ rv = aBundle->GetStringFromName(NS_ConvertUTF16toUTF8(aHeading).get(),
+ heading);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aResult.AppendLiteral("<sectiontitle>");
+ aResult.Append(heading);
+ aResult.AppendLiteral("</sectiontitle>");
+ }
+
+ for (i = 0; i < aCount; i++) {
+ switch (aArray[i].mAppendType) {
+ case eAppendLine:
+ rv = AppendLine(aArray[i], aConv, aResult);
+ break;
+ case eAppendLabel:
+ rv = AppendLabel(aArray[i], aBundle, aConv, aResult);
+ break;
+ case eAppendCityStateZip:
+ rv = AppendCityStateZip(aArray[i], aBundle, aConv, aResult);
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("append item failed");
+ break;
+ }
+ }
+ aResult.AppendLiteral("</section>");
+
+ return rv;
+}
+
+nsresult nsAbCardProperty::AppendLine(const AppendItem& aItem,
+ mozITXTToHTMLConv* aConv,
+ nsString& aResult) {
+ NS_ENSURE_ARG_POINTER(aConv);
+
+ nsString attrValue;
+ nsresult rv = GetPropertyAsAString(aItem.mColumn, attrValue);
+
+ if (NS_FAILED(rv) || attrValue.IsEmpty()) return NS_OK;
+
+ aResult.Append(char16_t('<'));
+ aResult.Append(NS_ConvertUTF8toUTF16(aItem.mColumn));
+ aResult.Append(char16_t('>'));
+
+ // use ScanTXT to convert < > & to safe values.
+ nsString safeText;
+ rv = aConv->ScanTXT(attrValue, mozITXTToHTMLConv::kEntities, safeText);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResult.Append(safeText);
+
+ aResult.AppendLiteral("</");
+ aResult.Append(NS_ConvertUTF8toUTF16(aItem.mColumn));
+ aResult.Append(char16_t('>'));
+
+ return NS_OK;
+}
+
+nsresult nsAbCardProperty::AppendLabel(const AppendItem& aItem,
+ nsIStringBundle* aBundle,
+ mozITXTToHTMLConv* aConv,
+ nsString& aResult) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+
+ nsresult rv;
+ nsString label, attrValue;
+
+ rv = GetPropertyAsAString(aItem.mColumn, attrValue);
+
+ if (NS_FAILED(rv) || attrValue.IsEmpty()) return NS_OK;
+
+ rv = aBundle->GetStringFromName(aItem.mLabel, label);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aResult.AppendLiteral("<labelrow><label>");
+
+ aResult.Append(label);
+ aResult.AppendLiteral(": </label>");
+
+ rv = AppendLine(aItem, aConv, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aResult.AppendLiteral("</labelrow>");
+
+ return NS_OK;
+}
+
+nsresult nsAbCardProperty::AppendCityStateZip(const AppendItem& aItem,
+ nsIStringBundle* aBundle,
+ mozITXTToHTMLConv* aConv,
+ nsString& aResult) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+
+ nsresult rv;
+ AppendItem item;
+ const char *statePropName, *zipPropName;
+
+ if (strcmp(aItem.mColumn, kHomeCityProperty) == 0) {
+ statePropName = kHomeStateProperty;
+ zipPropName = kHomeZipCodeProperty;
+ } else {
+ statePropName = kWorkStateProperty;
+ zipPropName = kWorkZipCodeProperty;
+ }
+
+ nsAutoString cityResult, stateResult, zipResult;
+
+ rv = AppendLine(aItem, aConv, cityResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ item.mColumn = statePropName;
+ item.mLabel = "";
+ item.mAppendType = eAppendUndefined;
+
+ rv = AppendLine(item, aConv, stateResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ item.mColumn = zipPropName;
+
+ rv = AppendLine(item, aConv, zipResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString formattedString;
+
+ if (!cityResult.IsEmpty() && !stateResult.IsEmpty() && !zipResult.IsEmpty()) {
+ AutoTArray<nsString, 3> formatStrings = {cityResult, stateResult,
+ zipResult};
+ rv = aBundle->FormatStringFromName("cityAndStateAndZip", formatStrings,
+ formattedString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (!cityResult.IsEmpty() && !stateResult.IsEmpty() &&
+ zipResult.IsEmpty()) {
+ AutoTArray<nsString, 2> formatStrings = {cityResult, stateResult};
+ rv = aBundle->FormatStringFromName("cityAndStateNoZip", formatStrings,
+ formattedString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if ((!cityResult.IsEmpty() && stateResult.IsEmpty() &&
+ !zipResult.IsEmpty()) ||
+ (cityResult.IsEmpty() && !stateResult.IsEmpty() &&
+ !zipResult.IsEmpty())) {
+ AutoTArray<nsString, 2> formatStrings = {
+ cityResult.IsEmpty() ? stateResult : cityResult, zipResult};
+ rv = aBundle->FormatStringFromName("cityOrStateAndZip", formatStrings,
+ formattedString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ if (!cityResult.IsEmpty())
+ formattedString = cityResult;
+ else if (!stateResult.IsEmpty())
+ formattedString = stateResult;
+ else
+ formattedString = zipResult;
+ }
+
+ aResult.Append(formattedString);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GenerateName(int32_t aGenerateFormat,
+ nsIStringBundle* aBundle,
+ nsAString& aResult) {
+ aResult.Truncate();
+
+ // Cache the first and last names
+ nsAutoString firstName, lastName;
+ GetFirstName(firstName);
+ GetLastName(lastName);
+
+ // No need to check for aBundle present straight away, only do that if we're
+ // actually going to use it.
+ if (aGenerateFormat == GENERATE_DISPLAY_NAME)
+ GetDisplayName(aResult);
+ else if (lastName.IsEmpty())
+ aResult = firstName;
+ else if (firstName.IsEmpty())
+ aResult = lastName;
+ else {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle(aBundle);
+ if (!bundle) {
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(stringBundleService, NS_ERROR_UNEXPECTED);
+
+ rv = stringBundleService->CreateBundle(sAddrbookProperties,
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsString result;
+
+ if (aGenerateFormat == GENERATE_LAST_FIRST_ORDER) {
+ AutoTArray<nsString, 2> stringParams = {lastName, firstName};
+
+ rv =
+ bundle->FormatStringFromName("lastFirstFormat", stringParams, result);
+ } else {
+ AutoTArray<nsString, 2> stringParams = {firstName, lastName};
+
+ rv =
+ bundle->FormatStringFromName("firstLastFormat", stringParams, result);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aResult.Assign(result);
+ }
+
+ if (aResult.IsEmpty()) {
+ // The normal names have failed, does this card have a company name? If so,
+ // use that instead, because that is likely to be more meaningful than an
+ // email address.
+ //
+ // If this errors, the string isn't found and we'll fall into the next
+ // check.
+ (void)GetPropertyAsAString(kCompanyProperty, aResult);
+ }
+
+ if (aResult.IsEmpty()) {
+ // see bug #211078
+ // if there is no generated name at this point
+ // use the userid from the email address
+ // it is better than nothing.
+ GetPrimaryEmail(aResult);
+ int32_t index = aResult.FindChar('@');
+ if (index != -1) aResult.SetLength(index);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GeneratePhoneticName(bool aLastNameFirst,
+ nsAString& aResult) {
+ nsAutoString firstName, lastName;
+ GetPropertyAsAString(kPhoneticFirstNameProperty, firstName);
+ GetPropertyAsAString(kPhoneticLastNameProperty, lastName);
+
+ if (aLastNameFirst) {
+ aResult = lastName;
+ aResult += firstName;
+ } else {
+ aResult = firstName;
+ aResult += lastName;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbCardProperty::GenerateChatName(nsAString& aResult) {
+ aResult.Truncate();
+
+#define CHECK_CHAT_PROPERTY(aProtocol) \
+ if (NS_SUCCEEDED(GetPropertyAsAString(k##aProtocol##Property, aResult)) && \
+ !aResult.IsEmpty()) \
+ return NS_OK
+ CHECK_CHAT_PROPERTY(Gtalk);
+ CHECK_CHAT_PROPERTY(AIM);
+ CHECK_CHAT_PROPERTY(Yahoo);
+ CHECK_CHAT_PROPERTY(Skype);
+ CHECK_CHAT_PROPERTY(QQ);
+ CHECK_CHAT_PROPERTY(MSN);
+ CHECK_CHAT_PROPERTY(ICQ);
+ CHECK_CHAT_PROPERTY(XMPP);
+ CHECK_CHAT_PROPERTY(IRC);
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbCardProperty.h b/comm/mailnews/addrbook/src/nsAbCardProperty.h
new file mode 100644
index 0000000000..a724b592a6
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbCardProperty.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/********************************************************************************************************
+
+ Interface for representing Address Book Person Card Property
+
+*********************************************************************************************************/
+
+#ifndef nsAbCardProperty_h__
+#define nsAbCardProperty_h__
+
+#include "nsIAbCard.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+#include "nsInterfaceHashtable.h"
+#include "nsIVariant.h"
+
+class nsIStringBundle;
+class mozITXTToHTMLConv;
+struct AppendItem;
+
+/*
+ * Address Book Card Property
+ */
+
+class nsAbCardProperty : public nsIAbCard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABCARD
+
+ nsAbCardProperty();
+
+ protected:
+ virtual ~nsAbCardProperty();
+ bool m_IsMailList;
+ nsCString m_MailListURI;
+
+ // Store most of the properties here
+ nsInterfaceHashtable<nsCStringHashKey, nsIVariant> m_properties;
+
+ nsCString m_directoryUID;
+
+ private:
+ nsresult AppendSection(const AppendItem* aArray, int16_t aCount,
+ const nsString& aHeading, nsIStringBundle* aBundle,
+ mozITXTToHTMLConv* aConv, nsString& aResult);
+ nsresult AppendLine(const AppendItem& aItem, mozITXTToHTMLConv* aConv,
+ nsString& aResult);
+ nsresult AppendLabel(const AppendItem& aItem, nsIStringBundle* aBundle,
+ mozITXTToHTMLConv* aConv, nsString& aResult);
+ nsresult AppendCityStateZip(const AppendItem& aItem, nsIStringBundle* aBundle,
+ mozITXTToHTMLConv* aConv, nsString& aResult);
+
+ nsresult ConvertToBase64EncodedXML(nsACString& result);
+ nsresult ConvertToXMLPrintData(nsAString& result);
+ nsresult ConvertToEscapedVCard(nsACString& result);
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbDirProperty.cpp b/comm/mailnews/addrbook/src/nsAbDirProperty.cpp
new file mode 100644
index 0000000000..67860e424d
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirProperty.cpp
@@ -0,0 +1,573 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbDirProperty.h"
+#include "nsIAbCard.h"
+#include "nsIPrefService.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "prmem.h"
+#include "nsIAbManager.h"
+#include "nsArrayUtils.h"
+#include "nsIUUIDGenerator.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "mozilla/dom/Promise.h"
+
+using mozilla::ErrorResult;
+using mozilla::dom::Promise;
+using namespace mozilla;
+
+// From nsDirPrefs
+#define kDefaultPosition 1
+
+nsAbDirProperty::nsAbDirProperty(void)
+ : m_LastModifiedDate(0), mIsValidURI(false) {
+ m_IsMailList = false;
+ mUID = EmptyCString();
+}
+
+nsAbDirProperty::~nsAbDirProperty(void) {
+#if 0
+ // this code causes a regression #138647
+ // don't turn it on until you figure it out
+ if (m_AddressList) {
+ uint32_t count;
+ nsresult rv;
+ rv = m_AddressList->GetLength(&count);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Count failed");
+ int32_t i;
+ for (i = count - 1; i >= 0; i--)
+ m_AddressList->RemoveElementAt(i);
+ }
+#endif
+}
+
+NS_IMPL_ISUPPORTS(nsAbDirProperty, nsIAbDirectory, nsISupportsWeakReference)
+
+NS_IMETHODIMP nsAbDirProperty::GetPropertiesChromeURI(nsACString& aResult) {
+ aResult.AssignLiteral(
+ "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetDirName(nsAString& aDirName) {
+ if (m_DirPrefId.IsEmpty()) {
+ aDirName = m_ListDirName;
+ return NS_OK;
+ }
+
+ nsCString dirName;
+ nsresult rv = GetLocalizedStringValue("description", EmptyCString(), dirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // In TB 2 only some prefs had chrome:// URIs. We had code in place that would
+ // only get the localized string pref for the particular address books that
+ // were built-in.
+ // Additionally, nsIPrefBranch::getComplexValue will only get a non-user-set,
+ // non-locked pref value if it is a chrome:// URI and will get the string
+ // value at that chrome URI. This breaks extensions/autoconfig that want to
+ // set default pref values and allow users to change directory names.
+ //
+ // Now we have to support this, and so if for whatever reason we fail to get
+ // the localized version, then we try and get the non-localized version
+ // instead. If the string value is empty, then we'll just get the empty value
+ // back here.
+ if (dirName.IsEmpty()) {
+ rv = GetStringValue("description", EmptyCString(), dirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ CopyUTF8toUTF16(dirName, aDirName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetDirName(const nsAString& aDirName) {
+ if (m_DirPrefId.IsEmpty()) {
+ m_ListDirName = aDirName;
+ return NS_OK;
+ }
+
+ // Store the old value.
+ nsString oldDirName;
+ nsresult rv = GetDirName(oldDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save the new value
+ rv = SetLocalizedStringValue("description", NS_ConvertUTF16toUTF8(aDirName));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ // We inherit from nsIAbDirectory, so this static cast should be safe.
+ observerService->NotifyObservers(static_cast<nsIAbDirectory*>(this),
+ "addrbook-directory-updated", u"DirName");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetDirType(int32_t* aDirType) {
+ return GetIntValue("dirType", nsIAbManager::LDAP_DIRECTORY_TYPE, aDirType);
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetFileName(nsACString& aFileName) {
+ return GetStringValue("filename", EmptyCString(), aFileName);
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetUID(nsACString& aUID) {
+ nsresult rv = NS_OK;
+ if (!mUID.IsEmpty()) {
+ aUID = mUID;
+ return rv;
+ }
+ if (!m_IsMailList) {
+ rv = GetStringValue("uid", EmptyCString(), aUID);
+ if (!aUID.IsEmpty()) {
+ return rv;
+ }
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ aUID.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(aUID);
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetUID(const nsACString& aUID) {
+ mUID = aUID;
+ if (m_IsMailList) {
+ return NS_OK;
+ }
+ return SetStringValue("uid", aUID);
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetURI(nsACString& aURI) {
+ // XXX Should we complete this for Mailing Lists?
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetPosition(int32_t* aPosition) {
+ return GetIntValue("position", kDefaultPosition, aPosition);
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetLastModifiedDate(
+ uint32_t* aLastModifiedDate) {
+ NS_ENSURE_ARG_POINTER(aLastModifiedDate);
+ *aLastModifiedDate = m_LastModifiedDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetLastModifiedDate(uint32_t aLastModifiedDate) {
+ if (aLastModifiedDate) {
+ m_LastModifiedDate = aLastModifiedDate;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetListNickName(nsAString& aListNickName) {
+ aListNickName = m_ListNickName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetListNickName(const nsAString& aListNickName) {
+ m_ListNickName = aListNickName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetDescription(nsAString& aDescription) {
+ aDescription = m_Description;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetDescription(const nsAString& aDescription) {
+ m_Description = aDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetIsMailList(bool* aIsMailList) {
+ *aIsMailList = m_IsMailList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetIsMailList(bool aIsMailList) {
+ m_IsMailList = aIsMailList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::CopyMailList(nsIAbDirectory* srcList) {
+ SetIsMailList(true);
+
+ nsString str;
+ srcList->GetDirName(str);
+ SetDirName(str);
+ srcList->GetListNickName(str);
+ SetListNickName(str);
+ srcList->GetDescription(str);
+ SetDescription(str);
+
+ nsAutoCString uid;
+ srcList->GetUID(uid);
+ SetUID(uid);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::Init(const char* aURI) {
+ mURI = aURI;
+ mIsValidURI = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::CleanUp(JSContext* cx, Promise** retval) {
+ nsIGlobalObject* globalObject =
+ xpc::NativeGlobal(JS::CurrentGlobalOrNull(cx));
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ promise->MaybeResolveWithUndefined();
+ promise.forget(retval);
+
+ return NS_OK;
+}
+
+// nsIAbDirectory NOT IMPLEMENTED methods
+NS_IMETHODIMP
+nsAbDirProperty::GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& childList) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::GetChildCardCount(uint32_t* count) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::GetChildCards(nsTArray<RefPtr<nsIAbCard>>& childCards) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::DeleteDirectory(nsIAbDirectory* directory) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::HasCard(nsIAbCard* cards, bool* hasCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::HasDirectory(nsIAbDirectory* dir, bool* hasDir) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::HasMailListWithName(const nsAString& aName, bool* aHasList) {
+ NS_ENSURE_ARG_POINTER(aHasList);
+
+ *aHasList = false;
+ nsCOMPtr<nsIAbDirectory> aDir;
+ nsresult rv = GetMailListFromName(aName, getter_AddRefs(aDir));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDir) {
+ *aHasList = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::AddMailList(nsIAbDirectory* list,
+ nsIAbDirectory** addedList) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::EditMailListToDatabase(nsIAbCard* listCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::AddCard(nsIAbCard* childCard,
+ nsIAbCard** addedCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::ModifyCard(nsIAbCard* aModifiedCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::DeleteCards(
+ const nsTArray<RefPtr<nsIAbCard>>& aCards) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::DropCard(nsIAbCard* childCard,
+ bool needToCopyCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::CardForEmailAddress(
+ const nsACString& aEmailAddress, nsIAbCard** aAbCard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetCardFromProperty(const char* aProperty,
+ const nsACString& aValue,
+ bool caseSensitive,
+ nsIAbCard** result) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetCardsFromProperty(
+ const char* aProperty, const nsACString& aValue, bool caseSensitive,
+ nsTArray<RefPtr<nsIAbCard>>& result) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsAbDirProperty::GetMailListFromName(const nsAString& aName,
+ nsIAbDirectory** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+ bool supportsLists = false;
+ nsresult rv = GetSupportsMailingLists(&supportsLists);
+ if (NS_FAILED(rv) || !supportsLists) return NS_OK;
+
+ if (m_IsMailList) return NS_OK;
+
+ if (!m_AddressList) {
+ nsresult rv;
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ uint32_t listCount = 0;
+ rv = m_AddressList->GetLength(&listCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < listCount; i++) {
+ nsCOMPtr<nsIAbDirectory> listDir(do_QueryElementAt(m_AddressList, i, &rv));
+ if (NS_SUCCEEDED(rv) && listDir) {
+ nsAutoString listName;
+ rv = listDir->GetDirName(listName);
+ if (NS_SUCCEEDED(rv) && listName.Equals(aName)) {
+ listDir.forget(aResult);
+ return NS_OK;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetSupportsMailingLists(
+ bool* aSupportsMailingsLists) {
+ NS_ENSURE_ARG_POINTER(aSupportsMailingsLists);
+ // We don't currently support nested mailing lists, so only return true if
+ // we're not a mailing list.
+ *aSupportsMailingsLists = !m_IsMailList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetReadOnly(bool* aReadOnly) {
+ NS_ENSURE_ARG_POINTER(aReadOnly);
+ // Default is that we are writable. Any implementation that is read-only must
+ // override this method.
+ *aReadOnly = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetIsRemote(bool* aIsRemote) {
+ NS_ENSURE_ARG_POINTER(aIsRemote);
+ *aIsRemote = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetIsSecure(bool* aIsSecure) {
+ NS_ENSURE_ARG_POINTER(aIsSecure);
+ *aIsSecure = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::UseForAutocomplete(
+ const nsACString& aIdentityKey, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // Is local autocomplete enabled?
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->GetBoolPref("mail.enable_autocomplete", aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If autocomplete is generally enabled, check if it has been disabled
+ // explicitly for this directory.
+ if (*aResult) {
+ (void)GetBoolValue("enable_autocomplete", true, aResult);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetDirPrefId(nsACString& aDirPrefId) {
+ aDirPrefId = m_DirPrefId;
+ return NS_OK;
+}
+
+nsresult nsAbDirProperty::InitDirectoryPrefs() {
+ if (m_DirPrefId.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString realPrefId(m_DirPrefId);
+ realPrefId.Append('.');
+
+ return prefService->GetBranch(realPrefId.get(),
+ getter_AddRefs(m_DirectoryPrefs));
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetIntValue(const char* aName,
+ int32_t aDefaultValue,
+ int32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ if (NS_FAILED(m_DirectoryPrefs->GetIntPref(aName, aResult)))
+ *aResult = aDefaultValue;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetBoolValue(const char* aName,
+ bool aDefaultValue, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ if (NS_FAILED(m_DirectoryPrefs->GetBoolPref(aName, aResult)))
+ *aResult = aDefaultValue;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::GetStringValue(const char* aName,
+ const nsACString& aDefaultValue,
+ nsACString& aResult) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString value;
+
+ /* unfortunately, there may be some prefs out there which look like (null) */
+ if (NS_SUCCEEDED(m_DirectoryPrefs->GetCharPref(aName, value)) &&
+ !value.EqualsLiteral("(null"))
+ aResult = value;
+ else
+ aResult = aDefaultValue;
+
+ return NS_OK;
+}
+/*
+ * Get localized unicode string pref from properties file, convert into an
+ * UTF8 string since address book prefs store as UTF8 strings. So far there
+ * are 2 default prefs stored in addressbook.properties.
+ * "ldap_2.servers.pab.description"
+ * "ldap_2.servers.history.description"
+ */
+NS_IMETHODIMP nsAbDirProperty::GetLocalizedStringValue(
+ const char* aName, const nsACString& aDefaultValue, nsACString& aResult) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ nsString wvalue;
+ nsCOMPtr<nsIPrefLocalizedString> locStr;
+
+ nsresult rv = m_DirectoryPrefs->GetComplexValue(
+ aName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(locStr));
+ if (NS_SUCCEEDED(rv)) {
+ rv = locStr->ToString(getter_Copies(wvalue));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (wvalue.IsEmpty())
+ aResult = aDefaultValue;
+ else
+ CopyUTF16toUTF8(wvalue, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetIntValue(const char* aName, int32_t aValue) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ return m_DirectoryPrefs->SetIntPref(aName, aValue);
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetBoolValue(const char* aName, bool aValue) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ return m_DirectoryPrefs->SetBoolPref(aName, aValue);
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetStringValue(const char* aName,
+ const nsACString& aValue) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ return m_DirectoryPrefs->SetCharPref(aName, aValue);
+}
+
+NS_IMETHODIMP nsAbDirProperty::SetLocalizedStringValue(
+ const char* aName, const nsACString& aValue) {
+ if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs()))
+ return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefLocalizedString> locStr(
+ do_CreateInstance(NS_PREFLOCALIZEDSTRING_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = locStr->SetData(NS_ConvertUTF8toUTF16(aValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return m_DirectoryPrefs->SetComplexValue(
+ aName, NS_GET_IID(nsIPrefLocalizedString), locStr);
+}
+
+NS_IMETHODIMP nsAbDirProperty::Search(const nsAString& query,
+ const nsAString& searchString,
+ nsIAbDirSearchListener* listener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbDirProperty.h b/comm/mailnews/addrbook/src/nsAbDirProperty.h
new file mode 100644
index 0000000000..cf3bd59b68
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirProperty.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/********************************************************************************************************
+
+ Interface for representing Address Book Directory
+
+*********************************************************************************************************/
+
+#ifndef nsAbDirProperty_h__
+#define nsAbDirProperty_h__
+
+#include "nsIAbDirectory.h" /* include the interface we are going to support */
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIPrefBranch.h"
+#include "nsIMutableArray.h"
+#include "nsWeakReference.h"
+
+/*
+ * Address Book Directory
+ */
+
+class nsAbDirProperty : public nsIAbDirectory, public nsSupportsWeakReference {
+ public:
+ nsAbDirProperty(void);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABDIRECTORY
+
+ protected:
+ virtual ~nsAbDirProperty(void);
+
+ /**
+ * Initialise the directory prefs for this branch
+ */
+ nsresult InitDirectoryPrefs();
+
+ uint32_t m_LastModifiedDate;
+
+ nsString m_ListDirName;
+ nsString m_ListName;
+ nsString m_ListNickName;
+ nsString m_Description;
+ bool m_IsMailList;
+
+ nsCString mURI;
+ nsCString mUID;
+ bool mIsValidURI;
+
+ /*
+ * Note that any derived implementations should ensure that this item
+ * (m_DirPrefId) is correctly initialised correctly
+ */
+ nsCString m_DirPrefId; // ie,"ldap_2.servers.pab"
+
+ nsCOMPtr<nsIPrefBranch> m_DirectoryPrefs;
+ nsCOMPtr<nsIMutableArray> m_AddressList;
+};
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp
new file mode 100644
index 0000000000..f0f5d18b0c
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp
@@ -0,0 +1,421 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIAbCard.h"
+#include "nsAbDirectoryQuery.h"
+#include "nsAbDirectoryQueryProxy.h"
+#include "nsAbBooleanExpression.h"
+#include "nsComponentManagerUtils.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsIAbDirSearchListener.h"
+#include "nsISimpleEnumerator.h"
+#include "nsMsgUtils.h"
+#include "nsQueryObject.h"
+
+NS_IMPL_ISUPPORTS(nsAbDirectoryQuerySimpleBooleanExpression,
+ nsIAbBooleanExpression)
+
+nsAbDirectoryQuerySimpleBooleanExpression::
+ nsAbDirectoryQuerySimpleBooleanExpression()
+ : mOperation(nsIAbBooleanOperationTypes::AND) {}
+
+nsAbDirectoryQuerySimpleBooleanExpression::
+ ~nsAbDirectoryQuerySimpleBooleanExpression() {}
+
+/* attribute nsAbBooleanOperationType operation; */
+NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::GetOperation(
+ nsAbBooleanOperationType* aOperation) {
+ if (!aOperation) return NS_ERROR_NULL_POINTER;
+
+ *aOperation = mOperation;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::SetOperation(
+ nsAbBooleanOperationType aOperation) {
+ if (aOperation != nsIAbBooleanOperationTypes::AND &&
+ aOperation != nsIAbBooleanOperationTypes::OR)
+ return NS_ERROR_FAILURE;
+
+ mOperation = aOperation;
+
+ return NS_OK;
+}
+
+/* attribute Array<nsISupports> expressions; */
+NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::GetExpressions(
+ nsTArray<RefPtr<nsISupports>>& aExpressions) {
+ aExpressions = mExpressions.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::SetExpressions(
+ const nsTArray<RefPtr<nsISupports>>& aExpressions) {
+ // Ensure all the items are of the right type.
+ nsresult rv;
+ nsCOMPtr<nsIAbBooleanConditionString> queryExpression;
+ for (auto expression : aExpressions) {
+ queryExpression = do_QueryInterface(expression, &rv);
+ if (NS_FAILED(rv)) return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Values ok, so we can just save and return.
+ mExpressions = aExpressions.Clone();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsAbDirectoryQueryArguments, nsIAbDirectoryQueryArguments)
+
+nsAbDirectoryQueryArguments::nsAbDirectoryQueryArguments()
+ : mQuerySubDirectories(true) {}
+
+nsAbDirectoryQueryArguments::~nsAbDirectoryQueryArguments() {}
+
+/* attribute nsISupports matchItems; */
+NS_IMETHODIMP nsAbDirectoryQueryArguments::GetExpression(
+ nsISupports** aExpression) {
+ if (!aExpression) return NS_ERROR_NULL_POINTER;
+
+ NS_IF_ADDREF(*aExpression = mExpression);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::SetExpression(
+ nsISupports* aExpression) {
+ mExpression = aExpression;
+ return NS_OK;
+}
+
+/* attribute boolean querySubDirectories; */
+NS_IMETHODIMP nsAbDirectoryQueryArguments::GetQuerySubDirectories(
+ bool* aQuerySubDirectories) {
+ NS_ENSURE_ARG_POINTER(aQuerySubDirectories);
+ *aQuerySubDirectories = mQuerySubDirectories;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::SetQuerySubDirectories(
+ bool aQuerySubDirectories) {
+ mQuerySubDirectories = aQuerySubDirectories;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::GetTypeSpecificArg(
+ nsISupports** aArg) {
+ NS_ENSURE_ARG_POINTER(aArg);
+
+ NS_IF_ADDREF(*aArg = mTypeSpecificArg);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::SetTypeSpecificArg(
+ nsISupports* aArg) {
+ mTypeSpecificArg = aArg;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::GetFilter(nsACString& aFilter) {
+ aFilter.Assign(mFilter);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbDirectoryQueryArguments::SetFilter(
+ const nsACString& aFilter) {
+ mFilter.Assign(aFilter);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsAbDirectoryQueryPropertyValue,
+ nsIAbDirectoryQueryPropertyValue)
+
+nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue() {}
+
+nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue(
+ const char* aName, const char16_t* aValue) {
+ mName = aName;
+ mValue = aValue;
+}
+
+nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue(
+ const char* aName, nsISupports* aValueISupports) {
+ mName = aName;
+ mValueISupports = aValueISupports;
+}
+
+nsAbDirectoryQueryPropertyValue::~nsAbDirectoryQueryPropertyValue() {}
+
+/* read only attribute string name; */
+NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetName(char** aName) {
+ *aName = mName.IsEmpty() ? 0 : ToNewCString(mName);
+
+ return NS_OK;
+}
+
+/* read only attribute wstring value; */
+NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetValue(char16_t** aValue) {
+ *aValue = ToNewUnicode(mValue);
+ if (!(*aValue))
+ return NS_ERROR_OUT_OF_MEMORY;
+ else
+ return NS_OK;
+}
+
+/* readonly attribute nsISupports valueISupports; */
+NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetValueISupports(
+ nsISupports** aValueISupports) {
+ if (!mValueISupports) return NS_ERROR_NULL_POINTER;
+
+ NS_IF_ADDREF(*aValueISupports = mValueISupports);
+ return NS_OK;
+}
+
+/* Implementation file */
+NS_IMPL_ISUPPORTS(nsAbDirectoryQuery, nsIAbDirectoryQuery)
+
+nsAbDirectoryQuery::nsAbDirectoryQuery() {}
+
+nsAbDirectoryQuery::~nsAbDirectoryQuery() {}
+
+NS_IMETHODIMP nsAbDirectoryQuery::DoQuery(
+ nsIAbDirectory* aDirectory, nsIAbDirectoryQueryArguments* arguments,
+ nsIAbDirSearchListener* listener, int32_t resultLimit, int32_t timeOut,
+ int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(aDirectory);
+
+ nsCOMPtr<nsISupports> supportsExpression;
+ nsresult rv = arguments->GetExpression(getter_AddRefs(supportsExpression));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbBooleanExpression> expression(
+ do_QueryInterface(supportsExpression, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool doSubDirectories;
+ rv = arguments->GetQuerySubDirectories(&doSubDirectories);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = query(aDirectory, expression, listener, doSubDirectories, &resultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = listener->OnSearchFinished(rv, true, nullptr, ""_ns);
+
+ *_retval = 0;
+ return rv;
+}
+
+/* void stopQuery (in long contextID); */
+NS_IMETHODIMP nsAbDirectoryQuery::StopQuery(int32_t contextID) { return NS_OK; }
+
+nsresult nsAbDirectoryQuery::query(nsIAbDirectory* directory,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener,
+ bool doSubDirectories,
+ int32_t* resultLimit) {
+ if (*resultLimit == 0) return NS_OK;
+
+ nsresult rv = queryCards(directory, expression, listener, resultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*resultLimit != 0 && doSubDirectories) {
+ rv = queryChildren(directory, expression, listener, doSubDirectories,
+ resultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+nsresult nsAbDirectoryQuery::queryChildren(nsIAbDirectory* directory,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener,
+ bool doSubDirectories,
+ int32_t* resultLimit) {
+ nsTArray<RefPtr<nsIAbDirectory>> subDirectories;
+ nsresult rv = directory->GetChildNodes(subDirectories);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIAbDirectory* subDirectory : subDirectories) {
+ rv = query(subDirectory, expression, listener, doSubDirectories,
+ resultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult nsAbDirectoryQuery::queryCards(nsIAbDirectory* directory,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener,
+ int32_t* resultLimit) {
+ nsTArray<RefPtr<nsIAbCard>> cards;
+ nsresult rv = directory->GetChildCards(cards);
+ if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ return NS_OK;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIAbCard* card : cards) {
+ rv = matchCard(card, expression, listener, resultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*resultLimit == 0) return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsAbDirectoryQuery::matchCard(nsIAbCard* card,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener,
+ int32_t* resultLimit) {
+ bool matchFound = false;
+ nsresult rv = matchCardExpression(card, expression, &matchFound);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (matchFound) {
+ (*resultLimit)--;
+ rv = listener->OnSearchFoundCard(card);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+nsresult nsAbDirectoryQuery::matchCardExpression(
+ nsIAbCard* card, nsIAbBooleanExpression* expression, bool* result) {
+ nsAbBooleanOperationType operation;
+ nsresult rv = expression->GetOperation(&operation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsISupports>> childExpressions;
+ rv = expression->GetExpressions(childExpressions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t count = childExpressions.Length();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (operation == nsIAbBooleanOperationTypes::NOT && count > 1)
+ return NS_ERROR_FAILURE;
+
+ bool value = *result = false;
+ nsCOMPtr<nsIAbBooleanConditionString> childCondition;
+ nsCOMPtr<nsIAbBooleanExpression> childExpression;
+
+ for (uint32_t i = 0; i < count; i++) {
+ childCondition = do_QueryObject(childExpressions[i], &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = matchCardCondition(card, childCondition, &value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ childExpression = do_QueryObject(childExpressions[i], &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = matchCardExpression(card, childExpression, &value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else
+ return NS_ERROR_FAILURE;
+ }
+ if (operation == nsIAbBooleanOperationTypes::OR && value)
+ break;
+ else if (operation == nsIAbBooleanOperationTypes::AND && !value)
+ break;
+ else if (operation == nsIAbBooleanOperationTypes::NOT)
+ value = !value;
+ }
+ *result = value;
+
+ return NS_OK;
+}
+
+nsresult nsAbDirectoryQuery::matchCardCondition(
+ nsIAbCard* card, nsIAbBooleanConditionString* condition, bool* matchFound) {
+ nsAbBooleanConditionType conditionType;
+ nsresult rv = condition->GetCondition(&conditionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString name;
+ rv = condition->GetName(getter_Copies(name));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (name.Equals("card:nsIAbCard")) {
+ *matchFound = (conditionType == nsIAbBooleanConditionTypes::Exists);
+ return NS_OK;
+ }
+
+ nsString matchValue;
+ rv = condition->GetValue(getter_Copies(matchValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (name.EqualsLiteral("IsMailList")) {
+ bool isMailList;
+ rv = card->GetIsMailList(&isMailList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only equals is supported.
+ if (conditionType != nsIAbBooleanConditionTypes::Is)
+ return NS_ERROR_FAILURE;
+
+ *matchFound = isMailList ? matchValue.EqualsLiteral("TRUE")
+ : matchValue.EqualsLiteral("FALSE");
+ return NS_OK;
+ }
+
+ nsString value;
+ (void)card->GetPropertyAsAString(name.get(), value);
+
+ if (value.IsEmpty()) {
+ *matchFound = (conditionType == nsIAbBooleanConditionTypes::DoesNotExist)
+ ? true
+ : false;
+ return NS_OK;
+ }
+
+ /* TODO
+ * What about allowing choice between case insensitive
+ * and case sensitive comparisons?
+ *
+ */
+ switch (conditionType) {
+ case nsIAbBooleanConditionTypes::Exists:
+ *matchFound = true;
+ break;
+ case nsIAbBooleanConditionTypes::Contains:
+ *matchFound = CaseInsensitiveFindInReadable(matchValue, value);
+ break;
+ case nsIAbBooleanConditionTypes::DoesNotContain:
+ *matchFound = !CaseInsensitiveFindInReadable(matchValue, value);
+ break;
+ case nsIAbBooleanConditionTypes::Is:
+ *matchFound = value.Equals(matchValue, nsCaseInsensitiveStringComparator);
+ break;
+ case nsIAbBooleanConditionTypes::IsNot:
+ *matchFound =
+ !value.Equals(matchValue, nsCaseInsensitiveStringComparator);
+ break;
+ case nsIAbBooleanConditionTypes::BeginsWith:
+ *matchFound = StringBeginsWith(value, matchValue,
+ nsCaseInsensitiveStringComparator);
+ break;
+ case nsIAbBooleanConditionTypes::LessThan:
+ *matchFound =
+ Compare(value, matchValue, nsCaseInsensitiveStringComparator) < 0;
+ break;
+ case nsIAbBooleanConditionTypes::GreaterThan:
+ *matchFound =
+ Compare(value, matchValue, nsCaseInsensitiveStringComparator) > 0;
+ break;
+ case nsIAbBooleanConditionTypes::EndsWith:
+ *matchFound =
+ StringEndsWith(value, matchValue, nsCaseInsensitiveStringComparator);
+ break;
+ case nsIAbBooleanConditionTypes::SoundsLike:
+ case nsIAbBooleanConditionTypes::RegExp:
+ *matchFound = false;
+ break;
+ default:
+ *matchFound = false;
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h
new file mode 100644
index 0000000000..2c0e7b0654
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsAbDirectoryQuery_h__
+#define nsAbDirectoryQuery_h__
+
+#include "nsIAbDirectoryQuery.h"
+#include "nsIAbDirectory.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIArray.h"
+#include "nsIAbBooleanExpression.h"
+
+class nsAbDirectoryQuerySimpleBooleanExpression
+ : public nsIAbBooleanExpression {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABBOOLEANEXPRESSION
+
+ nsAbDirectoryQuerySimpleBooleanExpression();
+
+ private:
+ virtual ~nsAbDirectoryQuerySimpleBooleanExpression();
+
+ public:
+ nsTArray<RefPtr<nsISupports>> mExpressions;
+ nsAbBooleanOperationType mOperation;
+};
+
+class nsAbDirectoryQueryArguments : public nsIAbDirectoryQueryArguments {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABDIRECTORYQUERYARGUMENTS
+
+ nsAbDirectoryQueryArguments();
+
+ private:
+ virtual ~nsAbDirectoryQueryArguments();
+
+ protected:
+ nsCOMPtr<nsISupports> mExpression;
+ nsCOMPtr<nsISupports> mTypeSpecificArg;
+ bool mQuerySubDirectories;
+ nsCString mFilter;
+};
+
+class nsAbDirectoryQueryPropertyValue
+ : public nsIAbDirectoryQueryPropertyValue {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIABDIRECTORYQUERYPROPERTYVALUE
+
+ nsAbDirectoryQueryPropertyValue();
+ nsAbDirectoryQueryPropertyValue(const char* aName, const char16_t* aValue);
+ nsAbDirectoryQueryPropertyValue(const char* aName,
+ nsISupports* aValueISupports);
+
+ protected:
+ virtual ~nsAbDirectoryQueryPropertyValue();
+ nsCString mName;
+ nsString mValue;
+ nsCOMPtr<nsISupports> mValueISupports;
+};
+
+class nsAbDirectoryQuery : public nsIAbDirectoryQuery {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABDIRECTORYQUERY
+
+ nsAbDirectoryQuery();
+
+ protected:
+ virtual ~nsAbDirectoryQuery();
+ nsresult query(nsIAbDirectory* directory, nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener, bool doSubDirectories,
+ int32_t* resultLimit);
+ nsresult queryChildren(nsIAbDirectory* directory,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener,
+ bool doSubDirectories, int32_t* resultLimit);
+ nsresult queryCards(nsIAbDirectory* directory,
+ nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener, int32_t* resultLimit);
+ nsresult matchCard(nsIAbCard* card, nsIAbBooleanExpression* expression,
+ nsIAbDirSearchListener* listener, int32_t* resultLimit);
+ nsresult matchCardExpression(nsIAbCard* card,
+ nsIAbBooleanExpression* expression,
+ bool* result);
+ nsresult matchCardCondition(nsIAbCard* card,
+ nsIAbBooleanConditionString* condition,
+ bool* matchFound);
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp
new file mode 100644
index 0000000000..984746b82a
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbDirectoryQuery.h"
+#include "nsAbDirectoryQueryProxy.h"
+
+NS_IMPL_ISUPPORTS(nsAbDirectoryQueryProxy, nsIAbDirectoryQueryProxy,
+ nsIAbDirectoryQuery)
+
+nsAbDirectoryQueryProxy::nsAbDirectoryQueryProxy() : mInitiated(false) {}
+
+nsAbDirectoryQueryProxy::~nsAbDirectoryQueryProxy() {}
+
+/* void initiate (in nsIAbDirectory directory); */
+NS_IMETHODIMP nsAbDirectoryQueryProxy::Initiate() {
+ if (mInitiated) return NS_OK;
+
+ mDirectoryQuery = new nsAbDirectoryQuery();
+
+ mInitiated = true;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h
new file mode 100644
index 0000000000..542d58e6e3
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsAbDirectoryQueryProxy_h__
+#define nsAbDirectoryQueryProxy_h__
+
+#include "nsIAbDirectoryQueryProxy.h"
+#include "nsCOMPtr.h"
+
+class nsAbDirectoryQueryProxy : public nsIAbDirectoryQueryProxy {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSIABDIRECTORYQUERY(mDirectoryQuery->)
+ NS_DECL_NSIABDIRECTORYQUERYPROXY
+
+ nsAbDirectoryQueryProxy();
+
+ protected:
+ virtual ~nsAbDirectoryQueryProxy();
+ bool mInitiated;
+ nsCOMPtr<nsIAbDirectoryQuery> mDirectoryQuery;
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbLDIFService.cpp b/comm/mailnews/addrbook/src/nsAbLDIFService.cpp
new file mode 100644
index 0000000000..2d40bec9b2
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbLDIFService.cpp
@@ -0,0 +1,787 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "nsString.h"
+#include "nsAbLDIFService.h"
+#include "nsIFile.h"
+#include "nsILineInputStream.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "nsISeekableStream.h"
+#include "mdb.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "prprf.h"
+#include "nsCRTGlue.h"
+#include "nsTArray.h"
+#include "nsIComponentManager.h"
+
+#include <ctype.h>
+
+NS_IMPL_ISUPPORTS(nsAbLDIFService, nsIAbLDIFService)
+
+// If we get a line longer than 32K it's just toooooo bad!
+#define kTextAddressBufferSz (64 * 1024)
+
+nsAbLDIFService::nsAbLDIFService() {
+ mStoreLocAsHome = false;
+ mLFCount = 0;
+ mCRCount = 0;
+}
+
+nsAbLDIFService::~nsAbLDIFService() {}
+
+#define RIGHT2 0x03
+#define RIGHT4 0x0f
+#define CONTINUED_LINE_MARKER '\001'
+
+// XXX TODO fix me
+// use the NSPR base64 library. see plbase64.h
+// see bug #145367
+static unsigned char b642nib[0x80] = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f,
+ 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12,
+ 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24,
+ 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
+ 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff};
+
+NS_IMETHODIMP nsAbLDIFService::ImportLDIFFile(nsIAbDirectory* aDirectory,
+ nsIFile* aSrc,
+ bool aStoreLocAsHome,
+ uint32_t* aProgress) {
+ NS_ENSURE_ARG_POINTER(aSrc);
+ NS_ENSURE_ARG_POINTER(aDirectory);
+
+ mStoreLocAsHome = aStoreLocAsHome;
+
+ char buf[1024];
+ char* pBuf = &buf[0];
+ int32_t startPos = 0;
+ uint32_t len = 0;
+ nsTArray<int32_t> listPosArray; // where each list/group starts in ldif file
+ nsTArray<int32_t> listSizeArray; // size of the list/group info
+ int32_t savedStartPos = 0;
+ int32_t filePos = 0;
+ uint64_t bytesLeft = 0;
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Initialize the parser for a run...
+ mLdifLine.Truncate();
+
+ while (NS_SUCCEEDED(inputStream->Available(&bytesLeft)) && bytesLeft > 0) {
+ if (NS_SUCCEEDED(inputStream->Read(pBuf, sizeof(buf), &len)) && len > 0) {
+ startPos = 0;
+
+ while (NS_SUCCEEDED(GetLdifStringRecord(buf, len, startPos))) {
+ if (mLdifLine.Find("groupOfNames") == -1)
+ AddLdifRowToDatabase(aDirectory, false);
+ else {
+ // keep file position for mailing list
+ listPosArray.AppendElement(savedStartPos);
+ listSizeArray.AppendElement(filePos + startPos - savedStartPos);
+ ClearLdifRecordBuffer();
+ }
+ savedStartPos = filePos + startPos;
+ }
+ filePos += len;
+ if (aProgress) *aProgress = (uint32_t)filePos;
+ }
+ }
+ // last row
+ if (!mLdifLine.IsEmpty() && mLdifLine.Find("groupOfNames") == -1)
+ AddLdifRowToDatabase(aDirectory, false);
+
+ // mail Lists
+ int32_t i, pos;
+ uint32_t size;
+ int32_t listTotal = listPosArray.Length();
+ char* listBuf;
+ ClearLdifRecordBuffer(); // make sure the buffer is clean
+
+ nsCOMPtr<nsISeekableStream> seekableStream =
+ do_QueryInterface(inputStream, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (i = 0; i < listTotal; i++) {
+ pos = listPosArray[i];
+ size = listSizeArray[i];
+ if (NS_SUCCEEDED(
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, pos))) {
+ // Allocate enough space for the lists/groups as the size varies.
+ listBuf = (char*)PR_Malloc(size);
+ if (!listBuf) continue;
+ if (NS_SUCCEEDED(inputStream->Read(listBuf, size, &len)) && len > 0) {
+ startPos = 0;
+
+ while (NS_SUCCEEDED(GetLdifStringRecord(listBuf, len, startPos))) {
+ if (mLdifLine.Find("groupOfNames") != -1) {
+ AddLdifRowToDatabase(aDirectory, true);
+ if (NS_SUCCEEDED(
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, 0)))
+ break;
+ }
+ }
+ }
+ PR_FREEIF(listBuf);
+ }
+ }
+
+ rv = inputStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+/*
+ * str_parse_line - takes a line of the form "type:[:] value" and splits it
+ * into components "type" and "value". if a double colon separates type from
+ * value, then value is encoded in base 64, and parse_line un-decodes it
+ * (in place) before returning.
+ * in LDIF, non-ASCII data is treated as base64 encoded UTF-8
+ */
+
+nsresult nsAbLDIFService::str_parse_line(char* line, char** type, char** value,
+ int* vlen) const {
+ char *p, *s, *d, *byte, *stop;
+ char nib;
+ int i, b64;
+
+ /* skip any leading space */
+ while (isspace(*line)) {
+ line++;
+ }
+ *type = line;
+
+ for (s = line; *s && *s != ':'; s++)
+ ; /* NULL */
+ if (*s == '\0') {
+ return NS_ERROR_FAILURE;
+ }
+
+ /* trim any space between type and : */
+ for (p = s - 1; p > line && isspace(*p); p--) {
+ *p = '\0';
+ }
+ *s++ = '\0';
+
+ /* check for double : - indicates base 64 encoded value */
+ if (*s == ':') {
+ s++;
+ b64 = 1;
+ /* single : - normally encoded value */
+ } else {
+ b64 = 0;
+ }
+
+ /* skip space between : and value */
+ while (isspace(*s)) {
+ s++;
+ }
+
+ /* if no value is present, error out */
+ if (*s == '\0') {
+ return NS_ERROR_FAILURE;
+ }
+
+ /* check for continued line markers that should be deleted */
+ for (p = s, d = s; *p; p++) {
+ if (*p != CONTINUED_LINE_MARKER) *d++ = *p;
+ }
+ *d = '\0';
+
+ *value = s;
+ if (b64) {
+ stop = PL_strchr(s, '\0');
+ byte = s;
+ for (p = s, *vlen = 0; p < stop; p += 4, *vlen += 3) {
+ for (i = 0; i < 3; i++) {
+ if (p[i] != '=' && (p[i] & 0x80 || b642nib[p[i] & 0x7f] > 0x3f)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ /* first digit */
+ nib = b642nib[p[0] & 0x7f];
+ byte[0] = nib << 2;
+ /* second digit */
+ nib = b642nib[p[1] & 0x7f];
+ byte[0] |= nib >> 4;
+ byte[1] = (nib & RIGHT4) << 4;
+ /* third digit */
+ if (p[2] == '=') {
+ *vlen += 1;
+ break;
+ }
+ nib = b642nib[p[2] & 0x7f];
+ byte[1] |= nib >> 2;
+ byte[2] = (nib & RIGHT2) << 6;
+ /* fourth digit */
+ if (p[3] == '=') {
+ *vlen += 2;
+ break;
+ }
+ nib = b642nib[p[3] & 0x7f];
+ byte[2] |= nib;
+
+ byte += 3;
+ }
+ s[*vlen] = '\0';
+ } else {
+ *vlen = (int)(d - s);
+ }
+ return NS_OK;
+}
+
+/*
+ * str_getline - return the next "line" (minus newline) of input from a
+ * string buffer of lines separated by newlines, terminated by \n\n
+ * or \0. this routine handles continued lines, bundling them into
+ * a single big line before returning. if a line begins with a white
+ * space character, it is a continuation of the previous line. the white
+ * space character (nb: only one char), and preceding newline are changed
+ * into CONTINUED_LINE_MARKER chars, to be deleted later by the
+ * str_parse_line() routine above.
+ *
+ * it takes a pointer to a pointer to the buffer on the first call,
+ * which it updates and must be supplied on subsequent calls.
+ */
+
+char* nsAbLDIFService::str_getline(char** next) const {
+ char* lineStr;
+ char c;
+
+ if (*next == nullptr || **next == '\n' || **next == '\0') {
+ return (nullptr);
+ }
+
+ lineStr = *next;
+ while ((*next = PL_strchr(*next, '\n')) != NULL) {
+ c = *(*next + 1);
+ if (isspace(c) && c != '\n') {
+ **next = CONTINUED_LINE_MARKER;
+ *(*next + 1) = CONTINUED_LINE_MARKER;
+ } else {
+ *(*next)++ = '\0';
+ break;
+ }
+ }
+
+ return (lineStr);
+}
+
+nsresult nsAbLDIFService::GetLdifStringRecord(char* buf, int32_t len,
+ int32_t& stopPos) {
+ for (; stopPos < len; stopPos++) {
+ char c = buf[stopPos];
+
+ if (c == 0xA) {
+ mLFCount++;
+ } else if (c == 0xD) {
+ mCRCount++;
+ } else {
+ if (mLFCount == 0 && mCRCount == 0)
+ mLdifLine.Append(c);
+ else if ((mLFCount > 1) || (mCRCount > 2 && mLFCount) ||
+ (!mLFCount && mCRCount > 1)) {
+ return NS_OK;
+ } else if ((mLFCount == 1 || mCRCount == 1)) {
+ mLdifLine.Append('\n');
+ mLdifLine.Append(c);
+ mLFCount = 0;
+ mCRCount = 0;
+ }
+ }
+ }
+
+ if (((stopPos == len) && (mLFCount > 1)) || (mCRCount > 2 && mLFCount) ||
+ (!mLFCount && mCRCount > 1))
+ return NS_OK;
+
+ return NS_ERROR_FAILURE;
+}
+
+void nsAbLDIFService::AddLdifRowToDatabase(nsIAbDirectory* aDirectory,
+ bool bIsList) {
+ if (!aDirectory) {
+ return;
+ }
+
+ // If no data to process then reset CR/LF counters and return.
+ if (mLdifLine.IsEmpty()) {
+ mLFCount = 0;
+ mCRCount = 0;
+ return;
+ }
+
+ nsCOMPtr<nsIAbCard> newCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1");
+ nsTArray<nsCString> members;
+
+ char* cursor = ToNewCString(mLdifLine);
+ char* saveCursor = cursor; /* keep for deleting */
+ char* line = 0;
+ char* typeSlot = 0;
+ char* valueSlot = 0;
+ int length = 0; // the length of an ldif attribute
+ while ((line = str_getline(&cursor)) != nullptr) {
+ if (NS_SUCCEEDED(str_parse_line(line, &typeSlot, &valueSlot, &length))) {
+ nsAutoCString colType(typeSlot);
+ nsAutoCString column(valueSlot);
+
+ // 4.x exports attributes like "givenname",
+ // mozilla does "givenName" to be compliant with RFC 2798
+ ToLowerCase(colType);
+
+ if (colType.EqualsLiteral("member") ||
+ colType.EqualsLiteral("uniquemember")) {
+ members.AppendElement(column);
+ } else {
+ AddLdifColToDatabase(aDirectory, newCard, colType, column, bIsList);
+ }
+ } else
+ continue; // parse error: continue with next loop iteration
+ }
+ free(saveCursor);
+
+ if (bIsList) {
+ nsCOMPtr<nsIAbDirectory> newList =
+ do_CreateInstance("@mozilla.org/addressbook/directoryproperty;1");
+ newList->SetIsMailList(true);
+
+ nsAutoString temp;
+ newCard->GetDisplayName(temp);
+ newList->SetDirName(temp);
+ temp.Truncate();
+ newCard->GetPropertyAsAString(kNicknameProperty, temp);
+ newList->SetListNickName(temp);
+ temp.Truncate();
+ newCard->GetPropertyAsAString(kNotesProperty, temp);
+ newList->SetDescription(temp);
+
+ nsIAbDirectory* outList;
+ nsresult rv = aDirectory->AddMailList(newList, &outList);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ int32_t count = members.Length();
+ for (int32_t i = 0; i < count; ++i) {
+ nsAutoCString email;
+ int32_t emailPos = members[i].Find("mail=");
+ emailPos += strlen("mail=");
+ email = Substring(members[i], emailPos);
+
+ nsCOMPtr<nsIAbCard> emailCard;
+ aDirectory->CardForEmailAddress(email, getter_AddRefs(emailCard));
+ if (emailCard) {
+ nsIAbCard* outCard;
+ outList->AddCard(emailCard, &outCard);
+ }
+ }
+ } else {
+ nsIAbCard* outCard;
+ aDirectory->AddCard(newCard, &outCard);
+ }
+
+ // Clear buffer for next record
+ ClearLdifRecordBuffer();
+}
+
+void nsAbLDIFService::AddLdifColToDatabase(nsIAbDirectory* aDirectory,
+ nsIAbCard* newCard,
+ nsCString colType, nsCString column,
+ bool bIsList) {
+ nsString value = NS_ConvertUTF8toUTF16(column);
+
+ char firstByte = colType.get()[0];
+ switch (firstByte) {
+ case 'b':
+ if (colType.EqualsLiteral("birthyear"))
+ newCard->SetPropertyAsAString(kBirthYearProperty, value);
+ else if (colType.EqualsLiteral("birthmonth"))
+ newCard->SetPropertyAsAString(kBirthMonthProperty, value);
+ else if (colType.EqualsLiteral("birthday"))
+ newCard->SetPropertyAsAString(kBirthDayProperty, value);
+ break; // 'b'
+
+ case 'c':
+ if (colType.EqualsLiteral("cn") || colType.EqualsLiteral("commonname")) {
+ newCard->SetDisplayName(value);
+ } else if (colType.EqualsLiteral("c") ||
+ colType.EqualsLiteral("countryname")) {
+ if (mStoreLocAsHome)
+ newCard->SetPropertyAsAString(kHomeCountryProperty, value);
+ else
+ newCard->SetPropertyAsAString(kWorkCountryProperty, value);
+ }
+
+ else if (colType.EqualsLiteral("cellphone"))
+ newCard->SetPropertyAsAString(kCellularProperty, value);
+
+ else if (colType.EqualsLiteral("carphone"))
+ newCard->SetPropertyAsAString(kCellularProperty, value);
+
+ else if (colType.EqualsLiteral("custom1"))
+ newCard->SetPropertyAsAString(kCustom1Property, value);
+
+ else if (colType.EqualsLiteral("custom2"))
+ newCard->SetPropertyAsAString(kCustom2Property, value);
+
+ else if (colType.EqualsLiteral("custom3"))
+ newCard->SetPropertyAsAString(kCustom3Property, value);
+
+ else if (colType.EqualsLiteral("custom4"))
+ newCard->SetPropertyAsAString(kCustom4Property, value);
+
+ else if (colType.EqualsLiteral("company"))
+ newCard->SetPropertyAsAString(kCompanyProperty, value);
+ break; // 'c'
+
+ case 'd':
+ if (colType.EqualsLiteral("description"))
+ newCard->SetPropertyAsAString(kNotesProperty, value);
+
+ else if (colType.EqualsLiteral("department"))
+ newCard->SetPropertyAsAString(kDepartmentProperty, value);
+
+ else if (colType.EqualsLiteral("displayname"))
+ newCard->SetDisplayName(value);
+ break; // 'd'
+
+ case 'f':
+
+ if (colType.EqualsLiteral("fax") ||
+ colType.EqualsLiteral("facsimiletelephonenumber"))
+ newCard->SetPropertyAsAString(kFaxProperty, value);
+ break; // 'f'
+
+ case 'g':
+ if (colType.EqualsLiteral("givenname")) newCard->SetFirstName(value);
+ break; // 'g'
+
+ case 'h':
+ if (colType.EqualsLiteral("homephone"))
+ newCard->SetPropertyAsAString(kHomePhoneProperty, value);
+
+ else if (colType.EqualsLiteral("homestreet"))
+ newCard->SetPropertyAsAString(kHomeAddressProperty, value);
+
+ else if (colType.EqualsLiteral("homeurl"))
+ newCard->SetPropertyAsAString(kHomeWebPageProperty, value);
+ break; // 'h'
+
+ case 'l':
+ if (colType.EqualsLiteral("l") || colType.EqualsLiteral("locality")) {
+ if (mStoreLocAsHome)
+ newCard->SetPropertyAsAString(kHomeCityProperty, value);
+ else
+ newCard->SetPropertyAsAString(kWorkCityProperty, value);
+ }
+ // labeledURI contains a URI and, optionally, a label
+ // This will remove the label and place the URI as the work URL
+ else if (colType.EqualsLiteral("labeleduri")) {
+ int32_t index = column.FindChar(' ');
+ if (index != -1) column.SetLength(index);
+
+ newCard->SetPropertyAsAString(kWorkWebPageProperty,
+ NS_ConvertUTF8toUTF16(column));
+ }
+
+ break; // 'l'
+
+ case 'm':
+ if (colType.EqualsLiteral("mail"))
+ newCard->SetPrimaryEmail(value);
+
+ else if (colType.EqualsLiteral("mobile"))
+ newCard->SetPropertyAsAString(kCellularProperty, value);
+
+ else if (colType.EqualsLiteral("mozilla_aimscreenname"))
+ newCard->SetPropertyAsAString(kAIMProperty, value);
+
+ else if (colType.EqualsLiteral("mozillacustom1"))
+ newCard->SetPropertyAsAString(kCustom1Property, value);
+
+ else if (colType.EqualsLiteral("mozillacustom2"))
+ newCard->SetPropertyAsAString(kCustom2Property, value);
+
+ else if (colType.EqualsLiteral("mozillacustom3"))
+ newCard->SetPropertyAsAString(kCustom3Property, value);
+
+ else if (colType.EqualsLiteral("mozillacustom4"))
+ newCard->SetPropertyAsAString(kCustom4Property, value);
+
+ else if (colType.EqualsLiteral("mozillahomecountryname"))
+ newCard->SetPropertyAsAString(kHomeCountryProperty, value);
+
+ else if (colType.EqualsLiteral("mozillahomelocalityname"))
+ newCard->SetPropertyAsAString(kHomeCityProperty, value);
+
+ else if (colType.EqualsLiteral("mozillahomestate"))
+ newCard->SetPropertyAsAString(kHomeStateProperty, value);
+
+ else if (colType.EqualsLiteral("mozillahomestreet"))
+ newCard->SetPropertyAsAString(kHomeAddressProperty, value);
+
+ else if (colType.EqualsLiteral("mozillahomestreet2"))
+ newCard->SetPropertyAsAString(kHomeAddress2Property, value);
+
+ else if (colType.EqualsLiteral("mozillahomepostalcode"))
+ newCard->SetPropertyAsAString(kHomeZipCodeProperty, value);
+
+ else if (colType.EqualsLiteral("mozillahomeurl"))
+ newCard->SetPropertyAsAString(kHomeWebPageProperty, value);
+
+ else if (colType.EqualsLiteral("mozillanickname"))
+ newCard->SetPropertyAsAString(kNicknameProperty, value);
+
+ else if (colType.EqualsLiteral("mozillasecondemail"))
+ newCard->SetPropertyAsAString(k2ndEmailProperty, value);
+
+ else if (colType.EqualsLiteral("mozillaworkstreet2"))
+ newCard->SetPropertyAsAString(kWorkAddress2Property, value);
+
+ else if (colType.EqualsLiteral("mozillaworkurl"))
+ newCard->SetPropertyAsAString(kWorkWebPageProperty, value);
+
+ break; // 'm'
+
+ case 'n':
+ if (colType.EqualsLiteral("notes"))
+ newCard->SetPropertyAsAString(kNotesProperty, value);
+
+ else if (colType.EqualsLiteral("nscpaimscreenname") ||
+ colType.EqualsLiteral("nsaimid"))
+ newCard->SetPropertyAsAString(kAIMProperty, value);
+
+ break; // 'n'
+
+ case 'o':
+ if (colType.EqualsLiteral("objectclass"))
+ break;
+
+ else if (colType.EqualsLiteral("ou") || colType.EqualsLiteral("orgunit"))
+ newCard->SetPropertyAsAString(kDepartmentProperty, value);
+
+ else if (colType.EqualsLiteral("o")) // organization
+ newCard->SetPropertyAsAString(kCompanyProperty, value);
+
+ break; // 'o'
+
+ case 'p':
+ if (colType.EqualsLiteral("postalcode")) {
+ if (mStoreLocAsHome)
+ newCard->SetPropertyAsAString(kHomeZipCodeProperty, value);
+ else
+ newCard->SetPropertyAsAString(kWorkZipCodeProperty, value);
+ }
+
+ else if (colType.EqualsLiteral("postofficebox")) {
+ nsAutoCString workAddr1, workAddr2;
+ SplitCRLFAddressField(column, workAddr1, workAddr2);
+ newCard->SetPropertyAsAString(kWorkAddressProperty,
+ NS_ConvertUTF8toUTF16(workAddr1));
+ newCard->SetPropertyAsAString(kWorkAddress2Property,
+ NS_ConvertUTF8toUTF16(workAddr2));
+ } else if (colType.EqualsLiteral("pager") ||
+ colType.EqualsLiteral("pagerphone"))
+ newCard->SetPropertyAsAString(kPagerProperty, value);
+
+ break; // 'p'
+
+ case 'r':
+ if (colType.EqualsLiteral("region")) {
+ newCard->SetPropertyAsAString(kWorkStateProperty, value);
+ }
+
+ break; // 'r'
+
+ case 's':
+ if (colType.EqualsLiteral("sn") || colType.EqualsLiteral("surname"))
+ newCard->SetPropertyAsAString(kLastNameProperty, value);
+
+ else if (colType.EqualsLiteral("street"))
+ newCard->SetPropertyAsAString(kWorkAddressProperty, value);
+
+ else if (colType.EqualsLiteral("streetaddress")) {
+ nsAutoCString addr1, addr2;
+ SplitCRLFAddressField(column, addr1, addr2);
+ if (mStoreLocAsHome) {
+ newCard->SetPropertyAsAString(kHomeAddressProperty,
+ NS_ConvertUTF8toUTF16(addr1));
+ newCard->SetPropertyAsAString(kHomeAddress2Property,
+ NS_ConvertUTF8toUTF16(addr2));
+ } else {
+ newCard->SetPropertyAsAString(kWorkAddressProperty,
+ NS_ConvertUTF8toUTF16(addr1));
+ newCard->SetPropertyAsAString(kWorkAddress2Property,
+ NS_ConvertUTF8toUTF16(addr2));
+ }
+ } else if (colType.EqualsLiteral("st")) {
+ if (mStoreLocAsHome)
+ newCard->SetPropertyAsAString(kHomeStateProperty, value);
+ else
+ newCard->SetPropertyAsAString(kWorkStateProperty, value);
+ }
+
+ break; // 's'
+
+ case 't':
+ if (colType.EqualsLiteral("title"))
+ newCard->SetPropertyAsAString(kJobTitleProperty, value);
+
+ else if (colType.EqualsLiteral("telephonenumber")) {
+ newCard->SetPropertyAsAString(kWorkPhoneProperty, value);
+ }
+
+ break; // 't'
+
+ case 'w':
+ if (colType.EqualsLiteral("workurl"))
+ newCard->SetPropertyAsAString(kWorkWebPageProperty, value);
+
+ break; // 'w'
+
+ case 'x':
+ if (colType.EqualsLiteral("xmozillanickname")) {
+ newCard->SetPropertyAsAString(kNicknameProperty, value);
+ }
+
+ break; // 'x'
+
+ case 'z':
+ if (colType.EqualsLiteral("zip")) // alias for postalcode
+ {
+ if (mStoreLocAsHome)
+ newCard->SetPropertyAsAString(kHomeZipCodeProperty, value);
+ else
+ newCard->SetPropertyAsAString(kWorkZipCodeProperty, value);
+ }
+
+ break; // 'z'
+
+ default:
+ break; // default
+ }
+}
+
+void nsAbLDIFService::ClearLdifRecordBuffer() {
+ if (!mLdifLine.IsEmpty()) {
+ mLdifLine.Truncate();
+ mLFCount = 0;
+ mCRCount = 0;
+ }
+}
+
+// Some common ldif fields, it an ldif file has NONE of these entries
+// then it is most likely NOT an ldif file!
+static const char* const sLDIFFields[] = {"objectclass", "sn", "dn", "cn",
+ "givenName", "mail", nullptr};
+#define kMaxLDIFLen 14
+
+// Count total number of legal ldif fields and records in the first 100 lines of
+// the file and if the average legal ldif field is 3 or higher than it's a valid
+// ldif file.
+NS_IMETHODIMP nsAbLDIFService::IsLDIFFile(nsIFile* pSrc, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(pSrc);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = false;
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIInputStream> fileStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), pSrc);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(fileStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t lineLen = 0;
+ int32_t lineCount = 0;
+ int32_t ldifFields = 0; // total number of legal ldif fields.
+ char field[kMaxLDIFLen];
+ int32_t fLen = 0;
+ const char* pChar;
+ int32_t recCount = 0; // total number of records.
+ int32_t i;
+ bool gotLDIF = false;
+ bool more = true;
+ nsCString line;
+
+ while (more && NS_SUCCEEDED(rv) && (lineCount < 100)) {
+ rv = lineInputStream->ReadLine(line, &more);
+
+ if (NS_SUCCEEDED(rv) && more) {
+ pChar = line.get();
+ lineLen = line.Length();
+ if (!lineLen && gotLDIF) {
+ recCount++;
+ gotLDIF = false;
+ }
+
+ if (lineLen && (*pChar != ' ') && (*pChar != '\t')) {
+ fLen = 0;
+
+ while (lineLen && (fLen < (kMaxLDIFLen - 1)) && (*pChar != ':')) {
+ field[fLen] = *pChar;
+ pChar++;
+ fLen++;
+ lineLen--;
+ }
+
+ field[fLen] = 0;
+
+ if (lineLen && (*pChar == ':') && (fLen < (kMaxLDIFLen - 1))) {
+ // see if this is an ldif field (case insensitive)?
+ i = 0;
+ while (sLDIFFields[i]) {
+ if (!PL_strcasecmp(sLDIFFields[i], field)) {
+ ldifFields++;
+ gotLDIF = true;
+ break;
+ }
+ i++;
+ }
+ }
+ }
+ }
+ lineCount++;
+ }
+
+ // If we just saw ldif address, increment recCount.
+ if (gotLDIF) recCount++;
+
+ rv = fileStream->Close();
+
+ if (recCount > 1) ldifFields /= recCount;
+
+ // If the average field number >= 3 then it's a good ldif file.
+ if (ldifFields >= 3) {
+ *_retval = true;
+ }
+
+ return rv;
+}
+
+void nsAbLDIFService::SplitCRLFAddressField(nsCString& inputAddress,
+ nsCString& outputLine1,
+ nsCString& outputLine2) const {
+ int32_t crlfPos = inputAddress.Find("\r\n");
+ if (crlfPos != -1) {
+ outputLine1 = Substring(inputAddress, 0, crlfPos);
+ outputLine2 = Substring(inputAddress, crlfPos + 2);
+ } else
+ outputLine1.Assign(inputAddress);
+}
diff --git a/comm/mailnews/addrbook/src/nsAbLDIFService.h b/comm/mailnews/addrbook/src/nsAbLDIFService.h
new file mode 100644
index 0000000000..84d892fdff
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbLDIFService.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef __nsAbLDIFService_h
+#define __nsAbLDIFService_h
+
+#include "nsIAbLDIFService.h"
+#include "nsCOMPtr.h"
+
+class nsAbLDIFService : public nsIAbLDIFService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABLDIFSERVICE
+
+ nsAbLDIFService();
+
+ private:
+ virtual ~nsAbLDIFService();
+ nsresult str_parse_line(char* line, char** type, char** value,
+ int* vlen) const;
+ char* str_getline(char** next) const;
+ nsresult GetLdifStringRecord(char* buf, int32_t len, int32_t& stopPos);
+ void AddLdifRowToDatabase(nsIAbDirectory* aDirectory, bool aIsList);
+ void AddLdifColToDatabase(nsIAbDirectory* aDirectory, nsIAbCard* newCard,
+ nsCString colType, nsCString column, bool bIsList);
+ void ClearLdifRecordBuffer();
+ void SplitCRLFAddressField(nsCString& inputAddress, nsCString& outputLine1,
+ nsCString& outputLine2) const;
+
+ bool mStoreLocAsHome;
+ nsCString mLdifLine;
+ int32_t mLFCount;
+ int32_t mCRCount;
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbOSXCard.h b/comm/mailnews/addrbook/src/nsAbOSXCard.h
new file mode 100644
index 0000000000..2c0650fd64
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXCard.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsAbOSXCard_h___
+#define nsAbOSXCard_h___
+
+#include "mozilla/Attributes.h"
+#include "nsAbCardProperty.h"
+
+#define NS_ABOSXCARD_URI_PREFIX "moz-abosxcard://"
+
+#define NS_IABOSXCARD_IID \
+ { \
+ 0xa7e5b697, 0x772d, 0x4fb5, { \
+ 0x81, 0x16, 0x23, 0xb7, 0x5a, 0xac, 0x94, 0x56 \
+ } \
+ }
+
+class nsIAbOSXCard : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABOSXCARD_IID)
+
+ virtual nsresult Init(const char* aUri) = 0;
+ virtual nsresult Update(bool aNotify) = 0;
+ virtual nsresult GetURI(nsACString& aURI) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbOSXCard, NS_IABOSXCARD_IID)
+
+class nsAbOSXCard : public nsAbCardProperty, public nsIAbOSXCard {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsresult Update(bool aNotify) override;
+ nsresult GetURI(nsACString& aURI) override;
+ nsresult Init(const char* aUri) override;
+ NS_IMETHOD GetUID(nsACString& uid) override;
+ NS_IMETHOD SetUID(const nsACString& aUID) override;
+ // this is needed so nsAbOSXUtils.mm can get at nsAbCardProperty
+ friend class nsAbOSXUtils;
+
+ private:
+ nsCString mURI;
+ nsCString mUID;
+
+ virtual ~nsAbOSXCard() {}
+};
+
+#endif // nsAbOSXCard_h___
diff --git a/comm/mailnews/addrbook/src/nsAbOSXCard.mm b/comm/mailnews/addrbook/src/nsAbOSXCard.mm
new file mode 100644
index 0000000000..ab77242490
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXCard.mm
@@ -0,0 +1,353 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbOSXCard.h"
+#include "nsAbOSXDirectory.h"
+#include "nsAbOSXUtils.h"
+#include "nsIAbManager.h"
+#include "nsObjCExceptions.h"
+#include "nsServiceManagerUtils.h"
+
+#include <AddressBook/AddressBook.h>
+
+NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXCard, nsAbCardProperty, nsIAbOSXCard)
+
+#ifdef DEBUG
+static ABPropertyType GetPropertType(ABRecord* aCard, NSString* aProperty) {
+ ABPropertyType propertyType = kABErrorInProperty;
+ if ([aCard isKindOfClass:[ABPerson class]])
+ propertyType = [ABPerson typeOfProperty:aProperty];
+ else if ([aCard isKindOfClass:[ABGroup class]])
+ propertyType = [ABGroup typeOfProperty:aProperty];
+ return propertyType;
+}
+#endif
+
+static void SetStringProperty(nsAbOSXCard* aCard, const nsString& aValue, const char* aMemberName,
+ bool aNotify, nsIAbManager* aAbManager) {
+ nsString oldValue;
+ nsresult rv = aCard->GetPropertyAsAString(aMemberName, oldValue);
+ if (NS_FAILED(rv)) oldValue.Truncate();
+
+ if (!aNotify) {
+ aCard->SetPropertyAsAString(aMemberName, aValue);
+ } else if (!oldValue.Equals(aValue)) {
+ aCard->SetPropertyAsAString(aMemberName, aValue);
+ }
+}
+
+static void SetStringProperty(nsAbOSXCard* aCard, NSString* aValue, const char* aMemberName,
+ bool aNotify, nsIAbManager* aAbManager) {
+ nsAutoString value;
+ if (aValue) AppendToString(aValue, value);
+
+ SetStringProperty(aCard, value, aMemberName, aNotify, aAbManager);
+}
+
+static void MapStringProperty(nsAbOSXCard* aCard, ABRecord* aOSXCard, NSString* aProperty,
+ const char* aMemberName, bool aNotify, nsIAbManager* aAbManager) {
+ NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
+ NS_ASSERTION(GetPropertType(aOSXCard, aProperty) == kABStringProperty, "Wrong type!");
+
+ SetStringProperty(aCard, [aOSXCard valueForProperty:aProperty], aMemberName, aNotify, aAbManager);
+}
+
+static ABMutableMultiValue* GetMultiValue(ABRecord* aCard, NSString* aProperty) {
+ NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
+ NS_ASSERTION(GetPropertType(aCard, aProperty) & kABMultiValueMask, "Wrong type!");
+
+ return [aCard valueForProperty:aProperty];
+}
+
+static void MapDate(nsAbOSXCard* aCard, NSDate* aDate, const char* aYearPropName,
+ const char* aMonthPropName, const char* aDayPropName, bool aNotify,
+ nsIAbManager* aAbManager) {
+ // XXX Should we pass a format and timezone?
+ NSCalendarDate* date = [aDate dateWithCalendarFormat:nil timeZone:nil];
+
+ nsAutoString value;
+ value.AppendInt(static_cast<int32_t>([date yearOfCommonEra]));
+ SetStringProperty(aCard, value, aYearPropName, aNotify, aAbManager);
+ value.Truncate();
+ value.AppendInt(static_cast<int32_t>([date monthOfYear]));
+ SetStringProperty(aCard, value, aMonthPropName, aNotify, aAbManager);
+ value.Truncate();
+ value.AppendInt(static_cast<int32_t>([date dayOfMonth]));
+ SetStringProperty(aCard, value, aDayPropName, aNotify, aAbManager);
+}
+
+static bool MapMultiValue(nsAbOSXCard* aCard, ABRecord* aOSXCard, const nsAbOSXPropertyMap& aMap,
+ bool aNotify, nsIAbManager* aAbManager) {
+ ABMultiValue* value = GetMultiValue(aOSXCard, aMap.mOSXProperty);
+ if (value) {
+ unsigned int j;
+ unsigned int count = [value count];
+ for (j = 0; j < count; ++j) {
+ if ([[value labelAtIndex:j] isEqualToString:aMap.mOSXLabel]) {
+ NSString* stringValue = (aMap.mOSXKey) ? [[value valueAtIndex:j] objectForKey:aMap.mOSXKey]
+ : [value valueAtIndex:j];
+
+ SetStringProperty(aCard, stringValue, aMap.mPropertyName, aNotify, aAbManager);
+
+ return true;
+ }
+ }
+ }
+ // String wasn't found, set value of card to empty if it was set previously
+ SetStringProperty(aCard, EmptyString(), aMap.mPropertyName, aNotify, aAbManager);
+
+ return false;
+}
+
+// Maps Address Book's instant messenger name to the corresponding nsIAbCard field name.
+static const char* InstantMessengerFieldName(NSString* aInstantMessengerName) {
+ if ([aInstantMessengerName isEqualToString:@"AIMInstant"]) {
+ return "_AimScreenName";
+ }
+ if ([aInstantMessengerName isEqualToString:@"GoogleTalkInstant"]) {
+ return "_GoogleTalk";
+ }
+ if ([aInstantMessengerName isEqualToString:@"ICQInstant"]) {
+ return "_ICQ";
+ }
+ if ([aInstantMessengerName isEqualToString:@"JabberInstant"]) {
+ return "_JabberId";
+ }
+ if ([aInstantMessengerName isEqualToString:@"MSNInstant"]) {
+ return "_MSN";
+ }
+ if ([aInstantMessengerName isEqualToString:@"QQInstant"]) {
+ return "_QQ";
+ }
+ if ([aInstantMessengerName isEqualToString:@"SkypeInstant"]) {
+ return "_Skype";
+ }
+ if ([aInstantMessengerName isEqualToString:@"YahooInstant"]) {
+ return "_Yahoo";
+ }
+
+ // Fall back to AIM for everything else.
+ // We don't have nsIAbCard fields for FacebookInstant and GaduGaduInstant.
+ return "_AimScreenName";
+}
+
+nsresult nsAbOSXCard::Init(const char* aUri) {
+ if (strncmp(aUri, NS_ABOSXCARD_URI_PREFIX, sizeof(NS_ABOSXCARD_URI_PREFIX) - 1) != 0)
+ return NS_ERROR_FAILURE;
+
+ mURI = aUri;
+
+ // Extract the UID part.
+ mUID = Substring(mURI, 16, mURI.Length());
+ // Now make sure we don't use the `:ABPerson` on the end, so that
+ // we don't expose it to extensions etc.
+ int32_t pos = mUID.RFindChar(':');
+ if (pos != kNotFound) {
+ mUID = Substring(mUID, 0, pos);
+ }
+ // Also lower case so that we match other UIDs generated by the address book.
+ ToLowerCase(mUID);
+
+ return Update(false);
+}
+
+nsresult nsAbOSXCard::GetURI(nsACString& aURI) {
+ if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ aURI = mURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOSXCard::GetUID(nsACString& uid) {
+ uid = mUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOSXCard::SetUID(const nsACString& aUID) {
+ // The UIDs are obtained from the OS X contacts and cannot be changed.
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsAbOSXCard::Update(bool aNotify) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];
+
+ const char* uid = &((mURI.get())[16]);
+ ABRecord* card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid]];
+ NS_ENSURE_TRUE(card, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIAbManager> abManager;
+ nsresult rv;
+ if (aNotify) {
+ abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if ([card isKindOfClass:[ABGroup class]]) {
+ m_IsMailList = true;
+ m_MailListURI.AssignLiteral(NS_ABOSXDIRECTORY_URI_PREFIX);
+ m_MailListURI.Append(uid);
+ MapStringProperty(this, card, kABGroupNameProperty, "DisplayName", aNotify, abManager);
+ MapStringProperty(this, card, kABGroupNameProperty, "LastName", aNotify, abManager);
+
+ return NS_OK;
+ }
+
+ bool foundHome = false, foundWork = false;
+
+ uint32_t i;
+ for (i = 0; i < nsAbOSXUtils::kPropertyMapSize; ++i) {
+ const nsAbOSXPropertyMap& propertyMap = nsAbOSXUtils::kPropertyMap[i];
+ if (!propertyMap.mOSXProperty) continue;
+
+ if (propertyMap.mOSXLabel) {
+ if (MapMultiValue(this, card, propertyMap, aNotify, abManager) &&
+ propertyMap.mOSXProperty == kABAddressProperty) {
+ if (propertyMap.mOSXLabel == kABAddressHomeLabel)
+ foundHome = true;
+ else
+ foundWork = true;
+ }
+ } else {
+ MapStringProperty(this, card, propertyMap.mOSXProperty, propertyMap.mPropertyName, aNotify,
+ abManager);
+ }
+ }
+
+ int flags = 0;
+ if (kABPersonFlags) flags = [[card valueForProperty:kABPersonFlags] intValue];
+
+#define SET_STRING(_value, _name, _notify, _session) \
+ SetStringProperty(this, _value, #_name, _notify, _session)
+
+ // If kABShowAsCompany is set we use the company name as display name.
+ if (kABPersonFlags && (flags & kABShowAsCompany)) {
+ nsString company;
+ nsresult rv = GetPropertyAsAString(kCompanyProperty, company);
+ if (NS_FAILED(rv)) company.Truncate();
+ SET_STRING(company, DisplayName, aNotify, abManager);
+ } else {
+ // Use the order used in the OS X address book to set DisplayName.
+ int order = kABPersonFlags && (flags & kABNameOrderingMask);
+ if (kABPersonFlags && (order == kABDefaultNameOrdering)) {
+ order = [addressBook defaultNameOrdering];
+ }
+
+ nsAutoString displayName, tempName;
+ if (kABPersonFlags && (order == kABFirstNameFirst)) {
+ GetFirstName(tempName);
+ displayName.Append(tempName);
+
+ GetLastName(tempName);
+
+ // Only append a space if the last name and the first name are not empty
+ if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' ');
+
+ displayName.Append(tempName);
+ } else {
+ GetLastName(tempName);
+ displayName.Append(tempName);
+
+ GetFirstName(tempName);
+
+ // Only append a space if the last name and the first name are not empty
+ if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' ');
+
+ displayName.Append(tempName);
+ }
+ SET_STRING(displayName, DisplayName, aNotify, abManager);
+ }
+
+ ABMultiValue* value = GetMultiValue(card, kABEmailProperty);
+ if (value) {
+ unsigned int count = [value count];
+ if (count > 0) {
+ unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];
+
+ if (j < count) SET_STRING([value valueAtIndex:j], PrimaryEmail, aNotify, abManager);
+
+ // If j is 0 (first in the list) we want the second in the list
+ // (index 1), if j is anything else we want the first in the list
+ // (index 0).
+ j = (j == 0);
+ if (j < count) SET_STRING([value valueAtIndex:j], SecondEmail, aNotify, abManager);
+ }
+ }
+
+ // We map the first home address we can find and the first work address
+ // we can find. If we find none, we map the primary address to the home
+ // address.
+ if (!foundHome && !foundWork) {
+ value = GetMultiValue(card, kABAddressProperty);
+ if (value) {
+ unsigned int count = [value count];
+ unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];
+
+ if (j < count) {
+ NSDictionary* address = [value valueAtIndex:j];
+ if (address) {
+ SET_STRING([address objectForKey:kABAddressStreetKey], HomeAddress, aNotify, abManager);
+ SET_STRING([address objectForKey:kABAddressCityKey], HomeCity, aNotify, abManager);
+ SET_STRING([address objectForKey:kABAddressStateKey], HomeState, aNotify, abManager);
+ SET_STRING([address objectForKey:kABAddressZIPKey], HomeZipCode, aNotify, abManager);
+ SET_STRING([address objectForKey:kABAddressCountryKey], HomeCountry, aNotify, abManager);
+ }
+ }
+ }
+ }
+ // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7.
+ value = GetMultiValue(card, kABInstantMessageProperty);
+ if (value) {
+ unsigned int count = [value count];
+ for (size_t i = 0; i < count; i++) {
+ id imValue = [value valueAtIndex:i];
+ // Depending on the macOS version, imValue can be an NSString or an NSDictionary.
+ if ([imValue isKindOfClass:[NSString class]]) {
+ if (i == [value indexForIdentifier:[value primaryIdentifier]]) {
+ SET_STRING(imValue, _AimScreenName, aNotify, abManager);
+ }
+ } else if ([imValue isKindOfClass:[NSDictionary class]]) {
+ NSString* instantMessageService = [imValue objectForKey:@"InstantMessageService"];
+ const char* fieldName = InstantMessengerFieldName(instantMessageService);
+ NSString* userName = [imValue objectForKey:@"InstantMessageUsername"];
+ SetStringProperty(this, userName, fieldName, aNotify, abManager);
+ }
+ }
+ }
+
+#define MAP_DATE(_date, _name, _notify, _session) \
+ MapDate(this, _date, #_name "Year", #_name "Month", #_name "Day", _notify, _session)
+
+ NSDate* date = [card valueForProperty:kABBirthdayProperty];
+ if (date) MAP_DATE(date, Birth, aNotify, abManager);
+
+ if (kABOtherDatesProperty) {
+ value = GetMultiValue(card, kABOtherDatesProperty);
+ if (value) {
+ unsigned int j, count = [value count];
+ for (j = 0; j < count; ++j) {
+ if ([[value labelAtIndex:j] isEqualToString:kABAnniversaryLabel]) {
+ date = [value valueAtIndex:j];
+ if (date) {
+ MAP_DATE(date, Anniversary, aNotify, abManager);
+
+ break;
+ }
+ }
+ }
+ }
+ }
+#undef MAP_DATE
+#undef SET_STRING
+
+ date = [card valueForProperty:kABModificationDateProperty];
+ if (date) SetPropertyAsUint32("LastModifiedDate", uint32_t([date timeIntervalSince1970]));
+ // XXX No way to notify about this?
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/comm/mailnews/addrbook/src/nsAbOSXDirectory.h b/comm/mailnews/addrbook/src/nsAbOSXDirectory.h
new file mode 100644
index 0000000000..3d5b0384a9
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXDirectory.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsAbOSXDirectory_h___
+#define nsAbOSXDirectory_h___
+
+#include "mozilla/Attributes.h"
+#include "nsISupports.h"
+#include "nsAbDirProperty.h"
+#include "nsIAbDirSearchListener.h"
+#include "nsIMutableArray.h"
+#include "nsInterfaceHashtable.h"
+#include "nsAbOSXCard.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+class nsIAbManager;
+class nsIAbBooleanExpression;
+
+#define NS_ABOSXDIRECTORY_URI_PREFIX "moz-abosxdirectory://"
+
+#define NS_IABOSXDIRECTORY_IID \
+ { \
+ 0x87ee4bd9, 0x8552, 0x498f, { \
+ 0x80, 0x85, 0x34, 0xf0, 0x2a, 0xbb, 0x56, 0x16 \
+ } \
+ }
+
+class nsIAbOSXDirectory : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABOSXDIRECTORY_IID)
+
+ virtual nsresult AssertChildNodes() = 0;
+ virtual nsresult Update() = 0;
+ virtual nsresult AssertDirectory(nsIAbManager* aManager,
+ nsIAbDirectory* aDirectory) = 0;
+ virtual nsresult AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) = 0;
+ virtual nsresult UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard,
+ nsIMutableArray* aCardList) = 0;
+ virtual nsresult UnassertDirectory(nsIAbManager* aManager,
+ nsIAbDirectory* aDirectory) = 0;
+ virtual nsresult DeleteUid(const nsACString& aUid) = 0;
+ virtual nsresult GetURI(nsACString& aURI) = 0;
+ virtual nsresult Init(const char* aUri) = 0;
+ virtual nsresult GetCardByUri(const nsACString& aUri,
+ nsIAbOSXCard** aResult) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbOSXDirectory, NS_IABOSXDIRECTORY_IID)
+
+class nsAbOSXDirectory final : public nsAbDirProperty,
+ public nsIAbOSXDirectory {
+ public:
+ nsAbOSXDirectory();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIAbOSXDirectory method
+ NS_IMETHOD Init(const char* aUri) override;
+
+ // nsAbDirProperty methods
+ NS_IMETHOD GetReadOnly(bool* aReadOnly) override;
+ NS_IMETHOD GetChildCardCount(uint32_t* aCount) override;
+ NS_IMETHOD GetChildCards(nsTArray<RefPtr<nsIAbCard>>& result) override;
+ NS_IMETHOD GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& result) override;
+ NS_IMETHOD HasCard(nsIAbCard* aCard, bool* aHasCard) override;
+ NS_IMETHOD HasDirectory(nsIAbDirectory* aDirectory,
+ bool* aHasDirectory) override;
+ NS_IMETHOD GetURI(nsACString& aURI) override;
+ NS_IMETHOD GetCardFromProperty(const char* aProperty,
+ const nsACString& aValue, bool caseSensitive,
+ nsIAbCard** aResult) override;
+ NS_IMETHOD GetCardsFromProperty(
+ const char* aProperty, const nsACString& aValue, bool aCaseSensitive,
+ nsTArray<RefPtr<nsIAbCard>>& aResult) override;
+ NS_IMETHOD CardForEmailAddress(const nsACString& aEmailAddress,
+ nsIAbCard** aResult) override;
+ NS_IMETHOD Search(const nsAString& query, const nsAString& searchString,
+ nsIAbDirSearchListener* listener) override;
+
+ // nsIAbOSXDirectory
+ nsresult AssertChildNodes() override;
+ nsresult AssertDirectory(nsIAbManager* aManager,
+ nsIAbDirectory* aDirectory) override;
+ nsresult AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) override;
+ nsresult UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard,
+ nsIMutableArray* aCardList) override;
+ nsresult UnassertDirectory(nsIAbManager* aManager,
+ nsIAbDirectory* aDirectory) override;
+
+ nsresult Update() override;
+
+ nsresult DeleteUid(const nsACString& aUid) override;
+
+ nsresult GetCardByUri(const nsACString& aUri,
+ nsIAbOSXCard** aResult) override;
+
+ nsresult GetRootOSXDirectory(nsIAbOSXDirectory** aResult);
+
+ private:
+ ~nsAbOSXDirectory();
+
+ // This is a list of nsIAbCards, kept separate from m_AddressList because:
+ // - nsIAbDirectory items that are mailing lists, must keep a list of
+ // nsIAbCards in m_AddressList, however
+ // - nsIAbDirectory items that are address books, must keep a list of
+ // nsIAbDirectory (i.e. mailing lists) in m_AddressList, AND no nsIAbCards.
+ //
+ // This wasn't too bad for mork, as that just gets a list from its database,
+ // but because we store our own copy of the list, we must store a separate
+ // list of nsIAbCards here. nsIMutableArray is used, because then it is
+ // interchangeable with m_AddressList.
+ nsCOMPtr<nsIMutableArray> mCardList;
+ nsInterfaceHashtable<nsCStringHashKey, nsIAbOSXCard> mCardStore;
+ nsCOMPtr<nsIAbOSXDirectory> mCacheTopLevelOSXAb;
+};
+
+#endif // nsAbOSXDirectory_h___
diff --git a/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm b/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm
new file mode 100644
index 0000000000..fe29a8a0d2
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm
@@ -0,0 +1,911 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbOSXDirectory.h"
+#include "nsAbOSXCard.h"
+#include "nsAbOSXUtils.h"
+#include "nsAbQueryStringToExpression.h"
+#include "nsCOMArray.h"
+#include "nsEnumeratorUtils.h"
+#include "nsIAbDirectoryQueryProxy.h"
+#include "nsIAbManager.h"
+#include "nsObjCExceptions.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIMutableArray.h"
+#include "nsArrayUtils.h"
+#include "nsIAbBooleanExpression.h"
+#include "nsComponentManagerUtils.h"
+#include "nsISimpleEnumerator.h"
+
+#include <AddressBook/AddressBook.h>
+
+#define kABDeletedRecords (kABDeletedRecords ? kABDeletedRecords : @"ABDeletedRecords")
+#define kABUpdatedRecords (kABUpdatedRecords ? kABUpdatedRecords : @"ABUpdatedRecords")
+#define kABInsertedRecords (kABInsertedRecords ? kABInsertedRecords : @"ABInsertedRecords")
+
+static nsresult GetOrCreateGroup(NSString* aUid, nsIAbDirectory** aResult) {
+ NS_ASSERTION(aUid, "No UID for group!.");
+
+ nsAutoCString uri(NS_ABOSXDIRECTORY_URI_PREFIX);
+ AppendToCString(aUid, uri);
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(uri, getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*aResult = directory);
+ return NS_OK;
+}
+
+static nsresult GetCard(ABRecord* aRecord, nsIAbCard** aResult, nsIAbOSXDirectory* osxDirectory) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* uid = [aRecord uniqueId];
+ NS_ASSERTION(uid, "No UID for card!.");
+ if (!uid) return NS_ERROR_FAILURE;
+
+ nsAutoCString uri(NS_ABOSXCARD_URI_PREFIX);
+ AppendToCString(uid, uri);
+ nsCOMPtr<nsIAbOSXCard> osxCard;
+ nsresult rv = osxDirectory->GetCardByUri(uri, getter_AddRefs(osxCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> card = do_QueryInterface(osxCard, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*aResult = card);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+static nsresult CreateCard(ABRecord* aRecord, nsIAbCard** aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* uid = [aRecord uniqueId];
+ NS_ASSERTION(uid, "No UID for card!.");
+ if (!uid) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbOSXCard> osxCard =
+ do_CreateInstance("@mozilla.org/addressbook/directory;1?type=moz-abosxcard", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString uri(NS_ABOSXCARD_URI_PREFIX);
+ AppendToCString(uid, uri);
+
+ rv = osxCard->Init(uri.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> card = do_QueryInterface(osxCard, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*aResult = card);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+static nsresult Sync(NSString* aUid) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];
+ ABRecord* card = [addressBook recordForUniqueId:aUid];
+ if ([card isKindOfClass:[ABGroup class]]) {
+ nsCOMPtr<nsIAbDirectory> directory;
+ GetOrCreateGroup(aUid, getter_AddRefs(directory));
+ nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory);
+
+ if (osxDirectory) {
+ osxDirectory->Update();
+ }
+ } else {
+ nsCOMPtr<nsIAbCard> abCard;
+ nsresult rv;
+
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"),
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetCard(card, getter_AddRefs(abCard), osxDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(abCard);
+ osxCard->Update(true);
+ }
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+@interface ABChangedMonitor : NSObject
+- (void)ABChanged:(NSNotification*)aNotification;
+@end
+
+@implementation ABChangedMonitor
+- (void)ABChanged:(NSNotification*)aNotification {
+ NSDictionary* changes = [aNotification userInfo];
+
+ nsresult rv;
+ NSArray* inserted = [changes objectForKey:kABInsertedRecords];
+
+ if (inserted) {
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"),
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ unsigned int i, count = [inserted count];
+ for (i = 0; i < count; ++i) {
+ ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];
+ ABRecord* card = [addressBook recordForUniqueId:[inserted objectAtIndex:i]];
+ if ([card isKindOfClass:[ABGroup class]]) {
+ nsCOMPtr<nsIAbDirectory> directory;
+ GetOrCreateGroup([inserted objectAtIndex:i], getter_AddRefs(directory));
+
+ rv = osxDirectory->AssertDirectory(abManager, directory);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ } else {
+ nsCOMPtr<nsIAbCard> abCard;
+ // Construct a card
+ nsresult rv = CreateCard(card, getter_AddRefs(abCard));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ rv = osxDirectory->AssertCard(abManager, abCard);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ }
+ }
+
+ NSArray* updated = [changes objectForKey:kABUpdatedRecords];
+ if (updated) {
+ unsigned int i, count = [updated count];
+ for (i = 0; i < count; ++i) {
+ NSString* uid = [updated objectAtIndex:i];
+ Sync(uid);
+ }
+ }
+
+ NSArray* deleted = [changes objectForKey:kABDeletedRecords];
+ if (deleted) {
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"),
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ unsigned int i, count = [deleted count];
+ for (i = 0; i < count; ++i) {
+ NSString* deletedUid = [deleted objectAtIndex:i];
+
+ nsAutoCString uid;
+ AppendToCString(deletedUid, uid);
+
+ rv = osxDirectory->DeleteUid(uid);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ }
+
+ if (!inserted && !updated && !deleted) {
+ // XXX This is supposed to mean "everything was updated", but we get
+ // this whenever something has changed, so not sure what to do.
+ }
+}
+@end
+
+static uint32_t sObserverCount = 0;
+static ABChangedMonitor* sObserver = nullptr;
+
+nsAbOSXDirectory::nsAbOSXDirectory() {}
+
+nsAbOSXDirectory::~nsAbOSXDirectory() {
+ if (--sObserverCount == 0) {
+ [[NSNotificationCenter defaultCenter] removeObserver:sObserver];
+ [sObserver release];
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXDirectory, nsAbDirProperty, nsIAbOSXDirectory)
+
+NS_IMETHODIMP
+nsAbOSXDirectory::Init(const char* aUri) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv;
+ rv = nsAbDirProperty::Init(aUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];
+ if (sObserverCount == 0) {
+ sObserver = [[ABChangedMonitor alloc] init];
+ [[NSNotificationCenter defaultCenter] addObserver:(ABChangedMonitor*)sObserver
+ selector:@selector(ABChanged:)
+ name:kABDatabaseChangedExternallyNotification
+ object:nil];
+ }
+ ++sObserverCount;
+
+ NSArray* cards;
+ nsCOMPtr<nsIMutableArray> cardList;
+ bool isRootOSXDirectory = false;
+
+ if (mURI.Length() <= sizeof(NS_ABOSXDIRECTORY_URI_PREFIX)) {
+ isRootOSXDirectory = true;
+
+ m_DirPrefId.AssignLiteral("ldap_2.servers.osx");
+
+ cards = [[addressBook people] arrayByAddingObjectsFromArray:[addressBook groups]];
+ if (!mCardList)
+ mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ else
+ rv = mCardList->Clear();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ cardList = mCardList;
+ } else {
+ nsAutoCString uid(Substring(mURI, sizeof(NS_ABOSXDIRECTORY_URI_PREFIX) - 1));
+ ABRecord* card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid.get()]];
+ NS_ASSERTION([card isKindOfClass:[ABGroup class]], "Huh.");
+
+ m_IsMailList = true;
+ AppendToString([card valueForProperty:kABGroupNameProperty], m_ListDirName);
+
+ ABGroup* group = (ABGroup*)[addressBook
+ recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]];
+ cards = [[group members] arrayByAddingObjectsFromArray:[group subgroups]];
+
+ if (!m_AddressList)
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ else
+ rv = m_AddressList->Clear();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ cardList = m_AddressList;
+ }
+
+ unsigned int nbCards = [cards count];
+ nsCOMPtr<nsIAbCard> card;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbOSXDirectory> rootOSXDirectory;
+ if (!isRootOSXDirectory) {
+ rv = GetRootOSXDirectory(getter_AddRefs(rootOSXDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ for (unsigned int i = 0; i < nbCards; ++i) {
+ // If we're a Group, it's likely that the cards we're going
+ // to create were already created in the root nsAbOSXDirectory,
+ if (!isRootOSXDirectory)
+ rv = GetCard([cards objectAtIndex:i], getter_AddRefs(card), rootOSXDirectory);
+ else {
+ // If we're not a Group, that means we're the root nsAbOSXDirectory,
+ // which means we have to create the cards from scratch.
+ rv = CreateCard([cards objectAtIndex:i], getter_AddRefs(card));
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We're going to want to tell the AB Manager that we've added some cards
+ // so that they show up in the address book views.
+ AssertCard(abManager, card);
+ }
+
+ if (isRootOSXDirectory) {
+ AssertChildNodes();
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetURI(nsACString& aURI) {
+ if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ aURI = mURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetReadOnly(bool* aReadOnly) {
+ NS_ENSURE_ARG_POINTER(aReadOnly);
+
+ *aReadOnly = true;
+ return NS_OK;
+}
+
+static bool CheckRedundantCards(nsIAbManager* aManager, nsIAbDirectory* aDirectory,
+ nsIAbCard* aCard, NSMutableArray* aCardList) {
+ nsresult rv;
+ nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(aCard, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoCString uri;
+ rv = osxCard->GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, false);
+ NSString* uid = [NSString stringWithUTF8String:(uri.get() + 21)];
+
+ unsigned int i, count = [aCardList count];
+ for (i = 0; i < count; ++i) {
+ if ([[[aCardList objectAtIndex:i] uniqueId] isEqualToString:uid]) {
+ [aCardList removeObjectAtIndex:i];
+ break;
+ }
+ }
+
+ if (i == count) {
+ return true;
+ }
+
+ return false;
+}
+
+nsresult nsAbOSXDirectory::GetRootOSXDirectory(nsIAbOSXDirectory** aResult) {
+ if (!mCacheTopLevelOSXAb) {
+ // Attempt to get card from the toplevel directories
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"),
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mCacheTopLevelOSXAb = osxDirectory;
+ }
+
+ NS_IF_ADDREF(*aResult = mCacheTopLevelOSXAb);
+ return NS_OK;
+}
+
+nsresult nsAbOSXDirectory::Update() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];
+ // Due to the horrible way the address book code works wrt mailing lists
+ // we have to use a different list depending on what we are. This pointer
+ // holds a reference to that list.
+ nsIMutableArray* cardList;
+ NSArray *groups, *cards;
+ if (m_IsMailList) {
+ ABGroup* group = (ABGroup*)[addressBook
+ recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]];
+ groups = nil;
+ cards = [[group members] arrayByAddingObjectsFromArray:[group subgroups]];
+
+ if (!m_AddressList) {
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // For mailing lists, store the cards in m_AddressList
+ cardList = m_AddressList;
+ } else {
+ groups = [addressBook groups];
+ cards = [[addressBook people] arrayByAddingObjectsFromArray:groups];
+
+ if (!mCardList) {
+ mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // For directories, store the cards in mCardList
+ cardList = mCardList;
+ }
+
+ NSMutableArray* mutableArray = [NSMutableArray arrayWithArray:cards];
+ uint32_t addressCount;
+ rv = cardList->GetLength(&addressCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (addressCount--) {
+ nsCOMPtr<nsIAbCard> card(do_QueryElementAt(cardList, addressCount, &rv));
+ if (NS_FAILED(rv)) break;
+
+ if (CheckRedundantCards(abManager, this, card, mutableArray))
+ cardList->RemoveElementAt(addressCount);
+ }
+
+ NSEnumerator* enumerator = [mutableArray objectEnumerator];
+ ABRecord* card;
+ nsCOMPtr<nsIAbCard> abCard;
+ nsCOMPtr<nsIAbOSXDirectory> rootOSXDirectory;
+ rv = GetRootOSXDirectory(getter_AddRefs(rootOSXDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while ((card = [enumerator nextObject])) {
+ rv = GetCard(card, getter_AddRefs(abCard), rootOSXDirectory);
+ if (NS_FAILED(rv)) rv = CreateCard(card, getter_AddRefs(abCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+ AssertCard(abManager, abCard);
+ }
+
+ card = (ABRecord*)[addressBook
+ recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]];
+ NSString* stringValue = [card valueForProperty:kABGroupNameProperty];
+ if (![stringValue isEqualToString:WrapString(m_ListDirName)]) {
+ nsAutoString oldValue(m_ListDirName);
+ AssignToString(stringValue, m_ListDirName);
+ }
+
+ if (groups) {
+ mutableArray = [NSMutableArray arrayWithArray:groups];
+ nsCOMPtr<nsIAbDirectory> directory;
+ // It is ok to use m_AddressList here as only top-level directories have
+ // groups, and they will be in m_AddressList
+ if (m_AddressList) {
+ rv = m_AddressList->GetLength(&addressCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (addressCount--) {
+ directory = do_QueryElementAt(m_AddressList, addressCount, &rv);
+ if (NS_FAILED(rv)) continue;
+
+ nsAutoCString uri;
+ directory->GetURI(uri);
+ uri.Cut(0, 21);
+ NSString* uid = [NSString stringWithUTF8String:uri.get()];
+
+ unsigned int j, arrayCount = [mutableArray count];
+ for (j = 0; j < arrayCount; ++j) {
+ if ([[[mutableArray objectAtIndex:j] uniqueId] isEqualToString:uid]) {
+ [mutableArray removeObjectAtIndex:j];
+ break;
+ }
+ }
+
+ if (j == arrayCount) {
+ UnassertDirectory(abManager, directory);
+ }
+ }
+ }
+
+ enumerator = [mutableArray objectEnumerator];
+ while ((card = [enumerator nextObject])) {
+ rv = GetOrCreateGroup([card uniqueId], getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AssertDirectory(abManager, directory);
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsAbOSXDirectory::AssertChildNodes() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Mailing lists can't have childnodes.
+ if (m_IsMailList) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NSArray* groups = [[ABAddressBook sharedAddressBook] groups];
+
+ unsigned int i, count = [groups count];
+
+ if (count > 0 && !m_AddressList) {
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ for (i = 0; i < count; ++i) {
+ rv = GetOrCreateGroup([[groups objectAtIndex:i] uniqueId], getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AssertDirectory(abManager, directory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsAbOSXDirectory::AssertDirectory(nsIAbManager* aManager, nsIAbDirectory* aDirectory) {
+ uint32_t pos;
+ if (m_AddressList && NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos)))
+ // We already have this directory, so no point in adding it again.
+ return NS_OK;
+
+ nsresult rv;
+ if (!m_AddressList) {
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = m_AddressList->AppendElement(aDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsAbOSXDirectory::AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) {
+ nsAutoCString ourUID;
+ GetUID(ourUID);
+ aCard->SetDirectoryUID(ourUID);
+
+ nsresult rv =
+ m_IsMailList ? m_AddressList->AppendElement(aCard) : mCardList->AppendElement(aCard);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the card's URI and add it to our card store
+ nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(aCard, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString uri;
+ rv = osxCard->GetURI(uri);
+
+ nsCOMPtr<nsIAbOSXCard> retrievedCard;
+ if (!mCardStore.Get(uri, getter_AddRefs(retrievedCard))) mCardStore.InsertOrUpdate(uri, osxCard);
+
+ return NS_OK;
+}
+
+nsresult nsAbOSXDirectory::UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard,
+ nsIMutableArray* aCardList) {
+ uint32_t pos;
+ if (NS_SUCCEEDED(aCardList->IndexOf(0, aCard, &pos))) {
+ nsresult rv = aCardList->RemoveElementAt(pos);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsAbOSXDirectory::UnassertDirectory(nsIAbManager* aManager, nsIAbDirectory* aDirectory) {
+ NS_ENSURE_TRUE(m_AddressList, NS_ERROR_NULL_POINTER);
+
+ uint32_t pos;
+ if (NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) {
+ nsresult rv = m_AddressList->RemoveElementAt(pos);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOSXDirectory::GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& aNodes) {
+ aNodes.Clear();
+ // Mailing lists don't have childnodes.
+ if (m_IsMailList || !m_AddressList) {
+ return NS_OK;
+ }
+
+ uint32_t count = 0;
+ nsresult rv = m_AddressList->GetLength(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aNodes.SetCapacity(count);
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIAbDirectory> dir = do_QueryElementAt(m_AddressList, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aNodes.AppendElement(&*dir);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetChildCardCount(uint32_t* aCount) {
+ nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList;
+ return srcCards->GetLength(aCount);
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetChildCards(nsTArray<RefPtr<nsIAbCard>>& aCards) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ aCards.Clear();
+
+ // Not a search, so just return the appropriate list of items.
+ nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList;
+ uint32_t count = 0;
+ nsresult rv = srcCards->GetLength(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aCards.SetCapacity(count);
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIAbCard> card = do_QueryElementAt(srcCards, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aCards.AppendElement(&*card);
+ }
+ return NS_OK;
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+/* Recursive method that searches for a child card by URI. If it cannot find
+ * it within this directory, it checks all subfolders.
+ */
+NS_IMETHODIMP
+nsAbOSXDirectory::GetCardByUri(const nsACString& aUri, nsIAbOSXCard** aResult) {
+ nsCOMPtr<nsIAbOSXCard> osxCard;
+
+ // Base Case
+ if (mCardStore.Get(aUri, getter_AddRefs(osxCard))) {
+ NS_IF_ADDREF(*aResult = osxCard);
+ return NS_OK;
+ }
+ // Search children
+ nsTArray<RefPtr<nsIAbDirectory>> children;
+ nsresult rv = this->GetChildNodes(children);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIAbDirectory* dir : children) {
+ nsCOMPtr<nsIAbOSXDirectory> childDirectory = do_QueryInterface(dir, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = childDirectory->GetCardByUri(aUri, getter_AddRefs(osxCard));
+ if (NS_SUCCEEDED(rv)) {
+ NS_IF_ADDREF(*aResult = osxCard);
+ return NS_OK;
+ }
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetCardFromProperty(const char* aProperty, const nsACString& aValue,
+ bool aCaseSensitive, nsIAbCard** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+
+ if (aValue.IsEmpty()) return NS_OK;
+
+ nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList;
+
+ if (!list) return NS_OK;
+
+ uint32_t length;
+ nsresult rv = list->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> card;
+ nsAutoCString cardValue;
+
+ for (uint32_t i = 0; i < length && !*aResult; ++i) {
+ card = do_QueryElementAt(list, i, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = card->GetPropertyAsAUTF8String(aProperty, cardValue);
+ if (NS_SUCCEEDED(rv)) {
+ bool equal = aCaseSensitive ? cardValue.Equals(aValue)
+ : cardValue.Equals(aValue, nsCaseInsensitiveCStringComparator);
+ if (equal) NS_IF_ADDREF(*aResult = card);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::GetCardsFromProperty(const char* aProperty, const nsACString& aValue,
+ bool aCaseSensitive, nsTArray<RefPtr<nsIAbCard>>& aResult) {
+ aResult.Clear();
+ if (aValue.IsEmpty()) return NS_OK;
+
+ nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList;
+ if (!list) return NS_OK;
+
+ uint32_t length;
+ nsresult rv = list->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResult.SetCapacity(length);
+ for (uint32_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsIAbCard> card = do_QueryElementAt(list, i, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString cardValue;
+ rv = card->GetPropertyAsAUTF8String(aProperty, cardValue);
+ if (NS_SUCCEEDED(rv)) {
+ bool equal = aCaseSensitive ? cardValue.Equals(aValue)
+ : cardValue.Equals(aValue, nsCaseInsensitiveCStringComparator);
+ if (equal) aResult.AppendElement(&*card);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::CardForEmailAddress(const nsACString& aEmailAddress, nsIAbCard** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+
+ if (aEmailAddress.IsEmpty()) return NS_OK;
+
+ nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList;
+
+ if (!list) return NS_OK;
+
+ uint32_t length;
+ nsresult rv = list->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> card;
+
+ for (uint32_t i = 0; i < length && !*aResult; ++i) {
+ card = do_QueryElementAt(list, i, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ bool hasEmailAddress = false;
+
+ rv = card->HasEmailAddress(aEmailAddress, &hasEmailAddress);
+ if (NS_SUCCEEDED(rv) && hasEmailAddress) NS_IF_ADDREF(*aResult = card);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::HasCard(nsIAbCard* aCard, bool* aHasCard) {
+ NS_ENSURE_ARG_POINTER(aCard);
+ NS_ENSURE_ARG_POINTER(aHasCard);
+
+ nsresult rv = NS_OK;
+ uint32_t index;
+ if (m_IsMailList) {
+ if (m_AddressList) rv = m_AddressList->IndexOf(0, aCard, &index);
+ } else if (mCardList)
+ rv = mCardList->IndexOf(0, aCard, &index);
+
+ *aHasCard = NS_SUCCEEDED(rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::HasDirectory(nsIAbDirectory* aDirectory, bool* aHasDirectory) {
+ NS_ENSURE_ARG_POINTER(aDirectory);
+ NS_ENSURE_ARG_POINTER(aHasDirectory);
+
+ *aHasDirectory = false;
+
+ uint32_t pos;
+ if (m_AddressList && NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos)))
+ *aHasDirectory = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAbOSXDirectory::Search(const nsAString& query, const nsAString& searchString,
+ nsIAbDirSearchListener* listener) {
+ nsresult rv;
+
+ nsCOMPtr<nsIAbBooleanExpression> expression;
+ rv = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query),
+ getter_AddRefs(expression));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectoryQueryArguments> arguments =
+ do_CreateInstance("@mozilla.org/addressbook/directory/query-arguments;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = arguments->SetExpression(expression);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't search the subdirectories. If the current directory is a mailing
+ // list, it won't have any subdirectories. If the current directory is an
+ // addressbook, searching both it and the subdirectories (the mailing
+ // lists), will yield duplicate results because every entry in a mailing
+ // list will be an entry in the parent addressbook.
+ rv = arguments->SetQuerySubDirectories(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Initiate the proxy query with the no query directory
+ nsCOMPtr<nsIAbDirectoryQueryProxy> queryProxy =
+ do_CreateInstance("@mozilla.org/addressbook/directory-query/proxy;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = queryProxy->Initiate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t context = 0;
+ rv = queryProxy->DoQuery(this, arguments, listener, -1, 0, &context);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsAbOSXDirectory::DeleteUid(const nsACString& aUid) {
+ if (!m_AddressList) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // At this stage we don't know if aUid represents a card or group. The OS X
+ // interfaces don't give us chance to find out, so we have to go through
+ // our lists to find it.
+
+ // First, we'll see if its in the group list as it is likely to be shorter.
+
+ // See if this item is in our address list
+ uint32_t addressCount;
+ rv = m_AddressList->GetLength(&addressCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString uri(NS_ABOSXDIRECTORY_URI_PREFIX);
+ uri.Append(aUid);
+
+ // Iterate backwards in case we remove something
+ while (addressCount--) {
+ nsCOMPtr<nsISupports> abItem(do_QueryElementAt(m_AddressList, addressCount, &rv));
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIAbDirectory> directory(do_QueryInterface(abItem, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString dirUri;
+ directory->GetURI(dirUri);
+ if (uri.Equals(dirUri)) return UnassertDirectory(abManager, directory);
+ } else {
+ nsCOMPtr<nsIAbOSXCard> osxCard(do_QueryInterface(abItem, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString cardUri;
+ osxCard->GetURI(cardUri);
+ if (uri.Equals(cardUri)) {
+ nsCOMPtr<nsIAbCard> card(do_QueryInterface(osxCard, &rv));
+ if (NS_SUCCEEDED(rv)) return UnassertCard(abManager, card, m_AddressList);
+ }
+ }
+ }
+ }
+
+ // Second, see if it is one of the cards.
+ if (!mCardList) return NS_ERROR_FAILURE;
+
+ uri = NS_ABOSXCARD_URI_PREFIX;
+ uri.Append(aUid);
+
+ rv = mCardList->GetLength(&addressCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (addressCount--) {
+ nsCOMPtr<nsIAbOSXCard> osxCard(do_QueryElementAt(mCardList, addressCount, &rv));
+ if (NS_FAILED(rv)) continue;
+
+ nsAutoCString cardUri;
+ osxCard->GetURI(cardUri);
+
+ if (uri.Equals(cardUri)) {
+ nsCOMPtr<nsIAbCard> card(do_QueryInterface(osxCard, &rv));
+ if (NS_SUCCEEDED(rv)) return UnassertCard(abManager, card, mCardList);
+ }
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbOSXUtils.h b/comm/mailnews/addrbook/src/nsAbOSXUtils.h
new file mode 100644
index 0000000000..42a57f45f6
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXUtils.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsAbOSXUtils_h___
+#define nsAbOSXUtils_h___
+
+#include <Foundation/NSString.h>
+#include "nsString.h"
+
+NSString* WrapString(const nsString& aString);
+void AppendToString(const NSString* aString, nsString& aResult);
+void AssignToString(const NSString* aString, nsString& aResult);
+void AppendToCString(const NSString* aString, nsCString& aResult);
+
+struct nsAbOSXPropertyMap {
+ NSString* const mOSXProperty;
+ NSString* const mOSXLabel;
+ NSString* const mOSXKey;
+ const char* mPropertyName;
+};
+
+class nsAbOSXUtils {
+ public:
+ static const nsAbOSXPropertyMap kPropertyMap[];
+ static const uint32_t kPropertyMapSize;
+};
+
+#endif // nsAbOSXUtils_h___
diff --git a/comm/mailnews/addrbook/src/nsAbOSXUtils.mm b/comm/mailnews/addrbook/src/nsAbOSXUtils.mm
new file mode 100644
index 0000000000..5c6f7b2b8d
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOSXUtils.mm
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbOSXUtils.h"
+#include "nsString.h"
+#include "nsAbOSXCard.h"
+#include "nsMemory.h"
+#include "mozilla/ArrayUtils.h"
+using namespace mozilla;
+
+#include <AddressBook/AddressBook.h>
+#define kABDepartmentProperty (kABDepartmentProperty ? kABDepartmentProperty : @"ABDepartment")
+
+NSString* WrapString(const nsString& aString) {
+ unichar* chars = reinterpret_cast<unichar*>(const_cast<char16_t*>(aString.get()));
+
+ return [NSString stringWithCharacters:chars length:aString.Length()];
+}
+
+void AppendToString(const NSString* aString, nsString& aResult) {
+ if (aString) {
+ const char* chars = [aString UTF8String];
+ if (chars) {
+ aResult.Append(NS_ConvertUTF8toUTF16(chars));
+ }
+ }
+}
+
+void AssignToString(const NSString* aString, nsString& aResult) {
+ if (aString) {
+ const char* chars = [aString UTF8String];
+ if (chars) CopyUTF8toUTF16(nsDependentCString(chars), aResult);
+ }
+}
+
+void AppendToCString(const NSString* aString, nsCString& aResult) {
+ if (aString) {
+ const char* chars = [aString UTF8String];
+ if (chars) {
+ aResult.Append(chars);
+ }
+ }
+}
+
+// Some properties can't be easily mapped back and forth.
+#define DONT_MAP(moz_name, osx_property, osx_label, osx_key)
+
+#define DEFINE_PROPERTY(moz_name, osx_property, osx_label, osx_key) \
+ {osx_property, osx_label, osx_key, #moz_name},
+
+// clang-format off
+const nsAbOSXPropertyMap nsAbOSXUtils::kPropertyMap[] = {
+ DEFINE_PROPERTY(FirstName, kABFirstNameProperty, nil, nil)
+ DEFINE_PROPERTY(LastName, kABLastNameProperty, nil, nil)
+ DONT_MAP("DisplayName", nil, nil, nil)
+ DEFINE_PROPERTY(PhoneticFirstName, kABFirstNamePhoneticProperty, nil, nil)
+ DEFINE_PROPERTY(PhoneticLastName, kABLastNamePhoneticProperty, nil, nil)
+ DEFINE_PROPERTY(NickName, kABNicknameProperty, nil, nil)
+ DONT_MAP(PrimaryEmail, kABEmailProperty, nil, nil)
+ DONT_MAP(SecondEmail, kABEmailProperty, nil, nil)
+ DEFINE_PROPERTY(WorkPhone, kABPhoneProperty, kABPhoneWorkLabel, nil)
+ DEFINE_PROPERTY(HomePhone, kABPhoneProperty, kABPhoneHomeLabel, nil)
+ DEFINE_PROPERTY(FaxNumber, kABPhoneProperty, kABPhoneWorkFAXLabel, nil)
+ DEFINE_PROPERTY(PagerNumber, kABPhoneProperty, kABPhonePagerLabel, nil)
+ DEFINE_PROPERTY(CellularNumber, kABPhoneProperty, kABPhoneMobileLabel, nil)
+ DEFINE_PROPERTY(HomeAddress, kABAddressProperty, kABAddressHomeLabel,
+ kABAddressStreetKey)
+ DEFINE_PROPERTY(HomeCity, kABAddressProperty, kABAddressHomeLabel,
+ kABAddressCityKey)
+ DEFINE_PROPERTY(HomeState, kABAddressProperty, kABAddressHomeLabel,
+ kABAddressStateKey)
+ DEFINE_PROPERTY(HomeZipCode, kABAddressProperty, kABAddressHomeLabel,
+ kABAddressZIPKey)
+ DEFINE_PROPERTY(HomeCountry, kABAddressProperty, kABAddressHomeLabel,
+ kABAddressCountryKey)
+ DEFINE_PROPERTY(WorkAddress, kABAddressProperty, kABAddressWorkLabel,
+ kABAddressStreetKey)
+ DEFINE_PROPERTY(WorkCity, kABAddressProperty, kABAddressWorkLabel,
+ kABAddressCityKey)
+ DEFINE_PROPERTY(WorkState, kABAddressProperty, kABAddressWorkLabel,
+ kABAddressStateKey)
+ DEFINE_PROPERTY(WorkZipCode, kABAddressProperty, kABAddressWorkLabel,
+ kABAddressZIPKey)
+ DEFINE_PROPERTY(WorkCountry, kABAddressProperty, kABAddressWorkLabel,
+ kABAddressCountryKey)
+ DEFINE_PROPERTY(JobTitle, kABJobTitleProperty, nil, nil)
+ DEFINE_PROPERTY(Department, kABDepartmentProperty, nil, nil)
+ DEFINE_PROPERTY(Company, kABOrganizationProperty, nil, nil)
+ // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7.
+ DONT_MAP(_AimScreenName, kABInstantMessageProperty, nil, nil)
+ DEFINE_PROPERTY(WebPage1, kABHomePageProperty, nil, nil)
+ DONT_MAP(WebPage2, kABHomePageProperty, nil, nil)
+ DONT_MAP(BirthYear, "birthyear", nil, nil)
+ DONT_MAP(BirthMonth, "birthmonth", nil, nil)
+ DONT_MAP(BirthDay, "birthday", nil, nil)
+ DONT_MAP(Custom1, "custom1", nil, nil)
+ DONT_MAP(Custom2, "custom2", nil, nil)
+ DONT_MAP(Custom3, "custom3", nil, nil)
+ DONT_MAP(Custom4, "custom4", nil, nil)
+ DEFINE_PROPERTY(Note, kABNoteProperty, nil, nil)
+ DONT_MAP("LastModifiedDate", modifytimestamp, nil, nil)
+};
+// clang-format on
+
+const uint32_t nsAbOSXUtils::kPropertyMapSize = ArrayLength(nsAbOSXUtils::kPropertyMap);
diff --git a/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp
new file mode 100644
index 0000000000..77568a62f2
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp
@@ -0,0 +1,1418 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsAbOutlookDirectory.h"
+#include "nsAbWinHelper.h"
+
+#include "nsString.h"
+#include "nsAbDirectoryQuery.h"
+#include "nsIAbBooleanExpression.h"
+#include "nsIAbManager.h"
+#include "nsAbQueryStringToExpression.h"
+#include "nsEnumeratorUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/Logging.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsCRTGlue.h"
+#include "nsArrayUtils.h"
+#include "nsMsgUtils.h"
+#include "nsQueryObject.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+
+#define PRINT_TO_CONSOLE 0
+#if PRINT_TO_CONSOLE
+# define PRINTF(args) printf args
+#else
+static mozilla::LazyLogModule gAbOutlookDirectoryLog("AbOutlookDirectory");
+# define PRINTF(args) \
+ MOZ_LOG(gAbOutlookDirectoryLog, mozilla::LogLevel::Debug, args)
+#endif
+
+nsAbOutlookDirectory::nsAbOutlookDirectory(void)
+ : nsAbDirProperty(),
+ mDirEntry(nullptr),
+ mCurrentQueryId(0),
+ mSearchContext(-1) {
+ mDirEntry = new nsMapiEntry;
+}
+
+nsAbOutlookDirectory::~nsAbOutlookDirectory(void) {
+ if (mDirEntry) {
+ delete mDirEntry;
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsAbOutlookDirectory, nsAbDirProperty,
+ nsIAbDirectoryQuery, nsIAbDirSearchListener)
+
+NS_IMETHODIMP nsAbOutlookDirectory::Init(const char* aUri) {
+ nsresult rv = nsAbDirProperty::Init(aUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString entry;
+ makeEntryIdFromURI(kOutlookDirectoryScheme, mURI.get(), entry);
+ nsAbWinHelperGuard mapiAddBook;
+ nsAutoString unichars;
+ ULONG objectType = 0;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ mDirEntry->Assign(entry);
+ if (!mapiAddBook->GetPropertyLong(*mDirEntry, PR_OBJECT_TYPE, objectType)) {
+ PRINTF(("Cannot get type.\n"));
+ return NS_ERROR_FAILURE;
+ }
+ if (!mapiAddBook->GetPropertyUString(*mDirEntry, PR_DISPLAY_NAME_W,
+ unichars)) {
+ PRINTF(("Cannot get name.\n"));
+ return NS_ERROR_FAILURE;
+ }
+
+ m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (objectType == MAPI_DISTLIST) {
+ m_IsMailList = true;
+ SetDirName(unichars);
+ // For a mailing list, we get all the cards into our member variable.
+ rv = GetCards(m_AddressList, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ m_IsMailList = false;
+ if (unichars.IsEmpty()) {
+ SetDirName(u"Outlook"_ns);
+ } else {
+ SetDirName(unichars);
+ }
+ // First, get the mailing lists, then the cards.
+ rv = GetNodes(m_AddressList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetCards(mCardList, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+// nsIAbDirectory methods
+
+NS_IMETHODIMP nsAbOutlookDirectory::GetDirType(int32_t* aDirType) {
+ NS_ENSURE_ARG_POINTER(aDirType);
+ *aDirType = nsIAbManager::MAPI_DIRECTORY_TYPE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::GetURI(nsACString& aURI) {
+ if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ aURI = mURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::GetChildNodes(
+ nsTArray<RefPtr<nsIAbDirectory>>& aNodes) {
+ aNodes.Clear();
+ // Mailing lists don't have childnodes.
+ if (m_IsMailList || !m_AddressList) {
+ return NS_OK;
+ }
+
+ uint32_t count = 0;
+ nsresult rv = m_AddressList->GetLength(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aNodes.SetCapacity(count);
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIAbDirectory> dir = do_QueryElementAt(m_AddressList, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aNodes.AppendElement(&*dir);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::GetChildCardCount(uint32_t* aCount) {
+ nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList;
+ return srcCards->GetLength(aCount);
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::GetChildCards(
+ nsTArray<RefPtr<nsIAbCard>>& aCards) {
+ aCards.Clear();
+
+ // Not a search, so just return the appropriate list of items.
+ nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList;
+ uint32_t count = 0;
+ nsresult rv = srcCards->GetLength(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aCards.SetCapacity(count);
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIAbCard> card = do_QueryElementAt(srcCards, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aCards.AppendElement(&*card);
+ }
+ return NS_OK;
+}
+
+// This is an exact copy of nsAbOSXDirectory::HasCard().
+NS_IMETHODIMP nsAbOutlookDirectory::HasCard(nsIAbCard* aCard, bool* aHasCard) {
+ NS_ENSURE_ARG_POINTER(aCard);
+ NS_ENSURE_ARG_POINTER(aHasCard);
+
+ nsresult rv = NS_OK;
+ uint32_t index;
+ if (m_IsMailList) {
+ if (m_AddressList) rv = m_AddressList->IndexOf(0, aCard, &index);
+ } else if (mCardList)
+ rv = mCardList->IndexOf(0, aCard, &index);
+
+ *aHasCard = NS_SUCCEEDED(rv);
+
+ return NS_OK;
+}
+
+// This is an exact copy of nsAbOSXDirectory::HasDirectory().
+NS_IMETHODIMP nsAbOutlookDirectory::HasDirectory(nsIAbDirectory* aDirectory,
+ bool* aHasDirectory) {
+ NS_ENSURE_ARG_POINTER(aDirectory);
+ NS_ENSURE_ARG_POINTER(aHasDirectory);
+
+ *aHasDirectory = false;
+
+ uint32_t pos;
+ if (m_AddressList &&
+ NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos)))
+ *aHasDirectory = true;
+
+ return NS_OK;
+}
+
+// This is an exact copy of nsAbOSXDirectory::CardForEmailAddress().
+NS_IMETHODIMP
+nsAbOutlookDirectory::CardForEmailAddress(const nsACString& aEmailAddress,
+ nsIAbCard** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+
+ if (aEmailAddress.IsEmpty()) return NS_OK;
+
+ nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList;
+
+ if (!list) return NS_OK;
+
+ uint32_t length;
+ nsresult rv = list->GetLength(&length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> card;
+
+ for (uint32_t i = 0; i < length && !*aResult; ++i) {
+ card = do_QueryElementAt(list, i, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ bool hasEmailAddress = false;
+
+ rv = card->HasEmailAddress(aEmailAddress, &hasEmailAddress);
+ if (NS_SUCCEEDED(rv) && hasEmailAddress) NS_IF_ADDREF(*aResult = card);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsAbOutlookDirectory::ExtractCardEntry(nsIAbCard* aCard,
+ nsCString& aEntry) {
+ aEntry.Truncate();
+
+ nsCString uri;
+ aCard->GetPropertyAsAUTF8String("OutlookEntryURI", uri);
+
+ // If we don't have a URI, uri will be empty. makeEntryIdFromURI doesn't set
+ // aEntry to anything if uri is empty, so it will be truncated, allowing us
+ // to accept cards not initialized by us.
+ makeEntryIdFromURI(kOutlookCardScheme, uri.get(), aEntry);
+ return NS_OK;
+}
+
+nsresult nsAbOutlookDirectory::ExtractDirectoryEntry(nsIAbDirectory* aDirectory,
+ nsCString& aEntry) {
+ aEntry.Truncate();
+ nsCString uri;
+ nsresult rv = aDirectory->GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), aEntry);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::DeleteCards(
+ const nsTArray<RefPtr<nsIAbCard>>& aCards) {
+ nsresult retCode = NS_OK;
+ nsAbWinHelperGuard mapiAddBook;
+
+ if (!mapiAddBook->IsOK()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString cardEntryString;
+ nsMapiEntry cardEntry;
+
+ for (auto card : aCards) {
+ retCode = ExtractCardEntry(card, cardEntryString);
+ if (NS_SUCCEEDED(retCode) && !cardEntryString.IsEmpty()) {
+ cardEntry.Assign(cardEntryString);
+ bool success = false;
+ if (m_IsMailList) {
+ nsAutoCString uri(mURI);
+ // Trim off the mailing list entry ID from the mailing list URI
+ // to get the top-level directory entry ID.
+ nsAutoCString topEntryString;
+ int32_t slashPos = uri.RFindChar('/');
+ uri.SetLength(slashPos);
+ makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString);
+ nsMapiEntry topDirEntry;
+ topDirEntry.Assign(topEntryString);
+ success =
+ mapiAddBook->DeleteEntryfromDL(topDirEntry, *mDirEntry, cardEntry);
+ } else {
+ success = mapiAddBook->DeleteEntry(*mDirEntry, cardEntry);
+ }
+ if (!success) {
+ PRINTF(("Cannot delete card %s.\n", cardEntryString.get()));
+ } else {
+ if (m_IsMailList) {
+ // It appears that removing a card from a mailing list makes
+ // our list go stale, so refresh it.
+ m_AddressList->Clear();
+ GetCards(m_AddressList, nullptr);
+ } else if (mCardList) {
+ uint32_t pos;
+ if (NS_SUCCEEDED(mCardList->IndexOf(0, card, &pos)))
+ mCardList->RemoveElementAt(pos);
+ }
+ retCode = NotifyItemDeletion(card, true);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ card->SetDirectoryUID(EmptyCString());
+ }
+ } else {
+ PRINTF(("Card doesn't belong in this directory.\n"));
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::DeleteDirectory(
+ nsIAbDirectory* aDirectory) {
+ if (!aDirectory) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult retCode = NS_OK;
+ nsAbWinHelperGuard mapiAddBook;
+ nsAutoCString dirEntryString;
+
+ if (!mapiAddBook->IsOK()) {
+ return NS_ERROR_FAILURE;
+ }
+ retCode = ExtractDirectoryEntry(aDirectory, dirEntryString);
+ if (NS_SUCCEEDED(retCode) && !dirEntryString.IsEmpty()) {
+ nsMapiEntry directoryEntry;
+
+ directoryEntry.Assign(dirEntryString);
+ if (!mapiAddBook->DeleteEntry(*mDirEntry, directoryEntry)) {
+ PRINTF(("Cannot delete directory %s.\n", dirEntryString.get()));
+ } else {
+ uint32_t pos;
+ if (m_AddressList &&
+ NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos)))
+ m_AddressList->RemoveElementAt(pos);
+
+ // Iterate over the cards of the directory to find the one
+ // representing the mailing list and also remove it.
+ if (mCardList) {
+ nsAutoCString listUID;
+ aDirectory->GetUID(listUID);
+
+ uint32_t nbCards = 0;
+ nsresult rv = mCardList->GetLength(&nbCards);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < nbCards; i++) {
+ nsCOMPtr<nsIAbCard> card = do_QueryElementAt(mCardList, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString cardUID;
+ rv = card->GetUID(cardUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cardUID.Equals(listUID)) {
+ mCardList->RemoveElementAt(i);
+ break;
+ }
+ }
+ }
+ retCode = NotifyItemDeletion(aDirectory, false);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ }
+ } else {
+ PRINTF(("Directory doesn't belong to this folder.\n"));
+ }
+ return retCode;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::AddCard(nsIAbCard* aCard,
+ nsIAbCard** aNewCard) {
+ NS_ENSURE_ARG_POINTER(aCard);
+ NS_ENSURE_ARG_POINTER(aNewCard);
+
+ *aNewCard = nullptr;
+ nsresult retCode = NS_OK;
+ nsAbWinHelperGuard mapiAddBook;
+ nsMapiEntry newEntry;
+ nsAutoCString cardEntryString;
+ bool isNewCard = false;
+ nsCOMPtr<nsIAbDirectory> topDir;
+
+ if (!mapiAddBook->IsOK()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString ourUID;
+ if (!m_IsMailList) {
+ // We're not dealing with a mailing list, so just create a new entry.
+ if (!mapiAddBook->CreateEntry(*mDirEntry, newEntry)) {
+ return NS_ERROR_FAILURE;
+ }
+ isNewCard = true;
+ GetUID(ourUID);
+ } else {
+ nsAutoCString dirURI(mURI);
+ // Trim off the mailing list entry ID from the mailing list URI
+ // to get the top-level directory entry ID.
+ nsAutoCString topEntryString;
+ int32_t slashPos = dirURI.RFindChar('/');
+ dirURI.SetLength(slashPos);
+ makeEntryIdFromURI(kOutlookDirectoryScheme, dirURI.get(), topEntryString);
+ nsMapiEntry topDirEntry;
+ topDirEntry.Assign(topEntryString);
+
+ // Add a card to a mailing list. We distinguish two cases:
+ // If there is already an Outlook card, we can just add it.
+ // If none exists, we need to create it first. Outlook has an option
+ // that allows a creation of a mailing list member solely in the list
+ // but we don't support this for now to avoid more MAPI complication.
+ retCode = ExtractCardEntry(aCard, cardEntryString);
+ if (NS_SUCCEEDED(retCode) && !cardEntryString.IsEmpty()) {
+ newEntry.Assign(cardEntryString);
+ } else {
+ if (!mapiAddBook->CreateEntry(topDirEntry, newEntry)) {
+ return NS_ERROR_FAILURE;
+ }
+ isNewCard = true;
+ }
+ nsAutoString display;
+ nsAutoString email;
+ aCard->GetDisplayName(display);
+ aCard->GetPrimaryEmail(email);
+ if (!mapiAddBook->AddEntryToDL(topDirEntry, *mDirEntry, newEntry,
+ display.get(), email.get())) {
+ return NS_ERROR_FAILURE;
+ }
+ // The UID of the card is the top directory's UID.
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &retCode));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ retCode = abManager->GetDirectory(dirURI, getter_AddRefs(topDir));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ topDir->GetUID(ourUID);
+ }
+
+ newEntry.ToString(cardEntryString);
+ nsAutoCString cardURI(kOutlookCardScheme);
+ cardURI.Append(cardEntryString);
+
+ nsCOMPtr<nsIAbCard> newCard;
+ retCode = OutlookCardForURI(cardURI, getter_AddRefs(newCard));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ // Make sure the card has a UID before setting its directory UID.
+ // This is a bit of a hack. If we get the UID of the card before setting its
+ // directory UID, we can avoid an unwanted `ModifyCard()` call inside
+ // `nsAbCardProperty::SetUID()`.
+ nsCString dummy;
+ newCard->GetUID(dummy);
+ newCard->SetDirectoryUID(ourUID);
+
+ if (isNewCard) {
+ retCode = newCard->Copy(aCard);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ // Set a decent display name of the card. This needs to be set
+ // on the card and not on the related contact via `SetPropertiesUString()`.
+ nsAutoString displayName;
+ newCard->GetDisplayName(displayName);
+ mapiAddBook->SetPropertyUString(newEntry, PR_DISPLAY_NAME_W,
+ displayName.get());
+
+ if (m_IsMailList) {
+ // Observed behavior for a new card in a mailing list is that
+ // Outlook returns __MailUser__ as first name. That value was
+ // previously set as display name when creating the bare card.
+ nsAutoString firstName;
+ newCard->GetFirstName(firstName);
+ if (StringBeginsWith(firstName,
+ NS_LITERAL_STRING_FROM_CSTRING(kDummyDisplayName))) {
+ newCard->SetFirstName(EmptyString());
+ }
+ }
+
+ retCode = ModifyCardInternal(newCard, true);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ }
+
+ if (m_IsMailList) {
+ m_AddressList->AppendElement(newCard);
+ if (isNewCard) {
+ // Add the new card to the cards of the top directory as well.
+ nsAbOutlookDirectory* topDirOL =
+ static_cast<nsAbOutlookDirectory*>(topDir.get());
+ topDirOL->mCardList->AppendElement(newCard);
+ }
+ } else {
+ mCardList->AppendElement(newCard);
+ }
+
+ NotifyItemAddition(newCard, true);
+
+ newCard.forget(aNewCard);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::DropCard(nsIAbCard* aData,
+ bool needToCopyCard) {
+ nsCOMPtr<nsIAbCard> addedCard;
+ return AddCard(aData, getter_AddRefs(addedCard));
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::AddMailList(nsIAbDirectory* aMailList,
+ nsIAbDirectory** addedList) {
+ NS_ENSURE_ARG_POINTER(aMailList);
+ NS_ENSURE_ARG_POINTER(addedList);
+ if (m_IsMailList) return NS_OK;
+
+ nsAbWinHelperGuard mapiAddBook;
+ nsMapiEntry newEntry;
+ nsAutoCString newEntryString;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ nsAutoString name;
+ nsresult rv = aMailList->GetDirName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mapiAddBook->CreateDistList(*mDirEntry, newEntry, name.get()))
+ return NS_ERROR_FAILURE;
+
+ newEntry.ToString(newEntryString);
+ nsAutoCString uri(mURI);
+ nsAutoCString topEntryString;
+ makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString);
+ uri.Append('/');
+ uri.Append(newEntryString);
+
+ RefPtr<nsAbOutlookDirectory> directory = new nsAbOutlookDirectory;
+
+ // We will later need the URI of the parent directory, so store it here.
+ directory->mParentEntryId = topEntryString;
+
+ // Light-weight initialisation. `nsAbOutlookDirectory::Init()` will get
+ // the object type wrong since we don't have cards yet and scan for cards
+ // which we don't have yet.
+ rv = directory->nsAbDirProperty::Init(uri.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ directory->mDirEntry->Assign(newEntryString);
+ directory->m_IsMailList = true;
+ directory->m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbDirectory> newList = do_QueryObject(directory);
+
+ rv = newList->CopyMailList(aMailList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Also create a card to match the list.
+ // That needs to happen before the notification.
+ nsAutoCString cardURI(kOutlookCardScheme);
+ cardURI.Append(newEntryString);
+ nsCOMPtr<nsIAbCard> newCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newCard->SetPropertyAsAUTF8String("OutlookEntryURI", cardURI);
+
+ // This is a bit of a hack. If we set the UID of the card before setting its
+ // directory UID, we can avoid an unwanted `ModifyCard()` call inside
+ // `nsAbCardProperty::SetUID()`.
+ nsAutoCString listUID;
+ newList->GetUID(listUID);
+ newCard->SetUID(listUID);
+ nsAutoCString ourUID;
+ GetUID(ourUID);
+ newCard->SetDirectoryUID(ourUID);
+ newCard->SetIsMailList(true);
+ newCard->SetMailListURI(uri.get());
+ newCard->SetDisplayName(name);
+ newCard->SetLastName(name);
+
+ mCardList->AppendElement(newCard);
+ m_AddressList->AppendElement(newList);
+
+ NotifyItemAddition(newList, false);
+
+ newList.forget(addedList);
+ return rv;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::EditMailListToDatabase(
+ nsIAbCard* listCard) {
+ nsresult rv;
+ nsString name;
+ nsAbWinHelperGuard mapiAddBook;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ rv = GetDirName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mapiAddBook->SetPropertyUString(*mDirEntry, PR_DISPLAY_NAME_W,
+ name.get()))
+ return NS_ERROR_FAILURE;
+
+ // Iterate over the cards of the parent directory to find the one
+ // representing the mailing list and also change its name.
+ nsAutoCString uri(mURI);
+ // Trim off the mailing list entry ID from the mailing list URI
+ // to get the top-level directory entry ID.
+ nsAutoCString topEntryString;
+ int32_t slashPos = uri.RFindChar('/');
+ uri.SetLength(slashPos);
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIAbDirectory> parent;
+ rv = abManager->GetDirectory(uri, getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString listUID;
+ GetUID(listUID);
+
+ uint32_t nbCards = 0;
+ nsAbOutlookDirectory* olDir =
+ static_cast<nsAbOutlookDirectory*>(parent.get());
+ rv = olDir->mCardList->GetLength(&nbCards);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < nbCards; i++) {
+ nsCOMPtr<nsIAbCard> card = do_QueryElementAt(olDir->mCardList, i, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString cardUID;
+ rv = card->GetUID(cardUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cardUID.Equals(listUID)) {
+ card->SetDisplayName(name);
+ break;
+ }
+ }
+
+ nsAutoCString dirUID;
+ if (listCard) {
+ // For mailing list cards, we use the UID of the top level directory.
+ listCard->GetDirectoryUID(dirUID);
+ NotifyItemModification(listCard, true, dirUID.get());
+ }
+ nsCOMPtr<nsIAbDirectory> dir = do_QueryObject(this);
+ // Use the UID of the parent.
+ parent->GetUID(dirUID);
+ NotifyItemModification(dir, false, dirUID.get());
+ return NS_OK;
+}
+
+static nsresult FindPrimaryEmailCondition(nsIAbBooleanExpression* aLevel,
+ nsAString& value) {
+ if (!aLevel) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult retCode = NS_OK;
+ nsTArray<RefPtr<nsISupports>> expressions;
+
+ retCode = aLevel->GetExpressions(expressions);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ for (uint32_t i = 0; i < expressions.Length(); ++i) {
+ RefPtr<nsIAbBooleanConditionString> condition =
+ do_QueryObject(expressions[i], &retCode);
+ if (NS_SUCCEEDED(retCode)) {
+ nsCString name;
+ retCode = condition->GetName(getter_Copies(name));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ if (name.EqualsLiteral("PrimaryEmail")) {
+ // We found a leaf in the boolean expression tree that compares
+ // "PrimaryEmail". So return the value and be done.
+ retCode = condition->GetValue(getter_Copies(value));
+ return retCode;
+ }
+ continue;
+ }
+
+ RefPtr<nsIAbBooleanExpression> subExpression =
+ do_QueryObject(expressions[i], &retCode);
+ if (NS_SUCCEEDED(retCode)) {
+ // Recurse into the sub-tree.
+ retCode = FindPrimaryEmailCondition(subExpression, value);
+ // If we found our leaf there, we're done.
+ if (NS_SUCCEEDED(retCode)) return retCode;
+ }
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+static nsresult GetConditionValue(nsIAbDirectoryQueryArguments* aArguments,
+ nsAString& value) {
+ if (!aArguments) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsresult retCode = NS_OK;
+
+ nsCOMPtr<nsISupports> supports;
+ retCode = aArguments->GetExpression(getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ nsCOMPtr<nsIAbBooleanExpression> booleanQuery =
+ do_QueryInterface(supports, &retCode);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ // Outlook can only query the PR_ANR property. So get its value from the
+ // PrimaryEmail condition.
+ retCode = FindPrimaryEmailCondition(booleanQuery, value);
+ return retCode;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::DoQuery(
+ nsIAbDirectory* aDirectory, nsIAbDirectoryQueryArguments* aArguments,
+ nsIAbDirSearchListener* aListener, int32_t aResultLimit, int32_t aTimeout,
+ int32_t* aReturnValue) {
+ if (!aArguments || !aListener || !aReturnValue) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // The only thing we can search here is PR_ANR. All other properties are
+ // skipped. Note that PR_ANR also searches in the recipient's name and
+ // e-mail address.
+ // https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/address-book-restrictions
+ // states:
+ // Ambiguous name restrictions are property restrictions using the PR_ANR
+ // property to match recipient names with entries in address book containers.
+
+ SRestriction restriction;
+ SPropValue val;
+ restriction.rt = RES_PROPERTY;
+ restriction.res.resProperty.relop = RELOP_EQ;
+ restriction.res.resProperty.ulPropTag = PR_ANR_W;
+ restriction.res.resProperty.lpProp = &val;
+ restriction.res.resProperty.lpProp->ulPropTag = PR_ANR_W;
+
+ nsAutoString value;
+ nsresult rv = GetConditionValue(aArguments, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ restriction.res.resProperty.lpProp->Value.lpszW = value.get();
+
+ rv = ExecuteQuery(&restriction, aListener, aResultLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aReturnValue = ++mCurrentQueryId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::StopQuery(int32_t aContext) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::Search(const nsAString& query,
+ const nsAString& searchString,
+ nsIAbDirSearchListener* listener) {
+ nsresult retCode = NS_OK;
+
+ // Note the following: We get a rather complicated query passed here from
+ // preference mail.addr_book.quicksearchquery.format.
+ // Outlook address book search only allows search by PR_ANR, which is a fuzzy
+ // Ambiguous Name Restriction search.
+
+ retCode = StopSearch();
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ nsCOMPtr<nsIAbBooleanExpression> expression;
+
+ nsCOMPtr<nsIAbDirectoryQueryArguments> arguments = do_CreateInstance(
+ "@mozilla.org/addressbook/directory/query-arguments;1", &retCode);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ retCode = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query),
+ getter_AddRefs(expression));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ retCode = arguments->SetExpression(expression);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ retCode = arguments->SetQuerySubDirectories(true);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ return DoQuery(this, arguments, listener, -1, 0, &mSearchContext);
+}
+
+nsresult nsAbOutlookDirectory::StopSearch(void) {
+ return StopQuery(mSearchContext);
+}
+
+// nsIAbDirSearchListener
+NS_IMETHODIMP nsAbOutlookDirectory::OnSearchFinished(
+ nsresult status, bool complete, nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAbOutlookDirectory::OnSearchFoundCard(nsIAbCard* aCard) {
+ mCardList->AppendElement(aCard);
+ return NS_OK;
+}
+
+nsresult nsAbOutlookDirectory::ExecuteQuery(SRestriction* aRestriction,
+ nsIAbDirSearchListener* aListener,
+ int32_t aResultLimit)
+
+{
+ if (!aListener) return NS_ERROR_NULL_POINTER;
+
+ nsresult retCode = NS_OK;
+
+ nsCOMPtr<nsIMutableArray> resultsArray(
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &retCode));
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ retCode = GetCards(resultsArray, aRestriction);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ uint32_t nbResults = 0;
+ retCode = resultsArray->GetLength(&nbResults);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ if (aResultLimit > 0 && nbResults > static_cast<uint32_t>(aResultLimit)) {
+ nbResults = static_cast<uint32_t>(aResultLimit);
+ }
+
+ uint32_t i = 0;
+ nsCOMPtr<nsIAbCard> card;
+
+ for (i = 0; i < nbResults; ++i) {
+ card = do_QueryElementAt(resultsArray, i, &retCode);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+
+ aListener->OnSearchFoundCard(card);
+ }
+
+ aListener->OnSearchFinished(NS_OK, true, nullptr, ""_ns);
+ return retCode;
+}
+
+// This function expects the aCards array to already be created.
+nsresult nsAbOutlookDirectory::GetCards(nsIMutableArray* aCards,
+ SRestriction* aRestriction) {
+ nsAbWinHelperGuard mapiAddBook;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ nsMapiEntryArray cardEntries;
+ LPSRestriction restriction = (LPSRestriction)aRestriction;
+
+ if (!mapiAddBook->GetCards(*mDirEntry, restriction, cardEntries)) {
+ PRINTF(("Cannot get cards.\n"));
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ nsAutoCString ourUID;
+ if (m_IsMailList) {
+ // Look up the parent directory (top-level directory) in the
+ // AddrBookManager. That relies on the fact that the top-level
+ // directory is already in its map before being initialised.
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString dirURI(kOutlookDirectoryScheme);
+ dirURI.Append(mParentEntryId);
+ nsCOMPtr<nsIAbDirectory> owningDir;
+ rv = abManager->GetDirectory(dirURI, getter_AddRefs(owningDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ owningDir->GetUID(ourUID);
+ } else {
+ GetUID(ourUID);
+ }
+
+ rv = NS_OK;
+
+ for (ULONG card = 0; card < cardEntries.mNbEntries; ++card) {
+ nsAutoCString cardEntryString;
+ nsAutoCString cardURI(kOutlookCardScheme);
+ nsCOMPtr<nsIAbCard> childCard;
+ cardEntries.mEntries[card].ToString(cardEntryString);
+ cardURI.Append(cardEntryString);
+
+ rv = OutlookCardForURI(cardURI, getter_AddRefs(childCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Make sure the card has a UID before setting its directory UID.
+ // This is a bit of a hack. If we get the UID of the card before setting its
+ // directory UID, we can avoid an unwanted `ModifyCard()` call inside
+ // `nsAbCardProperty::SetUID()`.
+ nsCString dummy;
+ childCard->GetUID(dummy);
+ childCard->SetDirectoryUID(ourUID);
+
+ aCards->AppendElement(childCard);
+ }
+ return rv;
+}
+
+nsresult nsAbOutlookDirectory::GetNodes(nsIMutableArray* aNodes) {
+ NS_ENSURE_ARG_POINTER(aNodes);
+
+ nsAbWinHelperGuard mapiAddBook;
+ nsMapiEntryArray nodeEntries;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ if (!mapiAddBook->GetNodes(*mDirEntry, nodeEntries)) {
+ PRINTF(("Cannot get nodes.\n"));
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString topEntryString;
+ mDirEntry->ToString(topEntryString);
+
+ for (ULONG node = 0; node < nodeEntries.mNbEntries; ++node) {
+ nsAutoCString dirEntryString;
+ nsAutoCString uri(kOutlookDirectoryScheme);
+ uri.Append(topEntryString);
+ uri.Append('/');
+ nodeEntries.mEntries[node].ToString(dirEntryString);
+ uri.Append(dirEntryString);
+
+ RefPtr<nsAbOutlookDirectory> directory = new nsAbOutlookDirectory;
+
+ // We will later need the URI of the parent directory, so store it here.
+ directory->mParentEntryId = topEntryString;
+ directory->Init(uri.get());
+
+ nsCOMPtr<nsIAbDirectory> dir = do_QueryObject(directory);
+ aNodes->AppendElement(dir);
+ }
+ return rv;
+}
+
+nsresult nsAbOutlookDirectory::commonNotification(
+ nsISupports* aItem, const char* aTopic, const char* aNotificationUID) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ // `dirUID` needs to stay in scope until the end of the function.
+ nsAutoCString dirUID;
+ if (!aNotificationUID) {
+ // Use the UID of the directory.
+ GetUID(dirUID);
+ aNotificationUID = dirUID.get();
+ }
+
+ observerService->NotifyObservers(
+ aItem, aTopic, NS_ConvertUTF8toUTF16(aNotificationUID).get());
+ return NS_OK;
+}
+
+nsresult nsAbOutlookDirectory::NotifyItemDeletion(
+ nsISupports* aItem, bool aIsCard, const char* aNotificationUID) {
+ const char* topic;
+ if (aIsCard) {
+ topic = m_IsMailList ? "addrbook-list-member-removed"
+ : "addrbook-contact-deleted";
+ } else {
+ topic = "addrbook-list-deleted";
+ }
+ return commonNotification(aItem, topic, aNotificationUID);
+}
+
+nsresult nsAbOutlookDirectory::NotifyItemAddition(
+ nsISupports* aItem, bool aIsCard, const char* aNotificationUID) {
+ const char* topic;
+ if (aIsCard) {
+ topic = m_IsMailList ? "addrbook-list-member-added"
+ : "addrbook-contact-created";
+ } else {
+ topic = "addrbook-list-created";
+ }
+ return commonNotification(aItem, topic, aNotificationUID);
+}
+
+nsresult nsAbOutlookDirectory::NotifyItemModification(
+ nsISupports* aItem, bool aIsCard, const char* aNotificationUID) {
+ return commonNotification(
+ aItem, aIsCard ? "addrbook-contact-updated" : "addrbook-list-updated",
+ aNotificationUID);
+}
+
+nsresult nsAbOutlookDirectory::NotifyCardPropertyChanges(nsIAbCard* aOld,
+ nsIAbCard* aNew) {
+ mozilla::JSONStringWriteFunc<nsCString> jsonString;
+ mozilla::JSONWriter w(jsonString);
+ w.Start();
+ w.StartObjectElement();
+ bool somethingChanged = false;
+ for (uint32_t i = 0; i < sizeof(CardStringProperties) / sizeof(char*); i++) {
+ nsAutoCString oldValue;
+ nsAutoCString newValue;
+ aOld->GetPropertyAsAUTF8String(CardStringProperties[i], oldValue);
+ aNew->GetPropertyAsAUTF8String(CardStringProperties[i], newValue);
+
+ if (!oldValue.Equals(newValue)) {
+ somethingChanged = true;
+ w.StartObjectProperty(mozilla::MakeStringSpan(CardStringProperties[i]));
+ if (oldValue.IsEmpty()) {
+ w.NullProperty("oldValue");
+ } else {
+ w.StringProperty("oldValue", mozilla::MakeStringSpan(oldValue.get()));
+ }
+ if (newValue.IsEmpty()) {
+ w.NullProperty("newValue");
+ } else {
+ w.StringProperty("newValue", mozilla::MakeStringSpan(newValue.get()));
+ }
+ w.EndObject();
+ }
+ }
+
+ for (uint32_t i = 0; i < sizeof(CardIntProperties) / sizeof(char*); i++) {
+ uint32_t oldValue = 0;
+ uint32_t newValue = 0;
+ aOld->GetPropertyAsUint32(CardIntProperties[i], &oldValue);
+ aNew->GetPropertyAsUint32(CardIntProperties[i], &newValue);
+
+ if (oldValue != newValue) {
+ somethingChanged = true;
+ w.StartObjectProperty(mozilla::MakeStringSpan(CardIntProperties[i]));
+ if (oldValue == 0) {
+ w.NullProperty("oldValue");
+ } else {
+ w.IntProperty("oldValue", oldValue);
+ }
+ if (newValue == 0) {
+ w.NullProperty("newValue");
+ } else {
+ w.IntProperty("newValue", newValue);
+ }
+ w.EndObject();
+ }
+ }
+ w.EndObject();
+ w.End();
+
+#if PRINT_TO_CONSOLE
+ printf("%s", jsonString.StringCRef().get());
+#endif
+
+ if (somethingChanged) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(
+ aNew, "addrbook-contact-properties-updated",
+ NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get());
+ }
+ return NS_OK;
+}
+
+static void UnicodeToWord(const char16_t* aUnicode, WORD& aWord) {
+ aWord = 0;
+ if (aUnicode == nullptr || *aUnicode == 0) {
+ return;
+ }
+ nsresult errorCode = NS_OK;
+ nsAutoString unichar(aUnicode);
+
+ aWord = static_cast<WORD>(unichar.ToInteger(&errorCode));
+ if (NS_FAILED(errorCode)) {
+ PRINTF(("Error conversion string %S: %08x.\n", (wchar_t*)(unichar.get()),
+ errorCode));
+ }
+}
+
+#define PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST "mail.addr_book.lastnamefirst"
+
+NS_IMETHODIMP nsAbOutlookDirectory::ModifyCard(nsIAbCard* aModifiedCard) {
+ return ModifyCardInternal(aModifiedCard, false);
+}
+
+nsresult nsAbOutlookDirectory::ModifyCardInternal(nsIAbCard* aModifiedCard,
+ bool aIsAddition) {
+ NS_ENSURE_ARG_POINTER(aModifiedCard);
+
+ nsString* properties = nullptr;
+ nsAutoString utility;
+ nsAbWinHelperGuard mapiAddBook;
+
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ nsCString cardEntryString;
+ nsresult retCode = ExtractCardEntry(aModifiedCard, cardEntryString);
+ NS_ENSURE_SUCCESS(retCode, retCode);
+ // If we don't have the card entry, we can't work.
+ if (cardEntryString.IsEmpty()) return NS_ERROR_FAILURE;
+
+ nsMapiEntry cardEntry;
+ cardEntry.Assign(cardEntryString);
+
+ // Get the existing card.
+ nsCString uri;
+ nsCOMPtr<nsIAbCard> oldCard;
+ aModifiedCard->GetPropertyAsAUTF8String("OutlookEntryURI", uri);
+ // If the following fails, we didn't get the old card, not fatal.
+ OutlookCardForURI(uri, getter_AddRefs(oldCard));
+
+ // First, all the standard properties in one go
+ properties = new nsString[index_LastProp];
+ if (!properties) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aModifiedCard->GetFirstName(properties[index_FirstName]);
+ aModifiedCard->GetLastName(properties[index_LastName]);
+ // This triple search for something to put in the name
+ // is because in the case of a mailing list edition in
+ // Mozilla, the display name will not be provided, and
+ // MAPI doesn't allow that, so we fall back on an optional
+ // name, and when all fails, on the email address.
+ aModifiedCard->GetDisplayName(properties[index_DisplayName]);
+ if (properties[index_DisplayName].IsEmpty()) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t format;
+ rv = prefBranch->GetIntPref(PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST, &format);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aModifiedCard->GenerateName(format, nullptr,
+ properties[index_DisplayName]);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (properties[index_DisplayName].IsEmpty()) {
+ aModifiedCard->GetPrimaryEmail(properties[index_DisplayName]);
+ }
+ }
+
+ nsMapiEntry dirEntry;
+ if (m_IsMailList) {
+ nsAutoCString uri(mURI);
+ // Trim off the mailing list entry ID from the mailing list URI
+ // to get the top-level directory entry ID.
+ nsAutoCString topEntryString;
+ int32_t slashPos = uri.RFindChar('/');
+ uri.SetLength(slashPos);
+ makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString);
+ dirEntry.Assign(topEntryString);
+ } else {
+ dirEntry.Assign(mDirEntry->mByteCount, mDirEntry->mEntryId);
+ }
+
+ aModifiedCard->SetDisplayName(properties[index_DisplayName]);
+ aModifiedCard->GetPropertyAsAString(kNicknameProperty,
+ properties[index_NickName]);
+ aModifiedCard->GetPropertyAsAString(kWorkPhoneProperty,
+ properties[index_WorkPhoneNumber]);
+ aModifiedCard->GetPropertyAsAString(kHomePhoneProperty,
+ properties[index_HomePhoneNumber]);
+ aModifiedCard->GetPropertyAsAString(kFaxProperty,
+ properties[index_WorkFaxNumber]);
+ aModifiedCard->GetPropertyAsAString(kPagerProperty,
+ properties[index_PagerNumber]);
+ aModifiedCard->GetPropertyAsAString(kCellularProperty,
+ properties[index_MobileNumber]);
+ aModifiedCard->GetPropertyAsAString(kHomeCityProperty,
+ properties[index_HomeCity]);
+ aModifiedCard->GetPropertyAsAString(kHomeStateProperty,
+ properties[index_HomeState]);
+ aModifiedCard->GetPropertyAsAString(kHomeZipCodeProperty,
+ properties[index_HomeZip]);
+ aModifiedCard->GetPropertyAsAString(kHomeCountryProperty,
+ properties[index_HomeCountry]);
+ aModifiedCard->GetPropertyAsAString(kWorkCityProperty,
+ properties[index_WorkCity]);
+ aModifiedCard->GetPropertyAsAString(kWorkStateProperty,
+ properties[index_WorkState]);
+ aModifiedCard->GetPropertyAsAString(kWorkZipCodeProperty,
+ properties[index_WorkZip]);
+ aModifiedCard->GetPropertyAsAString(kWorkCountryProperty,
+ properties[index_WorkCountry]);
+ aModifiedCard->GetPropertyAsAString(kJobTitleProperty,
+ properties[index_JobTitle]);
+ aModifiedCard->GetPropertyAsAString(kDepartmentProperty,
+ properties[index_Department]);
+ aModifiedCard->GetPropertyAsAString(kCompanyProperty,
+ properties[index_Company]);
+ aModifiedCard->GetPropertyAsAString(kWorkWebPageProperty,
+ properties[index_WorkWebPage]);
+ aModifiedCard->GetPropertyAsAString(kHomeWebPageProperty,
+ properties[index_HomeWebPage]);
+ aModifiedCard->GetPropertyAsAString(kNotesProperty, properties[index_Notes]);
+ if (!mapiAddBook->SetPropertiesUString(dirEntry, cardEntry,
+ OutlookCardMAPIProps, index_LastProp,
+ properties)) {
+ PRINTF(("Cannot set general properties.\n"));
+ }
+
+ delete[] properties;
+ nsString unichar;
+ nsString unichar2;
+ WORD year = 0;
+ WORD month = 0;
+ WORD day = 0;
+
+ aModifiedCard->GetPrimaryEmail(unichar);
+ if (!mapiAddBook->SetPropertyUString(cardEntry, PR_EMAIL_ADDRESS_W,
+ unichar.get())) {
+ PRINTF(("Cannot set primary email.\n"));
+ }
+ aModifiedCard->GetPropertyAsAString(kHomeAddressProperty, unichar);
+ aModifiedCard->GetPropertyAsAString(kHomeAddress2Property, unichar2);
+
+ utility.Assign(unichar.get());
+ if (!utility.IsEmpty()) utility.AppendLiteral("\r\n");
+
+ utility.Append(unichar2.get());
+ if (!mapiAddBook->SetPropertyUString(cardEntry, PR_HOME_ADDRESS_STREET_W,
+ utility.get())) {
+ PRINTF(("Cannot set home address.\n"));
+ }
+
+ unichar.Truncate();
+ aModifiedCard->GetPropertyAsAString(kWorkAddressProperty, unichar);
+ unichar2.Truncate();
+ aModifiedCard->GetPropertyAsAString(kWorkAddress2Property, unichar2);
+
+ utility.Assign(unichar.get());
+ if (!utility.IsEmpty()) utility.AppendLiteral("\r\n");
+
+ utility.Append(unichar2.get());
+ if (!mapiAddBook->SetPropertyUString(cardEntry, PR_BUSINESS_ADDRESS_STREET_W,
+ utility.get())) {
+ PRINTF(("Cannot set work address.\n"));
+ }
+
+ unichar.Truncate();
+ aModifiedCard->GetPropertyAsAString(kBirthYearProperty, unichar);
+ UnicodeToWord(unichar.get(), year);
+ unichar.Truncate();
+ aModifiedCard->GetPropertyAsAString(kBirthMonthProperty, unichar);
+ UnicodeToWord(unichar.get(), month);
+ unichar.Truncate();
+ aModifiedCard->GetPropertyAsAString(kBirthDayProperty, unichar);
+ UnicodeToWord(unichar.get(), day);
+ if (!mapiAddBook->SetPropertyDate(dirEntry, cardEntry, true, PR_BIRTHDAY,
+ year, month, day)) {
+ PRINTF(("Cannot set date.\n"));
+ }
+
+ if (!aIsAddition) {
+ NotifyItemModification(aModifiedCard, true);
+ if (oldCard) NotifyCardPropertyChanges(oldCard, aModifiedCard);
+ }
+
+ return retCode;
+}
+
+static void splitString(nsString& aSource, nsString& aTarget) {
+ aTarget.Truncate();
+ int32_t offset = aSource.FindChar('\n');
+
+ if (offset >= 0) {
+ const char16_t* source = aSource.get() + offset + 1;
+ while (*source) {
+ if (*source == '\n' || *source == '\r')
+ aTarget.Append(char16_t(' '));
+ else
+ aTarget.Append(*source);
+ ++source;
+ }
+ int32_t offsetCR = aSource.FindChar('\r');
+ aSource.SetLength(offsetCR >= 0 ? offsetCR : offset);
+ }
+}
+
+nsresult nsAbOutlookDirectory::OutlookCardForURI(const nsACString& aUri,
+ nsIAbCard** newCard) {
+ NS_ENSURE_ARG_POINTER(newCard);
+
+ nsAutoCString cardEntryString;
+ makeEntryIdFromURI(kOutlookCardScheme, PromiseFlatCString(aUri).get(),
+ cardEntryString);
+
+ nsAbWinHelperGuard mapiAddBook;
+ if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbCard> card =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ card->SetPropertyAsAUTF8String("OutlookEntryURI", aUri);
+
+ nsMapiEntry cardEntry;
+ cardEntry.Assign(cardEntryString);
+
+ nsString unichars[index_LastProp];
+ bool success[index_LastProp];
+
+ nsMapiEntry dirEntry;
+ if (m_IsMailList) {
+ nsAutoCString uri(mURI);
+ // Trim off the mailing list entry ID from the mailing list URI
+ // to get the top-level directory entry ID.
+ nsAutoCString topEntryString;
+ int32_t slashPos = uri.RFindChar('/');
+ uri.SetLength(slashPos);
+ makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString);
+ dirEntry.Assign(topEntryString);
+ } else {
+ dirEntry.Assign(mDirEntry->mByteCount, mDirEntry->mEntryId);
+ }
+
+ if (mapiAddBook->GetPropertiesUString(dirEntry, cardEntry,
+ OutlookCardMAPIProps, index_LastProp,
+ unichars, success)) {
+ if (success[index_FirstName]) card->SetFirstName(unichars[index_FirstName]);
+ if (success[index_LastName]) card->SetLastName(unichars[index_LastName]);
+ if (success[index_DisplayName])
+ card->SetDisplayName(unichars[index_DisplayName]);
+
+#define SETPROP(name, index) \
+ if (success[index]) card->SetPropertyAsAString(name, unichars[index])
+ SETPROP(kNicknameProperty, index_NickName);
+ SETPROP(kWorkPhoneProperty, index_WorkPhoneNumber);
+ SETPROP(kHomePhoneProperty, index_HomePhoneNumber);
+ SETPROP(kFaxProperty, index_WorkFaxNumber);
+ SETPROP(kPagerProperty, index_PagerNumber);
+ SETPROP(kCellularProperty, index_MobileNumber);
+ SETPROP(kHomeCityProperty, index_HomeCity);
+ SETPROP(kHomeStateProperty, index_HomeState);
+ SETPROP(kHomeZipCodeProperty, index_HomeZip);
+ SETPROP(kHomeCountryProperty, index_HomeCountry);
+ SETPROP(kWorkCityProperty, index_WorkCity);
+ SETPROP(kWorkStateProperty, index_WorkState);
+ SETPROP(kWorkZipCodeProperty, index_WorkZip);
+ SETPROP(kWorkCountryProperty, index_WorkCountry);
+ SETPROP(kJobTitleProperty, index_JobTitle);
+ SETPROP(kDepartmentProperty, index_Department);
+ SETPROP(kCompanyProperty, index_Company);
+ SETPROP(kWorkWebPageProperty, index_WorkWebPage);
+ SETPROP(kHomeWebPageProperty, index_HomeWebPage);
+ SETPROP(kNotesProperty, index_Notes);
+ }
+
+ ULONG cardType = 0;
+ if (mapiAddBook->GetPropertyLong(cardEntry, PR_OBJECT_TYPE, cardType)) {
+ card->SetIsMailList(cardType == MAPI_DISTLIST);
+ if (cardType == MAPI_DISTLIST) {
+ nsCString dirEntryString;
+ mDirEntry->ToString(dirEntryString);
+ nsAutoCString uri(kOutlookDirectoryScheme);
+ uri.Append(dirEntryString);
+ uri.Append('/');
+ nsCString originalUID;
+ AlignListEntryStringAndGetUID(cardEntryString, originalUID);
+ uri.Append(cardEntryString);
+ card->SetMailListURI(uri.get());
+ if (!originalUID.IsEmpty()) card->SetUID(originalUID);
+
+ // In case the display is by "First Last" or "Last, First", give the card
+ // a name, otherwise nothing is displayed.
+ if (success[index_DisplayName])
+ card->SetLastName(unichars[index_DisplayName]);
+ }
+ }
+
+ nsAutoString unichar;
+ nsAutoString unicharBis;
+ if (mapiAddBook->GetPropertyUString(cardEntry, PR_EMAIL_ADDRESS_W, unichar)) {
+ card->SetPrimaryEmail(unichar);
+ }
+ if (mapiAddBook->GetPropertyUString(cardEntry, PR_HOME_ADDRESS_STREET_W,
+ unichar)) {
+ splitString(unichar, unicharBis);
+ card->SetPropertyAsAString(kHomeAddressProperty, unichar);
+ card->SetPropertyAsAString(kHomeAddress2Property, unicharBis);
+ }
+ if (mapiAddBook->GetPropertyUString(cardEntry, PR_BUSINESS_ADDRESS_STREET_W,
+ unichar)) {
+ splitString(unichar, unicharBis);
+ card->SetPropertyAsAString(kWorkAddressProperty, unichar);
+ card->SetPropertyAsAString(kWorkAddress2Property, unicharBis);
+ }
+
+ WORD year = 0, month = 0, day = 0;
+ if (mapiAddBook->GetPropertyDate(dirEntry, cardEntry, true, PR_BIRTHDAY, year,
+ month, day)) {
+ card->SetPropertyAsUint32(kBirthYearProperty, year);
+ card->SetPropertyAsUint32(kBirthMonthProperty, month);
+ card->SetPropertyAsUint32(kBirthDayProperty, day);
+ }
+
+ card.forget(newCard);
+ return NS_OK;
+}
+
+void nsAbOutlookDirectory::AlignListEntryStringAndGetUID(
+ nsCString& aEntryString, nsCString& aOriginalUID) {
+ // Sadly when scanning for cards and finding a distribution list, the
+ // entry ID is different to the entry ID returned when scanning the top level
+ // directory for distribution lists. We make the adjustment here.
+ // We also retrieve the original UID from the mailing list.
+ nsAbWinHelperGuard mapiAddBook;
+ if (!mapiAddBook->IsOK()) return;
+
+ uint32_t nbLists = 0;
+ nsresult rv = m_AddressList->GetLength(&nbLists);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ for (uint32_t i = 0; i < nbLists; i++) {
+ nsCOMPtr<nsIAbDirectory> list = do_QueryElementAt(m_AddressList, i, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Get URI and extract entry ID.
+ nsAutoCString listURI;
+ list->GetURI(listURI);
+ int ind = listURI.RFindChar('/');
+ listURI = Substring(listURI, ind + 1);
+
+ if (aEntryString.Equals(listURI)) {
+ list->GetUID(aOriginalUID);
+ return;
+ }
+ if (mapiAddBook->CompareEntryIDs(aEntryString, listURI)) {
+ PRINTF(("Entry ID for mailing list replaced:\nWas: %s\nNow: %s\n",
+ aEntryString.get(), listURI.get()));
+ aEntryString = listURI;
+ list->GetUID(aOriginalUID);
+ return;
+ }
+ }
+ PRINTF(("Entry ID for mailing list not found.\n"));
+}
diff --git a/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h
new file mode 100644
index 0000000000..203f82df4a
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsAbOutlookDirectory_h___
+#define nsAbOutlookDirectory_h___
+
+#include "mozilla/Attributes.h"
+#include "nsIAbCard.h"
+#include "nsAbDirProperty.h"
+#include "nsIAbDirectoryQuery.h"
+#include "nsIAbDirSearchListener.h"
+#include "nsInterfaceHashtable.h"
+#include "nsIMutableArray.h"
+#include "nsAbWinHelper.h"
+
+struct nsMapiEntry;
+
+class nsAbOutlookDirectory : public nsAbDirProperty, // nsIAbDirectory
+ public nsIAbDirectoryQuery,
+ public nsIAbDirSearchListener {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIABDIRSEARCHLISTENER
+
+ nsAbOutlookDirectory(void);
+
+ // nsAbDirProperty methods
+ NS_IMETHOD GetDirType(int32_t* aDirType) override;
+ NS_IMETHOD GetURI(nsACString& aURI) override;
+ NS_IMETHOD GetChildCardCount(uint32_t* aCount) override;
+ NS_IMETHOD GetChildCards(nsTArray<RefPtr<nsIAbCard>>& result) override;
+ NS_IMETHOD GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& result) override;
+ NS_IMETHOD HasCard(nsIAbCard* aCard, bool* aHasCard) override;
+ NS_IMETHOD HasDirectory(nsIAbDirectory* aDirectory,
+ bool* aHasDirectory) override;
+ NS_IMETHOD DeleteCards(const nsTArray<RefPtr<nsIAbCard>>& aCards) override;
+ NS_IMETHOD DeleteDirectory(nsIAbDirectory* aDirectory) override;
+ NS_IMETHOD AddCard(nsIAbCard* aData, nsIAbCard** addedCard) override;
+ NS_IMETHOD ModifyCard(nsIAbCard* aModifiedCard) override;
+ NS_IMETHOD DropCard(nsIAbCard* aData, bool needToCopyCard) override;
+ NS_IMETHOD AddMailList(nsIAbDirectory* aMailList,
+ nsIAbDirectory** addedList) override;
+ NS_IMETHOD EditMailListToDatabase(nsIAbCard* listCard) override;
+ NS_IMETHOD CardForEmailAddress(const nsACString& aEmailAddress,
+ nsIAbCard** aResult) override;
+
+ // nsAbDirProperty method
+ NS_IMETHOD Init(const char* aUri) override;
+ // nsIAbDirectoryQuery methods
+ NS_DECL_NSIABDIRECTORYQUERY
+ // Perform a MAPI query.
+ nsresult ExecuteQuery(SRestriction* aRestriction,
+ nsIAbDirSearchListener* aListener,
+ int32_t aResultLimit);
+ NS_IMETHOD Search(const nsAString& query, const nsAString& searchString,
+ nsIAbDirSearchListener* listener) override;
+
+ protected:
+ nsresult StopSearch();
+ nsresult ExtractCardEntry(nsIAbCard* aCard, nsCString& aEntry);
+ nsresult ExtractDirectoryEntry(nsIAbDirectory* aDirectory, nsCString& aEntry);
+ void AlignListEntryStringAndGetUID(nsCString& aEntryString,
+ nsCString& aOriginalUID);
+
+ // Retrieve hierarchy as cards, with an optional restriction
+ nsresult GetCards(nsIMutableArray* aCards, SRestriction* aRestriction);
+ // Retrieve hierarchy as directories
+ nsresult GetNodes(nsIMutableArray* aNodes);
+ nsresult ModifyCardInternal(nsIAbCard* aModifiedCard, bool aIsAddition);
+ // Notification for the UI.
+ nsresult NotifyItemDeletion(nsISupports* aItem, bool aIsCard,
+ const char* aNotificationUID = nullptr);
+ nsresult NotifyItemAddition(nsISupports* aItem, bool aIsCard,
+ const char* aNotificationUID = nullptr);
+ nsresult NotifyItemModification(nsISupports* aItem, bool aIsCard,
+ const char* aNotificationUID = nullptr);
+ nsresult NotifyCardPropertyChanges(nsIAbCard* aOld, nsIAbCard* aNew);
+ nsresult commonNotification(nsISupports* aItem, const char* aTopic,
+ const char* aNotificationUID);
+ // Utility to produce a card from a URI.
+ nsresult OutlookCardForURI(const nsACString& aUri, nsIAbCard** card);
+
+ nsMapiEntry* mDirEntry;
+ // Keep track of context ID to be passed back from `DoQuery()`.
+ int32_t mCurrentQueryId;
+ // Data for the search interfaces
+ int32_t mSearchContext;
+
+ private:
+ virtual ~nsAbOutlookDirectory(void);
+ nsCString mParentEntryId;
+
+ // This is totally quirky. `m_AddressList` is defined in
+ // class nsAbDirProperty to hold a list of mailing lists,
+ // but there is no member to hold a list of cards.
+ // It gets worse: For mailing lists, `m_AddressList` holds the
+ // list of cards.
+ // So we'll do it as the Mac AB does and define a member for it.
+ // nsIMutableArray is used, because then it is interchangeable with
+ // `m_AddressList`.
+ nsCOMPtr<nsIMutableArray> mCardList;
+};
+
+enum {
+ index_DisplayName = 0,
+ index_FirstName,
+ index_LastName,
+ index_NickName,
+ index_WorkPhoneNumber,
+ index_HomePhoneNumber,
+ index_WorkFaxNumber,
+ index_PagerNumber,
+ index_MobileNumber,
+ index_HomeCity,
+ index_HomeState,
+ index_HomeZip,
+ index_HomeCountry,
+ index_WorkCity,
+ index_WorkState,
+ index_WorkZip,
+ index_WorkCountry,
+ index_JobTitle,
+ index_Department,
+ index_Company,
+ index_WorkWebPage,
+ index_HomeWebPage,
+ index_Notes,
+ index_LastProp
+};
+
+// The following properties are retrieved from the contact associated
+// with the address book entry. Email not available on contact,
+// the contact has three named email properties.
+static const ULONG OutlookCardMAPIProps[] = {
+ PR_DISPLAY_NAME_W,
+ PR_GIVEN_NAME_W,
+ PR_SURNAME_W,
+ PR_NICKNAME_W,
+ PR_BUSINESS_TELEPHONE_NUMBER_W,
+ PR_HOME_TELEPHONE_NUMBER_W,
+ PR_BUSINESS_FAX_NUMBER_W,
+ PR_PAGER_TELEPHONE_NUMBER_W,
+ PR_MOBILE_TELEPHONE_NUMBER_W,
+ PR_HOME_ADDRESS_CITY_W,
+ PR_HOME_ADDRESS_STATE_OR_PROVINCE_W,
+ PR_HOME_ADDRESS_POSTAL_CODE_W,
+ PR_HOME_ADDRESS_COUNTRY_W,
+ PR_BUSINESS_ADDRESS_CITY_W,
+ PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_W,
+ PR_BUSINESS_ADDRESS_POSTAL_CODE_W,
+ PR_BUSINESS_ADDRESS_COUNTRY_W,
+ PR_TITLE_W,
+ PR_DEPARTMENT_NAME_W,
+ PR_COMPANY_NAME_W,
+ PR_BUSINESS_HOME_PAGE_W,
+ PR_PERSONAL_HOME_PAGE_W,
+ PR_BODY_W};
+
+static const char* CardStringProperties[] = {
+ kFirstNameProperty, kLastNameProperty, kDisplayNameProperty,
+ kNicknameProperty, kPriEmailProperty,
+
+ kHomeAddressProperty, kHomeAddress2Property, kHomeCityProperty,
+ kHomeStateProperty, kHomeZipCodeProperty, kHomeCountryProperty,
+ kHomeWebPageProperty,
+
+ kWorkAddressProperty, kWorkAddress2Property, kWorkCityProperty,
+ kWorkStateProperty, kWorkZipCodeProperty, kWorkCountryProperty,
+ kWorkWebPageProperty,
+
+ kHomePhoneProperty, kWorkPhoneProperty, kFaxProperty,
+ kPagerProperty, kCellularProperty,
+
+ kJobTitleProperty, kDepartmentProperty, kCompanyProperty,
+ kNotesProperty};
+
+static const char* CardIntProperties[] = {
+ kBirthYearProperty, kBirthMonthProperty, kBirthDayProperty};
+
+#endif // nsAbOutlookDirectory_h___
diff --git a/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp b/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp
new file mode 100644
index 0000000000..7000c90317
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsAbOutlookInterface.h"
+#include "nsAbWinHelper.h"
+#include "nsComponentManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsAbOutlookInterface, nsIAbOutlookInterface)
+
+nsAbOutlookInterface::nsAbOutlookInterface(void) {}
+
+nsAbOutlookInterface::~nsAbOutlookInterface(void) {}
+
+NS_IMETHODIMP
+nsAbOutlookInterface::GetFolderURIs(const nsACString& aURI,
+ nsTArray<nsCString>& uris) {
+ uris.Clear();
+ nsresult rv = NS_OK;
+
+ nsAbWinHelperGuard mapiAddBook;
+ nsMapiEntryArray folders;
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mapiAddBook->IsOK() || !mapiAddBook->GetFolders(folders)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uris.SetCapacity(folders.mNbEntries);
+
+ for (ULONG i = 0; i < folders.mNbEntries; ++i) {
+ nsAutoCString entryId;
+ nsAutoCString uri(kOutlookDirectoryScheme);
+ folders.mEntries[i].ToString(entryId);
+ uri.Append(entryId);
+ uris.AppendElement(uri);
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbOutlookInterface.h b/comm/mailnews/addrbook/src/nsAbOutlookInterface.h
new file mode 100644
index 0000000000..fd51f83516
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbOutlookInterface.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#ifndef nsAbOutlookInterface_h___
+#define nsAbOutlookInterface_h___
+
+#include "nsIAbOutlookInterface.h"
+
+class nsAbOutlookInterface : public nsIAbOutlookInterface {
+ public:
+ nsAbOutlookInterface(void);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIABOUTLOOKINTERFACE
+
+ private:
+ virtual ~nsAbOutlookInterface(void);
+};
+
+#endif // nsAbOutlookInterface_h___
diff --git a/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp
new file mode 100644
index 0000000000..bf5e158de3
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp
@@ -0,0 +1,293 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsAbQueryStringToExpression.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsITextToSubURI.h"
+#include "nsAbBooleanExpression.h"
+#include "plstr.h"
+
+/**
+ * This code parses the query expression passed in as an addressbook URI.
+ * The expression takes the form:
+ * (BOOL1(FIELD1,OP1,VALUE1)..(FIELDn,OPn,VALUEn)(BOOL2(FIELD1,OP1,VALUE1)...)...)
+ *
+ * BOOLn A boolean operator joining subsequent terms delimited by ().
+ * For possible values see CreateBooleanExpression().
+ * FIELDn An addressbook card data field.
+ * OPn An operator for the search term.
+ * For possible values see CreateBooleanConditionString().
+ * VALUEn The value to be matched in the FIELDn via the OPn operator.
+ * The value must be URL encoded by the caller, if it contains any
+ * special characters including '(' and ')'.
+ */
+nsresult nsAbQueryStringToExpression::Convert(
+ const nsACString& aQueryString, nsIAbBooleanExpression** expression) {
+ nsresult rv;
+
+ nsAutoCString q(aQueryString);
+ q.StripWhitespace();
+ const char* queryChars = q.get();
+
+ nsCOMPtr<nsISupports> s;
+ rv = ParseExpression(&queryChars, getter_AddRefs(s));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Case: Not end of string
+ if (*queryChars != 0) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAbBooleanExpression> e(do_QueryInterface(s, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e.forget(expression);
+ return rv;
+}
+
+nsresult nsAbQueryStringToExpression::ParseExpression(
+ const char** index, nsISupports** expression) {
+ nsresult rv;
+
+ if (**index == '?') {
+ (*index)++;
+ }
+
+ if (**index != '(') return NS_ERROR_FAILURE;
+
+ const char* indexBracket = *index + 1;
+ while (*indexBracket && *indexBracket != '(' && *indexBracket != ')')
+ indexBracket++;
+
+ // Case: End of string
+ if (*indexBracket == 0) return NS_ERROR_FAILURE;
+
+ // Case: "((" or "()"
+ if (indexBracket == *index + 1) {
+ return NS_ERROR_FAILURE;
+ }
+ // Case: "(*("
+ else if (*indexBracket == '(') {
+ // printf ("Case: (*(: %s\n", *index);
+
+ nsCString operation;
+ rv = ParseOperationEntry(*index, indexBracket, getter_Copies(operation));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbBooleanExpression> e;
+ rv = CreateBooleanExpression(operation.get(), getter_AddRefs(e));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Case: "(*)(*)....(*))"
+ *index = indexBracket;
+ rv = ParseExpressions(index, e);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e.forget(expression);
+ }
+ // Case" "(*)"
+ else if (*indexBracket == ')') {
+ // printf ("Case: (*): %s\n", *index);
+
+ nsCOMPtr<nsIAbBooleanConditionString> conditionString;
+ rv = ParseCondition(index, indexBracket, getter_AddRefs(conditionString));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ conditionString.forget(expression);
+ }
+
+ if (**index != ')') return NS_ERROR_FAILURE;
+
+ (*index)++;
+
+ return NS_OK;
+}
+
+nsresult nsAbQueryStringToExpression::ParseExpressions(
+ const char** index, nsIAbBooleanExpression* expression) {
+ nsresult rv;
+ nsTArray<RefPtr<nsISupports>> expressions;
+
+ // Case: ")(*)(*)....(*))"
+ // printf ("Case: )(*)(*)....(*)): %s\n", *index);
+ while (**index == '(') {
+ nsCOMPtr<nsISupports> childExpression;
+ rv = ParseExpression(index, getter_AddRefs(childExpression));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ expressions.AppendElement(childExpression);
+ }
+
+ if (**index == 0) return NS_ERROR_FAILURE;
+
+ // Case: "))"
+ // printf ("Case: )): %s\n", *index);
+
+ if (**index != ')') return NS_ERROR_FAILURE;
+
+ expression->SetExpressions(expressions);
+
+ return NS_OK;
+}
+
+nsresult nsAbQueryStringToExpression::ParseCondition(
+ const char** index, const char* indexBracketClose,
+ nsIAbBooleanConditionString** conditionString) {
+ nsresult rv;
+
+ (*index)++;
+
+ nsCString entries[3];
+ for (int i = 0; i < 3; i++) {
+ rv = ParseConditionEntry(index, indexBracketClose,
+ getter_Copies(entries[i]));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*index == indexBracketClose) break;
+ }
+
+ if (*index != indexBracketClose) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAbBooleanConditionString> c;
+ rv = CreateBooleanConditionString(entries[0].get(), entries[1].get(),
+ entries[2].get(), getter_AddRefs(c));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ c.forget(conditionString);
+ return NS_OK;
+}
+
+nsresult nsAbQueryStringToExpression::ParseConditionEntry(
+ const char** index, const char* indexBracketClose, char** entry) {
+ const char* indexDeliminator = *index;
+ while (indexDeliminator != indexBracketClose && *indexDeliminator != ',')
+ indexDeliminator++;
+
+ int entryLength = indexDeliminator - *index;
+ if (entryLength)
+ *entry = PL_strndup(*index, entryLength);
+ else
+ *entry = 0;
+
+ if (indexDeliminator != indexBracketClose)
+ *index = indexDeliminator + 1;
+ else
+ *index = indexDeliminator;
+
+ return NS_OK;
+}
+
+nsresult nsAbQueryStringToExpression::ParseOperationEntry(
+ const char* indexBracketOpen1, const char* indexBracketOpen2,
+ char** operation) {
+ int operationLength = indexBracketOpen2 - indexBracketOpen1 - 1;
+ if (operationLength)
+ *operation = PL_strndup(indexBracketOpen1 + 1, operationLength);
+ else
+ *operation = 0;
+
+ return NS_OK;
+}
+
+nsresult nsAbQueryStringToExpression::CreateBooleanExpression(
+ const char* operation, nsIAbBooleanExpression** expression) {
+ nsAbBooleanOperationType op;
+ if (PL_strcasecmp(operation, "and") == 0)
+ op = nsIAbBooleanOperationTypes::AND;
+ else if (PL_strcasecmp(operation, "or") == 0)
+ op = nsIAbBooleanOperationTypes::OR;
+ else if (PL_strcasecmp(operation, "not") == 0)
+ op = nsIAbBooleanOperationTypes::NOT;
+ else
+ return NS_ERROR_FAILURE;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIAbBooleanExpression> expr =
+ do_CreateInstance("@mozilla.org/boolean-expression/n-peer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = expr->SetOperation(op);
+ expr.forget(expression);
+ return rv;
+}
+
+nsresult nsAbQueryStringToExpression::CreateBooleanConditionString(
+ const char* attribute, const char* condition, const char* value,
+ nsIAbBooleanConditionString** conditionString) {
+ if (attribute == 0 || condition == 0 || value == 0) return NS_ERROR_FAILURE;
+
+ nsAbBooleanConditionType c;
+
+ if (PL_strcasecmp(condition, "=") == 0)
+ c = nsIAbBooleanConditionTypes::Is;
+ else if (PL_strcasecmp(condition, "!=") == 0)
+ c = nsIAbBooleanConditionTypes::IsNot;
+ else if (PL_strcasecmp(condition, "lt") == 0)
+ c = nsIAbBooleanConditionTypes::LessThan;
+ else if (PL_strcasecmp(condition, "gt") == 0)
+ c = nsIAbBooleanConditionTypes::GreaterThan;
+ else if (PL_strcasecmp(condition, "bw") == 0)
+ c = nsIAbBooleanConditionTypes::BeginsWith;
+ else if (PL_strcasecmp(condition, "ew") == 0)
+ c = nsIAbBooleanConditionTypes::EndsWith;
+ else if (PL_strcasecmp(condition, "c") == 0)
+ c = nsIAbBooleanConditionTypes::Contains;
+ else if (PL_strcasecmp(condition, "!c") == 0)
+ c = nsIAbBooleanConditionTypes::DoesNotContain;
+ else if (PL_strcasecmp(condition, "~=") == 0)
+ c = nsIAbBooleanConditionTypes::SoundsLike;
+ else if (PL_strcasecmp(condition, "regex") == 0)
+ c = nsIAbBooleanConditionTypes::RegExp;
+ else if (PL_strcasecmp(condition, "ex") == 0)
+ c = nsIAbBooleanConditionTypes::Exists;
+ else if (PL_strcasecmp(condition, "!ex") == 0)
+ c = nsIAbBooleanConditionTypes::DoesNotExist;
+ else
+ return NS_ERROR_FAILURE;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIAbBooleanConditionString> cs = do_CreateInstance(
+ "@mozilla.org/boolean-expression/condition-string;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cs->SetCondition(c);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsITextToSubURI> textToSubURI =
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsString attributeUCS2;
+ nsString valueUCS2;
+
+ rv = textToSubURI->UnEscapeAndConvert(
+ "UTF-8"_ns, nsDependentCString(attribute), attributeUCS2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = textToSubURI->UnEscapeAndConvert("UTF-8"_ns, nsDependentCString(value),
+ valueUCS2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF16toUTF8 attributeUTF8(attributeUCS2);
+
+ rv = cs->SetName(attributeUTF8.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = cs->SetValue(valueUCS2.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ NS_ConvertUTF8toUTF16 valueUCS2(value);
+
+ rv = cs->SetName(attribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = cs->SetValue(valueUCS2.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ cs.forget(conditionString);
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h
new file mode 100644
index 0000000000..acab014278
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsAbQueryStringToExpression_h__
+#define nsAbQueryStringToExpression_h__
+
+#include "nsIAbBooleanExpression.h"
+
+class nsAbQueryStringToExpression {
+ public:
+ static nsresult Convert(const nsACString& aQueryString,
+ nsIAbBooleanExpression** expression);
+
+ protected:
+ static nsresult ParseExpression(const char** index, nsISupports** expression);
+ static nsresult ParseExpressions(const char** index,
+ nsIAbBooleanExpression* expression);
+ static nsresult ParseCondition(const char** index,
+ const char* indexBracketClose,
+ nsIAbBooleanConditionString** conditionString);
+
+ static nsresult ParseConditionEntry(const char** index,
+ const char* indexBracketClose,
+ char** entry);
+ static nsresult ParseOperationEntry(const char* indexBracketOpen1,
+ const char* indexBracketOpen2,
+ char** operation);
+
+ static nsresult CreateBooleanExpression(const char* operation,
+ nsIAbBooleanExpression** expression);
+ static nsresult CreateBooleanConditionString(
+ const char* attribute, const char* condition, const char* value,
+ nsIAbBooleanConditionString** conditionString);
+};
+
+#endif
diff --git a/comm/mailnews/addrbook/src/nsAbWinHelper.cpp b/comm/mailnews/addrbook/src/nsAbWinHelper.cpp
new file mode 100644
index 0000000000..79379c45c7
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbWinHelper.cpp
@@ -0,0 +1,1491 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#define INITGUID
+#define USES_IID_IMAPIProp
+#define USES_IID_IMessage
+#define USES_IID_IMAPIFolder
+#define USES_IID_IMAPIContainer
+#define USES_IID_IABContainer
+#define USES_IID_IMAPITable
+#define USES_IID_IDistList
+
+#include "nsAbWinHelper.h"
+#include "nsMapiAddressBook.h"
+
+#include <mapiguid.h>
+
+#include "mozilla/Logging.h"
+
+#define PRINT_TO_CONSOLE 0
+#if PRINT_TO_CONSOLE
+# define PRINTF(args) printf args
+#else
+static mozilla::LazyLogModule gAbWinHelperLog("AbWinHelper");
+# define PRINTF(args) MOZ_LOG(gAbWinHelperLog, mozilla::LogLevel::Debug, args)
+#endif
+
+// Small utility to ensure release of all MAPI interfaces
+template <class tInterface>
+struct nsMapiInterfaceWrapper {
+ tInterface mInterface;
+
+ nsMapiInterfaceWrapper(void) : mInterface(NULL) {}
+ ~nsMapiInterfaceWrapper(void) {
+ if (mInterface != NULL) {
+ mInterface->Release();
+ }
+ }
+ operator LPUNKNOWN*(void) {
+ return reinterpret_cast<LPUNKNOWN*>(&mInterface);
+ }
+ tInterface operator->(void) const { return mInterface; }
+ operator tInterface*(void) { return &mInterface; }
+ tInterface Get(void) const { return mInterface; }
+};
+
+static void assignEntryID(LPENTRYID& aTarget, LPENTRYID aSource,
+ ULONG aByteCount) {
+ if (aTarget != NULL) {
+ delete[] (reinterpret_cast<LPBYTE>(aTarget));
+ aTarget = NULL;
+ }
+ if (aSource != NULL) {
+ aTarget = reinterpret_cast<LPENTRYID>(new BYTE[aByteCount]);
+ memcpy(aTarget, aSource, aByteCount);
+ }
+}
+
+nsMapiEntry::nsMapiEntry(void) : mByteCount(0), mEntryId(NULL) {
+ MOZ_COUNT_CTOR(nsMapiEntry);
+}
+
+nsMapiEntry::nsMapiEntry(ULONG aByteCount, LPENTRYID aEntryId)
+ : mByteCount(0), mEntryId(NULL) {
+ Assign(aByteCount, aEntryId);
+ MOZ_COUNT_CTOR(nsMapiEntry);
+}
+
+void nsMapiEntry::Move(nsMapiEntry& target, nsMapiEntry& source) {
+ target.mByteCount = source.mByteCount;
+ target.mEntryId = source.mEntryId;
+ source.mByteCount = 0;
+ source.mEntryId = NULL;
+}
+
+nsMapiEntry::~nsMapiEntry(void) {
+ Assign(0, NULL);
+ MOZ_COUNT_DTOR(nsMapiEntry);
+}
+
+void nsMapiEntry::Assign(ULONG aByteCount, LPENTRYID aEntryId) {
+ assignEntryID(mEntryId, aEntryId, aByteCount);
+ mByteCount = aByteCount;
+}
+
+void nsMapiEntry::Assign(const nsCString& aString) {
+ Assign(0, NULL);
+ ULONG byteCount = aString.Length() / 2;
+
+ if ((aString.Length() & 0x01) != 0) {
+ // Something wrong here, we should always get an even number of hex digits.
+ byteCount += 1;
+ }
+ unsigned char* currentTarget = new unsigned char[byteCount];
+
+ mByteCount = byteCount;
+ mEntryId = reinterpret_cast<LPENTRYID>(currentTarget);
+ ULONG j = 0;
+ for (uint32_t i = 0; i < aString.Length(); i += 2) {
+ char c1 = aString.CharAt(i);
+ char c2 = i + 1 < aString.Length() ? aString.CharAt(i + 1) : '0';
+ // clang-format off
+ currentTarget[j] =
+ ((c1 <= '9' ? c1 - '0' : c1 - 'A' + 10) << 4) |
+ (c2 <= '9' ? c2 - '0' : c2 - 'A' + 10);
+ // clang-format on
+ j++;
+ }
+}
+
+void nsMapiEntry::ToString(nsCString& aString) const {
+ aString.Truncate();
+ aString.SetCapacity(mByteCount * 2);
+ char twoBytes[3];
+
+ for (ULONG i = 0; i < mByteCount; i++) {
+ sprintf(twoBytes, "%02X", (reinterpret_cast<unsigned char*>(mEntryId))[i]);
+ aString.Append(twoBytes);
+ }
+}
+
+void nsMapiEntry::Dump(void) const {
+ PRINTF(("%lu\n", mByteCount));
+ for (ULONG i = 0; i < mByteCount; ++i) {
+ PRINTF(("%02X", (reinterpret_cast<unsigned char*>(mEntryId))[i]));
+ }
+ PRINTF(("\n"));
+}
+
+nsMapiEntryArray::nsMapiEntryArray(void) : mEntries(NULL), mNbEntries(0) {
+ MOZ_COUNT_CTOR(nsMapiEntryArray);
+}
+
+nsMapiEntryArray::~nsMapiEntryArray(void) {
+ if (mEntries) {
+ delete[] mEntries;
+ }
+ MOZ_COUNT_DTOR(nsMapiEntryArray);
+}
+
+void nsMapiEntryArray::CleanUp(void) {
+ if (mEntries != NULL) {
+ delete[] mEntries;
+ mEntries = NULL;
+ mNbEntries = 0;
+ }
+}
+
+// Microsoft distinguishes between address book entries and contacts.
+// Address book entries are of class IMailUser and are stored in containers
+// of class IABContainer.
+// Local contacts are stored in the "contacts folder" of class IMAPIFolder and
+// are of class IMessage with "message class" IPM.Contact.
+// For local address books the entry ID of the contact can be derived from the
+// entry ID of the address book entry and vice versa.
+// Most attributes can be retrieved from both classes with some exceptions:
+// The primary e-mail address is only stored on the IMailUser, the contact
+// has three named email properties (which are not used so far).
+// The birthday is only stored on the contact.
+// `OpenMAPIObject()` can open the address book entry as well as the contact,
+// to open the concact it needs to get the message store from via the
+// address book container (or "directory" in Thunderbird terms).
+// Apart from Microsoft documentation, the best source of information
+// is the MAPI programmers mailing list at MAPI-L@PEACH.EASE.LSOFT.COM.
+// All the information that was needed to "refresh" the MAPI implementation
+// in Thunderbird was obtained via these threads:
+// https://peach.ease.lsoft.com/scripts/wa-PEACH.exe?A2=2012&L=MAPI-L&D=0&P=20988415
+// https://peach.ease.lsoft.com/scripts/wa-PEACH.exe?A2=2101&L=MAPI-L&D=0&P=21034512
+
+// Some stuff to access the entry ID of the contact (IMessage, IPM.Contact)
+// from the address book entry ID (IMailUser).
+// The address book entry ID has the following structure, see:
+// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/c33d5b9c-d044-4727-96e2-2051f8419ab1
+#define ABENTRY_FLAGS_LENGTH 4
+#define CONTAB_PROVIDER_ID \
+ "\xFE\x42\xAA\x0A\x18\xC7\x1A\x10\xE8\x85\x0B\x65\x1C\x24\x00\x00"
+#define CONTAB_PROVIDER_ID_LENGTH 16
+#define ABENTRY_VERSION "\x03\x00\x00\x00"
+#define ABENTRY_VERSION_LENGTH 4
+#define ABENTRY_TYPE "\x04\x00\x00\x00"
+#define ABENTRY_TYPE_LENGTH 4
+
+struct AbEntryId {
+ BYTE flags[ABENTRY_FLAGS_LENGTH];
+ BYTE provider[CONTAB_PROVIDER_ID_LENGTH];
+ BYTE version[ABENTRY_VERSION_LENGTH];
+ BYTE type[ABENTRY_TYPE_LENGTH];
+ ULONG index;
+ ULONG length;
+ BYTE idBytes[];
+};
+
+// Some stuff to access the entry IDs of members in a distribution list
+// (IMessage, IPM.DistList):
+// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocntc/02656215-1cb0-4b06-a077-b07e756216be
+// Also handy the reference to the so-called "one off" members:
+// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/b32d23af-85f6-4e92-8387-53a1950ae7ba
+#define DLENTRY_FLAGS_LENGTH 4
+#define DL_PROVIDER_ID \
+ "\xC0\x91\xAD\xD3\x51\x9D\xCF\x11\xA4\xA9\x00\xAA\x00\x47\xFA\xA4"
+#define DL_PROVIDER_ID_LENGTH 16
+#define DLENTRY_TYPE_LENGTH 1
+struct DlEntryId {
+ BYTE flags[DLENTRY_FLAGS_LENGTH];
+ BYTE provider[DL_PROVIDER_ID_LENGTH];
+ BYTE type[DLENTRY_TYPE_LENGTH];
+ BYTE idBytes[];
+};
+
+#define DLENTRY_OO_FLAGS_LENGTH 4
+#define DL_OO_PROVIDER_ID \
+ "\x81\x2B\x1F\xA4\xBE\xA3\x10\x19\x9D\x6E\x00\xDD\x01\x0F\x54\x02"
+#define DL_OO_PROVIDER_ID_LENGTH 16
+struct DlEntryIdOo {
+ BYTE flags[DLENTRY_OO_FLAGS_LENGTH];
+ BYTE provider[DL_OO_PROVIDER_ID_LENGTH];
+ // Note that the documentation specifies a two-byte version followed by a
+ // two-byte "bit collection", but MFCMapi
+ // (https://github.com/stephenegriffin/mfcmapi) shows, for example:
+ // dwBitmask: 0x80010000 = MAPI_UNICODE | MAPI_SEND_NO_RICH_INFO.
+ // Intel x86 and AMD64 / x86-64 hardware is little-endian, so that
+ // equates to 0x0000 0x01 0x80 in memory:
+ // M (1 bit): (mask 0x0100) (MIME) and U (1 bit): (mask 0x0080) (Unicode).
+ ULONG versionAndBits;
+ BYTE variable[];
+};
+
+using namespace mozilla;
+
+uint32_t nsAbWinHelper::sEntryCounter = 0;
+mozilla::StaticMutex nsAbWinHelper::sMutex;
+// There seems to be a deadlock/auto-destruction issue
+// in MAPI when multiple threads perform init/release
+// operations at the same time. So I've put a mutex
+// around both the initialize process and the destruction
+// one. I just hope the rest of the calls don't need the
+// same protection (MAPI is supposed to be thread-safe).
+
+nsAbWinHelper::nsAbWinHelper(void) : mLastError(S_OK), mAddressBook(NULL) {
+ MOZ_COUNT_CTOR(nsAbWinHelper);
+}
+
+nsAbWinHelper::~nsAbWinHelper(void) { MOZ_COUNT_DTOR(nsAbWinHelper); }
+
+BOOL nsAbWinHelper::GetFolders(nsMapiEntryArray& aFolders) {
+ aFolders.CleanUp();
+ nsMapiInterfaceWrapper<LPABCONT> rootFolder;
+ nsMapiInterfaceWrapper<LPMAPITABLE> folders;
+ ULONG objType = 0;
+ ULONG rowCount = 0;
+ SRestriction restriction;
+ SPropTagArray folderColumns;
+
+ mLastError = mAddressBook->OpenEntry(0, NULL, NULL, 0, &objType, rootFolder);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open root %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = rootFolder->GetHierarchyTable(0, folders);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get hierarchy %08lx.\n", mLastError));
+ return FALSE;
+ }
+ // We only take into account modifiable containers,
+ // otherwise, we end up with all the directory services...
+ restriction.rt = RES_BITMASK;
+ restriction.res.resBitMask.ulPropTag = PR_CONTAINER_FLAGS;
+ restriction.res.resBitMask.relBMR = BMR_NEZ;
+ restriction.res.resBitMask.ulMask = AB_MODIFIABLE;
+ mLastError = folders->Restrict(&restriction, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot restrict table %08lx.\n", mLastError));
+ }
+ folderColumns.cValues = 1;
+ folderColumns.aulPropTag[0] = PR_ENTRYID;
+ mLastError = folders->SetColumns(&folderColumns, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set columns %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = folders->GetRowCount(0, &rowCount);
+ if (HR_SUCCEEDED(mLastError)) {
+ aFolders.mEntries = new nsMapiEntry[rowCount];
+ aFolders.mNbEntries = 0;
+ do {
+ LPSRowSet rowSet = NULL;
+
+ rowCount = 0;
+ mLastError = folders->QueryRows(1, 0, &rowSet);
+ if (HR_SUCCEEDED(mLastError)) {
+ rowCount = rowSet->cRows;
+ if (rowCount > 0) {
+ nsMapiEntry& current = aFolders.mEntries[aFolders.mNbEntries++];
+ SPropValue& currentValue = rowSet->aRow->lpProps[0];
+
+ current.Assign(
+ currentValue.Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(currentValue.Value.bin.lpb));
+ }
+ MyFreeProws(rowSet);
+ } else {
+ PRINTF(("Cannot query rows %08lx.\n", mLastError));
+ }
+ } while (rowCount > 0);
+ }
+ return HR_SUCCEEDED(mLastError);
+}
+
+BOOL nsAbWinHelper::GetCards(const nsMapiEntry& aParent,
+ LPSRestriction aRestriction,
+ nsMapiEntryArray& aCards) {
+ aCards.CleanUp();
+ return GetContents(aParent, aRestriction, &aCards.mEntries, aCards.mNbEntries,
+ 0);
+}
+
+BOOL nsAbWinHelper::GetNodes(const nsMapiEntry& aParent,
+ nsMapiEntryArray& aNodes) {
+ aNodes.CleanUp();
+ return GetContents(aParent, NULL, &aNodes.mEntries, aNodes.mNbEntries,
+ MAPI_DISTLIST);
+}
+
+BOOL nsAbWinHelper::GetCardsCount(const nsMapiEntry& aParent, ULONG& aNbCards) {
+ aNbCards = 0;
+ return GetContents(aParent, NULL, NULL, aNbCards, 0);
+}
+
+BOOL nsAbWinHelper::GetPropertyString(const nsMapiEntry& aObject,
+ ULONG aPropertyTag, nsCString& aName) {
+ aName.Truncate();
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ nsMapiEntry nullEntry;
+ if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values,
+ valueCount)) {
+ return FALSE;
+ }
+
+ if (valueCount != 1 || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyString"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ if (PROP_TYPE(values->ulPropTag) == PT_STRING8) {
+ aName = values->Value.lpszA;
+ } else if (PROP_TYPE(values->ulPropTag) == PT_UNICODE) {
+ aName = NS_LossyConvertUTF16toASCII(values->Value.lpszW);
+ } else {
+ PRINTF(("Unexpected return value for property %08lx (x0A is PT_ERROR).\n",
+ values->ulPropTag));
+ success = FALSE;
+ }
+ FreeBuffer(values);
+ return success;
+}
+
+BOOL nsAbWinHelper::GetPropertyUString(const nsMapiEntry& aObject,
+ ULONG aPropertyTag, nsString& aName) {
+ aName.Truncate();
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ nsMapiEntry nullEntry;
+ if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values,
+ valueCount)) {
+ return FALSE;
+ }
+ if (valueCount != 1 || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyUString"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ if (PROP_TYPE(values->ulPropTag) == PT_UNICODE) {
+ aName = values->Value.lpszW;
+ } else if (PROP_TYPE(values->ulPropTag) == PT_STRING8) {
+ aName.AssignASCII(values->Value.lpszA);
+ } else {
+ PRINTF(("Unexpected return value for property %08lx (x0A is PT_ERROR).\n",
+ values->ulPropTag));
+ success = FALSE;
+ }
+ return success;
+}
+
+BOOL nsAbWinHelper::GetPropertiesUString(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[],
+ ULONG aNbProperties, nsString aNames[],
+ bool aSuccess[]) {
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ if (!GetMAPIProperties(aDir, aObject, aPropertyTags, aNbProperties, values,
+ valueCount, true))
+ return FALSE;
+
+ if (valueCount != aNbProperties || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertiesUString"));
+ return FALSE;
+ }
+ for (ULONG i = 0; i < valueCount; ++i) {
+ aNames[i].Truncate();
+ aSuccess[i] = false;
+ if (PROP_ID(values[i].ulPropTag) == PROP_ID(aPropertyTags[i])) {
+ if (PROP_TYPE(values[i].ulPropTag) == PT_STRING8) {
+ aNames[i].AssignASCII(values[i].Value.lpszA);
+ aSuccess[i] = true;
+ } else if (PROP_TYPE(values[i].ulPropTag) == PT_UNICODE) {
+ aNames[i] = values[i].Value.lpszW;
+ aSuccess[i] = true;
+ } else {
+ PRINTF(
+ ("Unexpected return value for property %08lx (x0A is PT_ERROR).\n",
+ values[i].ulPropTag));
+ }
+ }
+ }
+ FreeBuffer(values);
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::GetPropertyDate(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ bool fromContact, ULONG aPropertyTag,
+ WORD& aYear, WORD& aMonth, WORD& aDay) {
+ aYear = 0;
+ aMonth = 0;
+ aDay = 0;
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ if (!GetMAPIProperties(aDir, aObject, &aPropertyTag, 1, values, valueCount,
+ fromContact)) {
+ return FALSE;
+ }
+ if (valueCount != 1 || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyDate"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ if (PROP_TYPE(values->ulPropTag) == PT_SYSTIME) {
+ SYSTEMTIME readableTime;
+ if (FileTimeToSystemTime(&values->Value.ft, &readableTime)) {
+ aYear = readableTime.wYear;
+ aMonth = readableTime.wMonth;
+ aDay = readableTime.wDay;
+ }
+ } else {
+ PRINTF(("Cannot retrieve PT_SYSTIME property %08lx (x0A is PT_ERROR).\n",
+ values->ulPropTag));
+ success = FALSE;
+ }
+ FreeBuffer(values);
+ return success;
+}
+
+BOOL nsAbWinHelper::GetPropertyLong(const nsMapiEntry& aObject,
+ ULONG aPropertyTag, ULONG& aValue) {
+ aValue = 0;
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ nsMapiEntry nullEntry;
+ if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values,
+ valueCount)) {
+ return FALSE;
+ }
+ if (valueCount != 1 || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyLong"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ if (PROP_TYPE(values->ulPropTag) == PT_LONG) {
+ aValue = values->Value.ul;
+ } else {
+ PRINTF(("Cannot retrieve PT_LONG property %08lx (x0A is PT_ERROR).\n",
+ values->ulPropTag));
+ success = FALSE;
+ }
+ FreeBuffer(values);
+ return success;
+}
+
+BOOL nsAbWinHelper::GetPropertyBin(const nsMapiEntry& aObject,
+ ULONG aPropertyTag, nsMapiEntry& aValue) {
+ aValue.Assign(0, NULL);
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ nsMapiEntry nullEntry;
+ if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values,
+ valueCount)) {
+ return FALSE;
+ }
+ if (valueCount != 1 || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyBin"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ if (PROP_TYPE(values->ulPropTag) == PT_BINARY) {
+ aValue.Assign(values->Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(values->Value.bin.lpb));
+ } else {
+ PRINTF(("Cannot retrieve PT_BINARY property %08lx (x0A is PT_ERROR).\n",
+ values->ulPropTag));
+ success = FALSE;
+ }
+
+ FreeBuffer(values);
+ return success;
+}
+
+BOOL nsAbWinHelper::GetPropertiesMVBin(
+ const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties, nsMapiEntry* aEntryIDs[],
+ ULONG aNbElements[], bool aAllocateMore) {
+ LPSPropValue values = NULL;
+ ULONG valueCount = 0;
+
+ // Initialise output arrays.
+ for (ULONG i = 0; i < aNbProperties; i++) {
+ aEntryIDs[i] = NULL;
+ aNbElements[i] = 0;
+ }
+
+ if (!GetMAPIProperties(aDir, aObject, aPropertyTags, aNbProperties, values,
+ valueCount, true)) {
+ return FALSE;
+ }
+ if (valueCount != aNbProperties || values == NULL) {
+ PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyMVBin"));
+ return FALSE;
+ }
+
+ BOOL success = TRUE;
+ for (ULONG i = 0; i < valueCount; i++) {
+ if (PROP_TYPE(values[i].ulPropTag) == PT_MV_BINARY) {
+ ULONG count = values[i].Value.MVbin.cValues;
+ PRINTF(("Found %lu members in DL.\n", count));
+ aEntryIDs[i] = new nsMapiEntry[aAllocateMore ? count + 1 : count];
+ aNbElements[i] = count;
+ SBinary* currentValue = values[i].Value.MVbin.lpbin;
+ for (ULONG j = 0; j < count; j++) {
+ nsMapiEntry& current = aEntryIDs[i][j];
+ current.Assign(currentValue->cb,
+ reinterpret_cast<LPENTRYID>(currentValue->lpb));
+ currentValue++;
+ }
+ } else {
+ PRINTF(
+ ("Cannot retrieve PT_MV_BINARY property %08lx (x0A is PT_ERROR).\n",
+ values[i].ulPropTag));
+ success = FALSE;
+ }
+ }
+
+ FreeBuffer(values);
+ if (!success) {
+ for (ULONG i = 0; i < aNbProperties; i++) {
+ if (aNbElements[i] > 0) delete[] aEntryIDs[i];
+ aEntryIDs[i] = NULL;
+ aNbElements[i] = 0;
+ }
+ }
+ return success;
+}
+
+BOOL nsAbWinHelper::SetPropertiesMVBin(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[],
+ ULONG aNbProperties,
+ nsMapiEntry* aEntryIDs[],
+ ULONG aNbElements[]) {
+ LPSPropValue values = new SPropValue[aNbProperties];
+ if (!values) return FALSE;
+
+ for (ULONG i = 0; i < aNbProperties; i++) {
+ values[i].ulPropTag = aPropertyTags[i];
+ values[i].Value.MVbin.cValues = aNbElements[i];
+ values[i].Value.MVbin.lpbin = new SBinary[aNbElements[i]];
+
+ SBinary* currentValue = values[i].Value.MVbin.lpbin;
+ for (ULONG j = 0; j < aNbElements[i]; j++) {
+ currentValue->cb = aEntryIDs[i][j].mByteCount;
+ currentValue->lpb = reinterpret_cast<LPBYTE>(aEntryIDs[i][j].mEntryId);
+ currentValue++;
+ }
+ }
+ BOOL retCode = SetMAPIProperties(aDir, aObject, aNbProperties, values, true);
+ for (ULONG i = 0; i < aNbProperties; i++) {
+ delete[] values[i].Value.MVbin.lpbin;
+ }
+ delete[] values;
+ return retCode;
+}
+
+// This function, supposedly indicating whether a particular entry was
+// in a particular container, doesn't seem to work very well (has
+// a tendency to return TRUE even if we're talking to different containers...).
+BOOL nsAbWinHelper::TestOpenEntry(const nsMapiEntry& aContainer,
+ const nsMapiEntry& aEntry) {
+ nsMapiInterfaceWrapper<LPMAPICONTAINER> container;
+ nsMapiInterfaceWrapper<LPMAPIPROP> subObject;
+ ULONG objType = 0;
+
+ mLastError =
+ mAddressBook->OpenEntry(aContainer.mByteCount, aContainer.mEntryId,
+ &IID_IMAPIContainer, 0, &objType, container);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open container %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = container->OpenEntry(aEntry.mByteCount, aEntry.mEntryId, NULL, 0,
+ &objType, subObject);
+ return HR_SUCCEEDED(mLastError);
+}
+
+BOOL nsAbWinHelper::DeleteEntry(const nsMapiEntry& aContainer,
+ const nsMapiEntry& aEntry) {
+ nsMapiInterfaceWrapper<LPABCONT> container;
+ ULONG objType = 0;
+ SBinary entry;
+ SBinaryArray entryArray;
+
+ mLastError = mAddressBook->OpenEntry(aContainer.mByteCount,
+ aContainer.mEntryId, &IID_IABContainer,
+ MAPI_MODIFY, &objType, container);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open container %08lx.\n", mLastError));
+ return FALSE;
+ }
+ entry.cb = aEntry.mByteCount;
+ entry.lpb = reinterpret_cast<LPBYTE>(aEntry.mEntryId);
+ entryArray.cValues = 1;
+ entryArray.lpbin = &entry;
+ mLastError = container->DeleteEntries(&entryArray, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot delete entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::GetDlMembersTag(IMAPIProp* aMsg, ULONG& aDlMembersTag,
+ ULONG& aDlMembersTagOneOff) {
+ const GUID guid = {0x00062004,
+ 0x0000,
+ 0x0000,
+ {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};
+ MAPINAMEID nameID;
+ nameID.lpguid = (GUID*)&guid;
+ nameID.ulKind = MNID_ID;
+ LPSPropTagArray lppPropTags;
+ LPMAPINAMEID lpNameID[1] = {&nameID};
+
+ // Strangely requesting two tags at the same time doesn't appear to work,
+ // so request them separately.
+ // One should be able to set up `lpNameID` with two entries and get two
+ // tags returned in `lppPropTags`, but sadly the second one is always 0.
+ nameID.Kind.lID = 0x8055; // PidLidDistributionListMembers
+ mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get DL prop tag %08lx.\n", mLastError));
+ return FALSE;
+ }
+ aDlMembersTag = lppPropTags[0].aulPropTag[0] | PT_MV_BINARY;
+ mAddressFreeBuffer(lppPropTags);
+
+ nameID.Kind.lID = 0x8054; // PidLidDistributionListOneOffMembers
+ mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open DL prop tag (one off) %08lx.\n", mLastError));
+ return FALSE;
+ }
+ aDlMembersTagOneOff = lppPropTags[0].aulPropTag[0] | PT_MV_BINARY;
+ mAddressFreeBuffer(lppPropTags);
+
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::GetDlNameTag(IMAPIProp* aMsg, ULONG& aDlNameTag) {
+ const GUID guid = {0x00062004,
+ 0x0000,
+ 0x0000,
+ {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};
+ MAPINAMEID nameID;
+ nameID.lpguid = (GUID*)&guid;
+ nameID.ulKind = MNID_ID;
+ LPSPropTagArray lppPropTags;
+ LPMAPINAMEID lpNameID[1] = {&nameID};
+
+ nameID.Kind.lID = 0x8053; // PidLidDistributionListName
+ mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get DL prop tag %08lx.\n", mLastError));
+ return FALSE;
+ }
+ aDlNameTag = lppPropTags[0].aulPropTag[0] | PT_UNICODE;
+ mAddressFreeBuffer(lppPropTags);
+
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::DeleteEntryfromDL(const nsMapiEntry& aTopDir,
+ const nsMapiEntry& aDistList,
+ const nsMapiEntry& aEntry) {
+ // First we need to open the distribution list to get the property tag.
+ ULONG dlMembersTag = 0;
+ ULONG dlMembersTagOnOff = 0;
+ {
+ // We do this in a block is `msg` going out of scope will release the
+ // object.
+ nsMapiInterfaceWrapper<LPMAPIPROP> msg;
+ mLastError = OpenMAPIObject(aTopDir, aDistList, true, 0, msg);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open DL entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (!GetDlMembersTag(msg.Get(), dlMembersTag, dlMembersTagOnOff))
+ return FALSE;
+ }
+
+ // This will self-destruct when it goes out of scope.
+ nsMapiEntryArray dlMembers;
+ nsMapiEntryArray dlMembersOneOff;
+
+ // Turn IMailUser into IMessage/IPM.Contact.
+ // Check for magic provider GUID.
+ struct AbEntryId* abEntryId = (struct AbEntryId*)aEntry.mEntryId;
+ if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID,
+ CONTAB_PROVIDER_ID_LENGTH) != 0) {
+ PRINTF(("Cannot get to IMessage/IPM.Contact.\n"));
+ return FALSE;
+ }
+ ULONG contactIdLength = abEntryId->length;
+ LPENTRYID contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes));
+
+ ULONG tags[2] = {dlMembersTag, dlMembersTagOnOff};
+ nsMapiEntry* values[2];
+ ULONG counts[2];
+ if (!GetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) {
+ PRINTF(("Cannot get DL members.\n"));
+ return FALSE;
+ }
+ dlMembers.mEntries = values[0];
+ dlMembersOneOff.mEntries = values[1];
+ dlMembers.mNbEntries = counts[0];
+ dlMembersOneOff.mNbEntries = counts[1];
+
+ if (dlMembers.mNbEntries == 0) return FALSE;
+ if (dlMembers.mNbEntries != dlMembersOneOff.mNbEntries) {
+ PRINTF(("DL members and DL one off members have different length.\n"));
+ return FALSE;
+ }
+
+ ULONG result;
+ for (ULONG i = 0; i < dlMembers.mNbEntries; i++) {
+ struct DlEntryId* dlEntryId =
+ (struct DlEntryId*)dlMembers.mEntries[i].mEntryId;
+ if (memcmp(dlEntryId->provider, DL_PROVIDER_ID, DL_PROVIDER_ID_LENGTH) != 0)
+ continue;
+ mLastError = mAddressSession->CompareEntryIDs(
+ contactIdLength, contactId,
+ dlMembers.mEntries[i].mByteCount - sizeof(struct DlEntryId),
+ reinterpret_cast<LPENTRYID>(dlEntryId->idBytes), 0, &result);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("CompareEntryIDs failed with %08lx (DeleteEntryfromDL()).\n",
+ mLastError));
+ }
+ if (result) {
+ PRINTF(("Found card to be deleted at position %lu.\n", i));
+
+ // Kill/free entry and shuffle remaining cards down.
+ dlMembers.mEntries[i].Assign(0, NULL);
+ dlMembersOneOff.mEntries[i].Assign(0, NULL);
+ for (ULONG j = i + 1; j < dlMembers.mNbEntries; j++) {
+ nsMapiEntry::Move(dlMembers.mEntries[j - 1], dlMembers.mEntries[j]);
+ nsMapiEntry::Move(dlMembersOneOff.mEntries[j - 1],
+ dlMembersOneOff.mEntries[j]);
+ }
+ dlMembers.mNbEntries--;
+ dlMembersOneOff.mNbEntries--;
+
+ counts[0] = dlMembers.mNbEntries;
+ counts[1] = dlMembersOneOff.mNbEntries;
+ if (counts[0] >= 1) {
+ if (!SetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) {
+ PRINTF(("Cannot set DL members.\n"));
+ return FALSE;
+ }
+ } else {
+ static const SizedSPropTagArray(2, properties) = {
+ 2, {dlMembersTag, dlMembersTagOnOff}};
+ if (!DeleteMAPIProperties(aTopDir, aDistList,
+ (LPSPropTagArray)&properties, true)) {
+ PRINTF(("Cannot delete DL members.\n"));
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+BOOL nsAbWinHelper::AddEntryToDL(const nsMapiEntry& aTopDir,
+ const nsMapiEntry& aDistList,
+ const nsMapiEntry& aEntry,
+ const wchar_t* aDisplay,
+ const wchar_t* aEmail) {
+ // First we need to open the distribution list to get the property tag.
+ ULONG dlMembersTag = 0;
+ ULONG dlMembersTagOnOff = 0;
+ {
+ // We do this in a block is `msg` going out of scope will release the
+ // object.
+ nsMapiInterfaceWrapper<LPMAPIPROP> msg;
+ mLastError = OpenMAPIObject(aTopDir, aDistList, true, 0, msg);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open DL entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (!GetDlMembersTag(msg.Get(), dlMembersTag, dlMembersTagOnOff))
+ return FALSE;
+ }
+
+ // This will self-destruct when it goes out of scope.
+ nsMapiEntryArray dlMembers;
+ nsMapiEntryArray dlMembersOneOff;
+
+ // Turn IMailUser into IMessage/IPM.Contact.
+ // Check for magic provider GUID.
+ struct AbEntryId* abEntryId = (struct AbEntryId*)aEntry.mEntryId;
+ if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID,
+ CONTAB_PROVIDER_ID_LENGTH) != 0) {
+ PRINTF(("Cannot get to IMessage/IPM.Contact.\n"));
+ return FALSE;
+ }
+ ULONG contactIdLength = abEntryId->length;
+ LPENTRYID contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes));
+
+ ULONG tags[2] = {dlMembersTag, dlMembersTagOnOff};
+ nsMapiEntry* values[2];
+ ULONG counts[2];
+ // We ask for and array one entry larger.
+ if (!GetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts, true)) {
+ // If the properties aren't there, the list has no entries so far.
+ values[0] = new nsMapiEntry[1];
+ values[1] = new nsMapiEntry[1];
+ counts[0] = counts[1] = 0;
+ }
+ dlMembers.mEntries = values[0];
+ dlMembersOneOff.mEntries = values[1];
+ dlMembers.mNbEntries = counts[0];
+ dlMembersOneOff.mNbEntries = counts[1];
+
+ if (dlMembers.mNbEntries != dlMembersOneOff.mNbEntries) {
+ PRINTF(("DL members and DL one off members have different length.\n"));
+ return FALSE;
+ }
+
+ // Append a new entry at the end. The array is already large enough.
+
+ // Construct a distribution list entry based on a contact.
+ size_t dlEntryIdLength = sizeof(struct DlEntryId) + contactIdLength;
+ struct DlEntryId* dlEntryId = (DlEntryId*)moz_xmalloc(dlEntryIdLength);
+ memset(dlEntryId->flags, 0, DLENTRY_FLAGS_LENGTH);
+ memcpy(dlEntryId->provider, DL_PROVIDER_ID, DL_PROVIDER_ID_LENGTH);
+ // See documentation referenced above: 0xC3 = 0x80 | 0x40 | 0x03.
+ memset(dlEntryId->type, 0xC3, DLENTRY_TYPE_LENGTH);
+ memcpy(dlEntryId->idBytes, contactId, contactIdLength);
+ dlMembers.mEntries[dlMembers.mNbEntries].Assign(
+ dlEntryIdLength, reinterpret_cast<LPENTRYID>(dlEntryId));
+
+ // Construct a one-off entry.
+ size_t dlEntryIdOoLength = sizeof(struct DlEntryIdOo) +
+ 2 * (wcslen(aDisplay) + 4 + wcslen(aEmail) + 3);
+ struct DlEntryIdOo* dlEntryIdOo =
+ (DlEntryIdOo*)moz_xmalloc(dlEntryIdOoLength);
+ memset(dlEntryIdOo->flags, 0, DLENTRY_OO_FLAGS_LENGTH);
+ memcpy(dlEntryIdOo->provider, DL_OO_PROVIDER_ID, DL_OO_PROVIDER_ID_LENGTH);
+ dlEntryIdOo->versionAndBits = MAPI_UNICODE | MAPI_SEND_NO_RICH_INFO;
+
+ // Populate the variable part. A bit of stone-age programming ;-)
+ size_t length = 2 * (wcslen(aDisplay) + 1);
+ memcpy(dlEntryIdOo->variable, aDisplay, length);
+ size_t offset = length;
+
+ length = 2 * (4 + 1);
+ memcpy(dlEntryIdOo->variable + offset, L"SMTP", length);
+ offset += length;
+
+ length = 2 * (wcslen(aEmail) + 1);
+ memcpy(dlEntryIdOo->variable + offset, aEmail, length);
+
+ dlMembersOneOff.mEntries[dlMembersOneOff.mNbEntries].Assign(
+ dlEntryIdOoLength, reinterpret_cast<LPENTRYID>(dlEntryIdOo));
+
+ free(dlEntryId);
+ free(dlEntryIdOo);
+
+ dlMembers.mNbEntries++;
+ dlMembersOneOff.mNbEntries++;
+
+ counts[0] = dlMembers.mNbEntries;
+ counts[1] = dlMembersOneOff.mNbEntries;
+ if (!SetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) {
+ PRINTF(("Cannot set DL members.\n"));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::SetPropertyUString(const nsMapiEntry& aObject,
+ ULONG aPropertyTag,
+ const char16_t* aValue) {
+ SPropValue value;
+ nsAutoCString alternativeValue;
+
+ value.ulPropTag = aPropertyTag;
+ if (PROP_TYPE(aPropertyTag) == PT_UNICODE) {
+ value.Value.lpszW =
+ reinterpret_cast<wchar_t*>(const_cast<char16_t*>(aValue));
+ } else if (PROP_TYPE(aPropertyTag) == PT_STRING8) {
+ alternativeValue = NS_LossyConvertUTF16toASCII(aValue);
+ value.Value.lpszA = const_cast<char*>(alternativeValue.get());
+ } else {
+ PRINTF(("Property %08lx is not a string.\n", aPropertyTag));
+ return FALSE;
+ }
+ nsMapiEntry nullEntry;
+ return SetMAPIProperties(nullEntry, aObject, 1, &value, false);
+}
+
+BOOL nsAbWinHelper::SetPropertiesUString(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[],
+ ULONG aNbProperties,
+ nsString aValues[]) {
+ LPSPropValue values = new SPropValue[aNbProperties];
+ if (!values) return FALSE;
+
+ ULONG currentValue = 0;
+ nsAutoCString alternativeValue;
+ BOOL retCode = TRUE;
+
+ for (ULONG i = 0; i < aNbProperties; ++i) {
+ values[currentValue].ulPropTag = aPropertyTags[i];
+ if (PROP_TYPE(aPropertyTags[i]) == PT_UNICODE) {
+ const wchar_t* value = aValues[i].get();
+ values[currentValue++].Value.lpszW = const_cast<wchar_t*>(value);
+ } else if (PROP_TYPE(aPropertyTags[i]) == PT_STRING8) {
+ LossyCopyUTF16toASCII(aValues[i], alternativeValue);
+ char* av = strdup(alternativeValue.get());
+ if (!av) {
+ retCode = FALSE;
+ break;
+ }
+ values[currentValue++].Value.lpszA = av;
+ }
+ }
+ if (retCode)
+ retCode = SetMAPIProperties(aDir, aObject, currentValue, values, true);
+ for (ULONG i = 0; i < currentValue; ++i) {
+ if (PROP_TYPE(aPropertyTags[i]) == PT_STRING8) {
+ free(values[i].Value.lpszA);
+ }
+ }
+ delete[] values;
+ return retCode;
+}
+
+BOOL nsAbWinHelper::SetPropertyDate(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ bool fromContact, ULONG aPropertyTag,
+ WORD aYear, WORD aMonth, WORD aDay) {
+ SPropValue value;
+
+ value.ulPropTag = aPropertyTag;
+ if (PROP_TYPE(aPropertyTag) == PT_SYSTIME) {
+ SYSTEMTIME readableTime;
+
+ readableTime.wYear = aYear;
+ readableTime.wMonth = aMonth;
+ readableTime.wDay = aDay;
+ readableTime.wDayOfWeek = 0;
+ readableTime.wHour = 0;
+ readableTime.wMinute = 0;
+ readableTime.wSecond = 0;
+ readableTime.wMilliseconds = 0;
+ if (SystemTimeToFileTime(&readableTime, &value.Value.ft)) {
+ return SetMAPIProperties(aDir, aObject, 1, &value, fromContact);
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+BOOL nsAbWinHelper::CreateEntryInternal(const nsMapiEntry& aParent,
+ nsMapiEntry& aNewEntry,
+ const char* aContactClass,
+ const wchar_t* aName) {
+ // We create an IPM.Contact or IPM.DistList message in the contacts folder.
+ // To find that folder, we look for our `aParent` in the hierarchy table
+ // and use the matching `PR_CONTAB_FOLDER_ENTRYID` for the folder.
+ nsMapiInterfaceWrapper<LPABCONT> rootFolder;
+ nsMapiInterfaceWrapper<LPMAPITABLE> folders;
+ ULONG objType = 0;
+ mLastError = mAddressBook->OpenEntry(0, NULL, NULL, 0, &objType, rootFolder);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open root %08lx (creating new entry).\n", mLastError));
+ return FALSE;
+ }
+ mLastError = rootFolder->GetHierarchyTable(CONVENIENT_DEPTH, folders);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get hierarchy %08lx (creating new entry).\n", mLastError));
+ return FALSE;
+ }
+
+ // Request `PR_ENTRYID` and `PR_CONTAB_FOLDER_ENTRYID`.
+#define PR_CONTAB_FOLDER_ENTRYID PROP_TAG(PT_BINARY, 0x6610)
+ static const SizedSPropTagArray(2, properties) = {
+ 2, {PR_ENTRYID, PR_CONTAB_FOLDER_ENTRYID}};
+ mLastError = folders->SetColumns((LPSPropTagArray)&properties, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set columns %08lx (creating new entry).\n", mLastError));
+ return FALSE;
+ }
+
+ ULONG rowCount = 0;
+ bool found = false;
+ nsMapiEntry conTab;
+ mLastError = folders->GetRowCount(0, &rowCount);
+ if (HR_SUCCEEDED(mLastError)) {
+ do {
+ LPSRowSet rowSet = NULL;
+
+ rowCount = 0;
+ mLastError = folders->QueryRows(1, 0, &rowSet);
+ if (HR_SUCCEEDED(mLastError)) {
+ rowCount = rowSet->cRows;
+ if (rowCount > 0) {
+ ULONG result;
+ // Get entry ID from row and compare.
+ SPropValue& colValue = rowSet->aRow->lpProps[0];
+
+ mLastError = mAddressSession->CompareEntryIDs(
+ aParent.mByteCount, aParent.mEntryId, colValue.Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(colValue.Value.bin.lpb), 0, &result);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("CompareEntryIDs failed with %08lx (creating new entry).\n",
+ mLastError));
+ }
+ if (result) {
+ SPropValue& conTabValue = rowSet->aRow->lpProps[1];
+ conTab.Assign(
+ conTabValue.Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(conTabValue.Value.bin.lpb));
+ found = true;
+ break;
+ }
+ }
+ MyFreeProws(rowSet);
+ } else {
+ PRINTF(("Cannot query rows %08lx (creating new entry).\n", mLastError));
+ }
+ } while (rowCount > 0);
+ }
+ if (HR_FAILED(mLastError)) return HR_SUCCEEDED(mLastError);
+
+ if (!found) {
+ PRINTF(("Cannot find folder for contact in hierarchy table.\n"));
+ return FALSE;
+ }
+
+ // Open store and contact folder.
+ PRINTF(("Found contact folder associated with AB container.\n"));
+ nsMapiEntry storeEntry;
+ // Get the entry ID of the related store. This won't work for the
+ // Global Address List (GAL) since it doesn't provide contacts from a
+ // local store.
+ if (!GetPropertyBin(aParent, PR_STORE_ENTRYID, storeEntry)) {
+ PRINTF(("Cannot get PR_STORE_ENTRYID, likely not a local AB.\n"));
+ return FALSE;
+ }
+ nsMapiInterfaceWrapper<LPMDB> store;
+ mLastError = mAddressSession->OpenMsgStore(
+ 0, storeEntry.mByteCount, storeEntry.mEntryId, NULL, 0, store);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open MAPI message store %08lx.\n", mLastError));
+ return FALSE;
+ }
+ nsMapiInterfaceWrapper<LPMAPIFOLDER> contactFolder;
+ mLastError =
+ store->OpenEntry(conTab.mByteCount, conTab.mEntryId, &IID_IMAPIFolder,
+ MAPI_MODIFY, &objType, contactFolder);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open contact folder %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ // Crazy as it seems, contacts and distribution lists are stored as message.
+ nsMapiInterfaceWrapper<LPMESSAGE> newEntry;
+ mLastError = contactFolder->CreateMessage(&IID_IMessage, 0, newEntry);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot create new entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ SPropValue propValue;
+ LPSPropProblemArray problems = NULL;
+ propValue.ulPropTag = PR_MESSAGE_CLASS_A;
+ propValue.Value.lpszA = const_cast<char*>(aContactClass);
+ mLastError = newEntry->SetProps(1, &propValue, &problems);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set message class %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ if (strcmp(aContactClass, "IPM.DistList") == 0) {
+ // Set distribution list name.
+ problems = NULL;
+ GetDlNameTag(newEntry.Get(), propValue.ulPropTag);
+ propValue.Value.lpszW = const_cast<wchar_t*>(aName);
+ mLastError = newEntry->SetProps(1, &propValue, &problems);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set DL name %08lx.\n", mLastError));
+ return FALSE;
+ }
+ }
+
+ mLastError = newEntry->SaveChanges(KEEP_OPEN_READONLY);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot commit new entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ // Get the entry ID of the contact (IMessage).
+ SPropTagArray property;
+ LPSPropValue value = NULL;
+ ULONG valueCount = 0;
+ property.cValues = 1;
+ property.aulPropTag[0] = PR_ENTRYID;
+ mLastError = newEntry->GetProps(&property, 0, &valueCount, &value);
+ if (HR_FAILED(mLastError) || valueCount != 1) {
+ PRINTF(("Cannot get entry id %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ // Construct the entry ID of the related address book entry (IMailUser).
+ AbEntryId* abEntryId =
+ (AbEntryId*)moz_xmalloc(sizeof(AbEntryId) + value->Value.bin.cb);
+ if (!abEntryId) return FALSE;
+ memset(abEntryId, 0, 4); // Null out the flags.
+ memcpy(abEntryId->provider, CONTAB_PROVIDER_ID, CONTAB_PROVIDER_ID_LENGTH);
+ memcpy(abEntryId->version, ABENTRY_VERSION, ABENTRY_VERSION_LENGTH);
+ memcpy(abEntryId->type, ABENTRY_TYPE, ABENTRY_TYPE_LENGTH);
+ abEntryId->index = 0;
+ abEntryId->length = value->Value.bin.cb;
+ memcpy(abEntryId->idBytes, value->Value.bin.lpb, abEntryId->length);
+
+ aNewEntry.Assign(sizeof(AbEntryId) + value->Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(abEntryId));
+ FreeBuffer(value);
+
+ // We need to set a display name otherwise MAPI is really unhappy internally.
+ SPropValue displayName;
+ displayName.ulPropTag = PR_DISPLAY_NAME_W;
+ displayName.Value.lpszW = const_cast<wchar_t*>(aName);
+ nsMapiInterfaceWrapper<LPMAPIPROP> object;
+ mLastError =
+ mAddressBook->OpenEntry(aNewEntry.mByteCount, aNewEntry.mEntryId,
+ &IID_IMAPIProp, MAPI_MODIFY, &objType, object);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open newly created AB entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = object->SetProps(1, &displayName, &problems);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set display name %08lx.\n", mLastError));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+BOOL nsAbWinHelper::CreateEntry(const nsMapiEntry& aParent,
+ nsMapiEntry& aNewEntry) {
+ nsAutoString tempName(L"" kDummyDisplayName);
+ tempName.AppendInt(sEntryCounter++);
+ return CreateEntryInternal(aParent, aNewEntry, "IPM.Contact", tempName.get());
+}
+
+BOOL nsAbWinHelper::CreateDistList(const nsMapiEntry& aParent,
+ nsMapiEntry& aNewEntry,
+ const wchar_t* aName) {
+ return CreateEntryInternal(aParent, aNewEntry, "IPM.DistList", aName);
+}
+
+enum {
+ ContentsColumnEntryId = 0,
+ ContentsColumnObjectType,
+ ContentsColumnsSize
+};
+
+static const SizedSPropTagArray(ContentsColumnsSize, ContentsColumns) = {
+ ContentsColumnsSize, {PR_ENTRYID, PR_OBJECT_TYPE}};
+
+BOOL nsAbWinHelper::GetContents(const nsMapiEntry& aParent,
+ LPSRestriction aRestriction,
+ nsMapiEntry** aList, ULONG& aNbElements,
+ ULONG aMapiType) {
+ if (aList != NULL) {
+ *aList = NULL;
+ }
+ aNbElements = 0;
+ nsMapiInterfaceWrapper<LPMAPICONTAINER> parent;
+ nsMapiInterfaceWrapper<LPMAPITABLE> contents;
+ ULONG objType = 0;
+ ULONG rowCount = 0;
+
+ mLastError =
+ mAddressBook->OpenEntry(aParent.mByteCount, aParent.mEntryId,
+ &IID_IMAPIContainer, 0, &objType, parent);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open parent %08lx.\n", mLastError));
+ return FALSE;
+ }
+ // Historic comment: May be relevant in the future.
+ // WAB removed in bug 1687132.
+ // Here, flags for WAB and MAPI could be different, so this works
+ // only as long as we don't want to use any flag in GetContentsTable
+ mLastError = parent->GetContentsTable(0, contents);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get contents %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (aRestriction != NULL) {
+ mLastError = contents->Restrict(aRestriction, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set restriction %08lx.\n", mLastError));
+ return FALSE;
+ }
+ }
+ mLastError = contents->SetColumns((LPSPropTagArray)&ContentsColumns, 0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot set columns %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = contents->GetRowCount(0, &rowCount);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get result count %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (aList != NULL) {
+ *aList = new nsMapiEntry[rowCount];
+ }
+ aNbElements = 0;
+ do {
+ LPSRowSet rowSet = NULL;
+
+ rowCount = 0;
+ mLastError = contents->QueryRows(1, 0, &rowSet);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot query rows %08lx.\n", mLastError));
+ return FALSE;
+ }
+ rowCount = rowSet->cRows;
+ if (rowCount > 0 &&
+ (aMapiType == 0 ||
+ rowSet->aRow->lpProps[ContentsColumnObjectType].Value.ul ==
+ aMapiType)) {
+ if (aList != NULL) {
+ nsMapiEntry& current = (*aList)[aNbElements];
+ SPropValue& currentValue = rowSet->aRow->lpProps[ContentsColumnEntryId];
+
+ // Sometimes Outlooks spits the dummy here :-(
+ // That is meant to be a byte count and NOT an error code of 0x8004010F.
+ // We gloss over it.
+ if (currentValue.Value.bin.cb == (ULONG)MAPI_E_NOT_FOUND ||
+ currentValue.Value.bin.lpb == NULL) {
+ PRINTF(("Error fetching rows.\n"));
+ return TRUE;
+ }
+ current.Assign(currentValue.Value.bin.cb,
+ reinterpret_cast<LPENTRYID>(currentValue.Value.bin.lpb));
+ }
+ ++aNbElements;
+ }
+ MyFreeProws(rowSet);
+ } while (rowCount > 0);
+ return TRUE;
+}
+
+HRESULT nsAbWinHelper::OpenMAPIObject(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ bool aFromContact, ULONG aFlags,
+ LPUNKNOWN* aResult) {
+ nsMapiEntry storeEntry;
+ ULONG contactIdLength = 0;
+ LPENTRYID contactId = NULL;
+ if (aFromContact) {
+ // Get the entry ID of the related store. This won't work for the
+ // Global Address List (GAL) since it doesn't provide contacts from a
+ // local store.
+ if (!GetPropertyBin(aDir, PR_STORE_ENTRYID, storeEntry)) {
+ PRINTF(("Cannot get PR_STORE_ENTRYID, likely not a local AB.\n"));
+ aFromContact = false;
+ }
+ // Check for magic provider GUID.
+ struct AbEntryId* abEntryId = (struct AbEntryId*)aObject.mEntryId;
+ if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID,
+ CONTAB_PROVIDER_ID_LENGTH) != 0) {
+ aFromContact = false;
+ } else {
+ contactIdLength = abEntryId->length;
+ contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes));
+ }
+ }
+
+ ULONG objType = 0;
+ if (aFromContact) {
+ // Open the store.
+ HRESULT retCode;
+ nsMapiInterfaceWrapper<LPMDB> store;
+ retCode = mAddressSession->OpenMsgStore(
+ 0, storeEntry.mByteCount, storeEntry.mEntryId, NULL, 0, store);
+ if (HR_FAILED(retCode)) {
+ PRINTF(("Cannot open MAPI message store %08lx.\n", retCode));
+ return retCode;
+ }
+ // Open the contact object.
+ retCode = store->OpenEntry(contactIdLength, contactId, &IID_IMessage, 0,
+ &objType, aResult);
+ return retCode;
+ } else {
+ // Open the address book object.
+ return mAddressBook->OpenEntry(aObject.mByteCount, aObject.mEntryId,
+ &IID_IMAPIProp, 0, &objType, aResult);
+ }
+}
+
+BOOL nsAbWinHelper::GetMAPIProperties(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[],
+ ULONG aNbProperties, LPSPropValue& aValue,
+ ULONG& aValueCount, bool aFromContact) {
+ nsMapiInterfaceWrapper<LPMAPIPROP> object;
+ LPSPropTagArray properties = NULL;
+
+ mLastError = OpenMAPIObject(aDir, aObject, aFromContact, 0, object);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ AllocateBuffer(CbNewSPropTagArray(aNbProperties),
+ reinterpret_cast<void**>(&properties));
+ properties->cValues = aNbProperties;
+ for (ULONG i = 0; i < aNbProperties; ++i) {
+ properties->aulPropTag[i] = aPropertyTags[i];
+ }
+ mLastError = object->GetProps(properties, 0, &aValueCount, &aValue);
+ FreeBuffer(properties);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot get props %08lx.\n", mLastError));
+ }
+ return HR_SUCCEEDED(mLastError);
+}
+
+BOOL nsAbWinHelper::SetMAPIProperties(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ ULONG aNbProperties,
+ const LPSPropValue& aValues,
+ bool aFromContact) {
+ nsMapiInterfaceWrapper<LPMAPIPROP> object;
+ LPSPropProblemArray problems = NULL;
+
+ mLastError = OpenMAPIObject(aDir, aObject, aFromContact, MAPI_MODIFY, object);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = object->SetProps(aNbProperties, aValues, &problems);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot update the object %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (problems != NULL) {
+ for (ULONG i = 0; i < problems->cProblem; ++i) {
+ PRINTF(("Problem %lu: index %lu code %08lx.\n", i,
+ problems->aProblem[i].ulIndex, problems->aProblem[i].scode));
+ }
+ mAddressFreeBuffer(problems);
+ }
+ mLastError = object->SaveChanges(0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot commit changes %08lx.\n", mLastError));
+ }
+ return HR_SUCCEEDED(mLastError);
+}
+
+BOOL nsAbWinHelper::DeleteMAPIProperties(const nsMapiEntry& aDir,
+ const nsMapiEntry& aObject,
+ const LPSPropTagArray aProps,
+ bool aFromContact) {
+ nsMapiInterfaceWrapper<LPMAPIPROP> object;
+ LPSPropProblemArray problems = NULL;
+
+ mLastError = OpenMAPIObject(aDir, aObject, aFromContact, MAPI_MODIFY, object);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot open entry %08lx.\n", mLastError));
+ return FALSE;
+ }
+ mLastError = object->DeleteProps(aProps, &problems);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot update the object (DeleteProps) %08lx.\n", mLastError));
+ return FALSE;
+ }
+ if (problems != NULL) {
+ for (ULONG i = 0; i < problems->cProblem; ++i) {
+ PRINTF(("Problem %lu: index %lu code %08lx.\n", i,
+ problems->aProblem[i].ulIndex, problems->aProblem[i].scode));
+ }
+ mAddressFreeBuffer(problems);
+ }
+ mLastError = object->SaveChanges(0);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("Cannot commit changes %08lx.\n", mLastError));
+ }
+ return HR_SUCCEEDED(mLastError);
+}
+
+void nsAbWinHelper::MyFreeProws(LPSRowSet aRowset) {
+ if (aRowset == NULL) {
+ return;
+ }
+ ULONG i = 0;
+
+ for (i = 0; i < aRowset->cRows; ++i) {
+ FreeBuffer(aRowset->aRow[i].lpProps);
+ }
+ FreeBuffer(aRowset);
+}
+
+nsAbWinHelperGuard::nsAbWinHelperGuard() : mHelper(NULL) {
+ mHelper = new nsMapiAddressBook;
+}
+
+nsAbWinHelperGuard::~nsAbWinHelperGuard(void) { delete mHelper; }
+
+void makeEntryIdFromURI(const char* aScheme, const char* aUri,
+ nsCString& aEntry) {
+ aEntry.Truncate();
+ uint32_t schemeLength = strlen(aScheme);
+
+ if (strncmp(aUri, aScheme, schemeLength) == 0) {
+ // Assign string from position `schemeLength`.
+ aEntry = aUri + schemeLength;
+
+ // Now strip the parent directory before the /.
+ int ind = aEntry.FindChar('/');
+ if (ind != kNotFound) {
+ aEntry = Substring(aEntry, ind + 1);
+ }
+ }
+}
+
+bool nsAbWinHelper::CompareEntryIDs(nsCString& aEntryID1,
+ nsCString& aEntryID2) {
+ ULONG result;
+ nsMapiEntry e1;
+ nsMapiEntry e2;
+ e1.Assign(aEntryID1);
+ e2.Assign(aEntryID2);
+ mLastError = mAddressSession->CompareEntryIDs(
+ e1.mByteCount, e1.mEntryId, e2.mByteCount, e2.mEntryId, 0, &result);
+ if (HR_FAILED(mLastError)) {
+ PRINTF(("CompareEntryIDs failed with %08lx (CompareEntryIDs()).\n",
+ mLastError));
+ return false;
+ }
+ return result ? true : false;
+}
diff --git a/comm/mailnews/addrbook/src/nsAbWinHelper.h b/comm/mailnews/addrbook/src/nsAbWinHelper.h
new file mode 100644
index 0000000000..5411ae94fd
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsAbWinHelper.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#ifndef nsAbWinHelper_h___
+#define nsAbWinHelper_h___
+
+#include <windows.h>
+#include "../../mapi/include/mapix.h"
+
+#include "nsString.h"
+#include "mozilla/StaticMutex.h"
+
+#define kOutlookDirectoryScheme "moz-aboutlookdirectory:///"
+#define kOutlookCardScheme "moz-aboutlookcard:///"
+#define kDummyDisplayName "__MailUser__"
+
+struct nsMapiEntry {
+ // Can't be assigned since it would double up the reference in `mEntryId`.
+ nsMapiEntry& operator=(nsMapiEntry&) = delete;
+ ULONG mByteCount;
+ LPENTRYID mEntryId;
+
+ nsMapiEntry(void);
+ ~nsMapiEntry(void);
+ nsMapiEntry(ULONG aByteCount, LPENTRYID aEntryId);
+
+ static void Move(nsMapiEntry& target, nsMapiEntry& source);
+ void Assign(ULONG aByteCount, LPENTRYID aEntryId);
+ void Assign(const nsCString& aString);
+ void ToString(nsCString& aString) const;
+ void Dump(void) const;
+};
+
+struct nsMapiEntryArray {
+ nsMapiEntry* mEntries;
+ ULONG mNbEntries;
+
+ nsMapiEntryArray(void);
+ ~nsMapiEntryArray(void);
+
+ void CleanUp(void);
+};
+
+class nsAbWinHelper {
+ public:
+ nsAbWinHelper(void);
+ virtual ~nsAbWinHelper(void);
+
+ // Get the top address books
+ BOOL GetFolders(nsMapiEntryArray& aFolders);
+ // Get a list of entries for cards/mailing lists in a folder/mailing list
+ BOOL GetCards(const nsMapiEntry& aParent, LPSRestriction aRestriction,
+ nsMapiEntryArray& aCards);
+ // Get a list of mailing lists in a folder
+ BOOL GetNodes(const nsMapiEntry& aParent, nsMapiEntryArray& aNodes);
+ // Get the number of cards/mailing lists in a folder/mailing list
+ BOOL GetCardsCount(const nsMapiEntry& aParent, ULONG& aNbCards);
+ // Access last MAPI error
+ HRESULT LastError(void) const { return mLastError; }
+ // Get the value of a MAPI property of type string
+ BOOL GetPropertyString(const nsMapiEntry& aObject, ULONG aPropertyTag,
+ nsCString& aValue);
+ // Same as previous, but string is returned as unicode.
+ BOOL GetPropertyUString(const nsMapiEntry& aObject, ULONG aPropertyTag,
+ nsString& aValue);
+ // Get multiple string MAPI properties in one call.
+ // Retrieves the properties from the associated contact object (IMessage)
+ // not the address book entry (IMailUser).
+ BOOL GetPropertiesUString(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties,
+ nsString aValues[], bool aSuccess[]);
+ // Get the value of a MAPI property of type SYSTIME
+ BOOL GetPropertyDate(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ bool fromContact, ULONG aPropertyTag, WORD& aYear,
+ WORD& aMonth, WORD& aDay);
+ // Get the value of a MAPI property of type LONG
+ BOOL GetPropertyLong(const nsMapiEntry& aObject, ULONG aPropertyTag,
+ ULONG& aValue);
+ // Get the value of a MAPI property of type BIN
+ BOOL GetPropertyBin(const nsMapiEntry& aObject, ULONG aPropertyTag,
+ nsMapiEntry& aValue);
+ // Get the values of a multiple MAPI properties of type MV BIN
+ BOOL GetPropertiesMVBin(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties,
+ nsMapiEntry* aEntryIDs[], ULONG aNbElements[],
+ bool aAllocateMore = false);
+ // Set the value of a MAPI property of type MV BIN
+ BOOL SetPropertiesMVBin(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties,
+ nsMapiEntry* aEntryIDs[], ULONG aNbElements[]);
+ // Tests if a container contains an entry
+ BOOL TestOpenEntry(const nsMapiEntry& aContainer, const nsMapiEntry& aEntry);
+ // Delete an entry in the address book
+ BOOL DeleteEntry(const nsMapiEntry& aContainer, const nsMapiEntry& aEntry);
+ // Delete an entry from an Outlook distribution list.
+ BOOL DeleteEntryfromDL(const nsMapiEntry& aTopDir,
+ const nsMapiEntry& aDistList,
+ const nsMapiEntry& aEntry);
+ // Add an entry to an Outlook distribution list.
+ BOOL AddEntryToDL(const nsMapiEntry& aTopDir, const nsMapiEntry& aDistList,
+ const nsMapiEntry& aEntry, const wchar_t* aDisplay,
+ const wchar_t* aEmail);
+ // Set the value of a MAPI property of type string in unicode
+ BOOL SetPropertyUString(const nsMapiEntry& aObject, ULONG aPropertyTag,
+ const char16_t* aValue);
+ // Same as previous, but with a bunch of properties in one call.
+ // Sets the properties on the associated contact object (IMessage)
+ // not the address book entry (IMailUser).
+ BOOL SetPropertiesUString(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties,
+ nsString aValues[]);
+ // Set the value of a MAPI property of type SYSTIME
+ BOOL SetPropertyDate(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ bool fromContact, ULONG aPropertyTag, WORD aYear,
+ WORD aMonth, WORD aDay);
+ // Create entry in the address book
+ BOOL CreateEntry(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry);
+ // Create a distribution list in the address book
+ BOOL CreateDistList(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry,
+ const wchar_t* aName);
+ // Create entry worker
+ BOOL CreateEntryInternal(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry,
+ const char* aContactClass, const wchar_t* aName);
+ // Is the helper correctly initialised?
+ BOOL IsOK(void) const { return mAddressBook != NULL; }
+ // Helper to get distribution list members tag.
+ BOOL GetDlMembersTag(IMAPIProp* aMsg, ULONG& aDlMembersTag,
+ ULONG& aDlMembersTagOneOff);
+ // Helper to get distribution list name tag.
+ BOOL GetDlNameTag(IMAPIProp* aMsg, ULONG& aDlNameTag);
+ // Helper to compare entry IDs.
+ bool CompareEntryIDs(nsCString& aEntryID1, nsCString& aEntryID2);
+
+ protected:
+ HRESULT mLastError;
+ LPADRBOOK mAddressBook;
+ LPMAPISESSION mAddressSession;
+ LPMAPIFREEBUFFER mAddressFreeBuffer;
+ static uint32_t sEntryCounter;
+ static mozilla::StaticMutex sMutex;
+
+ // Retrieve the contents of a container, with an optional restriction
+ BOOL GetContents(const nsMapiEntry& aParent, LPSRestriction aRestriction,
+ nsMapiEntry** aList, ULONG& aNbElements, ULONG aMapiType);
+ // Retrieve the values of a set of properties on a MAPI object
+ BOOL GetMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const ULONG aPropertyTags[], ULONG aNbProperties,
+ LPSPropValue& aValues, ULONG& aValueCount,
+ bool aFromContact = false);
+ // Set the values of a set of properties on a MAPI object
+ BOOL SetMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ ULONG aNbProperties, const LPSPropValue& aValues,
+ bool aFromContact);
+ // Delete a set of properties on a MAPI object
+ BOOL DeleteMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ const LPSPropTagArray aProps, bool aFromContact);
+ HRESULT OpenMAPIObject(const nsMapiEntry& aDir, const nsMapiEntry& aObject,
+ bool aFromContact, ULONG aFlags, LPUNKNOWN* aResult);
+ // Clean-up a rowset returned by QueryRows
+ void MyFreeProws(LPSRowSet aSet);
+ // Allocation of a buffer for transmission to interfaces
+ virtual void AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) = 0;
+ // Destruction of a buffer provided by the interfaces
+ virtual void FreeBuffer(LPVOID aBuffer) = 0;
+
+ private:
+};
+
+class nsAbWinHelperGuard {
+ public:
+ explicit nsAbWinHelperGuard();
+ ~nsAbWinHelperGuard(void);
+
+ nsAbWinHelper* operator->(void) { return mHelper; }
+
+ private:
+ nsAbWinHelper* mHelper;
+};
+
+void makeEntryIdFromURI(const char* aScheme, const char* aUri,
+ nsCString& aEntry);
+#endif // nsAbWinHelper_h___
diff --git a/comm/mailnews/addrbook/src/nsLDAPURL.cpp b/comm/mailnews/addrbook/src/nsLDAPURL.cpp
new file mode 100644
index 0000000000..90e4e370e4
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsLDAPURL.cpp
@@ -0,0 +1,591 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsLDAPURL.h"
+#include "netCore.h"
+#include "plstr.h"
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIStandardURL.h"
+#include "nsMsgUtils.h"
+#include "nsUnicharUtils.h"
+#include "mozilla/Encoding.h"
+
+// The two schemes we support, LDAP and LDAPS
+//
+constexpr auto LDAP_SCHEME = "ldap"_ns;
+constexpr auto LDAP_SSL_SCHEME = "ldaps"_ns;
+
+NS_IMPL_ISUPPORTS(nsLDAPURL, nsILDAPURL, nsIURI)
+
+nsLDAPURL::nsLDAPURL() : mScope(SCOPE_BASE), mOptions(0) {}
+
+nsLDAPURL::~nsLDAPURL() {}
+
+nsresult nsLDAPURL::Init(uint32_t aUrlType, int32_t aDefaultPort,
+ const nsACString& aSpec, const char* aOriginCharset,
+ nsIURI* aBaseURI) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> base(aBaseURI);
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .Apply(&nsIStandardURLMutator::Init,
+ nsIStandardURL::URLTYPE_STANDARD, aDefaultPort,
+ PromiseFlatCString(aSpec), aOriginCharset, aBaseURI, nullptr)
+ .Finalize(mBaseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now get the spec from the mBaseURL in case it was a relative one
+ nsCString spec;
+ rv = mBaseURL->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return SetSpecInternal(spec);
+}
+
+void nsLDAPURL::GetPathInternal(nsCString& aPath) {
+ aPath.Assign('/');
+
+ if (!mDN.IsEmpty()) aPath.Append(mDN);
+
+ if (!mAttributes.IsEmpty()) aPath.Append('?');
+
+ // If mAttributes isn't empty, cut off the internally stored commas at start
+ // and end, and append to the path.
+ if (!mAttributes.IsEmpty())
+ aPath.Append(Substring(mAttributes, 1, mAttributes.Length() - 2));
+
+ if (mScope || !mFilter.IsEmpty()) {
+ aPath.Append((mAttributes.IsEmpty() ? "??" : "?"));
+ if (mScope) {
+ if (mScope == SCOPE_ONELEVEL)
+ aPath.Append("one");
+ else if (mScope == SCOPE_SUBTREE)
+ aPath.Append("sub");
+ }
+ if (!mFilter.IsEmpty()) {
+ aPath.Append('?');
+ aPath.Append(mFilter);
+ }
+ }
+}
+
+nsresult nsLDAPURL::SetPathInternal(const nsCString& aPath) {
+ nsCOMPtr<nsILDAPURLParser> parser =
+ do_CreateInstance("@mozilla.org/network/ldap-url-parser;1");
+ nsCOMPtr<nsILDAPURLParserResult> parserResult;
+ nsresult rv = parser->Parse(aPath, getter_AddRefs(parserResult));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ parserResult->GetDn(mDN);
+ parserResult->GetScope(&mScope);
+ parserResult->GetFilter(mFilter);
+ parserResult->GetOptions(&mOptions);
+
+ nsCString attributes;
+ parserResult->GetAttributes(attributes);
+ mAttributes.Truncate();
+ if (!attributes.IsEmpty()) {
+ // Always start and end with a comma if not empty.
+ mAttributes.Append(',');
+ mAttributes.Append(attributes);
+ mAttributes.Append(',');
+ }
+
+ return NS_OK;
+}
+
+// A string representation of the URI. Setting the spec
+// causes the new spec to be parsed, initializing the URI. Setting
+// the spec (or any of the accessors) causes also any currently
+// open streams on the URI's channel to be closed.
+
+NS_IMETHODIMP
+nsLDAPURL::GetSpec(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetSpec(_retval);
+}
+
+nsresult nsLDAPURL::SetSpecInternal(const nsACString& aSpec) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ // Cache the original spec in case we don't like what we've been passed and
+ // need to reset ourselves.
+ nsCString originalSpec;
+ nsresult rv = mBaseURL->GetSpec(originalSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_MutateURI(mBaseURL).SetSpec(aSpec).Finalize(mBaseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetPathInternal(PromiseFlatCString(aSpec));
+ if (NS_FAILED(rv)) {
+ nsresult rv2 =
+ NS_MutateURI(mBaseURL).SetSpec(originalSpec).Finalize(mBaseURL);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsLDAPURL::GetPrePath(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetPrePath(_retval);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetScheme(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetScheme(_retval);
+}
+
+nsresult nsLDAPURL::SetScheme(const nsACString& aScheme) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ if (aScheme.Equals(LDAP_SCHEME, nsCaseInsensitiveCStringComparator))
+ mOptions &= ~OPT_SECURE;
+ else if (aScheme.Equals(LDAP_SSL_SCHEME, nsCaseInsensitiveCStringComparator))
+ mOptions |= OPT_SECURE;
+ else
+ return NS_ERROR_MALFORMED_URI;
+
+ return NS_MutateURI(mBaseURL).SetScheme(aScheme).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetUserPass(nsACString& _retval) {
+ _retval.Truncate();
+ return NS_OK;
+}
+
+nsresult nsLDAPURL::SetUserPass(const nsACString& aUserPass) { return NS_OK; }
+
+NS_IMETHODIMP
+nsLDAPURL::GetUsername(nsACString& _retval) {
+ _retval.Truncate();
+ return NS_OK;
+}
+
+nsresult nsLDAPURL::SetUsername(const nsACString& aUsername) { return NS_OK; }
+
+NS_IMETHODIMP
+nsLDAPURL::GetPassword(nsACString& _retval) {
+ _retval.Truncate();
+ return NS_OK;
+}
+
+nsresult nsLDAPURL::SetPassword(const nsACString& aPassword) { return NS_OK; }
+
+NS_IMETHODIMP
+nsLDAPURL::GetHostPort(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetHostPort(_retval);
+}
+
+nsresult nsLDAPURL::SetHostPort(const nsACString& aHostPort) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return NS_MutateURI(mBaseURL).SetHostPort(aHostPort).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetHost(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetHost(_retval);
+}
+
+nsresult nsLDAPURL::SetHost(const nsACString& aHost) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return NS_MutateURI(mBaseURL).SetHost(aHost).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetPort(int32_t* _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetPort(_retval);
+}
+
+nsresult nsLDAPURL::SetPort(int32_t aPort) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return NS_MutateURI(mBaseURL).SetPort(aPort).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetPathQueryRef(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetPathQueryRef(_retval);
+}
+
+nsresult nsLDAPURL::SetPathQueryRef(const nsACString& aPath) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = SetPathInternal(PromiseFlatCString(aPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(aPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetAsciiSpec(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ // XXX handle extra items?
+ return mBaseURL->GetAsciiSpec(_retval);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetAsciiHost(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetAsciiHost(_retval);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetAsciiHostPort(nsACString& _retval) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->GetAsciiHostPort(_retval);
+}
+
+// boolean equals (in nsIURI other)
+// (based on nsSimpleURI::Equals)
+NS_IMETHODIMP nsLDAPURL::Equals(nsIURI* other, bool* _retval) {
+ *_retval = false;
+ if (other) {
+ nsresult rv;
+ nsCOMPtr<nsILDAPURL> otherURL(do_QueryInterface(other, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString thisSpec, otherSpec;
+ uint32_t otherOptions;
+
+ rv = GetSpec(thisSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = otherURL->GetSpec(otherSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = otherURL->GetOptions(&otherOptions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (thisSpec == otherSpec && mOptions == otherOptions) *_retval = true;
+ }
+ }
+ return NS_OK;
+}
+
+// boolean schemeIs(in const char * scheme);
+//
+NS_IMETHODIMP nsLDAPURL::SchemeIs(const char* aScheme, bool* aEquals) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ return mBaseURL->SchemeIs(aScheme, aEquals);
+}
+
+// nsIURI clone ();
+//
+nsresult nsLDAPURL::Clone(nsIURI** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ RefPtr<nsLDAPURL> clone = new nsLDAPURL();
+
+ clone->mDN = mDN;
+ clone->mScope = mScope;
+ clone->mFilter = mFilter;
+ clone->mOptions = mOptions;
+ clone->mAttributes = mAttributes;
+
+ nsresult rv = NS_MutateURI(mBaseURL).Finalize(clone->mBaseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ clone.forget(aResult);
+ return NS_OK;
+}
+
+// string resolve (in string relativePath);
+//
+NS_IMETHODIMP nsLDAPURL::Resolve(const nsACString& relativePath,
+ nsACString& _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// The following attributes come from nsILDAPURL
+
+// attribute AUTF8String dn;
+//
+NS_IMETHODIMP nsLDAPURL::GetDn(nsACString& _retval) {
+ _retval.Assign(mDN);
+ return NS_OK;
+}
+NS_IMETHODIMP nsLDAPURL::SetDn(const nsACString& aDn) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ mDN.Assign(aDn);
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetAttributes(nsACString& aAttributes) {
+ if (mAttributes.IsEmpty()) {
+ aAttributes.Truncate();
+ return NS_OK;
+ }
+
+ NS_ASSERTION(
+ mAttributes[0] == ',' && mAttributes[mAttributes.Length() - 1] == ',',
+ "mAttributes does not begin and end with a comma");
+
+ // We store the string internally with comma before and after, so strip
+ // them off here.
+ aAttributes = Substring(mAttributes, 1, mAttributes.Length() - 2);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLDAPURL::SetAttributes(const nsACString& aAttributes) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ if (aAttributes.IsEmpty())
+ mAttributes.Truncate();
+ else {
+ // We need to make sure we start off the string with a comma.
+ if (aAttributes[0] != ',') mAttributes = ',';
+
+ mAttributes.Append(aAttributes);
+
+ // Also end with a comma if appropriate.
+ if (mAttributes[mAttributes.Length() - 1] != ',') mAttributes.Append(',');
+ }
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::AddAttribute(const nsACString& aAttribute) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ if (mAttributes.IsEmpty()) {
+ mAttributes = ',';
+ mAttributes.Append(aAttribute);
+ mAttributes.Append(',');
+ } else {
+ // Wrap the attribute in commas, so that we can do an exact match.
+ nsAutoCString findAttribute(",");
+ findAttribute.Append(aAttribute);
+ findAttribute.Append(',');
+
+ // Check to see if the attribute is already stored. If it is, then also
+ // check to see if it is the last attribute in the string, or if the next
+ // character is a comma, this means we won't match substrings.
+ if (FindInReadable(findAttribute, mAttributes,
+ nsCaseInsensitiveCStringComparator)) {
+ return NS_OK;
+ }
+
+ mAttributes.Append(Substring(findAttribute, 1));
+ }
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::RemoveAttribute(const nsACString& aAttribute) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ if (mAttributes.IsEmpty()) return NS_OK;
+
+ // We use comma as delimiter (even first attr has a leading comma).
+ nsAutoCString findAttribute(",");
+ findAttribute.Append(aAttribute);
+ findAttribute.Append(',');
+
+ if (!FindInReadable(findAttribute, mAttributes,
+ nsCaseInsensitiveCStringComparator)) {
+ return NS_OK;
+ }
+ if (mAttributes.Equals(findAttribute, nsCaseInsensitiveCStringComparator)) {
+ mAttributes.Truncate();
+ } else {
+ mAttributes.ReplaceSubstring(findAttribute, ","_ns);
+ }
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::HasAttribute(const nsACString& aAttribute,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ // We use comma as delimiter (even first attr has a leading comma).
+ nsAutoCString findAttribute(",");
+ findAttribute.Append(aAttribute);
+ findAttribute.Append(',');
+
+ *_retval = FindInReadable(findAttribute, mAttributes,
+ nsCaseInsensitiveCStringComparator);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLDAPURL::GetScope(int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mScope;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLDAPURL::SetScope(int32_t aScope) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ // Only allow scopes supported by the C-SDK
+ if ((aScope != SCOPE_BASE) && (aScope != SCOPE_ONELEVEL) &&
+ (aScope != SCOPE_SUBTREE))
+ return NS_ERROR_MALFORMED_URI;
+
+ mScope = aScope;
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetFilter(nsACString& _retval) {
+ _retval.Assign(mFilter);
+ return NS_OK;
+}
+NS_IMETHODIMP nsLDAPURL::SetFilter(const nsACString& aFilter) {
+ if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED;
+
+ mFilter.Assign(aFilter);
+
+ if (mFilter.IsEmpty()) mFilter.AssignLiteral("(objectclass=*)");
+
+ // Now get the current path
+ nsCString newPath;
+ GetPathInternal(newPath);
+
+ // and update the base url
+ return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP nsLDAPURL::GetOptions(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mOptions;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLDAPURL::SetOptions(uint32_t aOptions) {
+ // Secure is the only option supported at the moment
+ if ((mOptions & OPT_SECURE) == (aOptions & OPT_SECURE)) return NS_OK;
+
+ mOptions = aOptions;
+
+ if ((aOptions & OPT_SECURE) == OPT_SECURE) return SetScheme(LDAP_SSL_SCHEME);
+
+ return SetScheme(LDAP_SCHEME);
+}
+
+nsresult nsLDAPURL::SetRef(const nsACString& aRef) {
+ return NS_MutateURI(mBaseURL).SetRef(aRef).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetRef(nsACString& result) { return mBaseURL->GetRef(result); }
+
+NS_IMETHODIMP nsLDAPURL::EqualsExceptRef(nsIURI* other, bool* result) {
+ return mBaseURL->EqualsExceptRef(other, result);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetSpecIgnoringRef(nsACString& result) {
+ return mBaseURL->GetSpecIgnoringRef(result);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetDisplaySpec(nsACString& aUnicodeSpec) {
+ return mBaseURL->GetDisplaySpec(aUnicodeSpec);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetDisplayHostPort(nsACString& aUnicodeHostPort) {
+ return mBaseURL->GetDisplayHostPort(aUnicodeHostPort);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetDisplayHost(nsACString& aUnicodeHost) {
+ return mBaseURL->GetDisplayHost(aUnicodeHost);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetDisplayPrePath(nsACString& aPrePath) {
+ return mBaseURL->GetDisplayPrePath(aPrePath);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetHasRef(bool* result) { return mBaseURL->GetHasRef(result); }
+
+NS_IMETHODIMP
+nsLDAPURL::GetFilePath(nsACString& aFilePath) {
+ return mBaseURL->GetFilePath(aFilePath);
+}
+
+nsresult nsLDAPURL::SetFilePath(const nsACString& aFilePath) {
+ return NS_MutateURI(mBaseURL).SetFilePath(aFilePath).Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP
+nsLDAPURL::GetQuery(nsACString& aQuery) { return mBaseURL->GetQuery(aQuery); }
+
+nsresult nsLDAPURL::SetQuery(const nsACString& aQuery) {
+ return NS_MutateURI(mBaseURL).SetQuery(aQuery).Finalize(mBaseURL);
+}
+
+nsresult nsLDAPURL::SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding) {
+ return NS_MutateURI(mBaseURL)
+ .SetQueryWithEncoding(aQuery, aEncoding)
+ .Finalize(mBaseURL);
+}
+
+NS_IMETHODIMP_(void)
+nsLDAPURL::Serialize(mozilla::ipc::URIParams& aParams) {
+ mBaseURL->Serialize(aParams);
+}
+
+NS_IMPL_ISUPPORTS(nsLDAPURL::Mutator, nsIURISetters, nsIURIMutator)
+
+NS_IMETHODIMP
+nsLDAPURL::Mutate(nsIURIMutator** aMutator) {
+ RefPtr<nsLDAPURL::Mutator> mutator = new nsLDAPURL::Mutator();
+ nsresult rv = mutator->InitFromURI(this);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mutator.forget(aMutator);
+ return NS_OK;
+}
diff --git a/comm/mailnews/addrbook/src/nsLDAPURL.h b/comm/mailnews/addrbook/src/nsLDAPURL.h
new file mode 100644
index 0000000000..60c3cbb6de
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsLDAPURL.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsString.h"
+#include "nsILDAPURL.h"
+#include "nsCOMPtr.h"
+#include "nsIURIMutator.h"
+
+/**
+ * nsLDAPURL
+ *
+ * nsLDAPURL uses an nsStandardURL stored in mBaseURL as its main url formatter.
+ *
+ * This is done to ensure that the pre-path sections of the URI are correctly
+ * formatted and to re-use the functions for nsIURI as appropriate.
+ *
+ * Handling of the path sections of the URI are done within nsLDAPURL/parts of
+ * the LDAP c-sdk. nsLDAPURL holds the individual sections of the path of the
+ * URI locally (to allow convenient get/set), but always updates the mBaseURL
+ * when one changes to ensure that mBaseURL.spec and the local data are kept
+ * consistent.
+ */
+
+class nsLDAPURL : public nsILDAPURL {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURI
+ NS_DECL_NSILDAPURL
+
+ nsLDAPURL();
+
+ protected:
+ virtual nsresult Clone(nsIURI** _retval);
+ virtual nsresult SetSpecInternal(const nsACString& aSpec);
+ virtual nsresult SetScheme(const nsACString& aScheme);
+ virtual nsresult SetUserPass(const nsACString& aUserPass);
+ virtual nsresult SetUsername(const nsACString& aUsername);
+ virtual nsresult SetPassword(const nsACString& aPassword);
+ virtual nsresult SetHostPort(const nsACString& aHostPort);
+ virtual nsresult SetHost(const nsACString& aHost);
+ virtual nsresult SetPort(int32_t aPort);
+ virtual nsresult SetPathQueryRef(const nsACString& aPath);
+ virtual nsresult SetRef(const nsACString& aRef);
+ virtual nsresult SetFilePath(const nsACString& aFilePath);
+ virtual nsresult SetQuery(const nsACString& aQuery);
+ virtual nsresult SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding);
+
+ public:
+ class Mutator : public nsIURIMutator, public BaseURIMutator<nsLDAPURL> {
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
+
+ NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD Finalize(nsIURI** aURI) override {
+ mURI.forget(aURI);
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetSpec(const nsACString& aSpec,
+ nsIURIMutator** aMutator) override {
+ if (aMutator) NS_ADDREF(*aMutator = this);
+ return InitFromSpec(aSpec);
+ }
+
+ explicit Mutator() {}
+
+ private:
+ virtual ~Mutator() {}
+
+ friend class nsLDAPURL;
+ };
+ friend BaseURIMutator<nsLDAPURL>;
+
+ protected:
+ virtual ~nsLDAPURL();
+
+ void GetPathInternal(nsCString& aPath);
+ nsresult SetPathInternal(const nsCString& aPath);
+
+ nsCString mDN; // Base Distinguished Name (Base DN)
+ int32_t mScope; // Search scope (base, one or sub)
+ nsCString mFilter; // LDAP search filter
+ uint32_t mOptions; // Options
+ nsCString
+ mAttributes; // Either empty ("") or comma-separated list with
+ // leading _and_ trailing commas (i.e ",attr1,attr2,").
+ nsCOMPtr<nsIURI> mBaseURL;
+};
diff --git a/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp b/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp
new file mode 100644
index 0000000000..bd6e080443
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMapiAddressBook.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/DebugOnly.h"
+
+#define PRINT_TO_CONSOLE 0
+#if PRINT_TO_CONSOLE
+# define PRINTF(args) printf args
+#else
+static mozilla::LazyLogModule gMapiAddressBookLog("MAPIAddressBook");
+# define PRINTF(args) \
+ MOZ_LOG(gMapiAddressBookLog, mozilla::LogLevel::Debug, args)
+#endif
+
+using namespace mozilla;
+
+HMODULE nsMapiAddressBook::mLibrary = NULL;
+int32_t nsMapiAddressBook::mLibUsage = 0;
+LPMAPIINITIALIZE nsMapiAddressBook::mMAPIInitialize = NULL;
+LPMAPIUNINITIALIZE nsMapiAddressBook::mMAPIUninitialize = NULL;
+LPMAPIALLOCATEBUFFER nsMapiAddressBook::mMAPIAllocateBuffer = NULL;
+LPMAPIFREEBUFFER nsMapiAddressBook::mMAPIFreeBuffer = NULL;
+LPMAPILOGONEX nsMapiAddressBook::mMAPILogonEx = NULL;
+
+BOOL nsMapiAddressBook::mInitialized = FALSE;
+BOOL nsMapiAddressBook::mLogonDone = FALSE;
+LPMAPISESSION nsMapiAddressBook::mRootSession = NULL;
+LPADRBOOK nsMapiAddressBook::mRootBook = NULL;
+
+BOOL nsMapiAddressBook::LoadMapiLibrary(void) {
+ if (mLibrary) {
+ ++mLibUsage;
+ return TRUE;
+ }
+ HMODULE libraryHandle = LoadLibraryW(L"MAPI32.DLL");
+
+ if (!libraryHandle) {
+ return FALSE;
+ }
+ FARPROC entryPoint = GetProcAddress(libraryHandle, "MAPIGetNetscapeVersion");
+
+ if (entryPoint) {
+ FreeLibrary(libraryHandle);
+ libraryHandle = LoadLibraryW(L"MAPI32BAK.DLL");
+ if (!libraryHandle) {
+ return FALSE;
+ }
+ }
+ mLibrary = libraryHandle;
+ ++mLibUsage;
+ mMAPIInitialize = reinterpret_cast<LPMAPIINITIALIZE>(
+ GetProcAddress(mLibrary, "MAPIInitialize"));
+ if (!mMAPIInitialize) {
+ return FALSE;
+ }
+ mMAPIUninitialize = reinterpret_cast<LPMAPIUNINITIALIZE>(
+ GetProcAddress(mLibrary, "MAPIUninitialize"));
+ if (!mMAPIUninitialize) {
+ return FALSE;
+ }
+ mMAPIAllocateBuffer = reinterpret_cast<LPMAPIALLOCATEBUFFER>(
+ GetProcAddress(mLibrary, "MAPIAllocateBuffer"));
+ if (!mMAPIAllocateBuffer) {
+ return FALSE;
+ }
+ mMAPIFreeBuffer = reinterpret_cast<LPMAPIFREEBUFFER>(
+ GetProcAddress(mLibrary, "MAPIFreeBuffer"));
+ if (!mMAPIFreeBuffer) {
+ return FALSE;
+ }
+ mMAPILogonEx =
+ reinterpret_cast<LPMAPILOGONEX>(GetProcAddress(mLibrary, "MAPILogonEx"));
+ if (!mMAPILogonEx) {
+ return FALSE;
+ }
+ MAPIINIT_0 mapiInit = {MAPI_INIT_VERSION, MAPI_MULTITHREAD_NOTIFICATIONS};
+ HRESULT retCode = mMAPIInitialize(&mapiInit);
+
+ if (HR_FAILED(retCode)) {
+ PRINTF(("Cannot initialize MAPI %08lx.\n", retCode));
+ return FALSE;
+ }
+ mInitialized = TRUE;
+ retCode = mMAPILogonEx(
+ 0, NULL, NULL,
+ MAPI_NO_MAIL | MAPI_USE_DEFAULT | MAPI_EXTENDED | MAPI_NEW_SESSION,
+ &mRootSession);
+ if (HR_FAILED(retCode)) {
+ PRINTF(("Cannot logon to MAPI %08lx.\n", retCode));
+ return FALSE;
+ }
+ mLogonDone = TRUE;
+ retCode = mRootSession->OpenAddressBook(0, NULL, 0, &mRootBook);
+ if (HR_FAILED(retCode)) {
+ PRINTF(("Cannot open MAPI address book %08lx.\n", retCode));
+ }
+ return HR_SUCCEEDED(retCode);
+}
+
+void nsMapiAddressBook::FreeMapiLibrary(void) {
+ if (mLibrary) {
+ if (--mLibUsage == 0) {
+ {
+ if (mRootBook) {
+ mRootBook->Release();
+ }
+ if (mRootSession) {
+ if (mLogonDone) {
+ mRootSession->Logoff(NULL, 0, 0);
+ mLogonDone = FALSE;
+ }
+ mRootSession->Release();
+ }
+ if (mInitialized) {
+ mMAPIUninitialize();
+ mInitialized = FALSE;
+ }
+ }
+ FreeLibrary(mLibrary);
+ mLibrary = NULL;
+ }
+ }
+}
+
+nsMapiAddressBook::nsMapiAddressBook(void) : nsAbWinHelper() {
+ mozilla::DebugOnly<BOOL> result = Initialize();
+
+ NS_ASSERTION(result == TRUE, "Couldn't initialize Mapi Helper");
+ MOZ_COUNT_CTOR(nsMapiAddressBook);
+}
+
+nsMapiAddressBook::~nsMapiAddressBook(void) {
+ StaticMutexAutoLock guard(sMutex);
+
+ FreeMapiLibrary();
+ MOZ_COUNT_DTOR(nsMapiAddressBook);
+}
+
+BOOL nsMapiAddressBook::Initialize(void) {
+ if (mAddressBook) {
+ return TRUE;
+ }
+ StaticMutexAutoLock guard(sMutex);
+
+ if (!LoadMapiLibrary()) {
+ PRINTF(("Cannot load library.\n"));
+ return FALSE;
+ }
+ mAddressBook = mRootBook;
+ mAddressSession = mRootSession;
+ mAddressFreeBuffer = mMAPIFreeBuffer;
+ return TRUE;
+}
+
+void nsMapiAddressBook::AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) {
+ mMAPIAllocateBuffer(aByteCount, aBuffer);
+}
+
+void nsMapiAddressBook::FreeBuffer(LPVOID aBuffer) { mMAPIFreeBuffer(aBuffer); }
diff --git a/comm/mailnews/addrbook/src/nsMapiAddressBook.h b/comm/mailnews/addrbook/src/nsMapiAddressBook.h
new file mode 100644
index 0000000000..dd0463dba0
--- /dev/null
+++ b/comm/mailnews/addrbook/src/nsMapiAddressBook.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#ifndef nsMapiAddressBook_h___
+#define nsMapiAddressBook_h___
+
+#include "mozilla/Attributes.h"
+#include "nsAbWinHelper.h"
+
+class nsMapiAddressBook : public nsAbWinHelper {
+ public:
+ nsMapiAddressBook(void);
+ virtual ~nsMapiAddressBook(void);
+
+ protected:
+ // Class members to handle the library/entry points
+ static HMODULE mLibrary;
+ static int32_t mLibUsage;
+ static LPMAPIINITIALIZE mMAPIInitialize;
+ static LPMAPIUNINITIALIZE mMAPIUninitialize;
+ static LPMAPIALLOCATEBUFFER mMAPIAllocateBuffer;
+ static LPMAPIFREEBUFFER mMAPIFreeBuffer;
+ static LPMAPILOGONEX mMAPILogonEx;
+ // Shared session and address book used by all instances.
+ // For reasons best left unknown, MAPI doesn't seem to like
+ // having different threads playing with supposedly different
+ // sessions and address books. They ll end up fighting over
+ // the same resources, with hangups and GPF resulting. Not nice.
+ // So it seems that if everybody (as long as some client is
+ // still alive) is using the same sessions and address books,
+ // MAPI feels better. And who are we to get in the way of MAPI
+ // happiness? Thus the following class members:
+ static BOOL mInitialized;
+ static BOOL mLogonDone;
+ static LPMAPISESSION mRootSession;
+ static LPADRBOOK mRootBook;
+
+ // Load the MAPI environment
+ BOOL Initialize(void);
+ // Allocation of a buffer for transmission to interfaces
+ virtual void AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) override;
+ // Destruction of a buffer provided by the interfaces
+ virtual void FreeBuffer(LPVOID aBuffer) override;
+ // Library management
+ static BOOL LoadMapiLibrary(void);
+ static void FreeMapiLibrary(void);
+
+ private:
+};
+
+#endif // nsMapiAddressBook_h___
diff --git a/comm/mailnews/addrbook/test/CardDAVServer.jsm b/comm/mailnews/addrbook/test/CardDAVServer.jsm
new file mode 100644
index 0000000000..5bd1275b41
--- /dev/null
+++ b/comm/mailnews/addrbook/test/CardDAVServer.jsm
@@ -0,0 +1,634 @@
+/* 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 = ["CardDAVServer"];
+
+const PREFIX_BINDINGS = {
+ card: "urn:ietf:params:xml:ns:carddav",
+ cs: "http://calendarserver.org/ns/",
+ d: "DAV:",
+};
+const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS)
+ .map(([prefix, url]) => `xmlns:${prefix}="${url}"`)
+ .join(" ");
+
+const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+const { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var CardDAVServer = {
+ books: {
+ "/addressbooks/me/default/": "Not This One",
+ "/addressbooks/me/test/": "CardDAV Test",
+ },
+ cards: new Map(),
+ movedCards: new Map(),
+ deletedCards: new Map(),
+ changeCount: 0,
+ server: null,
+ isOpen: false,
+
+ open(username, password, port = -1) {
+ this.server = new HttpServer();
+ this.server.start(port);
+ this.port = this.server.identity.primaryPort;
+ this.isOpen = true;
+
+ this.username = username;
+ this.password = password;
+ this.server.registerPathHandler("/ping", this.ping);
+
+ this.reset();
+ },
+
+ reopen() {
+ this.server.start(this.port);
+ this.isOpen = true;
+ },
+
+ reset() {
+ this.cards.clear();
+ this.deletedCards.clear();
+ this.changeCount = 0;
+ this.resetHandlers();
+ },
+
+ resetHandlers() {
+ // Address book discovery.
+
+ this.server.registerPathHandler("/", this.wellKnown.bind(this));
+ this.server.registerPathHandler(
+ "/.well-known/carddav",
+ this.wellKnown.bind(this)
+ );
+ this.server.registerPathHandler("/principals/", this.principals.bind(this));
+ this.server.registerPathHandler(
+ "/principals/me/",
+ this.myPrincipal.bind(this)
+ );
+ this.server.registerPathHandler(
+ "/addressbooks/me/",
+ this.myAddressBooks.bind(this)
+ );
+
+ // Address book interaction.
+
+ for (let path of Object.keys(this.books)) {
+ this.server.registerPathHandler(path, this.directoryHandler.bind(this));
+ this.server.registerPrefixHandler(path, this.cardHandler.bind(this));
+ }
+ },
+
+ close() {
+ if (!this.isOpen) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve =>
+ this.server.stop({
+ onStopped: () => {
+ this.isOpen = false;
+ resolve();
+ },
+ })
+ );
+ },
+
+ get origin() {
+ return `http://localhost:${this.server.identity.primaryPort}`;
+ },
+
+ get path() {
+ return "/addressbooks/me/test/";
+ },
+
+ get url() {
+ return `${this.origin}${this.path}`;
+ },
+
+ get altPath() {
+ return "/addressbooks/me/default/";
+ },
+
+ get altURL() {
+ return `${this.origin}${this.altPath}`;
+ },
+
+ checkAuth(request, response) {
+ if (!this.username || !this.password) {
+ return true;
+ }
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ let value = request.getHeader("Authorization");
+ if (!value.startsWith("Basic ")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ let [username, password] = atob(value.substring(6)).split(":");
+ if (username != this.username || password != this.password) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ return true;
+ },
+
+ ping(request, response) {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Content-Type", "text/plain");
+ response.write("pong");
+ },
+
+ wellKnown(request, response) {
+ response.setStatusLine("1.1", 301, "Moved Permanently");
+ response.setHeader("Location", "/principals/");
+ },
+
+ principals(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+ let propValues = {
+ "d:current-user-principal": "<href>/principals/me/</href>",
+ };
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(
+ `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/principals/</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>
+ </multistatus>`.replace(/>\s+</g, "><")
+ );
+ },
+
+ myPrincipal(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+ let propValues = {
+ "d:resourcetype": "<principal/>",
+ "card:addressbook-home-set": "<href>/addressbooks/me/</href>",
+ };
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(
+ `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/principals/me/</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>
+ </multistatus>`.replace(/>\s+</g, "><")
+ );
+ },
+
+ myAddressBooks(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/addressbooks/me/</href>
+ ${this._outputProps(propNames, {
+ "d:resourcetype": "<collection/>",
+ "d:displayname": "#addressbooks",
+ })}
+ </response>`;
+
+ for (let [path, name] of Object.entries(this.books)) {
+ output += `<response>
+ <href>${path}</href>
+ ${this._outputProps(propNames, {
+ "d:resourcetype": "<collection/><card:addressbook/>",
+ "d:displayname": name,
+ "d:current-user-privilege-set":
+ "<d:privilege><d:all/></d:privilege>",
+ })}
+ </response>`;
+ }
+
+ output += `</multistatus>`;
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ /** Handle any requests to the address book itself. */
+
+ directoryHandler(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let isRealDirectory = request.path == this.path;
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ switch (input.documentElement.localName) {
+ case "addressbook-query":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card);
+ this.addressBookQuery(input, response, isRealDirectory);
+ return;
+ case "addressbook-multiget":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card);
+ this.addressBookMultiGet(input, response, isRealDirectory);
+ return;
+ case "propfind":
+ Assert.equal(request.method, "PROPFIND");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d);
+ this.propFind(
+ input,
+ request.hasHeader("Depth") ? request.getHeader("Depth") : 0,
+ response,
+ isRealDirectory
+ );
+ return;
+ case "sync-collection":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d);
+ this.syncCollection(input, response, isRealDirectory);
+ return;
+ }
+
+ Assert.report(true, undefined, undefined, "Should not have reached here");
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`No handler found for <${input.documentElement.localName}>`);
+ },
+
+ addressBookQuery(input, response, isRealDirectory) {
+ if (this.mimicYahoo) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ let propNames = this._inputProps(input);
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ addressBookMultiGet(input, response, isRealDirectory) {
+ let propNames = this._inputProps(input);
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let href of input.querySelectorAll("href")) {
+ href = href.textContent;
+ if (this.movedCards.has(href)) {
+ href = this.movedCards.get(href);
+ }
+ let card = this.cards.get(href);
+ if (card) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ propFind(input, depth, response, isRealDirectory) {
+ let propNames = this._inputProps(input);
+
+ if (this.mimicYahoo && !propNames.includes("cs:getctag")) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ let propValues = {
+ "cs:getctag": this.changeCount,
+ "d:displayname": isRealDirectory ? "CardDAV Test" : "Not This One",
+ "d:resourcetype": "<collection/><card:addressbook/>",
+ "d:current-user-privilege-set": "<d:privilege><d:all/></d:privilege>",
+ };
+ if (!this.mimicYahoo) {
+ propValues["d:sync-token"] = `http://mochi.test/sync/${this.changeCount}`;
+ }
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>${isRealDirectory ? this.path : this.altPath}</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>`;
+ if (depth == 1 && isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ syncCollection(input, response, isRealDirectory) {
+ let token = input
+ .querySelector("sync-token")
+ .textContent.replace(/\D/g, "");
+ if (!token) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+ let propNames = this._inputProps(input);
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ if (card.changed > token) {
+ output += this._cardResponse(
+ href,
+ card,
+ propNames,
+ !this.mimicGoogle
+ );
+ }
+ }
+ for (let [href, deleted] of this.deletedCards) {
+ if (deleted > token) {
+ output += `<response>
+ <status>HTTP/1.1 404 Not Found</status>
+ <href>${href}</href>
+ <propstat>
+ <prop/>
+ <status>HTTP/1.1 418 I'm a teapot</status>
+ </propstat>
+ </response>`;
+ }
+ }
+ }
+ output += `<sync-token>http://mochi.test/sync/${this.changeCount}</sync-token>
+ </multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ _cardResponse(href, card, propNames, includeAddressData = true) {
+ let propValues = {
+ "d:getetag": card.etag,
+ "d:resourcetype": null,
+ };
+
+ if (includeAddressData) {
+ propValues["card:address-data"] = card.vCard;
+ }
+
+ let outString = `<response>
+ <href>${href}</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>`;
+ return outString;
+ },
+
+ _inputProps(input) {
+ let props = input.querySelectorAll("prop > *");
+ let propNames = [];
+
+ for (let p of props) {
+ Assert.equal(p.childElementCount, 0);
+ switch (p.localName) {
+ case "address-data":
+ case "addressbook-home-set":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.card);
+ propNames.push(`card:${p.localName}`);
+ break;
+ case "getctag":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.cs);
+ propNames.push(`cs:${p.localName}`);
+ break;
+ case "current-user-principal":
+ case "current-user-privilege-set":
+ case "displayname":
+ case "getetag":
+ case "resourcetype":
+ case "sync-token":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.d);
+ propNames.push(`d:${p.localName}`);
+ break;
+ default:
+ Assert.report(
+ true,
+ undefined,
+ undefined,
+ `Unknown property requested: ${p.nodeName}`
+ );
+ break;
+ }
+ }
+
+ return propNames;
+ },
+
+ _outputProps(propNames, propValues) {
+ let output = "";
+
+ let found = [];
+ let notFound = [];
+ for (let p of propNames) {
+ if (p in propValues && propValues[p] !== undefined) {
+ found.push(`<${p}>${propValues[p]}</${p}>`);
+ } else {
+ notFound.push(`<${p}/>`);
+ }
+ }
+
+ if (found.length > 0) {
+ output += `<propstat>
+ <prop>
+ ${found.join("\n")}
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>`;
+ }
+ if (notFound.length > 0) {
+ output += `<propstat>
+ <prop>
+ ${notFound.join("\n")}
+ </prop>
+ <status>HTTP/1.1 404 Not Found</status>
+ </propstat>`;
+ }
+
+ return output;
+ },
+
+ /** Handle any requests to address book cards. */
+
+ cardHandler(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let isRealDirectory = request.path.startsWith(this.path);
+ if (!isRealDirectory || !/\/[\w-]+\.vcf$/.test(request.path)) {
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Card not found at ${request.path}`);
+ return;
+ }
+
+ switch (request.method) {
+ case "GET":
+ this.getCard(request, response);
+ return;
+ case "PUT":
+ this.putCard(request, response);
+ return;
+ case "DELETE":
+ this.deleteCard(request, response);
+ return;
+ }
+
+ Assert.report(true, undefined, undefined, "Should not have reached here");
+ response.setStatusLine("1.1", 405, "Method Not Allowed");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Method not allowed: ${request.method}`);
+ },
+
+ getCard(request, response) {
+ let card = this.cards.get(request.path);
+ if (!card) {
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Card not found at ${request.path}`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Content-Type", "text/vcard");
+ response.setHeader("ETag", card.etag);
+ response.write(card.vCard);
+ },
+
+ putCard(request, response) {
+ if (request.hasHeader("If-Match")) {
+ let card = this.cards.get(request.path);
+ if (!card || card.etag != request.getHeader("If-Match")) {
+ response.setStatusLine("1.1", 412, "Precondition Failed");
+ return;
+ }
+ }
+
+ let vCard = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ if (this.mimicGoogle && !/^N[;:]/im.test(vCard)) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ this.putCardInternal(request.path, vCard);
+ response.setStatusLine("1.1", 204, "No Content");
+
+ if (this.responseDelay) {
+ response.processAsync();
+ this.responseDelay.promise.then(() => {
+ delete this.responseDelay;
+ response.finish();
+ });
+ }
+ },
+
+ putCardInternal(name, vCard) {
+ if (!name.startsWith("/")) {
+ name = this.path + name;
+ }
+ if (this.modifyCardOnPut && !this.cards.has(name)) {
+ vCard = vCard.replace(/UID:(\S+)/, (match, uid) => {
+ let newUID = [...uid].reverse().join("");
+ let newName = this.path + newUID + ".vcf";
+ this.movedCards.set(name, newName);
+ name = newName;
+ return "UID:" + newUID + "\r\nX-MODIFIED-BY-SERVER:1";
+ });
+ }
+ if (this.mimicGoogle && vCard.includes("\nPHOTO")) {
+ let [, version] = vCard.match(/VERSION:([34]\.0)/);
+ if (version && version != "3.0") {
+ let start = vCard.indexOf("\nPHOTO") + 1;
+ let end = vCard.indexOf("\n", start) + 1;
+ while (vCard[end] == " ") {
+ end = vCard.indexOf("\n", end) + 1;
+ }
+ vCard = vCard.substring(0, start) + vCard.substring(end);
+ }
+ }
+ let etag = "" + vCard.length;
+ this.cards.set(name, { etag, vCard, changed: ++this.changeCount });
+ this.deletedCards.delete(name);
+ },
+
+ deleteCard(request, response) {
+ this.deleteCardInternal(request.path);
+ response.setStatusLine("1.1", 204, "No Content");
+
+ if (this.responseDelay) {
+ response.processAsync();
+ this.responseDelay.promise.then(() => {
+ delete this.responseDelay;
+ response.finish();
+ });
+ }
+ },
+
+ deleteCardInternal(name) {
+ if (!name.startsWith("/")) {
+ name = this.path + name;
+ }
+ this.cards.delete(name);
+ this.deletedCards.set(name, ++this.changeCount);
+ },
+};
diff --git a/comm/mailnews/addrbook/test/LDAPServer.jsm b/comm/mailnews/addrbook/test/LDAPServer.jsm
new file mode 100644
index 0000000000..c8d8edb82b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/LDAPServer.jsm
@@ -0,0 +1,324 @@
+/* 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 = ["LDAPServer"];
+const PRINT_DEBUG = false;
+
+const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+
+/**
+ * This is a partial implementation of an LDAP server as defined by RFC 4511.
+ * It's not intended to serve any particular dataset, rather, tests should
+ * cause the application to make requests and tell the server what to respond.
+ *
+ * https://docs.ldap.com/specs/rfc4511.txt
+ *
+ * @implements {nsIInputStreamCallback}
+ * @implements {nsIServerSocketListener}
+ */
+var LDAPServer = {
+ BindRequest: 0x60,
+ UnbindRequest: 0x42,
+ SearchRequest: 0x63,
+ AbandonRequest: 0x50,
+
+ serverSocket: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInputStreamCallback",
+ "nsIServerSocketListener",
+ ]),
+
+ /**
+ * Start listening on an OS-selected port. The port number can be found at
+ * LDAPServer.port.
+ */
+ open() {
+ this.serverSocket = Cc[
+ "@mozilla.org/network/server-socket;1"
+ ].createInstance(Ci.nsIServerSocket);
+ this.serverSocket.init(-1, true, 1);
+ console.log(`socket open on port ${this.serverSocket.port}`);
+
+ this.serverSocket.asyncListen(this);
+ },
+ /**
+ * Stop listening for new connections and close any that are open.
+ */
+ close() {
+ this.serverSocket.close();
+ },
+ /**
+ * The port this server is listening on.
+ */
+ get port() {
+ return this.serverSocket.port;
+ },
+
+ /**
+ * Retrieves any data sent to the server since connection or the previous
+ * call to read(). This should be called every time the application is
+ * expected to send data.
+ *
+ * @returns {Promise} Resolves when data is received by the server, with the
+ * data as a byte array.
+ */
+ async read(expectedOperation) {
+ let data;
+ if (this._data) {
+ data = this._data;
+ delete this._data;
+ } else {
+ data = await new Promise(resolve => {
+ this._inputStreamReadyResolve = resolve;
+ });
+ }
+
+ // Simplified parsing to get the message ID and operation code.
+
+ let index = 4;
+ // The value at [1] may be more than one byte. If it is, skip more bytes.
+ if (data[1] & 0x80) {
+ index += data[1] & 0x7f;
+ }
+
+ // Assumes the ID is not greater than 127.
+ this._lastMessageID = data[index];
+
+ if (expectedOperation) {
+ let actualOperation = data[index + 1];
+
+ // Unbind and abandon requests can happen at any point, when an
+ // nsLDAPConnection is destroyed. This is unpredictable, and irrelevant
+ // for testing. Ignore.
+ if (
+ actualOperation == LDAPServer.UnbindRequest ||
+ actualOperation == LDAPServer.AbandonRequest
+ ) {
+ if (PRINT_DEBUG) {
+ console.log("Ignoring unbind or abandon request");
+ }
+ return this.read(expectedOperation);
+ }
+
+ Assert.equal(
+ actualOperation.toString(16),
+ expectedOperation.toString(16),
+ "LDAP Operation type"
+ );
+ }
+
+ return data;
+ },
+ /**
+ * Sends raw data to the application. Generally this shouldn't be used
+ * directly but it may be useful for testing.
+ *
+ * @param {byte[]} data - The data to write.
+ */
+ write(data) {
+ if (PRINT_DEBUG) {
+ console.log(
+ ">>> " + data.map(b => b.toString(16).padStart(2, 0)).join(" ")
+ );
+ }
+ this._outputStream.writeByteArray(data);
+ },
+ /**
+ * Sends a simple BindResponse to the application.
+ * See section 4.2.2 of the RFC.
+ */
+ writeBindResponse() {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+ let person = new Sequence(
+ 0x61,
+ new EnumeratedValue(0),
+ new StringValue(""),
+ new StringValue("")
+ );
+ message.children.push(person);
+ this.write(message.getBytes());
+ },
+ /**
+ * Sends a SearchResultEntry to the application.
+ * See section 4.5.2 of the RFC.
+ *
+ * @param {object} entry
+ * @param {string} entry.dn - The LDAP DN of the person.
+ * @param {string} entry.attributes - A key/value or key/array-of-values
+ * object representing the person.
+ */
+ writeSearchResultEntry({ dn, attributes }) {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+
+ let person = new Sequence(0x64, new StringValue(dn));
+ message.children.push(person);
+
+ let attributeSequence = new Sequence(0x30);
+ person.children.push(attributeSequence);
+
+ for (let [key, value] of Object.entries(attributes)) {
+ let seq = new Sequence(0x30, new StringValue(key), new Sequence(0x31));
+ if (typeof value == "string") {
+ value = [value];
+ }
+ for (let v of value) {
+ seq.children[1].children.push(new StringValue(v));
+ }
+ attributeSequence.children.push(seq);
+ }
+
+ this.write(message.getBytes());
+ },
+ /**
+ * Sends a SearchResultDone to the application.
+ * See RFC 4511 section 4.5.2.
+ */
+ writeSearchResultDone() {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+ let person = new Sequence(
+ 0x65,
+ new EnumeratedValue(0),
+ new StringValue(""),
+ new StringValue("")
+ );
+ message.children.push(person);
+ this.write(message.getBytes());
+ },
+
+ /**
+ * nsIServerSocketListener.onSocketAccepted
+ */
+ onSocketAccepted(socket, transport) {
+ let inputStream = transport
+ .openInputStream(0, 8192, 1024)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+
+ let outputStream = transport.openOutputStream(0, 0, 0);
+ this._outputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ this._outputStream.setOutputStream(outputStream);
+
+ if (this._socketConnectedResolve) {
+ this._socketConnectedResolve();
+ delete this._socketConnectedResolve;
+ }
+ inputStream.asyncWait(this, 0, 0, Services.tm.mainThread);
+ },
+ /**
+ * nsIServerSocketListener.onStopListening
+ */
+ onStopListening(socket, status) {
+ console.log(`socket closed with status ${status.toString(16)}`);
+ },
+
+ /**
+ * nsIInputStreamCallback.onInputStreamReady
+ */
+ onInputStreamReady(stream) {
+ let available;
+ try {
+ available = stream.available();
+ } catch (ex) {
+ if (
+ [Cr.NS_BASE_STREAM_CLOSED, Cr.NS_ERROR_NET_RESET].includes(ex.result)
+ ) {
+ return;
+ }
+ throw ex;
+ }
+
+ let binaryInputStream = Cc[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Ci.nsIBinaryInputStream);
+ binaryInputStream.setInputStream(stream);
+ let data = binaryInputStream.readByteArray(available);
+ if (PRINT_DEBUG) {
+ console.log(
+ "<<< " + data.map(b => b.toString(16).padStart(2, 0)).join(" ")
+ );
+ }
+
+ if (this._inputStreamReadyResolve) {
+ this._inputStreamReadyResolve(data);
+ delete this._inputStreamReadyResolve;
+ } else {
+ this._data = data;
+ }
+
+ stream.asyncWait(this, 0, 0, Services.tm.mainThread);
+ },
+};
+
+/**
+ * Helper classes to convert primitives to LDAP byte sequences.
+ */
+
+class Sequence {
+ constructor(number, ...children) {
+ this.number = number;
+ this.children = children;
+ }
+ getBytes() {
+ let bytes = [];
+ for (let c of this.children) {
+ bytes = bytes.concat(c.getBytes());
+ }
+ return [this.number].concat(getLengthBytes(bytes.length), bytes);
+ }
+}
+class IntegerValue {
+ constructor(int) {
+ this.int = int;
+ this.number = 0x02;
+ }
+ getBytes() {
+ let temp = this.int;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ return [this.number].concat(getLengthBytes(bytes.length), bytes);
+ }
+}
+class StringValue {
+ constructor(str) {
+ this.str = str;
+ }
+ getBytes() {
+ return [0x04].concat(
+ getLengthBytes(this.str.length),
+ Array.from(this.str, c => c.charCodeAt(0))
+ );
+ }
+}
+class EnumeratedValue extends IntegerValue {
+ constructor(int) {
+ super(int);
+ this.number = 0x0a;
+ }
+}
+
+function getLengthBytes(int) {
+ if (int < 128) {
+ return [int];
+ }
+
+ let temp = int;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ bytes.unshift(0x80 | bytes.length);
+ return bytes;
+}
diff --git a/comm/mailnews/addrbook/test/moz.build b/comm/mailnews/addrbook/test/moz.build
new file mode 100644
index 0000000000..c513212222
--- /dev/null
+++ b/comm/mailnews/addrbook/test/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+TESTING_JS_MODULES += [
+ "CardDAVServer.jsm",
+ "LDAPServer.jsm",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell.ini",
+ "unit/xpcshell_cardDAV.ini",
+]
diff --git a/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js
new file mode 100644
index 0000000000..4d810d8fc0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js
@@ -0,0 +1,7 @@
+/* globals user_pref */
+user_pref("ldap_2.servers.extension.description", "extension");
+user_pref("ldap_2.servers.extension.filename", "ldap1.mab");
+user_pref(
+ "ldap_2.servers.extension.uri",
+ "ldap://test.invalid:389/o=invalid??sub"
+);
diff --git a/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql
new file mode 100644
index 0000000000..68a226c325
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql
@@ -0,0 +1,95 @@
+-- Address book data for use in various tests.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 1),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 2),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 3),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 4);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastName', 'Email'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'DisplayName', 'Empty Email'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'FirstName', 'Empty'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'AllowRemoteContent', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PopularityIndex', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PreferMailFormat', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastModifiedDate', '0'),
+
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastName', 'LastName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom4', 'Custom41'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastModifiedDate', '1237281794'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage2', 'http://WebPage11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'NickName', 'NickName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'DisplayName', 'DisplayName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkZipCode', 'WorkZipCode1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', '_AimScreenName', 'ScreenName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress', 'WorkAddress1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCountry', 'HomeCountry1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkPhone', 'WorkPhone1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PrimaryEmail', 'PrimaryEmail1@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress', 'HomeAddress11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LowercasePrimaryEmail', 'primaryemail1@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCity', 'WorkCity1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'SecondEmail', 'SecondEmail1Ã@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeZipCode', 'HomeZipCode1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom3', 'Custom31'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FaxNumber', 'FaxNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom1', 'Custom11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomePhone', 'HomePhone1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FirstName', 'FirstName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCity', 'HomeCity1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PagerNumber', 'PagerNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'CellularNumber', 'CellularNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress2', 'WorkAddress21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkState', 'WorkState1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress2', 'HomeAddress21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage1', 'http://WebPage21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Notes', 'Notes1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom2', 'Custom21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Department', 'Department1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCountry', 'WorkCountry1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeState', 'HomeState1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'JobTitle', 'JobTitle1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Company', 'Organization1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PopularityIndex', '0'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'AllowRemoteContent', '1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PreferMailFormat', '0'),
+
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastModifiedDate', '1245128765'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'NickName', 'johnd'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'DisplayName', 'John Doe'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastName', 'Doe'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PrimaryEmail', 'john.doe@mailinator.invalid'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'FirstName', 'John'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LowercasePrimaryEmail', 'john.doe@mailinator.invalid'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'AllowRemoteContent', '0'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PopularityIndex', '0'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PreferMailFormat', '0'),
+
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'NickName', 'janed'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'DisplayName', 'Jane Doe'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastName', 'Doe'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PrimaryEmail', 'jane.doe@mailinator.invalid'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'FirstName', 'Jane'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LowercasePrimaryEmail', 'jane.doe@mailinator.invalid'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastModifiedDate', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'AllowRemoteContent', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PopularityIndex', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PreferMailFormat', '0'),
+
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'DisplayName', 'A vCard!'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'PrimaryEmail', 'first@something.invalid'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'SecondEmail', 'second@something.invalid'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', '_vCard', 'BEGIN:VCARD
+FN:A vCard!
+EMAIL:first@something.invalid
+EMAIL:second@something.invalid
+EMAIL:third@something.invalid
+EMAIL:fourth@something.invalid
+END:VCARD');
diff --git a/comm/mailnews/addrbook/test/unit/data/collect.sql b/comm/mailnews/addrbook/test/unit/data/collect.sql
new file mode 100644
index 0000000000..dba17a7392
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/collect.sql
@@ -0,0 +1,21 @@
+-- Collection address book for use in test_collection_2.js.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 1);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'DisplayName', 'Other Book'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastName', 'Book'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PrimaryEmail', 'other@book.invalid'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'FirstName', 'Other'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LowercasePrimaryEmail', 'other@book.invalid'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastModifiedDate', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'AllowRemoteContent', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PopularityIndex', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PreferMailFormat', '0');
diff --git a/comm/mailnews/addrbook/test/unit/data/export.csv b/comm/mailnews/addrbook/test/unit/data/export.csv
new file mode 100644
index 0000000000..92da47c7be
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.csv
@@ -0,0 +1,4 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Screen Name,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes
+contact,one,contact number one,,contact1@invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+contact,two,contact number two,,contact2@invalid,,,,,,,,,,,,,,,,,,,,"""worker""",,,,,,,,"custom, 1","custom 2","custom 3","custom
+4",here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.ldif b/comm/mailnews/addrbook/test/unit/data/export.ldif
new file mode 100644
index 0000000000..669b63f6a4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.ldif
@@ -0,0 +1,36 @@
+dn: cn=new list
+objectclass: top
+objectclass: groupOfNames
+cn: new list
+member: cn=contact number one,mail=contact1@invalid
+
+dn: cn=contact number one,mail=contact1@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: one
+cn: contact number one
+mail: contact1@invalid
+modifytimestamp: 12345
+
+dn: cn=contact number two,mail=contact2@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: two
+cn: contact number two
+mail: contact2@invalid
+modifytimestamp: 12345
+title: "worker"
+mozillaCustom1: custom, 1
+mozillaCustom2: custom 2
+mozillaCustom3:: Y3VzdG9tDTM=
+mozillaCustom4:: Y3VzdG9tCjQ=
+description:: aGVyZSdzIHNvbWUgdW5pY29kZSB0ZXh04oCm
+
diff --git a/comm/mailnews/addrbook/test/unit/data/export.txt b/comm/mailnews/addrbook/test/unit/data/export.txt
new file mode 100644
index 0000000000..82f9c468ae
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.txt
@@ -0,0 +1,4 @@
+First Name Last Name Display Name Nickname Primary Email Secondary Email Screen Name Work Phone Home Phone Fax Number Pager Number Mobile Number Home Address Home Address 2 Home City Home State Home ZipCode Home Country Work Address Work Address 2 Work City Work State Work ZipCode Work Country Job Title Department Organization Web Page 1 Web Page 2 Birth Year Birth Month Birth Day Custom 1 Custom 2 Custom 3 Custom 4 Notes
+contact one contact number one contact1@invalid
+contact two contact number two contact2@invalid """worker""" "custom, 1" "custom 2" "custom 3" "custom
+4" here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.vcf b/comm/mailnews/addrbook/test/unit/data/export.vcf
new file mode 100644
index 0000000000..91cc8b7016
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.vcf
@@ -0,0 +1,20 @@
+BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact1@invalid
+FN:contact number one
+N:one;contact;;;
+UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+END:VCARD
+BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact2@invalid
+FN:contact number two
+NOTE:here's some unicode text…
+TITLE:"worker"
+N:two;contact;;;
+X-CUSTOM1;VALUE=TEXT:custom\, 1
+X-CUSTOM2;VALUE=TEXT:custom 2
+X-CUSTOM3;VALUE=TEXT:custom 3
+X-CUSTOM4;VALUE=TEXT:custom\n4
+UID:yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json
new file mode 100644
index 0000000000..c239820d51
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json
@@ -0,0 +1,104 @@
+{
+ "eurus": {
+ "dn": "uid=eurus,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Eurus Holmes",
+ "givenName": "Eurus",
+ "mail": "eurus@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ },
+ "irene": {
+ "dn": "uid=irene,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Irene Adler",
+ "givenName": "irene",
+ "mail": "irene@bakerstreet.invalid",
+ "sn": "Adler"
+ }
+ },
+ "john": {
+ "dn": "uid=john,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "John Watson",
+ "givenName": "John",
+ "mail": "john@bakerstreet.invalid",
+ "sn": "Watson"
+ }
+ },
+ "lestrade": {
+ "dn": "uid=lestrade,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Greg Lestrade",
+ "givenName": "Greg",
+ "mail": "lestrade@bakerstreet.invalid",
+ "o": "New Scotland Yard",
+ "sn": "Lestrade"
+ }
+ },
+ "mary": {
+ "dn": "uid=mary,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mary Watson",
+ "givenName": "Mary",
+ "mail": "mary@bakerstreet.invalid",
+ "sn": "Watson"
+ }
+ },
+ "molly": {
+ "dn": "uid=molly,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Molly Hooper",
+ "givenName": "Molly",
+ "mail": "molly@bakerstreet.invalid",
+ "o": "St. Bartholomew's Hospital",
+ "sn": "Hooper"
+ }
+ },
+ "moriarty": {
+ "dn": "uid=moriarty,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Jim Moriarty",
+ "givenName": "Jim",
+ "mail": "moriarty@bakerstreet.invalid",
+ "sn": "Moriarty"
+ }
+ },
+ "mrs_hudson": {
+ "dn": "uid=mrs_hudson,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mrs Hudson",
+ "givenName": "Martha",
+ "mail": "mrs_hudson@bakerstreet.invalid",
+ "sn": "Hudson"
+ }
+ },
+ "mycroft": {
+ "dn": "uid=mycroft,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mycroft Holmes",
+ "givenName": "Mycroft",
+ "mail": "mycroft@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ },
+ "sherlock": {
+ "dn": "uid=sherlock,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Sherlock Holmes",
+ "givenName": "Sherlock",
+ "mail": "sherlock@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat
new file mode 100644
index 0000000000..7621d1e76d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat
@@ -0,0 +1,17 @@
+version="9"
+logging="no"
+name="From is in book 7"
+enabled="yes"
+type="17"
+action="Mark flagged"
+condition="AND (from,is in ab,moz-abmdbdirectory://abook-7.mab) AND (subject,contains,nothing)"
+name="From is not in book 8"
+enabled="yes"
+type="17"
+action="Mark flagged"
+condition="AND (from,isn't in ab,moz-abmdbdirectory://abook-8.na2.mab)"
+name="Not related"
+enabled="yes"
+type="17"
+action="Mark read"
+condition="AND (subject,contains,unrelated)"
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf
new file mode 100644
index 0000000000..f877f281ec
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf
@@ -0,0 +1,101 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:Binary JPEG
+NOTE:This v3.0 card has a JPEG photo as binary data and binary valuetype.
+PHOTO:/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQF
+ xQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhY
+ aKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wgARC
+ ACAAIADAREAAhEBAxEB/8QAGwAAAQUBAQAAAAAAAAAAAAAAAAIEBQYHAwH/xAAZAQEAAwEBAAA
+ AAAAAAAAAAAAAAgMEAQX/2gAMAwEAAhADEAAAAdUAAAr+rHE2wk67TnXtdstTYAAAAAR841jd5
+ 0PpytpSbS627Ll2TqErRk0XPBqdQkAAFT10Uj0MMrzO2l1v2TKckOgAOI90fyt9gzXgFU10Z56
+ mCdrobOJ71pO1t2ZwrsfXFCuc6caV5HqTdFrKcch9rzJjNoTdhgvO9SR9XzWMp8o3c42qJCeDo
+ ivkePZda9GueL6dF35oLRTo3kb6P6vlRkpWbNqn817aXIezsPZLjf5/l3m80265XO3jzvQpu7P
+ acl9nw6IPXjm8+x5CQAAMrKM09jw+XL/eS952Qo2V/XRd8Oi7YNkYSIAAANHMw9nxVcCXHlnJo
+ VOu/wDn32TLqQMR+BxGR4OXM39bxefZcO2N+2JSk4d1fydwA1GQs7D4BlOqi7/Pg7+oS8F8lZM
+ 992w3uoyAAAABLlM3ZKhrznXnExTdfvP2ZRthZMl9uqjJnoAAHHsc19PFBWyddzhoHnb7LmupW
+ qvH79Fip4/hzUs8VD09ADM/Qrr2nKtGaplqPmbvQKPojRLLNnyR78LGIkUNbY5J6dKepGENX8z
+ U/h0ACPlx3HvUAAibI5h6NLCzkvTPTvPtfw6AAAAABG2RgroJdeR7Zc8wAP/EACYQAAEEAgEEA
+ gMBAQAAAAAAAAQAAQIDBRETEBIgIQYUFTAxIiP/2gAIAQEAAQUC8SsjGtQPkvy9LJswNurIi2J
+ vf6SzKRY3nzIhJ1J1J079KSLaELm5shyKiIeOUyrUqqMr7ZOpOpJ5eNNk6Z4zIxKbrmj+CLNtR
+ bshKaZTffTfXS0m3F8WaxdKMvYYec5W2Y8CwtWelXPkItkn6xnpMy0tJ39iXSGvqm1lfyO/dmO
+ G+0SO/ExdNnJHudCYfa/HCasxAk1bgIqzBlRTBkjwU/Un9vpYG/cMjZyn/HI+ia+9oUOVIcaod
+ vEyHILan/0+k/pY63iye+6Xx2ftT/4W+Vt9Vaf+6Uv8xvUJ6vFhsyjHOMWpM0o0O8JdJ2QrX2Z
+ TXBdYoD1VN/XZ92XS27+3REeDLdb6+SLEzdcZFirGqh1MnxjEDzqFZ9da499nyOjtvFs5h/0O2
+ 1n7dzfriKuU88ZihWOJAkL8ig6oPHubyunx15CF0Ghuc39dMGPxjrP4z7EZs8XxYMjKMfkLxjN
+ kQUSa9/3w+Q38pfI0Y1M7vjhvtks2m6ZrCsSsTdLG5UgWgmNVcaq5RaTOLFlsitRJr3kjIiDSd
+ 5SQ9dpdoQsBKPAsMcuNUOOrwyBVI1WrDL5aZwcdcW4YlQlf6iXKkmxEO64QgiImHHp8f//EACg
+ RAAEDAwMEAgIDAAAAAAAAAAEAAhEDEiEEEDETICIwQVEyUhRhcf/aAAgBAwEBPwHtqaocMQrxy
+ ZX8piGpYUKrHcH1Fwbyq+ou8W9we5vBTNT+ya4OyO6pVtwFUqbjua4tyFSrB/8AvZWqW4CLlys
+ oN2AVh7JQdBkKlUvGznWiU90lUtODlyAzCbQYGElBAIBAJ9G7hHClXIZgpj7DKBnK1DvhUGXOl
+ cJwIdKuqVPGcKnpwPyXTb9LptViAWupx5hTKp+QKGBG2mdLIVYy4rS/incJtLqmTwmsDeO7UNu
+ pOCoCcpjbRGwErSp60jsEeoNtwNmiSqI+VT8cKPKCmUDTfc3j1kQ1UxCGNniH+qs61hKFEgXOX
+ O7clV2/KaZE+rUu+EM70RLk5twhUP1Kt9DjaJXSe83OVSKbYCAnaiy1uzhBuCa64SESo+u9xXS
+ LzLlVIaLGqjSnJ7OMhTd3uMbH+kyiJk+wuhZcdgJQEesz8Lp/aIJwgwDt/8QAJxEAAgECBQMFA
+ QEAAAAAAAAAAQIAAxEEEiAhMRATMBQiQUJRMmH/2gAIAQIBAT8B008MTu0NC/AtPRvPR1I2HqL
+ 8eJKbPxKWHCbnU1NX5EqYIfSOjIbNqoYbPu3EsFFh4WUOLNK+HNPccaMNQz+48dOeo6X0kA7GV
+ 6Pab/OlNM7ZYqhRYStiQmwnxKuKZagVf20A0BraaqCouUwixsZgk5aYip21nMpupW07VND3Lb/
+ sq4z4Sd+p+wYmoPmDFn5E9ShlKsrbDReYtfdmmHWyCY07gdM4oj/Y9Rn/AK1UWyuD1vLyuMyRR
+ YTGjg+K8v0EYXFozewkR8R3Eytz4/iDrTOal4qYuwiOGbKNDGwvMG+2WOuViPFhF2LaMQ2VJTf
+ I15WpB/cIaJEtbWq5jaU3X+F0Yl8zW/OlCrb2NCLR2sd+I1MEXXXhkst+tarkGinW+rSouZYCR
+ xqpU+4eruEFzHcubnSrFeNVOmah2gC0lt0qVlSO5c3PjXJ9p6k8ILRair7m3MfEM3Gn/8QANRA
+ AAQMBBAgEBQMFAAAAAAAAAQACAxESIUFRBBAgIjEyYXETIzBSQmKBscEzQ5FygqHR4f/aAAgBA
+ QAGPwLZIi3jmrcjnOdg3gAuWQnsrxIPorpgD81yu9Gsrr/aOJXLYYcMdrypHN7FU0lloe5vFWo
+ Xhw2jFo9DJickXyEuzJx9EPicWu6Kw/dm++x4MJ808T7dQA1VPoWmmhF4K3v1W8w/Op8rsOCc9
+ 5q43lWuSH3Zoqxavs2rsO6psX8NhwTZGYYZpr2crhVRwDgN4psfw8XdkYX3WBd1CebPG9COIEk
+ 4BWtLP9gX6DVyFvZy8qcj+oLdMb+xXnx2W51rqbrfAfh3h2U7vmop3Y3BBw5mqtqzGMRivKYB1
+ x2pG9FTYhydun6ok4qZnZ2q3+2/m6HPb33fTXVAJsmRqomP4WwD/KEsD9zFpy1EHgV4L8OU5jX
+ vuAXkRud1NwXnS0HtYqhv1OqioFXU6uEtf87F1zxe0qjYXF+OS33iMZNVbNo5uv1yHpReK/dqa
+ AK7W1oxNEyYcHCh7qOT3Nr6N6jiGF52I8m7yfHjxHdWRwBvCpMKKrXjbLqE9BijpGkNoXuuCqc
+ NZldzSfbUZoRv/EM1QqY6NIW6TGa0rzBCHTXOjHAkjgr2tlHS4qjqsdk65XbAiBuj+6o1WnKz+
+ 2295/CoOGsy6NdJi3NM8cFgO4+qpPEyQdQmxs5WigvVHAEdVWJzoz0V4bKOlxVHVY7J1ytVBe7
+ kCJdeSr0IoR/xCNn1OZ2aaRGHdcU1gJIaKX7PnUcTwZmiWt/00I0NRmqgWY/eVZiHc4n07OjtY
+ 353n8Iy6XLJM7oF4MEbdF0brxcqyea75uH8bP8A/8QAKBABAAIBAwEJAAMBAAAAAAAAAQARITF
+ BUXEQIGGBkaGxwdEw4fDx/9oACAEBAAE/Ie5pKwgb9PLmGBVcL5+tekRZ5AAPvFU8RT+xeuKfp
+ EAUI7n8NWYtMnkSqHizI8fzvATXl+D0iITpHo0+JzKDk6neZSPGqftmQsWi7doqi2MvdGiN4BU
+ gabdP53Nsjg2fsRQNhS7SaJWmLASpUIJrlY41GVqhj5PB2ZeK4cuxLgxsY/nU6jPRG1upiJlrP
+ Wqu2HT1m0iixZdqFhZpCSKkcYmvdq4NyLlYBLsYfOcH36y2LDmIxgWZ7ssbBttpxFjIr0j1aeL
+ 8v5AqYPOBYby33GW8UW+5NOjo/MeX2Yo+DKDWOTJeYRcPPqWvv8zqDHQx9Q/GvlnipitzcibIq
+ 3Oj9lEZddS6ve88p1MzSDrL2sIoReamp/8AhPeovUStgDdUD4frs0/Wod8GIN8MsFtYQ4Ho9Yu
+ lJnyYYK0X6GDYczU22vfsNOwpIra3Jv8AGO0y+os0M2Poke8rhs8RlWLvMQ0CcGEay1ZUoej0r
+ buVt7PgGG1bg6B6z7GX1inuI7eRcHVxNjjZrvn2iW5Y7Ki6iIecq97af18QRthdd/4QpQabL2g
+ ItPraShrK7NEXl+WnvUZNDy8DSZXjBcUyptuXHuf1CY18uPXSCJY2d4W6GgWrgmCBSuf+E4rhF
+ L37KocHw/3TsyOgx6jk8Yr1CFWdohfHmyPF7gC9lNyUeoXBVvhMCC1Y7ncPBXT1a/UKJvxZqK4
+ iEfgP2YABQwB212NfQOjxjZolKofxphLZDWOjCZSoKaOrPCzQuKdUuPSeqTAro0woOAcjz0ihK
+ rV3YVfCUD714HKwn9M73J3eFXNA84ZZALto5e7fh4tMPj9IY6zYgCqHFKuHuVjjy5ms66vufxg
+ PokdB9y0MNo2flmS25F5zURCT3EAAoKDuf//aAAwDAQACAAMAAAAQkkjRaEkkki6RagMkkMw12
+ 2ZkkVsEe43UnzMsoIVRiqelxLXtd8QAAdbM9EAAAOYYbEAEkOpoQAAkAtCZDAAAA5w91SgAAeB
+ gklMAAE/8kYCAArvskhUklhwkkkkkLUEk/8QAJREBAAICAQMDBQEAAAAAAAAAAQARITFBECBRM
+ GGRcYGhsfDB/9oACAEDAQE/EO0TVb54gsbntgPjc4qf77xTNnp4GWo98Rz7wIECBBrXTBTA+IB
+ bfcfkRKc5YEsJlmB2q21CacdinydJVWxtqZMwJbEMnS5cYYkzBv5N9CRRGXbBNs2kGnILf95gl
+ 3XzN7RKpjLAoEcTAANM1ffMk0RoRkzm5ULnwvEymTPZTwEAagECvPh/yXwIIDRFzNOJaIMmC2E
+ fR/tA6Nd304v4zGS9QKkuM6I6H3YrVlni+illMA8RLmCmxSlRxMz5JaXLY+itZi3mZR3KVu2EF
+ EuZr39L6AfuafeCIbS5cFBNUUfokHcyhgcCXLlW+ISKZDuIoiV3iilxgueV2NZOJczDt6KG8/M
+ CBQ51EJfezURtevafOCO+PsBf4ZQYg13U5dxNeUNsMCiu2ue4jmXalx4AUenwwC7dzCCibjPb/
+ 8QAJxEBAAICAAUEAgMBAAAAAAAAAQARITEQIEFRYTBxkbGB8KHR4cH/2gAIAQIBAT8Q5RNJ26z
+ SIfK/OoJ1P38RIwj+f8mWfjn6iV6LtGIbGBAgQOGumKyq8MpRTzJ0X2lQFQIEC4HLXKyO7Pp78
+ lzov5gBFtcsIZgqLUByXAaLGPg21/XBCHWE9BOsDL2IEwiPLLlPYz7F8DUWLOoly5c8xB+jHbY
+ TAvtMqbirt2w+HNVB1A6qF/MTT+Yu3ebS34n9ahuWRG7MuDLjFIHWUfx95jMhuFQF/T3i9q+b3
+ Egy6jDF74zCAJn9FMTIuMXcyzLXuIh2pf4gsPuPRC5dYji0NFsCscKOu1f89KnTayog742XZLE
+ umZ4pfRFNSlHXHJY+cRyMTG3NVmKVPOgCbLdEMvHCdfbgIsw68RFTDINr0KOa9fqXUO8LHt1N8
+ aA09Ht/k1Ge0yCi228r09DcACjjgY8ttkW2+Wmad5+mLC0tmNcvaX59MjLXwf3GioInZ9Z8zH4
+ H715f/8QAJxABAAEDAwQDAQADAQAAAAAAAREAITFBUWFxgZGhECCxwTDh8PH/2gAIAQEAAT8Q+
+ igVQC6tHWFGSLwL/wDM1JWcuSbMUrXRDeXBcIBK41SZhUQNuh6TRVmtA+4B80JQpEkf8ML5T4i
+ /pg5pfnQvaKenR5ay/Dlqdpu3oYgDMybzg9ysagohcqu7UgIVgWXZLnf7KsZWS9jT0Gu1NVM/N
+ Al/6PhyXq/vQpFFilnPzFY+3tk2TCWw2aOOfRsZl579xN4+VeLLdddnRei+pW4FdahygXd3VqQ
+ mO9G/DkP7U8cOlEYpDCw80UPmDVlSMAuI0+QQBaOnA6mj2mjuh1Xt3GO0tNRf1Jf5QwW0zHMh/
+ uOuKMs50bOKJBUWYTBILxcKWa2qEE5an+ehZFeqMoFEia/Hw1o+9kFNmyxNA+XhPDDpUnmDwk3
+ 2eKbBzd5g7BQrsJywEscqh3mjuqREA4BxhpLoLzLFXNOQksncgUDOly/7f55UUMIiVT5WaSjzf
+ fCT1XAgN5oflSToSRege6iVoI9LNrm1JRsFCpgaKvSgtmU1x009293IBwQao5huUzZvgUNYKM7
+ AJ8yeKWciqZNblk90unOYFtp2I1U7BmH5Qj3ku9MfYmCVXiHspxlllxTQXAfHoBR5VmAkAc3A9
+ naoo5sW6y051kdwn9PP4OVACxpdDhouW+yLMYfzDHeneSrmIrjoE1SBQjkF38rRuscQJ+UW+b2
+ yID9qZxVqDIANjCTGCXX4GO4jUad2PwWdcPmIu0Lj0MtMgLgD2OfVI5l8hDp/tNNlxlDFrfHal
+ 5Elan3lk8tqAEnX3avkpLTRMCB0sD6T6O3+fEeHDWXZ5Aef/UUZPMnPmw9Ky0brSc3t6oAILHw
+ aTCfLYe2hQrB18iUaW1X9SXNCJOsUlNB0U6NVAftLkdIYwr1hRCJ6ERZ2ZP8ACk1LQmW5zU6qQ
+ zewdYF7lIRUSgctNEppKUqaa3nSJsJfXOmjwtRkROUayMW3yND4/EMu4eqBDsEvQvtRMSEiMj9
+ l5JumiAarR1M8LZcaABe+Kcw1gMDTgV1Ab0lPCkLPQ/0q9Pg2E8NkWLURjU6QiXHsNyg7QpDTY
+ GAmbMgxmtprml2I3VMMzBe8QS06N0bPaj+6zXnHujJBSJI/QSTNTFt8AHUaCNRmwLvT5lYn7oL
+ mcFpoDuW4JdKLUUAgAwHyOnZtE6ugWphzZmX6E5Zy7MRA06DRZRkD7A7NBY06QsEisFrtLUDQD
+ 3SdMvCreVZpwuaveRt2KPrvmvOPdDGZmSReiZexrTbRPSoyrzNGGC6gy1NRZDqdAH+r1NeypCs
+ r+GgB9bZOSDsBD2xUmNbuEG4xr9XogwCN4cHL+2otZKzFyZVYCbv7U5SCRBvDcpnc38Ayult0o
+ 7Ggb9TVfhg/xom2zLhuRl6jpVogiHSxJ2iOlQEARHQJz0Xq7X9hQAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf
new file mode 100644
index 0000000000..720a5a009c
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:Binary PNG
+NOTE:This v3.0 card has a PNG photo as binary data and binary valuetype.
+PHOTO:iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAq0klEQVR42u2dB1RU57r3X
+ Xd965R859yP9d17Ts45SRTpvTOFOgiKDcUWY4kSe4kGu7EgNooggp0+gIoGFRRFLMBQREGRURB
+ rDDG5OelO2knul3vO8z3vu/ee2XtmDwxNR2Wv9V8qIs7M7/8+7X33zIAB/Vf/9bxfQwseWYTlf
+ RgVmvtQOSTjzvWQ9Lu/BCdebg6KOT+o/9V5ga/wIx9FDjv0qAThA8KHIQda/1ux7vQ/gt8rHN3
+ /6rzA1/Cj7VEIvx3hQ1g+wlc+hJDNFyB4QU5L8Jx0C/I9gfvuWAZurd4duKmivP8Ve3HAK1DtC
+ B8I/KEIf8g+NSjeOwwIXk3gB6feGB20q/lhQFrLPwPXnvkZv+Yh9rO8t95QeMU2Kfpf1ecDvAW
+ qBAUC+Duv4KrPJvA1iqQrbwWnNbcH7b8DgXvbIGBrDfjHN9TJk27WyRJufC1NvAm+8Wrw2d4MC
+ B+8NjeB56br4LHxGritawTXtVfBZfUVcF5ZD07LL6scoutU9ktrU+3erYm1XVQdabNA5dFP4tm
+ teg2FX4jwDyP8Agz7uPIpfBTC/z744F3g4Pvvvg1+qa0gT2kBWdItIPAlxuCvR/jvN4DLmqsI/
+ wrCrwfH6MuA8AHhA8IHhA/W86pg8JxKjeWsCtXgqEvRVjMu9BviaeR6Ap6B366Dn9EKITFlEJJ
+ UD4qMe8CHH8CHn4zwdyD8BDX4isB358NfhfBXIPxll8FhaR3Cr0X4NQi/msK3mlMJCB8QPiB8s
+ H77PNhMO9duO/Vsqt1bZ/rN0AfwlRz84RT+Rwgfiz6u4s++DyGZCD8d4R/QwfcXgx/XDD7bEP4
+ WIXw3Mfjv1YH9Ej58FcLH1T+rkoE/8yIHHxA+IHywn3waHCeWfIq/T0JzBPbT6034ZPUbwH+A8
+ O8j/HsI/y4E7kP4exj4fkbgexP4sQg/BuFvYOC7GoO/GOEvRPjzdfAtEf5gDv50Ar+MGoAYwQq
+ /ZvlOhVZolP+HOo6/j0JZ9BPtwhWW3lopgM8WfbTd48FXaOHfQfiY99Mw9O/C1b/TBPjrGrRFn
+ z58Oz78uQh/diUDFeET0NbTyxnomAb40DvQ9yglqr/jMHa5j9pv4T5yX6z/6vJvxeAPZXv9ITk
+ IP4uBHywKnyn6JFzFz8L3MgKfVPyOyxF+tA6+rTH4Mxj41m+X49cumQpfIKuZF+6SqNBPnL3cI
+ tItULFuo9M1koXFYBJ8XtEnBl+03Yu5LgrfiYPPVfwI30YPPgn9XPgnq74LK9+oMIX88FIbwXV
+ MlgUqFqVBgdfMYzCs4KEOfqEOfpgR+Np2rzP4/HZvLRZ9q6+Kw+e1e6Ta7ylkk4wwrexnpwkn5
+ 75U8F0ic6OcI3PbnSOVwCgXQg+2CuGz7V5Y3kMINQLfoN3roNc3aPe0vX6dEP7cpwefL8eJxU8
+ 8Rh4a8UKDdx6X5+E0Ll/lND4fnMYXaCXfWAPCXv8jHfzcB7TdU2QawjfW7onBdzOp11c9dfD6Q
+ hPUvpBdA4KOdZpwCBwnHGY08QiV+5xT0HGv/6B7vX6sib0+V/ET+LMqnrkBiGynnP2n91Dlkhc
+ CPMK2RNBqAtthUiHqKKtjVIo9twRFn1ivb9juYa+/y7Re32N9A/iQwk900GNY7ZuN0Iw4Y/j8u
+ Y4GCD0KoWso7DePgf2bH+DErEgrz+hL4r2+UgjfWLsnJe2eHnyu3fPY2Ai+GAFk+Ksr6fVXMvA
+ JeLLa6WDHnICz0HWqpBr8zqV/OUwqefe5g4+rXMmHbjf5OI5MT6BOMppyEsJy7nfe7pnS65Oij
+ 4XvvglXfuw1kOKf5dj6ua1h2j2HZXVgvUD1XEDXwp+tE46aC58X8BYIXmX/JlnlLHSEbTulGFW
+ CYY3oFPhsqDeAb6zdC9hrvNf3RvieuPrdtzaB6xZc/firDItAv1gCn2n3HBH+4DmVzx10KjKGZ
+ mU39cx1M4d/zBJXvFoHnoN+Cvvd06xKwXnRRWG7x8IP7ajXTxXC98HQ7xHfDG5xN8BlWxM4b70
+ OnvEIHk3hhwbwfL9Ru7VrZQ7wuwl98FydrDB1OUw+/ZlZ1gW46j0w1Gu04KfywE8vxRHqGdRZs
+ J11nq54g14/t/NeX4pFn0/STfBIbAbXhBvgHNcEjtuvg8M2zPk7miEg/ib4bcN6YON1bcVP2jt
+ zg24AvBPoVPNZ4fPBTahvzcoEuOoR/nENye2CFc9Ct367DDdRyObJOQje3ybS7vF6/XTxXt975
+ 01wS1aDC4J2TrwBDgnXwT7uGthuawT3JDX4J9yCgO1qkMY2aTd4yJTvqbd2fQSdyHohI2qCKWe
+ /MQsTEPh2byF8kudx1RuAn0HAl+Mc/Tz4Yqgm8Dve2r0j2Nr1TbkFrjvV4Jx8Axx3NIFD4nWwS
+ 7gGNnGNYLUd+/uduPJ33MKjX7j6tzbTvM+1e6S/Nyvoc7oLvQasF7FaTFRLW1eXyKL7ZgD/hIY
+ WeHTVs6FeC/48gr+Ae+MXcepWI2z3Oun1Zdjru+9SgyOCd0hqArsd18EGwVvFN8DguKswaDuu8
+ pQbEJDcAgGJt8AvTg1eOPTh9/qkx7fDx2I3veyFgG6DU0ubJXWM8PfkcTwzEyB8C4TfLoR/lg3
+ 1zIon4MkJGrJ1SsK6Qa+fI97r+6a2gCPCtUtuApsdCD2xEQYnIPS4KzBwez0M3HYZnHc2QUBKK
+ 7P6E26CdPMNba/P39cX29p9VtC1wE2CXmsIHUfXtu9dBttoonr6c8jj8g7Pq3kW8NVa+KTIe/s
+ ss+pn6lY93TPHF4tswnS6tcvCd8NVb5OM0JMQemIDDGLBW26/DJbbsJ3bUgvOOxohAFtCsvr9c
+ fXLMfeTYY+jCVu73JYu2dcnhzvIn80JOgXeAXTbZfVgh52N3Yor+Pe12sfpPUyZ/NQMgEMdpRB
+ +mWDV0/1z8sDwRRk0r5K2cZ31+v57b4NDShMM3oEhPvEqhnp0eNxlsELoVlsx523BFyhWBY74t
+ YDU2xCwE2uEHS3Y9t0ETxL6jW3t8uFzhznZEz1YSGG7WopDKhK9zrHRqpvQ5/YS9KUdQ7fD1tZ
+ +1VUquxVXdQdNMN16hysn9f3qn3w8min42LCvD59d9ZZzEf6CKhi0qErX7nXQ69vjirdOwLYtH
+ ou37fgibMNZ/VasdjerwDYWhyAxleC0pRoCcCAUsAtnA8m4H5DYApItzZ0d49Y7yVvOO8xZipP
+ K07gnUYIbVCdxZ/IEPdxJjdBj6NV9At1+dQPYr2kAh7WN4ICzDn60wsf+T59huYP6EH6RB231a
+ LXPhf1yXsjHBzKbWfWDFiL8d3HFbrhquLWrB989+TrYxuHW7PYasNtWDXZbVGC/uQrsN+EINOY
+ SOGy4CM6bKnAecBMCCXy6+rE9xNbPFV+MLh7jFpzkpfAnFlP4zpFF4DL2A3CNOAqO+HVrUsM8L
+ ejLOOhXhNBX60O/Bg7rroHj+uv42l6nj4NvArcxRzV9XPQxrR4t+DDn61Y+G/LnM6t+0BIVDIy
+ uBoeYhg6PccvTWnCQg9+3FadcW6rAMbYCnDZdAqeNF8FpwwVwXn8eXNaVgxxNEpjaRkO/XxKz+
+ r1jmrpxjLuMB/+UDv644wz8MUfBbXQhuI86DB4jDoHzhBNg/c7FrkNf3EPoa4xDd8BBlwM+d4d
+ NTWAl0upKQ9LP9EXoTyV532bqadrqkWrfAD4J+YtVMGhpNQxchlpZAw6bG420e0yv7xGPmzWbK
+ 3CFXwLnmIvgsvECuKwvx8LxHLitLcO+/izI4uohcDcWiSm48pNugxzhy+Ju0tDf1WPcdlP04Z/
+ kwT8mgO85PB+8huWB99BccJ5UjPVEZR9AFw/tWugI3HGjELo9Drvs8cCL/ZYbYLXI0ACkHpCGZ
+ AzvzdWvYEI/r+gzAn/gewh+eQ0MXIXhfP5ZkEed7PAYtxuGdreN58FtQzm4T00H9yFbdBqeAL4
+ rSiCQ5P1UJvST1S+Lv4X7/Ne6dIxbB78Ut6ZZ+BNY+JF8+EdY+AXgFc7A9wnLAd8hWaTSBruoC
+ 92CTvN5Z9DpKudBj9FBt9twDazJ80XodrjXYYcbYLbbbxjtXJwmFf/am1U/7fdp6OflfVrwzWb
+ DPg++3YJzIBmVAwp5CoTgmT9jx7hlSc0I8hxu3JSBx4TdQvisfGcqGfi4+v2w8JNj5S/ZpqYtn
+ 6nHuPHgJVPxa+GX8OAXMfAjWPgj+fCVLPxskIRkgjQ4HVdWJjiTKKjt0WuF7VpvQN9kuNIJdPK
+ 8rPFEsy2ef7DFjTCbhGYD8INnM7KaUwE+I/OaeqPwi+VWPw392rzPVPu04FuEYR/hW0arwOPNQ
+ ggM3gNBAWkwaWwGKPMbjB7jluJUz3tNKXjNUmqBLwwfC3EjfWHfDF9YM3sSeE06IIAvw8LPDU/
+ 2dtjrR13ktXtl2nbPHuGToo+Bf4IH/6hx+KEsfEUGyIIOgizwAMj994ELmqDXoccaQudWOoHOg
+ bbGbW+bHWpsj68zz5cDP4cBbzUXvwe52M65CPJhmd1PBbjDZ0Hm/Ezhx7V8vNBPWj1S8C3Flgu
+ 3eH0jlOA/ZD81wNYt5+Drr7+DX375xegxbmlsNUhXFIPHqB0U/lhfb3jlf/0b+L72e8if9yq0K
+ 18D76lZCB/zPlb9soQW8N2qFm/39Hv9t4W9vn67x6/43UYT+IfBcwSBn09DPYHvg/B9Eb4E4Us
+ 5+AH7wM9vL/jLd4MbRkRBu9Zd6JsZ2YlA51a6TaLOAIPxuVvh/ogVHnzhVjuBzoG3nl+JrwtqY
+ SV4Tvrgxx7k/iIlWf02gtXPhn4u7y8h8C+AdGQ2+IUdhGERWXD69C0KntO4zLuix7hlMVXgF11
+ E4YeFzQGNRgMeHh6A/zWEuv0Ovj71GkiWlYGc5P1EjBhxLTjxazCAb9Vhr3+GB79YFL77SFL0F
+ YBnOCn6lOAdlsvCz2LgY+hn4O9n4e+BAFkaBEpSwX366e5D39I5dLLSrXG30xp3QrVhnigBO4C
+ 19Sz0CgrdeoEOvO0i1OJKfK0qwS88c2+3DneQY1yC1c8Vflzox7w/+N0KkIzOAXl4BgwdmwOtb
+ Z8J4BOtKX4kepJXvv4S+C85Rg0wUj5KC59I5vhbuFXojtV+MwMfV78PDn30j3ET+IM5+DNN6PW
+ 5ij+CbfdGshU/B38oC3+ICHx/Ifwg3xQI9tkJrvMudA06C9wU6HSlp9wEq103Bfl98Pv1YI1zF
+ v5q1wdvtwTnKEsrwT3q1K/dqfyVhrmft/px0GP5bhV44RFv2YgsCBunFIVPlFP/megxbtn7mKM
+ WHwWP8DiYMHQWBL7xCoX/x9//G9Tu+TOsSE7Qwpdsv0XbPv4xbn67Z6Xf7nXY6xtv93xCc4Twg
+ /Th74ZAKYG/i8IP9k6GIEkKOOHZQx30JuPQt5sAHQ+5aKHj8GtwGiq5WZDf+eApdBHw9u9V4ni
+ cqAqCQvfndOVcnyU5yKmr/PVW/3xmyuc67ThIsdoPGZcLrXfE4RM1PHoieoxbsrYCZAsKwSsSX
+ 9DQVXBu/utwa+drcCvXETalxfBWvhqcSK7Vb/fYk702BDivwrcnOZ/f7on2+keMw2crflL0MfD
+ 3CeAHIvwgFr7CMwlCPBLBPzRdr3LXgx5nCN3ayEq3XFIDgzfhZtjuW2CJx+Yt96ISm8TBE+jGw
+ C/DwRrR8irwmV70r64Uf7HkaBcd+Yqtfiz8bOafBwkWfcQA2xNzsOD72qgBrqIBxI5x+6yvBcm
+ 8I+Az6xAWe7jiFp2AiNgiWvHLsOiTYL/vzvb7+u2eLeZ50r+Tin3k9CKIxnHx/pxrsD+7kepAV
+ gMk7qmHDYnV8M7qCzDx3bPgN6nISLun6/VF4fsx8AMIfAmBn8LA92LgD3FPwJolHnzePmkIPd4
+ 06NqVjtAHv1dDV/ugXc0waH8LleWmBtH8TqCLgkfojiuq8HwEamUVHpGrgjBpUrqp4V/DFH/cy
+ LdcW/lzq5+EfmKArTvPQXp6Opw6dcq4AT58InKMG+/g2XgFfOYcAe9Zh8E/tlbb7slw0ueJq4k
+ c7uD3+qTds5mFE0MM52T1rt9RAy13Pjf6/xpT/fVPIK+ohZpj8rwSYa/PtntyzPsEPi36KPw0E
+ fg7tPBDXeNgiGcirv5rDPSErkHnVvqgfWiAZbW0sCN5fuDBFhiY3grWy2uMhnlR8Ct14J1X4+m
+ hNVXgH5nziynhP0oX/pnij/T9dIuXzf2OUafBd2weTF1yCn799VcKn5igublZ9AU/c+Nzo3fte
+ s06CtLoM3R3j/T55DZueo5fb2vXFqt9hymldPUuxd3Bjx5/02XwHelK48c0aqzA8fPYiQW01+f
+ aPS18UvR572TgezLwh7Dww5y3wVCnrSCPyGXyeSfQLfWg05V+oIUCt4qu0a72N4gB9qg7zO+O+
+ uBZ6Bx4l7VVOFqvAt/ZJ2CYx/bozqr/EvHwr6v8PScWUgO03f+SGuDJkyeQm5tLTfDZZ4a1wMG
+ 6+xCULH7XrtcShB9H7t9v4r1DlxC+3awLdGgz4d0yqG/6pFfBG9PXX38PDVcfQcaBOlizvASip
+ uTrwU/UwXfZTuEPc9wC4Q6bsQ6o7zL0gemojFZ4A8UP8wPTsPjbdKXD/O6kD36NELzruiq8NV4
+ F7lhzDZUkPuzoVi4LciePrvfnhX9S/GHfbzenDHwiC2DTrloKn1NbWxs1QFFRkcGLmVJzG9aVt
+ RjAl+Ikyw+Pdwtu3Izmbe1iq+c4+RTI8fHknrj9VMB3praWT6Dq/G3I3lMFce+XQPSMPIiU7qD
+ whyH8cPtY8BuT3Sl0mt9TmylwqsxWeD2rFQbuvSlY7dYb603K71rwCN3lfSF4tw0IfyMqRgWKY
+ fthmOf2QUYMUBilX/0Lwj8Wfy7TTlADtD34UmAAonPnmHqgoaFB+4LVPPw7lDxso0pUtcCbmc0
+ wKVMNs4vaYO7ZR6DY3Wpw164d5n37qPOY64/D4q018NEn35oF/M70zVcauF57H3UPfHFFG1vpB
+ PjAlGaa1wn017Nvw+s5t+G13NtY7V/Trfalpud3Lfh1RCoD8B6bUHiiKjAyG4b6JhYby/8l9rz
+ hD7Plqwv/luhM7/GHYEp0qQF8op9++skgFaRU39YagCjz5l3YXv8All74EN46+RGE45RQcNcut
+ jmO2L6FziuDyzf+67kAL6aN1Z8YQCer/A0KHA2Aq5+E+YG7bsBrSoSf1wZ/y28D27W1gvzuYGJ
+ +p+DXGwfvuZmR34xCCA1M+Yex/A9c/meGP+WC3t92bjk1wKGSVlEDED18+FCQCvJu6gxwuO0Op
+ Fy7D2urHsI7pY9gwvF2Kh+8j88Bq16HqWfAd0YppB1ueW7Bc2r+7DsD6GSVcyt9EFnpuKCsMMT
+ /raAN/nqoDV7Lae12fifQ3TYaAY8nrLyI8NCNfM4HoAjdC8N8EgL07+OPJHfzMgbQy/+zmerf4
+ Z1SagCx8G8sFXDwix+0wUH1Pdhc9wAWl38IE08w8BV4x4/TjDJQ4H5C6pFW+OrbH557+JykR+8
+ y0HMZ6GSl/41d6VYx9dpQ/9cjd+AvhXfwEOzVHuV3900i4BG61zYV3kRLVA3yecdBMXQfhMmSi
+ vULwFhaAOKBT/70T5f/8T/DyV/w9GMdwuengqy8PK0B8lrvQlLDfVhZ8RBmnP4Ixh19BJ6YAxW
+ LL8Gxi49eGOh8Lal+LIDOrfS/Hr6DO4gqbah/PfMWNYDDmuoe5Xdx8NXgHVcNPkTxaIAFyDD8A
+ IQo0r7SLwBVhv0/M/2j7R8OfzwmH4M5G853agB+KiDwi+7fgX037kFMzQOYf+4RjFLeBx/cRTx
+ 26aMXEjyn0x9+I4DOrfSBe24I8vvAPXgcfmcjE+ZXdT+/i4KPZ+SbgMLBlxynrUEjDkLw8AOgl
+ /8LgW8AQQE4n9n29cbpX0xanUkG4FIBMUBOy11IuHIfll38EMYV3IO38J6+r7798YWGT/QQnyM
+ H/S9H78Crx+7Cqx/cxeNddYLCznZbfa/kdz54yfvlIFldpgUv2YFKqgb/aYcgaFQGBI1Mh1C/5
+ GjuDZw8yPv20A6APwDi9f/k0AcxwIEjzSYbgKSC+ItqSLt+D9apsPA79RCiD9584cHz5VX6kEJ
+ /tegu/Pn4XawFWno/v+uveIQuXYV7H28VaMFLk1E78b6KSXkQODoTlQEhwakqxgATDkURA+DpH
+ /EOAAtAct6vqwYgKm68B9suM23f/JcMPtH0usfw5xP34M8n78Gfiu/hzS71fZbffdjV7ovQZdG
+ nwG9CHsjWnaPgZSmoJBUEjM2GgDFZEIgHdxRhe//OGSBW1wKKdwDkjH93DEC0sPAmjE+th9ra2
+ pfOAHEtn8OfSu7Bf566jy2huk/zuy+72iW42mXzj+PuZz7IlxSDbFcNyFNrwG/dWfDHrfuAyBx
+ qhMCRGb9yLWCJeAuomwASA3hhndAdA5BUcPjwYVoUfvzxxy+VAao+08B/nr6Pq/8uOMbUiOf3D
+ V3L79rVrpffJWyYl+Jql087AvLJh0A+twhvvkH4u1ELisB/Ap7bHJ9LjYC7g8AZQMUZQLsHoNc
+ Ckhs93KafgB1ZjV02ANHjx4+pAUh7+P333780Bmj66ntqgMG7r/Vafteudr38TsP8LlQcHrSZe
+ gRkUw7jPsoh8NtTg7utlTQiUE3MY42AR/aD06KJAdoFBnhbzwCLGQM4v1Nichsoprq6OmoC0h2
+ 8TFHgb4daejW/09VOwOMNNdIdKhY8E+bJapfjfRYyjADUBCj/fWiANWeoGeRvFoAfEWsERciee
+ GIAYAzAHgLhG2CuzgD2C8pg1PzibhuAiIyIiQkePHjw0hjABUN/b+Z3GuaJVpTiucpzWvA0zON
+ ql+G0TzatUCv/2HJdRGCjgnxyATVD8LD9KmoA7Taw/hSQM8DyajypUkFHwU80P3XbAF988cVLl
+ wpmZlzv1fzOrXjp/JMgW3pKC95/L/66A8P/9KOoQkY8I3ARgYgzQtDwg20mR4A3cKfKa9IRqLj
+ 8UY+iQGNj40uRCkjBW1FRAaPiyw3D/Nau5Xcpl99T2VCfVAXSmR/godqTFDwJ8wH78evLT4P0b
+ TQA0fSjOjMYMQIWgk86NsAcoQGcZ5dAUkZDjwzATwXGjpE9ryKHY8kmGNf1EI2KL+t6fhcUdrz
+ 8zoZ6+cZykEZ9QE1AwAccQKWiKWYcY4QG4CQwgsAMxAC5fAN0XAQOXFMLVisqYPTckz02AEkFJ
+ A0QdXSi+HkQSWW3b9/Wno3UV8SO8i7nd33wNMzzQr1sUTEaoIiaIPBgDZX/Bhz6zDymM4EJRiD
+ FoMAAujnAeeEcgDXAGxsvg+fkQnj8X5oem0CtVnd6oticRQpZEuLFoBNjV1ZW0k2xOfnNXcvv8
+ ZUGhR0N82yo90+sAOk7RVoFZdRC4B6VNiIw0jeCzgy69MAYAQ1wCIxNApdn3dYOggauJgbAO2K
+ jz8PBwzd6bAD+iWL+MTJzD/FkoskP8WLQ+c9x7qHmLuV3KZ64EoBn8zsX6mXRmOdnHWeEBgjOq
+ gW/laXaiCA0wgdCI8wwNAIZBQOzF3CCdysYY4D2z3+Ao3WfMvf+r6qBN3An6/Ut9RC+sKRXDND
+ ZiWJzCfGkVuHqFn2RYlYfOl8RB652mN9lvPwu33IRJJhi/VJUOvAIPfAAG+ZxoCPB490ShC9hT
+ RCUIowIRo1gJD3gbiBjAOFuIGMA7kncatdAeEoTvLEeDbAZz+1jW9PY/GmvmED/GJm5iOR17nS
+ TGHRyEpqMuTt7fl3J79J3T+HdUsXgj+1cAAudKCi9loZ62fJSkMw5wZiACk0w74QgIgiNIGYGo
+ RHIdjAw5wHYA6HTS7X3A+g/mbMtX8KsI+Rt3Rpg9O6GXjEA/xjZs94w4lo3Lip1Fzpf+v27sfw
+ u33wBJPNLUGiA5AoITNeBD87EO6e2XaDRQTLnJGMCPSNwEYExwnETooLOABo8ESR6IKS3AJt6j
+ OxZbBh1lNdJVCLFKklV3UpxP/4s7N/1wXNhfg/WB4tPge+CEmqCgJ2VEITQSX6n2o8mWljMGIB
+ TB0YQmqFjI6AB8lXkQ530zwSSewKelgGe9oYRgW4sr/cUumDo1f6NoH83AM/mdxnekua7kBiAM
+ UHgrkoIzq4FBVEOhv5lpTQ1aNWpEU6YbATyeX5oAN44mDcMepoG6OsNo476dbL6yYSyN6DzVXn
+ /C+Pguf49vgJ8F51mDMAqMK2Kgifyw/6eSQ1MeujYCKakB6ER0ADKWMNZAFMI3nr05KmboDenh
+ Bx0sWKOQCeGI0OpvnoumVceiYNn83tAmgokS0sZA3BCA3Dw/eMu0ojApQaTjNDFOmGAc6Qymms
+ F9c8F1rV+9dQNwE0Je9IaGhvSkJ/b19D52nrpjiF4LOpIfg/CP0uWn8XcX8qKMYB05VkKP2DHJ
+ UFU4NKD0AzdNwKRnAyCXCJzFbpOQHg0vK71y6duAP6UkEQDU+sBDrp+BW9sQPM0FHWsiQVfqwV
+ PiroQlHT1OfB9F++GereUEWsEv83nMS1cMogKXTKCiXUCnhr6YYDL2BwL8nm+2kJQuy18Fi4/I
+ wPwW0Nj9QA/vIu1bc8KurYD+OlnbRunreixqBuedxlm7LmM90acofIl4hlBsvSMICJ0zQim1Ak
+ ntMJt4SfcBzu3i9UBKcfvPbMXkH+WkBsVc9V7bwxo+lqqD7+g/TsHnoT1oTl1cPcLDcxNqwefp
+ WcZGTGCLjWImKGnRmDNgAdE1YwBIpUlgjqAvUP4WRqAf4CEC+Vi0ElVby7Q+dpedVfbxhFNy6+
+ nUYHuD+xGA7x3VmcCfSMYNUMHRuhGnYAGKKcGwDogmtQB3EDIjn2PgHkp1575C8m9+URf9ep9p
+ XAM9Rz85Au3BX/nE12GBuDUC0boZp0QOCpzA2OAsTkezuP4dQCTBt7CGxnM4cUk+byvevW+0Nm
+ 7n1Hws4810pAvqA2+/xkNcI4xQUdGWHrGhPTQszohzCPOUnt/IM4DNI4iacDcX2xz1LqLrdQEY
+ n/XcPdL8Fl2jpExI3QUFXqpTpC9fex/BDeIYhpQatPAm0XMWBingu1//74fai/qQNk98FlerjO
+ BvhGiTTTCEp4RulEn4KHQx0IDjM2JpGlA0A2UQHnDp/3gelFvJdeBNzEAJ74RlokZoW/qBP9xy
+ iMGbxOD3YBG1w0wxeCuorZ+cL2kb3/4GbxXnGdVLjTC8nPGo0If1AnBij2BBgZwicxRCodCJ2D
+ qltp+eL2kkquPwXvleUZ8I6zohhG6XCec1o2b3yn6b9E3inIZm43dQB448qIAGQ1/+90/+gH2g
+ ubsbwTvVRfQABc6NYIvgibVel/UCXhfYKPRN4zEYlDNFIO6KFB+9ZN+gD097/DlD+C9+iJjAE5
+ 8I+iZIWB4OkiwWOuLOiFo6P55xg0wNjtKPwqs2netH2IPFXO0hTGAVhf0zKAzgl9kLgQF7+6TO
+ kE+/ehPnb5pNEYBDRcFSEfgPKUIMg/mQI3q8nMxiDG71f8Vrv41l1hdNGoEAjlg2EEI9k8F/4j
+ sPqkT8Hawo50bYGx2LIkCTEfAzAVWbVdqR7H9ZuiaNh5rBa+1l6gERuCZgYT7wND99BPXggJS6
+ Xm9zgvGrs0TyABoiG+SZacGcB2TacFEAdIRHKY3jngNTYXF09dDckKaYC6feTAbzVDXb4YOVr/
+ X+xWM1lbojMCawQcBy9/EO3UDd6PSqAECFXspXON1QrlhejChTvAfl3vD5E8OcRmTHY1zAdBNB
+ z8At/+YCu7/PhGG2EfBImqGVIEZMvrNYFj5ZzSB17pKnQl4RpDMOgEBQw9AYNBu1gCMCfzG53V
+ aJ3R1niDB3j9Ekqzo0odH4XSwXVcQFoKz1yZw++MEVuPBFRViPxPNsK7fDCIqqP0YvNZXMgbQq
+ gKGrzpHcz35jMXAIE46ExBgnRWMFC7mdFONEDA6q7XLnx7mOiZLgakAaCogI+JxBQL4rn8cBy5
+ /YOT8h0hQ2M+AhdPfFzVD9UtmBhL6g7bWogGqqIK21sDGotv06wEY4gOD9zIG0DOBfGK+0TqBb
+ wS/cUoK1pTBkhSPf4VIdyq69QGSWBCW6FLBEXBxWCEKn9FYcPrfRGMgyG46LJi2FpJeUjNM3ns
+ NvDZUweQ9jVB87VM6BiZfv3b9Y2oAqmBDI3gvKzeoE/SNQODTLqHDeQJrBCwAg0L2Xu32J4i6R
+ mRYYCrQ0K6AmCAyD1z/7+QO4TtSRYAD0SsREGg7FeZPW/PSmOFS6+eQX/cxrvYfDY+K51xF+Pt
+ 0JuBFAynerdtRwUhMQOoDUiiSM/0GgyWROoF8v0KWohjQkwu7gkg0AXCtoZPHBpPhO7wyGuypR
+ lEF2EyB+VPRDPG7DMxQW1P/Py96ZJg5+ygEhOxjxDOC/7B0CpmrE4RF4yUK158MiAKZLqGjeQJ
+ nBHL+XyFPKemVj5HHekBJ6gFqgrG5/3L+07Quw7f7/UgqW6oR4G/zFsybutrADAX5Bd9cqb/60
+ 4tmhk8+/Rb8Q/brDMAzgQ+OamnBuF5YMBIDkA0c0i1wRSJ5d4/O6gTS9wcF7f4u2G+XZa8YwC0
+ i3QJNoNaaIGRXd+BrOPhENr8brpXc+k00wypISUr7mW+G/BfIDIUfNNNPWCcK4BmBACX1Alcwc
+ kagIRzzPVcgUuH3E8Ad1Qk+mPsDyUTRb1fsgN683CIyLNEEGq0JnFZ2deVHI/gSffjWvwvXyuq
+ 3w2CUfHYSwk9FtfPNkJeX/1VtTZ3meTXD+CkFWgNQoQn8RmMxR8L8BhVjAhQBLMPhkFinQCaEH
+ Q+WWPj+qeoBfXGhCRRoAqAmiMgCx7/M6ErYp/kIoSuNwSca/NuhGpQH+V4E7yFmBqVS+fn58gs
+ /Pq3bvHqqa02PwT/0ACPWAH4k7+Mq94xRgedGFYb6UtoGCopEXqfgPyLTID0I6gTM+wHh6cw42
+ T/VY0BfXZgOoqgJSGEYlgYO/2e8STkfpeF+BsKPQugaEficDBxszAw52TkaczfDloQK8OMMwMo
+ Xb87wWXwGpFOOgN/QdNHagN8pkKmesTqBRA2yhczsJaTFDujrizFBJjWBk1+cKfC5kB/J/QwE7
+ 4FSi8CnsvxNWKqx/9+YGbLN0AyffPoE/MIOshKaQJAS9GoDvhFkU8XrBGICEkUChmdwRaJ6wNO
+ 63EYfjHKNICbIBidJrCnwiZT6PwfhR7Nhnw+fVWinPaxxM2R/d67s/D+etRm2JFbyDMAqtAMj6
+ JnAb1Q2eG1UCeoEzgzkgEcA/hu2SNSgASwHPM2LmADrAiApwdHhvc7gc/neQv/nIHQLVCyqnQc
+ fBv0mVIOyMPXxmJsZ6OrH8M5IzwQmRAN//Df8OsGL0wbmnUH1RsmRA57F5T7qQJTb6HTAqSE4W
+ i/sDD5RVEc/D+FHInwlgm8fRE0wRNmdx2XUDFlPzwwLV5wGORZ7cq0J0rsUDcg5fs+Yap5UtMf
+ 3G5WlP0pOHfAsL/dR+yPdR+z7hRjByX0NdNLqdSlPDfzNkB5XtM/CDJW1HyL8DJ7SuxQNpFMKw
+ XNTDatq8MI2Tz6xwKBIRBOoBpjD5TFyr4dbeJoGIwI4ea+DTlo9xbN6nE/DDORt9cNwDi8Pz2D
+ EM4Ep0UA+Nhc8Y2uoyLavbNIhY1PEdpTFAHO5PEbssXAPTb7rPnIfuAYngM0fRoORVk9pDo+3r
+ 8ywctN5kIVnIvxMERNkCE0gEg1855WAdOpR8MN/10GnoEF5DDDHy1WyMRvNAO7DdoHdX6eBkVb
+ P0pwec2+Z4XT5HZANz0JlMjJqBL4J0rvaKZgvfO1ZAs/VI92D4v7hOXw3OLksE+vzleb62Ltrh
+ rZ7X4BsRBYjE03QWTQQMYEGZd7wBUZwX1XsGZr0L/fgeLB99S39Pt/S3B+/MTNkZWb9UHa2/Gf
+ ODCTvh04oQPjZeibo1WigQT0/8LnLyX7RIBfXZWqvYTvB2W05WP17BNfnK5+n52HMDPv2Z/045
+ u38f0lHZgMV3wS9Fw2eT/iCO5DtFwa4uq9QeQVu+aeT7XywfCWc9PmWz+Nz4cywe0/64xFTEPq
+ oHISfA0IT9Fo0UKMsBrxIl4vT4ng3j+XN9m9Mj3len4NkdK6FZFSuWkLgc+KboHeigRKjwYsF/
+ 0W4JBFKCzSAGgVUfBP0JBoIh0fR/a+0GV6+EUoPNIAaBVoDUBPoGWFkjhoNoOlGNFCjATz6X2l
+ zhD8mT4HS+BL4fAmMQA1QgkrtRjRIRQP0h3yzhD82Lxrhg05K8BU3QTRGA48u1gZqNED/qjfHy
+ ycy38JnbH6J79h8QBOA0AR5fBOoJCQ9kOJwdG67ibWBBg0Q1f8qmy38gkiUBk0APtQAnAQmaMd
+ oEKUtDkl90HFtAGxtEIvqD/fmeHmPO2ThM66gBAVoAFb5IDQCAZ8XxasPLHy54lC/LtBFAw0qF
+ tUP3mzhjz8UjdKgCcCHim8CaoQSNEGkXnHogVJ3UBuQlBCL0aAfvNmCn3BYgVJ7jz8MaABG4xi
+ x0SAKDWBhWBzmR2I00BipDUhd0J/jzfnymnhE4TXhiAoFaABG44kRDqWiIlEWRuoDC4wGqSK1A
+ UkNpGOw7H91zRn8pEJLr4mFqWgAFYr8Go0mUJhUHI6jxWE7rzZQoxGi0QT9rdwLXh8oMCWofKg
+ KUtkuoT+v91/P3/X/Afw1kptmVhryAAAAAElFTkSuQmCC
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf
new file mode 100644
index 0000000000..c856944ae6
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI JPEG, version 3.0
+NOTE:This v3.0 card has a JPEG photo as a URI and binary valuetype.
+PHOTO:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQ
+ YHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/
+ 2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKC
+ goKCgoKCgoKCgoKCj/wgARCACAAIADAREAAhEBAxEB/8QAGwAAAQUBAQAAAAAAAAAAAAAAAAIE
+ BQYHAwH/xAAZAQEAAwEBAAAAAAAAAAAAAAAAAgMEAQX/2gAMAwEAAhADEAAAAdUAAAr+rHE2wk
+ 67TnXtdstTYAAAAAR841jd50PpytpSbS627Ll2TqErRk0XPBqdQkAAFT10Uj0MMrzO2l1v2TKc
+ kOgAOI90fyt9gzXgFU10Z56mCdrobOJ71pO1t2ZwrsfXFCuc6caV5HqTdFrKcch9rzJjNoTdhg
+ vO9SR9XzWMp8o3c42qJCeDoivkePZda9GueL6dF35oLRTo3kb6P6vlRkpWbNqn817aXIezsPZL
+ jf5/l3m80265XO3jzvQpu7Pacl9nw6IPXjm8+x5CQAAMrKM09jw+XL/eS952Qo2V/XRd8Oi7YN
+ kYSIAAANHMw9nxVcCXHlnJoVOu/wDn32TLqQMR+BxGR4OXM39bxefZcO2N+2JSk4d1fydwA1GQ
+ s7D4BlOqi7/Pg7+oS8F8lZM992w3uoyAAAABLlM3ZKhrznXnExTdfvP2ZRthZMl9uqjJnoAAHH
+ sc19PFBWyddzhoHnb7LmupWqvH79Fip4/hzUs8VD09ADM/Qrr2nKtGaplqPmbvQKPojRLLNnyR
+ 78LGIkUNbY5J6dKepGENX8zU/h0ACPlx3HvUAAibI5h6NLCzkvTPTvPtfw6AAAAABG2RgroJde
+ R7Zc8wAP/EACYQAAEEAgEEAgMBAQAAAAAAAAQAAQIDBRETEBIgIQYUFTAxIiP/2gAIAQEAAQUC
+ 8SsjGtQPkvy9LJswNurIi2Jvf6SzKRY3nzIhJ1J1J079KSLaELm5shyKiIeOUyrUqqMr7ZOpOp
+ J5eNNk6Z4zIxKbrmj+CLNtRbshKaZTffTfXS0m3F8WaxdKMvYYec5W2Y8CwtWelXPkItkn6xnp
+ My0tJ39iXSGvqm1lfyO/dmOG+0SO/ExdNnJHudCYfa/HCasxAk1bgIqzBlRTBkjwU/Un9vpYG/
+ cMjZyn/HI+ia+9oUOVIcaodvEyHILan/0+k/pY63iye+6Xx2ftT/4W+Vt9Vaf+6Uv8xvUJ6vFh
+ syjHOMWpM0o0O8JdJ2QrX2ZTXBdYoD1VN/XZ92XS27+3REeDLdb6+SLEzdcZFirGqh1MnxjEDz
+ qFZ9da499nyOjtvFs5h/0O21n7dzfriKuU88ZihWOJAkL8ig6oPHubyunx15CF0Ghuc39dMGPx
+ jrP4z7EZs8XxYMjKMfkLxjNkQUSa9/3w+Q38pfI0Y1M7vjhvtks2m6ZrCsSsTdLG5UgWgmNVca
+ q5RaTOLFlsitRJr3kjIiDSd5SQ9dpdoQsBKPAsMcuNUOOrwyBVI1WrDL5aZwcdcW4YlQlf6iXK
+ kmxEO64QgiImHHp8f//EACgRAAEDAwMEAgIDAAAAAAAAAAEAAhEDEiEEEDETICIwQVEyUhRhcf
+ /aAAgBAwEBPwHtqaocMQrxyZX8piGpYUKrHcH1Fwbyq+ou8W9we5vBTNT+ya4OyO6pVtwFUqbj
+ ua4tyFSrB/8AvZWqW4CLlysoN2AVh7JQdBkKlUvGznWiU90lUtODlyAzCbQYGElBAIBAJ9G7hH
+ ClXIZgpj7DKBnK1DvhUGXOlcJwIdKuqVPGcKnpwPyXTb9LptViAWupx5hTKp+QKGBG2mdLIVYy
+ 4rS/incJtLqmTwmsDeO7UNupOCoCcpjbRGwErSp60jsEeoNtwNmiSqI+VT8cKPKCmUDTfc3j1k
+ Q1UxCGNniH+qs61hKFEgXOXO7clV2/KaZE+rUu+EM70RLk5twhUP1Kt9DjaJXSe83OVSKbYCAn
+ aiy1uzhBuCa64SESo+u9xXSLzLlVIaLGqjSnJ7OMhTd3uMbH+kyiJk+wuhZcdgJQEesz8Lp/aI
+ JwgwDt/8QAJxEAAgECBQMFAQEAAAAAAAAAAQIAAxEEEiAhMRATMBQiQUJRMmH/2gAIAQIBAT8B
+ 008MTu0NC/AtPRvPR1I2HqL8eJKbPxKWHCbnU1NX5EqYIfSOjIbNqoYbPu3EsFFh4WUOLNK+HN
+ PccaMNQz+48dOeo6X0kA7GV6Pab/OlNM7ZYqhRYStiQmwnxKuKZagVf20A0BraaqCouUwixsZg
+ k5aYip21nMpupW07VND3Lb/sq4z4Sd+p+wYmoPmDFn5E9ShlKsrbDReYtfdmmHWyCY07gdM4oj
+ /Y9Rn/AK1UWyuD1vLyuMyRRYTGjg+K8v0EYXFozewkR8R3Eytz4/iDrTOal4qYuwiOGbKNDGwv
+ MG+2WOuViPFhF2LaMQ2VJTfI15WpB/cIaJEtbWq5jaU3X+F0Yl8zW/OlCrb2NCLR2sd+I1MEXX
+ Xhkst+tarkGinW+rSouZYCRxqpU+4eruEFzHcubnSrFeNVOmah2gC0lt0qVlSO5c3PjXJ9p6k8
+ ILRair7m3MfEM3Gn/8QANRAAAQMBBAgEBQMFAAAAAAAAAQACAxESIUFRBBAgIjEyYXETIzBSQm
+ KBscEzQ5FygqHR4f/aAAgBAQAGPwLZIi3jmrcjnOdg3gAuWQnsrxIPorpgD81yu9Gsrr/aOJXL
+ YYcMdrypHN7FU0lloe5vFWoXhw2jFo9DJickXyEuzJx9EPicWu6Kw/dm++x4MJ808T7dQA1VPo
+ WmmhF4K3v1W8w/Op8rsOCc95q43lWuSH3Zoqxavs2rsO6psX8NhwTZGYYZpr2crhVRwDgN4psf
+ w8XdkYX3WBd1CebPG9COIEk4BWtLP9gX6DVyFvZy8qcj+oLdMb+xXnx2W51rqbrfAfh3h2U7vm
+ op3Y3BBw5mqtqzGMRivKYB1x2pG9FTYhydun6ok4qZnZ2q3+2/m6HPb33fTXVAJsmRqomP4WwD
+ /KEsD9zFpy1EHgV4L8OU5jXvuAXkRud1NwXnS0HtYqhv1OqioFXU6uEtf87F1zxe0qjYXF+OS3
+ 3iMZNVbNo5uv1yHpReK/dqaAK7W1oxNEyYcHCh7qOT3Nr6N6jiGF52I8m7yfHjxHdWRwBvCpMK
+ KrXjbLqE9BijpGkNoXuuCqcNZldzSfbUZoRv/EM1QqY6NIW6TGa0rzBCHTXOjHAkjgr2tlHS4q
+ jqsdk65XbAiBuj+6o1WnKz+2295/CoOGsy6NdJi3NM8cFgO4+qpPEyQdQmxs5WigvVHAEdVWJz
+ oz0V4bKOlxVHVY7J1ytVBe7kCJdeSr0IoR/xCNn1OZ2aaRGHdcU1gJIaKX7PnUcTwZmiWt/00I
+ 0NRmqgWY/eVZiHc4n07OjtY353n8Iy6XLJM7oF4MEbdF0brxcqyea75uH8bP8A/8QAKBABAAIB
+ AwEJAAMBAAAAAAAAAQARITFBUXEQIGGBkaGxwdEw4fDx/9oACAEBAAE/Ie5pKwgb9PLmGBVcL5
+ +tekRZ5AAPvFU8RT+xeuKfpEAUI7n8NWYtMnkSqHizI8fzvATXl+D0iITpHo0+JzKDk6neZSPG
+ qftmQsWi7doqi2MvdGiN4BUgabdP53Nsjg2fsRQNhS7SaJWmLASpUIJrlY41GVqhj5PB2ZeK4c
+ uxLgxsY/nU6jPRG1upiJlrPWqu2HT1m0iixZdqFhZpCSKkcYmvdq4NyLlYBLsYfOcH36y2LDmI
+ xgWZ7ssbBttpxFjIr0j1aeL8v5AqYPOBYby33GW8UW+5NOjo/MeX2Yo+DKDWOTJeYRcPPqWvv8
+ zqDHQx9Q/GvlnipitzcibIq3Oj9lEZddS6ve88p1MzSDrL2sIoReamp/8AhPeovUStgDdUD4fr
+ s0/Wod8GIN8MsFtYQ4Ho9YulJnyYYK0X6GDYczU22vfsNOwpIra3Jv8AGO0y+os0M2Poke8rhs
+ 8RlWLvMQ0CcGEay1ZUoej0rbuVt7PgGG1bg6B6z7GX1inuI7eRcHVxNjjZrvn2iW5Y7Ki6iIec
+ q97af18QRthdd/4QpQabL2gItPraShrK7NEXl+WnvUZNDy8DSZXjBcUyptuXHuf1CY18uPXSCJ
+ Y2d4W6GgWrgmCBSuf+E4rhFL37KocHw/3TsyOgx6jk8Yr1CFWdohfHmyPF7gC9lNyUeoXBVvhM
+ CC1Y7ncPBXT1a/UKJvxZqK4iEfgP2YABQwB212NfQOjxjZolKofxphLZDWOjCZSoKaOrPCzQuK
+ dUuPSeqTAro0woOAcjz0ihKrV3YVfCUD714HKwn9M73J3eFXNA84ZZALto5e7fh4tMPj9IY6zY
+ gCqHFKuHuVjjy5ms66vufxgPokdB9y0MNo2flmS25F5zURCT3EAAoKDuf//aAAwDAQACAAMAAA
+ AQkkjRaEkkki6RagMkkMw122ZkkVsEe43UnzMsoIVRiqelxLXtd8QAAdbM9EAAAOYYbEAEkOpo
+ QAAkAtCZDAAAA5w91SgAAeBgklMAAE/8kYCAArvskhUklhwkkkkkLUEk/8QAJREBAAICAQMDBQ
+ EAAAAAAAAAAQARITFBECBRMGGRcYGhsfDB/9oACAEDAQE/EO0TVb54gsbntgPjc4qf77xTNnp4
+ GWo98Rz7wIECBBrXTBTA+IBbfcfkRKc5YEsJlmB2q21CacdinydJVWxtqZMwJbEMnS5cYYkzBv
+ 5N9CRRGXbBNs2kGnILf95gl3XzN7RKpjLAoEcTAANM1ffMk0RoRkzm5ULnwvEymTPZTwEAagEC
+ vPh/yXwIIDRFzNOJaIMmC2EfR/tA6Nd304v4zGS9QKkuM6I6H3YrVlni+illMA8RLmCmxSlRxM
+ z5JaXLY+itZi3mZR3KVu2EFEuZr39L6AfuafeCIbS5cFBNUUfokHcyhgcCXLlW+ISKZDuIoiV3
+ iilxgueV2NZOJczDt6KG8/MCBQ51EJfezURtevafOCO+PsBf4ZQYg13U5dxNeUNsMCiu2ue4jm
+ Xalx4AUenwwC7dzCCibjPb/8QAJxEBAAICAAUEAgMBAAAAAAAAAQARITEQIEFRYTBxkbGB8KHR
+ 4cH/2gAIAQIBAT8Q5RNJ26zSIfK/OoJ1P38RIwj+f8mWfjn6iV6LtGIbGBAgQOGumKyq8MpRTz
+ J0X2lQFQIEC4HLXKyO7Pp78lzov5gBFtcsIZgqLUByXAaLGPg21/XBCHWE9BOsDL2IEwiPLLlP
+ Yz7F8DUWLOoly5c8xB+jHbYTAvtMqbirt2w+HNVB1A6qF/MTT+Yu3ebS34n9ahuWRG7MuDLjFI
+ HWUfx95jMhuFQF/T3i9q+b3Egy6jDF74zCAJn9FMTIuMXcyzLXuIh2pf4gsPuPRC5dYji0NFsC
+ scKOu1f89KnTayog742XZLEumZ4pfRFNSlHXHJY+cRyMTG3NVmKVPOgCbLdEMvHCdfbgIsw68R
+ FTDINr0KOa9fqXUO8LHt1N8aA09Ht/k1Ge0yCi228r09DcACjjgY8ttkW2+Wmad5+mLC0tmNcv
+ aX59MjLXwf3GioInZ9Z8zH4H715f/8QAJxABAAEDAwQDAQADAQAAAAAAAREAITFBUWFxgZGhEC
+ CxwTDh8PH/2gAIAQEAAT8Q+igVQC6tHWFGSLwL/wDM1JWcuSbMUrXRDeXBcIBK41SZhUQNuh6T
+ RVmtA+4B80JQpEkf8ML5T4i/pg5pfnQvaKenR5ay/Dlqdpu3oYgDMybzg9ysagohcqu7UgIVgW
+ XZLnf7KsZWS9jT0Gu1NVM/NAl/6PhyXq/vQpFFilnPzFY+3tk2TCWw2aOOfRsZl579xN4+VeLL
+ dddnRei+pW4FdahygXd3VqQmO9G/DkP7U8cOlEYpDCw80UPmDVlSMAuI0+QQBaOnA6mj2mjuh1
+ Xt3GO0tNRf1Jf5QwW0zHMh/uOuKMs50bOKJBUWYTBILxcKWa2qEE5an+ehZFeqMoFEia/Hw1o+
+ 9kFNmyxNA+XhPDDpUnmDwk32eKbBzd5g7BQrsJywEscqh3mjuqREA4BxhpLoLzLFXNOQksncgU
+ DOly/7f55UUMIiVT5WaSjzffCT1XAgN5oflSToSRege6iVoI9LNrm1JRsFCpgaKvSgtmU1x009
+ 293IBwQao5huUzZvgUNYKM7AJ8yeKWciqZNblk90unOYFtp2I1U7BmH5Qj3ku9MfYmCVXiHspx
+ lllxTQXAfHoBR5VmAkAc3A9naoo5sW6y051kdwn9PP4OVACxpdDhouW+yLMYfzDHeneSrmIrjo
+ E1SBQjkF38rRuscQJ+UW+b2yID9qZxVqDIANjCTGCXX4GO4jUad2PwWdcPmIu0Lj0MtMgLgD2O
+ fVI5l8hDp/tNNlxlDFrfHal5Elan3lk8tqAEnX3avkpLTRMCB0sD6T6O3+fEeHDWXZ5Aef/UUZ
+ PMnPmw9Ky0brSc3t6oAILHwaTCfLYe2hQrB18iUaW1X9SXNCJOsUlNB0U6NVAftLkdIYwr1hRC
+ J6ERZ2ZP8ACk1LQmW5zU6qQzewdYF7lIRUSgctNEppKUqaa3nSJsJfXOmjwtRkROUayMW3yND4
+ /EMu4eqBDsEvQvtRMSEiMj9l5JumiAarR1M8LZcaABe+Kcw1gMDTgV1Ab0lPCkLPQ/0q9Pg2E8
+ NkWLURjU6QiXHsNyg7QpDTYGAmbMgxmtprml2I3VMMzBe8QS06N0bPaj+6zXnHujJBSJI/QSTN
+ TFt8AHUaCNRmwLvT5lYn7oLmcFpoDuW4JdKLUUAgAwHyOnZtE6ugWphzZmX6E5Zy7MRA06DRZR
+ kD7A7NBY06QsEisFrtLUDQD3SdMvCreVZpwuaveRt2KPrvmvOPdDGZmSReiZexrTbRPSoyrzNG
+ GC6gy1NRZDqdAH+r1NeypCsr+GgB9bZOSDsBD2xUmNbuEG4xr9XogwCN4cHL+2otZKzFyZVYCb
+ v7U5SCRBvDcpnc38Ayult0o7Ggb9TVfhg/xom2zLhuRl6jpVogiHSxJ2iOlQEARHQJz0Xq7X9h
+ QAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf
new file mode 100644
index 0000000000..bea653f3d5
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI PNG, version 3.0
+NOTE:This v3.0 card has a PNG photo as a URI and binary valuetype.
+PHOTO:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAA
+ q0klEQVR42u2dB1RU57r3XXd965R859yP9d17Ts45SRTpvTOFOgiKDcUWY4kSe4kGu7EgNoogg
+ p0+gIoGFRRFLMBQREGRURBrDDG5OelO2knul3vO8z3vu/ee2XtmDwxNR2Wv9V8qIs7M7/8+7X3
+ 3zIAB/Vf/9bxfQwseWYTlfRgVmvtQOSTjzvWQ9Lu/BCdebg6KOT+o/9V5ga/wIx9FDjv0qAThA
+ 8KHIQda/1ux7vQ/gt8rHN3/6rzA1/Cj7VEIvx3hQ1g+wlc+hJDNFyB4QU5L8Jx0C/I9gfvuWAZ
+ urd4duKmivP8Ve3HAK1DtCB8I/KEIf8g+NSjeOwwIXk3gB6feGB20q/lhQFrLPwPXnvkZv+Yh9
+ rO8t95QeMU2Kfpf1ecDvAWqBAUC+Duv4KrPJvA1iqQrbwWnNbcH7b8DgXvbIGBrDfjHN9TJk27
+ WyRJufC1NvAm+8Wrw2d4MCB+8NjeB56br4LHxGritawTXtVfBZfUVcF5ZD07LL6scoutU9ktrU
+ +3erYm1XVQdabNA5dFP4tmteg2FX4jwDyP8Agz7uPIpfBTC/z744F3g4Pvvvg1+qa0gT2kBWdI
+ tIPAlxuCvR/jvN4DLmqsI/wrCrwfH6MuA8AHhA8IHhA/W86pg8JxKjeWsCtXgqEvRVjMu9Bvia
+ eR6Ap6B366Dn9EKITFlEJJUD4qMe8CHH8CHn4zwdyD8BDX4isB358NfhfBXIPxll8FhaR3Cr0X
+ 4NQi/msK3mlMJCB8QPiB8sH77PNhMO9duO/Vsqt1bZ/rN0AfwlRz84RT+Rwgfiz6u4s++DyGZC
+ D8d4R/QwfcXgx/XDD7bEP4WIXw3Mfjv1YH9Ej58FcLH1T+rkoE/8yIHHxA+IHywn3waHCeWfIq
+ /T0JzBPbT6034ZPUbwH+A8O8j/HsI/y4E7kP4exj4fkbgexP4sQg/BuFvYOC7GoO/GOEvRPjzd
+ fAtEf5gDv50Ar+MGoAYwQq/ZvlOhVZolP+HOo6/j0JZ9BPtwhWW3lopgM8WfbTd48FXaOHfQfi
+ Y99Mw9O/C1b/TBPjrGrRFnz58Oz78uQh/diUDFeET0NbTyxnomAb40DvQ9yglqr/jMHa5j9pv4
+ T5yX6z/6vJvxeAPZXv9ITkIP4uBHywKnyn6JFzFz8L3MgKfVPyOyxF+tA6+rTH4Mxj41m+X49c
+ umQpfIKuZF+6SqNBPnL3cItItULFuo9M1koXFYBJ8XtEnBl+03Yu5LgrfiYPPVfwI30YPPgn9X
+ Pgnq74LK9+oMIX88FIbwXVMlgUqFqVBgdfMYzCs4KEOfqEOfpgR+Np2rzP4/HZvLRZ9q6+Kw+e
+ 1e6Ta7ylkk4wwrexnpwkn575U8F0ic6OcI3PbnSOVwCgXQg+2CuGz7V5Y3kMINQLfoN3roNc3a
+ Pe0vX6dEP7cpwefL8eJxU88Rh4a8UKDdx6X5+E0Ll/lND4fnMYXaCXfWAPCXv8jHfzcB7TdU2Q
+ awjfW7onBdzOp11c9dfD6QhPUvpBdA4KOdZpwCBwnHGY08QiV+5xT0HGv/6B7vX6sib0+V/ET+
+ LMqnrkBiGynnP2n91DlkhcCPMK2RNBqAtthUiHqKKtjVIo9twRFn1ivb9juYa+/y7Re32N9A/i
+ Qwk900GNY7ZuN0Iw4Y/j8uY4GCD0KoWso7DePgf2bH+DErEgrz+hL4r2+UgjfWLsnJe2eHnyu3
+ fPY2Ai+GAFk+Ksr6fVXMvAJeLLa6WDHnICz0HWqpBr8zqV/OUwqefe5g4+rXMmHbjf5OI5MT6B
+ OMppyEsJy7nfe7pnS65Oij4XvvglXfuw1kOKf5dj6ua1h2j2HZXVgvUD1XEDXwp+tE46aC58X8
+ BYIXmX/JlnlLHSEbTulGFWCYY3oFPhsqDeAb6zdC9hrvNf3RvieuPrdtzaB6xZc/firDItAv1g
+ Cn2n3HBH+4DmVzx10KjKGZmU39cx1M4d/zBJXvFoHnoN+Cvvd06xKwXnRRWG7x8IP7ajXTxXC9
+ 8HQ7xHfDG5xN8BlWxM4b70OnvEIHk3hhwbwfL9Ru7VrZQ7wuwl98FydrDB1OUw+/ZlZ1gW46j0
+ w1Gu04KfywE8vxRHqGdRZsJ11nq54g14/t/NeX4pFn0/STfBIbAbXhBvgHNcEjtuvg8M2zPk7m
+ iEg/ib4bcN6YON1bcVP2jtzg24AvBPoVPNZ4fPBTahvzcoEuOoR/nENye2CFc9Ct367DDdRyOb
+ JOQje3ybS7vF6/XTxXt97501wS1aDC4J2TrwBDgnXwT7uGthuawT3JDX4J9yCgO1qkMY2aTd4y
+ JTvqbd2fQSdyHohI2qCKWe/MQsTEPh2byF8kudx1RuAn0HAl+Mc/Tz4Yqgm8Dve2r0j2Nr1Tbk
+ FrjvV4Jx8Axx3NIFD4nWwS7gGNnGNYLUd+/uduPJ33MKjX7j6tzbTvM+1e6S/Nyvoc7oLvQasF
+ 7FaTFRLW1eXyKL7ZgD/hIYWeHTVs6FeC/48gr+Ae+MXcepWI2z3Oun1Zdjru+9SgyOCd0hqArs
+ d18EGwVvFN8DguKswaDuu8pQbEJDcAgGJt8AvTg1eOPTh9/qkx7fDx2I3veyFgG6DU0ubJXWM8
+ PfkcTwzEyB8C4TfLoR/lg31zIon4MkJGrJ1SsK6Qa+fI97r+6a2gCPCtUtuApsdCD2xEQYnIPS
+ 4KzBwez0M3HYZnHc2QUBKK7P6E26CdPMNba/P39cX29p9VtC1wE2CXmsIHUfXtu9dBttoonr6c
+ 8jj8g7Pq3kW8NVa+KTIe/sss+pn6lY93TPHF4tswnS6tcvCd8NVb5OM0JMQemIDDGLBW26/DJb
+ bsJ3bUgvOOxohAFtCsvr9cfXLMfeTYY+jCVu73JYu2dcnhzvIn80JOgXeAXTbZfVgh52N3Yor+
+ Pe12sfpPUyZ/NQMgEMdpRB+mWDV0/1z8sDwRRk0r5K2cZ31+v57b4NDShMM3oEhPvEqhnp0eNx
+ lsELoVlsx523BFyhWBY74tYDU2xCwE2uEHS3Y9t0ETxL6jW3t8uFzhznZEz1YSGG7WopDKhK9z
+ rHRqpvQ5/YS9KUdQ7fD1tZ+1VUquxVXdQdNMN16hysn9f3qn3w8min42LCvD59d9ZZzEf6CKhi
+ 0qErX7nXQ69vjirdOwLYtHou37fgibMNZ/VasdjerwDYWhyAxleC0pRoCcCAUsAtnA8m4H5DYA
+ pItzZ0d49Y7yVvOO8xZipPK07gnUYIbVCdxZ/IEPdxJjdBj6NV9At1+dQPYr2kAh7WN4ICzDn6
+ 0wsf+T59huYP6EH6RB231aLXPhf1yXsjHBzKbWfWDFiL8d3HFbrhquLWrB989+TrYxuHW7PYas
+ NtWDXZbVGC/uQrsN+EINOYSOGy4CM6bKnAecBMCCXy6+rE9xNbPFV+MLh7jFpzkpfAnFlP4zpF
+ F4DL2A3CNOAqO+HVrUsM8LejLOOhXhNBX60O/Bg7rroHj+uv42l6nj4NvArcxRzV9XPQxrR4t+
+ DDn61Y+G/LnM6t+0BIVDIyuBoeYhg6PccvTWnCQg9+3FadcW6rAMbYCnDZdAqeNF8FpwwVwXn8
+ eXNaVgxxNEpjaRkO/XxKz+r1jmrpxjLuMB/+UDv644wz8MUfBbXQhuI86DB4jDoHzhBNg/c7Fr
+ kNf3EPoa4xDd8BBlwM+d4dNTWAl0upKQ9LP9EXoTyV532bqadrqkWrfAD4J+YtVMGhpNQxchlp
+ ZAw6bG420e0yv7xGPmzWbK3CFXwLnmIvgsvECuKwvx8LxHLitLcO+/izI4uohcDcWiSm48pNug
+ xzhy+Ju0tDf1WPcdlP04Z/kwT8mgO85PB+8huWB99BccJ5UjPVEZR9AFw/tWugI3HGjELo9Drv
+ s8cCL/ZYbYLXI0ACkHpCGZAzvzdWvYEI/r+gzAn/gewh+eQ0MXIXhfP5ZkEed7PAYtxuGdreN5
+ 8FtQzm4T00H9yFbdBqeAL4rSiCQ5P1UJvST1S+Lv4X7/Ne6dIxbB78Ut6ZZ+BNY+JF8+EdY+AX
+ gFc7A9wnLAd8hWaTSBruoC92CTvN5Z9DpKudBj9FBt9twDazJ80XodrjXYYcbYLbbbxjtXJwmF
+ f/am1U/7fdp6OflfVrwzWbDPg++3YJzIBmVAwp5CoTgmT9jx7hlSc0I8hxu3JSBx4TdQvisfGc
+ qGfi4+v2w8JNj5S/ZpqYtn6nHuPHgJVPxa+GX8OAXMfAjWPgj+fCVLPxskIRkgjQ4HVdWJjiTK
+ Kjt0WuF7VpvQN9kuNIJdPK8rPFEsy2ef7DFjTCbhGYD8INnM7KaUwE+I/OaeqPwi+VWPw392rz
+ PVPu04FuEYR/hW0arwOPNQggM3gNBAWkwaWwGKPMbjB7jluJUz3tNKXjNUmqBLwwfC3EjfWHfD
+ F9YM3sSeE06IIAvw8LPDU/2dtjrR13ktXtl2nbPHuGToo+Bf4IH/6hx+KEsfEUGyIIOgizwAMj
+ 994ELmqDXoccaQudWOoHOgbbGbW+bHWpsj68zz5cDP4cBbzUXvwe52M65CPJhmd1PBbjDZ0Hm/
+ Ezhx7V8vNBPWj1S8C3Flgu3eH0jlOA/ZD81wNYt5+Drr7+DX375xegxbmlsNUhXFIPHqB0U/lh
+ fb3jlf/0b+L72e8if9yq0K18D76lZCB/zPlb9soQW8N2qFm/39Hv9t4W9vn67x6/43UYT+IfBc
+ wSBn09DPYHvg/B9Eb4E4Us5+AH7wM9vL/jLd4MbRkRBu9Zd6JsZ2YlA51a6TaLOAIPxuVvh/og
+ VHnzhVjuBzoG3nl+JrwtqYSV4Tvrgxx7k/iIlWf02gtXPhn4u7y8h8C+AdGQ2+IUdhGERWXD69
+ C0KntO4zLuix7hlMVXgF11E4YeFzQGNRgMeHh6A/zWEuv0Ovj71GkiWlYGc5P1EjBhxLTjxazC
+ Ab9Vhr3+GB79YFL77SFL0FYBnOCn6lOAdlsvCz2LgY+hn4O9n4e+BAFkaBEpSwX366e5D39I5d
+ LLSrXG30xp3QrVhnigBO4C19Sz0CgrdeoEOvO0i1OJKfK0qwS88c2+3DneQY1yC1c8Vflzox7w
+ /+N0KkIzOAXl4BgwdmwOtbZ8J4BOtKX4kepJXvv4S+C85Rg0wUj5KC59I5vhbuFXojtV+MwMfV
+ 78PDn30j3ET+IM5+DNN6PW5ij+CbfdGshU/B38oC3+ICHx/Ifwg3xQI9tkJrvMudA06C9wU6HS
+ lp9wEq103Bfl98Pv1YI1zFv5q1wdvtwTnKEsrwT3q1K/dqfyVhrmft/px0GP5bhV44RFv2YgsC
+ BunFIVPlFP/megxbtn7mKMWHwWP8DiYMHQWBL7xCoX/x9//G9Tu+TOsSE7Qwpdsv0XbPv4xbn6
+ 7Z6Xf7nXY6xtv93xCc4Twg/Th74ZAKYG/i8IP9k6GIEkKOOHZQx30JuPQt5sAHQ+5aKHj8GtwG
+ iq5WZDf+eApdBHw9u9V4nicqAqCQvfndOVcnyU5yKmr/PVW/3xmyuc67ThIsdoPGZcLrXfE4RM
+ 1PHoieoxbsrYCZAsKwSsSX9DQVXBu/utwa+drcCvXETalxfBWvhqcSK7Vb/fYk702BDivwrcnO
+ Z/f7on2+keMw2crflL0MfD3CeAHIvwgFr7CMwlCPBLBPzRdr3LXgx5nCN3ayEq3XFIDgzfhZtj
+ uW2CJx+Yt96ISm8TBE+jGwC/DwRrR8irwmV70r64Uf7HkaBcd+Yqtfiz8bOafBwkWfcQA2xNzs
+ OD72qgBrqIBxI5x+6yvBcm8I+Az6xAWe7jiFp2AiNgiWvHLsOiTYL/vzvb7+u2eLeZ50r+Tin3
+ k9CKIxnHx/pxrsD+7kepAVgMk7qmHDYnV8M7qCzDx3bPgN6nISLun6/VF4fsx8AMIfAmBn8LA9
+ 2LgD3FPwJolHnzePmkIPd406NqVjtAHv1dDV/ugXc0waH8LleWmBtH8TqCLgkfojiuq8HwEamU
+ VHpGrgjBpUrqp4V/DFH/cyLdcW/lzq5+EfmKArTvPQXp6Opw6dcq4AT58InKMG+/g2XgFfOYcA
+ e9Zh8E/tlbb7slw0ueJq4kc7uD3+qTds5mFE0MM52T1rt9RAy13Pjf6/xpT/fVPIK+ohZpj8rw
+ SYa/PtntyzPsEPi36KPw0Efg7tPBDXeNgiGcirv5rDPSErkHnVvqgfWiAZbW0sCN5fuDBFhiY3
+ grWy2uMhnlR8Ct14J1X4+mhNVXgH5nziynhP0oX/pnij/T9dIuXzf2OUafBd2weTF1yCn799Vc
+ Kn5igublZ9AU/c+Nzo3ftes06CtLoM3R3j/T55DZueo5fb2vXFqt9hymldPUuxd3Bjx5/02XwH
+ elK48c0aqzA8fPYiQW01+faPS18UvR572TgezLwh7Dww5y3wVCnrSCPyGXyeSfQLfWg05V+oIU
+ Ct4qu0a72N4gB9qg7zO+O+uBZ6Bx4l7VVOFqvAt/ZJ2CYx/bozqr/EvHwr6v8PScWUgO03f+SG
+ uDJkyeQm5tLTfDZZ4a1wMG6+xCULH7XrtcShB9H7t9v4r1DlxC+3awLdGgz4d0yqG/6pFfBG9P
+ XX38PDVcfQcaBOlizvASipuTrwU/UwXfZTuEPc9wC4Q6bsQ6o7zL0gemojFZ4A8UP8wPTsPjbd
+ KXD/O6kD36NELzruiq8NV4F7lhzDZUkPuzoVi4LciePrvfnhX9S/GHfbzenDHwiC2DTrloKn1N
+ bWxs1QFFRkcGLmVJzG9aVtRjAl+Ikyw+Pdwtu3Izmbe1iq+c4+RTI8fHknrj9VMB3praWT6Dq/
+ G3I3lMFce+XQPSMPIiU7qDwhyH8cPtY8BuT3Sl0mt9TmylwqsxWeD2rFQbuvSlY7dYb603K71r
+ wCN3lfSF4tw0IfyMqRgWKYfthmOf2QUYMUBilX/0Lwj8Wfy7TTlADtD34UmAAonPnmHqgoaFB+
+ 4LVPPw7lDxso0pUtcCbmc0wKVMNs4vaYO7ZR6DY3Wpw164d5n37qPOY64/D4q018NEn35oF/M7
+ 0zVcauF57H3UPfHFFG1vpBPjAlGaa1wn017Nvw+s5t+G13NtY7V/Trfalpud3Lfh1RCoD8B6bU
+ HiiKjAyG4b6JhYby/8l9rzhD7Plqwv/luhM7/GHYEp0qQF8op9++skgFaRU39YagCjz5l3YXv8
+ All74EN46+RGE45RQcNcutjmO2L6FziuDyzf+67kAL6aN1Z8YQCer/A0KHA2Aq5+E+YG7bsBrS
+ oSf1wZ/y28D27W1gvzuYGJ+p+DXGwfvuZmR34xCCA1M+Yex/A9c/meGP+WC3t92bjk1wKGSVlE
+ DED18+FCQCvJu6gxwuO0OpFy7D2urHsI7pY9gwvF2Kh+8j88Bq16HqWfAd0YppB1ueW7Bc2r+7
+ DsD6GSVcyt9EFnpuKCsMMT/raAN/nqoDV7Lae12fifQ3TYaAY8nrLyI8NCNfM4HoAjdC8N8EgL
+ 07+OPJHfzMgbQy/+zmerf4Z1SagCx8G8sFXDwix+0wUH1Pdhc9wAWl38IE08w8BV4x4/TjDJQ4
+ H5C6pFW+OrbH557+JykR+8y0HMZ6GSl/41d6VYx9dpQ/9cjd+AvhXfwEOzVHuV3900i4BG61zY
+ V3kRLVA3yecdBMXQfhMmSivULwFhaAOKBT/70T5f/8T/DyV/w9GMdwuengqy8PK0B8lrvQlLDf
+ VhZ8RBmnP4Ixh19BJ6YAxWLL8Gxi49eGOh8Lal+LIDOrfS/Hr6DO4gqbah/PfMWNYDDmuoe5Xd
+ x8NXgHVcNPkTxaIAFyDD8AIQo0r7SLwBVhv0/M/2j7R8OfzwmH4M5G853agB+KiDwi+7fgX037
+ kFMzQOYf+4RjFLeBx/cRTx26aMXEjyn0x9+I4DOrfSBe24I8vvAPXgcfmcjE+ZXdT+/i4KPZ+S
+ bgMLBlxynrUEjDkLw8AOgl/8LgW8AQQE4n9n29cbpX0xanUkG4FIBMUBOy11IuHIfll38EMYV3
+ IO38J6+r7798YWGT/QQnyMH/S9H78Crx+7Cqx/cxeNddYLCznZbfa/kdz54yfvlIFldpgUv2YF
+ Kqgb/aYcgaFQGBI1Mh1C/5GjuDZw8yPv20A6APwDi9f/k0AcxwIEjzSYbgKSC+ItqSLt+D9aps
+ PA79RCiD9584cHz5VX6kEJ/tegu/Pn4XawFWno/v+uveIQuXYV7H28VaMFLk1E78b6KSXkQODo
+ TlQEhwakqxgATDkURA+DpH/EOAAtAct6vqwYgKm68B9suM23f/JcMPtH0usfw5xP34M8n78Gfi
+ u/hzS71fZbffdjV7ovQZdGnwG9CHsjWnaPgZSmoJBUEjM2GgDFZEIgHdxRhe//OGSBW1wKKdwD
+ kjH93DEC0sPAmjE+th9ra2pfOAHEtn8OfSu7Bf566jy2huk/zuy+72iW42mXzj+PuZz7IlxSDb
+ FcNyFNrwG/dWfDHrfuAyBxqhMCRGb9yLWCJeAuomwASA3hhndAdA5BUcPjwYVoUfvzxxy+VAao
+ +08B/nr6Pq/8uOMbUiOf3DV3L79rVrpffJWyYl+Jql087AvLJh0A+twhvvkH4u1ELisB/Ap7bH
+ J9LjYC7g8AZQMUZQLsHoNcCkhs93KafgB1ZjV02ANHjx4+pAUh7+P333780Bmj66ntqgMG7r/V
+ afteudr38TsP8LlQcHrSZegRkUw7jPsoh8NtTg7utlTQiUE3MY42AR/aD06KJAdoFBnhbzwCLG
+ QM4v1Nichsoprq6OmoC0h28TFHgb4daejW/09VOwOMNNdIdKhY8E+bJapfjfRYyjADUBCj/fWi
+ ANWeoGeRvFoAfEWsERcieeGIAYAzAHgLhG2CuzgD2C8pg1PzibhuAiIyIiQkePHjw0hjABUN/b
+ +Z3GuaJVpTiucpzWvA0zONql+G0TzatUCv/2HJdRGCjgnxyATVD8LD9KmoA7Taw/hSQM8Dyajy
+ pUkFHwU80P3XbAF988cVLlwpmZlzv1fzOrXjp/JMgW3pKC95/L/66A8P/9KOoQkY8I3ARgYgzQ
+ tDwg20mR4A3cKfKa9IRqLj8UY+iQGNj40uRCkjBW1FRAaPiyw3D/Nau5Xcpl99T2VCfVAXSmR/
+ godqTFDwJ8wH78evLT4P0bTQA0fSjOjMYMQIWgk86NsAcoQGcZ5dAUkZDjwzATwXGjpE9ryKHY
+ 8kmGNf1EI2KL+t6fhcUdrz8zoZ6+cZykEZ9QE1AwAccQKWiKWYcY4QG4CQwgsAMxAC5fAN0XAQ
+ OXFMLVisqYPTckz02AEkFJA0QdXSi+HkQSWW3b9/Wno3UV8SO8i7nd33wNMzzQr1sUTEaoIiaI
+ PBgDZX/Bhz6zDymM4EJRiDFoMAAujnAeeEcgDXAGxsvg+fkQnj8X5oem0CtVnd6oticRQpZEuL
+ FoBNjV1ZW0k2xOfnNXcvv8ZUGhR0N82yo90+sAOk7RVoFZdRC4B6VNiIw0jeCzgy69MAYAQ1wC
+ IxNApdn3dYOggauJgbAO2Kjz8PBwzd6bAD+iWL+MTJzD/FkoskP8WLQ+c9x7qHmLuV3KZ64EoB
+ n8zsX6mXRmOdnHWeEBgjOqgW/laXaiCA0wgdCI8wwNAIZBQOzF3CCdysYY4D2z3+Ao3WfMvf+r
+ 6qBN3An6/Ut9RC+sKRXDNDZiWJzCfGkVuHqFn2RYlYfOl8RB652mN9lvPwu33IRJJhi/VJUOvA
+ IPfAAG+ZxoCPB490ShC9hTRCUIowIRo1gJD3gbiBjAOFuIGMA7kncatdAeEoTvLEeDbAZz+1jW
+ 9PY/GmvmED/GJm5iOR17nSTGHRyEpqMuTt7fl3J79J3T+HdUsXgj+1cAAudKCi9loZ62fJSkMw
+ 5wZiACk0w74QgIgiNIGYGoRHIdjAw5wHYA6HTS7X3A+g/mbMtX8KsI+Rt3Rpg9O6GXjEA/xjZs
+ 94w4lo3Lip1Fzpf+v27sfwu33wBJPNLUGiA5AoITNeBD87EO6e2XaDRQTLnJGMCPSNwEYExwnE
+ TooLOABo8ESR6IKS3AJt6jOxZbBh1lNdJVCLFKklV3UpxP/4s7N/1wXNhfg/WB4tPge+CEmqCg
+ J2VEITQSX6n2o8mWljMGIBTB0YQmqFjI6AB8lXkQ530zwSSewKelgGe9oYRgW4sr/cUumDo1f6
+ NoH83AM/mdxnekua7kBiAMUHgrkoIzq4FBVEOhv5lpTQ1aNWpEU6YbATyeX5oAN44mDcMepoG6
+ OsNo476dbL6yYSyN6DzVXn/C+Pguf49vgJ8F51mDMAqMK2Kgifyw/6eSQ1MeujYCKakB6ER0AD
+ KWMNZAFMI3nr05KmboDenhBx0sWKOQCeGI0OpvnoumVceiYNn83tAmgokS0sZA3BCA3Dw/eMu0
+ ojApQaTjNDFOmGAc6QymmsF9c8F1rV+9dQNwE0Je9IaGhvSkJ/b19D52nrpjiF4LOpIfg/CP0u
+ Wn8XcX8qKMYB05VkKP2DHJUFU4NKD0AzdNwKRnAyCXCJzFbpOQHg0vK71y6duAP6UkEQDU+sBD
+ rp+BW9sQPM0FHWsiQVfqwVPiroQlHT1OfB9F++GereUEWsEv83nMS1cMogKXTKCiXUCnhr6YYD
+ L2BwL8nm+2kJQuy18Fi4/IwPwW0Nj9QA/vIu1bc8KurYD+OlnbRunreixqBuedxlm7LmM90aco
+ fIl4hlBsvSMICJ0zQim1AkntMJt4SfcBzu3i9UBKcfvPbMXkH+WkBsVc9V7bwxo+lqqD7+g/Ts
+ HnoT1oTl1cPcLDcxNqwefpWcZGTGCLjWImKGnRmDNgAdE1YwBIpUlgjqAvUP4WRqAf4CEC+Vi0
+ ElVby7Q+dpedVfbxhFNy6+nUYHuD+xGA7x3VmcCfSMYNUMHRuhGnYAGKKcGwDogmtQB3EDIjn2
+ PgHkp1575C8m9+URf9ep9pXAM9Rz85Au3BX/nE12GBuDUC0boZp0QOCpzA2OAsTkezuP4dQCTB
+ t7CGxnM4cUk+byvevW+0Nm7n1Hws4810pAvqA2+/xkNcI4xQUdGWHrGhPTQszohzCPOUnt/IM4
+ DNI4iacDcX2xz1LqLrdQEYn/XcPdL8Fl2jpExI3QUFXqpTpC9fex/BDeIYhpQatPAm0XMWBing
+ u1//74fai/qQNk98FlerjOBvhGiTTTCEp4RulEn4KHQx0IDjM2JpGlA0A2UQHnDp/3gelFvJde
+ BNzEAJ74RlokZoW/qBP9xyiMGbxOD3YBG1w0wxeCuorZ+cL2kb3/4GbxXnGdVLjTC8nPGo0If1
+ AnBij2BBgZwicxRCodCJ2Dqltp+eL2kkquPwXvleUZ8I6zohhG6XCec1o2b3yn6b9E3inIZm43
+ dQB448qIAGQ1/+90/+gH2gubsbwTvVRfQABc6NYIvgibVel/UCXhfYKPRN4zEYlDNFIO6KFB+9
+ ZN+gD097/DlD+C9+iJjAE58I+iZIWB4OkiwWOuLOiFo6P55xg0wNjtKPwqs2netH2IPFXO0hTG
+ AVhf0zKAzgl9kLgQF7+6TOkE+/ehPnb5pNEYBDRcFSEfgPKUIMg/mQI3q8nMxiDG71f8Vrv41l
+ 1hdNGoEAjlg2EEI9k8F/4jsPqkT8Hawo50bYGx2LIkCTEfAzAVWbVdqR7H9ZuiaNh5rBa+1l6g
+ ERuCZgYT7wND99BPXggJS6Xm9zgvGrs0TyABoiG+SZacGcB2TacFEAdIRHKY3jngNTYXF09dDc
+ kKaYC6feTAbzVDXb4YOVr/X+xWM1lbojMCawQcBy9/EO3UDd6PSqAECFXspXON1QrlhejChTvA
+ fl3vD5E8OcRmTHY1zAdBNBz8At/+YCu7/PhGG2EfBImqGVIEZMvrNYFj5ZzSB17pKnQl4RpDMO
+ gEBQw9AYNBu1gCMCfzG53VaJ3R1niDB3j9Ekqzo0odH4XSwXVcQFoKz1yZw++MEVuPBFRViPxP
+ NsK7fDCIqqP0YvNZXMgbQqgKGrzpHcz35jMXAIE46ExBgnRWMFC7mdFONEDA6q7XLnx7mOiZLg
+ akAaCogI+JxBQL4rn8cBy5/YOT8h0hQ2M+AhdPfFzVD9UtmBhL6g7bWogGqqIK21sDGotv06wE
+ Y4gOD9zIG0DOBfGK+0TqBbwS/cUoK1pTBkhSPf4VIdyq69QGSWBCW6FLBEXBxWCEKn9FYcPrfR
+ GMgyG46LJi2FpJeUjNM3nsNvDZUweQ9jVB87VM6BiZfv3b9Y2oAqmBDI3gvKzeoE/SNQODTLqH
+ DeQJrBCwAg0L2Xu32J4i6RmRYYCrQ0K6AmCAyD1z/7+QO4TtSRYAD0SsREGg7FeZPW/PSmOFS6
+ +eQX/cxrvYfDY+K51xF+Pt0JuBFAynerdtRwUhMQOoDUiiSM/0GgyWROoF8v0KWohjQkwu7gkg
+ 0AXCtoZPHBpPhO7wyGuypRlEF2EyB+VPRDPG7DMxQW1P/Py96ZJg5+ygEhOxjxDOC/7B0CpmrE
+ 4RF4yUK158MiAKZLqGjeQJnBHL+XyFPKemVj5HHekBJ6gFqgrG5/3L+07Quw7f7/UgqW6oR4G/
+ zFsybutrADAX5Bd9cqb/604tmhk8+/Rb8Q/brDMAzgQ+OamnBuF5YMBIDkA0c0i1wRSJ5d4/O6
+ gTS9wcF7f4u2G+XZa8YwC0i3QJNoNaaIGRXd+BrOPhENr8brpXc+k00wypISUr7mW+G/BfIDIU
+ fNNNPWCcK4BmBACX1AlcwckagIRzzPVcgUuH3E8Ad1Qk+mPsDyUTRb1fsgN683CIyLNEEGq0Jn
+ FZ2deVHI/gSffjWvwvXyuq3w2CUfHYSwk9FtfPNkJeX/1VtTZ3meTXD+CkFWgNQoQn8RmMxR8L
+ 8BhVjAhQBLMPhkFinQCaEHQ+WWPj+qeoBfXGhCRRoAqAmiMgCx7/M6ErYp/kIoSuNwSca/NuhG
+ pQH+V4E7yFmBqVS+fn58gs/Pq3bvHqqa02PwT/0ACPWAH4k7+Mq94xRgedGFYb6UtoGCopEXqf
+ gPyLTID0I6gTM+wHh6cw42T/VY0BfXZgOoqgJSGEYlgYO/2e8STkfpeF+BsKPQugaEficDBxsz
+ Aw52TkaczfDloQK8OMMwMoXb87wWXwGpFOOgN/QdNHagN8pkKmesTqBRA2yhczsJaTFDujrizF
+ BJjWBk1+cKfC5kB/J/QwE74FSi8CnsvxNWKqx/9+YGbLN0AyffPoE/MIOshKaQJAS9GoDvhFkU
+ 8XrBGICEkUChmdwRaJ6wNO63EYfjHKNICbIBidJrCnwiZT6PwfhR7Nhnw+fVWinPaxxM2R/d67
+ s/D+etRm2JFbyDMAqtAMj6JnAb1Q2eG1UCeoEzgzkgEcA/hu2SNSgASwHPM2LmADrAiApwdHhv
+ c7gc/neQv/nIHQLVCyqnQcfBv0mVIOyMPXxmJsZ6OrH8M5IzwQmRAN//Df8OsGL0wbmnUH1Rsm
+ RA57F5T7qQJTb6HTAqSE4Wi/sDD5RVEc/D+FHInwlgm8fRE0wRNmdx2XUDFlPzwwLV5wGORZ7c
+ q0J0rsUDcg5fs+Yap5UtMf3G5WlP0pOHfAsL/dR+yPdR+z7hRjByX0NdNLqdSlPDfzNkB5XtM/
+ CDJW1HyL8DJ7SuxQNpFMKwXNTDatq8MI2Tz6xwKBIRBOoBpjD5TFyr4dbeJoGIwI4ea+DTlo9x
+ bN6nE/DDORt9cNwDi8Pz2DEM4Ep0UA+Nhc8Y2uoyLavbNIhY1PEdpTFAHO5PEbssXAPTb7rPnI
+ fuAYngM0fRoORVk9pDo+3r8ywctN5kIVnIvxMERNkCE0gEg1855WAdOpR8MN/10GnoEF5DDDHy
+ 1WyMRvNAO7DdoHdX6eBkVbP0pwec2+Z4XT5HZANz0JlMjJqBL4J0rvaKZgvfO1ZAs/VI92D4v7
+ hOXw3OLksE+vzleb62LtrhrZ7X4BsRBYjE03QWTQQMYEGZd7wBUZwX1XsGZr0L/fgeLB99S39P
+ t/S3B+/MTNkZWb9UHa2/GfODCTvh04oQPjZeibo1WigQT0/8LnLyX7RIBfXZWqvYTvB2W05WP1
+ 7BNfnK5+n52HMDPv2Z/045u38f0lHZgMV3wS9Fw2eT/iCO5DtFwa4uq9QeQVu+aeT7XywfCWc9
+ PmWz+Nz4cywe0/64xFTEPqoHISfA0IT9Fo0UKMsBrxIl4vT4ng3j+XN9m9Mj3len4NkdK6FZFS
+ uWkLgc+KboHeigRKjwYsF/0W4JBFKCzSAGgVUfBP0JBoIh0fR/a+0GV6+EUoPNIAaBVoDUBPoG
+ WFkjhoNoOlGNFCjATz6X2lzhD8mT4HS+BL4fAmMQA1QgkrtRjRIRQP0h3yzhD82Lxrhg05K8BU
+ 3QTRGA48u1gZqNED/qjfHyycy38JnbH6J79h8QBOA0AR5fBOoJCQ9kOJwdG67ibWBBg0Q1f8qm
+ y38gkiUBk0APtQAnAQmaMdoEKUtDkl90HFtAGxtEIvqD/fmeHmPO2ThM66gBAVoAFb5IDQCAZ8
+ XxasPLHy54lC/LtBFAw0qFtUP3mzhjz8UjdKgCcCHim8CaoQSNEGkXnHogVJ3UBuQlBCL0aAfv
+ NmCn3BYgVJ7jz8MaABG4xix0SAKDWBhWBzmR2I00BipDUhd0J/jzfnymnhE4TXhiAoFaABG44k
+ RDqWiIlEWRuoDC4wGqSK1AUkNpGOw7H91zRn8pEJLr4mFqWgAFYr8Go0mUJhUHI6jxWE7rzZQo
+ xGi0QT9rdwLXh8oMCWofKgKUtkuoT+v91/P3/X/Afw1kptmVhryAAAAAElFTkSuQmCC
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf
new file mode 100644
index 0000000000..be583a9bfd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI JPEG, version 3.0 with correct value type
+NOTE:This v3.0 card has a JPEG photo as a URI and URI valuetype.
+PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYE
+ BQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC
+ 0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo
+ KCgoKCgoKCgoKCgoKCgoKCgoKCj/wgARCACAAIADAREAAhEBAxEB/8QAGwAAAQUBAQAAAAAAAA
+ AAAAAAAAIEBQYHAwH/xAAZAQEAAwEBAAAAAAAAAAAAAAAAAgMEAQX/2gAMAwEAAhADEAAAAdUA
+ AAr+rHE2wk67TnXtdstTYAAAAAR841jd50PpytpSbS627Ll2TqErRk0XPBqdQkAAFT10Uj0MMr
+ zO2l1v2TKckOgAOI90fyt9gzXgFU10Z56mCdrobOJ71pO1t2ZwrsfXFCuc6caV5HqTdFrKcch9
+ rzJjNoTdhgvO9SR9XzWMp8o3c42qJCeDoivkePZda9GueL6dF35oLRTo3kb6P6vlRkpWbNqn81
+ 7aXIezsPZLjf5/l3m80265XO3jzvQpu7Pacl9nw6IPXjm8+x5CQAAMrKM09jw+XL/eS952Qo2V
+ /XRd8Oi7YNkYSIAAANHMw9nxVcCXHlnJoVOu/wDn32TLqQMR+BxGR4OXM39bxefZcO2N+2JSk4
+ d1fydwA1GQs7D4BlOqi7/Pg7+oS8F8lZM992w3uoyAAAABLlM3ZKhrznXnExTdfvP2ZRthZMl9
+ uqjJnoAAHHsc19PFBWyddzhoHnb7LmupWqvH79Fip4/hzUs8VD09ADM/Qrr2nKtGaplqPmbvQK
+ PojRLLNnyR78LGIkUNbY5J6dKepGENX8zU/h0ACPlx3HvUAAibI5h6NLCzkvTPTvPtfw6AAAAA
+ BG2RgroJdeR7Zc8wAP/EACYQAAEEAgEEAgMBAQAAAAAAAAQAAQIDBRETEBIgIQYUFTAxIiP/2g
+ AIAQEAAQUC8SsjGtQPkvy9LJswNurIi2Jvf6SzKRY3nzIhJ1J1J079KSLaELm5shyKiIeOUyrU
+ qqMr7ZOpOpJ5eNNk6Z4zIxKbrmj+CLNtRbshKaZTffTfXS0m3F8WaxdKMvYYec5W2Y8CwtWelX
+ PkItkn6xnpMy0tJ39iXSGvqm1lfyO/dmOG+0SO/ExdNnJHudCYfa/HCasxAk1bgIqzBlRTBkjw
+ U/Un9vpYG/cMjZyn/HI+ia+9oUOVIcaodvEyHILan/0+k/pY63iye+6Xx2ftT/4W+Vt9Vaf+6U
+ v8xvUJ6vFhsyjHOMWpM0o0O8JdJ2QrX2ZTXBdYoD1VN/XZ92XS27+3REeDLdb6+SLEzdcZFirG
+ qh1MnxjEDzqFZ9da499nyOjtvFs5h/0O21n7dzfriKuU88ZihWOJAkL8ig6oPHubyunx15CF0G
+ huc39dMGPxjrP4z7EZs8XxYMjKMfkLxjNkQUSa9/3w+Q38pfI0Y1M7vjhvtks2m6ZrCsSsTdLG
+ 5UgWgmNVcaq5RaTOLFlsitRJr3kjIiDSd5SQ9dpdoQsBKPAsMcuNUOOrwyBVI1WrDL5aZwcdcW
+ 4YlQlf6iXKkmxEO64QgiImHHp8f//EACgRAAEDAwMEAgIDAAAAAAAAAAEAAhEDEiEEEDETICIw
+ QVEyUhRhcf/aAAgBAwEBPwHtqaocMQrxyZX8piGpYUKrHcH1Fwbyq+ou8W9we5vBTNT+ya4OyO
+ 6pVtwFUqbjua4tyFSrB/8AvZWqW4CLlysoN2AVh7JQdBkKlUvGznWiU90lUtODlyAzCbQYGElB
+ AIBAJ9G7hHClXIZgpj7DKBnK1DvhUGXOlcJwIdKuqVPGcKnpwPyXTb9LptViAWupx5hTKp+QKG
+ BG2mdLIVYy4rS/incJtLqmTwmsDeO7UNupOCoCcpjbRGwErSp60jsEeoNtwNmiSqI+VT8cKPKC
+ mUDTfc3j1kQ1UxCGNniH+qs61hKFEgXOXO7clV2/KaZE+rUu+EM70RLk5twhUP1Kt9DjaJXSe8
+ 3OVSKbYCAnaiy1uzhBuCa64SESo+u9xXSLzLlVIaLGqjSnJ7OMhTd3uMbH+kyiJk+wuhZcdgJQ
+ Eesz8Lp/aIJwgwDt/8QAJxEAAgECBQMFAQEAAAAAAAAAAQIAAxEEEiAhMRATMBQiQUJRMmH/2g
+ AIAQIBAT8B008MTu0NC/AtPRvPR1I2HqL8eJKbPxKWHCbnU1NX5EqYIfSOjIbNqoYbPu3EsFFh
+ 4WUOLNK+HNPccaMNQz+48dOeo6X0kA7GV6Pab/OlNM7ZYqhRYStiQmwnxKuKZagVf20A0Braaq
+ CouUwixsZgk5aYip21nMpupW07VND3Lb/sq4z4Sd+p+wYmoPmDFn5E9ShlKsrbDReYtfdmmHWy
+ CY07gdM4oj/Y9Rn/AK1UWyuD1vLyuMyRRYTGjg+K8v0EYXFozewkR8R3Eytz4/iDrTOal4qYuw
+ iOGbKNDGwvMG+2WOuViPFhF2LaMQ2VJTfI15WpB/cIaJEtbWq5jaU3X+F0Yl8zW/OlCrb2NCLR
+ 2sd+I1MEXXXhkst+tarkGinW+rSouZYCRxqpU+4eruEFzHcubnSrFeNVOmah2gC0lt0qVlSO5c
+ 3PjXJ9p6k8ILRair7m3MfEM3Gn/8QANRAAAQMBBAgEBQMFAAAAAAAAAQACAxESIUFRBBAgIjEy
+ YXETIzBSQmKBscEzQ5FygqHR4f/aAAgBAQAGPwLZIi3jmrcjnOdg3gAuWQnsrxIPorpgD81yu9
+ Gsrr/aOJXLYYcMdrypHN7FU0lloe5vFWoXhw2jFo9DJickXyEuzJx9EPicWu6Kw/dm++x4MJ80
+ 8T7dQA1VPoWmmhF4K3v1W8w/Op8rsOCc95q43lWuSH3Zoqxavs2rsO6psX8NhwTZGYYZpr2crh
+ VRwDgN4psfw8XdkYX3WBd1CebPG9COIEk4BWtLP9gX6DVyFvZy8qcj+oLdMb+xXnx2W51rqbrf
+ Afh3h2U7vmop3Y3BBw5mqtqzGMRivKYB1x2pG9FTYhydun6ok4qZnZ2q3+2/m6HPb33fTXVAJs
+ mRqomP4WwD/KEsD9zFpy1EHgV4L8OU5jXvuAXkRud1NwXnS0HtYqhv1OqioFXU6uEtf87F1zxe
+ 0qjYXF+OS33iMZNVbNo5uv1yHpReK/dqaAK7W1oxNEyYcHCh7qOT3Nr6N6jiGF52I8m7yfHjxH
+ dWRwBvCpMKKrXjbLqE9BijpGkNoXuuCqcNZldzSfbUZoRv/EM1QqY6NIW6TGa0rzBCHTXOjHAk
+ jgr2tlHS4qjqsdk65XbAiBuj+6o1WnKz+2295/CoOGsy6NdJi3NM8cFgO4+qpPEyQdQmxs5Wig
+ vVHAEdVWJzoz0V4bKOlxVHVY7J1ytVBe7kCJdeSr0IoR/xCNn1OZ2aaRGHdcU1gJIaKX7PnUcT
+ wZmiWt/00I0NRmqgWY/eVZiHc4n07OjtY353n8Iy6XLJM7oF4MEbdF0brxcqyea75uH8bP8A/8
+ QAKBABAAIBAwEJAAMBAAAAAAAAAQARITFBUXEQIGGBkaGxwdEw4fDx/9oACAEBAAE/Ie5pKwgb
+ 9PLmGBVcL5+tekRZ5AAPvFU8RT+xeuKfpEAUI7n8NWYtMnkSqHizI8fzvATXl+D0iITpHo0+Jz
+ KDk6neZSPGqftmQsWi7doqi2MvdGiN4BUgabdP53Nsjg2fsRQNhS7SaJWmLASpUIJrlY41GVqh
+ j5PB2ZeK4cuxLgxsY/nU6jPRG1upiJlrPWqu2HT1m0iixZdqFhZpCSKkcYmvdq4NyLlYBLsYfO
+ cH36y2LDmIxgWZ7ssbBttpxFjIr0j1aeL8v5AqYPOBYby33GW8UW+5NOjo/MeX2Yo+DKDWOTJe
+ YRcPPqWvv8zqDHQx9Q/GvlnipitzcibIq3Oj9lEZddS6ve88p1MzSDrL2sIoReamp/8AhPeovU
+ StgDdUD4frs0/Wod8GIN8MsFtYQ4Ho9YulJnyYYK0X6GDYczU22vfsNOwpIra3Jv8AGO0y+os0
+ M2Poke8rhs8RlWLvMQ0CcGEay1ZUoej0rbuVt7PgGG1bg6B6z7GX1inuI7eRcHVxNjjZrvn2iW
+ 5Y7Ki6iIecq97af18QRthdd/4QpQabL2gItPraShrK7NEXl+WnvUZNDy8DSZXjBcUyptuXHuf1
+ CY18uPXSCJY2d4W6GgWrgmCBSuf+E4rhFL37KocHw/3TsyOgx6jk8Yr1CFWdohfHmyPF7gC9lN
+ yUeoXBVvhMCC1Y7ncPBXT1a/UKJvxZqK4iEfgP2YABQwB212NfQOjxjZolKofxphLZDWOjCZSo
+ KaOrPCzQuKdUuPSeqTAro0woOAcjz0ihKrV3YVfCUD714HKwn9M73J3eFXNA84ZZALto5e7fh4
+ tMPj9IY6zYgCqHFKuHuVjjy5ms66vufxgPokdB9y0MNo2flmS25F5zURCT3EAAoKDuf//aAAwD
+ AQACAAMAAAAQkkjRaEkkki6RagMkkMw122ZkkVsEe43UnzMsoIVRiqelxLXtd8QAAdbM9EAAAO
+ YYbEAEkOpoQAAkAtCZDAAAA5w91SgAAeBgklMAAE/8kYCAArvskhUklhwkkkkkLUEk/8QAJREB
+ AAICAQMDBQEAAAAAAAAAAQARITFBECBRMGGRcYGhsfDB/9oACAEDAQE/EO0TVb54gsbntgPjc4
+ qf77xTNnp4GWo98Rz7wIECBBrXTBTA+IBbfcfkRKc5YEsJlmB2q21CacdinydJVWxtqZMwJbEM
+ nS5cYYkzBv5N9CRRGXbBNs2kGnILf95gl3XzN7RKpjLAoEcTAANM1ffMk0RoRkzm5ULnwvEymT
+ PZTwEAagECvPh/yXwIIDRFzNOJaIMmC2EfR/tA6Nd304v4zGS9QKkuM6I6H3YrVlni+illMA8R
+ LmCmxSlRxMz5JaXLY+itZi3mZR3KVu2EFEuZr39L6AfuafeCIbS5cFBNUUfokHcyhgcCXLlW+I
+ SKZDuIoiV3iilxgueV2NZOJczDt6KG8/MCBQ51EJfezURtevafOCO+PsBf4ZQYg13U5dxNeUNs
+ MCiu2ue4jmXalx4AUenwwC7dzCCibjPb/8QAJxEBAAICAAUEAgMBAAAAAAAAAQARITEQIEFRYT
+ BxkbGB8KHR4cH/2gAIAQIBAT8Q5RNJ26zSIfK/OoJ1P38RIwj+f8mWfjn6iV6LtGIbGBAgQOGu
+ mKyq8MpRTzJ0X2lQFQIEC4HLXKyO7Pp78lzov5gBFtcsIZgqLUByXAaLGPg21/XBCHWE9BOsDL
+ 2IEwiPLLlPYz7F8DUWLOoly5c8xB+jHbYTAvtMqbirt2w+HNVB1A6qF/MTT+Yu3ebS34n9ahuW
+ RG7MuDLjFIHWUfx95jMhuFQF/T3i9q+b3Egy6jDF74zCAJn9FMTIuMXcyzLXuIh2pf4gsPuPRC
+ 5dYji0NFsCscKOu1f89KnTayog742XZLEumZ4pfRFNSlHXHJY+cRyMTG3NVmKVPOgCbLdEMvHC
+ dfbgIsw68RFTDINr0KOa9fqXUO8LHt1N8aA09Ht/k1Ge0yCi228r09DcACjjgY8ttkW2+Wmad5
+ +mLC0tmNcvaX59MjLXwf3GioInZ9Z8zH4H715f/8QAJxABAAEDAwQDAQADAQAAAAAAAREAITFB
+ UWFxgZGhECCxwTDh8PH/2gAIAQEAAT8Q+igVQC6tHWFGSLwL/wDM1JWcuSbMUrXRDeXBcIBK41
+ SZhUQNuh6TRVmtA+4B80JQpEkf8ML5T4i/pg5pfnQvaKenR5ay/Dlqdpu3oYgDMybzg9ysagoh
+ cqu7UgIVgWXZLnf7KsZWS9jT0Gu1NVM/NAl/6PhyXq/vQpFFilnPzFY+3tk2TCWw2aOOfRsZl5
+ 79xN4+VeLLdddnRei+pW4FdahygXd3VqQmO9G/DkP7U8cOlEYpDCw80UPmDVlSMAuI0+QQBaOn
+ A6mj2mjuh1Xt3GO0tNRf1Jf5QwW0zHMh/uOuKMs50bOKJBUWYTBILxcKWa2qEE5an+ehZFeqMo
+ FEia/Hw1o+9kFNmyxNA+XhPDDpUnmDwk32eKbBzd5g7BQrsJywEscqh3mjuqREA4BxhpLoLzLF
+ XNOQksncgUDOly/7f55UUMIiVT5WaSjzffCT1XAgN5oflSToSRege6iVoI9LNrm1JRsFCpgaKv
+ SgtmU1x009293IBwQao5huUzZvgUNYKM7AJ8yeKWciqZNblk90unOYFtp2I1U7BmH5Qj3ku9Mf
+ YmCVXiHspxlllxTQXAfHoBR5VmAkAc3A9naoo5sW6y051kdwn9PP4OVACxpdDhouW+yLMYfzDH
+ eneSrmIrjoE1SBQjkF38rRuscQJ+UW+b2yID9qZxVqDIANjCTGCXX4GO4jUad2PwWdcPmIu0Lj
+ 0MtMgLgD2OfVI5l8hDp/tNNlxlDFrfHal5Elan3lk8tqAEnX3avkpLTRMCB0sD6T6O3+fEeHDW
+ XZ5Aef/UUZPMnPmw9Ky0brSc3t6oAILHwaTCfLYe2hQrB18iUaW1X9SXNCJOsUlNB0U6NVAftL
+ kdIYwr1hRCJ6ERZ2ZP8ACk1LQmW5zU6qQzewdYF7lIRUSgctNEppKUqaa3nSJsJfXOmjwtRkRO
+ UayMW3yND4/EMu4eqBDsEvQvtRMSEiMj9l5JumiAarR1M8LZcaABe+Kcw1gMDTgV1Ab0lPCkLP
+ Q/0q9Pg2E8NkWLURjU6QiXHsNyg7QpDTYGAmbMgxmtprml2I3VMMzBe8QS06N0bPaj+6zXnHuj
+ JBSJI/QSTNTFt8AHUaCNRmwLvT5lYn7oLmcFpoDuW4JdKLUUAgAwHyOnZtE6ugWphzZmX6E5Zy
+ 7MRA06DRZRkD7A7NBY06QsEisFrtLUDQD3SdMvCreVZpwuaveRt2KPrvmvOPdDGZmSReiZexrT
+ bRPSoyrzNGGC6gy1NRZDqdAH+r1NeypCsr+GgB9bZOSDsBD2xUmNbuEG4xr9XogwCN4cHL+2ot
+ ZKzFyZVYCbv7U5SCRBvDcpnc38Ayult0o7Ggb9TVfhg/xom2zLhuRl6jpVogiHSxJ2iOlQEARH
+ QJz0Xq7X9hQAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf
new file mode 100644
index 0000000000..a56eb3ed97
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI PNG, version 3.0 with correct value type
+NOTE:This v3.0 card has a PNG photo as a URI and URI valuetype.
+PHOTO;VALUE=URI:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA
+ ADDPmHLAAAq0klEQVR42u2dB1RU57r3XXd965R859yP9d17Ts45SRTpvTOFOgiKDcUWY4kSe4k
+ Gu7EgNooggp0+gIoGFRRFLMBQREGRURBrDDG5OelO2knul3vO8z3vu/ee2XtmDwxNR2Wv9V8qI
+ s7M7/8+7X33zIAB/Vf/9bxfQwseWYTlfRgVmvtQOSTjzvWQ9Lu/BCdebg6KOT+o/9V5ga/wIx9
+ FDjv0qAThA8KHIQda/1ux7vQ/gt8rHN3/6rzA1/Cj7VEIvx3hQ1g+wlc+hJDNFyB4QU5L8Jx0C
+ /I9gfvuWAZurd4duKmivP8Ve3HAK1DtCB8I/KEIf8g+NSjeOwwIXk3gB6feGB20q/lhQFrLPwP
+ XnvkZv+Yh9rO8t95QeMU2Kfpf1ecDvAWqBAUC+Duv4KrPJvA1iqQrbwWnNbcH7b8DgXvbIGBrD
+ fjHN9TJk27WyRJufC1NvAm+8Wrw2d4MCB+8NjeB56br4LHxGritawTXtVfBZfUVcF5ZD07LL6s
+ coutU9ktrU+3erYm1XVQdabNA5dFP4tmteg2FX4jwDyP8Agz7uPIpfBTC/z744F3g4Pvvvg1+q
+ a0gT2kBWdItIPAlxuCvR/jvN4DLmqsI/wrCrwfH6MuA8AHhA8IHhA/W86pg8JxKjeWsCtXgqEv
+ RVjMu9BviaeR6Ap6B366Dn9EKITFlEJJUD4qMe8CHH8CHn4zwdyD8BDX4isB358NfhfBXIPxll
+ 8FhaR3Cr0X4NQi/msK3mlMJCB8QPiB8sH77PNhMO9duO/Vsqt1bZ/rN0AfwlRz84RT+Rwgfiz6
+ u4s++DyGZCD8d4R/QwfcXgx/XDD7bEP4WIXw3Mfjv1YH9Ej58FcLH1T+rkoE/8yIHHxA+IHywn
+ 3waHCeWfIq/T0JzBPbT6034ZPUbwH+A8O8j/HsI/y4E7kP4exj4fkbgexP4sQg/BuFvYOC7GoO
+ /GOEvRPjzdfAtEf5gDv50Ar+MGoAYwQq/ZvlOhVZolP+HOo6/j0JZ9BPtwhWW3lopgM8WfbTd4
+ 8FXaOHfQfiY99Mw9O/C1b/TBPjrGrRFnz58Oz78uQh/diUDFeET0NbTyxnomAb40DvQ9yglqr/
+ jMHa5j9pv4T5yX6z/6vJvxeAPZXv9ITkIP4uBHywKnyn6JFzFz8L3MgKfVPyOyxF+tA6+rTH4M
+ xj41m+X49cumQpfIKuZF+6SqNBPnL3cItItULFuo9M1koXFYBJ8XtEnBl+03Yu5LgrfiYPPVfw
+ I30YPPgn9XPgnq74LK9+oMIX88FIbwXVMlgUqFqVBgdfMYzCs4KEOfqEOfpgR+Np2rzP4/HZvL
+ RZ9q6+Kw+e1e6Ta7ylkk4wwrexnpwkn575U8F0ic6OcI3PbnSOVwCgXQg+2CuGz7V5Y3kMINQL
+ foN3roNc3aPe0vX6dEP7cpwefL8eJxU88Rh4a8UKDdx6X5+E0Ll/lND4fnMYXaCXfWAPCXv8jH
+ fzcB7TdU2QawjfW7onBdzOp11c9dfD6QhPUvpBdA4KOdZpwCBwnHGY08QiV+5xT0HGv/6B7vX6
+ sib0+V/ET+LMqnrkBiGynnP2n91DlkhcCPMK2RNBqAtthUiHqKKtjVIo9twRFn1ivb9juYa+/y
+ 7Re32N9A/iQwk900GNY7ZuN0Iw4Y/j8uY4GCD0KoWso7DePgf2bH+DErEgrz+hL4r2+UgjfWLs
+ nJe2eHnyu3fPY2Ai+GAFk+Ksr6fVXMvAJeLLa6WDHnICz0HWqpBr8zqV/OUwqefe5g4+rXMmHb
+ jf5OI5MT6BOMppyEsJy7nfe7pnS65Oij4XvvglXfuw1kOKf5dj6ua1h2j2HZXVgvUD1XEDXwp+
+ tE46aC58X8BYIXmX/JlnlLHSEbTulGFWCYY3oFPhsqDeAb6zdC9hrvNf3RvieuPrdtzaB6xZc/
+ firDItAv1gCn2n3HBH+4DmVzx10KjKGZmU39cx1M4d/zBJXvFoHnoN+Cvvd06xKwXnRRWG7x8I
+ P7ajXTxXC98HQ7xHfDG5xN8BlWxM4b70OnvEIHk3hhwbwfL9Ru7VrZQ7wuwl98FydrDB1OUw+/
+ ZlZ1gW46j0w1Gu04KfywE8vxRHqGdRZsJ11nq54g14/t/NeX4pFn0/STfBIbAbXhBvgHNcEjtu
+ vg8M2zPk7miEg/ib4bcN6YON1bcVP2jtzg24AvBPoVPNZ4fPBTahvzcoEuOoR/nENye2CFc9Ct
+ 367DDdRyObJOQje3ybS7vF6/XTxXt97501wS1aDC4J2TrwBDgnXwT7uGthuawT3JDX4J9yCgO1
+ qkMY2aTd4yJTvqbd2fQSdyHohI2qCKWe/MQsTEPh2byF8kudx1RuAn0HAl+Mc/Tz4Yqgm8Dve2
+ r0j2Nr1TbkFrjvV4Jx8Axx3NIFD4nWwS7gGNnGNYLUd+/uduPJ33MKjX7j6tzbTvM+1e6S/Nyv
+ oc7oLvQasF7FaTFRLW1eXyKL7ZgD/hIYWeHTVs6FeC/48gr+Ae+MXcepWI2z3Oun1Zdjru+9Sg
+ yOCd0hqArsd18EGwVvFN8DguKswaDuu8pQbEJDcAgGJt8AvTg1eOPTh9/qkx7fDx2I3veyFgG6
+ DU0ubJXWM8PfkcTwzEyB8C4TfLoR/lg31zIon4MkJGrJ1SsK6Qa+fI97r+6a2gCPCtUtuApsdC
+ D2xEQYnIPS4KzBwez0M3HYZnHc2QUBKK7P6E26CdPMNba/P39cX29p9VtC1wE2CXmsIHUfXtu9
+ dBttoonr6c8jj8g7Pq3kW8NVa+KTIe/sss+pn6lY93TPHF4tswnS6tcvCd8NVb5OM0JMQemIDD
+ GLBW26/DJbbsJ3bUgvOOxohAFtCsvr9cfXLMfeTYY+jCVu73JYu2dcnhzvIn80JOgXeAXTbZfV
+ gh52N3Yor+Pe12sfpPUyZ/NQMgEMdpRB+mWDV0/1z8sDwRRk0r5K2cZ31+v57b4NDShMM3oEhP
+ vEqhnp0eNxlsELoVlsx523BFyhWBY74tYDU2xCwE2uEHS3Y9t0ETxL6jW3t8uFzhznZEz1YSGG
+ 7WopDKhK9zrHRqpvQ5/YS9KUdQ7fD1tZ+1VUquxVXdQdNMN16hysn9f3qn3w8min42LCvD59d9
+ ZZzEf6CKhi0qErX7nXQ69vjirdOwLYtHou37fgibMNZ/VasdjerwDYWhyAxleC0pRoCcCAUsAt
+ nA8m4H5DYApItzZ0d49Y7yVvOO8xZipPK07gnUYIbVCdxZ/IEPdxJjdBj6NV9At1+dQPYr2kAh
+ 7WN4ICzDn60wsf+T59huYP6EH6RB231aLXPhf1yXsjHBzKbWfWDFiL8d3HFbrhquLWrB989+Tr
+ YxuHW7PYasNtWDXZbVGC/uQrsN+EINOYSOGy4CM6bKnAecBMCCXy6+rE9xNbPFV+MLh7jFpzkp
+ fAnFlP4zpFF4DL2A3CNOAqO+HVrUsM8LejLOOhXhNBX60O/Bg7rroHj+uv42l6nj4NvArcxRzV
+ 9XPQxrR4t+DDn61Y+G/LnM6t+0BIVDIyuBoeYhg6PccvTWnCQg9+3FadcW6rAMbYCnDZdAqeNF
+ 8FpwwVwXn8eXNaVgxxNEpjaRkO/XxKz+r1jmrpxjLuMB/+UDv644wz8MUfBbXQhuI86DB4jDoH
+ zhBNg/c7FrkNf3EPoa4xDd8BBlwM+d4dNTWAl0upKQ9LP9EXoTyV532bqadrqkWrfAD4J+YtVM
+ GhpNQxchlpZAw6bG420e0yv7xGPmzWbK3CFXwLnmIvgsvECuKwvx8LxHLitLcO+/izI4uohcDc
+ WiSm48pNugxzhy+Ju0tDf1WPcdlP04Z/kwT8mgO85PB+8huWB99BccJ5UjPVEZR9AFw/tWugI3
+ HGjELo9Drvs8cCL/ZYbYLXI0ACkHpCGZAzvzdWvYEI/r+gzAn/gewh+eQ0MXIXhfP5ZkEed7PA
+ YtxuGdreN58FtQzm4T00H9yFbdBqeAL4rSiCQ5P1UJvST1S+Lv4X7/Ne6dIxbB78Ut6ZZ+BNY+
+ JF8+EdY+AXgFc7A9wnLAd8hWaTSBruoC92CTvN5Z9DpKudBj9FBt9twDazJ80XodrjXYYcbYLb
+ bbxjtXJwmFf/am1U/7fdp6OflfVrwzWbDPg++3YJzIBmVAwp5CoTgmT9jx7hlSc0I8hxu3JSBx
+ 4TdQvisfGcqGfi4+v2w8JNj5S/ZpqYtn6nHuPHgJVPxa+GX8OAXMfAjWPgj+fCVLPxskIRkgjQ
+ 4HVdWJjiTKKjt0WuF7VpvQN9kuNIJdPK8rPFEsy2ef7DFjTCbhGYD8INnM7KaUwE+I/OaeqPwi
+ +VWPw392rzPVPu04FuEYR/hW0arwOPNQggM3gNBAWkwaWwGKPMbjB7jluJUz3tNKXjNUmqBLww
+ fC3EjfWHfDF9YM3sSeE06IIAvw8LPDU/2dtjrR13ktXtl2nbPHuGToo+Bf4IH/6hx+KEsfEUGy
+ IIOgizwAMj994ELmqDXoccaQudWOoHOgbbGbW+bHWpsj68zz5cDP4cBbzUXvwe52M65CPJhmd1
+ PBbjDZ0Hm/Ezhx7V8vNBPWj1S8C3Flgu3eH0jlOA/ZD81wNYt5+Drr7+DX375xegxbmlsNUhXF
+ IPHqB0U/lhfb3jlf/0b+L72e8if9yq0K18D76lZCB/zPlb9soQW8N2qFm/39Hv9t4W9vn67x6/
+ 43UYT+IfBcwSBn09DPYHvg/B9Eb4E4Us5+AH7wM9vL/jLd4MbRkRBu9Zd6JsZ2YlA51a6TaLOA
+ IPxuVvh/ogVHnzhVjuBzoG3nl+JrwtqYSV4Tvrgxx7k/iIlWf02gtXPhn4u7y8h8C+AdGQ2+IU
+ dhGERWXD69C0KntO4zLuix7hlMVXgF11E4YeFzQGNRgMeHh6A/zWEuv0Ovj71GkiWlYGc5P1Ej
+ BhxLTjxazCAb9Vhr3+GB79YFL77SFL0FYBnOCn6lOAdlsvCz2LgY+hn4O9n4e+BAFkaBEpSwX3
+ 66e5D39I5dLLSrXG30xp3QrVhnigBO4C19Sz0CgrdeoEOvO0i1OJKfK0qwS88c2+3DneQY1yC1
+ c8Vflzox7w/+N0KkIzOAXl4BgwdmwOtbZ8J4BOtKX4kepJXvv4S+C85Rg0wUj5KC59I5vhbuFX
+ ojtV+MwMfV78PDn30j3ET+IM5+DNN6PW5ij+CbfdGshU/B38oC3+ICHx/Ifwg3xQI9tkJrvMud
+ A06C9wU6HSlp9wEq103Bfl98Pv1YI1zFv5q1wdvtwTnKEsrwT3q1K/dqfyVhrmft/px0GP5bhV
+ 44RFv2YgsCBunFIVPlFP/megxbtn7mKMWHwWP8DiYMHQWBL7xCoX/x9//G9Tu+TOsSE7Qwpdsv
+ 0XbPv4xbn67Z6Xf7nXY6xtv93xCc4Twg/Th74ZAKYG/i8IP9k6GIEkKOOHZQx30JuPQt5sAHQ+
+ 5aKHj8GtwGiq5WZDf+eApdBHw9u9V4nicqAqCQvfndOVcnyU5yKmr/PVW/3xmyuc67ThIsdoPG
+ ZcLrXfE4RM1PHoieoxbsrYCZAsKwSsSX9DQVXBu/utwa+drcCvXETalxfBWvhqcSK7Vb/fYk70
+ 2BDivwrcnOZ/f7on2+keMw2crflL0MfD3CeAHIvwgFr7CMwlCPBLBPzRdr3LXgx5nCN3ayEq3X
+ FIDgzfhZtjuW2CJx+Yt96ISm8TBE+jGwC/DwRrR8irwmV70r64Uf7HkaBcd+Yqtfiz8bOafBwk
+ WfcQA2xNzsOD72qgBrqIBxI5x+6yvBcm8I+Az6xAWe7jiFp2AiNgiWvHLsOiTYL/vzvb7+u2eL
+ eZ50r+Tin3k9CKIxnHx/pxrsD+7kepAVgMk7qmHDYnV8M7qCzDx3bPgN6nISLun6/VF4fsx8AM
+ IfAmBn8LA92LgD3FPwJolHnzePmkIPd406NqVjtAHv1dDV/ugXc0waH8LleWmBtH8TqCLgkfoj
+ iuq8HwEamUVHpGrgjBpUrqp4V/DFH/cyLdcW/lzq5+EfmKArTvPQXp6Opw6dcq4AT58InKMG+/
+ g2XgFfOYcAe9Zh8E/tlbb7slw0ueJq4kc7uD3+qTds5mFE0MM52T1rt9RAy13Pjf6/xpT/fVPI
+ K+ohZpj8rwSYa/PtntyzPsEPi36KPw0Efg7tPBDXeNgiGcirv5rDPSErkHnVvqgfWiAZbW0sCN
+ 5fuDBFhiY3grWy2uMhnlR8Ct14J1X4+mhNVXgH5nziynhP0oX/pnij/T9dIuXzf2OUafBd2weT
+ F1yCn799VcKn5igublZ9AU/c+Nzo3ftes06CtLoM3R3j/T55DZueo5fb2vXFqt9hymldPUuxd3
+ Bjx5/02XwHelK48c0aqzA8fPYiQW01+faPS18UvR572TgezLwh7Dww5y3wVCnrSCPyGXyeSfQL
+ fWg05V+oIUCt4qu0a72N4gB9qg7zO+O+uBZ6Bx4l7VVOFqvAt/ZJ2CYx/bozqr/EvHwr6v8PSc
+ WUgO03f+SGuDJkyeQm5tLTfDZZ4a1wMG6+xCULH7XrtcShB9H7t9v4r1DlxC+3awLdGgz4d0yq
+ G/6pFfBG9PXX38PDVcfQcaBOlizvASipuTrwU/UwXfZTuEPc9wC4Q6bsQ6o7zL0gemojFZ4A8U
+ P8wPTsPjbdKXD/O6kD36NELzruiq8NV4F7lhzDZUkPuzoVi4LciePrvfnhX9S/GHfbzenDHwiC
+ 2DTrloKn1NbWxs1QFFRkcGLmVJzG9aVtRjAl+Ikyw+Pdwtu3Izmbe1iq+c4+RTI8fHknrj9VMB
+ 3praWT6Dq/G3I3lMFce+XQPSMPIiU7qDwhyH8cPtY8BuT3Sl0mt9TmylwqsxWeD2rFQbuvSlY7
+ dYb603K71rwCN3lfSF4tw0IfyMqRgWKYfthmOf2QUYMUBilX/0Lwj8Wfy7TTlADtD34UmAAonP
+ nmHqgoaFB+4LVPPw7lDxso0pUtcCbmc0wKVMNs4vaYO7ZR6DY3Wpw164d5n37qPOY64/D4q018
+ NEn35oF/M70zVcauF57H3UPfHFFG1vpBPjAlGaa1wn017Nvw+s5t+G13NtY7V/Trfalpud3Lfh
+ 1RCoD8B6bUHiiKjAyG4b6JhYby/8l9rzhD7Plqwv/luhM7/GHYEp0qQF8op9++skgFaRU39Yag
+ Cjz5l3YXv8All74EN46+RGE45RQcNcutjmO2L6FziuDyzf+67kAL6aN1Z8YQCer/A0KHA2Aq5+
+ E+YG7bsBrSoSf1wZ/y28D27W1gvzuYGJ+p+DXGwfvuZmR34xCCA1M+Yex/A9c/meGP+WC3t92b
+ jk1wKGSVlEDED18+FCQCvJu6gxwuO0OpFy7D2urHsI7pY9gwvF2Kh+8j88Bq16HqWfAd0YppB1
+ ueW7Bc2r+7DsD6GSVcyt9EFnpuKCsMMT/raAN/nqoDV7Lae12fifQ3TYaAY8nrLyI8NCNfM4Ho
+ AjdC8N8EgL07+OPJHfzMgbQy/+zmerf4Z1SagCx8G8sFXDwix+0wUH1Pdhc9wAWl38IE08w8BV
+ 4x4/TjDJQ4H5C6pFW+OrbH557+JykR+8y0HMZ6GSl/41d6VYx9dpQ/9cjd+AvhXfwEOzVHuV39
+ 00i4BG61zYV3kRLVA3yecdBMXQfhMmSivULwFhaAOKBT/70T5f/8T/DyV/w9GMdwuengqy8PK0
+ B8lrvQlLDfVhZ8RBmnP4Ixh19BJ6YAxWLL8Gxi49eGOh8Lal+LIDOrfS/Hr6DO4gqbah/PfMWN
+ YDDmuoe5Xdx8NXgHVcNPkTxaIAFyDD8AIQo0r7SLwBVhv0/M/2j7R8OfzwmH4M5G853agB+KiD
+ wi+7fgX037kFMzQOYf+4RjFLeBx/cRTx26aMXEjyn0x9+I4DOrfSBe24I8vvAPXgcfmcjE+ZXd
+ T+/i4KPZ+SbgMLBlxynrUEjDkLw8AOgl/8LgW8AQQE4n9n29cbpX0xanUkG4FIBMUBOy11IuHI
+ fll38EMYV3IO38J6+r7798YWGT/QQnyMH/S9H78Crx+7Cqx/cxeNddYLCznZbfa/kdz54yfvlI
+ FldpgUv2YFKqgb/aYcgaFQGBI1Mh1C/5GjuDZw8yPv20A6APwDi9f/k0AcxwIEjzSYbgKSC+It
+ qSLt+D9apsPA79RCiD9584cHz5VX6kEJ/tegu/Pn4XawFWno/v+uveIQuXYV7H28VaMFLk1E78
+ b6KSXkQODoTlQEhwakqxgATDkURA+DpH/EOAAtAct6vqwYgKm68B9suM23f/JcMPtH0usfw5xP
+ 34M8n78Gfiu/hzS71fZbffdjV7ovQZdGnwG9CHsjWnaPgZSmoJBUEjM2GgDFZEIgHdxRhe//OG
+ SBW1wKKdwDkjH93DEC0sPAmjE+th9ra2pfOAHEtn8OfSu7Bf566jy2huk/zuy+72iW42mXzj+P
+ uZz7IlxSDbFcNyFNrwG/dWfDHrfuAyBxqhMCRGb9yLWCJeAuomwASA3hhndAdA5BUcPjwYVoUf
+ vzxxy+VAao+08B/nr6Pq/8uOMbUiOf3DV3L79rVrpffJWyYl+Jql087AvLJh0A+twhvvkH4u1E
+ LisB/Ap7bHJ9LjYC7g8AZQMUZQLsHoNcCkhs93KafgB1ZjV02ANHjx4+pAUh7+P333780Bmj66
+ ntqgMG7r/Vafteudr38TsP8LlQcHrSZegRkUw7jPsoh8NtTg7utlTQiUE3MY42AR/aD06KJAdo
+ FBnhbzwCLGQM4v1Nichsoprq6OmoC0h28TFHgb4daejW/09VOwOMNNdIdKhY8E+bJapfjfRYyj
+ ADUBCj/fWiANWeoGeRvFoAfEWsERcieeGIAYAzAHgLhG2CuzgD2C8pg1PzibhuAiIyIiQkePHj
+ w0hjABUN/b+Z3GuaJVpTiucpzWvA0zONql+G0TzatUCv/2HJdRGCjgnxyATVD8LD9KmoA7Taw/
+ hSQM8DyajypUkFHwU80P3XbAF988cVLlwpmZlzv1fzOrXjp/JMgW3pKC95/L/66A8P/9KOoQkY
+ 8I3ARgYgzQtDwg20mR4A3cKfKa9IRqLj8UY+iQGNj40uRCkjBW1FRAaPiyw3D/Nau5Xcpl99T2
+ VCfVAXSmR/godqTFDwJ8wH78evLT4P0bTQA0fSjOjMYMQIWgk86NsAcoQGcZ5dAUkZDjwzATwX
+ GjpE9ryKHY8kmGNf1EI2KL+t6fhcUdrz8zoZ6+cZykEZ9QE1AwAccQKWiKWYcY4QG4CQwgsAMx
+ AC5fAN0XAQOXFMLVisqYPTckz02AEkFJA0QdXSi+HkQSWW3b9/Wno3UV8SO8i7nd33wNMzzQr1
+ sUTEaoIiaIPBgDZX/Bhz6zDymM4EJRiDFoMAAujnAeeEcgDXAGxsvg+fkQnj8X5oem0CtVnd6o
+ ticRQpZEuLFoBNjV1ZW0k2xOfnNXcvv8ZUGhR0N82yo90+sAOk7RVoFZdRC4B6VNiIw0jeCzgy
+ 69MAYAQ1wCIxNApdn3dYOggauJgbAO2Kjz8PBwzd6bAD+iWL+MTJzD/FkoskP8WLQ+c9x7qHmL
+ uV3KZ64EoBn8zsX6mXRmOdnHWeEBgjOqgW/laXaiCA0wgdCI8wwNAIZBQOzF3CCdysYY4D2z3+
+ Ao3WfMvf+r6qBN3An6/Ut9RC+sKRXDNDZiWJzCfGkVuHqFn2RYlYfOl8RB652mN9lvPwu33IRJ
+ Jhi/VJUOvAIPfAAG+ZxoCPB490ShC9hTRCUIowIRo1gJD3gbiBjAOFuIGMA7kncatdAeEoTvLE
+ eDbAZz+1jW9PY/GmvmED/GJm5iOR17nSTGHRyEpqMuTt7fl3J79J3T+HdUsXgj+1cAAudKCi9l
+ oZ62fJSkMw5wZiACk0w74QgIgiNIGYGoRHIdjAw5wHYA6HTS7X3A+g/mbMtX8KsI+Rt3Rpg9O6
+ GXjEA/xjZs94w4lo3Lip1Fzpf+v27sfwu33wBJPNLUGiA5AoITNeBD87EO6e2XaDRQTLnJGMCP
+ SNwEYExwnETooLOABo8ESR6IKS3AJt6jOxZbBh1lNdJVCLFKklV3UpxP/4s7N/1wXNhfg/WB4t
+ Pge+CEmqCgJ2VEITQSX6n2o8mWljMGIBTB0YQmqFjI6AB8lXkQ530zwSSewKelgGe9oYRgW4sr
+ /cUumDo1f6NoH83AM/mdxnekua7kBiAMUHgrkoIzq4FBVEOhv5lpTQ1aNWpEU6YbATyeX5oAN4
+ 4mDcMepoG6OsNo476dbL6yYSyN6DzVXn/C+Pguf49vgJ8F51mDMAqMK2Kgifyw/6eSQ1MeujYC
+ KakB6ER0ADKWMNZAFMI3nr05KmboDenhBx0sWKOQCeGI0OpvnoumVceiYNn83tAmgokS0sZA3B
+ CA3Dw/eMu0ojApQaTjNDFOmGAc6QymmsF9c8F1rV+9dQNwE0Je9IaGhvSkJ/b19D52nrpjiF4L
+ OpIfg/CP0uWn8XcX8qKMYB05VkKP2DHJUFU4NKD0AzdNwKRnAyCXCJzFbpOQHg0vK71y6duAP6
+ UkEQDU+sBDrp+BW9sQPM0FHWsiQVfqwVPiroQlHT1OfB9F++GereUEWsEv83nMS1cMogKXTKCi
+ XUCnhr6YYDL2BwL8nm+2kJQuy18Fi4/IwPwW0Nj9QA/vIu1bc8KurYD+OlnbRunreixqBuedxl
+ m7LmM90acofIl4hlBsvSMICJ0zQim1AkntMJt4SfcBzu3i9UBKcfvPbMXkH+WkBsVc9V7bwxo+
+ lqqD7+g/TsHnoT1oTl1cPcLDcxNqwefpWcZGTGCLjWImKGnRmDNgAdE1YwBIpUlgjqAvUP4WRq
+ Af4CEC+Vi0ElVby7Q+dpedVfbxhFNy6+nUYHuD+xGA7x3VmcCfSMYNUMHRuhGnYAGKKcGwDogm
+ tQB3EDIjn2PgHkp1575C8m9+URf9ep9pXAM9Rz85Au3BX/nE12GBuDUC0boZp0QOCpzA2OAsTk
+ ezuP4dQCTBt7CGxnM4cUk+byvevW+0Nm7n1Hws4810pAvqA2+/xkNcI4xQUdGWHrGhPTQszohz
+ CPOUnt/IM4DNI4iacDcX2xz1LqLrdQEYn/XcPdL8Fl2jpExI3QUFXqpTpC9fex/BDeIYhpQatP
+ Am0XMWBingu1//74fai/qQNk98FlerjOBvhGiTTTCEp4RulEn4KHQx0IDjM2JpGlA0A2UQHnDp
+ /3gelFvJdeBNzEAJ74RlokZoW/qBP9xyiMGbxOD3YBG1w0wxeCuorZ+cL2kb3/4GbxXnGdVLjT
+ C8nPGo0If1AnBij2BBgZwicxRCodCJ2Dqltp+eL2kkquPwXvleUZ8I6zohhG6XCec1o2b3yn6b
+ 9E3inIZm43dQB448qIAGQ1/+90/+gH2gubsbwTvVRfQABc6NYIvgibVel/UCXhfYKPRN4zEYlD
+ NFIO6KFB+9ZN+gD097/DlD+C9+iJjAE58I+iZIWB4OkiwWOuLOiFo6P55xg0wNjtKPwqs2netH
+ 2IPFXO0hTGAVhf0zKAzgl9kLgQF7+6TOkE+/ehPnb5pNEYBDRcFSEfgPKUIMg/mQI3q8nMxiDG
+ 71f8Vrv41l1hdNGoEAjlg2EEI9k8F/4jsPqkT8Hawo50bYGx2LIkCTEfAzAVWbVdqR7H9ZuiaN
+ h5rBa+1l6gERuCZgYT7wND99BPXggJS6Xm9zgvGrs0TyABoiG+SZacGcB2TacFEAdIRHKY3jng
+ NTYXF09dDckKaYC6feTAbzVDXb4YOVr/X+xWM1lbojMCawQcBy9/EO3UDd6PSqAECFXspXON1Q
+ rlhejChTvAfl3vD5E8OcRmTHY1zAdBNBz8At/+YCu7/PhGG2EfBImqGVIEZMvrNYFj5ZzSB17p
+ KnQl4RpDMOgEBQw9AYNBu1gCMCfzG53VaJ3R1niDB3j9Ekqzo0odH4XSwXVcQFoKz1yZw++MEV
+ uPBFRViPxPNsK7fDCIqqP0YvNZXMgbQqgKGrzpHcz35jMXAIE46ExBgnRWMFC7mdFONEDA6q7X
+ Lnx7mOiZLgakAaCogI+JxBQL4rn8cBy5/YOT8h0hQ2M+AhdPfFzVD9UtmBhL6g7bWogGqqIK21
+ sDGotv06wEY4gOD9zIG0DOBfGK+0TqBbwS/cUoK1pTBkhSPf4VIdyq69QGSWBCW6FLBEXBxWCE
+ Kn9FYcPrfRGMgyG46LJi2FpJeUjNM3nsNvDZUweQ9jVB87VM6BiZfv3b9Y2oAqmBDI3gvKzeoE
+ /SNQODTLqHDeQJrBCwAg0L2Xu32J4i6RmRYYCrQ0K6AmCAyD1z/7+QO4TtSRYAD0SsREGg7FeZ
+ PW/PSmOFS6+eQX/cxrvYfDY+K51xF+Pt0JuBFAynerdtRwUhMQOoDUiiSM/0GgyWROoF8v0KWo
+ hjQkwu7gkg0AXCtoZPHBpPhO7wyGuypRlEF2EyB+VPRDPG7DMxQW1P/Py96ZJg5+ygEhOxjxDO
+ C/7B0CpmrE4RF4yUK158MiAKZLqGjeQJnBHL+XyFPKemVj5HHekBJ6gFqgrG5/3L+07Quw7f7/
+ UgqW6oR4G/zFsybutrADAX5Bd9cqb/604tmhk8+/Rb8Q/brDMAzgQ+OamnBuF5YMBIDkA0c0i1
+ wRSJ5d4/O6gTS9wcF7f4u2G+XZa8YwC0i3QJNoNaaIGRXd+BrOPhENr8brpXc+k00wypISUr7m
+ W+G/BfIDIUfNNNPWCcK4BmBACX1AlcwckagIRzzPVcgUuH3E8Ad1Qk+mPsDyUTRb1fsgN683CI
+ yLNEEGq0JnFZ2deVHI/gSffjWvwvXyuq3w2CUfHYSwk9FtfPNkJeX/1VtTZ3meTXD+CkFWgNQo
+ Qn8RmMxR8L8BhVjAhQBLMPhkFinQCaEHQ+WWPj+qeoBfXGhCRRoAqAmiMgCx7/M6ErYp/kIoSu
+ NwSca/NuhGpQH+V4E7yFmBqVS+fn58gs/Pq3bvHqqa02PwT/0ACPWAH4k7+Mq94xRgedGFYb6U
+ toGCopEXqfgPyLTID0I6gTM+wHh6cw42T/VY0BfXZgOoqgJSGEYlgYO/2e8STkfpeF+BsKPQug
+ aEficDBxszAw52TkaczfDloQK8OMMwMoXb87wWXwGpFOOgN/QdNHagN8pkKmesTqBRA2yhczsJ
+ aTFDujrizFBJjWBk1+cKfC5kB/J/QwE74FSi8CnsvxNWKqx/9+YGbLN0AyffPoE/MIOshKaQJA
+ S9GoDvhFkU8XrBGICEkUChmdwRaJ6wNO63EYfjHKNICbIBidJrCnwiZT6PwfhR7Nhnw+fVWinP
+ axxM2R/d67s/D+etRm2JFbyDMAqtAMj6JnAb1Q2eG1UCeoEzgzkgEcA/hu2SNSgASwHPM2LmAD
+ rAiApwdHhvc7gc/neQv/nIHQLVCyqnQcfBv0mVIOyMPXxmJsZ6OrH8M5IzwQmRAN//Df8OsGL0
+ wbmnUH1RsmRA57F5T7qQJTb6HTAqSE4Wi/sDD5RVEc/D+FHInwlgm8fRE0wRNmdx2XUDFlPzww
+ LV5wGORZ7cq0J0rsUDcg5fs+Yap5UtMf3G5WlP0pOHfAsL/dR+yPdR+z7hRjByX0NdNLqdSlPD
+ fzNkB5XtM/CDJW1HyL8DJ7SuxQNpFMKwXNTDatq8MI2Tz6xwKBIRBOoBpjD5TFyr4dbeJoGIwI
+ 4ea+DTlo9xbN6nE/DDORt9cNwDi8Pz2DEM4Ep0UA+Nhc8Y2uoyLavbNIhY1PEdpTFAHO5PEbss
+ XAPTb7rPnIfuAYngM0fRoORVk9pDo+3r8ywctN5kIVnIvxMERNkCE0gEg1855WAdOpR8MN/10G
+ noEF5DDDHy1WyMRvNAO7DdoHdX6eBkVbP0pwec2+Z4XT5HZANz0JlMjJqBL4J0rvaKZgvfO1ZA
+ s/VI92D4v7hOXw3OLksE+vzleb62LtrhrZ7X4BsRBYjE03QWTQQMYEGZd7wBUZwX1XsGZr0L/f
+ geLB99S39Pt/S3B+/MTNkZWb9UHa2/GfODCTvh04oQPjZeibo1WigQT0/8LnLyX7RIBfXZWqvY
+ TvB2W05WP17BNfnK5+n52HMDPv2Z/045u38f0lHZgMV3wS9Fw2eT/iCO5DtFwa4uq9QeQVu+ae
+ T7XywfCWc9PmWz+Nz4cywe0/64xFTEPqoHISfA0IT9Fo0UKMsBrxIl4vT4ng3j+XN9m9Mj3len
+ 4NkdK6FZFSuWkLgc+KboHeigRKjwYsF/0W4JBFKCzSAGgVUfBP0JBoIh0fR/a+0GV6+EUoPNIA
+ aBVoDUBPoGWFkjhoNoOlGNFCjATz6X2lzhD8mT4HS+BL4fAmMQA1QgkrtRjRIRQP0h3yzhD82L
+ xrhg05K8BU3QTRGA48u1gZqNED/qjfHyycy38JnbH6J79h8QBOA0AR5fBOoJCQ9kOJwdG67ibW
+ BBg0Q1f8qmy38gkiUBk0APtQAnAQmaMdoEKUtDkl90HFtAGxtEIvqD/fmeHmPO2ThM66gBAVoA
+ Fb5IDQCAZ8XxasPLHy54lC/LtBFAw0qFtUP3mzhjz8UjdKgCcCHim8CaoQSNEGkXnHogVJ3UBu
+ QlBCL0aAfvNmCn3BYgVJ7jz8MaABG4xix0SAKDWBhWBzmR2I00BipDUhd0J/jzfnymnhE4TXhi
+ AoFaABG44kRDqWiIlEWRuoDC4wGqSK1AUkNpGOw7H91zRn8pEJLr4mFqWgAFYr8Go0mUJhUHI6
+ jxWE7rzZQoxGi0QT9rdwLXh8oMCWofKgKUtkuoT+v91/P3/X/Afw1kptmVhryAAAAAElFTkSuQmCC
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf
new file mode 100644
index 0000000000..f93a68969b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:4.0
+FN:URI JPEG, version 4.0
+NOTE:This v4.0 card has a JPEG photo as a URI and URI valuetype.
+PHOTO:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQ
+ YHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/
+ 2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKC
+ goKCgoKCgoKCgoKCj/wgARCACAAIADAREAAhEBAxEB/8QAGwAAAQUBAQAAAAAAAAAAAAAAAAIE
+ BQYHAwH/xAAZAQEAAwEBAAAAAAAAAAAAAAAAAgMEAQX/2gAMAwEAAhADEAAAAdUAAAr+rHE2wk
+ 67TnXtdstTYAAAAAR841jd50PpytpSbS627Ll2TqErRk0XPBqdQkAAFT10Uj0MMrzO2l1v2TKc
+ kOgAOI90fyt9gzXgFU10Z56mCdrobOJ71pO1t2ZwrsfXFCuc6caV5HqTdFrKcch9rzJjNoTdhg
+ vO9SR9XzWMp8o3c42qJCeDoivkePZda9GueL6dF35oLRTo3kb6P6vlRkpWbNqn817aXIezsPZL
+ jf5/l3m80265XO3jzvQpu7Pacl9nw6IPXjm8+x5CQAAMrKM09jw+XL/eS952Qo2V/XRd8Oi7YN
+ kYSIAAANHMw9nxVcCXHlnJoVOu/wDn32TLqQMR+BxGR4OXM39bxefZcO2N+2JSk4d1fydwA1GQ
+ s7D4BlOqi7/Pg7+oS8F8lZM992w3uoyAAAABLlM3ZKhrznXnExTdfvP2ZRthZMl9uqjJnoAAHH
+ sc19PFBWyddzhoHnb7LmupWqvH79Fip4/hzUs8VD09ADM/Qrr2nKtGaplqPmbvQKPojRLLNnyR
+ 78LGIkUNbY5J6dKepGENX8zU/h0ACPlx3HvUAAibI5h6NLCzkvTPTvPtfw6AAAAABG2RgroJde
+ R7Zc8wAP/EACYQAAEEAgEEAgMBAQAAAAAAAAQAAQIDBRETEBIgIQYUFTAxIiP/2gAIAQEAAQUC
+ 8SsjGtQPkvy9LJswNurIi2Jvf6SzKRY3nzIhJ1J1J079KSLaELm5shyKiIeOUyrUqqMr7ZOpOp
+ J5eNNk6Z4zIxKbrmj+CLNtRbshKaZTffTfXS0m3F8WaxdKMvYYec5W2Y8CwtWelXPkItkn6xnp
+ My0tJ39iXSGvqm1lfyO/dmOG+0SO/ExdNnJHudCYfa/HCasxAk1bgIqzBlRTBkjwU/Un9vpYG/
+ cMjZyn/HI+ia+9oUOVIcaodvEyHILan/0+k/pY63iye+6Xx2ftT/4W+Vt9Vaf+6Uv8xvUJ6vFh
+ syjHOMWpM0o0O8JdJ2QrX2ZTXBdYoD1VN/XZ92XS27+3REeDLdb6+SLEzdcZFirGqh1MnxjEDz
+ qFZ9da499nyOjtvFs5h/0O21n7dzfriKuU88ZihWOJAkL8ig6oPHubyunx15CF0Ghuc39dMGPx
+ jrP4z7EZs8XxYMjKMfkLxjNkQUSa9/3w+Q38pfI0Y1M7vjhvtks2m6ZrCsSsTdLG5UgWgmNVca
+ q5RaTOLFlsitRJr3kjIiDSd5SQ9dpdoQsBKPAsMcuNUOOrwyBVI1WrDL5aZwcdcW4YlQlf6iXK
+ kmxEO64QgiImHHp8f//EACgRAAEDAwMEAgIDAAAAAAAAAAEAAhEDEiEEEDETICIwQVEyUhRhcf
+ /aAAgBAwEBPwHtqaocMQrxyZX8piGpYUKrHcH1Fwbyq+ou8W9we5vBTNT+ya4OyO6pVtwFUqbj
+ ua4tyFSrB/8AvZWqW4CLlysoN2AVh7JQdBkKlUvGznWiU90lUtODlyAzCbQYGElBAIBAJ9G7hH
+ ClXIZgpj7DKBnK1DvhUGXOlcJwIdKuqVPGcKnpwPyXTb9LptViAWupx5hTKp+QKGBG2mdLIVYy
+ 4rS/incJtLqmTwmsDeO7UNupOCoCcpjbRGwErSp60jsEeoNtwNmiSqI+VT8cKPKCmUDTfc3j1k
+ Q1UxCGNniH+qs61hKFEgXOXO7clV2/KaZE+rUu+EM70RLk5twhUP1Kt9DjaJXSe83OVSKbYCAn
+ aiy1uzhBuCa64SESo+u9xXSLzLlVIaLGqjSnJ7OMhTd3uMbH+kyiJk+wuhZcdgJQEesz8Lp/aI
+ JwgwDt/8QAJxEAAgECBQMFAQEAAAAAAAAAAQIAAxEEEiAhMRATMBQiQUJRMmH/2gAIAQIBAT8B
+ 008MTu0NC/AtPRvPR1I2HqL8eJKbPxKWHCbnU1NX5EqYIfSOjIbNqoYbPu3EsFFh4WUOLNK+HN
+ PccaMNQz+48dOeo6X0kA7GV6Pab/OlNM7ZYqhRYStiQmwnxKuKZagVf20A0BraaqCouUwixsZg
+ k5aYip21nMpupW07VND3Lb/sq4z4Sd+p+wYmoPmDFn5E9ShlKsrbDReYtfdmmHWyCY07gdM4oj
+ /Y9Rn/AK1UWyuD1vLyuMyRRYTGjg+K8v0EYXFozewkR8R3Eytz4/iDrTOal4qYuwiOGbKNDGwv
+ MG+2WOuViPFhF2LaMQ2VJTfI15WpB/cIaJEtbWq5jaU3X+F0Yl8zW/OlCrb2NCLR2sd+I1MEXX
+ Xhkst+tarkGinW+rSouZYCRxqpU+4eruEFzHcubnSrFeNVOmah2gC0lt0qVlSO5c3PjXJ9p6k8
+ ILRair7m3MfEM3Gn/8QANRAAAQMBBAgEBQMFAAAAAAAAAQACAxESIUFRBBAgIjEyYXETIzBSQm
+ KBscEzQ5FygqHR4f/aAAgBAQAGPwLZIi3jmrcjnOdg3gAuWQnsrxIPorpgD81yu9Gsrr/aOJXL
+ YYcMdrypHN7FU0lloe5vFWoXhw2jFo9DJickXyEuzJx9EPicWu6Kw/dm++x4MJ808T7dQA1VPo
+ WmmhF4K3v1W8w/Op8rsOCc95q43lWuSH3Zoqxavs2rsO6psX8NhwTZGYYZpr2crhVRwDgN4psf
+ w8XdkYX3WBd1CebPG9COIEk4BWtLP9gX6DVyFvZy8qcj+oLdMb+xXnx2W51rqbrfAfh3h2U7vm
+ op3Y3BBw5mqtqzGMRivKYB1x2pG9FTYhydun6ok4qZnZ2q3+2/m6HPb33fTXVAJsmRqomP4WwD
+ /KEsD9zFpy1EHgV4L8OU5jXvuAXkRud1NwXnS0HtYqhv1OqioFXU6uEtf87F1zxe0qjYXF+OS3
+ 3iMZNVbNo5uv1yHpReK/dqaAK7W1oxNEyYcHCh7qOT3Nr6N6jiGF52I8m7yfHjxHdWRwBvCpMK
+ KrXjbLqE9BijpGkNoXuuCqcNZldzSfbUZoRv/EM1QqY6NIW6TGa0rzBCHTXOjHAkjgr2tlHS4q
+ jqsdk65XbAiBuj+6o1WnKz+2295/CoOGsy6NdJi3NM8cFgO4+qpPEyQdQmxs5WigvVHAEdVWJz
+ oz0V4bKOlxVHVY7J1ytVBe7kCJdeSr0IoR/xCNn1OZ2aaRGHdcU1gJIaKX7PnUcTwZmiWt/00I
+ 0NRmqgWY/eVZiHc4n07OjtY353n8Iy6XLJM7oF4MEbdF0brxcqyea75uH8bP8A/8QAKBABAAIB
+ AwEJAAMBAAAAAAAAAQARITFBUXEQIGGBkaGxwdEw4fDx/9oACAEBAAE/Ie5pKwgb9PLmGBVcL5
+ +tekRZ5AAPvFU8RT+xeuKfpEAUI7n8NWYtMnkSqHizI8fzvATXl+D0iITpHo0+JzKDk6neZSPG
+ qftmQsWi7doqi2MvdGiN4BUgabdP53Nsjg2fsRQNhS7SaJWmLASpUIJrlY41GVqhj5PB2ZeK4c
+ uxLgxsY/nU6jPRG1upiJlrPWqu2HT1m0iixZdqFhZpCSKkcYmvdq4NyLlYBLsYfOcH36y2LDmI
+ xgWZ7ssbBttpxFjIr0j1aeL8v5AqYPOBYby33GW8UW+5NOjo/MeX2Yo+DKDWOTJeYRcPPqWvv8
+ zqDHQx9Q/GvlnipitzcibIq3Oj9lEZddS6ve88p1MzSDrL2sIoReamp/8AhPeovUStgDdUD4fr
+ s0/Wod8GIN8MsFtYQ4Ho9YulJnyYYK0X6GDYczU22vfsNOwpIra3Jv8AGO0y+os0M2Poke8rhs
+ 8RlWLvMQ0CcGEay1ZUoej0rbuVt7PgGG1bg6B6z7GX1inuI7eRcHVxNjjZrvn2iW5Y7Ki6iIec
+ q97af18QRthdd/4QpQabL2gItPraShrK7NEXl+WnvUZNDy8DSZXjBcUyptuXHuf1CY18uPXSCJ
+ Y2d4W6GgWrgmCBSuf+E4rhFL37KocHw/3TsyOgx6jk8Yr1CFWdohfHmyPF7gC9lNyUeoXBVvhM
+ CC1Y7ncPBXT1a/UKJvxZqK4iEfgP2YABQwB212NfQOjxjZolKofxphLZDWOjCZSoKaOrPCzQuK
+ dUuPSeqTAro0woOAcjz0ihKrV3YVfCUD714HKwn9M73J3eFXNA84ZZALto5e7fh4tMPj9IY6zY
+ gCqHFKuHuVjjy5ms66vufxgPokdB9y0MNo2flmS25F5zURCT3EAAoKDuf//aAAwDAQACAAMAAA
+ AQkkjRaEkkki6RagMkkMw122ZkkVsEe43UnzMsoIVRiqelxLXtd8QAAdbM9EAAAOYYbEAEkOpo
+ QAAkAtCZDAAAA5w91SgAAeBgklMAAE/8kYCAArvskhUklhwkkkkkLUEk/8QAJREBAAICAQMDBQ
+ EAAAAAAAAAAQARITFBECBRMGGRcYGhsfDB/9oACAEDAQE/EO0TVb54gsbntgPjc4qf77xTNnp4
+ GWo98Rz7wIECBBrXTBTA+IBbfcfkRKc5YEsJlmB2q21CacdinydJVWxtqZMwJbEMnS5cYYkzBv
+ 5N9CRRGXbBNs2kGnILf95gl3XzN7RKpjLAoEcTAANM1ffMk0RoRkzm5ULnwvEymTPZTwEAagEC
+ vPh/yXwIIDRFzNOJaIMmC2EfR/tA6Nd304v4zGS9QKkuM6I6H3YrVlni+illMA8RLmCmxSlRxM
+ z5JaXLY+itZi3mZR3KVu2EFEuZr39L6AfuafeCIbS5cFBNUUfokHcyhgcCXLlW+ISKZDuIoiV3
+ iilxgueV2NZOJczDt6KG8/MCBQ51EJfezURtevafOCO+PsBf4ZQYg13U5dxNeUNsMCiu2ue4jm
+ Xalx4AUenwwC7dzCCibjPb/8QAJxEBAAICAAUEAgMBAAAAAAAAAQARITEQIEFRYTBxkbGB8KHR
+ 4cH/2gAIAQIBAT8Q5RNJ26zSIfK/OoJ1P38RIwj+f8mWfjn6iV6LtGIbGBAgQOGumKyq8MpRTz
+ J0X2lQFQIEC4HLXKyO7Pp78lzov5gBFtcsIZgqLUByXAaLGPg21/XBCHWE9BOsDL2IEwiPLLlP
+ Yz7F8DUWLOoly5c8xB+jHbYTAvtMqbirt2w+HNVB1A6qF/MTT+Yu3ebS34n9ahuWRG7MuDLjFI
+ HWUfx95jMhuFQF/T3i9q+b3Egy6jDF74zCAJn9FMTIuMXcyzLXuIh2pf4gsPuPRC5dYji0NFsC
+ scKOu1f89KnTayog742XZLEumZ4pfRFNSlHXHJY+cRyMTG3NVmKVPOgCbLdEMvHCdfbgIsw68R
+ FTDINr0KOa9fqXUO8LHt1N8aA09Ht/k1Ge0yCi228r09DcACjjgY8ttkW2+Wmad5+mLC0tmNcv
+ aX59MjLXwf3GioInZ9Z8zH4H715f/8QAJxABAAEDAwQDAQADAQAAAAAAAREAITFBUWFxgZGhEC
+ CxwTDh8PH/2gAIAQEAAT8Q+igVQC6tHWFGSLwL/wDM1JWcuSbMUrXRDeXBcIBK41SZhUQNuh6T
+ RVmtA+4B80JQpEkf8ML5T4i/pg5pfnQvaKenR5ay/Dlqdpu3oYgDMybzg9ysagohcqu7UgIVgW
+ XZLnf7KsZWS9jT0Gu1NVM/NAl/6PhyXq/vQpFFilnPzFY+3tk2TCWw2aOOfRsZl579xN4+VeLL
+ dddnRei+pW4FdahygXd3VqQmO9G/DkP7U8cOlEYpDCw80UPmDVlSMAuI0+QQBaOnA6mj2mjuh1
+ Xt3GO0tNRf1Jf5QwW0zHMh/uOuKMs50bOKJBUWYTBILxcKWa2qEE5an+ehZFeqMoFEia/Hw1o+
+ 9kFNmyxNA+XhPDDpUnmDwk32eKbBzd5g7BQrsJywEscqh3mjuqREA4BxhpLoLzLFXNOQksncgU
+ DOly/7f55UUMIiVT5WaSjzffCT1XAgN5oflSToSRege6iVoI9LNrm1JRsFCpgaKvSgtmU1x009
+ 293IBwQao5huUzZvgUNYKM7AJ8yeKWciqZNblk90unOYFtp2I1U7BmH5Qj3ku9MfYmCVXiHspx
+ lllxTQXAfHoBR5VmAkAc3A9naoo5sW6y051kdwn9PP4OVACxpdDhouW+yLMYfzDHeneSrmIrjo
+ E1SBQjkF38rRuscQJ+UW+b2yID9qZxVqDIANjCTGCXX4GO4jUad2PwWdcPmIu0Lj0MtMgLgD2O
+ fVI5l8hDp/tNNlxlDFrfHal5Elan3lk8tqAEnX3avkpLTRMCB0sD6T6O3+fEeHDWXZ5Aef/UUZ
+ PMnPmw9Ky0brSc3t6oAILHwaTCfLYe2hQrB18iUaW1X9SXNCJOsUlNB0U6NVAftLkdIYwr1hRC
+ J6ERZ2ZP8ACk1LQmW5zU6qQzewdYF7lIRUSgctNEppKUqaa3nSJsJfXOmjwtRkROUayMW3yND4
+ /EMu4eqBDsEvQvtRMSEiMj9l5JumiAarR1M8LZcaABe+Kcw1gMDTgV1Ab0lPCkLPQ/0q9Pg2E8
+ NkWLURjU6QiXHsNyg7QpDTYGAmbMgxmtprml2I3VMMzBe8QS06N0bPaj+6zXnHujJBSJI/QSTN
+ TFt8AHUaCNRmwLvT5lYn7oLmcFpoDuW4JdKLUUAgAwHyOnZtE6ugWphzZmX6E5Zy7MRA06DRZR
+ kD7A7NBY06QsEisFrtLUDQD3SdMvCreVZpwuaveRt2KPrvmvOPdDGZmSReiZexrTbRPSoyrzNG
+ GC6gy1NRZDqdAH+r1NeypCsr+GgB9bZOSDsBD2xUmNbuEG4xr9XogwCN4cHL+2otZKzFyZVYCb
+ v7U5SCRBvDcpnc38Ayult0o7Ggb9TVfhg/xom2zLhuRl6jpVogiHSxJ2iOlQEARHQJz0Xq7X9h
+ QAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf
new file mode 100644
index 0000000000..058d5fbbf2
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:4.0
+FN:URI PNG, version 4.0
+NOTE:This v4.0 card has a PNG photo as a URI and URI valuetype.
+PHOTO:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAA
+ q0klEQVR42u2dB1RU57r3XXd965R859yP9d17Ts45SRTpvTOFOgiKDcUWY4kSe4kGu7EgNoogg
+ p0+gIoGFRRFLMBQREGRURBrDDG5OelO2knul3vO8z3vu/ee2XtmDwxNR2Wv9V8qIs7M7/8+7X3
+ 3zIAB/Vf/9bxfQwseWYTlfRgVmvtQOSTjzvWQ9Lu/BCdebg6KOT+o/9V5ga/wIx9FDjv0qAThA
+ 8KHIQda/1ux7vQ/gt8rHN3/6rzA1/Cj7VEIvx3hQ1g+wlc+hJDNFyB4QU5L8Jx0C/I9gfvuWAZ
+ urd4duKmivP8Ve3HAK1DtCB8I/KEIf8g+NSjeOwwIXk3gB6feGB20q/lhQFrLPwPXnvkZv+Yh9
+ rO8t95QeMU2Kfpf1ecDvAWqBAUC+Duv4KrPJvA1iqQrbwWnNbcH7b8DgXvbIGBrDfjHN9TJk27
+ WyRJufC1NvAm+8Wrw2d4MCB+8NjeB56br4LHxGritawTXtVfBZfUVcF5ZD07LL6scoutU9ktrU
+ +3erYm1XVQdabNA5dFP4tmteg2FX4jwDyP8Agz7uPIpfBTC/z744F3g4Pvvvg1+qa0gT2kBWdI
+ tIPAlxuCvR/jvN4DLmqsI/wrCrwfH6MuA8AHhA8IHhA/W86pg8JxKjeWsCtXgqEvRVjMu9Bvia
+ eR6Ap6B366Dn9EKITFlEJJUD4qMe8CHH8CHn4zwdyD8BDX4isB358NfhfBXIPxll8FhaR3Cr0X
+ 4NQi/msK3mlMJCB8QPiB8sH77PNhMO9duO/Vsqt1bZ/rN0AfwlRz84RT+Rwgfiz6u4s++DyGZC
+ D8d4R/QwfcXgx/XDD7bEP4WIXw3Mfjv1YH9Ej58FcLH1T+rkoE/8yIHHxA+IHywn3waHCeWfIq
+ /T0JzBPbT6034ZPUbwH+A8O8j/HsI/y4E7kP4exj4fkbgexP4sQg/BuFvYOC7GoO/GOEvRPjzd
+ fAtEf5gDv50Ar+MGoAYwQq/ZvlOhVZolP+HOo6/j0JZ9BPtwhWW3lopgM8WfbTd48FXaOHfQfi
+ Y99Mw9O/C1b/TBPjrGrRFnz58Oz78uQh/diUDFeET0NbTyxnomAb40DvQ9yglqr/jMHa5j9pv4
+ T5yX6z/6vJvxeAPZXv9ITkIP4uBHywKnyn6JFzFz8L3MgKfVPyOyxF+tA6+rTH4Mxj41m+X49c
+ umQpfIKuZF+6SqNBPnL3cItItULFuo9M1koXFYBJ8XtEnBl+03Yu5LgrfiYPPVfwI30YPPgn9X
+ Pgnq74LK9+oMIX88FIbwXVMlgUqFqVBgdfMYzCs4KEOfqEOfpgR+Np2rzP4/HZvLRZ9q6+Kw+e
+ 1e6Ta7ylkk4wwrexnpwkn575U8F0ic6OcI3PbnSOVwCgXQg+2CuGz7V5Y3kMINQLfoN3roNc3a
+ Pe0vX6dEP7cpwefL8eJxU88Rh4a8UKDdx6X5+E0Ll/lND4fnMYXaCXfWAPCXv8jHfzcB7TdU2Q
+ awjfW7onBdzOp11c9dfD6QhPUvpBdA4KOdZpwCBwnHGY08QiV+5xT0HGv/6B7vX6sib0+V/ET+
+ LMqnrkBiGynnP2n91DlkhcCPMK2RNBqAtthUiHqKKtjVIo9twRFn1ivb9juYa+/y7Re32N9A/i
+ Qwk900GNY7ZuN0Iw4Y/j8uY4GCD0KoWso7DePgf2bH+DErEgrz+hL4r2+UgjfWLsnJe2eHnyu3
+ fPY2Ai+GAFk+Ksr6fVXMvAJeLLa6WDHnICz0HWqpBr8zqV/OUwqefe5g4+rXMmHbjf5OI5MT6B
+ OMppyEsJy7nfe7pnS65Oij4XvvglXfuw1kOKf5dj6ua1h2j2HZXVgvUD1XEDXwp+tE46aC58X8
+ BYIXmX/JlnlLHSEbTulGFWCYY3oFPhsqDeAb6zdC9hrvNf3RvieuPrdtzaB6xZc/firDItAv1g
+ Cn2n3HBH+4DmVzx10KjKGZmU39cx1M4d/zBJXvFoHnoN+Cvvd06xKwXnRRWG7x8IP7ajXTxXC9
+ 8HQ7xHfDG5xN8BlWxM4b70OnvEIHk3hhwbwfL9Ru7VrZQ7wuwl98FydrDB1OUw+/ZlZ1gW46j0
+ w1Gu04KfywE8vxRHqGdRZsJ11nq54g14/t/NeX4pFn0/STfBIbAbXhBvgHNcEjtuvg8M2zPk7m
+ iEg/ib4bcN6YON1bcVP2jtzg24AvBPoVPNZ4fPBTahvzcoEuOoR/nENye2CFc9Ct367DDdRyOb
+ JOQje3ybS7vF6/XTxXt97501wS1aDC4J2TrwBDgnXwT7uGthuawT3JDX4J9yCgO1qkMY2aTd4y
+ JTvqbd2fQSdyHohI2qCKWe/MQsTEPh2byF8kudx1RuAn0HAl+Mc/Tz4Yqgm8Dve2r0j2Nr1Tbk
+ FrjvV4Jx8Axx3NIFD4nWwS7gGNnGNYLUd+/uduPJ33MKjX7j6tzbTvM+1e6S/Nyvoc7oLvQasF
+ 7FaTFRLW1eXyKL7ZgD/hIYWeHTVs6FeC/48gr+Ae+MXcepWI2z3Oun1Zdjru+9SgyOCd0hqArs
+ d18EGwVvFN8DguKswaDuu8pQbEJDcAgGJt8AvTg1eOPTh9/qkx7fDx2I3veyFgG6DU0ubJXWM8
+ PfkcTwzEyB8C4TfLoR/lg31zIon4MkJGrJ1SsK6Qa+fI97r+6a2gCPCtUtuApsdCD2xEQYnIPS
+ 4KzBwez0M3HYZnHc2QUBKK7P6E26CdPMNba/P39cX29p9VtC1wE2CXmsIHUfXtu9dBttoonr6c
+ 8jj8g7Pq3kW8NVa+KTIe/sss+pn6lY93TPHF4tswnS6tcvCd8NVb5OM0JMQemIDDGLBW26/DJb
+ bsJ3bUgvOOxohAFtCsvr9cfXLMfeTYY+jCVu73JYu2dcnhzvIn80JOgXeAXTbZfVgh52N3Yor+
+ Pe12sfpPUyZ/NQMgEMdpRB+mWDV0/1z8sDwRRk0r5K2cZ31+v57b4NDShMM3oEhPvEqhnp0eNx
+ lsELoVlsx523BFyhWBY74tYDU2xCwE2uEHS3Y9t0ETxL6jW3t8uFzhznZEz1YSGG7WopDKhK9z
+ rHRqpvQ5/YS9KUdQ7fD1tZ+1VUquxVXdQdNMN16hysn9f3qn3w8min42LCvD59d9ZZzEf6CKhi
+ 0qErX7nXQ69vjirdOwLYtHou37fgibMNZ/VasdjerwDYWhyAxleC0pRoCcCAUsAtnA8m4H5DYA
+ pItzZ0d49Y7yVvOO8xZipPK07gnUYIbVCdxZ/IEPdxJjdBj6NV9At1+dQPYr2kAh7WN4ICzDn6
+ 0wsf+T59huYP6EH6RB231aLXPhf1yXsjHBzKbWfWDFiL8d3HFbrhquLWrB989+TrYxuHW7PYas
+ NtWDXZbVGC/uQrsN+EINOYSOGy4CM6bKnAecBMCCXy6+rE9xNbPFV+MLh7jFpzkpfAnFlP4zpF
+ F4DL2A3CNOAqO+HVrUsM8LejLOOhXhNBX60O/Bg7rroHj+uv42l6nj4NvArcxRzV9XPQxrR4t+
+ DDn61Y+G/LnM6t+0BIVDIyuBoeYhg6PccvTWnCQg9+3FadcW6rAMbYCnDZdAqeNF8FpwwVwXn8
+ eXNaVgxxNEpjaRkO/XxKz+r1jmrpxjLuMB/+UDv644wz8MUfBbXQhuI86DB4jDoHzhBNg/c7Fr
+ kNf3EPoa4xDd8BBlwM+d4dNTWAl0upKQ9LP9EXoTyV532bqadrqkWrfAD4J+YtVMGhpNQxchlp
+ ZAw6bG420e0yv7xGPmzWbK3CFXwLnmIvgsvECuKwvx8LxHLitLcO+/izI4uohcDcWiSm48pNug
+ xzhy+Ju0tDf1WPcdlP04Z/kwT8mgO85PB+8huWB99BccJ5UjPVEZR9AFw/tWugI3HGjELo9Drv
+ s8cCL/ZYbYLXI0ACkHpCGZAzvzdWvYEI/r+gzAn/gewh+eQ0MXIXhfP5ZkEed7PAYtxuGdreN5
+ 8FtQzm4T00H9yFbdBqeAL4rSiCQ5P1UJvST1S+Lv4X7/Ne6dIxbB78Ut6ZZ+BNY+JF8+EdY+AX
+ gFc7A9wnLAd8hWaTSBruoC92CTvN5Z9DpKudBj9FBt9twDazJ80XodrjXYYcbYLbbbxjtXJwmF
+ f/am1U/7fdp6OflfVrwzWbDPg++3YJzIBmVAwp5CoTgmT9jx7hlSc0I8hxu3JSBx4TdQvisfGc
+ qGfi4+v2w8JNj5S/ZpqYtn6nHuPHgJVPxa+GX8OAXMfAjWPgj+fCVLPxskIRkgjQ4HVdWJjiTK
+ Kjt0WuF7VpvQN9kuNIJdPK8rPFEsy2ef7DFjTCbhGYD8INnM7KaUwE+I/OaeqPwi+VWPw392rz
+ PVPu04FuEYR/hW0arwOPNQggM3gNBAWkwaWwGKPMbjB7jluJUz3tNKXjNUmqBLwwfC3EjfWHfD
+ F9YM3sSeE06IIAvw8LPDU/2dtjrR13ktXtl2nbPHuGToo+Bf4IH/6hx+KEsfEUGyIIOgizwAMj
+ 994ELmqDXoccaQudWOoHOgbbGbW+bHWpsj68zz5cDP4cBbzUXvwe52M65CPJhmd1PBbjDZ0Hm/
+ Ezhx7V8vNBPWj1S8C3Flgu3eH0jlOA/ZD81wNYt5+Drr7+DX375xegxbmlsNUhXFIPHqB0U/lh
+ fb3jlf/0b+L72e8if9yq0K18D76lZCB/zPlb9soQW8N2qFm/39Hv9t4W9vn67x6/43UYT+IfBc
+ wSBn09DPYHvg/B9Eb4E4Us5+AH7wM9vL/jLd4MbRkRBu9Zd6JsZ2YlA51a6TaLOAIPxuVvh/og
+ VHnzhVjuBzoG3nl+JrwtqYSV4Tvrgxx7k/iIlWf02gtXPhn4u7y8h8C+AdGQ2+IUdhGERWXD69
+ C0KntO4zLuix7hlMVXgF11E4YeFzQGNRgMeHh6A/zWEuv0Ovj71GkiWlYGc5P1EjBhxLTjxazC
+ Ab9Vhr3+GB79YFL77SFL0FYBnOCn6lOAdlsvCz2LgY+hn4O9n4e+BAFkaBEpSwX366e5D39I5d
+ LLSrXG30xp3QrVhnigBO4C19Sz0CgrdeoEOvO0i1OJKfK0qwS88c2+3DneQY1yC1c8Vflzox7w
+ /+N0KkIzOAXl4BgwdmwOtbZ8J4BOtKX4kepJXvv4S+C85Rg0wUj5KC59I5vhbuFXojtV+MwMfV
+ 78PDn30j3ET+IM5+DNN6PW5ij+CbfdGshU/B38oC3+ICHx/Ifwg3xQI9tkJrvMudA06C9wU6HS
+ lp9wEq103Bfl98Pv1YI1zFv5q1wdvtwTnKEsrwT3q1K/dqfyVhrmft/px0GP5bhV44RFv2YgsC
+ BunFIVPlFP/megxbtn7mKMWHwWP8DiYMHQWBL7xCoX/x9//G9Tu+TOsSE7Qwpdsv0XbPv4xbn6
+ 7Z6Xf7nXY6xtv93xCc4Twg/Th74ZAKYG/i8IP9k6GIEkKOOHZQx30JuPQt5sAHQ+5aKHj8GtwG
+ iq5WZDf+eApdBHw9u9V4nicqAqCQvfndOVcnyU5yKmr/PVW/3xmyuc67ThIsdoPGZcLrXfE4RM
+ 1PHoieoxbsrYCZAsKwSsSX9DQVXBu/utwa+drcCvXETalxfBWvhqcSK7Vb/fYk702BDivwrcnO
+ Z/f7on2+keMw2crflL0MfD3CeAHIvwgFr7CMwlCPBLBPzRdr3LXgx5nCN3ayEq3XFIDgzfhZtj
+ uW2CJx+Yt96ISm8TBE+jGwC/DwRrR8irwmV70r64Uf7HkaBcd+Yqtfiz8bOafBwkWfcQA2xNzs
+ OD72qgBrqIBxI5x+6yvBcm8I+Az6xAWe7jiFp2AiNgiWvHLsOiTYL/vzvb7+u2eLeZ50r+Tin3
+ k9CKIxnHx/pxrsD+7kepAVgMk7qmHDYnV8M7qCzDx3bPgN6nISLun6/VF4fsx8AMIfAmBn8LA9
+ 2LgD3FPwJolHnzePmkIPd406NqVjtAHv1dDV/ugXc0waH8LleWmBtH8TqCLgkfojiuq8HwEamU
+ VHpGrgjBpUrqp4V/DFH/cyLdcW/lzq5+EfmKArTvPQXp6Opw6dcq4AT58InKMG+/g2XgFfOYcA
+ e9Zh8E/tlbb7slw0ueJq4kc7uD3+qTds5mFE0MM52T1rt9RAy13Pjf6/xpT/fVPIK+ohZpj8rw
+ SYa/PtntyzPsEPi36KPw0Efg7tPBDXeNgiGcirv5rDPSErkHnVvqgfWiAZbW0sCN5fuDBFhiY3
+ grWy2uMhnlR8Ct14J1X4+mhNVXgH5nziynhP0oX/pnij/T9dIuXzf2OUafBd2weTF1yCn799Vc
+ Kn5igublZ9AU/c+Nzo3ftes06CtLoM3R3j/T55DZueo5fb2vXFqt9hymldPUuxd3Bjx5/02XwH
+ elK48c0aqzA8fPYiQW01+faPS18UvR572TgezLwh7Dww5y3wVCnrSCPyGXyeSfQLfWg05V+oIU
+ Ct4qu0a72N4gB9qg7zO+O+uBZ6Bx4l7VVOFqvAt/ZJ2CYx/bozqr/EvHwr6v8PScWUgO03f+SG
+ uDJkyeQm5tLTfDZZ4a1wMG6+xCULH7XrtcShB9H7t9v4r1DlxC+3awLdGgz4d0yqG/6pFfBG9P
+ XX38PDVcfQcaBOlizvASipuTrwU/UwXfZTuEPc9wC4Q6bsQ6o7zL0gemojFZ4A8UP8wPTsPjbd
+ KXD/O6kD36NELzruiq8NV4F7lhzDZUkPuzoVi4LciePrvfnhX9S/GHfbzenDHwiC2DTrloKn1N
+ bWxs1QFFRkcGLmVJzG9aVtRjAl+Ikyw+Pdwtu3Izmbe1iq+c4+RTI8fHknrj9VMB3praWT6Dq/
+ G3I3lMFce+XQPSMPIiU7qDwhyH8cPtY8BuT3Sl0mt9TmylwqsxWeD2rFQbuvSlY7dYb603K71r
+ wCN3lfSF4tw0IfyMqRgWKYfthmOf2QUYMUBilX/0Lwj8Wfy7TTlADtD34UmAAonPnmHqgoaFB+
+ 4LVPPw7lDxso0pUtcCbmc0wKVMNs4vaYO7ZR6DY3Wpw164d5n37qPOY64/D4q018NEn35oF/M7
+ 0zVcauF57H3UPfHFFG1vpBPjAlGaa1wn017Nvw+s5t+G13NtY7V/Trfalpud3Lfh1RCoD8B6bU
+ HiiKjAyG4b6JhYby/8l9rzhD7Plqwv/luhM7/GHYEp0qQF8op9++skgFaRU39YagCjz5l3YXv8
+ All74EN46+RGE45RQcNcutjmO2L6FziuDyzf+67kAL6aN1Z8YQCer/A0KHA2Aq5+E+YG7bsBrS
+ oSf1wZ/y28D27W1gvzuYGJ+p+DXGwfvuZmR34xCCA1M+Yex/A9c/meGP+WC3t92bjk1wKGSVlE
+ DED18+FCQCvJu6gxwuO0OpFy7D2urHsI7pY9gwvF2Kh+8j88Bq16HqWfAd0YppB1ueW7Bc2r+7
+ DsD6GSVcyt9EFnpuKCsMMT/raAN/nqoDV7Lae12fifQ3TYaAY8nrLyI8NCNfM4HoAjdC8N8EgL
+ 07+OPJHfzMgbQy/+zmerf4Z1SagCx8G8sFXDwix+0wUH1Pdhc9wAWl38IE08w8BV4x4/TjDJQ4
+ H5C6pFW+OrbH557+JykR+8y0HMZ6GSl/41d6VYx9dpQ/9cjd+AvhXfwEOzVHuV3900i4BG61zY
+ V3kRLVA3yecdBMXQfhMmSivULwFhaAOKBT/70T5f/8T/DyV/w9GMdwuengqy8PK0B8lrvQlLDf
+ VhZ8RBmnP4Ixh19BJ6YAxWLL8Gxi49eGOh8Lal+LIDOrfS/Hr6DO4gqbah/PfMWNYDDmuoe5Xd
+ x8NXgHVcNPkTxaIAFyDD8AIQo0r7SLwBVhv0/M/2j7R8OfzwmH4M5G853agB+KiDwi+7fgX037
+ kFMzQOYf+4RjFLeBx/cRTx26aMXEjyn0x9+I4DOrfSBe24I8vvAPXgcfmcjE+ZXdT+/i4KPZ+S
+ bgMLBlxynrUEjDkLw8AOgl/8LgW8AQQE4n9n29cbpX0xanUkG4FIBMUBOy11IuHIfll38EMYV3
+ IO38J6+r7798YWGT/QQnyMH/S9H78Crx+7Cqx/cxeNddYLCznZbfa/kdz54yfvlIFldpgUv2YF
+ Kqgb/aYcgaFQGBI1Mh1C/5GjuDZw8yPv20A6APwDi9f/k0AcxwIEjzSYbgKSC+ItqSLt+D9aps
+ PA79RCiD9584cHz5VX6kEJ/tegu/Pn4XawFWno/v+uveIQuXYV7H28VaMFLk1E78b6KSXkQODo
+ TlQEhwakqxgATDkURA+DpH/EOAAtAct6vqwYgKm68B9suM23f/JcMPtH0usfw5xP34M8n78Gfi
+ u/hzS71fZbffdjV7ovQZdGnwG9CHsjWnaPgZSmoJBUEjM2GgDFZEIgHdxRhe//OGSBW1wKKdwD
+ kjH93DEC0sPAmjE+th9ra2pfOAHEtn8OfSu7Bf566jy2huk/zuy+72iW42mXzj+PuZz7IlxSDb
+ FcNyFNrwG/dWfDHrfuAyBxqhMCRGb9yLWCJeAuomwASA3hhndAdA5BUcPjwYVoUfvzxxy+VAao
+ +08B/nr6Pq/8uOMbUiOf3DV3L79rVrpffJWyYl+Jql087AvLJh0A+twhvvkH4u1ELisB/Ap7bH
+ J9LjYC7g8AZQMUZQLsHoNcCkhs93KafgB1ZjV02ANHjx4+pAUh7+P333780Bmj66ntqgMG7r/V
+ afteudr38TsP8LlQcHrSZegRkUw7jPsoh8NtTg7utlTQiUE3MY42AR/aD06KJAdoFBnhbzwCLG
+ QM4v1Nichsoprq6OmoC0h28TFHgb4daejW/09VOwOMNNdIdKhY8E+bJapfjfRYyjADUBCj/fWi
+ ANWeoGeRvFoAfEWsERcieeGIAYAzAHgLhG2CuzgD2C8pg1PzibhuAiIyIiQkePHjw0hjABUN/b
+ +Z3GuaJVpTiucpzWvA0zONql+G0TzatUCv/2HJdRGCjgnxyATVD8LD9KmoA7Taw/hSQM8Dyajy
+ pUkFHwU80P3XbAF988cVLlwpmZlzv1fzOrXjp/JMgW3pKC95/L/66A8P/9KOoQkY8I3ARgYgzQ
+ tDwg20mR4A3cKfKa9IRqLj8UY+iQGNj40uRCkjBW1FRAaPiyw3D/Nau5Xcpl99T2VCfVAXSmR/
+ godqTFDwJ8wH78evLT4P0bTQA0fSjOjMYMQIWgk86NsAcoQGcZ5dAUkZDjwzATwXGjpE9ryKHY
+ 8kmGNf1EI2KL+t6fhcUdrz8zoZ6+cZykEZ9QE1AwAccQKWiKWYcY4QG4CQwgsAMxAC5fAN0XAQ
+ OXFMLVisqYPTckz02AEkFJA0QdXSi+HkQSWW3b9/Wno3UV8SO8i7nd33wNMzzQr1sUTEaoIiaI
+ PBgDZX/Bhz6zDymM4EJRiDFoMAAujnAeeEcgDXAGxsvg+fkQnj8X5oem0CtVnd6oticRQpZEuL
+ FoBNjV1ZW0k2xOfnNXcvv8ZUGhR0N82yo90+sAOk7RVoFZdRC4B6VNiIw0jeCzgy69MAYAQ1wC
+ IxNApdn3dYOggauJgbAO2Kjz8PBwzd6bAD+iWL+MTJzD/FkoskP8WLQ+c9x7qHmLuV3KZ64EoB
+ n8zsX6mXRmOdnHWeEBgjOqgW/laXaiCA0wgdCI8wwNAIZBQOzF3CCdysYY4D2z3+Ao3WfMvf+r
+ 6qBN3An6/Ut9RC+sKRXDNDZiWJzCfGkVuHqFn2RYlYfOl8RB652mN9lvPwu33IRJJhi/VJUOvA
+ IPfAAG+ZxoCPB490ShC9hTRCUIowIRo1gJD3gbiBjAOFuIGMA7kncatdAeEoTvLEeDbAZz+1jW
+ 9PY/GmvmED/GJm5iOR17nSTGHRyEpqMuTt7fl3J79J3T+HdUsXgj+1cAAudKCi9loZ62fJSkMw
+ 5wZiACk0w74QgIgiNIGYGoRHIdjAw5wHYA6HTS7X3A+g/mbMtX8KsI+Rt3Rpg9O6GXjEA/xjZs
+ 94w4lo3Lip1Fzpf+v27sfwu33wBJPNLUGiA5AoITNeBD87EO6e2XaDRQTLnJGMCPSNwEYExwnE
+ TooLOABo8ESR6IKS3AJt6jOxZbBh1lNdJVCLFKklV3UpxP/4s7N/1wXNhfg/WB4tPge+CEmqCg
+ J2VEITQSX6n2o8mWljMGIBTB0YQmqFjI6AB8lXkQ530zwSSewKelgGe9oYRgW4sr/cUumDo1f6
+ NoH83AM/mdxnekua7kBiAMUHgrkoIzq4FBVEOhv5lpTQ1aNWpEU6YbATyeX5oAN44mDcMepoG6
+ OsNo476dbL6yYSyN6DzVXn/C+Pguf49vgJ8F51mDMAqMK2Kgifyw/6eSQ1MeujYCKakB6ER0AD
+ KWMNZAFMI3nr05KmboDenhBx0sWKOQCeGI0OpvnoumVceiYNn83tAmgokS0sZA3BCA3Dw/eMu0
+ ojApQaTjNDFOmGAc6QymmsF9c8F1rV+9dQNwE0Je9IaGhvSkJ/b19D52nrpjiF4LOpIfg/CP0u
+ Wn8XcX8qKMYB05VkKP2DHJUFU4NKD0AzdNwKRnAyCXCJzFbpOQHg0vK71y6duAP6UkEQDU+sBD
+ rp+BW9sQPM0FHWsiQVfqwVPiroQlHT1OfB9F++GereUEWsEv83nMS1cMogKXTKCiXUCnhr6YYD
+ L2BwL8nm+2kJQuy18Fi4/IwPwW0Nj9QA/vIu1bc8KurYD+OlnbRunreixqBuedxlm7LmM90aco
+ fIl4hlBsvSMICJ0zQim1AkntMJt4SfcBzu3i9UBKcfvPbMXkH+WkBsVc9V7bwxo+lqqD7+g/Ts
+ HnoT1oTl1cPcLDcxNqwefpWcZGTGCLjWImKGnRmDNgAdE1YwBIpUlgjqAvUP4WRqAf4CEC+Vi0
+ ElVby7Q+dpedVfbxhFNy6+nUYHuD+xGA7x3VmcCfSMYNUMHRuhGnYAGKKcGwDogmtQB3EDIjn2
+ PgHkp1575C8m9+URf9ep9pXAM9Rz85Au3BX/nE12GBuDUC0boZp0QOCpzA2OAsTkezuP4dQCTB
+ t7CGxnM4cUk+byvevW+0Nm7n1Hws4810pAvqA2+/xkNcI4xQUdGWHrGhPTQszohzCPOUnt/IM4
+ DNI4iacDcX2xz1LqLrdQEYn/XcPdL8Fl2jpExI3QUFXqpTpC9fex/BDeIYhpQatPAm0XMWBing
+ u1//74fai/qQNk98FlerjOBvhGiTTTCEp4RulEn4KHQx0IDjM2JpGlA0A2UQHnDp/3gelFvJde
+ BNzEAJ74RlokZoW/qBP9xyiMGbxOD3YBG1w0wxeCuorZ+cL2kb3/4GbxXnGdVLjTC8nPGo0If1
+ AnBij2BBgZwicxRCodCJ2Dqltp+eL2kkquPwXvleUZ8I6zohhG6XCec1o2b3yn6b9E3inIZm43
+ dQB448qIAGQ1/+90/+gH2gubsbwTvVRfQABc6NYIvgibVel/UCXhfYKPRN4zEYlDNFIO6KFB+9
+ ZN+gD097/DlD+C9+iJjAE58I+iZIWB4OkiwWOuLOiFo6P55xg0wNjtKPwqs2netH2IPFXO0hTG
+ AVhf0zKAzgl9kLgQF7+6TOkE+/ehPnb5pNEYBDRcFSEfgPKUIMg/mQI3q8nMxiDG71f8Vrv41l
+ 1hdNGoEAjlg2EEI9k8F/4jsPqkT8Hawo50bYGx2LIkCTEfAzAVWbVdqR7H9ZuiaNh5rBa+1l6g
+ ERuCZgYT7wND99BPXggJS6Xm9zgvGrs0TyABoiG+SZacGcB2TacFEAdIRHKY3jngNTYXF09dDc
+ kKaYC6feTAbzVDXb4YOVr/X+xWM1lbojMCawQcBy9/EO3UDd6PSqAECFXspXON1QrlhejChTvA
+ fl3vD5E8OcRmTHY1zAdBNBz8At/+YCu7/PhGG2EfBImqGVIEZMvrNYFj5ZzSB17pKnQl4RpDMO
+ gEBQw9AYNBu1gCMCfzG53VaJ3R1niDB3j9Ekqzo0odH4XSwXVcQFoKz1yZw++MEVuPBFRViPxP
+ NsK7fDCIqqP0YvNZXMgbQqgKGrzpHcz35jMXAIE46ExBgnRWMFC7mdFONEDA6q7XLnx7mOiZLg
+ akAaCogI+JxBQL4rn8cBy5/YOT8h0hQ2M+AhdPfFzVD9UtmBhL6g7bWogGqqIK21sDGotv06wE
+ Y4gOD9zIG0DOBfGK+0TqBbwS/cUoK1pTBkhSPf4VIdyq69QGSWBCW6FLBEXBxWCEKn9FYcPrfR
+ GMgyG46LJi2FpJeUjNM3nsNvDZUweQ9jVB87VM6BiZfv3b9Y2oAqmBDI3gvKzeoE/SNQODTLqH
+ DeQJrBCwAg0L2Xu32J4i6RmRYYCrQ0K6AmCAyD1z/7+QO4TtSRYAD0SsREGg7FeZPW/PSmOFS6
+ +eQX/cxrvYfDY+K51xF+Pt0JuBFAynerdtRwUhMQOoDUiiSM/0GgyWROoF8v0KWohjQkwu7gkg
+ 0AXCtoZPHBpPhO7wyGuypRlEF2EyB+VPRDPG7DMxQW1P/Py96ZJg5+ygEhOxjxDOC/7B0CpmrE
+ 4RF4yUK158MiAKZLqGjeQJnBHL+XyFPKemVj5HHekBJ6gFqgrG5/3L+07Quw7f7/UgqW6oR4G/
+ zFsybutrADAX5Bd9cqb/604tmhk8+/Rb8Q/brDMAzgQ+OamnBuF5YMBIDkA0c0i1wRSJ5d4/O6
+ gTS9wcF7f4u2G+XZa8YwC0i3QJNoNaaIGRXd+BrOPhENr8brpXc+k00wypISUr7mW+G/BfIDIU
+ fNNNPWCcK4BmBACX1AlcwckagIRzzPVcgUuH3E8Ad1Qk+mPsDyUTRb1fsgN683CIyLNEEGq0Jn
+ FZ2deVHI/gSffjWvwvXyuq3w2CUfHYSwk9FtfPNkJeX/1VtTZ3meTXD+CkFWgNQoQn8RmMxR8L
+ 8BhVjAhQBLMPhkFinQCaEHQ+WWPj+qeoBfXGhCRRoAqAmiMgCx7/M6ErYp/kIoSuNwSca/NuhG
+ pQH+V4E7yFmBqVS+fn58gs/Pq3bvHqqa02PwT/0ACPWAH4k7+Mq94xRgedGFYb6UtoGCopEXqf
+ gPyLTID0I6gTM+wHh6cw42T/VY0BfXZgOoqgJSGEYlgYO/2e8STkfpeF+BsKPQugaEficDBxsz
+ Aw52TkaczfDloQK8OMMwMoXb87wWXwGpFOOgN/QdNHagN8pkKmesTqBRA2yhczsJaTFDujrizF
+ BJjWBk1+cKfC5kB/J/QwE74FSi8CnsvxNWKqx/9+YGbLN0AyffPoE/MIOshKaQJAS9GoDvhFkU
+ 8XrBGICEkUChmdwRaJ6wNO63EYfjHKNICbIBidJrCnwiZT6PwfhR7Nhnw+fVWinPaxxM2R/d67
+ s/D+etRm2JFbyDMAqtAMj6JnAb1Q2eG1UCeoEzgzkgEcA/hu2SNSgASwHPM2LmADrAiApwdHhv
+ c7gc/neQv/nIHQLVCyqnQcfBv0mVIOyMPXxmJsZ6OrH8M5IzwQmRAN//Df8OsGL0wbmnUH1Rsm
+ RA57F5T7qQJTb6HTAqSE4Wi/sDD5RVEc/D+FHInwlgm8fRE0wRNmdx2XUDFlPzwwLV5wGORZ7c
+ q0J0rsUDcg5fs+Yap5UtMf3G5WlP0pOHfAsL/dR+yPdR+z7hRjByX0NdNLqdSlPDfzNkB5XtM/
+ CDJW1HyL8DJ7SuxQNpFMKwXNTDatq8MI2Tz6xwKBIRBOoBpjD5TFyr4dbeJoGIwI4ea+DTlo9x
+ bN6nE/DDORt9cNwDi8Pz2DEM4Ep0UA+Nhc8Y2uoyLavbNIhY1PEdpTFAHO5PEbssXAPTb7rPnI
+ fuAYngM0fRoORVk9pDo+3r8ywctN5kIVnIvxMERNkCE0gEg1855WAdOpR8MN/10GnoEF5DDDHy
+ 1WyMRvNAO7DdoHdX6eBkVbP0pwec2+Z4XT5HZANz0JlMjJqBL4J0rvaKZgvfO1ZAs/VI92D4v7
+ hOXw3OLksE+vzleb62LtrhrZ7X4BsRBYjE03QWTQQMYEGZd7wBUZwX1XsGZr0L/fgeLB99S39P
+ t/S3B+/MTNkZWb9UHa2/GfODCTvh04oQPjZeibo1WigQT0/8LnLyX7RIBfXZWqvYTvB2W05WP1
+ 7BNfnK5+n52HMDPv2Z/045u38f0lHZgMV3wS9Fw2eT/iCO5DtFwa4uq9QeQVu+aeT7XywfCWc9
+ PmWz+Nz4cywe0/64xFTEPqoHISfA0IT9Fo0UKMsBrxIl4vT4ng3j+XN9m9Mj3len4NkdK6FZFS
+ uWkLgc+KboHeigRKjwYsF/0W4JBFKCzSAGgVUfBP0JBoIh0fR/a+0GV6+EUoPNIAaBVoDUBPoG
+ WFkjhoNoOlGNFCjATz6X2lzhD8mT4HS+BL4fAmMQA1QgkrtRjRIRQP0h3yzhD82Lxrhg05K8BU
+ 3QTRGA48u1gZqNED/qjfHyycy38JnbH6J79h8QBOA0AR5fBOoJCQ9kOJwdG67ibWBBg0Q1f8qm
+ y38gkiUBk0APtQAnAQmaMdoEKUtDkl90HFtAGxtEIvqD/fmeHmPO2ThM66gBAVoAFb5IDQCAZ8
+ XxasPLHy54lC/LtBFAw0qFtUP3mzhjz8UjdKgCcCHim8CaoQSNEGkXnHogVJ3UBuQlBCL0aAfv
+ NmCn3BYgVJ7jz8MaABG4xix0SAKDWBhWBzmR2I00BipDUhd0J/jzfnymnhE4TXhiAoFaABG44k
+ RDqWiIlEWRuoDC4wGqSK1AUkNpGOw7H91zRn8pEJLr4mFqWgAFYr8Go0mUJhUHI6jxWE7rzZQo
+ xGi0QT9rdwLXh8oMCWofKgKUtkuoT+v91/P3/X/Afw1kptmVhryAAAAAElFTkSuQmCC
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/head.js b/comm/mailnews/addrbook/test/unit/head.js
new file mode 100644
index 0000000000..7a5155ea26
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/head.js
@@ -0,0 +1,66 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+// Import the required setup scripts.
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
+
+function promiseDirectoryRemoved(uri) {
+ let removePromise = TestUtils.topicObserved("addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(uri);
+ return removePromise;
+}
+
+function acObserver() {}
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+ _resolve: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ this._resolve();
+ },
+
+ waitForResult() {
+ return new Promise(resolve => {
+ this._resolve = resolve;
+ });
+ },
+};
+
+function formatVCard(strings, ...values) {
+ let arr = [];
+ for (let str of strings) {
+ arr.push(str);
+ arr.push(values.shift());
+ }
+ let lines = arr.join("").split("\n");
+ let indent = lines[1].length - lines[1].trimLeft().length;
+ let outLines = [];
+ for (let line of lines) {
+ if (line.length > 0) {
+ outLines.push(line.substring(indent) + "\r\n");
+ }
+ }
+ return outLines.join("");
+}
diff --git a/comm/mailnews/addrbook/test/unit/head_cardDAV.js b/comm/mailnews/addrbook/test/unit/head_cardDAV.js
new file mode 100644
index 0000000000..5c6ac62f61
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/head_cardDAV.js
@@ -0,0 +1,149 @@
+/* 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 { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+Cu.importGlobalProperties(["fetch"]);
+
+do_get_profile();
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
+
+function initDirectory() {
+ // Set up a new directory and get the cards from the server. Do this by
+ // creating an instance of CardDAVDirectory rather than through the address
+ // book manager, so that we can access the internals of the directory.
+
+ Services.prefs.setIntPref("ldap_2.servers.carddav.carddav.syncinterval", 0);
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.carddav.url",
+ CardDAVServer.url
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.carddav.username",
+ "bob"
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.description",
+ "CardDAV Test"
+ );
+ Services.prefs.setIntPref(
+ "ldap_2.servers.carddav.dirType",
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.filename",
+ "carddav.sqlite"
+ );
+
+ if (!Services.logins.findLogins(CardDAVServer.origin, null, "test").length) {
+ // Save a username and password to the login manager.
+ let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo.init(CardDAVServer.origin, null, "test", "bob", "bob", "", "");
+ Services.logins.addLogin(loginInfo);
+ }
+
+ let directory = new CardDAVDirectory();
+ directory.init("jscarddav://carddav.sqlite");
+ return directory;
+}
+
+async function clearDirectory(directory) {
+ await directory.cleanUp();
+
+ let database = do_get_profile();
+ database.append("carddav.sqlite");
+ database.remove(false);
+}
+
+async function checkCardsOnServer(expectedCards) {
+ // Send a request to the server. When the server responds, we know it has
+ // completed all earlier requests.
+ await fetch(`${CardDAVServer.origin}/ping`);
+
+ info("Checking cards on server are correct.");
+ let actualCards = [...CardDAVServer.cards];
+ Assert.equal(actualCards.length, Object.keys(expectedCards).length);
+
+ for (let [href, { etag, vCard }] of actualCards) {
+ let baseName = href
+ .substring(CardDAVServer.path.length)
+ .replace(/\.vcf$/, "");
+ info(baseName);
+ Assert.equal(etag, expectedCards[baseName].etag);
+ Assert.equal(href, expectedCards[baseName].href);
+ // Decode the vCard which is stored as UTF-8 on the server.
+ vCard = new TextDecoder().decode(
+ Uint8Array.from(vCard, c => c.charCodeAt(0))
+ );
+ vCardEqual(vCard, expectedCards[baseName].vCard);
+ }
+}
+
+let observer = {
+ notifications: {
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ },
+ pendingPromise: null,
+ init() {
+ if (this.isInited) {
+ return;
+ }
+ this.isInited = true;
+
+ for (let key of Object.keys(this.notifications)) {
+ Services.obs.addObserver(observer, key);
+ }
+ },
+ checkAndClearNotifications(expected) {
+ Assert.deepEqual(this.notifications, expected);
+ for (let array of Object.values(this.notifications)) {
+ array.length = 0;
+ }
+ },
+ observe(subject, topic) {
+ let uid = subject.QueryInterface(Ci.nsIAbCard).UID;
+ info(`${topic}: ${uid}`);
+ if (this.pendingPromise && this.pendingPromise.topic == topic) {
+ let promise = this.pendingPromise;
+ this.pendingPromise = null;
+ promise.resolve(uid);
+ return;
+ }
+ this.notifications[topic].push(uid);
+ },
+ waitFor(topic) {
+ return new Promise(resolve => {
+ this.pendingPromise = { resolve, topic };
+ });
+ },
+};
+
+add_task(async () => {
+ CardDAVServer.open("bob", "bob");
+ registerCleanupFunction(async () => {
+ await CardDAVServer.close();
+ });
+});
+
+// Checks two vCard strings have the same lines, in any order.
+// Not very smart but smart enough.
+function vCardEqual(lhs, rhs, message) {
+ let lhsLines = lhs.split("\r\n").sort();
+ let rhsLines = rhs.split("\r\n").sort();
+ Assert.deepEqual(lhsLines, rhsLines, message);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js
new file mode 100644
index 0000000000..ebcb746f21
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+/**
+ * Tests for LDAPMessage.jsm.
+ */
+
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { LDAPResponse, SearchRequest } = ChromeUtils.import(
+ "resource:///modules/LDAPMessage.jsm"
+);
+
+/**
+ * Test filter string is converted to asn1 blocks correctly.
+ */
+add_task(function test_SearchRequest_filter() {
+ let req = new SearchRequest(
+ "ou=people,dc=planetexpress,dc=com",
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ "(memberof=cn=ship_crew,ou=people,dc=planetexpress,dc=com)",
+ "",
+ 0,
+ 0
+ );
+ let filterBlock = req.protocolOp.valueBlock.value[6];
+ let [filterKeyBlock, filterValueBlock] = filterBlock.valueBlock.value;
+ let filterKey = new TextDecoder().decode(filterKeyBlock.valueBlock.valueHex);
+ let filterValue = new TextDecoder().decode(
+ filterValueBlock.valueBlock.valueHex
+ );
+ Assert.equal(filterKey, "memberof", "Filter key should be correct");
+ Assert.equal(
+ filterValue,
+ "cn=ship_crew,ou=people,dc=planetexpress,dc=com",
+ "Filter value should be correct"
+ );
+});
+
+/**
+ * Test extensibleMatch filter is encoded correctly.
+ */
+add_task(function test_extensibleMatchFilter() {
+ // Test data is from https://ldap.com/ldapv3-wire-protocol-reference-search/.
+ // filter string, BER payload, description
+ let filterBER = [
+ [
+ "(uid:dn:caseIgnoreMatch:=jdoe)",
+ "a91f810f6361736549676e6f72654d61746368820375696483046a646f658401ff",
+ "<type>:dn:<rule>:=<value>",
+ ],
+ ["(uid:=jdoe)", "a90b820375696483046a646f65", "<type>:=<value>"],
+ [
+ "(:caseIgnoreMatch:=foo)",
+ "a916810f6361736549676e6f72654d617463688303666f6f",
+ ":<rule>:=<value>",
+ ],
+ // This one is not directly from ldap.com, but assembled from the above cases.
+ [
+ "(uid:caseIgnoreMatch:=jdoe)",
+ "a91c810f6361736549676e6f72654d61746368820375696483046a646f65",
+ "<type>:<rule>:=<value>",
+ ],
+ ];
+ for (let [filter, ber, description] of filterBER) {
+ let req = new SearchRequest(
+ "ou=people,dc=planetexpress,dc=com",
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ filter,
+ "",
+ 0,
+ 0
+ );
+ let filterBlock = req.protocolOp.valueBlock.value[6];
+ Assert.equal(
+ CommonUtils.bufferToHex(new Uint8Array(filterBlock.toBER())),
+ ber,
+ description
+ );
+ }
+});
+
+/**
+ * Test parsing to SearchResultReference works.
+ */
+add_task(function test_SearchResultReference() {
+ // A BER payload representing a SearchResultReference with two urls, test data
+ // is from https://ldap.com/ldapv3-wire-protocol-reference-search/.
+ let hex =
+ "306d020102736804326c6461703a2f2f6473312e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f04326c6461703a2f2f6473322e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f";
+ let res = LDAPResponse.fromBER(CommonUtils.hexToArrayBuffer(hex).buffer);
+
+ // Should be correctly parsed.
+ Assert.equal(res.constructor.name, "SearchResultReference");
+ Assert.deepEqual(res.result, [
+ "ldap://ds1.example.com:389/dc=example,dc=com??sub?",
+ "ldap://ds2.example.com:389/dc=example,dc=com??sub?",
+ ]);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js
new file mode 100644
index 0000000000..9e2d1cc97e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js
@@ -0,0 +1,66 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/**
+ * Test suite for nsILDAPSyncQuery.
+ */
+
+const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Ldapd.jsm"
+);
+const { BinaryServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Binaryd.jsm"
+);
+
+const nsILDAPSyncQuery = Ci.nsILDAPSyncQuery;
+const LDAPSyncQueryContractID = "@mozilla.org/ldapsyncquery;1";
+
+function getLDAPAttributes(urlSpec) {
+ let url = Services.io.newURI(urlSpec).QueryInterface(Ci.nsILDAPURL);
+ let ldapquery = Cc[LDAPSyncQueryContractID].createInstance(nsILDAPSyncQuery);
+ let payload = ldapquery.getQueryResults(url, Ci.nsILDAPConnection.VERSION3);
+ // Returns a string with one attr per line.
+ return payload;
+}
+
+add_task(async function test_LDAPSyncQuery() {
+ // Set up fake LDAP server, loaded with some contacts.
+ let daemon = new LDAPDaemon();
+ let raw = await IOUtils.readUTF8(
+ do_get_file(
+ "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json"
+ ).path
+ );
+ let testContacts = JSON.parse(raw);
+ daemon.add(...Object.values(testContacts));
+ // daemon.setDebug(true);
+
+ let server = new BinaryServer(LDAPHandlerFn, daemon);
+ server.start();
+
+ // Fetch only the Holmes family.
+ let out = getLDAPAttributes(
+ `ldap://localhost:${server.port}/??sub?(sn=Holmes)`
+ );
+ if (daemon.debug) {
+ dump(`--- getLDAPAttributes() ---\n${out}\n--------------------\n`);
+ }
+
+ // Make sure we got the contacts we expected:
+ Assert.ok(out.includes("cn=Eurus Holmes"));
+ Assert.ok(out.includes("cn=Mycroft Holmes"));
+ Assert.ok(out.includes("cn=Sherlock Holmes"));
+
+ // Sanity check: make sure some non-Holmes people were excluded.
+ Assert.ok(!out.includes("cn=John Watson"));
+ Assert.ok(!out.includes("cn=Jim Moriarty"));
+
+ // Fetch again but this time the filter is without parens.
+ out = getLDAPAttributes(`ldap://localhost:${server.port}/??sub?sn=Holmes`);
+
+ // Make sure we got the contacts we expected:
+ Assert.ok(out.includes("cn=Eurus Holmes"));
+ Assert.ok(out.includes("cn=Mycroft Holmes"));
+ Assert.ok(out.includes("cn=Sherlock Holmes"));
+
+ server.stop();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_abCardProperty.js b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js
new file mode 100644
index 0000000000..5feae230dc
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic nsIAbCard functions.
+ */
+
+// Intersperse these with UTF-8 values to check we handle them correctly.
+var kFNValue = "testFirst\u00D0";
+var kLNValue = "testLast";
+var kDNValue = "testDisplay\u00D1";
+var kEmailValue = "testEmail\u00D2@foo.invalid";
+var kEmailValueLC = "testemail\u00D2@foo.invalid";
+var kEmailValue2 = "test@test.foo.invalid";
+// Email without the @ or anything after it.
+var kEmailReducedValue = "testEmail\u00D2";
+var kCompanyValue = "Test\u00D0 Company";
+
+add_task(function testAbCardProperty() {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ // Test - Set First, Last and Display Names and Email Address
+ // via setProperty, and check correctly saved via their
+ // attributes. We're using firstName to check UTF-8 values.
+ card.setProperty("FirstName", kFNValue);
+ card.setProperty("LastName", kLNValue);
+ card.setProperty("DisplayName", kDNValue);
+ card.setProperty("PrimaryEmail", kEmailValue);
+
+ Assert.equal(card.firstName, kFNValue);
+ Assert.equal(card.lastName, kLNValue);
+ Assert.equal(card.displayName, kDNValue);
+ Assert.equal(card.primaryEmail, kEmailValue);
+
+ // Repeat in the opposite order.
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+ card.displayName = kDNValue;
+ card.primaryEmail = kEmailValue;
+
+ Assert.equal(card.getProperty("FirstName", "BAD"), kFNValue);
+ Assert.equal(card.getProperty("LastName", "BAD"), kLNValue);
+ Assert.equal(card.getProperty("DisplayName", "BAD"), kDNValue);
+ Assert.equal(card.getProperty("PrimaryEmail", "BAD"), kEmailValue);
+
+ // Test - generateName. Note: if the addressBook.properties
+ // value changes, this will affect these tests.
+
+ const {
+ GENERATE_DISPLAY_NAME,
+ GENERATE_LAST_FIRST_ORDER,
+ GENERATE_FIRST_LAST_ORDER,
+ } = Ci.nsIAbCard;
+
+ // Add a company name, so we can test fallback to company name.
+ card.setProperty("Company", kCompanyValue);
+
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue);
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kLNValue + ", " + kFNValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kFNValue + " " + kLNValue
+ );
+
+ // Test - generateName, with missing items.
+
+ card.displayName = "";
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kCompanyValue);
+
+ card.deleteProperty("Company");
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kEmailReducedValue);
+
+ // Reset company name for the first/last name tests.
+ card.setProperty("Company", kCompanyValue);
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue);
+
+ card.firstName = kFNValue;
+ card.lastName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue);
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kCompanyValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kCompanyValue);
+
+ card.deleteProperty("Company");
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kEmailReducedValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kEmailReducedValue
+ );
+
+ card.primaryEmail = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), "");
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), "");
+
+ // Test - generateNameWithBundle, most of this will have
+ // been tested above.
+
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+
+ Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue);
+
+ // Test - generatePhoneticName
+
+ card.setProperty("PhoneticFirstName", kFNValue);
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue);
+
+ card.setProperty("PhoneticLastName", "");
+ Assert.equal(card.generatePhoneticName(false), kFNValue);
+ Assert.equal(card.generatePhoneticName(true), kFNValue);
+
+ card.setProperty("PhoneticFirstName", "");
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue);
+
+ // Test - emailAddresses
+
+ card.deleteProperty("PrimaryEmail");
+ card.deleteProperty("SecondEmail");
+ Assert.deepEqual(card.emailAddresses, []);
+
+ card.primaryEmail = kEmailValue;
+ Assert.deepEqual(card.emailAddresses, [kEmailValue]);
+
+ card.setProperty("SecondEmail", kEmailValue2);
+ Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, [kEmailValue2]);
+
+ card.deleteProperty("SecondEmail");
+ Assert.deepEqual(card.emailAddresses, []);
+
+ // Test - hasEmailAddress
+
+ card.deleteProperty("PrimaryEmail");
+ card.deleteProperty("SecondEmail");
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.setProperty("PrimaryEmail", kEmailValue);
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.setProperty("SecondEmail", kEmailValue2);
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+
+ card.deleteProperty("PrimaryEmail");
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_addrBookCard.js b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js
new file mode 100644
index 0000000000..bf5a12b1dd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js
@@ -0,0 +1,260 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic nsIAbCard functions.
+ */
+
+const { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+const { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+// Intersperse these with UTF-8 values to check we handle them correctly.
+var kFNValue = "testFirst\u00D0";
+var kLNValue = "testLast";
+var kDNValue = "testDisplay\u00D1";
+var kEmailValue = "testEmail\u00D2@foo.invalid";
+var kEmailValueLC = "testemail\u00D2@foo.invalid";
+var kEmailValue2 = "test@test.foo.invalid";
+// Email without the @ or anything after it.
+var kEmailReducedValue = "testEmail\u00D2";
+
+add_task(function testAddrBookCard() {
+ let card = new AddrBookCard();
+
+ // Test - Set First, Last and Display Names and Email Address
+ // via setProperty, and check correctly saved via their
+ // attributes. We're using firstName to check UTF-8 values.
+ card.vCardProperties.addValue("n", [kLNValue, kFNValue, "", "", ""]);
+ card.vCardProperties.addValue("fn", kDNValue);
+ card.vCardProperties.addValue("email", kEmailValue);
+
+ Assert.equal(card.firstName, kFNValue);
+ Assert.equal(card.lastName, kLNValue);
+ Assert.equal(card.displayName, kDNValue);
+ Assert.equal(card.primaryEmail, kEmailValue);
+
+ // Repeat in the opposite order.
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+ card.displayName = kDNValue;
+ card.primaryEmail = kEmailValue;
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ kLNValue,
+ kFNValue,
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.vCardProperties.getFirstValue("fn"), kDNValue);
+ Assert.equal(card.vCardProperties.getFirstValue("email"), kEmailValue);
+
+ // Test - generateName. Note: if the addressBook.properties
+ // value changes, this will affect these tests.
+
+ const {
+ GENERATE_DISPLAY_NAME,
+ GENERATE_LAST_FIRST_ORDER,
+ GENERATE_FIRST_LAST_ORDER,
+ } = Ci.nsIAbCard;
+
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue);
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kLNValue + ", " + kFNValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kFNValue + " " + kLNValue
+ );
+
+ // Test - generateName, with missing items.
+
+ card.displayName = "";
+ Assert.equal(
+ card.generateName(GENERATE_DISPLAY_NAME),
+ kFNValue + " " + kLNValue
+ );
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue);
+
+ card.firstName = kFNValue;
+ card.lastName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue);
+
+ card.firstName = "";
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kEmailReducedValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kEmailReducedValue
+ );
+
+ card.vCardProperties.clearValues("email");
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), "");
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), "");
+
+ // Test - generateNameWithBundle, most of this will have
+ // been tested above.
+
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+
+ Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue);
+
+ // Test - generatePhoneticName
+
+ card.setProperty("PhoneticFirstName", kFNValue);
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue);
+
+ card.setProperty("PhoneticLastName", "");
+ Assert.equal(card.generatePhoneticName(false), kFNValue);
+ Assert.equal(card.generatePhoneticName(true), kFNValue);
+
+ card.setProperty("PhoneticFirstName", "");
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue);
+
+ // Test - emailAddresses
+
+ Assert.deepEqual(card.emailAddresses, []);
+
+ card.primaryEmail = kEmailValue;
+ Assert.deepEqual(card.emailAddresses, [kEmailValue]);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue2)
+ );
+ Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, [kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, []);
+
+ // Test - primaryEmail
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", "three@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", { pref: 2 }, "text", "two@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", "four@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", { pref: 1 }, "text", "one@invalid")
+ );
+ Assert.deepEqual(card.emailAddresses, [
+ "one@invalid",
+ "two@invalid",
+ "three@invalid",
+ "four@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "one@invalid");
+
+ // Setting primaryEmail to the existing value changes nothing.
+ card.primaryEmail = "one@invalid";
+ Assert.deepEqual(card.emailAddresses, [
+ "one@invalid",
+ "two@invalid",
+ "three@invalid",
+ "four@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "one@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", "2", undefined, undefined]
+ );
+
+ // Setting primaryEmail to another existing address replaces the address with the new one.
+ card.primaryEmail = "four@invalid";
+ Assert.deepEqual(card.emailAddresses, [
+ "four@invalid",
+ "two@invalid",
+ "three@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "four@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", "2", undefined]
+ );
+
+ // Setting primaryEmail to null promotes the next address.
+ card.primaryEmail = null;
+ Assert.deepEqual(card.emailAddresses, ["two@invalid", "three@invalid"]);
+ Assert.equal(card.primaryEmail, "two@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", undefined]
+ );
+
+ // Setting primaryEmail to a new address replaces the address with the new one.
+ card.primaryEmail = "five@invalid";
+ Assert.deepEqual(card.emailAddresses, ["five@invalid", "three@invalid"]);
+ Assert.equal(card.primaryEmail, "five@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", undefined]
+ );
+
+ // Setting primaryEmail to an empty string promotes the next address.
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, ["three@invalid"]);
+ Assert.equal(card.primaryEmail, "three@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1"]
+ );
+
+ // Setting primaryEmail to null clears the only address.
+ card.primaryEmail = null;
+ Assert.deepEqual(card.emailAddresses, []);
+ Assert.equal(card.primaryEmail, "");
+
+ // Test - hasEmailAddress
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue)
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue2)
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+
+ card.vCardProperties.removeEntry(
+ card.vCardProperties.getAllEntries("email")[0]
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js
new file mode 100644
index 0000000000..1b76099227
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic address book functions - tests obtaining the (default)
+ * personal address book and getting its details from the nsIAbDirectory.
+ *
+ * Functions/attributes not currently tested:
+ * - lastModifiedDate
+ * - childNodes
+ * - childCards
+ * - deleteDirectory
+ * - hasCard
+ * - hasDirectory
+ * - addCard
+ * - modifyCard
+ * - deleteCards
+ * - dropCard
+ * - addressLists
+ * - addMailList
+ * - listNickName
+ * - description
+ * - editMailListToDatabase
+ * - copyMailList
+ */
+
+// Main function for the this test so we can check both personal and
+// collected books work correctly in an easy manner.
+function check_ab(abConfig) {
+ // Test - Get the directory
+
+ let AB = MailServices.ab.getDirectory(abConfig.URI);
+
+ // Test - Is it the right type?
+
+ if (abConfig.dirType == 2) {
+ Assert.ok(AB instanceof Ci.nsIAbMDBDirectory);
+ }
+
+ // Test - Check attributes
+
+ Assert.equal(AB.propertiesChromeURI, kNormalPropertiesURI);
+ Assert.equal(AB.readOnly, abConfig.readOnly);
+ Assert.equal(AB.dirName, abConfig.dirName);
+ Assert.equal(AB.dirType, abConfig.dirType);
+ Assert.equal(AB.fileName, abConfig.fileName);
+ Assert.equal(AB.URI, abConfig.URI);
+ Assert.equal(AB.position, abConfig.position);
+ Assert.equal(AB.isMailList, false);
+ Assert.equal(AB.isRemote, false);
+ Assert.equal(AB.isSecure, false);
+ Assert.equal(AB.supportsMailingLists, true);
+ Assert.equal(AB.dirPrefId, abConfig.dirPrefID);
+
+ // Test - autocomplete enable/disable
+
+ // enable is the default
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", false);
+ Assert.equal(AB.useForAutocomplete(""), false);
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", true);
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ AB.setBoolValue("enable_autocomplete", false);
+ Assert.equal(AB.useForAutocomplete(""), false);
+
+ AB.setBoolValue("enable_autocomplete", true);
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ // Test - check getting default preferences
+
+ Assert.equal(AB.getIntValue("random", 54321), 54321);
+ Assert.equal(AB.getBoolValue("random", false), false);
+ Assert.equal(AB.getStringValue("random", "abc"), "abc");
+ Assert.equal(AB.getLocalizedStringValue("random", "xyz"), "xyz");
+
+ // Test - check get/set int preferences on nsIAbDirectory
+
+ AB.setIntValue("inttest", 12345);
+ Assert.equal(
+ Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"),
+ 12345
+ );
+ Assert.equal(AB.getIntValue("inttest", -1), 12345);
+
+ AB.setIntValue("inttest", 123456);
+ Assert.equal(
+ Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"),
+ 123456
+ );
+ Assert.equal(AB.getIntValue("inttest", -2), 123456);
+
+ // Test - check get/set bool preferences on nsIAbDirectory
+
+ AB.setBoolValue("booltest", true);
+ Assert.equal(
+ Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"),
+ true
+ );
+ Assert.equal(AB.getBoolValue("booltest", false), true);
+
+ AB.setBoolValue("booltest", false);
+ Assert.equal(
+ Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"),
+ false
+ );
+ Assert.equal(AB.getBoolValue("booltest", true), false);
+
+ // Test - check get/set string preferences on nsIAbDirectory
+
+ AB.setStringValue("stringtest", "tyu");
+ Assert.equal(
+ Services.prefs.getCharPref(abConfig.dirPrefID + ".stringtest"),
+ "tyu"
+ );
+ Assert.equal(AB.getStringValue("stringtest", ""), "tyu");
+}
+
+function run_test() {
+ // Check the default personal address book
+ check_ab(kPABData);
+
+ // Check the default collected address book
+ check_ab(kCABData);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug1522453.js b/comm/mailnews/addrbook/test/unit/test_bug1522453.js
new file mode 100644
index 0000000000..b2da2eba12
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug1522453.js
@@ -0,0 +1,72 @@
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+add_task(async function () {
+ do_get_profile();
+ MailServices.ab.directories;
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "list";
+ list = book.addMailList(list);
+
+ let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact1.firstName = "contact";
+ contact1.lastName = "1";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = book.addCard(contact1);
+ list.addCard(contact1);
+
+ let contact2 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact2.firstName = "contact";
+ contact2.lastName = "2";
+ // No email address!
+ contact2 = book.addCard(contact2);
+ list.addCard(contact2);
+
+ let contact3 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact3.firstName = "contact";
+ contact3.lastName = "3";
+ contact3.primaryEmail = "contact3@invalid";
+ contact3 = book.addCard(contact3);
+ list.addCard(contact3);
+
+ // book.childCards should contain the list and all three contacts.
+ let bookCards = book.childCards;
+ equal(bookCards.length, 1 + 3);
+ equal(list.UID, bookCards[0].UID);
+ equal(contact1.UID, bookCards[1].UID);
+ equal(contact2.UID, bookCards[2].UID);
+ equal(contact3.UID, bookCards[3].UID);
+
+ // list.childCards should contain contacts 1 and 3, and crucially, not die at 2.
+ let listCards = list.childCards;
+ equal(listCards.length, 2);
+ equal(contact1.UID, listCards[0].UID);
+ equal(contact3.UID, listCards[1].UID);
+
+ // Reload the address book manager.
+ let reloadPromise = TestUtils.topicObserved("addrbook-reloaded");
+ Services.obs.notifyObservers(null, "addrbook-reload");
+ await reloadPromise;
+
+ // Renew our references.
+ book = MailServices.ab.getDirectory(kPABData.URI);
+ list = book.childNodes[0];
+
+ // list.childCards should contain contacts 1 and 3.
+ listCards = list.childCards;
+ equal(listCards.length, 2);
+ equal(contact1.UID, listCards[0].UID);
+ equal(contact3.UID, listCards[1].UID);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_bug1769889.js b/comm/mailnews/addrbook/test/unit/test_bug1769889.js
new file mode 100644
index 0000000000..37cc91fa45
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug1769889.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+/**
+ * Tests that complex names are correctly flattened when stored in the
+ * database as FirstName/LastName, and when returned from the
+ * firstName/lastName getters.
+ */
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+add_task(async function testMultiValueLast() {
+ // Multiple last names.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:second-last,last;first;;;
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ ["second-last", "last"],
+ "first",
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.firstName, "first");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(card.lastName, "second-last last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "second-last last");
+});
+
+add_task(async function testMultiValueFirst() {
+ // Multiple first names.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:last;first,second;;;
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ "last",
+ ["first", "second"],
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.firstName, "first second");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first second");
+ Assert.equal(card.lastName, "last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "last");
+});
+
+add_task(async function testNotEnoughValues() {
+ // The name field doesn't have enough components. That's okay.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:last;first
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), ["last", "first"]);
+ Assert.equal(card.firstName, "first");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(card.lastName, "last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "last");
+});
+
+add_task(async function testStringValue() {
+ // This is a bad value. Let's just ignore it for first/last name purposes.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:first last
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), "first last");
+ Assert.equal(card.firstName, "");
+ Assert.equal(card.getProperty("FirstName", "RIGHT"), "RIGHT");
+ Assert.equal(card.lastName, "");
+ Assert.equal(card.getProperty("LastName", "RIGHT"), "RIGHT");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_bug387403.js b/comm/mailnews/addrbook/test/unit/test_bug387403.js
new file mode 100644
index 0000000000..9f8621c705
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug387403.js
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Test for bug 387403 crash when opening e-mail with broken vcard.
+ */
+
+function run_test() {
+ // Before bug 387403 this would hang, eating up all the memory until it
+ // crashed.
+ try {
+ Cc["@mozilla.org/addressbook/msgvcardservice;1"]
+ .getService(Ci.nsIMsgVCardService)
+ .escapedVCardToAbCard(
+ "begin:vcard\nfn;quoted-printable:Xxxx=C5=82xx Xxx\nn;quoted-printable:Xxx;Xxxx=C5=82xx \nadr;quoted-printable;quoted-printable;dom:;;xx. Xxxxxxxxxxxx X;Xxxxxx=C3=3"
+ );
+ } catch (ex) {}
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug448165.js b/comm/mailnews/addrbook/test/unit/test_bug448165.js
new file mode 100644
index 0000000000..57337f7fa1
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug448165.js
@@ -0,0 +1,18 @@
+/**
+ * A simple test to check for a regression of bug 448165: Mailnews crashes in
+ * nsAbMDBDirectory::DeleteCards if aCards is null
+ */
+function run_test() {
+ // get the Personal Address Book
+ let pab = MailServices.ab.getDirectory(kPABData.URI);
+ Assert.ok(pab instanceof Ci.nsIAbDirectory);
+ try {
+ pab.deleteCards(null); // this should throw an error
+ do_throw(
+ "Error, deleteCards should throw an error when null is passed to it"
+ );
+ } catch (e) {
+ // make sure the correct error message was thrown
+ Assert.equal(e.result, Cr.NS_ERROR_XPC_CANT_CONVERT_PRIMITIVE_TO_ARRAY);
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug534822.js b/comm/mailnews/addrbook/test/unit/test_bug534822.js
new file mode 100644
index 0000000000..4c18f64b5d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug534822.js
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for bug 534822 - non-built-in address books specified in preferences
+ * don't appear in address book lists.
+ */
+
+function run_test() {
+ // Read in the prefs that will be default.
+ let specialPrefs = do_get_file("data/bug534822prefs.js");
+
+ var profileDir = do_get_profile();
+ specialPrefs.copyTo(profileDir, "");
+
+ specialPrefs = profileDir;
+ specialPrefs.append("bug534822prefs.js");
+
+ Services.prefs.readUserPrefsFromFile(specialPrefs);
+
+ // Now load the ABs and check we've got all of them.
+ let results = [
+ { name: "extension", result: false },
+ { name: kPABData.dirName, result: false },
+ { name: kCABData.dirName, result: false },
+ ];
+
+ for (let dir of MailServices.ab.directories) {
+ for (let i = 0; i < results.length; ++i) {
+ if (results[i].name == dir.dirName) {
+ Assert.ok(!results[i].result);
+ results[i].result = true;
+ }
+ }
+ }
+
+ results.forEach(function (result) {
+ Assert.ok(result.result);
+ });
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js
new file mode 100644
index 0000000000..1e8e476c7a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js
@@ -0,0 +1,148 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const vCardTemplate =
+ "BEGIN:VCARD\r\nUID:{}\r\nFN:Move me around\r\nEND:VCARD\r\n";
+const initialVCard = vCardTemplate.replace("{}", "copyme");
+
+let cardDAVDirectory, localDirectory;
+let initialCard, localCard;
+
+add_task(async () => {
+ // Put some cards on the server.
+
+ CardDAVServer.putCardInternal("copyme.vcf", initialVCard);
+
+ localDirectory = MailServices.ab.getDirectoryFromId("ldap_2.servers.pab");
+ cardDAVDirectory = initDirectory();
+ await cardDAVDirectory.fetchAllFromServer();
+
+ observer.init();
+
+ // Check we have the initial version of the card.
+
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ initialCard = cardDAVDirectory.childCards[0];
+ Assert.equal(initialCard.UID, "copyme");
+ Assert.equal(initialCard.getProperty("_etag", ""), "55");
+ Assert.equal(
+ initialCard.getProperty("_href", ""),
+ `${CardDAVServer.path}copyme.vcf`
+ );
+ vCardEqual(initialCard.getProperty("_vCard", ""), initialVCard);
+});
+
+/** Copy the card to the local directory. */
+add_task(async function copyCardToLocal() {
+ localDirectory.dropCard(initialCard, true);
+ Assert.equal(localDirectory.childCards.length, 1);
+
+ localCard = localDirectory.childCards[0];
+ // The UID must change, since this is a copy.
+ Assert.notEqual(localCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+});
+
+/** Remove the card from the local directory for the next step. */
+add_task(async function () {
+ localDirectory.deleteCards(localDirectory.childCards);
+ Assert.equal(localDirectory.childCards.length, 0);
+});
+
+/** This time, move the card to the local directory. */
+add_task(async function moveCardToLocal() {
+ localDirectory.addCard(initialCard);
+ Assert.equal(localDirectory.childCards.length, 1);
+
+ localCard = localDirectory.childCards[0];
+ // UID should not change
+ Assert.equal(localCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+});
+
+/**
+ * Okay, let's go back again. First we'll need to remove the card from the
+ * CardDAV directory.
+ */
+add_task(async function () {
+ let deletedPromise = observer.waitFor("addrbook-contact-deleted");
+ cardDAVDirectory.deleteCards(cardDAVDirectory.childCards);
+ await deletedPromise;
+ Assert.equal(cardDAVDirectory.childCards.length, 0);
+});
+
+/** Copy the card back to the CardDAV directory. */
+add_task(async function copyCardToCardDAV() {
+ cardDAVDirectory.dropCard(localCard, true);
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ let newCard = cardDAVDirectory.childCards[0];
+ Assert.notEqual(newCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+
+ await observer.waitFor("addrbook-contact-updated");
+ let newCardAfterSync = cardDAVDirectory.childCards[0];
+ Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "85");
+ Assert.equal(
+ newCardAfterSync.getProperty("_href", "EMPTY"),
+ `${CardDAVServer.path}${newCard.UID}.vcf`
+ );
+ vCardEqual(
+ newCardAfterSync.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", newCard.UID)
+ );
+});
+
+/** Remove the card from the CardDAV directory again. */
+add_task(async function () {
+ let deletedPromise = observer.waitFor("addrbook-contact-deleted");
+ cardDAVDirectory.deleteCards(cardDAVDirectory.childCards);
+ await deletedPromise;
+ Assert.equal(cardDAVDirectory.childCards.length, 0);
+});
+
+/** This time, move the card to the CardDAV directory. */
+add_task(async function moveCardToCardDAV() {
+ cardDAVDirectory.addCard(localCard);
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ let newCard = cardDAVDirectory.childCards[0];
+ // UID should not change
+ Assert.equal(newCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ // _vCard property won't change until we send this card to the server.
+ vCardEqual(localCard.getProperty("_vCard", "EMPTY"), initialVCard);
+
+ await observer.waitFor("addrbook-contact-updated");
+ let newCardAfterSync = cardDAVDirectory.childCards[0];
+ Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "55");
+ Assert.equal(
+ newCardAfterSync.getProperty("_href", "EMPTY"),
+ `${CardDAVServer.path}copyme.vcf`
+ );
+ vCardEqual(newCardAfterSync.getProperty("_vCard", "EMPTY"), initialVCard);
+
+ await clearDirectory(cardDAVDirectory);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js
new file mode 100644
index 0000000000..fa32260328
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js
@@ -0,0 +1,550 @@
+/* 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/. */
+
+// Tests that changes in a CardDAV directory when offline or unable to reach
+// the server are (a) visible in the client immediately, and (b) sent to the
+// server when it's next available.
+//
+// Note that we close the server rather than using Services.io.offline, as
+// the server is localhost and therefore not affected by the offline setting.
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+var directory, restart, useSyncV1;
+
+async function subtest() {
+ // Put some cards on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n"
+ );
+
+ directory = initDirectory();
+
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ if (useSyncV1) {
+ directory._syncToken = null;
+ }
+
+ await subtestCreateCard();
+ await subtestUpdateCard();
+ await subtestDeleteCard();
+ await subtestCreateDeleteCard();
+ await subtestStillOffline();
+
+ // Check everything is still correct at the end.
+
+ info("Checking cards on client are correct.");
+ Assert.deepEqual(
+ directory.childCards.map(c => c.UID).sort(),
+ ["another-new-card", "change-me"],
+ "right cards remain on client"
+ );
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+function promiseSyncFailed() {
+ return TestUtils.topicObserved("addrbook-directory-sync-failed");
+}
+
+function promiseSyncSucceeded() {
+ return TestUtils.topicObserved("addrbook-directory-synced");
+}
+
+/**
+ * The behaviour should remain the same even if Thunderbird restarts.
+ * If `restart` is true, simulate restarting.
+ */
+async function pretendToRestart() {
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ if (!restart) {
+ return;
+ }
+
+ await directory.cleanUp();
+
+ info("Shutdown simulated, now restarting.");
+ directory = new CardDAVDirectory();
+ directory.init("jscarddav://carddav.sqlite");
+}
+
+/** Creating a new card while "offline". */
+async function subtestCreateCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, creating a new card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A New Card";
+ newCard.UID = "a-new-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "a-new-card"),
+ "card should have been added on client"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server before syncing"
+ );
+
+ contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+ let [notificationCard] = await contactPromise;
+ notificationCard.QueryInterface(Ci.nsIAbCard);
+ Assert.equal(
+ notificationCard.UID,
+ "a-new-card",
+ "correct card should have been updated"
+ );
+
+ Assert.equal(
+ notificationCard.getProperty("_href", "WRONG"),
+ `${CardDAVServer.path}a-new-card.vcf`,
+ "card should have been given _href property"
+ );
+ Assert.equal(
+ notificationCard.getProperty("_etag", "WRONG"),
+ "68",
+ "card should have been given _etag property"
+ );
+ vCardEqual(
+ notificationCard.getProperty("_vCard", "WRONG"),
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ "card should have been given _vCard property"
+ );
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "63",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n",
+ },
+ ["delete-me"]: {
+ etag: "61",
+ href: `${CardDAVServer.path}delete-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Changing an existing card while "offline". */
+async function subtestUpdateCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test");
+
+ info("Going offline, changing a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncFailedPromise = promiseSyncFailed();
+ let cardToChange = directory.childCards.find(c => c.UID == "change-me");
+ cardToChange.displayName = "I'm a new man!";
+ cardToChange = directory.modifyCard(cardToChange);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.find(c => c.UID == "change-me").displayName,
+ "I'm a new man!",
+ "card should have been changed on client while offline"
+ );
+ Assert.stringContains(
+ CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard,
+ "I shall be changed.",
+ "card should NOT have been changed on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.stringContains(
+ CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard,
+ "I shall be changed.",
+ "card should NOT have been changed on server before syncing"
+ );
+
+ contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+ let [notificationCard] = await contactPromise;
+ notificationCard.QueryInterface(Ci.nsIAbCard);
+ Assert.equal(
+ notificationCard.UID,
+ "change-me",
+ "correct card should have been updated"
+ );
+
+ Assert.equal(
+ notificationCard.getProperty("_href", "WRONG"),
+ `${CardDAVServer.path}change-me.vcf`,
+ "card _href property didn't change"
+ );
+ Assert.equal(
+ notificationCard.getProperty("_etag", "WRONG"),
+ "58",
+ "card _etag property did change"
+ );
+ vCardEqual(
+ notificationCard.getProperty("_vCard", "WRONG"),
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ "card _vCard property did change"
+ );
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["delete-me"]: {
+ etag: "61",
+ href: `${CardDAVServer.path}delete-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Deleting an existing card while "offline". */
+async function subtestDeleteCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test");
+
+ info("Going offline, deleting a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ let syncFailedPromise = promiseSyncFailed();
+ let cardToDelete = directory.childCards.find(c => c.UID == "delete-me");
+ directory.deleteCards([cardToDelete]);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card should have been removed on client while offline"
+ );
+ Assert.ok(
+ !directory.childCards.find(c => c.UID == "delete-me"),
+ "card should have been removed on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 3,
+ "card should NOT have been removed on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 3,
+ "card should NOT have been removed on server before syncing"
+ );
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Adding a new card and deleting it again while "offline". */
+async function subtestCreateDeleteCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, adding a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A Temporary Card";
+ newCard.UID = "a-temporary-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "a-temporary-card"),
+ "card should have been added on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Deleting the same card before syncing.");
+ contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ directory.deleteCards([newCard]);
+ await contactPromise;
+ // No addrbook-directory-sync-failed notification here, we didn't attempt to
+ // delete a card that wasn't on the server (it had no _href property).
+
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card should have been removed on client while offline"
+ );
+ Assert.ok(
+ !directory.childCards.find(c => c.UID == "a-temporary-card"),
+ "card should have been removed on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/**
+ * Check that doing a sync while offline does nothing crazy. First make both
+ * kinds of changes, then sync while offline.
+ */
+async function subtestStillOffline() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, adding a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "Another New Card";
+ newCard.UID = "another-new-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "another-new-card"),
+ "card should have been added on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Still offline, deleting a card.");
+ let cardToDelete = directory.childCards.find(c => c.UID == "a-new-card");
+ contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ syncFailedPromise = promiseSyncFailed();
+ directory.deleteCards([cardToDelete]);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ info("Still offline, attempting to sync.");
+ syncFailedPromise = promiseSyncFailed();
+ // Assert.rejects eats the thrown exception, so we don't see it logged here.
+ await Assert.rejects(
+ directory.syncWithServer(),
+ /NS_ERROR_CONNECTION_REFUSED/,
+ "Attempt to sync threw an exception"
+ );
+ await syncFailedPromise;
+
+ await pretendToRestart();
+ syncFailedPromise = promiseSyncFailed();
+ // Assert.rejects eats the thrown exception, so we don't see it logged here.
+ await Assert.rejects(
+ directory.syncWithServer(),
+ /NS_ERROR_CONNECTION_REFUSED/,
+ "Attempt to sync threw an exception"
+ );
+ await syncFailedPromise;
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["another-new-card"]: {
+ etag: "80",
+ href: `${CardDAVServer.path}another-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Another New Card\r\nUID:another-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+add_task(async function test_syncV1_noRestart() {
+ restart = false;
+ useSyncV1 = true;
+ await subtest();
+});
+
+add_task(async function test_syncV1_restart() {
+ restart = true;
+ useSyncV1 = true;
+ await subtest();
+});
+
+add_task(async function test_syncV2_noRestart() {
+ restart = false;
+ useSyncV1 = false;
+ await subtest();
+});
+
+add_task(async function test_syncV2_restart() {
+ restart = true;
+ useSyncV1 = false;
+ await subtest();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js
new file mode 100644
index 0000000000..244e27617e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+/* Tests what happens if a server modifies a card when it first arrives.
+ * In this test the server changes the card's UID and path, which Google's
+ * CardDAV server does, and also adds a new property. All changes should be
+ * reflected in the client. */
+
+add_task(async () => {
+ CardDAVServer.modifyCardOnPut = true;
+
+ let directory = initDirectory();
+ await directory.fetchAllFromServer();
+
+ observer.init();
+
+ // Create a new card, and check it has the right UID.
+
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A New Card";
+ newCard.UID = "a-new-card";
+ newCard = directory.addCard(newCard);
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["a-new-card"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(directory.childCards.length, 1);
+ Assert.equal(directory.childCards[0].UID, "a-new-card");
+
+ // Wait for notifications. Both arrive at once so we listen for the first.
+
+ let newUID = await observer.waitFor("addrbook-contact-created");
+ Assert.equal(newUID, "drac-wen-a");
+
+ // Check the original card was deleted.
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["a-new-card"],
+ });
+
+ // Check we have the card as modified by the server.
+
+ Assert.equal(directory.childCards.length, 1);
+ let modifiedCard = directory.childCards[0];
+ Assert.equal(modifiedCard.UID, "drac-wen-a");
+ Assert.equal(modifiedCard.getProperty("_etag", ""), "92");
+ Assert.equal(
+ modifiedCard.getProperty("_href", ""),
+ "/addressbooks/me/test/drac-wen-a.vcf"
+ );
+ Assert.stringContains(
+ modifiedCard.getProperty("_vCard", ""),
+ "UID:drac-wen-a\r\n"
+ );
+ Assert.stringContains(
+ modifiedCard.getProperty("_vCard", ""),
+ "X-MODIFIED-BY-SERVER:1\r\n"
+ );
+
+ await clearDirectory(directory);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js
new file mode 100644
index 0000000000..0ec5a65ae0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js
@@ -0,0 +1,282 @@
+/* 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/. */
+
+async function subtest() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "keep-me.vcf",
+ "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ // This one includes a character encoded with UTF-8.
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ // We'll only use this for the initial sync, so I think it's okay to use
+ // bulkAddCards and not get a notification for every contact.
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ info("Cards:");
+ let cardMap = new Map();
+ let oldETags = new Map();
+ for (let card of directory.childCards) {
+ info(card.displayName);
+ info(card.getProperty("_href", ""));
+ info(card.getProperty("_etag", ""));
+
+ cardMap.set(card.UID, card);
+ oldETags.set(card.UID, card.getProperty("_etag", ""));
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), [
+ "change-me",
+ "delete-me",
+ "keep-me",
+ ]);
+ Assert.equal(
+ cardMap.get("change-me").displayName,
+ "I'm going to be changed. Ïž"
+ );
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("delete-me.vcf");
+ CardDAVServer.putCardInternal(
+ "new.vcf",
+ "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n"
+ );
+
+ // Sync with the server.
+
+ info("Second sync with server.");
+
+ observer.init();
+ await directory.updateAllFromServerV1();
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["new"],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": ["delete-me"],
+ });
+
+ info("Cards:");
+ cardMap.clear();
+ for (let card of directory.childCards) {
+ info(card.displayName);
+ info(card.getProperty("_href", ""));
+ info(card.getProperty("_etag", ""));
+
+ cardMap.set(card.UID, card);
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]);
+
+ Assert.equal(
+ cardMap.get("keep-me").getProperty("_etag", ""),
+ oldETags.get("keep-me")
+ );
+
+ Assert.equal(cardMap.get("change-me").displayName, "I've been changed.");
+ Assert.notEqual(
+ cardMap.get("change-me").getProperty("_etag", ""),
+ oldETags.get("change-me")
+ );
+ oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", ""));
+
+ Assert.equal(cardMap.get("new").displayName, "I'm new!");
+ oldETags.set("new", cardMap.get("new").getProperty("_etag", ""));
+
+ oldETags.delete("delete-me");
+
+ // Double-check that what we have matches what's on the server.
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ new: {
+ etag: cardMap.get("new").getProperty("_etag", ""),
+ href: cardMap.get("new").getProperty("_href", ""),
+ vCard: cardMap.get("new").getProperty("_vCard", ""),
+ },
+ });
+
+ info("Third sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV1();
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ // Delete a card on the client.
+
+ info("Deleting a card on the client.");
+
+ try {
+ directory.deleteCards([cardMap.get("new")]);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["new"],
+ });
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Change a card on the client.
+
+ info("Changing a card on the client.");
+
+ try {
+ let changeMeCard = cardMap.get("change-me");
+ changeMeCard.displayName = "I've been changed again!";
+
+ directory.modifyCard(changeMeCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "change-me"
+ );
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": [],
+ });
+
+ changeMeCard = directory.childCards.find(c => c.UID == "change-me");
+ cardMap.set("change-me", changeMeCard);
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: changeMeCard.getProperty("_etag", ""),
+ href: changeMeCard.getProperty("_href", ""),
+ vCard: changeMeCard.getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Add a new card on the client.
+
+ info("Adding a new card on the client.");
+
+ try {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "I'm another new contact. Ï”";
+ newCard.UID = "another-new";
+ newCard = directory.addCard(newCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["another-new"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "another-new"
+ );
+
+ newCard = directory.childCards.find(c => c.UID == "another-new");
+ Assert.equal(
+ newCard.displayName,
+ "I'm another new contact. Ï”",
+ "non-ascii character survived the trip to the server"
+ );
+
+ await checkCardsOnServer({
+ "another-new": {
+ etag: newCard.getProperty("_etag", ""),
+ href: newCard.getProperty("_href", ""),
+ vCard: newCard.getProperty("_vCard", ""),
+ },
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ info("Fourth sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV1();
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+add_task(async function testNormal() {
+ await subtest();
+});
+
+add_task(async function testYahoo() {
+ CardDAVServer.mimicYahoo = true;
+ await subtest();
+ CardDAVServer.mimicYahoo = false;
+});
+
+add_task(async function testReadOnly() {
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js
new file mode 100644
index 0000000000..74f9c5ac88
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js
@@ -0,0 +1,408 @@
+/* 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/. */
+
+async function subtest() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "keep-me.vcf",
+ "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ // This one includes a character encoded with UTF-8.
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ // We'll only use this for the initial sync, so I think it's okay to use
+ // bulkAddCards and not get a notification for every contact.
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ let lastSyncToken = directory._syncToken;
+ info(`Token is: ${lastSyncToken}`);
+
+ info("Cards:");
+ let cardMap = new Map();
+ let oldETags = new Map();
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+
+ cardMap.set(card.UID, card);
+ oldETags.set(card.UID, card.getProperty("_etag", ""));
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), [
+ "change-me",
+ "delete-me",
+ "keep-me",
+ ]);
+ Assert.equal(
+ cardMap.get("change-me").displayName,
+ "I'm going to be changed. Ïž"
+ );
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("delete-me.vcf");
+ CardDAVServer.putCardInternal(
+ "new.vcf",
+ "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n"
+ );
+
+ // Sync with the server.
+
+ info("Second sync with server.");
+
+ observer.init();
+ await directory.updateAllFromServerV2();
+ Assert.notEqual(directory._syncToken, lastSyncToken);
+ lastSyncToken = directory._syncToken;
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["new"],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": ["delete-me"],
+ });
+
+ info("Cards:");
+ cardMap.clear();
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+
+ cardMap.set(card.UID, card);
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]);
+
+ Assert.equal(
+ cardMap.get("keep-me").getProperty("_etag", ""),
+ oldETags.get("keep-me")
+ );
+
+ Assert.equal(cardMap.get("change-me").displayName, "I've been changed.");
+ Assert.notEqual(
+ cardMap.get("change-me").getProperty("_etag", ""),
+ oldETags.get("change-me")
+ );
+ oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", ""));
+
+ Assert.equal(cardMap.get("new").displayName, "I'm new!");
+ oldETags.set("new", cardMap.get("new").getProperty("_etag", ""));
+
+ oldETags.delete("delete-me");
+
+ // Double-check that what we have matches what's on the server.
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ new: {
+ etag: cardMap.get("new").getProperty("_etag", ""),
+ href: cardMap.get("new").getProperty("_href", ""),
+ vCard: cardMap.get("new").getProperty("_vCard", ""),
+ },
+ });
+
+ info("Third sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV2();
+ // This time the token should NOT change, there's been no contact with the
+ // server since last time.
+ Assert.equal(directory._syncToken, lastSyncToken);
+ lastSyncToken = directory._syncToken;
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ // Delete a card on the client.
+
+ info("Deleting a card on the client.");
+
+ try {
+ directory.deleteCards([cardMap.get("new")]);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["new"],
+ });
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Change a card on the client.
+
+ info("Changing a card on the client.");
+
+ try {
+ let changeMeCard = cardMap.get("change-me");
+ changeMeCard.displayName = "I've been changed again!";
+ directory.modifyCard(changeMeCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "change-me"
+ );
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": [],
+ });
+
+ changeMeCard = directory.childCards.find(c => c.UID == "change-me");
+ cardMap.set("change-me", changeMeCard);
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: changeMeCard.getProperty("_etag", ""),
+ href: changeMeCard.getProperty("_href", ""),
+ vCard: changeMeCard.getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Add a new card on the client.
+
+ info("Adding a new card on the client.");
+
+ try {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "I'm another new contact. Ï”";
+ newCard.UID = "another-new";
+ newCard = directory.addCard(newCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["another-new"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "another-new"
+ );
+
+ newCard = directory.childCards.find(c => c.UID == "another-new");
+ Assert.equal(
+ newCard.displayName,
+ "I'm another new contact. Ï”",
+ "non-ascii character survived the trip to the server"
+ );
+
+ await checkCardsOnServer({
+ "another-new": {
+ etag: newCard.getProperty("_etag", ""),
+ href: newCard.getProperty("_href", ""),
+ vCard: newCard.getProperty("_vCard", ""),
+ },
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ info("Fourth sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV2();
+ if (directory.readOnly) {
+ Assert.equal(directory._syncToken, lastSyncToken);
+ } else {
+ Assert.notEqual(directory._syncToken, lastSyncToken);
+ }
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+add_task(async function testNormal() {
+ await subtest();
+});
+
+add_task(async function testGoogle() {
+ CardDAVServer.mimicGoogle = true;
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.carddav.vcard3", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.carddav.vcard3");
+ CardDAVServer.mimicGoogle = false;
+});
+
+add_task(async function testReadOnly() {
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly");
+});
+
+add_task(async function testExpiredToken() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "first.vcf",
+ "BEGIN:VCARD\r\nUID:first\r\nFN:First Person\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "second.vcf",
+ "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "third.vcf",
+ "BEGIN:VCARD\r\nUID:third\r\nFN:Third Person\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ info(`Token is: ${directory._syncToken}`);
+
+ info("Cards:");
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+ }
+
+ Assert.equal(directory.childCardCount, 3);
+ Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
+ "first",
+ "second",
+ "third",
+ ]);
+
+ // Corrupt the sync token. This will cause a 400 Bad Request response and a
+ // complete resync should happen.
+
+ directory._syncToken = "wrong token";
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "fourth.vcf",
+ "BEGIN:VCARD\r\nUID:fourth\r\nFN:Fourth\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "second.vcf",
+ "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person, but different\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("first.vcf");
+
+ // Sync with the server.
+
+ info("Sync with server.");
+
+ let notificationPromise = TestUtils.topicObserved(
+ "addrbook-directory-invalidated"
+ );
+ observer.init();
+ await directory.updateAllFromServerV2();
+ // Check what notifications were fired. There should be an "invalidated"
+ // notification, making the others redundant, but the "deleted"
+ // notification is hard to avoid.
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["first"],
+ });
+ await notificationPromise;
+
+ info(`Token is now: ${directory._syncToken}`);
+
+ info("Cards:");
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+ }
+
+ // Check that the changes were synced.
+
+ Assert.equal(directory.childCardCount, 3);
+ Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
+ "fourth",
+ "second",
+ "third",
+ ]);
+ Assert.equal(
+ directory.childCards.find(c => c.UID == "second").displayName,
+ "Second Person, but different"
+ );
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardForEmail.js b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js
new file mode 100644
index 0000000000..3e7a53f339
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests nsIAbDirectory::cardForEmailAddress
+ * - checks correct return when no email address supplied
+ * - checks correct return when no matching email address supplied
+ * - checks correct return when matching email address supplied.
+ *
+ * Uses: cardForEmail.mab
+ */
+
+function check_correct_card(card) {
+ Assert.ok(!!card);
+
+ Assert.equal(card.firstName, "FirstName1");
+ Assert.equal(card.lastName, "LastName1");
+ Assert.equal(card.displayName, "DisplayName1");
+ Assert.deepEqual(card.emailAddresses, [
+ "PrimaryEmail1@test.invalid",
+ "SecondEmail1\u00D0@test.invalid",
+ ]);
+}
+
+function run_test() {
+ loadABFile("data/cardForEmail", kPABData.fileName);
+
+ // Test - Get the directory
+ let AB = MailServices.ab.getDirectory(kPABData.URI);
+
+ // Test - Check that a null string succeeds and does not
+ // return a card (bug 404264)
+ Assert.ok(AB.cardForEmailAddress(null) == null);
+
+ // Test - Check that an empty string succeeds and does not
+ // return a card (bug 404264)
+ Assert.ok(AB.cardForEmailAddress("") == null);
+
+ // Test - Check that we don't match an email that doesn't exist
+ Assert.ok(AB.cardForEmailAddress("nocard@this.email.invalid") == null);
+
+ // Test - Check that we match this email and some of the fields
+ // of the card are correct.
+ var card = AB.cardForEmailAddress("PrimaryEmail1@test.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the primary email with insensitive case.
+ card = AB.cardForEmailAddress("pRimaryemAIL1@teST.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the second email.
+ card = AB.cardForEmailAddress("SecondEmail1\u00D0@test.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the second email with insensitive case.
+ card = AB.cardForEmailAddress("SECondEMail1\u00D0@TEST.inValid");
+
+ check_correct_card(card);
+
+ // Check that we match cards that have more than two email addresses.
+ card = AB.cardForEmailAddress("first@SOMETHING.invalid");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("second@something.INVALID");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("THIRD@something.invalid");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("FOURTH@SOMETHING.INVALID");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("A vCard!");
+ Assert.equal(card, null);
+
+ // Check getCardFromProperty returns null correctly for non-extant properties
+ Assert.equal(AB.getCardFromProperty("NickName", "", false), null);
+ Assert.equal(AB.getCardFromProperty("NickName", "NickName", false), null);
+
+ // Check case-insensitive searching works
+ card = AB.getCardFromProperty("NickName", "NickName1", true);
+ check_correct_card(card);
+ card = AB.getCardFromProperty("NickName", "NickName1", false);
+ check_correct_card(card);
+
+ Assert.equal(AB.getCardFromProperty("NickName", "nickName1", true), null);
+
+ card = AB.getCardFromProperty("NickName", "nickName1", false);
+ check_correct_card(card);
+
+ var cards = AB.getCardsFromProperty("LastName", "DOE", true);
+ Assert.equal(cards.length, 0);
+
+ cards = AB.getCardsFromProperty("LastName", "Doe", true);
+ var i = 0;
+ var data = ["John", "Jane"];
+
+ for (card of cards) {
+ i++;
+ Assert.equal(card.lastName, "Doe");
+ var index = data.indexOf(card.firstName);
+ Assert.notEqual(index, -1);
+ delete data[index];
+ }
+ Assert.equal(i, 2);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_collection.js b/comm/mailnews/addrbook/test/unit/test_collection.js
new file mode 100644
index 0000000000..720f28c246
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_collection.js
@@ -0,0 +1,404 @@
+/* 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/. */
+
+/*
+ * Test suite for the Address Collector Service.
+ *
+ * This tests the main collection functions for adding new cards and modifying
+ * existing ones.
+ *
+ * Tests against cards in different ABs are done in test_collection_2.js.
+ */
+
+// Source fields (emailHeader) and expected results for use for
+// testing the addition of new addresses to the database.
+//
+// Note: these email addresses should be different to allow collecting an
+// address to add a different card each time.
+var addEmailChecks =
+ // First 3 items aimed at basic collection and mail format.
+ [
+ {
+ emailHeader: "test0@foo.invalid",
+ primaryEmail: "test0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test1@foo.invalid",
+ primaryEmail: "test1@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test2@foo.invalid",
+ primaryEmail: "test2@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // UTF-8 based addresses (bug 407564)
+ {
+ emailHeader: "test0@\u00D0.invalid",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test0\u00D0@foo.invalid",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Screen names
+ {
+ emailHeader: "invalid\u00D00@aol.com",
+ primaryEmail: "invalid\u00D00@aol.com",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid\u00D00",
+ },
+ {
+ emailHeader: "invalid1\u00D00@cs.com",
+ primaryEmail: "invalid1\u00D00@cs.com",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid1\u00D00",
+ },
+ {
+ emailHeader: "invalid2\u00D00@netscape.net",
+ primaryEmail: "invalid2\u00D00@netscape.net",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid2\u00D00",
+ },
+ // Collection of names
+ {
+ emailHeader: "Test User <test3@foo.invalid>",
+ primaryEmail: "test3@foo.invalid",
+ displayName: "Test User",
+ firstName: "Test",
+ lastName: "User",
+ screenName: "",
+ },
+ {
+ emailHeader: "Test <test4@foo.invalid>",
+ primaryEmail: "test4@foo.invalid",
+ displayName: "Test",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Collection of names with UTF-8 specific items
+ {
+ emailHeader: "Test\u00D0 User <test5@foo.invalid>",
+ primaryEmail: "test5@foo.invalid",
+ displayName: "Test\u00D0 User",
+ firstName: "Test\u00D0",
+ lastName: "User",
+ screenName: "",
+ },
+ {
+ emailHeader: "Test\u00D0 <test6@foo.invalid>",
+ primaryEmail: "test6@foo.invalid",
+ displayName: "Test\u00D0",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ ];
+
+// Source fields (emailHeader) and expected results for use for
+// testing the modification of cards in the database.
+//
+// Note: these sets re-use some of the ones for ease of definition.
+var modifyEmailChecks =
+ // No display name/other details. Add details and modify mail format.
+ [
+ {
+ emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // No modification of existing cards with display names
+ {
+ emailHeader: "Modify2 User\u00D02 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify3 <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Check no modification of cards for mail format where format is not
+ // "unknown".
+ {
+ emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // No modification of cards with email in second email address.
+ {
+ emailHeader: "Modify Secondary <usersec\u00D0@foo.invalid>",
+ primaryEmail: "userprim\u00D0@foo.invalid",
+ secondEmail: "usersec\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <usersec\u00D0@foo.invalid>",
+ primaryEmail: "userprim\u00D0@foo.invalid",
+ secondEmail: "usersec\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ ];
+
+var collectChecker = {
+ addressCollect: null,
+ AB: null,
+ part: 0,
+
+ checkAddress(aDetails) {
+ info("checkAddress: " + aDetails.emailHeader);
+ try {
+ this.addressCollect.collectAddress(aDetails.emailHeader, true);
+
+ this.checkCardResult(aDetails, false);
+ } catch (e) {
+ throw new Error(
+ "FAILED in checkAddress emailHeader: " +
+ aDetails.emailHeader +
+ " part: " +
+ this.part +
+ " : " +
+ e
+ );
+ }
+ ++this.part;
+ },
+
+ checkAll(aDetailsArray) {
+ try {
+ // Formulate the string to add.
+ var emailHeader = "";
+ var i;
+
+ for (i = 0; i < aDetailsArray.length - 1; ++i) {
+ emailHeader += aDetailsArray[i].emailHeader + ", ";
+ }
+
+ emailHeader += aDetailsArray[aDetailsArray.length - 1].emailHeader;
+
+ // Now add it. In this case we just set the Mail format Type to unknown.
+ this.addressCollect.collectAddress(emailHeader, true);
+
+ for (i = 0; i < aDetailsArray.length; ++i) {
+ this.checkCardResult(aDetailsArray[i], true);
+ }
+ } catch (e) {
+ throw new Error("FAILED in checkAll item: " + i + " : " + e);
+ }
+ },
+
+ checkCardResult(aDetails) {
+ info("checkCardResult: " + aDetails.emailHeader);
+ try {
+ var card = this.AB.cardForEmailAddress(aDetails.primaryEmail);
+
+ Assert.ok(card != null);
+
+ if ("secondEmail" in aDetails) {
+ Assert.equal(card.emailAddresses[1], aDetails.secondEmail);
+ }
+
+ Assert.equal(card.displayName, aDetails.displayName);
+ Assert.equal(card.firstName, aDetails.firstName);
+ Assert.equal(card.lastName, aDetails.lastName);
+ Assert.equal(card.getProperty("_AimScreenName", ""), aDetails.screenName);
+ } catch (e) {
+ throw new Error(
+ "FAILED in checkCardResult emailHeader: " +
+ aDetails.emailHeader +
+ " : " +
+ e
+ );
+ }
+ },
+};
+
+function run_test() {
+ // Test - Get the address collecter
+
+ // XXX Getting all directories ensures we create all ABs because the
+ // address collecter can't currently create ABs itself (bug 314448).
+ MailServices.ab.directories;
+
+ // Get the actual AB for the collector so we can check cards have been
+ // added.
+ collectChecker.AB = MailServices.ab.getDirectory(
+ Services.prefs.getCharPref("mail.collect_addressbook")
+ );
+
+ // Get the actual collecter
+ collectChecker.addressCollect = Cc[
+ "@mozilla.org/addressbook/services/addressCollector;1"
+ ].getService(Ci.nsIAbAddressCollector);
+
+ // Test - Addition of header without email address.
+
+ collectChecker.addressCollect.collectAddress("MyTest <>", true);
+
+ // Address book should have no cards present.
+ Assert.equal(collectChecker.AB.childCards.length, 0);
+
+ // Test - Email doesn't exist, but don't add it.
+
+ // As we've just set everything up, we know we haven't got anything in the
+ // AB, so just try and collect without adding.
+ collectChecker.addressCollect.collectAddress(
+ addEmailChecks[0].emailHeader,
+ false
+ );
+
+ var card = collectChecker.AB.cardForEmailAddress(
+ addEmailChecks[0].emailHeader
+ );
+
+ Assert.ok(card == null);
+
+ // Test - Try and collect various emails and formats.
+
+ collectChecker.part = 0;
+
+ addEmailChecks.forEach(collectChecker.checkAddress, collectChecker);
+
+ // Test - Do all emails at the same time.
+
+ // First delete all existing cards
+ collectChecker.AB.deleteCards(collectChecker.AB.childCards);
+
+ // Address book should have no cards present.
+ Assert.equal(collectChecker.AB.childCards.length, 0);
+
+ Assert.equal(
+ collectChecker.AB.cardForEmailAddress(addEmailChecks[0].emailHeader),
+ null
+ );
+
+ // Now do all emails at the same time.
+ collectChecker.checkAll(addEmailChecks);
+
+ // Test - Try and modify various emails and formats.
+
+ // Add a basic card with just primary and second email to allow testing
+ // of the case where we don't modify when second email is matching.
+ card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = "userprim\u00D0@foo.invalid";
+ card.setProperty("SecondEmail", "usersec\u00D0@foo.invalid");
+
+ collectChecker.AB.addCard(card);
+
+ collectChecker.part = 0;
+
+ modifyEmailChecks.forEach(collectChecker.checkAddress, collectChecker);
+
+ // Test collectSingleAddress - Note: because the above tests test
+ // collectAddress which we know calls collectSingleAddress, we only need to
+ // test the case where aSkipCheckExisting is true.
+
+ // Add an email that is already there and check we get two instances of it in
+ // the AB.
+
+ const kSingleAddress =
+ modifyEmailChecks[modifyEmailChecks.length - 1].primaryEmail;
+ const kSingleDisplayName = "Test Single";
+
+ collectChecker.addressCollect.collectSingleAddress(
+ kSingleAddress,
+ kSingleDisplayName,
+ true,
+ true
+ );
+
+ // Try collecting the same address in another case. This shouldn't create any
+ // new card.
+ collectChecker.addressCollect.collectSingleAddress(
+ kSingleAddress.toUpperCase(),
+ kSingleDisplayName,
+ true,
+ true
+ );
+
+ var foundCards = [];
+
+ for (card of collectChecker.AB.childCards) {
+ if (card.primaryEmail == kSingleAddress) {
+ foundCards.push(card);
+ }
+ }
+
+ Assert.equal(foundCards.length, 2);
+
+ if (
+ foundCards[0].displayName != kSingleDisplayName &&
+ foundCards[1].displayName != kSingleDisplayName
+ ) {
+ do_throw("Error, collectSingleCard didn't create a new card");
+ }
+
+ if (foundCards[0].displayName != "" && foundCards[1].displayName != "") {
+ do_throw(
+ "Error, collectSingleCard created ok, but other card does not exist"
+ );
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_collection_2.js b/comm/mailnews/addrbook/test/unit/test_collection_2.js
new file mode 100644
index 0000000000..bff3d6e916
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_collection_2.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/*
+ * Test suite for the Address Collector Service part 2.
+ *
+ * This test checks that we don't collect addresses when they already exist
+ * in other address books.
+ */
+
+function run_test() {
+ // Test - Get the address collector
+ loadABFile("data/collect", kPABData.fileName);
+
+ // Get the actual collector
+ var addressCollect = Cc[
+ "@mozilla.org/addressbook/services/addressCollector;1"
+ ].getService(Ci.nsIAbAddressCollector);
+
+ // Set the new pref afterwards to ensure we change correctly
+ Services.prefs.setCharPref("mail.collect_addressbook", kCABData.URI);
+
+ // XXX Getting all directories ensures we create all ABs because the
+ // address collector can't currently create ABs itself (bug 314448).
+ MailServices.ab.directories;
+
+ addressCollect.collectAddress("Other Book <other@book.invalid>", true);
+
+ let PAB = MailServices.ab.getDirectory(kPABData.URI);
+
+ var cards = PAB.childCards;
+
+ Assert.equal(cards.length, 1);
+
+ Assert.equal(cards[0].displayName, "Other Book");
+ Assert.equal(cards[0].primaryEmail, "other@book.invalid");
+
+ // Check the CAB has no cards.
+ let CAB = MailServices.ab.getDirectory(kCABData.URI);
+ Assert.equal(CAB.childCards.length, 0);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_convertOnSave.js b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js
new file mode 100644
index 0000000000..da26ffe56c
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js
@@ -0,0 +1,329 @@
+/* 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/. */
+
+// Tests that any card added to an AddrBookDirectory is stored as a vCard.
+// Some properties are also recorded outside the vCard for performance reasons
+// and/or searching.
+
+// Each type of card is saved and checked twice: once with its own UID and
+// again with a new UID. This ensures that UIDs are appropriately stored.
+
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { SQLiteDirectory } = ChromeUtils.import(
+ "resource:///modules/SQLiteDirectory.jsm"
+);
+var { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+Services.prefs.setStringPref(
+ "ldap_2.servers.conversion.filename",
+ "conversion.sqlite"
+);
+
+var book = new SQLiteDirectory();
+book.init("jsaddrbook://conversion.sqlite");
+
+/** Tests an instance of nsAbCardProperty. */
+add_task(function testCardProperty() {
+ let cardProperty = Cc[
+ "@mozilla.org/addressbook/cardproperty;1"
+ ].createInstance(Ci.nsIAbCard);
+ cardProperty.UID = "99999999-8888-7777-6666-555555555555";
+ cardProperty.displayName = "display name";
+ cardProperty.firstName = "first";
+ cardProperty.lastName = "last";
+ cardProperty.primaryEmail = "primary@email";
+ cardProperty.setProperty("SecondEmail", "second@email");
+ cardProperty.setProperty("NickName", "nick");
+ cardProperty.setProperty("FaxNumber", "1234567");
+ cardProperty.setProperty("BirthYear", 2001);
+ cardProperty.setProperty("BirthMonth", 1);
+ cardProperty.setProperty("BirthDay", 1);
+ cardProperty.setProperty("FakeProperty", "fake value");
+
+ saveCardAndTest(cardProperty, false);
+ saveCardAndTest(cardProperty, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *doesn't* contain a vCard, and
+ * is therefore the same as a card that hasn't yet been migrated to vCard.
+ */
+add_task(function testABCard() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FaxNumber", "1234567"],
+ ["BirthYear", 2001],
+ ["BirthMonth", 1],
+ ["BirthDay", 1],
+ ["FakeProperty", "fake value"],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard.
+ */
+add_task(function testABCardWithVCard() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FakeProperty", "fake value"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard.
+ */
+add_task(function testABCardWithVCardOnly() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["FakeProperty", "fake value"], // NO
+ ["PopularityIndex", 0], // NO
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard, but also some
+ * properties that shouldn't exist because their value is stored in the vCard.
+ */
+add_task(function testABCardWithVCardAndExtraProps() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FaxNumber", "1234567"],
+ ["BirthYear", 2001],
+ ["BirthMonth", 1],
+ ["BirthDay", 1],
+ ["FakeProperty", "fake value"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/** Tests an instance of AddrBookCard, created from scratch. */
+add_task(function testABCardConstructed() {
+ let abCard = new AddrBookCard();
+ abCard.UID = "99999999-8888-7777-6666-555555555555";
+ abCard.displayName = "display name";
+ abCard.firstName = "first";
+ abCard.lastName = "last";
+ abCard.primaryEmail = "primary@email";
+ abCard.vCardProperties.addValue("email", "second@email");
+ abCard.vCardProperties.addValue("nickname", "nick");
+ abCard.vCardProperties.addEntry(
+ new VCardPropertyEntry("tel", { type: "fax" }, "text", "1234567")
+ );
+ abCard.vCardProperties.addEntry(
+ new VCardPropertyEntry("bday", {}, "date", "20010101")
+ );
+ abCard.setProperty("FakeProperty", "fake value");
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/** Tests an instance of AddrBookCard, created from scratch. */
+add_task(function testABCardConstructionThrows() {
+ let abCard = new AddrBookCard();
+ abCard.UID = "99999999-8888-7777-6666-555555555555";
+ abCard.displayName = "display name";
+ abCard.firstName = "first";
+ abCard.lastName = "last";
+ abCard.primaryEmail = "primary@email";
+ // these properties will be forgotten
+ Assert.throws(
+ () => abCard.setProperty("SecondEmail", "second@email"),
+ /Unable to set SecondEmail as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("NickName", "nick"),
+ /Unable to set NickName as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("FaxNumber", "1234567"),
+ /Unable to set FaxNumber as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthYear", 2001),
+ /Unable to set BirthYear as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthMonth", 1),
+ /Unable to set BirthMonth as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthDay", 1),
+ /Unable to set BirthDay as a property/
+ );
+ abCard.setProperty("FakeProperty", "fake value");
+});
+
+function saveCardAndTest(card, useNewUID) {
+ info(`Saving the card ${useNewUID ? "with" : "without"} a new UID`);
+
+ Assert.equal(book.childCardCount, 0);
+
+ let savedCard = book.dropCard(card, useNewUID);
+ Assert.deepEqual(Array.from(savedCard.properties, p => p.name).sort(), [
+ "DisplayName",
+ "FakeProperty",
+ "FirstName",
+ "LastModifiedDate",
+ "LastName",
+ "NickName",
+ "PopularityIndex",
+ "PrimaryEmail",
+ "SecondEmail",
+ "_vCard",
+ ]);
+
+ if (useNewUID) {
+ Assert.notEqual(savedCard.UID, "99999999-8888-7777-6666-555555555555");
+ } else {
+ Assert.equal(savedCard.UID, "99999999-8888-7777-6666-555555555555");
+ }
+
+ Assert.equal(savedCard.getProperty("DisplayName", "WRONG"), "display name");
+ Assert.equal(savedCard.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(savedCard.getProperty("LastName", "WRONG"), "last");
+ Assert.equal(savedCard.getProperty("PrimaryEmail", "WRONG"), "primary@email");
+ Assert.equal(savedCard.getProperty("SecondEmail", "WRONG"), "second@email");
+ Assert.equal(savedCard.getProperty("NickName", "WRONG"), "nick");
+ Assert.equal(savedCard.getProperty("FakeProperty", "WRONG"), "fake value");
+ Assert.equal(savedCard.getProperty("PopularityIndex", "WRONG"), "0");
+
+ let vCard = savedCard.getProperty("_vCard", "WRONG");
+ Assert.stringContains(vCard, "\r\nEMAIL;PREF=1:primary@email\r\n");
+ Assert.stringContains(vCard, "\r\nEMAIL:second@email\r\n");
+ Assert.stringContains(vCard, "\r\nFN:display name\r\n");
+ Assert.stringContains(vCard, "\r\nNICKNAME:nick\r\n");
+ Assert.stringContains(vCard, "\r\nBDAY;VALUE=DATE:20010101\r\n");
+ Assert.stringContains(vCard, "\r\nN:last;first;;;\r\n");
+ Assert.stringContains(vCard, "\r\nTEL;TYPE=fax;VALUE=TEXT:1234567\r\n");
+ Assert.stringContains(vCard, `\r\nUID:${savedCard.UID}\r\n`);
+
+ let modifiedDate = parseInt(
+ savedCard.getProperty("LastModifiedDate", ""),
+ 10
+ );
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+
+ Assert.equal(savedCard.displayName, "display name");
+ Assert.equal(savedCard.firstName, "first");
+ Assert.equal(savedCard.lastName, "last");
+ Assert.equal(savedCard.primaryEmail, "primary@email");
+ Assert.deepEqual(savedCard.emailAddresses, ["primary@email", "second@email"]);
+
+ Assert.ok(savedCard.supportsVCard);
+ Assert.ok(savedCard.vCardProperties);
+
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("fn"), [
+ "display name",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("email"), [
+ "primary@email",
+ "second@email",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("nickname"), [
+ "nick",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("bday"), [
+ "2001-01-01",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("n"), [
+ ["last", "first", "", "", ""],
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("tel"), ["1234567"]);
+
+ book.deleteCards(book.childCards);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_db_enumerator.js b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js
new file mode 100644
index 0000000000..50fe8b7d06
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js
@@ -0,0 +1,89 @@
+/**
+ * This test verifies that we don't crash if we have an enumerator on an
+ * addr database and delete the underlying directory, which forces the ab
+ * closed.
+ */
+var ab_prefix = "test-537815-";
+var card_properties = {
+ FirstName: "01-first-3",
+ LastName: "02-last",
+ PrimaryEmail: "08-email-1@zindus.invalid",
+};
+var max_addressbooks = 10;
+
+function bug_537815_fixture_setup() {
+ let i, key;
+
+ for (i = 1; i <= max_addressbooks; i++) {
+ let ab_name = ab_prefix + i;
+ MailServices.ab.newAddressBook(
+ ab_name,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ dump("created: " + ab_name + "\n");
+
+ for (var j = 1; j < 2; j++) {
+ for (let elem of MailServices.ab.directories) {
+ let uri = elem.URI;
+ let dir = MailServices.ab.getDirectory(uri);
+
+ dump("considering: j: " + j + " " + elem.dirName + "\n");
+
+ if (j == 1 && elem.dirName.startsWith(ab_prefix)) {
+ for (i = 1; i <= 1000; i++) {
+ let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsIAbCard);
+
+ for (key in card_properties) {
+ abCard.setProperty(key, card_properties[key]);
+ }
+
+ abCard = dir.addCard(abCard);
+ }
+ dump("populated: " + elem.dirName + "\n");
+ }
+ }
+ }
+ }
+}
+
+function bug_537815_test() {
+ for (let elem of MailServices.ab.directories) {
+ let uri = elem.URI;
+ let dir = MailServices.ab.getDirectory(uri);
+ if (elem.dirName.startsWith(ab_prefix)) {
+ for (let abCard of dir.childCards) {
+ for (let key in card_properties) {
+ abCard.getProperty(key, null);
+ }
+ }
+ dump("visited all cards in: " + elem.dirName + "\n");
+ }
+ }
+}
+
+function test_bug_537815() {
+ bug_537815_fixture_setup();
+ bug_537815_test();
+ bug_537815_fixture_tear_down();
+}
+
+function bug_537815_fixture_tear_down() {
+ let a_uri = {};
+ for (let elem of MailServices.ab.directories) {
+ if (elem.dirName.startsWith(ab_prefix)) {
+ a_uri[elem.URI] = true;
+ dump("to be deleted: " + elem.dirName + "\n");
+ }
+ }
+
+ for (let uri in a_uri) {
+ MailServices.ab.deleteAddressBook(uri);
+ }
+}
+
+function run_test() {
+ test_bug_537815();
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_delete_book.js b/comm/mailnews/addrbook/test/unit/test_delete_book.js
new file mode 100644
index 0000000000..8c63bb43b0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_delete_book.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function getExistingDirectories() {
+ return MailServices.ab.directories.map(d => d.dirPrefId);
+}
+
+add_task(async function clearPref() {
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "delete me",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.deleteme",
+ "ldap_2.servers.history",
+ ]);
+ Services.prefs.setStringPref("mail.collect_addressbook", book.URI);
+
+ await promiseDirectoryRemoved(book.URI);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+});
+
+add_task(async function protectBuiltIns() {
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("this is completely wrong");
+ }, /NS_ERROR_MALFORMED_URI/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://bad.sqlite");
+ }, /NS_ERROR_UNEXPECTED/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://history.sqlite");
+ }, /NS_ERROR_FAILURE/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://abook.sqlite");
+ }, /NS_ERROR_FAILURE/);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_export.js b/comm/mailnews/addrbook/test/unit/test_export.js
new file mode 100644
index 0000000000..34874e2f69
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_export.js
@@ -0,0 +1,156 @@
+/* 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/. */
+
+"use strict";
+
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { AddrBookUtils } = ChromeUtils.import(
+ "resource:///modules/AddrBookUtils.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+async function subtest(cardConstructor) {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ let contact1 = cardConstructor();
+ contact1.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact1.displayName = "contact number one";
+ contact1.firstName = "contact";
+ contact1.lastName = "one";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = book.addCard(contact1);
+
+ let contact2 = cardConstructor();
+ contact2.UID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+ contact2.displayName = "contact number two";
+ contact2.firstName = "contact";
+ contact2.lastName = "two";
+ contact2.primaryEmail = "contact2@invalid";
+ if (contact2.supportsVCard) {
+ contact2.vCardProperties.addValue("title", `"worker"`);
+ contact2.vCardProperties.addValue("note", "here's some unicode text…");
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom1", {}, "text", "custom, 1")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom2", {}, "text", "custom\t2")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom3", {}, "text", "custom\r3")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom4", {}, "text", "custom\n4")
+ );
+ } else {
+ contact2.setProperty("JobTitle", `"worker"`);
+ contact2.setProperty("Notes", "here's some unicode text…");
+ contact2.setProperty("Custom1", "custom, 1");
+ contact2.setProperty("Custom2", "custom\t2");
+ contact2.setProperty("Custom3", "custom\r3");
+ contact2.setProperty("Custom4", "custom\n4");
+ }
+ contact2 = book.addCard(contact2);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ list.addCard(contact1);
+
+ await compareAgainstFile(
+ "export.csv",
+ AddrBookUtils.exportDirectoryToDelimitedText(book, ",")
+ );
+ await compareAgainstFile(
+ "export.txt",
+ AddrBookUtils.exportDirectoryToDelimitedText(book, "\t")
+ );
+ await compareAgainstFile(
+ "export.vcf",
+ AddrBookUtils.exportDirectoryToVCard(book)
+ );
+ // modifytimestamp is always changing, replace it with a fixed value.
+ await compareAgainstFile(
+ "export.ldif",
+ AddrBookUtils.exportDirectoryToLDIF(book).replace(
+ /modifytimestamp: \d+/g,
+ "modifytimestamp: 12345"
+ )
+ );
+}
+
+async function compareAgainstFile(fileName, actual) {
+ info(`checking against ${fileName}`);
+
+ // The test files are UTF-8 encoded and have Windows line endings. The
+ // exportDirectoryTo* functions are platform-dependent, except for VCard
+ // which always uses Windows line endings.
+
+ let file = do_get_file(`data/${fileName}`);
+ let expected = await IOUtils.readUTF8(file.path);
+
+ if (AppConstants.platform != "win" && fileName != "export.vcf") {
+ expected = expected.replace(/\r\n/g, "\n");
+ }
+
+ // From here on, \r is just another character. It will be the last character
+ // on lines where Windows line endings exist.
+ let expectedLines = expected.split("\n");
+ let actualLines = actual.split("\n");
+ info(actual);
+ Assert.deepEqual(actualLines.sort(), expectedLines.sort());
+ // equal(actualLines.length, expectedLines.length, "correct number of lines");
+
+ // for (let l = 0; l < expectedLines.length; l++) {
+ // let expectedLine = expectedLines[l];
+ // let actualLine = actualLines[l];
+ // if (actualLine == expectedLine) {
+ // ok(true, `line ${l + 1} matches`);
+ // } else {
+ // for (let c = 0; c < expectedLine.length && c < actualLine.length; c++) {
+ // if (actualLine[c] != expectedLine[c]) {
+ // // This call to equal automatically prints some extra characters of
+ // // context. Hopefully that helps with debugging.
+ // equal(
+ // actualLine.substring(c - 10, c + 10),
+ // expectedLine.substring(c - 10, c + 10),
+ // `line ${l + 1} does not match at character ${c + 1}`
+ // );
+ // }
+ // }
+ // equal(
+ // expectedLine.length,
+ // actualLine.length,
+ // `line ${l + 1} lengths differ`
+ // );
+ // }
+ // }
+}
+
+add_task(async function addrBookCard() {
+ return subtest(() => new AddrBookCard());
+});
+
+add_task(async function cardProperty() {
+ return subtest(() =>
+ Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard)
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js
new file mode 100644
index 0000000000..957285bbba
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js
@@ -0,0 +1,420 @@
+/* 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/. */
+
+"use strict";
+
+var FILE_NAME = "abook-1.sqlite";
+var SCHEME = "jsaddrbook";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var book, contact, list, listCard;
+var observer = {
+ topics: [
+ "addrbook-directory-created",
+ "addrbook-directory-updated",
+ "addrbook-directory-deleted",
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-properties-updated",
+ "addrbook-contact-deleted",
+ "addrbook-list-created",
+ "addrbook-list-updated",
+ "addrbook-list-deleted",
+ "addrbook-list-member-added",
+ "addrbook-list-member-removed",
+ ],
+ setUp() {
+ for (let topic of this.topics) {
+ Services.obs.addObserver(observer, topic);
+ }
+ },
+ cleanUp() {
+ for (let topic of this.topics) {
+ Services.obs.removeObserver(observer, topic);
+ }
+ },
+
+ events: [],
+ observe(subject, topic, data) {
+ this.events.push([topic, subject, data]);
+ },
+ checkEvents(...events) {
+ info(
+ "Actual events: " +
+ JSON.stringify(
+ observer.events.map(e =>
+ e.map(a => {
+ if (a instanceof Ci.nsIAbDirectory) {
+ return `[nsIAbDirectory]`;
+ }
+ if (a instanceof Ci.nsIAbCard) {
+ return `[nsIAbCard]`;
+ }
+ return a;
+ })
+ )
+ )
+ );
+ equal(observer.events.length, events.length);
+
+ let actualEvents = observer.events.slice();
+ observer.events.length = 0;
+
+ for (let j = 0; j < events.length; j++) {
+ let expectedEvent = events[j];
+ let actualEvent = actualEvents[j];
+
+ for (let i = 0; i < expectedEvent.length; i++) {
+ try {
+ expectedEvent[i].QueryInterface(Ci.nsIAbCard);
+ ok(actualEvent[i].equals(expectedEvent[i]));
+ } catch (ex) {
+ if (expectedEvent[i] instanceof Ci.nsIAbDirectory) {
+ equal(actualEvent[i].UID, expectedEvent[i].UID);
+ } else if (expectedEvent[i] === null) {
+ ok(!actualEvent[i]);
+ } else if (expectedEvent[i] !== undefined) {
+ equal(actualEvent[i], expectedEvent[i]);
+ }
+ }
+ }
+ }
+
+ return actualEvents;
+ },
+};
+
+var baseAddressBookCount;
+
+add_setup(function () {
+ let profileDir = do_get_profile();
+ observer.setUp();
+
+ let dirs = MailServices.ab.directories;
+ // On Mac we might be loading the OS X Address Book. If we are, then we
+ // need to take acccount of that here, so that the test still pass on
+ // development machines.
+ if (
+ AppConstants.platform == "macosx" &&
+ dirs[0].URI == "moz-abosxdirectory:///"
+ ) {
+ equal(dirs.length, 3);
+ equal(dirs[1].fileName, kPABData.fileName);
+ equal(dirs[2].fileName, kCABData.fileName);
+ } else {
+ equal(dirs.length, 2);
+ equal(dirs[0].fileName, kPABData.fileName);
+ equal(dirs[1].fileName, kCABData.fileName);
+ }
+ // Also record the address book counts so that we get the expected counts
+ // correct further down in the test.
+ baseAddressBookCount = dirs.length;
+
+ // Check the PAB file was created.
+ let pabFile = profileDir.clone();
+ pabFile.append(kPABData.fileName);
+ ok(pabFile.exists());
+
+ // Check the CAB file was created.
+ let cabFile = profileDir.clone();
+ cabFile.append(kCABData.fileName);
+ ok(cabFile.exists());
+});
+
+add_task(async function createAddressBook() {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ observer.checkEvents(["addrbook-directory-created", book]);
+
+ // Check nsIAbDirectory properties.
+ ok(!book.readOnly);
+ ok(!book.isRemote);
+ ok(!book.isSecure);
+ equal(book.dirName, "new book");
+ equal(book.dirType, Ci.nsIAbManager.JS_DIRECTORY_TYPE);
+ equal(book.fileName, FILE_NAME);
+ equal(book.UID.length, 36);
+ equal(book.URI, `${SCHEME}://${FILE_NAME}`);
+ equal(book.isMailList, false);
+ equal(book.supportsMailingLists, true);
+ equal(book.dirPrefId, "ldap_2.servers.newbook");
+
+ // Check enumerations.
+ equal(Array.from(book.childNodes).length, 0);
+ equal(Array.from(book.childCards).length, 0);
+
+ // Check prefs.
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.description"),
+ "new book"
+ );
+ equal(
+ Services.prefs.getIntPref("ldap_2.servers.newbook.dirType"),
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.filename"),
+ FILE_NAME
+ );
+ equal(Services.prefs.getStringPref("ldap_2.servers.newbook.uid"), book.UID);
+ equal(MailServices.ab.directories.length, baseAddressBookCount + 1);
+
+ // Check the file was created.
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append(FILE_NAME);
+ ok(dbFile.exists());
+});
+
+add_task(async function editAddressBook() {
+ book.dirName = "updated book";
+ observer.checkEvents(["addrbook-directory-updated", book, "DirName"]);
+ equal(book.dirName, "updated book");
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.description"),
+ "updated book"
+ );
+});
+
+add_task(async function createContact() {
+ contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.displayName = "a new contact";
+ contact.firstName = "new";
+ contact.lastName = "contact";
+ contact.primaryEmail = "test@invalid";
+ contact.setProperty("Foo", "This will be deleted later.");
+ contact = book.addCard(contact);
+ observer.checkEvents(["addrbook-contact-created", contact, book.UID]);
+
+ let cards = book.childCards;
+ equal(cards.length, 1);
+ ok(cards[0].equals(contact));
+
+ // Check nsIAbCard properties.
+ equal(contact.directoryUID, book.UID);
+ equal(contact.UID.length, 36);
+ equal(contact.firstName, "new");
+ equal(contact.lastName, "contact");
+ equal(contact.displayName, "a new contact");
+ equal(contact.primaryEmail, "test@invalid");
+ equal(contact.getProperty("Foo", ""), "This will be deleted later.");
+ equal(contact.isMailList, false);
+ let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10);
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+
+ // Check nsIAbCard methods.
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_DISPLAY_NAME),
+ "a new contact"
+ );
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER),
+ "contact, new"
+ );
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_FIRST_LAST_ORDER),
+ "new contact"
+ );
+});
+
+add_task(async function editContact() {
+ contact.firstName = "updated";
+ contact.lastName = "contact";
+ contact.displayName = "updated contact";
+ contact.setProperty("Foo", null);
+ contact.setProperty("Bar1", "a new property");
+ contact.setProperty("Bar2", "");
+ contact.setProperty("LastModifiedDate", 0);
+ book.modifyCard(contact);
+ let [, propertyEvent] = observer.checkEvents(
+ ["addrbook-contact-updated", contact, book.UID],
+ ["addrbook-contact-properties-updated", contact]
+ );
+ Assert.deepEqual(JSON.parse(propertyEvent[2]), {
+ DisplayName: {
+ oldValue: "a new contact",
+ newValue: "updated contact",
+ },
+ Foo: {
+ oldValue: "This will be deleted later.",
+ newValue: null,
+ },
+ Bar1: {
+ oldValue: null,
+ newValue: "a new property",
+ },
+ FirstName: {
+ oldValue: "new",
+ newValue: "updated",
+ },
+ _vCard: {
+ oldValue: formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ FN:a new contact
+ EMAIL;PREF=1:test@invalid
+ N:contact;new;;;
+ UID:${contact.UID}
+ END:VCARD`,
+ newValue: formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ FN:updated contact
+ EMAIL;PREF=1:test@invalid
+ N:contact;updated;;;
+ UID:${contact.UID}
+ END:VCARD`,
+ },
+ });
+ contact = book.childCards[0];
+ equal(contact.firstName, "updated");
+ equal(contact.lastName, "contact");
+ equal(contact.displayName, "updated contact");
+ equal(contact.getProperty("Foo", "empty"), "empty");
+ equal(contact.getProperty("Bar1", ""), "a new property");
+ equal(contact.getProperty("Bar2", "no value"), "no value");
+ let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10);
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+});
+
+add_task(async function createMailingList() {
+ list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ // Skip checking events temporarily, until listCard is defined.
+
+ // Check enumerations.
+ let childNodes = book.childNodes;
+ equal(childNodes.length, 1);
+ equal(childNodes[0].UID, list.UID); // TODO Object equality doesn't work because of XPCOM.
+ let childCards = book.childCards;
+ equal(childCards.length, 2);
+ if (childCards[0].isMailList) {
+ listCard = childCards[0];
+ ok(childCards[1].equals(contact));
+ } else {
+ ok(childCards[0].equals(contact));
+ listCard = childCards[1];
+ }
+ equal(listCard.UID, list.UID);
+
+ observer.checkEvents(["addrbook-list-created", list, book.UID]);
+
+ // Check nsIAbDirectory properties.
+ equal(list.dirName, "new list");
+ equal(list.UID.length, 36);
+ equal(list.URI, `${SCHEME}://${FILE_NAME}/${list.UID}`);
+ equal(list.isMailList, true);
+ equal(list.supportsMailingLists, false);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ equal(Array.from(list.childCards).length, 0);
+
+ // Check nsIAbCard properties.
+ equal(listCard.firstName, "");
+ equal(listCard.lastName, "new list");
+ equal(listCard.primaryEmail, "");
+ equal(listCard.displayName, "new list");
+});
+
+add_task(async function editMailingList() {
+ list.dirName = "updated list";
+ list.editMailListToDatabase(null);
+ observer.checkEvents(["addrbook-list-updated", list, book.UID]);
+ equal("updated list", list.dirName);
+});
+
+add_task(async function addMailingListMember() {
+ list.addCard(contact);
+ observer.checkEvents(["addrbook-list-member-added", contact, list.UID]);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ let childCards = list.childCards;
+ equal(childCards.length, 1);
+ ok(childCards[0].equals(contact));
+});
+
+add_task(async function removeMailingListMember() {
+ list.deleteCards([contact]);
+ observer.checkEvents(["addrbook-list-member-removed", contact, list.UID]);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ equal(Array.from(list.childCards).length, 0);
+});
+
+add_task(async function deleteMailingList() {
+ book.deleteDirectory(list);
+ observer.checkEvents(["addrbook-list-deleted", list, book.UID]);
+});
+
+add_task(async function deleteContact() {
+ book.deleteCards([contact]);
+ observer.checkEvents(["addrbook-contact-deleted", contact, book.UID]);
+
+ // Check enumerations.
+ equal(Array.from(book.childNodes).length, 0);
+ equal(Array.from(book.childCards).length, 0);
+});
+
+// Tests that the UID on a new contact can be set.
+add_task(async function createContactWithUID() {
+ let contactWithUID = Cc[
+ "@mozilla.org/addressbook/cardproperty;1"
+ ].createInstance(Ci.nsIAbCard);
+ contactWithUID.UID = "I'm a UID!";
+ contactWithUID = book.addCard(contactWithUID);
+ equal("I'm a UID!", contactWithUID.UID, "New contact has the UID we set");
+
+ Assert.throws(() => {
+ // Set the UID after it already exists.
+ contactWithUID.UID = "This should not be possible";
+ }, /NS_ERROR_UNEXPECTED/);
+
+ // Setting the UID to it's existing value should not fail.
+ contactWithUID.UID = contactWithUID.UID; // eslint-disable-line no-self-assign
+
+ book.deleteCards([contactWithUID]);
+ observer.events.length = 0;
+});
+
+add_task(async function deleteAddressBook() {
+ await promiseDirectoryRemoved(book.URI);
+
+ observer.checkEvents(["addrbook-directory-deleted", book, null]);
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.dirType"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.description"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.filename"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.uid"));
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append(FILE_NAME);
+ ok(!dbFile.exists());
+ equal(MailServices.ab.directories.length, baseAddressBookCount);
+ Assert.throws(() => {
+ MailServices.ab.getDirectory(`${SCHEME}://${FILE_NAME}`);
+ }, /NS_ERROR_FAILURE/);
+});
+
+add_task(async function cleanUp() {
+ observer.checkEvents();
+ observer.cleanUp();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_ldap1.js b/comm/mailnews/addrbook/test/unit/test_ldap1.js
new file mode 100644
index 0000000000..e323d71386
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldap1.js
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic LDAP address book functions
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ let abCountBeforeStart = MailServices.ab.directories.length;
+
+ // Test - Create an LDAP directory
+ let abUri = MailServices.ab.newAddressBook(
+ "test",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ let abCountAfterCreate = MailServices.ab.directories.length;
+ Assert.equal(abCountAfterCreate, abCountBeforeStart + 1);
+
+ // Test - Check we have the directory.
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Test - Check various fields
+ Assert.equal(abDir.dirName, "test");
+ Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec);
+ Assert.ok(abDir.readOnly);
+
+ // Test - Write a UTF-8 Auth DN and check it
+ abDir.authDn = "test\u00D0";
+
+ Assert.equal(abDir.authDn, "test\u00D0");
+
+ // Test - searchDuringLocalAutocomplete
+
+ // Set up an account and identity in the account manager
+ let identity = MailServices.accounts.createIdentity();
+
+ const localAcTests = [
+ // Online checks
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: false,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: false,
+ result: false,
+ },
+ // Offline checks with and without global prefs set, no identity key
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: true,
+ },
+ // Offline checks with and without global prefs set, with identity key
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ // Offline checks, no global prefs, identity ones only
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: kPABData.dirPrefID,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ // Offline checks, global prefs and identity ones
+ {
+ useDir: true,
+ dirSer: kPABData.dirPrefID,
+ idOver: true,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: true,
+ idSer: kPABData.dirPrefID,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ ];
+
+ function checkAc(element, index, array) {
+ dump("Testing index " + index + "\n");
+ Services.prefs.setBoolPref(
+ "ldap_2.autoComplete.useDirectory",
+ element.useDir
+ );
+ Services.prefs.setCharPref(
+ "ldap_2.autoComplete.directoryServer",
+ element.dirSer
+ );
+ identity.overrideGlobalPref = element.idOver;
+ identity.directoryServer = element.idSer;
+ Services.io.offline = element.offline;
+
+ Assert.equal(abDir.useForAutocomplete(element.idKey), element.result);
+ }
+
+ localAcTests.forEach(checkAc);
+
+ MailServices.ab.deleteAddressBook(abDir.URI);
+
+ let abCountAfterDelete = MailServices.ab.directories.length;
+ Assert.equal(abCountAfterDelete, abCountBeforeStart);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldap2.js b/comm/mailnews/addrbook/test/unit/test_ldap2.js
new file mode 100644
index 0000000000..2dc39c4a86
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldap2.js
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for bug 532170. LDAP address book named with cyrillic/chinese
+ * letters doesn't work.
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Test - Create an LDAP directory
+
+ // Use a UTF-8 based directory name
+ var abUri = MailServices.ab.newAddressBook(
+ "\u041C\u0435\u043B\u0435\u043D\u043A\u0438",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ // Test - Check we have the directory.
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Test - Check various fields
+ Assert.equal(abDir.dirName, "\u041C\u0435\u043B\u0435\u043D\u043A\u0438");
+ Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec);
+ Assert.ok(abDir.readOnly);
+
+ // XXX I'd really like a better check than this, to check that searching
+ // works correctly. However we haven't got the support for that at the moment
+ // and this at least ensures that we get a consistent ascii based preference
+ // for the directory.
+ Assert.equal(abDir.dirPrefId, "ldap_2.servers._nonascii");
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapOffline.js b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js
new file mode 100644
index 0000000000..ed81344d03
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite to check that we correctly get child cards for LDAP directories
+ * when offline and that we don't crash.
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+// Main function for the this test so we can check both personal and
+// collected books work correctly in an easy manner.
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Test set-up
+ let abUri = MailServices.ab.newAddressBook(
+ "test",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ const kLDAPFileName = "ldap-1.sqlite";
+
+ // Test setup - copy the data file into place
+ loadABFile("data/cardForEmail", kLDAPFileName);
+
+ // And tell the ldap directory we want this file.
+ abDir.replicationFileName = kLDAPFileName;
+
+ // Now go offline
+ Services.io.offline = true;
+
+ // Make sure we clear any memory that is now loose, so that the crash would
+ // be triggered.
+ gc();
+
+ // Now try and get the card that has been replicated for offline use.
+ Assert.equal(abDir.childCards.length, 5);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapReplication.js b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js
new file mode 100644
index 0000000000..220417a095
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js
@@ -0,0 +1,159 @@
+/* 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 { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+const autocompleteService = Cc[
+ "@mozilla.org/autocomplete/search;1?name=addrbook"
+].getService(Ci.nsIAutoCompleteSearch);
+const jsonFile = do_get_file("data/ldap_contacts.json");
+const replicationService = Cc[
+ "@mozilla.org/addressbook/ldap-replication-service;1"
+].getService(Ci.nsIAbLDAPReplicationService);
+
+add_task(async () => {
+ LDAPServer.open();
+ let ldapContacts = await IOUtils.readJSON(jsonFile.path);
+
+ let bookPref = MailServices.ab.newAddressBook(
+ "XPCShell",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ 0
+ );
+ let book = MailServices.ab.getDirectoryFromId(bookPref);
+ book.QueryInterface(Ci.nsIAbLDAPDirectory);
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ Services.prefs.setCharPref("ldap_2.autoComplete.directoryServer", bookPref);
+ Services.prefs.setBoolPref("ldap_2.autoComplete.useDirectory", true);
+
+ registerCleanupFunction(async () => {
+ LDAPServer.close();
+ });
+
+ let progressResolve;
+ let progressPromise = new Promise(resolve => (progressResolve = resolve));
+ let progressListener = {
+ onStateChange(webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ info("replication started");
+ }
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ info("replication ended");
+ progressResolve();
+ }
+ },
+ onProgressChange(
+ webProgress,
+ request,
+ currentSelfProgress,
+ maxSelfProgress,
+ currentTotalProgress,
+ maxTotalProgress
+ ) {},
+ onLocationChange(webProgress, request, location, flags) {},
+ onStatusChange(webProgress, request, status, message) {},
+ onSecurityChange(webProgress, request, state) {},
+ onContentBlockingEvent(webProgress, request, event) {},
+ };
+
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ for (let contact of Object.values(ldapContacts)) {
+ LDAPServer.writeSearchResultEntry(contact);
+ }
+ LDAPServer.writeSearchResultDone();
+
+ await progressPromise;
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ Services.io.offline = true;
+
+ let cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Greg Lestrade",
+ "Irene Adler",
+ "Jim Moriarty",
+ "John Watson",
+ "Mary Watson",
+ "Molly Hooper",
+ "Mrs Hudson",
+ "Mycroft Holmes",
+ "Sherlock Holmes",
+ ]);
+
+ await new Promise(resolve => {
+ autocompleteService.startSearch("molly", '{"type":"addr_to"}', null, {
+ onSearchResult(search, result) {
+ equal(result.matchCount, 1);
+ equal(result.getValueAt(0), "Molly Hooper <molly@bakerstreet.invalid>");
+ resolve();
+ },
+ });
+ });
+ await new Promise(resolve => {
+ autocompleteService.startSearch("watson", '{"type":"addr_to"}', null, {
+ onSearchResult(search, result) {
+ equal(result.matchCount, 2);
+ equal(result.getValueAt(0), "John Watson <john@bakerstreet.invalid>");
+ equal(result.getValueAt(1), "Mary Watson <mary@bakerstreet.invalid>");
+ resolve();
+ },
+ });
+ });
+
+ // Do it again with different information from the server. Ensure we have the new information.
+
+ progressPromise = new Promise(resolve => (progressResolve = resolve));
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.eurus);
+ LDAPServer.writeSearchResultEntry(ldapContacts.mary);
+ LDAPServer.writeSearchResultEntry(ldapContacts.molly);
+ LDAPServer.writeSearchResultDone();
+
+ await progressPromise;
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Mary Watson",
+ "Molly Hooper",
+ ]);
+
+ // Do it again but cancel. Ensure we still have the old information.
+
+ progressPromise = new Promise(resolve => (progressResolve = resolve));
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.john);
+ LDAPServer.writeSearchResultEntry(ldapContacts.sherlock);
+ LDAPServer.writeSearchResultEntry(ldapContacts.mrs_hudson);
+ replicationService.cancelReplication(book);
+
+ await progressPromise;
+
+ cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Mary Watson",
+ "Molly Hooper",
+ ]);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapquery.js b/comm/mailnews/addrbook/test/unit/test_ldapquery.js
new file mode 100644
index 0000000000..90b1f1673d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapquery.js
@@ -0,0 +1,181 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * Test basic LDAP querying.
+ */
+
+const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Ldapd.jsm"
+);
+const { BinaryServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Binaryd.jsm"
+);
+
+/**
+ * Adaptor class to implement nsILDAPMessageListener with a promise.
+ * It should be passed into LDAP functions as a normal listener. The
+ * caller can then await the promise attribute.
+ * Based on the pattern used in PromiseTestUtils.jsm.
+ *
+ * This base class just rejects all callbacks. Derived classes should
+ * implement the callbacks they need to handle.
+ *
+ * @implements {nsILDAPMessageListener}
+ */
+class PromiseListener {
+ constructor() {
+ this.QueryInterface = ChromeUtils.generateQI(["nsILDAPMessageListener"]);
+ this.promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ }
+ onLDAPMessage(message) {
+ this._reject(new Error("Unexpected onLDAPMessage"));
+ }
+ onLDAPInit() {
+ this._reject(new Error("Unexpected onLDAPInit"));
+ }
+ onLDAPError(status, secInfo, location) {
+ this._reject(new Error(`Unexpected onLDAPError (0x${status.toString(16)}`));
+ }
+}
+
+/**
+ * PromiseInitListener resolves the promise when onLDAPInit is called.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseInitListener extends PromiseListener {
+ onLDAPInit() {
+ this._resolve();
+ }
+}
+
+/**
+ * PromiseBindListener resolves when a bind operation completes.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseBindListener extends PromiseListener {
+ onLDAPMessage(message) {
+ if (Ci.nsILDAPErrors.SUCCESS != message.errorCode) {
+ this._reject(
+ new Error(`Operation failed (LDAP code ${message.errorCode})`)
+ );
+ }
+ if (Ci.nsILDAPMessage.RES_BIND == message.type) {
+ this._resolve(); // All done.
+ }
+ }
+}
+
+/**
+ * PromiseSearchListener collects search results, returning them via promise
+ * when the search is complete.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseSearchListener extends PromiseListener {
+ constructor() {
+ super();
+ this._results = [];
+ }
+ onLDAPMessage(message) {
+ if (Ci.nsILDAPMessage.RES_SEARCH_RESULT == message.type) {
+ this._resolve(this._results); // All done.
+ }
+ if (Ci.nsILDAPMessage.RES_SEARCH_ENTRY == message.type) {
+ this._results.push(message);
+ }
+ }
+}
+
+add_task(async function test_basic_query() {
+ // Load in some test contact data (characters from Sherlock Holmes).
+ let raw = await IOUtils.readUTF8(
+ do_get_file(
+ "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json"
+ ).path
+ );
+ let testContacts = JSON.parse(raw);
+
+ // Set up fake LDAP server, loaded with the test contacts.
+ let daemon = new LDAPDaemon();
+ daemon.add(...Object.values(testContacts));
+ // daemon.setDebug(true);
+ let server = new BinaryServer(LDAPHandlerFn, daemon);
+ server.start();
+
+ // Connect to the fake server.
+ let url = `ldap://localhost:${server.port}`;
+ let ldapURL = Services.io.newURI(url).QueryInterface(Ci.nsILDAPURL);
+ let conn = Cc["@mozilla.org/network/ldap-connection;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsILDAPConnection);
+
+ // Initialisation is async.
+ let initListener = new PromiseInitListener();
+ conn.init(ldapURL, null, initListener, null, Ci.nsILDAPConnection.VERSION3);
+ await initListener.promise;
+
+ // Perform bind.
+ let bindListener = new PromiseBindListener();
+ let bindOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance(
+ Ci.nsILDAPOperation
+ );
+ bindOp.init(conn, bindListener, null);
+ bindOp.simpleBind(""); // no password
+ await bindListener.promise;
+
+ // Run a search.
+ let searchListener = new PromiseSearchListener();
+ let searchOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance(
+ Ci.nsILDAPOperation
+ );
+ searchOp.init(conn, searchListener, null);
+ searchOp.searchExt(
+ "", // dn
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ "(sn=Holmes)", // filter: Find the Holmes family members.
+ "", // wanted_attributes
+ 0, // timeOut
+ 100 // maxEntriesWanted
+ );
+ let matches = await searchListener.promise;
+
+ // Make sure we got the contacts we expected (just use cn for comparing):
+ const holmesCNs = ["Eurus Holmes", "Mycroft Holmes", "Sherlock Holmes"];
+ const holmesGivenNames = ["Eurus", "Mycroft", "Sherlock"];
+ const nonHolmesCNs = [
+ "Greg Lestrade",
+ "Irene Adler",
+ "Jim Moriarty",
+ "John Watson",
+ "Mary Watson",
+ "Molly Hooper",
+ "Mrs Hudson",
+ ];
+ let cns = matches.map(ent => ent.getValues("cn")[0]);
+ cns.sort();
+ Assert.deepEqual(cns, holmesCNs);
+
+ // Test getValues is case insensitive about the attribute name.
+ let givenNames = matches.map(ent => ent.getValues("givenname")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+ givenNames = matches.map(ent => ent.getValues("givenName")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+ givenNames = matches.map(ent => ent.getValues("GIVENNAME")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+
+ // Sanity check: make sure the non-Holmes contacts were excluded.
+ nonHolmesCNs.forEach(cn => Assert.ok(!cns.includes(cn)));
+
+ server.stop();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_mailList1.js b/comm/mailnews/addrbook/test/unit/test_mailList1.js
new file mode 100644
index 0000000000..0889257da6
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_mailList1.js
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for mailing list functions.
+ *
+ * This suite relies on abLists1.mab. checkLists requires that the mailing list
+ * name be "TestList<n>" where <n> is the number of the list that also matches
+ * the <n> in the uri: moz-ab???directory://path/MailList<n>
+ */
+
+function checkLists(childNodes, number) {
+ let count = 0;
+ // See comment above for matching requirements
+ for (let list of childNodes) {
+ if (list.isMailList && list.dirName.startsWith("TestList")) {
+ Assert.equal(list.URI, `${kPABData.URI}/${list.UID}`);
+ count++;
+ }
+ }
+
+ Assert.equal(count, number);
+}
+
+function run_test() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ // Test - Get the directory.
+
+ // XXX Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ let AB = MailServices.ab.getDirectory(kPABData.URI);
+
+ // Test - Check all the expected mailing lists exist.
+
+ // There are three lists in abLists.mab by default.
+ checkLists(AB.childNodes, 3);
+
+ // Test - Add a new list.
+
+ var mailList = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+
+ mailList.isMailList = true;
+ mailList.dirName = "TestList4";
+ mailList.listNickName = "test4";
+ mailList.description = "test4description";
+
+ AB.addMailList(mailList);
+
+ // check them
+ checkLists(AB.childNodes, 4);
+
+ // Test - Remove a list.
+
+ mailList = MailServices.ab.getDirectory(
+ kPABData.URI + "/46cf4cbf-5945-43e4-a822-30c2f2969db9"
+ );
+
+ AB.deleteDirectory(mailList);
+
+ // check them
+ checkLists(AB.childNodes, 3);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js
new file mode 100644
index 0000000000..8faf6e064a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsAbAutoCompleteSearch
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+function acObserver() {}
+
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ },
+};
+
+function run_test() {
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=mydomain"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+ let obsNews = new acObserver();
+ let obsFollowup = new acObserver();
+
+ // Set up an identity in the account manager with the default settings
+ let identity = MailServices.accounts.createIdentity();
+
+ // Initially disable autocomplete
+ identity.autocompleteToMyDomain = false;
+ identity.email = "myemail@foo.invalid";
+
+ // Set up autocomplete parameters
+ let params = JSON.stringify({ idKey: identity.key, type: "addr_to" });
+ let paramsNews = JSON.stringify({
+ idKey: identity.key,
+ type: "addr_newsgroups",
+ });
+ let paramsFollowup = JSON.stringify({
+ idKey: identity.key,
+ type: "addr_followup",
+ });
+
+ // Test - Valid search - this should return no results (autocomplete disabled)
+ acs.startSearch("test", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "test");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Now enable autocomplete for this identity
+ identity.autocompleteToMyDomain = true;
+
+ // Test - Search with empty string
+
+ acs.startSearch(null, params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, null);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ acs.startSearch("", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check ignoring result with comma
+
+ acs.startSearch("a,b", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "a,b");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check returning search string with @ sign
+
+ acs.startSearch("a@b", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "a@b");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 1);
+
+ Assert.equal(obs._result.getValueAt(0), "a@b");
+ Assert.equal(obs._result.getLabelAt(0), "a@b");
+ Assert.equal(obs._result.getCommentAt(0), null);
+ Assert.equal(obs._result.getStyleAt(0), "default-match");
+ Assert.equal(obs._result.getImageAt(0), null);
+
+ // No autocomplete for addr_newsgroups!
+ acs.startSearch("a@b", paramsNews, null, obsNews);
+ Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0);
+
+ // No autocomplete for addr_followup!
+ acs.startSearch("a@b", paramsFollowup, null, obsFollowup);
+ Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0);
+
+ // Test - Add default domain
+
+ acs.startSearch("test1", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "test1");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 1);
+
+ Assert.equal(obs._result.getValueAt(0), "test1@foo.invalid");
+ Assert.equal(obs._result.getLabelAt(0), "test1@foo.invalid");
+ Assert.equal(obs._result.getCommentAt(0), null);
+ Assert.equal(obs._result.getStyleAt(0), "default-match");
+ Assert.equal(obs._result.getImageAt(0), null);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
new file mode 100644
index 0000000000..ffe48506ce
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
@@ -0,0 +1,468 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * First test suite for nsAbAutoCompleteSearch - tests searching in address
+ * books for autocomplete matches, and checks sort order is correct (without
+ * popularity checks).
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// Input and results arrays for the autocomplete tests. This are potentially
+// more complicated than really required, but it was easier to do them
+// on a pattern rather just doing the odd spot check.
+//
+// Note the expected arrays are in expected sort order as well.
+var results = [
+ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName }, // 0
+ { email: "di <emai@foo.invalid>", dirName: kPABData.dirName }, // 1
+ { email: "dis <email@foo.invalid>", dirName: kPABData.dirName }, // 2
+ { email: "disp <e@foo.invalid>", dirName: kPABData.dirName }, // 3
+ { email: "displ <em@foo.invalid>", dirName: kPABData.dirName }, // 4
+ {
+ email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5
+ dirName: kCABData.dirName,
+ },
+ { email: "t <list>", dirName: kPABData.dirName }, // 6
+ { email: "te <lis>", dirName: kPABData.dirName }, // 7
+ { email: "tes <li>", dirName: kPABData.dirName }, // 8
+ // this contact has a nickname of "abcdef"
+ { email: "test <l>", dirName: kPABData.dirName }, // 9
+ { email: "doh, james <DohJames@foo.invalid>", dirName: kPABData.dirName }, // 10
+];
+var firstNames = [
+ { search: "f", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "fi", expected: [0, 1, 3, 4, 5] },
+ { search: "fir", expected: [0, 1, 4, 5] },
+ { search: "firs", expected: [0, 1, 5] },
+ { search: "first", expected: [1, 5] },
+ { search: "firstn", expected: [5] },
+];
+
+var lastNames = [
+ { search: "l", expected: [6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 10] },
+ { search: "la", expected: [0, 2, 3, 4, 5] },
+ { search: "las", expected: [0, 3, 4, 5] },
+ { search: "last", expected: [0, 4, 5] },
+ { search: "lastn", expected: [0, 5] },
+ { search: "lastna", expected: [5] },
+];
+
+var displayNames = [
+ { search: "d", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "di", expected: [1, 2, 3, 4, 5] },
+ { search: "dis", expected: [2, 3, 4, 5] },
+ { search: "disp", expected: [3, 4, 5] },
+ { search: "displ", expected: [4, 5] },
+ { search: "displa", expected: [5] },
+ { search: "doh,", expected: [10] },
+];
+
+var nickNames = [
+ { search: "n", expected: [4, 0, 1, 2, 3, 5, 10] },
+ { search: "ni", expected: [0, 1, 2, 3, 5] },
+ { search: "nic", expected: [1, 2, 3, 5] },
+ { search: "nick", expected: [2, 3, 5] },
+ { search: "nickn", expected: [3, 5] },
+ { search: "nickna", expected: [5] },
+];
+
+var emails = [
+ { search: "e", expected: [0, 1, 2, 3, 4, 5, 10, 7, 8, 9] },
+ { search: "em", expected: [0, 1, 2, 4, 5] },
+ { search: "ema", expected: [0, 1, 2, 5] },
+ { search: "emai", expected: [1, 2, 5] },
+ { search: "email", expected: [2, 5] },
+];
+
+// "l" case tested above
+var lists = [
+ { search: "li", expected: [6, 7, 8, 0, 1, 2, 3, 4, 5, 10] },
+ { search: "lis", expected: [6, 7] },
+ { search: "list", expected: [6] },
+ { search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] },
+ { search: "te", expected: [7, 8, 9, 5] },
+ { search: "tes", expected: [8, 9, 5] },
+ { search: "test", expected: [9, 5] },
+ { search: "abcdef", expected: [9] }, // Bug 441586
+];
+
+var bothNames = [
+ { search: "f l", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "l f", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "firstn lastna", expected: [5] },
+ { search: "lastna firstna", expected: [5] },
+];
+
+var inputs = [
+ firstNames,
+ lastNames,
+ displayNames,
+ nickNames,
+ emails,
+ lists,
+ bothNames,
+];
+
+var PAB_CARD_DATA = [
+ {
+ FirstName: "firs",
+ LastName: "lastn",
+ DisplayName: "d",
+ NickName: "ni",
+ PrimaryEmail: "ema@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "first",
+ LastName: "l",
+ DisplayName: "di",
+ NickName: "nic",
+ PrimaryEmail: "emai@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "f",
+ LastName: "la",
+ DisplayName: "dis",
+ NickName: "nick",
+ PrimaryEmail: "email@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "fi",
+ LastName: "las",
+ DisplayName: "disp",
+ NickName: "nickn",
+ PrimaryEmail: "e@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "fir",
+ LastName: "last",
+ DisplayName: "displ",
+ NickName: "n",
+ PrimaryEmail: "em@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "Doh",
+ LastName: "James",
+ DisplayName: "doh, james",
+ NickName: "j",
+ PrimaryEmail: "DohJames@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+];
+
+var PAB_LIST_DATA = [
+ {
+ dirName: "t",
+ listNickName: null,
+ description: "list",
+ },
+ {
+ dirName: "te",
+ listNickName: null,
+ description: "lis",
+ },
+ {
+ dirName: "tes",
+ listNickName: null,
+ description: "li",
+ },
+ {
+ dirName: "test",
+ listNickName: "abcdef",
+ description: "l",
+ },
+];
+
+var CAB_CARD_DATA = [
+ {
+ FirstName: "FirstName1",
+ LastName: "LastName1",
+ DisplayName: "DisplayName1",
+ NickName: "NickName1",
+ PrimaryEmail: "PrimaryEmail1@test.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "Empty",
+ LastName: "Email",
+ DisplayName: "Empty Email",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+];
+
+var CAB_LIST_DATA = [];
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+ let ab = MailServices.ab.getDirectory(aDirURI);
+
+ // Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ for (let card of ab.childCards) {
+ ab.dropCard(card, false);
+ }
+
+ aCardData.forEach(function (cd) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (var prop in cd) {
+ card.setProperty(prop, cd[prop]);
+ }
+ ab.addCard(card);
+ });
+
+ aMailListData.forEach(function (ld) {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ for (var prop in ld) {
+ list[prop] = ld[prop];
+ }
+ ab.addMailList(list);
+ });
+}
+
+add_task(async () => {
+ // Set up addresses for in the personal address book.
+ setupAddressBookData(kPABData.URI, PAB_CARD_DATA, PAB_LIST_DATA);
+ // ... and collected addresses address book.
+ setupAddressBookData(kCABData.URI, CAB_CARD_DATA, CAB_LIST_DATA);
+
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+ let obsNews = new acObserver();
+ let obsFollowup = new acObserver();
+
+ // Test - Check disabling of autocomplete
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", false);
+
+ let param = JSON.stringify({ type: "addr_to" });
+ let paramNews = JSON.stringify({ type: "addr_newsgroups" });
+ let paramFollowup = JSON.stringify({ type: "addr_followup" });
+
+ let resultPromise = obs.waitForResult();
+ acs.startSearch("abc", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "abc");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check Enabling of autocomplete, but with empty string.
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", true);
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch(null, param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, null);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_IGNORED);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+ Assert.equal(obs._result.defaultIndex, -1);
+
+ // Test - No matches
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch("asjdkljdgfjglkfg", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "asjdkljdgfjglkfg");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+ Assert.equal(obs._result.defaultIndex, -1);
+
+ // Test - Matches
+
+ // Basic quick-check
+ resultPromise = obs.waitForResult();
+ acs.startSearch("email", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "email");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), "");
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // quick-check that nothing is found for addr_newsgroups
+ resultPromise = obsNews.waitForResult();
+ acs.startSearch("email", paramNews, null, obsNews);
+ await resultPromise;
+ Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0);
+
+ // quick-check that nothing is found for addr_followup
+ resultPromise = obsFollowup.waitForResult();
+ acs.startSearch("a@b", paramFollowup, null, obsFollowup);
+ await resultPromise;
+ Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0);
+
+ // Now quick-check with the address book name in the comment column.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch("email", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "email");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // Check input with different case
+ resultPromise = obs.waitForResult();
+ acs.startSearch("EMAIL", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "EMAIL");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let prevRes = obs._result;
+ print("Search #" + index + ": search=" + element.search);
+ resultPromise = obs.waitForResult();
+ acs.startSearch(element.search, param, prevRes, obs);
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (result " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+
+ // Test - Popularity Index
+ print("Checking by popularity index:");
+ let pab = MailServices.ab.getDirectory(kPABData.URI);
+
+ for (let card of pab.childCards) {
+ if (card.isMailList) {
+ continue;
+ }
+
+ switch (card.displayName) {
+ case "dis": // 2
+ case "disp": // 3
+ card.setProperty("PopularityIndex", 4);
+ break;
+ case "displ": // 4
+ card.setProperty("PopularityIndex", 5);
+ break;
+ case "d": // 0
+ card.setProperty("PopularityIndex", 1);
+ break;
+ case "di": // 1
+ card.setProperty("PopularityIndex", 20);
+ break;
+ default:
+ break;
+ }
+
+ pab.modifyCard(card);
+ }
+
+ const popularitySearch = [
+ { search: "d", expected: [1, 4, 2, 3, 0, 5, 10, 9] },
+ { search: "di", expected: [1, 4, 2, 3, 5] },
+ { search: "dis", expected: [4, 2, 3, 5] },
+ { search: "disp", expected: [4, 3, 5] },
+ { search: "displ", expected: [4, 5] },
+ { search: "displa", expected: [5] },
+ ];
+
+ for (let i = 0; i < popularitySearch.length; i++) {
+ await checkInputItem(popularitySearch[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
new file mode 100644
index 0000000000..b2dafd41e8
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Second Test suite for nsAbAutoCompleteSearch - test follow-on lookup after
+ * a previous search.
+ *
+ * We run this test without address books, constructing manually ourselves,
+ * so that we can ensure that we're not getting the data out of the address
+ * books.
+ */
+
+var { getModelQuery } = ChromeUtils.import(
+ "resource:///modules/ABQueryUtils.jsm"
+);
+
+// taken from nsAbAutoCompleteSearch.js
+var ACR = Ci.nsIAutoCompleteResult;
+var nsIAbAutoCompleteResult = Ci.nsIAbAutoCompleteResult;
+
+function nsAbAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+ this.modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+ this.asyncDirectories = [];
+}
+
+nsAbAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ modelQuery: null,
+ searchString: null,
+ searchResult: ACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt: function getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt: function getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt: function getStyleAt(aIndex) {
+ return "local-abook";
+ },
+
+ getImageAt: function getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsIAbAutoCompleteResult
+
+ getCardAt: function getCardAt(aIndex) {
+ return this._searchResults[aIndex].card;
+ },
+
+ getEmailToUse: function getEmailToUse(aIndex) {
+ // For this test we can just use the primary email here.
+ return this._searchResults[aIndex].card.primaryEmail;
+ },
+
+ isCompleteResult: function isCompleteResult(aIndex) {
+ // For this test we claim all results are complete.
+ return true;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAutoCompleteResult",
+ "nsIAbAutoCompleteResult",
+ ]),
+};
+
+function createCard(chars, popularity) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.firstName = "firstName".slice(0, chars);
+ card.lastName = "lastName".slice(0, chars);
+ card.displayName = "displayName".slice(0, chars);
+ card.primaryEmail = "email".slice(0, chars) + "@foo.invalid";
+ card.setProperty("NickName", "nickName".slice(0, chars));
+
+ return card;
+}
+
+var results = [
+ { email: "d <e@foo.invalid>", dirName: kPABData.dirName },
+ { email: "di <em@foo.invalid>", dirName: kPABData.dirName },
+ { email: "dis <ema@foo.invalid>", dirName: kPABData.dirName },
+];
+
+var firstNames = [
+ { search: "fi", expected: [1, 2] },
+ { search: "fir", expected: [2] },
+];
+
+var lastNames = [
+ { search: "la", expected: [1, 2] },
+ { search: "las", expected: [2] },
+];
+
+var inputs = [firstNames, lastNames];
+
+add_task(async () => {
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ // Ensure we've got the comment column set up for extra checking.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ // Make up the last autocomplete result
+ var lastResult = new nsAbAutoCompleteResult();
+
+ lastResult.searchString = "";
+ lastResult.searchResult = ACR.RESULT_SUCCESS;
+ lastResult.defaultIndex = 0;
+ lastResult.errorDescription = null;
+ for (let i = 0; i < results.length; ++i) {
+ lastResult._searchResults.push({
+ value: results[i].email,
+ comment: results[i].dirName,
+ card: createCard(i + 1, 0),
+ });
+ }
+
+ // Test - Matches
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ lastResult,
+ obs
+ );
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
new file mode 100644
index 0000000000..4916c30bc5
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Third Test suite for nsAbAutoCompleteSearch - test for duplicate elimination
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ {
+ email: "test@foo.invalid",
+ displayName: "",
+ popularityIndex: 0,
+ firstName: "test0",
+ value: "test@foo.invalid",
+ },
+ {
+ email: "test@foo.invalid",
+ displayName: "",
+ popularityIndex: 1,
+ firstName: "test1",
+ value: "test@foo.invalid",
+ },
+ {
+ email: "abc@foo.invalid",
+ displayName: "",
+ popularityIndex: 1,
+ firstName: "test2",
+ value: "abc@foo.invalid",
+ },
+ {
+ email: "foo1@foo.invalid",
+ displayName: "d",
+ popularityIndex: 0,
+ firstName: "first1",
+ value: "d <foo1@foo.invalid>",
+ },
+ {
+ email: "foo2@foo.invalid",
+ displayName: "di",
+ popularityIndex: 1,
+ firstName: "first1",
+ value: "di <foo2@foo.invalid>",
+ },
+ {
+ email: "foo3@foo.invalid",
+ displayName: "dis",
+ popularityIndex: 2,
+ firstName: "first2",
+ value: "dis <foo3@foo.invalid>",
+ },
+ {
+ email: "foo2@foo.invalid",
+ displayName: "di",
+ popularityIndex: 3,
+ firstName: "first2",
+ value: "di <foo2@foo.invalid>",
+ },
+ // this just tests we can search for the special chars '(' and ')', bug 749097
+ {
+ email: "bracket@not.invalid",
+ secondEmail: "h@not.invalid",
+ firstName: "Mr.",
+ displayName: "Mr. (Bracket)",
+ value: "Mr. (Bracket) <bracket@not.invalid>",
+ popularityIndex: 2,
+ },
+ {
+ email: "mr@(bracket).not.invalid",
+ secondEmail: "bracket@not.invalid",
+ firstName: "Mr.",
+ displayName: "Mr. Bracket",
+ value: "Mr. Bracket <mr@(bracket).not.invalid>",
+ popularityIndex: 1,
+ },
+];
+
+var duplicates = [
+ { search: "test", expected: [1, 2] },
+ { search: "first", expected: [6, 5, 3] },
+ { search: "(bracket)", expected: [7, 8] },
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ card.firstName = element.firstName;
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ // Test - duplicate elements
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ cards[element.expected[i]].value
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ Assert.equal(
+ obs._result.getCardAt(i).firstName,
+ cards[element.expected[i]].firstName
+ );
+ }
+ }
+
+ for (let i = 0; i < duplicates.length; i++) {
+ await checkInputItem(duplicates[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
new file mode 100644
index 0000000000..e1de6f1bbd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
@@ -0,0 +1,258 @@
+/*
+ * Fourth Test suite for nsAbAutoCompleteSearch - test for second email address.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ // Basic tests for primary and secondary emails.
+ {
+ email: "primary@test.invalid",
+ secondEmail: "second@test.invalid",
+ firstName: "",
+ },
+ {
+ email: "test1@test.invalid",
+ secondEmail: "test2@test.invalid",
+ firstName: "firstName",
+ },
+ {
+ email: "bar1@test.invalid",
+ secondEmail: "bar2@test.invalid",
+ firstName: "sweet",
+ },
+ {
+ email: "boo1@test.invalid",
+ secondEmail: "boo2@test.invalid",
+ firstName: "sample",
+ },
+ {
+ email: "name@test.invalid",
+ secondEmail: "thename@test.invalid",
+ firstName: "thename",
+ },
+ // Test to check correct sorting of primary and secondary emails.
+ {
+ email: "foo_b@test.invalid",
+ secondEmail: "foo_a@test.invalid",
+ displayName: "sortbasic",
+ },
+ {
+ email: "d@test.invalid",
+ secondEmail: "e@test.invalid",
+ displayName: "testsort",
+ },
+ {
+ email: "c@test.invalid",
+ secondEmail: "a@test.invalid",
+ displayName: "testsort",
+ },
+ // "2testsort" does the same as "testsort" but turns the cards around to
+ // ensure the order is always consistent.
+ {
+ email: "c@test.invalid",
+ secondEmail: "a@test.invalid",
+ displayName: "2testsort",
+ },
+ {
+ email: "d@test.invalid",
+ secondEmail: "e@test.invalid",
+ displayName: "2testsort",
+ },
+ {
+ email: "g@test.invalid",
+ secondEmail: "f@test.invalid",
+ displayName: "3testsort",
+ popularityIndex: 3,
+ },
+ {
+ email: "j@test.invalid",
+ secondEmail: "h@test.invalid",
+ displayName: "3testsort",
+ popularityIndex: 5,
+ },
+ // Add a contact that matches, but has no email. Should not show up.
+ { displayName: "primaryX" },
+];
+
+// These are for the initial search
+var searches = [
+ "primary",
+ "second",
+ "firstName",
+ "thename",
+ "sortbasic",
+ "testsort",
+ "2testsort",
+ "3testsort",
+];
+
+var expectedResults = [
+ ["primary@test.invalid", "second@test.invalid"], // searching for primary/second returns
+ [
+ "second@test.invalid", // both the emails as the new search query
+ "primary@test.invalid",
+ ], // looks in both the fields.
+ ["test1@test.invalid", "test2@test.invalid"],
+ ["thename@test.invalid", "name@test.invalid"],
+ ["sortbasic <foo_b@test.invalid>", "sortbasic <foo_a@test.invalid>"],
+ [
+ "testsort <c@test.invalid>",
+ "testsort <a@test.invalid>",
+ "testsort <d@test.invalid>",
+ "testsort <e@test.invalid>",
+ "3testsort <j@test.invalid>",
+ "3testsort <h@test.invalid>",
+ "3testsort <g@test.invalid>",
+ "3testsort <f@test.invalid>",
+ "2testsort <c@test.invalid>",
+ "2testsort <a@test.invalid>",
+ "2testsort <d@test.invalid>",
+ "2testsort <e@test.invalid>",
+ ],
+ [
+ "2testsort <c@test.invalid>",
+ "2testsort <a@test.invalid>",
+ "2testsort <d@test.invalid>",
+ "2testsort <e@test.invalid>",
+ ],
+ [
+ "3testsort <j@test.invalid>",
+ "3testsort <h@test.invalid>",
+ "3testsort <g@test.invalid>",
+ "3testsort <f@test.invalid>",
+ ],
+];
+
+// These are for subsequent searches - reducing the number of results.
+var reductionSearches = ["b", "bo", "boo2"];
+
+var reductionExpectedResults = [
+ [
+ "bar1@test.invalid",
+ "bar2@test.invalid",
+ "boo1@test.invalid",
+ "boo2@test.invalid",
+ "sortbasic <foo_b@test.invalid>",
+ "sortbasic <foo_a@test.invalid>",
+ ],
+ ["boo1@test.invalid", "boo2@test.invalid"],
+ ["boo2@test.invalid"],
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ if ("secondEmail" in element) {
+ card.setProperty("SecondEmail", element.secondEmail);
+ }
+ card.displayName = element.displayName;
+ if ("popularityIndex" in element) {
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ }
+ card.firstName = element.firstName;
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ print("Checking Initial Searches");
+
+ async function checkSearch(element, index) {
+ print("Search #" + index + ": search=" + element);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, expectedResults[index].length);
+
+ for (let i = 0; i < expectedResults[index].length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), expectedResults[index][i]);
+ Assert.equal(obs._result.getLabelAt(i), expectedResults[index][i]);
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ }
+ }
+
+ for (let i = 0; i < searches.length; i++) {
+ await checkSearch(searches[i], i);
+ }
+
+ print("Checking Reduction of Search Results");
+
+ var lastResult = null;
+
+ async function checkReductionSearch(element, index) {
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ lastResult,
+ obs
+ );
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(
+ obs._result.matchCount,
+ reductionExpectedResults[index].length
+ );
+
+ for (var i = 0; i < reductionExpectedResults[index].length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ reductionExpectedResults[index][i]
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ reductionExpectedResults[index][i]
+ );
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ }
+ lastResult = obs._result;
+ }
+
+ for (let i = 0; i < reductionSearches.length; i++) {
+ await checkReductionSearch(reductionSearches[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
new file mode 100644
index 0000000000..a10ac5e4b4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * This suite ensures that we can correctly read and re-set the popularity
+ * indexes on a
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var results = [
+ { email: "d <ema@test.invalid>", dirName: kPABData.dirName },
+ { email: "di <emai@test.invalid>", dirName: kPABData.dirName },
+ { email: "dis <email@test.invalid>", dirName: kPABData.dirName },
+ { email: "disp <e@test.invalid>", dirName: kPABData.dirName },
+ { email: "displ <em@test.invalid>", dirName: kPABData.dirName },
+ { email: "t <list>", dirName: kPABData.dirName },
+ { email: "te <lis>", dirName: kPABData.dirName },
+ { email: "tes <li>", dirName: kPABData.dirName },
+ // this contact has a nickname of "abcdef"
+ { email: "test <l>", dirName: kPABData.dirName },
+];
+
+var firstNames = [
+ { search: "f", expected: [4, 0, 1, 2, 3, 8] },
+ { search: "fi", expected: [4, 0, 1, 3] },
+ { search: "fir", expected: [4, 0, 1] },
+ { search: "firs", expected: [0, 1] },
+ { search: "first", expected: [1] },
+];
+
+var lastNames = [
+ { search: "l", expected: [5, 6, 7, 8, 4, 0, 1, 2, 3] },
+ { search: "la", expected: [4, 0, 2, 3] },
+ { search: "las", expected: [4, 0, 3] },
+ { search: "last", expected: [4, 0] },
+ { search: "lastn", expected: [0] },
+];
+
+var inputs = [firstNames, lastNames];
+
+add_task(async () => {
+ loadABFile("../../../data/tb2hexpopularity", kPABData.fileName);
+
+ // Test - Create a new search component
+
+ let acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ let obs = new acObserver();
+
+ // Ensure we've got the comment column set up for extra checking.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ // Test - Matches
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+
+ // Card at result number 4 is the one with the TB 2 popularity set as "a"
+ // in the file, so check that we're now setting the popularity to 10
+ // and hence future tests don't have to convert it.
+ if (element.expected[i] == 4) {
+ let result = obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ Assert.equal(
+ result.getCardAt(i).getProperty("PopularityIndex", -1),
+ 10
+ );
+ }
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
new file mode 100644
index 0000000000..08b38de7c3
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests for for nsAbAutoCompleteSearch scoring.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ {
+ // 0
+ email: "jd.who@example.com",
+ displayName: "John Doe (:xx)",
+ popularityIndex: 0,
+ firstName: "John",
+ value: "John Doe (:xx) <jd.who@example.com>",
+ },
+
+ {
+ // 1
+ email: "janey_who@example.com",
+ displayName: "Jane Doe",
+ popularityIndex: 0,
+ value: "Jane Doe <janey_who@example.com>",
+ },
+
+ {
+ // 2
+ email: "pf@example.com",
+ displayName: 'Paul "Shitbreak" Finch',
+ popularityIndex: 0,
+ value: 'Paul "Shitbreak" Finch <pf@example.com>',
+ },
+
+ {
+ // 3
+ email: "js@example.com",
+ displayName: "Janine (Stifflers Mom)",
+ popularityIndex: 0,
+ value: "Janine (Stifflers Mom) <js@example.com>",
+ },
+
+ {
+ // 4
+ email: "ex0@example.com",
+ displayName: "Ajden",
+ popularityIndex: 0,
+ value: "Ajden <ex0@example.com>",
+ },
+
+ {
+ // 5
+ email: "5@example.com",
+ displayName: "Foxx",
+ popularityIndex: 0,
+ value: "Foxx <5@example.com>",
+ },
+
+ {
+ // 6
+ email: "6@example.com",
+ displayName: "thewho",
+ popularityIndex: 0,
+ value: "thewho <6@example.com>",
+ },
+
+ {
+ // 7
+ email: "7@example.com",
+ displayName: "fakeshit",
+ popularityIndex: 0,
+ value: "fakeshit <7@example.com>",
+ },
+
+ {
+ // 8
+ email: "8@example.com",
+ displayName: "mastiff",
+ popularityIndex: 0,
+ value: "mastiff <8@example.com>",
+ },
+
+ {
+ // 9
+ email: "9@example.com",
+ displayName: "anyjohn",
+ popularityIndex: 0,
+ value: "anyjohn <9@example.com>",
+ },
+
+ {
+ // 10
+ email: "10@example.com",
+ displayName: "däsh l18n",
+ popularityIndex: 0,
+ value: "däsh l18n <10@example.com>",
+ },
+
+ {
+ // 11
+ email: "11@example.com",
+ displayName: "paul mary",
+ popularityIndex: 0,
+ firstName: "paul",
+ lastName: "mary meyer",
+ value: "paul mary <11@example.com>",
+ },
+
+ {
+ // 12
+ email: "12@example.com",
+ displayName: "paul meyer",
+ popularityIndex: 0,
+ firstName: "paul",
+ lastName: "mary meyer",
+ value: "paul meyer <12@example.com>",
+ },
+
+ {
+ // 13
+ email: "13@example.com",
+ displayName: "mr iron man (exp dev)",
+ popularityIndex: 0,
+ firstName: "iron",
+ lastName: "man",
+ value: "mr iron man (exp dev) <13@example.com>",
+ },
+
+ {
+ // 14
+ email: "14@example.com",
+ displayName: "michael",
+ popularityIndex: 0,
+ nickName: "short",
+ value: "michael <14@example.com>",
+ },
+
+ {
+ // 15
+ email: "15@example.com",
+ displayName: "good boy",
+ popularityIndex: 0,
+ nickName: "sh",
+ value: "good boy <15@example.com>",
+ },
+
+ {
+ // 16
+ email: "16@example.com",
+ displayName: "sherlock holmes",
+ popularityIndex: 0,
+ value: "sherlock holmes <16@example.com>",
+ },
+];
+
+var inputs = [
+ { search: "john", expected: [0, 9] },
+ { search: "doe", expected: [1, 0] },
+ { search: "jd", expected: [0, 4] },
+ { search: "who", expected: [1, 0, 6] },
+ { search: "xx", expected: [0, 5] },
+ { search: "jan", expected: [1, 3] },
+ // expecting nickname to score highest.
+ { search: "sh", expected: [15, 14, 2, 16, 10, 7] },
+ { search: "st", expected: [3, 8] },
+ { search: "paul mary", expected: [11, 12] },
+ { search: '"paul mary"', expected: [11] },
+ { search: '"iron man" mr "exp dev"', expected: [13] },
+ { search: "short", expected: [14] },
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ card.firstName = element.firstName;
+ card.lastName = element.lastName;
+ if ("nickName" in element) {
+ card.setProperty("NickName", element.nickName);
+ }
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ // Test - duplicate elements
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ cards[element.expected[i]].value
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value);
+ }
+ }
+
+ for (let i = 0; i < inputs.length; i++) {
+ await checkInputItem(inputs[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
new file mode 100644
index 0000000000..28bd2d1836
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for nsAbAutoCompleteSearch - tests searching in address
+ * books for autocomplete matches, and checks sort order is correct
+ * according to scores.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// Input and results arrays for the autocomplete tests.
+
+// Note the expected arrays are in expected sort order as well.
+
+var results = [
+ { email: "Tomas Doe <tomez.doe@foo.invalid>" }, // 0
+ { email: "Tomas Doe <tomez.doe@foo2.invalid>" }, // 1
+ { email: "Tomas Doe <tomez.doe@b.example.com>" }, // 2
+ { email: "Tomas Doe <tomez.doe@a.example.com>" }, // 3
+ { email: "Tomek Smith <tomek@example.com>" }, // 4
+];
+
+var inputs = [
+ [
+ { search: "t", expected: [2, 3, 0, 1, 4] },
+ { search: "tom", expected: [0, 1, 2, 3, 4] },
+ { search: "tomek", expected: [4] },
+ ],
+];
+
+var PAB_CARD_DATA = [
+ {
+ FirstName: "Tomas",
+ LastName: "Doe",
+ DisplayName: "Tomas Doe",
+ NickName: "tom",
+ PrimaryEmail: "tomez.doe@foo.invalid",
+ SecondEmail: "tomez.doe@foo2.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 10,
+ // Poison the card data with an unparseable birthday. This will cause the
+ // vCard parser to throw an exception, but it should be caught and the
+ // search should carry on as normal.
+ BirthDay: 25,
+ BirthMonth: 9,
+ BirthYear: "NaN",
+ },
+ {
+ FirstName: "Tomas",
+ LastName: "Doe",
+ DisplayName: "Tomas Doe",
+ PrimaryEmail: "tomez.doe@b.example.com",
+ SecondEmail: "tomez.doe@a.example.com",
+ PreferDisplayName: true,
+ PopularityIndex: 200,
+ },
+ {
+ FirstName: "Tomek",
+ LastName: "Smith",
+ DisplayName: "Tomek Smith",
+ PrimaryEmail: "tomek@example.com",
+ PreferDisplayName: true,
+ PopularityIndex: 3,
+ },
+];
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+ let ab = MailServices.ab.getDirectory(aDirURI);
+
+ // Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ for (let card of ab.childCards) {
+ ab.dropCard(card, false);
+ }
+
+ aCardData.forEach(function (cd) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (var prop in cd) {
+ card.setProperty(prop, cd[prop]);
+ }
+ ab.addCard(card);
+ });
+
+ aMailListData.forEach(function (ld) {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ for (var prop in ld) {
+ list[prop] = ld[prop];
+ }
+ ab.addMailList(list);
+ });
+}
+
+add_task(async () => {
+ // Set up addresses for in the personal address book.
+ setupAddressBookData(kPABData.URI, PAB_CARD_DATA, []);
+
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ let param = JSON.stringify({ type: "addr_to" });
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let prevRes = obs._result;
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(element.search, param, prevRes, obs);
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (result " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js
new file mode 100644
index 0000000000..37238c51e8
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsAbManager functions relating to add/delete directories and
+ * getting the list of directories..
+ */
+
+function checkDirs(aDirs, aDirArray) {
+ // Don't modify the passed in array.
+ var dirArray = aDirArray.concat();
+
+ for (let dir of aDirs) {
+ var loc = dirArray.indexOf(dir.URI);
+
+ Assert.equal(MailServices.ab.getDirectory(dir.URI), dir);
+
+ if (loc == -1) {
+ do_throw(
+ "Unexpected directory " + dir.URI + " found in address book list"
+ );
+ } else {
+ dirArray[loc] = null;
+ }
+ }
+
+ dirArray.forEach(function (value) {
+ Assert.equal(value, null);
+ });
+}
+
+function addDirectory(dirName) {
+ // Add the directory
+ let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType);
+ return MailServices.ab.getDirectoryFromId(dirPrefId);
+}
+
+async function run_test() {
+ var expectedABs = [kPABData.URI, kCABData.URI];
+
+ // Test - Check initial directories
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Add a directory
+
+ var newDirectory1 = addDirectory("testAb1");
+
+ // Test - Check new directory list
+ expectedABs.push(newDirectory1.URI);
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Repeat for a second directory
+
+ var newDirectory2 = addDirectory("testAb2");
+
+ // Test - Check new directory list
+ expectedABs.push(newDirectory2.URI);
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Remove a directory
+
+ var pos = expectedABs.indexOf(newDirectory1.URI);
+
+ expectedABs.splice(pos, 1);
+
+ await promiseDirectoryRemoved(newDirectory1.URI);
+ newDirectory1 = null;
+
+ // Test - Check new directory list
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Repeat the removal
+
+ await promiseDirectoryRemoved(newDirectory2.URI);
+ newDirectory2 = null;
+
+ expectedABs.pop();
+
+ // Test - Check new directory list
+ checkDirs(MailServices.ab.directories, expectedABs);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js
new file mode 100644
index 0000000000..851017a593
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/**
+ * Tests that an address book, once renamed, is not deleted when a sibling address book is deleted.
+ */
+
+function addDirectory(dirName) {
+ let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType);
+ return MailServices.ab.getDirectoryFromId(dirPrefId);
+}
+
+function renameDirectory(directory, newName) {
+ directory.dirName = newName;
+}
+
+/**
+ * Create 4 addressbooks (directories). Rename the second one and delete
+ * the third one. Check if their names are still correct. (bug 745664)
+ */
+async function run_test() {
+ let dirNames = ["testAb0", "testAb1", "testAb2", "testAb3"];
+ let directories = [];
+
+ for (let dirName of dirNames) {
+ directories.push(addDirectory(dirName));
+ }
+
+ dirNames[1] = "newTestAb1";
+ renameDirectory(directories[1], dirNames[1]);
+ for (let dir in dirNames) {
+ Assert.equal(dirNames[dir], directories[dir].dirName);
+ }
+ await promiseDirectoryRemoved(directories[2].URI);
+ dirNames.splice(2, 1);
+ directories.splice(2, 1);
+
+ for (let dir in dirNames) {
+ Assert.equal(dirNames[dir], directories[dir].dirName);
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js
new file mode 100644
index 0000000000..9b9d5a124d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+/**
+ * Creating a new address book with the same name as an existing one should
+ * always produce a unique preference branch. Check that it does.
+ */
+add_task(function testSameName() {
+ let name0 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name0, "ldap_2.servers.name");
+
+ let name1 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name1, "ldap_2.servers.name_1");
+
+ let name2 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name2, "ldap_2.servers.name_2");
+
+ let name3 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name3, "ldap_2.servers.name_3");
+});
+
+/**
+ * Tests that creating a new book with the UID argument assigns the UID to
+ * that book and stores it in the preferences.
+ */
+function subtestCreateWithUID(type, uidValue) {
+ let prefID = MailServices.ab.newAddressBook(
+ "Got a UID",
+ null,
+ type,
+ uidValue
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${prefID}.uid`, ""),
+ uidValue,
+ "UID is saved to the preferences"
+ );
+
+ let book = MailServices.ab.getDirectoryFromId(prefID);
+ Assert.equal(book.UID, uidValue, "created book has the right UID");
+}
+
+add_task(function testCreateWithUID_JS() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE,
+ "01234567-89ab-cdef-0123-456789abcdef"
+ );
+
+ Assert.throws(
+ () =>
+ MailServices.ab.newAddressBook(
+ "Should fail",
+ null,
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE,
+ "01234567-89ab-cdef-0123-456789abcdef"
+ ),
+ /NS_ERROR_ABORT/,
+ "reusing a UID should throw an exception"
+ );
+});
+
+add_task(function testCreateWithUID_CardDAV() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE,
+ "456789ab-cdef-0123-4567-89abcdef0123"
+ );
+});
+
+add_task(function testCreateWithUID_LDAP() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE,
+ "89abcdef-0123-4567-89ab-cdef01234567"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js
new file mode 100644
index 0000000000..42eb370f7b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js
@@ -0,0 +1,43 @@
+/* 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/. */
+
+"use strict";
+
+add_task(async function createAddressBook() {
+ Assert.ok(!MailServices.ab.getDirectoryFromUID("nonsense"));
+
+ let pabFromURI = MailServices.ab.getDirectory(kPABData.URI);
+ let pabFromId = MailServices.ab.getDirectoryFromId(kPABData.dirPrefID);
+ let pabFromUID = MailServices.ab.getDirectoryFromUID(pabFromURI.UID);
+
+ Assert.equal(pabFromId, pabFromURI);
+ Assert.equal(pabFromUID, pabFromURI);
+
+ let historyFromURI = MailServices.ab.getDirectory(kCABData.URI);
+ let historyFromId = MailServices.ab.getDirectoryFromId(kCABData.dirPrefID);
+ let historyFromUID = MailServices.ab.getDirectoryFromUID(historyFromURI.UID);
+
+ Assert.equal(historyFromId, historyFromURI);
+ Assert.equal(historyFromUID, historyFromURI);
+ Assert.notEqual(historyFromUID, pabFromUID);
+
+ let newPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ kPABData.dirType
+ );
+ let newFromId = MailServices.ab.getDirectoryFromId(newPrefId);
+
+ let newFromURI = MailServices.ab.getDirectory(newFromId.URI);
+ let newFromUID = MailServices.ab.getDirectoryFromUID(newFromId.UID);
+
+ Assert.equal(newFromId, newFromURI);
+ Assert.equal(newFromUID, newFromURI);
+ Assert.notEqual(newFromUID, pabFromUID);
+ Assert.notEqual(newFromUID, historyFromUID);
+
+ await promiseDirectoryRemoved(newFromId.URI);
+
+ Assert.ok(!MailServices.ab.getDirectoryFromUID(newFromId.UID));
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js
new file mode 100644
index 0000000000..05beb37a9e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Tests getMailListFromName() and mailListNameExists() which relies on it.
+ */
+add_task(function testGetMailListFromName() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ for (let listName of ["TestList1", "TestList2", "TestList3"]) {
+ Assert.ok(
+ MailServices.ab.mailListNameExists(listName),
+ `AddrBookManager has ${listName}`
+ );
+
+ let list = MailServices.ab.getMailListFromName(listName);
+ Assert.ok(list, `"${listName}" is not null`);
+ Assert.equal(
+ list.dirName,
+ listName,
+ `"${listName}" dirName is "${listName}"`
+ );
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js
new file mode 100644
index 0000000000..7d51cecee1
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/**
+ * Test suite for the getMailListFromName() function.
+ */
+
+"use strict";
+
+/**
+ * Tests the getMailListFromName function returns the correct nsIAbDirectory,
+ * also tests the hasMailListWithName function as it uses the same code.
+ */
+add_task(function testGetMailListFromName() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ // Test all top level lists are returned.
+ let root = MailServices.ab.getDirectory(kPABData.URI);
+ for (let listName of ["TestList1", "TestList2", "TestList3"]) {
+ Assert.ok(root.hasMailListWithName(listName), `parent has "${listName}"`);
+
+ let list = root.getMailListFromName(listName);
+ Assert.ok(list, `"${listName}" is not null`);
+ Assert.equal(
+ list.dirName,
+ listName,
+ `"${listName}" dirName is "${listName}"`
+ );
+ }
+
+ Assert.ok(
+ !root.hasMailListWithName("Non existent"),
+ "hasMailListWithName() returns false for non-existent list name"
+ );
+ Assert.ok(
+ !root.getMailListFromName("Non existent"),
+ "getMailListFromName() returns null for non-existent list name"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js
new file mode 100644
index 0000000000..b24b9ca20e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js
@@ -0,0 +1,428 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsLDAPURL functions.
+ */
+
+// If we are still using the wallet service, then default port numbers
+// are still visible in the password manager, and therefore we need to have
+// them in the url. The toolkit login manager doesn't do this.
+const usingWallet = "nsIWalletService" in Ci;
+const portAdpt = usingWallet ? ":389" : "";
+
+const ldapURLs = [
+ {
+ url: "ldap://localhost/dc=test",
+ spec: "ldap://localhost/dc=test",
+ asciiSpec: "ldap://localhost/dc=test",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: -1,
+ scheme: "ldap",
+ path: "/dc=test",
+ prePath: "ldap://localhost",
+ hostPort: "localhost",
+ displaySpec: "ldap://localhost/dc=test",
+ displayPrePath: "ldap://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: 0,
+ },
+ {
+ url: "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)",
+ spec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ asciiSpec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: usingWallet ? 389 : -1,
+ scheme: "ldap",
+ path: "/dc=test,dc=abc??sub?(objectclass=*)",
+ prePath: "ldap://localhost" + portAdpt,
+ hostPort: "localhost" + portAdpt,
+ displaySpec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ displayPrePath: "ldap://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost" + portAdpt,
+ dn: "dc=test,dc=abc",
+ scope: Ci.nsILDAPURL.SCOPE_SUBTREE,
+ filter: "(objectclass=*)",
+ options: 0,
+ },
+ {
+ url: "ldap://\u65e5\u672c\u8a93.jp:389/dc=tes\u65e5t??one?(oc=xyz)",
+ spec:
+ "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ asciiSpec:
+ "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ host: "xn--wgv71a309e.jp",
+ asciiHost: "xn--wgv71a309e.jp",
+ port: usingWallet ? 389 : -1,
+ scheme: "ldap",
+ path: "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ prePath: "ldap://xn--wgv71a309e.jp" + portAdpt,
+ hostPort: "xn--wgv71a309e.jp" + portAdpt,
+ displaySpec:
+ "ldap://\u65e5\u672c\u8a93.jp" +
+ portAdpt +
+ "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ displayPrePath: "ldap://\u65e5\u672c\u8a93.jp" + portAdpt,
+ displayHost: "\u65e5\u672c\u8a93.jp",
+ displayHostPort: "\u65e5\u672c\u8a93.jp" + portAdpt,
+ dn: "dc=tes\u65e5t",
+ scope: Ci.nsILDAPURL.SCOPE_ONELEVEL,
+ filter: "(oc=xyz)",
+ options: 0,
+ },
+ {
+ url: "ldaps://localhost/dc=test",
+ spec: "ldaps://localhost/dc=test",
+ asciiSpec: "ldaps://localhost/dc=test",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://localhost",
+ hostPort: "localhost",
+ displaySpec: "ldaps://localhost/dc=test",
+ displayPrePath: "ldaps://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+ {
+ url: "ldaps://127.0.0.1/dc=test",
+ spec: "ldaps://127.0.0.1/dc=test",
+ asciiSpec: "ldaps://127.0.0.1/dc=test",
+ host: "127.0.0.1",
+ asciiHost: "127.0.0.1",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://127.0.0.1",
+ hostPort: "127.0.0.1",
+ displaySpec: "ldaps://127.0.0.1/dc=test",
+ displayPrePath: "ldaps://127.0.0.1",
+ displayHost: "127.0.0.1",
+ displayHostPort: "127.0.0.1",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+ {
+ url: "ldaps://[::1]/dc=test",
+ spec: "ldaps://[::1]/dc=test",
+ asciiSpec: "ldaps://[::1]/dc=test",
+ host: "::1",
+ asciiHost: "::1",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://[::1]",
+ hostPort: "[::1]",
+ displaySpec: "ldaps://[::1]/dc=test",
+ displayPrePath: "ldaps://[::1]",
+ displayHost: "::1",
+ displayHostPort: "[::1]",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+];
+
+function run_test() {
+ var url;
+
+ // Test - get and check urls.
+
+ for (let part = 0; part < ldapURLs.length; ++part) {
+ dump("url: " + ldapURLs[part].url + "\n");
+ url = Services.io.newURI(ldapURLs[part].url);
+
+ Assert.equal(url.spec, ldapURLs[part].spec);
+ Assert.equal(url.asciiSpec, ldapURLs[part].asciiSpec);
+ Assert.equal(url.scheme, ldapURLs[part].scheme);
+ Assert.equal(url.host, ldapURLs[part].host);
+ Assert.equal(url.asciiHost, ldapURLs[part].asciiHost);
+ Assert.equal(url.port, ldapURLs[part].port);
+ Assert.equal(url.pathQueryRef, ldapURLs[part].path);
+ Assert.equal(url.prePath, ldapURLs[part].prePath);
+ Assert.equal(url.hostPort, ldapURLs[part].hostPort);
+ Assert.equal(url.displaySpec, ldapURLs[part].displaySpec);
+ Assert.equal(url.displayPrePath, ldapURLs[part].displayPrePath);
+ Assert.equal(url.displayHost, ldapURLs[part].displayHost);
+ Assert.equal(url.displayHostPort, ldapURLs[part].displayHostPort);
+ // XXX nsLDAPURL ought to have classinfo.
+ url = url.QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.dn, ldapURLs[part].dn);
+ Assert.equal(url.scope, ldapURLs[part].scope);
+ Assert.equal(url.filter, ldapURLs[part].filter);
+ Assert.equal(url.options, ldapURLs[part].options);
+ }
+
+ // Test - Check changing ldap values
+ dump("Other Tests\n");
+
+ // Start off with a base url
+ const kBaseURL = "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)";
+
+ url = Services.io.newURI(kBaseURL).QueryInterface(Ci.nsILDAPURL);
+
+ // Test - dn
+
+ url.dn = "dc=short";
+
+ Assert.equal(url.dn, "dc=short");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??sub?(objectclass=*)"
+ );
+
+ // Test - scope
+
+ url.scope = Ci.nsILDAPURL.SCOPE_BASE;
+
+ Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_BASE);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short???(objectclass=*)"
+ );
+
+ url.scope = Ci.nsILDAPURL.SCOPE_ONELEVEL;
+
+ Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_ONELEVEL);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - filter
+
+ url.filter = "(&(oc=ygh)(l=Ереван))";
+
+ Assert.equal(url.filter, "(&(oc=ygh)(l=Ереван))");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short??one?(&(oc=ygh)(l=%D0%95%D1%80%D0%B5%D0%B2%D0%B0%D0%BD))"
+ );
+
+ url.filter = "";
+
+ Assert.equal(url.filter, "(objectclass=*)");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - scheme
+
+ // An old version used to have a bug whereby if you set the scheme to the
+ // same thing twice, you'd get the options set wrongly.
+ url = url
+ .mutate()
+ .setScheme("ldaps")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 1);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+ url = url
+ .mutate()
+ .setScheme("ldaps")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 1);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ Assert.ok(url.schemeIs("ldaps"));
+ Assert.ok(!url.schemeIs("ldap"));
+
+ url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+ url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ Assert.ok(url.schemeIs("ldap"));
+ Assert.ok(!url.schemeIs("ldaps"));
+
+ // Test - Options
+
+ url.options = Ci.nsILDAPURL.OPT_SECURE;
+
+ Assert.equal(url.options, Ci.nsILDAPURL.OPT_SECURE);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ url.options = 0;
+
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - Equals
+
+ var url2 = Services.io
+ .newURI("ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)")
+ .QueryInterface(Ci.nsILDAPURL);
+
+ Assert.ok(url.equals(url2));
+
+ url2 = url2
+ .mutate()
+ .setSpec("ldap://localhost:389/dc=short??sub?(objectclass=*)")
+ .finalize();
+
+ Assert.ok(!url.equals(url2));
+
+ // Test Attributes
+
+ Assert.equal(url.attributes.length, 0);
+
+ // Nothing should happen if the attribute doesn't exist
+ url.removeAttribute("abc");
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ url.addAttribute("dn");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short?dn?one?(objectclass=*)"
+ );
+
+ Assert.equal(url.attributes, "dn");
+
+ url.removeAttribute("dn");
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ var newAttrs = "abc,def,ghi,jkl";
+ url.attributes = newAttrs;
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short?" +
+ newAttrs +
+ "?one?(objectclass=*)"
+ );
+
+ // Try adding an existing attribute - should do nothing
+ url.addAttribute("def");
+ Assert.equal(url.attributes, newAttrs);
+
+ // url.addAttribute("jk");
+
+ Assert.ok(url.hasAttribute("jkl"));
+ Assert.ok(url.hasAttribute("def"));
+ Assert.ok(url.hasAttribute("ABC"));
+ Assert.ok(!url.hasAttribute("cde"));
+ Assert.ok(!url.hasAttribute("3446"));
+ Assert.ok(!url.hasAttribute("kl"));
+ Assert.ok(!url.hasAttribute("jk"));
+
+ // Sub-string of an attribute, so this shouldn't change anything.
+ url.removeAttribute("kl");
+ url.removeAttribute("jk");
+ url.removeAttribute("ef");
+ Assert.equal(url.attributes, newAttrs);
+
+ url.removeAttribute("abc");
+ newAttrs = newAttrs.substring(4);
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short?" +
+ newAttrs +
+ "?one?(objectclass=*)"
+ );
+
+ // This shouldn't fail, just clear the list
+ url.attributes = "";
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Set attributes via the url spec
+
+ newAttrs = "abc,def,ghi,jkl";
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)"
+ );
+
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short??one?(objectclass=*)")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+
+ var attrs = url.attributes;
+ Assert.equal(attrs.length, 0);
+ Assert.equal(url.spec, "ldap://localhost/dc=short??one?(objectclass=*)");
+
+ // Test - clone
+
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)")
+ .finalize();
+
+ var newUrl = url.mutate().finalize();
+
+ Assert.equal(
+ newUrl.spec,
+ "ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)"
+ );
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_photoURL.js b/comm/mailnews/addrbook/test/unit/test_photoURL.js
new file mode 100644
index 0000000000..a6e7796264
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_photoURL.js
@@ -0,0 +1,35 @@
+/* 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 { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+/**
+ * Tests that vCard photo data is correctly translated into a URL for display.
+ */
+add_task(async function testVCardPhotoURL() {
+ let jpegPrefix = "data:image/jpeg;base64,/9j/4AAQSkZJRgABA";
+ let pngPrefix = "data:image/png;base64,iVBORw0KGgoAAAANSU";
+
+ for (let [fileName, expectedURL] of [
+ // Version 3, binary data, binary value type
+ ["v3-binary-jpeg.vcf", jpegPrefix],
+ ["v3-binary-png.vcf", pngPrefix],
+ // Version 3, URI data, binary value type (mismatch)
+ ["v3-uri-binary-jpeg.vcf", jpegPrefix],
+ ["v3-uri-binary-png.vcf", pngPrefix],
+ // Version 3, URI data, URI value type
+ ["v3-uri-uri-jpeg.vcf", jpegPrefix],
+ ["v3-uri-uri-png.vcf", pngPrefix],
+ // Version 4, URI data, URI value type
+ ["v4-uri-jpeg.vcf", jpegPrefix],
+ ["v4-uri-png.vcf", pngPrefix],
+ ]) {
+ info(`testing ${fileName}`);
+ let file = do_get_file(`data/${fileName}`);
+ let vCard = await IOUtils.readUTF8(file.path);
+ let card = VCardUtils.vCardToAbCard(vCard);
+
+ Assert.equal(card.photoURL.substring(0, 40), expectedURL);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js
new file mode 100644
index 0000000000..e879b05cbc
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js
@@ -0,0 +1,79 @@
+/* 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 { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+
+/**
+ * Tests that the mail.displayname.version preference is correctly incremented
+ * if a card's DisplayName or PreferDisplayName properties change.
+ */
+add_task(async function () {
+ function getPrefValue() {
+ return Services.prefs.getIntPref("mail.displayname.version", -999);
+ }
+
+ /**
+ * Effectively the same as the function of the same name in nsMsgDBView.cpp.
+ * This proves the cardForEmailAddress cache in AddrBookManager is correctly
+ * cleared when the preference changes.
+ */
+ function getDisplayNameInAddressBook() {
+ let card = MailServices.ab.cardForEmailAddress("first.last@invalid");
+ if (!card) {
+ return null;
+ }
+
+ let preferDisplayName = card.getPropertyAsBool("PreferDisplayName", true);
+ return preferDisplayName ? card.displayName : card.primaryEmail;
+ }
+
+ Assert.equal(getPrefValue(), -999, "pref has no initial value");
+ Assert.equal(getDisplayNameInAddressBook(), null, "card doesn't exist yet");
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = new AddrBookCard();
+ card.firstName = "first";
+ card.lastName = "last";
+ card.displayName = "first last";
+ card.primaryEmail = "first.last@invalid";
+ book.addCard(card);
+
+ Assert.equal(getPrefValue(), 1, "pref created by adding card");
+ Assert.equal(getDisplayNameInAddressBook(), "first last");
+
+ [card] = book.childCards;
+ card.displayName = "display";
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 2, "pref updated by changing display name");
+ Assert.equal(getDisplayNameInAddressBook(), "display");
+
+ [card] = book.childCards;
+ card.setPropertyAsBool("PreferDisplayName", true);
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 3, "pref updated by adding flag");
+ Assert.equal(getDisplayNameInAddressBook(), "display");
+
+ [card] = book.childCards;
+ card.displayName = "display name";
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 4, "pref updated by changing display name");
+ Assert.equal(getDisplayNameInAddressBook(), "display name");
+
+ [card] = book.childCards;
+ card.setPropertyAsBool("PreferDisplayName", false);
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 5, "pref updated by clearing flag");
+ Assert.equal(getDisplayNameInAddressBook(), "first.last@invalid");
+
+ book.deleteCards([card]);
+
+ Assert.equal(getPrefValue(), 6, "pref updated by deleting card");
+ Assert.equal(getDisplayNameInAddressBook(), null, "card no longer exists");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_search.js b/comm/mailnews/addrbook/test/unit/test_search.js
new file mode 100644
index 0000000000..c25ba17b96
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_search.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+"use strict";
+
+const { getModelQuery, generateQueryURI } = ChromeUtils.import(
+ "resource:///modules/ABQueryUtils.jsm"
+);
+
+const jsonFile = do_get_file("data/ldap_contacts.json");
+
+add_task(async () => {
+ let contacts = await IOUtils.readJSON(jsonFile.path);
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ for (let [name, { attributes }] of Object.entries(contacts)) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = attributes.cn;
+ card.firstName = attributes.givenName;
+ card.lastName = attributes.sn;
+ card.primaryEmail = attributes.mail;
+ contacts[name] = book.addCard(card);
+ }
+
+ let doSearch = async function (searchString, ...expectedContacts) {
+ let foundCards = await new Promise(resolve => {
+ let listener = {
+ cards: [],
+ onSearchFoundCard(card) {
+ this.cards.push(card);
+ },
+ onSearchFinished(status, complete, secInfo, location) {
+ resolve(this.cards);
+ },
+ };
+ book.search(searchString, "", listener);
+ });
+
+ Assert.equal(foundCards.length, expectedContacts.length);
+ for (let name of expectedContacts) {
+ Assert.ok(foundCards.find(c => c.equals(contacts[name])));
+ }
+ };
+
+ await doSearch("(DisplayName,c,watson)", "john", "mary");
+
+ let modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+ await doSearch(
+ generateQueryURI(modelQuery, ["holmes"]),
+ "eurus",
+ "mycroft",
+ "sherlock"
+ );
+ await doSearch(generateQueryURI(modelQuery, ["adler"]), "irene");
+ await doSearch(generateQueryURI(modelQuery, ["redbeard"]));
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCard.js b/comm/mailnews/addrbook/test/unit/test_vCard.js
new file mode 100644
index 0000000000..328be1c8cd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCard.js
@@ -0,0 +1,474 @@
+/* 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/. */
+
+let { VCardProperties, VCardUtils } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+const ANY_UID = "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+
+add_task(function testVCardToPropertyMap() {
+ function check(vCardLine, expectedProps) {
+ let vCard = `BEGIN:VCARD\r\n${vCardLine}\r\nEND:VCARD\r\n`;
+ info(vCard);
+ let properties = VCardProperties.fromVCard(vCard).toPropertyMap();
+ // Check that every property in expectedProps is present in `properties`.
+ // No other property can be present unless it is in `propWhitelist`.
+ for (let [name, value] of properties) {
+ if (name in expectedProps) {
+ Assert.equal(value, expectedProps[name], `expected ${name}`);
+ delete expectedProps[name];
+ } else {
+ Assert.ok(false, `card should not have property '${name}'`);
+ }
+ }
+
+ for (let name of Object.keys(expectedProps)) {
+ Assert.ok(false, `expected ${name} not found`);
+ }
+ }
+
+ // Name
+ check("N:Last;First", { FirstName: "First", LastName: "Last" });
+ check("N:Last;First;;;", { FirstName: "First", LastName: "Last" });
+ check("N:Last;First;Middle;Prefix;Suffix", {
+ FirstName: "First",
+ LastName: "Last",
+ AdditionalNames: "Middle",
+ NamePrefix: "Prefix",
+ NameSuffix: "Suffix",
+ });
+ check("N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.", {
+ FirstName: "John",
+ LastName: "Stevenson",
+ AdditionalNames: "Philip Paul",
+ NamePrefix: "Dr.",
+ NameSuffix: "Jr. M.D. A.C.P.",
+ });
+
+ // Address
+ check(
+ "ADR:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ WorkPOBox: "PO Box 3.14",
+ WorkAddress2: "Apartment 4",
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ }
+ );
+ check(
+ "ADR;TYPE=work:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ WorkPOBox: "PO Box 3.14",
+ WorkAddress2: "Apartment 4",
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ }
+ );
+ check(
+ "ADR;TYPE=home:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ HomePOBox: "PO Box 3.14",
+ HomeAddress2: "Apartment 4",
+ HomeAddress: "123 Main Street",
+ HomeCity: "Any Town",
+ HomeState: "CA",
+ HomeZipCode: "91921-1234",
+ HomeCountry: "U.S.A.",
+ }
+ );
+
+ // Phone
+ check("TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=home:11-2358-13-21", { HomePhone: "11-2358-13-21" });
+ check("TEL;TYPE=cell:11-2358-13-21", { CellularNumber: "11-2358-13-21" });
+ check("TEL;TYPE=pager:11-2358-13-21", { PagerNumber: "11-2358-13-21" });
+ check("TEL;TYPE=fax:11-2358-13-21", { FaxNumber: "11-2358-13-21" });
+
+ check("TEL;TYPE=work;PREF:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work,cell:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work;TYPE=cell:11-2358-13-21", {
+ WorkPhone: "11-2358-13-21",
+ });
+ check("TEL;TYPE=work;VALUE=TEXT:11-2358-13-21", {
+ WorkPhone: "11-2358-13-21",
+ });
+ check("TEL;TYPE=home;VALUE=TEXT:011-2358-13-21", {
+ HomePhone: "011-2358-13-21",
+ });
+ check(
+ "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21\r\nTEL;TYPE=home;VALUE=TEXT:011-2358-13-21",
+ {
+ WorkPhone: "11-2358-13-21",
+ HomePhone: "011-2358-13-21",
+ }
+ );
+ check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", {
+ CellularNumber: "11-2358-13-21",
+ });
+ check("TEL;TYPE=cell;PREF=1:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", {
+ CellularNumber: "11-2358-13-21",
+ });
+ check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell;PREF=1:011-2358-13-21", {
+ CellularNumber: "011-2358-13-21",
+ });
+
+ // Birthday
+ check("BDAY;VALUE=DATE:19830403", {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "1983",
+ });
+ check("BDAY:--0415", { BirthDay: "15", BirthMonth: "4" });
+ check("BDAY:2001", { BirthYear: "2001" });
+ check("BDAY:2006-06", { BirthYear: "2006", BirthMonth: "6" });
+ check("BDAY:--12", { BirthMonth: "12" });
+ check("BDAY:---30", { BirthDay: "30" });
+ // These are error cases, testing that it doesn't throw.
+ check("BDAY;VALUE=DATE:NaN-NaN-NaN", {});
+ check("BDAY;VALUE=TEXT:07/07/1949", {});
+
+ // Anniversary
+ check("ANNIVERSARY;VALUE=DATE:20041207", {
+ AnniversaryDay: "7",
+ AnniversaryMonth: "12",
+ AnniversaryYear: "2004",
+ });
+
+ // Organization: any number of values is valid here.
+ check("ORG:Acme Widgets, Inc.", {
+ Company: "Acme Widgets, Inc.",
+ });
+ check("ORG:Acme Widgets, Inc.;Manufacturing", {
+ Company: "Acme Widgets, Inc.",
+ Department: "Manufacturing",
+ });
+ check("ORG:Acme Widgets, Inc.;Manufacturing;Thingamies", {
+ Company: "Acme Widgets, Inc.",
+ Department: "Manufacturing",
+ });
+
+ // URL
+ // If no type is given assume its WebPage1 (work).
+ check("URL:https://www.thunderbird.net/", {
+ WebPage1: "https://www.thunderbird.net/",
+ });
+
+ check("URL;TYPE=work:https://developer.thunderbird.net/", {
+ WebPage1: "https://developer.thunderbird.net/",
+ });
+
+ check("URL;TYPE=home:https://addons.thunderbird.net/", {
+ WebPage2: "https://addons.thunderbird.net/",
+ });
+
+ check(
+ formatVCard`
+ URL;TYPE=home:https://addons.thunderbird.net/
+ URL;TYPE=work:https://developer.thunderbird.net/`,
+ {
+ WebPage1: "https://developer.thunderbird.net/",
+ WebPage2: "https://addons.thunderbird.net/",
+ }
+ );
+
+ // If a URL without a type is given and a Work Web Page do not import the URL without type.
+ check(
+ formatVCard`
+ URL:https://www.thunderbird.net/
+ URL;TYPE=home:https://addons.thunderbird.net/
+ URL;TYPE=work:https://developer.thunderbird.net/`,
+ {
+ WebPage1: "https://developer.thunderbird.net/",
+ WebPage2: "https://addons.thunderbird.net/",
+ }
+ );
+ // Email: just to be difficult, email is stored by priority, not type.
+ check("EMAIL:first@invalid", { PrimaryEmail: "first@invalid" });
+ check("EMAIL;PREF=1:first@invalid", { PrimaryEmail: "first@invalid" });
+
+ check("EMAIL;PREF=1:first@invalid\r\nEMAIL:second@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+ check("EMAIL:second@invalid\r\nEMAIL;PREF=1:first@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+
+ check("EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+ check("EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+
+ check(
+ "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+
+ // Group-prefixed properties.
+ check(
+ formatVCard`
+ item1.EMAIL:first@invalid
+ item1.X-ABLabel:First`,
+ {
+ PrimaryEmail: "first@invalid",
+ }
+ );
+ check(
+ formatVCard`
+ item1.EMAIL:first@invalid
+ item1.X-ABLabel:First
+ item2.EMAIL:second@invalid
+ item2.X-ABLabel:Second`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check(
+ formatVCard`
+ foo-bar.EMAIL:first@invalid
+ foo-bar.X-ABLabel:First
+ EMAIL:second@invalid`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check(
+ formatVCard`
+ EMAIL:first@invalid
+ abc.EMAIL:second@invalid
+ abc.X-ABLabel:Second`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check("xyz.TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+});
+
+add_task(function testAbCardToVCard() {
+ function check(abCardProps, ...expectedLines) {
+ let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (let [name, value] of Object.entries(abCardProps)) {
+ if (name == "UID") {
+ abCard.UID = abCardProps.UID;
+ continue;
+ }
+ abCard.setProperty(name, value);
+ }
+
+ let vCard = VCardUtils.abCardToVCard(abCard);
+ info(vCard);
+ let vCardLines = vCard.split("\r\n");
+ if (expectedLines.includes(ANY_UID)) {
+ for (let i = 0; i < vCardLines.length; i++) {
+ if (vCardLines[i].startsWith("UID:")) {
+ vCardLines[i] = ANY_UID;
+ }
+ }
+ }
+
+ for (let line of expectedLines) {
+ Assert.ok(vCardLines.includes(line), line);
+ }
+ }
+
+ // UID
+ check(
+ {
+ UID: "12345678-1234-1234-1234-123456789012",
+ },
+ "UID:12345678-1234-1234-1234-123456789012"
+ );
+
+ // Name
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ },
+ "N:Last;First;;;",
+ ANY_UID
+ );
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ AdditionalNames: "Middle",
+ NamePrefix: "Prefix",
+ NameSuffix: "Suffix",
+ },
+ "N:Last;First;Middle;Prefix;Suffix",
+ ANY_UID
+ );
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ NameSuffix: "Suffix",
+ },
+ "N:Last;First;;;Suffix",
+ ANY_UID
+ );
+
+ // Address
+ check(
+ {
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ },
+ "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ ANY_UID
+ );
+ check(
+ {
+ HomeAddress: "123 Main Street",
+ HomeCity: "Any Town",
+ HomeState: "CA",
+ HomeZipCode: "91921-1234",
+ HomeCountry: "U.S.A.",
+ },
+ "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ ANY_UID
+ );
+
+ // Phone
+ check(
+ {
+ WorkPhone: "11-2358-13-21",
+ },
+ "TEL;VALUE=TEXT:11-2358-13-21",
+ ANY_UID
+ );
+ check(
+ {
+ HomePhone: "011-2358-13-21",
+ },
+ "TEL;VALUE=TEXT:011-2358-13-21",
+ ANY_UID
+ );
+ check(
+ {
+ WorkPhone: "11-2358-13-21",
+ HomePhone: "011-2358-13-21",
+ },
+ "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21",
+ "TEL;TYPE=home;VALUE=TEXT:011-2358-13-21",
+ ANY_UID
+ );
+
+ // Birthday
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "1983",
+ },
+ "BDAY;VALUE=DATE:19830403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "", // No value.
+ },
+ "BDAY;VALUE=DATE:--0403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ // BirthYear missing altogether.
+ },
+ "BDAY;VALUE=DATE:--0403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "", // No value.
+ BirthMonth: "", // No value.
+ BirthYear: "1983",
+ },
+ "BDAY;VALUE=DATE:1983",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "", // No value.
+ BirthMonth: "", // No value.
+ BirthYear: "", // No value.
+ },
+ ANY_UID
+ );
+
+ // Anniversary
+ check(
+ {
+ AnniversaryDay: "7",
+ AnniversaryMonth: "12",
+ AnniversaryYear: "2004",
+ },
+ "ANNIVERSARY;VALUE=DATE:20041207",
+ ANY_UID
+ );
+
+ // Email
+ check({ PrimaryEmail: "first@invalid" }, "EMAIL;PREF=1:first@invalid");
+ check({ SecondEmail: "second@invalid" }, "EMAIL:second@invalid");
+ check(
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" },
+ "EMAIL;PREF=1:first@invalid",
+ "EMAIL:second@invalid"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCard21.js b/comm/mailnews/addrbook/test/unit/test_vCard21.js
new file mode 100644
index 0000000000..28fb1b21d4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCard21.js
@@ -0,0 +1,190 @@
+/* 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/. */
+
+let { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+add_task(async () => {
+ function check(vCardLines, expectedProps) {
+ checkWithCase(vCardLines, expectedProps.slice(), false);
+ checkWithCase(
+ vCardLines,
+ expectedProps.map(p => {
+ if (p.params?.type) {
+ p.params.type = p.params.type.toLowerCase();
+ }
+ return p;
+ }),
+ true
+ );
+ }
+
+ function checkWithCase(vCardLines, expectedProps, lowerCase) {
+ let vCard = `BEGIN:VCARD\r\nVERSION:2.1\r\n${vCardLines}\r\nEND:VCARD\r\n`;
+ if (lowerCase) {
+ vCard = vCard.toLowerCase();
+ }
+ info(vCard);
+ let abCard = VCardUtils.vCardToAbCard(vCard);
+ for (let propertyEntry of abCard.vCardProperties.entries) {
+ let index = expectedProps.findIndex(
+ p =>
+ p.name == propertyEntry.name &&
+ p.value.toString() == propertyEntry.value.toString()
+ );
+ Assert.greater(index, -1);
+ let [prop] = expectedProps.splice(index, 1);
+ Assert.deepEqual(propertyEntry.params, prop.params ?? {});
+ }
+
+ for (let { name, value } of expectedProps) {
+ ok(false, `expected ${name}=${value} not found`);
+ }
+ }
+
+ // Different types of phone number.
+ check("TEL:1234567", [{ name: "tel", value: "1234567" }]);
+ check("TEL;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { pref: 1 } },
+ ]);
+ check("TEL;CELL:1234567", [
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ ]);
+ check("TEL;CELL;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ ]);
+ check("TEL;HOME:1234567", [
+ { name: "tel", value: "1234567", params: { type: "HOME" } },
+ ]);
+ check("TEL;HOME;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "HOME", pref: 1 } },
+ ]);
+ check("TEL;VOICE:1234567", [{ name: "tel", value: "1234567" }]);
+ check("TEL;VOICE;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { pref: 1 } },
+ ]);
+ check("TEL;WORK:1234567", [
+ { name: "tel", value: "1234567", params: { type: "WORK" } },
+ ]);
+ check("TEL;WORK;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "WORK", pref: 1 } },
+ ]);
+
+ // Combinations of phone number types.
+ check("TEL;CELL:1234567\r\nTEL;HOME:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ { name: "tel", value: "9876543", params: { type: "HOME" } },
+ ]);
+ check("TEL;CELL;PREF:1234567\r\nTEL;HOME:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "9876543", params: { type: "HOME" } },
+ ]);
+
+ // Phone number preference.
+ check("TEL;CELL;PREF:1234567\r\nTEL;CELL:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "9876543", params: { type: "CELL" } },
+ ]);
+ check("TEL;CELL:1234567\r\nTEL;CELL;PREF:9876543", [
+ { name: "tel", value: "9876543", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ ]);
+
+ // Different types of email.
+ check("EMAIL:pref@invalid", [{ name: "email", value: "pref@invalid" }]);
+ check("EMAIL;PREF:pref@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ ]);
+ check("EMAIL;WORK:work@invalid", [
+ { name: "email", value: "work@invalid", params: { type: "WORK" } },
+ ]);
+ check("EMAIL;WORK;PREF:work@invalid", [
+ { name: "email", value: "work@invalid", params: { type: "WORK", pref: 1 } },
+ ]);
+ check("EMAIL;HOME:home@invalid", [
+ { name: "email", value: "home@invalid", params: { type: "HOME" } },
+ ]);
+ check("EMAIL;HOME;PREF:home@invalid", [
+ { name: "email", value: "home@invalid", params: { type: "HOME", pref: 1 } },
+ ]);
+ check("EMAIL;INTERNET:mail@invalid", [
+ { name: "email", value: "mail@invalid" },
+ ]);
+
+ // Email preference.
+ check("EMAIL;PREF:pref@invalid\r\nEMAIL:other@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ { name: "email", value: "other@invalid" },
+ ]);
+ check("EMAIL:other@invalid\r\nEMAIL;PREF:pref@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ { name: "email", value: "other@invalid" },
+ ]);
+
+ // Address types. Multiple types are allowed, some we don't care about.
+ check("ADR:;;street;town;state", [
+ { name: "adr", value: ["", "", "street", "town", "state"] },
+ ]);
+ check("ADR;WORK:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "WORK" },
+ },
+ ]);
+ check("ADR;HOME:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "HOME" },
+ },
+ ]);
+ check("ADR;DOM:;;street;town;state", [
+ { name: "adr", value: ["", "", "street", "town", "state"] },
+ ]);
+ check("ADR;POSTAL;WORK:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "WORK" },
+ },
+ ]);
+ check("ADR;PARCEL;HOME:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "HOME" },
+ },
+ ]);
+
+ // Quoted-printable handling.
+ check("FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=C3=A9=24=74=20=23=31", [
+ { name: "fn", value: "té$t #1" },
+ ]);
+ check(
+ "FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=65=73=74=20=F0=9F=92=A9",
+ [{ name: "fn", value: "test 💩" }]
+ );
+ check("ORG;QUOTED-PRINTABLE:=74=65=73=74 #3", [
+ { name: "org", value: "test #3" },
+ ]);
+ check("N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=C5=82ast;=C6=92irst", [
+ { name: "n", value: ["Å‚ast", "Æ’irst"] },
+ ]);
+ check(
+ "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\nline 2=0D=0A=\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=",
+ [
+ { name: "note", value: "line 1\r\nline 2\r\nline 3" },
+ { name: "nickname", value: "foo=" },
+ { name: "title", value: "bar=" },
+ ]
+ );
+ check(
+ "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\r\nline 2=0D=0A=\r\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=",
+ [
+ { name: "note", value: "line 1\r\nline 2\r\nline 3" },
+ { name: "nickname", value: "foo=" },
+ { name: "title", value: "bar=" },
+ ]
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCardProperties.js b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js
new file mode 100644
index 0000000000..cf2c28a634
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js
@@ -0,0 +1,899 @@
+/* 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/. */
+
+/* Tests of VCardProperties and VCardPropertyEntry. */
+
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { VCardProperties, VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+function propertyEqual(actual, expected, message) {
+ let actualAsObject = {
+ name: actual.name,
+ params: actual.params,
+ type: actual.type,
+ value: actual.value,
+ };
+ Assert.deepEqual(actualAsObject, expected, message);
+}
+
+function propertyArrayEqual(actual, expected, message) {
+ Assert.deepEqual(
+ actual.map(a => {
+ return {
+ name: a.name,
+ params: a.params,
+ type: a.type,
+ value: a.value,
+ };
+ }),
+ expected,
+ message
+ );
+}
+
+/**
+ * Tests that AddrBookCard supports vCard.
+ */
+add_task(function testAddrBookCard() {
+ let card = new AddrBookCard();
+ Assert.equal(card.supportsVCard, true, "AddrBookCard supports vCard");
+ Assert.ok(card.vCardProperties, "AddrBookCard has vCardProperties");
+ Assert.equal(card.vCardProperties.constructor.name, "VCardProperties");
+});
+
+/**
+ * Tests that nsAbCardProperty does not support vCard.
+ */
+add_task(function testABCardProperty() {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ Assert.equal(
+ card.supportsVCard,
+ false,
+ "nsAbCardProperty does not support vCard"
+ );
+ Assert.strictEqual(
+ card.vCardProperties,
+ null,
+ "nsAbCardProperty has no vCardProperties"
+ );
+});
+
+/**
+ * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a
+ * simple value type.
+ */
+add_task(function testPropertyEntrySingleValue() {
+ let entry = new VCardPropertyEntry("fn", {}, "text", "Juliet");
+ let clone = entry.clone();
+
+ Assert.ok(entry.equals(entry), "original is equal to itself");
+ Assert.ok(entry.equals(clone), "original is equal to cloned object");
+ Assert.ok(clone.equals(entry), "cloned object is equal to original");
+ Assert.ok(clone.equals(clone), "cloned object is equal to itself");
+
+ Assert.equal(clone.value, entry.value, "values are identical");
+
+ let other = new VCardPropertyEntry("n", {}, "text", "Romeo");
+ Assert.ok(!entry.equals(other), "original is not equal to another object");
+ Assert.ok(!other.equals(entry), "another object is not equal to original");
+});
+
+/**
+ * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a
+ * complex value type.
+ */
+add_task(function testPropertyEntryMultiValue() {
+ // A name entry for somebody named "Mr One Two Three Four Senior".
+ let entry = new VCardPropertyEntry("n", {}, "text", [
+ "Four",
+ "One",
+ ["Two", "Three"],
+ "Mr",
+ "Senior",
+ ]);
+ let clone = entry.clone();
+
+ Assert.ok(entry.equals(entry), "original is equal to itself");
+ Assert.ok(entry.equals(clone), "original is equal to cloned object");
+ Assert.ok(clone.equals(entry), "cloned object is equal to original");
+ Assert.ok(clone.equals(clone), "cloned object is equal to itself");
+
+ Assert.deepEqual(clone.value, entry.value, "values are identical");
+
+ Assert.notEqual(
+ clone.value,
+ entry.value,
+ "value arrays are separate objects"
+ );
+ Assert.notEqual(
+ clone.value[2],
+ entry.value[2],
+ "subvalue arrays are separate objects"
+ );
+
+ // A name entry for somebody named "Mr One Two Three Four Junior".
+ let other = new VCardPropertyEntry("n", {}, "text", [
+ "Four",
+ "One",
+ ["Two", "Three"],
+ "Mr",
+ "Junior",
+ ]);
+ Assert.ok(!entry.equals(other), "original is not equal to another object");
+ Assert.ok(!other.equals(entry), "another object is not equal to original");
+});
+
+/**
+ * Tests creating a VCardProperties from a vCard string,
+ * then recreating the vCard.
+ */
+add_task(function testFromToVCard() {
+ let inVCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ FN:Mike Test
+ N:Test;Mike;;;
+ EMAIL;PREF=1:mike@test.invalid
+ NICKNAME:Testing Mike
+ CATEGORIES:testers,quality control,QA
+ END:VCARD`;
+ let properties = VCardProperties.fromVCard(inVCard);
+
+ Assert.equal(properties.entries.length, 6, "entry count");
+ propertyEqual(
+ properties.getFirstEntry("version"),
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ "version entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("fn"),
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "Mike Test",
+ },
+ "fn entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("n"),
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["Test", "Mike", "", "", ""],
+ },
+ "n entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("email"),
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "mike@test.invalid",
+ },
+ "email entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("nickname"),
+ {
+ name: "nickname",
+ params: {},
+ type: "text",
+ value: "Testing Mike",
+ },
+ "multivalue entry with one value"
+ );
+ propertyEqual(
+ properties.getFirstEntry("categories"),
+ {
+ name: "categories",
+ params: {},
+ type: "text",
+ value: ["testers", "quality control", "QA"],
+ },
+ "multivalue entry with multiple values"
+ );
+
+ let outVCard = properties.toVCard();
+ Assert.equal(outVCard, inVCard, "vCard reproduction");
+});
+
+/**
+ * Tests creating a VCardProperties from a Map of old-style address book
+ * properties, then recreating the Map.
+ */
+add_task(function testFromToPropertyMap() {
+ let inProperties = [
+ ["DisplayName", "Mike Test"],
+ ["LastName", "Test"],
+ ["FirstName", "Mike"],
+ ["PrimaryEmail", "mike@test.invalid"],
+ ["Custom1", "custom one"],
+ ["Custom2", "custom two"],
+ ["Custom3", "custom three"],
+ ["Custom4", "custom four"],
+ ];
+ let properties = VCardProperties.fromPropertyMap(
+ new Map(inProperties),
+ "3.0"
+ );
+
+ Assert.equal(properties.entries.length, 8, "entry count");
+ propertyEqual(
+ properties.getFirstEntry("version"),
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ "version entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("fn"),
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "Mike Test",
+ },
+ "fn entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("n"),
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["Test", "Mike", "", "", ""],
+ },
+ "n entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("email"),
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "mike@test.invalid",
+ },
+ "email entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom1"),
+ {
+ name: "x-custom1",
+ params: {},
+ type: "text",
+ value: "custom one",
+ },
+ "custom1 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom2"),
+ {
+ name: "x-custom2",
+ params: {},
+ type: "text",
+ value: "custom two",
+ },
+ "custom2 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom3"),
+ {
+ name: "x-custom3",
+ params: {},
+ type: "text",
+ value: "custom three",
+ },
+ "custom3 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom4"),
+ {
+ name: "x-custom4",
+ params: {},
+ type: "text",
+ value: "custom four",
+ },
+ "custom4 entry"
+ );
+
+ let outProperties = properties.toPropertyMap();
+ Assert.equal(outProperties.size, 8, "property count");
+ for (let [key, value] of inProperties) {
+ Assert.equal(outProperties.get(key), value, `${key} property`);
+ }
+
+ // Tests that `toPropertyMap` doesn't break multi-value entries, which could
+ // happen if `toAbCard` operates on the original entry instead of a clone.
+ properties = new VCardProperties();
+ properties.addEntry(
+ new VCardPropertyEntry("org", {}, "text", ["one", "two", "three", "four"])
+ );
+ properties.toPropertyMap();
+ Assert.deepEqual(properties.getFirstValue("org"), [
+ "one",
+ "two",
+ "three",
+ "four",
+ ]);
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using VCardPropertyEntry.
+ */
+add_task(function testEntryMethods() {
+ // Sanity check.
+
+ let props = new VCardProperties();
+ Assert.deepEqual(props.entries, [], "props has no entries");
+
+ // Add property entries.
+
+ // Real VCardPropertyEntry objects.
+ let charlie = new VCardPropertyEntry(
+ "email",
+ { type: "home" },
+ "text",
+ "charlie@invalid"
+ );
+ let delta = new VCardPropertyEntry(
+ "email",
+ { type: "work" },
+ "text",
+ "delta@invalid"
+ );
+
+ // Ordinary objects for Assert.deepEqual comparison. Use these objects to be
+ // sure of the values being tested.
+ let data = {
+ charlie: {
+ name: "email",
+ params: { type: "home" },
+ type: "text",
+ value: "charlie@invalid",
+ },
+ delta: {
+ name: "email",
+ params: { type: "work" },
+ type: "text",
+ value: "delta@invalid",
+ },
+ juliet: {
+ name: "email",
+ params: { type: "home" },
+ type: "text",
+ value: "juliet@invalid",
+ },
+ };
+
+ Assert.ok(props.addEntry(charlie));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.charlie],
+ "props.email has one entry"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["charlie@invalid"],
+ "props.email has one value"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "charlie@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(props.entries, [data.charlie], "props has one entry");
+
+ Assert.ok(props.addEntry(delta));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.charlie, data.delta],
+ "props.email has two entries"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["charlie@invalid", "delta@invalid"],
+ "props.email has two values"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "charlie@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(
+ props.entries,
+ [data.charlie, data.delta],
+ "props has two entries"
+ );
+
+ Assert.ok(!props.addEntry(charlie));
+ propertyArrayEqual(
+ props.entries,
+ [data.charlie, data.delta],
+ "props still has two entries"
+ );
+
+ // Update a property entry.
+
+ charlie.value = "juliet@invalid";
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.juliet, data.delta],
+ "props.email has two entries"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["juliet@invalid", "delta@invalid"],
+ "props.email has two values"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "juliet@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(
+ props.entries,
+ [data.juliet, data.delta],
+ "props has two entries"
+ );
+
+ // Clone a property entry.
+
+ let juliet = charlie.clone();
+ Assert.notEqual(
+ juliet,
+ charlie,
+ "cloned VCardPropertyEntry is not the same object"
+ );
+ propertyEqual(
+ juliet,
+ data.juliet,
+ "cloned VCardPropertyEntry has the same properties"
+ );
+
+ // Delete a property entry.
+
+ Assert.ok(props.removeEntry(delta));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.juliet],
+ "props.email has one entry"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["juliet@invalid"],
+ "props.email has one value"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "juliet@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(props.entries, [data.juliet], "props has one entry");
+
+ // Delete a property entry using a clone of it.
+
+ Assert.ok(props.removeEntry(juliet));
+ propertyArrayEqual(props.entries, [], "all entries removed");
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using names and values.
+ * Uses the vCard 3 default entry types.
+ */
+add_task(function testValueMethods3() {
+ let props = new VCardProperties();
+
+ // Add a value.
+
+ let first = props.addValue("tel", "1234567");
+ propertyEqual(first, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "1234567",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ ]);
+
+ // Add a second value.
+
+ let second = props.addValue("tel", "2345678");
+ propertyEqual(second, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "2345678",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ ]);
+
+ // Add a value that already exists. The existing property should be returned.
+
+ let secondCopy = props.addValue("tel", "2345678");
+ Assert.equal(secondCopy, second);
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ ]);
+
+ // Add a third value.
+
+ let third = props.addValue("tel", "3456789");
+ propertyEqual(third, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "3456789",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the second value.
+
+ props.removeValue("tel", "2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove a value that's already been removed.
+
+ props.removeValue("tel", "2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove a value that never existed.
+
+ props.removeValue("tel", "4567890");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the first value.
+
+ props.removeValue("tel", "1234567");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the last value.
+
+ props.removeValue("tel", "3456789");
+ propertyArrayEqual(props.entries, []);
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using names and values.
+ * Uses the vCard 4 default entry types.
+ */
+add_task(function testValueMethods4() {
+ let props = new VCardProperties("4.0");
+
+ // Add a value.
+
+ let first = props.addValue("tel", "tel:1234567");
+ propertyEqual(first, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:1234567",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ ]);
+
+ // Add a second value.
+
+ let second = props.addValue("tel", "tel:2345678");
+ propertyEqual(second, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:2345678",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ ]);
+
+ // Add a value that already exists. The existing property should be returned.
+
+ let secondCopy = props.addValue("tel", "tel:2345678");
+ Assert.equal(secondCopy, second);
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ ]);
+
+ // Add a third value.
+
+ let third = props.addValue("tel", "tel:3456789");
+ propertyEqual(third, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:3456789",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the second value.
+
+ props.removeValue("tel", "tel:2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove a value that's already been removed.
+
+ props.removeValue("tel", "tel:2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove a value that never existed.
+
+ props.removeValue("tel", "tel:4567890");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the first value.
+
+ props.removeValue("tel", "tel:1234567");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the last value.
+
+ props.removeValue("tel", "tel:3456789");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ ]);
+});
+
+/**
+ * Tests retrieving entries and values in preference order.
+ */
+add_task(function testSortMethods() {
+ let props = new VCardProperties();
+ props.addEntry(new VCardPropertyEntry("email", {}, "text", "third@invalid"));
+ props.addEntry(
+ new VCardPropertyEntry("email", { pref: 2 }, "text", "second@invalid")
+ );
+ props.addEntry(new VCardPropertyEntry("email", {}, "text", "fourth@invalid"));
+ props.addEntry(
+ new VCardPropertyEntry("email", { pref: 1 }, "text", "first@invalid")
+ );
+
+ propertyArrayEqual(props.getAllEntriesSorted("email"), [
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "first@invalid",
+ },
+ {
+ name: "email",
+ params: { pref: 2 },
+ type: "text",
+ value: "second@invalid",
+ },
+ { name: "email", params: {}, type: "text", value: "third@invalid" },
+ { name: "email", params: {}, type: "text", value: "fourth@invalid" },
+ ]);
+
+ Assert.deepEqual(props.getAllValuesSorted("email"), [
+ "first@invalid",
+ "second@invalid",
+ "third@invalid",
+ "fourth@invalid",
+ ]);
+});
+
+/**
+ * Tests the `clone` method of VCardProperties.
+ */
+add_task(function testClone() {
+ let properties = VCardProperties.fromVCard(
+ formatVCard`
+ BEGIN:VCARD
+ FN:this is a test
+ N:test;this;is,a;;
+ EMAIL;PREF=1;TYPE=WORK:test@invalid
+ EMAIL:test@test.invalid
+ END:VCARD`
+ );
+ let clone = properties.clone();
+
+ Assert.deepEqual(clone.entries, properties.entries);
+ Assert.notEqual(clone.entries, properties.entries);
+
+ for (let i = 0; i < 4; i++) {
+ Assert.deepEqual(clone.entries[i].value, properties.entries[i].value);
+ Assert.notEqual(clone.entries[i], properties.entries[i]);
+ Assert.ok(clone.entries[i].equals(properties.entries[i]));
+ }
+
+ Assert.equal(clone.toVCard(), properties.toVCard());
+});
+
+/**
+ * Tests that entries with a group prefix are correctly handled, and the
+ * `getGroupedEntries` method of VCardProperties.
+ */
+add_task(function testGroupEntries() {
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ GROUP1.FN:test
+ GROUP1.X-FOO:bar
+ NOTE:this doesn't have a group
+ END:VCARD`;
+
+ let properties = VCardProperties.fromVCard(vCard);
+
+ let data = [
+ {
+ name: "fn",
+ params: {
+ group: "group1",
+ },
+ type: "text",
+ value: "test",
+ },
+ {
+ name: "x-foo",
+ params: {
+ group: "group1",
+ },
+ type: "unknown",
+ value: "bar",
+ },
+ {
+ name: "note",
+ params: {},
+ type: "text",
+ value: "this doesn't have a group",
+ },
+ ];
+
+ propertyArrayEqual(properties.entries, data);
+ Assert.equal(properties.toVCard(), vCard);
+ propertyArrayEqual(properties.getGroupedEntries("group1"), data.slice(0, 2));
+
+ let clone = properties.clone();
+ propertyArrayEqual(clone.entries, data);
+ Assert.equal(clone.toVCard(), vCard);
+ propertyArrayEqual(clone.getGroupedEntries("group1"), data.slice(0, 2));
+});
+
+/**
+ * Tests that we correctly fix Google's bad escaping of colons in values, and
+ * other characters in URI values.
+ */
+add_task(function testGoogleEscaping() {
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c\\:oding;;;
+ FN:en\\\\c\\:oding test
+ TITLE:title\\:title\\;title\\,title\\\\title\\\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel\\:0123\\\\4567
+ EMAIL:test\\\\test@invalid
+ NOTE:notes\\:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:http\\://host/url\\:url\\;url\\,url\\\\url
+ END:VCARD`;
+
+ let goodVCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c:oding;;;
+ FN:en\\\\c:oding test
+ TITLE:title:title\\;title\\,title\\\\title\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel:01234567
+ EMAIL:test\\\\test@invalid
+ NOTE:notes:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:http://host/url:url;url,url\\url
+ END:VCARD`;
+
+ let data = [
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["test", "en\\c:oding", "", "", ""],
+ },
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "en\\c:oding test",
+ },
+ {
+ name: "title",
+ params: {},
+ type: "text",
+ value: "title:title;title,title\\title\\:title\\;title\\,title\\\\",
+ },
+ {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "tel:01234567",
+ },
+ {
+ name: "email",
+ params: {},
+ type: "text",
+ value: "test\\test@invalid",
+ },
+ {
+ name: "note",
+ params: {},
+ type: "text",
+ value: "notes:\nnotes;\nnotes,\nnotes\\",
+ },
+ {
+ name: "url",
+ params: {},
+ type: "uri",
+ value: "http://host/url:url;url,url\\url",
+ },
+ ];
+
+ let properties = VCardProperties.fromVCard(vCard, { isGoogleCardDAV: true });
+ propertyArrayEqual(properties.entries, data);
+ Assert.equal(properties.toVCard(), goodVCard);
+
+ let goodProperties = VCardProperties.fromVCard(goodVCard);
+ propertyArrayEqual(goodProperties.entries, data);
+ Assert.equal(goodProperties.toVCard(), goodVCard);
+});
diff --git a/comm/mailnews/addrbook/test/unit/xpcshell.ini b/comm/mailnews/addrbook/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..2701be7a7a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/xpcshell.ini
@@ -0,0 +1,60 @@
+[DEFAULT]
+head = head.js
+support-files = data/*
+tags = addrbook
+
+[test_abCardProperty.js]
+[test_addrBookCard.js]
+[test_basic_nsIAbDirectory.js]
+[test_bug387403.js]
+tags = addrbook vcard
+[test_bug448165.js]
+[test_bug534822.js]
+[test_bug1522453.js]
+[test_bug1769889.js]
+tags = addrbook vcard
+[test_cardForEmail.js]
+[test_collection.js]
+[test_collection_2.js]
+[test_convertOnSave.js]
+tags = addrbook vcard
+[test_db_enumerator.js]
+[test_delete_book.js]
+[test_export.js]
+tags = addrbook vcard
+[test_jsaddrbook.js]
+[test_LDAPMessage.js]
+[test_LDAPSyncQuery.js]
+[test_ldap1.js]
+[test_ldap2.js]
+[test_ldapOffline.js]
+[test_ldapquery.js]
+[test_ldapReplication.js]
+skip-if = debug # Fails for unknown reasons.
+[test_mailList1.js]
+[test_nsAbAutoCompleteMyDomain.js]
+[test_nsAbAutoCompleteSearch1.js]
+[test_nsAbAutoCompleteSearch2.js]
+[test_nsAbAutoCompleteSearch3.js]
+[test_nsAbAutoCompleteSearch4.js]
+[test_nsAbAutoCompleteSearch5.js]
+[test_nsAbAutoCompleteSearch6.js]
+[test_nsAbAutoCompleteSearch7.js]
+[test_nsAbManager2.js]
+[test_nsAbManager3.js]
+[test_nsAbManager4.js]
+[test_nsAbManager5.js]
+[test_nsAbManager6.js]
+[test_nsIAbCard.js]
+tags = addrbook vcard
+[test_nsIAbDirectory_getMailListFromName.js]
+[test_nsLDAPURL.js]
+[test_photoURL.js]
+[test_preferDisplayName.js]
+[test_search.js]
+[test_vCard.js]
+tags = addrbook vcard
+[test_vCard21.js]
+tags = addrbook vcard
+[test_vCardProperties.js]
+tags = addrbook vcard
diff --git a/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini
new file mode 100644
index 0000000000..720607fbe0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head_cardDAV.js
+tags = addrbook carddav vcard
+prefs =
+ carddav.setup.loglevel=Debug
+ carddav.sync.loglevel=Debug
+
+[test_cardDAV_copyCard.js]
+[test_cardDAV_offline.js]
+[test_cardDAV_serverModified.js]
+[test_cardDAV_syncV1.js]
+[test_cardDAV_syncV2.js]
diff --git a/comm/mailnews/base/.eslintrc.js b/comm/mailnews/base/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/base/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/base/content/.eslintrc.js b/comm/mailnews/base/content/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/base/content/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/base/content/dateFormat.js b/comm/mailnews/base/content/dateFormat.js
new file mode 100644
index 0000000000..600aa873fa
--- /dev/null
+++ b/comm/mailnews/base/content/dateFormat.js
@@ -0,0 +1,227 @@
+/* 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/. */
+
+/* Utilities to show and parse user-entered date values used in filter and search rules. */
+
+"use strict";
+
+const formatYMD = 1;
+const formatYDM = 2;
+const formatMDY = 3;
+const formatMYD = 4;
+const formatDMY = 5;
+const formatDYM = 6;
+const formatMIN = 1;
+const formatMAX = 6;
+
+var gSearchDateFormat = 0;
+var gSearchDateSeparator;
+var gSearchDateLeadingZeros;
+
+/**
+ * Get the short date format option of the current locale.
+ * This supports the common case which the date separator is
+ * either '/', '-', '.' and using Christian year.
+ */
+function initLocaleShortDateFormat() {
+ try {
+ const dateFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ });
+ var aDate = new Date(1999, 11, 2);
+ // Short formats can be space-separated, like 02 Dec 1999.
+ var dateString = dateFormatter
+ .format(aDate)
+ .replace(" 2", "2")
+ .replace(/ /g, "/");
+
+ // find out the separator
+ var possibleSeparators = "/-.";
+ var arrayOfStrings;
+ for (let i = 0; i < possibleSeparators.length; ++i) {
+ arrayOfStrings = dateString.split(possibleSeparators[i]);
+ if (arrayOfStrings.length == 3) {
+ gSearchDateSeparator = possibleSeparators[i];
+ break;
+ }
+ }
+
+ // check the format option
+ if (arrayOfStrings.length != 3) {
+ // no successful split
+ console.error(
+ `initLocaleShortDateFormat: could not analyze date format of ${dateString}, defaulting to yyyy/mm/dd`
+ );
+ } else {
+ // The date will contain a zero if the system settings include leading zeros.
+ gSearchDateLeadingZeros = dateString.includes("0");
+
+ // Match 2 as number, since that will match both "2" and "02".
+ // Let's not look for 12 since it could be Dec instead.
+ if (arrayOfStrings[0] == 2) {
+ // 02.12.1999 or 02.1999.12
+ gSearchDateFormat = arrayOfStrings[1] == "1999" ? formatDYM : formatDMY;
+ } else if (arrayOfStrings[1] == 2) {
+ // 12.02.1999 or 1999.02.12
+ gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYDM : formatMDY;
+ } else {
+ // implies arrayOfStrings[2] == 2
+ // 12.1999.02 or 1999.12.02
+ gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYMD : formatMYD;
+ }
+ }
+ } catch (e) {
+ console.error("initLocaleShortDateFormat: caught an exception: " + e);
+ gSearchDateFormat = 0;
+ }
+}
+
+function initializeSearchDateFormat() {
+ if (gSearchDateFormat > 0) {
+ return;
+ }
+
+ // get a search date format option and a separator
+ try {
+ gSearchDateFormat = Services.prefs.getComplexValue(
+ "mailnews.search_date_format",
+ Ci.nsIPrefLocalizedString
+ ).data;
+
+ gSearchDateFormat = parseInt(gSearchDateFormat);
+
+ // if the option is 0 then try to use the format of the current locale
+ if (gSearchDateFormat == 0) {
+ initLocaleShortDateFormat();
+ } else {
+ // initialize the search date format based on preferences
+ if (gSearchDateFormat < formatMIN || gSearchDateFormat > formatMAX) {
+ gSearchDateFormat = formatYMD;
+ }
+
+ gSearchDateSeparator = Services.prefs.getComplexValue(
+ "mailnews.search_date_separator",
+ Ci.nsIPrefLocalizedString
+ ).data;
+
+ gSearchDateLeadingZeros =
+ Services.prefs.getComplexValue(
+ "mailnews.search_date_leading_zeros",
+ Ci.nsIPrefLocalizedString
+ ).data == "true";
+ }
+ } catch (e) {
+ console.error("initializeSearchDateFormat: caught an exception: " + e);
+ gSearchDateFormat = 0;
+ }
+
+ if (gSearchDateFormat == 0) {
+ // Set to yyyy/mm/dd in case we couldn't determine in any way.
+ gSearchDateFormat = formatYMD;
+ gSearchDateSeparator = "/";
+ gSearchDateLeadingZeros = true;
+ }
+}
+
+function convertPRTimeToString(tm) {
+ var time = new Date();
+ // PRTime is in microseconds, JavaScript time is in milliseconds
+ // so divide by 1000 when converting
+ time.setTime(tm / 1000);
+
+ return convertDateToString(time);
+}
+
+function convertDateToString(time) {
+ initializeSearchDateFormat();
+
+ var year = time.getFullYear();
+ var month = time.getMonth() + 1; // since js month is 0-11
+ if (gSearchDateLeadingZeros && month < 10) {
+ month = "0" + month;
+ }
+ var date = time.getDate(); // day
+ if (gSearchDateLeadingZeros && date < 10) {
+ date = "0" + date;
+ }
+
+ var dateStr;
+ var sep = gSearchDateSeparator;
+
+ switch (gSearchDateFormat) {
+ case formatYMD:
+ dateStr = year + sep + month + sep + date;
+ break;
+ case formatYDM:
+ dateStr = year + sep + date + sep + month;
+ break;
+ case formatMDY:
+ dateStr = month + sep + date + sep + year;
+ break;
+ case formatMYD:
+ dateStr = month + sep + year + sep + date;
+ break;
+ case formatDMY:
+ dateStr = date + sep + month + sep + year;
+ break;
+ case formatDYM:
+ dateStr = date + sep + year + sep + month;
+ break;
+ default:
+ dump("valid search date format option is 1-6\n");
+ }
+
+ return dateStr;
+}
+
+function convertStringToPRTime(str) {
+ initializeSearchDateFormat();
+
+ var arrayOfStrings = str.split(gSearchDateSeparator);
+ var year, month, date;
+
+ // set year, month, date based on the format option
+ switch (gSearchDateFormat) {
+ case formatYMD:
+ year = arrayOfStrings[0];
+ month = arrayOfStrings[1];
+ date = arrayOfStrings[2];
+ break;
+ case formatYDM:
+ year = arrayOfStrings[0];
+ month = arrayOfStrings[2];
+ date = arrayOfStrings[1];
+ break;
+ case formatMDY:
+ year = arrayOfStrings[2];
+ month = arrayOfStrings[0];
+ date = arrayOfStrings[1];
+ break;
+ case formatMYD:
+ year = arrayOfStrings[1];
+ month = arrayOfStrings[0];
+ date = arrayOfStrings[2];
+ break;
+ case formatDMY:
+ year = arrayOfStrings[2];
+ month = arrayOfStrings[1];
+ date = arrayOfStrings[0];
+ break;
+ case formatDYM:
+ year = arrayOfStrings[1];
+ month = arrayOfStrings[2];
+ date = arrayOfStrings[0];
+ break;
+ default:
+ dump("valid search date format option is 1-6\n");
+ }
+
+ month -= 1; // since js month is 0-11
+
+ var time = new Date(year, month, date);
+
+ // JavaScript time is in milliseconds, PRTime is in microseconds
+ // so multiply by 1000 when converting
+ return time.getTime() * 1000;
+}
diff --git a/comm/mailnews/base/content/folder-menupopup.js b/comm/mailnews/base/content/folder-menupopup.js
new file mode 100644
index 0000000000..61a912a4fd
--- /dev/null
+++ b/comm/mailnews/base/content/folder-menupopup.js
@@ -0,0 +1,1238 @@
+/* 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/. */
+
+"use strict";
+
+/* globals MozElements MozXULElement PanelUI */
+
+// This file implements `folder-menupopup` custom elements used in traditional
+// menus.
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+
+ const LazyModules = {};
+
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "FeedUtils",
+ "resource:///modules/FeedUtils.jsm"
+ );
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "FolderUtils",
+ "resource:///modules/FolderUtils.jsm"
+ );
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+ );
+
+ /**
+ * Creates an element, sets attributes on it, including always setting the
+ * "generated" attribute to "true", and returns the element. The "generated"
+ * attribute is used to determine which elements to remove when clearing
+ * the menu.
+ *
+ * @param {string} tagName - The tag name of the element to generate.
+ * @param {object} [attributes] - Optional attributes to set on the element.
+ * @param {object} [isObject] - The optional "is" object to use when creating
+ * the element, typically `{is: "folder-menupopup"}`.
+ */
+ function generateElement(tagName, attributes, isObject) {
+ const element = document.createXULElement(tagName, isObject);
+ element.setAttribute("generated", "true");
+
+ if (attributes) {
+ Object.entries(attributes).forEach(([key, value]) => {
+ element.setAttribute(key, value);
+ });
+ }
+ return element;
+ }
+
+ /**
+ * A function to add shared code to the classes for the `folder-menupopup`
+ * custom element. Takes a "Base" class, and returns a class that extends
+ * the "Base" class.
+ *
+ * @param {Class} Base - A class to be extended with shared functionality.
+ * @returns {Class} A class that extends the first class.
+ */
+ let FolderMenu = Base =>
+ class extends Base {
+ constructor() {
+ super();
+
+ window.addEventListener(
+ "unload",
+ () => {
+ // Clean up when being destroyed.
+ this._removeListener();
+ this._teardown();
+ },
+ { once: true }
+ );
+
+ // If non-null, the subFolders of this nsIMsgFolder will be used to
+ // populate this menu. If this is null, the menu will be populated
+ // using the root-folders for all accounts.
+ this._parentFolder = null;
+
+ this._stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/folderWidgets.properties"
+ );
+
+ // Various filtering modes can be used with this menu-binding. To use
+ // one of them, append the mode="foo" attribute to the element. When
+ // building the menu, we will then use this._filters[mode] as a filter
+ // function to eliminate folders that should not be shown.
+ // note: extensions should feel free to plug in here.
+ this._filters = {
+ // Returns true if messages can be filed in the folder.
+ filing(folder) {
+ if (!folder.server.canFileMessagesOnServer) {
+ return false;
+ }
+
+ return folder.canFileMessages || folder.hasSubFolders;
+ },
+
+ // Returns true if we can get mail for this folder. (usually this just
+ // means the "root" fake folder).
+ getMail(folder) {
+ if (folder.isServer && folder.server.type != "none") {
+ return true;
+ }
+ if (folder.server.type == "nntp" || folder.server.type == "rss") {
+ return true;
+ }
+ return false;
+ },
+
+ // Returns true if we can add filters to this folder/account.
+ filters(folder) {
+ // We can always filter news.
+ if (folder.server.type == "nntp") {
+ return true;
+ }
+
+ return folder.server.canHaveFilters;
+ },
+
+ subscribe(folder) {
+ return folder.canSubscribe;
+ },
+
+ newFolder(folder) {
+ return (
+ folder.canCreateSubfolders &&
+ folder.server.canCreateFoldersOnServer
+ );
+ },
+
+ deferred(folder) {
+ return (
+ folder.server.canCreateFoldersOnServer && !folder.supportsOffline
+ );
+ },
+
+ // Folders that are not in a deferred account.
+ notDeferred(folder) {
+ let server = folder.server;
+ return !(
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount
+ );
+ },
+
+ // Folders that can be searched.
+ search(folder) {
+ if (
+ !folder.server.canSearchMessages ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ return false;
+ }
+ return true;
+ },
+
+ // Folders that can subscribe feeds.
+ feeds(folder) {
+ if (
+ folder.server.type != "rss" ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ return false;
+ }
+ return true;
+ },
+
+ junk(folder) {
+ // Don't show servers (nntp & any others) which do not allow search or filing
+ // I don't really understand why canSearchMessages is needed, but it was included in
+ // earlier code, so I include it as well.
+ if (
+ !folder.server.canFileMessagesOnServer ||
+ !folder.server.canSearchMessages
+ ) {
+ return false;
+ }
+ // Show parents that might have usable subfolders, or usable folders.
+ return folder.hasSubFolders || folder.canFileMessages;
+ },
+ };
+
+ // Is this list containing only servers (accounts) and no real folders?
+ this._serversOnly = true;
+
+ /**
+ * Our listener to let us know when folders change/appear/disappear so
+ * we can know to rebuild ourselves.
+ *
+ * @implements {nsIFolderListener}
+ */
+ this._listener = {
+ _menu: this,
+ _clearMenu(menu) {
+ // I'm not quite sure why this isn't always a function (bug 514445).
+ if (menu._teardown) {
+ menu._teardown();
+ }
+ },
+
+ _setCssSelectorsForItem(item) {
+ const child = this._getChildForItem(item);
+ if (child) {
+ this._menu._setCssSelectors(child._folder, child);
+ }
+ },
+
+ _folderAddedOrRemoved(folder) {
+ if (this._filterFunction && !this._filterFunction(folder)) {
+ return;
+ }
+ // xxx we can optimize this later
+ this._clearMenu(this._menu);
+ },
+
+ onFolderAdded(parentFolder, child) {
+ this._folderAddedOrRemoved(child);
+ },
+ onMessageAdded(parentFolder, msg) {},
+ onFolderRemoved(parentFolder, child) {
+ this._folderAddedOrRemoved(child);
+ },
+ onMessageRemoved(parentFolder, msg) {},
+
+ // xxx I stole this listener list from nsMsgFolderDatasource.cpp, but
+ // someone should really document what events are fired when, so that
+ // we make sure we're updating at the right times.
+ onFolderPropertyChanged(item, property, old, newItem) {},
+ onFolderIntPropertyChanged(item, property, old, aNew) {
+ if (item instanceof Ci.nsIMsgFolder) {
+ if (property == "FolderFlag") {
+ if (
+ this._menu.getAttribute("showFavorites") != "true" ||
+ !this._menu._initializedSpecials.has("favorites")
+ ) {
+ return;
+ }
+
+ if (
+ (old & Ci.nsMsgFolderFlags.Favorite) !=
+ (aNew & Ci.nsMsgFolderFlags.Favorite)
+ ) {
+ setTimeout(this._clearMenu, 0, this._menu);
+ }
+ }
+ }
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderBoolPropertyChanged(item, property, old, newItem) {
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderUnicharPropertyChanged(item, property, old, newItem) {
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderPropertyFlagChanged(item, property, old, newItem) {},
+
+ onFolderEvent(folder, eventName) {
+ if (eventName == "MRMTimeChanged") {
+ if (
+ this._menu.getAttribute("showRecent") != "true" ||
+ !this._menu._initializedSpecials.has("recent") ||
+ !this._menu.childWrapper.firstElementChild
+ ) {
+ return;
+ }
+
+ const recentMenuItem = this._menu.childWrapper.firstElementChild;
+ const recentSubMenu =
+ this._menu._getSubMenuForMenuItem(recentMenuItem);
+
+ // If this folder is already in the recent menu, return.
+ if (
+ !recentSubMenu ||
+ this._getChildForItem(folder, recentSubMenu)
+ ) {
+ return;
+ }
+ } else if (eventName == "RenameCompleted") {
+ // Special casing folder renames here, since they require more work
+ // since sort-order may have changed.
+ if (!this._getChildForItem(folder)) {
+ return;
+ }
+ } else {
+ return;
+ }
+ // Folder renamed, or new recent folder, so rebuild.
+ setTimeout(this._clearMenu, 0, this._menu);
+ },
+
+ /**
+ * Helper function to check and see whether we have a menuitem for this
+ * particular nsIMsgFolder.
+ *
+ * @param {nsIMsgFolder} item - The folder to check.
+ * @param {Element} [menu] - Optional menu to look in, defaults to this._menu.
+ * @returns {Element|null} The menuitem for that folder, or null if no
+ * child for that folder exists.
+ */
+ _getChildForItem(item, menu = this._menu) {
+ if (
+ !menu ||
+ !menu.childWrapper.hasChildNodes() ||
+ !(item instanceof Ci.nsIMsgFolder)
+ ) {
+ return null;
+ }
+ for (let child of menu.childWrapper.children) {
+ if (child._folder && child._folder.URI == item.URI) {
+ return child;
+ }
+ }
+ return null;
+ },
+ };
+
+ // True if we have already built our menu items and are now just
+ // listening for changes.
+ this._initialized = false;
+
+ // A Set listing which of our special menus are already built.
+ // E.g. "recent", "favorites".
+ this._initializedSpecials = new Set();
+
+ // The format for displaying names of folders.
+ this._displayformat = null;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ // Call the connectedCallback of the "base" class this mixin class is extending.
+ super.connectedCallback();
+
+ // Get the displayformat if set.
+ if (this.parentNode && this.parentNode.localName == "menulist") {
+ this._displayformat = this.parentNode.getAttribute("displayformat");
+ }
+ }
+
+ set parentFolder(val) {
+ this._parentFolder = val;
+ this._teardown();
+ }
+
+ get parentFolder() {
+ return this._parentFolder;
+ }
+
+ /**
+ * Make sure we remove our listener when the window is being destroyed
+ * or the widget torn down.
+ */
+ _removeListener() {
+ if (!this._initialized) {
+ return;
+ }
+ MailServices.mailSession.RemoveFolderListener(this._listener);
+ }
+
+ /**
+ * Call this if you do not know whether the menu items have been built,
+ * but know that they need to be built now if they haven't been yet.
+ */
+ _ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+
+ // The excludeServers attribute is a comma separated list of server keys.
+ const excludeServers = this.hasAttribute("excludeServers")
+ ? this.getAttribute("excludeServers").split(",")
+ : [];
+
+ // Extensions and other consumers can add to these modes too, see the
+ // note on the _filters field. (Note: empty strings ("") are falsy in JS.)
+ const mode = this.getAttribute("mode");
+
+ const filterFunction = mode ? this._filters[mode] : folder => true;
+
+ const folders = this._getFolders(
+ this._parentFolder,
+ excludeServers,
+ mode ? filterFunction : null
+ );
+
+ this._listener._filterFunction = filterFunction;
+
+ this._build(folders, mode);
+
+ // Lastly, we add a listener to get notified of changes in the folder
+ // structure.
+ MailServices.mailSession.AddFolderListener(
+ this._listener,
+ Ci.nsIFolderListener.all
+ );
+
+ this._initialized = true;
+ }
+
+ /**
+ * Get the folders that will appear in the menu.
+ *
+ * @param {Element} parentFolder - The parent menu popup/view element.
+ * @param {string[]} excludeServers - Server keys for the servers to exclude.
+ * @param {Function} [filterFunction] - Function for filtering the folders.
+ */
+ _getFolders(parentFolder, excludeServers, filterFunction) {
+ let folders;
+
+ // If we have a parent folder, just get the subFolders for that parent.
+ if (parentFolder) {
+ folders = parentFolder.subFolders;
+ } else {
+ // If we don't have a parent, then we assume we should build the
+ // top-level accounts. (Actually we build the fake root folders for
+ // those accounts.)
+ let accounts = LazyModules.FolderUtils.allAccountsSorted(true);
+
+ // Now generate our folder list. Note that we'll special case this
+ // situation elsewhere, to avoid destroying the sort order we just made.
+ folders = accounts.map(acct => acct.incomingServer.rootFolder);
+ }
+
+ if (filterFunction) {
+ folders = folders.filter(filterFunction);
+ }
+
+ if (excludeServers.length > 0) {
+ folders = folders.filter(
+ folder => !excludeServers.includes(folder.server.key)
+ );
+ }
+ return folders;
+ }
+
+ /**
+ * Actually constructs the menu items based on the folders given.
+ *
+ * @param {nsIMsgFolder[]} folders - An array of nsIMsgFolders to use for building.
+ * @param {string} [mode] - The filtering mode. See comment on _filters field.
+ */
+ _build(folders, mode) {
+ let globalInboxFolder = null;
+
+ // See if this is the toplevel menu (usually with accounts).
+ if (!this._parentFolder) {
+ this._addTopLevelMenuItems();
+
+ // If we are showing the accounts for deferring, move Local Folders to the top.
+ if (mode == "deferred") {
+ globalInboxFolder =
+ MailServices.accounts.localFoldersServer.rootFolder;
+ let localFoldersIndex = folders.indexOf(globalInboxFolder);
+ if (localFoldersIndex != -1) {
+ folders.splice(localFoldersIndex, 1);
+ folders.unshift(globalInboxFolder);
+ }
+ }
+ // If we're the root of the folder hierarchy, then we actually don't
+ // want to sort the folders, but rather the accounts to which the
+ // folders belong. Since that sorting was already done, we don't need
+ // to do anything for that case here.
+ } else {
+ this._maybeAddParentFolderMenuItem(mode);
+
+ // Sort the list of folders. We give first priority to the sortKey
+ // property if it is available, otherwise a case-insensitive
+ // comparison of names.
+ folders = folders.sort((a, b) => a.compareSortKeys(b));
+ }
+
+ this._addFoldersMenuItems(folders, mode, globalInboxFolder);
+ if (!this._parentFolder) {
+ this._addTopLevelBottomMenuItems();
+ }
+ }
+
+ /**
+ * Add menu items that only appear at top level, like "Recent".
+ */
+ _addTopLevelMenuItems() {
+ const showRecent = this.getAttribute("showRecent") == "true";
+ const showFavorites = this.getAttribute("showFavorites") == "true";
+
+ if (showRecent) {
+ this.childWrapper.appendChild(
+ this._buildSpecialMenu({
+ special: "recent",
+ label: this.getAttribute("recentLabel"),
+ accessKey: this.getAttribute("recentAccessKey"),
+ })
+ );
+ }
+ if (showFavorites) {
+ this.childWrapper.appendChild(
+ this._buildSpecialMenu({
+ special: "favorites",
+ label: this.getAttribute("favoritesLabel"),
+ accessKey: this.getAttribute("favoritesAccessKey"),
+ })
+ );
+ }
+ if (showRecent || showFavorites) {
+ this.childWrapper.appendChild(this._buildSeparator());
+ }
+ }
+
+ /**
+ * Add menu items that only appear at top level (but last), like "<last>".
+ */
+ _addTopLevelBottomMenuItems() {
+ if (this.getAttribute("showLast") != "true") {
+ return;
+ }
+ const folderURI = Services.prefs.getStringPref(
+ "mail.last_msg_movecopy_target_uri"
+ );
+ const folder =
+ folderURI && LazyModules.MailUtils.getExistingFolder(folderURI);
+ if (!folder) {
+ return;
+ }
+
+ this.childWrapper.appendChild(this._buildSeparator());
+ const attributes = {
+ label: `${folder.prettyName} - ${folder.server.prettyName}`,
+ ...this._getCssSelectorAttributes(folder),
+ };
+ this.childWrapper.appendChild(this._buildMenuItem(attributes, folder));
+ }
+
+ /**
+ * Populate a "recent" or "favorites" special submenu with either the
+ * recently used or favorite folders, to allow for easy access.
+ *
+ * @param {Element} menu - The menu or toolbarbutton element for which one
+ * wants to populate the special sub menu.
+ * @param {Element} submenu - The submenu element, typically a menupopup.
+ */
+ _populateSpecialSubmenu(menu, submenu) {
+ let specialType = menu.getAttribute("special");
+ if (this._initializedSpecials.has(specialType)) {
+ return;
+ }
+
+ // Iterate through all folders in all accounts matching the current filter.
+ let specialFolders = MailServices.accounts.allFolders;
+ if (this._listener._filterFunction) {
+ specialFolders = specialFolders.filter(
+ this._listener._filterFunction
+ );
+ }
+
+ switch (specialType) {
+ case "recent":
+ // Find the most recently modified ones.
+ specialFolders = LazyModules.FolderUtils.getMostRecentFolders(
+ specialFolders,
+ Services.prefs.getIntPref("mail.folder_widget.max_recent"),
+ "MRMTime"
+ );
+ break;
+ case "favorites":
+ specialFolders = specialFolders.filter(folder =>
+ folder.getFlag(Ci.nsMsgFolderFlags.Favorite)
+ );
+ break;
+ }
+
+ // Cache the pretty names so that they do not need to be fetched
+ // with quadratic complexity when sorting by name.
+ let specialFoldersMap = specialFolders.map(folder => {
+ return {
+ folder,
+ name: folder.prettyName,
+ };
+ });
+
+ // Because we're scanning across multiple accounts, we can end up with
+ // several folders with the same name. Find those dupes.
+ let dupeNames = new Set();
+ for (let i = 0; i < specialFoldersMap.length; i++) {
+ for (let j = i + 1; j < specialFoldersMap.length; j++) {
+ if (specialFoldersMap[i].name == specialFoldersMap[j].name) {
+ dupeNames.add(specialFoldersMap[i].name);
+ }
+ }
+ }
+
+ for (let folderItem of specialFoldersMap) {
+ // If this folder name appears multiple times in the recent list,
+ // append the server name to disambiguate.
+ // TODO:
+ // - maybe this could use verboseFolderFormat from messenger.properties
+ // instead of hardcoded " - ".
+ // - disambiguate folders with same name in same account
+ // (in different subtrees).
+ let label = folderItem.name;
+ if (dupeNames.has(label)) {
+ label += " - " + folderItem.folder.server.prettyName;
+ }
+
+ folderItem.label = label;
+ }
+
+ // Make sure the entries are sorted alphabetically.
+ specialFoldersMap.sort((a, b) =>
+ LazyModules.FolderUtils.folderNameCompare(a.label, b.label)
+ );
+
+ // Create entries for each of the recent folders.
+ for (let folderItem of specialFoldersMap) {
+ let attributes = {
+ label: folderItem.label,
+ ...this._getCssSelectorAttributes(folderItem.folder),
+ };
+
+ submenu.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folderItem.folder)
+ );
+ }
+
+ if (specialFoldersMap.length == 0) {
+ menu.setAttribute("disabled", "true");
+ }
+
+ this._initializedSpecials.add(specialType);
+ }
+
+ /**
+ * Add a menu item that refers back to the parent folder when there is a
+ * showFileHereLabel attribute or no mode attribute. However don't
+ * add such a menu item if one of the following conditions is met:
+ * (-) There is no parent folder.
+ * (-) Folder is server and showAccountsFileHere is explicitly false.
+ * (-) Current folder has a mode, the parent folder can be selected,
+ * no messages can be filed into the parent folder (e.g. when the
+ * parent folder is a news group or news server) and the folder
+ * mode is not equal to newFolder.
+ * The menu item will have the value of the fileHereLabel attribute as
+ * label or if the attribute does not exist the name of the parent
+ * folder instead.
+ *
+ * @param {string} mode - The mode attribute.
+ */
+ _maybeAddParentFolderMenuItem(mode) {
+ let folder = this._parentFolder;
+ if (
+ folder &&
+ (this.getAttribute("showFileHereLabel") == "true" || !mode)
+ ) {
+ let showAccountsFileHere = this.getAttribute("showAccountsFileHere");
+ if (
+ (!folder.isServer || showAccountsFileHere != "false") &&
+ (!mode ||
+ mode == "newFolder" ||
+ folder.noSelect ||
+ folder.canFileMessages ||
+ showAccountsFileHere == "true")
+ ) {
+ let attributes = {};
+
+ if (this.hasAttribute("fileHereLabel")) {
+ attributes.label = this.getAttribute("fileHereLabel");
+ attributes.accesskey = this.getAttribute("fileHereAccessKey");
+ } else {
+ attributes.label = folder.prettyName;
+ Object.assign(attributes, this._getCssSelectorAttributes(folder));
+ }
+
+ if (folder.noSelect) {
+ attributes.disabled = "true";
+ }
+
+ this.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folder)
+ );
+ this.childWrapper.appendChild(this._buildSeparator());
+ }
+ }
+ }
+
+ /**
+ * Add menu items, one for each folder.
+ *
+ * @param {nsIMsgFolder[]} folders - Array of folder objects.
+ * @param {string} mode - The mode attribute.
+ * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder.
+ */
+ _addFoldersMenuItems(folders, mode, globalInboxFolder) {
+ // disableServers attribute is a comma separated list of server keys.
+ const disableServers = this.hasAttribute("disableServers")
+ ? this.getAttribute("disableServers").split(",")
+ : [];
+
+ // We need to call this, or hasSubFolders will always return false.
+ // Remove this workaround when Bug 502900 is fixed.
+ LazyModules.MailUtils.discoverFolders();
+ this._serversOnly = true;
+
+ let [shouldExpand, labels] = this._getShouldExpandAndLabels();
+
+ for (let folder of folders) {
+ if (!folder.isServer) {
+ this._serversOnly = false;
+ }
+
+ let attributes = {
+ label: this._getFolderLabel(mode, globalInboxFolder, folder),
+ ...this._getCssSelectorAttributes(folder),
+ };
+
+ if (disableServers.includes(folder.server.key)) {
+ attributes.disabled = "true";
+ }
+
+ if (!folder.hasSubFolders || !shouldExpand(folder.server.type)) {
+ // There are no subfolders, create a simple menu item.
+ this.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folder)
+ );
+ } else {
+ // There are subfolders, create a menu item with a submenu.
+ // xxx this is slightly problematic in that we haven't confirmed
+ // whether any of the subfolders will pass the filter.
+
+ this._serversOnly = false;
+
+ let submenuAttributes = {};
+
+ [
+ "class",
+ "type",
+ "fileHereLabel",
+ "showFileHereLabel",
+ "oncommand",
+ "showAccountsFileHere",
+ "mode",
+ "disableServers",
+ "position",
+ ].forEach(attribute => {
+ if (this.hasAttribute(attribute)) {
+ submenuAttributes[attribute] = this.getAttribute(attribute);
+ }
+ });
+
+ const [menuItem, submenu] = this._buildMenuItemWithSubmenu(
+ attributes,
+ true,
+ folder,
+ submenuAttributes
+ );
+
+ // If there are labels, we add an item and separator to the submenu.
+ if (labels) {
+ const serverAttributes = { label: labels[folder.server.type] };
+
+ submenu.childWrapper.appendChild(
+ this._buildMenuItem(serverAttributes, folder, this)
+ );
+
+ submenu.childWrapper.appendChild(this._buildSeparator());
+ }
+
+ this.childWrapper.appendChild(menuItem);
+ }
+ }
+ }
+
+ /**
+ * Return the label to use for a folder.
+ *
+ * @param {string} mode - The mode, e.g. "deferred".
+ * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder.
+ * @param {nsIMsgFolder} folder - The folder for which we are getting a label.
+ * @returns {string} The label to use for the folder.
+ */
+ _getFolderLabel(mode, globalInboxFolder, folder) {
+ if (
+ mode == "deferred" &&
+ folder.isServer &&
+ folder.server.rootFolder == globalInboxFolder
+ ) {
+ return this._stringBundle.formatStringFromName("globalInbox", [
+ folder.prettyName,
+ ]);
+ }
+ return folder.prettyName;
+ }
+
+ /**
+ * Let the user have a list of subfolders for all account types, none of
+ * them, or only some of them. Returns an array containing a function that
+ * determines whether to show subfolders for a given account type, and an
+ * object mapping account types to label names (may be null).
+ *
+ * @returns {any[]} - An array; [0] is the shouldExpand function, [1] is
+ * the labels object.
+ */
+ _getShouldExpandAndLabels() {
+ let shouldExpand;
+ let labels = null;
+ if (
+ this.getAttribute("expandFolders") == "true" ||
+ !this.hasAttribute("expandFolders")
+ ) {
+ shouldExpand = () => true;
+ } else if (this.getAttribute("expandFolders") == "false") {
+ shouldExpand = () => false;
+ } else {
+ // We want a subfolder list for only some servers. We also may need
+ // to create headers to select the servers. If so, then headlabels
+ // is a comma-delimited list of labels corresponding to the server
+ // types specified in expandFolders.
+ let types = this.getAttribute("expandFolders").split(/ *, */);
+ // Set the labels. labels[type] = label
+ if (this.hasAttribute("headlabels")) {
+ let labelNames = this.getAttribute("headlabels").split(/ *, */);
+ labels = {};
+ // If the length isn't equal, don't give them any of the labels,
+ // since any combination will probably be wrong.
+ if (labelNames.length == types.length) {
+ for (let index in types) {
+ labels[types[index]] = labelNames[index];
+ }
+ }
+ }
+ shouldExpand = e => types.includes(e);
+ }
+ return [shouldExpand, labels];
+ }
+
+ /**
+ * Set attributes on a menu, menuitem, or toolbarbutton element to allow
+ * for CSS styling.
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem.
+ * @param {Element} menuNode - The actual DOM node to set attributes on.
+ */
+ _setCssSelectors(folder, menuNode) {
+ const cssAttributes = this._getCssSelectorAttributes(folder);
+
+ Object.entries(cssAttributes).forEach(([key, value]) =>
+ menuNode.setAttribute(key, value)
+ );
+ }
+
+ /**
+ * Returns attributes to be set on a menu, menuitem, or toolbarbutton
+ * element to allow for CSS styling.
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu item.
+ * @returns {object} Contains the CSS selector attributes.
+ */
+ _getCssSelectorAttributes(folder) {
+ let attributes = {};
+
+ // First the SpecialFolder attribute.
+ attributes.SpecialFolder =
+ LazyModules.FolderUtils.getSpecialFolderString(folder);
+
+ // Now the biffState.
+ let biffStates = ["NewMail", "NoMail", "UnknownMail"];
+ for (let state of biffStates) {
+ if (folder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) {
+ attributes.BiffState = state;
+ break;
+ }
+ }
+
+ attributes.IsServer = folder.isServer;
+ attributes.IsSecure = folder.server.isSecure;
+ attributes.ServerType = folder.server.type;
+ attributes.IsFeedFolder =
+ !!LazyModules.FeedUtils.getFeedUrlsInFolder(folder);
+
+ return attributes;
+ }
+
+ /**
+ * This function returns a formatted display name for a menulist
+ * selected folder. The desired format is set as the 'displayformat'
+ * attribute of the folderpicker's <menulist>, one of:
+ * 'name' (default) - Folder
+ * 'verbose' - Folder on Account
+ * 'path' - Account/Folder/Subfolder
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem.
+ * @returns {string} The display name.
+ */
+ getDisplayName(folder) {
+ if (folder.isServer) {
+ return folder.prettyName;
+ }
+
+ if (this._displayformat == "verbose") {
+ return this._stringBundle.formatStringFromName(
+ "verboseFolderFormat",
+ [folder.prettyName, folder.server.prettyName]
+ );
+ }
+
+ if (this._displayformat == "path") {
+ return (
+ LazyModules.FeedUtils.getFolderPrettyPath(folder) || folder.name
+ );
+ }
+
+ return folder.name;
+ }
+
+ /**
+ * Makes a given folder selected.
+ * TODO: This function does not work yet for the appmenu. However, as of
+ * June 2019, this functionality is not used in the appmenu.
+ *
+ * @param {nsIMsgFolder} inputFolder - The folder to select (if none,
+ * then Choose Folder). If inputFolder is not in this popup, but is
+ * instead a descendant of a member of the popup, that ancestor will be
+ * selected.
+ * @returns {boolean} Is true if any usable folder was found, otherwise false.
+ */
+ selectFolder(inputFolder) {
+ // Set the label of the menulist element as if folder had been selected.
+ function setupParent(folder, menulist, noFolders) {
+ let menupopup = menulist.menupopup;
+ if (folder) {
+ menulist.setAttribute("label", menupopup.getDisplayName(folder));
+ } else if (noFolders) {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("noFolders")
+ );
+ } else if (menupopup._serversOnly) {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("chooseAccount")
+ );
+ } else {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("chooseFolder")
+ );
+ }
+ menulist.setAttribute("value", folder ? folder.URI : "");
+ menulist.setAttribute("IsServer", folder ? folder.isServer : false);
+ menulist.setAttribute(
+ "IsSecure",
+ folder ? folder.server.isSecure : false
+ );
+ menulist.setAttribute(
+ "ServerType",
+ folder ? folder.server.type : "none"
+ );
+ menulist.setAttribute(
+ "SpecialFolder",
+ folder
+ ? LazyModules.FolderUtils.getSpecialFolderString(folder)
+ : "none"
+ );
+ menulist.setAttribute(
+ "IsFeedFolder",
+ Boolean(folder && LazyModules.FeedUtils.getFeedUrlsInFolder(folder))
+ );
+ }
+
+ let folder;
+ if (inputFolder) {
+ for (let child of this.children) {
+ if (
+ child &&
+ child._folder &&
+ !child.disabled &&
+ (child._folder.URI == inputFolder.URI ||
+ (child.tagName == "menu" &&
+ child._folder.isAncestorOf(inputFolder)))
+ ) {
+ if (child._folder.URI == inputFolder.URI) {
+ this.parentNode.selectedItem = child;
+ }
+ folder = inputFolder;
+ break;
+ }
+ }
+ }
+
+ // If the caller specified a folder to select and it was not
+ // found, or if the caller didn't pass a folder (meaning a logical
+ // and valid folder wasn't determined), don't blow up but reset
+ // attributes and set a nice Choose Folder label so the user may
+ // select a valid folder per the filter for this picker. If there are
+ // no children, then no folder passed the filter; disable the menulist
+ // as there's nothing to choose from.
+ let noFolders;
+ if (!this.childElementCount) {
+ this.parentNode.setAttribute("disabled", true);
+ noFolders = true;
+ } else {
+ this.parentNode.removeAttribute("disabled");
+ noFolders = false;
+ }
+
+ setupParent(folder, this.parentNode, noFolders);
+ return !!folder;
+ }
+
+ /**
+ * Removes all menu items from this menu, removes their submenus. This
+ * function is called when a change that affects this menu is detected
+ * by the listener.
+ */
+ _teardown() {
+ if (!this._initialized) {
+ return;
+ }
+ const children = this.childWrapper.children;
+ // We iterate in reverse order because children is live so it changes
+ // as we remove child nodes.
+ for (let i = children.length - 1; i >= 0; i--) {
+ const item = children[i];
+ if (item.getAttribute("generated") != "true") {
+ continue;
+ }
+ const submenu = this._getSubMenuForMenuItem(item);
+
+ if (submenu && "_teardown" in submenu) {
+ submenu._teardown();
+ submenu.remove();
+ }
+ item.remove();
+ }
+
+ this._removeListener();
+
+ this._initialized = false;
+ this._initializedSpecials.clear();
+ }
+ };
+
+ /**
+ * The MozFolderMenupopup widget is used as a menupopup that contains menu
+ * items and submenus for all folders from every account (or some subset of
+ * folders and accounts). It is also used to provide a menu with a menuitem
+ * for each account. Each menu item gets displayed with the folder or
+ * account name and icon.
+ *
+ * @augments {MozElements.MozMenuPopup}
+ */
+ let MozFolderMenuPopup = FolderMenu(
+ class extends MozElements.MozMenuPopup {
+ constructor() {
+ super();
+
+ // To improve performance, only build the menu when it is shown.
+ this.addEventListener(
+ "popupshowing",
+ event => {
+ this._ensureInitialized();
+ },
+ true
+ );
+
+ // Because the menu items in a panelview go inside a child vbox but are
+ // direct children of a menupopup, we set up a consistent way to append
+ // and access menu items for both cases.
+ this.childWrapper = this;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.setAttribute("is", "folder-menupopup");
+
+ // Find out if we are in a wrapper (customize toolbars mode is active).
+ let inWrapper = false;
+ let node = this;
+ while (XULElement.isInstance(node)) {
+ if (node.id.startsWith("wrapper-")) {
+ inWrapper = true;
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ if (!inWrapper) {
+ if (this.hasAttribute("original-width")) {
+ // If we were in a wrapper before and have a width stored, restore it now.
+ if (this.getAttribute("original-width") == "none") {
+ this.removeAttribute("width");
+ } else {
+ this.setAttribute("width", this.getAttribute("original-width"));
+ }
+
+ this.removeAttribute("original-width");
+ }
+
+ // If we are a child of a menulist, and we aren't in a wrapper, we
+ // need to build our content right away, otherwise the menulist
+ // won't have proper sizing.
+ if (this.parentNode && this.parentNode.localName == "menulist") {
+ this._ensureInitialized();
+ }
+ } else {
+ // But if we're in a wrapper, remove our children, because we're
+ // getting re-created when the toolbar customization closes.
+ this._teardown();
+
+ // Store our current width and set a safe small width when we show
+ // in a wrapper.
+ if (!this.hasAttribute("original-width")) {
+ this.setAttribute(
+ "original-width",
+ this.hasAttribute("width") ? this.getAttribute("width") : "none"
+ );
+ this.setAttribute("width", "100");
+ }
+ }
+ }
+
+ /**
+ * Given a menu item, return the menupopup that it opens.
+ *
+ * @param {Element} menu - The menu item, typically a `menu` element.
+ * @returns {Element|null} The `menupopup` element or null if none found.
+ */
+ _getSubMenuForMenuItem(menu) {
+ return menu.querySelector("menupopup");
+ }
+
+ /**
+ * Returns a `menuseparator` element for use in a `menupopup`.
+ */
+ _buildSeparator() {
+ return generateElement("menuseparator");
+ }
+
+ /**
+ * Builds a menu item (`menuitem`) element that does not open a submenu
+ * (i.e. not a `menu` element).
+ *
+ * @param {object} [attributes] - Attributes to set on the element.
+ * @param {nsIMsgFolder} folder - The folder associated with the menu item.
+ * @returns {Element} A `menuitem`.
+ */
+ _buildMenuItem(attributes, folder) {
+ const menuitem = generateElement("menuitem", attributes);
+ menuitem.classList.add("folderMenuItem", "menuitem-iconic");
+ menuitem._folder = folder;
+ return menuitem;
+ }
+
+ /**
+ * Builds a menu item (`menu`) element and an associated submenu
+ * (`menupopup`) element.
+ *
+ * @param {object} attributes - Attributes to set on the `menu` element.
+ * @param {boolean} folderSubmenu - Whether the submenu is to be a
+ * `folder-menupopup` element.
+ * @param {nsIMsgFolder} [folder] - The folder associated with the menu item.
+ * @param {object} submenuAttributes - Attributes to set on the `menupopup` element.
+ * @returns {Element[]} Array containing the `menu` and
+ * `menupopup` elements.
+ */
+ _buildMenuItemWithSubmenu(
+ attributes,
+ folderSubmenu,
+ folder,
+ submenuAttributes
+ ) {
+ const menu = generateElement("menu", attributes);
+ menu.classList.add("folderMenuItem", "menu-iconic");
+
+ const isObject = folderSubmenu ? { is: "folder-menupopup" } : null;
+
+ const menupopup = generateElement(
+ "menupopup",
+ submenuAttributes,
+ isObject
+ );
+
+ if (folder) {
+ menu._folder = folder;
+ menupopup._parentFolder = folder;
+ }
+
+ if (!menupopup.childWrapper) {
+ menupopup.childWrapper = menupopup;
+ }
+
+ menu.appendChild(menupopup);
+
+ return [menu, menupopup];
+ }
+
+ /**
+ * Build a special menu item (`menu`) and an empty submenu (`menupopup`)
+ * for it. The submenu is populated just before it is shown by
+ * `_populateSpecialSubmenu`.
+ *
+ * The submenu (`menupopup`) is just a standard element, not a custom
+ * element (`folder-menupopup`).
+ *
+ * @param {object} [attributes] - Attributes to set on the menu item element.
+ * @returns {Element} The menu item (`menu`) element.
+ */
+ _buildSpecialMenu(attributes) {
+ const [menu, menupopup] = this._buildMenuItemWithSubmenu(attributes);
+
+ menupopup.addEventListener(
+ "popupshowing",
+ event => {
+ this._populateSpecialSubmenu(menu, menupopup);
+ },
+ { once: true }
+ );
+
+ return menu;
+ }
+ }
+ );
+
+ customElements.define("folder-menupopup", MozFolderMenuPopup, {
+ extends: "menupopup",
+ });
+}
diff --git a/comm/mailnews/base/content/folderProps.js b/comm/mailnews/base/content/folderProps.js
new file mode 100644
index 0000000000..324bfcac40
--- /dev/null
+++ b/comm/mailnews/base/content/folderProps.js
@@ -0,0 +1,480 @@
+/* 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/. */
+
+/* import-globals-from retention.js */
+/* global BigInt */
+
+var { FolderTreeProperties } = ChromeUtils.import(
+ "resource:///modules/FolderTreeProperties.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+
+var gMsgFolder;
+var gLockedPref = null;
+
+var gDefaultColor = "";
+
+window.addEventListener("load", folderPropsOnLoad);
+document.addEventListener("dialogaccept", folderPropsOKButton);
+document.addEventListener("dialogcancel", folderCancelButton);
+
+/**
+ * The folderPropsSink is the class that gets notified of an imap folder's
+ * properties.
+ *
+ * @implements {nsIMsgImapFolderProps}
+ */
+var gFolderPropsSink = {
+ setFolderType(folderTypeString) {
+ var typeLabel = document.getElementById("folderType.text");
+ if (typeLabel) {
+ typeLabel.setAttribute("value", folderTypeString);
+ }
+ // get the element for the folder type label and set value on it.
+ },
+
+ setFolderTypeDescription(folderDescription) {
+ var folderTypeLabel = document.getElementById("folderDescription.text");
+ if (folderTypeLabel) {
+ folderTypeLabel.setAttribute("value", folderDescription);
+ }
+ },
+
+ setFolderPermissions(folderPermissions) {
+ var permissionsLabel = document.getElementById("folderPermissions.text");
+ var descTextNode = document.createTextNode(folderPermissions);
+ permissionsLabel.appendChild(descTextNode);
+ },
+
+ serverDoesntSupportACL() {
+ var typeLabel = document.getElementById("folderTypeLabel");
+ if (typeLabel) {
+ typeLabel.setAttribute("hidden", "true");
+ }
+ var permissionsLabel = document.getElementById("permissionsDescLabel");
+ if (permissionsLabel) {
+ permissionsLabel.setAttribute("hidden", "true");
+ }
+ },
+
+ setQuotaStatus(folderQuotaStatus) {
+ var quotaStatusLabel = document.getElementById("folderQuotaStatus");
+ if (quotaStatusLabel) {
+ quotaStatusLabel.setAttribute("value", folderQuotaStatus);
+ }
+ },
+
+ showQuotaData(showData) {
+ var quotaStatusLabel = document.getElementById("folderQuotaStatus");
+ var folderQuotaData = document.getElementById("folderQuotaData");
+
+ if (quotaStatusLabel && folderQuotaData) {
+ quotaStatusLabel.hidden = showData;
+ folderQuotaData.hidden = !showData;
+ }
+ },
+
+ setQuotaData(folderQuota) {
+ let quotaDetails = document.getElementById("quotaDetails");
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+
+ for (let quota of folderQuota) {
+ let li = document.createElement("li");
+ let name = document.createElement("span");
+ name.textContent = quota.name;
+ li.appendChild(name);
+
+ let progress = document.createElement("progress");
+ progress.classList.add("quota-percentage");
+ progress.setAttribute("value", quota.usage);
+ progress.setAttribute("max", quota.limit);
+
+ li.appendChild(progress);
+
+ let percentage = document.createElement("span");
+ document.l10n.setAttributes(percentage, "quota-percent-used", {
+ percent: Number((100n * BigInt(quota.usage)) / BigInt(quota.limit)),
+ });
+ li.appendChild(percentage);
+
+ li.appendChild(document.createTextNode(" — "));
+
+ let details = document.createElement("span");
+ if (/STORAGE/i.test(quota.name)) {
+ let usage = messenger.formatFileSize(quota.usage * 1024);
+ let limit = messenger.formatFileSize(quota.limit * 1024);
+ details.textContent = `${usage} / ${limit}`;
+ } else {
+ details.textContent = `${quota.usage} / ${quota.limit}`;
+ }
+ li.appendChild(details);
+
+ quotaDetails.appendChild(li);
+ }
+ },
+};
+
+function doEnabling() {
+ var nameTextbox = document.getElementById("name");
+ document.querySelector("dialog").getButton("accept").disabled =
+ !nameTextbox.value;
+}
+
+function folderPropsOKButton(event) {
+ if (gMsgFolder) {
+ if (
+ document.getElementById("offline.selectForOfflineFolder").checked ||
+ document.getElementById("offline.selectForOfflineNewsgroup").checked
+ ) {
+ gMsgFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+
+ if (document.getElementById("folderCheckForNewMessages").checked) {
+ gMsgFolder.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+ } else {
+ gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.CheckNew);
+ }
+
+ let glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch");
+ if (!glodaCheckbox.hidden) {
+ if (glodaCheckbox.checked) {
+ // We pass true here so that folders such as trash and junk can still
+ // have a priority set.
+ Gloda.resetFolderIndexingPriority(gMsgFolder, true);
+ } else {
+ Gloda.setFolderIndexingPriority(
+ gMsgFolder,
+ Gloda.getFolderForFolder(gMsgFolder).kIndexingNeverPriority
+ );
+ }
+ }
+
+ var retentionSettings = saveCommonRetentionSettings(
+ gMsgFolder.retentionSettings
+ );
+ retentionSettings.useServerDefaults = document.getElementById(
+ "retention.useDefault"
+ ).checked;
+ gMsgFolder.retentionSettings = retentionSettings;
+
+ let color = document.getElementById("color").value;
+ if (color == gDefaultColor) {
+ color = undefined;
+ }
+ FolderTreeProperties.setColor(gMsgFolder.URI, color);
+ // Tell 3-pane tabs to update the folder's color.
+ Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color);
+ }
+
+ try {
+ // This throws an exception when an illegal folder name was entered.
+ top.okCallback(
+ document.getElementById("name").value,
+ window.arguments[0].name
+ );
+ } catch (e) {
+ event.preventDefault();
+ }
+}
+
+function folderCancelButton(event) {
+ // Clear any previewed color.
+ Services.obs.notifyObservers(gMsgFolder, "folder-color-preview");
+}
+
+function folderPropsOnLoad() {
+ let styles = getComputedStyle(document.body);
+ let folderColors = {
+ Inbox: styles.getPropertyValue("--folder-color-inbox"),
+ Sent: styles.getPropertyValue("--folder-color-sent"),
+ Outbox: styles.getPropertyValue("--folder-color-outbox"),
+ Drafts: styles.getPropertyValue("--folder-color-draft"),
+ Trash: styles.getPropertyValue("--folder-color-trash"),
+ Archive: styles.getPropertyValue("--folder-color-archive"),
+ Templates: styles.getPropertyValue("--folder-color-template"),
+ Spam: styles.getPropertyValue("--folder-color-spam"),
+ Virtual: styles.getPropertyValue("--folder-color-folder-filter"),
+ RSS: styles.getPropertyValue("--folder-color-rss"),
+ Newsgroup: styles.getPropertyValue("--folder-color-newsletter"),
+ };
+ gDefaultColor = styles.getPropertyValue("--folder-color-folder");
+
+ // look in arguments[0] for parameters
+ if (window.arguments && window.arguments[0]) {
+ if (window.arguments[0].title) {
+ document.title = window.arguments[0].title;
+ }
+ if (window.arguments[0].okCallback) {
+ top.okCallback = window.arguments[0].okCallback;
+ }
+ }
+
+ if (window.arguments[0].folder) {
+ // Fill in folder name, based on what they selected in the folder pane.
+ gMsgFolder = window.arguments[0].folder;
+ }
+
+ if (window.arguments[0].name) {
+ // Initialize name textbox with the given name and remember this
+ // value so we can tell whether the folder needs to be renamed
+ // when the dialog is accepted.
+ var nameTextbox = document.getElementById("name");
+ nameTextbox.value = window.arguments[0].name;
+ }
+
+ const serverType = window.arguments[0].serverType;
+
+ // Do this first, because of gloda we may want to override some of the hidden
+ // statuses.
+ hideShowControls(serverType);
+
+ if (gMsgFolder) {
+ // We really need a functioning database, so we'll detect problems
+ // and create one if we have to.
+ try {
+ gMsgFolder.msgDatabase;
+ } catch (e) {
+ gMsgFolder.updateFolder(window.arguments[0].msgWindow);
+ }
+
+ // Check the current folder name against known folder names to set the
+ // correct default color, if needed.
+ let selectedFolderName = "";
+
+ switch (window.arguments[0].serverType) {
+ case "rss":
+ selectedFolderName = "RSS";
+ break;
+ case "nntp":
+ selectedFolderName = "Newsgroup";
+ break;
+ default:
+ selectedFolderName = window.arguments[0].name;
+ break;
+ }
+
+ if (Object.keys(folderColors).includes(selectedFolderName)) {
+ gDefaultColor = folderColors[selectedFolderName];
+ }
+
+ let colorInput = document.getElementById("color");
+ colorInput.value =
+ FolderTreeProperties.getColor(gMsgFolder.URI) || gDefaultColor;
+ colorInput.addEventListener("input", event => {
+ // Preview the chosen color.
+ Services.obs.notifyObservers(
+ gMsgFolder,
+ "folder-color-preview",
+ colorInput.value
+ );
+ });
+ let resetColorButton = document.getElementById("resetColor");
+ resetColorButton.addEventListener("click", function () {
+ colorInput.value = gDefaultColor;
+ // Preview the default color.
+ Services.obs.notifyObservers(
+ gMsgFolder,
+ "folder-color-preview",
+ gDefaultColor
+ );
+ });
+
+ var locationTextbox = document.getElementById("location");
+
+ // Decode the displayed mailbox:// URL as it's useful primarily for debugging,
+ // whereas imap and news urls are sent around.
+ locationTextbox.value =
+ serverType == "imap" || serverType == "nntp"
+ ? gMsgFolder.folderURL
+ : decodeURI(gMsgFolder.folderURL);
+
+ if (gMsgFolder.canRename) {
+ document.getElementById("name").removeAttribute("readonly");
+ }
+
+ if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Offline)) {
+ if (serverType == "imap" || serverType == "pop3") {
+ document.getElementById(
+ "offline.selectForOfflineFolder"
+ ).checked = true;
+ }
+
+ if (serverType == "nntp") {
+ document.getElementById(
+ "offline.selectForOfflineNewsgroup"
+ ).checked = true;
+ }
+ } else {
+ if (serverType == "imap" || serverType == "pop3") {
+ document.getElementById(
+ "offline.selectForOfflineFolder"
+ ).checked = false;
+ }
+
+ if (serverType == "nntp") {
+ document.getElementById(
+ "offline.selectForOfflineNewsgroup"
+ ).checked = false;
+ }
+ }
+
+ // set check for new mail checkbox
+ document.getElementById("folderCheckForNewMessages").checked =
+ gMsgFolder.getFlag(Ci.nsMsgFolderFlags.CheckNew);
+
+ // if gloda indexing is off, hide the related checkbox
+ var glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch");
+ var glodaEnabled = Services.prefs.getBoolPref(
+ "mailnews.database.global.indexer.enabled"
+ );
+ if (
+ !glodaEnabled ||
+ gMsgFolder.flags &
+ (Ci.nsMsgFolderFlags.Queue | Ci.nsMsgFolderFlags.Newsgroup)
+ ) {
+ glodaCheckbox.hidden = true;
+ } else {
+ // otherwise, the user can choose whether this file gets indexed
+ let glodaFolder = Gloda.getFolderForFolder(gMsgFolder);
+ glodaCheckbox.checked =
+ glodaFolder.indexingPriority != glodaFolder.kIndexingNeverPriority;
+ }
+ }
+
+ if (serverType == "imap") {
+ let imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ imapFolder.fillInFolderProps(gFolderPropsSink);
+
+ let users = [...imapFolder.getOtherUsersWithAccess()];
+ if (users.length) {
+ document.getElementById("folderOtherUsers").hidden = false;
+ document.getElementById("folderOtherUsersText").textContent =
+ users.join(", ");
+ }
+
+ // Disable "Repair Folder" when offline as that would cause the offline store
+ // to get deleted and redownloaded.
+ document.getElementById("folderRebuildSummaryButton").disabled =
+ gMsgFolder.supportsOffline && Services.io.offline;
+ }
+
+ var retentionSettings = gMsgFolder.retentionSettings;
+ initCommonRetentionSettings(retentionSettings);
+ document.getElementById("retention.useDefault").checked =
+ retentionSettings.useServerDefaults;
+
+ // set folder sizes
+ let numberOfMsgs = gMsgFolder.getTotalMessages(false);
+ if (numberOfMsgs >= 0) {
+ document.getElementById("numberOfMessages").value = numberOfMsgs;
+ }
+
+ try {
+ let sizeOnDisk = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger)
+ .formatFileSize(gMsgFolder.sizeOnDisk, true);
+ document.getElementById("sizeOnDisk").value = sizeOnDisk;
+ } catch (e) {}
+
+ onCheckKeepMsg();
+ onUseDefaultRetentionSettings();
+
+ // select the initial tab
+ if (window.arguments[0].tabID) {
+ document.getElementById("folderPropTabBox").selectedTab =
+ document.getElementById(window.arguments[0].tabID);
+ }
+}
+
+function hideShowControls(serverType) {
+ let controls = document.querySelectorAll("[hidefor]");
+ var len = controls.length;
+ for (var i = 0; i < len; i++) {
+ var control = controls[i];
+ var hideFor = control.getAttribute("hidefor");
+ if (!hideFor) {
+ throw new Error("hidefor empty");
+ }
+
+ // hide unsupported server type
+ // adding support for hiding multiple server types using hideFor="server1,server2"
+ var hideForBool = false;
+ var hideForTokens = hideFor.split(",");
+ for (var j = 0; j < hideForTokens.length; j++) {
+ if (hideForTokens[j] == serverType) {
+ hideForBool = true;
+ break;
+ }
+ }
+ control.hidden = hideForBool;
+ }
+
+ // hide the privileges button if the imap folder doesn't have an admin url
+ // maybe should leave this hidden by default and only show it in this case instead
+ try {
+ var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (imapFolder) {
+ var privilegesButton = document.getElementById("imap.FolderPrivileges");
+ if (privilegesButton) {
+ if (!imapFolder.hasAdminUrl) {
+ privilegesButton.setAttribute("hidden", "true");
+ }
+ }
+ }
+ } catch (ex) {}
+
+ if (gMsgFolder) {
+ // Hide "check for new mail" checkbox if this is an Inbox.
+ if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Inbox)) {
+ document.getElementById("folderCheckForNewMessages").hidden = true;
+ }
+ // Retention policy doesn't apply to Drafts/Templates/Outbox.
+ if (
+ gMsgFolder.isSpecialFolder(
+ Ci.nsMsgFolderFlags.Drafts |
+ Ci.nsMsgFolderFlags.Templates |
+ Ci.nsMsgFolderFlags.Queue,
+ true
+ )
+ ) {
+ document.getElementById("Retention").hidden = true;
+ }
+ }
+}
+
+function onOfflineFolderDownload() {
+ // we need to create a progress window and pass that in as the second parameter here.
+ gMsgFolder.downloadAllForOffline(null, window.arguments[0].msgWindow);
+}
+
+function onFolderPrivileges() {
+ var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (imapFolder) {
+ imapFolder.folderPrivileges(window.arguments[0].msgWindow);
+ }
+ // let's try closing the modal dialog to see if it fixes the various problems running this url
+ window.close();
+}
+
+function onUseDefaultRetentionSettings() {
+ var useDefault = document.getElementById("retention.useDefault").checked;
+ document.getElementById("retention.keepMsg").disabled = useDefault;
+ document.getElementById("retention.keepNewMsgMinLabel").disabled = useDefault;
+ document.getElementById("retention.keepOldMsgMinLabel").disabled = useDefault;
+
+ var keepMsg = document.getElementById("retention.keepMsg").value;
+ const nsIMsgRetentionSettings = Ci.nsIMsgRetentionSettings;
+ document.getElementById("retention.keepOldMsgMin").disabled =
+ useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByAge;
+ document.getElementById("retention.keepNewMsgMin").disabled =
+ useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByNumHeaders;
+}
+
+function RebuildSummaryInformation() {
+ window.arguments[0].rebuildSummaryCallback();
+}
diff --git a/comm/mailnews/base/content/folderProps.xhtml b/comm/mailnews/base/content/folderProps.xhtml
new file mode 100644
index 0000000000..2c0376cc2a
--- /dev/null
+++ b/comm/mailnews/base/content/folderProps.xhtml
@@ -0,0 +1,338 @@
+<?xml version="1.0"?>
+<!-- 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 https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderProps.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/folderProps.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&folderProps.windowtitle.label;</title>
+ <link rel="localization" href="messenger/folderprops.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/retention.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/folderProps.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="folderPropertiesDialog">
+ <tabbox id="folderPropTabBox">
+ <tabs id="folderPropTabs">
+ <tab id="GeneralTab" label="&generalInfo.label;" />
+ <tab id="Retention" label="&retention.label;" />
+ <tab
+ id="SynchronizationTab"
+ hidefor="pop3,rss,none"
+ label="&folderSynchronizationTab.label;"
+ />
+ <tab
+ id="SharingTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderSharingTab.label;"
+ />
+ <tab
+ id="QuotaTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderQuotaTab.label;"
+ />
+ </tabs>
+ <tabpanels id="folderPropTabPanels">
+ <vbox id="GeneralPanel">
+ <hbox id="nameBox" align="center" class="input-container">
+ <label
+ id="nameLabel"
+ value="&folderProps.name.label;"
+ control="name"
+ accesskey="&folderProps.name.accesskey;"
+ />
+ <hbox align="center" class="input-container">
+ <html:input
+ id="name"
+ type="text"
+ readonly="readonly"
+ oninput="doEnabling();"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ />
+ <label
+ id="colorLabel"
+ value="&folderProps.color.label;"
+ control="color"
+ accesskey="&folderProps.color.accesskey;"
+ />
+ <html:input
+ id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"
+ />
+ <button
+ id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"
+ />
+ </hbox>
+ <label
+ id="locationLabel"
+ value="&folderProps.location.label;"
+ control="location"
+ accesskey="&folderProps.location.accesskey;"
+ />
+ <html:input
+ id="location"
+ type="text"
+ readonly="readonly"
+ class="uri-element input-inline"
+ aria-labelledby="locationLabel"
+ />
+ </hbox>
+ <vbox>
+ <spacer height="2" />
+ <hbox align="center">
+ <label value="&numberOfMessages.label;" />
+ <label id="numberOfMessages" value="&numberUnknown.label;" />
+ <spacer flex="1" />
+ <label value="&sizeOnDisk.label;" />
+ <label id="sizeOnDisk" value="&sizeUnknown.label;" />
+ </hbox>
+ <spacer height="2" />
+ </vbox>
+ <checkbox
+ id="folderIncludeInGlobalSearch"
+ hidefor="nntp"
+ label="&folderIncludeInGlobalSearch.label;"
+ accesskey="&folderIncludeInGlobalSearch.accesskey;"
+ />
+ <checkbox
+ hidefor="pop3,none,nntp"
+ id="folderCheckForNewMessages"
+ label="&folderCheckForNewMessages2.label;"
+ accesskey="&folderCheckForNewMessages2.accesskey;"
+ />
+ <separator class="thin" />
+ <hbox>
+ <description id="folderRebuildSummaryExplanation" flex="1">
+ &folderRebuildSummaryFile.explanation;
+ </description>
+ <vbox>
+ <button
+ id="folderRebuildSummaryButton"
+ label="&folderRebuildSummaryFile2.label;"
+ oncommand="RebuildSummaryInformation();"
+ accesskey="&folderRebuildSummaryFile2.accesskey;"
+ tooltiptext="&folderRebuildSummaryFileTip2.label;"
+ align="center"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+
+ <vbox id="RetentionPanel" align="start">
+ <description hidefor="imap,pop3" class="desc"
+ >&retentionCleanup.label;</description
+ >
+ <description hidefor="pop3,rss,none,nntp" class="desc"
+ >&retentionCleanupImap.label;</description
+ >
+ <description hidefor="imap,rss,none,nntp" class="desc"
+ >&retentionCleanupPop.label;</description
+ >
+
+ <hbox align="center" class="indent">
+ <checkbox
+ wsm_persist="true"
+ id="retention.useDefault"
+ accesskey="&retentionUseAccount.accesskey;"
+ label="&retentionUseAccount.label;"
+ checked="true"
+ oncommand="onUseDefaultRetentionSettings()"
+ />
+ </hbox>
+ <vbox class="indent">
+ <hbox class="indent">
+ <radiogroup
+ wsm_persist="true"
+ id="retention.keepMsg"
+ aria-labelledby="retention.useDefault"
+ >
+ <radio
+ wsm_persist="true"
+ value="1"
+ accesskey="&retentionKeepAll.accesskey;"
+ label="&retentionKeepAll.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepNewMsg"
+ accesskey="&retentionKeepRecent.accesskey;"
+ value="3"
+ label="&retentionKeepRecent.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepNewMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="2000"
+ wsm_persist="true"
+ aria-labelledby="keepNewMsg retention.keepNewMsgMin retention.keepNewMsgMinLabel"
+ />
+ <label
+ value="&message.label;"
+ control="retention.keepNewMsgMin"
+ id="retention.keepNewMsgMinLabel"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepMsg"
+ accesskey="&retentionDeleteMsg.accesskey;"
+ value="2"
+ label="&retentionDeleteMsg.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepOldMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="30"
+ wsm_persist="true"
+ aria-labelledby="keepMsg retention.keepOldMsgMin retention.keepOldMsgMinLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="retention.keepOldMsgMin"
+ id="retention.keepOldMsgMinLabel"
+ />
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <hbox class="indent">
+ <checkbox
+ id="retention.applyToFlagged"
+ wsm_persist="true"
+ label="&retentionApplyToFlagged.label;"
+ accesskey="&retentionApplyToFlagged.accesskey;"
+ observes="retention.keepMsg"
+ checked="true"
+ />
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <vbox id="SyncPanel" align="start">
+ <vbox>
+ <checkbox
+ hidefor="nntp"
+ wsm_persist="true"
+ id="offline.selectForOfflineFolder"
+ label="&offlineFolder.check.label;"
+ accesskey="&offlineFolder.check.accesskey;"
+ />
+ <checkbox
+ hidefor="imap"
+ wsm_persist="true"
+ id="offline.selectForOfflineNewsgroup"
+ label="&selectofflineNewsgroup.check.label;"
+ accesskey="&selectofflineNewsgroup.check.accesskey;"
+ />
+ </vbox>
+ <button
+ hidefor="nntp"
+ label="&offlineFolder.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineFolder.button.accesskey;"
+ id="offline.offlineFolderDownloadButton"
+ orient="right"
+ />
+ <button
+ hidefor="imap"
+ label="&offlineNewsgroup.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineNewsgroup.button.accesskey;"
+ id="offline.offlineNewsgroupDownloadButton"
+ orient="right"
+ />
+ </vbox>
+
+ <vbox id="SharingPanel">
+ <hbox align="start">
+ <label value="&folderType.label;" id="folderTypeLabel" />
+ <label value="" id="folderType.text" />
+ </hbox>
+ <vbox align="start">
+ <label value="" id="folderDescription.text" />
+ <label value=" " />
+ <label
+ value="&permissionsDesc.label;"
+ id="permissionsDescLabel"
+ />
+
+ <description id="folderPermissions.text"></description>
+ </vbox>
+ <vbox id="folderOtherUsers" align="start" hidden="true">
+ <label value=" " />
+ <label value="&folderOtherUsers.label;" />
+ <description id="folderOtherUsersText"></description>
+ </vbox>
+ <spacer flex="1" />
+ <vbox align="start">
+ <button
+ hidefor="pop3,none,rss,nntp"
+ label="&privileges.button.label;"
+ oncommand="onFolderPrivileges();"
+ accesskey="&privileges.button.accesskey;"
+ id="imap.FolderPrivileges"
+ orient="right"
+ />
+ </vbox>
+ </vbox>
+
+ <vbox id="quotaPanel">
+ <label id="folderQuotaStatus" flex="1" />
+ <hbox id="folderQuotaData" hidden="true">
+ <html:ul id="quotaDetails"></html:ul>
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/jsTreeView.js b/comm/mailnews/base/content/jsTreeView.js
new file mode 100644
index 0000000000..704fd2475f
--- /dev/null
+++ b/comm/mailnews/base/content/jsTreeView.js
@@ -0,0 +1,239 @@
+/* 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/. */
+
+/* exported PROTO_TREE_VIEW */
+
+/**
+ * This file contains a prototype object designed to make the implementation of
+ * nsITreeViews in javascript simpler. This object requires that consumers
+ * override the _rebuild function. This function must set the _rowMap object to
+ * an array of objects fitting the following interface:
+ *
+ * readonly attribute string id - a unique identifier for the row/object
+ * readonly attribute integer level - the hierarchy level of the row
+ * attribute boolean open - whether or not this item's children are exposed
+ * string getText(aColName) - return the text to display for this row in the
+ * specified column
+ * string getProperties() - return the css-selectors
+ * attribute array children - return an array of child-objects also meeting this
+ * interface
+ */
+
+function PROTO_TREE_VIEW() {
+ this._tree = null;
+ this._rowMap = [];
+ this._persistOpenMap = [];
+}
+
+PROTO_TREE_VIEW.prototype = {
+ get rowCount() {
+ return this._rowMap.length;
+ },
+
+ /**
+ * CSS files will cue off of these. Note that we reach into the rowMap's
+ * items so that custom data-displays can define their own properties
+ */
+ getCellProperties(aRow, aCol) {
+ return this._rowMap[aRow].getProperties(aCol);
+ },
+
+ /**
+ * The actual text to display in the tree
+ */
+ getCellText(aRow, aCol) {
+ return this._rowMap[aRow].getText(aCol.id);
+ },
+
+ getCellValue(aRow, aCol) {
+ return this._rowMap[aRow].getValue(aCol.id);
+ },
+
+ /**
+ * The jstv items take care of assigning this when building children lists
+ */
+ getLevel(aIndex) {
+ return this._rowMap[aIndex].level;
+ },
+
+ /**
+ * This is easy since the jstv items assigned the _parent property when making
+ * the child lists
+ */
+ getParentIndex(aIndex) {
+ return this._rowMap.indexOf(this._rowMap[aIndex]._parent);
+ },
+
+ /**
+ * This is duplicative for our normal jstv views, but custom data-displays may
+ * want to do something special here
+ */
+ getRowProperties(aRow) {
+ return this._rowMap[aRow].getProperties();
+ },
+
+ /**
+ * If an item in our list has the same level and parent as us, it's a sibling
+ */
+ hasNextSibling(aIndex, aNextIndex) {
+ let targetLevel = this._rowMap[aIndex].level;
+ for (let i = aNextIndex + 1; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].level == targetLevel) {
+ return true;
+ }
+ if (this._rowMap[i].level < targetLevel) {
+ return false;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * If we have a child-list with at least one element, we are a container.
+ */
+ isContainer(aIndex) {
+ return this._rowMap[aIndex].children.length > 0;
+ },
+
+ isContainerEmpty(aIndex) {
+ // If the container has no children, the container is empty.
+ return !this._rowMap[aIndex].children.length;
+ },
+
+ /**
+ * Just look at the jstv item here
+ */
+ isContainerOpen(aIndex) {
+ return this._rowMap[aIndex].open;
+ },
+
+ isEditable(aRow, aCol) {
+ // We don't support editing rows in the tree yet.
+ return false;
+ },
+
+ isSeparator(aIndex) {
+ // There are no separators in our trees
+ return false;
+ },
+
+ isSorted() {
+ // We do our own customized sorting
+ return false;
+ },
+
+ setTree(aTree) {
+ this._tree = aTree;
+ },
+
+ recursivelyAddToMap(aChild, aNewIndex) {
+ // When we add sub-children, we're going to need to increase our index
+ // for the next add item at our own level.
+ let currentCount = this._rowMap.length;
+ if (aChild.children.length && aChild.open) {
+ for (let [i, child] of this._rowMap[aNewIndex].children.entries()) {
+ let index = aNewIndex + i + 1;
+ this._rowMap.splice(index, 0, child);
+ aNewIndex += this.recursivelyAddToMap(child, index);
+ }
+ }
+ return this._rowMap.length - currentCount;
+ },
+
+ /**
+ * Opens or closes a container with children. The logic here is a bit hairy, so
+ * be very careful about changing anything.
+ */
+ toggleOpenState(aIndex) {
+ // Ok, this is a bit tricky.
+ this._rowMap[aIndex]._open = !this._rowMap[aIndex].open;
+
+ if (!this._rowMap[aIndex].open) {
+ // We're closing the current container. Remove the children
+
+ // Note that we can't simply splice out children.length, because some of
+ // them might have children too. Find out how many items we're actually
+ // going to splice
+ let level = this._rowMap[aIndex].level;
+ let row = aIndex + 1;
+ while (row < this._rowMap.length && this._rowMap[row].level > level) {
+ row++;
+ }
+ let count = row - aIndex - 1;
+ this._rowMap.splice(aIndex + 1, count);
+
+ // Remove us from the persist map
+ let index = this._persistOpenMap.indexOf(this._rowMap[aIndex].id);
+ if (index != -1) {
+ this._persistOpenMap.splice(index, 1);
+ }
+
+ // Notify the tree of changes
+ if (this._tree) {
+ this._tree.rowCountChanged(aIndex + 1, -count);
+ }
+ } else {
+ // We're opening the container. Add the children to our map
+
+ // Note that these children may have been open when we were last closed,
+ // and if they are, we also have to add those grandchildren to the map
+ let oldCount = this._rowMap.length;
+ this.recursivelyAddToMap(this._rowMap[aIndex], aIndex);
+
+ // Add this container to the persist map
+ let id = this._rowMap[aIndex].id;
+ if (!this._persistOpenMap.includes(id)) {
+ this._persistOpenMap.push(id);
+ }
+
+ // Notify the tree of changes
+ if (this._tree) {
+ this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount);
+ }
+ }
+
+ // Invalidate the toggled row, so that the open/closed marker changes
+ if (this._tree) {
+ this._tree.invalidateRow(aIndex);
+ }
+ },
+
+ // We don't implement any of these at the moment
+ canDrop(aIndex, aOrientation) {},
+ drop(aRow, aOrientation) {},
+ selectionChanged() {},
+ setCellText(aRow, aCol, aValue) {},
+ setCellValue(aRow, aCol, aValue) {},
+ getColumnProperties(aCol) {
+ return "";
+ },
+ getImageSrc(aRow, aCol) {},
+ getProgressMode(aRow, aCol) {},
+ cycleCell(aRow, aCol) {},
+ cycleHeader(aCol) {},
+
+ _tree: null,
+
+ /**
+ * An array of jstv items, where each item corresponds to a row in the tree
+ */
+ _rowMap: null,
+
+ /**
+ * This is a javascript map of which containers we had open, so that we can
+ * persist their state over-time. It is designed to be used as a JSON object.
+ */
+ _persistOpenMap: null,
+
+ _restoreOpenStates() {
+ // Note that as we iterate through here, .length may grow
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._persistOpenMap.includes(this._rowMap[i].id)) {
+ this.toggleOpenState(i);
+ }
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+};
diff --git a/comm/mailnews/base/content/junkCommands.js b/comm/mailnews/base/content/junkCommands.js
new file mode 100644
index 0000000000..1554d54256
--- /dev/null
+++ b/comm/mailnews/base/content/junkCommands.js
@@ -0,0 +1,449 @@
+/* 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/. */
+
+/**
+ * Functions use for junk processing commands
+ */
+
+/*
+ * TODO: These functions make the false assumption that a view only contains
+ * a single folder. This is not true for XF saved searches.
+ *
+ * globals prerequisites used:
+ *
+ * top.window.MsgStatusFeedback
+ */
+
+/* globals gDBView, gViewWrapper */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+);
+
+/**
+ * Determines the actions that should be carried out on the messages
+ * that are being marked as junk
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages being marked as junk.
+ * @returns {object} result an object with two properties.
+ * @returns {boolean} result.markRead - Whether the messages should be marked
+ * as read.
+ * @returns {?nsIMsgFolder} result.junkTargetFolder - Where the messages should
+ * be moved, or null if they should not be moved.
+ */
+function determineActionsForJunkMsgs(aFolder) {
+ var actions = { markRead: false, junkTargetFolder: null };
+ var spamSettings = aFolder.server.spamSettings;
+
+ // note we will do moves/marking as read even if the spam
+ // feature is disabled, since the user has asked to use it
+ // despite the disabling
+
+ actions.markRead = spamSettings.markAsReadOnSpam;
+ actions.junkTargetFolder = null;
+
+ // move only when the corresponding setting is activated
+ // and the currently viewed folder is not the junk folder.
+ if (spamSettings.moveOnSpam && !aFolder.getFlag(Ci.nsMsgFolderFlags.Junk)) {
+ var spamFolderURI = spamSettings.spamFolderURI;
+ if (!spamFolderURI) {
+ // XXX TODO
+ // we should use nsIPromptService to inform the user of the problem,
+ // e.g. when the junk folder was accidentally deleted.
+ dump("determineActionsForJunkMsgs: no spam folder found, not moving.");
+ } else {
+ actions.junkTargetFolder = MailUtils.getOrCreateFolder(spamFolderURI);
+ }
+ }
+
+ return actions;
+}
+
+/**
+ * Performs required operations on a list of newly-classified junk messages.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages being marked as
+ * junk.
+ * @param {nsIMsgDBHdr[]} aJunkMsgHdrs - New junk messages.
+ * @param {nsIMsgDBHdr[]} aGoodMsgHdrs - New good messages.
+ */
+async function performActionsOnJunkMsgs(aFolder, aJunkMsgHdrs, aGoodMsgHdrs) {
+ return new Promise((resolve, reject) => {
+ if (aFolder instanceof Ci.nsIMsgImapMailFolder) {
+ // need to update IMAP custom flags
+ if (aJunkMsgHdrs.length) {
+ let junkMsgKeys = aJunkMsgHdrs.map(hdr => hdr.messageKey);
+ aFolder.storeCustomKeywords(null, "Junk", "NonJunk", junkMsgKeys);
+ }
+
+ if (aGoodMsgHdrs.length) {
+ let goodMsgKeys = aGoodMsgHdrs.map(hdr => hdr.messageKey);
+ aFolder.storeCustomKeywords(null, "NonJunk", "Junk", goodMsgKeys);
+ }
+ }
+ if (!aJunkMsgHdrs.length) {
+ resolve();
+ return;
+ }
+
+ let actionParams = determineActionsForJunkMsgs(aFolder);
+ if (actionParams.markRead) {
+ aFolder.markMessagesRead(aJunkMsgHdrs, true);
+ }
+
+ if (!actionParams.junkTargetFolder) {
+ resolve();
+ return;
+ }
+
+ // @implements {nsIMsgCopyServiceListener}
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
+ OnStartCopy() {},
+ OnProgress(progress, progressMax) {},
+ SetMessageKey(key) {},
+ GetMessageId() {},
+ OnStopCopy(status) {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ return;
+ }
+ let uri = actionParams.junkTargetFolder.URI;
+ reject(new Error(`Moving junk to ${uri} failed.`));
+ },
+ };
+ MailServices.copy.copyMessages(
+ aFolder,
+ aJunkMsgHdrs,
+ actionParams.junkTargetFolder,
+ true /* isMove */,
+ listener,
+ top.msgWindow,
+ true /* allow undo */
+ );
+ });
+}
+
+/**
+ * Helper object storing the list of pending messages to process,
+ * and implementing junk processing callback.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages to be analyzed for junk.
+ * @param {integer} aTotalMessages - Number of messages to process, used for
+ * progress report only.
+ */
+
+function MessageClassifier(aFolder, aTotalMessages) {
+ this.mFolder = aFolder;
+ this.mJunkMsgHdrs = [];
+ this.mGoodMsgHdrs = [];
+ this.mMessages = {};
+ this.mMessageQueue = [];
+ this.mTotalMessages = aTotalMessages;
+ this.mProcessedMessages = 0;
+ this.firstMessage = true;
+ this.lastStatusTime = Date.now();
+}
+
+/**
+ * @implements {nsIJunkMailClassificationListener}
+ */
+MessageClassifier.prototype = {
+ /**
+ * Starts the message classification process for a message. If the message
+ * sender's address is whitelisted, the message is skipped.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The header of the message to classify.
+ * @param {nsISpamSettings} aSpamSettings - The object with information about
+ * whitelists
+ */
+ analyzeMessage(aMsgHdr, aSpamSettings) {
+ var junkscoreorigin = aMsgHdr.getStringProperty("junkscoreorigin");
+ if (junkscoreorigin == "user") {
+ // don't override user-set junk status
+ return;
+ }
+
+ // check whitelisting
+ if (aSpamSettings.checkWhiteList(aMsgHdr)) {
+ // message is ham from whitelist
+ var db = aMsgHdr.folder.msgDatabase;
+ db.setStringProperty(
+ aMsgHdr.messageKey,
+ "junkscore",
+ Ci.nsIJunkMailPlugin.IS_HAM_SCORE
+ );
+ db.setStringProperty(aMsgHdr.messageKey, "junkscoreorigin", "whitelist");
+ this.mGoodMsgHdrs.push(aMsgHdr);
+ return;
+ }
+
+ let messageURI = aMsgHdr.folder.generateMessageURI(aMsgHdr.messageKey);
+ this.mMessages[messageURI] = aMsgHdr;
+ if (this.firstMessage) {
+ this.firstMessage = false;
+ MailServices.junk.classifyMessage(messageURI, top.msgWindow, this);
+ } else {
+ this.mMessageQueue.push(messageURI);
+ }
+ },
+
+ /**
+ * Callback function from nsIJunkMailPlugin with classification results.
+ *
+ * @param {string} aClassifiedMsgURI - URI of classified message.
+ * @param {integer} aClassification - Junk classification (0: UNCLASSIFIED, 1: GOOD, 2: JUNK)
+ * @param {integer} aJunkPercent - 0 - 100 indicator of junk likelihood,
+ * with 100 meaning probably junk.
+ * @see {nsIJunkMailClassificationListener}
+ */
+ async onMessageClassified(aClassifiedMsgURI, aClassification, aJunkPercent) {
+ if (!aClassifiedMsgURI) {
+ // Ignore end of batch.
+ return;
+ }
+ var score =
+ aClassification == Ci.nsIJunkMailPlugin.JUNK
+ ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE
+ : Ci.nsIJunkMailPlugin.IS_HAM_SCORE;
+ const statusDisplayInterval = 1000; // milliseconds between status updates
+
+ // set these props via the db (instead of the message header
+ // directly) so that the nsMsgDBView knows to update the UI
+ //
+ var msgHdr = this.mMessages[aClassifiedMsgURI];
+ var db = msgHdr.folder.msgDatabase;
+ db.setStringProperty(msgHdr.messageKey, "junkscore", score);
+ db.setStringProperty(msgHdr.messageKey, "junkscoreorigin", "plugin");
+ db.setStringProperty(msgHdr.messageKey, "junkpercent", aJunkPercent);
+
+ if (aClassification == Ci.nsIJunkMailPlugin.JUNK) {
+ this.mJunkMsgHdrs.push(msgHdr);
+ } else if (aClassification == Ci.nsIJunkMailPlugin.GOOD) {
+ this.mGoodMsgHdrs.push(msgHdr);
+ }
+
+ var nextMsgURI = this.mMessageQueue.shift();
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+
+ if (nextMsgURI) {
+ ++this.mProcessedMessages;
+ if (Date.now() > this.lastStatusTime + statusDisplayInterval) {
+ this.lastStatusTime = Date.now();
+ var percentDone = 0;
+ if (this.mTotalMessages) {
+ percentDone = Math.round(
+ (this.mProcessedMessages * 100) / this.mTotalMessages
+ );
+ }
+ top.window.MsgStatusFeedback.showStatusString(
+ bundle.formatStringFromName("junkAnalysisPercentComplete", [
+ percentDone + "%",
+ ])
+ );
+ }
+ MailServices.junk.classifyMessage(nextMsgURI, top.msgWindow, this);
+ } else {
+ top.window.MsgStatusFeedback.showStatusString(
+ bundle.GetStringFromName("processingJunkMessages")
+ );
+ await performActionsOnJunkMsgs(
+ this.mFolder,
+ this.mJunkMsgHdrs,
+ this.mGoodMsgHdrs
+ );
+ setTimeout(() => {
+ top.window.MsgStatusFeedback.showStatusString("");
+ }, 500);
+ }
+ },
+};
+
+/**
+ * Filter all messages in the current folder for junk
+ */
+async function filterFolderForJunk() {
+ await processFolderForJunk(true);
+}
+
+/**
+ * Filter selected messages in the current folder for junk
+ */
+async function analyzeMessagesForJunk() {
+ await processFolderForJunk(false);
+}
+
+/**
+ * Filter messages in the current folder for junk
+ *
+ * @param {boolean} aAll - true to filter all messages, else filter selection.
+ */
+async function processFolderForJunk(aAll) {
+ let indices;
+ if (aAll) {
+ // need to expand all threads, so we analyze everything
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var count = treeView.rowCount;
+ if (!count) {
+ return;
+ }
+ } else {
+ indices =
+ AppConstants.MOZ_APP_NAME == "seamonkey"
+ ? window.GetSelectedIndices(gDBView)
+ : window.threadTree?.selectedIndices;
+ if (!indices || !indices.length) {
+ return;
+ }
+ }
+ let totalMessages = aAll ? count : indices.length;
+
+ // retrieve server and its spam settings via the header of an arbitrary message
+ let tmpMsgURI;
+ for (let i = 0; i < totalMessages; i++) {
+ let index = aAll ? i : indices[i];
+ try {
+ tmpMsgURI = gDBView.getURIForViewIndex(index);
+ break;
+ } catch (e) {
+ // dummy headers will fail, so look for another
+ continue;
+ }
+ }
+ if (!tmpMsgURI) {
+ return;
+ }
+
+ let tmpMsgHdr =
+ MailServices.messageServiceFromURI(tmpMsgURI).messageURIToMsgHdr(tmpMsgURI);
+ let spamSettings = tmpMsgHdr.folder.server.spamSettings;
+
+ // create a classifier instance to classify messages in the folder.
+ let msgClassifier = new MessageClassifier(tmpMsgHdr.folder, totalMessages);
+
+ for (let i = 0; i < totalMessages; i++) {
+ let index = aAll ? i : indices[i];
+ try {
+ let msgURI = gDBView.getURIForViewIndex(index);
+ let msgHdr =
+ MailServices.messageServiceFromURI(msgURI).messageURIToMsgHdr(msgURI);
+ msgClassifier.analyzeMessage(msgHdr, spamSettings);
+ } catch (ex) {
+ // blow off errors here - dummy headers will fail
+ }
+ }
+ if (msgClassifier.firstMessage) {
+ // the async plugin was not used, maybe all whitelisted?
+ await performActionsOnJunkMsgs(
+ msgClassifier.mFolder,
+ msgClassifier.mJunkMsgHdrs,
+ msgClassifier.mGoodMsgHdrs
+ );
+ }
+}
+
+/**
+ * Delete junk messages in the current folder. This provides the guarantee that
+ * the method will be synchronous if no messages are deleted.
+ *
+ * @returns {integer} The number of messages deleted.
+ */
+function deleteJunkInFolder() {
+ // use direct folder commands if possible so we don't mess with the selection
+ let selectedFolder = gViewWrapper.displayedFolder;
+ if (!selectedFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ let junkMsgHdrs = [];
+ for (let msgHdr of gDBView.msgFolder.messages) {
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ if (junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE) {
+ junkMsgHdrs.push(msgHdr);
+ }
+ }
+
+ if (junkMsgHdrs.length) {
+ gDBView.msgFolder.deleteMessages(
+ junkMsgHdrs,
+ top.msgWindow,
+ false,
+ false,
+ null,
+ true
+ );
+ }
+ return junkMsgHdrs.length;
+ }
+
+ // Folder is virtual, let the view do the work (but we lose selection)
+
+ // need to expand all threads, so we find everything
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var count = treeView.rowCount;
+ if (!count) {
+ return 0;
+ }
+
+ var treeSelection = treeView.selection;
+
+ var clearedSelection = false;
+
+ // select the junk messages
+ var messageUri;
+ let numMessagesDeleted = 0;
+ for (let i = 0; i < count; ++i) {
+ try {
+ messageUri = gDBView.getURIForViewIndex(i);
+ } catch (ex) {
+ continue; // blow off errors for dummy rows
+ }
+ let msgHdr =
+ MailServices.messageServiceFromURI(messageUri).messageURIToMsgHdr(
+ messageUri
+ );
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ var isJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE;
+ // if the message is junk, select it.
+ if (isJunk) {
+ // only do this once
+ if (!clearedSelection) {
+ // clear the current selection
+ // since we will be deleting all selected messages
+ treeSelection.clearSelection();
+ clearedSelection = true;
+ treeSelection.selectEventsSuppressed = true;
+ }
+ treeSelection.rangedSelect(i, i, true /* augment */);
+ numMessagesDeleted++;
+ }
+ }
+
+ // if we didn't clear the selection
+ // there was no junk, so bail.
+ if (!clearedSelection) {
+ return 0;
+ }
+
+ treeSelection.selectEventsSuppressed = false;
+ // delete the selected messages
+ //
+ // We'll leave no selection after the delete
+ if ("gNextMessageViewIndexAfterDelete" in window) {
+ window.gNextMessageViewIndexAfterDelete = 0xffffffff; // nsMsgViewIndex_None
+ }
+ gDBView.doCommand(Ci.nsMsgViewCommandType.deleteMsg);
+ treeSelection.clearSelection();
+ return numMessagesDeleted;
+}
diff --git a/comm/mailnews/base/content/junkLog.js b/comm/mailnews/base/content/junkLog.js
new file mode 100644
index 0000000000..7f2b73edf7
--- /dev/null
+++ b/comm/mailnews/base/content/junkLog.js
@@ -0,0 +1,48 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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 { MailE10SUtils } = ChromeUtils.import(
+ "resource:///modules/MailE10SUtils.jsm"
+);
+
+var gLogView;
+var gLogFile;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+function onLoad() {
+ gLogView = document.getElementById("logView");
+ gLogView.browsingContext.allowJavascript = false; // for security, disable JS
+
+ gLogView.addEventListener("load", () => {
+ addStyling();
+ });
+
+ gLogFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ gLogFile.append("junklog.html");
+ if (gLogFile.exists()) {
+ MailE10SUtils.loadURI(gLogView, Services.io.newFileURI(gLogFile).spec);
+ } else {
+ addStyling(); // set style for initial about:blank
+ }
+}
+
+function clearLog() {
+ if (gLogFile.exists()) {
+ gLogFile.remove(false);
+ gLogView.setAttribute("src", "about:blank"); // we don't have a log file to show
+ }
+}
+
+function addStyling() {
+ let style = gLogView.contentDocument.createElement("style");
+ gLogView.contentDocument.head.appendChild(style);
+ style.sheet.insertRule(
+ `@media (prefers-color-scheme: dark) {
+ :root { scrollbar-color: rgba(249, 249, 250, .4) rgba(20, 20, 25, .3);}
+ body { color: #f9f9fa; }
+ }`
+ );
+}
diff --git a/comm/mailnews/base/content/junkLog.xhtml b/comm/mailnews/base/content/junkLog.xhtml
new file mode 100644
index 0000000000..28c7071c4e
--- /dev/null
+++ b/comm/mailnews/base/content/junkLog.xhtml
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/junkLog.dtd">
+
+<html
+ id="viewLogWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="screenX screenY width height"
+ width="600"
+ height="375"
+ scrolling="false"
+>
+ <head>
+ <title>&adaptiveJunkLog.title;</title>
+ <script defer="defer" src="chrome://messenger/content/junkLog.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ >
+ <vbox flex="1">
+ <hbox>
+ <label value="&adaptiveJunkLogInfo.label;" />
+ <spacer flex="1" />
+ <button
+ label="&clearLog.label;"
+ accesskey="&clearLog.accesskey;"
+ oncommand="clearLog();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox flex="1">
+ <browser
+ id="logView"
+ type="content"
+ disablehistory="true"
+ disablesecurity="true"
+ src="about:blank"
+ autofind="false"
+ flex="1"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/markByDate.js b/comm/mailnews/base/content/markByDate.js
new file mode 100644
index 0000000000..672897f1e1
--- /dev/null
+++ b/comm/mailnews/base/content/markByDate.js
@@ -0,0 +1,120 @@
+/* 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/. */
+
+/* import-globals-from dateFormat.js */
+
+var MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
+var MICROSECONDS_PER_DAY = 1000 * MILLISECONDS_PER_HOUR * 24;
+
+window.addEventListener("load", onLoad);
+
+document.addEventListener("dialogaccept", onAccept);
+
+function onLoad() {
+ var upperDateBox = document.getElementById("upperDate");
+ // focus the upper bound control - this is where we expect most users to enter
+ // a date
+ upperDateBox.focus();
+
+ // and give it an initial date - "yesterday"
+ var initialDate = new Date();
+ initialDate.setHours(0);
+ initialDate.setTime(initialDate.getTime() - MILLISECONDS_PER_HOUR);
+ // note that this is sufficient - though it is at the end of the previous day,
+ // we convert it to a date string, and then the time part is truncated
+ upperDateBox.value = convertDateToString(initialDate);
+ upperDateBox.select(); // allows to start overwriting immediately
+}
+
+function onAccept() {
+ // get the times as entered by the user
+ var lowerDateString = document.getElementById("lowerDate").value;
+ // the fallback for the lower bound, if not entered, is the "beginning of
+ // time" (1970-01-01), which actually is simply 0 :)
+ var prLower = lowerDateString ? convertStringToPRTime(lowerDateString) : 0;
+
+ var upperDateString = document.getElementById("upperDate").value;
+ var prUpper;
+ if (upperDateString == "") {
+ // for the upper bound, the fallback is "today".
+ var dateThisMorning = new Date();
+ dateThisMorning.setMilliseconds(0);
+ dateThisMorning.setSeconds(0);
+ dateThisMorning.setMinutes(0);
+ dateThisMorning.setHours(0);
+ // Javascript time is in milliseconds, PRTime is in microseconds
+ prUpper = dateThisMorning.getTime() * 1000;
+ } else {
+ prUpper = convertStringToPRTime(upperDateString);
+ }
+
+ // for the upper date, we have to do a correction:
+ // if the user enters a date, then she means (hopefully) that all messages sent
+ // at this day should be marked, too, but the PRTime calculated from this would
+ // point to the beginning of the day. So we need to increment it by
+ // [number of micro seconds per day]. This will denote the first microsecond of
+ // the next day then, which is later used as exclusive boundary
+ prUpper += MICROSECONDS_PER_DAY;
+
+ markInDatabase(prLower, prUpper);
+}
+
+/**
+ * M arks all headers in the database, whose time is between the two given
+ * times, as read.
+ *
+ * @param {integer} lower - PRTime for the lower bound (inclusive).
+ * @param {integer} upper - PRTime for the upper bound (exclusive).
+ */
+function markInDatabase(lower, upper) {
+ let messageFolder;
+ let messageDatabase;
+ // extract the database
+ if (window.arguments && window.arguments[0]) {
+ messageFolder = window.arguments[0];
+ messageDatabase = messageFolder.msgDatabase;
+ }
+
+ if (!messageDatabase) {
+ dump("markByDate::markInDatabase: there /is/ no database to operate on!\n");
+ return;
+ }
+
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerms = [];
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, messageFolder);
+
+ const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ const nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+ let searchTerm = searchSession.createTerm();
+ searchTerm.attrib = nsMsgSearchAttrib.Date;
+ searchTerm.op = nsMsgSearchOp.IsBefore;
+ let value = searchTerm.value;
+ value.attrib = nsMsgSearchAttrib.Date;
+ value.date = upper;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+
+ if (lower) {
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = nsMsgSearchAttrib.Date;
+ searchTerm.op = nsMsgSearchOp.IsAfter;
+ value = searchTerm.value;
+ value.attrib = nsMsgSearchAttrib.Date;
+ value.date = lower;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+ }
+
+ let msgEnumerator = messageDatabase.getFilterEnumerator(searchTerms);
+ let headers = [...msgEnumerator];
+
+ if (headers.length) {
+ messageFolder.markMessagesRead(headers, true);
+ }
+}
diff --git a/comm/mailnews/base/content/markByDate.xhtml b/comm/mailnews/base/content/markByDate.xhtml
new file mode 100644
index 0000000000..7928bf2b59
--- /dev/null
+++ b/comm/mailnews/base/content/markByDate.xhtml
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/markByDate.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&messageMarkByDate.label;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dateFormat.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/markByDate.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel">
+ <hbox align="center" pack="end">
+ <label
+ id="lowerDateLabel"
+ control="lowerDate"
+ value="&markByDateLower.label;"
+ accesskey="&markByDateLower.accesskey;"
+ />
+ <html:input
+ id="lowerDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="lowerDateLabel"
+ size="11"
+ />
+ </hbox>
+ <hbox align="center" pack="end">
+ <label
+ id="upperDateLabel"
+ control="upperDate"
+ value="&markByDateUpper.label;"
+ accesskey="&markByDateUpper.accesskey;"
+ />
+ <html:input
+ id="upperDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="upperDateLabel"
+ size="11"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/menulist-charsetpicker.js b/comm/mailnews/base/content/menulist-charsetpicker.js
new file mode 100644
index 0000000000..eb354621a7
--- /dev/null
+++ b/comm/mailnews/base/content/menulist-charsetpicker.js
@@ -0,0 +1,86 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+// The menulist CE is defined lazily. Create one now to get menulist defined,
+// allowing us to inherit from it.
+if (!customElements.get("menulist")) {
+ delete document.createXULElement("menulist");
+}
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ /**
+ * MozMenulistCharsetpicker is a menulist widget that is automatically
+ * populated with charset selections.
+ *
+ * @augments {MozMenuList}
+ */
+ class MozMenulistCharsetpickerViewing extends customElements.get("menulist") {
+ /**
+ * Get the charset values to show in the list.
+ *
+ * @abstract
+ * @returns {string[]} an array of character encoding names
+ */
+ get charsetValues() {
+ return [
+ "UTF-8",
+ "Big5",
+ "EUC-KR",
+ "gbk",
+ "KOI8-R",
+ "ISO-2022-JP",
+ "ISO-8859-1",
+ "ISO-8859-2",
+ "ISO-8859-7",
+ "windows-874",
+ "windows-1250",
+ "windows-1251",
+ "windows-1252",
+ "windows-1255",
+ "windows-1256",
+ "windows-1257",
+ "windows-1258",
+ ];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ if (this.menupopup) {
+ return;
+ }
+
+ let charsetBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/charsetTitles.properties"
+ );
+ this.charsetValues
+ .map(item => {
+ let strCharset = charsetBundle.GetStringFromName(
+ item.toLowerCase() + ".title"
+ );
+ return { label: strCharset, value: item };
+ })
+ .sort((a, b) => {
+ if (a.value == "UTF-8" || a.label < b.label) {
+ return -1;
+ } else if (b.value == "UTF-8" || a.label > b.label) {
+ return 1;
+ }
+ return 0;
+ })
+ .forEach(item => {
+ this.appendItem(item.label, item.value);
+ });
+ }
+ }
+ customElements.define(
+ "menulist-charsetpicker-viewing",
+ MozMenulistCharsetpickerViewing,
+ { extends: "menulist" }
+ );
+}
diff --git a/comm/mailnews/base/content/msgAccountCentral.js b/comm/mailnews/base/content/msgAccountCentral.js
new file mode 100644
index 0000000000..7574c19da3
--- /dev/null
+++ b/comm/mailnews/base/content/msgAccountCentral.js
@@ -0,0 +1,238 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm");
+var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm");
+
+var gSelectedServer = null;
+var gSelectedFolder = null;
+
+window.addEventListener("DOMContentLoaded", OnInit);
+
+/**
+ * Set up the whole page depending on the selected folder/account.
+ * The folder is passed in via the document URL.
+ */
+function OnInit() {
+ let el = document.getElementById("setupTitle");
+
+ document.l10n.setAttributes(el, "setup-title", {
+ accounts: MailServices.accounts.accounts.length,
+ });
+
+ // Selected folder URI is passed as folderURI argument in the query string.
+ let folderURI = decodeURIComponent(
+ document.location.search.replace("?folderURI=", "")
+ );
+ gSelectedFolder = folderURI ? MailUtils.getExistingFolder(folderURI) : null;
+ gSelectedServer = gSelectedFolder ? gSelectedFolder.server : null;
+
+ if (gSelectedServer) {
+ // Display and collapse items presented to the user based on account type
+ updateAccountCentralUI();
+ } else {
+ // If there is no gSelectedServer, we are in a brand new profile.
+ document.getElementById("headerFirstRun").hidden = false;
+ document.getElementById("headerExistingAccounts").hidden = true;
+ document.getElementById("version").textContent = Services.appinfo.version;
+
+ // Update the style of the account setup buttons and area.
+ let accountSection = document.getElementById("accountSetupSection");
+ for (let btn of accountSection.querySelectorAll(".btn-hub")) {
+ btn.classList.remove("btn-inline");
+ }
+ accountSection.classList.remove("zebra");
+
+ document.getElementById("accountFeaturesSection").hidden = true;
+ }
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+}
+
+/**
+ * Show items in the AccountCentral page depending on the capabilities
+ * of the given server.
+ */
+function updateAccountCentralUI() {
+ // Set the account name.
+ document.getElementById("accountName").textContent =
+ gSelectedServer.prettyName;
+
+ // Update the account logo.
+ document
+ .getElementById("accountLogo")
+ .setAttribute("type", gSelectedServer.type);
+
+ let exceptions = [];
+ let protocolInfo = null;
+ try {
+ protocolInfo = gSelectedServer.protocolInfo;
+ } catch (e) {
+ exceptions.push(e);
+ }
+
+ // Is this a RSS account?
+ let isRssAccount = gSelectedServer?.type == "rss";
+
+ // Is this an NNTP account?
+ let isNNTPAccount = gSelectedServer?.type == "nntp";
+
+ // Is this a Local Folders account?
+ const isLocalFoldersAccount = gSelectedServer?.type == "none";
+
+ document
+ .getElementById("readButton")
+ .toggleAttribute("hidden", !getReadMessagesFolder());
+
+ // It can compose messages.
+ let showComposeMsgLink = false;
+ try {
+ showComposeMsgLink = protocolInfo && protocolInfo.showComposeMsgLink;
+ document
+ .getElementById("composeButton")
+ .toggleAttribute("hidden", !showComposeMsgLink);
+ } catch (e) {
+ exceptions.push(e);
+ }
+
+ // It can subscribe to a newsgroup.
+ document
+ .getElementById("nntpSubscriptionButton")
+ .toggleAttribute("hidden", !isNNTPAccount);
+
+ // It can subscribe to an RSS feed.
+ document
+ .getElementById("rssSubscriptionButton")
+ .toggleAttribute("hidden", !isRssAccount);
+
+ // It can search messages.
+ let canSearchMessages = false;
+ try {
+ canSearchMessages = gSelectedServer && gSelectedServer.canSearchMessages;
+ document
+ .getElementById("searchButton")
+ .toggleAttribute("hidden", !canSearchMessages);
+ } catch (e) {
+ exceptions.push(e);
+ }
+
+ // It can create filters.
+ let canHaveFilters = false;
+ try {
+ canHaveFilters = gSelectedServer && gSelectedServer.canHaveFilters;
+ document
+ .getElementById("filterButton")
+ .toggleAttribute("hidden", !canHaveFilters);
+ } catch (e) {
+ exceptions.push(e);
+ }
+
+ // It can have End-to-end Encryption.
+ document
+ .getElementById("e2eButton")
+ .toggleAttribute(
+ "hidden",
+ isNNTPAccount || isRssAccount || isLocalFoldersAccount
+ );
+
+ // Check if we collected any exception.
+ while (exceptions.length) {
+ console.error(
+ "Error in setting AccountCentral Items: " + exceptions.pop() + "\n"
+ );
+ }
+}
+
+/**
+ * For the selected server, check for new messges and display first
+ * suitable folder (genrally Inbox) for reading.
+ */
+function readMessages() {
+ const folder = getReadMessagesFolder();
+ top.MsgGetMessage([folder]);
+ parent.displayFolder(folder);
+}
+
+/**
+ * Find the folder Read Messages should use.
+ *
+ * @returns {?nsIMsgFolder} folder to use, if we have a suitable one.
+ */
+function getReadMessagesFolder() {
+ const folder = MailUtils.getInboxFolder(gSelectedServer);
+ if (folder) {
+ return folder;
+ }
+ // For feeds and nntp, show the first non-trash folder. Don't use Outbox.
+ return gSelectedServer.rootFolder.descendants.find(
+ f =>
+ !(f.flags & Ci.nsMsgFolderFlags.Trash) &&
+ !(f.flags & Ci.nsMsgFolderFlags.Queue)
+ );
+}
+
+/**
+ * Open the AccountManager to view the settings for a given account.
+ *
+ * @param {string} selectPage - The xhtml file name for the viewing page,
+ * null for the account main page, other pages are 'am-server.xhtml',
+ * 'am-copies.xhtml', 'am-offline.xhtml', 'am-addressing.xhtml',
+ * 'am-smtp.xhtml'
+ */
+function viewSettings(selectPage) {
+ window.browsingContext.topChromeWindow.MsgAccountManager(
+ selectPage,
+ gSelectedServer
+ );
+}
+
+/**
+ * Bring up the search interface for selected account.
+ */
+function searchMessages() {
+ top.document
+ .getElementById("tabmail")
+ .currentAbout3Pane.commandController.doCommand("cmd_searchMessages");
+}
+
+/**
+ * Open the filters window.
+ */
+function createMsgFilters() {
+ window.browsingContext.topChromeWindow.MsgFilters(null, gSelectedFolder);
+}
+
+/**
+ * Open the subscribe dialog.
+ */
+function subscribe() {
+ if (!gSelectedServer) {
+ return;
+ }
+ if (gSelectedServer.type == "rss") {
+ window.browsingContext.topChromeWindow.openSubscriptionsDialog(
+ gSelectedServer.rootFolder
+ );
+ } else {
+ window.browsingContext.topChromeWindow.MsgSubscribe(gSelectedFolder);
+ }
+}
+
+/**
+ * Open the target's url on an external browser.
+ *
+ * @param {Event} event - The keypress or click event.
+ */
+function openLink(event) {
+ event.preventDefault();
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(Services.io.newURI(event.target.href));
+}
diff --git a/comm/mailnews/base/content/msgAccountCentral.xhtml b/comm/mailnews/base/content/msgAccountCentral.xhtml
new file mode 100644
index 0000000000..843840c088
--- /dev/null
+++ b/comm/mailnews/base/content/msgAccountCentral.xhtml
@@ -0,0 +1,309 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountCentral.css" type="text/css"?>
+
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ windowtype="mail:accountcentral"
+ lightweightthemes="true"
+>
+ <head>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountCentral.ftl" />
+
+ <script
+ defer="defer"
+ src="chrome://communicator/content/utilityOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgAccountCentral.js"
+ ></script>
+ </head>
+
+ <body>
+ <main id="accountCentral">
+ <header
+ id="headerFirstRun"
+ class="account-central-header"
+ hidden="hidden"
+ >
+ <img
+ id="brandLogo"
+ src="chrome://branding/content/about-logo.svg"
+ alt=""
+ />
+ <aside>
+ <h1
+ class="account-central-title"
+ data-l10n-id="account-central-title"
+ ></h1>
+ <div class="account-central-version">
+ <span id="version"></span>
+ <a
+ id="releasenotes"
+ data-l10n-id="release-notes"
+ onclick="openAboutDialog();"
+ onkeypress="if (event.key == 'Enter') { openAboutDialog(); }"
+ >
+ <img
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ alt=""
+ />
+ </a>
+ </div>
+ </aside>
+ </header>
+
+ <header
+ id="headerExistingAccounts"
+ class="account-central-header summary-header"
+ >
+ <span id="accountLogo"></span>
+ <aside>
+ <h1 id="accountName"></h1>
+ </aside>
+ <aside class="settings-btn-container">
+ <button
+ id="settingsButton"
+ type="button"
+ data-l10n-id="account-settings"
+ class="btn-link"
+ onclick="viewSettings(null);"
+ ></button>
+ </aside>
+ </header>
+
+ <section id="accountFeaturesSection" class="account-central-section">
+ <aside class="row-container account-options">
+ <button
+ id="readButton"
+ type="button"
+ data-l10n-id="read"
+ class="btn-link btn-inline"
+ onclick="readMessages();"
+ ></button>
+ <button
+ id="nntpSubscriptionButton"
+ type="button"
+ data-l10n-id="nntp-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="rssSubscriptionButton"
+ type="button"
+ data-l10n-id="rss-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="composeButton"
+ type="button"
+ data-l10n-id="compose"
+ class="btn-link btn-inline"
+ onclick="window.browsingContext.topChromeWindow.MsgNewMessage(event);"
+ hidden="hidden"
+ ></button>
+ <button
+ id="searchButton"
+ type="button"
+ data-l10n-id="search"
+ class="btn-link btn-inline"
+ onclick="searchMessages();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="filterButton"
+ type="button"
+ data-l10n-id="filter"
+ class="btn-link btn-inline"
+ onclick="createMsgFilters();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="e2eButton"
+ type="button"
+ data-l10n-id="e2e"
+ class="btn-link btn-inline"
+ onclick="viewSettings('am-e2e.xhtml');"
+ hidden="hidden"
+ ></button>
+ </aside>
+ </section>
+
+ <section
+ id="accountSetupSection"
+ class="account-central-section setup-section zebra"
+ >
+ <h2 id="setupTitle" class="section-title"></h2>
+
+ <aside class="row-container">
+ <button
+ id="setupEmail"
+ type="button"
+ data-l10n-id="email-label"
+ class="btn-hub btn-inline"
+ onclick="top.openAccountSetupTab();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="email-description"></p>
+ </div>
+
+ <button
+ id="setupAddressBook"
+ type="button"
+ data-l10n-id="addressbook-label"
+ class="btn-hub btn-inline"
+ onclick="top.addNewAddressBook('CARDDAV');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="addressbook-description"></p>
+ </div>
+
+ <button
+ id="setupCalendar"
+ type="button"
+ data-l10n-id="calendar-label"
+ class="btn-hub btn-inline"
+ onclick="window.browsingContext.topChromeWindow.cal.window.openCalendarWizard(window);"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="calendar-description"></p>
+ </div>
+
+ <button
+ id="setupChat"
+ type="button"
+ data-l10n-id="chat-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddIMAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="chat-description"></p>
+ </div>
+
+ <button
+ id="setupFilelink"
+ type="button"
+ data-l10n-id="filelink-label"
+ class="btn-hub btn-inline"
+ onclick="top.openOptionsDialog('paneCompose', 'compositionAttachmentsCategory');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="filelink-description"></p>
+ </div>
+
+ <button
+ id="setupFeeds"
+ type="button"
+ data-l10n-id="feeds-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddFeedAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="feeds-description"></p>
+ </div>
+
+ <button
+ id="setupNewsgroups"
+ type="button"
+ data-l10n-id="newsgroups-label"
+ class="btn-hub btn-inline"
+ onclick="top.openNewsgroupAccountWizard();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="newsgroups-description"></p>
+ </div>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="import-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="import-paragraph2"></p>
+ </aside>
+ <aside class="row-container">
+ <button
+ id="importButton"
+ type="button"
+ data-l10n-id="import-label"
+ class="btn-hub btn-inline"
+ onclick="top.toImport();"
+ ></button>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="about-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="about-paragraph"></p>
+ <p
+ id="donationParagraph"
+ data-l10n-id="about-paragraph-consider-donation"
+ >
+ <a
+ href="https://give.thunderbird.net/en-US/?utm_source=account_hub_tb_release&amp;utm_medium=referral&amp;utm_content=paragraph_text"
+ class="donation-link"
+ data-l10n-name="donation-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </p>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="resources-title"></h2>
+
+ <aside class="row-container">
+ <a
+ id="featuresLink"
+ href="https://www.thunderbird.net/en-US/features/"
+ class="resource-link"
+ data-l10n-id="explore-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="supportLink"
+ href="https://support.mozilla.org/products/thunderbird"
+ class="resource-link"
+ data-l10n-id="support-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="involvedLink"
+ href="https://www.thunderbird.net/en-US/get-involved/"
+ class="resource-link"
+ data-l10n-id="involved-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="developerLink"
+ href="https://developer.thunderbird.net/"
+ class="resource-link"
+ data-l10n-id="developer-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </aside>
+ </section>
+ </main>
+ </body>
+</html>
diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.js b/comm/mailnews/base/content/msgSelectOfflineFolders.js
new file mode 100644
index 0000000000..18f304bae7
--- /dev/null
+++ b/comm/mailnews/base/content/msgSelectOfflineFolders.js
@@ -0,0 +1,189 @@
+/* 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/. */
+
+/* globals PROTO_TREE_VIEW */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FolderUtils: "resource:///modules/FolderUtils.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+var gFolderTreeView = new PROTO_TREE_VIEW();
+
+var gSelectOffline = {
+ _treeElement: null,
+ _rollbackMap: new Map(),
+
+ load() {
+ for (let account of FolderUtils.allAccountsSorted(true)) {
+ let server = account.incomingServer;
+ if (
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount
+ ) {
+ continue;
+ }
+ if (!server.rootFolder.supportsOffline) {
+ continue;
+ }
+
+ gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder));
+ }
+
+ this._treeElement = document.getElementById("synchronizeTree");
+ // TODO: Expand relevant rows.
+ this._treeElement.view = gFolderTreeView;
+ },
+
+ onKeyPress(aEvent) {
+ // For now, only do something on space key.
+ if (aEvent.charCode != aEvent.DOM_VK_SPACE) {
+ return;
+ }
+
+ let selection = this._treeElement.view.selection;
+ let start = {};
+ let end = {};
+ let numRanges = selection.getRangeCount();
+
+ for (let range = 0; range < numRanges; range++) {
+ selection.getRangeAt(range, start, end);
+ for (let i = start.value; i <= end.value; i++) {
+ this._toggle(i);
+ }
+ }
+ },
+
+ onClick(aEvent) {
+ // We only care about button 0 (left click) events.
+ if (aEvent.button != 0) {
+ return;
+ }
+
+ // We don't want to toggle when clicking on header or tree (scrollbar) or
+ // on treecol.
+ if (aEvent.target.nodeName != "treechildren") {
+ return;
+ }
+
+ let treeCellInfo = this._treeElement.getCellAt(
+ aEvent.clientX,
+ aEvent.clientY
+ );
+
+ if (treeCellInfo.row == -1 || treeCellInfo.col.id != "syncCol") {
+ return;
+ }
+
+ this._toggle(treeCellInfo.row);
+ },
+
+ _toggle(aRow) {
+ let folder = gFolderTreeView._rowMap[aRow]._folder;
+
+ if (folder.isServer) {
+ return;
+ }
+
+ // Save our current state for rollback, if necessary.
+ if (!this._rollbackMap.has(folder)) {
+ this._rollbackMap.set(
+ folder,
+ folder.getFlag(Ci.nsMsgFolderFlags.Offline)
+ );
+ }
+
+ folder.toggleFlag(Ci.nsMsgFolderFlags.Offline);
+ gFolderTreeView._tree.invalidateRow(aRow);
+ },
+
+ onCancel() {
+ for (let [folder, value] of this._rollbackMap) {
+ if (value != folder.getFlag(Ci.nsMsgFolderFlags.Offline)) {
+ folder.toggleFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+ },
+};
+
+window.addEventListener("load", () => gSelectOffline.load());
+document.addEventListener("dialogcancel", () => gSelectOffline.onCancel());
+
+/**
+ * A tree row representing a single folder.
+ */
+class FolderRow {
+ constructor(folder, parent = null) {
+ this._folder = folder;
+ this._open = false;
+ this._level = parent ? parent.level + 1 : 0;
+ this._parent = parent;
+ this._children = null;
+ }
+
+ get id() {
+ return this._folder.URI;
+ }
+
+ get text() {
+ return this.getText("folderNameCol");
+ }
+
+ getText(aColName) {
+ switch (aColName) {
+ case "folderNameCol":
+ return this._folder.abbreviatedName;
+ default:
+ return "";
+ }
+ }
+
+ get open() {
+ return this._open;
+ }
+
+ get level() {
+ return this._level;
+ }
+
+ getProperties(column) {
+ let properties = "";
+ switch (column?.id) {
+ case "folderNameCol":
+ // From folderUtils.jsm.
+ properties = FolderUtils.getFolderProperties(this._folder, this.open);
+ break;
+ case "syncCol":
+ if (this._folder.isServer) {
+ return "isServer-true";
+ }
+ properties = "syncCol";
+ if (this._folder.getFlag(Ci.nsMsgFolderFlags.Offline)) {
+ properties += " synchronize-true";
+ }
+ break;
+ }
+ return properties;
+ }
+
+ get children() {
+ if (this._children === null) {
+ this._children = [];
+ for (let subFolder of this._folder.subFolders) {
+ if (subFolder.supportsOffline) {
+ this._children.push(new FolderRow(subFolder, this));
+ }
+ }
+ this._children.sort((a, b) => a._folder.compareSortKeys(b._folder));
+ }
+ return this._children;
+ }
+}
diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml
new file mode 100644
index 0000000000..88e0ecbd39
--- /dev/null
+++ b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/msgSelectOffline.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+
+<html
+ id="selectOffline"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:selectOffline"
+ lightweightthemes="true"
+ width="450"
+ height="400"
+ persist="width height screenX screenY"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSelect.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSelectOfflineFolders.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+
+ <label class="desc" control="synchronizeTree"
+ >&MsgSelectDesc.label;</label
+ >
+
+ <tree
+ id="synchronizeTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="offline"
+ onkeypress="gSelectOffline.onKeyPress(event);"
+ onclick="gSelectOffline.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ persist="hidden width"
+ label="&MsgSelectItems.label;"
+ primary="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="syncCol"
+ style="flex: 1 auto"
+ persist="hidden width"
+ label="&MsgSelectInd.label;"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/msgSynchronize.js b/comm/mailnews/base/content/msgSynchronize.js
new file mode 100644
index 0000000000..4393a2d301
--- /dev/null
+++ b/comm/mailnews/base/content/msgSynchronize.js
@@ -0,0 +1,192 @@
+/* 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 { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gSynchronizeTree = null;
+var gParentMsgWindow;
+var gMsgWindow;
+
+var gInitialFolderStates = {};
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogaccept", syncOkButton);
+
+function onLoad() {
+ gParentMsgWindow = window.arguments?.[0]?.msgWindow;
+
+ document.getElementById("syncMail").checked = Services.prefs.getBoolPref(
+ "mailnews.offline_sync_mail"
+ );
+ document.getElementById("syncNews").checked = Services.prefs.getBoolPref(
+ "mailnews.offline_sync_news"
+ );
+ document.getElementById("sendMessage").checked = Services.prefs.getBoolPref(
+ "mailnews.offline_sync_send_unsent"
+ );
+ document.getElementById("workOffline").checked = Services.prefs.getBoolPref(
+ "mailnews.offline_sync_work_offline"
+ );
+}
+
+function syncOkButton() {
+ var syncMail = document.getElementById("syncMail").checked;
+ var syncNews = document.getElementById("syncNews").checked;
+ var sendMessage = document.getElementById("sendMessage").checked;
+ var workOffline = document.getElementById("workOffline").checked;
+
+ Services.prefs.setBoolPref("mailnews.offline_sync_mail", syncMail);
+ Services.prefs.setBoolPref("mailnews.offline_sync_news", syncNews);
+ Services.prefs.setBoolPref("mailnews.offline_sync_send_unsent", sendMessage);
+ Services.prefs.setBoolPref("mailnews.offline_sync_work_offline", workOffline);
+
+ if (syncMail || syncNews || sendMessage || workOffline) {
+ var offlineManager = Cc[
+ "@mozilla.org/messenger/offline-manager;1"
+ ].getService(Ci.nsIMsgOfflineManager);
+ if (offlineManager) {
+ offlineManager.synchronizeForOffline(
+ syncNews,
+ syncMail,
+ sendMessage,
+ workOffline,
+ gParentMsgWindow
+ );
+ }
+ }
+}
+
+function OnSelect() {
+ top.window.openDialog(
+ "chrome://messenger/content/msgSelectOfflineFolders.xhtml",
+ "",
+ "centerscreen,chrome,modal,titlebar,resizable=yes"
+ );
+ return true;
+}
+
+// All the code below is only used by Seamonkey.
+
+function selectOkButton() {
+ return true;
+}
+
+function selectCancelButton() {
+ for (var resourceValue in gInitialFolderStates) {
+ let folder = MailUtils.getExistingFolder(resourceValue);
+ if (gInitialFolderStates[resourceValue]) {
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+ return true;
+}
+
+function SortSynchronizePane(column, sortKey) {
+ var node = FindInWindow(window, column);
+ if (!node) {
+ dump("Couldn't find sort column\n");
+ return;
+ }
+
+ node.setAttribute("sort", sortKey);
+ node.setAttribute("sortDirection", "natural");
+ var col = gSynchronizeTree.columns[column];
+ gSynchronizeTree.view.cycleHeader(col);
+}
+
+function FindInWindow(currentWindow, id) {
+ var item = currentWindow.document.getElementById(id);
+ if (item) {
+ return item;
+ }
+
+ for (var i = 0; i < currentWindow.frames.length; i++) {
+ var frameItem = FindInWindow(currentWindow.frames[i], id);
+ if (frameItem) {
+ return frameItem;
+ }
+ }
+
+ return null;
+}
+
+function onSynchronizeClick(event) {
+ // we only care about button 0 (left click) events
+ if (event.button != 0) {
+ return;
+ }
+
+ let treeCellInfo = gSynchronizeTree.getCellAt(event.clientX, event.clientY);
+ if (treeCellInfo.row == -1) {
+ return;
+ }
+
+ if (treeCellInfo.childElt == "twisty") {
+ var folderResource = GetFolderResource(gSynchronizeTree, treeCellInfo.row);
+ var folder = folderResource.QueryInterface(Ci.nsIMsgFolder);
+
+ if (!gSynchronizeTree.view.isContainerOpen(treeCellInfo.row)) {
+ var serverType = folder.server.type;
+ // imap is the only server type that does folder discovery
+ if (serverType != "imap") {
+ return;
+ }
+
+ if (folder.isServer) {
+ var server = folder.server;
+ server.performExpand(gMsgWindow);
+ } else {
+ var imapFolder = folderResource.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (imapFolder) {
+ imapFolder.performExpand(gMsgWindow);
+ }
+ }
+ }
+ } else if (treeCellInfo.col.id == "syncCol") {
+ UpdateNode(
+ GetFolderResource(gSynchronizeTree, treeCellInfo.row),
+ treeCellInfo.row
+ );
+ }
+}
+
+function onSynchronizeTreeKeyPress(event) {
+ // for now, only do something on space key
+ if (event.charCode != KeyEvent.DOM_VK_SPACE) {
+ return;
+ }
+
+ var treeSelection = gSynchronizeTree.view.selection;
+ for (let i = 0; i < treeSelection.getRangeCount(); i++) {
+ var start = {},
+ end = {};
+ treeSelection.getRangeAt(i, start, end);
+ for (let k = start.value; k <= end.value; k++) {
+ UpdateNode(GetFolderResource(gSynchronizeTree, k), k);
+ }
+ }
+}
+
+function UpdateNode(resource, row) {
+ var folder = resource.QueryInterface(Ci.nsIMsgFolder);
+
+ if (folder.isServer) {
+ return;
+ }
+
+ if (!(resource.Value in gInitialFolderStates)) {
+ gInitialFolderStates[resource.Value] = folder.getFlag(
+ Ci.nsMsgFolderFlags.Offline
+ );
+ }
+
+ folder.toggleFlag(Ci.nsMsgFolderFlags.Offline);
+}
+
+function GetFolderResource(aTree, aIndex) {
+ return aTree.view.getResourceAtIndex(aIndex);
+}
diff --git a/comm/mailnews/base/content/msgSynchronize.xhtml b/comm/mailnews/base/content/msgSynchronize.xhtml
new file mode 100644
index 0000000000..27df730cac
--- /dev/null
+++ b/comm/mailnews/base/content/msgSynchronize.xhtml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!-- 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 https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+<html
+ id="msgSynchronize"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:synchronizeOffline"
+ lightweightthemes="true"
+ style="height: 22em"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSynchronize.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSynchronize.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="msg-synchronizer" style="width: 63ch">
+ <html:p>&MsgSyncDesc.label;</html:p>
+ <html:p>&MsgSyncDirections.label;</html:p>
+
+ <vbox class="indent" align="start">
+ <checkbox
+ id="syncMail"
+ hidable="true"
+ hidefor="pop3"
+ label="&syncTypeMail.label;"
+ accesskey="&syncTypeMail.accesskey;"
+ />
+ <checkbox
+ id="syncNews"
+ label="&syncTypeNews.label;"
+ accesskey="&syncTypeNews.accesskey;"
+ />
+ </vbox>
+ <vbox align="start">
+ <checkbox
+ id="sendMessage"
+ label="&sendMessage.label;"
+ accesskey="&sendMessage.accesskey;"
+ />
+ <checkbox
+ id="workOffline"
+ label="&workOffline.label;"
+ accesskey="&workOffline.accesskey;"
+ />
+ </vbox>
+ <separator class="thin" />
+ <hbox pack="end">
+ <button
+ id="select"
+ label="&selectButton.label;"
+ accesskey="&selectButton.accesskey;"
+ oncommand="OnSelect();"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/newFolderDialog.js b/comm/mailnews/base/content/newFolderDialog.js
new file mode 100644
index 0000000000..2476f198b0
--- /dev/null
+++ b/comm/mailnews/base/content/newFolderDialog.js
@@ -0,0 +1,82 @@
+/* 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 FOLDERS = 1;
+var MESSAGES = 2;
+var dialog;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOK);
+
+function onLoad() {
+ var windowArgs = window.arguments[0];
+
+ dialog = {};
+
+ dialog.nameField = document.getElementById("name");
+ dialog.nameField.focus();
+
+ // call this when OK is pressed
+ dialog.okCallback = windowArgs.okCallback;
+
+ // pre select the folderPicker, based on what they selected in the folder pane
+ dialog.folder = windowArgs.folder;
+ try {
+ document
+ .getElementById("MsgNewFolderPopup")
+ .selectFolder(windowArgs.folder);
+ } catch (ex) {
+ // selected a child folder
+ document
+ .getElementById("msgNewFolderPicker")
+ .setAttribute("label", windowArgs.folder.prettyName);
+ }
+
+ // can folders contain both folders and messages?
+ if (windowArgs.dualUseFolders) {
+ dialog.folderType = FOLDERS | MESSAGES;
+
+ // hide the section when folder contain both folders and messages.
+ var newFolderTypeBox = document.getElementById("newFolderTypeBox");
+ newFolderTypeBox.setAttribute("hidden", "true");
+ } else {
+ // set our folder type by calling the default selected type's oncommand
+ document.getElementById("folderGroup").selectedItem.doCommand();
+ }
+
+ doEnabling();
+}
+
+function onFolderSelect(event) {
+ dialog.folder = event.target._folder;
+ document
+ .getElementById("msgNewFolderPicker")
+ .setAttribute("label", dialog.folder.prettyName);
+}
+
+function onOK() {
+ var name = dialog.nameField.value;
+
+ // do name validity check?
+
+ // make sure name ends in "/" if folder to create can only contain folders
+ if (dialog.folderType == FOLDERS && !name.endsWith("/")) {
+ dialog.okCallback(name + "/", dialog.folder);
+ } else {
+ dialog.okCallback(name, dialog.folder);
+ }
+}
+
+function onFoldersOnly() {
+ dialog.folderType = FOLDERS;
+}
+
+function onMessagesOnly() {
+ dialog.folderType = MESSAGES;
+}
+
+function doEnabling() {
+ document.querySelector("dialog").getButton("accept").disabled =
+ !dialog.nameField.value;
+}
diff --git a/comm/mailnews/base/content/newFolderDialog.xhtml b/comm/mailnews/base/content/newFolderDialog.xhtml
new file mode 100644
index 0000000000..bc141b95f3
--- /dev/null
+++ b/comm/mailnews/base/content/newFolderDialog.xhtml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % newFolderDTD SYSTEM "chrome://messenger/locale/newFolderDialog.dtd">
+%newFolderDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&newFolderDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <label
+ id="nameLabel"
+ value="&name.label;"
+ accesskey="&name.accesskey;"
+ control="name"
+ />
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+
+ <separator />
+
+ <label
+ value="&description.label;"
+ accesskey="&description.accesskey;"
+ control="msgNewFolderPicker"
+ />
+
+ <menulist
+ id="msgNewFolderPicker"
+ class="folderMenuItem"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="MsgNewFolderPopup"
+ showFileHereLabel="true"
+ class="menulist-menupopup"
+ mode="newFolder"
+ oncommand="onFolderSelect(event)"
+ />
+ </menulist>
+
+ <vbox id="newFolderTypeBox">
+ <separator class="thin" />
+
+ <label value="&folderRestriction1.label;" />
+ <label value="&folderRestriction2.label;" />
+
+ <separator class="thin" />
+
+ <radiogroup id="folderGroup" orient="horizontal" class="indent">
+ <radio oncommand="onFoldersOnly();" label="&foldersOnly.label;" />
+ <radio
+ oncommand="onMessagesOnly();"
+ label="&messagesOnly.label;"
+ selected="true"
+ />
+ </radiogroup>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/newmailalert.js b/comm/mailnews/base/content/newmailalert.js
new file mode 100644
index 0000000000..898c8d6724
--- /dev/null
+++ b/comm/mailnews/base/content/newmailalert.js
@@ -0,0 +1,109 @@
+/* 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 { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+var gAlertListener = null;
+
+// NOTE: We must wait until "load" instead of "DOMContentLoaded" because
+// otherwise the window height and width is not set in time for
+// window.moveTo.
+window.addEventListener("load", onAlertLoad);
+
+function prefillAlertInfo() {
+ // unwrap all the args....
+ // arguments[0] --> The real nsIMsgFolder with new mail.
+ // arguments[1] --> The keys of new messages.
+ // arguments[2] --> The nsIObserver to receive window closed event.
+ let [folder, newMsgKeys, listener] = window.arguments;
+ newMsgKeys = newMsgKeys.wrappedJSObject;
+ gAlertListener = listener.QueryInterface(Ci.nsIObserver);
+
+ // Generate an account label string based on the root folder.
+ var label = document.getElementById("alertTitle");
+ var totalNumNewMessages = newMsgKeys.length;
+ let message = document
+ .getElementById("bundle_messenger")
+ .getString("newMailAlert_message");
+ label.value = PluralForm.get(totalNumNewMessages, message)
+ .replace("#1", folder.server.rootFolder.prettyName)
+ .replace("#2", totalNumNewMessages);
+
+ // <folder-summary> handles rendering of new messages.
+ var folderSummaryInfoEl = document.getElementById("folderSummaryInfo");
+ folderSummaryInfoEl.maxMsgHdrsInPopup = 6;
+ folderSummaryInfoEl.render(folder, newMsgKeys);
+}
+
+function onAlertLoad() {
+ let dragSession = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession();
+ if (dragSession && dragSession.sourceNode) {
+ // If a drag session is active, adjusting this window's dimensions causes
+ // the drag session to be abruptly terminated. To avoid interrupting the
+ // user, wait until the drag is finished and then set up and show the alert.
+ dragSession.sourceNode.addEventListener("dragend", () => doOnAlertLoad());
+ } else {
+ doOnAlertLoad();
+ }
+}
+
+function doOnAlertLoad() {
+ prefillAlertInfo();
+
+ if (!document.getElementById("folderSummaryInfo").hasMessages()) {
+ closeAlert(); // no mail, so don't bother showing the alert...
+ return;
+ }
+
+ // resize the alert based on our current content
+ let alertTextBox = document.getElementById("alertTextBox");
+ let alertImageBox = document.getElementById("alertImageBox");
+ alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px";
+
+ // Show in bottom right, offset by 10px.
+ // We wait one cycle until the window has resized.
+ setTimeout(() => {
+ let x = screen.availLeft + screen.availWidth - window.outerWidth - 10;
+ let y = screen.availTop + screen.availHeight - window.outerHeight - 10;
+ window.moveTo(x, y);
+ });
+
+ let openTime = Services.prefs.getIntPref("alerts.totalOpenTime");
+ var alertContainer = document.getElementById("alertContainer");
+ // Don't fade in if the prefers-reduced-motion is true.
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+ alertContainer.setAttribute("noanimation", true);
+ setTimeout(closeAlert, openTime);
+ return;
+ }
+
+ alertContainer.addEventListener("animationend", function hideAlert(event) {
+ if (event.animationName == "fade-in") {
+ alertContainer.removeEventListener("animationend", hideAlert);
+ setTimeout(fadeOutAlert, openTime);
+ }
+ });
+
+ alertContainer.setAttribute("fade-in", true);
+}
+
+function fadeOutAlert() {
+ var alertContainer = document.getElementById("alertContainer");
+ alertContainer.addEventListener("animationend", function fadeOut(event) {
+ if (event.animationName == "fade-out") {
+ alertContainer.removeEventListener("animationend", fadeOut);
+ closeAlert();
+ }
+ });
+ alertContainer.setAttribute("fade-out", true);
+}
+
+function closeAlert() {
+ window.close();
+ gAlertListener.observe(null, "newmailalert-closed", "");
+}
diff --git a/comm/mailnews/base/content/newmailalert.xhtml b/comm/mailnews/base/content/newmailalert.xhtml
new file mode 100644
index 0000000000..1346be9c9a
--- /dev/null
+++ b/comm/mailnews/base/content/newmailalert.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/newmailalert.css" type="text/css"?>
+
+<html
+ id="newMailAlertNotification"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="alert:alert"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newmailalert.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ role="alert"
+ >
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox id="alertContainer" align="start">
+ <hbox id="alertImageBox" align="center" pack="center">
+ <html:img
+ id="alertImage"
+ alt=""
+ src="chrome://branding/content/icon64.png"
+ srcset="chrome://branding/content/icon128.png 2x"
+ />
+ </hbox>
+
+ <vbox id="alertTextBox">
+ <label id="alertTitle" />
+ <separator id="alertSeparator" />
+ <folder-summary id="folderSummaryInfo" />
+ </vbox>
+
+ <toolbarbutton
+ id="closeButton"
+ class="close-icon"
+ onclick="closeAlert();"
+ />
+ </hbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/newsError.js b/comm/mailnews/base/content/newsError.js
new file mode 100644
index 0000000000..693083a166
--- /dev/null
+++ b/comm/mailnews/base/content/newsError.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+// Error url must be formatted like this:
+// about:newserror?r=response&m=messageid&k=messagekey&f=folderuri
+// "r" is required; "m" and "f" are optional, but "k" always comes with "m".
+
+var folderUri;
+
+function initPage() {
+ let uri = document.documentURI;
+ let query = uri.slice(uri.indexOf("?") + 1);
+ let params = {};
+ for (let piece of query.split("&")) {
+ let [key, value] = piece.split("=");
+ params[key] = decodeURIComponent(value);
+ }
+
+ document.getElementById("ngResp").textContent = params.r;
+
+ if ("m" in params) {
+ document.getElementById("msgId").textContent = params.m;
+ document.getElementById("msgKey").textContent = params.k;
+ } else {
+ document.getElementById("messageIdDesc").hidden = true;
+ }
+
+ if ("f" in params) {
+ folderUri = params.f;
+ } else {
+ document.getElementById("errorTryAgain").hidden = true;
+ }
+}
+
+function removeExpired() {
+ document.location.href = folderUri + "?list-ids";
+}
+
+let errorTryAgain = document.getElementById("errorTryAgain");
+errorTryAgain.addEventListener("click", function () {
+ removeExpired();
+});
+
+// This must be called in this way,
+// see mozilla-central/docshell/resources/content/netError.js after which
+// this is modelled.
+initPage();
diff --git a/comm/mailnews/base/content/newsError.xhtml b/comm/mailnews/base/content/newsError.xhtml
new file mode 100644
index 0000000000..519d7c3897
--- /dev/null
+++ b/comm/mailnews/base/content/newsError.xhtml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html [ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+%htmlDTD;
+<!ENTITY % netErrorDTD
+ SYSTEM "chrome://messenger/locale/newsError.dtd">
+%netErrorDTD; ]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:" />
+ <title>&newsError.title;</title>
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/aboutNetError.css"
+ type="text/css"
+ media="all"
+ />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link
+ rel="icon"
+ type="image/png"
+ id="favicon"
+ href="chrome://global/skin/icons/warning.svg"
+ />
+ </head>
+ <body>
+ <div id="errorPageContainer">
+ <div id="errorTitle">
+ <h1 id="errorTitleText">&articleNotFound.title;</h1>
+ </div>
+ <div id="errorLongContent">
+ <div id="errorShortDesc">
+ <p id="errorShortDescText"><b>&articleNotFound.desc;</b></p>
+ </div>
+ <div id="errorLongDesc">
+ <ul>
+ <li>&serverResponded.title; <span id="ngResp" /></li>
+ <li>&articleExpired.title;</li>
+ <li id="messageIdDesc">
+ &trySearching.title; &lt;<span id="msgId" />&gt; (<span
+ id="msgKey"
+ />)
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- This button really means "remove all expired articles", but we use
+ the "errorTryAgain" id to piggyback on toolkit's CSS. -->
+ <button id="errorTryAgain">&removeExpiredArticles.title;</button>
+ </div>
+ <script src="chrome://messenger/content/newsError.js" />
+ </body>
+</html>
diff --git a/comm/mailnews/base/content/renameFolderDialog.js b/comm/mailnews/base/content/renameFolderDialog.js
new file mode 100644
index 0000000000..8525e0ba93
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.js
@@ -0,0 +1,43 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOK);
+
+var dialog;
+function onLoad() {
+ var windowArgs = window.arguments[0];
+
+ dialog = {};
+
+ dialog.OKButton = document.querySelector("dialog").getButton("accept");
+
+ dialog.nameField = document.getElementById("name");
+ dialog.nameField.value = windowArgs.name;
+ dialog.nameField.select();
+ dialog.nameField.focus();
+
+ // call this when OK is pressed
+ dialog.okCallback = windowArgs.okCallback;
+
+ // pre select the folderPicker, based on what they selected in the folder pane
+ dialog.preselectedFolderURI = windowArgs.preselectedURI;
+
+ doEnabling();
+}
+
+function onOK() {
+ dialog.okCallback(dialog.nameField.value, dialog.preselectedFolderURI);
+}
+
+function doEnabling() {
+ if (dialog.nameField.value) {
+ if (dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = false;
+ }
+ } else if (!dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = true;
+ }
+}
diff --git a/comm/mailnews/base/content/renameFolderDialog.xhtml b/comm/mailnews/base/content/renameFolderDialog.xhtml
new file mode 100644
index 0000000000..f81ba88db0
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: xml; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- -->
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/renameFolderDialog.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&renameFolderDialog.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/renameFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <html:label
+ id="nameLabel"
+ for="name"
+ style="display: inline-block"
+ accesskey="&rename.accesskey;"
+ >&rename.label;</html:label
+ >
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/retention.js b/comm/mailnews/base/content/retention.js
new file mode 100644
index 0000000000..ac40f173b3
--- /dev/null
+++ b/comm/mailnews/base/content/retention.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+/* globals gLockedPref */ // From either folderProps.js or am-offline.js.
+
+function initCommonRetentionSettings(retentionSettings) {
+ document.getElementById("retention.keepMsg").value =
+ retentionSettings.retainByPreference;
+ document.getElementById("retention.keepOldMsgMin").value =
+ retentionSettings.daysToKeepHdrs > 0
+ ? retentionSettings.daysToKeepHdrs
+ : 30;
+ document.getElementById("retention.keepNewMsgMin").value =
+ retentionSettings.numHeadersToKeep > 0
+ ? retentionSettings.numHeadersToKeep
+ : 2000;
+
+ document.getElementById("retention.applyToFlagged").checked =
+ !retentionSettings.applyToFlaggedMessages;
+}
+
+function saveCommonRetentionSettings(aRetentionSettings) {
+ aRetentionSettings.retainByPreference =
+ document.getElementById("retention.keepMsg").value;
+
+ aRetentionSettings.daysToKeepHdrs = document.getElementById(
+ "retention.keepOldMsgMin"
+ ).value;
+ aRetentionSettings.numHeadersToKeep = document.getElementById(
+ "retention.keepNewMsgMin"
+ ).value;
+
+ aRetentionSettings.applyToFlaggedMessages = !document.getElementById(
+ "retention.applyToFlagged"
+ ).checked;
+
+ return aRetentionSettings;
+}
+
+function onCheckKeepMsg() {
+ if (gLockedPref && gLockedPref["retention.keepMsg"]) {
+ // if the pref associated with the radiobutton is locked, as indicated
+ // by the gLockedPref, skip this function. All elements in this
+ // radiogroup have been locked by the function onLockPreference.
+ return;
+ }
+
+ var keepMsg = document.getElementById("retention.keepMsg").value;
+ document.getElementById("retention.keepOldMsgMin").disabled = keepMsg != 2;
+ document.getElementById("retention.keepNewMsgMin").disabled = keepMsg != 3;
+}
diff --git a/comm/mailnews/base/content/shutdownWindow.js b/comm/mailnews/base/content/shutdownWindow.js
new file mode 100644
index 0000000000..68c7f6b6f0
--- /dev/null
+++ b/comm/mailnews/base/content/shutdownWindow.js
@@ -0,0 +1,97 @@
+/* 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 curTaskIndex = 0;
+var numTasks = 0;
+var stringBundle;
+
+var msgShutdownService = Cc[
+ "@mozilla.org/messenger/msgshutdownservice;1"
+].getService(Ci.nsIMsgShutdownService);
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogcancel", onCancel);
+
+function onLoad() {
+ numTasks = msgShutdownService.getNumTasks();
+
+ stringBundle = document.getElementById("bundle_shutdown");
+ document.title = stringBundle.getString("shutdownDialogTitle");
+
+ updateTaskProgressLabel(1);
+ updateProgressMeter(0);
+
+ msgShutdownService.startShutdownTasks();
+}
+
+function updateProgressLabel(inTaskName) {
+ var curTaskLabel = document.getElementById("shutdownStatus_label");
+ curTaskLabel.value = inTaskName;
+}
+
+function updateTaskProgressLabel(inCurTaskNum) {
+ var taskProgressLabel = document.getElementById("shutdownTask_label");
+ taskProgressLabel.value = stringBundle.getFormattedString("taskProgress", [
+ inCurTaskNum,
+ numTasks,
+ ]);
+}
+
+function updateProgressMeter(inPercent) {
+ var taskProgressmeter = document.getElementById("shutdown_progressmeter");
+ taskProgressmeter.value = inPercent;
+}
+
+function onCancel() {
+ msgShutdownService.cancelShutdownTasks();
+}
+
+function nsMsgShutdownTaskListener() {
+ msgShutdownService.setShutdownListener(this);
+}
+
+nsMsgShutdownTaskListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ window.close();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ updateProgressMeter((aCurTotalProgress / aMaxTotalProgress) * 100);
+ updateTaskProgressLabel(aCurTotalProgress + 1);
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // we can ignore this notification
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (aMessage) {
+ updateProgressLabel(aMessage);
+ }
+ },
+
+ onSecurityChange(aWebProgress, aRequest, state) {
+ // we can ignore this notification
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ // we can ignore this notification
+ },
+};
+
+var MsgShutdownTaskListener = new nsMsgShutdownTaskListener();
diff --git a/comm/mailnews/base/content/shutdownWindow.xhtml b/comm/mailnews/base/content/shutdownWindow.xhtml
new file mode 100644
index 0000000000..32237dc44f
--- /dev/null
+++ b/comm/mailnews/base/content/shutdownWindow.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<!--
+# 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/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ width="600"
+ height="150"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/shutdownWindow.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="cancel">
+ <stringbundle
+ id="bundle_shutdown"
+ src="chrome://messenger/locale/shutdownWindow.properties"
+ />
+
+ <vbox align="center">
+ <label id="shutdownStatus_label" value="" />
+ <separator class="thin" />
+ </vbox>
+
+ <html:progress id="shutdown_progressmeter" max="100" />
+
+ <vbox align="center">
+ <label id="shutdownTask_label" value="" />
+ <separator class="thick" />
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/subscribe.js b/comm/mailnews/base/content/subscribe.js
new file mode 100644
index 0000000000..46eb51ce52
--- /dev/null
+++ b/comm/mailnews/base/content/subscribe.js
@@ -0,0 +1,496 @@
+/* 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/. */
+
+/* globals msgWindow, nsMsgStatusFeedback */ // From mailWindow.js
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gSubscribeTree = null;
+var gSubscribeBody = null;
+var okCallback = null;
+var gChangeTable = {};
+var gServerURI = null;
+var gSubscribableServer = null;
+var gNameField = null;
+var gNameFieldLabel = null;
+var gStatusFeedback;
+var gSearchView = null;
+var gSearchTree = null;
+var gSubscribeBundle;
+
+window.addEventListener("DOMContentLoaded", SubscribeOnLoad);
+window.addEventListener("unload", SubscribeOnUnload);
+
+document.addEventListener("dialogaccept", subscribeOK);
+document.addEventListener("dialogcancel", subscribeCancel);
+
+function Stop() {
+ if (gSubscribableServer) {
+ gSubscribableServer.stopPopulating(msgWindow);
+ }
+}
+
+function SetServerTypeSpecificTextValues() {
+ if (!gServerURI) {
+ return;
+ }
+
+ let serverType = MailUtils.getExistingFolder(gServerURI).server.type;
+
+ // Set the server specific ui elements.
+ let subscribeLabelString = gSubscribeBundle.getString(
+ "subscribeLabel-" + serverType
+ );
+ let currentListTab = "currentListTab-" + serverType;
+ let currentListTabLabel = gSubscribeBundle.getString(
+ currentListTab + ".label"
+ );
+ let currentListTabAccesskey = gSubscribeBundle.getString(
+ currentListTab + ".accesskey"
+ );
+
+ document
+ .getElementById("currentListTab")
+ .setAttribute("label", currentListTabLabel);
+ document
+ .getElementById("currentListTab")
+ .setAttribute("accesskey", currentListTabAccesskey);
+ document.getElementById("newGroupsTab").collapsed = serverType != "nntp"; // show newGroupsTab only for nntp servers
+ document
+ .getElementById("subscribeLabel")
+ .setAttribute("value", subscribeLabelString);
+}
+
+function onServerClick(aFolder) {
+ gServerURI = aFolder.server.serverURI;
+ let serverMenu = document.getElementById("serverMenu");
+ serverMenu.menupopup.selectFolder(aFolder);
+
+ SetServerTypeSpecificTextValues();
+ ShowCurrentList();
+}
+
+var MySubscribeListener = {
+ OnDonePopulating() {
+ gStatusFeedback._stopMeteors();
+ document.getElementById("stopButton").disabled = true;
+ document.getElementById("refreshButton").disabled = false;
+ document.getElementById("currentListTab").disabled = false;
+ document.getElementById("newGroupsTab").disabled = false;
+ },
+};
+
+function SetUpTree(forceToServer, getOnlyNew) {
+ if (!gServerURI) {
+ return;
+ }
+
+ var server = MailUtils.getExistingFolder(gServerURI).server;
+ try {
+ CleanUpSearchView();
+ gSubscribableServer = server.QueryInterface(Ci.nsISubscribableServer);
+
+ // Enable (or disable) the search related UI.
+ EnableSearchUI();
+
+ // Clear out the text field when switching server.
+ gNameField.value = "";
+
+ // Since there is no text, switch to the Subscription view.
+ toggleSubscriptionView(false);
+
+ gSubscribeTree.view = gSubscribableServer.folderView;
+ gSubscribableServer.subscribeListener = MySubscribeListener;
+
+ document.getElementById("currentListTab").disabled = true;
+ document.getElementById("newGroupsTab").disabled = true;
+ document.getElementById("refreshButton").disabled = true;
+
+ gStatusFeedback._startMeteors();
+ gStatusFeedback.setStatusString("");
+ gStatusFeedback.showStatusString(
+ gSubscribeBundle.getString("pleaseWaitString")
+ );
+ document.getElementById("stopButton").disabled = false;
+
+ gSubscribableServer.startPopulating(msgWindow, forceToServer, getOnlyNew);
+ } catch (e) {
+ if (e.result == 0x80550014) {
+ // NS_MSG_ERROR_OFFLINE
+ gStatusFeedback.setStatusString(
+ gSubscribeBundle.getString("offlineState")
+ );
+ } else {
+ console.error("Failed to populate subscribe tree: " + e);
+ gStatusFeedback.setStatusString(
+ gSubscribeBundle.getString("errorPopulating")
+ );
+ }
+ Stop();
+ }
+}
+
+function SubscribeOnUnload() {
+ try {
+ CleanUpSearchView();
+ } catch (ex) {
+ dump("Failed to remove the subscribe tree: " + ex + "\n");
+ }
+
+ msgWindow.closeWindow();
+}
+
+function EnableSearchUI() {
+ if (gSubscribableServer.supportsSubscribeSearch) {
+ gNameField.removeAttribute("disabled");
+ gNameFieldLabel.removeAttribute("disabled");
+ } else {
+ gNameField.setAttribute("disabled", true);
+ gNameFieldLabel.setAttribute("disabled", true);
+ }
+}
+
+function SubscribeOnLoad() {
+ gSubscribeBundle = document.getElementById("bundle_subscribe");
+
+ gSubscribeTree = document.getElementById("subscribeTree");
+ gSubscribeBody = document.getElementById("subscribeTreeBody");
+ gSearchTree = document.getElementById("searchTree");
+ gSearchTree = document.getElementById("searchTree");
+ gNameField = document.getElementById("namefield");
+ gNameFieldLabel = document.getElementById("namefieldlabel");
+
+ // eslint-disable-next-line no-global-assign
+ msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+ );
+ msgWindow.domWindow = window;
+ gStatusFeedback = new nsMsgStatusFeedback();
+ msgWindow.statusFeedback = gStatusFeedback;
+ msgWindow.rootDocShell.allowAuth = true;
+ msgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+
+ // look in arguments[0] for parameters
+ if (window.arguments && window.arguments[0]) {
+ if (window.arguments[0].okCallback) {
+ top.okCallback = window.arguments[0].okCallback;
+ }
+ }
+
+ var serverMenu = document.getElementById("serverMenu");
+
+ gServerURI = null;
+ let folder =
+ "folder" in window.arguments[0] ? window.arguments[0].folder : null;
+ if (folder && folder.server instanceof Ci.nsISubscribableServer) {
+ serverMenu.menupopup.selectFolder(folder.server.rootMsgFolder);
+ try {
+ CleanUpSearchView();
+ gSubscribableServer = folder.server.QueryInterface(
+ Ci.nsISubscribableServer
+ );
+ // Enable (or disable) the search related UI.
+ EnableSearchUI();
+ gServerURI = folder.server.serverURI;
+ } catch (ex) {
+ // dump("not a subscribable server\n");
+ CleanUpSearchView();
+ gSubscribableServer = null;
+ gServerURI = null;
+ }
+ }
+
+ if (!gServerURI) {
+ // dump("subscribe: no uri\n");
+ // dump("xxx todo: use the default news server. right now, I'm just using the first server\n");
+
+ serverMenu.selectedIndex = 0;
+
+ if (serverMenu.selectedItem) {
+ gServerURI = serverMenu.selectedItem.getAttribute("id");
+ } else {
+ // dump("xxx todo none of your servers are subscribable\n");
+ // dump("xxx todo fix this by disabling subscribe if no subscribable server or, add a CREATE SERVER button, like in 4.x\n");
+ return;
+ }
+ }
+
+ SetServerTypeSpecificTextValues();
+
+ ShowCurrentList();
+
+ gNameField.focus();
+}
+
+function subscribeOK() {
+ if (top.okCallback) {
+ top.okCallback(top.gChangeTable);
+ }
+ Stop();
+ if (gSubscribableServer) {
+ gSubscribableServer.subscribeCleanup();
+ }
+}
+
+function subscribeCancel() {
+ Stop();
+ if (gSubscribableServer) {
+ gSubscribableServer.subscribeCleanup();
+ }
+}
+
+function SetState(name, state) {
+ var changed = gSubscribableServer.setState(name, state);
+ if (changed) {
+ StateChanged(name, state);
+ }
+}
+
+function StateChanged(name, state) {
+ if (gServerURI in gChangeTable) {
+ if (name in gChangeTable[gServerURI]) {
+ var oldValue = gChangeTable[gServerURI][name];
+ if (oldValue != state) {
+ delete gChangeTable[gServerURI][name];
+ }
+ } else {
+ gChangeTable[gServerURI][name] = state;
+ }
+ } else {
+ gChangeTable[gServerURI] = {};
+ gChangeTable[gServerURI][name] = state;
+ }
+}
+
+function InSearchMode() {
+ return !document.getElementById("searchView").hidden;
+}
+
+function SearchOnClick(event) {
+ // We only care about button 0 (left click) events.
+ if (event.button != 0 || event.target.localName != "treechildren") {
+ return;
+ }
+
+ let treeCellInfo = gSearchTree.getCellAt(event.clientX, event.clientY);
+ if (treeCellInfo.row == -1 || treeCellInfo.row > gSearchView.rowCount - 1) {
+ return;
+ }
+
+ if (treeCellInfo.col.id == "subscribedColumn2") {
+ if (event.detail != 2) {
+ // Single clicked on the check box
+ // (in the "subscribedColumn2" column) reverse state.
+ // If double click, do nothing.
+ ReverseStateFromRow(treeCellInfo.row);
+ }
+ } else if (event.detail == 2) {
+ // Double clicked on a row, reverse state.
+ ReverseStateFromRow(treeCellInfo.row);
+ }
+
+ // Invalidate the row.
+ InvalidateSearchTreeRow(treeCellInfo.row);
+}
+
+function ReverseStateFromRow(aRow) {
+ // To determine if the row is subscribed or not,
+ // we get the properties for the "subscribedColumn2" cell in the row
+ // and look for the "subscribed" property.
+ // If the "subscribed" string is in the list of properties
+ // we are subscribed.
+ let col = gSearchTree.columns.nameColumn2;
+ let name = gSearchView.getCellValue(aRow, col);
+ let isSubscribed = gSubscribableServer.isSubscribed(name);
+ SetStateFromRow(aRow, !isSubscribed);
+}
+
+function SetStateFromRow(row, state) {
+ var col = gSearchTree.columns.nameColumn2;
+ var name = gSearchView.getCellValue(row, col);
+ SetState(name, state);
+}
+
+function SetSubscribeState(state) {
+ try {
+ // We need to iterate over the tree selection, and set the state for
+ // all rows in the selection.
+ var inSearchMode = InSearchMode();
+ var view = inSearchMode ? gSearchView : gSubscribeTree.view;
+ var colId = inSearchMode ? "nameColumn2" : "nameColumn";
+
+ var sel = view.selection;
+ for (var i = 0; i < sel.getRangeCount(); ++i) {
+ var start = {},
+ end = {};
+ sel.getRangeAt(i, start, end);
+ for (var k = start.value; k <= end.value; ++k) {
+ if (inSearchMode) {
+ SetStateFromRow(k, state);
+ } else {
+ let name = view.getCellValue(k, gSubscribeTree.columns[colId]);
+ SetState(name, state, k);
+ }
+ }
+ }
+
+ if (inSearchMode) {
+ // Force a repaint.
+ InvalidateSearchTree();
+ } else {
+ gSubscribeTree.invalidate();
+ }
+ } catch (ex) {
+ dump("SetSubscribedState failed: " + ex + "\n");
+ }
+}
+
+function ReverseStateFromNode(row) {
+ let name = gSubscribeTree.view.getCellValue(
+ row,
+ gSubscribeTree.columns.nameColumn
+ );
+ SetState(name, !gSubscribableServer.isSubscribed(name), row);
+}
+
+function SubscribeOnClick(event) {
+ // We only care about button 0 (left click) events.
+ if (event.button != 0 || event.target.localName != "treechildren") {
+ return;
+ }
+
+ let treeCellInfo = gSubscribeTree.getCellAt(event.clientX, event.clientY);
+ if (
+ treeCellInfo.row == -1 ||
+ treeCellInfo.row > gSubscribeTree.view.rowCount - 1
+ ) {
+ return;
+ }
+
+ if (event.detail == 2) {
+ // Only toggle subscribed state when double clicking something
+ // that isn't a container.
+ if (!gSubscribeTree.view.isContainer(treeCellInfo.row)) {
+ ReverseStateFromNode(treeCellInfo.row);
+ }
+ } else if (event.detail == 1) {
+ // If the user single clicks on the subscribe check box, we handle it here.
+ if (treeCellInfo.col.id == "subscribedColumn") {
+ ReverseStateFromNode(treeCellInfo.row);
+ }
+ }
+}
+
+function Refresh() {
+ // Clear out the textfield's entry.
+ gNameField.value = "";
+
+ var newGroupsTab = document.getElementById("newGroupsTab");
+ SetUpTree(true, newGroupsTab.selected);
+}
+
+function ShowCurrentList() {
+ // Clear out the textfield's entry on call of Refresh().
+ gNameField.value = "";
+
+ // Make sure the current list tab is selected.
+ document.getElementById("subscribeTabs").selectedIndex = 0;
+
+ // Try loading the hostinfo before talk to server.
+ SetUpTree(false, false);
+}
+
+function ShowNewGroupsList() {
+ // Clear out the textfield's entry.
+ gNameField.value = "";
+
+ // Make sure the new groups tab is selected.
+ document.getElementById("subscribeTabs").selectedIndex = 1;
+
+ // Force it to talk to the server and get new groups.
+ SetUpTree(true, true);
+}
+
+function InvalidateSearchTreeRow(row) {
+ gSearchTree.invalidateRow(row);
+}
+
+function InvalidateSearchTree() {
+ gSearchTree.invalidate();
+}
+
+/**
+ * Toggle the tree panel in the dialog between search view and subscribe view.
+ *
+ * @param {boolean} toggle - If true, show the search view else show the
+ * subscribe view.
+ */
+function toggleSubscriptionView(toggle) {
+ document.getElementById("subscribeView").hidden = toggle;
+ document.getElementById("searchView").hidden = !toggle;
+}
+
+function Search() {
+ let searchValue = gNameField.value;
+ if (
+ searchValue.length &&
+ gSubscribableServer &&
+ gSubscribableServer.supportsSubscribeSearch
+ ) {
+ toggleSubscriptionView(true);
+ gSubscribableServer.setSearchValue(searchValue);
+
+ if (!gSearchView && gSubscribableServer) {
+ gSearchView = gSubscribableServer.QueryInterface(Ci.nsITreeView);
+ gSearchView.selection = null;
+ gSearchTree.view = gSearchView;
+ }
+ return;
+ }
+ toggleSubscriptionView(false);
+}
+
+function CleanUpSearchView() {
+ if (gSearchView) {
+ gSearchView.selection = null;
+ gSearchView = null;
+ }
+}
+
+function onSearchTreeKeyPress(event) {
+ // For now, only do something on space key.
+ if (event.charCode != KeyEvent.DOM_VK_SPACE) {
+ return;
+ }
+
+ var treeSelection = gSearchView.selection;
+ for (let i = 0; i < treeSelection.getRangeCount(); i++) {
+ var start = {},
+ end = {};
+ treeSelection.getRangeAt(i, start, end);
+ for (let k = start.value; k <= end.value; k++) {
+ ReverseStateFromRow(k);
+ }
+
+ // Force a repaint.
+ InvalidateSearchTree();
+ }
+}
+
+function onSubscribeTreeKeyPress(event) {
+ // For now, only do something on space key.
+ if (event.charCode != KeyEvent.DOM_VK_SPACE) {
+ return;
+ }
+
+ var treeSelection = gSubscribeTree.view.selection;
+ for (let i = 0; i < treeSelection.getRangeCount(); i++) {
+ var start = {},
+ end = {};
+ treeSelection.getRangeAt(i, start, end);
+ for (let k = start.value; k <= end.value; k++) {
+ ReverseStateFromNode(k);
+ }
+ }
+}
diff --git a/comm/mailnews/base/content/subscribe.xhtml b/comm/mailnews/base/content/subscribe.xhtml
new file mode 100644
index 0000000000..f2838b95e2
--- /dev/null
+++ b/comm/mailnews/base/content/subscribe.xhtml
@@ -0,0 +1,235 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/subscribe.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/searchBox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/subscribe.dtd">
+
+<html
+ id="subscribeWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="width height screenX screenY"
+ lightweightthemes="true"
+ windowtype="mailnews:subscribe"
+ scrolling="false"
+>
+ <head>
+ <title>&subscribeDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/mailWindow.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/subscribe.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_subscribe"
+ src="chrome://messenger/locale/subscribe.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox flex="1">
+ <hbox>
+ <vbox>
+ <hbox pack="end" align="center" flex="1">
+ <label
+ value="&server.label;"
+ accesskey="&server.accesskey;"
+ control="serverMenu"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="namefieldlabel"
+ accesskey="&namefield.accesskey;"
+ value="&namefield.label;"
+ control="namefield"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="serverMenu" flex="1" class="folderMenuItem">
+ <menupopup
+ is="folder-menupopup"
+ mode="subscribe"
+ expandFolders="false"
+ oncommand="onServerClick(event.target._folder);"
+ />
+ </menulist>
+ <search-textbox
+ id="namefield"
+ class="themeableSearchBox"
+ flex="1"
+ timeout="300"
+ aria-controls="subscribeTree"
+ oncommand="Search();"
+ />
+ </vbox>
+ </hbox>
+ <spacer />
+ <separator class="thin" />
+ <spacer />
+ <vbox flex="1">
+ <tabbox flex="1" handleCtrlTab="false">
+ <tabs id="subscribeTabs">
+ <tab
+ id="currentListTab"
+ selected="true"
+ onclick="if (!event.target.disabled) ShowCurrentList()"
+ oncommand="ShowCurrentList()"
+ />
+ <tab
+ id="newGroupsTab"
+ label="&newGroupsTab.label;"
+ accesskey="&newGroupsTab.accesskey;"
+ onclick="if (!event.target.disabled) ShowNewGroupsList()"
+ oncommand="ShowNewGroupsList()"
+ />
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="treepanel" flex="1" orient="vertical">
+ <label id="subscribeLabel" />
+ <hbox flex="1">
+ <hbox id="subscribeView" flex="1">
+ <tree
+ id="subscribeTree"
+ treelines="true"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ onkeypress="onSubscribeTreeKeyPress(event);"
+ onclick="SubscribeOnClick(event);"
+ >
+ <treecols id="theSubscribeColumns">
+ <treecol
+ id="nameColumn"
+ primary="true"
+ hideheader="true"
+ crop="center"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn"
+ type="checkbox"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="subscribeTreeBody" />
+ </tree>
+ </hbox>
+ <hbox id="searchView" flex="1" hidden="true">
+ <tree
+ id="searchTree"
+ flex="1"
+ disableKeyNavigation="true"
+ hidecolumnpicker="true"
+ onkeypress="onSearchTreeKeyPress(event);"
+ onclick="SearchOnClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="nameColumn2"
+ primary="true"
+ hideheader="true"
+ sortDirection="ascending"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn2"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="searchTreeBody" />
+ </tree>
+ </hbox>
+ <vbox>
+ <button
+ id="subscribe"
+ label="&subscribeButton.label;"
+ accesskey="&subscribeButton.accesskey;"
+ oncommand="SetSubscribeState(true)"
+ />
+ <button
+ id="unsubscribe"
+ label="&unsubscribeButton.label;"
+ accesskey="&unsubscribeButton.accesskey;"
+ oncommand="SetSubscribeState(false)"
+ />
+ <button
+ id="refreshButton"
+ label="&refreshButton.label;"
+ accesskey="&refreshButton.accesskey;"
+ oncommand="Refresh()"
+ />
+ <button
+ id="stopButton"
+ label="&stopButton.label;"
+ accesskey="&stopButton.accesskey;"
+ oncommand="Stop()"
+ disabled="true"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ <hbox>
+ <hbox id="statusContainerBox" flex="1">
+ <label id="statusText" class="statusbarpanel" crop="end" flex="1" />
+ <hbox
+ id="statusbar-progresspanel"
+ class="statusbarpanel statusbarpanel-progress"
+ collapsed="true"
+ pack="end"
+ flex="1"
+ >
+ <html:progress
+ class="progressmeter-statusbar"
+ id="statusbar-icon"
+ value="0"
+ max="100"
+ />
+ </hbox>
+ </hbox>
+ <hbox />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/virtualFolderListEdit.js b/comm/mailnews/base/content/virtualFolderListEdit.js
new file mode 100644
index 0000000000..775d3ba1c7
--- /dev/null
+++ b/comm/mailnews/base/content/virtualFolderListEdit.js
@@ -0,0 +1,206 @@
+/* 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/. */
+
+/* globals PROTO_TREE_VIEW */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FolderUtils: "resource:///modules/FolderUtils.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+window.addEventListener("DOMContentLoaded", () => {
+ gSelectVirtual.load();
+});
+
+var gFolderTreeView = new PROTO_TREE_VIEW();
+
+var gSelectVirtual = {
+ _treeElement: null,
+ _selectedList: new Set(),
+
+ load() {
+ if (window.arguments[0].searchFolderURIs) {
+ let srchFolderUriArray = window.arguments[0].searchFolderURIs.split("|");
+ for (let uri of srchFolderUriArray) {
+ this._selectedList.add(MailUtils.getOrCreateFolder(uri));
+ }
+ }
+
+ // Add the top level of the folder tree.
+ for (let account of FolderUtils.allAccountsSorted(true)) {
+ let server = account.incomingServer;
+ if (
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount
+ ) {
+ continue;
+ }
+
+ gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder));
+ }
+
+ // Recursively expand the tree to show all selected folders.
+ function expandToSelected(row, i) {
+ hiddenFolders.delete(row._folder);
+ for (let folder of hiddenFolders) {
+ if (row._folder.isAncestorOf(folder)) {
+ gFolderTreeView.toggleOpenState(i);
+ for (let j = row.children.length - 1; j >= 0; j--) {
+ expandToSelected(row.children[j], i + j + 1);
+ }
+ break;
+ }
+ }
+ }
+
+ let hiddenFolders = new Set(gSelectVirtual._selectedList);
+ for (let i = gFolderTreeView.rowCount - 1; i >= 0; i--) {
+ expandToSelected(gFolderTreeView._rowMap[i], i);
+ }
+
+ this._treeElement = document.getElementById("folderPickerTree");
+ this._treeElement.view = gFolderTreeView;
+ },
+
+ onKeyPress(aEvent) {
+ // For now, only do something on space key.
+ if (aEvent.charCode != aEvent.DOM_VK_SPACE) {
+ return;
+ }
+
+ let selection = this._treeElement.view.selection;
+ let start = {};
+ let end = {};
+ let numRanges = selection.getRangeCount();
+
+ for (let range = 0; range < numRanges; range++) {
+ selection.getRangeAt(range, start, end);
+ for (let i = start.value; i <= end.value; i++) {
+ this._toggle(i);
+ }
+ }
+ },
+
+ onClick(aEvent) {
+ // We only care about button 0 (left click) events.
+ if (aEvent.button != 0) {
+ return;
+ }
+
+ // We don't want to toggle when clicking on header or tree (scrollbar) or
+ // on treecol.
+ if (aEvent.target.nodeName != "treechildren") {
+ return;
+ }
+
+ let treeCellInfo = this._treeElement.getCellAt(
+ aEvent.clientX,
+ aEvent.clientY
+ );
+ if (treeCellInfo.row == -1 || treeCellInfo.col.id != "selectedCol") {
+ return;
+ }
+
+ this._toggle(treeCellInfo.row);
+ },
+
+ _toggle(aRow) {
+ let folder = gFolderTreeView._rowMap[aRow]._folder;
+ if (this._selectedList.has(folder)) {
+ this._selectedList.delete(folder);
+ } else {
+ this._selectedList.add(folder);
+ }
+
+ gFolderTreeView._tree.invalidateRow(aRow);
+ },
+
+ onAccept() {
+ // XXX We should just pass the folder objects around...
+ let uris = [...this._selectedList.values()]
+ .map(folder => folder.URI)
+ .join("|");
+
+ if (window.arguments[0].okCallback) {
+ window.arguments[0].okCallback(uris);
+ }
+ },
+};
+
+document.addEventListener("dialogaccept", () => gSelectVirtual.onAccept());
+
+/**
+ * A tree row representing a single folder.
+ */
+class FolderRow {
+ constructor(folder, parent = null) {
+ this._folder = folder;
+ this._open = false;
+ this._level = parent ? parent.level + 1 : 0;
+ this._parent = parent;
+ this._children = null;
+ }
+
+ get id() {
+ return this._folder.URI;
+ }
+
+ get text() {
+ return this.getText("folderNameCol");
+ }
+
+ getText(aColName) {
+ switch (aColName) {
+ case "folderNameCol":
+ return this._folder.abbreviatedName;
+ default:
+ return "";
+ }
+ }
+
+ get open() {
+ return this._open;
+ }
+
+ get level() {
+ return this._level;
+ }
+
+ getProperties(column) {
+ let properties = "";
+ switch (column?.id) {
+ case "folderNameCol":
+ // From folderUtils.jsm.
+ properties = FolderUtils.getFolderProperties(this._folder, this.open);
+ break;
+ case "selectedCol":
+ properties = "selectedColumn";
+ if (gSelectVirtual._selectedList.has(this._folder)) {
+ properties += " selected-true";
+ }
+ break;
+ }
+ return properties;
+ }
+
+ get children() {
+ if (this._children === null) {
+ this._children = [];
+ for (let subFolder of this._folder.subFolders) {
+ if (!subFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ this._children.push(new FolderRow(subFolder, this));
+ }
+ }
+ this._children.sort((a, b) => a._folder.compareSortKeys(b._folder));
+ }
+ return this._children;
+ }
+}
diff --git a/comm/mailnews/base/content/virtualFolderListEdit.xhtml b/comm/mailnews/base/content/virtualFolderListEdit.xhtml
new file mode 100644
index 0000000000..326892734a
--- /dev/null
+++ b/comm/mailnews/base/content/virtualFolderListEdit.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- 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/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/mailWindow1.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/virtualFolderListDialog.dtd">
+
+<html
+ id="virtualFolderList"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ persist="width height screenX screenY"
+ width="400"
+ height="370"
+ windowtype="mailnews:virtualFolderList"
+ scrolling="false"
+>
+ <head>
+ <title>&virtualFolderListTitle.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/virtualFolderListEdit.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <label control="folderPickerTree">&virtualFolderDesc.label;</label>
+ <tree
+ id="folderPickerTree"
+ flex="1"
+ style="height: 290px"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="virtual"
+ onkeypress="gSelectVirtual.onKeyPress(event);"
+ onclick="gSelectVirtual.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ label="&folderName.label;"
+ style="flex: 10 10 auto"
+ primary="true"
+ crop="center"
+ />
+ <treecol
+ id="selectedCol"
+ label="&folderSearch.label;"
+ style="flex: 1 auto"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/content/virtualFolderProperties.js b/comm/mailnews/base/content/virtualFolderProperties.js
new file mode 100644
index 0000000000..10429ec3d6
--- /dev/null
+++ b/comm/mailnews/base/content/virtualFolderProperties.js
@@ -0,0 +1,383 @@
+/* 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/. */
+
+/* import-globals-from ../../search/content/searchTerm.js */
+
+var gPickedFolder;
+var gMailView = null;
+var msgWindow; // important, don't change the name of this variable. it's really a global used by commandglue.js
+var gSearchTermSession; // really an in memory temporary filter we use to read in and write out the search terms
+var gSearchFolderURIs = "";
+var gMessengerBundle = null;
+var gFolderBundle = null;
+var gDefaultColor = "";
+var gMsgFolder;
+
+var { FolderTreeProperties } = ChromeUtils.import(
+ "resource:///modules/FolderTreeProperties.jsm"
+);
+var { FolderUtils } = ChromeUtils.import("resource:///modules/FolderUtils.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+var { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogaccept", onOK);
+document.addEventListener("dialogcancel", onCancel);
+
+function onLoad() {
+ var windowArgs = window.arguments[0];
+ var acceptButton = document.querySelector("dialog").getButton("accept");
+
+ gMessengerBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+
+ gFolderBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/folderWidgets.properties"
+ );
+
+ // call this when OK is pressed
+ msgWindow = windowArgs.msgWindow; // eslint-disable-line no-global-assign
+
+ initializeSearchWidgets();
+
+ setSearchScope(Ci.nsMsgSearchScope.offlineMail);
+ if (windowArgs.editExistingFolder) {
+ acceptButton.label = document
+ .querySelector("dialog")
+ .getAttribute("editFolderAcceptButtonLabel");
+ acceptButton.accesskey = document
+ .querySelector("dialog")
+ .getAttribute("editFolderAcceptButtonAccessKey");
+ InitDialogWithVirtualFolder(windowArgs.folder);
+ } else {
+ // we are creating a new virtual folder
+ acceptButton.label = document
+ .querySelector("dialog")
+ .getAttribute("newFolderAcceptButtonLabel");
+ acceptButton.accesskey = document
+ .querySelector("dialog")
+ .getAttribute("newFolderAcceptButtonAccessKey");
+ // it is possible that we were given arguments to pre-fill the dialog with...
+ gSearchTermSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+
+ if (windowArgs.searchTerms) {
+ // then add them to our search session
+ for (let searchTerm of windowArgs.searchTerms) {
+ gSearchTermSession.appendTerm(searchTerm);
+ }
+ }
+ if (windowArgs.folder) {
+ // pre select the folderPicker, based on what they selected in the folder pane
+ gPickedFolder = windowArgs.folder;
+ try {
+ document
+ .getElementById("msgNewFolderPopup")
+ .selectFolder(windowArgs.folder);
+ } catch (ex) {
+ document
+ .getElementById("msgNewFolderPicker")
+ .setAttribute("label", windowArgs.folder.prettyName);
+ }
+
+ // if the passed in URI is not a server then pre-select it as the folder to search
+ if (!windowArgs.folder.isServer) {
+ gSearchFolderURIs = windowArgs.folder.URI;
+ }
+ }
+
+ let folderNameField = document.getElementById("name");
+ folderNameField.removeAttribute("hidden");
+ folderNameField.focus();
+ if (windowArgs.newFolderName) {
+ folderNameField.value = windowArgs.newFolderName;
+ }
+ if (windowArgs.searchFolderURIs) {
+ gSearchFolderURIs = windowArgs.searchFolderURIs;
+ }
+
+ setupSearchRows(gSearchTermSession.searchTerms);
+ doEnabling(); // we only need to disable/enable the OK button for new virtual folders
+ }
+
+ if (typeof windowArgs.searchOnline != "undefined") {
+ document.getElementById("searchOnline").checked = windowArgs.searchOnline;
+ }
+ updateOnlineSearchState();
+ updateFoldersCount();
+}
+
+function setupSearchRows(aSearchTerms) {
+ if (aSearchTerms && aSearchTerms.length > 0) {
+ // Load the search terms for the folder.
+ initializeSearchRows(Ci.nsMsgSearchScope.offlineMail, aSearchTerms);
+ } else {
+ onMore(null);
+ }
+}
+
+function updateOnlineSearchState() {
+ var enableCheckbox = false;
+ var checkbox = document.getElementById("searchOnline");
+ // only enable the checkbox for selection, for online servers
+ var srchFolderUriArray = gSearchFolderURIs.split("|");
+ if (srchFolderUriArray[0]) {
+ var realFolder = MailUtils.getOrCreateFolder(srchFolderUriArray[0]);
+ enableCheckbox = realFolder.server.offlineSupportLevel; // anything greater than 0 is an online server like IMAP or news
+ }
+
+ if (enableCheckbox) {
+ checkbox.removeAttribute("disabled");
+ } else {
+ checkbox.setAttribute("disabled", true);
+ checkbox.checked = false;
+ }
+}
+
+function InitDialogWithVirtualFolder(aVirtualFolder) {
+ let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder(
+ window.arguments[0].folder
+ );
+ gMsgFolder = window.arguments[0].folder;
+
+ let styles = getComputedStyle(document.body);
+ let folderColors = {
+ Inbox: styles.getPropertyValue("--folder-color-inbox"),
+ Sent: styles.getPropertyValue("--folder-color-sent"),
+ Outbox: styles.getPropertyValue("--folder-color-outbox"),
+ Drafts: styles.getPropertyValue("--folder-color-draft"),
+ Trash: styles.getPropertyValue("--folder-color-trash"),
+ Archive: styles.getPropertyValue("--folder-color-archive"),
+ Templates: styles.getPropertyValue("--folder-color-template"),
+ Junk: styles.getPropertyValue("--folder-color-spam"),
+ Virtual: styles.getPropertyValue("--folder-color-folder-filter"),
+ RSS: styles.getPropertyValue("--folder-color-rss"),
+ Newsgroup: styles.getPropertyValue("--folder-color-newsletter"),
+ };
+ gDefaultColor = styles.getPropertyValue("--folder-color-folder");
+
+ // when editing an existing folder, hide the folder picker that stores the parent location of the folder
+ document.getElementById("msgNewFolderPicker").collapsed = true;
+ let items = document.getElementsByClassName("chooseFolderLocation");
+ for (let item of items) {
+ item.setAttribute("hidden", true);
+ }
+ let folderNameField = document.getElementById("existingName");
+ folderNameField.removeAttribute("hidden");
+
+ // Show the icon color options.
+ document.getElementById("iconColorContainer").collapsed = false;
+
+ let folderType = FolderUtils.getSpecialFolderString(gMsgFolder);
+ if (folderType in folderColors) {
+ gDefaultColor = folderColors[folderType];
+ }
+
+ let colorInput = document.getElementById("color");
+ colorInput.value =
+ FolderTreeProperties.getColor(aVirtualFolder.URI) || gDefaultColor;
+ colorInput.addEventListener("input", event => {
+ // Preview the chosen color.
+ Services.obs.notifyObservers(
+ gMsgFolder,
+ "folder-color-preview",
+ colorInput.value
+ );
+ });
+ let resetColorButton = document.getElementById("resetColor");
+ resetColorButton.addEventListener("click", function () {
+ colorInput.value = gDefaultColor;
+ // Preview the default color.
+ Services.obs.notifyObservers(
+ gMsgFolder,
+ "folder-color-preview",
+ gDefaultColor
+ );
+ });
+
+ gSearchFolderURIs = virtualFolderWrapper.searchFolderURIs;
+ updateFoldersCount();
+ document.getElementById("searchOnline").checked =
+ virtualFolderWrapper.onlineSearch;
+ gSearchTermSession = virtualFolderWrapper.searchTermsSession;
+
+ setupSearchRows(gSearchTermSession.searchTerms);
+
+ // set the name of the folder
+ let name = gFolderBundle.formatStringFromName("verboseFolderFormat", [
+ aVirtualFolder.prettyName,
+ aVirtualFolder.server.prettyName,
+ ]);
+ folderNameField.setAttribute("value", name);
+ // update the window title based on the name of the saved search
+ document.title = gMessengerBundle.formatStringFromName(
+ "editVirtualFolderPropertiesTitle",
+ [aVirtualFolder.prettyName]
+ );
+}
+
+function onFolderPick(aEvent) {
+ gPickedFolder = aEvent.target._folder;
+ document.getElementById("msgNewFolderPopup").selectFolder(gPickedFolder);
+}
+
+function onOK(event) {
+ var name = document.getElementById("name").value;
+ var searchOnline = document.getElementById("searchOnline").checked;
+
+ if (!gSearchFolderURIs) {
+ Services.prompt.alert(
+ window,
+ null,
+ gMessengerBundle.GetStringFromName("alertNoSearchFoldersSelected")
+ );
+ event.preventDefault();
+ return;
+ }
+
+ if (window.arguments[0].editExistingFolder) {
+ // update the search terms
+ gSearchTermSession.searchTerms = saveSearchTerms(
+ gSearchTermSession.searchTerms,
+ gSearchTermSession
+ );
+ // save the settings
+ let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder(
+ window.arguments[0].folder
+ );
+ virtualFolderWrapper.searchTerms = gSearchTermSession.searchTerms;
+ virtualFolderWrapper.searchFolders = gSearchFolderURIs;
+ virtualFolderWrapper.onlineSearch = searchOnline;
+ virtualFolderWrapper.cleanUpMessageDatabase();
+
+ MailServices.accounts.saveVirtualFolders();
+
+ let color = document.getElementById("color").value;
+ if (color == gDefaultColor) {
+ color = undefined;
+ }
+ FolderTreeProperties.setColor(gMsgFolder.URI, color);
+ // Tell 3-pane tabs to update the folder's color.
+ Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color);
+
+ if (window.arguments[0].onOKCallback) {
+ window.arguments[0].onOKCallback();
+ }
+
+ return;
+ }
+
+ var uri = gPickedFolder.URI;
+ if (name && uri) {
+ // create a new virtual folder
+ // check to see if we already have a folder with the same name and alert the user if so...
+ var parentFolder = MailUtils.getOrCreateFolder(uri);
+
+ // sanity check the name based on the logic used by nsMsgBaseUtils.cpp. It can't start with a '.', it can't end with a '.', '~' or ' '.
+ // it can't contain a ';' or '#'.
+ if (/^\.|[\.\~ ]$|[\;\#]/.test(name)) {
+ Services.prompt.alert(
+ window,
+ null,
+ gMessengerBundle.GetStringFromName("folderCreationFailed")
+ );
+ event.preventDefault();
+ return;
+ } else if (parentFolder.containsChildNamed(name)) {
+ Services.prompt.alert(
+ window,
+ null,
+ gMessengerBundle.GetStringFromName("folderExists")
+ );
+ event.preventDefault();
+ return;
+ }
+
+ gSearchTermSession.searchTerms = saveSearchTerms(
+ gSearchTermSession.searchTerms,
+ gSearchTermSession
+ );
+ VirtualFolderHelper.createNewVirtualFolder(
+ name,
+ parentFolder,
+ gSearchFolderURIs,
+ gSearchTermSession.searchTerms,
+ searchOnline
+ );
+ }
+}
+
+function onCancel(event) {
+ if (gMsgFolder) {
+ // Clear any previewed color.
+ Services.obs.notifyObservers(gMsgFolder, "folder-color-preview");
+ }
+}
+
+function doEnabling() {
+ var acceptButton = document.querySelector("dialog").getButton("accept");
+ acceptButton.disabled = !document.getElementById("name").value;
+}
+
+function chooseFoldersToSearch() {
+ // if we have some search folders already, then root the folder picker dialog off the account
+ // for those folders. Otherwise fall back to the preselectedfolderURI which is the parent folder
+ // for this new virtual folder.
+ window.openDialog(
+ "chrome://messenger/content/virtualFolderListEdit.xhtml",
+ "",
+ "chrome,titlebar,modal,centerscreen,resizable",
+ {
+ searchFolderURIs: gSearchFolderURIs,
+ okCallback: onFolderListDialogCallback,
+ }
+ );
+}
+
+// callback routine from chooseFoldersToSearch
+function onFolderListDialogCallback(searchFolderURIs) {
+ gSearchFolderURIs = searchFolderURIs;
+ updateFoldersCount();
+ updateOnlineSearchState(); // we may have changed the server type we are searching...
+}
+
+function updateFoldersCount() {
+ let srchFolderUriArray = gSearchFolderURIs.split("|");
+ let folderCount = gSearchFolderURIs ? srchFolderUriArray.length : 0;
+ let foldersList = document.getElementById("chosenFoldersCount");
+ foldersList.textContent = PluralForm.get(
+ folderCount,
+ gMessengerBundle.GetStringFromName("virtualFolderSourcesChosen")
+ ).replace("#1", folderCount);
+ if (folderCount > 0) {
+ let folderNames = [];
+ for (let folderURI of srchFolderUriArray) {
+ let folder = MailUtils.getOrCreateFolder(folderURI);
+ let name = this.gMessengerBundle.formatStringFromName(
+ "verboseFolderFormat",
+ [folder.prettyName, folder.server.prettyName]
+ );
+ folderNames.push(name);
+ }
+ foldersList.setAttribute("tooltiptext", folderNames.join("\n"));
+ } else {
+ foldersList.removeAttribute("tooltiptext");
+ }
+}
+
+function onEnterInSearchTerm() {
+ // stub function called by the core search widget code...
+ // nothing for us to do here
+}
diff --git a/comm/mailnews/base/content/virtualFolderProperties.xhtml b/comm/mailnews/base/content/virtualFolderProperties.xhtml
new file mode 100644
index 0000000000..07b8283f84
--- /dev/null
+++ b/comm/mailnews/base/content/virtualFolderProperties.xhtml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<!-- 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/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % folderDTD SYSTEM "chrome://messenger/locale/virtualFolderProperties.dtd">
+ %folderDTD;
+ <!ENTITY % folderPropsDTD SYSTEM "chrome://messenger/locale/folderProps.dtd">
+ %folderPropsDTD;
+ <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+ %searchTermDTD;
+]>
+<html id="virtualFolderProperties" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ windowtype="mailnews:virtualFolderProperties"
+ style="min-width: 60em; min-height: 30em;"
+ scrolling="false">
+<head>
+ <title>&virtualFolderProperties.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchWidgets.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchTerm.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dateFormat.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/virtualFolderProperties.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="virtualFolderPropertiesDialog"
+ buttons="accept,cancel"
+ newFolderAcceptButtonLabel="&newFolderButton.label;"
+ newFolderAcceptButtonAccessKey="&newFolderButton.accesskey;"
+ editFolderAcceptButtonLabel="&editFolderButton.label;"
+ editFolderAcceptButtonAccessKey="&editFolderButton.accesskey;"
+ style="min-width: 60em; min-height: 30em;">
+
+ <html:div class="grid-two-column-fr grid-items-center">
+ <label id="nameLabel" value="&name.label;" accesskey="&name.accesskey;"
+ control="name"/>
+ <hbox class="input-container">
+ <html:input id="name"
+ hidden="hidden"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"/>
+ <html:input id="existingName"
+ readonly="readonly"
+ hidden="hidden"
+ class="input-inline"
+ tabindex="0"/>
+ <hbox id="iconColorContainer" align="center" collapsed="true">
+ <label id="colorLabel" value="&folderProps.color.label;" control="color"
+ accesskey="&folderProps.color.accesskey;"/>
+ <html:input id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"/>
+ <button id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"/>
+ </hbox>
+ </hbox>
+ <label class="chooseFolderLocation" value="&description.label;"
+ accesskey="&description.accesskey;" control="msgNewFolderPicker"/>
+ <menulist id="msgNewFolderPicker" class="folderMenuItem chooseFolderLocation"
+ align="center" displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgNewFolderPopup" class="menulist-menupopup"
+ mode="newFolder" showFileHereLabel="true" oncommand="onFolderPick(event);"/>
+ </menulist>
+ <label value="&folderSelectionCaption.label;"/>
+ <hbox align="center">
+ <label id="chosenFoldersCount"/>
+ <spacer flex="1"/>
+ <button id="folderListPicker" label="&chooseFoldersButton.label;"
+ accesskey="&chooseFoldersButton.accesskey;"
+ oncommand="chooseFoldersToSearch();"/>
+ </hbox>
+ </html:div>
+
+ <checkbox id="searchOnline" label="&searchOnline.label;"
+ accesskey="&searchOnline.accesskey;"/>
+
+ <separator class="thin"/>
+
+ <vbox id="virtualFolderSearchTerms">
+ <label value="&searchTermCaption.label;"/>
+ <hbox id="virtualFolderSearchTermListBoxWrapper">
+ <vbox id="virtualFolderSearchTermListBox">
+#include ../../search/content/searchTerm.inc.xhtml
+ </hbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/moz.build b/comm/mailnews/base/moz.build
new file mode 100644
index 0000000000..a49689ab64
--- /dev/null
+++ b/comm/mailnews/base/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/base/prefs/.eslintrc.js b/comm/mailnews/base/prefs/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/base/prefs/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/base/prefs/content/AccountManager.js b/comm/mailnews/base/prefs/content/AccountManager.js
new file mode 100644
index 0000000000..8b319d51d4
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountManager.js
@@ -0,0 +1,1949 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+/**
+ * Here's how this dialog works:
+ * The main dialog contains a tree on the left (id="accounttree") and an
+ * iframe which loads a particular preference document (such as am-main.xhtml)
+ * on the right.
+ *
+ * When the user clicks on items in the tree on the left, two things have
+ * to be determined before the UI can be updated:
+ * - the relevant account
+ * - the relevant page
+ *
+ * When both of these are known, this is what happens:
+ * - every form element of the previous page is saved in the account value
+ * hashtable for the previous account
+ * - the relevant page is loaded into the iframe
+ * - each form element in the page is filled in with an appropriate value
+ * from the current account's hashtable
+ * - in the iframe inside the page, if there is an onInit() method,
+ * it is called. The onInit method can further update this page based
+ * on values set in the previous step.
+ */
+
+/* import-globals-from accountUtils.js */
+/* import-globals-from am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm");
+var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FolderUtils",
+ "resource:///modules/FolderUtils.jsm"
+);
+var { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+var { ChatIcons } = ChromeUtils.importESModule(
+ "resource:///modules/chatIcons.sys.mjs"
+);
+
+XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () {
+ const { SubDialogManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/SubDialog.sys.mjs"
+ );
+ return new SubDialogManager({
+ dialogStack: document.getElementById("dialogStack"),
+ dialogTemplate: document.getElementById("dialogTemplate"),
+ dialogOptions: {
+ styleSheets: [
+ "chrome://messenger/skin/preferences/dialog.css",
+ "chrome://messenger/skin/preferences/preferences.css",
+ ],
+ resizeCallback: ({ title, frame }) => {
+ UIFontSize.registerWindow(frame.contentWindow);
+
+ // Resize the dialog to fit the content with edited font size.
+ requestAnimationFrame(() => {
+ let dialogs = frame.ownerGlobal.gSubDialog._dialogs;
+ let dialog = dialogs.find(
+ d => d._frame.contentDocument == frame.contentDocument
+ );
+ if (dialog) {
+ UIFontSize.resizeSubDialog(dialog);
+ }
+ });
+ },
+ },
+ });
+});
+
+// If Local directory has changed the app needs to restart. Once this is set
+// a restart will be attempted at each attempt to close the Account manager with OK.
+var gRestartNeeded = false;
+
+// This is a hash-map for every account we've touched in the pane. Each entry
+// has additional maps of attribute-value pairs that we're going to want to save
+// when the user hits OK.
+var accountArray;
+var gGenericAttributeTypes;
+
+var currentAccount;
+var currentPageId;
+
+var pendingAccount;
+var pendingPageId;
+
+/**
+ * This array contains filesystem folders that are deemed inappropriate
+ * for use as the local directory pref for message storage.
+ * It is global to allow extensions to add to/remove from it if needed.
+ * Extensions adding new server types should first consider setting
+ * nsIMsgProtocolInfo(of the server type).defaultLocalPath properly
+ * so that the test will allow that directory automatically.
+ * See the checkLocalDirectoryIsSafe function for description of the members.
+ */
+var gDangerousLocalStorageDirs = [
+ // profile folder
+ { dirsvc: "ProfD", OS: null },
+ // GRE install folder
+ { dirsvc: "GreD", OS: null },
+ // Application install folder
+ { dirsvc: "CurProcD", OS: null },
+ // system temporary folder
+ { dirsvc: "TmpD", OS: null },
+ // Windows system folder
+ { dirsvc: "SysD", OS: "WINNT" },
+ // Windows folder
+ { dirsvc: "WinD", OS: "WINNT" },
+ // Program Files folder
+ { dirsvc: "ProgF", OS: "WINNT" },
+ // trash folder
+ { dirsvc: "Trsh", OS: "Darwin" },
+ // Mac OS system folder
+ { dir: "/System", OS: "Darwin" },
+ // devices folder
+ { dir: "/dev", OS: "Darwin,Linux" },
+ // process info folder
+ { dir: "/proc", OS: "Linux" },
+ // system state folder
+ { dir: "/sys", OS: "Linux" },
+];
+
+// This sets an attribute in a xul element so that we can later
+// know what value to substitute in a prefstring. Different
+// preference types set different attributes. We get the value
+// in the same way as the function getAccountValue() determines it.
+function updateElementWithKeys(account, element, type) {
+ switch (type) {
+ case "identity":
+ element.identitykey = account.defaultIdentity.key;
+ break;
+ case "pop3":
+ case "imap":
+ case "nntp":
+ case "server":
+ element.serverkey = account.incomingServer.key;
+ break;
+ case "smtp":
+ if (MailServices.smtp.defaultServer) {
+ element.serverkey = MailServices.smtp.defaultServer.key;
+ }
+ break;
+ default:
+ // dump("unknown element type! "+type+"\n");
+ }
+}
+
+// called when the whole document loads
+// perform initialization here
+function onLoad() {
+ let selectedServer = document.documentElement.server;
+ let selectPage = document.documentElement.selectPage || null;
+
+ // Arguments can have two properties: (1) "server," the nsIMsgIncomingServer
+ // to select initially and (2) "selectPage," the page for that server to that
+ // should be selected.
+
+ accountArray = {};
+ gGenericAttributeTypes = {};
+
+ gAccountTree.load();
+
+ setTimeout(selectServer, 0, selectedServer, selectPage);
+
+ let contentFrame = document.getElementById("contentFrame");
+ contentFrame.addEventListener("load", event => {
+ let inputElements = contentFrame.contentDocument.querySelectorAll(
+ "checkbox, input, menulist, textarea, radiogroup, richlistbox"
+ );
+ contentFrame.contentDocument.addEventListener("prefchange", event => {
+ onAccept(true);
+ });
+ for (let input of inputElements) {
+ if (input.localName == "input" || input.localName == "textarea") {
+ input.addEventListener("change", event => {
+ onAccept(true);
+ });
+ } else {
+ input.addEventListener("command", event => {
+ onAccept(true);
+ });
+ }
+ }
+ UIFontSize.registerWindow(contentFrame.contentWindow);
+ });
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+}
+
+function onUnload() {
+ gAccountTree.unload();
+}
+
+function selectServer(server, selectPageId) {
+ let accountTree = document.getElementById("accounttree");
+
+ // Default to showing the first account.
+ let accountRow = accountTree.rows[0];
+
+ // Find the tree-node for the account we want to select.
+ if (server) {
+ for (let row of accountTree.children) {
+ let account = row._account;
+ if (account && server == account.incomingServer) {
+ accountRow = row;
+ // Make sure all the panes of the account to be selected are shown.
+ accountTree.expandRow(accountRow);
+ break;
+ }
+ }
+ }
+
+ let pageToSelect = accountRow;
+
+ if (selectPageId) {
+ // Find the page that also corresponds to this server.
+ // It either is the accountRow itself...
+ let pageId = accountRow.getAttribute("PageTag");
+ if (pageId != selectPageId) {
+ // ... or one of its children.
+ pageToSelect = accountRow.querySelector(
+ '[PageTag="' + selectPageId + '"]'
+ );
+ }
+ }
+
+ accountTree.selectedIndex = accountTree.rows.indexOf(pageToSelect);
+}
+
+function replaceWithDefaultSmtpServer(deletedSmtpServerKey) {
+ // First we replace the smtpserverkey in every identity.
+ for (let identity of MailServices.accounts.allIdentities) {
+ if (identity.smtpServerKey == deletedSmtpServerKey) {
+ identity.smtpServerKey = "";
+ }
+ }
+
+ // When accounts have already been loaded in the panel then the first
+ // replacement will be overwritten when the accountvalues are written out
+ // from the pagedata. We get the loaded accounts and check to make sure
+ // that the account exists for the accountid and that it has a default
+ // identity associated with it (to exclude smtpservers and local folders)
+ // Then we check only for the identity[type] and smtpServerKey[slot] and
+ // replace that with the default smtpserverkey if necessary.
+
+ for (var accountid in accountArray) {
+ var account = accountArray[accountid]._account;
+ if (account && account.defaultIdentity) {
+ var accountValues = accountArray[accountid];
+ var smtpServerKey = getAccountValue(
+ account,
+ accountValues,
+ "identity",
+ "smtpServerKey",
+ null,
+ false
+ );
+ if (smtpServerKey == deletedSmtpServerKey) {
+ setAccountValue(accountValues, "identity", "smtpServerKey", "");
+ }
+ }
+ }
+}
+
+/**
+ * Called when OK is clicked on the dialog.
+ *
+ * @param {boolean} aDoChecks - If true, execute checks on data, otherwise hope
+ * they were already done elsewhere and proceed directly to saving the data.
+ */
+function onAccept(aDoChecks) {
+ if (aDoChecks) {
+ // Check if user/host have been modified correctly.
+ if (!checkUserServerChanges(true)) {
+ return false;
+ }
+
+ if (!checkAccountNameIsValid()) {
+ return false;
+ }
+ }
+
+ // Run checks as if the page was being left.
+ if ("onLeave" in top.frames.contentFrame) {
+ if (!top.frames.contentFrame.onLeave()) {
+ // Prevent closing Account manager if user declined the changes.
+ return false;
+ }
+ }
+
+ if (!onSave()) {
+ return false;
+ }
+
+ // hack hack - save the prefs file NOW in case we crash
+ Services.prefs.savePrefFile(null);
+
+ if (gRestartNeeded) {
+ MailUtils.restartApplication();
+ // Prevent closing Account manager in case restart failed. If restart did not fail,
+ // return value does not matter, as we are restarting.
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * See if the given path to a directory is usable on the current OS.
+ *
+ * aLocalPath the nsIFile of a directory to check.
+ */
+function checkDirectoryIsValid(aLocalPath) {
+ // Any directory selected in the file picker already exists.
+ // Any directory specified in prefs.js will be created at start if it does
+ // not exist yet.
+ // If at the time of entering Account Manager the directory does not exist,
+ // it must be invalid in the current OS or not creatable due to permissions.
+ // Even then, the backend sometimes tries to create a new one
+ // under the current profile.
+ if (!aLocalPath.exists() || !aLocalPath.isDirectory()) {
+ return false;
+ }
+
+ if (Services.appinfo.OS == "WINNT") {
+ // Do not allow some special filenames on Windows.
+ // Taken from mozilla/widget/windows/nsDataObj.cpp::MangleTextToValidFilename()
+ let dirLeafName = aLocalPath.leafName;
+ const kForbiddenNames = [
+ "COM1",
+ "COM2",
+ "COM3",
+ "COM4",
+ "COM5",
+ "COM6",
+ "COM7",
+ "COM8",
+ "COM9",
+ "LPT1",
+ "LPT2",
+ "LPT3",
+ "LPT4",
+ "LPT5",
+ "LPT6",
+ "LPT7",
+ "LPT8",
+ "LPT9",
+ "CON",
+ "PRN",
+ "AUX",
+ "NUL",
+ "CLOCK$",
+ ];
+ if (kForbiddenNames.includes(dirLeafName)) {
+ return false;
+ }
+ }
+
+ // The directory must be readable and writable to work as a mail store.
+ if (!(aLocalPath.isReadable() && aLocalPath.isWritable())) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Even if the local path is usable, there are some special folders we do not
+ * want to allow for message storage as they cause problems (see e.g. bug 750781).
+ *
+ * aLocalPath The nsIFile of a directory to check.
+ */
+function checkDirectoryIsAllowed(aLocalPath) {
+ /**
+ * Check if the local path (aLocalPath) is 'safe' i.e. NOT a parent
+ * or subdirectory of the given special system/app directory (aDirToCheck).
+ *
+ * @param {object} aDirToCheck - An object describing the special directory.
+ * @param {string} aDirToCheck.dirsvc - A path keyword to retrieve from the
+ * Directory service.
+ * @param {string} aDirToCheck.dir - An absolute filesystem path.
+ * @param {string} aDirToCheck.OS - A string of comma separated values defining on which.
+ * Operating systems the folder is unusable:
+ * - null = all
+ * - WINNT = Windows
+ * - Darwin = OS X
+ * - Linux = Linux
+ * @param {string} aDirToCheck.safeSubdirs - An array of directory names that
+ * are allowed to be used under the tested directory.
+ * @param {nsIFile} aLocalPath - An nsIFile of the directory to check,
+ * intended for message storage.
+ */
+ function checkLocalDirectoryIsSafe(aDirToCheck, aLocalPath) {
+ if (aDirToCheck.OS) {
+ if (!aDirToCheck.OS.split(",").includes(Services.appinfo.OS)) {
+ return true;
+ }
+ }
+
+ let testDir = null;
+ if ("dirsvc" in aDirToCheck) {
+ try {
+ testDir = Services.dirsvc.get(aDirToCheck.dirsvc, Ci.nsIFile);
+ } catch (e) {
+ console.error(
+ "The special folder " +
+ aDirToCheck.dirsvc +
+ " cannot be retrieved on this platform: " +
+ e
+ );
+ }
+
+ if (!testDir) {
+ return true;
+ }
+ } else if ("dir" in aDirToCheck) {
+ testDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ testDir.initWithPath(aDirToCheck.dir);
+ if (!testDir.exists()) {
+ return true;
+ }
+ } else {
+ console.error("No directory to check?");
+ return true;
+ }
+
+ testDir.normalize();
+
+ if (testDir.equals(aLocalPath) || aLocalPath.contains(testDir)) {
+ return false;
+ }
+
+ if (testDir.contains(aLocalPath)) {
+ if (!("safeSubdirs" in aDirToCheck)) {
+ return false;
+ }
+
+ // While the tested directory may not be safe,
+ // a subdirectory of some safe subdirectories may be fine.
+ let isInSubdir = false;
+ for (let subDir of aDirToCheck.safeSubdirs) {
+ let checkDir = testDir.clone();
+ checkDir.append(subDir);
+ if (checkDir.contains(aLocalPath)) {
+ isInSubdir = true;
+ break;
+ }
+ }
+ return isInSubdir;
+ }
+
+ return true;
+ } // end of checkDirectoryIsNotSpecial
+
+ // If the server type has a nsIMsgProtocolInfo.defaultLocalPath set,
+ // allow that directory.
+ if (currentAccount.incomingServer) {
+ try {
+ let defaultPath =
+ currentAccount.incomingServer.protocolInfo.defaultLocalPath;
+ if (defaultPath) {
+ defaultPath.normalize();
+ if (defaultPath.contains(aLocalPath)) {
+ return true;
+ }
+ }
+ } catch (e) {
+ /* No problem if this fails. */
+ }
+ }
+
+ for (let tryDir of gDangerousLocalStorageDirs) {
+ if (!checkLocalDirectoryIsSafe(tryDir, aLocalPath)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Check if the specified directory does meet all the requirements
+ * for safe mail storage.
+ *
+ * aLocalPath the nsIFile of a directory to check.
+ */
+function checkDirectoryIsUsable(aLocalPath) {
+ const kAlertTitle = document
+ .getElementById("bundle_prefs")
+ .getString("prefPanel-server");
+ const originalPath = aLocalPath;
+
+ let invalidPath = false;
+ try {
+ aLocalPath.normalize();
+ } catch (e) {
+ invalidPath = true;
+ }
+
+ if (invalidPath || !checkDirectoryIsValid(aLocalPath)) {
+ let alertString = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("localDirectoryInvalid", [originalPath.path]);
+ Services.prompt.alert(window, kAlertTitle, alertString);
+ return false;
+ }
+
+ if (!checkDirectoryIsAllowed(aLocalPath)) {
+ let alertNotAllowed = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("localDirectoryNotAllowed", [originalPath.path]);
+ Services.prompt.alert(window, kAlertTitle, alertNotAllowed);
+ return false;
+ }
+
+ // Check that no other account has this same or dependent local directory.
+ for (let server of MailServices.accounts.allServers) {
+ if (server.key == currentAccount.incomingServer.key) {
+ continue;
+ }
+
+ let serverPath = server.localPath;
+ try {
+ serverPath.normalize();
+ let alertStringID = null;
+ if (serverPath.equals(aLocalPath)) {
+ alertStringID = "directoryAlreadyUsedByOtherAccount";
+ } else if (serverPath.contains(aLocalPath)) {
+ alertStringID = "directoryParentUsedByOtherAccount";
+ } else if (aLocalPath.contains(serverPath)) {
+ alertStringID = "directoryChildUsedByOtherAccount";
+ }
+
+ if (alertStringID) {
+ let alertString = document
+ .getElementById("bundle_prefs")
+ .getFormattedString(alertStringID, [server.prettyName]);
+
+ Services.prompt.alert(window, kAlertTitle, alertString);
+ return false;
+ }
+ } catch (e) {
+ // The other account's path is seriously broken, so we can't compare it.
+ console.error(
+ "The Local Directory path of the account " +
+ server.prettyName +
+ " seems invalid."
+ );
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Check if the user and/or host names have been changed and if so check
+ * if the new names already exists for an account or are empty.
+ * Also check if the Local Directory path was changed.
+ *
+ * @param {boolean} showAlert - Show and alert if a problem with the host / user
+ * name is found.
+ */
+function checkUserServerChanges(showAlert) {
+ const prefBundle = document.getElementById("bundle_prefs");
+ const alertTitle = prefBundle.getString("prefPanel-server");
+ var alertText = null;
+
+ var accountValues = getValueArrayFor(currentAccount);
+ if (!accountValues) {
+ return true;
+ }
+
+ let currentServer = currentAccount ? currentAccount.incomingServer : null;
+
+ // If this type doesn't exist (just removed) then return.
+ if (!("server" in accountValues) || !accountValues.server) {
+ return true;
+ }
+
+ // Get the new username, hostname and type from the page.
+ var typeElem = getPageFormElement("server.type");
+ var hostElem = getPageFormElement("server.hostName");
+ var userElem = getPageFormElement("server.username");
+ if (typeElem && userElem && hostElem) {
+ var newType = getFormElementValue(typeElem);
+ var oldHost = getAccountValue(
+ currentAccount,
+ accountValues,
+ "server",
+ "hostName",
+ null,
+ false
+ );
+ var newHost = getFormElementValue(hostElem);
+ var oldUser = getAccountValue(
+ currentAccount,
+ accountValues,
+ "server",
+ "username",
+ null,
+ false
+ );
+
+ var newUser = getFormElementValue(userElem);
+ var checkUser = true;
+ // There is no username needed for e.g. news so reset it.
+ if (currentServer && !currentServer.protocolInfo.requiresUsername) {
+ oldUser = newUser = "";
+ checkUser = false;
+ }
+ alertText = null;
+ // If something is changed then check if the new user/host already exists.
+ if (oldUser != newUser || oldHost != newHost) {
+ newUser = newUser.trim();
+ newHost = cleanUpHostName(newHost);
+ if (checkUser && newUser == "") {
+ alertText = prefBundle.getString("userNameEmpty");
+ } else if (!isLegalHostNameOrIP(newHost)) {
+ alertText = prefBundle.getString("enterValidServerName");
+ } else {
+ let sameServer = MailServices.accounts.findServer(
+ newUser,
+ newHost,
+ newType
+ );
+ if (sameServer && sameServer != currentServer) {
+ alertText = prefBundle.getString("modifiedAccountExists");
+ } else {
+ // New hostname passed all checks. We may have cleaned it up so set
+ // the new value back into the input element.
+ setFormElementValue(hostElem, newHost);
+ }
+ }
+
+ if (alertText) {
+ if (showAlert) {
+ Services.prompt.alert(window, alertTitle, alertText);
+ }
+ // Restore the old values before return
+ if (checkUser) {
+ setFormElementValue(userElem, oldUser);
+ }
+ setFormElementValue(hostElem, oldHost);
+ // If no message is shown to the user, silently revert the values
+ // and consider the check a success.
+ return !showAlert;
+ }
+
+ // If username is changed remind users to change Your Name and Email Address.
+ // If server name is changed and has defined filters then remind users
+ // to edit rules.
+ if (showAlert) {
+ let filterList;
+ if (currentServer && checkUser) {
+ filterList = currentServer.getEditableFilterList(null);
+ }
+ let changeText = "";
+ if (
+ oldHost != newHost &&
+ filterList != undefined &&
+ filterList.filterCount
+ ) {
+ changeText = prefBundle.getString("serverNameChanged");
+ }
+ // In the event that oldHost == newHost or oldUser == newUser,
+ // the \n\n will be trimmed off before the message is shown.
+ if (oldUser != newUser) {
+ changeText =
+ changeText + "\n\n" + prefBundle.getString("userNameChanged");
+ }
+
+ if (changeText != "") {
+ Services.prompt.alert(window, alertTitle, changeText.trim());
+ }
+ }
+
+ let l10n = new Localization(["messenger/accountManager.ftl"], true);
+ let cancel = Services.prompt.confirmEx(
+ window,
+ alertTitle,
+ l10n.formatValueSync("server-change-restart-required"),
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL,
+ prefBundle.getString("localDirectoryRestart"),
+ null,
+ null,
+ null,
+ {}
+ );
+ if (cancel) {
+ setFormElementValue(hostElem, oldHost);
+ setFormElementValue(userElem, oldUser);
+ return false;
+ }
+ gRestartNeeded = true;
+ }
+ }
+
+ // Check the new value of the server.localPath field for validity.
+ var pathElem = getPageFormElement("server.localPath");
+ if (!pathElem) {
+ return true;
+ }
+ let dir = getFormElementValue(pathElem);
+ if (!checkDirectoryIsUsable(dir)) {
+ // return false; // Temporarily disable this. Just show warning but do not block. See bug 921371.
+ console.error(
+ `Local directory ${dir.path} of account ${currentAccount.key} is not safe to use. Consider changing it.`
+ );
+ }
+
+ // Warn if the Local directory path was changed.
+ // This can be removed once bug 2654 is fixed.
+ let oldLocalDir = getAccountValue(
+ currentAccount,
+ accountValues,
+ "server",
+ "localPath",
+ null,
+ false
+ ); // both return nsIFile
+ let newLocalDir = getFormElementValue(pathElem);
+ if (oldLocalDir && newLocalDir && oldLocalDir.path != newLocalDir.path) {
+ let brandName = document
+ .getElementById("bundle_brand")
+ .getString("brandShortName");
+ alertText = prefBundle.getFormattedString("localDirectoryChanged", [
+ brandName,
+ ]);
+
+ let cancel = Services.prompt.confirmEx(
+ window,
+ alertTitle,
+ alertText,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL,
+ prefBundle.getString("localDirectoryRestart"),
+ null,
+ null,
+ null,
+ {}
+ );
+ if (cancel) {
+ setFormElementValue(pathElem, oldLocalDir);
+ return false;
+ }
+ gRestartNeeded = true;
+ }
+
+ return true;
+}
+
+/**
+ * If account name is not valid, alert the user.
+ */
+function checkAccountNameIsValid() {
+ if (!currentAccount) {
+ return true;
+ }
+
+ const prefBundle = document.getElementById("bundle_prefs");
+ let alertText = null;
+
+ let serverNameElem = getPageFormElement("server.prettyName");
+ if (serverNameElem) {
+ let accountName = getFormElementValue(serverNameElem);
+
+ if (!accountName) {
+ alertText = prefBundle.getString("accountNameEmpty");
+ } else if (accountNameExists(accountName, currentAccount.key)) {
+ alertText = prefBundle.getString("accountNameExists");
+ // Change the account name to prevent UI freeze.
+ let counter = 2;
+ while (
+ accountNameExists(`${accountName}_${counter}`, currentAccount.key)
+ ) {
+ counter++;
+ }
+ serverNameElem.value = `${accountName}_${counter}`;
+ }
+
+ if (alertText) {
+ const alertTitle = prefBundle.getString("accountWizard");
+ Services.prompt.alert(window, alertTitle, alertText);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function onSave() {
+ if (pendingPageId) {
+ dump("ERROR: " + pendingPageId + " hasn't loaded yet! Not saving.\n");
+ return false;
+ }
+
+ // make sure the current visible page is saved
+ savePage(currentAccount);
+
+ for (var accountid in accountArray) {
+ var accountValues = accountArray[accountid];
+ var account = accountArray[accountid]._account;
+ if (!saveAccount(accountValues, account)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Highlight the default account row in the account tree,
+ * optionally un-highlight the previous one.
+ *
+ * @param {?nsIMsgAccount} newDefault - The account that has become the new
+ * default. Can be given as null if there is none.
+ * @param {?nsIMsgAccount} oldDefault - The account that has stopped being the
+ * default. Can be given as null if there was none.
+ */
+function markDefaultServer(newDefault, oldDefault) {
+ if (oldDefault == newDefault) {
+ return;
+ }
+
+ let accountTree = document.getElementById("accounttree");
+ for (let accountRow of accountTree.children) {
+ if (newDefault && newDefault == accountRow._account) {
+ accountRow.classList.add("isDefaultServer");
+ }
+ if (oldDefault && oldDefault == accountRow._account) {
+ accountRow.classList.remove("isDefaultServer");
+ }
+ }
+}
+
+/**
+ * Notify the UI to rebuild the account tree.
+ */
+function rebuildAccountTree() {
+ // TODO: Reimplement or replace.
+}
+
+/**
+ * Make currentAccount (currently selected in the account tree) the default one.
+ */
+function onSetDefault(event) {
+ // Make sure this function was not called while the control item is disabled
+ if (event.target.getAttribute("disabled") == "true") {
+ return;
+ }
+
+ let previousDefault = MailServices.accounts.defaultAccount;
+ MailServices.accounts.defaultAccount = currentAccount;
+ markDefaultServer(currentAccount, previousDefault);
+
+ // Update gloda's myContact with the new default account's default identity.
+ Gloda._initMyIdentities();
+
+ gAccountTree.load();
+}
+
+function onRemoveAccount(event) {
+ if (event.target.getAttribute("disabled") == "true" || !currentAccount) {
+ return;
+ }
+
+ let server = currentAccount.incomingServer;
+
+ let canDelete = server.protocolInfo.canDelete || server.canDelete;
+ if (!canDelete) {
+ return;
+ }
+
+ let serverList = [];
+ let accountTree = document.getElementById("accounttree");
+ // build the list of servers in the account tree (order is important)
+ for (let row of accountTree.children) {
+ if ("_account" in row) {
+ let curServer = row._account.incomingServer;
+ if (!serverList.includes(curServer)) {
+ serverList.push(curServer);
+ }
+ }
+ }
+
+ // get position of the current server in the server list
+ let serverIndex = serverList.indexOf(server);
+
+ // After the current server is deleted, choose the next server/account,
+ // or the previous one if the last one was deleted.
+ if (serverIndex == serverList.length - 1) {
+ serverIndex--;
+ } else {
+ serverIndex++;
+ }
+
+ // Need to save these before the account and its server is removed.
+ let serverId = server.serverURI;
+
+ // Confirm account deletion.
+ let removeArgs = {
+ server,
+ account: currentAccount,
+ result: false,
+ };
+
+ let onCloseDialog = function () {
+ // If result is true, the account was removed.
+ if (!removeArgs.result) {
+ return;
+ }
+
+ // clear cached data out of the account array
+ currentAccount = currentPageId = null;
+ if (serverId in accountArray) {
+ delete accountArray[serverId];
+ }
+
+ if (serverIndex >= 0 && serverIndex < serverList.length) {
+ selectServer(serverList[serverIndex], null);
+ }
+
+ // Either the default account was deleted so there is a new one
+ // or the default account was not changed. Either way, there is
+ // no need to unmark the old one.
+ markDefaultServer(MailServices.accounts.defaultAccount, null);
+ };
+
+ gSubDialog.open(
+ "chrome://messenger/content/removeAccount.xhtml",
+ {
+ features: "resizable=no",
+ closingCallback: onCloseDialog,
+ },
+ removeArgs
+ );
+}
+
+function saveAccount(accountValues, account) {
+ var identity = null;
+ var server = null;
+
+ if (account) {
+ identity = account.defaultIdentity;
+ server = account.incomingServer;
+ }
+
+ for (var type in accountValues) {
+ var dest;
+ try {
+ if (type == "identity") {
+ dest = identity;
+ } else if (type == "server") {
+ dest = server;
+ } else if (type == "pop3") {
+ dest = server.QueryInterface(Ci.nsIPop3IncomingServer);
+ } else if (type == "imap") {
+ dest = server.QueryInterface(Ci.nsIImapIncomingServer);
+ } else if (type == "none") {
+ dest = server.QueryInterface(Ci.nsINoIncomingServer);
+ } else if (type == "nntp") {
+ dest = server.QueryInterface(Ci.nsINntpIncomingServer);
+ } else if (type == "smtp") {
+ dest = MailServices.smtp.defaultServer;
+ }
+ } catch (ex) {
+ // don't do anything, just means we don't support that
+ }
+ if (dest == undefined) {
+ continue;
+ }
+ var typeArray = accountValues[type];
+
+ for (var slot in typeArray) {
+ if (
+ type in gGenericAttributeTypes &&
+ slot in gGenericAttributeTypes[type]
+ ) {
+ var methodName = "get";
+ switch (gGenericAttributeTypes[type][slot]) {
+ case "int":
+ methodName += "Int";
+ break;
+ case "wstring":
+ methodName += "Unichar";
+ break;
+ case "string":
+ methodName += "Char";
+ break;
+ case "bool":
+ // in some cases
+ // like for radiogroups of type boolean
+ // the value will be "false" instead of false
+ // we need to convert it.
+ if (typeArray[slot] == "false") {
+ typeArray[slot] = false;
+ } else if (typeArray[slot] == "true") {
+ typeArray[slot] = true;
+ }
+
+ methodName += "Bool";
+ break;
+ default:
+ dump(
+ "unexpected preftype: " +
+ gGenericAttributeTypes[type][slot] +
+ "\n"
+ );
+ break;
+ }
+ methodName += methodName + "Value" in dest ? "Value" : "Attribute";
+ if (dest[methodName](slot) != typeArray[slot]) {
+ methodName = methodName.replace("get", "set");
+ dest[methodName](slot, typeArray[slot]);
+ }
+ } else if (
+ slot in dest &&
+ typeArray[slot] != undefined &&
+ dest[slot] != typeArray[slot]
+ ) {
+ try {
+ dest[slot] = typeArray[slot];
+ } catch (ex) {
+ // hrm... need to handle special types here
+ }
+ }
+ }
+ }
+
+ // if we made account changes to the spam settings, we'll need to re-initialize
+ // our settings object
+ if (server && server.spamSettings) {
+ try {
+ server.spamSettings.initialize(server);
+ } catch (e) {
+ let accountName = getAccountValue(
+ account,
+ getValueArrayFor(account),
+ "server",
+ "prettyName",
+ null,
+ false
+ );
+ let alertText = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("junkSettingsBroken", [accountName]);
+ let review = Services.prompt.confirmEx(
+ window,
+ null,
+ alertText,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ if (!review) {
+ onAccountTreeSelect("am-junk.xhtml", account);
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Set enabled/disabled state for the actions in the Account Actions menu.
+ * Called only by Thunderbird.
+ */
+function initAccountActionsButtons(menupopup) {
+ if (!Services.prefs.getBoolPref("mail.chat.enabled")) {
+ document.getElementById("accountActionsAddIMAccount").hidden = true;
+ }
+
+ updateItems(
+ document.getElementById("accounttree"),
+ getCurrentAccount(),
+ document.getElementById("accountActionsAddMailAccount"),
+ document.getElementById("accountActionsDropdownSetDefault"),
+ document.getElementById("accountActionsDropdownRemove")
+ );
+
+ updateBlockedItems(menupopup.children, true);
+}
+
+/**
+ * Determine enabled/disabled state for the passed in elements
+ * representing account actions.
+ */
+function updateItems(
+ tree,
+ account,
+ addAccountItem,
+ setDefaultItem,
+ removeItem
+) {
+ // Start with items disabled and then find out what can be enabled.
+ let canSetDefault = false;
+ let canDelete = false;
+
+ if (account && tree.selectedIndex >= 0) {
+ // Only try to check properties if there was anything selected in the tree
+ // and it belongs to an account.
+ // Otherwise we have either selected a SMTP server, or there is some
+ // problem. Either way, we don't want the user to act on it.
+ let server = account.incomingServer;
+
+ if (
+ account != MailServices.accounts.defaultAccount &&
+ server.canBeDefaultServer &&
+ account.identities.length > 0
+ ) {
+ canSetDefault = true;
+ }
+
+ canDelete = server.protocolInfo.canDelete || server.canDelete;
+ }
+
+ setEnabled(addAccountItem, true);
+ setEnabled(setDefaultItem, canSetDefault);
+ setEnabled(removeItem, canDelete);
+}
+
+/**
+ * Disable buttons/menu items if their control preference is locked.
+ *
+ * @param {Node[]|NodeList} aItems - Elements to be checked.
+ * @param {boolean} aMustBeTrue - If true then the pref must be boolean and set
+ * to true to trigger the disabling.
+ */
+function updateBlockedItems(aItems, aMustBeTrue) {
+ for (let item of aItems) {
+ let prefstring = item.getAttribute("prefstring");
+ if (!prefstring) {
+ continue;
+ }
+
+ if (
+ Services.prefs.prefIsLocked(prefstring) &&
+ (!aMustBeTrue || Services.prefs.getBoolPref(prefstring))
+ ) {
+ item.setAttribute("disabled", true);
+ }
+ }
+}
+
+/**
+ * Set enabled/disabled state for the control.
+ */
+function setEnabled(control, enabled) {
+ if (!control) {
+ return;
+ }
+
+ if (enabled) {
+ control.removeAttribute("disabled");
+ } else {
+ control.setAttribute("disabled", true);
+ }
+}
+
+// Called when someone clicks on an account. Figure out context by what they
+// clicked on. This is also called when an account is removed. In this case,
+// nothing is selected.
+function onAccountTreeSelect(pageId, account) {
+ let tree = document.getElementById("accounttree");
+
+ let changeView = pageId && account;
+ if (!changeView) {
+ if (tree.selectedIndex < 0) {
+ return false;
+ }
+
+ let node = tree.rows[tree.selectedIndex];
+ account = "_account" in node ? node._account : null;
+
+ pageId = node.getAttribute("PageTag");
+ }
+
+ if (pageId == currentPageId && account == currentAccount) {
+ return true;
+ }
+
+ if (
+ document
+ .getElementById("contentFrame")
+ .contentDocument.getElementById("server.localPath")
+ ) {
+ // Check if user/host names have been changed or the Local Directory is invalid.
+ if (!checkUserServerChanges(false)) {
+ changeView = true;
+ account = currentAccount;
+ pageId = currentPageId;
+ }
+
+ if (gRestartNeeded) {
+ onAccept(false);
+ }
+ }
+
+ if (
+ document
+ .getElementById("contentFrame")
+ .contentDocument.getElementById("server.prettyName")
+ ) {
+ // Check if account name is valid.
+ if (!checkAccountNameIsValid()) {
+ changeView = true;
+ account = currentAccount;
+ pageId = currentPageId;
+ }
+ }
+
+ if (currentPageId) {
+ // Change focus to the account tree first so that any 'onchange' handlers
+ // on elements in the current page have a chance to run before the page
+ // is saved and replaced by the new one.
+ tree.focus();
+ }
+
+ // Provide opportunity to do cleanups or checks when the current page is being left.
+ if ("onLeave" in top.frames.contentFrame) {
+ top.frames.contentFrame.onLeave();
+ }
+
+ // save the previous page
+ savePage(currentAccount);
+
+ let changeAccount = account != currentAccount;
+
+ if (changeView) {
+ selectServer(account.incomingServer, pageId);
+ }
+
+ if (pageId != currentPageId) {
+ // loading a complete different page
+
+ // prevent overwriting with bad stuff
+ currentAccount = currentPageId = null;
+
+ pendingAccount = account;
+ pendingPageId = pageId;
+ loadPage(pageId);
+ } else if (changeAccount) {
+ // same page, different server
+ restorePage(pageId, account);
+ }
+
+ return true;
+}
+
+// page has loaded
+function onPanelLoaded(pageId) {
+ if (pageId != pendingPageId) {
+ // if we're reloading the current page, we'll assume the
+ // page has asked itself to be completely reloaded from
+ // the prefs. to do this, clear out the the old entry in
+ // the account data, and then restore theh page
+ if (pageId == currentPageId) {
+ var serverId = currentAccount
+ ? currentAccount.incomingServer.serverURI
+ : "global";
+ delete accountArray[serverId];
+ restorePage(currentPageId, currentAccount);
+ }
+ } else {
+ restorePage(pendingPageId, pendingAccount);
+ }
+
+ // probably unnecessary, but useful for debugging
+ pendingAccount = null;
+ pendingPageId = null;
+}
+
+function pageURL(pageId) {
+ // If we have a special non account manager pane (e.g. about:blank),
+ // do not translate it into ChromePackageName URL.
+ if (!pageId.startsWith("am-")) {
+ return pageId;
+ }
+
+ let chromePackageName;
+ try {
+ // we could compare against "main","server","copies","offline","addressing",
+ // "smtp" and "advanced" first to save the work, but don't,
+ // as some of these might be turned into extensions (for thunderbird)
+ let packageName = pageId.split("am-")[1].split(".xhtml")[0];
+ chromePackageName = MailServices.accounts.getChromePackageName(packageName);
+ } catch (ex) {
+ chromePackageName = "messenger";
+ }
+ return "chrome://" + chromePackageName + "/content/" + pageId;
+}
+
+function loadPage(pageId) {
+ const loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ document
+ .getElementById("contentFrame")
+ .webNavigation.fixupAndLoadURIString(pageURL(pageId), loadURIOptions);
+}
+
+// save the values of the widgets to the given server
+function savePage(account) {
+ if (!account) {
+ return;
+ }
+
+ // tell the page that it's about to save
+ if ("onSave" in top.frames.contentFrame) {
+ top.frames.contentFrame.onSave();
+ }
+
+ var accountValues = getValueArrayFor(account);
+ if (!accountValues) {
+ return;
+ }
+ // Reset accountArray so that only the current page will be saved. This is
+ // needed to prevent resetting prefs unintentionally. An example is when
+ // changing username/hostname, MsgIncomingServer.jsm will modify identities,
+ // without this, identities changes may be reverted to old values in
+ // accountArray.
+ accountArray = {};
+ accountValues = {};
+ let serverId = account.incomingServer.serverURI;
+ accountArray[serverId] = accountValues;
+ accountArray[serverId]._account = account;
+
+ var pageElements = getPageFormElements();
+ if (!pageElements) {
+ return;
+ }
+
+ // store the value in the account
+ for (let i = 0; i < pageElements.length; i++) {
+ if (pageElements[i].id) {
+ let vals = pageElements[i].id.split(".");
+ if (vals.length >= 2) {
+ let type = vals[0];
+ let slot = pageElements[i].id.slice(type.length + 1);
+
+ setAccountValue(
+ accountValues,
+ type,
+ slot,
+ getFormElementValue(pageElements[i])
+ );
+ }
+ }
+ }
+}
+
+function setAccountValue(accountValues, type, slot, value) {
+ if (!(type in accountValues)) {
+ accountValues[type] = {};
+ }
+
+ accountValues[type][slot] = value;
+}
+
+function getAccountValue(
+ account,
+ accountValues,
+ type,
+ slot,
+ preftype,
+ isGeneric
+) {
+ if (!(type in accountValues)) {
+ accountValues[type] = {};
+ }
+
+ // fill in the slot from the account if necessary
+ if (
+ !(slot in accountValues[type]) ||
+ accountValues[type][slot] == undefined
+ ) {
+ var server;
+ if (account) {
+ server = account.incomingServer;
+ }
+ var source = null;
+ try {
+ if (type == "identity") {
+ source = account.defaultIdentity;
+ } else if (type == "server") {
+ source = account.incomingServer;
+ } else if (type == "pop3") {
+ source = server.QueryInterface(Ci.nsIPop3IncomingServer);
+ } else if (type == "imap") {
+ source = server.QueryInterface(Ci.nsIImapIncomingServer);
+ } else if (type == "none") {
+ source = server.QueryInterface(Ci.nsINoIncomingServer);
+ } else if (type == "nntp") {
+ source = server.QueryInterface(Ci.nsINntpIncomingServer);
+ } else if (type == "smtp") {
+ source = MailServices.smtp.defaultServer;
+ }
+ } catch (ex) {}
+
+ if (source) {
+ if (isGeneric) {
+ if (!(type in gGenericAttributeTypes)) {
+ gGenericAttributeTypes[type] = {};
+ }
+
+ // we need the preftype later, for setting when we save.
+ gGenericAttributeTypes[type][slot] = preftype;
+ var methodName = "get";
+ switch (preftype) {
+ case "int":
+ methodName += "Int";
+ break;
+ case "wstring":
+ methodName += "Unichar";
+ break;
+ case "string":
+ methodName += "Char";
+ break;
+ case "bool":
+ methodName += "Bool";
+ break;
+ default:
+ dump("unexpected preftype: " + preftype + "\n");
+ break;
+ }
+ methodName += methodName + "Value" in source ? "Value" : "Attribute";
+ accountValues[type][slot] = source[methodName](slot);
+ } else if (slot in source) {
+ accountValues[type][slot] = source[slot];
+ } else {
+ accountValues[type][slot] = null;
+ }
+ } else {
+ accountValues[type][slot] = null;
+ }
+ }
+ return accountValues[type][slot];
+}
+
+// restore the values of the widgets from the given server
+function restorePage(pageId, account) {
+ if (!account) {
+ return;
+ }
+
+ var accountValues = getValueArrayFor(account);
+ if (!accountValues) {
+ return;
+ }
+
+ if ("onPreInit" in top.frames.contentFrame) {
+ top.frames.contentFrame.onPreInit(account, accountValues);
+ }
+
+ var pageElements = getPageFormElements();
+ if (!pageElements) {
+ return;
+ }
+
+ // restore the value from the account
+ for (let i = 0; i < pageElements.length; i++) {
+ if (pageElements[i].id) {
+ let vals = pageElements[i].id.split(".");
+ if (vals.length >= 2) {
+ let type = vals[0];
+ let slot = pageElements[i].id.slice(type.length + 1);
+
+ // buttons are lockable, but don't have any data so we skip that part.
+ // elements that do have data, we get the values at poke them in.
+ if (pageElements[i].localName != "button") {
+ var value = getAccountValue(
+ account,
+ accountValues,
+ type,
+ slot,
+ pageElements[i].getAttribute("preftype"),
+ pageElements[i].getAttribute("genericattr") == "true"
+ );
+ setFormElementValue(pageElements[i], value);
+ }
+ var element = pageElements[i];
+ switch (type) {
+ case "identity":
+ element.identitykey = account.defaultIdentity.key;
+ break;
+ case "pop3":
+ case "imap":
+ case "nntp":
+ case "server":
+ element.serverkey = account.incomingServer.key;
+ break;
+ case "smtp":
+ if (MailServices.smtp.defaultServer) {
+ element.serverkey = MailServices.smtp.defaultServer.key;
+ }
+ break;
+ }
+ var isLocked = getAccountValueIsLocked(pageElements[i]);
+ setEnabled(pageElements[i], !isLocked);
+ }
+ }
+ }
+
+ // tell the page that new values have been loaded
+ if ("onInit" in top.frames.contentFrame) {
+ top.frames.contentFrame.onInit(pageId, account.incomingServer.serverURI);
+ }
+
+ // everything has succeeded, vervied by setting currentPageId
+ currentPageId = pageId;
+ currentAccount = account;
+}
+
+/**
+ * Gets the value of a widget in current the account settings page,
+ * automatically setting the right property of it depending on element type.
+ *
+ * @param {HTMLInputElement} formElement - An input element.
+ */
+function getFormElementValue(formElement) {
+ try {
+ var type = formElement.localName;
+ if (type == "checkbox") {
+ if (formElement.getAttribute("reversed")) {
+ return !formElement.checked;
+ }
+ return formElement.checked;
+ }
+ if (type == "input" && formElement.getAttribute("datatype") == "nsIFile") {
+ if (formElement.value) {
+ let localfile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+
+ localfile.initWithPath(formElement.value);
+ return localfile;
+ }
+ return null;
+ }
+ if (type == "input" || "value" in formElement) {
+ return formElement.value.trim();
+ }
+ return null;
+ } catch (ex) {
+ console.error("getFormElementValue failed, ex=" + ex + "\n");
+ }
+ return null;
+}
+
+/**
+ * Sets the value of a widget in current the account settings page,
+ * automatically setting the right property of it depending on element type.
+ *
+ * @param {HTMLInputElement} formElement - An input element.
+ * @param {string|nsIFile} value - The value to store in the element.
+ */
+function setFormElementValue(formElement, value) {
+ var type = formElement.localName;
+ if (type == "checkbox") {
+ if (value == null) {
+ formElement.checked = false;
+ } else if (formElement.getAttribute("reversed")) {
+ formElement.checked = !value;
+ } else {
+ formElement.checked = value;
+ }
+ } else if (type == "radiogroup" || type == "menulist") {
+ if (value == null) {
+ formElement.selectedIndex = 0;
+ } else {
+ formElement.value = value;
+ }
+ } else if (
+ type == "input" &&
+ formElement.getAttribute("datatype") == "nsIFile"
+ ) {
+ // handle nsIFile
+ if (value) {
+ let localfile = value.QueryInterface(Ci.nsIFile);
+ try {
+ formElement.value = localfile.path;
+ } catch (ex) {
+ dump("Still need to fix uninitialized nsIFile problem!\n");
+ }
+ } else {
+ formElement.value = "";
+ }
+ } else if (type == "input") {
+ if (value == null) {
+ formElement.value = null;
+ } else {
+ formElement.value = value;
+ }
+ } else if (type == "label") {
+ formElement.value = value || "";
+ } else if (value == null) {
+ // let the form figure out what to do with it
+ formElement.value = null;
+ } else {
+ formElement.value = value;
+ }
+}
+
+//
+// conversion routines - get data associated
+// with a given pageId, serverId, etc
+//
+
+// helper routine for account manager panels to get the current account for the selected server
+function getCurrentAccount() {
+ return currentAccount;
+}
+
+/**
+ * Get the array of persisted form elements for the given page.
+ */
+function getPageFormElements() {
+ // Uses getElementsByAttribute() which returns a live NodeList which is usually
+ // faster than e.g. querySelector().
+ if ("getElementsByAttribute" in top.frames.contentFrame.document) {
+ return top.frames.contentFrame.document.getElementsByAttribute(
+ "wsm_persist",
+ "true"
+ );
+ }
+
+ return null;
+}
+
+/**
+ * Get a single persisted form element in the current page.
+ *
+ * @param {srtring} aId - ID of the element requested.
+ */
+function getPageFormElement(aId) {
+ let elem = top.frames.contentFrame.document.getElementById(aId);
+ if (elem && elem.getAttribute("wsm_persist") == "true") {
+ return elem;
+ }
+
+ return null;
+}
+
+// get the value array for the given account
+function getValueArrayFor(account) {
+ var serverId = account ? account.incomingServer.serverURI : "global";
+
+ if (!(serverId in accountArray)) {
+ accountArray[serverId] = {};
+ accountArray[serverId]._account = account;
+ }
+
+ return accountArray[serverId];
+}
+
+/**
+ * Sets the name of the account rowitem in the tree pane.
+ *
+ * @param {string} aAccountKey - The key of the account to change.
+ * @param {string} aLabel - The value of the label to set.
+ */
+function setAccountLabel(aAccountKey, aLabel) {
+ let row = document.getElementById(aAccountKey);
+ if (row) {
+ row.setAttribute("aria-label", aLabel);
+ row.title = aLabel;
+ row.querySelector(".name").textContent = aLabel;
+ }
+ rebuildAccountTree(false);
+}
+
+var gAccountTree = {
+ load() {
+ this._build();
+
+ let mainTree = document.getElementById("accounttree");
+ mainTree.__defineGetter__("_orderableChildren", function () {
+ let rows = [...this.children];
+ rows.pop();
+ return rows;
+ });
+ mainTree.addEventListener("ordering", event => {
+ if (!event.detail || event.detail.id == "smtp") {
+ event.preventDefault();
+ }
+ });
+ mainTree.addEventListener("ordered", event => {
+ let accountKeyList = Array.from(mainTree.children, row => row.id);
+ accountKeyList.pop(); // Remove SMTP.
+ MailServices.accounts.reorderAccounts(accountKeyList);
+ rebuildAccountTree();
+ });
+ mainTree.addEventListener("expanded", event => {
+ this._dataStore.setValue(
+ document.documentURI,
+ event.target.id,
+ "open",
+ "true"
+ );
+ });
+ mainTree.addEventListener("collapsed", event => {
+ this._dataStore.setValue(
+ document.documentURI,
+ event.target.id,
+ "open",
+ "false"
+ );
+ });
+
+ MailServices.accounts.addIncomingServerListener(this);
+ },
+ unload() {
+ MailServices.accounts.removeIncomingServerListener(this);
+ },
+ onServerLoaded(server) {
+ // We assume the newly appeared server was created by the user so we select
+ // it in the tree.
+ this._build(server);
+ },
+ onServerUnloaded(aServer) {
+ this._build();
+ },
+ onServerChanged(aServer) {},
+
+ _dataStore: Services.xulStore,
+
+ /**
+ * Retrieve from XULStore.json whether the account should be expanded (open)
+ * in the account tree.
+ *
+ * @param {string} aAccountKey - Key of the account to check.
+ */
+ _getAccountOpenState(aAccountKey) {
+ if (!this._dataStore.hasValue(document.documentURI, aAccountKey, "open")) {
+ // If there was no value stored, use opened state.
+ return "true";
+ }
+ // Retrieve the persisted value from XULStore.json.
+ // It is stored under the URI of the current document and ID of the XUL element.
+ return this._dataStore.getValue(document.documentURI, aAccountKey, "open");
+ },
+
+ _build(newServer) {
+ var bundle = document.getElementById("bundle_prefs");
+ function getString(aString) {
+ return bundle.getString(aString);
+ }
+ var panels = [
+ { string: getString("prefPanel-server"), src: "am-server.xhtml" },
+ { string: getString("prefPanel-copies"), src: "am-copies.xhtml" },
+ {
+ string: getString("prefPanel-synchronization"),
+ src: "am-offline.xhtml",
+ },
+ { string: getString("prefPanel-diskspace"), src: "am-offline.xhtml" },
+ { string: getString("prefPanel-addressing"), src: "am-addressing.xhtml" },
+ { string: getString("prefPanel-junk"), src: "am-junk.xhtml" },
+ ];
+
+ let accounts = FolderUtils.allAccountsSorted(false);
+
+ let mainTree = document.getElementById("accounttree");
+ // Clear off all children...
+ while (mainTree.hasChildNodes()) {
+ mainTree.lastChild.remove();
+ }
+
+ for (let account of accounts) {
+ let accountName = null;
+ let accountKey = account.key;
+ let amChrome = "about:blank";
+ let panelsToKeep = [];
+ let server = null;
+
+ // This "try {} catch {}" block is intentionally very long to catch
+ // unknown exceptions and confine them to this single account.
+ // This may happen from broken accounts. See e.g. bug 813929.
+ // Other accounts can still be shown properly if they are valid.
+ try {
+ server = account.incomingServer;
+
+ if (
+ server.type == "im" &&
+ !Services.prefs.getBoolPref("mail.chat.enabled")
+ ) {
+ continue;
+ }
+
+ accountName = server.prettyName;
+
+ // Now add our panels.
+ let idents = MailServices.accounts.getIdentitiesForServer(server);
+ if (idents.length) {
+ panelsToKeep.push(panels[0]); // The server panel is valid
+ panelsToKeep.push(panels[1]); // also the copies panel
+ panelsToKeep.push(panels[4]); // and addressing
+ }
+
+ // Everyone except News, RSS and IM has a junk panel
+ // XXX: unextensible!
+ // The existence of server.spamSettings can't currently be used for this.
+ if (
+ server.type != "nntp" &&
+ server.type != "rss" &&
+ server.type != "im"
+ ) {
+ panelsToKeep.push(panels[5]);
+ }
+
+ // Check offline/diskspace support level.
+ let diskspace = server.supportsDiskSpace;
+ if (server.offlineSupportLevel >= 10 && diskspace) {
+ panelsToKeep.push(panels[2]);
+ } else if (diskspace) {
+ panelsToKeep.push(panels[3]);
+ }
+
+ // extensions
+ const CATEGORY = "mailnews-accountmanager-extensions";
+ for (let { data } of Services.catMan.enumerateCategory(CATEGORY)) {
+ try {
+ let svc = Cc[
+ Services.catMan.getCategoryEntry(CATEGORY, data)
+ ].getService(Ci.nsIMsgAccountManagerExtension);
+ if (svc.showPanel(server)) {
+ let bundleName =
+ "chrome://" +
+ svc.chromePackageName +
+ "/locale/am-" +
+ svc.name +
+ ".properties";
+ let bundle = Services.strings.createBundle(bundleName);
+ let title = bundle.GetStringFromName("prefPanel-" + svc.name);
+ panelsToKeep.push({
+ string: title,
+ src: "am-" + svc.name + ".xhtml",
+ });
+ }
+ } catch (e) {
+ // Fetching of this extension panel failed so do not show it,
+ // just log error.
+ let extName = data || "(unknown)";
+ console.error(
+ "Error accessing panel from extension '" + extName + "': " + e
+ );
+ }
+ }
+ amChrome = server.accountManagerChrome;
+ } catch (e) {
+ // Show only a placeholder in the account list saying this account
+ // is broken, with no child panels.
+ let accountID = accountName || accountKey;
+ console.error("Error accessing account " + accountID + ": " + e);
+ accountName = "Invalid account " + accountID;
+ panelsToKeep.length = 0;
+ }
+
+ // Create the top level tree-item.
+ let treeitem = document
+ .getElementById("accountTreeItem")
+ .content.firstElementChild.cloneNode(true);
+ mainTree.appendChild(treeitem);
+ treeitem.setAttribute("aria-label", accountName);
+ treeitem.title = accountName;
+ treeitem.querySelector(".name").textContent = accountName;
+ treeitem.setAttribute("PageTag", amChrome);
+ // Add icons based on account type.
+ if (server) {
+ treeitem.classList.add("serverType-" + server.type);
+ if (server.isSecure) {
+ treeitem.classList.add("isSecure");
+ }
+ // For IM accounts, we can try to fetch a protocol specific icon.
+ if (server.type == "im") {
+ treeitem.querySelector(".icon").style.backgroundImage =
+ "url(" +
+ ChatIcons.getProtocolIconURI(
+ server.wrappedJSObject.imAccount.protocol
+ ) +
+ ")";
+ treeitem.id = accountKey;
+ }
+ }
+
+ if (panelsToKeep.length > 0) {
+ let treekids = treeitem.querySelector("ul");
+ for (let panel of panelsToKeep) {
+ let kidtreeitem = document.createElement("li");
+ kidtreeitem.title = panel.string;
+ treekids.appendChild(kidtreeitem);
+ let kidtreerow = document.createElement("div");
+ kidtreeitem.appendChild(kidtreerow);
+ let kidtreecell = document.createElement("span");
+ kidtreecell.classList.add("name");
+ kidtreecell.tabIndex = -1;
+ kidtreerow.appendChild(kidtreecell);
+ kidtreecell.textContent = panel.string;
+ kidtreeitem.setAttribute("PageTag", panel.src);
+ kidtreeitem._account = account;
+ kidtreeitem.id = `${accountKey}/${panel.src}`;
+ }
+ treeitem.id = accountKey;
+ // Load the 'open' state of the account from XULStore.json.
+ if (this._getAccountOpenState(accountKey) != "true") {
+ treeitem.classList.add("collapsed");
+ }
+ }
+ treeitem._account = account;
+ }
+
+ markDefaultServer(MailServices.accounts.defaultAccount, null);
+
+ // Now add the outgoing server node.
+ let treeitem = document
+ .getElementById("accountTreeItem")
+ .content.firstElementChild.cloneNode(true);
+ mainTree.appendChild(treeitem);
+ treeitem.id = "smtp";
+ treeitem.querySelector(".name").textContent = getString("prefPanel-smtp");
+ treeitem.setAttribute("PageTag", "am-smtp.xhtml");
+ treeitem.classList.add("serverType-smtp");
+
+ // If a new server was created, select the server after rebuild of the tree.
+ if (newServer) {
+ setTimeout(selectServer, 0, newServer);
+ }
+ },
+};
diff --git a/comm/mailnews/base/prefs/content/AccountManager.xhtml b/comm/mailnews/base/prefs/content/AccountManager.xhtml
new file mode 100644
index 0000000000..7769ff730b
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountManager.xhtml
@@ -0,0 +1,171 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % accountManagerDTD SYSTEM "chrome://messenger/locale/AccountManager.dtd">
+ %accountManagerDTD;
+ <!ENTITY % utilityDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd">
+ %utilityDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false">
+<head>
+ <title>&accountManagerTitle.label;</title>
+
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; script-src chrome: 'unsafe-inline'; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
+ <meta name="color-scheme" content="light dark" />
+
+ <link rel="icon" href="chrome://messenger/skin/icons/new/compact/account-settings.svg" />
+
+ <link rel="stylesheet" href="chrome://messenger/skin/inContentDialog.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/icons.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/folderPane.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/shared/tree-listbox.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/shared/accountManager.css" />
+
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountManager.ftl" />
+ <link rel="localization" href="messenger/addressbook/aboutAddressBook.ftl" />
+
+ <script defer="defer" src="chrome://messenger/content/mailCore.js"></script>
+ <script defer="defer" src="chrome://communicator/content/utilityOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/AccountManager.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/tree-listbox.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { onLoad(event); });
+ window.addEventListener("unload", event => { onUnload(); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/>
+ <html:aside id="accountTreeBox">
+ <html:ol is="orderable-tree-listbox" id="accounttree" class="no-overscroll"
+ role="tree"
+ flex="1"
+ onselect="onAccountTreeSelect(null, null);">
+ </html:ol>
+ <template id="accountTreeItem" xmlns="http://www.w3.org/1999/xhtml">
+ <li>
+ <div draggable="true">
+ <div class="twisty">
+ <img class="twisty-icon" alt=""
+ src="chrome://global/skin/icons/arrow-down-12.svg" />
+ </div>
+ <div class="icon"></div>
+ <span class="name" tabindex="-1"></span>
+ </div>
+ <ul></ul>
+ </li>
+ </template>
+
+ <button id="accountActionsButton" type="menu"
+ label="&accountActionsButton.label;"
+ accesskey="&accountActionsButton.accesskey;">
+ <menupopup id="accountActionsDropdown"
+ onpopupshowing="initAccountActionsButtons(this);">
+ <menuitem id="accountActionsAddMailAccount"
+ label="&addMailAccountButton.label;"
+ accesskey="&addMailAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="openAccountSetupTab();"/>
+ <menuitem id="accountActionsAddIMAccount"
+ label="&addIMAccountButton.label;"
+ accesskey="&addIMAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="AddIMAccount(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsAddFeedAccount"
+ label="&addFeedAccountButton.label;"
+ accesskey="&addFeedAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="AddFeedAccount(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsAddOtherAccount"
+ data-l10n-id="account-action-add-newsgroup-account"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="openNewsgroupAccountWizard(); event.stopPropagation();"/>
+ <menuseparator id="accountActionsDropdownSep1"/>
+ <menuitem id="accountActionsDropdownSetDefault"
+ label="&setDefaultButton.label;"
+ accesskey="&setDefaultButton.accesskey;"
+ prefstring="mail.disable_button.set_default_account"
+ oncommand="onSetDefault(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsDropdownRemove"
+ label="&removeButton.label;"
+ accesskey="&removeButton.accesskey;"
+ prefstring="mail.disable_button.delete_account"
+ oncommand="onRemoveAccount(event); event.stopPropagation();"/>
+ </menupopup>
+ </button>
+
+ <vbox class="sidebar-footer-list">
+ <html:a id="prefsButton" class="sidebar-footer-link"
+ onclick="openOptionsDialog();">
+ <html:img class="sidebar-footer-icon"
+ src="chrome://messenger/skin/icons/new/compact/settings.svg" alt="" />
+ <label class="sidebar-footer-label"
+ data-l10n-id="open-preferences-sidebar-button2"
+ flex="1"/>
+ </html:a>
+
+ <html:a id="addonsButton" class="sidebar-footer-link"
+ onclick="window.browsingContext.topChromeWindow.openAddonsMgr();">
+ <html:img class="sidebar-footer-icon"
+ src="chrome://messenger/skin/icons/new/compact/extension.svg"
+ alt="" />
+ <label class="sidebar-footer-label"
+ data-l10n-id="open-addons-sidebar-button"
+ flex="1"/>
+ </html:a>
+ </vbox>
+ </html:aside>
+ <html:aside>
+ <iframe id="contentFrame" name="contentFrame" />
+ </html:aside>
+
+ <stack id="dialogStack" hidden="true"/>
+ <vbox id="dialogTemplate"
+ class="dialogOverlay"
+ align="center"
+ pack="center"
+ topmost="true"
+ hidden="true">
+ <vbox class="dialogBox"
+ pack="end"
+ role="dialog"
+ aria-labelledby="dialogTitle">
+ <hbox class="dialogTitleBar" align="center">
+ <label class="dialogTitle" flex="1"/>
+ <button class="dialogClose close-icon"
+ aria-label="&accountManagerCloseButton.label;"/>
+ </hbox>
+ <browser class="dialogFrame"
+ autoscroll="false"
+ disablehistory="true"/>
+ </vbox>
+ </vbox>
+
+ <dialog xmlns="http://www.w3.org/1999/xhtml" id="editVCardDialog">
+ <form autocomplete="off">
+#include ../../../../mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
+ <vcard-edit class="edit"/>
+ <menu class="dialog-menu-container">
+ <button type="reset" class="cancel"
+ data-l10n-id="edit-vcard-dialog-cancel-button"
+ data-l10n-attrs="accesskey"></button>
+ <button type="submit" class="accept primary"
+ data-l10n-id="edit-vcard-dialog-accept-button"
+ data-l10n-attrs="accesskey"></button>
+ </menu>
+ </form>
+ </dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/AccountWizard.js b/comm/mailnews/base/prefs/content/AccountWizard.js
new file mode 100644
index 0000000000..69047c3f9e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountWizard.js
@@ -0,0 +1,605 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/* import-globals-from accountUtils.js */
+/* import-globals-from amUtils.js */
+/* import-globals-from aw-identity.js */
+/* import-globals-from aw-incoming.js */
+/* import-globals-from aw-accname.js */
+/* import-globals-from aw-done.js */
+
+/* NOTE: This Account Wizard is *only* for Newsgroup accounts.
+ * Historically, it was a generic Account Wizard, hence the generic naming.
+ */
+
+/*
+ data flow into the account wizard like this:
+
+ For new accounts:
+ * pageData -> accountData -> createAccount -> finishAccount
+
+ for "unfinished accounts"
+ * account -> accountData -> pageData -> accountData -> finishAccount
+*/
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+var contentWindow;
+
+var gPageData;
+
+var nsIMsgIdentity = Ci.nsIMsgIdentity;
+var nsIMsgIncomingServer = Ci.nsIMsgIncomingServer;
+var gPrefsBundle, gMessengerBundle;
+
+// the current nsIMsgAccount
+var gCurrentAccount;
+
+// The default account before we create a new account.
+// We need to store this as just asking for the default account may switch
+// it to the newly created one if there was none before.
+var gDefaultAccount;
+
+// the current associative array that
+// will eventually be dumped into the account
+var gCurrentAccountData = null;
+
+// default picker mode for copies and folders
+var gDefaultSpecialFolderPickerMode = "0";
+
+// event handlers
+function onAccountWizardLoad() {
+ document.querySelector("wizard").addEventListener("wizardcancel", onCancel);
+ document
+ .querySelector("wizard")
+ .addEventListener("wizardfinish", FinishAccount);
+ let identityPage = document.getElementById("identitypage");
+ identityPage.addEventListener("pageshow", identityPageInit);
+ identityPage.addEventListener("pageadvanced", identityPageUnload);
+ identityPage.next = "newsserver";
+ let newsserverPage = document.getElementById("newsserver");
+ newsserverPage.addEventListener("pageshow", incomingPageInit);
+ newsserverPage.addEventListener("pageadvanced", incomingPageUnload);
+ newsserverPage.next = "accnamepage";
+ let accnamePage = document.getElementById("accnamepage");
+ accnamePage.addEventListener("pageshow", acctNamePageInit);
+ accnamePage.addEventListener("pageadvanced", acctNamePageUnload);
+ accnamePage.next = "done";
+ let donePage = document.getElementById("done");
+ donePage.addEventListener("pageshow", donePageInit);
+
+ gPrefsBundle = document.getElementById("bundle_prefs");
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ checkForInvalidAccounts();
+
+ // It is fine if there is no default account, this is expected the first
+ // time you launch mail on a new profile.
+ gDefaultAccount = MailServices.accounts.defaultAccount;
+
+ identityPageInit();
+}
+
+function onCancel() {
+ if ("ActivationOnCancel" in this && this.ActivationOnCancel()) {
+ return false;
+ }
+ var firstInvalidAccount = getInvalidAccounts(
+ MailServices.accounts.accounts
+ ).find(account => account.incomingServer.type == "nntp");
+ var closeWizard = true;
+
+ // if the user cancels the the wizard when it pops up because of
+ // an invalid account (example, a webmail account that activation started)
+ // we just force create it by setting some values and calling the FinishAccount()
+ // see bug #47521 for the full discussion
+ if (firstInvalidAccount) {
+ var pageData = GetPageData();
+ // set the fullName if it doesn't exist
+ if (!pageData.fullName) {
+ pageData.fullName = "";
+ }
+
+ // set the email if it doesn't exist
+ if (!pageData.email) {
+ pageData.email = "user@domain.invalid";
+ }
+
+ // call FinishAccount() and not onFinish(), since the "finish"
+ // button may be disabled
+ FinishAccount();
+ } else if (!MailServices.accounts.accounts.length) {
+ // since this is not an invalid account
+ // really cancel if the user hits the "cancel" button
+ // if the length of the account list is less than 1, there are no accounts
+ let confirmMsg = gPrefsBundle.getString("cancelWizard");
+ let confirmTitle = gPrefsBundle.getString("accountWizard");
+ let result = Services.prompt.confirmEx(
+ window,
+ confirmTitle,
+ confirmMsg,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ gPrefsBundle.getString("WizardExit"),
+ gPrefsBundle.getString("WizardContinue"),
+ null,
+ null,
+ { value: 0 }
+ );
+
+ if (result == 1) {
+ closeWizard = false;
+ }
+ }
+
+ return closeWizard;
+}
+
+function FinishAccount() {
+ try {
+ var pageData = GetPageData();
+
+ var accountData = gCurrentAccountData;
+
+ if (!accountData) {
+ accountData = {};
+ }
+
+ // we may need local folders before account is "Finished"
+ // if it's a pop3 account which defers to Local Folders.
+ verifyLocalFoldersAccount();
+
+ PageDataToAccountData(pageData, accountData);
+
+ // we might be simply finishing another account
+ if (!gCurrentAccount) {
+ gCurrentAccount = createAccount(accountData);
+ }
+
+ // transfer all attributes from the accountdata
+ finishAccount(gCurrentAccount, accountData);
+
+ setupCopiesAndFoldersServer(gCurrentAccount, accountData);
+
+ if (gCurrentAccount.incomingServer.canBeDefaultServer) {
+ EnableCheckMailAtStartUpIfNeeded(gCurrentAccount);
+ }
+
+ // in case we crash, force us a save of the prefs file NOW
+ try {
+ MailServices.accounts.saveAccountInfo();
+ } catch (ex) {
+ dump("Error saving account info: " + ex + "\n");
+ }
+ let openerWindow = window.opener.top;
+ // The following block is the same as in feedAccountWizard.js.
+ if ("selectServer" in openerWindow) {
+ // Opened from Account Settings.
+ openerWindow.selectServer(gCurrentAccount.incomingServer);
+ }
+
+ // Post a message to the main window on successful account setup.
+ openerWindow.postMessage("account-created", "*");
+
+ window.close();
+ } catch (ex) {
+ dump("FinishAccount failed, " + ex + "\n");
+ }
+}
+
+// prepopulate pageData with stuff from accountData
+// use: to prepopulate the wizard with account information
+function AccountDataToPageData(accountData, pageData) {
+ var server = accountData.incomingServer;
+ pageData.hostname = server.hostName;
+ pageData.prettyName = server.prettyName || "";
+
+ var identity;
+
+ if (accountData.identity) {
+ dump("This is an accountdata\n");
+ identity = accountData.identity;
+ } else if (accountData.identities) {
+ identity = accountData.identities[0];
+ dump("this is an account, id= " + identity + "\n");
+ }
+
+ pageData.email = identity.email || "";
+ pageData.fullName = identity.fullName || "";
+}
+
+// take data from each page of pageData and dump it into accountData
+// use: to put results of wizard into a account-oriented object
+function PageDataToAccountData(pageData, accountData) {
+ if (!accountData.identity) {
+ accountData.identity = {};
+ }
+ if (!accountData.incomingServer) {
+ accountData.incomingServer = {};
+ }
+
+ var identity = accountData.identity;
+ var server = accountData.incomingServer;
+
+ if (pageData.email) {
+ identity.email = pageData.email;
+ }
+ if (pageData.fullName) {
+ identity.fullName = pageData.fullName;
+ }
+
+ server.hostName = pageData.hostname;
+ if (pageData.prettyName) {
+ server.prettyName = pageData.prettyName;
+ }
+}
+
+// given an accountData structure, create an account
+// (but don't fill in any fields, that's for finishAccount()
+function createAccount(accountData) {
+ let hostName = accountData.incomingServer.hostName;
+ // If we're here, the server must not be associated with any account, so reuse
+ // it.
+ let server = NntpUtils.findServer(hostName);
+
+ if (!server) {
+ dump(`MailServices.accounts.createIncomingServer(${hostName})\n`);
+ // Create a (actual) server.
+ server = MailServices.accounts.createIncomingServer(null, hostName, "nntp");
+ }
+
+ dump("MailServices.accounts.createAccount()\n");
+ // Create an account.
+ let account = MailServices.accounts.createAccount();
+
+ // only create an identity for this account if we really have one
+ // (use the email address as a check)
+ if (accountData.identity && accountData.identity.email) {
+ dump("MailServices.accounts.createIdentity()\n");
+ // Create an identity.
+ let identity = MailServices.accounts.createIdentity();
+
+ // New nntp identities should use plain text by default;
+ // we want that GNKSA (The Good Net-Keeping Seal of Approval).
+ identity.composeHtml = false;
+
+ account.addIdentity(identity);
+ }
+
+ // we mark the server as invalid so that the account manager won't
+ // tell RDF about the new server - it's not quite finished getting
+ // set up yet, in particular, the deferred storage pref hasn't been set.
+ server.valid = false;
+ // Set the new account to use the new server.
+ account.incomingServer = server;
+ server.valid = true;
+ return account;
+}
+
+// given an accountData structure, copy the data into the
+// given account, incoming server, and so forth
+function finishAccount(account, accountData) {
+ if (accountData.incomingServer) {
+ var destServer = account.incomingServer;
+ var srcServer = accountData.incomingServer;
+ copyObjectToInterface(destServer, srcServer, true);
+
+ // See if there are any protocol-specific attributes.
+ // If so, we use the type to get the IID, QueryInterface
+ // as appropriate, then copy the data over.
+ const typeProperty = "ServerType-" + srcServer.type;
+ let serverAttrs =
+ typeProperty in srcServer ? srcServer[typeProperty] : null;
+ dump(`srcServer.${typeProperty} = ${serverAttrs}\n`);
+ if (serverAttrs) {
+ // handle server-specific stuff
+ var IID;
+ try {
+ IID = destServer.protocolInfo.serverIID;
+ } catch (ex) {
+ console.error("Could not get IID for " + srcServer.type + ": " + ex);
+ }
+
+ if (IID) {
+ let destProtocolServer = destServer.QueryInterface(IID);
+ let srcProtocolServer = srcServer["ServerType-" + srcServer.type];
+
+ dump("Copying over " + srcServer.type + "-specific data\n");
+ copyObjectToInterface(destProtocolServer, srcProtocolServer, false);
+ }
+ }
+
+ account.incomingServer.valid = true;
+ // hack to cause an account loaded notification now the server is valid
+ account.incomingServer = account.incomingServer; // eslint-disable-line no-self-assign
+ }
+
+ // copy identity info
+ var destIdentity = account.identities.length ? account.identities[0] : null;
+
+ if (destIdentity) {
+ // does this account have an identity?
+ if (accountData.identity && accountData.identity.email) {
+ // fixup the email address if we have a default domain
+ let emailArray = accountData.identity.email.split("@");
+ if (emailArray.length < 2 && accountData.domain) {
+ accountData.identity.email += "@" + accountData.domain;
+ }
+
+ copyObjectToInterface(destIdentity, accountData.identity, true);
+ destIdentity.valid = true;
+ }
+
+ /**
+ * If signature file need to be set, get the path to the signature file.
+ * Signature files, if exist, are placed under default location. Get
+ * default files location for messenger using directory service. Signature
+ * file name should be extracted from the account data to build the complete
+ * path for signature file. Once the path is built, set the identity's signature pref.
+ */
+ if (destIdentity.attachSignature) {
+ var sigFileName = accountData.signatureFileName;
+ let sigFile = MailServices.mailSession.getDataFilesDir("messenger");
+ sigFile.append(sigFileName);
+ destIdentity.signature = sigFile;
+ }
+ } // if the account has an identity...
+
+ if (this.FinishAccountHook != undefined) {
+ this.FinishAccountHook(accountData.domain);
+ }
+}
+
+// Helper method used by copyObjectToInterface which attempts to set dest[attribute] as a generic
+// attribute on the xpconnect object, src.
+// This routine skips any attribute that begins with ServerType-
+function setGenericAttribute(dest, src, attribute) {
+ if (!attribute.toLowerCase().startsWith("servertype-") && src[attribute]) {
+ switch (typeof src[attribute]) {
+ case "string":
+ dest.setUnicharAttribute(attribute, src[attribute]);
+ break;
+ case "boolean":
+ dest.setBoolAttribute(attribute, src[attribute]);
+ break;
+ case "number":
+ dest.setIntAttribute(attribute, src[attribute]);
+ break;
+ default:
+ dump(
+ "Error: No Generic attribute " +
+ attribute +
+ " found for: " +
+ dest +
+ "\n"
+ );
+ break;
+ }
+ }
+}
+
+// copy over all attributes from dest into src that already exist in src
+// the assumption is that src is an XPConnect interface full of attributes
+// @param useGenericFallback if we can't set an attribute directly on src, then fall back
+// and try setting it generically. This assumes that src supports setIntAttribute, setUnicharAttribute
+// and setBoolAttribute.
+function copyObjectToInterface(dest, src, useGenericFallback) {
+ if (!dest) {
+ return;
+ }
+ if (!src) {
+ return;
+ }
+
+ var attribute;
+ for (attribute in src) {
+ if (dest.__lookupSetter__(attribute)) {
+ if (dest[attribute] != src[attribute]) {
+ dest[attribute] = src[attribute];
+ }
+ } else if (useGenericFallback) {
+ // fall back to setting the attribute generically
+ setGenericAttribute(dest, src, attribute);
+ }
+ } // for each attribute in src we want to copy
+}
+
+// check if there already is a "Local Folders"
+// if not, create it.
+function verifyLocalFoldersAccount() {
+ var localMailServer = null;
+ try {
+ localMailServer = MailServices.accounts.localFoldersServer;
+ } catch (ex) {
+ // dump("exception in findserver: " + ex + "\n");
+ localMailServer = null;
+ }
+
+ try {
+ if (!localMailServer) {
+ // dump("Creating local mail account\n");
+ // creates a copy of the identity you pass in
+ MailServices.accounts.createLocalMailAccount();
+ try {
+ localMailServer = MailServices.accounts.localFoldersServer;
+ } catch (ex) {
+ dump(
+ "error! we should have found the local mail server after we created it.\n"
+ );
+ localMailServer = null;
+ }
+ }
+ } catch (ex) {
+ dump("Error in verifyLocalFoldersAccount" + ex + "\n");
+ }
+}
+
+function setupCopiesAndFoldersServer(account, accountData) {
+ try {
+ var server = account.incomingServer;
+
+ if (!account.identities.length) {
+ return false;
+ }
+
+ let identity = account.identities[0];
+ // For this server, do we default the folder prefs to this server, or to the "Local Folders" server
+ // If it's deferred, we use the local folders account.
+ var defaultCopiesAndFoldersPrefsToServer =
+ server.defaultCopiesAndFoldersPrefsToServer;
+
+ var copiesAndFoldersServer = null;
+ if (defaultCopiesAndFoldersPrefsToServer) {
+ copiesAndFoldersServer = server;
+ } else {
+ if (!MailServices.accounts.localFoldersServer) {
+ dump("error! we should have a local mail server at this point\n");
+ return false;
+ }
+ copiesAndFoldersServer = MailServices.accounts.localFoldersServer;
+ }
+
+ setDefaultCopiesAndFoldersPrefs(
+ identity,
+ copiesAndFoldersServer,
+ accountData
+ );
+ } catch (ex) {
+ // return false (meaning we did not setupCopiesAndFoldersServer)
+ // on any error
+ dump("Error in setupCopiesAndFoldersServer: " + ex + "\n");
+ return false;
+ }
+ return true;
+}
+
+function setDefaultCopiesAndFoldersPrefs(identity, server, accountData) {
+ var rootFolder = server.rootFolder;
+
+ // we need to do this or it is possible that the server's draft,
+ // stationery fcc folder will not be in rdf
+ //
+ // this can happen in a couple cases
+ // 1) the first account we create, creates the local mail. since
+ // local mail was just created, it obviously hasn't been opened,
+ // or in rdf..
+ // 2) the account we created is of a type where
+ // defaultCopiesAndFoldersPrefsToServer is true
+ // this since we are creating the server, it obviously hasn't been
+ // opened, or in rdf.
+ //
+ // this makes the assumption that the server's draft, stationery fcc folder
+ // are at the top level (ie subfolders of the root folder.) this works
+ // because we happen to be doing things that way, and if the user changes
+ // that, it will work because to change the folder, it must be in rdf,
+ // coming from the folder cache, in the worst case.
+ var msgFolder = rootFolder.QueryInterface(Ci.nsIMsgFolder);
+
+ /**
+ * When a new account is created, folders 'Sent', 'Drafts'
+ * and 'Templates' are not created then, but created on demand at runtime.
+ * But we do need to present them as possible choices in the Copies and Folders
+ * UI. To do that, folder URIs have to be created and stored in the prefs file.
+ * So, if there is a need to build special folders, append the special folder
+ * names and create right URIs.
+ */
+ var folderDelim = "/";
+
+ /* we use internal names known to everyone like Sent, Templates and Drafts */
+ /* if folder names were already given in isp rdf, we use them,
+ otherwise we use internal names known to everyone like Sent, Templates and Drafts */
+
+ // Note the capital F, D and S!
+ var draftFolder =
+ accountData.identity && accountData.identity.DraftFolder
+ ? accountData.identity.DraftFolder
+ : "Drafts";
+ var stationeryFolder =
+ accountData.identity && accountData.identity.StationeryFolder
+ ? accountData.identity.StationeryFolder
+ : "Templates";
+ var fccFolder =
+ accountData.identity && accountData.identity.FccFolder
+ ? accountData.identity.FccFolder
+ : "Sent";
+
+ identity.draftFolder = msgFolder.server.serverURI + folderDelim + draftFolder;
+ identity.stationeryFolder =
+ msgFolder.server.serverURI + folderDelim + stationeryFolder;
+ identity.fccFolder = msgFolder.server.serverURI + folderDelim + fccFolder;
+
+ // Note the capital F, D and S!
+ identity.fccFolderPickerMode =
+ accountData.identity && accountData.identity.FccFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+ identity.draftsFolderPickerMode =
+ accountData.identity && accountData.identity.DraftFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+ identity.tmplFolderPickerMode =
+ accountData.identity && accountData.identity.StationeryFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+}
+
+function checkForInvalidAccounts() {
+ var firstInvalidAccount = getInvalidAccounts(
+ MailServices.accounts.accounts
+ ).find(account => account.incomingServer.type == "nntp");
+
+ if (firstInvalidAccount) {
+ var pageData = GetPageData();
+ dump(
+ "We have an invalid account, " +
+ firstInvalidAccount +
+ ", let's use that!\n"
+ );
+ gCurrentAccount = firstInvalidAccount;
+
+ var accountData = {};
+ accountData.incomingServer = firstInvalidAccount.incomingServer;
+ accountData.identity = firstInvalidAccount.identities[0];
+ AccountDataToPageData(accountData, pageData);
+
+ gCurrentAccountData = accountData;
+ }
+}
+
+function getUsernameFromEmail(email) {
+ return email && email.substr(0, email.indexOf("@"));
+}
+
+function GetPageData() {
+ if (!gPageData) {
+ gPageData = {};
+ }
+
+ return gPageData;
+}
+
+// flush the XUL cache - just for debugging purposes - not called
+function onFlush() {
+ Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", true);
+ Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", false);
+}
+
+/** If there are no default accounts..
+ * this is will be the new default, so enable
+ * check for mail at startup
+ */
+function EnableCheckMailAtStartUpIfNeeded(newAccount) {
+ // Check if default account existed.
+ // If no such account, make this one the default account
+ // and turn on the new mail check at startup for the current account
+ if (!gDefaultAccount) {
+ MailServices.accounts.defaultAccount = newAccount;
+ newAccount.incomingServer.loginAtStartUp = true;
+ newAccount.incomingServer.downloadOnBiff = true;
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/AccountWizard.xhtml b/comm/mailnews/base/prefs/content/AccountWizard.xhtml
new file mode 100644
index 0000000000..b58db60f60
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountWizard.xhtml
@@ -0,0 +1,161 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!-- NOTE: This Account Wizard is *only* for Newsgroup accounts. Historically,
+ - it was a generic Account Wizard, hence the generic naming. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountWizard.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % AccountWizardDTD SYSTEM "chrome://messenger/locale/AccountWizard.dtd">
+%AccountWizardDTD;
+<!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd">
+%newsblogDTD;
+]>
+
+<window id="AccountWizard" title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="onAccountWizardLoad();"
+ style="width: 40em; height: 38em;"
+ lightweightthemes="true">
+ <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ </linkset>
+
+ <script src="chrome://messenger/content/globalOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+ <script src="chrome://messenger/content/accountUtils.js"/>
+ <script src="chrome://messenger/content/amUtils.js"/>
+ <script src="chrome://messenger/content/AccountWizard.js"/>
+ <script src="chrome://messenger/content/aw-identity.js"/>
+ <script src="chrome://messenger/content/aw-incoming.js"/>
+ <script src="chrome://messenger/content/aw-accname.js"/>
+ <script src="chrome://messenger/content/aw-done.js"/>
+
+ <wizard>
+ <!-- Identity page : Collects user's full name and email address -->
+ <wizardpage id="identitypage" pageid="identitypage"
+ label="&identityTitle.label;">
+ <vbox>
+ <description flex="1">&identityDesc.label;</description>
+ <separator/>
+ <description flex="1">&fullnameDesc.label; &fullnameExample.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="fullnameLabel" class="awIdentityLabel" value="&fullnameLabel.label;"
+ accesskey="&fullnameLabel.accesskey;" control="fullName"/>
+ <html:input id="fullName"
+ type="text"
+ required="required"
+ wsm_persist="true"
+ name="fullName"
+ class="input-inline"
+ aria-labelledby="fullnameLabel"
+ oninput="identityPageValidate();"/>
+ </hbox>
+ <separator/>
+ <vbox>
+ <hbox>
+ <description id="emailDescText" flex="1"/>
+ </hbox>
+ <hbox align="center">
+ <label id="emailFieldLabel"
+ class="awIdentityLabel"
+ value="&emailLabel.label;"
+ accesskey="&emailLabel.accesskey;"
+ control="email"/>
+ <hbox class="uri-element input-container" align="center" flex="1">
+ <html:input id="email"
+ type="email"
+ required="required"
+ wsm_persist="true"
+ name="email"
+ aria-labelledby="emailFieldLabel"
+ oninput="identityPageValidate();"
+ class="uri-element input-inline"/>
+ </hbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- News Server page : Collects the News groups server name -->
+ <wizardpage id="newsserver" pageid="newsserver"
+ label="&incomingTitle.label;">
+ <vbox flex="1">
+ <description>&newsServerNameDesc.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="newsServerLabel" control="newsServer"
+ value="&newsServerLabel.label;"
+ accesskey="&newsServerLabel.accesskey;"
+ style="width: 8em;"/>
+ <html:input id="newsServer"
+ type="text"
+ wsm_persist="true"
+ class="uri-element input-inline"
+ aria-labelledby="newsServerLabel"
+ oninput="incomingPageValidate();"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Account name page : User gets a choice to enter a pretty name for the account -->
+ <!-- Defaults : Mail account -> Email address, Newsgroup account -> Newsgroup server name -->
+ <wizardpage id="accnamepage" pageid="accnamepage"
+ label="&accnameTitle.label;">
+ <vbox flex="1">
+ <description>&accnameDesc.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="prettyNameLabel" class="label"
+ value="&accnameLabel.label;"
+ style="width: 8em;"
+ accesskey="&accnameLabel.accesskey;"
+ control="prettyName"/>
+ <html:input id="prettyName"
+ type="text"
+ size="40"
+ wsm_persist="true"
+ class="input-inline"
+ aria-labelledby="prettyNameLabel"
+ oninput="acctNamePageValidate();"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Done page : this page summarizes information collected to create a mail/news account -->
+ <wizardpage id="done" pageid="done"
+ label="&completionTitle.label;">
+ <vbox flex="1">
+ <description>&completionText.label;</description>
+ <separator class="thin"/>
+ <html:div class="grid-two-column-fr grid-items-baseline">
+ <label id="account.name.label" flex="1" class="label" value="&accnameLabel.label;"/>
+ <label id="account.name.text" flex="1" class="label"/>
+ <label id="identity.email.label" flex="1" class="label" value="&emailLabel.label;"/>
+ <label id="identity.email.text" flex="1" class="label"/>
+ <label id="server.username.label" flex="1" class="label" value="&incomingUsername.label;"/>
+ <label id="server.username.text" flex="1" class="label"/>
+ <label id="newsServer.name.label" flex="1" class="label" value="&newsServerNamePrefix.label;"/>
+ <label id="newsServer.name.text" flex="1" class="label"/>
+ </html:div>
+ <separator/>
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <description>&clickFinish.label;</description>
+#else
+ <description>&clickFinish.labelMac;</description>
+#endif
+ </vbox>
+ </wizardpage>
+ </wizard>
+</window>
diff --git a/comm/mailnews/base/prefs/content/SmtpServerEdit.js b/comm/mailnews/base/prefs/content/SmtpServerEdit.js
new file mode 100644
index 0000000000..56d6076c5f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/SmtpServerEdit.js
@@ -0,0 +1,241 @@
+/* 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 { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+
+var gSmtpServer;
+var gSmtpUsername;
+var gSmtpDescription;
+var gSmtpUsernameLabel;
+var gSmtpHostname;
+var gSmtpPort;
+var gSmtpAuthMethod;
+var gSmtpSocketType;
+var gPort;
+var gDefaultPort;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onAccept);
+
+function onLoad() {
+ gSmtpServer = window.arguments[0].server;
+ initSmtpSettings(gSmtpServer);
+}
+
+function onAccept(event) {
+ if (!isLegalHostNameOrIP(cleanUpHostName(gSmtpHostname.value))) {
+ let prefsBundle = document.getElementById("bundle_prefs");
+ let brandBundle = document.getElementById("bundle_brand");
+ let alertTitle = brandBundle.getString("brandShortName");
+ let alertMsg = prefsBundle.getString("enterValidServerName");
+ Services.prompt.alert(window, alertTitle, alertMsg);
+
+ window.arguments[0].result = false;
+ event.preventDefault();
+ return;
+ }
+
+ // If we didn't have an SMTP server to initialize with,
+ // we must be creating one.
+ try {
+ if (!gSmtpServer) {
+ gSmtpServer = MailServices.smtp.createServer();
+ window.arguments[0].addSmtpServer = gSmtpServer.key;
+ }
+
+ saveSmtpSettings(gSmtpServer);
+ } catch (ex) {
+ console.error("Error saving smtp server: " + ex);
+ }
+
+ window.arguments[0].result = true;
+}
+
+function initSmtpSettings(server) {
+ gSmtpUsername = document.getElementById("smtpUsername");
+ gSmtpDescription = document.getElementById("smtp.description");
+ gSmtpUsernameLabel = document.getElementById("smtpUsernameLabel");
+ gSmtpHostname = document.getElementById("smtp.hostname");
+ gSmtpPort = document.getElementById("smtp.port");
+ gSmtpAuthMethod = document.getElementById("smtp.authMethod");
+ gSmtpSocketType = document.getElementById("smtp.socketType");
+ gDefaultPort = document.getElementById("smtp.defaultPort");
+ gPort = document.getElementById("smtp.port");
+
+ if (server) {
+ gSmtpHostname.value = server.hostname;
+ gSmtpDescription.value = server.description;
+ gSmtpPort.value = server.port;
+ gSmtpUsername.value = server.username;
+ gSmtpAuthMethod.value = server.authMethod;
+ gSmtpSocketType.value = server.socketType < 4 ? server.socketType : 1;
+ } else {
+ // New server, load default values.
+ gSmtpAuthMethod.value = Services.prefs.getIntPref(
+ "mail.smtpserver.default.authMethod"
+ );
+ gSmtpSocketType.value = Services.prefs.getIntPref(
+ "mail.smtpserver.default.try_ssl"
+ );
+ }
+
+ // Although sslChanged will set a label for cleartext password,
+ // we need to use the long label so that we can size the dialog.
+ setLabelFromStringBundle("authMethod-no", "authNo");
+ setLabelFromStringBundle(
+ "authMethod-password-encrypted",
+ "authPasswordEncrypted"
+ );
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ "authPasswordCleartextInsecurely"
+ );
+ setLabelFromStringBundle("authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("authMethod-oauth2", "authOAuth2");
+ setLabelFromStringBundle("authMethod-anysecure", "authAnySecure");
+ setLabelFromStringBundle("authMethod-any", "authAny");
+
+ window.sizeToContent();
+
+ sslChanged(false);
+ authMethodChanged(false);
+
+ if (MailServices.smtp.defaultServer) {
+ onLockPreference();
+ }
+
+ // Hide OAuth2 option if we can't use it.
+ let details = server
+ ? OAuth2Providers.getHostnameDetails(server.hostname)
+ : null;
+ document.getElementById("authMethod-oauth2").hidden = !details;
+
+ // Hide deprecated/hidden auth options, unless selected
+ hideUnlessSelected(document.getElementById("authMethod-anysecure"));
+ hideUnlessSelected(document.getElementById("authMethod-any"));
+
+ // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't
+ // allow users to choose it anymore. Hide the option unless the user already
+ // has it set.
+ hideUnlessSelected(document.getElementById("connectionSecurityType-1"));
+}
+
+function hideUnlessSelected(element) {
+ element.hidden = !element.selected;
+}
+
+function setLabelFromStringBundle(elementID, stringName) {
+ document.getElementById(elementID).label = document
+ .getElementById("bundle_messenger")
+ .getString(stringName);
+}
+
+// Disables xul elements that have associated preferences locked.
+function onLockPreference() {
+ try {
+ let allPrefElements = {
+ hostname: gSmtpHostname,
+ description: gSmtpDescription,
+ port: gSmtpPort,
+ authMethod: gSmtpAuthMethod,
+ try_ssl: gSmtpSocketType,
+ };
+ disableIfLocked(allPrefElements);
+ } catch (e) {
+ // non-fatal
+ console.error("Error while getting locked prefs: " + e);
+ }
+}
+
+/**
+ * Does the work of disabling an element given the array which contains
+ * id/prefstring pairs.
+ *
+ * @param {Element[]} prefstrArray - Elements to check.
+ *
+ * TODO: try to merge this with disableIfLocked function in am-offline.js (bug 755885)
+ */
+function disableIfLocked(prefstrArray) {
+ let smtpPrefBranch = Services.prefs.getBranch(
+ "mail.smtpserver." + MailServices.smtp.defaultServer.key + "."
+ );
+
+ for (let prefstring in prefstrArray) {
+ if (smtpPrefBranch.prefIsLocked(prefstring)) {
+ prefstrArray[prefstring].disabled = true;
+ }
+ }
+}
+
+function saveSmtpSettings(server) {
+ if (server) {
+ server.hostname = cleanUpHostName(gSmtpHostname.value);
+ server.description = gSmtpDescription.value;
+ server.port = gSmtpPort.value;
+ server.authMethod = gSmtpAuthMethod.value;
+ server.username = gSmtpUsername.value;
+ server.socketType = gSmtpSocketType.value;
+ }
+}
+
+function authMethodChanged(userAction) {
+ var noUsername = gSmtpAuthMethod.value == Ci.nsMsgAuthMethod.none;
+ gSmtpUsername.disabled = noUsername;
+ gSmtpUsernameLabel.disabled = noUsername;
+}
+
+/**
+ * Resets the default port to SMTP or SMTPS, dependending on
+ * the |gSmtpSocketType| value, and sets the port to use to this default,
+ * if that's appropriate.
+ *
+ * @param {boolean} userAction - false for dialog initialization,
+ * true for user action.
+ */
+function sslChanged(userAction) {
+ const DEFAULT_SMTP_PORT = "587";
+ const DEFAULT_SMTPS_PORT = "465";
+ var socketType = gSmtpSocketType.value;
+ var otherDefaultPort;
+ var prevDefaultPort = gDefaultPort.value;
+
+ if (socketType == Ci.nsMsgSocketType.SSL) {
+ gDefaultPort.value = DEFAULT_SMTPS_PORT;
+ otherDefaultPort = DEFAULT_SMTP_PORT;
+ } else {
+ gDefaultPort.value = DEFAULT_SMTP_PORT;
+ otherDefaultPort = DEFAULT_SMTPS_PORT;
+ }
+
+ // If the port is not set,
+ // or the user is causing the default port to change,
+ // and the port is set to the default for the other protocol,
+ // then set the port to the default for the new protocol.
+ if (
+ gPort.value == 0 ||
+ (userAction &&
+ gDefaultPort.value != prevDefaultPort &&
+ gPort.value == otherDefaultPort)
+ ) {
+ gPort.value = gDefaultPort.value;
+ }
+
+ // switch "insecure password" label
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ socketType == Ci.nsMsgSocketType.SSL ||
+ socketType == Ci.nsMsgSocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml b/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml
new file mode 100644
index 0000000000..d0bbda97c6
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml
@@ -0,0 +1,208 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/smtpEditOverlay.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&smtpEditTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/SmtpServerEdit.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog style="width: 100vw; height: 100vh">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox id="smtpServerEditor">
+ <html:div>
+ <html:fieldset>
+ <html:legend>&settings.caption;</html:legend>
+ <hbox flex="1">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtp.description.label"
+ value="&serverDescription.label;"
+ accesskey="&serverDescription.accesskey;"
+ control="smtp.description"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtp.hostname.label"
+ value="&serverName.label;"
+ accesskey="&serverName.accesskey;"
+ control="smtp.hostname"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&serverPort.label;"
+ accesskey="&serverPort.accesskey;"
+ control="smtp.port"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <html:input
+ id="smtp.description"
+ type="text"
+ preftype="string"
+ class="input-inline"
+ prefstring="mail.smtpserver.%serverkey%.description"
+ aria-labelledby="smtp.description.label"
+ />
+ <html:input
+ id="smtp.hostname"
+ type="text"
+ preftype="string"
+ class="uri-element input-inline"
+ prefstring="mail.smtpserver.%serverkey%.hostname"
+ aria-labelledby="smtp.hostname.label"
+ />
+ <hbox align="center">
+ <html:input
+ id="smtp.port"
+ type="number"
+ class="size5 input-inline"
+ min="0"
+ max="65535"
+ preftype="int"
+ prefstring="mail.smtpserver.%serverkey%.port"
+ />
+ <label value="&serverPortDefault.label;" />
+ <label id="smtp.defaultPort" />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&security.caption;</html:legend>
+
+ <hbox flex="1">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&connectionSecurity.label;"
+ accesskey="&connectionSecurity.accesskey;"
+ control="smtp.socketType"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&authMethod.label;"
+ accesskey="&authMethod.accesskey;"
+ control="server.authMethod"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtpUsernameLabel"
+ value="&userName.label;"
+ accesskey="&userName.accesskey;"
+ control="smtpUsername"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <menulist
+ id="smtp.socketType"
+ oncommand="sslChanged(true);"
+ prefstring="mail.smtpserver.%serverkey%.try_ssl"
+ >
+ <menupopup id="smtp.socketTypePopup">
+ <menuitem
+ value="0"
+ label="&connectionSecurityType-0.label;"
+ />
+ <menuitem
+ id="connectionSecurityType-1"
+ value="1"
+ label="&connectionSecurityType-1.label;"
+ disabled="true"
+ hidden="true"
+ />
+ <menuitem
+ value="2"
+ label="&connectionSecurityType-2.label;"
+ />
+ <menuitem
+ value="3"
+ label="&connectionSecurityType-3.label;"
+ />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="smtp.authMethod"
+ oncommand="authMethodChanged(true);"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.smtpserver.%serverkey%.authMethod"
+ >
+ <menupopup id="smtp.authMethodPopup">
+ <menuitem id="authMethod-no" value="1" />
+ <menuitem id="authMethod-password-cleartext" value="3" />
+ <menuitem id="authMethod-password-encrypted" value="4" />
+ <menuitem id="authMethod-kerberos" value="5" />
+ <menuitem id="authMethod-ntlm" value="6" />
+ <menuitem id="authMethod-oauth2" value="10" />
+ <menuitem id="authMethod-anysecure" value="8" />
+ <menuitem id="authMethod-any" value="9" />
+ </menupopup>
+ </menulist>
+ <hbox class="input-container">
+ <html:input
+ id="smtpUsername"
+ type="text"
+ class="input-inline"
+ preftype="string"
+ prefstring="mail.smtpserver.%serverkey%.username"
+ aria-labelledby="smtpUsernameLabel"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/accountUtils.js b/comm/mailnews/base/prefs/content/accountUtils.js
new file mode 100644
index 0000000000..8af2e72cf3
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/accountUtils.js
@@ -0,0 +1,369 @@
+/* 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/. */
+
+/* import-globals-from AccountManager.js */
+/* globals openTab */ // From utilityOverlay.js
+/* globals SelectFolder */ // From messageWindow.js or messenger.js.
+/* globals MsgGetMessage */ // From mailWindowOverlay.js.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gAnyValidIdentity = false; // If there are no valid identities for any account
+// returns the first account with an invalid server or identity
+
+var gNewAccountToLoad = null; // used to load new messages if we come from the mail3pane
+
+function getInvalidAccounts(accounts) {
+ let invalidAccounts = [];
+ for (let account of accounts) {
+ try {
+ if (!account.incomingServer.valid) {
+ invalidAccounts.push(account);
+ // skip to the next account
+ continue;
+ }
+ } catch (ex) {
+ // this account is busted, just keep going
+ continue;
+ }
+
+ for (let identity of account.identities) {
+ if (identity.valid) {
+ gAnyValidIdentity = true;
+ } else {
+ invalidAccounts.push(account);
+ }
+ }
+ }
+ return invalidAccounts;
+}
+
+function showMailIntegrationDialog() {
+ const nsIShellService = Ci.nsIShellService;
+
+ try {
+ var shellService =
+ Cc["@mozilla.org/suite/shell-service;1"].getService(nsIShellService);
+ var appTypesCheck =
+ shellService.shouldBeDefaultClientFor &
+ (nsIShellService.MAIL | nsIShellService.NEWS);
+
+ // show the default client dialog only if we have at least one account,
+ // if we should check for the default client, and we want to check if we are
+ // the default for mail/news and are not the default client for mail/news
+ if (
+ appTypesCheck &&
+ shellService.shouldCheckDefaultClient &&
+ !shellService.isDefaultClient(true, appTypesCheck)
+ ) {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://communicator/content/defaultClientDialog.xhtml",
+ "DefaultClient",
+ "modal,centerscreen,chrome,resizable=no"
+ );
+ }
+ } catch (ex) {}
+}
+
+/**
+ * Check that an account exists which requires Local Folders.
+ *
+ * @returns {boolean} - true if at least 1 account exists that requires
+ * Local Folders, else false.
+ */
+function requireLocalFoldersAccount() {
+ return MailServices.accounts.accounts.some(account =>
+ ["imap", "pop3", "nntp"].includes(account.incomingServer?.type)
+ );
+}
+
+/**
+ * Open the Nntp Account Wizard, or focus it if it's already open.
+ */
+function openNewsgroupAccountWizard() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/AccountWizard.xhtml",
+ "AccountWizard",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+function AddIMAccount() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/chat/imAccountWizard.xhtml",
+ "",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+function AddFeedAccount() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger-newsblog/content/feedAccountWizard.xhtml",
+ "",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+/**
+ * Opens Address Book tab and triggers the address book creation dialog based on
+ * the passed type.
+ *
+ * @param {string} type - The address book type needing creation. Accepted types
+ * are "JS", "LDAP", and "CARDDAV".
+ */
+function addNewAddressBook(type) {
+ window.browsingContext.topChromeWindow.toAddressBook({
+ action: `create_ab_${type}`,
+ });
+}
+
+function showCalendarWizard() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://calendar/content/calendar-creation.xhtml",
+ "caEditServer",
+ "chrome,titlebar,resizable,centerscreen",
+ {}
+ );
+}
+
+/**
+ * Opens the account settings window on the specified account
+ * and page of settings. If the window is already open it is only focused.
+ *
+ * @param {?string} selectPage - The file name for the viewing page, or null for
+ * the account main page. Other pages are 'am-server.xhtml',
+ * 'am-copies.xhtml', 'am-offline.xhtml', 'am-addressing.xhtml',
+ * 'am-smtp.xhtml'
+ * @param {nsIMsgIncomingServer} [server] - The server of the account to select.
+ */
+async function MsgAccountManager(selectPage, server) {
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!win) {
+ // No window available, so force open a new one.
+ openTab(
+ "contentTab",
+ {
+ url: "about:accountsettings",
+ onLoad(event, browser) {
+ browser.contentDocument.documentElement.server = server;
+ browser.contentDocument.documentElement.selectPage = selectPage;
+ browser.contentDocument.getElementById("accounttree").focus();
+ },
+ },
+ "window"
+ );
+ return;
+ }
+
+ let tabmail = win.document.getElementById("tabmail");
+ // If the server wasn't specified, and we have the window open, try
+ // and use the currently selected folder to work out the server to select.
+ if (!server) {
+ server = tabmail.currentAbout3Pane?.gFolder ?? null;
+ }
+ // If the server is still not found, account settings will default to
+ // the first account.
+
+ // If Account settings tab is already open, change the server
+ // and the selected page, reload the tab and switch to the tab.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsettings") {
+ tab.browser.contentDocument.documentElement.server = server;
+ tab.browser.contentDocument.documentElement.selectPage = selectPage;
+ tab.browser.contentWindow.onLoad();
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ // Else fallback to opening a new tab in the window.
+ tabmail.openTab("contentTab", {
+ url: "about:accountsettings",
+ onLoad(event, browser) {
+ browser.contentDocument.documentElement.server = server;
+ browser.contentDocument.documentElement.selectPage = selectPage;
+ browser.contentDocument.getElementById("accounttree").focus();
+ },
+ });
+}
+
+function loadInboxForNewAccount() {
+ // gNewAccountToLoad is set in the final screen of the Account Wizard if a POP account
+ // was created, the download messages box is checked, and the wizard was opened from the 3pane
+ if (gNewAccountToLoad) {
+ var rootMsgFolder = gNewAccountToLoad.incomingServer.rootMsgFolder;
+ const kInboxFlag = Ci.nsMsgFolderFlags.Inbox;
+ var inboxFolder = rootMsgFolder.getFolderWithFlags(kInboxFlag);
+ SelectFolder(inboxFolder.URI);
+ window.focus();
+ setTimeout(MsgGetMessage, 0);
+ gNewAccountToLoad = null;
+ }
+}
+
+// returns true if we migrated - it knows this because 4.x did not have the
+// pref mailnews.quotingPrefs.version, so if it's not set, we're either
+// migrating from 4.x, or a much older version of Mozilla.
+function migrateGlobalQuotingPrefs(allIdentities) {
+ // if reply_on_top and auto_quote exist then, if non-default
+ // migrate and delete, if default just delete.
+ var reply_on_top = 0;
+ var auto_quote = true;
+ var quotingPrefs = Services.prefs.getIntPref(
+ "mailnews.quotingPrefs.version",
+ 0
+ );
+ var migrated = false;
+
+ // If the quotingPrefs version is 0 then we need to migrate our preferences
+ if (quotingPrefs == 0) {
+ migrated = true;
+ try {
+ reply_on_top = Services.prefs.getIntPref("mailnews.reply_on_top");
+ auto_quote = Services.prefs.getBoolPref("mail.auto_quote");
+ } catch (ex) {}
+
+ if (!auto_quote || reply_on_top) {
+ for (let identity of allIdentities) {
+ if (identity.valid) {
+ identity.autoQuote = auto_quote;
+ identity.replyOnTop = reply_on_top;
+ }
+ }
+ }
+ Services.prefs.setIntPref("mailnews.quotingPrefs.version", 1);
+ }
+ return migrated;
+}
+
+/**
+ * Open the Account Setup Tab or focus it if it's already open.
+ */
+function openAccountSetupTab() {
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsetup") {
+ let accountSetup = tabInfo.browser.contentWindow.gAccountSetup;
+ // Reset the entire UI only if the previously opened setup was completed.
+ if (accountSetup._currentModename == "success") {
+ accountSetup.resetSetup();
+ }
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ tabmail.openTab("contentTab", { url: "about:accountsetup" });
+}
+
+/**
+ * Open the account setup tab and switch to the success view to show the newly
+ * created account, or show an error if the account wasn't created.
+ *
+ * @param {object} account - A newly created account.
+ * @param {string} name - The account name defined in the provider's website.
+ * @param {string} email - The newly created email address.
+ */
+function openAccountSetupTabWithAccount(account, name, email) {
+ // Define which actions we need to take after the account setup tab has been
+ // loaded and we have access to its objects.
+ let onTabLoaded = function (event, browser, account) {
+ let accountSetup = browser.contentWindow.gAccountSetup;
+
+ if (account) {
+ // Update the account setup variables before kicking off the success view
+ // which will start fetching linked services with these values.
+ accountSetup._realname = name;
+ accountSetup._email = email;
+ accountSetup._password = account.incomingServer.password;
+ accountSetup.showSuccessView(account);
+ return;
+ }
+
+ accountSetup.showErrorNotification("account-setup-provisioner-error");
+ };
+
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsetup") {
+ let accountSetup = tabInfo.browser.contentWindow.gAccountSetup;
+ // Reset the entire UI only if the previously opened setup was completed.
+ if (accountSetup._currentModename == "success") {
+ accountSetup.resetSetup();
+ }
+ tabmail.switchToTab(tabInfo);
+ onTabLoaded(null, tabInfo.browser, account);
+ return;
+ }
+ }
+
+ // Open the account setup tab.
+ tabmail.openTab("contentTab", {
+ url: "about:accountsetup",
+ onLoad(event, browser) {
+ onTabLoaded(event, browser, account);
+ },
+ });
+}
+
+/**
+ * Open the Account Provisioner Tab or focus it if it's already open.
+ */
+function openAccountProvisionerTab() {
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountprovisioner") {
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ tabmail.openTab("contentTab", { url: "about:accountprovisioner" });
+}
+
+/**
+ * Reveal the Folder Pane after an account creation callback.
+ */
+function updateMailPaneUI() {
+ // Nothing to update since no account has been created.
+ if (MailServices.accounts.accounts.length == 0) {
+ return;
+ }
+
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ // Set the folderPaneVisible to true in the tabmail to prevent collapsing
+ // on tab switch.
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+ tabmail.tabInfo[0].folderPaneVisible = true;
+}
+
+/**
+ * Open the OpenPGP Key Manager from outside the Account Settings.
+ */
+function openKeyManager() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://openpgp/content/ui/enigmailKeyManager.xhtml",
+ "enigmail:KeyManager",
+ "dialog,centerscreen,resizable",
+ {
+ cancelCallback: null,
+ okCallback: null,
+ }
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml b/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml
new file mode 100644
index 0000000000..44d393f4ed
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml
@@ -0,0 +1,125 @@
+# 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/.
+
+ <vbox flex="1" id="compositionAndAddressing">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&compositionGroupTitle.label;</html:legend>
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.composeHtml" label="&useHtml.label;"
+ accesskey="&useHtml.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.compose_html"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.autoQuote" label="&autoQuote.label;"
+ accesskey="&autoQuote.accesskey;"
+ pref="true" preftype="bool" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.auto_quote"/>
+ </hbox>
+ <hbox align="center">
+ <label value="&quoting.label;" accesskey="&quoting.accesskey;" control="identity.replyOnTop"/>
+ <menulist wsm_persist="true" id="identity.replyOnTop" oncommand="quoteEnabling();"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.reply_on_top">
+ <menupopup>
+ <menuitem value="1" label="&aboveQuote.label;"/>
+ <menuitem value="0" label="&belowQuote.label;"/>
+ <menuitem value="2" label="&selectAndQuote.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox class="indent" align="center" id="placeBox">
+ <label value="&place.label;" accesskey="&place.accesskey;" control="identity.sig_bottom"/>
+ <menulist wsm_persist="true" id="identity.sig_bottom" genericattr="true"
+ pref="true" preftype="bool" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.sig_bottom">
+ <menupopup>
+ <menuitem value="true" label="&belowText.label;"/>
+ <menuitem value="false" label="&aboveText.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <checkbox id="identity.sig_on_reply" wsm_persist="true"
+ label="&includeSigOnReply.label;"
+ accesskey="&includeSigOnReply.accesskey;"
+ preftype="bool" genericattr="true"
+ prefstring="mail.identity.%identitykey%.sig_on_reply"/>
+
+ <checkbox id="identity.sig_on_fwd" wsm_persist="true"
+ label="&includeSigOnForward.label;"
+ accesskey="&includeSigOnForward.accesskey;"
+ preftype="bool" genericattr="true"
+ prefstring="mail.identity.%identitykey%.sig_on_fwd"/>
+
+ <separator class="thin"/>
+
+ <hbox pack="end">
+ <button id="globalComposingPrefsLink"
+ label="&globalComposingPrefs.label;"
+ accesskey="&globalComposingPrefs.accesskey;"
+ oncommand="showGlobalComposingPrefs();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin"/>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&addressingGroupTitle.label;</html:legend>
+#ifndef MOZ_THUNDERBIRD
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.autocompleteToMyDomain"
+ label="&autocompleteToMyDomain.label;"
+ accesskey="&autocompleteToMyDomain.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.autocompleteToMyDomain"/>
+ </hbox>
+
+ <separator class="thin"/>
+#endif
+
+ <label control="identity.overrideGlobal_Pref">&addressingText.label;</label>
+ <radiogroup id="identity.overrideGlobal_Pref" class="indent"
+ oncommand="LDAPenabling();" wsm_persist="true"
+ genericattr="true" preftype="bool"
+ prefstring="mail.identity.%identitykey%.overrideGlobal_Pref">
+ <radio value="false" label="&useGlobal.label;"
+ accesskey="&useGlobal.accesskey;"/>
+ <radio value="true" id="directories" label="&directories.label;"
+ accesskey="&directories.accesskey;"/>
+ <hbox class="indent">
+ <menulist is="menulist-addrbooks" id="identity.directoryServer"
+ none="&directoriesNone.label;"
+ remoteonly="true"
+ wsm_persist="true"
+ preftype="string"
+ prefstring="mail.identity.%identitykey%.directoryServer"
+ style="min-width: 16em;"
+ aria-labelledby="directories"
+ flex="1"/>
+ <button id="editButton" label="&editDirectories.label;"
+ accesskey="&editDirectories.accesskey;"
+ oncommand="onEditDirectories();"/>
+ </hbox>
+ </radiogroup>
+
+ <separator class="thin"/>
+
+ <hbox pack="end">
+ <button id="globalAddressingPrefsLink"
+ label="&globalAddressingPrefs.label;"
+ accesskey="&globalAddressingPrefs.accesskey;"
+ oncommand="showGlobalAddressingPrefs();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
diff --git a/comm/mailnews/base/prefs/content/am-addressing.js b/comm/mailnews/base/prefs/content/am-addressing.js
new file mode 100644
index 0000000000..24a3bf0004
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+/* import-globals-from am-prefs.js */
+/* import-globals-from amUtils.js */
+
+function onLoad() {
+ parent.onPanelLoaded("am-addressing.xhtml");
+}
+
+function onInit(aPageId, aServerId) {
+ onInitCompositionAndAddressing();
+}
+
+function onInitCompositionAndAddressing() {
+ LDAPenabling();
+ quoteEnabling();
+}
+
+function onEditDirectories() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/addressbook/pref-editdirectories.xhtml"
+ );
+}
+
+function onPreInit(account, accountValues) {}
+
+function LDAPenabling() {
+ onCheckItem("identity.directoryServer", ["directories"]);
+ onCheckItem("editButton", ["directories"]);
+}
+
+function quoteEnabling() {
+ var placebox = document.getElementById("placeBox");
+
+ if (document.getElementById("identity.replyOnTop").value == "1") {
+ placebox.firstElementChild.removeAttribute("disabled");
+ placebox.lastElementChild.removeAttribute("disabled");
+ } else {
+ placebox.firstElementChild.setAttribute("disabled", "true");
+ placebox.lastElementChild.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Open the Preferences dialog on the tab with Addressing options.
+ */
+function showGlobalAddressingPrefs() {
+ openPrefsFromAccountManager(
+ "paneCompose",
+ "compositionAddressingCategory",
+ null,
+ "addressing_pane"
+ );
+}
+
+/**
+ * Open the Preferences dialog on the tab with Composing options.
+ */
+function showGlobalComposingPrefs() {
+ openPrefsFromAccountManager(
+ "paneCompose",
+ null,
+ null,
+ "composing_messages_pane"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-addressing.xhtml b/comm/mailnews/base/prefs/content/am-addressing.xhtml
new file mode 100644
index 0000000000..554ee398c1
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-addressing.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&addressing.label;</title>
+ <script defer="defer" src="chrome://messenger/content/am-addressing.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { onLoad(); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&addressing.label;"/>
+ </hbox>
+#include am-addressing.inc.xhtml
+ </vbox>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-archiveoptions.js b/comm/mailnews/base/prefs/content/am-archiveoptions.js
new file mode 100644
index 0000000000..44c0817149
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-archiveoptions.js
@@ -0,0 +1,74 @@
+/* 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 gIdentity = null;
+
+window.addEventListener("load", onLoadArchiveOptions);
+document.addEventListener("dialogaccept", onAcceptArchiveOptions);
+
+/**
+ * Load the archive options dialog, set the radio/checkbox items to the
+ * appropriate values, and update the archive hierarchy example.
+ */
+function onLoadArchiveOptions() {
+ // extract the account
+ gIdentity = window.arguments[0].identity;
+
+ let granularity = document.getElementById("archiveGranularity");
+ granularity.selectedIndex = gIdentity.archiveGranularity;
+ granularity.addEventListener("command", updateArchiveExample);
+
+ let kfs = document.getElementById("archiveKeepFolderStructure");
+ kfs.checked = gIdentity.archiveKeepFolderStructure;
+ kfs.addEventListener("command", updateArchiveExample);
+
+ updateArchiveExample();
+}
+
+/**
+ * Save the archive settings to the current identity.
+ */
+function onAcceptArchiveOptions() {
+ gIdentity.archiveGranularity =
+ document.getElementById("archiveGranularity").selectedIndex;
+ gIdentity.archiveKeepFolderStructure = document.getElementById(
+ "archiveKeepFolderStructure"
+ ).checked;
+}
+
+/**
+ * Update the example tree to show what the current options would look like.
+ */
+function updateArchiveExample() {
+ let granularity = document.getElementById("archiveGranularity").selectedIndex;
+ let kfs = document.getElementById("archiveKeepFolderStructure").checked;
+ let hierarchy = [
+ document.getElementsByClassName("root"),
+ document.getElementsByClassName("year"),
+ document.getElementsByClassName("month"),
+ ];
+
+ // First, show/hide the appropriate levels in the hierarchy and turn the
+ // necessary items into containers.
+ for (let i = 0; i < hierarchy.length; i++) {
+ for (let j = 0; j < hierarchy[i].length; j++) {
+ hierarchy[i][j].setAttribute("container", granularity > i);
+ hierarchy[i][j].setAttribute("open", granularity > i);
+ hierarchy[i][j].hidden = granularity < i;
+ }
+ }
+
+ // Next, handle the "keep folder structures" case by moving a tree item around
+ // and making sure its parent is a container.
+ let folders = document.getElementById("folders");
+ folders.hidden = !kfs;
+ if (kfs) {
+ let parent = hierarchy[granularity][0];
+ parent.setAttribute("container", true);
+ parent.setAttribute("open", true);
+
+ let treechildren = parent.children[1];
+ treechildren.appendChild(folders);
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml b/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml
new file mode 100644
index 0000000000..e34de7a420
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml
@@ -0,0 +1,136 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-archiveoptions.dtd">
+<html
+ id="archiveOptions"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 500px"
+ scrolling="false"
+>
+ <head>
+ <title>&dialogTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-archiveoptions.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog style="width: 100vw; height: 100vh">
+ <vbox flex="1">
+ <label>&archiveGranularityPrefix.label;</label>
+ <radiogroup id="archiveGranularity">
+ <radio
+ label="&archiveFlat.label;"
+ accesskey="&archiveFlat.accesskey;"
+ class="indent"
+ />
+ <radio
+ label="&archiveYearly.label;"
+ accesskey="&archiveYearly.accesskey;"
+ class="indent"
+ />
+ <radio
+ label="&archiveMonthly.label;"
+ accesskey="&archiveMonthly.accesskey;"
+ class="indent"
+ />
+ </radiogroup>
+ <checkbox
+ id="archiveKeepFolderStructure"
+ label="&keepFolderStructure.label;"
+ accesskey="&keepFolderStructure.accesskey;"
+ />
+
+ <html:div>
+ <html:fieldset flex="1" style="margin-block-end: 5px">
+ <html:legend>&archiveExample.label;</html:legend>
+ <hbox flex="1">
+ <tree
+ id="archiveTree"
+ hidecolumnpicker="true"
+ disabled="true"
+ flex="1"
+ style="min-height: 8em"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ primary="true"
+ hideheader="true"
+ style="flex: 1 auto"
+ />
+ </treecols>
+ <treechildren>
+ <treeitem class="root">
+ <treerow>
+ <treecell
+ properties="specialFolder-Archive"
+ label="&archiveFolderName.label;"
+ />
+ </treerow>
+ <treechildren>
+ <treeitem id="folders">
+ <treerow>
+ <treecell label="&inboxFolderName.label;" />
+ </treerow>
+ </treeitem>
+ <treeitem class="year">
+ <treerow>
+ <treecell label="2020" />
+ </treerow>
+ <treechildren>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2020-11" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2020-12" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem class="year">
+ <treerow>
+ <treecell label="2021" />
+ </treerow>
+ <treechildren>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2021-01" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2021-02" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </tree>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-copies.inc.xhtml b/comm/mailnews/base/prefs/content/am-copies.inc.xhtml
new file mode 100644
index 0000000000..75834fd60a
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.inc.xhtml
@@ -0,0 +1,308 @@
+# 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/.
+
+ <vbox id="copiesAndFolders" flex="1">
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/am-copies.ftl"/>
+ </linkset>
+
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+
+ <label id="identity.fccFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_folder"/>
+ <label id="identity.draftFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.draft_folder"/>
+ <label id="identity.archiveFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archive_folder"/>
+ <label id="identity.stationeryFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.stationary_folder"/>
+ <label id="identity.email" hidden="true" wsm_persist="true"/>
+ <label id="identity.fccFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_folder_picker_mode"/>
+ <label id="identity.draftsFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.drafts_folder_picker_mode"/>
+ <label id="identity.archivesFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archives_folder_picker_mode"/>
+ <label id="identity.tmplFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.tmpl_folder_picker_mode"/>
+ <html:div>
+ <html:fieldset id="copiesGroup">
+ <html:legend>&sendingPrefix.label;</html:legend>
+
+ <hbox align="center">
+ <checkbox id="identity.doFcc" wsm_persist="true" label="&fccMailFolder.label;"
+ accesskey="&fccMailFolder.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc"
+ oncommand="setupFccItems();"/>
+ </hbox>
+ <radiogroup id="doFcc" aria-labelledby="identity.doFcc">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="fcc_selectAccount"
+ class="depends-on-do-fcc"
+ value="0" label="&sentFolderOn.label;"
+ accesskey="&sentFolderOn.accesskey;"
+ oncommand="setPickersState('msgFccAccountPicker', 'msgFccFolderPicker', event)"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="fcc_selectFolder"
+ class="depends-on-do-fcc"
+ value="1" label="&sentInOtherFolder.label;"
+ accesskey="&sentInOtherFolder.accesskey;"
+ oncommand="setPickersState('msgFccFolderPicker', 'msgFccAccountPicker', event)"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgFccAccountPicker"
+ class="folderMenuItem depends-on-do-fcc"
+ aria-labelledby="fcc_selectAccount">
+ <menupopup is="folder-menupopup" id="msgFccAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('fcc', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgFccFolderPicker"
+ class="folderMenuItem depends-on-do-fcc"
+ aria-labelledby="fcc_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgFccFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('fcc', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="fccReplyFollowsParent" hidable="true" hidefor="nntp,rss">
+ <checkbox id="identity.fccReplyFollowsParent" wsm_persist="true"
+ class="depends-on-do-fcc"
+ label="&fccReplyFollowsParent.label;"
+ accesskey="&fccReplyFollowsParent.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_reply_follows_parent"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <checkbox id="identity.doCc" wsm_persist="true" label="&ccAddress.label;"
+ accesskey="&ccAddress.accesskey;"
+ control="identity.doCcList"
+ oncommand="identityDoCcBccOnCommand(event);"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.doCc"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="identity.doBcc" wsm_persist="true" label="&bccAddress.label;"
+ accesskey="&bccAddress.accesskey;"
+ control="identity.doBccList"
+ oncommand="identityDoCcBccOnCommand(event);"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.doBcc"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <hbox flex="1" class="input-container" align="center">
+ <html:input id="identity.doCcList" wsm_persist="true"
+ type="text"
+ aria-labelledby="identity.doCc"
+ prefstring="mail.identity.%identitykey%.doCcList"
+ class="uri-element input-inline"
+ placeholder="&ccAddressList.placeholder;"
+ onblur="identityDoCcBccOnBlur(event);"/>
+ </hbox>
+ <hbox flex="1" class="input-container" align="center">
+ <html:input id="identity.doBccList" wsm_persist="true"
+ type="text"
+ aria-labelledby="identity.doBcc"
+ prefstring="mail.identity.%identitykey%.doBccList"
+ class="uri-element input-inline"
+ placeholder="&bccAddressList.placeholder;"
+ onblur="identityDoCcBccOnBlur(event);"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <description class="indent tip-caption"
+ data-l10n-id="account-prefs-show-address-row-description"/>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="archivesGroup">
+ <html:legend>&archivesTitle.label;</html:legend>
+
+ <hbox pack="start">
+ <checkbox id="identity.archiveEnabled" wsm_persist="true"
+ label="&keepArchives.label;"
+ accesskey="&keepArchives.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archive_enabled"
+ oncommand="setupArchiveItems();"/>
+ </hbox>
+
+ <radiogroup id="messageArchives">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="archive_selectAccount"
+ class="depends-on-archive"
+ value="0" label="&archivesFolderOn.label;"
+ accesskey="&archivesFolderOn.accesskey;"
+ oncommand="setPickersState('msgArchivesAccountPicker', 'msgArchivesFolderPicker', event)"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="archive_selectFolder"
+ class="depends-on-archive"
+ value="1" label="&archiveInOtherFolder.label;"
+ accesskey="&archiveInOtherFolder.accesskey;"
+ oncommand="setPickersState('msgArchivesFolderPicker', 'msgArchivesAccountPicker', event)"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgArchivesAccountPicker"
+ class="folderMenuItem depends-on-archive"
+ aria-labelledby="archive_selectAccount">
+ <menupopup is="folder-menupopup" id="msgArchivesAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('archive', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgArchivesFolderPicker"
+ class="folderMenuItem depends-on-archive"
+ aria-labelledby="archive_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgArchivesFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('archive', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ <hbox pack="end">
+ <button id="archiveHierarchyButton"
+ class="depends-on-archive"
+ label="&archiveHierarchyButton.label;"
+ accesskey="&archiveHierarchyButton.accesskey;"
+ oncommand="ChangeArchiveHierarchy();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="foldersGroup">
+ <html:legend>&specialFolders.label;</html:legend>
+
+ <hbox align="center">
+ <label value="&keepDrafts2.label;" control="messageDrafts"/>
+ </hbox>
+
+ <radiogroup id="messageDrafts">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="draft_selectAccount"
+ oncommand="setPickersState('msgDraftsAccountPicker', 'msgDraftsFolderPicker', event)"
+ value="0" label="&draftsFolderOn.label;"
+ accesskey="&draftsFolderOn.accesskey;"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="draft_selectFolder"
+ oncommand="setPickersState('msgDraftsFolderPicker', 'msgDraftsAccountPicker', event)"
+ value="1" label="&draftInOtherFolder.label;"
+ accesskey="&draftInOtherFolder.accesskey;"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgDraftsAccountPicker"
+ class="folderMenuItem"
+ aria-labelledby="draft_selectAccount">
+ <menupopup is="folder-menupopup" id="msgDraftAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('draft', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgDraftsFolderPicker"
+ class="folderMenuItem"
+ aria-labelledby="draft_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgDraftFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('draft', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <label value="&keepTemplates.label;" control="messageTemplates"/>
+ </hbox>
+
+ <radiogroup id="messageTemplates">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="tmpl_selectAccount"
+ oncommand="setPickersState('msgStationeryAccountPicker', 'msgStationeryFolderPicker', event)"
+ value="0" label="&templatesFolderOn.label;"
+ accesskey="&templatesFolderOn.accesskey;"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="tmpl_selectFolder"
+ oncommand="setPickersState('msgStationeryFolderPicker', 'msgStationeryAccountPicker', event)"
+ value="1" label="&templateInOtherFolder.label;"
+ accesskey="&templateInOtherFolder.accesskey;"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgStationeryAccountPicker"
+ class="folderMenuItem"
+ aria-labelledby="tmpl_selectAccount">
+ <menupopup is="folder-menupopup" id="msgFccAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('tmpl', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgStationeryFolderPicker"
+ class="folderMenuItem"
+ aria-labelledby="tmpl_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgTemplFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('tmpl', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ <hbox align="center">
+ <checkbox id="identity.showSaveMsgDlg" wsm_persist="true" label="&saveMessageDlg.label;"
+ accesskey="&saveMessageDlg.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.showSaveMsgDlg"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
diff --git a/comm/mailnews/base/prefs/content/am-copies.js b/comm/mailnews/base/prefs/content/am-copies.js
new file mode 100644
index 0000000000..f848bab0c0
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.js
@@ -0,0 +1,555 @@
+/* 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/. */
+
+/* import-globals-from am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gFccRadioElemChoice,
+ gDraftsRadioElemChoice,
+ gArchivesRadioElemChoice,
+ gTmplRadioElemChoice;
+var gFccRadioElemChoiceLocked,
+ gDraftsRadioElemChoiceLocked,
+ gArchivesRadioElemChoiceLocked,
+ gTmplRadioElemChoiceLocked;
+var gDefaultPickerMode = "1";
+
+var gFccFolderWithDelim,
+ gDraftsFolderWithDelim,
+ gArchivesFolderWithDelim,
+ gTemplatesFolderWithDelim;
+var gAccount;
+var gCurrentServerId;
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+ var type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+}
+
+/*
+ * Set the global radio element choices and initialize folder/account pickers.
+ * Also, initialize other UI elements (cc, bcc, fcc picker controller checkboxes).
+ */
+function onInit(aPageId, aServerId) {
+ gCurrentServerId = aServerId;
+ onInitCopiesAndFolders();
+}
+
+function onInitCopiesAndFolders() {
+ SetGlobalRadioElemChoices();
+
+ SetFolderDisplay(
+ gFccRadioElemChoice,
+ gFccRadioElemChoiceLocked,
+ "fcc",
+ "msgFccAccountPicker",
+ "identity.fccFolder",
+ "msgFccFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gArchivesRadioElemChoice,
+ gArchivesRadioElemChoiceLocked,
+ "archive",
+ "msgArchivesAccountPicker",
+ "identity.archiveFolder",
+ "msgArchivesFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gDraftsRadioElemChoice,
+ gDraftsRadioElemChoiceLocked,
+ "draft",
+ "msgDraftsAccountPicker",
+ "identity.draftFolder",
+ "msgDraftsFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gTmplRadioElemChoice,
+ gTmplRadioElemChoiceLocked,
+ "tmpl",
+ "msgStationeryAccountPicker",
+ "identity.stationeryFolder",
+ "msgStationeryFolderPicker"
+ );
+
+ setupDoCcBccItems("identity.doCc", "identity.doCcList");
+ setupDoCcBccItems("identity.doBcc", "identity.doBccList");
+ setupFccItems();
+ setupArchiveItems();
+
+ SetSpecialFolderNamesWithDelims();
+}
+
+// Initialize the picker mode choices (account/folder picker) into global vars
+function SetGlobalRadioElemChoices() {
+ var pickerModeElement = document.getElementById(
+ "identity.fccFolderPickerMode"
+ );
+ gFccRadioElemChoice = pickerModeElement.getAttribute("value");
+ gFccRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gFccRadioElemChoice) {
+ gFccRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById(
+ "identity.archivesFolderPickerMode"
+ );
+ gArchivesRadioElemChoice = pickerModeElement.getAttribute("value");
+ gArchivesRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gArchivesRadioElemChoice) {
+ gArchivesRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById(
+ "identity.draftsFolderPickerMode"
+ );
+ gDraftsRadioElemChoice = pickerModeElement.getAttribute("value");
+ gDraftsRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gDraftsRadioElemChoice) {
+ gDraftsRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById("identity.tmplFolderPickerMode");
+ gTmplRadioElemChoice = pickerModeElement.getAttribute("value");
+ gTmplRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gTmplRadioElemChoice) {
+ gTmplRadioElemChoice = gDefaultPickerMode;
+ }
+}
+
+/*
+ * Set Account and Folder elements based on the values read from
+ * preferences file. Default picker mode, if none specified at this stage, is
+ * set to 1 i.e., Other picker displaying the folder value read from the
+ * preferences file.
+ */
+function SetFolderDisplay(
+ pickerMode,
+ disableMode,
+ radioElemPrefix,
+ accountPickerId,
+ folderPickedField,
+ folderPickerId
+) {
+ if (!pickerMode) {
+ pickerMode = gDefaultPickerMode;
+ }
+
+ var selectAccountRadioId = radioElemPrefix + "_selectAccount";
+ var selectAccountRadioElem = document.getElementById(selectAccountRadioId);
+ var selectFolderRadioId = radioElemPrefix + "_selectFolder";
+ var selectFolderRadioElem = document.getElementById(selectFolderRadioId);
+ var accountPicker = document.getElementById(accountPickerId);
+ var folderPicker = document.getElementById(folderPickerId);
+ var rg = selectAccountRadioElem.radioGroup;
+ var folderPickedElement = document.getElementById(folderPickedField);
+ var uri = folderPickedElement.getAttribute("value");
+ // Get message folder from the given uri.
+ // There is no need to check for the existence of special folders as
+ // these folders are created on demand at runtime in case of imap accounts.
+ // For POP3 accounts, special folders are created at the account creation time.
+ var msgFolder = MailUtils.getOrCreateFolder(uri);
+ InitFolderDisplay(msgFolder.server.rootFolder, accountPicker);
+ InitFolderDisplay(msgFolder, folderPicker);
+
+ switch (pickerMode) {
+ case "0":
+ rg.selectedItem = selectAccountRadioElem;
+ SetPickerEnabling(accountPickerId, folderPickerId);
+ break;
+
+ case "1":
+ rg.selectedItem = selectFolderRadioElem;
+ SetPickerEnabling(folderPickerId, accountPickerId);
+ break;
+
+ default:
+ dump("Error in setting initial folder display on pickers\n");
+ break;
+ }
+
+ // Check to see if we need to lock page elements. Disable radio buttons
+ // and account/folder pickers when locked.
+ if (disableMode) {
+ selectAccountRadioElem.setAttribute("disabled", "true");
+ selectFolderRadioElem.setAttribute("disabled", "true");
+ accountPicker.setAttribute("disabled", "true");
+ folderPicker.setAttribute("disabled", "true");
+ }
+}
+
+// Initialize the folder display based on prefs values
+function InitFolderDisplay(folder, folderPicker) {
+ folderPicker.menupopup.selectFolder(folder);
+ folderPicker.folder = folder;
+}
+
+/**
+ * Capture any menulist changes and update the folder property.
+ *
+ * @param {string} aGroup - The prefix for the menulist we're handling
+ * (e.g. "drafts")
+ * @param {"Account"|"Folder"} aType - "Account" for the account picker or
+ * "Folder" for the folder picker.
+ * @param {Event} aEvent - The event that we're responding to.
+ */
+function noteSelectionChange(aGroup, aType, aEvent) {
+ var checkedElem = document.getElementById(aGroup + "_select" + aType);
+ var folder = aEvent.target._folder;
+ var modeValue = checkedElem.value;
+ var radioGroup = checkedElem.radioGroup.getAttribute("id");
+ var picker;
+
+ switch (radioGroup) {
+ case "doFcc":
+ gFccRadioElemChoice = modeValue;
+ picker = document.getElementById("msgFcc" + aType + "Picker");
+ break;
+
+ case "messageArchives":
+ gArchivesRadioElemChoice = modeValue;
+ picker = document.getElementById("msgArchives" + aType + "Picker");
+ updateArchiveHierarchyButton(folder);
+ break;
+
+ case "messageDrafts":
+ gDraftsRadioElemChoice = modeValue;
+ picker = document.getElementById("msgDrafts" + aType + "Picker");
+ break;
+
+ case "messageTemplates":
+ gTmplRadioElemChoice = modeValue;
+ picker = document.getElementById("msgStationery" + aType + "Picker");
+ break;
+ }
+
+ picker.folder = folder;
+ picker.menupopup.selectFolder(folder);
+}
+
+// Need to append special folders when account picker is selected.
+// Create a set of global special folder vars to be suffixed to the
+// server URI of the selected account.
+function SetSpecialFolderNamesWithDelims() {
+ var folderDelim = "/";
+ /* we use internal names known to everyone like "Sent", "Templates" and "Drafts" */
+
+ gFccFolderWithDelim = folderDelim + "Sent";
+ gArchivesFolderWithDelim = folderDelim + "Archives";
+ gDraftsFolderWithDelim = folderDelim + "Drafts";
+ gTemplatesFolderWithDelim = folderDelim + "Templates";
+}
+
+// Save all changes on this page
+function onSave() {
+ onSaveCopiesAndFolders();
+}
+
+function onSaveCopiesAndFolders() {
+ SaveFolderSettings(
+ gFccRadioElemChoice,
+ "doFcc",
+ gFccFolderWithDelim,
+ "msgFccAccountPicker",
+ "msgFccFolderPicker",
+ "identity.fccFolder",
+ "identity.fccFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gArchivesRadioElemChoice,
+ "messageArchives",
+ gArchivesFolderWithDelim,
+ "msgArchivesAccountPicker",
+ "msgArchivesFolderPicker",
+ "identity.archiveFolder",
+ "identity.archivesFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gDraftsRadioElemChoice,
+ "messageDrafts",
+ gDraftsFolderWithDelim,
+ "msgDraftsAccountPicker",
+ "msgDraftsFolderPicker",
+ "identity.draftFolder",
+ "identity.draftsFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gTmplRadioElemChoice,
+ "messageTemplates",
+ gTemplatesFolderWithDelim,
+ "msgStationeryAccountPicker",
+ "msgStationeryFolderPicker",
+ "identity.stationeryFolder",
+ "identity.tmplFolderPickerMode"
+ );
+}
+
+// Save folder settings and radio element choices
+function SaveFolderSettings(
+ radioElemChoice,
+ radioGroupId,
+ folderSuffix,
+ accountPickerId,
+ folderPickerId,
+ folderElementId,
+ folderPickerModeId
+) {
+ var formElement = document.getElementById(folderElementId);
+ var uri;
+
+ if (
+ radioElemChoice == "0" ||
+ !document.getElementById(folderPickerId).value
+ ) {
+ // Default or revert to default if no folder chosen.
+ radioElemChoice = "0";
+ uri = document.getElementById(accountPickerId).folder.URI;
+ if (uri) {
+ // Create Folder URI.
+ uri = uri + folderSuffix;
+ }
+ } else if (radioElemChoice == "1") {
+ uri = document.getElementById(folderPickerId).folder.URI;
+ } else {
+ dump("Error saving folder preferences.\n");
+ return;
+ }
+
+ formElement.setAttribute("value", uri);
+
+ formElement = document.getElementById(folderPickerModeId);
+ formElement.setAttribute("value", radioElemChoice);
+}
+
+// Check the Fcc Self item and setup associated picker state
+function setupFccItems() {
+ let checked = document.getElementById("identity.doFcc").checked;
+ document.querySelectorAll(".depends-on-do-fcc").forEach(e => {
+ if (checked) {
+ e.removeAttribute("disabled");
+ } else {
+ e.setAttribute("disabled", "true");
+ }
+ });
+ if (!checked) {
+ return;
+ }
+
+ switch (gFccRadioElemChoice) {
+ case "0":
+ if (!gFccRadioElemChoiceLocked) {
+ SetPickerEnabling("msgFccAccountPicker", "msgFccFolderPicker");
+ }
+ SetRadioButtons("fcc_selectAccount", "fcc_selectFolder");
+ break;
+
+ case "1":
+ if (!gFccRadioElemChoiceLocked) {
+ SetPickerEnabling("msgFccFolderPicker", "msgFccAccountPicker");
+ }
+ SetRadioButtons("fcc_selectFolder", "fcc_selectAccount");
+ break;
+
+ default:
+ dump("Error in setting Fcc elements.\n");
+ break;
+ }
+}
+
+/**
+ * Handle the initial status and value of the Auto-Cc/Bcc text input fields.
+ *
+ * @param {string} checkboxId - The ID of an Auto-Cc/Bcc checkbox.
+ * @param {string} inputId - The ID of an Auto-Cc/Bcc text input element.
+ */
+function setupDoCcBccItems(checkboxId, inputId) {
+ // Enable address input according to the status of the checkbox.
+ let input = document.getElementById(inputId);
+ input.disabled = !document.getElementById(checkboxId).checked;
+ // Safeguard against space-padded address list to ensure list visibility.
+ input.value = input.value.trim();
+}
+
+/**
+ * Handle the command event of the Auto-Cc/Bcc checkboxes.
+ * Disable the respective text input element if the checkbox is not checked, and
+ * handle the default value of the input when the checkbox is toggled.
+ *
+ * @param {Event} event - The command event of the checkbox.
+ */
+function identityDoCcBccOnCommand(event) {
+ let checkbox = event.target;
+ let checked = checkbox.checked;
+ // For checkboxes #identity.doCc and #identity.doBcc, get the corresponding
+ // inputs: #identity.doCcList and #identity.doBccList.
+ let input = document.getElementById(`${checkbox.id}List`);
+ input.disabled = !checked;
+
+ // User toggled checkbox.
+ let identityEmailAddress = document.getElementById("identity.email").value;
+ if (checked) {
+ // If user checks the checkbox and there's no address, default to identity's
+ // email address.
+ if (!input.value) {
+ input.value = identityEmailAddress;
+ }
+ input.select();
+ return;
+ }
+
+ if (input.value == identityEmailAddress) {
+ // If user unchecks checkbox and the input has default address, clear input.
+ input.value = "";
+ }
+}
+
+/**
+ * Handle the blur event of the Auto-Cc/Bcc checkboxes.
+ *
+ * @param {Event} event - The blur event of the checkbox.
+ */
+function identityDoCcBccOnBlur(event) {
+ let input = event.target;
+ // Safeguard against space-padded address list to ensure list visibility.
+ input.value = input.value.trim();
+}
+
+// Enable and disable pickers based on the radio element clicked
+function SetPickerEnabling(enablePickerId, disablePickerId) {
+ var activePicker = document.getElementById(enablePickerId);
+ activePicker.removeAttribute("disabled");
+
+ var inactivePicker = document.getElementById(disablePickerId);
+ inactivePicker.setAttribute("disabled", "true");
+}
+
+// Set radio element choices and picker states
+function setPickersState(enablePickerId, disablePickerId, event) {
+ SetPickerEnabling(enablePickerId, disablePickerId);
+
+ var radioElemValue = event.target.value;
+
+ switch (event.target.id) {
+ case "fcc_selectAccount":
+ case "fcc_selectFolder":
+ gFccRadioElemChoice = radioElemValue;
+ break;
+ case "archive_selectAccount":
+ case "archive_selectFolder":
+ gArchivesRadioElemChoice = radioElemValue;
+ updateArchiveHierarchyButton(
+ document.getElementById(enablePickerId).folder
+ );
+ break;
+ case "draft_selectAccount":
+ case "draft_selectFolder":
+ gDraftsRadioElemChoice = radioElemValue;
+ break;
+ case "tmpl_selectAccount":
+ case "tmpl_selectFolder":
+ gTmplRadioElemChoice = radioElemValue;
+ break;
+ default:
+ dump("Error in setting picker state.\n");
+ }
+}
+
+// This routine is to restore the correct radio element
+// state when the fcc self checkbox broadcasts the change
+function SetRadioButtons(selectPickerId, unselectPickerId) {
+ var activeRadioElem = document.getElementById(selectPickerId);
+ activeRadioElem.radioGroup.selectedItem = activeRadioElem;
+}
+
+/**
+ * Enable/disable the archive hierarchy button depending on what folder is
+ * currently selected (Gmail IMAP folders should have the button disabled, since
+ * changing the archive hierarchy does nothing there.
+ *
+ * @param {nsIMsgFolder} archiveFolder - The currently-selected folder to store
+ * archives in
+ */
+function updateArchiveHierarchyButton(archiveFolder) {
+ let isGmailImap =
+ archiveFolder.server.type == "imap" &&
+ archiveFolder.server.QueryInterface(Ci.nsIImapIncomingServer).isGMailServer;
+ document.getElementById("archiveHierarchyButton").disabled = isGmailImap;
+}
+
+/**
+ * Enable or disable (as appropriate) the controls for setting archive options
+ */
+function setupArchiveItems() {
+ let checked = document.getElementById("identity.archiveEnabled").checked;
+ document.querySelectorAll(".depends-on-archive").forEach(e => {
+ if (checked) {
+ e.removeAttribute("disabled");
+ } else {
+ e.setAttribute("disabled", "true");
+ }
+ });
+ if (!checked) {
+ return;
+ }
+
+ switch (gArchivesRadioElemChoice) {
+ case "0":
+ if (!gArchivesRadioElemChoiceLocked) {
+ SetPickerEnabling(
+ "msgArchivesAccountPicker",
+ "msgArchivesFolderPicker"
+ );
+ }
+ SetRadioButtons("archive_selectAccount", "archive_selectFolder");
+ updateArchiveHierarchyButton(
+ document.getElementById("msgArchivesAccountPicker").folder
+ );
+ break;
+
+ case "1":
+ if (!gArchivesRadioElemChoiceLocked) {
+ SetPickerEnabling(
+ "msgArchivesFolderPicker",
+ "msgArchivesAccountPicker"
+ );
+ }
+ SetRadioButtons("archive_selectFolder", "archive_selectAccount");
+ updateArchiveHierarchyButton(
+ document.getElementById("msgArchivesFolderPicker").folder
+ );
+ break;
+
+ default:
+ dump("Error in setting Archive elements.\n");
+ }
+}
+
+/**
+ * Open a dialog to edit the folder hierarchy used when archiving messages.
+ */
+function ChangeArchiveHierarchy() {
+ let identity = parent.gIdentity || parent.getCurrentAccount().defaultIdentity;
+ let arg = { identity };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-archiveoptions.xhtml",
+ undefined,
+ arg
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-copies.xhtml b/comm/mailnews/base/prefs/content/am-copies.xhtml
new file mode 100644
index 0000000000..c142cb8eb5
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-copies.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&copyAndFolderTitle.label;</title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-copies.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { parent.onPanelLoaded('am-copies.xhtml'); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&copyAndFolderTitle.label;"/>
+ </hbox>
+#include am-copies.inc.xhtml
+ </vbox>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-identities-list.js b/comm/mailnews/base/prefs/content/am-identities-list.js
new file mode 100644
index 0000000000..02d20e1434
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identities-list.js
@@ -0,0 +1,206 @@
+/* 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 { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+
+var gIdentityListBox; // the root <richlistbox> node
+var gAddButton;
+var gEditButton;
+var gSetDefaultButton;
+var gDeleteButton;
+
+var gAccount = null; // the account we are showing the identities for
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogaccept", onOk);
+document.addEventListener("dialogcancel", onOk);
+
+function onLoad() {
+ gIdentityListBox = document.getElementById("identitiesList");
+ gAddButton = document.getElementById("cmd_add");
+ gEditButton = document.getElementById("cmd_edit");
+ gSetDefaultButton = document.getElementById("cmd_default");
+ gDeleteButton = document.getElementById("cmd_delete");
+
+ // extract the account
+ gAccount = window.arguments[0].account;
+
+ var accountName = window.arguments[0].accountName;
+ document.title = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("identity-list-title", [accountName]);
+
+ refreshIdentityList(0);
+}
+
+/**
+ * Rebuilds the listbox holding the list of identities.
+ *
+ * @param {number} aSelectIndex - Attempt to select the identity with this index.
+ */
+function refreshIdentityList(aSelectIndex) {
+ // Remove all children.
+ while (gIdentityListBox.hasChildNodes()) {
+ gIdentityListBox.lastChild.remove();
+ }
+
+ // Build the list from the identities array.
+ for (let identity of gAccount.identities) {
+ if (identity.valid) {
+ let label = document.createXULElement("label");
+ label.setAttribute("value", identity.identityName);
+
+ let listitem = document.createXULElement("richlistitem");
+ listitem.appendChild(label);
+ listitem.setAttribute("key", identity.key);
+ gIdentityListBox.appendChild(listitem);
+ }
+ }
+
+ // Ensure one identity is always selected.
+ if (!aSelectIndex || aSelectIndex < 0) {
+ aSelectIndex = 0;
+ } else if (aSelectIndex >= gIdentityListBox.itemCount) {
+ aSelectIndex = gIdentityListBox.itemCount - 1;
+ }
+
+ // This also fires the onselect event, which in turn calls updateButtons().
+ gIdentityListBox.selectedIndex = aSelectIndex;
+}
+
+/**
+ * Opens the identity editor dialog.
+ *
+ * @param {nsIMsgIdentity} identity - The identity (if any) to load in the dialog.
+ */
+function openIdentityEditor(identity) {
+ let args = { identity, account: gAccount, result: false };
+
+ let indexToSelect = identity
+ ? gIdentityListBox.selectedIndex
+ : gIdentityListBox.itemCount;
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-identity-edit.xhtml",
+ { closingCallback: onCloseIdentity },
+ args
+ );
+
+ function onCloseIdentity() {
+ if (args.result) {
+ refreshIdentityList(indexToSelect);
+ }
+ }
+}
+
+function getSelectedIdentity() {
+ if (gIdentityListBox.selectedItems.length != 1) {
+ return null;
+ }
+
+ let identityKey = gIdentityListBox.selectedItems[0].getAttribute("key");
+ return (
+ gAccount.identities.find(id => id.valid && id.key == identityKey) || null
+ );
+}
+
+function onEdit(event) {
+ var id = getSelectedIdentity();
+ openIdentityEditor(id);
+}
+
+/**
+ * Enable/disable buttons depending on number of identities and current selection.
+ */
+function updateButtons() {
+ // In this listbox there should always be one item selected.
+ if (
+ gIdentityListBox.selectedItems.length != 1 ||
+ gIdentityListBox.itemCount == 0
+ ) {
+ // But in case this is not met (e.g. there is no identity for some reason,
+ // or the list is being rebuilt), disable all buttons.
+ gEditButton.setAttribute("disabled", "true");
+ gDeleteButton.setAttribute("disabled", "true");
+ gSetDefaultButton.setAttribute("disabled", "true");
+ return;
+ }
+
+ gEditButton.setAttribute("disabled", "false");
+ gDeleteButton.setAttribute(
+ "disabled",
+ gIdentityListBox.itemCount <= 1 ? "true" : "false"
+ );
+ gSetDefaultButton.setAttribute(
+ "disabled",
+ gIdentityListBox.selectedIndex == 0 ? "true" : "false"
+ );
+ // The Add command is always enabled.
+}
+
+function onSetDefault(event) {
+ let identity = getSelectedIdentity();
+ if (!identity) {
+ return;
+ }
+
+ // If the first identity is selected, there is nothing to do.
+ if (gIdentityListBox.selectedIndex == 0) {
+ return;
+ }
+
+ gAccount.defaultIdentity = identity;
+ // Rebuilt the identity list and select the moved identity again.
+ refreshIdentityList(0);
+ // Update gloda's myContact with the new default identity.
+ Gloda._initMyIdentities();
+}
+
+function onDelete(event) {
+ if (gIdentityListBox.itemCount <= 1) {
+ // don't support deleting the last identity
+ return;
+ }
+
+ // get delete confirmation
+ let selectedIdentity = getSelectedIdentity();
+
+ let prefsBundle = document.getElementById("bundle_prefs");
+ let confirmTitle = prefsBundle.getFormattedString(
+ "identity-delete-confirm-title",
+ [window.arguments[0].accountName]
+ );
+ let confirmText = prefsBundle.getFormattedString("identity-delete-confirm", [
+ selectedIdentity.identityName,
+ ]);
+ let confirmButton = prefsBundle.getString("identity-delete-confirm-button");
+
+ if (
+ Services.prompt.confirmEx(
+ window,
+ confirmTitle,
+ confirmText,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL,
+ confirmButton,
+ null,
+ null,
+ null,
+ {}
+ )
+ ) {
+ return;
+ }
+
+ let selectedItemIndex = gIdentityListBox.selectedIndex;
+
+ gAccount.removeIdentity(selectedIdentity);
+
+ refreshIdentityList(selectedItemIndex);
+}
+
+function onOk() {
+ window.arguments[0].result = true;
+}
diff --git a/comm/mailnews/base/prefs/content/am-identities-list.xhtml b/comm/mailnews/base/prefs/content/am-identities-list.xhtml
new file mode 100644
index 0000000000..243630bd75
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identities-list.xhtml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-identities-list.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 600px"
+ scrolling="false"
+>
+ <head>
+ <title><!-- identity-list-title --></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-identities-list.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&identitiesListClose.label;"
+ buttonaccesskeyaccept="&identitiesListClose.accesskey;"
+ style="height: 100vh"
+ >
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <commandset>
+ <command id="cmd_add" oncommand="openIdentityEditor(null);" />
+ <command id="cmd_edit" oncommand="onEdit(event);" disabled="true" />
+ <command
+ id="cmd_default"
+ oncommand="onSetDefault(event);"
+ disabled="true"
+ />
+ <command id="cmd_delete" oncommand="onDelete(event);" disabled="true" />
+ </commandset>
+
+ <keyset>
+ <key keycode="VK_INSERT" command="cmd_add" />
+ <key keycode="VK_DELETE" command="cmd_delete" />
+ </keyset>
+
+ <label control="identitiesList">&identitiesListManageDesc.label;</label>
+
+ <separator class="thin" />
+
+ <html:div class="grid-two-column-auto-min" style="flex: 1">
+ <richlistbox
+ id="identitiesList"
+ ondblclick="onEdit(event);"
+ onselect="updateButtons();"
+ seltype="single"
+ flex="1"
+ style="min-height: 10em; max-height: 20em"
+ />
+
+ <html:div class="flex-content-column">
+ <button
+ id="addButton"
+ command="cmd_add"
+ label="&identitiesListAdd.label;"
+ accesskey="&identitiesListAdd.accesskey;"
+ />
+ <button
+ id="editButton"
+ disabled="true"
+ command="cmd_edit"
+ label="&identitiesListEdit.label;"
+ accesskey="&identitiesListEdit.accesskey;"
+ />
+ <button
+ id="setDefaultButton"
+ disabled="true"
+ command="cmd_default"
+ label="&identitiesListDefault.label;"
+ accesskey="&identitiesListDefault.accesskey;"
+ />
+ <button
+ id="deleteButton"
+ disabled="true"
+ command="cmd_delete"
+ label="&identitiesListDelete.label;"
+ accesskey="&identitiesListDelete.accesskey;"
+ />
+ </html:div>
+ </html:div>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-identity-edit.js b/comm/mailnews/base/prefs/content/am-identity-edit.js
new file mode 100644
index 0000000000..216edcaa53
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identity-edit.js
@@ -0,0 +1,577 @@
+/* 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/. */
+
+/* import-globals-from am-addressing.js */
+/* import-globals-from am-copies.js */
+/* import-globals-from ../../../../mail/extensions/am-e2e/am-e2e.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gIdentity = null; // the identity we are editing (may be null for a new identity)
+var gAccount = null; // the account the identity is (or will be) associated with
+
+document.addEventListener("dialogaccept", onOk);
+
+function onLoadIdentityProperties() {
+ // extract the account
+ gIdentity = window.arguments[0].identity;
+ gAccount = window.arguments[0].account;
+ let prefBundle = document.getElementById("bundle_prefs");
+
+ if (gIdentity) {
+ let listName = gIdentity.identityName;
+ document.title = prefBundle.getFormattedString("identityDialogTitleEdit", [
+ listName,
+ ]);
+ } else {
+ document.title = prefBundle.getString("identityDialogTitleAdd");
+ }
+
+ loadSMTPServerList();
+
+ initIdentityValues(gIdentity);
+ initCopiesAndFolder(gIdentity);
+ initCompositionAndAddressing(gIdentity);
+ initE2EEncryption(gIdentity);
+
+ // E2E needs an email... hide until we have one.
+ document.getElementById("identityE2ETab").hidden = !gIdentity?.email;
+}
+
+// based on the values of gIdentity, initialize the identity fields we expose to the user
+function initIdentityValues(identity) {
+ function initSmtpServer(aServerKey) {
+ // Select a server in the SMTP server menulist by its key.
+ // The value of the identity.smtpServerKey is null when the
+ // "use default server" option is used so, if we get that passed in, select
+ // the useDefaultItem representing this option by using the value of "".
+ document.getElementById("identity.smtpServerKey").value = aServerKey || "";
+ }
+
+ if (identity) {
+ document.getElementById("identity.fullName").value = identity.fullName;
+ document.getElementById("identity.email").value = identity.email;
+ document.getElementById("identity.replyTo").value = identity.replyTo;
+ document.getElementById("identity.organization").value =
+ identity.organization;
+ document.getElementById("identity.attachSignature").checked =
+ identity.attachSignature;
+ document.getElementById("identity.htmlSigText").value =
+ identity.htmlSigText;
+ document.getElementById("identity.htmlSigFormat").checked =
+ identity.htmlSigFormat;
+
+ if (identity.signature) {
+ document.getElementById("identity.signature").value =
+ identity.signature.path;
+ }
+
+ document.getElementById("identity.attachVCard").checked =
+ identity.attachVCard;
+ document.getElementById("identity.escapedVCard").value =
+ identity.escapedVCard || "";
+
+ document.getElementById("identity.catchAll").checked = identity.catchAll;
+ document.getElementById("identity.catchAllHint").value =
+ identity.catchAllHint;
+
+ initSmtpServer(identity.smtpServerKey);
+
+ // In am-main.xhtml this field has no ID, because it's hidden by other means.
+ let catchAllBox = document.getElementById("identityCatchAllBox");
+ if (catchAllBox) {
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ catchAllBox.hidden = servers.length > 0 && servers[0].type == "nntp";
+ }
+
+ // This field does not exist for the default identity shown in the am-main.xhtml pane.
+ let idLabel = document.getElementById("identity.label");
+ if (idLabel) {
+ idLabel.value = identity.label;
+ }
+ } else {
+ // We're adding an identity, use the best default we have.
+ initSmtpServer(gAccount.defaultIdentity.smtpServerKey);
+
+ // Hide catchAll until we know what this identitity is associated with.
+ document.getElementById("identityCatchAllBox").hidden = true;
+ }
+
+ setupSignatureItems();
+}
+
+function initCopiesAndFolder(identity) {
+ // if we are editing an existing identity, use it...otherwise copy our values from the default identity
+ var copiesAndFoldersIdentity = identity ? identity : gAccount.defaultIdentity;
+
+ document.getElementById("identity.fccFolder").value =
+ copiesAndFoldersIdentity.fccFolder;
+ document.getElementById("identity.draftFolder").value =
+ copiesAndFoldersIdentity.draftFolder;
+ document.getElementById("identity.archiveFolder").value =
+ copiesAndFoldersIdentity.archiveFolder;
+ document.getElementById("identity.stationeryFolder").value =
+ copiesAndFoldersIdentity.stationeryFolder;
+
+ document.getElementById("identity.fccFolderPickerMode").value =
+ copiesAndFoldersIdentity.fccFolderPickerMode
+ ? copiesAndFoldersIdentity.fccFolderPickerMode
+ : 0;
+ document.getElementById("identity.draftsFolderPickerMode").value =
+ copiesAndFoldersIdentity.draftsFolderPickerMode
+ ? copiesAndFoldersIdentity.draftsFolderPickerMode
+ : 0;
+ document.getElementById("identity.archivesFolderPickerMode").value =
+ copiesAndFoldersIdentity.archivesFolderPickerMode
+ ? copiesAndFoldersIdentity.archivesFolderPickerMode
+ : 0;
+ document.getElementById("identity.tmplFolderPickerMode").value =
+ copiesAndFoldersIdentity.tmplFolderPickerMode
+ ? copiesAndFoldersIdentity.tmplFolderPickerMode
+ : 0;
+
+ document.getElementById("identity.doCc").checked =
+ copiesAndFoldersIdentity.doCc;
+ document.getElementById("identity.doCcList").value =
+ copiesAndFoldersIdentity.doCcList;
+ document.getElementById("identity.doBcc").checked =
+ copiesAndFoldersIdentity.doBcc;
+ document.getElementById("identity.doBccList").value =
+ copiesAndFoldersIdentity.doBccList;
+ document.getElementById("identity.doFcc").checked =
+ copiesAndFoldersIdentity.doFcc;
+ document.getElementById("identity.fccReplyFollowsParent").checked =
+ copiesAndFoldersIdentity.fccReplyFollowsParent;
+ document.getElementById("identity.showSaveMsgDlg").checked =
+ copiesAndFoldersIdentity.showSaveMsgDlg;
+ document.getElementById("identity.archiveEnabled").checked =
+ copiesAndFoldersIdentity.archiveEnabled;
+
+ onInitCopiesAndFolders(); // am-copies.js method
+}
+
+function initCompositionAndAddressing(identity) {
+ // if we are editing an existing identity, use it...otherwise copy our values from the default identity
+ var addressingIdentity = identity ? identity : gAccount.defaultIdentity;
+
+ document.getElementById("identity.directoryServer").value =
+ addressingIdentity.directoryServer;
+ document.getElementById("identity.overrideGlobal_Pref").value =
+ addressingIdentity.overrideGlobalPref;
+ let autoCompleteElement = document.getElementById(
+ "identity.autocompleteToMyDomain"
+ );
+ if (autoCompleteElement) {
+ // Thunderbird does not have this element.
+ autoCompleteElement.checked = addressingIdentity.autocompleteToMyDomain;
+ }
+
+ document.getElementById("identity.composeHtml").checked =
+ addressingIdentity.composeHtml;
+ document.getElementById("identity.autoQuote").checked =
+ addressingIdentity.autoQuote;
+ document.getElementById("identity.replyOnTop").value =
+ addressingIdentity.replyOnTop;
+ document.getElementById("identity.sig_bottom").value =
+ addressingIdentity.sigBottom;
+ document.getElementById("identity.sig_on_reply").checked =
+ addressingIdentity.sigOnReply;
+ document.getElementById("identity.sig_on_fwd").checked =
+ addressingIdentity.sigOnForward;
+
+ onInitCompositionAndAddressing(); // am-addressing.js method
+}
+
+function onOk(event) {
+ if (!validEmailAddress()) {
+ event.preventDefault();
+ return;
+ }
+
+ // if we are adding a new identity, create an identity, set the fields and add it to the
+ // account.
+ if (!gIdentity) {
+ // ask the account manager to create a new identity for us
+ gIdentity = MailServices.accounts.createIdentity();
+
+ // copy in the default identity settings so we inherit lots of stuff like the default drafts folder, etc.
+ gIdentity.copy(gAccount.defaultIdentity);
+
+ // assume the identity is valid by default?
+ gIdentity.valid = true;
+
+ // add the identity to the account
+ gAccount.addIdentity(gIdentity);
+
+ // now fall through to saveFields which will save our new values
+ }
+
+ // if we are modifying an existing identity, save the fields
+ saveIdentitySettings(gIdentity);
+ saveCopiesAndFolderSettings(gIdentity);
+ saveAddressingAndCompositionSettings(gIdentity);
+ saveE2EEncryptionSettings(gIdentity);
+
+ window.arguments[0].result = true;
+}
+
+// returns false and prompts the user if
+// the identity does not have an email address
+function validEmailAddress() {
+ var emailAddress = document.getElementById("identity.email").value;
+
+ // quickly test for an @ sign to test for an email address. We don't have
+ // to be anymore precise than that.
+ if (!emailAddress.includes("@")) {
+ // alert user about an invalid email address
+
+ var prefBundle = document.getElementById("bundle_prefs");
+
+ Services.prompt.alert(
+ window,
+ prefBundle.getString("identity-edit-req-title"),
+ prefBundle.getString("identity-edit-req")
+ );
+ return false;
+ }
+
+ return true;
+}
+
+function saveIdentitySettings(identity) {
+ if (identity) {
+ let idLabel = document.getElementById("identity.label");
+ if (idLabel) {
+ identity.label = idLabel.value;
+ }
+ identity.fullName = document.getElementById("identity.fullName").value;
+ identity.email = document.getElementById("identity.email").value;
+ identity.replyTo = document.getElementById("identity.replyTo").value;
+ identity.organization = document.getElementById(
+ "identity.organization"
+ ).value;
+ identity.attachSignature = document.getElementById(
+ "identity.attachSignature"
+ ).checked;
+ identity.htmlSigText = document.getElementById(
+ "identity.htmlSigText"
+ ).value;
+ identity.htmlSigFormat = document.getElementById(
+ "identity.htmlSigFormat"
+ ).checked;
+
+ identity.attachVCard = document.getElementById(
+ "identity.attachVCard"
+ ).checked;
+ identity.escapedVCard = document.getElementById(
+ "identity.escapedVCard"
+ ).value;
+ identity.catchAll = document.getElementById("identity.catchAll").checked;
+ identity.catchAllHint = document.getElementById(
+ "identity.catchAllHint"
+ ).value;
+ identity.smtpServerKey = document.getElementById(
+ "identity.smtpServerKey"
+ ).value;
+
+ let attachSignaturePath =
+ document.getElementById("identity.signature").value;
+ identity.signature = null; // this is important so we don't accidentally inherit the default
+
+ if (attachSignaturePath) {
+ // convert signature path back into a nsIFile
+ var sfile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ sfile.initWithPath(attachSignaturePath);
+ if (sfile.exists()) {
+ identity.signature = sfile;
+ }
+ }
+ }
+}
+
+function saveCopiesAndFolderSettings(identity) {
+ onSaveCopiesAndFolders(); // am-copies.js routine
+
+ identity.fccFolder = document.getElementById("identity.fccFolder").value;
+ identity.draftFolder = document.getElementById("identity.draftFolder").value;
+ identity.archiveFolder = document.getElementById(
+ "identity.archiveFolder"
+ ).value;
+ identity.stationeryFolder = document.getElementById(
+ "identity.stationeryFolder"
+ ).value;
+ identity.fccFolderPickerMode = document.getElementById(
+ "identity.fccFolderPickerMode"
+ ).value;
+ identity.draftsFolderPickerMode = document.getElementById(
+ "identity.draftsFolderPickerMode"
+ ).value;
+ identity.archivesFolderPickerMode = document.getElementById(
+ "identity.archivesFolderPickerMode"
+ ).value;
+ identity.tmplFolderPickerMode = document.getElementById(
+ "identity.tmplFolderPickerMode"
+ ).value;
+ identity.doCc = document.getElementById("identity.doCc").checked;
+ identity.doCcList = document.getElementById("identity.doCcList").value;
+ identity.doBcc = document.getElementById("identity.doBcc").checked;
+ identity.doBccList = document.getElementById("identity.doBccList").value;
+ identity.doFcc = document.getElementById("identity.doFcc").checked;
+ identity.fccReplyFollowsParent = document.getElementById(
+ "identity.fccReplyFollowsParent"
+ ).checked;
+ identity.showSaveMsgDlg = document.getElementById(
+ "identity.showSaveMsgDlg"
+ ).checked;
+ identity.archiveEnabled = document.getElementById(
+ "identity.archiveEnabled"
+ ).checked;
+}
+
+function saveAddressingAndCompositionSettings(identity) {
+ identity.directoryServer = document.getElementById(
+ "identity.directoryServer"
+ ).value;
+ identity.overrideGlobalPref =
+ document.getElementById("identity.overrideGlobal_Pref").value == "true";
+ let autoCompleteElement = document.getElementById(
+ "identity.autocompleteToMyDomain"
+ );
+ if (autoCompleteElement) {
+ // Thunderbird does not have this element.
+ identity.autocompleteToMyDomain = autoCompleteElement.checked;
+ }
+ identity.composeHtml = document.getElementById(
+ "identity.composeHtml"
+ ).checked;
+ identity.autoQuote = document.getElementById("identity.autoQuote").checked;
+ identity.replyOnTop = document.getElementById("identity.replyOnTop").value;
+ identity.sigBottom =
+ document.getElementById("identity.sig_bottom").value == "true";
+ identity.sigOnReply = document.getElementById(
+ "identity.sig_on_reply"
+ ).checked;
+ identity.sigOnForward = document.getElementById(
+ "identity.sig_on_fwd"
+ ).checked;
+}
+
+function selectFile() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+
+ var prefBundle = document.getElementById("bundle_prefs");
+ var title = prefBundle.getString("choosefile");
+ fp.init(window, title, nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterAll);
+
+ // Get current signature folder, if there is one.
+ // We can set that to be the initial folder so that users
+ // can maintain their signatures better.
+ var sigFolder = GetSigFolder();
+ if (sigFolder) {
+ fp.displayDirectory = sigFolder;
+ }
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ document.getElementById("identity.signature").value = fp.file.path;
+ document
+ .getElementById("identity.signature")
+ .dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+/**
+ * Adjust the catch-all hint so that is removes stars from the allowed pattern.
+ * We only allow to use stars for matching full domains *@example.com,
+ * not *foo@example.com.
+ *
+ * @param {Event} event - the oninput event of the catchAllHint input field.
+ */
+function handleInputCatchAllHint(event) {
+ let value = event.target.value;
+ event.target.value = value
+ .replace(/(\*[^@]+)/g, "*")
+ .replace(/(^|\s)@/g, "$1*@")
+ .replace(/\s*[;,]/g, ",")
+ .replace(/\s+/g, " ");
+}
+
+function GetSigFolder() {
+ var sigFolder = null;
+ try {
+ var account = parent.getCurrentAccount();
+ var identity = account.defaultIdentity;
+ var signatureFile = identity.signature;
+
+ if (signatureFile) {
+ signatureFile = signatureFile.QueryInterface(Ci.nsIFile);
+ sigFolder = signatureFile.parent;
+
+ if (!sigFolder.exists()) {
+ sigFolder = null;
+ }
+ }
+ } catch (ex) {
+ dump("failed to get signature folder..\n");
+ }
+ return sigFolder;
+}
+
+// Signature textbox is active unless option to select from file is checked.
+// If a signature is need to be attached, the associated items which
+// displays the absolute path to the signature (in a textbox) and the way
+// to select a new signature file (a button) are enabled. Otherwise, they
+// are disabled. Check to see if the attachSignature is locked to block
+// broadcasting events.
+function setupSignatureItems() {
+ var signature = document.getElementById("identity.signature");
+ var browse = document.getElementById("identity.sigbrowsebutton");
+ var htmlSigText = document.getElementById("identity.htmlSigText");
+ var htmlSigFormat = document.getElementById("identity.htmlSigFormat");
+ var attachSignature = document.getElementById("identity.attachSignature");
+ var checked = attachSignature.checked;
+
+ if (checked) {
+ htmlSigText.setAttribute("disabled", "disabled");
+ htmlSigFormat.setAttribute("disabled", "true");
+ } else {
+ htmlSigText.removeAttribute("disabled");
+ htmlSigFormat.removeAttribute("disabled");
+ }
+
+ if (checked && !getAccountValueIsLocked(signature)) {
+ signature.removeAttribute("disabled");
+ } else {
+ signature.setAttribute("disabled", "disabled");
+ }
+
+ if (checked && !getAccountValueIsLocked(browse)) {
+ browse.removeAttribute("disabled");
+ } else {
+ browse.setAttribute("disabled", "true");
+ }
+}
+
+function editVCard() {
+ // Read vCard hidden value from UI.
+ let escapedVCard = document.getElementById("identity.escapedVCard");
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+ let vCardEdit = dialog.querySelector("vcard-edit");
+
+ vCardEdit.vCardString = decodeURIComponent(escapedVCard.value);
+
+ top.addEventListener("keydown", editVCardKeyDown, { capture: true });
+ form.addEventListener("submit", editVCardSubmit);
+ form.addEventListener("reset", editVCardReset);
+
+ top.gSubDialog._topDialog?._overlay.removeAttribute("topmost");
+ dialog.showModal();
+}
+
+function editVCardKeyDown(event) {
+ let dialog = top.document.getElementById("editVCardDialog");
+ if (event.keyCode == KeyboardEvent.DOM_VK_ESCAPE && dialog.open) {
+ // This is a bit of a hack to prevent other dialogs (particularly
+ // SubDialogs) from closing when the vCard dialog is open.
+ event.preventDefault();
+ editVCardReset();
+ }
+}
+
+function editVCardSubmit(event) {
+ let escapedVCard = document.getElementById("identity.escapedVCard");
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+ let vCardEdit = dialog.querySelector("vcard-edit");
+
+ vCardEdit.saveVCard();
+ escapedVCard.value = encodeURIComponent(vCardEdit.vCardString);
+ // Trigger a change event so for the am-main view the event listener
+ // set up in AccountManager.js onLoad() can make sure to save the change.
+ escapedVCard.dispatchEvent(new CustomEvent("change"));
+
+ top.gSubDialog._topDialog?._overlay.setAttribute("topmost", "true");
+ dialog.close();
+
+ event.preventDefault();
+ form.removeEventListener("submit", editVCardSubmit);
+ form.removeEventListener("reset", editVCardReset);
+}
+
+function editVCardReset() {
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+
+ top.gSubDialog._topDialog?._overlay.setAttribute("topmost", "true");
+ dialog.close();
+
+ form.removeEventListener("submit", editVCardSubmit);
+ form.removeEventListener("reset", editVCardReset);
+}
+
+function getAccountForFolderPickerState() {
+ return gAccount;
+}
+
+/**
+ * Build the SMTP server list for display.
+ */
+function loadSMTPServerList() {
+ var smtpServerList = document.getElementById("identity.smtpServerKey");
+ let defaultServer = MailServices.smtp.defaultServer;
+ let currentValue = smtpServerList.value;
+
+ var smtpPopup = smtpServerList.menupopup;
+ while (smtpPopup.lastChild.nodeName != "menuseparator") {
+ smtpPopup.lastChild.remove();
+ }
+
+ for (let server of MailServices.smtp.servers) {
+ let serverName = "";
+ if (server.description) {
+ serverName = server.description + " - ";
+ } else if (server.username) {
+ serverName = server.username + " - ";
+ }
+ serverName += server.hostname;
+
+ if (defaultServer.key == server.key) {
+ serverName +=
+ " " +
+ document
+ .getElementById("bundle_messenger")
+ .getString("defaultServerTag");
+ }
+
+ smtpServerList.appendItem(serverName, server.key);
+ }
+
+ smtpServerList.value = currentValue;
+}
+
+/**
+ * Open dialog for editing properties of currently selected SMTP server.
+ */
+function editCurrentSMTP() {
+ let smtpKey = document.getElementById("identity.smtpServerKey").value;
+ let server =
+ smtpKey === ""
+ ? MailServices.smtp.defaultServer
+ : MailServices.smtp.getServerByKey(smtpKey);
+ let args = { server, result: false, addSmtpServer: "" };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/SmtpServerEdit.xhtml",
+ { closingCallback: loadSMTPServerList },
+ args
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-identity-edit.xhtml b/comm/mailnews/base/prefs/content/am-identity-edit.xhtml
new file mode 100644
index 0000000000..d967b18202
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identity-edit.xhtml
@@ -0,0 +1,239 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+
+<!DOCTYPE html [
+<!ENTITY % identityEditDTD SYSTEM "chrome://messenger/locale/am-identity-edit.dtd" >
+%identityEditDTD;
+<!ENTITY % identityDTD SYSTEM "chrome://messenger/locale/am-main.dtd" >
+%identityDTD;
+<!ENTITY % copiesDTD SYSTEM "chrome://messenger/locale/am-copies.dtd">
+%copiesDTD;
+<!ENTITY % addressingDTD SYSTEM "chrome://messenger/locale/am-addressing.dtd" >
+%addressingDTD;
+<!ENTITY % e2eDTD SYSTEM "chrome://messenger/locale/am-smime.dtd" >
+%e2eDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="height: 900px; min-width: 800px;">
+<head>
+ <title><!-- identityDialogTitleEdit --></title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/preferencesBindings.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-copies.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-addressing.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-e2e.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-identity-edit.js"></script>
+ <script>
+ // NOTE: am-identity-edit.js is not only used for this page.
+ window.addEventListener("DOMContentLoaded", onLoadIdentityProperties);
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="identityDialog">
+ <stringbundle id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"/>
+ <stringbundle id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"/>
+
+ <tabbox flex="1" style="overflow: hidden;">
+ <tabs id="identitySettings">
+ <tab id="identitySettingsTab" label="&settingsTab.label;"/>
+ <tab id="identityCopiesFoldersTab" label="&copiesFoldersTab.label;"/>
+ <tab id="identityAddressingTab" label="&addressingTab.label;"/>
+ <tab id="identityE2ETab" label="&e2eTitle.label;"/>
+ </tabs>
+
+ <tabpanels id="identityTabsPanels" flex="1">
+ <!-- Identity Settings Tab -->
+ <vbox flex="1" name="settings">
+ <html:div>
+ <html:fieldset>
+ <html:legend>&publicData.label;</html:legend>
+ <html:table class="identity-table">
+ <html:tr>
+ <html:th>
+ <label id="identity.fullName.label"
+ value="&name.label;"
+ control="identity.fullName"
+ accesskey="&name.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.fullName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.fullName.label"
+ size="30"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.email.label"
+ value="&email.label;"
+ control="identity.email"
+ accesskey="&email.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.email"
+ type="email"
+ class="uri-element input-inline"
+ aria-labelledby="identity.email.label"/>
+
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.replyTo.label"
+ value="&replyTo.label;"
+ accesskey="&replyTo.accesskey;"
+ control="identity.replyTo"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.replyTo"
+ type="text"
+ class="uri-element input-inline"
+ placeholder="&replyTo.placeholder;"
+ aria-labelledby="identity.replyTo.label"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.organization.label"
+ value="&organization.label;"
+ control="identity.organization"
+ accesskey="&organization.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.organization"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.organization.label"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label value="&signatureText.label;" control="identity.htmlSigText"
+ accesskey="&signatureText.accesskey;"/>
+ </html:th>
+ <html:td style="width:100%;">
+ <checkbox id="identity.htmlSigFormat" label="&signatureHtml.label;"
+ accesskey="&signatureHtml.accesskey;" style="width:100%;"/>
+ </html:td>
+ </html:tr>
+ </html:table>
+
+ <separator class="thin"/>
+
+ <hbox class="indent input-container" flex="1">
+ <html:textarea id="identity.htmlSigText" style="flex-grow: 1;" rows="4" class="signatureBox"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="identity.attachSignature" label="&signatureFile.label;"
+ flex="1" accesskey="&signatureFile.accesskey;"
+ oncommand="setupSignatureItems();"/>
+ </hbox>
+
+ <hbox align="center" class="indent input-container">
+ <html:input id="identity.signature"
+ type="text"
+ datatype="nsIFile"
+ name="identity.signature"
+ aria-labelledby="identity.attachSignature"
+ class="uri-element input-inline"/>
+ <button id="identity.sigbrowsebutton" class="push"
+ label="&choose.label;" accesskey="&choose.accesskey;"
+ oncommand="selectFile()"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="identity.attachVCard" label="&attachVCard.label;"
+ flex="1" accesskey="&attachVCard.accesskey;"/>
+ <button class="push" label="&editVCard.label;"
+ accesskey="&editVCard.accesskey;" oncommand="editVCard()"/>
+ <label id="identity.escapedVCard" hidden="true"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&privateData.label;</html:legend>
+
+ <hbox id="identityCatchAllBox" align="center" class="input-container">
+ <checkbox id="identity.catchAll"
+ label="&catchAll.label;"
+ accesskey="&catchAll.accesskey;"
+ style="margin-block:auto;"/>
+ <html:input id="identity.catchAllHint"
+ type="text"
+ oninput="handleInputCatchAllHint(event);"
+ placeholder="list@example.com, *@example.com"
+ class="input-inline"
+ aria-labelledby="identity.catchAll"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <label value="&smtpName.label;"
+ control="identity.smtpServerKey"
+ accesskey="&smtpName.accesskey;"/>
+ <hbox align="center">
+ <menulist id="identity.smtpServerKey" flex="1">
+ <menupopup id="smtpPopup">
+ <menuitem id="useDefaultItem" value=""
+ label="&smtpDefaultServer.label;"/>
+ <menuseparator/>
+ <!-- list will be inserted here -->
+ </menupopup>
+ </menulist>
+
+ <button id="editSmtp"
+ label="&smtpServerEdit.label;"
+ accesskey="&smtpServerEdit.accesskey;"
+ oncommand="editCurrentSMTP();"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" class="input-container">
+ <label id="identity.input.label" value="&identityAlias.label;"
+ accesskey="&identityAlias.accesskey;"
+ style="margin-block: auto;"
+ control="identity.label"/>
+ <html:input id="identity.label"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.input.label"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ </vbox>
+
+ <!-- Copies & Folders Tab -->
+#include am-copies.inc.xhtml
+
+ <!-- Composition & Addressing Tab -->
+#include am-addressing.inc.xhtml
+
+ <!-- Security Tab -->
+#include ../../../../mail/extensions/am-e2e/am-e2e.inc.xhtml
+
+ </tabpanels>
+ </tabbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-junk.js b/comm/mailnews/base/prefs/content/am-junk.js
new file mode 100644
index 0000000000..a102efdfae
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-junk.js
@@ -0,0 +1,335 @@
+/* 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/. */
+
+/* import-globals-from am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gDeferredToAccount = "";
+
+function onInit(aPageId, aServerId) {
+ // manually adjust several pref UI elements
+ document.getElementById("server.spamLevel.visible").checked =
+ document.getElementById("server.spamLevel").value > 0;
+
+ let deferredToURI = null;
+ if (gDeferredToAccount) {
+ deferredToURI =
+ MailServices.accounts.getAccount(gDeferredToAccount).incomingServer
+ .serverURI;
+ }
+
+ let spamActionTargetAccountElement = document.getElementById(
+ "server.spamActionTargetAccount"
+ );
+ let spamActionTargetFolderElement = document.getElementById(
+ "server.spamActionTargetFolder"
+ );
+
+ let spamActionTargetAccount = spamActionTargetAccountElement.value;
+ let spamActionTargetFolder = spamActionTargetFolderElement.value;
+
+ let moveOnSpamCheckbox = document.getElementById("server.moveOnSpam");
+ let moveOnSpamValue = moveOnSpamCheckbox.checked;
+
+ // Check if there are any invalid junk targets and fix them.
+ [spamActionTargetAccount, spamActionTargetFolder, moveOnSpamValue] =
+ sanitizeJunkTargets(
+ spamActionTargetAccount,
+ spamActionTargetFolder,
+ deferredToURI || aServerId,
+ document.getElementById("server.moveTargetMode").value,
+ MailUtils.getOrCreateFolder(aServerId).server.spamSettings,
+ moveOnSpamValue
+ );
+
+ spamActionTargetAccountElement.value = spamActionTargetAccount;
+ spamActionTargetFolderElement.value = spamActionTargetFolder;
+ moveOnSpamCheckbox.checked = moveOnSpamValue;
+
+ let server = MailUtils.getOrCreateFolder(spamActionTargetAccount);
+ document.getElementById("actionAccountPopup").selectFolder(server);
+
+ let folder = MailUtils.getExistingFolder(spamActionTargetFolder);
+ document.getElementById("actionFolderPopup").selectFolder(folder);
+
+ var currentArray = [];
+ if (document.getElementById("server.useWhiteList").checked) {
+ currentArray = document
+ .getElementById("server.whiteListAbURI")
+ .value.split(" ");
+ }
+
+ // set up the whitelist UI
+ var wList = document.getElementById("whiteListAbURI");
+ // Ensure the whitelist is empty
+ while (wList.hasChildNodes()) {
+ wList.lastChild.remove();
+ }
+
+ // Populate the listbox with address books
+ let abItems = [];
+ for (let ab of MailServices.ab.directories) {
+ // We skip mailing lists and remote address books.
+ if (ab.isMailList || ab.isRemote) {
+ continue;
+ }
+
+ abItems.push({ label: ab.dirName, URI: ab.URI });
+ }
+
+ // Sort the list
+ function sortFunc(a, b) {
+ return a.label.localeCompare(b.label);
+ }
+ abItems.sort(sortFunc);
+
+ // And then append each item to the listbox
+ for (let abItem of abItems) {
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("label", abItem.label);
+ checkbox.checked = currentArray.includes(abItem.URI);
+
+ let item = document.createXULElement("richlistitem");
+ item.appendChild(checkbox);
+ item.setAttribute("value", abItem.URI);
+ wList.appendChild(item);
+ }
+
+ wList.addEventListener("keypress", event => {
+ if ([" ", "Enter"].includes(event.key)) {
+ let checkbox = wList.currentItem.firstElementChild;
+ checkbox.checked = !checkbox.checked;
+ wList.dispatchEvent(new CustomEvent("command"));
+ }
+ });
+
+ // enable or disable the whitelist
+ onAdaptiveJunkToggle();
+
+ // set up trusted IP headers
+ var serverFilterList = document.getElementById("useServerFilterList");
+ serverFilterList.value = document.getElementById(
+ "server.serverFilterName"
+ ).value;
+ if (!serverFilterList.selectedItem) {
+ serverFilterList.selectedIndex = 0;
+ }
+
+ // enable or disable the useServerFilter checkbox
+ onCheckItem("useServerFilterList", ["server.useServerFilter"]);
+
+ updateJunkTargetsAndRetention();
+}
+
+function onPreInit(account, accountValues) {
+ if (
+ top.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ ) == "pop3"
+ ) {
+ gDeferredToAccount = top.getAccountValue(
+ account,
+ accountValues,
+ "pop3",
+ "deferredToAccount",
+ null,
+ false
+ );
+ }
+
+ buildServerFilterMenuList();
+}
+
+/**
+ * Called when someone checks or unchecks the adaptive junk mail checkbox.
+ * set the value of the hidden element accordingly
+ *
+ * @param {boolean} aValue - The boolean value of the checkbox.
+ */
+function updateSpamLevel(aValue) {
+ document.getElementById("server.spamLevel").value = aValue ? 100 : 0;
+ onAdaptiveJunkToggle();
+}
+
+/**
+ * Propagate changes to the server filter menu list back to
+ * our hidden wsm element.
+ */
+function onServerFilterListChange() {
+ document.getElementById("server.serverFilterName").value =
+ document.getElementById("useServerFilterList").value;
+}
+
+/**
+ * Called when someone checks or unchecks the adaptive junk mail checkbox.
+ * We need to enable or disable the whitelist accordingly.
+ */
+function onAdaptiveJunkToggle() {
+ onCheckItem("whiteListAbURI", ["server.spamLevel.visible"]);
+ onCheckItem("whiteListLabel", ["server.spamLevel.visible"]);
+
+ // Enable/disable individual listbox rows.
+ // Setting enable/disable on the parent listbox does not seem to work.
+ let wList = document.getElementById("whiteListAbURI");
+ let wListDisabled = wList.disabled;
+
+ for (let i = 0; i < wList.getRowCount(); i++) {
+ let item = wList.getItemAtIndex(i);
+ item.setAttribute("disabled", wListDisabled);
+ item.firstElementChild.setAttribute("disabled", wListDisabled);
+ }
+}
+
+/**
+ * Called when someone checks or unchecks the "move new junk messages to"
+ * Enable/disable the radio group accordingly.
+ */
+function updateJunkTargetsAndRetention() {
+ onCheckItem("server.moveTargetMode", ["server.moveOnSpam"]);
+ updateJunkTargets();
+ onCheckItem("server.purgeSpam", ["server.moveOnSpam"]);
+ document.getElementById("purgeLabel").disabled =
+ document.getElementById("server.purgeSpam").disabled;
+ updateJunkRetention();
+}
+
+/**
+ * Enable/disable the folder pickers depending on which radio item is selected.
+ */
+function updateJunkTargets() {
+ onCheckItem("actionTargetAccount", ["server.moveOnSpam", "moveTargetMode0"]);
+ onCheckItem("actionTargetFolder", ["server.moveOnSpam", "moveTargetMode1"]);
+}
+
+/**
+ * Enable/disable the junk deletion interval depending on the state
+ * of the controlling checkbox.
+ */
+function updateJunkRetention() {
+ onCheckItem("server.purgeSpamInterval", [
+ "server.purgeSpam",
+ "server.moveOnSpam",
+ ]);
+}
+
+function onSave() {
+ onSaveWhiteList();
+}
+
+/**
+ * Propagate changes to the whitelist menu list back to
+ * our hidden wsm element.
+ */
+function onSaveWhiteList() {
+ var wList = document.getElementById("whiteListAbURI");
+ var wlArray = [];
+
+ for (let i = 0; i < wList.getRowCount(); i++) {
+ // Due to bug 448582, do not trust any properties of the listitems
+ // as they may not return the right value or may even not exist.
+ // Always get the attributes only.
+ var wlNode = wList.getItemAtIndex(i);
+ if (wlNode.firstElementChild.getAttribute("checked") == "true") {
+ let abURI = wlNode.getAttribute("value");
+ wlArray.push(abURI);
+ }
+ }
+ var wlValue = wlArray.join(" ");
+ document
+ .getElementById("server.whiteListAbURI")
+ .setAttribute("value", wlValue);
+ document.getElementById("server.useWhiteList").checked = wlValue != "";
+}
+
+/**
+ * Called when a new value is chosen in one of the junk target folder pickers.
+ * Sets the menu label according to the folder name.
+ */
+function onActionTargetChange(aEvent, aWSMElementId) {
+ let folder = aEvent.target._folder;
+ document.getElementById(aWSMElementId).value = folder.URI;
+ document.getElementById("actionFolderPopup").selectFolder(folder);
+}
+
+/**
+ * Enumerates over the "ISPDL" directories, calling buildServerFilterListFromDir
+ * for each one.
+ */
+function buildServerFilterMenuList() {
+ const KEY_ISP_DIRECTORY_LIST = "ISPDL";
+ let ispHeaderList = document.getElementById("useServerFilterList");
+
+ // Ensure the menulist is empty.
+ ispHeaderList.removeAllItems();
+
+ // Now walk through the isp directories looking for sfd files.
+ let ispDirectories = Services.dirsvc.get(
+ KEY_ISP_DIRECTORY_LIST,
+ Ci.nsISimpleEnumerator
+ );
+
+ let menuEntries = [];
+ while (ispDirectories.hasMoreElements()) {
+ let ispDirectory = ispDirectories.getNext().QueryInterface(Ci.nsIFile);
+ if (ispDirectory) {
+ menuEntries.push.apply(
+ menuEntries,
+ buildServerFilterListFromDir(ispDirectory, menuEntries)
+ );
+ }
+ }
+
+ menuEntries.sort((a, b) => a.localeCompare(b));
+ for (let entry of menuEntries) {
+ ispHeaderList.appendItem(entry, entry);
+ }
+}
+
+/**
+ * Helper function called by buildServerFilterMenuList. Enumerates over the
+ * passed in directory looking for .sfd files. For each entry found, it gets
+ * appended to the menu list.
+ *
+ * @param {nsIFile} aDir - Directory to look for .sfd files
+ * @param {string[]} aExistingEntries - Filter names already found.
+ */
+function buildServerFilterListFromDir(aDir, aExistingEntries) {
+ let newEntries = [];
+ // Now iterate over each file in the directory looking for .sfd files.
+ const kSuffix = ".sfd";
+
+ for (let entry of aDir.directoryEntries) {
+ // we only care about files that end in .sfd
+ if (entry.isFile() && entry.leafName.endsWith(kSuffix)) {
+ let fileName = entry.leafName.slice(0, -kSuffix.length);
+ // If we've already added an item with this name, then don't add it again.
+ if (!aExistingEntries.includes(fileName)) {
+ newEntries.push(fileName);
+ }
+ }
+ }
+ return newEntries;
+}
+
+/**
+ * Open the Preferences dialog on the Junk settings tab.
+ */
+function showGlobalJunkPrefs() {
+ openPrefsFromAccountManager(
+ "panePrivacy",
+ "privacyJunkCategory",
+ null,
+ "junk_pane"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-junk.xhtml b/comm/mailnews/base/prefs/content/am-junk.xhtml
new file mode 100644
index 0000000000..216d3c0431
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-junk.xhtml
@@ -0,0 +1,293 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % junkMailDTD SYSTEM "chrome://messenger/locale/am-junk.dtd">
+%junkMailDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&junkSettings.label;</title>
+ <script defer="defer" src="chrome://messenger/content/am-junk.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-junk.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&junkSettings.label;" />
+ </hbox>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&junkClassification.label;</html:legend>
+
+ <label
+ hidden="true"
+ id="server.spamLevel"
+ wsm_persist="true"
+ pref="true"
+ preftype="int"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamLevel"
+ />
+ <label
+ hidden="true"
+ id="server.spamActionTargetAccount"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamActionTargetAccount"
+ />
+ <label
+ hidden="true"
+ id="server.spamActionTargetFolder"
+ wsm_persist="true"
+ pref="true"
+ preftype="wstring"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamActionTargetFolder"
+ />
+ <label
+ hidden="true"
+ id="server.whiteListAbURI"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.whiteListAbURI"
+ />
+ <label
+ hidden="true"
+ id="server.serverFilterName"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.serverFilterName"
+ />
+
+ <checkbox
+ id="server.spamLevel.visible"
+ oncommand="updateSpamLevel(this.checked);"
+ accesskey="&level.accesskey;"
+ prefstring="mail.server.%serverkey%.spamLevel"
+ label="&level.label;"
+ />
+
+ <separator class="thin" />
+
+ <description width="1">&trainingDescription.label;</description>
+
+ <separator class="thin" />
+ <spacer height="3" />
+
+ <vbox class="indent">
+ <checkbox
+ hidden="true"
+ id="server.useWhiteList"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ prefstring="mail.server.%serverkey%.useWhiteList"
+ />
+ <label
+ id="whiteListLabel"
+ accesskey="&whitelistHeader.accesskey;"
+ control="whiteListAbURI"
+ >&whitelistHeader.label;</label
+ >
+ <richlistbox id="whiteListAbURI" height="200px" />
+ </vbox>
+
+ <separator />
+
+ <vbox>
+ <hbox>
+ <checkbox
+ id="server.useServerFilter"
+ label="&ispHeaders.label;"
+ accesskey="&ispHeaders.accesskey;"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ oncommand="onCheckItem('useServerFilterList', [this.id]);"
+ prefstring="mail.server.%serverkey%.useServerFilter"
+ />
+ <menulist
+ id="useServerFilterList"
+ oncommand="onServerFilterListChange();"
+ aria-labelledby="server.useServerFilter"
+ />
+ </hbox>
+ </vbox>
+
+ <separator class="thin" />
+
+ <description class="indent tip-caption" width="1"
+ >&ispHeadersWarning.label;</description
+ >
+ </html:fieldset>
+ </html:div>
+
+ <separator />
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&junkActions.label;</html:legend>
+
+ <checkbox
+ id="server.moveOnSpam"
+ label="&move.label;"
+ accesskey="&move.accesskey;"
+ oncommand="updateJunkTargetsAndRetention();"
+ wsm_persist="true"
+ pref="true"
+ preftype="bool"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.moveOnSpam"
+ />
+
+ <radiogroup
+ id="server.moveTargetMode"
+ aria-labelledby="server.moveOnSpam"
+ prefstring="mail.server.%serverkey%.moveTargetMode"
+ wsm_persist="true"
+ pref="true"
+ preftype="int"
+ genericattr="true"
+ oncommand="updateJunkTargets();"
+ prefvalue="value"
+ >
+ <hbox class="specialFolderPickerGrid indent">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="moveTargetMode0"
+ value="0"
+ label="&junkFolderOn.label;"
+ accesskey="&junkFolderOn.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="moveTargetMode1"
+ value="1"
+ label="&otherFolder.label;"
+ accesskey="&otherFolder.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist
+ id="actionTargetAccount"
+ class="folderMenuItem"
+ aria-labelledby="moveTargetMode0"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="actionAccountPopup"
+ class="menulist-menupopup"
+ expandFolders="false"
+ mode="filing"
+ oncommand="onActionTargetChange(event, 'server.spamActionTargetAccount');"
+ />
+ </menulist>
+ <menulist
+ id="actionTargetFolder"
+ class="folderMenuItem"
+ aria-labelledby="moveTargetMode1"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="actionFolderPopup"
+ mode="junk"
+ showFileHereLabel="true"
+ oncommand="onActionTargetChange(event, 'server.spamActionTargetFolder');"
+ />
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent">
+ <checkbox
+ id="server.purgeSpam"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ prefstring="mail.server.%serverkey%.purgeSpam"
+ accesskey="&purge1.accesskey;"
+ oncommand="updateJunkRetention();"
+ label="&purge1.label;"
+ />
+ <html:input
+ id="server.purgeSpamInterval"
+ type="number"
+ class="size3 input-inline"
+ min="1"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="int"
+ aria-labelledby="server.purgeSpam server.purgeSpamInterval purgeLabel"
+ prefstring="mail.server.%serverkey%.purgeSpamInterval"
+ />
+ <label
+ id="purgeLabel"
+ value="&purge2.label;"
+ control="server.purgeSpamInterval"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <hbox pack="end">
+ <button
+ id="globalJunkPrefsLink"
+ label="&globalJunkPrefs.label;"
+ accesskey="&globalJunkPrefs.accesskey;"
+ oncommand="showGlobalJunkPrefs();"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-main.js b/comm/mailnews/base/prefs/content/am-main.js
new file mode 100644
index 0000000000..b71ad49fb8
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-main.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/* import-globals-from am-identity-edit.js */
+
+var gAccount;
+
+/**
+ * Initialize am-main account settings page when it gets shown.
+ * Update an account's main settings title and set up signature items.
+ */
+function onInit() {
+ setAccountTitle();
+ setupSignatureItems();
+ Services.obs.addObserver(
+ onDefaultIdentityChange,
+ "account-default-identity-changed"
+ );
+}
+
+window.addEventListener("unload", function () {
+ Services.obs.removeObserver(
+ onDefaultIdentityChange,
+ "account-default-identity-changed"
+ );
+});
+
+/**
+ * If the default identity for the current account changes, loads the values
+ * from the new default identity.
+ */
+function onDefaultIdentityChange(subject, topic, data) {
+ if (data == gAccount.key) {
+ initIdentityValues(subject.QueryInterface(Ci.nsIMsgIdentity));
+ }
+}
+
+/**
+ * Handle the blur event of the #server.prettyName pref input.
+ * Update account name in account manager tree and account settings' main title.
+ *
+ * @param {Event} event - Blur event from the pretty name input.
+ */
+function serverPrettyNameOnBlur(event) {
+ parent.setAccountLabel(gAccount.key, event.target.value);
+ setAccountTitle();
+}
+
+/**
+ * Update an account's main settings title with the account name if applicable.
+ */
+function setAccountTitle() {
+ let accountName = document.getElementById("server.prettyName");
+ let title = document.querySelector("#am-main-title .dialogheader-title");
+ let titleValue = title.getAttribute("defaultTitle");
+ if (accountName.value) {
+ titleValue += " - " + accountName.value;
+ }
+
+ title.setAttribute("value", titleValue);
+ document.title = titleValue;
+}
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+ loadSMTPServerList();
+ let type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+}
+
+function manageIdentities() {
+ // We want to save the current identity information before bringing up the multiple identities
+ // UI. This ensures that the changes are reflected in the identity list dialog
+ // onSave();
+
+ if (!gAccount) {
+ return;
+ }
+
+ var accountName = document.getElementById("server.prettyName").value;
+
+ var args = { account: gAccount, accountName, result: false };
+
+ // save the current identity settings so they show up correctly
+ // if the user just changed them in the manage identities dialog
+ var identity = gAccount.defaultIdentity;
+ saveIdentitySettings(identity);
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-identities-list.xhtml",
+ { closingCallback: onCloseIdentities },
+ args
+ );
+
+ function onCloseIdentities() {
+ if (args.result) {
+ // Refresh the SMTP list in case the user changed server properties
+ // from the identity dialog.
+ loadSMTPServerList();
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-main.xhtml b/comm/mailnews/base/prefs/content/am-main.xhtml
new file mode 100644
index 0000000000..09f5453fb8
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-main.xhtml
@@ -0,0 +1,344 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-main.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-identity-edit.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-main.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-main.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox id="am-main-title" class="dialogheader">
+ <label class="dialogheader-title" defaultTitle="&accountTitle.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox class="input-container">
+ <label
+ id="server.prettyName.label"
+ value="&accountName.label;"
+ control="server.prettyName"
+ accesskey="&accountName.accesskey;"
+ />
+ <html:input
+ id="server.prettyName"
+ type="text"
+ wsm_persist="true"
+ class="input-inline"
+ onblur="serverPrettyNameOnBlur(event);"
+ prefstring="mail.server.%serverkey%.name"
+ aria-labelledby="server.prettyName.label"
+ />
+ </hbox>
+
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&identityTitle.label;</html:legend>
+ <description>&identityDesc.label;</description>
+ <separator class="thin" />
+ <html:table class="identity-table">
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.fullName.label.label"
+ value="&name.label;"
+ control="identity.fullName"
+ accesskey="&name.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.fullName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.fullName.label"
+ wsm_persist="true"
+ size="30"
+ prefstring="mail.identity.%identitykey%.fullName"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.email.label"
+ value="&email.label;"
+ control="identity.email"
+ accesskey="&email.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.email"
+ type="email"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.useremail"
+ class="uri-element input-inline"
+ aria-labelledby="identity.email.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.replyTo.label"
+ value="&replyTo.label;"
+ control="identity.replyTo"
+ accesskey="&replyTo.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.replyTo"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.reply_to"
+ class="uri-element input-inline"
+ placeholder="&replyTo.placeholder;"
+ aria-labelledby="identity.replyTo.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.organization.label"
+ value="&organization.label;"
+ control="identity.organization"
+ accesskey="&organization.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.organization"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.organization"
+ class="input-inline"
+ aria-labelledby="identity.organization.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ value="&signatureText.label;"
+ control="identity.htmlSigText"
+ accesskey="&signatureText.accesskey;"
+ />
+ </html:th>
+ <html:td style="width: 100%">
+ <hbox align="center">
+ <checkbox
+ id="identity.htmlSigFormat"
+ wsm_persist="true"
+ label="&signatureHtml.label;"
+ prefattribute="value"
+ accesskey="&signatureHtml.accesskey;"
+ style="width: 100%"
+ prefstring="mail.identity.%identitykey%.htmlSigFormat"
+ />
+ </hbox>
+ </html:td>
+ </html:tr>
+ </html:table>
+
+ <hbox
+ class="indent"
+ flex="1"
+ style="min-height: 100px; display: flex"
+ >
+ <html:textarea
+ id="identity.htmlSigText"
+ wsm_persist="true"
+ rows="4"
+ style="flex-grow: 1"
+ prefstring="mail.identity.%identitykey%.htmlSigText"
+ class="signatureBox"
+ />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ id="identity.attachSignature"
+ wsm_persist="true"
+ label="&signatureFile.label;"
+ flex="1"
+ accesskey="&signatureFile.accesskey;"
+ oncommand="setupSignatureItems();"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.attach_signature"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent input-container">
+ <html:input
+ id="identity.signature"
+ type="text"
+ datatype="nsIFile"
+ wsm_persist="true"
+ name="identity.signature"
+ aria-labelledby="identity.attachSignature"
+ prefstring="mail.identity.%identitykey%.sig_file"
+ class="uri-element input-inline"
+ />
+ <button
+ class="push"
+ name="browse"
+ label="&choose.label;"
+ accesskey="&choose.accesskey;"
+ oncommand="selectFile()"
+ wsm_persist="true"
+ id="identity.sigbrowsebutton"
+ prefstring="mail.identity.%identitykey%.sigbrowse.disable"
+ />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="identity.attachVCard"
+ label="&attachVCard.label;"
+ flex="1"
+ accesskey="&attachVCard.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.attach_vcard"
+ />
+ <button
+ class="push"
+ name="editVCard"
+ label="&editVCard.label;"
+ accesskey="&editVCard.accesskey;"
+ oncommand="editVCard()"
+ />
+ <html:input
+ id="identity.escapedVCard"
+ type="hidden"
+ value=""
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.escapedVCard"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center" class="input-container" hidefor="nntp">
+ <checkbox
+ id="identity.catchAll"
+ wsm_persist="true"
+ prefattribute="value"
+ label="&catchAll.label;"
+ accesskey="&catchAll.accesskey;"
+ style="margin-block: auto"
+ prefstring="mail.identity.%identitykey%.catchAll"
+ />
+ <html:input
+ id="identity.catchAllHint"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.catchAllHint"
+ class="input-inline"
+ oninput="handleInputCatchAllHint(event);"
+ placeholder="list@example.com, *@example.com"
+ aria-labelledby="identity.catchAll"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ value="&smtpName.label;"
+ control="identity.smtpServerKey"
+ accesskey="&smtpName.accesskey;"
+ />
+ <menulist
+ wsm_persist="true"
+ id="identity.smtpServerKey"
+ flex="1"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.smtpServer"
+ >
+ <menupopup id="smtpPopup">
+ <menuitem
+ value=""
+ label="&smtpDefaultServer.label;"
+ id="useDefaultItem"
+ />
+ <menuseparator />
+ <!-- list will be inserted here -->
+ </menupopup>
+ </menulist>
+ <button
+ id="editSmtp"
+ label="&smtpServerEdit.label;"
+ accesskey="&smtpServerEdit.accesskey;"
+ oncommand="editCurrentSMTP();"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <spacer flex="1" />
+ <button
+ label="&manageIdentities.label;"
+ oncommand="manageIdentities(event);"
+ accesskey="&manageIdentities.accesskey;"
+ wsm_persist="true"
+ id="identity.manageIdentitiesbutton"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-offline.js b/comm/mailnews/base/prefs/content/am-offline.js
new file mode 100644
index 0000000000..72158edace
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-offline.js
@@ -0,0 +1,435 @@
+/* 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/. */
+
+/* import-globals-from am-prefs.js */
+/* import-globals-from ../../content/retention.js */
+
+var gIncomingServer;
+var gServerType;
+var gImapIncomingServer;
+var gPref = null;
+var gLockedPref = {};
+var gOfflineMap = null; // map of folder URLs to offline flags
+var gOfflineFolders; // initial state of allFoldersOffline checkbox
+var gToggleOccurred = false;
+
+function onInit(aPageId, aServerId) {
+ onLockPreference();
+
+ // init values here
+ initServerSettings();
+ initRetentionSettings();
+ initDownloadSettings();
+ initOfflineSettings();
+
+ onCheckItem1("offline.notDownloadMin", "offline.notDownload");
+ onCheckItem1("nntp.downloadMsgMin", "nntp.downloadMsg");
+ onCheckItem1("nntp.removeBodyMin", "nntp.removeBody");
+ onCheckKeepMsg();
+}
+
+/**
+ * Store initial offline flag for each folder and the allFoldersOffline
+ * checkbox. Use to restore the flags and checkbox if edits are canceled.
+ */
+function initOfflineSettings() {
+ gOfflineMap = collectOfflineFolders();
+ gOfflineFolders = document.getElementById("offline.folders").checked;
+ gToggleOccurred = false;
+}
+
+function initServerSettings() {
+ document.getElementById("offline.notDownload").checked =
+ gIncomingServer.limitOfflineMessageSize;
+ document.getElementById("autosyncNotDownload").checked =
+ gIncomingServer.limitOfflineMessageSize;
+ if (gIncomingServer.maxMessageSize > 0) {
+ document.getElementById("offline.notDownloadMin").value =
+ gIncomingServer.maxMessageSize;
+ } else {
+ document.getElementById("offline.notDownloadMin").value = "50";
+ }
+
+ if (gServerType == "imap") {
+ gImapIncomingServer = gIncomingServer.QueryInterface(
+ Ci.nsIImapIncomingServer
+ );
+ document.getElementById("offline.folders").checked =
+ gImapIncomingServer.offlineDownload;
+ }
+}
+
+function initRetentionSettings() {
+ let retentionSettings = gIncomingServer.retentionSettings;
+ initCommonRetentionSettings(retentionSettings);
+
+ document.getElementById("nntp.removeBody").checked =
+ retentionSettings.cleanupBodiesByDays;
+ document.getElementById("nntp.removeBodyMin").value =
+ retentionSettings.daysToKeepBodies > 0
+ ? retentionSettings.daysToKeepBodies
+ : 30;
+}
+
+function initDownloadSettings() {
+ let downloadSettings = gIncomingServer.downloadSettings;
+ document.getElementById("nntp.downloadMsg").checked =
+ downloadSettings.downloadByDate;
+ document.getElementById("nntp.notDownloadRead").checked =
+ downloadSettings.downloadUnreadOnly;
+ document.getElementById("nntp.downloadMsgMin").value =
+ downloadSettings.ageLimitOfMsgsToDownload > 0
+ ? downloadSettings.ageLimitOfMsgsToDownload
+ : 30;
+
+ // Figure out what the most natural division of the autosync pref into
+ // a value and an interval is.
+ let autosyncSelect = document.getElementById("autosyncSelect");
+ let autosyncInterval = document.getElementById("autosyncInterval");
+ let autosyncValue = document.getElementById("autosyncValue");
+ let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays");
+ let autosyncPrefValue =
+ autosyncPref.value == "" ? -1 : parseInt(autosyncPref.value, 10);
+
+ // Clear the preference until we're done initializing.
+ autosyncPref.value = "";
+
+ if (autosyncPrefValue <= 0) {
+ // Special-case values <= 0 to have an interval of "All" and disabled
+ // controls for value and interval.
+ autosyncSelect.value = 0;
+ autosyncInterval.value = 1;
+ autosyncInterval.disabled = true;
+ autosyncValue.value = 30;
+ autosyncValue.disabled = true;
+ } else {
+ // Otherwise, get the list of possible intervals, in order from
+ // largest to smallest.
+ let valuesToTest = [];
+ for (let i = autosyncInterval.itemCount - 1; i >= 0; i--) {
+ valuesToTest.push(autosyncInterval.getItemAtIndex(i).value);
+ }
+
+ // and find the first one that divides the preference evenly.
+ for (let i in valuesToTest) {
+ if (!(autosyncPrefValue % valuesToTest[i])) {
+ autosyncSelect.value = 1;
+ autosyncInterval.value = valuesToTest[i];
+ autosyncValue.value = autosyncPrefValue / autosyncInterval.value;
+ break;
+ }
+ }
+ autosyncInterval.disabled = false;
+ autosyncValue.disabled = false;
+ }
+ autosyncPref.value = autosyncPrefValue;
+}
+
+function onPreInit(account, accountValues) {
+ gServerType = top.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(gServerType);
+ gIncomingServer = account.incomingServer;
+ gIncomingServer.type = gServerType;
+
+ // 10 is OFFLINE_SUPPORT_LEVEL_REGULAR, see nsIMsgIncomingServer.idl
+ // currently, there is no offline without diskspace
+ var titleStringID =
+ gIncomingServer.offlineSupportLevel >= 10
+ ? "prefPanel-synchronization"
+ : "prefPanel-diskspace";
+
+ var prefBundle = document.getElementById("bundle_prefs");
+ document
+ .querySelector("#headertitle > .dialogheader-title")
+ .setAttribute("value", prefBundle.getString(titleStringID));
+ document.title = prefBundle.getString(titleStringID);
+
+ if (gServerType == "pop3") {
+ var pop3Server = gIncomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ // hide retention settings for deferred accounts
+ if (pop3Server.deferredToAccount.length) {
+ var retentionRadio = document.getElementById("retention.keepMsg");
+ retentionRadio.setAttribute("hidden", "true");
+ var retentionLabel = document.getElementById("retentionDescriptionPop");
+ retentionLabel.setAttribute("hidden", "true");
+ var applyToFlaggedCheckbox = document.getElementById(
+ "retention.applyToFlagged"
+ );
+ applyToFlaggedCheckbox.setAttribute("hidden", "true");
+ }
+ }
+}
+
+function onClickSelect() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/msgSelectOfflineFolders.xhtml"
+ );
+}
+
+/**
+ * Handle updates to the Autosync
+ */
+function onAutosyncChange() {
+ let autosyncSelect = document.getElementById("autosyncSelect");
+ let autosyncInterval = document.getElementById("autosyncInterval");
+ let autosyncValue = document.getElementById("autosyncValue");
+ let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays");
+
+ // If we're not done initializing, don't do anything.
+ // (See initDownloadSettings() for more details.)
+ if (autosyncPref.value == "") {
+ return;
+ }
+
+ // If the user selected the All option, disable the autosync and the
+ // textbox.
+ if (autosyncSelect.value == 0) {
+ autosyncPref.value = -1;
+ autosyncInterval.disabled = true;
+ autosyncValue.disabled = true;
+ return;
+ }
+
+ let max = 0x7fffffff / (60 * 60 * 24 * autosyncInterval.value);
+ autosyncValue.setAttribute("max", max);
+ if (autosyncValue.value > max) {
+ autosyncValue.value = Math.floor(max);
+ }
+
+ autosyncInterval.disabled = false;
+ autosyncValue.disabled = false;
+ autosyncPref.value = autosyncValue.value * autosyncInterval.value;
+}
+
+function onAutosyncNotDownload() {
+ // This function is called when the autosync version of offline.notDownload
+ // is changed it simply copies the new checkbox value over to the element
+ // driving the preference.
+ document.getElementById("offline.notDownload").checked =
+ document.getElementById("autosyncNotDownload").checked;
+ onCheckItem1("offline.notDownloadMin", "offline.notDownload");
+}
+
+function onCancel() {
+ // restore the offline flags for all folders
+ restoreOfflineFolders(gOfflineMap);
+ document.getElementById("offline.folders").checked = gOfflineFolders;
+}
+
+/**
+ * Prompt to avoid unexpected folder sync changes.
+ */
+function onLeave() {
+ let changed = false;
+ if (gToggleOccurred) {
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (
+ gOfflineMap[folder.folderURL] !=
+ folder.getFlag(Ci.nsMsgFolderFlags.Offline)
+ ) {
+ // A change to the Offline flag to a folder was made.
+ changed = true;
+ break;
+ }
+ }
+ gToggleOccurred = false;
+ }
+
+ if (changed) {
+ // The user changed the "Keep messages in all folders..." checkbox and
+ // caused changes in online/offline status for all folders in this
+ // account. Prompt whether to restore the original status.
+ let prefBundle = document.getElementById("bundle_prefs");
+ let title = prefBundle.getString("confirmSyncChangesTitle");
+ let question = prefBundle.getString("confirmSyncChanges");
+ let discard = prefBundle.getString("confirmSyncChangesDiscard");
+ let result = Services.prompt.confirmEx(
+ window,
+ title,
+ question,
+ Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ null,
+ discard,
+ null,
+ null,
+ { value: 0 }
+ );
+ if (result == 1) {
+ // User clicked Discard button, so restore the online/offline changes for
+ // the current account. Changes made through the "Advanced..." dialog to
+ // other accounts will not be restored.
+ onCancel();
+ return false;
+ }
+ }
+ return true;
+}
+
+function onSave() {
+ var downloadSettings = Cc[
+ "@mozilla.org/msgDatabase/downloadSettings;1"
+ ].createInstance(Ci.nsIMsgDownloadSettings);
+
+ gIncomingServer.limitOfflineMessageSize = document.getElementById(
+ "offline.notDownload"
+ ).checked;
+ gIncomingServer.maxMessageSize = document.getElementById(
+ "offline.notDownloadMin"
+ ).value;
+
+ var retentionSettings = saveCommonRetentionSettings(
+ gIncomingServer.retentionSettings
+ );
+
+ retentionSettings.daysToKeepBodies =
+ document.getElementById("nntp.removeBodyMin").value;
+ retentionSettings.cleanupBodiesByDays =
+ document.getElementById("nntp.removeBody").checked;
+
+ downloadSettings.downloadByDate =
+ document.getElementById("nntp.downloadMsg").checked;
+ downloadSettings.downloadUnreadOnly = document.getElementById(
+ "nntp.notDownloadRead"
+ ).checked;
+ downloadSettings.ageLimitOfMsgsToDownload = document.getElementById(
+ "nntp.downloadMsgMin"
+ ).value;
+
+ gIncomingServer.retentionSettings = retentionSettings;
+ gIncomingServer.downloadSettings = downloadSettings;
+
+ if (gImapIncomingServer) {
+ // Set the pref on the incomingserver, and set the flag on all folders.
+ gImapIncomingServer.offlineDownload =
+ document.getElementById("offline.folders").checked;
+ }
+}
+
+// Does the work of disabling an element given the array which contains xul id/prefstring pairs.
+// Also saves the id/locked state in an array so that other areas of the code can avoid
+// stomping on the disabled state indiscriminately.
+function disableIfLocked(prefstrArray) {
+ for (let i = 0; i < prefstrArray.length; i++) {
+ var id = prefstrArray[i].id;
+ var element = document.getElementById(id);
+ if (gPref.prefIsLocked(prefstrArray[i].prefstring)) {
+ element.disabled = true;
+ gLockedPref[id] = true;
+ } else {
+ element.removeAttribute("disabled");
+ gLockedPref[id] = false;
+ }
+ }
+}
+
+// Disables xul elements that have associated preferences locked.
+function onLockPreference() {
+ var initPrefString = "mail.server";
+ var finalPrefString;
+
+ // This panel does not use the code in AccountManager.js to handle
+ // the load/unload/disable. keep in mind new prefstrings and changes
+ // to code in AccountManager, and update these as well.
+ var allPrefElements = [
+ { prefstring: "limit_offline_message_size", id: "offline.notDownload" },
+ { prefstring: "limit_offline_message_size", id: "autosyncNotDownload" },
+ { prefstring: "max_size", id: "offline.notDownloadMin" },
+ { prefstring: "downloadUnreadOnly", id: "nntp.notDownloadRead" },
+ { prefstring: "downloadByDate", id: "nntp.downloadMsg" },
+ { prefstring: "ageLimit", id: "nntp.downloadMsgMin" },
+ { prefstring: "retainBy", id: "retention.keepMsg" },
+ { prefstring: "daysToKeepHdrs", id: "retention.keepOldMsgMin" },
+ { prefstring: "numHdrsToKeep", id: "retention.keepNewMsgMin" },
+ { prefstring: "daysToKeepBodies", id: "nntp.removeBodyMin" },
+ { prefstring: "cleanupBodies", id: "nntp.removeBody" },
+ { prefstring: "applyToFlagged", id: "retention.applyToFlagged" },
+ { prefstring: "disable_button.selectFolder", id: "selectNewsgroupsButton" },
+ {
+ prefstring: "disable_button.selectFolder",
+ id: "selectImapFoldersButton",
+ },
+ ];
+
+ finalPrefString = initPrefString + "." + gIncomingServer.key + ".";
+ gPref = Services.prefs.getBranch(finalPrefString);
+
+ disableIfLocked(allPrefElements);
+}
+
+// XXX TODO: Function should be merged with onCheckItem in bug 755885.
+function onCheckItem1(changeElementId, checkElementId) {
+ var element = document.getElementById(changeElementId);
+ var checked = document.getElementById(checkElementId).checked;
+ if (checked && !gLockedPref[checkElementId]) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+}
+
+function toggleOffline() {
+ let offline = document.getElementById("offline.folders").checked;
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (offline) {
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+ gToggleOccurred = true;
+}
+
+function collectOfflineFolders() {
+ let offlineFolderMap = {};
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ offlineFolderMap[folder.folderURL] = folder.getFlag(
+ Ci.nsMsgFolderFlags.Offline
+ );
+ }
+
+ return offlineFolderMap;
+}
+
+function restoreOfflineFolders(offlineFolderMap) {
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (offlineFolderMap[folder.folderURL]) {
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+}
+
+/**
+ * Checks if the user selected a permanent removal of messages from a server
+ * listed in the confirmfor attribute and warns about it.
+ *
+ * @param {Element} aRadio - The radiogroup element containing the retention options.
+ */
+function warnServerRemove(aRadio) {
+ let confirmFor = aRadio.getAttribute("confirmfor");
+
+ if (
+ confirmFor &&
+ confirmFor.split(",").includes(gServerType) &&
+ aRadio.value != 1
+ ) {
+ let prefBundle = document.getElementById("bundle_prefs");
+ let title = prefBundle.getString("removeFromServerTitle");
+ let question = prefBundle.getString("removeFromServer");
+ if (!Services.prompt.confirm(window, title, question)) {
+ // If the user doesn't agree, fall back to not deleting anything.
+ aRadio.value = 1;
+ onCheckKeepMsg();
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-offline.xhtml b/comm/mailnews/base/prefs/content/am-offline.xhtml
new file mode 100644
index 0000000000..0d3a12e396
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-offline.xhtml
@@ -0,0 +1,350 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-offline.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/retention.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-offline.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-offline.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <label hidden="true" wsm_persist="true" id="server.type" />
+ <label
+ id="imap.autoSyncMaxAgeDays"
+ hidden="true"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.autosync_max_age_days"
+ />
+
+ <hbox id="headertitle" class="dialogheader">
+ <label class="dialogheader-title" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset id="offline.titlebox" hidefor="pop3,none,rss">
+ <html:legend>&syncGroupTitle.label;</html:legend>
+
+ <vbox>
+ <checkbox
+ hidefor="pop3,nntp,none"
+ id="offline.folders"
+ label="&allFoldersOffline2.label;"
+ oncommand="toggleOffline()"
+ accesskey="&allFoldersOffline2.accesskey;"
+ />
+
+ <description hidefor="pop3,nntp,none,rss"
+ >&allFoldersOfflineNote.label;</description
+ >
+
+ <separator class="thin" hidefor="pop3,nntp,none" />
+
+ <hbox hidefor="pop3,nntp,none" pack="end">
+ <button
+ label="&offlineImapAdvancedOffline.label;"
+ accesskey="&offlineImapAdvancedOffline.accesskey;"
+ oncommand="onClickSelect()"
+ id="selectImapFoldersButton"
+ class="selectForOfflineUseButton"
+ />
+ </hbox>
+
+ <hbox hidefor="pop3,imap,none" pack="end">
+ <button
+ label="&offlineSelectNntp.label;"
+ accesskey="&offlineSelectNntp.accesskey;"
+ oncommand="onClickSelect()"
+ id="selectNewsgroupsButton"
+ class="selectForOfflineUseButton"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="diskspace.titlebox">
+ <html:legend hidefor="pop3,none,rss"
+ >&diskspaceGroupTitle.label;</html:legend
+ >
+
+ <description hidefor="pop3,nntp,none,rss"
+ >&doNotDownloadImap.label;</description
+ >
+ <description hidefor="pop3,imap,none,rss"
+ >&doNotDownloadNntp.label;</description
+ >
+ <description hidefor="imap,nntp,none,rss"
+ >&doNotDownloadPop3Movemail.label;</description
+ >
+
+ <!-- IMAP Autosync Preference -->
+ <radiogroup
+ hidefor="pop3,nntp,none,rss"
+ id="autosyncSelect"
+ class="indent"
+ >
+ <radio
+ id="useAutosync.AllMsg"
+ value="0"
+ accesskey="&allAutosync.accesskey;"
+ label="&allAutosync.label;"
+ oncommand="onAutosyncChange();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ id="useAutosync.ByAge"
+ accesskey="&ageAutosync.accesskey;"
+ value="1"
+ label="&ageAutosyncBefore.label;"
+ oncommand="onAutosyncChange();"
+ />
+ <html:input
+ id="autosyncValue"
+ type="number"
+ class="size4 input-inline autosync"
+ min="1"
+ onchange="onAutosyncChange();"
+ aria-labelledby="ageAutosyncBefore autosyncValue ageAutosyncMiddle autosyncInterval ageAutosyncAfter"
+ />
+ <label
+ id="ageAutosyncMiddle"
+ control="autosyncValue"
+ value="&ageAutosyncMiddle.label;"
+ />
+ <menulist id="autosyncInterval" onselect="onAutosyncChange();">
+ <menupopup>
+ <menuitem label="&dayAgeInterval.label;" value="1" />
+ <menuitem label="&weekAgeInterval.label;" value="7" />
+ <menuitem label="&monthAgeInterval.label;" value="31" />
+ <menuitem label="&yearAgeInterval.label;" value="365" />
+ </menupopup>
+ </menulist>
+ <label
+ id="ageAutosyncAfter"
+ control="autosyncInterval"
+ value="&ageAutosyncAfter.label;"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent" hidefor="rss">
+ <checkbox
+ hidefor="pop3,imap,none"
+ id="nntp.notDownloadRead"
+ wsm_persist="true"
+ label="&nntpNotDownloadRead.label;"
+ accesskey="&nntpNotDownloadRead.accesskey;"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent" hidefor="none,rss">
+ <checkbox
+ wsm_persist="true"
+ id="offline.notDownload"
+ hidefor="imap"
+ label="&offlineNotDownload.label;"
+ accesskey="&offlineNotDownload.accesskey;"
+ oncommand="onCheckItem('offline.notDownloadMin', [this.id]);"
+ />
+ <checkbox
+ wsm_persist="true"
+ id="autosyncNotDownload"
+ hidefor="pop3,nntp"
+ label="&autosyncNotDownload.label;"
+ accesskey="&autosyncNotDownload.accesskey;"
+ oncommand="onAutosyncNotDownload();"
+ />
+ <html:input
+ id="offline.notDownloadMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="50"
+ wsm_persist="true"
+ aria-labelledby="offline.notDownload offline.notDownloadMin kbLabel"
+ />
+ <label
+ value="&kb.label;"
+ control="offline.notDownloadMin"
+ id="kbLabel"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent" hidefor="pop3,imap,none,rss">
+ <checkbox
+ wsm_persist="true"
+ id="nntp.downloadMsg"
+ label="&nntpDownloadMsg.label;"
+ accesskey="&nntpDownloadMsg.accesskey;"
+ oncommand="onCheckItem('nntp.downloadMsgMin', [this.id]);"
+ />
+ <html:input
+ id="nntp.downloadMsgMin"
+ type="number"
+ class="size2 input-inline"
+ min="1"
+ value="30"
+ wsm_persist="true"
+ aria-labelledby="nntp.downloadMsg nntp.downloadMsgMin daysOldLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="nntp.downloadMsgMin"
+ id="daysOldLabel"
+ />
+ </hbox>
+
+ <vbox align="start">
+ <separator hidefor="none,rss" />
+ <label
+ id="retentionDescription"
+ hidefor="imap,pop3"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanup.label;</label
+ >
+ <label
+ id="retentionDescriptionImap"
+ hidefor="pop3,nntp,none,rss"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanupImap.label;</label
+ >
+ <label
+ id="retentionDescriptionPop"
+ hidefor="imap,nntp,none,rss"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanupPop.label;</label
+ >
+
+ <radiogroup
+ hidefor=""
+ confirmfor="imap,pop3"
+ id="retention.keepMsg"
+ class="indent"
+ oncommand="warnServerRemove(this);"
+ >
+ <radio
+ id="retention.keepAllMsg"
+ value="1"
+ accesskey="&retentionKeepAll.accesskey;"
+ label="&retentionKeepAll.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ id="retention.keepNewMsg"
+ accesskey="&retentionKeepRecent.accesskey;"
+ value="3"
+ label="&retentionKeepRecent.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepNewMsgMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="2000"
+ aria-labelledby="retention.keepNewMsg retention.keepNewMsgMin newMsgLabel"
+ />
+ <label
+ value="&message.label;"
+ control="retention.keepNewMsgMin"
+ id="newMsgLabel"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="retention.keepOldMsg"
+ accesskey="&retentionKeepMsg.accesskey;"
+ value="2"
+ label="&retentionKeepMsg.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepOldMsgMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="30"
+ aria-labelledby="retention.keepOldMsg retention.keepOldMsgMin oldMsgLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="retention.keepOldMsgMin"
+ id="oldMsgLabel"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent">
+ <checkbox
+ id="retention.applyToFlagged"
+ label="&retentionApplyToFlagged.label;"
+ hidefor=""
+ accesskey="&retentionApplyToFlagged.accesskey;"
+ checked="true"
+ />
+ </hbox>
+ <hbox align="center" class="indent" hidefor="pop3,imap,none,rss">
+ <checkbox
+ id="nntp.removeBody"
+ accesskey="&nntpRemoveMsgBody.accesskey;"
+ label="&nntpRemoveMsgBody.label;"
+ oncommand="onCheckItem('nntp.removeBodyMin', [this.id]);"
+ />
+ <html:input
+ id="nntp.removeBodyMin"
+ type="number"
+ class="size2 input-inline"
+ min="1"
+ value="30"
+ aria-labelledby="nntp.removeBody nntp.removeBodyMin daysOldMsg"
+ />
+ <label
+ value="&daysOld.label;"
+ control="nntp.removeBodyMin"
+ id="daysOldMsg"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-prefs.js b/comm/mailnews/base/prefs/content/am-prefs.js
new file mode 100644
index 0000000000..cff09a5d93
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-prefs.js
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+/* functions for disabling front end elements when the appropriate
+ back-end preference is locked. */
+
+/**
+ * Prefs in MailNews require dynamic portions to indicate
+ * which of multiple servers or identities. This function
+ * takes a string and a xul element.
+ *
+ * @param {string} aStr - The string is a prefstring with a token %tokenname%.
+ * @param {Element} aElement - The element has an attribute of name |tokenname|
+ * whose value is substituted into the string and returned by the function.
+ * Any tokens which do not have associated attribute value are not
+ * substituted, and left in the string as-is.
+ */
+function substPrefTokens(aStr, aElement) {
+ let tokenpat = /%(\w+)%/;
+ let token;
+ let newprefstr = "";
+
+ let prefPartsArray = aStr.split(".");
+ /* here's a little loop that goes through
+ each part of the string separated by a dot, and
+ if any parts are of the form %string%, it will replace
+ them with the value of the attribute of that name from
+ the xul object */
+ for (let i = 0; i < prefPartsArray.length; i++) {
+ token = prefPartsArray[i].match(tokenpat);
+ if (token) {
+ // We've got a %% match.
+ if (token[1]) {
+ if (aElement[token[1]]) {
+ newprefstr += aElement[token[1]] + "."; // here's where we get the info
+ } else {
+ // All we got was this stinkin %.
+ newprefstr += prefPartsArray[i] + ".";
+ }
+ }
+ } else {
+ // token is falsy.
+ newprefstr += prefPartsArray[i] + ".";
+ }
+ }
+ newprefstr = newprefstr.slice(0, -1); // remove the last char, a dot
+ if (newprefstr.length <= 0) {
+ newprefstr = null;
+ }
+
+ return newprefstr;
+}
+
+/**
+ * A simple function to check if a pref in an element is locked.
+ *
+ * @param {Element} aElement - An element with the pref related attributes
+ * (pref, preftype, prefstring)
+ * @returns {boolean} whether the prefstring specified in that element is
+ * locked (true/false). If it does not have a valid prefstring, a false is
+ * returned.
+ */
+function getAccountValueIsLocked(aElement) {
+ let prefstring = aElement.getAttribute("prefstring");
+ if (prefstring) {
+ let prefstr = substPrefTokens(prefstring, aElement);
+ // see if the prefstring is locked
+ if (prefstr) {
+ return Services.prefs.prefIsLocked(prefstr);
+ }
+ }
+ return false;
+}
+
+/**
+ * Enables/disables element (slave) according to the checked state
+ * of another elements (masters).
+ *
+ * @param {string} aChangeElementId - Slave element which should be enabled
+ * if all the checkElementIDs are checked. Otherwise it gets disabled.
+ * @param {string[]} aCheckElementIds - An array of IDs of the master elements.
+ *
+ * @see bug 728681 for the pattern on how this is used.
+ */
+function onCheckItem(aChangeElementId, aCheckElementIds) {
+ let elementToControl = document.getElementById(aChangeElementId);
+ let disabled = false;
+
+ for (let notifyId of aCheckElementIds) {
+ let notifyElement = document.getElementById(notifyId);
+ let notifyElementState = null;
+ if ("checked" in notifyElement) {
+ notifyElementState = notifyElement.checked;
+ } else if ("selected" in notifyElement) {
+ notifyElementState = notifyElement.selected;
+ } else {
+ console.error("Unknown type of control element: " + notifyElement.id);
+ }
+
+ if (!notifyElementState) {
+ disabled = true;
+ break;
+ }
+ }
+
+ if (!disabled && getAccountValueIsLocked(elementToControl)) {
+ disabled = true;
+ }
+
+ elementToControl.disabled = disabled;
+}
+
+/**
+ * Hides and shows elements relevant for the given server type.
+ *
+ * @param {string} serverType - Name of the server type for which to show/hide elements.
+ */
+function hideShowControls(serverType) {
+ let controls = document.querySelectorAll("[hidefor]");
+ for (let controlNo = 0; controlNo < controls.length; controlNo++) {
+ let control = controls[controlNo];
+ let hideFor = control.getAttribute("hidefor");
+
+ // Hide unsupported server types using hideFor="servertype1,servertype2".
+ let hide = false;
+ let hideForTokens = hideFor.split(",");
+ for (let tokenNo = 0; tokenNo < hideForTokens.length; tokenNo++) {
+ if (hideForTokens[tokenNo] == serverType) {
+ hide = true;
+ break;
+ }
+ }
+
+ if (hide) {
+ control.setAttribute("hidden", "true");
+ } else {
+ control.removeAttribute("hidden");
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-server-advanced.js b/comm/mailnews/base/prefs/content/am-server-advanced.js
new file mode 100644
index 0000000000..a37a8eced0
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server-advanced.js
@@ -0,0 +1,151 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOk);
+
+// pull stuff out of window.arguments
+var gServerSettings = window.arguments[0];
+
+var gFirstDeferredAccount;
+// initialize the controls with the "gServerSettings" argument
+
+var gControls;
+function getControls() {
+ if (!gControls) {
+ gControls = document.getElementsByAttribute("amsa_persist", "true");
+ }
+ return gControls;
+}
+
+function getLocalFoldersAccount() {
+ return MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+}
+
+function onLoad() {
+ var prettyName = gServerSettings.serverPrettyName;
+
+ if (prettyName) {
+ document.getElementById("serverPrettyName").value = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("forAccount", [prettyName]);
+ }
+
+ if (gServerSettings.serverType == "imap") {
+ document.getElementById("pop3Panel").hidden = true;
+ } else if (gServerSettings.serverType == "pop3") {
+ document.getElementById("imapPanel").hidden = true;
+ let radioGroup = document.getElementById("folderStorage");
+
+ gFirstDeferredAccount = gServerSettings.deferredToAccount;
+ let folderPopup = document.getElementById("deferredServerPopup");
+
+ // The current account should not be shown in the folder picker
+ // of the "other account" option.
+ folderPopup._teardown();
+ folderPopup.setAttribute(
+ "excludeServers",
+ gServerSettings.account.incomingServer.key
+ );
+ folderPopup._ensureInitialized();
+
+ if (gFirstDeferredAccount.length) {
+ // The current account is deferred.
+ let account = MailServices.accounts.getAccount(gFirstDeferredAccount);
+ radioGroup.value = "otherAccount";
+ folderPopup.selectFolder(account.incomingServer.rootFolder);
+ } else {
+ // Current account is not deferred.
+ radioGroup.value = "currentAccount";
+ // If there are no suitable accounts to defer to, then the menulist is
+ // disabled by the picker with an appropriate message.
+ folderPopup.selectFolder();
+ if (gServerSettings.account.incomingServer.isDeferredTo) {
+ // Some other account already defers to this account
+ // therefore this one can't be deferred further.
+ radioGroup.disabled = true;
+ }
+ }
+
+ let picker = document.getElementById("deferredServerFolderPicker");
+ picker.disabled = radioGroup.selectedIndex != 1;
+ }
+
+ var controls = getControls();
+
+ for (let i = 0; i < controls.length; i++) {
+ var slot = controls[i].id;
+ if (slot in gServerSettings) {
+ if (controls[i].localName == "checkbox") {
+ controls[i].checked = gServerSettings[slot];
+ } else {
+ controls[i].value = gServerSettings[slot];
+ }
+ }
+ }
+}
+
+function onOk(event) {
+ // Handle account deferral settings for POP3 accounts.
+ if (gServerSettings.serverType == "pop3") {
+ var radioGroup = document.getElementById("folderStorage");
+ var gPrefsBundle = document.getElementById("bundle_prefs");
+ let picker = document.getElementById("deferredServerFolderPicker");
+
+ // This account wasn't previously deferred, but is now deferred.
+ if (radioGroup.value != "currentAccount" && !gFirstDeferredAccount.length) {
+ // If the user hasn't selected a folder, keep the default.
+ if (!picker.selectedItem) {
+ return;
+ }
+
+ var confirmDeferAccount = gPrefsBundle.getString(
+ "confirmDeferAccountWarning"
+ );
+
+ var confirmTitle = gPrefsBundle.getString("confirmDeferAccountTitle");
+
+ if (!Services.prompt.confirm(window, confirmTitle, confirmDeferAccount)) {
+ event.preventDefault();
+ return;
+ }
+ }
+ switch (radioGroup.value) {
+ case "currentAccount":
+ gServerSettings.deferredToAccount = "";
+ break;
+ case "otherAccount":
+ let server = picker.selectedItem._folder.server;
+ let account = MailServices.accounts.FindAccountForServer(server);
+ gServerSettings.deferredToAccount = account.key;
+ break;
+ }
+ }
+
+ // Save the controls back to the "gServerSettings" array.
+ var controls = getControls();
+ for (let i = 0; i < controls.length; i++) {
+ var slot = controls[i].id;
+ if (slot in gServerSettings) {
+ if (controls[i].localName == "checkbox") {
+ gServerSettings[slot] = controls[i].checked;
+ } else {
+ gServerSettings[slot] = controls[i].value;
+ }
+ }
+ }
+}
+
+// Set radio element choices and picker states
+function updateInboxAccount(enablePicker) {
+ document.getElementById("deferredServerFolderPicker").disabled =
+ !enablePicker;
+ document.getElementById("deferGetNewMail").disabled = !enablePicker;
+}
diff --git a/comm/mailnews/base/prefs/content/am-server-advanced.xhtml b/comm/mailnews/base/prefs/content/am-server-advanced.xhtml
new file mode 100644
index 0000000000..b8349af38f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server-advanced.xhtml
@@ -0,0 +1,222 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-server-advanced.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&serverAdvanced.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-server-advanced.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel" style="width: 100vw; height: 100vh">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <label id="serverPrettyName" />
+
+ <separator class="thin" />
+
+ <!-- IMAP Panel -->
+ <vbox id="imapPanel">
+ <hbox align="center">
+ <label
+ id="serverDirectoryLabel"
+ value="&serverDirectory.label;"
+ accesskey="&serverDirectory.accesskey;"
+ control="serverDirectory"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="serverDirectory"
+ type="text"
+ class="input-inline"
+ aria-labelledby="serverDirectoryLabel"
+ amsa_persist="true"
+ />
+ </hbox>
+ </hbox>
+
+ <checkbox
+ id="usingSubscription"
+ amsa_persist="true"
+ label="&usingSubscription.label;"
+ accesskey="&usingSubscription.accesskey;"
+ />
+
+ <checkbox
+ id="dualUseFolders"
+ amsa_persist="true"
+ label="&dualUseFolders.label;"
+ accesskey="&dualUseFolders.accesskey;"
+ />
+
+ <separator class="groove" />
+ <hbox align="center">
+ <label
+ id="maximumConnectionsNumberLabel"
+ control="maximumConnectionsNumber"
+ value="&maximumConnectionsNumber.label;"
+ accesskey="&maximumConnectionsNumber.accesskey;"
+ />
+ <html:input
+ id="maximumConnectionsNumber"
+ type="number"
+ class="size3 input-inline"
+ min="1"
+ max="1000"
+ amsa_persist="true"
+ aria-labelledby="maximumConnectionsNumberLabel"
+ />
+ </hbox>
+
+ <separator class="groove" />
+ <description>&namespaceDesc.label;</description>
+ <hbox class="indent">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="personalNamespaceLabel"
+ control="personalNamespace"
+ value="&personalNamespace.label;"
+ accesskey="&personalNamespace.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="publicNamespaceLabel"
+ control="publicNamespace"
+ value="&publicNamespace.label;"
+ accesskey="&publicNamespace.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="otherUsersNamespaceLabel"
+ control="otherUsersNamespace"
+ value="&otherUsersNamespace.label;"
+ accesskey="&otherUsersNamespace.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <html:input
+ id="personalNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="personalNamespaceLabel"
+ amsa_persist="true"
+ />
+ <html:input
+ id="publicNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="publicNamespaceLabel"
+ amsa_persist="true"
+ />
+ <html:input
+ id="otherUsersNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="otherUsersNamespaceLabel"
+ amsa_persist="true"
+ />
+ </vbox>
+ </hbox>
+ <hbox class="indent">
+ <checkbox
+ id="overrideNamespaces"
+ amsa_persist="true"
+ label="&overrideNamespaces.label;"
+ accesskey="&overrideNamespaces.accesskey;"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- POP3 Panel -->
+ <vbox id="pop3Panel">
+ <label flex="1" control="folderStorage"
+ >&pop3DeferringDesc.label;</label
+ >
+ <hbox align="center">
+ <radiogroup
+ id="folderStorage"
+ flex="1"
+ orient="horizontal"
+ amsa_persist="true"
+ onselect="updateInboxAccount(this.selectedIndex == 1);"
+ >
+ <vbox flex="1">
+ <hbox>
+ <radio
+ id="deferToCurrentAccount"
+ value="currentAccount"
+ label="&accountInbox.label;"
+ accesskey="&accountInbox.accesskey;"
+ />
+ </hbox>
+ <vbox flex="1">
+ <hbox>
+ <radio
+ id="deferToOtherAccount"
+ value="otherAccount"
+ label="&deferToServer.label;"
+ accesskey="&deferToServer.accesskey;"
+ >
+ </radio>
+ <menulist
+ id="deferredServerFolderPicker"
+ class="folderMenuItem"
+ flex="1"
+ aria-labelledby="deferToServer"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="deferredServerPopup"
+ expandFolders="false"
+ mode="deferred"
+ oncommand="this.selectFolder(event.target._folder);"
+ />
+ </menulist>
+ </hbox>
+ <checkbox
+ amsa_persist="true"
+ id="deferGetNewMail"
+ label="&deferGetNewMail.label;"
+ class="indent"
+ accesskey="&deferGetNewMail.accesskey;"
+ />
+ </vbox>
+ </vbox>
+ </radiogroup>
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-server.js b/comm/mailnews/base/prefs/content/am-server.js
new file mode 100644
index 0000000000..f73535b35c
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server.js
@@ -0,0 +1,615 @@
+/* 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/. */
+
+/* import-globals-from am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gServer;
+var gOriginalStoreType;
+
+/**
+ * Called when the store type menu is clicked.
+ *
+ * @param {object} aStoreTypeElement - store type menu list element.
+ */
+function clickStoreTypeMenu(aStoreTypeElement) {
+ if (aStoreTypeElement.value == gOriginalStoreType) {
+ return;
+ }
+
+ // Response from migration dialog modal. If the conversion is complete
+ // 'response.newRootFolder' will hold the path to the new account root folder,
+ // otherwise 'response.newRootFolder' will be null.
+ let response = { newRootFolder: null };
+ // Send 'response' as an argument to converterDialog.xhtml.
+ window.browsingContext.topChromeWindow.openDialog(
+ "converterDialog.xhtml",
+ "mailnews:mailstoreconverter",
+ "modal,centerscreen,resizable=no,width=700,height=130",
+ gServer,
+ aStoreTypeElement.value,
+ response
+ );
+ changeStoreType(response);
+}
+
+/**
+ * Revert store type to the original store type if converter modal closes
+ * before migration is complete, otherwise change original store type to
+ * currently selected store type.
+ *
+ * @param {object} aResponse - response from migration dialog modal.
+ */
+function changeStoreType(aResponse) {
+ if (aResponse.newRootFolder) {
+ // The conversion is complete.
+ // Set local path to the new account root folder which is present
+ // in 'aResponse.newRootFolder'.
+ if (gServer.type == "nntp") {
+ let newRootFolder = aResponse.newRootFolder;
+ let lastSlash = newRootFolder.lastIndexOf("/");
+ let newsrc =
+ newRootFolder.slice(0, lastSlash) +
+ "/newsrc-" +
+ newRootFolder.slice(lastSlash + 1);
+ document.getElementById("nntp.newsrcFilePath").value = newsrc;
+ }
+
+ document.getElementById("server.localPath").value = aResponse.newRootFolder;
+ gOriginalStoreType = document.getElementById(
+ "server.storeTypeMenulist"
+ ).value;
+ MailUtils.restartApplication();
+ } else {
+ // The conversion failed or was cancelled.
+ // Restore selected item to what was selected before conversion.
+ document.getElementById("server.storeTypeMenulist").value =
+ gOriginalStoreType;
+ }
+}
+
+function onSave() {
+ let storeContractID = document.getElementById("server.storeTypeMenulist")
+ .selectedItem.value;
+ document
+ .getElementById("server.storeContractID")
+ .setAttribute("value", storeContractID);
+}
+
+function onInit(aPageId, aServerId) {
+ initServerType();
+
+ onCheckItem("server.biffMinutes", ["server.doBiff"]);
+ onCheckItem("nntp.maxArticles", ["nntp.notifyOn"]);
+ setupMailOnServerUI();
+ setupFixedUI();
+ let serverType = document.getElementById("server.type").getAttribute("value");
+ if (serverType == "imap") {
+ setupImapDeleteUI(aServerId);
+ }
+
+ // OAuth2 are only supported on IMAP and POP.
+ document.getElementById("authMethod-oauth2").hidden =
+ serverType != "imap" && serverType != "pop3";
+ // TLS Cert (External) only supported on IMAP.
+ document.getElementById("authMethod-external").hidden = serverType != "imap";
+
+ // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't
+ // allow users to choose it anymore. Hide the option unless the user already
+ // has it set.
+ hideUnlessSelected(document.getElementById("connectionSecurityType-1"));
+
+ // UI for account store type.
+ let storeTypeElement = document.getElementById("server.storeTypeMenulist");
+ // set the menuitem to match the account
+ let currentStoreID = document
+ .getElementById("server.storeContractID")
+ .getAttribute("value");
+ let targetItem = storeTypeElement.getElementsByAttribute(
+ "value",
+ currentStoreID
+ );
+ storeTypeElement.selectedItem = targetItem[0];
+ // Disable store type change if store has not been used yet.
+ storeTypeElement.setAttribute(
+ "disabled",
+ gServer.getBoolValue("canChangeStoreType")
+ ? "false"
+ : !Services.prefs.getBoolPref("mail.store_conversion_enabled")
+ );
+ // Initialise 'gOriginalStoreType' to the item that was originally selected.
+ gOriginalStoreType = storeTypeElement.value;
+}
+
+function onPreInit(account, accountValues) {
+ var type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+
+ gServer = account.incomingServer;
+}
+
+function initServerType() {
+ var serverType = document.getElementById("server.type").getAttribute("value");
+ var propertyName = "serverType-" + serverType;
+
+ var messengerBundle = document.getElementById("bundle_messenger");
+ var verboseName;
+ try {
+ verboseName = messengerBundle.getString(propertyName);
+ } catch (e) {
+ // Addon-provided server types do not have a description string,
+ // then display the raw server type.
+ verboseName = serverType;
+ }
+ setDivText("servertypeVerbose", verboseName);
+
+ secureSelect(true);
+
+ setLabelFromStringBundle("authMethod-no", "authNo");
+ setLabelFromStringBundle("authMethod-old", "authOld");
+ setLabelFromStringBundle("authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("authMethod-external", "authExternal");
+ setLabelFromStringBundle("authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("authMethod-oauth2", "authOAuth2");
+ setLabelFromStringBundle("authMethod-anysecure", "authAnySecure");
+ setLabelFromStringBundle("authMethod-any", "authAny");
+ setLabelFromStringBundle(
+ "authMethod-password-encrypted",
+ "authPasswordEncrypted"
+ );
+ // authMethod-password-cleartext already set in secureSelect()
+
+ // Hide deprecated/hidden auth options, unless selected
+ hideUnlessSelected(document.getElementById("authMethod-no"));
+ hideUnlessSelected(document.getElementById("authMethod-old"));
+ hideUnlessSelected(document.getElementById("authMethod-anysecure"));
+ hideUnlessSelected(document.getElementById("authMethod-any"));
+}
+
+function hideUnlessSelected(element) {
+ element.hidden = !element.selected;
+}
+
+function setLabelFromStringBundle(elementID, stringName) {
+ document.getElementById(elementID).label = document
+ .getElementById("bundle_messenger")
+ .getString(stringName);
+}
+
+function setDivText(divname, value) {
+ var div = document.getElementById(divname);
+ if (!div) {
+ return;
+ }
+ div.setAttribute("value", value);
+}
+
+function onAdvanced() {
+ // Store the server type and, if an IMAP or POP3 server,
+ // the settings needed for the IMAP/POP3 tab into the array
+ var serverSettings = {};
+ var serverType = document.getElementById("server.type").getAttribute("value");
+ serverSettings.serverType = serverType;
+
+ serverSettings.serverPrettyName = gServer.prettyName;
+ serverSettings.account = top.getCurrentAccount();
+
+ if (serverType == "imap") {
+ serverSettings.dualUseFolders = document.getElementById(
+ "imap.dualUseFolders"
+ ).checked;
+ serverSettings.usingSubscription = document.getElementById(
+ "imap.usingSubscription"
+ ).checked;
+ serverSettings.maximumConnectionsNumber = document
+ .getElementById("imap.maximumConnectionsNumber")
+ .getAttribute("value");
+ serverSettings.personalNamespace = document
+ .getElementById("imap.personalNamespace")
+ .getAttribute("value");
+ serverSettings.publicNamespace = document
+ .getElementById("imap.publicNamespace")
+ .getAttribute("value");
+ serverSettings.serverDirectory = document
+ .getElementById("imap.serverDirectory")
+ .getAttribute("value");
+ serverSettings.otherUsersNamespace = document
+ .getElementById("imap.otherUsersNamespace")
+ .getAttribute("value");
+ serverSettings.overrideNamespaces = document.getElementById(
+ "imap.overrideNamespaces"
+ ).checked;
+ } else if (serverType == "pop3") {
+ serverSettings.deferGetNewMail = document.getElementById(
+ "pop3.deferGetNewMail"
+ ).checked;
+ serverSettings.deferredToAccount = document
+ .getElementById("pop3.deferredToAccount")
+ .getAttribute("value");
+ }
+
+ let onCloseAdvanced = function () {
+ if (serverType == "imap") {
+ document.getElementById("imap.dualUseFolders").checked =
+ serverSettings.dualUseFolders;
+ document.getElementById("imap.usingSubscription").checked =
+ serverSettings.usingSubscription;
+ document
+ .getElementById("imap.maximumConnectionsNumber")
+ .setAttribute("value", serverSettings.maximumConnectionsNumber);
+ document
+ .getElementById("imap.personalNamespace")
+ .setAttribute("value", serverSettings.personalNamespace);
+ document
+ .getElementById("imap.publicNamespace")
+ .setAttribute("value", serverSettings.publicNamespace);
+ document
+ .getElementById("imap.serverDirectory")
+ .setAttribute("value", serverSettings.serverDirectory);
+ document
+ .getElementById("imap.otherUsersNamespace")
+ .setAttribute("value", serverSettings.otherUsersNamespace);
+ document.getElementById("imap.overrideNamespaces").checked =
+ serverSettings.overrideNamespaces;
+ } else if (serverType == "pop3") {
+ document.getElementById("pop3.deferGetNewMail").checked =
+ serverSettings.deferGetNewMail;
+ document
+ .getElementById("pop3.deferredToAccount")
+ .setAttribute("value", serverSettings.deferredToAccount);
+ let pop3Server = gServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ // we're explicitly setting this so we'll go through the SetDeferredToAccount method
+ pop3Server.deferredToAccount = serverSettings.deferredToAccount;
+ // Setting the server to be deferred causes a rebuild of the account tree,
+ // losing the current selection. Reselect the current server again as it
+ // didn't really disappear.
+ parent.selectServer(
+ parent.getCurrentAccount().incomingServer,
+ parent.currentPageId
+ );
+
+ // Iterate over all accounts to see if any of their junk targets are now
+ // invalid (pointed to the account that is now deferred).
+ // If any such target is found it is reset to a new safe folder
+ // (the deferred to account or Local Folders). If junk was really moved
+ // to that folder (moveOnSpam = true) then moving junk is disabled
+ // (so that the user notices it and checks the settings).
+ // This is the same sanitization as in am-junk.js, just applied to all POP accounts.
+ let deferredURI =
+ serverSettings.deferredToAccount &&
+ MailServices.accounts.getAccount(serverSettings.deferredToAccount)
+ .incomingServer.serverURI;
+
+ for (let account of MailServices.accounts.accounts) {
+ let accountValues = parent.getValueArrayFor(account);
+ let type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ // Try to keep this list of account types not having Junk settings
+ // synchronized with the list in AccountManager.js.
+ if (type != "nntp" && type != "rss" && type != "im") {
+ let spamActionTargetAccount = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "spamActionTargetAccount",
+ "string",
+ true
+ );
+ let spamActionTargetFolder = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "spamActionTargetFolder",
+ "wstring",
+ true
+ );
+ let moveOnSpam = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "moveOnSpam",
+ "bool",
+ true
+ );
+
+ // Check if there are any invalid junk targets and fix them.
+ [spamActionTargetAccount, spamActionTargetFolder, moveOnSpam] =
+ sanitizeJunkTargets(
+ spamActionTargetAccount,
+ spamActionTargetFolder,
+ deferredURI || account.incomingServer.serverURI,
+ parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "moveTargetMode",
+ "int",
+ true
+ ),
+ account.incomingServer.spamSettings,
+ moveOnSpam
+ );
+
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "moveOnSpam",
+ moveOnSpam
+ );
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "spamActionTargetAccount",
+ spamActionTargetAccount
+ );
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "spamActionTargetFolder",
+ spamActionTargetFolder
+ );
+ }
+ }
+ }
+ document.dispatchEvent(new CustomEvent("prefchange"));
+ };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-server-advanced.xhtml",
+ { closingCallback: onCloseAdvanced },
+ serverSettings
+ );
+}
+
+function secureSelect(aLoading) {
+ var socketType = document.getElementById("server.socketType").value;
+ var defaultPort = gServer.protocolInfo.getDefaultServerPort(false);
+ var defaultPortSecure = gServer.protocolInfo.getDefaultServerPort(true);
+ var port = document.getElementById("server.port");
+ var portDefault = document.getElementById("defaultPort");
+ var prevDefaultPort = portDefault.value;
+
+ if (socketType == Ci.nsMsgSocketType.SSL) {
+ portDefault.value = defaultPortSecure;
+ if (
+ port.value == "" ||
+ (!aLoading &&
+ port.value == defaultPort &&
+ prevDefaultPort != portDefault.value)
+ ) {
+ port.value = defaultPortSecure;
+ }
+ } else {
+ portDefault.value = defaultPort;
+ if (
+ port.value == "" ||
+ (!aLoading &&
+ port.value == defaultPortSecure &&
+ prevDefaultPort != portDefault.value)
+ ) {
+ port.value = defaultPort;
+ }
+ }
+
+ // switch "insecure password" label
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ socketType == Ci.nsMsgSocketType.SSL ||
+ socketType == Ci.nsMsgSocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely"
+ );
+}
+
+function setupMailOnServerUI() {
+ onCheckItem("pop3.deleteMailLeftOnServer", ["pop3.leaveMessagesOnServer"]);
+ setupAgeMsgOnServerUI();
+}
+
+function setupAgeMsgOnServerUI() {
+ const kLeaveMsgsId = "pop3.leaveMessagesOnServer";
+ const kDeleteByAgeId = "pop3.deleteByAgeFromServer";
+ onCheckItem(kDeleteByAgeId, [kLeaveMsgsId]);
+ onCheckItem("daysEnd", [kLeaveMsgsId]);
+ onCheckItem("pop3.numDaysToLeaveOnServer", [kLeaveMsgsId, kDeleteByAgeId]);
+}
+
+function setupFixedUI() {
+ var controls = [
+ document.getElementById("fixedServerName"),
+ document.getElementById("fixedUserName"),
+ document.getElementById("fixedServerPort"),
+ ];
+
+ var len = controls.length;
+ for (let i = 0; i < len; i++) {
+ var fixedElement = controls[i];
+ var otherElement = document.getElementById(
+ fixedElement.getAttribute("use")
+ );
+
+ fixedElement.setAttribute("collapsed", "true");
+ otherElement.removeAttribute("collapsed");
+ }
+}
+
+function BrowseForNewsrc() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ const nsIFile = Ci.nsIFile;
+
+ var newsrcTextBox = document.getElementById("nntp.newsrcFilePath");
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(
+ window,
+ document.getElementById("browseForNewsrc").getAttribute("filepickertitle"),
+ nsIFilePicker.modeSave
+ );
+
+ var currentNewsrcFile;
+ try {
+ currentNewsrcFile = Cc["@mozilla.org/file/local;1"].createInstance(nsIFile);
+ currentNewsrcFile.initWithPath(newsrcTextBox.value);
+ } catch (e) {
+ dump("Failed to create nsIFile instance for the current newsrc file.\n");
+ }
+
+ if (currentNewsrcFile) {
+ fp.displayDirectory = currentNewsrcFile.parent;
+ fp.defaultString = currentNewsrcFile.leafName;
+ }
+
+ fp.appendFilters(nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ newsrcTextBox.value = fp.file.path;
+ newsrcTextBox.dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+function setupImapDeleteUI(aServerId) {
+ // read delete_model preference
+ let deleteModel = document
+ .getElementById("imap.deleteModel")
+ .getAttribute("value");
+ selectImapDeleteModel(deleteModel);
+
+ // read trash folder path preference
+ let trashFolderName = getTrashFolderName();
+
+ // set folderPicker menulist
+ let trashPopup = document.getElementById("msgTrashFolderPopup");
+ trashPopup._teardown();
+ trashPopup._parentFolder = MailUtils.getOrCreateFolder(aServerId);
+ trashPopup._ensureInitialized();
+
+ // Escape backslash and double-quote with another backslash before encoding.
+ let trashEscaped = trashFolderName.replace(/([\\"])/g, "\\$1");
+
+ // Convert the folder path from JS Unicode to MUTF-7 if necessary.
+ let imapServer = trashPopup._parentFolder.server.QueryInterface(
+ Ci.nsIImapIncomingServer
+ );
+
+ let trashFolder;
+ if (imapServer.utf8AcceptEnabled) {
+ // Trash folder with UTF8=ACCEPT capability in effect.
+ trashFolder = MailUtils.getOrCreateFolder(aServerId + "/" + trashEscaped);
+ } else {
+ // Traditional MUTF-7.
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ trashFolder = MailUtils.getOrCreateFolder(
+ aServerId + "/" + manager.unicodeToMutf7(trashEscaped)
+ );
+ }
+ trashPopup.selectFolder(trashFolder);
+ trashPopup.parentNode.folder = trashFolder;
+}
+
+function selectImapDeleteModel(choice) {
+ // set deleteModel to selected mode
+ document.getElementById("imap.deleteModel").setAttribute("value", choice);
+
+ switch (choice) {
+ case "0": // markDeleted
+ // disable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .setAttribute("disabled", "true");
+ break;
+ case "1": // moveToTrashFolder
+ // enable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .removeAttribute("disabled");
+ break;
+ case "2": // deleteImmediately
+ // disable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .setAttribute("disabled", "true");
+ break;
+ default:
+ dump("Error in enabling/disabling server.TrashFolderPicker\n");
+ break;
+ }
+}
+
+// Capture any menulist changes from folderPicker
+function folderPickerChange(aEvent) {
+ let folder = aEvent.target._folder;
+ // Since we need to deal with localised folder names, we simply use
+ // the path of the URI like we do in nsImapIncomingServer::DiscoveryDone().
+ // Note that the path is returned with a leading slash which we need to remove.
+ let folderPath = Services.io.newURI(folder.URI).pathQueryRef.substring(1);
+ let folderPathUnescaped = Services.io.unescapeString(
+ folderPath,
+ Ci.nsINetUtil.ESCAPE_URL_PATH
+ );
+
+ // Convert the folder path from MUTF-7 or UTF-8 to Unicode.
+ let imapServer = folder.server.QueryInterface(Ci.nsIImapIncomingServer);
+
+ let trashUnicode;
+ if (imapServer.utf8AcceptEnabled) {
+ // UTF8=ACCEPT capability in effect. Unescaping has brought back
+ // raw UTF-8 bytes, so convert them to JS Unicode.
+ let typedarray = new Uint8Array(folderPathUnescaped.length);
+ for (let i = 0; i < folderPathUnescaped.length; i++) {
+ typedarray[i] = folderPathUnescaped.charCodeAt(i);
+ }
+ let utf8Decoder = new TextDecoder("utf-8");
+ trashUnicode = utf8Decoder.decode(typedarray);
+ } else {
+ // We need to convert that from MUTF-7 to Unicode.
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ trashUnicode = manager.mutf7ToUnicode(folderPathUnescaped);
+ }
+
+ // Set the value to be persisted.
+ document
+ .getElementById("imap.trashFolderName")
+ .setAttribute("value", trashUnicode);
+
+ // Update the widget to show/do correct things even for subfolders.
+ let trashFolderPicker = document.getElementById("msgTrashFolderPicker");
+ trashFolderPicker.menupopup.selectFolder(folder);
+}
+
+// Get trash_folder_name from prefs. Despite its name this returns
+// a folder path, for example INBOX/Trash.
+function getTrashFolderName() {
+ let trashFolderName = document
+ .getElementById("imap.trashFolderName")
+ .getAttribute("value");
+ // if the preference hasn't been set, set it to a sane default
+ if (!trashFolderName) {
+ trashFolderName = "Trash"; // XXX Is this a useful default?
+ document
+ .getElementById("imap.trashFolderName")
+ .setAttribute("value", trashFolderName);
+ }
+ return trashFolderName;
+}
diff --git a/comm/mailnews/base/prefs/content/am-server.xhtml b/comm/mailnews/base/prefs/content/am-server.xhtml
new file mode 100644
index 0000000000..b41e93e290
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server.xhtml
@@ -0,0 +1,674 @@
+<?xml version="1.0"?>
+<!-- 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 https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % trashDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">
+%trashDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&serverSettings.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-server.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/menulist-charsetpicker.js"
+ ></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-server.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <label hidden="true" wsm_persist="true" id="server.type" />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.storeContractID"
+ genericattr="true"
+ id="server.storeContractID"
+ />
+
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&serverSettings.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <div xmlns="http://www.w3.org/1999/xhtml" id="amServerSetting">
+ <div>
+ <xul:label value="&serverType.label;" />
+ </div>
+ <div>
+ <xul:label id="servertypeVerbose" />
+ </div>
+ <div style="grid-row: 2">
+ <xul:label
+ value="&serverName.label;"
+ accesskey="&serverName.accesskey;"
+ control="server.hostName"
+ />
+ </div>
+ <div class="input-container" style="grid-row: 2">
+ <xul:label
+ id="fixedServerName"
+ collapsed="true"
+ use="server.hostName"
+ />
+ <html:input
+ id="server.hostName"
+ type="text"
+ wsm_persist="true"
+ size="20"
+ prefstring="mail.server.%serverkey%.hostname"
+ class="uri-element input-flex input-inline"
+ aria-labelledby="fixedServerName"
+ />
+ </div>
+ <div class="input-container" style="grid-row: 2">
+ <xul:label
+ value="&port.label;"
+ accesskey="&port.accesskey;"
+ control="server.port"
+ />
+ <xul:label id="fixedServerPort" collapsed="true" use="server.port" />
+ <html:input
+ id="server.port"
+ type="number"
+ class="size3"
+ min="1"
+ max="65535"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.port"
+ />
+ <xul:label value="&serverPortDefault.label;" />
+ <xul:label id="defaultPort" />
+ </div>
+ <div hidefor="nntp" style="grid-row: 3">
+ <xul:label
+ value="&userName.label;"
+ accesskey="&userName.accesskey;"
+ control="server.username"
+ />
+ </div>
+ <div class="input-container" hidefor="nntp" style="grid-row: 3">
+ <xul:label
+ id="fixedUserName"
+ collapsed="true"
+ use="server.username"
+ />
+ <html:input
+ id="server.username"
+ type="text"
+ wsm_persist="true"
+ size="20"
+ class="input-flex input-inline"
+ prefstring="mail.server.%serverkey%.username"
+ aria-labelledby="fixedUserName"
+ />
+ </div>
+ </div>
+
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&securitySettings.label;</html:legend>
+ <html:table>
+ <html:tr>
+ <html:td>
+ <label
+ value="&connectionSecurity.label;"
+ accesskey="&connectionSecurity.accesskey;"
+ control="server.socketType"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ wsm_persist="true"
+ id="server.socketType"
+ oncommand="secureSelect();"
+ style="width: 100%"
+ >
+ <menupopup id="server.socketTypePopup">
+ <menuitem
+ value="0"
+ label="&connectionSecurityType-0.label;"
+ />
+ <menuitem
+ id="connectionSecurityType-1"
+ value="1"
+ label="&connectionSecurityType-1.label;"
+ disabled="true"
+ />
+ <menuitem
+ value="2"
+ label="&connectionSecurityType-2.label;"
+ hidefor="nntp"
+ />
+ <menuitem
+ value="3"
+ label="&connectionSecurityType-3.label;"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:td>
+ <label
+ value="&authMethod.label;"
+ accesskey="&authMethod.accesskey;"
+ control="server.authMethod"
+ hidefor="nntp"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ id="server.authMethod"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.authMethod"
+ hidefor="nntp"
+ style="width: 100%"
+ >
+ <menupopup id="server.authMethodPopup">
+ <menuitem id="authMethod-no" value="1" />
+ <menuitem id="authMethod-old" value="2" />
+ <menuitem id="authMethod-password-cleartext" value="3" />
+ <menuitem id="authMethod-password-encrypted" value="4" />
+ <menuitem id="authMethod-kerberos" value="5" />
+ <menuitem id="authMethod-ntlm" value="6" />
+ <menuitem id="authMethod-external" value="7" />
+ <menuitem id="authMethod-oauth2" value="10" />
+ <menuitem id="authMethod-anysecure" value="8" />
+ <menuitem id="authMethod-any" value="9" />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+ </html:table>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&serverSettings.label;</html:legend>
+ <vbox align="start">
+ <checkbox
+ wsm_persist="true"
+ id="server.loginAtStartUp"
+ label="&loginAtStartup.label;"
+ accesskey="&loginAtStartup.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.login_at_startup"
+ />
+ </vbox>
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="server.doBiff"
+ label="&biffStart.label;"
+ accesskey="&biffStart.accesskey;"
+ oncommand="onCheckItem('server.biffMinutes', [this.id]);"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.check_new_mail"
+ />
+ <html:input
+ id="server.biffMinutes"
+ type="number"
+ class="size3"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="server.doBiff server.biffMinutes biffEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.check_time"
+ />
+ <label
+ id="biffEnd"
+ control="server.biffMinutes"
+ value="&biffEnd.label;"
+ />
+ </hbox>
+ <vbox align="start" hidefor="pop3,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="imap.useIdle"
+ label="&useIdleNotifications.label;"
+ accesskey="&useIdleNotifications.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.use_idle"
+ />
+ </vbox>
+
+ <!-- Necessary for POP3 (Bug 480945) -->
+ <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=480945 -->
+ <vbox align="start" hidefor="imap,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="server.downloadOnBiff"
+ label="&downloadOnBiff.label;"
+ prefattribute="value"
+ accesskey="&downloadOnBiff.accesskey;"
+ prefstring="mail.server.%serverkey%.download_on_biff"
+ />
+ </vbox>
+ <!-- POP3 -->
+ <vbox align="start" hidefor="imap,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="pop3.headersOnly"
+ label="&headersOnly.label;"
+ accesskey="&headersOnly.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.headers_only"
+ />
+
+ <checkbox
+ wsm_persist="true"
+ id="pop3.leaveMessagesOnServer"
+ label="&leaveOnServer.label;"
+ oncommand="setupMailOnServerUI();"
+ accesskey="&leaveOnServer.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.leave_on_server"
+ />
+
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="pop3.deleteByAgeFromServer"
+ class="indent"
+ label="&deleteByAgeFromServer.label;"
+ oncommand="setupAgeMsgOnServerUI();"
+ accesskey="&deleteByAgeFromServer.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.delete_by_age_from_server"
+ />
+ <html:input
+ id="pop3.numDaysToLeaveOnServer"
+ type="number"
+ class="size3"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="pop3.deleteByAgeFromServer pop3.numDaysToLeaveOnServer daysEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.num_days_to_leave_on_server"
+ />
+ <label
+ id="daysEnd"
+ control="pop3.numDaysToLeaveOnServer"
+ value="&daysEnd.label;"
+ />
+ </hbox>
+
+ <checkbox
+ wsm_persist="true"
+ id="pop3.deleteMailLeftOnServer"
+ class="indent"
+ label="&deleteOnServer2.label;"
+ accesskey="&deleteOnServer2.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.delete_mail_left_on_server"
+ />
+
+ <!-- hidden elements for data transfer to and from advanced... dialog -->
+ <hbox flex="1" hidefor="imap,nntp" hidden="true">
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="pop3.deferGetNewMail"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.deferGetNewMail"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="pop3.deferredToAccount"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.deferredToAccount"
+ />
+ </hbox>
+ </vbox>
+ <!-- IMAP -->
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.trashFolderName"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.trash_folder_name"
+ />
+
+ <separator class="thin" />
+
+ <hbox align="center" hidefor="pop3,nntp">
+ <label
+ value="&deleteMessagePrefix.label;"
+ align="start"
+ control="imap.deleteModel"
+ />
+ </hbox>
+ <vbox>
+ <hbox
+ align="center"
+ id="imap.deleteModel.box"
+ hidefor="pop3,nntp"
+ flex="1"
+ >
+ <radiogroup
+ id="imap.deleteModel"
+ wsm_persist="true"
+ oncommand="selectImapDeleteModel(this.value);"
+ prefstring="mail.server.%serverkey%.delete_model"
+ >
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelMoveToTrash"
+ value="1"
+ label="&modelMoveToTrash.label;"
+ accesskey="&modelMoveToTrash.accesskey;"
+ />
+ <menulist
+ id="msgTrashFolderPicker"
+ class="folderMenuItem"
+ style="max-width: 300px"
+ flex="1"
+ crop="center"
+ aria-labelledby="modelMoveToTrash"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="msgTrashFolderPopup"
+ mode="filing"
+ showFileHereLabel="true"
+ oncommand="folderPickerChange(event);"
+ />
+ </menulist>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelMarkDeleted"
+ value="0"
+ label="&modelMarkDeleted.label;"
+ accesskey="&modelMarkDeleted.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelDeleteImmediately"
+ value="2"
+ label="&modelDeleteImmediately.label;"
+ accesskey="&modelDeleteImmediately.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <hbox pack="end">
+ <!-- This button should have identical attributes to the
+ server.popAdvancedButton except the hidefor attribute. -->
+ <button
+ label="&advancedButton.label;"
+ accesskey="&advancedButton.accesskey;"
+ oncommand="onAdvanced();"
+ wsm_persist="true"
+ id="server.imapAdvancedButton"
+ prefstring="mail.server.%serverkey%.advanced.disable"
+ hidefor="pop3,nntp"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- Advanced IMAP settings -->
+ <hbox flex="1" hidefor="pop3,nntp" hidden="true">
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.dualUseFolders"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.dual_use_folders"
+ />
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.usingSubscription"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.using_subscription"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.maximumConnectionsNumber"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.personalNamespace"
+ />
+ <label hidden="true" wsm_persist="true" id="imap.publicNamespace" />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.otherUsersNamespace"
+ />
+ <label hidden="true" wsm_persist="true" id="imap.serverDirectory" />
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.overrideNamespaces"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.override_namespaces"
+ />
+ </hbox>
+
+ <!-- NNTP -->
+ <hbox hidefor="pop3,imap" align="center">
+ <checkbox
+ id="nntp.notifyOn"
+ wsm_persist="true"
+ label="&maxMessagesStart.label;"
+ accesskey="&maxMessagesStart.accesskey;"
+ oncommand="onCheckItem('nntp.maxArticles', [this.id]);"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.notify.on"
+ />
+ <html:input
+ id="nntp.maxArticles"
+ type="number"
+ class="size4"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="nntp.notifyOn nntp.maxArticles maxMessagesEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.max_articles"
+ />
+ <label
+ control="nntp.maxArticles"
+ value="&maxMessagesEnd.label;"
+ id="maxMessagesEnd"
+ />
+ </hbox>
+ <checkbox
+ hidefor="pop3,imap"
+ wsm_persist="true"
+ id="nntp.pushAuth"
+ label="&alwaysAuthenticate.label;"
+ accesskey="&alwaysAuthenticate.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.always_authenticate"
+ />
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&messageStorage.label;</html:legend>
+
+ <hbox align="end">
+ <vbox align="start" flex="1" id="exitHandlingBox=">
+ <checkbox
+ hidefor="pop3,nntp"
+ wsm_persist="true"
+ id="imap.cleanupInboxOnExit"
+ label="&expungeOnExit.label;"
+ accesskey="&expungeOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.cleanup_inbox_on_exit"
+ />
+ <checkbox
+ hidefor="nntp"
+ wsm_persist="true"
+ id="server.emptyTrashOnExit"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"
+ />
+ </vbox>
+ <button
+ label="&advancedButton.label;"
+ accesskey="&advancedButton.accesskey;"
+ oncommand="onAdvanced();"
+ wsm_persist="true"
+ id="server.popAdvancedButton"
+ prefstring="mail.server.%serverkey%.advanced.disable"
+ hidefor="imap,nntp"
+ />
+ </hbox>
+ <hbox align="center">
+ <label
+ value="&storeType.label;"
+ accesskey="&storeType.accesskey;"
+ control="server.storeTypeMenulist"
+ />
+ <menulist
+ id="server.storeTypeMenulist"
+ oncommand="clickStoreTypeMenu(this);"
+ >
+ <menupopup id="server.storeTypeMenupopup">
+ <menuitem
+ id="server.mboxStore"
+ value="@mozilla.org/msgstore/berkeleystore;1"
+ label="&mboxStore2.label;"
+ />
+ <menuitem
+ id="server.maildirStore"
+ value="@mozilla.org/msgstore/maildirstore;1"
+ label="&maildirStore.label;"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <hbox align="center" hidefor="imap,pop3">
+ <label
+ id="nntp.newsrcFilePath.label"
+ value="&newsrcFilePath1.label;"
+ control="nntp.newsrcFilePath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="nntp.newsrcFilePath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.newsrc.file"
+ class="uri-element input-inline"
+ aria-labelledby="nntp.newsrcFilePath.label"
+ />
+ </hbox>
+ <button
+ id="browseForNewsrc"
+ label="&browseNewsrc.label;"
+ filepickertitle="&newsrcPicker1.label;"
+ accesskey="&browseNewsrc.accesskey;"
+ oncommand="BrowseForNewsrc();"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ id="server.localPath.label"
+ value="&localPath1.label;"
+ control="server.localPath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="server.localPath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ class="uri-element input-inline"
+ aria-labelledby="server.localPath.label"
+ />
+ </hbox>
+ <button
+ id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders();"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox hidefor="imap,pop3" align="center" iscontrolcontainer="true">
+ <separator class="thin" />
+ <label value="&serverDefaultCharset2.label;" control="nntp.charset" />
+ <menulist
+ is="menulist-charsetpicker-viewing"
+ id="nntp.charset"
+ hidable="true"
+ hidefor="imap,pop3"
+ wsm_persist="true"
+ preftype="string"
+ prefstring="mail.server.%serverkey%.charset"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js
new file mode 100644
index 0000000000..12c1e7b837
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js
@@ -0,0 +1,94 @@
+/* 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 { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gAccount;
+var gOriginalStoreType;
+
+/**
+ * Called when the store type menu is clicked.
+ *
+ * @param {object} aStoreTypeElement - store type menu list element.
+ */
+function clickStoreTypeMenu(aStoreTypeElement) {
+ if (aStoreTypeElement.value == gOriginalStoreType) {
+ return;
+ }
+
+ // Response from migration dialog modal. If the conversion is complete
+ // 'response.newRootFolder' will hold the path to the new account root folder,
+ // otherwise 'response.newRootFolder' will be null.
+ let response = { newRootFolder: null };
+ // Send 'response' as an argument to converterDialog.xhtml.
+ window.browsingContext.topChromeWindow.openDialog(
+ "converterDialog.xhtml",
+ "mailnews:mailstoreconverter",
+ "modal,centerscreen,resizable=no,width=700,height=130",
+ gAccount.incomingServer,
+ aStoreTypeElement.value,
+ response
+ );
+ changeStoreType(response);
+}
+
+/**
+ * Revert store type to the original store type if converter modal closes
+ * before migration is complete, otherwise change original store type to
+ * currently selected store type.
+ *
+ * @param {object} aResponse - response from migration dialog modal.
+ */
+function changeStoreType(aResponse) {
+ if (aResponse.newRootFolder) {
+ // The conversion is complete.
+ // Set local path to the new account root folder which is present
+ // in 'aResponse.newRootFolder'.
+ document.getElementById("server.localPath").value = aResponse.newRootFolder;
+ gOriginalStoreType = document.getElementById(
+ "server.storeTypeMenulist"
+ ).value;
+ MailUtils.restartApplication();
+ } else {
+ // The conversion failed or was cancelled.
+ // Restore selected item to what was selected before conversion.
+ document.getElementById("server.storeTypeMenulist").value =
+ gOriginalStoreType;
+ }
+}
+
+function onInit(aPageId, aServerId) {
+ // UI for account store type
+ let storeTypeElement = document.getElementById("server.storeTypeMenulist");
+ // set the menuitem to match the account
+ let currentStoreID = document
+ .getElementById("server.storeContractID")
+ .getAttribute("value");
+ let targetItem = storeTypeElement.getElementsByAttribute(
+ "value",
+ currentStoreID
+ );
+ storeTypeElement.selectedItem = targetItem[0];
+ // Disable store type change if store has not been used yet.
+ storeTypeElement.setAttribute(
+ "disabled",
+ gAccount.incomingServer.getBoolValue("canChangeStoreType")
+ ? "false"
+ : !Services.prefs.getBoolPref("mail.store_conversion_enabled")
+ );
+ // Initialise 'gOriginalStoreType' to the item that was originally selected.
+ gOriginalStoreType = storeTypeElement.value;
+}
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+}
+
+function onSave() {
+ let storeContractID = document.getElementById("server.storeTypeMenulist")
+ .selectedItem.value;
+ document
+ .getElementById("server.storeContractID")
+ .setAttribute("value", storeContractID);
+}
diff --git a/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml
new file mode 100644
index 0000000000..1cdc36550f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml
@@ -0,0 +1,152 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % accountNoIdentDTD SYSTEM "chrome://messenger/locale/am-serverwithnoidentities.dtd">
+%accountNoIdentDTD;
+<!ENTITY % accountServerTopDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">%accountServerTopDTD;
+]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&accountTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-serverwithnoidentities.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-serverwithnoidentities.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&accountTitle.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <label
+ hidden="true"
+ wsm_persist="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.storeContractID"
+ genericattr="true"
+ id="server.storeContractID"
+ />
+
+ <description class="secDesc">&accountSettingsDesc.label;</description>
+ <hbox class="input-container">
+ <label
+ id="server.prettyName.label"
+ value="&accountName.label;"
+ control="server.prettyName"
+ accesskey="&accountName.accesskey;"
+ />
+ <html:input
+ id="server.prettyName"
+ type="text"
+ wsm_persist="true"
+ class="input-inline"
+ onblur="parent.setAccountLabel(gAccount.key, this.value);"
+ prefstring="mail.server.%serverkey%.name"
+ aria-labelledby="server.prettyName.label"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&messageStorage.label;</html:legend>
+
+ <vbox align="start">
+ <checkbox
+ wsm_persist="true"
+ id="server.emptyTrashOnExit"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"
+ />
+ <hbox align="center">
+ <label
+ value="&storeType.label;"
+ accesskey="&storeType.accesskey;"
+ control="server.storeTypeMenulist"
+ />
+ <menulist
+ id="server.storeTypeMenulist"
+ oncommand="clickStoreTypeMenu(this);"
+ >
+ <menupopup id="server.storeTypeMenupopup">
+ <menuitem
+ id="server.mboxStore"
+ value="@mozilla.org/msgstore/berkeleystore;1"
+ label="&mboxStore2.label;"
+ />
+ <menuitem
+ id="server.maildirStore"
+ value="@mozilla.org/msgstore/maildirstore;1"
+ label="&maildirStore.label;"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ id="server.localPath.label"
+ value="&localPath1.label;"
+ control="server.localPath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="server.localPath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ class="uri-element input-inline"
+ aria-labelledby="server.localPath.label"
+ />
+ </hbox>
+ <button
+ id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders()"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-smtp.js b/comm/mailnews/base/prefs/content/am-smtp.js
new file mode 100644
index 0000000000..4dd48fa795
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-smtp.js
@@ -0,0 +1,277 @@
+/* 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/. */
+
+/* import-globals-from amUtils.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+window.addEventListener("DOMContentLoaded", event => {
+ gSmtpServerListWindow.onLoad();
+});
+
+var gSmtpServerListWindow = {
+ mBundle: null,
+ mServerList: null,
+ mAddButton: null,
+ mEditButton: null,
+ mDeleteButton: null,
+ mSetDefaultServerButton: null,
+
+ onLoad() {
+ parent.onPanelLoaded("am-smtp.xhtml");
+
+ this.mBundle = document.getElementById("bundle_messenger");
+ this.mServerList = document.getElementById("smtpList");
+ this.mAddButton = document.getElementById("addButton");
+ this.mEditButton = document.getElementById("editButton");
+ this.mDeleteButton = document.getElementById("deleteButton");
+ this.mSetDefaultServerButton = document.getElementById("setDefaultButton");
+
+ this.refreshServerList("", false);
+
+ this.updateButtons();
+ },
+
+ onSelectionChanged(aEvent) {
+ var server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ this.updateButtons();
+ this.updateServerInfoBox(server);
+ },
+
+ onDeleteServer(aEvent) {
+ var server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ // confirm deletion
+ let cancel = Services.prompt.confirmEx(
+ window,
+ this.mBundle.getString("smtpServers-confirmServerDeletionTitle"),
+ this.mBundle.getFormattedString(
+ "smtpServers-confirmServerDeletion",
+ [server.hostname],
+ 1
+ ),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (!cancel) {
+ // Remove password information first.
+ try {
+ server.forgetPassword();
+ } catch (e) {
+ /* It is OK if this fails. */
+ }
+ // Remove the server.
+ MailServices.smtp.deleteServer(server);
+ parent.replaceWithDefaultSmtpServer(server.key);
+ this.refreshServerList("", true);
+ }
+ },
+
+ onAddServer(aEvent) {
+ this.openServerEditor(null);
+ },
+
+ onEditServer(aEvent) {
+ let server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ this.openServerEditor(server);
+ },
+
+ onSetDefaultServer(aEvent) {
+ let server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ MailServices.smtp.defaultServer = server;
+ this.refreshServerList(MailServices.smtp.defaultServer.key, true);
+ },
+
+ updateButtons() {
+ let server = this.getSelectedServer();
+
+ // can't delete default server
+ if (server && MailServices.smtp.defaultServer == server) {
+ this.mSetDefaultServerButton.setAttribute("disabled", "true");
+ this.mDeleteButton.setAttribute("disabled", "true");
+ } else {
+ this.mSetDefaultServerButton.removeAttribute("disabled");
+ this.mDeleteButton.removeAttribute("disabled");
+ }
+
+ if (!server) {
+ this.mEditButton.setAttribute("disabled", "true");
+ } else {
+ this.mEditButton.removeAttribute("disabled");
+ }
+ },
+
+ updateServerInfoBox(aServer) {
+ var noneSelected = this.mBundle.getString("smtpServerList-NotSpecified");
+
+ document.getElementById("nameValue").textContent = aServer.hostname;
+ document.getElementById("descriptionValue").textContent =
+ aServer.description || noneSelected;
+ document.getElementById("portValue").textContent =
+ aServer.port || noneSelected;
+ document.getElementById("userNameValue").textContent =
+ aServer.username || noneSelected;
+ document.getElementById("useSecureConnectionValue").textContent =
+ this.mBundle.getString(
+ "smtpServer-ConnectionSecurityType-" + aServer.socketType
+ );
+
+ const AuthMethod = Ci.nsMsgAuthMethod;
+ const SocketType = Ci.nsMsgSocketType;
+ var authStr = "";
+ switch (aServer.authMethod) {
+ case AuthMethod.none:
+ authStr = "authNo";
+ break;
+ case AuthMethod.passwordEncrypted:
+ authStr = "authPasswordEncrypted";
+ break;
+ case AuthMethod.GSSAPI:
+ authStr = "authKerberos";
+ break;
+ case AuthMethod.NTLM:
+ authStr = "authNTLM";
+ break;
+ case AuthMethod.secure:
+ authStr = "authAnySecure";
+ break;
+ case AuthMethod.passwordCleartext:
+ authStr =
+ aServer.socketType == SocketType.SSL ||
+ aServer.socketType == SocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely";
+ break;
+ case AuthMethod.OAuth2:
+ authStr = "authOAuth2";
+ break;
+ default:
+ // leave empty
+ console.error(
+ "Warning: unknown value for smtpserver... authMethod: " +
+ aServer.authMethod
+ );
+ }
+ document.getElementById("authMethodValue").textContent = authStr
+ ? this.mBundle.getString(authStr)
+ : noneSelected;
+ },
+
+ refreshServerList(aServerKeyToSelect, aFocusList) {
+ while (this.mServerList.hasChildNodes()) {
+ this.mServerList.lastChild.remove();
+ }
+ for (let server of MailServices.smtp.servers) {
+ let listitem = this.createSmtpListItem(
+ server,
+ MailServices.smtp.defaultServer.key == server.key
+ );
+ this.mServerList.appendChild(listitem);
+ }
+
+ if (aServerKeyToSelect) {
+ this.setSelectedServer(
+ this.mServerList.querySelector('[key="' + aServerKeyToSelect + '"]')
+ );
+ } else {
+ // Select the default server.
+ this.setSelectedServer(
+ this.mServerList.querySelector('[default="true"]')
+ );
+ }
+
+ if (aFocusList) {
+ this.mServerList.focus();
+ }
+ },
+
+ createSmtpListItem(aServer, aIsDefault) {
+ var listitem = document.createXULElement("richlistitem");
+ var serverName = "";
+
+ if (aServer.description) {
+ serverName = aServer.description + " - ";
+ } else if (aServer.username) {
+ serverName = aServer.username + " - ";
+ }
+
+ serverName += aServer.hostname;
+
+ if (aIsDefault) {
+ serverName += " " + this.mBundle.getString("defaultServerTag");
+ listitem.setAttribute("default", "true");
+ }
+
+ let label = document.createXULElement("label");
+ label.setAttribute("value", serverName);
+ listitem.appendChild(label);
+ listitem.setAttribute("key", aServer.key);
+ listitem.setAttribute("class", "smtpServerListItem");
+
+ // give it some unique id
+ listitem.id = "smtpServer." + aServer.key;
+ return listitem;
+ },
+
+ openServerEditor(aServer) {
+ let args = editSMTPServer(aServer);
+
+ // now re-select the server which was just added
+ if (args.result) {
+ this.refreshServerList(aServer ? aServer.key : args.addSmtpServer, true);
+ }
+
+ return args.result;
+ },
+
+ setSelectedServer(aServer) {
+ if (!aServer) {
+ return;
+ }
+
+ setTimeout(
+ function (aServerList) {
+ aServerList.ensureElementIsVisible(aServer);
+ aServerList.selectItem(aServer);
+ },
+ 0,
+ this.mServerList
+ );
+ },
+
+ getSelectedServer() {
+ // The list of servers is a single selection listbox
+ // therefore 1 item is always selected.
+ // But if there are no SMTP servers defined yet, nothing will be selected.
+ let selection = this.mServerList.selectedItem;
+ if (!selection) {
+ return null;
+ }
+
+ let serverKey = selection.getAttribute("key");
+ return MailServices.smtp.getServerByKey(serverKey);
+ },
+};
diff --git a/comm/mailnews/base/prefs/content/am-smtp.xhtml b/comm/mailnews/base/prefs/content/am-smtp.xhtml
new file mode 100644
index 0000000000..851fb8da3e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-smtp.xhtml
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-advanced.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&smtpServer.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-smtp.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox flex="1" style="overflow: auto"
+ ><vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&smtpServer.label;" />
+ </hbox>
+
+ <separator class="thin" />
+ <label control="smtpList">&smtpDescription.label;</label>
+ <separator class="thin" />
+
+ <hbox flex="1">
+ <richlistbox
+ id="smtpList"
+ onselect="gSmtpServerListWindow.onSelectionChanged(event);"
+ ondblclick="gSmtpServerListWindow.onEditServer(event);"
+ seltype="single"
+ flex="1"
+ style="height: 400px"
+ />
+
+ <vbox>
+ <button
+ id="addButton"
+ oncommand="gSmtpServerListWindow.onAddServer(event);"
+ label="&smtpListAdd.label;"
+ accesskey="&smtpListAdd.accesskey;"
+ />
+ <button
+ id="editButton"
+ oncommand="gSmtpServerListWindow.onEditServer(event);"
+ label="&smtpListEdit.label;"
+ accesskey="&smtpListEdit.accesskey;"
+ />
+ <separator />
+ <button
+ id="deleteButton"
+ disabled="true"
+ oncommand="gSmtpServerListWindow.onDeleteServer(event);"
+ label="&smtpListDelete.label;"
+ accesskey="&smtpListDelete.accesskey;"
+ />
+ <button
+ id="setDefaultButton"
+ disabled="true"
+ oncommand="gSmtpServerListWindow.onSetDefaultServer(event);"
+ label="&smtpListSetDefault.label;"
+ accesskey="&smtpListSetDefault.accesskey;"
+ />
+ </vbox>
+ </hbox>
+
+ <separator />
+
+ <label class="header">&serverDetails.label;</label>
+ <html:table id="smtpServerInfoBox">
+ <html:tr>
+ <html:th id="descriptionLabel">&serverDescription.label;</html:th>
+ <html:td id="descriptionValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="nameLabel">&serverName.label;</html:th>
+ <html:td id="nameValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="portLabel">&serverPort.label;</html:th>
+ <html:td id="portValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="userNameLabel">&userName.label;</html:th>
+ <html:td id="userNameValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="authMethodLabel">&authMethod.label;</html:th>
+ <html:td id="authMethodValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="connectionSecurityLabel"
+ >&connectionSecurity.label;</html:th
+ >
+ <html:td id="useSecureConnectionValue"></html:td>
+ </html:tr>
+ </html:table>
+ <separator flex="1" /> </vbox
+ ></vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/amUtils.js b/comm/mailnews/base/prefs/content/amUtils.js
new file mode 100644
index 0000000000..b63f27b6c4
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/amUtils.js
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* import-globals-from am-smtp.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+function BrowseForLocalFolders() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ const nsIFile = Ci.nsIFile;
+
+ var currentFolderTextBox = document.getElementById("server.localPath");
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+
+ fp.init(
+ window,
+ document
+ .getElementById("browseForLocalFolder")
+ .getAttribute("filepickertitle"),
+ nsIFilePicker.modeGetFolder
+ );
+
+ var currentFolder = Cc["@mozilla.org/file/local;1"].createInstance(nsIFile);
+ try {
+ currentFolder.initWithPath(currentFolderTextBox.value);
+ fp.displayDirectory = currentFolder;
+ } catch (e) {
+ console.error(
+ `Failed to set folder path from value=${currentFolderTextBox.value}\n`
+ );
+ }
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ // Retrieve the selected folder.
+ let selectedFolder = fp.file;
+
+ // Check if the folder can be used for mail storage.
+ if (!top.checkDirectoryIsUsable(selectedFolder)) {
+ return;
+ }
+
+ currentFolderTextBox.value = selectedFolder.path;
+ currentFolderTextBox.dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+/**
+ * Return server/folder name formatted with server name if needed.
+ *
+ * @param {nsIMsgFolder} aTargetFolder - nsIMsgFolder to format name for
+ @returns {string} THe formatted name.
+ * If target.isServer then only its name is returned.
+ * Otherwise return the name as "<foldername> on <servername>".
+ */
+function prettyFolderName(aTargetFolder) {
+ if (aTargetFolder.isServer) {
+ return aTargetFolder.prettyName;
+ }
+
+ return document
+ .getElementById("bundle_messenger")
+ .getFormattedString("verboseFolderFormat", [
+ aTargetFolder.prettyName,
+ aTargetFolder.server.prettyName,
+ ]);
+}
+
+/**
+ * Checks validity of junk target server name and folder.
+ *
+ * @param {string} aTargetURI - The URI specification to check.
+ * @param {boolean} aIsServer - true if the URI specifies only a server
+ * (without folder)
+ *
+ * @returns {string} the value of aTargetURI if it is valid (usable), otherwise null
+ */
+function checkJunkTargetFolder(aTargetURI, aIsServer) {
+ try {
+ // Does the target account exist?
+ let targetServer;
+ if (aIsServer) {
+ targetServer = MailUtils.getOrCreateFolder(aTargetURI + "/Junk").server;
+ } else {
+ targetServer = MailUtils.getExistingFolder(aTargetURI).server;
+ }
+
+ // If the target server has deferred storage, Junk can't be stored into it.
+ if (targetServer.rootFolder != targetServer.rootMsgFolder) {
+ return null;
+ }
+ } catch (e) {
+ return null;
+ }
+
+ return aTargetURI;
+}
+
+/**
+ * Finds a usable target for storing Junk mail.
+ * If the passed in server URI is not usable, choose Local Folders.
+ *
+ * @param {string} aTargetURI - The URI of a server or folder to try first
+ * @param {boolean} aIsServer - true if the URI specifies only a server (without folder)
+ *
+ * @returns {string} the server/folder URI of a usable target for storing Junk
+ */
+function chooseJunkTargetFolder(aTargetURI, aIsServer) {
+ let server = null;
+
+ if (aTargetURI) {
+ server = MailUtils.getOrCreateFolder(aTargetURI).server;
+ if (
+ !server.canCreateFoldersOnServer ||
+ !server.canSearchMessages ||
+ server.rootFolder != server.rootMsgFolder
+ ) {
+ server = null;
+ }
+ }
+ if (!server) {
+ server = MailServices.accounts.localFoldersServer;
+ }
+
+ return server.serverURI + (!aIsServer ? "/Junk" : "");
+}
+
+/**
+ * Fixes junk target folders if they point to an invalid/unusable (e.g. deferred)
+ * folder/account. Only returns the new safe values. It is up to the caller
+ * to push them to the proper elements/prefs.
+ *
+ * @param {string} aSpamActionTargetAccount - The value of the
+ * server.*.spamActionTargetAccount pref value (URI).
+ * @param {string} aSpamActionTargetFolder - The value of the
+ * server.*.spamActionTargetFolder pref value (URI).
+ * @param {string} aProposedTarget - The URI of a new target to try.
+ * @param {integer} aMoveTargetModeValue - The value of the
+ * server.*.moveTargetMode pref value (0/1).
+ * @param {nsISpamSettings} aServerSpamSettings - The nsISpamSettings object
+ * of any server (used just for the MOVE_TARGET_MODE_* constants).
+ * @param {boolean} aMoveOnSpam - The server.*.moveOnSpam pref value).
+ *
+ * @returns {object[]} an array containing:
+ * newTargetAccount new safe junk target account
+ * newTargetAccount new safe junk target folder
+ * newMoveOnSpam new moveOnSpam value
+ */
+function sanitizeJunkTargets(
+ aSpamActionTargetAccount,
+ aSpamActionTargetFolder,
+ aProposedTarget,
+ aMoveTargetModeValue,
+ aServerSpamSettings,
+ aMoveOnSpam
+) {
+ // Check if folder targets are valid.
+ aSpamActionTargetAccount = checkJunkTargetFolder(
+ aSpamActionTargetAccount,
+ true
+ );
+ if (!aSpamActionTargetAccount) {
+ // If aSpamActionTargetAccount is not valid,
+ // reset to default behavior to NOT move junk messages...
+ if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_ACCOUNT) {
+ aMoveOnSpam = false;
+ }
+
+ // ... and find a good default target.
+ aSpamActionTargetAccount = chooseJunkTargetFolder(aProposedTarget, true);
+ }
+
+ aSpamActionTargetFolder = checkJunkTargetFolder(
+ aSpamActionTargetFolder,
+ false
+ );
+ if (!aSpamActionTargetFolder) {
+ // If aSpamActionTargetFolder is not valid,
+ // reset to default behavior to NOT move junk messages...
+ if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_FOLDER) {
+ aMoveOnSpam = false;
+ }
+
+ // ... and find a good default target.
+ aSpamActionTargetFolder = chooseJunkTargetFolder(aProposedTarget, false);
+ }
+
+ return [aSpamActionTargetAccount, aSpamActionTargetFolder, aMoveOnSpam];
+}
+
+/**
+ * Opens Preferences (Options) dialog on the Advanced pane, General tab
+ * so that the user sees where the global receipts settings can be found.
+ *
+ * @param {string} aTBPaneId - Thunderbird pref paneID to open.
+ * @param {string} aTBScrollPaneTo - Thunderbird ID of the element to scroll into view.
+ * @param {any} aTBOtherArgs - Other arguments to send to the pref tab.
+ * @param {string} aSMPaneId - Seamonkey pref pane to open.
+ */
+function openPrefsFromAccountManager(
+ aTBPaneId,
+ aTBScrollPaneTo,
+ aTBOtherArgs,
+ aSMPaneId
+) {
+ let win =
+ Services.wm.getMostRecentWindow("mail:3pane") ||
+ Services.wm.getMostRecentWindow("mail:messageWindow") ||
+ Services.wm.getMostRecentWindow("msgcompose");
+ if (!win) {
+ return;
+ }
+
+ // If openOptionsDialog() exists, we are in Thunderbird.
+ if (typeof win.openOptionsDialog == "function") {
+ win.openOptionsDialog(aTBPaneId, aTBScrollPaneTo, aTBOtherArgs);
+ }
+ // If goPreferences() exists, we are in Seamonkey.
+ if (typeof win.goPreferences == "function") {
+ win.goPreferences(aSMPaneId);
+ }
+}
+
+/**
+ * Check if the given account name already exists in any account.
+ *
+ * @param {string} aAccountName - The account name string to look for.
+ * @param {string} [aAccountKey] - The key of an account that is skipped when
+ * searching the name. If unset, do not skip any account.
+ */
+function accountNameExists(aAccountName, aAccountKey) {
+ for (let account of MailServices.accounts.accounts) {
+ if (
+ account.key != aAccountKey &&
+ account.incomingServer &&
+ aAccountName == account.incomingServer.prettyName
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Open a dialog to edit properties of an SMTP server.
+ *
+ * @param {nsISmtpServer} aServer - The server to edit.
+ * @returns {object} Object with result member to indicate whether 'OK'
+ * was clicked and addSmtpServer with key of newly created server.
+ */
+function editSMTPServer(aServer) {
+ let args = { server: aServer, result: false, addSmtpServer: "" };
+
+ let onCloseSMTPDialog = function () {
+ if (args.result) {
+ gSmtpServerListWindow.refreshServerList(aServer, true);
+ }
+ };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/SmtpServerEdit.xhtml",
+ { closingCallback: onCloseSMTPDialog },
+ args
+ );
+
+ return args;
+}
diff --git a/comm/mailnews/base/prefs/content/aw-accname.js b/comm/mailnews/base/prefs/content/aw-accname.js
new file mode 100644
index 0000000000..97dce27406
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-accname.js
@@ -0,0 +1,28 @@
+/* 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+function acctNamePageValidate() {
+ var accountname = document.getElementById("prettyName").value;
+ // Check if this accountname already exists. If so, return false so that
+ // user can enter a different unique account name.
+ document.querySelector("wizard").canAdvance =
+ !!accountname && !accountNameExists(accountname);
+}
+
+function acctNamePageUnload() {
+ var pageData = parent.GetPageData();
+ var accountname = document.getElementById("prettyName").value;
+ pageData.prettyName = accountname;
+ return true;
+}
+
+function acctNamePageInit() {
+ var accountNameInput = document.getElementById("prettyName");
+ if (accountNameInput.value == "") {
+ accountNameInput.value = parent.GetPageData().hostname;
+ }
+ acctNamePageValidate();
+}
diff --git a/comm/mailnews/base/prefs/content/aw-done.js b/comm/mailnews/base/prefs/content/aw-done.js
new file mode 100644
index 0000000000..93ac3994bc
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-done.js
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function donePageInit() {
+ var pageData = parent.GetPageData();
+
+ var email = pageData.email;
+ setDivTextFromForm("identity.email", email);
+ setDivTextFromForm("server.username", getUsernameFromEmail(email));
+ setDivTextFromForm("account.name", pageData.prettyName);
+ setDivTextFromForm("newsServer.name", pageData.hostname);
+}
+
+function setDivTextFromForm(divid, value) {
+ // collapse the row if the div has no value
+ let label = document.getElementById(`${divid}.label`);
+ let text = document.getElementById(`${divid}.text`);
+
+ if (!value) {
+ text.style.display = "none";
+ label.style.display = "none";
+ return;
+ }
+
+ // otherwise fill in the .text element
+ text.style.display = null;
+ label.style.display = null;
+
+ // set the value
+ text.setAttribute("value", value);
+}
diff --git a/comm/mailnews/base/prefs/content/aw-identity.js b/comm/mailnews/base/prefs/content/aw-identity.js
new file mode 100644
index 0000000000..66d63d6e1e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-identity.js
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+var gPrefsBundle;
+
+function identityPageValidate() {
+ document.querySelector("wizard").canAdvance =
+ document.getElementById("fullName").validity.valid &&
+ document.getElementById("email").validity.valid;
+}
+
+function identityPageUnload() {
+ var pageData = parent.GetPageData();
+ var name = document.getElementById("fullName").value;
+ let email = document.getElementById("email").value.trim();
+ pageData.fullName = name;
+ pageData.email = email;
+
+ return true;
+}
+
+function identityPageInit() {
+ gPrefsBundle = document.getElementById("bundle_prefs");
+ setEmailDescriptionText();
+ checkForFullName();
+ checkForEmail();
+ identityPageValidate();
+}
+
+function setEmailDescriptionText() {
+ var emailDescText = document.getElementById("emailDescText");
+ var emailFieldLabel = document.getElementById("emailFieldLabel");
+
+ // Set the default field label
+ emailFieldLabel.setAttribute(
+ "value",
+ gPrefsBundle.getString("emailFieldText")
+ );
+
+ // Check for obtained values and set with default values if needed
+ var username = gPrefsBundle.getString("exampleEmailUserName");
+ var domain = gPrefsBundle.getString("exampleEmailDomain");
+
+ let displayText = gPrefsBundle.getFormattedString("defaultEmailText", [
+ username,
+ domain,
+ ]);
+
+ // Display the dynamically generated text for email description
+ emailDescText.textContent = displayText;
+}
+
+function checkForFullName() {
+ var name = document.getElementById("fullName");
+ if (name.value == "" && "@mozilla.org/userinfo;1" in Cc) {
+ name.value = Cc["@mozilla.org/userinfo;1"].getService(
+ Ci.nsIUserInfo
+ ).fullname;
+ }
+}
+
+function checkForEmail() {
+ var email = document.getElementById("email");
+ var pageData = parent.GetPageData();
+ if (pageData.email) {
+ email.value = pageData.email;
+ }
+ if (email.value == "" && "@mozilla.org/userinfo;1" in Cc) {
+ email.value = Cc["@mozilla.org/userinfo;1"].getService(
+ Ci.nsIUserInfo
+ ).emailAddress;
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/aw-incoming.js b/comm/mailnews/base/prefs/content/aw-incoming.js
new file mode 100644
index 0000000000..a4f746d71d
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-incoming.js
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+var { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+function incomingPageValidate() {
+ let hostName = cleanUpHostName(document.getElementById("newsServer").value);
+
+ let hasAccount = false;
+ let server = NntpUtils.findServer(hostName);
+ if (server) {
+ // It's OK if a server exists, as long as it's not used by any account.
+ hasAccount = MailServices.accounts.FindAccountForServer(server);
+ }
+ // Can advance if it's a legal host name and we do not already have a server
+ // in use with the same host name.
+ document.querySelector("wizard").canAdvance =
+ !!isLegalHostNameOrIP(hostName) && !hasAccount;
+}
+
+function incomingPageUnload() {
+ parent.GetPageData().hostname = cleanUpHostName(
+ document.getElementById("newsServer").value
+ );
+
+ return true;
+}
+
+function incomingPageInit() {
+ var pageData = parent.GetPageData();
+ if (pageData.hostname) {
+ document.getElementById("newsServer").value = pageData.hostname;
+ }
+ incomingPageValidate();
+}
diff --git a/comm/mailnews/base/prefs/content/converterDialog.js b/comm/mailnews/base/prefs/content/converterDialog.js
new file mode 100644
index 0000000000..e0ac74fd5b
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/converterDialog.js
@@ -0,0 +1,385 @@
+/* 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/. */
+
+/**
+ * This file contains functionality for the front-end part of the mail store
+ * type conversion.
+ */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FolderUtils",
+ "resource:///modules/FolderUtils.jsm"
+);
+var MailstoreConverter = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+window.addEventListener("DOMContentLoaded", () => {
+ for (let img of document.querySelectorAll(".infoIcon")) {
+ img.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/new/activity/warning.svg"
+ );
+ }
+});
+
+var log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+// {nsIMsgIncomingServer} server for the account to be migrated.
+var gServer;
+// {nsIMsgFolder} account root folder.
+var gFolder;
+// 'gResponse.newRootFolder' is path to the new account root folder if migration
+// is complete, else null.
+// 'gResponse' is set to the modified response parameter received from
+// am-server.js.
+var gResponse;
+// Array to hold deferred accounts.
+var gDeferredAccounts = [];
+// Value of Services.io.offline before migration.
+var gOriginalOffline;
+/**
+ * Place account name in migration dialog modal.
+ *
+ * @param {nsIMsgIncomingServer} aServer - account server.
+ */
+function placeAccountName(aServer) {
+ gOriginalOffline = Services.io.offline;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/converterDialog.properties"
+ );
+
+ let brandShortName = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+ // 'deferredToRootFolder' holds path of rootMsgFolder of account to which
+ // other accounts have been deferred.
+ let deferredToRootFolder = aServer.rootMsgFolder.filePath.path;
+ // String to hold names of deferred accounts separated by commas.
+ let deferredAccountsString = "";
+ // Account to which other accounts have been deferred.
+ let deferredToAccount;
+ // Array of all accounts.
+ let accounts = FolderUtils.allAccountsSorted(true);
+
+ for (let account of accounts) {
+ if (
+ account.incomingServer.rootFolder.filePath.path == deferredToRootFolder
+ ) {
+ // Other accounts may be deferred to this account.
+ deferredToAccount = account;
+ } else if (
+ account.incomingServer.rootMsgFolder.filePath.path == deferredToRootFolder
+ ) {
+ // This is a deferred account.
+ gDeferredAccounts.push(account);
+ }
+ }
+
+ // String to hold the names of accounts to be converted separated by commas.
+ let accountsToConvert = "";
+
+ if (gDeferredAccounts.length >= 1) {
+ // Add account names to 'accountsToConvert' and 'deferredAccountsString'.
+ for (let i = 0; i < gDeferredAccounts.length; i++) {
+ if (i < gDeferredAccounts.length - 1) {
+ accountsToConvert +=
+ gDeferredAccounts[i].incomingServer.username + ", ";
+ deferredAccountsString +=
+ gDeferredAccounts[i].incomingServer.username + ", ";
+ } else {
+ accountsToConvert += gDeferredAccounts[i].incomingServer.username;
+ deferredAccountsString += gDeferredAccounts[i].incomingServer.username;
+ }
+ }
+
+ // Username of Local Folders is "nobody". So it's better to use
+ // its hostname which is "Local Folders".
+ // TODO: maybe test against .key == MailServices.accounts.localFoldersServer.key ?
+ if (deferredToAccount.incomingServer.hostName == "Local Folders") {
+ accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
+ } else {
+ accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
+ }
+ log.info(accountsToConvert + " will be converted");
+ let storeContractId = Services.prefs.getCharPref(
+ "mail.server." + deferredToAccount.incomingServer.key + ".storeContractID"
+ );
+
+ if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
+ storeContractId = "maildir";
+ } else {
+ storeContractId = "mbox";
+ }
+
+ // Username of Local Folders is "nobody". So it's better to use
+ // its hostname which is "Local Folders".
+ // TODO: maybe test against .key != MailServices.accounts.localFoldersServer.key ?
+ let deferredToAccountName = deferredToAccount.incomingServer.hostName;
+ if (deferredToAccountName != "Local Folders") {
+ deferredToAccountName = deferredToAccount.incomingServer.username;
+ }
+
+ if (aServer.rootFolder.filePath.path != deferredToRootFolder) {
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName(
+ "converterDialog.warningForDeferredAccount",
+ [
+ aServer.username,
+ deferredToAccountName,
+ deferredToAccountName,
+ deferredAccountsString,
+ accountsToConvert,
+ storeContractId,
+ brandShortName,
+ ]
+ );
+ } else {
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName(
+ "converterDialog.warningForDeferredToAccount",
+ [
+ deferredToAccountName,
+ deferredAccountsString,
+ accountsToConvert,
+ storeContractId,
+ brandShortName,
+ ]
+ );
+ }
+
+ document.getElementById("messageSpan").textContent =
+ bundle.formatStringFromName("converterDialog.messageForDeferredAccount", [
+ accountsToConvert,
+ storeContractId,
+ ]);
+ gServer = deferredToAccount.incomingServer;
+ } else {
+ // No account is deferred.
+ let storeContractId = Services.prefs.getCharPref(
+ "mail.server." + aServer.key + ".storeContractID"
+ );
+ if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
+ storeContractId = "maildir";
+ } else {
+ storeContractId = "mbox";
+ }
+
+ let tempName = aServer.username;
+ if (tempName == "nobody") {
+ tempName = "Local Folders";
+ } else if (!tempName) {
+ tempName = aServer.hostName;
+ }
+
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName("converterDialog.warning", [
+ tempName,
+ storeContractId,
+ brandShortName,
+ ]);
+ document.getElementById("messageSpan").textContent =
+ bundle.formatStringFromName("converterDialog.message", [
+ tempName,
+ storeContractId,
+ ]);
+ gServer = aServer;
+ }
+
+ // Forces the resize of the dialog to the actual content
+ window.sizeToContent();
+}
+
+/**
+ * Start the conversion process.
+ *
+ * @param {string} aSelectedStoreType - mailstore type selected by user.
+ * @param {object} aResponse - response from the migration dialog modal.
+ */
+function startContinue(aSelectedStoreType, aResponse) {
+ gResponse = aResponse;
+ gFolder = gServer.rootFolder.filePath;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/converterDialog.properties"
+ );
+
+ document
+ .getElementById("progress")
+ .addEventListener("progress", function (e) {
+ document.getElementById("progress").value = e.detail;
+ document.getElementById("progressPercent").textContent =
+ bundle.formatStringFromName("converterDialog.percentDone", [e.detail]);
+ });
+
+ document.getElementById("warningArea").hidden = true;
+ document.getElementById("progressArea").hidden = false;
+
+ // Storing original prefs and root folder path
+ // to revert changes in case of error.
+ let p1 = "mail.server." + gServer.key + ".directory";
+ let p2 = "mail.server." + gServer.key + ".directory-rel";
+ let p3 = "mail.server." + gServer.key + ".newsrc.file";
+ let p4 = "mail.server." + gServer.key + ".newsrc.file-rel";
+ let p5 = "mail.server." + gServer.key + ".storeContractID";
+
+ let originalDirectoryPref = Services.prefs.getCharPref(p1);
+ let originalDirectoryRelPref = Services.prefs.getCharPref(p2);
+ let originalNewsrcFilePref;
+ let originalNewsrcFileRelPref;
+ if (gServer.type == "nntp") {
+ originalNewsrcFilePref = Services.prefs.getCharPref(p3);
+ originalNewsrcFileRelPref = Services.prefs.getCharPref(p4);
+ }
+ let originalStoreContractID = Services.prefs.getCharPref(p5);
+ let originalRootFolderPath = gServer.rootFolder.filePath.path;
+
+ /**
+ * Called when promise returned by convertMailStoreTo() is rejected.
+ *
+ * @param {string} aReason - error because of which the promise was rejected.
+ */
+ function promiseRejected(aReason) {
+ log.error("Conversion to '" + aSelectedStoreType + "' failed: " + aReason);
+ document.getElementById("messageSpan").hidden = true;
+
+ document.getElementById("errorSpan").hidden = false;
+ gResponse.newRootFolder = null;
+
+ // Revert prefs.
+ Services.prefs.setCharPref(p1, originalDirectoryPref);
+ Services.prefs.setCharPref(p2, originalDirectoryRelPref);
+ if (gServer.type == "nntp") {
+ Services.prefs.setCharPref(p3, originalNewsrcFilePref);
+ Services.prefs.setCharPref(p4, originalNewsrcFileRelPref);
+ }
+ Services.prefs.setCharPref(p5, originalStoreContractID);
+ Services.prefs.savePrefFile(null);
+ if (gServer.rootFolder.filePath.path != originalRootFolderPath) {
+ gServer.rootFolder.filePath = new FileUtils.File(originalRootFolderPath);
+ }
+ Services.io.offline = gOriginalOffline;
+ }
+
+ /**
+ * Called when promise returned by convertMailStoreTo() is resolved.
+ *
+ * @param {string} aVal - path of the new account root folder with which the
+ * promise returned by convertMailStoreTo() is resolved.
+ */
+ function promiseResolved(aVal) {
+ log.info("Converted to '" + aSelectedStoreType + "' - " + aVal);
+
+ gResponse.newRootFolder = aVal;
+ for (let deferredAccount of gDeferredAccounts) {
+ let defServer = deferredAccount.incomingServer;
+ defServer.rootMsgFolder.filePath = new FileUtils.File(aVal);
+ Services.prefs.setCharPref(
+ "mail.server." + defServer.key + ".storeContractID",
+ aSelectedStoreType
+ );
+ }
+
+ Services.io.offline = gOriginalOffline;
+ document.getElementById("cancel").hidden = true;
+ document.getElementById("finish").hidden = false;
+ document.getElementById("messageSpan").hidden = true;
+ document.getElementById("completeSpan").hidden = false;
+ }
+
+ /**
+ * Check whether an mbox folder can be compacted or not.
+ *
+ * @param {nsIMsgFolder} aFolder - mbox folder that is to be checked.
+ */
+ function canCompact(aFolder) {
+ if (aFolder.expungedBytes != 0) {
+ return true;
+ }
+ if (aFolder.hasSubFolders) {
+ for (let subFolder of aFolder.subFolders) {
+ if (canCompact(subFolder)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Compaction (compactAll()) works only for mbox folders which satisfy one of
+ // the following 2 conditions -
+ // 1. Messages are moved out of the folder.
+ // 2. Messages are moved out of some descendant folder of the folder.
+ // If the account root folder can be compacted, start the conversion after
+ // compacting it.
+ if (
+ originalStoreContractID == "@mozilla.org/msgstore/berkeleystore;1" &&
+ canCompact(gServer.rootFolder)
+ ) {
+ let urlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ let pConvert = MailstoreConverter.convertMailStoreTo(
+ originalStoreContractID,
+ gServer,
+ document.getElementById("progress")
+ );
+ pConvert
+ .then(function (val) {
+ promiseResolved(val);
+ })
+ .catch(function (reason) {
+ promiseRejected(reason);
+ });
+ },
+ };
+ gServer.rootFolder.compactAll(urlListener, null);
+ } else {
+ let pConvert = MailstoreConverter.convertMailStoreTo(
+ originalStoreContractID,
+ gServer,
+ document.getElementById("progress")
+ );
+ pConvert
+ .then(function (val) {
+ promiseResolved(val);
+ })
+ .catch(function (reason) {
+ promiseRejected(reason);
+ });
+ }
+}
+
+/**
+ * Cancel the conversion.
+ *
+ * @param {object} aResponse - response param from the migration dialog modal.
+ */
+function cancelConversion(aResponse) {
+ gResponse = aResponse;
+ gResponse.newRootFolder = null;
+ MailstoreConverter.terminateWorkers();
+ Services.io.offline = gOriginalOffline;
+ window.close();
+}
+
+/**
+ * Called when "finish" button is clicked.
+ */
+function finishConversion() {
+ window.close();
+}
diff --git a/comm/mailnews/base/prefs/content/converterDialog.xhtml b/comm/mailnews/base/prefs/content/converterDialog.xhtml
new file mode 100644
index 0000000000..40944fd556
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/converterDialog.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/.
+-->
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % converterDTD SYSTEM "chrome://messenger/locale/converterDialog.dtd">
+%converterDTD; ]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="UTF-8" />
+ <title>&converterDialog.title;</title>
+ <link
+ rel="stylesheet"
+ href="chrome://messenger/skin/shared/converterDialog.css"
+ />
+ <script src="chrome://messenger/content/converterDialog.js"></script>
+ </head>
+
+ <body onload="placeAccountName(window.arguments[0]);">
+ <div id="warningArea" class="convert-area">
+ <img class="infoIcon" alt="" />
+ <p id="warningSpan"></p>
+ <div class="controls">
+ <button onclick="cancelConversion(window.arguments[2]);">
+ &converterDialog.cancelButton;
+ </button>
+ <button
+ onclick="startContinue(window.arguments[1], window.arguments[2]);"
+ >
+ &converterDialog.continueButton;
+ </button>
+ </div>
+ </div>
+ <div id="progressArea" class="convert-area" hidden="hidden">
+ <img class="infoIcon" alt="" />
+ <p id="messageSpan"></p>
+ <p id="completeSpan" hidden="hidden">&converterDialog.complete;</p>
+ <p id="errorSpan" hidden="hidden">&converterDialog.error;</p>
+ <progress id="progress" value="0" max="100"></progress>
+ <span id="progressPercent"></span>
+ <div class="controls">
+ <button id="cancel" onclick="cancelConversion(window.arguments[1]);">
+ &converterDialog.cancelButton;
+ </button>
+ <button id="finish" onclick="finishConversion();" hidden="hidden">
+ &converterDialog.finishButton;
+ </button>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/removeAccount.js b/comm/mailnews/base/prefs/content/removeAccount.js
new file mode 100644
index 0000000000..724e45e139
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/removeAccount.js
@@ -0,0 +1,168 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gServer;
+var gDialog;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogdisclosure", showInfo);
+document.addEventListener("dialogaccept", onAccept);
+document.subDialogSetDefaultFocus = isInitialFocus => {
+ gDialog.getButton("cancel").focus();
+ delete document.subDialogSetDefaultFocus;
+};
+
+function onLoad(event) {
+ gServer = window.arguments[0].account.incomingServer;
+ gDialog = document.querySelector("dialog");
+
+ let bundle = document.getElementById("bundle_removeAccount");
+ let removeQuestion = bundle.getFormattedString("removeQuestion", [
+ gServer.prettyName,
+ ]);
+ document.getElementById("accountName").textContent = removeQuestion;
+
+ // Allow to remove account data if it has a local storage.
+ let localDirectory = gServer.localPath;
+ if (localDirectory && localDirectory.exists()) {
+ localDirectory.normalize();
+
+ // Do not allow removal if localPath is outside of profile folder.
+ let profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ profilePath.normalize();
+
+ // TODO: bug 77652, decide what to do for deferred accounts.
+ // And inform the user if the account localPath is outside the profile.
+ if (
+ gServer.isDeferredTo ||
+ (gServer instanceof Ci.nsIPop3IncomingServer &&
+ gServer.deferredToAccount) ||
+ !profilePath.contains(localDirectory)
+ ) {
+ document.getElementById("removeData").disabled = true;
+ }
+ } else {
+ document.getElementById("removeDataPossibility").collapsed = true;
+ }
+
+ if (gServer.type == "im") {
+ let dataCheckbox = document.getElementById("removeData");
+ dataCheckbox.label = dataCheckbox.getAttribute("labelChat");
+ dataCheckbox.accessKey = dataCheckbox.getAttribute("accesskeyChat");
+ }
+
+ enableRemove();
+}
+
+function enableRemove() {
+ gDialog.getButton("accept").disabled =
+ !document.getElementById("removeAccount").checked &&
+ !document.getElementById("removeData").checked;
+}
+
+/**
+ * Show the local directory.
+ */
+function openLocalDirectory() {
+ let nsLocalFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+ );
+ let localDir = gServer.localPath.path;
+ try {
+ new nsLocalFile(localDir).reveal();
+ } catch (e) {
+ // Reveal may fail e.g. on Linux, then just show the path as a string.
+ document.getElementById("localDirectory").value = localDir;
+ document.getElementById("localDirectory").collapsed = false;
+ }
+}
+
+function showInfo() {
+ let descs = document.querySelectorAll("vbox.indent");
+ for (let desc of descs) {
+ desc.collapsed = false;
+ }
+
+ // TODO: bug 1238271, this should use showFor attributes if possible.
+ if (gServer.type == "imap" || gServer.type == "nntp") {
+ document.getElementById("serverAccount").collapsed = false;
+ } else if (gServer.type == "im") {
+ document.getElementById("chatAccount").collapsed = false;
+ } else {
+ document.getElementById("localAccount").collapsed = false;
+ }
+
+ parent.gSubDialog._topDialog.resizeDialog();
+ gDialog.getButton("disclosure").disabled = true;
+ gDialog.getButton("disclosure").blur();
+}
+
+function removeAccount() {
+ let removeAccount = document.getElementById("removeAccount").checked;
+ let removeData = document.getElementById("removeData").checked;
+ let account = window.arguments[0].account;
+ try {
+ // Remove the requested account data.
+ if (removeAccount) {
+ try {
+ // Remove password information first.
+ account.incomingServer.forgetPassword();
+ } catch (e) {
+ /* It is OK if this fails. */
+ }
+ // Remove account
+ MailServices.accounts.removeAccount(account, removeData);
+ account = null;
+ delete window.arguments[0].account;
+ gServer = null;
+ window.arguments[0].result = true;
+ } else if (removeData) {
+ // Remove files only.
+ // TODO: bug 1302193
+ window.arguments[0].result = false;
+ }
+
+ document.getElementById("success").hidden = false;
+ } catch (ex) {
+ document.getElementById("failure").hidden = false;
+ console.error("Failure to remove account: " + ex);
+ window.arguments[0].result = false;
+ }
+ document.getElementById("progress").hidden = true;
+}
+
+function onAccept(event) {
+ // If Cancel is disabled, we already tried to remove the account
+ // and can only close the dialog.
+ if (gDialog.getButton("cancel").disabled) {
+ return;
+ }
+
+ gDialog.getButton("accept").disabled = true;
+ gDialog.getButton("cancel").disabled = true;
+ gDialog.getButton("disclosure").disabled = true;
+
+ // Change the "Remove" to an "OK" button by clearing the custom label.
+ gDialog.removeAttribute("buttonlabelaccept");
+ gDialog.removeAttribute("buttonaccesskeyaccept");
+ gDialog.getButton("accept").removeAttribute("label");
+ gDialog.getButton("accept").removeAttribute("accesskey");
+ gDialog.buttons = "accept";
+
+ document.getElementById("removeAccountSection").hidden = true;
+ document.getElementById("confirmationSection").hidden = false;
+ window.sizeToContent();
+
+ removeAccount();
+
+ gDialog.getButton("accept").disabled = false;
+ event.preventDefault();
+}
diff --git a/comm/mailnews/base/prefs/content/removeAccount.xhtml b/comm/mailnews/base/prefs/content/removeAccount.xhtml
new file mode 100644
index 0000000000..510b3fef84
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/removeAccount.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/checkbox.css" type="text/css"?>
+
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % removalDTD SYSTEM "chrome://messenger/locale/removeAccount.dtd">
+%removalDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="600"
+ scrolling="false">
+<head>
+ <title>&dialogTitle;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/removeAccount.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="removeAccountDialog"
+ buttons="accept,disclosure,cancel"
+ buttonlabelaccept="&removeButton.label;"
+ buttonaccesskeyaccept="&removeButton.accesskey;"
+ defaultButton="cancel"
+ style="width:100vw; height:100vh;">
+ <stringbundle id="bundle_removeAccount"
+ src="chrome://messenger/locale/removeAccount.properties"/>
+ <vbox id="removeAccountSection">
+ <label id="accountName"></label>
+ <separator class="thin"/>
+ <checkbox id="removeAccount"
+ label="&removeAccount.label;"
+ checked="true"
+ disabled="true"
+ accesskey="&removeAccount.accesskey;"
+ oncommand="enableRemove();"/>
+ <vbox class="indent" collapsed="true">
+ <description>
+ &removeAccount.desc;
+ </description>
+ </vbox>
+ <vbox id="removeDataPossibility" collapsed="false">
+ <checkbox id="removeData"
+ label="&removeData.label;"
+ labelChat="&removeDataChat.label;"
+ accesskey="&removeData.accesskey;"
+ accesskeyChat="&removeDataChat.accesskey;"
+ oncommand="enableRemove();"/>
+ <vbox id="removeAccountBox" class="indent" collapsed="true">
+ <description id="localAccount" collapsed="true">
+ &removeDataLocalAccount.desc;
+ </description>
+ <description id="serverAccount" collapsed="true">
+ &removeDataServerAccount.desc;
+ </description>
+ <description id="chatAccount" collapsed="true">
+ &removeDataChatAccount.desc;
+ </description>
+ <hbox align="center">
+ <button id="showLocalDirectory"
+ label="&showData.label;"
+ accesskey="&showData.accesskey;"
+ oncommand="openLocalDirectory();"/>
+ <label id="localDirectory" collapsed="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="confirmationSection" align="center" pack="center" flex="1" hidden="true">
+ <vbox id="progress" align="center">
+ <label>&progressPending;</label>
+ <html:progress max="100"/>
+ </vbox>
+ <label id="success" hidden="true">&progressSuccess;</label>
+ <label id="failure" hidden="true">&progressFailure;</label>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/public/MailNewsTypes.h b/comm/mailnews/base/public/MailNewsTypes.h
new file mode 100644
index 0000000000..e6f58cb56b
--- /dev/null
+++ b/comm/mailnews/base/public/MailNewsTypes.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef MailNewsTypes_h__
+#define MailNewsTypes_h__
+
+#include "msgCore.h"
+#include "MailNewsTypes2.h"
+
+/* nsMsgKey is a unique ID for a particular message in a folder. If you want
+ a handle to a message that will remain valid even after resorting the folder
+ or otherwise changing their indices, you want one of these rather than a
+ nsMsgViewIndex. nsMsgKeys don't survive local mail folder compression,
+ however.
+ */
+const nsMsgKey nsMsgKey_None = 0xffffffff;
+
+/* nsMsgViewIndex
+ *
+ * A generic index type from which other index types are derived. All
+ * nsMsgViewIndex derived types are zero based.
+ *
+ * The following index types are currently supported:
+ * - nsMsgViewIndex - an index into the list of messages or folders or groups,
+ * where zero is the first one to show, one is the second, etc...
+ * - AB_SelectionIndex
+ * - AB_NameCompletionIndex
+ */
+
+const nsMsgViewIndex nsMsgViewIndex_None = 0xFFFFFFFF;
+
+/* kSizeUnknown is a special value of folder size that indicates the size
+ * is unknown yet. Usually this causes the folder to determine the real size
+ * immediately as it is queried by a consumer.
+ */
+const int64_t kSizeUnknown = -1;
+
+#endif
diff --git a/comm/mailnews/base/public/MailNewsTypes2.idl b/comm/mailnews/base/public/MailNewsTypes2.idl
new file mode 100644
index 0000000000..aeb8df2ec6
--- /dev/null
+++ b/comm/mailnews/base/public/MailNewsTypes2.idl
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+typedef unsigned long nsMsgKey;
+typedef unsigned long nsMsgViewIndex;
+
+typedef long nsMsgSearchScopeValue;
+
+typedef long nsMsgPriorityValue;
+typedef long nsMsgSocketTypeValue;
+typedef long nsMsgAuthMethodValue;
+
+typedef unsigned long nsMsgJunkStatus;
+
+typedef unsigned long nsMsgJunkScore;
+
+[scriptable, uuid(94C0D8D8-2045-11d3-8A8F-0060B0FC04D2)]
+interface nsMsgPriority : nsISupports {
+ const nsMsgPriorityValue notSet = 0;
+ const nsMsgPriorityValue none = 1;
+ const nsMsgPriorityValue lowest = 2;
+ const nsMsgPriorityValue low = 3;
+ const nsMsgPriorityValue normal = 4;
+ const nsMsgPriorityValue high = 5;
+ const nsMsgPriorityValue highest = 6;
+ //the default priority (if none) is set in the message
+ const nsMsgPriorityValue Default = 4;
+};
+
+/**
+ * Defines whether to use SSL or STARTTLS or not.
+ * Used by @see nsIMsgIncomingServer.socketType
+ * and @see nsISmtpServer.socketType
+ */
+[scriptable, uuid(bc78bc74-1b34-48e8-ac2b-968e8dff1aeb)]
+interface nsMsgSocketType : nsISupports {
+ /// No SSL or STARTTLS
+ const nsMsgSocketTypeValue plain = 0;
+ /// Use TLS via STARTTLS, but only if server offers it.
+ /// @deprecated This is vulnerable to MITM attacks
+ const nsMsgSocketTypeValue trySTARTTLS = 1;
+ /// Insist on TLS via STARTTLS.
+ /// Uses normal port.
+ const nsMsgSocketTypeValue alwaysSTARTTLS = 2;
+ /// Connect via SSL.
+ /// Needs special SSL port.
+ const nsMsgSocketTypeValue SSL = 3;
+};
+
+/**
+ * Defines which authentication schemes we should try.
+ * Used by @see nsIMsgIncomingServer.authMethod
+ * and @see nsISmtpServer.authMethod
+ */
+[scriptable, uuid(4a10e647-d179-4a53-b7ef-df575ff5f405)]
+interface nsMsgAuthMethod : nsISupports {
+ // 0 is intentionally undefined and invalid
+ /// No login needed. E.g. IP-address-based.
+ const nsMsgAuthMethodValue none = 1;
+ /// Do not use AUTH commands (e.g. AUTH=PLAIN),
+ /// but the original login commands that the protocol specified
+ /// (POP: "USER"/"PASS", IMAP: "login", not valid for SMTP)
+ const nsMsgAuthMethodValue old = 2;
+ /// password in the clear. AUTH=PLAIN/LOGIN or old-style login.
+ const nsMsgAuthMethodValue passwordCleartext = 3;
+ /// hashed password. CRAM-MD5, DIGEST-MD5
+ const nsMsgAuthMethodValue passwordEncrypted = 4;
+ /// Kerberos / GSSAPI (Unix single-signon)
+ const nsMsgAuthMethodValue GSSAPI = 5;
+ /// NTLM is a Windows single-singon scheme.
+ /// Includes MSN / Passport.net, which is the same with a different name.
+ const nsMsgAuthMethodValue NTLM = 6;
+ /// Auth External is cert-based authentication
+ const nsMsgAuthMethodValue External = 7;
+ /// Encrypted password or Kerberos / GSSAPI or NTLM.
+ /// @deprecated - for migration only.
+ const nsMsgAuthMethodValue secure = 8;
+ /// Let us pick any of the auth types supported by the server.
+ /// Discouraged, because vulnerable to MITM attacks, even if server offers secure auth.
+ const nsMsgAuthMethodValue anything = 9;
+
+ /// Use OAuth2 to authenticate.
+ const nsMsgAuthMethodValue OAuth2 = 10;
+};
+
+typedef long nsMsgViewSortOrderValue;
+typedef long nsMsgViewSortTypeValue;
+typedef long nsMsgViewTypeValue;
+typedef long nsMsgViewFlagsTypeValue;
diff --git a/comm/mailnews/base/public/moz.build b/comm/mailnews/base/public/moz.build
new file mode 100644
index 0000000000..be6deb7da9
--- /dev/null
+++ b/comm/mailnews/base/public/moz.build
@@ -0,0 +1,80 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "MailNewsTypes2.idl",
+ "mozINewMailListener.idl",
+ "mozINewMailNotificationService.idl",
+ "msgIOAuth2Module.idl",
+ "nsICopyMessageListener.idl",
+ "nsICopyMessageStreamListener.idl",
+ "nsIFolderListener.idl",
+ "nsIFolderLookupService.idl",
+ "nsIIncomingServerListener.idl",
+ "nsIMailAuthModule.idl",
+ "nsIMailChannel.idl",
+ "nsIMapiRegistry.idl",
+ "nsIMessenger.idl",
+ "nsIMessengerMigrator.idl",
+ "nsIMessengerOSIntegration.idl",
+ "nsIMessengerWindowService.idl",
+ "nsIMsgAccount.idl",
+ "nsIMsgAccountManager.idl",
+ "nsIMsgAsyncPrompter.idl",
+ "nsIMsgBiffManager.idl",
+ "nsIMsgContentPolicy.idl",
+ "nsIMsgCopyService.idl",
+ "nsIMsgCopyServiceListener.idl",
+ "nsIMsgCustomColumnHandler.idl",
+ "nsIMsgDBView.idl",
+ "nsIMsgEnumerator.idl",
+ "nsIMsgFolder.idl",
+ "nsIMsgFolderCache.idl",
+ "nsIMsgFolderCacheElement.idl",
+ "nsIMsgFolderCompactor.idl",
+ "nsIMsgFolderListener.idl",
+ "nsIMsgFolderNotificationService.idl",
+ "nsIMsgHdr.idl",
+ "nsIMsgIdentity.idl",
+ "nsIMsgIncomingServer.idl",
+ "nsIMsgMailNewsUrl.idl",
+ "nsIMsgMailSession.idl",
+ "nsIMsgMdnGenerator.idl",
+ "nsIMsgMessageService.idl",
+ "nsIMsgOfflineManager.idl",
+ "nsIMsgPluggableStore.idl",
+ "nsIMsgProgress.idl",
+ "nsIMsgProtocolHandler.idl",
+ "nsIMsgProtocolInfo.idl",
+ "nsIMsgPurgeService.idl",
+ "nsIMsgShutdown.idl",
+ "nsIMsgStatusFeedback.idl",
+ "nsIMsgTagService.idl",
+ "nsIMsgThread.idl",
+ "nsIMsgUserFeedbackListener.idl",
+ "nsIMsgWindow.idl",
+ "nsISpamSettings.idl",
+ "nsIStatusBarBiffManager.idl",
+ "nsIStopwatch.idl",
+ "nsISubscribableServer.idl",
+ "nsIUrlListener.idl",
+ "nsIUserInfo.idl",
+ "nsMsgFolderFlags.idl",
+ "nsMsgMessageFlags.idl",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ XPIDL_SOURCES += [
+ "nsIMessengerWindowsIntegration.idl",
+ ]
+
+XPIDL_MODULE = "msgbase"
+
+EXPORTS += [
+ "MailNewsTypes.h",
+ "msgCore.h",
+ "nsMsgHeaderMasks.h",
+ "nsMsgLocalFolderHdrs.h",
+]
diff --git a/comm/mailnews/base/public/mozINewMailListener.idl b/comm/mailnews/base/public/mozINewMailListener.idl
new file mode 100644
index 0000000000..467acb671c
--- /dev/null
+++ b/comm/mailnews/base/public/mozINewMailListener.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(e15f104f-a16d-4e51-a362-4b4c5efe05b9)]
+/**
+ * Callback interface for objects interested in receiving new mail notifications
+ * from mozINewMailNotificationService
+ * NOTE: THIS INTERFACE IS UNDER ACTIVE DEVELOPMENT AND SUBJECT TO CHANGE,
+ * see https://bugzilla.mozilla.org/show_bug.cgi?id=715799
+ */
+interface mozINewMailListener : nsISupports {
+ /** The new mail notification service will call this when the number of interesting
+ * messages has changed
+ *
+ * @param unreadCount The number of unread messages the user cares to be notified about
+ */
+ void onCountChanged(in unsigned long count);
+};
diff --git a/comm/mailnews/base/public/mozINewMailNotificationService.idl b/comm/mailnews/base/public/mozINewMailNotificationService.idl
new file mode 100644
index 0000000000..cdd049bcca
--- /dev/null
+++ b/comm/mailnews/base/public/mozINewMailNotificationService.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface mozINewMailListener;
+
+typedef long newMailListenerFlag;
+
+[scriptable, uuid(7fef9018-c9f1-4cbd-b57c-d6555cf3a668)]
+/** New mail notification service. This service watches all the relevant
+ * folder and message change events, preferences etc. and keeps track of
+ * the specific messages the user wants notifications for.
+ * NOTE: THIS INTERFACE IS UNDER ACTIVE DEVELOPMENT AND SUBJECT TO CHANGE,
+ * see https://bugzilla.mozilla.org/show_bug.cgi?id=715799
+ * Registered mozINewMailListeners are called when the message count or
+ * specific list of notified messages changes.
+ * ** Should also document the observer service callback that allows
+ * plugins to override notifications by folder
+ */
+interface mozINewMailNotificationService : nsISupports {
+ /**
+ * @name Notification flags
+ * These flags determine which notifications will be sent.
+ * @{
+ */
+ /// mozINewMailListener::count notification
+ const newMailListenerFlag count = 0x1;
+
+ /// mozINewMailListener::messages notification
+ const newMailListenerFlag messages = 0x2;
+
+ /** @} */
+
+ /**
+ * addListener - Register a mozINewMailListener to receive callbacks
+ * when the count or list of notification-worthy messages
+ * changes.
+ * @param aListener mozINewMailListener to call back
+ * @param flags Bitmask of newMailListenerFlag values indicating
+ * the particular callbacks this listener wants.
+ * If the listener is already registered with the
+ * notification service, the existing set of flags is
+ * replaced by the values passed in this parameter.
+ */
+ void addListener(in mozINewMailListener aListener,
+ in newMailListenerFlag flags);
+ /**
+ * removeListener - remove a listener from the service
+ * @param aListener The listener to remove
+ */
+ void removeListener(in mozINewMailListener aListener);
+
+ /// The current count of notification-worth unread messages
+ readonly attribute long messageCount;
+};
diff --git a/comm/mailnews/base/public/msgCore.h b/comm/mailnews/base/public/msgCore.h
new file mode 100644
index 0000000000..679e510887
--- /dev/null
+++ b/comm/mailnews/base/public/msgCore.h
@@ -0,0 +1,210 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* Include files we are going to want available to all files....these files
+ include NSPR, memory, and string header files among others */
+
+#ifndef msgCore_h__
+#define msgCore_h__
+
+#include "nscore.h"
+#include "nspr.h"
+#include "plstr.h"
+#include "nsCRTGlue.h"
+
+class nsIMsgDBHdr;
+class nsIMsgFolder;
+
+// include common interfaces such as the service manager and the repository....
+#include "nsIServiceManager.h"
+#include "nsIComponentManager.h"
+
+/*
+ * The suffix we use for the mail summary file.
+ */
+#define SUMMARY_SUFFIX u".msf"
+#define SUMMARY_SUFFIX8 ".msf"
+#define SUMMARY_SUFFIX_LENGTH 4
+
+/*
+ * The suffix we use for folder subdirectories.
+ */
+#define FOLDER_SUFFIX u".sbd"
+#define FOLDER_SUFFIX8 ".sbd"
+#define FOLDER_SUFFIX_LENGTH 4
+
+/*
+ * These are folder property strings, which are used in several places.
+
+ */
+// Most recently used (opened, moved to, got new messages)
+#define MRU_TIME_PROPERTY "MRUTime"
+// Most recently moved to, for recent folders list in move menu
+#define MRM_TIME_PROPERTY "MRMTime"
+
+/* NS_ERROR_MODULE_MAILNEWS is defined in mozilla/xpcom/public/nsError.h */
+
+/*
+ * NS_ERROR macros - use these macros to generate error constants
+ * to be used by XPCOM interfaces and possibly other useful things
+ * do not use these macros in your code - declare error macros for
+ * each specific error you need.
+ *
+ * for example:
+ * #define NS_MSG_ERROR_NO_SUCH_FOLDER NS_MSG_GENERATE_FAILURE(4)
+ *
+ */
+
+/* use these routines to generate error values */
+#define NS_MSG_GENERATE_RESULT(severity, value) \
+ NS_ERROR_GENERATE(severity, NS_ERROR_MODULE_MAILNEWS, value)
+
+#define NS_MSG_GENERATE_SUCCESS(value) \
+ NS_ERROR_GENERATE_SUCCESS(NS_ERROR_MODULE_MAILNEWS, value)
+
+#define NS_MSG_GENERATE_FAILURE(value) \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_MAILNEWS, value)
+
+/* these are shortcuts to generate simple errors with a zero value */
+#define NS_MSG_SUCCESS NS_MSG_GENERATE_SUCCESS(0)
+#define NS_MSG_FAILURE NS_MSG_GENERATE_FAILURE(0)
+
+#define IS_SPACE(VAL) \
+ (((((PRIntn)(VAL)) & 0x7f) == ((PRIntn)(VAL))) && isspace((PRIntn)(VAL)))
+
+#define IS_DIGIT(i) ((((unsigned int)(i)) > 0x7f) ? (int)0 : isdigit(i))
+#if defined(XP_WIN)
+# define IS_ALPHA(VAL) (isascii((int)(VAL)) && isalpha((int)(VAL)))
+#else
+# define IS_ALPHA(VAL) \
+ ((((unsigned int)(VAL)) > 0x7f) ? (int)0 : isalpha((int)(VAL)))
+#endif
+
+/* for retrieving information out of messenger nsresults */
+
+#define NS_IS_MSG_ERROR(err) \
+ (NS_ERROR_GET_MODULE(err) == NS_ERROR_MODULE_MAILNEWS)
+
+#define NS_MSG_SUCCEEDED(err) (NS_IS_MSG_ERROR(err) && NS_SUCCEEDED(err))
+
+#define NS_MSG_FAILED(err) (NS_IS_MSG_ERROR(err) && NS_FAILED(err))
+
+#define NS_MSG_PASSWORD_PROMPT_CANCELLED NS_MSG_GENERATE_SUCCESS(1)
+
+/**
+ * Indicates that a search is done/terminated because it was interrupted.
+ * Interrupting a search originally notified listeners with
+ * OnSearchDone(NS_OK), so we define a success value to continue doing this,
+ * and because the search was fine except for an explicit call to interrupt it.
+ */
+#define NS_MSG_SEARCH_INTERRUPTED NS_MSG_GENERATE_SUCCESS(2)
+
+/* This is where we define our errors. There has to be a central
+ place so we don't use the same error codes for different errors.
+*/
+#define NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE NS_MSG_GENERATE_FAILURE(5)
+#define NS_MSG_ERROR_FOLDER_SUMMARY_MISSING NS_MSG_GENERATE_FAILURE(6)
+#define NS_MSG_ERROR_FOLDER_MISSING NS_MSG_GENERATE_FAILURE(7)
+
+#define NS_MSG_MESSAGE_NOT_FOUND NS_MSG_GENERATE_FAILURE(8)
+#define NS_MSG_NOT_A_MAIL_FOLDER NS_MSG_GENERATE_FAILURE(9)
+
+#define NS_MSG_FOLDER_BUSY NS_MSG_GENERATE_FAILURE(10)
+
+#define NS_MSG_COULD_NOT_CREATE_DIRECTORY NS_MSG_GENERATE_FAILURE(11)
+#define NS_MSG_CANT_CREATE_FOLDER NS_MSG_GENERATE_FAILURE(12)
+
+#define NS_MSG_FILTER_PARSE_ERROR NS_MSG_GENERATE_FAILURE(13)
+
+#define NS_MSG_FOLDER_UNREADABLE NS_MSG_GENERATE_FAILURE(14)
+
+#define NS_MSG_ERROR_WRITING_MAIL_FOLDER NS_MSG_GENERATE_FAILURE(15)
+
+#define NS_MSG_ERROR_NO_SEARCH_VALUES NS_MSG_GENERATE_FAILURE(16)
+
+#define NS_MSG_ERROR_INVALID_SEARCH_SCOPE NS_MSG_GENERATE_FAILURE(17)
+
+#define NS_MSG_ERROR_INVALID_SEARCH_TERM NS_MSG_GENERATE_FAILURE(18)
+
+#define NS_MSG_FOLDER_EXISTS NS_MSG_GENERATE_FAILURE(19)
+
+#define NS_MSG_ERROR_OFFLINE NS_MSG_GENERATE_FAILURE(20)
+
+#define NS_MSG_POP_FILTER_TARGET_ERROR NS_MSG_GENERATE_FAILURE(21)
+
+#define NS_MSG_INVALID_OR_MISSING_SERVER NS_MSG_GENERATE_FAILURE(22)
+
+#define NS_MSG_SERVER_USERNAME_MISSING NS_MSG_GENERATE_FAILURE(23)
+
+#define NS_MSG_INVALID_DBVIEW_INDEX NS_MSG_GENERATE_FAILURE(24)
+
+#define NS_MSG_NEWS_ARTICLE_NOT_FOUND NS_MSG_GENERATE_FAILURE(25)
+
+#define NS_MSG_ERROR_COPY_FOLDER_ABORTED NS_MSG_GENERATE_FAILURE(26)
+// this error means a url was queued but never run because one of the urls
+// it was queued after failed. We send an OnStopRunningUrl with this error code
+// so the listeners can know that we didn't run the url.
+#define NS_MSG_ERROR_URL_ABORTED NS_MSG_GENERATE_FAILURE(27)
+
+// when num of custom headers exceeds 50
+#define NS_MSG_CUSTOM_HEADERS_OVERFLOW NS_MSG_GENERATE_FAILURE(28)
+
+// when custom header has invalid characters (as per rfc 2822)
+#define NS_MSG_INVALID_CUSTOM_HEADER NS_MSG_GENERATE_FAILURE(29)
+
+// when local caches are password protect and user isn't auth
+#define NS_MSG_USER_NOT_AUTHENTICATED NS_MSG_GENERATE_FAILURE(30)
+
+#define NS_MSG_ERROR_COPYING_FROM_TMP_DOWNLOAD \
+ NS_MSG_GENERATE_FAILURE(31) // pop3 downloaded to tmp file, and failed.
+
+// The code tried to stream a message using the aLocalOnly argument, but
+// the message was not cached locally.
+#define NS_MSG_ERROR_MSG_NOT_OFFLINE NS_MSG_GENERATE_FAILURE(32)
+
+// The imap server returned NO or BAD for an IMAP command
+#define NS_MSG_ERROR_IMAP_COMMAND_FAILED NS_MSG_GENERATE_FAILURE(33)
+
+#define NS_MSG_ERROR_INVALID_FOLDER_NAME NS_MSG_GENERATE_FAILURE(34)
+
+/* Error codes for message compose are defined in
+ compose\src\nsMsgComposeStringBundle.h. Message compose use the same error
+ code space as other mailnews modules. To avoid any conflict, values between
+ 12500 and 12999 are reserved.
+*/
+#define NS_MSGCOMP_ERROR_BEGIN 12500
+/* NS_ERROR_NNTP_NO_CROSS_POSTING lives here, and not in
+ * nsMsgComposeStringBundle.h, because it is used in news and compose. */
+#define NS_ERROR_NNTP_NO_CROSS_POSTING NS_MSG_GENERATE_FAILURE(12554)
+#define NS_MSGCOMP_ERROR_END 12999
+
+#if defined(XP_WIN)
+# define MSG_LINEBREAK "\015\012"
+# define MSG_LINEBREAK_LEN 2
+#else
+# define MSG_LINEBREAK "\012"
+# define MSG_LINEBREAK_LEN 1
+#endif
+
+/*
+ * On Windows, we use \r\n as the line terminator in mbox files. On
+ * other platforms, we use \n. However, we need to be able to
+ * recognize line terminators produced on any platform, because we
+ * allow profiles (including the mbox files they contain) to be shared
+ * between platforms.
+ *
+ * Returns 0 (i.e., false) if the line is not blank, or otherwise the
+ * length of the line terminator, i.e., 1 for \n or 2 for \r\n.
+ */
+#define IS_MSG_LINEBREAK(line) \
+ (line[0] == '\012' ? 1 : ((line[0] == '\015' && line[1] == '\012') ? 2 : 0))
+
+#define NS_MSG_BASE
+#define NS_MSG_BASE_STATIC_MEMBER_(type) type
+
+/// The number of microseconds in a day. This comes up a lot.
+#define PR_USEC_PER_DAY (PRTime(PR_USEC_PER_SEC) * 60 * 60 * 24)
+
+#endif // msgCore_h__
diff --git a/comm/mailnews/base/public/msgIOAuth2Module.idl b/comm/mailnews/base/public/msgIOAuth2Module.idl
new file mode 100644
index 0000000000..e3d1f8aa15
--- /dev/null
+++ b/comm/mailnews/base/public/msgIOAuth2Module.idl
@@ -0,0 +1,56 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgIncomingServer;
+interface nsISmtpServer;
+
+/**
+ * A listener callback for OAuth2 SASL authentication. This would be represented
+ * as a promise, but this needs to be consumed by C++ code.
+ */
+[scriptable, uuid(9a088b49-bc13-4f99-9478-053a6a43e370)]
+interface msgIOAuth2ModuleListener : nsISupports {
+ /**
+ * Called on successful OAuth2 authentication with the base64-encoded
+ * string to send as the client initial response for SASL XOAUTH2.
+ */
+ void onSuccess(in ACString aBearerToken);
+
+ /// Called on failed OAuth2 authentication.
+ void onFailure(in nsresult aError);
+};
+
+/**
+ * An interface for managing the responsibilities of using OAuth2 to produce a
+ * bearer token, for use in SASL steps.
+ */
+[scriptable, uuid(68c275f8-cfa7-4622-b279-af290616cae6)]
+interface msgIOAuth2Module : nsISupports {
+ /**
+ * Initialize the OAuth2 parameters from an SMTP server, and return whether or
+ * not we can authenticate with OAuth2.
+ */
+ bool initFromSmtp(in nsISmtpServer aSmtpServer);
+
+ /**
+ * Initialize the OAuth2 parameters from an incoming server, and return
+ * whether or not we can authenticate with OAuth2.
+ */
+ bool initFromMail(in nsIMsgIncomingServer aServer);
+
+ /**
+ * Connect to the OAuth2 server to get an access token.
+ * @param aWithUI If false, do not allow a dialog to be popped up to query
+ * for a password.
+ * @param aCallback Listener that handles the async response.
+ */
+ void connect(in boolean aWithUI, in msgIOAuth2ModuleListener aCallback);
+};
+
+%{C++
+#define MSGIOAUTH2MODULE_CONTRACTID "@mozilla.org/mail/oauth2-module;1"
+%}
diff --git a/comm/mailnews/base/public/nsICopyMessageListener.idl b/comm/mailnews/base/public/nsICopyMessageListener.idl
new file mode 100644
index 0000000000..4c13dab280
--- /dev/null
+++ b/comm/mailnews/base/public/nsICopyMessageListener.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIInputStream;
+
+[scriptable, uuid(53CA78FE-E231-11d2-8A4D-0060B0FC04D2)]
+
+/* Use this for any object that wants to handle copying/moving messages to it */
+
+interface nsICopyMessageListener : nsISupports
+{
+ void beginCopy();
+ void startMessage();
+ void copyData(in nsIInputStream aIStream, in long aLength);
+ void endMessage(in nsMsgKey key);
+ void endCopy(in boolean copySucceeded);
+ void endMove(in boolean moveSucceeded);
+};
diff --git a/comm/mailnews/base/public/nsICopyMessageStreamListener.idl b/comm/mailnews/base/public/nsICopyMessageStreamListener.idl
new file mode 100644
index 0000000000..772a69080b
--- /dev/null
+++ b/comm/mailnews/base/public/nsICopyMessageStreamListener.idl
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsICopyMessageListener;
+interface nsIURI;
+
+[scriptable, uuid(7741DAEC-2125-11d3-8A90-0060B0FC04D2)]
+
+interface nsICopyMessageStreamListener: nsISupports
+{
+ void init(in nsICopyMessageListener destination);
+ void startMessage();
+ void endMessage(in nsMsgKey key);
+ void endCopy(in nsIURI uri, in nsresult status);
+};
diff --git a/comm/mailnews/base/public/nsIFolderListener.idl b/comm/mailnews/base/public/nsIFolderListener.idl
new file mode 100644
index 0000000000..3906ef2f46
--- /dev/null
+++ b/comm/mailnews/base/public/nsIFolderListener.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+
+typedef unsigned long folderListenerNotifyFlagValue;
+
+/**
+ * nsIFolderListener defines callbacks to handle various notifications
+ * about changes in folders.
+ * These listeners can be attached to individual folders, or they
+ * can be registered globally, with nsIMsgMailSession.
+ * These notifications originate from nsIMsgFolder implementations.
+ * (nsIMsgFolder has corresponding methods for generating these
+ * notifications).
+ */
+[scriptable, uuid(f60ee1a2-6d81-422c-958f-d408b1b2daa7)]
+interface nsIFolderListener : nsISupports {
+ // "added" flag covers adding both messages and child folders.
+ const folderListenerNotifyFlagValue added = 0x1;
+ void onFolderAdded(in nsIMsgFolder parent, in nsIMsgFolder child);
+ void onMessageAdded(in nsIMsgFolder parent, in nsIMsgDBHdr msg);
+
+ // "removed" flag covers removing both messages and child folders.
+ const folderListenerNotifyFlagValue removed = 0x2;
+ void onFolderRemoved(in nsIMsgFolder parent, in nsIMsgFolder child);
+ void onMessageRemoved(in nsIMsgFolder parent, in nsIMsgDBHdr msg);
+
+ const folderListenerNotifyFlagValue propertyChanged = 0x4;
+ void onFolderPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in AUTF8String oldValue,
+ in AUTF8String newValue);
+
+ const folderListenerNotifyFlagValue intPropertyChanged = 0x8;
+ // While this property handles long long (64bit wide) values,
+ // the Javascript engine will only pass values up to 2^53 to the consumers.
+ void onFolderIntPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in long long oldValue,
+ in long long newValue);
+
+ const folderListenerNotifyFlagValue boolPropertyChanged = 0x10;
+ void onFolderBoolPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in boolean oldValue,
+ in boolean newValue);
+
+ const folderListenerNotifyFlagValue unicharPropertyChanged = 0x20;
+ void onFolderUnicharPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in AString oldValue,
+ in AString newValue);
+
+ const folderListenerNotifyFlagValue propertyFlagChanged = 0x40;
+ void onFolderPropertyFlagChanged(in nsIMsgDBHdr msg,
+ in ACString property,
+ in unsigned long oldFlag,
+ in unsigned long newFlag);
+
+ const folderListenerNotifyFlagValue event = 0x80;
+ void onFolderEvent(in nsIMsgFolder folder, in ACString event);
+
+ const folderListenerNotifyFlagValue all = 0xFFFFFFFF;
+};
diff --git a/comm/mailnews/base/public/nsIFolderLookupService.idl b/comm/mailnews/base/public/nsIFolderLookupService.idl
new file mode 100644
index 0000000000..fece4102ad
--- /dev/null
+++ b/comm/mailnews/base/public/nsIFolderLookupService.idl
@@ -0,0 +1,52 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+
+/**
+ * This service provides a way to lookup any nsIMsgFolder.
+ *
+ * When looking up folders by URL, note that the URL must be encoded to be a
+ * valid folder URL. Of particular note are the following requirements:
+ * - invalid characters in paths must be percent-encoded
+ * - the URL MUST NOT have a trailing slash (excepting root folders)
+ * - the case must match the expected value exactly
+ * An example of a valid URL is thus:
+ * imap://someuser%40google.com@imap.google.com/INBOX
+ *
+ * The contractid for this service is "@mozilla.org/mail/folder-lookup;1".
+ */
+[scriptable,uuid(f5ed5997-3945-48fc-a59d-d2191a94bb60)]
+interface nsIFolderLookupService : nsISupports
+{
+ /**
+ * Returns a folder with the given URL or null if no such folder exists.
+ *
+ * @param aUrl The folder URL
+ */
+ nsIMsgFolder getFolderForURL(in AUTF8String aUrl);
+
+ /**
+ * Returns a folder with the given URL.
+ * Will happily create and return an invalid (unparented) folder.
+ * Will return null if aUrl is not a folder url.
+ * NOTE: don't use this for new code! It's here purely to help
+ * transition away from RDF-based folder creation.
+ *
+ * @param aUrl The folder URL
+ */
+ nsIMsgFolder getOrCreateFolderForURL(in AUTF8String aUrl);
+
+ /**
+ * Set pretty name again from original name on all folders,
+ * typically used when locale changes.
+ */
+ void setPrettyNameFromOriginalAllFolders();
+};
+
+%{C++
+#define NSIFLS_CONTRACTID "@mozilla.org/mail/folder-lookup;1"
+%}
diff --git a/comm/mailnews/base/public/nsIIncomingServerListener.idl b/comm/mailnews/base/public/nsIIncomingServerListener.idl
new file mode 100644
index 0000000000..f2dd7faca5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIIncomingServerListener.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+
+[scriptable, uuid(E6B64B86-90CB-11d3-8B02-0060B0FC04D2)]
+interface nsIIncomingServerListener : nsISupports {
+ /**
+ * Notification sent when a server is first loaded into the account manager.
+ *
+ * @param server Loaded server.
+ */
+ void onServerLoaded(in nsIMsgIncomingServer server);
+
+ /**
+ * Notification sent when a server is unloaded from the account manager.
+ *
+ * @param server Unloaded server.
+ */
+ void onServerUnloaded(in nsIMsgIncomingServer server);
+
+ /**
+ * Notification sent when a server hostname or username changes.
+ *
+ * @param server Server that was changed.
+ */
+ void onServerChanged(in nsIMsgIncomingServer server);
+};
diff --git a/comm/mailnews/base/public/nsIMailAuthModule.idl b/comm/mailnews/base/public/nsIMailAuthModule.idl
new file mode 100644
index 0000000000..83c0bdca0d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMailAuthModule.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsIAuthModule provides GSSAPI, NTLM authentications, but it is not
+ * scriptable. nsIMailAuthModule wraps nsIAuthModule and makes it easy to use in
+ * JavaScript.
+ *
+ * The contract id is: "@mozilla.org/mail/auth-module;1".
+ *
+ * @see nsIAuthModule
+ */
+[scriptable, uuid(9895e904-642e-11eb-bb69-6b87832ac976)]
+interface nsIMailAuthModule : nsISupports {
+
+ /**
+ * Initialize an auth module. This is a combination of
+ * nsIAuthModule::CreateInstance and nsIAuthModule::Init. The aType argument
+ * is passed to CreateInstance, other arguments are passed to Init.
+ */
+ void init(in string aType,
+ in ACString aServiceName,
+ in unsigned long aServiceFlags,
+ in AString aDomain,
+ in AString aUsername,
+ in AString aPassword);
+
+ /**
+ * Get the next token in a sequence of authentication steps.
+ * @param aInToken
+ * A base64 encoded string, usually a server challenge.
+ * @returns
+ * A base64 encoded string, usually a response to a server challenge.
+ */
+ ACString getNextToken(in ACString aInToken);
+};
diff --git a/comm/mailnews/base/public/nsIMailChannel.idl b/comm/mailnews/base/public/nsIMailChannel.idl
new file mode 100644
index 0000000000..f5785e6e65
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMailChannel.idl
@@ -0,0 +1,109 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "calIItipItem.idl"
+#include "nsIPropertyBag2.idl"
+#include "nsIMsgSMIMEHeaderSink.idl"
+
+interface nsIMailProgressListener;
+interface nsIWebProgress;
+interface nsIRequest;
+
+/**
+ * An interface that email-streaming channels can use to provide access to
+ * parsed message headers, message attachment info, and other metadata.
+ * The intended use is by QIing nsIChannel to nsIMailChannel.
+ */
+[scriptable, uuid(e4abdb58-54fa-4deb-8c43-714a69519b3a)]
+interface nsIMailChannel : nsISupports {
+ /**
+ * Called by MIME emitters to add a header to this mail channel.
+ * Do not call otherwise.
+ */
+ void addHeaderFromMIME(in AUTF8String name, in AUTF8String value);
+
+ /**
+ * Header names for this request, available at onStopRequest.
+ * The number of header names is the same as the number of header values,
+ * and they are in the same order.
+ */
+ readonly attribute Array<AUTF8String> headerNames;
+
+ /**
+ * Header values for this request, available at onStopRequest.
+ */
+ readonly attribute Array<AUTF8String> headerValues;
+
+ /**
+ * Called by MIME emitters to add attachment info to this mail channel.
+ * Do not call otherwise.
+ */
+ void handleAttachmentFromMIME(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String uri,
+ in boolean aNotDownloaded);
+
+ /**
+ * Called by MIME emitters to add attachment info to this mail channel.
+ * Do not call otherwise.
+ */
+ void addAttachmentFieldFromMIME(in AUTF8String field, in AUTF8String value);
+
+ /**
+ * Attachments for this request, available at onStopRequest.
+ */
+ readonly attribute Array<nsIPropertyBag2> attachments;
+
+ /**
+ * The character set of the message, according to the MIME parser. Not the
+ * character set of the channel, which should always be UTF-8.
+ */
+ attribute AUTF8String mailCharacterSet;
+
+ /**
+ * The method property of iMIP attachments, as determined by the MIME parser.
+ * Not to be set after onStopRequest.
+ */
+ attribute AUTF8String imipMethod;
+
+ /**
+ * The actual iMIP invitation, as created by CalMIMEConverter.
+ * Not to be set after onStopRequest.
+ */
+ attribute calIItipItem imipItem;
+
+ /**
+ * Set this in onStartRequest to receive security status notifications.
+ */
+ attribute nsIMsgSMIMEHeaderSink smimeHeaderSink;
+
+ /**
+ * A listener for progress events. This object must also implement
+ * nsISupportsWeakReference.
+ */
+ attribute nsIMailProgressListener listener;
+};
+
+[scriptable, uuid(1286f969-1c20-422e-8247-233fe0d26ba5)]
+interface nsIMailProgressListener : nsISupports {
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the headers to the channel.
+ */
+ void onHeadersComplete(in nsIMailChannel mailChannel);
+
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the message body to the channel.
+ */
+ void onBodyComplete(in nsIMailChannel mailChannel);
+
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the attachment information to the channel.
+ */
+ void onAttachmentsComplete(in nsIMailChannel mailChannel);
+};
diff --git a/comm/mailnews/base/public/nsIMapiRegistry.idl b/comm/mailnews/base/public/nsIMapiRegistry.idl
new file mode 100644
index 0000000000..c4a2c6d545
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMapiRegistry.idl
@@ -0,0 +1,50 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+/**
+ * This interface provides support for registering Mozilla as the default
+ * Mail Client. This interface can also be used to get/set the user preference
+ * for the default Mail Client.
+ *
+ */
+
+[scriptable, uuid(47D707C3-4369-46A6-A053-5118E12579D6)]
+interface nsIMapiRegistry: nsISupports {
+
+ /** This is set to TRUE if Mozilla is the default mail application
+ */
+ attribute boolean isDefaultMailClient;
+
+ /* Set to TRUE if Mozilla is the default news application */
+ attribute boolean isDefaultNewsClient;
+
+ /* Set to TRUE if we are the default feed/rss application */
+ attribute boolean isDefaultFeedClient;
+
+ /** This is set TRUE only once per session.
+ */
+ readonly attribute boolean showDialog;
+
+ /** This will bring the dialog asking the user if he/she wants to set
+ * Mozilla as default Mail Client.
+ * Call this only if Mozilla is not the default Mail client
+ */
+ void showMailIntegrationDialog(in mozIDOMWindowProxy parentWindow);
+
+ /* After being installed, when we first launch, make sure we add the correct
+ OS registry entries to make us show up as registered mail and news client
+ in the OS
+ */
+
+ void registerMailAndNewsClient();
+};
+
+%{C++
+#define NS_IMAPIREGISTRY_CONTRACTID "@mozilla.org/mapiregistry;1"
+#define NS_IMAPIREGISTRY_CLASSNAME "Mozilla MAPI Registry"
+%}
diff --git a/comm/mailnews/base/public/nsIMessenger.idl b/comm/mailnews/base/public/nsIMessenger.idl
new file mode 100644
index 0000000000..437879782a
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessenger.idl
@@ -0,0 +1,127 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsrootidl.idl"
+#include "nsIMsgWindow.idl"
+#include "nsIMsgIdentity.idl"
+
+interface nsIMsgDBHdr;
+interface mozIDOMWindowProxy;
+interface nsITransactionManager;
+interface nsIMsgMessageService;
+interface nsIFile;
+interface nsIUrlListener;
+
+[scriptable, uuid(01b967c8-b289-4e32-ad46-6eb7c89d4106)]
+interface nsIMessenger : nsISupports {
+
+ const long eUnknown = 0;
+ const long eDeleteMsg = 1;
+ const long eMoveMsg = 2;
+ const long eCopyMsg = 3;
+ const long eMarkAllMsg = 4;
+
+ readonly attribute nsITransactionManager transactionManager;
+
+ void setWindow(in mozIDOMWindowProxy ptr, in nsIMsgWindow msgWindow);
+
+ boolean canUndo();
+ boolean canRedo();
+ unsigned long getUndoTransactionType();
+ unsigned long getRedoTransactionType();
+ void undo(in nsIMsgWindow msgWindow);
+ void redo(in nsIMsgWindow msgWindow);
+
+ /**
+ * Saves a given message to a file or template.
+ *
+ * @param aURI The URI of the message to save
+ * @param aAsFile If true, save as file, otherwise save as a template
+ * @param aIdentity When saving as a template, this is used to determine
+ * the location to save the template to.
+ * @param aMsgFilename When saving as a file, the filename to save the
+ * message as, or the default filename for the file
+ * picker.
+ * @param aBypassFilePicker
+ * If not specified or false, this function will show
+ * a file picker when saving as a file. If true, no
+ * file picker will be shown.
+ */
+ void saveAs(in AUTF8String aURI, in boolean aAsFile,
+ in nsIMsgIdentity aIdentity, in AString aMsgFilename,
+ [optional] in boolean aBypassFilePicker);
+
+ /**
+ * Save the given messages as files in a folder - the user will be prompted
+ * for which folder to use.
+ * @param count message count
+ * @param filenameArray the filenames to use
+ * @param messageUriArray uris of the messages to save
+ */
+ void saveMessages(in Array<AString> filenameArray,
+ in Array<AUTF8String> messageUriArray);
+
+ void saveAttachment(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in boolean isExternalAttachment);
+ void saveAllAttachments(in Array<AUTF8String> contentTypeArray,
+ in Array<AUTF8String> urlArray,
+ in Array<AUTF8String> displayNameArray,
+ in Array<AUTF8String> messageUriArray);
+
+ void saveAttachmentToFile(in nsIFile aFile,
+ in AUTF8String aUrl,
+ in AUTF8String aMessageUri,
+ in AUTF8String aContentType,
+ in nsIUrlListener aListener);
+
+ /**
+ * For a single message and attachments, save these attachments to a file, and
+ * remove from the message. No warning windows will appear, so this is
+ * suitable for use in test and filtering.
+ *
+ * @param aDestFolder Folder to save files in
+ * @param aCount Number of attachments to save
+ * @param aContentTypeArray Content types of the attachments
+ * @param aUrlArray Urls for the attachments
+ * @param aDisplayNameArray Files names to save attachments to. Unique
+ * names will be created if needed.
+ * @param aMessageUriArray Uri for the source message
+ * @param aListener Listener to inform of start and stop of detach
+ */
+ void detachAttachmentsWOPrompts(in nsIFile aDestFolder,
+ in Array<AUTF8String> aContentTypeArray,
+ in Array<AUTF8String> aUrlArray,
+ in Array<AUTF8String> aDisplayNameArray,
+ in Array<AUTF8String> aMessageUriArray,
+ in nsIUrlListener aListener);
+
+ void detachAttachment(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in boolean saveFirst,
+ [optional] in boolean withoutWarning);
+ void detachAllAttachments(in Array<AUTF8String> contentTypeArray,
+ in Array<AUTF8String> urlArray,
+ in Array<AUTF8String> displayNameArray,
+ in Array<AUTF8String> messageUriArray,
+ in boolean saveFirst,
+ [optional] in boolean withoutWarning);
+ // saveAttachmentToFolder is used by the drag and drop code to drop an attachment to a destination folder
+ // We need to return the actual file path (including the filename).
+ nsIFile saveAttachmentToFolder(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in nsIFile aDestFolder);
+
+ nsIMsgDBHdr msgHdrFromURI(in AUTF8String aUri);
+
+ AString formatFileSize(in unsigned long long aPos, [optional] in boolean aUseKB);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerMigrator.idl b/comm/mailnews/base/public/nsIMessengerMigrator.idl
new file mode 100644
index 0000000000..e5a74123d3
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerMigrator.idl
@@ -0,0 +1,14 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(54818d98-1dd2-11b2-82aa-a9197f997503)]
+interface nsIMessengerMigrator: nsISupports {
+ /* migrate old mailnews prefs to the 5.x world */
+ void UpgradePrefs();
+
+ void createLocalMailAccount(in boolean migrating);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerOSIntegration.idl b/comm/mailnews/base/public/nsIMessengerOSIntegration.idl
new file mode 100644
index 0000000000..99c5127701
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerOSIntegration.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Common interfaces to integrate with different platforms, how they are
+ * implemented depends on the specific platform.
+ */
+[scriptable, uuid(d9e45fee-1dd1-11b2-938c-9147855ed837)]
+interface nsIMessengerOSIntegration : nsISupports {
+ /**
+ * Update the unread count.
+ * @param unreadCount - The number of unread messages.
+ * @param unreadTooltip - The tooltip for the unread count.
+ */
+ void updateUnreadCount(in unsigned long unreadCount,
+ in AString unreadTooltip);
+
+ /**
+ * Use this to do necessary clean up on exit, e.g. reset the badge/tray icon.
+ */
+ void onExit();
+};
diff --git a/comm/mailnews/base/public/nsIMessengerWindowService.idl b/comm/mailnews/base/public/nsIMessengerWindowService.idl
new file mode 100644
index 0000000000..9056a734a8
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerWindowService.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(113a1a5a-1dd2-11b2-b1b7-a85ccc06c8ce)]
+interface nsIMessengerWindowService : nsISupports {
+ /**
+ * @param aWindowType the type of window you want to create. i.e. "mail:3pane"
+ * @param aFolderURI the folder resource you want pre-selected (if any)
+ * @param aMsgKey a particular message you may want selected in that folder (if any)
+ */
+ void openMessengerWindowWithUri(in string aWindowType, in AUTF8String aFolderURI, in nsMsgKey aMsgKey);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl b/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl
new file mode 100644
index 0000000000..86f33dd651
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+/* 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/. */
+
+interface mozIDOMWindowProxy;
+
+#include "nsIBaseWindow.idl"
+#include "nsIMessengerOSIntegration.idl"
+
+[scriptable, uuid(e14eb9fe-e05e-4b78-bd31-5b7e1497f91b)]
+interface nsIMessengerWindowsIntegration : nsIMessengerOSIntegration {
+ void hideWindow(in nsIBaseWindow aWindow);
+
+ void showWindow(in mozIDOMWindowProxy aWindow);
+};
diff --git a/comm/mailnews/base/public/nsIMsgAccount.idl b/comm/mailnews/base/public/nsIMsgAccount.idl
new file mode 100644
index 0000000000..aa59222824
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAccount.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIMsgIdentity.idl"
+
+/**
+ * An account consists of an incoming server and one or more
+ * outgoing identities. An account is identified by a key,
+ * which is the <account> string in the account preferences,
+ * such as in mail.account.<account>.identities.
+ */
+
+[scriptable, uuid(84181351-4ec8-4ca8-8439-5c68cc591177)]
+interface nsIMsgAccount : nsISupports {
+
+ /// Internal key identifying itself
+ attribute ACString key;
+
+ /// Incoming server stuff
+ attribute nsIMsgIncomingServer incomingServer;
+
+ /// Outgoing identity list (array of nsIMsgIdentity's)
+ readonly attribute Array<nsIMsgIdentity> identities;
+
+ /// The default identity for this account.
+ attribute nsIMsgIdentity defaultIdentity;
+
+ /// Add a new identity to this account
+ void addIdentity(in nsIMsgIdentity identity);
+
+ /// Remove an identity from this account
+ void removeIdentity(in nsIMsgIdentity identity);
+
+ /// Clear all user preferences associated with an account.
+ void clearAllValues();
+
+ /// Name in javascript
+ AString toString();
+
+ /**
+ * Create the server, with error returns.
+ *
+ * Normally each valid account also has a valid server.
+ *
+ * If an extension that creates a server type failed to load, then we
+ * may have an existing account without a valid server. We don't want
+ * to simply delete that account, as that would make the user re-enter
+ * all of the account information after what may be a temporary
+ * update glitch. But we also don't want to leave junk lying around
+ * forever. So what we do is note the time when we first noticed
+ * that the server was unavailable. After a period of time set
+ * in a preference, if the server is still unavailable then delete
+ * the associated account.
+ *
+ * Accounts with invalid server are not shown in any way by the account
+ * manager. But if the server becomes available (for example if an extension
+ * is loaded), then the account will reappear when accounts are loaded.
+ *
+ * Preference definitions:
+ *
+ * mail.server.serverN.secondsToLeaveUnavailable
+ * mail.server.serverN.timeFoundUnavailable
+ *
+ * secondsToLeaveUnavailable: is set by the extension to indicate the
+ * delay, in seconds, between first detection that a server is
+ * unavailable, and the time it can be deleted. This should be set
+ * by the extension when the server is created. If missing, treat as 0.
+ * A typical value would be 2592000 (30 days)(24 hr/day)(3600 seconds/hr)
+ *
+ * timeFoundUnavailable: is set by core code the first time that a
+ * server is detected as unavailable, using now() converted to seconds.
+ * If that time + secondsToLeaveUnavailable is exceeded, core code may
+ * delete the server and its associated account (though for now we default
+ * to the previous behavior, which is to just delete the account).
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE if the server component type could not
+ * be created
+ * NS_ERROR_ALREADY_INITIALIZED if the server is already created
+ * (Other errors may also be possible)
+ */
+ void createServer();
+};
diff --git a/comm/mailnews/base/public/nsIMsgAccountManager.idl b/comm/mailnews/base/public/nsIMsgAccountManager.idl
new file mode 100644
index 0000000000..39f4aecd79
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAccountManager.idl
@@ -0,0 +1,256 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgAccount.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIIncomingServerListener.idl"
+#include "nsIMsgFolder.idl"
+
+interface nsIMsgFolderCache;
+interface nsIFolderListener;
+
+[scriptable, uuid(d5ab0eea-49c5-42f2-b2e6-8ad306606d8b)]
+interface nsIMsgAccountManager : nsISupports {
+
+ ACString getUniqueAccountKey();
+
+ nsIMsgAccount createAccount();
+ /*
+ * Return the account with the provided key, or null if none found.
+ */
+ nsIMsgAccount getAccount(in ACString key);
+
+ /**
+ * Removes the account from the list of accounts.
+ *
+ * @param aAccount the account to remove
+ * @param aRemoveFiles remove data directory (local directory) of this account
+ */
+ void removeAccount(in nsIMsgAccount aAccount, [optional] in boolean aRemoveFiles);
+
+ /*
+ * creates a new identity and assigns it a new, unique "key"
+ */
+ nsIMsgIdentity createIdentity();
+
+ /**
+ * Scan the preferences to find a unique server key.
+ */
+ ACString getUniqueServerKey();
+
+ /*
+ * creates a new server and assigns it a new, unique "key"
+ * the given type will be used to construct a ContractID
+ *
+ * @param type "imap", "pop3", "nntp", "none", "rss", "generic"
+ * (suffix of contract ID @mozilla.org/messenger/server;1?type= )
+ */
+ nsIMsgIncomingServer createIncomingServer(in ACString username,
+ in ACString hostname,
+ in ACString type);
+
+ /**
+ * Removes the server from the list of servers
+ *
+ * @param aServer server to remove
+ * @param aRemoveFiles remove directory from profile
+ *
+ * @throws NS_ERROR_FAILURE if server not found
+ */
+ void removeIncomingServer(in nsIMsgIncomingServer aServer,
+ in boolean aRemoveFiles);
+ /*
+ * get the identity with the given key
+ * if the identity does not exist, it will be created
+ */
+ nsIMsgIdentity getIdentity(in ACString key);
+
+ /*
+ * Gets the existing incoming server with the given key
+ * if the server's type does not exist in the preference,
+ * an error is returned/thrown
+ */
+ nsIMsgIncomingServer getIncomingServer(in ACString key);
+
+ /* account list stuff */
+
+ /**
+ * Returns the account that was marked as the default one.
+ * Only some server types can serve as default account.
+ * If there is no such account, null is returned.
+ * You can only set the defaultAccount to an
+ * account already in the account manager.
+ */
+ attribute nsIMsgAccount defaultAccount;
+
+ /**
+ * Ordered list of all accounts, by the order they are in the prefs.
+ * Accounts with hidden servers are not returned.
+ * array of nsIMsgAccount
+ */
+ readonly attribute Array<nsIMsgAccount> accounts;
+
+ /* list of all identities in all accounts
+ * array of nsIMsgIdentity
+ */
+ readonly attribute Array<nsIMsgIdentity> allIdentities;
+
+ /* list of all servers in all accounts, except for hidden and IM servers
+ * array of nsIMsgIncomingServer
+ */
+ readonly attribute Array<nsIMsgIncomingServer> allServers;
+
+ /* summary of summary files folder cache */
+ readonly attribute nsIMsgFolderCache folderCache;
+
+ /* are we shutting down */
+ readonly attribute boolean shutdownInProgress;
+
+ /**
+ * for preventing unauthenticated users from seeing header information
+ */
+ attribute boolean userNeedsToAuthenticate;
+ /*
+ * search for the server with the given username, hostname, and type
+ * the type is the same as is specified in the preferences,
+ * i.e. "imap", "pop3", "none", or "nntp"
+ */
+ nsIMsgIncomingServer findServer(in ACString userName,
+ in ACString hostname,
+ in ACString type,
+ [optional] in long port);
+
+ /*
+ * search for the server with the given uri
+ * an analog to FindServer()
+ */
+ nsIMsgIncomingServer findServerByURI(in nsIURI aURI);
+
+ /**
+ * find the index of this server in the (ordered) list of accounts
+ */
+ long FindServerIndex(in nsIMsgIncomingServer server);
+
+ /**
+ * Finds an account for the given incoming server.
+ *
+ * @param server An incoming server to find the account for.
+ * @return If found, the nsIMsgAccount representing the account found.
+ * Otherwise returns null.
+ */
+ nsIMsgAccount FindAccountForServer(in nsIMsgIncomingServer server);
+
+ /* given a server, return all identities in accounts that have this server
+ * returns an array of nsIMsgIdentity
+ */
+ Array<nsIMsgIdentity> getIdentitiesForServer(in nsIMsgIncomingServer server);
+
+ /**
+ * given a server, return the first identity in accounts that have this server
+ */
+ nsIMsgIdentity getFirstIdentityForServer(in nsIMsgIncomingServer server);
+
+ /* given an identity, return all servers in accounts that have
+ * this identity
+ * returns an array of nsIMsgIncomingServer
+ */
+ Array<nsIMsgIncomingServer> getServersForIdentity(in nsIMsgIdentity identity);
+
+ /* there is a special server "Local Folders" that is guaranteed to exist.
+ * this will allow you to get */
+ attribute nsIMsgIncomingServer localFoldersServer;
+
+ // Create the account for that special server.
+ void createLocalMailAccount();
+
+ /**
+ * Kicks off the creation of all accounts. You do not need to call this and
+ * all accounts should be loaded lazily if you use any of the above.
+ */
+ void loadAccounts();
+
+ /** Frees all the account manager data structures. */
+ void unloadAccounts();
+
+ /**
+ * When the server for an account could not be loaded, typically because the
+ * extension providing it could not be loaded, it is deactivated for a period
+ * of time as documented in nsIMsgAccount.idl. The server is normally only
+ * rechecked at startup but this function can be used to recheck all servers
+ * at any time to avoid having to restart to reactivate an account.
+ */
+ void reactivateAccounts();
+
+ void setSpecialFolders();
+
+ void loadVirtualFolders();
+
+ void WriteToFolderCache(in nsIMsgFolderCache folderCache);
+ void saveVirtualFolders();
+ void closeCachedConnections();
+ void shutdownServers();
+
+ void CleanupOnExit();
+ void SetFolderDoingEmptyTrash(in nsIMsgFolder folder);
+ boolean GetEmptyTrashInProgress();
+
+ void SetFolderDoingCleanupInbox(in nsIMsgFolder folder);
+ boolean GetCleanupInboxInProgress();
+
+ void addRootFolderListener(in nsIFolderListener listener);
+ void removeRootFolderListener(in nsIFolderListener listener);
+
+ // these are going away in favor of add/removeRootFolderListener
+ void addIncomingServerListener(in nsIIncomingServerListener serverListener);
+ void removeIncomingServerListener(in nsIIncomingServerListener serverListener);
+
+ // these are going away in favor of nsIMsgFolder::NotifyEvent(in ACString event);
+ // XXX what does this mean? There is no such function yet.
+ void notifyServerLoaded(in nsIMsgIncomingServer server);
+ void notifyServerUnloaded(in nsIMsgIncomingServer server);
+ void notifyServerChanged(in nsIMsgIncomingServer server);
+
+ // force account info out to prefs file
+ void saveAccountInfo();
+
+ ACString getChromePackageName(in ACString aExtensionName);
+
+ /// Enumerate all incoming servers and their folders and return in an array.
+ readonly attribute Array<nsIMsgFolder> allFolders;
+
+ /**
+ * Iterates over all folders looking for one with the passed in path,
+ * and returns the uri for the matching folder. In the future,
+ * the folder lookup service will provide this functionality.
+ *
+ * @param aLocalPath path of the folder whose uri we want.
+ * @return the URI of the folder that corresponds to aLocalPath
+ */
+ AUTF8String folderUriForPath(in nsIFile aLocalPath);
+
+ // Used to sort servers (accounts) for e.g. the folder pane
+ long getSortOrder(in nsIMsgIncomingServer server);
+
+ /**
+ * Sets new order of accounts.
+ *
+ * @param accountKeys - Account keys in the new preferred order.
+ */
+ void reorderAccounts(in Array<ACString> accountKeys);
+};
+
+%{C++
+#define MAILNEWS_ACCOUNTMANAGER_EXTENSIONS "mailnews-accountmanager-extensions"
+%}
+
+[scriptable, uuid(70032DE0-CD59-41ba-839D-FC1B65367EE7)]
+interface nsIMsgAccountManagerExtension : nsISupports
+{
+ readonly attribute ACString name; // examples: mdn
+ boolean showPanel(in nsIMsgIncomingServer server);
+ readonly attribute ACString chromePackageName; // example: messenger, chrome://messenger/content/am-mdn.xhtml and chrome://messenger/locale/am-mdn.properties
+};
diff --git a/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl b/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl
new file mode 100644
index 0000000000..0e1a68846b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl
@@ -0,0 +1,79 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgAsyncPromptListener;
+
+/**
+ * The nsIMsgAsyncPrompter is intended to provide a way to make asynchronous
+ * message prompts into synchronous ones - so that the user is only prompted
+ * with one at a time.
+ */
+[scriptable, uuid(15f67d0f-947a-4a1e-8f72-6ab7162b4b9c)]
+interface nsIMsgAsyncPrompter : nsISupports {
+ /**
+ * Queues an async prompt request. If there are none queued then this will be
+ * actioned straight away, otherwise the prompt will be queued for action
+ * once previous prompt(s) have been cleared.
+ *
+ * Queued prompts using the same aKey may be amalgamated into one prompt to
+ * save repeated prompts to the user.
+ *
+ * @param aKey A key to determine whether or not the queued prompts can
+ * be combined.
+ * @param aPromptImmediately If the user is retrying a failed password, we
+ * need to prompt right away, even if there is a
+ * prompt up, or prompts queued up. Note that
+ * immediately may not be synchronously, on OS/X.
+ * @param aCaller An nsIMsgAsyncPromptListener to call back to when the prompt
+ * is ready to be made.
+ */
+ void queueAsyncAuthPrompt(in ACString aKey, in boolean aPromptImmediately,
+ in nsIMsgAsyncPromptListener aCaller);
+};
+
+[scriptable, function, uuid(acca94c9-378e-46e3-9a91-6655bf9c91a3)]
+interface nsIMsgAsyncPromptCallback : nsISupports {
+ /**
+ * Called when an auth result is available. Can be passed as a function.
+ *
+ * @param aResult True if there is auth information available following the
+ * prompt, false otherwise.
+ */
+ void onAuthResult(in boolean aResult);
+};
+
+/**
+ * This is used in combination with nsIMsgAsyncPrompter.
+ */
+[scriptable, uuid(fb5307a3-39d0-462e-92c8-c5c288a2612f)]
+interface nsIMsgAsyncPromptListener : nsISupports {
+ /**
+ * This method has been deprecated, please use onPromptStartAsync instead.
+ */
+ boolean onPromptStart();
+
+ /**
+ * Called when the listener should do its prompt. This can happen
+ * synchronously or asynchronously, but in any case when done the callback
+ * method should be called.
+ *
+ * @param aCallback The callback to execute when auth prompt has completed.
+ */
+ void onPromptStartAsync(in nsIMsgAsyncPromptCallback aCallback);
+
+ /**
+ * Called in the case that the queued prompt was combined with another and
+ * there is now authentication information available.
+ */
+ void onPromptAuthAvailable();
+
+ /**
+ * Called in the case that the queued prompt was combined with another but
+ * the prompt was canceled.
+ */
+ void onPromptCanceled();
+};
diff --git a/comm/mailnews/base/public/nsIMsgBiffManager.idl b/comm/mailnews/base/public/nsIMsgBiffManager.idl
new file mode 100644
index 0000000000..168ce1ace1
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgBiffManager.idl
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+
+[scriptable, uuid(17275D52-1622-11d3-8A84-0060B0FC04D2)]
+interface nsIMsgBiffManager : nsISupports {
+
+ void init();
+ void addServerBiff(in nsIMsgIncomingServer server);
+ void removeServerBiff(in nsIMsgIncomingServer server);
+ void forceBiff(in nsIMsgIncomingServer server);
+ void forceBiffAll();
+ void shutdown();
+};
diff --git a/comm/mailnews/base/public/nsIMsgContentPolicy.idl b/comm/mailnews/base/public/nsIMsgContentPolicy.idl
new file mode 100644
index 0000000000..882a39b49b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgContentPolicy.idl
@@ -0,0 +1,35 @@
+/* -*- mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c29b2fd3-64d0-4083-a096-c20a9b847a99)]
+
+/**
+ * This interface provide functions which help extension developers
+ * add their customized schema to the exposed protocls of nsMsgContentPolicy.
+ * By default, a list of existing protocols (such as imap and nntp)
+ * are allowed to process urls locally, while non-matching urls are required
+ * to be processed as external.
+ * This interface allows additional protocols to be added to
+ * the list of protocols that are processed locally.
+ * Typically this would be used in cases where a new messaging protocol
+ * is being added by an extension.
+ */
+interface nsIMsgContentPolicy : nsISupports {
+ /**
+ * Add the specific aScheme to nsMsgContentPolicy's exposed protocols.
+ *
+ * @param aScheme scheme who will be added to nsMsgContentPolicy's exposed protocols
+ */
+ void addExposedProtocol(in ACString aScheme);
+
+ /**
+ * Remove the specific aScheme from nsMsgContentPolicy's exposed protocols.
+ *
+ * @param aScheme scheme who will be removed from nsMsgContentPolicy's exposed protocols
+ */
+ void removeExposedProtocol(in ACString aScheme);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCopyService.idl b/comm/mailnews/base/public/nsIMsgCopyService.idl
new file mode 100644
index 0000000000..b28ee9170c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCopyService.idl
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsrootidl.idl"
+#include "nsISupports.idl"
+#include "nsIMsgFolder.idl"
+#include "nsIMsgCopyServiceListener.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIFile;
+
+/**
+ * nsIMsgCopyService is a central point for kicking off message and folder
+ * copy/move operations.
+ * Each operation is queued up and executed in sequence. The actual work is
+ * handled by folder code in an asynchronous fashion. The folder indicates
+ * completion by calling notifyCompletion().
+ *
+ * If the operation was initiated with a non-null nsIMsgCopyServiceListener,
+ * its OnStartCopy() and OnStopCopy() methods will be called when the
+ * operation begins/ends. Any errors are communicated via the result code
+ * parameter passed to OnStopCopy().
+ */
+[scriptable, uuid(f21e428b-73c5-4607-993b-d37325b33722)]
+interface nsIMsgCopyService : nsISupports {
+
+ /**
+ * Copies or moves existing messages from source folder to destination folder.
+ *
+ * @param srcFolder Source folder of an operation.
+ * @param messages The array of nsIMsgHdrs in source folder which will be moved/copied.
+ * @param dstFolder Destination folder of operation.
+ * @param isMove false for copy operation, true for move operation.
+ * @param listener Listener which receive operation notifications
+ * @param msgWindow Window for notification callbacks, can be null.
+ * @param allowUndo Specifies if this operation will be done as an transaction
+ * that can be undone.
+ */
+ void copyMessages(in nsIMsgFolder srcFolder,
+ in Array<nsIMsgDBHdr> messages,
+ in nsIMsgFolder dstFolder,
+ in boolean isMove,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow,
+ in boolean allowUndo);
+
+ /**
+ * Copies or moves a folder into an existing destination folder.
+ *
+ * @param srcFolder The nsIMsgFolder which will be moved/copied.
+ * @param dstFolder The destination folder of operation.
+ * @param isMove false for copy operation, true for move operation.
+ * @param listener Listener which receive operation notifications.
+ * @param msgWindow Window for notification callbacks, can be null.
+ */
+ void copyFolder(in nsIMsgFolder srcFolder,
+ in nsIMsgFolder dstFolder,
+ in boolean isMove,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow);
+
+ /**
+ * Copies message in rfc format from file to folder.
+ *
+ * @param aFile A file which contains message in rfc format which
+ * will copied to destFolder.
+ * @param dstFolder Destination folder where a message will be copied.
+ * @param msgToReplace Header which identifies a message to use as a source
+ * of message properties, or null. For example, when
+ * deleting an attachment, the processed message is
+ * stored in a file, but the metadata should be copied
+ * from the original message. This method will NOT delete
+ * the original message.
+ * @param isDraftOrTemplate Specifies whether a message is a stored in draft
+ * folder or not. If is true listener should
+ * implement GetMessageId and return unique id for
+ * message in destination folder. This is important
+ * for IMAP servers which doesn't support uidplus.
+ * If destination folder contains message with the
+ * same message-id then it is possible that listener
+ * get wrong message key in callback
+ * nsIMsgCopyServiceListener::SetMessageKey.
+ * @param aMsgFlags Message flags which will be set after message is
+ * copied
+ * @param aMsgKeywords Keywords which will be set for newly copied
+ * message.
+ * @param listener Listener which receive copy notifications.
+ * @param msgWindow Window for notification callbacks, can be null.
+ */
+ void copyFileMessage(in nsIFile aFile,
+ in nsIMsgFolder dstFolder,
+ in nsIMsgDBHdr msgToReplace,
+ in boolean isDraftOrTemplate,
+ in unsigned long aMsgFlags,
+ in ACString aMsgKeywords,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow);
+
+ /**
+ * Notify the message copy service that the destination folder has finished
+ * it's messages copying operation so that the copy service can continue
+ * copying the rest of the messages if there are more to copy with.
+ * aSupport and dstFolder uniquely identify a copy service request.
+ *
+ * @param aSupport The originator of CopyMessages or copyFileMessage; it can
+ * be either a nsIMsgFolder or a nsIFile
+ * @param dstFolder The destination folder which performs the copy operation
+ * @param result The result of the copy operation
+ */
+ void notifyCompletion(in nsISupports aSupport,
+ in nsIMsgFolder dstFolder,
+ in nsresult result);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl b/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl
new file mode 100644
index 0000000000..0c915c8d38
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsrootidl.idl"
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(da6b9843-5464-4630-b121-c5970aa3d6ed)]
+interface nsIMsgCopyServiceListener : nsISupports {
+
+ /**
+ * Notify the observer that the message has started to be copied. This
+ * method is called only once, at the beginning of a message
+ * copyoperation.
+ */
+ void OnStartCopy();
+
+ /**
+ * Notify the observer that progress as occurred for the message copy
+ * aProgress -
+ * aProgressMax -
+ */
+ void OnProgress(in uint32_t aProgress,
+ in uint32_t aProgressMax);
+
+ /**
+ * Setting newly created message key. This method is tailored specifically
+ * for nsIMsgCopyService::copyFileMessage() when saving Drafts/Templates.
+ * We need to have a way to inform the client what's the key of the newly
+ * created message.
+ * aKey -
+ */
+ void SetMessageKey(in nsMsgKey aKey);
+
+ /**
+ * Getting the file message message ID. This method is tailored
+ * specifically for nsIMsgCopyService::copyFileMessage() when saving
+ * Drafts/Templates. In order to work with imap server which doesn't
+ * support uidplus we have to use search command to retrieve the key of
+ * newly created message. Message ID generated by the compose guarantee its
+ * uniqueness.
+ * aMessageId -
+ */
+ void GetMessageId(out ACString aMessageId);
+
+ /**
+ * Notify the observer that the message copied operation has completed.
+ * This method is called regardless of whether the the operation was
+ * successful.
+ * aStatus - indicate whether the operation was succeeded
+ */
+ void OnStopCopy(in nsresult aStatus);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl b/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl
new file mode 100644
index 0000000000..1b5287c56e
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsITreeView.idl"
+
+interface nsIMsgDBHdr;
+
+ /* //TODO JavaDoc
+ When implementing a js custom column handler (of type nsITreeView) you must implement the following
+ functions:
+ 1. isEditable
+ 2. GetCellProperties
+ 3. GetImageSrc
+ 4. GetCellText
+ 5. CycleCell
+ 6. GetSortStringForRow
+ 7. GetSortLongForRow
+ 8. isString
+
+ You can, at your option, implement
+ 9. GetRowProperties.
+
+ With Bug 1192696, Grouped By Sort was implemented for custom columns.
+ Implementers should consider that the value returned by GetSortStringForRow
+ will be displayed in the grouped header row, as well as be used as the
+ sort string.
+
+ If implementing a c++ custom column handler, you must define all
+ nsITreeView and nsIMsgCustomColumnHandler methods.
+ */
+
+[scriptable, uuid(00f75b13-3ac4-4a17-a8b9-c6e4dd1b3f32)]
+interface nsIMsgCustomColumnHandler : nsITreeView
+{
+ AString getSortStringForRow(in nsIMsgDBHdr aHdr);
+ unsigned long getSortLongForRow(in nsIMsgDBHdr aHdr);
+ boolean isString();
+};
diff --git a/comm/mailnews/base/public/nsIMsgDBView.idl b/comm/mailnews/base/public/nsIMsgDBView.idl
new file mode 100644
index 0000000000..1c824c58f9
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgDBView.idl
@@ -0,0 +1,558 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIMessenger;
+interface nsIMsgDBHdr;
+interface nsIMsgThread;
+interface nsIMsgDBViewCommandUpdater;
+interface nsIMsgJSTree;
+interface nsIMsgDatabase;
+interface nsIMsgSearchSession;
+interface nsIMsgEnumerator;
+interface nsIMsgCustomColumnHandler;
+
+typedef long nsMsgViewNotificationCodeValue;
+typedef long nsMsgViewCommandCheckStateValue;
+typedef long nsMsgViewCommandTypeValue;
+typedef long nsMsgNavigationTypeValue;
+
+[scriptable, uuid(682a18be-fd18-11d4-a5be-0060b0fc04b7)]
+interface nsMsgViewSortOrder : nsISupports
+{
+ const nsMsgViewSortOrderValue none = 0;
+ const nsMsgViewSortOrderValue ascending = 1;
+ const nsMsgViewSortOrderValue descending = 2;
+};
+
+[scriptable, uuid(f28a1cdf-06c3-4e98-8f66-f49991670071)]
+interface nsMsgViewType : nsISupports {
+ const nsMsgViewTypeValue eShowAllThreads = 0;
+ const nsMsgViewTypeValue eShowThreadsWithUnread = 2;
+ const nsMsgViewTypeValue eShowWatchedThreadsWithUnread = 3;
+ const nsMsgViewTypeValue eShowQuickSearchResults = 4;
+ const nsMsgViewTypeValue eShowVirtualFolderResults = 5;
+ const nsMsgViewTypeValue eShowSearch = 6;
+};
+
+[scriptable, uuid(64852276-1dd2-11b2-8103-afe12002c053)]
+interface nsMsgViewFlagsType : nsISupports
+{
+ /**
+ * flags for GetViewFlags
+ */
+ const nsMsgViewFlagsTypeValue kNone = 0x0;
+ const nsMsgViewFlagsTypeValue kThreadedDisplay = 0x1;
+ const nsMsgViewFlagsTypeValue kShowIgnored = 0x8;
+ const nsMsgViewFlagsTypeValue kUnreadOnly = 0x10;
+ const nsMsgViewFlagsTypeValue kExpandAll = 0x20;
+ const nsMsgViewFlagsTypeValue kGroupBySort = 0x40;
+};
+
+[scriptable, uuid(b94fc200-3008-420a-85c7-67842f133ef8)]
+interface nsMsgViewSortType : nsISupports
+{
+ const nsMsgViewSortTypeValue byNone = 0x11; /* not sorted */
+ const nsMsgViewSortTypeValue byDate = 0x12;
+ const nsMsgViewSortTypeValue bySubject = 0x13;
+ const nsMsgViewSortTypeValue byAuthor = 0x14;
+ const nsMsgViewSortTypeValue byId = 0x15;
+ const nsMsgViewSortTypeValue byThread = 0x16;
+ const nsMsgViewSortTypeValue byPriority = 0x17;
+ const nsMsgViewSortTypeValue byStatus = 0x18;
+ const nsMsgViewSortTypeValue bySize = 0x19;
+ const nsMsgViewSortTypeValue byFlagged = 0x1a;
+ const nsMsgViewSortTypeValue byUnread = 0x1b;
+ const nsMsgViewSortTypeValue byRecipient = 0x1c;
+ const nsMsgViewSortTypeValue byLocation = 0x1d;
+ const nsMsgViewSortTypeValue byTags = 0x1e;
+ const nsMsgViewSortTypeValue byJunkStatus = 0x1f;
+ const nsMsgViewSortTypeValue byAttachments = 0x20;
+ const nsMsgViewSortTypeValue byAccount = 0x21;
+ const nsMsgViewSortTypeValue byCustom = 0x22;
+ const nsMsgViewSortTypeValue byReceived = 0x23;
+ const nsMsgViewSortTypeValue byCorrespondent = 0x24;
+};
+
+[scriptable, uuid(255d1c1e-fde7-11d4-a5be-0060b0fc04b7)]
+interface nsMsgViewNotificationCode : nsISupports
+{
+ const nsMsgViewNotificationCodeValue none = 0;
+ /* No change; this call is just being used to potentially nest other sets of calls
+ inside it. The "where" and "num" parameters are unused.
+ */
+ const nsMsgViewNotificationCodeValue insertOrDelete = 1;
+ /* Some lines have been inserted or deleted.
+ The "where" parameter will indicate the first line that has been added or
+ removed; the "num" parameter will indicate how many lines, and will be positive on
+ an insertion and negative on a deletion.
+ */
+ const nsMsgViewNotificationCodeValue changed = 2;
+ /* Some lines have had their contents changed (e.g., messages have been marked read
+ or something.) "where" indicates the first line with a change; "num" indicates
+ how many changed.
+ */
+ const nsMsgViewNotificationCodeValue all = 4;
+ /* Everything changed. We're now not displaying anything like what we were; we
+ probably opened a new folder or something. The FE needs to forget anything it ever knew
+ about what was being displayed, and start over. The "where" and "num" parameters are
+ unused.
+ */
+};
+
+[scriptable, uuid(4ec9248e-0108-11d5-a5be-0060b0fc04b7)]
+interface nsMsgViewCommandCheckState : nsISupports
+{
+ const nsMsgViewCommandCheckStateValue notUsed = 0;
+ const nsMsgViewCommandCheckStateValue checked = 1;
+ const nsMsgViewCommandCheckStateValue unchecked = 2;
+};
+
+[scriptable, uuid(ad36e6cc-0109-11d5-a5be-0060b0fc04b7)]
+interface nsMsgViewCommandType : nsISupports
+{
+ const nsMsgViewCommandTypeValue markMessagesRead = 0;
+ const nsMsgViewCommandTypeValue markMessagesUnread = 1;
+ const nsMsgViewCommandTypeValue toggleMessageRead = 2;
+
+ const nsMsgViewCommandTypeValue flagMessages = 3;
+ const nsMsgViewCommandTypeValue unflagMessages = 4;
+
+ const nsMsgViewCommandTypeValue toggleThreadWatched = 6;
+
+ const nsMsgViewCommandTypeValue deleteMsg = 7;
+ const nsMsgViewCommandTypeValue deleteNoTrash = 8;
+ const nsMsgViewCommandTypeValue markThreadRead = 9;
+ const nsMsgViewCommandTypeValue markAllRead = 10;
+ const nsMsgViewCommandTypeValue expandAll = 11;
+ const nsMsgViewCommandTypeValue collapseAll = 12;
+
+ const nsMsgViewCommandTypeValue copyMessages = 13;
+ const nsMsgViewCommandTypeValue moveMessages = 14;
+
+ const nsMsgViewCommandTypeValue selectAll = 15;
+ const nsMsgViewCommandTypeValue downloadSelectedForOffline = 16;
+ const nsMsgViewCommandTypeValue downloadFlaggedForOffline = 17;
+
+ const nsMsgViewCommandTypeValue selectThread = 18;
+ const nsMsgViewCommandTypeValue selectFlagged = 19;
+ const nsMsgViewCommandTypeValue cmdRequiringMsgBody = 20;
+ const nsMsgViewCommandTypeValue label0 = 21;
+ const nsMsgViewCommandTypeValue label1 = 22;
+ const nsMsgViewCommandTypeValue label2 = 23;
+ const nsMsgViewCommandTypeValue label3 = 24;
+ const nsMsgViewCommandTypeValue label4 = 25;
+ const nsMsgViewCommandTypeValue label5 = 26;
+ const nsMsgViewCommandTypeValue lastLabel = 26;
+
+ const nsMsgViewCommandTypeValue junk = 27;
+ const nsMsgViewCommandTypeValue unjunk = 28;
+ const nsMsgViewCommandTypeValue undeleteMsg = 29;
+
+ const nsMsgViewCommandTypeValue applyFilters = 30;
+ const nsMsgViewCommandTypeValue runJunkControls = 31;
+ const nsMsgViewCommandTypeValue deleteJunk = 32;
+};
+
+[scriptable, uuid(65903eb2-1dd2-11b2-ac45-c5b69c1618d7)]
+interface nsMsgNavigationType : nsISupports
+{
+ const nsMsgNavigationTypeValue firstMessage = 1;
+ const nsMsgNavigationTypeValue nextMessage = 2;
+ const nsMsgNavigationTypeValue previousMessage = 3;
+ const nsMsgNavigationTypeValue lastMessage = 4;
+ /**
+ * must match nsMsgViewCommandTypeValue toggleThreadKilled
+ */
+ const nsMsgNavigationTypeValue toggleThreadKilled = 5;
+ const nsMsgNavigationTypeValue firstUnreadMessage = 6;
+ const nsMsgNavigationTypeValue nextUnreadMessage = 7;
+ const nsMsgNavigationTypeValue previousUnreadMessage = 8;
+ const nsMsgNavigationTypeValue lastUnreadMessage = 9;
+ const nsMsgNavigationTypeValue nextUnreadThread = 10;
+ const nsMsgNavigationTypeValue nextUnreadFolder = 11;
+ const nsMsgNavigationTypeValue nextFolder = 12;
+ const nsMsgNavigationTypeValue readMore = 13;
+ /**
+ * Go back to the previous visited message
+ */
+ const nsMsgNavigationTypeValue back = 15;
+ /**
+ * Go forward to the previous visited message
+ */
+ const nsMsgNavigationTypeValue forward = 16;
+ const nsMsgNavigationTypeValue firstFlagged = 17;
+ const nsMsgNavigationTypeValue nextFlagged = 18;
+ const nsMsgNavigationTypeValue previousFlagged = 19;
+ const nsMsgNavigationTypeValue firstNew = 20;
+ const nsMsgNavigationTypeValue editUndo = 21;
+ const nsMsgNavigationTypeValue editRedo = 22;
+ const nsMsgNavigationTypeValue toggleSubthreadKilled = 23;
+};
+
+/*
+ * The contract ID for this component is @mozilla.org/msgDBView/msgDBViewService;1.
+ */
+[scriptable, uuid(bcf6afbe-7d4f-11ec-9092-eb4fed0a5aaa)]
+interface nsIMsgDBViewService : nsISupports
+{
+ /**
+ * JS-callable service to initialize static variables in nsMsgDBView.cpp
+ * upon initialization or when locale changes.
+ */
+ void initializeDBViewStrings();
+};
+
+[scriptable, uuid(fe8a2326-4dd0-11e5-8b8a-206a8aa7a25c)]
+interface nsIMsgDBView : nsISupports
+{
+ /** A shim of XULTreeElement, with only the methods called by nsMsgDBView. */
+ void setJSTree(in nsIMsgJSTree tree);
+
+ void open(in nsIMsgFolder folder, in nsMsgViewSortTypeValue sortType, in nsMsgViewSortOrderValue sortOrder, in nsMsgViewFlagsTypeValue viewFlags, out long count);
+ void openWithHdrs(in nsIMsgEnumerator aHeaders, in nsMsgViewSortTypeValue aSortType,
+ in nsMsgViewSortOrderValue aSortOrder,
+ in nsMsgViewFlagsTypeValue aViewFlags, out long aCount);
+ void close();
+
+ void init(in nsIMessenger aMessengerInstance, in nsIMsgWindow aMsgWindow, in nsIMsgDBViewCommandUpdater aCommandUpdater);
+
+ void sort(in nsMsgViewSortTypeValue sortType, in nsMsgViewSortOrderValue sortOrder);
+
+ void doCommand(in nsMsgViewCommandTypeValue command);
+ void doCommandWithFolder(in nsMsgViewCommandTypeValue command, in nsIMsgFolder destFolder);
+ void getCommandStatus(in nsMsgViewCommandTypeValue command, out boolean selectable_p,
+ out nsMsgViewCommandCheckStateValue selected_p);
+ void applyCommandToIndices(in nsMsgViewCommandTypeValue command,
+ in Array<nsMsgViewIndex> selection);
+
+ readonly attribute nsMsgViewTypeValue viewType;
+ attribute nsMsgViewFlagsTypeValue viewFlags;
+ /** Assigning to this value does not induce a sort; use the sort() method! */
+ attribute nsMsgViewSortTypeValue sortType;
+ readonly attribute nsMsgViewSortOrderValue sortOrder;
+ /**
+ * Reflects the current secondary sort when a secondary sort is in effect.
+ * If the primary sort is by date or id, the value of this attribute is moot.
+ * Assigning to this value does not induce a sort; use the sort() method once
+ * to set your secondary sort, then use it again to set your primary sort.
+ * The only conceivable reason to write to this value is if you have a
+ * grouped view where you want to affect the sort order of the (secondary)
+ * date sort. (Secondary sort is always by date for grouped views.)
+ */
+ attribute nsMsgViewSortTypeValue secondarySortType;
+ /**
+ * Reflects the current secondary sort order.
+ * Assigning to this value does not induce a sort; use the sort() method for
+ * all primary and secondary sort needs. The only reason to assign to this
+ * value is to affect the secondary sort of a grouped view.
+ */
+ attribute nsMsgViewSortOrderValue secondarySortOrder;
+ readonly attribute nsMsgKey keyForFirstSelectedMessage;
+ readonly attribute nsMsgViewIndex viewIndexForFirstSelectedMsg;
+ /**
+ * this method will automatically expand the destination thread,
+ * if needs be.
+ */
+ void viewNavigate(in nsMsgNavigationTypeValue motion, out nsMsgKey resultId, out nsMsgViewIndex resultIndex, out nsMsgViewIndex threadIndex, in boolean wrap);
+
+ readonly attribute nsIMsgFolder msgFolder;
+ attribute nsIMsgFolder viewFolder; // in the case of virtual folders, the VF db.
+
+ nsMsgKey getKeyAt(in nsMsgViewIndex index);
+
+ /**
+ * Get the view flags at the passed in index.
+ *
+ * @param aIndex - index to get the view flags for
+ *
+ * @ return - 32 bit view flags (e.g., elided)
+ */
+ unsigned long getFlagsAt(in nsMsgViewIndex aIndex);
+
+ /**
+ * Get the msg hdr at the passed in index
+ *
+ * @param aIndex - index to get the msg hdr at.
+ *
+ * @return - msg hdr at the passed in index
+ * @exception - NS_MSG_INVALID_DBVIEW_INDEX
+ */
+ nsIMsgDBHdr getMsgHdrAt(in nsMsgViewIndex aIndex);
+
+ nsIMsgFolder getFolderForViewIndex(in nsMsgViewIndex index); // mainly for search
+ AUTF8String getURIForViewIndex(in nsMsgViewIndex index);
+ nsIMsgDBView cloneDBView(in nsIMessenger aMessengerInstance, in nsIMsgWindow aMsgWindow, in nsIMsgDBViewCommandUpdater aCommandUpdater);
+
+ /**
+ * Provides a list of the message headers for the currently selected messages.
+ * If the "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+ * then any collapsed thread roots that are selected will also (conceptually)
+ * have all of the messages in that thread selected and they will be included
+ * in the returned list. The one exception to this is if the front end fails
+ * to summarize the selection, and we fall back to just displaying a single
+ * message. In that case, we won't include the children of the collapsed
+ * thread. However, the numSelected attribute will count those children,
+ * because the summarizeSelection code uses that to know that it should
+ * try to summarize the selection.
+ *
+ * If the user has right-clicked on a message, this will return that message
+ * (and any collapsed children if so enabled) and not the selection prior to
+ * the right-click.
+ *
+ * @return an array containing the selected message headers. You are free to
+ * mutate the array; it will not affect the underlying selection.
+ */
+ Array<nsIMsgDBHdr> getSelectedMsgHdrs();
+
+ Array<AUTF8String> getURIsForSelection();
+ Array<nsMsgViewIndex> getIndicesForSelection();
+
+ readonly attribute AUTF8String URIForFirstSelectedMessage;
+ readonly attribute nsIMsgDBHdr hdrForFirstSelectedMessage;
+
+ /**
+ * The number of selected messages. If the
+ * "mail.operate_on_msgs_in_collapsed_threads" preference is enabled, then
+ * any collapsed thread roots that are selected will also conceptually have
+ * all of the messages in that thread selected.
+ */
+ readonly attribute unsigned long numSelected;
+ readonly attribute nsMsgViewIndex msgToSelectAfterDelete;
+ readonly attribute nsMsgViewIndex currentlyDisplayedMessage;
+
+ /**
+ * Number of messages in view, including messages in collapsed threads.
+ * Not currently implemented for threads with unread or watched threads
+ * with unread.
+ */
+ readonly attribute long numMsgsInView;
+ // used by "go to folder" feature
+ // and "remember last selected message" feature
+ // if key is not found, we don't select.
+ void selectMsgByKey(in nsMsgKey key);
+
+ void selectFolderMsgByKey(in nsIMsgFolder aFolder, in nsMsgKey aKey);
+ // we'll suppress displaying messages if the message pane is collapsed
+ attribute boolean suppressMsgDisplay;
+
+ // we'll suppress command updating during folder loading
+ attribute boolean suppressCommandUpdating;
+
+ /**
+ * Suppress change notifications. This is faster than Begin/EndUpdateBatch
+ * on the tree, but less safe in that you're responsible for row invalidation
+ * and row count changes.
+ */
+ attribute boolean suppressChangeNotifications;
+
+ //to notify tree that rows are going away
+ void onDeleteCompleted(in boolean succeeded);
+
+ readonly attribute nsIMsgDatabase db;
+
+ readonly attribute boolean supportsThreading;
+
+ attribute nsIMsgSearchSession searchSession;
+ readonly attribute boolean removeRowOnMoveOrDelete;
+
+ /**
+ * Finds the view index of the passed in msgKey. Note this should not
+ * be called on cross-folder views since the msgKey may not be unique.
+ *
+ * @param aMsgKey - key to find.
+ * @param aExpand - whether to expand a collapsed thread to find the key.
+ *
+ * @return - view index of msg hdr, -1 if hdr not found.
+ */
+ nsMsgViewIndex findIndexFromKey(in nsMsgKey aMsgKey, in boolean aExpand);
+ /**
+ * Finds the view index of the passed in msgHdr.
+ *
+ * @param aMsgHdr - hdr to find.
+ * @param aExpand - whether to expand a collapsed thread to find the hdr.
+ *
+ * @return - view index of msg hdr, -1 if hdr not found.
+ */
+ nsMsgViewIndex findIndexOfMsgHdr(in nsIMsgDBHdr aMsgHdr, in boolean aExpand);
+
+ /**
+ * Expands a thread and selects all it's member messages.
+ *
+ * @param aIndex - View index of a message in the thread to expand (can be
+ any message which is a member of the thread).
+ * @param aAugment - If true, Augment the existing selection.
+ * If false, replace it.
+ */
+ void ExpandAndSelectThreadByIndex(in nsMsgViewIndex aIndex, in boolean aAugment);
+
+ /**
+ * This method returns the nsIMsgThread object containing the header displayed
+ * at the desired row. For grouped views and cross folder saved searches,
+ * this will be the view thread, not the db thread.
+ *
+ * @param aIndex view index we want corresponding thread object of.
+ *
+ * @return the thread object at the requested view index
+ */
+ nsIMsgThread getThreadContainingIndex(in nsMsgViewIndex aIndex);
+
+ /**
+ * Insert rows into the view. The caller should use NoteChange() below to
+ * update the view.
+ *
+ * @param aIndex view index for insertion start.
+ * @param aNumRows number of rows to insert.
+ * @param aKey msgKey.
+ * @param aFlags msgFlags.
+ * @param aLevel treeview indent level.
+ * @param aFolder nsIMsgFolder, required for search/xfvf views.
+ */
+ void insertTreeRows(in nsMsgViewIndex aIndex, in unsigned long aNumRows,
+ in nsMsgKey aKey, in nsMsgViewFlagsTypeValue aFlags,
+ in unsigned long aLevel, in nsIMsgFolder aFolder);
+
+ /**
+ * Remove rows from the view. The caller should use NoteChange() below to
+ * update the view.
+ *
+ * @param aIndex view index for removal start.
+ * @param aNumRows number of rows to remove.
+ */
+ void removeTreeRows(in nsMsgViewIndex aIndex, in unsigned long aNumRows);
+
+ /**
+ * Notify tree that rows have changed.
+ *
+ * @param aFirstLineChanged first view index for changed rows.
+ * @param aNumRows number of rows changed; < 0 means removed.
+ * @param aChangeType changeType.
+ */
+ void NoteChange(in nsMsgViewIndex aFirstLineChanged, in long aNumRows,
+ in nsMsgViewNotificationCodeValue aChangeType);
+
+ /**
+ * Return the view thread corresponding to aMsgHdr. If we're a cross-folder
+ * view, then it would be the cross folder view thread, otherwise, the
+ * db thread object.
+ *
+ * @param aMsgHdr message header we want the view thread object of.
+ *
+ * @return view thread object for msg hdr.
+ */
+ nsIMsgThread getThreadContainingMsgHdr(in nsIMsgDBHdr aMsgHdr);
+
+ // use lines or kB for size?
+ readonly attribute boolean usingLines;
+
+ // Custom Column Implementation note: see nsIMsgCustomColumnHandler
+
+ // attaches a custom column handler to a specific column (can be a new column or a built in)
+ void addColumnHandler(in AString aColumn, in nsIMsgCustomColumnHandler aHandler);
+
+ // removes a custom column handler leaving the column to be handled by the system
+ void removeColumnHandler(in AString aColumn);
+
+ // returns the custom column handler attached to a specific column - if any
+ nsIMsgCustomColumnHandler getColumnHandler(in AString aColumn);
+
+ /**
+ * The custom column to use for sorting purposes (when sort type is
+ * nsMsgViewSortType.byCustom.)
+ */
+ attribute AString curCustomColumn;
+
+ /**
+ * The custom column used for a secondary sort, blank if secondarySort is
+ * not byCustom. The secondary sort design is such that the desired secondary
+ * is sorted first, followed by sort by desired primary. The secondary is
+ * read only, as it is set internally according to this design.
+ */
+ readonly attribute AString secondaryCustomColumn;
+ /**
+ * Scriptable accessor for the cell text for a column
+ *
+ * @param aRow - row we want cell text for
+ * @param aColumnName - name of column we want cell text for
+ *
+ * @returns The cell text for the given row and column, if any.
+ * @notes This does not work for custom columns yet.
+ */
+ AString cellTextForColumn(in long aRow, in AString aColumnName);
+
+ /**
+ * Get all of the data needed to display a row. Effectively a combination of
+ * CellTextForColumn, GetRowProperties and GetLevel, for performance reasons.
+ *
+ * @param aRow - Index of the row we want data for.
+ * @param aColumnNames - The column names we want cell text for.
+ * @param aProperties - The properties of the row.
+ * @param aThreadLevel - The thread level of the row.
+ *
+ * @returns The cell text for the columns in `aColumnNames`.
+ */
+ Array<AString> cellDataForColumns(in long aRow,
+ in Array<AString> aColumnNames,
+ out AString aProperties,
+ out long aThreadLevel);
+};
+
+/* this interface is rapidly morphing from a command updater interface into a more generic
+ FE updater interface to handle changes in the view
+*/
+
+[scriptable, uuid(ce8f52ee-e742-4b31-8bdd-2b3a8168a117)]
+interface nsIMsgDBViewCommandUpdater : nsISupports
+{
+ /* Eventually we'll flush this out into some kind of rich interface
+ which may take specific selection changed type notifications like
+ no selections, single selection, multi-selection, etc. For starters,
+ we are going to keep it generic. The back end will only push an update
+ command status when the # of selected items changes.
+ */
+
+ void updateCommandStatus();
+
+ /* displayed message has changed */
+ void displayMessageChanged(in nsIMsgFolder aFolder, in AString aSubject, in ACString aKeywords);
+
+ /**
+ * allows the backend to tell the front end to re-determine
+ * which message we should selet after a delete or move
+ */
+ void updateNextMessageAfterDelete();
+
+ /**
+ * tell the front end that the selection has changed, and may need to be
+ * resummarized.
+ *
+ * @return true if we did summarize, false otherwise.
+ */
+ boolean summarizeSelection();
+
+ /**
+ * Tell the front end that the selected message was removed and it should update.
+ */
+ void selectedMessageRemoved();
+};
+
+/**
+ * A shim of XULTreeElement, with only the methods called by nsMsgDBView.
+ */
+[scriptable, uuid(c5f6b1a2-f56a-49cb-b863-badf158206d5)]
+interface nsIMsgJSTree : nsISupports
+{
+ void beginUpdateBatch();
+ void endUpdateBatch();
+ void ensureRowIsVisible(in long index);
+ void invalidate();
+ void invalidateRange(in long startIndex, in long endIndex);
+ void rowCountChanged(in long index, in long count);
+ attribute long currentIndex;
+};
diff --git a/comm/mailnews/base/public/nsIMsgEnumerator.idl b/comm/mailnews/base/public/nsIMsgEnumerator.idl
new file mode 100644
index 0000000000..9ce2523a88
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgEnumerator.idl
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgThread;
+
+/**
+ * nsIJSIterator implements the JavaScript iterator protocol.
+ * For details on the JS side, see:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
+ */
+[scriptable, uuid(4406175e-1aa4-414c-ad7c-86bd76e102f8)]
+interface nsIJSIterator : nsISupports {
+ /**
+ * @returns {{value: Object, done: Boolean}} the iteration state.
+ * 'value' holds the next item, and 'done' flags the end of the iteration.
+ * (notionally, both 'value' and 'done' are always present, but a missing
+ * 'done' is considered to be false, and 'value' should be ignored if
+ * 'done' is true, according to the protocol).
+ */
+ [implicit_jscontext]
+ jsval next();
+};
+
+
+/**
+ * nsIMsgEnumerator is an object for iterating forward over an ordered set of
+ * messages (nsIMsgHdrs). There is no provision to reset the iteration.
+ */
+[scriptable, uuid(5760cb6d-1a71-485f-ad85-5a98a9da2104)]
+interface nsIMsgEnumerator : nsISupports {
+ /**
+ * Implements the JavaScript iterable protocol, which allows
+ * for...of constructs and the like.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
+ * NOTE: the nsIMsgEnumerator is "used up" by iterator(), just as it is by
+ * getNext()/hasMoreElements(). So you can only loop over the enumerator once.
+ */
+ [symbol]
+ nsIJSIterator iterator();
+
+ /**
+ * Called to determine whether or not there are any messages remaining
+ * to be retrieved from the enumerator.
+ *
+ * @returns true if getNext() may be called to fetch a message, or
+ * false if there are no more messages.
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Called to retrieve the next message in the set.
+ *
+ * @returns the next element in the enumeration.
+ */
+ nsIMsgDBHdr getNext();
+};
+
+/**
+ * nsIMsgThreadEnumerator is an object for iterating forward over an ordered set of
+ * message threads (nsIMsgThread). There is no provision to reset the iteration.
+ */
+[scriptable, uuid(e39ae1a8-2b94-4f2b-abb4-250171047501)]
+interface nsIMsgThreadEnumerator : nsISupports {
+ /**
+ * Implements the JavaScript iterable protocol, which allows
+ * for...of constructs and the like.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
+ * NOTE: the nsIMsgThreadEnumerator is "used up" by iterator(), just as it is by
+ * getNext()/hasMoreElements(). So you can only loop over the enumerator once.
+ */
+ [symbol]
+ nsIJSIterator iterator();
+
+ /**
+ * Called to determine whether or not there are any messages remaining
+ * to be retrieved from the enumerator.
+ *
+ * @returns true if getNext() may be called to fetch a message, or
+ * false if there are no more messages.
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Called to retrieve the next thread in the set.
+ *
+ * @returns the next element in the enumeration.
+ */
+ nsIMsgThread getNext();
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolder.idl b/comm/mailnews/base/public/nsIMsgFolder.idl
new file mode 100644
index 0000000000..4f0edb8818
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolder.idl
@@ -0,0 +1,877 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIFolderListener.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIMsgCopyServiceListener.idl"
+#include "nsIUrlListener.idl"
+#include "nsIMsgEnumerator.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIMsgDatabase;
+interface nsIDBFolderInfo;
+interface nsIMsgFilterList;
+interface nsIFile;
+interface nsIOutputStream;
+interface nsIInputStream;
+interface nsIFile;
+interface nsIMsgIdentity;
+interface nsIMsgThread;
+interface nsIMsgPluggableStore;
+
+typedef long nsMsgBiffState;
+
+// enumerated type for determining if a message has been replied to, forwarded, etc.
+typedef long nsMsgDispositionState;
+
+/*
+ * The contract ID for this component is @mozilla.org/msgFolder/msgFolderService;1.
+ */
+[scriptable, uuid(5639c204-48ac-4115-897f-3b16821fe118)]
+interface nsIMsgFolderService : nsISupports
+{
+ /**
+ * JS-callable service to initialize static variables in nsMsgDBFolder.cpp
+ * upon initialization or when locale changes.
+ */
+ void initializeFolderStrings();
+};
+
+[scriptable, uuid(5d253ba2-42aa-43a7-b584-0059855ababf)]
+interface nsIMsgFolder : nsISupports {
+
+ const nsMsgBiffState nsMsgBiffState_NewMail = 0; // User has new mail waiting.
+ const nsMsgBiffState nsMsgBiffState_NoMail = 1; // No new mail is waiting.
+ const nsMsgBiffState nsMsgBiffState_Unknown = 2; // We dunno whether there is new mail.
+
+ /* folder name properties */
+ readonly attribute AUTF8String URI;
+ attribute AString name;
+ attribute AString prettyName;
+ readonly attribute AString abbreviatedName;
+
+ /**
+ * Set pretty name again from original name,
+ * typically used when locale changes.
+ */
+ void setPrettyNameFromOriginal();
+
+ attribute nsIMsgFolder parent;
+
+ /// Returns an enumerator containing the messages within the current database.
+ readonly attribute nsIMsgEnumerator messages;
+
+ /**
+ * This method is called by the folder-lookup-service after constructing
+ * a folder to initialize its URI. You would not normally
+ * call this method directly.
+ *
+ * @param uri - The URI of the folder.
+ */
+ void Init(in AUTF8String uri);
+
+ void startFolderLoading();
+ void endFolderLoading();
+
+ void folderNamesReady(out boolean aReady);
+
+ /* get new headers for db */
+ void updateFolder(in nsIMsgWindow aWindow);
+
+ /**
+ * URL for this folder
+ */
+ readonly attribute AUTF8String folderURL;
+
+ /**
+ * should probably move to the server
+ */
+ readonly attribute boolean showDeletedMessages;
+
+ /**
+ * this folder's parent server
+ */
+ readonly attribute nsIMsgIncomingServer server;
+
+ /**
+ * is this folder the "phantom" server folder?
+ */
+ readonly attribute boolean isServer;
+ readonly attribute boolean canSubscribe;
+ readonly attribute boolean canFileMessages;
+ readonly attribute boolean noSelect; // this is an imap no select folder
+ readonly attribute boolean imapShared; // this is an imap shared folder
+ readonly attribute boolean canDeleteMessages; // can't delete from imap read-only
+
+ /**
+ * does this folder allow subfolders?
+ * for example, newsgroups cannot have subfolders, and the INBOX
+ * on some IMAP servers cannot have subfolders
+ */
+ readonly attribute boolean canCreateSubfolders;
+
+ /**
+ * can you change the name of this folder?
+ * for example, newsgroups
+ * and some special folders can't be renamed
+ */
+ readonly attribute boolean canRename;
+
+ readonly attribute boolean canCompact;
+
+ /**
+ * the phantom server folder
+ */
+ readonly attribute nsIMsgFolder rootFolder;
+
+ /**
+ * Get the server's list of filters. (Or in the case of news, the
+ * filter list for this newsgroup)
+ * This list SHOULD be used for all incoming messages.
+ *
+ * Since the returned nsIMsgFilterList is mutable, it is not necessary to call
+ * setFilterList after the filters have been changed.
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters
+ */
+ nsIMsgFilterList getFilterList(in nsIMsgWindow msgWindow);
+
+ /**
+ * Set the server's list of filters.
+ *
+ * Note that this does not persist the filter list. To change the contents
+ * of the existing filters, use getFilterList and mutate the values as
+ * appropriate.
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setFilterList(in nsIMsgFilterList filterList);
+
+ /**
+ * Get user editable filter list. This does not have to be the same as
+ * the filterlist above, typically depending on the users preferences.
+ * The filters in this list are not processed, but only to be edited by
+ * the user.
+ * @see getFilterList
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters
+ */
+ nsIMsgFilterList getEditableFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set user editable filter list.
+ * This does not persist the filterlist, @see setFilterList
+ * @see getEditableFilterList
+ * @see setFilterList
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setEditableFilterList(in nsIMsgFilterList aFilterList);
+
+ /**
+ * Force close the mail database associated with this folder.
+ */
+ void ForceDBClosed();
+ /**
+ * Close and backup a folder database prior to reparsing
+ *
+ * @param newName New name of the corresponding message folder.
+ * Used in rename to set the file name to match the renamed
+ * folder. Set to empty to use the existing folder name.
+ */
+ void closeAndBackupFolderDB(in ACString newName);
+
+ /**
+ * Delete the backing store of the folder, but not the folder itself.
+ */
+ [noscript]
+ void deleteStorage();
+
+ /**
+ * Delete this folder and its children, if any.
+ * Note: this may mean moving it to trash and/or requesting confirmation
+ * from the user, depending on implementation.
+ * So the deletion may not take place immediately (or at all!)
+ *
+ * @param msgWindow msgWindow to display status feedback in
+ */
+ void deleteSelf(in nsIMsgWindow msgWindow);
+
+ /**
+ * Delete the given subfolder of this folder.
+ * It does not need to be a direct child.
+ *
+ * @param folder a child subfolder to delete
+ * @param deleteStorage whether to also delete the folder storage on disk
+ */
+ void propagateDelete(in nsIMsgFolder folder, in boolean deleteStorage);
+
+ /**
+ * Delete the folder and all of its subfolders.
+ *
+ * @param deleteStorage whether to also delete the folder storage on disk
+ */
+ [noscript]
+ void recursiveDelete(in boolean deleteStorage);
+
+ /**
+ * Create a subfolder of the current folder with the passed in name.
+ * For IMAP, this will be an async operation and the folder won't exist
+ * until it is created on the server.
+ *
+ * @param folderName name of the folder to create.
+ * @param msgWindow msgWindow to display status feedback in.
+ *
+ * @exception NS_MSG_FOLDER_EXISTS
+ */
+ void createSubfolder(in AString folderName, in nsIMsgWindow msgWindow);
+
+ /**
+ * Adds the subfolder with the passed name to the folder hierarchy.
+ * This is used internally during folder discovery; It shouldn't be
+ * used to create folders since it won't create storage for the folder,
+ * especially for imap. Unless you know exactly what you're doing, you
+ * should be using createSubfolder + getChildNamed or createLocalSubfolder.
+ *
+ * @param aFolderName Name of the folder to add.
+ * @returns The folder added.
+ */
+ nsIMsgFolder addSubfolder(in AString aFolderName);
+
+ /* this method ensures the storage for the folder exists.
+ For local folders, it creates the berkeley mailbox if missing.
+ For imap folders, it subscribes to the folder if it exists,
+ or creates it if it doesn't exist
+ */
+ void createStorageIfMissing(in nsIUrlListener urlListener);
+
+ /**
+ * Compact this folder (Expunge _and_ compact, for IMAP folders).
+ *
+ * @param aListener Notified upon completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon completion,
+ * with a null URL.
+ * @param aMsgWindow For progress/status, can be null.
+ */
+ void compact(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+ /**
+ * Compact all folders in the account corresponding to this folder.
+ *
+ * @param aListener Notified upon completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon completion,
+ * with a null URL.
+ * @param aMsgWindow For progress/status, can be null.
+ */
+ void compactAll(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+
+ void emptyTrash(in nsIUrlListener aListener);
+
+ /**
+ * change the name of the folder
+ *
+ * @param name the new name of the folder
+ */
+ void rename(in AString name, in nsIMsgWindow msgWindow);
+ void renameSubFolders( in nsIMsgWindow msgWindow, in nsIMsgFolder oldFolder);
+
+ AString generateUniqueSubfolderName(in AString prefix,
+ in nsIMsgFolder otherFolder);
+
+ void updateSummaryTotals(in boolean force);
+ void summaryChanged();
+ /**
+ * get the total number of unread messages in this folder,
+ * or in all subfolders
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+ long getNumUnread(in boolean deep);
+
+ /**
+ * get the total number of messages in this folder,
+ * or in all subfolders
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+ long getTotalMessages(in boolean deep);
+
+ /**
+ * These functions are used for tricking the front end into thinking that we
+ * have more messages than are really in the DB. This is usually after an
+ * IMAP message copy where we don't want to do an expensive select until the
+ * user actually opens that folder. These functions are called when
+ * MSG_Master::GetFolderLineById is populating a MSG_FolderLine struct used
+ * by the FE.
+ */
+ readonly attribute long numPendingUnread;
+ readonly attribute long numPendingTotalMessages;
+ void changeNumPendingUnread(in long delta);
+ void changeNumPendingTotalMessages(in long delta);
+
+ /**
+ * does this folder have new messages
+ *
+ */
+ attribute boolean hasNewMessages;
+
+ /**
+ * Indicates whether this folder or any of its subfolders have new messages.
+ */
+ readonly attribute boolean hasFolderOrSubfolderNewMessages;
+
+ /**
+ * return the first new message in the folder
+ *
+ */
+ readonly attribute nsIMsgDBHdr firstNewMessage;
+
+ /**
+ * clear new status flag of all of the new messages
+ */
+ void clearNewMessages();
+
+ readonly attribute long long expungedBytes;
+
+ /**
+ * Can this folder be deleted?
+ * For example, special folders and isServer folders cannot be deleted.
+ */
+ readonly attribute boolean deletable;
+
+ /**
+ * should we be displaying recipients instead of the sender?
+ * for example, in the Sent folder, recipients are more relevant
+ * than the sender
+ */
+ readonly attribute boolean displayRecipients;
+
+ /**
+ * used to determine if it will take a long time to download all
+ * the headers in this folder - so that we can do folder notifications
+ * synchronously instead of asynchronously
+ */
+ readonly attribute boolean manyHeadersToDownload;
+
+ readonly attribute ACString relativePathName;
+
+ /**
+ * size of this folder on disk (not including .msf file)
+ * for imap, it's the sum of the size of the messages
+ */
+ attribute long long sizeOnDisk;
+
+ readonly attribute ACString username;
+ readonly attribute ACString hostname;
+
+ /**
+ * Sets a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to set on the folder.
+ */
+ void setFlag(in unsigned long flag);
+
+ /**
+ * Clears a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to clear on the folder.
+ */
+ void clearFlag(in unsigned long flag);
+
+ /**
+ * Determines if a flag is set on the folder or not. The known flags are
+ * defined in nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to check on the folder.
+ * @return True if the flag exists.
+ */
+ boolean getFlag(in unsigned long flag);
+
+ /**
+ * Toggles a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to toggle
+ */
+ void toggleFlag(in unsigned long flag);
+
+ /**
+ * Called to notify the database and/or listeners of a change of flag. The
+ * known flags are defined in nsMsgFolderFlags.h
+ *
+ * @note This doesn't need to be called for normal flag changes via
+ * the *Flag functions on this interface.
+ *
+ * @param flag The flag that was changed.
+ */
+ void onFlagChange(in unsigned long flag);
+
+ /**
+ * Direct access to the set/get all the flags at once.
+ */
+ attribute unsigned long flags;
+
+ /**
+ * Gets the first folder that has the specified flags set.
+ *
+ * @param flags The flag(s) to check for.
+ * @return The folder or the first available child folder that has
+ * the specified flags set, or null if there are none.
+ */
+ nsIMsgFolder getFolderWithFlags(in unsigned long flags);
+
+ /**
+ * Gets the folders that have the specified flag set.
+ *
+ * @param flags The flag(s) to check for.
+ * @return An array of folders that have the specified flags set.
+ * The array may have zero elements.
+ */
+ Array<nsIMsgFolder> getFoldersWithFlags(in unsigned long flags);
+
+ /**
+ * Check if this folder (or one of its ancestors) is special.
+ *
+ * @param flags The "special" flags to check.
+ * @param checkAncestors Should ancestors be checked too.
+ */
+ boolean isSpecialFolder(in unsigned long flags,
+ [optional] in boolean checkAncestors);
+
+ AUTF8String getUriForMsg(in nsIMsgDBHdr msgHdr);
+
+ /**
+ * Deletes the messages from the folder.
+ *
+ * @param messages The array of nsIMsgDBHdr objects to be deleted.
+ * @param msgWindow The standard message window object, for alerts et al.
+ * @param deleteStorage Whether or not the message should be truly deleted, as
+ opposed to moving to trash.
+ * @param isMove Whether or not this is a deletion for moving messages.
+ * @param allowUndo Whether this action should be undoable.
+ */
+ void deleteMessages(in Array<nsIMsgDBHdr> messages,
+ in nsIMsgWindow msgWindow,
+ in boolean deleteStorage, in boolean isMove,
+ in nsIMsgCopyServiceListener listener, in boolean allowUndo);
+
+ void copyMessages(in nsIMsgFolder srcFolder, in Array<nsIMsgDBHdr> messages,
+ in boolean isMove, in nsIMsgWindow msgWindow,
+ in nsIMsgCopyServiceListener listener, in boolean isFolder,
+ in boolean allowUndo);
+
+ void copyFolder(in nsIMsgFolder srcFolder, in boolean isMoveFolder,
+ in nsIMsgWindow msgWindow, in nsIMsgCopyServiceListener listener);
+
+ void copyFileMessage(in nsIFile file, in nsIMsgDBHdr msgToReplace,
+ in boolean isDraft, in unsigned long newMsgFlags,
+ in ACString aKeywords,
+ in nsIMsgWindow msgWindow,
+ in nsIMsgCopyServiceListener listener);
+
+ void acquireSemaphore(in nsISupports semHolder);
+ void releaseSemaphore(in nsISupports semHolder);
+ boolean testSemaphore(in nsISupports semHolder);
+ readonly attribute boolean locked;
+
+ void getNewMessages(in nsIMsgWindow aWindow, in nsIUrlListener aListener);
+
+ /**
+ * Write out summary data for this folder to the given folder cache.
+ */
+ void writeToFolderCache(in nsIMsgFolderCache folderCache, in boolean deep);
+
+ attribute unsigned long biffState;
+
+ /**
+ * The number of new messages since this folder's last biff.
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+
+ long getNumNewMessages(in boolean deep);
+
+ void setNumNewMessages(in long numNewMessages);
+
+ /**
+ * are we running a url as a result of the user clicking get msg?
+ */
+ attribute boolean gettingNewMessages;
+
+ /**
+ * local path of this folder
+ */
+ attribute nsIFile filePath;
+
+ /// an nsIFile corresponding to the .msf file.
+ readonly attribute nsIFile summaryFile;
+
+ readonly attribute AUTF8String baseMessageURI;
+ AUTF8String generateMessageURI(in nsMsgKey msgKey);
+
+ const nsMsgDispositionState nsMsgDispositionState_None = -1;
+ const nsMsgDispositionState nsMsgDispositionState_Replied = 0;
+ const nsMsgDispositionState nsMsgDispositionState_Forwarded = 1;
+ const nsMsgDispositionState nsMsgDispositionState_Redirected = 2;
+
+ void addMessageDispositionState(in nsIMsgDBHdr aMessage,
+ in nsMsgDispositionState aDispositionFlag);
+
+ void markMessagesRead(in Array<nsIMsgDBHdr> messages, in boolean markRead);
+ void markAllMessagesRead(in nsIMsgWindow aMsgWindow);
+ void markMessagesFlagged(in Array<nsIMsgDBHdr> messages, in boolean markFlagged);
+ void markThreadRead(in nsIMsgThread thread);
+
+ /**
+ * Gets the message database for the folder.
+ *
+ * Note that if the database is out of date, the implementation MAY choose to
+ * throw an error. For a handle to the database which MAY NOT throw an error,
+ * one can use getDBFolderInfoAndDB.
+ *
+ * The attribute can also be set to another database or to null to force the
+ * folder to reopen the same database when it is needed again.
+ *
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING If the database does not
+ * exist.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE If the database contains
+ * out of date information.
+ * @see nsIMsgFolder::getDBFolderInfoAndDB.
+ */
+ attribute nsIMsgDatabase msgDatabase;
+
+ /// Close the database if not open in folder.
+ void closeDBIfFolderNotOpen(in boolean forceClosed);
+
+ /// Does the folder have a local reference to the msgDatabase?
+ readonly attribute boolean databaseOpen;
+
+ /**
+ * Get the backup message database, used in reparsing. This database must
+ * be created first using closeAndBackupFolderDB()
+ *
+ * @return backup message database
+ */
+ nsIMsgDatabase getBackupMsgDatabase();
+ /**
+ * Remove the backup message database file
+ */
+ void removeBackupMsgDatabase();
+ /**
+ * Open the backup message database file
+ */
+ void openBackupMsgDatabase();
+ nsIMsgDatabase getDBFolderInfoAndDB(out nsIDBFolderInfo folderInfo);
+ nsIMsgDBHdr GetMessageHeader(in nsMsgKey msgKey);
+
+ readonly attribute boolean supportsOffline;
+ boolean shouldStoreMsgOffline(in nsMsgKey msgKey);
+ boolean hasMsgOffline(in nsMsgKey msgKey);
+
+ /**
+ * Get an offline store output stream for the passed message header.
+ *
+ * @param aHdr hdr of message to get outputstream for
+ * @returns An output stream to write to.
+ */
+ nsIOutputStream getOfflineStoreOutputStream(in nsIMsgDBHdr aHdr);
+
+ /**
+ * !!! DEPRECATED (Bug 1733849) !!!
+ * Use getLocalMsgStream() instead.
+ *
+ * Get an input stream for the passed message header. The stream will
+ * be positioned at the start of the message.
+ *
+ * @param aHdr hdr of message to get the input stream for.
+ * @returns an input stream to read the message from
+ */
+ nsIInputStream getMsgInputStream(in nsIMsgDBHdr aHdr);
+
+ /**
+ * Returns an input stream for reading a locally-stored message.
+ * (That means any message in a local folder, or a message marked as
+ * Offline on a non-local folder).
+ * The stream contains the single message.
+ * The returned stream should _not_ be considered seekable.
+ *
+ * @param hdr The message to get the input stream for.
+ * @returns Input stream to read the message from.
+ */
+ nsIInputStream getLocalMsgStream(in nsIMsgDBHdr aHdr);
+
+ void downloadMessagesForOffline(in Array<nsIMsgDBHdr> messages,
+ in nsIMsgWindow window);
+ nsIMsgFolder getChildWithURI(in AUTF8String uri, in boolean deep,
+ in boolean caseInsensitive);
+ void downloadAllForOffline(in nsIUrlListener listener, in nsIMsgWindow window);
+ /**
+ * Turn notifications on/off for various notification types. Currently only
+ * supporting allMessageCountNotifications which refers to both total and
+ * unread message counts.
+ */
+ const unsigned long allMessageCountNotifications = 0;
+ void enableNotifications(in long notificationType, in boolean enable);
+ boolean isCommandEnabled(in ACString command);
+ boolean matchOrChangeFilterDestination(in nsIMsgFolder folder,
+ in boolean caseInsensitive);
+ boolean confirmFolderDeletionForFilter(in nsIMsgWindow msgWindow);
+ void alertFilterChanged(in nsIMsgWindow msgWindow);
+ void throwAlertMsg(in string msgName, in nsIMsgWindow msgWindow);
+ AString getStringWithFolderNameFromBundle(in string msgName);
+ void notifyCompactCompleted();
+ /**
+ * Calculate ordering of this folder against another.
+ */
+ long compareSortKeys(in nsIMsgFolder msgFolder);
+
+ attribute nsIMsgRetentionSettings retentionSettings;
+ attribute nsIMsgDownloadSettings downloadSettings;
+ boolean callFilterPlugins(in nsIMsgWindow aMsgWindow);
+ /**
+ * used for order in the folder pane, folder pickers, etc.
+ */
+ attribute long sortOrder;
+
+ attribute nsIDBFolderInfo dBTransferInfo;
+
+ /**
+ * Set a string property on the folder.
+ */
+ void setStringProperty(in string propertyName, in AUTF8String propertyValue);
+
+ /**
+ * Returns the value of a string property.
+ * Throws an error if property does not exist.
+ */
+ AUTF8String getStringProperty(in string propertyName);
+
+ /* does not persist across sessions */
+ attribute nsMsgKey lastMessageLoaded;
+
+ /**
+ * Returns an array containing nsIMsgFolder items that are
+ * subfolders of the instance this is called on.
+ */
+ readonly attribute Array<nsIMsgFolder> subFolders;
+
+ /**
+ * Returns true if this folder has sub folders.
+ */
+ readonly attribute boolean hasSubFolders;
+
+ /**
+ * Returns the number of sub folders that this folder has.
+ */
+ readonly attribute unsigned long numSubFolders;
+
+ /**
+ * Determines if this folder is an ancestor of the supplied folder.
+ *
+ * @param folder The folder that may or may not be a descendent of this
+ * folder.
+ */
+ boolean isAncestorOf(in nsIMsgFolder folder);
+
+ /**
+ * Looks in immediate children of this folder for the given name.
+ *
+ * @param name the name of the target subfolder
+ */
+ boolean containsChildNamed(in AString name);
+
+ /**
+ * Return the child folder which the specified name.
+ *
+ * @param aName The name of the child folder to find
+ * @return The child folder
+ * @exception NS_ERROR_FAILURE Thrown if the folder with aName does not exist
+ */
+ nsIMsgFolder getChildNamed(in AString aName);
+
+ /**
+ * Finds the sub folder with the specified name.
+ *
+ * @param escapedSubFolderName The name of the sub folder to find.
+ * @note Even if the folder doesn't currently exist,
+ * a nsIMsgFolder may be returned.
+ */
+ nsIMsgFolder findSubFolder(in ACString escapedSubFolderName);
+
+ void AddFolderListener(in nsIFolderListener listener);
+ void RemoveFolderListener(in nsIFolderListener listener);
+
+ // These notification functions invoke the appropriate nsIFolderListener
+ // method on all the listeners attached to this folder, and _also_
+ // all the nsIFolderListeners registered in the MailSession.
+ // See nsIMsgMailSession AddFolderListener()/RemoveFolderListener().
+ void NotifyPropertyChanged(in ACString property,
+ in ACString oldValue,
+ in ACString newValue);
+ void NotifyIntPropertyChanged(in ACString property,
+ in long long oldValue,
+ in long long newValue);
+ void NotifyBoolPropertyChanged(in ACString property,
+ in boolean oldValue,
+ in boolean newValue);
+ void NotifyPropertyFlagChanged(in nsIMsgDBHdr item,
+ in ACString property,
+ in unsigned long oldValue,
+ in unsigned long newValue);
+ void NotifyUnicharPropertyChanged(in ACString property,
+ in AString oldValue,
+ in AString newValue);
+
+ void notifyMessageAdded(in nsIMsgDBHdr msg);
+ void notifyMessageRemoved(in nsIMsgDBHdr msg);
+ void notifyFolderAdded(in nsIMsgFolder child);
+ void notifyFolderRemoved(in nsIMsgFolder child);
+
+ void NotifyFolderEvent(in ACString event);
+
+ // Gets all descendants, not just first level children.
+ readonly attribute Array<nsIMsgFolder> descendants;
+ void Shutdown(in boolean shutdownChildren);
+
+ void copyDataToOutputStreamForAppend(in nsIInputStream aIStream,
+ in long aLength, in nsIOutputStream outputStream);
+ void copyDataDone();
+ void setJunkScoreForMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aJunkScore);
+ void applyRetentionSettings();
+
+ /**
+ * Get the beginning of the message bodies for the passed in keys and store
+ * them in the msg hdr property "preview". This is intended for
+ * new mail alerts, title tips on folders with new messages, and perhaps
+ * titletips/message preview in the thread pane.
+ *
+ * @param aKeysToFetch keys of msgs to fetch
+ * @param aUrlListener url listener to notify if we run url to fetch msgs
+ *
+ * @result aAsyncResults if true, we ran a url to fetch one or more of msg bodies
+ *
+ */
+ boolean fetchMsgPreviewText(in Array<nsMsgKey> aKeysToFetch,
+ in nsIUrlListener aUrlListener);
+
+ // used to set/clear tags - we could have a single method to setKeywords which
+ // would figure out the diffs, but these methods might be more convenient.
+ // keywords are space delimited, in the case of multiple keywords
+ void addKeywordsToMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aKeywords);
+ void removeKeywordsFromMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aKeywords);
+ /**
+ * Extract the message text from aStream.
+ *
+ * @param aStream stream to read from
+ * @param aCharset character set to use to interpret the body. If an empty string, then the
+ * charset is retrieved from the headers. msgHdr.charset is recommended in case you have it.
+ * @param aBytesToRead number of bytes to read from the stream. The function will read till the end
+ * of the line, and there will also be some read ahead due to NS_ReadLine
+ * @param aMaxOutputLen desired length of the converted message text. Used to control how many characters
+ * of msg text we want to store.
+ * @param aCompressQuotes Replace quotes and citations with " ... " in the preview text
+ * @param aStripHTMLTags strip HTML tags from the output, if present
+ * @param[out] aContentType the content type of the MIME part that was used to generate the text --
+ * for an HTML part, this will be "text/html" even though aStripHTMLTags might be true
+ */
+ AUTF8String getMsgTextFromStream(in nsIInputStream aStream, in ACString aCharset,
+ in unsigned long aBytesToRead, in unsigned long aMaxOutputLen,
+ in boolean aCompressQuotes, in boolean aStripHTMLTags,
+ out ACString aContentType);
+
+ AString convertMsgSnippetToPlainText(in AString aMessageText);
+
+ // this allows a folder to have a special identity. E.g., you might want to
+ // associate an identity with a particular newsgroup, or for IMAP shared folders in
+ // the other users namespace, you might want to create a delegated identity
+ readonly attribute nsIMsgIdentity customIdentity;
+
+ /**
+ * @{
+ * Processing flags, used to manage message processing.
+ *
+ * @param msgKey message key
+ * @return processing flags
+ */
+ unsigned long getProcessingFlags(in nsMsgKey msgKey);
+
+ /**
+ * @param msgKey message key
+ * @param mask mask to OR into the flags
+ */
+ void orProcessingFlags(in nsMsgKey msgKey, in unsigned long mask);
+
+ /**
+ * @param msgKey message key
+ * @param mask mask to AND into the flags
+ */
+ void andProcessingFlags(in nsMsgKey msgKey, in unsigned long mask);
+ /** @} */
+
+ /**
+ * Gets an inherited string property from the folder.
+ *
+ * If the forcePropertyEmpty boolean is set (see below), return an
+ * empty string.
+ *
+ * If the specified folder has a non-empty value for the property,
+ * return that value. Otherwise, return getInheritedStringProperty
+ * for the folder's parent.
+ *
+ * If a folder is the root folder for a server, then instead of
+ * checking the folder property, check the property of the same name
+ * for the server using nsIMsgIncomingServer.getCharValue(...)
+ *
+ * Note nsIMsgIncomingServer.getCharValue for a server inherits from
+ * the preference mail.server.default.(propertyName) as a global value
+ *
+ * (ex: if propertyName = "IAmAGlobal" and no folder nor server properties
+ * are set, then the inherited property will return the preference value
+ * mail.server.default.IAmAGlobal)
+ *
+ * If the propertyName is undefined, returns an empty, void string.
+ *
+ * @param propertyName The name of the property for the value to retrieve.
+ */
+ ACString getInheritedStringProperty(in string propertyName);
+
+ /**
+ * Set a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ * @param aForcePropertyEmpty true if an empty inherited property should be returned
+ */
+ void setForcePropertyEmpty(in string propertyName, in boolean aForcePropertyEmpty);
+
+ /**
+ * Get a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ *
+ * @return true if an empty inherited property should be returned
+ */
+ boolean getForcePropertyEmpty(in string propertyName);
+
+ /**
+ * Pluggable store for this folder. Currently, this will always be the same
+ * as the pluggable store for the server.
+ */
+ readonly attribute nsIMsgPluggableStore msgStore;
+
+ /**
+ * Protocol type, i.e. "pop3", "imap", "nntp", "none", etc
+ * used to construct URLs for this account type.
+ */
+ readonly attribute ACString incomingServerType;
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCache.idl b/comm/mailnews/base/public/nsIMsgFolderCache.idl
new file mode 100644
index 0000000000..b5ba40afdc
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCache.idl
@@ -0,0 +1,48 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIMsgFolderCacheElement;
+
+/**
+ * nsIMsgFolderCache is a store of values which might be slow for the folder
+ * to calculate. For example: the number of unread messages.
+ * The account manager holds the cache, and each folder manipulates its cached
+ * properties via nsIMsgFolderCacheElement.
+ */
+[scriptable, uuid(78C2B6A2-E29F-44de-9543-10DBB51E245C)]
+interface nsIMsgFolderCache : nsISupports
+{
+ /**
+ * Set up the cache, loading/saving to cacheFile.
+ * If a new-style cacheFile isn't found, it looks for an old panacea.dat,
+ * specified by legacyFile and migrates it to the new format.
+ * Neither file has to exist - it'll just start up with an empty cache.
+ * nsMsgFolderCache (the only implementation) will autosave to cacheFile
+ * when changes are made.
+ *
+ * @param cacheFile File to persist the cache data (folderCache.json).
+ * @param legacyFile Old panacea.dat file to check for and migrate, if
+ * cacheFile doesn't exist.
+ */
+ void init(in nsIFile cacheFile, in nsIFile legacyFile);
+
+ /**
+ * Return an nsIMsgFolderCacheElement for a given folder.
+ * Unless createIfMissing is set, a missing entry will cause failure.
+ */
+ nsIMsgFolderCacheElement getCacheElement(in ACString key, in boolean createIfMissing);
+ void removeElement(in ACString key);
+
+ /**
+ * Write immediately to cacheFile if any data has been changed.
+ * Write immediately to cacheFile if any data has been changed.
+ * This happens in the cache dtor anyway, but we use it during shutdown and
+ * in unit testing (so tests don't have to wait for JS garbage collection).
+ */
+ void flush();
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl b/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl
new file mode 100644
index 0000000000..dd4b54c4f0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Interface for a folder to get/set its values in the foldercache.
+ */
+[scriptable, uuid(c7392b12-f68a-46b2-af5e-d47350bb17c3)]
+interface nsIMsgFolderCacheElement : nsISupports
+{
+ readonly attribute ACString key;
+ // Notes on the getCached...() functions:
+ // - They will fail if the property doesn't exist. That is, they'll
+ // throw an exception in JS, or return an NS_ERROR_* code in C++.
+ // - null values are returned as empty string (for getCachedString()),
+ // or zero (for the numeric accessors). NOTE: there should be no
+ // way to actually set properties to null, but the
+ // panacea.dat->folderCache.json might have introduced some, so we
+ // need to handle them.
+ // - On the C++ side there is legacy code which calls these functions
+ // without checking the error code and relies on the return value
+ // remaining unchanged if the function fails.
+ AUTF8String getCachedString(in string propertyName);
+ long getCachedInt32(in string propertyName);
+ unsigned long getCachedUInt32(in string propertyName);
+ long long getCachedInt64(in string propertyName);
+
+ void setCachedString(in string propertyName, in AUTF8String propertyValue);
+ void setCachedInt32(in string propertyName, in long propertyValue);
+ void setCachedUInt32(in string propertyName, in unsigned long propertyValue);
+ void setCachedInt64(in string propertyName, in long long propertyValue);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCompactor.idl b/comm/mailnews/base/public/nsIMsgFolderCompactor.idl
new file mode 100644
index 0000000000..96df873f32
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCompactor.idl
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIUrlListener;
+
+[scriptable, uuid(38c7e876-3083-4aea-8dcd-0ea0ec1753a3)]
+
+/**
+ * Use this for any object that wants to handle compacting folders.
+ * Currently, the folders themselves create this object.
+ */
+
+interface nsIMsgFolderCompactor : nsISupports
+{
+ /**
+ * Compact the passed in array of folders.
+ *
+ * @param folders The folders to compact.
+ * @param listener Notified of completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon
+ * completion, with a null URL.
+ * @param window Used for progress/status, can be null.
+ */
+ void compactFolders(in Array<nsIMsgFolder> folders,
+ in nsIUrlListener listener,
+ in nsIMsgWindow window);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderListener.idl b/comm/mailnews/base/public/nsIMsgFolderListener.idl
new file mode 100644
index 0000000000..34932a3361
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderListener.idl
@@ -0,0 +1,227 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgFolder;
+
+/**
+ * nsIMsgFolderListener defines the callbacks which are invoked by
+ * nsIMsgFolderNotificationService.
+ *
+ * This is similar to nsIFolderListener, but with slightly different semantics,
+ * especially w.r.t. moving messages and folders. Some listeners want to know
+ * about moves, instead of getting an itemAdded and itemRemoved notification.
+ * Folder listeners also only tend to get called if a view is open on the folder,
+ * which is not always the case. I don't want to change nsIFolderListener at this
+ * point since there are lots of extensions that rely on it. Eventually,
+ * these two interfaces should be combined somehow.
+ */
+
+[scriptable, uuid(2f87be72-0565-4e64-a824-0eb9c258f884)]
+interface nsIMsgFolderListener : nsISupports {
+ /**
+ * Notified immediately after a message is added to a folder. This could be a
+ * new incoming message to a local folder, or a new message in an IMAP folder
+ * when it is opened.
+ *
+ * You may want to consider using the msgsClassified notification instead of
+ * this notification if any of the following are true:
+ *
+ * - You only want to be notified about messages after junk classification
+ * has occurred (if it is going to occur for a message). This also goes for
+ * trait classification which is a generic use of the bayesian engine at
+ * the heart of the spam logic.
+ *
+ * - You only want to be notified about messages after all filters have been
+ * run. Although some filters may be run before the msgAdded notification
+ * is generated, filters dependent on junk/trait classification wait until
+ * classification completes.
+ *
+ * @param aMsg The message header that was just added
+ */
+ void msgAdded(in nsIMsgDBHdr aMsg);
+
+ /**
+ * Notification that (new to the client) messages have been through junk and
+ * trait classification. This event will occur for all messages at some point
+ * after their existence is revealed by msgAdded.
+ *
+ * Because junk classification does not run if no messages have ever been
+ * marked as junk by the user, it is possible to receive this message without
+ * any classification having actually been performed. We still generate the
+ * notification in this case so that code is reliably notified about the
+ * existence of the new message headers.
+ *
+ * @param aMsgs The message headers that have been classified or were
+ * intentionally not classified.
+ * @param aJunkProcessed Were the messages processed for junk classification?
+ * @param aTraitProcessed Were the messages processed for trait
+ * classification?
+ */
+ void msgsClassified(in Array<nsIMsgDBHdr> aMsgs, in boolean aJunkProcessed,
+ in boolean aTraitProcessed);
+
+ /**
+ * msgsJunkStatusChanged indicates that some messages that had already been
+ * reported by msgsClassified have had their junk status changed. This
+ * event will not fire for the initial automatic classification of
+ * messages; msgsClassified will tell you about those messages.
+ * This is not guaranteed to be a comprehensive source of junk
+ * notification events; right now any time an nsMsgDBView marks things as
+ * junk/non-junk a notification is produced.
+ *
+ * @param {nsIMsgDBHdr[]} messages - The affected messages.
+ *
+ */
+ void msgsJunkStatusChanged(in Array<nsIMsgDBHdr> messages);
+
+ /**
+ * Notified after a command to delete a group of messages has been given, but before the
+ * messages have actually been deleted.
+ *
+ * @param aMsgs An array of the message headers about to be deleted
+ *
+ * @note
+ * This notification will not take place if the messages are being deleted from the folder
+ * as the result of a move to another folder. Instead, the msgsMoveCopyCompleted() notification
+ * takes place.
+ *
+ * @note
+ * "Deleting" to a trash folder is actually a move, and is covered by msgsMoveCopyCompleted()
+ *
+ * @note
+ * If the user has selected the IMAP delete model (marking messages as deleted, then purging them
+ * later) for an IMAP account, this notification will not take place on the delete. This will only
+ * take place on the purge.
+ */
+ void msgsDeleted(in Array<nsIMsgDBHdr> aMsgs);
+
+ /**
+ * Notified after a command to move or copy a group of messages completes. In
+ * case of a move, this is before the messages have been deleted from the
+ * source folder.
+ *
+ * @param aMove true if a move, false if a copy
+ * @param aSrcMsgs An array of the message headers in the source folder
+ * @param aDestFolder The folder these messages were moved to.
+ * @param aDestMsgs This provides the list of target message headers.
+ For imap messages, these will be "pseudo" headers, with
+ a made up UID. When we download the "real" header, we
+ will send a msgKeyChanged notification. Currently, if
+ the imap move/copy happens strictly online (essentially,
+ not user-initiated), then aDestMsgs will be null.
+ *
+ * @note
+ * If messages are moved from a server which uses the IMAP delete model,
+ * you'll get aMove = false. That's because the messages are not deleted from
+ * the source database, but instead simply marked deleted.
+ */
+ void msgsMoveCopyCompleted(in boolean aMove,
+ in Array<nsIMsgDBHdr> aSrcMsgs,
+ in nsIMsgFolder aDestFolder,
+ in Array<nsIMsgDBHdr> aDestMsgs);
+
+ /**
+ * Notification sent when the msg key for a header may have changed.
+ * This is used when we create a header for an offline imap move result,
+ * without knowing what the ultimate UID will be. When we download the
+ * headers for the new message, we replace the old "pseudo" header with
+ * a new header that has the correct UID/message key. The uid of the new hdr
+ * may turn out to be the same as aOldKey if we've guessed correctly but
+ * the listener can use this notification to know that it can ignore the
+ * msgAdded notification that's coming for aNewHdr. We do NOT send a
+ * msgsDeleted notification for the pseudo header.
+ *
+ * @param aOldKey The fake UID. The header with this key has been removed
+ * by the time this is called.
+ * @param aNewHdr The header that replaces the header with aOldKey.
+ */
+ void msgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * msgUnincorporatedMoved: A message received via POP was moved by a
+ * "before junk" rule.
+ *
+ * @param {nsIMsgFolder} srcFolder - Folder the message was moved from.
+ * @param {nsIMsgDBHdr} msg - The message.
+ */
+ void msgUnincorporatedMoved(in nsIMsgFolder srcFolder, in nsIMsgDBHdr msg);
+
+ /**
+ * Notified after a folder has been added.
+ *
+ * @param aFolder The folder that has just been added
+ */
+ void folderAdded(in nsIMsgFolder aFolder);
+
+ /**
+ * Notified after a folder has been deleted and its corresponding file(s) deleted from disk.
+ *
+ * @param aFolder The folder that has just been deleted
+ *
+ * @note
+ * "Deleting" to a trash folder is actually a move, and is covered by folderMoveCopyCompleted()
+ */
+ void folderDeleted(in nsIMsgFolder aFolder);
+
+ /**
+ * Notified after a command to move or copy a folder completes. In case of a move, at this point,
+ * the original folder and its files have already been moved to the new location.
+ *
+ * @param aMove true if a move, false if a copy
+ * @param aSrcFolder The original folder that was moved
+ * @param aDestFolder The parent folder this folder was moved to
+ */
+ void folderMoveCopyCompleted(in boolean aMove,
+ in nsIMsgFolder aSrcFolder,
+ in nsIMsgFolder aDestFolder);
+
+ /**
+ * Notified after a folder is renamed.
+ *
+ * @param aOrigFolder The folder with the old name
+ * @param aNewFolder The folder with the new name
+ */
+ void folderRenamed(in nsIMsgFolder aOrigFolder, in nsIMsgFolder aNewFolder);
+
+
+ /**
+ * Called to indicate nsIMsgFolderCompactor is beginning compaction of the
+ * folder. If the summary file was missing or out-of-date and a parse
+ * is required, this notification will come after the completion of the
+ * parse. The compactor will be holding the folder's semaphore when
+ * this notification is generated. This only happens for local folders
+ * currently.
+ *
+ * @param {nsIMsgFolder} folder - Target folder of the compaction.
+ */
+ void folderCompactStart(in nsIMsgFolder folder);
+
+
+ /**
+ * Called when nsIMsgFolderCompactor has completed compaction of the folder.
+ * At this point, the folder semaphore has been released and the database
+ * has been committed.
+ *
+ * @param {nsIMsgFolder} folder - Target folder of the compaction.
+ */
+ void folderCompactFinish(in nsIMsgFolder folder);
+
+ /**
+ * The user has opted to rebuild the mork msf index for a folder.
+ * Following this notification, the database will be closed, backed up
+ * (so that header properties can be propagated), and then rebuilt from the
+ * source. The rebuild is triggered by a call to updateFolder, so an
+ * nsIFolderListener OnFolderEvent(folder, FolderLoaded atom) notification
+ * will be received if you want to know when this is all completed.
+ * Note: this event is only generated for Thunderbird because the event
+ * currently comes from Thunderbird-specific code.
+ *
+ * @param {nsIMsgFolder} folder - The folder being reindexed.
+ */
+ void folderReindexTriggered(in nsIMsgFolder folder);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl b/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl
new file mode 100644
index 0000000000..79cf0fb7d2
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl
@@ -0,0 +1,119 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgFolder;
+interface nsIMsgFolderListener;
+
+typedef unsigned long msgFolderListenerFlag;
+
+
+/**
+ * nsIMsgFolderNotificationService provides a central point for sending out
+ * notifications related to folders.
+ * nsIMsgFolderListeners are registered with the service along with flags to
+ * indicate which kinds of notifications are of interest.
+ */
+[scriptable, uuid(e54a592c-2f23-4771-9670-bdb9d4f5dbbd)]
+interface nsIMsgFolderNotificationService : nsISupports {
+ /**
+ * @name Notification flags
+ * These flags determine which notifications will be sent.
+ * @{
+ */
+ /// nsIMsgFolderListener::msgAdded notification
+ const msgFolderListenerFlag msgAdded = 0x1;
+
+ /// nsIMsgFolderListener::msgsDeleted notification
+ const msgFolderListenerFlag msgsDeleted = 0x2;
+
+ /// nsIMsgFolderListener::msgsMoveCopyCompleted notification
+ const msgFolderListenerFlag msgsMoveCopyCompleted = 0x4;
+
+ /// nsIMsgFolderListener::msgsClassified notification
+ const msgFolderListenerFlag msgsClassified = 0x8;
+
+ /// nsIMsgFolderListener::msgsJunkStatusChanged notification
+ const msgFolderListenerFlag msgsJunkStatusChanged = 0x10;
+
+ /// nsIMsgFolderListener::msgUnincorporatedMoved notification
+ const msgFolderListenerFlag msgUnincorporatedMoved = 0x20;
+
+ /// nsIMsgFolderListener::folderAdded notification
+ const msgFolderListenerFlag folderAdded = 0x8000;
+
+ /// nsIMsgFolderListener::folderDeleted notification
+ const msgFolderListenerFlag folderDeleted = 0x1000;
+
+ /// nsIMsgFolderListener::folderMoveCopyCompleted notification
+ const msgFolderListenerFlag folderMoveCopyCompleted = 0x2000;
+
+ /// nsIMsgFolderListener::folderRenamed notification
+ const msgFolderListenerFlag folderRenamed = 0x4000;
+
+ /// nsIMsgFolderListener::folderCompactStart notification
+ const msgFolderListenerFlag folderCompactStart = 0x10000;
+
+ /// nsIMsgFolderListener::folderCompactFinish notification
+ const msgFolderListenerFlag folderCompactFinish = 0x20000;
+
+ /// nsIMsgFolderListener::folderReindexTriggered notification
+ const msgFolderListenerFlag folderReindexTriggered = 0x40000;
+
+ /// nsIMsgFolderListener::msgKeyChanged notification
+ const msgFolderListenerFlag msgKeyChanged = 0x2000000;
+
+ /** @} */
+
+ readonly attribute boolean hasListeners;
+ void addListener(in nsIMsgFolderListener aListener,
+ in msgFolderListenerFlag flags);
+ void removeListener(in nsIMsgFolderListener aListener);
+
+ // message-specific functions
+ // single message for added, array for delete/move/copy
+ void notifyMsgAdded(in nsIMsgDBHdr aMsg);
+ void notifyMsgsClassified(in Array<nsIMsgDBHdr> aMsgs,
+ in boolean aJunkProcessed,
+ in boolean aTraitProcessed);
+ void notifyMsgsJunkStatusChanged(in Array<nsIMsgDBHdr> messages);
+ void notifyMsgsDeleted(in Array<nsIMsgDBHdr> aMsgs);
+ void notifyMsgsMoveCopyCompleted(in boolean aMove,
+ in Array<nsIMsgDBHdr> aSrcMsgs,
+ in nsIMsgFolder aDestFolder,
+ in Array<nsIMsgDBHdr> aDestMsgs);
+
+ /**
+ * Notify listeners that the msg key for a header has changed. Currently,
+ * this is used when we create a header for an offline imap move result,
+ * without knowing what the ultimate UID will be. When we download the
+ * headers for the new message, we replace the old "pseudo" header with
+ * a new header that has the correct UID/message key, by cloning the pseudo
+ * header, which maintains all the existing header attributes.
+ *
+ * @param aOldKey The fake UID. The header with this key has been removed
+ * by the time this is called.
+ * @param aNewHdr The header that replaces the header with aOldKey.
+ */
+ void notifyMsgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
+
+ void notifyMsgUnincorporatedMoved(in nsIMsgFolder srcFolder, in nsIMsgDBHdr msg);
+
+ // folder specific functions
+ // single folders, all the time
+ void notifyFolderAdded(in nsIMsgFolder aFolder);
+ void notifyFolderDeleted(in nsIMsgFolder aFolder);
+ void notifyFolderMoveCopyCompleted(in boolean aMove,
+ in nsIMsgFolder aSrcFolder,
+ in nsIMsgFolder aDestFolder);
+ void notifyFolderRenamed(in nsIMsgFolder aOrigFolder,
+ in nsIMsgFolder aNewFolder);
+
+ void notifyFolderCompactStart(in nsIMsgFolder folder);
+ void notifyFolderCompactFinish(in nsIMsgFolder folder);
+ void notifyFolderReindexTriggered(in nsIMsgFolder folder);
+};
diff --git a/comm/mailnews/base/public/nsIMsgHdr.idl b/comm/mailnews/base/public/nsIMsgHdr.idl
new file mode 100644
index 0000000000..14dc6cddf1
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgHdr.idl
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIUTF8StringEnumerator;
+
+[scriptable, uuid(3c11ddbe-c805-40c5-b9c9-d065fad5d0be)]
+interface nsIMsgDBHdr : nsISupports
+{
+ void setStringProperty(in string propertyName, in AUTF8String propertyValue);
+ AUTF8String getStringProperty(in string propertyName);
+ unsigned long getUint32Property(in string propertyName);
+ void setUint32Property(in string propertyName,
+ in unsigned long propertyVal);
+
+ // accessors, to make our JS cleaner
+ readonly attribute boolean isRead;
+ readonly attribute boolean isFlagged;
+
+ // Special accessor that checks if a message is part of an ignored subthread
+ readonly attribute boolean isKilled;
+
+ // Mark message routines
+ void markRead(in boolean read);
+ void markFlagged(in boolean flagged);
+ void markHasAttachments(in boolean hasAttachments);
+
+ attribute nsMsgPriorityValue priority;
+
+ /* flag handling routines */
+ attribute unsigned long flags;
+ unsigned long orFlags(in unsigned long flags);
+ unsigned long andFlags(in unsigned long flags);
+
+ /* various threading stuff */
+ attribute nsMsgKey threadId;
+ attribute nsMsgKey messageKey;
+ attribute nsMsgKey threadParent;
+
+ /* meta information about the message, learned from reading the message */
+
+ /**
+ * For "Offline" supporting folders (IMAP, NNTP), .messageSize is
+ * the size of the original message on the server.
+ * For Local folders, this is the exact size of the message as written to
+ * the msgStore.
+ * See also Bug 1764857.
+ */
+ attribute unsigned long messageSize;
+ attribute unsigned long lineCount;
+ /**
+ * The offset into the local folder/offline store of the message. This
+ * will be pluggable store-dependent, e.g., for mail dir it should
+ * always be 0.
+ */
+ attribute unsigned long long messageOffset;
+ /**
+ * For "Offline" supporting folders (IMAP, NNTP): .offlineMessageSize is
+ * the exact size of the local copy of the message in the msgStore.
+ * If the message is not flagged Offline, this will be zero or unset.
+ * For Local folders, this is unset or zero.
+ * See also Bug 1764857.
+ */
+ attribute unsigned long offlineMessageSize;
+ /* common headers */
+ attribute PRTime date;
+ readonly attribute unsigned long dateInSeconds;
+ attribute string messageId;
+ attribute string ccList;
+ attribute string bccList;
+ attribute string author;
+ attribute AUTF8String subject;
+ attribute string recipients;
+
+ /* anything below here still has to be fixed */
+ void setReferences(in AUTF8String references);
+ readonly attribute unsigned short numReferences;
+ AUTF8String getStringReference(in long refNum);
+
+ readonly attribute AString mime2DecodedAuthor;
+ readonly attribute AString mime2DecodedSubject;
+ readonly attribute AString mime2DecodedRecipients;
+
+ Array<octet> getAuthorCollationKey();
+ Array<octet> getSubjectCollationKey();
+ Array<octet> getRecipientsCollationKey();
+
+ attribute string charset;
+
+ /**
+ * Returns the effective character set for the message (@ref charset).
+ * For NNTP, if there is no specific set defined for the message,
+ * the character set of the server instead.
+ */
+ readonly attribute ACString effectiveCharset;
+
+ attribute string accountKey;
+ readonly attribute nsIMsgFolder folder;
+
+ /// Array of names of all database properties in the header.
+ readonly attribute Array<AUTF8String> properties;
+};
+/* *******************************************************************************/
diff --git a/comm/mailnews/base/public/nsIMsgIdentity.idl b/comm/mailnews/base/public/nsIMsgIdentity.idl
new file mode 100644
index 0000000000..f02a240694
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgIdentity.idl
@@ -0,0 +1,311 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIFile.idl"
+
+/**
+ * This interface contains all the personal outgoing mail information
+ * for a given person.
+ * Each identity is identified by a key, which is the <id> string in
+ * the identity preferences, such as in mail.identity.<id>.replyTo.
+ */
+[scriptable, uuid(9dede9a0-f6fc-4afc-8fc9-a6af52414b3d)]
+interface nsIMsgIdentity : nsISupports {
+ /**
+ * Internal preferences ID.
+ */
+ attribute ACString key;
+
+ /**
+ * A unique identifier for this identity that can be used for the same
+ * identity synced across multiple profiles. Auto-generated on first use.
+ */
+ attribute AUTF8String UID;
+
+ /**
+ * Label describing this identity. May be empty.
+ */
+ attribute AString label;
+
+ /**
+ * Pretty display name to identify this specific identity. Will return a
+ * composed string like "fullname <email> (label)".
+ */
+ readonly attribute AString identityName;
+
+ /**
+ * User's full name, i.e. John Doe.
+ */
+ attribute AString fullName;
+
+ /**
+ * User's e-mail address, i.e. john@doe.com.
+ */
+ attribute ACString email;
+
+ /**
+ * Do we use multiple e-mail addresses (like Catch-All) with this identity?
+ */
+ attribute boolean catchAll;
+
+ /**
+ * Hint for when to use this identity as catch all. It is a comma separated
+ * list of things to look for delivery in headers when replying to a message
+ * that was not directly addressed to a matching identity.
+ */
+ attribute AUTF8String catchAllHint;
+
+ /**
+ * Formats fullName and email into the proper string to use as sender:
+ * name <email>
+ */
+ readonly attribute AString fullAddress;
+
+ /**
+ * Optional replyTo address, i.e. johnNOSPAM@doe.com.
+ */
+ attribute AUTF8String replyTo;
+
+ /**
+ * Optional organization.
+ */
+ attribute AString organization;
+
+ /**
+ * Should we compose with HTML by default?
+ */
+ attribute boolean composeHtml;
+
+ /**
+ * Should we attach a signature from file?
+ */
+ attribute boolean attachSignature;
+
+ /**
+ * Should we attach a vcard by default?
+ */
+ attribute boolean attachVCard;
+
+ /**
+ * Should we automatically quote the original message?
+ */
+ attribute boolean autoQuote;
+
+ /**
+ * What should our quoting preference be?
+ */
+ attribute long replyOnTop;
+
+ /**
+ * Should our signature be at the end of the quoted text when replying
+ * above it?
+ */
+ attribute boolean sigBottom;
+
+ /**
+ * Include a signature when forwarding a message?
+ */
+ attribute boolean sigOnForward;
+
+ /**
+ * Include a signature when replying to a message?
+ */
+ attribute boolean sigOnReply;
+
+ /**
+ * The current signature file.
+ */
+ attribute nsIFile signature;
+
+ /**
+ * Modification time of the signature file.
+ */
+ attribute long signatureDate;
+
+ /**
+ * Signature text if not read from file; format depends on htmlSigFormat.
+ */
+ attribute AString htmlSigText;
+
+ /**
+ * Does htmlSigText contain HTML? Use plain text if false.
+ */
+ attribute boolean htmlSigFormat;
+
+ /**
+ * Suppress the double-dash signature separator
+ */
+ attribute boolean suppressSigSep;
+
+ /**
+ * The encoded string representing the vcard.
+ */
+ attribute ACString escapedVCard;
+
+ attribute boolean doFcc;
+ /// URI for the fcc (Sent) folder
+ attribute AUTF8String fccFolder;
+ attribute boolean fccReplyFollowsParent;
+
+ /**
+ * @{
+ * these attributes control whether the special folder pickers for
+ * fcc, drafts,archives, and templates are set to pick between servers
+ * (e.g., Sent on accountName) or to pick any folder on any account.
+ * "0" means choose between servers; "1" means use the full folder picker.
+ */
+ attribute ACString fccFolderPickerMode;
+ attribute ACString draftsFolderPickerMode;
+ attribute ACString archivesFolderPickerMode;
+ attribute ACString tmplFolderPickerMode;
+ /** @} */
+
+ // Don't call bccSelf, bccOthers, and bccList directly, they are
+ // only used for migration and backward compatibility. Use doBcc
+ // and doBccList instead.
+ attribute boolean bccSelf;
+ attribute boolean bccOthers;
+ attribute ACString bccList;
+
+ attribute boolean doCc;
+ attribute AUTF8String doCcList;
+
+ attribute boolean doBcc;
+ attribute AUTF8String doBccList;
+ /**
+ * @{
+ * URIs for the special folders (drafts, templates, archive)
+ */
+ attribute AUTF8String draftFolder;
+ attribute AUTF8String archiveFolder;
+ attribute AUTF8String stationeryFolder;
+ /** @} */
+
+ attribute boolean archiveEnabled;
+ /**
+ * @{
+ * This attribute and constants control the granularity of sub-folders of the
+ * Archives folder - either messages go in the single archive folder, or a
+ * yearly archive folder, or in a monthly archive folder with a yearly
+ * parent folder. If the server doesn't support folders that both contain
+ * messages and have sub-folders, we will ignore this setting.
+ */
+ attribute long archiveGranularity;
+ const long singleArchiveFolder = 0;
+ const long perYearArchiveFolders = 1;
+ const long perMonthArchiveFolders = 2;
+ /// Maintain the source folder name when creating Archive subfolders
+ attribute boolean archiveKeepFolderStructure;
+ /** @} */
+
+ attribute boolean showSaveMsgDlg;
+ attribute ACString directoryServer;
+ attribute boolean overrideGlobalPref;
+ /**
+ * If this is false, don't append the user's domain
+ * to an autocomplete address with no matches
+ */
+ attribute boolean autocompleteToMyDomain;
+ /**
+ * valid determines if the UI should use this identity
+ * and the wizard uses this to determine whether or not
+ * to ask the user to complete all the fields
+ */
+ attribute boolean valid;
+
+ /**
+ * this is really dangerous. this destroys all pref values
+ * do not call this unless you know what you're doing!
+ */
+ void clearAllValues();
+
+ /**
+ * the preferred smtp server for this identity.
+ * if this is set, this the smtp server that should be used
+ * for the message send
+ */
+ attribute ACString smtpServerKey;
+
+ /**
+ * default request for return receipt option for this identity
+ * if this is set, the Return Receipt menu item on the compose
+ * window will be checked
+ */
+ readonly attribute boolean requestReturnReceipt;
+ readonly attribute long receiptHeaderType;
+
+ /**
+ * default request for DSN option for this identity
+ * if this is set, the DSN menu item on the compose
+ * window will be checked
+ */
+ readonly attribute boolean requestDSN;
+
+ /**
+ * If true, include supported Autocrypt headers whenever sending a
+ * plain text or OpenPGP message. (Ignored when sending S/MIME.)
+ */
+ attribute boolean sendAutocryptHeaders;
+
+ /**
+ * If true, automatically attach the user's own public OpenPGP key
+ * whenever adding an OpenPGP digital signature.
+ */
+ attribute boolean attachPgpKey;
+
+ /**
+ * If true, encrypt draft messages that are saved to disk or on the
+ * mail server. This requires that a personal OpenPGP key or S/MIME
+ * certificate is configured and valid, matching the selected
+ * encryption technology for the current message.
+ */
+ attribute boolean autoEncryptDrafts;
+
+ /*
+ * If true, when sending an OpenPGP encrypted message, encrypt the
+ * email's subject, too.
+ */
+ attribute boolean protectSubject;
+
+ /*
+ * The default encryption setting for new emails that aren't already
+ * in an encryption context. (When forwarding or replying to an
+ * encrypted message we always automatically turn on encryption.)
+ * 0: disable encryption
+ * 1: optional encryption (not implemented)
+ * 2: require encryption
+ */
+ attribute long encryptionPolicy;
+
+ /*
+ * If true, add a digital signature for messages that aren't using
+ * encryption. (A message that uses encryption will automatically
+ * have signing enabled.)
+ */
+ attribute boolean signMail;
+
+ /* copy the attributes of the identity we pass in */
+ void copy(in nsIMsgIdentity identity);
+
+ /**
+ * these generic getter / setters, useful for extending mailnews
+ * note, these attributes persist across sessions
+ */
+ AString getUnicharAttribute(in string name);
+ void setUnicharAttribute(in string name, in AString value);
+
+ ACString getCharAttribute(in string name);
+ void setCharAttribute(in string name, in ACString value);
+
+ boolean getBoolAttribute(in string name);
+ void setBoolAttribute(in string name, in boolean value);
+
+ long getIntAttribute(in string name);
+ void setIntAttribute(in string name, in long value);
+
+ /* useful for debugging */
+ AString toString();
+};
diff --git a/comm/mailnews/base/public/nsIMsgIncomingServer.idl b/comm/mailnews/base/public/nsIMsgIncomingServer.idl
new file mode 100644
index 0000000000..c1b4c7e16d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgIncomingServer.idl
@@ -0,0 +1,596 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgFolderCache;
+interface nsIMsgWindow;
+interface nsIMsgProtocolInfo;
+interface nsIMsgFilterList;
+interface nsIMsgRetentionSettings;
+interface nsIMsgDownloadSettings;
+interface nsISpamSettings;
+interface nsIMsgFilterPlugin;
+interface nsIUrlListener;
+interface nsIMsgDBHdr;
+interface nsIFile;
+interface nsIURI;
+interface nsIMsgPluggableStore;
+
+/*
+ * Interface for incoming mail/news host
+ * this is the base interface for all mail server types (imap, pop, nntp, etc)
+ * often you will want to add extra interfaces that give you server-specific
+ * attributes and methods.
+ */
+[scriptable, uuid(aa9a3389-9dac-41f1-9ec5-18287cfaa47c)]
+interface nsIMsgIncomingServer : nsISupports {
+
+ /**
+ * internal pref key - guaranteed to be unique across all servers
+ */
+ attribute ACString key;
+
+ /**
+ * A unique identifier for this server that can be used for the same
+ * server synced across multiple profiles. Auto-generated on first use.
+ */
+ attribute AUTF8String UID;
+
+ /**
+ * pretty name - should be "userid on hostname"
+ * if the pref is not set
+ */
+ attribute AString prettyName;
+
+ /**
+ * helper function to construct the pretty name in a server type
+ * specific way - e.g., mail for foo@test.com, news on news.mozilla.org
+ */
+ readonly attribute AString constructedPrettyName;
+
+ /**
+ * hostname of the server
+ */
+ attribute AUTF8String hostName;
+
+ /* port of the server */
+ attribute long port;
+
+ /**
+ * userid to log into the server
+ */
+ attribute AUTF8String username;
+
+ /**
+ * protocol type, i.e. "pop3", "imap", "nntp", "none", etc
+ * used to construct URLs
+ */
+ attribute ACString type;
+
+ /**
+ * The CLIENTID to use for this server.
+ * @see https://tools.ietf.org/html/draft-yu-imap-client-id-01
+ */
+ attribute ACString clientid;
+
+ /**
+ * Whether the CLIENTID feature above is enabled.
+ */
+ attribute boolean clientidEnabled;
+
+ /**
+ * The proper instance of nsIMsgProtocolInfo corresponding to this server type.
+ */
+ readonly attribute nsIMsgProtocolInfo protocolInfo;
+
+ readonly attribute AString accountManagerChrome;
+
+ /**
+ * The schema for the local mail store, such as "mailbox", "imap", or "news"
+ * used to construct URIs. The contractID for the nsIMsgMessageService
+ * implementation that will manage access to messages associated with this
+ * server is constructed using this type.
+ */
+ readonly attribute ACString localStoreType;
+
+ /**
+ * The schema for the nsIMsgDatabase implementation, such as "mailbox" or
+ * "imap", that will be used to construct the database instance used by
+ * message folders associated with this server.
+ */
+ readonly attribute ACString localDatabaseType;
+
+ // Perform specific tasks (reset flags, remove files, etc) for account user/server name changes.
+ void onUserOrHostNameChanged(in AUTF8String oldName, in AUTF8String newName,
+ in bool hostnameChanged);
+
+ /// cleartext utf16 version of the password
+ attribute AString password;
+
+ /**
+ * Attempts to get the password first from the password manager, if that
+ * fails it will attempt to get it from the user if aMsgWindow is supplied.
+ *
+ * @param aPromptString The text of the prompt if the user is prompted for
+ * password.
+ * @param aPromptTitle The title of the prompt if the user is prompted.
+ * @return The obtained password. Could be an empty password.
+ *
+ * @exception NS_ERROR_FAILURE The password could not be obtained.
+ *
+ * @note NS_MSG_PASSWORD_PROMPT_CANCELLED is a success code that is returned
+ * if the prompt was presented to the user but the user cancelled the
+ * prompt.
+ */
+ AString getPasswordWithUI(in AString aPromptString, in AString aPromptTitle);
+
+ /* forget the password in memory and in single signon database */
+ void forgetPassword();
+
+ /**
+ * Forget the password in memory which is cached for the session.
+ *
+ * @param modifyLogin Only relevant for nsImapIncomingServer override. When
+ * true and authentication method is oauth2, the password
+ * and user authenticated flag are not cleared.
+ */
+ void forgetSessionPassword(in boolean modifyLogin);
+
+ /* should we download whole messages when biff goes off? */
+ attribute boolean downloadOnBiff;
+
+ /* should we biff the server? */
+ attribute boolean doBiff;
+
+ /* how often to biff */
+ attribute long biffMinutes;
+
+ /* current biff state */
+ attribute unsigned long biffState;
+
+ /* are we running a url as a result of biff going off? (different from user clicking get msg) */
+ attribute boolean performingBiff;
+
+ /* the on-disk path to message storage for this server */
+ attribute nsIFile localPath;
+
+ /// message store to use for the folders under this server.
+ readonly attribute nsIMsgPluggableStore msgStore;
+
+ /* the RDF URI for the root mail folder */
+ readonly attribute AUTF8String serverURI;
+
+ /* the root folder for this server, even if server is deferred */
+ attribute nsIMsgFolder rootFolder;
+
+ /* root folder for this account
+ - if account is deferred, root folder of deferred-to account */
+ readonly attribute nsIMsgFolder rootMsgFolder;
+
+ /* are we already getting new Messages on the current server..
+ This is used to help us prevent multiple get new msg commands from
+ going off at the same time. */
+ attribute boolean serverBusy;
+
+ /**
+ * Is the server using a secure channel (SSL or STARTTLS).
+ */
+ readonly attribute boolean isSecure;
+
+ /**
+ * Authentication mechanism.
+ *
+ * @see nsMsgAuthMethod (in MailNewsTypes2.idl)
+ * Same as "mail.server...authMethod" pref
+ */
+ attribute nsMsgAuthMethodValue authMethod;
+
+ /**
+ * Whether to SSL or STARTTLS or not
+ *
+ * @see nsMsgSocketType (in MailNewsTypes2.idl)
+ * Same as "mail.server...socketType" pref
+ */
+ attribute nsMsgSocketTypeValue socketType;
+
+ /* empty trash on exit */
+ attribute boolean emptyTrashOnExit;
+
+ /**
+ * Get the server's list of filters.
+ *
+ * This SHOULD be the same filter list as the root folder's, if the server
+ * supports per-folder filters. Furthermore, this list SHOULD be used for all
+ * incoming messages.
+ *
+ * Since the returned nsIMsgFilterList is mutable, it is not necessary to call
+ * setFilterList after the filters have been changed.
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters.
+ */
+ nsIMsgFilterList getFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set the server's list of filters.
+ *
+ * Note that this does not persist the filter list. To change the contents
+ * of the existing filters, use getFilterList and mutate the values as
+ * appropriate.
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setFilterList(in nsIMsgFilterList aFilterList);
+
+ /**
+ * Get user editable filter list. This does not have to be the same as
+ * the filterlist above, typically depending on the users preferences.
+ * The filters in this list are not processed, but only to be edited by
+ * the user.
+ * @see getFilterList
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters.
+ */
+ nsIMsgFilterList getEditableFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set user editable filter list.
+ * This does not persist the filterlist, @see setFilterList
+ * @see getEditableFilterList
+ * @see setFilterList
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setEditableFilterList(in nsIMsgFilterList aFilterList);
+
+ /* we use this to set the default local path. we use this when migrating prefs */
+ void setDefaultLocalPath(in nsIFile aDefaultLocalPath);
+
+ /**
+ * Verify that we can logon
+ *
+ * @param aUrlListener - gets called back with success or failure.
+ * @param aMsgWindow nsIMsgWindow to use for notification callbacks.
+ * @return - the url that we run.
+ */
+ nsIURI verifyLogon(in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ /* do a biff */
+ void performBiff(in nsIMsgWindow aMsgWindow);
+
+ /* get new messages */
+ void getNewMessages(in nsIMsgFolder aFolder, in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+ /* this checks if a server needs a password to do biff */
+ readonly attribute boolean serverRequiresPasswordForBiff;
+
+ /* this gets called when the server is expanded in the folder pane */
+ void performExpand(in nsIMsgWindow aMsgWindow);
+
+ /* Write out all known folder data to folderCache */
+ void writeToFolderCache(in nsIMsgFolderCache folderCache);
+
+ /* close any server connections */
+ void closeCachedConnections();
+
+ /* ... */
+ void shutdown();
+
+ /**
+ * Get or set the value as determined by the preference tree.
+ *
+ * These methods MUST NOT fail if the preference is not set, and therefore
+ * they MUST have a default value. This default value is provided in practice
+ * by use of a default preference tree. The standard format for the pref
+ * branches are <tt>mail.server.<i>key</i>.</tt> for per-server preferences,
+ * such that the preference is <tt>mail.server.<i>key</i>.<i>attr</i></tt>.
+ *
+ * The attributes are passed in as strings for ease of access by the C++
+ * consumers of this method.
+ *
+ * @param attr The value for which the preference should be accessed.
+ * @param value The value of the preference to set.
+ * @return The value of the preference.
+ * @{
+ */
+ boolean getBoolValue(in string attr);
+ void setBoolValue(in string attr, in boolean value);
+
+ ACString getCharValue(in string attr);
+ void setCharValue(in string attr, in ACString value);
+
+ AString getUnicharValue(in string attr);
+ void setUnicharValue(in string attr, in AString value);
+
+ long getIntValue(in string attr);
+ void setIntValue(in string attr, in long value);
+ /** @} */
+
+ /**
+ * Get or set the value as determined by the preference tree.
+ *
+ * These methods MUST NOT fail if the preference is not set, and therefore
+ * they MUST have a default value. This default value is provided in practice
+ * by use of a default preference tree. The standard format for the pref
+ * branches are <tt>mail.server.<i>key</i>.</tt> for per-server preferences,
+ * such that the preference is <tt>mail.server.<i>key</i>.<i>attr</i></tt>.
+ *
+ * The attributes are passed in as strings for ease of access by the C++
+ * consumers of this method.
+ *
+ * There are two preference names on here for legacy reasons, where the first
+ * is the name which will be using a (preferred) relative preference and the
+ * second a deprecated absolute preference. Implementations that do not have
+ * to worry about supporting legacy preferences can safely ignore this second
+ * parameter. Callers must still provide a valid value, though.
+ *
+ * @param relpref The name of the relative file preference.
+ * @param absref The name of the absolute file preference.
+ * @param aValue The value of the preference to set.
+ * @return The value of the preference.
+ * @{
+ */
+ nsIFile getFileValue(in string relpref, in string abspref);
+ void setFileValue(in string relpref, in string abspref, in nsIFile aValue);
+ /** @} */
+
+ /**
+ * this is really dangerous. this destroys all pref values
+ * do not call this unless you know what you're doing!
+ */
+ void clearAllValues();
+
+ /**
+ * This is also very dangerous. This will low-level remove the files
+ * associated with this server on disk. It does not notify any listeners.
+ */
+ void removeFiles();
+
+ attribute boolean valid;
+
+ AString toString();
+
+ /* used for comparing nsIMsgIncomingServers */
+ boolean equals(in nsIMsgIncomingServer server);
+
+ /* Get Messages at startup */
+ readonly attribute boolean downloadMessagesAtStartup;
+
+ /* check to this if the server supports filters */
+ attribute boolean canHaveFilters;
+
+ /**
+ * can this server be removed from the account manager? for
+ * instance, local mail is not removable, but an imported folder is
+ */
+ attribute boolean canDelete;
+
+ attribute boolean loginAtStartUp;
+
+ attribute boolean limitOfflineMessageSize;
+ attribute long maxMessageSize;
+
+ attribute nsIMsgRetentionSettings retentionSettings;
+
+ /* check if this server can be a default server */
+ readonly attribute boolean canBeDefaultServer;
+
+ /* check if this server allows search operations */
+ readonly attribute boolean canSearchMessages;
+
+ /* display startup page once per account per session */
+ attribute boolean displayStartupPage;
+ attribute nsIMsgDownloadSettings downloadSettings;
+
+ /*
+ * Offline support level. Support level can vary based on abilities
+ * and features each server can offer wrt to offline service.
+ * Here is the legend to determine the each support level details
+ *
+ * supportLevel == 0 --> no offline support (default)
+ * supportLevel == 10 --> regular offline feature support
+ * supportLevel == 20 --> extended offline feature support
+ *
+ * Each server can initialize itself to the support level if needed
+ * to override the default choice i.e., no offline support.
+ *
+ * POP3, None will default to 0.
+ * IMAP level 10 and NEWS with level 20.
+ *
+ */
+ attribute long offlineSupportLevel;
+
+ /* create pretty name for migrated accounts */
+ AString generatePrettyNameForMigration();
+
+ /* does this server have disk space settings? */
+ readonly attribute boolean supportsDiskSpace;
+
+ /**
+ * Hide this server/account from the UI - used for smart mailboxes.
+ * The server can be retrieved from the account manager by name using the
+ * various Find methods, but nsIMsgAccountManager's GetAccounts and
+ * GetAllServers methods won't return the server/account.
+ */
+ attribute boolean hidden;
+
+ /**
+ * If the server supports Fcc/Sent/etc, default prefs can point to
+ * the server. Otherwise, copies and folders prefs should point to
+ * Local Folders.
+ *
+ * By default this value is set to true via global pref 'allows_specialfolders_usage'
+ * (mailnews.js). For Nntp, the value is overridden to be false.
+ * If ISPs want to modify this value, they should do that in their rdf file
+ * by using this attribute. Please look at mozilla/mailnews/base/ispdata/aol.rdf for
+ * usage example.
+ */
+ attribute boolean defaultCopiesAndFoldersPrefsToServer;
+
+ /* can this server allows sub folder creation */
+ attribute boolean canCreateFoldersOnServer;
+
+ /* can this server allows message filing ? */
+ attribute boolean canFileMessagesOnServer;
+
+ /* can this server allow compacting folders ? */
+ readonly attribute boolean canCompactFoldersOnServer;
+
+ /* can this server allow undo delete ? */
+ readonly attribute boolean canUndoDeleteOnServer;
+
+ /* used for setting up the filter UI */
+ readonly attribute nsMsgSearchScopeValue filterScope;
+
+ /* used for setting up the search UI */
+ readonly attribute nsMsgSearchScopeValue searchScope;
+
+ /**
+ * If the password for the server is available either via authentication
+ * in the current session or from password manager stored entries, return
+ * false. Otherwise, return true. If password is obtained from password
+ * manager, set the password member variable.
+ */
+ readonly attribute boolean passwordPromptRequired;
+
+ /**
+ * for mail, this configures both the MDN filter, and the server-side
+ * spam filter filters, if needed.
+ *
+ * If we have set up to filter return receipts into
+ * our Sent folder, this utility method creates
+ * a filter to do that, and adds it to our filterList
+ * if it doesn't exist. If it does, it will enable it.
+ *
+ * this is not used by news filters (yet).
+ */
+ void configureTemporaryFilters(in nsIMsgFilterList filterList);
+
+ /**
+ * If Sent folder pref is changed we need to clear the temporary
+ * return receipt filter so that the new return receipt filter can
+ * be recreated (by ConfigureTemporaryReturnReceiptsFilter()).
+ */
+ void clearTemporaryReturnReceiptsFilter();
+
+ /**
+ * spam settings
+ */
+ readonly attribute nsISpamSettings spamSettings;
+ readonly attribute nsIMsgFilterPlugin spamFilterPlugin;
+
+ nsIMsgFolder getMsgFolderFromURI(in nsIMsgFolder aFolderResource, in AUTF8String aURI);
+
+ /// Indicates if any other server has deferred storage to this account.
+ readonly attribute boolean isDeferredTo;
+
+ const long keepDups = 0;
+ const long deleteDups = 1;
+ const long moveDupsToTrash = 2;
+ const long markDupsRead = 3;
+
+ attribute long incomingDuplicateAction;
+
+ // check if new hdr is a duplicate of a recently arrived header
+ boolean isNewHdrDuplicate(in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Set a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ * @param aForcePropertyEmpty true if an empty inherited property should be returned
+ */
+ void setForcePropertyEmpty(in string propertyName, in boolean aForcePropertyEmpty);
+
+ /**
+ * Get a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ *
+ * @return true if an empty inherited property should be returned
+ */
+ boolean getForcePropertyEmpty(in string propertyName);
+
+ /**
+ * Return the order in which this server type should appear in the folder pane.
+ * This sort order is a number between 100000000 and 900000000 so that RDF can
+ * use it as a string.
+ * The current return values are these:
+ * 0 = default account, 100000000 = mail accounts (POP3/IMAP4),
+ * 200000000 = Local Folders, 300000000 = IM accounts,
+ * 400000000 = RSS, 500000000 = News
+ * If a new server type is created a TB UI reviewer must decide its sort order.
+ */
+ readonly attribute long sortOrder;
+};
+
+%{C++
+/*
+ * Following values for offline support have been used by
+ * various files. If you are modifying any of the values
+ * below, please do take care of the following files.
+ * - mozilla/mailnews/base/src/nsMsgAccountManagerDS.cpp
+ * - mozilla/mailnews/base/util/nsMsgIncomingServer.cpp
+ * - mozilla/mailnews/imap/src/nsImapIncomingServer.cpp
+ * - mozilla/mailnews/local/src/nsPop3IncomingServer.cpp
+ * - mozilla/mailnews/news/src/nsNntpIncomingServer.cpp
+ * - mozilla/mailnews/base/content/msgAccountCentral.js
+ * - mozilla/modules/libpref/src/init/mailnews.js
+ * - ns/modules/libpref/src/init/mailnews-ns.js
+ * - ns/mailnews/base/ispdata/aol.rdf
+ * - ns/mailnews/base/ispdata/nswebmail.rdf
+ */
+#define OFFLINE_SUPPORT_LEVEL_NONE 0
+#define OFFLINE_SUPPORT_LEVEL_REGULAR 10
+#define OFFLINE_SUPPORT_LEVEL_EXTENDED 20
+#define OFFLINE_SUPPORT_LEVEL_UNDEFINED -1
+
+// Value when no port setting is found
+#define PORT_NOT_SET -1
+
+/* some useful macros to implement nsIMsgIncomingServer accessors */
+#define NS_IMPL_SERVERPREF_STR(_class, _postfix, _prefname) \
+NS_IMETHODIMP \
+_class::Get##_postfix(nsACString& retval) \
+{ \
+ return GetCharValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(const nsACString& chvalue) \
+{ \
+ return SetCharValue(_prefname, chvalue); \
+}
+
+#define NS_IMPL_SERVERPREF_BOOL(_class, _postfix, _prefname)\
+NS_IMETHODIMP \
+_class::Get##_postfix(bool *retval) \
+{ \
+ return GetBoolValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(bool bvalue) \
+{ \
+ return SetBoolValue(_prefname, bvalue); \
+}
+
+#define NS_IMPL_SERVERPREF_INT(_class, _postfix, _prefname)\
+NS_IMETHODIMP \
+_class::Get##_postfix(int32_t *retval) \
+{ \
+ return GetIntValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(int32_t ivalue) \
+{ \
+ return SetIntValue(_prefname, ivalue); \
+}
+
+%}
diff --git a/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl b/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl
new file mode 100644
index 0000000000..290760c6f0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl
@@ -0,0 +1,211 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIURL.idl"
+
+interface nsIFile;
+interface nsIUrlListener;
+interface nsIMsgStatusFeedback;
+interface nsIMsgIncomingServer;
+interface nsIMsgWindow;
+interface nsILoadGroup;
+interface nsIMsgSearchSession;
+interface nsICacheEntry;
+interface nsIMimeHeaders;
+interface nsIStreamListener;
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+interface nsIDocShell;
+interface nsITransportSecurityInfo;
+
+[scriptable, builtinclass, uuid(995455ba-5bb4-4643-8d70-2b877a2e1320)]
+interface nsIMsgMailNewsUrl : nsIURL {
+ [noscript,notxpcom,nostdcall]
+ nsresult setFileNameInternal(in ACString aFileName);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setSpecInternal(in ACString aSpec);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setPortInternal(in long aPort);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setQueryInternal(in ACString aQuery);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setUsernameInternal(in ACString aUsername);
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Eventually we'd like to push this type of functionality up into nsIURI.
+ // The idea is to allow the "application" (the part of the code which wants to
+ // run a url in order to perform some action) to register itself as a listener
+ // on url. As a url listener, the app will be informed when the url begins to run
+ // and when the url is finished.
+ ////////////////////////////////////////////////////////////////////////////////
+ void RegisterListener(in nsIUrlListener aUrlListener);
+ void UnRegisterListener(in nsIUrlListener aUrlListener);
+
+ readonly attribute nsIURI baseURI;
+
+ // if you really want to know what the current state of the url is (running or not
+ // running) you should look into becoming a urlListener...
+ void SetUrlState(in boolean runningUrl, in nsresult aStatusCode);
+ void GetUrlState(out boolean runningUrl);
+
+ readonly attribute nsIMsgIncomingServer server;
+
+ /**
+ * Transport-level security information (if any), in the case of a security
+ * error having occurred.
+ * This value should be considered undefined if an NSS error has not
+ * occurred. Read it as: "the secInfo that was being used when a failure
+ * occurred", not: "the secInfo that failed".
+ * Seems a bit ugly adding more state here, but the idea is that a
+ * nsIUrlListener.OnStopRunningUrl() needs to be able to access a bad
+ * certificate, so as to have the option of adding an exemption (See
+ * Bug 1590473).
+ */
+ attribute nsITransportSecurityInfo failedSecInfo;
+
+ /**
+ * The folder associated with this url.
+ *
+ * @exception NS_ERROR_FAILURE May be thrown if the url does not
+ * relate to a folder, e.g. standalone
+ * .eml messages.
+ */
+ attribute nsIMsgFolder folder;
+
+ attribute nsIMsgStatusFeedback statusFeedback;
+
+ /**
+ * The maximum progress for this URL. This might be a count, or it might
+ * be a number of bytes. A value of -1 indicates that this is unknown.
+ */
+ attribute long long maxProgress;
+
+ attribute nsIMsgWindow msgWindow;
+
+ // current mime headers if reading message
+ attribute nsIMimeHeaders mimeHeaders;
+
+ // the load group is computed from the msgWindow
+ readonly attribute nsILoadGroup loadGroup;
+
+ // search session, if we're running a search.
+ attribute nsIMsgSearchSession searchSession;
+ attribute boolean updatingFolder;
+ attribute boolean msgIsInLocalCache;
+ attribute boolean suppressErrorMsgs; // used to avoid displaying biff error messages
+
+ /**
+ * Set after an error occurred.
+ * It is not translated and contains no parameters.
+ * It is unique to each different kind of error, i.e. the same
+ * error has the same code, but a different error has a different code.
+ * This allows to recover from specific errors programmatically,
+ * or to keep error statistics.
+ * If the error comes from a server, the implementor should make
+ * efforts to pass on comparable server error identifiers and include
+ * them here, e.g. as suffixes. Example: "imap-sasl-S474", where-as "S474"
+ * comes from the server annd "imap-sasl-" is the prefix for where the
+ * server reports appears.
+ */
+ attribute ACString errorCode;
+ /**
+ * Set after an error occurred.
+ * An error message that can be displayed directly
+ * to the end user without further processing.
+ * It must have been translated.
+ * It may contain contain values as part of the message.
+ */
+ attribute AString errorMessage;
+
+ /**
+ * To be used in error situations, e.g. to give the URI to an error page
+ * that describes the problem.
+ */
+ attribute AUTF8String seeOtherURI;
+
+ attribute nsICacheEntry memCacheEntry;
+
+ const unsigned long eCopy = 0;
+ const unsigned long eMove = 1;
+ const unsigned long eDisplay = 2;
+ boolean IsUrlType(in unsigned long type);
+ nsIStreamListener getSaveAsListener(in boolean addDummyEnvelope, in nsIFile aFile);
+
+ /// Returns true if the URI is for a message (e.g., imap-message://)
+ readonly attribute boolean isMessageUri;
+
+ /**
+ * Loads the URI in a docshell. This will give priority to loading the
+ * URI in the passed-in docshell. If it can't be loaded there
+ * however, the URL dispatcher will go through its normal process of content
+ * loading.
+ *
+ * @param docshell The docshell that will consume the load.
+ *
+ * @param aLoadFlags Flags to modify load behaviour. Flags are defined in
+ * nsIWebNavigation. Normally only LOAD_FLAGS_NONE or
+ * LOAD_FLAGS_IS_LINK is needed, but there are eleven
+ * other allowed sets of flags. See nsDocShellLoadTypes.h
+ */
+ void loadURI(in nsIDocShell docshell,
+ in unsigned long aLoadFlags);
+
+};
+
+//////////////////////////////////////////////////////////////////////////////////
+// This is a very small interface which I'm grouping with the mailnewsUrl interface.
+// Several url types (mailbox, imap, nntp) have similar properties because they can
+// represent mail messages. For instance, these urls can be have URI
+// equivalents which represent a message.
+// We want to provide the app the ability to get the URI for the
+// url. This URI to URL mapping doesn't exist for all mailnews urls...hence I'm
+// grouping it into a separate interface...
+//////////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(388a37ec-2e1a-4a4f-9d8b-189bedf1bda2)]
+interface nsIMsgMessageUrl : nsISupports {
+ // get and set the RDF URI associated with the url. Note, not all urls have
+ // had uri's set on them so be prepared to handle cases where this string is empty.
+ attribute AUTF8String uri;
+ // used by imap, pop and nntp in order to implement save message to disk
+ attribute nsIFile messageFile;
+ attribute boolean AddDummyEnvelope;
+ attribute boolean canonicalLineEnding;
+ attribute AUTF8String originalSpec;
+
+ // This is used when creating a principal for the URL with a "normalized" spec
+ // that doesn't contain all the bits in the query part that mailnews URLs have.
+ // We need this to implement nsIURIWithSpecialOrigin, since mailnews URLs
+ // have ORIGIN_IS_FULL_SPEC.
+ readonly attribute AUTF8String normalizedSpec;
+
+ /**
+ * A message db header for that message.
+ *
+ * @note This attribute is not guaranteed to be set, so callers that
+ * actually require an nsIMsgDBHdr will need to use the uri attribute
+ * on this interface to get the appropriate nsIMsgMessageService and
+ * then get the header from there.
+ */
+ readonly attribute nsIMsgDBHdr messageHeader;
+};
+
+//////////////////////////////////////////////////////////////////////////////////
+// This is a very small interface which I'm grouping with the mailnewsUrl interface.
+// I want to isolate out all the I18N specific information that may be associated with
+// any given mailnews url. This gives I18N their own "sandbox" of routines they can add
+// and tweak as they see fit. For now it contains mostly charset information.
+//////////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(D71E0785-2862-11d4-98C1-001083010E9B)]
+interface nsIMsgI18NUrl : nsISupports {
+ // when true the user wants us to auto-detect the character set.
+ attribute boolean autodetectCharset;
+};
diff --git a/comm/mailnews/base/public/nsIMsgMailSession.idl b/comm/mailnews/base/public/nsIMsgMailSession.idl
new file mode 100644
index 0000000000..d11929edab
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMailSession.idl
@@ -0,0 +1,78 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/*
+ * The mail session is a replacement for the old 4.x MSG_Master object. It
+ * contains mail session generic information such as the account manager, etc
+ * I'm starting this off as an empty interface and as people feel they need to
+ * add more information to it, they can. I think this is a better approach
+ * than trying to port over the old MSG_Master in its entirety as that had a
+ * lot of cruft in it....
+ */
+
+#include "nsIFolderListener.idl"
+
+interface nsIFile;
+interface nsIMsgWindow;
+interface nsIMsgUserFeedbackListener;
+interface nsIMsgMailNewsUrl;
+
+[scriptable, uuid(577ead34-553e-4cd6-b484-76ff6662082d)]
+interface nsIMsgMailSession : nsISupports {
+ void Shutdown();
+
+ /**
+ * Adds a listener to be notified when folders update.
+ *
+ * @param aListener The listener to add.
+ * @param aNotifyFlags A combination of flags detailing on which operations
+ * to notify the listener. See nsIFolderListener.idl for
+ * details.
+ */
+ void AddFolderListener(in nsIFolderListener aListener,
+ in folderListenerNotifyFlagValue aNotifyFlags);
+ /**
+ * Removes a listener from the folder notification list.
+ *
+ * @param aListener The listener to remove.
+ */
+ void RemoveFolderListener(in nsIFolderListener aListener);
+
+ /**
+ * Adds a listener to be notified of alert or prompt style feedback that
+ * should go to the user.
+ *
+ * @param aListener The listener to add.
+ */
+ void addUserFeedbackListener(in nsIMsgUserFeedbackListener aListener);
+
+ /**
+ * Removes a user feedback listener.
+ *
+ * @param aListener The listener to remove.
+ */
+ void removeUserFeedbackListener(in nsIMsgUserFeedbackListener aListener);
+
+ /**
+ * Call to alert the listeners of the message. If there are no listeners,
+ * or the listeners do not handle the alert, then this function will present
+ * the user with a modal dialog if aMsgWindow isn't null.
+ *
+ * @param aMessage The localized message string to alert.
+ * @param aUrl Optional mailnews url which is relevant to the operation
+ * which caused the alert to be generated.
+ */
+ void alertUser(in AString aMessage, [optional] in nsIMsgMailNewsUrl aUrl);
+
+ readonly attribute nsIMsgWindow topmostMsgWindow;
+ void AddMsgWindow(in nsIMsgWindow msgWindow);
+ void RemoveMsgWindow(in nsIMsgWindow msgWindow);
+ boolean IsFolderOpenInWindow(in nsIMsgFolder folder);
+
+ AUTF8String ConvertMsgURIToMsgURL(in AUTF8String aURI, in nsIMsgWindow aMsgWindow);
+ nsIFile getDataFilesDir(in string dirName);
+};
diff --git a/comm/mailnews/base/public/nsIMsgMdnGenerator.idl b/comm/mailnews/base/public/nsIMsgMdnGenerator.idl
new file mode 100644
index 0000000000..0da7475f8c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMdnGenerator.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgFolder;
+interface nsIMimeHeaders;
+
+typedef long EDisposeType;
+typedef long ReceiptHdrType;
+typedef long MDNIncorporateType;
+
+[scriptable, uuid(440EA3DE-DACA-4886-9875-84E6CD7D7927)]
+interface nsIMsgMdnGenerator : nsISupports
+{
+ const EDisposeType eDisplayed = 0;
+ const EDisposeType eDispatched = 1;
+ const EDisposeType eProcessed = 2;
+ const EDisposeType eDeleted = 3;
+ const EDisposeType eDenied = 4;
+ const EDisposeType eFailed = 5;
+
+ const ReceiptHdrType eDntType = 0;
+ const ReceiptHdrType eRrtType = 1;
+ const ReceiptHdrType eDntRrtType = 2;
+
+ const MDNIncorporateType eIncorporateInbox = 0;
+ const MDNIncorporateType eIncorporateSent = 1;
+
+ /**
+ * Prepare the sending of a mdn reply, and checks the prefs whether a
+ * reply should be send. Might send the message automatically if the
+ * prefs say it should.
+ * @param eType One of EDisposeType above, indicating the action that led
+ * to sending the mdn reply
+ * @param aWindow The window the message was displayed in, acting as parent
+ * for any (error) dialogs
+ * @param folder The folder the message is in
+ * @param key the message key
+ * @param headers the message headers
+ * @param autoAction true if the request action led to sending the mdn
+ * reply was an automatic action, false if it was user initiated
+ * @returns true if the user needs to be asked for permission
+ * false in other cases (whether the message was sent or denied)
+ */
+ boolean process(in EDisposeType eType, in nsIMsgWindow aWindow,
+ in nsIMsgFolder folder, in nsMsgKey key,
+ in nsIMimeHeaders headers, in boolean autoAction);
+
+ /**
+ * Must be called when the user was asked for permission and agreed to
+ * sending the mdn reply.
+ * May only be called when |process| returned |true|. Behaviour is
+ * unspecified in other cases
+ */
+ void userAgreed();
+
+ /**
+ * Must be called when the user was asked for permission and declined to
+ * send the mdn reply.
+ * Will mark the message so that the user won't be asked next time.
+ * May only be called when |process| returned |true|. Behaviour is
+ * unspecified in other cases.
+ */
+ void userDeclined();
+};
diff --git a/comm/mailnews/base/public/nsIMsgMessageService.idl b/comm/mailnews/base/public/nsIMsgMessageService.idl
new file mode 100644
index 0000000000..93d9902e63
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMessageService.idl
@@ -0,0 +1,226 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+interface nsIURI;
+interface nsIUrlListener;
+interface nsIStreamListener;
+interface nsIMsgWindow;
+interface nsIFile;
+interface nsIMsgFolder;
+interface nsIMsgSearchSession;
+interface nsIMsgDBHdr;
+interface nsIStreamConverter;
+interface nsICacheEntry;
+
+%{C++
+#include "MailNewsTypes.h"
+%}
+
+/**
+ * nsIMsgMessageService provides higher-level, UI-oriented calls for
+ * dealing with messages in a protocol-agnostic way.
+ * Things the user would recognise as actions they initiated.
+ * This covers things like displaying messages, copying them, saving them
+ * to disk, saving attachments...
+ */
+[scriptable, uuid(3aa7080a-73ac-4394-9636-fc00e182319b)]
+interface nsIMsgMessageService : nsISupports {
+
+ /**
+ * If you want a handle on the running task, pass in a valid nsIURI
+ * ptr. You can later interrupt this action by asking the netlib
+ * service manager to interrupt the url you are given back.
+ * Remember to release aURL when you are done with it. Pass nullptr
+ * in for aURL if you don't care about the returned URL.
+ */
+
+ /**
+ * Pass in the URI for the message you want to have copied.
+ *
+ * @param aSrcURI
+ * @param aCopyListener already knows about the destination folder.
+ * @param aMoveMessage TRUE if you want the message to be moved.
+ * FALSE leaves it as just a copy.
+ * @param aUrlListener
+ * @param aMsgWindow
+ */
+ void copyMessage(in AUTF8String aSrcURI, in nsIStreamListener aCopyListener, in boolean aMoveMessage,
+ in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Copy multiple messages at a time
+ *
+ * @param keys
+ * @param srcFolder
+ * @param aCopyListener
+ * @param aMoveMessage
+ * @param aUrlListener
+ * @param aMsgWindow
+ * @returns URI that's run to perform the copy
+ */
+ nsIURI CopyMessages(in Array<nsMsgKey> aKeys,
+ in nsIMsgFolder srcFolder,
+ in nsIStreamListener aCopyListener,
+ in boolean aMoveMessage,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * When you want a message displayed.... this loads it into the consumer.
+ *
+ * @param aMessageURI Is a uri representing the message to display.
+ * @param aDisplayConsumer Is (for now) an nsIDocShell which we'll use to load
+ * the message into.
+ * XXXbz Should it be an nsIWebNavigation or something?
+ * @param aMsgWindow
+ * @param aUrlListener
+ * @param aAutodetectCharset (optional) if the characterset should be auto-detected.
+ */
+ void loadMessage(in AUTF8String aMessageURI,
+ in nsISupports aDisplayConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener,
+ in boolean aAutodetectCharset);
+
+ /**
+ * When you want to spool a message out to a file on disk.
+ * This is an asynch operation of course. You must pass in a
+ * url listener in order to figure out when the operation is done.
+ *
+ * @param aMessageURI The uri representing the message to spool out to disk.
+ * @param aFile The file you want the message saved to
+ * @param aGenerateDummyEnvelope Usually FALSE. Set to TRUE if you want the msg
+ * appended at the end of the file.
+ * @param aUrlListener
+ * @param aURL
+ * @param canonicalLineEnding
+ * @param aMsgWindow
+ */
+ void SaveMessageToDisk(in AUTF8String aMessageURI, in nsIFile aFile,
+ in boolean aGenerateDummyEnvelope,
+ in nsIUrlListener aUrlListener, out nsIURI aURL,
+ in boolean canonicalLineEnding, in nsIMsgWindow aMsgWindow);
+
+ /**
+ * When you have a uri and you would like to convert that
+ * to a url which can be run through necko, you can use this method.
+ * the Uri MUST refer to a message and not a folder!
+ *
+ * @param aMessageURI A message uri to convert.
+ * @param aMsgWindow
+ *
+ * @return a URL which can be run through necko
+ */
+ nsIURI getUrlForUri(in AUTF8String aMessageURI, [optional] in nsIMsgWindow aMsgWindow);
+
+ /**
+ *
+ *
+ * @param aSearchSession
+ * @param aMsgWindow
+ * @param aMsgFolder
+ * @param aSearchUri
+ */
+ void Search(in nsIMsgSearchSession aSearchSession, in nsIMsgWindow aMsgWindow, in nsIMsgFolder aMsgFolder, in AUTF8String aSearchUri);
+
+ /**
+ * This method streams a message to the passed in consumer. If aConvertData is true, it
+ * will create a stream converter from message rfc822 to star/star. It will also tack
+ * aAdditionalHeader onto the url (e.g., "header=filter").
+ *
+ * @param aMessageURI uri of message to stream
+ * @param aConsumer generally, a stream listener listening to the message
+ * @param aMsgWindow msgWindow for give progress and status feedback
+ * @param aUrlListener gets notified when url starts and stops
+ * @param aConvertData should we create a stream converter?
+ * @param aAdditionalHeader added to URI, e.g., "header=filter"
+ * @param aLocalOnly whether data should be retrieved only from local caches
+ * If streaming over the network is required and this is true, then
+ * an exception is thrown. This defaults to false.
+ *
+ * @note If we're offline, then even if aLocalOnly is false, we won't stream over the
+ * network
+ *
+ * @return the URL that gets run
+ */
+ nsIURI streamMessage(in AUTF8String aMessageURI, in nsISupports aConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener,
+ in boolean aConvertData,
+ in ACString aAdditionalHeader,
+ [optional] in boolean aLocalOnly);
+
+ /**
+ * This method streams a message's headers to the passed in consumer.
+ * This is for consumers who want a particular header but don't
+ * want to stream the whole message.
+ *
+ * @param aMessageURI uri of message whose headers we are to stream
+ * @param aConsumer a stream listener listening to the message
+ headers.
+ * @param aUrlListener gets notified when url starts and stops, if we run a url.
+ * @param aLocalOnly whether data should be retrieved only from local caches
+ * If streaming over the network is required and this is true, then
+ * an exception is thrown. This defaults to false.
+ *
+ * @note If we're offline, then even if aLocalOnly is false, we won't stream over the
+ * network
+ *
+ * @return the URL that gets run, if any.
+ */
+ nsIURI streamHeaders(in AUTF8String aMessageURI, in nsIStreamListener aConsumer,
+ in nsIUrlListener aUrlListener,
+ [optional] in boolean aLocalOnly);
+
+ /**
+ * Determines whether a message is in the memory cache. Local folders
+ * don't implement this.
+ * The URL needs to address a message, not a message part, all query
+ * qualifiers will be stripped before looking up the entry in the cache.
+ *
+ * @param aUrl The URL of the message, possibly with an appropriate command in it
+ * @param aFolder The folder this message is in
+ *
+ * @return TRUE if the message is in mem cache; FALSE if it is not.
+ */
+ boolean isMsgInMemCache(in nsIURI aUrl,
+ in nsIMsgFolder aFolder);
+
+ /**
+ * now the the message datasource is going away
+ * we need away to go from message uri to go nsIMsgDBHdr
+ *
+ * @param uri A message uri to get nsIMsgDBHdr for.
+ *
+ * @return nsIMsgDBHdr for specified uri or null if failed.
+ */
+ nsIMsgDBHdr messageURIToMsgHdr(in AUTF8String uri);
+};
+
+/**
+ * Some mail protocols (like imap) allow you to fetch individual mime parts. We use this interface
+ * to represent message services whose protocols support this. To use this interface, you should get
+ * the message service then QI for this interface. If it's present, then can fetch a mime part.
+ */
+[scriptable, uuid(3728C255-480C-11d4-98D0-001083010E9B)]
+interface nsIMsgMessageFetchPartService : nsISupports
+{
+ /**
+ * Used to fetch an individual mime part
+ *
+ * @param aURI url representing the message
+ * @param aMessageURI RDF URI including the part to fetch
+ * @param aDisplayConsumer
+ * @param aMsgWindow
+ * @param aUrlListener
+ *
+ * @return
+ */
+ nsIURI fetchMimePart(in nsIURI aURI, in AUTF8String aMessageUri, in nsISupports aDisplayConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+};
diff --git a/comm/mailnews/base/public/nsIMsgOfflineManager.idl b/comm/mailnews/base/public/nsIMsgOfflineManager.idl
new file mode 100644
index 0000000000..282af2d2b5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgOfflineManager.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+// this is a service -there's only one Offline Manager, because you can only do one operation at a time
+// (go online or offline).
+
+interface nsIMsgWindow;
+
+[scriptable, uuid(5e885fec-09b0-11d5-a5bf-0060b0fc04b7)]
+interface nsIMsgOfflineManager : nsISupports
+{
+ attribute nsIMsgWindow window; // should be a progress window.
+ attribute boolean inProgress; // an online->offine or online->offline operation in progress.
+ // the offline menu should be disabled.
+ void goOnline(in boolean sendUnsentMessages, in boolean playbackOfflineImapOperations, in nsIMsgWindow aMsgWindow);
+ void synchronizeForOffline(in boolean downloadNews, in boolean downloadMail, in boolean sendUnsentMessages,
+ in boolean goOfflineWhenDone, in nsIMsgWindow aMsgWindow);
+};
diff --git a/comm/mailnews/base/public/nsIMsgPluggableStore.idl b/comm/mailnews/base/public/nsIMsgPluggableStore.idl
new file mode 100644
index 0000000000..5273d6f0a6
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgPluggableStore.idl
@@ -0,0 +1,335 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgCopyServiceListener;
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIOutputStream;
+interface nsIInputStream;
+interface nsIUrlListener;
+interface nsIMsgDatabase;
+interface nsITransaction;
+
+[scriptable, uuid(F732CE58-E540-4dc4-B803-9456056EBEFC)]
+
+/**
+ * Pluggable message store interface. Each incoming server can have a different
+ * message store.
+ * All methods are synchronous unless otherwise specified.
+ */
+interface nsIMsgPluggableStore : nsISupports {
+ /**
+ * Examines the store and adds subfolders for the existing folders in the
+ * profile directory. aParentFolder->AddSubfolder is the normal way
+ * to register the subfolders. This method is expected to be synchronous.
+ * This shouldn't be confused with server folder discovery, which is allowed
+ * to be asynchronous.
+ *
+ * @param aParentFolder folder whose existing children we want to discover.
+ * This will be the root folder for the server object.
+ * @param aDeep true if we should discover all descendents. Would we ever
+ * not want to do this?
+ */
+
+ void discoverSubFolders(in nsIMsgFolder aParentFolder, in boolean aDeep);
+ /**
+ * Creates storage for a new, empty folder.
+ *
+ * @param aParent parent folder
+ * @param aFolderName leaf name of folder.
+ * @return newly created folder.
+ * @exception NS_MSG_FOLDER_EXISTS If the child exists.
+ * @exception NS_MSG_CANT_CREATE_FOLDER for other errors.
+ */
+ nsIMsgFolder createFolder(in nsIMsgFolder aParent, in AString aFolderName);
+
+ /**
+ * Delete storage for a folder and its subfolders, if any.
+ * This is a real delete, not a move to the trash folder.
+ *
+ * @param aFolder folder to delete
+ */
+ void deleteFolder(in nsIMsgFolder aFolder);
+
+ /**
+ * Rename storage for an existing folder.
+ *
+ * @param aFolder folder to rename
+ * @param aNewName name to give new folder
+ * @return the renamed folder object
+ */
+ nsIMsgFolder renameFolder(in nsIMsgFolder aFolder, in AString aNewName);
+
+ /**
+ * Tells if the store has the requested amount of space available in the
+ * specified folder.
+ *
+ * @param aFolder folder we want to add messages to.
+ * @param aSpaceRequested How many bytes we're trying to add to the store.
+ *
+ * The function returns an exception if there is not enough space to
+ * indicate the reason of the shortage:
+ * NS_ERROR_FILE_TOO_BIG = the store cannot grow further due to internal limits
+ * NS_ERROR_FILE_NO_DEVICE_SPACE = there is not enough space on the disk
+ */
+ boolean hasSpaceAvailable(in nsIMsgFolder aFolder,
+ in long long aSpaceRequested);
+
+ /**
+ * Move/Copy a folder to a new parent folder. This method is asynchronous.
+ * The store needs to use the aListener to notify the core code of the
+ * completion of the operation. And it must send the appropriate
+ * nsIMsgFolderNotificationService notifications.
+ *
+ * @param aSrcFolder folder to move/copy
+ * @param aDstFolder parent dest folder
+ * @param aIsMoveFolder true if move, false if copy. If move, source folder
+ * is deleted when copy completes.
+ * @param aMsgWindow used to display progress, may be null
+ * @param aListener - used to get notification when copy is done.
+ * @param aNewName Optional new name for the target folder.
+ * If rename is not needed, set this to empty string.
+ */
+ void copyFolder(in nsIMsgFolder aSrcFolder, in nsIMsgFolder aDstFolder,
+ in boolean aIsMoveFolder, in nsIMsgWindow aMsgWindow,
+ in nsIMsgCopyServiceListener aListener,
+ in AString aNewName);
+
+ /**
+ * Get an output stream for a message in a folder.
+ *
+ * @param aFolder folder to create a message output stream for.
+ * @param aNewHdr If aNewHdr is set on input, then this is probably for
+ * offline storage of an existing message. If null, the
+ * this is a newly downloaded message and the store needs
+ * to create a new header for the new message. If the db
+ * is invalid, this can be null. But if the db is valid,
+ * the store should create a message header with the right
+ * message key, or whatever other property it needs to set to
+ * be able to retrieve the message contents later. If the store
+ * needs to base any of this on the contents of the message,
+ * it will need remember the message header and hook into
+ * the output stream somehow to alter the message header.
+ *
+ * @return The output stream to write to. The output stream will be positioned
+ * for writing (e.g., for berkeley mailbox, it will be at the end).
+ */
+ nsIOutputStream getNewMsgOutputStream(in nsIMsgFolder aFolder,
+ inout nsIMsgDBHdr aNewHdr);
+
+
+ /**
+ * Called when the current message is discarded, e.g., it is moved
+ * to an other folder as a filter action, or is deleted because it's
+ * a duplicate. This gives the berkeley mailbox store a chance to simply
+ * truncate the Inbox w/o leaving a deleted message in the store.
+ *
+ * discardNewMessage closes aOutputStream always unless the passed stream
+ * is nullptr due to error processing..
+ * (Clarification/Rationale in Bug 1121842, 1122698, 1242030)
+ *
+ * @param aOutputStream stream we were writing the message to be discarded to
+ * @param aNewHdr header of message to discard
+ */
+ void discardNewMessage(in nsIOutputStream aOutputStream,
+ in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Must be called by code that calls getNewMsgOutputStream to finish
+ * the process of storing a new message, if the new msg has not been
+ * discarded. Could/should this be combined with discardNewMessage?
+ *
+ * finishNewMessage closes aOutputStream always unless the passed stream
+ * is nullptr due to error processing.
+ * (Clarification/Rationale in Bug 1121842, 1122698, 1242030)
+ *
+ * @param aOutputStream stream we were writing the message to.
+ * @param aNewHdr header of message finished.
+ */
+ void finishNewMessage(in nsIOutputStream aOutputStream,
+ in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Called by pop3 message filters when a newly downloaded message is being
+ * moved by an incoming filter. This is called before finishNewMessage, and
+ * it allows the store to optimize that case.
+ *
+ * @param aNewHdr msg hdr of message being moved.
+ * @param aDestFolder folder to move message to, in the same store.
+ *
+ * @return true if successful, false if the store doesn't want to optimize
+ * this.
+ * @exception If the moved failed. values TBD
+ */
+ boolean moveNewlyDownloadedMessage(in nsIMsgDBHdr aNewHdr,
+ in nsIMsgFolder aDestFolder);
+
+ /**
+ * Get an input stream that we can read the contents of a message from.
+ *
+ * @param aMsgFolder Folder containing the message
+ * @param aMsgToken token that identifies message. This is store-dependent,
+ * and must be set as a string property "storeToken" on the
+ * message hdr by the store when the message is added
+ * to the store.
+ */
+ nsIInputStream getMsgInputStream(in nsIMsgFolder aFolder,
+ in ACString aMsgToken);
+
+ /**
+ * This is a hack to expose to allow JsAccount folders to implement a
+ * working getLocalMsgStream().
+ * It just provides a way to construct a SlicedInputStream from JS.
+ * It'll be removed once Bug 1733849 is complete.
+ *
+ * @param inStream The stream providing the data to be sliced.
+ * Should not be read after calling this function.
+ * @param start Where slice begins, from current position of inStream.
+ * @param length The size of the slice.
+ *
+ * @return A new input stream which produces the data slice when read from.
+ */
+ nsIInputStream sliceStream(in nsIInputStream inStream,
+ in unsigned long long start,
+ in unsigned long length);
+
+ /**
+ * Delete the passed in messages. These message should all be in the
+ * same folder.
+ * @param aHdrArray array of nsIMsgDBHdr's.
+ */
+ void deleteMessages(in Array<nsIMsgDBHdr> aHdrArray);
+
+ /**
+ * This allows the store to handle a msg move/copy if it wants. This lets
+ * it optimize move/copies within the same store. E.g., for maildir, a
+ * msg move mostly entails moving the file containing the message, and
+ * updating the db.
+ * If the store does the copy, it must return the appropriate undo action,
+ * which can be store dependent. And it must send the appropriate
+ * nsIMsgFolderNotificationService notifications.
+ * If the store does not perform the copy, it returns false and the caller
+ * has to handle the copy itself (by streaming messages).
+ * This function is synchronous.
+ *
+ * @param isMove true if this is a move, false if it is a copy.
+ * @param aHdrArray array of nsIMsgDBHdr's, all in the same folder
+ * @param aDstFolder folder to move/copy the messages to.
+ * @param aDstHdrs array of nsIMsgDBHdr's in the destination folder.
+ * @param[out,optional] aUndoAction transaction to provide undo, if
+ * the store does the copy itself.
+ * @return true if messages were copied, false if the core code should
+ * do the copy.
+ */
+ boolean copyMessages(in boolean isMove,
+ in Array<nsIMsgDBHdr> aHdrArray,
+ in nsIMsgFolder aDstFolder,
+ out Array<nsIMsgDBHdr> aDstHdrs,
+ out nsITransaction aUndoAction);
+
+ /**
+ * Does this store require compaction? For example, maildir doesn't require
+ * compaction at all. Berkeley mailbox does. A sqlite store probably doesn't.
+ * This is a static property of the store. It doesn't mean that any particular
+ * folder has space that can be reclaimed via compaction. Right now, the core
+ * code keeps track of the size of messages deleted, which it can use in
+ * conjunction with this store attribute.
+ */
+ readonly attribute boolean supportsCompaction;
+
+ /**
+ * Remove deleted messages from the store, reclaiming space. Some stores
+ * won't need to do anything here (e.g., maildir), and those stores
+ * should return false for needsCompaction. This operation is asynchronous,
+ * and the passed url listener should be called when the operation is done.
+ *
+ * @param aFolder folder whose storage is to be compacted
+ * @param aListener listener notified when compaction is done.
+ * @param aMsgWindow window to display progress/status in.
+ */
+ void compactFolder(in nsIMsgFolder aFolder, in nsIUrlListener aListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Is the summary file for the passed folder valid? For Berkeley Mailboxes,
+ * for local mail folders, this checks the timestamp and size of the local
+ * mail folder against values stored in the db. For other stores, this may
+ * be a noop, though other stores could certainly become invalid. For
+ * Berkeley Mailboxes, this is to deal with the case of other apps altering
+ * mailboxes from outside mailnews code, and this is certainly possible
+ * with other stores.
+ *
+ * @param aFolder Folder to check if summary is valid for.
+ * @param aDB DB to check validity of.
+ *
+ * @return return true if the summary file is valid, false otherwise.
+ */
+ boolean isSummaryFileValid(in nsIMsgFolder aFolder, in nsIMsgDatabase aDB);
+
+ /**
+ * Marks the summary file for aFolder as valid or invalid. This method
+ * may not be required, since it's really used by Berkeley Mailbox code
+ * to fix the timestamp and size for a folder.
+ *
+ * @param aFolder folder whose summary file should be marked (in)valid.
+ * @param aDB db to mark valid (may not be the folder's db in odd cases
+ * like folder compaction.
+ * @param aValid whether to mark it valid or invalid.
+ */
+ void setSummaryFileValid(in nsIMsgFolder aFolder, in nsIMsgDatabase aDB,
+ in boolean aValid);
+
+ /**
+ * Rebuild the index from information in the store. This involves creating
+ * a new nsIMsgDatabase for the folder, adding the information for all the
+ * messages in the store, and then copying the new msg database over the
+ * existing database. For Berkeley mailbox, we try to maintain meta data
+ * stored in the existing database when possible, and other stores should do
+ * the same. Ideally, I would figure out a way of making that easy. That
+ * might entail reworking the rebuild index process into one where the store
+ * would iterate over the messages, and stream each message through the
+ * message parser, and the common code would handle maintaining the
+ * meta data. But the berkeley mailbox code needs to do some parsing because
+ * it doesn't know how big the message is (i.e., the stream can't simply be
+ * a file stream).
+ * This operation is asynchronous,
+ * and the passed url listener should be called when the operation is done.
+ *
+ * @param aFolder folder whose storage is to be compacted
+ * @param aMsgDB db to put parsed headers in.
+ * @param aMsgWindow msgWindow to use for progress updates.
+ * @param aListener listener notified when the index is rebuilt.
+ */
+ void rebuildIndex(in nsIMsgFolder aFolder, in nsIMsgDatabase aMsgDB,
+ in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+
+ /**
+ * Sets/Clears the passed flags on the passed messages.
+ * @param aHdrArray array of nsIMsgDBHdr's
+ * @param aFlags flags to set/clear
+ * @param aSet true to set the flag(s), false to clear.
+ */
+ void changeFlags(in Array<nsIMsgDBHdr> aHdrArray, in unsigned long aFlags,
+ in boolean aSet);
+ /**
+ *Sets/Clears the passed keywords on the passed messages.
+ * @param aHdrArray array of nsIMsgDBHdr's
+ * @param aKeywords keywords to set/clear
+ * @param aAdd true to add the keyword(s), false to remove.
+ */
+ void changeKeywords(in Array<nsIMsgDBHdr> aHdrArray, in ACString aKeywords,
+ in boolean aAdd);
+
+ /**
+ * Identifies a specific type of store. Please use this only for legacy
+ * bug fixes, and not as a method to change behavior!
+ *
+ * Typical values: "mbox", "maildir"
+ */
+ readonly attribute ACString storeType;
+};
diff --git a/comm/mailnews/base/public/nsIMsgProgress.idl b/comm/mailnews/base/public/nsIMsgProgress.idl
new file mode 100644
index 0000000000..4e370d090c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProgress.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "domstubs.idl"
+#include "nsIPrompt.idl"
+#include "nsIWebProgressListener.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIMsgWindow;
+
+[scriptable, uuid(6d6fe91d-7f9a-4552-9737-9f74b0e75538)]
+interface nsIMsgProgress: nsIWebProgressListener {
+
+ /**
+ * Open the progress dialog, you can specify parameters through an xpcom object
+ */
+ void openProgressDialog(in mozIDOMWindowProxy parent,
+ in nsIMsgWindow aMsgWindow,
+ in string dialogURL,
+ in boolean inDisplayModal,
+ in nsISupports parameters);
+
+ /* Close the progress dialog */
+ void closeProgressDialog(in boolean forceClose);
+
+ /* Register a Web Progress Listener */
+ void registerListener(in nsIWebProgressListener listener);
+
+ /* Unregister a Web Progress Listener */
+ void unregisterListener(in nsIWebProgressListener listener);
+
+ /* Indicated if the user asked to cancel the current process */
+ attribute boolean processCanceledByUser;
+
+ attribute nsIMsgWindow msgWindow;
+};
diff --git a/comm/mailnews/base/public/nsIMsgProtocolHandler.idl b/comm/mailnews/base/public/nsIMsgProtocolHandler.idl
new file mode 100644
index 0000000000..a526104676
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProtocolHandler.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+[scriptable, uuid(4e9e4a43-343a-4309-a88b-08c5f37f5965)]
+interface nsIMsgProtocolHandler : nsISupports {
+ nsIURI newURI(in AUTF8String aSpec, in string aOriginCharset, in nsIURI aBaseURI);
+};
diff --git a/comm/mailnews/base/public/nsIMsgProtocolInfo.idl b/comm/mailnews/base/public/nsIMsgProtocolInfo.idl
new file mode 100644
index 0000000000..7a505673e0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProtocolInfo.idl
@@ -0,0 +1,97 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+%{C++
+#define NS_MSGPROTOCOLINFO_CONTRACTID_PREFIX \
+ "@mozilla.org/messenger/protocol/info;1?type="
+%}
+
+[scriptable, uuid(9428b5f5-8b12-493c-aae2-18296c2877b1)]
+interface nsIMsgProtocolInfo : nsISupports
+{
+ /**
+ * the default path to store local data for this type of
+ * server. Each server is usually in a subdirectory below this
+ */
+ attribute nsIFile defaultLocalPath;
+
+ /**
+ * the IID of the protocol-specific interface for this server
+ * usually used from JS to dynamically get server-specific attributes
+ */
+ readonly attribute nsIIDPtr serverIID;
+
+ /**
+ * does this server type require a username?
+ * for instance, news does not but IMAP/POP do
+ */
+ readonly attribute boolean requiresUsername;
+
+ /**
+ * if the pretty name of the server should
+ * just be the e-mail address. Otherwise it usually
+ * ends up being something like "news on hostname"
+ */
+ readonly attribute boolean preflightPrettyNameWithEmailAddress;
+
+ /**
+ * can this type of server be removed from the account manager?
+ * for instance, local mail is not removable
+ */
+ readonly attribute boolean canDelete;
+
+ /**
+ * can this type of server log in at startup?
+ */
+ readonly attribute boolean canLoginAtStartUp;
+
+ /**
+ * can you duplicate this server?
+ * for instance, local mail is unique and should not be duplicated.
+ */
+ readonly attribute boolean canDuplicate;
+
+ /* the default port
+ This is similar to nsIProtocolHanderl.defaultPort,
+ but for architectural reasons, there is a mail-specific interface to this.
+ When the input param isSecure is set to true, for all supported protocols,
+ the secure port value is returned. If isSecure is set to false the default
+ port value is returned */
+ long getDefaultServerPort(in boolean isSecure);
+
+ /**
+ * An attribute that tell us whether on not we can
+ * get messages for the given server type
+ * this is poorly named right now.
+ * it's really is there an inbox for this type?
+ * XXX todo, rename this.
+ */
+ readonly attribute boolean canGetMessages;
+
+ /**
+ * do messages arrive for this server
+ * if they do, we can use our junk controls on it.
+ */
+ readonly attribute boolean canGetIncomingMessages;
+
+ /**
+ * do biff by default?
+ */
+ readonly attribute boolean defaultDoBiff;
+
+ /**
+ * do we need to show compose message link in the AccountCentral page ?
+ */
+ readonly attribute boolean showComposeMsgLink;
+
+ /**
+ * Will new folders be created asynchronously?
+ */
+ readonly attribute boolean foldersCreatedAsync;
+};
diff --git a/comm/mailnews/base/public/nsIMsgPurgeService.idl b/comm/mailnews/base/public/nsIMsgPurgeService.idl
new file mode 100644
index 0000000000..a914d41525
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgPurgeService.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c73294b2-b619-4915-b0e8-314d4215e08d)]
+interface nsIMsgPurgeService : nsISupports {
+
+ void init();
+ void shutdown();
+};
diff --git a/comm/mailnews/base/public/nsIMsgShutdown.idl b/comm/mailnews/base/public/nsIMsgShutdown.idl
new file mode 100644
index 0000000000..419e5219a5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgShutdown.idl
@@ -0,0 +1,67 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+interface nsIUrlListener;
+interface nsIMsgWindow;
+interface nsIWebProgressListener;
+
+[scriptable, uuid(D1B43428-B631-4629-B691-AB0E01A2DB4B)]
+interface nsIMsgShutdownTask : nsISupports
+{
+ /**
+ * Inform the caller whether or not the task needs to be run. This method
+ * gives the task the flexibility to cancel running a task on shutdown
+ * if nothing needs to be run.
+ */
+ readonly attribute boolean needsToRunTask;
+
+ /**
+ * At shutdown-time, this function will be called to all registered implementors.
+ * Shutdown will be temporarily postponed until |OnStopRequest()| has been called
+ * on the passed in url-listener.
+ * @param inUrlListener The URL listener to report events to.
+ * @param inMsgWindow The current message window to allow for posing dialogs.
+ * @return If the shutdown URL was run or not. If the URL is running, the task
+ * will be responsible for notifying |inUrlListener| when the task is completed.
+ */
+ boolean doShutdownTask(in nsIUrlListener inUrlListener, in nsIMsgWindow inMsgWindow);
+
+ /**
+ * Get the displayable name of the current task. This textual information will be
+ * shown to the user so they know what shutdown task is being performed.
+ * @return The name of the current task being performed.
+ */
+ AString getCurrentTaskName();
+};
+
+[scriptable, uuid(483C8ABB-ECF9-48A3-A394-2C604B603BD5)]
+interface nsIMsgShutdownService : nsISupports
+{
+ /**
+ * Get the number of tasks that will need to be processed at shutdown time.
+ * @return The number of shutdown tasks to do.
+ */
+ long getNumTasks();
+
+ /**
+ * Start the shutdown tasks.
+ */
+ void startShutdownTasks();
+
+ /**
+ * Tell the service to stop running tasks and go ahead and shutdown the application.
+ */
+ void cancelShutdownTasks();
+
+ /**
+ * Set the shutdown listener.
+ */
+ void setShutdownListener(in nsIWebProgressListener inListener);
+
+ /**
+ * Set the status text of the shutdown progress dialog.
+ */
+ void setStatusText(in AString inStatusString);
+};
diff --git a/comm/mailnews/base/public/nsIMsgStatusFeedback.idl b/comm/mailnews/base/public/nsIMsgStatusFeedback.idl
new file mode 100644
index 0000000000..2aedd4713b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgStatusFeedback.idl
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(AACBFA34-8D29-4A08-9283-A8E5B3AB067F)]
+interface nsIMsgStatusFeedback : nsISupports {
+ void showStatusString(in AString aStatus);
+ void startMeteors();
+ void stopMeteors();
+ void showProgress(in long aPercent);
+ void setStatusString(in AString aStatus); // will be displayed until next user action
+
+ /* aStatusFeedback: a wrapped JS status feedback object */
+ void setWrappedStatusFeedback(in nsIMsgStatusFeedback aStatusFeedback);
+};
diff --git a/comm/mailnews/base/public/nsIMsgTagService.idl b/comm/mailnews/base/public/nsIMsgTagService.idl
new file mode 100644
index 0000000000..0317b64960
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgTagService.idl
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/*
+ * Keys are the internal representation of tags, and use a limited range of
+ * characters, basically the characters allowed in imap keywords, which are
+ * alphanumeric characters, but don't include spaces. Keys are stored on
+ * the imap server, in local mail messages, and in summary files.
+ *
+ * Tags are the user visible representation of keys, and are full unicode
+ * strings. Tags should allow any unicode character.
+ *
+ * This service will do the mapping between keys and tags. When a tag
+ * is added, we'll need to "compute" the corresponding key to use. This
+ * will probably entail replacing illegal ascii characters (' ', '/', etc)
+ * with '_' and then converting to imap mod utf7. We'll then need to make
+ * sure that no other keyword has the same value since that algorithm
+ * doesn't guarantee a unique mapping.
+ *
+ * Tags are sorted internally by 'importance' by their ordinal strings (which by
+ * default are equal to a tag's key and thus only stored if different).
+ * The alphanumerically 'smallest' string is called the 'most important' one and
+ * comes first in any sorted array. The remainder follows in ascending order.
+ */
+
+[scriptable, uuid(84d593a3-5d8a-45e6-96e2-9189acd422e1)]
+interface nsIMsgTag : nsISupports {
+ readonly attribute ACString key; // distinct tag identifier
+ readonly attribute AString tag; // human readable tag name
+ readonly attribute ACString color; // tag color
+ readonly attribute ACString ordinal; // custom sort string (usually empty)
+};
+
+[scriptable, uuid(97360ce3-0fba-4f1c-8214-af7bdc6f8587)]
+interface nsIMsgTagService : nsISupports {
+ // create new tag by deriving the key from the tag
+ void addTag(in AString tag, in ACString color, in ACString ordinal);
+ // create/update tag with known key
+ void addTagForKey(in ACString key, in AString tag, in ACString color, in ACString ordinal);
+ // get the key representation of a given tag
+ ACString getKeyForTag(in AString tag);
+ // get the first key by ordinal order
+ ACString getTopKey(in ACString keyList);
+ // support functions for single tag aspects
+ AString getTagForKey(in ACString key); // look up the tag for a key.
+ void setTagForKey(in ACString key, in AString tag); // this can be used to "rename" a tag
+ ACString getColorForKey(in ACString key);
+ AString getSelectorForKey(in ACString key); // return wide string to avoid conversion
+ void setColorForKey(in ACString key, in ACString color);
+ ACString getOrdinalForKey(in ACString key);
+ void setOrdinalForKey(in ACString key, in ACString ordinal);
+ // delete a tag from the list of known tags (but not from any messages)
+ void deleteKey(in ACString key);
+ // get all known tags
+ Array<nsIMsgTag> getAllTags();
+ /*
+ * Determines if the token in aKey corresponds to a current valid tag
+ *
+ * @param aKey The string to test
+ * @return True if aKey is a current token
+ */
+ boolean isValidKey(in ACString aKey);
+};
diff --git a/comm/mailnews/base/public/nsIMsgThread.idl b/comm/mailnews/base/public/nsIMsgThread.idl
new file mode 100644
index 0000000000..91cb995c84
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgThread.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgEnumerator;
+interface nsIMsgDBHdr;
+interface nsIDBChangeAnnouncer;
+
+[scriptable, uuid(84052876-90e9-4e21-ad38-13e2bb751d8f)]
+interface nsIMsgThread : nsISupports {
+ attribute nsMsgKey threadKey;
+ attribute unsigned long flags;
+ attribute ACString subject;
+ attribute unsigned long newestMsgDate;
+ readonly attribute unsigned long numChildren;
+ readonly attribute unsigned long numUnreadChildren;
+
+ void addChild(in nsIMsgDBHdr child, in nsIMsgDBHdr inReplyTo, in boolean threadInThread, in nsIDBChangeAnnouncer announcer);
+ nsMsgKey getChildKeyAt(in unsigned long index);
+ nsIMsgDBHdr getChild(in nsMsgKey msgKey);
+ nsIMsgDBHdr getChildHdrAt(in unsigned long index);
+ nsIMsgDBHdr getRootHdr();
+ void removeChildAt(in unsigned long index);
+ void removeChildHdr(in nsIMsgDBHdr child, in nsIDBChangeAnnouncer announcer);
+
+ void markChildRead(in boolean bRead);
+
+ nsIMsgDBHdr getFirstUnreadChild();
+
+ nsIMsgEnumerator enumerateMessages(in nsMsgKey parent);
+};
diff --git a/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl b/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl
new file mode 100644
index 0000000000..f798c4b86c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl
@@ -0,0 +1,28 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgMailNewsUrl;
+
+/**
+ * Implement this interface to subscribe to errors and warnings passed out via
+ * nsIMsgMailSession.
+ */
+[scriptable, uuid(5e909ffa-77fe-4ce3-bf3c-06c54596d03d)]
+interface nsIMsgUserFeedbackListener : nsISupports {
+ /**
+ * Called when an alert from a protocol level implementation is generated.
+ *
+ * @param aMessage The localized message string to alert.
+ * @param aUrl Optional mailnews url which is relevant to the operation
+ * which caused the alert to be generated.
+ * @return True if you serviced the alert and it does not need
+ * to be prompted to the user separately.
+ * Note: The caller won't prompt if msgWindow in aUrl is
+ * null, regardless of the value returned.
+ */
+ boolean onAlert(in AString aMessage, [optional] in nsIMsgMailNewsUrl aUrl);
+};
diff --git a/comm/mailnews/base/public/nsIMsgWindow.idl b/comm/mailnews/base/public/nsIMsgWindow.idl
new file mode 100644
index 0000000000..2cbae8e23f
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgWindow.idl
@@ -0,0 +1,64 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgStatusFeedback;
+interface nsIMsgFolder;
+interface nsITransactionManager;
+interface nsIDocShell;
+interface mozIDOMWindowProxy;
+interface nsIPrompt;
+interface nsIInterfaceRequestor;
+interface nsIAuthPrompt;
+interface nsIPrincipal;
+
+[scriptable, uuid(a846fe48-4022-4296-a1c4-1dcd7eaecfe5)]
+interface nsIMsgWindow : nsISupports {
+ attribute nsIMsgStatusFeedback statusFeedback;
+ attribute nsITransactionManager transactionManager;
+ attribute nsIMsgFolder openFolder;
+
+ /**
+ * @note Setting this attribute has various side effects, including
+ * wiring up this object as the parent nsIURIContentListener for the
+ * passed-in docshell as well as setting the message content policy service
+ * to listen for OnLocationChange notifications.
+ */
+ attribute nsIDocShell rootDocShell;
+
+ /**
+ * @note Small helper function used to optimize our use of a weak reference
+ * on the message window docshell. Under no circumstances should you be
+ * holding on to the docshell returned here outside the scope of your routine.
+ */
+ readonly attribute nsIDocShell messageWindowDocShell;
+
+ /**
+ * These are currently used to set notification callbacks on
+ * protocol channels to handle things like bad cert exceptions.
+ */
+ attribute nsIInterfaceRequestor notificationCallbacks;
+
+ /**
+ Has a running url been stopped? If you care about checking
+ this flag, you need to clear it before you start your operation since
+ there's no convenient place to clear it.
+ */
+ attribute boolean stopped;
+
+ attribute mozIDOMWindowProxy domWindow;
+
+ void StopUrls();
+
+ /**
+ when the msg window is being unloaded from the content window,
+ we can use this notification to force a flush on anything the
+ msg window hangs on too. For some reason xpconnect is still hanging
+ onto the msg window even though all of our objects have let go of it
+ this forces a release...
+ */
+ void closeWindow();
+};
diff --git a/comm/mailnews/base/public/nsISpamSettings.idl b/comm/mailnews/base/public/nsISpamSettings.idl
new file mode 100644
index 0000000000..96c81c6c08
--- /dev/null
+++ b/comm/mailnews/base/public/nsISpamSettings.idl
@@ -0,0 +1,97 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIOutputStream;
+interface nsIMsgIncomingServer;
+interface nsIMsgDBHdr;
+interface nsIFile;
+
+[scriptable, uuid(1772BE95-FDA9-4dfd-A663-8AF92C1E3024)]
+interface nsISpamSettings: nsISupports {
+ /**
+ * 0 for nothing, 100 for highest
+ */
+ attribute long level;
+
+ attribute boolean moveOnSpam;
+ readonly attribute boolean markAsReadOnSpam;
+
+ /**
+ * Most consumers will just use spamFolderURI rather than accessing any of
+ * target attributes directly.
+ */
+ attribute long moveTargetMode;
+ const long MOVE_TARGET_MODE_ACCOUNT = 0;
+ const long MOVE_TARGET_MODE_FOLDER = 1;
+ // Despite their name the following are URIs.
+ attribute AUTF8String actionTargetAccount;
+ attribute AUTF8String actionTargetFolder;
+
+ /**
+ * built from moveTargetMode, actionTargetAccount, actionTargetFolder
+ */
+ readonly attribute AUTF8String spamFolderURI;
+
+ attribute boolean purge;
+ /**
+ * interval, in days
+ */
+ attribute long purgeInterval;
+
+ attribute boolean useWhiteList;
+ attribute AUTF8String whiteListAbURI;
+
+ /**
+ * Should we do something when the user manually marks a message as junk?
+ */
+ readonly attribute boolean manualMark;
+
+ /**
+ * With manualMark true, which action (move to the Junk folder, or delete)
+ * should we take when the user marks a message as junk.
+ */
+ readonly attribute long manualMarkMode;
+ const long MANUAL_MARK_MODE_MOVE = 0;
+ const long MANUAL_MARK_MODE_DELETE = 1;
+
+ /**
+ * integrate with server-side spam detection programs
+ */
+ attribute boolean useServerFilter;
+ attribute ACString serverFilterName;
+ readonly attribute nsIFile serverFilterFile;
+ const long TRUST_POSITIVES = 1;
+ const long TRUST_NEGATIVES = 2;
+ attribute long serverFilterTrustFlags;
+
+ // for logging
+ readonly attribute boolean loggingEnabled;
+ attribute nsIOutputStream logStream;
+ void logJunkHit(in nsIMsgDBHdr aMsgHdr, in boolean aMoveMessage);
+ void logJunkString(in string aLogText);
+ void clone(in nsISpamSettings aSpamSettings);
+
+ // aServer -> spam settings are associated with a particular server
+ void initialize(in nsIMsgIncomingServer aServer);
+
+ /**
+ * check if junk processing for a message should be bypassed
+ *
+ * Typically this is determined by comparing message to: address
+ * to a whitelist of known good addresses or domains.
+ *
+ * @param aMsgHdr database header representing the message.
+ *
+ * @return true if this message is whitelisted, and junk
+ * processing should be bypassed
+ *
+ * false otherwise (including in case of error)
+ */
+ boolean checkWhiteList(in nsIMsgDBHdr aMsgHdr);
+
+};
diff --git a/comm/mailnews/base/public/nsIStatusBarBiffManager.idl b/comm/mailnews/base/public/nsIStatusBarBiffManager.idl
new file mode 100644
index 0000000000..88dc4d7b40
--- /dev/null
+++ b/comm/mailnews/base/public/nsIStatusBarBiffManager.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIMsgFolder.idl"
+#include "nsIFolderListener.idl"
+
+[scriptable, uuid(20b81f2b-ea81-4baa-b378-c5e6d3dc94e5)]
+interface nsIStatusBarBiffManager : nsIFolderListener {
+ // see nsIMsgFolder for definition and constants
+ readonly attribute nsMsgBiffState biffState;
+};
diff --git a/comm/mailnews/base/public/nsIStopwatch.idl b/comm/mailnews/base/public/nsIStopwatch.idl
new file mode 100644
index 0000000000..6e40ace070
--- /dev/null
+++ b/comm/mailnews/base/public/nsIStopwatch.idl
@@ -0,0 +1,44 @@
+/* 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/. */
+#include "nsISupports.idl"
+
+/**
+ * Simple stopwatch mechanism for determining the amount of wall-clock time and
+ * CPU time (user + system) that has elapsed. It is not fancy. It is either
+ * running or it is not. If you want coherent cpu and real time values, then
+ * you had better stop it first. It does not keep counting when stopped,
+ * although one could add a resumeRetroactive or something to accomplish that.
+ */
+[scriptable, uuid(7a671d6e-d48f-4a4f-b87e-644815a5e381)]
+interface nsIStopwatch : nsISupports {
+ /**
+ * Start the stopwatch; all counters are reset to zero. If you want to
+ * keep the already accumulated values, use resume instead.
+ */
+ void start();
+
+ /**
+ * Stop the stopwatch.
+ */
+ void stop();
+
+ /**
+ * Resume the stopwatch without clearing the existing counters. Any time
+ * already accumulated on cpuTime/realTime will be kept.
+ */
+ void resume();
+
+ /**
+ * The total CPU time (user + system) in seconds accumulated between calls to
+ * start/resume and stop. You have to stop the stopwatch to cause this value
+ * to update.
+ */
+ readonly attribute double cpuTimeSeconds;
+ /**
+ * The total wall clock time in seconds accumulated between calls to
+ * start/resume and stop. You have to stop the stopwatch to cause this value
+ * to update.
+ */
+ readonly attribute double realTimeSeconds;
+};
diff --git a/comm/mailnews/base/public/nsISubscribableServer.idl b/comm/mailnews/base/public/nsISubscribableServer.idl
new file mode 100644
index 0000000000..70b0d980db
--- /dev/null
+++ b/comm/mailnews/base/public/nsISubscribableServer.idl
@@ -0,0 +1,74 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgIncomingServer;
+interface nsITreeView;
+interface nsIUTF8StringEnumerator;
+
+/**
+ * A listener to receive notification of the subscribable folders of a server.
+ */
+[scriptable, uuid(f337b84a-1dd1-11b2-97c7-fb8b2e3f2280)]
+interface nsISubscribeListener : nsISupports {
+ /**
+ * The server has finished finding all folders to subscribe to.
+ */
+ void OnDonePopulating();
+};
+
+[scriptable, uuid(14b8597a-755b-4e93-b364-e0903801e6ea)]
+interface nsISubscribableServer : nsISupports {
+ attribute nsISubscribeListener subscribeListener;
+ attribute char delimiter;
+
+ void startPopulating(in nsIMsgWindow aMsgWindow, in boolean forceToServer, in boolean getOnlyNew);
+ void startPopulatingWithUri(in nsIMsgWindow aMsgWindow, in boolean forceToServer, in AUTF8String uri);
+ void stopPopulating(in nsIMsgWindow aMsgWindow);
+
+ // return true if state changed, false otherwise
+ boolean setState(in AUTF8String path, in boolean state);
+
+ void subscribeCleanup();
+
+ void subscribe(in wstring name);
+ void unsubscribe(in wstring name);
+
+ void commitSubscribeChanges();
+
+ // other stuff
+ void setIncomingServer(in nsIMsgIncomingServer server);
+ void addTo(in AUTF8String aName, in boolean addAsSubscribed,
+ in boolean aSubscribable, in boolean aChangeIfExists);
+ void setAsSubscribed(in AUTF8String path);
+ void updateSubscribed();
+ void setShowFullName(in boolean showFullName);
+
+ // if path is null, use the root
+ boolean hasChildren(in AUTF8String path);
+ // if path is null, use the root
+ boolean isSubscribed(in AUTF8String path);
+ // if path is null, use the root
+ boolean isSubscribable(in AUTF8String path);
+ // if path is null, use the root
+ AString getLeafName(in AUTF8String path);
+
+ /**
+ * Returns the children uris underneath the specified uri (path).
+ *
+ * @param aPath The server's uri; If this is null or empty, then the
+ * root server uri will be used.
+ */
+ Array<AUTF8String> getChildURIs(in AUTF8String aPath);
+ // if path is null, use the root
+ AUTF8String getFirstChildURI(in AUTF8String path);
+
+ // for searching
+ void setSearchValue(in AString searchValue);
+ readonly attribute boolean supportsSubscribeSearch;
+ readonly attribute nsITreeView folderView;
+};
diff --git a/comm/mailnews/base/public/nsIUrlListener.idl b/comm/mailnews/base/public/nsIUrlListener.idl
new file mode 100644
index 0000000000..93df30b6cd
--- /dev/null
+++ b/comm/mailnews/base/public/nsIUrlListener.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+%{C++
+#include "nsIURL.h"
+%}
+
+/// General interface that signify URL processing.
+[scriptable, uuid(47618220-D008-11d2-8069-006008128C4E)]
+interface nsIUrlListener : nsISupports {
+ /**
+ * Called to signify the beginning of an URL processing.
+ *
+ * @param url URL being processed.
+ */
+ void OnStartRunningUrl(in nsIURI url);
+
+ /**
+ * Called to signify the end of an URL processing.
+ * This call is always preceded by a call to OnStartRunningUrl.
+ *
+ * @param url URL being processed.
+ * @param aExitCode A result code of URL processing.
+ */
+ void OnStopRunningUrl(in nsIURI url, in nsresult aExitCode);
+};
diff --git a/comm/mailnews/base/public/nsIUserInfo.idl b/comm/mailnews/base/public/nsIUserInfo.idl
new file mode 100644
index 0000000000..c8e6fef78d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIUserInfo.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * These are things the system may know about the current user.
+ */
+[scriptable, uuid(6c1034f0-1dd2-11b2-aa14-e6657ed7bb0b)]
+interface nsIUserInfo : nsISupports
+{
+ readonly attribute AString fullname;
+
+ readonly attribute AString emailAddress;
+
+ readonly attribute AString username;
+
+ readonly attribute AString domain;
+};
+
+%{C++
+
+// 14c13684-1dd2-11b2-9463-bb10ba742554
+#define NS_USERINFO_CID \
+{ 0x14c13684, 0x1dd2, 0x11b2, \
+ {0x94, 0x63, 0xbb, 0x10, 0xba, 0x74, 0x25, 0x54}}
+
+#define NS_USERINFO_CONTRACTID "@mozilla.org/userinfo;1"
+
+%}
diff --git a/comm/mailnews/base/public/nsMsgFolderFlags.idl b/comm/mailnews/base/public/nsMsgFolderFlags.idl
new file mode 100644
index 0000000000..a36fe002e4
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgFolderFlags.idl
@@ -0,0 +1,117 @@
+/*-*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+// This must be limited to unsigned long (uint32_t, no uint64_t)
+// as long as nsIMsgFolder exposes the 'flags' property which contains
+// all the flags values. The callers are used to do
+// (folder.flags & nsMsgFolderFlags.<flagname>) in Javascript
+// which cuts the value to 32bit only. See bug 813459.
+typedef unsigned long nsMsgFolderFlagType;
+
+/// Flags about a folder or a newsgroup.
+[scriptable,uuid(440cd0fc-b4b3-4a0f-a492-92fbe7920588)]
+interface nsMsgFolderFlags : nsISupports {
+ /**
+ * @name Folder Type Flags
+ * These flags define the type of folder. Exactly one will be set.
+ * @{
+ */
+ /// This folder is a newsgroup folder.
+ const nsMsgFolderFlagType Newsgroup = 0x00000001;
+ /// Used to be for a folder that is a news server (NewsHost).
+ const nsMsgFolderFlagType Unused3 = 0x00000002;
+ /// This folder is a mail folder.
+ const nsMsgFolderFlagType Mail = 0x00000004;
+ /** @} */
+
+ /** Whether this is a directory: NewsHosts are always directories;
+ * NewsGroups can be directories if we are in ``show all groups'' mode;
+ * Mail folders will have this bit if they are really directories, not files.
+ * (Note that directories may have zero children.)
+ */
+ const nsMsgFolderFlagType Directory = 0x00000008;
+ /** Whether the children of this folder are currently hidden in the listing.
+ * This will only be present if the nsMsgFolderFlags::Directory bit is on.
+ */
+ const nsMsgFolderFlagType Elided = 0x00000010;
+ /// Whether this is a virtual search folder
+ const nsMsgFolderFlagType Virtual = 0x00000020;
+
+ /** @name News Folder Flags
+ * These flags only occur in folders which have
+ * the nsMsgFolderFlags::Newsgroup bit set, and do
+ * not have the nsMsgFolderFlags::Directory or
+ * nsMsgFolderFlags::Elided bits set.
+ * @{
+ */
+ /// Used to be for folders representing a subscribed newsgroup (Subscribed).
+ const nsMsgFolderFlagType Unused5 = 0x00000040;
+ /// Used to be for new newsgroups added by the `Check New Groups' command.
+ const nsMsgFolderFlagType Unused2 = 0x00000080;
+ /** @} */
+
+ /** @name Mail Folder Flags
+ * These flags only occur in folders which have
+ * the nsMsgFolderFlags::Mail bit set, and do
+ * not have the nsMsgFolderFlags::Directory or
+ * nsMsgFolderFlags::Elided bits set.
+ * @{
+ */
+ /// Whether this is the trash folder.
+ const nsMsgFolderFlagType Trash = 0x00000100;
+ /// Whether this is a folder that sent mail gets delivered to.
+ const nsMsgFolderFlagType SentMail = 0x00000200;
+ /// Whether this is the folder in which unfinished, unsent messages are saved for later editing.
+ const nsMsgFolderFlagType Drafts = 0x00000400;
+ /// Whether this is the folder in which messages are queued for later delivery.
+ const nsMsgFolderFlagType Queue = 0x00000800;
+ /// Whether this is the primary inbox folder.
+ const nsMsgFolderFlagType Inbox = 0x00001000;
+ /// Whether this folder on online IMAP
+ const nsMsgFolderFlagType ImapBox = 0x00002000;
+ /// Whether this is an archive folder
+ const nsMsgFolderFlagType Archive = 0x00004000;
+ /// This used to be used for virtual newsgroups
+ const nsMsgFolderFlagType Unused1 = 0x00008000;
+ /// Used to be for categories
+ const nsMsgFolderFlagType Unused4 = 0x00010000;
+ /// Used to be for new msgs in a folder
+ const nsMsgFolderFlagType Unused7 = 0x00020000;
+ /// Used to be for a folder that is an IMAP server (ImapServer)
+ const nsMsgFolderFlagType Unused6 = 0x00040000;
+ /// This folder is an IMAP personal folder
+ const nsMsgFolderFlagType ImapPersonal = 0x00080000;
+ /// This folder is an IMAP public folder
+ const nsMsgFolderFlagType ImapPublic = 0x00100000;
+ /// This folder is another user's IMAP folder. Think of it like a folder that someone would share.
+ const nsMsgFolderFlagType ImapOtherUser = 0x00200000;
+ /// Whether this is the template folder
+ const nsMsgFolderFlagType Templates = 0x00400000;
+ /// This folder is one of your personal folders that is shared with other users
+ const nsMsgFolderFlagType PersonalShared = 0x00800000;
+ /// This folder is an IMAP \\Noselect folder
+ const nsMsgFolderFlagType ImapNoselect = 0x01000000;
+ /// This folder created offline (this is never set in current code,
+ /// but it is still checked for and obeyed if found on a folder.
+ const nsMsgFolderFlagType CreatedOffline = 0x02000000;
+ /// This imap folder cannot have children :-(
+ const nsMsgFolderFlagType ImapNoinferiors = 0x04000000;
+ /// This folder configured for offline use
+ const nsMsgFolderFlagType Offline = 0x08000000;
+ /// This folder has offline events to play back
+ const nsMsgFolderFlagType OfflineEvents = 0x10000000;
+ /// This folder is checked for new messages
+ const nsMsgFolderFlagType CheckNew = 0x20000000;
+ /// This folder is for spam messages
+ const nsMsgFolderFlagType Junk = 0x40000000;
+ /// This folder is in favorites view
+ const nsMsgFolderFlagType Favorite = 0x80000000;
+ /// Special-use folders
+ const nsMsgFolderFlagType SpecialUse = Inbox|Drafts|Trash|SentMail|
+ Templates|Junk|Archive|Queue;
+ /** @} */
+};
diff --git a/comm/mailnews/base/public/nsMsgGroupnameFlags.h b/comm/mailnews/base/public/nsMsgGroupnameFlags.h
new file mode 100644
index 0000000000..11056807ab
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgGroupnameFlags.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _msgGroupnameFlags_h_
+#define _msgGroupnameFlags_h_
+
+/* Flags in the subscribe pane (used inside of MSG_GroupNameLine). Where
+ the flags overlap with the nsMsgFolderFlags flags, it has the same value,
+ to reduce the chance of someone using the wrong constant. */
+
+/* Whether the children of this group are currently hidden in the listing.
+ This will only be present if it has any children. */
+#define MSG_GROUPNAME_FLAG_ELIDED 0x0010
+
+/* Whether this folder represents a moderated newsgroup. */
+#define MSG_GROUPNAME_FLAG_MODERATED 0x0020
+
+/* Whether this folder represents a subscribed newsgroup. */
+#define MSG_GROUPNAME_FLAG_SUBSCRIBED 0x0040
+
+/* A newsgroup which has just been added by the `Check New Groups` command. */
+#define MSG_GROUPNAME_FLAG_NEW_GROUP 0x0080
+
+/* Whether there are children of this group. Whether those children are visible
+ in this list is determined by the above "ELIDED" flag. Setting this to the
+ same value as an nsMsgFolderFlags IMAP server, since an IMAP _server_ will
+ never appear in the subscribe pane. */
+#define MSG_GROUPNAME_FLAG_HASCHILDREN 0x40000
+
+/* folder is an IMAP personal folder */
+#define MSG_GROUPNAME_FLAG_IMAP_PERSONAL 0x80000
+
+/* folder is an IMAP public folder */
+#define MSG_GROUPNAME_FLAG_IMAP_PUBLIC 0x100000
+
+/* folder is another user's IMAP folder */
+#define MSG_GROUPNAME_FLAG_IMAP_OTHER_USER 0x200000
+
+/* A \NoSelect IMAP folder */
+#define MSG_GROUPNAME_FLAG_IMAP_NOSELECT 0x400000
+
+/* whether or not this folder is one of your personal folders that is shared
+ with other users */
+#define MSG_GROUPNAME_FLAG_PERSONAL_SHARED 0x800000
+
+#endif
diff --git a/comm/mailnews/base/public/nsMsgHeaderMasks.h b/comm/mailnews/base/public/nsMsgHeaderMasks.h
new file mode 100644
index 0000000000..6de0194096
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgHeaderMasks.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _msgHeaderMasks_h_
+#define _msgHeaderMasks_h_
+// clang-format off
+
+DO NOT USE ANYMORE!!!
+/* This set enumerates the header fields which may be displayed in the
+ message composition window.
+ */
+#define MSG_FROM_HEADER_MASK 0x00000001
+#define MSG_REPLY_TO_HEADER_MASK 0x00000002
+#define MSG_TO_HEADER_MASK 0x00000004
+#define MSG_CC_HEADER_MASK 0x00000008
+#define MSG_BCC_HEADER_MASK 0x00000010
+#define MSG_FCC_HEADER_MASK 0x00000020
+#define MSG_NEWSGROUPS_HEADER_MASK 0x00000040
+#define MSG_FOLLOWUP_TO_HEADER_MASK 0x00000080
+#define MSG_SUBJECT_HEADER_MASK 0x00000100
+#define MSG_ATTACHMENTS_HEADER_MASK 0x00000200
+
+/* These next four are typically not ever displayed in the UI, but are still
+ stored and used internally. */
+#define MSG_ORGANIZATION_HEADER_MASK 0x00000400
+#define MSG_REFERENCES_HEADER_MASK 0x00000800
+#define MSG_OTHERRANDOMHEADERS_HEADER_MASK 0x00001000
+#define MSG_NEWSPOSTURL_HEADER_MASK 0x00002000
+
+#define MSG_PRIORITY_HEADER_MASK 0x00004000
+//#define MSG_NEWS_FCC_HEADER_MASK 0x00008000
+//#define MSG_MESSAGE_ENCODING_HEADER_MASK 0x00010000
+#define MSG_CHARACTER_SET_HEADER_MASK 0x00008000
+#define MSG_MESSAGE_ID_HEADER_MASK 0x00010000
+//#define MSG_NEWS_BCC_HEADER_MASK 0x00080000
+
+/* This is also not exposed to the UI; it's used internally to help remember
+ whether the original message had an HTML portion that we can quote. */
+//#define MSG_HTML_PART_HEADER_MASK 0x00100000
+
+/* The "body=" pseudo-header (as in "mailto:me?body=hi+there") */
+//#define MSG_DEFAULTBODY_HEADER_MASK 0x00200000
+
+#define MSG_X_TEMPLATE_HEADER_MASK 0x00020000
+
+#define MSG_FCC2_HEADER_MASK 0x00400000
+
+/* IMAP folders for posting */
+//#define MSG_IMAP_FOLDER_HEADER_MASK 0x02000000
+// clang-format on
+#endif
diff --git a/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h b/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h
new file mode 100644
index 0000000000..bc794567f8
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgLocalFolderHdrs_H
+#define _nsMsgLocalFolderHdrs_H
+// clang-format off
+
+/* The Netscape-specific header fields that we use for storing our
+ various bits of state in mail folders.
+ */
+#define X_MOZILLA_STATUS "X-Mozilla-Status"
+#define X_MOZILLA_STATUS_FORMAT X_MOZILLA_STATUS ": %4.4x"
+#define X_MOZILLA_STATUS_LEN /*1234567890123456*/ 16
+
+#define X_MOZILLA_STATUS2 "X-Mozilla-Status2"
+#define X_MOZILLA_STATUS2_FORMAT X_MOZILLA_STATUS2 ": %8.8x"
+#define X_MOZILLA_STATUS2_LEN /*12345678901234567*/ 17
+
+#define X_MOZILLA_DRAFT_INFO "X-Mozilla-Draft-Info"
+#define X_MOZILLA_DRAFT_INFO_LEN /*12345678901234567890*/ 20
+
+#define X_MOZILLA_NEWSHOST "X-Mozilla-News-Host"
+#define X_MOZILLA_NEWSHOST_LEN /*1234567890123456789*/ 19
+
+#define X_UIDL "X-UIDL"
+#define X_UIDL_LEN /*123456*/ 6
+
+#define CONTENT_LENGTH "Content-Length"
+#define CONTENT_LENGTH_LEN /*12345678901234*/ 14
+
+/* Provide a common means of detecting empty lines in a message. i.e. to detect the end of headers among other things...*/
+#define EMPTY_MESSAGE_LINE(buf) (buf[0] == '\r' || buf[0] == '\n' || buf[0] == '\0')
+
+
+// The default data for the X-Mozilla-Keys header. 80 spaces, room to set
+// a bunch of keywords before we have to rewrite the rest of the message.
+#define X_MOZILLA_KEYWORDS_BLANK " "
+#define X_MOZILLA_KEYWORDS_BLANK_LEN 80
+
+/* blank filled header to store keyword/tags in the mailbox */
+#define X_MOZILLA_KEYWORDS "X-Mozilla-Keys: " X_MOZILLA_KEYWORDS_BLANK MSG_LINEBREAK
+#define X_MOZILLA_KEYWORDS_LEN (sizeof(X_MOZILLA_KEYWORDS) - 1)
+
+// clang-format on
+#endif
diff --git a/comm/mailnews/base/public/nsMsgMessageFlags.idl b/comm/mailnews/base/public/nsMsgMessageFlags.idl
new file mode 100644
index 0000000000..2dac966b12
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgMessageFlags.idl
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+typedef unsigned long nsMsgMessageFlagType;
+
+/// Flags about a single message.
+[scriptable,uuid(1ea3acdb-7b9f-4e35-9513-76e0a0cc6baa)]
+interface nsMsgMessageFlags : nsISupports
+{
+ /// This message has been read
+ const nsMsgMessageFlagType Read = 0x00000001;
+
+ /// A reply to this message has been successfully sent
+ const nsMsgMessageFlagType Replied = 0x00000002;
+
+ /// This message has been flagged
+ const nsMsgMessageFlagType Marked = 0x00000004;
+
+ /**
+ * This message has already gone, but the folder hasn't been compacted yet.
+ * Since actually removing a message from a folder is a semi-expensive
+ * operation, we tend to delay it; messages with this bit set will be removed
+ * the next time folder compaction is done. Once this bit is set, it never
+ * gets un-set.
+ */
+ const nsMsgMessageFlagType Expunged = 0x00000008;
+
+ /**
+ * The subject of this message has "Re:" on the front. The folder summary
+ * uniquifies all of the strings in it, and to help this, any string which
+ * begins with "Re:" has that stripped first. This bit is then set, so that
+ * when presenting the message, we know to put it back (since the "Re:" is
+ * not itself stored in the file.)
+ */
+ const nsMsgMessageFlagType HasRe = 0x00000010;
+
+ /// The children of this sub-thread are folded in the display
+ const nsMsgMessageFlagType Elided = 0x00000020;
+
+ /// The message is a feed, originally downloaded in a server.type=rss account
+ const nsMsgMessageFlagType FeedMsg = 0x00000040;
+
+ /// This news article or IMAP message is present in the disk cache
+ const nsMsgMessageFlagType Offline = 0x00000080;
+
+ /// This thread is being watched
+ const nsMsgMessageFlagType Watched = 0x00000100;
+
+ /// This message's sender has been authenticated when sending this message
+ const nsMsgMessageFlagType SenderAuthed = 0x00000200;
+
+ /**
+ * This message's body is only the first ten or so of the message, and we
+ * need to add a link to let the user download the rest of it from the POP
+ * server.
+ */
+ const nsMsgMessageFlagType Partial = 0x00000400;
+
+ /**
+ * This message is queued for delivery. This only ever gets set on messages
+ * in the queue folder, but is used to protect against the case of other
+ * messages having made their way in there somehow -- if some other program
+ * put a message in the queue, we don't want to later deliver it!
+ */
+ const nsMsgMessageFlagType Queued = 0x00000800;
+
+ /// This message has been forwarded
+ const nsMsgMessageFlagType Forwarded = 0x00001000;
+
+ /// This message has been redirected
+ const nsMsgMessageFlagType Redirected = 0x00002000;
+
+ /**
+ * These are used to remember the message priority in the mozilla status
+ * flags, so we can regenerate a priority after a rule (or user) has changed
+ * it. They are not returned in MSG_MessageLine.flags, just in mozilla-status,
+ * so if you need more non-persistent flags, you could share these bits. But
+ * it would be wrong.
+ */
+ const nsMsgMessageFlagType Priorities = 0x0000E000;
+
+ /// This message is new since the last time the folder was closed
+ const nsMsgMessageFlagType New = 0x00010000;
+
+ /// This thread has been ignored
+ const nsMsgMessageFlagType Ignored = 0x00040000;
+
+ /// This IMAP message has been marked deleted on the server
+ const nsMsgMessageFlagType IMAPDeleted = 0x00200000;
+
+ /**
+ * This message has requested to send a message delivery notification to its
+ * sender
+ */
+ const nsMsgMessageFlagType MDNReportNeeded = 0x00400000;
+
+ /**
+ * A message delivery notification has been sent for this message. No more
+ * reports should be sent.
+ */
+ const nsMsgMessageFlagType MDNReportSent = 0x00800000;
+
+ /// This message is a template
+ const nsMsgMessageFlagType Template = 0x01000000;
+
+ // 0x8000000 is MSG_VIEW_FLAG_ISTHREAD, do not use.
+
+ /// This message has files attached to it
+ const nsMsgMessageFlagType Attachment = 0x10000000;
+
+ // 0x20000000 is MSG_VIEW_FLAG_DUMMY, do not use.
+ // 0x40000000 is MSG_VIEW_FLAG_HASCHILDREN, do not use.
+
+ /**
+ * These are used to remember the message labels in the mozilla status2
+ * flags. so we can regenerate a priority after a rule (or user) has changed
+ * it. They are not returned in nsMsgHdr.flags, just in mozilla-status2, so
+ * if you need more non-persistent flags, you could share these bits. But it
+ * would be wrong.
+ */
+ const nsMsgMessageFlagType Labels = 0x0E000000;
+
+ // We're trying to reserve the high byte of the flags for view flags, so,
+ // don't add flags to the high byte if possible.
+
+ /// The list of all message flags to not write to disk
+ const nsMsgMessageFlagType RuntimeOnly = Elided;
+};
+
+typedef unsigned long nsMsgProcessingFlagType;
+
+/**
+ * Definitions of processing flags. These flags are not saved to the database.
+ * They are used to define states for message processing. Any changes
+ * to these flags need to be supported in the key sets in nsMsgDBFolder
+ */
+[scriptable,uuid(1f7d642b-de2a-45f0-a27f-9c9ce0b741d8)]
+interface nsMsgProcessingFlags : nsISupports
+{
+ /// This message needs junk classification
+ const nsMsgProcessingFlagType ClassifyJunk = 0x00000001;
+
+ /// This message needs traits classification
+ const nsMsgProcessingFlagType ClassifyTraits = 0x00000002;
+
+ /// This message has completed any needed traits classification
+ const nsMsgProcessingFlagType TraitsDone = 0x00000004;
+
+ /// This message has completed any needed postPlugin filtering
+ const nsMsgProcessingFlagType FiltersDone = 0x00000008;
+
+ /// This message has a move scheduled by filters
+ const nsMsgProcessingFlagType FilterToMove = 0x00000010;
+
+ /**
+ * This message is new to the folder and has yet to be reported via the
+ * msgsClassified notification. This flag is required because the previously
+ * used mechanism relied on the database's list of new messages and its
+ * concept of 'new' is overloaded and has user-visible ramifications. This
+ * led to messages potentially being considered multiple times.
+ *
+ * Unfortunately none of the Done processing flags above are suitable for our
+ * needs because they are not consistently applied and basically constitute
+ * memory leaks (which makes the not consistently applied thing a good
+ * thing.)
+ *
+ * I suspect we cannot reliably convert the Done flags above to our use case
+ * either because of the situation where the user quits the program after the
+ * messages are added but before the messages are processed. Since the
+ * processing flags are suppression flags, assuming the 'new' status is
+ * persisted to the next time we are run, then this would represent a
+ * change in behaviour. I would need to exactly understand the new semantics
+ * to know for sure though.
+ */
+ const nsMsgProcessingFlagType NotReportedClassified = 0x00000020;
+
+ /// Number of processing flags
+ const nsMsgProcessingFlagType NumberOfFlags = 6;
+};
diff --git a/comm/mailnews/base/src/ABQueryUtils.jsm b/comm/mailnews/base/src/ABQueryUtils.jsm
new file mode 100644
index 0000000000..4944971ddf
--- /dev/null
+++ b/comm/mailnews/base/src/ABQueryUtils.jsm
@@ -0,0 +1,159 @@
+/* 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/. */
+
+/**
+ * This file contains helper methods for dealing with addressbook search URIs.
+ */
+
+const EXPORTED_SYMBOLS = [
+ "getSearchTokens",
+ "getModelQuery",
+ "modelQueryHasUserValue",
+ "generateQueryURI",
+ "encodeABTermValue",
+];
+
+/**
+ * Parse the multiword search string to extract individual search terms
+ * (separated on the basis of spaces) or quoted exact phrases to search
+ * against multiple fields of the addressbook cards.
+ *
+ * @param {string} aSearchString - The full search string entered by the user.
+ *
+ * @returns {Array} Array of separated search terms from the full search string.
+ */
+function getSearchTokens(aSearchString) {
+ // Trim leading and trailing whitespace and comma(s) to prevent empty search
+ // words when splitting unquoted parts of search string below.
+ let searchString = aSearchString
+ .replace(/^[,\s]+/, "")
+ .replace(/[,\s]+$/, "");
+ if (searchString == "") {
+ return [];
+ }
+
+ let quotedTerms = [];
+
+ // Split up multiple search words to create a *foo* and *bar* search against
+ // search fields, using the OR-search template from modelQuery for each word.
+ // If the search query has quoted terms like "foo bar", extract them as is.
+ let startIndex;
+ while ((startIndex = searchString.indexOf('"')) != -1) {
+ let endIndex = searchString.indexOf('"', startIndex + 1);
+ if (endIndex == -1) {
+ endIndex = searchString.length;
+ }
+
+ quotedTerms.push(searchString.substring(startIndex + 1, endIndex));
+ let query = searchString.substring(0, startIndex);
+ if (endIndex < searchString.length) {
+ query += searchString.substr(endIndex + 1);
+ }
+
+ searchString = query.trim();
+ }
+
+ let searchWords = [];
+ if (searchString.length != 0) {
+ // Split non-quoted search terms on whitespace and comma(s): Allow flexible
+ // incremental searches, and prevent false negatives for |Last, First| with
+ // |View > Show Name As > Last, First|, where comma is not found in data.
+ searchWords = quotedTerms.concat(searchString.split(/[,\s]+/));
+ } else {
+ searchWords = quotedTerms;
+ }
+
+ return searchWords;
+}
+
+/**
+ * For AB quicksearch or recipient autocomplete, get the normal or phonetic model
+ * query URL part from prefs, allowing users to customize these searches.
+ *
+ * @param {string} aBasePrefName - The full pref name of default, non-phonetic
+ * model query, e.g. mail.addr_book.quicksearchquery.format. If phonetic
+ * search is used, corresponding pref must exist:
+ * e.g. mail.addr_book.quicksearchquery.format.phonetic
+ * @returns {boolean} depending on mail.addr_book.show_phonetic_fields pref,
+ * the value of aBasePrefName or aBasePrefName + ".phonetic"
+ */
+function getModelQuery(aBasePrefName) {
+ let modelQuery = "";
+ if (
+ Services.prefs.getComplexValue(
+ "mail.addr_book.show_phonetic_fields",
+ Ci.nsIPrefLocalizedString
+ ).data == "true"
+ ) {
+ modelQuery = Services.prefs.getCharPref(aBasePrefName + ".phonetic");
+ } else {
+ modelQuery = Services.prefs.getCharPref(aBasePrefName);
+ }
+ // remove leading "?" to migrate existing customized values for mail.addr_book.quicksearchquery.format
+ // todo: could this be done in a once-off migration at install time to avoid repetitive calls?
+ if (modelQuery.startsWith("?")) {
+ modelQuery = modelQuery.slice(1);
+ }
+ return modelQuery;
+}
+
+/**
+ * Check if the currently used pref with the model query was customized by user.
+ *
+ * @param {string} aBasePrefName - The full pref name of default, non-phonetic
+ * model query, e.g. mail.addr_book.quicksearchquery.format
+ * If phonetic search is used, corresponding pref must exist:
+ * e.g. mail.addr_book.quicksearchquery.format.phonetic
+ * @returns {boolean} true or false
+ */
+function modelQueryHasUserValue(aBasePrefName) {
+ if (
+ Services.prefs.getComplexValue(
+ "mail.addr_book.show_phonetic_fields",
+ Ci.nsIPrefLocalizedString
+ ).data == "true"
+ ) {
+ return Services.prefs.prefHasUserValue(aBasePrefName + ".phonetic");
+ }
+ return Services.prefs.prefHasUserValue(aBasePrefName);
+}
+
+/*
+ * Given a database model query and a list of search tokens,
+ * return query URI.
+ *
+ * @param aModelQuery database model query
+ * @param aSearchWords an array of search tokens.
+ *
+ * @return query URI.
+ */
+function generateQueryURI(aModelQuery, aSearchWords) {
+ // If there are no search tokens, we simply return an empty string.
+ if (!aSearchWords || aSearchWords.length == 0) {
+ return "";
+ }
+
+ let queryURI = "";
+ aSearchWords.forEach(
+ searchWord =>
+ (queryURI += aModelQuery.replace(/@V/g, encodeABTermValue(searchWord)))
+ );
+
+ // queryURI has all the (or(...)) searches, link them up with (and(...)).
+ queryURI = "?(and" + queryURI + ")";
+
+ return queryURI;
+}
+
+/**
+ * Encode the string passed as value into an addressbook search term.
+ * The '(' and ')' characters are special for the addressbook
+ * search query language, but are not escaped in encodeURIComponent()
+ * so must be done manually on top of it.
+ */
+function encodeABTermValue(aString) {
+ return encodeURIComponent(aString)
+ .replace(/\(/g, "%28")
+ .replace(/\)/g, "%29");
+}
diff --git a/comm/mailnews/base/src/FolderLookupService.jsm b/comm/mailnews/base/src/FolderLookupService.jsm
new file mode 100644
index 0000000000..9530dc6e6e
--- /dev/null
+++ b/comm/mailnews/base/src/FolderLookupService.jsm
@@ -0,0 +1,163 @@
+/* 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/. */
+
+/**
+ * This module implements the folder lookup service (nsIFolderLookupService).
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["FolderLookupService"];
+
+// This ensures that the service is only created once.
+var gCreated = false;
+
+/**
+ * FolderLookupService maintains an index of folders and provides
+ * lookup by folder URI.
+ *
+ * @class
+ */
+function FolderLookupService() {
+ if (gCreated) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ this._map = new Map();
+ gCreated = true;
+}
+
+FolderLookupService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderLookupService"]),
+
+ /**
+ * Fetch the folder corresponding to the given URI.
+ * Will only return folders which already exist and have a parent. If this
+ * not the case then null is returned.
+ *
+ * @param {string} uri - URI of folder to get.
+ * @returns {nsIMsgFolder|null}
+ */
+ getFolderForURL(uri) {
+ let folder = this._getExisting(uri);
+
+ if (folder && !this._isValidFolder(folder)) {
+ folder = null; // no dangling folders!
+ }
+ return folder;
+ },
+
+ /**
+ * Fetch the folder corresponding to the given URI, creating it if it does
+ * not exist. If the folder is created, it will be a "dangling" folder,
+ * without a parent and not part of a normal folder hierarchy.
+ * A lot of code relies on this behaviour, but for new code this
+ * call should probably be avoided.
+ *
+ * @param {string} uri - URI of folder to get.
+ * @returns {nsIMsgFolder}
+ */
+ getOrCreateFolderForURL(uri) {
+ let folder = this._getExisting(uri);
+ if (folder) {
+ return folder;
+ }
+
+ // Create new folder.
+
+ // Check that uri has an active scheme, in case this folder is from
+ // an extension that is currently disabled or hasn't started up yet.
+ let schemeMatch = uri.match(/^([-+.\w]+):/);
+ if (!schemeMatch) {
+ return null;
+ }
+ let scheme = schemeMatch[1];
+ let contractID = "@mozilla.org/mail/folder-factory;1?name=" + scheme;
+ if (!(contractID in Cc)) {
+ console.error(
+ "getOrCreateFolderForURL: factory not registered for " + uri
+ );
+ return null;
+ }
+
+ let factory = Components.manager.getClassObject(
+ Cc[contractID],
+ Ci.nsIFactory
+ );
+ if (!factory) {
+ console.error(
+ "getOrCreateFolderForURL: failed to get factory for " + uri
+ );
+ return null;
+ }
+
+ folder = factory.createInstance(Ci.nsIMsgFolder);
+ if (folder) {
+ folder.Init(uri);
+ // Add the new folder to our map. Store a weak reference instead, so that
+ // the folder can be closed when necessary.
+ let weakRef = folder
+ .QueryInterface(Ci.nsISupportsWeakReference)
+ .GetWeakReference();
+ this._map.set(uri, weakRef);
+ }
+
+ return folder;
+ },
+
+ /**
+ * Set pretty name again from original name on all folders,
+ * typically used when locale changes.
+ */
+ setPrettyNameFromOriginalAllFolders() {
+ for (const val of this._map.values()) {
+ try {
+ let folder = val.QueryReferent(Ci.nsIMsgFolder);
+ folder.setPrettyNameFromOriginal();
+ } catch (e) {}
+ }
+ },
+
+ // "private" stuff starts here.
+
+ /**
+ * Internal helper to find a folder (which may or may not be dangling).
+ *
+ * @param {string} uri - URI of folder to look up.
+ *
+ * @returns {nsIMsgFolder|null} - The folder, if in the index, else null.
+ */
+ _getExisting(uri) {
+ let folder = null;
+ // already created?
+ if (this._map.has(uri)) {
+ try {
+ folder = this._map.get(uri).QueryReferent(Ci.nsIMsgFolder);
+ } catch (e) {
+ // The object was deleted, so we can drop it.
+ this._map.delete(uri);
+ }
+ }
+ return folder;
+ },
+
+ /**
+ * Internal helper function to test if a folder is dangling or parented.
+ * Because we can return folders that don't exist, and we may be working
+ * with a deleted folder but we're still holding on to the reference. For
+ * valid folders, one of two scenarios is true: either the folder has a parent
+ * (the deletion code clears the parent to indicate its nonvalidity), or the
+ * folder is a root folder of some server. Getting the root folder may throw
+ * an exception if we attempted to create a server that doesn't exist, so we
+ * need to guard for that error.
+ *
+ * @returns {boolean} - true if folder valid (and parented).
+ */
+ _isValidFolder(folder) {
+ try {
+ return folder.parent != null || folder.rootFolder == folder;
+ } catch (e) {
+ return false;
+ }
+ },
+};
diff --git a/comm/mailnews/base/src/FolderUtils.jsm b/comm/mailnews/base/src/FolderUtils.jsm
new file mode 100644
index 0000000000..a438aa7480
--- /dev/null
+++ b/comm/mailnews/base/src/FolderUtils.jsm
@@ -0,0 +1,364 @@
+/* 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/. */
+
+/**
+ * This file contains helper methods for dealing with nsIMsgFolders.
+ */
+
+const EXPORTED_SYMBOLS = ["FolderUtils"];
+
+var FolderUtils = {
+ allAccountsSorted,
+ compareAccounts,
+ folderNameCompare,
+ getFolderIcon,
+ getFolderProperties,
+ getMostRecentFolders,
+ getSpecialFolderString,
+ canRenameDeleteJunkMail,
+ isSmartTagsFolder,
+ isSmartVirtualFolder,
+};
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Returns a string representation of a folder's "special" type.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder whose special type to return.
+ * @returns {string} the special type of the folder.
+ */
+function getSpecialFolderString(aFolder) {
+ let flags = aFolder.flags;
+ if (flags & Ci.nsMsgFolderFlags.Inbox) {
+ return "Inbox";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Trash) {
+ return "Trash";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Queue) {
+ return "Outbox";
+ }
+ if (flags & Ci.nsMsgFolderFlags.SentMail) {
+ return "Sent";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Drafts) {
+ return "Drafts";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Templates) {
+ return "Templates";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Junk) {
+ return "Junk";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Archive) {
+ return "Archive";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Virtual) {
+ return "Virtual";
+ }
+ return "none";
+}
+
+/**
+ * This function is meant to be used with trees. It returns the property list
+ * for all of the common properties that css styling is based off of.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder whose properties should be
+ * returned as a string.
+ * @param {boolean} aOpen - Whether the folder is open (not expanded).
+ *
+ * @returns {string} A string of the property names, delimited by space.
+ */
+function getFolderProperties(aFolder, aOpen) {
+ const nsIMsgFolder = Ci.nsIMsgFolder;
+ let properties = [];
+
+ properties.push("folderNameCol");
+
+ properties.push("serverType-" + aFolder.server.type);
+
+ // set the SpecialFolder attribute
+ properties.push("specialFolder-" + getSpecialFolderString(aFolder));
+
+ // Now set the biffState
+ switch (aFolder.biffState) {
+ case nsIMsgFolder.nsMsgBiffState_NewMail:
+ properties.push("biffState-NewMail");
+ break;
+ case nsIMsgFolder.nsMsgBiffState_NoMail:
+ properties.push("biffState-NoMail");
+ break;
+ default:
+ properties.push("biffState-UnknownMail");
+ }
+
+ properties.push("isSecure-" + aFolder.server.isSecure);
+
+ // A folder has new messages, or a closed folder or any subfolder has new messages.
+ if (
+ aFolder.hasNewMessages ||
+ (!aOpen && aFolder.hasSubFolders && aFolder.hasFolderOrSubfolderNewMessages)
+ ) {
+ properties.push("newMessages-true");
+ }
+
+ if (aFolder.isServer) {
+ properties.push("isServer-true");
+ } else {
+ // We only set this if we're not a server
+ let shallowUnread = aFolder.getNumUnread(false);
+ if (shallowUnread > 0) {
+ properties.push("hasUnreadMessages-true");
+ } else {
+ // Make sure that shallowUnread isn't negative
+ shallowUnread = 0;
+ }
+ let deepUnread = aFolder.getNumUnread(true);
+ if (deepUnread - shallowUnread > 0) {
+ properties.push("subfoldersHaveUnreadMessages-true");
+ }
+ }
+
+ properties.push("noSelect-" + aFolder.noSelect);
+ properties.push("imapShared-" + aFolder.imapShared);
+
+ return properties.join(" ");
+}
+
+/**
+ * Returns the sort order value based on the server type to be used for sorting.
+ * The servers (accounts) go in the following order:
+ * (0) default account, (1) other mail accounts, (2) Local Folders,
+ * (3) IM accounts, (4) RSS, (5) News, (9) others (no server)
+ * This ordering is encoded in the .sortOrder property of each server type.
+ *
+ * @param {nsIMsgIncomingServer} aServer -The server to get sort order for.
+ */
+function getServerSortOrder(aServer) {
+ // If there is no server sort this object to the end.
+ if (!aServer) {
+ return 999999999;
+ }
+
+ // Otherwise get the server sort order from the Account manager.
+ return MailServices.accounts.getSortOrder(aServer);
+}
+
+/**
+ * Compares the passed in accounts according to their precedence.
+ */
+function compareAccounts(aAccount1, aAccount2) {
+ return (
+ getServerSortOrder(aAccount1.incomingServer) -
+ getServerSortOrder(aAccount2.incomingServer)
+ );
+}
+
+/**
+ * Returns a list of accounts sorted by server type.
+ *
+ * @param {boolean} aExcludeIMAccounts - Remove IM accounts from the list?
+ */
+function allAccountsSorted(aExcludeIMAccounts) {
+ // This is a HACK to work around bug 41133. If we have one of the
+ // dummy "news" accounts there, that account won't have an
+ // incomingServer attached to it, and everything will blow up.
+ let accountList = MailServices.accounts.accounts.filter(
+ a => a.incomingServer
+ );
+
+ // Remove IM servers.
+ if (aExcludeIMAccounts) {
+ accountList = accountList.filter(a => a.incomingServer.type != "im");
+ }
+
+ return accountList;
+}
+
+/**
+ * Returns the most recently used/modified folders from the passed in list.
+ *
+ * @param {nsIMsgFolder[]} aFolderList - The array of folders to search
+ * for recent folders.
+ * @param {integer} aMaxHits - How many folders to return.
+ * @param {"MRMTime"|"MRUTime"} aTimeProperty - Which folder time property to
+ * use. Use "MRMTime" for most recently modified time.
+ * Use "MRUTime" for most recently used time.
+ */
+function getMostRecentFolders(aFolderList, aMaxHits, aTimeProperty) {
+ let recentFolders = [];
+ const monthOld = Math.floor((Date.now() - 31 * 24 * 60 * 60 * 1000) / 1000);
+
+ /**
+ * This sub-function will add a folder to the recentFolders array if it
+ * is among the aMaxHits most recent. If we exceed aMaxHits folders,
+ * it will pop the oldest folder, ensuring that we end up with the
+ * right number.
+ *
+ * @param {nsIMsgFolders} aFolder - The folder to check for recency.
+ */
+ let oldestTime = 0;
+ function addIfRecent(aFolder) {
+ let time = 0;
+ try {
+ time = Number(aFolder.getStringProperty(aTimeProperty)) || 0;
+ } catch (e) {}
+ if (time <= oldestTime || time < monthOld) {
+ return;
+ }
+
+ if (recentFolders.length == aMaxHits) {
+ recentFolders.sort((a, b) => a.time < b.time);
+ recentFolders.pop();
+ oldestTime = recentFolders[recentFolders.length - 1].time;
+ }
+ recentFolders.push({ folder: aFolder, time });
+ }
+
+ for (let folder of aFolderList) {
+ addIfRecent(folder);
+ }
+
+ return recentFolders.map(f => f.folder);
+}
+
+/**
+ * A locale dependent comparison function to produce a case-insensitive sort order
+ * used to sort folder names.
+ *
+ * @param {string} aString1 - First string to compare.
+ * @param {string} aString2 - Second string to compare.
+ * @returns {interger} A positive number if aString1 > aString2,
+ * negative number if aString1 > aString2, otherwise 0.
+ */
+function folderNameCompare(aString1, aString2) {
+ // TODO: improve this as described in bug 992651.
+ return aString1
+ .toLocaleLowerCase()
+ .localeCompare(aString2.toLocaleLowerCase());
+}
+
+/**
+ * Get the icon to use for this folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to get icon for.
+ * @returns {string} URL of suitable icon.
+ */
+function getFolderIcon(folder) {
+ let iconName;
+ if (folder.isServer) {
+ switch (folder.server.type) {
+ case "nntp":
+ iconName = folder.server.isSecure ? "globe-secure.svg" : "globe.svg";
+ break;
+ case "imap":
+ case "pop":
+ iconName = folder.server.isSecure ? "mail-secure.svg" : "mail.svg";
+ break;
+ case "none":
+ iconName = "folder.svg";
+ break;
+ case "rss":
+ iconName = "rss.svg";
+ break;
+ default:
+ iconName = "mail.svg";
+ break;
+ }
+ } else if (folder.server.type == "nntp") {
+ iconName = "newsletter.svg";
+ } else {
+ switch (getSpecialFolderString(folder)) {
+ case "Virtual":
+ if (isSmartTagsFolder(folder)) {
+ iconName = "tag.svg";
+ } else {
+ iconName = "folder-filter.svg";
+ }
+ break;
+ case "Junk":
+ iconName = "spam.svg";
+ break;
+ case "Templates":
+ iconName = "template.svg";
+ break;
+ case "Archive":
+ iconName = "archive.svg";
+ break;
+ case "Trash":
+ iconName = "trash.svg";
+ break;
+ case "Drafts":
+ iconName = "draft.svg";
+ break;
+ case "Outbox":
+ iconName = "outbox.svg";
+ break;
+ case "Sent":
+ iconName = "sent.svg";
+ break;
+ case "Inbox":
+ iconName = "inbox.svg";
+ break;
+ default:
+ iconName = "folder.svg";
+ break;
+ }
+ }
+
+ return `chrome://messenger/skin/icons/new/compact/${iconName}`;
+}
+
+/**
+ * Checks if `folder` is a virtual folder for the Unified Folders pane mode.
+ *
+ * @param {nsIMsgFolder} folder
+ * @returns {boolean}
+ */
+function isSmartVirtualFolder(folder) {
+ return (
+ folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.server.hostName == "smart mailboxes" &&
+ folder.parent?.isServer
+ );
+}
+
+/**
+ * Checks if `folder` is a virtual folder for the Tags folder pane mode.
+ *
+ * @param {nsIMsgFolder} folder
+ * @returns {boolean}
+ */
+function isSmartTagsFolder(folder) {
+ return (
+ folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.server.hostName == "smart mailboxes" &&
+ folder.parent?.name == "tags"
+ );
+}
+
+/**
+ * Checks if the configured junk mail can be renamed or deleted.
+ *
+ * @param {string} aFolderUri
+ */
+function canRenameDeleteJunkMail(aFolderUri) {
+ // Go through junk mail settings for all servers and see if the folder is set/used by anyone.
+ for (let server of MailServices.accounts.allServers) {
+ let settings = server.spamSettings;
+ // If junk mail control or move junk mail to folder option is disabled then
+ // allow the folder to be removed/renamed since the folder is not used in this case.
+ if (!settings.level || !settings.moveOnSpam) {
+ continue;
+ }
+ if (settings.spamFolderURI == aFolderUri) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/base/src/HeaderReader.h b/comm/mailnews/base/src/HeaderReader.h
new file mode 100644
index 0000000000..da8defbbe0
--- /dev/null
+++ b/comm/mailnews/base/src/HeaderReader.h
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef HeaderReader_h__
+#define HeaderReader_h__
+
+#include <algorithm>
+#include "LineReader.h"
+#include "nsMsgUtils.h"
+#include "nsString.h"
+#include "mozilla/Span.h"
+
+/**
+ * HeaderReader parses mail headers from a buffer.
+ * The input is fed in via Parse(), and a callback function is invoked for
+ * each header encountered.
+ *
+ * General goals:
+ *
+ * - Incremental. Parse() can be called multiple times as a buffer grows.
+ * - Works in-place. Headers are returned as byte ranges within the data.
+ * - Works with a partial header block (e.g. sniffing the first N bytes
+ * of a message file). It won't mistakenly emit an incomplete header.
+ * - Track exact byte offsets for values, to support rewriting headers in
+ * place. This is needed to support X-Mozilla-Status et al.
+ * - Avoids copying data where possible.
+ * - Callback is inlined.
+ * - Callback can halt processing (by returning false).
+ * - Tolerant of real-world oddness in input data (for now, we just skip
+ * lines which don't make sense).
+ *
+ * Example usage:
+ * nsCString raw = "To: Alice\r\nFrom: Bob\r\n\r\n...Message body..."_ns;
+ * auto cb = [&](HeaderReader::Header const& hdr) {
+ * printf("-> '%s':'%s'\n", hdr.Name(raw), hdr.Value(raw));
+ * return true;
+ * };
+ *
+ * HeaderReader rdr;
+ * rdr.Parse(raw, cb);
+ * // -> 'To':'Alice'
+ * // -> 'From':'Bob'
+ *
+ * See TestHeaderReader.cpp for more examples.
+ */
+class HeaderReader {
+ public:
+ /**
+ * Parse() scans an input buffer and invokes a callback for each complete
+ * header found.
+ *
+ * It can be called any number of times - it'll pick up where it left off.
+ * The idea is that the caller can accumulate data in multiple chunks and
+ * call Parse() to extract headers incrementally as they come in.
+ * It does rely on data being a single contiguous allocation, but it
+ * doesn't require the data being located in the same memory location
+ * each time. So can it can be safely used on a growable buffer.
+ *
+ * Signature of callback is:
+ * bool hdrCallback(HeaderReader::Hdr const& hdr);
+ *
+ * The callback should return true to continue parsing, or false to halt.
+ * This allows, for example, an early-out if you're scanning for one
+ * specific header and don't care about the rest.
+ *
+ * Parse() stops when one of these conditions is true:
+ * 1. The end of the header block is reached (the final blank line marker
+ * is consumed). Subsequent calls to IsComplete() will return true.
+ * 2. The callback returns false. If Parse() is called again, it will
+ * safely pick up where it left off.
+ * 3. No more headers can be read. There may be some unconsumed data
+ * returned (eg a partial line). Parse() can be safely called again
+ * when more data becomes available. It will resume from the point it
+ * reached previously.
+ *
+ * It is safe to call Parse() on a truncated header block. It will only
+ * invoke the callback for headers which are unambiguously complete.
+ *
+ * @param data - bytes containing the header block to parse.
+ * @param hdrCallback - callback to invoke for each header found
+ *
+ * @returns a span containing the unconsumed (leftover) data.
+ */
+ template <typename HeaderFn>
+ mozilla::Span<const char> Parse(mozilla::Span<const char> data,
+ HeaderFn hdrCallback);
+
+ /**
+ * Complete() returns true if the header block has been fully parsed.
+ * Further calls to Parse() will consume no more data.
+ * The blank line which separates the header block from the body is consumed.
+ */
+ bool IsComplete() const { return mFinished; }
+
+ /**
+ * Hdr holds offsets to a name/value pair within a header block.
+ * The name starts at pos.
+ * The value starts at pos+rawValOffset.
+ */
+ struct Hdr {
+ uint32_t pos{0}; // Start position of header within the block.
+ uint32_t len{0}; // Length of entire header, including final EOL.
+ uint32_t nameLen{0}; // Length of name.
+ uint32_t rawValOffset{0}; // Where the value starts, relative to pos.
+ uint32_t rawValLen{0}; // Excludes final EOL.
+ bool IsEmpty() const { return len == 0; }
+
+ /**
+ * Access the header name as a string.
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns the name within data, wrapped for string access (so it is
+ * valid only as long as data is valid).
+ */
+ nsDependentCSubstring Name(mozilla::Span<const char> data) const {
+ return nsDependentCSubstring(data.Elements() + pos, nameLen);
+ }
+ /**
+ * Access the raw value as a string.
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns the raw data, EOLs and all, wrapped for string access (so it
+ * is valid only as long as data is valid).
+ */
+ nsDependentCSubstring RawValue(mozilla::Span<const char> data) const {
+ return nsDependentCSubstring(data.Elements() + pos + rawValOffset,
+ rawValLen);
+ }
+ /**
+ * Decode the 'cooked' value into a string.
+ * NOTE: handles unfolding multi-line values. No attempt (yet) at dealing
+ * with comments or quoted strings...
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns a new string containing the value.
+ */
+ nsCString Value(mozilla::Span<const char> data) const {
+ nsCString val(RawValue(data));
+ val.ReplaceSubstring("\r\n"_ns, ""_ns);
+ val.ReplaceSubstring("\n"_ns, ""_ns);
+ return val;
+ }
+
+ /**
+ * EOL() returns a string containing the eol characters at the end of the
+ * header. It will be "\n" or "\r\n".
+ * Calling this on an empty hdr struct is unsupported.
+ */
+ nsDependentCSubstring EOL(mozilla::Span<const char> data) const {
+ MOZ_ASSERT(len >= 2); // Empty or malformed?
+
+ uint32_t i = pos + len;
+ int n = 0;
+ if (data[i - 1] == '\n') {
+ ++n;
+ if (data[i - 2] == '\r') {
+ ++n;
+ }
+ }
+ return nsDependentCSubstring(data.Elements() + pos + len - n, n);
+ }
+ };
+
+ private:
+ // How far Parse() has gone so far.
+ uint32_t mPos{0};
+
+ // The current header we're accumulating.
+ Hdr mHdr;
+
+ // Number of EOL chars at the end of previous line (so we can strip it if the
+ // next line is folded).
+ int mEOLSize{0};
+
+ // Set when end of header block detected.
+ bool mFinished{false};
+
+ template <typename HeaderFn>
+ bool HandleLine(mozilla::Span<const char> line, HeaderFn hdrCallback);
+};
+
+// Parse() implementation.
+template <typename HeaderFn>
+mozilla::Span<const char> HeaderReader::Parse(mozilla::Span<const char> data,
+ HeaderFn hdrCallback) {
+ // If were're resuming, skip what we've already scanned.
+ auto remaining = mozilla::Span<const char>(data.cbegin() + mPos, data.cend());
+ if (mFinished) {
+ return remaining;
+ }
+ // Iterate over all the lines of our input.
+ remaining = SplitLines(remaining,
+ [this, hdrCallback](mozilla::Span<const char> line) {
+ return HandleLine(line, hdrCallback);
+ });
+
+ if (!mFinished) {
+ // We didn't get to the end of the header block, but we may still be
+ // able to finalise a previously-started header...
+ if (!mHdr.IsEmpty()) {
+ if (remaining.Length() > 0 && remaining[0] != ' ' &&
+ remaining[0] != '\t') {
+ // Next line isn't folded, so we know the header is complete.
+ mHdr.rawValLen -= mEOLSize;
+ hdrCallback(mHdr);
+ } else {
+ // Can't tell if header is complete. Rewind and try again next time.
+ mPos = mHdr.pos;
+ remaining =
+ mozilla::Span<const char>(data.cbegin() + mPos, data.cend());
+ }
+ mHdr = Hdr();
+ }
+ }
+ return remaining;
+}
+
+// Helper function - we call this on each complete line we encounter.
+template <typename HeaderFn>
+bool HeaderReader::HandleLine(mozilla::Span<const char> line,
+ HeaderFn hdrCallback) {
+ // Should never be here if we've finished.
+ MOZ_ASSERT(!mFinished);
+ // we should _never_ see empty strings.
+ MOZ_ASSERT(!line.IsEmpty());
+
+ // Find the EOL sequence (CRLF or LF).
+ auto eol = line.cend();
+ auto p = eol;
+ if (p > line.cbegin() && *(p - 1) == '\n') {
+ --eol;
+ if ((p - 1) > line.cbegin() && *(p - 2) == '\r') {
+ --eol;
+ }
+ }
+ // We should never have been called with a non-terminated line.
+ MOZ_ASSERT(eol != line.cend());
+
+ // Blank line indicates end of header block.
+ if (eol == line.cbegin()) {
+ if (!mHdr.IsEmpty()) {
+ // Emit the completed header.
+ mHdr.rawValLen -= mEOLSize;
+ hdrCallback(mHdr);
+ mHdr = Hdr();
+ }
+ mFinished = true;
+ mPos += line.Length();
+ return false; // Stop.
+ }
+
+ // A folded line?
+ // Leading space or tab indicates continuation of previous value.
+ if (line[0] == ' ' || line[0] == '\t') {
+ if (!mHdr.IsEmpty()) {
+ // Grow the existing header.
+ mHdr.len += line.Length();
+ mHdr.rawValLen += line.Length();
+ mEOLSize = line.cend() - eol;
+ } else {
+ // UHOH - a folded value but we haven't started a header...
+ // Not much we can do, so we'll just ignore the line.
+ NS_WARNING("Malformed header (bare continuation)");
+ }
+ mPos += line.Length();
+ return true; // Next line, please.
+ }
+
+ bool keepGoing = true;
+ // By now, we're expecting a "name: value" line, to start a fresh header.
+ if (!mHdr.IsEmpty()) {
+ // Flush previous header now we know it's complete.
+ mHdr.rawValLen -= mEOLSize;
+ keepGoing = hdrCallback(mHdr);
+ mHdr = Hdr();
+ }
+
+ auto colon = std::find(line.cbegin(), line.cend(), ':');
+ if (colon == line.cend()) {
+ // UHOH. We were expecting a "name: value" line, but didn't find one.
+ // Just ignore this line.
+ NS_WARNING("Malformed header (expected 'name: value')");
+ mPos += line.Length();
+ return keepGoing;
+ }
+ auto val = colon + 1;
+ if (*val == ' ' || *val == '\t') {
+ // Skip single leading whitespace.
+ ++val;
+ }
+
+ // Start filling out the new header (it may grow if folded lines come next).
+ mHdr.pos = mPos;
+ mHdr.len = line.Length();
+ mHdr.nameLen = colon - line.cbegin();
+
+ mHdr.rawValOffset = val - line.cbegin();
+ mHdr.rawValLen = line.cend() - val;
+ mEOLSize = line.cend() - eol;
+ mPos += line.Length();
+ return keepGoing;
+}
+
+#endif
diff --git a/comm/mailnews/base/src/JXON.jsm b/comm/mailnews/base/src/JXON.jsm
new file mode 100644
index 0000000000..00c1f2bb1f
--- /dev/null
+++ b/comm/mailnews/base/src/JXON.jsm
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a modification of the JXON parsers found on the page
+// <https://developer.mozilla.org/en-US/docs/JXON>
+
+var EXPORTED_SYMBOLS = ["JXON"];
+
+var JXON = new (function () {
+ const sValueProp = "value"; /* you can customize these values */
+ const sAttributesProp = "attr";
+ const sAttrPref = "@";
+ const sElementListPrefix = "$";
+ const sConflictSuffix = "_"; // used when there's a name conflict with special JXON properties
+ const aCache = [];
+ const rIsBool = /^(?:true|false)$/i;
+
+ function parseText(sValue) {
+ if (rIsBool.test(sValue)) {
+ return sValue.toLowerCase() === "true";
+ }
+ if (isFinite(sValue)) {
+ return parseFloat(sValue);
+ }
+ if (isFinite(Date.parse(sValue))) {
+ return new Date(sValue);
+ }
+ return sValue;
+ }
+
+ function EmptyTree() {}
+ EmptyTree.prototype = {
+ toString() {
+ return "null";
+ },
+ valueOf() {
+ return null;
+ },
+ };
+
+ function objectify(vValue) {
+ if (vValue === null) {
+ return new EmptyTree();
+ } else if (vValue instanceof Object) {
+ return vValue;
+ }
+ return new vValue.constructor(vValue); // What does this? copy?
+ }
+
+ function createObjTree(oParentNode, nVerb, bFreeze, bNesteAttr) {
+ const nLevelStart = aCache.length;
+ const bChildren = oParentNode.hasChildNodes();
+ const bAttributes = oParentNode.attributes && oParentNode.attributes.length;
+ const bHighVerb = Boolean(nVerb & 2);
+
+ var sProp = 0;
+ var vContent = 0;
+ var nLength = 0;
+ var sCollectedTxt = "";
+ var vResult = bHighVerb
+ ? {}
+ : /* put here the default value for empty nodes: */ true;
+
+ if (bChildren) {
+ for (
+ var oNode, nItem = 0;
+ nItem < oParentNode.childNodes.length;
+ nItem++
+ ) {
+ oNode = oParentNode.childNodes.item(nItem);
+ if (oNode.nodeType === 4) {
+ // CDATASection
+ sCollectedTxt += oNode.nodeValue;
+ } else if (oNode.nodeType === 3) {
+ // Text
+ sCollectedTxt += oNode.nodeValue;
+ } else if (oNode.nodeType === 1) {
+ // Element
+ aCache.push(oNode);
+ }
+ }
+ }
+
+ const nLevelEnd = aCache.length;
+ const vBuiltVal = parseText(sCollectedTxt);
+
+ if (!bHighVerb && (bChildren || bAttributes)) {
+ vResult = nVerb === 0 ? objectify(vBuiltVal) : {};
+ }
+
+ for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {
+ sProp = aCache[nElId].nodeName;
+ if (sProp == sValueProp || sProp == sAttributesProp) {
+ sProp = sProp + sConflictSuffix;
+ }
+ vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);
+ if (!vResult.hasOwnProperty(sProp)) {
+ vResult[sProp] = vContent;
+ vResult[sElementListPrefix + sProp] = [];
+ }
+ vResult[sElementListPrefix + sProp].push(vContent);
+ nLength++;
+ }
+
+ if (bAttributes) {
+ const nAttrLen = oParentNode.attributes.length;
+ const sAPrefix = bNesteAttr ? "" : sAttrPref;
+ const oAttrParent = bNesteAttr ? {} : vResult;
+
+ for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {
+ oAttrib = oParentNode.attributes.item(nAttrib);
+ oAttrParent[sAPrefix + oAttrib.name] = parseText(oAttrib.value);
+ }
+
+ if (bNesteAttr) {
+ if (bFreeze) {
+ Object.freeze(oAttrParent);
+ }
+ vResult[sAttributesProp] = oAttrParent;
+ nLength -= nAttrLen - 1;
+ }
+ }
+
+ if (
+ nVerb === 3 ||
+ ((nVerb === 2 || (nVerb === 1 && nLength > 0)) && sCollectedTxt)
+ ) {
+ vResult[sValueProp] = vBuiltVal;
+ } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {
+ vResult = vBuiltVal;
+ }
+
+ if (bFreeze && (bHighVerb || nLength > 0)) {
+ Object.freeze(vResult);
+ }
+
+ aCache.length = nLevelStart;
+
+ return vResult;
+ }
+
+ this.build = function (
+ oXMLParent,
+ nVerbosity /* optional */,
+ bFreeze /* optional */,
+ bNesteAttributes /* optional */
+ ) {
+ const _nVerb =
+ typeof nVerbosity === "number"
+ ? nVerbosity & 3
+ : /* put here the default verbosity level: */ 1;
+ return createObjTree(
+ oXMLParent,
+ _nVerb,
+ bFreeze || false,
+ bNesteAttributes !== undefined ? bNesteAttributes : _nVerb === 3
+ );
+ };
+})();
diff --git a/comm/mailnews/base/src/LineReader.h b/comm/mailnews/base/src/LineReader.h
new file mode 100644
index 0000000000..292c7ced7c
--- /dev/null
+++ b/comm/mailnews/base/src/LineReader.h
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef LineReader_h__
+#define LineReader_h__
+
+#include <algorithm>
+#include "mozilla/Span.h"
+#include "mozilla/Vector.h"
+
+/**
+ * FirstLine() returns the first line of a span.
+ * The EOL sequence (CRLF or LF) is included in the returned line.
+ * If no lines are found an empty span is returned.
+ */
+inline mozilla::Span<const char> FirstLine(
+ mozilla::Span<const char> const& data) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // no line ending found - return empty span.
+ return data.First(0);
+ }
+ ++eol;
+ return mozilla::Span<const char>(data.cbegin(), eol);
+}
+
+/**
+ * LineReader breaks up continuous character streams into lines.
+ * Data is fed in by calling Feed() as often as required, and a
+ * callback function is invoked to handle each resulting line.
+ *
+ * The resulting lines include the end-of-line char(s), except for any
+ * non-terminated final line.
+ * LF ('\n') is used as the line terminator. CRLF-terminated lines will
+ * be handled correctly - the resultant lines will include the line
+ * terminators exactly as they appear in the input data.
+ *
+ * Goals for LineReader:
+ * - Byte exact. The bytes fed in will appear _exactly_ in the callback fn.
+ * - Callback can be inlined (due to templating).
+ * - Avoid copying data if possible. The internal buffer is only used when
+ * lines are split across incoming chunks of data.
+ * - Tries to avoid heap allocation. If the internal buffer is used, it'll
+ * only allocate memory for long lines (>80 chars).
+ *
+ * Example usage:
+ *
+ * auto callback = [](mozilla::Span<const char> line) {
+ * printf("%s\n", nsCString(line).get());
+ * return true;
+ * };
+ *
+ * LineReader c;
+ * c.Feed("Line 1\r\nLine 2\r\nLine 3", callback);
+ * // -> "Line 1\r\n"
+ * // -> "Line 2\r\n"
+ * c.Feed("\r\nLeftovers.", callback);
+ * // -> "Line 3\r\n"
+ * c.Flush(callback);
+ * // -> "Leftovers."
+ *
+ * See TestLineReader.cpp for more examples.
+ */
+class LineReader {
+ public:
+ /*
+ * Feed() takes in a chunk of data to be split up into lines. You can call
+ * this as often as required to feed in all your data. Don't forget to call
+ * Flush() after the last Feed(), in case the last line has no line endings!
+ *
+ * The callback will be invoked once for each full line extracted.
+ * It should have the form:
+ * The callback is of the form:
+ * bool callback(mozilla::Span<const char> line);
+ *
+ * The data in `line` should be considered valid only until the callback
+ * returns. So if the callback wants to retain data it needs to copy it.
+ * `line` will include any EOL character(s).
+ * The callback should return true to continue processing.
+ * If the callback returns false, processing will stop, even if there is
+ * more data available.
+ */
+ template <typename LineFn>
+ void Feed(mozilla::Span<const char> data, LineFn callback) {
+ bool keepGoing = true;
+ while (!data.IsEmpty() && keepGoing) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // No LF. Just collect and wait for more.
+ // TODO: limit maximum mBuf size, to stop maliciously-crafted input
+ // OOMing us?
+ if (!mBuf.append(data.data(), data.size())) {
+ NS_ERROR("OOM!");
+ }
+ return;
+ }
+
+ // Consume everything up to and including the LF.
+ ++eol;
+ mozilla::Span<const char> line(data.cbegin(), eol);
+ data = mozilla::Span<const char>(eol, data.cend());
+
+ if (mBuf.empty()) {
+ // Pass the data through directly, no copying.
+ keepGoing = callback(line);
+ } else {
+ // Complete the line we previously started.
+ if (!mBuf.append(line.data(), line.size())) {
+ NS_ERROR("OOM!");
+ }
+ keepGoing = callback(mBuf);
+ mBuf.clear();
+ }
+ }
+ }
+
+ /*
+ * Flush() will invoke the callback with any leftover data, after the last
+ * Feed() call has completed.
+ * The line passed to the callback will be a partial line, without a final
+ * LF. If the input data has a final LF, there will be nothing to flush,
+ * and the callback will not be invoked.
+ */
+ template <typename LineFn>
+ void Flush(LineFn callback) {
+ if (!mBuf.empty()) {
+ callback(mBuf);
+ mBuf.clear();
+ }
+ }
+
+ private:
+ // Growable buffer, to collect lines which come in as multiple parts.
+ // Can handle lines up to 80 chars before needing to reallocate.
+ mozilla::Vector<char, 80> mBuf;
+};
+
+/**
+ * SplitLines() invokes a callback for every complete line it finds in the
+ * input data.
+ *
+ * The callback is of the form:
+ * bool callback(mozilla::Span<const char> line);
+ * where line is a span pointing to the range of bytes in the input data
+ * which comprises the line.
+ *
+ * If the callback returns false, processing is halted.
+ *
+ * The lines passed to the callback include end-of-line (EOL) character(s).
+ *
+ * Lines are considered terminated by '\n' (LF) but this means CRLF-delimited
+ * data is also handled correctly.
+ *
+ * This function is byte-exact: if you concatenate all the line spans, along
+ * with the unconsumed data returned at the end, you'll end up with the exact
+ * same byte sequence as the original input data.
+ *
+ * @param data - The input bytes.
+ * @param callback - The callback to invoke for each line.
+ *
+ * @returns the unconsumed data. Usually this will be empty, or an incomplete
+ * line at the end (with no EOL). However if the callback returned
+ * false, all the unused data will be returned.
+ */
+template <typename LineFn>
+mozilla::Span<const char> SplitLines(mozilla::Span<const char> data,
+ LineFn callback) {
+ while (!data.IsEmpty()) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // No LF - we're done. May or may not be some leftover data.
+ break;
+ }
+
+ // Consume everything up to and including the LF.
+ ++eol;
+ mozilla::Span<const char> line(data.cbegin(), eol);
+ data = mozilla::Span<const char>(eol, data.cend());
+
+ if (callback(line) == false) {
+ break;
+ }
+ }
+ return data;
+}
+
+#endif
diff --git a/comm/mailnews/base/src/LineReader.jsm b/comm/mailnews/base/src/LineReader.jsm
new file mode 100644
index 0000000000..2417457e3c
--- /dev/null
+++ b/comm/mailnews/base/src/LineReader.jsm
@@ -0,0 +1,68 @@
+/* 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 = ["LineReader"];
+
+/**
+ * For a single request, mail servers may return several multi-line responses. A
+ * definition of multi-line responses can be found at rfc3977#section-3.1.1.
+ *
+ * This class helps dealing with multi-line responses by:
+ * - Break up a response to lines
+ * - Join incomplete line from a previous response with the current response
+ * - Remove stuffed dot (.. at the beginning of a line)
+ * - Detect the end of the response (\r\n.\r\n)
+ */
+class LineReader {
+ processingMultiLineResponse = false;
+ _data = "";
+
+ /**
+ * Read a multi-line response, emit each line through a callback.
+ *
+ * @param {string} data - A multi-line response received from the server.
+ * @param {Function} lineCallback - A line will be passed to the callback each
+ * time.
+ * @param {Function} doneCallback - A function to be called when data is ended.
+ */
+ read(data, lineCallback, doneCallback) {
+ this._data += data;
+ if (this._data == ".\r\n" || this._data.endsWith("\r\n.\r\n")) {
+ this.processingMultiLineResponse = false;
+ this._data = this._data.slice(0, -3);
+ } else {
+ this.processingMultiLineResponse = true;
+ }
+ if (this._running) {
+ // This function can be called multiple times, but this._data should only
+ // be consumed once.
+ return;
+ }
+
+ let i = 0;
+ this._running = true;
+ while (this._data) {
+ let index = this._data.indexOf("\r\n");
+ if (index == -1) {
+ // Not enough data, save it for the next round.
+ break;
+ }
+ let line = this._data.slice(0, index + 2);
+ if (line.startsWith("..")) {
+ // Remove stuffed dot.
+ line = line.slice(1);
+ }
+ lineCallback(line);
+ this._data = this._data.slice(index + 2);
+ if (++i % 100 == 0) {
+ // Prevent blocking main process for too long.
+ Services.tm.spinEventLoopUntilEmpty();
+ }
+ }
+ this._running = false;
+ if (!this.processingMultiLineResponse && !this._data) {
+ doneCallback();
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailAuthenticator.jsm b/comm/mailnews/base/src/MailAuthenticator.jsm
new file mode 100644
index 0000000000..cf52a88f17
--- /dev/null
+++ b/comm/mailnews/base/src/MailAuthenticator.jsm
@@ -0,0 +1,468 @@
+/* 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 = [
+ "SmtpAuthenticator",
+ "NntpAuthenticator",
+ "Pop3Authenticator",
+ "ImapAuthenticator",
+];
+
+var { MailCryptoUtils } = ChromeUtils.import(
+ "resource:///modules/MailCryptoUtils.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+/**
+ * A base class for interfaces when authenticating a mail connection.
+ */
+class MailAuthenticator {
+ /**
+ * Get the hostname for a connection.
+ *
+ * @returns {string}
+ */
+ get hostname() {
+ throw Components.Exception(
+ "hostname getter not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the username for a connection.
+ *
+ * @returns {string}
+ */
+ get username() {
+ throw Components.Exception(
+ "username getter not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Forget cached password.
+ */
+ forgetPassword() {
+ throw Components.Exception(
+ "forgetPassword not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the password for a connection.
+ *
+ * @returns {string}
+ */
+ getPassword() {
+ throw Components.Exception(
+ "getPassword not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the CRAM-MD5 auth token for a connection.
+ *
+ * @param {string} password - The password, used as HMAC-MD5 secret.
+ * @param {string} challenge - The base64 encoded server challenge.
+ * @returns {string}
+ */
+ getCramMd5Token(password, challenge) {
+ // Hash the challenge.
+ let signature = MailCryptoUtils.hmacMd5(
+ new TextEncoder().encode(password),
+ new TextEncoder().encode(atob(challenge))
+ );
+ // Get the hex form of the signature.
+ let hex = [...signature].map(x => x.toString(16).padStart(2, "0")).join("");
+ return btoa(`${this.username} ${hex}`);
+ }
+
+ /**
+ * Get the OAuth token for a connection.
+ *
+ * @returns {string}
+ */
+ async getOAuthToken() {
+ throw Components.Exception(
+ "getOAuthToken not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Init a nsIMailAuthModule instance for GSSAPI auth.
+ *
+ * @param {('smtp'|'imap')} protocol - The protocol name.
+ */
+ initGssapiAuth(protocol) {
+ this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance(
+ Ci.nsIMailAuthModule
+ );
+ this._authModule.init(
+ "sasl-gssapi", // Auth module type
+ `${protocol}@${this.hostname}`,
+ 0, // nsIAuthModule::REQ_DEFAULT
+ null, // domain
+ this.username,
+ null // password
+ );
+ }
+
+ /**
+ * Get the next token in a sequence of GSSAPI auth steps.
+ *
+ * @param {string} inToken - A base64 encoded string, usually server challenge.
+ * @returns {string}
+ */
+ getNextGssapiToken(inToken) {
+ return this._authModule.getNextToken(inToken);
+ }
+
+ /**
+ * Init a nsIMailAuthModule instance for NTLM auth.
+ */
+ initNtlmAuth() {
+ this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance(
+ Ci.nsIMailAuthModule
+ );
+ this._authModule.init(
+ "ntlm", // Auth module type
+ null, // Service name
+ 0, // nsIAuthModule::REQ_DEFAULT
+ null, // domain
+ this.username,
+ this.getPassword()
+ );
+ }
+
+ /**
+ * Get the next token in a sequence of NTLM auth steps.
+ *
+ * @param {string} inToken - A base64 encoded string, usually server challenge.
+ * @returns {string}
+ */
+ getNextNtlmToken(inToken) {
+ return this._authModule.getNextToken(inToken);
+ }
+
+ /**
+ * Show a dialog for authentication failure.
+ *
+ * @returns {number} - 0: Retry; 1: Cancel; 2: New password.
+ */
+ promptAuthFailed() {
+ throw Components.Exception(
+ "promptAuthFailed not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Show a dialog for authentication failure.
+ *
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {string} accountname - A user defined account name or the server hostname.
+ * @returns {number} 0: Retry; 1: Cancel; 2: New password.
+ */
+ _promptAuthFailed(msgWindow, accountname) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let message = bundle.formatStringFromName("mailServerLoginFailed2", [
+ this.hostname,
+ this.username,
+ ]);
+
+ let title = bundle.formatStringFromName(
+ "mailServerLoginFailedTitleWithAccount",
+ [accountname]
+ );
+
+ let retryButtonLabel = bundle.GetStringFromName(
+ "mailServerLoginFailedRetryButton"
+ );
+ let newPasswordButtonLabel = bundle.GetStringFromName(
+ "mailServerLoginFailedEnterNewPasswordButton"
+ );
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING +
+ Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_CANCEL +
+ Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING;
+ let dummyValue = { value: false };
+
+ return Services.prompt.confirmEx(
+ msgWindow?.domWindow,
+ title,
+ message,
+ buttonFlags,
+ retryButtonLabel,
+ null,
+ newPasswordButtonLabel,
+ null,
+ dummyValue
+ );
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an SMTP connection.
+ *
+ * @augments {MailAuthenticator}
+ */
+class SmtpAuthenticator extends MailAuthenticator {
+ /**
+ * @param {nsISmtpServer} server - The associated server instance.
+ */
+ constructor(server) {
+ super();
+ this._server = server;
+ }
+
+ get hostname() {
+ return this._server.hostname;
+ }
+
+ get username() {
+ return this._server.username;
+ }
+
+ forgetPassword() {
+ this._server.forgetPassword();
+ }
+
+ getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+ let username = this._server.username;
+ let promptString;
+ if (username) {
+ promptString = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPromptWithUsername",
+ [this._server.hostname, username]
+ );
+ } else {
+ promptString = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPrompt",
+ [this._server.hostname]
+ );
+ }
+ let promptTitle = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPromptTitleWithHostname",
+ [this._server.hostname]
+ );
+ return this._server.getPasswordWithUI(promptString, promptTitle);
+ }
+
+ /**
+ * Get the ByteString form of the current password.
+ *
+ * @returns {string}
+ */
+ getByteStringPassword() {
+ return MailStringUtils.stringToByteString(this.getPassword());
+ }
+
+ /**
+ * Get the PLAIN auth token for a connection.
+ *
+ * @returns {string}
+ */
+ getPlainToken() {
+ // According to rfc4616#section-2, password should be UTF-8 BinaryString
+ // before base64 encoded.
+ return btoa("\0" + this.username + "\0" + this.getByteStringPassword());
+ }
+
+ async getOAuthToken() {
+ let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance(
+ Ci.msgIOAuth2Module
+ );
+ if (!oauth2Module.initFromSmtp(this._server)) {
+ return Promise.reject(`initFromSmtp failed, hostname: ${this.hostname}`);
+ }
+ return new Promise((resolve, reject) => {
+ oauth2Module.connect(true, {
+ onSuccess: token => {
+ resolve(token);
+ },
+ onFailure: e => {
+ reject(e);
+ },
+ });
+ });
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(
+ null,
+ this._server.description || this.hostname
+ );
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an incoming server.
+ *
+ * @augments {MailAuthenticator}
+ */
+class IncomingServerAuthenticator extends MailAuthenticator {
+ /**
+ * @param {nsIMsgIncomingServer} server - The associated server instance.
+ */
+ constructor(server) {
+ super();
+ this._server = server;
+ }
+
+ get hostname() {
+ return this._server.hostName;
+ }
+
+ get username() {
+ return this._server.username;
+ }
+
+ forgetPassword() {
+ this._server.forgetPassword();
+ }
+
+ /**
+ * Get the ByteString form of the current password.
+ *
+ * @returns {string}
+ */
+ async getByteStringPassword() {
+ return MailStringUtils.stringToByteString(await this.getPassword());
+ }
+
+ /**
+ * Get the PLAIN auth token for a connection.
+ *
+ * @returns {string}
+ */
+ async getPlainToken() {
+ // According to rfc4616#section-2, password should be UTF-8 BinaryString
+ // before base64 encoded.
+ return btoa(
+ "\0" + this.username + "\0" + (await this.getByteStringPassword())
+ );
+ }
+
+ async getOAuthToken() {
+ let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance(
+ Ci.msgIOAuth2Module
+ );
+ if (!oauth2Module.initFromMail(this._server)) {
+ return Promise.reject(`initFromMail failed, hostname: ${this.hostname}`);
+ }
+ return new Promise((resolve, reject) => {
+ oauth2Module.connect(true, {
+ onSuccess: token => {
+ resolve(token);
+ },
+ onFailure: e => {
+ reject(e);
+ },
+ });
+ });
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating a NNTP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class NntpAuthenticator extends IncomingServerAuthenticator {
+ /**
+ * @returns {string} - NNTP server has no userName pref, need to pass it in.
+ */
+ get username() {
+ return this._username;
+ }
+
+ set username(value) {
+ this._username = value;
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating a POP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class Pop3Authenticator extends IncomingServerAuthenticator {
+ async getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/localMsgs.properties"
+ );
+ let params = [this._server.username, this._server.hostName];
+ let promptString = composeBundle.formatStringFromName(
+ "pop3EnterPasswordPrompt",
+ params
+ );
+ let promptTitle = composeBundle.formatStringFromName(
+ "pop3EnterPasswordPromptTitleWithUsername",
+ [this._server.hostName]
+ );
+ return this._server.wrappedJSObject.getPasswordWithUIAsync(
+ promptString,
+ promptTitle
+ );
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an IMAP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class ImapAuthenticator extends IncomingServerAuthenticator {
+ async getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/imapMsgs.properties"
+ );
+ let params = [this._server.username, this._server.hostName];
+ let promptString = composeBundle.formatStringFromName(
+ "imapEnterServerPasswordPrompt",
+ params
+ );
+ let promptTitle = composeBundle.formatStringFromName(
+ "imapEnterPasswordPromptTitleWithUsername",
+ [this._server.hostName]
+ );
+ return this._server.wrappedJSObject.getPasswordWithUIAsync(
+ promptString,
+ promptTitle
+ );
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
diff --git a/comm/mailnews/base/src/MailChannel.sys.mjs b/comm/mailnews/base/src/MailChannel.sys.mjs
new file mode 100644
index 0000000000..a5fbf9ee75
--- /dev/null
+++ b/comm/mailnews/base/src/MailChannel.sys.mjs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * @see {nsIMailChannel}
+ */
+export class MailChannel {
+ _headerNames = [];
+ _headerValues = [];
+ _attachments = [];
+ _mailCharacterSet = null;
+ _progressListener = null;
+
+ addHeaderFromMIME(name, value) {
+ this._headerNames.push(name);
+ this._headerValues.push(value);
+ }
+
+ get headerNames() {
+ return this._headerNames;
+ }
+
+ get headerValues() {
+ return this._headerValues;
+ }
+
+ handleAttachmentFromMIME(contentType, url, displayName, uri, notDownloaded) {
+ let attachment = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ attachment.setPropertyAsAUTF8String("contentType", contentType);
+ attachment.setPropertyAsAUTF8String("url", url);
+ attachment.setPropertyAsAUTF8String("displayName", displayName);
+ attachment.setPropertyAsAUTF8String("uri", uri);
+ attachment.setPropertyAsBool("notDownloaded", notDownloaded);
+ this._attachments.push(attachment);
+ }
+
+ addAttachmentFieldFromMIME(field, value) {
+ let attachment = this._attachments[this._attachments.length - 1];
+ attachment.setPropertyAsAUTF8String(field, value);
+ }
+
+ get attachments() {
+ return this._attachments.slice();
+ }
+
+ get mailCharacterSet() {
+ return this._mailCharacterSet;
+ }
+
+ set mailCharacterSet(value) {
+ let ccm = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ this._mailCharacterSet = ccm.getCharsetAlias(value);
+ }
+
+ imipMethod = null;
+ imipItem = null;
+ smimeHeaderSink = null;
+
+ get listener() {
+ return this._progressListener?.get();
+ }
+
+ set listener(listener) {
+ this._progressListener = Cu.getWeakReference(listener);
+ }
+}
diff --git a/comm/mailnews/base/src/MailCryptoUtils.jsm b/comm/mailnews/base/src/MailCryptoUtils.jsm
new file mode 100644
index 0000000000..6c378e6703
--- /dev/null
+++ b/comm/mailnews/base/src/MailCryptoUtils.jsm
@@ -0,0 +1,76 @@
+/* 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 = ["MailCryptoUtils"];
+
+var MailCryptoUtils = {
+ /**
+ * Converts a binary string into a Uint8Array.
+ *
+ * @param {BinaryString} str - The string to convert.
+ * @returns {Uint8Array}.
+ */
+ binaryStringToTypedArray(str) {
+ let arr = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+ },
+
+ /**
+ * The HMAC-MD5 transform works like:
+ *
+ * MD5(K XOR opad, MD5(K XOR ipad, m))
+ *
+ * where
+ * K is an n byte key
+ * ipad is the byte 0x36 repeated 64 times
+ * opad is the byte 0x5c repeated 64 times
+ * m is the message being processed
+
+ * @param {Uint8Array} key
+ * @param {Uint8Array} data
+ * @returns {Uint8Array}
+ */
+ hmacMd5(key, data) {
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ let digest;
+
+ // If key is longer than 64 bytes, reset it to MD5(key).
+ if (key.length > 64) {
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(key, key.length);
+ digest = hasher.finish(false);
+ key = this.binaryStringToTypedArray(digest);
+ }
+
+ // Generate innerPad and outerPad.
+ let innerPad = new Uint8Array(64);
+ let outerPad = new Uint8Array(64);
+ for (let i = 0; i < 64; i++) {
+ let base = key[i] || 0;
+ innerPad[i] = base ^ 0x36;
+ outerPad[i] = base ^ 0x5c;
+ }
+
+ // Perform inner MD5.
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(innerPad, 64);
+ hasher.update(data, data.length);
+ digest = hasher.finish(false);
+
+ let result = this.binaryStringToTypedArray(digest);
+
+ // Perform outer MD5.
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(outerPad, 64);
+ hasher.update(result, result.length);
+ digest = hasher.finish(false);
+
+ return this.binaryStringToTypedArray(digest);
+ },
+};
diff --git a/comm/mailnews/base/src/MailNewsDLF.cpp b/comm/mailnews/base/src/MailNewsDLF.cpp
new file mode 100644
index 0000000000..8969992414
--- /dev/null
+++ b/comm/mailnews/base/src/MailNewsDLF.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#include "nsCOMPtr.h"
+#include "MailNewsDLF.h"
+#include "nsIChannel.h"
+#include "plstr.h"
+#include "nsString.h"
+#include "nsICategoryManager.h"
+#include "nsIServiceManager.h"
+#include "nsIStreamConverterService.h"
+#include "nsIStreamListener.h"
+#include "nsNetCID.h"
+#include "nsMsgUtils.h"
+
+namespace mozilla {
+namespace mailnews {
+NS_IMPL_ISUPPORTS(MailNewsDLF, nsIDocumentLoaderFactory)
+
+MailNewsDLF::MailNewsDLF() {}
+
+MailNewsDLF::~MailNewsDLF() {}
+
+NS_IMETHODIMP
+MailNewsDLF::CreateInstance(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup,
+ const nsACString& aContentType,
+ nsIDocShell* aContainer, nsISupports* aExtraInfo,
+ nsIStreamListener** aDocListener,
+ nsIContentViewer** aDocViewer) {
+ nsresult rv;
+
+ bool viewSource =
+ (PL_strstr(PromiseFlatCString(aContentType).get(), "view-source") != 0);
+
+ aChannel->SetContentType(nsLiteralCString(TEXT_HTML));
+
+ // Get the HTML category
+ nsCOMPtr<nsICategoryManager> catMan(
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractID;
+ rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", TEXT_HTML, contractID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDocumentLoaderFactory> factory(
+ do_GetService(contractID.get(), &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStreamListener> listener;
+
+ if (viewSource) {
+ rv = factory->CreateInstance(
+ "view-source", aChannel, aLoadGroup,
+ nsLiteralCString(TEXT_HTML "; x-view-type=view-source"), aContainer,
+ aExtraInfo, getter_AddRefs(listener), aDocViewer);
+ } else {
+ rv = factory->CreateInstance(
+ "view", aChannel, aLoadGroup, nsLiteralCString(TEXT_HTML), aContainer,
+ aExtraInfo, getter_AddRefs(listener), aDocViewer);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStreamConverterService> scs(
+ do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return scs->AsyncConvertData(MESSAGE_RFC822, TEXT_HTML, listener, aChannel,
+ aDocListener);
+}
+
+NS_IMETHODIMP
+MailNewsDLF::CreateInstanceForDocument(nsISupports* aContainer,
+ mozilla::dom::Document* aDocument,
+ const char* aCommand,
+ nsIContentViewer** aDocViewer) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/base/src/MailNewsDLF.h b/comm/mailnews/base/src/MailNewsDLF.h
new file mode 100644
index 0000000000..95455f4b56
--- /dev/null
+++ b/comm/mailnews/base/src/MailNewsDLF.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#ifndef MailNewsDLF_h__
+#define MailNewsDLF_h__
+
+#include "nsIDocumentLoaderFactory.h"
+#include "nsMimeTypes.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/*
+ * This factory is a thin wrapper around the text/html loader factory. All it
+ * does is convert message/rfc822 to text/html and delegate the rest of the
+ * work to the text/html factory.
+ */
+class MailNewsDLF : public nsIDocumentLoaderFactory {
+ public:
+ MailNewsDLF();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOCUMENTLOADERFACTORY
+
+ private:
+ virtual ~MailNewsDLF();
+};
+} // namespace mailnews
+} // namespace mozilla
+
+#define MAILNEWSDLF_CATEGORIES \
+ {"Gecko-Content-Viewers", MESSAGE_RFC822, \
+ "@mozilla.org/mailnews/document-loader-factory;1"},
+
+#endif
diff --git a/comm/mailnews/base/src/MailNotificationManager.jsm b/comm/mailnews/base/src/MailNotificationManager.jsm
new file mode 100644
index 0000000000..c16e37eb2f
--- /dev/null
+++ b/comm/mailnews/base/src/MailNotificationManager.jsm
@@ -0,0 +1,478 @@
+/* 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 = ["MailNotificationManager"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ WinUnreadBadge: "resource:///modules/WinUnreadBadge.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization(["messenger/messenger.ftl"])
+);
+
+/**
+ * A module that listens to folder change events, and show notifications for new
+ * mails if necessary.
+ */
+class MailNotificationManager {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIFolderListener",
+ "mozINewMailListener",
+ ]);
+
+ constructor() {
+ this._systemAlertAvailable = true;
+ this._unreadChatCount = 0;
+ this._unreadMailCount = 0;
+ // @type {Map<nsIMsgFolder, number>} - A map of folder and its last biff time.
+ this._folderBiffTime = new Map();
+ // @type {Set<nsIMsgFolder>} - A set of folders to show alert for.
+ this._pendingFolders = new Set();
+
+ this._logger = console.createInstance({
+ prefix: "mail.notification",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.notification.loglevel",
+ });
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Ensure that OS integration is defined before we attempt to initialize the
+ // system tray icon.
+ XPCOMUtils.defineLazyGetter(this, "_osIntegration", () => {
+ try {
+ return Cc["@mozilla.org/messenger/osintegration;1"].getService(
+ Ci.nsIMessengerOSIntegration
+ );
+ } catch (e) {
+ // We don't have OS integration on all platforms.
+ return null;
+ }
+ });
+
+ if (["macosx", "win"].includes(AppConstants.platform)) {
+ // We don't have indicator for unread count on Linux yet.
+ Cc["@mozilla.org/newMailNotificationService;1"]
+ .getService(Ci.mozINewMailNotificationService)
+ .addListener(this, Ci.mozINewMailNotificationService.count);
+
+ Services.obs.addObserver(this, "unread-im-count-changed");
+ Services.obs.addObserver(this, "profile-before-change");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ Services.obs.addObserver(this, "new-directed-incoming-message");
+ }
+
+ if (AppConstants.platform == "win") {
+ Services.obs.addObserver(this, "windows-refresh-badge-tray");
+ Services.prefs.addObserver("mail.biff.show_badge", this);
+ Services.prefs.addObserver("mail.biff.show_tray_icon_always", this);
+ }
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "alertclickcallback":
+ // Display the associated message when an alert is clicked.
+ let msgHdr = Cc["@mozilla.org/messenger;1"]
+ .getService(Ci.nsIMessenger)
+ .msgHdrFromURI(data);
+ lazy.MailUtils.displayMessageInFolderTab(msgHdr, true);
+ return;
+ case "unread-im-count-changed":
+ this._logger.log(
+ `Unread chat count changed to ${this._unreadChatCount}`
+ );
+ this._unreadChatCount = parseInt(data, 10) || 0;
+ this._updateUnreadCount();
+ return;
+ case "new-directed-incoming-messenger":
+ this._animateDockIcon();
+ return;
+ case "windows-refresh-badge-tray":
+ this._updateUnreadCount();
+ return;
+ case "profile-before-change":
+ this._osIntegration?.onExit();
+ return;
+ case "newmailalert-closed":
+ // newmailalert.xhtml is closed, try to show the next queued folder.
+ this._customizedAlertShown = false;
+ this._showCustomizedAlert();
+ return;
+ case "nsPref:changed":
+ if (
+ data == "mail.biff.show_badge" ||
+ data == "mail.biff.show_tray_icon_always"
+ ) {
+ this._updateUnreadCount();
+ }
+ }
+ }
+
+ /**
+ * Following are nsIFolderListener interfaces. Do nothing about them.
+ */
+ onFolderAdded() {}
+ onMessageAdded() {}
+ onFolderRemoved() {}
+ onMessageRemoved() {}
+ onFolderPropertyChanged() {}
+ /**
+ * The only nsIFolderListener interface we care about.
+ *
+ * @see nsIFolderListener
+ */
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (!Services.prefs.getBoolPref("mail.biff.show_alert")) {
+ return;
+ }
+
+ this._logger.debug(
+ `onFolderIntPropertyChanged; property=${property}: ${oldValue} => ${newValue}, folder.URI=${folder.URI}`
+ );
+
+ switch (property) {
+ case "BiffState":
+ if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+ // The folder argument is a root folder.
+ this._fillAlertInfo(folder);
+ }
+ break;
+ case "NewMailReceived":
+ // The folder argument is a real folder.
+ this._fillAlertInfo(folder);
+ break;
+ }
+ }
+ onFolderBoolPropertyChanged() {}
+ onFolderUnicharPropertyChanged() {}
+ onFolderPropertyFlagChanged() {}
+ onFolderEvent() {}
+
+ /**
+ * @see mozINewMailNotificationService
+ */
+ onCountChanged(count) {
+ this._logger.log(`Unread mail count changed to ${count}`);
+ this._unreadMailCount = count;
+ this._updateUnreadCount();
+ }
+
+ /**
+ * Show an alert according to the changed folder.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change
+ * event, can be a root folder or a real folder.
+ */
+ async _fillAlertInfo(changedFolder) {
+ let folder = this._getFirstRealFolderWithNewMail(changedFolder);
+ if (!folder) {
+ return;
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ let numNewMessages = newMsgKeys.length;
+ if (!numNewMessages) {
+ return;
+ }
+
+ this._logger.debug(
+ `Filling alert info; folder.URI=${folder.URI}, numNewMessages=${numNewMessages}`
+ );
+ let firstNewMsgHdr = folder.msgDatabase.getMsgHdrForKey(newMsgKeys[0]);
+
+ let title = this._getAlertTitle(folder, numNewMessages);
+ let body;
+ try {
+ body = await this._getAlertBody(folder, firstNewMsgHdr);
+ } catch (e) {
+ this._logger.error(e);
+ }
+ if (!title || !body) {
+ return;
+ }
+ this._showAlert(firstNewMsgHdr, title, body);
+ this._animateDockIcon();
+ }
+
+ /**
+ * Iterate the subfolders of changedFolder, return the first real folder with
+ * new mail.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change event.
+ * @returns {nsIMsgFolder} The first real folder.
+ */
+ _getFirstRealFolderWithNewMail(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;
+ }
+
+ if (folder.getNumNewMessages(false) > 0) {
+ return folder;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the title for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {number} numNewMessages - The count of new messages.
+ * @returns {string} The alert title.
+ */
+ _getAlertTitle(folder, numNewMessages) {
+ return this._bundle.formatStringFromName(
+ numNewMessages == 1
+ ? "newMailNotification_message"
+ : "newMailNotification_messages",
+ [folder.server.prettyName, numNewMessages.toString()]
+ );
+ }
+
+ /**
+ * Get the body for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @returns {string} The alert body.
+ */
+ async _getAlertBody(folder, msgHdr) {
+ await new Promise((resolve, reject) => {
+ let isAsync = folder.fetchMsgPreviewText([msgHdr.messageKey], {
+ OnStartRunningUrl() {},
+ // @see nsIUrlListener
+ OnStopRunningUrl(url, exitCode) {
+ Components.isSuccessCode(exitCode) ? resolve() : reject();
+ },
+ });
+ if (!isAsync) {
+ resolve();
+ }
+ });
+
+ let alertBody = "";
+
+ let subject = Services.prefs.getBoolPref("mail.biff.alert.show_subject")
+ ? msgHdr.mime2DecodedSubject
+ : "";
+ let author = "";
+ if (Services.prefs.getBoolPref("mail.biff.alert.show_sender")) {
+ let addressObjects = MailServices.headerParser.makeFromDisplayAddress(
+ msgHdr.mime2DecodedAuthor
+ );
+ let { name, email } = addressObjects[0] || {};
+ author = name || email;
+ }
+ if (subject && author) {
+ alertBody += this._bundle.formatStringFromName(
+ "newMailNotification_messagetitle",
+ [subject, author]
+ );
+ } else if (subject) {
+ alertBody += subject;
+ } else if (author) {
+ alertBody += author;
+ }
+ let showPreview = Services.prefs.getBoolPref(
+ "mail.biff.alert.show_preview"
+ );
+ if (showPreview) {
+ let previewLength = Services.prefs.getIntPref(
+ "mail.biff.alert.preview_length",
+ 40
+ );
+ let preview = msgHdr.getStringProperty("preview").slice(0, previewLength);
+ if (preview) {
+ alertBody += (alertBody ? "\n" : "") + preview;
+ }
+ }
+ return alertBody;
+ }
+
+ /**
+ * Show the alert.
+ *
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @param {string} title - The alert title.
+ * @param {string} body - The alert body.
+ */
+ _showAlert(msgHdr, title, body) {
+ let folder = msgHdr.folder;
+
+ // Try to use system alert first.
+ if (
+ Services.prefs.getBoolPref("mail.biff.use_system_alert", true) &&
+ this._systemAlertAvailable
+ ) {
+ let alertsService = Cc["@mozilla.org/system-alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ );
+ let cookie = folder.generateMessageURI(msgHdr.messageKey);
+ try {
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ cookie, // name
+ "chrome://messenger/skin/icons/new-mail-alert.png",
+ title,
+ body,
+ true, // clickable
+ cookie
+ );
+ alertsService.showAlert(alert, this);
+ return;
+ } catch (e) {
+ this._logger.error(e);
+ this._systemAlertAvailable = false;
+ }
+ }
+
+ // The use_system_alert pref is false or showAlert somehow failed, use the
+ // customized alert window.
+ this._showCustomizedAlert(folder);
+ }
+
+ /**
+ * Show a customized alert window (newmailalert.xhtml), if there is already
+ * one showing, do not show another one, because the newer one will block the
+ * older one. Instead, save the folder and newMsgKeys to this._pendingFolders.
+ *
+ * @param {nsIMsgFolder} [folder] - The folder containing new messages.
+ */
+ _showCustomizedAlert(folder) {
+ if (this._customizedAlertShown) {
+ // Queue the folder.
+ this._pendingFolders.add(folder);
+ return;
+ }
+ if (!folder) {
+ // Get the next folder from the queue.
+ folder = this._pendingFolders.keys().next().value;
+ if (!folder) {
+ return;
+ }
+ this._pendingFolders.delete(folder);
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ if (!newMsgKeys.length) {
+ // No NEW message in the current folder, try the next queued folder.
+ this._showCustomizedAlert();
+ return;
+ }
+
+ let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ args.appendElement(folder);
+ args.appendElement({
+ wrappedJSObject: newMsgKeys,
+ });
+ args.appendElement(this);
+ Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/newmailalert.xhtml",
+ "_blank",
+ "chrome,dialog=yes,titlebar=no,popup=yes",
+ args
+ );
+ this._customizedAlertShown = true;
+ this._folderBiffTime.set(folder, Date.now());
+ }
+
+ /**
+ * Get all NEW messages from a folder that we received after last biff time.
+ *
+ * @param {nsIMsgFolder} folder - The message folder to check.
+ * @returns {number[]} An array of message keys.
+ */
+ _getNewMsgKeysNotNotified(folder) {
+ let msgDb = folder.msgDatabase;
+ let lastBiffTime = this._folderBiffTime.get(folder) || 0;
+ return msgDb
+ .getNewList()
+ .slice(-folder.getNumNewMessages(false))
+ .filter(key => {
+ let msgHdr = msgDb.getMsgHdrForKey(key);
+ return msgHdr.dateInSeconds * 1000 > lastBiffTime;
+ });
+ }
+
+ async _updateUnreadCount() {
+ if (this._updatingUnreadCount) {
+ // _updateUnreadCount can be triggered faster than we finish rendering the
+ // badge. When that happens, set a flag and return.
+ this._pendingUpdate = true;
+ return;
+ }
+ this._updatingUnreadCount = true;
+
+ this._logger.debug(
+ `Update unreadMailCount=${this._unreadMailCount}, unreadChatCount=${this._unreadChatCount}`
+ );
+ let count = this._unreadMailCount + this._unreadChatCount;
+ let tooltip = "";
+ if (AppConstants.platform == "win") {
+ if (!Services.prefs.getBoolPref("mail.biff.show_badge", true)) {
+ count = 0;
+ }
+ if (count > 0) {
+ tooltip = await lazy.l10n.formatValue("unread-messages-os-tooltip", {
+ count,
+ });
+ }
+ await lazy.WinUnreadBadge.updateUnreadCount(count, tooltip);
+ }
+ this._osIntegration?.updateUnreadCount(count, tooltip);
+
+ this._updatingUnreadCount = false;
+ if (this._pendingUpdate) {
+ // There was at least one _updateUnreadCount call while we were rendering
+ // the badge. Render one more time will ensure the badge reflects the
+ // current state.
+ this._pendingUpdate = false;
+ this._updateUnreadCount();
+ }
+ }
+
+ _animateDockIcon() {
+ if (Services.prefs.getBoolPref("mail.biff.animate_dock_icon", false)) {
+ Services.wm.getMostRecentWindow("mail:3pane")?.getAttention();
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailNotificationService.jsm b/comm/mailnews/base/src/MailNotificationService.jsm
new file mode 100644
index 0000000000..8f2c57aad6
--- /dev/null
+++ b/comm/mailnews/base/src/MailNotificationService.jsm
@@ -0,0 +1,375 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * Platform-independent code to count new and unread messages and pass the
+ * information to platform-specific notification modules.
+ */
+
+var EXPORTED_SYMBOLS = ["NewMailNotificationService"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * NewMailNotificationService.
+ *
+ * @implements {mozINewMailNotificationService}
+ * @implements {nsIFolderListener}
+ * @implements {nsIObserver}
+ */
+class NewMailNotificationService {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIFolderListener",
+ "mozINewMailNotificationService",
+ ]);
+
+ #unreadCount = 0;
+ #newCount = 0;
+ #listeners = [];
+ #log = null;
+
+ constructor() {
+ this.#log = console.createInstance({
+ prefix: "mail.notification",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.notification.loglevel",
+ });
+
+ Services.obs.addObserver(this, "profile-before-change");
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged |
+ Ci.nsIFolderListener.added |
+ Ci.nsIFolderListener.removed |
+ Ci.nsIFolderListener.propertyFlagChanged
+ );
+ if (!this.useNewCountInBadge) {
+ let total = 0;
+ for (let server of MailServices.accounts.allServers) {
+ // Don't bother counting RSS or NNTP servers
+ let type = server.type;
+ if (type == "rss" || type == "nntp") {
+ continue;
+ }
+
+ let rootFolder = server.rootFolder;
+ if (rootFolder) {
+ total += this.countUnread(rootFolder);
+ }
+ }
+ this.#unreadCount = total;
+ }
+ }
+
+ get useNewCountInBadge() {
+ return Services.prefs.getBoolPref(
+ "mail.biff.use_new_count_in_badge",
+ false
+ );
+ }
+
+ /** Setter. Used for unit tests. */
+ set unreadCount(count) {
+ this.#unreadCount = count;
+ }
+
+ observe(subject, topic, data) {
+ if (topic == "profile-before-change") {
+ try {
+ MailServices.mailSession.RemoveFolderListener(this);
+ Services.obs.removeObserver(this, "profile-before-change");
+ } catch (e) {
+ this.#log.error("Unable to deregister listeners at shutdown: " + e);
+ }
+ }
+ }
+
+ // Count all the unread messages below the given folder
+ countUnread(folder) {
+ this.#log.debug(`countUnread for ${folder.URI}`);
+ let unreadCount = 0;
+
+ let allFolders = [folder, ...folder.descendants];
+ for (let folder of allFolders) {
+ if (this.confirmShouldCount(folder)) {
+ let count = folder.getNumUnread(false);
+ this.#log.debug(`${folder.URI} has ${count} unread`);
+ if (count > 0) {
+ unreadCount += count;
+ }
+ }
+ }
+ return unreadCount;
+ }
+
+ /**
+ * Filter out special folders and then ask for observers to see if
+ * we should monitor unread messages in this folder.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder we're asking about.
+ */
+ confirmShouldCount(aFolder) {
+ let shouldCount = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ shouldCount.data = true;
+
+ // If it's not a mail folder we don't count it by default
+ if (!(aFolder.flags & Ci.nsMsgFolderFlags.Mail)) {
+ shouldCount.data = false;
+ } else if (aFolder.server?.type == "rss") {
+ // For whatever reason, RSS folders have the 'Mail' flag.
+ shouldCount.data = false;
+ } else if (
+ aFolder.flags & Ci.nsMsgFolderFlags.SpecialUse &&
+ !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox)
+ ) {
+ // It's a special folder *other than the inbox*, don't count it by default.
+ shouldCount.data = false;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.Virtual) {
+ shouldCount.data = false;
+ } else {
+ // If we're only counting inboxes and it's not an inbox...
+ let onlyCountInboxes = Services.prefs.getBoolPref(
+ "mail.notification.count.inbox_only",
+ true
+ );
+ if (onlyCountInboxes && !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox)) {
+ shouldCount.data = false;
+ }
+ }
+
+ this.#log.debug(`${aFolder.URI}: shouldCount=${shouldCount.data}`);
+ Services.obs.notifyObservers(
+ shouldCount,
+ "before-count-unread-for-folder",
+ aFolder.URI
+ );
+ return shouldCount.data;
+ }
+
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ try {
+ if (property == "FolderSize") {
+ return;
+ }
+ this.#log.trace(
+ `Changed int ${property} of ${folder.folderURL}: ${oldValue} -> ${newValue}`
+ );
+ if (property == "BiffState") {
+ this.#biffStateChanged(folder, oldValue, newValue);
+ } else if (property == "TotalUnreadMessages") {
+ this.#totalUnreadMessagesChanged(folder, oldValue, newValue);
+ } else if (property == "NewMailReceived") {
+ this.#newMailReceived(folder, oldValue, newValue);
+ }
+ } catch (error) {
+ this.#log.error("onFolderIntPropertyChanged: " + error);
+ }
+ }
+
+ #biffStateChanged(folder, oldValue, newValue) {
+ if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+ if (folder.server && !folder.server.performingBiff) {
+ this.#log.debug(
+ `${folder.URI} notified, but server not performing biff`
+ );
+ return;
+ }
+
+ // Biff notifications come in for the top level of the server, we need to
+ // look for the folder that actually contains the new mail.
+
+ let allFolders = [folder, ...folder.descendants];
+
+ this.#log.debug(`${folder.URI} notified; will check subfolders`);
+ let newCount = 0;
+
+ for (let folder of allFolders) {
+ if (this.confirmShouldCount(folder)) {
+ let folderNew = folder.getNumNewMessages(false);
+ this.#log.debug(`${folder.URI}: ${folderNew} new`);
+ if (folderNew > 0) {
+ newCount += folderNew;
+ }
+ }
+ }
+ if (newCount > 0) {
+ this.#newCount += newCount;
+ this.#log.debug(`${folder.URI}: new mail count ${this.#newCount}`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+ } else if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NoMail) {
+ // Dodgy - when any folder tells us it has no mail, clear all unread mail
+ this.#newCount = 0;
+ this.#log.debug(`${folder.URI}: no new mail`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+ }
+
+ #newMailReceived(folder, oldValue, newValue) {
+ if (!this.confirmShouldCount(folder)) {
+ return;
+ }
+
+ if (!oldValue || oldValue < 0) {
+ oldValue = 0;
+ }
+ this.#newCount += newValue - oldValue;
+ this.#log.debug(`#newMailReceived ${folder.URI} - ${this.#newCount} new`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+
+ #totalUnreadMessagesChanged(folder, oldValue, newValue) {
+ if (!this.confirmShouldCount(folder)) {
+ return;
+ }
+
+ // treat "count unknown" as zero
+ if (oldValue < 0) {
+ oldValue = 0;
+ }
+ if (newValue < 0) {
+ newValue = 0;
+ }
+
+ this.#unreadCount += newValue - oldValue;
+ if (!this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#unreadCount
+ );
+ }
+ }
+
+ onFolderAdded(parentFolder, child) {
+ if (child.rootFolder == child) {
+ this.#log.trace(`Added root folder ${child.folderURL}`);
+ } else {
+ this.#log.trace(
+ `Added child folder ${child.folderURL} to ${parentFolder.folderURL}`
+ );
+ }
+ }
+
+ onMessageAdded(parentFolder, msg) {
+ if (this.confirmShouldCount(msg.folder)) {
+ this.#log.trace(`Added <${msg.messageId}> to ${msg.folder.folderURL}`);
+ }
+ }
+
+ onFolderPropertyFlagChanged(msg, property, oldFlag, newFlag) {
+ if (
+ oldFlag & Ci.nsMsgMessageFlags.New &&
+ !(newFlag & Ci.nsMsgMessageFlags.New)
+ ) {
+ this.#log.trace(
+ `<${msg.messageId}> marked read in ${msg.folder.folderURL}`
+ );
+ } else if (newFlag & Ci.nsMsgMessageFlags.New) {
+ this.#log.trace(
+ `<${msg.messageId}> marked unread in ${msg.folder.folderURL}`
+ );
+ }
+ }
+
+ onFolderRemoved(parentFolder, child) {
+ if (child.rootFolder == child) {
+ this.#log.trace(`Removed root folder ${child.folderURL}`);
+ } else {
+ this.#log.trace(
+ `Removed child folder ${child.folderURL} from ${parentFolder?.folderURL}`
+ );
+ }
+ }
+
+ onMessageRemoved(parentFolder, msg) {
+ if (!msg.isRead) {
+ this.#log.trace(
+ `Removed unread <${msg.messageId}> from ${msg.folder.folderURL}`
+ );
+ }
+ }
+
+ // Implement mozINewMailNotificationService
+
+ get messageCount() {
+ if (this.useNewCountInBadge) {
+ return this.#newCount;
+ }
+ return this.#unreadCount;
+ }
+
+ addListener(aListener, flags) {
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.obj === aListener) {
+ l.flags = flags;
+ return;
+ }
+ }
+
+ // Ensure that first-time listeners get an accurate mail count.
+ if (flags & Ci.mozINewMailNotificationService.count) {
+ const count = this.useNewCountInBadge
+ ? this.#newCount
+ : this.#unreadCount;
+ aListener.onCountChanged(count);
+ }
+
+ // If we get here, the listener wasn't already in the list
+ this.#listeners.push({ obj: aListener, flags });
+ }
+
+ removeListener(aListener) {
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.obj === aListener) {
+ this.#listeners.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ listenersForFlag(flag) {
+ let list = [];
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.flags & flag) {
+ list.push(l.obj);
+ }
+ }
+ return list;
+ }
+
+ _notifyListeners(flag, func, value) {
+ let list = this.listenersForFlag(flag);
+ for (let i = 0; i < list.length; i++) {
+ list[i][func](value);
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailServices.jsm b/comm/mailnews/base/src/MailServices.jsm
new file mode 100644
index 0000000000..f316b16bde
--- /dev/null
+++ b/comm/mailnews/base/src/MailServices.jsm
@@ -0,0 +1,169 @@
+/* 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 EXPORTED_SYMBOLS = ["MailServices"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+var MailServices = {
+ /**
+ * Gets the `nsIMsgMessageService` for a given message URI. This should have
+ * the same behaviour as `GetMessageServiceFromURI` (nsMsgUtils.cpp).
+ *
+ * @param {string} uri - The URI of a folder or message.
+ * @returns {nsIMsgMessageService}
+ */
+ messageServiceFromURI(uri) {
+ let index = uri.indexOf(":");
+ if (index == -1) {
+ throw new Components.Exception(
+ `Bad message URI: ${uri}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let protocol = uri.substring(0, index);
+ if (protocol == "file") {
+ protocol = "mailbox";
+ }
+ return Cc[
+ `@mozilla.org/messenger/messageservice;1?type=${protocol}`
+ ].getService(Ci.nsIMsgMessageService);
+ },
+};
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mailSession",
+ "@mozilla.org/messenger/services/session;1",
+ "nsIMsgMailSession"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "accounts",
+ "@mozilla.org/messenger/account-manager;1",
+ "nsIMsgAccountManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "pop3",
+ "@mozilla.org/messenger/popservice;1",
+ "nsIPop3Service"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "imap",
+ "@mozilla.org/messenger/imapservice;1",
+ "nsIImapService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "nntp",
+ "@mozilla.org/messenger/nntpservice;1",
+ "nsINntpService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "smtp",
+ "@mozilla.org/messengercompose/smtp;1",
+ "nsISmtpService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "compose",
+ "@mozilla.org/messengercompose;1",
+ "nsIMsgComposeService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "ab",
+ "@mozilla.org/abmanager;1",
+ "nsIAbManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "copy",
+ "@mozilla.org/messenger/messagecopyservice;1",
+ "nsIMsgCopyService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mfn",
+ "@mozilla.org/messenger/msgnotificationservice;1",
+ "nsIMsgFolderNotificationService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "headerParser",
+ "@mozilla.org/messenger/headerparser;1",
+ "nsIMsgHeaderParser"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mimeConverter",
+ "@mozilla.org/messenger/mimeconverter;1",
+ "nsIMimeConverter"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "tags",
+ "@mozilla.org/messenger/tagservice;1",
+ "nsIMsgTagService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "filters",
+ "@mozilla.org/messenger/services/filters;1",
+ "nsIMsgFilterService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "junk",
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter",
+ "nsIJunkMailPlugin"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "newMailNotification",
+ "@mozilla.org/newMailNotificationService;1",
+ "mozINewMailNotificationService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "folderLookup",
+ "@mozilla.org/mail/folder-lookup;1",
+ "nsIFolderLookupService"
+);
+
+// Clean up all of these references at shutdown, so that they don't appear as
+// a memory leak in test logs.
+Services.obs.addObserver(
+ {
+ observe() {
+ for (let key of Object.keys(MailServices)) {
+ delete MailServices[key];
+ }
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ },
+ },
+ "xpcom-shutdown"
+);
diff --git a/comm/mailnews/base/src/MailStringUtils.jsm b/comm/mailnews/base/src/MailStringUtils.jsm
new file mode 100644
index 0000000000..c892c328e4
--- /dev/null
+++ b/comm/mailnews/base/src/MailStringUtils.jsm
@@ -0,0 +1,102 @@
+/* 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 = ["MailStringUtils"];
+
+var MailStringUtils = {
+ /**
+ * Convert a ByteString to a Uint8Array.
+ *
+ * @param {ByteString} str - The input string.
+ * @returns {Uint8Array} The output Uint8Array.
+ */
+ byteStringToUint8Array(str) {
+ let arr = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+ },
+
+ /**
+ * Convert a Uint8Array to a ByteString.
+ *
+ * @param {Uint8Array} arr - The input Uint8Array.
+ * @returns {ByteString} The output string.
+ */
+ uint8ArrayToByteString(arr) {
+ let str = "";
+ for (let i = 0; i < arr.length; i += 65536) {
+ str += String.fromCharCode.apply(null, arr.subarray(i, i + 65536));
+ }
+ return str;
+ },
+
+ /**
+ * Convert a ByteString to a string.
+ *
+ * @param {ByteString} str - The ByteString to convert.
+ * @returns {string} The converted string.
+ */
+ byteStringToString(str) {
+ return new TextDecoder().decode(this.byteStringToUint8Array(str));
+ },
+
+ /**
+ * Convert a string to a ByteString.
+ *
+ * @param {string} str - The string to convert.
+ * @returns {ByteString} The converted ByteString.
+ */
+ stringToByteString(str) {
+ return this.uint8ArrayToByteString(new TextEncoder().encode(str));
+ },
+
+ /**
+ * Detect the text encoding of a ByteString.
+ *
+ * @param {ByteString} str - The input string.
+ * @returns {string} The output charset name.
+ */
+ detectCharset(str) {
+ // Check the BOM.
+ let charset = "";
+ if (str.length >= 2) {
+ let byte0 = str.charCodeAt(0);
+ let byte1 = str.charCodeAt(1);
+ let byte2 = str.charCodeAt(2);
+ if (byte0 == 0xfe && byte1 == 0xff) {
+ charset = "UTF-16BE";
+ } else if (byte0 == 0xff && byte1 == 0xfe) {
+ charset = "UTF-16LE";
+ } else if (byte0 == 0xef && byte1 == 0xbb && byte2 == 0xbf) {
+ charset = "UTF-8";
+ }
+ }
+ if (charset) {
+ return charset;
+ }
+
+ // Use mozilla::EncodingDetector.
+ let compUtils = Cc[
+ "@mozilla.org/messengercompose/computils;1"
+ ].createInstance(Ci.nsIMsgCompUtils);
+ return compUtils.detectCharset(str);
+ },
+
+ /**
+ * Read and detect the charset of a file, then convert the file content to
+ * DOMString. If you're absolutely sure it's a UTF-8 encoded file, use
+ * IOUtils.readUTF8 instead.
+ *
+ * @param {string} path - An absolute file path.
+ * @returns {DOMString} The file content.
+ */
+ async readEncoded(path) {
+ let arr = await IOUtils.read(path);
+ let str = this.uint8ArrayToByteString(arr);
+ let charset = this.detectCharset(str);
+ return new TextDecoder(charset).decode(arr);
+ },
+};
diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp
new file mode 100644
index 0000000000..2e4efc72f5
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp
@@ -0,0 +1,51 @@
+/* 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/. */
+
+// This was copied from netwerk/base/LoadContextInfo.cpp
+
+#include "MailnewsLoadContextInfo.h"
+
+#include "mozilla/dom/ToJSValue.h"
+#include "nsIChannel.h"
+#include "nsILoadContext.h"
+#include "nsIWebNavigation.h"
+#include "nsNetUtil.h"
+
+// MailnewsLoadContextInfo
+
+NS_IMPL_ISUPPORTS(MailnewsLoadContextInfo, nsILoadContextInfo)
+
+MailnewsLoadContextInfo::MailnewsLoadContextInfo(
+ bool aIsPrivate, bool aIsAnonymous,
+ mozilla::OriginAttributes aOriginAttributes)
+ : mIsPrivate(aIsPrivate),
+ mIsAnonymous(aIsAnonymous),
+ mOriginAttributes(aOriginAttributes) {
+ mOriginAttributes.SyncAttributesWithPrivateBrowsing(mIsPrivate);
+}
+
+MailnewsLoadContextInfo::~MailnewsLoadContextInfo() {}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetIsPrivate(bool* aIsPrivate) {
+ *aIsPrivate = mIsPrivate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetIsAnonymous(bool* aIsAnonymous) {
+ *aIsAnonymous = mIsAnonymous;
+ return NS_OK;
+}
+
+mozilla::OriginAttributes const*
+MailnewsLoadContextInfo::OriginAttributesPtr() {
+ return &mOriginAttributes;
+}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetOriginAttributes(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
+ if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.h b/comm/mailnews/base/src/MailnewsLoadContextInfo.h
new file mode 100644
index 0000000000..b44f7ae43b
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.h
@@ -0,0 +1,32 @@
+/* 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/. */
+
+// This was copied from netwerk/base/LoadContextInfo.h
+
+#ifndef MailnewsLoadContextInfo_h__
+#define MailnewsLoadContextInfo_h__
+
+#include "nsILoadContextInfo.h"
+
+class nsIChannel;
+class nsILoadContext;
+
+class MailnewsLoadContextInfo : public nsILoadContextInfo {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSILOADCONTEXTINFO
+
+ MailnewsLoadContextInfo(bool aIsPrivate, bool aIsAnonymous,
+ mozilla::OriginAttributes aOriginAttributes);
+
+ private:
+ virtual ~MailnewsLoadContextInfo();
+
+ protected:
+ bool mIsPrivate : 1;
+ bool mIsAnonymous : 1;
+ mozilla::OriginAttributes mOriginAttributes;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/MailnewsMigrator.jsm b/comm/mailnews/base/src/MailnewsMigrator.jsm
new file mode 100644
index 0000000000..bc6ad9c3ef
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsMigrator.jsm
@@ -0,0 +1,352 @@
+/* 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/. */
+
+/**
+ * Migrate profile (prefs and other files) from older versions of Mailnews to
+ * current.
+ * This should be run at startup. It migrates as needed: each migration
+ * function should be written to be a no-op when the value is already migrated
+ * or was never used in the old version.
+ */
+
+const EXPORTED_SYMBOLS = ["migrateMailnews"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "migrateServerUris",
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+var kServerPrefVersion = 1;
+var kSmtpPrefVersion = 1;
+var kABRemoteContentPrefVersion = 1;
+
+function migrateMailnews() {
+ let migrations = [
+ migrateProfileClientid,
+ migrateServerAuthPref,
+ migrateServerAndUserName,
+ migrateABRemoteContentSettings,
+ ];
+
+ for (let fn of migrations) {
+ try {
+ fn();
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+/**
+ * Creates the server specific 'CLIENTID' prefs and tries to pair up any imap
+ * services with smtp services which are using the same username and hostname.
+ */
+function migrateProfileClientid() {
+ // Comma-separated list of all account ids.
+ let accounts = Services.prefs.getCharPref("mail.accountmanager.accounts", "");
+ // Comma-separated list of all smtp servers.
+ let smtpServers = Services.prefs.getCharPref("mail.smtpservers", "");
+ // If both accounts and smtpservers are empty then there is nothing to do.
+ if (accounts.length == 0 && smtpServers.length == 0) {
+ return;
+ }
+ // A cache to allow CLIENTIDs to be stored and shared across services that
+ // share a username and hostname.
+ let clientidCache = new Map();
+ // There may be accounts but no smtpservers so check the length before
+ // trying to split the smtp servers and iterate in the loop below.
+ if (smtpServers.length > 0) {
+ // Now walk all smtp servers and generate any missing CLIENTIDS, caching
+ // all CLIENTIDS along the way to be reused for matching imap servers
+ // if possible.
+
+ // Since the length of the smtpServers string is non-zero then we can split
+ // the string by comma and iterate each entry in the comma-separated list.
+ for (let key of smtpServers.split(",")) {
+ let server = "mail.smtpserver." + key + ".";
+ if (
+ !Services.prefs.prefHasUserValue(server + "clientid") ||
+ !Services.prefs.getCharPref(server + "clientid", "")
+ ) {
+ // Always give outgoing servers a new unique CLIENTID.
+ let newClientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ Services.prefs.setCharPref(server + "clientid", newClientid);
+ }
+ let username = Services.prefs.getCharPref(server + "username", "");
+ if (!username) {
+ // Not all SMTP servers require a username.
+ continue;
+ }
+
+ // Cache all CLIENTIDs from all outgoing servers to reuse them for any
+ // incoming servers which have a matching username and hostname.
+ let hostname = Services.prefs.getCharPref(server + "hostname");
+ let combinedKey;
+ try {
+ combinedKey =
+ username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+ } catch (e) {
+ combinedKey = username + "@" + hostname;
+ }
+ clientidCache.set(
+ combinedKey,
+ Services.prefs.getCharPref(server + "clientid")
+ );
+ }
+ }
+
+ // Now walk all imap accounts and generate any missing CLIENTIDS, reusing
+ // cached CLIENTIDS if possible.
+ for (let key of accounts.split(",")) {
+ let serverKey = Services.prefs.getCharPref(
+ "mail.account." + key + ".server"
+ );
+ let server = "mail.server." + serverKey + ".";
+ // Check if this imap server needs the CLIENTID preference to be populated.
+ if (
+ !Services.prefs.prefHasUserValue(server + "clientid") ||
+ !Services.prefs.getCharPref(server + "clientid", "")
+ ) {
+ // Clientid should only be provisioned for imap accounts.
+ if (Services.prefs.getCharPref(server + "type", "") != "imap") {
+ continue;
+ }
+ // Grab username + hostname to check if a CLIENTID is cached.
+ let username = Services.prefs.getCharPref(server + "userName", "");
+ if (!username) {
+ continue;
+ }
+ let hostname = Services.prefs.getCharPref(server + "hostname");
+ let combinedKey;
+ try {
+ combinedKey =
+ username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+ } catch (e) {
+ combinedKey = username + "@" + hostname;
+ }
+ if (!clientidCache.has(combinedKey)) {
+ // Generate a new CLIENTID if no matches were found from smtp servers.
+ let newClientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ Services.prefs.setCharPref(server + "clientid", newClientid);
+ } else {
+ // Otherwise if a cached CLIENTID was found for this username + hostname
+ // then we can just use the outgoing CLIENTID which was matching.
+ Services.prefs.setCharPref(
+ server + "clientid",
+ clientidCache.get(combinedKey)
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Migrates from pref useSecAuth to pref authMethod
+ */
+function migrateServerAuthPref() {
+ // comma-separated list of all accounts.
+ var accounts = Services.prefs
+ .getCharPref("mail.accountmanager.accounts")
+ .split(",");
+ for (let i = 0; i < accounts.length; i++) {
+ let accountKey = accounts[i]; // e.g. "account1"
+ if (!accountKey) {
+ continue;
+ }
+ let serverKey = Services.prefs.getCharPref(
+ "mail.account." + accountKey + ".server"
+ );
+ let server = "mail.server." + serverKey + ".";
+ if (Services.prefs.prefHasUserValue(server + "authMethod")) {
+ continue;
+ }
+ if (
+ !Services.prefs.prefHasUserValue(server + "useSecAuth") &&
+ !Services.prefs.prefHasUserValue(server + "auth_login")
+ ) {
+ continue;
+ }
+ if (Services.prefs.prefHasUserValue(server + "migrated")) {
+ continue;
+ }
+ // auth_login = false => old-style auth
+ // else: useSecAuth = true => "secure auth"
+ // else: cleartext pw
+ let auth_login = Services.prefs.getBoolPref(server + "auth_login", true);
+ // old default, default pref now removed
+ let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false);
+
+ if (auth_login) {
+ if (useSecAuth) {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.secure
+ );
+ } else {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.passwordCleartext
+ );
+ }
+ } else {
+ Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.old);
+ }
+ Services.prefs.setIntPref(server + "migrated", kServerPrefVersion);
+ }
+
+ // same again for SMTP servers
+ var smtpservers = Services.prefs.getCharPref("mail.smtpservers").split(",");
+ for (let i = 0; i < smtpservers.length; i++) {
+ if (!smtpservers[i]) {
+ continue;
+ }
+ let server = "mail.smtpserver." + smtpservers[i] + ".";
+ if (Services.prefs.prefHasUserValue(server + "authMethod")) {
+ continue;
+ }
+ if (
+ !Services.prefs.prefHasUserValue(server + "useSecAuth") &&
+ !Services.prefs.prefHasUserValue(server + "auth_method")
+ ) {
+ continue;
+ }
+ if (Services.prefs.prefHasUserValue(server + "migrated")) {
+ continue;
+ }
+ // auth_method = 0 => no auth
+ // else: useSecAuth = true => "secure auth"
+ // else: cleartext pw
+ let auth_method = Services.prefs.getIntPref(server + "auth_method", 1);
+ let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false);
+
+ if (auth_method) {
+ if (useSecAuth) {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.secure
+ );
+ } else {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.passwordCleartext
+ );
+ }
+ } else {
+ Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.none);
+ }
+ Services.prefs.setIntPref(server + "migrated", kSmtpPrefVersion);
+ }
+}
+
+/**
+ * For each mail.server.key. branch,
+ * - migrate realhostname to hostname
+ * - migrate realuserName to userName
+ */
+function migrateServerAndUserName() {
+ let branch = Services.prefs.getBranch("mail.server.");
+
+ // Collect all the server keys.
+ let keySet = new Set();
+ for (let name of branch.getChildList("")) {
+ keySet.add(name.split(".")[0]);
+ }
+ keySet.delete("default");
+
+ for (let key of keySet) {
+ let type = branch.getCharPref(`${key}.type`, "");
+ let hostname = branch.getCharPref(`${key}.hostname`, "");
+ let username = branch.getCharPref(`${key}.userName`, "");
+ let realHostname = branch.getCharPref(`${key}.realhostname`, "");
+ if (realHostname) {
+ branch.setCharPref(`${key}.hostname`, realHostname);
+ branch.clearUserPref(`${key}.realhostname`);
+ }
+ let realUsername = branch.getCharPref(`${key}.realuserName`, "");
+ if (realUsername) {
+ branch.setCharPref(`${key}.userName`, realUsername);
+ branch.clearUserPref(`${key}.realuserName`);
+ }
+ // Previously, when hostname/username changed, LoginManager and many prefs
+ // still contain the old hostname/username, try to migrate them to use the
+ // new hostname/username.
+ if (
+ ["imap", "pop3", "nntp"].includes(type) &&
+ (realHostname || realUsername)
+ ) {
+ let localStoreType = { imap: "imap", pop3: "mailbox", nntp: "news" }[
+ type
+ ];
+ lazy.migrateServerUris(
+ localStoreType,
+ hostname,
+ username,
+ realHostname || hostname,
+ realUsername || username
+ );
+ }
+ }
+}
+
+/**
+ * The address book used to contain information about whether to allow remote
+ * content for a given contact. Now we use the permission manager for that.
+ * Do a one-time migration for it.
+ */
+function migrateABRemoteContentSettings() {
+ if (Services.prefs.prefHasUserValue("mail.ab_remote_content.migrated")) {
+ return;
+ }
+
+ // Search through all of our local address books looking for a match.
+ for (let addrbook of MailServices.ab.directories) {
+ let migrateAddress = function (aEmail) {
+ let uri = Services.io.newURI(
+ "chrome://messenger/content/email=" + aEmail
+ );
+ Services.perms.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipal(uri, {}),
+ "image",
+ Services.perms.ALLOW_ACTION
+ );
+ };
+
+ try {
+ // If it's a read-only book, don't try to find a card as we we could never
+ // have set the AllowRemoteContent property.
+ if (addrbook.readOnly) {
+ continue;
+ }
+
+ for (let card of addrbook.childCards) {
+ if (card.getProperty("AllowRemoteContent", "0") == "0") {
+ // Not allowed for this contact.
+ continue;
+ }
+
+ for (let emailAddress of card.emailAddresses) {
+ migrateAddress(emailAddress);
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ Services.prefs.setIntPref(
+ "mail.ab_remote_content.migrated",
+ kABRemoteContentPrefVersion
+ );
+}
diff --git a/comm/mailnews/base/src/MsgAsyncPrompter.jsm b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
new file mode 100644
index 0000000000..e04e9a9418
--- /dev/null
+++ b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
@@ -0,0 +1,621 @@
+/* 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 EXPORTED_SYMBOLS = ["MsgAsyncPrompter", "MsgAuthPrompt"];
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Deprecated: "resource://gre/modules/Deprecated.sys.mjs",
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "dialogsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "passwordsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "brandFullName", function () {
+ return Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandFullName");
+});
+
+function runnablePrompter(asyncPrompter, hashKey) {
+ this._asyncPrompter = asyncPrompter;
+ this._hashKey = hashKey;
+}
+
+runnablePrompter.prototype = {
+ _asyncPrompter: null,
+ _hashKey: null,
+
+ _promiseAuthPrompt(listener) {
+ return new Promise((resolve, reject) => {
+ try {
+ listener.onPromptStartAsync({ onAuthResult: resolve });
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) {
+ // Fall back to onPromptStart, for add-ons compat
+ lazy.Deprecated.warning(
+ "onPromptStart has been replaced by onPromptStartAsync",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399"
+ );
+ let ok = listener.onPromptStart();
+ resolve(ok);
+ } else {
+ reject(e);
+ }
+ }
+ });
+ },
+
+ async run() {
+ await Services.logins.initializationPromise;
+ this._asyncPrompter._log.debug("Running prompt for " + this._hashKey);
+ let prompter = this._asyncPrompter._pendingPrompts[this._hashKey];
+ let ok = false;
+ try {
+ ok = await this._promiseAuthPrompt(prompter.first);
+ } catch (ex) {
+ console.error("runnablePrompter:run: " + ex + "\n");
+ prompter.first.onPromptCanceled();
+ }
+
+ delete this._asyncPrompter._pendingPrompts[this._hashKey];
+
+ for (var consumer of prompter.consumers) {
+ try {
+ if (ok) {
+ consumer.onPromptAuthAvailable();
+ } else {
+ consumer.onPromptCanceled();
+ }
+ } catch (ex) {
+ // Log the error for extension devs and others to pick up.
+ console.error(
+ "runnablePrompter:run: consumer.onPrompt* reported an exception: " +
+ ex +
+ "\n"
+ );
+ }
+ }
+ this._asyncPrompter._asyncPromptInProgress--;
+
+ this._asyncPrompter._log.debug(
+ "Finished running prompter for " + this._hashKey
+ );
+ this._asyncPrompter._doAsyncAuthPrompt();
+ },
+};
+
+function MsgAsyncPrompter() {
+ this._pendingPrompts = {};
+ // By default, only log warnings to the error console
+ // You can use the preference:
+ // msgAsyncPrompter.loglevel
+ // To change this up. Values should be one of:
+ // Fatal/Error/Warn/Info/Config/Debug/Trace/All
+ this._log = console.createInstance({
+ prefix: "mail.asyncprompter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.asyncprompter.loglevel",
+ });
+}
+
+MsgAsyncPrompter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgAsyncPrompter"]),
+
+ _pendingPrompts: null,
+ _asyncPromptInProgress: 0,
+ _log: null,
+
+ queueAsyncAuthPrompt(aKey, aJumpQueue, aCaller) {
+ if (aKey in this._pendingPrompts) {
+ this._log.debug(
+ "Prompt bound to an existing one in the queue, key: " + aKey
+ );
+ this._pendingPrompts[aKey].consumers.push(aCaller);
+ return;
+ }
+
+ this._log.debug("Adding new prompt to the queue, key: " + aKey);
+ let asyncPrompt = {
+ first: aCaller,
+ consumers: [],
+ };
+
+ this._pendingPrompts[aKey] = asyncPrompt;
+ if (aJumpQueue) {
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Forcing runnablePrompter for " + aKey);
+
+ let runnable = new runnablePrompter(this, aKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ } else {
+ this._doAsyncAuthPrompt();
+ }
+ },
+
+ _doAsyncAuthPrompt() {
+ if (this._asyncPromptInProgress > 0) {
+ this._log.debug(
+ "_doAsyncAuthPrompt bypassed - prompt already in progress"
+ );
+ return;
+ }
+
+ // Find the first prompt key we have in the queue.
+ let hashKey = null;
+ for (hashKey in this._pendingPrompts) {
+ break;
+ }
+
+ if (!hashKey) {
+ return;
+ }
+
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Dispatching runnablePrompter for " + hashKey);
+
+ let runnable = new runnablePrompter(this, hashKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+};
+
+/**
+ * An implementation of nsIAuthPrompt which is roughly the same as
+ * LoginManagerAuthPrompter was before the check box option was removed from
+ * nsIPromptService.
+ *
+ * Calls our own version of promptUsernameAndPassword/promptPassword, which
+ * directly open the prompt.
+ *
+ * @implements {nsIAuthPrompt}
+ */
+class MsgAuthPrompt {
+ QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt"]);
+
+ _getFormattedOrigin(aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI);
+ }
+
+ return uri.scheme + "://" + uri.displayHostPort;
+ }
+
+ _getRealmInfo(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString)) {
+ return [null, null, null];
+ }
+
+ let uri = Services.io.newURI(aRealmString);
+ let pathname = "";
+
+ if (uri.pathQueryRef != "/") {
+ pathname = uri.pathQueryRef;
+ }
+
+ let formattedOrigin = this._getFormattedOrigin(uri);
+
+ return [formattedOrigin, formattedOrigin + pathname, uri.username];
+ }
+
+ _getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return lazy.passwordsBundle.formatStringFromName(key, formatArgs);
+ }
+ return lazy.passwordsBundle.GetStringFromName(key);
+ }
+
+ /**
+ * Wrapper around the prompt service prompt. Saving random fields here
+ * doesn't really make sense and therefore isn't implemented.
+ */
+ prompt(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aDefaultText,
+ aResult
+ ) {
+ if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
+ throw new Components.Exception(
+ "prompt only supports SAVE_PASSWORD_NEVER",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ if (aDefaultText) {
+ aResult.value = aDefaultText;
+ }
+
+ return Services.prompt.prompt(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aResult,
+ null,
+ {}
+ );
+ }
+
+ /**
+ * Looks up a username and password in the database. Will prompt the user
+ * with a dialog, even if a username and password are found.
+ */
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aUsername,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm] = this._getRealmInfo(aPasswordRealm);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == aUsername.value) {
+ checkBox.value = true;
+ aUsername.value = login.username;
+ // If the caller provided a password, prefer it.
+ if (!aPassword.value) {
+ aPassword.value = login.password;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (!ok || !checkBox.value || !origin) {
+ return ok;
+ }
+
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ aUsername.value,
+ aPassword.value
+ );
+ Services.logins.addLogin(newLogin);
+
+ return ok;
+ }
+
+ /**
+ * If a password is found in the database for the password realm, it is
+ * returned straight away without displaying a dialog.
+ *
+ * If a password is not found in the database, the user will be prompted
+ * with a dialog with a text field and ok/cancel buttons. If the user
+ * allows it, then the password will be saved in the database.
+ */
+ promptPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ if (!aPassword.value) {
+ // Look for existing logins.
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == username) {
+ aPassword.value = login.password;
+ return true;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptPassword(
+ aDialogTitle,
+ aText,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (ok && checkBox.value && origin && aPassword.value) {
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ username,
+ aPassword.value
+ );
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ return ok;
+ }
+
+ /**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ *
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+ promptPassword2(dialogTitle, text, password, checkMsg, checkValue) {
+ return nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+ );
+ }
+
+ /**
+ * Requests a username and a password. Implementations will commonly show a
+ * dialog with a username and password field, depending on flags also a
+ * domain field.
+ *
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {number} level - One of the level constants from nsIAuthPrompt2.
+ * See there for descriptions of the levels.
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * The implementation should fill in this object with the information
+ * entered by the user before returning.
+ * @param {string} checkboxLabel
+ * Text to appear with the checkbox. If null, check box will not be shown.
+ * @param {object} checkValue
+ * Contains the initial checked state of the checkbox when this method
+ * is called and the final checked state after this method returns.
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+ promptAuth(channel, level, authInfo, checkboxLabel, checkValue) {
+ let title = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ let text = lazy.dialogsBundle.formatStringFromName(
+ "EnterUserPasswordFor2",
+ [`${channel.URI.scheme}://${channel.URI.host}`]
+ );
+
+ let username = { value: authInfo.username || "" };
+ let password = { value: authInfo.password || "" };
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ title,
+ text,
+ username,
+ password,
+ checkboxLabel,
+ checkValue
+ );
+
+ if (ok) {
+ authInfo.username = username.value;
+ authInfo.password = password.value;
+ }
+
+ return ok;
+ }
+}
+
+/**
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} username
+ * Contains the default value for the username field when this method
+ * is called (null value is ok). Upon return, if the user pressed OK,
+ * then this parameter contains a newly allocated string value.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+function nsIPrompt_promptUsernameAndPassword(
+ dialogTitle,
+ text,
+ username,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptUserAndPass",
+ title: dialogTitle,
+ text,
+ user: username.value,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ username.value = args.user;
+ password.value = args.pass;
+ }
+
+ return ok;
+}
+
+/**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ *
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+function nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptPassword",
+ title: dialogTitle,
+ text,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ password.value = args.pass;
+ }
+
+ return ok;
+}
diff --git a/comm/mailnews/base/src/MsgDBCacheManager.jsm b/comm/mailnews/base/src/MsgDBCacheManager.jsm
new file mode 100644
index 0000000000..9506f08e5e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgDBCacheManager.jsm
@@ -0,0 +1,185 @@
+/* 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/. */
+
+/**
+ * Message DB Cache manager
+ */
+
+/* :::::::: Constants and Helpers ::::::::::::::: */
+
+const EXPORTED_SYMBOLS = ["msgDBCacheManager"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var log = console.createInstance({
+ prefix: "mailnews.database.dbcache",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.database.dbcache.loglevel",
+});
+
+/**
+ */
+var DBCACHE_INTERVAL_DEFAULT_MS = 60000; // 1 minute
+
+/* :::::::: The Module ::::::::::::::: */
+
+var msgDBCacheManager = {
+ _initialized: false,
+
+ _msgDBCacheTimer: null,
+
+ _msgDBCacheTimerIntervalMS: DBCACHE_INTERVAL_DEFAULT_MS,
+
+ _dbService: null,
+
+ /**
+ * This is called on startup
+ */
+ init() {
+ if (this._initialized) {
+ return;
+ }
+
+ this._dbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+
+ // we listen for "quit-application-granted" instead of
+ // "quit-application-requested" because other observers of the
+ // latter can cancel the shutdown.
+ Services.obs.addObserver(this, "quit-application-granted");
+
+ this.startPeriodicCheck();
+
+ this._initialized = true;
+ },
+
+ /* ........ Timer Callback ................*/
+
+ _dbCacheCheckTimerCallback() {
+ msgDBCacheManager.checkCachedDBs();
+ },
+
+ /* ........ Observer Notification Handler ................*/
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ // This is observed before any windows start unloading if something other
+ // than the last 3pane window closing requested the application be
+ // shutdown. For example, when the user quits via the file menu.
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.stopPeriodicCheck();
+ break;
+ }
+ },
+
+ /* ........ Public API ................*/
+
+ /**
+ * Stops db cache check
+ */
+ stopPeriodicCheck() {
+ if (this._dbCacheCheckTimer) {
+ this._dbCacheCheckTimer.cancel();
+
+ delete this._dbCacheCheckTimer;
+ this._dbCacheCheckTimer = null;
+ }
+ },
+
+ /**
+ * Starts periodic db cache check
+ */
+ startPeriodicCheck() {
+ if (!this._dbCacheCheckTimer) {
+ this._dbCacheCheckTimer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+
+ this._dbCacheCheckTimer.initWithCallback(
+ this._dbCacheCheckTimerCallback,
+ this._msgDBCacheTimerIntervalMS,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ }
+ },
+
+ /**
+ * Checks if any DBs need to be closed due to inactivity or too many of them open.
+ */
+ checkCachedDBs() {
+ let idleLimit = Services.prefs.getIntPref("mail.db.idle_limit");
+ let maxOpenDBs = Services.prefs.getIntPref("mail.db.max_open");
+
+ // db.lastUseTime below is in microseconds while Date.now and idleLimit pref
+ // is in milliseconds.
+ let closeThreshold = (Date.now() - idleLimit) * 1000;
+ let cachedDBs = this._dbService.openDBs;
+ log.info(
+ "Periodic check of cached folder databases (DBs), count=" +
+ cachedDBs.length
+ );
+ // Count databases that are already closed or get closed now due to inactivity.
+ let numClosing = 0;
+ // Count databases whose folder is open in a window.
+ let numOpenInWindow = 0;
+ let dbs = [];
+ for (let db of cachedDBs) {
+ if (!db.folder?.databaseOpen) {
+ // The DB isn't really open anymore.
+ log.debug("Skipping, DB not open for folder: " + db.folder?.name);
+ numClosing++;
+ continue;
+ }
+
+ if (MailServices.mailSession.IsFolderOpenInWindow(db.folder)) {
+ // The folder is open in a window so this DB must not be closed.
+ log.debug("Skipping, DB open in window for folder: " + db.folder.name);
+ numOpenInWindow++;
+ continue;
+ }
+
+ if (db.lastUseTime < closeThreshold) {
+ // DB open too log without activity.
+ log.debug("Closing expired DB for folder: " + db.folder.name);
+ db.folder.msgDatabase = null;
+ numClosing++;
+ continue;
+ }
+
+ // Database eligible for closing.
+ dbs.push(db);
+ }
+ log.info(
+ "DBs open in a window: " +
+ numOpenInWindow +
+ ", DBs open: " +
+ dbs.length +
+ ", DBs already closing: " +
+ numClosing
+ );
+ let dbsToClose = Math.max(
+ dbs.length - Math.max(maxOpenDBs - numOpenInWindow, 0),
+ 0
+ );
+ if (dbsToClose > 0) {
+ // Close some DBs so that we do not have more than maxOpenDBs.
+ // However, we skipped DBs for folders that are open in a window
+ // so if there are so many windows open, it may be possible for
+ // more than maxOpenDBs folders to stay open after this loop.
+ log.info("Need to close " + dbsToClose + " more DBs");
+ // Order databases by lowest lastUseTime (oldest) at the end.
+ dbs.sort((a, b) => b.lastUseTime - a.lastUseTime);
+ while (dbsToClose > 0) {
+ let db = dbs.pop();
+ log.debug("Closing DB for folder: " + db.folder.name);
+ db.folder.msgDatabase = null;
+ dbsToClose--;
+ }
+ }
+ },
+};
diff --git a/comm/mailnews/base/src/MsgIncomingServer.jsm b/comm/mailnews/base/src/MsgIncomingServer.jsm
new file mode 100644
index 0000000000..768fe9340e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgIncomingServer.jsm
@@ -0,0 +1,1268 @@
+/* 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 = ["migrateServerUris", "MsgIncomingServer"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * When hostname/username changes, update the corresponding entry in
+ * nsILoginManager.
+ *
+ * @param {string} localStoreType - The store type of the current server.
+ * @param {string} oldHostname - The hostname before the change.
+ * @param {string} oldUsername - The username before the change.
+ * @param {string} newHostname - The hostname after the change.
+ * @param {string} newUsername - The username after the change.
+ */
+function migratePassword(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+) {
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname;
+ let oldServerUri = `${localStoreType}://${encodeURIComponent(oldHostname)}`;
+ newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname;
+ let newServerUri = `${localStoreType}://${encodeURIComponent(newHostname)}`;
+
+ let logins = Services.logins.findLogins(oldServerUri, "", oldServerUri);
+ for (let login of logins) {
+ if (login.username == oldUsername) {
+ // If a nsILoginInfo exists for the old hostname/username, update it to
+ // use the new hostname/username.
+ let newLogin = Cc[
+ "@mozilla.org/login-manager/loginInfo;1"
+ ].createInstance(Ci.nsILoginInfo);
+ newLogin.init(
+ newServerUri,
+ null,
+ newServerUri,
+ newUsername,
+ login.password,
+ "",
+ ""
+ );
+ Services.logins.modifyLogin(login, newLogin);
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update the folder attributes in related
+ * identities.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateIdentities(oldServerUri, newServerUri) {
+ for (let identity of MailServices.accounts.allIdentities) {
+ let attributes = [
+ "fcc_folder",
+ "draft_folder",
+ "archive_folder",
+ "stationery_folder",
+ ];
+ for (let attr of attributes) {
+ let folderUri = identity.getUnicharAttribute(attr);
+ if (folderUri.startsWith(oldServerUri)) {
+ identity.setUnicharAttribute(
+ attr,
+ folderUri.replace(oldServerUri, newServerUri)
+ );
+ }
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update .spamActionTargetAccount and
+ * .spamActionTargetFolder prefs.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateSpamActions(oldServerUri, newServerUri) {
+ for (let server of MailServices.accounts.allServers) {
+ let targetAccount = server.getCharValue("spamActionTargetAccount");
+ let targetFolder = server.getUnicharValue("spamActionTargetFolder");
+ if (targetAccount.startsWith(oldServerUri)) {
+ server.setCharValue(
+ "spamActionTargetAccount",
+ targetAccount.replace(oldServerUri, newServerUri)
+ );
+ }
+ if (targetFolder.startsWith(oldServerUri)) {
+ server.setUnicharValue(
+ "spamActionTargetFolder",
+ targetFolder.replace(oldServerUri, newServerUri)
+ );
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update targetFolderUri in related filters
+ * to the new folder uri.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateFilters(oldServerUri, newServerUri) {
+ for (let server of MailServices.accounts.allServers) {
+ let filterList;
+ try {
+ filterList = server.getFilterList(null);
+ if (!server.canHaveFilters || !filterList) {
+ continue;
+ }
+ } catch (e) {
+ continue;
+ }
+ let changed = false;
+ for (let i = 0; i < filterList.filterCount; i++) {
+ let filter = filterList.getFilterAt(i);
+ for (let action of filter.sortedActionList) {
+ let targetFolderUri;
+ try {
+ targetFolderUri = action.targetFolderUri;
+ } catch (e) {
+ continue;
+ }
+ if (targetFolderUri.startsWith(oldServerUri)) {
+ action.targetFolderUri = targetFolderUri.replace(
+ oldServerUri,
+ newServerUri
+ );
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ filterList.saveToDefaultFile();
+ }
+ }
+}
+
+/**
+ * Migrate server uris in LoginManager and various account/folder prefs.
+ *
+ * @param {string} localStoreType - The store type of the current server.
+ * @param {string} oldHostname - The hostname before the change.
+ * @param {string} oldUsername - The username before the change.
+ * @param {string} newHostname - The hostname after the change.
+ * @param {string} newUsername - The username after the change.
+ */
+function migrateServerUris(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+) {
+ try {
+ migratePassword(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+ );
+ } catch (e) {
+ console.error(e);
+ }
+
+ let oldAuth = oldUsername ? `${encodeURIComponent(oldUsername)}@` : "";
+ let newAuth = newUsername ? `${encodeURIComponent(newUsername)}@` : "";
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname;
+ let oldServerUri = `${localStoreType}://${oldAuth}${encodeURIComponent(
+ oldHostname
+ )}`;
+ newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname;
+ let newServerUri = `${localStoreType}://${newAuth}${encodeURIComponent(
+ newHostname
+ )}`;
+
+ try {
+ migrateIdentities(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ migrateSpamActions(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ migrateFilters(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+/**
+ * A base class for incoming server, should not be used directly.
+ *
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsIObserver}
+ * @abstract
+ */
+class MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ "nsIObserver",
+ ]);
+
+ constructor() {
+ // nsIMsgIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Char", "type"],
+ ["Char", "clientid"],
+ ["Int", "authMethod"],
+ ["Int", "biffMinutes", "check_time"],
+ ["Int", "maxMessageSize", "max_size"],
+ ["Int", "incomingDuplicateAction", "dup_action"],
+ ["Bool", "clientidEnabled"],
+ ["Bool", "downloadOnBiff", "download_on_biff"],
+ ["Bool", "valid"],
+ ["Bool", "emptyTrashOnExit", "empty_trash_on_exit"],
+ ["Bool", "canDelete"],
+ ["Bool", "loginAtStartUp", "login_at_startup"],
+ [
+ "Bool",
+ "defaultCopiesAndFoldersPrefsToServer",
+ "allows_specialfolders_usage",
+ ],
+ ["Bool", "canCreateFoldersOnServer", "canCreateFolders"],
+ ["Bool", "canFileMessagesOnServer", "canFileMessages"],
+ ["Bool", "limitOfflineMessageSize", "limit_offline_message_size"],
+ ["Bool", "hidden"],
+ ]);
+
+ // nsIMsgIncomingServer attributes.
+ this.performingBiff = false;
+ this.accountManagerChrome = "am-main.xhtml";
+ this.biffState = Ci.nsIMsgFolder.nsMsgBiffState_Unknown;
+ this.downloadMessagesAtStartup = false;
+ this.canHaveFilters = true;
+ this.canBeDefaultServer = false;
+ this.displayStartupPage = true;
+ this.supportsDiskSpace = true;
+ this.canCompactFoldersOnServer = true;
+ this.canUndoDeleteOnServer = true;
+ this.sortOrder = 100000000;
+
+ // @type {Map<string, number>} - The key is MsgId+Subject, the value is
+ // this._hdrIndex.
+ this._knownHdrMap = new Map();
+ this._hdrIndex = 0;
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed");
+ }
+
+ /**
+ * Observe() receives notifications for all accounts, not just this server's
+ * account. So we ignore all notifications not intended for this server.
+ * When the state of the password manager changes we need to clear the
+ * this server's password from the cache in case the user just changed or
+ * removed the password or username.
+ * OAuth2 servers often automatically change the password manager's stored
+ * password (the token).
+ */
+ observe(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ // Check that the notification is for this server and user.
+ let otherFullName = "";
+ let otherUsername = "";
+ if (subject instanceof Ci.nsILoginInfo) {
+ // The login info for a server has been removed with data being
+ // "removeLogin" or "removeAllLogins".
+ otherFullName = subject.origin;
+ otherUsername = subject.username;
+ } else if (subject instanceof Ci.nsIArray) {
+ // Probably a 2 element array containing old and new login info due to
+ // data being "modifyLogin". E.g., a user has modified the password or
+ // username in the password manager or an OAuth2 token string has
+ // automatically changed. Only need to look at names in first array
+ // element (login info before any modification) since the user might
+ // have changed the username as found in the 2nd elements. (The
+ // hostname can't be modified in the password manager.
+ otherFullName = subject.queryElementAt(0, Ci.nsISupports).origin;
+ otherUsername = subject.queryElementAt(0, Ci.nsISupports).username;
+ }
+ if (otherFullName) {
+ if (
+ otherFullName != "mailbox://" + this.hostName ||
+ otherUsername != this.username
+ ) {
+ // Not for this server; keep this server's cached password.
+ return;
+ }
+ } else if (data != "hostSavingDisabled") {
+ // "hostSavingDisabled" only occurs during test_smtpServer.js and
+ // expects the password to be removed from memory cache. Otherwise, we
+ // don't have enough information to decide to remove the cached
+ // password, so keep it.
+ return;
+ }
+ // Remove the password for this server cached in memory.
+ this.password = "";
+ }
+ }
+
+ /**
+ * Set up getters/setters for attributes that map directly to pref values.
+ *
+ * @param {string[]} attributes - An array of attributes. Each attribute is
+ * defined by its type, name and corresponding prefName.
+ */
+ _mapAttrsToPrefs(attributes) {
+ for (let [type, attrName, prefName] of attributes) {
+ prefName = prefName || attrName;
+ Object.defineProperty(this, attrName, {
+ configurable: true,
+ get: () => this[`get${type}Value`](prefName),
+ set: value => {
+ this[`set${type}Value`](prefName, value);
+ },
+ });
+ }
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ set key(key) {
+ this._key = key;
+ this._prefs = Services.prefs.getBranch(`mail.server.${key}.`);
+ this._defaultPrefs = Services.prefs.getBranch("mail.server.default.");
+ }
+
+ get UID() {
+ let uid = this._prefs.getStringPref("uid", "");
+ if (uid) {
+ return uid;
+ }
+ return (this.UID = Services.uuid
+ .generateUUID()
+ .toString()
+ .substring(1, 37));
+ }
+
+ set UID(uid) {
+ if (this._prefs.prefHasUserValue("uid")) {
+ throw new Components.Exception("uid is already set", Cr.NS_ERROR_ABORT);
+ }
+ this._prefs.setStringPref("uid", uid);
+ }
+
+ get hostName() {
+ let hostname = this.getUnicharValue("hostname");
+ if (hostname.includes(":")) {
+ // Reformat the hostname if it contains a port number.
+ this.hostName = hostname;
+ return this.hostName;
+ }
+ return hostname;
+ }
+
+ set hostName(value) {
+ let oldName = this.hostName;
+ this._setHostName("hostname", value);
+
+ if (oldName && oldName != value) {
+ this.onUserOrHostNameChanged(oldName, value, true);
+ }
+ }
+
+ _setHostName(prefName, value) {
+ let [host, port] = value.split(":");
+ if (port) {
+ this.port = Number(port);
+ }
+ this.setUnicharValue(prefName, host);
+ }
+
+ get username() {
+ return this.getUnicharValue("userName");
+ }
+
+ set username(value) {
+ let oldName = this.username;
+ if (oldName && oldName != value) {
+ this.setUnicharValue("userName", value);
+ this.onUserOrHostNameChanged(oldName, value, false);
+ } else {
+ this.setUnicharValue("userName", value);
+ }
+ }
+
+ get port() {
+ let port = this.getIntValue("port");
+ if (port > 1) {
+ return port;
+ }
+
+ // If the port isn't set, use the default port based on the protocol.
+ return this.protocolInfo.getDefaultServerPort(
+ this.socketType == Ci.nsMsgSocketType.SSL
+ );
+ }
+
+ set port(value) {
+ this.setIntValue("port", value);
+ }
+
+ get protocolInfo() {
+ return Cc[
+ `@mozilla.org/messenger/protocol/info;1?type=${this.type}`
+ ].getService(Ci.nsIMsgProtocolInfo);
+ }
+
+ get socketType() {
+ try {
+ return this._prefs.getIntPref("socketType");
+ } catch (e) {
+ // socketType is set to default value. Look at isSecure setting.
+ if (this._prefs.getBoolPref("isSecure", false)) {
+ return Ci.nsMsgSocketType.SSL;
+ }
+ return this._defaultPrefs.getIntPref(
+ "socketType",
+ Ci.nsMsgSocketType.plain
+ );
+ }
+ }
+
+ set socketType(value) {
+ let wasSecure = this.isSecure;
+ this._prefs.setIntPref("socketType", value);
+ let isSecure = this.isSecure;
+ if (wasSecure != isSecure) {
+ this.rootFolder.NotifyBoolPropertyChanged(
+ "isSecure",
+ wasSecure,
+ isSecure
+ );
+ }
+ }
+
+ get isSecure() {
+ return [Ci.nsMsgSocketType.alwaysSTARTTLS, Ci.nsMsgSocketType.SSL].includes(
+ this.socketType
+ );
+ }
+
+ get serverURI() {
+ return this._getServerURI(true);
+ }
+
+ /**
+ * Get server URI in the form of localStoreType://[user@]hostname.
+ *
+ * @param {boolean} includeUsername - Whether to include the username.
+ * @returns {string}
+ */
+ _getServerURI(includeUsername) {
+ let auth =
+ includeUsername && this.username
+ ? `${encodeURIComponent(this.username)}@`
+ : "";
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ let hostname = this.hostName.includes(":")
+ ? `[${this.hostName}]`
+ : this.hostName;
+ return `${this.localStoreType}://${auth}${encodeURIComponent(hostname)}`;
+ }
+
+ get prettyName() {
+ return this.getUnicharValue("name") || this.constructedPrettyName;
+ }
+
+ set prettyName(value) {
+ this.setUnicharValue("name", value);
+ this.rootFolder.prettyName = value;
+ }
+
+ /**
+ * Construct a pretty name from username and hostname.
+ *
+ * @param {string} username - The user name.
+ * @param {string} hostname - The host name.
+ * @returns {string}
+ */
+ _constructPrettyName(username, hostname) {
+ let prefix = username ? `${username} on ` : "";
+ return `${prefix}${hostname}`;
+ }
+
+ get constructedPrettyName() {
+ return this._constructPrettyName(this.username, this.hostName);
+ }
+
+ get localPath() {
+ let localPath = this.getFileValue("directory-rel", "directory");
+ if (localPath) {
+ // If the local path has already been set, use it.
+ return localPath;
+ }
+
+ // Create the path using protocol info and hostname.
+ localPath = this.protocolInfo.defaultLocalPath;
+ if (!localPath.exists()) {
+ localPath.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ localPath.append(this.hostName);
+ localPath.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ this.localPath = localPath;
+ return localPath;
+ }
+
+ set localPath(localPath) {
+ this.setFileValue("directory-rel", "directory", localPath);
+ }
+
+ get rootFolder() {
+ if (!this._rootFolder) {
+ this._rootFolder = MailServices.folderLookup.getOrCreateFolderForURL(
+ this.serverURI
+ );
+ }
+ return this._rootFolder;
+ }
+
+ get rootMsgFolder() {
+ return this.rootFolder;
+ }
+
+ get msgStore() {
+ if (!this._msgStore) {
+ let contractId = this.getCharValue("storeContractID");
+ if (!contractId) {
+ contractId = "@mozilla.org/msgstore/berkeleystore;1";
+ this.setCharValue("storeContractID", contractId);
+ }
+
+ // After someone starts using the pluggable store, we can no longer
+ // change the value.
+ this.setBoolValue("canChangeStoreType", false);
+
+ this._msgStore = Cc[contractId].createInstance(Ci.nsIMsgPluggableStore);
+ }
+ return this._msgStore;
+ }
+
+ get doBiff() {
+ try {
+ return this._prefs.getBoolPref("check_new_mail");
+ } catch (e) {
+ return this.protocolInfo.defaultDoBiff;
+ }
+ }
+
+ set doBiff(value) {
+ let biffManager = Cc["@mozilla.org/messenger/biffManager;1"].getService(
+ Ci.nsIMsgBiffManager
+ );
+ if (value) {
+ biffManager.addServerBiff(this);
+ } else {
+ biffManager.removeServerBiff(this);
+ }
+ this._prefs.setBoolPref("check_new_mail", value);
+ }
+
+ /**
+ * type, attribute name, pref name
+ */
+ _retentionSettingsPrefs = [
+ ["Int", "retainByPreference", "retainBy"],
+ ["Int", "numHeadersToKeep", "numHdrsToKeep"],
+ ["Int", "daysToKeepHdrs"],
+ ["Int", "daysToKeepBodies"],
+ ["Bool", "cleanupBodiesByDays", "cleanupBodies"],
+ ["Bool", "applyToFlaggedMessages"],
+ ];
+
+ get retentionSettings() {
+ let settings = Cc[
+ "@mozilla.org/msgDatabase/retentionSettings;1"
+ ].createInstance(Ci.nsIMsgRetentionSettings);
+ for (let [type, attrName, prefName] of this._retentionSettingsPrefs) {
+ prefName = prefName || attrName;
+ settings[attrName] = this[`get${type}Value`](prefName);
+ }
+ return settings;
+ }
+
+ set retentionSettings(settings) {
+ for (let [type, attrName, prefName] of this._retentionSettingsPrefs) {
+ prefName = prefName || attrName;
+ this[`set${type}Value`](prefName, settings[attrName]);
+ }
+ }
+
+ get spamSettings() {
+ if (!this.getCharValue("spamActionTargetAccount")) {
+ this.setCharValue("spamActionTargetAccount", this.serverURI);
+ }
+ if (!this._spamSettings) {
+ this._spamSettings = Cc[
+ "@mozilla.org/messenger/spamsettings;1"
+ ].createInstance(Ci.nsISpamSettings);
+ try {
+ this._spamSettings.initialize(this);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return this._spamSettings;
+ }
+
+ get spamFilterPlugin() {
+ if (!this._spamFilterPlugin) {
+ this._spamFilterPlugin = Cc[
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter"
+ ].getService(Ci.nsIMsgFilterPlugin);
+ }
+ return this._spamFilterPlugin;
+ }
+
+ get isDeferredTo() {
+ let account = MailServices.accounts.FindAccountForServer(this);
+ if (!account) {
+ return false;
+ }
+ return MailServices.accounts.allServers.some(
+ server => server.getCharValue("deferred_to_account") == account.key
+ );
+ }
+
+ get serverRequiresPasswordForBiff() {
+ return true;
+ }
+
+ /**
+ * type, attribute name, pref name
+ */
+ _downloadSettingsPrefs = [
+ ["Int", "ageLimitOfMsgsToDownload", "ageLimit"],
+ ["Bool", "downloadUnreadOnly"],
+ ["Bool", "downloadByDate"],
+ ];
+
+ get downloadSettings() {
+ if (!this._downloadSettings) {
+ this._downloadSettings = Cc[
+ "@mozilla.org/msgDatabase/downloadSettings;1"
+ ].createInstance(Ci.nsIMsgDownloadSettings);
+ for (let [type, attrName, prefName] of this._downloadSettingsPrefs) {
+ prefName = prefName || attrName;
+ this._downloadSettings[attrName] = this[`get${type}Value`](prefName);
+ }
+ }
+ return this._downloadSettings;
+ }
+
+ set downloadSettings(settings) {
+ this._downloadSettings = settings;
+ for (let [type, attrName, prefName] of this._downloadSettingsPrefs) {
+ prefName = prefName || attrName;
+ this[`set${type}Value`](prefName, settings[attrName]);
+ }
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_NONE = 0;
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ let level = this.getIntValue("offline_support_level");
+ return level == OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? OFFLINE_SUPPORT_LEVEL_NONE
+ : level;
+ }
+
+ set offlineSupportLevel(value) {
+ this.setIntValue("offline_support_level", value);
+ }
+
+ get filterScope() {
+ return Ci.nsMsgSearchScope.offlineMailFilter;
+ }
+
+ get searchScope() {
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+
+ get passwordPromptRequired() {
+ if (!this.serverRequiresPasswordForBiff) {
+ // If the password is not even required for biff we don't need to check
+ // any further.
+ return false;
+ }
+ if (!this.password) {
+ // If the password is empty, check to see if it is stored.
+ this.password = this._getPasswordWithoutUI();
+ }
+ if (this.password) {
+ return false;
+ }
+ return this.authMethod != Ci.nsMsgAuthMethod.OAuth2;
+ }
+
+ getCharValue(prefName) {
+ try {
+ return this._prefs.getCharPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getCharPref(prefName, "");
+ }
+ }
+
+ setCharValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getCharPref(prefName, "");
+ if (!value || value == defaultValue) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setCharPref(prefName, value);
+ }
+ }
+
+ getUnicharValue(prefName) {
+ try {
+ return this._prefs.getStringPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getStringPref(prefName, "");
+ }
+ }
+
+ setUnicharValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getStringPref(prefName, "");
+ if (!value || value == defaultValue) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setStringPref(prefName, value);
+ }
+ }
+
+ getIntValue(prefName) {
+ try {
+ return this._prefs.getIntPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getIntPref(prefName, 0);
+ }
+ }
+
+ setIntValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getIntPref(prefName, value - 1);
+ if (defaultValue == value) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setIntPref(prefName, value);
+ }
+ }
+
+ getBoolValue(prefName) {
+ try {
+ return this._prefs.getBoolPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getBoolPref(prefName, false);
+ }
+ }
+
+ setBoolValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getBoolPref(prefName, !value);
+ if (defaultValue == value) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setBoolPref(prefName, value);
+ }
+ }
+
+ getFileValue(relPrefName, absPrefName) {
+ try {
+ let file = this._prefs.getComplexValue(
+ relPrefName,
+ Ci.nsIRelativeFilePref
+ ).file;
+ file.normalize();
+ return file;
+ } catch (e) {
+ try {
+ let file = this._prefs.getComplexValue(absPrefName, Ci.nsIFile);
+ this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ return file;
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+
+ setFileValue(relPrefName, absPrefName, file) {
+ this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ this._prefs.setComplexValue(absPrefName, Ci.nsIFile, file);
+ }
+
+ onUserOrHostNameChanged(oldValue, newValue, hostnameChanged) {
+ migrateServerUris(
+ this.localStoreType,
+ hostnameChanged ? oldValue : this.hostName,
+ hostnameChanged ? this.username : oldValue,
+ this.hostName,
+ this.username
+ );
+ this._spamSettings = null;
+
+ // Clear the clientid because the user or host have changed.
+ this.clientid = "";
+
+ let atIndex = newValue.indexOf("@");
+ if (!this.prettyName || (!hostnameChanged && atIndex != -1)) {
+ // If new username contains @ then better not update the pretty name.
+ return;
+ }
+
+ atIndex = this.prettyName.indexOf("@");
+ if (
+ !hostnameChanged &&
+ atIndex != -1 &&
+ oldValue == this.prettyName.slice(0, atIndex)
+ ) {
+ // If username changed and the pretty name has the old username before @,
+ // update to the new username.
+ this.prettyName = newValue + this.prettyName.slice(atIndex);
+ } else if (
+ hostnameChanged &&
+ oldValue == this.prettyName.slice(atIndex + 1)
+ ) {
+ // If hostname changed and the pretty name has the old hostname after @,
+ // update to the new hostname.
+ this.prettyName = this.prettyName.slice(0, atIndex + 1) + newValue;
+ } else {
+ // Set the `name` pref anyway, to make tests happy.
+ // eslint-disable-next-line no-self-assign
+ this.prettyName = this.prettyName;
+ }
+ }
+
+ /**
+ * Try to get the password from nsILoginManager.
+ *
+ * @returns {string}
+ */
+ _getPasswordWithoutUI() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ return login.password;
+ }
+ }
+ return null;
+ }
+
+ getPasswordWithUI(promptMessage, promptTitle) {
+ let password = this._getPasswordWithoutUI();
+ if (password) {
+ this.password = password;
+ return this.password;
+ }
+ let outUsername = {};
+ let outPassword = {};
+ let ok;
+ let authPrompt;
+ try {
+ // This prompt has a checkbox for saving password.
+ authPrompt = Cc["@mozilla.org/messenger/msgAuthPrompt;1"].getService(
+ Ci.nsIAuthPrompt
+ );
+ } catch (e) {
+ // Often happens in tests. This prompt has no checkbox for saving password.
+ authPrompt = Services.ww.getNewAuthPrompter(null);
+ }
+ if (this.username) {
+ ok = authPrompt.promptPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outPassword
+ );
+ } else {
+ ok = authPrompt.promptUsernameAndPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outUsername,
+ outPassword
+ );
+ }
+ if (ok) {
+ if (outUsername.value) {
+ this.username = outUsername.value;
+ }
+ this.password = outPassword.value;
+ } else {
+ throw Components.Exception("Password dialog canceled", Cr.NS_ERROR_ABORT);
+ }
+ return this.password;
+ }
+
+ forgetPassword() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ Services.logins.removeLogin(login);
+ }
+ }
+ this.password = "";
+ }
+
+ forgetSessionPassword() {
+ this.password = "";
+ }
+
+ closeCachedConnections() {}
+
+ shutdown() {
+ this.closeCachedConnections();
+
+ if (this._filterList) {
+ this._filterList.logStream = null;
+ this._filterList = null;
+ }
+ if (this._spamSettings) {
+ this._spamSettings.logStream = null;
+ this._spamSettings = null;
+ }
+ }
+
+ getFilterList(msgWindow) {
+ if (!this._filterList) {
+ if (!this.rootFolder.filePath.path) {
+ // Happens in tests.
+ return null;
+ }
+ let filterFile = this.rootFolder.filePath.clone();
+ filterFile.append("msgFilterRules.dat");
+ try {
+ this._filterList = MailServices.filters.OpenFilterList(
+ filterFile,
+ this.rootFolder,
+ msgWindow
+ );
+ } catch (e) {
+ console.error(e);
+ const NS_ERROR_FILE_FS_CORRUPTED = 0x80520016;
+ if (e.result == NS_ERROR_FILE_FS_CORRUPTED && filterFile.exists()) {
+ // OpenFilterList will create a new one next time.
+ filterFile.renameTo(filterFile.parent, "msgFilterRules.dat.orig");
+ }
+ }
+ }
+ return this._filterList;
+ }
+
+ setFilterList(value) {
+ this._filterList = value;
+ }
+
+ getEditableFilterList(msgWindow) {
+ if (!this._editableFilterList) {
+ return this.getFilterList(msgWindow);
+ }
+ return this._editableFilterList;
+ }
+
+ setEditableFilterList(value) {
+ this._editableFilterList = value;
+ }
+
+ setDefaultLocalPath(value) {
+ this.protocolInfo.setDefaultLocalPath(value);
+ }
+
+ getNewMessages(folder, msgWindow, urlListener) {
+ folder.getNewMessages(msgWindow, urlListener);
+ }
+
+ writeToFolderCache(folderCache) {
+ this.rootFolder.writeToFolderCache(folderCache, true);
+ }
+
+ clearAllValues() {
+ for (let prefName of this._prefs.getChildList("")) {
+ this._prefs.clearUserPref(prefName);
+ }
+ }
+
+ removeFiles() {
+ if (this.getCharValue("deferred_to_account") || this.isDeferredTo) {
+ throw Components.Exception(
+ "Should not remove files for a deferred account",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ this.localPath.remove(true);
+ }
+
+ getMsgFolderFromURI(folder, uri) {
+ try {
+ return this.rootMsgFolder.getChildWithURI(uri, true, true) || folder;
+ } catch (e) {
+ return folder;
+ }
+ }
+
+ isNewHdrDuplicate(newHdr) {
+ // If the message has been partially downloaded, the message should not
+ // be considered a duplicated message. See bug 714090.
+ if (newHdr.flags & Ci.nsMsgMessageFlags.Partial) {
+ return false;
+ }
+
+ if (!newHdr.subject || !newHdr.messageId) {
+ return false;
+ }
+
+ let key = `${newHdr.messageId}${newHdr.subject}`;
+ if (this._knownHdrMap.get(key)) {
+ return true;
+ }
+
+ this._knownHdrMap.set(key, ++this._hdrIndex);
+
+ const MAX_SIZE = 500;
+ if (this._knownHdrMap.size > MAX_SIZE) {
+ // Release the oldest half of downloaded hdrs.
+ for (let [k, v] of this._knownHdrMap) {
+ if (v < this._hdrIndex - MAX_SIZE / 2) {
+ this._knownHdrMap.delete(k);
+ } else if (this._knownHdrMap.size <= MAX_SIZE / 2) {
+ break;
+ }
+ }
+ }
+ return false;
+ }
+
+ equals(server) {
+ return this.key == server.key;
+ }
+
+ _configureTemporaryReturnReceiptsFilter(filterList) {
+ let identity = MailServices.accounts.getFirstIdentityForServer(this);
+ if (!identity) {
+ return;
+ }
+ let incorp = Ci.nsIMsgMdnGenerator.eIncorporateInbox;
+ if (identity.getBoolAttribute("use_custom_prefs")) {
+ incorp = this.getIntValue("incorporate_return_receipt");
+ } else {
+ incorp = Services.prefs.getIntPref("mail.incorporate.return_receipt");
+ }
+
+ let enable = incorp == Ci.nsIMsgMdnGenerator.eIncorporateSent;
+
+ const FILTER_NAME = "mozilla-temporary-internal-MDN-receipt-filter";
+ let filter = filterList.getFilterNamed(FILTER_NAME);
+
+ if (filter) {
+ filter.enabled = enable;
+ return;
+ } else if (!enable || !identity.fccFolder) {
+ return;
+ }
+
+ filter = filterList.createFilter(FILTER_NAME);
+ if (!filter) {
+ return;
+ }
+
+ filter.enabled = true;
+ filter.temporary = true;
+
+ let term = filter.createTerm();
+ let value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ value.str = "multipart/report";
+ term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = true;
+ term.arbitraryHeader = "Content-Type";
+ term.value = value;
+ filter.appendTerm(term);
+
+ term = filter.createTerm();
+ value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ value.str = "disposition-notification";
+ term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = true;
+ term.arbitraryHeader = "Content-Type";
+ term.value = value;
+ filter.appendTerm(term);
+
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = identity.fccFolder;
+ filter.appendAction(action);
+ filterList.insertFilterAt(0, filter);
+ }
+
+ _configureTemporaryServerSpamFilters(filterList) {
+ let spamSettings = this.spamSettings;
+ if (!spamSettings.useServerFilter) {
+ return;
+ }
+ let serverFilterName = spamSettings.serverFilterName;
+ let serverFilterTrustFlags = spamSettings.serverFilterTrustFlags;
+ if (!serverFilterName || !serverFilterName) {
+ return;
+ }
+
+ // Check if filters have been setup already.
+ let yesFilterName = `${serverFilterName}Yes`;
+ let noFilterName = `${serverFilterName}No`;
+ let filter = filterList.getFilterNamed(yesFilterName);
+ if (!filter) {
+ filter = filterList.getFilterNamed(noFilterName);
+ }
+ if (filter) {
+ return;
+ }
+
+ let serverFilterList = MailServices.filters.OpenFilterList(
+ spamSettings.serverFilterFile,
+ null,
+ null
+ );
+ filter = serverFilterList.getFilterNamed(yesFilterName);
+ if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_POSITIVES) {
+ filter.temporary = true;
+ // Check if we're supposed to move junk mail to junk folder; if so, add
+ // filter action to do so.
+ let searchTerms = filter.searchTerms;
+ if (searchTerms.length) {
+ searchTerms[0].beginsGrouping = true;
+ searchTerms.at(-1).endsGrouping = true;
+ }
+
+ // Create a new term, checking if the user set junk status. The term will
+ // search for junkscoreorigin != "user".
+ let term = filter.createTerm();
+ term.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin;
+ term.op = Ci.nsMsgSearchOp.Isnt;
+ term.booleanAnd = true;
+ let value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin;
+ value.str = "user";
+ term.value = value;
+ filter.appendTerm(term);
+
+ if (spamSettings.moveOnSpam) {
+ let spamFolderURI = spamSettings.spamFolderURI;
+ if (spamFolderURI) {
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = spamFolderURI;
+ filter.appendAction(action);
+ }
+ }
+
+ if (spamSettings.markAsReadOnSpam) {
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(action);
+ }
+ filterList.insertFilterAt(0, filter);
+ }
+
+ filter = serverFilterList.getFilterNamed(noFilterName);
+ if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_NEGATIVES) {
+ filter.temporary = true;
+ filterList.insertFilterAt(0, filter);
+ }
+ }
+
+ configureTemporaryFilters(filterList) {
+ this._configureTemporaryReturnReceiptsFilter(filterList);
+ this._configureTemporaryServerSpamFilters(filterList);
+ }
+
+ clearTemporaryReturnReceiptsFilter() {
+ if (!this._filterList) {
+ return;
+ }
+ let filter = this._filterList.getFilterNamed(
+ "mozilla-temporary-internal-MDN-receipt-filter"
+ );
+ if (filter) {
+ this._filterList.removeFilter(filter);
+ }
+ }
+
+ getForcePropertyEmpty(name) {
+ return this.getCharValue(`${name}.empty`) == "true";
+ }
+
+ setForcePropertyEmpty(name, value) {
+ return this.setCharValue(`${name}.empty`, value ? "true" : "");
+ }
+
+ performExpand(msgWindow) {}
+
+ get wrappedJSObject() {
+ return this;
+ }
+
+ _passwordPromise = null;
+
+ /**
+ * Show a password prompt. If a prompt is currently shown, just wait for it.
+ *
+ * @param {string} message - The text inside the prompt.
+ * @param {string} title - The title of the prompt.
+ */
+ async getPasswordWithUIAsync(message, title) {
+ if (this._passwordPromise) {
+ await this._passwordPromise;
+ return this.password;
+ }
+ let deferred = {};
+ this._passwordPromise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ try {
+ this.getPasswordWithUI(message, title);
+ } catch (e) {
+ deferred.reject(e);
+ throw e;
+ } finally {
+ this._passwordPromise = null;
+ }
+ deferred.resolve();
+ return this.password;
+ }
+}
diff --git a/comm/mailnews/base/src/MsgKeySet.jsm b/comm/mailnews/base/src/MsgKeySet.jsm
new file mode 100644
index 0000000000..bbbd580ba9
--- /dev/null
+++ b/comm/mailnews/base/src/MsgKeySet.jsm
@@ -0,0 +1,132 @@
+/* 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 = ["MsgKeySet"];
+
+/**
+ * A structure to represent a set of articles. This is usually for lines from
+ * the newsrc, which have article lists like
+ *
+ * 1-29627,29635,29658,32861-32863
+ *
+ * so the data has these properties:
+ *
+ * - strictly increasing
+ * - large subsequences of monotonically increasing ranges
+ * - gaps in the set are usually small, but not always
+ * - consecutive ranges tend to be large
+ */
+class MsgKeySet {
+ /**
+ * @param {string} [str] - The raw string to represent a set of articles.
+ */
+ constructor(str) {
+ // An array of tuples, each tuple contains the start and end value of a sub
+ // range.
+ // @type {Array<[number, number]>}
+ this._ranges = str
+ ? str.split(",").map(part => {
+ let [start, end] = part.split("-");
+ return [+start, +end || +start];
+ })
+ : [];
+ }
+
+ /**
+ * Add a value to the set.
+ *
+ * @param {number} value - The value to add.
+ */
+ add(value) {
+ this.addRange(value, value);
+ }
+
+ /**
+ * Add a range to the set.
+ *
+ * @param {number} low - The smallest value of the range.
+ * @param {number} high - The largest value of the range.
+ */
+ addRange(low, high) {
+ let index = 0;
+ for (let [start] of this._ranges) {
+ if (start > low) {
+ break;
+ }
+ index++;
+ }
+ this._ranges.splice(index, 0, [low, high]);
+ this._rebuild();
+ }
+
+ /**
+ * Check if a value is in the set.
+ *
+ * @param {number} value - The value to check.
+ * @returns {boolean}
+ */
+ has(value) {
+ return this._ranges.some(([start, end]) =>
+ end ? start <= value && value <= end : start == value
+ );
+ }
+
+ /**
+ * Get the last range that is in the input range, but not in the key set.
+ *
+ * @param {number} low - The smallest value of the input range.
+ * @param {number} high - The largest value of the input range.
+ * @returns {number[]} - Array of lenght two with [low, high].
+ */
+ getLastMissingRange(low, high) {
+ let length = this._ranges.length;
+ for (let i = length - 1; i >= 0; i--) {
+ let [start, end] = this._ranges[i];
+ if (end < high) {
+ return [Math.max(low, end + 1), high];
+ } else if (low < start && high > start) {
+ high = start - 1;
+ } else {
+ return [];
+ }
+ }
+ return [low, high];
+ }
+
+ /**
+ * Get the string representation of the key set.
+ *
+ * @returns {string}
+ */
+ toString() {
+ return this._ranges
+ .map(([start, end]) => (start == end ? start : `${start}-${end}`))
+ .join(",");
+ }
+
+ /**
+ * Sub ranges may become overlapped after some operations. This method merges
+ * them if needed.
+ */
+ _rebuild() {
+ if (this._ranges.length < 2) {
+ return;
+ }
+ let newRanges = [];
+ let [cursorStart, cursorEnd] = this._ranges[0];
+ for (let [start, end] of this._ranges.slice(1)) {
+ if (cursorEnd < start - 1) {
+ // No overlap between the two ranges.
+ newRanges.push([cursorStart, cursorEnd]);
+ cursorStart = start;
+ cursorEnd = end;
+ } else {
+ // Overlapped, merge them.
+ cursorEnd = end;
+ }
+ }
+ newRanges.push([cursorStart, cursorEnd]);
+ this._ranges = newRanges;
+ }
+}
diff --git a/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs
new file mode 100644
index 0000000000..7c9088e12e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs
@@ -0,0 +1,53 @@
+/* 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/. */
+
+/**
+ * @see {nsIMsgProtocolInfo}
+ */
+export class MsgProtocolInfo {
+ get defaultLocalPath() {
+ let file = this._getFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF);
+ if (!file) {
+ file = Services.dirsvc.get(this.DIR_SERVICE_PROP, Ci.nsIFile);
+ this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, file);
+ }
+ if (!file.exists()) {
+ file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o775);
+ }
+ file.normalize();
+ return file;
+ }
+
+ set defaultLocalPath(value) {
+ this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, value);
+ }
+
+ _getFileValue(relPrefName, absPrefName) {
+ try {
+ return Services.prefs.getComplexValue(relPrefName, Ci.nsIRelativeFilePref)
+ .file;
+ } catch (e) {
+ try {
+ let file = Services.prefs.getComplexValue(absPrefName, Ci.nsIFile);
+ Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ return file;
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+
+ _setFileValue(relPrefName, absPrefName, file) {
+ Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ Services.prefs.setComplexValue(absPrefName, Ci.nsIFile, file);
+ }
+}
diff --git a/comm/mailnews/base/src/OAuth2.jsm b/comm/mailnews/base/src/OAuth2.jsm
new file mode 100644
index 0000000000..c5148d41a7
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2.jsm
@@ -0,0 +1,364 @@
+/* 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/. */
+
+/**
+ * Provides OAuth 2.0 authentication.
+ *
+ * @see RFC 6749
+ */
+var EXPORTED_SYMBOLS = ["OAuth2"];
+
+var { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+// Only allow one connecting window per endpoint.
+var gConnecting = {};
+
+/**
+ * Constructor for the OAuth2 object.
+ *
+ * @class
+ * @param {?string} scope - The scope as specified by RFC 6749 Section 3.3.
+ * Will not be included in the requests if falsy.
+ * @param {object} issuerDetails
+ * @param {string} issuerDetails.authorizationEndpoint - The authorization
+ * endpoint as defined by RFC 6749 Section 3.1.
+ * @param {string} issuerDetails.clientId - The client_id as specified by RFC
+ * 6749 Section 2.3.1.
+ * @param {string} issuerDetails.clientSecret - The client_secret as specified
+ * in RFC 6749 section 2.3.1. Will not be included in the requests if null.
+ * @param {boolean} issuerDetails.usePKCE - Whether to use PKCE as specified
+ * in RFC 7636 during the oauth registration process
+ * @param {string} issuerDetails.redirectionEndpoint - The redirect_uri as
+ * specified by RFC 6749 section 3.1.2.
+ * @param {string} issuerDetails.tokenEndpoint - The token endpoint as defined
+ * by RFC 6749 Section 3.2.
+ */
+function OAuth2(scope, issuerDetails) {
+ this.scope = scope;
+ this.authorizationEndpoint = issuerDetails.authorizationEndpoint;
+ this.clientId = issuerDetails.clientId;
+ this.consumerSecret = issuerDetails.clientSecret || null;
+ this.usePKCE = issuerDetails.usePKCE;
+ this.redirectionEndpoint =
+ issuerDetails.redirectionEndpoint || "http://localhost";
+ this.tokenEndpoint = issuerDetails.tokenEndpoint;
+
+ this.extraAuthParams = [];
+
+ this.log = console.createInstance({
+ prefix: "mailnews.oauth",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.oauth.loglevel",
+ });
+}
+
+OAuth2.prototype = {
+ clientId: null,
+ consumerSecret: null,
+ requestWindowURI: "chrome://messenger/content/browserRequest.xhtml",
+ requestWindowFeatures: "chrome,centerscreen,width=980,height=750",
+ requestWindowTitle: "",
+ scope: null,
+ usePKCE: false,
+ codeChallenge: null,
+
+ accessToken: null,
+ refreshToken: null,
+ tokenExpires: 0,
+
+ connect(aSuccess, aFailure, aWithUI, aRefresh) {
+ this.connectSuccessCallback = aSuccess;
+ this.connectFailureCallback = aFailure;
+
+ if (this.accessToken && !this.tokenExpired && !aRefresh) {
+ aSuccess();
+ } else if (this.refreshToken) {
+ this.requestAccessToken(this.refreshToken, true);
+ } else {
+ if (!aWithUI) {
+ aFailure('{ "error": "auth_noui" }');
+ return;
+ }
+ if (gConnecting[this.authorizationEndpoint]) {
+ aFailure("Window already open");
+ return;
+ }
+ this.requestAuthorization();
+ }
+ },
+
+ /**
+ * True if the token has expired, or will expire within the grace time.
+ */
+ get tokenExpired() {
+ // 30 seconds to allow for network inefficiency, clock drift, etc.
+ const OAUTH_GRACE_TIME_MS = 30 * 1000;
+ return this.tokenExpires - OAUTH_GRACE_TIME_MS < Date.now();
+ },
+
+ requestAuthorization() {
+ let params = new URLSearchParams({
+ response_type: "code",
+ client_id: this.clientId,
+ redirect_uri: this.redirectionEndpoint,
+ });
+
+ // The scope is optional.
+ if (this.scope) {
+ params.append("scope", this.scope);
+ }
+
+ // See rfc7636
+ if (this.usePKCE) {
+ // Convert base64 to base64url (rfc4648#section-5)
+ const to_b64url = b =>
+ b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ params.append("code_challenge_method", "S256");
+
+ // rfc7636#section-4.1
+ // code_verifier = high-entropy cryptographic random STRING ... with a minimum
+ // length of 43 characters and a maximum length of 128 characters.
+ const code_verifier = to_b64url(
+ btoa(CryptoUtils.generateRandomBytesLegacy(64))
+ );
+ this.codeVerifier = code_verifier;
+
+ // rfc7636#section-4.2
+ // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
+ const code_challenge = to_b64url(CryptoUtils.sha256Base64(code_verifier));
+ params.append("code_challenge", code_challenge);
+ }
+
+ for (let [name, value] of this.extraAuthParams) {
+ params.append(name, value);
+ }
+
+ let authEndpointURI = this.authorizationEndpoint + "?" + params.toString();
+ this.log.info(
+ "Interacting with the resource owner to obtain an authorization grant " +
+ "from the authorization endpoint: " +
+ authEndpointURI
+ );
+
+ this._browserRequest = {
+ account: this,
+ url: authEndpointURI,
+ _active: true,
+ iconURI: "",
+ cancelled() {
+ if (!this._active) {
+ return;
+ }
+
+ this.account.finishAuthorizationRequest();
+ this.account.onAuthorizationFailed(
+ Cr.NS_ERROR_ABORT,
+ '{ "error": "cancelled"}'
+ );
+ },
+
+ loaded(aWindow, aWebProgress) {
+ if (!this._active) {
+ return;
+ }
+
+ this._listener = {
+ window: aWindow,
+ webProgress: aWebProgress,
+ _parent: this.account,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ _cleanUp() {
+ this.webProgress.removeProgressListener(this);
+ this.window.close();
+ delete this.window;
+ },
+
+ _checkForRedirect(url) {
+ if (!url.startsWith(this._parent.redirectionEndpoint)) {
+ return;
+ }
+
+ this._parent.finishAuthorizationRequest();
+ this._parent.onAuthorizationReceived(url);
+ },
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ const wpl = Ci.nsIWebProgressListener;
+ if (aStateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK)) {
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this._checkForRedirect(channel.URI.spec);
+ }
+ },
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ this._checkForRedirect(aLocation.spec);
+ },
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+ };
+ aWebProgress.addProgressListener(
+ this._listener,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ aWindow.document.title = this.account.requestWindowTitle;
+ },
+ };
+
+ const windowPrivacy = Services.prefs.getBoolPref(
+ "mailnews.oauth.usePrivateBrowser",
+ false
+ )
+ ? "private"
+ : "non-private";
+ const windowFeatures = `${this.requestWindowFeatures},${windowPrivacy}`;
+
+ this.wrappedJSObject = this._browserRequest;
+ gConnecting[this.authorizationEndpoint] = true;
+ Services.ww.openWindow(
+ null,
+ this.requestWindowURI,
+ null,
+ windowFeatures,
+ this
+ );
+ },
+ finishAuthorizationRequest() {
+ gConnecting[this.authorizationEndpoint] = false;
+ if (!("_browserRequest" in this)) {
+ return;
+ }
+
+ this._browserRequest._active = false;
+ if ("_listener" in this._browserRequest) {
+ this._browserRequest._listener._cleanUp();
+ }
+ delete this._browserRequest;
+ },
+
+ /**
+ * @param {string} aURL - Redirection URI with additional parameters.
+ */
+ onAuthorizationReceived(aURL) {
+ this.log.info("OAuth2 authorization response received: url=" + aURL);
+ const url = new URL(aURL);
+ if (url.searchParams.has("code")) {
+ // @see RFC 6749 section 4.1.2: Authorization Response
+ this.requestAccessToken(url.searchParams.get("code"), false);
+ } else {
+ // @see RFC 6749 section 4.1.2.1: Error Response
+ if (url.searchParams.has("error")) {
+ let error = url.searchParams.get("error");
+ let errorDescription = url.searchParams.get("error_description") || "";
+ if (error == "invalid_scope") {
+ errorDescription += ` Invalid scope: ${this.scope}.`;
+ }
+ if (url.searchParams.has("error_uri")) {
+ errorDescription += ` See ${url.searchParams.get("error_uri")}.`;
+ }
+ this.log.error(`Authorization error [${error}]: ${errorDescription}`);
+ }
+ this.onAuthorizationFailed(null, aURL);
+ }
+ },
+
+ onAuthorizationFailed(aError, aData) {
+ this.connectFailureCallback(aData);
+ },
+
+ /**
+ * Request a new access token, or refresh an existing one.
+ *
+ * @param {string} aCode - The token issued to the client.
+ * @param {boolean} aRefresh - Whether it's a refresh of a token or not.
+ */
+ requestAccessToken(aCode, aRefresh) {
+ // @see RFC 6749 section 4.1.3. Access Token Request
+ // @see RFC 6749 section 6. Refreshing an Access Token
+
+ let data = new URLSearchParams();
+ data.append("client_id", this.clientId);
+ if (this.consumerSecret !== null) {
+ // Section 2.3.1. of RFC 6749 states that empty secrets MAY be omitted
+ // by the client. This OAuth implementation delegates this decision to
+ // the caller: If the secret is null, it will be omitted.
+ data.append("client_secret", this.consumerSecret);
+ }
+
+ if (aRefresh) {
+ this.log.info(
+ `Making a refresh request to the token endpoint: ${this.tokenEndpoint}`
+ );
+ data.append("grant_type", "refresh_token");
+ data.append("refresh_token", aCode);
+ } else {
+ this.log.info(
+ `Making access token request to the token endpoint: ${this.tokenEndpoint}`
+ );
+ data.append("grant_type", "authorization_code");
+ data.append("code", aCode);
+ data.append("redirect_uri", this.redirectionEndpoint);
+ if (this.usePKCE) {
+ data.append("code_verifier", this.codeVerifier);
+ }
+ }
+
+ fetch(this.tokenEndpoint, {
+ method: "POST",
+ cache: "no-cache",
+ body: data,
+ })
+ .then(response => response.json())
+ .then(result => {
+ let resultStr = JSON.stringify(result, null, 2);
+ if ("error" in result) {
+ // RFC 6749 section 5.2. Error Response
+ let err = result.error;
+ if ("error_description" in result) {
+ err += "; " + result.error_description;
+ }
+ if ("error_uri" in result) {
+ err += "; " + result.error_uri;
+ }
+ this.log.warn(`Error response from the authorization server: ${err}`);
+ this.log.info(`Error response details: ${resultStr}`);
+
+ // Typically in production this would be {"error": "invalid_grant"}.
+ // That is, the token expired or was revoked (user changed password?).
+ // Reset the tokens we have and call success so that the auth flow
+ // will be re-triggered.
+ this.accessToken = null;
+ this.refreshToken = null;
+ this.connectSuccessCallback();
+ return;
+ }
+
+ // RFC 6749 section 5.1. Successful Response
+ this.log.info(
+ `Successful response from the authorization server: ${resultStr}`
+ );
+ this.accessToken = result.access_token;
+ if ("refresh_token" in result) {
+ this.refreshToken = result.refresh_token;
+ }
+ if ("expires_in" in result) {
+ this.tokenExpires = new Date().getTime() + result.expires_in * 1000;
+ } else {
+ this.tokenExpires = Number.MAX_VALUE;
+ }
+ this.connectSuccessCallback();
+ })
+ .catch(err => {
+ this.log.info(`Connection to authorization server failed: ${err}`);
+ this.connectFailureCallback(err);
+ });
+ },
+};
diff --git a/comm/mailnews/base/src/OAuth2Module.jsm b/comm/mailnews/base/src/OAuth2Module.jsm
new file mode 100644
index 0000000000..79826779c4
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2Module.jsm
@@ -0,0 +1,203 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["OAuth2Module"];
+
+var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm");
+var { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+
+/**
+ * OAuth2Module is the glue layer that gives XPCOM access to an OAuth2
+ * bearer token it can use to authenticate in SASL steps.
+ * It also takes care of persising the refreshToken for later usage.
+ *
+ * @implements {msgIOAuth2Module}
+ */
+function OAuth2Module() {}
+OAuth2Module.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]),
+
+ initFromSmtp(aServer) {
+ return this._initPrefs(
+ "mail.smtpserver." + aServer.key + ".",
+ aServer.username,
+ aServer.hostname
+ );
+ },
+ initFromMail(aServer) {
+ return this._initPrefs(
+ "mail.server." + aServer.key + ".",
+ aServer.username,
+ aServer.hostName
+ );
+ },
+ initFromABDirectory(aDirectory, aHostname) {
+ this._initPrefs(
+ aDirectory.dirPrefId + ".",
+ aDirectory.getStringValue("carddav.username", "") || aDirectory.UID,
+ aHostname
+ );
+ },
+ _initPrefs(root, aUsername, aHostname) {
+ let issuer = Services.prefs.getStringPref(root + "oauth2.issuer", null);
+ let scope = Services.prefs.getStringPref(root + "oauth2.scope", null);
+
+ let details = OAuth2Providers.getHostnameDetails(aHostname);
+ if (
+ details &&
+ (details[0] != issuer ||
+ !scope?.split(" ").every(s => details[1].split(" ").includes(s)))
+ ) {
+ // Found in the list of hardcoded providers. Use the hardcoded values.
+ // But only if what we had wasn't a narrower scope of current
+ // defaults. Updating scope would cause re-authorization.
+ [issuer, scope] = details;
+ // Store them for the future, can be useful once we support
+ // dynamic registration.
+ Services.prefs.setStringPref(root + "oauth2.issuer", issuer);
+ Services.prefs.setStringPref(root + "oauth2.scope", scope);
+ }
+ if (!issuer || !scope) {
+ // We need these properties for OAuth2 support.
+ return false;
+ }
+
+ // Find the app key we need for the OAuth2 string. Eventually, this should
+ // be using dynamic client registration, but there are no current
+ // implementations that we can test this with.
+ const issuerDetails = OAuth2Providers.getIssuerDetails(issuer);
+ if (!issuerDetails.clientId) {
+ return false;
+ }
+
+ // Username is needed to generate the XOAUTH2 string.
+ this._username = aUsername;
+ // loginOrigin is needed to save the refresh token in the password manager.
+ this._loginOrigin = "oauth://" + issuer;
+ // We use the scope to indicate realm when storing in the password manager.
+ this._scope = scope;
+
+ // Define the OAuth property and store it.
+ this._oauth = new OAuth2(scope, issuerDetails);
+
+ // Try hinting the username...
+ this._oauth.extraAuthParams = [["login_hint", aUsername]];
+
+ // Set the window title to something more useful than "Unnamed"
+ this._oauth.requestWindowTitle = Services.strings
+ .createBundle("chrome://messenger/locale/messenger.properties")
+ .formatStringFromName("oauth2WindowTitle", [aUsername, aHostname]);
+
+ // This stores the refresh token in the login manager.
+ Object.defineProperty(this._oauth, "refreshToken", {
+ get: () => this.refreshToken,
+ set: token => {
+ this.refreshToken = token;
+ },
+ });
+
+ return true;
+ },
+
+ get refreshToken() {
+ for (let login of Services.logins.findLogins(this._loginOrigin, null, "")) {
+ if (
+ login.username == this._username &&
+ (login.httpRealm == this._scope ||
+ login.httpRealm.split(" ").includes(this._scope))
+ ) {
+ return login.password;
+ }
+ }
+ return "";
+ },
+ set refreshToken(token) {
+ // Check if we already have a login with this username, and modify the
+ // password on that, if we do.
+ let logins = Services.logins.findLogins(
+ this._loginOrigin,
+ null,
+ this._scope
+ );
+ for (let login of logins) {
+ if (login.username == this._username) {
+ if (token) {
+ if (token != login.password) {
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ propBag.setProperty("password", token);
+ Services.logins.modifyLogin(login, propBag);
+ }
+ } else {
+ Services.logins.removeLogin(login);
+ }
+ return;
+ }
+ }
+
+ // Unless the token is null, we need to create and fill in a new login
+ if (token) {
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(
+ this._loginOrigin,
+ null,
+ this._scope,
+ this._username,
+ token,
+ "",
+ ""
+ );
+ Services.logins.addLogin(login);
+ }
+ },
+
+ connect(aWithUI, aListener) {
+ let oauth = this._oauth;
+ let promptlistener = {
+ onPromptStartAsync(callback) {
+ this.onPromptAuthAvailable(callback);
+ },
+
+ onPromptAuthAvailable: callback => {
+ oauth.connect(
+ () => {
+ aListener.onSuccess(
+ btoa(
+ `user=${this._username}\x01auth=Bearer ${oauth.accessToken}\x01\x01`
+ )
+ );
+ if (callback) {
+ callback.onAuthResult(true);
+ }
+ },
+ () => {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ if (callback) {
+ callback.onAuthResult(false);
+ }
+ },
+ aWithUI,
+ false
+ );
+ },
+ onPromptCanceled() {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ },
+ onPromptStart() {},
+ };
+
+ let asyncprompter = Cc[
+ "@mozilla.org/messenger/msgAsyncPrompter;1"
+ ].getService(Ci.nsIMsgAsyncPrompter);
+ let promptkey = this._loginOrigin + "/" + this._username;
+ asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener);
+ },
+};
diff --git a/comm/mailnews/base/src/OAuth2Providers.jsm b/comm/mailnews/base/src/OAuth2Providers.jsm
new file mode 100644
index 0000000000..86300b2ead
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2Providers.jsm
@@ -0,0 +1,259 @@
+/* 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/. */
+
+/**
+ * Details of supported OAuth2 Providers.
+ */
+var EXPORTED_SYMBOLS = ["OAuth2Providers"];
+
+// When we add a Google mail account, ask for address book and calendar scopes
+// as well. Then we can add an address book or calendar without asking again.
+//
+// Don't ask for all the scopes when adding an address book or calendar
+// independently of the mail set-up process. If a mail account already exists,
+// we already have a token, and if it doesn't the user is likely to be setting
+// up an address book/calendar without wanting mail.
+const GOOGLE_SCOPES =
+ "https://mail.google.com/ https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar";
+const FASTMAIL_SCOPES =
+ "https://www.fastmail.com/dev/protocol-imap https://www.fastmail.com/dev/protocol-pop https://www.fastmail.com/dev/protocol-smtp https://www.fastmail.com/dev/protocol-carddav https://www.fastmail.com/dev/protocol-caldav";
+const COMCAST_SCOPES = "https://email.comcast.net/ profile openid";
+
+/**
+ * Map of hostnames to [issuer, scope].
+ */
+var kHostnames = new Map([
+ ["imap.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["smtp.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["pop.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["imap.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["smtp.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["pop.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ [
+ "www.googleapis.com",
+ ["accounts.google.com", "https://www.googleapis.com/auth/carddav"],
+ ],
+
+ ["imap.mail.ru", ["o2.mail.ru", "mail.imap"]],
+ ["smtp.mail.ru", ["o2.mail.ru", "mail.imap"]],
+
+ ["imap.yandex.com", ["oauth.yandex.com", "mail:imap_full"]],
+ ["smtp.yandex.com", ["oauth.yandex.com", "mail:smtp"]],
+
+ ["imap.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+ ["pop.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+ ["smtp.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+
+ ["imap.aol.com", ["login.aol.com", "mail-w"]],
+ ["pop.aol.com", ["login.aol.com", "mail-w"]],
+ ["smtp.aol.com", ["login.aol.com", "mail-w"]],
+
+ [
+ "outlook.office365.com",
+ [
+ "login.microsoftonline.com",
+ "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access",
+ ],
+ ],
+ [
+ "smtp.office365.com",
+ [
+ "login.microsoftonline.com",
+ "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access",
+ ],
+ ],
+
+ ["imap.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ ["pop.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ ["smtp.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ [
+ "carddav.fastmail.com",
+ ["www.fastmail.com", "https://www.fastmail.com/dev/protocol-carddav"],
+ ],
+
+ ["imap.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+ ["pop.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+ ["smtp.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+
+ // For testing purposes.
+ ["mochi.test", ["mochi.test", "test_scope"]],
+]);
+
+/**
+ * Map of issuers to clientId, clientSecret, authorizationEndpoint, tokenEndpoint,
+ * and usePKCE (RFC7636).
+ * Issuer is a unique string for the organization that a Thunderbird account
+ * was registered at.
+ *
+ * For the moment these details are hard-coded, since dynamic client
+ * registration is not yet supported. Don't copy these values for your
+ * own application - register one for yourself! This code (and possibly even the
+ * registration itself) will disappear when this is switched to dynamic
+ * client registration.
+ */
+var kIssuers = new Map([
+ [
+ "accounts.google.com",
+ {
+ clientId:
+ "406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com",
+ clientSecret: "kSmqreRr0qwBWJgbf5Y-PjSU",
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
+ tokenEndpoint: "https://www.googleapis.com/oauth2/v3/token",
+ },
+ ],
+ [
+ "o2.mail.ru",
+ {
+ clientId: "thunderbird",
+ clientSecret: "I0dCAXrcaNFujaaY",
+ authorizationEndpoint: "https://o2.mail.ru/login",
+ tokenEndpoint: "https://o2.mail.ru/token",
+ },
+ ],
+ [
+ "oauth.yandex.com",
+ {
+ clientId: "2a00bba7374047a6ab79666485ffce31",
+ clientSecret: "3ded85b4ec574c2187a55dc49d361280",
+ authorizationEndpoint: "https://oauth.yandex.com/authorize",
+ tokenEndpoint: "https://oauth.yandex.com/token",
+ },
+ ],
+ [
+ "login.yahoo.com",
+ {
+ clientId:
+ "dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--",
+ clientSecret: "f2de6a30ae123cdbc258c15e0812799010d589cc",
+ authorizationEndpoint: "https://api.login.yahoo.com/oauth2/request_auth",
+ tokenEndpoint: "https://api.login.yahoo.com/oauth2/get_token",
+ },
+ ],
+ [
+ "login.aol.com",
+ {
+ clientId:
+ "dj0yJmk9OXRHc1FqZHRQYzVvJmQ9WVdrOU1UQnJOR0pvTjJrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD02NQ--",
+ clientSecret: "79c1c11991d148ddd02a919000d69879942fc278",
+ authorizationEndpoint: "https://api.login.aol.com/oauth2/request_auth",
+ tokenEndpoint: "https://api.login.aol.com/oauth2/get_token",
+ },
+ ],
+
+ [
+ "login.microsoftonline.com",
+ {
+ clientId: "9e5f94bc-e8a4-4e73-b8be-63364c29d753", // Application (client) ID
+ // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
+ authorizationEndpoint:
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+ tokenEndpoint:
+ "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ redirectionEndpoint: "https://localhost",
+ },
+ ],
+
+ [
+ "www.fastmail.com",
+ {
+ clientId: "35f141ae",
+ authorizationEndpoint: "https://api.fastmail.com/oauth/authorize",
+ tokenEndpoint: "https://api.fastmail.com/oauth/refresh",
+ usePKCE: true,
+ },
+ ],
+
+ [
+ "comcast.net",
+ {
+ clientId: "thunderbird-oauth",
+ clientSecret: "fc5d0a314549bb3d059e0cec751fa4bd40a9cc7b",
+ authorizationEndpoint: "https://oauth.xfinity.com/oauth/authorize",
+ tokenEndpoint: "https://oauth.xfinity.com/oauth/token",
+ usePKCE: true,
+ },
+ ],
+
+ // For testing purposes.
+ [
+ "mochi.test",
+ {
+ clientId: "test_client_id",
+ clientSecret: "test_secret",
+ authorizationEndpoint:
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs",
+ tokenEndpoint:
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/token.sjs",
+ // I don't know why, but tests refuse to work with a plain HTTP endpoint
+ // (the request is redirected to HTTPS, which we're not listening to).
+ // Just use an HTTPS endpoint.
+ redirectionEndpoint: "https://localhost",
+ },
+ ],
+]);
+
+/**
+ * OAuth2Providers: Methods to lookup OAuth2 parameters for supported OAuth2
+ * providers.
+ */
+var OAuth2Providers = {
+ /**
+ * Map a hostname to the relevant issuer and scope.
+ *
+ * @param {string} hostname - The hostname of the server. For example
+ * "imap.googlemail.com".
+ *
+ * @returns {Array} An array containing [issuer, scope] for the hostname, or
+ * undefined if not found.
+ * - issuer is a string representing the organization
+ * - scope is an OAuth2 parameter describing the required access level
+ */
+ getHostnameDetails(hostname) {
+ // During CardDAV SRV autodiscovery, rfc6764#section-6 says:
+ //
+ // * The client will need to make authenticated HTTP requests to
+ // the service. Typically, a "user identifier" is required for
+ // some form of user/password authentication. When a user
+ // identifier is required, clients MUST first use the "mailbox"
+ //
+ // However macOS Contacts does not do this and just uses the "localpart"
+ // instead. To work around this bug, during SRV autodiscovery Fastmail
+ // returns SRV records of the form '0 1 443 d[0-9]+.carddav.fastmail.com.'
+ // which encodes the internal domainid of the queried SRV domain in the
+ // sub-domain of the Target (rfc2782) of the SRV result. This can
+ // then be extracted from the Host header on each DAV request, the
+ // original domain looked up and attached to the "localpart" to create
+ // a full "mailbox", allowing autodiscovery to just work for usernames
+ // in any domain including self hosted domains.
+ //
+ // So for this hostname -> issuer/scope lookup to work, we need to
+ // look not just at the hostname, but also any sub-domains of this
+ // hostname.
+ while (hostname.includes(".")) {
+ let foundHost = kHostnames.get(hostname);
+ if (foundHost) {
+ return foundHost;
+ }
+ hostname = hostname.replace(/^[^.]*[.]/, "");
+ }
+ return undefined;
+ },
+
+ /**
+ * Map an issuer to OAuth2 account details.
+ *
+ * @param {string} issuer - The organization issuing OAuth2 parameters, e.g.
+ * "accounts.google.com".
+ *
+ * @returns {Array} An array containing [clientId, clientSecret, authorizationEndpoint, tokenEndpoint].
+ * clientId and clientSecret are strings representing the account registered
+ * for Thunderbird with the organization.
+ * authorizationEndpoint and tokenEndpoint are url strings representing
+ * endpoints to access OAuth2 authentication.
+ */
+ getIssuerDetails(issuer) {
+ return kIssuers.get(issuer);
+ },
+};
diff --git a/comm/mailnews/base/src/TemplateUtils.jsm b/comm/mailnews/base/src/TemplateUtils.jsm
new file mode 100644
index 0000000000..fca56fca67
--- /dev/null
+++ b/comm/mailnews/base/src/TemplateUtils.jsm
@@ -0,0 +1,90 @@
+/* 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 EXPORTED_SYMBOLS = ["PluralStringFormatter", "makeFriendlyDateAgo"];
+
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+function PluralStringFormatter(aBundleURI) {
+ this._bundle = Services.strings.createBundle(aBundleURI);
+}
+
+PluralStringFormatter.prototype = {
+ get(aStringName, aReplacements, aPluralCount) {
+ let str = this._bundle.GetStringFromName(aStringName);
+ if (aPluralCount !== undefined) {
+ str = PluralForm.get(aPluralCount, str);
+ }
+ if (aReplacements !== undefined) {
+ for (let i = 0; i < aReplacements.length; i++) {
+ str = str.replace("#" + (i + 1), aReplacements[i]);
+ }
+ }
+ return str;
+ },
+};
+
+var gTemplateUtilsStrings = new PluralStringFormatter(
+ "chrome://messenger/locale/templateUtils.properties"
+);
+
+const _dateFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+});
+const _dayMonthFormatter = new Services.intl.DateTimeFormat(undefined, {
+ month: "long",
+ day: "numeric",
+});
+const _timeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ timeStyle: "short",
+});
+const _weekdayFormatter = new Services.intl.DateTimeFormat(undefined, {
+ weekday: "long",
+});
+
+/**
+ * Helper function to generate a localized "friendly" representation of
+ * time relative to the present. If the time input is "today", it returns
+ * a string corresponding to just the time. If it's yesterday, it returns
+ * "yesterday" (localized). If it's in the last week, it returns the day
+ * of the week. If it's before that, it returns the date.
+ *
+ * @param {Date} time - The time (better be in the past!)
+ * @returns {string} A "human-friendly" representation of that time
+ * relative to now.
+ */
+function makeFriendlyDateAgo(time) {
+ // TODO: use Intl.RelativeTimeFormat instead.
+ // Figure out when today begins
+ let now = new Date();
+ let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+ // Get the end time to display
+ let end = time;
+
+ // Figure out if the end time is from today, yesterday,
+ // this week, etc.
+ let dateTime;
+ let kDayInMsecs = 24 * 60 * 60 * 1000;
+ let k6DaysInMsecs = 6 * kDayInMsecs;
+ if (end >= today) {
+ // activity finished after today started, show the time
+ dateTime = _timeFormatter.format(end);
+ } else if (today - end < kDayInMsecs) {
+ // activity finished after yesterday started, show yesterday
+ dateTime = gTemplateUtilsStrings.get("yesterday");
+ } else if (today - end < k6DaysInMsecs) {
+ // activity finished after last week started, show day of week
+ dateTime = _weekdayFormatter.format(end);
+ } else if (now.getFullYear() == end.getFullYear()) {
+ // activity must have been from some time ago.. show month/day
+ dateTime = _dayMonthFormatter.format(end);
+ } else {
+ // not this year, so show full date format
+ dateTime = _dateFormatter.format(end);
+ }
+ return dateTime;
+}
diff --git a/comm/mailnews/base/src/UrlListener.cpp b/comm/mailnews/base/src/UrlListener.cpp
new file mode 100644
index 0000000000..ce2ef4904a
--- /dev/null
+++ b/comm/mailnews/base/src/UrlListener.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "UrlListener.h"
+
+NS_IMPL_ISUPPORTS(UrlListener, nsIUrlListener)
+
+NS_IMETHODIMP UrlListener::OnStartRunningUrl(nsIURI* url) {
+ if (!mStartFn) {
+ return NS_OK;
+ }
+ return mStartFn(url);
+}
+
+NS_IMETHODIMP UrlListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ if (!mStopFn) {
+ return NS_OK;
+ }
+ return mStopFn(url, exitCode);
+}
diff --git a/comm/mailnews/base/src/UrlListener.h b/comm/mailnews/base/src/UrlListener.h
new file mode 100644
index 0000000000..22a0597854
--- /dev/null
+++ b/comm/mailnews/base/src/UrlListener.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef UrlListener_h__
+#define UrlListener_h__
+
+#include <functional> // For std::function.
+#include "nsIUrlListener.h"
+class nsIURI;
+
+/**
+ * UrlListener is a small nsIUrlListener implementation which allows
+ * callable objects (including lambdas) to be plugged in instead of deriving
+ * your own nsIUrlListener.
+ *
+ * The aim is to encourage more readable code by allowing the start/stop
+ * notifications of a long-running operation to be handled near to where the
+ * operation was initiated.
+ *
+ * A contrived example:
+ *
+ * void Kick() {
+ * UrlListener* listener = new UrlListener;
+ * listener->mStopFn = [](nsIURI* url, nsresult status) -> nsresult {
+ * // Note that we may get here waaaaaaay after Kick() has returned...
+ * printf("LongRunningOperation is finished.\n");
+ * return NS_OK;
+ * };
+ * thingService.startLongRunningOperation(listener);
+ * //...continue doing other stuff while operation is ongoing...
+ * }
+ *
+ * Traditionally, c-c code has tended to use multiple inheritance to add
+ * listener callbacks to the class of the object initiating the operation.
+ * This has a couple of undesirable side effects:
+ *
+ * 1) It separates out the onStopRunningUrl handling into some other
+ * part of the code, which makes the order of things much harder to follow.
+ * 2) Often the same onStopRunningUrl handler will be used for many different
+ * kinds of operations (see nsImapMailFolder::OnStopRunningUrl(), for
+ * example).
+ * 3) It exposes implementation details as part of the public interface
+ * e.g see all the listener types nsMsgDBFolder derives from to implement
+ * it's internals. That's all just confusing noise that shouldn't be seen
+ * from outside the class.
+ *
+ * Just as PromiseTestUtils.jsm brings the Javascript side up from callback
+ * hell to async lovelyness, this can be used to raise the C++ side from
+ * callback-somewhere-else-maybe-in-this-class-but-who-can-really-tell hell
+ * up to normal callback hell :-)
+ *
+ */
+class UrlListener : public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ UrlListener() {}
+ /**
+ * mStartFn and mStopFn are the OnStartRunning() and OnStopRunningUrl()
+ * handlers. It's fine for them to be null (often you'll only need mStopFn).
+ */
+ std::function<nsresult(nsIURI*)> mStartFn;
+ std::function<nsresult(nsIURI*, nsresult)> mStopFn;
+
+ protected:
+ virtual ~UrlListener() {}
+};
+
+#endif // UrlListener_h__
diff --git a/comm/mailnews/base/src/VirtualFolderWrapper.jsm b/comm/mailnews/base/src/VirtualFolderWrapper.jsm
new file mode 100644
index 0000000000..6fc86c8d1e
--- /dev/null
+++ b/comm/mailnews/base/src/VirtualFolderWrapper.jsm
@@ -0,0 +1,257 @@
+/* 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/. */
+
+/*
+ * Wrap everything about virtual folders.
+ */
+
+const EXPORTED_SYMBOLS = ["VirtualFolderHelper"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var VirtualFolderHelper = {
+ /**
+ * Create a new virtual folder (an actual nsIMsgFolder that did not previously
+ * exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper.
+ *
+ * If the call to addSubfolder fails (and therefore throws), we will NOT catch
+ * it.
+ *
+ * @param {string} aFolderName - The name of the new folder to create.
+ * @param {nsIMsgFolder} aParentFolder - The folder in which to create the
+ * search folder.
+ * @param {nsIMsgFolder[]} aSearchFolders A list of nsIMsgFolders that you
+ * want to use as the sources for the virtual folder OR a string that is
+ * the already '|' delimited list of folder URIs to use.
+ * @param {nsIMsgSearchTerms[]} aSearchTerms - The search terms to
+ * use for the virtual folder.
+ * @param {boolean} aOnlineSearch Should the search attempt to use the
+ * server's search capabilities when possible and appropriate?
+ * @returns {VirtualFolderWrapper} The VirtualFolderWrapper wrapping the
+ * newly created folder. You would probably only want this for its
+ * virtualFolder attribute which has the nsIMsgFolder we created.
+ * Be careful about accessing any of the other attributes, as they will
+ * bring its message database back to life.
+ */
+ createNewVirtualFolder(
+ aFolderName,
+ aParentFolder,
+ aSearchFolders,
+ aSearchTerms,
+ aOnlineSearch
+ ) {
+ let msgFolder = aParentFolder.addSubfolder(aFolderName);
+ msgFolder.prettyName = aFolderName;
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
+
+ let wrappedVirt = new VirtualFolderWrapper(msgFolder);
+ wrappedVirt.searchTerms = aSearchTerms;
+ wrappedVirt.searchFolders = aSearchFolders;
+ wrappedVirt.onlineSearch = aOnlineSearch;
+
+ let msgDatabase = msgFolder.msgDatabase;
+ msgDatabase.summaryValid = true;
+ msgDatabase.close(true);
+
+ aParentFolder.notifyFolderAdded(msgFolder);
+ MailServices.accounts.saveVirtualFolders();
+
+ return wrappedVirt;
+ },
+
+ /**
+ * Given an existing nsIMsgFolder that is a virtual folder, wrap it into a
+ * VirtualFolderWrapper.
+ *
+ * @param {nsIMsgFolder} aMsgFolder - The folder to use.
+ */
+ wrapVirtualFolder(aMsgFolder) {
+ return new VirtualFolderWrapper(aMsgFolder);
+ },
+};
+
+/**
+ * Abstracts dealing with the properties of a virtual folder that differentiate
+ * it from a non-virtual folder. A virtual folder is an odd duck. When
+ * holding an nsIMsgFolder that is a virtual folder, it is distinguished by
+ * the virtual flag and a number of properties that tell us the string
+ * representation of its search, the folders it searches over, and whether we
+ * use online searching or not.
+ * Virtual folders and their defining attributes are loaded from
+ * virtualFolders.dat (in the profile directory) by the account manager at
+ * startup, (re-)creating them if need be. It also saves them back to the
+ * file at shutdown. The most important thing the account manager does is to
+ * create VirtualFolderChangeListener instances that are registered with the
+ * message database service. This means that if one of the databases for the
+ * folders that the virtual folder includes is opened for some reason (for
+ * example, new messages are added to the folder because of a filter or they
+ * are delivered there), the virtual folder gets a chance to know about this
+ * and update the virtual folder's "cache" of information, such as the message
+ * counts or the presence of the message in the folder.
+ * The odd part is that a lot of the virtual folder logic also happens as a
+ * result of the nsMsgDBView subclasses being told the search query and the
+ * underlying folders. This makes for an odd collaboration of UI and backend
+ * logic.
+ *
+ * Justification for this class: Virtual folders aren't all that complex, but
+ * they are complex enough that we don't want to have the same code duplicated
+ * all over the place. We also don't want to have a loose assembly of global
+ * functions for working with them. So here we are.
+ *
+ * Important! Accessing any of our attributes results in the message database
+ * being loaded so that we can access the dBFolderInfo associated with the
+ * database. The message database is not automatically forgotten by the
+ * folder, which can lead to an (effective) memory leak. Please make sure
+ * that you are playing your part in not leaking memory by only using the
+ * wrapper when you have a serious need to access the database, and by
+ * forcing the folder to forget about the database when you are done by
+ * setting the database to null (unless you know with confidence someone else
+ * definitely wants the database around and will clean it up.)
+ *
+ * @param {nsIMsgFolder} aVirtualFolder - Folder to wrap.
+ */
+function VirtualFolderWrapper(aVirtualFolder) {
+ this.virtualFolder = aVirtualFolder;
+}
+VirtualFolderWrapper.prototype = {
+ /**
+ * @returns {nsIMsgFolders[]} The list of nsIMsgFolders that this virtual
+ * folder is a search over.
+ */
+ get searchFolders() {
+ return this.dbFolderInfo
+ .getCharProperty("searchFolderUri")
+ .split("|")
+ .sort() // Put folders in URI order so a parent is always before a child.
+ .map(uri => MailServices.folderLookup.getOrCreateFolderForURL(uri))
+ .filter(Boolean);
+ },
+ /**
+ * Set the search folders that back this virtual folder.
+ *
+ * @param {string|nsIMsgFolder[]} aFolders - Either a "|"-delimited string of
+ * folder URIs or a list of folders.
+ */
+ set searchFolders(aFolders) {
+ if (typeof aFolders == "string") {
+ this.dbFolderInfo.setCharProperty("searchFolderUri", aFolders);
+ } else {
+ let uris = aFolders.map(folder => folder.URI);
+ this.dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|"));
+ }
+ Services.obs.notifyObservers(this.virtualFolder, "search-folders-changed");
+ },
+
+ /**
+ * @returns {string} a "|"-delimited string containing the URIs of the folders
+ * that back this virtual folder.
+ */
+ get searchFolderURIs() {
+ return this.dbFolderInfo.getCharProperty("searchFolderUri");
+ },
+
+ /**
+ * @returns {nsIMsgSearchTerm[]} The list of search terms that define this
+ * virtual folder.
+ */
+ get searchTerms() {
+ return this.searchTermsSession.searchTerms;
+ },
+ /**
+ * @returns {nsIMsgFilterList} A newly created filter with the search terms
+ * loaded into it that define this virtual folder. The filter is apparently
+ * useful as an nsIMsgSearchSession stand-in to some code.
+ */
+ get searchTermsSession() {
+ // Temporary means it doesn't get exposed to the UI and doesn't get saved to
+ // disk. Which is good, because this is just a trick to parse the string
+ // into search terms.
+ let filterList = MailServices.filters.getTempFilterList(this.virtualFolder);
+ let tempFilter = filterList.createFilter("temp");
+ filterList.parseCondition(tempFilter, this.searchString);
+ return tempFilter;
+ },
+
+ /**
+ * Set the search string for this virtual folder to the stringified version of
+ * the provided list of nsIMsgSearchTerm search terms. If you already have
+ * a strinigified version of the search constraint, just set |searchString|
+ * directly.
+ *
+ * @param {string[]} aTerms - a list of search terms
+ */
+ set searchTerms(aTerms) {
+ let condition = "";
+ for (let term of aTerms) {
+ if (condition) {
+ condition += " ";
+ }
+ if (term.matchAll) {
+ condition = "ALL";
+ break;
+ }
+ condition += term.booleanAnd ? "AND (" : "OR (";
+ condition += term.termAsString + ")";
+ }
+ this.searchString = condition;
+ },
+
+ /**
+ * @returns {string} the set of search terms that define this virtual folder
+ * as a string. You may prefer to use |searchTerms| which converts them
+ * into a list of nsIMsgSearchTerms instead.
+ */
+ get searchString() {
+ return this.dbFolderInfo.getCharProperty("searchStr");
+ },
+ /**
+ * Set the search that defines this virtual folder from a string. If you have
+ * a list of nsIMsgSearchTerms, you should use |searchTerms| instead.
+ *
+ * @param {string} aSearchString
+ */
+ set searchString(aSearchString) {
+ this.dbFolderInfo.setCharProperty("searchStr", aSearchString);
+ },
+
+ /**
+ * @returns {boolean} whether the virtual folder is configured for online search.
+ */
+ get onlineSearch() {
+ return this.dbFolderInfo.getBooleanProperty("searchOnline", false);
+ },
+ /**
+ * Set whether the virtual folder is configured for online search.
+ *
+ * @param {boolean} aOnlineSearch
+ */
+ set onlineSearch(aOnlineSearch) {
+ this.dbFolderInfo.setBooleanProperty("searchOnline", aOnlineSearch);
+ },
+
+ /**
+ * @returns {?nsIDBFolderInfo} The dBFolderInfo associated with the virtual
+ * folder directly. Maybe null. Will cause the message database to be
+ * opened, which may have memory bloat/leak ramifications, so make sure
+ * the folder's database was already going to be opened anyways or that you
+ * call |cleanUpMessageDatabase|.
+ */
+ get dbFolderInfo() {
+ let msgDatabase = this.virtualFolder.msgDatabase;
+ return msgDatabase && msgDatabase.dBFolderInfo;
+ },
+
+ /**
+ * Avoid memory bloat by making the virtual folder forget about its database.
+ * If the database is actually in use (read: someone is keeping it alive by
+ * having references to it from places other than the nsIMsgFolder), the
+ * folder will be able to re-establish the reference for minimal cost.
+ */
+ cleanUpMessageDatabase() {
+ this.virtualFolder.msgDatabase.close(true);
+ this.virtualFolder.msgDatabase = null;
+ },
+};
diff --git a/comm/mailnews/base/src/WinUnreadBadge.jsm b/comm/mailnews/base/src/WinUnreadBadge.jsm
new file mode 100644
index 0000000000..819d8c5719
--- /dev/null
+++ b/comm/mailnews/base/src/WinUnreadBadge.jsm
@@ -0,0 +1,246 @@
+/* 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/.
+ *
+ * Based on https://github.com/bstreiff/unread-badge.
+ *
+ * Copyright (c) 2013-2020 Brandon Streiff
+ */
+
+const EXPORTED_SYMBOLS = ["WinUnreadBadge"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ imgTools: ["@mozilla.org/image/tools;1", "imgITools"],
+ taskbar: ["@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"],
+});
+
+/**
+ * Get an imgIContainer instance from a canvas element.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element.
+ * @param {number} width - The width of the canvas to use.
+ * @param {number} height - The height of the canvas to use.
+ * @returns {imgIContainer}
+ */
+function getCanvasAsImgContainer(canvas, width, height) {
+ let imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
+
+ // Create an imgIEncoder so we can turn the image data into a PNG stream.
+ let imgEncoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].getService(
+ Ci.imgIEncoder
+ );
+ imgEncoder.initFromData(
+ imageData.data,
+ imageData.data.length,
+ imageData.width,
+ imageData.height,
+ imageData.width * 4,
+ imgEncoder.INPUT_FORMAT_RGBA,
+ ""
+ );
+
+ // Now turn the PNG stream into an imgIContainer.
+ let imgBuffer = lazy.NetUtil.readInputStreamToString(
+ imgEncoder,
+ imgEncoder.available()
+ );
+ let iconImage = lazy.imgTools.decodeImageFromBuffer(
+ imgBuffer,
+ imgBuffer.length,
+ "image/png"
+ );
+
+ // Close the PNG stream.
+ imgEncoder.close();
+ return iconImage;
+}
+
+/**
+ * Draw text centered in the middle of a CanvasRenderingContext2D.
+ *
+ * @param {CanvasRenderingContext2D} cxt - The canvas context to operate on.
+ * @param {string} text - The text to draw.
+ */
+function drawUnreadCountText(cxt, text) {
+ cxt.save();
+
+ let imageSize = cxt.canvas.width;
+
+ // Use smaller fonts for longer text to try and squeeze it in.
+ let fontSize = imageSize * (0.95 - 0.15 * text.length);
+
+ cxt.font = "500 " + fontSize + "px Calibri";
+ cxt.fillStyle = "#ffffff";
+ cxt.textAlign = "center";
+
+ // TODO: There isn't a textBaseline for accurate vertical centering ('middle' is the
+ // middle of the 'em block', and digits extend higher than 'm'), and the Mozilla core
+ // does not currently support computation of ascenders and descenters in measureText().
+ // So, we just assume that the font is 70% of the 'px' height we requested, then
+ // compute where the baseline ought to be located.
+ let approximateHeight = fontSize * 0.7;
+
+ cxt.textBaseline = "alphabetic";
+ cxt.fillText(
+ text,
+ imageSize / 2,
+ imageSize - (imageSize - approximateHeight) / 2
+ );
+
+ cxt.restore();
+}
+
+/**
+ * Create a flat badge, as is the Windows 8/10 style.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element to draw the badge.
+ * @param {string} text - The text to draw in the badge.
+ */
+function createModernBadgeStyle(canvas, text) {
+ let cxt = canvas.getContext("2d");
+ let iconSize = canvas.width;
+
+ // Draw the background.
+ cxt.save();
+ // Solid color first.
+ cxt.fillStyle = "#ff0039";
+ cxt.shadowOffsetX = 0;
+ cxt.shadowOffsetY = 0;
+ cxt.shadowColor = "rgba(0,0,0,0.7)";
+ cxt.shadowBlur = iconSize / 10;
+ cxt.beginPath();
+ cxt.arc(iconSize / 2, iconSize / 2, iconSize / 2.25, 0, Math.PI * 2, true);
+ cxt.fill();
+ cxt.clip();
+ cxt.closePath();
+ cxt.restore();
+
+ drawUnreadCountText(cxt, text);
+}
+
+/**
+ * Downsample by 4X with simple averaging.
+ *
+ * Drawing at 4X and then downscaling like this gives us better results than
+ * using either CanvasRenderingContext2D.drawImage() to resize or letting
+ * the Windows taskbar service handle the resize, both of which seem to just
+ * give us a simple point resize.
+ *
+ * @param {Window} window - The DOM window.
+ * @param {HTMLCanvasElement} canvas - The input canvas element to resize.
+ * @returns {HTMLCanvasElement} The resized canvas element.
+ */
+function downsampleBy4X(window, canvas) {
+ let resizedCanvas = window.document.createElement("canvas");
+ resizedCanvas.width = resizedCanvas.height = canvas.width / 4;
+ resizedCanvas.style.width = resizedCanvas.style.height =
+ resizedCanvas.width + "px";
+
+ let source = canvas
+ .getContext("2d")
+ .getImageData(0, 0, canvas.width, canvas.height);
+ let downsampled = resizedCanvas
+ .getContext("2d")
+ .createImageData(resizedCanvas.width, resizedCanvas.height);
+
+ for (let y = 0; y < resizedCanvas.height; ++y) {
+ for (let x = 0; x < resizedCanvas.width; ++x) {
+ let r = 0,
+ g = 0,
+ b = 0,
+ a = 0;
+ let index;
+
+ for (let i = 0; i < 4; ++i) {
+ for (let j = 0; j < 4; ++j) {
+ index = ((y * 4 + i) * source.width + (x * 4 + j)) * 4;
+ r += source.data[index];
+ g += source.data[index + 1];
+ b += source.data[index + 2];
+ a += source.data[index + 3];
+ }
+ }
+
+ index = (y * downsampled.width + x) * 4;
+ downsampled.data[index] = Math.round(r / 16);
+ downsampled.data[index + 1] = Math.round(g / 16);
+ downsampled.data[index + 2] = Math.round(b / 16);
+ downsampled.data[index + 3] = Math.round(a / 16);
+ }
+ }
+
+ resizedCanvas.getContext("2d").putImageData(downsampled, 0, 0);
+
+ return resizedCanvas;
+}
+
+/**
+ * A module to manage the unread badge icon on Windows.
+ */
+var WinUnreadBadge = {
+ /**
+ * Keeping an instance of nsITaskbarOverlayIconController alive
+ * to show a taskbar icon after the updateUnreadCount method exits.
+ */
+ _controller: null,
+
+ /**
+ * Update the unread badge.
+ *
+ * @param {number} unreadCount - Unread message count.
+ * @param {number} unreadTooltip - Unread message count tooltip.
+ */
+ async updateUnreadCount(unreadCount, unreadTooltip) {
+ let window = Services.wm.getMostRecentBrowserWindow();
+ if (!window) {
+ return;
+ }
+ if (!this._controller) {
+ this._controller = lazy.taskbar.getOverlayIconController(window.docShell);
+ }
+ if (unreadCount == 0) {
+ // Remove the badge if no unread.
+ this._controller.setOverlayIcon(null, "");
+ return;
+ }
+
+ // Draw the badge in a canvas.
+ let smallIconSize = Cc["@mozilla.org/windows-ui-utils;1"].getService(
+ Ci.nsIWindowsUIUtils
+ ).systemSmallIconSize;
+ let iconSize = Math.floor(
+ (window.windowUtils.displayDPI / 96) * smallIconSize
+ );
+ let iconSize4X = iconSize * 4;
+ let badge = window.document.createElement("canvas");
+ badge.width = badge.height = iconSize4X;
+ badge.style.width = badge.style.height = badge.width + "px";
+
+ createModernBadgeStyle(
+ badge,
+ unreadCount < 100 ? unreadCount.toString() : "99+"
+ );
+
+ badge = downsampleBy4X(window, badge);
+ let icon = getCanvasAsImgContainer(badge, iconSize, iconSize);
+ // Purge image from cache to force encodeImage() to not be lazy
+ icon.requestDiscard();
+ // Side effect of encodeImage() is that it decodes original image
+ lazy.imgTools.encodeImage(icon, "image/png");
+ // Somehow this is needed to prevent NS_ERROR_NOT_AVAILABLE error in
+ // setOverlayIcon.
+ await new Promise(resolve => window.setTimeout(resolve));
+
+ this._controller.setOverlayIcon(icon, unreadTooltip);
+ },
+};
diff --git a/comm/mailnews/base/src/components.conf b/comm/mailnews/base/src/components.conf
new file mode 100644
index 0000000000..776148e132
--- /dev/null
+++ b/comm/mailnews/base/src/components.conf
@@ -0,0 +1,359 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{a30be08c-afc8-4fed-9af7-79778a23db23}",
+ "contract_ids": ["@mozilla.org/mail/folder-lookup;1"],
+ "jsm": "resource:///modules/FolderLookupService.jsm",
+ "constructor": "FolderLookupService",
+ },
+ {
+ "cid": "{49b04761-23dd-45d7-903d-619418a4d319}",
+ "contract_ids": ["@mozilla.org/messenger/msgAsyncPrompter;1"],
+ "jsm": "resource:///modules/MsgAsyncPrompter.jsm",
+ "constructor": "MsgAsyncPrompter",
+ },
+ {
+ "cid": "{26ebf3a7-3e52-4b7a-a89b-fa7c0b7506f9}",
+ "contract_ids": ["@mozilla.org/messenger/msgAuthPrompt;1"],
+ "jsm": "resource:///modules/MsgAsyncPrompter.jsm",
+ "constructor": "MsgAuthPrompt",
+ },
+ {
+ "cid": "{b63d8e4c-bf60-439b-be0e-7c9f67291042}",
+ "contract_ids": ["@mozilla.org/mail/oauth2-module;1"],
+ "jsm": "resource:///modules/OAuth2Module.jsm",
+ "constructor": "OAuth2Module",
+ },
+ {
+ "cid": "{740880e6-e299-4165-b82f-df1dcab3ae22}",
+ "contract_ids": ["@mozilla.org/newMailNotificationService;1"],
+ "jsm": "resource:///modules/MailNotificationService.jsm",
+ "constructor": "NewMailNotificationService",
+ "name": "MailNotification",
+ "interfaces": ["mozINewMailNotificationService"],
+ },
+ {
+ "cid": "{37246055-3596-4bfa-911f-3d2977e8d284}",
+ "contract_ids": ["@mozilla.org/mail/auth-module;1"],
+ "type": "nsMailAuthModule",
+ "headers": ["/comm/mailnews/base/src/nsMailAuthModule.h"],
+ },
+ {
+ "cid": "{e9aef539-29db-4936-9fdc-40ba11c70cb3}",
+ "contract_ids": ["@mozilla.org/mail/notification-manager;1"],
+ "jsm": "resource:///modules/MailNotificationManager.jsm",
+ "constructor": "MailNotificationManager",
+ },
+ {
+ "cid": "{4a85a5d0-cddd-11d2-b7f6-00805f05ffa5}",
+ "contract_ids": [
+ "@mozilla.org/appshell/component/messenger;1",
+ "@mozilla.org/messenger/windowservice;1",
+ ],
+ "type": "nsMessengerBootstrap",
+ "headers": ["/comm/mailnews/base/src/nsMessengerBootstrap.h"],
+ },
+ {
+ "cid": "{d5124441-d59e-11d2-806a-006080128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/services/session;1"],
+ "type": "nsMsgMailSession",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"],
+ "name": "MailSession",
+ "interfaces": ["nsIMsgMailSession"],
+ },
+ {
+ "cid": "{f436a174-e2c0-4955-9afe-e3feb68aee56}",
+ "contract_ids": ["@mozilla.org/messenger;1"],
+ "type": "nsMessenger",
+ "headers": ["/comm/mailnews/base/src/nsMessenger.h"],
+ },
+ {
+ "cid": "{d2876e50-e62c-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/account-manager;1"],
+ "type": "nsMsgAccountManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgAccountManager.h"],
+ "name": "AccountManager",
+ "interfaces": ["nsIMsgAccountManager"],
+ },
+ {
+ "cid": "{68b25510-e641-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/account;1"],
+ "type": "nsMsgAccount",
+ "headers": ["/comm/mailnews/base/src/nsMsgAccount.h"],
+ },
+ {
+ "cid": "{8fbf6ac0-ebcc-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/identity;1"],
+ "type": "nsMsgIdentity",
+ "headers": ["/comm/mailnews/base/src/nsMsgIdentity.h"],
+ },
+ {
+ "cid": "{4a374e7e-190f-11d3-8a88-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/biffManager;1"],
+ "type": "nsMsgBiffManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgBiffManager.h"],
+ },
+ {
+ "cid": "{a687b474-afd8-418f-8ad9-f362202ae9a9}",
+ "contract_ids": ["@mozilla.org/messenger/purgeService;1"],
+ "type": "nsMsgPurgeService",
+ "headers": ["/comm/mailnews/base/src/nsMsgPurgeService.h"],
+ },
+ {
+ "cid": "{7f9a9fb0-4161-11d4-9876-00c04fa0d2a6}",
+ "contract_ids": ["@mozilla.org/messenger/statusBarBiffManager;1"],
+ "type": "nsStatusBarBiffManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsStatusBarBiffManager.h"],
+ },
+ {
+ "cid": "{7741daed-2125-11d3-8a90-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/copymessagestreamlistener;1"],
+ "type": "nsCopyMessageStreamListener",
+ "headers": ["/comm/mailnews/base/src/nsCopyMessageStreamListener.h"],
+ },
+ {
+ "cid": "{c766e666-29bd-11d3-afb3-001083002da8}",
+ "contract_ids": ["@mozilla.org/messenger/messagecopyservice;1"],
+ "type": "nsMsgCopyService",
+ "headers": ["/comm/mailnews/base/src/nsMsgCopyService.h"],
+ "name": "Copy",
+ "interfaces": ["nsIMsgCopyService"],
+ },
+ {
+ "cid": "{bcdca970-3b22-11d3-8d76-0080f58a6617}",
+ "contract_ids": ["@mozilla.org/messenger/msgFolderCache;1"],
+ "type": "nsMsgFolderCache",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderCache.h"],
+ },
+ {
+ "cid": "{bd85a417-5433-11d3-8ac5-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/statusfeedback;1"],
+ "type": "nsMsgStatusFeedback",
+ "headers": ["/comm/mailnews/base/src/nsMsgStatusFeedback.h"],
+ },
+ {
+ "cid": "{bb460dff-8bf0-11d3-8afe-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/msgwindow;1"],
+ "type": "nsMsgWindow",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgWindow.h"],
+ },
+ {
+ "cid": "{8510876a-1dd2-11b2-8253-91f71b348a25}",
+ "contract_ids": ["@mozilla.org/messenger/subscribableserver;1"],
+ "type": "nsSubscribableServer",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsSubscribableServer.h"],
+ },
+ {
+ "cid": "{56c4c2ac-fe4a-4528-aa78-f8fb579b029c}",
+ "contract_ids": ["@mozilla.org/messenger/foldercompactor;1"],
+ "type": "nsMsgFolderCompactor",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderCompactor.h"],
+ },
+ {
+ "cid": "{52f860e0-1dd2-11b2-aa72-bb751981bd00}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threaded"],
+ "type": "nsMsgThreadedDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgThreadedDBView.h"],
+ },
+ {
+ "cid": "{ca79a00e-010d-11d5-a5be-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threadswithunread"],
+ "type": "nsMsgThreadsWithUnreadDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"],
+ },
+ {
+ "cid": "{597e1ffe-0123-11d5-a5be-0060b0fc04b7}",
+ "contract_ids": [
+ "@mozilla.org/messenger/msgdbview;1?type=watchedthreadswithunread"
+ ],
+ "type": "nsMsgWatchedThreadsWithUnreadDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"],
+ },
+ {
+ "cid": "{aeac118c-0823-11d5-a5bf-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=search"],
+ "type": "nsMsgSearchDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSearchDBView.h"],
+ },
+ {
+ "cid": "{2dd9d0fe-b609-11d6-bacc-00108335748d}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=quicksearch"],
+ "type": "nsMsgQuickSearchDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgQuickSearchDBView.h"],
+ },
+ {
+ "cid": "{2af6e050-04f6-495a-8387-86b0aeb1863c}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=xfvf"],
+ "type": "nsMsgXFVirtualFolderDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h"],
+ },
+ {
+ "cid": "{e4603d6c-0a74-47c5-b69e-2f8876990304}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=group"],
+ "type": "nsMsgGroupView",
+ "headers": ["/comm/mailnews/base/src/nsMsgGroupView.h"],
+ },
+ {
+ "cid": "{bcf6afbe-7d4f-11ec-9092-eb4fed0a5aaa}",
+ "contract_ids": ["@mozilla.org/msgDBView/msgDBViewService;1"],
+ "type": "nsMsgDBViewService",
+ "headers": ["/comm/mailnews/base/src/nsMsgDBView.h"],
+ "name": "DBView",
+ "interfaces": ["nsIMsgDBViewService"],
+ },
+ {
+ "cid": "{ac6c518a-09b2-11d5-a5bf-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/offline-manager;1"],
+ "type": "nsMsgOfflineManager",
+ "headers": ["/comm/mailnews/base/src/nsMsgOfflineManager.h"],
+ },
+ {
+ "cid": "{9f4dd201-3b1f-11d5-9daa-c345c9453d3c}",
+ "contract_ids": ["@mozilla.org/messenger/progress;1"],
+ "type": "nsMsgProgress",
+ "headers": ["/comm/mailnews/base/src/nsMsgProgress.h"],
+ },
+ {
+ "cid": "{ce6038ae-e5e0-4372-9cff-2a6633333b2b}",
+ "contract_ids": ["@mozilla.org/messenger/spamsettings;1"],
+ "type": "nsSpamSettings",
+ "headers": ["/comm/mailnews/base/src/nsSpamSettings.h"],
+ },
+ {
+ "cid": "{b3db9392-1b15-48ba-a136-0cc3db13d87b}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=cid"],
+ "type": "nsCidProtocolHandler",
+ "headers": ["/comm/mailnews/base/src/nsCidProtocolHandler.h"],
+ "protocol_config": {
+ "scheme": "cid",
+ "flags": [
+ "URI_DANGEROUS_TO_LOAD"
+ ],
+ },
+ },
+ {
+ "cid": "{b897da55-8256-4cf5-892b-32e77bc7c50b}",
+ "contract_ids": ["@mozilla.org/messenger/tagservice;1"],
+ "type": "nsMsgTagService",
+ "headers": ["/comm/mailnews/base/src/nsMsgTagService.h"],
+ "name": "Tag",
+ "interfaces": ["nsIMsgTagService"],
+ },
+ {
+ "cid": "{0c8ec907-49c7-49bc-8bdf-b16e29bd6c47}",
+ "contract_ids": ["@mozilla.org/msgFolder/msgFolderService;1"],
+ "type": "nsMsgFolderService",
+ "headers": ["/comm/mailnews/base/src/nsMsgDBFolder.h"],
+ "name": "Folder",
+ "interfaces": ["nsIMsgFolderService"],
+ },
+ {
+ "cid": "{f1f7cbcd-d5e3-45a0-aa2d-cecf1a95ab03}",
+ "contract_ids": ["@mozilla.org/messenger/msgnotificationservice;1"],
+ "type": "nsMsgFolderNotificationService",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderNotificationService.h"],
+ "name": "FolderNotification",
+ "interfaces": ["nsIMsgFolderNotificationService"],
+ },
+ {
+ "cid": "{dbfcfdf0-4489-4faa-8122-190fd1efa16c}",
+ "contract_ids": ["@mozilla.org/messenger/content-policy;1"],
+ "type": "nsMsgContentPolicy",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgContentPolicy.h"],
+ "categories": {"content-policy": "@mozilla.org/messenger/content-policy;1"},
+ },
+ {
+ "cid": "{483c8abb-ecf9-48a3-a394-2c604b603bd5}",
+ "contract_ids": ["@mozilla.org/messenger/msgshutdownservice;1"],
+ "type": "nsMsgShutdownService",
+ "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"],
+ },
+ {
+ "cid": "{03f9bb53-a680-4349-8de9-d26864d9ffd9}",
+ "contract_ids": ["@mozilla.org/mail/dir-provider;1"],
+ "type": "nsMailDirProvider",
+ "headers": ["/comm/mailnews/base/src/nsMailDirProvider.h"],
+ "categories": {"xpcom-directory-providers": "mail-directory-provider"},
+ },
+ {
+ "cid": "{6ef7eafd-72d0-4c56-9409-67e16d0f255b}",
+ "contract_ids": ["@mozilla.org/stopwatch;1"],
+ "type": "nsStopwatch",
+ "headers": ["/comm/mailnews/base/src/nsStopwatch.h"],
+ },
+ {
+ "cid": "{de0f34a9-a87f-4f4c-b978-6187db187b90}",
+ "contract_ids": ["@mozilla.org/mailnews/document-loader-factory;1"],
+ "type": "mailnews::MailNewsDLF",
+ "headers": ["/comm/mailnews/base/src/MailNewsDLF.h"],
+ "categories": {"Gecko-Content-Viewers": "message/rfc822"},
+ },
+ {
+ "cid": "{bf88b48c-fd8e-40b4-ba36-c7c3ad6d8ac9}",
+ "contract_ids": ["@mozilla.org/embedcomp/base-command-controller;1"],
+ "type": "nsBaseCommandController",
+ "headers": ["/dom/commandhandler/nsBaseCommandController.h"],
+ },
+ {
+ "cid": "{9c8f9601-801a-11d2-98ba-00805f297d89}",
+ "contract_ids": ["@mozilla.org/transactionmanager;1"],
+ "type": "TransactionManager",
+ "headers": ["/editor/txmgr/TransactionManager.h"],
+ },
+ {
+ "cid": "{14c13684-1dd2-11b2-9463-bb10ba742554}",
+ "contract_ids": ["@mozilla.org/userinfo;1"],
+ "type": "nsUserInfo",
+ "headers": ["/comm/mailnews/base/src/nsUserInfo.h"],
+ },
+ # The XPCOM registration for nsSyncStreamListener was moved to comm-central
+ # in bug 1501718. In bug 1800606 all uses were removed from comm-central.
+ # The registration is maintained for some MailExtension Experiments.
+ # Use of this service is discouraged due to its synchronous processing and
+ # probable deprecation in the future. Instead use asynchronous code like in
+ # `get_msg_source()` in ComposeHelpers.jsm.
+ {
+ "cid": "{439400d3-6f23-43db-8b06-8aafe1869bd8}",
+ "contract_ids": ["@mozilla.org/network/sync-stream-listener;1"],
+ "constructor": "SyncStreamListenerCreate",
+ "headers": ["/comm/mailnews/base/src/nsMsgUtils.h"],
+ },
+]
+
+if buildconfig.substs["OS_ARCH"] == "Darwin":
+ Classes += [
+ {
+ "cid": "{746b28a5-d239-4719-b1a2-cf8093332ae3}",
+ "contract_ids": ["@mozilla.org/messenger/osintegration;1"],
+ "type": "nsMessengerOSXIntegration",
+ "headers": ["/comm/mailnews/base/src/nsMessengerOSXIntegration.h"],
+ },
+ ]
+
+ Categories = {
+ "app-startup": {
+ "OS Integration": "@mozilla.org/messenger/osintegration;1",
+ }
+ }
+
+if buildconfig.substs["OS_ARCH"] == "WINNT":
+ Classes += [
+ {
+ "cid": "{a74dd1d6-2ec4-4985-98f3-f69e18d20811}",
+ "contract_ids": ["@mozilla.org/messenger/osintegration;1"],
+ "type": "nsMessengerWinIntegration",
+ "headers": ["/comm/mailnews/base/src/nsMessengerWinIntegration.h"],
+ },
+ ]
diff --git a/comm/mailnews/base/src/converterWorker.js b/comm/mailnews/base/src/converterWorker.js
new file mode 100644
index 0000000000..188476c1e1
--- /dev/null
+++ b/comm/mailnews/base/src/converterWorker.js
@@ -0,0 +1,533 @@
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+/**
+ * This worker will perform mbox<->maildir conversions on a tree of
+ * directories. It operates purely at the filesystem level.
+ *
+ * The initial message data should pass in these params to control
+ * the conversion:
+ *
+ * srcType - source mailstore type ('mbox' or 'maildir')
+ * destType - destination mailstore type ('maildir' or 'mbox')
+ * srcRoot - root path of source (eg ".../ImapMail/imap.example.com")
+ * destRoot - root path of destination (eg "/tmp/imap.example.com-maildir")
+ *
+ * The conversion is non-destructive - srcRoot will be left untouched.
+ *
+ * The worker will post progress messages back to the main thread of
+ * the form:
+ *
+ * {"msg": "progress", "val": val, "total": total}
+ *
+ * Where `val` is the current progress, out of `total`.
+ * The units used for val and total are undefined.
+ *
+ * When the conversion is complete, before exiting, the worker sends a
+ * message of the form:
+ *
+ * {"msg": "success"}
+ *
+ * Errors are posted back to the main thread via the standard
+ * "error" event.
+ *
+ */
+
+/**
+ * Merge all the messages in a maildir into a single mbox file.
+ *
+ * @param {string} maildir - Path to the source maildir.
+ * @param {string} mboxFilename - Path of the mbox file to create.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates. Param is number of
+ * "units" processed since last update.
+ */
+async function maildirToMBox(maildir, mboxFilename, progressFn) {
+ // Helper to format dates
+ // eg "Thu Jan 18 12:34:56 2018"
+ let fmtUTC = function (d) {
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const monthNames = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ return (
+ dayNames[d.getUTCDay()] +
+ " " +
+ monthNames[d.getUTCMonth()] +
+ " " +
+ d.getUTCDate().toString().padStart(2) +
+ " " +
+ d.getUTCHours().toString().padStart(2, "0") +
+ ":" +
+ d.getUTCMinutes().toString().padStart(2, "0") +
+ ":" +
+ d.getUTCSeconds().toString().padStart(2, "0") +
+ " " +
+ d.getUTCFullYear()
+ );
+ };
+
+ // Initialize mbox file
+ await IOUtils.write(mboxFilename, new Uint8Array(), {
+ mode: "create",
+ });
+
+ // Iterate over all the message files in "cur".
+ let curPath = PathUtils.join(maildir, "cur");
+ let paths = await IOUtils.getChildren(curPath);
+ let files = await Promise.all(
+ paths.map(async path => {
+ let stat = await IOUtils.stat(path);
+ return {
+ path,
+ creationDate: stat.creationTime,
+ };
+ })
+ );
+ // We write out the mbox messages ordered by creation time.
+ // Not ideal, but best we can do without parsing message.
+ files.sort(function (a, b) {
+ return a.creationDate - b.creationDate;
+ });
+
+ for (let ent of files) {
+ let raw = await IOUtils.read(ent.path);
+ // Old converter had a bug where maildir messages included the
+ // leading "From " marker, so we need to cope with any
+ // cases of this left in the wild.
+ if (String.fromCharCode.apply(null, raw.slice(0, 5)) != "From ") {
+ // Write the separator line.
+ // Technically, timestamp should be the reception time of the
+ // message, but we don't really want to have to parse the
+ // message here and nothing is likely to rely on it.
+ let sepLine = "From - " + fmtUTC(new Date()) + "\n";
+ await IOUtils.writeUTF8(mboxFilename, sepLine, {
+ mode: "append",
+ });
+ }
+
+ await IOUtils.write(mboxFilename, raw, {
+ mode: "append",
+ });
+ // Maildir progress is one per message.
+ progressFn(1);
+ }
+}
+
+/**
+ * Split an mbox file up into a maildir.
+ *
+ * @param {string} mboxPath - Path of the mbox file to split.
+ * @param {string} maildirPath - Path of the maildir to create.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates. One parameter is
+ * passed - the number of "cost units"
+ * since the previous update.
+ */
+async function mboxToMaildir(mboxPath, maildirPath, progressFn) {
+ // Create the maildir structure.
+ await IOUtils.makeDirectory(maildirPath);
+ let curDirPath = PathUtils.join(maildirPath, "cur");
+ let tmpDirPath = PathUtils.join(maildirPath, "tmp");
+ await IOUtils.makeDirectory(curDirPath);
+ await IOUtils.makeDirectory(tmpDirPath);
+
+ const CHUNK_SIZE = 1000000;
+ // SAFE_MARGIN is how much to keep back between chunks in order to
+ // cope with separator lines which might span chunks.
+ const SAFE_MARGIN = 100;
+
+ // A regexp to match mbox separator lines. Separator lines in the wild can
+ // have all sorts of forms, for example:
+ //
+ // "From "
+ // "From MAILER-DAEMON Fri Jul 8 12:08:34 2011"
+ // "From - Mon Jul 11 12:08:34 2011"
+ // "From bob@example.com Fri Jul 8 12:08:34 2011"
+ //
+ // So we accept any line beginning with "From " and ignore the rest of it.
+ //
+ // We also require a message header on the next line, in order
+ // to better cope with unescaped "From " lines in the message body.
+ // note: the first subexpression matches the separator line, so
+ // that it can be removed from the input.
+ let sepRE = /^(From (?:.*?)\r?\n)[\x21-\x7E]+:/gm;
+
+ // Use timestamp as starting name for output messages, incrementing
+ // by one for each.
+ let ident = Date.now();
+
+ /**
+ * Helper. Convert a string into a Uint8Array, using no encoding. The low
+ * byte of each 16 bit character will be used, the high byte discarded.
+ *
+ * @param {string} str - Input string with chars in 0-255 range.
+ * @returns {Uint8Array} The output bytes.
+ */
+ let stringToBytes = function (str) {
+ var bytes = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ bytes[i] = str.charCodeAt(i);
+ }
+ return bytes;
+ };
+
+ /**
+ * Helper. Convert a Uint8Array directly into a string, using each byte
+ * directly as a character code. So all characters in the resulting string
+ * will range from 0 to 255, even though they are 16 bit values.
+ *
+ * @param {Uint8Array} bytes - The bytes to convert.
+ * @returns {string} The byte values in string form.
+ */
+ let bytesToString = function (bytes) {
+ return bytes.reduce(function (str, b) {
+ return str + String.fromCharCode(b);
+ }, "");
+ };
+
+ let outPath;
+
+ /**
+ * Helper. Write out a block of bytes to the current message file, starting
+ * a new file if required.
+ *
+ * @param {string} str - The bytes to append (as chars in range 0-255).
+ */
+ let writeToMsg = async function (str) {
+ let mode = "append";
+ if (!outPath) {
+ outPath = PathUtils.join(curDirPath, ident.toString() + ".eml");
+ ident += 1;
+ mode = "create";
+ }
+ // We know that str is really raw 8-bit data, not UTF-16. So we can
+ // discard the upper byte and just keep the low byte of each char.
+ let raw = stringToBytes(str);
+ await IOUtils.write(outPath, raw, { mode });
+ // For mbox->maildir conversion, progress is measured in bytes.
+ progressFn(raw.byteLength);
+ };
+
+ let buf = "";
+ let eof = false;
+ let offset = 0;
+ while (!eof) {
+ let rawBytes = await IOUtils.read(mboxPath, {
+ offset,
+ maxBytes: CHUNK_SIZE,
+ });
+ // We're using JavaScript strings (which hold 16bit characters) to store
+ // 8 bit data. This sucks, but is faster than trying to operate directly
+ // upon Uint8Arrays. A lot of work goes into optimising JavaScript strings.
+ buf += bytesToString(rawBytes);
+ offset += rawBytes.byteLength;
+ eof = rawBytes.byteLength < CHUNK_SIZE;
+
+ let pos = 0;
+ sepRE.lastIndex = 0; // start at beginning of buf
+ let m = null;
+ while ((m = sepRE.exec(buf)) !== null) {
+ // Output everything up to the line separator.
+ if (m.index > pos) {
+ await writeToMsg(buf.substring(pos, m.index));
+ }
+ pos = m.index;
+ pos += m[1].length; // skip the "From " line
+ // Reset the current message file path if any.
+ if (outPath) {
+ outPath = null;
+ }
+ }
+
+ // Deal with whatever is left in the buffer.
+ let endPos = buf.length;
+ if (!eof) {
+ // Keep back enough to cope with separator lines crossing
+ // chunk boundaries.
+ endPos -= SAFE_MARGIN;
+ if (endPos < pos) {
+ endPos = pos;
+ }
+ }
+
+ if (endPos > pos) {
+ await writeToMsg(buf.substring(pos, endPos));
+ }
+ buf = buf.substring(endPos);
+ }
+}
+
+/**
+ * Check if directory is a subfolder directory.
+ *
+ * @param {string} name - Name of directory to check.
+ * @returns {boolean} - true if subfolder.
+ */
+function isSBD(name) {
+ return name.substr(-4) == ".sbd";
+}
+
+/**
+ * Check if file is a type which should be copied verbatim as part of a
+ * conversion.
+ * See also: nsMsgLocalStoreUtils::nsShouldIgnoreFile().
+ *
+ * @param {string} name - Name of file to check.
+ * @returns {boolean} - true if file should be copied verbatim.
+ */
+function isFileToCopy(name) {
+ let ext4 = name.substr(-4);
+ // Database and config files.
+ if (ext4 == ".msf" || ext4 == ".dat") {
+ return true;
+ }
+ // Summary files.
+ if (ext4 == ".snm" || ext4 == ".toc") {
+ return true;
+ }
+ // A few files we know might be lurking there.
+ const SPECIAL_FILES = [
+ "filterlog.html",
+ "junklog.html",
+ "feeds.json",
+ "feeds.json.tmp",
+ "feeds.json.backup",
+ "feeds.json.corrupt",
+ "feeditems.json",
+ "feeditems.json.tmp",
+ "feeditems.json.backup",
+ "feeditems.json.corrupt",
+ "mailfilt.log",
+ "filters.js",
+ ];
+ if (SPECIAL_FILES.includes(name)) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Check if file is an mbox.
+ * (actually we can't really tell if it's an mbox or not just from the name.
+ * we just assume it is, if it's not .msf or .dat).
+ *
+ * @param {string} name - Name of file to check.
+ * @returns {boolean} - true if file is an mbox
+ */
+function isMBoxName(name) {
+ // If it's not a "special" file, assume it's mbox.
+ return !isFileToCopy(name);
+}
+
+/**
+ * Check if directory is a maildir (by looking for a "cur" subdir).
+ *
+ * @param {string} dir - Path of directory to check.
+ * @returns {Promise<boolean>} - true if directory is a maildir.
+ */
+async function isMaildir(dir) {
+ try {
+ let cur = PathUtils.join(dir, "cur");
+ let fi = await IOUtils.stat(cur);
+ return fi.type === "directory";
+ } catch (ex) {
+ if (ex instanceof DOMException && ex.name === "NotFoundError") {
+ // "cur" does not exist - not a maildir.
+ return false;
+ }
+ throw ex; // Other error.
+ }
+}
+
+/**
+ * Count the number of messages in the "cur" dir of maildir.
+ *
+ * @param {string} maildir - Path of maildir.
+ * @returns {Promise<number>} - number of messages found.
+ */
+async function countMaildirMsgs(maildir) {
+ let cur = PathUtils.join(maildir, "cur");
+ let paths = await IOUtils.getChildren(cur);
+ return paths.length;
+}
+
+/**
+ * Recursively calculate the 'cost' of a hierarchy of maildir folders.
+ * This is the figure used for progress updates.
+ * For maildir, cost is 1 per message.
+ *
+ * @param {string} srcPath - Path of root dir containing maildirs.
+ * @returns {Promise<number>} - calculated conversion cost.
+ */
+async function calcMaildirCost(srcPath) {
+ let cost = 0;
+ for (let path of await IOUtils.getChildren(srcPath)) {
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ let name = PathUtils.filename(path);
+ if (isSBD(name)) {
+ // Recurse into subfolder.
+ cost += await calcMaildirCost(path);
+ } else if (await isMaildir(path)) {
+ // Looks like a maildir. Cost is number of messages.
+ cost += await countMaildirMsgs(path);
+ }
+ }
+ }
+ return cost;
+}
+
+/**
+ * Recursively calculate the 'cost' of a hierarchy of mbox folders.
+ * This is the figure used for progress updates.
+ * For mbox, cost is the total byte size of data. This avoids the need to
+ * parse the mbox files to count the number of messages.
+ * Note that this byte count cost is not 100% accurate because it includes
+ * the "From " lines which are not written into the maildir files. But it's
+ * definitely close enough to give good user feedback.
+ *
+ * @param {string} srcPath - Path of root dir containing maildirs.
+ * @returns {Promise<number>} - calculated conversion cost.
+ */
+async function calcMBoxCost(srcPath) {
+ let cost = 0;
+ for (const path of await IOUtils.getChildren(srcPath)) {
+ let stat = await IOUtils.stat(path);
+ let name = PathUtils.filename(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ cost += await calcMBoxCost(path);
+ }
+ } else if (isMBoxName(name)) {
+ cost += stat.size;
+ }
+ }
+ return cost;
+}
+
+/**
+ * Recursively convert a tree of mbox-based folders to maildirs.
+ *
+ * @param {string} srcPath - Root path containing mboxes.
+ * @param {string} destPath - Where to create destination root.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates (called with number of
+ * cost "units" since last update)
+ */
+async function convertTreeMBoxToMaildir(srcPath, destPath, progressFn) {
+ await IOUtils.makeDirectory(destPath);
+
+ for (const path of await IOUtils.getChildren(srcPath)) {
+ let name = PathUtils.filename(path);
+ let dest = PathUtils.join(destPath, name);
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ await convertTreeMBoxToMaildir(path, dest, progressFn);
+ }
+ } else if (isFileToCopy(name)) {
+ await IOUtils.copy(path, dest);
+ } else if (isMBoxName(name)) {
+ // It's an mbox. Convert it.
+ await mboxToMaildir(path, dest, progressFn);
+ }
+ }
+}
+
+/**
+ * Recursively convert a tree of maildir-based folders to mbox.
+ *
+ * @param {string} srcPath - Root path containing maildirs.
+ * @param {string} destPath - Where to create destination root.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates (called with number of
+ * cost "units" since last update)
+ */
+async function convertTreeMaildirToMBox(srcPath, destPath, progressFn) {
+ await IOUtils.makeDirectory(destPath);
+
+ for (let path of await IOUtils.getChildren(srcPath)) {
+ let name = PathUtils.filename(path);
+ let dest = PathUtils.join(destPath, name);
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ await convertTreeMaildirToMBox(path, dest, progressFn);
+ } else if (await isMaildir(path)) {
+ // It's a maildir - convert it.
+ await maildirToMBox(path, dest, progressFn);
+ }
+ } else if (isFileToCopy(name)) {
+ await IOUtils.copy(path, dest);
+ }
+ }
+}
+
+// propagate unhandled rejections to the error handler on the main thread
+self.addEventListener("unhandledrejection", function (error) {
+ throw error.reason;
+});
+
+self.addEventListener("message", function (e) {
+ // Unpack the request params from the main thread.
+ let srcType = e.data.srcType;
+ let destType = e.data.destType;
+ let srcRoot = e.data.srcRoot;
+ let destRoot = e.data.destRoot;
+ // destRoot will be a temporary dir, so if it all goes pear-shaped
+ // we can just bail out without cleaning up.
+
+ // Configure the conversion.
+ let costFn = null;
+ let convertFn = null;
+ if (srcType == "maildir" && destType == "mbox") {
+ costFn = calcMaildirCost;
+ convertFn = convertTreeMaildirToMBox;
+ } else if (srcType == "mbox" && destType == "maildir") {
+ costFn = calcMBoxCost;
+ convertFn = convertTreeMBoxToMaildir;
+ } else {
+ throw new Error(`Unsupported conversion: ${srcType} => ${destType}`);
+ }
+
+ // Go!
+ costFn(srcRoot).then(totalCost => {
+ let v = 0;
+ let progressFn = function (n) {
+ v += n;
+ self.postMessage({ msg: "progress", val: v, total: totalCost });
+ };
+ convertFn(srcRoot, destRoot, progressFn).then(() => {
+ // We fake a final progress update, with exactly 100% completed.
+ // Our byte-counting on mbox->maildir conversion will fall slightly short:
+ // The total is estimated from the mbox filesize, but progress is tracked
+ // by counting bytes as they are written out - and the mbox "From " lines
+ // are _not_ written out to the maildir files.
+ // This is still accurate enough to provide progress to the user, but we
+ // don't want the GUI left showing "progress 97% - conversion complete!"
+ // or anything silly like that.
+ self.postMessage({ msg: "progress", val: totalCost, total: totalCost });
+
+ // Let the main thread know we succeeded.
+ self.postMessage({ msg: "success" });
+ });
+ });
+});
diff --git a/comm/mailnews/base/src/hostnameUtils.jsm b/comm/mailnews/base/src/hostnameUtils.jsm
new file mode 100644
index 0000000000..e80c210b2e
--- /dev/null
+++ b/comm/mailnews/base/src/hostnameUtils.jsm
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+/* 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/. */
+
+/**
+ * Generic shared utility code for checking of IP and hostname validity.
+ */
+
+const EXPORTED_SYMBOLS = [
+ "isLegalHostNameOrIP",
+ "isLegalHostName",
+ "isLegalIPv4Address",
+ "isLegalIPv6Address",
+ "isLegalIPAddress",
+ "isLegalLocalIPAddress",
+ "cleanUpHostName",
+ "kMinPort",
+ "kMaxPort",
+];
+
+var kMinPort = 1;
+var kMaxPort = 65535;
+
+/**
+ * Check if aHostName is an IP address or a valid hostname.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in addition to decimal.
+ * @returns {?string} Unobscured host name if aHostName is valid.
+ * Returns null if it's not.
+ */
+function isLegalHostNameOrIP(aHostName, aAllowExtendedIPFormats) {
+ /*
+ RFC 1123:
+ Whenever a user inputs the identity of an Internet host, it SHOULD
+ be possible to enter either (1) a host domain name or (2) an IP
+ address in dotted-decimal ("#.#.#.#") form. The host SHOULD check
+ the string syntactically for a dotted-decimal number before
+ looking it up in the Domain Name System.
+ */
+
+ return (
+ isLegalIPAddress(aHostName, aAllowExtendedIPFormats) ||
+ isLegalHostName(aHostName)
+ );
+}
+
+/**
+ * Check if aHostName is a valid hostname.
+ *
+ * @returns {?string} The host name if it is valid. Returns null if it's not.
+ */
+function isLegalHostName(aHostName) {
+ /*
+ RFC 952:
+ A "name" (Net, Host, Gateway, or Domain name) is a text string up
+ to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus
+ sign (-), and period (.). Note that periods are only allowed when
+ they serve to delimit components of "domain style names". (See
+ RFC-921, "Domain Name System Implementation Schedule", for
+ background). No blank or space characters are permitted as part of a
+ name. No distinction is made between upper and lower case. The first
+ character must be an alpha character. The last character must not be
+ a minus sign or period.
+
+ RFC 1123:
+ The syntax of a legal Internet host name was specified in RFC-952
+ [DNS:4]. One aspect of host name syntax is hereby changed: the
+ restriction on the first character is relaxed to allow either a
+ letter or a digit. Host software MUST support this more liberal
+ syntax.
+
+ Host software MUST handle host names of up to 63 characters and
+ SHOULD handle host names of up to 255 characters.
+
+ RFC 1034:
+ Relative names are either taken relative to a well known origin, or to a
+ list of domains used as a search list. Relative names appear mostly at
+ the user interface, where their interpretation varies from
+ implementation to implementation, and in master files, where they are
+ relative to a single origin domain name. The most common interpretation
+ uses the root "." as either the single origin or as one of the members
+ of the search list, so a multi-label relative name is often one where
+ the trailing dot has been omitted to save typing.
+
+ Since a complete domain name ends with the root label, this leads to
+ a printed form which ends in a dot.
+ */
+
+ const hostPattern =
+ /^(([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.?$/i;
+ return aHostName.length <= 255 && hostPattern.test(aHostName)
+ ? aHostName
+ : null;
+}
+
+/**
+ * Check if aHostName is a valid IPv4 address.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - If false, only IPv4 addresses
+ * in the common decimal format (4 components, each up to 255)
+ * will be accepted, no hex/octal formats.
+ * @returns {string} Unobscured canonicalized address if aHostName is an
+ * IPv4 address. Returns null if it's not.
+ */
+function isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) {
+ // Scammers frequently obscure the IP address by encoding each component as
+ // decimal, octal, hex or in some cases a mix match of each. There can even
+ // be less than 4 components where the last number covers the missing components.
+ // See the test at mailnews/base/test/unit/test_hostnameUtils.js for possible
+ // combinations.
+
+ if (!aHostName) {
+ return null;
+ }
+
+ // Break the IP address down into individual components.
+ let ipComponents = aHostName.split(".");
+ let componentCount = ipComponents.length;
+ if (componentCount > 4 || (componentCount < 4 && !aAllowExtendedIPFormats)) {
+ return null;
+ }
+
+ /**
+ * Checks validity of an IP address component.
+ *
+ * @param {string} aValue - The component string.
+ * @param {integer} aWidth - How many components does this string cover.
+ * @returns {integer|null} The value of the component in decimal if it is valid.
+ * Returns null if it's not.
+ */
+ const kPowersOf256 = [1, 256, 65536, 16777216, 4294967296];
+ function isLegalIPv4Component(aValue, aWidth) {
+ let component;
+ // Is the component decimal?
+ if (/^(0|([1-9][0-9]{0,9}))$/.test(aValue)) {
+ component = parseInt(aValue, 10);
+ } else if (aAllowExtendedIPFormats) {
+ // Is the component octal?
+ if (/^(0[0-7]{1,12})$/.test(aValue)) {
+ component = parseInt(aValue, 8);
+ } else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) {
+ // The component is hex.
+ component = parseInt(aValue, 16);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ // Make sure the component in not larger than the expected maximum.
+ if (component >= kPowersOf256[aWidth]) {
+ return null;
+ }
+
+ return component;
+ }
+
+ for (let i = 0; i < componentCount; i++) {
+ // If we are on the last supplied component but we do not have 4,
+ // the last one covers the remaining ones.
+ let componentWidth = i == componentCount - 1 ? 4 - i : 1;
+ let componentValue = isLegalIPv4Component(ipComponents[i], componentWidth);
+ if (componentValue == null) {
+ return null;
+ }
+
+ // If we have a component spanning multiple ones, split it.
+ for (let j = 0; j < componentWidth; j++) {
+ ipComponents[i + j] =
+ (componentValue >> ((componentWidth - 1 - j) * 8)) & 255;
+ }
+ }
+
+ // First component of zero is not valid.
+ if (ipComponents[0] == 0) {
+ return null;
+ }
+
+ return ipComponents.join(".");
+}
+
+/**
+ * Check if aHostName is a valid IPv6 address.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @returns {string} Unobscured canonicalized address if aHostName is an
+ * IPv6 address. Returns null if it's not.
+ */
+function isLegalIPv6Address(aHostName) {
+ if (!aHostName) {
+ return null;
+ }
+
+ // Break the IP address down into individual components.
+ let ipComponents = aHostName.toLowerCase().split(":");
+
+ // Make sure there are at least 3 components.
+ if (ipComponents.length < 3) {
+ return null;
+ }
+
+ let ipLength = ipComponents.length - 1;
+
+ // Take care if the last part is written in decimal using dots as separators.
+ let lastPart = isLegalIPv4Address(ipComponents[ipLength], false);
+ if (lastPart) {
+ let lastPartComponents = lastPart.split(".");
+ // Convert it into standard IPv6 components.
+ ipComponents[ipLength] = (
+ (lastPartComponents[0] << 8) |
+ lastPartComponents[1]
+ ).toString(16);
+ ipComponents[ipLength + 1] = (
+ (lastPartComponents[2] << 8) |
+ lastPartComponents[3]
+ ).toString(16);
+ }
+
+ // Make sure that there is only one empty component.
+ let emptyIndex;
+ for (let i = 1; i < ipComponents.length - 1; i++) {
+ if (ipComponents[i] == "") {
+ // If we already found an empty component return null.
+ if (emptyIndex) {
+ return null;
+ }
+
+ emptyIndex = i;
+ }
+ }
+
+ // If we found an empty component, extend it.
+ if (emptyIndex) {
+ ipComponents[emptyIndex] = 0;
+
+ // Add components so we have a total of 8.
+ for (let count = ipComponents.length; count < 8; count++) {
+ ipComponents.splice(emptyIndex, 0, 0);
+ }
+ }
+
+ // Make sure there are 8 components.
+ if (ipComponents.length != 8) {
+ return null;
+ }
+
+ // Format all components to 4 character hex value.
+ for (let i = 0; i < ipComponents.length; i++) {
+ if (ipComponents[i] == "") {
+ ipComponents[i] = 0;
+ }
+
+ // Make sure the component is a number and it isn't larger than 0xffff.
+ if (/^[0-9a-f]{1,4}$/.test(ipComponents[i])) {
+ ipComponents[i] = parseInt(ipComponents[i], 16);
+ if (isNaN(ipComponents[i]) || ipComponents[i] > 0xffff) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ // Pad the component with 0:s.
+ ipComponents[i] = ("0000" + ipComponents[i].toString(16)).substr(-4);
+ }
+
+ // TODO: support Zone indices in Link-local addresses? Currently they are rejected.
+ // http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices
+
+ let hostName = ipComponents.join(":");
+ // Treat 0000:0000:0000:0000:0000:0000:0000:0000 as an invalid IPv6 address.
+ return hostName != "0000:0000:0000:0000:0000:0000:0000:0000"
+ ? hostName
+ : null;
+}
+
+/**
+ * Check if aHostName is a valid IP address (IPv4 or IPv6).
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in
+ * addition to decimal.
+ * @returns {?string} Unobscured canonicalized IPv4 or IPv6 address if it is
+ * valid, otherwise null.
+ */
+function isLegalIPAddress(aHostName, aAllowExtendedIPFormats) {
+ return (
+ isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) ||
+ isLegalIPv6Address(aHostName)
+ );
+}
+
+/**
+ * Check if aIPAddress is a local or private IP address.
+ * Note: if the passed in address is not in canonical (unobscured form),
+ * the result may be wrong.
+ *
+ * @param {string} aIPAddress - A valid IP address literal in canonical
+ * (unobscured) form.
+ * @returns {boolean} frue if it is a local/private IPv4 or IPv6 address.
+ */
+function isLegalLocalIPAddress(aIPAddress) {
+ // IPv4 address?
+ let ipComponents = aIPAddress.split(".");
+ if (ipComponents.length == 4) {
+ // Check if it's a local or private IPv4 address.
+ return (
+ ipComponents[0] == 10 ||
+ ipComponents[0] == 127 || // loopback address
+ (ipComponents[0] == 192 && ipComponents[1] == 168) ||
+ (ipComponents[0] == 169 && ipComponents[1] == 254) ||
+ (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32)
+ );
+ }
+
+ // IPv6 address?
+ ipComponents = aIPAddress.split(":");
+ if (ipComponents.length == 8) {
+ // ::1/128 - localhost
+ if (
+ ipComponents[0] == "0000" &&
+ ipComponents[1] == "0000" &&
+ ipComponents[2] == "0000" &&
+ ipComponents[3] == "0000" &&
+ ipComponents[4] == "0000" &&
+ ipComponents[5] == "0000" &&
+ ipComponents[6] == "0000" &&
+ ipComponents[7] == "0001"
+ ) {
+ return true;
+ }
+
+ // fe80::/10 - link local addresses
+ if (ipComponents[0] == "fe80") {
+ return true;
+ }
+
+ // fc00::/7 - unique local addresses
+ if (
+ ipComponents[0].startsWith("fc") || // usage has not been defined yet
+ ipComponents[0].startsWith("fd")
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
+/**
+ * Clean up the hostname or IP. Usually used to sanitize a value input by the user.
+ * It is usually applied before we know if the hostname is even valid.
+ *
+ * @param {string} aHostName - The hostname or IP string to clean up.
+ */
+function cleanUpHostName(aHostName) {
+ // TODO: Bug 235312: if UTF8 string was input, convert to punycode using convertUTF8toACE()
+ // but bug 563172 needs resolving first.
+ return aHostName.trim();
+}
diff --git a/comm/mailnews/base/src/mailstoreConverter.jsm b/comm/mailnews/base/src/mailstoreConverter.jsm
new file mode 100644
index 0000000000..6e5be5ebe1
--- /dev/null
+++ b/comm/mailnews/base/src/mailstoreConverter.jsm
@@ -0,0 +1,339 @@
+/* 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 EXPORTED_SYMBOLS = ["convertMailStoreTo", "terminateWorkers"];
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+let log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+
+let gConverterWorker = null;
+
+/**
+ * Sets a server to use a different type of mailstore, converting
+ * all the existing data.
+ *
+ * @param {string} aMailstoreContractId - XPCOM id of new mailstore type.
+ * @param {nsIMsgServer} aServer - server to migrate.
+ * @param {?Element} aEventTarget - If set, element to send progress events.
+ *
+ * @returns {Promise<string>} - Resolves with a string containing the new root
+ * directory for the migrated server.
+ * Rejects with an error message.
+ */
+function convertMailStoreTo(aMailstoreContractId, aServer, aEventTarget) {
+ let accountRootFolder = aServer.rootFolder.filePath;
+
+ let srcType = null;
+ let destType = null;
+ if (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1") {
+ srcType = "maildir";
+ destType = "mbox";
+ } else {
+ srcType = "mbox";
+ destType = "maildir";
+ }
+
+ // Go offline before conversion, so there aren't messages coming in during
+ // the process.
+ Services.io.offline = true;
+ let destDir = createTmpConverterFolder(
+ accountRootFolder,
+ aMailstoreContractId
+ );
+
+ // Return a promise that will complete once the worker is done.
+ return new Promise(function (resolve, reject) {
+ let worker = new ChromeWorker("resource:///modules/converterWorker.js");
+ gConverterWorker = worker;
+
+ // Helper to log error, clean up and reject with error message.
+ let bailout = function (errmsg) {
+ log.error("bailing out (" + errmsg + ")");
+ // Cleanup.
+ log.info("Trying to remove converter folder: " + destDir.path);
+ destDir.remove(true);
+ reject(errmsg);
+ };
+
+ // Handle exceptions thrown by the worker thread.
+ worker.addEventListener("error", function (e) {
+ // (e is type ErrorEvent)
+
+ // if we're lucky, the error will contain location info
+ if (e.filename && e.lineno) {
+ bailout(e.filename + ":" + e.lineno + ": " + e.message);
+ } else {
+ bailout(e.message);
+ }
+ });
+
+ // Handle updates from the worker thread.
+ worker.addEventListener("message", function (e) {
+ let response = e.data;
+ // log.debug("WORKER SAYS: " + JSON.stringify(response) + "\n");
+ if (response.msg == "progress") {
+ let val = response.val;
+ let total = response.total;
+
+ // Send the percentage completion to the GUI.
+ // XXX TODO: should probably check elapsed time, and throttle
+ // the events to avoid spending all our time drawing!
+ let ev = new Event("progress");
+ ev.detail = parseInt((val / total) * 100);
+ if (aEventTarget) {
+ aEventTarget.dispatchEvent(ev);
+ }
+ }
+ if (response.msg == "success") {
+ // If we receive this, the worker has completed, without errors.
+ let storeTypeIDs = {
+ mbox: "@mozilla.org/msgstore/berkeleystore;1",
+ maildir: "@mozilla.org/msgstore/maildirstore;1",
+ };
+ let newStoreTypeID = storeTypeIDs[destType];
+
+ try {
+ let finalRoot = installNewRoot(aServer, destDir, newStoreTypeID);
+ log.info(
+ "Conversion complete. Converted dir installed as: " + finalRoot
+ );
+ resolve(finalRoot);
+ } catch (e) {
+ bailout("installNewRoot() failed");
+ }
+ }
+ });
+
+ // Kick off the worker.
+ worker.postMessage({
+ srcType,
+ destType,
+ srcRoot: accountRootFolder.path,
+ destRoot: destDir.path,
+ });
+ });
+}
+
+/**
+ * Checks if Converter folder exists in tmp dir, removes it and creates a new
+ * "Converter" folder.
+ *
+ * @param {nsIFile} aFolder - account root folder.
+ * @param {string} aMailstoreContractId - XPCOM id of dest mailstore type
+ *
+ * @returns {nsIFile} - the new tmp directory to use as converter dest.
+ */
+function createTmpConverterFolder(aFolder, aMailstoreContractId) {
+ let tmpDir = FileUtils.getDir("TmpD", [], false);
+ let tmpFolder;
+ switch (aMailstoreContractId) {
+ case "@mozilla.org/msgstore/maildirstore;1": {
+ if (aFolder.leafName.substr(-8) == "-maildir") {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(
+ tmpDir.path,
+ aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox"
+ )
+ );
+ } else {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(tmpDir.path, aFolder.leafName + "-mbox")
+ );
+ }
+
+ if (tmpFolder.exists()) {
+ log.info(
+ "Temporary Converter folder " +
+ tmpFolder.path +
+ " exists in tmp dir. Removing it"
+ );
+ tmpFolder.remove(true);
+ }
+ return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+ }
+
+ case "@mozilla.org/msgstore/berkeleystore;1": {
+ if (aFolder.leafName.substr(-5) == "-mbox") {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(
+ tmpDir.path,
+ aFolder.leafName.substr(0, aFolder.leafName.length - 5) + "-maildir"
+ )
+ );
+ } else {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(tmpDir.path, aFolder.leafName + "-maildir")
+ );
+ }
+
+ if (tmpFolder.exists()) {
+ log.info(
+ "Temporary Converter folder " +
+ tmpFolder.path +
+ "exists in tmp dir. Removing it"
+ );
+ tmpFolder.remove(true);
+ }
+ return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+ }
+
+ default: {
+ throw new Error(
+ "Unexpected mailstoreContractId: " + aMailstoreContractId
+ );
+ }
+ }
+}
+
+/**
+ * Switch server over to use the newly-converted directory tree.
+ * Moves the converted directory into an appropriate place for the server.
+ *
+ * @param {nsIMsgServer} server - server to migrate.
+ * @param {string} dir - dir of converted mailstore to install
+ * (will be moved by this function).
+ * @param {string} newStoreTypeID - XPCOM id of new mailstore type.
+ * @returns {string} new location of dir.
+ */
+function installNewRoot(server, dir, newStoreTypeID) {
+ let accountRootFolder = server.rootFolder.filePath;
+
+ // Migration is complete, get path of parent of account root
+ // folder into "parentPath" check if Converter folder already
+ // exists in "parentPath". If yes, remove it.
+ let lastSlash = accountRootFolder.path.lastIndexOf("/");
+ let parentPath = accountRootFolder.parent.path;
+ log.info("Path to parent folder of account root folder: " + parentPath);
+
+ let parent = new FileUtils.File(parentPath);
+ log.info("Path to parent folder of account root folder: " + parent.path);
+
+ let converterFolder = new FileUtils.File(
+ PathUtils.join(parent.path, dir.leafName)
+ );
+ if (converterFolder.exists()) {
+ log.info(
+ "Converter folder exists in " +
+ parentPath +
+ ". Removing already existing folder"
+ );
+ converterFolder.remove(true);
+ }
+
+ // Move Converter folder into the parent of account root folder.
+ try {
+ dir.moveTo(parent, dir.leafName);
+ // {nsIFile} new account root folder.
+ log.info("Path to new account root folder: " + converterFolder.path);
+ } catch (e) {
+ // Cleanup.
+ log.error(e);
+ log.error("Trying to remove converter folder: " + converterFolder.path);
+ converterFolder.remove(true);
+ throw e;
+ }
+
+ // If the account is imap then copy the msf file for the original
+ // root folder and rename the copy with the name of the new root
+ // folder.
+ if (server.type != "pop3" && server.type != "none") {
+ let converterFolderMsf = new FileUtils.File(
+ PathUtils.join(parent.path, dir.leafName + ".msf")
+ );
+ if (converterFolderMsf.exists()) {
+ converterFolderMsf.remove(true);
+ }
+
+ let oldRootFolderMsf = new FileUtils.File(
+ PathUtils.join(parent.path, accountRootFolder.leafName + ".msf")
+ );
+ if (oldRootFolderMsf.exists()) {
+ oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName);
+ }
+ }
+
+ if (server.type == "nntp") {
+ let converterFolderNewsrc = new FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + dir.leafName)
+ );
+ if (converterFolderNewsrc.exists()) {
+ converterFolderNewsrc.remove(true);
+ }
+ let oldNewsrc = new FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + accountRootFolder.leafName)
+ );
+ if (oldNewsrc.exists()) {
+ oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName);
+ }
+ }
+
+ server.rootFolder.filePath = converterFolder;
+ server.localPath = converterFolder;
+ log.info("Path to account root folder: " + server.rootFolder.filePath.path);
+
+ // Set various preferences.
+ let p1 = "mail.server." + server.key + ".directory";
+ let p2 = "mail.server." + server.key + ".directory-rel";
+ let p3 = "mail.server." + server.key + ".newsrc.file";
+ let p4 = "mail.server." + server.key + ".newsrc.file-rel";
+ let p5 = "mail.server." + server.key + ".storeContractID";
+
+ Services.prefs.setCharPref(p1, converterFolder.path);
+ log.info(p1 + ": " + converterFolder.path);
+
+ // The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com
+ // " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap
+ // accounts) ie the last slash "/" is followed by the root folder
+ // name. So, replace the old root folder name that follows the last
+ // slash with the new root folder name to set the correct value of
+ // directory-rel pref.
+ let directoryRel = Services.prefs.getCharPref(p2);
+ lastSlash = directoryRel.lastIndexOf("/");
+ directoryRel =
+ directoryRel.slice(0, lastSlash) + "/" + converterFolder.leafName;
+ Services.prefs.setCharPref(p2, directoryRel);
+ log.info(p2 + ": " + directoryRel);
+
+ if (server.type == "nntp") {
+ let newNewsrc = FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + converterFolder.leafName)
+ );
+ Services.prefs.setCharPref(p3, newNewsrc.path);
+
+ // The newsrc.file-rel pref is of the form "[ProfD]News/newsrc-
+ // news.mozilla.org" ie the last slash "/" is followed by the
+ // newsrc file name. So, replace the old newsrc file name that
+ // follows the last slash with the new newsrc file name to set
+ // the correct value of newsrc.file-rel pref.
+ let newsrcRel = Services.prefs.getCharPref(p4);
+ lastSlash = newsrcRel.lastIndexOf("/");
+ newsrcRel = newsrcRel.slice(0, lastSlash) + "/" + newNewsrc.leafName;
+ Services.prefs.setCharPref(p4, newsrcRel);
+ log.info(p4 + ": " + newsrcRel);
+ }
+
+ Services.prefs.setCharPref(p5, newStoreTypeID);
+
+ Services.prefs.savePrefFile(null);
+
+ return converterFolder.path;
+}
+
+/**
+ * Terminate any workers involved in the conversion process.
+ */
+function terminateWorkers() {
+ // We're only using a single worker right now.
+ if (gConverterWorker !== null) {
+ gConverterWorker.terminate();
+ gConverterWorker = null;
+ }
+}
diff --git a/comm/mailnews/base/src/moz.build b/comm/mailnews/base/src/moz.build
new file mode 100644
index 0000000000..8255f50c67
--- /dev/null
+++ b/comm/mailnews/base/src/moz.build
@@ -0,0 +1,154 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "HeaderReader.h",
+ "LineReader.h",
+ "nsImapMoveCoalescer.h",
+ "nsMailAuthModule.h",
+ "nsMailChannel.h",
+ "nsMailDirServiceDefs.h",
+ "nsMsgCompressIStream.h",
+ "nsMsgCompressOStream.h",
+ "nsMsgDBFolder.h",
+ "nsMsgEnumerator.h",
+ "nsMsgI18N.h",
+ "nsMsgIdentity.h",
+ "nsMsgIncomingServer.h",
+ "nsMsgKeySet.h",
+ "nsMsgLineBuffer.h",
+ "nsMsgMailNewsUrl.h",
+ "nsMsgProtocol.h",
+ "nsMsgReadStateTxn.h",
+ "nsMsgTxn.h",
+ "nsMsgUtils.h",
+ "nsNewMailnewsURI.h",
+ "nsQuarantinedOutputStream.h",
+ "UrlListener.h",
+]
+
+SOURCES += [
+ "MailNewsDLF.cpp",
+ "MailnewsLoadContextInfo.cpp",
+ "nsCidProtocolHandler.cpp",
+ "nsCopyMessageStreamListener.cpp",
+ "nsImapMoveCoalescer.cpp",
+ "nsMailAuthModule.cpp",
+ "nsMailChannel.cpp",
+ "nsMailDirProvider.cpp",
+ "nsMessenger.cpp",
+ "nsMessengerBootstrap.cpp",
+ "nsMsgAccount.cpp",
+ "nsMsgAccountManager.cpp",
+ "nsMsgBiffManager.cpp",
+ "nsMsgCompressIStream.cpp",
+ "nsMsgCompressOStream.cpp",
+ "nsMsgContentPolicy.cpp",
+ "nsMsgCopyService.cpp",
+ "nsMsgDBFolder.cpp",
+ "nsMsgDBView.cpp",
+ "nsMsgEnumerator.cpp",
+ "nsMsgFileStream.cpp",
+ "nsMsgFolderCache.cpp",
+ "nsMsgFolderCompactor.cpp",
+ "nsMsgFolderNotificationService.cpp",
+ "nsMsgGroupThread.cpp",
+ "nsMsgGroupView.cpp",
+ "nsMsgI18N.cpp",
+ "nsMsgIdentity.cpp",
+ "nsMsgIncomingServer.cpp",
+ "nsMsgKeySet.cpp",
+ "nsMsgLineBuffer.cpp",
+ "nsMsgMailNewsUrl.cpp",
+ "nsMsgMailSession.cpp",
+ "nsMsgOfflineManager.cpp",
+ "nsMsgProgress.cpp",
+ "nsMsgProtocol.cpp",
+ "nsMsgPurgeService.cpp",
+ "nsMsgQuickSearchDBView.cpp",
+ "nsMsgReadStateTxn.cpp",
+ "nsMsgSearchDBView.cpp",
+ "nsMsgSpecialViews.cpp",
+ "nsMsgStatusFeedback.cpp",
+ "nsMsgTagService.cpp",
+ "nsMsgThreadedDBView.cpp",
+ "nsMsgTxn.cpp",
+ "nsMsgUtils.cpp",
+ "nsMsgWindow.cpp",
+ "nsMsgXFViewThread.cpp",
+ "nsMsgXFVirtualFolderDBView.cpp",
+ "nsNewMailnewsURI.cpp",
+ "nsQuarantinedOutputStream.cpp",
+ "nsSpamSettings.cpp",
+ "nsStatusBarBiffManager.cpp",
+ "nsStopwatch.cpp",
+ "nsSubscribableServer.cpp",
+ "UrlListener.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += [
+ "nsMessengerWinIntegration.cpp",
+ # This file cannot be built in unified mode because of name clashes with Windows headers.
+ "nsUserInfoWin.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ SOURCES += [
+ "nsMessengerUnixIntegration.cpp",
+ "nsUserInfoUnix.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += [
+ "nsMessengerOSXIntegration.mm",
+ "nsUserInfoMac.mm",
+ ]
+
+EXTRA_JS_MODULES += [
+ "ABQueryUtils.jsm",
+ "converterWorker.js",
+ "FolderLookupService.jsm",
+ "FolderUtils.jsm",
+ "hostnameUtils.jsm",
+ "JXON.jsm",
+ "LineReader.jsm",
+ "MailAuthenticator.jsm",
+ "MailChannel.sys.mjs",
+ "MailCryptoUtils.jsm",
+ "MailnewsMigrator.jsm",
+ "MailNotificationManager.jsm",
+ "MailNotificationService.jsm",
+ "MailServices.jsm",
+ "mailstoreConverter.jsm",
+ "MailStringUtils.jsm",
+ "MsgAsyncPrompter.jsm",
+ "MsgDBCacheManager.jsm",
+ "MsgIncomingServer.jsm",
+ "MsgKeySet.jsm",
+ "MsgProtocolInfo.sys.mjs",
+ "OAuth2.jsm",
+ "OAuth2Module.jsm",
+ "OAuth2Providers.jsm",
+ "TemplateUtils.jsm",
+ "VirtualFolderWrapper.jsm",
+ "WinUnreadBadge.jsm",
+]
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/netwerk/base",
+ "/toolkit/components/jsoncpp/include",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "mail"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.cpp b/comm/mailnews/base/src/nsCidProtocolHandler.cpp
new file mode 100644
index 0000000000..50f14bd782
--- /dev/null
+++ b/comm/mailnews/base/src/nsCidProtocolHandler.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCidProtocolHandler.h"
+#include "nsString.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsComponentManagerUtils.h"
+
+nsCidProtocolHandler::nsCidProtocolHandler() {}
+
+nsCidProtocolHandler::~nsCidProtocolHandler() {}
+
+NS_IMPL_ISUPPORTS(nsCidProtocolHandler, nsIProtocolHandler)
+
+NS_IMETHODIMP nsCidProtocolHandler::GetScheme(nsACString& aScheme) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsCidProtocolHandler::NewURI(const nsACString& aSpec,
+ const char* aOriginCharset,
+ nsIURI* aBaseURI, nsIURI** _retval) {
+ // the right fix is to use the baseSpec (or aBaseUri)
+ // and specify the cid, and then fix mime
+ // to handle that, like it does with "...&part=1.2"
+ // for now, do about blank to prevent spam
+ // from popping up annoying alerts about not implementing the cid
+ // protocol
+ nsCOMPtr<nsIURI> url;
+ nsresult rv = NS_NewURI(getter_AddRefs(url), "about:blank");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ url.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCidProtocolHandler::NewChannel(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo,
+ nsIChannel** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsCidProtocolHandler::AllowPort(int32_t port, const char* scheme,
+ bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.h b/comm/mailnews/base/src/nsCidProtocolHandler.h
new file mode 100644
index 0000000000..9d84812ee7
--- /dev/null
+++ b/comm/mailnews/base/src/nsCidProtocolHandler.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsCidProtocolHandler_h__
+#define nsCidProtocolHandler_h__
+
+#include "nsCOMPtr.h"
+#include "nsIProtocolHandler.h"
+
+class nsCidProtocolHandler : public nsIProtocolHandler {
+ public:
+ nsCidProtocolHandler();
+ static nsresult NewURI(const nsACString& aSpec, const char* aOriginCharset,
+ nsIURI* aBaseURI, nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+
+ private:
+ virtual ~nsCidProtocolHandler();
+};
+
+#endif /* nsCidProtocolHandler_h__ */
diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp
new file mode 100644
index 0000000000..343c27ac54
--- /dev/null
+++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCopyMessageStreamListener.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+
+NS_IMPL_ISUPPORTS(nsCopyMessageStreamListener, nsIStreamListener,
+ nsIRequestObserver, nsICopyMessageStreamListener)
+
+nsCopyMessageStreamListener::nsCopyMessageStreamListener() {}
+
+nsCopyMessageStreamListener::~nsCopyMessageStreamListener() {
+ // All member variables are nsCOMPtr's.
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::Init(
+ nsICopyMessageListener* destination) {
+ mDestination = destination;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::StartMessage() {
+ if (mDestination) {
+ return mDestination->StartMessage();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::EndMessage(nsMsgKey key) {
+ if (mDestination) {
+ return mDestination->EndMessage(key);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnDataAvailable(
+ nsIRequest* /* request */, nsIInputStream* aIStream, uint64_t sourceOffset,
+ uint32_t aLength) {
+ return mDestination->CopyData(aIStream, aLength);
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnStartRequest(nsIRequest* request) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIURI> uri;
+
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ if (NS_SUCCEEDED(rv)) rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_SUCCEEDED(rv)) rv = mDestination->BeginCopy();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::EndCopy(nsIURI* uri,
+ nsresult status) {
+ nsresult rv;
+ bool copySucceeded = (status == NS_BINDING_SUCCEEDED);
+ rv = mDestination->EndCopy(copySucceeded);
+ // If this is a move and we finished the copy, delete the old message.
+ bool moveMessage = false;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailURL(do_QueryInterface(uri));
+ if (mailURL) rv = mailURL->IsUrlType(nsIMsgMailNewsUrl::eMove, &moveMessage);
+
+ if (NS_FAILED(rv)) moveMessage = false;
+
+ // OK, this is wrong if we're moving to an imap folder, for example. This
+ // really says that we were able to pull the message from the source, NOT that
+ // we were able to put it in the destination!
+ if (moveMessage) {
+ // don't do this if we're moving to an imap folder - that's handled
+ // elsewhere.
+ nsCOMPtr<nsIMsgImapMailFolder> destImap = do_QueryInterface(mDestination);
+ // if the destination is a local folder, it will handle the delete from the
+ // source in EndMove
+ if (!destImap) rv = mDestination->EndMove(copySucceeded);
+ }
+ // Even if the above actions failed we probably still want to return NS_OK.
+ // There should probably be some error dialog if either the copy or delete
+ // failed.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsresult rv;
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return EndCopy(uri, aStatus);
+}
diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.h b/comm/mailnews/base/src/nsCopyMessageStreamListener.h
new file mode 100644
index 0000000000..509dcef019
--- /dev/null
+++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NSCOPYMESSAGESTREAMLISTENER_H
+#define NSCOPYMESSAGESTREAMLISTENER_H
+
+#include "nsICopyMessageStreamListener.h"
+#include "nsIStreamListener.h"
+#include "nsICopyMessageListener.h"
+#include "nsCOMPtr.h"
+
+class nsCopyMessageStreamListener : public nsIStreamListener,
+ public nsICopyMessageStreamListener {
+ public:
+ nsCopyMessageStreamListener();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICOPYMESSAGESTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ protected:
+ virtual ~nsCopyMessageStreamListener();
+ nsCOMPtr<nsICopyMessageListener> mDestination;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.cpp b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp
new file mode 100644
index 0000000000..160f204a61
--- /dev/null
+++ b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsImapMoveCoalescer.h"
+#include "nsIImapService.h"
+#include "nsIMsgCopyService.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/ArrayUtils.h"
+
+NS_IMPL_ISUPPORTS(nsImapMoveCoalescer, nsIUrlListener)
+
+nsImapMoveCoalescer::nsImapMoveCoalescer(nsIMsgFolder* sourceFolder,
+ nsIMsgWindow* msgWindow) {
+ m_sourceFolder = sourceFolder;
+ m_msgWindow = msgWindow;
+ m_hasPendingMoves = false;
+}
+
+nsImapMoveCoalescer::~nsImapMoveCoalescer() {}
+
+nsresult nsImapMoveCoalescer::AddMove(nsIMsgFolder* folder, nsMsgKey key) {
+ m_hasPendingMoves = true;
+ int32_t folderIndex = m_destFolders.IndexOf(folder);
+ nsTArray<nsMsgKey>* keysToAdd = nullptr;
+
+ if (folderIndex >= 0)
+ keysToAdd = &(m_sourceKeyArrays[folderIndex]);
+ else {
+ m_destFolders.AppendObject(folder);
+ keysToAdd = m_sourceKeyArrays.AppendElement();
+ if (!keysToAdd) return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!keysToAdd->Contains(key)) keysToAdd->AppendElement(key);
+
+ return NS_OK;
+}
+
+nsresult nsImapMoveCoalescer::PlaybackMoves(
+ bool doNewMailNotification /* = false */) {
+ int32_t numFolders = m_destFolders.Count();
+ // Nothing to do, so don't change the member variables.
+ if (numFolders == 0) return NS_OK;
+
+ nsresult rv = NS_OK;
+ m_hasPendingMoves = false;
+ m_doNewMailNotification = doNewMailNotification;
+ m_outstandingMoves = 0;
+
+ for (int32_t i = 0; i < numFolders; ++i) {
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // is this the right place to make sure dest folder exists
+ // (and has proper flags?), before we start copying?
+ nsCOMPtr<nsIMsgFolder> destFolder(m_destFolders[i]);
+ nsTArray<nsMsgKey>& keysToAdd = m_sourceKeyArrays[i];
+ int32_t numNewMessages = 0;
+ int32_t numKeysToAdd = keysToAdd.Length();
+ if (numKeysToAdd == 0) continue;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages(keysToAdd.Length());
+ for (uint32_t keyIndex = 0; keyIndex < keysToAdd.Length(); keyIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr;
+ rv = m_sourceFolder->GetMessageHeader(keysToAdd.ElementAt(keyIndex),
+ getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) {
+ messages.AppendElement(mailHdr);
+ bool isRead = false;
+ mailHdr->GetIsRead(&isRead);
+ if (!isRead) numNewMessages++;
+ }
+ }
+ uint32_t destFlags;
+ destFolder->GetFlags(&destFlags);
+ if (!(destFlags &
+ nsMsgFolderFlags::Junk)) // don't set has new on junk folder
+ {
+ destFolder->SetNumNewMessages(numNewMessages);
+ if (numNewMessages > 0) destFolder->SetHasNewMessages(true);
+ }
+ // adjust the new message count on the source folder
+ int32_t oldNewMessageCount = 0;
+ m_sourceFolder->GetNumNewMessages(false, &oldNewMessageCount);
+ if (oldNewMessageCount >= numKeysToAdd)
+ oldNewMessageCount -= numKeysToAdd;
+ else
+ oldNewMessageCount = 0;
+
+ m_sourceFolder->SetNumNewMessages(oldNewMessageCount);
+
+ keysToAdd.Clear();
+ nsCOMPtr<nsIMsgCopyService> copySvc =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ if (copySvc) {
+ nsCOMPtr<nsIMsgCopyServiceListener> listener;
+ if (m_doNewMailNotification) {
+ nsMoveCoalescerCopyListener* copyListener =
+ new nsMoveCoalescerCopyListener(this, destFolder);
+ if (copyListener) listener = copyListener;
+ }
+ rv = copySvc->CopyMessages(m_sourceFolder, messages, destFolder, true,
+ listener, m_msgWindow, false /*allowUndo*/);
+ if (NS_SUCCEEDED(rv)) m_outstandingMoves++;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMoveCoalescer::OnStartRunningUrl(nsIURI* aUrl) {
+ NS_ASSERTION(aUrl, "just a sanity check");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMoveCoalescer::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ m_outstandingMoves--;
+ if (m_doNewMailNotification && !m_outstandingMoves) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_sourceFolder);
+ if (imapFolder) imapFolder->NotifyIfNewMail();
+ }
+ return NS_OK;
+}
+
+nsTArray<nsMsgKey>* nsImapMoveCoalescer::GetKeyBucket(uint32_t keyArrayIndex) {
+ NS_ASSERTION(keyArrayIndex < MOZ_ARRAY_LENGTH(m_keyBuckets), "invalid index");
+
+ return keyArrayIndex < mozilla::ArrayLength(m_keyBuckets)
+ ? &(m_keyBuckets[keyArrayIndex])
+ : nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsMoveCoalescerCopyListener, nsIMsgCopyServiceListener)
+
+nsMoveCoalescerCopyListener::nsMoveCoalescerCopyListener(
+ nsImapMoveCoalescer* coalescer, nsIMsgFolder* destFolder) {
+ m_destFolder = destFolder;
+ m_coalescer = coalescer;
+}
+
+nsMoveCoalescerCopyListener::~nsMoveCoalescerCopyListener() {}
+
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStartCopy() {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void SetMessageKey (in uint32_t aKey); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::SetMessageKey(uint32_t aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void GetMessageId (in nsACString aMessageId); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::GetMessageId(nsACString& messageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnStopCopy (in nsresult aStatus); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStopCopy(nsresult aStatus) {
+ nsresult rv = NS_OK;
+ if (NS_SUCCEEDED(aStatus)) {
+ // if the dest folder is imap, update it.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_destFolder);
+ if (imapFolder) {
+ uint32_t folderFlags;
+ m_destFolder->GetFlags(&folderFlags);
+ if (!(folderFlags & (nsMsgFolderFlags::Junk | nsMsgFolderFlags::Trash))) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> url;
+ rv = imapService->SelectFolder(m_destFolder, m_coalescer, nullptr,
+ getter_AddRefs(url));
+ }
+ } else // give junk filters a chance to run on new msgs in destination
+ // local folder
+ {
+ bool filtersRun;
+ m_destFolder->CallFilterPlugins(nullptr, &filtersRun);
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.h b/comm/mailnews/base/src/nsImapMoveCoalescer.h
new file mode 100644
index 0000000000..5471c4bd88
--- /dev/null
+++ b/comm/mailnews/base/src/nsImapMoveCoalescer.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsImapMoveCoalescer_H
+#define _nsImapMoveCoalescer_H
+
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgWindow.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgCopyServiceListener.h"
+
+// imap move coalescer class - in order to keep nsImapMailFolder from growing
+// like Topsy Logically, we want to keep track of an nsTArray<nsMsgKey> per
+// nsIMsgFolder, and then be able to retrieve them one by one and play back the
+// moves. This utility class will be used by both the filter code and the
+// offline playback code, to avoid multiple moves to the same folder.
+
+class nsImapMoveCoalescer : public nsIUrlListener {
+ public:
+ friend class nsMoveCoalescerCopyListener;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ nsImapMoveCoalescer(nsIMsgFolder* sourceFolder, nsIMsgWindow* msgWindow);
+
+ nsresult AddMove(nsIMsgFolder* folder, nsMsgKey key);
+ nsresult PlaybackMoves(bool doNewMailNotification = false);
+ // this lets the caller store keys in an arbitrary number of buckets. If the
+ // bucket for the passed in index doesn't exist, it will get created.
+ nsTArray<nsMsgKey>* GetKeyBucket(uint32_t keyArrayIndex);
+ nsIMsgWindow* GetMsgWindow() { return m_msgWindow; }
+ bool HasPendingMoves() { return m_hasPendingMoves; }
+
+ protected:
+ virtual ~nsImapMoveCoalescer();
+ // m_sourceKeyArrays and m_destFolders are parallel arrays.
+ nsTArray<nsTArray<nsMsgKey> > m_sourceKeyArrays;
+ nsCOMArray<nsIMsgFolder> m_destFolders;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgFolder> m_sourceFolder;
+ bool m_doNewMailNotification;
+ bool m_hasPendingMoves;
+ nsTArray<nsMsgKey> m_keyBuckets[2];
+ int32_t m_outstandingMoves;
+};
+
+class nsMoveCoalescerCopyListener final : public nsIMsgCopyServiceListener {
+ public:
+ nsMoveCoalescerCopyListener(nsImapMoveCoalescer* coalescer,
+ nsIMsgFolder* destFolder);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ nsCOMPtr<nsIMsgFolder> m_destFolder;
+
+ nsImapMoveCoalescer* m_coalescer;
+ // when we get OnStopCopy, update the folder. When we've finished all the
+ // copies, send the biff notification.
+
+ private:
+ ~nsMoveCoalescerCopyListener();
+};
+
+#endif // _nsImapMoveCoalescer_H
diff --git a/comm/mailnews/base/src/nsMailAuthModule.cpp b/comm/mailnews/base/src/nsMailAuthModule.cpp
new file mode 100644
index 0000000000..69089737df
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailAuthModule.cpp
@@ -0,0 +1,85 @@
+/* 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/. */
+
+#include "nsIAuthModule.h"
+#include "nsIMailAuthModule.h"
+#include "nsMailAuthModule.h"
+#include "nsString.h"
+#include "plbase64.h"
+
+NS_IMPL_ISUPPORTS(nsMailAuthModule, nsIMailAuthModule)
+
+nsMailAuthModule::nsMailAuthModule() {}
+
+nsMailAuthModule::~nsMailAuthModule() {}
+
+/**
+ * A simple wrap of CreateInstance and Init of nsIAuthModule.
+ */
+NS_IMETHODIMP
+nsMailAuthModule::Init(const char* type, const nsACString& serviceName,
+ uint32_t serviceFlags, const nsAString& domain,
+ const nsAString& username, const nsAString& password) {
+ mAuthModule = nsIAuthModule::CreateInstance(type);
+ return mAuthModule->Init(serviceName, serviceFlags, domain, username,
+ password);
+}
+
+/**
+ * A wrap of nsIAuthModule::GetNextToken with two extra processings:
+ * 1. inToken is base64 decoded then passed to nsIAuthModule::GetNextToken.
+ * 2. The out value of nsIAuthModule::GetNextToken is base64 encoded then
+ * assigned to outToken.
+ */
+NS_IMETHODIMP
+nsMailAuthModule::GetNextToken(const nsACString& inToken,
+ nsACString& outToken) {
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen = 0, outBufLen = 0;
+ uint32_t len = inToken.Length();
+ if (len > 0) {
+ // Decode into the input buffer.
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+
+ // Strip off any padding (see bug 230351).
+ char* challenge = ToNewCString(inToken);
+ while (challenge[len - 1] == '=') len--;
+
+ // We need to know the exact length of the decoded string to give to
+ // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable
+ // of telling us that. So, we figure it out for ourselves.
+
+ // For every 4 characters, add 3 to the destination
+ // If there are 3 remaining, add 2
+ // If there are 2 remaining, add 1
+ // 1 remaining is an error
+ inBufLen =
+ (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0);
+ PL_Base64Decode(challenge, len, (char*)inBuf);
+ free(challenge);
+ } else {
+ inBufLen = 0;
+ inBuf = NULL;
+ }
+
+ rv = mAuthModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen);
+ free(inBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // It's not an error if outBuf is empty, return an empty string as reply to
+ // the server.
+ if (outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str) {
+ outToken.Adopt(base64Str);
+ } else {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+ free(outBuf);
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMailAuthModule.h b/comm/mailnews/base/src/nsMailAuthModule.h
new file mode 100644
index 0000000000..e6bca30623
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailAuthModule.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMailAuthModule_h__
+#define nsMailAuthModule_h__
+
+#include "nsCOMPtr.h"
+#include "nsIAuthModule.h"
+#include "nsIMailAuthModule.h"
+
+class nsMailAuthModule : public nsIMailAuthModule {
+ public:
+ nsMailAuthModule();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMAILAUTHMODULE
+
+ protected:
+ virtual ~nsMailAuthModule();
+
+ private:
+ nsCOMPtr<nsIAuthModule> mAuthModule;
+};
+
+#endif /* nsMailAuthModule_h__ */
diff --git a/comm/mailnews/base/src/nsMailChannel.cpp b/comm/mailnews/base/src/nsMailChannel.cpp
new file mode 100644
index 0000000000..1e427718e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailChannel.cpp
@@ -0,0 +1,139 @@
+/* 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/. */
+
+#include "nsMailChannel.h"
+#include "nsHashPropertyBag.h"
+#include "nsServiceManagerUtils.h"
+#include "nsICharsetConverterManager.h"
+
+NS_IMETHODIMP
+nsMailChannel::AddHeaderFromMIME(const nsACString& name,
+ const nsACString& value) {
+ mHeaderNames.AppendElement(name);
+ mHeaderValues.AppendElement(value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetHeaderNames(nsTArray<nsCString>& aHeaderNames) {
+ aHeaderNames = mHeaderNames.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetHeaderValues(nsTArray<nsCString>& aHeaderValues) {
+ aHeaderValues = mHeaderValues.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::HandleAttachmentFromMIME(const nsACString& contentType,
+ const nsACString& url,
+ const nsACString& displayName,
+ const nsACString& uri,
+ bool notDownloaded) {
+ RefPtr<nsIWritablePropertyBag2> attachment = new nsHashPropertyBag();
+ attachment->SetPropertyAsAUTF8String(u"contentType"_ns, contentType);
+ attachment->SetPropertyAsAUTF8String(u"url"_ns, url);
+ attachment->SetPropertyAsAUTF8String(u"displayName"_ns, displayName);
+ attachment->SetPropertyAsAUTF8String(u"uri"_ns, uri);
+ attachment->SetPropertyAsBool(u"notDownloaded"_ns, notDownloaded);
+ mAttachments.AppendElement(attachment);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::AddAttachmentFieldFromMIME(const nsACString& field,
+ const nsACString& value) {
+ nsIWritablePropertyBag2* attachment = mAttachments.LastElement();
+ attachment->SetPropertyAsAUTF8String(NS_ConvertUTF8toUTF16(nsCString(field)),
+ value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetAttachments(
+ nsTArray<RefPtr<nsIPropertyBag2> >& aAttachments) {
+ aAttachments.Clear();
+ for (nsIWritablePropertyBag2* attachment : mAttachments) {
+ aAttachments.AppendElement(static_cast<nsIPropertyBag2*>(attachment));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetMailCharacterSet(nsACString& aMailCharacterSet) {
+ aMailCharacterSet = mMailCharacterSet;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetMailCharacterSet(const nsACString& aMailCharacterSet) {
+ mMailCharacterSet = aMailCharacterSet;
+
+ // Convert to a canonical charset name instead of using the charset name from
+ // the message header as is. This is needed for charset menu item to have a
+ // check mark correctly.
+ nsresult rv;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return ccm->GetCharsetAlias(PromiseFlatCString(aMailCharacterSet).get(),
+ mMailCharacterSet);
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetImipMethod(nsACString& aImipMethod) {
+ aImipMethod = mImipMethod;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetImipMethod(const nsACString& aImipMethod) {
+ mImipMethod = aImipMethod;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetImipItem(calIItipItem** aImipItem) {
+ NS_IF_ADDREF(*aImipItem = mImipItem);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetImipItem(calIItipItem* aImipItem) {
+ mImipItem = aImipItem;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetSmimeHeaderSink(nsIMsgSMIMEHeaderSink** aSmimeHeaderSink) {
+ NS_IF_ADDREF(*aSmimeHeaderSink = mSmimeHeaderSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetSmimeHeaderSink(nsIMsgSMIMEHeaderSink* aSmimeHeaderSink) {
+ mSmimeHeaderSink = aSmimeHeaderSink;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetListener(nsIMailProgressListener** aListener) {
+ nsCOMPtr<nsIMailProgressListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ NS_IF_ADDREF(*aListener = listener);
+ } else {
+ *aListener = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetListener(nsIMailProgressListener* aListener) {
+ nsresult rv;
+ mListener = do_GetWeakReference(aListener, &rv);
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMailChannel.h b/comm/mailnews/base/src/nsMailChannel.h
new file mode 100644
index 0000000000..91cf59705a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailChannel.h
@@ -0,0 +1,30 @@
+/* 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/. */
+
+#ifndef nsMailChannel_h__
+#define nsMailChannel_h__
+
+#include "nsIMailChannel.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsTArray.h"
+#include "nsTString.h"
+#include "calIItipItem.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMailChannel : public nsIMailChannel {
+ public:
+ NS_DECL_NSIMAILCHANNEL
+
+ protected:
+ nsTArray<nsCString> mHeaderNames;
+ nsTArray<nsCString> mHeaderValues;
+ nsTArray<RefPtr<nsIWritablePropertyBag2>> mAttachments;
+ nsCString mMailCharacterSet;
+ nsCString mImipMethod;
+ nsCOMPtr<calIItipItem> mImipItem;
+ nsCOMPtr<nsIMsgSMIMEHeaderSink> mSmimeHeaderSink;
+ nsWeakPtr mListener;
+};
+
+#endif /* nsMailChannel_h__ */
diff --git a/comm/mailnews/base/src/nsMailDirProvider.cpp b/comm/mailnews/base/src/nsMailDirProvider.cpp
new file mode 100644
index 0000000000..5d092203cf
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirProvider.cpp
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMailDirProvider.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsXULAppAPI.h"
+#include "nsCOMArray.h"
+#include "nsEnumeratorUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIChromeRegistry.h"
+#include "nsICategoryManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDirectoryServiceUtils.h"
+
+#define MAIL_DIR_50_NAME "Mail"
+#define IMAP_MAIL_DIR_50_NAME "ImapMail"
+#define NEWS_DIR_50_NAME "News"
+
+nsresult nsMailDirProvider::EnsureDirectory(nsIFile* aDirectory) {
+ bool exists;
+ nsresult rv = aDirectory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists) rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0700);
+
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsMailDirProvider, nsIDirectoryServiceProvider,
+ nsIDirectoryServiceProvider2)
+
+NS_IMETHODIMP
+nsMailDirProvider::GetFile(const char* aKey, bool* aPersist,
+ nsIFile** aResult) {
+ // NOTE: This function can be reentrant through the NS_GetSpecialDirectory
+ // call, so be careful not to cause infinite recursion.
+ // i.e. the check for supported files must come first.
+ const char* leafName = nullptr;
+ bool isDirectory = true;
+
+ if (!strcmp(aKey, NS_APP_MAIL_50_DIR)) {
+ leafName = MAIL_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_IMAP_MAIL_50_DIR)) {
+ leafName = IMAP_MAIL_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_NEWS_50_DIR)) {
+ leafName = NEWS_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_MESSENGER_FOLDER_CACHE_50_FILE)) {
+ isDirectory = false;
+ leafName = "folderCache.json";
+ } else if (!strcmp(aKey, NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE)) {
+ isDirectory = false;
+ leafName = "panacea.dat";
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> parentDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(parentDir));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFile> file;
+ rv = parentDir->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ nsDependentCString leafStr(leafName);
+ rv = file->AppendNative(leafStr);
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ if (isDirectory && NS_SUCCEEDED(file->Exists(&exists)) && !exists)
+ rv = EnsureDirectory(file);
+
+ *aPersist = true;
+ file.forget(aResult);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::GetFiles(const char* aKey, nsISimpleEnumerator** aResult) {
+ if (strcmp(aKey, ISP_DIRECTORY_LIST) != 0) return NS_ERROR_FAILURE;
+
+ // The list of isp directories includes the isp directory
+ // in the current process dir (i.e. <path to thunderbird.exe>\isp and
+ // <path to thunderbird.exe>\isp\locale
+ // and isp and isp\locale for each active extension
+
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ if (!dirSvc) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIFile> currentProcessDir;
+ nsresult rv = dirSvc->Get(NS_XPCOM_CURRENT_PROCESS_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(currentProcessDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> directoryEnumerator;
+ rv = NS_NewSingletonEnumerator(getter_AddRefs(directoryEnumerator),
+ currentProcessDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = new AppendingEnumerator(directoryEnumerator));
+ return NS_SUCCESS_AGGREGATE_RESULT;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::AppendingEnumerator::HasMoreElements(bool* aResult) {
+ *aResult = mNext || mNextWithLocale ? true : false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::AppendingEnumerator::GetNext(nsISupports** aResult) {
+ // Set the return value to the next directory we want to enumerate over
+ if (aResult) NS_ADDREF(*aResult = mNext);
+
+ if (mNextWithLocale) {
+ mNext = mNextWithLocale;
+ mNextWithLocale = nullptr;
+ return NS_OK;
+ }
+
+ mNext = nullptr;
+
+ // Ignore all errors
+
+ bool more;
+ while (NS_SUCCEEDED(mBase->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsISupports> nextbasesupp;
+ mBase->GetNext(getter_AddRefs(nextbasesupp));
+
+ nsCOMPtr<nsIFile> nextbase(do_QueryInterface(nextbasesupp));
+ if (!nextbase) continue;
+
+ nextbase->Clone(getter_AddRefs(mNext));
+ if (!mNext) continue;
+
+ mNext->AppendNative("isp"_ns);
+ bool exists;
+ nsresult rv = mNext->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ break;
+ }
+
+ mNext = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsMailDirProvider::AppendingEnumerator::AppendingEnumerator(
+ nsISimpleEnumerator* aBase)
+ : mBase(aBase) {
+ // Initialize mNext to begin
+ GetNext(nullptr);
+}
diff --git a/comm/mailnews/base/src/nsMailDirProvider.h b/comm/mailnews/base/src/nsMailDirProvider.h
new file mode 100644
index 0000000000..e67c87a9e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirProvider.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMailDirProvider_h__
+#define nsMailDirProvider_h__
+
+#include "nsIDirectoryService.h"
+#include "nsSimpleEnumerator.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+
+class nsMailDirProvider final : public nsIDirectoryServiceProvider2 {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER2
+
+ private:
+ ~nsMailDirProvider() {}
+
+ nsresult EnsureDirectory(nsIFile* aDirectory);
+
+ class AppendingEnumerator final : public nsSimpleEnumerator {
+ public:
+ const nsID& DefaultInterface() override { return NS_GET_IID(nsIFile); }
+
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ explicit AppendingEnumerator(nsISimpleEnumerator* aBase);
+
+ private:
+ ~AppendingEnumerator() override = default;
+ nsCOMPtr<nsISimpleEnumerator> mBase;
+ nsCOMPtr<nsIFile> mNext;
+ nsCOMPtr<nsIFile> mNextWithLocale;
+ };
+};
+
+#endif // nsMailDirProvider_h__
diff --git a/comm/mailnews/base/src/nsMailDirServiceDefs.h b/comm/mailnews/base/src/nsMailDirServiceDefs.h
new file mode 100644
index 0000000000..3753c96782
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirServiceDefs.h
@@ -0,0 +1,31 @@
+/* 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/. */
+
+#ifndef nsMailDirectoryServiceDefs_h___
+#define nsMailDirectoryServiceDefs_h___
+
+//=============================================================================
+//
+// Defines property names for directories available from the mail-specific
+// nsMailDirProvider.
+//
+// System and XPCOM properties are defined in nsDirectoryServiceDefs.h.
+// General application properties are defined in nsAppDirectoryServiceDefs.h.
+//
+//=============================================================================
+
+// ----------------------------------------------------------------------------
+// Files and directories that exist on a per-profile basis.
+// ----------------------------------------------------------------------------
+
+#define NS_APP_MAIL_50_DIR "MailD"
+#define NS_APP_IMAP_MAIL_50_DIR "IMapMD"
+#define NS_APP_NEWS_50_DIR "NewsD"
+
+#define NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE "MLFCaF"
+#define NS_APP_MESSENGER_FOLDER_CACHE_50_FILE "MFCaF"
+
+#define ISP_DIRECTORY_LIST "ISPDL"
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessenger.cpp b/comm/mailnews/base/src/nsMessenger.cpp
new file mode 100644
index 0000000000..7fc5c05934
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessenger.cpp
@@ -0,0 +1,2446 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "prsystem.h"
+
+#include "nsMessenger.h"
+
+// xpcom
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsIStringStream.h"
+#include "nsLocalFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsQuickSort.h"
+#include "nsNativeCharsetUtils.h"
+#include "mozilla/Path.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+
+// necko
+#include "nsMimeTypes.h"
+#include "nsIURL.h"
+#include "nsIPrompt.h"
+#include "nsIStreamListener.h"
+#include "nsIStreamConverterService.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIMIMEInfo.h"
+
+// gecko
+#include "nsLayoutCID.h"
+#include "nsIContentViewer.h"
+
+/* for access to docshell */
+#include "nsPIDOMWindow.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIWebNavigation.h"
+#include "nsContentUtils.h"
+#include "nsDocShellLoadState.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "nsFrameLoader.h"
+#include "mozilla/dom/Document.h"
+
+// mail
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMailboxUrl.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgIncomingServer.h"
+
+#include "nsIMsgMessageService.h"
+
+#include "nsIMsgHdr.h"
+// compose
+#include "nsNativeCharsetUtils.h"
+
+// draft/folders/sendlater/etc
+#include "nsIMsgCopyService.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIUrlListener.h"
+#include "UrlListener.h"
+
+// undo
+#include "nsITransaction.h"
+#include "nsMsgTxn.h"
+
+// charset conversions
+#include "nsIMimeConverter.h"
+
+// Save As
+#include "nsIStringBundle.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIMIMEService.h"
+#include "nsITransfer.h"
+
+#define MESSENGER_SAVE_DIR_PREF_NAME "messenger.save.dir"
+#define MIMETYPE_DELETED "text/x-moz-deleted"
+#define ATTACHMENT_PERMISSION 00664
+
+//
+// Convert an nsString buffer to plain text...
+//
+#include "nsMsgUtils.h"
+#include "nsCharsetSource.h"
+#include "nsIChannel.h"
+#include "nsIOutputStream.h"
+#include "nsIPrincipal.h"
+
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/dom/RemoteType.h"
+#include "nsQueryObject.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static void ConvertAndSanitizeFileName(const nsACString& displayName,
+ nsString& aResult) {
+ nsCString unescapedName;
+
+ /* we need to convert the UTF-8 fileName to platform specific character set.
+ The display name is in UTF-8 because it has been escaped from JS
+ */
+ MsgUnescapeString(displayName, 0, unescapedName);
+ CopyUTF8toUTF16(unescapedName, aResult);
+
+ // replace platform specific path separator and illegale characters to avoid
+ // any confusion
+ aResult.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, u'-');
+}
+
+// ***************************************************
+// jefft - this is a rather obscured class serves for Save Message As File,
+// Save Message As Template, and Save Attachment to a file
+// It's used to save out a single item. If multiple items are to be saved,
+// a nsSaveAllAttachmentsState should be set, which holds a list of items.
+//
+class nsSaveAllAttachmentsState;
+
+class nsSaveMsgListener : public nsIUrlListener,
+ public nsIMsgCopyServiceListener,
+ public nsIStreamListener,
+ public nsICancelable {
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ public:
+ nsSaveMsgListener(nsIFile* file, nsMessenger* aMessenger,
+ nsIUrlListener* aListener);
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSICANCELABLE
+
+ nsCOMPtr<nsIFile> m_file;
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ char m_dataBuffer[FILE_IO_BUFFER_SIZE];
+ nsCOMPtr<nsIChannel> m_channel;
+ nsCString m_templateUri;
+ RefPtr<nsMessenger> m_messenger;
+ nsSaveAllAttachmentsState* m_saveAllAttachmentsState;
+
+ // rhp: For character set handling
+ bool m_doCharsetConversion;
+ nsString m_charset;
+ enum { eUnknown, ePlainText, eHTML } m_outputFormat;
+ nsCString m_msgBuffer;
+
+ nsCString m_contentType; // used only when saving attachment
+
+ nsCOMPtr<nsITransfer> mTransfer;
+ nsCOMPtr<nsIUrlListener> mListener;
+ nsCOMPtr<nsIURI> mListenerUri;
+ int64_t mProgress;
+ int64_t mMaxProgress;
+ bool mCanceled;
+ bool mInitialized;
+ bool mUrlHasStopped;
+ bool mRequestHasStopped;
+ nsresult InitializeDownload(nsIRequest* aRequest);
+
+ private:
+ virtual ~nsSaveMsgListener();
+};
+
+// This helper class holds a list of attachments to be saved and (optionally)
+// detached. It's used by nsSaveMsgListener (which only sticks around for a
+// single item, then passes the nsSaveAllAttachmentsState along to the next
+// SaveAttachment() call).
+class nsSaveAllAttachmentsState {
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ public:
+ nsSaveAllAttachmentsState(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray,
+ const PathChar* directoryName,
+ bool detachingAttachments,
+ nsIUrlListener* overallListener);
+ virtual ~nsSaveAllAttachmentsState();
+
+ uint32_t m_count;
+ uint32_t m_curIndex;
+ PathChar* m_directoryName;
+ nsTArray<nsCString> m_contentTypeArray;
+ nsTArray<nsCString> m_urlArray;
+ nsTArray<nsCString> m_displayNameArray;
+ nsTArray<nsCString> m_messageUriArray;
+ bool m_detachingAttachments;
+ // The listener to invoke when all the items have been saved.
+ nsCOMPtr<nsIUrlListener> m_overallListener;
+ // if detaching, do without warning? Will create unique files instead of
+ // prompting if duplicate files exist.
+ bool m_withoutWarning;
+ // if detaching first, remember where we saved to.
+ nsTArray<nsCString> m_savedFiles;
+};
+
+//
+// nsMessenger
+//
+nsMessenger::nsMessenger() {}
+
+nsMessenger::~nsMessenger() {}
+
+NS_IMPL_ISUPPORTS(nsMessenger, nsIMessenger, nsISupportsWeakReference)
+
+NS_IMETHODIMP nsMessenger::SetWindow(mozIDOMWindowProxy* aWin,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aWin) {
+ aMsgWindow->GetTransactionManager(getter_AddRefs(mTxnMgr));
+ mMsgWindow = aMsgWindow;
+ mWindow = aWin;
+
+ NS_ENSURE_TRUE(aWin, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWin);
+ mDocShell = win->GetDocShell();
+ } else {
+ mWindow = nullptr;
+ mDocShell = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMessenger::nsFilePickerShownCallback,
+ nsIFilePickerShownCallback)
+nsMessenger::nsFilePickerShownCallback::nsFilePickerShownCallback() {
+ mResult = nsIFilePicker::returnOK;
+ mPickerDone = false;
+}
+
+NS_IMETHODIMP
+nsMessenger::nsFilePickerShownCallback::Done(
+ nsIFilePicker::ResultCode aResult) {
+ mResult = aResult;
+ mPickerDone = true;
+ return NS_OK;
+}
+
+nsresult nsMessenger::ShowPicker(nsIFilePicker* aPicker,
+ nsIFilePicker::ResultCode* aResult) {
+ nsCOMPtr<nsIFilePickerShownCallback> callback =
+ new nsMessenger::nsFilePickerShownCallback();
+ nsFilePickerShownCallback* cb =
+ static_cast<nsFilePickerShownCallback*>(callback.get());
+
+ nsresult rv;
+ rv = aPicker->Open(callback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Spin the event loop until the callback was called.
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+ while (!cb->mPickerDone) {
+ NS_ProcessNextEvent(thread, true);
+ }
+
+ *aResult = cb->mResult;
+ return NS_OK;
+}
+
+nsresult nsMessenger::PromptIfFileExists(nsIFile* file) {
+ nsresult rv = NS_ERROR_FAILURE;
+ bool exists;
+ file->Exists(&exists);
+ if (!exists) return NS_OK;
+
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+ if (!dialog) return rv;
+ nsAutoString path;
+ bool dialogResult = false;
+ nsString errorMessage;
+
+ file->GetPath(path);
+ AutoTArray<nsString, 1> pathFormatStrings = {path};
+
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = mStringBundle->FormatStringFromName("fileExists", pathFormatStrings,
+ errorMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = dialog->Confirm(nullptr, errorMessage.get(), &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (dialogResult) return NS_OK; // user says okay to replace
+
+ // if we don't re-init the path for redisplay the picker will
+ // show the full path, not just the file name
+ nsCOMPtr<nsIFile> currentFile =
+ do_CreateInstance("@mozilla.org/file/local;1");
+ if (!currentFile) return NS_ERROR_FAILURE;
+
+ rv = currentFile->InitWithPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString leafName;
+ currentFile->GetLeafName(leafName);
+ if (!leafName.IsEmpty())
+ path.Assign(leafName); // path should be a copy of leafName
+
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString saveAttachmentStr;
+ GetString(u"SaveAttachment"_ns, saveAttachmentStr);
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave);
+ filePicker->SetDefaultString(path);
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir) {
+ filePicker->SetDisplayDirectory(lastSaveDir);
+ }
+
+ nsIFilePicker::ResultCode dialogReturn;
+ rv = ShowPicker(filePicker, &dialogReturn);
+ if (NS_FAILED(rv) || dialogReturn == nsIFilePicker::returnCancel) {
+ // XXX todo
+ // don't overload the return value like this
+ // change this function to have an out boolean
+ // that we check to see if the user cancelled
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // reset the file to point to the new path
+ return file->InitWithFile(localFile);
+}
+
+NS_IMETHODIMP nsMessenger::SaveAttachmentToFile(nsIFile* aFile,
+ const nsACString& aURL,
+ const nsACString& aMessageUri,
+ const nsACString& aContentType,
+ nsIUrlListener* aListener) {
+ return SaveAttachment(aFile, aURL, aMessageUri, aContentType, nullptr,
+ aListener);
+}
+
+NS_IMETHODIMP
+nsMessenger::DetachAttachmentsWOPrompts(
+ nsIFile* aDestFolder, const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray, nsIUrlListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ if (!aContentTypeArray.Length()) return NS_OK;
+ nsCOMPtr<nsIFile> attachmentDestination;
+ nsresult rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PathString path = attachmentDestination->NativePath();
+
+ nsAutoString unescapedFileName;
+ ConvertAndSanitizeFileName(aDisplayNameArray[0], unescapedFileName);
+ rv = attachmentDestination->Append(unescapedFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set up to detach the attachments once they've been saved out.
+ // NOTE: nsSaveAllAttachmentsState has a detach option, but I'd like to
+ // phase it out, so we set up a listener to call DetachAttachments()
+ // instead.
+ UrlListener* listener = new UrlListener;
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ aContentTypeArray, aUrlArray, aDisplayNameArray, aMessageUriArray,
+ path.get(),
+ false, // detach = false
+ listener);
+
+ // Note: saveState is kept in existence by SaveAttachment() until after
+ // the last item is saved.
+ listener->mStopFn = [saveState, self = RefPtr<nsMessenger>(this),
+ originalListener = nsCOMPtr<nsIUrlListener>(aListener)](
+ nsIURI* url, nsresult status) -> nsresult {
+ if (NS_SUCCEEDED(status)) {
+ status = self->DetachAttachments(
+ saveState->m_contentTypeArray, saveState->m_urlArray,
+ saveState->m_displayNameArray, saveState->m_messageUriArray,
+ &saveState->m_savedFiles, originalListener,
+ saveState->m_withoutWarning);
+ }
+ if (NS_FAILED(status) && originalListener) {
+ return originalListener->OnStopRunningUrl(nullptr, status);
+ }
+ return NS_OK;
+ };
+
+ // This method is used in filters, where we don't want to warn
+ saveState->m_withoutWarning = true;
+
+ rv = SaveAttachment(attachmentDestination, aUrlArray[0], aMessageUriArray[0],
+ aContentTypeArray[0], saveState, nullptr);
+ return rv;
+}
+
+// Internal helper for Saving attachments.
+// It handles a single attachment, but multiple attachments can be saved
+// by passing in an nsSaveAllAttachmentsState. In this case, SaveAttachment()
+// will be called for each attachment, and the saveState keeps track of which
+// one we're up to.
+//
+// aListener is invoked to cover this single attachment save.
+// If a saveState is used, it can also contain a nsIUrlListener which
+// will be invoked when _all_ the saves are complete.
+//
+// SaveAttachment() takes ownership of the saveState passed in.
+// If SaveAttachment() fails, then
+// saveState->m_overallListener->OnStopRunningUrl()
+// will be invoked and saveState itself will be deleted.
+//
+// Even though SaveAttachment() takes ownership of saveState,
+// nsSaveMsgListener is responsible for finally deleting it when the
+// last save operation successfully completes.
+//
+// Yes, this is convoluted. Bug 1788159 covers simplifying all this stuff.
+nsresult nsMessenger::SaveAttachment(nsIFile* aFile, const nsACString& aURL,
+ const nsACString& aMessageUri,
+ const nsACString& aContentType,
+ nsSaveAllAttachmentsState* saveState,
+ nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIMsgMessageFetchPartService> fetchService;
+ nsAutoCString urlString;
+ nsAutoCString fullMessageUri(aMessageUri);
+
+ nsresult rv = NS_OK;
+
+ // This instance will be held onto by the listeners, and will be released once
+ // the transfer has been completed.
+ RefPtr<nsSaveMsgListener> saveListener(
+ new nsSaveMsgListener(aFile, this, aListener));
+
+ saveListener->m_contentType = aContentType;
+ if (saveState) {
+ if (saveState->m_overallListener && saveState->m_curIndex == 0) {
+ // This is the first item, so tell the caller we're starting.
+ saveState->m_overallListener->OnStartRunningUrl(nullptr);
+ }
+ saveListener->m_saveAllAttachmentsState = saveState;
+ // Record the resultant file:// URL for each saved attachment as we go
+ // along. It'll be used later if we want to also detach them from the email.
+ // Placeholder text will be inserted into the email to replace the
+ // removed attachment pointing at it's final resting place.
+ nsCOMPtr<nsIURI> outputURI;
+ rv = NS_NewFileURI(getter_AddRefs(outputURI), aFile);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString fileUriSpec;
+ rv = outputURI->GetSpec(fileUriSpec);
+ if NS_SUCCEEDED (rv) {
+ saveState->m_savedFiles.AppendElement(fileUriSpec);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIURI> URL;
+ if (NS_SUCCEEDED(rv)) {
+ urlString = aURL;
+ // strip out ?type=application/x-message-display because it confuses libmime
+
+ int32_t typeIndex = urlString.Find("?type=application/x-message-display");
+ if (typeIndex != kNotFound) {
+ urlString.Cut(typeIndex,
+ sizeof("?type=application/x-message-display") - 1);
+ // we also need to replace the next '&' with '?'
+ int32_t firstPartIndex = urlString.FindChar('&');
+ if (firstPartIndex != kNotFound) urlString.SetCharAt('?', firstPartIndex);
+ }
+
+ urlString.ReplaceSubstring("/;section", "?section");
+ rv = NS_NewURI(getter_AddRefs(URL), urlString);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = GetMessageServiceFromURI(aMessageUri, getter_AddRefs(messageService));
+ if (NS_SUCCEEDED(rv)) {
+ fetchService = do_QueryInterface(messageService);
+ // if the message service has a fetch part service then we know we can
+ // fetch mime parts...
+ if (fetchService) {
+ int32_t partPos = urlString.FindChar('?');
+ if (partPos == kNotFound) return NS_ERROR_FAILURE;
+ fullMessageUri.Append(Substring(urlString, partPos));
+ }
+
+ nsCOMPtr<nsIStreamListener> convertedListener;
+ saveListener->QueryInterface(NS_GET_IID(nsIStreamListener),
+ getter_AddRefs(convertedListener));
+
+ nsCOMPtr<nsIURI> dummyNull;
+ if (fetchService)
+ rv = fetchService->FetchMimePart(URL, fullMessageUri, convertedListener,
+ mMsgWindow, saveListener,
+ getter_AddRefs(dummyNull));
+ else
+ rv = messageService->LoadMessage(fullMessageUri, convertedListener,
+ mMsgWindow, nullptr, false);
+ } // if we got a message service
+ } // if we created a url
+
+ if (NS_FAILED(rv)) {
+ if (saveState) {
+ // If we had a listener, make sure it sees the failure!
+ if (saveState->m_overallListener) {
+ saveState->m_overallListener->OnStopRunningUrl(nullptr, rv);
+ }
+ // Ugh. Ownership is all over the place here!
+ // Usually nsSaveMsgListener is responsible for cleaning up
+ // nsSaveAllAttachmentsState... but we're not getting
+ // that far, so have to clean it up here!
+ delete saveState;
+ saveListener->m_saveAllAttachmentsState = nullptr;
+ }
+ Alert("saveAttachmentFailed");
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAttachmentToFolder(const nsACString& contentType,
+ const nsACString& url,
+ const nsACString& displayName,
+ const nsACString& messageUri,
+ nsIFile* aDestFolder, nsIFile** aOutFile) {
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> attachmentDestination;
+ rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString unescapedFileName;
+ ConvertAndSanitizeFileName(displayName, unescapedFileName);
+ rv = attachmentDestination->Append(unescapedFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+#ifdef XP_MACOSX
+ rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+#endif
+
+ rv = SaveAttachment(attachmentDestination, url, messageUri, contentType,
+ nullptr, nullptr);
+ attachmentDestination.forget(aOutFile);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri,
+ bool aIsExternalAttachment) {
+ return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri,
+ false);
+}
+
+nsresult nsMessenger::SaveOneAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri,
+ bool detaching) {
+ nsresult rv = NS_ERROR_OUT_OF_MEMORY;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIFilePicker::ResultCode dialogResult;
+ nsCOMPtr<nsIFile> localFile;
+ nsCOMPtr<nsIFile> lastSaveDir;
+ nsCString filePath;
+ nsString saveAttachmentStr;
+ nsString defaultDisplayString;
+ ConvertAndSanitizeFileName(aDisplayName, defaultDisplayString);
+
+ if (detaching) {
+ GetString(u"DetachAttachment"_ns, saveAttachmentStr);
+ } else {
+ GetString(u"SaveAttachment"_ns, saveAttachmentStr);
+ }
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave);
+ filePicker->SetDefaultString(defaultDisplayString);
+
+ // Check if the attachment file name has an extension (which must not
+ // contain spaces) and set it as the default extension for the attachment.
+ int32_t extensionIndex = defaultDisplayString.RFindChar('.');
+ if (extensionIndex > 0 &&
+ defaultDisplayString.FindChar(' ', extensionIndex) == kNotFound) {
+ nsString extension;
+ extension = Substring(defaultDisplayString, extensionIndex + 1);
+ filePicker->SetDefaultExtension(extension);
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsString filterName;
+ AutoTArray<nsString, 1> extensionParam = {extension};
+ rv = mStringBundle->FormatStringFromName("saveAsType", extensionParam,
+ filterName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ extension.InsertLiteral(u"*.", 0);
+ filePicker->AppendFilter(filterName, extension);
+ }
+
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetLastSaveDirectory(localFile);
+
+ PathString dirName = localFile->NativePath();
+
+ AutoTArray<nsCString, 1> contentTypeArray = {
+ PromiseFlatCString(aContentType)};
+ AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)};
+ AutoTArray<nsCString, 1> displayNameArray = {
+ PromiseFlatCString(aDisplayName)};
+ AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)};
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ contentTypeArray, urlArray, displayNameArray, messageUriArray,
+ dirName.get(), detaching, nullptr);
+
+ // SaveAttachment takes ownership of saveState.
+ return SaveAttachment(localFile, aURL, aMessageUri, aContentType, saveState,
+ nullptr);
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray) {
+ uint32_t len = contentTypeArray.Length();
+ NS_ENSURE_TRUE(urlArray.Length() == len, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(displayNameArray.Length() == len, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(messageUriArray.Length() == len, NS_ERROR_INVALID_ARG);
+ if (len == 0) {
+ return NS_OK;
+ }
+ return SaveAllAttachments(contentTypeArray, urlArray, displayNameArray,
+ messageUriArray, false);
+}
+
+nsresult nsMessenger::SaveAllAttachments(
+ const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray, bool detaching) {
+ nsresult rv = NS_ERROR_OUT_OF_MEMORY;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ nsCOMPtr<nsIFile> localFile;
+ nsCOMPtr<nsIFile> lastSaveDir;
+ nsIFilePicker::ResultCode dialogResult;
+ nsString saveAttachmentStr;
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (detaching) {
+ GetString(u"DetachAllAttachments"_ns, saveAttachmentStr);
+ } else {
+ GetString(u"SaveAllAttachments"_ns, saveAttachmentStr);
+ }
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeGetFolder);
+
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PathString dirName = localFile->NativePath();
+
+ nsString unescapedName;
+ ConvertAndSanitizeFileName(displayNameArray[0], unescapedName);
+ rv = localFile->Append(unescapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ contentTypeArray, urlArray, displayNameArray, messageUriArray,
+ dirName.get(), detaching, nullptr);
+ // SaveAttachment takes ownership of saveState.
+ rv = SaveAttachment(localFile, urlArray[0], messageUriArray[0],
+ contentTypeArray[0], saveState, nullptr);
+ return rv;
+}
+
+enum MESSENGER_SAVEAS_FILE_TYPE {
+ EML_FILE_TYPE = 0,
+ HTML_FILE_TYPE = 1,
+ TEXT_FILE_TYPE = 2,
+ ANY_FILE_TYPE = 3
+};
+#define HTML_FILE_EXTENSION ".htm"
+#define HTML_FILE_EXTENSION2 ".html"
+#define TEXT_FILE_EXTENSION ".txt"
+
+/**
+ * Adjust the file name, removing characters from the middle of the name if
+ * the name would otherwise be too long - too long for what file systems
+ * usually support.
+ */
+nsresult nsMessenger::AdjustFileIfNameTooLong(nsIFile* aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsAutoString path;
+ nsresult rv = aFile->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Most common file systems have a max filename length of 255. On windows, the
+ // total path length is (at least for all practical purposees) limited to 255.
+ // Let's just don't allow paths longer than that elsewhere either for
+ // simplicity.
+ uint32_t MAX = 255;
+ if (path.Length() > MAX) {
+ nsAutoString leafName;
+ rv = aFile->GetLeafName(leafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t pathLengthUpToLeaf = path.Length() - leafName.Length();
+ if (pathLengthUpToLeaf >= MAX - 8) { // want at least 8 chars for name
+ return NS_ERROR_FILE_NAME_TOO_LONG;
+ }
+ uint32_t x = MAX - pathLengthUpToLeaf; // x = max leaf size
+ nsAutoString truncatedLeaf;
+ truncatedLeaf.Append(Substring(leafName, 0, x / 2));
+ truncatedLeaf.AppendLiteral("...");
+ truncatedLeaf.Append(
+ Substring(leafName, leafName.Length() - x / 2 + 3, leafName.Length()));
+ rv = aFile->SetLeafName(truncatedLeaf);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAs(const nsACString& aURI, bool aAsFile,
+ nsIMsgIdentity* aIdentity, const nsAString& aMsgFilename,
+ bool aBypassFilePicker) {
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIUrlListener> urlListener;
+ RefPtr<nsSaveMsgListener> saveListener;
+ nsCOMPtr<nsIStreamListener> convertedListener;
+ int32_t saveAsFileType = EML_FILE_TYPE;
+
+ nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(messageService));
+ if (NS_FAILED(rv)) goto done;
+
+ if (aAsFile) {
+ nsCOMPtr<nsIFile> saveAsFile;
+ // show the file picker if BypassFilePicker is not specified (null) or false
+ if (!aBypassFilePicker) {
+ rv = GetSaveAsFile(aMsgFilename, &saveAsFileType,
+ getter_AddRefs(saveAsFile));
+ // A null saveAsFile means that the user canceled the save as
+ if (NS_FAILED(rv) || !saveAsFile) goto done;
+ } else {
+ saveAsFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ rv = saveAsFile->InitWithPath(aMsgFilename);
+ if (NS_FAILED(rv)) goto done;
+ if (StringEndsWith(aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator))
+ saveAsFileType = TEXT_FILE_TYPE;
+ else if ((StringEndsWith(
+ aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator)) ||
+ (StringEndsWith(
+ aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2),
+ nsCaseInsensitiveStringComparator)))
+ saveAsFileType = HTML_FILE_TYPE;
+ else
+ saveAsFileType = EML_FILE_TYPE;
+ }
+
+ rv = AdjustFileIfNameTooLong(saveAsFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(saveAsFile);
+ if (NS_FAILED(rv)) {
+ goto done;
+ }
+
+ // After saveListener goes out of scope, the listener will be owned by
+ // whoever the listener is registered with, usually a URL.
+ saveListener = new nsSaveMsgListener(saveAsFile, this, nullptr);
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) goto done;
+
+ if (saveAsFileType == EML_FILE_TYPE) {
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aURI, saveAsFile, false, urlListener, getter_AddRefs(dummyNull), true,
+ mMsgWindow);
+ } else {
+ nsAutoCString urlString(aURI);
+
+ // we can't go RFC822 to TXT until bug #1775 is fixed
+ // so until then, do the HTML to TXT conversion in
+ // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText()
+ //
+ // Setup the URL for a "Save As..." Operation...
+ // For now, if this is a save as TEXT operation, then do
+ // a "printing" operation
+ if (saveAsFileType == TEXT_FILE_TYPE) {
+ saveListener->m_outputFormat = nsSaveMsgListener::ePlainText;
+ saveListener->m_doCharsetConversion = true;
+ urlString.AppendLiteral("?header=print");
+ } else {
+ saveListener->m_outputFormat = nsSaveMsgListener::eHTML;
+ saveListener->m_doCharsetConversion = false;
+ urlString.AppendLiteral("?header=saveas");
+ }
+
+ nsCOMPtr<nsIURI> url;
+ rv = NS_NewURI(getter_AddRefs(url), urlString);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewURI failed");
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+
+ saveListener->m_channel = nullptr;
+ rv = NS_NewInputStreamChannel(
+ getter_AddRefs(saveListener->m_channel), url, nullptr, nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed");
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIStreamConverterService> streamConverterService =
+ do_GetService("@mozilla.org/streamConverters;1");
+ nsCOMPtr<nsISupports> channelSupport =
+ do_QueryInterface(saveListener->m_channel);
+
+ // we can't go RFC822 to TXT until bug #1775 is fixed
+ // so until then, do the HTML to TXT conversion in
+ // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText()
+ rv = streamConverterService->AsyncConvertData(
+ MESSAGE_RFC822, TEXT_HTML, saveListener, channelSupport,
+ getter_AddRefs(convertedListener));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncConvertData failed");
+ if (NS_FAILED(rv)) goto done;
+
+ rv = messageService->LoadMessage(urlString, convertedListener, mMsgWindow,
+ nullptr, false);
+ }
+ } else {
+ // ** save as Template
+ nsCOMPtr<nsIFile> tmpFile;
+ nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp",
+ getter_AddRefs(tmpFile));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For temp file, we should use restrictive 00600 instead of
+ // ATTACHMENT_PERMISSION
+ rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ if (NS_FAILED(rv)) goto done;
+
+ // The saveListener is owned by whoever we ultimately register the
+ // listener with, generally a URL.
+ saveListener = new nsSaveMsgListener(tmpFile, this, nullptr);
+
+ if (aIdentity)
+ rv = aIdentity->GetStationeryFolder(saveListener->m_templateUri);
+ if (NS_FAILED(rv)) goto done;
+
+ bool needDummyHeader =
+ StringBeginsWith(saveListener->m_templateUri, "mailbox://"_ns);
+ bool canonicalLineEnding =
+ StringBeginsWith(saveListener->m_templateUri, "imap://"_ns);
+
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aURI, tmpFile, needDummyHeader, urlListener, getter_AddRefs(dummyNull),
+ canonicalLineEnding, mMsgWindow);
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ }
+ return rv;
+}
+
+nsresult nsMessenger::GetSaveAsFile(const nsAString& aMsgFilename,
+ int32_t* aSaveAsFileType,
+ nsIFile** aSaveAsFile) {
+ nsresult rv;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString saveMailAsStr;
+ GetString(u"SaveMailAs"_ns, saveMailAsStr);
+ filePicker->Init(mWindow, saveMailAsStr, nsIFilePicker::modeSave);
+
+ // if we have a non-null filename use it, otherwise use default save message
+ // one
+ if (aMsgFilename.IsEmpty()) {
+ nsString saveMsgStr;
+ GetString(u"defaultSaveMessageAsFileName"_ns, saveMsgStr);
+ filePicker->SetDefaultString(saveMsgStr);
+ } else {
+ filePicker->SetDefaultString(aMsgFilename);
+ }
+
+ // because we will be using GetFilterIndex()
+ // we must call AppendFilters() one at a time,
+ // in MESSENGER_SAVEAS_FILE_TYPE order
+ nsString emlFilesStr;
+ GetString(u"EMLFiles"_ns, emlFilesStr);
+ filePicker->AppendFilter(emlFilesStr, u"*.eml"_ns);
+ filePicker->AppendFilters(nsIFilePicker::filterHTML);
+ filePicker->AppendFilters(nsIFilePicker::filterText);
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ // Save as the "All Files" file type by default. We want to save as .eml by
+ // default, but the filepickers on some platforms don't switch extensions
+ // based on the file type selected (bug 508597).
+ filePicker->SetFilterIndex(ANY_FILE_TYPE);
+ // Yes, this is fine even if we ultimately save as HTML or text. On Windows,
+ // this actually is a boolean telling the file picker to automatically add
+ // the correct extension depending on the filter. On Mac or Linux this is a
+ // no-op.
+ filePicker->SetDefaultExtension(u"eml"_ns);
+
+ nsIFilePicker::ResultCode dialogResult;
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ nsCOMPtr<nsIFile> localFile;
+ rv = ShowPicker(filePicker, &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (dialogResult == nsIFilePicker::returnCancel) {
+ // We'll indicate this by setting the outparam to null.
+ *aSaveAsFile = nullptr;
+ return NS_OK;
+ }
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t selectedSaveAsFileType;
+ rv = filePicker->GetFilterIndex(&selectedSaveAsFileType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If All Files was selected, look at the extension
+ if (selectedSaveAsFileType == ANY_FILE_TYPE) {
+ nsAutoString fileName;
+ rv = localFile->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2),
+ nsCaseInsensitiveStringComparator))
+ *aSaveAsFileType = HTML_FILE_TYPE;
+ else if (StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator))
+ *aSaveAsFileType = TEXT_FILE_TYPE;
+ else
+ // The default is .eml
+ *aSaveAsFileType = EML_FILE_TYPE;
+ } else {
+ *aSaveAsFileType = selectedSaveAsFileType;
+ }
+
+ if (dialogResult == nsIFilePicker::returnReplace) {
+ // be extra safe and only delete when the file is really a file
+ bool isFile;
+ rv = localFile->IsFile(&isFile);
+ if (NS_SUCCEEDED(rv) && isFile) {
+ rv = localFile->Remove(false /* recursive delete */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // We failed, or this isn't a file. We can't do anything about it.
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ *aSaveAsFile = nullptr;
+ localFile.forget(aSaveAsFile);
+ return NS_OK;
+}
+
+/**
+ * Show a Save All dialog allowing the user to pick which folder to save
+ * messages to.
+ * @param [out] aSaveDir directory to save to. Will be null on cancel.
+ */
+nsresult nsMessenger::GetSaveToDir(nsIFile** aSaveDir) {
+ nsresult rv;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString chooseFolderStr;
+ GetString(u"ChooseFolder"_ns, chooseFolderStr);
+ filePicker->Init(mWindow, chooseFolderStr, nsIFilePicker::modeGetFolder);
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ nsIFilePicker::ResultCode dialogResult;
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) {
+ // We'll indicate this by setting the outparam to null.
+ *aSaveDir = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> dir;
+ rv = filePicker->GetFile(getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(dir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSaveDir = nullptr;
+ dir.forget(aSaveDir);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveMessages(const nsTArray<nsString>& aFilenameArray,
+ const nsTArray<nsCString>& aMessageUriArray) {
+ MOZ_ASSERT(aFilenameArray.Length() == aMessageUriArray.Length());
+
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> saveDir;
+ rv = GetSaveToDir(getter_AddRefs(saveDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!saveDir) // A null saveDir means that the user canceled the save.
+ return NS_OK;
+
+ for (uint32_t i = 0; i < aFilenameArray.Length(); i++) {
+ nsCOMPtr<nsIFile> saveToFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = saveToFile->InitWithFile(saveDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = saveToFile->Append(aFilenameArray[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AdjustFileIfNameTooLong(saveToFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(saveToFile);
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIUrlListener> urlListener;
+
+ rv = GetMessageServiceFromURI(aMessageUriArray[i],
+ getter_AddRefs(messageService));
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+
+ RefPtr<nsSaveMsgListener> saveListener =
+ new nsSaveMsgListener(saveToFile, this, nullptr);
+
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+
+ // Ok, now save the message.
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aMessageUriArray[i], saveToFile, false, urlListener,
+ getter_AddRefs(dummyNull), true, mMsgWindow);
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMessenger::Alert(const char* stringName) {
+ nsresult rv = NS_OK;
+
+ if (mDocShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+
+ if (dialog) {
+ nsString alertStr;
+ GetString(NS_ConvertASCIItoUTF16(stringName), alertStr);
+ rv = dialog->Alert(nullptr, alertStr.get());
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::MsgHdrFromURI(const nsACString& aUri, nsIMsgDBHdr** aMsgHdr) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsresult rv;
+
+ rv = GetMessageServiceFromURI(aUri, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgService->MessageURIToMsgHdr(aUri, aMsgHdr);
+}
+
+NS_IMETHODIMP nsMessenger::GetUndoTransactionType(uint32_t* txnType) {
+ NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *txnType = nsMessenger::eUnknown;
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::CanUndo(bool* bValue) {
+ NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *bValue = false;
+ int32_t count = 0;
+ rv = mTxnMgr->GetNumberOfUndoItems(&count);
+ if (NS_SUCCEEDED(rv) && count > 0) *bValue = true;
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::GetRedoTransactionType(uint32_t* txnType) {
+ NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *txnType = nsMessenger::eUnknown;
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::CanRedo(bool* bValue) {
+ NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *bValue = false;
+ int32_t count = 0;
+ rv = mTxnMgr->GetNumberOfRedoItems(&count);
+ if (NS_SUCCEEDED(rv) && count > 0) *bValue = true;
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMessenger::Undo(nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (mTxnMgr) {
+ int32_t numTxn = 0;
+ rv = mTxnMgr->GetNumberOfUndoItems(&numTxn);
+ if (NS_SUCCEEDED(rv) && numTxn > 0) {
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get()))
+ ->SetMsgWindow(msgWindow);
+ }
+ nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr;
+ txnMgr->UndoTransaction();
+ }
+ }
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMessenger::Redo(nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (mTxnMgr) {
+ int32_t numTxn = 0;
+ rv = mTxnMgr->GetNumberOfRedoItems(&numTxn);
+ if (NS_SUCCEEDED(rv) && numTxn > 0) {
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get()))
+ ->SetMsgWindow(msgWindow);
+ }
+ nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr;
+ txnMgr->RedoTransaction();
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::GetTransactionManager(nsITransactionManager** aTxnMgr) {
+ NS_ENSURE_TRUE(mTxnMgr && aTxnMgr, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aTxnMgr = mTxnMgr);
+ return NS_OK;
+}
+
+nsSaveMsgListener::nsSaveMsgListener(nsIFile* aFile, nsMessenger* aMessenger,
+ nsIUrlListener* aListener) {
+ m_file = aFile;
+ m_messenger = aMessenger;
+ mListener = aListener;
+ mUrlHasStopped = false;
+ mRequestHasStopped = false;
+
+ // rhp: for charset handling
+ m_doCharsetConversion = false;
+ m_saveAllAttachmentsState = nullptr;
+ mProgress = 0;
+ mMaxProgress = -1;
+ mCanceled = false;
+ m_outputFormat = eUnknown;
+ mInitialized = false;
+}
+
+nsSaveMsgListener::~nsSaveMsgListener() {}
+
+//
+// nsISupports
+//
+NS_IMPL_ISUPPORTS(nsSaveMsgListener, nsIUrlListener, nsIMsgCopyServiceListener,
+ nsIStreamListener, nsIRequestObserver, nsICancelable)
+
+NS_IMETHODIMP
+nsSaveMsgListener::Cancel(nsresult status) {
+ mCanceled = true;
+ return NS_OK;
+}
+
+//
+// nsIUrlListener
+//
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartRunningUrl(nsIURI* url) {
+ if (mListener) mListener->OnStartRunningUrl(url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+ mUrlHasStopped = true;
+
+ // ** save as template goes here
+ if (!m_templateUri.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> templateFolder;
+ rv = GetOrCreateFolder(m_templateUri, getter_AddRefs(templateFolder));
+ if (NS_FAILED(rv)) goto done;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ if (copyService) {
+ nsCOMPtr<nsIFile> clone;
+ m_file->Clone(getter_AddRefs(clone));
+ rv = copyService->CopyFileMessage(clone, templateFolder, nullptr, true,
+ nsMsgMessageFlags::Read, EmptyCString(),
+ this, nullptr);
+ // Clear this so we don't end up in a loop if OnStopRunningUrl gets
+ // called again.
+ m_templateUri.Truncate();
+ }
+ } else if (m_outputStream && mRequestHasStopped) {
+ m_outputStream->Close();
+ m_outputStream = nullptr;
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ if (m_file) m_file->Remove(false);
+ if (m_messenger) m_messenger->Alert("saveMessageFailed");
+ }
+
+ if (mRequestHasStopped && mListener)
+ mListener->OnStopRunningUrl(url, exitCode);
+ else
+ mListenerUri = url;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartCopy(void) { return NS_OK; }
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::SetMessageKey(nsMsgKey aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::GetMessageId(nsACString& aMessageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopCopy(nsresult aStatus) {
+ if (m_file) m_file->Remove(false);
+ return aStatus;
+}
+
+// initializes the progress window if we are going to show one
+// and for OSX, sets creator flags on the output file
+nsresult nsSaveMsgListener::InitializeDownload(nsIRequest* aRequest) {
+ nsresult rv = NS_OK;
+
+ mInitialized = true;
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+
+ if (!channel) return rv;
+
+ // Get the max progress from the URL if we haven't already got it.
+ if (mMaxProgress == -1) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) mailnewsUrl->GetMaxProgress(&mMaxProgress);
+ }
+
+ if (!m_contentType.IsEmpty()) {
+ nsCOMPtr<nsIMIMEService> mimeService(
+ do_GetService(NS_MIMESERVICE_CONTRACTID));
+ nsCOMPtr<nsIMIMEInfo> mimeinfo;
+
+ mimeService->GetFromTypeAndExtension(m_contentType, EmptyCString(),
+ getter_AddRefs(mimeinfo));
+
+ // create a download progress window
+
+ // Set saveToDisk explicitly to avoid launching the saved file.
+ // See
+ // https://hg.mozilla.org/mozilla-central/file/814a6f071472/toolkit/components/jsdownloads/src/DownloadLegacy.js#l164
+ mimeinfo->SetPreferredAction(nsIHandlerInfo::saveToDisk);
+
+ // When we don't allow warnings, also don't show progress, as this
+ // is an environment (typically filters) where we don't want
+ // interruption.
+ bool allowProgress = true;
+ if (m_saveAllAttachmentsState)
+ allowProgress = !m_saveAllAttachmentsState->m_withoutWarning;
+ if (allowProgress) {
+ nsCOMPtr<nsITransfer> tr = do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
+ if (tr && m_file) {
+ PRTime timeDownloadStarted = PR_Now();
+
+ nsCOMPtr<nsIURI> outputURI;
+ NS_NewFileURI(getter_AddRefs(outputURI), m_file);
+
+ nsCOMPtr<nsIURI> url;
+ channel->GetURI(getter_AddRefs(url));
+ rv = tr->Init(url, nullptr, outputURI, EmptyString(), mimeinfo,
+ timeDownloadStarted, nullptr, this, false,
+ nsITransfer::DOWNLOAD_ACCEPTABLE, nullptr, false);
+
+ // now store the web progresslistener
+ mTransfer = tr;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartRequest(nsIRequest* request) {
+ if (m_file)
+ MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), m_file, -1,
+ ATTACHMENT_PERMISSION);
+ if (!m_outputStream) {
+ mCanceled = true;
+ if (m_messenger) m_messenger->Alert("saveAttachmentFailed");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsresult rv = NS_OK;
+ mRequestHasStopped = true;
+
+ // rhp: If we are doing the charset conversion magic, this is different
+ // processing, otherwise, its just business as usual.
+ // If we need text/plain, then we need to convert the HTML and then convert
+ // to the systems charset.
+ if (m_doCharsetConversion && m_outputStream) {
+ // For HTML, code is emitted immediately in OnDataAvailable.
+ MOZ_ASSERT(m_outputFormat == ePlainText,
+ "For HTML, m_doCharsetConversion shouldn't be set");
+ NS_ConvertUTF8toUTF16 utf16Buffer(m_msgBuffer);
+ ConvertBufToPlainText(utf16Buffer, false, false, false);
+
+ nsCString outCString;
+ // NS_CopyUnicodeToNative() doesn't return an error, so we have no choice
+ // but to always use UTF-8.
+ CopyUTF16toUTF8(utf16Buffer, outCString);
+ uint32_t writeCount;
+ rv = m_outputStream->Write(outCString.get(), outCString.Length(),
+ &writeCount);
+ if (outCString.Length() != writeCount) rv = NS_ERROR_FAILURE;
+ }
+
+ if (m_outputStream) {
+ m_outputStream->Close();
+ m_outputStream = nullptr;
+ }
+
+ // Are there more attachments to deal with?
+ nsSaveAllAttachmentsState* state = m_saveAllAttachmentsState;
+ if (state) {
+ state->m_curIndex++;
+ if (!mCanceled && state->m_curIndex < state->m_count) {
+ // Yes, start on the next attachment.
+ uint32_t i = state->m_curIndex;
+ nsString unescapedName;
+ RefPtr<nsLocalFile> localFile =
+ new nsLocalFile(nsTDependentString<PathChar>(state->m_directoryName));
+ if (localFile->NativePath().IsEmpty()) {
+ rv = NS_ERROR_FAILURE;
+ goto done;
+ }
+
+ ConvertAndSanitizeFileName(state->m_displayNameArray[i], unescapedName);
+ rv = localFile->Append(unescapedName);
+ if (NS_FAILED(rv)) goto done;
+
+ // When we are running with no warnings (typically filters and other
+ // automatic uses), then don't prompt for duplicates, but create a unique
+ // file instead.
+ if (!state->m_withoutWarning) {
+ rv = m_messenger->PromptIfFileExists(localFile);
+ if (NS_FAILED(rv)) goto done;
+ } else {
+ rv = localFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ if (NS_FAILED(rv)) goto done;
+ }
+ // Start the next attachment saving.
+ // NOTE: null listener passed in on subsequent saves! The original
+ // listener will already have been invoked.
+ // See Bug 1789565
+ rv = m_messenger->SaveAttachment(
+ localFile, state->m_urlArray[i], state->m_messageUriArray[i],
+ state->m_contentTypeArray[i], state, nullptr);
+ if (NS_FAILED(rv)) {
+ // If SaveAttachment() fails, state will have been deleted, and
+ // m_overallListener->OnStopRunningUrl() will have been called.
+ state = nullptr;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ done:
+ if (NS_FAILED(rv) && state) {
+ if (state->m_overallListener) {
+ state->m_overallListener->OnStopRunningUrl(nullptr, rv);
+ }
+ delete state;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ } else {
+ // All attachments have been saved.
+ if (state->m_overallListener) {
+ state->m_overallListener->OnStopRunningUrl(
+ nullptr, mCanceled ? NS_ERROR_FAILURE : NS_OK);
+ }
+ // Check if we're supposed to be detaching attachments after saving them.
+ if (state->m_detachingAttachments && !mCanceled) {
+ m_messenger->DetachAttachments(
+ state->m_contentTypeArray, state->m_urlArray,
+ state->m_displayNameArray, state->m_messageUriArray,
+ &state->m_savedFiles, nullptr, state->m_withoutWarning);
+ }
+ delete m_saveAllAttachmentsState;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ }
+
+ if (mTransfer) {
+ mTransfer->OnProgressChange64(nullptr, nullptr, mMaxProgress, mMaxProgress,
+ mMaxProgress, mMaxProgress);
+ mTransfer->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+ mTransfer = nullptr; // break any circular dependencies between the
+ // progress dialog and use
+ }
+
+ if (mUrlHasStopped && mListener)
+ mListener->OnStopRunningUrl(mListenerUri, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStream, uint64_t srcOffset,
+ uint32_t count) {
+ nsresult rv = NS_ERROR_FAILURE;
+ // first, check to see if we've been canceled....
+ if (mCanceled) // then go cancel our underlying channel too
+ return request->Cancel(NS_BINDING_ABORTED);
+
+ if (!mInitialized) InitializeDownload(request);
+
+ if (m_outputStream) {
+ mProgress += count;
+ uint64_t available;
+ uint32_t readCount, maxReadCount = sizeof(m_dataBuffer);
+ uint32_t writeCount;
+ rv = inStream->Available(&available);
+ while (NS_SUCCEEDED(rv) && available) {
+ if (maxReadCount > available) maxReadCount = (uint32_t)available;
+ rv = inStream->Read(m_dataBuffer, maxReadCount, &readCount);
+
+ // rhp:
+ // Ok, now we do one of two things. If we are sending out HTML, then
+ // just write it to the HTML stream as it comes along...but if this is
+ // a save as TEXT operation, we need to buffer this up for conversion
+ // when we are done. When the stream converter for HTML-TEXT gets in
+ // place, this magic can go away.
+ //
+ if (NS_SUCCEEDED(rv)) {
+ if ((m_doCharsetConversion) && (m_outputFormat == ePlainText))
+ m_msgBuffer.Append(Substring(m_dataBuffer, m_dataBuffer + readCount));
+ else
+ rv = m_outputStream->Write(m_dataBuffer, readCount, &writeCount);
+
+ available -= readCount;
+ }
+ }
+
+ if (NS_SUCCEEDED(rv) && mTransfer) // Send progress notification.
+ mTransfer->OnProgressChange64(nullptr, request, mProgress, mMaxProgress,
+ mProgress, mMaxProgress);
+ }
+ return rv;
+}
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+nsresult nsMessenger::InitStringBundle() {
+ if (mStringBundle) return NS_OK;
+
+ const char propertyURL[] = MESSENGER_STRING_URL;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ return sBundleService->CreateBundle(propertyURL,
+ getter_AddRefs(mStringBundle));
+}
+
+void nsMessenger::GetString(const nsString& aStringName, nsString& aValue) {
+ nsresult rv;
+ aValue.Truncate();
+
+ if (!mStringBundle) InitStringBundle();
+
+ if (mStringBundle)
+ rv = mStringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(aStringName).get(), aValue);
+ else
+ rv = NS_ERROR_FAILURE;
+
+ if (NS_FAILED(rv) || aValue.IsEmpty()) aValue = aStringName;
+ return;
+}
+
+nsSaveAllAttachmentsState::nsSaveAllAttachmentsState(
+ const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray, const PathChar* dirName,
+ bool detachingAttachments, nsIUrlListener* overallListener)
+ : m_contentTypeArray(contentTypeArray.Clone()),
+ m_urlArray(urlArray.Clone()),
+ m_displayNameArray(displayNameArray.Clone()),
+ m_messageUriArray(messageUriArray.Clone()),
+ m_detachingAttachments(detachingAttachments),
+ m_overallListener(overallListener),
+ m_withoutWarning(false) {
+ m_count = contentTypeArray.Length();
+ m_curIndex = 0;
+ m_directoryName = NS_xstrdup(dirName);
+}
+
+nsSaveAllAttachmentsState::~nsSaveAllAttachmentsState() {
+ free(m_directoryName);
+}
+
+nsresult nsMessenger::GetLastSaveDirectory(nsIFile** aLastSaveDir) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this can fail, and it will, on the first time we call it, as there is no
+ // default for this pref.
+ nsCOMPtr<nsIFile> localFile;
+ rv = prefBranch->GetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+ if (NS_SUCCEEDED(rv)) localFile.forget(aLastSaveDir);
+ return rv;
+}
+
+nsresult nsMessenger::SetLastSaveDirectory(nsIFile* aLocalFile) {
+ NS_ENSURE_ARG_POINTER(aLocalFile);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if the file is a directory, just use it for the last dir chosen
+ // otherwise, use the parent of the file as the last dir chosen.
+ // IsDirectory() will return error on saving a file, as the
+ // file doesn't exist yet.
+ bool isDirectory;
+ rv = aLocalFile->IsDirectory(&isDirectory);
+ if (NS_SUCCEEDED(rv) && isDirectory) {
+ rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile), aLocalFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIFile> parent;
+ rv = aLocalFile->GetParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile), parent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessenger::FormatFileSize(uint64_t aSize, bool aUseKB,
+ nsAString& aFormattedSize) {
+ return ::FormatFileSize(aSize, aUseKB, aFormattedSize);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Detach/Delete Attachments
+///////////////////////////////////////////////////////////////////////////////
+
+static const char* GetAttachmentPartId(const char* aAttachmentUrl) {
+ static const char partIdPrefix[] = "part=";
+ const char* partId = PL_strstr(aAttachmentUrl, partIdPrefix);
+ return partId ? (partId + sizeof(partIdPrefix) - 1) : nullptr;
+}
+
+static int CompareAttachmentPartId(const char* aAttachUrlLeft,
+ const char* aAttachUrlRight) {
+ // part ids are numbers separated by periods, like "1.2.3.4".
+ // we sort by doing a numerical comparison on each item in turn. e.g. "1.4" <
+ // "1.25" shorter entries come before longer entries. e.g. "1.4" < "1.4.1.2"
+ // return values:
+ // -2 left is a parent of right
+ // -1 left is less than right
+ // 0 left == right
+ // 1 right is greater than left
+ // 2 right is a parent of left
+
+ const char* partIdLeft = GetAttachmentPartId(aAttachUrlLeft);
+ const char* partIdRight = GetAttachmentPartId(aAttachUrlRight);
+
+ // for detached attachments the URL does not contain any "part=xx"
+ if (!partIdLeft) partIdLeft = "0";
+
+ if (!partIdRight) partIdRight = "0";
+
+ long idLeft, idRight;
+ do {
+ MOZ_ASSERT(partIdLeft && IS_DIGIT(*partIdLeft),
+ "Invalid character in part id string");
+ MOZ_ASSERT(partIdRight && IS_DIGIT(*partIdRight),
+ "Invalid character in part id string");
+
+ // if the part numbers are different then the numerically smaller one is
+ // first
+ char* fixConstLoss;
+ idLeft = strtol(partIdLeft, &fixConstLoss, 10);
+ partIdLeft = fixConstLoss;
+ idRight = strtol(partIdRight, &fixConstLoss, 10);
+ partIdRight = fixConstLoss;
+ if (idLeft != idRight) return idLeft < idRight ? -1 : 1;
+
+ // if one part id is complete but the other isn't, then the shortest one
+ // is first (parents before children)
+ if (*partIdLeft != *partIdRight) return *partIdRight ? -2 : 2;
+
+ // if both part ids are complete (*partIdLeft == *partIdRight now) then
+ // they are equal
+ if (!*partIdLeft) return 0;
+
+ MOZ_ASSERT(*partIdLeft == '.', "Invalid character in part id string");
+ MOZ_ASSERT(*partIdRight == '.', "Invalid character in part id string");
+
+ ++partIdLeft;
+ ++partIdRight;
+ } while (true);
+}
+
+// ------------------------------------
+
+// struct on purpose -> show that we don't ever want a vtable
+struct msgAttachment {
+ msgAttachment(const nsACString& aContentType, const nsACString& aUrl,
+ const nsACString& aDisplayName, const nsACString& aMessageUri)
+ : mContentType(aContentType),
+ mUrl(aUrl),
+ mDisplayName(aDisplayName),
+ mMessageUri(aMessageUri) {}
+
+ nsCString mContentType;
+ nsCString mUrl;
+ nsCString mDisplayName;
+ nsCString mMessageUri;
+};
+
+// ------------------------------------
+
+class nsAttachmentState {
+ public:
+ nsAttachmentState();
+ nsresult Init(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray);
+ nsresult PrepareForAttachmentDelete();
+
+ private:
+ static int CompareAttachmentsByPartId(const void* aLeft, const void* aRight);
+
+ public:
+ uint32_t mCurIndex;
+ nsTArray<msgAttachment> mAttachmentArray;
+};
+
+nsAttachmentState::nsAttachmentState() : mCurIndex(0) {}
+
+nsresult nsAttachmentState::Init(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray) {
+ MOZ_ASSERT(aContentTypeArray.Length() > 0);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ uint32_t count = aContentTypeArray.Length();
+ mCurIndex = 0;
+ mAttachmentArray.Clear();
+ mAttachmentArray.SetCapacity(count);
+
+ for (uint32_t u = 0; u < count; ++u) {
+ mAttachmentArray.AppendElement(
+ msgAttachment(aContentTypeArray[u], aUrlArray[u], aDisplayNameArray[u],
+ aMessageUriArray[u]));
+ }
+
+ return NS_OK;
+}
+
+nsresult nsAttachmentState::PrepareForAttachmentDelete() {
+ // this must be called before any processing
+ if (mCurIndex != 0) return NS_ERROR_FAILURE;
+
+ // this prepares the attachment list for use in deletion. In order to prepare,
+ // we sort the attachments in numerical ascending order on their part id,
+ // remove all duplicates and remove any subparts which will be removed
+ // automatically by the removal of the parent.
+ //
+ // e.g. the attachment list processing (showing only part ids)
+ // before: 1.11, 1.3, 1.2, 1.2.1.3, 1.4.1.2
+ // sorted: 1.2, 1.2.1.3, 1.3, 1.4.1.2, 1.11
+ // after: 1.2, 1.3, 1.4.1.2, 1.11
+
+ // sort
+ qsort(mAttachmentArray.Elements(), mAttachmentArray.Length(),
+ sizeof(msgAttachment), CompareAttachmentsByPartId);
+
+ // remove duplicates and sub-items
+ int nCompare;
+ for (uint32_t u = 1; u < mAttachmentArray.Length();) {
+ nCompare = ::CompareAttachmentPartId(mAttachmentArray[u - 1].mUrl.get(),
+ mAttachmentArray[u].mUrl.get());
+ if (nCompare == 0 ||
+ nCompare == -2) // [u-1] is the same as or a parent of [u]
+ {
+ // shuffle the array down (and thus keeping the sorted order)
+ mAttachmentArray.RemoveElementAt(u);
+ } else {
+ ++u;
+ }
+ }
+
+ return NS_OK;
+}
+
+// Static compare callback for sorting.
+int nsAttachmentState::CompareAttachmentsByPartId(const void* aLeft,
+ const void* aRight) {
+ msgAttachment& attachLeft = *((msgAttachment*)aLeft);
+ msgAttachment& attachRight = *((msgAttachment*)aRight);
+ return ::CompareAttachmentPartId(attachLeft.mUrl.get(),
+ attachRight.mUrl.get());
+}
+
+// ------------------------------------
+// Helper class to coordinate deleting attachments from a message.
+//
+// Implementation notes:
+// The basic technique is to use nsIMsgMessageService.streamMessage() to
+// stream the message through a streamconverter which is set up to strip
+// out the attachments. The result is written out to a temporary file,
+// which is then copied over the old message using
+// nsIMsgCopyService.copyFileMessage() and the old message deleted with
+// nsIMsgFolder.deleteMessages(). Phew.
+//
+// The nsIStreamListener, nsIUrlListener and nsIMsgCopyServiceListener
+// inheritances here are just unfortunately-exposed implementation details.
+// And they are a bit of a mess. Some are used multiple times, for different
+// phases of the operation. So we use m_state to keep track.
+class AttachmentDeleter : public nsIStreamListener,
+ public nsIUrlListener,
+ public nsIMsgCopyServiceListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ public:
+ AttachmentDeleter();
+ nsresult StartProcessing(nsMessenger* aMessenger, nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach, bool aSaveFirst);
+
+ public:
+ nsAttachmentState* mAttach; // list of attachments to process
+ bool mSaveFirst; // detach (true) or delete (false)
+ nsCOMPtr<nsIFile> mMsgFile; // temporary file (processed mail)
+ nsCOMPtr<nsIOutputStream> mMsgFileStream; // temporary file (processed mail)
+ nsCOMPtr<nsIMsgMessageService> mMessageService; // original message service
+ nsCOMPtr<nsIMsgDBHdr> mOriginalMessage; // original message header
+ nsCOMPtr<nsIMsgFolder> mMessageFolder; // original message folder
+ nsCOMPtr<nsIMessenger> mMessenger; // our messenger instance
+ nsCOMPtr<nsIMsgWindow> mMsgWindow; // our UI window
+ nsMsgKey mOriginalMessageKey; // old message key
+ nsMsgKey mNewMessageKey; // new message key
+ uint32_t mOrigMsgFlags;
+
+ enum {
+ eStarting,
+ eCopyingNewMsg,
+ eUpdatingFolder, // for IMAP
+ eDeletingOldMessage,
+ eSelectingNewMessage
+ } m_state;
+ // temp
+ nsTArray<nsCString> mDetachedFileUris;
+
+ // The listener to invoke when the full operation is complete.
+ nsCOMPtr<nsIUrlListener> mListener;
+
+ private:
+ nsresult InternalStartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach, bool aSaveFirst);
+ nsresult DeleteOriginalMessage();
+ virtual ~AttachmentDeleter();
+};
+
+//
+// nsISupports
+//
+NS_IMPL_ISUPPORTS(AttachmentDeleter, nsIStreamListener, nsIRequestObserver,
+ nsIUrlListener, nsIMsgCopyServiceListener)
+
+//
+// nsIRequestObserver
+//
+NS_IMETHODIMP
+AttachmentDeleter::OnStartRequest(nsIRequest* aRequest) {
+ // called when we start processing the StreamMessage request.
+ // This is called after OnStartRunningUrl().
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
+ // called when we have completed processing the StreamMessage request.
+ // This is called before OnStopRunningUrl(). This means that we have now
+ // received all data of the message and we have completed processing.
+ // We now start to copy the processed message from the temporary file
+ // back into the message store, replacing the original message.
+
+ mMessageFolder->CopyDataDone();
+ if (NS_FAILED(aStatusCode)) return aStatusCode;
+
+ // copy the file back into the folder. Note: setting msgToReplace only copies
+ // metadata, so we do the delete ourselves
+ nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService;
+ nsresult rv = this->QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener),
+ getter_AddRefs(listenerCopyService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mMsgFileStream->Close();
+ mMsgFileStream = nullptr;
+ mNewMessageKey = nsMsgKey_None;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ m_state = eCopyingNewMsg;
+ // clone file because nsIFile on Windows caches the wrong file size.
+ nsCOMPtr<nsIFile> clone;
+ mMsgFile->Clone(getter_AddRefs(clone));
+ if (copyService) {
+ nsCString originalKeys;
+ mOriginalMessage->GetStringProperty("keywords", originalKeys);
+ rv = copyService->CopyFileMessage(clone, mMessageFolder, mOriginalMessage,
+ false, mOrigMsgFlags, originalKeys,
+ listenerCopyService, mMsgWindow);
+ }
+ return rv;
+}
+
+//
+// nsIStreamListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInStream,
+ uint64_t aSrcOffset, uint32_t aCount) {
+ if (!mMsgFileStream) return NS_ERROR_NULL_POINTER;
+ return mMessageFolder->CopyDataToOutputStreamForAppend(aInStream, aCount,
+ mMsgFileStream);
+}
+
+//
+// nsIUrlListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStartRunningUrl(nsIURI* aUrl) {
+ // called when we start processing the StreamMessage request. This is
+ // called before OnStartRequest().
+ return NS_OK;
+}
+
+nsresult AttachmentDeleter::DeleteOriginalMessage() {
+ nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService;
+ QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener),
+ getter_AddRefs(listenerCopyService));
+
+ mOriginalMessage->SetUint32Property("attachmentDetached", 1);
+ RefPtr<nsIMsgDBHdr> doomed(mOriginalMessage);
+ mOriginalMessage = nullptr;
+ m_state = eDeletingOldMessage;
+ return mMessageFolder->DeleteMessages({doomed}, // messages
+ mMsgWindow, // msgWindow
+ true, // deleteStorage
+ false, // isMove
+ listenerCopyService, // listener
+ false); // allowUndo
+}
+
+// This is called (potentially) multiple times.
+// Firstly, as a result of StreamMessage() (when the message is being passed
+// through a streamconverter to strip the attachments).
+// Secondly, after the DeleteMessages() call. But maybe not for IMAP?
+// Maybe also after CopyFileMessage()? Gah.
+NS_IMETHODIMP
+AttachmentDeleter::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ nsresult rv = NS_OK;
+ if (mOriginalMessage && m_state == eUpdatingFolder)
+ rv = DeleteOriginalMessage();
+
+ return rv;
+}
+
+//
+// nsIMsgCopyServiceListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStartCopy(void) {
+ // never called?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ // never called?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::SetMessageKey(nsMsgKey aKey) {
+ // called during the copy of the modified message back into the message
+ // store to notify us of the message key of the newly created message.
+ mNewMessageKey = aKey;
+
+ nsCString folderURI;
+ nsresult rv = mMessageFolder->GetURI(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozilla::JSONStringWriteFunc<nsCString> jsonString;
+ mozilla::JSONWriter data(jsonString);
+ data.Start();
+ data.IntProperty("oldMessageKey", mOriginalMessageKey);
+ data.IntProperty("newMessageKey", aKey);
+ data.StringProperty("folderURI", folderURI);
+ data.End();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "attachment-delete-msgkey-changed",
+ NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::GetMessageId(nsACString& aMessageId) {
+ // never called?
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStopCopy(nsresult aStatus) {
+ // This is called via `CopyFileMessage()` and `DeleteMessages()`.
+ // `m_state` tells us which callback it is.
+ if (m_state == eDeletingOldMessage) {
+ m_state = eSelectingNewMessage;
+
+ // OK... that's it. The entire operation is now done.
+ // (there may still be another call to OnStopRunningUrl(), but that'll be
+ // a no-op in this state).
+ if (mListener) {
+ mListener->OnStopRunningUrl(nullptr, aStatus);
+ }
+ return NS_OK;
+ }
+
+ // For non-IMAP messages, the original is deleted here, for IMAP messages
+ // that happens in `OnStopRunningUrl()` which isn't called for non-IMAP
+ // messages.
+ const nsACString& messageUri = mAttach->mAttachmentArray[0].mMessageUri;
+ if (mOriginalMessage &&
+ !Substring(messageUri, 0, 13).EqualsLiteral("imap-message:")) {
+ return DeleteOriginalMessage();
+ } else {
+ // Arrange for the message to be deleted in the next `OnStopRunningUrl()`
+ // call.
+ m_state = eUpdatingFolder;
+ }
+
+ return NS_OK;
+}
+
+//
+// local methods
+//
+
+AttachmentDeleter::AttachmentDeleter()
+ : mAttach(nullptr),
+ mSaveFirst(false),
+ mOriginalMessageKey(nsMsgKey_None),
+ mNewMessageKey(nsMsgKey_None),
+ mOrigMsgFlags(0),
+ m_state(eStarting) {}
+
+AttachmentDeleter::~AttachmentDeleter() {
+ if (mAttach) {
+ delete mAttach;
+ }
+ if (mMsgFileStream) {
+ mMsgFileStream->Close();
+ mMsgFileStream = nullptr;
+ }
+ if (mMsgFile) {
+ mMsgFile->Remove(false);
+ }
+}
+
+nsresult AttachmentDeleter::StartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach,
+ bool detaching) {
+ if (mListener) {
+ mListener->OnStartRunningUrl(nullptr);
+ }
+
+ nsresult rv =
+ InternalStartProcessing(aMessenger, aMsgWindow, aAttach, detaching);
+ if (NS_FAILED(rv)) {
+ if (mListener) {
+ mListener->OnStopRunningUrl(nullptr, rv);
+ }
+ }
+ return rv;
+}
+
+nsresult AttachmentDeleter::InternalStartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach,
+ bool detaching) {
+ aMessenger->QueryInterface(NS_GET_IID(nsIMessenger),
+ getter_AddRefs(mMessenger));
+ mMsgWindow = aMsgWindow;
+ mAttach = aAttach;
+
+ nsresult rv;
+
+ // all attachments refer to the same message
+ const nsCString& messageUri = mAttach->mAttachmentArray[0].mMessageUri;
+
+ // get the message service, original message and folder for this message
+ rv = GetMessageServiceFromURI(messageUri, getter_AddRefs(mMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMessageService->MessageURIToMsgHdr(messageUri,
+ getter_AddRefs(mOriginalMessage));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mOriginalMessage->GetMessageKey(&mOriginalMessageKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mOriginalMessage->GetFolder(getter_AddRefs(mMessageFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mOriginalMessage->GetFlags(&mOrigMsgFlags);
+
+ // ensure that we can store and delete messages in this folder, if we
+ // can't then we can't do attachment deleting
+ bool canDelete = false;
+ mMessageFolder->GetCanDeleteMessages(&canDelete);
+ bool canFile = false;
+ mMessageFolder->GetCanFileMessages(&canFile);
+ if (!canDelete || !canFile) return NS_ERROR_FAILURE;
+
+ // create an output stream on a temporary file. This stream will save the
+ // modified message data to a file which we will later use to replace the
+ // existing message. The file is removed in the destructor.
+ rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp",
+ getter_AddRefs(mMsgFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For temp file, we should use restrictive 00600 instead of
+ // ATTACHMENT_PERMISSION
+ rv = mMsgFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mMsgFileStream), mMsgFile,
+ -1, ATTACHMENT_PERMISSION);
+
+ // Create the additional header for data conversion. This will tell the stream
+ // converter which MIME emitter we want to use, and it will tell the MIME
+ // emitter which attachments should be deleted.
+ // It also supplies the path of the already-saved attachments, so that
+ // path can be noted in the message, where those attachements are removed.
+ // The X-Mozilla-External-Attachment-URL header will be added, with the
+ // location of the saved attachment.
+ const char* partId;
+ const char* nextField;
+ nsAutoCString sHeader("attach&del=");
+ nsAutoCString detachToHeader("&detachTo=");
+ for (uint32_t u = 0; u < mAttach->mAttachmentArray.Length(); ++u) {
+ if (u > 0) {
+ sHeader.Append(',');
+ if (detaching) detachToHeader.Append(',');
+ }
+ partId = GetAttachmentPartId(mAttach->mAttachmentArray[u].mUrl.get());
+ if (partId) {
+ nextField = PL_strchr(partId, '&');
+ sHeader.Append(partId, nextField ? nextField - partId : -1);
+ }
+ if (detaching) {
+ // The URI can contain commas, so percent-encode those first.
+ nsAutoCString uri(mDetachedFileUris[u]);
+ int ind = uri.FindChar(',');
+ while (ind != kNotFound) {
+ uri.Replace(ind, 1, "%2C");
+ ind = uri.FindChar(',');
+ }
+ detachToHeader.Append(uri);
+ }
+ }
+
+ if (detaching) sHeader.Append(detachToHeader);
+ // stream this message to our listener converting it via the attachment mime
+ // converter. The listener will just write the converted message straight to
+ // disk.
+ nsCOMPtr<nsISupports> listenerSupports;
+ rv = this->QueryInterface(NS_GET_IID(nsISupports),
+ getter_AddRefs(listenerSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> listenerUrlListener =
+ do_QueryInterface(listenerSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = mMessageService->StreamMessage(messageUri, listenerSupports, mMsgWindow,
+ listenerUrlListener, true, sHeader, false,
+ getter_AddRefs(dummyNull));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// ------------------------------------
+
+NS_IMETHODIMP
+nsMessenger::DetachAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri, bool aSaveFirst,
+ bool withoutWarning = false) {
+ if (aSaveFirst)
+ return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri,
+ true);
+ AutoTArray<nsCString, 1> contentTypeArray = {
+ PromiseFlatCString(aContentType)};
+ AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)};
+ AutoTArray<nsCString, 1> displayNameArray = {
+ PromiseFlatCString(aDisplayName)};
+ AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)};
+ return DetachAttachments(contentTypeArray, urlArray, displayNameArray,
+ messageUriArray, nullptr, nullptr, withoutWarning);
+}
+
+NS_IMETHODIMP
+nsMessenger::DetachAllAttachments(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ bool aSaveFirst,
+ bool withoutWarning = false) {
+ NS_ENSURE_ARG_MIN(aContentTypeArray.Length(), 1);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ if (aSaveFirst)
+ return SaveAllAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray, true);
+ else
+ return DetachAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray, nullptr, nullptr,
+ withoutWarning);
+}
+
+nsresult nsMessenger::DetachAttachments(
+ const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ nsTArray<nsCString>* saveFileUris, nsIUrlListener* aListener,
+ bool withoutWarning) {
+ // if withoutWarning no dialog for user
+ if (!withoutWarning && NS_FAILED(PromptIfDeleteAttachments(
+ saveFileUris != nullptr, aDisplayNameArray)))
+ return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ // ensure that our arguments are valid
+ // char * partId;
+ for (uint32_t u = 0; u < aContentTypeArray.Length(); ++u) {
+ // ensure all of the message URI are the same, we cannot process
+ // attachments from different messages
+ if (u > 0 && aMessageUriArray[0] != aMessageUriArray[u]) {
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ // ensure that we don't have deleted messages in this list
+ if (aContentTypeArray[u].EqualsLiteral(MIMETYPE_DELETED)) {
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ // for the moment we prevent any attachments other than root level
+ // attachments being deleted (i.e. you can't delete attachments from a
+ // email forwarded as an attachment). We do this by ensuring that the
+ // part id only has a single period in it (e.g. "1.2").
+ // TODO: support non-root level attachment delete
+ // partId = ::GetAttachmentPartId(aUrlArray[u]);
+ // if (!partId || PL_strchr(partId, '.') != PL_strrchr(partId, '.'))
+ // {
+ // rv = NS_ERROR_INVALID_ARG;
+ // break;
+ // }
+ }
+ if (NS_FAILED(rv)) {
+ Alert("deleteAttachmentFailure");
+ return rv;
+ }
+
+ // TODO: ensure that nothing else is processing this message uri at the same
+ // time
+
+ // TODO: if any of the selected attachments are messages that contain other
+ // attachments we need to warn the user that all sub-attachments of those
+ // messages will also be deleted. Best to display a list of them.
+
+ RefPtr<AttachmentDeleter> deleter = new AttachmentDeleter;
+ deleter->mListener = aListener;
+ if (saveFileUris) {
+ deleter->mDetachedFileUris = saveFileUris->Clone();
+ }
+ // create the attachments for use by the deleter
+ nsAttachmentState* attach = new nsAttachmentState;
+ rv = attach->Init(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray);
+ if (NS_SUCCEEDED(rv)) rv = attach->PrepareForAttachmentDelete();
+ if (NS_FAILED(rv)) {
+ delete attach;
+ return rv;
+ }
+
+ // initialize our deleter with the attachments and details. The deleter
+ // takes ownership of 'attach' immediately irrespective of the return value
+ // (error or not).
+ return deleter->StartProcessing(this, mMsgWindow, attach,
+ saveFileUris != nullptr);
+}
+
+nsresult nsMessenger::PromptIfDeleteAttachments(
+ bool aSaveFirst, const nsTArray<nsCString>& aDisplayNameArray) {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+ if (!dialog) return rv;
+
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // create the list of attachments we are removing
+ nsString displayString;
+ nsString attachmentList;
+ for (uint32_t u = 0; u < aDisplayNameArray.Length(); ++u) {
+ ConvertAndSanitizeFileName(aDisplayNameArray[u], displayString);
+ attachmentList.Append(displayString);
+ attachmentList.Append(char16_t('\n'));
+ }
+ AutoTArray<nsString, 1> formatStrings = {attachmentList};
+
+ // format the message and display
+ nsString promptMessage;
+ const char* propertyName =
+ aSaveFirst ? "detachAttachments" : "deleteAttachments";
+ rv = mStringBundle->FormatStringFromName(propertyName, formatStrings,
+ promptMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool dialogResult = false;
+ rv = dialog->Confirm(nullptr, promptMessage.get(), &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dialogResult ? NS_OK : NS_ERROR_FAILURE;
+}
diff --git a/comm/mailnews/base/src/nsMessenger.h b/comm/mailnews/base/src/nsMessenger.h
new file mode 100644
index 0000000000..b6eab3f179
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessenger.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMsgAppCore_h
+#define __nsMsgAppCore_h
+
+#include "nscore.h"
+#include "nsIMessenger.h"
+#include "nsCOMPtr.h"
+#include "nsITransactionManager.h"
+#include "nsIFile.h"
+#include "nsIDocShell.h"
+#include "nsString.h"
+#include "nsIStringBundle.h"
+#include "nsIFile.h"
+#include "nsIFilePicker.h"
+#include "nsWeakReference.h"
+#include "mozIDOMWindow.h"
+#include "nsTArray.h"
+#include "nsIMsgStatusFeedback.h"
+
+class nsSaveAllAttachmentsState;
+
+class nsMessenger : public nsIMessenger, public nsSupportsWeakReference {
+ using PathString = mozilla::PathString;
+
+ public:
+ nsMessenger();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGER
+
+ nsresult Alert(const char* stringName);
+
+ nsresult SaveAttachment(nsIFile* file, const nsACString& unescapedUrl,
+ const nsACString& messageUri,
+ const nsACString& contentType,
+ nsSaveAllAttachmentsState* saveState,
+ nsIUrlListener* aListener);
+ nsresult PromptIfFileExists(nsIFile* file);
+ nsresult DetachAttachments(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ nsTArray<nsCString>* saveFileUris,
+ nsIUrlListener* aListener,
+ bool withoutWarning = false);
+ nsresult SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray,
+ bool detaching);
+ nsresult SaveOneAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri, bool detaching);
+
+ protected:
+ virtual ~nsMessenger();
+
+ void GetString(const nsString& aStringName, nsString& stringValue);
+ nsresult InitStringBundle();
+ nsresult PromptIfDeleteAttachments(
+ bool saveFirst, const nsTArray<nsCString>& displayNameArray);
+
+ private:
+ nsresult GetLastSaveDirectory(nsIFile** aLastSaveAsDir);
+ // if aLocalFile is a dir, we use it. otherwise, we use the parent of
+ // aLocalFile.
+ nsresult SetLastSaveDirectory(nsIFile* aLocalFile);
+
+ nsresult AdjustFileIfNameTooLong(nsIFile* aFile);
+
+ nsresult GetSaveAsFile(const nsAString& aMsgFilename,
+ int32_t* aSaveAsFileType, nsIFile** aSaveAsFile);
+
+ nsresult GetSaveToDir(nsIFile** aSaveToDir);
+ nsresult ShowPicker(nsIFilePicker* aPicker,
+ nsIFilePicker::ResultCode* aResult);
+
+ class nsFilePickerShownCallback : public nsIFilePickerShownCallback {
+ virtual ~nsFilePickerShownCallback() {}
+
+ public:
+ nsFilePickerShownCallback();
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Done(nsIFilePicker::ResultCode aResult) override;
+
+ public:
+ bool mPickerDone;
+ nsIFilePicker::ResultCode mResult;
+ };
+
+ nsString mId;
+ nsCOMPtr<nsITransactionManager> mTxnMgr;
+
+ /* rhp - need this to drive message display */
+ nsCOMPtr<mozIDOMWindowProxy> mWindow;
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ nsCOMPtr<nsIDocShell> mDocShell;
+
+ // String bundles...
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+
+ nsCOMPtr<nsISupports> mSearchContext;
+};
+
+#define NS_MESSENGER_CID \
+ { /* f436a174-e2c0-4955-9afe-e3feb68aee56 */ \
+ 0xf436a174, 0xe2c0, 0x4955, { \
+ 0x9a, 0xfe, 0xe3, 0xfe, 0xb6, 0x8a, 0xee, 0x56 \
+ } \
+ }
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.cpp b/comm/mailnews/base/src/nsMessengerBootstrap.cpp
new file mode 100644
index 0000000000..9d01b5380d
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerBootstrap.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMessengerBootstrap.h"
+#include "nsCOMPtr.h"
+
+#include "nsIMutableArray.h"
+#include "nsIMsgFolder.h"
+#include "nsIWindowWatcher.h"
+#include "nsMsgUtils.h"
+#include "nsISupportsPrimitives.h"
+#include "mozIDOMWindow.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMessengerBootstrap, nsIMessengerWindowService)
+
+nsMessengerBootstrap::nsMessengerBootstrap() {}
+
+nsMessengerBootstrap::~nsMessengerBootstrap() {}
+
+NS_IMETHODIMP nsMessengerBootstrap::OpenMessengerWindowWithUri(
+ const char* windowType, const nsACString& aFolderURI,
+ nsMsgKey aMessageKey) {
+ bool standAloneMsgWindow = false;
+ nsAutoCString chromeUrl("chrome://messenger/content/");
+ if (windowType && !strcmp(windowType, "mail:messageWindow")) {
+ chromeUrl.AppendLiteral("messageWindow.xhtml");
+ standAloneMsgWindow = true;
+ } else {
+ chromeUrl.AppendLiteral("messenger.xhtml");
+ }
+ nsresult rv;
+ nsCOMPtr<nsIMutableArray> argsArray(
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // create scriptable versions of our strings that we can store in our
+ // nsIMutableArray....
+ if (!aFolderURI.IsEmpty()) {
+ if (standAloneMsgWindow) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetExistingFolder(aFolderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString msgUri;
+ folder->GetBaseMessageURI(msgUri);
+
+ nsCOMPtr<nsISupportsCString> scriptableMsgURI(
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableMsgURI, NS_ERROR_FAILURE);
+ msgUri.Append('#');
+ msgUri.AppendInt(aMessageKey, 10);
+ scriptableMsgURI->SetData(msgUri);
+ argsArray->AppendElement(scriptableMsgURI);
+ }
+ nsCOMPtr<nsISupportsCString> scriptableFolderURI(
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableFolderURI, NS_ERROR_FAILURE);
+
+ scriptableFolderURI->SetData(aFolderURI);
+ argsArray->AppendElement(scriptableFolderURI);
+
+ if (!standAloneMsgWindow) {
+ nsCOMPtr<nsISupportsPRUint32> scriptableMessageKey(
+ do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableMessageKey, NS_ERROR_FAILURE);
+ scriptableMessageKey->SetData(aMessageKey);
+ argsArray->AppendElement(scriptableMessageKey);
+ }
+ }
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to use the "mailnews.reuse_thread_window2" pref
+ // to determine if we should open a new window, or use an existing one.
+ nsCOMPtr<mozIDOMWindowProxy> newWindow;
+ return wwatch->OpenWindow(0, chromeUrl, "_blank"_ns,
+ "chrome,all,dialog=no"_ns, argsArray,
+ getter_AddRefs(newWindow));
+}
diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.h b/comm/mailnews/base/src/nsMessengerBootstrap.h
new file mode 100644
index 0000000000..a81e81eeb9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerBootstrap.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMessenger_h
+#define __nsMessenger_h
+
+#include "nscore.h"
+#include "nsIMessengerWindowService.h"
+
+#define NS_MESSENGERBOOTSTRAP_CID \
+ { /* 4a85a5d0-cddd-11d2-b7f6-00805f05ffa5 */ \
+ 0x4a85a5d0, 0xcddd, 0x11d2, { \
+ 0xb7, 0xf6, 0x00, 0x80, 0x5f, 0x05, 0xff, 0xa5 \
+ } \
+ }
+
+class nsMessengerBootstrap : public nsIMessengerWindowService {
+ public:
+ nsMessengerBootstrap();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMESSENGERWINDOWSERVICE
+
+ private:
+ virtual ~nsMessengerBootstrap();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.h b/comm/mailnews/base/src/nsMessengerOSXIntegration.h
new file mode 100644
index 0000000000..e4b503cb12
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMessengerOSXIntegration_h
+#define __nsMessengerOSXIntegration_h
+
+#include "nsIMessengerOSIntegration.h"
+
+class nsMessengerOSXIntegration : public nsIMessengerOSIntegration {
+ public:
+ nsMessengerOSXIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ virtual ~nsMessengerOSXIntegration();
+
+ nsresult RestoreDockIcon();
+};
+
+#endif // __nsMessengerOSXIntegration_h
diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.mm b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm
new file mode 100644
index 0000000000..858e1417df
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMessengerOSXIntegration.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "mozilla/ErrorResult.h"
+
+#include <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+
+nsMessengerOSXIntegration::nsMessengerOSXIntegration() {}
+
+nsMessengerOSXIntegration::~nsMessengerOSXIntegration() {}
+
+NS_IMPL_ADDREF(nsMessengerOSXIntegration)
+NS_IMPL_RELEASE(nsMessengerOSXIntegration)
+
+NS_INTERFACE_MAP_BEGIN(nsMessengerOSXIntegration)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration)
+NS_INTERFACE_MAP_END
+
+nsresult nsMessengerOSXIntegration::RestoreDockIcon() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ id tile = [[NSApplication sharedApplication] dockTile];
+ [tile setBadgeLabel:nil];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMessengerOSXIntegration::UpdateUnreadCount(uint32_t unreadCount, const nsAString& unreadTooltip) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (unreadCount == 0) {
+ RestoreDockIcon();
+ return NS_OK;
+ }
+
+ nsAutoString total;
+ if (unreadCount > 99) {
+ total.AppendLiteral("99+");
+ } else {
+ total.AppendInt(unreadCount);
+ }
+ id tile = [[NSApplication sharedApplication] dockTile];
+ [tile setBadgeLabel:[NSString stringWithFormat:@"%S", (const unichar*)total.get()]];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMessengerOSXIntegration::OnExit() {
+ RestoreDockIcon();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp
new file mode 100644
index 0000000000..96ddb9c48a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMessengerUnixIntegration.h"
+#include "nsString.h"
+
+/**
+ * This is only a placeholder for now, register it in components.conf later if
+ * needed.
+ */
+nsMessengerUnixIntegration::nsMessengerUnixIntegration() {}
+
+NS_IMPL_ISUPPORTS(nsMessengerUnixIntegration, nsIMessengerOSIntegration)
+
+NS_IMETHODIMP
+nsMessengerUnixIntegration::UpdateUnreadCount(uint32_t unreadCount,
+ const nsAString& unreadTooltip) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerUnixIntegration::OnExit() { return NS_OK; }
diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.h b/comm/mailnews/base/src/nsMessengerUnixIntegration.h
new file mode 100644
index 0000000000..180f268560
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMessengerUnixIntegration_h
+#define __nsMessengerUnixIntegration_h
+
+#include "nsIMessengerOSIntegration.h"
+
+class nsMessengerUnixIntegration : public nsIMessengerOSIntegration {
+ public:
+ nsMessengerUnixIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ virtual ~nsMessengerUnixIntegration() {}
+};
+
+#endif // __nsMessengerUnixIntegration_h
diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.cpp b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp
new file mode 100644
index 0000000000..7db74af5e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp
@@ -0,0 +1,379 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <windows.h>
+#include <shellapi.h>
+#include <strsafe.h>
+
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "mozIDOMWindow.h"
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsIMsgWindow.h"
+#include "nsIObserverService.h"
+#include "nsIPrefService.h"
+#include "nsIWidget.h"
+#include "nsIWindowMediator.h"
+#include "nsMessengerWinIntegration.h"
+#include "nsMsgDBFolder.h"
+#include "nsPIDOMWindow.h"
+
+#define IDI_MAILBIFF 32576
+#define SHOW_TRAY_ICON_PREF "mail.biff.show_tray_icon"
+#define SHOW_TRAY_ICON_ALWAYS_PREF "mail.biff.show_tray_icon_always"
+
+// since we are including windows.h in this file, undefine get user name....
+#ifdef GetUserName
+# undef GetUserName
+#endif
+
+#ifndef NIIF_USER
+# define NIIF_USER 0x00000004
+#endif
+
+#ifndef NIIF_NOSOUND
+# define NIIF_NOSOUND 0x00000010
+#endif
+
+using namespace mozilla;
+
+nsMessengerWinIntegration::nsMessengerWinIntegration() {}
+
+nsMessengerWinIntegration::~nsMessengerWinIntegration() {}
+
+NS_IMPL_ADDREF(nsMessengerWinIntegration)
+NS_IMPL_RELEASE(nsMessengerWinIntegration)
+
+NS_INTERFACE_MAP_BEGIN(nsMessengerWinIntegration)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerWindowsIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration)
+NS_INTERFACE_MAP_END
+
+static HWND hwndForDOMWindow(mozIDOMWindowProxy* window) {
+ if (!window) {
+ return 0;
+ }
+ nsCOMPtr<nsPIDOMWindowOuter> pidomwindow = nsPIDOMWindowOuter::From(window);
+
+ nsCOMPtr<nsIBaseWindow> ppBaseWindow =
+ do_QueryInterface(pidomwindow->GetDocShell());
+ if (!ppBaseWindow) return 0;
+
+ nsCOMPtr<nsIWidget> ppWidget;
+ ppBaseWindow->GetMainWidget(getter_AddRefs(ppWidget));
+
+ return (HWND)(ppWidget->GetNativeData(NS_NATIVE_WIDGET));
+}
+
+static void activateWindow(mozIDOMWindowProxy* win) {
+ // Try to get native window handle.
+ HWND hwnd = hwndForDOMWindow(win);
+ if (hwnd) {
+ // Restore the window if it is minimized.
+ if (::IsIconic(hwnd)) ::ShowWindow(hwnd, SW_RESTORE);
+ // Use the OS call, if possible.
+ ::SetForegroundWindow(hwnd);
+ } else {
+ // Use internal method.
+ nsCOMPtr<nsPIDOMWindowOuter> privateWindow = nsPIDOMWindowOuter::From(win);
+ privateWindow->Focus(mozilla::dom::CallerType::System);
+ }
+}
+
+NOTIFYICONDATAW sMailIconData = {
+ /* cbSize */ (DWORD)NOTIFYICONDATAW_V2_SIZE,
+ /* hWnd */ 0,
+ /* uID */ 2,
+ /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO,
+ /* uCallbackMessage */ WM_USER,
+ /* hIcon */ 0,
+ /* szTip */ L"",
+ /* dwState */ 0,
+ /* dwStateMask */ 0,
+ /* szInfo */ L"",
+ /* uVersion */ {30000},
+ /* szInfoTitle */ L"",
+ /* dwInfoFlags */ NIIF_USER | NIIF_NOSOUND};
+
+static nsCOMArray<nsIBaseWindow> sHiddenWindows;
+static HWND sIconWindow;
+static uint32_t sUnreadCount;
+/* static */
+LRESULT CALLBACK nsMessengerWinIntegration::IconWindowProc(HWND msgWindow,
+ UINT msg, WPARAM wp,
+ LPARAM lp) {
+ nsresult rv;
+ static UINT sTaskbarRecreated;
+
+ switch (msg) {
+ case WM_USER:
+ if (msg == WM_USER && lp == WM_LBUTTONDOWN) {
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ bool showTrayIcon;
+ rv = prefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ bool showTrayIconAlways;
+ if (NS_FAILED(prefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF,
+ &showTrayIconAlways))) {
+ showTrayIconAlways = false;
+ }
+ if ((!showTrayIcon || !sUnreadCount) && !showTrayIconAlways) {
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>(
+ ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) {
+ instance->mTrayIconShown = false;
+ }
+ }
+
+ // No minimzed window, bring the most recent 3pane window to the front.
+ if (sHiddenWindows.Length() == 0) {
+ nsCOMPtr<nsIWindowMediator> windowMediator =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ rv = windowMediator->GetMostRecentBrowserWindow(
+ getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ if (domWindow) {
+ activateWindow(domWindow);
+ return TRUE;
+ }
+ }
+
+ // Bring the minimized windows to the front.
+ for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) {
+ auto window = sHiddenWindows.SafeElementAt(i);
+ if (!window) {
+ continue;
+ }
+ window->SetVisibility(true);
+
+ nsCOMPtr<nsIWidget> widget;
+ window->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ continue;
+ }
+
+ HWND hwnd = (HWND)(widget->GetNativeData(NS_NATIVE_WIDGET));
+ ::ShowWindow(hwnd, SW_RESTORE);
+ ::SetForegroundWindow(hwnd);
+
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(window, "windows-refresh-badge-tray", 0);
+ }
+
+ sHiddenWindows.Clear();
+ }
+ break;
+ case WM_CREATE:
+ sTaskbarRecreated = ::RegisterWindowMessageW(L"TaskbarCreated");
+ break;
+ default:
+ if (msg == sTaskbarRecreated) {
+ // When taskbar is recreated (e.g. by restarting Windows Explorer), all
+ // tray icons are removed. If there are windows minimized to tray icon,
+ // we have to recreate the tray icon, otherwise the windows can't be
+ // restored.
+ if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>(
+ ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) {
+ instance->mTrayIconShown = false;
+ }
+ for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) {
+ auto window = sHiddenWindows.SafeElementAt(i);
+ if (!window) {
+ continue;
+ }
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(window, "windows-refresh-badge-tray", 0);
+ }
+ }
+ break;
+ }
+ return ::DefWindowProc(msgWindow, msg, wp, lp);
+}
+
+nsresult nsMessengerWinIntegration::HideWindow(nsIBaseWindow* aWindow) {
+ NS_ENSURE_ARG(aWindow);
+ aWindow->SetVisibility(false);
+ sHiddenWindows.AppendElement(aWindow);
+
+ nsresult rv;
+ rv = CreateIconWindow();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mTrayIconShown) {
+ auto idi = IDI_APPLICATION;
+ if (sUnreadCount > 0) {
+ idi = MAKEINTRESOURCE(IDI_MAILBIFF);
+ }
+ sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi);
+ nsresult rv = SetTooltip();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ::Shell_NotifyIconW(NIM_ADD, &sMailIconData);
+ ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData);
+ mTrayIconShown = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::ShowWindow(mozIDOMWindowProxy* aWindow) {
+ activateWindow(aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::UpdateUnreadCount(uint32_t unreadCount,
+ const nsAString& unreadTooltip) {
+ sUnreadCount = unreadCount;
+ mUnreadTooltip = unreadTooltip;
+ nsresult rv = UpdateTrayIcon();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::OnExit() {
+ if (mTrayIconShown) {
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ mTrayIconShown = false;
+ }
+ return NS_OK;
+}
+
+/**
+ * Set a tooltip to the tray icon. Including the brand short name, and unread
+ * message count.
+ */
+nsresult nsMessengerWinIntegration::SetTooltip() {
+ nsresult rv = NS_OK;
+ if (mBrandShortName.IsEmpty()) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = bundle->GetStringFromName("brandShortName", mBrandShortName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsString tooltip = mBrandShortName;
+ if (!mUnreadTooltip.IsEmpty()) {
+ tooltip.AppendLiteral("\n");
+ tooltip.Append(mUnreadTooltip);
+ }
+ size_t destLength =
+ sizeof sMailIconData.szTip / (sizeof sMailIconData.szTip[0]);
+ ::StringCchCopyNW(sMailIconData.szTip, destLength, tooltip.get(),
+ tooltip.Length());
+ return rv;
+}
+
+/**
+ * Create a custom window for the taskbar icon if it's not created yet.
+ */
+nsresult nsMessengerWinIntegration::CreateIconWindow() {
+ if (sMailIconData.hWnd) {
+ return NS_OK;
+ }
+
+ const wchar_t kClassName[] = L"IconWindowClass";
+ WNDCLASS classStruct = {/* style */ 0,
+ /* lpfnWndProc */ &IconWindowProc,
+ /* cbClsExtra */ 0,
+ /* cbWndExtra */ 0,
+ /* hInstance */ 0,
+ /* hIcon */ 0,
+ /* hCursor */ 0,
+ /* hbrBackground */ 0,
+ /* lpszMenuName */ 0,
+ /* lpszClassName */ kClassName};
+
+ // Register the window class.
+ NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE);
+ // Create the window.
+ NS_ENSURE_TRUE(sIconWindow = ::CreateWindow(
+ /* className */ kClassName,
+ /* title */ 0,
+ /* style */ WS_CAPTION,
+ /* x, y, cx, cy */ 0, 0, 0, 0,
+ /* parent */ 0,
+ /* menu */ 0,
+ /* instance */ 0,
+ /* create struct */ 0),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(::SetWindowLongPtrW(sIconWindow, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(this)) == 0,
+ NS_ERROR_FAILURE);
+
+ sMailIconData.hWnd = sIconWindow;
+ return NS_OK;
+}
+
+/**
+ * Update the tray icon according to the current unread count and pref value.
+ */
+nsresult nsMessengerWinIntegration::UpdateTrayIcon() {
+ nsresult rv;
+
+ rv = CreateIconWindow();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mPrefBranch) {
+ mPrefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = SetTooltip();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool showTrayIconAlways;
+ if (NS_FAILED(mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF,
+ &showTrayIconAlways))) {
+ showTrayIconAlways = false;
+ }
+ if (sUnreadCount > 0 || showTrayIconAlways) {
+ auto idi = IDI_APPLICATION;
+ if (sUnreadCount > 0) {
+ // Only showing the new mail marker when there are actual unread mail
+ idi = MAKEINTRESOURCE(IDI_MAILBIFF);
+ }
+ sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi);
+ if (mTrayIconShown) {
+ // If the tray icon is already shown, just modify it.
+ ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData);
+ } else {
+ bool showTrayIcon;
+ rv = mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (showTrayIcon) {
+ // Show a tray icon only if the pref value is true.
+ ::Shell_NotifyIconW(NIM_ADD, &sMailIconData);
+ ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData);
+ mTrayIconShown = true;
+ }
+ }
+ } else if (mTrayIconShown) {
+ if (sHiddenWindows.Length() > 0) {
+ // At least one window is minimized, modify the icon only.
+ sMailIconData.hIcon =
+ ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION);
+ ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData);
+ } else if (!showTrayIconAlways) {
+ // No unread, no need to show the tray icon.
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ mTrayIconShown = false;
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.h b/comm/mailnews/base/src/nsMessengerWinIntegration.h
new file mode 100644
index 0000000000..0fb9f1a718
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerWinIntegration.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMessengerWinIntegration_h
+#define __nsMessengerWinIntegration_h
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIMessengerWindowsIntegration.h"
+#include "nsIStringBundle.h"
+#include "nsIPrefBranch.h"
+
+class nsMessengerWinIntegration : public nsIMessengerWindowsIntegration {
+ public:
+ nsMessengerWinIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGERWINDOWSINTEGRATION
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ static LRESULT CALLBACK IconWindowProc(HWND msgWindow, UINT msg, WPARAM wp,
+ LPARAM lp);
+
+ virtual ~nsMessengerWinIntegration();
+
+ nsresult CreateIconWindow();
+ nsresult SetTooltip();
+ nsresult UpdateTrayIcon();
+
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ bool mTrayIconShown = false;
+ nsString mBrandShortName;
+ nsString mUnreadTooltip;
+};
+
+#endif // __nsMessengerWinIntegration_h
diff --git a/comm/mailnews/base/src/nsMsgAccount.cpp b/comm/mailnews/base/src/nsMsgAccount.cpp
new file mode 100644
index 0000000000..8751e4cdde
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccount.cpp
@@ -0,0 +1,413 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "prprf.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsCRTGlue.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsPrintfCString.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsMsgAccount.h"
+#include "nsIMsgAccount.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgAccount, nsIMsgAccount)
+
+nsMsgAccount::nsMsgAccount()
+ : m_identitiesValid(false), mTriedToGetServer(false) {}
+
+nsMsgAccount::~nsMsgAccount() {}
+
+nsresult nsMsgAccount::getPrefService() {
+ if (m_prefs) return NS_OK;
+
+ nsresult rv;
+ NS_ENSURE_FALSE(m_accountKey.IsEmpty(), NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString accountRoot("mail.account.");
+ accountRoot.Append(m_accountKey);
+ accountRoot.Append('.');
+ return prefs->GetBranch(accountRoot.get(), getter_AddRefs(m_prefs));
+}
+
+NS_IMETHODIMP
+nsMsgAccount::GetIncomingServer(nsIMsgIncomingServer** aIncomingServer) {
+ NS_ENSURE_ARG_POINTER(aIncomingServer);
+
+ // create the incoming server lazily
+ if (!mTriedToGetServer && !m_incomingServer) {
+ mTriedToGetServer = true;
+ // ignore the error (and return null), but it's still bad so warn
+ mozilla::DebugOnly<nsresult> rv = createIncomingServer();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "couldn't lazily create the server\n");
+ }
+
+ NS_IF_ADDREF(*aIncomingServer = m_incomingServer);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::CreateServer() {
+ if (m_incomingServer) return NS_ERROR_ALREADY_INITIALIZED;
+ return createIncomingServer();
+}
+
+nsresult nsMsgAccount::createIncomingServer() {
+ // from here, load mail.account.myaccount.server
+ // Load the incoming server
+ //
+ // ex) mail.account.myaccount.server = "myserver"
+
+ nsresult rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the "server" pref
+ nsCString serverKey;
+ rv = m_prefs->GetCharPref("server", serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the server from the account manager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->GetIncomingServer(serverKey, getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ rv = server->GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hostname.IsEmpty()) {
+ NS_WARNING(
+ nsPrintfCString("Server had no hostname; key=%s", serverKey.get())
+ .get());
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // store the server in this structure
+ m_incomingServer = server;
+ accountManager->NotifyServerLoaded(server);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetIncomingServer(nsIMsgIncomingServer* aIncomingServer) {
+ NS_ENSURE_ARG_POINTER(aIncomingServer);
+
+ nsCString key;
+ nsresult rv = aIncomingServer->GetKey(key);
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_prefs->SetCharPref("server", key);
+ }
+
+ m_incomingServer = aIncomingServer;
+
+ bool serverValid;
+ (void)aIncomingServer->GetValid(&serverValid);
+ // only notify server loaded if server is valid so
+ // account manager only gets told about finished accounts.
+ if (serverValid) {
+ // this is the point at which we can notify listeners about the
+ // creation of the root folder, which implies creation of the new server.
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = aIncomingServer->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFolderListener> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mailSession->OnFolderAdded(nullptr, rootFolder);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyFolderAdded(rootFolder);
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->NotifyServerLoaded(aIncomingServer);
+
+ // Force built-in folders to be created and discovered. Then, notify
+ // listeners about them.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = rootFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ mailSession->OnFolderAdded(rootFolder, msgFolder);
+ notifier->NotifyFolderAdded(msgFolder);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::GetIdentities(nsTArray<RefPtr<nsIMsgIdentity>>& identities) {
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+ identities.Clear();
+ identities.AppendElements(m_identities);
+ return NS_OK;
+}
+
+/*
+ * set up the m_identities array
+ * do not call this more than once or we'll leak.
+ */
+nsresult nsMsgAccount::createIdentities() {
+ NS_ENSURE_FALSE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ nsresult rv;
+ m_identities.Clear();
+
+ nsCString identityKey;
+ rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_prefs->GetCharPref("identities", identityKey);
+ if (identityKey.IsEmpty()) {
+ // not an error if no identities, but strtok will be unhappy.
+ m_identitiesValid = true;
+ return NS_OK;
+ }
+ // get the server from the account manager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char* newStr = identityKey.BeginWriting();
+ char* token = NS_strtok(",", &newStr);
+
+ // temporaries used inside the loop
+ nsCOMPtr<nsIMsgIdentity> identity;
+ nsAutoCString key;
+
+ // iterate through id1,id2, etc
+ while (token) {
+ key = token;
+ key.StripWhitespace();
+
+ // create the account
+ rv = accountManager->GetIdentity(key, getter_AddRefs(identity));
+ if (NS_SUCCEEDED(rv)) {
+ m_identities.AppendElement(identity);
+ }
+
+ // advance to next key, if any
+ token = NS_strtok(",", &newStr);
+ }
+
+ m_identitiesValid = true;
+ return rv;
+}
+
+/* attribute nsIMsgIdentity defaultIdentity; */
+NS_IMETHODIMP
+nsMsgAccount::GetDefaultIdentity(nsIMsgIdentity** aDefaultIdentity) {
+ NS_ENSURE_ARG_POINTER(aDefaultIdentity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_NOT_INITIALIZED);
+
+ // Default identity is the first in the list.
+ if (m_identities.IsEmpty()) {
+ *aDefaultIdentity = nullptr;
+ } else {
+ NS_IF_ADDREF(*aDefaultIdentity = m_identities[0]);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetDefaultIdentity(nsIMsgIdentity* aDefaultIdentity) {
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ auto position = m_identities.IndexOf(aDefaultIdentity);
+ if (position == m_identities.NoIndex) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Move it to the front of the list.
+ m_identities.RemoveElementAt(position);
+ m_identities.InsertElementAt(0, aDefaultIdentity);
+
+ nsresult rv = saveIdentitiesPref();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aDefaultIdentity, "account-default-identity-changed",
+ NS_ConvertUTF8toUTF16(m_accountKey).get());
+ }
+
+ return NS_OK;
+}
+
+/* void addIdentity (in nsIMsgIdentity identity); */
+NS_IMETHODIMP
+nsMsgAccount::AddIdentity(nsIMsgIdentity* identity) {
+ NS_ENSURE_ARG_POINTER(identity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ // hack hack - need to add this to the list of identities.
+ // for now just treat this as a Setxxx accessor
+ // when this is actually implemented, don't refcount the default identity
+ nsCString key;
+ nsresult rv = identity->GetKey(key);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString identityList;
+ m_prefs->GetCharPref("identities", identityList);
+
+ nsAutoCString newIdentityList(identityList);
+
+ nsAutoCString testKey; // temporary to strip whitespace
+ bool foundIdentity = false; // if the input identity is found
+
+ if (!identityList.IsEmpty()) {
+ char* newStr = identityList.BeginWriting();
+ char* token = NS_strtok(",", &newStr);
+
+ // look for the identity key that we're adding
+ while (token) {
+ testKey = token;
+ testKey.StripWhitespace();
+
+ if (testKey.Equals(key)) foundIdentity = true;
+
+ token = NS_strtok(",", &newStr);
+ }
+ }
+
+ // if it didn't already exist, append it
+ if (!foundIdentity) {
+ if (newIdentityList.IsEmpty())
+ newIdentityList = key;
+ else {
+ newIdentityList.Append(',');
+ newIdentityList.Append(key);
+ }
+ }
+
+ m_prefs->SetCharPref("identities", newIdentityList);
+
+ // now add it to the in-memory list
+ m_identities.AppendElement(identity);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(identity, "account-identity-added",
+ NS_ConvertUTF8toUTF16(key).get());
+ }
+ }
+
+ return NS_OK;
+}
+
+/* void removeIdentity (in nsIMsgIdentity identity); */
+NS_IMETHODIMP
+nsMsgAccount::RemoveIdentity(nsIMsgIdentity* aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ // At least one identity must stay after the delete.
+ NS_ENSURE_TRUE(m_identities.Length() > 1, NS_ERROR_FAILURE);
+
+ nsCString key;
+ nsresult rv = aIdentity->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_identities.RemoveElement(aIdentity)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Notify before clearing the pref values, so we do not get the superfluous
+ // update notifications.
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aIdentity, "account-identity-removed",
+ NS_ConvertUTF8toUTF16(key).get());
+ }
+
+ // Clear out the actual pref values associated with the identity.
+ aIdentity->ClearAllValues();
+ return saveIdentitiesPref();
+}
+
+nsresult nsMsgAccount::saveIdentitiesPref() {
+ nsAutoCString newIdentityList;
+
+ // Iterate over the existing identities and build the pref value,
+ // a string of identity keys: id1, id2, idX...
+ nsCString key;
+ bool first = true;
+ for (auto identity : m_identities) {
+ identity->GetKey(key);
+
+ if (first) {
+ newIdentityList = key;
+ first = false;
+ } else {
+ newIdentityList.Append(',');
+ newIdentityList.Append(key);
+ }
+ }
+
+ // Save the pref.
+ m_prefs->SetCharPref("identities", newIdentityList);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccount::GetKey(nsACString& accountKey) {
+ accountKey = m_accountKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetKey(const nsACString& accountKey) {
+ m_accountKey = accountKey;
+ m_prefs = nullptr;
+ m_identitiesValid = false;
+ m_identities.Clear();
+ return createIdentities();
+}
+
+NS_IMETHODIMP
+nsMsgAccount::ToString(nsAString& aResult) {
+ nsAutoString val;
+ aResult.AssignLiteral("[nsIMsgAccount: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(m_accountKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::ClearAllValues() {
+ nsTArray<nsCString> prefNames;
+ nsresult rv = m_prefs->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ m_prefs->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgAccount.h b/comm/mailnews/base/src/nsMsgAccount.h
new file mode 100644
index 0000000000..60b8005390
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccount.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsIMsgAccount.h"
+#include "nsIPrefBranch.h"
+#include "nsString.h"
+
+class nsMsgAccount : public nsIMsgAccount {
+ public:
+ nsMsgAccount();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGACCOUNT
+
+ private:
+ virtual ~nsMsgAccount();
+ nsCString m_accountKey;
+ nsCOMPtr<nsIPrefBranch> m_prefs;
+ nsCOMPtr<nsIMsgIncomingServer> m_incomingServer;
+
+ bool m_identitiesValid;
+ nsTArray<nsCOMPtr<nsIMsgIdentity>> m_identities;
+
+ nsresult getPrefService();
+ nsresult createIncomingServer();
+ nsresult createIdentities();
+ nsresult saveIdentitiesPref();
+
+ // Have we tried to get the server yet?
+ bool mTriedToGetServer;
+};
diff --git a/comm/mailnews/base/src/nsMsgAccountManager.cpp b/comm/mailnews/base/src/nsMsgAccountManager.cpp
new file mode 100644
index 0000000000..5352486cb4
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccountManager.cpp
@@ -0,0 +1,3546 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * The account manager service - manages all accounts, servers, and identities
+ */
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsIThread.h"
+#include "nscore.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/RefCountType.h"
+#include "mozilla/RefPtr.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsMsgAccountManager.h"
+#include "prmem.h"
+#include "prcmon.h"
+#include "prthread.h"
+#include "plstr.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsUnicharUtils.h"
+#include "nscore.h"
+#include "prprf.h"
+#include "nsIMsgFolderCache.h"
+#include "nsMsgFolderCache.h"
+#include "nsMsgUtils.h"
+#include "nsMsgDBFolder.h"
+#include "nsIFile.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISmtpService.h"
+#include "nsIMsgBiffManager.h"
+#include "nsIMsgPurgeService.h"
+#include "nsIObserverService.h"
+#include "nsINoIncomingServer.h"
+#include "nsIMsgMailSession.h"
+#include "nsIDirectoryService.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIImapUrl.h"
+#include "nsICategoryManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgHdr.h"
+#include "nsILineInputStream.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsIStringBundle.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFilterList.h"
+#include "nsDirectoryServiceUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "nsIFileStreams.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsXULAppAPI.h"
+#include "nsICacheStorageService.h"
+#include "UrlListener.h"
+#include "nsIIDNService.h"
+
+#define PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS "mail.accountmanager.accounts"
+#define PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT \
+ "mail.accountmanager.defaultaccount"
+#define PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER \
+ "mail.accountmanager.localfoldersserver"
+#define PREF_MAIL_SERVER_PREFIX "mail.server."
+#define ACCOUNT_PREFIX "account"
+#define SERVER_PREFIX "server"
+#define ID_PREFIX "id"
+#define ABOUT_TO_GO_OFFLINE_TOPIC "network:offline-about-to-go-offline"
+#define ACCOUNT_DELIMITER ','
+#define APPEND_ACCOUNTS_VERSION_PREF_NAME "append_preconfig_accounts.version"
+#define MAILNEWS_ROOT_PREF "mailnews."
+#define PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS \
+ "mail.accountmanager.appendaccounts"
+
+#define NS_MSGACCOUNT_CID \
+ { \
+ 0x68b25510, 0xe641, 0x11d2, { \
+ 0xb7, 0xfc, 0x0, 0x80, 0x5f, 0x5, 0xff, 0xa5 \
+ } \
+ }
+static NS_DEFINE_CID(kMsgAccountCID, NS_MSGACCOUNT_CID);
+
+#define SEARCH_FOLDER_FLAG "searchFolderFlag"
+#define SEARCH_FOLDER_FLAG_LEN (sizeof(SEARCH_FOLDER_FLAG) - 1)
+
+const char* kSearchFolderUriProp = "searchFolderUri";
+
+bool nsMsgAccountManager::m_haveShutdown = false;
+bool nsMsgAccountManager::m_shutdownInProgress = false;
+
+NS_IMPL_ISUPPORTS(nsMsgAccountManager, nsIMsgAccountManager, nsIObserver,
+ nsISupportsWeakReference, nsIFolderListener)
+
+nsMsgAccountManager::nsMsgAccountManager()
+ : m_accountsLoaded(false),
+ m_emptyTrashInProgress(false),
+ m_cleanupInboxInProgress(false),
+ m_userAuthenticated(false),
+ m_loadingVirtualFolders(false),
+ m_virtualFoldersLoaded(false),
+ m_lastFindServerPort(0) {}
+
+nsMsgAccountManager::~nsMsgAccountManager() {
+ if (!m_haveShutdown) {
+ Shutdown();
+ // Don't remove from Observer service in Shutdown because Shutdown also gets
+ // called from xpcom shutdown observer. And we don't want to remove from
+ // the service in that case.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "search-folders-changed");
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ observerService->RemoveObserver(this, "quit-application-granted");
+ observerService->RemoveObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC);
+ observerService->RemoveObserver(this, "sleep_notification");
+ }
+ }
+}
+
+nsresult nsMsgAccountManager::Init() {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+
+ m_prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "search-folders-changed", true);
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ observerService->AddObserver(this, "quit-application-granted", true);
+ observerService->AddObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC, true);
+ observerService->AddObserver(this, "profile-before-change", true);
+ observerService->AddObserver(this, "sleep_notification", true);
+ }
+
+ // Make sure PSM gets initialized before any accounts use certificates.
+ net_EnsurePSMInit();
+
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::Shutdown() {
+ if (m_haveShutdown) // do not shutdown twice
+ return NS_OK;
+
+ nsresult rv;
+
+ SaveVirtualFolders();
+
+ if (m_dbService) {
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ m_dbService->UnregisterPendingListener(listener);
+ }
+
+ m_dbService = nullptr;
+ }
+ m_virtualFolders.Clear();
+ if (m_msgFolderCache) WriteToFolderCache(m_msgFolderCache);
+ (void)ShutdownServers();
+ (void)UnloadAccounts();
+
+ // shutdown removes nsIIncomingServer listener from biff manager, so do it
+ // after accounts have been unloaded
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+ if (NS_SUCCEEDED(rv) && biffService) biffService->Shutdown();
+
+ // shutdown removes nsIIncomingServer listener from purge service, so do it
+ // after accounts have been unloaded
+ nsCOMPtr<nsIMsgPurgeService> purgeService =
+ do_GetService("@mozilla.org/messenger/purgeService;1", &rv);
+ if (NS_SUCCEEDED(rv) && purgeService) purgeService->Shutdown();
+
+ if (m_msgFolderCache) {
+ // The DTOR is meant to do the flushing, but observed behaviour is
+ // that it doesn't always get called. So flush explicitly.
+ m_msgFolderCache->Flush();
+ m_msgFolderCache = nullptr;
+ }
+
+ m_haveShutdown = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetShutdownInProgress(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_shutdownInProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUserNeedsToAuthenticate(bool* aRetval) {
+ NS_ENSURE_ARG_POINTER(aRetval);
+ if (!m_userAuthenticated)
+ return m_prefs->GetBoolPref("mail.password_protect_local_cache", aRetval);
+ *aRetval = !m_userAuthenticated;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetUserNeedsToAuthenticate(bool aUserNeedsToAuthenticate) {
+ m_userAuthenticated = !aUserNeedsToAuthenticate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "search-folders-changed")) {
+ nsCOMPtr<nsIMsgFolder> virtualFolder = do_QueryInterface(aSubject);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ nsCString srchFolderUris;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUris);
+ AddVFListenersForVF(virtualFolder, srchFolderUris);
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ Shutdown();
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, "quit-application-granted")) {
+ // CleanupOnExit will set m_shutdownInProgress to true.
+ CleanupOnExit();
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, ABOUT_TO_GO_OFFLINE_TOPIC)) {
+ nsAutoString dataString(u"offline"_ns);
+ if (someData) {
+ nsAutoString someDataString(someData);
+ if (dataString.Equals(someDataString)) CloseCachedConnections();
+ }
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, "sleep_notification")) return CloseCachedConnections();
+
+ if (!strcmp(aTopic, "profile-before-change")) {
+ Shutdown();
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUniqueAccountKey(nsACString& aResult) {
+ int32_t lastKey = 0;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ prefservice->GetBranch("", getter_AddRefs(prefBranch));
+
+ rv = prefBranch->GetIntPref("mail.account.lastKey", &lastKey);
+ if (NS_FAILED(rv) || lastKey == 0) {
+ // If lastKey pref does not contain a valid value, loop over existing
+ // pref names mail.account.* .
+ nsCOMPtr<nsIPrefBranch> prefBranchAccount;
+ rv = prefservice->GetBranch("mail.account.",
+ getter_AddRefs(prefBranchAccount));
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<nsCString> prefList;
+ rv = prefBranchAccount->GetChildList("", prefList);
+ if (NS_SUCCEEDED(rv)) {
+ // Pref names are of the format accountX.
+ // Find the maximum value of 'X' used so far.
+ for (auto& prefName : prefList) {
+ if (StringBeginsWith(prefName, nsLiteralCString(ACCOUNT_PREFIX))) {
+ int32_t dotPos = prefName.FindChar('.');
+ if (dotPos != kNotFound) {
+ nsCString keyString(Substring(prefName, strlen(ACCOUNT_PREFIX),
+ dotPos - strlen(ACCOUNT_PREFIX)));
+ int32_t thisKey = keyString.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) lastKey = std::max(lastKey, thisKey);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Use next available key and store the value in the pref.
+ aResult.Assign(ACCOUNT_PREFIX);
+ aResult.AppendInt(++lastKey);
+ rv = prefBranch->SetIntPref("mail.account.lastKey", lastKey);
+ } else {
+ // If pref service is not working, try to find a free accountX key
+ // by checking which keys exist.
+ int32_t i = 1;
+ nsCOMPtr<nsIMsgAccount> account;
+
+ do {
+ aResult = ACCOUNT_PREFIX;
+ aResult.AppendInt(i++);
+ GetAccount(aResult, getter_AddRefs(account));
+ } while (account);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUniqueServerKey(nsACString& aResult) {
+ nsAutoCString prefResult;
+ bool usePrefsScan = true;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) usePrefsScan = false;
+
+ // Loop over existing pref names mail.server.server(lastKey).type
+ nsCOMPtr<nsIPrefBranch> prefBranchServer;
+ if (prefService) {
+ rv = prefService->GetBranch(PREF_MAIL_SERVER_PREFIX,
+ getter_AddRefs(prefBranchServer));
+ if (NS_FAILED(rv)) usePrefsScan = false;
+ }
+
+ if (usePrefsScan) {
+ nsAutoCString type;
+ nsAutoCString typeKey;
+ for (uint32_t lastKey = 1;; lastKey++) {
+ aResult.AssignLiteral(SERVER_PREFIX);
+ aResult.AppendInt(lastKey);
+ typeKey.Assign(aResult);
+ typeKey.AppendLiteral(".type");
+ prefBranchServer->GetCharPref(typeKey.get(), type);
+ if (type.IsEmpty()) // a server slot with no type is considered empty
+ return NS_OK;
+ }
+ } else {
+ // If pref service fails, try to find a free serverX key
+ // by checking which keys exist.
+ nsAutoCString internalResult;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ uint32_t i = 1;
+ do {
+ aResult.AssignLiteral(SERVER_PREFIX);
+ aResult.AppendInt(i++);
+ m_incomingServers.Get(aResult, getter_AddRefs(server));
+ } while (server);
+ return NS_OK;
+ }
+}
+
+nsresult nsMsgAccountManager::CreateIdentity(nsIMsgIdentity** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv;
+ nsAutoCString key;
+ nsCOMPtr<nsIMsgIdentity> identity;
+ int32_t i = 1;
+ do {
+ key.AssignLiteral(ID_PREFIX);
+ key.AppendInt(i++);
+ m_identities.Get(key, getter_AddRefs(identity));
+ } while (identity);
+
+ rv = createKeyedIdentity(key, _retval);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIdentity(const nsACString& key,
+ nsIMsgIdentity** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv = NS_OK;
+ *_retval = nullptr;
+
+ if (!key.IsEmpty()) {
+ nsCOMPtr<nsIMsgIdentity> identity;
+ m_identities.Get(key, getter_AddRefs(identity));
+ if (identity)
+ identity.forget(_retval);
+ else // identity doesn't exist. create it.
+ rv = createKeyedIdentity(key, _retval);
+ }
+
+ return rv;
+}
+
+/*
+ * the shared identity-creation code
+ * create an identity and add it to the accountmanager's list.
+ */
+nsresult nsMsgAccountManager::createKeyedIdentity(const nsACString& key,
+ nsIMsgIdentity** aIdentity) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIdentity> identity =
+ do_CreateInstance("@mozilla.org/messenger/identity;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ identity->SetKey(key);
+ m_identities.InsertOrUpdate(key, identity);
+ identity.forget(aIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateIncomingServer(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type,
+ nsIMsgIncomingServer** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString key;
+ GetUniqueServerKey(key);
+ rv = createKeyedServer(key, username, hostname, type, _retval);
+ if (*_retval) {
+ nsCString defaultStore;
+ m_prefs->GetCharPref("mail.serverDefaultStoreContractID", defaultStore);
+ (*_retval)->SetCharValue("storeContractID", defaultStore);
+
+ // From when we first create the account until we have created some folders,
+ // we can change the store type.
+ (*_retval)->SetBoolValue("canChangeStoreType", true);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIncomingServer(const nsACString& key,
+ nsIMsgIncomingServer** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv;
+
+ if (m_incomingServers.Get(key, _retval)) return NS_OK;
+
+ // server doesn't exist, so create it
+ // this is really horrible because we are doing our own prefname munging
+ // instead of leaving it up to the incoming server.
+ // this should be fixed somehow so that we can create the incoming server
+ // and then read from the incoming server's attributes
+
+ // in order to create the right kind of server, we have to look
+ // at the pref for this server to get the username, hostname, and type
+ nsAutoCString serverPrefPrefix(PREF_MAIL_SERVER_PREFIX);
+ serverPrefPrefix.Append(key);
+
+ nsCString serverType;
+ nsAutoCString serverPref(serverPrefPrefix);
+ serverPref.AppendLiteral(".type");
+ rv = m_prefs->GetCharPref(serverPref.get(), serverType);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED);
+
+ //
+ // .userName
+ serverPref = serverPrefPrefix;
+ serverPref.AppendLiteral(".userName");
+ nsCString username;
+ rv = m_prefs->GetCharPref(serverPref.get(), username);
+
+ // .hostname
+ serverPref = serverPrefPrefix;
+ serverPref.AppendLiteral(".hostname");
+ nsCString hostname;
+ rv = m_prefs->GetCharPref(serverPref.get(), hostname);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED);
+
+ return createKeyedServer(key, username, hostname, serverType, _retval);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveIncomingServer(nsIMsgIncomingServer* aServer,
+ bool aRemoveFiles) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsCString serverKey;
+ nsresult rv = aServer->GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // close cached connections and forget session password
+ LogoutOfServer(aServer);
+
+ // invalidate the FindServer() cache if we are removing the cached server
+ if (m_lastFindServerResult == aServer)
+ SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0,
+ EmptyCString());
+
+ m_incomingServers.Remove(serverKey);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = aServer->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = rootFolder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier =
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1");
+ nsCOMPtr<nsIFolderListener> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+
+ for (auto folder : allDescendants) {
+ folder->ForceDBClosed();
+ if (notifier) notifier->NotifyFolderDeleted(folder);
+ if (mailSession) {
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ folder->GetParent(getter_AddRefs(parentFolder));
+ mailSession->OnFolderRemoved(parentFolder, folder);
+ }
+ }
+ if (notifier) notifier->NotifyFolderDeleted(rootFolder);
+ if (mailSession) mailSession->OnFolderRemoved(nullptr, rootFolder);
+
+ removeListenersFromFolder(rootFolder);
+ NotifyServerUnloaded(aServer);
+ if (aRemoveFiles) {
+ rv = aServer->RemoveFiles();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aServer, "message-server-removed",
+ NS_ConvertUTF8toUTF16(serverKey).get());
+ }
+
+ // now clear out the server once and for all.
+ // watch out! could be scary
+ aServer->ClearAllValues();
+ rootFolder->Shutdown(true);
+ return rv;
+}
+
+/*
+ * create a server when you know the key and the type
+ */
+nsresult nsMsgAccountManager::createKeyedServer(
+ const nsACString& key, const nsACString& username,
+ const nsACString& hostname, const nsACString& type,
+ nsIMsgIncomingServer** aServer) {
+ nsresult rv;
+ *aServer = nullptr;
+
+ // construct the contractid
+ nsAutoCString serverContractID("@mozilla.org/messenger/server;1?type=");
+ serverContractID += type;
+
+ // finally, create the server
+ // (This will fail if type is from an extension that has been removed)
+ nsCOMPtr<nsIMsgIncomingServer> server =
+ do_CreateInstance(serverContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_AVAILABLE);
+
+ int32_t port;
+ nsCOMPtr<nsIMsgIncomingServer> existingServer;
+ server->SetKey(key);
+ server->SetType(type);
+ server->SetUsername(username);
+ server->SetHostName(hostname);
+ server->GetPort(&port);
+ FindServer(username, hostname, type, port, getter_AddRefs(existingServer));
+ // don't allow duplicate servers.
+ if (existingServer) return NS_ERROR_FAILURE;
+
+ m_incomingServers.InsertOrUpdate(key, server);
+
+ // now add all listeners that are supposed to be
+ // waiting on root folders
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter(
+ mFolderListeners);
+ while (iter.HasMore()) {
+ rootFolder->AddFolderListener(iter.GetNext());
+ }
+
+ server.forget(aServer);
+ return NS_OK;
+}
+
+void nsMsgAccountManager::removeListenersFromFolder(nsIMsgFolder* aFolder) {
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter(
+ mFolderListeners);
+ while (iter.HasMore()) {
+ aFolder->RemoveFolderListener(iter.GetNext());
+ }
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveAccount(nsIMsgAccount* aAccount,
+ bool aRemoveFiles = false) {
+ NS_ENSURE_ARG_POINTER(aAccount);
+ // Hold account in scope while we tidy up potentially-shared identities.
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_accounts.RemoveElement(aAccount)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ rv = OutputAccountsPref();
+ // If we couldn't write out the pref, restore the account.
+ if (NS_FAILED(rv)) {
+ m_accounts.AppendElement(aAccount);
+ return rv;
+ }
+
+ // If it's the default, choose a new default account.
+ if (m_defaultAccount == aAccount) AutosetDefaultAccount();
+
+ // XXX - need to figure out if this is the last time this server is
+ // being used, and only send notification then.
+ // (and only remove from hashtable then too!)
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = aAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) RemoveIncomingServer(server, aRemoveFiles);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = aAccount->GetIdentities(identities);
+ if (NS_SUCCEEDED(rv)) {
+ for (auto identity : identities) {
+ bool identityStillUsed = false;
+ // for each identity, see if any remaining account still uses it,
+ // and if not, clear it.
+ // Note that we are also searching here accounts with missing servers from
+ // unloaded extension types.
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> existingIdentities;
+ account->GetIdentities(existingIdentities);
+ auto pos = existingIdentities.IndexOf(identity);
+ if (pos != existingIdentities.NoIndex) {
+ identityStillUsed = true;
+ break;
+ }
+ }
+ // clear out all identity information if no other account uses it.
+ if (!identityStillUsed) identity->ClearAllValues();
+ }
+ }
+
+ nsCString accountKey;
+ aAccount->GetKey(accountKey);
+
+ // It is not a critical problem if this fails as the account was already
+ // removed from the list of accounts so should not ever be referenced.
+ // Just print it out for debugging.
+ rv = aAccount->ClearAllValues();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "removing of account prefs failed");
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "message-account-removed",
+ NS_ConvertUTF8toUTF16(accountKey).get());
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::OutputAccountsPref() {
+ nsCString accountKey;
+ mAccountKeyList.Truncate();
+
+ for (uint32_t index = 0; index < m_accounts.Length(); index++) {
+ m_accounts[index]->GetKey(accountKey);
+ if (index) mAccountKeyList.Append(ACCOUNT_DELIMITER);
+ mAccountKeyList.Append(accountKey);
+ }
+ return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS,
+ mAccountKeyList);
+}
+
+/**
+ * Get the default account. If no default account, return null.
+ */
+NS_IMETHODIMP
+nsMsgAccountManager::GetDefaultAccount(nsIMsgAccount** aDefaultAccount) {
+ NS_ENSURE_ARG_POINTER(aDefaultAccount);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_defaultAccount) {
+ nsCString defaultKey;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT,
+ defaultKey);
+ if (NS_SUCCEEDED(rv)) {
+ rv = GetAccount(defaultKey, getter_AddRefs(m_defaultAccount));
+ if (NS_SUCCEEDED(rv) && m_defaultAccount) {
+ bool canBeDefault = false;
+ rv = CheckDefaultAccount(m_defaultAccount, canBeDefault);
+ if (NS_FAILED(rv) || !canBeDefault) m_defaultAccount = nullptr;
+ }
+ }
+ }
+
+ NS_IF_ADDREF(*aDefaultAccount = m_defaultAccount);
+ return NS_OK;
+}
+
+/**
+ * Check if the given account can be default.
+ */
+nsresult nsMsgAccountManager::CheckDefaultAccount(nsIMsgAccount* aAccount,
+ bool& aCanBeDefault) {
+ aCanBeDefault = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // Server could be null if created by an unloaded extension.
+ nsresult rv = aAccount->GetIncomingServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (server) {
+ // Check if server can be default.
+ rv = server->GetCanBeDefaultServer(&aCanBeDefault);
+ }
+ return rv;
+}
+
+/**
+ * Pick the first account that can be default and make it the default.
+ */
+nsresult nsMsgAccountManager::AutosetDefaultAccount() {
+ for (nsIMsgAccount* account : m_accounts) {
+ bool canBeDefault = false;
+ nsresult rv = CheckDefaultAccount(account, canBeDefault);
+ if (NS_SUCCEEDED(rv) && canBeDefault) {
+ return SetDefaultAccount(account);
+ }
+ }
+
+ // No accounts can be the default. Clear it.
+ if (m_defaultAccount) {
+ nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount;
+ m_defaultAccount = nullptr;
+ (void)setDefaultAccountPref(nullptr);
+ (void)notifyDefaultServerChange(oldAccount, nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetDefaultAccount(nsIMsgAccount* aDefaultAccount) {
+ if (!aDefaultAccount) return NS_ERROR_INVALID_ARG;
+
+ if (m_defaultAccount != aDefaultAccount) {
+ bool canBeDefault = false;
+ nsresult rv = CheckDefaultAccount(aDefaultAccount, canBeDefault);
+ if (NS_FAILED(rv) || !canBeDefault) {
+ // Report failure if we were explicitly asked to use an unusable server.
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount;
+ m_defaultAccount = aDefaultAccount;
+ (void)setDefaultAccountPref(aDefaultAccount);
+ (void)notifyDefaultServerChange(oldAccount, aDefaultAccount);
+ }
+ return NS_OK;
+}
+
+// fire notifications
+nsresult nsMsgAccountManager::notifyDefaultServerChange(
+ nsIMsgAccount* aOldAccount, nsIMsgAccount* aNewAccount) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ // first tell old server it's no longer the default
+ if (aOldAccount) {
+ rv = aOldAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder)
+ rootFolder->NotifyBoolPropertyChanged(kDefaultServer, true, false);
+ }
+ }
+
+ // now tell new server it is.
+ if (aNewAccount) {
+ rv = aNewAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder)
+ rootFolder->NotifyBoolPropertyChanged(kDefaultServer, false, true);
+ }
+ }
+
+ // only notify if the user goes and changes default account
+ if (aOldAccount && aNewAccount) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (observerService)
+ observerService->NotifyObservers(nullptr, "mailDefaultAccountChanged",
+ nullptr);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::setDefaultAccountPref(
+ nsIMsgAccount* aDefaultAccount) {
+ nsresult rv;
+
+ if (aDefaultAccount) {
+ nsCString key;
+ rv = aDefaultAccount->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT, key);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else
+ m_prefs->ClearUserPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT);
+
+ return NS_OK;
+}
+
+void nsMsgAccountManager::LogoutOfServer(nsIMsgIncomingServer* aServer) {
+ if (!aServer) return;
+ mozilla::DebugOnly<nsresult> rv = aServer->Shutdown();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Shutdown of server failed");
+ rv = aServer->ForgetSessionPassword(false);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "failed to remove the password associated with server");
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetFolderCache(
+ nsIMsgFolderCache** aFolderCache) {
+ NS_ENSURE_ARG_POINTER(aFolderCache);
+
+ if (m_msgFolderCache) {
+ NS_IF_ADDREF(*aFolderCache = m_msgFolderCache);
+ return NS_OK;
+ }
+
+ // Create the foldercache.
+ nsCOMPtr<nsIFile> cacheFile;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_FOLDER_CACHE_50_FILE,
+ getter_AddRefs(cacheFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> legacyFile;
+ rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE,
+ getter_AddRefs(legacyFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_msgFolderCache = new nsMsgFolderCache();
+ m_msgFolderCache->Init(cacheFile, legacyFile);
+ NS_IF_ADDREF(*aFolderCache = m_msgFolderCache);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAccounts(nsTArray<RefPtr<nsIMsgAccount>>& accounts) {
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ accounts.Clear();
+ accounts.SetCapacity(m_accounts.Length());
+ for (auto existingAccount : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ existingAccount->GetIncomingServer(getter_AddRefs(server));
+ if (!server) continue;
+ if (server) {
+ bool hidden = false;
+ server->GetHidden(&hidden);
+ if (hidden) continue;
+ }
+ accounts.AppendElement(existingAccount);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAllIdentities(
+ nsTArray<RefPtr<nsIMsgIdentity>>& result) {
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ result.Clear();
+
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = account->GetIdentities(identities);
+ if (NS_FAILED(rv)) continue;
+
+ for (auto identity : identities) {
+ // Have we already got this identity?
+ nsAutoCString key;
+ rv = identity->GetKey(key);
+ if (NS_FAILED(rv)) continue;
+
+ bool found = false;
+ for (auto thisIdentity : result) {
+ nsAutoCString thisKey;
+ rv = thisIdentity->GetKey(thisKey);
+ if (NS_FAILED(rv)) continue;
+
+ if (key == thisKey) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) result.AppendElement(identity);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAllServers(
+ nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) {
+ servers.Clear();
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (!server) continue;
+
+ bool hidden = false;
+ server->GetHidden(&hidden);
+ if (hidden) continue;
+
+ nsCString type;
+ if (NS_FAILED(server->GetType(type))) {
+ NS_WARNING("server->GetType() failed");
+ continue;
+ }
+
+ if (!type.EqualsLiteral("im")) {
+ servers.AppendElement(server);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::LoadAccounts() {
+ nsresult rv;
+
+ // for now safeguard multiple calls to this function
+ if (m_accountsLoaded) return NS_OK;
+
+ // If we have code trying to do things after we've unloaded accounts,
+ // ignore it.
+ if (m_shutdownInProgress || m_haveShutdown) return NS_ERROR_FAILURE;
+
+ // Make sure correct modules are loaded before creating any server.
+ nsCOMPtr<nsIObserver> moduleLoader;
+ moduleLoader =
+ do_GetService("@mozilla.org/messenger/imap-module-loader;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+
+ if (NS_SUCCEEDED(rv))
+ mailSession->AddFolderListener(
+ this, nsIFolderListener::added | nsIFolderListener::removed |
+ nsIFolderListener::intPropertyChanged);
+
+ // Ensure biff service has started
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) biffService->Init();
+
+ // Ensure purge service has started
+ nsCOMPtr<nsIMsgPurgeService> purgeService =
+ do_GetService("@mozilla.org/messenger/purgeService;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) purgeService->Init();
+
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // mail.accountmanager.accounts is the main entry point for all accounts
+ nsCString accountList;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, accountList);
+
+ /**
+ * Check to see if we need to add pre-configured accounts.
+ * Following prefs are important to note in understanding the procedure here.
+ *
+ * 1. pref("mailnews.append_preconfig_accounts.version", version number);
+ * This pref registers the current version in the user prefs file. A default
+ * value is stored in mailnews.js file. If a given vendor needs to add more
+ * preconfigured accounts, the default version number can be increased.
+ * Comparing version number from user's prefs file and the default one from
+ * mailnews.js, we can add new accounts and any other version level changes
+ * that need to be done.
+ *
+ * 2. pref("mail.accountmanager.appendaccounts", <comma sep. account list>);
+ * This pref contains the list of pre-configured accounts that ISP/Vendor
+ * wants to add to the existing accounts list.
+ */
+ nsCOMPtr<nsIPrefBranch> defaultsPrefBranch;
+ rv = prefservice->GetDefaultBranch(MAILNEWS_ROOT_PREF,
+ getter_AddRefs(defaultsPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefservice->GetBranch(MAILNEWS_ROOT_PREF, getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t appendAccountsCurrentVersion = 0;
+ int32_t appendAccountsDefaultVersion = 0;
+ rv = prefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ &appendAccountsCurrentVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultsPrefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ &appendAccountsDefaultVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update the account list if needed
+ if ((appendAccountsCurrentVersion <= appendAccountsDefaultVersion)) {
+ // Get a list of pre-configured accounts
+ nsCString appendAccountList;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS,
+ appendAccountList);
+ appendAccountList.StripWhitespace();
+
+ // If there are pre-configured accounts, we need to add them to the
+ // existing list.
+ if (!appendAccountList.IsEmpty()) {
+ if (!accountList.IsEmpty()) {
+ // Tokenize the data and add each account
+ // in the user's current mailnews account list
+ nsTArray<nsCString> accountsArray;
+ ParseString(accountList, ACCOUNT_DELIMITER, accountsArray);
+ uint32_t i = accountsArray.Length();
+
+ // Append each account in the pre-configured account list
+ ParseString(appendAccountList, ACCOUNT_DELIMITER, accountsArray);
+
+ // Now add each account that does not already appear in the list
+ for (; i < accountsArray.Length(); i++) {
+ if (accountsArray.IndexOf(accountsArray[i]) == i) {
+ accountList.Append(ACCOUNT_DELIMITER);
+ accountList.Append(accountsArray[i]);
+ }
+ }
+ } else {
+ accountList = appendAccountList;
+ }
+ // Increase the version number so that updates will happen as and when
+ // needed
+ rv = prefBranch->SetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ appendAccountsCurrentVersion + 1);
+ }
+ }
+
+ // It is ok to return null accounts like when we create new profile.
+ m_accountsLoaded = true;
+ m_haveShutdown = false;
+
+ if (accountList.IsEmpty()) return NS_OK;
+
+ /* parse accountList and run loadAccount on each string, comma-separated */
+ nsCOMPtr<nsIMsgAccount> account;
+ // Tokenize the data and add each account
+ // in the user's current mailnews account list
+ nsTArray<nsCString> accountsArray;
+ ParseString(accountList, ACCOUNT_DELIMITER, accountsArray);
+
+ // These are the duplicate accounts we found. We keep track of these
+ // because if any other server defers to one of these accounts, we need
+ // to defer to the correct account.
+ nsCOMArray<nsIMsgAccount> dupAccounts;
+
+ // Now add each account that does not already appear in the list
+ for (uint32_t i = 0; i < accountsArray.Length(); i++) {
+ // if we've already seen this exact account, advance to the next account.
+ // After the loop, we'll notice that we don't have as many actual accounts
+ // as there were accounts in the pref, and rewrite the pref.
+ if (accountsArray.IndexOf(accountsArray[i]) != i) continue;
+
+ // get the "server" pref to see if we already have an account with this
+ // server. If we do, we ignore this account.
+ nsAutoCString serverKeyPref("mail.account.");
+ serverKeyPref += accountsArray[i];
+
+ nsCOMPtr<nsIPrefBranch> accountPrefBranch;
+ rv = prefservice->GetBranch(serverKeyPref.get(),
+ getter_AddRefs(accountPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverKeyPref += ".server";
+ nsCString serverKey;
+ rv = m_prefs->GetCharPref(serverKeyPref.get(), serverKey);
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIMsgAccount> serverAccount;
+ findAccountByServerKey(serverKey, getter_AddRefs(serverAccount));
+ // If we have an existing account with the same server, ignore this account
+ if (serverAccount) continue;
+
+ if (NS_FAILED(createKeyedAccount(accountsArray[i], true,
+ getter_AddRefs(account))) ||
+ !account) {
+ NS_WARNING("unexpected entry in account list; prefs corrupt?");
+ continue;
+ }
+
+ // See nsIMsgAccount.idl for a description of the secondsToLeaveUnavailable
+ // and timeFoundUnavailable preferences
+ nsAutoCString toLeavePref(PREF_MAIL_SERVER_PREFIX);
+ toLeavePref.Append(serverKey);
+ nsAutoCString unavailablePref(
+ toLeavePref); // this is the server-specific prefix
+ unavailablePref.AppendLiteral(".timeFoundUnavailable");
+ toLeavePref.AppendLiteral(".secondsToLeaveUnavailable");
+ int32_t secondsToLeave = 0;
+ int32_t timeUnavailable = 0;
+
+ m_prefs->GetIntPref(toLeavePref.get(), &secondsToLeave);
+
+ // force load of accounts (need to find a better way to do this)
+ nsTArray<RefPtr<nsIMsgIdentity>> unused;
+ account->GetIdentities(unused);
+
+ rv = account->CreateServer();
+ bool deleteAccount = NS_FAILED(rv);
+
+ if (secondsToLeave) { // we need to process timeUnavailable
+ if (NS_SUCCEEDED(rv)) // clear the time if server is available
+ {
+ m_prefs->ClearUserPref(unavailablePref.get());
+ }
+ // NS_ERROR_NOT_AVAILABLE signifies a server that could not be
+ // instantiated, presumably because of an invalid type.
+ else if (rv == NS_ERROR_NOT_AVAILABLE) {
+ m_prefs->GetIntPref(unavailablePref.get(), &timeUnavailable);
+ if (!timeUnavailable) { // we need to set it, this must be the first
+ // time unavailable
+ uint32_t nowSeconds;
+ PRTime2Seconds(PR_Now(), &nowSeconds);
+ m_prefs->SetIntPref(unavailablePref.get(), nowSeconds);
+ deleteAccount = false;
+ }
+ }
+ }
+
+ // Our server is still unavailable. Have we timed out yet?
+ if (rv == NS_ERROR_NOT_AVAILABLE && timeUnavailable != 0) {
+ uint32_t nowSeconds;
+ PRTime2Seconds(PR_Now(), &nowSeconds);
+ if ((int32_t)nowSeconds < timeUnavailable + secondsToLeave)
+ deleteAccount = false;
+ }
+
+ if (deleteAccount) {
+ dupAccounts.AppendObject(account);
+ m_accounts.RemoveElement(account);
+ }
+ }
+
+ // Check if we removed one or more of the accounts in the pref string.
+ // If so, rewrite the pref string.
+ if (accountsArray.Length() != m_accounts.Length()) OutputAccountsPref();
+
+ int32_t cnt = dupAccounts.Count();
+ nsCOMPtr<nsIMsgAccount> dupAccount;
+
+ // Go through the accounts seeing if any existing server is deferred to
+ // an account we removed. If so, fix the deferral. Then clean up the prefs
+ // for the removed account.
+ for (int32_t i = 0; i < cnt; i++) {
+ dupAccount = dupAccounts[i];
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ /*
+ * This loop gets run for every incoming server, and is passed a
+ * duplicate account. It checks that the server is not deferred to the
+ * duplicate account. If it is, then it looks up the information for the
+ * duplicate account's server (username, hostName, type), and finds an
+ * account with a server with the same username, hostname, and type, and
+ * if it finds one, defers to that account instead. Generally, this will
+ * be a Local Folders account, since 2.0 has a bug where duplicate Local
+ * Folders accounts are created.
+ */
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ nsCString type;
+ server->GetType(type);
+ if (type.EqualsLiteral("pop3")) {
+ nsCString deferredToAccount;
+ // Get the pref directly, because the GetDeferredToAccount accessor
+ // attempts to fix broken deferrals, but we know more about what the
+ // deferred to account was.
+ server->GetCharValue("deferred_to_account", deferredToAccount);
+ if (!deferredToAccount.IsEmpty()) {
+ nsCString dupAccountKey;
+ dupAccount->GetKey(dupAccountKey);
+ if (deferredToAccount.Equals(dupAccountKey)) {
+ nsresult rv;
+ nsCString accountPref("mail.account.");
+ nsCString dupAccountServerKey;
+ accountPref.Append(dupAccountKey);
+ accountPref.AppendLiteral(".server");
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv =
+ prefBranch->GetCharPref(accountPref.get(), dupAccountServerKey);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIPrefBranch> serverPrefBranch;
+ nsCString serverKeyPref(PREF_MAIL_SERVER_PREFIX);
+ serverKeyPref.Append(dupAccountServerKey);
+ serverKeyPref.Append('.');
+ rv = prefservice->GetBranch(serverKeyPref.get(),
+ getter_AddRefs(serverPrefBranch));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCString userName;
+ nsCString hostName;
+ nsCString type;
+ serverPrefBranch->GetCharPref("userName", userName);
+ serverPrefBranch->GetCharPref("hostname", hostName);
+ serverPrefBranch->GetCharPref("type", type);
+ // Find a server with the same info.
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ accountManager->FindServer(userName, hostName, type, 0,
+ getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgAccount> replacement;
+ accountManager->FindAccountForServer(server,
+ getter_AddRefs(replacement));
+ if (replacement) {
+ nsCString accountKey;
+ replacement->GetKey(accountKey);
+ if (!accountKey.IsEmpty())
+ server->SetCharValue("deferred_to_account", accountKey);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ nsAutoCString accountKeyPref("mail.account.");
+ nsCString dupAccountKey;
+ dupAccount->GetKey(dupAccountKey);
+ if (dupAccountKey.IsEmpty()) continue;
+ accountKeyPref.Append(dupAccountKey);
+ accountKeyPref.Append('.');
+
+ nsCOMPtr<nsIPrefBranch> accountPrefBranch;
+ rv = prefservice->GetBranch(accountKeyPref.get(),
+ getter_AddRefs(accountPrefBranch));
+ if (accountPrefBranch) {
+ nsTArray<nsCString> prefNames;
+ nsresult rv = accountPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ accountPrefBranch->ClearUserPref(prefName.get());
+ }
+ }
+ }
+
+ // Make sure we have an account that points at the local folders server
+ nsCString localFoldersServerKey;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER,
+ localFoldersServerKey);
+
+ if (!localFoldersServerKey.IsEmpty()) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetIncomingServer(localFoldersServerKey, getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgAccount> localFoldersAccount;
+ findAccountByServerKey(localFoldersServerKey,
+ getter_AddRefs(localFoldersAccount));
+ // If we don't have an existing account pointing at the local folders
+ // server, we're going to add one.
+ if (!localFoldersAccount) {
+ nsCOMPtr<nsIMsgAccount> account;
+ (void)CreateAccount(getter_AddRefs(account));
+ if (account) account->SetIncomingServer(server);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ReactivateAccounts() {
+ for (nsIMsgAccount* account : m_accounts) {
+ // This will error out if the account already has its server, or
+ // if this isn't the account that the extension is trying to reactivate.
+ if (NS_SUCCEEDED(account->CreateServer())) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ account->GetIncomingServer(getter_AddRefs(server));
+ // This triggers all of the notifications required by the UI.
+ account->SetIncomingServer(server);
+ }
+ }
+ return NS_OK;
+}
+
+// this routine goes through all the identities and makes sure
+// that the special folders for each identity have the
+// correct special folder flags set, e.g, the Sent folder has
+// the sent flag set.
+//
+// it also goes through all the spam settings for each account
+// and makes sure the folder flags are set there, too
+NS_IMETHODIMP
+nsMsgAccountManager::SetSpecialFolders() {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ GetAllIdentities(identities);
+
+ for (auto identity : identities) {
+ nsresult rv;
+ nsCString folderUri;
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ identity->GetFccFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ rv = folder->SetFlag(nsMsgFolderFlags::SentMail);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetDraftFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ rv = folder->SetFlag(nsMsgFolderFlags::Drafts);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetArchiveFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ bool archiveEnabled;
+ identity->GetArchiveEnabled(&archiveEnabled);
+ if (archiveEnabled)
+ rv = folder->SetFlag(nsMsgFolderFlags::Archive);
+ else
+ rv = folder->ClearFlag(nsMsgFolderFlags::Archive);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetStationeryFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ folder->SetFlag(nsMsgFolderFlags::Templates);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ // XXX todo
+ // get all servers
+ // get all spam settings for each server
+ // set the JUNK folder flag on the spam folders, right?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::UnloadAccounts() {
+ // release the default account
+ m_defaultAccount = nullptr;
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (!server) continue;
+ nsresult rv;
+ NotifyServerUnloaded(server);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv)) {
+ removeListenersFromFolder(rootFolder);
+
+ rootFolder->Shutdown(true);
+ }
+ }
+
+ m_accounts.Clear(); // will release all elements
+ m_identities.Clear();
+ m_incomingServers.Clear();
+ mAccountKeyList.Truncate();
+ SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0,
+ EmptyCString());
+
+ if (m_accountsLoaded) {
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (mailSession) mailSession->RemoveFolderListener(this);
+ m_accountsLoaded = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ShutdownServers() {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) server->Shutdown();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CloseCachedConnections() {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) server->CloseCachedConnections();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CleanupOnExit() {
+ // This can get called multiple times, and potentially re-entrantly.
+ // So add some protection against that.
+ if (m_shutdownInProgress) return NS_OK;
+
+ m_shutdownInProgress = true;
+
+ nsresult rv;
+ // If enabled, clear cache on shutdown. This is common to all accounts.
+ bool clearCache = false;
+ m_prefs->GetBoolPref("privacy.clearOnShutdown.cache", &clearCache);
+ if (clearCache) {
+ nsCOMPtr<nsICacheStorageService> cacheStorageService =
+ do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
+ if (NS_SUCCEEDED(rv)) cacheStorageService->Clear();
+ }
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+
+ bool emptyTrashOnExit = false;
+ bool cleanupInboxOnExit = false;
+
+ if (WeAreOffline()) break;
+
+ if (!server) continue;
+
+ server->GetEmptyTrashOnExit(&emptyTrashOnExit);
+ nsCOMPtr<nsIImapIncomingServer> imapserver = do_QueryInterface(server);
+ if (imapserver) {
+ imapserver->GetCleanupInboxOnExit(&cleanupInboxOnExit);
+ imapserver->SetShuttingDown(true);
+ }
+ if (emptyTrashOnExit || cleanupInboxOnExit) {
+ nsCOMPtr<nsIMsgFolder> root;
+ server->GetRootFolder(getter_AddRefs(root));
+ nsCString type;
+ server->GetType(type);
+ if (root) {
+ nsString passwd;
+ int32_t authMethod = 0;
+ bool serverRequiresPasswordForAuthentication = true;
+ bool isImap = type.EqualsLiteral("imap");
+ if (isImap) {
+ server->GetServerRequiresPasswordForBiff(
+ &serverRequiresPasswordForAuthentication);
+ server->GetPassword(passwd);
+ server->GetAuthMethod(&authMethod);
+ }
+ if (!isImap || (isImap && (!serverRequiresPasswordForAuthentication ||
+ !passwd.IsEmpty() ||
+ authMethod == nsMsgAuthMethod::OAuth2))) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) continue;
+
+ if (isImap && cleanupInboxOnExit) {
+ // Find the inbox.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = root->GetSubFolders(subFolders);
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* folder : subFolders) {
+ uint32_t flags;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Inbox) {
+ // This is inbox, so Compact() it. There's an implied
+ // Expunge too, because this is IMAP.
+ RefPtr<UrlListener> cleanupListener = new UrlListener();
+ RefPtr<nsMsgAccountManager> self = this;
+ // This runs when the compaction (+expunge) is complete.
+ cleanupListener->mStopFn =
+ [self](nsIURI* url, nsresult status) -> nsresult {
+ if (self->m_folderDoingCleanupInbox) {
+ PR_CEnterMonitor(self->m_folderDoingCleanupInbox);
+ PR_CNotifyAll(self->m_folderDoingCleanupInbox);
+ self->m_cleanupInboxInProgress = false;
+ PR_CExitMonitor(self->m_folderDoingCleanupInbox);
+ self->m_folderDoingCleanupInbox = nullptr;
+ }
+ return NS_OK;
+ };
+
+ rv = folder->Compact(cleanupListener, nullptr);
+ if (NS_SUCCEEDED(rv))
+ accountManager->SetFolderDoingCleanupInbox(folder);
+ break;
+ }
+ }
+ }
+ }
+
+ if (emptyTrashOnExit) {
+ RefPtr<UrlListener> emptyTrashListener = new UrlListener();
+ RefPtr<nsMsgAccountManager> self = this;
+ // This runs when the trash-emptying is complete.
+ // (It'll be a nsIImapUrl::nsImapDeleteAllMsgs url).
+ emptyTrashListener->mStopFn = [self](nsIURI* url,
+ nsresult status) -> nsresult {
+ if (self->m_folderDoingEmptyTrash) {
+ PR_CEnterMonitor(self->m_folderDoingEmptyTrash);
+ PR_CNotifyAll(self->m_folderDoingEmptyTrash);
+ self->m_emptyTrashInProgress = false;
+ PR_CExitMonitor(self->m_folderDoingEmptyTrash);
+ self->m_folderDoingEmptyTrash = nullptr;
+ }
+ return NS_OK;
+ };
+
+ rv = root->EmptyTrash(emptyTrashListener);
+ if (isImap && NS_SUCCEEDED(rv))
+ accountManager->SetFolderDoingEmptyTrash(root);
+ }
+
+ if (isImap) {
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+
+ // Pause until any possible inbox-compaction and trash-emptying
+ // are complete (or time out).
+ bool inProgress = false;
+ if (cleanupInboxOnExit) {
+ int32_t loopCount = 0; // used to break out after 5 seconds
+ accountManager->GetCleanupInboxInProgress(&inProgress);
+ while (inProgress && loopCount++ < 5000) {
+ accountManager->GetCleanupInboxInProgress(&inProgress);
+ PR_CEnterMonitor(root);
+ PR_CWait(root, PR_MicrosecondsToInterval(1000UL));
+ PR_CExitMonitor(root);
+ NS_ProcessPendingEvents(thread,
+ PR_MicrosecondsToInterval(1000UL));
+ }
+ }
+ if (emptyTrashOnExit) {
+ accountManager->GetEmptyTrashInProgress(&inProgress);
+ int32_t loopCount = 0;
+ while (inProgress && loopCount++ < 5000) {
+ accountManager->GetEmptyTrashInProgress(&inProgress);
+ PR_CEnterMonitor(root);
+ PR_CWait(root, PR_MicrosecondsToInterval(1000UL));
+ PR_CExitMonitor(root);
+ NS_ProcessPendingEvents(thread,
+ PR_MicrosecondsToInterval(1000UL));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Try to do this early on in the shutdown process before
+ // necko shuts itself down.
+ CloseCachedConnections();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::WriteToFolderCache(nsIMsgFolderCache* folderCache) {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ iter.Data()->WriteToFolderCache(folderCache);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::createKeyedAccount(const nsCString& key,
+ bool forcePositionToEnd,
+ nsIMsgAccount** aAccount) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccount> account = do_CreateInstance(kMsgAccountCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ account->SetKey(key);
+
+ nsCString localFoldersAccountKey;
+ nsCString lastFolderAccountKey;
+ if (!forcePositionToEnd) {
+ nsCOMPtr<nsIMsgIncomingServer> localFoldersServer;
+ rv = GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+ if (NS_SUCCEEDED(rv)) {
+ for (auto account : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = account->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server == localFoldersServer) {
+ account->GetKey(localFoldersAccountKey);
+ break;
+ }
+ }
+ }
+
+ // Extracting the account key of the last mail acoount.
+ for (int32_t index = m_accounts.Length() - 1; index >= 0; index--) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_accounts[index]->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ nsCString accountType;
+ rv = server->GetType(accountType);
+ if (NS_SUCCEEDED(rv) && !accountType.EqualsLiteral("im")) {
+ m_accounts[index]->GetKey(lastFolderAccountKey);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!forcePositionToEnd && !localFoldersAccountKey.IsEmpty() &&
+ !lastFolderAccountKey.IsEmpty() &&
+ lastFolderAccountKey == localFoldersAccountKey) {
+ // Insert account before Local Folders if that is the last account.
+ m_accounts.InsertElementAt(m_accounts.Length() - 1, account);
+ } else {
+ m_accounts.AppendElement(account);
+ }
+
+ nsCString newAccountKeyList;
+ nsCString accountKey;
+ for (uint32_t index = 0; index < m_accounts.Length(); index++) {
+ m_accounts[index]->GetKey(accountKey);
+ if (index) newAccountKeyList.Append(ACCOUNT_DELIMITER);
+ newAccountKeyList.Append(accountKey);
+ }
+ mAccountKeyList = newAccountKeyList;
+
+ m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, mAccountKeyList);
+ account.forget(aAccount);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateAccount(nsIMsgAccount** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsAutoCString key;
+ GetUniqueAccountKey(key);
+
+ return createKeyedAccount(key, false, _retval);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAccount(const nsACString& aKey,
+ nsIMsgAccount** aAccount) {
+ NS_ENSURE_ARG_POINTER(aAccount);
+ *aAccount = nullptr;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgAccount> account(m_accounts[i]);
+ nsCString key;
+ account->GetKey(key);
+ if (key.Equals(aKey)) {
+ account.forget(aAccount);
+ break;
+ }
+ }
+
+ // If not found, create on demand.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindServerIndex(nsIMsgIncomingServer* server,
+ int32_t* result) {
+ NS_ENSURE_ARG_POINTER(server);
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsCString key;
+ nsresult rv = server->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // do this by account because the account list is in order
+ uint32_t i;
+ for (i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server));
+ if (!server || NS_FAILED(rv)) continue;
+
+ nsCString serverKey;
+ rv = server->GetKey(serverKey);
+ if (NS_FAILED(rv)) continue;
+
+ // stop when found,
+ // index will be set to the current index
+ if (serverKey.Equals(key)) break;
+ }
+
+ // Even if the search failed, we can return index.
+ // This means that all servers not in the array return an index higher
+ // than all "registered" servers.
+ *result = i;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::AddIncomingServerListener(
+ nsIIncomingServerListener* serverListener) {
+ m_incomingServerListeners.AppendObject(serverListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::RemoveIncomingServerListener(
+ nsIIncomingServerListener* serverListener) {
+ m_incomingServerListeners.RemoveObject(serverListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerLoaded(
+ nsIMsgIncomingServer* server) {
+ int32_t count = m_incomingServerListeners.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerLoaded(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerUnloaded(
+ nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ int32_t count = m_incomingServerListeners.Count();
+ // Clear this to cut shutdown leaks. We are always passing valid non-null
+ // server here.
+ server->SetFilterList(nullptr);
+
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerUnloaded(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerChanged(
+ nsIMsgIncomingServer* server) {
+ int32_t count = m_incomingServerListeners.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerChanged(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindServerByURI(nsIURI* aURI,
+ nsIMsgIncomingServer** aResult) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get username and hostname and port so we can get the server
+ nsAutoCString username;
+ nsAutoCString escapedUsername;
+ rv = aURI->GetUserPass(escapedUsername);
+ if (NS_SUCCEEDED(rv) && !escapedUsername.IsEmpty())
+ MsgUnescapeString(escapedUsername, 0, username);
+
+ nsAutoCString hostname;
+ nsAutoCString escapedHostname;
+ rv = aURI->GetHost(escapedHostname);
+ if (NS_SUCCEEDED(rv) && !escapedHostname.IsEmpty()) {
+ MsgUnescapeString(escapedHostname, 0, hostname);
+ }
+
+ nsAutoCString type;
+ rv = aURI->GetScheme(type);
+ if (NS_SUCCEEDED(rv) && !type.IsEmpty()) {
+ // Remove "-message" from the scheme in case we get called with
+ // "imap-message", "mailbox-message", or friends.
+ if (StringEndsWith(type, "-message"_ns)) type.SetLength(type.Length() - 8);
+ // now modify type if pop or news
+ if (type.EqualsLiteral("pop")) type.AssignLiteral("pop3");
+ // we use "nntp" in the server list so translate it here.
+ else if (type.EqualsLiteral("news"))
+ type.AssignLiteral("nntp");
+ // we use "any" as the wildcard type.
+ else if (type.EqualsLiteral("any"))
+ type.Truncate();
+ }
+
+ int32_t port = 0;
+ // check the port of the scheme is not 'none' or blank
+ if (!(type.EqualsLiteral("none") || type.IsEmpty())) {
+ rv = aURI->GetPort(&port);
+ // Set the port to zero if we got a -1 (use default)
+ if (NS_SUCCEEDED(rv) && (port == -1)) port = 0;
+ }
+
+ return findServerInternal(username, hostname, type, port, aResult);
+}
+
+nsresult nsMsgAccountManager::findServerInternal(
+ const nsACString& username, const nsACString& serverHostname,
+ const nsACString& type, int32_t port, nsIMsgIncomingServer** aResult) {
+ if ((m_lastFindServerUserName.Equals(username)) &&
+ (m_lastFindServerHostName.Equals(serverHostname)) &&
+ (m_lastFindServerType.Equals(type)) && (m_lastFindServerPort == port) &&
+ m_lastFindServerResult) {
+ NS_ADDREF(*aResult = m_lastFindServerResult);
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCString hostname;
+ nsCOMPtr<nsIIDNService> idnService =
+ do_GetService("@mozilla.org/network/idn-service;1");
+ rv = idnService->Normalize(serverHostname, hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ // Find matching server by user+host+type+port.
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+
+ if (!server) continue;
+
+ nsCString thisHostname;
+ rv = server->GetHostName(thisHostname);
+ if (NS_FAILED(rv)) continue;
+
+ rv = idnService->Normalize(thisHostname, thisHostname);
+ if (NS_FAILED(rv)) continue;
+
+ // If the hostname was a IP with trailing dot, that dot gets removed
+ // during URI mutation. We may well be here in findServerInternal to
+ // find a server from a folder URI. Remove the trailing dot so we can
+ // find the server.
+ nsCString thisHostnameNoDot(thisHostname);
+ if (!thisHostname.IsEmpty() &&
+ thisHostname.CharAt(thisHostname.Length() - 1) == '.') {
+ thisHostnameNoDot.Cut(thisHostname.Length() - 1, 1);
+ }
+
+ nsCString thisUsername;
+ rv = server->GetUsername(thisUsername);
+ if (NS_FAILED(rv)) continue;
+
+ nsCString thisType;
+ rv = server->GetType(thisType);
+ if (NS_FAILED(rv)) continue;
+
+ int32_t thisPort = -1; // use the default port identifier
+ // Don't try and get a port for the 'none' scheme
+ if (!thisType.EqualsLiteral("none")) {
+ rv = server->GetPort(&thisPort);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ }
+
+ // treat "" as a wild card, so if the caller passed in "" for the desired
+ // attribute treat it as a match
+ if ((type.IsEmpty() || thisType.Equals(type)) &&
+ (hostname.IsEmpty() ||
+ thisHostname.Equals(hostname, nsCaseInsensitiveCStringComparator) ||
+ thisHostnameNoDot.Equals(hostname,
+ nsCaseInsensitiveCStringComparator)) &&
+ (!(port != 0) || (port == thisPort)) &&
+ (username.IsEmpty() || thisUsername.Equals(username))) {
+ // stop on first find; cache for next time
+ SetLastServerFound(server, hostname, username, port, type);
+
+ NS_ADDREF(*aResult = server); // Was populated from member variable.
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_UNEXPECTED;
+}
+
+// Always return NS_OK;
+NS_IMETHODIMP
+nsMsgAccountManager::FindServer(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type, int32_t port,
+ nsIMsgIncomingServer** aResult) {
+ *aResult = nullptr;
+ findServerInternal(username, hostname, type, port, aResult);
+ return NS_OK;
+}
+
+void nsMsgAccountManager::findAccountByServerKey(const nsCString& aKey,
+ nsIMsgAccount** aResult) {
+ *aResult = nullptr;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server));
+ if (!server || NS_FAILED(rv)) continue;
+
+ nsCString key;
+ rv = server->GetKey(key);
+ if (NS_FAILED(rv)) continue;
+
+ // if the keys are equal, the servers are equal
+ if (key.Equals(aKey)) {
+ NS_ADDREF(*aResult = m_accounts[i]);
+ break; // stop on first found account
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindAccountForServer(nsIMsgIncomingServer* server,
+ nsIMsgAccount** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!server) {
+ (*aResult) = nullptr;
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ nsCString key;
+ rv = server->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ findAccountByServerKey(key, aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetFirstIdentityForServer(nsIMsgIncomingServer* aServer,
+ nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ NS_ENSURE_ARG_POINTER(aIdentity);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ nsresult rv = GetIdentitiesForServer(aServer, identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // not all servers have identities
+ // for example, Local Folders
+ if (identities.IsEmpty()) {
+ *aIdentity = nullptr;
+ } else {
+ NS_IF_ADDREF(*aIdentity = identities[0]);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIdentitiesForServer(
+ nsIMsgIncomingServer* server,
+ nsTArray<RefPtr<nsIMsgIdentity>>& identities) {
+ NS_ENSURE_ARG_POINTER(server);
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ identities.Clear();
+
+ nsAutoCString serverKey;
+ rv = server->GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto account : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> thisServer;
+ rv = account->GetIncomingServer(getter_AddRefs(thisServer));
+ if (NS_FAILED(rv) || !thisServer) continue;
+
+ nsAutoCString thisServerKey;
+ rv = thisServer->GetKey(thisServerKey);
+ if (serverKey.Equals(thisServerKey)) {
+ nsTArray<RefPtr<nsIMsgIdentity>> theseIdentities;
+ rv = account->GetIdentities(theseIdentities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ identities.AppendElements(theseIdentities);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetServersForIdentity(
+ nsIMsgIdentity* aIdentity,
+ nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ servers.Clear();
+
+ nsresult rv;
+ rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ if (NS_FAILED(account->GetIdentities(identities))) continue;
+
+ nsCString identityKey;
+ aIdentity->GetKey(identityKey);
+ for (auto thisIdentity : identities) {
+ nsCString thisIdentityKey;
+ rv = thisIdentity->GetKey(thisIdentityKey);
+
+ if (NS_SUCCEEDED(rv) && identityKey.Equals(thisIdentityKey)) {
+ nsCOMPtr<nsIMsgIncomingServer> thisServer;
+ rv = account->GetIncomingServer(getter_AddRefs(thisServer));
+ if (thisServer && NS_SUCCEEDED(rv)) {
+ servers.AppendElement(thisServer);
+ break;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::AddRootFolderListener(nsIFolderListener* aListener) {
+ NS_ENSURE_TRUE(aListener, NS_OK);
+ mFolderListeners.AppendElement(aListener);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv = rootFolder->AddFolderListener(aListener);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveRootFolderListener(nsIFolderListener* aListener) {
+ NS_ENSURE_TRUE(aListener, NS_OK);
+ mFolderListeners.RemoveElement(aListener);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv = rootFolder->RemoveFolderListener(aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::SetLocalFoldersServer(
+ nsIMsgIncomingServer* aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsCString key;
+ nsresult rv = aServer->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, key);
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetLocalFoldersServer(
+ nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsCString serverKey;
+
+ nsresult rv = m_prefs->GetCharPref(
+ PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, serverKey);
+
+ if (NS_SUCCEEDED(rv) && !serverKey.IsEmpty()) {
+ rv = GetIncomingServer(serverKey, aServer);
+ if (NS_SUCCEEDED(rv)) return rv;
+ // otherwise, we're going to fall through to looking for an existing local
+ // folders account, because now we fail creating one if one already exists.
+ }
+
+ // try ("nobody","Local Folders","none"), and work down to any "none" server.
+ rv = findServerInternal("nobody"_ns, "Local Folders"_ns, "none"_ns, 0,
+ aServer);
+ if (NS_FAILED(rv) || !*aServer) {
+ rv = findServerInternal("nobody"_ns, EmptyCString(), "none"_ns, 0, aServer);
+ if (NS_FAILED(rv) || !*aServer) {
+ rv = findServerInternal(EmptyCString(), "Local Folders"_ns, "none"_ns, 0,
+ aServer);
+ if (NS_FAILED(rv) || !*aServer)
+ rv = findServerInternal(EmptyCString(), EmptyCString(), "none"_ns, 0,
+ aServer);
+ }
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!*aServer) return NS_ERROR_FAILURE;
+
+ // we don't want the Smart Mailboxes server to be the local server.
+ bool hidden;
+ (*aServer)->GetHidden(&hidden);
+ if (hidden) return NS_ERROR_FAILURE;
+
+ rv = SetLocalFoldersServer(*aServer);
+ return rv;
+}
+
+nsresult nsMsgAccountManager::GetLocalFoldersPrettyName(
+ nsString& localFoldersName) {
+ // we don't want "nobody at Local Folders" to show up in the
+ // folder pane, so we set the pretty name to a localized "Local Folders"
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+
+ rv = sBundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return bundle->GetStringFromName("localFolders", localFoldersName);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateLocalMailAccount() {
+ // create the server
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = CreateIncomingServer("nobody"_ns, "Local Folders"_ns, "none"_ns,
+ getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString localFoldersName;
+ rv = GetLocalFoldersPrettyName(localFoldersName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ server->SetPrettyName(localFoldersName);
+
+ nsCOMPtr<nsINoIncomingServer> noServer;
+ noServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // create the directory structure for old 4.x "Local Mail"
+ // under <profile dir>/Mail/Local Folders or
+ // <"mail.directory" pref>/Local Folders
+ nsCOMPtr<nsIFile> mailDir;
+ bool dirExists;
+
+ // we want <profile>/Mail
+ rv = NS_GetSpecialDirectory(NS_APP_MAIL_50_DIR, getter_AddRefs(mailDir));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = mailDir->Exists(&dirExists);
+ if (NS_SUCCEEDED(rv) && !dirExists)
+ rv = mailDir->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ if (NS_FAILED(rv)) return rv;
+
+ // set the default local path for "none"
+ rv = server->SetDefaultLocalPath(mailDir);
+ if (NS_FAILED(rv)) return rv;
+
+ // Create an account when valid server values are established.
+ // This will keep the status of accounts sane by avoiding the addition of
+ // incomplete accounts.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = CreateAccount(getter_AddRefs(account));
+ if (NS_FAILED(rv)) return rv;
+
+ // notice, no identity for local mail
+ // hook the server to the account
+ // after we set the server's local path
+ // (see bug #66018)
+ account->SetIncomingServer(server);
+
+ // remember this as the local folders server
+ return SetLocalFoldersServer(server);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetFolderDoingEmptyTrash(nsIMsgFolder* folder) {
+ m_folderDoingEmptyTrash = folder;
+ m_emptyTrashInProgress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetEmptyTrashInProgress(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_emptyTrashInProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetFolderDoingCleanupInbox(nsIMsgFolder* folder) {
+ m_folderDoingCleanupInbox = folder;
+ m_cleanupInboxInProgress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetCleanupInboxInProgress(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_cleanupInboxInProgress;
+ return NS_OK;
+}
+
+void nsMsgAccountManager::SetLastServerFound(nsIMsgIncomingServer* server,
+ const nsACString& hostname,
+ const nsACString& username,
+ const int32_t port,
+ const nsACString& type) {
+ m_lastFindServerResult = server;
+ m_lastFindServerHostName = hostname;
+ m_lastFindServerUserName = username;
+ m_lastFindServerPort = port;
+ m_lastFindServerType = type;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SaveAccountInfo() {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return pref->SavePrefFile(nullptr);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetChromePackageName(const nsACString& aExtensionName,
+ nsACString& aChromePackageName) {
+ nsresult rv;
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> e;
+ rv = catman->EnumerateCategory(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS,
+ getter_AddRefs(e));
+ if (NS_SUCCEEDED(rv) && e) {
+ while (true) {
+ nsCOMPtr<nsISupports> supports;
+ rv = e->GetNext(getter_AddRefs(supports));
+ nsCOMPtr<nsISupportsCString> catEntry = do_QueryInterface(supports);
+ if (NS_FAILED(rv) || !catEntry) break;
+
+ nsAutoCString entryString;
+ rv = catEntry->GetData(entryString);
+ if (NS_FAILED(rv)) break;
+
+ nsCString contractidString;
+ rv = catman->GetCategoryEntry(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS,
+ entryString, contractidString);
+ if (NS_FAILED(rv)) break;
+
+ nsCOMPtr<nsIMsgAccountManagerExtension> extension =
+ do_GetService(contractidString.get(), &rv);
+ if (NS_FAILED(rv) || !extension) break;
+
+ nsCString name;
+ rv = extension->GetName(name);
+ if (NS_FAILED(rv)) break;
+
+ if (name.Equals(aExtensionName))
+ return extension->GetChromePackageName(aChromePackageName);
+ }
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+class VFChangeListenerEvent : public mozilla::Runnable {
+ public:
+ VFChangeListenerEvent(VirtualFolderChangeListener* vfChangeListener,
+ nsIMsgFolder* virtFolder, nsIMsgDatabase* virtDB)
+ : mozilla::Runnable("VFChangeListenerEvent"),
+ mVFChangeListener(vfChangeListener),
+ mFolder(virtFolder),
+ mDB(virtDB) {}
+
+ NS_IMETHOD Run() {
+ if (mVFChangeListener) mVFChangeListener->ProcessUpdateEvent(mFolder, mDB);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<VirtualFolderChangeListener> mVFChangeListener;
+ nsCOMPtr<nsIMsgFolder> mFolder;
+ nsCOMPtr<nsIMsgDatabase> mDB;
+};
+
+NS_IMPL_ISUPPORTS(VirtualFolderChangeListener, nsIDBChangeListener)
+
+VirtualFolderChangeListener::VirtualFolderChangeListener()
+ : m_searchOnMsgStatus(false), m_batchingEvents(false) {}
+
+nsresult VirtualFolderChangeListener::Init() {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ nsresult rv = m_virtualFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(msgDB));
+ if (NS_SUCCEEDED(rv) && msgDB) {
+ nsCString searchTermString;
+ dbFolderInfo->GetCharProperty("searchStr", searchTermString);
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ rv = filterService->GetTempFilterList(m_virtualFolder,
+ getter_AddRefs(filterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFilter> tempFilter;
+ filterList->CreateFilter(u"temp"_ns, getter_AddRefs(tempFilter));
+ NS_ENSURE_SUCCESS(rv, rv);
+ filterList->ParseCondition(tempFilter, searchTermString.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = tempFilter->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we add the search scope right before we match the header,
+ // because we don't want the search scope caching the body input
+ // stream, because that holds onto the mailbox file, breaking
+ // compaction.
+
+ // add each search term to the search session
+ for (nsIMsgSearchTerm* searchTerm : searchTerms) {
+ nsMsgSearchAttribValue attrib;
+ searchTerm->GetAttrib(&attrib);
+ if (attrib == nsMsgSearchAttrib::MsgStatus) m_searchOnMsgStatus = true;
+ m_searchSession->AppendTerm(searchTerm);
+ }
+ }
+ return rv;
+}
+
+/**
+ * nsIDBChangeListener
+ */
+
+NS_IMETHODIMP
+VirtualFolderChangeListener::OnHdrPropertyChanged(
+ nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange,
+ uint32_t* aStatus, nsIDBChangeListener* aInstigator) {
+ const uint32_t kMatch = 0x1;
+ const uint32_t kRead = 0x2;
+ const uint32_t kNew = 0x4;
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+ NS_ENSURE_ARG_POINTER(aStatus);
+
+ uint32_t flags;
+ bool match;
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we don't want any early returns from this function, until we've
+ // called ClearScopes on the search session.
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &match);
+ m_searchSession->ClearScopes();
+ NS_ENSURE_SUCCESS(rv, rv);
+ aHdrChanged->GetFlags(&flags);
+
+ if (aPreChange) // We're looking at the old header, save status
+ {
+ *aStatus = 0;
+ if (match) *aStatus |= kMatch;
+ if (flags & nsMsgMessageFlags::Read) *aStatus |= kRead;
+ if (flags & nsMsgMessageFlags::New) *aStatus |= kNew;
+ return NS_OK;
+ }
+
+ // This is the post change section where changes are detected
+
+ bool wasMatch = *aStatus & kMatch;
+ if (!match && !wasMatch) // header not in virtual folder
+ return NS_OK;
+
+ int32_t totalDelta = 0, unreadDelta = 0, newDelta = 0;
+
+ if (match) {
+ totalDelta++;
+ if (!(flags & nsMsgMessageFlags::Read)) unreadDelta++;
+ if (flags & nsMsgMessageFlags::New) newDelta++;
+ }
+
+ if (wasMatch) {
+ totalDelta--;
+ if (!(*aStatus & kRead)) unreadDelta--;
+ if (*aStatus & kNew) newDelta--;
+ }
+
+ if (!(unreadDelta || totalDelta || newDelta)) return NS_OK;
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta);
+
+ if (newDelta) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetNumNewMessages(numNewMessages + newDelta);
+ m_virtualFolder->SetHasNewMessages(numNewMessages + newDelta > 0);
+ }
+
+ if (totalDelta) {
+ dbFolderInfo->ChangeNumMessages(totalDelta);
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1);
+ }
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+
+ return NS_OK;
+}
+
+void VirtualFolderChangeListener::DecrementNewMsgCount() {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ if (numNewMessages > 0) numNewMessages--;
+ m_virtualFolder->SetNumNewMessages(numNewMessages);
+ if (!numNewMessages) m_virtualFolder->SetHasNewMessages(false);
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ bool oldMatch = false, newMatch = false;
+ // we don't want any early returns from this function, until we've
+ // called ClearScopes 0n the search session.
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &newMatch);
+ if (NS_SUCCEEDED(rv) && m_searchOnMsgStatus) {
+ // if status is a search criteria, check if the header matched before
+ // it changed, in order to determine if we need to bump the counts.
+ aHdrChanged->SetFlags(aOldFlags);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &oldMatch);
+ // restore new flags even on match failure.
+ aHdrChanged->SetFlags(aNewFlags);
+ } else
+ oldMatch = newMatch;
+ m_searchSession->ClearScopes();
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we don't want to change the total counts if this virtual folder is open in
+ // a view, because we won't remove the header from view while it's open. On
+ // the other hand, it's hard to fix the count when the user clicks away to
+ // another folder, w/o re-running the search, or setting some sort of pending
+ // count change. Maybe this needs to be handled in the view code...the view
+ // could do the same calculation and also keep track of the counts changed.
+ // Then, when the view was closed, if it's a virtual folder, it could update
+ // the counts for the db.
+ if (oldMatch != newMatch ||
+ (oldMatch && (aOldFlags & nsMsgMessageFlags::Read) !=
+ (aNewFlags & nsMsgMessageFlags::Read))) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t totalDelta = 0, unreadDelta = 0;
+ if (oldMatch != newMatch) {
+ // bool isOpen = false;
+ // nsCOMPtr<nsIMsgMailSession> mailSession =
+ // do_GetService("@mozilla.org/messenger/services/session;1");
+ // if (mailSession && aFolder)
+ // mailSession->IsFolderOpenInWindow(m_virtualFolder, &isOpen);
+ // we can't remove headers that no longer match - but we might add headers
+ // that newly match, someday.
+ // if (!isOpen)
+ totalDelta = (oldMatch) ? -1 : 1;
+ }
+ bool msgHdrIsRead;
+ aHdrChanged->GetIsRead(&msgHdrIsRead);
+ if (oldMatch == newMatch) // read flag changed state
+ unreadDelta = (msgHdrIsRead) ? -1 : 1;
+ else if (oldMatch) // else header should removed
+ unreadDelta = (aOldFlags & nsMsgMessageFlags::Read) ? 0 : -1;
+ else // header should be added
+ unreadDelta = (aNewFlags & nsMsgMessageFlags::Read) ? 0 : 1;
+ if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta);
+ if (totalDelta) dbFolderInfo->ChangeNumMessages(totalDelta);
+ if (unreadDelta == -1 && aOldFlags & nsMsgMessageFlags::New)
+ DecrementNewMsgCount();
+
+ if (totalDelta) {
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1);
+ }
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ } else if (oldMatch && (aOldFlags & nsMsgMessageFlags::New) &&
+ !(aNewFlags & nsMsgMessageFlags::New))
+ DecrementNewMsgCount();
+
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrDeleted(
+ nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool match = false;
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ // Since the notifier went to the trouble of passing in the msg flags,
+ // we should use them when doing the match.
+ uint32_t msgFlags;
+ aHdrDeleted->GetFlags(&msgFlags);
+ aHdrDeleted->SetFlags(aFlags);
+ rv = m_searchSession->MatchHdr(aHdrDeleted, msgDB, &match);
+ aHdrDeleted->SetFlags(msgFlags);
+ m_searchSession->ClearScopes();
+ if (match) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool msgHdrIsRead;
+ aHdrDeleted->GetIsRead(&msgHdrIsRead);
+ if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(-1);
+ dbFolderInfo->ChangeNumMessages(-1);
+ if (aFlags & nsMsgMessageFlags::New) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetNumNewMessages(numNewMessages - 1);
+ if (numNewMessages == 1) m_virtualFolder->SetHasNewMessages(false);
+ }
+
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrDeleted, false);
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrAdded(
+ nsIMsgDBHdr* aNewHdr, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool match = false;
+ if (!m_searchSession) return NS_ERROR_NULL_POINTER;
+
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aNewHdr, msgDB, &match);
+ m_searchSession->ClearScopes();
+ if (match) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool msgHdrIsRead;
+ uint32_t msgFlags;
+ aNewHdr->GetIsRead(&msgHdrIsRead);
+ aNewHdr->GetFlags(&msgFlags);
+ if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(1);
+ if (msgFlags & nsMsgMessageFlags::New) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetHasNewMessages(true);
+ m_virtualFolder->SetNumNewMessages(numNewMessages + 1);
+ }
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aNewHdr, true);
+ dbFolderInfo->ChangeNumMessages(1);
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnParentChanged(
+ nsMsgKey aKeyChanged, nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB = do_QueryInterface(instigator);
+ if (msgDB) msgDB->RemoveListener(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VirtualFolderChangeListener::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnReadChanged(
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnJunkScoreChanged(
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+nsresult VirtualFolderChangeListener::PostUpdateEvent(
+ nsIMsgFolder* virtualFolder, nsIMsgDatabase* virtDatabase) {
+ if (m_batchingEvents) return NS_OK;
+ m_batchingEvents = true;
+ nsCOMPtr<nsIRunnable> event =
+ new VFChangeListenerEvent(this, virtualFolder, virtDatabase);
+ return NS_DispatchToCurrentThread(event);
+}
+
+void VirtualFolderChangeListener::ProcessUpdateEvent(nsIMsgFolder* virtFolder,
+ nsIMsgDatabase* virtDB) {
+ m_batchingEvents = false;
+ virtFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDB->Commit(nsMsgDBCommitType::kLargeCommit);
+}
+
+nsresult nsMsgAccountManager::GetVirtualFoldersFile(nsCOMPtr<nsIFile>& aFile) {
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = profileDir->AppendNative("virtualFolders.dat"_ns);
+ if (NS_SUCCEEDED(rv)) aFile = profileDir;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::LoadVirtualFolders() {
+ nsCOMPtr<nsIFile> file;
+ GetVirtualFoldersFile(file);
+ if (!file) return NS_ERROR_FAILURE;
+
+ if (m_virtualFoldersLoaded) return NS_OK;
+
+ m_loadingVirtualFolders = true;
+
+ // Before loading virtual folders, ensure that all real folders exist.
+ // Some may not have been created yet, which would break virtual folders
+ // that depend on them.
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ nsresult rv = GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto server : allServers) {
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ rootFolder->GetSubFolders(dummy);
+ }
+ }
+ }
+
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = fileStream->Init(file, PR_RDONLY, 0664, false);
+ nsCOMPtr<nsILineInputStream> lineInputStream(do_QueryInterface(fileStream));
+
+ bool isMore = true;
+ nsAutoCString buffer;
+ int32_t version = -1;
+ nsCOMPtr<nsIMsgFolder> virtualFolder;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
+ if (!buffer.IsEmpty()) {
+ if (version == -1) {
+ buffer.Cut(0, 8);
+ nsresult irv;
+ version = buffer.ToInteger(&irv);
+ continue;
+ }
+ if (StringBeginsWith(buffer, "uri="_ns)) {
+ buffer.Cut(0, 4);
+ dbFolderInfo = nullptr;
+
+ rv = GetOrCreateFolder(buffer, getter_AddRefs(virtualFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ virtualFolder->SetFlag(nsMsgFolderFlags::Virtual);
+
+ nsCOMPtr<nsIMsgFolder> grandParent;
+ nsCOMPtr<nsIMsgFolder> oldParent;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ bool isServer;
+ // This loop handles creating virtual folders without an existing
+ // parent.
+ do {
+ // need to add the folder as a sub-folder of its parent.
+ int32_t lastSlash = buffer.RFindChar('/');
+ if (lastSlash == kNotFound) break;
+ nsDependentCSubstring parentUri(buffer, 0, lastSlash);
+ // hold a reference so it won't get deleted before it's parented.
+ oldParent = parentFolder;
+
+ rv = GetOrCreateFolder(parentUri, getter_AddRefs(parentFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString currentFolderNameStr;
+ nsAutoCString currentFolderNameCStr;
+ MsgUnescapeString(
+ nsCString(Substring(buffer, lastSlash + 1, buffer.Length())), 0,
+ currentFolderNameCStr);
+ CopyUTF8toUTF16(currentFolderNameCStr, currentFolderNameStr);
+ nsCOMPtr<nsIMsgFolder> childFolder;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // force db to get created.
+ // XXX TODO: is this SetParent() right? Won't it screw up if virtual
+ // folder is nested >2 deep? Leave for now, but revisit when getting
+ // rid of dangling folders (BenC).
+ virtualFolder->SetParent(parentFolder);
+ rv = virtualFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ m_dbService->CreateNewDB(virtualFolder, getter_AddRefs(db));
+ if (db)
+ rv = db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ else
+ break;
+
+ parentFolder->AddSubfolder(currentFolderNameStr,
+ getter_AddRefs(childFolder));
+ if (childFolder) parentFolder->NotifyFolderAdded(childFolder);
+ // here we make sure if our parent is rooted - if not, we're
+ // going to loop and add our parent as a child of its grandparent
+ // and repeat until we get to the server, or a folder that
+ // has its parent set.
+ parentFolder->GetParent(getter_AddRefs(grandParent));
+ parentFolder->GetIsServer(&isServer);
+ buffer.SetLength(lastSlash);
+ } while (!grandParent && !isServer);
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "scope="_ns)) {
+ buffer.Cut(0, 6);
+ // if this is a cross folder virtual folder, we have a list of folders
+ // uris, and we have to add a pending listener for each of them.
+ if (!buffer.IsEmpty()) {
+ ParseAndVerifyVirtualFolderScope(buffer);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, buffer);
+ AddVFListenersForVF(virtualFolder, buffer);
+ }
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "terms="_ns)) {
+ buffer.Cut(0, 6);
+ dbFolderInfo->SetCharProperty("searchStr", buffer);
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "searchOnline="_ns)) {
+ buffer.Cut(0, 13);
+ dbFolderInfo->SetBooleanProperty("searchOnline",
+ buffer.EqualsLiteral("true"));
+ } else if (dbFolderInfo &&
+ Substring(buffer, 0, SEARCH_FOLDER_FLAG_LEN + 1)
+ .Equals(SEARCH_FOLDER_FLAG "=")) {
+ buffer.Cut(0, SEARCH_FOLDER_FLAG_LEN + 1);
+ dbFolderInfo->SetCharProperty(SEARCH_FOLDER_FLAG, buffer);
+ }
+ }
+ }
+
+ m_loadingVirtualFolders = false;
+ m_virtualFoldersLoaded = true;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::SaveVirtualFolders() {
+ if (!m_virtualFoldersLoaded) return NS_OK;
+
+ nsCOMPtr<nsIFile> file;
+ GetVirtualFoldersFile(file);
+
+ // Open a buffered, safe output stream
+ nsCOMPtr<nsIOutputStream> outStream;
+ nsresult rv = MsgNewSafeBufferedFileOutputStream(
+ getter_AddRefs(outStream), file, PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE,
+ 0664);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ WriteLineToOutputStream("version=", "1", outStream);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> virtualFolders;
+ nsresult rv = rootFolder->GetFoldersWithFlags(nsMsgFolderFlags::Virtual,
+ virtualFolders);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ for (auto msgFolder : virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = msgFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db)); // force db to get created.
+ if (dbFolderInfo) {
+ nsCString srchFolderUri;
+ nsCString searchTerms;
+ nsCString regexScope;
+ nsCString vfFolderFlag;
+ bool searchOnline = false;
+ dbFolderInfo->GetBooleanProperty("searchOnline", false,
+ &searchOnline);
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri);
+ dbFolderInfo->GetCharProperty("searchStr", searchTerms);
+ // logically searchFolderFlag is an int, but since we want to
+ // write out a string, get it as a string.
+ dbFolderInfo->GetCharProperty(SEARCH_FOLDER_FLAG, vfFolderFlag);
+ nsCString uri;
+ msgFolder->GetURI(uri);
+ if (!srchFolderUri.IsEmpty() && !searchTerms.IsEmpty()) {
+ WriteLineToOutputStream("uri=", uri.get(), outStream);
+ if (!vfFolderFlag.IsEmpty())
+ WriteLineToOutputStream(SEARCH_FOLDER_FLAG "=",
+ vfFolderFlag.get(), outStream);
+ WriteLineToOutputStream("scope=", srchFolderUri.get(), outStream);
+ WriteLineToOutputStream("terms=", searchTerms.get(), outStream);
+ WriteLineToOutputStream(
+ "searchOnline=", searchOnline ? "true" : "false", outStream);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outStream, &rv);
+ NS_ASSERTION(safeStream, "expected a safe output stream!");
+ if (safeStream) {
+ rv = safeStream->Finish();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to save personal dictionary file! possible data loss");
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgAccountManager::WriteLineToOutputStream(
+ const char* prefix, const char* line, nsIOutputStream* outputStream) {
+ uint32_t writeCount;
+ outputStream->Write(prefix, strlen(prefix), &writeCount);
+ outputStream->Write(line, strlen(line), &writeCount);
+ outputStream->Write("\n", 1, &writeCount);
+ return NS_OK;
+}
+
+/**
+ * Parse the '|' separated folder uri string into individual folders, verify
+ * that the folders are real. If we were to add things like wildcards, we
+ * could implement the expansion into real folders here.
+ *
+ * @param buffer On input, list of folder uri's, on output, verified list.
+ */
+void nsMsgAccountManager::ParseAndVerifyVirtualFolderScope(nsCString& buffer) {
+ if (buffer.Equals("*")) {
+ // This is a special virtual folder that searches all folders in all
+ // accounts. Folders are chosen by the front end at search time.
+ return;
+ }
+ nsCString verifiedFolders;
+ nsTArray<nsCString> folderUris;
+ ParseString(buffer, '|', folderUris);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsIMsgFolder> parent;
+
+ for (uint32_t i = 0; i < folderUris.Length(); i++) {
+ nsCOMPtr<nsIMsgFolder> realFolder;
+ nsresult rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder));
+ if (!NS_SUCCEEDED(rv)) {
+ continue;
+ }
+ realFolder->GetParent(getter_AddRefs(parent));
+ if (!parent) continue;
+ realFolder->GetServer(getter_AddRefs(server));
+ if (!server) continue;
+ if (!verifiedFolders.IsEmpty()) verifiedFolders.Append('|');
+ verifiedFolders.Append(folderUris[i]);
+ }
+ buffer.Assign(verifiedFolders);
+}
+
+// This conveniently works to add a single folder as well.
+nsresult nsMsgAccountManager::AddVFListenersForVF(
+ nsIMsgFolder* virtualFolder, const nsCString& srchFolderUris) {
+ if (srchFolderUris.Equals("*")) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Avoid any possible duplicate listeners for this virtual folder.
+ RemoveVFListenerForVF(virtualFolder, nullptr);
+
+ nsTArray<nsCString> folderUris;
+ ParseString(srchFolderUris, '|', folderUris);
+
+ for (uint32_t i = 0; i < folderUris.Length(); i++) {
+ nsCOMPtr<nsIMsgFolder> realFolder;
+ rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<VirtualFolderChangeListener> dbListener =
+ new VirtualFolderChangeListener();
+ NS_ENSURE_TRUE(dbListener, NS_ERROR_OUT_OF_MEMORY);
+ dbListener->m_virtualFolder = virtualFolder;
+ dbListener->m_folderWatching = realFolder;
+ if (NS_FAILED(dbListener->Init())) {
+ dbListener = nullptr;
+ continue;
+ }
+ m_virtualFolderListeners.AppendElement(dbListener);
+ m_dbService->RegisterPendingListener(realFolder, dbListener);
+ }
+ if (!m_virtualFolders.Contains(virtualFolder)) {
+ m_virtualFolders.AppendElement(virtualFolder);
+ }
+ return NS_OK;
+}
+
+// This is called if a folder that's part of the scope of a saved search
+// has gone away.
+nsresult nsMsgAccountManager::RemoveVFListenerForVF(nsIMsgFolder* virtualFolder,
+ nsIMsgFolder* folder) {
+ nsresult rv;
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ if (listener->m_virtualFolder == virtualFolder &&
+ (!folder || listener->m_folderWatching == folder)) {
+ m_dbService->UnregisterPendingListener(listener);
+ m_virtualFolderListeners.RemoveElement(listener);
+ if (folder) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetAllFolders(
+ nsTArray<RefPtr<nsIMsgFolder>>& aAllFolders) {
+ aAllFolders.Clear();
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ nsresult rv = GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto server : allServers) {
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> descendents;
+ rootFolder->GetDescendants(descendents);
+ aAllFolders.AppendElements(descendents);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* folder) {
+ if (!parent) {
+ // This method gets called for folders that aren't connected to anything,
+ // such as a junk folder that appears when an IMAP account is created. We
+ // don't want these added to the virtual folder.
+ return NS_OK;
+ }
+
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+
+ bool addToSmartFolders = false;
+ folder->IsSpecialFolder(nsMsgFolderFlags::Inbox |
+ nsMsgFolderFlags::Templates |
+ nsMsgFolderFlags::Trash |
+ nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Junk,
+ false, &addToSmartFolders);
+ // For Sent/Archives/Trash, we treat sub-folders of those folders as
+ // "special", and want to add them the smart folders search scope.
+ // So we check if this is a sub-folder of one of those special folders
+ // and set the corresponding folderFlag if so.
+ if (!addToSmartFolders) {
+ bool isSpecial = false;
+ folder->IsSpecialFolder(nsMsgFolderFlags::SentMail, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::SentMail;
+ }
+ folder->IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::Archive;
+ }
+ folder->IsSpecialFolder(nsMsgFolderFlags::Trash, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::Trash;
+ }
+ }
+ nsresult rv = NS_OK;
+ // if this is a special folder, check if we have a saved search over
+ // folders with this flag, and if so, add this folder to the scope.
+ if (addToSmartFolders) {
+ // quick way to enumerate the saved searches.
+ for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ // found a saved search over folders w/ the same flag as the new folder.
+ if (vfFolderFlag & folderFlags) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+
+ // "normalize" searchURI so we can search for |folderURI|.
+ if (!searchURI.IsEmpty()) {
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ }
+ nsCString folderURI;
+ folder->GetURI(folderURI);
+ folderURI.Insert('|', 0);
+ folderURI.Append('|');
+
+ int32_t index = searchURI.Find(folderURI);
+ if (index == kNotFound) {
+ searchURI.Cut(0, 1);
+ folderURI.Cut(0, 1);
+ folderURI.SetLength(folderURI.Length() - 1);
+ searchURI.Append(folderURI);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(virtualFolder, "search-folders-changed",
+ nullptr);
+ }
+
+ // Add sub-folders to smart folder.
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = folder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ for (auto subFolder : allDescendants) {
+ subFolder->GetParent(getter_AddRefs(parentFolder));
+ OnFolderAdded(parentFolder, subFolder);
+ }
+ }
+ }
+ }
+ }
+
+ // Find any virtual folders that search `parent`, and add `folder` to them.
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ if (listener->m_folderWatching == parent) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ listener->m_virtualFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(db));
+
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ if (addToSmartFolders && vfFolderFlag &&
+ !(vfFolderFlag & nsMsgFolderFlags::Trash)) {
+ // Don't add folders of one type to the unified folder of another
+ // type, unless it's the Trash unified folder.
+ continue;
+ }
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+
+ // "normalize" searchURI so we can search for |folderURI|.
+ if (!searchURI.IsEmpty()) {
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ }
+ nsCString folderURI;
+ folder->GetURI(folderURI);
+
+ int32_t index = searchURI.Find(folderURI);
+ if (index == kNotFound) {
+ searchURI.Cut(0, 1);
+ searchURI.Append(folderURI);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(listener->m_virtualFolder,
+ "search-folders-changed", nullptr);
+ }
+ }
+ }
+ }
+
+ // need to make sure this isn't happening during loading of virtualfolders.dat
+ if (folderFlags & nsMsgFolderFlags::Virtual && !m_loadingVirtualFolders) {
+ // When a new virtual folder is added, need to create a db Listener for it.
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString srchFolderUri;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri);
+ AddVFListenersForVF(folder, srchFolderUri);
+ rv = SaveVirtualFolders();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderRemoved(nsIMsgFolder* parentFolder,
+ nsIMsgFolder* folder) {
+ nsresult rv = NS_OK;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ // if we removed a VF, flush VF list to disk.
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ RemoveVFListenerForVF(folder, nullptr);
+ m_virtualFolders.RemoveElement(folder);
+ rv = SaveVirtualFolders();
+ // clear flags on deleted folder if it's a virtual folder, so that creating
+ // a new folder with the same name doesn't cause confusion.
+ folder->SetFlags(0);
+ return rv;
+ }
+ // need to update the saved searches to check for a few things:
+ // 1. Folder removed was in the scope of a saved search - if so, remove the
+ // uri from the scope of the saved search.
+ // 2. If the scope is now empty, remove the saved search.
+
+ // build a "normalized" uri that we can do a find on.
+ nsCString removedFolderURI;
+ folder->GetURI(removedFolderURI);
+ removedFolderURI.Insert('|', 0);
+ removedFolderURI.Append('|');
+
+ // Enumerate the saved searches.
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgFolder> savedSearch = listener->m_virtualFolder;
+ savedSearch->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+ // "normalize" searchURI so we can search for |folderURI|.
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ int32_t index = searchURI.Find(removedFolderURI);
+ if (index != kNotFound) {
+ RemoveVFListenerForVF(savedSearch, folder);
+
+ // remove |folderURI
+ searchURI.Cut(index, removedFolderURI.Length() - 1);
+ // remove last '|' we added
+ searchURI.SetLength(searchURI.Length() - 1);
+
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+
+ // If saved search is empty now, delete it. But not if it's a smart
+ // folder.
+ if (searchURI.IsEmpty() && !vfFolderFlag) {
+ db = nullptr;
+ dbFolderInfo = nullptr;
+
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = savedSearch->GetParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!parent) continue;
+ parent->PropagateDelete(savedSearch, true);
+ } else {
+ if (!searchURI.IsEmpty()) {
+ // Remove leading '|' we added (or one after |folderURI, if first
+ // URI).
+ searchURI.Cut(0, 1);
+ }
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(savedSearch, "search-folders-changed", nullptr);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property,
+ const nsACString& oldValue, const nsACString& newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::OnFolderIntPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ int64_t oldValue,
+ int64_t newValue) {
+ if (aProperty.Equals(kFolderFlag)) {
+ if (newValue & nsMsgFolderFlags::Virtual) {
+ // This is a virtual folder, let's get out of here.
+ return NS_OK;
+ }
+ uint32_t smartFlagsChanged =
+ (oldValue ^ newValue) &
+ (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue);
+ if (smartFlagsChanged) {
+ if (smartFlagsChanged & newValue) {
+ // if the smart folder flag was set, calling OnFolderAdded will
+ // do the right thing.
+ nsCOMPtr<nsIMsgFolder> parent;
+ aFolder->GetParent(getter_AddRefs(parent));
+ nsresult rv = OnFolderAdded(parent, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This folder has one of the smart folder flags.
+ // Remove it from any other smart folders it might've been included in
+ // because of the flags of its ancestors.
+ RemoveFolderFromSmartFolder(
+ aFolder, (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue) &
+ ~newValue);
+ return NS_OK;
+ }
+ RemoveFolderFromSmartFolder(aFolder, smartFlagsChanged);
+
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ nsresult rv = aFolder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto subFolder : allDescendants) {
+ RemoveFolderFromSmartFolder(subFolder, smartFlagsChanged);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::RemoveFolderFromSmartFolder(
+ nsIMsgFolder* aFolder, uint32_t flagsChanged) {
+ nsCString removedFolderURI;
+ aFolder->GetURI(removedFolderURI);
+ removedFolderURI.Insert('|', 0);
+ removedFolderURI.Append('|');
+ uint32_t flags;
+ aFolder->GetFlags(&flags);
+ NS_ASSERTION(!(flags & flagsChanged), "smart folder flag should not be set");
+ // Flag was removed. Look for smart folder based on that flag,
+ // and remove this folder from its scope.
+ for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ // found a smart folder over the removed flag
+ if (vfFolderFlag & flagsChanged) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+ // "normalize" searchURI so we can search for |folderURI|.
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ int32_t index = searchURI.Find(removedFolderURI);
+ if (index != kNotFound) {
+ RemoveVFListenerForVF(virtualFolder, aFolder);
+
+ // remove |folderURI
+ searchURI.Cut(index, removedFolderURI.Length() - 1);
+ // remove last '|' we added
+ searchURI.SetLength(searchURI.Length() - 1);
+
+ // remove leading '|' we added (or one after |folderURI, if first URI)
+ searchURI.Cut(0, 1);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(virtualFolder, "search-folders-changed",
+ nullptr);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderBoolPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, bool oldValue,
+ bool newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyFlagChanged(
+ nsIMsgDBHdr* msg, const nsACString& property, uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderEvent(nsIMsgFolder* aFolder,
+ const nsACString& aEvent) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FolderUriForPath(nsIFile* aLocalPath,
+ nsACString& aMailboxUri) {
+ NS_ENSURE_ARG_POINTER(aLocalPath);
+ bool equals;
+ if (m_lastPathLookedUp &&
+ NS_SUCCEEDED(aLocalPath->Equals(m_lastPathLookedUp, &equals)) && equals) {
+ aMailboxUri = m_lastFolderURIForPath;
+ return NS_OK;
+ }
+ nsTArray<RefPtr<nsIMsgFolder>> folders;
+ nsresult rv = GetAllFolders(folders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto folder : folders) {
+ nsCOMPtr<nsIFile> folderPath;
+ rv = folder->GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if we're equal
+ rv = folderPath->Equals(aLocalPath, &equals);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (equals) {
+ rv = folder->GetURI(aMailboxUri);
+ m_lastFolderURIForPath = aMailboxUri;
+ aLocalPath->Clone(getter_AddRefs(m_lastPathLookedUp));
+ return rv;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetSortOrder(nsIMsgIncomingServer* aServer,
+ int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+
+ // If the passed in server is the default, return its sort order as 0
+ // regardless of its server sort order.
+
+ nsCOMPtr<nsIMsgAccount> defaultAccount;
+ nsresult rv = GetDefaultAccount(getter_AddRefs(defaultAccount));
+ if (NS_SUCCEEDED(rv) && defaultAccount) {
+ nsCOMPtr<nsIMsgIncomingServer> defaultServer;
+ rv = m_defaultAccount->GetIncomingServer(getter_AddRefs(defaultServer));
+ if (NS_SUCCEEDED(rv) && (aServer == defaultServer)) {
+ *aSortOrder = 0;
+ return NS_OK;
+ }
+ // It is OK if there is no default account.
+ }
+
+ // This function returns the sort order by querying the server object for its
+ // sort order value and then incrementing it by the position of the server in
+ // the accounts list. This ensures that even when several accounts have the
+ // same sort order value, the returned value is not the same and keeps
+ // their relative order in the account list when and unstable sort is run
+ // on the returned sort order values.
+ int32_t sortOrder;
+ int32_t serverIndex;
+
+ rv = aServer->GetSortOrder(&sortOrder);
+ if (NS_SUCCEEDED(rv)) rv = FindServerIndex(aServer, &serverIndex);
+
+ if (NS_FAILED(rv)) {
+ *aSortOrder = 999999999;
+ } else {
+ *aSortOrder = sortOrder + serverIndex;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ReorderAccounts(const nsTArray<nsCString>& newAccounts) {
+ nsTArray<nsCString> allNewAccounts = newAccounts.Clone();
+
+ // Add all hidden accounts to the list of new accounts.
+ nsresult rv;
+ for (auto account : m_accounts) {
+ nsCString key;
+ account->GetKey(key);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = account->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ bool hidden = false;
+ rv = server->GetHidden(&hidden);
+ if (NS_SUCCEEDED(rv) && hidden && !allNewAccounts.Contains(key)) {
+ allNewAccounts.AppendElement(key);
+ }
+ }
+ }
+
+ // Check that the new account list contains all the existing accounts,
+ // just in a different order.
+ if (allNewAccounts.Length() != m_accounts.Length())
+ return NS_ERROR_INVALID_ARG;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); i++) {
+ nsCString accountKey;
+ m_accounts[i]->GetKey(accountKey);
+ if (!allNewAccounts.Contains(accountKey)) return NS_ERROR_INVALID_ARG;
+ }
+
+ // In-place swap the elements in m_accounts to the order defined in
+ // newAccounts.
+ for (uint32_t i = 0; i < allNewAccounts.Length(); i++) {
+ nsCString newKey = allNewAccounts[i];
+ for (uint32_t j = i; j < m_accounts.Length(); j++) {
+ nsCString oldKey;
+ m_accounts[j]->GetKey(oldKey);
+ if (newKey.Equals(oldKey)) {
+ if (i != j) std::swap(m_accounts[i], m_accounts[j]);
+ break;
+ }
+ }
+ }
+
+ return OutputAccountsPref();
+}
diff --git a/comm/mailnews/base/src/nsMsgAccountManager.h b/comm/mailnews/base/src/nsMsgAccountManager.h
new file mode 100644
index 0000000000..9dc1e45da3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccountManager.h
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+#include "nscore.h"
+#include "nsIMsgAccountManager.h"
+#include "nsCOMPtr.h"
+#include "nsISmtpServer.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolder.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsIUrlListener.h"
+#include "nsCOMArray.h"
+#include "nsIMsgSearchSession.h"
+#include "nsInterfaceHashtable.h"
+#include "nsIMsgDatabase.h"
+#include "nsIDBChangeListener.h"
+#include "nsTObserverArray.h"
+
+class VirtualFolderChangeListener final : public nsIDBChangeListener {
+ public:
+ VirtualFolderChangeListener();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDBCHANGELISTENER
+
+ nsresult Init();
+ /**
+ * Posts an event to update the summary totals and commit the db.
+ * We post the event to avoid committing each time we're called
+ * in a synchronous loop.
+ */
+ nsresult PostUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db);
+ /// Handles event posted to event queue to batch notifications.
+ void ProcessUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db);
+
+ void DecrementNewMsgCount();
+
+ // folder we're listening to db changes on behalf of.
+ nsCOMPtr<nsIMsgFolder> m_virtualFolder;
+ // folder whose db we're listening to.
+ nsCOMPtr<nsIMsgFolder> m_folderWatching;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ bool m_searchOnMsgStatus;
+ bool m_batchingEvents;
+
+ private:
+ ~VirtualFolderChangeListener() {}
+};
+
+class nsMsgAccountManager : public nsIMsgAccountManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIFolderListener {
+ public:
+ nsMsgAccountManager();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* nsIMsgAccountManager methods */
+
+ NS_DECL_NSIMSGACCOUNTMANAGER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIFOLDERLISTENER
+
+ nsresult Init();
+ nsresult Shutdown();
+ void LogoutOfServer(nsIMsgIncomingServer* aServer);
+
+ private:
+ virtual ~nsMsgAccountManager();
+
+ bool m_accountsLoaded;
+ nsCOMPtr<nsIMsgFolderCache> m_msgFolderCache;
+ nsTArray<nsCOMPtr<nsIMsgAccount>> m_accounts;
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgIdentity> m_identities;
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgIncomingServer>
+ m_incomingServers;
+ nsCOMPtr<nsIMsgAccount> m_defaultAccount;
+ nsCOMArray<nsIIncomingServerListener> m_incomingServerListeners;
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>
+ m_virtualFolderListeners;
+ nsTArray<nsCOMPtr<nsIMsgFolder>> m_virtualFolders;
+ nsCOMPtr<nsIMsgFolder> m_folderDoingEmptyTrash;
+ nsCOMPtr<nsIMsgFolder> m_folderDoingCleanupInbox;
+ bool m_emptyTrashInProgress;
+ bool m_cleanupInboxInProgress;
+
+ nsCString mAccountKeyList;
+
+ // These are static because the account manager may go away during
+ // shutdown, and get recreated.
+ static bool m_haveShutdown;
+ static bool m_shutdownInProgress;
+
+ bool m_userAuthenticated;
+ bool m_loadingVirtualFolders;
+ bool m_virtualFoldersLoaded;
+
+ /* we call FindServer() a lot. so cache the last server found */
+ nsCOMPtr<nsIMsgIncomingServer> m_lastFindServerResult;
+ nsCString m_lastFindServerHostName;
+ nsCString m_lastFindServerUserName;
+ int32_t m_lastFindServerPort;
+ nsCString m_lastFindServerType;
+
+ void SetLastServerFound(nsIMsgIncomingServer* server,
+ const nsACString& hostname,
+ const nsACString& username, const int32_t port,
+ const nsACString& type);
+
+ // Cache the results of the last call to FolderUriFromDirInProfile
+ nsCOMPtr<nsIFile> m_lastPathLookedUp;
+ nsCString m_lastFolderURIForPath;
+
+ /* internal creation routines - updates m_identities and m_incomingServers */
+ nsresult createKeyedAccount(const nsCString& key, bool forcePositionToEnd,
+ nsIMsgAccount** _retval);
+ nsresult createKeyedServer(const nsACString& key, const nsACString& username,
+ const nsACString& password, const nsACString& type,
+ nsIMsgIncomingServer** _retval);
+
+ nsresult createKeyedIdentity(const nsACString& key, nsIMsgIdentity** _retval);
+
+ nsresult GetLocalFoldersPrettyName(nsString& localFoldersName);
+
+ /**
+ * Check if the given account can be the set as the default account.
+ */
+ nsresult CheckDefaultAccount(nsIMsgAccount* aAccount, bool& aCanBeDefault);
+
+ /**
+ * Find a new account that can serve as default.
+ */
+ nsresult AutosetDefaultAccount();
+
+ // sets the pref for the default server
+ nsresult setDefaultAccountPref(nsIMsgAccount* aDefaultAccount);
+
+ // Write out the accounts pref from the m_accounts list of accounts.
+ nsresult OutputAccountsPref();
+
+ // fires notifications to the appropriate root folders
+ nsresult notifyDefaultServerChange(nsIMsgAccount* aOldAccount,
+ nsIMsgAccount* aNewAccount);
+
+ //
+ // account enumerators
+ // ("element" is always an account)
+ //
+
+ // find the servers that correspond to the given identity
+ static bool findServersForIdentity(nsISupports* element, void* aData);
+
+ void findAccountByServerKey(const nsCString& aKey, nsIMsgAccount** aResult);
+
+ //
+ // server enumerators
+ // ("element" is always a server)
+ //
+
+ nsresult findServerInternal(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type, int32_t port,
+ nsIMsgIncomingServer** aResult);
+
+ // handle virtual folders
+ static nsresult GetVirtualFoldersFile(nsCOMPtr<nsIFile>& file);
+ static nsresult WriteLineToOutputStream(const char* prefix, const char* line,
+ nsIOutputStream* outputStream);
+ void ParseAndVerifyVirtualFolderScope(nsCString& buffer);
+ nsresult AddVFListenersForVF(nsIMsgFolder* virtualFolder,
+ const nsCString& srchFolderUris);
+
+ nsresult RemoveVFListenerForVF(nsIMsgFolder* virtualFolder,
+ nsIMsgFolder* folder);
+
+ nsresult RemoveFolderFromSmartFolder(nsIMsgFolder* aFolder,
+ uint32_t flagsChanged);
+
+ nsresult SetSendLaterUriPref(nsIMsgIncomingServer* server);
+
+ nsCOMPtr<nsIPrefBranch> m_prefs;
+ nsCOMPtr<nsIMsgDBService> m_dbService;
+
+ //
+ // root folder listener stuff
+ //
+
+ // this array is for folder listeners that are supposed to be listening
+ // on the root folders.
+ // When a new server is created, all of the the folder listeners
+ // should be added to the new server
+ // When a new listener is added, it should be added to all root folders.
+ // similar for when servers are deleted or listeners removed
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>> mFolderListeners;
+
+ void removeListenersFromFolder(nsIMsgFolder* aFolder);
+};
diff --git a/comm/mailnews/base/src/nsMsgBiffManager.cpp b/comm/mailnews/base/src/nsMsgBiffManager.cpp
new file mode 100644
index 0000000000..9d990a30bd
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgBiffManager.cpp
@@ -0,0 +1,341 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgBiffManager.h"
+#include "nsIMsgAccountManager.h"
+#include "nsStatusBarBiffManager.h"
+#include "nsCOMArray.h"
+#include "mozilla/Logging.h"
+#include "nspr.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIObserverService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsITimer.h"
+#include "mozilla/Services.h"
+
+#define PREF_BIFF_JITTER "mail.biff.add_interval_jitter"
+
+#define NS_STATUSBARBIFFMANAGER_CID \
+ { \
+ 0x7f9a9fb0, 0x4161, 0x11d4, { \
+ 0x98, 0x76, 0x00, 0xc0, 0x4f, 0xa0, 0xd2, 0xa6 \
+ } \
+ }
+static NS_DEFINE_CID(kStatusBarBiffManagerCID, NS_STATUSBARBIFFMANAGER_CID);
+
+static mozilla::LazyLogModule MsgBiffLogModule("MsgBiff");
+
+NS_IMPL_ISUPPORTS(nsMsgBiffManager, nsIMsgBiffManager,
+ nsIIncomingServerListener, nsIObserver,
+ nsISupportsWeakReference)
+
+void OnBiffTimer(nsITimer* timer, void* aBiffManager) {
+ nsMsgBiffManager* biffManager = (nsMsgBiffManager*)aBiffManager;
+ biffManager->PerformBiff();
+}
+
+nsMsgBiffManager::nsMsgBiffManager() {
+ mHaveShutdown = false;
+ mInited = false;
+}
+
+nsMsgBiffManager::~nsMsgBiffManager() {
+ if (mBiffTimer) mBiffTimer->Cancel();
+
+ if (!mHaveShutdown) Shutdown();
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "wake_notification");
+ observerService->RemoveObserver(this, "sleep_notification");
+ }
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Init() {
+ if (mInited) return NS_OK;
+
+ mInited = true;
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->AddIncomingServerListener(this);
+
+ // in turbo mode on profile change we don't need to do anything below this
+ if (mHaveShutdown) {
+ mHaveShutdown = false;
+ return NS_OK;
+ }
+
+ // Ensure status bar biff service has started
+ nsCOMPtr<nsIFolderListener> statusBarBiffService =
+ do_GetService(kStatusBarBiffManagerCID, &rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "sleep_notification", true);
+ observerService->AddObserver(this, "wake_notification", true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Shutdown() {
+ if (mBiffTimer) {
+ mBiffTimer->Cancel();
+ mBiffTimer = nullptr;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->RemoveIncomingServerListener(this);
+
+ mHaveShutdown = true;
+ mInited = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "sleep_notification") && mBiffTimer) {
+ mBiffTimer->Cancel();
+ mBiffTimer = nullptr;
+ } else if (!strcmp(aTopic, "wake_notification")) {
+ // wait 10 seconds after waking up to start biffing again.
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, 10000,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mBiffTimer timer");
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::AddServerBiff(nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ int32_t biffMinutes;
+
+ nsresult rv = server->GetBiffMinutes(&biffMinutes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't add if biffMinutes isn't > 0
+ if (biffMinutes > 0) {
+ int32_t serverIndex = FindServer(server);
+ // Only add it if it hasn't been added already.
+ if (serverIndex == -1) {
+ nsBiffEntry biffEntry;
+ biffEntry.server = server;
+ rv = SetNextBiffTime(biffEntry, PR_Now());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddBiffEntry(biffEntry);
+ SetupNextBiff();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::RemoveServerBiff(nsIMsgIncomingServer* server) {
+ int32_t pos = FindServer(server);
+ if (pos != -1) mBiffArray.RemoveElementAt(pos);
+
+ // Should probably reset biff time if this was the server that gets biffed
+ // next.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::ForceBiff(nsIMsgIncomingServer* server) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::ForceBiffAll() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerLoaded(nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ bool doBiff = false;
+ nsresult rv = server->GetDoBiff(&doBiff);
+
+ if (NS_SUCCEEDED(rv) && doBiff) rv = AddServerBiff(server);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerUnloaded(nsIMsgIncomingServer* server) {
+ return RemoveServerBiff(server);
+}
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerChanged(nsIMsgIncomingServer* server) {
+ // nothing required. If the hostname or username changed
+ // the next time biff fires, we'll ping the right server
+ return NS_OK;
+}
+
+int32_t nsMsgBiffManager::FindServer(nsIMsgIncomingServer* server) {
+ uint32_t count = mBiffArray.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ if (server == mBiffArray[i].server.get()) return i;
+ }
+ return -1;
+}
+
+nsresult nsMsgBiffManager::AddBiffEntry(nsBiffEntry& biffEntry) {
+ uint32_t i;
+ uint32_t count = mBiffArray.Length();
+ for (i = 0; i < count; i++) {
+ if (biffEntry.nextBiffTime < mBiffArray[i].nextBiffTime) break;
+ }
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("inserting biff entry at %d", i));
+ mBiffArray.InsertElementAt(i, biffEntry);
+ return NS_OK;
+}
+
+nsresult nsMsgBiffManager::SetNextBiffTime(nsBiffEntry& biffEntry,
+ PRTime currentTime) {
+ nsIMsgIncomingServer* server = biffEntry.server;
+ NS_ENSURE_TRUE(server, NS_ERROR_FAILURE);
+
+ int32_t biffInterval;
+ nsresult rv = server->GetBiffMinutes(&biffInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add biffInterval, converted in microseconds, to current time.
+ // Force 64-bit multiplication.
+ PRTime chosenTimeInterval = biffInterval * 60000000LL;
+ biffEntry.nextBiffTime = currentTime + chosenTimeInterval;
+
+ // Check if we should jitter.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ bool shouldUseBiffJitter = false;
+ prefs->GetBoolPref(PREF_BIFF_JITTER, &shouldUseBiffJitter);
+ if (shouldUseBiffJitter) {
+ // Calculate a jitter of +/-5% on chosenTimeInterval
+ // - minimum 1 second (to avoid a modulo with 0)
+ // - maximum 30 seconds (to avoid problems when biffInterval is very
+ // large)
+ int64_t jitter = (int64_t)(0.05 * (int64_t)chosenTimeInterval);
+ jitter =
+ std::max<int64_t>(1000000LL, std::min<int64_t>(jitter, 30000000LL));
+ jitter = ((rand() % 2) ? 1 : -1) * (rand() % jitter);
+
+ biffEntry.nextBiffTime += jitter;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgBiffManager::SetupNextBiff() {
+ if (mBiffArray.Length() > 0) {
+ // Get the next biff entry
+ const nsBiffEntry& biffEntry = mBiffArray[0];
+ PRTime currentTime = PR_Now();
+ int64_t biffDelay;
+ int64_t ms(1000);
+
+ if (currentTime > biffEntry.nextBiffTime) {
+ // Let's wait 30 seconds before firing biff again
+ biffDelay = 30 * PR_USEC_PER_SEC;
+ } else
+ biffDelay = biffEntry.nextBiffTime - currentTime;
+
+ // Convert biffDelay into milliseconds
+ int64_t timeInMS = biffDelay / ms;
+ uint32_t timeInMSUint32 = (uint32_t)timeInMS;
+
+ // Can't currently reset a timer when it's in the process of
+ // calling Notify. So, just release the timer here and create a new one.
+ if (mBiffTimer) mBiffTimer->Cancel();
+
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("setting %d timer", timeInMSUint32));
+
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, timeInMSUint32,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mBiffTimer timer");
+ }
+ }
+ return NS_OK;
+}
+
+// This is the function that does a biff on all of the servers whose time it is
+// to biff.
+nsresult nsMsgBiffManager::PerformBiff() {
+ PRTime currentTime = PR_Now();
+ nsCOMArray<nsIMsgFolder> targetFolders;
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, ("performing biffs"));
+
+ uint32_t count = mBiffArray.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ // Take a copy of the entry rather than the a reference so that we can
+ // remove and add if necessary, but keep the references and memory alive.
+ nsBiffEntry current = mBiffArray[i];
+ if (current.nextBiffTime < currentTime) {
+ bool serverBusy = false;
+ bool serverRequiresPassword = true;
+ bool passwordPromptRequired;
+
+ current.server->GetPasswordPromptRequired(&passwordPromptRequired);
+ current.server->GetServerBusy(&serverBusy);
+ current.server->GetServerRequiresPasswordForBiff(&serverRequiresPassword);
+ // find the dest folder we're actually downloading to...
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ current.server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ int32_t targetFolderIndex = targetFolders.IndexOfObject(rootMsgFolder);
+ if (targetFolderIndex == kNotFound)
+ targetFolders.AppendObject(rootMsgFolder);
+
+ // so if we need to be authenticated to biff, check that we are
+ // (since we don't want to prompt the user for password UI)
+ // and make sure the server isn't already in the middle of downloading
+ // new messages
+ if (!serverBusy && (!serverRequiresPassword || !passwordPromptRequired) &&
+ targetFolderIndex == kNotFound) {
+ nsCString serverKey;
+ current.server->GetKey(serverKey);
+ nsresult rv = current.server->PerformBiff(nullptr);
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("biffing server %s rv = %" PRIx32, serverKey.get(),
+ static_cast<uint32_t>(rv)));
+ } else {
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("not biffing server serverBusy = %d requirespassword = %d "
+ "password prompt required = %d targetFolderIndex = %d",
+ serverBusy, serverRequiresPassword, passwordPromptRequired,
+ targetFolderIndex));
+ }
+ // if we didn't do this server because the destination server was already
+ // being biffed into, leave this server in the biff array so it will fire
+ // next.
+ if (targetFolderIndex == kNotFound) {
+ mBiffArray.RemoveElementAt(i);
+ i--; // Because we removed it we need to look at the one that just
+ // moved up.
+ SetNextBiffTime(current, currentTime);
+ AddBiffEntry(current);
+ }
+#ifdef DEBUG_David_Bienvenu
+ else
+ printf("dest account performing biff\n");
+#endif
+ } else
+ // since we're in biff order, there's no reason to keep checking
+ break;
+ }
+ SetupNextBiff();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgBiffManager.h b/comm/mailnews/base/src/nsMsgBiffManager.h
new file mode 100644
index 0000000000..e334429d71
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgBiffManager.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NSMSGBIFFMANAGER_H
+#define NSMSGBIFFMANAGER_H
+
+#include "msgCore.h"
+#include "nsIMsgBiffManager.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsIIncomingServerListener.h"
+#include "nsWeakReference.h"
+#include "nsIObserver.h"
+
+typedef struct {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ PRTime nextBiffTime;
+} nsBiffEntry;
+
+class nsMsgBiffManager : public nsIMsgBiffManager,
+ public nsIIncomingServerListener,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgBiffManager();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGBIFFMANAGER
+ NS_DECL_NSIINCOMINGSERVERLISTENER
+ NS_DECL_NSIOBSERVER
+
+ nsresult PerformBiff();
+
+ protected:
+ virtual ~nsMsgBiffManager();
+
+ int32_t FindServer(nsIMsgIncomingServer* server);
+ nsresult SetNextBiffTime(nsBiffEntry& biffEntry, PRTime currentTime);
+ nsresult SetupNextBiff();
+ nsresult AddBiffEntry(nsBiffEntry& biffEntry);
+
+ protected:
+ nsCOMPtr<nsITimer> mBiffTimer;
+ nsTArray<nsBiffEntry> mBiffArray;
+ bool mHaveShutdown;
+ bool mInited;
+};
+
+#endif // NSMSGBIFFMANAGER_H
diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.cpp b/comm/mailnews/base/src/nsMsgCompressIStream.cpp
new file mode 100644
index 0000000000..a0dd6b7776
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressIStream.cpp
@@ -0,0 +1,203 @@
+/* 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/. */
+
+#include "nsMsgCompressIStream.h"
+#include "prio.h"
+#include "prmem.h"
+#include <algorithm>
+
+#define BUFFER_SIZE 16384
+
+nsMsgCompressIStream::nsMsgCompressIStream()
+ : m_dataptr(nullptr), m_dataleft(0), m_inflateAgain(false) {}
+
+nsMsgCompressIStream::~nsMsgCompressIStream() { Close(); }
+
+NS_IMPL_ISUPPORTS(nsMsgCompressIStream, nsIInputStream, nsIAsyncInputStream)
+
+nsresult nsMsgCompressIStream::InitInputStream(nsIInputStream* rawStream) {
+ // protect against repeat calls
+ if (m_iStream) return NS_ERROR_UNEXPECTED;
+
+ // allocate some memory for buffering
+ m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // allocate some memory for buffering
+ m_databuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_databuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // set up zlib object
+ m_zstream.zalloc = Z_NULL;
+ m_zstream.zfree = Z_NULL;
+ m_zstream.opaque = Z_NULL;
+
+ // http://zlib.net/manual.html is rather silent on the topic, but
+ // perl's Compress::Raw::Zlib manual says:
+ // -WindowBits
+ // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS.
+ if (inflateInit2(&m_zstream, -MAX_WBITS) != Z_OK) return NS_ERROR_FAILURE;
+
+ m_iStream = rawStream;
+
+ return NS_OK;
+}
+
+nsresult nsMsgCompressIStream::DoInflation() {
+ // if there's something in the input buffer of the zstream, process it.
+ m_zstream.next_out = (Bytef*)m_databuf.get();
+ m_zstream.avail_out = BUFFER_SIZE;
+ int zr = inflate(&m_zstream, Z_SYNC_FLUSH);
+
+ // inflate() should normally be called until it returns
+ // Z_STREAM_END or an error, and Z_BUF_ERROR just means
+ // unable to progress any further (possible if we filled
+ // an output buffer exactly)
+ if (zr == Z_BUF_ERROR || zr == Z_STREAM_END) zr = Z_OK;
+
+ // otherwise it's an error
+ if (zr != Z_OK) return NS_ERROR_FAILURE;
+
+ // http://www.zlib.net/manual.html says:
+ // If inflate returns Z_OK and with zero avail_out, it must be called
+ // again after making room in the output buffer because there might be
+ // more output pending.
+ m_inflateAgain = m_zstream.avail_out ? false : true;
+
+ // set the pointer to the start of the buffer, and the count to how
+ // based on how many bytes are left unconsumed.
+ m_dataptr = m_databuf.get();
+ m_dataleft = BUFFER_SIZE - m_zstream.avail_out;
+
+ return NS_OK;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgCompressIStream::Close() { return CloseWithStatus(NS_OK); }
+
+NS_IMETHODIMP nsMsgCompressIStream::CloseWithStatus(nsresult reason) {
+ nsresult rv = NS_OK;
+
+ if (m_iStream) {
+ // pass the status through to our wrapped stream
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
+ do_QueryInterface(m_iStream);
+ if (asyncInputStream) rv = asyncInputStream->CloseWithStatus(reason);
+
+ // tidy up
+ m_iStream = nullptr;
+ inflateEnd(&m_zstream);
+ }
+
+ // clean up all the buffers
+ m_zbuf = nullptr;
+ m_databuf = nullptr;
+ m_dataptr = nullptr;
+ m_dataleft = 0;
+
+ return rv;
+}
+
+/* unsigned long long available (); */
+NS_IMETHODIMP nsMsgCompressIStream::Available(uint64_t* aResult) {
+ if (!m_iStream) return NS_BASE_STREAM_CLOSED;
+
+ // check if there's anything still in flight
+ if (!m_dataleft && m_inflateAgain) {
+ nsresult rv = DoInflation();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // we'll be returning this many to the next read, guaranteed
+ if (m_dataleft) {
+ *aResult = m_dataleft;
+ return NS_OK;
+ }
+
+ // this value isn't accurate, but will give a good true/false
+ // indication for idle purposes, and next read will fill
+ // m_dataleft, so we'll have an accurate count for the next call.
+ return m_iStream->Available(aResult);
+}
+
+/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgCompressIStream::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aResult) {
+ if (!m_iStream) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ // There are two stages of buffering:
+ // * m_zbuf contains the compressed data from the remote server
+ // * m_databuf contains the uncompressed raw bytes for consumption
+ // by the local client.
+ //
+ // Each buffer will only be filled when the following buffers
+ // have been entirely consumed.
+ //
+ // m_dataptr and m_dataleft are respectively a pointer to the
+ // unconsumed portion of m_databuf and the number of bytes
+ // of uncompressed data remaining in m_databuf.
+ //
+ // both buffers have a maximum size of BUFFER_SIZE, so it is
+ // possible that multiple inflate passes will be required to
+ // consume all of m_zbuf.
+ while (!m_dataleft) {
+ // get some more data if we don't already have any
+ if (!m_inflateAgain) {
+ uint32_t bytesRead;
+ nsresult rv =
+ m_iStream->Read(m_zbuf.get(), (uint32_t)BUFFER_SIZE, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bytesRead) return NS_BASE_STREAM_CLOSED;
+ m_zstream.next_in = (Bytef*)m_zbuf.get();
+ m_zstream.avail_in = bytesRead;
+ }
+
+ nsresult rv = DoInflation();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aResult = std::min(m_dataleft, aCount);
+
+ if (*aResult) {
+ memcpy(aBuf, m_dataptr, *aResult);
+ m_dataptr += *aResult;
+ m_dataleft -= *aResult;
+ }
+
+ return NS_OK;
+}
+
+/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in
+ * voidPtr aClosure, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgCompressIStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure,
+ uint32_t aCount,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgCompressIStream::AsyncWait(nsIInputStreamCallback* callback,
+ uint32_t flags, uint32_t amount,
+ nsIEventTarget* target) {
+ if (!m_iStream) return NS_BASE_STREAM_CLOSED;
+
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream = do_QueryInterface(m_iStream);
+ if (asyncInputStream)
+ return asyncInputStream->AsyncWait(callback, flags, amount, target);
+
+ return NS_OK;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgCompressIStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompressIStream::StreamStatus() {
+ return m_iStream->StreamStatus();
+}
diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.h b/comm/mailnews/base/src/nsMsgCompressIStream.h
new file mode 100644
index 0000000000..642d35f9fa
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressIStream.h
@@ -0,0 +1,33 @@
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIInputStream.h"
+#include "nsCOMPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "zlib.h"
+
+class nsMsgCompressIStream final : public nsIAsyncInputStream {
+ public:
+ nsMsgCompressIStream();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIINPUTSTREAM
+ NS_DECL_NSIASYNCINPUTSTREAM
+
+ nsresult InitInputStream(nsIInputStream* rawStream);
+
+ protected:
+ ~nsMsgCompressIStream();
+ nsresult DoInflation();
+ nsCOMPtr<nsIInputStream> m_iStream;
+ mozilla::UniquePtr<char[]> m_zbuf;
+ mozilla::UniquePtr<char[]> m_databuf;
+ char* m_dataptr;
+ uint32_t m_dataleft;
+ bool m_inflateAgain;
+ z_stream m_zstream;
+};
diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.cpp b/comm/mailnews/base/src/nsMsgCompressOStream.cpp
new file mode 100644
index 0000000000..fd490274ca
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressOStream.cpp
@@ -0,0 +1,128 @@
+/* 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/. */
+
+#include "nsMsgCompressOStream.h"
+#include "prio.h"
+#include "prmem.h"
+
+#define BUFFER_SIZE 16384
+
+nsMsgCompressOStream::nsMsgCompressOStream() : m_zbuf(nullptr) {}
+
+nsMsgCompressOStream::~nsMsgCompressOStream() { Close(); }
+
+NS_IMPL_ISUPPORTS(nsMsgCompressOStream, nsIOutputStream)
+
+nsresult nsMsgCompressOStream::InitOutputStream(nsIOutputStream* rawStream) {
+ // protect against repeat calls
+ if (m_oStream) return NS_ERROR_UNEXPECTED;
+
+ // allocate some memory for a buffer
+ m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // set up the zlib object
+ m_zstream.zalloc = Z_NULL;
+ m_zstream.zfree = Z_NULL;
+ m_zstream.opaque = Z_NULL;
+
+ // http://zlib.net/manual.html is rather silent on the topic, but
+ // perl's Compress::Raw::Zlib manual says:
+ // -WindowBits [...]
+ // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS.
+ if (deflateInit2(&m_zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS,
+ MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) != Z_OK)
+ return NS_ERROR_FAILURE;
+
+ m_oStream = rawStream;
+
+ return NS_OK;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgCompressOStream::Close() {
+ if (m_oStream) {
+ m_oStream = nullptr;
+ deflateEnd(&m_zstream);
+ }
+ m_zbuf = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::Write(const char* buf, uint32_t count, uint32_t* result) {
+ if (!m_oStream) return NS_BASE_STREAM_CLOSED;
+
+ m_zstream.next_in = (Bytef*)buf;
+ m_zstream.avail_in = count;
+
+ // keep looping until the buffer doesn't get filled
+ do {
+ m_zstream.next_out = (Bytef*)m_zbuf.get();
+ m_zstream.avail_out = BUFFER_SIZE;
+ // Using "Z_SYNC_FLUSH" may cause excess flushes if the calling
+ // code does a lot of small writes. An option with the IMAP
+ // protocol is to check the buffer for "\n" at the end, but
+ // in the interests of keeping this generic, don't optimise
+ // yet. An alternative is to require ->Flush always, but that
+ // is likely to break callers.
+ int zr = deflate(&m_zstream, Z_SYNC_FLUSH);
+ if (zr == Z_STREAM_END || zr == Z_BUF_ERROR)
+ zr = Z_OK; // not an error for our purposes
+ if (zr != Z_OK) return NS_ERROR_FAILURE;
+
+ uint32_t out_size = BUFFER_SIZE - m_zstream.avail_out;
+ const char* out_buf = m_zbuf.get();
+
+ // push everything in the buffer before repeating
+ while (out_size) {
+ uint32_t out_result;
+ nsresult rv = m_oStream->Write(out_buf, out_size, &out_result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!out_result) return NS_BASE_STREAM_CLOSED;
+ out_size -= out_result;
+ out_buf += out_result;
+ }
+
+ // http://www.zlib.net/manual.html says:
+ // If deflate returns with avail_out == 0, this function must be
+ // called again with the same value of the flush parameter and
+ // more output space (updated avail_out), until the flush is
+ // complete (deflate returns with non-zero avail_out).
+ } while (!m_zstream.avail_out);
+
+ *result = count;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::Flush(void) {
+ if (!m_oStream) return NS_BASE_STREAM_CLOSED;
+
+ return m_oStream->Flush();
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::WriteFrom(nsIInputStream* inStr, uint32_t count,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgCompressOStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompressOStream::StreamStatus() {
+ return m_oStream->StreamStatus();
+}
diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.h b/comm/mailnews/base/src/nsMsgCompressOStream.h
new file mode 100644
index 0000000000..96d38ad9c0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressOStream.h
@@ -0,0 +1,26 @@
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIOutputStream.h"
+#include "nsCOMPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "zlib.h"
+
+class nsMsgCompressOStream final : public nsIOutputStream {
+ public:
+ nsMsgCompressOStream();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIOUTPUTSTREAM
+
+ nsresult InitOutputStream(nsIOutputStream* rawStream);
+
+ protected:
+ ~nsMsgCompressOStream();
+ nsCOMPtr<nsIOutputStream> m_oStream;
+ mozilla::UniquePtr<char[]> m_zbuf;
+ z_stream m_zstream;
+};
diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.cpp b/comm/mailnews/base/src/nsMsgContentPolicy.cpp
new file mode 100644
index 0000000000..6494732cad
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgContentPolicy.cpp
@@ -0,0 +1,928 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgContentPolicy.h"
+#include "nsIMsgMailSession.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIAbManager.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgHdr.h"
+#include "nsIEncryptedSMIMEURIsSrvc.h"
+#include "nsNetUtil.h"
+#include "nsIMsgComposeService.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIWebNavigation.h"
+#include "nsContentPolicyUtils.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsFrameLoader.h"
+#include "nsMsgUtils.h"
+#include "nsThreadUtils.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "nsINntpUrl.h"
+#include "nsILoadInfo.h"
+#include "nsSandboxFlags.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsIObserverService.h"
+
+static const char kBlockRemoteImages[] =
+ "mailnews.message_display.disable_remote_image";
+static const char kTrustedDomains[] = "mail.trusteddomains";
+
+using namespace mozilla;
+using namespace mozilla::mailnews;
+
+// Per message headder flags to keep track of whether the user is allowing
+// remote content for a particular message. if you change or add more values to
+// these constants, be sure to modify the corresponding definitions in
+// mailWindowOverlay.js
+#define kNoRemoteContentPolicy 0
+#define kBlockRemoteContent 1
+#define kAllowRemoteContent 2
+
+NS_IMPL_ISUPPORTS(nsMsgContentPolicy, nsIContentPolicy, nsIMsgContentPolicy,
+ nsIObserver, nsISupportsWeakReference)
+
+nsMsgContentPolicy::nsMsgContentPolicy() { mBlockRemoteImages = true; }
+
+nsMsgContentPolicy::~nsMsgContentPolicy() {
+ // hey, we are going away...clean up after ourself....unregister our observer
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefInternal =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ prefInternal->RemoveObserver(kBlockRemoteImages, this);
+ }
+}
+
+nsresult nsMsgContentPolicy::Init() {
+ nsresult rv;
+
+ // register ourself as an observer on the mail preference to block remote
+ // images
+ nsCOMPtr<nsIPrefBranch> prefInternal =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ prefInternal->AddObserver(kBlockRemoteImages, this, true);
+
+ prefInternal->GetCharPref(kTrustedDomains, mTrustedMailDomains);
+ prefInternal->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);
+
+ // Grab a handle on the PermissionManager service for managing allowed remote
+ // content senders.
+ mPermissionManager = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * @returns true if the sender referenced by aMsgHdr is explicitly allowed to
+ * load remote images according to the PermissionManager
+ */
+bool nsMsgContentPolicy::ShouldAcceptRemoteContentForSender(
+ nsIMsgDBHdr* aMsgHdr) {
+ if (!aMsgHdr) return false;
+
+ // extract the e-mail address from the msg hdr
+ nsCString author;
+ nsresult rv = aMsgHdr->GetAuthor(getter_Copies(author));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCString emailAddress;
+ ExtractEmail(EncodedHeader(author), emailAddress);
+ if (emailAddress.IsEmpty()) return false;
+
+ nsCOMPtr<nsIIOService> ios =
+ do_GetService("@mozilla.org/network/io-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+ nsCOMPtr<nsIURI> mailURI;
+ emailAddress.InsertLiteral("chrome://messenger/content/email=", 0);
+ rv = ios->NewURI(emailAddress, nullptr, nullptr, getter_AddRefs(mailURI));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // check with permission manager
+ uint32_t permission = 0;
+ mozilla::OriginAttributes attrs;
+ RefPtr<mozilla::BasePrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(mailURI, attrs);
+ rv = mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns,
+ &permission);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Only return true if the permission manager has an explicit allow
+ return (permission == nsIPermissionManager::ALLOW_ACTION);
+}
+
+/**
+ * Extract the host name from aContentLocation, and look it up in our list
+ * of trusted domains.
+ */
+bool nsMsgContentPolicy::IsTrustedDomain(nsIURI* aContentLocation) {
+ bool trustedDomain = false;
+ // get the host name of the server hosting the remote image
+ nsAutoCString host;
+ nsresult rv = aContentLocation->GetHost(host);
+
+ if (NS_SUCCEEDED(rv) && !mTrustedMailDomains.IsEmpty())
+ trustedDomain = MsgHostDomainIsTrusted(host, mTrustedMailDomains);
+
+ return trustedDomain;
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeGuess,
+ int16_t* aDecision) {
+ nsresult rv = NS_OK;
+ ExtContentPolicyType aContentType = aLoadInfo->GetExternalContentPolicyType();
+ nsCOMPtr<nsISupports> aRequestingContext;
+ if (aContentType == ExtContentPolicy::TYPE_DOCUMENT)
+ aRequestingContext = aLoadInfo->ContextForTopLevelLoad();
+ else
+ aRequestingContext = aLoadInfo->LoadingNode();
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal();
+ nsCOMPtr<nsIURI> aRequestingLocation;
+ if (loadingPrincipal) {
+ BasePrincipal::Cast(loadingPrincipal)
+ ->GetURI(getter_AddRefs(aRequestingLocation));
+ }
+
+ // The default decision at the start of the function is to accept the load.
+ // Once we have checked the content type and the requesting location, then
+ // we switch it to reject.
+ //
+ // Be very careful about returning error codes - if this method returns an
+ // NS_ERROR_*, any decision made here will be ignored, and the document could
+ // be accepted when we don't want it to be.
+ //
+ // In most cases if an error occurs, its something we didn't expect so we
+ // should be rejecting the document anyway.
+ *aDecision = nsIContentPolicy::ACCEPT;
+
+ NS_ENSURE_ARG_POINTER(aContentLocation);
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "aContentType: %d\naContentLocation = %s\n", aContentType,
+ aContentLocation->GetSpecOrDefault().get());
+ fprintf(stderr, "aRequestingContext is %s\n",
+ aRequestingContext ? "not null" : "null");
+#endif
+
+#ifndef MOZ_THUNDERBIRD
+ // Go find out if we are dealing with mailnews. Anything else
+ // isn't our concern and we accept content.
+ nsCOMPtr<nsIDocShell> rootDocShell;
+ rv = GetRootDocShellForContext(aRequestingContext,
+ getter_AddRefs(rootDocShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We only want to deal with mailnews
+ if (rootDocShell->GetAppType() != nsIDocShell::APP_TYPE_MAIL) return NS_OK;
+#endif
+
+ switch (aContentType) {
+ // Plugins (nsIContentPolicy::TYPE_OBJECT) are blocked on document load.
+ case ExtContentPolicy::TYPE_DOCUMENT:
+ // At this point, we have no intention of supporting a different JS
+ // setting on a subdocument, so we don't worry about TYPE_SUBDOCUMENT
+ // here.
+
+ if (NS_IsMainThread()) {
+ rv = SetDisableItemsOnMailNewsUrlDocshells(aContentLocation, aLoadInfo);
+ } else {
+ auto SetDisabling = [&, location = nsCOMPtr(aContentLocation),
+ loadInfo = nsCOMPtr(aLoadInfo)]() -> auto {
+ rv = SetDisableItemsOnMailNewsUrlDocshells(location, loadInfo);
+ };
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("SetDisabling", SetDisabling);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ }
+ // if something went wrong during the tweaking, reject this content
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set disable items on docShells");
+ *aDecision = nsIContentPolicy::REJECT_TYPE;
+ return NS_OK;
+ }
+ break;
+
+ case ExtContentPolicy::TYPE_CSP_REPORT:
+ // We cannot block CSP reports.
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ break;
+
+ default:
+ break;
+ }
+
+ // NOTE: Not using NS_ENSURE_ARG_POINTER because this is a legitimate case
+ // that can happen. Also keep in mind that the default policy used for a
+ // failure code is ACCEPT.
+ if (!aRequestingLocation) return NS_ERROR_INVALID_POINTER;
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "aRequestingLocation = %s\n",
+ aRequestingLocation->GetSpecOrDefault().get());
+#endif
+
+ // If the requesting location is safe, accept the content location request.
+ if (IsSafeRequestingLocation(aRequestingLocation)) return rv;
+
+ // Now default to reject so early returns via NS_ENSURE_SUCCESS
+ // cause content to be rejected.
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+
+ // We want to establish the following:
+ // \--------\ requester | | |
+ // content \------------\ | | |
+ // requested \| mail message | news message | http(s)/data etc.
+ // -------------------------+---------------+--------------+------------------
+ // mail message content | load if same | don't load | don't load
+ // mailbox, imap, JsAccount | message (1) | (2) | (3)
+ // -------------------------+---------------+--------------+------------------
+ // news message | don't load (4)| load (5) | load (6)
+ // -------------------------+---------------+--------------+------------------
+ // http(s)/data, etc. | (default) | (default) | (default)
+ // -------------------------+---------------+--------------+------------------
+ nsCOMPtr<nsIMsgMessageUrl> contentURL(do_QueryInterface(aContentLocation));
+ if (contentURL) {
+ nsCOMPtr<nsINntpUrl> contentNntpURL(do_QueryInterface(aContentLocation));
+ if (!contentNntpURL) {
+ // Mail message (mailbox, imap or JsAccount) content requested, for
+ // example a message part, like an image: To load mail message content the
+ // requester must have the same "normalized" principal. This is basically
+ // a "same origin" test, it protects against cross-loading of mail message
+ // content from other mail or news messages.
+ nsCOMPtr<nsIMsgMessageUrl> requestURL(
+ do_QueryInterface(aRequestingLocation));
+ // If the request URL is not also a message URL, then we don't accept.
+ if (requestURL) {
+ nsCString contentPrincipalSpec, requestPrincipalSpec;
+ nsresult rv1 = contentURL->GetNormalizedSpec(contentPrincipalSpec);
+ nsresult rv2 = requestURL->GetNormalizedSpec(requestPrincipalSpec);
+ if (NS_SUCCEEDED(rv1) && NS_SUCCEEDED(rv2) &&
+ contentPrincipalSpec.Equals(requestPrincipalSpec))
+ *aDecision = nsIContentPolicy::ACCEPT; // (1)
+ }
+ return NS_OK; // (2) and (3)
+ }
+
+ // News message content requested. Don't accept request coming
+ // from a mail message since it would access the news server.
+ nsCOMPtr<nsIMsgMessageUrl> requestURL(
+ do_QueryInterface(aRequestingLocation));
+ if (requestURL) {
+ nsCOMPtr<nsINntpUrl> requestNntpURL(
+ do_QueryInterface(aRequestingLocation));
+ if (!requestNntpURL) return NS_OK; // (4)
+ }
+ *aDecision = nsIContentPolicy::ACCEPT; // (5) and (6)
+ return NS_OK;
+ }
+
+ // If exposed protocol not covered by the test above or protocol that has been
+ // specifically exposed by an add-on, or is a chrome url, then allow the load.
+ if (IsExposedProtocol(aContentLocation)) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // Never load unexposed protocols except for web protocols and file.
+ // Protocols like ftp are always blocked.
+ if (ShouldBlockUnexposedProtocol(aContentLocation)) return NS_OK;
+
+ // Mailnews URIs are not loaded in child processes, so I think that beyond
+ // here, if we're in a child process, the decision will always be accept.
+ //
+ // targetContext->Canonical does not work in a child process, so we can't
+ // really move on anyway.
+ if (!XRE_IsParentProcess()) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // Find out the URI that originally initiated the set of requests for this
+ // context.
+ RefPtr<mozilla::dom::BrowsingContext> targetContext;
+ rv = aLoadInfo->GetTargetBrowsingContext(getter_AddRefs(targetContext));
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (!targetContext) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> originatorLocation;
+ dom::CanonicalBrowsingContext* cbc = targetContext->Canonical();
+ if (cbc) {
+ dom::WindowGlobalParent* wgp = cbc->GetCurrentWindowGlobal();
+ if (wgp) {
+ originatorLocation = wgp->GetDocumentURI();
+ }
+ }
+ if (!originatorLocation) {
+ return NS_OK;
+ }
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "originatorLocation = %s\n",
+ originatorLocation->GetSpecOrDefault().get());
+#endif
+
+ // Don't load remote content for encrypted messages.
+ nsCOMPtr<nsIEncryptedSMIMEURIsService> encryptedURIService = do_GetService(
+ "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isEncrypted;
+ rv = encryptedURIService->IsEncrypted(aRequestingLocation->GetSpecOrDefault(),
+ &isEncrypted);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEncrypted) {
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ NotifyContentWasBlocked(targetContext->Id(), aContentLocation);
+ return NS_OK;
+ }
+
+ // If we are allowing all remote content...
+ if (!mBlockRemoteImages) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ uint32_t permission;
+ mozilla::OriginAttributes attrs;
+ RefPtr<mozilla::BasePrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(aContentLocation, attrs);
+ mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns,
+ &permission);
+ switch (permission) {
+ case nsIPermissionManager::UNKNOWN_ACTION: {
+ // No exception was found for this location.
+ break;
+ }
+ case nsIPermissionManager::ALLOW_ACTION: {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+ case nsIPermissionManager::DENY_ACTION: {
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ return NS_OK;
+ }
+ }
+
+ // Handle compose windows separately from mail. Work out if we're in a compose
+ // window or not.
+ nsCOMPtr<nsIMsgCompose> msgCompose =
+ GetMsgComposeForBrowsingContext(targetContext);
+ if (msgCompose) {
+ ComposeShouldLoad(msgCompose, aRequestingContext, originatorLocation,
+ aContentLocation, aDecision);
+ return NS_OK;
+ }
+
+ // Allow content when using a remote page.
+ bool isHttp;
+ bool isHttps;
+ rv = originatorLocation->SchemeIs("http", &isHttp);
+ nsresult rv2 = originatorLocation->SchemeIs("https", &isHttps);
+ if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2) && (isHttp || isHttps)) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // The default decision is still to reject.
+ ShouldAcceptContentForPotentialMsg(targetContext->Id(), aRequestingLocation,
+ aContentLocation, aDecision);
+ return NS_OK;
+}
+
+/**
+ * Determines if the requesting location is a safe one, i.e. its under the
+ * app/user's control - so file, about, chrome etc.
+ */
+bool nsMsgContentPolicy::IsSafeRequestingLocation(nsIURI* aRequestingLocation) {
+ if (!aRequestingLocation) return false;
+
+ // If aRequestingLocation is one of chrome, resource, file or view-source,
+ // allow aContentLocation to load.
+ bool isChrome;
+ bool isRes;
+ bool isFile;
+ bool isViewSource;
+
+ nsresult rv = aRequestingLocation->SchemeIs("chrome", &isChrome);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("resource", &isRes);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("file", &isFile);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("view-source", &isViewSource);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (isChrome || isRes || isFile || isViewSource) return true;
+
+ // Only allow about: to load anything if the requesting location is not the
+ // special about:blank one.
+ bool isAbout;
+ rv = aRequestingLocation->SchemeIs("about", &isAbout);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (!isAbout) return false;
+
+ nsCString fullSpec;
+ rv = aRequestingLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return !fullSpec.EqualsLiteral("about:blank");
+}
+
+/**
+ * Determines if the content location is a scheme that we're willing to expose
+ * for unlimited loading of content.
+ */
+bool nsMsgContentPolicy::IsExposedProtocol(nsIURI* aContentLocation) {
+ nsAutoCString contentScheme;
+ nsresult rv = aContentLocation->GetScheme(contentScheme);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Check some exposed protocols. Not all protocols in the list of
+ // network.protocol-handler.expose.* prefs in all-thunderbird.js are
+ // admitted purely based on their scheme.
+ // news, snews, nntp, imap and mailbox are checked before the call
+ // to this function by matching content location and requesting location.
+ if (contentScheme.LowerCaseEqualsLiteral("mailto")) return true;
+
+ if (contentScheme.LowerCaseEqualsLiteral("about")) {
+ // We want to allow about pages to load content freely. But not about:blank.
+ nsAutoCString fullSpec;
+ rv = aContentLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (fullSpec.EqualsLiteral("about:blank")) {
+ return false;
+ }
+ return true;
+ }
+
+ // check if customized exposed scheme
+ if (mCustomExposedProtocols.Contains(contentScheme)) return true;
+
+ bool isChrome;
+ rv = aContentLocation->SchemeIs("chrome", &isChrome);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isRes;
+ rv = aContentLocation->SchemeIs("resource", &isRes);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isData;
+ rv = aContentLocation->SchemeIs("data", &isData);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isMozExtension;
+ rv = aContentLocation->SchemeIs("moz-extension", &isMozExtension);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return isChrome || isRes || isData || isMozExtension;
+}
+
+/**
+ * We block most unexposed protocols that access remote data
+ * - apart from web protocols, and file.
+ */
+bool nsMsgContentPolicy::ShouldBlockUnexposedProtocol(
+ nsIURI* aContentLocation) {
+ // Error condition - we must return true so that we block.
+
+ // about:blank is "web", it should not be blocked.
+ nsAutoCString fullSpec;
+ nsresult rv = aContentLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, true);
+ if (fullSpec.EqualsLiteral("about:blank")) {
+ return false;
+ }
+
+ bool isHttp;
+ rv = aContentLocation->SchemeIs("http", &isHttp);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isHttps;
+ rv = aContentLocation->SchemeIs("https", &isHttps);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isWs; // websocket
+ rv = aContentLocation->SchemeIs("ws", &isWs);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isWss; // secure websocket
+ rv = aContentLocation->SchemeIs("wss", &isWss);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isBlob;
+ rv = aContentLocation->SchemeIs("blob", &isBlob);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isFile;
+ rv = aContentLocation->SchemeIs("file", &isFile);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ return !isHttp && !isHttps && !isWs && !isWss && !isBlob && !isFile;
+}
+
+/**
+ * The default for this function will be to reject the content request.
+ * When determining if to allow the request for a given msg hdr, the function
+ * will go through the list of remote content blocking criteria:
+ *
+ * #1 Allow if there is a db header for a manual override.
+ * #2 Allow if the message is in an RSS folder.
+ * #3 Allow if the domain for the remote image in our white list.
+ * #4 Allow if the author has been specifically white listed.
+ */
+int16_t nsMsgContentPolicy::ShouldAcceptRemoteContentForMsgHdr(
+ nsIMsgDBHdr* aMsgHdr, nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation) {
+ if (!aMsgHdr) return static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);
+
+ // Case #1, check the db hdr for the remote content policy on this particular
+ // message.
+ uint32_t remoteContentPolicy = kNoRemoteContentPolicy;
+ aMsgHdr->GetUint32Property("remoteContentPolicy", &remoteContentPolicy);
+
+ // Case #2, check if the message is in an RSS folder
+ bool isRSS = false;
+ IsRSSArticle(aRequestingLocation, &isRSS);
+
+ // Case #3, the domain for the remote image is in our white list
+ bool trustedDomain = IsTrustedDomain(aContentLocation);
+
+ // Case 4 means looking up items in the permissions database. So if
+ // either of the two previous items means we load the data, just do it.
+ if (isRSS || remoteContentPolicy == kAllowRemoteContent || trustedDomain)
+ return nsIContentPolicy::ACCEPT;
+
+ // Case #4, author is in our white list..
+ bool allowForSender = ShouldAcceptRemoteContentForSender(aMsgHdr);
+
+ int16_t result = allowForSender
+ ? static_cast<int16_t>(nsIContentPolicy::ACCEPT)
+ : static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);
+
+ // kNoRemoteContentPolicy means we have never set a value on the message
+ if (result == nsIContentPolicy::REJECT_REQUEST && !remoteContentPolicy)
+ aMsgHdr->SetUint32Property("remoteContentPolicy", kBlockRemoteContent);
+
+ return result;
+}
+
+class RemoteContentNotifierEvent : public mozilla::Runnable {
+ public:
+ RemoteContentNotifierEvent(uint64_t aBrowsingContextId, nsIURI* aContentURI)
+ : mozilla::Runnable("RemoteContentNotifierEvent"),
+ mBrowsingContextId(aBrowsingContextId),
+ mContentURI(aContentURI) {}
+
+ NS_IMETHOD Run() {
+ nsAutoString data;
+ data.AppendInt(mBrowsingContextId);
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(mContentURI, "remote-content-blocked",
+ data.get());
+ return NS_OK;
+ }
+
+ private:
+ uint64_t mBrowsingContextId;
+ nsCOMPtr<nsIURI> mContentURI;
+};
+
+/**
+ * This function is used to show a blocked remote content notification.
+ */
+void nsMsgContentPolicy::NotifyContentWasBlocked(uint64_t aBrowsingContextId,
+ nsIURI* aContentLocation) {
+ // Post this as an event because it can cause dom mutations, and we
+ // get called at a bad time to be causing dom mutations.
+ NS_DispatchToCurrentThread(
+ new RemoteContentNotifierEvent(aBrowsingContextId, aContentLocation));
+}
+
+/**
+ * This function is used to determine if we allow content for a remote message.
+ * If we reject loading remote content, then we'll inform the message window
+ * that this message has remote content (and hence we are not loading it).
+ *
+ * See ShouldAcceptRemoteContentForMsgHdr for the actual decisions that
+ * determine if we are going to allow remote content.
+ */
+void nsMsgContentPolicy::ShouldAcceptContentForPotentialMsg(
+ uint64_t aBrowsingContextId, nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation, int16_t* aDecision) {
+ NS_ASSERTION(
+ *aDecision == nsIContentPolicy::REJECT_REQUEST,
+ "AllowContentForPotentialMessage expects default decision to be reject!");
+
+ // Is it a mailnews url?
+ nsresult rv;
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl(
+ do_QueryInterface(aRequestingLocation, &rv));
+ if (NS_FAILED(rv)) {
+ // It isn't a mailnews url - so we accept the load here, and let other
+ // content policies make the decision if we should be loading it or not.
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return;
+ }
+
+ nsCString resourceURI;
+ rv = msgUrl->GetUri(resourceURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(resourceURI, getter_AddRefs(msgHdr));
+
+ // Get a decision on whether or not to allow remote content for this message
+ // header.
+ *aDecision = ShouldAcceptRemoteContentForMsgHdr(msgHdr, aRequestingLocation,
+ aContentLocation);
+
+ // If we're not allowing the remote content, tell the nsIMsgWindow loading
+ // this url that this is the case, so that the UI knows to show the remote
+ // content header bar, so the user can override if they wish.
+ if (*aDecision == nsIContentPolicy::REJECT_REQUEST) {
+ NotifyContentWasBlocked(aBrowsingContextId, aContentLocation);
+ }
+}
+
+/**
+ * Content policy logic for compose windows
+ */
+void nsMsgContentPolicy::ComposeShouldLoad(nsIMsgCompose* aMsgCompose,
+ nsISupports* aRequestingContext,
+ nsIURI* aOriginatorLocation,
+ nsIURI* aContentLocation,
+ int16_t* aDecision) {
+ NS_ASSERTION(*aDecision == nsIContentPolicy::REJECT_REQUEST,
+ "ComposeShouldLoad expects default decision to be reject!");
+
+ nsCString originalMsgURI;
+ nsresult rv = aMsgCompose->GetOriginalMsgURI(originalMsgURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (!originalMsgURI.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(originalMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ *aDecision =
+ ShouldAcceptRemoteContentForMsgHdr(msgHdr, nullptr, aContentLocation);
+
+ if (!aOriginatorLocation->GetSpecOrDefault().EqualsLiteral(
+ "about:blank?compose")) {
+ return;
+ }
+ }
+
+ // We want to allow the user to add remote content, but do that only when
+ // the allowRemoteContent was set. This way quoted remoted content won't
+ // automatically load, but e.g. pasted content will load because the UI
+ // code toggles the flag.
+ nsCOMPtr<mozilla::dom::Element> element =
+ do_QueryInterface(aRequestingContext);
+ RefPtr<mozilla::dom::HTMLImageElement> image =
+ mozilla::dom::HTMLImageElement::FromNodeOrNull(element);
+ if (image) {
+ // Special case image elements.
+ bool allowRemoteContent = false;
+ aMsgCompose->GetAllowRemoteContent(&allowRemoteContent);
+ if (allowRemoteContent) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return;
+ }
+ }
+}
+
+already_AddRefed<nsIMsgCompose>
+nsMsgContentPolicy::GetMsgComposeForBrowsingContext(
+ mozilla::dom::BrowsingContext* aBrowsingContext) {
+ nsresult rv;
+
+ nsIDocShell* shell = aBrowsingContext->GetDocShell();
+ if (!shell) return nullptr;
+ nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docShellTreeItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootItem, &rv));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgComposeService> composeService(
+ do_GetService("@mozilla.org/messengercompose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgCompose> msgCompose;
+ // Don't bother checking rv, as GetMsgComposeForDocShell returns
+ // NS_ERROR_FAILURE for not found.
+ composeService->GetMsgComposeForDocShell(docShell,
+ getter_AddRefs(msgCompose));
+ return msgCompose.forget();
+}
+
+nsresult nsMsgContentPolicy::SetDisableItemsOnMailNewsUrlDocshells(
+ nsIURI* aContentLocation, nsILoadInfo* aLoadInfo) {
+ // XXX if this class changes so that this method can be called from
+ // ShouldProcess, and if it's possible for this to be null when called from
+ // ShouldLoad, but not in the corresponding ShouldProcess call,
+ // we need to re-think the assumptions underlying this code.
+
+ NS_ENSURE_ARG_POINTER(aContentLocation);
+ NS_ENSURE_ARG_POINTER(aLoadInfo);
+
+ RefPtr<mozilla::dom::BrowsingContext> browsingContext =
+ aLoadInfo->GetTargetBrowsingContext();
+ if (!browsingContext) {
+ return NS_OK;
+ }
+
+ // We're only worried about policy settings in content docshells.
+ if (!browsingContext->IsContent()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = browsingContext->GetDocShell();
+ if (!docShell) {
+ // If there's no docshell to get to, there's nowhere for the JavaScript to
+ // run, so we're already safe and don't need to disable anything.
+ return NS_OK;
+ }
+
+ // Ensure starting off unsandboxed. We sandbox later if needed.
+ MOZ_ALWAYS_SUCCEEDS(browsingContext->SetSandboxFlags(SANDBOXED_NONE));
+
+ nsresult rv;
+ bool isAllowedContent = !ShouldBlockUnexposedProtocol(aContentLocation);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aContentLocation);
+ if (!msgUrl && !isAllowedContent) {
+ // If it's not a mailnews url or allowed content url (http[s]|file) then
+ // bail; otherwise set whether JavaScript is allowed.
+ return NS_OK;
+ }
+
+ if (!isAllowedContent) {
+ // Disable JavaScript on message URLs.
+ rv = browsingContext->SetAllowJavascript(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = browsingContext->SetAllowContentRetargetingOnChildren(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // NOTE! Do not set single sandboxing flags only. Sandboxing - when used -
+ // starts off with all things sandboxed, and individual sandbox keywords
+ // will *allow* the specific feature.
+ // Disabling by setting single flags without starting off with all things
+ // sandboxed would the normal assumptions about sandboxing.
+ // The flags - contrary to the keywords - *prevent* a given feature.
+ uint32_t sandboxFlags = SANDBOX_ALL_FLAGS;
+
+ // Do not block links with target attribute from opening (at all).
+ // xref bug 421310 - we would like to prevent using target, but *handle*
+ // links like the target wasn't there.
+ sandboxFlags &= ~SANDBOXED_AUXILIARY_NAVIGATION;
+
+ // For some unexplicable reason, when SANDBOXED_ORIGIN is in affect, then
+ // images will not work with test --verify. So unset it.
+ sandboxFlags &= ~SANDBOXED_ORIGIN;
+
+ // Having both SANDBOXED_TOPLEVEL_NAVIGATION and
+ // SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION will generate a warning,
+ // see BothAllowTopNavigationAndUserActivationPresent. So unset it.
+ sandboxFlags &= ~SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION;
+
+ rv = browsingContext->SetSandboxFlags(sandboxFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // JavaScript is allowed on non-message URLs.
+ rv = browsingContext->SetAllowJavascript(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = browsingContext->SetAllowContentRetargetingOnChildren(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = docShell->SetAllowPlugins(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * Gets the root docshell from a requesting context.
+ */
+nsresult nsMsgContentPolicy::GetRootDocShellForContext(
+ nsISupports* aRequestingContext, nsIDocShell** aDocShell) {
+ NS_ENSURE_ARG_POINTER(aRequestingContext);
+ nsresult rv;
+
+ nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext);
+ NS_ENSURE_TRUE(shell, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docshellTreeItem->GetInProcessRootTreeItem(getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(rootItem, aDocShell);
+}
+
+/**
+ * Gets the originating URI that started off a set of requests, accounting
+ * for multiple iframes.
+ *
+ * Navigates up the docshell tree from aRequestingContext and finds the
+ * highest parent with the same type docshell as aRequestingContext, then
+ * returns the URI associated with that docshell.
+ */
+nsresult nsMsgContentPolicy::GetOriginatingURIForContext(
+ nsISupports* aRequestingContext, nsIURI** aURI) {
+ NS_ENSURE_ARG_POINTER(aRequestingContext);
+ nsresult rv;
+
+ nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext);
+ if (!shell) {
+ *aURI = nullptr;
+ return NS_OK;
+ }
+ nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docshellTreeItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIWebNavigation> webNavigation(do_QueryInterface(rootItem, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return webNavigation->GetCurrentURI(aURI);
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::ShouldProcess(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeGuess,
+ int16_t* aDecision) {
+ // XXX Returning ACCEPT is presumably only a reasonable thing to do if we
+ // think that ShouldLoad is going to catch all possible cases (i.e. that
+ // everything we use to make decisions is going to be available at
+ // ShouldLoad time, and not only become available in time for ShouldProcess).
+ // Do we think that's actually the case?
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgContentPolicy::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic)) {
+ NS_LossyConvertUTF16toASCII pref(aData);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIPrefBranch> prefBranchInt = do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (pref.Equals(kBlockRemoteImages))
+ prefBranchInt->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Implementation of nsIMsgContentPolicy
+ *
+ */
+NS_IMETHODIMP
+nsMsgContentPolicy::AddExposedProtocol(const nsACString& aScheme) {
+ if (mCustomExposedProtocols.Contains(nsCString(aScheme))) return NS_OK;
+
+ mCustomExposedProtocols.AppendElement(aScheme);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::RemoveExposedProtocol(const nsACString& aScheme) {
+ mCustomExposedProtocols.RemoveElement(nsCString(aScheme));
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.h b/comm/mailnews/base/src/nsMsgContentPolicy.h
new file mode 100644
index 0000000000..e6d3d10e79
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgContentPolicy.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/******************************************************************************
+ * nsMsgContentPolicy enforces the specified content policy on images, js,
+ * plugins, etc. This is the class used to determine what elements in a message
+ * should be loaded.
+ *
+ * nsMsgCookiePolicy enforces our cookie policy for mail and RSS messages.
+ ******************************************************************************/
+
+#ifndef _nsMsgContentPolicy_H_
+#define _nsMsgContentPolicy_H_
+
+#include "nsIContentPolicy.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsString.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgCompose.h"
+#include "nsIDocShell.h"
+#include "nsIPermissionManager.h"
+#include "nsIMsgContentPolicy.h"
+#include "nsTArray.h"
+
+/* DBFCFDF0-4489-4faa-8122-190FD1EFA16C */
+#define NS_MSGCONTENTPOLICY_CID \
+ { \
+ 0xdbfcfdf0, 0x4489, 0x4faa, { \
+ 0x81, 0x22, 0x19, 0xf, 0xd1, 0xef, 0xa1, 0x6c \
+ } \
+ }
+
+#define NS_MSGCONTENTPOLICY_CONTRACTID "@mozilla.org/messenger/content-policy;1"
+
+class nsIMsgDBHdr;
+class nsIDocShell;
+
+class nsMsgContentPolicy : public nsIContentPolicy,
+ public nsIObserver,
+ public nsIMsgContentPolicy,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgContentPolicy();
+
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPOLICY
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMSGCONTENTPOLICY
+
+ protected:
+ virtual ~nsMsgContentPolicy();
+
+ bool mBlockRemoteImages;
+ nsCString mTrustedMailDomains;
+ nsCOMPtr<nsIPermissionManager> mPermissionManager;
+
+ bool IsTrustedDomain(nsIURI* aContentLocation);
+ bool IsSafeRequestingLocation(nsIURI* aRequestingLocation);
+ bool IsExposedProtocol(nsIURI* aContentLocation);
+ bool IsExposedChromeProtocol(nsIURI* aContentLocation);
+ bool ShouldBlockUnexposedProtocol(nsIURI* aContentLocation);
+
+ bool ShouldAcceptRemoteContentForSender(nsIMsgDBHdr* aMsgHdr);
+ int16_t ShouldAcceptRemoteContentForMsgHdr(nsIMsgDBHdr* aMsgHdr,
+ nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation);
+ void NotifyContentWasBlocked(uint64_t aBrowsingContextId,
+ nsIURI* aContentLocation);
+ void ShouldAcceptContentForPotentialMsg(uint64_t aBrowsingContextId,
+ nsIURI* aOriginatorLocation,
+ nsIURI* aContentLocation,
+ int16_t* aDecision);
+ void ComposeShouldLoad(nsIMsgCompose* aMsgCompose,
+ nsISupports* aRequestingContext,
+ nsIURI* aOriginatorLocation, nsIURI* aContentLocation,
+ int16_t* aDecision);
+ already_AddRefed<nsIMsgCompose> GetMsgComposeForBrowsingContext(
+ mozilla::dom::BrowsingContext* aRequestingContext);
+
+ nsresult GetRootDocShellForContext(nsISupports* aRequestingContext,
+ nsIDocShell** aDocShell);
+ nsresult GetOriginatingURIForContext(nsISupports* aRequestingContext,
+ nsIURI** aURI);
+ nsresult SetDisableItemsOnMailNewsUrlDocshells(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo);
+
+ nsTArray<nsCString> mCustomExposedProtocols;
+};
+
+#endif // _nsMsgContentPolicy_H_
diff --git a/comm/mailnews/base/src/nsMsgCopyService.cpp b/comm/mailnews/base/src/nsMsgCopyService.cpp
new file mode 100644
index 0000000000..22df6bb6e9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCopyService.cpp
@@ -0,0 +1,587 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgCopyService.h"
+#include "nsCOMArray.h"
+#include "nspr.h"
+#include "nsIFile.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule gCopyServiceLog("MsgCopyService");
+
+// ******************** nsCopySource ******************
+
+nsCopySource::nsCopySource() : m_processed(false) {
+ MOZ_COUNT_CTOR(nsCopySource);
+}
+
+nsCopySource::nsCopySource(nsIMsgFolder* srcFolder) : m_processed(false) {
+ MOZ_COUNT_CTOR(nsCopySource);
+ m_msgFolder = srcFolder;
+}
+
+nsCopySource::~nsCopySource() { MOZ_COUNT_DTOR(nsCopySource); }
+
+void nsCopySource::AddMessage(nsIMsgDBHdr* aMsg) {
+ m_messageArray.AppendElement(aMsg);
+}
+
+// ************ nsCopyRequest *****************
+//
+
+nsCopyRequest::nsCopyRequest()
+ : m_requestType(nsCopyMessagesType),
+ m_isMoveOrDraftOrTemplate(false),
+ m_processed(false),
+ m_newMsgFlags(0) {
+ MOZ_COUNT_CTOR(nsCopyRequest);
+}
+
+nsCopyRequest::~nsCopyRequest() {
+ MOZ_COUNT_DTOR(nsCopyRequest);
+
+ int32_t j = m_copySourceArray.Length();
+ while (j-- > 0) delete m_copySourceArray.ElementAt(j);
+}
+
+nsresult nsCopyRequest::Init(nsCopyRequestType type, nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, bool bVal,
+ uint32_t newMsgFlags,
+ const nsACString& newMsgKeywords,
+ nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* msgWindow, bool allowUndo) {
+ nsresult rv = NS_OK;
+ m_requestType = type;
+ m_srcSupport = aSupport;
+ m_dstFolder = dstFolder;
+ m_isMoveOrDraftOrTemplate = bVal;
+ m_allowUndo = allowUndo;
+ m_newMsgFlags = newMsgFlags;
+ m_newMsgKeywords = newMsgKeywords;
+
+ if (listener) m_listener = listener;
+ if (msgWindow) {
+ m_msgWindow = msgWindow;
+ if (m_allowUndo) msgWindow->GetTransactionManager(getter_AddRefs(m_txnMgr));
+ }
+ if (type == nsCopyFoldersType) {
+ // To support multiple copy folder operations to the same destination, we
+ // need to save the leaf name of the src file spec so that FindRequest() is
+ // able to find the right request when copy finishes.
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryInterface(aSupport, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString folderName;
+ rv = srcFolder->GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_dstFolderName = folderName;
+ }
+
+ return rv;
+}
+
+nsCopySource* nsCopyRequest::AddNewCopySource(nsIMsgFolder* srcFolder) {
+ nsCopySource* newSrc = new nsCopySource(srcFolder);
+ if (newSrc) {
+ m_copySourceArray.AppendElement(newSrc);
+ if (srcFolder == m_dstFolder) newSrc->m_processed = true;
+ }
+ return newSrc;
+}
+
+// ************* nsMsgCopyService ****************
+//
+
+nsMsgCopyService::nsMsgCopyService() {}
+
+nsMsgCopyService::~nsMsgCopyService() {
+ int32_t i = m_copyRequests.Length();
+
+ while (i-- > 0) ClearRequest(m_copyRequests.ElementAt(i), NS_ERROR_FAILURE);
+}
+
+void nsMsgCopyService::LogCopyCompletion(nsISupports* aSrc,
+ nsIMsgFolder* aDest) {
+ nsCString srcFolderUri, destFolderUri;
+ nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aSrc));
+ if (srcFolder) srcFolder->GetURI(srcFolderUri);
+ aDest->GetURI(destFolderUri);
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info,
+ ("NotifyCompletion - src %s dest %s\n", srcFolderUri.get(),
+ destFolderUri.get()));
+}
+
+void nsMsgCopyService::LogCopyRequest(const char* logMsg,
+ nsCopyRequest* aRequest) {
+ nsCString srcFolderUri, destFolderUri;
+ nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aRequest->m_srcSupport));
+ if (srcFolder) srcFolder->GetURI(srcFolderUri);
+ aRequest->m_dstFolder->GetURI(destFolderUri);
+ uint32_t numMsgs = 0;
+ if (aRequest->m_requestType == nsCopyMessagesType &&
+ aRequest->m_copySourceArray.Length() > 0) {
+ numMsgs = aRequest->m_copySourceArray[0]->m_messageArray.Length();
+ }
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info,
+ ("request %p %s - src %s dest %s numItems %d type=%d", aRequest,
+ logMsg, srcFolderUri.get(), destFolderUri.get(), numMsgs,
+ aRequest->m_requestType));
+}
+
+nsresult nsMsgCopyService::ClearRequest(nsCopyRequest* aRequest, nsresult rv) {
+ if (aRequest) {
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest(
+ NS_SUCCEEDED(rv) ? "Clearing OK request" : "Clearing failed request",
+ aRequest);
+
+ if (NS_SUCCEEDED(rv) && aRequest->m_requestType == nsCopyFoldersType) {
+ // Send folder copy/move notifications to nsIMsgFolderListeners.
+ // BAD SMELL ALERT: Seems odd that this is the only place the folder
+ // notification is invoked from the copyService.
+ // For message copy/move operations, the folder code handles the
+ // notification (to take one example).
+ // This suggests lack of clarity of responsibility.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ for (nsCopySource* copySource : aRequest->m_copySourceArray) {
+ notifier->NotifyFolderMoveCopyCompleted(
+ aRequest->m_isMoveOrDraftOrTemplate, copySource->m_msgFolder,
+ aRequest->m_dstFolder);
+ }
+ }
+ }
+
+ // undo stuff
+ if (aRequest->m_allowUndo && aRequest->m_copySourceArray.Length() > 1 &&
+ aRequest->m_txnMgr)
+ aRequest->m_txnMgr->EndBatch(false);
+
+ m_copyRequests.RemoveElement(aRequest);
+ if (aRequest->m_listener) aRequest->m_listener->OnStopCopy(rv);
+ delete aRequest;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgCopyService::QueueRequest(nsCopyRequest* aRequest,
+ bool* aCopyImmediately) {
+ NS_ENSURE_ARG_POINTER(aRequest);
+ NS_ENSURE_ARG_POINTER(aCopyImmediately);
+ *aCopyImmediately = true;
+ nsCopyRequest* copyRequest;
+
+ // Check through previous requests to see if the copy can start immediately.
+ uint32_t cnt = m_copyRequests.Length();
+
+ for (uint32_t i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ if (aRequest->m_requestType == nsCopyFoldersType) {
+ // For copy folder, see if both destination folder (root)
+ // (ie, Local Folder) and folder name (ie, abc) are the same.
+ if (copyRequest->m_dstFolderName == aRequest->m_dstFolderName &&
+ SameCOMIdentity(copyRequest->m_dstFolder, aRequest->m_dstFolder)) {
+ *aCopyImmediately = false;
+ break;
+ }
+ } else if (SameCOMIdentity(copyRequest->m_dstFolder,
+ aRequest->m_dstFolder)) {
+ // If dst are same and we already have a request, we cannot copy
+ // immediately.
+ *aCopyImmediately = false;
+ break;
+ }
+ }
+
+ // Queue it.
+ m_copyRequests.AppendElement(aRequest);
+ return NS_OK;
+}
+
+nsresult nsMsgCopyService::DoCopy(nsCopyRequest* aRequest) {
+ NS_ENSURE_ARG(aRequest);
+ bool copyImmediately;
+ QueueRequest(aRequest, &copyImmediately);
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest(copyImmediately ? "DoCopy" : "QueueRequest", aRequest);
+
+ // if no active request for this dest folder then we can copy immediately
+ if (copyImmediately) return DoNextCopy();
+
+ return NS_OK;
+}
+
+nsresult nsMsgCopyService::DoNextCopy() {
+ nsresult rv = NS_OK;
+ nsCopyRequest* copyRequest = nullptr;
+ nsCopySource* copySource = nullptr;
+ uint32_t i, j, scnt;
+
+ uint32_t cnt = m_copyRequests.Length();
+ if (cnt > 0) {
+ nsCOMArray<nsIMsgFolder> activeTargets;
+
+ // ** jt -- always FIFO
+ for (i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ copySource = nullptr;
+ scnt = copyRequest->m_copySourceArray.Length();
+ if (!copyRequest->m_processed) {
+ // if the target folder of this request already has an active
+ // copy request, skip this request for now.
+ if (activeTargets.ContainsObject(copyRequest->m_dstFolder)) {
+ copyRequest = nullptr;
+ continue;
+ }
+ if (scnt <= 0) goto found; // must be CopyFileMessage
+ for (j = 0; j < scnt; j++) {
+ copySource = copyRequest->m_copySourceArray.ElementAt(j);
+ if (!copySource->m_processed) goto found;
+ }
+ if (j >= scnt) // all processed set the value
+ copyRequest->m_processed = true;
+ }
+ if (copyRequest->m_processed) {
+ // Keep track of folders actively getting copied to.
+ activeTargets.AppendObject(copyRequest->m_dstFolder);
+ }
+ }
+ found:
+ if (copyRequest && !copyRequest->m_processed) {
+ if (copyRequest->m_listener) copyRequest->m_listener->OnStartCopy();
+ if (copyRequest->m_requestType == nsCopyMessagesType && copySource) {
+ copySource->m_processed = true;
+ rv = copyRequest->m_dstFolder->CopyMessages(
+ copySource->m_msgFolder, copySource->m_messageArray,
+ copyRequest->m_isMoveOrDraftOrTemplate, copyRequest->m_msgWindow,
+ copyRequest->m_listener, false,
+ copyRequest->m_allowUndo); // isFolder operation false
+
+ } else if (copyRequest->m_requestType == nsCopyFoldersType) {
+ NS_ENSURE_STATE(copySource);
+ copySource->m_processed = true;
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = copyRequest->m_dstFolder;
+ nsCOMPtr<nsIMsgFolder> srcFolder = copySource->m_msgFolder;
+
+ // If folder transfer is not within the same server and if a folder
+ // move was requested, set the request move flag false to avoid
+ // removing the list of marked deleted messages in the source folder.
+ bool isMove = copyRequest->m_isMoveOrDraftOrTemplate;
+ if (copyRequest->m_isMoveOrDraftOrTemplate) {
+ bool sameServer;
+ IsOnSameServer(dstFolder, srcFolder, &sameServer);
+ if (!sameServer) copyRequest->m_isMoveOrDraftOrTemplate = false;
+ }
+
+ // NOTE: The folder invokes NotifyCompletion() when the operation is
+ // complete. Some folders (localfolder!) invoke it before CopyFolder()
+ // even returns. This will likely delete the request object, so
+ // you have to assume that copyRequest is invalid when CopyFolder()
+ // returns.
+ rv = dstFolder->CopyFolder(srcFolder, isMove, copyRequest->m_msgWindow,
+ copyRequest->m_listener);
+ // If CopyFolder() fails (e.g. destination folder already exists),
+ // it won't send a completion notification (NotifyCompletion()).
+ // So copyRequest will still exist, and we need to ditch it.
+ if (NS_FAILED(rv)) {
+ ClearRequest(copyRequest, rv);
+ }
+ } else if (copyRequest->m_requestType == nsCopyFileMessageType) {
+ nsCOMPtr<nsIFile> aFile(
+ do_QueryInterface(copyRequest->m_srcSupport, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ // ** in case of saving draft/template; the very first
+ // time we may not have the original message to replace
+ // with; if we do we shall have an instance of copySource
+ nsCOMPtr<nsIMsgDBHdr> aMessage;
+ if (copySource) {
+ aMessage = copySource->m_messageArray[0];
+ copySource->m_processed = true;
+ }
+ copyRequest->m_processed = true;
+ rv = copyRequest->m_dstFolder->CopyFileMessage(
+ aFile, aMessage, copyRequest->m_isMoveOrDraftOrTemplate,
+ copyRequest->m_newMsgFlags, copyRequest->m_newMsgKeywords,
+ copyRequest->m_msgWindow, copyRequest->m_listener);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+/**
+ * Find a request in m_copyRequests which matches the passed in source
+ * and destination folders.
+ *
+ * @param aSupport the iSupports of the source folder.
+ * @param dstFolder the destination folder of the copy request.
+ */
+nsCopyRequest* nsMsgCopyService::FindRequest(nsISupports* aSupport,
+ nsIMsgFolder* dstFolder) {
+ nsCopyRequest* copyRequest = nullptr;
+ uint32_t cnt = m_copyRequests.Length();
+ for (uint32_t i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ if (SameCOMIdentity(copyRequest->m_srcSupport, aSupport) &&
+ SameCOMIdentity(copyRequest->m_dstFolder.get(), dstFolder))
+ break;
+
+ // When copying folders the notification of the message copy serves as a
+ // proxy for the folder copy. Check for that here.
+ if (copyRequest->m_requestType == nsCopyFoldersType) {
+ // If the src is different then check next request.
+ if (!SameCOMIdentity(copyRequest->m_srcSupport, aSupport)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ // See if the parent of the copied folder is the same as the one when the
+ // request was made. Note if the destination folder is already a server
+ // folder then no need to get parent.
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ nsresult rv = NS_OK;
+ bool isServer = false;
+ dstFolder->GetIsServer(&isServer);
+ if (!isServer) rv = dstFolder->GetParent(getter_AddRefs(parentMsgFolder));
+ if ((NS_FAILED(rv)) || (!parentMsgFolder && !isServer) ||
+ (copyRequest->m_dstFolder.get() != parentMsgFolder)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ // Now checks if the folder name is the same.
+ nsString folderName;
+ rv = dstFolder->GetName(folderName);
+ if (NS_FAILED(rv)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ if (copyRequest->m_dstFolderName == folderName) break;
+ } else
+ copyRequest = nullptr;
+ }
+
+ return copyRequest;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgCopyService, nsIMsgCopyService)
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsMsgCopyService::CopyMessages(
+ nsIMsgFolder* srcFolder, /* UI src folder */
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgFolder* dstFolder,
+ bool isMove, nsIMsgCopyServiceListener* listener, nsIMsgWindow* window,
+ bool allowUndo) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Debug, ("CopyMessages"));
+
+ if (srcFolder == dstFolder) {
+ NS_ERROR("src and dest folders for msg copy can't be the same");
+ return NS_ERROR_FAILURE;
+ }
+ nsCopyRequest* copyRequest;
+ nsCopySource* copySource = nullptr;
+ nsIMsgDBHdr* msg;
+ nsCOMPtr<nsIMsgFolder> curFolder;
+ nsCOMPtr<nsISupports> aSupport;
+ int cnt;
+ nsresult rv;
+
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // make sure dest folder exists
+ // and has proper flags, before we start copying?
+
+ // bail early if nothing to do
+ if (messages.IsEmpty()) {
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+ return NS_OK;
+ }
+
+ copyRequest = new nsCopyRequest();
+ if (!copyRequest) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> unprocessed = messages.Clone();
+ aSupport = srcFolder;
+
+ rv = copyRequest->Init(nsCopyMessagesType, aSupport, dstFolder, isMove,
+ 0 /* new msg flags, not used */, EmptyCString(),
+ listener, window, allowUndo);
+ if (NS_FAILED(rv)) goto done;
+
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest("CopyMessages request", copyRequest);
+
+ // Build up multiple nsCopySource objects. Each holds a single source folder
+ // and all the messages in the folder that are to be copied.
+ cnt = unprocessed.Length();
+ while (cnt-- > 0) {
+ msg = unprocessed[cnt];
+ rv = msg->GetFolder(getter_AddRefs(curFolder));
+
+ if (NS_FAILED(rv)) goto done;
+ if (!copySource) {
+ // Begin a folder grouping.
+ copySource = copyRequest->AddNewCopySource(curFolder);
+ if (!copySource) {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto done;
+ }
+ }
+
+ // Stash message if in the current folder grouping.
+ if (curFolder == copySource->m_msgFolder) {
+ copySource->AddMessage(msg);
+ unprocessed.RemoveElementAt((size_t)cnt);
+ }
+
+ if (cnt == 0) {
+ // Finished a folder. Start a new pass to handle any remaining messages
+ // in other folders.
+ cnt = unprocessed.Length();
+ if (cnt > 0) {
+ // Force to create a new one and continue grouping the messages.
+ copySource = nullptr;
+ }
+ }
+ }
+
+ // undo stuff
+ if (NS_SUCCEEDED(rv) && copyRequest->m_allowUndo &&
+ copyRequest->m_copySourceArray.Length() > 1 && copyRequest->m_txnMgr) {
+ nsCOMPtr<nsITransactionManager> txnMgr = copyRequest->m_txnMgr;
+ txnMgr->BeginBatch(nullptr);
+ }
+
+done:
+
+ if (NS_FAILED(rv))
+ delete copyRequest;
+ else
+ rv = DoCopy(copyRequest);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::CopyFolder(nsIMsgFolder* srcFolder, nsIMsgFolder* dstFolder,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* window) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+ nsCopyRequest* copyRequest;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> curFolder;
+
+ copyRequest = new nsCopyRequest();
+ rv = copyRequest->Init(nsCopyFoldersType, srcFolder, dstFolder, isMove,
+ 0 /* new msg flags, not used */, EmptyCString(),
+ listener, window, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ copyRequest->AddNewCopySource(srcFolder);
+ return DoCopy(copyRequest);
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::CopyFileMessage(nsIFile* file, nsIMsgFolder* dstFolder,
+ nsIMsgDBHdr* msgToReplace, bool isDraft,
+ uint32_t aMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* window) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ nsCopyRequest* copyRequest;
+ nsCopySource* copySource = nullptr;
+
+ NS_ENSURE_ARG_POINTER(file);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+
+ copyRequest = new nsCopyRequest();
+ if (!copyRequest) return rv;
+
+ rv = copyRequest->Init(nsCopyFileMessageType, file, dstFolder, isDraft,
+ aMsgFlags, aNewMsgKeywords, listener, window, false);
+ if (NS_FAILED(rv)) goto done;
+
+ if (msgToReplace) {
+ // The actual source of the message is a file not a folder, but
+ // we still need an nsCopySource to reference the old message header
+ // which will be used to recover message metadata.
+ copySource = copyRequest->AddNewCopySource(nullptr);
+ if (!copySource) {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto done;
+ }
+ copySource->AddMessage(msgToReplace);
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ delete copyRequest;
+ } else {
+ rv = DoCopy(copyRequest);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::NotifyCompletion(nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, nsresult result) {
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyCompletion(aSupport, dstFolder);
+ nsCopyRequest* copyRequest = nullptr;
+ uint32_t numOrigRequests = m_copyRequests.Length();
+ do {
+ // loop for copy requests, because if we do a cross server folder copy,
+ // we'll have a copy request for the folder copy, which will in turn
+ // generate a copy request for the messages in the folder, which
+ // will have the same src support.
+ copyRequest = FindRequest(aSupport, dstFolder);
+
+ if (copyRequest) {
+ // ClearRequest can cause a new request to get added to m_copyRequests
+ // with matching source and dest folders if the copy listener starts
+ // a new copy. We want to ignore any such request here, because it wasn't
+ // the one that was completed. So we keep track of how many original
+ // requests there were.
+ if (m_copyRequests.IndexOf(copyRequest) >= numOrigRequests) break;
+ // check if this copy request is done by making sure all the
+ // sources have been processed.
+ int32_t sourceIndex, sourceCount;
+ sourceCount = copyRequest->m_copySourceArray.Length();
+ for (sourceIndex = 0; sourceIndex < sourceCount;) {
+ if (!(copyRequest->m_copySourceArray.ElementAt(sourceIndex))
+ ->m_processed)
+ break;
+ sourceIndex++;
+ }
+ // if all sources processed, mark the request as processed
+ if (sourceIndex >= sourceCount) copyRequest->m_processed = true;
+ // if this request is done, or failed, clear it.
+ if (copyRequest->m_processed || NS_FAILED(result)) {
+ ClearRequest(copyRequest, result);
+ numOrigRequests--;
+ } else
+ break;
+ } else
+ break;
+ } while (copyRequest);
+
+ return DoNextCopy();
+}
diff --git a/comm/mailnews/base/src/nsMsgCopyService.h b/comm/mailnews/base/src/nsMsgCopyService.h
new file mode 100644
index 0000000000..ab9be438d0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCopyService.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsMsgCopyService_h__
+#define nsMsgCopyService_h__
+
+#include "nscore.h"
+#include "nsIMsgCopyService.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgWindow.h"
+#include "nsITransactionManager.h"
+#include "nsTArray.h"
+
+typedef enum _nsCopyRequestType {
+ nsCopyMessagesType = 0x0,
+ nsCopyFileMessageType = 0x1,
+ nsCopyFoldersType = 0x2
+} nsCopyRequestType;
+
+class nsCopyRequest;
+
+// nsCopySource represents either:
+// 1. a bundle of messages to be copied, all from the same folder.
+// or
+// 2. a folder to be copied (and no messages).
+class nsCopySource {
+ public:
+ nsCopySource();
+ explicit nsCopySource(nsIMsgFolder* srcFolder);
+ ~nsCopySource();
+ void AddMessage(nsIMsgDBHdr* aMsg);
+
+ nsCOMPtr<nsIMsgFolder> m_msgFolder;
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_messageArray;
+ bool m_processed;
+};
+
+class nsCopyRequest {
+ public:
+ nsCopyRequest();
+ ~nsCopyRequest();
+
+ nsresult Init(nsCopyRequestType type, nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, bool bVal, uint32_t newMsgFlags,
+ const nsACString& newMsgKeywords,
+ nsIMsgCopyServiceListener* listener, nsIMsgWindow* msgWindow,
+ bool allowUndo);
+ nsCopySource* AddNewCopySource(nsIMsgFolder* srcFolder);
+
+ nsCOMPtr<nsISupports> m_srcSupport; // ui source folder or file spec
+ nsCOMPtr<nsIMsgFolder> m_dstFolder;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgCopyServiceListener> m_listener;
+ nsCOMPtr<nsITransactionManager> m_txnMgr;
+ nsCopyRequestType m_requestType;
+ bool m_isMoveOrDraftOrTemplate;
+ bool m_allowUndo;
+ bool m_processed;
+ uint32_t m_newMsgFlags;
+ nsCString m_newMsgKeywords;
+ nsString m_dstFolderName; // used for copy folder.
+ nsTArray<nsCopySource*> m_copySourceArray; // array of nsCopySource
+};
+
+class nsMsgCopyService : public nsIMsgCopyService {
+ public:
+ nsMsgCopyService();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIMSGCOPYSERVICE
+
+ private:
+ virtual ~nsMsgCopyService();
+
+ nsresult ClearRequest(nsCopyRequest* aRequest, nsresult rv);
+ nsresult DoCopy(nsCopyRequest* aRequest);
+ nsresult DoNextCopy();
+ nsCopyRequest* FindRequest(nsISupports* aSupport, nsIMsgFolder* dstFolder);
+ nsresult QueueRequest(nsCopyRequest* aRequest, bool* aCopyImmediately);
+ void LogCopyCompletion(nsISupports* aSrc, nsIMsgFolder* aDest);
+ void LogCopyRequest(const char* logMsg, nsCopyRequest* aRequest);
+
+ nsTArray<nsCopyRequest*> m_copyRequests;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgDBFolder.cpp b/comm/mailnews/base/src/nsMsgDBFolder.cpp
new file mode 100644
index 0000000000..8e82ce1a5c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBFolder.cpp
@@ -0,0 +1,5573 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsUnicharUtils.h"
+#include "nsMsgDBFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsMsgDatabase.h"
+#include "nsIMsgAccountManager.h"
+#include "nsISeekableStream.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIChannel.h"
+#include "nsITransport.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgFolderCompactor.h"
+#include "nsIDocShell.h"
+#include "nsIMsgWindow.h"
+#include "nsIPrompt.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAbCard.h"
+#include "nsIAbDirectory.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIMsgMailSession.h"
+#include "nsTextFormatter.h"
+#include "nsReadLine.h"
+#include "nsLayoutCID.h"
+#include "nsIParserUtils.h"
+#include "nsIDocumentEncoder.h"
+#include "nsMsgI18N.h"
+#include "nsIMIMEHeaderParam.h"
+#include "plbase64.h"
+#include <time.h>
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIMimeHeaders.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIMsgTraitService.h"
+#include "nsIMessenger.h"
+#include "nsThreadUtils.h"
+#include "nsITransactionManager.h"
+#include "nsMsgReadStateTxn.h"
+#include "prmem.h"
+#include "nsIPK11TokenDB.h"
+#include "nsIPK11Token.h"
+#include "nsMsgLocalFolderHdrs.h"
+#define oneHour 3600000000U
+#include "nsMsgUtils.h"
+#include "nsIMsgFilterService.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsMimeTypes.h"
+#include "nsIMsgFilter.h"
+#include "nsIScriptError.h"
+#include "nsIURIMutator.h"
+#include "nsIXULAppInfo.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Components.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Utf8.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+using namespace mozilla;
+
+extern LazyLogModule FILTERLOGMODULE;
+extern LazyLogModule DBLog;
+
+static PRTime gtimeOfLastPurgeCheck; // variable to know when to check for
+ // purge threshold
+
+#define PREF_MAIL_PROMPT_PURGE_THRESHOLD "mail.prompt_purge_threshhold"
+#define PREF_MAIL_PURGE_THRESHOLD "mail.purge_threshhold"
+#define PREF_MAIL_PURGE_THRESHOLD_MB "mail.purge_threshhold_mb"
+#define PREF_MAIL_PURGE_MIGRATED "mail.purge_threshold_migrated"
+#define PREF_MAIL_PURGE_ASK "mail.purge.ask"
+#define PREF_MAIL_WARN_FILTER_CHANGED "mail.warn_filter_changed"
+
+const char* kUseServerRetentionProp = "useServerRetention";
+
+/**
+ * mozilla::intl APIs require sizeable buffers. This class abstracts over
+ * the nsTArray.
+ */
+class nsTArrayU8Buffer {
+ public:
+ using CharType = uint8_t;
+
+ // Do not allow copy or move. Move could be added in the future if needed.
+ nsTArrayU8Buffer(const nsTArrayU8Buffer&) = delete;
+ nsTArrayU8Buffer& operator=(const nsTArrayU8Buffer&) = delete;
+
+ explicit nsTArrayU8Buffer(nsTArray<CharType>& aArray) : mArray(aArray) {}
+
+ /**
+ * Ensures the buffer has enough space to accommodate |size| elements.
+ */
+ [[nodiscard]] bool reserve(size_t size) {
+ mArray.SetCapacity(size);
+ // nsTArray::SetCapacity returns void, return true to keep the API the same
+ // as the other Buffer implementations.
+ return true;
+ }
+
+ /**
+ * Returns the raw data inside the buffer.
+ */
+ CharType* data() { return mArray.Elements(); }
+
+ /**
+ * Returns the count of elements written into the buffer.
+ */
+ size_t length() const { return mArray.Length(); }
+
+ /**
+ * Returns the buffer's overall capacity.
+ */
+ size_t capacity() const { return mArray.Capacity(); }
+
+ /**
+ * Resizes the buffer to the given amount of written elements.
+ */
+ void written(size_t amount) {
+ MOZ_ASSERT(amount <= mArray.Capacity());
+ // This sets |mArray|'s internal size so that it matches how much was
+ // written. This is necessary because the write happens across FFI
+ // boundaries.
+ mArray.SetLengthAndRetainStorage(amount);
+ }
+
+ private:
+ nsTArray<CharType>& mArray;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFolderService, nsIMsgFolderService)
+
+// This method serves the only purpose to re-initialize the
+// folder name strings when UI initialization is done.
+// XXX TODO: This can be removed when the localization system gets
+// initialized in M-C code before, for example, the permission manager
+// triggers folder creation during imap: URI creation.
+// In fact, the entire class together with nsMsgDBFolder::FolderNamesReady()
+// can be removed.
+NS_IMETHODIMP nsMsgFolderService::InitializeFolderStrings() {
+ nsMsgDBFolder::initializeStrings();
+ nsMsgDBFolder::gInitializeStringsDone = true;
+ nsMsgDBFolder::gIsEnglishApp = -1;
+ return NS_OK;
+}
+
+mozilla::UniquePtr<mozilla::intl::Collator>
+ nsMsgDBFolder::gCollationKeyGenerator = nullptr;
+
+nsString nsMsgDBFolder::kLocalizedInboxName;
+nsString nsMsgDBFolder::kLocalizedTrashName;
+nsString nsMsgDBFolder::kLocalizedSentName;
+nsString nsMsgDBFolder::kLocalizedDraftsName;
+nsString nsMsgDBFolder::kLocalizedTemplatesName;
+nsString nsMsgDBFolder::kLocalizedUnsentName;
+nsString nsMsgDBFolder::kLocalizedJunkName;
+nsString nsMsgDBFolder::kLocalizedArchivesName;
+
+nsString nsMsgDBFolder::kLocalizedBrandShortName;
+
+nsrefcnt nsMsgDBFolder::mInstanceCount = 0;
+bool nsMsgDBFolder::gInitializeStringsDone = false;
+// This is used in `nonEnglishApp()` to determine localised
+// folders strings.
+// -1: not retrieved yet, 1: English, 0: non-English.
+int nsMsgDBFolder::gIsEnglishApp;
+
+// We define strings for folder properties and events.
+// Properties:
+constexpr nsLiteralCString kBiffState = "BiffState"_ns;
+constexpr nsLiteralCString kCanFileMessages = "CanFileMessages"_ns;
+constexpr nsLiteralCString kDefaultServer = "DefaultServer"_ns;
+constexpr nsLiteralCString kFlagged = "Flagged"_ns;
+constexpr nsLiteralCString kFolderFlag = "FolderFlag"_ns;
+constexpr nsLiteralCString kFolderSize = "FolderSize"_ns;
+constexpr nsLiteralCString kIsDeferred = "isDeferred"_ns;
+constexpr nsLiteralCString kIsSecure = "isSecure"_ns;
+constexpr nsLiteralCString kJunkStatusChanged = "JunkStatusChanged"_ns;
+constexpr nsLiteralCString kKeywords = "Keywords"_ns;
+constexpr nsLiteralCString kMRMTimeChanged = "MRMTimeChanged"_ns;
+constexpr nsLiteralCString kMsgLoaded = "msgLoaded"_ns;
+constexpr nsLiteralCString kName = "Name"_ns;
+constexpr nsLiteralCString kNewMailReceived = "NewMailReceived"_ns;
+constexpr nsLiteralCString kNewMessages = "NewMessages"_ns;
+constexpr nsLiteralCString kOpen = "open"_ns;
+constexpr nsLiteralCString kSortOrder = "SortOrder"_ns;
+constexpr nsLiteralCString kStatus = "Status"_ns;
+constexpr nsLiteralCString kSynchronize = "Synchronize"_ns;
+constexpr nsLiteralCString kTotalMessages = "TotalMessages"_ns;
+constexpr nsLiteralCString kTotalUnreadMessages = "TotalUnreadMessages"_ns;
+
+// Events:
+constexpr nsLiteralCString kAboutToCompact = "AboutToCompact"_ns;
+constexpr nsLiteralCString kCompactCompleted = "CompactCompleted"_ns;
+constexpr nsLiteralCString kDeleteOrMoveMsgCompleted =
+ "DeleteOrMoveMsgCompleted"_ns;
+constexpr nsLiteralCString kDeleteOrMoveMsgFailed = "DeleteOrMoveMsgFailed"_ns;
+constexpr nsLiteralCString kFiltersApplied = "FiltersApplied"_ns;
+constexpr nsLiteralCString kFolderCreateCompleted = "FolderCreateCompleted"_ns;
+constexpr nsLiteralCString kFolderCreateFailed = "FolderCreateFailed"_ns;
+constexpr nsLiteralCString kFolderLoaded = "FolderLoaded"_ns;
+constexpr nsLiteralCString kNumNewBiffMessages = "NumNewBiffMessages"_ns;
+constexpr nsLiteralCString kRenameCompleted = "RenameCompleted"_ns;
+
+NS_IMPL_ISUPPORTS(nsMsgDBFolder, nsISupportsWeakReference, nsIMsgFolder,
+ nsIDBChangeListener, nsIUrlListener,
+ nsIJunkMailClassificationListener,
+ nsIMsgTraitClassificationListener)
+
+nsMsgDBFolder::nsMsgDBFolder(void)
+ : mAddListener(true),
+ mNewMessages(false),
+ mGettingNewMessages(false),
+ mLastMessageLoaded(nsMsgKey_None),
+ m_numOfflineMsgLines(0),
+ m_bytesAddedToLocalMsg(0),
+ m_tempMessageStreamBytesWritten(0),
+ mFlags(0),
+ mNumUnreadMessages(-1),
+ mNumTotalMessages(-1),
+ mNotifyCountChanges(true),
+ mExpungedBytes(0),
+ mInitializedFromCache(false),
+ mSemaphoreHolder(nullptr),
+ mNumPendingUnreadMessages(0),
+ mNumPendingTotalMessages(0),
+ mFolderSize(kSizeUnknown),
+ mNumNewBiffMessages(0),
+ mHaveParsedURI(false),
+ mIsServerIsValid(false),
+ mIsServer(false),
+ mBayesJunkClassifying(false),
+ mBayesTraitClassifying(false) {
+ if (mInstanceCount++ <= 0) {
+ initializeStrings();
+
+ do {
+ nsresult rv;
+ // We need to check whether we're running under xpcshell,
+ // in that case, we always assume that the strings are good.
+ // XXX TODO: This hack can be removed when the localization system gets
+ // initialized in M-C code before, for example, the permission manager
+ // triggers folder creation during imap: URI creation.
+ nsCOMPtr<nsIXULAppInfo> appinfo =
+ do_GetService("@mozilla.org/xre/app-info;1", &rv);
+ if (NS_FAILED(rv)) break;
+ nsAutoCString appName;
+ rv = appinfo->GetName(appName);
+ if (NS_FAILED(rv)) break;
+ if (appName.Equals("xpcshell")) gInitializeStringsDone = true;
+ } while (false);
+
+ createCollationKeyGenerator();
+ gtimeOfLastPurgeCheck = 0;
+ }
+
+ mProcessingFlag[0].bit = nsMsgProcessingFlags::ClassifyJunk;
+ mProcessingFlag[1].bit = nsMsgProcessingFlags::ClassifyTraits;
+ mProcessingFlag[2].bit = nsMsgProcessingFlags::TraitsDone;
+ mProcessingFlag[3].bit = nsMsgProcessingFlags::FiltersDone;
+ mProcessingFlag[4].bit = nsMsgProcessingFlags::FilterToMove;
+ mProcessingFlag[5].bit = nsMsgProcessingFlags::NotReportedClassified;
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ mProcessingFlag[i].keys = nsMsgKeySetU::Create();
+}
+
+nsMsgDBFolder::~nsMsgDBFolder(void) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ delete mProcessingFlag[i].keys;
+
+ if (--mInstanceCount == 0) {
+ nsMsgDBFolder::gCollationKeyGenerator = nullptr;
+ }
+ // shutdown but don't shutdown children.
+ Shutdown(false);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::FolderNamesReady(bool* aReady) {
+ *aReady = gInitializeStringsDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Shutdown(bool shutdownChildren) {
+ if (mDatabase) {
+ mDatabase->RemoveListener(this);
+ mDatabase->ForceClosed();
+ mDatabase = nullptr;
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+ }
+
+ if (shutdownChildren) {
+ int32_t count = mSubFolders.Count();
+
+ for (int32_t i = 0; i < count; i++) mSubFolders[i]->Shutdown(true);
+
+ // Reset incoming server pointer and pathname.
+ mServer = nullptr;
+ mPath = nullptr;
+ mHaveParsedURI = false;
+ mName.Truncate();
+ mSubFolders.Clear();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ForceDBClosed() {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) mSubFolders[i]->ForceDBClosed();
+
+ if (mDatabase) {
+ mDatabase->ForceClosed();
+ mDatabase = nullptr;
+ } else {
+ nsCOMPtr<nsIMsgDBService> mailDBFactory(
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1"));
+ if (mailDBFactory) mailDBFactory->ForceFolderDBClosed(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CloseAndBackupFolderDB(const nsACString& newName) {
+ ForceDBClosed();
+
+ // We only support backup for mail at the moment
+ if (!(mFlags & nsMsgFolderFlags::Mail)) return NS_OK;
+
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> dbFile;
+ rv = GetSummaryFileLocation(folderPath, getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv = GetBackupSummaryFile(getter_AddRefs(backupDBFile), newName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+
+ backupDBFile->Remove(false);
+ bool backupExists;
+ backupDBFile->Exists(&backupExists);
+ NS_ASSERTION(!backupExists, "Couldn't delete database backup");
+ if (backupExists) return NS_ERROR_FAILURE;
+
+ if (!newName.IsEmpty()) {
+ nsAutoCString backupName;
+ rv = backupDBFile->GetNativeLeafName(backupName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return dbFile->CopyToNative(backupDir, backupName);
+ } else
+ return dbFile->CopyToNative(backupDir, EmptyCString());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OpenBackupMsgDatabase() {
+ if (mBackupDatabase) return NS_OK;
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = folderPath->GetLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(backupDBDummyFolder, this, false, true,
+ getter_AddRefs(mBackupDatabase));
+ // we add a listener so that we can close the db during OnAnnouncerGoingAway.
+ // There should not be any other calls to the listener with the backup
+ // database
+ if (NS_SUCCEEDED(rv) && mBackupDatabase) mBackupDatabase->AddListener(this);
+
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ // this is normal in reparsing
+ rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveBackupMsgDatabase() {
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = folderPath->GetLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv =
+ GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+
+ return backupDBFile->Remove(false);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::StartFolderLoading(void) {
+ if (mDatabase) mDatabase->RemoveListener(this);
+ mAddListener = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::EndFolderLoading(void) {
+ if (mDatabase) mDatabase->AddListener(this);
+ mAddListener = true;
+ UpdateSummaryTotals(true);
+
+ // GGGG check for new mail here and call SetNewMessages...?? -- ONE OF
+ // THE 2 PLACES
+ if (mDatabase) m_newMsgs.Clear();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetExpungedBytes(int64_t* count) {
+ NS_ENSURE_ARG_POINTER(count);
+
+ if (mDatabase) {
+ nsresult rv;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (NS_FAILED(rv)) return rv;
+ rv = folderInfo->GetExpungedBytes(count);
+ if (NS_SUCCEEDED(rv)) mExpungedBytes = *count; // sync up with the database
+ return rv;
+ } else {
+ ReadDBFolderInfo(false);
+ *count = mExpungedBytes;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHasNewMessages(bool* hasNewMessages) {
+ NS_ENSURE_ARG_POINTER(hasNewMessages);
+ *hasNewMessages = mNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetHasNewMessages(bool curNewMessages) {
+ if (curNewMessages != mNewMessages) {
+ // Only change mru time if we're going from doesn't have new to has new.
+ // technically, we should probably update mru time for every new message
+ // but we would pay a performance penalty for that. If the user
+ // opens the folder, the mrutime will get updated anyway.
+ if (curNewMessages) SetMRUTime();
+ bool oldNewMessages = mNewMessages;
+ mNewMessages = curNewMessages;
+ NotifyBoolPropertyChanged(kNewMessages, oldNewMessages, curNewMessages);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHasFolderOrSubfolderNewMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ bool hasNewMessages = mNewMessages;
+
+ if (!hasNewMessages) {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ bool hasNew = false;
+ mSubFolders[i]->GetHasFolderOrSubfolderNewMessages(&hasNew);
+ if (hasNew) {
+ hasNewMessages = true;
+ break;
+ }
+ }
+ }
+
+ *aResult = hasNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetGettingNewMessages(bool* gettingNewMessages) {
+ NS_ENSURE_ARG_POINTER(gettingNewMessages);
+ *gettingNewMessages = mGettingNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetGettingNewMessages(bool gettingNewMessages) {
+ mGettingNewMessages = gettingNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFirstNewMessage(nsIMsgDBHdr** firstNewMessage) {
+ // If there's not a db then there can't be new messages. Return failure since
+ // you should use HasNewMessages first.
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsMsgKey key;
+ rv = mDatabase->GetFirstNew(&key);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ return mDatabase->GetMsgHdrForKey(key, firstNewMessage);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ClearNewMessages() {
+ nsresult rv = NS_OK;
+ bool dbWasCached = mDatabase != nullptr;
+ if (!dbWasCached) GetDatabase();
+
+ if (mDatabase) {
+ mDatabase->GetNewList(m_saveNewMsgs);
+ mDatabase->ClearNewList(true);
+ }
+ if (!dbWasCached) SetMsgDatabase(nullptr);
+
+ m_newMsgs.Clear();
+ mNumNewBiffMessages = 0;
+ return rv;
+}
+
+void nsMsgDBFolder::UpdateNewMessages() {
+ if (!(mFlags & nsMsgFolderFlags::Virtual)) {
+ bool hasNewMessages = false;
+ for (uint32_t keyIndex = 0; keyIndex < m_newMsgs.Length(); keyIndex++) {
+ bool containsKey = false;
+ mDatabase->ContainsKey(m_newMsgs[keyIndex], &containsKey);
+ if (!containsKey) continue;
+ bool isRead = false;
+ nsresult rv2 = mDatabase->IsRead(m_newMsgs[keyIndex], &isRead);
+ if (NS_SUCCEEDED(rv2) && !isRead) {
+ hasNewMessages = true;
+ mDatabase->AddToNewList(m_newMsgs[keyIndex]);
+ }
+ }
+ SetHasNewMessages(hasNewMessages);
+ }
+}
+
+// helper function that gets the cache element that corresponds to the passed in
+// file spec. This could be static, or could live in another class - it's not
+// specific to the current nsMsgDBFolder. If it lived at a higher level, we
+// could cache the account manager and folder cache.
+nsresult nsMsgDBFolder::GetFolderCacheElemFromFile(
+ nsIFile* file, nsIMsgFolderCacheElement** cacheElement) {
+ nsresult result;
+ NS_ENSURE_ARG_POINTER(file);
+ NS_ENSURE_ARG_POINTER(cacheElement);
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+#ifdef DEBUG_bienvenu1
+ bool exists;
+ NS_ASSERTION(NS_SUCCEEDED(fileSpec->Exists(&exists)) && exists,
+ "whoops, file doesn't exist, mac will break");
+#endif
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &result);
+ if (NS_SUCCEEDED(result)) {
+ result = accountMgr->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(result) && folderCache) {
+ nsCString persistentPath;
+ result = file->GetPersistentDescriptor(persistentPath);
+ NS_ENSURE_SUCCESS(result, result);
+ result =
+ folderCache->GetCacheElement(persistentPath, false, cacheElement);
+ }
+ }
+ return result;
+}
+
+nsresult nsMsgDBFolder::ReadDBFolderInfo(bool force) {
+ // Since it turns out to be pretty expensive to open and close
+ // the DBs all the time, if we have to open it once, get everything
+ // we might need while we're here
+ nsresult result = NS_OK;
+
+ // If we reload the cache we might get stale info, so don't do it.
+ if (!mInitializedFromCache) {
+ // Path is used as a key into the foldercache.
+ nsCOMPtr<nsIFile> dbPath;
+ result = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ result = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(result) && cacheElement) {
+ if (NS_SUCCEEDED(ReadFromFolderCacheElem(cacheElement))) {
+ mInitializedFromCache = true;
+ }
+ }
+ }
+ }
+
+ if (force || !mInitializedFromCache) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ bool weOpenedDB = !mDatabase;
+ result =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(result)) {
+ if (folderInfo) {
+ if (!mInitializedFromCache) {
+ folderInfo->GetFlags((int32_t*)&mFlags);
+#ifdef DEBUG_bienvenu1
+ nsString name;
+ GetName(name);
+ NS_ASSERTION(Compare(name, kLocalizedTrashName) ||
+ (mFlags & nsMsgFolderFlags::Trash),
+ "lost trash flag");
+#endif
+ mInitializedFromCache = true;
+ }
+
+ folderInfo->GetNumMessages(&mNumTotalMessages);
+ folderInfo->GetNumUnreadMessages(&mNumUnreadMessages);
+ folderInfo->GetExpungedBytes(&mExpungedBytes);
+
+ nsCString utf8Name;
+ folderInfo->GetFolderName(utf8Name);
+ if (!utf8Name.IsEmpty()) CopyUTF8toUTF16(utf8Name, mName);
+
+ // These should be put in IMAP folder only.
+ // folderInfo->GetImapTotalPendingMessages(&mNumPendingTotalMessages);
+ // folderInfo->GetImapUnreadPendingMessages(&mNumPendingUnreadMessages);
+
+ if (db) {
+ bool hasnew;
+ nsresult rv;
+ rv = db->HasNew(&hasnew);
+ if (NS_FAILED(rv)) return rv;
+ }
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ }
+ } else {
+ // we tried to open DB but failed - don't keep trying.
+ // If a DB is created, we will call this method with force == TRUE,
+ // and read from the db that way.
+ mInitializedFromCache = true;
+ }
+ }
+ return result;
+}
+
+nsresult nsMsgDBFolder::SendFlagNotifications(nsIMsgDBHdr* item,
+ uint32_t oldFlags,
+ uint32_t newFlags) {
+ nsresult rv = NS_OK;
+ uint32_t changedFlags = oldFlags ^ newFlags;
+ if ((changedFlags & nsMsgMessageFlags::Read) &&
+ (changedFlags & nsMsgMessageFlags::New)) {
+ //..so..if the msg is read in the folder and the folder has new msgs clear
+ // the account level and status bar biffs.
+ rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags);
+ rv = SetBiffState(nsMsgBiffState_NoMail);
+ } else if (changedFlags &
+ (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied |
+ nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::IMAPDeleted |
+ nsMsgMessageFlags::New | nsMsgMessageFlags::Offline))
+ rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags);
+ else if ((changedFlags & nsMsgMessageFlags::Marked))
+ rv = NotifyPropertyFlagChanged(item, kFlagged, oldFlags, newFlags);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow*) {
+ NS_ASSERTION(false, "imap and news need to override this");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ NS_ASSERTION(false, "imap and news need to override this");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMsgStore(nsIMsgPluggableStore** aStore) {
+ NS_ENSURE_ARG_POINTER(aStore);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+ return server->GetMsgStore(aStore);
+}
+
+nsresult nsMsgDBFolder::GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream) {
+ NS_ENSURE_ARG(aFileStream);
+
+ *offset = 0;
+ *size = 0;
+
+ // Initialise to nullptr since this is checked by some callers for the success
+ // of the function.
+ *aFileStream = nullptr;
+
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdr->GetOfflineMessageSize(size);
+ NS_ENSURE_TRUE(*size != 0, NS_ERROR_UNEXPECTED);
+ rv = GetMsgInputStream(hdr, aFileStream);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) nsMsgDBFolder::GetOfflineFileStream: "
+ "GetMsgInputStream(hdr, aFileStream); rv=0x%" PRIx32 "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ // Return early: If we could not find an offline stream, clear the offline
+ // flag which will fall back to reading the message from the server.
+ if (mDatabase) mDatabase->MarkOffline(msgKey, false, nullptr);
+
+ return rv;
+ }
+
+ // Check if the database has the correct offset into the offline store by
+ // reading up to 300 bytes. If it is incorrect, clear the offline flag on the
+ // message and return false. This will cause a fall back to reading the
+ // message from the server. We will also advance the offset past the envelope
+ // header ("From " or "FCC") and "X-Mozilla-Status*" lines so these line are
+ // not included when the message is read from the file.
+ // Note: This occurs for both mbox and maildir offline store and probably any
+ // future pluggable store that may be supported.
+ nsCOMPtr<nsISeekableStream> seekableStream = do_QueryInterface(*aFileStream);
+ if (seekableStream) {
+ int64_t o;
+ seekableStream->Tell(&o);
+ *offset = uint64_t(o);
+ char startOfMsg[301];
+ uint32_t bytesRead = 0;
+ uint32_t bytesToRead = sizeof(startOfMsg) - 1;
+ rv = (*aFileStream)->Read(startOfMsg, bytesToRead, &bytesRead);
+ startOfMsg[bytesRead] = '\0';
+ uint32_t msgOffset = 0;
+ uint32_t keepMsgOffset = 0;
+ char* headerLine = startOfMsg;
+ int32_t findPos;
+ // Check a few lines in startOfMsg[] to verify message record validity.
+ bool line1 = true;
+ bool foundError = false;
+ // If Read() above fails, don't check any lines and set record bad.
+ // Even if Read() succeeds, don't enter the loop below if bytesRead is 0.
+ bool foundNextLine = NS_SUCCEEDED(rv) && (bytesRead > 0) ? true : false;
+ while (foundNextLine) {
+ headerLine = startOfMsg + msgOffset;
+ // Ignore lines beginning X-Mozilla-Status or X-Mozilla-Status2
+ if (!strncmp(headerLine, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN) ||
+ !strncmp(headerLine, X_MOZILLA_STATUS2, X_MOZILLA_STATUS2_LEN)) {
+ // If there is an invalid line ahead of X-Mozilla-Status lines,
+ // immediately flag this a bad record. Only the "From " or "FCC"
+ // delimiter line is expected and OK before this.
+ if (foundError) {
+ break;
+ }
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ line1 = false;
+ continue;
+ }
+ if (line1) {
+ // Ignore "From " and, for Drafts, "FCC" when on first line.
+ if ((!strncmp(headerLine, "From ", 5) ||
+ ((mFlags & nsMsgFolderFlags::Drafts) &&
+ !strncmp(headerLine, "FCC", 3)))) {
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ line1 = false;
+ continue;
+ }
+ }
+ bool validOrFrom = false;
+ // Check if line looks like a valid header (just check for a colon). Also
+ // a line beginning with "From " as is sometimes returned by broken IMAP
+ // servers is also acceptable. Also, size of message must be greater than
+ // the offset of the first header line into the message (msgOffset).
+ findPos = MsgFindCharInSet(nsDependentCString(headerLine), ":\n\r", 0);
+ if (((findPos != kNotFound && headerLine[findPos] == ':') ||
+ !strncmp(headerLine, "From ", 5)) &&
+ *size > msgOffset) {
+ validOrFrom = true;
+ }
+ if (!foundError) {
+ if (validOrFrom) {
+ // Record looks OK, accept it.
+ break;
+ } else {
+ foundError = true;
+ keepMsgOffset = msgOffset;
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ if (MOZ_LOG_TEST(DBLog, LogLevel::Info)) {
+ char save;
+ if (foundNextLine) {
+ // Temporarily null terminate the bad header line for logging.
+ save = startOfMsg[msgOffset - 1]; // should be \r or \n
+ startOfMsg[msgOffset - 1] = 0;
+
+ // DEBUG: the error happened while checking network outage
+ // condition.
+ // XXX TODO We may need to check if dovecot and other imap
+ // servers are returning funny "From " line, etc.
+ NS_WARNING(
+ nsPrintfCString("Strange startOfMsg: |%s|\n", startOfMsg)
+ .get());
+ }
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("Invalid header line in offline store: %s",
+ startOfMsg + keepMsgOffset));
+ if (foundNextLine) startOfMsg[msgOffset - 1] = save;
+ }
+ line1 = false;
+ continue;
+ }
+ } else {
+ if (validOrFrom) {
+ // Previous was bad, this is good, accept the record at bad line.
+ foundError = false;
+ msgOffset = keepMsgOffset;
+ break;
+ }
+ // If reached, two consecutive lines bad, reject the record
+ break;
+ }
+ } // while (foundNextLine)
+
+ if (!foundNextLine) {
+ // Can't find a valid header line in the buffer or buffer read() failed.
+ foundError = true;
+ }
+
+ if (!foundError) {
+ *offset += msgOffset;
+ *size -= msgOffset;
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, *offset);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ } else {
+ // Offline store message record looks bad. Cause message fetch from
+ // server and store in RAM cache.
+ MOZ_ASSERT(mDatabase); // Would have crashed above if mDatabase null!
+ mDatabase->MarkOffline(msgKey, false, nullptr);
+ MOZ_LOG(DBLog, LogLevel::Error,
+ ("Leading offline store file content appears invalid, will fetch "
+ "message from server."));
+ MOZ_LOG(
+ DBLog, LogLevel::Error,
+ ("First 300 bytes of offline store content are:\n%s", startOfMsg));
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ // Eventually this will be purely a matter of fetching the storeToken
+ // from the header and asking the msgStore for an inputstream.
+ // But for now, the InputStream returned by the mbox msgStore doesn't
+ // EOF at the end of the message, so we need to jump through hoops here.
+ // For now, we implement it in the derived classes, as the message size
+ // is stored in different msgHdr attributes depending on folder type.
+ // See Bug 1764857.
+ // Also, IMAP has a gmail hack to work with (multiple msgHdrs referrring
+ // to the same locally-stored message).
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetOfflineStoreOutputStream(nsIMsgDBHdr* aHdr,
+ nsIOutputStream** aOutputStream) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+ NS_ENSURE_ARG_POINTER(aHdr);
+
+ nsCOMPtr<nsIMsgPluggableStore> offlineStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(offlineStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = offlineStore->GetNewMsgOutputStream(this, &aHdr, aOutputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMsgInputStream(nsIMsgDBHdr* aMsgHdr,
+ nsIInputStream** aInputStream) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aInputStream);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString storeToken;
+ rv = aMsgHdr->GetStringProperty("storeToken", storeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle legacy DB which has mbox offset but no storeToken.
+ // If this is still needed (open question), it should be done as separate
+ // migration pass, probably at folder creation when store and DB are set
+ // up (but that's tricky at the moment, because the DB is created
+ // on-demand).
+ if (storeToken.IsEmpty()) {
+ nsAutoCString storeType;
+ msgStore->GetStoreType(storeType);
+ if (!storeType.EqualsLiteral("mbox")) {
+ return NS_ERROR_FAILURE; // DB is missing storeToken.
+ }
+ uint64_t offset;
+ aMsgHdr->GetMessageOffset(&offset);
+ storeToken = nsPrintfCString("%" PRIu64, offset);
+ rv = aMsgHdr->SetStringProperty("storeToken", storeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = msgStore->GetMsgInputStream(this, storeToken, aInputStream);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) nsMsgDBFolder::GetMsgInputStream: msgStore->"
+ "GetMsgInputStream(this, ...) returned error rv=0x%" PRIx32
+ "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// path coming in is the root path without the leaf name,
+// on the way out, it's the whole path.
+nsresult nsMsgDBFolder::CreateFileForDB(const nsAString& userLeafName,
+ nsIFile* path, nsIFile** dbFile) {
+ NS_ENSURE_ARG_POINTER(dbFile);
+
+ nsAutoString proposedDBName(userLeafName);
+ NS_MsgHashIfNecessary(proposedDBName);
+
+ // (note, the caller of this will be using the dbFile to call db->Open()
+ // will turn the path into summary file path, and append the ".msf" extension)
+ //
+ // we want db->Open() to create a new summary file
+ // so we have to jump through some hoops to make sure the .msf it will
+ // create is unique. now that we've got the "safe" proposedDBName,
+ // we append ".msf" to see if the file exists. if so, we make the name
+ // unique and then string off the ".msf" so that we pass the right thing
+ // into Open(). this isn't ideal, since this is not atomic
+ // but it will make do.
+ nsresult rv;
+ nsCOMPtr<nsIFile> dbPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath->InitWithFile(path);
+ proposedDBName.AppendLiteral(SUMMARY_SUFFIX);
+ dbPath->Append(proposedDBName);
+ bool exists;
+ dbPath->Exists(&exists);
+ if (exists) {
+ rv = dbPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath->GetLeafName(proposedDBName);
+ }
+ // now, take the ".msf" off
+ proposedDBName.SetLength(proposedDBName.Length() - SUMMARY_SUFFIX_LENGTH);
+ dbPath->SetLeafName(proposedDBName);
+
+ dbPath.forget(dbFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMsgDatabase(nsIMsgDatabase** aMsgDatabase) {
+ NS_ENSURE_ARG_POINTER(aMsgDatabase);
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_FAILURE;
+ NS_ADDREF(*aMsgDatabase = mDatabase);
+ mDatabase->SetLastUseTime(PR_Now());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetMsgDatabase(nsIMsgDatabase* aMsgDatabase) {
+ if (mDatabase) {
+ // commit here - db might go away when all these refs are released.
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ mDatabase->ClearCachedHdrs();
+ if (!aMsgDatabase) {
+ mDatabase->GetNewList(m_newMsgs);
+ }
+ }
+ mDatabase = aMsgDatabase;
+
+ if (aMsgDatabase) aMsgDatabase->AddListener(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDatabaseOpen(bool* aOpen) {
+ NS_ENSURE_ARG_POINTER(aOpen);
+
+ *aOpen = (mDatabase != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetBackupMsgDatabase(nsIMsgDatabase** aMsgDatabase) {
+ NS_ENSURE_ARG_POINTER(aMsgDatabase);
+ nsresult rv = OpenBackupMsgDatabase();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) OpenBackupMsgDatabase(); returns error=0x%" PRIx32
+ "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mBackupDatabase) return NS_ERROR_FAILURE;
+
+ NS_ADDREF(*aMsgDatabase = mBackupDatabase);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** database) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnReadChanged(nsIDBChangeListener* aInstigator) {
+ /* do nothing. if you care about this, override it. see nsNewsFolder.cpp */
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) {
+ NotifyFolderEvent(kJunkStatusChanged);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ /* do nothing. if you care about this, override it.*/
+ return NS_OK;
+}
+
+// 1. When the status of a message changes.
+NS_IMETHODIMP nsMsgDBFolder::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (aHdrChanged) {
+ SendFlagNotifications(aHdrChanged, aOldFlags, aNewFlags);
+ UpdateSummaryTotals(true);
+ }
+
+ // The old state was new message state
+ // We check and see if this state has changed
+ if (aOldFlags & nsMsgMessageFlags::New) {
+ // state changing from new to something else
+ if (!(aNewFlags & nsMsgMessageFlags::New))
+ CheckWithNewMessagesStatus(false);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::CheckWithNewMessagesStatus(bool messageAdded) {
+ bool hasNewMessages;
+ if (messageAdded)
+ SetHasNewMessages(true);
+ else // message modified or deleted
+ {
+ if (mDatabase) {
+ nsresult rv = mDatabase->HasNew(&hasNewMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetHasNewMessages(hasNewMessages);
+ }
+ }
+
+ return NS_OK;
+}
+
+// 3. When a message gets deleted, we need to see if it was new
+// When we lose a new message we need to check if there are still new
+// messages
+NS_IMETHODIMP nsMsgDBFolder::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ // check to see if a new message is being deleted
+ // as in this case, if there is only one new message and it's being deleted
+ // the folder newness has to be cleared.
+ CheckWithNewMessagesStatus(false);
+ // Remove all processing flags. This is generally a good thing although
+ // undo-ing a message back into position will not re-gain the flags.
+ nsMsgKey msgKey;
+ aHdrChanged->GetMessageKey(&msgKey);
+ AndProcessingFlags(msgKey, 0);
+ return OnHdrAddedOrDeleted(aHdrChanged, false);
+}
+
+// 2. When a new messages gets added, we need to see if it's new.
+NS_IMETHODIMP nsMsgDBFolder::OnHdrAdded(nsIMsgDBHdr* aHdrChanged,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (aFlags & nsMsgMessageFlags::New) CheckWithNewMessagesStatus(true);
+ return OnHdrAddedOrDeleted(aHdrChanged, true);
+}
+
+nsresult nsMsgDBFolder::OnHdrAddedOrDeleted(nsIMsgDBHdr* aHdrChanged,
+ bool added) {
+ if (added)
+ NotifyMessageAdded(aHdrChanged);
+ else
+ NotifyMessageRemoved(aHdrChanged);
+ UpdateSummaryTotals(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnParentChanged(nsMsgKey aKeyChanged,
+ nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> hdrChanged;
+ mDatabase->GetMsgHdrForKey(aKeyChanged, getter_AddRefs(hdrChanged));
+ // In reality we probably want to just change the parent because otherwise we
+ // will lose things like selection.
+ if (hdrChanged) {
+ // First delete the child from the old threadParent
+ OnHdrAddedOrDeleted(hdrChanged, false);
+ // Then add it to the new threadParent
+ OnHdrAddedOrDeleted(hdrChanged, true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ if (mBackupDatabase && instigator == mBackupDatabase) {
+ mBackupDatabase->RemoveListener(this);
+ mBackupDatabase = nullptr;
+ } else if (mDatabase) {
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetManyHeadersToDownload(bool* retval) {
+ NS_ENSURE_ARG_POINTER(retval);
+ int32_t numTotalMessages;
+
+ // is there any reason to return false?
+ if (!mDatabase)
+ *retval = true;
+ else if (NS_SUCCEEDED(GetTotalMessages(false, &numTotalMessages)) &&
+ numTotalMessages <= 0)
+ *retval = true;
+ else
+ *retval = false;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result) {
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t msgFlags = 0;
+ hdr->GetFlags(&msgFlags);
+ // check if we already have this message body offline
+ if (!(msgFlags & nsMsgMessageFlags::Offline)) {
+ *result = true;
+ // check against the server download size limit .
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ bool limitDownloadSize = false;
+ rv = incomingServer->GetLimitOfflineMessageSize(&limitDownloadSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (limitDownloadSize) {
+ int32_t maxDownloadMsgSize = 0;
+ uint32_t msgSize;
+ hdr->GetMessageSize(&msgSize);
+ rv = incomingServer->GetMaxMessageSize(&maxDownloadMsgSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ maxDownloadMsgSize *= 1024;
+ if (msgSize > (uint32_t)maxDownloadMsgSize) *result = false;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSupportsOffline(bool* aSupportsOffline) {
+ NS_ENSURE_ARG_POINTER(aSupportsOffline);
+ if (mFlags & nsMsgFolderFlags::Virtual) {
+ *aSupportsOffline = false;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!server) return NS_ERROR_FAILURE;
+
+ int32_t offlineSupportLevel;
+ rv = server->GetOfflineSupportLevel(&offlineSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSupportsOffline = (offlineSupportLevel >= OFFLINE_SUPPORT_LEVEL_REGULAR);
+ return NS_OK;
+}
+
+// Note: this probably always returns false for local folders!
+// Looks like it's only ever used for IMAP folders.
+NS_IMETHODIMP nsMsgDBFolder::ShouldStoreMsgOffline(nsMsgKey msgKey,
+ bool* result) {
+ NS_ENSURE_ARG(result);
+ uint32_t flags = 0;
+ *result = false;
+ GetFlags(&flags);
+ return flags & nsMsgFolderFlags::Offline
+ ? MsgFitsDownloadCriteria(msgKey, result)
+ : NS_OK;
+}
+
+// Looks like this implementation is only ever used for IMAP folders.
+NS_IMETHODIMP nsMsgDBFolder::HasMsgOffline(nsMsgKey msgKey, bool* result) {
+ NS_ENSURE_ARG(result);
+ *result = false;
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t msgFlags = 0;
+ hdr->GetFlags(&msgFlags);
+ // check if we already have this message body offline
+ if ((msgFlags & nsMsgMessageFlags::Offline)) *result = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFlags(uint32_t* _retval) {
+ ReadDBFolderInfo(false);
+ *_retval = mFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = NS_OK;
+
+ element->GetCachedUInt32("flags", &mFlags);
+ element->GetCachedInt32("totalMsgs", &mNumTotalMessages);
+ element->GetCachedInt32("totalUnreadMsgs", &mNumUnreadMessages);
+ element->GetCachedInt32("pendingUnreadMsgs", &mNumPendingUnreadMessages);
+ element->GetCachedInt32("pendingMsgs", &mNumPendingTotalMessages);
+ element->GetCachedInt64("expungedBytes", &mExpungedBytes);
+ element->GetCachedInt64("folderSize", &mFolderSize);
+
+#ifdef DEBUG_bienvenu1
+ nsCString uri;
+ GetURI(uri);
+ printf("read total %ld for %s\n", mNumTotalMessages, uri.get());
+#endif
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetFolderCacheKey(nsIFile** aFile) {
+ nsresult rv;
+ bool isServer = false;
+ GetIsServer(&isServer);
+
+ // if it's a server, we don't need the .msf appended to the name
+ nsCOMPtr<nsIFile> dbPath;
+ if (isServer) {
+ rv = GetFilePath(getter_AddRefs(dbPath));
+ } else {
+ rv = GetSummaryFile(getter_AddRefs(dbPath));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath.forget(aFile);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::FlushToFolderCache() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+ rv = accountManager->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(rv) && folderCache)
+ rv = WriteToFolderCache(folderCache, false);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCache(nsIMsgFolderCache* folderCache,
+ bool deep) {
+ nsresult rv = NS_OK;
+
+ if (folderCache) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ nsCOMPtr<nsIFile> dbPath;
+ rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+#ifdef DEBUG_bienvenu1
+ bool exists;
+ NS_ASSERTION(NS_SUCCEEDED(dbPath->Exists(&exists)) && exists,
+ "file spec we're adding to cache should exist");
+#endif
+ if (NS_SUCCEEDED(rv) && dbPath) {
+ nsCString persistentPath;
+ rv = dbPath->GetPersistentDescriptor(persistentPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderCache->GetCacheElement(persistentPath, true,
+ getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(rv) && cacheElement)
+ rv = WriteToFolderCacheElem(cacheElement);
+ }
+
+ if (deep) {
+ for (nsIMsgFolder* msgFolder : mSubFolders) {
+ rv = msgFolder->WriteToFolderCache(folderCache, true);
+ if (NS_FAILED(rv)) break;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = NS_OK;
+
+ element->SetCachedUInt32("flags", mFlags);
+ element->SetCachedInt32("totalMsgs", mNumTotalMessages);
+ element->SetCachedInt32("totalUnreadMsgs", mNumUnreadMessages);
+ element->SetCachedInt32("pendingUnreadMsgs", mNumPendingUnreadMessages);
+ element->SetCachedInt32("pendingMsgs", mNumPendingTotalMessages);
+ element->SetCachedInt64("expungedBytes", mExpungedBytes);
+ element->SetCachedInt64("folderSize", mFolderSize);
+
+#ifdef DEBUG_bienvenu1
+ nsCString uri;
+ GetURI(uri);
+ printf("writing total %ld for %s\n", mNumTotalMessages, uri.get());
+#endif
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) {
+ NS_ENSURE_ARG_POINTER(aMessage);
+
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsMsgKey msgKey;
+ aMessage->GetMessageKey(&msgKey);
+
+ if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Replied)
+ mDatabase->MarkReplied(msgKey, true, nullptr);
+ else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Forwarded)
+ mDatabase->MarkForwarded(msgKey, true, nullptr);
+ else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Redirected)
+ mDatabase->MarkRedirected(msgKey, true, nullptr);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::AddMarkAllReadUndoAction(nsIMsgWindow* msgWindow,
+ nsMsgKey* thoseMarked,
+ uint32_t numMarked) {
+ RefPtr<nsMsgReadStateTxn> readStateTxn = new nsMsgReadStateTxn();
+ if (!readStateTxn) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv = readStateTxn->Init(this, numMarked, thoseMarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = readStateTxn->SetTransactionType(nsIMessenger::eMarkAllMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ rv = msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = txnMgr->DoTransaction(readStateTxn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) {
+ nsresult rv = GetDatabase();
+ m_newMsgs.Clear();
+
+ if (NS_SUCCEEDED(rv)) {
+ EnableNotifications(allMessageCountNotifications, false);
+ nsTArray<nsMsgKey> thoseMarked;
+ rv = mDatabase->MarkAllRead(thoseMarked);
+ EnableNotifications(allMessageCountNotifications, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup a undo-state
+ if (aMsgWindow && thoseMarked.Length() > 0)
+ rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(),
+ thoseMarked.Length());
+ }
+
+ SetHasNewMessages(false);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::MarkThreadRead(nsIMsgThread* thread) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<nsMsgKey> keys;
+ return mDatabase->MarkThreadRead(thread, nullptr, keys);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) {
+ bool updatingFolder = false;
+ if (NS_SUCCEEDED(mailUrl->GetUpdatingFolder(&updatingFolder)) &&
+ updatingFolder)
+ NotifyFolderEvent(kFolderLoaded);
+
+ // be sure to remove ourselves as a url listener
+ mailUrl->UnRegisterListener(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetRetentionSettings(nsIMsgRetentionSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ *settings = nullptr;
+ nsresult rv = NS_OK;
+ bool useServerDefaults = false;
+ if (!m_retentionSettings) {
+ nsCString useServerRetention;
+ GetStringProperty(kUseServerRetentionProp, useServerRetention);
+ if (useServerRetention.EqualsLiteral("1")) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ rv = incomingServer->GetRetentionSettings(settings);
+ useServerDefaults = true;
+ }
+ } else {
+ GetDatabase();
+ if (mDatabase) {
+ // get the settings from the db - if the settings from the db say the
+ // folder is not overriding the incoming server settings, get the
+ // settings from the server.
+ rv = mDatabase->GetMsgRetentionSettings(settings);
+ if (NS_SUCCEEDED(rv) && *settings) {
+ (*settings)->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ NS_IF_RELEASE(*settings);
+ if (NS_SUCCEEDED(rv) && incomingServer)
+ incomingServer->GetRetentionSettings(settings);
+ }
+ if (useServerRetention.EqualsLiteral("1") != useServerDefaults) {
+ if (useServerDefaults)
+ useServerRetention.Assign('1');
+ else
+ useServerRetention.Assign('0');
+ SetStringProperty(kUseServerRetentionProp, useServerRetention);
+ }
+ }
+ } else
+ return NS_ERROR_FAILURE;
+ }
+ // Only cache the retention settings if we've overridden the server
+ // settings (otherwise, we won't notice changes to the server settings).
+ if (!useServerDefaults) m_retentionSettings = *settings;
+ } else
+ NS_IF_ADDREF(*settings = m_retentionSettings);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetRetentionSettings(
+ nsIMsgRetentionSettings* settings) {
+ bool useServerDefaults;
+ nsCString useServerRetention;
+
+ settings->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ useServerRetention.Assign('1');
+ m_retentionSettings = nullptr;
+ } else {
+ useServerRetention.Assign('0');
+ m_retentionSettings = settings;
+ }
+ SetStringProperty(kUseServerRetentionProp, useServerRetention);
+ GetDatabase();
+ if (mDatabase) mDatabase->SetMsgRetentionSettings(settings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDownloadSettings(
+ nsIMsgDownloadSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ nsresult rv = NS_OK;
+ if (!m_downloadSettings) {
+ GetDatabase();
+ if (mDatabase) {
+ // get the settings from the db - if the settings from the db say the
+ // folder is not overriding the incoming server settings, get the settings
+ // from the server.
+ rv =
+ mDatabase->GetMsgDownloadSettings(getter_AddRefs(m_downloadSettings));
+ if (NS_SUCCEEDED(rv) && m_downloadSettings) {
+ bool useServerDefaults;
+ m_downloadSettings->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer)
+ incomingServer->GetDownloadSettings(
+ getter_AddRefs(m_downloadSettings));
+ }
+ }
+ }
+ }
+ NS_IF_ADDREF(*settings = m_downloadSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetDownloadSettings(
+ nsIMsgDownloadSettings* settings) {
+ m_downloadSettings = settings;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsCommandEnabled(const nsACString& command,
+ bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = true;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::WriteStartOfNewLocalMessage() {
+ nsAutoCString result;
+ uint32_t writeCount;
+ time_t now = time((time_t*)0);
+ char* ct = ctime(&now);
+ ct[24] = 0;
+ result = "From - ";
+ result += ct;
+ result += MSG_LINEBREAK;
+ m_bytesAddedToLocalMsg = result.Length();
+
+ MOZ_ASSERT(m_tempMessageStream,
+ "Temporary message stream must not be nullptr");
+
+ nsresult rv =
+ m_tempMessageStream->Write(result.get(), result.Length(), &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+
+ constexpr auto MozillaStatus = "X-Mozilla-Status: 0001"_ns MSG_LINEBREAK;
+ rv = m_tempMessageStream->Write(MozillaStatus.get(), MozillaStatus.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+ m_bytesAddedToLocalMsg += writeCount;
+ constexpr auto MozillaStatus2 =
+ "X-Mozilla-Status2: 00000000"_ns MSG_LINEBREAK;
+ m_bytesAddedToLocalMsg += MozillaStatus2.Length();
+ rv = m_tempMessageStream->Write(MozillaStatus2.get(), MozillaStatus2.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::StartNewOfflineMessage() {
+ bool isLocked;
+ GetLocked(&isLocked);
+ bool hasSemaphore = false;
+ if (isLocked) {
+ // it's OK if we, the folder, have the semaphore.
+ TestSemaphore(static_cast<nsIMsgFolder*>(this), &hasSemaphore);
+ if (!hasSemaphore) {
+ NS_WARNING("folder locked trying to download offline");
+ return NS_MSG_FOLDER_BUSY;
+ }
+ }
+ m_tempMessageStreamBytesWritten = 0;
+ nsresult rv = GetOfflineStoreOutputStream(
+ m_offlineHeader, getter_AddRefs(m_tempMessageStream));
+ if (NS_SUCCEEDED(rv) && !hasSemaphore)
+ AcquireSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (NS_SUCCEEDED(rv)) WriteStartOfNewLocalMessage();
+ m_numOfflineMsgLines = 0;
+ return rv;
+}
+
+nsresult nsMsgDBFolder::EndNewOfflineMessage(nsresult status) {
+ int64_t curStorePos;
+ uint64_t messageOffset;
+ uint32_t messageSize;
+
+ nsMsgKey messageKey;
+
+ nsresult rv1, rv2;
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_offlineHeader->GetMessageKey(&messageKey);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Are we being asked to abort and clean up?
+ if (NS_FAILED(status)) {
+ mDatabase->MarkOffline(messageKey, false, nullptr);
+ if (m_tempMessageStream) {
+ msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader);
+ }
+ m_tempMessageStream = nullptr;
+ m_offlineHeader = nullptr;
+ return NS_OK;
+ }
+
+ if (m_tempMessageStream) {
+ m_tempMessageStream->Flush();
+ }
+
+ // Some sanity checking.
+ // This will go away once nsIMsgPluggableStore stops serving up seekable
+ // output streams.
+ // If quarantining (mailnews.downloadToTempFile == true) is on we'll already
+ // have a non-seekable stream.
+ nsCOMPtr<nsISeekableStream> seekable;
+ if (m_tempMessageStream) seekable = do_QueryInterface(m_tempMessageStream);
+ if (seekable) {
+ int64_t tellPos;
+ seekable->Tell(&tellPos);
+ curStorePos = tellPos;
+
+ // N.B. This only works if we've set the offline flag for the message,
+ // so be careful about moving the call to MarkOffline above.
+ m_offlineHeader->GetMessageOffset(&messageOffset);
+ curStorePos -= messageOffset;
+ m_offlineHeader->GetMessageSize(&messageSize);
+ messageSize += m_bytesAddedToLocalMsg;
+ // unix/mac has a one byte line ending, but the imap server returns
+ // crlf terminated lines.
+ if (MSG_LINEBREAK_LEN == 1) messageSize -= m_numOfflineMsgLines;
+
+ // We clear the offline flag on the message if the size
+ // looks wrong. Check if we're off by more than one byte per line.
+ if (messageSize > (uint32_t)curStorePos &&
+ (messageSize - (uint32_t)curStorePos) >
+ (uint32_t)m_numOfflineMsgLines) {
+ mDatabase->MarkOffline(messageKey, false, nullptr);
+ // we should truncate the offline store at messageOffset
+ ReleaseSemaphore(static_cast<nsIMsgFolder*>(this));
+ rv1 = rv2 = NS_OK;
+ if (msgStore) {
+ // DiscardNewMessage now closes the stream all the time.
+ rv1 = msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader);
+ m_tempMessageStream = nullptr; // avoid accessing closed stream
+ } else {
+ rv2 = m_tempMessageStream->Close();
+ m_tempMessageStream = nullptr; // ditto
+ }
+ // XXX We should check for errors of rv1 and rv2.
+ if (NS_FAILED(rv1)) NS_WARNING("DiscardNewMessage returned error");
+ if (NS_FAILED(rv2))
+ NS_WARNING("m_tempMessageStream->Close() returned error");
+#ifdef _DEBUG
+ nsAutoCString message("Offline message too small: messageSize=");
+ message.AppendInt(messageSize);
+ message.AppendLiteral(" curStorePos=");
+ message.AppendInt(curStorePos);
+ message.AppendLiteral(" numOfflineMsgLines=");
+ message.AppendInt(m_numOfflineMsgLines);
+ message.AppendLiteral(" bytesAdded=");
+ message.AppendInt(m_bytesAddedToLocalMsg);
+ NS_ERROR(message.get());
+#endif
+ m_offlineHeader = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ } // seekable
+
+ // Success! Finalise the message.
+ mDatabase->MarkOffline(messageKey, true, nullptr);
+ m_offlineHeader->SetOfflineMessageSize(m_tempMessageStreamBytesWritten);
+ m_offlineHeader->SetLineCount(m_numOfflineMsgLines);
+
+ // (But remember, stream might be buffered and closing/flushing could still
+ // fail!)
+
+ rv1 = rv2 = NS_OK;
+ if (msgStore) {
+ rv1 = msgStore->FinishNewMessage(m_tempMessageStream, m_offlineHeader);
+ m_tempMessageStream = nullptr;
+ }
+
+ // We can not let this happen: I think the code assumes this.
+ // That is the if-expression above is always true.
+ NS_ASSERTION(msgStore, "msgStore is nullptr");
+
+ // Notify users of the errors for now, just use NS_WARNING.
+ if (NS_FAILED(rv1)) NS_WARNING("FinishNewMessage returned error");
+ if (NS_FAILED(rv2)) NS_WARNING("m_tempMessageStream->Close() returned error");
+
+ m_tempMessageStream = nullptr;
+ m_offlineHeader = nullptr;
+
+ if (NS_FAILED(rv1)) return rv1;
+ if (NS_FAILED(rv2)) return rv2;
+
+ return rv;
+}
+
+class AutoCompactEvent : public mozilla::Runnable {
+ public:
+ AutoCompactEvent(nsIMsgWindow* aMsgWindow, nsMsgDBFolder* aFolder)
+ : mozilla::Runnable("AutoCompactEvent"),
+ mMsgWindow(aMsgWindow),
+ mFolder(aFolder) {}
+
+ NS_IMETHOD Run() {
+ if (mFolder) mFolder->HandleAutoCompactEvent(mMsgWindow);
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ RefPtr<nsMsgDBFolder> mFolder;
+};
+
+nsresult nsMsgDBFolder::HandleAutoCompactEvent(nsIMsgWindow* aWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountMgr->GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numServers = allServers.Length();
+ if (numServers > 0) {
+ nsTArray<RefPtr<nsIMsgFolder>> folderArray;
+ nsTArray<RefPtr<nsIMsgFolder>> offlineFolderArray;
+ int64_t totalExpungedBytes = 0;
+ int64_t offlineExpungedBytes = 0;
+ int64_t localExpungedBytes = 0;
+ uint32_t serverIndex = 0;
+ do {
+ nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = server->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!msgStore) continue;
+ bool supportsCompaction;
+ msgStore->GetSupportsCompaction(&supportsCompaction);
+ if (!supportsCompaction) continue;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ int32_t offlineSupportLevel;
+ rv = server->GetOfflineSupportLevel(&offlineSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rootFolder->GetDescendants(allDescendants);
+ int64_t expungedBytes = 0;
+ if (offlineSupportLevel > 0) {
+ uint32_t flags;
+ for (auto folder : allDescendants) {
+ expungedBytes = 0;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Offline)
+ folder->GetExpungedBytes(&expungedBytes);
+ if (expungedBytes > 0) {
+ offlineFolderArray.AppendElement(folder);
+ offlineExpungedBytes += expungedBytes;
+ }
+ }
+ } else // pop or local
+ {
+ for (auto folder : allDescendants) {
+ expungedBytes = 0;
+ folder->GetExpungedBytes(&expungedBytes);
+ if (expungedBytes > 0) {
+ folderArray.AppendElement(folder);
+ localExpungedBytes += expungedBytes;
+ }
+ }
+ }
+ }
+ } while (++serverIndex < numServers);
+ totalExpungedBytes = localExpungedBytes + offlineExpungedBytes;
+ int32_t purgeThreshold;
+ rv = GetPurgeThreshold(&purgeThreshold);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (totalExpungedBytes > ((int64_t)purgeThreshold * 1024)) {
+ bool okToCompact = false;
+ nsCOMPtr<nsIPrefService> pref =
+ do_GetService(NS_PREFSERVICE_CONTRACTID);
+ nsCOMPtr<nsIPrefBranch> branch;
+ pref->GetBranch("", getter_AddRefs(branch));
+
+ bool askBeforePurge;
+ branch->GetBoolPref(PREF_MAIL_PURGE_ASK, &askBeforePurge);
+ if (askBeforePurge && aWindow) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString compactSize;
+ FormatFileSize(totalExpungedBytes, true, compactSize);
+
+ bool neverAsk = false; // "Do not ask..." - unchecked by default.
+ int32_t buttonPressed = 0;
+
+ nsCOMPtr<nsIWindowWatcher> ww(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ nsCOMPtr<nsIWritablePropertyBag2> props(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ props->SetPropertyAsAString(u"compactSize"_ns, compactSize);
+ nsCOMPtr<mozIDOMWindowProxy> migrateWizard;
+ rv = ww->OpenWindow(
+ nullptr,
+ "chrome://messenger/content/compactFoldersDialog.xhtml"_ns,
+ "_blank"_ns, "chrome,dialog,modal,centerscreen"_ns, props,
+ getter_AddRefs(migrateWizard));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = props->GetPropertyAsBool(u"checked"_ns, &neverAsk);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv =
+ props->GetPropertyAsInt32(u"buttonNumClicked"_ns, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (buttonPressed == 0) {
+ okToCompact = true;
+ if (neverAsk) // [X] Remove deletions automatically and do not ask
+ branch->SetBoolPref(PREF_MAIL_PURGE_ASK, false);
+ }
+ } else
+ okToCompact = aWindow || !askBeforePurge;
+
+ if (okToCompact) {
+ NotifyFolderEvent(kAboutToCompact);
+
+ if (localExpungedBytes > 0 || offlineExpungedBytes > 0) {
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor = do_CreateInstance(
+ "@mozilla.org/messenger/foldercompactor;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsIMsgFolder* f : offlineFolderArray) {
+ folderArray.AppendElement(f);
+ }
+ rv = folderCompactor->CompactFolders(folderArray, nullptr, aWindow);
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::AutoCompact(nsIMsgWindow* aWindow) {
+ // we don't check for null aWindow, because this routine can get called
+ // in unit tests where we have no window. Just assume not OK if no window.
+ bool prompt;
+ nsresult rv = GetPromptPurgeThreshold(&prompt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ PRTime timeNow = PR_Now(); // time in microseconds
+ PRTime timeAfterOneHourOfLastPurgeCheck = gtimeOfLastPurgeCheck + oneHour;
+ if (timeAfterOneHourOfLastPurgeCheck < timeNow && prompt) {
+ gtimeOfLastPurgeCheck = timeNow;
+ nsCOMPtr<nsIRunnable> event = new AutoCompactEvent(aWindow, this);
+ // Post this as an event because it can put up an alert, which
+ // might cause issues depending on the stack when we are called.
+ if (event) NS_DispatchToCurrentThread(event);
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetPromptPurgeThreshold(bool* aPrompt) {
+ NS_ENSURE_ARG(aPrompt);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ rv = prefBranch->GetBoolPref(PREF_MAIL_PROMPT_PURGE_THRESHOLD, aPrompt);
+ if (NS_FAILED(rv)) {
+ *aPrompt = false;
+ rv = NS_OK;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetPurgeThreshold(int32_t* aThreshold) {
+ NS_ENSURE_ARG(aThreshold);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ int32_t thresholdMB = 200;
+ bool thresholdMigrated = false;
+ prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, &thresholdMB);
+ prefBranch->GetBoolPref(PREF_MAIL_PURGE_MIGRATED, &thresholdMigrated);
+ if (!thresholdMigrated) {
+ *aThreshold = 20480;
+ (void)prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD, aThreshold);
+ if (*aThreshold / 1024 != thresholdMB) {
+ thresholdMB = std::max(1, *aThreshold / 1024);
+ prefBranch->SetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, thresholdMB);
+ }
+ prefBranch->SetBoolPref(PREF_MAIL_PURGE_MIGRATED, true);
+ }
+ *aThreshold = thresholdMB * 1024;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP // called on the folder that is renamed or about to be deleted
+nsMsgDBFolder::MatchOrChangeFilterDestination(nsIMsgFolder* newFolder,
+ bool caseInsensitive,
+ bool* found) {
+ NS_ENSURE_ARG_POINTER(found);
+ *found = false;
+ nsCString oldUri;
+ nsresult rv = GetURI(oldUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString newUri;
+ if (newFolder) // for matching uri's this will be null
+ {
+ rv = newFolder->GetURI(newUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountMgr->GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto server : allServers) {
+ if (server) {
+ bool canHaveFilters;
+ rv = server->GetCanHaveFilters(&canHaveFilters);
+ if (NS_SUCCEEDED(rv) && canHaveFilters) {
+ // update the filterlist to match the new folder name
+ rv = server->GetFilterList(nullptr, getter_AddRefs(filterList));
+ if (NS_SUCCEEDED(rv) && filterList) {
+ bool match;
+ rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri,
+ caseInsensitive, &match);
+ if (NS_SUCCEEDED(rv) && match) {
+ *found = true;
+ if (newFolder && !newUri.IsEmpty())
+ rv = filterList->SaveToDefaultFile();
+ }
+ }
+ // update the editable filterlist to match the new folder name
+ rv = server->GetEditableFilterList(nullptr, getter_AddRefs(filterList));
+ if (NS_SUCCEEDED(rv) && filterList) {
+ bool match;
+ rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri,
+ caseInsensitive, &match);
+ if (NS_SUCCEEDED(rv) && match) {
+ *found = true;
+ if (newFolder && !newUri.IsEmpty())
+ rv = filterList->SaveToDefaultFile();
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDBTransferInfo(nsIDBFolderInfo** aTransferInfo) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), getter_AddRefs(db));
+ NS_ENSURE_STATE(dbFolderInfo);
+ return dbFolderInfo->GetTransferInfo(aTransferInfo);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetDBTransferInfo(nsIDBFolderInfo* aTransferInfo) {
+ NS_ENSURE_ARG(aTransferInfo);
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ dbFolderInfo->InitFromTransferInfo(aTransferInfo);
+ dbFolderInfo->SetBooleanProperty("forceReparse", false);
+ }
+ db->SetSummaryValid(true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetStringProperty(const char* propertyName,
+ nsACString& propertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ nsCOMPtr<nsIFile> dbPath;
+ nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ rv = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (cacheElement) // try to get from cache
+ rv = cacheElement->GetCachedString(propertyName, propertyValue);
+ if (NS_FAILED(rv)) // if failed, then try to get from db, usually.
+ {
+ if (strcmp(propertyName, MRU_TIME_PROPERTY) == 0 ||
+ strcmp(propertyName, MRM_TIME_PROPERTY) == 0 ||
+ strcmp(propertyName, "LastPurgeTime") == 0) {
+ // Don't open DB for missing time properties.
+ // Missing time properties can happen if the folder was never
+ // accessed, for exaple after an import. They happen if
+ // folderCache.json is removed or becomes invalid after moving
+ // a profile (see bug 1726660).
+ propertyValue.Truncate();
+ return NS_OK;
+ }
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ bool exists;
+ rv = dbPath->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_MSG_ERROR_FOLDER_MISSING;
+ bool weOpenedDB = !mDatabase;
+ rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv))
+ rv = folderInfo->GetCharProperty(propertyName, propertyValue);
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ if (NS_SUCCEEDED(rv)) {
+ // Now that we have the value, store it in our cache.
+ if (cacheElement) {
+ cacheElement->SetCachedString(propertyName, propertyValue);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetStringProperty(const char* propertyName,
+ const nsACString& propertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ nsCOMPtr<nsIFile> dbPath;
+ GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (cacheElement) // try to set in the cache
+ cacheElement->SetCachedString(propertyName, propertyValue);
+ }
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv)) {
+ folderInfo->SetCharProperty(propertyName, propertyValue);
+ db->Commit(nsMsgDBCommitType::kLargeCommit); // committing the db also
+ // commits the cache
+ }
+ return NS_OK;
+}
+
+// Get/Set ForcePropertyEmpty is only used with inherited properties
+NS_IMETHODIMP
+nsMsgDBFolder::GetForcePropertyEmpty(const char* aPropertyName, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ nsCString value;
+ GetStringProperty(nameEmpty.get(), value);
+ *_retval = value.EqualsLiteral("true");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetForcePropertyEmpty(const char* aPropertyName, bool aValue) {
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ return SetStringProperty(nameEmpty.get(), aValue ? "true"_ns : ""_ns);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetInheritedStringProperty(const char* aPropertyName,
+ nsACString& aPropertyValue) {
+ NS_ENSURE_ARG_POINTER(aPropertyName);
+ nsCString value;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ bool forceEmpty = false;
+
+ if (!mIsServer) {
+ GetForcePropertyEmpty(aPropertyName, &forceEmpty);
+ } else {
+ // root folders must get their values from the server
+ GetServer(getter_AddRefs(server));
+ if (server) server->GetForcePropertyEmpty(aPropertyName, &forceEmpty);
+ }
+
+ if (forceEmpty) {
+ aPropertyValue.Truncate();
+ return NS_OK;
+ }
+
+ // servers will automatically inherit from the preference
+ // mail.server.default.(propertyName)
+ if (server) return server->GetCharValue(aPropertyName, aPropertyValue);
+
+ GetStringProperty(aPropertyName, value);
+ if (value.IsEmpty()) {
+ // inherit from the parent
+ nsCOMPtr<nsIMsgFolder> parent;
+ GetParent(getter_AddRefs(parent));
+ if (parent)
+ return parent->GetInheritedStringProperty(aPropertyName, aPropertyValue);
+ }
+
+ aPropertyValue.Assign(value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (aMsgURI.IsEmpty()) // This signifies end of batch.
+ {
+ // Apply filters if needed.
+ if (!mPostBayesMessagesToFilter.IsEmpty()) {
+ // Apply post-bayes filtering.
+ nsCOMPtr<nsIMsgFilterService> filterService(
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv));
+ if (NS_SUCCEEDED(rv))
+ // We use a null nsIMsgWindow because we don't want some sort of ui
+ // appearing in the middle of automatic filtering (plus I really don't
+ // want to propagate that value.)
+ rv = filterService->ApplyFilters(nsMsgFilterType::PostPlugin,
+ mPostBayesMessagesToFilter, this,
+ nullptr, nullptr);
+ mPostBayesMessagesToFilter.Clear();
+ }
+
+ // If we classified any messages, send out a notification.
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrs;
+ rv = MsgGetHeadersFromKeys(mDatabase, mClassifiedMsgKeys, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hdrs.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(
+ "@mozilla.org/messenger/msgnotificationservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsClassified(hdrs, mBayesJunkClassifying,
+ mBayesTraitClassifying);
+ }
+ return rv;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if this message needs junk classification
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) {
+ mClassifiedMsgKeys.AppendElement(msgKey);
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyJunk);
+
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(aClassification == nsIJunkMailPlugin::JUNK
+ ? nsIJunkMailPlugin::IS_SPAM_SCORE
+ : nsIJunkMailPlugin::IS_HAM_SCORE);
+ mDatabase->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "plugin"_ns);
+
+ nsAutoCString strPercent;
+ strPercent.AppendInt(aJunkPercent);
+ mDatabase->SetStringProperty(msgKey, "junkpercent", strPercent);
+
+ if (aClassification == nsIJunkMailPlugin::JUNK) {
+ // IMAP has its own way of marking read.
+ if (!(mFlags & nsMsgFolderFlags::ImapBox)) {
+ bool markAsReadOnSpam;
+ (void)spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam);
+ if (markAsReadOnSpam) {
+ rv = mDatabase->MarkRead(msgKey, true, this);
+ if (!NS_SUCCEEDED(rv))
+ NS_WARNING("failed marking spam message as read");
+ }
+ }
+ // mail folders will log junk hits with move info. Perhaps we should
+ // add a log here for non-mail folders as well, that don't override
+ // onMessageClassified
+ // rv = spamSettings->LogJunkHit(msgHdr, false);
+ // NS_ENSURE_SUCCESS(rv,rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnMessageTraitsClassified(const nsACString& aMsgURI,
+ const nsTArray<uint32_t>& aTraits,
+ const nsTArray<uint32_t>& aPercents) {
+ if (aMsgURI.IsEmpty()) // This signifies end of batch
+ return NS_OK; // We are not handling batching
+
+ MOZ_ASSERT(aTraits.Length() == aPercents.Length());
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+ if (!(processingFlags & nsMsgProcessingFlags::ClassifyTraits)) return NS_OK;
+
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyTraits);
+
+ nsCOMPtr<nsIMsgTraitService> traitService;
+ traitService = do_GetService("@mozilla.org/msg-trait-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < aTraits.Length(); i++) {
+ if (aTraits[i] == nsIJunkMailPlugin::JUNK_TRAIT)
+ continue; // junk is processed by the junk listener
+ nsAutoCString traitId;
+ rv = traitService->GetId(aTraits[i], traitId);
+ traitId.InsertLiteral("bayespercent/", 0);
+ nsAutoCString strPercent;
+ strPercent.AppendInt(aPercents[i]);
+ mDatabase->SetStringPropertyByHdr(msgHdr, traitId.get(), strPercent);
+ }
+ return NS_OK;
+}
+
+/**
+ * Call the filter plugins (XXX currently just one)
+ */
+NS_IMETHODIMP
+nsMsgDBFolder::CallFilterPlugins(nsIMsgWindow* aMsgWindow, bool* aFiltersRun) {
+ NS_ENSURE_ARG_POINTER(aFiltersRun);
+
+ nsString folderName;
+ GetPrettyName(folderName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filter plugins on folder '%s'",
+ NS_ConvertUTF16toUTF8(folderName).get()));
+
+ *aFiltersRun = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ int32_t spamLevel = 0;
+
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverType;
+ server->GetType(serverType);
+
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ nsCOMPtr<nsIMsgFilterPlugin> filterPlugin;
+ server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin));
+ if (!filterPlugin) // it's not an error not to have the filter plugin.
+ return NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIJunkMailPlugin> junkMailPlugin = do_QueryInterface(filterPlugin);
+ if (!junkMailPlugin) // we currently only support the junk mail plugin
+ return NS_OK;
+
+ // if it's a news folder, then we really don't support junk in the ui
+ // yet the legacy spamLevel seems to think we should analyze it.
+ // Maybe we should upgrade that, but for now let's not analyze. We'll
+ // let an extension set an inherited property if they really want us to
+ // analyze this. We need that anyway to allow extension-based overrides.
+ // When we finalize adding junk in news to core, we'll deal with the
+ // spamLevel issue
+
+ // if this is the junk folder, or the trash folder
+ // don't analyze for spam, because we don't care
+ //
+ // if it's the sent, unsent, templates, or drafts,
+ // don't analyze for spam, because the user
+ // created that message
+ //
+ // if it's a public imap folder, or another users
+ // imap folder, don't analyze for spam, because
+ // it's not ours to analyze
+ //
+
+ bool filterForJunk = true;
+ if (serverType.EqualsLiteral("rss") ||
+ (mFlags &
+ (nsMsgFolderFlags::SpecialUse | nsMsgFolderFlags::ImapPublic |
+ nsMsgFolderFlags::Newsgroup | nsMsgFolderFlags::ImapOtherUser) &&
+ !(mFlags & nsMsgFolderFlags::Inbox)))
+ filterForJunk = false;
+
+ spamSettings->GetLevel(&spamLevel);
+ if (!spamLevel) filterForJunk = false;
+
+ /*
+ * We'll use inherited folder properties for the junk trait to override the
+ * standard server-based activation of junk processing. This provides a
+ * hook for extensions to customize the application of junk filtering.
+ * Set inherited property "dobayes.mailnews@mozilla.org#junk" to "true"
+ * to force junk processing, and "false" to skip junk processing.
+ */
+
+ nsAutoCString junkEnableOverride;
+ GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk",
+ junkEnableOverride);
+ if (junkEnableOverride.EqualsLiteral("true"))
+ filterForJunk = true;
+ else if (junkEnableOverride.EqualsLiteral("false"))
+ filterForJunk = false;
+
+ bool userHasClassified = false;
+ // if the user has not classified any messages yet, then we shouldn't bother
+ // running the junk mail controls. This creates a better first use experience.
+ // See Bug #250084.
+ junkMailPlugin->GetUserHasClassified(&userHasClassified);
+ if (!userHasClassified) filterForJunk = false;
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Spam filter: %s", filterForJunk ? "true" : "false"));
+
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ rv = GetMsgDatabase(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if trait processing needed
+
+ nsCOMPtr<nsIMsgTraitService> traitService(
+ do_GetService("@mozilla.org/msg-trait-service;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<uint32_t> proIndices;
+ rv = traitService->GetEnabledProIndices(proIndices);
+ bool filterForOther = false;
+ // We just skip this on failure, since it is rarely used.
+ if (NS_SUCCEEDED(rv)) {
+ for (uint32_t i = 0; i < proIndices.Length(); ++i) {
+ // The trait service determines which traits are globally enabled or
+ // disabled. If a trait is enabled, it can still be made inactive
+ // on a particular folder using an inherited property. To do that,
+ // set "dobayes." + trait proID as an inherited folder property with
+ // the string value "false"
+ //
+ // If any non-junk traits are active on the folder, then the bayes
+ // processing will calculate probabilities for all enabled traits.
+
+ if (proIndices[i] != nsIJunkMailPlugin::JUNK_TRAIT) {
+ filterForOther = true;
+ nsAutoCString traitId;
+ nsAutoCString property("dobayes.");
+ traitService->GetId(proIndices[i], traitId);
+ property.Append(traitId);
+ nsAutoCString isEnabledOnFolder;
+ GetInheritedStringProperty(property.get(), isEnabledOnFolder);
+ if (isEnabledOnFolder.EqualsLiteral("false")) filterForOther = false;
+ // We might have to allow a "true" override in the future, but
+ // for now there is no way for that to affect the processing
+ break;
+ }
+ }
+ }
+
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Trait classification: %s", filterForOther ? "true" : "false"));
+ // clang-format on
+
+ // Do we need to apply message filters?
+ bool filterPostPlugin = false; // Do we have a post-analysis filter?
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ GetFilterList(aMsgWindow, getter_AddRefs(filterList));
+ if (filterList) {
+ uint32_t filterCount = 0;
+ filterList->GetFilterCount(&filterCount);
+ for (uint32_t index = 0; index < filterCount && !filterPostPlugin;
+ ++index) {
+ nsCOMPtr<nsIMsgFilter> filter;
+ filterList->GetFilterAt(index, getter_AddRefs(filter));
+ if (!filter) continue;
+ nsMsgFilterTypeType filterType;
+ filter->GetFilterType(&filterType);
+ if (!(filterType & nsMsgFilterType::PostPlugin)) continue;
+ bool enabled = false;
+ filter->GetEnabled(&enabled);
+ if (!enabled) continue;
+ filterPostPlugin = true;
+ }
+ }
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Post-classification filters: %s",
+ filterPostPlugin ? "true" : "false"));
+
+ // If there is nothing to do, leave now but let NotifyHdrsNotBeingClassified
+ // generate the msgsClassified notification for all newly added messages as
+ // tracked by the NotReportedClassified processing flag.
+ if (!filterForOther && !filterForJunk && !filterPostPlugin) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("No filters need to be run"));
+ NotifyHdrsNotBeingClassified();
+ return NS_OK;
+ }
+
+ // get the list of new messages
+ //
+ nsTArray<nsMsgKey> newKeys;
+ rv = database->GetNewList(newKeys);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filters on %" PRIu32 " new messages",
+ (uint32_t)newKeys.Length()));
+
+ nsTArray<nsMsgKey> newMessageKeys;
+ // Start from m_saveNewMsgs (and clear its current state). m_saveNewMsgs is
+ // where we stash the list of new messages when we are told to clear the list
+ // of new messages by the UI (which purges the list from the nsMsgDatabase).
+ newMessageKeys.SwapElements(m_saveNewMsgs);
+ newMessageKeys.AppendElements(newKeys);
+
+ // build up list of keys to classify
+ nsTArray<nsMsgKey> classifyMsgKeys;
+ nsCString uri;
+
+ uint32_t numNewMessages = newMessageKeys.Length();
+ for (uint32_t i = 0; i < numNewMessages; ++i) {
+ nsMsgKey msgKey = newMessageKeys[i];
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filters on message with key %" PRIu32, msgKeyToInt(msgKey)));
+ // clang-format on
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = database->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ if (!NS_SUCCEEDED(rv)) continue;
+ // per-message junk tests.
+ bool filterMessageForJunk = false;
+ while (filterForJunk) // we'll break from this at the end
+ {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Spam filter"));
+ nsCString junkScore;
+ msgHdr->GetStringProperty("junkscore", junkScore);
+ if (!junkScore.IsEmpty()) {
+ // ignore already scored messages.
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Message already scored previously, skipping"));
+ break;
+ }
+
+ bool whiteListMessage = false;
+ spamSettings->CheckWhiteList(msgHdr, &whiteListMessage);
+ if (whiteListMessage) {
+ // mark this msg as non-junk, because we whitelisted it.
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_HAM_SCORE);
+ database->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ database->SetStringProperty(msgKey, "junkscoreorigin", "whitelist"_ns);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Message whitelisted, skipping"));
+ break; // skip this msg since it's in the white list
+ }
+ filterMessageForJunk = true;
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Message is to be classified"));
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyJunk);
+ // Since we are junk processing, we want to defer the msgsClassified
+ // notification until the junk classification has occurred. The event
+ // is sufficiently reliable that we know this will be handled in
+ // OnMessageClassified at the end of the batch. We clear the
+ // NotReportedClassified flag since we know the message is in good hands.
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::NotReportedClassified);
+ break;
+ }
+
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ bool filterMessageForOther = false;
+ // trait processing
+ if (!(processingFlags & nsMsgProcessingFlags::TraitsDone)) {
+ // don't do trait processing on this message again
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::TraitsDone);
+ if (filterForOther) {
+ filterMessageForOther = true;
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyTraits);
+ }
+ }
+
+ if (filterMessageForJunk || filterMessageForOther)
+ classifyMsgKeys.AppendElement(newMessageKeys[i]);
+
+ // Set messages to filter post-bayes.
+ // Have we already filtered this message?
+ if (!(processingFlags & nsMsgProcessingFlags::FiltersDone)) {
+ if (filterPostPlugin) {
+ // Don't do filters on this message again.
+ // (Only set this if we are actually filtering since this is
+ // tantamount to a memory leak.)
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Filters done on this message"));
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::FiltersDone);
+ mPostBayesMessagesToFilter.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ NotifyHdrsNotBeingClassified();
+ // If there weren't any new messages, just return.
+ if (newMessageKeys.IsEmpty()) return NS_OK;
+
+ // If we do not need to do any work, leave.
+ // (We needed to get the list of new messages so we could get their headers so
+ // we can send notifications about them here.)
+
+ if (!classifyMsgKeys.IsEmpty()) {
+ // Remember what classifications are the source of this decision for when
+ // we perform the notification in OnMessageClassified at the conclusion of
+ // classification.
+ mBayesJunkClassifying = filterForJunk;
+ mBayesTraitClassifying = filterForOther;
+
+ uint32_t numMessagesToClassify = classifyMsgKeys.Length();
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running Spam classification on %" PRIu32 " messages",
+ numMessagesToClassify));
+
+ nsTArray<nsCString> messageURIs(numMessagesToClassify);
+ for (uint32_t msgIndex = 0; msgIndex < numMessagesToClassify; ++msgIndex) {
+ nsCString tmpStr;
+ rv = GenerateMessageURI(classifyMsgKeys[msgIndex], tmpStr);
+ if (NS_SUCCEEDED(rv)) {
+ messageURIs.AppendElement(tmpStr);
+ } else {
+ NS_WARNING(
+ "nsMsgDBFolder::CallFilterPlugins(): could not"
+ " generate URI for message");
+ }
+ }
+ // filterMsgs
+ *aFiltersRun = true;
+
+ // Already got proIndices, but need antiIndices too.
+ nsTArray<uint32_t> antiIndices;
+ rv = traitService->GetEnabledAntiIndices(antiIndices);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = junkMailPlugin->ClassifyTraitsInMessages(
+ messageURIs, proIndices, antiIndices, this, aMsgWindow, this);
+ } else if (filterPostPlugin) {
+ // Nothing to classify, so need to end batch ourselves. We do this so that
+ // post analysis filters will run consistently on a folder, even if
+ // disabled junk processing, which could be dynamic through whitelisting,
+ // makes the bayes analysis unnecessary.
+ OnMessageClassified(EmptyCString(), nsIJunkMailPlugin::UNCLASSIFIED, 0);
+ }
+
+ return rv;
+}
+
+/**
+ * Adds the messages in the NotReportedClassified mProcessing set to the
+ * (possibly empty) array of msgHdrsNotBeingClassified, and send the
+ * nsIMsgFolderNotificationService notification.
+ */
+nsresult nsMsgDBFolder::NotifyHdrsNotBeingClassified() {
+ if (mProcessingFlag[5].keys) {
+ nsTArray<nsMsgKey> keys;
+ mProcessingFlag[5].keys->ToMsgKeyArray(keys);
+ if (keys.Length()) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsNotBeingClassified;
+ rv = MsgGetHeadersFromKeys(mDatabase, keys, msgHdrsNotBeingClassified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Since we know we've handled all the NotReportedClassified messages,
+ // we clear the set by deleting and recreating it.
+ delete mProcessingFlag[5].keys;
+ mProcessingFlag[5].keys = nsMsgKeySetU::Create();
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier)
+ notifier->NotifyMsgsClassified(msgHdrsNotBeingClassified,
+ // no classification is being performed
+ false, false);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetLastMessageLoaded(nsMsgKey* aMsgKey) {
+ NS_ENSURE_ARG_POINTER(aMsgKey);
+ *aMsgKey = mLastMessageLoaded;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetLastMessageLoaded(nsMsgKey aMsgKey) {
+ mLastMessageLoaded = aMsgKey;
+ return NS_OK;
+}
+
+// Returns true if: a) there is no need to prompt or b) the user is already
+// logged in or c) the user logged in successfully.
+bool nsMsgDBFolder::PromptForMasterPasswordIfNecessary() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool userNeedsToAuthenticate = false;
+ // if we're PasswordProtectLocalCache, then we need to find out if the server
+ // is authenticated.
+ (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate);
+ if (!userNeedsToAuthenticate) return true;
+
+ // Do we have a master password?
+ nsCOMPtr<nsIPK11TokenDB> tokenDB =
+ do_GetService(NS_PK11TOKENDB_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIPK11Token> token;
+ rv = tokenDB->GetInternalKeyToken(getter_AddRefs(token));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool result;
+ rv = token->CheckPassword(EmptyCString(), &result);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (result) {
+ // We don't have a master password, so this function isn't supported,
+ // therefore just tell account manager we've authenticated and return true.
+ accountManager->SetUserNeedsToAuthenticate(false);
+ return true;
+ }
+
+ // We have a master password, so try and login to the slot.
+ rv = token->Login(false);
+ if (NS_FAILED(rv))
+ // Login failed, so we didn't get a password (e.g. prompt cancelled).
+ return false;
+
+ // Double-check that we are now logged in
+ rv = token->IsLoggedIn(&result);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ accountManager->SetUserNeedsToAuthenticate(!result);
+ return result;
+}
+
+// this gets called after the last junk mail classification has run.
+nsresult nsMsgDBFolder::PerformBiffNotifications(void) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t numBiffMsgs = 0;
+ nsCOMPtr<nsIMsgFolder> root;
+ rv = GetRootFolder(getter_AddRefs(root));
+ root->GetNumNewMessages(true, &numBiffMsgs);
+ if (numBiffMsgs > 0) {
+ server->SetPerformingBiff(true);
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail);
+ server->SetPerformingBiff(false);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::initializeStrings() {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bundle->GetStringFromName("inboxFolderName", kLocalizedInboxName);
+ bundle->GetStringFromName("trashFolderName", kLocalizedTrashName);
+ bundle->GetStringFromName("sentFolderName", kLocalizedSentName);
+ bundle->GetStringFromName("draftsFolderName", kLocalizedDraftsName);
+ bundle->GetStringFromName("templatesFolderName", kLocalizedTemplatesName);
+ bundle->GetStringFromName("junkFolderName", kLocalizedJunkName);
+ bundle->GetStringFromName("outboxFolderName", kLocalizedUnsentName);
+ bundle->GetStringFromName("archivesFolderName", kLocalizedArchivesName);
+
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bundle->GetStringFromName("brandShortName", kLocalizedBrandShortName);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::createCollationKeyGenerator() {
+ if (!gCollationKeyGenerator) {
+ auto result = mozilla::intl::LocaleService::TryCreateComponent<Collator>();
+ if (result.isErr()) {
+ NS_WARNING("Could not create mozilla::intl::Collation.");
+ return NS_ERROR_FAILURE;
+ }
+
+ gCollationKeyGenerator = result.unwrap();
+
+ // Sort in a case-insensitive way, where "base" letters are considered
+ // equal, e.g: a = á, a = A, a ≠ b.
+ Collator::Options options{};
+ options.sensitivity = Collator::Sensitivity::Base;
+ auto optResult = gCollationKeyGenerator->SetOptions(options);
+
+ if (optResult.isErr()) {
+ NS_WARNING("Could not configure the mozilla::intl::Collation.");
+ gCollationKeyGenerator = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::Init(const nsACString& uri) {
+ mURI = uri;
+ return CreateBaseMessageURI(uri);
+}
+
+nsresult nsMsgDBFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ // Each folder needs to implement this.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetURI(nsACString& name) {
+ name = mURI;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+#if 0
+typedef bool
+(*nsArrayFilter)(nsISupports* element, void* data);
+#endif
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ folders.ClearAndRetainStorage();
+ folders.SetCapacity(mSubFolders.Length());
+ for (nsIMsgFolder* f : mSubFolders) {
+ folders.AppendElement(f);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::FindSubFolder(const nsACString& aEscapedSubFolderName,
+ nsIMsgFolder** aFolder) {
+ // XXX use necko here
+ nsAutoCString uri;
+ uri.Append(mURI);
+ uri.Append('/');
+ uri.Append(aEscapedSubFolderName);
+
+ return GetOrCreateFolder(uri, aFolder);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetHasSubFolders(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mSubFolders.Count() > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetNumSubFolders(uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mSubFolders.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddFolderListener(nsIFolderListener* listener) {
+ NS_ENSURE_ARG_POINTER(listener);
+ mListeners.AppendElement(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveFolderListener(nsIFolderListener* listener) {
+ NS_ENSURE_ARG_POINTER(listener);
+ mListeners.RemoveElement(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetParent(nsIMsgFolder* aParent) {
+ mParent = do_GetWeakReference(aParent);
+ if (aParent) {
+ nsresult rv;
+ // servers do not have parents, so we must not be a server
+ mIsServer = false;
+ mIsServerIsValid = true;
+
+ // also set the server itself while we're here.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = aParent->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) mServer = do_GetWeakReference(server);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetParent(nsIMsgFolder** aParent) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ nsCOMPtr<nsIMsgFolder> parent = do_QueryReferent(mParent);
+ parent.forget(aParent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMessages(nsIMsgEnumerator** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ // Make sure mDatabase is set.
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mDatabase->EnumerateMessages(result);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::UpdateFolder(nsIMsgWindow*) { return NS_OK; }
+
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgDBFolder::GetFolderURL(nsACString& url) {
+ url.Assign(EmptyCString());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetServer(nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsresult rv;
+ // short circuit the server if we have it.
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv);
+ if (NS_FAILED(rv)) {
+ // try again after parsing the URI
+ rv = parseURI(true);
+ server = do_QueryReferent(mServer);
+ }
+ server.forget(aServer);
+ return *aServer ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDBFolder::parseURI(bool needServer) {
+ nsresult rv;
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // empty path tells us it's a server.
+ if (!mIsServerIsValid) {
+ nsAutoCString path;
+ rv = url->GetPathQueryRef(path);
+ if (NS_SUCCEEDED(rv)) mIsServer = path.EqualsLiteral("/");
+ mIsServerIsValid = true;
+ }
+
+ // grab the name off the leaf of the server
+ if (mName.IsEmpty()) {
+ // mName:
+ // the name is the trailing directory in the path
+ nsAutoCString fileName;
+ nsAutoCString escapedFileName;
+ url->GetFileName(escapedFileName);
+ if (!escapedFileName.IsEmpty()) {
+ // XXX conversion to unicode here? is fileName in UTF8?
+ // yes, let's say it is in utf8
+ MsgUnescapeString(escapedFileName, 0, fileName);
+ NS_ASSERTION(mozilla::IsUtf8(fileName), "fileName is not in UTF-8");
+ CopyUTF8toUTF16(fileName, mName);
+ }
+ }
+
+ // grab the server by parsing the URI and looking it up
+ // in the account manager...
+ // But avoid this extra work by first asking the parent, if any
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv);
+ if (NS_FAILED(rv)) {
+ // first try asking the parent instead of the URI
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ GetParent(getter_AddRefs(parentMsgFolder));
+
+ if (parentMsgFolder)
+ rv = parentMsgFolder->GetServer(getter_AddRefs(server));
+
+ // no parent. do the extra work of asking
+ if (!server && needServer) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverType;
+ GetIncomingServerType(serverType);
+ if (serverType.IsEmpty()) {
+ NS_WARNING("can't determine folder's server type");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = NS_MutateURI(url).SetScheme(serverType).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mServer = do_GetWeakReference(server);
+ } /* !mServer */
+
+ // now try to find the local path for this folder
+ if (server) {
+ nsAutoCString newPath;
+ nsAutoCString escapedUrlPath;
+ nsAutoCString urlPath;
+ url->GetFilePath(escapedUrlPath);
+ if (!escapedUrlPath.IsEmpty()) {
+ MsgUnescapeString(escapedUrlPath, 0, urlPath);
+
+ // transform the filepath from the URI, such as
+ // "/folder1/folder2/foldern"
+ // to
+ // "folder1.sbd/folder2.sbd/foldern"
+ // (remove leading / and add .sbd to first n-1 folders)
+ // to be appended onto the server's path
+ bool isNewsFolder = false;
+ nsAutoCString scheme;
+ if (NS_SUCCEEDED(url->GetScheme(scheme))) {
+ isNewsFolder = scheme.EqualsLiteral("news") ||
+ scheme.EqualsLiteral("snews") ||
+ scheme.EqualsLiteral("nntp");
+ }
+ NS_MsgCreatePathStringFromFolderURI(urlPath.get(), newPath, scheme,
+ isNewsFolder);
+ }
+
+ // now append munged path onto server path
+ nsCOMPtr<nsIFile> serverPath;
+ rv = server->GetLocalPath(getter_AddRefs(serverPath));
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mPath && serverPath) {
+ if (!newPath.IsEmpty()) {
+ // I hope this is temporary - Ultimately,
+ // NS_MsgCreatePathStringFromFolderURI will need to be fixed.
+#if defined(XP_WIN)
+ newPath.ReplaceChar('/', '\\');
+#endif
+ rv = serverPath->AppendRelativeNativePath(newPath);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to append to the serverPath");
+ if (NS_FAILED(rv)) {
+ mPath = nullptr;
+ return rv;
+ }
+ }
+ mPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mPath->InitWithFile(serverPath);
+ }
+ // URI is completely parsed when we've attempted to get the server
+ mHaveParsedURI = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetIsServer(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // make sure we've parsed the URI
+ if (!mIsServerIsValid) {
+ nsresult rv = parseURI();
+ if (NS_FAILED(rv) || !mIsServerIsValid) return NS_ERROR_FAILURE;
+ }
+
+ *aResult = mIsServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetNoSelect(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetImapShared(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ return GetFlag(nsMsgFolderFlags::PersonalShared, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // by default, you can't subscribe.
+ // if otherwise, override it.
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanFileMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // varada - checking folder flag to see if it is the "Unsent Messages"
+ // and if so return FALSE
+ if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_FAILED(rv)) return rv;
+
+ // by default, you can't file messages into servers, only to folders
+ // if otherwise, override it.
+ *aResult = !isServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanDeleteMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // Checking folder flag to see if it is the "Unsent Messages"
+ // or a virtual folder, and if so return FALSE
+ if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ // by default, you can create subfolders on server and folders
+ // if otherwise, override it.
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanRename(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_FAILED(rv)) return rv;
+ // by default, you can't rename servers, only folders
+ // if otherwise, override it.
+ //
+ // check if the folder is a special folder
+ // (Trash, Drafts, Unsent Messages, Inbox, Sent, Templates, Junk, Archives)
+ // if it is, don't allow the user to rename it
+ // (which includes dnd moving it with in the same server)
+ //
+ // this errors on the side of caution. we'll return false a lot
+ // more often if we use flags,
+ // instead of checking if the folder really is being used as a
+ // special folder by looking at the "copies and folders" prefs on the
+ // identities.
+ *aResult = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanCompact(bool* canCompact) {
+ NS_ENSURE_ARG_POINTER(canCompact);
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // servers (root folder) cannot be compacted
+ // virtual search folders cannot be compacted
+ *canCompact = !isServer && !(mFlags & nsMsgFolderFlags::Virtual);
+ // If *canCompact now true and folder is imap, keep *canCompact true and
+ // return; otherwise, when not imap, type of store controls it. E.g., mbox
+ // sets *canCompact true, maildir sets it false.
+ if (*canCompact && !(mFlags & nsMsgFolderFlags::ImapBox)) {
+ // Check if the storage type supports compaction
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ GetMsgStore(getter_AddRefs(msgStore));
+ if (msgStore) msgStore->GetSupportsCompaction(canCompact);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetPrettyName(nsAString& name) {
+ return GetName(name);
+}
+
+static bool nonEnglishApp() {
+ if (nsMsgDBFolder::gIsEnglishApp == -1) {
+ nsAutoCString locale;
+ mozilla::intl::LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
+ nsMsgDBFolder::gIsEnglishApp =
+ (locale.EqualsLiteral("en") || StringBeginsWith(locale, "en-"_ns)) ? 1
+ : 0;
+ }
+ return nsMsgDBFolder::gIsEnglishApp ? false : true;
+}
+
+static bool hasTrashName(const nsAString& name) {
+ // Microsoft calls the folder "Deleted". If the application is non-English,
+ // we want to use the localised name instead.
+ return name.LowerCaseEqualsLiteral("trash") ||
+ (name.LowerCaseEqualsLiteral("deleted") && nonEnglishApp());
+}
+
+static bool hasDraftsName(const nsAString& name) {
+ // Some IMAP providers call the folder "Draft". If the application is
+ // non-English, we want to use the localised name instead.
+ return name.LowerCaseEqualsLiteral("drafts") ||
+ (name.LowerCaseEqualsLiteral("draft") && nonEnglishApp());
+}
+
+static bool hasSentName(const nsAString& name) {
+ // Some IMAP providers call the folder for sent messages "Outbox". That IMAP
+ // folder is not related to Thunderbird's local folder for queued messages.
+ // If we find such a folder with the 'SentMail' flag, we can safely localize
+ // its name if the application is non-English.
+ return name.LowerCaseEqualsLiteral("sent") ||
+ (name.LowerCaseEqualsLiteral("outbox") && nonEnglishApp());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetPrettyName(const nsAString& name) {
+ nsresult rv;
+ // Keep original name.
+ mOriginalName = name;
+
+ // Set pretty name only if special flag is set and if it the default folder
+ // name
+ if (mFlags & nsMsgFolderFlags::Inbox && name.LowerCaseEqualsLiteral("inbox"))
+ rv = SetName(kLocalizedInboxName);
+ else if (mFlags & nsMsgFolderFlags::SentMail && hasSentName(name))
+ rv = SetName(kLocalizedSentName);
+ else if (mFlags & nsMsgFolderFlags::Drafts && hasDraftsName(name))
+ rv = SetName(kLocalizedDraftsName);
+ else if (mFlags & nsMsgFolderFlags::Templates &&
+ name.LowerCaseEqualsLiteral("templates"))
+ rv = SetName(kLocalizedTemplatesName);
+ else if (mFlags & nsMsgFolderFlags::Trash && hasTrashName(name))
+ rv = SetName(kLocalizedTrashName);
+ else if (mFlags & nsMsgFolderFlags::Queue &&
+ name.LowerCaseEqualsLiteral("unsent messages"))
+ rv = SetName(kLocalizedUnsentName);
+ else if (mFlags & nsMsgFolderFlags::Junk &&
+ name.LowerCaseEqualsLiteral("junk"))
+ rv = SetName(kLocalizedJunkName);
+ else if (mFlags & nsMsgFolderFlags::Archive &&
+ name.LowerCaseEqualsLiteral("archives"))
+ rv = SetName(kLocalizedArchivesName);
+ else
+ rv = SetName(name);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetPrettyNameFromOriginal(void) {
+ if (mOriginalName.IsEmpty()) return NS_OK;
+ return SetPrettyName(mOriginalName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetName(nsAString& name) {
+ nsresult rv;
+ if (!mHaveParsedURI && mName.IsEmpty()) {
+ rv = parseURI();
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // if it's a server, just forward the call
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) return server->GetPrettyName(name);
+ }
+
+ name = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetName(const nsAString& name) {
+ // override the URI-generated name
+ if (!mName.Equals(name)) {
+ mName = name;
+ // old/new value doesn't matter here
+ NotifyUnicharPropertyChanged(kName, name, name);
+ }
+ return NS_OK;
+}
+
+// For default, just return name
+NS_IMETHODIMP nsMsgDBFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) {
+ return GetName(aAbbreviatedName);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetChildNamed(const nsAString& aName, nsIMsgFolder** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy); // initialize mSubFolders
+ *aChild = nullptr;
+
+ for (nsIMsgFolder* child : mSubFolders) {
+ nsString folderName;
+ nsresult rv = child->GetName(folderName);
+ // case-insensitive compare is probably LCD across OS filesystems
+ if (NS_SUCCEEDED(rv) &&
+ folderName.Equals(aName, nsCaseInsensitiveStringComparator)) {
+ NS_ADDREF(*aChild = child);
+ return NS_OK;
+ }
+ }
+ // don't return NS_OK if we didn't find the folder
+ // see http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c15
+ // and http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c17
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetChildWithURI(const nsACString& uri, bool deep,
+ bool caseInsensitive,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+ // will return nullptr if we can't find it
+ *child = nullptr;
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* folder : subFolders) {
+ nsCString folderURI;
+ rv = folder->GetURI(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool equal =
+ (caseInsensitive
+ ? uri.Equals(folderURI, nsCaseInsensitiveCStringComparator)
+ : uri.Equals(folderURI));
+ if (equal) {
+ NS_ADDREF(*child = folder);
+ return NS_OK;
+ }
+ if (deep) {
+ rv = folder->GetChildWithURI(uri, deep, caseInsensitive, child);
+ if (NS_FAILED(rv)) return rv;
+
+ if (*child) return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetShowDeletedMessages(bool* showDeletedMessages) {
+ NS_ENSURE_ARG_POINTER(showDeletedMessages);
+ *showDeletedMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DeleteStorage() {
+ ForceDBClosed();
+
+ // Delete the .msf file.
+ // NOTE: this doesn't remove .msf files in subfolders, but
+ // both nsMsgBrkMBoxStore::DeleteFolder() and
+ // nsMsgMaildirStore::DeleteFolder() will remove those .msf files
+ // as a side-effect of deleting the .sbd directory.
+ nsCOMPtr<nsIFile> summaryFile;
+ nsresult rv = GetSummaryFile(getter_AddRefs(summaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool exists = false;
+ summaryFile->Exists(&exists);
+ if (exists) {
+ rv = summaryFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Ask the msgStore to delete the actual storage (mbox, maildir or whatever
+ // else may be supported in future).
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->DeleteFolder(this);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DeleteSelf(nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ GetParent(getter_AddRefs(parent));
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+ return parent->PropagateDelete(this, true);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CreateStorageIfMissing(
+ nsIUrlListener* /* urlListener */) {
+ NS_ASSERTION(false, "needs to be overridden");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::PropagateDelete(nsIMsgFolder* folder,
+ bool deleteStorage) {
+ // first, find the folder we're looking to delete
+ nsresult rv = NS_OK;
+
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> child(mSubFolders[i]);
+ if (folder == child.get()) {
+ // Remove self as parent
+ child->SetParent(nullptr);
+ // maybe delete disk storage for it, and its subfolders
+ rv = child->RecursiveDelete(deleteStorage);
+ if (NS_SUCCEEDED(rv)) {
+ // Remove from list of subfolders.
+ mSubFolders.RemoveObjectAt(i);
+ NotifyFolderRemoved(child);
+ break;
+ } else // setting parent back if we failed
+ child->SetParent(this);
+ } else
+ rv = child->PropagateDelete(folder, deleteStorage);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RecursiveDelete(bool deleteStorage) {
+ // If deleteStorage is true, recursively deletes disk storage for this folder
+ // and all its subfolders.
+ // Regardless of deleteStorage, always unlinks them from the children lists
+ // and frees memory for the subfolders but NOT for _this_
+ // and does not remove _this_ from the parent's list of children.
+
+ nsCOMPtr<nsIFile> dbPath;
+ // first remove the deleted folder from the folder cache;
+ nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+ rv = accountMgr->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(rv) && folderCache) {
+ nsCString persistentPath;
+ rv = dbPath->GetPersistentDescriptor(persistentPath);
+ if (NS_SUCCEEDED(rv)) folderCache->RemoveElement(persistentPath);
+ }
+ }
+
+ int32_t count = mSubFolders.Count();
+ while (count > 0) {
+ nsCOMPtr<nsIMsgFolder> child(mSubFolders[0]);
+ child->SetParent(nullptr);
+ rv = child->RecursiveDelete(deleteStorage);
+ if (NS_SUCCEEDED(rv)) {
+ // unlink it from this child's list
+ mSubFolders.RemoveObjectAt(0);
+ NotifyFolderRemoved(child);
+ } else {
+ // setting parent back if we failed for some reason
+ child->SetParent(this);
+ break;
+ }
+
+ count--;
+ }
+
+ // now delete the disk storage for _this_
+ if (deleteStorage && NS_SUCCEEDED(rv)) {
+ // All delete commands use deleteStorage = true, and local moves use false.
+ // IMAP moves use true, leaving this here in the hope that bug 439108
+ // works out.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderDeleted(this);
+ rv = DeleteStorage();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddSubfolder(const nsAString& name,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+
+ int32_t flags = 0;
+ nsresult rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+
+ // URI should use UTF-8
+ // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax)
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(name, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure the containing (.sbd) dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // fix for #192780
+ // if this is the root folder
+ // make sure the the special folders
+ // have the right uri.
+ // on disk, host\INBOX should be a folder with the uri
+ // mailbox://user@host/Inbox" as mailbox://user@host/Inbox !=
+ // mailbox://user@host/INBOX
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder &&
+ (rootFolder.get() == (nsIMsgFolder*)this)) {
+ if (escapedName.LowerCaseEqualsLiteral("inbox"))
+ uri += "Inbox";
+ else if (escapedName.LowerCaseEqualsLiteral("unsent%20messages"))
+ uri += "Unsent%20Messages";
+ else if (escapedName.LowerCaseEqualsLiteral("drafts"))
+ uri += "Drafts";
+ else if (escapedName.LowerCaseEqualsLiteral("trash"))
+ uri += "Trash";
+ else if (escapedName.LowerCaseEqualsLiteral("sent"))
+ uri += "Sent";
+ else if (escapedName.LowerCaseEqualsLiteral("templates"))
+ uri += "Templates";
+ else if (escapedName.LowerCaseEqualsLiteral("archives"))
+ uri += "Archives";
+ else
+ uri += escapedName.get();
+ } else
+ uri += escapedName.get();
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = GetChildWithURI(uri, false /*deep*/, true /*case Insensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ folder->GetFlags((uint32_t*)&flags);
+ flags |= nsMsgFolderFlags::Mail;
+ folder->SetParent(this);
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+
+ // Only set these if these are top level children.
+ if (NS_SUCCEEDED(rv) && isServer) {
+ if (name.LowerCaseEqualsLiteral("inbox")) {
+ flags |= nsMsgFolderFlags::Inbox;
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_Unknown);
+ } else if (name.LowerCaseEqualsLiteral("trash"))
+ flags |= nsMsgFolderFlags::Trash;
+ else if (name.LowerCaseEqualsLiteral("unsent messages") ||
+ name.LowerCaseEqualsLiteral("outbox"))
+ flags |= nsMsgFolderFlags::Queue;
+ }
+
+ folder->SetFlags(flags);
+
+ if (folder) mSubFolders.AppendObject(folder);
+
+ folder.forget(child);
+ // at this point we must be ok and we don't want to return failure in case
+ // GetIsServer failed.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::EmptyTrash(nsIUrlListener* aListener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgDBFolder::CheckIfFolderExists(const nsAString& newFolderName,
+ nsIMsgFolder* parentFolder,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(parentFolder);
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = parentFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ nsString folderName;
+
+ msgFolder->GetName(folderName);
+ if (folderName.Equals(newFolderName, nsCaseInsensitiveStringComparator)) {
+ ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ }
+ }
+ return NS_OK;
+}
+
+bool nsMsgDBFolder::ConfirmAutoFolderRename(nsIMsgWindow* msgWindow,
+ const nsString& aOldName,
+ const nsString& aNewName) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsString folderName;
+ GetName(folderName);
+ AutoTArray<nsString, 3> formatStrings = {aOldName, folderName, aNewName};
+
+ nsString confirmString;
+ rv = bundle->FormatStringFromName("confirmDuplicateFolderRename",
+ formatStrings, confirmString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ bool confirmed = false;
+ rv = ThrowConfirmationPrompt(msgWindow, confirmString, &confirmed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ return confirmed;
+}
+
+nsresult nsMsgDBFolder::AddDirectorySeparator(nsIFile* path) {
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ return path->SetLeafName(leafName);
+}
+
+/* Finds the subdirectory associated with this folder. That is if the path is
+ c:\Inbox, it will return c:\Inbox.sbd if it succeeds. If that path doesn't
+ currently exist then it will create it. Path is strictly an out parameter.
+ */
+nsresult nsMsgDBFolder::CreateDirectoryForFolder(nsIFile** resultFile) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isServer) {
+ // Server dir doesn't have .sbd suffix.
+ // Ensure it exists and is a directory.
+ bool pathExists;
+ path->Exists(&pathExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!pathExists) {
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ bool isDir;
+ rv = path->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ path.forget(resultFile);
+ return NS_OK;
+ }
+
+ // Append .sbd suffix.
+ rv = AddDirectorySeparator(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Already exists?
+ bool exists;
+ rv = path->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (exists) {
+ bool isDir;
+ rv = path->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ // Uhoh. Not the dir we were expecting!
+ return NS_MSG_COULD_NOT_CREATE_DIRECTORY;
+ }
+ // Already been created.
+ path.forget(resultFile);
+ return NS_OK;
+ }
+
+ // Need to create it.
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ path.forget(resultFile);
+ return NS_OK;
+}
+
+/* Finds the backup directory associated with this folder, stored on the temp
+ drive. If that path doesn't currently exist then it will create it. Path is
+ strictly an out parameter.
+ */
+nsresult nsMsgDBFolder::CreateBackupDirectory(nsIFile** resultFile) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = path->Append(u"MozillaMailnews"_ns);
+ bool pathIsDirectory;
+ path->IsDirectory(&pathIsDirectory);
+
+ // If that doesn't exist, then we have to create this directory
+ if (!pathIsDirectory) {
+ bool pathExists;
+ path->Exists(&pathExists);
+ // If for some reason there's a file with the directory separator
+ // then we are going to fail.
+ rv = pathExists ? NS_MSG_COULD_NOT_CREATE_DIRECTORY
+ : path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ }
+ if (NS_SUCCEEDED(rv)) path.forget(resultFile);
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetBackupSummaryFile(nsIFile** aBackupFile,
+ const nsACString& newName) {
+ nsCOMPtr<nsIFile> backupDir;
+ nsresult rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!newName.IsEmpty()) {
+ rv = backupDBDummyFolder->AppendNative(newName);
+ } else // if newName is null, use the folder name
+ {
+ nsCOMPtr<nsIFile> folderPath;
+ rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString folderName;
+ rv = folderPath->GetNativeLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->AppendNative(folderName);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv =
+ GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ backupDBFile.forget(aBackupFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Rename(const nsAString& aNewName,
+ nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIFile> oldPathFile;
+ nsresult rv = GetFilePath(getter_AddRefs(oldPathFile));
+ if (NS_FAILED(rv)) return rv;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ rv = GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_ERROR_FAILURE;
+ nsCOMPtr<nsISupports> parentSupport = do_QueryInterface(parentFolder);
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = GetSummaryFileLocation(oldPathFile, getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> dirFile;
+ int32_t count = mSubFolders.Count();
+
+ if (count > 0) rv = CreateDirectoryForFolder(getter_AddRefs(dirFile));
+
+ nsAutoString newDiskName(aNewName);
+ NS_MsgHashIfNecessary(newDiskName);
+
+ if (mName.Equals(aNewName, nsCaseInsensitiveStringComparator)) {
+ rv = ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ } else {
+ nsCOMPtr<nsIFile> parentPathFile;
+ parentFolder->GetFilePath(getter_AddRefs(parentPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isDirectory = false;
+ parentPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) AddDirectorySeparator(parentPathFile);
+
+ rv = CheckIfFolderExists(aNewName, parentFolder, msgWindow);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ ForceDBClosed();
+
+ // Save of dir name before appending .msf
+ nsAutoString newNameDirStr(newDiskName);
+
+ if (!(mFlags & nsMsgFolderFlags::Virtual))
+ rv = oldPathFile->MoveTo(nullptr, newDiskName);
+ if (NS_SUCCEEDED(rv)) {
+ newDiskName.AppendLiteral(SUMMARY_SUFFIX);
+ oldSummaryFile->MoveTo(nullptr, newDiskName);
+ } else {
+ ThrowAlertMsg("folderRenameFailed", msgWindow);
+ return rv;
+ }
+
+ if (NS_SUCCEEDED(rv) && count > 0) {
+ // rename "*.sbd" directory
+ newNameDirStr.AppendLiteral(FOLDER_SUFFIX);
+ dirFile->MoveTo(nullptr, newNameDirStr);
+ }
+
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ if (parentSupport) {
+ rv = parentFolder->AddSubfolder(aNewName, getter_AddRefs(newFolder));
+ if (newFolder) {
+ newFolder->SetPrettyName(EmptyString());
+ newFolder->SetPrettyName(aNewName);
+ newFolder->SetFlags(mFlags);
+ bool changed = false;
+ MatchOrChangeFilterDestination(newFolder, true /*case-insensitive*/,
+ &changed);
+ if (changed) AlertFilterChanged(msgWindow);
+
+ if (count > 0) newFolder->RenameSubFolders(msgWindow, this);
+
+ if (parentFolder) {
+ SetParent(nullptr);
+ parentFolder->PropagateDelete(this, false);
+ parentFolder->NotifyFolderAdded(newFolder);
+ }
+ newFolder->NotifyFolderEvent(kRenameCompleted);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ContainsChildNamed(const nsAString& name,
+ bool* containsChild) {
+ NS_ENSURE_ARG_POINTER(containsChild);
+ nsCOMPtr<nsIMsgFolder> child;
+ GetChildNamed(name, getter_AddRefs(child));
+ *containsChild = child != nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsAncestorOf(nsIMsgFolder* child,
+ bool* isAncestor) {
+ NS_ENSURE_ARG_POINTER(isAncestor);
+ nsresult rv = NS_OK;
+
+ int32_t count = mSubFolders.Count();
+
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ if (folder.get() == child)
+ *isAncestor = true;
+ else
+ folder->IsAncestorOf(child, isAncestor);
+
+ if (*isAncestor) return NS_OK;
+ }
+ *isAncestor = false;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GenerateUniqueSubfolderName(
+ const nsAString& prefix, nsIMsgFolder* otherFolder, nsAString& name) {
+ /* only try 256 times */
+ for (int count = 0; count < 256; count++) {
+ nsAutoString uniqueName;
+ uniqueName.Assign(prefix);
+ if (count > 0) {
+ uniqueName.AppendInt(count);
+ }
+ bool containsChild;
+ bool otherContainsChild = false;
+ ContainsChildNamed(uniqueName, &containsChild);
+ if (otherFolder)
+ otherFolder->ContainsChildNamed(uniqueName, &otherContainsChild);
+
+ if (!containsChild && !otherContainsChild) {
+ name = uniqueName;
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::UpdateSummaryTotals(bool force) {
+ if (!mNotifyCountChanges) return NS_OK;
+
+ int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+ // We need to read this info from the database
+ nsresult rv = ReadDBFolderInfo(force);
+
+ if (NS_SUCCEEDED(rv)) {
+ int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+
+ // Need to notify listeners that total count changed.
+ if (oldTotalMessages != newTotalMessages)
+ NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages,
+ newTotalMessages);
+
+ if (oldUnreadMessages != newUnreadMessages)
+ NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages,
+ newUnreadMessages);
+
+ FlushToFolderCache();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SummaryChanged() {
+ UpdateSummaryTotals(false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumUnread(bool deep, int32_t* numUnread) {
+ NS_ENSURE_ARG_POINTER(numUnread);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t total = isServer ? 0 : mNumUnreadMessages + mNumPendingUnreadMessages;
+
+ if (deep) {
+ if (total < 0) // deep search never returns negative counts
+ total = 0;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ int32_t num;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ folder->GetNumUnread(deep, &num);
+ total += num;
+ }
+ }
+ }
+ *numUnread = total;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetTotalMessages(bool deep,
+ int32_t* totalMessages) {
+ NS_ENSURE_ARG_POINTER(totalMessages);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t total = isServer ? 0 : mNumTotalMessages + mNumPendingTotalMessages;
+
+ if (deep) {
+ if (total < 0) // deep search never returns negative counts
+ total = 0;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ int32_t num;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ folder->GetTotalMessages(deep, &num);
+ total += num;
+ }
+ }
+ }
+ *totalMessages = total;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumPendingUnread(int32_t* aPendingUnread) {
+ *aPendingUnread = mNumPendingUnreadMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumPendingTotalMessages(
+ int32_t* aPendingTotal) {
+ *aPendingTotal = mNumPendingTotalMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingUnread(int32_t delta) {
+ if (delta) {
+ int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ mNumPendingUnreadMessages += delta;
+ int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ NS_ASSERTION(newUnreadMessages >= 0,
+ "shouldn't have negative unread message count");
+ if (newUnreadMessages >= 0) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo)
+ folderInfo->SetImapUnreadPendingMessages(mNumPendingUnreadMessages);
+ NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages,
+ newUnreadMessages);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingTotalMessages(int32_t delta) {
+ if (delta) {
+ int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+ mNumPendingTotalMessages += delta;
+ int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo)
+ folderInfo->SetImapTotalPendingMessages(mNumPendingTotalMessages);
+ NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages,
+ newTotalMessages);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetFlag(uint32_t flag) {
+ // If calling this function causes us to open the db (i.e., it was not
+ // open before), we're going to close the db before returning.
+ bool dbWasOpen = mDatabase != nullptr;
+
+ ReadDBFolderInfo(false);
+ // OnFlagChange can be expensive, so don't call it if we don't need to
+ bool flagSet;
+ nsresult rv;
+
+ if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv;
+
+ if (!flagSet) {
+ mFlags |= flag;
+ OnFlagChange(flag);
+ }
+ if (!dbWasOpen && mDatabase) SetMsgDatabase(nullptr);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ClearFlag(uint32_t flag) {
+ // OnFlagChange can be expensive, so don't call it if we don't need to
+ bool flagSet;
+ nsresult rv;
+
+ if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv;
+
+ if (flagSet) {
+ mFlags &= ~flag;
+ OnFlagChange(flag);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFlag(uint32_t flag, bool* _retval) {
+ *_retval = ((mFlags & flag) != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ToggleFlag(uint32_t flag) {
+ mFlags ^= flag;
+ OnFlagChange(flag);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnFlagChange(uint32_t flag) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+#ifdef DEBUG_bienvenu1
+ nsString name;
+ rv = GetName(name);
+ NS_ASSERTION(Compare(name, kLocalizedTrashName) ||
+ (mFlags & nsMsgFolderFlags::Trash),
+ "lost trash flag");
+#endif
+ folderInfo->SetFlags((int32_t)mFlags);
+ if (db) db->Commit(nsMsgDBCommitType::kLargeCommit);
+
+ if (mFlags & flag)
+ NotifyIntPropertyChanged(kFolderFlag, mFlags & ~flag, mFlags);
+ else
+ NotifyIntPropertyChanged(kFolderFlag, mFlags | flag, mFlags);
+
+ if (flag & nsMsgFolderFlags::Offline) {
+ bool newValue = mFlags & nsMsgFolderFlags::Offline;
+ rv = NotifyBoolPropertyChanged(kSynchronize, !newValue, !!newValue);
+ } else if (flag & nsMsgFolderFlags::Elided) {
+ bool newValue = mFlags & nsMsgFolderFlags::Elided;
+ rv = NotifyBoolPropertyChanged(kOpen, !!newValue, !newValue);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetFlags(uint32_t aFlags) {
+ if (mFlags != aFlags) {
+ uint32_t changedFlags = aFlags ^ mFlags;
+ mFlags = aFlags;
+ OnFlagChange(changedFlags);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFolderWithFlags(uint32_t aFlags,
+ nsIMsgFolder** aResult) {
+ if ((mFlags & aFlags) == aFlags) {
+ NS_ADDREF(*aResult = this);
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy); // initialize mSubFolders
+
+ int32_t count = mSubFolders.Count();
+ *aResult = nullptr;
+ for (int32_t i = 0; !*aResult && i < count; ++i)
+ mSubFolders[i]->GetFolderWithFlags(aFlags, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFoldersWithFlags(
+ uint32_t aFlags, nsTArray<RefPtr<nsIMsgFolder>>& aResult) {
+ aResult.Clear();
+
+ // Ensure initialisation of mSubFolders.
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy);
+
+ if ((mFlags & aFlags) == aFlags) {
+ aResult.AppendElement(this);
+ }
+
+ // Recurse down through children.
+ for (nsIMsgFolder* child : mSubFolders) {
+ nsTArray<RefPtr<nsIMsgFolder>> subMatches;
+ child->GetFoldersWithFlags(aFlags, subMatches);
+ aResult.AppendElements(subMatches);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsSpecialFolder(uint32_t aFlags,
+ bool aCheckAncestors,
+ bool* aIsSpecial) {
+ NS_ENSURE_ARG_POINTER(aIsSpecial);
+
+ if ((mFlags & aFlags) == 0) {
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ GetParent(getter_AddRefs(parentMsgFolder));
+
+ if (parentMsgFolder && aCheckAncestors)
+ parentMsgFolder->IsSpecialFolder(aFlags, aCheckAncestors, aIsSpecial);
+ else
+ *aIsSpecial = false;
+ } else {
+ // The user can set their INBOX to be their SENT folder.
+ // in that case, we want this folder to act like an INBOX,
+ // and not a SENT folder
+ *aIsSpecial = !((aFlags & nsMsgFolderFlags::SentMail) &&
+ (mFlags & nsMsgFolderFlags::Inbox));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+ *deletable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDisplayRecipients(bool* displayRecipients) {
+ *displayRecipients = false;
+ if (mFlags & nsMsgFolderFlags::SentMail &&
+ !(mFlags & nsMsgFolderFlags::Inbox))
+ *displayRecipients = true;
+ else if (mFlags & nsMsgFolderFlags::Queue)
+ *displayRecipients = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AcquireSemaphore(nsISupports* semHolder) {
+ nsresult rv = NS_OK;
+ if (mSemaphoreHolder == NULL)
+ mSemaphoreHolder = semHolder; // Don't AddRef due to ownership issues.
+ else
+ rv = NS_MSG_FOLDER_BUSY;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ReleaseSemaphore(nsISupports* semHolder) {
+ if (!mSemaphoreHolder || mSemaphoreHolder == semHolder)
+ mSemaphoreHolder = NULL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::TestSemaphore(nsISupports* semHolder,
+ bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = (mSemaphoreHolder == semHolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetLocked(bool* isLocked) {
+ *isLocked = mSemaphoreHolder != NULL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetRelativePathName(nsACString& pathName) {
+ pathName.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+ *size = kSizeUnknown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetSizeOnDisk(int64_t aSizeOnDisk) {
+ NotifyIntPropertyChanged(kFolderSize, mFolderSize, aSizeOnDisk);
+ mFolderSize = aSizeOnDisk;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetUsername(nsACString& userName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetUsername(userName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHostname(nsACString& hostName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetHostName(hostName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNewMessages(nsIMsgWindow*,
+ nsIUrlListener* /* aListener */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetBiffState(uint32_t* aBiffState) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetBiffState(aBiffState);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetBiffState(uint32_t aBiffState) {
+ uint32_t oldBiffState = nsMsgBiffState_Unknown;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) server->GetBiffState(&oldBiffState);
+
+ if (oldBiffState != aBiffState) {
+ // Get the server and notify it and not inbox.
+ if (!mIsServer) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetRootFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) return folder->SetBiffState(aBiffState);
+ }
+ if (server) server->SetBiffState(aBiffState);
+
+ NotifyIntPropertyChanged(kBiffState, oldBiffState, aBiffState);
+ } else if (aBiffState == oldBiffState &&
+ aBiffState == nsMsgBiffState_NewMail) {
+ // The folder has been updated, so update the MRUTime
+ SetMRUTime();
+ // biff is already set, but notify that there is additional new mail for the
+ // folder
+ NotifyIntPropertyChanged(kNewMailReceived, 0, mNumNewBiffMessages);
+ } else if (aBiffState == nsMsgBiffState_NoMail) {
+ // even if the old biff state equals the new biff state, it is still
+ // possible that we've never cleared the number of new messages for this
+ // particular folder. This happens when the new mail state got cleared by
+ // viewing a new message in folder that is different from this one. Biff
+ // state is stored per server
+ // the num. of new messages is per folder.
+ SetNumNewMessages(0);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumNewMessages(bool deep,
+ int32_t* aNumNewMessages) {
+ NS_ENSURE_ARG_POINTER(aNumNewMessages);
+
+ int32_t numNewMessages = (!deep || !(mFlags & nsMsgFolderFlags::Virtual))
+ ? mNumNewBiffMessages
+ : 0;
+ if (deep) {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ int32_t num;
+ mSubFolders[i]->GetNumNewMessages(deep, &num);
+ if (num > 0) // it's legal for counts to be negative if we don't know
+ numNewMessages += num;
+ }
+ }
+ *aNumNewMessages = numNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetNumNewMessages(int32_t aNumNewMessages) {
+ if (aNumNewMessages != mNumNewBiffMessages) {
+ int32_t oldNumMessages = mNumNewBiffMessages;
+ mNumNewBiffMessages = aNumNewMessages;
+
+ nsAutoCString oldNumMessagesStr;
+ oldNumMessagesStr.AppendInt(oldNumMessages);
+ nsAutoCString newNumMessagesStr;
+ newNumMessagesStr.AppendInt(aNumNewMessages);
+ NotifyPropertyChanged(kNumNewBiffMessages, oldNumMessagesStr,
+ newNumMessagesStr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetRootFolder(nsIMsgFolder** aRootFolder) {
+ NS_ENSURE_ARG_POINTER(aRootFolder);
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetRootMsgFolder(aRootFolder);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetFilePath(nsIFile* aFile) {
+ mPath = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetFilePath(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsresult rv;
+ // make a new nsIFile object in case the caller
+ // alters the underlying file object.
+ nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mPath) {
+ rv = parseURI(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = file->InitWithFile(mPath);
+ file.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSummaryFile(nsIFile** aSummaryFile) {
+ NS_ENSURE_ARG_POINTER(aSummaryFile);
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> newSummaryLocation =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation->InitWithFile(pathFile);
+
+ nsString fileName;
+ rv = newSummaryLocation->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ fileName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = newSummaryLocation->SetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation.forget(aSummaryFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages,
+ bool markRead) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto message : messages) {
+ rv = message->MarkRead(markRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkMessagesFlagged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markFlagged) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto message : messages) {
+ rv = message->MarkFlagged(markFlagged);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetJunkScoreForMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& junkScore) {
+ GetDatabase();
+ if (mDatabase) {
+ for (auto message : aMessages) {
+ nsMsgKey msgKey;
+ (void)message->GetMessageKey(&msgKey);
+ mDatabase->SetStringProperty(msgKey, "junkscore", junkScore);
+ mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "filter"_ns);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::ApplyRetentionSettings() { return ApplyRetentionSettings(true); }
+
+nsresult nsMsgDBFolder::ApplyRetentionSettings(bool deleteViaFolder) {
+ if (mFlags & nsMsgFolderFlags::Virtual) // ignore virtual folders.
+ return NS_OK;
+ bool weOpenedDB = !mDatabase;
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings;
+ nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings));
+ if (NS_SUCCEEDED(rv)) {
+ nsMsgRetainByPreference retainByPreference =
+ nsIMsgRetentionSettings::nsMsgRetainAll;
+
+ retentionSettings->GetRetainByPreference(&retainByPreference);
+ if (retainByPreference != nsIMsgRetentionSettings::nsMsgRetainAll) {
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mDatabase)
+ rv = mDatabase->ApplyRetentionSettings(retentionSettings,
+ deleteViaFolder);
+ }
+ }
+ // we don't want applying retention settings to keep the db open, because
+ // if we try to purge a bunch of folders, that will leave the dbs all open.
+ // So if we opened the db, close it.
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyMessages(nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener, bool isFolder,
+ bool allowUndo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder,
+ nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* messageToReplace,
+ bool isDraftOrTemplate, uint32_t aNewMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CopyDataToOutputStreamForAppend(
+ nsIInputStream* aInStream, int32_t aLength,
+ nsIOutputStream* aOutputStream) {
+ if (!aInStream) return NS_OK;
+
+ uint32_t uiWritten;
+ return aOutputStream->WriteFrom(aInStream, aLength, &uiWritten);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CopyDataDone() { return NS_OK; }
+
+#define NOTIFY_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter( \
+ mListeners); \
+ nsCOMPtr<nsIFolderListener> listener; \
+ while (iter.HasMore()) { \
+ listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyPropertyChanged(const nsACString& aProperty,
+ const nsACString& aOldValue,
+ const nsACString& aNewValue) {
+ NOTIFY_LISTENERS(OnFolderPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderPropertyChanged(this, aProperty,
+ aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyUnicharPropertyChanged(const nsACString& aProperty,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ NOTIFY_LISTENERS(OnFolderUnicharPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderUnicharPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyIntPropertyChanged(const nsACString& aProperty,
+ int64_t aOldValue, int64_t aNewValue) {
+ // Don't send off count notifications if they are turned off.
+ if (!mNotifyCountChanges && (aProperty.Equals(kTotalMessages) ||
+ aProperty.Equals(kTotalUnreadMessages)))
+ return NS_OK;
+
+ NOTIFY_LISTENERS(OnFolderIntPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderIntPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyBoolPropertyChanged(const nsACString& aProperty,
+ bool aOldValue, bool aNewValue) {
+ NOTIFY_LISTENERS(OnFolderBoolPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderBoolPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyPropertyFlagChanged(nsIMsgDBHdr* aItem,
+ const nsACString& aProperty,
+ uint32_t aOldValue,
+ uint32_t aNewValue) {
+ NOTIFY_LISTENERS(OnFolderPropertyFlagChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderPropertyFlagChanged(
+ aItem, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyMessageAdded(nsIMsgDBHdr* msg) {
+ // Notify our directly-registered listeners.
+ NOTIFY_LISTENERS(OnMessageAdded, (this, msg));
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderListenerManager->OnMessageAdded(this, msg);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::NotifyMessageRemoved(nsIMsgDBHdr* msg) {
+ // Notify our directly-registered listeners.
+ NOTIFY_LISTENERS(OnMessageRemoved, (this, msg));
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderListenerManager->OnMessageRemoved(this, msg);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyFolderAdded(nsIMsgFolder* child) {
+ NOTIFY_LISTENERS(OnFolderAdded, (this, child));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderAdded(this, child);
+}
+
+nsresult nsMsgDBFolder::NotifyFolderRemoved(nsIMsgFolder* child) {
+ NOTIFY_LISTENERS(OnFolderRemoved, (this, child));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderRemoved(this, child);
+}
+
+nsresult nsMsgDBFolder::NotifyFolderEvent(const nsACString& aEvent) {
+ NOTIFY_LISTENERS(OnFolderEvent, (this, aEvent));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderEvent(this, aEvent);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetFilterList(nsIMsgFilterList* aFilterList) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetFilterList(aFilterList);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetEditableFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetEditableFilterList(aFilterList);
+}
+
+/* void enableNotifications (in long notificationType, in boolean enable); */
+NS_IMETHODIMP nsMsgDBFolder::EnableNotifications(int32_t notificationType,
+ bool enable) {
+ if (notificationType == nsIMsgFolder::allMessageCountNotifications) {
+ mNotifyCountChanges = enable;
+ if (enable) {
+ UpdateSummaryTotals(true);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMessageHeader(nsMsgKey msgKey,
+ nsIMsgDBHdr** aMsgHdr) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ nsCOMPtr<nsIMsgDatabase> database;
+ nsresult rv = GetMsgDatabase(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return (database) ? database->GetMsgHdrForKey(msgKey, aMsgHdr)
+ : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDescendants(
+ nsTArray<RefPtr<nsIMsgFolder>>& aDescendants) {
+ aDescendants.Clear();
+ for (nsIMsgFolder* child : mSubFolders) {
+ aDescendants.AppendElement(child);
+ nsTArray<RefPtr<nsIMsgFolder>> grandchildren;
+ child->GetDescendants(grandchildren);
+ aDescendants.AppendElements(grandchildren);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetBaseMessageURI(nsACString& baseMessageURI) {
+ if (mBaseMessageURI.IsEmpty()) return NS_ERROR_FAILURE;
+ baseMessageURI = mBaseMessageURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetUriForMsg(nsIMsgDBHdr* msgHdr,
+ nsACString& aURI) {
+ NS_ENSURE_ARG(msgHdr);
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ nsAutoCString uri;
+ uri.Assign(mBaseMessageURI);
+
+ // append a "#" followed by the message key.
+ uri.Append('#');
+ uri.AppendInt(msgKey);
+ aURI = uri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GenerateMessageURI(nsMsgKey msgKey,
+ nsACString& aURI) {
+ nsCString uri;
+ nsresult rv = GetBaseMessageURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // append a "#" followed by the message key.
+ uri.Append('#');
+ uri.AppendInt(msgKey);
+ aURI = uri;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::GetBaseStringBundle(nsIStringBundle** aBundle) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ bundleService->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ bundle.forget(aBundle);
+ return NS_OK;
+}
+
+// Do not use this routine if you have to call it very often because
+// it creates a new bundle each time
+nsresult nsMsgDBFolder::GetStringFromBundle(const char* msgName,
+ nsString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle)
+ rv = bundle->GetStringFromName(msgName, aResult);
+ return rv;
+}
+
+nsresult nsMsgDBFolder::ThrowConfirmationPrompt(nsIMsgWindow* msgWindow,
+ const nsAString& confirmString,
+ bool* confirmed) {
+ if (msgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog && !confirmString.IsEmpty())
+ dialog->Confirm(nullptr, nsString(confirmString).get(), confirmed);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetStringWithFolderNameFromBundle(const char* msgName,
+ nsAString& aResult) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle) {
+ nsString folderName;
+ GetName(folderName);
+ AutoTArray<nsString, 2> formatStrings = {folderName,
+ kLocalizedBrandShortName};
+ nsString resultStr;
+ rv = bundle->FormatStringFromName(msgName, formatStrings, resultStr);
+ if (NS_SUCCEEDED(rv)) aResult.Assign(resultStr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ConfirmFolderDeletionForFilter(
+ nsIMsgWindow* msgWindow, bool* confirmed) {
+ nsString confirmString;
+ nsresult rv = GetStringWithFolderNameFromBundle(
+ "confirmFolderDeletionForFilter", confirmString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ThrowConfirmationPrompt(msgWindow, confirmString, confirmed);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ThrowAlertMsg(const char* msgName,
+ nsIMsgWindow* msgWindow) {
+ if (!msgWindow) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Assemble a pretty folder identifier, e.g. "Trash on bob@example.com".
+ nsAutoString ident;
+ nsAutoString folderName;
+ GetName(folderName);
+ nsAutoString serverName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server)))) {
+ server->GetPrettyName(serverName);
+ bundle->FormatStringFromName("verboseFolderFormat",
+ {folderName, serverName}, ident);
+ }
+ if (ident.IsEmpty()) {
+ ident = folderName; // Fallback, just in case.
+ }
+
+ // Format the actual error message (NOTE: not all error messages use the
+ // params - extra values are just ignored).
+ nsAutoString alertString;
+ rv = bundle->FormatStringFromName(msgName, {ident, kLocalizedBrandShortName},
+ alertString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Include the folder identifier in the alert title for good measure,
+ // because not all the error messages include the folder.
+ nsAutoString title;
+ bundle->FormatStringFromName("folderErrorAlertTitle", {ident}, title);
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ rv = msgWindow->GetDomWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dlgService->Alert(domWindow, title.IsEmpty() ? nullptr : title.get(),
+ alertString.get());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AlertFilterChanged(nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG(msgWindow);
+ nsresult rv = NS_OK;
+ bool checkBox = false;
+ GetWarnFilterChanged(&checkBox);
+ if (!checkBox) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ nsString alertString;
+ rv = GetStringFromBundle("alertFilterChanged", alertString);
+ nsString alertCheckbox;
+ rv = GetStringFromBundle("alertFilterCheckbox", alertCheckbox);
+ if (!alertString.IsEmpty() && !alertCheckbox.IsEmpty() && docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog) {
+ dialog->AlertCheck(nullptr, alertString.get(), alertCheckbox.get(),
+ &checkBox);
+ SetWarnFilterChanged(checkBox);
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetWarnFilterChanged(bool* aVal) {
+ NS_ENSURE_ARG(aVal);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = prefBranch->GetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal);
+ if (NS_FAILED(rv)) *aVal = false;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::SetWarnFilterChanged(bool aVal) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->SetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyCompactCompleted() {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CloseDBIfFolderNotOpen(bool aForceClosed) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool folderOpen;
+ session->IsFolderOpenInWindow(this, &folderOpen);
+ if (!folderOpen &&
+ !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox))) {
+ if (aForceClosed && mDatabase) mDatabase->ForceClosed();
+ SetMsgDatabase(nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetSortOrder(int32_t order) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSortOrder(int32_t* order) {
+ NS_ENSURE_ARG_POINTER(order);
+
+ uint32_t flags;
+ nsresult rv = GetFlags(&flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (flags & nsMsgFolderFlags::Inbox)
+ *order = 0;
+ else if (flags & nsMsgFolderFlags::Drafts)
+ *order = 1;
+ else if (flags & nsMsgFolderFlags::Templates)
+ *order = 2;
+ else if (flags & nsMsgFolderFlags::SentMail)
+ *order = 3;
+ else if (flags & nsMsgFolderFlags::Archive)
+ *order = 4;
+ else if (flags & nsMsgFolderFlags::Junk)
+ *order = 5;
+ else if (flags & nsMsgFolderFlags::Trash)
+ *order = 6;
+ else if (flags & nsMsgFolderFlags::Virtual)
+ *order = 7;
+ else if (flags & nsMsgFolderFlags::Queue)
+ *order = 8;
+ else
+ *order = 9;
+
+ return NS_OK;
+}
+
+// static Helper function for CompareSortKeys().
+// Builds a collation key for a given folder based on "{sortOrder}{name}"
+nsresult nsMsgDBFolder::BuildFolderSortKey(nsIMsgFolder* aFolder,
+ nsTArray<uint8_t>& aKey) {
+ aKey.Clear();
+ int32_t order;
+ nsresult rv = aFolder->GetSortOrder(&order);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString orderString;
+ orderString.AppendInt(order);
+ nsString folderName;
+ rv = aFolder->GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ orderString.Append(folderName);
+ NS_ENSURE_TRUE(gCollationKeyGenerator, NS_ERROR_NULL_POINTER);
+
+ nsTArrayU8Buffer buffer(aKey);
+
+ auto result = gCollationKeyGenerator->GetSortKey(orderString, buffer);
+ NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CompareSortKeys(nsIMsgFolder* aFolder,
+ int32_t* sortOrder) {
+ nsTArray<uint8_t> sortKey1;
+ nsTArray<uint8_t> sortKey2;
+ nsresult rv = BuildFolderSortKey(this, sortKey1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = BuildFolderSortKey(aFolder, sortKey2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *sortOrder = gCollationKeyGenerator->CompareSortKeys(sortKey1, sortKey2);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::FetchMsgPreviewText(
+ nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) {
+ NS_ENSURE_ARG_POINTER(aAsyncResults);
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMsgTextFromStream(
+ nsIInputStream* stream, const nsACString& aCharset, uint32_t bytesToRead,
+ uint32_t aMaxOutputLen, bool aCompressQuotes, bool aStripHTMLTags,
+ nsACString& aContentType, nsACString& aMsgText) {
+ /*
+ 1. non mime message - the message body starts after the blank line
+ following the headers.
+ 2. mime message, multipart/alternative - we could simply scan for the
+ boundary line, advance past its headers, and treat the next few lines
+ as the text.
+ 3. mime message, text/plain - body follows headers
+ 4. multipart/mixed - scan past boundary, treat next part as body.
+ */
+
+ UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>);
+
+ nsAutoCString msgText;
+ nsAutoString contentType;
+ nsAutoString encoding;
+ nsAutoCString curLine;
+ nsAutoCString charset(aCharset);
+
+ // might want to use a state var instead of bools.
+ bool msgBodyIsHtml = false;
+ bool more = true;
+ bool reachedEndBody = false;
+ bool isBase64 = false;
+ bool inMsgBody = false;
+ bool justPassedEndBoundary = false;
+
+ uint32_t bytesRead = 0;
+
+ nsresult rv;
+
+ // Both are used to extract data from the headers
+ nsCOMPtr<nsIMimeHeaders> mimeHeaders(
+ do_CreateInstance(NS_IMIMEHEADERS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMIMEHeaderParam> mimeHdrParam(
+ do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Stack of boundaries, used to figure out where we are
+ nsTArray<nsCString> boundaryStack;
+
+ while (!inMsgBody && bytesRead <= bytesToRead) {
+ nsAutoCString msgHeaders;
+ // We want to NS_ReadLine until we get to a blank line (the end of the
+ // headers)
+ while (more) {
+ rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (curLine.IsEmpty()) break;
+ msgHeaders.Append(curLine);
+ msgHeaders.AppendLiteral("\r\n");
+ bytesRead += curLine.Length();
+ if (bytesRead > bytesToRead) break;
+ }
+
+ // There's no point in processing if we can't get the body
+ if (bytesRead > bytesToRead) break;
+
+ // Process the headers, looking for things we need
+ rv = mimeHeaders->Initialize(msgHeaders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contentTypeHdr;
+ mimeHeaders->ExtractHeader("Content-Type", false, contentTypeHdr);
+
+ // Get the content type
+ // If we don't have a content type, then we assign text/plain
+ // this is in violation of the RFC for multipart/digest, though
+ // Also, if we've just passed an end boundary, we're going to ignore this.
+ if (!justPassedEndBoundary && contentTypeHdr.IsEmpty())
+ contentType.AssignLiteral(u"text/plain");
+ else
+ mimeHdrParam->GetParameter(contentTypeHdr, nullptr, EmptyCString(), false,
+ nullptr, contentType);
+
+ justPassedEndBoundary = false;
+
+ // If we are multipart, then we need to get the boundary
+ if (StringBeginsWith(contentType, u"multipart/"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ nsAutoString boundaryParam;
+ mimeHdrParam->GetParameter(contentTypeHdr, "boundary", EmptyCString(),
+ false, nullptr, boundaryParam);
+ if (!boundaryParam.IsEmpty()) {
+ nsAutoCString boundary("--"_ns);
+ boundary.Append(NS_ConvertUTF16toUTF8(boundaryParam));
+ boundaryStack.AppendElement(boundary);
+ }
+ }
+
+ // If we are message/rfc822, then there's another header block coming up
+ else if (contentType.LowerCaseEqualsLiteral("message/rfc822"))
+ continue;
+
+ // If we are a text part, then we want it
+ else if (StringBeginsWith(contentType, u"text/"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ inMsgBody = true;
+
+ if (contentType.LowerCaseEqualsLiteral("text/html")) msgBodyIsHtml = true;
+
+ // Also get the charset if required
+ if (charset.IsEmpty()) {
+ nsAutoString charsetW;
+ mimeHdrParam->GetParameter(contentTypeHdr, "charset", EmptyCString(),
+ false, nullptr, charsetW);
+ charset.Assign(NS_ConvertUTF16toUTF8(charsetW));
+ }
+
+ // Finally, get the encoding
+ nsAutoCString encodingHdr;
+ mimeHeaders->ExtractHeader("Content-Transfer-Encoding", false,
+ encodingHdr);
+ if (!encodingHdr.IsEmpty())
+ mimeHdrParam->GetParameter(encodingHdr, nullptr, EmptyCString(), false,
+ nullptr, encoding);
+
+ if (encoding.LowerCaseEqualsLiteral(ENCODING_BASE64)) isBase64 = true;
+ }
+
+ // We need to consume the rest, until the next headers
+ uint32_t count = boundaryStack.Length();
+ nsAutoCString boundary;
+ nsAutoCString endBoundary;
+ if (count) {
+ boundary.Assign(boundaryStack.ElementAt(count - 1));
+ endBoundary.Assign(boundary);
+ endBoundary.AppendLiteral("--");
+ }
+ while (more) {
+ rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (count) {
+ // If we've reached a MIME final delimiter, pop and break
+ if (StringBeginsWith(curLine, endBoundary)) {
+ if (inMsgBody) reachedEndBody = true;
+ boundaryStack.RemoveElementAt(count - 1);
+ justPassedEndBoundary = true;
+ break;
+ }
+ // If we've reached the end of this MIME part, we can break
+ if (StringBeginsWith(curLine, boundary)) {
+ if (inMsgBody) reachedEndBody = true;
+ break;
+ }
+ }
+
+ // Only append the text if we're actually in the message body
+ if (inMsgBody) {
+ msgText.Append(curLine);
+ if (!isBase64) msgText.AppendLiteral("\r\n");
+ }
+
+ bytesRead += curLine.Length();
+ if (bytesRead > bytesToRead) break;
+ }
+ }
+ lineBuffer.reset();
+
+ // if the snippet is encoded, decode it
+ if (!encoding.IsEmpty())
+ decodeMsgSnippet(NS_ConvertUTF16toUTF8(encoding), !reachedEndBody, msgText);
+
+ // In order to turn our snippet into unicode, we need to convert it from the
+ // charset we detected earlier.
+ nsString unicodeMsgBodyStr;
+ nsMsgI18NConvertToUnicode(charset, msgText, unicodeMsgBodyStr);
+
+ // now we've got a msg body. If it's html, convert it to plain text.
+ if (msgBodyIsHtml && aStripHTMLTags)
+ ConvertMsgSnippetToPlainText(unicodeMsgBodyStr, unicodeMsgBodyStr);
+
+ // We want to remove any whitespace from the beginning and end of the string
+ unicodeMsgBodyStr.Trim(" \t\r\n", true, true);
+
+ // step 3, optionally remove quoted text from the snippet
+ nsString compressedQuotesMsgStr;
+ if (aCompressQuotes)
+ compressQuotesInMsgSnippet(unicodeMsgBodyStr, compressedQuotesMsgStr);
+
+ // now convert back to utf-8 which is more convenient for storage
+ CopyUTF16toUTF8(aCompressQuotes ? compressedQuotesMsgStr : unicodeMsgBodyStr,
+ aMsgText);
+
+ // finally, truncate the string based on aMaxOutputLen
+ if (aMsgText.Length() > aMaxOutputLen) {
+ if (NS_IsAscii(aMsgText.BeginReading()))
+ aMsgText.SetLength(aMaxOutputLen);
+ else
+ nsMsgI18NShrinkUTF8Str(nsCString(aMsgText), aMaxOutputLen, aMsgText);
+ }
+
+ // Also assign the content type being returned
+ aContentType.Assign(NS_ConvertUTF16toUTF8(contentType));
+ return rv;
+}
+
+/**
+ * decodeMsgSnippet - helper function which applies the appropriate transfer
+ * decoding to the message snippet based on aEncodingType. Currently handles
+ * base64 and quoted-printable. If aEncodingType refers to an encoding we
+ * don't handle, the message data is passed back unmodified.
+ * @param aEncodingType the encoding type (base64, quoted-printable)
+ * @param aIsComplete the snippet is actually the entire message so the
+ * decoder doesn't have to worry about partial data
+ * @param aMsgSnippet in/out argument. The encoded msg snippet and then the
+ * decoded snippet
+ */
+void nsMsgDBFolder::decodeMsgSnippet(const nsACString& aEncodingType,
+ bool aIsComplete, nsCString& aMsgSnippet) {
+ if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_BASE64)) {
+ int32_t base64Len = aMsgSnippet.Length();
+ if (aIsComplete) base64Len -= base64Len % 4;
+ char* decodedBody = PL_Base64Decode(aMsgSnippet.get(), base64Len, nullptr);
+ if (decodedBody) aMsgSnippet.Adopt(decodedBody);
+ } else if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_QUOTED_PRINTABLE)) {
+ MsgStripQuotedPrintable(aMsgSnippet);
+ }
+}
+
+/**
+ * stripQuotesFromMsgSnippet - Reduces quoted reply text including the citation
+ * (Scott wrote:) from the message snippet to " ... ". Assumes the snippet has
+ * been decoded and converted to plain text.
+ * @param aMsgSnippet in/out argument. The string to strip quotes from.
+ */
+void nsMsgDBFolder::compressQuotesInMsgSnippet(const nsString& aMsgSnippet,
+ nsAString& aCompressedQuotes) {
+ int32_t msgBodyStrLen = aMsgSnippet.Length();
+ bool lastLineWasAQuote = false;
+ int32_t offset = 0;
+ int32_t lineFeedPos = 0;
+ while (offset < msgBodyStrLen) {
+ lineFeedPos = aMsgSnippet.FindChar('\n', offset);
+ if (lineFeedPos != -1) {
+ const nsAString& currentLine =
+ Substring(aMsgSnippet, offset, lineFeedPos - offset);
+ // this catches quoted text ("> "), nested quotes of any level (">> ",
+ // ">>> ", ...) it also catches empty line quoted text (">"). It might be
+ // over aggressive and require tweaking later. Try to strip the citation.
+ // If the current line ends with a ':' and the next line looks like a
+ // quoted reply (starts with a ">") skip the current line
+ if (StringBeginsWith(currentLine, u">"_ns) ||
+ (lineFeedPos + 1 < msgBodyStrLen && lineFeedPos &&
+ aMsgSnippet[lineFeedPos - 1] == char16_t(':') &&
+ aMsgSnippet[lineFeedPos + 1] == char16_t('>'))) {
+ lastLineWasAQuote = true;
+ } else if (!currentLine.IsEmpty()) {
+ if (lastLineWasAQuote) {
+ aCompressedQuotes += u" ... "_ns;
+ lastLineWasAQuote = false;
+ }
+
+ aCompressedQuotes += currentLine;
+ // Don't forget to substitute a space for the line feed.
+ aCompressedQuotes += char16_t(' ');
+ }
+
+ offset = lineFeedPos + 1;
+ } else {
+ aCompressedQuotes.Append(
+ Substring(aMsgSnippet, offset, msgBodyStrLen - offset));
+ break;
+ }
+ }
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ConvertMsgSnippetToPlainText(
+ const nsAString& aMessageText, nsAString& aOutText) {
+ uint32_t flags = nsIDocumentEncoder::OutputLFLineBreak |
+ nsIDocumentEncoder::OutputNoScriptContent |
+ nsIDocumentEncoder::OutputNoFramesContent |
+ nsIDocumentEncoder::OutputBodyOnly;
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(aMessageText, flags, 80, aOutText);
+}
+
+nsresult nsMsgDBFolder::GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr,
+ nsIInputStream* stream) {
+ nsCString msgBody;
+ nsAutoCString charset;
+ msgHdr->GetCharset(getter_Copies(charset));
+ nsAutoCString contentType;
+ nsresult rv = GetMsgTextFromStream(stream, charset, 4096, 255, true, true,
+ contentType, msgBody);
+ // replaces all tabs and line returns with a space,
+ // then trims off leading and trailing white space
+ msgBody.CompressWhitespace();
+ msgHdr->SetStringProperty("preview", msgBody);
+ return rv;
+}
+
+void nsMsgDBFolder::UpdateTimestamps(bool allowUndo) {
+ if (!(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) {
+ SetMRUTime();
+ if (allowUndo) // This is a proxy for a user-initiated act.
+ {
+ bool isArchive;
+ IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isArchive);
+ if (!isArchive) {
+ SetMRMTime();
+ NotifyFolderEvent(kMRMTimeChanged);
+ }
+ }
+ }
+}
+
+void nsMsgDBFolder::SetMRUTime() {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ SetStringProperty(MRU_TIME_PROPERTY, nowStr);
+}
+
+void nsMsgDBFolder::SetMRMTime() {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ SetStringProperty(MRM_TIME_PROPERTY, nowStr);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = NS_OK;
+ GetDatabase();
+ if (mDatabase) {
+ nsCString keywords;
+
+ for (auto message : aMessages) {
+ message->GetStringProperty("keywords", keywords);
+ nsTArray<nsCString> keywordArray;
+ ParseString(aKeywords, ' ', keywordArray);
+ uint32_t addCount = 0;
+ for (uint32_t j = 0; j < keywordArray.Length(); j++) {
+ int32_t start, length;
+ if (!MsgFindKeyword(keywordArray[j], keywords, &start, &length)) {
+ if (!keywords.IsEmpty()) keywords.Append(' ');
+ keywords.Append(keywordArray[j]);
+ addCount++;
+ }
+ }
+ // avoid using the message key to set the string property, because
+ // in the case of filters running on incoming pop3 mail with quarantining
+ // turned on, the message key is wrong.
+ mDatabase->SetStringPropertyByHdr(message, "keywords", keywords);
+
+ if (addCount) NotifyPropertyFlagChanged(message, kKeywords, 0, addCount);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = NS_OK;
+ GetDatabase();
+ if (mDatabase) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<nsCString> keywordArray;
+ ParseString(aKeywords, ' ', keywordArray);
+ nsCString keywords;
+ // If the tag is also a label, we should remove the label too...
+
+ for (auto message : aMessages) {
+ rv = message->GetStringProperty("keywords", keywords);
+ uint32_t removeCount = 0;
+ for (uint32_t j = 0; j < keywordArray.Length(); j++) {
+ int32_t startOffset, length;
+ if (MsgFindKeyword(keywordArray[j], keywords, &startOffset, &length)) {
+ // delete any leading space delimiters
+ while (startOffset && keywords.CharAt(startOffset - 1) == ' ') {
+ startOffset--;
+ length++;
+ }
+ // but if the keyword is at the start then delete the following space
+ if (!startOffset &&
+ length < static_cast<int32_t>(keywords.Length()) &&
+ keywords.CharAt(length) == ' ')
+ length++;
+ keywords.Cut(startOffset, length);
+ removeCount++;
+ }
+ }
+
+ if (removeCount) {
+ mDatabase->SetStringPropertyByHdr(message, "keywords", keywords);
+ NotifyPropertyFlagChanged(message, kKeywords, removeCount, 0);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetCustomIdentity(nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ *aIdentity = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetProcessingFlags(nsMsgKey aKey,
+ uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = 0;
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (mProcessingFlag[i].keys && mProcessingFlag[i].keys->IsMember(aKey))
+ *aFlags |= mProcessingFlag[i].bit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OrProcessingFlags(nsMsgKey aKey, uint32_t mask) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (mProcessingFlag[i].bit & mask && mProcessingFlag[i].keys)
+ mProcessingFlag[i].keys->Add(aKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AndProcessingFlags(nsMsgKey aKey, uint32_t mask) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (!(mProcessingFlag[i].bit & mask) && mProcessingFlag[i].keys)
+ mProcessingFlag[i].keys->Remove(aKey);
+ return NS_OK;
+}
+
+// Each implementation must provide an override of this, connecting the folder
+// type to the corresponding incoming server type.
+NS_IMETHODIMP nsMsgDBFolder::GetIncomingServerType(
+ nsACString& aIncomingServerType) {
+ NS_ASSERTION(false, "subclasses need to override this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void nsMsgDBFolder::ClearProcessingFlags() {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) {
+ // There is no clear method so we need to delete and re-create.
+ delete mProcessingFlag[i].keys;
+ mProcessingFlag[i].keys = nsMsgKeySetU::Create();
+ }
+}
+
+nsresult nsMsgDBFolder::MessagesInKeyOrder(
+ nsTArray<nsMsgKey> const& aKeyArray, nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messages) {
+ messages.Clear();
+ messages.SetCapacity(aKeyArray.Length());
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ for (auto key : aKeyArray) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) messages.AppendElement(msgHdr);
+ }
+ }
+ return rv;
+}
+
+/* static */ nsMsgKeySetU* nsMsgKeySetU::Create() {
+ nsMsgKeySetU* set = new nsMsgKeySetU;
+ if (set) {
+ set->loKeySet = nsMsgKeySet::Create();
+ set->hiKeySet = nsMsgKeySet::Create();
+ if (!(set->loKeySet && set->hiKeySet)) {
+ delete set;
+ set = nullptr;
+ }
+ }
+ return set;
+}
+
+nsMsgKeySetU::nsMsgKeySetU() : hiKeySet(nullptr) {}
+
+nsMsgKeySetU::~nsMsgKeySetU() {}
+
+const uint32_t kLowerBits = 0x7fffffff;
+
+int nsMsgKeySetU::Add(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->Add(intKey);
+ return hiKeySet->Add(intKey & kLowerBits);
+}
+
+int nsMsgKeySetU::Remove(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->Remove(intKey);
+ return hiKeySet->Remove(intKey & kLowerBits);
+}
+
+bool nsMsgKeySetU::IsMember(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->IsMember(intKey);
+ return hiKeySet->IsMember(intKey & kLowerBits);
+}
+
+nsresult nsMsgKeySetU::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) {
+ nsresult rv = loKeySet->ToMsgKeyArray(aArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return hiKeySet->ToMsgKeyArray(aArray);
+}
diff --git a/comm/mailnews/base/src/nsMsgDBFolder.h b/comm/mailnews/base/src/nsMsgDBFolder.h
new file mode 100644
index 0000000000..1cd01199f6
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBFolder.h
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgDBFolder_h__
+#define nsMsgDBFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsIMsgFolder.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsCOMPtr.h"
+#include "nsIDBChangeListener.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIURL.h"
+#include "nsIFile.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIMsgFilterList.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgHdr.h"
+#include "nsIOutputStream.h"
+#include "nsITransport.h"
+#include "nsIStringBundle.h"
+#include "nsTObserverArray.h"
+#include "nsCOMArray.h"
+#include "nsMsgKeySet.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFilterPlugin.h"
+#include "mozilla/intl/Collator.h"
+
+// We declare strings for folder properties and events.
+// Properties:
+extern const nsLiteralCString kBiffState;
+extern const nsLiteralCString kCanFileMessages;
+extern const nsLiteralCString kDefaultServer;
+extern const nsLiteralCString kFlagged;
+extern const nsLiteralCString kFolderFlag;
+extern const nsLiteralCString kFolderSize;
+extern const nsLiteralCString kIsDeferred;
+extern const nsLiteralCString kIsSecure;
+extern const nsLiteralCString kJunkStatusChanged;
+extern const nsLiteralCString kKeywords;
+extern const nsLiteralCString kMRMTimeChanged;
+extern const nsLiteralCString kMsgLoaded;
+extern const nsLiteralCString kName;
+extern const nsLiteralCString kNewMailReceived;
+extern const nsLiteralCString kNewMessages;
+extern const nsLiteralCString kOpen;
+extern const nsLiteralCString kSortOrder;
+extern const nsLiteralCString kStatus;
+extern const nsLiteralCString kSynchronize;
+extern const nsLiteralCString kTotalMessages;
+extern const nsLiteralCString kTotalUnreadMessages;
+
+// Events:
+extern const nsLiteralCString kAboutToCompact;
+extern const nsLiteralCString kCompactCompleted;
+extern const nsLiteralCString kDeleteOrMoveMsgCompleted;
+extern const nsLiteralCString kDeleteOrMoveMsgFailed;
+extern const nsLiteralCString kFiltersApplied;
+extern const nsLiteralCString kFolderCreateCompleted;
+extern const nsLiteralCString kFolderCreateFailed;
+extern const nsLiteralCString kFolderLoaded;
+extern const nsLiteralCString kNumNewBiffMessages;
+extern const nsLiteralCString kRenameCompleted;
+
+using mozilla::intl::Collator;
+
+class nsIMsgFolderCacheElement;
+class nsMsgKeySetU;
+
+class nsMsgFolderService final : public nsIMsgFolderService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERSERVICE
+
+ nsMsgFolderService(){};
+
+ protected:
+ ~nsMsgFolderService(){};
+};
+
+/*
+ * nsMsgDBFolder
+ * class derived from nsMsgFolder for those folders that use an nsIMsgDatabase
+ */
+class nsMsgDBFolder : public nsSupportsWeakReference,
+ public nsIMsgFolder,
+ public nsIDBChangeListener,
+ public nsIUrlListener,
+ public nsIJunkMailClassificationListener,
+ public nsIMsgTraitClassificationListener {
+ public:
+ friend class nsMsgFolderService;
+
+ nsMsgDBFolder(void);
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGFOLDER
+ NS_DECL_NSIDBCHANGELISTENER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+ NS_DECL_NSIMSGTRAITCLASSIFICATIONLISTENER
+
+ NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element);
+ NS_IMETHOD ReadFromFolderCacheElem(nsIMsgFolderCacheElement* element);
+
+ nsresult CreateDirectoryForFolder(nsIFile** result);
+ nsresult CreateBackupDirectory(nsIFile** result);
+ nsresult GetBackupSummaryFile(nsIFile** result, const nsACString& newName);
+ nsresult GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr,
+ nsIInputStream* stream);
+ nsresult HandleAutoCompactEvent(nsIMsgWindow* aMsgWindow);
+ static int gIsEnglishApp;
+
+ protected:
+ virtual ~nsMsgDBFolder();
+
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI);
+
+ void compressQuotesInMsgSnippet(const nsString& aMessageText,
+ nsAString& aCompressedQuotesStr);
+ void decodeMsgSnippet(const nsACString& aEncodingType, bool aIsComplete,
+ nsCString& aMsgSnippet);
+
+ // helper routine to parse the URI and update member variables
+ nsresult parseURI(bool needServer = false);
+ nsresult GetBaseStringBundle(nsIStringBundle** aBundle);
+ nsresult GetStringFromBundle(const char* msgName, nsString& aResult);
+ nsresult ThrowConfirmationPrompt(nsIMsgWindow* msgWindow,
+ const nsAString& confirmString,
+ bool* confirmed);
+ nsresult GetWarnFilterChanged(bool* aVal);
+ nsresult SetWarnFilterChanged(bool aVal);
+ nsresult CreateCollationKey(const nsString& aSource, uint8_t** aKey,
+ uint32_t* aLength);
+
+ // All children will override this to create the right class of object.
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) = 0;
+ virtual nsresult ReadDBFolderInfo(bool force);
+ virtual nsresult FlushToFolderCache();
+ virtual nsresult GetDatabase() = 0;
+ virtual nsresult SendFlagNotifications(nsIMsgDBHdr* item, uint32_t oldFlags,
+ uint32_t newFlags);
+
+ // Overriden by IMAP to handle gmail hack.
+ virtual nsresult GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream);
+
+ nsresult CheckWithNewMessagesStatus(bool messageAdded);
+ void UpdateNewMessages();
+ nsresult OnHdrAddedOrDeleted(nsIMsgDBHdr* hdrChanged, bool added);
+ nsresult CreateFileForDB(const nsAString& userLeafName, nsIFile* baseDir,
+ nsIFile** dbFile);
+
+ nsresult GetFolderCacheKey(nsIFile** aFile);
+ nsresult GetFolderCacheElemFromFile(nsIFile* file,
+ nsIMsgFolderCacheElement** cacheElement);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult CheckIfFolderExists(const nsAString& newFolderName,
+ nsIMsgFolder* parentFolder,
+ nsIMsgWindow* msgWindow);
+ bool ConfirmAutoFolderRename(nsIMsgWindow* aMsgWindow,
+ const nsString& aOldName,
+ const nsString& aNewName);
+
+ // Returns true if: a) there is no need to prompt or b) the user is already
+ // logged in or c) the user logged in successfully.
+ static bool PromptForMasterPasswordIfNecessary();
+
+ // Offline support methods. Used by IMAP and News folders, but not local
+ // folders.
+ nsresult StartNewOfflineMessage();
+ nsresult WriteStartOfNewLocalMessage();
+ nsresult EndNewOfflineMessage(nsresult status);
+
+ nsresult AutoCompact(nsIMsgWindow* aWindow);
+ // this is a helper routine that ignores whether nsMsgMessageFlags::Offline is
+ // set for the folder
+ nsresult MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result);
+ nsresult GetPromptPurgeThreshold(bool* aPrompt);
+ nsresult GetPurgeThreshold(int32_t* aThreshold);
+ nsresult ApplyRetentionSettings(bool deleteViaFolder);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult AddMarkAllReadUndoAction(
+ nsIMsgWindow* msgWindow, nsMsgKey* thoseMarked, uint32_t numMarked);
+
+ nsresult PerformBiffNotifications(
+ void); // if there are new, non spam messages, do biff
+
+ // Helper function for Move code to call to update the MRU and MRM time.
+ void UpdateTimestamps(bool allowUndo);
+ void SetMRUTime();
+ void SetMRMTime();
+ /**
+ * Clear all processing flags, presumably because message keys are no longer
+ * valid.
+ */
+ void ClearProcessingFlags();
+
+ nsresult NotifyHdrsNotBeingClassified();
+ static nsresult BuildFolderSortKey(nsIMsgFolder* aFolder,
+ nsTArray<uint8_t>& aKey);
+ /**
+ * Produce an array of messages ordered like the input keys.
+ */
+ nsresult MessagesInKeyOrder(nsTArray<nsMsgKey> const& aKeyArray,
+ nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messages);
+ nsCString mURI;
+
+ nsCOMPtr<nsIMsgDatabase> mDatabase;
+ nsCOMPtr<nsIMsgDatabase> mBackupDatabase;
+ bool mAddListener;
+ bool mNewMessages;
+ bool mGettingNewMessages;
+ nsMsgKey mLastMessageLoaded;
+
+ /*
+ * Start of offline-message-writing vars.
+ * These track offline message writing for IMAP and News folders.
+ * But *not* for local folders, which do their own thing.
+ * They are set up by StartNewOfflineMessage() and cleaned up
+ * by EndNewOfflineMessage().
+ * IMAP folder also uses these vars when saving messages to disk.
+ */
+
+ // The header of the message currently being written.
+ nsCOMPtr<nsIMsgDBHdr> m_offlineHeader;
+ int32_t m_numOfflineMsgLines;
+ // Number of bytes added due to add X-Mozilla-* headers.
+ int32_t m_bytesAddedToLocalMsg;
+ // This is currently used when we do a save as of an imap or news message..
+ // Also used by IMAP/News offline messsage writing.
+ nsCOMPtr<nsIOutputStream> m_tempMessageStream;
+ // The number of bytes written to m_tempMessageStream so far.
+ uint32_t m_tempMessageStreamBytesWritten;
+
+ /*
+ * End of offline message tracking vars
+ */
+
+ nsCOMPtr<nsIMsgRetentionSettings> m_retentionSettings;
+ nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings;
+ static nsrefcnt mInstanceCount;
+
+ uint32_t mFlags;
+ nsWeakPtr mParent; // This won't be refcounted for ownership reasons.
+ int32_t mNumUnreadMessages; /* count of unread messages (-1 means unknown; -2
+ means unknown but we already tried to find
+ out.) */
+ int32_t mNumTotalMessages; /* count of existing messages. */
+ bool mNotifyCountChanges;
+ int64_t mExpungedBytes;
+ nsCOMArray<nsIMsgFolder> mSubFolders;
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>> mListeners;
+
+ bool mInitializedFromCache;
+ nsISupports* mSemaphoreHolder; // set when the folder is being written to
+ // Due to ownership issues, this won't be
+ // AddRef'd.
+
+ nsWeakPtr mServer;
+
+ // These values are used for tricking the front end into thinking that we have
+ // more messages than are really in the DB. This is usually after and IMAP
+ // message copy where we don't want to do an expensive select until the user
+ // actually opens that folder
+ int32_t mNumPendingUnreadMessages;
+ int32_t mNumPendingTotalMessages;
+ int64_t mFolderSize;
+
+ int32_t mNumNewBiffMessages;
+
+ // these are previous set of new msgs, which we might
+ // want to run junk controls on. This is in addition to "new" hdrs
+ // in the db, which might get cleared because the user clicked away
+ // from the folder.
+ nsTArray<nsMsgKey> m_saveNewMsgs;
+
+ // These are the set of new messages for a folder who has had
+ // its db closed, without the user reading the folder. This
+ // happens with pop3 mail filtered to a different local folder.
+ nsTArray<nsMsgKey> m_newMsgs;
+
+ //
+ // stuff from the uri
+ //
+ bool mHaveParsedURI; // is the URI completely parsed?
+ bool mIsServerIsValid;
+ bool mIsServer;
+ nsString mName;
+ nsString mOriginalName;
+ nsCOMPtr<nsIFile> mPath;
+ nsCString mBaseMessageURI; // The uri with the message scheme
+
+ // static stuff for cross-instance objects like atoms
+ static nsrefcnt gInstanceCount;
+
+ static nsresult initializeStrings();
+ static nsresult createCollationKeyGenerator();
+
+ static nsString kLocalizedInboxName;
+ static nsString kLocalizedTrashName;
+ static nsString kLocalizedSentName;
+ static nsString kLocalizedDraftsName;
+ static nsString kLocalizedTemplatesName;
+ static nsString kLocalizedUnsentName;
+ static nsString kLocalizedJunkName;
+ static nsString kLocalizedArchivesName;
+
+ static nsString kLocalizedBrandShortName;
+
+ static mozilla::UniquePtr<mozilla::intl::Collator> gCollationKeyGenerator;
+ static bool gInitializeStringsDone;
+
+ // store of keys that have a processing flag set
+ struct {
+ uint32_t bit;
+ nsMsgKeySetU* keys;
+ } mProcessingFlag[nsMsgProcessingFlags::NumberOfFlags];
+
+ // list of nsIMsgDBHdrs for messages to process post-bayes
+ nsTArray<RefPtr<nsIMsgDBHdr>> mPostBayesMessagesToFilter;
+
+ /**
+ * The list of message keys that have been classified for msgsClassified
+ * batch notification purposes. We add to this list in OnMessageClassified
+ * when we are told about a classified message (a URI is provided), and we
+ * notify for the list and clear it when we are told all the messages in
+ * the batch were classified (a URI is not provided).
+ */
+ nsTArray<nsMsgKey> mClassifiedMsgKeys;
+ // Is the current bayes filtering doing junk classification?
+ bool mBayesJunkClassifying;
+ // Is the current bayes filtering doing trait classification?
+ bool mBayesTraitClassifying;
+};
+
+// This class is a kludge to allow nsMsgKeySet to be used with uint32_t keys
+class nsMsgKeySetU {
+ public:
+ // Creates an empty set.
+ static nsMsgKeySetU* Create();
+ ~nsMsgKeySetU();
+ // IsMember() returns whether the given key is a member of this set.
+ bool IsMember(nsMsgKey key);
+ // Add() adds the given key to the set. (Returns 1 if a change was
+ // made, 0 if it was already there, and negative on error.)
+ int Add(nsMsgKey key);
+ // Remove() removes the given article from the set.
+ int Remove(nsMsgKey key);
+ // Add the keys in the set to aArray.
+ nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray);
+
+ protected:
+ nsMsgKeySetU();
+ RefPtr<nsMsgKeySet> loKeySet;
+ RefPtr<nsMsgKeySet> hiKeySet;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgDBView.cpp b/comm/mailnews/base/src/nsMsgDBView.cpp
new file mode 100644
index 0000000000..6532b52e0e
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBView.cpp
@@ -0,0 +1,7411 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <algorithm>
+#include "msgCore.h"
+#include "prmem.h"
+#include "nsArrayUtils.h"
+#include "nsIMsgCustomColumnHandler.h"
+#include "nsMsgDBView.h"
+#include "nsISupports.h"
+#include "nsIMsgFolder.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgFolder.h"
+#include "MailNewsTypes2.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsImapCore.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgCopyService.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgAccountManager.h"
+#include "nsTreeColumns.h"
+#include "nsTextFormatter.h"
+#include "nsIMimeConverter.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIPrompt.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMemory.h"
+#include "nsIAbManager.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "mozilla/Components.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsTArray.h"
+#include "mozilla/intl/OSPreferences.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+using namespace mozilla::mailnews;
+
+nsString nsMsgDBView::kHighestPriorityString;
+nsString nsMsgDBView::kHighPriorityString;
+nsString nsMsgDBView::kLowestPriorityString;
+nsString nsMsgDBView::kLowPriorityString;
+nsString nsMsgDBView::kNormalPriorityString;
+
+nsString nsMsgDBView::kReadString;
+nsString nsMsgDBView::kRepliedString;
+nsString nsMsgDBView::kForwardedString;
+nsString nsMsgDBView::kRedirectedString;
+nsString nsMsgDBView::kNewString;
+
+nsString nsMsgDBView::kTodayString;
+nsString nsMsgDBView::kYesterdayString;
+nsString nsMsgDBView::kLastWeekString;
+nsString nsMsgDBView::kTwoWeeksAgoString;
+nsString nsMsgDBView::kOldMailString;
+nsString nsMsgDBView::kFutureDateString;
+
+bool nsMsgDBView::m_dateFormatsInitialized = false;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatDefault = kDateFormatShort;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatThisWeek = kDateFormatShort;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatToday = kDateFormatNone;
+
+nsString nsMsgDBView::m_connectorPattern;
+nsCOMPtr<nsIStringBundle> nsMsgDBView::mMessengerStringBundle;
+
+static const uint32_t kMaxNumSortColumns = 2;
+
+static void GetCachedName(const nsCString& unparsedString,
+ int32_t displayVersion, nsACString& cachedName);
+
+static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field,
+ const nsAString& newName);
+
+// viewSortInfo is context data passed into the sort comparison functions -
+// FnSortIdUint32 for comparing numeric fields, FnSortIdKey for everything
+// else. If a comparison function finds two elements with equal primary
+// ordering, it'll call SecondaryCompare() to break the deadlock.
+// SecondaryCompare() uses the same comparison functions again, but using the
+// secondary key and potentially with different criteria (eg secondary sort
+// order might be different to primary). The viewSortInfo::isSecondarySort
+// flag lets the comparison function know not to call SecondaryCompare()
+// again (and again and again)...
+class viewSortInfo {
+ public:
+ nsMsgDBView* view;
+ nsIMsgDatabase* db; // Which db to use for collation compares.
+ bool isSecondarySort;
+ bool ascendingSort;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgDBViewService, nsIMsgDBViewService)
+NS_IMETHODIMP nsMsgDBViewService::InitializeDBViewStrings() {
+ nsMsgDBView::InitializeLiterals();
+ nsMsgDBView::m_connectorPattern.Truncate();
+ nsMsgDBView::mMessengerStringBundle = nullptr;
+ // Initialize date display format.
+ if (!nsMsgDBView::m_dateFormatsInitialized) {
+ nsMsgDBView::InitDisplayFormats();
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ADDREF(nsMsgDBView)
+NS_IMPL_RELEASE(nsMsgDBView)
+
+NS_INTERFACE_MAP_BEGIN(nsMsgDBView)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgDBView)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgDBView)
+ NS_INTERFACE_MAP_ENTRY(nsIDBChangeListener)
+ NS_INTERFACE_MAP_ENTRY(nsITreeView)
+ NS_INTERFACE_MAP_ENTRY(nsIJunkMailClassificationListener)
+NS_INTERFACE_MAP_END
+
+nsMsgDBView::nsMsgDBView() {
+ // Member initializers and constructor code.
+ m_sortValid = false;
+ m_checkedCustomColumns = false;
+ m_sortOrder = nsMsgViewSortOrder::none;
+ m_sortType = nsMsgViewSortType::byNone;
+ m_viewFlags = nsMsgViewFlagsType::kNone;
+ m_secondarySort = nsMsgViewSortType::byId;
+ m_secondarySortOrder = nsMsgViewSortOrder::ascending;
+ m_cachedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedViewIndex = nsMsgViewIndex_None;
+ mNumSelectedRows = 0;
+ mSuppressMsgDisplay = false;
+ mSuppressCommandUpdating = false;
+ mSuppressChangeNotification = false;
+ mSummarizeFailed = false;
+ mSelectionSummarized = false;
+
+ mIsNews = false;
+ mIsRss = false;
+ mIsXFVirtual = false;
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ m_deletingRows = false;
+ mNumMessagesRemainingInBatch = 0;
+ mShowSizeInLines = false;
+ mSortThreadsByRoot = false;
+
+ // mCommandsNeedDisablingBecauseOfSelection - A boolean that tell us if we
+ // needed to disable commands because of what's selected. If we're offline
+ // w/o a downloaded msg selected, or a dummy message was selected.
+ mCommandsNeedDisablingBecauseOfSelection = false;
+ mRemovingRow = false;
+ m_saveRestoreSelectionDepth = 0;
+ mRecentlyDeletedArrayIndex = 0;
+}
+
+void nsMsgDBView::InitializeLiterals() {
+ // Priority strings.
+ GetString(u"priorityHighest", kHighestPriorityString);
+ GetString(u"priorityHigh", kHighPriorityString);
+ GetString(u"priorityLowest", kLowestPriorityString);
+ GetString(u"priorityLow", kLowPriorityString);
+ GetString(u"priorityNormal", kNormalPriorityString);
+
+ GetString(u"read", kReadString);
+ GetString(u"replied", kRepliedString);
+ GetString(u"forwarded", kForwardedString);
+ GetString(u"redirected", kRedirectedString);
+ GetString(u"new", kNewString);
+
+ GetString(u"today", kTodayString);
+ GetString(u"yesterday", kYesterdayString);
+ GetString(u"last7Days", kLastWeekString);
+ GetString(u"last14Days", kTwoWeeksAgoString);
+ GetString(u"older", kOldMailString);
+ GetString(u"futureDate", kFutureDateString);
+}
+
+nsMsgDBView::~nsMsgDBView() {
+ if (m_db) m_db->RemoveListener(this);
+}
+
+// Helper function used to fetch strings from the messenger string bundle
+void nsMsgDBView::GetString(const char16_t* aStringName, nsAString& aValue) {
+ nsresult res = NS_ERROR_UNEXPECTED;
+
+ if (!nsMsgDBView::mMessengerStringBundle) {
+ static const char propertyURL[] = MESSENGER_STRING_URL;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+
+ if (sBundleService)
+ res = sBundleService->CreateBundle(
+ propertyURL, getter_AddRefs(nsMsgDBView::mMessengerStringBundle));
+ }
+
+ if (nsMsgDBView::mMessengerStringBundle)
+ res = mMessengerStringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(aStringName).get(), aValue);
+
+ if (NS_FAILED(res)) {
+ aValue = aStringName;
+ }
+}
+
+// Helper function used to fetch localized strings from the prefs
+nsresult nsMsgDBView::GetPrefLocalizedString(const char* aPrefName,
+ nsString& aResult) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ nsCOMPtr<nsIPrefLocalizedString> pls;
+ nsString ucsval;
+
+ prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->GetComplexValue(
+ aPrefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(pls));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pls->ToString(getter_Copies(ucsval));
+ aResult = ucsval.get();
+ return rv;
+}
+
+nsresult nsMsgDBView::AppendKeywordProperties(const nsACString& keywords,
+ nsAString& properties,
+ bool* tagAdded) {
+ *tagAdded = false;
+ // Get the top most keyword's CSS selector and append that as a property.
+ nsresult rv;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString topKey;
+ rv = mTagService->GetTopKey(keywords, topKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (topKey.IsEmpty()) return NS_OK;
+
+ nsString selector;
+ rv = mTagService->GetSelectorForKey(topKey, selector);
+ if (NS_SUCCEEDED(rv)) {
+ *tagAdded = true;
+ properties.Append(' ');
+ properties.Append(selector);
+ }
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// nsITreeView Implementation Methods (and helper methods)
+///////////////////////////////////////////////////////////////////////////
+
+static nsresult GetDisplayNameInAddressBook(const nsACString& emailAddress,
+ nsAString& displayName) {
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> cardForAddress;
+ rv = abManager->CardForEmailAddress(emailAddress,
+ getter_AddRefs(cardForAddress));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (cardForAddress) {
+ bool preferDisplayName = true;
+ rv = cardForAddress->GetPropertyAsBool("PreferDisplayName", true,
+ &preferDisplayName);
+
+ if (NS_FAILED(rv) || preferDisplayName)
+ rv = cardForAddress->GetDisplayName(displayName);
+ }
+
+ return rv;
+}
+
+/*
+ * The unparsedString has following format:
+ * "version|displayname"
+ */
+static void GetCachedName(const nsCString& unparsedString,
+ int32_t displayVersion, nsACString& cachedName) {
+ nsresult err;
+
+ // Get version #.
+ int32_t cachedVersion = unparsedString.ToInteger(&err);
+ if (cachedVersion != displayVersion) return;
+
+ // Get cached name.
+ int32_t pos = unparsedString.FindChar('|');
+ if (pos != kNotFound) cachedName = Substring(unparsedString, pos + 1);
+}
+
+static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field,
+ const nsAString& newName) {
+ nsCString newCachedName;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ int32_t currentDisplayNameVersion = 0;
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+
+ // Save version number.
+ newCachedName.AppendInt(currentDisplayNameVersion);
+ newCachedName.Append('|');
+
+ // Save name.
+ newCachedName.Append(NS_ConvertUTF16toUTF8(newName));
+
+ aHdr->SetStringProperty(header_field, newCachedName);
+}
+
+nsresult nsMsgDBView::FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aSenderString) {
+ nsCString unparsedAuthor;
+ bool showCondensedAddresses = false;
+ int32_t currentDisplayNameVersion = 0;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+ prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses);
+
+ aHdr->GetStringProperty("sender_name", unparsedAuthor);
+
+ // If the author is already computed, use it.
+ if (!unparsedAuthor.IsEmpty()) {
+ nsCString cachedDisplayName;
+ GetCachedName(unparsedAuthor, currentDisplayNameVersion, cachedDisplayName);
+ if (!cachedDisplayName.IsEmpty()) {
+ CopyUTF8toUTF16(cachedDisplayName, aSenderString);
+ return NS_OK;
+ }
+ }
+
+ nsCString author;
+ (void)aHdr->GetAuthor(getter_Copies(author));
+
+ nsCString headerCharset;
+ aHdr->GetEffectiveCharset(headerCharset);
+
+ nsString name;
+ nsCString emailAddress;
+ nsCOMArray<msgIAddressObject> addresses =
+ EncodedHeader(author, headerCharset.get());
+ bool multipleAuthors = addresses.Length() > 1;
+
+ ExtractFirstAddress(addresses, name, emailAddress);
+
+ if (showCondensedAddresses)
+ GetDisplayNameInAddressBook(emailAddress, aSenderString);
+
+ if (aSenderString.IsEmpty()) {
+ // We can't use the display name in the card; use the name contained in
+ // the header or email address.
+ if (name.IsEmpty()) {
+ CopyUTF8toUTF16(emailAddress, aSenderString);
+ } else {
+ int32_t atPos;
+ if ((atPos = name.FindChar('@')) == kNotFound ||
+ name.FindChar('.', atPos) == kNotFound) {
+ aSenderString = name;
+ } else {
+ // Found @ followed by a dot, so this looks like a spoofing case.
+ aSenderString = name;
+ aSenderString.AppendLiteral(" <");
+ AppendUTF8toUTF16(emailAddress, aSenderString);
+ aSenderString.Append('>');
+ }
+ }
+ }
+
+ if (multipleAuthors) {
+ aSenderString.AppendLiteral(" ");
+ nsAutoString val;
+ GetString(u"andOthers", val);
+ aSenderString.Append(val);
+ }
+
+ UpdateCachedName(aHdr, "sender_name", aSenderString);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount) {
+ nsCString accountKey;
+ nsresult rv = aHdr->GetAccountKey(getter_Copies(accountKey));
+
+ // Cache the account manager?
+ nsCOMPtr<nsIMsgAccountManager> accountManager(
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccount> account;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (!accountKey.IsEmpty())
+ rv = accountManager->GetAccount(accountKey, getter_AddRefs(account));
+
+ if (account) {
+ account->GetIncomingServer(getter_AddRefs(server));
+ } else {
+ nsCOMPtr<nsIMsgFolder> folder;
+ aHdr->GetFolder(getter_AddRefs(folder));
+ if (folder) folder->GetServer(getter_AddRefs(server));
+ }
+
+ if (server)
+ server->GetPrettyName(aAccount);
+ else
+ CopyASCIItoUTF16(accountKey, aAccount);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchRecipients(nsIMsgDBHdr* aHdr,
+ nsAString& aRecipientsString) {
+ nsCString recipients;
+ int32_t currentDisplayNameVersion = 0;
+ bool showCondensedAddresses = false;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+ prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses);
+
+ aHdr->GetStringProperty("recipient_names", recipients);
+
+ if (!recipients.IsEmpty()) {
+ nsCString cachedRecipients;
+ GetCachedName(recipients, currentDisplayNameVersion, cachedRecipients);
+
+ // Recipients have already been cached, check if the addressbook
+ // was changed after cache.
+ if (!cachedRecipients.IsEmpty()) {
+ CopyUTF8toUTF16(cachedRecipients, aRecipientsString);
+ return NS_OK;
+ }
+ }
+
+ nsCString unparsedRecipients;
+ nsresult rv = aHdr->GetRecipients(getter_Copies(unparsedRecipients));
+
+ nsCString headerCharset;
+ aHdr->GetEffectiveCharset(headerCharset);
+
+ nsTArray<nsString> names;
+ nsTArray<nsCString> emails;
+ ExtractAllAddresses(EncodedHeader(unparsedRecipients, headerCharset.get()),
+ names, UTF16ArrayAdapter<>(emails));
+
+ uint32_t numAddresses = names.Length();
+
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ // Go through each email address in the recipients and compute its
+ // display name.
+ for (uint32_t i = 0; i < numAddresses; i++) {
+ nsString recipient;
+ nsCString& curAddress = emails[i];
+ nsString& curName = names[i];
+
+ if (showCondensedAddresses)
+ GetDisplayNameInAddressBook(curAddress, recipient);
+
+ if (recipient.IsEmpty()) {
+ // We can't use the display name in the card; use the name contained in
+ // the header or email address.
+ if (curName.IsEmpty()) {
+ CopyUTF8toUTF16(curAddress, recipient);
+ } else {
+ int32_t atPos;
+ if ((atPos = curName.FindChar('@')) == kNotFound ||
+ curName.FindChar('.', atPos) == kNotFound) {
+ recipient = curName;
+ } else {
+ // Found @ followed by a dot, so this looks like a spoofing case.
+ recipient = curName;
+ recipient.AppendLiteral(" <");
+ AppendUTF8toUTF16(curAddress, recipient);
+ recipient.Append('>');
+ }
+ }
+ }
+
+ // Add ', ' between each recipient.
+ if (i != 0) aRecipientsString.AppendLiteral(u", ");
+
+ aRecipientsString.Append(recipient);
+ }
+
+ if (numAddresses == 0 && unparsedRecipients.FindChar(':') != kNotFound) {
+ // No addresses and a colon, so an empty group like
+ // "undisclosed-recipients: ;".
+ // Add group name so at least something displays.
+ nsString group;
+ CopyUTF8toUTF16(unparsedRecipients, group);
+ aRecipientsString.Assign(group);
+ }
+
+ UpdateCachedName(aHdr, "recipient_names", aRecipientsString);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags,
+ nsAString& aValue) {
+ if (aFlags & nsMsgMessageFlags::HasRe) {
+ nsString subject;
+ aMsgHdr->GetMime2DecodedSubject(subject);
+ aValue.AssignLiteral("Re: ");
+ aValue.Append(subject);
+ } else {
+ aMsgHdr->GetMime2DecodedSubject(aValue);
+ }
+
+ return NS_OK;
+}
+
+// In case we want to play around with the date string, I've broken it out into
+// a separate routine. Set rcvDate to true to get the Received: date instead
+// of the Date: date.
+nsresult nsMsgDBView::FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString,
+ bool rcvDate) {
+ PRTime dateOfMsg;
+ PRTime dateOfMsgLocal;
+ uint32_t rcvDateSecs;
+ nsresult rv;
+
+ // Silently return Date: instead if Received: is unavailable.
+ if (rcvDate) {
+ rv = aHdr->GetUint32Property("dateReceived", &rcvDateSecs);
+ if (rcvDateSecs != 0) Seconds2PRTime(rcvDateSecs, &dateOfMsg);
+ }
+
+ if (!rcvDate || rcvDateSecs == 0) rv = aHdr->GetDate(&dateOfMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PRTime currentTime = PR_Now();
+ PRExplodedTime explodedCurrentTime;
+ PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &explodedCurrentTime);
+ PRExplodedTime explodedMsgTime;
+ PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime);
+
+ // If the message is from today, don't show the date, only the time (3:15 pm).
+ // If the message is from the last week, show the day of the week
+ // (Mon 3:15 pm). In all other cases, show the full date (03/19/01 3:15 pm).
+
+ nsDateFormatSelectorComm dateFormat = m_dateFormatDefault;
+ if (explodedCurrentTime.tm_year == explodedMsgTime.tm_year &&
+ explodedCurrentTime.tm_month == explodedMsgTime.tm_month &&
+ explodedCurrentTime.tm_mday == explodedMsgTime.tm_mday) {
+ // Same day.
+ dateFormat = m_dateFormatToday;
+ } else if (currentTime > dateOfMsg) {
+ // The following chunk of code allows us to show a day instead of a number
+ // if the message was received within the last 7 days. i.e. Mon 5:10pm
+ // (depending on the mail.ui.display.dateformat.thisweek pref).
+ // The concrete format used is dependent on a preference setting
+ // (see InitDisplayFormats).
+ // Convert the times from GMT to local time
+ int64_t GMTLocalTimeShift =
+ PR_USEC_PER_SEC * int64_t(explodedCurrentTime.tm_params.tp_gmt_offset +
+ explodedCurrentTime.tm_params.tp_dst_offset);
+ currentTime += GMTLocalTimeShift;
+ dateOfMsgLocal = dateOfMsg + GMTLocalTimeShift;
+
+ // Find the most recent midnight.
+ int64_t todaysMicroSeconds = currentTime % PR_USEC_PER_DAY;
+ int64_t mostRecentMidnight = currentTime - todaysMicroSeconds;
+
+ // Most recent midnight minus 6 days.
+ int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6);
+
+ // Was the message sent during the last week?
+ if (dateOfMsgLocal >= mostRecentWeek) dateFormat = m_dateFormatThisWeek;
+ }
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ switch (dateFormat) {
+ case kDateFormatNone:
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatLong:
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatShort:
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatWeekday: {
+ // We want weekday + time.
+ nsAutoString timeString;
+ nsAutoString weekdayString;
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ timeString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozilla::intl::DateTimeFormat::ComponentsBag components{};
+ components.weekday =
+ mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(
+ components, &explodedMsgTime, weekdayString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (nsMsgDBView::m_connectorPattern.IsEmpty()) {
+ nsAutoCString locale;
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ mozilla::intl::LocaleService::GetInstance()->GetRegionalPrefsLocales(
+ regionalPrefsLocales);
+ locale.Assign(regionalPrefsLocales[0]);
+ nsAutoCString str;
+ mozilla::intl::OSPreferences::GetInstance()
+ ->GetDateTimeConnectorPattern(locale, str);
+ nsMsgDBView::m_connectorPattern = NS_ConvertUTF8toUTF16(str);
+ }
+
+ nsAutoString pattern(nsMsgDBView::m_connectorPattern);
+ int32_t ind = pattern.Find(u"{1}"_ns);
+ if (ind != kNotFound) {
+ pattern.Replace(ind, 3, weekdayString);
+ }
+ ind = pattern.Find(u"{0}"_ns);
+ if (ind != kNotFound) {
+ pattern.Replace(ind, 3, timeString);
+ }
+ aDateString = pattern;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::FetchStatus(uint32_t aFlags, nsAString& aStatusString) {
+ if (aFlags & nsMsgMessageFlags::Replied)
+ aStatusString = kRepliedString;
+ else if (aFlags & nsMsgMessageFlags::Forwarded)
+ aStatusString = kForwardedString;
+ else if (aFlags & nsMsgMessageFlags::Redirected)
+ aStatusString = kRedirectedString;
+ else if (aFlags & nsMsgMessageFlags::New)
+ aStatusString = kNewString;
+ else if (aFlags & nsMsgMessageFlags::Read)
+ aStatusString = kReadString;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString) {
+ nsresult rv;
+ nsAutoString formattedSizeString;
+ uint32_t msgSize = 0;
+
+ // For news, show the line count, not the size if the user wants so.
+ if (mShowSizeInLines) {
+ aHdr->GetLineCount(&msgSize);
+ formattedSizeString.AppendInt(msgSize);
+ } else {
+ uint32_t flags = 0;
+
+ aHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial)
+ aHdr->GetUint32Property("onlineSize", &msgSize);
+
+ if (msgSize == 0) aHdr->GetMessageSize(&msgSize);
+
+ rv = FormatFileSize(msgSize, true, formattedSizeString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ aSizeString = formattedSizeString;
+ // The formattingString Length includes the null terminator byte!
+ if (!formattedSizeString.Last())
+ aSizeString.SetLength(formattedSizeString.Length() - 1);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchPriority(nsIMsgDBHdr* aHdr,
+ nsAString& aPriorityString) {
+ nsMsgPriorityValue priority = nsMsgPriority::notSet;
+ aHdr->GetPriority(&priority);
+
+ switch (priority) {
+ case nsMsgPriority::highest:
+ aPriorityString = kHighestPriorityString;
+ break;
+ case nsMsgPriority::high:
+ aPriorityString = kHighPriorityString;
+ break;
+ case nsMsgPriority::low:
+ aPriorityString = kLowPriorityString;
+ break;
+ case nsMsgPriority::lowest:
+ aPriorityString = kLowestPriorityString;
+ break;
+ case nsMsgPriority::normal:
+ aPriorityString = kNormalPriorityString;
+ break;
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchKeywords(nsIMsgDBHdr* aHdr,
+ nsACString& keywordString) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ nsresult rv = NS_OK;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCString keywords;
+ aHdr->GetStringProperty("keywords", keywords);
+ keywordString = keywords;
+ return NS_OK;
+}
+
+// If the row is a collapsed thread, we optionally roll-up the keywords in all
+// the messages in the thread, otherwise, return just the keywords for the row.
+nsresult nsMsgDBView::FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr,
+ nsACString& keywordString) {
+ nsresult rv = FetchKeywords(aHdr, keywordString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool cascadeKeywordsUp = true;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ prefs->GetBoolPref("mailnews.display_reply_tag_colors_for_collapsed_threads",
+ &cascadeKeywordsUp);
+
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ cascadeKeywordsUp) {
+ if ((m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) &&
+ (m_flags[aRow] & nsMsgMessageFlags::Elided)) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCString moreKeywords;
+ for (uint32_t index = 0; index < numChildren; index++) {
+ thread->GetChildHdrAt(index, getter_AddRefs(msgHdr));
+ rv = FetchKeywords(msgHdr, moreKeywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!keywordString.IsEmpty() && !moreKeywords.IsEmpty())
+ keywordString.Append(' ');
+
+ keywordString.Append(moreKeywords);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ nsresult rv = NS_OK;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsString tags;
+ nsCString keywords;
+ aHdr->GetStringProperty("keywords", keywords);
+
+ nsTArray<nsCString> keywordsArray;
+ ParseString(keywords, ' ', keywordsArray);
+ nsAutoString tag;
+
+ for (uint32_t i = 0; i < keywordsArray.Length(); i++) {
+ rv = mTagService->GetTagForKey(keywordsArray[i], tag);
+ if (NS_SUCCEEDED(rv) && !tag.IsEmpty()) {
+ if (!tags.IsEmpty()) tags.Append((char16_t)' ');
+
+ tags.Append(tag);
+ }
+ }
+
+ aTagString = tags;
+ return NS_OK;
+}
+
+/**
+ * Lowercase the email and remove a possible plus addressing part.
+ * E.g. John+test@example.com -> john@example.com.
+ */
+static void ToLowerCaseDropPlusAddessing(nsCString& aEmail) {
+ ToLowerCase(aEmail);
+ int32_t indPlus;
+ if ((indPlus = aEmail.FindChar('+')) == kNotFound) return;
+ int32_t indAt;
+ indAt = aEmail.FindChar('@', indPlus);
+ if (indAt == kNotFound) return;
+ aEmail.ReplaceLiteral(indPlus, indAt - indPlus, "");
+}
+
+bool nsMsgDBView::IsOutgoingMsg(nsIMsgDBHdr* aHdr) {
+ nsString author;
+ aHdr->GetMime2DecodedAuthor(author);
+
+ nsCString emailAddress;
+ nsString name;
+ ExtractFirstAddress(DecodedHeader(author), name, emailAddress);
+ ToLowerCaseDropPlusAddessing(emailAddress);
+ return mEmails.Contains(emailAddress);
+}
+
+// If you call SaveAndClearSelection make sure to call RestoreSelection(),
+// otherwise m_saveRestoreSelectionDepth will be incorrect and will lead to
+// selection msg problems.
+nsresult nsMsgDBView::SaveAndClearSelection(nsMsgKey* aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray) {
+ // Always return a value in the first parameter.
+ if (aCurrentMsgKey) *aCurrentMsgKey = nsMsgKey_None;
+
+ // We don't do anything on nested Save / Restore calls.
+ m_saveRestoreSelectionDepth++;
+ if (m_saveRestoreSelectionDepth != 1) return NS_OK;
+
+ if (!mTreeSelection) return NS_OK;
+
+ // First, freeze selection.
+ mTreeSelection->SetSelectEventsSuppressed(true);
+
+ // Second, save the current index.
+ if (aCurrentMsgKey) {
+ int32_t currentIndex;
+ if (NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && uint32_t(currentIndex) < GetSize())
+ *aCurrentMsgKey = m_keys[currentIndex];
+ else
+ *aCurrentMsgKey = nsMsgKey_None;
+ }
+
+ // Third, get an array of view indices for the selection.
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ int32_t numIndices = selection.Length();
+ aMsgKeyArray.SetLength(numIndices);
+
+ // Now store the msg key for each selected item.
+ nsMsgKey msgKey;
+ for (int32_t index = 0; index < numIndices; index++) {
+ msgKey = m_keys[selection[index]];
+ aMsgKeyArray[index] = msgKey;
+ }
+
+ // Clear the selection, we'll manually restore it later.
+ if (mTreeSelection) mTreeSelection->ClearSelection();
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::RestoreSelection(nsMsgKey aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray) {
+ // We don't do anything on nested Save / Restore calls.
+ m_saveRestoreSelectionDepth--;
+ if (m_saveRestoreSelectionDepth) return NS_OK;
+
+ // Don't assert.
+ if (!mTreeSelection) return NS_OK;
+
+ // Turn our message keys into corresponding view indices.
+ int32_t arraySize = aMsgKeyArray.Length();
+ nsMsgViewIndex currentViewPosition = nsMsgViewIndex_None;
+ nsMsgViewIndex newViewPosition = nsMsgViewIndex_None;
+
+ // If we are threaded, we need to do a little more work
+ // we need to find (and expand) all the threads that contain messages
+ // that we had selected before.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ for (int32_t index = 0; index < arraySize; index++)
+ FindKey(aMsgKeyArray[index], true /* expand */);
+ }
+
+ for (int32_t index = 0; index < arraySize; index++) {
+ newViewPosition = FindKey(aMsgKeyArray[index], false);
+ // Add the index back to the selection.
+ if (newViewPosition != nsMsgViewIndex_None)
+ mTreeSelection->ToggleSelect(newViewPosition);
+ }
+
+ // Make sure the currentView was preserved.
+ if (aCurrentMsgKey != nsMsgKey_None)
+ currentViewPosition = FindKey(aCurrentMsgKey, true);
+
+ if (mJSTree) mJSTree->SetCurrentIndex(currentViewPosition);
+
+ // Make sure the current message is once again visible in the thread pane
+ // so we don't have to go search for it in the thread pane
+ if (currentViewPosition != nsMsgViewIndex_None) {
+ if (mJSTree) {
+ mJSTree->EnsureRowIsVisible(currentViewPosition);
+ } else if (mTree) {
+ mTree->EnsureRowIsVisible(currentViewPosition);
+ }
+ }
+
+ // Unfreeze selection.
+ mTreeSelection->SetSelectEventsSuppressed(false);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GenerateURIForMsgKey(nsMsgKey aMsgKey,
+ nsIMsgFolder* folder,
+ nsACString& aURI) {
+ NS_ENSURE_ARG(folder);
+ return folder->GenerateMessageURI(aMsgKey, aURI);
+}
+
+nsresult nsMsgDBView::GetMessageEnumerator(nsIMsgEnumerator** enumerator) {
+ return m_db->EnumerateMessages(enumerator);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(col);
+ NS_ENSURE_ARG_POINTER(_retval);
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ const nsAString& colID = col->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->IsEditable(row, col, _retval);
+ return NS_OK;
+ }
+
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCellValue(int32_t row, nsTreeColumn* col,
+ const nsAString& value) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCellText(int32_t row, nsTreeColumn* col,
+ const nsAString& value) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRowCount(int32_t* aRowCount) {
+ *aRowCount = GetSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSelection(nsITreeSelection** aSelection) {
+ NS_IF_ADDREF(*aSelection = mTreeSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSelection(nsITreeSelection* aSelection) {
+ mTreeSelection = aSelection;
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::UpdateDisplayMessage(nsMsgViewIndex viewPosition) {
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (!commandUpdater) return NS_OK;
+
+ if (!IsValidIndex(viewPosition)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // Get the subject and the folder for the message and inform the front
+ // end that we changed the message we are currently displaying.
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(viewPosition, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString subject;
+ if (viewPosition >= (nsMsgViewIndex)m_flags.Length())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ FetchSubject(msgHdr, m_flags[viewPosition], subject);
+
+ nsCString keywords;
+ rv = msgHdr->GetStringProperty("keywords", keywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder = m_viewFolder ? m_viewFolder : m_folder;
+
+ commandUpdater->DisplayMessageChanged(folder, subject, keywords);
+
+ if (folder) {
+ if (viewPosition >= (nsMsgViewIndex)m_keys.Length())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ rv = folder->SetLastMessageLoaded(m_keys[viewPosition]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectionChangedXPCOM() {
+ // If the currentSelection changed then we have a message to display -
+ // not if we are in the middle of deleting rows.
+ if (m_deletingRows) return NS_OK;
+
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ bool commandsNeedDisablingBecauseOfSelection = false;
+
+ if (!selection.IsEmpty()) {
+ if (WeAreOffline())
+ commandsNeedDisablingBecauseOfSelection = !OfflineMsgSelected(selection);
+
+ if (!NonDummyMsgSelected(selection))
+ commandsNeedDisablingBecauseOfSelection = true;
+ }
+
+ bool selectionSummarized = false;
+ mSummarizeFailed = false;
+ // Let the front-end adjust the message pane appropriately with either
+ // the message body, or a summary of the selection.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (commandUpdater) {
+ commandUpdater->SummarizeSelection(&selectionSummarized);
+ // Check if the selection was not summarized, but we expected it to be,
+ // and if so, remember it so GetHeadersFromSelection won't include
+ // the messages in collapsed threads.
+ if (!selectionSummarized &&
+ (selection.Length() > 1 ||
+ (selection.Length() == 1 &&
+ m_flags[selection[0]] & nsMsgMessageFlags::Elided &&
+ OperateOnMsgsInCollapsedThreads()))) {
+ mSummarizeFailed = true;
+ }
+ }
+
+ bool summaryStateChanged = selectionSummarized != mSelectionSummarized;
+ mSelectionSummarized = selectionSummarized;
+
+ if (!mTreeSelection || selection.Length() != 1 || selectionSummarized) {
+ // If we have zero or multiple items selected, we shouldn't be displaying
+ // any message.
+ m_currentlyDisplayedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedMsgUri.Truncate();
+ m_currentlyDisplayedViewIndex = nsMsgViewIndex_None;
+ }
+
+ // Determine if we need to push command update notifications out to the UI.
+ // We need to push a command update notification iff, one of the following
+ // conditions are met
+ // (1) the selection went from 0 to 1
+ // (2) it went from 1 to 0
+ // (3) it went from 1 to many
+ // (4) it went from many to 1 or 0
+ // (5) a different msg was selected - perhaps it was offline or not,
+ // matters only when we are offline
+ // (6) we did a forward/back, or went from having no history to having
+ // history - not sure how to tell this.
+ // (7) whether the selection was summarized or not changed.
+
+ // I think we're going to need to keep track of whether forward/back were
+ // enabled/should be enabled, and when this changes, force a command update.
+
+ if (!summaryStateChanged &&
+ (selection.Length() == mNumSelectedRows ||
+ (selection.Length() > 1 && mNumSelectedRows > 1)) &&
+ commandsNeedDisablingBecauseOfSelection ==
+ mCommandsNeedDisablingBecauseOfSelection) {
+ // Don't update commands if we're suppressing them, or if we're removing
+ // rows, unless it was the last row.
+ } else if (!mSuppressCommandUpdating && commandUpdater &&
+ (!mRemovingRow || GetSize() == 0)) {
+ commandUpdater->UpdateCommandStatus();
+ }
+
+ mCommandsNeedDisablingBecauseOfSelection =
+ commandsNeedDisablingBecauseOfSelection;
+ mNumSelectedRows = selection.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // This is where we tell the tree to apply styles to a particular row.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = NS_OK;
+
+ rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ if (IsOutgoingMsg(msgHdr)) properties.AppendLiteral(" outgoing");
+
+ nsCString keywordProperty;
+ FetchRowKeywords(index, msgHdr, keywordProperty);
+ bool tagAdded = false;
+ if (!keywordProperty.IsEmpty()) {
+ AppendKeywordProperties(keywordProperty, properties, &tagAdded);
+ }
+ if (tagAdded) {
+ properties.AppendLiteral(" tagged");
+ } else {
+ properties.AppendLiteral(" untagged");
+ }
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ if (!(flags & nsMsgMessageFlags::Read))
+ properties.AppendLiteral(" unread");
+ else
+ properties.AppendLiteral(" read");
+
+ if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied");
+
+ if (flags & nsMsgMessageFlags::Forwarded)
+ properties.AppendLiteral(" forwarded");
+
+ if (flags & nsMsgMessageFlags::Redirected)
+ properties.AppendLiteral(" redirected");
+
+ if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new");
+
+ if (m_flags[index] & nsMsgMessageFlags::Marked)
+ properties.AppendLiteral(" flagged");
+
+ // Give the custom column handlers a chance to style the row.
+ for (int i = 0; i < m_customColumnHandlers.Count(); i++) {
+ nsString extra;
+ m_customColumnHandlers[i]->GetRowProperties(index, extra);
+ if (!extra.IsEmpty()) {
+ properties.Append(' ');
+ properties.Append(extra);
+ }
+ }
+
+ // For threaded display add the ignoreSubthread property to the
+ // subthread top row (this row). For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (flags & nsMsgMessageFlags::Ignored)) {
+ properties.AppendLiteral(" ignoreSubthread");
+ } else {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) properties.AppendLiteral(" ignoreSubthread");
+ }
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+
+ if ((flags & nsMsgMessageFlags::Offline) ||
+ (localFolder && !(flags & nsMsgMessageFlags::Partial)))
+ properties.AppendLiteral(" offline");
+
+ if (flags & nsMsgMessageFlags::Attachment)
+ properties.AppendLiteral(" attach");
+
+ if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) &&
+ (flags & nsMsgMessageFlags::IMAPDeleted))
+ properties.AppendLiteral(" imapdeleted");
+
+ nsCString imageSize;
+ msgHdr->GetStringProperty("imageSize", imageSize);
+ if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage");
+
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (!junkScoreStr.IsEmpty()) {
+ if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ properties.AppendLiteral(" junk");
+ else
+ properties.AppendLiteral(" notjunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(index, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread");
+
+ // For threaded display add the ignore/watch properties to the
+ // thread top row. For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (m_flags[index] & MSG_VIEW_FLAG_ISTHREAD))) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Watched)
+ properties.AppendLiteral(" watch");
+ if (flags & nsMsgMessageFlags::Ignored)
+ properties.AppendLiteral(" ignore");
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetColumnProperties(nsTreeColumn* col, nsAString& properties) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col,
+ nsAString& properties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // This is where we tell the tree to apply styles to a particular row
+ // i.e. if the row is an unread message...
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = NS_OK;
+
+ rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = col->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+ if (colHandler != nullptr) {
+ colHandler->GetCellProperties(aRow, col, properties);
+ } else if (colID[0] == 'c') {
+ // Correspondent.
+ if (IsOutgoingMsg(msgHdr))
+ properties.AssignLiteral("outgoing");
+ else
+ properties.AssignLiteral("incoming");
+ }
+
+ if (!properties.IsEmpty()) properties.Append(' ');
+
+ properties.Append(mMessageType);
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ if (!(flags & nsMsgMessageFlags::Read))
+ properties.AppendLiteral(" unread");
+ else
+ properties.AppendLiteral(" read");
+
+ if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied");
+
+ if (flags & nsMsgMessageFlags::Forwarded)
+ properties.AppendLiteral(" forwarded");
+
+ if (flags & nsMsgMessageFlags::Redirected)
+ properties.AppendLiteral(" redirected");
+
+ if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new");
+
+ if (m_flags[aRow] & nsMsgMessageFlags::Marked)
+ properties.AppendLiteral(" flagged");
+
+ // For threaded display add the ignoreSubthread property to the
+ // subthread top row (this row). For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (flags & nsMsgMessageFlags::Ignored)) {
+ properties.AppendLiteral(" ignoreSubthread");
+ } else {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) properties.AppendLiteral(" ignoreSubthread");
+ }
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+
+ if ((flags & nsMsgMessageFlags::Offline) ||
+ (localFolder && !(flags & nsMsgMessageFlags::Partial)))
+ properties.AppendLiteral(" offline");
+
+ if (flags & nsMsgMessageFlags::Attachment)
+ properties.AppendLiteral(" attach");
+
+ if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) &&
+ (flags & nsMsgMessageFlags::IMAPDeleted))
+ properties.AppendLiteral(" imapdeleted");
+
+ nsCString imageSize;
+ msgHdr->GetStringProperty("imageSize", imageSize);
+ if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage");
+
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (!junkScoreStr.IsEmpty()) {
+ if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ properties.AppendLiteral(" junk");
+ else
+ properties.AppendLiteral(" notjunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ nsCString keywords;
+ FetchRowKeywords(aRow, msgHdr, keywords);
+ bool tagAdded = false;
+ if (!keywords.IsEmpty()) {
+ AppendKeywordProperties(keywords, properties, &tagAdded);
+ }
+ if (tagAdded) {
+ properties.AppendLiteral(" tagged");
+ } else {
+ properties.AppendLiteral(" untagged");
+ }
+
+ // This is a double fetch of the keywords property since we also fetch
+ // it for the tags - do we want to do this?
+ // I'm not sure anyone uses the kw- property, though it could be nice
+ // for people wanting to extend the thread pane.
+ nsCString keywordProperty;
+ msgHdr->GetStringProperty("keywords", keywordProperty);
+ if (!keywordProperty.IsEmpty()) {
+ NS_ConvertUTF8toUTF16 keywords(keywordProperty);
+ int32_t spaceIndex = 0;
+ do {
+ spaceIndex = keywords.FindChar(' ');
+ int32_t endOfKeyword =
+ (spaceIndex == -1) ? keywords.Length() : spaceIndex;
+ properties.AppendLiteral(" kw-");
+ properties.Append(StringHead(keywords, endOfKeyword));
+ if (spaceIndex > 0) keywords.Cut(0, endOfKeyword + 1);
+ } while (spaceIndex > 0);
+ }
+
+#ifdef SUPPORT_PRIORITY_COLORS
+ // Add special styles for priority.
+ nsMsgPriorityValue priority;
+ msgHdr->GetPriority(&priority);
+ switch (priority) {
+ case nsMsgPriority::highest:
+ properties.append(" priority-highest");
+ break;
+ case nsMsgPriority::high:
+ properties.append(" priority-high");
+ break;
+ case nsMsgPriority::low:
+ properties.append(" priority-low");
+ break;
+ case nsMsgPriority::lowest:
+ properties.append(" priority-lowest");
+ break;
+ default:
+ break;
+ }
+#endif
+
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread");
+
+ // For threaded display add the ignore/watch properties to the
+ // thread top row. For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD))) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Watched)
+ properties.AppendLiteral(" watch");
+ if (flags & nsMsgMessageFlags::Ignored)
+ properties.AppendLiteral(" ignore");
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainer(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = !!(flags & MSG_VIEW_FLAG_HASCHILDREN);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainerOpen(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = (flags & MSG_VIEW_FLAG_HASCHILDREN) &&
+ !(flags & nsMsgMessageFlags::Elided);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainerEmpty(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = !(flags & MSG_VIEW_FLAG_HASCHILDREN);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsSeparator(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *_retval = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetParentIndex(int32_t rowIndex, int32_t* _retval) {
+ *_retval = -1;
+
+ int32_t rowIndexLevel;
+ nsresult rv = GetLevel(rowIndex, &rowIndexLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t i;
+ for (i = rowIndex; i >= 0; i--) {
+ int32_t l;
+ GetLevel(i, &l);
+ if (l < rowIndexLevel) {
+ *_retval = i;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::HasNextSibling(int32_t rowIndex, int32_t afterIndex,
+ bool* _retval) {
+ *_retval = false;
+
+ int32_t rowIndexLevel;
+ GetLevel(rowIndex, &rowIndexLevel);
+
+ int32_t i;
+ int32_t count;
+ GetRowCount(&count);
+ for (i = afterIndex + 1; i < count; i++) {
+ int32_t l;
+ GetLevel(i, &l);
+ if (l < rowIndexLevel) break;
+
+ if (l == rowIndexLevel) {
+ *_retval = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetLevel(int32_t index, int32_t* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ *_retval = m_levels[index];
+ else
+ *_retval = 0;
+
+ return NS_OK;
+}
+
+// Search view will override this since headers can span db's.
+nsresult nsMsgDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) {
+ nsresult rv = NS_OK;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsMsgKey key = m_keys[index];
+ if (key == nsMsgKey_None || !m_db) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (key == m_cachedMsgKey) {
+ NS_IF_ADDREF(*msgHdr = m_cachedHdr);
+ } else {
+ rv = m_db->GetMsgHdrForKey(key, msgHdr);
+ if (NS_SUCCEEDED(rv)) {
+ m_cachedHdr = *msgHdr;
+ m_cachedMsgKey = key;
+ }
+ }
+
+ return rv;
+}
+
+void nsMsgDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ if ((int32_t)index < 0 || index > m_keys.Length()) {
+ // Something's gone wrong in a caller, but we have no clue why.
+ // Return without adding the header to the view.
+ NS_ERROR("Index for message header insertion out of array range!");
+ return;
+ }
+
+ m_keys.InsertElementAt(index, msgKey);
+ m_flags.InsertElementAt(index, flags);
+ m_levels.InsertElementAt(index, level);
+}
+
+void nsMsgDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level) {
+ m_keys[index] = msgKey;
+ m_flags[index] = flags;
+ m_levels[index] = level;
+}
+
+nsresult nsMsgDBView::GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** aFolder) {
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) {
+ NS_IF_ADDREF(*db = m_db);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ NS_ENSURE_ARG_POINTER(aCol);
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ const nsAString& colID = aCol->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->GetImageSrc(aRow, aCol, aValue);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = aCol->GetId();
+
+ aValue.Truncate();
+ if (colID.IsEmpty()) return NS_OK;
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ // Provide a string "value" for cells that do not normally have text.
+ // Use empty string for the normal states "Read", "Not Starred",
+ // "No Attachment" and "Not Junk".
+ switch (colID.First()) {
+ case 'a':
+ if (colID.EqualsLiteral("attachmentCol") &&
+ flags & nsMsgMessageFlags::Attachment) {
+ GetString(u"messageHasAttachment", aValue);
+ }
+ break;
+ case 'f':
+ if (colID.EqualsLiteral("flaggedCol") &&
+ flags & nsMsgMessageFlags::Marked) {
+ GetString(u"messageHasFlag", aValue);
+ }
+ break;
+ case 'j':
+ if (colID.EqualsLiteral("junkStatusCol") && JunkControlsEnabled(aRow)) {
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ // Only need to assign a real value for junk, it's empty already
+ // as it should be for non-junk.
+ if (!junkScoreStr.IsEmpty() &&
+ (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE))
+ aValue.AssignLiteral("messageJunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Converting junkScore to integer failed.");
+ }
+ break;
+ case 't':
+ if (colID.EqualsLiteral("threadCol") &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // thread column
+ bool isContainer, isContainerEmpty, isContainerOpen;
+ IsContainer(aRow, &isContainer);
+ if (isContainer) {
+ IsContainerEmpty(aRow, &isContainerEmpty);
+ if (!isContainerEmpty) {
+ IsContainerOpen(aRow, &isContainerOpen);
+ GetString(
+ isContainerOpen ? u"messageExpanded" : u"messageCollapsed",
+ aValue);
+ }
+ }
+ }
+ break;
+ case 'u':
+ if (colID.EqualsLiteral("unreadButtonColHeader") &&
+ !(flags & nsMsgMessageFlags::Read)) {
+ GetString(u"messageUnread", aValue);
+ }
+ break;
+ default:
+ aValue.Assign(colID);
+ break;
+ }
+
+ return rv;
+}
+
+void nsMsgDBView::RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr) {
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ if (mRecentlyDeletedArrayIndex >= mRecentlyDeletedMsgIds.Length())
+ mRecentlyDeletedMsgIds.AppendElement(messageId);
+ else
+ mRecentlyDeletedMsgIds[mRecentlyDeletedArrayIndex] = messageId;
+
+ // Only remember last 20 deleted msgs.
+ mRecentlyDeletedArrayIndex = (mRecentlyDeletedArrayIndex + 1) % 20;
+}
+
+bool nsMsgDBView::WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr) {
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ return mRecentlyDeletedMsgIds.Contains(messageId);
+}
+
+/**
+ * CUSTOM COLUMNS.
+ */
+
+// Add a custom column handler.
+NS_IMETHODIMP
+nsMsgDBView::AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) {
+ bool custColInSort = false;
+ size_t index = m_customColumnHandlerIDs.IndexOf(column);
+
+ nsAutoString strColID(column);
+
+ // Does not exist.
+ if (index == m_customColumnHandlerIDs.NoIndex) {
+ m_customColumnHandlerIDs.AppendElement(strColID);
+ m_customColumnHandlers.AppendObject(handler);
+ } else {
+ // Insert new handler into the appropriate place in the COMPtr array;
+ // no need to replace the column ID (it's the same).
+ m_customColumnHandlers.ReplaceObjectAt(handler, index);
+ }
+
+ // Check if the column name matches any of the columns in
+ // m_sortColumns, and if so, set m_sortColumns[i].mColHandler
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom &&
+ sortInfo.mCustomColumnName.Equals(column)) {
+ custColInSort = true;
+ sortInfo.mColHandler = handler;
+ }
+ }
+
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ // Grouped view has its own ways.
+ return NS_OK;
+
+ // This cust col is in sort columns, and all are now registered, so sort.
+ if (custColInSort && !CustomColumnsInSortAndNotRegistered())
+ Sort(m_sortType, m_sortOrder);
+
+ return NS_OK;
+}
+
+// Remove a custom column handler.
+NS_IMETHODIMP
+nsMsgDBView::RemoveColumnHandler(const nsAString& aColID) {
+ // Here we should check if the column name matches any of the columns in
+ // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler.
+ size_t index = m_customColumnHandlerIDs.IndexOf(aColID);
+
+ if (index != m_customColumnHandlerIDs.NoIndex) {
+ m_customColumnHandlerIDs.RemoveElementAt(index);
+ m_customColumnHandlers.RemoveObjectAt(index);
+ // Check if the column name matches any of the columns in
+ // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler.
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom &&
+ sortInfo.mCustomColumnName.Equals(aColID))
+ sortInfo.mColHandler = nullptr;
+ }
+
+ return NS_OK;
+ }
+
+ // Can't remove a column that isn't currently custom handled.
+ return NS_ERROR_FAILURE;
+}
+
+// TODO: NS_ENSURE_SUCCESS
+nsIMsgCustomColumnHandler* nsMsgDBView::GetCurColumnHandler() {
+ return GetColumnHandler(m_curCustomColumn);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCurCustomColumn(const nsAString& aColID) {
+ m_curCustomColumn = aColID;
+ if (m_viewFolder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ folderInfo->SetProperty("customSortCol", aColID);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCurCustomColumn(nsAString& result) {
+ result = m_curCustomColumn;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondaryCustomColumn(nsAString& result) {
+ result = m_secondaryCustomColumn;
+ return NS_OK;
+}
+
+nsIMsgCustomColumnHandler* nsMsgDBView::GetColumnHandler(
+ const nsAString& colID) {
+ size_t index = m_customColumnHandlerIDs.IndexOf(colID);
+ return (index != m_customColumnHandlerIDs.NoIndex)
+ ? m_customColumnHandlers[index]
+ : nullptr;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetColumnHandler(const nsAString& aColID,
+ nsIMsgCustomColumnHandler** aHandler) {
+ NS_ENSURE_ARG_POINTER(aHandler);
+ nsAutoString column(aColID);
+ NS_IF_ADDREF(*aHandler = GetColumnHandler(column));
+ return (*aHandler) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+// Check if any active sort columns are custom. If none are custom, return false
+// and go on as always. If any are custom, and all are not registered yet,
+// return true (so that the caller can postpone sort). When the custom column
+// observer is notified with MsgCreateDBView and registers the handler,
+// AddColumnHandler will sort once all required handlers are set.
+bool nsMsgDBView::CustomColumnsInSortAndNotRegistered() {
+ // The initial sort on view open has been started, subsequent user initiated
+ // sort callers can ignore verifying cust col registration.
+ m_checkedCustomColumns = true;
+
+ // DecodeColumnSort must have already created m_sortColumns, otherwise we
+ // can't know, but go on anyway.
+ if (!m_sortColumns.Length()) return false;
+
+ bool custColNotRegistered = false;
+ for (uint32_t i = 0; i < m_sortColumns.Length() && !custColNotRegistered;
+ i++) {
+ if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom &&
+ m_sortColumns[i].mColHandler == nullptr)
+ custColNotRegistered = true;
+ }
+
+ return custColNotRegistered;
+}
+// END CUSTOM COLUMNS.
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ const nsAString& colID = aCol->GetId();
+
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ aValue.Truncate();
+
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->GetCellText(aRow, aCol, aValue);
+ return NS_OK;
+ }
+
+ return CellTextForColumn(aRow, colID, aValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) {
+ if (aColumnName.IsEmpty()) {
+ aValue.Truncate();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+
+ switch (aColumnName.First()) {
+ case 's':
+ if (aColumnName.EqualsLiteral("subjectCol"))
+ rv = FetchSubject(msgHdr, m_flags[aRow], aValue);
+ else if (aColumnName.EqualsLiteral("senderCol"))
+ rv = FetchAuthor(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("sizeCol"))
+ rv = FetchSize(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("statusCol")) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ rv = FetchStatus(flags, aValue);
+ }
+ break;
+ case 'r':
+ if (aColumnName.EqualsLiteral("recipientCol"))
+ rv = FetchRecipients(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("receivedCol"))
+ rv = FetchDate(msgHdr, aValue, true);
+ break;
+ case 'd':
+ if (aColumnName.EqualsLiteral("dateCol")) rv = FetchDate(msgHdr, aValue);
+ break;
+ case 'c':
+ if (aColumnName.EqualsLiteral("correspondentCol")) {
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, aValue);
+ else
+ rv = FetchAuthor(msgHdr, aValue);
+ }
+ break;
+ case 'p':
+ if (aColumnName.EqualsLiteral("priorityCol"))
+ rv = FetchPriority(msgHdr, aValue);
+ break;
+ case 'a':
+ if (aColumnName.EqualsLiteral("accountCol"))
+ rv = FetchAccount(msgHdr, aValue);
+ break;
+ case 't':
+ // total msgs in thread column
+ if (aColumnName.EqualsLiteral("totalCol") &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) {
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ nsAutoString formattedCountString;
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ formattedCountString.AppendInt(numChildren);
+ aValue.Assign(formattedCountString);
+ }
+ }
+ } else if (aColumnName.EqualsLiteral("tagsCol")) {
+ rv = FetchTags(msgHdr, aValue);
+ }
+ break;
+ case 'u':
+ // unread msgs in thread col
+ if (aColumnName.EqualsLiteral("unreadCol") &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) {
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ nsAutoString formattedCountString;
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ formattedCountString.AppendInt(numUnreadChildren);
+ aValue.Assign(formattedCountString);
+ }
+ }
+ }
+ }
+ break;
+ case 'j': {
+ if (aColumnName.EqualsLiteral("junkStatusCol")) {
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ CopyASCIItoUTF16(junkScoreStr, aValue);
+ }
+ break;
+ }
+ case 'i': {
+ if (aColumnName.EqualsLiteral("idCol")) {
+ nsAutoString keyString;
+ nsMsgKey key;
+ msgHdr->GetMessageKey(&key);
+ keyString.AppendInt((int64_t)key);
+ aValue.Assign(keyString);
+ }
+ break;
+ }
+ case 'l': {
+ if (aColumnName.EqualsLiteral("locationCol")) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ folder->GetPrettyName(aValue);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CellDataForColumns(int32_t aRow,
+ const nsTArray<nsString>& aColumnNames,
+ nsAString& aProperties, int32_t* aThreadLevel,
+ nsTArray<nsString>& _retval) {
+ nsresult rv;
+ _retval.Clear();
+
+ uint32_t count = aColumnNames.Length();
+ _retval.SetCapacity(count);
+ for (nsString column : aColumnNames) {
+ nsString text;
+ rv = CellTextForColumn(aRow, column, text);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ return rv;
+ }
+ _retval.AppendElement(text);
+ }
+
+ rv = GetRowProperties(aRow, aProperties);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ return rv;
+ }
+
+ rv = GetLevel(aRow, aThreadLevel);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ aProperties.Truncate();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetTree(mozilla::dom::XULTreeElement* tree) {
+ mTree = tree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetJSTree(nsIMsgJSTree* tree) {
+ mJSTree = tree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::ToggleOpenState(int32_t index) {
+ uint32_t numChanged;
+ nsresult rv = ToggleExpansion(index, &numChanged);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CycleHeader(nsTreeColumn* aCol) {
+ // Let HandleColumnClick() in threadPane.js handle it
+ // since it will set / clear the sort indicators.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) {
+ if (!IsValidIndex(row)) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = col->GetId();
+
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->CycleCell(row, col);
+ return NS_OK;
+ }
+
+ // The cyclers below don't work for the grouped header dummy row, currently.
+ // A future implementation should consider both collapsed and expanded state.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&
+ m_flags[row] & MSG_VIEW_FLAG_DUMMY)
+ return NS_OK;
+
+ if (colID.IsEmpty()) return NS_OK;
+
+ switch (colID.First()) {
+ case 'u':
+ if (colID.EqualsLiteral("unreadButtonColHeader")) {
+ ApplyCommandToIndices(nsMsgViewCommandType::toggleMessageRead,
+ {(nsMsgViewIndex)row});
+ }
+ break;
+ case 't':
+ if (colID.EqualsLiteral("threadCol")) {
+ ExpandAndSelectThreadByIndex(row, false);
+ } else if (colID.EqualsLiteral("tagsCol")) {
+ // XXX Do we want to keep this behaviour but switch it to tags?
+ // We could enumerate over the tags and go to the next one - it looks
+ // to me like this wasn't working before tags landed, so maybe not
+ // worth bothering with.
+ }
+ break;
+ case 'f':
+ if (colID.EqualsLiteral("flaggedCol")) {
+ // toggle the flagged status of the element at row.
+ if (m_flags[row] & nsMsgMessageFlags::Marked) {
+ ApplyCommandToIndices(nsMsgViewCommandType::unflagMessages,
+ {(nsMsgViewIndex)row});
+ } else {
+ ApplyCommandToIndices(nsMsgViewCommandType::flagMessages,
+ {(nsMsgViewIndex)row});
+ }
+ }
+ break;
+ case 'j': {
+ if (!colID.EqualsLiteral("junkStatusCol") || !JunkControlsEnabled(row)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(row, getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ nsCString junkScoreStr;
+ rv = msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (junkScoreStr.IsEmpty() ||
+ (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_HAM_SCORE)) {
+ ApplyCommandToIndices(nsMsgViewCommandType::junk,
+ {(nsMsgViewIndex)row});
+ } else {
+ ApplyCommandToIndices(nsMsgViewCommandType::unjunk,
+ {(nsMsgViewIndex)row});
+ }
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Converting junkScore to integer failed.");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// end nsITreeView Implementation Methods
+///////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ m_viewFlags = viewFlags;
+ m_sortOrder = sortOrder;
+ m_sortType = sortType;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool userNeedsToAuthenticate = false;
+ // If we're PasswordProtectLocalCache, then we need to find out if the
+ // server is authenticated.
+ (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate);
+ if (userNeedsToAuthenticate) return NS_MSG_USER_NOT_AUTHENTICATED;
+
+ if (folder) {
+ // Search view will have a null folder.
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(m_db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgDBService->RegisterPendingListener(folder, this);
+ m_folder = folder;
+
+ if (!m_viewFolder) {
+ // There is never a viewFolder already set except for the single folder
+ // saved search case, where the backing folder m_folder is different from
+ // the m_viewFolder with its own dbFolderInfo state.
+ m_viewFolder = folder;
+ }
+
+ SetMRUTimeForFolder(m_viewFolder);
+
+ RestoreSortInfo();
+
+ // Determine if we are in a news folder or not. If yes, we'll show lines
+ // instead of size, and special icons in the thread pane.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString type;
+ rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // I'm not sure this is correct, because XF virtual folders with mixed news
+ // and mail can have this set.
+ mIsNews = type.LowerCaseEqualsLiteral("nntp");
+
+ // Default to a virtual folder if folder not set, since synthetic search
+ // views may not have a folder.
+ uint32_t folderFlags = nsMsgFolderFlags::Virtual;
+ if (folder) folder->GetFlags(&folderFlags);
+
+ mIsXFVirtual = folderFlags & nsMsgFolderFlags::Virtual;
+ if (!mIsXFVirtual && type.LowerCaseEqualsLiteral("rss")) mIsRss = true;
+
+ // Special case nntp --> news since we'll break themes if we try to be
+ // consistent.
+ if (mIsNews)
+ mMessageType.AssignLiteral("news");
+ else
+ CopyUTF8toUTF16(type, mMessageType);
+
+ GetImapDeleteModel(nullptr);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ prefs->GetBoolPref("mailnews.sort_threads_by_root", &mSortThreadsByRoot);
+ if (mIsNews)
+ prefs->GetBoolPref("news.show_size_in_lines", &mShowSizeInLines);
+ }
+ }
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = accountManager->GetAllIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto identity : identities) {
+ if (!identity) continue;
+
+ nsCString email;
+ identity->GetEmail(email);
+ if (!email.IsEmpty()) {
+ ToLowerCaseDropPlusAddessing(email);
+ mEmails.PutEntry(email);
+ }
+
+ identity->GetReplyTo(email);
+ if (!email.IsEmpty()) {
+ ToLowerCaseDropPlusAddessing(email);
+ mEmails.PutEntry(email);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::Close() {
+ int32_t oldSize = GetSize();
+ // This is important, because the tree will ask us for our row count, which
+ // gets determined from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // Clear these out since they no longer apply if we're switching a folder
+ mJunkHdrs.Clear();
+
+ // This needs to happen after we remove all the keys, since RowCountChanged()
+ // will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ ClearHdrCache();
+ if (m_db) {
+ m_db->RemoveListener(this);
+ m_db = nullptr;
+ }
+ if (m_folder) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgDBService->UnregisterPendingListener(this);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::Init(nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ mMessengerWeak = do_GetWeakReference(aMessengerInstance);
+ mMsgWindowWeak = do_GetWeakReference(aMsgWindow);
+ mCommandUpdater = do_GetWeakReference(aCmdUpdater);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressCommandUpdating(bool aSuppressCommandUpdating) {
+ mSuppressCommandUpdating = aSuppressCommandUpdating;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressCommandUpdating(bool* aSuppressCommandUpdating) {
+ *aSuppressCommandUpdating = mSuppressCommandUpdating;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressMsgDisplay(bool aSuppressDisplay) {
+ mSuppressMsgDisplay = aSuppressDisplay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressMsgDisplay(bool* aSuppressDisplay) {
+ *aSuppressDisplay = mSuppressMsgDisplay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetUsingLines(bool* aUsingLines) {
+ *aUsingLines = mShowSizeInLines;
+ return NS_OK;
+}
+
+int CompareViewIndices(const void* v1, const void* v2, void*) {
+ nsMsgViewIndex i1 = *(nsMsgViewIndex*)v1;
+ nsMsgViewIndex i2 = *(nsMsgViewIndex*)v2;
+ return i1 - i2;
+}
+
+// Array<nsMsgViewIndex> getIndicesForSelection();
+NS_IMETHODIMP
+nsMsgDBView::GetIndicesForSelection(nsTArray<nsMsgViewIndex>& indices) {
+ indices.Clear();
+ if (mTreeSelection) {
+ int32_t viewSize = GetSize();
+ int32_t count;
+ mTreeSelection->GetCount(&count);
+ indices.SetCapacity(count);
+ int32_t selectionCount;
+ mTreeSelection->GetRangeCount(&selectionCount);
+ for (int32_t i = 0; i < selectionCount; i++) {
+ int32_t startRange = -1;
+ int32_t endRange = -1;
+ mTreeSelection->GetRangeAt(i, &startRange, &endRange);
+ if (startRange >= 0 && startRange < viewSize) {
+ for (int32_t rangeIndex = startRange;
+ rangeIndex <= endRange && rangeIndex < viewSize; rangeIndex++) {
+ indices.AppendElement(rangeIndex);
+ }
+ }
+ }
+
+ NS_ASSERTION(indices.Length() == uint32_t(count),
+ "selection count is wrong");
+ } else {
+ // If there is no tree selection object then we must be in stand alone
+ // message mode. In that case the selected indices are really just the
+ // current message key.
+ nsMsgViewIndex viewIndex = FindViewIndex(m_currentlyDisplayedMsgKey);
+ if (viewIndex != nsMsgViewIndex_None) indices.AppendElement(viewIndex);
+ }
+
+ return NS_OK;
+}
+
+// Array<nsIMsgDBHdr> getSelectedMsgHdrs();
+NS_IMETHODIMP
+nsMsgDBView::GetSelectedMsgHdrs(nsTArray<RefPtr<nsIMsgDBHdr>>& aResult) {
+ nsMsgViewIndexArray selection;
+ aResult.Clear();
+ nsresult rv = GetIndicesForSelection(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetHeadersFromSelection(selection, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetURIsForSelection(nsTArray<nsCString>& uris) {
+ uris.Clear();
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ nsresult rv = GetSelectedMsgHdrs(messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uris.SetCapacity(messages.Length());
+ for (nsIMsgDBHdr* msgHdr : messages) {
+ nsCString tmpUri;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ rv = GenerateURIForMsgKey(msgKey, folder, tmpUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uris.AppendElement(tmpUri);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetURIForViewIndex(nsMsgViewIndex index, nsACString& result) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = m_folder;
+ if (!folder) {
+ rv = GetFolderForViewIndex(index, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (index == nsMsgViewIndex_None || index >= m_flags.Length() ||
+ m_flags[index] & MSG_VIEW_FLAG_DUMMY) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ return GenerateURIForMsgKey(m_keys[index], folder, result);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) {
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ nsresult rv = NS_OK;
+ switch (command) {
+ case nsMsgViewCommandType::copyMessages:
+ case nsMsgViewCommandType::moveMessages:
+ rv = ApplyCommandToIndicesWithFolder(command, selection, destFolder);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+
+ nsresult rv = NS_OK;
+ switch (command) {
+ case nsMsgViewCommandType::downloadSelectedForOffline:
+ return DownloadForOffline(msgWindow, selection);
+ case nsMsgViewCommandType::downloadFlaggedForOffline:
+ return DownloadFlaggedForOffline(msgWindow);
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::flagMessages:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::deleteMsg:
+ case nsMsgViewCommandType::undeleteMsg:
+ case nsMsgViewCommandType::deleteNoTrash:
+ case nsMsgViewCommandType::markThreadRead:
+ case nsMsgViewCommandType::junk:
+ case nsMsgViewCommandType::unjunk:
+ rv = ApplyCommandToIndices(command, selection);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ break;
+ case nsMsgViewCommandType::selectAll:
+ if (mTreeSelection) {
+ // If in threaded mode, we need to expand all before selecting.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ rv = ExpandAll();
+
+ mTreeSelection->SelectAll();
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+ }
+ break;
+ case nsMsgViewCommandType::selectThread:
+ rv = ExpandAndSelectThread();
+ break;
+ case nsMsgViewCommandType::selectFlagged:
+ if (!mTreeSelection) {
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ mTreeSelection->SetSelectEventsSuppressed(true);
+ mTreeSelection->ClearSelection();
+ // XXX ExpandAll?
+ uint32_t numIndices = GetSize();
+ for (uint32_t curIndex = 0; curIndex < numIndices; curIndex++) {
+ if (m_flags[curIndex] & nsMsgMessageFlags::Marked)
+ mTreeSelection->ToggleSelect(curIndex);
+ }
+
+ mTreeSelection->SetSelectEventsSuppressed(false);
+ }
+ break;
+ case nsMsgViewCommandType::markAllRead:
+ if (m_folder) {
+ SetSuppressChangeNotifications(true);
+ rv = m_folder->MarkAllMessagesRead(msgWindow);
+ SetSuppressChangeNotifications(false);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+ }
+ break;
+ case nsMsgViewCommandType::toggleThreadWatched:
+ rv = ToggleWatched(selection);
+ break;
+ case nsMsgViewCommandType::expandAll:
+ rv = ExpandAll();
+ m_viewFlags |= nsMsgViewFlagsType::kExpandAll;
+ SetViewFlags(m_viewFlags);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ break;
+ case nsMsgViewCommandType::collapseAll:
+ rv = CollapseAll();
+ m_viewFlags &= ~nsMsgViewFlagsType::kExpandAll;
+ SetViewFlags(m_viewFlags);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+bool nsMsgDBView::ServerSupportsFilterAfterTheFact() {
+ // Cross folder virtual folders might not have a folder set.
+ if (!m_folder) return false;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ // Unexpected.
+ if (NS_FAILED(rv)) return false;
+
+ // Filter after the fact is implement using search so if you can't search,
+ // you can't filter after the fact.
+ bool canSearch;
+ rv = server->GetCanSearchMessages(&canSearch);
+ // Unexpected.
+ if (NS_FAILED(rv)) return false;
+
+ return canSearch;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCommandStatus(nsMsgViewCommandTypeValue command,
+ bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) {
+ nsresult rv = NS_OK;
+
+ bool haveSelection;
+ int32_t rangeCount;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ // If range count is non-zero, we have at least one item selected, so we
+ // have a selection.
+ if (mTreeSelection &&
+ NS_SUCCEEDED(mTreeSelection->GetRangeCount(&rangeCount)) &&
+ rangeCount > 0) {
+ haveSelection = NonDummyMsgSelected(selection);
+ } else {
+ // If we don't have a tree selection we must be in stand alone mode.
+ haveSelection = IsValidIndex(m_currentlyDisplayedViewIndex);
+ }
+
+ switch (command) {
+ case nsMsgViewCommandType::deleteMsg:
+ case nsMsgViewCommandType::deleteNoTrash: {
+ bool canDelete;
+ if (m_folder &&
+ NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) &&
+ !canDelete) {
+ *selectable_p = false;
+ } else {
+ *selectable_p = haveSelection;
+ }
+ break;
+ }
+ case nsMsgViewCommandType::applyFilters:
+ // Disable if no messages.
+ // XXX todo, check that we have filters, and at least one is enabled.
+ *selectable_p = GetSize();
+ if (*selectable_p) *selectable_p = ServerSupportsFilterAfterTheFact();
+
+ break;
+ case nsMsgViewCommandType::runJunkControls:
+ // Disable if no messages.
+ // XXX todo, check that we have JMC enabled?
+ *selectable_p = GetSize() && JunkControlsEnabled(nsMsgViewIndex_None);
+ break;
+ case nsMsgViewCommandType::deleteJunk: {
+ // Disable if no messages, or if we can't delete (like news and
+ // certain imap folders).
+ bool canDelete;
+ *selectable_p =
+ GetSize() && m_folder &&
+ NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && canDelete;
+ break;
+ }
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::flagMessages:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::toggleThreadWatched:
+ case nsMsgViewCommandType::markThreadRead:
+ case nsMsgViewCommandType::downloadSelectedForOffline:
+ *selectable_p = haveSelection;
+ break;
+ case nsMsgViewCommandType::junk:
+ case nsMsgViewCommandType::unjunk:
+ *selectable_p = haveSelection && !selection.IsEmpty() &&
+ JunkControlsEnabled(selection[0]);
+ break;
+ case nsMsgViewCommandType::cmdRequiringMsgBody:
+ *selectable_p =
+ haveSelection && (!WeAreOffline() || OfflineMsgSelected(selection));
+ break;
+ case nsMsgViewCommandType::downloadFlaggedForOffline:
+ case nsMsgViewCommandType::markAllRead:
+ *selectable_p = true;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+// This method needs to be overridden by the various view classes
+// that have different kinds of threads. For example, in a
+// threaded quick search db view, we'd only want to include children
+// of the thread that fit the view (IMO). And when we have threaded
+// cross folder views, we would include all the children of the
+// cross-folder thread.
+nsresult nsMsgDBView::ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> thread;
+ GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (!msgHdr) {
+ NS_ASSERTION(false, "couldn't find message to expand");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ for (uint32_t i = 1; i < numChildren && NS_SUCCEEDED(rv); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = thread->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ messageArray.AppendElement(msgHdr);
+ }
+ }
+
+ return rv;
+}
+
+bool nsMsgDBView::OperateOnMsgsInCollapsedThreads() {
+ if (!mJSTree && mTreeSelection) {
+ RefPtr<mozilla::dom::XULTreeElement> selTree;
+ mTreeSelection->GetTree(getter_AddRefs(selTree));
+ // No tree means stand-alone message window.
+ if (!selTree) return false;
+ }
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool includeCollapsedMsgs = false;
+ prefBranch->GetBoolPref("mail.operate_on_msgs_in_collapsed_threads",
+ &includeCollapsedMsgs);
+ return includeCollapsedMsgs;
+}
+
+nsresult nsMsgDBView::GetHeadersFromSelection(
+ nsTArray<nsMsgViewIndex> const& selection,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs) {
+ hdrs.Clear();
+ hdrs.SetCapacity(selection.Length()); // Best guess.
+ nsresult rv = NS_OK;
+
+ // Don't include collapsed messages if the front end failed to summarize
+ // the selection.
+ bool includeCollapsedMsgs =
+ OperateOnMsgsInCollapsedThreads() && !mSummarizeFailed;
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ if (viewIndex == nsMsgViewIndex_None) {
+ continue;
+ }
+
+ uint32_t viewIndexFlags = m_flags[viewIndex];
+ if (viewIndexFlags & MSG_VIEW_FLAG_DUMMY) {
+ // If collapsed dummy header selected, list its children.
+ if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ rv = ListCollapsedChildren(viewIndex, hdrs);
+
+ continue;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ hdrs.AppendElement(msgHdr);
+ if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided &&
+ viewIndexFlags & MSG_VIEW_FLAG_HASCHILDREN &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ rv = ListCollapsedChildren(viewIndex, hdrs);
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) {
+ if (m_deletingRows) {
+ NS_ASSERTION(false, "Last move did not complete");
+ return NS_OK;
+ }
+
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs;
+ rv = GetHeadersFromSelection(selection, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_deletingRows = isMove && mDeleteModel != nsMsgImapDeleteModels::IMAPDelete;
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElements(selection);
+ }
+
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return copyService->CopyMessages(m_folder /* source folder */, hdrs,
+ destFolder, isMove, nullptr /* listener */,
+ window, true /* allow Undo */);
+}
+
+nsresult nsMsgDBView::ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ switch (command) {
+ case nsMsgViewCommandType::copyMessages:
+ NS_ASSERTION(!(m_folder == destFolder),
+ "The source folder and the destination folder are the same");
+ if (m_folder != destFolder)
+ rv = CopyMessages(msgWindow, selection, false /* isMove */, destFolder);
+
+ break;
+ case nsMsgViewCommandType::moveMessages:
+ NS_ASSERTION(!(m_folder == destFolder),
+ "The source folder and the destination folder are the same");
+ if (m_folder != destFolder)
+ rv = CopyMessages(msgWindow, selection, true /* isMove */, destFolder);
+
+ break;
+ default:
+ NS_ASSERTION(false, "unhandled command");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::ApplyCommandToIndices(nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) {
+ if (selection.IsEmpty()) {
+ // Return quietly, just in case/
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(selection[0], getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ if (command == nsMsgViewCommandType::deleteMsg)
+ return DeleteMessages(msgWindow, selection, false);
+
+ if (command == nsMsgViewCommandType::deleteNoTrash)
+ return DeleteMessages(msgWindow, selection, true);
+
+ nsTArray<nsMsgKey> imapUids;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ bool thisIsImapFolder = (imapFolder != nullptr);
+ nsCOMPtr<nsIJunkMailPlugin> junkPlugin;
+
+ // If this is a junk command, get the junk plugin.
+ if (command == nsMsgViewCommandType::junk ||
+ command == nsMsgViewCommandType::unjunk) {
+ // Get the folder from the first item; we assume that
+ // all messages in the view are from the same folder (no
+ // more junk status column in the 'search messages' dialog
+ // like in earlier versions...).
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilterPlugin> filterPlugin;
+ rv = server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ junkPlugin = do_QueryInterface(filterPlugin, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ false);
+
+ // No sense going through the code that handles messages in collasped threads
+ // for mark thread read.
+ if (command == nsMsgViewCommandType::markThreadRead) {
+ for (nsMsgViewIndex viewIndex : selection) {
+ SetThreadOfMsgReadByIndex(viewIndex, imapUids, true);
+ }
+ } else {
+ // Turn the selection into an array of msg hdrs. This may include messages
+ // in collapsed threads
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ rv = GetHeadersFromSelection(selection, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t length = messages.Length();
+
+ if (thisIsImapFolder) {
+ imapUids.SetLength(length);
+ }
+
+ for (uint32_t i = 0; i < length; i++) {
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr(messages[i]);
+ msgHdr->GetMessageKey(&msgKey);
+ if (thisIsImapFolder) imapUids[i] = msgKey;
+
+ switch (command) {
+ case nsMsgViewCommandType::junk:
+ mNumMessagesRemainingInBatch++;
+ mJunkHdrs.AppendElement(msgHdr);
+ rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr,
+ nsIJunkMailPlugin::JUNK);
+ break;
+ case nsMsgViewCommandType::unjunk:
+ mNumMessagesRemainingInBatch++;
+ mJunkHdrs.AppendElement(msgHdr);
+ rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr,
+ nsIJunkMailPlugin::GOOD);
+ break;
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::undeleteMsg:
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::flagMessages:
+ // This is completely handled in the code below.
+ break;
+ default:
+ NS_ERROR("unhandled command");
+ break;
+ }
+ }
+
+ switch (command) {
+ case nsMsgViewCommandType::toggleMessageRead: {
+ if (messages.IsEmpty()) break;
+
+ uint32_t msgFlags;
+ messages[0]->GetFlags(&msgFlags);
+ folder->MarkMessagesRead(messages,
+ !(msgFlags & nsMsgMessageFlags::Read));
+ break;
+ }
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ folder->MarkMessagesRead(
+ messages, command == nsMsgViewCommandType::markMessagesRead);
+ break;
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::flagMessages:
+ folder->MarkMessagesFlagged(
+ messages, command == nsMsgViewCommandType::flagMessages);
+ break;
+ default:
+ break;
+ }
+
+ // Provide junk-related batch notifications.
+ if (command == nsMsgViewCommandType::junk ||
+ command == nsMsgViewCommandType::unjunk) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgsJunkStatusChanged(messages);
+ }
+ }
+ }
+
+ folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true);
+
+ if (thisIsImapFolder) {
+ imapMessageFlagsType flags = kNoImapMsgFlag;
+ bool addFlags = false;
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ switch (command) {
+ case nsMsgViewCommandType::markThreadRead:
+ flags |= kImapMsgSeenFlag;
+ addFlags = true;
+ break;
+ case nsMsgViewCommandType::undeleteMsg:
+ flags = kImapMsgDeletedFlag;
+ addFlags = false;
+ break;
+ case nsMsgViewCommandType::junk:
+ return imapFolder->StoreCustomKeywords(msgWindow, "Junk"_ns,
+ "NonJunk"_ns, imapUids, nullptr);
+ case nsMsgViewCommandType::unjunk: {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetHdrForFirstSelectedMessage(getter_AddRefs(msgHdr));
+ uint32_t msgFlags = 0;
+ if (msgHdr) msgHdr->GetFlags(&msgFlags);
+
+ if (msgFlags & nsMsgMessageFlags::IMAPDeleted)
+ imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids,
+ nullptr);
+
+ return imapFolder->StoreCustomKeywords(msgWindow, "NonJunk"_ns,
+ "Junk"_ns, imapUids, nullptr);
+ }
+ default:
+ break;
+ }
+
+ // Can't get here without thisIsImapThreadPane == TRUE.
+ if (flags != kNoImapMsgFlag) {
+ imapFolder->StoreImapFlags(flags, addFlags, imapUids, nullptr);
+ }
+ }
+
+ return rv;
+}
+
+/**
+ * View modifications methods by index.
+ */
+
+// This method just removes the specified line from the view. It does
+// NOT delete it from the database.
+nsresult nsMsgDBView::RemoveByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ m_keys.RemoveElementAt(index);
+ m_flags.RemoveElementAt(index);
+ m_levels.RemoveElementAt(index);
+
+ // The call to NoteChange() has to happen after we remove the key as
+ // NoteChange() will call RowCountChanged() which will call our GetRowCount().
+ // An example where view is not the listener - D&D messages.
+ if (!m_deletingRows)
+ NoteChange(index, -1, nsMsgViewNotificationCode::insertOrDelete);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ if (m_deletingRows) {
+ NS_WARNING("Last delete did not complete");
+ return NS_OK;
+ }
+
+ nsresult rv;
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs;
+ rv = GetHeadersFromSelection(selection, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* warnCollapsedPref = "mail.warn_on_collapsed_thread_operation";
+ const char* warnShiftDelPref = "mail.warn_on_shift_delete";
+ const char* warnNewsPref = "news.warn_on_delete";
+ const char* warnTrashDelPref = "mail.warn_on_delete_from_trash";
+ const char* activePref = nullptr;
+ nsString warningName;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool trashFolder = false;
+ rv = m_folder->GetFlag(nsMsgFolderFlags::Trash, &trashFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (trashFolder) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnTrashDelPref, &pref);
+ if (pref) {
+ activePref = warnTrashDelPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteFromTrash.desc");
+ }
+ }
+
+ if (!activePref && (selection.Length() != hdrs.Length())) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnCollapsedPref, &pref);
+ if (pref) {
+ activePref = warnCollapsedPref;
+ warningName.AssignLiteral("confirmMsgDelete.collapsed.desc");
+ }
+ }
+
+ if (!activePref && deleteStorage && !trashFolder) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnShiftDelPref, &pref);
+ if (pref) {
+ activePref = warnShiftDelPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc");
+ }
+ }
+
+ if (!activePref && mIsNews) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnNewsPref, &pref);
+ if (pref) {
+ activePref = warnNewsPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc");
+ }
+ }
+
+ if (activePref) {
+ nsCOMPtr<nsIPrompt> dialog;
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = wwatch->GetNewPrompter(0, getter_AddRefs(dialog));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // "Don't ask..." - unchecked by default.
+ bool dontAsk = false;
+ int32_t buttonPressed = 0;
+
+ nsString dialogTitle;
+ nsString confirmString;
+ nsString checkboxText;
+ nsString buttonApplyNowText;
+ GetString(u"confirmMsgDelete.title", dialogTitle);
+ GetString(u"confirmMsgDelete.dontAsk.label", checkboxText);
+ GetString(u"confirmMsgDelete.delete.label", buttonApplyNowText);
+
+ GetString(warningName.get(), confirmString);
+
+ const uint32_t buttonFlags =
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1);
+
+ rv = dialog->ConfirmEx(dialogTitle.get(), confirmString.get(), buttonFlags,
+ buttonApplyNowText.get(), nullptr, nullptr,
+ checkboxText.get(), &dontAsk, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (buttonPressed) return NS_ERROR_FAILURE;
+
+ if (dontAsk) prefBranch->SetBoolPref(activePref, false);
+ }
+
+ if (!deleteStorage) {
+ rv = m_folder->MarkMessagesRead(hdrs, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) {
+ m_deletingRows = true;
+ }
+
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElements(selection);
+ }
+
+ rv = m_folder->DeleteMessages(hdrs, window, deleteStorage, false, nullptr,
+ true /* allow Undo */);
+ if (NS_FAILED(rv)) {
+ m_deletingRows = false;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::DownloadForOffline(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection) {
+ nsresult rv = NS_OK;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsMsgKey key = m_keys[viewIndex];
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (!(flags & nsMsgMessageFlags::Offline)) {
+ messages.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ m_folder->DownloadMessagesForOffline(messages, window);
+ return rv;
+}
+
+nsresult nsMsgDBView::DownloadFlaggedForOffline(nsIMsgWindow* window) {
+ nsresult rv = NS_OK;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = GetMessageEnumerator(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = enumerator->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ uint32_t flags;
+ header->GetFlags(&flags);
+ if ((flags & nsMsgMessageFlags::Marked) &&
+ !(flags & nsMsgMessageFlags::Offline)) {
+ messages.AppendElement(header);
+ }
+ }
+ }
+ }
+
+ m_folder->DownloadMessagesForOffline(messages, window);
+ return rv;
+}
+
+// Read/unread handling.
+nsresult nsMsgDBView::ToggleReadByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ return SetReadByIndex(index, !(m_flags[index] & nsMsgMessageFlags::Read));
+}
+
+nsresult nsMsgDBView::SetReadByIndex(nsMsgViewIndex index, bool read) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (read) {
+ OrExtraFlag(index, nsMsgMessageFlags::Read);
+ // MarkRead() will clear this flag in the db and then call OnKeyChange(),
+ // but because we are the instigator of the change we'll ignore the change.
+ // So we need to clear it in m_flags to keep the db and m_flags in sync.
+ AndExtraFlag(index, ~nsMsgMessageFlags::New);
+ } else {
+ AndExtraFlag(index, ~nsMsgMessageFlags::Read);
+ }
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dbToUse->MarkRead(m_keys[index], read, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex threadIndex = GetThreadIndex(index);
+ if (threadIndex != index)
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::SetThreadOfMsgReadByIndex(
+ nsMsgViewIndex index, nsTArray<nsMsgKey>& keysMarkedRead, bool /*read*/) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ rv = MarkThreadOfMsgRead(m_keys[index], index, keysMarkedRead, true);
+ return rv;
+}
+
+nsresult nsMsgDBView::SetFlaggedByIndex(nsMsgViewIndex index, bool mark) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mark)
+ OrExtraFlag(index, nsMsgMessageFlags::Marked);
+ else
+ AndExtraFlag(index, ~nsMsgMessageFlags::Marked);
+
+ rv = dbToUse->MarkMarked(m_keys[index], mark, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ return rv;
+}
+
+nsresult nsMsgDBView::SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin,
+ nsIMsgDBHdr* aMsgHdr,
+ nsMsgJunkStatus aNewClassification) {
+ // Get the old junk score.
+ nsCString junkScoreStr;
+ nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr);
+
+ // And the old origin.
+ nsCString oldOriginStr;
+ rv = aMsgHdr->GetStringProperty("junkscoreorigin", oldOriginStr);
+
+ // If this was not classified by the user, say so.
+ nsMsgJunkStatus oldUserClassification;
+ if (oldOriginStr.get()[0] != 'u') {
+ oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED;
+ } else {
+ // Otherwise, pass the actual user classification.
+ if (junkScoreStr.IsEmpty())
+ oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED;
+ else if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ oldUserClassification = nsIJunkMailPlugin::JUNK;
+ else
+ oldUserClassification = nsIJunkMailPlugin::GOOD;
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ // Get the URI for this message so we can pass it to the plugin.
+ nsCString uri;
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIMsgDatabase> db;
+ aMsgHdr->GetMessageKey(&msgKey);
+ rv = aMsgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ GenerateURIForMsgKey(msgKey, folder, uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Tell the plugin about this change, so that it can (potentially)
+ // adjust its database appropriately.
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ rv = aJunkPlugin->SetMessageClassification(
+ uri, oldUserClassification, aNewClassification, msgWindow, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This routine is only reached if the user someone touched the UI
+ // and told us the junk status of this message.
+ // Set origin first so that listeners on the junkscore will
+ // know the correct origin.
+ rv = db->SetStringProperty(msgKey, "junkscoreorigin", "user"_ns);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetStringPropertyByIndex failed");
+
+ // Set the junk score on the message itself.
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(aNewClassification == nsIJunkMailPlugin::JUNK
+ ? nsIJunkMailPlugin::IS_SPAM_SCORE
+ : nsIJunkMailPlugin::IS_HAM_SCORE);
+ db->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+nsresult nsMsgDBView::GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) {
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent)
+
+{
+ // Note: we know all messages in a batch have the same
+ // classification, since unlike OnMessageClassified
+ // methods in other classes (such as nsLocalMailFolder
+ // and nsImapMailFolder), this class, nsMsgDBView, currently
+ // only triggers message classifications due to a command to
+ // mark some of the messages in the view as junk, or as not
+ // junk - so the classification is dictated to the filter,
+ // not suggested by it.
+ //
+ // For this reason the only thing we (may) have to do is
+ // perform the action on all of the junk messages.
+
+ uint32_t numJunk = mJunkHdrs.Length();
+ NS_ASSERTION(aClassification == nsIJunkMailPlugin::GOOD || numJunk,
+ "the classification of a manually-marked junk message has "
+ "been classified as junk, yet there seem to be no such "
+ "outstanding messages");
+
+ // Is this the last message in the batch?
+ if (--mNumMessagesRemainingInBatch == 0 && numJunk > 0) {
+ PerformActionsOnJunkMsgs(aClassification == nsIJunkMailPlugin::JUNK);
+ mJunkHdrs.Clear();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::PerformActionsOnJunkMsgs(bool msgsAreJunk) {
+ uint32_t numJunkHdrs = mJunkHdrs.Length();
+ if (!numJunkHdrs) {
+ NS_ERROR("no indices of marked-as-junk messages to act on");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ mJunkHdrs[0]->GetFolder(getter_AddRefs(srcFolder));
+
+ bool moveMessages, changeReadState;
+ nsCOMPtr<nsIMsgFolder> targetFolder;
+
+ nsresult rv = DetermineActionsForJunkChange(msgsAreJunk, srcFolder,
+ moveMessages, changeReadState,
+ getter_AddRefs(targetFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Nothing to do, bail out.
+ if (!(moveMessages || changeReadState)) return NS_OK;
+
+ if (changeReadState) {
+ // Notes on marking junk as read:
+ // 1. There are 2 occasions on which junk messages are marked as
+ // read: after a manual marking (here and in the front end) and after
+ // automatic classification by the bayesian filter (see code for local
+ // mail folders and for imap mail folders). The server-specific
+ // markAsReadOnSpam pref only applies to the latter, the former is
+ // controlled by "mailnews.ui.junk.manualMarkAsJunkMarksRead".
+ // 2. Even though move/delete on manual mark may be
+ // turned off, we might still need to mark as read.
+
+ rv = srcFolder->MarkMessagesRead(mJunkHdrs, msgsAreJunk);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "marking marked-as-junk messages as read failed");
+ }
+
+ if (moveMessages) {
+ // Check if one of the messages to be junked is actually selected.
+ // If more than one message being junked, one must be selected.
+ // If no tree selection at all, must be in stand-alone message window.
+ bool junkedMsgSelected = numJunkHdrs > 1 || !mTreeSelection;
+ for (nsMsgViewIndex junkIndex = 0;
+ !junkedMsgSelected && junkIndex < numJunkHdrs; junkIndex++) {
+ nsMsgViewIndex hdrIndex = FindHdr(mJunkHdrs[junkIndex]);
+ if (hdrIndex != nsMsgViewIndex_None)
+ mTreeSelection->IsSelected(hdrIndex, &junkedMsgSelected);
+ }
+
+ // If a junked msg is selected, tell the FE to call
+ // SetNextMessageAfterDelete() because a delete is coming.
+ if (junkedMsgSelected) {
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (commandUpdater) {
+ rv = commandUpdater->UpdateNextMessageAfterDelete();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ if (targetFolder) {
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyService->CopyMessages(srcFolder, mJunkHdrs, targetFolder, true,
+ nullptr, msgWindow, true);
+ } else if (msgsAreJunk) {
+ if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ // Unfortunately the DeleteMessages in this case is interpreted by
+ // IMAP as a delete toggle. So what we have to do is to assemble a
+ // new delete array, keeping only those that are not deleted.
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete;
+ for (nsIMsgDBHdr* msgHdr : mJunkHdrs) {
+ if (msgHdr) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (!(flags & nsMsgMessageFlags::IMAPDeleted)) {
+ hdrsToDelete.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ if (!hdrsToDelete.IsEmpty())
+ rv = srcFolder->DeleteMessages(hdrsToDelete, msgWindow, false, false,
+ nullptr, true);
+ } else {
+ rv = srcFolder->DeleteMessages(mJunkHdrs, msgWindow, false, false,
+ nullptr, true);
+ }
+ } else if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(srcFolder));
+ nsTArray<nsMsgKey> imapUids(numJunkHdrs);
+ for (nsIMsgDBHdr* msgHdr : mJunkHdrs) {
+ nsMsgKey key;
+ msgHdr->GetMessageKey(&key);
+ imapUids.AppendElement(key);
+ }
+
+ imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, nullptr);
+ }
+
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "move or deletion of message marked-as-junk/non junk failed");
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::DetermineActionsForJunkChange(
+ bool msgsAreJunk, nsIMsgFolder* srcFolder, bool& moveMessages,
+ bool& changeReadState, nsIMsgFolder** targetFolder) {
+ // There are two possible actions which may be performed
+ // on messages marked as spam: marking as read and moving
+ // somewhere. When a message is marked as non junk,
+ // it may be moved to the inbox, and marked unread.
+ moveMessages = false;
+ changeReadState = false;
+
+ // The 'somewhere', junkTargetFolder, can be a folder,
+ // but if it remains null we'll delete the messages.
+ *targetFolder = nullptr;
+
+ uint32_t folderFlags;
+ srcFolder->GetFlags(&folderFlags);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = srcFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle the easy case of marking a junk message as good first.
+ // Set the move target folder to the inbox, if any.
+ if (!msgsAreJunk) {
+ if (folderFlags & nsMsgFolderFlags::Junk) {
+ prefBranch->GetBoolPref("mail.spam.markAsNotJunkMarksUnRead",
+ &changeReadState);
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, targetFolder);
+ moveMessages = *targetFolder != nullptr;
+ }
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // When the user explicitly marks a message as junk, we can mark it as read,
+ // too. This is independent of the "markAsReadOnSpam" pref, which applies
+ // only to automatically-classified messages.
+ // Note that this behaviour should match the one in the front end for marking
+ // as junk via toolbar/context menu.
+ prefBranch->GetBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead",
+ &changeReadState);
+
+ // Now let's determine whether we'll be taking the second action,
+ // the move / deletion (and also determine which of these two).
+ bool manualMark;
+ (void)spamSettings->GetManualMark(&manualMark);
+ if (!manualMark) return NS_OK;
+
+ int32_t manualMarkMode;
+ (void)spamSettings->GetManualMarkMode(&manualMarkMode);
+ NS_ASSERTION(manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE ||
+ manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE,
+ "bad manual mark mode");
+
+ if (manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE) {
+ // If this is a junk folder (not only "the" junk folder for this account)
+ // don't do the move.
+ if (folderFlags & nsMsgFolderFlags::Junk) return NS_OK;
+
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(!spamFolderURI.IsEmpty(),
+ "spam folder URI is empty, can't move");
+ if (!spamFolderURI.IsEmpty()) {
+ rv = FindFolder(spamFolderURI, targetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*targetFolder) {
+ moveMessages = true;
+ } else {
+ // XXX TODO: GetOrCreateJunkFolder will only create a folder with
+ // localized name "Junk" regardless of spamFolderURI. So if someone
+ // sets the junk folder to an existing folder of a different name,
+ // then deletes that folder, this will fail to create the correct
+ // folder.
+ rv = GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */);
+ if (NS_SUCCEEDED(rv))
+ rv = GetExistingFolder(spamFolderURI, targetFolder);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed");
+ }
+ }
+
+ return NS_OK;
+ }
+
+ // At this point manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE).
+
+ // If this is in the trash, let's not delete.
+ if (folderFlags & nsMsgFolderFlags::Trash) return NS_OK;
+
+ return srcFolder->GetCanDeleteMessages(&moveMessages);
+}
+
+// Reversing threads involves reversing the threads but leaving the
+// expanded messages ordered relative to the thread, so we
+// make a copy of each array and copy them over.
+void nsMsgDBView::ReverseThreads() {
+ nsTArray<uint32_t> newFlagArray;
+ nsTArray<nsMsgKey> newKeyArray;
+ nsTArray<uint8_t> newLevelArray;
+
+ uint32_t viewSize = GetSize();
+ uint32_t startThread = viewSize;
+ uint32_t nextThread = viewSize;
+ uint32_t destIndex = 0;
+
+ newKeyArray.SetLength(m_keys.Length());
+ newFlagArray.SetLength(m_flags.Length());
+ newLevelArray.SetLength(m_levels.Length());
+
+ while (startThread) {
+ startThread--;
+
+ if (m_flags[startThread] & MSG_VIEW_FLAG_ISTHREAD) {
+ for (uint32_t sourceIndex = startThread; sourceIndex < nextThread;
+ sourceIndex++) {
+ newKeyArray[destIndex] = m_keys[sourceIndex];
+ newFlagArray[destIndex] = m_flags[sourceIndex];
+ newLevelArray[destIndex] = m_levels[sourceIndex];
+ destIndex++;
+ }
+ // Because we're copying in reverse order.
+ nextThread = startThread;
+ }
+ }
+
+ m_keys.SwapElements(newKeyArray);
+ m_flags.SwapElements(newFlagArray);
+ m_levels.SwapElements(newLevelArray);
+}
+
+void nsMsgDBView::ReverseSort() {
+ uint32_t topIndex = GetSize();
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+
+ // Go up half the array swapping values.
+ for (uint32_t bottomIndex = 0; bottomIndex < --topIndex; bottomIndex++) {
+ // Swap flags.
+ uint32_t tempFlags = m_flags[bottomIndex];
+ m_flags[bottomIndex] = m_flags[topIndex];
+ m_flags[topIndex] = tempFlags;
+
+ // Swap keys.
+ nsMsgKey tempKey = m_keys[bottomIndex];
+ m_keys[bottomIndex] = m_keys[topIndex];
+ m_keys[topIndex] = tempKey;
+
+ if (folders) {
+ // Swap folders -- needed when search is done across multiple folders.
+ nsIMsgFolder* bottomFolder = folders->ObjectAt(bottomIndex);
+ nsIMsgFolder* topFolder = folders->ObjectAt(topIndex);
+ folders->ReplaceObjectAt(topFolder, bottomIndex);
+ folders->ReplaceObjectAt(bottomFolder, topIndex);
+ }
+
+ // No need to swap elements in m_levels; since we only call
+ // ReverseSort in non-threaded mode, m_levels are all the same.
+ }
+}
+
+int nsMsgDBView::FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2,
+ viewSortInfo* sortInfo) {
+ int32_t retVal = 0;
+
+ nsIMsgDatabase* db = sortInfo->db;
+
+ mozilla::DebugOnly<nsresult> rv =
+ db->CompareCollationKeys(pItem1->key, pItem2->key, &retVal);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed");
+
+ if (retVal) return sortInfo->ascendingSort ? retVal : -retVal;
+
+ return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder,
+ pItem2->id, pItem2->folder, sortInfo);
+}
+
+int nsMsgDBView::FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2,
+ viewSortInfo* sortInfo) {
+ if (pItem1->dword > pItem2->dword) {
+ return (sortInfo->ascendingSort) ? 1 : -1;
+ }
+
+ if (pItem1->dword < pItem2->dword) {
+ return (sortInfo->ascendingSort) ? -1 : 1;
+ }
+
+ return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder,
+ pItem2->id, pItem2->folder, sortInfo);
+}
+
+// XXX are these still correct?
+// To compensate for memory alignment required for systems such as HP-UX, these
+// values must be 4 bytes aligned. Don't break this when modifying the
+// constants.
+const int kMaxSubjectKey = 160;
+const int kMaxLocationKey = 160; // Also used for account.
+const int kMaxAuthorKey = 160;
+const int kMaxRecipientKey = 80;
+
+// There are cases when pFieldType is not set:
+// one case returns NS_ERROR_UNEXPECTED;
+// the other case now return NS_ERROR_NULL_POINTER (this is only when
+// colHandler below is null, but is very unlikely).
+// The latter case used to return NS_OK, which was incorrect.
+nsresult nsMsgDBView::GetFieldTypeAndLenForSort(
+ nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen, eFieldType* pFieldType,
+ nsIMsgCustomColumnHandler* colHandler) {
+ NS_ENSURE_ARG_POINTER(pMaxLen);
+ NS_ENSURE_ARG_POINTER(pFieldType);
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySubject:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxSubjectKey;
+ break;
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags:
+ case nsMsgViewSortType::byLocation:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxLocationKey;
+ break;
+ case nsMsgViewSortType::byRecipient:
+ case nsMsgViewSortType::byCorrespondent:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxRecipientKey;
+ break;
+ case nsMsgViewSortType::byAuthor:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxAuthorKey;
+ break;
+ case nsMsgViewSortType::byDate:
+ case nsMsgViewSortType::byReceived:
+ case nsMsgViewSortType::byPriority:
+ case nsMsgViewSortType::byThread:
+ case nsMsgViewSortType::byId:
+ case nsMsgViewSortType::bySize:
+ case nsMsgViewSortType::byFlagged:
+ case nsMsgViewSortType::byUnread:
+ case nsMsgViewSortType::byStatus:
+ case nsMsgViewSortType::byJunkStatus:
+ case nsMsgViewSortType::byAttachments:
+ *pFieldType = kU32;
+ *pMaxLen = 0;
+ break;
+ case nsMsgViewSortType::byCustom: {
+ if (colHandler == nullptr) {
+ NS_WARNING("colHandler is null. *pFieldType is not set.");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ bool isString;
+ colHandler->IsString(&isString);
+
+ if (isString) {
+ *pFieldType = kCollationKey;
+ // 80 - do we need a separate k?
+ *pMaxLen = kMaxRecipientKey;
+ } else {
+ *pFieldType = kU32;
+ *pMaxLen = 0;
+ }
+ break;
+ }
+ case nsMsgViewSortType::byNone:
+ // Bug 901948.
+ return NS_ERROR_INVALID_ARG;
+ default: {
+ nsAutoCString message("unexpected switch value: sortType=");
+ message.AppendInt(sortType);
+ NS_WARNING(message.get());
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+#define MSG_STATUS_MASK \
+ (nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded)
+
+nsresult nsMsgDBView::GetStatusSortValue(nsIMsgDBHdr* msgHdr,
+ uint32_t* result) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(result);
+
+ uint32_t messageFlags;
+ nsresult rv = msgHdr->GetFlags(&messageFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (messageFlags & nsMsgMessageFlags::New) {
+ // Happily, new by definition stands alone.
+ *result = 0;
+ return NS_OK;
+ }
+
+ switch (messageFlags & MSG_STATUS_MASK) {
+ case nsMsgMessageFlags::Replied:
+ *result = 2;
+ break;
+ case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied:
+ *result = 1;
+ break;
+ case nsMsgMessageFlags::Forwarded:
+ *result = 3;
+ break;
+ default:
+ *result = (messageFlags & nsMsgMessageFlags::Read) ? 4 : 5;
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetLongField(nsIMsgDBHdr* msgHdr,
+ nsMsgViewSortTypeValue sortType,
+ uint32_t* result,
+ nsIMsgCustomColumnHandler* colHandler) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(result);
+
+ bool isRead;
+ uint32_t bits;
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySize:
+ rv = (mShowSizeInLines) ? msgHdr->GetLineCount(result)
+ : msgHdr->GetMessageSize(result);
+ break;
+ case nsMsgViewSortType::byPriority:
+ nsMsgPriorityValue priority;
+ rv = msgHdr->GetPriority(&priority);
+ // Treat "none" as "normal" when sorting.
+ if (priority == nsMsgPriority::none) priority = nsMsgPriority::normal;
+
+ // We want highest priority to have lowest value
+ // so ascending sort will have highest priority first.
+ *result = nsMsgPriority::highest - priority;
+ break;
+ case nsMsgViewSortType::byStatus:
+ rv = GetStatusSortValue(msgHdr, result);
+ break;
+ case nsMsgViewSortType::byFlagged:
+ bits = 0;
+ rv = msgHdr->GetFlags(&bits);
+ // Make flagged come out on top.
+ *result = !(bits & nsMsgMessageFlags::Marked);
+ break;
+ case nsMsgViewSortType::byUnread:
+ rv = msgHdr->GetIsRead(&isRead);
+ if (NS_SUCCEEDED(rv)) *result = !isRead;
+
+ break;
+ case nsMsgViewSortType::byJunkStatus: {
+ nsCString junkScoreStr;
+ rv = msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ // Unscored messages should come before messages that are scored
+ // junkScoreStr is "", and "0" - "100"; normalize to 0 - 101.
+ *result = junkScoreStr.IsEmpty() ? (0) : atoi(junkScoreStr.get()) + 1;
+ break;
+ }
+ case nsMsgViewSortType::byAttachments:
+ bits = 0;
+ rv = msgHdr->GetFlags(&bits);
+ *result = !(bits & nsMsgMessageFlags::Attachment);
+ break;
+ case nsMsgViewSortType::byDate:
+ // When sorting threads by date, we may want the date of the newest msg
+ // in the thread.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ !mSortThreadsByRoot) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv)) {
+ thread->GetNewestMsgDate(result);
+ break;
+ }
+ }
+ rv = msgHdr->GetDateInSeconds(result);
+ break;
+ case nsMsgViewSortType::byReceived:
+ // When sorting threads by received date, we may want the received date
+ // of the newest msg in the thread.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ !mSortThreadsByRoot) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ thread->GetNewestMsgDate(result);
+ } else {
+ // Already in seconds.
+ rv = msgHdr->GetUint32Property("dateReceived", result);
+ if (*result == 0)
+ // Use Date instead, we have no Received property
+ rv = msgHdr->GetDateInSeconds(result);
+ }
+ break;
+ case nsMsgViewSortType::byCustom:
+ if (colHandler != nullptr) {
+ colHandler->GetSortLongForRow(msgHdr, result);
+ rv = NS_OK;
+ } else {
+ NS_ASSERTION(false,
+ "should not be here (Sort Type: byCustom (Long), but no "
+ "custom handler)");
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ break;
+ case nsMsgViewSortType::byNone:
+ // Bug 901948.
+ return NS_ERROR_INVALID_ARG;
+
+ case nsMsgViewSortType::byId:
+ // Handled by caller, since caller knows the key.
+ default:
+ NS_ERROR("should not be here");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+MsgViewSortColumnInfo::MsgViewSortColumnInfo(
+ const MsgViewSortColumnInfo& other) {
+ mSortType = other.mSortType;
+ mSortOrder = other.mSortOrder;
+ mCustomColumnName = other.mCustomColumnName;
+ mColHandler = other.mColHandler;
+}
+
+bool MsgViewSortColumnInfo::operator==(
+ const MsgViewSortColumnInfo& other) const {
+ return (mSortType == nsMsgViewSortType::byCustom)
+ ? mCustomColumnName.Equals(other.mCustomColumnName)
+ : mSortType == other.mSortType;
+}
+
+nsresult nsMsgDBView::EncodeColumnSort(nsString& columnSortString) {
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ columnSortString.Append((char)sortInfo.mSortType);
+ columnSortString.Append((char)sortInfo.mSortOrder + '0');
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom) {
+ columnSortString.Append(sortInfo.mCustomColumnName);
+ columnSortString.Append((char16_t)'\r');
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::DecodeColumnSort(nsString& columnSortString) {
+ const char16_t* stringPtr = columnSortString.BeginReading();
+ while (*stringPtr) {
+ MsgViewSortColumnInfo sortColumnInfo;
+ sortColumnInfo.mSortType = (nsMsgViewSortTypeValue)*stringPtr++;
+ sortColumnInfo.mSortOrder = (nsMsgViewSortOrderValue)(*stringPtr++) - '0';
+ if (sortColumnInfo.mSortType == nsMsgViewSortType::byCustom) {
+ while (*stringPtr && *stringPtr != '\r')
+ sortColumnInfo.mCustomColumnName.Append(*stringPtr++);
+
+ sortColumnInfo.mColHandler =
+ GetColumnHandler(sortColumnInfo.mCustomColumnName);
+
+ // Advance past '\r'.
+ if (*stringPtr) stringPtr++;
+ }
+
+ m_sortColumns.AppendElement(sortColumnInfo);
+ }
+
+ return NS_OK;
+}
+
+// Secondary Sort Key: when you select a column to sort, that
+// becomes the new Primary sort key, and all previous sort keys
+// become secondary. For example, if you first click on Date,
+// the messages are sorted by Date; then click on From, and now the
+// messages are sorted by From, and for each value of From the
+// messages are in Date order.
+
+void nsMsgDBView::PushSort(const MsgViewSortColumnInfo& newSort) {
+ // Handle byNone (bug 901948) ala a mail/base/modules/DBViewerWrapper.jsm
+ // where we don't push the secondary sort type if it's ::byNone;
+ // (and secondary sort type is NOT the same as the first sort type
+ // there). This code should behave the same way.
+
+ // We don't expect to be passed sort type ::byNone,
+ // but if we are it's safe to ignore it.
+ if (newSort.mSortType == nsMsgViewSortType::byNone) return;
+
+ // byId is a unique key (misnamed as Order Received). If we are sorting byId,
+ // we don't need to keep any secondary sort keys.
+ if (newSort.mSortType == nsMsgViewSortType::byId) m_sortColumns.Clear();
+
+ m_sortColumns.RemoveElement(newSort);
+ m_sortColumns.InsertElementAt(0, newSort);
+ if (m_sortColumns.Length() > kMaxNumSortColumns)
+ m_sortColumns.RemoveElementAt(kMaxNumSortColumns);
+}
+
+nsresult nsMsgDBView::GetCollationKey(nsIMsgDBHdr* msgHdr,
+ nsMsgViewSortTypeValue sortType,
+ nsTArray<uint8_t>& result,
+ nsIMsgCustomColumnHandler* colHandler) {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySubject:
+ rv = msgHdr->GetSubjectCollationKey(result);
+ break;
+ case nsMsgViewSortType::byLocation:
+ rv = GetLocationCollationKey(msgHdr, result);
+ break;
+ case nsMsgViewSortType::byRecipient: {
+ nsString recipients;
+ rv = FetchRecipients(msgHdr, recipients);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = dbToUse->CreateCollationKey(recipients, result);
+ }
+ break;
+ }
+ case nsMsgViewSortType::byAuthor: {
+ rv = msgHdr->GetAuthorCollationKey(result);
+ nsString author;
+ rv = FetchAuthor(msgHdr, author);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = dbToUse->CreateCollationKey(author, result);
+ }
+ break;
+ }
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags: {
+ nsString str;
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+
+ if (!dbToUse)
+ // Probably a search view.
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+
+ rv = (sortType == nsMsgViewSortType::byAccount)
+ ? FetchAccount(msgHdr, str)
+ : FetchTags(msgHdr, str);
+ if (NS_SUCCEEDED(rv) && dbToUse)
+ rv = dbToUse->CreateCollationKey(str, result);
+
+ break;
+ }
+ case nsMsgViewSortType::byCustom:
+ if (colHandler != nullptr) {
+ nsAutoString strKey;
+ rv = colHandler->GetSortStringForRow(msgHdr, strKey);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "failed to get sort string for custom row");
+ nsAutoString strTemp(strKey);
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = dbToUse->CreateCollationKey(strKey, result);
+ } else {
+ NS_ERROR(
+ "should not be here (Sort Type: byCustom (String), but no custom "
+ "handler)");
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ break;
+ case nsMsgViewSortType::byCorrespondent: {
+ nsString value;
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, value);
+ else
+ rv = FetchAuthor(msgHdr, value);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = dbToUse->CreateCollationKey(value, result);
+ }
+ break;
+ }
+ default:
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ // Bailing out with failure will stop the sort and leave us in
+ // a bad state. Try to continue on, instead.
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to get the collation key");
+ if (NS_FAILED(rv)) {
+ result.Clear();
+ }
+
+ return NS_OK;
+}
+
+// As the location collation key is created getting folder from the msgHdr,
+// it is defined in this file and not from the db.
+nsresult nsMsgDBView::GetLocationCollationKey(nsIMsgDBHdr* msgHdr,
+ nsTArray<uint8_t>& result) {
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = folder->GetMsgDatabase(getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString locationString;
+ rv = folder->GetPrettyName(locationString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dbToUse->CreateCollationKey(locationString, result);
+}
+
+nsresult nsMsgDBView::SaveSortInfo(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ if (m_viewFolder) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+ // Save off sort type and order, view type and flags.
+ folderInfo->SetSortType(sortType);
+ folderInfo->SetSortOrder(sortOrder);
+
+ nsString sortColumnsString;
+ rv = EncodeColumnSort(sortColumnsString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ folderInfo->SetProperty("sortColumns", sortColumnsString);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::RestoreSortInfo() {
+ if (!m_viewFolder) return NS_OK;
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+ // Restore m_sortColumns from db.
+ nsString sortColumnsString;
+ folderInfo->GetProperty("sortColumns", sortColumnsString);
+ DecodeColumnSort(sortColumnsString);
+ if (m_sortColumns.Length() > 1) {
+ m_secondarySort = m_sortColumns[1].mSortType;
+ m_secondarySortOrder = m_sortColumns[1].mSortOrder;
+ m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName;
+ }
+
+ // Restore curCustomColumn from db.
+ folderInfo->GetProperty("customSortCol", m_curCustomColumn);
+ }
+
+ return NS_OK;
+}
+
+// Called by msgDBView::Sort, at which point any persisted active custom
+// columns must be registered. If not, reset their m_sortColumns entries
+// to byDate; Sort will fill in values if necessary based on new user sort.
+void nsMsgDBView::EnsureCustomColumnsValid() {
+ if (!m_sortColumns.Length()) return;
+
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom &&
+ m_sortColumns[i].mColHandler == nullptr) {
+ m_sortColumns[i].mSortType = nsMsgViewSortType::byDate;
+ m_sortColumns[i].mCustomColumnName.Truncate();
+ // There are only two...
+ if (i == 0 && m_sortType != nsMsgViewSortType::byCustom)
+ SetCurCustomColumn(EmptyString());
+ if (i == 1) m_secondaryCustomColumn.Truncate();
+ }
+ }
+}
+
+int32_t nsMsgDBView::SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1,
+ nsMsgKey key2, nsIMsgFolder* folder2,
+ viewSortInfo* comparisonContext) {
+ nsMsgViewSortTypeValue sortType = comparisonContext->view->m_secondarySort;
+ bool isAscendingSort = comparisonContext->view->m_secondarySortOrder ==
+ nsMsgViewSortOrder::ascending;
+
+ // We need to make sure that in the case of the secondary sort field also
+ // matching, we don't recurse.
+ if (comparisonContext->isSecondarySort ||
+ sortType == nsMsgViewSortType::byId) {
+ if (key1 > key2) {
+ return isAscendingSort ? 1 : -1;
+ }
+
+ if (key1 < key2) {
+ return isAscendingSort ? -1 : 1;
+ }
+
+ return 0;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2;
+ nsresult rv = folder1->GetMessageHeader(key1, getter_AddRefs(hdr1));
+ NS_ENSURE_SUCCESS(rv, 0);
+ rv = folder2->GetMessageHeader(key2, getter_AddRefs(hdr2));
+ NS_ENSURE_SUCCESS(rv, 0);
+ IdKey EntryInfo1, EntryInfo2;
+
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the *secondary* sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = nullptr;
+ if (sortType == nsMsgViewSortType::byCustom &&
+ comparisonContext->view->m_sortColumns.Length() > 1) {
+ colHandler = comparisonContext->view->m_sortColumns[1].mColHandler;
+ }
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return 0 right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ hdr1->GetMessageKey(&EntryInfo1.id);
+ hdr2->GetMessageKey(&EntryInfo2.id);
+
+ // Set up new viewSortInfo data for our secondary comparison.
+ viewSortInfo ctx = {
+ .view = comparisonContext->view,
+ .db = comparisonContext->db,
+ .isSecondarySort = true, // To avoid recursing back here!
+ .ascendingSort = isAscendingSort,
+ };
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(hdr1, sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ rv = GetCollationKey(hdr2, sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ return FnSortIdKey(&EntryInfo1, &EntryInfo2, &ctx);
+ case kU32:
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(hdr1, sortType, &EntryInfo1.dword, colHandler);
+ GetLongField(hdr2, sortType, &EntryInfo2.dword, colHandler);
+ }
+ return FnSortIdUint32(&EntryInfo1, &EntryInfo2, &ctx);
+ default:
+ return 0;
+ }
+}
+
+NS_IMETHODIMP nsMsgDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ EnsureCustomColumnsValid();
+
+ // If we're doing a stable sort, we can't just reverse the messages.
+ // Check also that the custom column we're sorting on hasn't changed.
+ // Otherwise, to be on the safe side, resort.
+ // Note: m_curCustomColumn is the desired (possibly new) custom column name,
+ // while m_sortColumns[0].mCustomColumnName is the name for the last completed
+ // sort, since these are persisted after each sort.
+ if (m_sortType == sortType && m_sortValid &&
+ (sortType != nsMsgViewSortType::byCustom ||
+ (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() &&
+ m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) &&
+ m_sortColumns.Length() < 2) {
+ // Same as it ever was. Do nothing.
+ if (m_sortOrder == sortOrder) return NS_OK;
+
+ // For secondary sort, remember the sort order on a per column basis.
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder;
+
+ SaveSortInfo(sortType, sortOrder);
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ ReverseThreads();
+ } else {
+ ReverseSort();
+ }
+
+ m_sortOrder = sortOrder;
+ // We just reversed the sort order, we still need to invalidate the view.
+ return NS_OK;
+ }
+
+ if (sortType == nsMsgViewSortType::byThread) return NS_OK;
+
+ // If a sortType has changed, or the sortType is byCustom and a column has
+ // changed, this is the new primary sortColumnInfo.
+ // Note: m_curCustomColumn is the desired (possibly new) custom column name,
+ // while m_sortColumns[0].mCustomColumnName is the name for the last completed
+ // sort, since these are persisted after each sort.
+ if (m_sortType != sortType ||
+ (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() &&
+ !m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) {
+ // For secondary sort, remember the sort order of the original primary sort!
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = m_sortOrder;
+
+ MsgViewSortColumnInfo sortColumnInfo;
+ sortColumnInfo.mSortType = sortType;
+ sortColumnInfo.mSortOrder = sortOrder;
+ if (sortType == nsMsgViewSortType::byCustom) {
+ GetCurCustomColumn(sortColumnInfo.mCustomColumnName);
+ sortColumnInfo.mColHandler = GetCurColumnHandler();
+ }
+
+ PushSort(sortColumnInfo);
+ } else {
+ // For primary sort, remember the sort order on a per column basis.
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder;
+ }
+
+ if (m_sortColumns.Length() > 1) {
+ m_secondarySort = m_sortColumns[1].mSortType;
+ m_secondarySortOrder = m_sortColumns[1].mSortOrder;
+ m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName;
+ }
+
+ SaveSortInfo(sortType, sortOrder);
+ // Figure out how much memory we'll need, and then malloc it.
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // If we did not obtain proper fieldType, it needs to be checked
+ // because the subsequent code does not handle it very well.
+ nsresult rv =
+ GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+
+ // Don't sort if the field type is not supported: Bug 901948.
+ if (NS_FAILED(rv)) return NS_OK;
+
+ nsTArray<void*> ptrs;
+ uint32_t arraySize = GetSize();
+
+ if (!arraySize) return NS_OK;
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+ if (!dbToUse) return NS_ERROR_FAILURE;
+ }
+
+ viewSortInfo qsPrivateData{
+ .view = this,
+ .db = dbToUse,
+ .isSecondarySort = false,
+ .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ switch (fieldType) {
+ case kCollationKey: {
+ // Sort on a non-numeric field. We'll be calculating a collation key for
+ // each message.
+ nsTArray<IdKey> entries;
+ entries.SetLength(arraySize);
+ nsTArray<IdKey*> pPtrBase;
+ pPtrBase.SetLength(arraySize);
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ IdKey* info = &entries[i];
+ pPtrBase[i] = info;
+ info->id = m_keys[i];
+ info->bits = m_flags[i];
+ info->dword = 0;
+ info->folder = folders ? folders->ObjectAt(i) : m_folder.get();
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found");
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetCollationKey(msgHdr, sortType, info->key, colHandler);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Perform the sort.
+ std::sort(pPtrBase.begin(), pPtrBase.end(),
+ [&qsPrivateData](const auto& lhs, const auto& rhs) {
+ return FnSortIdKey(lhs, rhs, &qsPrivateData) < 0;
+ });
+
+ // Now update the view state to reflect the new order.
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ m_keys[i] = pPtrBase[i]->id;
+ m_flags[i] = pPtrBase[i]->bits;
+ if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i);
+ }
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ m_sortValid = true;
+ return NS_OK;
+ }
+ case kU32: {
+ // Sort on a numeric field.
+ nsTArray<IdUint32> entries;
+ entries.SetLength(arraySize);
+ nsTArray<IdUint32*> pPtrBase;
+ pPtrBase.SetLength(arraySize);
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ IdUint32* info = &entries[i];
+ pPtrBase[i] = info;
+ info->id = m_keys[i];
+ info->bits = m_flags[i];
+ info->folder = folders ? folders->ObjectAt(i) : m_folder.get();
+ if (sortType == nsMsgViewSortType::byId) {
+ info->dword = info->id; // No msgHdr required.
+ } else {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found");
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetLongField(msgHdr, sortType, &info->dword, colHandler);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Perform the sort.
+ std::sort(pPtrBase.begin(), pPtrBase.end(),
+ [&qsPrivateData](const auto& lhs, const auto& rhs) {
+ return FnSortIdUint32(lhs, rhs, &qsPrivateData) < 0;
+ });
+
+ // Now update the view state to reflect the new order.
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ m_keys[i] = pPtrBase[i]->id;
+ m_flags[i] = pPtrBase[i]->bits;
+ if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i);
+ }
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ m_sortValid = true;
+ return NS_OK;
+ }
+ default:
+ // If we get this far, we've got a bad fieldType.
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+
+nsMsgViewIndex nsMsgDBView::GetIndexOfFirstDisplayedKeyInThread(
+ nsIMsgThread* threadHdr, bool allowDummy) {
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+ uint32_t childIndex = 0;
+ // We could speed up the unreadOnly view by starting our search with the first
+ // unread message in the thread. Sometimes, that will be wrong, however, so
+ // let's skip it until we're sure it's necessary.
+ // (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)
+ // ? threadHdr->GetFirstUnreadKey(m_db) : threadHdr->GetChildAt(0);
+ uint32_t numThreadChildren;
+ threadHdr->GetNumChildren(&numThreadChildren);
+ while (retIndex == nsMsgViewIndex_None && childIndex < numThreadChildren) {
+ nsCOMPtr<nsIMsgDBHdr> childHdr;
+ threadHdr->GetChildHdrAt(childIndex++, getter_AddRefs(childHdr));
+ if (childHdr) retIndex = FindHdr(childHdr, 0, allowDummy);
+ }
+
+ return retIndex;
+}
+
+nsresult nsMsgDBView::GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) {
+ nsresult rv;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)
+ rv = threadHdr->GetFirstUnreadChild(result);
+ else
+ rv = threadHdr->GetChildHdrAt(0, result);
+
+ return rv;
+}
+
+// Find the view index of the thread containing the passed msgKey, if
+// the thread is in the view. MsgIndex is passed in as a shortcut if
+// it turns out the msgKey is the first message in the thread,
+// then we can avoid looking for the msgKey.
+nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsg(
+ nsMsgKey msgKey, nsMsgViewIndex msgIndex /* = nsMsgViewIndex_None */,
+ int32_t* pThreadCount /* = NULL */, uint32_t* pFlags /* = NULL */) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return nsMsgViewIndex_None;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = m_db->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+ return ThreadIndexOfMsgHdr(msgHdr, msgIndex, pThreadCount, pFlags);
+}
+
+nsMsgViewIndex nsMsgDBView::GetThreadIndex(nsMsgViewIndex msgIndex) {
+ if (!IsValidIndex(msgIndex)) return nsMsgViewIndex_None;
+
+ // Scan up looking for level 0 message.
+ while (m_levels[msgIndex] && msgIndex) --msgIndex;
+
+ return msgIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex msgIndex,
+ int32_t* pThreadCount,
+ uint32_t* pFlags) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+
+ if (threadHdr != nullptr) {
+ if (msgIndex == nsMsgViewIndex_None) msgIndex = FindHdr(msgHdr, 0, true);
+
+ // Hdr is not in view, need to find by thread.
+ if (msgIndex == nsMsgViewIndex_None) {
+ msgIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr, true);
+ // nsMsgKey threadKey = (msgIndex == nsMsgViewIndex_None) ? nsMsgKey_None
+ // :
+ // GetAt(msgIndex);
+ if (pFlags) threadHdr->GetFlags(pFlags);
+ }
+
+ nsMsgViewIndex startOfThread = msgIndex;
+ while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0)
+ startOfThread--;
+
+ retIndex = startOfThread;
+ if (pThreadCount) {
+ int32_t numChildren = 0;
+ nsMsgViewIndex threadIndex = startOfThread;
+ do {
+ threadIndex++;
+ numChildren++;
+ } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0);
+
+ *pThreadCount = numChildren;
+ }
+ }
+
+ return retIndex;
+}
+
+nsMsgKey nsMsgDBView::GetKeyOfFirstMsgInThread(nsMsgKey key) {
+ // Just report no key for any failure. This can occur when a
+ // message is deleted from a threaded view.
+ nsCOMPtr<nsIMsgThread> pThread;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (NS_FAILED(rv)) return nsMsgKey_None;
+
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (NS_FAILED(rv)) return nsMsgKey_None;
+
+ nsMsgKey firstKeyInThread = nsMsgKey_None;
+
+ if (!pThread) return firstKeyInThread;
+
+ // ### dmb UnreadOnly - this is wrong. But didn't seem to matter in 4.x
+ pThread->GetChildKeyAt(0, &firstKeyInThread);
+ return firstKeyInThread;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetKeyAt(nsMsgViewIndex index, nsMsgKey* result) {
+ NS_ENSURE_ARG(result);
+ *result = GetAt(index);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetFlagsAt(nsMsgViewIndex aIndex, uint32_t* aResult) {
+ NS_ENSURE_ARG(aResult);
+ if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *aResult = m_flags[aIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetMsgHdrAt(nsMsgViewIndex aIndex, nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG(aResult);
+ if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ return GetMsgHdrForViewIndex(aIndex, aResult);
+}
+
+nsMsgViewIndex nsMsgDBView::FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex,
+ bool allowDummy) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ nsMsgViewIndex viewIndex = m_keys.IndexOf(msgKey, startIndex);
+ if (viewIndex == nsMsgViewIndex_None) return viewIndex;
+
+ // If we're supposed to allow dummies, and the previous index is a dummy that
+ // is not elided, then it must be the dummy corresponding to our node and
+ // we should return that instead.
+ if (allowDummy && viewIndex &&
+ (m_flags[viewIndex - 1] & MSG_VIEW_FLAG_DUMMY) &&
+ !(m_flags[viewIndex - 1] & nsMsgMessageFlags::Elided)) {
+ viewIndex--;
+ } else if (!allowDummy && m_flags[viewIndex] & MSG_VIEW_FLAG_DUMMY) {
+ // We're not allowing dummies, and we found a dummy, look again
+ // one past the dummy.
+ return m_keys.IndexOf(msgKey, viewIndex + 1);
+ }
+
+ // Check that the message we found matches the message we were looking for.
+ if (viewIndex != nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgDBHdr> foundMsgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(foundMsgHdr));
+ if (NS_FAILED(rv) || foundMsgHdr != msgHdr) {
+ viewIndex = nsMsgViewIndex_None;
+ }
+ }
+
+ return viewIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::FindKey(nsMsgKey key, bool expand) {
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+ retIndex = (nsMsgViewIndex)(m_keys.IndexOf(key));
+ // For dummy headers, try to expand if the caller says so. And if the thread
+ // is expanded, ignore the dummy header and return the real header index.
+ if (retIndex != nsMsgViewIndex_None &&
+ m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY &&
+ !(m_flags[retIndex] & nsMsgMessageFlags::Elided)) {
+ return (nsMsgViewIndex)m_keys.IndexOf(key, retIndex + 1);
+ }
+
+ if (key != nsMsgKey_None &&
+ (retIndex == nsMsgViewIndex_None ||
+ m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY) &&
+ expand && m_db) {
+ nsMsgKey threadKey = GetKeyOfFirstMsgInThread(key);
+ if (threadKey != nsMsgKey_None) {
+ nsMsgViewIndex threadIndex = FindKey(threadKey, false);
+ if (threadIndex != nsMsgViewIndex_None) {
+ uint32_t flags = m_flags[threadIndex];
+ if ((flags & nsMsgMessageFlags::Elided &&
+ NS_SUCCEEDED(ExpandByIndex(threadIndex, nullptr))) ||
+ flags & MSG_VIEW_FLAG_DUMMY) {
+ retIndex = (nsMsgViewIndex)m_keys.IndexOf(key, threadIndex + 1);
+ }
+ }
+ }
+ }
+
+ return retIndex;
+}
+
+nsresult nsMsgDBView::GetThreadCount(nsMsgViewIndex index,
+ uint32_t* pThreadCount) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgThread> pThread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (NS_SUCCEEDED(rv) && pThread != nullptr)
+ rv = pThread->GetNumChildren(pThreadCount);
+
+ return rv;
+}
+
+// This counts the number of messages in an expanded thread, given the
+// index of the first message in the thread.
+int32_t nsMsgDBView::CountExpandedThread(nsMsgViewIndex index) {
+ int32_t numInThread = 0;
+ nsMsgViewIndex startOfThread = index;
+ while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0)
+ startOfThread--;
+
+ nsMsgViewIndex threadIndex = startOfThread;
+ do {
+ threadIndex++;
+ numInThread++;
+ } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0);
+
+ return numInThread;
+}
+
+// Returns the number of lines that would be added (> 0) or removed (< 0)
+// if we were to try to expand/collapse the passed index.
+nsresult nsMsgDBView::ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) {
+ uint32_t numChildren;
+ nsresult rv;
+
+ *expansionDelta = 0;
+ if (index >= ((nsMsgViewIndex)m_keys.Length()))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ char flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK;
+
+ // The client can pass in the key of any message
+ // in a thread and get the expansion delta for the thread.
+
+ if (flags & nsMsgMessageFlags::Elided) {
+ rv = GetThreadCount(index, &numChildren);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *expansionDelta = numChildren - 1;
+ } else {
+ numChildren = CountExpandedThread(index);
+ *expansionDelta = -(int32_t)(numChildren - 1);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleExpansion(nsMsgViewIndex index,
+ uint32_t* numChanged) {
+ nsresult rv;
+ NS_ENSURE_ARG(numChanged);
+ *numChanged = 0;
+ nsMsgViewIndex threadIndex = GetThreadIndex(index);
+ if (threadIndex == nsMsgViewIndex_None) {
+ NS_ASSERTION(false, "couldn't find thread");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ int32_t flags = m_flags[threadIndex];
+
+ // If not a thread, or doesn't have children, no expand/collapse.
+ // If we add sub-thread expand collapse, this will need to be relaxed.
+ if (!(flags & MSG_VIEW_FLAG_ISTHREAD) || !(flags & MSG_VIEW_FLAG_HASCHILDREN))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ if (flags & nsMsgMessageFlags::Elided)
+ rv = ExpandByIndex(threadIndex, numChanged);
+ else
+ rv = CollapseByIndex(threadIndex, numChanged);
+
+ // If we collaps/uncollapse a thread, this changes the selected URIs.
+ SelectionChangedXPCOM();
+ return rv;
+}
+
+nsresult nsMsgDBView::ExpandAndSelectThread() {
+ nsresult rv;
+
+ NS_ASSERTION(mTreeSelection, "no tree selection");
+ if (!mTreeSelection) return NS_ERROR_UNEXPECTED;
+
+ int32_t index;
+ rv = mTreeSelection->GetCurrentIndex(&index);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ExpandAndSelectThreadByIndex(index, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ExpandAndSelectThreadByIndex(nsMsgViewIndex index,
+ bool augment) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsresult rv;
+
+ nsMsgViewIndex threadIndex;
+ bool inThreadedMode = (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay);
+
+ if (inThreadedMode) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ threadIndex = ThreadIndexOfMsgHdr(msgHdr, index);
+ if (threadIndex == nsMsgViewIndex_None) {
+ NS_ASSERTION(false, "couldn't find thread");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+ } else {
+ threadIndex = index;
+ }
+
+ int32_t flags = m_flags[threadIndex];
+ int32_t count = 0;
+
+ if (inThreadedMode && flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & MSG_VIEW_FLAG_HASCHILDREN) {
+ // If closed, expand this thread.
+ if (flags & nsMsgMessageFlags::Elided) {
+ uint32_t numExpanded;
+ rv = ExpandByIndex(threadIndex, &numExpanded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get the number of messages in the expanded thread so we know how many
+ // to select.
+ count = CountExpandedThread(threadIndex);
+ } else {
+ count = 1;
+ }
+
+ NS_ASSERTION(count > 0, "bad count");
+
+ // Update the selection.
+
+ NS_ASSERTION(mTreeSelection, "no tree selection");
+ if (!mTreeSelection) return NS_ERROR_UNEXPECTED;
+
+ // The count should be 1 or greater. If there was only one message in the
+ // thread, we just select it. If more, we select all of them.
+ mTreeSelection->RangedSelect(threadIndex + count - 1, threadIndex, augment);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ExpandAll() {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ for (int32_t i = GetSize() - 1; i >= 0; i--) {
+ uint32_t numExpanded;
+ uint32_t flags = m_flags[i];
+ if (flags & nsMsgMessageFlags::Elided) ExpandByIndex(i, &numExpanded);
+ }
+
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+
+ SelectionChangedXPCOM();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(pThread);
+ if (!m_db) return NS_ERROR_FAILURE;
+ return m_db->GetThreadContainingMsgHdr(msgHdr, pThread);
+}
+
+nsresult nsMsgDBView::ExpandByIndex(nsMsgViewIndex index,
+ uint32_t* pNumExpanded) {
+ if ((uint32_t)index >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t flags = m_flags[index];
+ uint32_t numExpanded = 0;
+
+ NS_ASSERTION(flags & nsMsgMessageFlags::Elided,
+ "can't expand an already expanded thread");
+ flags &= ~nsMsgMessageFlags::Elided;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> pThread;
+ nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(pThread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) {
+ // Keep top level hdr in thread, even though read.
+ if (flags & nsMsgMessageFlags::Read) {
+ m_levels.AppendElement(0);
+ }
+
+ rv = ListUnreadIdsInThread(pThread, index, &numExpanded);
+ } else {
+ rv = ListIdsInThread(pThread, index, &numExpanded);
+ }
+
+ if (numExpanded > 0) {
+ m_flags[index] = flags;
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ NoteChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete);
+
+ if (pNumExpanded != nullptr) *pNumExpanded = numExpanded;
+
+ return rv;
+}
+
+nsresult nsMsgDBView::CollapseAll() {
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ for (uint32_t i = 0; i < GetSize(); i++) {
+ uint32_t numExpanded;
+ uint32_t flags = m_flags[i];
+ if (!(flags & nsMsgMessageFlags::Elided) &&
+ (flags & MSG_VIEW_FLAG_HASCHILDREN))
+ CollapseByIndex(i, &numExpanded);
+ }
+
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ SelectionChangedXPCOM();
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::CollapseByIndex(nsMsgViewIndex index,
+ uint32_t* pNumCollapsed) {
+ nsresult rv;
+ int32_t flags = m_flags[index];
+ int32_t rowDelta = 0;
+
+ if (flags & nsMsgMessageFlags::Elided ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ !(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ return NS_OK;
+ }
+
+ if (index > m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ rv = ExpansionDelta(index, &rowDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ flags |= nsMsgMessageFlags::Elided;
+
+ m_flags[index] = flags;
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ // Don't count first header in thread.
+ int32_t numRemoved = -rowDelta;
+ if (index + 1 + numRemoved > m_keys.Length()) {
+ NS_ERROR("trying to remove too many rows");
+ numRemoved -= (index + 1 + numRemoved) - m_keys.Length();
+ if (numRemoved <= 0) return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ // Start at first id after thread.
+ RemoveRows(index + 1, numRemoved);
+ if (pNumCollapsed != nullptr) *pNumCollapsed = numRemoved;
+
+ NoteChange(index + 1, rowDelta, nsMsgViewNotificationCode::insertOrDelete);
+
+ return rv;
+}
+
+nsresult nsMsgDBView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ nsresult rv = NS_OK;
+ // Views can override this behaviour, which is to append to view.
+ // This is the mail behaviour, but threaded views will want
+ // to insert in order...
+ if (newHdr) rv = AddHdr(newHdr);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetThreadContainingIndex(nsMsgViewIndex index,
+ nsIMsgThread** resultThread) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetThreadContainingMsgHdr(msgHdr, resultThread);
+}
+
+nsMsgViewIndex nsMsgDBView::GetIndexForThread(nsIMsgDBHdr* msgHdr) {
+ // Take advantage of the fact that we're already sorted
+ // and find the insert index via a binary search, though expanded threads
+ // make that tricky.
+
+ nsMsgViewIndex highIndex = m_keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ comparisonContext.db = hdrDB.get();
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2;
+ // Need to adjust tryIndex if it's not a thread.
+ while (m_levels[tryIndex] && tryIndex) tryIndex--;
+
+ if (tryIndex < lowIndex) {
+ NS_ERROR("try index shouldn't be less than low index");
+ break;
+ }
+
+ EntryInfo2.id = m_keys[tryIndex];
+ GetFolderForViewIndex(tryIndex, &EntryInfo2.folder);
+ EntryInfo2.folder->Release();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // ### this should get the db from the folder...
+ GetDBForViewIndex(tryIndex, getter_AddRefs(db));
+ if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr));
+
+ if (!tryHdr) break;
+
+ if (tryHdr == msgHdr) {
+ NS_WARNING("didn't expect header to already be in view");
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ // We already made sure tryIndex was at a thread at the top of the loop.
+ } else {
+ lowIndex = tryIndex + 1;
+ while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++;
+ }
+ }
+
+ return highIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::GetInsertIndexHelper(
+ nsIMsgDBHdr* msgHdr, nsTArray<nsMsgKey>& keys,
+ nsCOMArray<nsIMsgFolder>* folders, nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewSortTypeValue sortType) {
+ nsMsgViewIndex highIndex = keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ rv = EntryInfo1.folder->GetMsgDatabase(&comparisonContext.db);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+ comparisonContext.db->Release();
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex - 1) / 2;
+ EntryInfo2.id = keys[tryIndex];
+ EntryInfo2.folder = folders ? folders->ObjectAt(tryIndex) : m_folder.get();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ EntryInfo2.folder->GetMessageHeader(EntryInfo2.id, getter_AddRefs(tryHdr));
+ if (!tryHdr) break;
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ } else {
+ lowIndex = tryIndex + 1;
+ }
+ }
+
+ return highIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::GetInsertIndex(nsIMsgDBHdr* msgHdr) {
+ if (!GetSize()) return 0;
+
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0 &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ m_sortOrder != nsMsgViewSortType::byId) {
+ return GetIndexForThread(msgHdr);
+ }
+
+ return GetInsertIndexHelper(msgHdr, m_keys, GetFolders(), m_sortOrder,
+ m_sortType);
+}
+
+nsresult nsMsgDBView::AddHdr(nsIMsgDBHdr* msgHdr, nsMsgViewIndex* resultIndex) {
+ uint32_t flags = 0;
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(m_keys.Length() == m_flags.Length() &&
+ (int)m_keys.Length() == m_levels.Length(),
+ "view arrays out of sync!");
+#endif
+
+ if (resultIndex) *resultIndex = nsMsgViewIndex_None;
+
+ if (!GetShowingIgnored()) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (thread) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Ignored) return NS_OK;
+ }
+
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) return NS_OK;
+ }
+
+ nsMsgKey msgKey, threadId;
+ nsMsgKey threadParent;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetThreadId(&threadId);
+ msgHdr->GetThreadParent(&threadParent);
+
+ msgHdr->GetFlags(&flags);
+ // XXX this isn't quite right, is it?
+ // Should be checking that our thread parent key is none?
+ if (threadParent == nsMsgKey_None) flags |= MSG_VIEW_FLAG_ISTHREAD;
+
+ nsMsgViewIndex insertIndex = GetInsertIndex(msgHdr);
+ if (insertIndex == nsMsgViewIndex_None) {
+ // If unreadonly, level is 0 because we must be the only msg in the thread.
+ int32_t levelToAdd = 0;
+
+ if (m_sortOrder == nsMsgViewSortOrder::ascending) {
+ InsertMsgHdrAt(GetSize(), msgHdr, msgKey, flags, levelToAdd);
+ if (resultIndex) *resultIndex = GetSize() - 1;
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete);
+ } else {
+ InsertMsgHdrAt(0, msgHdr, msgKey, flags, levelToAdd);
+ if (resultIndex) *resultIndex = 0;
+
+ // The call to NoteChange() has to happen after we insert the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(0, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ m_sortValid = false;
+ } else {
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, flags, 0);
+ if (resultIndex) *resultIndex = insertIndex;
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ OnHeaderAddedOrDeleted();
+ return NS_OK;
+}
+
+bool nsMsgDBView::WantsThisThread(nsIMsgThread* /*threadHdr*/) {
+ // Default is to want all threads.
+ return true;
+}
+
+nsMsgViewIndex nsMsgDBView::FindParentInThread(
+ nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ while (parentKey != nsMsgKey_None) {
+ nsMsgViewIndex parentIndex =
+ m_keys.IndexOf(parentKey, startOfThreadViewIndex);
+ if (parentIndex != nsMsgViewIndex_None) return parentIndex;
+
+ if (NS_FAILED(m_db->GetMsgHdrForKey(parentKey, getter_AddRefs(msgHdr))))
+ break;
+
+ msgHdr->GetThreadParent(&parentKey);
+ }
+
+ return startOfThreadViewIndex;
+}
+
+nsresult nsMsgDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsCOMPtr<nsIMsgEnumerator> msgEnumerator;
+ nsresult rv =
+ threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ (void)threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ // Bogus, but harmless.
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+
+ // Skip the first one.
+ bool hasMore;
+ while (NS_SUCCEEDED(msgEnumerator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*pNumListed == numChildren) {
+ MOZ_ASSERT_UNREACHABLE("thread corrupt in db");
+ // If we've listed more messages than are in the thread, then the db
+ // is corrupt, and we should invalidate it.
+ // We'll use this rv to indicate there's something wrong with the db
+ // though for now it probably won't get paid attention to.
+ m_db->SetSummaryValid(false);
+ return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ // We are not going to process subthreads, horribly invalidating the
+ // numChildren characteristic.
+ if (ignored) continue;
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags, newFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ SetMsgHdrAt(msgHdr, *viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level);
+ // Turn off thread or elided bit if they got turned on (maybe from new
+ // only view?)
+ msgHdr->AndFlags(~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided),
+ &newFlags);
+ (*pNumListed)++;
+ (*viewIndex)++;
+ rv = ListIdsInThreadOrder(threadHdr, msgKey, level + 1, viewIndex,
+ pNumListed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+void nsMsgDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ m_keys.InsertElementsAt(viewIndex, numRows, 0);
+ m_flags.InsertElementsAt(viewIndex, numRows, 0);
+ m_levels.InsertElementsAt(viewIndex, numRows, 1);
+}
+
+void nsMsgDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ m_keys.RemoveElementsAt(viewIndex, numRows);
+ m_flags.RemoveElementsAt(viewIndex, numRows);
+ m_levels.RemoveElementsAt(viewIndex, numRows);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::InsertTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows,
+ nsMsgKey aKey, nsMsgViewFlagsTypeValue aFlags,
+ uint32_t aLevel, nsIMsgFolder* aFolder) {
+ if (GetSize() < aIndex) return NS_ERROR_UNEXPECTED;
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ if (folders) {
+ // In a search/xfvf view only, a folder is required.
+ NS_ENSURE_ARG_POINTER(aFolder);
+ for (size_t i = 0; i < aNumRows; i++)
+ // Insert into m_folders.
+ if (!folders->InsertObjectAt(aFolder, aIndex + i))
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ m_keys.InsertElementsAt(aIndex, aNumRows, aKey);
+ m_flags.InsertElementsAt(aIndex, aNumRows, aFlags);
+ m_levels.InsertElementsAt(aIndex, aNumRows, aLevel);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::RemoveTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows) {
+ // Prevent a crash if attempting to remove rows which don't exist.
+ if (GetSize() < aIndex + aNumRows) return NS_ERROR_UNEXPECTED;
+
+ nsMsgDBView::RemoveRows(aIndex, aNumRows);
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ if (folders)
+ // In a search/xfvf view only, remove from m_folders.
+ if (!folders->RemoveObjectsAt(aIndex, aNumRows)) return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG(threadHdr);
+ // These children ids should be in thread order.
+ nsresult rv = NS_OK;
+ uint32_t i;
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+ InsertEmptyRows(viewIndex, numChildren);
+
+ // ### need to rework this when we implemented threading in group views.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) {
+ nsMsgKey parentKey = m_keys[startOfThreadViewIndex];
+ // If the thread is bigger than the hdr cache, expanding the thread
+ // can be slow. Increasing the hdr cache size will help a fair amount.
+ uint32_t hdrCacheSize;
+ m_db->GetMsgHdrCacheSize(&hdrCacheSize);
+ if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(numChildren);
+
+ // If this fails, *pNumListed will be 0, and we'll fall back to just
+ // enumerating the messages in the thread below.
+ rv = ListIdsInThreadOrder(threadHdr, parentKey, 1, &viewIndex, pNumListed);
+ if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(hdrCacheSize);
+ }
+
+ if (!*pNumListed) {
+ uint32_t ignoredHeaders = 0;
+ // If we're not threaded, just list em out in db order.
+ for (i = 1; i <= numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+
+ if (msgHdr != nullptr) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) {
+ ignoredHeaders++;
+ continue;
+ }
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags, newFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, 1);
+ // Here, we're either flat, or we're grouped - in either case,
+ // level is 1. Turn off thread or elided bit if they got turned on
+ // (maybe from new only view?).
+ if (i > 0)
+ msgHdr->AndFlags(
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), &newFlags);
+
+ (*pNumListed)++;
+ viewIndex++;
+ }
+ }
+
+ if (ignoredHeaders + *pNumListed < numChildren) {
+ MOZ_ASSERT_UNREACHABLE("thread corrupt in db");
+ // If we've listed fewer messages than are in the thread, then the db
+ // is corrupt, and we should invalidate it.
+ // We'll use this rv to indicate there's something wrong with the db
+ // though for now it probably won't get paid attention to.
+ m_db->SetSummaryValid(false);
+ rv = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ }
+
+ // We may have added too many elements (i.e., subthreads were cut).
+ // XXX Fix for cross folder view case.
+ if (*pNumListed < numChildren)
+ RemoveRows(viewIndex, numChildren - *pNumListed);
+
+ return rv;
+}
+
+int32_t nsMsgDBView::FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) {
+ nsCOMPtr<nsIMsgDBHdr> curMsgHdr = msgHdr;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+
+ // Look through the ancestors of the passed in msgHdr in turn, looking for
+ // them in the view, up to the start of the thread. If we find an ancestor,
+ // then our level is one greater than the level of the ancestor.
+ while (curMsgHdr) {
+ nsMsgKey parentKey;
+ curMsgHdr->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) break;
+
+ // Scan up to find view index of ancestor, if any.
+ for (nsMsgViewIndex indexToTry = viewIndex;
+ indexToTry && indexToTry-- >= startOfThread;) {
+ if (m_keys[indexToTry] == parentKey) return m_levels[indexToTry] + 1;
+ }
+
+ // If msgHdr's key is its parentKey, we'll loop forever, so protect
+ // against that corruption.
+ if (msgKey == parentKey || NS_FAILED(m_db->GetMsgHdrForKey(
+ parentKey, getter_AddRefs(curMsgHdr)))) {
+ NS_ERROR(
+ "msgKey == parentKey, or GetMsgHdrForKey failed, this used to be an "
+ "infinite loop condition");
+ curMsgHdr = nullptr;
+ } else {
+ // Need to update msgKey so the check for a msgHdr with matching
+ // key+parentKey will work after first time through loop.
+ curMsgHdr->GetMessageKey(&msgKey);
+ }
+ }
+
+ return 1;
+}
+
+// XXX Can this be combined with GetIndexForThread??
+nsMsgViewIndex nsMsgDBView::GetThreadRootIndex(nsIMsgDBHdr* msgHdr) {
+ if (!msgHdr) {
+ NS_WARNING("null msgHdr parameter");
+ return nsMsgViewIndex_None;
+ }
+
+ // Take advantage of the fact that we're already sorted
+ // and find the thread root via a binary search.
+
+ nsMsgViewIndex highIndex = m_keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ comparisonContext.db = hdrDB.get();
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2;
+ // Need to adjust tryIndex if it's not a thread.
+ while (m_levels[tryIndex] && tryIndex) tryIndex--;
+
+ if (tryIndex < lowIndex) {
+ NS_ERROR("try index shouldn't be less than low index");
+ break;
+ }
+
+ EntryInfo2.id = m_keys[tryIndex];
+ GetFolderForViewIndex(tryIndex, &EntryInfo2.folder);
+ EntryInfo2.folder->Release();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // ### this should get the db from the folder...
+ GetDBForViewIndex(tryIndex, getter_AddRefs(db));
+ if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr));
+
+ if (!tryHdr) break;
+
+ if (tryHdr == msgHdr) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ // We already made sure tryIndex was at a thread at the top of the loop.
+ } else {
+ lowIndex = tryIndex + 1;
+ while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++;
+ }
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> resultHdr;
+ GetMsgHdrForViewIndex(highIndex, getter_AddRefs(resultHdr));
+
+ if (resultHdr != msgHdr) {
+ NS_WARNING("didn't find hdr");
+ highIndex = FindHdr(msgHdr);
+#ifdef DEBUG_David_Bienvenu
+ if (highIndex != nsMsgViewIndex_None) {
+ NS_WARNING("but find hdr did");
+ printf("level of found hdr = %d\n", m_levels[highIndex]);
+ ValidateSort();
+ }
+#endif
+ return highIndex;
+ }
+
+ return msgHdr == resultHdr ? highIndex : nsMsgViewIndex_None;
+}
+
+#ifdef DEBUG_David_Bienvenu
+
+void nsMsgDBView::InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo) {
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType");
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+
+ msgHdr->GetMessageKey(&EntryInfo.id);
+ msgHdr->GetFolder(&EntryInfo.folder);
+ EntryInfo.folder->Release();
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId)
+ EntryInfo.dword = EntryInfo.id;
+ else
+ GetLongField(msgHdr, m_sortType, &EntryInfo.dword, colHandler);
+
+ break;
+ default:
+ NS_ERROR("invalid field type");
+ }
+}
+
+void nsMsgDBView::ValidateSort() {
+ IdKey EntryInfo1, EntryInfo2;
+ nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2;
+
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // It is not entirely clear what we should do since,
+ // if fieldType is not available, there is no way to know
+ // how to compare the field to check for sorting.
+ // So we bomb out here. It is OK since this is debug code
+ // inside #ifdef DEBUG_David_Bienvenu
+ nsresult rv =
+ GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType");
+
+ viewSortInfo comparisonContext;
+ comparisonContext.view = this;
+ comparisonContext.isSecondarySort = false;
+ comparisonContext.ascendingSort =
+ (m_sortOrder == nsMsgViewSortOrder::ascending);
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetDBForViewIndex(0, getter_AddRefs(db));
+ // This is only for comparing collation keys - it could be any db.
+ comparisonContext.db = db.get();
+
+ for (nsMsgViewIndex i = 0; i < m_keys.Length();) {
+ // Ignore non threads.
+ if (m_levels[i]) {
+ i++;
+ continue;
+ }
+
+ // Find next header.
+ nsMsgViewIndex j = i + 1;
+ while (j < m_keys.Length() && m_levels[j]) j++;
+
+ if (j == m_keys.Length()) break;
+
+ InitEntryInfoForIndex(i, EntryInfo1);
+ InitEntryInfoForIndex(j, EntryInfo2);
+ const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2;
+ int retStatus = 0;
+ if (fieldType == kCollationKey)
+ retStatus = FnSortIdKey(&pValue1, &pValue2, &comparisonContext);
+ else if (fieldType == kU32)
+ retStatus = FnSortIdUint32(&pValue1, &pValue2, &comparisonContext);
+
+ if (retStatus &&
+ (retStatus < 0) == (m_sortOrder == nsMsgViewSortOrder::ascending)) {
+ NS_ERROR("view not sorted correctly");
+ break;
+ }
+ // j is the new i.
+ i = j;
+ }
+}
+
+#endif
+
+nsresult nsMsgDBView::ListUnreadIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG(threadHdr);
+ // These children ids should be in thread order.
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+ nsMsgKey topLevelMsgKey = m_keys[startOfThreadViewIndex];
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ uint32_t i;
+ for (i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) continue;
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ bool isRead = AdjustReadFlag(msgHdr, &msgFlags);
+ if (!isRead) {
+ // Just make sure flag is right in db.
+ m_db->MarkHdrRead(msgHdr, false, nullptr);
+ if (msgKey != topLevelMsgKey) {
+ InsertMsgHdrAt(
+ viewIndex, msgHdr, msgKey, msgFlags,
+ FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex));
+ viewIndex++;
+ (*pNumListed)++;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ // If we're not the instigator, update flags if this key is in our view.
+ if (aInstigator != this) {
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+ nsMsgKey msgKey;
+ aHdrChanged->GetMessageKey(&msgKey);
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ if (index != nsMsgViewIndex_None) {
+ uint32_t viewOnlyFlags =
+ m_flags[index] & (MSG_VIEW_FLAGS | nsMsgMessageFlags::Elided);
+
+ // XXX what about saving the old view only flags, like IsThread and
+ // HasChildren?
+ // I think we'll want to save those away.
+ m_flags[index] = aNewFlags | viewOnlyFlags;
+ // Tell the view the extra flag changed, so it can
+ // update the previous view, if any.
+ OnExtraFlagChanged(index, aNewFlags);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::New)) {
+ nsMsgViewIndex threadIndex =
+ ThreadIndexOfMsgHdr(aHdrChanged, index, nullptr, nullptr);
+
+ // May need to fix thread counts.
+ if (threadIndex != nsMsgViewIndex_None && threadIndex != index)
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // Don't need to propagate notifications, right?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ nsMsgViewIndex deletedIndex = FindHdr(aHdrChanged);
+ if (IsValidIndex(deletedIndex)) {
+ // Check if this message is currently selected. If it is, tell the frontend
+ // to be prepared for a delete.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ bool isMsgSelected = false;
+ if (mTreeSelection && commandUpdater) {
+ mTreeSelection->IsSelected(deletedIndex, &isMsgSelected);
+ if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete();
+ }
+
+ RemoveByIndex(deletedIndex);
+
+ if (isMsgSelected) {
+ // Now tell the front end that the delete happened.
+ commandUpdater->SelectedMessageRemoved();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrAdded(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ return OnNewHeader(aHdrChanged, aParentKey, false);
+ // Probably also want to pass that parent key in, since we went to the
+ // trouble of figuring out what it is.
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ if (aPreChange) return NS_OK;
+
+ if (aHdrToChange) {
+ nsMsgViewIndex index = FindHdr(aHdrToChange);
+ if (index != nsMsgViewIndex_None)
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) {
+ if (m_db) {
+ m_db->RemoveListener(this);
+ m_db = nullptr;
+ }
+
+ int32_t saveSize = GetSize();
+ ClearHdrCache();
+
+ // This is important, because the tree will ask us for our
+ // row count, which get determine from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // Tell the tree all the rows have gone away.
+ if (mTree) mTree->RowCountChanged(0, -saveSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -saveSize);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ if (!strcmp(aEvent, "DBOpened")) m_db = aDB;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnReadChanged(nsIDBChangeListener* aInstigator) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgDBView::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+void nsMsgDBView::ClearHdrCache() {
+ m_cachedHdr = nullptr;
+ m_cachedMsgKey = nsMsgKey_None;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressChangeNotifications(bool aSuppressChangeNotifications) {
+ mSuppressChangeNotification = aSuppressChangeNotifications;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressChangeNotifications(
+ bool* aSuppressChangeNotifications) {
+ NS_ENSURE_ARG_POINTER(aSuppressChangeNotifications);
+ *aSuppressChangeNotifications = mSuppressChangeNotification;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::NoteChange(nsMsgViewIndex firstLineChanged, int32_t numChanged,
+ nsMsgViewNotificationCodeValue changeType) {
+ if ((mTree || mJSTree) && !mSuppressChangeNotification) {
+ switch (changeType) {
+ case nsMsgViewNotificationCode::changed:
+ if (mTree)
+ mTree->InvalidateRange(firstLineChanged,
+ firstLineChanged + numChanged - 1);
+ if (mJSTree)
+ mJSTree->InvalidateRange(firstLineChanged,
+ firstLineChanged + numChanged - 1);
+ break;
+ case nsMsgViewNotificationCode::insertOrDelete:
+ if (numChanged < 0) mRemovingRow = true;
+
+ // The caller needs to have adjusted m_keys before getting here, since
+ // RowCountChanged() will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(firstLineChanged, numChanged);
+ if (mJSTree) mJSTree->RowCountChanged(firstLineChanged, numChanged);
+ mRemovingRow = false;
+ [[fallthrough]];
+ case nsMsgViewNotificationCode::all:
+ ClearHdrCache();
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSortOrder(nsMsgViewSortOrderValue* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = m_sortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSortType(nsMsgViewSortTypeValue* aSortType) {
+ NS_ENSURE_ARG_POINTER(aSortType);
+ *aSortType = m_sortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSortType(nsMsgViewSortTypeValue aSortType) {
+ m_sortType = aSortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ERROR("you should be overriding this");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondarySortOrder(nsMsgViewSortOrderValue* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = m_secondarySortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSecondarySortOrder(nsMsgViewSortOrderValue aSortOrder) {
+ m_secondarySortOrder = aSortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondarySortType(nsMsgViewSortTypeValue* aSortType) {
+ NS_ENSURE_ARG_POINTER(aSortType);
+ *aSortType = m_secondarySort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSecondarySortType(nsMsgViewSortTypeValue aSortType) {
+ m_secondarySort = aSortType;
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo) {
+ nsresult rv = m_db->GetDBFolderInfo(dbFolderInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Save off sort type and order, view type and flags.
+ (*dbFolderInfo)->SetSortType(m_sortType);
+ (*dbFolderInfo)->SetSortOrder(m_sortOrder);
+ (*dbFolderInfo)->SetViewFlags(m_viewFlags);
+ nsMsgViewTypeValue viewType;
+ GetViewType(&viewType);
+ (*dbFolderInfo)->SetViewType(viewType);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewFlags(nsMsgViewFlagsTypeValue* aViewFlags) {
+ NS_ENSURE_ARG_POINTER(aViewFlags);
+ *aViewFlags = m_viewFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) {
+ // If we're turning off threaded display, we need to expand all so that all
+ // messages will be displayed.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(aViewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ ExpandAll();
+ // Invalidate the sort so sorting will do something.
+ m_sortValid = false;
+ }
+
+ m_viewFlags = aViewFlags;
+
+ if (m_viewFolder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderInfo->SetViewFlags(aViewFlags);
+ } else
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::MarkThreadOfMsgRead(nsMsgKey msgId,
+ nsMsgViewIndex msgIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead,
+ bool bRead) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(msgIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgViewIndex threadIndex;
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsCOMPtr<nsIMsgDBHdr> firstHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(firstHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey firstHdrId;
+ firstHdr->GetMessageKey(&firstHdrId);
+ if (msgId != firstHdrId)
+ threadIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr);
+ else
+ threadIndex = msgIndex;
+
+ return MarkThreadRead(threadHdr, threadIndex, idsMarkedRead, bRead);
+}
+
+nsresult nsMsgDBView::MarkThreadRead(nsIMsgThread* threadHdr,
+ nsMsgViewIndex threadIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead,
+ bool bRead) {
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ idsMarkedRead.SetCapacity(numChildren);
+ for (int32_t childIndex = 0; childIndex < (int32_t)numChildren;
+ childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(msgHdr));
+ NS_ASSERTION(msgHdr, "msgHdr is null");
+ if (!msgHdr) continue;
+
+ bool isRead;
+ nsMsgKey hdrMsgId;
+ msgHdr->GetMessageKey(&hdrMsgId);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ db->IsRead(hdrMsgId, &isRead);
+
+ if (isRead != bRead) {
+ // MarkHdrRead will change the unread count on the thread.
+ db->MarkHdrRead(msgHdr, bRead, nullptr);
+ // Insert at the front. Should we insert at the end?
+ idsMarkedRead.InsertElementAt(0, hdrMsgId);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsMsgDBView::AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags) {
+ // If we're a cross-folder view, just bail on this.
+ if (GetFolders()) return *msgFlags & nsMsgMessageFlags::Read;
+
+ bool isRead = false;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ m_db->IsRead(msgKey, &isRead);
+ // Just make sure flag is right in db.
+#ifdef DEBUG_David_Bienvenu
+ NS_ASSERTION(isRead == ((*msgFlags & nsMsgMessageFlags::Read) != 0),
+ "msgFlags out of sync");
+#endif
+ if (isRead)
+ *msgFlags |= nsMsgMessageFlags::Read;
+ else
+ *msgFlags &= ~nsMsgMessageFlags::Read;
+
+ m_db->MarkHdrRead(msgHdr, isRead, nullptr);
+ return isRead;
+}
+
+// Starting from startIndex, performs the passed in navigation, including
+// any marking read needed, and returns the resultId and resultIndex of the
+// destination of the navigation. If no message is found in the view,
+// it returns a resultId of nsMsgKey_None and an resultIndex of
+// nsMsgViewIndex_None.
+NS_IMETHODIMP
+nsMsgDBView::ViewNavigate(nsMsgNavigationTypeValue motion, nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap) {
+ NS_ENSURE_ARG_POINTER(pResultKey);
+ NS_ENSURE_ARG_POINTER(pResultIndex);
+ NS_ENSURE_ARG_POINTER(pThreadIndex);
+
+ int32_t currentIndex;
+ nsMsgViewIndex startIndex;
+
+ if (!mTreeSelection) {
+ // We must be in stand alone message mode.
+ currentIndex = FindViewIndex(m_currentlyDisplayedMsgKey);
+ } else {
+ nsresult rv = mTreeSelection->GetCurrentIndex(&currentIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ startIndex = currentIndex;
+ return nsMsgDBView::NavigateFromPos(motion, startIndex, pResultKey,
+ pResultIndex, pThreadIndex, wrap);
+}
+
+nsresult nsMsgDBView::NavigateFromPos(nsMsgNavigationTypeValue motion,
+ nsMsgViewIndex startIndex,
+ nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap) {
+ nsresult rv = NS_OK;
+ nsMsgKey resultThreadKey;
+ nsMsgViewIndex curIndex;
+ nsMsgViewIndex lastIndex =
+ (GetSize() > 0) ? (nsMsgViewIndex)GetSize() - 1 : nsMsgViewIndex_None;
+ nsMsgViewIndex threadIndex = nsMsgViewIndex_None;
+
+ // If there aren't any messages in the view, bail out.
+ if (GetSize() <= 0) {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ *pResultKey = nsMsgKey_None;
+
+ switch (motion) {
+ case nsMsgNavigationType::firstMessage:
+ *pResultIndex = 0;
+ *pResultKey = m_keys[0];
+ break;
+ case nsMsgNavigationType::nextMessage:
+ // Return same index and id on next on last message.
+ *pResultIndex = std::min(startIndex + 1, lastIndex);
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::previousMessage:
+ if (startIndex != nsMsgViewIndex_None && startIndex > 0) {
+ *pResultIndex = startIndex - 1;
+ }
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::lastMessage:
+ *pResultIndex = lastIndex;
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstFlagged:
+ rv = FindFirstFlagged(pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::nextFlagged:
+ rv = FindNextFlagged(startIndex + 1, pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::previousFlagged:
+ rv = FindPrevFlagged(startIndex, pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstNew:
+ rv = FindFirstNew(pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstUnreadMessage:
+ startIndex = nsMsgViewIndex_None;
+ // Note fall through - is this motion ever used?
+ [[fallthrough]];
+ case nsMsgNavigationType::nextUnreadMessage:
+ for (curIndex = (startIndex == nsMsgViewIndex_None) ? 0 : startIndex;
+ curIndex <= lastIndex && lastIndex != nsMsgViewIndex_None;
+ curIndex++) {
+ uint32_t flags = m_flags[curIndex];
+ // Don't return start index since navigate should move.
+ if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) &&
+ (curIndex != startIndex)) {
+ *pResultIndex = curIndex;
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ }
+
+ // Check for collapsed thread with new children.
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ GetThreadContainingIndex(curIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (!threadHdr) continue;
+
+ uint32_t numUnreadChildren;
+ threadHdr->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ uint32_t numExpanded;
+ ExpandByIndex(curIndex, &numExpanded);
+ lastIndex += numExpanded;
+ if (pThreadIndex) *pThreadIndex = curIndex;
+ }
+ }
+ }
+
+ if (curIndex > lastIndex) {
+ // Wrap around by starting at index 0.
+ if (wrap) {
+ nsMsgKey startKey = GetAt(startIndex);
+ rv = NavigateFromPos(nsMsgNavigationType::nextUnreadMessage,
+ nsMsgViewIndex_None, pResultKey, pResultIndex,
+ pThreadIndex, false);
+
+ if (*pResultKey == startKey) {
+ // wrapped around and found start message!
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ }
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ }
+ }
+ break;
+ case nsMsgNavigationType::previousUnreadMessage:
+ if (!IsValidIndex(startIndex)) break;
+
+ rv = FindPrevUnread(m_keys[startIndex], pResultKey, &resultThreadKey);
+ if (NS_SUCCEEDED(rv)) {
+ *pResultIndex = FindViewIndex(*pResultKey);
+ if (*pResultKey != resultThreadKey &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ threadIndex = GetThreadIndex(*pResultIndex);
+ if (*pResultIndex == nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = m_db->GetMsgHdrForKey(*pResultKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (threadHdr) break;
+
+ uint32_t numUnreadChildren;
+ threadHdr->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ uint32_t numExpanded;
+ ExpandByIndex(threadIndex, &numExpanded);
+ }
+
+ *pResultIndex = FindViewIndex(*pResultKey);
+ }
+ }
+
+ if (pThreadIndex) *pThreadIndex = threadIndex;
+ }
+ break;
+ case nsMsgNavigationType::lastUnreadMessage:
+ break;
+ case nsMsgNavigationType::nextUnreadThread:
+ if (startIndex != nsMsgViewIndex_None) {
+ ApplyCommandToIndices(nsMsgViewCommandType::markThreadRead,
+ {startIndex});
+ }
+
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, startIndex,
+ pResultKey, pResultIndex, pThreadIndex, true);
+ case nsMsgNavigationType::toggleThreadKilled: {
+ bool resultKilled;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ ToggleIgnored(selection, &threadIndex, &resultKilled);
+ if (resultKilled) {
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadThread,
+ threadIndex, pResultKey, pResultIndex,
+ pThreadIndex, true);
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ }
+ case nsMsgNavigationType::toggleSubthreadKilled: {
+ bool resultKilled;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ ToggleMessageKilled(selection, &threadIndex, &resultKilled);
+ if (resultKilled) {
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage,
+ threadIndex, pResultKey, pResultIndex,
+ pThreadIndex, true);
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ }
+ // Check where navigate says this will take us. If we have the message
+ // in the view, return it. Otherwise, return an error.
+ case nsMsgNavigationType::back:
+ case nsMsgNavigationType::forward:
+ // Handled purely in JS.
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ break;
+ default:
+ NS_ERROR("unsupported motion");
+ break;
+ }
+
+ return NS_OK;
+}
+
+// Note that these routines do NOT expand collapsed threads! This mimics the
+// old behaviour, but it's also because we don't remember whether a thread
+// contains a flagged message the same way we remember if a thread contains
+// new messages. It would be painful to dive down into each collapsed thread
+// to update navigate status. We could cache this info, but it would still be
+// expensive the first time this status needs to get updated.
+nsresult nsMsgDBView::FindNextFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex) {
+ nsMsgViewIndex lastIndex = (nsMsgViewIndex)GetSize() - 1;
+ nsMsgViewIndex curIndex;
+
+ *pResultIndex = nsMsgViewIndex_None;
+
+ if (GetSize() > 0) {
+ for (curIndex = startIndex; curIndex <= lastIndex; curIndex++) {
+ uint32_t flags = m_flags[curIndex];
+ if (flags & nsMsgMessageFlags::Marked) {
+ *pResultIndex = curIndex;
+ break;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FindFirstNew(nsMsgViewIndex* pResultIndex) {
+ if (m_db) {
+ nsMsgKey firstNewKey = nsMsgKey_None;
+ m_db->GetFirstNew(&firstNewKey);
+ *pResultIndex = (firstNewKey != nsMsgKey_None) ? FindKey(firstNewKey, true)
+ : nsMsgViewIndex_None;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey,
+ nsMsgKey* resultThreadId) {
+ nsMsgViewIndex startIndex = FindViewIndex(startKey);
+ nsMsgViewIndex curIndex = startIndex;
+ nsresult rv = NS_MSG_MESSAGE_NOT_FOUND;
+
+ if (startIndex == nsMsgViewIndex_None) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ *pResultKey = nsMsgKey_None;
+ if (resultThreadId) *resultThreadId = nsMsgKey_None;
+
+ for (; (int)curIndex >= 0 && (*pResultKey == nsMsgKey_None); curIndex--) {
+ uint32_t flags = m_flags[curIndex];
+ if (curIndex != startIndex && flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & nsMsgMessageFlags::Elided) {
+ NS_ERROR("fix this");
+ // nsMsgKey threadId = m_keys[curIndex];
+ // rv = m_db->GetUnreadKeyInThread(threadId, pResultKey, resultThreadId);
+ if (NS_SUCCEEDED(rv) && (*pResultKey != nsMsgKey_None)) break;
+ }
+
+ if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) &&
+ (curIndex != startIndex)) {
+ *pResultKey = m_keys[curIndex];
+ rv = NS_OK;
+ break;
+ }
+ }
+
+ // Found unread message but we don't know the thread.
+ NS_ASSERTION(!(*pResultKey != nsMsgKey_None && resultThreadId &&
+ *resultThreadId == nsMsgKey_None),
+ "fix this");
+ return rv;
+}
+
+nsresult nsMsgDBView::FindFirstFlagged(nsMsgViewIndex* pResultIndex) {
+ return FindNextFlagged(0, pResultIndex);
+}
+
+nsresult nsMsgDBView::FindPrevFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex) {
+ nsMsgViewIndex curIndex;
+
+ *pResultIndex = nsMsgViewIndex_None;
+
+ if (GetSize() > 0 && IsValidIndex(startIndex)) {
+ curIndex = startIndex;
+ do {
+ if (curIndex != 0) curIndex--;
+
+ uint32_t flags = m_flags[curIndex];
+ if (flags & nsMsgMessageFlags::Marked) {
+ *pResultIndex = curIndex;
+ break;
+ }
+ } while (curIndex != 0);
+ }
+
+ return NS_OK;
+}
+
+bool nsMsgDBView::IsValidIndex(nsMsgViewIndex index) {
+ return index != nsMsgViewIndex_None &&
+ (index < (nsMsgViewIndex)m_keys.Length());
+}
+
+nsresult nsMsgDBView::OrExtraFlag(nsMsgViewIndex index, uint32_t orflag) {
+ uint32_t flag;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ flag = m_flags[index];
+ flag |= orflag;
+ m_flags[index] = flag;
+ OnExtraFlagChanged(index, flag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::AndExtraFlag(nsMsgViewIndex index, uint32_t andflag) {
+ uint32_t flag;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ flag = m_flags[index];
+ flag &= andflag;
+ m_flags[index] = flag;
+ OnExtraFlagChanged(index, flag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ m_flags[index] = extraflag;
+ OnExtraFlagChanged(index, extraflag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex,
+ bool* resultToggleState) {
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Ignored state is toggled based on the first selected thread.
+ nsMsgViewIndex threadIndex =
+ GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread));
+ NS_ENSURE_STATE(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t ignored = threadFlags & nsMsgMessageFlags::Ignored;
+
+ // Process threads in reverse order.
+ // Otherwise collapsing the threads will invalidate the indices.
+ threadIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < threadIndex) {
+ threadIndex =
+ GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread));
+ thread->GetFlags(&threadFlags);
+ if ((threadFlags & nsMsgMessageFlags::Ignored) == ignored)
+ SetThreadIgnored(thread, threadIndex, !ignored);
+ }
+ }
+
+ if (resultIndex) *resultIndex = threadIndex;
+
+ if (resultToggleState) *resultToggleState = !ignored;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleMessageKilled(
+ nsTArray<nsMsgViewIndex> const& selection, nsMsgViewIndex* resultIndex,
+ bool* resultToggleState) {
+ NS_ENSURE_ARG_POINTER(resultToggleState);
+
+ nsCOMPtr<nsIMsgDBHdr> header;
+ // Ignored state is toggled based on the first selected message.
+ nsresult rv = GetMsgHdrForViewIndex(selection[0], getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ uint32_t ignored = msgFlags & nsMsgMessageFlags::Ignored;
+
+ // Process messages in reverse order.
+ // Otherwise the indices may be invalidated.
+ nsMsgViewIndex msgIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < msgIndex) {
+ msgIndex = selection[numIndices];
+ rv = GetMsgHdrForViewIndex(msgIndex, getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+ header->GetFlags(&msgFlags);
+ if ((msgFlags & nsMsgMessageFlags::Ignored) == ignored)
+ SetSubthreadKilled(header, msgIndex, !ignored);
+ }
+ }
+
+ if (resultIndex) *resultIndex = msgIndex;
+
+ if (resultToggleState) *resultToggleState = !ignored;
+
+ return NS_OK;
+}
+
+nsMsgViewIndex nsMsgDBView::GetThreadFromMsgIndex(nsMsgViewIndex index,
+ nsIMsgThread** threadHdr) {
+ if (threadHdr == nullptr) return nsMsgViewIndex_None;
+ nsMsgKey msgKey = GetAt(index);
+
+ nsresult rv = GetThreadContainingIndex(index, threadHdr);
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+
+ if (*threadHdr == nullptr) return nsMsgViewIndex_None;
+
+ nsMsgKey threadKey;
+ (*threadHdr)->GetThreadKey(&threadKey);
+ nsMsgViewIndex threadIndex;
+ if (msgKey != threadKey)
+ threadIndex = GetIndexOfFirstDisplayedKeyInThread(*threadHdr);
+ else
+ threadIndex = index;
+ return threadIndex;
+}
+
+nsresult nsMsgDBView::ToggleWatched(nsTArray<nsMsgViewIndex> const& selection) {
+ MOZ_ASSERT(!selection.IsEmpty());
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Watched state is toggled based on the first selected thread.
+ nsMsgViewIndex threadIndex =
+ GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread));
+ NS_ENSURE_STATE(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t watched = threadFlags & nsMsgMessageFlags::Watched;
+
+ // Process threads in reverse order for consistency with ToggleIgnored.
+ threadIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < threadIndex) {
+ threadIndex =
+ GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread));
+ thread->GetFlags(&threadFlags);
+ if ((threadFlags & nsMsgMessageFlags::Watched) == watched)
+ SetThreadWatched(thread, threadIndex, !watched);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetThreadIgnored(nsIMsgThread* thread,
+ nsMsgViewIndex threadIndex,
+ bool ignored) {
+ if (!IsValidIndex(threadIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ if (ignored) {
+ nsTArray<nsMsgKey> idsMarkedRead;
+ MarkThreadRead(thread, threadIndex, idsMarkedRead, true);
+ CollapseByIndex(threadIndex, nullptr);
+ }
+
+ if (!m_db) return NS_ERROR_FAILURE;
+
+ return m_db->MarkThreadIgnored(thread, m_keys[threadIndex], ignored, this);
+}
+
+nsresult nsMsgDBView::SetSubthreadKilled(nsIMsgDBHdr* header,
+ nsMsgViewIndex msgIndex,
+ bool ignored) {
+ if (!IsValidIndex(msgIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NoteChange(msgIndex, 1, nsMsgViewNotificationCode::changed);
+
+ if (!m_db) return NS_ERROR_FAILURE;
+
+ nsresult rv = m_db->MarkHeaderKilled(header, ignored, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (ignored) {
+ nsCOMPtr<nsIMsgThread> thread;
+ nsresult rv;
+ rv = GetThreadContainingMsgHdr(header, getter_AddRefs(thread));
+ // So we didn't mark threads read.
+ if (NS_FAILED(rv)) return NS_OK;
+
+ uint32_t children, current;
+ thread->GetNumChildren(&children);
+
+ nsMsgKey headKey;
+ header->GetMessageKey(&headKey);
+
+ for (current = 0; current < children; current++) {
+ nsMsgKey newKey;
+ thread->GetChildKeyAt(current, &newKey);
+ if (newKey == headKey) break;
+ }
+
+ // Process all messages, starting with this message.
+ for (; current < children; current++) {
+ nsCOMPtr<nsIMsgDBHdr> nextHdr;
+ bool isKilled;
+
+ thread->GetChildHdrAt(current, getter_AddRefs(nextHdr));
+ nextHdr->GetIsKilled(&isKilled);
+
+ // Ideally, the messages should stop processing here.
+ // However, the children are ordered not by thread...
+ if (isKilled) nextHdr->MarkRead(true);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetThreadWatched(nsIMsgThread* thread,
+ nsMsgViewIndex index, bool watched) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsresult rv = m_db->MarkThreadWatched(thread, m_keys[index], watched, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetViewFolder(nsIMsgFolder* aMsgFolder) {
+ m_viewFolder = aMsgFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_viewFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetNumSelected(uint32_t* aNumSelected) {
+ NS_ENSURE_ARG_POINTER(aNumSelected);
+
+ if (!mTreeSelection) {
+ // No tree selection can mean we're in the stand alone mode.
+ *aNumSelected = (m_currentlyDisplayedMsgKey != nsMsgKey_None) ? 1 : 0;
+ return NS_OK;
+ }
+
+ bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads();
+
+ // We call this a lot from the front end JS, so make it fast.
+ nsresult rv = mTreeSelection->GetCount((int32_t*)aNumSelected);
+ if (!*aNumSelected || !includeCollapsedMsgs ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return rv;
+
+ int32_t numSelectedIncludingCollapsed = *aNumSelected;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ int32_t numIndices = selection.Length();
+ // Iterate over the selection, counting up the messages in collapsed
+ // threads.
+ for (int32_t i = 0; i < numIndices; i++) {
+ if (m_flags[selection[i]] & nsMsgMessageFlags::Elided) {
+ int32_t collapsedCount;
+ ExpansionDelta(selection[i], &collapsedCount);
+ numSelectedIncludingCollapsed += collapsedCount;
+ }
+ }
+
+ *aNumSelected = numSelectedIncludingCollapsed;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ return (m_folder) ? m_folder->GetTotalMessages(false, aNumMsgs)
+ : NS_ERROR_FAILURE;
+}
+
+/**
+ * @note For the IMAP delete model, this applies to both deleting and
+ * undeleting a message.
+ */
+NS_IMETHODIMP
+nsMsgDBView::GetMsgToSelectAfterDelete(nsMsgViewIndex* msgToSelectAfterDelete) {
+ NS_ENSURE_ARG_POINTER(msgToSelectAfterDelete);
+ *msgToSelectAfterDelete = nsMsgViewIndex_None;
+
+ bool isMultiSelect = false;
+ int32_t startFirstRange = nsMsgViewIndex_None;
+ int32_t endFirstRange = nsMsgViewIndex_None;
+ if (!mTreeSelection) {
+ // If we don't have a tree selection then we must be in stand alone mode.
+ // return the index of the current message key as the first selected index.
+ *msgToSelectAfterDelete = FindViewIndex(m_currentlyDisplayedMsgKey);
+ } else {
+ int32_t selectionCount;
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeCount(&selectionCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t i = 0; i < selectionCount; i++) {
+ rv = mTreeSelection->GetRangeAt(i, &startRange, &endRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save off the first range in case we need it later.
+ if (i == 0) {
+ startFirstRange = startRange;
+ endFirstRange = endRange;
+ } else {
+ // If the tree selection is goofy (eg adjacent or overlapping ranges),
+ // complain about it, but don't try and cope. Just live with the fact
+ // that one of the deleted messages is going to end up selected.
+ NS_WARNING_ASSERTION(
+ endFirstRange != startRange,
+ "goofy tree selection state: two ranges are adjacent!");
+ }
+
+ *msgToSelectAfterDelete =
+ std::min(*msgToSelectAfterDelete, (nsMsgViewIndex)startRange);
+ }
+
+ // Multiple selection either using Ctrl, Shift, or one of the affordances
+ // to select an entire thread.
+ isMultiSelect = (selectionCount > 1 || (endRange - startRange) > 0);
+ }
+
+ if (*msgToSelectAfterDelete == nsMsgViewIndex_None) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ GetMsgFolder(getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ bool thisIsImapFolder = (imapFolder != nullptr);
+ // Need to update the imap-delete model, can change more than once in a
+ // session.
+ if (thisIsImapFolder) GetImapDeleteModel(nullptr);
+
+ // If mail.delete_matches_sort_order is true,
+ // for views sorted in descending order (newest at the top), make
+ // msgToSelectAfterDelete advance in the same direction as the sort order.
+ bool deleteMatchesSort = false;
+ if (m_sortOrder == nsMsgViewSortOrder::descending &&
+ *msgToSelectAfterDelete) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mail.delete_matches_sort_order",
+ &deleteMatchesSort);
+ }
+
+ if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ if (isMultiSelect) {
+ if (deleteMatchesSort)
+ *msgToSelectAfterDelete = startFirstRange - 1;
+ else
+ *msgToSelectAfterDelete = endFirstRange + 1;
+ } else {
+ if (deleteMatchesSort)
+ *msgToSelectAfterDelete -= 1;
+ else
+ *msgToSelectAfterDelete += 1;
+ }
+ } else if (deleteMatchesSort) {
+ *msgToSelectAfterDelete -= 1;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRemoveRowOnMoveOrDelete(bool* aRemoveRowOnMoveOrDelete) {
+ NS_ENSURE_ARG_POINTER(aRemoveRowOnMoveOrDelete);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder);
+ if (!imapFolder) {
+ *aRemoveRowOnMoveOrDelete = true;
+ return NS_OK;
+ }
+
+ // Need to update the imap-delete model, can change more than once in a
+ // session.
+ GetImapDeleteModel(nullptr);
+
+ // Unlike the other imap delete models, "mark as deleted" does not remove
+ // rows on delete (or move).
+ *aRemoveRowOnMoveOrDelete =
+ (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCurrentlyDisplayedMessage(
+ nsMsgViewIndex* currentlyDisplayedMessage) {
+ NS_ENSURE_ARG_POINTER(currentlyDisplayedMessage);
+ *currentlyDisplayedMessage = FindViewIndex(m_currentlyDisplayedMsgKey);
+ return NS_OK;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) {
+ NS_ENSURE_ARG_POINTER(hdr);
+
+ nsresult rv;
+ nsMsgKey key;
+ rv = GetKeyForFirstSelectedMessage(&key);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ if (key == nsMsgKey_None) {
+ *hdr = nullptr;
+ return NS_OK;
+ }
+
+ if (!m_db) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ rv = m_db->GetMsgHdrForKey(key, hdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgDBView::GetURIForFirstSelectedMessage(nsACString& uri) {
+ nsresult rv;
+ nsMsgViewIndex viewIndex;
+ rv = GetViewIndexForFirstSelectedMsg(&viewIndex);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ return GetURIForViewIndex(viewIndex, uri);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnDeleteCompleted(bool aSucceeded) {
+ if (m_deletingRows && aSucceeded) {
+ uint32_t numIndices = mIndicesToNoteChange.Length();
+ if (numIndices && (mTree || mJSTree)) {
+ if (numIndices > 1) mIndicesToNoteChange.Sort();
+
+ // The call to NoteChange() has to happen after we are done removing the
+ // keys as NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ if (numIndices > 1) {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ }
+
+ for (uint32_t i = 0; i < numIndices; i++)
+ NoteChange(mIndicesToNoteChange[i], -1,
+ nsMsgViewNotificationCode::insertOrDelete);
+
+ if (numIndices > 1) {
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+ }
+
+ mIndicesToNoteChange.Clear();
+ }
+
+ m_deletingRows = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetDb(nsIMsgDatabase** aDB) {
+ NS_ENSURE_ARG_POINTER(aDB);
+ NS_IF_ADDREF(*aDB = m_db);
+ return NS_OK;
+}
+
+bool nsMsgDBView::OfflineMsgSelected(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+ if (localFolder) {
+ return true;
+ }
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ // For cross-folder saved searches, we need to check if any message
+ // is in a local folder.
+ if (!m_folder) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ GetFolderForViewIndex(viewIndex, getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder);
+ if (localFolder) {
+ return true;
+ }
+ }
+
+ uint32_t flags = m_flags[viewIndex];
+ if ((flags & nsMsgMessageFlags::Offline)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool nsMsgDBView::NonDummyMsgSelected(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads();
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ uint32_t flags = m_flags[viewIndex];
+ // We now treat having a collapsed dummy message selected as if
+ // the whole group was selected so we can apply commands to the group.
+ if (!(flags & MSG_VIEW_FLAG_DUMMY) ||
+ (flags & nsMsgMessageFlags::Elided && includeCollapsedMsgs)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewIndexForFirstSelectedMsg(nsMsgViewIndex* aViewIndex) {
+ NS_ENSURE_ARG_POINTER(aViewIndex);
+ // If we don't have a tree selection we must be in stand alone mode...
+ if (!mTreeSelection) {
+ *aViewIndex = m_currentlyDisplayedViewIndex;
+ return NS_OK;
+ }
+
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ // Check that the first index is valid, it may not be if nothing is selected.
+ if (startRange < 0 || uint32_t(startRange) >= GetSize())
+ return NS_ERROR_UNEXPECTED;
+
+ *aViewIndex = startRange;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetKeyForFirstSelectedMessage(nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+ // If we don't have a tree selection we must be in stand alone mode...
+ if (!mTreeSelection) {
+ *key = m_currentlyDisplayedMsgKey;
+ return NS_OK;
+ }
+
+ int32_t selectionCount;
+ mTreeSelection->GetRangeCount(&selectionCount);
+ if (selectionCount == 0) {
+ *key = nsMsgKey_None;
+ return NS_OK;
+ }
+
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ // Check that the first index is valid, it may not be if nothing is selected.
+ if (startRange < 0 || uint32_t(startRange) >= GetSize())
+ return NS_ERROR_UNEXPECTED;
+
+ if (m_flags[startRange] & MSG_VIEW_FLAG_DUMMY)
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *key = m_keys[startRange];
+ return NS_OK;
+}
+
+nsCOMArray<nsIMsgFolder>* nsMsgDBView::GetFolders() { return nullptr; }
+
+nsresult nsMsgDBView::AdjustRowCount(int32_t rowCountBeforeSort,
+ int32_t rowCountAfterSort) {
+ int32_t rowChange = rowCountAfterSort - rowCountBeforeSort;
+
+ if (rowChange) {
+ // This is not safe to use when you have a selection.
+ // RowCountChanged() will call AdjustSelection().
+ uint32_t numSelected = 0;
+ GetNumSelected(&numSelected);
+ NS_ASSERTION(
+ numSelected == 0,
+ "it is not save to call AdjustRowCount() when you have a selection");
+
+ if (mTree) mTree->RowCountChanged(0, rowChange);
+ if (mJSTree) mJSTree->RowCountChanged(0, rowChange);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetImapDeleteModel(nsIMsgFolder* folder) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // For the search view.
+ if (folder)
+ folder->GetServer(getter_AddRefs(server));
+ else if (m_folder)
+ m_folder->GetServer(getter_AddRefs(server));
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv);
+ if (NS_SUCCEEDED(rv) && imapServer) imapServer->GetDeleteModel(&mDeleteModel);
+
+ return rv;
+}
+
+//
+// CanDrop
+//
+// Can't drop on the thread pane.
+//
+NS_IMETHODIMP
+nsMsgDBView::CanDrop(int32_t index, int32_t orient,
+ mozilla::dom::DataTransfer* dataTransfer, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+
+ return NS_OK;
+}
+
+//
+// Drop
+//
+// Can't drop on the thread pane.
+//
+NS_IMETHODIMP
+nsMsgDBView::Drop(int32_t row, int32_t orient,
+ mozilla::dom::DataTransfer* dataTransfer) {
+ return NS_OK;
+}
+
+//
+// IsSorted
+//
+// ...
+//
+NS_IMETHODIMP
+nsMsgDBView::IsSorted(bool* _retval) {
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectFolderMsgByKey(nsIMsgFolder* aFolder, nsMsgKey aKey) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ if (aKey == nsMsgKey_None) return NS_ERROR_FAILURE;
+
+ // This is OK for non search views.
+
+ nsMsgViewIndex viewIndex = FindKey(aKey, true /* expand */);
+
+ if (mTree) mTreeSelection->SetCurrentIndex(viewIndex);
+
+ // Make sure the current message is once again visible in the thread pane
+ // so we don't have to go search for it in the thread pane.
+ if (mTree && viewIndex != nsMsgViewIndex_None) {
+ mTreeSelection->Select(viewIndex);
+ mTree->EnsureRowIsVisible(viewIndex);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectMsgByKey(nsMsgKey aKey) {
+ NS_ASSERTION(aKey != nsMsgKey_None, "bad key");
+ if (aKey == nsMsgKey_None) return NS_OK;
+
+ // Use SaveAndClearSelection()
+ // and RestoreSelection() so that we'll clear the current selection
+ // but pass in a different key array so that we'll
+ // select (and load) the desired message.
+
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ nsresult rv = SaveAndClearSelection(nullptr, preservedSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now, restore our desired selection.
+ AutoTArray<nsMsgKey, 1> keyArray;
+ keyArray.AppendElement(aKey);
+
+ // If the key was not found
+ // (this can happen with "remember last selected message")
+ // nothing will be selected.
+ rv = RestoreSelection(aKey, keyArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgDBView* newMsgDBView = new nsMsgDBView();
+
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ NS_ENSURE_ARG_POINTER(aNewMsgDBView);
+ if (aMsgWindow) {
+ aNewMsgDBView->mMsgWindowWeak = do_GetWeakReference(aMsgWindow);
+ aMsgWindow->SetOpenFolder(m_viewFolder ? m_viewFolder : m_folder);
+ }
+
+ aNewMsgDBView->mMessengerWeak = do_GetWeakReference(aMessengerInstance);
+ aNewMsgDBView->mCommandUpdater = do_GetWeakReference(aCmdUpdater);
+ aNewMsgDBView->m_folder = m_folder;
+ aNewMsgDBView->m_viewFlags = m_viewFlags;
+ aNewMsgDBView->m_sortOrder = m_sortOrder;
+ aNewMsgDBView->m_sortType = m_sortType;
+ aNewMsgDBView->m_curCustomColumn = m_curCustomColumn;
+ aNewMsgDBView->m_secondarySort = m_secondarySort;
+ aNewMsgDBView->m_secondarySortOrder = m_secondarySortOrder;
+ aNewMsgDBView->m_secondaryCustomColumn = m_secondaryCustomColumn;
+ aNewMsgDBView->m_db = m_db;
+ if (m_db) aNewMsgDBView->m_db->AddListener(aNewMsgDBView);
+
+ aNewMsgDBView->mIsNews = mIsNews;
+ aNewMsgDBView->mIsRss = mIsRss;
+ aNewMsgDBView->mIsXFVirtual = mIsXFVirtual;
+ aNewMsgDBView->mShowSizeInLines = mShowSizeInLines;
+ aNewMsgDBView->mDeleteModel = mDeleteModel;
+ aNewMsgDBView->m_flags = m_flags.Clone();
+ aNewMsgDBView->m_levels = m_levels.Clone();
+ aNewMsgDBView->m_keys = m_keys.Clone();
+
+ aNewMsgDBView->m_customColumnHandlerIDs = m_customColumnHandlerIDs.Clone();
+ aNewMsgDBView->m_customColumnHandlers.AppendObjects(m_customColumnHandlers);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSearchSession(nsIMsgSearchSession** aSession) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSupportsThreading(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::FindIndexFromKey(nsMsgKey aMsgKey, bool aExpand,
+ nsMsgViewIndex* aIndex) {
+ NS_ENSURE_ARG_POINTER(aIndex);
+ *aIndex = FindKey(aMsgKey, aExpand);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::FindIndexOfMsgHdr(nsIMsgDBHdr* aMsgHdr, bool aExpand,
+ nsMsgViewIndex* aIndex) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aIndex);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(aMsgHdr);
+ if (threadIndex != nsMsgViewIndex_None) {
+ if (aExpand && (m_flags[threadIndex] & nsMsgMessageFlags::Elided))
+ ExpandByIndex(threadIndex, nullptr);
+
+ *aIndex = FindHdr(aMsgHdr, threadIndex);
+ } else {
+ *aIndex = nsMsgViewIndex_None;
+ }
+ } else {
+ *aIndex = FindHdr(aMsgHdr);
+ }
+
+ return NS_OK;
+}
+
+static void getDateFormatPref(nsIPrefBranch* _prefBranch,
+ const char* _prefLocalName,
+ nsDateFormatSelectorComm& _format) {
+ // Read.
+ int32_t nFormatSetting(0);
+ nsresult result = _prefBranch->GetIntPref(_prefLocalName, &nFormatSetting);
+ if (NS_SUCCEEDED(result)) {
+ // Translate.
+ nsDateFormatSelectorComm res;
+ res = static_cast<nsDateFormatSelectorComm>(nFormatSetting);
+ // Transfer if valid.
+ if (res >= kDateFormatNone && res <= kDateFormatShort)
+ _format = res;
+ else if (res == kDateFormatWeekday)
+ _format = res;
+ }
+}
+
+nsresult nsMsgDBView::InitDisplayFormats() {
+ m_dateFormatsInitialized = true;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrefBranch> dateFormatPrefs;
+ rv = prefs->GetBranch("mail.ui.display.dateformat.",
+ getter_AddRefs(dateFormatPrefs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ getDateFormatPref(dateFormatPrefs, "default", m_dateFormatDefault);
+ getDateFormatPref(dateFormatPrefs, "thisweek", m_dateFormatThisWeek);
+ getDateFormatPref(dateFormatPrefs, "today", m_dateFormatToday);
+ return rv;
+}
+
+void nsMsgDBView::SetMRUTimeForFolder(nsIMsgFolder* folder) {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ folder->SetStringProperty(MRU_TIME_PROPERTY, nowStr);
+}
+
+nsMsgDBView::nsMsgViewHdrEnumerator::nsMsgViewHdrEnumerator(nsMsgDBView* view) {
+ // We need to clone the view because the caller may clear the
+ // current view immediately. It also makes it easier to expand all
+ // if we're working on a copy.
+ nsCOMPtr<nsIMsgDBView> clonedView;
+ view->CloneDBView(nullptr, nullptr, nullptr, getter_AddRefs(clonedView));
+ m_view = static_cast<nsMsgDBView*>(clonedView.get());
+ // Make sure we enumerate over collapsed threads by expanding all.
+ m_view->ExpandAll();
+ m_curHdrIndex = 0;
+}
+
+nsMsgDBView::nsMsgViewHdrEnumerator::~nsMsgViewHdrEnumerator() {
+ if (m_view) m_view->Close();
+}
+
+NS_IMETHODIMP
+nsMsgDBView::nsMsgViewHdrEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ if (m_curHdrIndex >= m_view->GetSize()) return NS_ERROR_FAILURE;
+
+ // Ignore dummy header. We won't have empty groups, so
+ // we know the view index is good.
+ if (m_view->m_flags[m_curHdrIndex] & MSG_VIEW_FLAG_DUMMY) ++m_curHdrIndex;
+
+ nsCOMPtr<nsIMsgDBHdr> nextHdr;
+
+ nsresult rv =
+ m_view->GetMsgHdrForViewIndex(m_curHdrIndex++, getter_AddRefs(nextHdr));
+ nextHdr.forget(aItem);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::nsMsgViewHdrEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_curHdrIndex < m_view->GetSize();
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetViewEnumerator(nsIMsgEnumerator** enumerator) {
+ NS_IF_ADDREF(*enumerator = new nsMsgViewHdrEnumerator(this));
+ return (*enumerator) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult nsMsgDBView::GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folder->GetMsgDatabase(db);
+}
+
+/**
+ * Determine whether junk commands should be enabled on this view.
+ * Junk commands are always enabled for mail. For nntp and rss, they
+ * may be selectively enabled using an inherited folder property.
+ *
+ * @param aViewIndex view index of the message to check
+ * @return true if junk controls should be enabled
+ */
+bool nsMsgDBView::JunkControlsEnabled(nsMsgViewIndex aViewIndex) {
+ // For normal mail, junk commands are always enabled.
+ if (!(mIsNews || mIsRss || mIsXFVirtual)) return true;
+
+ // We need to check per message or folder.
+ nsCOMPtr<nsIMsgFolder> folder = m_folder;
+ if (!folder && IsValidIndex(aViewIndex))
+ GetFolderForViewIndex(aViewIndex, getter_AddRefs(folder));
+
+ if (folder) {
+ // Check if this is a mail message in search folders.
+ if (mIsXFVirtual) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ folder->GetServer(getter_AddRefs(server));
+ nsAutoCString type;
+ if (server) server->GetType(type);
+
+ if (!(type.LowerCaseEqualsLiteral("nntp") ||
+ type.LowerCaseEqualsLiteral("rss")))
+ return true;
+ }
+
+ // For rss and news, check the inherited folder property.
+ nsAutoCString junkEnableOverride;
+ folder->GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk",
+ junkEnableOverride);
+ if (junkEnableOverride.EqualsLiteral("true")) return true;
+ }
+
+ return false;
+}
diff --git a/comm/mailnews/base/src/nsMsgDBView.h b/comm/mailnews/base/src/nsMsgDBView.h
new file mode 100644
index 0000000000..6033a490bc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBView.h
@@ -0,0 +1,558 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgDBView_H_
+#define _nsMsgDBView_H_
+
+#include "nsIMsgDBView.h"
+#include "nsIMsgWindow.h"
+#include "nsIMessenger.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "MailNewsTypes.h"
+#include "nsIDBChangeListener.h"
+#include "nsITreeView.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsITreeSelection.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgThread.h"
+#include "nsMsgUtils.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIStringBundle.h"
+#include "nsMsgTagService.h"
+#include "nsCOMArray.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIMsgCustomColumnHandler.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsMsgEnumerator.h"
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+typedef AutoTArray<nsMsgViewIndex, 1> nsMsgViewIndexArray;
+static_assert(nsMsgViewIndex(nsMsgViewIndexArray::NoIndex) ==
+ nsMsgViewIndex_None,
+ "These need to be the same value.");
+
+enum eFieldType { kCollationKey, kU32 };
+
+// This is used in an nsTArray<> to keep track of a multi-column sort.
+class MsgViewSortColumnInfo {
+ public:
+ MsgViewSortColumnInfo(const MsgViewSortColumnInfo& other);
+ MsgViewSortColumnInfo()
+ : mSortType(nsMsgViewSortType::byNone),
+ mSortOrder(nsMsgViewSortOrder::none) {}
+ bool operator==(const MsgViewSortColumnInfo& other) const;
+ nsMsgViewSortTypeValue mSortType;
+ nsMsgViewSortOrderValue mSortOrder;
+ // If mSortType == byCustom, info about the custom column sort.
+ nsString mCustomColumnName;
+ nsCOMPtr<nsIMsgCustomColumnHandler> mColHandler;
+};
+
+// Reserve some bits in the msg flags for the view-only flags.
+// NOTE: this bit space is shared by nsMsgMessageFlags (and labels).
+#define MSG_VIEW_FLAG_ISTHREAD 0x8000000
+#define MSG_VIEW_FLAG_DUMMY 0x20000000
+#define MSG_VIEW_FLAG_HASCHILDREN 0x40000000
+#define MSG_VIEW_FLAGS \
+ (MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_ISTHREAD)
+
+// Helper struct for sorting by numeric fields.
+// Associates a message with a key for ordering it in the view.
+struct IdUint32 {
+ nsMsgKey id;
+ uint32_t bits;
+ uint32_t dword; // The numeric key.
+ nsIMsgFolder* folder;
+};
+
+// Extends IdUint32 for sorting by a collation key field (eg subject).
+// (Also used as IdUint32 a couple of places to simplify the code, where
+// the overhead of an unused nsTArray isn't a big deal).
+struct IdKey : public IdUint32 {
+ nsTArray<uint8_t> key;
+};
+
+class nsMsgDBViewService final : public nsIMsgDBViewService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDBVIEWSERVICE
+
+ nsMsgDBViewService(){};
+
+ protected:
+ ~nsMsgDBViewService(){};
+};
+
+// This is an abstract implementation class.
+// The actual view objects will be instances of sub-classes of this class.
+class nsMsgDBView : public nsIMsgDBView,
+ public nsIDBChangeListener,
+ public nsITreeView,
+ public nsIJunkMailClassificationListener {
+ public:
+ friend class nsMsgDBViewService;
+ nsMsgDBView();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDBVIEW
+ NS_DECL_NSIDBCHANGELISTENER
+ NS_DECL_NSITREEVIEW
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+
+ nsMsgViewIndex GetInsertIndexHelper(nsIMsgDBHdr* msgHdr,
+ nsTArray<nsMsgKey>& keys,
+ nsCOMArray<nsIMsgFolder>* folders,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewSortTypeValue sortType);
+ int32_t SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1, nsMsgKey key2,
+ nsIMsgFolder* folder2,
+ class viewSortInfo* comparisonContext);
+
+ protected:
+ virtual ~nsMsgDBView();
+
+ static nsString kHighestPriorityString;
+ static nsString kHighPriorityString;
+ static nsString kLowestPriorityString;
+ static nsString kLowPriorityString;
+ static nsString kNormalPriorityString;
+
+ static nsString kReadString;
+ static nsString kRepliedString;
+ static nsString kForwardedString;
+ static nsString kRedirectedString;
+ static nsString kNewString;
+
+ // Used for group views.
+ static nsString kTodayString;
+ static nsString kYesterdayString;
+ static nsString kLastWeekString;
+ static nsString kTwoWeeksAgoString;
+ static nsString kOldMailString;
+ static nsString kFutureDateString;
+
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ nsCOMPtr<nsIMsgJSTree> mJSTree;
+ nsCOMPtr<nsITreeSelection> mTreeSelection;
+ // We cache this to determine when to push command status notifications.
+ uint32_t mNumSelectedRows;
+ // Set when the message pane is collapsed.
+ bool mSuppressMsgDisplay;
+ bool mSuppressCommandUpdating;
+ // Set when we're telling the outline a row is being removed. Used to
+ // suppress msg loading during delete/move operations.
+ bool mRemovingRow;
+ bool mCommandsNeedDisablingBecauseOfSelection;
+ bool mSuppressChangeNotification;
+
+ virtual const char* GetViewName(void) { return "MsgDBView"; }
+ nsresult FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aAuthorString);
+ nsresult FetchRecipients(nsIMsgDBHdr* aHdr, nsAString& aRecipientsString);
+ nsresult FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags,
+ nsAString& aValue);
+ nsresult FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString,
+ bool rcvDate = false);
+ nsresult FetchStatus(uint32_t aFlags, nsAString& aStatusString);
+ nsresult FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString);
+ nsresult FetchPriority(nsIMsgDBHdr* aHdr, nsAString& aPriorityString);
+ nsresult FetchLabel(nsIMsgDBHdr* aHdr, nsAString& aLabelString);
+ nsresult FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString);
+ nsresult FetchKeywords(nsIMsgDBHdr* aHdr, nsACString& keywordString);
+ nsresult FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr,
+ nsACString& keywordString);
+ nsresult FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount);
+ bool IsOutgoingMsg(nsIMsgDBHdr* aHdr);
+
+ // The default enumerator is over the db, but things like
+ // quick search views will enumerate just the displayed messages.
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator);
+ // this is a message enumerator that enumerates based on the view contents
+ virtual nsresult GetViewEnumerator(nsIMsgEnumerator** enumerator);
+
+ // Save and Restore Selection are a pair of routines you should
+ // use when performing an operation which is going to change the view
+ // and you want to remember the selection. (i.e. for sorting).
+ // Call SaveAndClearSelection and we'll give you an array of msg keys for
+ // the current selection. We also freeze and clear the selection.
+ // When you are done changing the view,
+ // call RestoreSelection passing in the same array
+ // and we'll restore the selection AND unfreeze selection in the UI.
+ nsresult SaveAndClearSelection(nsMsgKey* aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray);
+ nsresult RestoreSelection(nsMsgKey aCurrentmsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray);
+
+ // This is not safe to use when you have a selection.
+ // RowCountChanged() will call AdjustSelection().
+ // It should be called after SaveAndClearSelection() and before
+ // RestoreSelection().
+ nsresult AdjustRowCount(int32_t rowCountBeforeSort,
+ int32_t rowCountAfterSort);
+
+ nsresult GenerateURIForMsgKey(nsMsgKey aMsgKey, nsIMsgFolder* folder,
+ nsACString& aURI);
+
+ // Routines used in building up view.
+ virtual bool WantsThisThread(nsIMsgThread* thread);
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex = nullptr);
+ bool GetShowingIgnored() {
+ return (m_viewFlags & nsMsgViewFlagsType::kShowIgnored) != 0;
+ }
+ bool OperateOnMsgsInCollapsedThreads();
+
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* aNewHdr, nsMsgKey parentKey,
+ bool ensureListed);
+ virtual nsMsgViewIndex GetInsertIndex(nsIMsgDBHdr* msgHdr);
+ nsMsgViewIndex GetIndexForThread(nsIMsgDBHdr* hdr);
+ nsMsgViewIndex GetThreadRootIndex(nsIMsgDBHdr* msgHdr);
+ virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr);
+ // Given a view index, return the index of the top-level msg in the thread.
+ nsMsgViewIndex GetThreadIndex(nsMsgViewIndex msgIndex);
+
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level);
+ virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level);
+ virtual void InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows);
+ virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows);
+ nsresult ToggleExpansion(nsMsgViewIndex index, uint32_t* numChanged);
+ nsresult ExpandByIndex(nsMsgViewIndex index, uint32_t* pNumExpanded);
+ nsresult CollapseByIndex(nsMsgViewIndex index, uint32_t* pNumCollapsed);
+ nsresult ExpandAll();
+ nsresult CollapseAll();
+ nsresult ExpandAndSelectThread();
+
+ // Helper routines for thread expanding and collapsing.
+ nsresult GetThreadCount(nsMsgViewIndex viewIndex, uint32_t* pThreadCount);
+ /**
+ * Retrieve the view index of the first displayed message in the given thread.
+ * @param threadHdr The thread you care about.
+ * @param allowDummy Should dummy headers be returned when the non-dummy
+ * header is available? If the root node of the thread is a dummy header
+ * and you pass false, then we will return the first child of the thread
+ * unless the thread is elided, in which case we will return the root.
+ * If you pass true, we will always return the root.
+ * @return the view index of the first message in the thread, if any.
+ */
+ nsMsgViewIndex GetIndexOfFirstDisplayedKeyInThread(nsIMsgThread* threadHdr,
+ bool allowDummy = false);
+ virtual nsresult GetFirstMessageHdrToDisplayInThread(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr** result);
+ virtual nsMsgViewIndex ThreadIndexOfMsg(
+ nsMsgKey msgKey, nsMsgViewIndex msgIndex = nsMsgViewIndex_None,
+ int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr);
+ nsMsgViewIndex ThreadIndexOfMsgHdr(
+ nsIMsgDBHdr* msgHdr, nsMsgViewIndex msgIndex = nsMsgViewIndex_None,
+ int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr);
+ nsMsgKey GetKeyOfFirstMsgInThread(nsMsgKey key);
+ int32_t CountExpandedThread(nsMsgViewIndex index);
+ virtual nsresult ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta);
+ void ReverseSort();
+ void ReverseThreads();
+ nsresult SaveSortInfo(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ nsresult RestoreSortInfo();
+ nsresult PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo);
+ void SetMRUTimeForFolder(nsIMsgFolder* folder);
+
+ nsMsgKey GetAt(nsMsgViewIndex index) {
+ return m_keys.SafeElementAt(index, nsMsgKey_None);
+ }
+
+ nsMsgViewIndex FindViewIndex(nsMsgKey key) { return FindKey(key, false); }
+ /**
+ * Find the message header if it is visible in this view. (Messages in
+ * threads/groups that are elided will not be
+ * @param msgHdr Message header to look for.
+ * @param startIndex The index to start looking from.
+ * @param allowDummy Are dummy headers acceptable? If yes, then for a group
+ * with a dummy header, we return the root of the thread (the dummy
+ * header), otherwise we return the actual "content" header for the
+ * message.
+ * @return The view index of the header found, if any.
+ */
+ virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex = 0,
+ bool allowDummy = false);
+ virtual nsMsgViewIndex FindKey(nsMsgKey key, bool expand);
+ virtual nsresult GetDBForViewIndex(nsMsgViewIndex index, nsIMsgDatabase** db);
+ virtual nsCOMArray<nsIMsgFolder>* GetFolders();
+ virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder);
+
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex viewIndex,
+ uint32_t* pNumListed);
+ nsresult ListUnreadIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed);
+ nsMsgViewIndex FindParentInThread(nsMsgKey parentKey,
+ nsMsgViewIndex startOfThreadViewIndex);
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed);
+ uint32_t GetSize(void) { return (m_keys.Length()); }
+
+ // For commands.
+ virtual nsresult ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder);
+ virtual nsresult CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder);
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage);
+ nsresult GetHeadersFromSelection(nsTArray<nsMsgViewIndex> const& selection,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs);
+ // ListCollapsedChildren() adds to messageArray (rather than replacing it).
+ virtual nsresult ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray);
+
+ nsresult SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin,
+ nsIMsgDBHdr* aMsgHdr,
+ nsMsgJunkStatus aNewClassification);
+ nsresult ToggleReadByIndex(nsMsgViewIndex index);
+ nsresult SetReadByIndex(nsMsgViewIndex index, bool read);
+ nsresult SetThreadOfMsgReadByIndex(nsMsgViewIndex index,
+ nsTArray<nsMsgKey>& keysMarkedRead,
+ bool read);
+ nsresult SetFlaggedByIndex(nsMsgViewIndex index, bool mark);
+ nsresult OrExtraFlag(nsMsgViewIndex index, uint32_t orflag);
+ nsresult AndExtraFlag(nsMsgViewIndex index, uint32_t andflag);
+ nsresult SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag);
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index);
+ virtual void OnExtraFlagChanged(nsMsgViewIndex /*index*/,
+ uint32_t /*extraFlag*/) {}
+ virtual void OnHeaderAddedOrDeleted() {}
+ nsresult ToggleWatched(nsTArray<nsMsgViewIndex> const& selection);
+ nsresult SetThreadWatched(nsIMsgThread* thread, nsMsgViewIndex index,
+ bool watched);
+ nsresult SetThreadIgnored(nsIMsgThread* thread, nsMsgViewIndex threadIndex,
+ bool ignored);
+ nsresult SetSubthreadKilled(nsIMsgDBHdr* header, nsMsgViewIndex msgIndex,
+ bool ignored);
+ nsresult DownloadForOffline(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection);
+ nsresult DownloadFlaggedForOffline(nsIMsgWindow* window);
+ nsMsgViewIndex GetThreadFromMsgIndex(nsMsgViewIndex index,
+ nsIMsgThread** threadHdr);
+ /// Should junk commands be enabled for the current message in the view?
+ bool JunkControlsEnabled(nsMsgViewIndex aViewIndex);
+
+ // For sorting.
+ nsresult GetFieldTypeAndLenForSort(
+ nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen,
+ eFieldType* pFieldType, nsIMsgCustomColumnHandler* colHandler = nullptr);
+ nsresult GetCollationKey(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType,
+ nsTArray<uint8_t>& result,
+ nsIMsgCustomColumnHandler* colHandler = nullptr);
+ nsresult GetLongField(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType,
+ uint32_t* result,
+ nsIMsgCustomColumnHandler* colHandler = nullptr);
+
+ static int FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2,
+ viewSortInfo* sortInfo);
+ static int FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2,
+ viewSortInfo* sortInfo);
+
+ nsresult GetStatusSortValue(nsIMsgDBHdr* msgHdr, uint32_t* result);
+ nsresult GetLocationCollationKey(nsIMsgDBHdr* msgHdr,
+ nsTArray<uint8_t>& result);
+ void PushSort(const MsgViewSortColumnInfo& newSort);
+ nsresult EncodeColumnSort(nsString& columnSortString);
+ nsresult DecodeColumnSort(nsString& columnSortString);
+ // For view navigation.
+ nsresult NavigateFromPos(nsMsgNavigationTypeValue motion,
+ nsMsgViewIndex startIndex, nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap);
+ nsresult FindNextFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex);
+ nsresult FindFirstNew(nsMsgViewIndex* pResultIndex);
+ nsresult FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey,
+ nsMsgKey* resultThreadId);
+ nsresult FindFirstFlagged(nsMsgViewIndex* pResultIndex);
+ nsresult FindPrevFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex);
+ nsresult MarkThreadOfMsgRead(nsMsgKey msgId, nsMsgViewIndex msgIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead, bool bRead);
+ nsresult MarkThreadRead(nsIMsgThread* threadHdr, nsMsgViewIndex threadIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead, bool bRead);
+ bool IsValidIndex(nsMsgViewIndex index);
+ nsresult ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex, bool* resultToggleState);
+ nsresult ToggleMessageKilled(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex,
+ bool* resultToggleState);
+ bool OfflineMsgSelected(nsTArray<nsMsgViewIndex> const& selection);
+ bool NonDummyMsgSelected(nsTArray<nsMsgViewIndex> const& selection);
+ static void GetString(const char16_t* aStringName, nsAString& aValue);
+ static nsresult GetPrefLocalizedString(const char* aPrefName,
+ nsString& aResult);
+ nsresult AppendKeywordProperties(const nsACString& keywords,
+ nsAString& properties, bool* tagAdded);
+ static nsresult InitLabelStrings(void);
+ nsresult CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater);
+ static void InitializeLiterals();
+ virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex);
+ nsresult GetImapDeleteModel(nsIMsgFolder* folder);
+ nsresult UpdateDisplayMessage(nsMsgViewIndex viewPosition);
+ nsresult GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db);
+
+ bool AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags);
+ void FreeAll(nsTArray<void*>* ptrs);
+ void ClearHdrCache();
+
+ // The message held in each row.
+ nsTArray<nsMsgKey> m_keys;
+ // Flags for each row, combining nsMsgMessageFlags and MSG_VIEW_FLAGS.
+ nsTArray<uint32_t> m_flags;
+ // Threading level of each row (1=top)
+ nsTArray<uint8_t> m_levels;
+
+ nsMsgImapDeleteModel mDeleteModel;
+
+ // Cache the most recently asked for header and corresponding msgKey.
+ nsCOMPtr<nsIMsgDBHdr> m_cachedHdr;
+ nsMsgKey m_cachedMsgKey;
+
+ // We need to store the message key for the message we are currently
+ // displaying to ensure we don't try to redisplay the same message just
+ // because the selection changed (i.e. after a sort).
+ nsMsgKey m_currentlyDisplayedMsgKey;
+ nsCString m_currentlyDisplayedMsgUri;
+ nsMsgViewIndex m_currentlyDisplayedViewIndex;
+ // If we're deleting messages, we want to hold off loading messages on
+ // selection changed until the delete is done and we want to batch
+ // notifications.
+ bool m_deletingRows;
+ // For certain special folders and descendants of those folders
+ // (like the "Sent" folder, "Sent/Old Sent").
+ // The Sender column really shows recipients.
+
+ // Server types for this view's folder
+ bool mIsNews; // We have special icons for news.
+ bool mIsRss; // RSS affects enabling of junk commands.
+ bool mIsXFVirtual; // A virtual folder with multiple folders.
+
+ bool mShowSizeInLines; // For news we show lines instead of size when true.
+ bool mSortThreadsByRoot; // As opposed to by the newest message.
+ bool m_sortValid;
+ bool m_checkedCustomColumns;
+ bool mSelectionSummarized;
+ // We asked the front end to summarize the selection and it did not.
+ bool mSummarizeFailed;
+ uint8_t m_saveRestoreSelectionDepth;
+
+ nsCOMPtr<nsIMsgDatabase> m_db;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ // For virtual folders, the VF db.
+ nsCOMPtr<nsIMsgFolder> m_viewFolder;
+ nsString mMessageType;
+ nsTArray<MsgViewSortColumnInfo> m_sortColumns;
+ nsMsgViewSortTypeValue m_sortType;
+ nsMsgViewSortOrderValue m_sortOrder;
+ nsString m_curCustomColumn;
+ nsMsgViewSortTypeValue m_secondarySort;
+ nsMsgViewSortOrderValue m_secondarySortOrder;
+ nsString m_secondaryCustomColumn;
+ nsMsgViewFlagsTypeValue m_viewFlags;
+
+ // I18N date formatter service which we'll want to cache locally.
+ nsCOMPtr<nsIMsgTagService> mTagService;
+ nsWeakPtr mMessengerWeak;
+ nsWeakPtr mMsgWindowWeak;
+ // We push command update notifications to the UI from this.
+ nsWeakPtr mCommandUpdater;
+ static nsCOMPtr<nsIStringBundle> mMessengerStringBundle;
+
+ // Used to determine when to start and end junk plugin batches.
+ uint32_t mNumMessagesRemainingInBatch;
+
+ // These are the headers of the messages in the current
+ // batch/series of batches of messages manually marked
+ // as junk.
+ nsTArray<RefPtr<nsIMsgDBHdr>> mJunkHdrs;
+
+ nsTArray<uint32_t> mIndicesToNoteChange;
+
+ nsTHashtable<nsCStringHashKey> mEmails;
+
+ // The saved search views keep track of the XX most recently deleted msg ids,
+ // so that if the delete is undone, we can add the msg back to the search
+ // results, even if it no longer matches the search criteria (e.g., a saved
+ // search over unread messages). We use mRecentlyDeletedArrayIndex to treat
+ // the array as a list of the XX most recently deleted msgs.
+ nsTArray<nsCString> mRecentlyDeletedMsgIds;
+ uint32_t mRecentlyDeletedArrayIndex;
+ void RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr);
+ bool WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr);
+
+ // These hold pointers (and IDs) for the nsIMsgCustomColumnHandler object
+ // that constitutes the custom column handler.
+ nsCOMArray<nsIMsgCustomColumnHandler> m_customColumnHandlers;
+ nsTArray<nsString> m_customColumnHandlerIDs;
+
+ nsIMsgCustomColumnHandler* GetColumnHandler(const nsAString& colID);
+ nsIMsgCustomColumnHandler* GetCurColumnHandler();
+ bool CustomColumnsInSortAndNotRegistered();
+ void EnsureCustomColumnsValid();
+
+#ifdef DEBUG_David_Bienvenu
+ void InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo);
+ void ValidateSort();
+#endif
+
+ protected:
+ static nsresult InitDisplayFormats();
+
+ private:
+ static bool m_dateFormatsInitialized;
+ static nsDateFormatSelectorComm m_dateFormatDefault;
+ static nsDateFormatSelectorComm m_dateFormatThisWeek;
+ static nsDateFormatSelectorComm m_dateFormatToday;
+ static nsString m_connectorPattern;
+
+ bool ServerSupportsFilterAfterTheFact();
+
+ nsresult PerformActionsOnJunkMsgs(bool msgsAreJunk);
+ nsresult DetermineActionsForJunkChange(bool msgsAreJunk,
+ nsIMsgFolder* srcFolder,
+ bool& moveMessages,
+ bool& changeReadState,
+ nsIMsgFolder** targetFolder);
+
+ class nsMsgViewHdrEnumerator final : public nsBaseMsgEnumerator {
+ public:
+ explicit nsMsgViewHdrEnumerator(nsMsgDBView* view);
+
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ RefPtr<nsMsgDBView> m_view;
+ nsMsgViewIndex m_curHdrIndex;
+
+ private:
+ virtual ~nsMsgViewHdrEnumerator() override;
+ };
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgEnumerator.cpp b/comm/mailnews/base/src/nsMsgEnumerator.cpp
new file mode 100644
index 0000000000..5763a8d4dc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgEnumerator.cpp
@@ -0,0 +1,138 @@
+/* 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/. */
+
+#include "nsMsgEnumerator.h"
+
+#include "mozilla/dom/IteratorResultBinding.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/ResultExtensions.h"
+#include "nsContentUtils.h"
+
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/**
+ * Internal class to support iteration over nsBaseMsgEnumerator in javascript.
+ */
+class JSMsgIterator final : public nsIJSIterator {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIJSITERATOR
+
+ explicit JSMsgIterator(nsBaseMsgEnumerator* aEnumerator)
+ : mEnumerator(aEnumerator) {}
+
+ private:
+ ~JSMsgIterator() = default;
+ RefPtr<nsBaseMsgEnumerator> mEnumerator;
+};
+
+NS_IMETHODIMP JSMsgIterator::Next(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ // result is object of the form: {value: ..., done: ...}
+ RootedDictionary<IteratorResult> result(aCx);
+
+ // We're really using the enumerator itself as the iterator.
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) {
+ result.mDone = true;
+ // Leave value unset.
+ } else {
+ result.mDone = false;
+
+ JS::Rooted<JS::Value> value(aCx);
+ MOZ_TRY(
+ nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgDBHdr), &value));
+ result.mValue = value;
+ }
+
+ if (!ToJSValue(aCx, result, aResult)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(JSMsgIterator, nsIJSIterator)
+
+// nsBaseMsgEnumerator implementation.
+
+NS_IMETHODIMP nsBaseMsgEnumerator::Iterator(nsIJSIterator** aResult) {
+ auto result = MakeRefPtr<JSMsgIterator>(this);
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseMsgEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsBaseMsgEnumerator::HasMoreElements(bool* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(nsBaseMsgEnumerator, nsIMsgEnumerator)
+
+/**
+ * Internal class to support iteration over nsBaseMsgThreadEnumerator in
+ * javascript.
+ */
+class JSThreadIterator final : public nsIJSIterator {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIJSITERATOR
+
+ explicit JSThreadIterator(nsBaseMsgThreadEnumerator* aEnumerator)
+ : mEnumerator(aEnumerator) {}
+
+ private:
+ ~JSThreadIterator() = default;
+ RefPtr<nsBaseMsgThreadEnumerator> mEnumerator;
+};
+
+NS_IMETHODIMP JSThreadIterator::Next(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ // result is object of the form: {value: ..., done: ...}
+ RootedDictionary<IteratorResult> result(aCx);
+
+ // We're really using the enumerator itself as the iterator.
+ nsCOMPtr<nsIMsgThread> msg;
+ if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) {
+ result.mDone = true;
+ // Leave value unset.
+ } else {
+ result.mDone = false;
+
+ JS::Rooted<JS::Value> value(aCx);
+ MOZ_TRY(nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgThread),
+ &value));
+ result.mValue = value;
+ }
+
+ if (!ToJSValue(aCx, result, aResult)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(JSThreadIterator, nsIJSIterator)
+
+// nsBaseMsgThreadEnumerator implementation.
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::Iterator(nsIJSIterator** aResult) {
+ auto result = MakeRefPtr<JSThreadIterator>(this);
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::GetNext(nsIMsgThread** aItem) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::HasMoreElements(bool* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(nsBaseMsgThreadEnumerator, nsIMsgThreadEnumerator)
diff --git a/comm/mailnews/base/src/nsMsgEnumerator.h b/comm/mailnews/base/src/nsMsgEnumerator.h
new file mode 100644
index 0000000000..306c3e3d72
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgEnumerator.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgEnumerator_H_
+#define _nsMsgEnumerator_H_
+
+#include "nsIMsgEnumerator.h"
+
+/**
+ * A base implementation nsIMsgEnumerator for stepping over an ordered set
+ * of nsIMsgDBHdr objects.
+ * This provides the javascript iterable protocol (to support for...of
+ * constructs), but getNext() and hasMoreElements() must be implemented by
+ * derived classes.
+ */
+class nsBaseMsgEnumerator : public nsIMsgEnumerator {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGENUMERATOR
+ nsBaseMsgEnumerator(){};
+
+ protected:
+ virtual ~nsBaseMsgEnumerator(){};
+};
+
+/**
+ * A base implementation nsIMsgThreadEnumerator for stepping over an ordered
+ * set of nsIMsgThread objects.
+ * This provides the javascript iterable protocol (to support for...of
+ * constructs), but getNext() and hasMoreElements() must be implemented by
+ * derived classes.
+ */
+class nsBaseMsgThreadEnumerator : public nsIMsgThreadEnumerator {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTHREADENUMERATOR
+ nsBaseMsgThreadEnumerator(){};
+
+ protected:
+ virtual ~nsBaseMsgThreadEnumerator(){};
+};
+
+#endif /* _nsMsgEnumerator_H_ */
diff --git a/comm/mailnews/base/src/nsMsgFileStream.cpp b/comm/mailnews/base/src/nsMsgFileStream.cpp
new file mode 100644
index 0000000000..9e18ae70f8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFileStream.cpp
@@ -0,0 +1,190 @@
+/* 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/. */
+
+#include "nsIFile.h"
+#include "nsMsgFileStream.h"
+#include "prerr.h"
+#include "prerror.h"
+
+/* From nsDebugImpl.cpp: */
+static nsresult ErrorAccordingToNSPR() {
+ PRErrorCode err = PR_GetError();
+ switch (err) {
+ case PR_OUT_OF_MEMORY_ERROR:
+ return NS_ERROR_OUT_OF_MEMORY;
+ case PR_WOULD_BLOCK_ERROR:
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ case PR_FILE_NOT_FOUND_ERROR:
+ return NS_ERROR_FILE_NOT_FOUND;
+ case PR_READ_ONLY_FILESYSTEM_ERROR:
+ return NS_ERROR_FILE_READ_ONLY;
+ case PR_NOT_DIRECTORY_ERROR:
+ return NS_ERROR_FILE_NOT_DIRECTORY;
+ case PR_IS_DIRECTORY_ERROR:
+ return NS_ERROR_FILE_IS_DIRECTORY;
+ case PR_LOOP_ERROR:
+ return NS_ERROR_FILE_UNRESOLVABLE_SYMLINK;
+ case PR_FILE_EXISTS_ERROR:
+ return NS_ERROR_FILE_ALREADY_EXISTS;
+ case PR_FILE_IS_LOCKED_ERROR:
+ return NS_ERROR_FILE_IS_LOCKED;
+ case PR_FILE_TOO_BIG_ERROR:
+ return NS_ERROR_FILE_TOO_BIG;
+ case PR_NO_DEVICE_SPACE_ERROR:
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ case PR_NAME_TOO_LONG_ERROR:
+ return NS_ERROR_FILE_NAME_TOO_LONG;
+ case PR_DIRECTORY_NOT_EMPTY_ERROR:
+ return NS_ERROR_FILE_DIR_NOT_EMPTY;
+ case PR_NO_ACCESS_RIGHTS_ERROR:
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+}
+
+nsMsgFileStream::nsMsgFileStream() {
+ mFileDesc = nullptr;
+ mSeekedToEnd = false;
+}
+
+nsMsgFileStream::~nsMsgFileStream() {
+ if (mFileDesc) PR_Close(mFileDesc);
+}
+
+NS_IMPL_ISUPPORTS(nsMsgFileStream, nsIInputStream, nsIOutputStream,
+ nsITellableStream, nsISeekableStream)
+
+nsresult nsMsgFileStream::InitWithFile(nsIFile* file) {
+ return file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 0664, &mFileDesc);
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Seek(int32_t whence, int64_t offset) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ bool seekingToEnd = whence == PR_SEEK_END && offset == 0;
+ if (seekingToEnd && mSeekedToEnd) return NS_OK;
+
+ int64_t cnt = PR_Seek64(mFileDesc, offset, (PRSeekWhence)whence);
+ if (cnt == int64_t(-1)) {
+ return ErrorAccordingToNSPR();
+ }
+
+ mSeekedToEnd = seekingToEnd;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Tell(int64_t* result) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int64_t cnt = PR_Seek64(mFileDesc, 0, PR_SEEK_CUR);
+ if (cnt == int64_t(-1)) {
+ return ErrorAccordingToNSPR();
+ }
+ *result = cnt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::SetEOF() {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgFileStream::Close() {
+ nsresult rv = NS_OK;
+ if (mFileDesc && (PR_Close(mFileDesc) == PR_FAILURE))
+ rv = NS_BASE_STREAM_OSERROR;
+ mFileDesc = nullptr;
+ return rv;
+}
+
+/* unsigned long long available (); */
+NS_IMETHODIMP nsMsgFileStream::Available(uint64_t* aResult) {
+ if (!mFileDesc) return NS_BASE_STREAM_CLOSED;
+
+ int64_t avail = PR_Available64(mFileDesc);
+ if (avail == -1) return ErrorAccordingToNSPR();
+
+ *aResult = avail;
+ return NS_OK;
+}
+
+/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgFileStream::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aResult) {
+ if (!mFileDesc) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ int32_t bytesRead = PR_Read(mFileDesc, aBuf, aCount);
+ if (bytesRead == -1) return ErrorAccordingToNSPR();
+
+ *aResult = bytesRead;
+ return NS_OK;
+}
+
+/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in
+ * voidPtr aClosure, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgFileStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure, uint32_t aCount,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgFileStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Write(const char* buf, uint32_t count, uint32_t* result) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int32_t cnt = PR_Write(mFileDesc, buf, count);
+ if (cnt == -1) {
+ return ErrorAccordingToNSPR();
+ }
+ *result = cnt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Flush(void) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int32_t cnt = PR_Sync(mFileDesc);
+ if (cnt == -1) return ErrorAccordingToNSPR();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::WriteFrom(nsIInputStream* inStr, uint32_t count,
+ uint32_t* _retval) {
+ MOZ_ASSERT_UNREACHABLE("WriteFrom (see source comment)");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // File streams intentionally do not support this method.
+ // If you need something like this, then you should wrap
+ // the file stream using nsIBufferedOutputStream
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* _retval) {
+ MOZ_ASSERT_UNREACHABLE("WriteSegments (see source comment)");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // File streams intentionally do not support this method.
+ // If you need something like this, then you should wrap
+ // the file stream using nsIBufferedOutputStream
+}
+
+NS_IMETHODIMP nsMsgFileStream::StreamStatus() {
+ return mFileDesc ? NS_OK : NS_BASE_STREAM_CLOSED;
+}
diff --git a/comm/mailnews/base/src/nsMsgFileStream.h b/comm/mailnews/base/src/nsMsgFileStream.h
new file mode 100644
index 0000000000..11b5c24e91
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFileStream.h
@@ -0,0 +1,35 @@
+/* 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/. */
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISeekableStream.h"
+#include "prio.h"
+
+class nsMsgFileStream final : public nsIInputStream,
+ public nsIOutputStream,
+ public nsISeekableStream {
+ public:
+ nsMsgFileStream();
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Available(uint64_t* _retval) override;
+ NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) override;
+ NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
+ uint32_t aCount, uint32_t* _retval) override;
+ NS_DECL_NSIOUTPUTSTREAM
+ NS_DECL_NSISEEKABLESTREAM
+ NS_DECL_NSITELLABLESTREAM
+
+ nsresult InitWithFile(nsIFile* localFile);
+
+ protected:
+ ~nsMsgFileStream();
+
+ PRFileDesc* mFileDesc;
+ bool mSeekedToEnd;
+};
diff --git a/comm/mailnews/base/src/nsMsgFolderCache.cpp b/comm/mailnews/base/src/nsMsgFolderCache.cpp
new file mode 100644
index 0000000000..c843730f24
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCache.cpp
@@ -0,0 +1,570 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsISafeOutputStream.h"
+#include "prprf.h"
+#include "mozilla/Logging.h"
+// Mork-related includes.
+#include "nsIMdbFactoryFactory.h"
+#include "mdb.h"
+// Includes for jsoncpp.
+#include "json/json.h"
+#include <string>
+
+using namespace mozilla;
+
+static LazyLogModule sFolderCacheLog("MsgFolderCache");
+
+// Helper functions for migration of legacy pancea.dat files.
+static nsresult importFromMork(PathString const& dbName, Json::Value& root);
+static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store,
+ nsIMdbTable* table, Json::Value& root);
+static void applyEntry(nsCString const& name, nsCString const& val,
+ Json::Value& obj);
+
+/*
+ * nsMsgFolderCacheElement
+ * Folders are given this to let them manipulate their cache data.
+ */
+class nsMsgFolderCacheElement : public nsIMsgFolderCacheElement {
+ public:
+ nsMsgFolderCacheElement(nsMsgFolderCache* owner, nsACString const& key)
+ : mOwner(owner), mKey(key) {}
+ nsMsgFolderCacheElement() = delete;
+
+ NS_DECL_ISUPPORTS
+ NS_IMETHOD GetKey(nsACString& key) override {
+ key = mKey;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetCachedString(const char* name, nsACString& _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::stringValue)) {
+ _retval = o.asString().c_str();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedInt32(const char* name, int32_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::intValue)) {
+ *_retval = o.asInt();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedUInt32(const char* name, uint32_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::uintValue)) {
+ *_retval = o.asUInt();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedInt64(const char* name, int64_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ // isConvertibleTo() doesn't seem to support Int64. Hence multiple checks.
+ if (o.isNumeric() || o.isNull() || o.isBool()) {
+ *_retval = o.asInt64();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD SetCachedString(const char* name,
+ const nsACString& value) override {
+ if (Obj()[name] != PromiseFlatCString(value).get()) {
+ Obj()[name] = PromiseFlatCString(value).get();
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetCachedInt32(const char* name, int32_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetCachedUInt32(const char* name, uint32_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+ NS_IMETHOD SetCachedInt64(const char* name, int64_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ protected:
+ virtual ~nsMsgFolderCacheElement() {}
+ RefPtr<nsMsgFolderCache> mOwner;
+ nsAutoCString mKey;
+
+ // Helper to get the Json object for this nsFolderCacheElement,
+ // creating it if it doesn't already exist.
+ Json::Value& Obj() {
+ Json::Value& root = *mOwner->mRoot;
+ // This will create an empty object if it doesn't already exist.
+ Json::Value& v = root[mKey.get()];
+ if (v.isObject()) {
+ return v;
+ }
+ // uhoh... either the folder entry doesn't exist (expected) or
+ // the json file wasn't the structure we were expecting.
+ // We _really_ don't want jsoncpp to be throwing exceptions, so in either
+ // case we'll create a fresh new empty object there.
+ root[mKey.get()] = Json::Value(Json::objectValue);
+ return root[mKey.get()];
+ }
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCacheElement, nsIMsgFolderCacheElement)
+
+/*
+ * nsMsgFolderCache implementation
+ */
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCache, nsIMsgFolderCache)
+
+// mRoot dynamically allocated here to avoid exposing Json in header file.
+nsMsgFolderCache::nsMsgFolderCache()
+ : mRoot(new Json::Value(Json::objectValue)),
+ mSavePending(false),
+ mSaveTimer(NS_NewTimer()) {}
+
+NS_IMETHODIMP nsMsgFolderCache::Init(nsIFile* cacheFile, nsIFile* legacyFile) {
+ mCacheFile = cacheFile;
+ // Is there a JSON file to load?
+ bool exists;
+ nsresult rv = cacheFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ rv = LoadFolderCache(cacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Failed to load %s (code 0x%x)",
+ cacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv)));
+ }
+ // Ignore error. If load fails, we'll just start off with empty cache.
+ return NS_OK;
+ }
+
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("No cache file found."));
+
+ // No sign of new-style JSON file. Maybe there's an old panacea.dat we can
+ // migrate?
+ rv = legacyFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Found %s. Attempting migration.",
+ legacyFile->HumanReadablePath().get()));
+ Json::Value root(Json::objectValue);
+ rv = importFromMork(legacyFile->NativePath(), root);
+ if (NS_SUCCEEDED(rv)) {
+ *mRoot = root;
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Migration: Legacy cache imported"));
+ // Migrate it to JSON.
+ rv = SaveFolderCache(cacheFile);
+ if (NS_SUCCEEDED(rv)) {
+ // We're done with the legacy panacea.dat - remove it.
+ legacyFile->Remove(false);
+ } else {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Migration: save failed (code 0x%x)", static_cast<uint32_t>(rv)));
+ }
+ } else {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Migration: import failed (code 0x%x)", static_cast<uint32_t>(rv)));
+ }
+ }
+ // Never fails.
+ return NS_OK;
+}
+
+nsMsgFolderCache::~nsMsgFolderCache() {
+ Flush();
+ delete mRoot;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::Flush() {
+ if (mSavePending) {
+ mSaveTimer->Cancel();
+ mSavePending = false;
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("Forced save."));
+ nsresult rv = SaveFolderCache(mCacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Failed to write to %s (code 0x%x)",
+ mCacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv)));
+ }
+ }
+ return NS_OK;
+}
+
+// Read the cache data from inFile.
+// It's atomic - if a failure occurs, the cache data will be left unchanged.
+nsresult nsMsgFolderCache::LoadFolderCache(nsIFile* inFile) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Loading %s", inFile->HumanReadablePath().get()));
+
+ nsCOMPtr<nsIInputStream> inStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), inFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString data;
+ rv = NS_ConsumeStream(inStream, UINT32_MAX, data);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Read failed."));
+ return rv;
+ }
+
+ Json::Value root;
+ Json::CharReaderBuilder builder;
+ std::unique_ptr<Json::CharReader> const reader(builder.newCharReader());
+ if (!reader->parse(data.BeginReading(), data.EndReading(), &root, nullptr)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Error parsing JSON"));
+ return NS_ERROR_FAILURE; // parsing failed.
+ }
+ if (!root.isObject()) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("JSON root is not an object"));
+ return NS_ERROR_FAILURE; // bad format.
+ }
+ *mRoot = root;
+ return NS_OK;
+}
+
+// Write the cache data to outFile.
+nsresult nsMsgFolderCache::SaveFolderCache(nsIFile* outFile) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Save to %s", outFile->HumanReadablePath().get()));
+
+ // Serialise the data.
+ Json::StreamWriterBuilder b;
+ // b["indentation"] = "";
+ std::string out = Json::writeString(b, *mRoot);
+
+ // Safe stream, writes to a tempfile first then moves into proper place when
+ // Finish() is called. Could use NS_NewAtomicFileOutputStream, but seems hard
+ // to justify a full filesystem flush).
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsresult rv =
+ NS_NewSafeLocalFileOutputStream(getter_AddRefs(outputStream), outFile,
+ PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* ptr = out.data();
+ uint32_t remaining = out.length();
+ while (remaining > 0) {
+ uint32_t written = 0;
+ rv = outputStream->Write(ptr, remaining, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ remaining -= written;
+ ptr += written;
+ }
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outputStream);
+ MOZ_ASSERT(safeStream);
+ rv = safeStream->Finish();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::GetCacheElement(
+ const nsACString& pathKey, bool createIfMissing,
+ nsIMsgFolderCacheElement** result) {
+ nsAutoCString key(pathKey);
+ if (mRoot->isMember(key.get()) || createIfMissing) {
+ nsCOMPtr<nsIMsgFolderCacheElement> element =
+ new nsMsgFolderCacheElement(this, pathKey);
+ element.forget(result);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::RemoveElement(const nsACString& key) {
+ mRoot->removeMember(PromiseFlatCString(key).get());
+ return NS_OK;
+}
+
+void nsMsgFolderCache::SetModified() {
+ if (mSavePending) {
+ return;
+ }
+ nsresult rv = mSaveTimer->InitWithNamedFuncCallback(
+ doSave, (void*)this, kSaveDelayMs, nsITimer::TYPE_ONE_SHOT,
+ "msgFolderCache::doSave");
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("AutoSave in %ds", kSaveDelayMs / 1000));
+ mSavePending = true;
+ }
+}
+
+// static
+void nsMsgFolderCache::doSave(nsITimer*, void* closure) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("AutoSave"));
+ nsMsgFolderCache* that = static_cast<nsMsgFolderCache*>(closure);
+ nsresult rv = that->SaveFolderCache(that->mCacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error,
+ ("Failed writing %s (code 0x%x)",
+ that->mCacheFile->HumanReadablePath().get(),
+ static_cast<uint32_t>(rv)));
+ }
+ that->mSavePending = false;
+}
+
+// Helper to apply a legacy property to the new JSON format.
+static void applyEntry(nsCString const& name, nsCString const& val,
+ Json::Value& obj) {
+ // The old mork version stored all numbers as hex, so we need to convert
+ // them into proper Json numeric types. But there's no type info in the
+ // database so we just have to know which values to convert.
+ // We can find a list of all the numeric values by grepping the codebase
+ // for GetCacheInt32/GetCachedInt64. We treat everything else as a string.
+ // It's much harder to get a definitive list of possible keys for strings,
+ // because nsIMsgFolderCache is also used to cache nsDBFolderInfo data -
+ // see nsMsgDBFolder::GetStringProperty().
+
+ // One of the Int32 properties?
+ if (name.EqualsLiteral("hierDelim") ||
+ name.EqualsLiteral("lastSyncTimeInSec") ||
+ name.EqualsLiteral("nextUID") || name.EqualsLiteral("pendingMsgs") ||
+ name.EqualsLiteral("pendingUnreadMsgs") ||
+ name.EqualsLiteral("serverRecent") || name.EqualsLiteral("serverTotal") ||
+ name.EqualsLiteral("serverUnseen") || name.EqualsLiteral("totalMsgs") ||
+ name.EqualsLiteral("totalUnreadMsgs")) {
+ if (val.IsEmpty()) {
+ return;
+ }
+ int32_t i32;
+ if (PR_sscanf(val.get(), "%x", &i32) != 1) {
+ return;
+ }
+ obj[name.get()] = i32;
+ return;
+ }
+
+ // Flags were int32. But the upper bit can be set, meaning we'll get
+ // annoying negative values, which isn't what we want. Not so much of an
+ // issue with legacy pancea.dat as it was all hex strings anyway. But let's
+ // fix it up as we go to JSON.
+ if (name.EqualsLiteral("aclFlags") || name.EqualsLiteral("boxFlags") ||
+ name.EqualsLiteral("flags")) {
+ uint32_t u32;
+ if (PR_sscanf(val.get(), "%x", &u32) != 1) {
+ return;
+ }
+ obj[name.get()] = u32;
+ return;
+ }
+
+ // One of the Int64 properties?
+ if (name.EqualsLiteral("expungedBytes") || name.EqualsLiteral("folderSize")) {
+ if (val.IsEmpty()) {
+ return;
+ }
+ int64_t i64;
+ if (PR_sscanf(val.get(), "%llx", &i64) != 1) {
+ return;
+ }
+ obj[name.get()] = i64;
+ return;
+ }
+
+ // Assume anything else is a string.
+ obj[name.get()] = val.get();
+}
+
+// Import an old panacea.dat mork file, converting it into our JSON form.
+// The flow of this is taken from the old implementation. There are a couple
+// of steps that may not be strictly required, but have been left in anyway
+// on the grounds that it works.
+static nsresult importFromMork(PathString const& dbName, Json::Value& root) {
+ nsresult rv;
+ nsCOMPtr<nsIMdbFactoryService> factoryService =
+ do_GetService("@mozilla.org/db/mork;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMdbFactory> factory;
+ rv = factoryService->GetMdbFactory(getter_AddRefs(factory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMdbEnv> env;
+ rv = factory->MakeEnv(nullptr, getter_AddRefs(env));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ env->SetAutoClear(true);
+ nsCOMPtr<nsIMdbFile> dbFile;
+ rv = factory->OpenOldFile(env,
+ nullptr, // Use default heap alloc fns.
+ dbName.get(),
+ mdbBool_kTrue, // Frozen (read only).
+ getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Unsure if we actually need this...
+ mdb_bool canOpen;
+ mdbYarn outFormatVersion;
+ rv = factory->CanOpenFilePort(env, dbFile, &canOpen, &outFormatVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canOpen) {
+ return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+
+ mdbOpenPolicy inOpenPolicy;
+ inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0;
+ inOpenPolicy.mOpenPolicy_MinMemory = 0;
+ inOpenPolicy.mOpenPolicy_MaxLazy = 0;
+
+ nsCOMPtr<nsIMdbThumb> thumb;
+ rv = factory->OpenFileStore(env,
+ nullptr, // Use default heap alloc fns.
+ dbFile, &inOpenPolicy, getter_AddRefs(thumb));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Unsure what this is doing. Applying appended-but-unapplied writes?
+ {
+ mdb_count outTotal; // total somethings to do in operation
+ mdb_count outCurrent; // subportion of total completed so far
+ mdb_bool outDone; // is operation finished?
+ mdb_bool outBroken; // is operation irreparably dead and broken?
+ do {
+ rv = thumb->DoMore(env, &outTotal, &outCurrent, &outDone, &outBroken);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } while (!outBroken && !outDone);
+ }
+
+ // Finally, open the store.
+ nsCOMPtr<nsIMdbStore> store;
+ rv = factory->ThumbToOpenStore(env, thumb, getter_AddRefs(store));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Resolve some tokens we'll need.
+ const char* kFoldersScope = "ns:msg:db:row:scope:folders:all";
+ mdb_token folderRowScopeToken;
+ rv = store->StringToToken(env, kFoldersScope, &folderRowScopeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the table. Only one, and we assume id=1. Eek! But original code
+ // did this too...
+ mdbOid allFoldersTableOID{folderRowScopeToken, 1};
+ nsCOMPtr<nsIMdbTable> allFoldersTable;
+ rv = store->GetTable(env, &allFoldersTableOID,
+ getter_AddRefs(allFoldersTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // GetTable() can return null even without an error.
+ NS_ENSURE_STATE(allFoldersTable);
+
+ rv = convertTable(env, store, allFoldersTable, root);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// The legacy panacea.dat mork db has a single table, with a row per
+// folder. This function reads it in and writes it into our Json::Value
+// object.
+static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store,
+ nsIMdbTable* table, Json::Value& root) {
+ MOZ_ASSERT(root.isObject());
+ MOZ_ASSERT(table);
+
+ nsresult rv;
+ nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
+ rv = table->GetTableRowCursor(env, -1, getter_AddRefs(rowCursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // For each row in the table...
+ while (true) {
+ nsCOMPtr<nsIMdbRow> row;
+ mdb_pos pos;
+ rv = rowCursor->NextRow(env, getter_AddRefs(row), &pos);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!row) {
+ break; // That's all the rows done.
+ }
+
+ nsCOMPtr<nsIMdbRowCellCursor> cellCursor;
+ rv = row->GetRowCellCursor(env, -1, getter_AddRefs(cellCursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Json::Value obj(Json::objectValue);
+ // For each cell in the row...
+ nsAutoCString rowKey;
+ while (true) {
+ nsCOMPtr<nsIMdbCell> cell;
+ mdb_column column;
+ rv = cellCursor->NextCell(env, getter_AddRefs(cell), &column, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break; // No more cells.
+ }
+
+ // Get the column name
+ nsAutoCString colName;
+ {
+ char buf[100];
+ mdbYarn colYarn{buf, 0, sizeof(buf), 0, 0, nullptr};
+ // Get the column of the cell
+ nsresult rv = store->TokenToString(env, column, &colYarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ colName.Assign((const char*)colYarn.mYarn_Buf, colYarn.mYarn_Fill);
+ }
+ // Get the value
+ nsAutoCString colValue;
+ {
+ mdbYarn yarn;
+ cell->AliasYarn(env, &yarn);
+ colValue.Assign((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill);
+ }
+ if (colName.EqualsLiteral("key")) {
+ rowKey = colValue;
+ } else {
+ applyEntry(colName, colValue, obj);
+ }
+ }
+ if (rowKey.IsEmpty()) {
+ continue;
+ }
+
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Migration: migrated key '%s' (%d properties)", rowKey.get(),
+ (int)obj.size()));
+ root[rowKey.get()] = obj;
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderCache.h b/comm/mailnews/base/src/nsMsgFolderCache.h
new file mode 100644
index 0000000000..480814ad10
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCache.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgFolderCache_H
+#define nsMsgFolderCache_H
+
+#include "nsIMsgFolderCache.h"
+#include "nsIFile.h"
+#include "nsITimer.h"
+
+namespace Json {
+class Value;
+};
+
+/**
+ * nsMsgFolderCache implements the folder cache, which stores values which
+ * might be slow for the folder to calculate.
+ * It persists the cache data by dumping it out to a .json file when changes
+ * are made. To avoid huge numbers of writes, this autosaving is deferred -
+ * when a cached value is changed, it'll wait a minute or so before
+ * writing, to collect any other changes that occur during that time.
+ * If any changes are outstanding at destruction time, it'll perform an
+ * immediate save then.
+ */
+class nsMsgFolderCache : public nsIMsgFolderCache {
+ public:
+ friend class nsMsgFolderCacheElement;
+
+ nsMsgFolderCache();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERCACHE
+
+ protected:
+ virtual ~nsMsgFolderCache();
+
+ nsresult LoadFolderCache(nsIFile* jsonFile);
+ nsresult SaveFolderCache(nsIFile* jsonFile);
+ // Flag that a save is required. It'll be deferred by kAutoSaveDelayMs.
+ void SetModified();
+ static constexpr uint32_t kSaveDelayMs = 1000 * 60 * 1; // 1 minute.
+ static void doSave(nsITimer*, void* closure);
+
+ // Path to the JSON file backing the cache.
+ nsCOMPtr<nsIFile> mCacheFile;
+
+ // This is our data store. Kept as a Json::Value for ease of saving, but
+ // it's actually not a bad format for access (it's basically a std::map).
+ // Using a pointer to allow forward declaration. The json headers aren't
+ // in the include path for other modules, so we don't want to expose them
+ // here.
+ Json::Value* mRoot;
+
+ bool mSavePending;
+ nsCOMPtr<nsITimer> mSaveTimer;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.cpp b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp
new file mode 100644
index 0000000000..c9740bedb8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp
@@ -0,0 +1,1391 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsIMsgHdr.h"
+#include "nsIChannel.h"
+#include "nsIStreamListener.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsISeekableStream.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIPrompt.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsMailHeaders.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsMsgFolderCompactor.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsPrintfCString.h"
+#include "nsIStringBundle.h"
+#include "nsICopyMessageStreamListener.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgPluggableStore.h"
+#include "mozilla/Buffer.h"
+#include "HeaderReader.h"
+#include "LineReader.h"
+#include "mozilla/Components.h"
+
+static nsresult GetBaseStringBundle(nsIStringBundle** aBundle) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ return bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", aBundle);
+}
+
+#define COMPACTOR_READ_BUFF_SIZE 16384
+
+/**
+ * nsFolderCompactState is a helper class for nsFolderCompactor, which
+ * handles compacting the mbox for a single local folder.
+ *
+ * This class also patches X-Mozilla-* headers where required. Usually
+ * these headers are edited in-place without changing the overall size,
+ * but sometimes there's not enough room. So as compaction involves
+ * rewriting the whole file anyway, we take the opportunity to make some
+ * more space and correct those headers.
+ *
+ * NOTE (for future cleanups):
+ *
+ * This base class calls nsIMsgMessageService.copyMessages() to iterate
+ * through messages, passing itself in as a listener. Callbacks from
+ * both nsICopyMessageStreamListener and nsIStreamListener are invoked.
+ *
+ * nsOfflineStoreCompactState uses a different mechanism - see separate
+ * notes below.
+ *
+ * The way the service invokes the listener callbacks is pretty quirky
+ * and probably needs a good sorting out, but for now I'll just document what
+ * I've observed here:
+ *
+ * - The service calls OnStartRequest() at the start of the first message.
+ * - StartMessage() is called at the start of subsequent messages.
+ * - EndCopy() is called at the end of every message except the last one,
+ * where OnStopRequest() is invoked instead.
+ * - OnDataAvailable() is called to pass the message body of each message
+ * (in multiple calls if the message is big enough).
+ * - EndCopy() doesn't ever seem to be passed a failing error code from
+ * what I can see, and its own return code is ignored by upstream code.
+ */
+class nsFolderCompactState : public nsIStreamListener,
+ public nsICopyMessageStreamListener,
+ public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICOPYMESSAGESTREAMLISTENER
+ NS_DECL_NSIURLLISTENER
+
+ nsFolderCompactState(void);
+
+ nsresult Compact(nsIMsgFolder* folder,
+ std::function<void(nsresult, uint64_t)> completionFn,
+ nsIMsgWindow* msgWindow);
+
+ protected:
+ virtual ~nsFolderCompactState(void);
+
+ virtual nsresult InitDB(nsIMsgDatabase* db);
+ virtual nsresult StartCompacting();
+ virtual nsresult FinishCompact();
+ void CloseOutputStream();
+ void CleanupTempFilesAfterError();
+ nsresult FlushBuffer();
+
+ nsresult Init(nsIMsgFolder* aFolder, const char* aBaseMsgUri,
+ nsIMsgDatabase* aDb, nsIFile* aPath, nsIMsgWindow* aMsgWindow);
+ nsresult BuildMessageURI(const char* baseURI, nsMsgKey key, nsCString& uri);
+ nsresult ShowStatusMsg(const nsString& aMsg);
+ nsresult ReleaseFolderLock();
+ void ShowCompactingStatusMsg();
+
+ nsCString m_baseMessageUri; // base message uri
+ nsCString m_messageUri; // current message uri being copy
+ nsCOMPtr<nsIMsgFolder> m_folder; // current folder being compact
+ nsCOMPtr<nsIMsgDatabase> m_db; // new database for the compact folder
+ nsCOMPtr<nsIFile> m_file; // new mailbox for the compact folder
+ nsCOMPtr<nsIOutputStream> m_fileStream; // output file stream for writing
+ // All message keys that need to be copied over.
+ nsTArray<nsMsgKey> m_keys;
+
+ // Sum of the sizes of the messages, accumulated as we visit each msg.
+ uint64_t m_totalMsgSize{0};
+ // Number of bytes that can be expunged while compacting.
+ uint64_t m_totalExpungedBytes{0};
+ // Index of the current copied message key in key array.
+ uint32_t m_curIndex{0};
+ // Offset in mailbox of new message.
+ uint64_t m_startOfNewMsg{0};
+ mozilla::Buffer<char> m_buffer{COMPACTOR_READ_BUFF_SIZE};
+ uint32_t m_bufferCount{0};
+
+ // We'll use this if we need to output any EOLs - we try to preserve the
+ // convention found in the input data.
+ nsCString m_eolSeq{MSG_LINEBREAK};
+
+ // The status of the copying operation.
+ nsresult m_status{NS_OK};
+ nsCOMPtr<nsIMsgMessageService> m_messageService; // message service for
+ // copying
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgDBHdr> m_curSrcHdr;
+ // Flag set when we're waiting for local folder to complete parsing.
+ bool m_parsingFolder;
+ // Flag to indicate we're starting a new message, and that no data has
+ // been written for it yet.
+ bool m_startOfMsg;
+ // Function which will be run when the folder compaction completes.
+ // Takes a result code and the number of bytes which were expunged.
+ std::function<void(nsresult, uint64_t)> m_completionFn;
+ bool m_alreadyWarnedDiskSpace{false};
+};
+
+NS_IMPL_ISUPPORTS(nsFolderCompactState, nsIRequestObserver, nsIStreamListener,
+ nsICopyMessageStreamListener, nsIUrlListener)
+
+nsFolderCompactState::nsFolderCompactState() {
+ m_parsingFolder = false;
+ m_startOfMsg = true;
+}
+
+nsFolderCompactState::~nsFolderCompactState() {
+ CloseOutputStream();
+ if (NS_FAILED(m_status)) {
+ CleanupTempFilesAfterError();
+ // if for some reason we failed remove the temp folder and database
+ }
+}
+
+void nsFolderCompactState::CloseOutputStream() {
+ if (m_fileStream) {
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+}
+
+void nsFolderCompactState::CleanupTempFilesAfterError() {
+ CloseOutputStream();
+ if (m_db) m_db->ForceClosed();
+ nsCOMPtr<nsIFile> summaryFile;
+ GetSummaryFileLocation(m_file, getter_AddRefs(summaryFile));
+ m_file->Remove(false);
+ summaryFile->Remove(false);
+}
+
+nsresult nsFolderCompactState::BuildMessageURI(const char* baseURI,
+ nsMsgKey key, nsCString& uri) {
+ uri.Append(baseURI);
+ uri.Append('#');
+ uri.AppendInt(key);
+
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::InitDB(nsIMsgDatabase* db) {
+ nsCOMPtr<nsIMsgDatabase> mailDBFactory;
+ nsresult rv = db->ListAllKeys(m_keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(m_file, m_folder, true, false,
+ getter_AddRefs(m_db));
+
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ // if it's out of date then reopen with upgrade.
+ return msgDBService->OpenMailDBFromFile(m_file, m_folder, true, true,
+ getter_AddRefs(m_db));
+ return rv;
+}
+
+nsresult nsFolderCompactState::Compact(
+ nsIMsgFolder* folder, std::function<void(nsresult, uint64_t)> completionFn,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(folder);
+ m_completionFn = completionFn;
+ m_window = msgWindow;
+ nsresult rv;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIFile> path;
+ nsCString baseMessageURI;
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder, &rv);
+ if (NS_SUCCEEDED(rv) && localFolder) {
+ rv = localFolder->GetDatabaseWOReparse(getter_AddRefs(db));
+ if (NS_FAILED(rv) || !db) {
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ m_folder = folder; // will be used to compact
+ m_parsingFolder = true;
+ rv = localFolder->ParseFolder(m_window, this);
+ }
+ return rv;
+ } else {
+ bool valid;
+ rv = db->GetSummaryValid(&valid);
+ if (!valid) // we are probably parsing the folder because we selected it.
+ {
+ folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return NS_OK;
+ }
+ }
+ } else {
+ rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = folder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ do {
+ bool exists = false;
+ rv = path->Exists(&exists);
+ if (!exists) {
+ // No need to compact if the local file does not exist.
+ // Can happen e.g. on IMAP when the folder is not marked for offline use.
+ break;
+ }
+
+ int64_t expunged = 0;
+ folder->GetExpungedBytes(&expunged);
+ if (expunged == 0) {
+ // No need to compact if nothing would be expunged.
+ break;
+ }
+
+ int64_t diskSize;
+ rv = folder->GetSizeOnDisk(&diskSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t diskFree;
+ rv = path->GetDiskSpaceAvailable(&diskFree);
+ if (NS_FAILED(rv)) {
+ // If GetDiskSpaceAvailable() failed, better bail out fast.
+ if (rv != NS_ERROR_NOT_IMPLEMENTED) return rv;
+ // Some platforms do not have GetDiskSpaceAvailable implemented.
+ // In that case skip the preventive free space analysis and let it
+ // fail in compact later if space actually wasn't available.
+ } else {
+ // Let's try to not even start compact if there is really low free space.
+ // It may still fail later as we do not know how big exactly the folder DB
+ // will end up being. The DB already doesn't contain references to
+ // messages that are already deleted. So theoretically it shouldn't shrink
+ // with compact. But in practice, the automatic shrinking of the DB may
+ // still have not yet happened. So we cap the final size at 1KB per
+ // message.
+ db->Commit(nsMsgDBCommitType::kCompressCommit);
+
+ int64_t dbSize;
+ rv = db->GetDatabaseSize(&dbSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t totalMsgs;
+ rv = folder->GetTotalMessages(false, &totalMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t expectedDBSize =
+ std::min<int64_t>(dbSize, ((int64_t)totalMsgs) * 1024);
+ if (diskFree < diskSize - expunged + expectedDBSize) {
+ if (!m_alreadyWarnedDiskSpace) {
+ folder->ThrowAlertMsg("compactFolderInsufficientSpace", m_window);
+ m_alreadyWarnedDiskSpace = true;
+ }
+ break;
+ }
+ }
+
+ rv = folder->GetBaseMessageURI(baseMessageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Init(folder, baseMessageURI.get(), db, path, m_window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isLocked = true;
+ m_folder->GetLocked(&isLocked);
+ if (isLocked) {
+ CleanupTempFilesAfterError();
+ m_folder->ThrowAlertMsg("compactFolderDeniedLock", m_window);
+ break;
+ }
+
+ // If we got here start the real compacting.
+ nsCOMPtr<nsISupports> supports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports));
+ m_folder->AcquireSemaphore(supports);
+ m_totalExpungedBytes += expunged;
+ return StartCompacting();
+
+ } while (false); // block for easy skipping the compaction using 'break'
+
+ // Skipped folder, for whatever reason.
+ folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::ShowStatusMsg(const nsString& aMsg) {
+ if (!m_window || aMsg.IsEmpty()) return NS_OK;
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ nsresult rv = m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (NS_FAILED(rv) || !statusFeedback) return NS_OK;
+
+ // Try to prepend account name to the message.
+ nsString statusMessage;
+ do {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_folder->GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) break;
+ nsAutoString accountName;
+ rv = server->GetPrettyName(accountName);
+ if (NS_FAILED(rv)) break;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) break;
+ AutoTArray<nsString, 2> params = {accountName, aMsg};
+ rv = bundle->FormatStringFromName("statusMessage", params, statusMessage);
+ } while (false);
+
+ // If fetching any of the needed info failed, just show the original message.
+ if (NS_FAILED(rv)) statusMessage.Assign(aMsg);
+ return statusFeedback->SetStatusString(statusMessage);
+}
+
+nsresult nsFolderCompactState::Init(nsIMsgFolder* folder,
+ const char* baseMsgUri, nsIMsgDatabase* db,
+ nsIFile* path, nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ m_folder = folder;
+ m_baseMessageUri = baseMsgUri;
+ m_file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_file->InitWithFile(path);
+
+ m_file->SetNativeLeafName("nstmp"_ns);
+ // Make sure we are not crunching existing nstmp file.
+ rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_window = aMsgWindow;
+ m_totalMsgSize = 0;
+ rv = InitDB(db);
+ if (NS_FAILED(rv)) {
+ CleanupTempFilesAfterError();
+ return rv;
+ }
+
+ m_curIndex = 0;
+
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_fileStream), m_file, -1,
+ 00600);
+ if (NS_FAILED(rv))
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ else
+ rv = GetMessageServiceFromURI(nsDependentCString(baseMsgUri),
+ getter_AddRefs(m_messageService));
+ if (NS_FAILED(rv)) {
+ m_status = rv;
+ }
+ return rv;
+}
+
+void nsFolderCompactState::ShowCompactingStatusMsg() {
+ nsString statusString;
+ nsresult rv = m_folder->GetStringWithFolderNameFromBundle("compactingFolder",
+ statusString);
+ if (!statusString.IsEmpty() && NS_SUCCEEDED(rv)) ShowStatusMsg(statusString);
+}
+
+NS_IMETHODIMP nsFolderCompactState::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+// If we had to kick off a folder parse, this will be called when it
+// completes.
+NS_IMETHODIMP nsFolderCompactState::OnStopRunningUrl(nsIURI* url,
+ nsresult status) {
+ if (m_parsingFolder) {
+ m_parsingFolder = false;
+ if (NS_SUCCEEDED(status)) {
+ // Folder reparse succeeded. Start compacting it.
+ status = Compact(m_folder, m_completionFn, m_window);
+ if (NS_SUCCEEDED(status)) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // This is from bug 249754. The aim is to close the DB file to avoid
+ // running out of filehandles when large numbers of folders are compacted.
+ // But it seems like filehandle management would be better off being
+ // handled by the DB class itself (it might be already, but it's hard to
+ // tell)...
+ m_folder->SetMsgDatabase(nullptr);
+
+ if (m_completionFn) {
+ m_completionFn(status, m_totalExpungedBytes);
+ }
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::StartCompacting() {
+ nsresult rv = NS_OK;
+ // Notify that compaction is beginning. We do this even if there are no
+ // messages to be copied because the summary database still gets blown away
+ // which is still pretty interesting. (And we like consistency.)
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyFolderCompactStart(m_folder);
+ }
+
+ // TODO: test whether sorting the messages (m_keys) by messageOffset
+ // would improve performance on large files (less seeks).
+ // The m_keys array is in the order as stored in DB and on IMAP or News
+ // the messages stored on the mbox file are not necessarily in the same order.
+ if (m_keys.Length() > 0) {
+ nsCOMPtr<nsIURI> notUsed;
+ ShowCompactingStatusMsg();
+ NS_ADDREF_THIS();
+ rv = m_messageService->CopyMessages(m_keys, m_folder, this, false, nullptr,
+ m_window, getter_AddRefs(notUsed));
+ } else { // no messages to copy with
+ FinishCompact();
+ }
+ return rv;
+}
+
+nsresult nsFolderCompactState::FinishCompact() {
+ NS_ENSURE_TRUE(m_folder, NS_ERROR_NOT_INITIALIZED);
+ NS_ENSURE_TRUE(m_file, NS_ERROR_NOT_INITIALIZED);
+
+ // All okay time to finish up the compact process
+ nsCOMPtr<nsIFile> path;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+
+ // get leaf name and database name of the folder
+ nsresult rv = m_folder->GetFilePath(getter_AddRefs(path));
+ nsCOMPtr<nsIFile> folderPath =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderPath->InitWithFile(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = GetSummaryFileLocation(folderPath, getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString dbName;
+ oldSummaryFile->GetNativeLeafName(dbName);
+ nsAutoCString folderName;
+ path->GetNativeLeafName(folderName);
+
+ // close down the temp file stream; preparing for deleting the old folder
+ // and its database; then rename the temp folder and database
+ if (m_fileStream) {
+ m_fileStream->Flush();
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+
+ // make sure the new database is valid.
+ // Close it so we can rename the .msf file.
+ if (m_db) {
+ m_db->ForceClosed();
+ m_db = nullptr;
+ }
+
+ nsCOMPtr<nsIFile> newSummaryFile;
+ rv = GetSummaryFileLocation(m_file, getter_AddRefs(newSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ m_folder->GetDBTransferInfo(getter_AddRefs(transferInfo));
+
+ // close down database of the original folder
+ m_folder->ForceDBClosed();
+
+ nsCOMPtr<nsIFile> cloneFile;
+ int64_t fileSize = 0;
+ rv = m_file->Clone(getter_AddRefs(cloneFile));
+ if (NS_SUCCEEDED(rv)) rv = cloneFile->GetFileSize(&fileSize);
+ bool tempFileRightSize = ((uint64_t)fileSize == m_totalMsgSize);
+ NS_WARNING_ASSERTION(tempFileRightSize,
+ "temp file not of expected size in compact");
+
+ bool folderRenameSucceeded = false;
+ bool msfRenameSucceeded = false;
+ if (NS_SUCCEEDED(rv) && tempFileRightSize) {
+ // First we're going to try and move the old summary file out the way.
+ // We don't delete it yet, as we want to keep the files in sync.
+ nsCOMPtr<nsIFile> tempSummaryFile;
+ rv = oldSummaryFile->Clone(getter_AddRefs(tempSummaryFile));
+ if (NS_SUCCEEDED(rv))
+ rv = tempSummaryFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+
+ nsAutoCString tempSummaryFileName;
+ if (NS_SUCCEEDED(rv))
+ rv = tempSummaryFile->GetNativeLeafName(tempSummaryFileName);
+
+ if (NS_SUCCEEDED(rv))
+ rv = oldSummaryFile->MoveToNative((nsIFile*)nullptr, tempSummaryFileName);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error moving compacted folder's db out of the way");
+ if (NS_SUCCEEDED(rv)) {
+ // Now we've successfully moved the summary file out the way, try moving
+ // the newly compacted message file over the old one.
+ rv = m_file->MoveToNative((nsIFile*)nullptr, folderName);
+ folderRenameSucceeded = NS_SUCCEEDED(rv);
+ NS_WARNING_ASSERTION(folderRenameSucceeded,
+ "error renaming compacted folder");
+ if (folderRenameSucceeded) {
+ // That worked, so land the new summary file in the right place.
+ nsCOMPtr<nsIFile> renamedCompactedSummaryFile;
+ newSummaryFile->Clone(getter_AddRefs(renamedCompactedSummaryFile));
+ if (renamedCompactedSummaryFile) {
+ rv = renamedCompactedSummaryFile->MoveToNative((nsIFile*)nullptr,
+ dbName);
+ msfRenameSucceeded = NS_SUCCEEDED(rv);
+ }
+ NS_WARNING_ASSERTION(msfRenameSucceeded,
+ "error renaming compacted folder's db");
+ }
+
+ if (!msfRenameSucceeded) {
+ // Do our best to put the summary file back to where it was
+ rv = tempSummaryFile->MoveToNative((nsIFile*)nullptr, dbName);
+ if (NS_SUCCEEDED(rv)) {
+ // Flagging that a renamed db no longer exists.
+ tempSummaryFile = nullptr;
+ } else {
+ NS_WARNING("error restoring uncompacted folder's db");
+ }
+ }
+ }
+ // We don't want any temporarily renamed summary file to lie around
+ if (tempSummaryFile) tempSummaryFile->Remove(false);
+ }
+
+ NS_WARNING_ASSERTION(msfRenameSucceeded, "compact failed");
+ nsresult rvReleaseFolderLock = ReleaseFolderLock();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvReleaseFolderLock),
+ "folder lock not released successfully");
+ rv = NS_FAILED(rv) ? rv : rvReleaseFolderLock;
+
+ // Cleanup of nstmp-named compacted files if failure
+ if (!folderRenameSucceeded) {
+ // remove the abandoned compacted version with the wrong name
+ m_file->Remove(false);
+ }
+ if (!msfRenameSucceeded) {
+ // remove the abandoned compacted summary file
+ newSummaryFile->Remove(false);
+ }
+
+ if (msfRenameSucceeded) {
+ // Transfer local db information from transferInfo
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenFolderDB(m_folder, true, getter_AddRefs(m_db));
+ NS_ENSURE_TRUE(m_db, NS_FAILED(rv) ? rv : NS_ERROR_FAILURE);
+ // These errors are expected.
+ rv = (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ ? NS_OK
+ : rv;
+ m_db->SetSummaryValid(true);
+ if (transferInfo) m_folder->SetDBTransferInfo(transferInfo);
+
+ // since we're transferring info from the old db, we need to reset the
+ // expunged bytes
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0);
+ }
+ if (m_db) m_db->Close(true);
+ m_db = nullptr;
+
+ // Notify that compaction of the folder is completed.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyFolderCompactFinish(m_folder);
+ }
+
+ m_folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(rv, m_totalExpungedBytes);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::ReleaseFolderLock() {
+ nsresult result = NS_OK;
+ if (!m_folder) return result;
+ bool haveSemaphore;
+ nsCOMPtr<nsISupports> supports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports));
+ result = m_folder->TestSemaphore(supports, &haveSemaphore);
+ if (NS_SUCCEEDED(result) && haveSemaphore)
+ result = m_folder->ReleaseSemaphore(supports);
+ return result;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::OnStartRequest(nsIRequest* request) {
+ return StartMessage();
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (NS_FAILED(status)) {
+ // Set m_status to status so the destructor can remove the temp folder and
+ // database.
+ m_status = status;
+ CleanupTempFilesAfterError();
+ m_folder->NotifyCompactCompleted();
+ ReleaseFolderLock();
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ } else {
+ // XXX TODO: Error checking and handling missing here.
+ EndCopy(nullptr, status);
+ if (m_curIndex >= m_keys.Length()) {
+ msgHdr = nullptr;
+ // no more to copy finish it up
+ FinishCompact();
+ } else {
+ // in case we're not getting an error, we still need to pretend we did get
+ // an error, because the compact did not successfully complete.
+ m_folder->NotifyCompactCompleted();
+ CleanupTempFilesAfterError();
+ ReleaseFolderLock();
+ }
+ }
+ NS_RELEASE_THIS(); // kill self
+ return status;
+}
+
+// Handle the message data.
+// (NOTE: nsOfflineStoreCompactState overrides this)
+NS_IMETHODIMP
+nsFolderCompactState::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) {
+ MOZ_ASSERT(m_fileStream);
+ MOZ_ASSERT(inStr);
+
+ nsresult rv = NS_OK;
+
+ // TODO: This block should be moved in to the callback that indicates the
+ // start of a new message, but it's complicated because of the derived
+ // nsOfflineStoreCompactState and also the odd message copy listener
+ // orderings. Leaving it here for now, but it's ripe for tidying up in
+ // future.
+ if (m_startOfMsg) {
+ m_messageUri.Truncate(); // clear the previous message uri
+ if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri))) {
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(m_curSrcHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ while (count > 0) {
+ uint32_t maxReadCount =
+ std::min((uint32_t)m_buffer.Length() - m_bufferCount, count);
+ uint32_t readCount;
+ rv = inStr->Read(m_buffer.Elements() + m_bufferCount, maxReadCount,
+ &readCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ count -= readCount;
+ m_bufferCount += readCount;
+ if (m_bufferCount == m_buffer.Length()) {
+ rv = FlushBuffer();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ if (m_bufferCount > 0) {
+ rv = FlushBuffer();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+// Helper to write data to an outputstream, until complete or error.
+static nsresult WriteSpan(nsIOutputStream* writeable,
+ mozilla::Span<const char> data) {
+ while (!data.IsEmpty()) {
+ uint32_t n;
+ nsresult rv = writeable->Write(data.Elements(), data.Length(), &n);
+ NS_ENSURE_SUCCESS(rv, rv);
+ data = data.Last(data.Length() - n);
+ }
+ return NS_OK;
+}
+
+// Flush contents of m_buffer to the output file.
+// (NOTE: not used by nsOfflineStoreCompactState)
+// More complicated than it should be because we need to fiddle with
+// some of the X-Mozilla-* headers on the fly.
+nsresult nsFolderCompactState::FlushBuffer() {
+ MOZ_ASSERT(m_fileStream);
+ nsresult rv;
+ auto buf = m_buffer.AsSpan().First(m_bufferCount);
+ if (!m_startOfMsg) {
+ // We only do header twiddling for the first chunk. So from now on we
+ // just copy data verbatim.
+ rv = WriteSpan(m_fileStream, buf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_bufferCount = 0;
+ return NS_OK;
+ }
+
+ // This is the first chunk of a new message. We'll update the
+ // X-Mozilla-(Status|Status2|Keys) headers as we go.
+ m_startOfMsg = false;
+
+ // Sniff the data to see if we can spot any CRs.
+ // If so, we'll use CRLFs instead of platform-native EOLs.
+ auto sniffChunk = buf.First(std::min<size_t>(buf.Length(), 512));
+ auto cr = std::find(sniffChunk.cbegin(), sniffChunk.cend(), '\r');
+ if (cr != sniffChunk.cend()) {
+ m_eolSeq.Assign("\r\n"_ns);
+ }
+
+ // Add a "From " line if missing.
+ // NOTE: Ultimately we should never see "From " lines in this data - it's an
+ // mbox detail the message streaming should filter out. But for now we'll
+ // handle it optionally.
+ nsAutoCString fromLine;
+ auto l = FirstLine(buf);
+ if (l.Length() > 5 &&
+ nsDependentCSubstring(l.Elements(), 5).EqualsLiteral("From ")) {
+ fromLine = nsDependentCSubstring(l);
+ buf = buf.From(l.Length());
+ } else {
+ fromLine = "From "_ns + m_eolSeq;
+ }
+ rv = WriteSpan(m_fileStream, fromLine);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read as many headers as we can. We might not have the complete header
+ // block our in buffer, but that's OK - the X-Mozilla-* ones should be
+ // right at the start).
+ nsTArray<HeaderReader::Hdr> headers;
+ HeaderReader rdr;
+ auto leftover = rdr.Parse(buf, [&](auto const& hdr) -> bool {
+ auto const& name = hdr.Name(buf);
+ if (!name.EqualsLiteral(HEADER_X_MOZILLA_STATUS) &&
+ !name.EqualsLiteral(HEADER_X_MOZILLA_STATUS2) &&
+ !name.EqualsLiteral(HEADER_X_MOZILLA_KEYWORDS)) {
+ headers.AppendElement(hdr);
+ }
+ return true;
+ });
+
+ // Write out X-Mozilla-* headers first - we'll create these from scratch.
+ uint32_t msgFlags = 0;
+ nsAutoCString keywords;
+ if (m_curSrcHdr) {
+ m_curSrcHdr->GetFlags(&msgFlags);
+ m_curSrcHdr->GetStringProperty("keywords", keywords);
+ // growKeywords is set if msgStore didn't have enough room to edit
+ // X-Mozilla-* headers in situ. We'll rewrite all those headers
+ // regardless but we still want to clear it.
+ uint32_t grow;
+ m_curSrcHdr->GetUint32Property("growKeywords", &grow);
+ if (grow) {
+ m_curSrcHdr->SetUint32Property("growKeywords", 0);
+ }
+ }
+
+ auto out =
+ nsPrintfCString(HEADER_X_MOZILLA_STATUS ": %4.4x", msgFlags & 0xFFFF);
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ out = nsPrintfCString(HEADER_X_MOZILLA_STATUS2 ": %8.8x",
+ msgFlags & 0xFFFF0000);
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Try to leave room for future in-place keyword edits.
+ while (keywords.Length() < X_MOZILLA_KEYWORDS_BLANK_LEN) {
+ keywords.Append(' ');
+ }
+ out = nsPrintfCString(HEADER_X_MOZILLA_KEYWORDS ": %s", keywords.get());
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write out the rest of the headers.
+ for (auto const& hdr : headers) {
+ rv = WriteSpan(m_fileStream, buf.Subspan(hdr.pos, hdr.len));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // The header parser consumes the blank line, If we've completed parsing
+ // we need to output it now.
+ // If we haven't parsed all the headers yet then the blank line will be
+ // safely copied verbatim as part of the remaining data.
+ if (rdr.IsComplete()) {
+ rv = WriteSpan(m_fileStream, m_eolSeq);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Write out everything else in the buffer verbatim.
+ if (leftover.Length() > 0) {
+ rv = WriteSpan(m_fileStream, leftover);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ m_bufferCount = 0;
+ return NS_OK;
+}
+
+/**
+ * nsOfflineStoreCompactState is a helper class for nsFolderCompactor which
+ * handles compacting the mbox for a single offline IMAP folder.
+ *
+ * nsOfflineStoreCompactState does *not* do any special X-Mozilla-* header
+ * handling, unlike the base class.
+ *
+ * NOTE (for future cleanups):
+ * This class uses a different mechanism to iterate through messages. It uses
+ * nsIMsgMessageService.streamMessage() to stream each message in turn,
+ * passing itself in as an nsIStreamListener. The nsICopyMessageStreamListener
+ * callbacks implemented in the base class are _not_ used here.
+ * For each message, the standard OnStartRequest(), OnDataAvailable()...,
+ * OnStopRequest() sequence is seen.
+ * Nothing too fancy, but it's not always clear where code from the base class
+ * is being used and when it is not, so it can be complicated to pick through.
+ *
+ */
+class nsOfflineStoreCompactState : public nsFolderCompactState {
+ public:
+ nsOfflineStoreCompactState(void);
+ virtual ~nsOfflineStoreCompactState(void);
+ NS_IMETHOD OnStopRequest(nsIRequest* request, nsresult status) override;
+ NS_IMETHODIMP OnDataAvailable(nsIRequest* request, nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) override;
+
+ protected:
+ nsresult CopyNextMessage(bool& done);
+ virtual nsresult InitDB(nsIMsgDatabase* db) override;
+ virtual nsresult StartCompacting() override;
+ virtual nsresult FinishCompact() override;
+
+ char m_dataBuffer[COMPACTOR_READ_BUFF_SIZE + 1]; // temp data buffer for
+ // copying message
+ uint32_t m_offlineMsgSize;
+};
+
+nsOfflineStoreCompactState::nsOfflineStoreCompactState()
+ : m_offlineMsgSize(0) {}
+
+nsOfflineStoreCompactState::~nsOfflineStoreCompactState() {}
+
+nsresult nsOfflineStoreCompactState::InitDB(nsIMsgDatabase* db) {
+ // Start with the list of messages we have offline as the possible
+ // message to keep when compacting the offline store.
+ db->ListAllOfflineMsgs(m_keys);
+ m_db = db;
+ return NS_OK;
+}
+
+/**
+ * This will copy one message to the offline store, but if it fails to
+ * copy the next message, it will keep trying messages until it finds one
+ * it can copy, or it runs out of messages.
+ */
+nsresult nsOfflineStoreCompactState::CopyNextMessage(bool& done) {
+ while (m_curIndex < m_keys.Length()) {
+ // Filter out msgs that have the "pendingRemoval" attribute set.
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ nsCString pendingRemoval;
+ nsresult rv =
+ m_db->GetMsgHdrForKey(m_keys[m_curIndex], getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdr->GetStringProperty("pendingRemoval", pendingRemoval);
+ if (!pendingRemoval.IsEmpty()) {
+ m_curIndex++;
+ // Turn off offline flag for message, since after the compact is
+ // completed; we won't have the message in the offline store.
+ uint32_t resultFlags;
+ hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ // We need to clear this in case the user changes the offline retention
+ // settings.
+ hdr->SetStringProperty("pendingRemoval", ""_ns);
+ continue;
+ }
+ m_messageUri.Truncate(); // clear the previous message uri
+ rv = BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_startOfMsg = true;
+ nsCOMPtr<nsISupports> thisSupports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(thisSupports));
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = m_messageService->StreamMessage(m_messageUri, thisSupports, m_window,
+ nullptr, false, EmptyCString(), true,
+ getter_AddRefs(dummyNull));
+ // if copy fails, we clear the offline flag on the source message.
+ if (NS_FAILED(rv)) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ m_messageService->MessageURIToMsgHdr(m_messageUri, getter_AddRefs(hdr));
+ if (hdr) {
+ uint32_t resultFlags;
+ hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ }
+ m_curIndex++;
+ continue;
+ } else
+ break;
+ }
+ done = m_curIndex >= m_keys.Length();
+ // In theory, we might be able to stream the next message, so
+ // return NS_OK, not rv.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineStoreCompactState::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ nsresult rv = status;
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ nsCOMPtr<nsIChannel> channel;
+ bool done = false;
+
+ // The NS_MSG_ERROR_MSG_NOT_OFFLINE error should allow us to continue, so we
+ // check for it specifically and don't terminate the compaction.
+ if (NS_FAILED(rv) && rv != NS_MSG_ERROR_MSG_NOT_OFFLINE) goto done;
+
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ if (NS_FAILED(rv)) goto done;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) goto done;
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(msgHdr));
+ if (NS_FAILED(rv)) goto done;
+
+ // This is however an unexpected condition, so let's print a warning.
+ if (rv == NS_MSG_ERROR_MSG_NOT_OFFLINE) {
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ nsPrintfCString msg("Message expectedly not available offline: %s",
+ spec.get());
+ NS_WARNING(msg.get());
+ }
+
+ if (msgHdr) {
+ if (NS_SUCCEEDED(status)) {
+ msgHdr->SetMessageOffset(m_startOfNewMsg);
+ nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg);
+ msgHdr->SetStringProperty("storeToken", storeToken);
+ msgHdr->SetOfflineMessageSize(m_offlineMsgSize);
+ } else {
+ uint32_t resultFlags;
+ msgHdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ }
+ }
+
+ if (m_window) {
+ m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback)
+ statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length());
+ }
+ // advance to next message
+ m_curIndex++;
+ rv = CopyNextMessage(done);
+ if (done) {
+ m_db->Commit(nsMsgDBCommitType::kCompressCommit);
+ msgHdr = nullptr;
+ // no more to copy finish it up
+ ReleaseFolderLock();
+ FinishCompact();
+ NS_RELEASE_THIS(); // kill self
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ m_status = rv; // set the status to rv so the destructor can remove the
+ // temp folder and database
+ ReleaseFolderLock();
+ NS_RELEASE_THIS(); // kill self
+
+ if (m_completionFn) {
+ m_completionFn(m_status, m_totalExpungedBytes);
+ }
+ return rv;
+ }
+ return rv;
+}
+
+nsresult nsOfflineStoreCompactState::FinishCompact() {
+ // All okay time to finish up the compact process
+ nsCOMPtr<nsIFile> path;
+ uint32_t flags;
+
+ // get leaf name and database name of the folder
+ m_folder->GetFlags(&flags);
+ nsresult rv = m_folder->GetFilePath(getter_AddRefs(path));
+
+ nsCString leafName;
+ path->GetNativeLeafName(leafName);
+
+ if (m_fileStream) {
+ // close down the temp file stream; preparing for deleting the old folder
+ // and its database; then rename the temp folder and database
+ m_fileStream->Flush();
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+
+ // make sure the new database is valid
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0);
+ // this forces the m_folder to update mExpungedBytes from the db folder info.
+ int64_t expungedBytes;
+ m_folder->GetExpungedBytes(&expungedBytes);
+ m_folder->UpdateSummaryTotals(true);
+ m_db->SetSummaryValid(true);
+
+ // remove the old folder
+ path->Remove(false);
+
+ // rename the copied folder to be the original folder
+ m_file->MoveToNative((nsIFile*)nullptr, leafName);
+
+ ShowStatusMsg(EmptyString());
+ m_folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::Init(nsICopyMessageListener* destination) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::StartMessage() {
+ nsresult rv = NS_ERROR_FAILURE;
+ NS_ASSERTION(m_fileStream, "Fatal, null m_fileStream...");
+ if (m_fileStream) {
+ nsCOMPtr<nsISeekableStream> seekableStream =
+ do_QueryInterface(m_fileStream, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // this will force an internal flush, but not a sync. Tell should really do
+ // an internal flush, but it doesn't, and I'm afraid to change that
+ // nsIFileStream.cpp code anymore.
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, 0);
+ // record the new message key for the message
+ int64_t curStreamPos;
+ seekableStream->Tell(&curStreamPos);
+ m_startOfNewMsg = curStreamPos;
+ rv = NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::EndMessage(nsMsgKey key) { return NS_OK; }
+
+// XXX TODO: This function is sadly lacking all status checking, it always
+// returns NS_OK and moves onto the next message.
+NS_IMETHODIMP
+nsFolderCompactState::EndCopy(nsIURI* uri, nsresult status) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+
+ if (m_curIndex >= m_keys.Length()) {
+ NS_ASSERTION(false, "m_curIndex out of bounds");
+ return NS_OK;
+ }
+
+ // Take note of the end offset of the message (without the trailing blank
+ // line).
+ nsCOMPtr<nsITellableStream> tellable(do_QueryInterface(m_fileStream));
+ MOZ_ASSERT(tellable);
+ int64_t endOfMsg;
+ nsresult rv = tellable->Tell(&endOfMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Messages need to have trailing blank lines */
+ rv = WriteSpan(m_fileStream, m_eolSeq);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /*
+ * Done with the current message; copying the existing message header
+ * to the new database.
+ */
+ if (m_curSrcHdr) {
+ nsMsgKey key;
+ m_curSrcHdr->GetMessageKey(&key);
+ m_db->CopyHdrFromExistingHdr(key, m_curSrcHdr, true,
+ getter_AddRefs(newMsgHdr));
+ }
+ m_curSrcHdr = nullptr;
+ if (newMsgHdr) {
+ nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg);
+ newMsgHdr->SetStringProperty("storeToken", storeToken);
+ newMsgHdr->SetMessageOffset(m_startOfNewMsg);
+ uint64_t msgSize = endOfMsg - m_startOfNewMsg;
+ newMsgHdr->SetMessageSize(msgSize);
+
+ m_totalMsgSize += msgSize + m_eolSeq.Length();
+ }
+
+ // m_db->Commit(nsMsgDBCommitType::kLargeCommit); // no sense committing
+ // until the end
+ // advance to next message
+ m_curIndex++;
+ m_startOfMsg = true;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ if (m_window) {
+ m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback)
+ statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length());
+ }
+ return NS_OK;
+}
+
+nsresult nsOfflineStoreCompactState::StartCompacting() {
+ nsresult rv = NS_OK;
+ if (m_keys.Length() > 0 && m_curIndex == 0) {
+ NS_ADDREF_THIS(); // we own ourselves, until we're done, anyway.
+ ShowCompactingStatusMsg();
+ bool done = false;
+ rv = CopyNextMessage(done);
+ if (!done) return rv;
+ }
+ ReleaseFolderLock();
+ FinishCompact();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsOfflineStoreCompactState::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ if (!m_fileStream || !inStr) return NS_ERROR_FAILURE;
+
+ nsresult rv = NS_OK;
+
+ if (m_startOfMsg) {
+ m_offlineMsgSize = 0;
+ m_messageUri.Truncate(); // clear the previous message uri
+ if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri))) {
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(m_curSrcHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ uint32_t maxReadCount, readCount, writeCount;
+ uint32_t bytesWritten;
+
+ while (NS_SUCCEEDED(rv) && (int32_t)count > 0) {
+ maxReadCount =
+ count > sizeof(m_dataBuffer) - 1 ? sizeof(m_dataBuffer) - 1 : count;
+ writeCount = 0;
+ rv = inStr->Read(m_dataBuffer, maxReadCount, &readCount);
+
+ if (NS_SUCCEEDED(rv)) {
+ if (m_startOfMsg) {
+ m_startOfMsg = false;
+ // check if there's an envelope header; if not, write one.
+ if (strncmp(m_dataBuffer, "From ", 5)) {
+ m_fileStream->Write("From " CRLF, 7, &bytesWritten);
+ m_offlineMsgSize += bytesWritten;
+ }
+ }
+ m_fileStream->Write(m_dataBuffer, readCount, &bytesWritten);
+ m_offlineMsgSize += bytesWritten;
+ writeCount += bytesWritten;
+ count -= readCount;
+ if (writeCount != readCount) {
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+ }
+ }
+ return rv;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// nsMsgFolderCompactor implementation
+//////////////////////////////////////////////////////////////////////////////
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCompactor, nsIMsgFolderCompactor)
+
+nsMsgFolderCompactor::nsMsgFolderCompactor() {}
+
+nsMsgFolderCompactor::~nsMsgFolderCompactor() {}
+
+NS_IMETHODIMP nsMsgFolderCompactor::CompactFolders(
+ const nsTArray<RefPtr<nsIMsgFolder>>& folders, nsIUrlListener* listener,
+ nsIMsgWindow* window) {
+ MOZ_ASSERT(mQueue.IsEmpty());
+ mWindow = window;
+ mListener = listener;
+ mTotalBytesGained = 0;
+ mQueue = folders.Clone();
+ mQueue.Reverse();
+
+ // Can't guarantee that anyone will keep us in scope until we're done, so...
+ MOZ_ASSERT(!mKungFuDeathGrip);
+ mKungFuDeathGrip = this;
+
+ // nsIMsgFolderCompactor idl states this isn't called...
+ // but maybe it should be?
+ // if (mListener) {
+ // mListener->OnStartRunningUrl(nullptr);
+ // }
+
+ NextFolder();
+
+ return NS_OK;
+}
+
+void nsMsgFolderCompactor::NextFolder() {
+ while (!mQueue.IsEmpty()) {
+ // Should only ever have one compactor running.
+ MOZ_ASSERT(mCompactor == nullptr);
+
+ nsCOMPtr<nsIMsgFolder> folder = mQueue.PopLastElement();
+
+ // Sanity check - should we be compacting this folder?
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Skipping folder with no msgStore");
+ continue;
+ }
+ bool storeSupportsCompaction;
+ msgStore->GetSupportsCompaction(&storeSupportsCompaction);
+ if (!storeSupportsCompaction) {
+ NS_WARNING("Trying to compact a non-mbox folder");
+ continue; // just skip it.
+ }
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder));
+ if (imapFolder) {
+ uint32_t flags;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Offline) {
+ mCompactor = new nsOfflineStoreCompactState();
+ }
+ } else {
+ mCompactor = new nsFolderCompactState();
+ }
+ if (!mCompactor) {
+ NS_WARNING("skipping compact of non-offline folder");
+ continue;
+ }
+ nsCString uri;
+ folder->GetURI(uri);
+
+ // Callback for when a folder compaction completes.
+ auto completionFn = [self = RefPtr<nsMsgFolderCompactor>(this),
+ compactState = mCompactor](nsresult status,
+ uint64_t expungedBytes) {
+ if (NS_SUCCEEDED(status)) {
+ self->mTotalBytesGained += expungedBytes;
+ } else {
+ // Failed. We want to keep going with the next folder, but make sure
+ // we return a failing code upon overall completion.
+ self->mOverallStatus = status;
+ NS_WARNING("folder compact failed.");
+ }
+
+ // Release our lock on the compactor - it's done.
+ self->mCompactor = nullptr;
+ self->NextFolder();
+ };
+
+ rv = mCompactor->Compact(folder, completionFn, mWindow);
+ if (NS_SUCCEEDED(rv)) {
+ // Now wait for the compactor to let us know it's finished,
+ // via the completion callback fn.
+ return;
+ }
+ mOverallStatus = rv;
+ mCompactor = nullptr;
+ NS_WARNING("folder compact failed - skipping folder");
+ }
+
+ // Done. No more folders to compact.
+
+ if (mListener) {
+ // If there were multiple failures, this will communicate only the
+ // last one, but that's OK. Main thing is to indicate that _something_
+ // went wrong.
+ mListener->OnStopRunningUrl(nullptr, mOverallStatus);
+ }
+ ShowDoneStatus();
+
+ // We're not needed any more.
+ mKungFuDeathGrip = nullptr;
+ return;
+}
+
+void nsMsgFolderCompactor::ShowDoneStatus() {
+ if (!mWindow) {
+ return;
+ }
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ nsAutoString expungedAmount;
+ FormatFileSize(mTotalBytesGained, true, expungedAmount);
+ AutoTArray<nsString, 1> params = {expungedAmount};
+ nsString msg;
+ rv = bundle->FormatStringFromName("compactingDone", params, msg);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mWindow->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback) {
+ statusFeedback->SetStatusString(msg);
+ }
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.h b/comm/mailnews/base/src/nsMsgFolderCompactor.h
new file mode 100644
index 0000000000..b9de16be12
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCompactor.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsMsgFolderCompactor_h
+#define _nsMsgFolderCompactor_h
+
+#include "nsIMsgFolderCompactor.h"
+
+class nsIMsgFolder;
+class nsIMsgWindow;
+class nsFolderCompactState;
+
+/**
+ * nsMsgFolderCompactor implements nsIMsgFolderCompactor, which allows the
+ * caller to kick off a batch of folder compactions (via compactFolders()).
+ */
+class nsMsgFolderCompactor : public nsIMsgFolderCompactor {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERCOMPACTOR
+
+ nsMsgFolderCompactor();
+
+ protected:
+ virtual ~nsMsgFolderCompactor();
+ // The folders waiting to be compacted.
+ nsTArray<RefPtr<nsIMsgFolder>> mQueue;
+
+ // If any individual folders fail to compact, we stash the latest fail code
+ // here (to return via listener upon overall completion).
+ nsresult mOverallStatus{NS_OK};
+
+ // If set, OnStopRunningUrl() will be called when all folders done.
+ nsCOMPtr<nsIUrlListener> mListener;
+ // If set, progress status updates will be sent here.
+ nsCOMPtr<nsIMsgWindow> mWindow;
+ RefPtr<nsMsgFolderCompactor> mKungFuDeathGrip;
+ uint64_t mTotalBytesGained{0};
+
+ // The currently-running compactor.
+ RefPtr<nsFolderCompactState> mCompactor;
+
+ void NextFolder();
+ void ShowDoneStatus();
+};
+#endif
diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp
new file mode 100644
index 0000000000..7983a8ec56
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgFolderNotificationService.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIImapIncomingServer.h"
+
+//
+// nsMsgFolderNotificationService
+//
+NS_IMPL_ISUPPORTS(nsMsgFolderNotificationService,
+ nsIMsgFolderNotificationService)
+
+nsMsgFolderNotificationService::nsMsgFolderNotificationService() {}
+
+nsMsgFolderNotificationService::~nsMsgFolderNotificationService() {
+ /* destructor code */
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::GetHasListeners(
+ bool* aHasListeners) {
+ NS_ENSURE_ARG_POINTER(aHasListeners);
+ *aHasListeners = mListeners.Length() > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::AddListener(
+ nsIMsgFolderListener* aListener, msgFolderListenerFlag aFlags) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ MsgFolderListener listener(aListener, aFlags);
+ mListeners.AppendElementUnlessExists(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::RemoveListener(
+ nsIMsgFolderListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+#define NOTIFY_MSGFOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<MsgFolderListener>::ForwardIterator iter(mListeners); \
+ while (iter.HasMore()) { \
+ const MsgFolderListener& listener = iter.GetNext(); \
+ if (listener.mFlags & propertyflag_) \
+ listener.mListener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgAdded(
+ nsIMsgDBHdr* aMsg) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgAdded, MsgAdded, (aMsg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsClassified(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs, bool aJunkProcessed,
+ bool aTraitProcessed) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsClassified, MsgsClassified,
+ (aMsgs, aJunkProcessed, aTraitProcessed));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsJunkStatusChanged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsJunkStatusChanged, MsgsJunkStatusChanged,
+ (messages));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsDeleted(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsDeleted, MsgsDeleted, (aMsgs));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsMoveCopyCompleted(
+ bool aMove, const nsTArray<RefPtr<nsIMsgDBHdr>>& aSrcMsgs,
+ nsIMsgFolder* aDestFolder, const nsTArray<RefPtr<nsIMsgDBHdr>>& aDestMsgs) {
+ // IMAP delete model means that a "move" isn't really a move, it is a copy,
+ // followed by storing the IMAP deleted flag on the message.
+ bool isReallyMove = aMove;
+ if (aMove && !mListeners.IsEmpty() && !aSrcMsgs.IsEmpty()) {
+ nsresult rv;
+ // Assume that all the source messages are from the same server.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = aSrcMsgs[0]->GetFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(msgFolder));
+ if (imapFolder) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) {
+ nsMsgImapDeleteModel deleteModel;
+ imapServer->GetDeleteModel(&deleteModel);
+ if (deleteModel == nsMsgImapDeleteModels::IMAPDelete)
+ isReallyMove = false;
+ }
+ }
+ }
+
+ NOTIFY_MSGFOLDER_LISTENERS(msgsMoveCopyCompleted, MsgsMoveCopyCompleted,
+ (isReallyMove, aSrcMsgs, aDestFolder, aDestMsgs));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFolderNotificationService::NotifyMsgKeyChanged(nsMsgKey aOldKey,
+ nsIMsgDBHdr* aNewHdr) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgKeyChanged, MsgKeyChanged, (aOldKey, aNewHdr));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFolderNotificationService::NotifyMsgUnincorporatedMoved(
+ nsIMsgFolder* srcFolder, nsIMsgDBHdr* msg) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgUnincorporatedMoved, MsgUnincorporatedMoved,
+ (srcFolder, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderAdded(
+ nsIMsgFolder* aFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderAdded, FolderAdded, (aFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderDeleted(
+ nsIMsgFolder* aFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderDeleted, FolderDeleted, (aFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderMoveCopyCompleted(
+ bool aMove, nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDestFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderMoveCopyCompleted, FolderMoveCopyCompleted,
+ (aMove, aSrcFolder, aDestFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderRenamed(
+ nsIMsgFolder* aOrigFolder, nsIMsgFolder* aNewFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderRenamed, FolderRenamed,
+ (aOrigFolder, aNewFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactStart(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderCompactStart, FolderCompactStart, (folder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactFinish(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderCompactFinish, FolderCompactFinish,
+ (folder));
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderReindexTriggered(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderReindexTriggered, FolderReindexTriggered,
+ (folder));
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.h b/comm/mailnews/base/src/nsMsgFolderNotificationService.h
new file mode 100644
index 0000000000..fecf3b3064
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsMsgFolderNotificationService_h__
+#define nsMsgFolderNotificationService_h__
+
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIMsgFolderListener.h"
+#include "nsTObserverArray.h"
+#include "nsCOMPtr.h"
+
+class nsMsgFolderNotificationService final
+ : public nsIMsgFolderNotificationService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERNOTIFICATIONSERVICE
+
+ nsMsgFolderNotificationService();
+
+ private:
+ ~nsMsgFolderNotificationService();
+ struct MsgFolderListener {
+ nsCOMPtr<nsIMsgFolderListener> mListener;
+ msgFolderListenerFlag mFlags;
+
+ MsgFolderListener(nsIMsgFolderListener* aListener,
+ msgFolderListenerFlag aFlags)
+ : mListener(aListener), mFlags(aFlags) {}
+ MsgFolderListener(const MsgFolderListener& aListener)
+ : mListener(aListener.mListener), mFlags(aListener.mFlags) {}
+ ~MsgFolderListener() {}
+
+ int operator==(nsIMsgFolderListener* aListener) const {
+ return mListener == aListener;
+ }
+ int operator==(const MsgFolderListener& aListener) const {
+ return mListener == aListener.mListener;
+ }
+ };
+
+ nsTObserverArray<MsgFolderListener> mListeners;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgGroupThread.cpp b/comm/mailnews/base/src/nsMsgGroupThread.cpp
new file mode 100644
index 0000000000..32a132d27a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupThread.cpp
@@ -0,0 +1,731 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgGroupThread.h"
+#include "nsMsgDBView.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+#include "nsSimpleEnumerator.h"
+
+NS_IMPL_ISUPPORTS(nsMsgGroupThread, nsIMsgThread)
+
+nsMsgGroupThread::nsMsgGroupThread() { Init(); }
+
+nsMsgGroupThread::nsMsgGroupThread(nsIMsgDatabase* db) {
+ m_db = db;
+ Init();
+}
+
+void nsMsgGroupThread::Init() {
+ m_threadKey = nsMsgKey_None;
+ m_threadRootKey = nsMsgKey_None;
+ m_numUnreadChildren = 0;
+ m_flags = 0;
+ m_newestMsgDate = 0;
+ m_dummy = false;
+}
+
+nsMsgGroupThread::~nsMsgGroupThread() {}
+
+NS_IMETHODIMP nsMsgGroupThread::SetThreadKey(nsMsgKey threadKey) {
+ m_threadKey = threadKey;
+ // by definition, the initial thread key is also the thread root key.
+ m_threadRootKey = threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetThreadKey(nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetFlags(uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetFlags(uint32_t aFlags) {
+ m_flags = aFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetSubject(const nsACString& aSubject) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetSubject(nsACString& result) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_keys.Length(); // - ((m_dummy) ? 1 : 0);
+ return NS_OK;
+}
+
+uint32_t nsMsgGroupThread::NumRealChildren() {
+ return m_keys.Length() - ((m_dummy) ? 1 : 0);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNumUnreadChildren(
+ uint32_t* aNumUnreadChildren) {
+ NS_ENSURE_ARG_POINTER(aNumUnreadChildren);
+ *aNumUnreadChildren = m_numUnreadChildren;
+ return NS_OK;
+}
+
+void nsMsgGroupThread::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ m_keys.InsertElementAt(index, msgKey);
+}
+
+void nsMsgGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ m_keys[index] = msgKey;
+}
+
+nsMsgViewIndex nsMsgGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ return (nsMsgViewIndex)m_keys.IndexOf(msgKey);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::AddChild(nsIMsgDBHdr* child,
+ nsIMsgDBHdr* inReplyTo,
+ bool threadInThread,
+ nsIDBChangeAnnouncer* announcer) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsMsgViewIndex nsMsgGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ nsMsgKey newHdrKey;
+ child->GetMessageKey(&newHdrKey);
+ uint32_t insertIndex = 0;
+ // since we're sorted by date, we could do a binary search for the
+ // insert point. Or, we could start at the end...
+ if (m_keys.Length() > 0) {
+ nsMsgViewSortTypeValue sortType;
+ nsMsgViewSortOrderValue sortOrder;
+ (void)view->GetSortType(&sortType);
+ (void)view->GetSortOrder(&sortOrder);
+ // historical behavior is ascending date order unless our primary sort is
+ // on date
+ nsMsgViewSortOrderValue threadSortOrder =
+ (sortType == nsMsgViewSortType::byDate &&
+ sortOrder == nsMsgViewSortOrder::descending)
+ ? nsMsgViewSortOrder::descending
+ : nsMsgViewSortOrder::ascending;
+ // new behavior is tricky and uses the secondary sort order if the secondary
+ // sort is on the date
+ nsMsgViewSortTypeValue secondarySortType;
+ nsMsgViewSortOrderValue secondarySortOrder;
+ (void)view->GetSecondarySortType(&secondarySortType);
+ (void)view->GetSecondarySortOrder(&secondarySortOrder);
+ if (secondarySortType == nsMsgViewSortType::byDate)
+ threadSortOrder = secondarySortOrder;
+ // sort by date within group.
+ insertIndex = GetInsertIndexFromView(view, child, threadSortOrder);
+ }
+ m_keys.InsertElementAt(insertIndex, newHdrKey);
+ if (!insertIndex) m_threadRootKey = newHdrKey;
+ return insertIndex;
+}
+
+nsMsgViewIndex nsMsgGroupThread::GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) {
+ return view->GetInsertIndexHelper(child, m_keys, nullptr, threadSortOrder,
+ nsMsgViewSortType::byDate);
+}
+
+nsMsgViewIndex nsMsgGroupThread::AddChildFromGroupView(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ uint32_t newHdrFlags = 0;
+ uint32_t msgDate;
+ nsMsgKey newHdrKey = 0;
+
+ child->GetFlags(&newHdrFlags);
+ child->GetMessageKey(&newHdrKey);
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate);
+
+ child->AndFlags(~(nsMsgMessageFlags::Watched), &newHdrFlags);
+ uint32_t numChildren;
+
+ // get the num children before we add the new header.
+ GetNumChildren(&numChildren);
+
+ // if this is an empty thread, set the root key to this header's key
+ if (numChildren == 0) m_threadRootKey = newHdrKey;
+
+ if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1);
+
+ return AddMsgHdrInDateOrder(child, view);
+}
+
+nsresult nsMsgGroupThread::ReparentNonReferenceChildrenOf(
+ nsIMsgDBHdr* topLevelHdr, nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChildKeyAt(uint32_t aIndex,
+ nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (aIndex >= m_keys.Length()) return NS_ERROR_INVALID_ARG;
+ *aResult = m_keys[aIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChildHdrAt(uint32_t aIndex,
+ nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+ return m_db->GetMsgHdrForKey(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChild(nsMsgKey msgKey,
+ nsIMsgDBHdr** aResult) {
+ return GetChildHdrAt(m_keys.IndexOf(msgKey), aResult);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::RemoveChildAt(uint32_t aIndex) {
+ NS_ENSURE_TRUE(aIndex < m_keys.Length(), NS_MSG_MESSAGE_NOT_FOUND);
+
+ m_keys.RemoveElementAt(aIndex);
+ return NS_OK;
+}
+
+nsresult nsMsgGroupThread::RemoveChild(nsMsgKey msgKey) {
+ m_keys.RemoveElement(msgKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::RemoveChildHdr(
+ nsIMsgDBHdr* child, nsIDBChangeAnnouncer* announcer) {
+ NS_ENSURE_ARG_POINTER(child);
+
+ uint32_t flags;
+ nsMsgKey key;
+
+ child->GetFlags(&flags);
+ child->GetMessageKey(&key);
+
+ // if this was the newest msg, clear the newest msg date so we'll recalc.
+ uint32_t date;
+ child->GetDateInSeconds(&date);
+ if (date == m_newestMsgDate) SetNewestMsgDate(0);
+
+ if (!(flags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1);
+ nsMsgViewIndex threadIndex = FindMsgHdr(child);
+ bool wasFirstChild = threadIndex == 0;
+ nsresult rv = RemoveChildAt(threadIndex);
+ // if we're deleting the root of a dummy thread, need to update the threadKey
+ // and the dummy header at position 0
+ if (m_dummy && wasFirstChild && m_keys.Length() > 1) {
+ nsIMsgDBHdr* newRootChild;
+ rv = GetChildHdrAt(1, &newRootChild);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetMsgHdrAt(0, newRootChild);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgGroupThread::ReparentChildrenOf(nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer) {
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ if (numChildren > 0) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey threadParent;
+
+ curHdr->GetThreadParent(&threadParent);
+ if (threadParent == oldParent) {
+ nsMsgKey curKey;
+
+ curHdr->SetThreadParent(newParent);
+ curHdr->GetMessageKey(&curKey);
+ if (announcer)
+ announcer->NotifyParentChangedAll(curKey, oldParent, newParent,
+ nullptr);
+ // if the old parent was the root of the thread, then only the first
+ // child gets promoted to root, and other children become children of
+ // the new root.
+ if (newParent == nsMsgKey_None) {
+ m_threadRootKey = curKey;
+ newParent = curKey;
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::MarkChildRead(bool bRead) {
+ ChangeUnreadChildCount(bRead ? -1 : 1);
+ return NS_OK;
+}
+
+// this could be moved into utils, because I think it's the same as the db impl.
+class nsMsgGroupThreadEnumerator : public nsBaseMsgEnumerator {
+ public:
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ // nsMsgGroupThreadEnumerator methods:
+ typedef nsresult (*nsMsgGroupThreadEnumeratorFilter)(nsIMsgDBHdr* hdr,
+ void* closure);
+
+ nsMsgGroupThreadEnumerator(nsMsgGroupThread* thread, nsMsgKey startKey,
+ nsMsgGroupThreadEnumeratorFilter filter,
+ void* closure);
+ int32_t MsgKeyFirstChildIndex(nsMsgKey inMsgKey);
+
+ protected:
+ virtual ~nsMsgGroupThreadEnumerator();
+
+ nsresult Prefetch();
+
+ nsCOMPtr<nsIMsgDBHdr> mResultHdr;
+ RefPtr<nsMsgGroupThread> mThread;
+ nsMsgKey mThreadParentKey;
+ nsMsgKey mFirstMsgKey;
+ int32_t mChildIndex;
+ bool mDone;
+ bool mNeedToPrefetch;
+ nsMsgGroupThreadEnumeratorFilter mFilter;
+ void* mClosure;
+ bool mFoundChildren;
+};
+
+nsMsgGroupThreadEnumerator::nsMsgGroupThreadEnumerator(
+ nsMsgGroupThread* thread, nsMsgKey startKey,
+ nsMsgGroupThreadEnumeratorFilter filter, void* closure)
+ : mDone(false), mFilter(filter), mClosure(closure), mFoundChildren(false) {
+ mThreadParentKey = startKey;
+ mChildIndex = 0;
+ mThread = thread;
+ mNeedToPrefetch = true;
+ mFirstMsgKey = nsMsgKey_None;
+
+ nsresult rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) mResultHdr->GetMessageKey(&mFirstMsgKey);
+
+ uint32_t numChildren;
+ mThread->GetNumChildren(&numChildren);
+
+ if (mThreadParentKey != nsMsgKey_None) {
+ nsMsgKey msgKey = nsMsgKey_None;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ mResultHdr->GetMessageKey(&msgKey);
+ if (msgKey == startKey) {
+ mChildIndex = MsgKeyFirstChildIndex(msgKey);
+ mDone = (mChildIndex < 0);
+ break;
+ }
+
+ if (mDone) break;
+ } else
+ NS_ASSERTION(false, "couldn't get child from thread");
+ }
+ }
+
+#ifdef DEBUG_bienvenu1
+ nsCOMPtr<nsIMsgDBHdr> child;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey threadParent;
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ child->GetMessageKey(&msgKey);
+ child->GetThreadParent(&threadParent);
+
+ printf("index = %ld key = %ld parent = %lx\n", childIndex, msgKey,
+ threadParent);
+ }
+ }
+#endif
+}
+
+nsMsgGroupThreadEnumerator::~nsMsgGroupThreadEnumerator() {}
+
+int32_t nsMsgGroupThreadEnumerator::MsgKeyFirstChildIndex(nsMsgKey inMsgKey) {
+ // look through rest of thread looking for a child of this message.
+ // If the inMsgKey is the first message in the thread, then all children
+ // without parents are considered to be children of inMsgKey.
+ // Otherwise, only true children qualify.
+ uint32_t numChildren;
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ int32_t firstChildIndex = -1;
+
+ mThread->GetNumChildren(&numChildren);
+
+ for (uint32_t curChildIndex = 0; curChildIndex < numChildren;
+ curChildIndex++) {
+ nsresult rv = mThread->GetChildHdrAt(curChildIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey parentKey;
+
+ curHdr->GetThreadParent(&parentKey);
+ if (parentKey == inMsgKey) {
+ firstChildIndex = curChildIndex;
+ break;
+ }
+ }
+ }
+#ifdef DEBUG_bienvenu1
+ printf("first child index of %ld = %ld\n", inMsgKey, firstChildIndex);
+#endif
+ return firstChildIndex;
+}
+
+NS_IMETHODIMP nsMsgGroupThreadEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+ nsresult rv = NS_OK;
+
+ if (mNeedToPrefetch) rv = Prefetch();
+
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ NS_ADDREF(*aItem = mResultHdr);
+ mNeedToPrefetch = true;
+ }
+ return rv;
+}
+
+nsresult nsMsgGroupThreadEnumerator::Prefetch() {
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ mResultHdr = nullptr;
+ if (mThreadParentKey == nsMsgKey_None) {
+ rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && mResultHdr,
+ "better be able to get root hdr");
+ mChildIndex = 0; // since root can be anywhere, set mChildIndex to 0.
+ } else if (!mDone) {
+ uint32_t numChildren;
+ mThread->GetNumChildren(&numChildren);
+
+ while ((uint32_t)mChildIndex < numChildren) {
+ rv = mThread->GetChildHdrAt(mChildIndex++, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ nsMsgKey parentKey;
+ nsMsgKey curKey;
+
+ if (mFilter && NS_FAILED(mFilter(mResultHdr, mClosure))) {
+ mResultHdr = nullptr;
+ continue;
+ }
+
+ mResultHdr->GetThreadParent(&parentKey);
+ mResultHdr->GetMessageKey(&curKey);
+ // if the parent is the same as the msg we're enumerating over,
+ // or the parentKey isn't set, and we're iterating over the top
+ // level message in the thread, then leave mResultHdr set to cur msg.
+ if (parentKey == mThreadParentKey ||
+ (parentKey == nsMsgKey_None && mThreadParentKey == mFirstMsgKey &&
+ curKey != mThreadParentKey))
+ break;
+ mResultHdr = nullptr;
+ } else
+ NS_ASSERTION(false, "better be able to get child");
+ }
+ // if (!mResultHdr && mThreadParentKey == mFirstMsgKey && !mFoundChildren &&
+ // numChildren > 1) {
+ // mThread->ReparentMsgsWithInvalidParent(numChildren, mThreadParentKey);
+ // }
+ }
+ if (!mResultHdr) {
+ mDone = true;
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mDone = true;
+ return rv;
+ } else
+ mNeedToPrefetch = false;
+ mFoundChildren = true;
+
+#ifdef DEBUG_bienvenu1
+ nsMsgKey debugMsgKey;
+ mResultHdr->GetMessageKey(&debugMsgKey);
+ printf("next for %ld = %ld\n", mThreadParentKey, debugMsgKey);
+#endif
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThreadEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (mNeedToPrefetch) Prefetch();
+ *aResult = !mDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::EnumerateMessages(nsMsgKey parentKey,
+ nsIMsgEnumerator** result) {
+ NS_ADDREF(*result = new nsMsgGroupThreadEnumerator(this, parentKey, nullptr,
+ nullptr));
+ return NS_OK;
+}
+
+#if 0
+nsresult nsMsgGroupThread::ReparentMsgsWithInvalidParent(uint32_t numChildren, nsMsgKey threadParentKey)
+{
+ nsresult ret = NS_OK;
+ // run through looking for messages that don't have a correct parent,
+ // i.e., a parent that's in the thread!
+ for (int32_t childIndex = 0; childIndex < (int32_t) numChildren; childIndex++)
+ {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(ret) && curChild)
+ {
+ nsMsgKey parentKey;
+ nsCOMPtr<nsIMsgDBHdr> parent;
+
+ curChild->GetThreadParent(&parentKey);
+
+ if (parentKey != nsMsgKey_None)
+ {
+ GetChild(parentKey, getter_AddRefs(parent));
+ if (!parent)
+ curChild->SetThreadParent(threadParentKey);
+ }
+ }
+ }
+ return ret;
+}
+#endif
+
+NS_IMETHODIMP nsMsgGroupThread::GetRootHdr(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ *result = nullptr;
+ int32_t resultIndex = -1;
+
+ if (m_threadRootKey != nsMsgKey_None) {
+ nsresult ret = GetChildHdrForKey(m_threadRootKey, result, &resultIndex);
+ if (NS_SUCCEEDED(ret) && *result)
+ return ret;
+ else {
+ printf("need to reset thread root key\n");
+ uint32_t numChildren;
+ nsMsgKey threadParentKey = nsMsgKey_None;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(ret) && curChild) {
+ nsMsgKey parentKey;
+
+ curChild->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) {
+ NS_ASSERTION(!(*result), "two top level msgs, not good");
+ curChild->GetMessageKey(&threadParentKey);
+ m_threadRootKey = threadParentKey;
+ curChild.forget(result);
+ }
+ }
+ }
+ if (*result) {
+ return NS_OK;
+ }
+ }
+ // if we can't get the thread root key, we'll just get the first hdr.
+ // there's a bug where sometimes we weren't resetting the thread root key
+ // when removing the thread root key.
+ }
+ return GetChildHdrAt(0, result);
+}
+
+nsresult nsMsgGroupThread::ChangeUnreadChildCount(int32_t delta) {
+ m_numUnreadChildren += delta;
+ return NS_OK;
+}
+
+nsresult nsMsgGroupThread::GetChildHdrForKey(nsMsgKey desiredKey,
+ nsIMsgDBHdr** result,
+ int32_t* resultIndex) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ uint32_t childIndex;
+ for (childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ child->GetMessageKey(&msgKey);
+
+ if (msgKey == desiredKey) {
+ child.forget(result);
+ break;
+ }
+ }
+ }
+ if (resultIndex) *resultIndex = (int32_t)childIndex;
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetFirstUnreadChild(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ bool isRead;
+ rv = m_db->IsRead(msgKey, &isRead);
+ if (NS_SUCCEEDED(rv) && !isRead) {
+ child.forget(result);
+ break;
+ }
+ }
+ }
+
+ return (*result) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNewestMsgDate(uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // if this hasn't been set, figure it out by enumerating the msgs in the
+ // thread.
+ if (!m_newestMsgDate) {
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ uint32_t msgDate;
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate;
+ }
+ }
+ }
+ *aResult = m_newestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetNewestMsgDate(uint32_t aNewestMsgDate) {
+ m_newestMsgDate = aNewestMsgDate;
+ return NS_OK;
+}
+
+nsMsgXFGroupThread::nsMsgXFGroupThread() {}
+
+nsMsgXFGroupThread::~nsMsgXFGroupThread() {}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_folders.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetChildHdrAt(uint32_t aIndex,
+ nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_folders.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+ return m_folders.ObjectAt(aIndex)->GetMessageHeader(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetChildKeyAt(uint32_t aIndex,
+ nsMsgKey* aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::RemoveChildAt(uint32_t aIndex) {
+ NS_ENSURE_TRUE(aIndex < m_folders.Length(), NS_MSG_MESSAGE_NOT_FOUND);
+
+ nsresult rv = nsMsgGroupThread::RemoveChildAt(aIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_folders.RemoveElementAt(aIndex);
+ return NS_OK;
+}
+
+void nsMsgXFGroupThread::InsertMsgHdrAt(nsMsgViewIndex index,
+ nsIMsgDBHdr* hdr) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, index);
+ nsMsgGroupThread::InsertMsgHdrAt(index, hdr);
+}
+
+void nsMsgXFGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.ReplaceObjectAt(folder, index);
+ nsMsgGroupThread::SetMsgHdrAt(index, hdr);
+}
+
+nsMsgViewIndex nsMsgXFGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ size_t index = 0;
+ while (true) {
+ index = m_keys.IndexOf(msgKey, index);
+ if (index == m_keys.NoIndex || m_folders[index] == folder) break;
+ index++;
+ }
+ return (nsMsgViewIndex)index;
+}
+
+nsMsgViewIndex nsMsgXFGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ nsMsgViewIndex insertIndex =
+ nsMsgGroupThread::AddMsgHdrInDateOrder(child, view);
+ nsCOMPtr<nsIMsgFolder> folder;
+ child->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, insertIndex);
+ return insertIndex;
+}
+nsMsgViewIndex nsMsgXFGroupThread::GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) {
+ return view->GetInsertIndexHelper(child, m_keys, &m_folders, threadSortOrder,
+ nsMsgViewSortType::byDate);
+}
diff --git a/comm/mailnews/base/src/nsMsgGroupThread.h b/comm/mailnews/base/src/nsMsgGroupThread.h
new file mode 100644
index 0000000000..735b9580e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupThread.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgThread.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgDBView.h"
+
+class nsMsgGroupView;
+
+class nsMsgGroupThread : public nsIMsgThread {
+ public:
+ friend class nsMsgGroupView;
+
+ nsMsgGroupThread();
+ explicit nsMsgGroupThread(nsIMsgDatabase* db);
+
+ NS_DECL_NSIMSGTHREAD
+ NS_DECL_ISUPPORTS
+
+ protected:
+ virtual ~nsMsgGroupThread();
+
+ void Init();
+ nsMsgViewIndex AddChildFromGroupView(nsIMsgDBHdr* child, nsMsgDBView* view);
+ nsresult RemoveChild(nsMsgKey msgKey);
+ nsresult RerootThread(nsIMsgDBHdr* newParentOfOldRoot, nsIMsgDBHdr* oldRoot,
+ nsIDBChangeAnnouncer* announcer);
+
+ virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view);
+ virtual nsMsgViewIndex GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder);
+ nsresult ReparentNonReferenceChildrenOf(nsIMsgDBHdr* topLevelHdr,
+ nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer);
+
+ nsresult ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer);
+ nsresult ChangeUnreadChildCount(int32_t delta);
+ nsresult GetChildHdrForKey(nsMsgKey desiredKey, nsIMsgDBHdr** result,
+ int32_t* resultIndex);
+ uint32_t NumRealChildren();
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr);
+ virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr);
+ virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr);
+
+ nsMsgKey m_threadKey;
+ uint32_t m_numUnreadChildren;
+ uint32_t m_flags;
+ nsMsgKey m_threadRootKey;
+ uint32_t m_newestMsgDate;
+ nsTArray<nsMsgKey> m_keys;
+ bool m_dummy; // top level msg is a dummy, e.g., grouped by age.
+ nsCOMPtr<nsIMsgDatabase> m_db; // should we make a weak ref or just a ptr?
+};
+
+class nsMsgXFGroupThread : public nsMsgGroupThread {
+ public:
+ nsMsgXFGroupThread();
+
+ NS_IMETHOD GetNumChildren(uint32_t* aNumChildren) override;
+ NS_IMETHOD GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) override;
+ NS_IMETHOD GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) override;
+ NS_IMETHOD RemoveChildAt(uint32_t aIndex) override;
+
+ protected:
+ virtual ~nsMsgXFGroupThread();
+
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override;
+ virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override;
+ virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr) override;
+ virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) override;
+ virtual nsMsgViewIndex GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) override;
+
+ nsCOMArray<nsIMsgFolder> m_folders;
+};
diff --git a/comm/mailnews/base/src/nsMsgGroupView.cpp b/comm/mailnews/base/src/nsMsgGroupView.cpp
new file mode 100644
index 0000000000..ca42d01a5a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupView.cpp
@@ -0,0 +1,941 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgUtils.h"
+#include "nsMsgGroupView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgSearchSession.h"
+#include "nsMsgGroupThread.h"
+#include "nsTreeColumns.h"
+#include "nsMsgMessageFlags.h"
+#include <plhash.h>
+#include "mozilla/Attributes.h"
+
+// Allocate this more to avoid reallocation on new mail.
+#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25
+// Max msghdr cache entries.
+#define MSGHDR_CACHE_MAX_SIZE 8192
+#define MSGHDR_CACHE_DEFAULT_SIZE 100
+
+nsMsgGroupView::nsMsgGroupView() { m_dayChanged = false; }
+
+nsMsgGroupView::~nsMsgGroupView() {}
+
+NS_IMETHODIMP
+nsMsgGroupView::Open(nsIMsgFolder* aFolder, nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) {
+ nsresult rv =
+ nsMsgDBView::Open(aFolder, aSortType, aSortOrder, aViewFlags, aCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ PersistFolderInfo(getter_AddRefs(dbFolderInfo));
+
+ nsCOMPtr<nsIMsgEnumerator> headers;
+ rv = m_db->EnumerateMessages(getter_AddRefs(headers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return OpenWithHdrs(headers, aSortType, aSortOrder, aViewFlags, aCount);
+}
+
+void nsMsgGroupView::InternalClose() {
+ m_groupsTable.Clear();
+ // Nothing to do if we're not grouped.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) return;
+
+ bool rcvDate = false;
+
+ if (m_sortType == nsMsgViewSortType::byReceived) rcvDate = true;
+
+ if (m_db && ((m_sortType == nsMsgViewSortType::byDate) ||
+ (m_sortType == nsMsgViewSortType::byReceived))) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ uint32_t expandFlags = 0;
+ uint32_t num = GetSize();
+
+ for (uint32_t i = 0; i < num; i++) {
+ if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD &&
+ !(m_flags[i] & nsMsgMessageFlags::Elided)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t ageBucket;
+ nsresult rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ if (NS_SUCCEEDED(rv)) expandFlags |= 1 << ageBucket;
+ }
+ }
+ }
+ dbFolderInfo->SetUint32Property("dateGroupFlags", expandFlags);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::Close() {
+ InternalClose();
+ return nsMsgDBView::Close();
+}
+
+// Set rcvDate to true to get the Received: date instead of the Date: date.
+nsresult nsMsgGroupView::GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr,
+ uint32_t* aAgeBucket, bool rcvDate) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aAgeBucket);
+
+ PRTime dateOfMsg;
+ nsresult rv;
+ if (!rcvDate)
+ rv = aMsgHdr->GetDate(&dateOfMsg);
+ else {
+ uint32_t rcvDateSecs;
+ rv = aMsgHdr->GetUint32Property("dateReceived", &rcvDateSecs);
+ Seconds2PRTime(rcvDateSecs, &dateOfMsg);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PRTime currentTime = PR_Now();
+ PRExplodedTime currentExplodedTime;
+ PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &currentExplodedTime);
+ PRExplodedTime explodedMsgTime;
+ PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime);
+
+ if (m_lastCurExplodedTime.tm_mday &&
+ m_lastCurExplodedTime.tm_mday != currentExplodedTime.tm_mday)
+ // This will cause us to rebuild the view.
+ m_dayChanged = true;
+
+ m_lastCurExplodedTime = currentExplodedTime;
+ if (currentExplodedTime.tm_year == explodedMsgTime.tm_year &&
+ currentExplodedTime.tm_month == explodedMsgTime.tm_month &&
+ currentExplodedTime.tm_mday == explodedMsgTime.tm_mday) {
+ // Same day.
+ *aAgeBucket = 1;
+ }
+ // Figure out how many days ago this msg arrived.
+ else if (currentTime > dateOfMsg) {
+ // Setting the time variables to local time.
+ int64_t GMTLocalTimeShift = currentExplodedTime.tm_params.tp_gmt_offset +
+ currentExplodedTime.tm_params.tp_dst_offset;
+ GMTLocalTimeShift *= PR_USEC_PER_SEC;
+ currentTime += GMTLocalTimeShift;
+ dateOfMsg += GMTLocalTimeShift;
+
+ // The most recent midnight, counting from current time.
+ int64_t mostRecentMidnight = currentTime - currentTime % PR_USEC_PER_DAY;
+ int64_t yesterday = mostRecentMidnight - PR_USEC_PER_DAY;
+ // Most recent midnight minus 6 days.
+ int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6);
+
+ // Was the message sent yesterday?
+ if (dateOfMsg >= yesterday)
+ *aAgeBucket = 2;
+ else if (dateOfMsg >= mostRecentWeek)
+ *aAgeBucket = 3;
+ else {
+ int64_t lastTwoWeeks = mostRecentMidnight - PR_USEC_PER_DAY * 13;
+ *aAgeBucket = (dateOfMsg >= lastTwoWeeks) ? 4 : 5;
+ }
+ } else {
+ // All that remains is a future date.
+ *aAgeBucket = 6;
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgGroupView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) {
+ nsCString cStringKey;
+ aHashKey.Truncate();
+ nsresult rv = NS_OK;
+ bool rcvDate = false;
+
+ switch (m_sortType) {
+ case nsMsgViewSortType::bySubject:
+ (void)msgHdr->GetSubject(cStringKey);
+ CopyASCIItoUTF16(cStringKey, aHashKey);
+ break;
+ case nsMsgViewSortType::byAuthor:
+ rv = nsMsgDBView::FetchAuthor(msgHdr, aHashKey);
+ break;
+ case nsMsgViewSortType::byRecipient:
+ (void)msgHdr->GetRecipients(getter_Copies(cStringKey));
+ CopyASCIItoUTF16(cStringKey, aHashKey);
+ break;
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags: {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ if (!dbToUse)
+ // Probably a search view.
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+
+ rv = (m_sortType == nsMsgViewSortType::byAccount)
+ ? FetchAccount(msgHdr, aHashKey)
+ : FetchTags(msgHdr, aHashKey);
+ } break;
+ case nsMsgViewSortType::byAttachments: {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ aHashKey.Assign(flags & nsMsgMessageFlags::Attachment ? '1' : '0');
+ break;
+ }
+ case nsMsgViewSortType::byFlagged: {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ aHashKey.Assign(flags & nsMsgMessageFlags::Marked ? '1' : '0');
+ break;
+ }
+ case nsMsgViewSortType::byPriority: {
+ nsMsgPriorityValue priority;
+ msgHdr->GetPriority(&priority);
+ aHashKey.AppendInt(priority);
+ } break;
+ case nsMsgViewSortType::byStatus: {
+ uint32_t status = 0;
+ GetStatusSortValue(msgHdr, &status);
+ aHashKey.AppendInt(status);
+ } break;
+ case nsMsgViewSortType::byReceived:
+ rcvDate = true;
+ [[fallthrough]];
+ case nsMsgViewSortType::byDate: {
+ uint32_t ageBucket;
+ rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ if (NS_SUCCEEDED(rv)) aHashKey.AppendInt(ageBucket);
+
+ break;
+ }
+ case nsMsgViewSortType::byCustom: {
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (colHandler) {
+ bool isString;
+ colHandler->IsString(&isString);
+ if (isString) {
+ rv = colHandler->GetSortStringForRow(msgHdr, aHashKey);
+ } else {
+ uint32_t intKey;
+ rv = colHandler->GetSortLongForRow(msgHdr, &intKey);
+ aHashKey.AppendInt(intKey);
+ }
+ }
+ break;
+ }
+ case nsMsgViewSortType::byCorrespondent:
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, aHashKey);
+ else
+ rv = FetchAuthor(msgHdr, aHashKey);
+
+ break;
+ default:
+ NS_ASSERTION(false, "no hash key for this type");
+ rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+nsMsgGroupThread* nsMsgGroupView::CreateGroupThread(nsIMsgDatabase* db) {
+ return new nsMsgGroupThread(db);
+}
+
+nsMsgGroupThread* nsMsgGroupView::AddHdrToThread(nsIMsgDBHdr* msgHdr,
+ bool* pNewThread) {
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ nsString hashKey;
+ nsresult rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return nullptr;
+
+ // if (m_sortType == nsMsgViewSortType::byDate)
+ // msgKey = ((nsPRUint32Key *)hashKey)->GetValue();
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ bool newThread = !msgThread;
+ *pNewThread = newThread;
+ // Index of first message in thread in view.
+ nsMsgViewIndex viewIndexOfThread;
+ // Index of newly added header in thread.
+ nsMsgViewIndex threadInsertIndex;
+
+ nsMsgGroupThread* foundThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (foundThread) {
+ // Find the view index of the root node of the thread in the view.
+ viewIndexOfThread = GetIndexOfFirstDisplayedKeyInThread(foundThread, true);
+ if (viewIndexOfThread == nsMsgViewIndex_None) {
+ // Something is wrong with the group table. Remove the old group and
+ // insert a new one.
+ m_groupsTable.Remove(hashKey);
+ foundThread = nullptr;
+ *pNewThread = newThread = true;
+ }
+ }
+
+ // If the thread does not already exist, create one
+ if (!foundThread) {
+ foundThread = CreateGroupThread(m_db);
+ msgThread = foundThread;
+ m_groupsTable.InsertOrUpdate(hashKey, msgThread);
+ if (GroupViewUsesDummyRow()) {
+ foundThread->m_dummy = true;
+ msgFlags |= MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_HASCHILDREN;
+ }
+
+ viewIndexOfThread = GetInsertIndex(msgHdr);
+ if (viewIndexOfThread == nsMsgViewIndex_None)
+ viewIndexOfThread = m_keys.Length();
+
+ // Add the thread root node to the view.
+ InsertMsgHdrAt(
+ viewIndexOfThread, msgHdr, msgKey,
+ msgFlags | MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided, 0);
+
+ // For dummy rows, Have the header serve as the dummy node (it will be
+ // added again for its actual content later).
+ if (GroupViewUsesDummyRow()) foundThread->InsertMsgHdrAt(0, msgHdr);
+
+ // Calculate the (integer thread key); this really only needs to be done for
+ // the byDate case where the expanded state of the groups can be easily
+ // persisted and restored because of the bounded, consecutive value space
+ // occupied. We calculate an integer value in all cases mainly because
+ // it's the sanest choice available...
+ // (The thread key needs to be an integer, so parse hash keys that are
+ // stringified integers to real integers, and hash actual strings into
+ // integers.)
+ if ((m_sortType == nsMsgViewSortType::byAttachments) ||
+ (m_sortType == nsMsgViewSortType::byFlagged) ||
+ (m_sortType == nsMsgViewSortType::byPriority) ||
+ (m_sortType == nsMsgViewSortType::byStatus) ||
+ (m_sortType == nsMsgViewSortType::byReceived) ||
+ (m_sortType == nsMsgViewSortType::byDate))
+ foundThread->m_threadKey =
+ atoi(NS_LossyConvertUTF16toASCII(hashKey).get());
+ else
+ foundThread->m_threadKey =
+ (nsMsgKey)PL_HashString(NS_LossyConvertUTF16toASCII(hashKey).get());
+ }
+
+ // Add the message to the thread as an actual content-bearing header.
+ // (If we use dummy rows, it was already added to the thread during creation.)
+ threadInsertIndex = foundThread->AddChildFromGroupView(msgHdr, this);
+ // Check if new hdr became thread root.
+ if (!newThread && threadInsertIndex == 0) {
+ // Update the root node's header (in the view) to be the same as the root
+ // node in the thread.
+ SetMsgHdrAt(msgHdr, viewIndexOfThread, msgKey,
+ (msgFlags & ~(nsMsgMessageFlags::Elided)) |
+ // Maintain elided flag and dummy flag.
+ (m_flags[viewIndexOfThread] &
+ (nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_DUMMY)) |
+ // Ensure thread and has-children flags are set.
+ MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN,
+ 0);
+ // Update the content-bearing copy in the thread to match. (the root and
+ // first nodes in the thread should always be the same header.)
+ // Note: the guy who used to be the root will still exist. If our list of
+ // nodes was [A A], a new node B is introduced which sorts to be the first
+ // node, giving us [B A A], our copy makes that [B B A], and things are
+ // right in the world (since we want the first two headers to be the same
+ // since one is our dummy and one is real.)
+ if (GroupViewUsesDummyRow()) {
+ // Replace the old duplicate dummy header.
+ // We do not update the content-bearing copy in the view to match; we
+ // leave that up to OnNewHeader, which is the piece of code who gets to
+ // care about whether the thread's children are shown or not (elided).
+ foundThread->SetMsgHdrAt(1, msgHdr);
+ }
+ }
+
+ return foundThread;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ nsresult rv = NS_OK;
+
+ m_groupsTable.Clear();
+ if (aSortType == nsMsgViewSortType::byThread ||
+ aSortType == nsMsgViewSortType::byId ||
+ aSortType == nsMsgViewSortType::byNone ||
+ aSortType == nsMsgViewSortType::bySize)
+ return NS_ERROR_INVALID_ARG;
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags | nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort;
+ SaveSortInfo(m_sortType, m_sortOrder);
+
+ if (m_sortType == nsMsgViewSortType::byCustom) {
+ // If the desired sort is a custom column and there is no handler found,
+ // it hasn't been registered yet; after the custom column observer is
+ // notified with MsgCreateDBView and registers the handler, it will come
+ // back and build the view.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (!colHandler) return rv;
+ }
+
+ bool hasMore;
+ nsCOMPtr<nsISupports> supports;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ while (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ bool notUsed;
+ AddHdrToThread(msgHdr, &notUsed);
+ }
+ }
+ uint32_t expandFlags = 0;
+ bool expandAll = m_viewFlags & nsMsgViewFlagsType::kExpandAll;
+ uint32_t viewFlag =
+ (m_sortType == nsMsgViewSortType::byDate) ? MSG_VIEW_FLAG_DUMMY : 0;
+ if (viewFlag && m_db) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (dbFolderInfo)
+ dbFolderInfo->GetUint32Property("dateGroupFlags", 0, &expandFlags);
+ }
+ // Go through the view updating the flags for threads with more than one
+ // message, and if grouped by date, expanding threads that were expanded
+ // before.
+ for (uint32_t viewIndex = 0; viewIndex < m_keys.Length(); viewIndex++) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingIndex(viewIndex, getter_AddRefs(thread));
+ if (thread) {
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ if (numChildren > 1 || viewFlag)
+ OrExtraFlag(viewIndex, viewFlag | MSG_VIEW_FLAG_HASCHILDREN);
+ if (expandAll || expandFlags) {
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread);
+ if (expandAll || expandFlags & (1 << groupThread->m_threadKey)) {
+ uint32_t numExpanded;
+ ExpandByIndex(viewIndex, &numExpanded);
+ viewIndex += numExpanded;
+ }
+ }
+ }
+ }
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+// We wouldn't need this if we never instantiated this directly,
+// but instead used nsMsgThreadedDBView with the grouping flag set.
+// Or, we could get rid of the nsMsgThreadedDBView impl of this method.
+NS_IMETHODIMP
+nsMsgGroupView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowAllThreads;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgGroupView* newMsgDBView = (nsMsgGroupView*)aNewMsgDBView;
+
+ // If grouped, we need to clone the group thread hash table.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) {
+ for (auto iter = m_groupsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_groupsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ }
+ return NS_OK;
+}
+
+// E.g., if the day has changed, we need to close and re-open the view.
+// Or, if we're switching between grouping and threading in a cross-folder
+// saved search. In that case, we needed to build an enumerator based on the
+// old view type, and internally close the view based on its old type, but
+// rebuild the new view based on the new view type. So we pass the new
+// view flags to OpenWithHdrs.
+nsresult nsMsgGroupView::RebuildView(nsMsgViewFlagsTypeValue newFlags) {
+ nsCOMPtr<nsIMsgEnumerator> headers;
+ if (NS_SUCCEEDED(GetMessageEnumerator(getter_AddRefs(headers)))) {
+ int32_t count;
+ m_dayChanged = false;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ nsMsgKey curSelectedKey;
+ SaveAndClearSelection(&curSelectedKey, preservedSelection);
+ InternalClose();
+ int32_t oldSize = GetSize();
+ // This is important, because the tree will ask us for our row count,
+ // which gets determined from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // This needs to happen after we remove all the keys, since
+ // RowCountChanged() will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ SetSuppressChangeNotifications(true);
+ nsresult rv =
+ OpenWithHdrs(headers, m_sortType, m_sortOrder, newFlags, &count);
+ SetSuppressChangeNotifications(false);
+ if (mTree) mTree->RowCountChanged(0, GetSize());
+ if (mJSTree) mJSTree->RowCountChanged(0, GetSize());
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now, restore our desired selection.
+ AutoTArray<nsMsgKey, 1> keyArray;
+ keyArray.AppendElement(curSelectedKey);
+
+ return RestoreSelection(curSelectedKey, keyArray);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgGroupView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnNewHeader(newHdr, aParentKey, ensureListed);
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ bool newThread;
+ nsMsgGroupThread* thread = AddHdrToThread(newHdr, &newThread);
+ if (thread) {
+ // Find the view index of (the root node of) the thread.
+ nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(newHdr);
+ // May need to fix thread counts.
+ if (threadIndex != nsMsgViewIndex_None) {
+ if (newThread) {
+ // AddHdrToThread creates the header elided, so we need to un-elide it
+ // if we want it expanded.
+ if (m_viewFlags & nsMsgViewFlagsType::kExpandAll)
+ m_flags[threadIndex] &= ~nsMsgMessageFlags::Elided;
+ } else {
+ m_flags[threadIndex] |=
+ MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD;
+ }
+
+ int32_t numRowsToInvalidate = 1;
+ // If the thread is expanded (not elided), we should add the header to
+ // the view.
+ if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) {
+ uint32_t msgIndexInThread = thread->FindMsgHdr(newHdr);
+ bool insertedAtThreadRoot = !msgIndexInThread;
+ // Add any new display node and potentially fix-up changes in the root.
+ // (If this is a new thread and we are not using a dummy row, the only
+ // node to display is the root node which has already been added by
+ // AddHdrToThread. And since there is just the one, no change in root
+ // could have occurred, so we have nothing to do.)
+ if (!newThread || GroupViewUsesDummyRow()) {
+ // We never want to insert/update the root node, because
+ // AddHdrToThread has already done that for us (in all cases).
+ if (insertedAtThreadRoot) msgIndexInThread++;
+ // If this header is the new parent of the thread... AND
+ // If we are not using a dummy row, this means we need to append our
+ // old node as the first child of the new root.
+ // (If we are using a dummy row, the old node's "content" node already
+ // exists (at position threadIndex + 1) and we need to insert the
+ // "content" copy of the new root node there, pushing our old
+ // "content" node down.)
+ // Example mini-diagrams, wrapping the to-add thing with ()
+ // No dummy row; we had: [A], now we have [B], we want [B (A)].
+ // Dummy row; we had: [A A], now we have [B A], we want [B (B) A].
+ // (Coming into this we're adding 'B')
+ if (!newThread && insertedAtThreadRoot && !GroupViewUsesDummyRow()) {
+ // Grab a copy of the old root node ('A') from the thread so we can
+ // insert it. (offset msgIndexInThread=1 is the right thing; we are
+ // non-dummy.)
+ thread->GetChildHdrAt(msgIndexInThread, &newHdr);
+ }
+ // Nothing to do for dummy case, we're already inserting 'B'.
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ newHdr->GetMessageKey(&msgKey);
+ newHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(threadIndex + msgIndexInThread, newHdr, msgKey,
+ msgFlags, 1);
+ }
+ // The call to NoteChange() has to happen after we add the key
+ // as NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ // (msgIndexInThread states - new thread: 0, old thread at root: 1).
+ if (newThread && GroupViewUsesDummyRow())
+ NoteChange(threadIndex, 2, nsMsgViewNotificationCode::insertOrDelete);
+ else
+ NoteChange(threadIndex + msgIndexInThread, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+
+ numRowsToInvalidate = msgIndexInThread;
+ } else if (newThread) {
+ // We still need the addition notification for new threads when elided.
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ NoteChange(threadIndex, numRowsToInvalidate,
+ nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // If thread is expanded, we need to add hdr to view...
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ nsresult rv = GetThreadContainingMsgHdr(aHdrChanged, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & nsMsgMessageFlags::Read)
+ thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read);
+
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ nsCOMPtr<nsIMsgThread> thread;
+ nsMsgKey keyDeleted;
+ aHdrDeleted->GetMessageKey(&keyDeleted);
+
+ nsresult rv = GetThreadContainingMsgHdr(aHdrDeleted, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgViewIndex viewIndexOfThread =
+ GetIndexOfFirstDisplayedKeyInThread(thread, true); // Yes to dummy node.
+
+ thread->RemoveChildHdr(aHdrDeleted, nullptr);
+
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread);
+
+ bool rootDeleted = viewIndexOfThread != nsMsgKey_None &&
+ m_keys[viewIndexOfThread] == keyDeleted;
+ rv = nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, aInstigator);
+ if (groupThread->m_dummy) {
+ if (!groupThread->NumRealChildren()) {
+ // Get rid of dummy.
+ thread->RemoveChildAt(0);
+ if (viewIndexOfThread != nsMsgKey_None) {
+ RemoveByIndex(viewIndexOfThread);
+ if (m_deletingRows)
+ mIndicesToNoteChange.AppendElement(viewIndexOfThread);
+ }
+ } else if (rootDeleted) {
+ // Reflect new thread root into view.dummy row.
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ thread->GetChildHdrAt(0, getter_AddRefs(hdr));
+ if (hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ SetMsgHdrAt(hdr, viewIndexOfThread, msgKey, m_flags[viewIndexOfThread],
+ 0);
+ }
+ }
+ }
+ if (!groupThread->m_keys.Length()) {
+ nsString hashKey;
+ rv = HashHdr(aHdrDeleted, hashKey);
+ if (NS_SUCCEEDED(rv)) m_groupsTable.Remove(hashKey);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetRowProperties(int32_t aRow, nsAString& aProperties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) {
+ aProperties.AssignLiteral("dummy");
+ return NS_OK;
+ }
+
+ return nsMsgDBView::GetRowProperties(aRow, aProperties);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProperties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) {
+ aProperties.AssignLiteral("dummy read");
+
+ if (!(m_flags[aRow] & nsMsgMessageFlags::Elided)) return NS_OK;
+
+ // Set unread property if a collapsed group thread has unread.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString hashKey;
+ rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return NS_OK;
+
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (!groupThread) return NS_OK;
+
+ uint32_t numUnrMsg = 0;
+ groupThread->GetNumUnreadChildren(&numUnrMsg);
+ if (numUnrMsg > 0) aProperties.AppendLiteral(" hasUnread");
+
+ return NS_OK;
+ }
+
+ return nsMsgDBView::GetCellProperties(aRow, aCol, aProperties);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) ||
+ aColumnName.EqualsLiteral("unreadCol"))
+ return nsMsgDBView::CellTextForColumn(aRow, aColumnName, aValue);
+
+ // We only treat "subject" and "total" here.
+ bool isSubject;
+ if (!(isSubject = aColumnName.EqualsLiteral("subjectCol")) &&
+ !aColumnName.EqualsLiteral("totalCol"))
+ return NS_OK;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString hashKey;
+ rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return NS_OK;
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (isSubject) {
+ uint32_t flags;
+ bool rcvDate = false;
+ msgHdr->GetFlags(&flags);
+ aValue.Truncate();
+ switch (m_sortType) {
+ case nsMsgViewSortType::byReceived:
+ rcvDate = true;
+ [[fallthrough]];
+ case nsMsgViewSortType::byDate: {
+ uint32_t ageBucket = 0;
+ GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ switch (ageBucket) {
+ case 1:
+ aValue.Assign(nsMsgDBView::kTodayString);
+ break;
+ case 2:
+ aValue.Assign(nsMsgDBView::kYesterdayString);
+ break;
+ case 3:
+ aValue.Assign(nsMsgDBView::kLastWeekString);
+ break;
+ case 4:
+ aValue.Assign(nsMsgDBView::kTwoWeeksAgoString);
+ break;
+ case 5:
+ aValue.Assign(nsMsgDBView::kOldMailString);
+ break;
+ default:
+ // Future date, error/spoofed.
+ aValue.Assign(nsMsgDBView::kFutureDateString);
+ break;
+ }
+ break;
+ }
+ case nsMsgViewSortType::bySubject:
+ FetchSubject(msgHdr, m_flags[aRow], aValue);
+ break;
+ case nsMsgViewSortType::byAuthor:
+ FetchAuthor(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byStatus:
+ rv = FetchStatus(m_flags[aRow], aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"messagesWithNoStatus", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byTags:
+ rv = FetchTags(msgHdr, aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"untaggedMessages", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byPriority:
+ FetchPriority(msgHdr, aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"noPriority", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byAccount:
+ FetchAccount(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byRecipient:
+ FetchRecipients(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byAttachments:
+ GetString(flags & nsMsgMessageFlags::Attachment ? u"attachments"
+ : u"noAttachments",
+ aValue);
+ break;
+ case nsMsgViewSortType::byFlagged:
+ GetString(
+ flags & nsMsgMessageFlags::Marked ? u"groupFlagged" : u"notFlagged",
+ aValue);
+ break;
+ // byLocation is a special case; we don't want to have duplicate
+ // all this logic in nsMsgSearchDBView, and its hash key is what we
+ // want anyways, so just copy it across.
+ case nsMsgViewSortType::byLocation:
+ case nsMsgViewSortType::byCorrespondent:
+ aValue = hashKey;
+ break;
+ case nsMsgViewSortType::byCustom: {
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (colHandler) {
+ bool isString;
+ colHandler->IsString(&isString);
+ if (isString) {
+ rv = colHandler->GetSortStringForRow(msgHdr.get(), aValue);
+ } else {
+ uint32_t intKey;
+ rv = colHandler->GetSortLongForRow(msgHdr.get(), &intKey);
+ aValue.AppendInt(intKey);
+ }
+ }
+ if (aValue.IsEmpty()) aValue.Assign('*');
+ break;
+ }
+
+ default:
+ NS_ASSERTION(false, "we don't sort by group for this type");
+ break;
+ }
+
+ if (groupThread) {
+ // Get number of messages in group.
+ nsAutoString formattedCountMsg;
+ uint32_t numMsg = groupThread->NumRealChildren();
+ formattedCountMsg.AppendInt(numMsg);
+
+ // Get number of unread messages.
+ nsAutoString formattedCountUnrMsg;
+ uint32_t numUnrMsg = 0;
+ groupThread->GetNumUnreadChildren(&numUnrMsg);
+ formattedCountUnrMsg.AppendInt(numUnrMsg);
+
+ // Add text to header.
+ aValue.AppendLiteral(u" (");
+ if (numUnrMsg) {
+ aValue.Append(formattedCountUnrMsg);
+ aValue.Append(u'/');
+ }
+
+ aValue.Append(formattedCountMsg);
+ aValue.Append(u')');
+ }
+ } else {
+ nsAutoString formattedCountString;
+ uint32_t numChildren = (groupThread) ? groupThread->NumRealChildren() : 0;
+ formattedCountString.AppendInt(numChildren);
+ aValue.Assign(formattedCountString);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::GetThreadContainingMsgHdr(msgHdr, pThread);
+
+ nsString hashKey;
+ nsresult rv = HashHdr(msgHdr, hashKey);
+ *pThread = nullptr;
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgThread> thread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(thread));
+ thread.forget(pThread);
+ }
+
+ return (*pThread) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+int32_t nsMsgGroupView::FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::FindLevelInThread(msgHdr, startOfThread, viewIndex);
+
+ return (startOfThread == viewIndex) ? 0 : 1;
+}
+
+bool nsMsgGroupView::GroupViewUsesDummyRow() {
+ // Return true to always use a header row as root grouped parent row.
+ return true;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) {
+ nsMsgDBView::AddColumnHandler(column, handler);
+
+ // If the sortType is byCustom and the desired custom column is the one just
+ // registered, build the view.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&
+ m_sortType == nsMsgViewSortType::byCustom) {
+ nsAutoString curCustomColumn;
+ GetCurCustomColumn(curCustomColumn);
+ if (curCustomColumn == column) RebuildView(m_viewFlags);
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgGroupView.h b/comm/mailnews/base/src/nsMsgGroupView.h
new file mode 100644
index 0000000000..3b646f159b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupView.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgGroupView_H_
+#define _nsMsgGroupView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBView.h"
+#include "nsInterfaceHashtable.h"
+
+class nsIMsgThread;
+class nsMsgGroupThread;
+
+// Please note that if you override a method of nsMsgDBView,
+// you will most likely want to check the m_viewFlags to see if
+// we're grouping, and if not, call the base class implementation.
+class nsMsgGroupView : public nsMsgDBView {
+ public:
+ nsMsgGroupView();
+ virtual ~nsMsgGroupView();
+
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater);
+ NS_IMETHOD Close() override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+
+ NS_IMETHOD GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProperties) override;
+ NS_IMETHOD GetRowProperties(int32_t aRow, nsAString& aProperties) override;
+ NS_IMETHOD CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) override;
+ NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) override;
+ NS_IMETHOD AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) override;
+
+ protected:
+ virtual void InternalClose();
+ nsMsgGroupThread* AddHdrToThread(nsIMsgDBHdr* msgHdr, bool* pNewThread);
+ virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey);
+ // Helper function to get age bucket for a hdr, useful when grouped by date.
+ nsresult GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr, uint32_t* aAgeBucket,
+ bool rcvDate = false);
+ nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool /*ensureListed*/) override;
+ virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) override;
+
+ // Returns true if we are grouped by a sort attribute that uses a dummy row.
+ bool GroupViewUsesDummyRow();
+ nsresult RebuildView(nsMsgViewFlagsTypeValue viewFlags);
+ virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db);
+
+ nsInterfaceHashtable<nsStringHashKey, nsIMsgThread> m_groupsTable;
+ PRExplodedTime m_lastCurExplodedTime{0};
+ bool m_dayChanged;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgI18N.cpp b/comm/mailnews/base/src/nsMsgI18N.cpp
new file mode 100644
index 0000000000..1c81456403
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgI18N.cpp
@@ -0,0 +1,403 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsICharsetConverterManager.h"
+#include "mozilla/Utf8.h"
+#include "nsIServiceManager.h"
+
+#include "nsISupports.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIMimeConverter.h"
+#include "nsMsgUtils.h"
+#include "nsMsgI18N.h"
+#include "nsILineInputStream.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsUTF8Utils.h"
+#include "nsNetUtil.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIFileStreams.h"
+#include "../../intl/nsUTF7ToUnicode.h"
+#include "../../intl/nsMUTF7ToUnicode.h"
+#include "../../intl/nsUnicodeToMUTF7.h"
+
+#include <stdlib.h>
+#include <tuple>
+
+//
+// International functions necessary for composition
+//
+
+nsresult nsMsgI18NConvertFromUnicode(const nsACString& aCharset,
+ const nsAString& inString,
+ nsACString& outString,
+ bool aReportUencNoMapping) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+
+ auto encoding = mozilla::Encoding::ForLabelNoReplacement(aCharset);
+ if (!encoding) {
+ return NS_ERROR_UCONV_NOCONV;
+ } else if (encoding == UTF_16LE_ENCODING || encoding == UTF_16BE_ENCODING) {
+ // We shouldn't ever ship anything in these encodings.
+ return NS_ERROR_UCONV_NOCONV;
+ }
+
+ nsresult rv;
+ std::tie(rv, std::ignore) = encoding->Encode(inString, outString);
+
+ if (rv == NS_OK_HAD_REPLACEMENTS) {
+ rv = aReportUencNoMapping ? NS_ERROR_UENC_NOMAPPING : NS_OK;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset,
+ const nsACString& inString,
+ nsAString& outString) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ if (aCharset.IsEmpty()) {
+ // Despite its name, it also works for Latin-1.
+ CopyASCIItoUTF16(inString, outString);
+ return NS_OK;
+ }
+
+ if (aCharset.Equals("UTF-8", nsCaseInsensitiveCStringComparator)) {
+ return UTF_8_ENCODING->DecodeWithBOMRemoval(inString, outString);
+ }
+
+ // Look up Thunderbird's special aliases from charsetalias.properties.
+ nsresult rv;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString newCharset;
+ rv = ccm->GetCharsetAlias(PromiseFlatCString(aCharset).get(), newCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (newCharset.Equals("UTF-7", nsCaseInsensitiveCStringComparator)) {
+ // Special treatment for decoding UTF-7 since it's not handled by
+ // encoding_rs.
+ return CopyUTF7toUTF16(inString, outString);
+ }
+
+ auto encoding = mozilla::Encoding::ForLabelNoReplacement(newCharset);
+ if (!encoding) return NS_ERROR_UCONV_NOCONV;
+ return encoding->DecodeWithoutBOMHandling(inString, outString);
+}
+
+// This is used to decode UTF-7. No support for encoding in UTF-7.
+nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) {
+ // UTF-7 encoding size cannot be larger than the size in UTF-16.
+ nsUTF7ToUnicode converter;
+ int32_t inLen = aSrc.Length();
+ int32_t outLen = inLen;
+ aDest.SetLength(outLen);
+ converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(),
+ &outLen);
+ MOZ_ASSERT(inLen == (int32_t)aSrc.Length(),
+ "UTF-7 should not produce a longer output");
+ aDest.SetLength(outLen);
+ return NS_OK;
+}
+
+nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest) {
+#define IMAP_UTF7_BUF_LENGTH 100
+ nsUnicodeToMUTF7 converter;
+ static char buffer[IMAP_UTF7_BUF_LENGTH];
+ const char16_t* in = aSrc.BeginReading();
+ int32_t inLen = aSrc.Length();
+ int32_t outLen;
+ aDest.Truncate();
+ while (inLen > 0) {
+ outLen = IMAP_UTF7_BUF_LENGTH;
+ int32_t remaining = inLen;
+ converter.ConvertNoBuffNoErr(in, &remaining, buffer, &outLen);
+ aDest.Append(buffer, outLen);
+ in += remaining;
+ inLen -= remaining;
+ }
+ outLen = IMAP_UTF7_BUF_LENGTH;
+ converter.FinishNoBuff(buffer, &outLen);
+ if (outLen > 0) aDest.Append(buffer, outLen);
+ return NS_OK;
+}
+
+// Hacky function to use for IMAP folders where the name can be in
+// MUTF-7 or UTF-8.
+nsresult CopyFolderNameToUTF16(const nsACString& aSrc, nsAString& aDest) {
+ if (NS_IsAscii(aSrc.BeginReading(), aSrc.Length())) {
+ // An ASCII string may not be valid MUTF-7. For example, it may contain an
+ // ampersand not immediately followed by a dash which is invalid MUTF-7.
+ // Check for validity by converting to UTF-16 and then back to MUTF-7 and
+ // the result should be unchanged. If the MUTF-7 is invalid, treat it as
+ // UTF-8.
+ if (NS_SUCCEEDED(CopyMUTF7toUTF16(aSrc, aDest))) {
+ nsAutoCString tmp;
+ CopyUTF16toMUTF7(aDest, tmp);
+ if (aSrc.Equals(tmp)) return NS_OK;
+ }
+ }
+ // Do if aSrc non-ASCII or if ASCII but invalid MUTF-7.
+ CopyUTF8toUTF16(aSrc, aDest);
+ return NS_OK;
+}
+
+nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) {
+ // MUTF-7 encoding size cannot be larger than the size in UTF-16.
+ nsMUTF7ToUnicode converter;
+ int32_t inLen = aSrc.Length();
+ int32_t outLen = inLen;
+ aDest.SetLength(outLen);
+ converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(),
+ &outLen);
+ MOZ_ASSERT(inLen == (int32_t)aSrc.Length(),
+ "MUTF-7 should not produce a longer output");
+ aDest.SetLength(outLen);
+ return NS_OK;
+}
+
+// MIME encoder, output string should be freed by PR_FREE
+// XXX : fix callers later to avoid allocation and copy
+char* nsMsgI18NEncodeMimePartIIStr(const char* header, bool structured,
+ const char* charset, int32_t fieldnamelen,
+ bool usemime) {
+ // No MIME, convert to the outgoing mail charset.
+ if (!usemime) {
+ nsAutoCString convertedStr;
+ if (NS_SUCCEEDED(nsMsgI18NConvertFromUnicode(
+ charset ? nsDependentCString(charset) : EmptyCString(),
+ NS_ConvertUTF8toUTF16(header), convertedStr)))
+ return PL_strdup(convertedStr.get());
+ else
+ return PL_strdup(header);
+ }
+
+ nsAutoCString encodedString;
+ nsresult res;
+ nsCOMPtr<nsIMimeConverter> converter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &res);
+ if (NS_SUCCEEDED(res) && nullptr != converter) {
+ res = converter->EncodeMimePartIIStr_UTF8(
+ nsDependentCString(header), structured, fieldnamelen,
+ nsIMimeConverter::MIME_ENCODED_WORD_SIZE, encodedString);
+ }
+
+ return NS_SUCCEEDED(res) ? PL_strdup(encodedString.get()) : nullptr;
+}
+
+// Return True if a charset is stateful (e.g. JIS).
+bool nsMsgI18Nstateful_charset(const char* charset) {
+ // TODO: use charset manager's service
+ return (PL_strcasecmp(charset, "ISO-2022-JP") == 0);
+}
+
+bool nsMsgI18Nmultibyte_charset(const char* charset) {
+ nsresult res;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &res);
+ bool result = false;
+
+ if (NS_SUCCEEDED(res)) {
+ nsAutoString charsetData;
+ res = ccm->GetCharsetData(charset, u".isMultibyte", charsetData);
+ if (NS_SUCCEEDED(res)) {
+ result = charsetData.LowerCaseEqualsLiteral("true");
+ }
+ }
+
+ return result;
+}
+
+bool nsMsgI18Ncheck_data_in_charset_range(const char* charset,
+ const char16_t* inString) {
+ if (!charset || !*charset || !inString || !*inString) return true;
+
+ bool res = true;
+
+ auto encoding =
+ mozilla::Encoding::ForLabelNoReplacement(nsDependentCString(charset));
+ if (!encoding) return false;
+ auto encoder = encoding->NewEncoder();
+
+ uint8_t buffer[512];
+ auto src = mozilla::MakeStringSpan(inString);
+ auto dst = mozilla::Span(buffer);
+ while (true) {
+ uint32_t result;
+ size_t read;
+ size_t written;
+ std::tie(result, read, written) =
+ encoder->EncodeFromUTF16WithoutReplacement(src, dst, false);
+ if (result == mozilla::kInputEmpty) {
+ // All converted successfully.
+ break;
+ } else if (result != mozilla::kOutputFull) {
+ // Didn't use all the input but the output isn't full, hence
+ // there was an unencodable character.
+ res = false;
+ break;
+ }
+ src = src.From(read);
+ // dst = dst.From(written); // Just overwrite output since we don't need it.
+ }
+
+ return res;
+}
+
+// Simple parser to parse META charset.
+// It only supports the case when the description is within one line.
+const char* nsMsgI18NParseMetaCharset(nsIFile* file) {
+ static char charset[nsIMimeConverter::MAX_CHARSET_NAME_LENGTH + 1];
+
+ *charset = '\0';
+
+ bool isDirectory = false;
+ file->IsDirectory(&isDirectory);
+ if (isDirectory) {
+ NS_ERROR("file is a directory");
+ return charset;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, charset);
+
+ rv = fileStream->Init(file, PR_RDONLY, 0664, false);
+ nsCOMPtr<nsILineInputStream> lineStream = do_QueryInterface(fileStream, &rv);
+
+ nsCString curLine;
+ bool more = true;
+ while (NS_SUCCEEDED(rv) && more) {
+ rv = lineStream->ReadLine(curLine, &more);
+ if (curLine.IsEmpty()) continue;
+
+ ToUpperCase(curLine);
+
+ if (curLine.Find("/HEAD") != -1) break;
+
+ if (curLine.Find("META") != -1 && curLine.Find("HTTP-EQUIV") != -1 &&
+ curLine.Find("CONTENT-TYPE") != -1 && curLine.Find("CHARSET") != -1) {
+ char* cp = (char*)PL_strchr(PL_strstr(curLine.get(), "CHARSET"), '=');
+ char* token = nullptr;
+ if (cp) {
+ char* newStr = cp + 1;
+ token = NS_strtok(" \"\'", &newStr);
+ }
+ if (token) {
+ PL_strncpy(charset, token, sizeof(charset));
+ charset[sizeof(charset) - 1] = '\0';
+
+ // this function cannot parse a file if it is really
+ // encoded by one of the following charsets
+ // so we can say that the charset label must be incorrect for
+ // the .html if we actually see those charsets parsed
+ // and we should ignore them
+ if (!PL_strncasecmp("UTF-16", charset, sizeof("UTF-16") - 1) ||
+ !PL_strncasecmp("UTF-32", charset, sizeof("UTF-32") - 1))
+ charset[0] = '\0';
+
+ break;
+ }
+ }
+ }
+
+ return charset;
+}
+
+nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString, uint32_t aMaxLength,
+ nsACString& outString) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ if (inString.Length() < aMaxLength) {
+ outString.Assign(inString);
+ return NS_OK;
+ }
+ NS_ASSERTION(mozilla::IsUtf8(inString), "Invalid UTF-8 string is inputted");
+ const char* start = inString.get();
+ const char* end = start + inString.Length();
+ const char* last = start + aMaxLength;
+ const char* cur = start;
+ const char* prev = nullptr;
+ bool err = false;
+ while (cur < last) {
+ prev = cur;
+ if (!UTF8CharEnumerator::NextChar(&cur, end, &err) || err) break;
+ }
+ if (!prev || err) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ uint32_t len = prev - start;
+ outString.Assign(Substring(inString, 0, len));
+ return NS_OK;
+}
+
+void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString,
+ const nsACString& charset,
+ nsAString& outString) {
+ if (mozilla::IsUtf8(inString)) {
+ CopyUTF8toUTF16(inString, outString);
+ return;
+ }
+
+ nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, outString);
+ if (NS_SUCCEEDED(rv)) return;
+
+ const char* cur = inString.BeginReading();
+ const char* end = inString.EndReading();
+ outString.Truncate();
+ while (cur < end) {
+ char c = *cur++;
+ if (c & char(0x80))
+ outString.Append(UCS2_REPLACEMENT_CHAR);
+ else
+ outString.Append(c);
+ }
+}
+
+void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString,
+ const nsACString& charset,
+ nsACString& outString) {
+ if (mozilla::IsUtf8(inString)) {
+ outString.Assign(inString);
+ return;
+ }
+
+ nsAutoString utf16Text;
+ nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, utf16Text);
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(utf16Text, outString);
+ return;
+ }
+
+ // EF BF BD (UTF-8 encoding of U+FFFD)
+ constexpr auto utf8ReplacementChar = "\357\277\275"_ns;
+ const char* cur = inString.BeginReading();
+ const char* end = inString.EndReading();
+ outString.Truncate();
+ while (cur < end) {
+ char c = *cur++;
+ if (c & char(0x80))
+ outString.Append(utf8ReplacementChar);
+ else
+ outString.Append(c);
+ }
+}
diff --git a/comm/mailnews/base/src/nsMsgI18N.h b/comm/mailnews/base/src/nsMsgI18N.h
new file mode 100644
index 0000000000..2268b64e26
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgI18N.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgI18N_H_
+#define _nsMsgI18N_H_
+
+#include "nscore.h"
+#include "msgCore.h"
+#include "nsString.h"
+class nsIFile;
+
+/**
+ * Encode an input string into RFC 2047 form.
+ *
+ * @param header [IN] A header to encode.
+ * @param structured [IN] Specify the header is structured or non-structured
+ * field (See RFC-822).
+ * @param charset [IN] Charset name to convert.
+ * @param fieldnamelen [IN] Header field name length. (e.g. "From: " -> 6)
+ * @param usemime [IN] If false then apply charset conversion only no MIME
+ * encoding.
+ * @return Encoded buffer (in C string) or NULL in case of error.
+ */
+NS_MSG_BASE char* nsMsgI18NEncodeMimePartIIStr(const char* header,
+ bool structured,
+ const char* charset,
+ int32_t fieldnamelen,
+ bool usemime);
+
+/**
+ * Check if given charset is stateful (e.g. ISO-2022-JP).
+ *
+ * @param charset [IN] Charset name.
+ * @return True if stateful
+ */
+NS_MSG_BASE bool nsMsgI18Nstateful_charset(const char* charset);
+
+/**
+ * Check if given charset is multibyte (e.g. Shift_JIS, Big5).
+ *
+ * @param charset [IN] Charset name.
+ * @return True if multibyte
+ */
+NS_MSG_BASE bool nsMsgI18Nmultibyte_charset(const char* charset);
+
+/**
+ * Check the input (unicode) string is in a range of the given charset after the
+ * conversion. Note, do not use this for large string (e.g. message body) since
+ * this actually applies the conversion to the buffer.
+ *
+ * @param charset [IN] Charset to be converted.
+ * @param inString [IN] Input unicode string to be examined.
+ * @return True if the string can be converted within the charset range.
+ * False if one or more characters cannot be converted to the
+ * target charset.
+ */
+NS_MSG_BASE bool nsMsgI18Ncheck_data_in_charset_range(const char* charset,
+ const char16_t* inString);
+/**
+ * Convert from unicode to target charset.
+ *
+ * @param charset [IN] Charset name.
+ * @param inString [IN] Unicode string to convert.
+ * @param outString [OUT] Converted output string.
+ * @param aReportUencNoMapping [IN] Set encoder to report (instead of using
+ * replacement char on errors). Set to true
+ * to receive NS_ERROR_UENC_NOMAPPING when
+ * that happens. Note that
+ * NS_ERROR_UENC_NOMAPPING is a success code!
+ * @return nsresult.
+ */
+NS_MSG_BASE nsresult nsMsgI18NConvertFromUnicode(
+ const nsACString& aCharset, const nsAString& inString,
+ nsACString& outString, bool reportUencNoMapping = false);
+/**
+ * Convert from charset to unicode.
+ *
+ * @param charset [IN] Charset name.
+ * @param inString [IN] Input string to convert.
+ * @param outString [OUT] Output unicode string.
+ * @return nsresult.
+ */
+NS_MSG_BASE nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset,
+ const nsACString& inString,
+ nsAString& outString);
+/**
+ * Parse for META charset.
+ *
+ * @param file [IN] A nsIFile.
+ * @return A charset name or empty string if not found.
+ */
+NS_MSG_BASE const char* nsMsgI18NParseMetaCharset(nsIFile* file);
+
+/**
+ * Shrink the aStr to aMaxLength bytes. Note that this doesn't check whether
+ * the aUTF8Str is valid UTF-8 string.
+ *
+ * @param inString [IN] Input UTF-8 string (it must be valid UTF-8 string)
+ * @param aMaxLength [IN] Shrink to this length (it means bytes)
+ * @param outString [OUT] Shrunken UTF-8 string
+ * @return nsresult
+ */
+NS_MSG_BASE nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString,
+ uint32_t aMaxLength,
+ nsACString& outString);
+
+/*
+ * Convert raw bytes in header to UTF-16
+ *
+ * @param inString [IN] Input raw octets
+ * @param outString [OUT] Output UTF-16 string
+ */
+NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString,
+ const nsACString& charset,
+ nsAString& outString);
+
+/*
+ * Convert raw bytes in header to UTF-8
+ *
+ * @param inString [IN] Input raw octets
+ * @param outString [OUT] Output UTF-8 string
+ */
+NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString,
+ const nsACString& charset,
+ nsACString& outString);
+
+// Decode UTF-7 to UTF-16. No encoding supported.
+NS_MSG_BASE nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest);
+
+// Convert between UTF-16 and modified UTF-7 used for IMAP.
+NS_MSG_BASE nsresult CopyFolderNameToUTF16(const nsACString& aSrc,
+ nsAString& aDest);
+NS_MSG_BASE nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest);
+NS_MSG_BASE nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest);
+
+#endif /* _nsMsgI18N_H_ */
diff --git a/comm/mailnews/base/src/nsMsgIdentity.cpp b/comm/mailnews/base/src/nsMsgIdentity.cpp
new file mode 100644
index 0000000000..a36107203d
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIdentity.cpp
@@ -0,0 +1,645 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMsgIdentity.h"
+#include "nsIPrefService.h"
+#include "nsString.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIMsgAccountManager.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsIMsgHeaderParser.h"
+#include "prprf.h"
+#include "nsISupportsPrimitives.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIUUIDGenerator.h"
+#include "mozilla/Components.h"
+
+#define REL_FILE_PREF_SUFFIX "-rel"
+
+NS_IMPL_ISUPPORTS(nsMsgIdentity, nsIMsgIdentity)
+
+/*
+ * accessors for pulling values directly out of preferences
+ * instead of member variables, etc
+ */
+
+NS_IMETHODIMP
+nsMsgIdentity::GetKey(nsACString& aKey) {
+ aKey = mKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetKey(const nsACString& identityKey) {
+ mKey = identityKey;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString branchName;
+ branchName.AssignLiteral("mail.identity.");
+ branchName += mKey;
+ branchName.Append('.');
+ rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = prefs->GetBranch("mail.identity.default.",
+ getter_AddRefs(mDefPrefBranch));
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetUID(nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return mPrefBranch->GetCharPref("uid", uid);
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(uid);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetUID(const nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return NS_ERROR_ABORT;
+ }
+ return SetCharAttribute("uid", uid);
+}
+
+nsresult nsMsgIdentity::GetIdentityName(nsAString& idName) {
+ idName.AssignLiteral("");
+ // Try to use "fullname <email>" as the name.
+ nsresult rv = GetFullAddress(idName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If a non-empty label exists, append it.
+ nsString label;
+ rv = GetLabel(label);
+ if (NS_SUCCEEDED(rv) &&
+ !label.IsEmpty()) { // TODO: this should be localizable
+ idName.AppendLiteral(" (");
+ idName.Append(label);
+ idName.Append(')');
+ }
+
+ if (!idName.IsEmpty()) return NS_OK;
+
+ // If we still found nothing to use, use our key.
+ return ToString(idName);
+}
+
+nsresult nsMsgIdentity::GetFullAddress(nsAString& fullAddress) {
+ nsAutoString fullName;
+ nsresult rv = GetFullName(fullName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString email;
+ rv = GetEmail(email);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fullName.IsEmpty() && email.IsEmpty()) {
+ fullAddress.Truncate();
+ } else {
+ nsCOMPtr<msgIAddressObject> mailbox;
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ mozilla::components::HeaderParser::Service());
+ NS_ENSURE_TRUE(headerParser, NS_ERROR_UNEXPECTED);
+ headerParser->MakeMailboxObject(fullName, NS_ConvertUTF8toUTF16(email),
+ getter_AddRefs(mailbox));
+ mailbox->ToString(fullAddress);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::ToString(nsAString& aResult) {
+ aResult.AssignLiteral("[nsIMsgIdentity: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(mKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+/* Identity attribute accessors */
+
+NS_IMETHODIMP
+nsMsgIdentity::GetSignature(nsIFile** sig) {
+ bool gotRelPref;
+ nsresult rv =
+ NS_GetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", nullptr,
+ gotRelPref, sig, mPrefBranch);
+ if (NS_SUCCEEDED(rv) && !gotRelPref) {
+ rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", *sig,
+ mPrefBranch);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to write signature file pref.");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetSignature(nsIFile* sig) {
+ nsresult rv = NS_OK;
+ if (sig)
+ rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", sig,
+ mPrefBranch);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::ClearAllValues() {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = mPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ mPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_IDPREF_STR(EscapedVCard, "escapedVCard")
+NS_IMPL_IDPREF_STR(SmtpServerKey, "smtpServer")
+NS_IMPL_IDPREF_WSTR(FullName, "fullName")
+NS_IMPL_IDPREF_STR(Email, "useremail")
+NS_IMPL_IDPREF_BOOL(CatchAll, "catchAll")
+NS_IMPL_IDPREF_STR(CatchAllHint, "catchAllHint")
+NS_IMPL_IDPREF_WSTR(Label, "label")
+NS_IMPL_IDPREF_STR(ReplyTo, "reply_to")
+NS_IMPL_IDPREF_WSTR(Organization, "organization")
+NS_IMPL_IDPREF_BOOL(ComposeHtml, "compose_html")
+NS_IMPL_IDPREF_BOOL(AttachVCard, "attach_vcard")
+NS_IMPL_IDPREF_BOOL(AttachSignature, "attach_signature")
+NS_IMPL_IDPREF_WSTR(HtmlSigText, "htmlSigText")
+NS_IMPL_IDPREF_BOOL(HtmlSigFormat, "htmlSigFormat")
+
+NS_IMPL_IDPREF_BOOL(AutoQuote, "auto_quote")
+NS_IMPL_IDPREF_INT(ReplyOnTop, "reply_on_top")
+NS_IMPL_IDPREF_BOOL(SigBottom, "sig_bottom")
+NS_IMPL_IDPREF_BOOL(SigOnForward, "sig_on_fwd")
+NS_IMPL_IDPREF_BOOL(SigOnReply, "sig_on_reply")
+
+NS_IMPL_IDPREF_INT(SignatureDate, "sig_date")
+
+NS_IMPL_IDPREF_BOOL(DoFcc, "fcc")
+
+NS_IMPL_FOLDERPREF_STR(FccFolder, "fcc_folder", "Sent"_ns,
+ nsMsgFolderFlags::SentMail)
+NS_IMPL_IDPREF_STR(FccFolderPickerMode, "fcc_folder_picker_mode")
+NS_IMPL_IDPREF_BOOL(FccReplyFollowsParent, "fcc_reply_follows_parent")
+NS_IMPL_IDPREF_STR(DraftsFolderPickerMode, "drafts_folder_picker_mode")
+NS_IMPL_IDPREF_STR(ArchivesFolderPickerMode, "archives_folder_picker_mode")
+NS_IMPL_IDPREF_STR(TmplFolderPickerMode, "tmpl_folder_picker_mode")
+
+NS_IMPL_IDPREF_BOOL(BccSelf, "bcc_self")
+NS_IMPL_IDPREF_BOOL(BccOthers, "bcc_other")
+NS_IMPL_IDPREF_STR(BccList, "bcc_other_list")
+
+NS_IMPL_IDPREF_BOOL(SuppressSigSep, "suppress_signature_separator")
+
+NS_IMPL_IDPREF_BOOL(DoCc, "doCc")
+NS_IMPL_IDPREF_STR(DoCcList, "doCcList")
+
+NS_IMPL_IDPREF_BOOL(AttachPgpKey, "attachPgpKey")
+NS_IMPL_IDPREF_BOOL(SendAutocryptHeaders, "sendAutocryptHeaders")
+NS_IMPL_IDPREF_BOOL(AutoEncryptDrafts, "autoEncryptDrafts")
+NS_IMPL_IDPREF_BOOL(ProtectSubject, "protectSubject")
+NS_IMPL_IDPREF_INT(EncryptionPolicy, "encryptionpolicy")
+NS_IMPL_IDPREF_BOOL(SignMail, "sign_mail")
+
+NS_IMETHODIMP
+nsMsgIdentity::GetDoBcc(bool* aValue) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetBoolPref("doBcc", aValue);
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ bool bccSelf = false;
+ GetBccSelf(&bccSelf);
+
+ bool bccOthers = false;
+ GetBccOthers(&bccOthers);
+
+ nsCString others;
+ GetBccList(others);
+
+ *aValue = bccSelf || (bccOthers && !others.IsEmpty());
+
+ return SetDoBcc(*aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetDoBcc(bool aValue) {
+ return SetBoolAttribute("doBcc", aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetDoBccList(nsACString& aValue) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString val;
+ nsresult rv = mPrefBranch->GetCharPref("doBccList", val);
+ aValue = val;
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ bool bccSelf = false;
+ rv = GetBccSelf(&bccSelf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bccSelf) GetEmail(aValue);
+
+ bool bccOthers = false;
+ rv = GetBccOthers(&bccOthers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString others;
+ rv = GetBccList(others);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bccOthers && !others.IsEmpty()) {
+ if (bccSelf) aValue.Append(',');
+ aValue.Append(others);
+ }
+
+ return SetDoBccList(aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetDoBccList(const nsACString& aValue) {
+ return SetCharAttribute("doBccList", aValue);
+}
+
+NS_IMPL_FOLDERPREF_STR(DraftFolder, "draft_folder", "Drafts"_ns,
+ nsMsgFolderFlags::Drafts)
+NS_IMPL_FOLDERPREF_STR(ArchiveFolder, "archive_folder", "Archives"_ns,
+ nsMsgFolderFlags::Archive)
+NS_IMPL_FOLDERPREF_STR(StationeryFolder, "stationery_folder", "Templates"_ns,
+ nsMsgFolderFlags::Templates)
+
+NS_IMPL_IDPREF_BOOL(ArchiveEnabled, "archive_enabled")
+NS_IMPL_IDPREF_INT(ArchiveGranularity, "archive_granularity")
+NS_IMPL_IDPREF_BOOL(ArchiveKeepFolderStructure, "archive_keep_folder_structure")
+
+NS_IMPL_IDPREF_BOOL(ShowSaveMsgDlg, "showSaveMsgDlg")
+NS_IMPL_IDPREF_STR(DirectoryServer, "directoryServer")
+NS_IMPL_IDPREF_BOOL(OverrideGlobalPref, "overrideGlobal_Pref")
+NS_IMPL_IDPREF_BOOL(AutocompleteToMyDomain, "autocompleteToMyDomain")
+
+NS_IMPL_IDPREF_BOOL(Valid, "valid")
+
+nsresult nsMsgIdentity::getFolderPref(const char* prefname, nsACString& retval,
+ const nsACString& folderName,
+ uint32_t folderflag) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval);
+ if (NS_SUCCEEDED(rv) && !retval.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(retval, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // Make sure that folder hierarchy is built so that legitimate parent-child
+ // relationship is established.
+ folder->GetServer(getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsCOMPtr<nsIMsgFolder> deferredToRootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ server->GetRootMsgFolder(getter_AddRefs(deferredToRootFolder));
+ // check if we're using a deferred account - if not, use the uri;
+ // otherwise, fall through to code that will fix this pref.
+ if (rootFolder == deferredToRootFolder) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = server->GetMsgFolderFromURI(folder, retval,
+ getter_AddRefs(msgFolder));
+ return NS_SUCCEEDED(rv) ? msgFolder->GetURI(retval) : rv;
+ }
+ }
+ }
+
+ // if the server doesn't exist, fall back to the default pref.
+ rv = mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval);
+ if (NS_SUCCEEDED(rv) && !retval.IsEmpty())
+ return setFolderPref(prefname, retval, folderflag);
+
+ // here I think we need to create a uri for the folder on the
+ // default server for this identity.
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> servers;
+ rv = accountManager->GetServersForIdentity(this, servers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (servers.IsEmpty()) {
+ // if there are no servers for this identity, return generic failure.
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIMsgIncomingServer> server(servers[0]);
+ bool defaultToServer;
+ server->GetDefaultCopiesAndFoldersPrefsToServer(&defaultToServer);
+ // if we should default to special folders on the server,
+ // use the local folders server
+ if (!defaultToServer) {
+ rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ // this will get the deferred to server's root folder, if "server"
+ // is deferred, e.g., using the pop3 global inbox.
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (rootFolder) {
+ rv = rootFolder->GetURI(retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ retval.Append('/');
+ retval.Append(folderName);
+ return setFolderPref(prefname, retval, folderflag);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgIdentity::setFolderPref(const char* prefname,
+ const nsACString& value,
+ uint32_t folderflag) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString oldpref;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ if (folderflag == nsMsgFolderFlags::SentMail) {
+ // Clear the temporary return receipt filter so that the new filter
+ // rule can be recreated (by ConfigureTemporaryFilters()).
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> servers;
+ rv = accountManager->GetServersForIdentity(this, servers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!servers.IsEmpty()) {
+ servers[0]->ClearTemporaryReturnReceiptsFilter();
+ // okay to fail; no need to check for return code
+ }
+ }
+
+ // get the old folder, and clear the special folder flag on it
+ rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, oldpref);
+ if (NS_SUCCEEDED(rv) && !oldpref.IsEmpty()) {
+ rv = GetOrCreateFolder(oldpref, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) {
+ rv = folder->ClearFlag(folderflag);
+ }
+ }
+
+ // set the new folder, and set the special folder flags on it
+ rv = SetUnicharAttribute(prefname, NS_ConvertUTF8toUTF16(value));
+ if (NS_SUCCEEDED(rv) && !value.IsEmpty()) {
+ rv = GetOrCreateFolder(value, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) rv = folder->SetFlag(folderflag);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetUnicharAttribute(const char* aName,
+ const nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (!val.IsEmpty())
+ return mPrefBranch->SetStringPref(aName, NS_ConvertUTF16toUTF8(val));
+
+ mPrefBranch->ClearUserPref(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetUnicharAttribute(const char* aName,
+ nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString valueUtf8;
+ if (NS_FAILED(
+ mPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8)))
+ mDefPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, val);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetCharAttribute(const char* aName,
+ const nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (!val.IsEmpty()) return mPrefBranch->SetCharPref(aName, val);
+
+ mPrefBranch->ClearUserPref(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetCharAttribute(const char* aName,
+ nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString tmpVal;
+ if (NS_FAILED(mPrefBranch->GetCharPref(aName, tmpVal)))
+ mDefPrefBranch->GetCharPref(aName, tmpVal);
+ val = tmpVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetBoolAttribute(const char* aName, bool val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ return mPrefBranch->SetBoolPref(aName, val);
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetBoolAttribute(const char* aName, bool* val) {
+ NS_ENSURE_ARG_POINTER(val);
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ *val = false;
+
+ if (NS_FAILED(mPrefBranch->GetBoolPref(aName, val)))
+ mDefPrefBranch->GetBoolPref(aName, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetIntAttribute(const char* aName, int32_t val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ return mPrefBranch->SetIntPref(aName, val);
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetIntAttribute(const char* aName, int32_t* val) {
+ NS_ENSURE_ARG_POINTER(val);
+
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ *val = 0;
+
+ if (NS_FAILED(mPrefBranch->GetIntPref(aName, val)))
+ mDefPrefBranch->GetIntPref(aName, val);
+
+ return NS_OK;
+}
+
+#define COPY_IDENTITY_FILE_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ nsCOMPtr<nsIFile> macro_spec; \
+ macro_rv = SRC_ID->MACRO_GETTER(getter_AddRefs(macro_spec)); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_spec); \
+ }
+
+#define COPY_IDENTITY_INT_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ int32_t macro_oldInt; \
+ macro_rv = SRC_ID->MACRO_GETTER(&macro_oldInt); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldInt); \
+ }
+
+#define COPY_IDENTITY_BOOL_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ bool macro_oldBool; \
+ macro_rv = SRC_ID->MACRO_GETTER(&macro_oldBool); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldBool); \
+ }
+
+#define COPY_IDENTITY_STR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsCString macro_oldStr; \
+ nsresult macro_rv; \
+ macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \
+ if (NS_SUCCEEDED(macro_rv)) { \
+ this->MACRO_SETTER(macro_oldStr); \
+ } \
+ }
+
+#define COPY_IDENTITY_WSTR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsString macro_oldStr; \
+ nsresult macro_rv; \
+ macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \
+ if (NS_SUCCEEDED(macro_rv)) { \
+ this->MACRO_SETTER(macro_oldStr); \
+ } \
+ }
+
+NS_IMETHODIMP
+nsMsgIdentity::Copy(nsIMsgIdentity* identity) {
+ NS_ENSURE_ARG_POINTER(identity);
+
+ COPY_IDENTITY_BOOL_VALUE(identity, GetComposeHtml, SetComposeHtml)
+ COPY_IDENTITY_STR_VALUE(identity, GetEmail, SetEmail)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetCatchAll, SetCatchAll)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetLabel, SetLabel)
+ COPY_IDENTITY_STR_VALUE(identity, GetReplyTo, SetReplyTo)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetFullName, SetFullName)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetOrganization, SetOrganization)
+ COPY_IDENTITY_STR_VALUE(identity, GetDraftFolder, SetDraftFolder)
+ COPY_IDENTITY_STR_VALUE(identity, GetArchiveFolder, SetArchiveFolder)
+ COPY_IDENTITY_STR_VALUE(identity, GetFccFolder, SetFccFolder)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetFccReplyFollowsParent,
+ SetFccReplyFollowsParent)
+ COPY_IDENTITY_STR_VALUE(identity, GetStationeryFolder, SetStationeryFolder)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveEnabled, SetArchiveEnabled)
+ COPY_IDENTITY_INT_VALUE(identity, GetArchiveGranularity,
+ SetArchiveGranularity)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveKeepFolderStructure,
+ SetArchiveKeepFolderStructure)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachSignature, SetAttachSignature)
+ COPY_IDENTITY_FILE_VALUE(identity, GetSignature, SetSignature)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetHtmlSigText, SetHtmlSigText)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetHtmlSigFormat, SetHtmlSigFormat)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAutoQuote, SetAutoQuote)
+ COPY_IDENTITY_INT_VALUE(identity, GetReplyOnTop, SetReplyOnTop)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigBottom, SetSigBottom)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnForward, SetSigOnForward)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnReply, SetSigOnReply)
+ COPY_IDENTITY_INT_VALUE(identity, GetSignatureDate, SetSignatureDate)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachVCard, SetAttachVCard)
+ COPY_IDENTITY_STR_VALUE(identity, GetEscapedVCard, SetEscapedVCard)
+ COPY_IDENTITY_STR_VALUE(identity, GetSmtpServerKey, SetSmtpServerKey)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSuppressSigSep, SetSuppressSigSep)
+
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachPgpKey, SetAttachPgpKey)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSendAutocryptHeaders,
+ SetSendAutocryptHeaders)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAutoEncryptDrafts, SetAutoEncryptDrafts)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetProtectSubject, SetProtectSubject)
+ COPY_IDENTITY_INT_VALUE(identity, GetEncryptionPolicy, SetEncryptionPolicy)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSignMail, SetSignMail)
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetRequestReturnReceipt(bool* aVal) {
+ NS_ENSURE_ARG_POINTER(aVal);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs)
+ return GetBoolAttribute("request_return_receipt_on", aVal);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetBoolPref("mail.receipt.request_return_receipt_on", aVal);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetReceiptHeaderType(int32_t* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs)
+ return GetIntAttribute("request_receipt_header_type", aType);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetIntPref("mail.receipt.request_header_type", aType);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetRequestDSN(bool* aVal) {
+ NS_ENSURE_ARG_POINTER(aVal);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("dsn_use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs) return GetBoolAttribute("dsn_always_request_on", aVal);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetBoolPref("mail.dsn.always_request_on", aVal);
+}
diff --git a/comm/mailnews/base/src/nsMsgIdentity.h b/comm/mailnews/base/src/nsMsgIdentity.h
new file mode 100644
index 0000000000..1b9e0f1635
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIdentity.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgIdentity_h___
+#define nsMsgIdentity_h___
+
+#include "nsIMsgIdentity.h"
+#include "nsIPrefBranch.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+class nsMsgIdentity final : public nsIMsgIdentity {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGIDENTITY
+
+ private:
+ ~nsMsgIdentity() {}
+ nsCString mKey;
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ nsCOMPtr<nsIPrefBranch> mDefPrefBranch;
+
+ protected:
+ nsresult getFolderPref(const char* pref, nsACString& retval,
+ const nsACString& folderName, uint32_t folderFlag);
+ nsresult setFolderPref(const char* pref, const nsACString& retval,
+ uint32_t folderFlag);
+};
+
+#define NS_IMPL_IDPREF_STR(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsACString& retval) { \
+ return GetCharAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsACString& value) { \
+ return SetCharAttribute(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_WSTR(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsAString& retval) { \
+ return GetUnicharAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsAString& value) { \
+ return SetUnicharAttribute(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_BOOL(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(bool* retval) { \
+ return GetBoolAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(bool value) { \
+ return mPrefBranch->SetBoolPref(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_INT(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(int32_t* retval) { \
+ return GetIntAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(int32_t value) { \
+ return mPrefBranch->SetIntPref(_prefname, value); \
+ }
+
+#define NS_IMPL_FOLDERPREF_STR(_postfix, _prefname, _foldername, _flag) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsACString& retval) { \
+ nsresult rv; \
+ nsCString folderPref; \
+ rv = getFolderPref(_prefname, folderPref, _foldername, _flag); \
+ retval = folderPref; \
+ return rv; \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsACString& value) { \
+ return setFolderPref(_prefname, value, _flag); \
+ }
+
+#endif /* nsMsgIdentity_h___ */
diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.cpp b/comm/mailnews/base/src/nsMsgIncomingServer.cpp
new file mode 100644
index 0000000000..0d914ffbd9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIncomingServer.cpp
@@ -0,0 +1,2142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgIncomingServer.h"
+#include "nscore.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "prprf.h"
+
+#include "nsIServiceManager.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsISupportsPrimitives.h"
+
+#include "nsIMsgBiffManager.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgDBFolder.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIPrefService.h"
+#include "nsIRelativeFilePref.h"
+#include "mozilla/nsRelativeFilePref.h"
+#include "nsIDocShell.h"
+#include "nsIAuthPrompt.h"
+#include "nsNetUtil.h"
+#include "nsIWindowWatcher.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgHdr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsILoginInfo.h"
+#include "nsILoginManager.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgMdnGenerator.h"
+#include "nsMsgUtils.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "nsIMsgFilter.h"
+#include "nsIObserverService.h"
+#include "mozilla/Unused.h"
+#include "nsIUUIDGenerator.h"
+#include "nsArrayUtils.h"
+
+#define PORT_NOT_SET -1
+
+nsMsgIncomingServer::nsMsgIncomingServer()
+ : m_rootFolder(nullptr),
+ m_downloadedHdrs(50),
+ m_numMsgsDownloaded(0),
+ m_biffState(nsIMsgFolder::nsMsgBiffState_Unknown),
+ m_serverBusy(false),
+ m_canHaveFilters(true),
+ m_displayStartupPage(true),
+ mPerformingBiff(false) {}
+
+nsresult nsMsgIncomingServer::Init() {
+ // We need to know when the password manager changes.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ observerService->AddObserver(this, "passwordmgr-storage-changed", false);
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ return NS_OK;
+}
+
+nsMsgIncomingServer::~nsMsgIncomingServer() {}
+
+NS_IMPL_ISUPPORTS(nsMsgIncomingServer, nsIMsgIncomingServer,
+ nsISupportsWeakReference, nsIObserver)
+
+/**
+ * Observe() receives notifications for all accounts, not just this server's
+ * account. So we ignore all notifications not intended for this server.
+ * When the state of the password manager changes we need to clear the
+ * this server's password from the cache in case the user just changed or
+ * removed the password or username.
+ * Oauth2 servers often automatically change the password manager's stored
+ * password (the token).
+ */
+NS_IMETHODIMP
+nsMsgIncomingServer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsresult rv;
+ if (strcmp(aTopic, "passwordmgr-storage-changed") == 0) {
+ nsAutoString otherFullName;
+ nsAutoString otherUserName;
+ // Check that the notification is for this server.
+ nsCOMPtr<nsILoginInfo> loginInfo = do_QueryInterface(aSubject);
+ if (loginInfo) {
+ // The login info for this server has been removed with aData being
+ // "removeLogin" or "removeAllLogins".
+ loginInfo->GetOrigin(otherFullName);
+ loginInfo->GetUsername(otherUserName);
+ } else {
+ // Probably a 2 element array containing old and new login info due to
+ // aData being "modifyLogin". E.g., a user has modified password or
+ // username in the password manager or an OAuth2 token string has
+ // automatically changed.
+ nsCOMPtr<nsIArray> logins = do_QueryInterface(aSubject);
+ if (logins) {
+ // Only need to look at names in first array element (login info before
+ // any modification) since the user might have changed the username as
+ // found in the 2nd elements. (The hostname can't be modified in the
+ // password manager.)
+ nsCOMPtr<nsILoginInfo> login;
+ logins->QueryElementAt(0, NS_GET_IID(nsILoginInfo),
+ getter_AddRefs(login));
+ if (login) {
+ login->GetOrigin(otherFullName);
+ login->GetUsername(otherUserName);
+ }
+ }
+ }
+ if (!otherFullName.IsEmpty()) {
+ nsAutoCString thisHostname;
+ nsAutoCString thisUsername;
+ GetHostName(thisHostname);
+ GetUsername(thisUsername);
+ nsAutoCString thisFullName;
+ GetType(thisFullName);
+ if (thisFullName.EqualsLiteral("pop3")) {
+ // Note: POP3 now handled by MsgIncomingServer.jsm so does not occur.
+ MOZ_ASSERT_UNREACHABLE("pop3 should not use nsMsgIncomingServer");
+ thisFullName = "mailbox://"_ns + thisHostname;
+ } else {
+ thisFullName += "://"_ns + thisHostname;
+ }
+ if (!thisFullName.Equals(NS_ConvertUTF16toUTF8(otherFullName)) ||
+ !thisUsername.Equals(NS_ConvertUTF16toUTF8(otherUserName))) {
+ // Not for this server; keep this server's cached password.
+ return NS_OK;
+ }
+ } else if (NS_strcmp(aData, u"hostSavingDisabled") != 0) {
+ // "hostSavingDisabled" only occurs during test_smtpServer.js and
+ // expects the password to be removed from memory cache. Otherwise, we
+ // don't have enough information to decide to remove the cached
+ // password, so keep it.
+ return NS_OK;
+ }
+ // When nsMsgImapIncomingServer::ForgetSessionPassword called with
+ // parameter modifyLogin true and if the server uses OAuth2, it causes the
+ // password to not be cleared from cache. This is needed by autosync. When
+ // the aData paremater of Observe() is not "modifyLogin" but is
+ // e.g., "removeLogin" or "removeAllLogins", ForgetSessionPassword(false)
+ // will still clear the cached password regardless of authentication method.
+ rv = ForgetSessionPassword(NS_strcmp(aData, u"modifyLogin") == 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ // Now remove ourselves from the observer service as well.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ observerService->RemoveObserver(this, "passwordmgr-storage-changed");
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetServerBusy(bool aServerBusy) {
+ m_serverBusy = aServerBusy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetServerBusy(bool* aServerBusy) {
+ NS_ENSURE_ARG_POINTER(aServerBusy);
+ *aServerBusy = m_serverBusy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetKey(nsACString& serverKey) {
+ serverKey = m_serverKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetKey(const nsACString& serverKey) {
+ m_serverKey.Assign(serverKey);
+
+ // in order to actually make use of the key, we need the prefs
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString branchName;
+ branchName.AssignLiteral("mail.server.");
+ branchName.Append(m_serverKey);
+ branchName.Append('.');
+ rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return prefs->GetBranch("mail.server.default.",
+ getter_AddRefs(mDefPrefBranch));
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUID(nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return GetCharValue("uid", uid);
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(uid);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUID(const nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return NS_ERROR_ABORT;
+ }
+ return SetCharValue("uid", uid);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetRootFolder(nsIMsgFolder* aRootFolder) {
+ m_rootFolder = aRootFolder;
+ return NS_OK;
+}
+
+// this will return the root folder of this account,
+// even if this server is deferred.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetRootFolder(nsIMsgFolder** aRootFolder) {
+ NS_ENSURE_ARG_POINTER(aRootFolder);
+ if (!m_rootFolder) {
+ nsresult rv = CreateRootFolder();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aRootFolder = m_rootFolder);
+ return NS_OK;
+}
+
+// this will return the root folder of the deferred to account,
+// if this server is deferred.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetRootMsgFolder(nsIMsgFolder** aRootMsgFolder) {
+ return GetRootFolder(aRootMsgFolder);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::PerformExpand(nsIMsgWindow* aMsgWindow) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgIncomingServer::VerifyLogon(nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) {
+ // This has to be implemented in the derived class, but in case someone
+ // doesn't implement it just return not implemented.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetNewMessages(nsIMsgFolder* aFolder,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ return aFolder->GetNewMessages(aMsgWindow, aUrlListener);
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetPerformingBiff(bool* aPerformingBiff) {
+ NS_ENSURE_ARG_POINTER(aPerformingBiff);
+ *aPerformingBiff = mPerformingBiff;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetPerformingBiff(bool aPerformingBiff) {
+ mPerformingBiff = aPerformingBiff;
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgIncomingServer, BiffState, uint32_t, m_biffState)
+
+NS_IMETHODIMP nsMsgIncomingServer::WriteToFolderCache(
+ nsIMsgFolderCache* folderCache) {
+ nsresult rv = NS_OK;
+ if (m_rootFolder) {
+ rv = m_rootFolder->WriteToFolderCache(folderCache, true /* deep */);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::Shutdown() {
+ nsresult rv = CloseCachedConnections();
+ mFilterPlugin = nullptr;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mFilterList) {
+ // close the filter log stream
+ rv = mFilterList->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFilterList = nullptr;
+ }
+
+ if (mSpamSettings) {
+ // close the spam log stream
+ rv = mSpamSettings->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamSettings = nullptr;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::CloseCachedConnections() {
+ // derived class should override if they cache connections.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDownloadMessagesAtStartup(bool* getMessagesAtStartup) {
+ // derived class should override if they need to do this.
+ *getMessagesAtStartup = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanHaveFilters(bool* canHaveFilters) {
+ NS_ENSURE_ARG_POINTER(canHaveFilters);
+ *canHaveFilters = m_canHaveFilters;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetCanHaveFilters(bool aCanHaveFilters) {
+ m_canHaveFilters = aCanHaveFilters;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanBeDefaultServer(bool* canBeDefaultServer) {
+ // derived class should override if they need to do this.
+ *canBeDefaultServer = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanSearchMessages(bool* canSearchMessages) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canSearchMessages);
+ *canSearchMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanCompactFoldersOnServer(
+ bool* canCompactFoldersOnServer) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canCompactFoldersOnServer);
+ *canCompactFoldersOnServer = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canUndoDeleteOnServer);
+ *canUndoDeleteOnServer = true;
+ return NS_OK;
+}
+
+// construct <localStoreType>://[<username>@]<hostname
+NS_IMETHODIMP
+nsMsgIncomingServer::GetServerURI(nsACString& aResult) {
+ nsresult rv;
+ rv = GetLocalStoreType(aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResult.AppendLiteral("://");
+
+ nsCString username;
+ rv = GetUsername(username);
+ if (NS_SUCCEEDED(rv) && !username.IsEmpty()) {
+ nsCString escapedUsername;
+ MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+ // not all servers have a username
+ aResult.Append(escapedUsername);
+ aResult.Append('@');
+ }
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) {
+ nsCString escapedHostname;
+ MsgEscapeString(hostname, nsINetUtil::ESCAPE_URL_PATH, escapedHostname);
+ // not all servers have a hostname
+ aResult.Append(escapedHostname);
+ }
+ return NS_OK;
+}
+
+// helper routine to create local folder on disk, if it doesn't exist.
+nsresult nsMsgIncomingServer::CreateLocalFolder(const nsAString& folderName) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> child;
+ rv = rootFolder->GetChildNamed(folderName, getter_AddRefs(child));
+ if (child) return NS_OK;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->CreateFolder(rootFolder, folderName, getter_AddRefs(child));
+}
+
+nsresult nsMsgIncomingServer::CreateRootFolder() {
+ nsresult rv;
+ // get the URI from the incoming server
+ nsCString serverUri;
+ rv = GetServerURI(serverUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetOrCreateFolder(serverUri, getter_AddRefs(m_rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetBoolValue(const char* prefname, bool* val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ NS_ENSURE_ARG_POINTER(val);
+ *val = false;
+
+ if (NS_FAILED(mPrefBranch->GetBoolPref(prefname, val)))
+ mDefPrefBranch->GetBoolPref(prefname, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetBoolValue(const char* prefname, bool val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ bool defaultValue;
+ nsresult rv = mDefPrefBranch->GetBoolPref(prefname, &defaultValue);
+
+ if (NS_SUCCEEDED(rv) && val == defaultValue)
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetBoolPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetIntValue(const char* prefname, int32_t* val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ NS_ENSURE_ARG_POINTER(val);
+ *val = 0;
+
+ if (NS_FAILED(mPrefBranch->GetIntPref(prefname, val)))
+ mDefPrefBranch->GetIntPref(prefname, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFileValue(const char* aRelPrefName,
+ const char* aAbsPrefName,
+ nsIFile** aLocalFile) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Get the relative first
+ nsCOMPtr<nsIRelativeFilePref> relFilePref;
+ nsresult rv = mPrefBranch->GetComplexValue(aRelPrefName,
+ NS_GET_IID(nsIRelativeFilePref),
+ getter_AddRefs(relFilePref));
+ if (relFilePref) {
+ rv = relFilePref->GetFile(aLocalFile);
+ NS_ASSERTION(*aLocalFile, "An nsIRelativeFilePref has no file.");
+ if (NS_SUCCEEDED(rv)) (*aLocalFile)->Normalize();
+ } else {
+ rv = mPrefBranch->GetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile),
+ reinterpret_cast<void**>(aLocalFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIRelativeFilePref> relFilePref =
+ new mozilla::nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(*aLocalFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ rv = mPrefBranch->SetComplexValue(
+ aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetFileValue(const char* aRelPrefName,
+ const char* aAbsPrefName,
+ nsIFile* aLocalFile) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Write the relative path.
+ nsCOMPtr<nsIRelativeFilePref> relFilePref = new mozilla::nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(aLocalFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ nsresult rv = mPrefBranch->SetComplexValue(
+ aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ if (NS_FAILED(rv)) return rv;
+
+ return mPrefBranch->SetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile),
+ aLocalFile);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetIntValue(const char* prefname, int32_t val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ int32_t defaultVal;
+ nsresult rv = mDefPrefBranch->GetIntPref(prefname, &defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal == val)
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetIntPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCharValue(const char* prefname, nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString tmpVal;
+ if (NS_FAILED(mPrefBranch->GetCharPref(prefname, tmpVal)))
+ mDefPrefBranch->GetCharPref(prefname, tmpVal);
+ val = tmpVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUnicharValue(const char* prefname, nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString valueUtf8;
+ if (NS_FAILED(
+ mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8)))
+ mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, val);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetCharValue(const char* prefname, const nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (val.IsEmpty()) {
+ mPrefBranch->ClearUserPref(prefname);
+ return NS_OK;
+ }
+
+ nsCString defaultVal;
+ nsresult rv = mDefPrefBranch->GetCharPref(prefname, defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal.Equals(val))
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetCharPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUnicharValue(const char* prefname,
+ const nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (val.IsEmpty()) {
+ mPrefBranch->ClearUserPref(prefname);
+ return NS_OK;
+ }
+
+ nsCString defaultVal;
+ nsresult rv =
+ mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal.Equals(NS_ConvertUTF16toUTF8(val)))
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetStringPref(prefname, NS_ConvertUTF16toUTF8(val));
+
+ return rv;
+}
+
+// pretty name is the display name to show to the user
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPrettyName(nsAString& retval) {
+ nsresult rv = GetUnicharValue("name", retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if there's no name, then just return the hostname
+ return retval.IsEmpty() ? GetConstructedPrettyName(retval) : rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetPrettyName(const nsAString& value) {
+ SetUnicharValue("name", value);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) rootFolder->SetPrettyName(value);
+ return NS_OK;
+}
+
+// construct the pretty name to show to the user if they haven't
+// specified one. This should be overridden for news and mail.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetConstructedPrettyName(nsAString& retval) {
+ nsCString username;
+ nsresult rv = GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!username.IsEmpty()) {
+ CopyASCIItoUTF16(username, retval);
+ retval.AppendLiteral(" on ");
+ }
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ retval.Append(NS_ConvertASCIItoUTF16(hostname));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ToString(nsAString& aResult) {
+ aResult.AssignLiteral("[nsIMsgIncomingServer: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(m_serverKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetPassword(const nsAString& aPassword) {
+ m_password = aPassword;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetPassword(nsAString& aPassword) {
+ aPassword = m_password;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) {
+ NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff);
+ *aServerRequiresPasswordForBiff = true;
+ return NS_OK;
+}
+
+// This sets m_password if we find a password in the pw mgr.
+nsresult nsMsgIncomingServer::GetPasswordWithoutUI() {
+ nsresult rv;
+ nsCOMPtr<nsILoginManager> loginMgr(
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current server URI
+ nsCString currServerUri;
+ rv = GetLocalStoreType(currServerUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.AppendLiteral("://");
+
+ nsCString temp;
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.Append(temp);
+
+ NS_ConvertUTF8toUTF16 currServer(currServerUri);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins);
+
+ // Login manager can produce valid fails, e.g. NS_ERROR_ABORT when a user
+ // cancels the master password dialog. Therefore handle that here, but don't
+ // warn about it.
+ if (NS_FAILED(rv)) return rv;
+ uint32_t numLogins = logins.Length();
+
+ // Don't abort here, if we didn't find any or failed, then we'll just have
+ // to prompt.
+ if (numLogins > 0) {
+ nsCString serverCUsername;
+ rv = GetUsername(serverCUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 serverUsername(serverCUsername);
+
+ nsString username;
+ for (uint32_t i = 0; i < numLogins; ++i) {
+ rv = logins[i]->GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (username.Equals(serverUsername)) {
+ nsString password;
+ rv = logins[i]->GetPassword(password);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_password = password;
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPasswordWithUI(const nsAString& aPromptMessage,
+ const nsAString& aPromptTitle,
+ nsAString& aPassword) {
+ nsresult rv = NS_OK;
+
+ if (m_password.IsEmpty()) {
+ // let's see if we have the password in the password manager and
+ // can avoid this prompting thing. This makes it easier to get embedders
+ // to get up and running w/o a password prompting UI.
+ rv = GetPasswordWithoutUI();
+ // If GetPasswordWithoutUI returns NS_ERROR_ABORT, the most likely case
+ // is the user canceled getting the master password, so just return
+ // straight away, as they won't want to get prompted again.
+ if (rv == NS_ERROR_ABORT) return NS_MSG_PASSWORD_PROMPT_CANCELLED;
+ }
+ if (m_password.IsEmpty()) {
+ nsCOMPtr<nsIAuthPrompt> authPrompt =
+ do_GetService("@mozilla.org/messenger/msgAuthPrompt;1");
+ if (authPrompt) {
+ // prompt the user for the password
+ nsCString serverUri;
+ rv = GetLocalStoreType(serverUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverUri.AppendLiteral("://");
+ nsCString temp;
+ rv = GetUsername(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!temp.IsEmpty()) {
+ nsCString escapedUsername;
+ MsgEscapeString(temp, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+ serverUri.Append(escapedUsername);
+ serverUri.Append('@');
+ }
+
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverUri.Append(temp);
+
+ // we pass in the previously used password, if any, into PromptPassword
+ // so that it will appear as ******. This means we can't use an nsString
+ // and getter_Copies.
+ char16_t* uniPassword = nullptr;
+ if (!aPassword.IsEmpty()) uniPassword = ToNewUnicode(aPassword);
+
+ bool okayValue = true;
+ rv = authPrompt->PromptPassword(PromiseFlatString(aPromptTitle).get(),
+ PromiseFlatString(aPromptMessage).get(),
+ NS_ConvertASCIItoUTF16(serverUri).get(),
+ nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY,
+ &uniPassword, &okayValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!okayValue) // if the user pressed cancel, just return an empty
+ // string;
+ {
+ aPassword.Truncate();
+ return NS_MSG_PASSWORD_PROMPT_CANCELLED;
+ }
+
+ // we got a password back...so remember it
+ rv = SetPassword(nsDependentString(uniPassword));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_FREEIF(uniPassword);
+ } // if we got a prompt dialog
+ else
+ return NS_ERROR_FAILURE;
+ } // if the password is empty
+ return GetPassword(aPassword);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ForgetPassword() {
+ nsresult rv;
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current server URI
+ nsCString currServerUri;
+ rv = GetLocalStoreType(currServerUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.AppendLiteral("://");
+
+ nsCString temp;
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.Append(temp);
+
+ NS_ConvertUTF8toUTF16 currServer(currServerUri);
+
+ nsCString serverCUsername;
+ rv = GetUsername(serverCUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 serverUsername(serverCUsername);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // There should only be one-login stored for this url, however just in case
+ // there isn't.
+ nsString username;
+ for (uint32_t i = 0; i < logins.Length(); ++i) {
+ rv = logins[i]->GetUsername(username);
+ int32_t atPos = serverUsername.FindChar('@');
+ if (NS_SUCCEEDED(rv) &&
+ (username.Equals(serverUsername) ||
+ StringHead(serverUsername, atPos).Equals(username))) {
+ // If this fails, just continue, we'll still want to remove the password
+ // from our local cache.
+ loginMgr->RemoveLogin(logins[i]);
+ }
+ }
+
+ return SetPassword(EmptyString());
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ForgetSessionPassword(bool modifyLogin) {
+ m_password.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDefaultLocalPath(nsIFile* aDefaultLocalPath) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return protocolInfo->SetDefaultLocalPath(aDefaultLocalPath);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalPath(nsIFile** aLocalPath) {
+ nsresult rv;
+
+ // if the local path has already been set, use it
+ rv = GetFileValue("directory-rel", "directory", aLocalPath);
+ if (NS_SUCCEEDED(rv) && *aLocalPath) return rv;
+
+ // otherwise, create the path using the protocol info.
+ // note we are using the
+ // hostname, unless that directory exists.
+ // this should prevent all collisions.
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> localPath;
+ rv = protocolInfo->GetDefaultLocalPath(getter_AddRefs(localPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = localPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // set the leaf name to "dummy", and then call MakeUnique with a suggested
+ // leaf name
+ rv = localPath->AppendNative(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = localPath->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLocalPath(localPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ localPath.forget(aLocalPath);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetMsgStore(nsIMsgPluggableStore** aMsgStore) {
+ NS_ENSURE_ARG_POINTER(aMsgStore);
+ if (!m_msgStore) {
+ nsCString storeContractID;
+ nsresult rv;
+ // We don't want there to be a default pref, I think, since
+ // we can't change the default. We may want no pref to mean
+ // berkeley store, and then set the store pref off of some sort
+ // of default when creating a server. But we need to make sure
+ // that we do always write a store pref.
+ GetCharValue("storeContractID", storeContractID);
+ if (storeContractID.IsEmpty()) {
+ storeContractID.AssignLiteral("@mozilla.org/msgstore/berkeleystore;1");
+ SetCharValue("storeContractID", storeContractID);
+ }
+
+ // After someone starts using the pluggable store, we can no longer
+ // change the value.
+ SetBoolValue("canChangeStoreType", false);
+
+ // Right now, we just have one pluggable store per server. If we want
+ // to support multiple, this pref could be a list of pluggable store
+ // contract id's.
+ m_msgStore = do_CreateInstance(storeContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_IF_ADDREF(*aMsgStore = m_msgStore);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetLocalPath(nsIFile* aLocalPath) {
+ NS_ENSURE_ARG_POINTER(aLocalPath);
+ nsresult rv = aLocalPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetFileValue("directory-rel", "directory", aLocalPath);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalStoreType(nsACString& aResult) {
+ MOZ_ASSERT_UNREACHABLE(
+ "nsMsgIncomingServer superclass not implementing GetLocalStoreType!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalDatabaseType(nsACString& aResult) {
+ MOZ_ASSERT_UNREACHABLE(
+ "nsMsgIncomingServer superclass not implementing GetLocalDatabaseType!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetAccountManagerChrome(nsAString& aResult) {
+ aResult.AssignLiteral("am-main.xhtml");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::Equals(nsIMsgIncomingServer* server, bool* _retval) {
+ nsresult rv;
+
+ NS_ENSURE_ARG_POINTER(server);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCString key1;
+ nsCString key2;
+
+ rv = GetKey(key1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = server->GetKey(key2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // compare the server keys
+ *_retval = key1.Equals(key2, nsCaseInsensitiveCStringComparator);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ClearAllValues() {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = mPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ mPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::RemoveFiles() {
+ // IMPORTANT, see bug #77652
+ // TODO: Decide what to do for deferred accounts.
+ nsCString deferredToAccount;
+ GetCharValue("deferred_to_account", deferredToAccount);
+ bool isDeferredTo = true;
+ GetIsDeferredTo(&isDeferredTo);
+ if (!deferredToAccount.IsEmpty() || isDeferredTo) {
+ NS_ASSERTION(false, "shouldn't remove files for a deferred account");
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIFile> localPath;
+ nsresult rv = GetLocalPath(getter_AddRefs(localPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return localPath->Remove(true);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetFilterList(nsIMsgFilterList* aFilterList) {
+ mFilterList = aFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (!mFilterList) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ // use GetRootFolder so for deferred pop3 accounts, we'll get the filters
+ // file from the deferred account, not the deferred to account,
+ // so that filters will still be per-server.
+ nsresult rv = GetRootFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString filterType;
+ rv = GetCharValue("filter.type", filterType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!filterType.IsEmpty() && !filterType.EqualsLiteral("default")) {
+ nsAutoCString contractID("@mozilla.org/filterlist;1?type=");
+ contractID += filterType;
+ ToLowerCase(contractID);
+ mFilterList = do_CreateInstance(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mFilterList->SetFolder(msgFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+ }
+
+ // The default case, a local folder, is a bit special. It requires
+ // more initialization.
+
+ nsCOMPtr<nsIFile> thisFolder;
+ rv = msgFolder->GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFilterFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mFilterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFilterFile->AppendNative("msgFilterRules.dat"_ns);
+
+ bool fileExists;
+ mFilterFile->Exists(&fileExists);
+ if (!fileExists) {
+ nsCOMPtr<nsIFile> oldFilterFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = oldFilterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ oldFilterFile->AppendNative("rules.dat"_ns);
+
+ oldFilterFile->Exists(&fileExists);
+ if (fileExists) // copy rules.dat --> msgFilterRules.dat
+ {
+ rv = oldFilterFile->CopyToNative(thisFolder, "msgFilterRules.dat"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterService->OpenFilterList(mFilterFile, msgFolder, aMsgWindow,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetEditableFilterList(
+ nsIMsgFilterList* aEditableFilterList) {
+ mEditableFilterList = aEditableFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (!mEditableFilterList) {
+ bool editSeparate;
+ nsresult rv = GetBoolValue("filter.editable.separate", &editSeparate);
+ if (NS_FAILED(rv) || !editSeparate)
+ return GetFilterList(aMsgWindow, aResult);
+
+ nsCString filterType;
+ rv = GetCharValue("filter.editable.type", filterType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractID("@mozilla.org/filterlist;1?type=");
+ contractID += filterType;
+ ToLowerCase(contractID);
+ mEditableFilterList = do_CreateInstance(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ // use GetRootFolder so for deferred pop3 accounts, we'll get the filters
+ // file from the deferred account, not the deferred to account,
+ // so that filters will still be per-server.
+ rv = GetRootFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mEditableFilterList->SetFolder(msgFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = mEditableFilterList);
+ return NS_OK;
+ }
+
+ NS_IF_ADDREF(*aResult = mEditableFilterList);
+ return NS_OK;
+}
+
+// If the hostname contains ':' (like hostname:1431)
+// then parse and set the port number.
+nsresult nsMsgIncomingServer::InternalSetHostName(const nsACString& aHostname,
+ const char* prefName) {
+ nsCString hostname;
+ hostname = aHostname;
+ if (hostname.CountChar(':') == 1) {
+ int32_t colonPos = hostname.FindChar(':');
+ nsAutoCString portString(Substring(hostname, colonPos));
+ hostname.SetLength(colonPos);
+ nsresult err;
+ int32_t port = portString.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) SetPort(port);
+ }
+ return SetCharValue(prefName, hostname);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::OnUserOrHostNameChanged(const nsACString& oldName,
+ const nsACString& newName,
+ bool hostnameChanged) {
+ nsresult rv;
+
+ // 1. Reset password so that users are prompted for new password for the new
+ // user/host.
+ int32_t atPos = newName.FindChar('@');
+ if (hostnameChanged) {
+ ForgetPassword();
+ }
+
+ // 2. Replace all occurrences of old name in the acct name with the new one.
+ nsString acctName;
+ rv = GetPrettyName(acctName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 3. Clear the clientid because the user or host have changed.
+ SetClientid(EmptyCString());
+
+ // Will be generated again when used.
+ mPrefBranch->ClearUserPref("spamActionTargetAccount");
+
+ // If new username contains @ then better do not update the account name.
+ if (acctName.IsEmpty() || (!hostnameChanged && (atPos != kNotFound)))
+ return NS_OK;
+
+ atPos = acctName.FindChar('@');
+
+ // get previous username and hostname
+ nsCString userName, hostName;
+ if (hostnameChanged) {
+ rv = GetUsername(userName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hostName.Assign(oldName);
+ } else {
+ userName.Assign(oldName);
+ rv = GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // switch corresponding part of the account name to the new name...
+ if (!hostnameChanged && (atPos != kNotFound)) {
+ // ...if username changed and the previous username was equal to the part
+ // of the account name before @
+ if (StringHead(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(userName)))
+ acctName.Replace(0, userName.Length(), NS_ConvertASCIItoUTF16(newName));
+ }
+ if (hostnameChanged) {
+ // ...if hostname changed and the previous hostname was equal to the part
+ // of the account name after @, or to the whole account name
+ if (atPos == kNotFound)
+ atPos = 0;
+ else
+ atPos += 1;
+ if (Substring(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(hostName))) {
+ acctName.Replace(atPos, acctName.Length() - atPos,
+ NS_ConvertASCIItoUTF16(newName));
+ }
+ }
+
+ return SetPrettyName(acctName);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetHostName(const nsACString& aHostname) {
+ nsCString oldName;
+ nsresult rv = GetHostName(oldName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = InternalSetHostName(aHostname, "hostname");
+
+ if (!oldName.IsEmpty() &&
+ !aHostname.Equals(oldName, nsCaseInsensitiveCStringComparator))
+ rv = OnUserOrHostNameChanged(oldName, aHostname, true);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetHostName(nsACString& aResult) {
+ nsresult rv = GetCharValue("hostname", aResult);
+ if (aResult.CountChar(':') == 1) {
+ // gack, we need to reformat the hostname - SetHostName will do that
+ SetHostName(aResult);
+ rv = GetCharValue("hostname", aResult);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUsername(const nsACString& aUsername) {
+ nsCString oldName;
+ nsresult rv = GetUsername(oldName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!oldName.IsEmpty() && !oldName.Equals(aUsername)) {
+ // If only username changed and the new name just added a domain we can keep
+ // the password.
+ int32_t atPos = aUsername.FindChar('@');
+ if ((atPos == kNotFound) ||
+ !StringHead(NS_ConvertASCIItoUTF16(aUsername), atPos)
+ .Equals(NS_ConvertASCIItoUTF16(oldName))) {
+ ForgetPassword();
+ }
+ rv = SetCharValue("userName", aUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = OnUserOrHostNameChanged(oldName, aUsername, false);
+ } else {
+ rv = SetCharValue("userName", aUsername);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUsername(nsACString& aResult) {
+ return GetCharValue("userName", aResult);
+}
+
+#define BIFF_PREF_NAME "check_new_mail"
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDoBiff(bool* aDoBiff) {
+ NS_ENSURE_ARG_POINTER(aDoBiff);
+
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv;
+
+ rv = mPrefBranch->GetBoolPref(BIFF_PREF_NAME, aDoBiff);
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ // if the pref isn't set, use the default
+ // value based on the protocol
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = protocolInfo->GetDefaultDoBiff(aDoBiff);
+ // note, don't call SetDoBiff()
+ // since we keep changing our minds on
+ // if biff should be on or off, let's keep the ability
+ // to change the default in future builds.
+ // if we call SetDoBiff() here, it will be in the users prefs.
+ // and we can't do anything after that.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDoBiff(bool aDoBiff) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Update biffManager immediately, no restart required. Adding/removing
+ // existing/non-existing server is handled without error checking.
+ nsresult rv;
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+ if (NS_SUCCEEDED(rv) && biffService) {
+ if (aDoBiff)
+ (void)biffService->AddServerBiff(this);
+ else
+ (void)biffService->RemoveServerBiff(this);
+ }
+
+ return mPrefBranch->SetBoolPref(BIFF_PREF_NAME, aDoBiff);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPort(int32_t* aPort) {
+ NS_ENSURE_ARG_POINTER(aPort);
+
+ nsresult rv;
+ rv = GetIntValue("port", aPort);
+ // We can't use a port of 0, because the URI parsing code fails.
+ if (*aPort != PORT_NOT_SET && *aPort) return rv;
+
+ // if the port isn't set, use the default
+ // port based on the protocol
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool useSSLPort = (socketType == nsMsgSocketType::SSL);
+ return protocolInfo->GetDefaultServerPort(useSSLPort, aPort);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetPort(int32_t aPort) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool useSSLPort = (socketType == nsMsgSocketType::SSL);
+
+ int32_t defaultPort;
+ protocolInfo->GetDefaultServerPort(useSSLPort, &defaultPort);
+ return SetIntValue("port", aPort == defaultPort ? PORT_NOT_SET : aPort);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetProtocolInfo(nsIMsgProtocolInfo** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCString type;
+ nsresult rv = GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractid(NS_MSGPROTOCOLINFO_CONTRACTID_PREFIX);
+ contractid.Append(type);
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo =
+ do_GetService(contractid.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ protocolInfo.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetRetentionSettings(
+ nsIMsgRetentionSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ nsMsgRetainByPreference retainByPreference;
+ int32_t daysToKeepHdrs = 0;
+ int32_t numHeadersToKeep = 0;
+ int32_t daysToKeepBodies = 0;
+ bool cleanupBodiesByDays = false;
+ bool applyToFlaggedMessages = false;
+ nsresult rv = NS_OK;
+ // Create an empty retention settings object,
+ // get the settings from the server prefs, and init the object from the prefs.
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings =
+ do_CreateInstance("@mozilla.org/msgDatabase/retentionSettings;1");
+ if (retentionSettings) {
+ rv = GetIntValue("retainBy", (int32_t*)&retainByPreference);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("numHdrsToKeep", &numHeadersToKeep);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("daysToKeepHdrs", &daysToKeepHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("daysToKeepBodies", &daysToKeepBodies);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetBoolValue("cleanupBodies", &cleanupBodiesByDays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetBoolValue("applyToFlaggedMessages", &applyToFlaggedMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ retentionSettings->SetRetainByPreference(retainByPreference);
+ retentionSettings->SetNumHeadersToKeep((uint32_t)numHeadersToKeep);
+ retentionSettings->SetDaysToKeepBodies(daysToKeepBodies);
+ retentionSettings->SetDaysToKeepHdrs(daysToKeepHdrs);
+ retentionSettings->SetCleanupBodiesByDays(cleanupBodiesByDays);
+ retentionSettings->SetApplyToFlaggedMessages(applyToFlaggedMessages);
+ } else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ NS_IF_ADDREF(*settings = retentionSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetRetentionSettings(
+ nsIMsgRetentionSettings* settings) {
+ nsMsgRetainByPreference retainByPreference;
+ uint32_t daysToKeepHdrs = 0;
+ uint32_t numHeadersToKeep = 0;
+ uint32_t daysToKeepBodies = 0;
+ bool cleanupBodiesByDays = false;
+ bool applyToFlaggedMessages = false;
+ settings->GetRetainByPreference(&retainByPreference);
+ settings->GetNumHeadersToKeep(&numHeadersToKeep);
+ settings->GetDaysToKeepBodies(&daysToKeepBodies);
+ settings->GetDaysToKeepHdrs(&daysToKeepHdrs);
+ settings->GetCleanupBodiesByDays(&cleanupBodiesByDays);
+ settings->GetApplyToFlaggedMessages(&applyToFlaggedMessages);
+ nsresult rv = SetIntValue("retainBy", retainByPreference);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("numHdrsToKeep", numHeadersToKeep);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("daysToKeepHdrs", daysToKeepHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("daysToKeepBodies", daysToKeepBodies);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetBoolValue("cleanupBodies", cleanupBodiesByDays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetBoolValue("applyToFlaggedMessages", applyToFlaggedMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDisplayStartupPage(bool* displayStartupPage) {
+ NS_ENSURE_ARG_POINTER(displayStartupPage);
+ *displayStartupPage = m_displayStartupPage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDisplayStartupPage(bool displayStartupPage) {
+ m_displayStartupPage = displayStartupPage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetDownloadSettings(
+ nsIMsgDownloadSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ bool downloadUnreadOnly = false;
+ bool downloadByDate = false;
+ uint32_t ageLimitOfMsgsToDownload = 0;
+ nsresult rv = NS_OK;
+ if (!m_downloadSettings) {
+ m_downloadSettings =
+ do_CreateInstance("@mozilla.org/msgDatabase/downloadSettings;1");
+ if (m_downloadSettings) {
+ rv = GetBoolValue("downloadUnreadOnly", &downloadUnreadOnly);
+ rv = GetBoolValue("downloadByDate", &downloadByDate);
+ rv = GetIntValue("ageLimit", (int32_t*)&ageLimitOfMsgsToDownload);
+ m_downloadSettings->SetDownloadUnreadOnly(downloadUnreadOnly);
+ m_downloadSettings->SetDownloadByDate(downloadByDate);
+ m_downloadSettings->SetAgeLimitOfMsgsToDownload(ageLimitOfMsgsToDownload);
+ } else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ // Create an empty download settings object,
+ // get the settings from the server prefs, and init the object from the
+ // prefs.
+ }
+ NS_IF_ADDREF(*settings = m_downloadSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetDownloadSettings(
+ nsIMsgDownloadSettings* settings) {
+ m_downloadSettings = settings;
+ bool downloadUnreadOnly = false;
+ bool downloadByDate = false;
+ uint32_t ageLimitOfMsgsToDownload = 0;
+ m_downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ m_downloadSettings->GetDownloadByDate(&downloadByDate);
+ m_downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
+ nsresult rv = SetBoolValue("downloadUnreadOnly", downloadUnreadOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetBoolValue("downloadByDate", downloadByDate);
+ return SetIntValue("ageLimit", ageLimitOfMsgsToDownload);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSupportsDiskSpace(bool* aSupportsDiskSpace) {
+ NS_ENSURE_ARG_POINTER(aSupportsDiskSpace);
+ *aSupportsDiskSpace = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetOfflineSupportLevel(int32_t* aSupportLevel) {
+ NS_ENSURE_ARG_POINTER(aSupportLevel);
+
+ nsresult rv = GetIntValue("offline_support_level", aSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*aSupportLevel == OFFLINE_SUPPORT_LEVEL_UNDEFINED)
+ *aSupportLevel = OFFLINE_SUPPORT_LEVEL_NONE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetOfflineSupportLevel(int32_t aSupportLevel) {
+ SetIntValue("offline_support_level", aSupportLevel);
+ return NS_OK;
+}
+
+// Called only during the migration process. A unique name is generated for the
+// migrated account.
+NS_IMETHODIMP
+nsMsgIncomingServer::GeneratePrettyNameForMigration(nsAString& aPrettyName) {
+ /**
+ * 4.x had provisions for multiple imap servers to be maintained under
+ * single identity. So, when migrated each of those server accounts need
+ * to be represented by unique account name. nsImapIncomingServer will
+ * override the implementation for this to do the right thing.
+ */
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFilterScope(nsMsgSearchScopeValue* filterScope) {
+ NS_ENSURE_ARG_POINTER(filterScope);
+ *filterScope = nsMsgSearchScope::offlineMailFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSearchScope(nsMsgSearchScopeValue* searchScope) {
+ NS_ENSURE_ARG_POINTER(searchScope);
+ *searchScope = nsMsgSearchScope::offlineMail;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetIsSecure(bool* aIsSecure) {
+ NS_ENSURE_ARG_POINTER(aIsSecure);
+ int32_t socketType;
+ nsresult rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aIsSecure = (socketType == nsMsgSocketType::alwaysSTARTTLS ||
+ socketType == nsMsgSocketType::SSL);
+ return NS_OK;
+}
+
+// use the convenience macros to implement the accessors
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, AuthMethod, "authMethod")
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, BiffMinutes, "check_time")
+NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Type, "type")
+NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Clientid, "clientid")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, ClientidEnabled, "clientidEnabled")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, DownloadOnBiff, "download_on_biff")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Valid, "valid")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, EmptyTrashOnExit,
+ "empty_trash_on_exit")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanDelete, "canDelete")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LoginAtStartUp, "login_at_startup")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer,
+ DefaultCopiesAndFoldersPrefsToServer,
+ "allows_specialfolders_usage")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanCreateFoldersOnServer,
+ "canCreateFolders")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanFileMessagesOnServer,
+ "canFileMessages")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LimitOfflineMessageSize,
+ "limit_offline_message_size")
+
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, MaxMessageSize, "max_size")
+
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, IncomingDuplicateAction,
+ "dup_action")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Hidden, "hidden")
+
+NS_IMETHODIMP nsMsgIncomingServer::GetSocketType(int32_t* aSocketType) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetIntPref("socketType", aSocketType);
+
+ // socketType is set to default value. Look at isSecure setting
+ if (NS_FAILED(rv)) {
+ bool isSecure;
+ rv = mPrefBranch->GetBoolPref("isSecure", &isSecure);
+ if (NS_SUCCEEDED(rv) && isSecure) {
+ *aSocketType = nsMsgSocketType::SSL;
+ // don't call virtual method in case overrides call GetSocketType
+ nsMsgIncomingServer::SetSocketType(*aSocketType);
+ } else {
+ if (!mDefPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+ rv = mDefPrefBranch->GetIntPref("socketType", aSocketType);
+ if (NS_FAILED(rv)) *aSocketType = nsMsgSocketType::plain;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetSocketType(int32_t aSocketType) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ int32_t socketType = nsMsgSocketType::plain;
+ mPrefBranch->GetIntPref("socketType", &socketType);
+
+ nsresult rv = mPrefBranch->SetIntPref("socketType", aSocketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isSecureOld = (socketType == nsMsgSocketType::alwaysSTARTTLS ||
+ socketType == nsMsgSocketType::SSL);
+ bool isSecureNew = (aSocketType == nsMsgSocketType::alwaysSTARTTLS ||
+ aSocketType == nsMsgSocketType::SSL);
+ if ((isSecureOld != isSecureNew) && m_rootFolder) {
+ m_rootFolder->NotifyBoolPropertyChanged(kIsSecure, isSecureOld,
+ isSecureNew);
+ }
+ return NS_OK;
+}
+
+// Check if the password is available and return a boolean indicating whether
+// it is being authenticated or not.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPasswordPromptRequired(bool* aPasswordIsRequired) {
+ NS_ENSURE_ARG_POINTER(aPasswordIsRequired);
+ *aPasswordIsRequired = true;
+
+ // If the password is not even required for biff we don't need to check any
+ // further
+ nsresult rv = GetServerRequiresPasswordForBiff(aPasswordIsRequired);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!*aPasswordIsRequired) return NS_OK;
+
+ // If the password is empty, check to see if it is stored and to be retrieved
+ if (m_password.IsEmpty()) (void)GetPasswordWithoutUI();
+
+ *aPasswordIsRequired = m_password.IsEmpty();
+ if (*aPasswordIsRequired) {
+ // Set *aPasswordIsRequired false if authMethod is oauth2.
+ int32_t authMethod = 0;
+ rv = GetAuthMethod(&authMethod);
+ if (NS_SUCCEEDED(rv) && authMethod == nsMsgAuthMethod::OAuth2) {
+ *aPasswordIsRequired = false;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::ConfigureTemporaryFilters(
+ nsIMsgFilterList* aFilterList) {
+ nsresult rv = ConfigureTemporaryReturnReceiptsFilter(aFilterList);
+ if (NS_FAILED(rv)) // shut up warnings...
+ return rv;
+ return ConfigureTemporaryServerSpamFilters(aFilterList);
+}
+
+nsresult nsMsgIncomingServer::ConfigureTemporaryServerSpamFilters(
+ nsIMsgFilterList* filterList) {
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ nsresult rv = GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useServerFilter;
+ rv = spamSettings->GetUseServerFilter(&useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we aren't configured to use server filters, then return early.
+ if (!useServerFilter) return NS_OK;
+
+ // For performance reasons, we'll handle clearing of filters if the user turns
+ // off the server-side filters from the junk mail controls, in the junk mail
+ // controls.
+ nsAutoCString serverFilterName;
+ spamSettings->GetServerFilterName(serverFilterName);
+ if (serverFilterName.IsEmpty()) return NS_OK;
+ int32_t serverFilterTrustFlags = 0;
+ (void)spamSettings->GetServerFilterTrustFlags(&serverFilterTrustFlags);
+ if (!serverFilterTrustFlags) return NS_OK;
+ // check if filters have been setup already.
+ nsAutoString yesFilterName, noFilterName;
+ CopyASCIItoUTF16(serverFilterName, yesFilterName);
+ yesFilterName.AppendLiteral("Yes");
+
+ CopyASCIItoUTF16(serverFilterName, noFilterName);
+ noFilterName.AppendLiteral("No");
+
+ nsCOMPtr<nsIMsgFilter> newFilter;
+ (void)filterList->GetFilterNamed(yesFilterName, getter_AddRefs(newFilter));
+
+ if (!newFilter)
+ (void)filterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter));
+ if (newFilter) return NS_OK;
+
+ nsCOMPtr<nsIFile> file;
+ spamSettings->GetServerFilterFile(getter_AddRefs(file));
+
+ // it's possible that we can no longer find the sfd file (i.e. the user
+ // disabled an extnsion that was supplying the .sfd file.
+ if (!file) return NS_OK;
+
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ nsCOMPtr<nsIMsgFilterList> serverFilterList;
+
+ rv = filterService->OpenFilterList(file, NULL, NULL,
+ getter_AddRefs(serverFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = serverFilterList->GetFilterNamed(yesFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_POSITIVES) {
+ newFilter->SetTemporary(true);
+ // check if we're supposed to move junk mail to junk folder; if so,
+ // add filter action to do so.
+
+ /*
+ * We don't want this filter to activate on messages that have
+ * been marked by the user as not spam. This occurs when messages that
+ * were marked as good are moved back into the inbox. But to
+ * do this with a filter, we have to add a boolean term. That requires
+ * that we rewrite the existing filter search terms to group them.
+ */
+
+ // get the list of search terms from the filter
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = newFilter->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t count = searchTerms.Length();
+ if (count > 1) // don't need to group a single term
+ {
+ // beginGrouping the first term, and endGrouping the last term
+ searchTerms[0]->SetBeginsGrouping(true);
+ searchTerms[count - 1]->SetEndsGrouping(true);
+ }
+
+ // Create a new term, checking if the user set junk status. The term will
+ // search for junkscoreorigin != "user"
+ nsCOMPtr<nsIMsgSearchTerm> searchTerm;
+ rv = newFilter->CreateTerm(getter_AddRefs(searchTerm));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ searchTerm->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin);
+ searchTerm->SetOp(nsMsgSearchOp::Isnt);
+ searchTerm->SetBooleanAnd(true);
+
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+ searchTerm->GetValue(getter_AddRefs(searchValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+ searchValue->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin);
+ searchValue->SetStr(u"user"_ns);
+ searchTerm->SetValue(searchValue);
+
+ newFilter->AppendTerm(searchTerm);
+
+ bool moveOnSpam, markAsReadOnSpam;
+ spamSettings->GetMoveOnSpam(&moveOnSpam);
+ if (moveOnSpam) {
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ if (NS_SUCCEEDED(rv) && (!spamFolderURI.IsEmpty())) {
+ nsCOMPtr<nsIMsgRuleAction> moveAction;
+ rv = newFilter->CreateAction(getter_AddRefs(moveAction));
+ if (NS_SUCCEEDED(rv)) {
+ moveAction->SetType(nsMsgFilterAction::MoveToFolder);
+ moveAction->SetTargetFolderUri(spamFolderURI);
+ newFilter->AppendAction(moveAction);
+ }
+ }
+ }
+ spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam);
+ if (markAsReadOnSpam) {
+ nsCOMPtr<nsIMsgRuleAction> markAsReadAction;
+ rv = newFilter->CreateAction(getter_AddRefs(markAsReadAction));
+ if (NS_SUCCEEDED(rv)) {
+ markAsReadAction->SetType(nsMsgFilterAction::MarkRead);
+ newFilter->AppendAction(markAsReadAction);
+ }
+ }
+ filterList->InsertFilterAt(0, newFilter);
+ }
+
+ rv =
+ serverFilterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter));
+ if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_NEGATIVES) {
+ newFilter->SetTemporary(true);
+ filterList->InsertFilterAt(0, newFilter);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgIncomingServer::ConfigureTemporaryReturnReceiptsFilter(
+ nsIMsgFilterList* filterList) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = accountMgr->GetFirstIdentityForServer(this, getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // this can return success and a null identity...
+
+ bool useCustomPrefs = false;
+ int32_t incorp = nsIMsgMdnGenerator::eIncorporateInbox;
+ NS_ENSURE_TRUE(identity, NS_ERROR_NULL_POINTER);
+
+ identity->GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ if (useCustomPrefs)
+ rv = GetIntValue("incorporate_return_receipt", &incorp);
+ else {
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) prefs->GetIntPref("mail.incorporate.return_receipt", &incorp);
+ }
+
+ bool enable = (incorp == nsIMsgMdnGenerator::eIncorporateSent);
+
+ // this is a temporary, internal mozilla filter
+ // it will not show up in the UI, it will not be written to disk
+ constexpr auto internalReturnReceiptFilterName =
+ u"mozilla-temporary-internal-MDN-receipt-filter"_ns;
+
+ nsCOMPtr<nsIMsgFilter> newFilter;
+ rv = filterList->GetFilterNamed(internalReturnReceiptFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter)
+ newFilter->SetEnabled(enable);
+ else if (enable) {
+ nsCString actionTargetFolderUri;
+ rv = identity->GetFccFolder(actionTargetFolderUri);
+ if (!actionTargetFolderUri.IsEmpty()) {
+ filterList->CreateFilter(internalReturnReceiptFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter) {
+ newFilter->SetEnabled(true);
+ // this internal filter is temporary
+ // and should not show up in the UI or be written to disk
+ newFilter->SetTemporary(true);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ nsCOMPtr<nsIMsgSearchValue> value;
+
+ rv = newFilter->CreateTerm(getter_AddRefs(term));
+ if (NS_SUCCEEDED(rv)) {
+ rv = term->GetValue(getter_AddRefs(value));
+ if (NS_SUCCEEDED(rv)) {
+ // we need to use OtherHeader + 1 so nsMsgFilter::GetTerm will
+ // return our custom header.
+ value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ value->SetStr(u"multipart/report"_ns);
+ term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ term->SetOp(nsMsgSearchOp::Contains);
+ term->SetBooleanAnd(true);
+ term->SetArbitraryHeader("Content-Type"_ns);
+ term->SetValue(value);
+ newFilter->AppendTerm(term);
+ }
+ }
+ rv = newFilter->CreateTerm(getter_AddRefs(term));
+ if (NS_SUCCEEDED(rv)) {
+ rv = term->GetValue(getter_AddRefs(value));
+ if (NS_SUCCEEDED(rv)) {
+ // XXX todo
+ // determine if ::OtherHeader is the best way to do this.
+ // see nsMsgSearchOfflineMail::MatchTerms()
+ value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ value->SetStr(u"disposition-notification"_ns);
+ term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ term->SetOp(nsMsgSearchOp::Contains);
+ term->SetBooleanAnd(true);
+ term->SetArbitraryHeader("Content-Type"_ns);
+ term->SetValue(value);
+ newFilter->AppendTerm(term);
+ }
+ }
+ nsCOMPtr<nsIMsgRuleAction> filterAction;
+ rv = newFilter->CreateAction(getter_AddRefs(filterAction));
+ if (NS_SUCCEEDED(rv)) {
+ filterAction->SetType(nsMsgFilterAction::MoveToFolder);
+ filterAction->SetTargetFolderUri(actionTargetFolderUri);
+ newFilter->AppendAction(filterAction);
+ filterList->InsertFilterAt(0, newFilter);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ClearTemporaryReturnReceiptsFilter() {
+ if (mFilterList) {
+ nsCOMPtr<nsIMsgFilter> mdnFilter;
+ nsresult rv = mFilterList->GetFilterNamed(
+ u"mozilla-temporary-internal-MDN-receipt-filter"_ns,
+ getter_AddRefs(mdnFilter));
+ if (NS_SUCCEEDED(rv) && mdnFilter)
+ return mFilterList->RemoveFilter(mdnFilter);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetMsgFolderFromURI(nsIMsgFolder* aFolderResource,
+ const nsACString& aURI,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_TRUE(rootMsgFolder, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = rootMsgFolder->GetChildWithURI(aURI, true, true /*caseInsensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_FAILED(rv) || !msgFolder) msgFolder = aFolderResource;
+ NS_IF_ADDREF(*aFolder = msgFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSpamSettings(nsISpamSettings** aSpamSettings) {
+ NS_ENSURE_ARG_POINTER(aSpamSettings);
+
+ nsAutoCString spamActionTargetAccount;
+ GetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ if (spamActionTargetAccount.IsEmpty()) {
+ GetServerURI(spamActionTargetAccount);
+ SetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ }
+
+ if (!mSpamSettings) {
+ nsresult rv;
+ mSpamSettings =
+ do_CreateInstance("@mozilla.org/messenger/spamsettings;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamSettings->Initialize(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_ADDREF(*aSpamSettings = mSpamSettings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSpamFilterPlugin(nsIMsgFilterPlugin** aFilterPlugin) {
+ NS_ENSURE_ARG_POINTER(aFilterPlugin);
+ if (!mFilterPlugin) {
+ nsresult rv;
+ mFilterPlugin = do_GetService(
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aFilterPlugin = mFilterPlugin);
+ return NS_OK;
+}
+
+// get all the servers that defer to the account for the passed in server. Note
+// that destServer may not be "this"
+nsresult nsMsgIncomingServer::GetDeferredServers(
+ nsIMsgIncomingServer* destServer,
+ nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> thisAccount;
+ accountManager->FindAccountForServer(destServer, getter_AddRefs(thisAccount));
+ if (thisAccount) {
+ nsCString accountKey;
+ thisAccount->GetKey(accountKey);
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ accountManager->GetAllServers(allServers);
+ for (auto server : allServers) {
+ nsCOMPtr<nsIPop3IncomingServer> popServer(do_QueryInterface(server));
+ if (popServer) {
+ nsCString deferredToAccount;
+ popServer->GetDeferredToAccount(deferredToAccount);
+ if (deferredToAccount.Equals(accountKey))
+ aServers.AppendElement(popServer);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetIsDeferredTo(bool* aIsDeferredTo) {
+ NS_ENSURE_ARG_POINTER(aIsDeferredTo);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1");
+ if (accountManager) {
+ nsCOMPtr<nsIMsgAccount> thisAccount;
+ accountManager->FindAccountForServer(this, getter_AddRefs(thisAccount));
+ if (thisAccount) {
+ nsCString accountKey;
+ thisAccount->GetKey(accountKey);
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ accountManager->GetAllServers(allServers);
+ for (auto server : allServers) {
+ if (server) {
+ nsCString deferredToAccount;
+ server->GetCharValue("deferred_to_account", deferredToAccount);
+ if (deferredToAccount.Equals(accountKey)) {
+ *aIsDeferredTo = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+ *aIsDeferredTo = false;
+ return NS_OK;
+}
+
+const long kMaxDownloadTableSize = 500;
+
+// hash the concatenation of the message-id and subject as the hash table key,
+// and store the arrival index as the value. To limit the size of the hash
+// table, we just throw out ones with a lower ordinal value than the cut-off
+// point.
+NS_IMETHODIMP nsMsgIncomingServer::IsNewHdrDuplicate(nsIMsgDBHdr* aNewHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+ *aResult = false;
+
+ // If the message has been partially downloaded, the message should not
+ // be considered a duplicated message. See bug 714090.
+ uint32_t flags;
+ aNewHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) return NS_OK;
+
+ nsAutoCString strHashKey;
+ nsCString messageId, subject;
+ aNewHdr->GetMessageId(getter_Copies(messageId));
+ strHashKey.Append(messageId);
+ aNewHdr->GetSubject(subject);
+ // err on the side of caution and ignore messages w/o subject or messageid.
+ if (subject.IsEmpty() || messageId.IsEmpty()) return NS_OK;
+ strHashKey.Append(subject);
+ int32_t hashValue = m_downloadedHdrs.Get(strHashKey);
+ if (hashValue)
+ *aResult = true;
+ else {
+ // we store the current size of the hash table as the hash
+ // value - this allows us to delete older entries.
+ m_downloadedHdrs.InsertOrUpdate(strHashKey, ++m_numMsgsDownloaded);
+ // Check if hash table is larger than some reasonable size
+ // and if is it, iterate over hash table deleting messages
+ // with an arrival index < number of msgs downloaded - half the reasonable
+ // size.
+ if (m_downloadedHdrs.Count() >= kMaxDownloadTableSize) {
+ for (auto iter = m_downloadedHdrs.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() < m_numMsgsDownloaded - kMaxDownloadTableSize / 2) {
+ iter.Remove();
+ } else if (m_downloadedHdrs.Count() <= kMaxDownloadTableSize / 2) {
+ break;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetForcePropertyEmpty(const char* aPropertyName,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ nsCString value;
+ GetCharValue(nameEmpty.get(), value);
+ *_retval = value.EqualsLiteral("true");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetForcePropertyEmpty(const char* aPropertyName,
+ bool aValue) {
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ return SetCharValue(nameEmpty.get(), aValue ? "true"_ns : ""_ns);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSortOrder(int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = 100000000;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.h b/comm/mailnews/base/src/nsMsgIncomingServer.h
new file mode 100644
index 0000000000..c75c1f07c9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIncomingServer.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgIncomingServer_h__
+#define nsMsgIncomingServer_h__
+
+#include "nsIMsgIncomingServer.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgFilterList.h"
+#include "msgCore.h"
+#include "nsIMsgFolder.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsWeakReference.h"
+#include "nsIMsgDatabase.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsTHashMap.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIObserver.h"
+
+class nsIMsgFolderCache;
+class nsIMsgProtocolInfo;
+
+/*
+ * base class for nsIMsgIncomingServer - derive your class from here
+ * if you want to get some free implementation
+ *
+ * this particular implementation is not meant to be used directly.
+ */
+
+class nsMsgIncomingServer : public nsIMsgIncomingServer,
+ public nsSupportsWeakReference,
+ public nsIObserver {
+ public:
+ nsMsgIncomingServer();
+ nsresult Init();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGINCOMINGSERVER
+ NS_DECL_NSIOBSERVER
+
+ protected:
+ virtual ~nsMsgIncomingServer();
+ nsCString m_serverKey;
+
+ // Sets m_password, if password found. Can return NS_ERROR_ABORT if the
+ // user cancels the master password dialog.
+ nsresult GetPasswordWithoutUI();
+
+ nsresult ConfigureTemporaryReturnReceiptsFilter(nsIMsgFilterList* filterList);
+ nsresult ConfigureTemporaryServerSpamFilters(nsIMsgFilterList* filterList);
+
+ nsCOMPtr<nsIMsgFolder> m_rootFolder;
+ nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings;
+
+ // For local servers, where we put messages. For imap/pop3, where we store
+ // offline messages.
+ nsCOMPtr<nsIMsgPluggableStore> m_msgStore;
+
+ /// Helper routine to create local folder on disk if it doesn't exist
+ /// under the account's rootFolder.
+ nsresult CreateLocalFolder(const nsAString& folderName);
+
+ static nsresult GetDeferredServers(
+ nsIMsgIncomingServer* destServer,
+ nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers);
+
+ nsresult CreateRootFolder();
+ virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) = 0;
+
+ nsresult InternalSetHostName(const nsACString& aHostname,
+ const char* prefName);
+
+ nsCOMPtr<nsIFile> mFilterFile;
+ nsCOMPtr<nsIMsgFilterList> mFilterList;
+ nsCOMPtr<nsIMsgFilterList> mEditableFilterList;
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ nsCOMPtr<nsIPrefBranch> mDefPrefBranch;
+
+ // these allow us to handle duplicate incoming messages, e.g. delete them.
+ nsTHashMap<nsCStringHashKey, int32_t> m_downloadedHdrs;
+ int32_t m_numMsgsDownloaded;
+
+ private:
+ uint32_t m_biffState;
+ bool m_serverBusy;
+ nsCOMPtr<nsISpamSettings> mSpamSettings;
+ nsCOMPtr<nsIMsgFilterPlugin> mFilterPlugin; // XXX should be a list
+
+ protected:
+ nsString m_password;
+ bool m_canHaveFilters;
+ bool m_displayStartupPage;
+ bool mPerformingBiff;
+};
+
+#endif // nsMsgIncomingServer_h__
diff --git a/comm/mailnews/base/src/nsMsgKeySet.cpp b/comm/mailnews/base/src/nsMsgKeySet.cpp
new file mode 100644
index 0000000000..7423e2560a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgKeySet.cpp
@@ -0,0 +1,1412 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "prlog.h"
+
+#include "MailNewsTypes.h"
+#include "nsMsgKeySet.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsTArray.h"
+#include "nsMemory.h"
+#include <ctype.h>
+
+#if defined(DEBUG_seth_) || defined(DEBUG_sspitzer_)
+# define DEBUG_MSGKEYSET 1
+#endif
+
+/* A compressed encoding for sets of article. This is usually for lines from
+ the newsrc, which have article lists like
+
+ 1-29627,29635,29658,32861-32863
+
+ so the data has these properties:
+
+ - strictly increasing
+ - large subsequences of monotonically increasing ranges
+ - gaps in the set are usually small, but not always
+ - consecutive ranges tend to be large
+
+ The biggest win is to run-length encode the data, storing ranges as two
+ numbers (start+length or start,end). We could also store each number as a
+ delta from the previous number for further compression, but that gets kind
+ of tricky, since there are no guarantees about the sizes of the gaps, and
+ we'd have to store variable-length words.
+
+ Current data format:
+
+ DATA := SIZE [ CHUNK ]*
+ CHUNK := [ RANGE | VALUE ]
+ RANGE := -LENGTH START
+ START := VALUE
+ LENGTH := int32_t
+ VALUE := a literal positive integer, for now
+ it could also be an offset from the previous value.
+ LENGTH could also perhaps be a less-than-32-bit quantity,
+ at least most of the time.
+
+ Lengths of CHUNKs are stored negative to distinguish the beginning of
+ a chunk from a literal: negative means two-word sequence, positive
+ means one-word sequence.
+
+ 0 represents a literal 0, but should not occur, and should never occur
+ except in the first position.
+
+ A length of -1 won't occur either, except temporarily - a sequence of
+ two elements is represented as two literals, since they take up the same
+ space.
+
+ Another optimization we make is to notice that we typically ask the
+ question ``is N a member of the set'' for increasing values of N. So the
+ set holds a cache of the last value asked for, and can simply resume the
+ search from there. */
+
+nsMsgKeySet::nsMsgKeySet(/* MSG_NewsHost* host*/) {
+ m_cached_value = -1;
+ m_cached_value_index = 0;
+ m_length = 0;
+ m_data_size = 10;
+ m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size);
+#ifdef NEWSRC_DOES_HOST_STUFF
+ m_host = host;
+#endif
+}
+
+nsMsgKeySet::~nsMsgKeySet() { PR_FREEIF(m_data); }
+
+bool nsMsgKeySet::Grow() {
+ int32_t new_size;
+ int32_t* new_data;
+ new_size = m_data_size * 2;
+ new_data = (int32_t*)PR_REALLOC(m_data, sizeof(int32_t) * new_size);
+ if (!new_data) return false;
+ m_data_size = new_size;
+ m_data = new_data;
+ return true;
+}
+
+nsMsgKeySet::nsMsgKeySet(const char* numbers /* , MSG_NewsHost* host */) {
+ int32_t *head, *tail, *end;
+
+#ifdef NEWSRC_DOES_HOST_STUFF
+ m_host = host;
+#endif
+ m_cached_value = -1;
+ m_cached_value_index = 0;
+ m_length = 0;
+ m_data_size = 10;
+ m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size);
+ if (!m_data) return;
+
+ head = m_data;
+ tail = head;
+ end = head + m_data_size;
+
+ if (!numbers) {
+ return;
+ }
+
+ while (isspace(*numbers)) numbers++;
+ while (*numbers) {
+ int32_t from = 0;
+ int32_t to;
+
+ if (tail >= end - 4) {
+ /* out of room! */
+ int32_t tailo = tail - head;
+ if (!Grow()) {
+ PR_FREEIF(m_data);
+ return;
+ }
+ /* data may have been relocated */
+ head = m_data;
+ tail = head + tailo;
+ end = head + m_data_size;
+ }
+
+ while (isspace(*numbers)) numbers++;
+ if (*numbers && !isdigit(*numbers)) {
+ break; /* illegal character */
+ }
+ while (isdigit(*numbers)) {
+ from = (from * 10) + (*numbers++ - '0');
+ }
+ while (isspace(*numbers)) numbers++;
+ if (*numbers != '-') {
+ to = from;
+ } else {
+ to = 0;
+ numbers++;
+ while (*numbers >= '0' && *numbers <= '9')
+ to = (to * 10) + (*numbers++ - '0');
+ while (isspace(*numbers)) numbers++;
+ }
+
+ if (to < from) to = from; /* illegal */
+
+ /* This is a hack - if the newsrc file specifies a range 1-x as
+ being read, we internally pretend that article 0 is read as well.
+ (But if only 2-x are read, then 0 is not read.) This is needed
+ because some servers think that article 0 is an article (I think)
+ but some news readers (including Netscape 1.1) choke if the .newsrc
+ file has lines beginning with 0... ### */
+ if (from == 1) from = 0;
+
+ if (to == from) {
+ /* Write it as a literal */
+ *tail = from;
+ tail++;
+ } else /* Write it as a range. */ {
+ *tail = -(to - from);
+ tail++;
+ *tail = from;
+ tail++;
+ }
+
+ while (*numbers == ',' || isspace(*numbers)) {
+ numbers++;
+ }
+ }
+
+ m_length = tail - head; /* size of data */
+}
+
+nsMsgKeySet* nsMsgKeySet::Create(/*MSG_NewsHost* host*/) {
+ nsMsgKeySet* set = new nsMsgKeySet(/* host */);
+ if (set && set->m_data == NULL) {
+ delete set;
+ set = NULL;
+ }
+ return set;
+}
+
+nsMsgKeySet* nsMsgKeySet::Create(const char* value /* , MSG_NewsHost* host */) {
+#ifdef DEBUG_MSGKEYSET
+ printf("create from %s\n", value);
+#endif
+
+ nsMsgKeySet* set = new nsMsgKeySet(value /* , host */);
+ if (set && set->m_data == NULL) {
+ delete set;
+ set = NULL;
+ }
+ return set;
+}
+
+/* Returns the lowest non-member of the set greater than 0.
+ */
+int32_t nsMsgKeySet::FirstNonMember() {
+ if (m_length <= 0) {
+ return 1;
+ } else if (m_data[0] < 0 && m_data[1] != 1 && m_data[1] != 0) {
+ /* first range not equal to 0 or 1, always return 1 */
+ return 1;
+ } else if (m_data[0] < 0) {
+ /* it's a range */
+ /* If there is a range [N-M] we can presume that M+1 is not in the
+ set. */
+ return (m_data[1] - m_data[0] + 1);
+ } else {
+ /* it's a literal */
+ if (m_data[0] == 1) {
+ /* handle "1,..." */
+ if (m_length > 1 && m_data[1] == 2) {
+ /* This is "1,2,M-N,..." or "1,2,M,..." where M >= 4. Note
+ that M will never be 3, because in that case we would have
+ started with a range: "1-3,..." */
+ return 3;
+ } else {
+ return 2; /* handle "1,M-N,.." or "1,M,..."
+ where M >= 3; */
+ }
+ } else if (m_data[0] == 0) {
+ /* handle "0,..." */
+ if (m_length > 1 && m_data[1] == 1) {
+ /* this is 0,1, (see above) */
+ return 2;
+ } else {
+ return 1;
+ }
+
+ } else {
+ /* handle "M,..." where M >= 2. */
+ return 1;
+ }
+ }
+}
+
+nsresult nsMsgKeySet::Output(char** outputStr) {
+ NS_ENSURE_ARG(outputStr);
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t s_size;
+ char* s_head;
+ char *s, *s_end;
+ int32_t last_art = -1;
+
+ *outputStr = nullptr;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ s_size = (size * 12) +
+ 10; // dmb - try to make this allocation get used at least once.
+ s_head = (char*)moz_xmalloc(s_size);
+ if (!s_head) return NS_ERROR_OUT_OF_MEMORY;
+
+ s_head[0] = '\0'; // otherwise, s_head will contain garbage.
+ s = s_head;
+ s_end = s + s_size;
+
+ while (tail < end) {
+ int32_t from;
+ int32_t to;
+
+ if (s > (s_end - (12 * 2 + 10))) { /* 12 bytes for each number (enough
+ for "2147483647" aka 2^31-1),
+ plus 10 bytes of slop. */
+ int32_t so = s - s_head;
+ s_size += 200;
+ char* tmp = (char*)moz_xmalloc(s_size);
+ if (tmp) PL_strcpy(tmp, s_head);
+ free(s_head);
+ s_head = tmp;
+ if (!s_head) return NS_ERROR_OUT_OF_MEMORY;
+ s = s_head + so;
+ s_end = s_head + s_size;
+ }
+
+ if (*tail < 0) {
+ /* it's a range */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else /* it's a literal */
+ {
+ from = *tail;
+ to = from;
+ tail++;
+ }
+ if (from == 0) {
+ from = 1; /* See 'hack' comment above ### */
+ }
+ if (from <= last_art) from = last_art + 1;
+ if (from <= to) {
+ if (from < to) {
+ PR_snprintf(s, s_end - s, "%lu-%lu,", from, to);
+ } else {
+ PR_snprintf(s, s_end - s, "%lu,", from);
+ }
+ s += PL_strlen(s);
+ last_art = to;
+ }
+ }
+ if (last_art >= 0) {
+ s--; /* Strip off the last ',' */
+ }
+
+ *s = 0;
+
+ *outputStr = s_head;
+ return NS_OK;
+}
+
+int32_t nsMsgKeySet::GetLastMember() {
+ if (m_length > 1) {
+ int32_t nextToLast = m_data[m_length - 2];
+ if (nextToLast < 0) // is range at end?
+ {
+ int32_t last = m_data[m_length - 1];
+ return (-nextToLast + last - 1);
+ } else // no, so last number must be last member
+ {
+ return m_data[m_length - 1];
+ }
+ } else if (m_length == 1)
+ return m_data[0]; // must be only 1 read.
+ else
+ return 0;
+}
+
+void nsMsgKeySet::SetLastMember(int32_t newHighWaterMark) {
+ if (newHighWaterMark < GetLastMember()) {
+ while (true) {
+ if (m_length > 1) {
+ int32_t nextToLast = m_data[m_length - 2];
+ int32_t curHighWater;
+ if (nextToLast < 0) // is range at end?
+ {
+ int32_t rangeStart = m_data[m_length - 1];
+ int32_t rangeLength = -nextToLast;
+ curHighWater = (rangeLength + rangeStart - 1);
+ if (curHighWater > newHighWaterMark) {
+ if (rangeStart > newHighWaterMark) {
+ m_length -= 2; // throw away whole range
+ } else if (rangeStart == newHighWaterMark) {
+ // turn range into single element.
+ m_data[m_length - 2] = newHighWaterMark;
+ m_length--;
+ break;
+ } else // just shorten range
+ {
+ m_data[m_length - 2] = -(newHighWaterMark - rangeStart);
+ break;
+ }
+ } else {
+ // prevent the infinite loop
+ // see bug #13062
+ break;
+ }
+ } else if (m_data[m_length - 1] >
+ newHighWaterMark) // no, so last number must be last member
+ {
+ m_length--;
+ } else
+ break;
+ } else
+ break;
+ }
+ // well, the whole range is probably invalid, because the server probably
+ // re-ordered ids, but what can you do?
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ }
+}
+
+int32_t nsMsgKeySet::GetFirstMember() {
+ if (m_length > 1) {
+ int32_t first = m_data[0];
+ if (first < 0) // is range at start?
+ {
+ int32_t second = m_data[1];
+ return (second);
+ } else // no, so first number must be first member
+ {
+ return m_data[0];
+ }
+ } else if (m_length == 1)
+ return m_data[0]; // must be only 1 read.
+ else
+ return 0;
+}
+
+/* Re-compresses a `nsMsgKeySet' object.
+
+ The assumption is made that the `nsMsgKeySet' is syntactically correct
+ (all ranges have a length of at least 1, and all values are non-
+ decreasing) but will optimize the compression, for example, merging
+ consecutive literals or ranges into one range.
+
+ Returns true if successful, false if there wasn't enough memory to
+ allocate scratch space.
+
+ #### This should be changed to modify the buffer in place.
+
+ Also note that we never call Optimize() unless we actually changed
+ something, so it's a great place to tell the MSG_NewsHost* that something
+ changed.
+ */
+bool nsMsgKeySet::Optimize() {
+ int32_t input_size;
+ int32_t output_size;
+ int32_t* input_tail;
+ int32_t* output_data;
+ int32_t* output_tail;
+ int32_t* input_end;
+ int32_t* output_end;
+
+ input_size = m_length;
+ output_size = input_size + 1;
+ input_tail = m_data;
+ output_data = (int32_t*)PR_Malloc(sizeof(int32_t) * output_size);
+ if (!output_data) return false;
+
+ output_tail = output_data;
+ input_end = input_tail + input_size;
+ output_end = output_data + output_size;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (input_tail < input_end) {
+ int32_t from, to;
+ bool range_p = (*input_tail < 0);
+
+ if (range_p) {
+ /* it's a range */
+ from = input_tail[1];
+ to = from + (-(input_tail[0]));
+
+ /* Copy it over */
+ *output_tail++ = *input_tail++;
+ *output_tail++ = *input_tail++;
+ } else {
+ /* it's a literal */
+ from = *input_tail;
+ to = from;
+
+ /* Copy it over */
+ *output_tail++ = *input_tail++;
+ }
+ NS_ASSERTION(output_tail < output_end, "invalid end of output string");
+ if (output_tail >= output_end) {
+ PR_Free(output_data);
+ return false;
+ }
+
+ /* As long as this chunk is followed by consecutive chunks,
+ keep extending it. */
+ while (input_tail < input_end &&
+ ((*input_tail > 0 && /* literal... */
+ *input_tail == to + 1) || /* ...and consecutive, or */
+ (*input_tail <= 0 && /* range... */
+ input_tail[1] == to + 1)) /* ...and consecutive. */
+ ) {
+ if (!range_p) {
+ /* convert the literal to a range. */
+ output_tail++;
+ output_tail[-2] = 0;
+ output_tail[-1] = from;
+ range_p = true;
+ }
+
+ if (*input_tail > 0) { /* literal */
+ output_tail[-2]--; /* increase length by 1 */
+ to++;
+ input_tail++;
+ } else {
+ int32_t L2 = (-*input_tail) + 1;
+ output_tail[-2] -= L2; /* increase length by N */
+ to += L2;
+ input_tail += 2;
+ }
+ }
+ }
+
+ PR_Free(m_data);
+ m_data = output_data;
+ m_data_size = output_size;
+ m_length = output_tail - output_data;
+
+ /* One last pass to turn [N - N+1] into [N, N+1]. */
+ output_tail = output_data;
+ output_end = output_tail + m_length;
+ while (output_tail < output_end) {
+ if (*output_tail < 0) {
+ /* it's a range */
+ if (output_tail[0] == -1) {
+ output_tail[0] = output_tail[1];
+ output_tail[1]++;
+ }
+ output_tail += 2;
+ } else {
+ /* it's a literal */
+ output_tail++;
+ }
+ }
+
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ return true;
+}
+
+bool nsMsgKeySet::IsMember(int32_t number) {
+ bool value = false;
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ /* If there is a value cached, and that value is smaller than the
+ value we're looking for, skip forward that far. */
+ if (m_cached_value > 0 && m_cached_value < number) {
+ tail += m_cached_value_index;
+ }
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+ if (from > number) {
+ /* This range begins after the number - we've passed it. */
+ value = false;
+ goto DONE;
+ } else if (to >= number) {
+ /* In range. */
+ value = true;
+ goto DONE;
+ } else {
+ tail += 2;
+ }
+ } else {
+ /* it's a literal */
+ if (*tail == number) {
+ /* bang */
+ value = true;
+ goto DONE;
+ } else if (*tail > number) {
+ /* This literal is after the number - we've passed it. */
+ value = false;
+ goto DONE;
+ } else {
+ tail++;
+ }
+ }
+ }
+
+DONE:
+ /* Store the position of this chunk for next time. */
+ m_cached_value = number;
+ m_cached_value_index = tail - head;
+
+ return value;
+}
+
+int nsMsgKeySet::Add(int32_t number) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+#ifdef DEBUG_MSGKEYSET
+ printf("add %d\n", number);
+#endif
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ NS_ASSERTION(number >= 0, "can't have negative items");
+ if (number < 0) return 0;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+
+ if (from <= number && to >= number) {
+ /* This number is already present - we don't need to do
+ anything. */
+ return 0;
+ }
+
+ if (to > number) {
+ /* We have found the point before which the new number
+ should be inserted. */
+ break;
+ }
+
+ tail += 2;
+ } else {
+ /* it's a literal */
+ if (*tail == number) {
+ /* This number is already present - we don't need to do
+ anything. */
+ return 0;
+ }
+
+ if (*tail > number) {
+ /* We have found the point before which the new number
+ should be inserted. */
+ break;
+ }
+
+ tail++;
+ }
+ }
+
+ /* At this point, `tail' points to a position in the set which represents
+ a value greater than `new'; or it is at `end'. In the interest of
+ avoiding massive duplication of code, simply insert a literal here and
+ then run the optimizer.
+ */
+ int mid = (tail - head);
+
+ if (m_data_size <= m_length + 1) {
+ int endo = end - head;
+ if (!Grow()) {
+ // out of memory
+ return -1;
+ }
+ head = m_data;
+ end = head + endo;
+ }
+
+ if (tail == end) {
+ /* at the end */
+ /* Add a literal to the end. */
+ m_data[m_length++] = number;
+ } else {
+ /* need to insert (or edit) in the middle */
+ int32_t i;
+ for (i = size; i > mid; i--) {
+ m_data[i] = m_data[i - 1];
+ }
+ m_data[i] = number;
+ m_length++;
+ }
+
+ Optimize();
+ return 1;
+}
+
+int nsMsgKeySet::Remove(int32_t number) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+#ifdef DEBUG_MSGKEYSET
+ printf("remove %d\n", number);
+#endif
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ // **** I am not sure this is a right thing to comment the following
+ // statements out. The reason for this is due to the implementation of
+ // offline save draft and template. We use faked UIDs (negative ids) for
+ // offline draft and template in order to distinguish them from real
+ // UID. David I need your help here. **** jt
+
+ // PR_ASSERT(number >= 0);
+ // if (number < 0) {
+ // return -1;
+ /// }
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (tail < end) {
+ int32_t mid = (tail - m_data);
+
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+
+ if (number < from || number > to) {
+ /* Not this range */
+ tail += 2;
+ continue;
+ }
+
+ if (to == from + 1) {
+ /* If this is a range [N - N+1] and we are removing M
+ (which must be either N or N+1) replace it with a
+ literal. This reduces the length by 1. */
+ m_data[mid] = (number == from ? to : from);
+ while (++mid < m_length) {
+ m_data[mid] = m_data[mid + 1];
+ }
+ m_length--;
+ Optimize();
+ return 1;
+ } else if (to == from + 2) {
+ /* If this is a range [N - N+2] and we are removing M,
+ replace it with the literals L,M (that is, either
+ (N, N+1), (N, N+2), or (N+1, N+2). The overall
+ length remains the same. */
+ m_data[mid] = from;
+ m_data[mid + 1] = to;
+ if (from == number) {
+ m_data[mid] = from + 1;
+ } else if (to == number) {
+ m_data[mid + 1] = to - 1;
+ }
+ Optimize();
+ return 1;
+ } else if (from == number) {
+ /* This number is at the beginning of a long range (meaning a
+ range which will still be long enough to remain a range.)
+ Increase start and reduce length of the range. */
+ m_data[mid]++;
+ m_data[mid + 1]++;
+ Optimize();
+ return 1;
+ } else if (to == number) {
+ /* This number is at the end of a long range (meaning a range
+ which will still be long enough to remain a range.)
+ Just decrease the length of the range. */
+ m_data[mid]++;
+ Optimize();
+ return 1;
+ } else {
+ /* The number being deleted is in the middle of a range which
+ must be split. This increases overall length by 2.
+ */
+ int32_t i;
+ int endo = end - head;
+ if (m_data_size - m_length <= 2) {
+ if (!Grow())
+ // out of memory
+ return -1;
+ }
+ head = m_data;
+ end = head + endo;
+
+ for (i = m_length + 2; i > mid + 2; i--) {
+ m_data[i] = m_data[i - 2];
+ }
+
+ m_data[mid] = (-(number - from - 1));
+ m_data[mid + 1] = from;
+ m_data[mid + 2] = (-(to - number - 1));
+ m_data[mid + 3] = number + 1;
+ m_length += 2;
+
+ /* Oops, if we've ended up with a range with a 0 length,
+ which is illegal, convert it to a literal, which reduces
+ the overall length by 1. */
+ if (m_data[mid] == 0) {
+ /* first range */
+ m_data[mid] = m_data[mid + 1];
+ for (i = mid + 1; i < m_length; i++) {
+ m_data[i] = m_data[i + 1];
+ }
+ m_length--;
+ }
+ if (m_data[mid + 2] == 0) {
+ /* second range */
+ m_data[mid + 2] = m_data[mid + 3];
+ for (i = mid + 3; i < m_length; i++) {
+ m_data[i] = m_data[i + 1];
+ }
+ m_length--;
+ }
+ Optimize();
+ return 1;
+ }
+ } else {
+ /* it's a literal */
+ if (*tail != number) {
+ /* Not this literal */
+ tail++;
+ continue;
+ }
+
+ /* Excise this literal. */
+ m_length--;
+ while (mid < m_length) {
+ m_data[mid] = m_data[mid + 1];
+ mid++;
+ }
+ Optimize();
+ return 1;
+ }
+ }
+
+ /* It wasn't here at all. */
+ return 0;
+}
+
+static int32_t* msg_emit_range(int32_t* tmp, int32_t a, int32_t b) {
+ if (a == b) {
+ *tmp++ = a;
+ } else {
+ NS_ASSERTION(a < b && a >= 0, "range is out of order");
+ *tmp++ = -(b - a);
+ *tmp++ = a;
+ }
+ return tmp;
+}
+
+int nsMsgKeySet::AddRange(int32_t start, int32_t end) {
+ int32_t tmplength;
+ int32_t* tmp;
+ int32_t* in;
+ int32_t* out;
+ int32_t* tail;
+ int32_t a;
+ int32_t b;
+ bool didit = false;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ NS_ASSERTION(start <= end, "invalid range");
+ if (start > end) return -1;
+
+ if (start == end) {
+ return Add(start);
+ }
+
+ tmplength = m_length + 2;
+ tmp = (int32_t*)PR_Malloc(sizeof(int32_t) * tmplength);
+
+ if (!tmp)
+ // out of memory
+ return -1;
+
+ in = m_data;
+ out = tmp;
+ tail = in + m_length;
+
+#define EMIT(x, y) out = msg_emit_range(out, x, y)
+
+ while (in < tail) {
+ // Set [a,b] to be this range.
+ if (*in < 0) {
+ b = -*in++;
+ a = *in++;
+ b += a;
+ } else {
+ a = b = *in++;
+ }
+
+ if (a <= start && b >= end) {
+ // We already have the entire range marked.
+ PR_Free(tmp);
+ return 0;
+ }
+ if (start > b + 1) {
+ // No overlap yet.
+ EMIT(a, b);
+ } else if (end < a - 1) {
+ // No overlap, and we passed it.
+ EMIT(start, end);
+ EMIT(a, b);
+ didit = true;
+ break;
+ } else {
+ // The ranges overlap. Suck this range into our new range, and
+ // keep looking for other ranges that might overlap.
+ start = start < a ? start : a;
+ end = end > b ? end : b;
+ }
+ }
+ if (!didit) EMIT(start, end);
+ while (in < tail) {
+ *out++ = *in++;
+ }
+
+#undef EMIT
+
+ PR_Free(m_data);
+ m_data = tmp;
+ m_length = out - tmp;
+ m_data_size = tmplength;
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ return 1;
+}
+
+int32_t nsMsgKeySet::CountMissingInRange(int32_t range_start,
+ int32_t range_end) {
+ int32_t count;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+ NS_ASSERTION(range_start >= 0 && range_end >= 0 && range_end >= range_start,
+ "invalid range");
+ if (range_start < 0 || range_end < 0 || range_end < range_start) return -1;
+
+ head = m_data;
+ tail = head;
+ end = head + m_length;
+
+ count = range_end - range_start + 1;
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+ if (from < range_start) from = range_start;
+ if (to > range_end) to = range_end;
+
+ if (to >= from) count -= (to - from + 1);
+
+ tail += 2;
+ } else {
+ /* it's a literal */
+ if (*tail >= range_start && *tail <= range_end) count--;
+ tail++;
+ }
+ NS_ASSERTION(count >= 0, "invalid count");
+ }
+ return count;
+}
+
+int nsMsgKeySet::FirstMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t from = 0;
+ int32_t to = 0;
+ int32_t a;
+ int32_t b;
+
+ NS_ASSERTION(first && last, "invalid parameter");
+ if (!first || !last) return -1;
+
+ *first = *last = 0;
+
+ NS_ASSERTION(min <= max && min > 0, "invalid min or max param");
+ if (min > max || min <= 0) return -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ a = to + 1;
+ if (*tail < 0) { /* We got a range. */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else {
+ from = to = tail[0];
+ tail++;
+ }
+ b = from - 1;
+ /* At this point, [a,b] is the range of unread articles just before
+ the current range of read articles [from,to]. See if this range
+ intersects the [min,max] range we were given. */
+ if (a > max) return 0; /* It's hopeless; there are none. */
+ if (a <= b && b >= min) {
+ /* Ah-hah! We found an intersection. */
+ *first = a > min ? a : min;
+ *last = b < max ? b : max;
+ return 0;
+ }
+ }
+ /* We found no holes in the newsrc that overlaps the range, nor did we hit
+ something read beyond the end of the range. So, the great infinite
+ range of unread articles at the end of any newsrc line intersects the
+ range we want, and we just need to return that. */
+ a = to + 1;
+ *first = a > min ? a : min;
+ *last = max;
+ return 0;
+}
+
+// I'm guessing we didn't include this because we didn't think we're going
+// to need it. I'm not so sure. I'm putting it in for now.
+int nsMsgKeySet::LastMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t from = 0;
+ int32_t to = 0;
+ int32_t a;
+ int32_t b;
+
+ NS_ASSERTION(first && last, "invalid null param");
+ if (!first || !last) return -1;
+
+ *first = *last = 0;
+
+ NS_ASSERTION(min <= max && min > 0, "invalid min or max");
+ if (min > max || min <= 0) return -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ a = to + 1;
+ if (*tail < 0) { /* We got a range. */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else {
+ from = to = tail[0];
+ tail++;
+ }
+ b = from - 1;
+ /* At this point, [a,b] is the range of unread articles just before
+ the current range of read articles [from,to]. See if this range
+ intersects the [min,max] range we were given. */
+ if (a > max)
+ return 0; /* We're done. If we found something, it's already
+ sitting in [*first,*last]. */
+ if (a <= b && b >= min) {
+ /* Ah-hah! We found an intersection. */
+ *first = a > min ? a : min;
+ *last = b < max ? b : max;
+ /* Continue on, looking for a later range. */
+ }
+ }
+ if (to < max) {
+ /* The great infinite range of unread articles at the end of any newsrc
+ line intersects the range we want, and we just need to return that. */
+ a = to + 1;
+ *first = a > min ? a : min;
+ *last = max;
+ }
+ return 0;
+}
+
+/**
+ * Fill the passed in aArray with the keys in the message key set.
+ */
+nsresult nsMsgKeySet::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t last_art = -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ int32_t from;
+ int32_t to;
+
+ if (*tail < 0) {
+ /* it's a range */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else /* it's a literal */
+ {
+ from = *tail;
+ to = from;
+ tail++;
+ }
+ // The horrible news-hack used to adjust from to 1 if it was zero right
+ // here, but there is no longer a consumer of this method with that
+ // broken use-case.
+ if (from <= last_art) from = last_art + 1;
+ if (from <= to) {
+ if (from < to) {
+ for (int32_t i = from; i <= to; ++i) {
+ aArray.AppendElement(i);
+ }
+ } else {
+ aArray.AppendElement(from);
+ }
+ last_art = to;
+ }
+ }
+
+ return NS_OK;
+}
+
+#ifdef DEBUG /* A lot of test cases for the above */
+
+# define countof(x) (sizeof(x) / sizeof(*(x)))
+
+void nsMsgKeySet::test_decoder(const char* string) {
+ nsMsgKeySet set(string /* , NULL */);
+ char* tmp;
+ set.Output(&tmp);
+ printf("\t\"%s\"\t--> \"%s\"\n", string, tmp);
+ free(tmp);
+}
+
+# define START(STRING) \
+ string = STRING; \
+ if (!(set = nsMsgKeySet::Create(string))) abort()
+
+# define FROB(N, PUSHP) \
+ i = N; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s %c %3lu =\n", (unsigned long)set->m_length, s, \
+ (PUSHP ? '+' : '-'), (unsigned long)i); \
+ free(s); \
+ if (PUSHP ? set->Add(i) < 0 : set->Remove(i) < 0) abort(); \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s optimized =\n", (unsigned long)set->m_length, s); \
+ free(s);
+
+# define END() \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \
+ free(s); \
+ delete set;
+
+void nsMsgKeySet::test_adder(void) {
+ const char* string;
+ nsMsgKeySet* set;
+ char* s;
+ int32_t i;
+
+ START("0-70,72-99,105,107,110-111,117-200");
+
+ FROB(205, true);
+ FROB(206, true);
+ FROB(207, true);
+ FROB(208, true);
+ FROB(208, true);
+ FROB(109, true);
+ FROB(72, true);
+
+ FROB(205, false);
+ FROB(206, false);
+ FROB(207, false);
+ FROB(208, false);
+ FROB(208, false);
+ FROB(109, false);
+ FROB(72, false);
+
+ FROB(72, true);
+ FROB(109, true);
+ FROB(208, true);
+ FROB(208, true);
+ FROB(207, true);
+ FROB(206, true);
+ FROB(205, true);
+
+ FROB(205, false);
+ FROB(206, false);
+ FROB(207, false);
+ FROB(208, false);
+ FROB(208, false);
+ FROB(109, false);
+ FROB(72, false);
+
+ FROB(100, true);
+ FROB(101, true);
+ FROB(102, true);
+ FROB(103, true);
+ FROB(106, true);
+ FROB(104, true);
+ FROB(109, true);
+ FROB(108, true);
+ END();
+
+ // clang-format off
+ START("1-6"); FROB(7, false); END();
+ START("1-6"); FROB(6, false); END();
+ START("1-6"); FROB(5, false); END();
+ START("1-6"); FROB(4, false); END();
+ START("1-6"); FROB(3, false); END();
+ START("1-6"); FROB(2, false); END();
+ START("1-6"); FROB(1, false); END();
+ START("1-6"); FROB(0, false); END();
+
+ START("1-3"); FROB(1, false); END();
+ START("1-3"); FROB(2, false); END();
+ START("1-3"); FROB(3, false); END();
+
+ START("1,3,5-7,9,10"); FROB(5, false); END();
+ START("1,3,5-7,9,10"); FROB(6, false); END();
+ START("1,3,5-7,9,10"); FROB(7, false); FROB(7, true); FROB(8, true);
+ FROB (4, true); FROB (2, false); FROB (2, true);
+
+ FROB (4, false); FROB (5, false); FROB (6, false); FROB (7, false);
+ FROB (8, false); FROB (9, false); FROB (10, false); FROB (3, false);
+ FROB (2, false); FROB (1, false); FROB (1, false); FROB (0, false);
+ END();
+ // clang-format on
+}
+
+# undef START
+# undef FROB
+# undef END
+
+# define START(STRING) \
+ string = STRING; \
+ if (!(set = nsMsgKeySet::Create(string))) abort()
+
+# define FROB(N, M) \
+ i = N; \
+ j = M; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s + %3lu-%3lu =\n", (unsigned long)set->m_length, s, \
+ (unsigned long)i, (unsigned long)j); \
+ free(s); \
+ switch (set->AddRange(i, j)) { \
+ case 0: \
+ printf("(no-op)\n"); \
+ break; \
+ case 1: \
+ break; \
+ default: \
+ abort(); \
+ } \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s\n", (unsigned long)set->m_length, s); \
+ free(s);
+
+# define END() \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \
+ free(s); \
+ delete set;
+
+void nsMsgKeySet::test_ranges(void) {
+ const char* string;
+ nsMsgKeySet* set;
+ char* s;
+ int32_t i;
+ int32_t j;
+
+ START("20-40,72-99,105,107,110-111,117-200");
+
+ FROB(205, 208);
+ FROB(50, 70);
+ FROB(0, 10);
+ FROB(112, 113);
+ FROB(101, 101);
+ FROB(5, 75);
+ FROB(103, 109);
+ FROB(2, 20);
+ FROB(1, 9999);
+
+ END();
+
+# undef START
+# undef FROB
+# undef END
+}
+
+# define TEST(N) \
+ if (!with_cache) set->m_cached_value = -1; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf(" %3d = %s\n", N, (set->IsMember(N) ? "true" : "false")); \
+ free(s);
+
+void nsMsgKeySet::test_member(bool with_cache) {
+ nsMsgKeySet* set;
+ char* s;
+
+ const char* st1 = "1-70,72-99,105,107,110-111,117-200";
+ printf("\n\nTesting %s (with%s cache)\n", st1, with_cache ? "" : "out");
+ if (!(set = Create(st1))) {
+ abort();
+ }
+
+ TEST(-1);
+ TEST(0);
+ TEST(1);
+ TEST(20);
+
+ delete set;
+ const char* st2 = "0-70,72-99,105,107,110-111,117-200";
+ printf("\n\nTesting %s (with%s cache)\n", st2, with_cache ? "" : "out");
+ if (!(set = Create(st2))) {
+ abort();
+ }
+
+ TEST(-1);
+ TEST(0);
+ TEST(1);
+ TEST(20);
+ TEST(69);
+ TEST(70);
+ TEST(71);
+ TEST(72);
+ TEST(73);
+ TEST(74);
+ TEST(104);
+ TEST(105);
+ TEST(106);
+ TEST(107);
+ TEST(108);
+ TEST(109);
+ TEST(110);
+ TEST(111);
+ TEST(112);
+ TEST(116);
+ TEST(117);
+ TEST(118);
+ TEST(119);
+ TEST(200);
+ TEST(201);
+ TEST(65535);
+
+ delete set;
+}
+
+# undef TEST
+
+// static void
+// test_newsrc (char *file)
+// {
+// FILE *fp = fopen (file, "r");
+// char buf [1024];
+// if (! fp) abort ();
+// while (fgets (buf, sizeof (buf), fp))
+// {
+// if (!strncmp (buf, "options ", 8))
+// fwrite (buf, 1, strlen (buf), stdout);
+// else
+// {
+// char *sep = buf;
+// while (*sep != 0 && *sep != ':' && *sep != '!')
+// sep++;
+// if (*sep) sep++;
+// while (isspace (*sep)) sep++;
+// fwrite (buf, 1, sep - buf, stdout);
+// if (*sep)
+// {
+// char *s;
+// msg_NewsRCSet *set = msg_parse_newsrc_set (sep, &allocinfo);
+// if (! set)
+// abort ();
+// if (! msg_OptimizeNewsRCSet (set))
+// abort ();
+// if (! ((s = msg_format_newsrc_set (set))))
+// abort ();
+// msg_free_newsrc_set (set, &allocinfo);
+// fwrite (s, 1, strlen (s), stdout);
+// free (s);
+// fwrite ("\n", 1, 1, stdout);
+// }
+// }
+// }
+// fclose (fp);
+// }
+
+void nsMsgKeySet::RunTests() {
+ test_decoder("");
+ test_decoder(" ");
+ test_decoder("0");
+ test_decoder("1");
+ test_decoder("123");
+ test_decoder(" 123 ");
+ test_decoder(" 123 4");
+ test_decoder(" 1,2, 3, 4");
+ test_decoder("0-70,72-99,100,101");
+ test_decoder(" 0-70 , 72 - 99 ,100,101 ");
+ test_decoder("0 - 268435455");
+ /* This one overflows - we can't help it.
+ test_decoder ("0 - 4294967295"); */
+
+ test_adder();
+
+ test_ranges();
+
+ test_member(false);
+ test_member(true);
+
+ // test_newsrc ("/u/montulli/.newsrc");
+ /* test_newsrc ("/u/jwz/.newsrc");*/
+}
+
+#endif /* DEBUG */
diff --git a/comm/mailnews/base/src/nsMsgKeySet.h b/comm/mailnews/base/src/nsMsgKeySet.h
new file mode 100644
index 0000000000..7390645870
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgKeySet.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgKeySet_H_
+#define _nsMsgKeySet_H_
+
+#include "msgCore.h"
+#include "nsTArray.h"
+
+// nsMsgKeySet represents a set of articles. Typically, it is the set of
+// read articles from a .newsrc file, but it can be used for other purposes
+// too.
+
+#if 0
+// If a MSG_NewsHost* is supplied to the creation routine, then that
+// MSG_NewsHost will be notified whenever a change is made to set.
+class MSG_NewsHost;
+#endif
+
+class nsMsgKeySet {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(nsMsgKeySet);
+ // Creates an empty set.
+ static nsMsgKeySet* Create(/* MSG_NewsHost* host = NULL*/);
+
+ // Creates a set from the list of numbers, as might be found in a
+ // newsrc file.
+ static nsMsgKeySet* Create(const char* str /* , MSG_NewsHost* host = NULL*/);
+
+ // FirstNonMember() returns the lowest non-member of the set that is
+ // greater than 0.
+ int32_t FirstNonMember();
+
+ // Output() converts to a string representation suitable for writing to a
+ // .newsrc file.
+ nsresult Output(char** outputStr);
+
+ // IsMember() returns whether the given article is a member of this set.
+ bool IsMember(int32_t art);
+
+ // Add() adds the given article to the set. (Returns 1 if a change was
+ // made, 0 if it was already there, and negative on error.)
+ int Add(int32_t art);
+
+ // Remove() removes the given article from the set.
+ int Remove(int32_t art);
+
+ // AddRange() adds the (inclusive) given range of articles to the set.
+ int AddRange(int32_t first, int32_t last);
+
+ // CountMissingInRange() takes an inclusive range of articles and returns
+ // the number of articles in that range which are not in the set.
+ int32_t CountMissingInRange(int32_t start, int32_t end);
+
+ // FirstMissingRange() takes an inclusive range and finds the first range
+ // of articles that are not in the set. If none, return zeros.
+ int FirstMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last);
+
+ // LastMissingRange() takes an inclusive range and finds the last range
+ // of articles that are not in the set. If none, return zeros.
+ int LastMissingRange(int32_t min, int32_t max, int32_t* first, int32_t* last);
+
+ int32_t GetLastMember();
+ int32_t GetFirstMember();
+ void SetLastMember(int32_t highWaterMark);
+ // For debugging only...
+ int32_t getLength() { return m_length; }
+
+ /**
+ * Fill the passed in aArray with the keys in the message key set.
+ */
+ nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray);
+
+#ifdef DEBUG
+ static void RunTests();
+#endif
+
+ protected:
+ nsMsgKeySet(/* MSG_NewsHost* host */);
+ explicit nsMsgKeySet(const char* /* , MSG_NewsHost* host */);
+ bool Grow();
+ bool Optimize();
+
+#ifdef DEBUG
+ static void test_decoder(const char*);
+ static void test_adder();
+ static void test_ranges();
+ static void test_member(bool with_cache);
+#endif
+
+ int32_t* m_data; /* the numbers composing the `chunks' */
+ int32_t m_data_size; /* size of that malloc'ed block */
+ int32_t m_length; /* active area */
+
+ int32_t m_cached_value; /* a potential set member, or -1 if unset*/
+ int32_t m_cached_value_index; /* the index into `data' at which a search
+ to determine whether `cached_value' was
+ a member of the set ended. */
+#ifdef NEWSRC_DOES_HOST_STUFF
+ MSG_NewsHost* m_host;
+#endif
+ ~nsMsgKeySet();
+};
+
+#endif /* _nsMsgKeySet_H_ */
diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.cpp b/comm/mailnews/base/src/nsMsgLineBuffer.cpp
new file mode 100644
index 0000000000..60d38b5b8f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgLineBuffer.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "prlog.h"
+#include "prmem.h"
+#include "nsMsgLineBuffer.h"
+#include "nsMsgUtils.h"
+#include "nsIInputStream.h" // used by nsMsgLineStreamBuffer
+
+nsByteArray::nsByteArray() {
+ MOZ_COUNT_CTOR(nsByteArray);
+ m_buffer = NULL;
+ m_bufferSize = 0;
+ m_bufferPos = 0;
+}
+
+nsByteArray::~nsByteArray() {
+ MOZ_COUNT_DTOR(nsByteArray);
+ PR_FREEIF(m_buffer);
+}
+
+nsresult nsByteArray::GrowBuffer(uint64_t desired_size, uint32_t quantum) {
+ if (desired_size > PR_UINT32_MAX) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (m_bufferSize < desired_size) {
+ char* new_buf;
+ uint32_t increment = desired_size - m_bufferSize;
+ if (increment < quantum) /* always grow by a minimum of N bytes */
+ increment = quantum;
+
+ new_buf =
+ (m_buffer ? (char*)PR_REALLOC(m_buffer, (m_bufferSize + increment))
+ : (char*)PR_MALLOC(m_bufferSize + increment));
+ if (!new_buf) return NS_ERROR_OUT_OF_MEMORY;
+ m_buffer = new_buf;
+ m_bufferSize += increment;
+ }
+ return NS_OK;
+}
+
+nsresult nsByteArray::AppendString(const char* string) {
+ uint32_t strLength = (string) ? PL_strlen(string) : 0;
+ return AppendBuffer(string, strLength);
+}
+
+nsresult nsByteArray::AppendBuffer(const char* buffer, uint32_t length) {
+ nsresult ret = NS_OK;
+ if (m_bufferPos + length > m_bufferSize)
+ ret = GrowBuffer(m_bufferPos + length, 1024);
+ if (NS_SUCCEEDED(ret)) {
+ memcpy(m_buffer + m_bufferPos, buffer, length);
+ m_bufferPos += length;
+ }
+ return ret;
+}
+
+nsMsgLineBuffer::nsMsgLineBuffer() { MOZ_COUNT_CTOR(nsMsgLineBuffer); }
+
+nsMsgLineBuffer::~nsMsgLineBuffer() { MOZ_COUNT_DTOR(nsMsgLineBuffer); }
+
+nsresult nsMsgLineBuffer::BufferInput(const char* net_buffer,
+ int32_t net_buffer_size) {
+ if (net_buffer_size < 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult status = NS_OK;
+ if (m_bufferPos > 0 && m_buffer && m_buffer[m_bufferPos - 1] == '\r' &&
+ net_buffer_size > 0 && net_buffer[0] != '\n') {
+ /* The last buffer ended with a CR. The new buffer does not start
+ with a LF. This old buffer should be shipped out and discarded. */
+ PR_ASSERT(m_bufferSize > m_bufferPos);
+ if (m_bufferSize <= m_bufferPos) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
+ return NS_ERROR_FAILURE;
+ }
+ m_bufferPos = 0;
+ }
+ while (net_buffer_size > 0) {
+ const char* net_buffer_end = net_buffer + net_buffer_size;
+ const char* newline = 0;
+ const char* s;
+
+ for (s = net_buffer; s < net_buffer_end; s++) {
+ /* Move forward in the buffer until the first newline.
+ Stop when we see CRLF, CR, or LF, or the end of the buffer.
+ *But*, if we see a lone CR at the *very end* of the buffer,
+ treat this as if we had reached the end of the buffer without
+ seeing a line terminator. This is to catch the case of the
+ buffers splitting a CRLF pair, as in "FOO\r\nBAR\r" "\nBAZ\r\n".
+ */
+ if (*s == '\r' || *s == '\n') {
+ newline = s;
+ if (newline[0] == '\r') {
+ if (s == net_buffer_end - 1) {
+ /* CR at end - wait for the next character. */
+ newline = 0;
+ break;
+ } else if (newline[1] == '\n') {
+ /* CRLF seen; swallow both. */
+ newline++;
+ }
+ }
+ newline++;
+ break;
+ }
+ }
+
+ /* Ensure room in the net_buffer and append some or all of the current
+ chunk of data to it. */
+ {
+ const char* end = (newline ? newline : net_buffer_end);
+ uint64_t desired_size = (end - net_buffer) + (uint64_t)m_bufferPos + 1;
+ if (desired_size >= PR_INT32_MAX) {
+ // We're not willing to buffer more than 2GB data without seeing
+ // a newline, something is wrong with the input.
+ // Using this limit prevents us from overflowing.
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (desired_size >= m_bufferSize) {
+ status = GrowBuffer(desired_size, 1024);
+ if (NS_FAILED(status)) return status;
+ }
+ memcpy(m_buffer + m_bufferPos, net_buffer, (end - net_buffer));
+ m_bufferPos += (end - net_buffer);
+ }
+
+ /* Now m_buffer contains either a complete line, or as complete
+ a line as we have read so far.
+
+ If we have a line, process it, and then remove it from `m_buffer'.
+ Then go around the loop again, until we drain the incoming data.
+ */
+ if (!newline) return NS_OK;
+
+ if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ net_buffer_size -= (newline - net_buffer);
+ net_buffer = newline;
+ m_bufferPos = 0;
+ }
+ return NS_OK;
+}
+
+// If there's still some data (non CRLF terminated) flush it out
+nsresult nsMsgLineBuffer::Flush() {
+ nsresult rv = NS_OK;
+ if (m_bufferPos > 0) {
+ rv = HandleLine(m_buffer, m_bufferPos);
+ m_bufferPos = 0;
+ }
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This is a utility class used to efficiently extract lines from an input
+// stream by buffering read but unprocessed stream data in a buffer.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+nsMsgLineStreamBuffer::nsMsgLineStreamBuffer(uint32_t aBufferSize,
+ bool aAllocateNewLines,
+ bool aEatCRLFs, char aLineToken)
+ : m_eatCRLFs(aEatCRLFs),
+ m_allocateNewLines(aAllocateNewLines),
+ m_lineToken(aLineToken) {
+ NS_ASSERTION(aBufferSize > 0, "invalid buffer size!!!");
+ m_dataBuffer = nullptr;
+ m_startPos = 0;
+ m_numBytesInBuffer = 0;
+
+ // used to buffer incoming data by ReadNextLineFromInput
+ if (aBufferSize > 0) {
+ m_dataBuffer = (char*)PR_CALLOC(sizeof(char) * aBufferSize);
+ }
+
+ m_dataBufferSize = aBufferSize;
+}
+
+nsMsgLineStreamBuffer::~nsMsgLineStreamBuffer() {
+ PR_FREEIF(m_dataBuffer); // release our buffer...
+}
+
+nsresult nsMsgLineStreamBuffer::GrowBuffer(uint32_t desiredSize) {
+ char* newBuffer = (char*)PR_REALLOC(m_dataBuffer, desiredSize);
+ NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
+ m_dataBuffer = newBuffer;
+ m_dataBufferSize = desiredSize;
+ return NS_OK;
+}
+
+void nsMsgLineStreamBuffer::ClearBuffer() {
+ m_startPos = 0;
+ m_numBytesInBuffer = 0;
+}
+
+// aInputStream - the input stream we want to read a line from
+// aPauseForMoreData is returned as true if the stream does not yet contain a
+// line and we must wait for more data to come into the stream. Note to people
+// wishing to modify this function: Be *VERY CAREFUL* this is a critical
+// function used by all of our mail protocols including imap, nntp, and pop. If
+// you screw it up, you could break a lot of stuff.....
+
+char* nsMsgLineStreamBuffer::ReadNextLine(nsIInputStream* aInputStream,
+ uint32_t& aNumBytesInLine,
+ bool& aPauseForMoreData,
+ nsresult* prv,
+ bool addLineTerminator) {
+ // try to extract a line from m_inputBuffer. If we don't have an entire line,
+ // then read more bytes out from the stream. If the stream is empty then wait
+ // on the monitor for more data to come in.
+
+ NS_ASSERTION(m_dataBuffer && m_dataBufferSize > 0,
+ "invalid input arguments for read next line from input");
+
+ if (prv) *prv = NS_OK;
+ // initialize out values
+ aPauseForMoreData = false;
+ aNumBytesInLine = 0;
+ char* endOfLine = nullptr;
+ char* startOfLine = m_dataBuffer + m_startPos;
+
+ if (m_numBytesInBuffer > 0) // any data in our internal buffer?
+ endOfLine = PL_strchr(startOfLine, m_lineToken); // see if we already
+ // have a line ending...
+
+ // it's possible that we got here before the first time we receive data from
+ // the server so aInputStream will be nullptr...
+ if (!endOfLine && aInputStream) // get some more data from the server
+ {
+ nsresult rv;
+ uint64_t numBytesInStream = 0;
+ uint32_t numBytesCopied = 0;
+ bool nonBlockingStream;
+ aInputStream->IsNonBlocking(&nonBlockingStream);
+ rv = aInputStream->Available(&numBytesInStream);
+ if (NS_FAILED(rv)) {
+ if (prv) *prv = rv;
+ aNumBytesInLine = 0;
+ return nullptr;
+ }
+ if (!nonBlockingStream && numBytesInStream == 0) // if no data available,
+ numBytesInStream = m_dataBufferSize / 2; // ask for half the data
+ // buffer size.
+
+ // if the number of bytes we want to read from the stream, is greater than
+ // the number of bytes left in our buffer, then we need to shift the start
+ // pos and its contents down to the beginning of m_dataBuffer...
+ uint32_t numFreeBytesInBuffer =
+ m_dataBufferSize - m_startPos - m_numBytesInBuffer;
+ if (numBytesInStream >= numFreeBytesInBuffer) {
+ if (m_startPos) {
+ memmove(m_dataBuffer, startOfLine, m_numBytesInBuffer);
+ // make sure the end of the buffer is terminated
+ m_dataBuffer[m_numBytesInBuffer] = '\0';
+ m_startPos = 0;
+ startOfLine = m_dataBuffer;
+ numFreeBytesInBuffer = m_dataBufferSize - m_numBytesInBuffer;
+ }
+ // If we didn't make enough space (or any), grow the buffer
+ if (numBytesInStream >= numFreeBytesInBuffer) {
+ int64_t growBy = (numBytesInStream - numFreeBytesInBuffer) * 2 + 1;
+ // GrowBuffer cannot handle over 4GB size.
+ if (m_dataBufferSize + growBy > PR_UINT32_MAX) return nullptr;
+ // try growing buffer by twice as much as we need.
+ nsresult rv = GrowBuffer(m_dataBufferSize + growBy);
+ // if we can't grow the buffer, we have to bail.
+ if (NS_FAILED(rv)) return nullptr;
+ startOfLine = m_dataBuffer;
+ numFreeBytesInBuffer += growBy;
+ }
+ NS_ASSERTION(m_startPos == 0, "m_startPos should be 0 .....");
+ }
+
+ uint32_t numBytesToCopy = /* leave one for a null terminator */
+ std::min(uint64_t(numFreeBytesInBuffer - 1), numBytesInStream);
+ if (numBytesToCopy > 0) {
+ // read the data into the end of our data buffer
+ char* startOfNewData = startOfLine + m_numBytesInBuffer;
+ rv = aInputStream->Read(startOfNewData, numBytesToCopy, &numBytesCopied);
+ if (prv) *prv = rv;
+ uint32_t i;
+ for (i = 0; i < numBytesCopied; i++) // replace nulls with spaces
+ {
+ if (!startOfNewData[i]) startOfNewData[i] = ' ';
+ }
+ m_numBytesInBuffer += numBytesCopied;
+ m_dataBuffer[m_startPos + m_numBytesInBuffer] = '\0';
+
+ // okay, now that we've tried to read in more data from the stream,
+ // look for another end of line character in the new data
+ endOfLine = PL_strchr(startOfNewData, m_lineToken);
+ }
+ }
+
+ // okay, now check again for endOfLine.
+ if (endOfLine) {
+ if (!m_eatCRLFs) endOfLine += 1; // count for LF or CR
+
+ aNumBytesInLine = endOfLine - startOfLine;
+
+ if (m_eatCRLFs && aNumBytesInLine > 0 &&
+ startOfLine[aNumBytesInLine - 1] == '\r')
+ aNumBytesInLine--; // Remove the CR in a CRLF sequence.
+
+ // PR_CALLOC zeros out the allocated line
+ char* newLine = (char*)PR_CALLOC(
+ aNumBytesInLine + (addLineTerminator ? MSG_LINEBREAK_LEN : 0) + 1);
+ if (!newLine) {
+ aNumBytesInLine = 0;
+ aPauseForMoreData = true;
+ return nullptr;
+ }
+
+ memcpy(newLine, startOfLine,
+ aNumBytesInLine); // copy the string into the new line buffer
+ if (addLineTerminator) {
+ memcpy(newLine + aNumBytesInLine, MSG_LINEBREAK, MSG_LINEBREAK_LEN);
+ aNumBytesInLine += MSG_LINEBREAK_LEN;
+ }
+
+ if (m_eatCRLFs)
+ endOfLine += 1; // advance past LF or CR if we haven't already done so...
+
+ // now we need to update the data buffer to go past the line we just read
+ // out.
+ m_numBytesInBuffer -= (endOfLine - startOfLine);
+ if (m_numBytesInBuffer)
+ m_startPos = endOfLine - m_dataBuffer;
+ else
+ m_startPos = 0;
+
+ return newLine;
+ }
+
+ aPauseForMoreData = true;
+ return nullptr; // if we somehow got here. we don't have another line in the
+ // buffer yet...need to wait for more data...
+}
+
+bool nsMsgLineStreamBuffer::NextLineAvailable() {
+ return (m_numBytesInBuffer > 0 &&
+ PL_strchr(m_dataBuffer + m_startPos, m_lineToken));
+}
diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.h b/comm/mailnews/base/src/nsMsgLineBuffer.h
new file mode 100644
index 0000000000..2ac579be3b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgLineBuffer.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMsgLineBuffer_H
+#define _nsMsgLineBuffer_H
+
+#include "msgCore.h" // precompiled header...
+
+// I can't believe I have to have this stupid class, but I can't find
+// anything suitable (nsStrImpl might be, when it's done). nsIByteBuffer
+// would do, if I had a stream for input, which I don't.
+
+class nsByteArray {
+ public:
+ nsByteArray();
+ virtual ~nsByteArray();
+ uint32_t GetSize() { return m_bufferSize; }
+ uint32_t GetBufferPos() { return m_bufferPos; }
+ nsresult GrowBuffer(uint64_t desired_size, uint32_t quantum = 1024);
+ nsresult AppendString(const char* string);
+ nsresult AppendBuffer(const char* buffer, uint32_t length);
+ void ResetWritePos() { m_bufferPos = 0; }
+ char* GetBuffer() { return m_buffer; }
+
+ protected:
+ char* m_buffer;
+ uint32_t m_bufferSize;
+ uint32_t
+ m_bufferPos; // write Pos in m_buffer - where the next byte should go.
+};
+
+/**
+ * nsMsgLineBuffer breaks up incoming data into lines.
+ * It accepts CRLF, CR or LF line endings.
+ *
+ * Data is fed in via BufferInput(). The virtual HandleLine() will be
+ * invoked for each line. The data passed to HandleLine() is verbatim,
+ * and will include whatever line endings were in the source data.
+ *
+ * Flush() should be called when the data is exhausted, to handle any
+ * leftover bytes in the buffer (e.g. if the data doesn't end with an EOL).
+ */
+class nsMsgLineBuffer : private nsByteArray {
+ public:
+ nsMsgLineBuffer();
+ virtual ~nsMsgLineBuffer();
+ nsresult BufferInput(const char* net_buffer, int32_t net_buffer_size);
+
+ /**
+ * HandleLine should be implemented by derived classes, to handle a line.
+ * The line will have whatever end-of-line characters were present in the
+ * source data (potentially none, if the data ends mid-line).
+ */
+ virtual nsresult HandleLine(const char* line, uint32_t line_length) = 0;
+
+ /**
+ * Flush processes any unprocessed data currently in the buffer. Should
+ * be called when the source data is exhausted.
+ */
+ nsresult Flush();
+};
+
+// I'm adding this utility class here for lack of a better place. This utility
+// class is similar to nsMsgLineBuffer except it works from an input stream. It
+// is geared towards efficiently parsing new lines out of a stream by storing
+// read but unprocessed bytes in a buffer. I envision the primary use of this to
+// be our mail protocols such as imap, news and pop which need to process line
+// by line data being returned in the form of a proxied stream from the server.
+
+class nsIInputStream;
+
+class nsMsgLineStreamBuffer {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(nsMsgLineStreamBuffer)
+
+ // aBufferSize -- size of the buffer you want us to use for buffering stream
+ // data
+ // aEndOfLinetoken -- The delimiter string to be used for determining the end
+ // of line. This allows us to parse platform specific end of
+ // line endings by making it a parameter.
+ // aAllocateNewLines -- true if you want calls to ReadNextLine to allocate new
+ // memory for the line.
+ // if false, the char * returned is just a ptr into the buffer.
+ // Subsequent calls to ReadNextLine will alter the data so your
+ // ptr only has a life time of a per call.
+ // aEatCRLFs -- true if you don't want to see the CRLFs on the lines
+ // returned by ReadNextLine.
+ // false if you do want to see them.
+ // aLineToken -- Specify the line token to look for, by default is LF ('\n')
+ // which cover as well CRLF. If lines are terminated with a CR
+ // only, you need to set aLineToken to CR ('\r')
+ nsMsgLineStreamBuffer(
+ uint32_t aBufferSize, bool aAllocateNewLines, bool aEatCRLFs = true,
+ char aLineToken = '\n'); // specify the size of the buffer you want the
+ // class to use....
+
+ // Caller must free the line returned using PR_Free
+ // aEndOfLinetoken -- delimiter used to denote the end of a line.
+ // aNumBytesInLine -- The number of bytes in the line returned
+ // aPauseForMoreData -- There is not enough data in the stream to make a line
+ // at this time...
+ char* ReadNextLine(nsIInputStream* aInputStream, uint32_t& aNumBytesInLine,
+ bool& aPauseForMoreData, nsresult* rv = nullptr,
+ bool addLineTerminator = false);
+ nsresult GrowBuffer(uint32_t desiredSize);
+ void ClearBuffer();
+ bool NextLineAvailable();
+
+ private:
+ virtual ~nsMsgLineStreamBuffer();
+
+ protected:
+ bool m_eatCRLFs;
+ bool m_allocateNewLines;
+ char* m_dataBuffer;
+ uint32_t m_dataBufferSize;
+ uint32_t m_startPos;
+ uint32_t m_numBytesInBuffer;
+ char m_lineToken;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp
new file mode 100644
index 0000000000..3796aeaef2
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp
@@ -0,0 +1,1070 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsIMsgAccountManager.h"
+#include "nsString.h"
+#include "nsILoadGroup.h"
+#include "nsIDocShell.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsIStreamListener.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "nsIFile.h"
+#include "prmem.h"
+#include <time.h>
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+#include "nsProxyRelease.h"
+#include "mozilla/Encoding.h"
+#include "nsDocShellLoadState.h"
+#include "nsContentUtils.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIChannel.h"
+
+nsMsgMailNewsUrl::nsMsgMailNewsUrl() {
+ // nsIURI specific state
+ m_runningUrl = false;
+ m_updatingFolder = false;
+ m_msgIsInLocalCache = false;
+ m_suppressErrorMsgs = false;
+ m_hasNormalizedOrigin = false; // SetSpecInternal() will set this correctly.
+ mMaxProgress = -1;
+}
+
+#define NOTIFY_URL_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter( \
+ mUrlListeners); \
+ while (iter.HasMore()) { \
+ nsCOMPtr<nsIUrlListener> listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+nsMsgMailNewsUrl::~nsMsgMailNewsUrl() {
+ // In IMAP this URL is created and destroyed on the imap thread,
+ // so we must ensure that releases of XPCOM objects (which might be
+ // implemented by non-threadsafe JS components) are released on the
+ // main thread.
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_baseURL", m_baseURL.forget());
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mMimeHeaders",
+ mMimeHeaders.forget());
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_searchSession",
+ m_searchSession.forget());
+
+ nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter(
+ mUrlListeners);
+ while (iter.HasMore()) {
+ nsCOMPtr<nsIUrlListener> listener = iter.GetNext();
+ if (listener)
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mUrlListeners",
+ listener.forget());
+ }
+}
+
+NS_IMPL_ADDREF(nsMsgMailNewsUrl)
+NS_IMPL_RELEASE(nsMsgMailNewsUrl)
+
+// We want part URLs to QI to nsIURIWithSpecialOrigin so we can give
+// them a "normalized" origin. URLs that already have a "normalized"
+// origin should not QI to nsIURIWithSpecialOrigin.
+NS_INTERFACE_MAP_BEGIN(nsMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIURL)
+ NS_INTERFACE_MAP_ENTRY(nsIURI)
+ NS_INTERFACE_MAP_ENTRY(nsISerializable)
+ NS_INTERFACE_MAP_ENTRY(nsIClassInfo)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIURIWithSpecialOrigin,
+ m_hasNormalizedOrigin)
+NS_INTERFACE_MAP_END
+
+//--------------------------
+// Support for serialization
+//--------------------------
+// nsMsgMailNewsUrl is only partly serialized by serializing the "base URL"
+// which is an nsStandardURL, or by only serializing the Spec. This may
+// cause problems in the future. See bug 1512356 and bug 1515337 for details,
+// follow-up in bug 1512698.
+
+NS_IMETHODIMP_(void)
+nsMsgMailNewsUrl::Serialize(mozilla::ipc::URIParams& aParams) {
+ m_baseURL->Serialize(aParams);
+}
+
+//----------------------------
+// Support for nsISerializable
+//----------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::Read(nsIObjectInputStream* stream) {
+ nsAutoCString urlstr;
+ nsresult rv = NS_ReadOptionalCString(stream, urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIURI> url;
+ rv = ioService->NewURI(urlstr, nullptr, nullptr, getter_AddRefs(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_baseURL = do_QueryInterface(url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Write(nsIObjectOutputStream* stream) {
+ nsAutoCString urlstr;
+ nsresult rv = m_baseURL->GetSpec(urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_WriteOptionalStringZ(stream, urlstr.get());
+}
+
+//-------------------------
+// Support for nsIClassInfo
+//-------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::GetInterfaces(nsTArray<nsIID>& array) {
+ array.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetScriptableHelper(
+ nsIXPCScriptable** _retval) {
+ *_retval = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetContractID(nsACString& aContractID) {
+ aContractID.SetIsVoid(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassDescription(
+ nsACString& aClassDescription) {
+ aClassDescription.SetIsVoid(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassID(nsCID** aClassID) {
+ *aClassID = (nsCID*)moz_xmalloc(sizeof(nsCID));
+ return GetClassIDNoAlloc(*aClassID);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFlags(uint32_t* aFlags) {
+ *aFlags = 0;
+ return NS_OK;
+}
+
+#define NS_MSGMAILNEWSURL_CID \
+ { \
+ 0x3fdae3ab, 0x4ac1, 0x4ad4, { \
+ 0xb2, 0x8a, 0x28, 0xd0, 0xfa, 0x36, 0x39, 0x29 \
+ } \
+ }
+static NS_DEFINE_CID(kNS_MSGMAILNEWSURL_CID, NS_MSGMAILNEWSURL_CID);
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassIDNoAlloc(nsCID* aClassIDNoAlloc) {
+ *aClassIDNoAlloc = kNS_MSGMAILNEWSURL_CID;
+ return NS_OK;
+}
+
+//------------------------------------
+// Support for nsIURIWithSpecialOrigin
+//------------------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::GetOrigin(nsIURI** aOrigin) {
+ MOZ_ASSERT(m_hasNormalizedOrigin,
+ "nsMsgMailNewsUrl::GetOrigin() can only be called for URLs with "
+ "normalized spec");
+
+ if (!m_normalizedOrigin) {
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl;
+ QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl));
+
+ nsAutoCString spec;
+ if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(spec))) {
+ MOZ_ASSERT(false, "Can't get normalized spec");
+ // just use the normal spec.
+ GetSpec(spec);
+ }
+
+ nsresult rv = NS_NewURI(getter_AddRefs(m_normalizedOrigin), spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aOrigin = m_normalizedOrigin);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIMsgMailNewsUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgMailNewsUrl::GetUrlState(bool* aRunningUrl) {
+ if (aRunningUrl) *aRunningUrl = m_runningUrl;
+
+ return NS_OK;
+}
+
+nsresult nsMsgMailNewsUrl::SetUrlState(bool aRunningUrl, nsresult aExitCode) {
+ // if we already knew this running state, return, unless the url was aborted
+ if (m_runningUrl == aRunningUrl && aExitCode != NS_MSG_ERROR_URL_ABORTED) {
+ return NS_OK;
+ }
+ m_runningUrl = aRunningUrl;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+
+ // put this back - we need it for urls that don't run through the doc loader
+ if (NS_SUCCEEDED(GetStatusFeedback(getter_AddRefs(statusFeedback))) &&
+ statusFeedback) {
+ if (m_runningUrl)
+ statusFeedback->StartMeteors();
+ else {
+ statusFeedback->ShowProgress(0);
+ statusFeedback->StopMeteors();
+ }
+ }
+
+ if (m_runningUrl) {
+ NOTIFY_URL_LISTENERS(OnStartRunningUrl, (this));
+ } else {
+ NOTIFY_URL_LISTENERS(OnStopRunningUrl, (this, aExitCode));
+ mUrlListeners.Clear();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::RegisterListener(nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aUrlListener);
+ mUrlListeners.AppendElement(aUrlListener);
+ return NS_OK;
+}
+
+nsresult nsMsgMailNewsUrl::UnRegisterListener(nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aUrlListener);
+
+ // Due to the way mailnews is structured, some listeners attempt to remove
+ // themselves twice. This may in fact be an error in the coding, however
+ // if they didn't do it as they do currently, then they could fail to remove
+ // their listeners.
+ mUrlListeners.RemoveElement(aUrlListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetServer(
+ nsIMsgIncomingServer** aIncomingServer) {
+ // mscott --> we could cache a copy of the server here....but if we did, we
+ // run the risk of leaking the server if any single url gets leaked....of
+ // course that shouldn't happen...but it could. so i'm going to look it up
+ // every time and we can look at caching it later.
+
+ nsresult rv;
+
+ nsAutoCString urlstr;
+ rv = m_baseURL->GetSpec(urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(urlstr)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ rv = GetScheme(scheme);
+ if (NS_SUCCEEDED(rv)) {
+ if (scheme.EqualsLiteral("pop")) scheme.AssignLiteral("pop3");
+ // we use "nntp" in the server list so translate it here.
+ if (scheme.EqualsLiteral("news")) scheme.AssignLiteral("nntp");
+ rv = NS_MutateURI(url).SetScheme(scheme).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->FindServerByURI(url, aIncomingServer);
+ if (!*aIncomingServer && scheme.EqualsLiteral("imap")) {
+ // look for any imap server with this host name so clicking on
+ // other users folder urls will work. We could override this method
+ // for imap urls, or we could make caching of servers work and
+ // just set the server in the imap code for this case.
+ rv = NS_MutateURI(url).SetUserPass(EmptyCString()).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, aIncomingServer);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+ *aMsgWindow = nullptr;
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ msgWindow.forget(aMsgWindow);
+ return *aMsgWindow ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgWindow(nsIMsgWindow* aMsgWindow) {
+#ifdef DEBUG_David_Bienvenu
+ NS_ASSERTION(aMsgWindow || !m_msgWindowWeak,
+ "someone crunching non-null msg window");
+#endif
+ m_msgWindowWeak = do_GetWeakReference(aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetStatusFeedback(
+ nsIMsgStatusFeedback** aMsgFeedback) {
+ // note: it is okay to return a null status feedback and not return an error
+ // it's possible the url really doesn't have status feedback
+ *aMsgFeedback = nullptr;
+ if (!m_statusFeedbackWeak) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) msgWindow->GetStatusFeedback(aMsgFeedback);
+ } else {
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback(
+ do_QueryReferent(m_statusFeedbackWeak));
+ statusFeedback.forget(aMsgFeedback);
+ }
+ return *aMsgFeedback ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetStatusFeedback(
+ nsIMsgStatusFeedback* aMsgFeedback) {
+ if (aMsgFeedback) m_statusFeedbackWeak = do_GetWeakReference(aMsgFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMaxProgress(int64_t* aMaxProgress) {
+ *aMaxProgress = mMaxProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMaxProgress(int64_t aMaxProgress) {
+ mMaxProgress = aMaxProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ *aLoadGroup = nullptr;
+ // note: it is okay to return a null load group and not return an error
+ // it's possible the url really doesn't have load group
+ nsCOMPtr<nsILoadGroup> loadGroup(do_QueryReferent(m_loadGroupWeak));
+ if (!loadGroup) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) {
+ // XXXbz This is really weird... why are we getting some
+ // random loadgroup we're not really a part of?
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ loadGroup = do_GetInterface(docShell);
+ m_loadGroupWeak = do_GetWeakReference(loadGroup);
+ }
+ }
+ loadGroup.forget(aLoadGroup);
+ return *aLoadGroup ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUpdatingFolder(bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ *aResult = m_updatingFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetUpdatingFolder(bool updatingFolder) {
+ m_updatingFolder = updatingFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgIsInLocalCache(bool* aMsgIsInLocalCache) {
+ NS_ENSURE_ARG(aMsgIsInLocalCache);
+ *aMsgIsInLocalCache = m_msgIsInLocalCache;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgIsInLocalCache(bool aMsgIsInLocalCache) {
+ m_msgIsInLocalCache = aMsgIsInLocalCache;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSuppressErrorMsgs(bool* aSuppressErrorMsgs) {
+ NS_ENSURE_ARG(aSuppressErrorMsgs);
+ *aSuppressErrorMsgs = m_suppressErrorMsgs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSuppressErrorMsgs(bool aSuppressErrorMsgs) {
+ m_suppressErrorMsgs = aSuppressErrorMsgs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorCode(nsACString& aErrorCode) {
+ aErrorCode = m_errorCode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorCode(const nsACString& aErrorCode) {
+ m_errorCode.Assign(aErrorCode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorMessage(nsAString& aErrorMessage) {
+ aErrorMessage = m_errorMessage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorMessage(
+ const nsAString& aErrorMessage) {
+ m_errorMessage.Assign(aErrorMessage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSeeOtherURI(const nsACString& aSeeOtherURI) {
+ m_seeOtherURI.Assign(aSeeOtherURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSeeOtherURI(nsACString& aSeeOtherURI) {
+ aSeeOtherURI = m_seeOtherURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::IsUrlType(uint32_t type, bool* isType) {
+ // base class doesn't know about any specific types
+ NS_ENSURE_ARG(isType);
+ *isType = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSearchSession(
+ nsIMsgSearchSession* aSearchSession) {
+ if (aSearchSession) m_searchSession = aSearchSession;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSearchSession(
+ nsIMsgSearchSession** aSearchSession) {
+ NS_ENSURE_ARG(aSearchSession);
+ NS_IF_ADDREF(*aSearchSession = m_searchSession);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// End nsIMsgMailNewsUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIURI support
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSpec(nsACString& aSpec) {
+ return m_baseURL->GetSpec(aSpec);
+}
+
+nsresult nsMsgMailNewsUrl::CreateURL(const nsACString& aSpec, nsIURL** aURL) {
+ nsCOMPtr<nsIURL> url;
+ nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(aSpec)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ url.forget(aURL);
+ return NS_OK;
+}
+
+#define FILENAME_PART_LEN 10
+
+nsresult nsMsgMailNewsUrl::SetSpecInternal(const nsACString& aSpec) {
+ nsAutoCString spec(aSpec);
+ // Parse out "filename" attribute if present.
+ char *start, *end;
+ start = PL_strcasestr(spec.BeginWriting(), "?filename=");
+ if (!start) start = PL_strcasestr(spec.BeginWriting(), "&filename=");
+ if (start) { // Make sure we only get our own value.
+ end = PL_strcasestr((char*)(start + FILENAME_PART_LEN), "&");
+ if (end) {
+ *end = 0;
+ mAttachmentFileName = start + FILENAME_PART_LEN;
+ *end = '&';
+ } else
+ mAttachmentFileName = start + FILENAME_PART_LEN;
+ }
+
+ // Now, set the rest.
+ nsresult rv = CreateURL(aSpec, getter_AddRefs(m_baseURL));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check whether the URL is in normalized form.
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl;
+ QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl));
+
+ nsAutoCString normalizedSpec;
+ if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(normalizedSpec))) {
+ // If we can't get the normalized spec, never QI this to
+ // nsIURIWithSpecialOrigin.
+ m_hasNormalizedOrigin = false;
+ } else {
+ m_hasNormalizedOrigin = !spec.Equals(normalizedSpec);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetPrePath(aPrePath);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetScheme(nsACString& aScheme) {
+ return m_baseURL->GetScheme(aScheme);
+}
+
+nsresult nsMsgMailNewsUrl::SetScheme(const nsACString& aScheme) {
+ return NS_MutateURI(m_baseURL).SetScheme(aScheme).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUserPass(nsACString& aUserPass) {
+ return m_baseURL->GetUserPass(aUserPass);
+}
+
+nsresult nsMsgMailNewsUrl::SetUserPass(const nsACString& aUserPass) {
+ return NS_MutateURI(m_baseURL).SetUserPass(aUserPass).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUsername(nsACString& aUsername) {
+ /* note: this will return an escaped string */
+ return m_baseURL->GetUsername(aUsername);
+}
+
+nsresult nsMsgMailNewsUrl::SetUsername(const nsACString& aUsername) {
+ return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetUsernameInternal(const nsACString& aUsername) {
+ return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPassword(nsACString& aPassword) {
+ return m_baseURL->GetPassword(aPassword);
+}
+
+nsresult nsMsgMailNewsUrl::SetPassword(const nsACString& aPassword) {
+ return NS_MutateURI(m_baseURL).SetPassword(aPassword).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetHostPort(nsACString& aHostPort) {
+ return m_baseURL->GetHostPort(aHostPort);
+}
+
+nsresult nsMsgMailNewsUrl::SetHostPort(const nsACString& aHostPort) {
+ return NS_MutateURI(m_baseURL).SetHostPort(aHostPort).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetHost(nsACString& aHost) {
+ return m_baseURL->GetHost(aHost);
+}
+
+nsresult nsMsgMailNewsUrl::SetHost(const nsACString& aHost) {
+ return NS_MutateURI(m_baseURL).SetHost(aHost).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPort(int32_t* aPort) {
+ return m_baseURL->GetPort(aPort);
+}
+
+nsresult nsMsgMailNewsUrl::SetPort(int32_t aPort) {
+ return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetPortInternal(int32_t aPort) {
+ return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPathQueryRef(nsACString& aPath) {
+ return m_baseURL->GetPathQueryRef(aPath);
+}
+
+nsresult nsMsgMailNewsUrl::SetPathQueryRef(const nsACString& aPath) {
+ return NS_MutateURI(m_baseURL).SetPathQueryRef(aPath).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHost(nsACString& aHostA) {
+ return m_baseURL->GetAsciiHost(aHostA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHostPort(nsACString& aHostPortA) {
+ return m_baseURL->GetAsciiHostPort(aHostPortA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiSpec(nsACString& aSpecA) {
+ return m_baseURL->GetAsciiSpec(aSpecA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetBaseURI(nsIURI** aBaseURI) {
+ NS_ENSURE_ARG_POINTER(aBaseURI);
+ return m_baseURL->QueryInterface(NS_GET_IID(nsIURI), (void**)aBaseURI);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Equals(nsIURI* other, bool* _retval) {
+ // The passed-in URI might be a mail news url. Pass our inner URL to its
+ // Equals method. The other mail news url will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->Equals(m_baseURL, _retval);
+
+ return m_baseURL->Equals(other, _retval);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::EqualsExceptRef(nsIURI* other, bool* result) {
+ // The passed-in URI might be a mail news url. Pass our inner URL to its
+ // Equals method. The other mail news url will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->EqualsExceptRef(m_baseURL, result);
+
+ return m_baseURL->EqualsExceptRef(other, result);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetSpecIgnoringRef(nsACString& result) {
+ return m_baseURL->GetSpecIgnoringRef(result);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplaySpec(nsACString& aUnicodeSpec) {
+ return m_baseURL->GetDisplaySpec(aUnicodeSpec);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayHostPort(nsACString& aHostPort) {
+ return m_baseURL->GetDisplayHostPort(aHostPort);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayHost(nsACString& aHost) {
+ return m_baseURL->GetDisplayHost(aHost);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetDisplayPrePath(aPrePath);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetHasRef(bool* result) {
+ return m_baseURL->GetHasRef(result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SchemeIs(const char* aScheme, bool* _retval) {
+ return m_baseURL->SchemeIs(aScheme, _retval);
+}
+
+nsresult nsMsgMailNewsUrl::Clone(nsIURI** _retval) {
+ nsresult rv;
+ nsAutoCString urlSpec;
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ rv = GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> newUri;
+ rv = ioService->NewURI(urlSpec, nullptr, nullptr, getter_AddRefs(newUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // add the msg window to the cloned url
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgMailNewsUrl = do_QueryInterface(newUri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgMailNewsUrl->SetMsgWindow(msgWindow);
+ }
+
+ newUri.forget(_retval);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Resolve(const nsACString& relativePath,
+ nsACString& result) {
+ // only resolve anchor urls....i.e. urls which start with '#' against the
+ // mailnews url... everything else shouldn't be resolved against mailnews
+ // urls.
+ nsresult rv = NS_OK;
+
+ if (relativePath.IsEmpty()) {
+ // Return base URL.
+ rv = GetSpec(result);
+ } else if (!relativePath.IsEmpty() &&
+ relativePath.First() == '#') // an anchor
+ {
+ rv = m_baseURL->Resolve(relativePath, result);
+ } else {
+ // if relativePath is a complete url with it's own scheme then allow it...
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ nsAutoCString scheme;
+
+ rv = ioService->ExtractScheme(relativePath, scheme);
+ // if we have a fully qualified scheme then pass the relative path back as
+ // the result
+ if (NS_SUCCEEDED(rv) && !scheme.IsEmpty()) {
+ result = relativePath;
+ rv = NS_OK;
+ } else {
+ result.Truncate();
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetDirectory(nsACString& aDirectory) {
+ return m_baseURL->GetDirectory(aDirectory);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileName(nsACString& aFileName) {
+ if (!mAttachmentFileName.IsEmpty()) {
+ aFileName = mAttachmentFileName;
+ return NS_OK;
+ }
+ return m_baseURL->GetFileName(aFileName);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileBaseName(nsACString& aFileBaseName) {
+ return m_baseURL->GetFileBaseName(aFileBaseName);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileExtension(nsACString& aFileExtension) {
+ if (!mAttachmentFileName.IsEmpty()) {
+ int32_t pos = mAttachmentFileName.RFindChar(char16_t('.'));
+ if (pos > 0)
+ aFileExtension =
+ Substring(mAttachmentFileName, pos + 1 /* skip the '.' */);
+ return NS_OK;
+ }
+ return m_baseURL->GetFileExtension(aFileExtension);
+}
+
+nsresult nsMsgMailNewsUrl::SetFileNameInternal(const nsACString& aFileName) {
+ mAttachmentFileName = aFileName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetQuery(nsACString& aQuery) {
+ return m_baseURL->GetQuery(aQuery);
+}
+
+nsresult nsMsgMailNewsUrl::SetQuery(const nsACString& aQuery) {
+ return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetQueryInternal(const nsACString& aQuery) {
+ return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetQueryWithEncoding(
+ const nsACString& aQuery, const mozilla::Encoding* aEncoding) {
+ return NS_MutateURI(m_baseURL)
+ .SetQueryWithEncoding(aQuery, aEncoding)
+ .Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetRef(nsACString& aRef) {
+ return m_baseURL->GetRef(aRef);
+}
+
+nsresult nsMsgMailNewsUrl::SetRef(const nsACString& aRef) {
+ return NS_MutateURI(m_baseURL).SetRef(aRef).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFilePath(nsACString& o_DirFile) {
+ return m_baseURL->GetFilePath(o_DirFile);
+}
+
+nsresult nsMsgMailNewsUrl::SetFilePath(const nsACString& i_DirFile) {
+ return NS_MutateURI(m_baseURL).SetFilePath(i_DirFile).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetCommonBaseSpec(nsIURI* uri2,
+ nsACString& result) {
+ return m_baseURL->GetCommonBaseSpec(uri2, result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetRelativeSpec(nsIURI* uri2,
+ nsACString& result) {
+ return m_baseURL->GetRelativeSpec(uri2, result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMemCacheEntry(nsICacheEntry* memCacheEntry) {
+ m_memCacheEntry = memCacheEntry;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMemCacheEntry(
+ nsICacheEntry** memCacheEntry) {
+ NS_ENSURE_ARG(memCacheEntry);
+ nsresult rv = NS_OK;
+
+ if (m_memCacheEntry) {
+ NS_ADDREF(*memCacheEntry = m_memCacheEntry);
+ } else {
+ *memCacheEntry = nullptr;
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMimeHeaders(nsIMimeHeaders** mimeHeaders) {
+ NS_ENSURE_ARG_POINTER(mimeHeaders);
+ NS_IF_ADDREF(*mimeHeaders = mMimeHeaders);
+ return (mMimeHeaders) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMimeHeaders(nsIMimeHeaders* mimeHeaders) {
+ mMimeHeaders = mimeHeaders;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::LoadURI(nsIDocShell* docShell,
+ uint32_t aLoadFlags) {
+ NS_ENSURE_ARG_POINTER(docShell);
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(this);
+ loadState->SetLoadFlags(aLoadFlags);
+ loadState->SetLoadType(MAKE_LOAD_TYPE(LOAD_NORMAL, aLoadFlags));
+ loadState->SetFirstParty(false);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ return docShell->LoadURI(loadState, false);
+}
+
+#define SAVE_BUF_SIZE FILE_IO_BUFFER_SIZE
+class nsMsgSaveAsListener : public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsMsgSaveAsListener(nsIFile* aFile, bool addDummyEnvelope);
+ nsresult SetupMsgWriteStream(nsIFile* aFile, bool addDummyEnvelope);
+
+ protected:
+ virtual ~nsMsgSaveAsListener();
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ nsCOMPtr<nsIFile> m_outputFile;
+ bool m_addDummyEnvelope;
+ bool m_writtenData;
+ uint32_t m_leftOver;
+ char m_dataBuffer[SAVE_BUF_SIZE +
+ 1]; // temporary buffer for this save operation
+};
+
+NS_IMPL_ISUPPORTS(nsMsgSaveAsListener, nsIStreamListener, nsIRequestObserver)
+
+nsMsgSaveAsListener::nsMsgSaveAsListener(nsIFile* aFile,
+ bool addDummyEnvelope) {
+ m_outputFile = aFile;
+ m_writtenData = false;
+ m_addDummyEnvelope = addDummyEnvelope;
+ m_leftOver = 0;
+}
+
+nsMsgSaveAsListener::~nsMsgSaveAsListener() {}
+
+NS_IMETHODIMP nsMsgSaveAsListener::OnStartRequest(nsIRequest* request) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSaveAsListener::OnStopRequest(nsIRequest* request, nsresult aStatus) {
+ if (m_outputStream) {
+ m_outputStream->Flush();
+ m_outputStream->Close();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSaveAsListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStream,
+ uint64_t srcOffset,
+ uint32_t count) {
+ nsresult rv;
+ uint64_t available;
+ rv = inStream->Available(&available);
+ if (!m_writtenData) {
+ m_writtenData = true;
+ rv = SetupMsgWriteStream(m_outputFile, m_addDummyEnvelope);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bool useCanonicalEnding = false;
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(uri);
+ if (msgUrl) msgUrl->GetCanonicalLineEnding(&useCanonicalEnding);
+
+ const char* lineEnding = (useCanonicalEnding) ? CRLF : MSG_LINEBREAK;
+ uint32_t lineEndingLength = (useCanonicalEnding) ? 2 : MSG_LINEBREAK_LEN;
+
+ uint32_t readCount, maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+ uint32_t writeCount;
+ char *start, *end, lastCharInPrevBuf = '\0';
+ uint32_t linebreak_len = 0;
+
+ while (count > 0) {
+ if (count < maxReadCount) maxReadCount = count;
+ rv = inStream->Read(m_dataBuffer + m_leftOver, maxReadCount, &readCount);
+ if (NS_FAILED(rv)) return rv;
+
+ m_leftOver += readCount;
+ m_dataBuffer[m_leftOver] = '\0';
+
+ start = m_dataBuffer;
+ // make sure we don't insert another LF, accidentally, by ignoring
+ // second half of CRLF spanning blocks.
+ if (lastCharInPrevBuf == '\r' && *start == '\n') start++;
+
+ end = PL_strpbrk(start, "\r\n");
+ if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1;
+
+ count -= readCount;
+ maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+
+ if (!end && count > maxReadCount)
+ // must be a very very long line; sorry cannot handle it
+ return NS_ERROR_FAILURE;
+
+ while (start && end) {
+ if (m_outputStream && PL_strncasecmp(start, "X-Mozilla-Status:", 17) &&
+ PL_strncasecmp(start, "X-Mozilla-Status2:", 18) &&
+ PL_strncmp(start, "From - ", 7)) {
+ rv = m_outputStream->Write(start, end - start, &writeCount);
+ nsresult tmp =
+ m_outputStream->Write(lineEnding, lineEndingLength, &writeCount);
+ if (NS_FAILED(tmp)) {
+ rv = tmp;
+ }
+ }
+ start = end + linebreak_len;
+ if (start >= m_dataBuffer + m_leftOver) {
+ maxReadCount = SAVE_BUF_SIZE;
+ m_leftOver = 0;
+ break;
+ }
+ end = PL_strpbrk(start, "\r\n");
+ if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1;
+ if (start && !end) {
+ m_leftOver -= (start - m_dataBuffer);
+ memcpy(m_dataBuffer, start,
+ m_leftOver + 1); // including null
+ maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+ }
+ }
+ if (NS_FAILED(rv)) return rv;
+ if (end) lastCharInPrevBuf = *end;
+ }
+ return rv;
+
+ // rv = m_outputStream->WriteFrom(inStream, std::min(available, count),
+ // &bytesWritten);
+}
+
+nsresult nsMsgSaveAsListener::SetupMsgWriteStream(nsIFile* aFile,
+ bool addDummyEnvelope) {
+ // If the file already exists, delete it, but do this before
+ // getting the outputstream.
+ // Due to bug 328027, the nsSaveMsgListener created in
+ // nsMessenger::SaveAs now opens the stream on the nsIFile
+ // object, thus creating an empty file. Actual save operations for
+ // IMAP and NNTP use this nsMsgSaveAsListener here, though, so we
+ // have to close the stream before deleting the file, else data
+ // would still be written happily into a now non-existing file.
+ // (Windows doesn't care, btw, just unixoids do...)
+ aFile->Remove(false);
+
+ nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream),
+ aFile, -1, 0666);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_outputStream && addDummyEnvelope) {
+ nsAutoCString result;
+ uint32_t writeCount;
+
+ time_t now = time((time_t*)0);
+ char* ct = ctime(&now);
+ // Remove the ending new-line character.
+ ct[24] = '\0';
+ result = "From - ";
+ result += ct;
+ result += MSG_LINEBREAK;
+ m_outputStream->Write(result.get(), result.Length(), &writeCount);
+
+ result = "X-Mozilla-Status: 0001";
+ result += MSG_LINEBREAK;
+ result += "X-Mozilla-Status2: 00000000";
+ result += MSG_LINEBREAK;
+ m_outputStream->Write(result.get(), result.Length(), &writeCount);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSaveAsListener(
+ bool addDummyEnvelope, nsIFile* aFile, nsIStreamListener** aSaveListener) {
+ NS_ENSURE_ARG_POINTER(aSaveListener);
+ nsMsgSaveAsListener* saveAsListener =
+ new nsMsgSaveAsListener(aFile, addDummyEnvelope);
+ return saveAsListener->QueryInterface(NS_GET_IID(nsIStreamListener),
+ (void**)aSaveListener);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::SetFailedSecInfo(nsITransportSecurityInfo* secInfo) {
+ mFailedSecInfo = secInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFailedSecInfo(
+ nsITransportSecurityInfo** secInfo) {
+ NS_ENSURE_ARG_POINTER(secInfo);
+ NS_IF_ADDREF(*secInfo = mFailedSecInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetFolder(nsIMsgFolder* /* aFolder */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFolder(nsIMsgFolder** /* aFolder */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetIsMessageUri(bool* aIsMessageUri) {
+ NS_ENSURE_ARG(aIsMessageUri);
+ nsAutoCString scheme;
+ m_baseURL->GetScheme(scheme);
+ *aIsMessageUri = StringEndsWith(scheme, "-message"_ns);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgMailNewsUrl::Mutator, nsIURISetters, nsIURIMutator)
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::Mutate(nsIURIMutator** aMutator) {
+ RefPtr<nsMsgMailNewsUrl::Mutator> mutator = new nsMsgMailNewsUrl::Mutator();
+ nsresult rv = mutator->InitFromURI(this);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mutator.forget(aMutator);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.h b/comm/mailnews/base/src/nsMsgMailNewsUrl.h
new file mode 100644
index 0000000000..21d56e8e65
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgMailNewsUrl_h___
+#define nsMsgMailNewsUrl_h___
+
+#include "msgCore.h"
+#include "nscore.h"
+#include "nsISupports.h"
+#include "nsIUrlListener.h"
+#include "nsTObserverArray.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIMimeHeaders.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIURL.h"
+#include "nsIURIWithSpecialOrigin.h"
+#include "nsILoadGroup.h"
+#include "nsIMsgSearchSession.h"
+#include "nsICacheEntry.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsString.h"
+#include "nsIURIMutator.h"
+#include "nsISerializable.h"
+#include "nsIClassInfo.h"
+#include "nsITransportSecurityInfo.h"
+
+///////////////////////////////////////////////////////////////////////////////////
+// Okay, I found that all of the mail and news url interfaces needed to support
+// several common interfaces (in addition to those provided through nsIURI).
+// So I decided to group them all in this implementation so we don't have to
+// duplicate the code.
+//
+//////////////////////////////////////////////////////////////////////////////////
+
+class NS_MSG_BASE nsMsgMailNewsUrl : public nsIMsgMailNewsUrl,
+ public nsIURIWithSpecialOrigin,
+ public nsISerializable,
+ public nsIClassInfo {
+ public:
+ nsMsgMailNewsUrl();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGMAILNEWSURL
+ NS_DECL_NSIURI
+ NS_DECL_NSIURL
+ NS_DECL_NSIURIWITHSPECIALORIGIN
+ NS_DECL_NSISERIALIZABLE
+ NS_DECL_NSICLASSINFO
+
+ protected:
+ virtual nsresult Clone(nsIURI** _retval);
+ virtual nsresult SetScheme(const nsACString& aScheme);
+ virtual nsresult SetUserPass(const nsACString& aUserPass);
+ virtual nsresult SetUsername(const nsACString& aUsername);
+ virtual nsresult SetPassword(const nsACString& aPassword);
+ virtual nsresult SetHostPort(const nsACString& aHostPort);
+ virtual nsresult SetHost(const nsACString& aHost);
+ virtual nsresult SetPort(int32_t aPort);
+ virtual nsresult SetPathQueryRef(const nsACString& aPath);
+ virtual nsresult SetRef(const nsACString& aRef);
+ virtual nsresult SetFilePath(const nsACString& aFilePath);
+ virtual nsresult SetQuery(const nsACString& aQuery);
+ virtual nsresult SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding);
+ virtual nsresult CreateURL(const nsACString& aSpec,
+ nsIURL** aURL); // nsMailboxUrl overrides this.
+
+ public:
+ class Mutator : public nsIURIMutator,
+ public BaseURIMutator<nsMsgMailNewsUrl> {
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
+
+ NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD Finalize(nsIURI** aURI) override {
+ mURI.forget(aURI);
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetSpec(const nsACString& aSpec,
+ nsIURIMutator** aMutator) override {
+ if (aMutator) NS_ADDREF(*aMutator = this);
+ return InitFromSpec(aSpec);
+ }
+
+ explicit Mutator() {}
+
+ private:
+ virtual ~Mutator() {}
+
+ friend class nsMsgMailNewsUrl;
+ };
+ friend BaseURIMutator<nsMsgMailNewsUrl>;
+
+ protected:
+ virtual ~nsMsgMailNewsUrl();
+
+ nsCOMPtr<nsIURL> m_baseURL;
+ nsCOMPtr<nsIURI> m_normalizedOrigin;
+ nsWeakPtr m_statusFeedbackWeak;
+ nsWeakPtr m_msgWindowWeak;
+ nsWeakPtr m_loadGroupWeak;
+ nsCOMPtr<nsIMimeHeaders> mMimeHeaders;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ nsCOMPtr<nsICacheEntry> m_memCacheEntry;
+ nsCString m_errorCode;
+ nsCString m_seeOtherURI;
+ nsString m_errorMessage;
+ nsString m_errorParameters;
+ int64_t mMaxProgress;
+ bool m_runningUrl;
+ bool m_updatingFolder;
+ bool m_msgIsInLocalCache;
+ bool m_suppressErrorMsgs;
+ bool m_hasNormalizedOrigin;
+
+ // the following field is really a bit of a hack to make
+ // open attachments work. The external applications code sometimes tries to
+ // figure out the right handler to use by looking at the file extension of the
+ // url we are trying to load. Unfortunately, the attachment file name really
+ // isn't part of the url string....so we'll store it here...and if the url we
+ // are running is an attachment url, we'll set it here. Then when the helper
+ // apps code asks us for it, we'll return the right value.
+ nsCString mAttachmentFileName;
+
+ nsTObserverArray<nsCOMPtr<nsIUrlListener> > mUrlListeners;
+
+ // Security info from the socket transport (if any), after a failed operation.
+ // Here so that urlListeners can access and handle bad certificates in
+ // their OnStopRunningUrl() callback.
+ nsCOMPtr<nsITransportSecurityInfo> mFailedSecInfo;
+};
+
+#endif /* nsMsgMailNewsUrl_h___ */
diff --git a/comm/mailnews/base/src/nsMsgMailSession.cpp b/comm/mailnews/base/src/nsMsgMailSession.cpp
new file mode 100644
index 0000000000..ac34d5d487
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailSession.cpp
@@ -0,0 +1,671 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMsgMailSession.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIChromeRegistry.h"
+#include "nsIDirectoryService.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsPIDOMWindow.h"
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "nsIObserverService.h"
+#include "nsIAppStartup.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIAppShellService.h"
+#include "nsAppShellCID.h"
+#include "nsIWindowMediator.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "prcmon.h"
+#include "nsThreadUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIProperties.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Components.h"
+#include "nsFocusManager.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+NS_IMPL_ISUPPORTS(nsMsgMailSession, nsIMsgMailSession, nsIFolderListener)
+
+nsMsgMailSession::nsMsgMailSession() {}
+
+nsMsgMailSession::~nsMsgMailSession() { Shutdown(); }
+
+nsresult nsMsgMailSession::Init() {
+ // Ensures the shutdown service is initialised
+ nsresult rv;
+ nsCOMPtr<nsIMsgShutdownService> shutdownService =
+ do_GetService("@mozilla.org/messenger/msgshutdownservice;1", &rv);
+ return rv;
+}
+
+nsresult nsMsgMailSession::Shutdown() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgMailSession::AddFolderListener(nsIFolderListener* aListener,
+ uint32_t aNotifyFlags) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ // we don't care about the notification flags for equivalence purposes
+ size_t index = mListeners.IndexOf(aListener);
+ NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener");
+ if (index == size_t(-1)) {
+ folderListener newListener(aListener, aNotifyFlags);
+ mListeners.AppendElement(newListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::RemoveFolderListener(
+ nsIFolderListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+#define NOTIFY_FOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<folderListener>::ForwardIterator iter(mListeners); \
+ while (iter.HasMore()) { \
+ const folderListener& fL = iter.GetNext(); \
+ if (fL.mNotifyFlags & nsIFolderListener::propertyflag_) \
+ fL.mListener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ const nsACString& aOldValue,
+ const nsACString& aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(propertyChanged, OnFolderPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderUnicharPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(unicharPropertyChanged,
+ OnFolderUnicharPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderIntPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ int64_t aOldValue,
+ int64_t aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(intPropertyChanged, OnFolderIntPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderBoolPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ bool aOldValue, bool aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(boolPropertyChanged, OnFolderBoolPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderPropertyFlagChanged(nsIMsgDBHdr* aItem,
+ const nsACString& aProperty,
+ uint32_t aOldValue,
+ uint32_t aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(propertyFlagChanged, OnFolderPropertyFlagChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ NOTIFY_FOLDER_LISTENERS(added, OnFolderAdded, (parent, child));
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgMailSession::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ NOTIFY_FOLDER_LISTENERS(added, OnMessageAdded, (parent, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ NOTIFY_FOLDER_LISTENERS(removed, OnFolderRemoved, (parent, child));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ NOTIFY_FOLDER_LISTENERS(removed, OnMessageRemoved, (parent, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderEvent(nsIMsgFolder* aFolder,
+ const nsACString& aEvent) {
+ NOTIFY_FOLDER_LISTENERS(event, OnFolderEvent, (aFolder, aEvent));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::AddUserFeedbackListener(
+ nsIMsgUserFeedbackListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ size_t index = mFeedbackListeners.IndexOf(aListener);
+ NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener");
+ if (index == size_t(-1)) mFeedbackListeners.AppendElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::RemoveUserFeedbackListener(
+ nsIMsgUserFeedbackListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mFeedbackListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::AlertUser(const nsAString& aMessage,
+ nsIMsgMailNewsUrl* aUrl) {
+ bool listenersNotified = false;
+ nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener>>::ForwardIterator iter(
+ mFeedbackListeners);
+ nsCOMPtr<nsIMsgUserFeedbackListener> listener;
+
+ while (iter.HasMore()) {
+ bool notified = false;
+ listener = iter.GetNext();
+ listener->OnAlert(aMessage, aUrl, &notified);
+ listenersNotified = listenersNotified || notified;
+ }
+
+ // If the listeners notified the user, then we don't need to. Also exit if
+ // aUrl is null because we won't have a nsIMsgWindow in that case.
+ if (listenersNotified || !aUrl) return NS_OK;
+
+ // If the url hasn't got a message window, then the error was a generated as a
+ // result of background activity (e.g. autosync, biff, etc), and hence we
+ // shouldn't prompt either.
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ aUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+
+ if (!msgWindow) return NS_OK;
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ msgWindow->GetDomWindow(getter_AddRefs(domWindow));
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dlgService->Alert(domWindow, nullptr, PromiseFlatString(aMessage).get());
+
+ return NS_OK;
+}
+
+nsresult nsMsgMailSession::GetTopmostMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ *aMsgWindow = nullptr;
+
+ uint32_t count = mWindows.Count();
+
+ if (count == 1) {
+ NS_ADDREF(*aMsgWindow = mWindows[0]);
+ return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE;
+ } else if (count > 1) {
+ // If multiple message windows then we have lots more work.
+ nsresult rv;
+
+ // The msgWindows array does not hold z-order info. Use mediator to get
+ // the top most window then match that with the msgWindows array.
+ nsCOMPtr<nsIWindowMediator> windowMediator =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> windowEnum;
+
+ rv = windowMediator->GetEnumerator(nullptr, getter_AddRefs(windowEnum));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> windowSupports;
+ nsCOMPtr<nsPIDOMWindowOuter> topMostWindow;
+ nsAutoString windowType;
+ bool more;
+
+ // loop to get the top most with attribute "mail:3pane" or
+ // "mail:messageWindow"
+ windowEnum->HasMoreElements(&more);
+ while (more) {
+ rv = windowEnum->GetNext(getter_AddRefs(windowSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(windowSupports, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ do_QueryInterface(windowSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+
+ mozilla::dom::Document* domDocument = window->GetDoc();
+ NS_ENSURE_TRUE(domDocument, NS_ERROR_FAILURE);
+
+ mozilla::dom::Element* domElement = domDocument->GetDocumentElement();
+ NS_ENSURE_TRUE(domElement, NS_ERROR_FAILURE);
+
+ domElement->GetAttribute(u"windowtype"_ns, windowType);
+ if (windowType.EqualsLiteral("mail:3pane") ||
+ windowType.EqualsLiteral("mail:messageWindow")) {
+ // topMostWindow is the last 3pane/messageWindow found, not necessarily
+ // the top most.
+ topMostWindow = window;
+ RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
+ nsCOMPtr<mozIDOMWindowProxy> currentWindow =
+ do_QueryInterface(windowSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIDOMWindowProxy> activeWindow;
+ rv = fm->GetActiveWindow(getter_AddRefs(activeWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (currentWindow == activeWindow) {
+ // We are sure topMostWindow is really the top most now.
+ break;
+ }
+ }
+
+ windowEnum->HasMoreElements(&more);
+ }
+
+ // identified the top most window
+ if (topMostWindow) {
+ // use this for the match
+ nsIDocShell* topDocShell = topMostWindow->GetDocShell();
+
+ // loop for the msgWindow array to find the match
+ nsCOMPtr<nsIDocShell> docShell;
+
+ while (count) {
+ nsIMsgWindow* msgWindow = mWindows[--count];
+
+ rv = msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (topDocShell == docShell) {
+ NS_IF_ADDREF(*aMsgWindow = msgWindow);
+ break;
+ }
+ }
+ }
+ }
+
+ return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgMailSession::AddMsgWindow(nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(msgWindow);
+
+ mWindows.AppendObject(msgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::RemoveMsgWindow(nsIMsgWindow* msgWindow) {
+ mWindows.RemoveObject(msgWindow);
+ // Mac keeps a hidden window open so the app doesn't shut down when
+ // the last window is closed. So don't shutdown the account manager in that
+ // case. Similarly, for suite, we don't want to disable mailnews when the
+ // last mail window is closed.
+#if !defined(XP_MACOSX) && !defined(MOZ_SUITE)
+ if (!mWindows.Count()) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+ accountManager->CleanupOnExit();
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::IsFolderOpenInWindow(nsIMsgFolder* folder,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+
+ uint32_t count = mWindows.Count();
+
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> openFolder;
+ mWindows[i]->GetOpenFolder(getter_AddRefs(openFolder));
+ if (folder == openFolder.get()) {
+ *aResult = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::ConvertMsgURIToMsgURL(const nsACString& aURI,
+ nsIMsgWindow* aMsgWindow,
+ nsACString& aURL) {
+ // convert the rdf msg uri into a url that represents the message...
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIURI> tURI;
+ rv = msgService->GetUrlForUri(aURI, aMsgWindow, getter_AddRefs(tURI));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER);
+
+ rv = tURI->GetSpec(aURL);
+ return rv;
+}
+
+//-------------------------------------------------------------------------
+// GetSelectedLocaleDataDir - If a locale is selected, appends the selected
+// locale to the defaults data dir and returns
+// that new defaults data dir
+//-------------------------------------------------------------------------
+nsresult nsMsgMailSession::GetSelectedLocaleDataDir(nsIFile* defaultsDir) {
+ NS_ENSURE_ARG_POINTER(defaultsDir);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// GetDataFilesDir - Gets the application's default folder and then appends the
+// subdirectory named passed in as param dirName. If there is
+// a selected locale, will append that to the dir path before
+// returning the value
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+nsMsgMailSession::GetDataFilesDir(const char* dirName, nsIFile** dataFilesDir) {
+ NS_ENSURE_ARG_POINTER(dirName);
+ NS_ENSURE_ARG_POINTER(dataFilesDir);
+
+ nsresult rv;
+ nsCOMPtr<nsIProperties> directoryService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> defaultsDir;
+ rv = directoryService->Get(NS_APP_DEFAULTS_50_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(defaultsDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultsDir->AppendNative(nsDependentCString(dirName));
+ if (NS_SUCCEEDED(rv)) rv = GetSelectedLocaleDataDir(defaultsDir);
+
+ defaultsDir.forget(dataFilesDir);
+
+ return rv;
+}
+
+/********************************************************************************/
+
+NS_IMPL_ISUPPORTS(nsMsgShutdownService, nsIMsgShutdownService, nsIUrlListener,
+ nsIObserver)
+
+nsMsgShutdownService::nsMsgShutdownService()
+ : mTaskIndex(0),
+ mQuitMode(nsIAppStartup::eAttemptQuit),
+ mProcessedShutdown(false),
+ mQuitForced(false),
+ mReadyToQuit(false) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "quit-application-requested", false);
+ observerService->AddObserver(this, "quit-application-granted", false);
+ observerService->AddObserver(this, "quit-application", false);
+ }
+}
+
+nsMsgShutdownService::~nsMsgShutdownService() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "quit-application-requested");
+ observerService->RemoveObserver(this, "quit-application-granted");
+ observerService->RemoveObserver(this, "quit-application");
+ }
+}
+
+nsresult nsMsgShutdownService::ProcessNextTask() {
+ bool shutdownTasksDone = true;
+
+ uint32_t count = mShutdownTasks.Length();
+ if (mTaskIndex < count) {
+ shutdownTasksDone = false;
+
+ nsCOMPtr<nsIMsgShutdownTask> curTask = mShutdownTasks[mTaskIndex];
+ nsString taskName;
+ curTask->GetCurrentTaskName(taskName);
+ SetStatusText(taskName);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgWindow> topMsgWindow;
+ mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow));
+
+ bool taskIsRunning = true;
+ nsresult rv = curTask->DoShutdownTask(this, topMsgWindow, &taskIsRunning);
+ if (NS_FAILED(rv) || !taskIsRunning) {
+ // We have failed, let's go on to the next task.
+ mTaskIndex++;
+ mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0,
+ (int32_t)mTaskIndex, count);
+ ProcessNextTask();
+ }
+ }
+
+ if (shutdownTasksDone) {
+ if (mMsgProgress)
+ mMsgProgress->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP, NS_OK);
+ AttemptShutdown();
+ }
+
+ return NS_OK;
+}
+
+void nsMsgShutdownService::AttemptShutdown() {
+ if (mQuitForced) {
+ PR_CEnterMonitor(this);
+ mReadyToQuit = true;
+ PR_CNotifyAll(this);
+ PR_CExitMonitor(this);
+ } else {
+ nsCOMPtr<nsIAppStartup> appStartup =
+ mozilla::components::AppStartup::Service();
+ NS_ENSURE_TRUE_VOID(appStartup);
+ bool userAllowedQuit = true;
+ NS_ENSURE_SUCCESS_VOID(appStartup->Quit(mQuitMode, 0, &userAllowedQuit));
+ }
+}
+
+NS_IMETHODIMP nsMsgShutdownService::SetShutdownListener(
+ nsIWebProgressListener* inListener) {
+ NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE);
+ mMsgProgress->RegisterListener(inListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ // Due to bug 459376 we don't always get quit-application-requested and
+ // quit-application-granted. quit-application-requested is preferred, but if
+ // we don't then we have to hook onto quit-application, but we don't want
+ // to do the checking twice so we set some flags to prevent that.
+ if (!strcmp(aTopic, "quit-application-granted")) {
+ // Quit application has been requested and granted, therefore we will shut
+ // down.
+ mProcessedShutdown = true;
+ return NS_OK;
+ }
+
+ // If we've already processed a shutdown notification, no need to do it again.
+ if (!strcmp(aTopic, "quit-application")) {
+ if (mProcessedShutdown)
+ return NS_OK;
+ else
+ mQuitForced = true;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(observerService);
+
+ nsCOMPtr<nsISimpleEnumerator> listenerEnum;
+ nsresult rv = observerService->EnumerateObservers(
+ "msg-shutdown", getter_AddRefs(listenerEnum));
+ if (NS_SUCCEEDED(rv) && listenerEnum) {
+ bool hasMore;
+ listenerEnum->HasMoreElements(&hasMore);
+ if (!hasMore) return NS_OK;
+
+ while (hasMore) {
+ nsCOMPtr<nsISupports> curObject;
+ listenerEnum->GetNext(getter_AddRefs(curObject));
+
+ nsCOMPtr<nsIMsgShutdownTask> curTask = do_QueryInterface(curObject);
+ if (curTask) {
+ bool shouldRunTask;
+ curTask->GetNeedsToRunTask(&shouldRunTask);
+ if (shouldRunTask) mShutdownTasks.AppendObject(curTask);
+ }
+
+ listenerEnum->HasMoreElements(&hasMore);
+ }
+
+ if (mShutdownTasks.Count() < 1) return NS_ERROR_FAILURE;
+
+ mTaskIndex = 0;
+
+ mMsgProgress = do_CreateInstance("@mozilla.org/messenger/progress;1");
+ NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgWindow> topMsgWindow;
+ mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow));
+
+ nsCOMPtr<mozIDOMWindowProxy> internalDomWin;
+ if (topMsgWindow)
+ topMsgWindow->GetDomWindow(getter_AddRefs(internalDomWin));
+
+ if (!internalDomWin) {
+ // First see if there is a window open.
+ nsCOMPtr<nsIWindowMediator> winMed =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID);
+ winMed->GetMostRecentWindow(nullptr, getter_AddRefs(internalDomWin));
+
+ // If not use the hidden window.
+ if (!internalDomWin) {
+ nsCOMPtr<nsIAppShellService> appShell(
+ do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ appShell->GetHiddenDOMWindow(getter_AddRefs(internalDomWin));
+ NS_ENSURE_TRUE(internalDomWin,
+ NS_ERROR_FAILURE); // bail if we don't get a window.
+ }
+ }
+
+ if (!mQuitForced) {
+ nsCOMPtr<nsISupportsPRBool> stopShutdown = do_QueryInterface(aSubject);
+ stopShutdown->SetData(true);
+
+ // If the attempted quit was a restart, be sure to restart the app once
+ // the tasks have been run. This is usually the case when addons or
+ // updates are going to be installed.
+ if (aData && nsDependentString(aData).EqualsLiteral("restart"))
+ mQuitMode |= nsIAppStartup::eRestart;
+ }
+
+ mMsgProgress->OpenProgressDialog(
+ internalDomWin, topMsgWindow,
+ "chrome://messenger/content/shutdownWindow.xhtml", false, nullptr);
+
+ if (mQuitForced) {
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+
+ mReadyToQuit = false;
+ while (!mReadyToQuit) {
+ PR_CEnterMonitor(this);
+ // Waiting for 50 milliseconds
+ PR_CWait(this, PR_MicrosecondsToInterval(50000UL));
+ PR_CExitMonitor(this);
+ NS_ProcessPendingEvents(thread);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// nsIUrlListener
+NS_IMETHODIMP nsMsgShutdownService::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::OnStopRunningUrl(nsIURI* url,
+ nsresult aExitCode) {
+ mTaskIndex++;
+
+ if (mMsgProgress) {
+ int32_t numTasks = mShutdownTasks.Count();
+ mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0, (int32_t)mTaskIndex,
+ numTasks);
+ }
+
+ ProcessNextTask();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::GetNumTasks(int32_t* inNumTasks) {
+ *inNumTasks = mShutdownTasks.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::StartShutdownTasks() {
+ ProcessNextTask();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::CancelShutdownTasks() {
+ AttemptShutdown();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::SetStatusText(
+ const nsAString& inStatusString) {
+ nsString statusString(inStatusString);
+ if (mMsgProgress)
+ mMsgProgress->OnStatusChange(nullptr, nullptr, NS_OK,
+ nsString(statusString).get());
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgMailSession.h b/comm/mailnews/base/src/nsMsgMailSession.h
new file mode 100644
index 0000000000..09cea632e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailSession.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgMailSession_h___
+#define nsMsgMailSession_h___
+
+#include "nsIMsgMailSession.h"
+#include "nsISupports.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIMsgWindow.h"
+#include "nsCOMArray.h"
+#include "nsIMsgShutdown.h"
+#include "nsIObserver.h"
+#include "nsIMsgProgress.h"
+#include "nsTObserverArray.h"
+#include "nsIMsgUserFeedbackListener.h"
+#include "nsIUrlListener.h"
+
+///////////////////////////////////////////////////////////////////////////////////
+// The mail session is a replacement for the old 4.x MSG_Master object. It
+// contains mail session generic information such as the user's current mail
+// identity, .... I'm starting this off as an empty interface and as people feel
+// they need to add more information to it, they can. I think this is a better
+// approach than trying to port over the old MSG_Master in its entirety as that
+// had a lot of cruft in it....
+//////////////////////////////////////////////////////////////////////////////////
+
+// nsMsgMailSession also implements nsIFolderListener, in order to relay
+// notifications to its registered listeners.
+// Calling a method on the MailSession causes that method to be invoked upon
+// all the registered listeners (but not listeners directly attached to
+// folders!)
+// In normal operation, most notifications will originate from the
+// nsIMsgFolder.Notify*() functions, which invoke both folder-local
+// listeners, and the global MailSession-registered ones).
+class nsMsgMailSession : public nsIMsgMailSession, public nsIFolderListener {
+ public:
+ nsMsgMailSession();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGMAILSESSION
+ NS_DECL_NSIFOLDERLISTENER
+
+ nsresult Init();
+ nsresult GetSelectedLocaleDataDir(nsIFile* defaultsDir);
+
+ protected:
+ virtual ~nsMsgMailSession();
+
+ struct folderListener {
+ nsCOMPtr<nsIFolderListener> mListener;
+ uint32_t mNotifyFlags;
+
+ folderListener(nsIFolderListener* aListener, uint32_t aNotifyFlags)
+ : mListener(aListener), mNotifyFlags(aNotifyFlags) {}
+ folderListener(const folderListener& aListener)
+ : mListener(aListener.mListener),
+ mNotifyFlags(aListener.mNotifyFlags) {}
+ ~folderListener() {}
+
+ int operator==(nsIFolderListener* aListener) const {
+ return mListener == aListener;
+ }
+ int operator==(const folderListener& aListener) const {
+ return mListener == aListener.mListener &&
+ mNotifyFlags == aListener.mNotifyFlags;
+ }
+ };
+
+ nsTObserverArray<folderListener> mListeners;
+ nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener> > mFeedbackListeners;
+
+ nsCOMArray<nsIMsgWindow> mWindows;
+ // stick this here temporarily
+ nsCOMPtr<nsIMsgWindow> m_temporaryMsgWindow;
+};
+
+/********************************************************************************/
+
+class nsMsgShutdownService : public nsIMsgShutdownService,
+ public nsIUrlListener,
+ public nsIObserver {
+ public:
+ nsMsgShutdownService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSHUTDOWNSERVICE
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIOBSERVER
+
+ protected:
+ nsresult ProcessNextTask();
+ void AttemptShutdown();
+
+ private:
+ virtual ~nsMsgShutdownService();
+
+ nsCOMArray<nsIMsgShutdownTask> mShutdownTasks;
+ nsCOMPtr<nsIMsgProgress> mMsgProgress;
+ uint32_t mTaskIndex;
+ uint32_t mQuitMode;
+ bool mProcessedShutdown;
+ bool mQuitForced;
+ bool mReadyToQuit;
+};
+
+#endif /* nsMsgMailSession_h__ */
diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.cpp b/comm/mailnews/base/src/nsMsgOfflineManager.cpp
new file mode 100644
index 0000000000..7d6935fc89
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgOfflineManager.cpp
@@ -0,0 +1,352 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * The offline manager service - manages going online and offline, and
+ * synchronization
+ */
+#include "msgCore.h"
+#include "netCore.h"
+#include "nsMsgOfflineManager.h"
+#include "nsIServiceManager.h"
+#include "nsIImapService.h"
+#include "nsIMsgSendLater.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsINntpService.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+
+#define NS_MSGSENDLATER_CID \
+ { /* E15C83F1-1CF4-11d3-8EF0-00A024A7D144 */ \
+ 0xe15c83f1, 0x1cf4, 0x11d3, { \
+ 0x8e, 0xf0, 0x0, 0xa0, 0x24, 0xa7, 0xd1, 0x44 \
+ } \
+ }
+static NS_DEFINE_CID(kMsgSendLaterCID, NS_MSGSENDLATER_CID);
+
+NS_IMPL_ISUPPORTS(nsMsgOfflineManager, nsIMsgOfflineManager,
+ nsIMsgSendLaterListener, nsIObserver,
+ nsISupportsWeakReference, nsIUrlListener)
+
+nsMsgOfflineManager::nsMsgOfflineManager()
+ : m_inProgress(false),
+ m_sendUnsentMessages(false),
+ m_downloadNews(false),
+ m_downloadMail(false),
+ m_playbackOfflineImapOps(false),
+ m_goOfflineWhenDone(false),
+ m_curState(eNoState),
+ m_curOperation(eNoOp) {}
+
+nsMsgOfflineManager::~nsMsgOfflineManager() {}
+
+/* attribute nsIMsgWindow window; */
+NS_IMETHODIMP nsMsgOfflineManager::GetWindow(nsIMsgWindow** aWindow) {
+ NS_ENSURE_ARG(aWindow);
+ NS_IF_ADDREF(*aWindow = m_window);
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgOfflineManager::SetWindow(nsIMsgWindow* aWindow) {
+ m_window = aWindow;
+ if (m_window)
+ m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+ else
+ m_statusFeedback = nullptr;
+ return NS_OK;
+}
+
+/* attribute boolean inProgress; */
+NS_IMETHODIMP nsMsgOfflineManager::GetInProgress(bool* aInProgress) {
+ NS_ENSURE_ARG(aInProgress);
+ *aInProgress = m_inProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::SetInProgress(bool aInProgress) {
+ m_inProgress = aInProgress;
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::StopRunning(nsresult exitStatus) {
+ m_inProgress = false;
+ return exitStatus;
+}
+
+nsresult nsMsgOfflineManager::AdvanceToNextState(nsresult exitStatus) {
+ // NS_BINDING_ABORTED is used for the user pressing stop, which
+ // should cause us to abort the offline process. Other errors
+ // should allow us to continue.
+ if (exitStatus == NS_BINDING_ABORTED) {
+ return StopRunning(exitStatus);
+ }
+ if (m_curOperation == eGoingOnline) {
+ switch (m_curState) {
+ case eNoState:
+
+ m_curState = eSendingUnsent;
+ if (m_sendUnsentMessages) {
+ SendUnsentMessages();
+ } else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eSendingUnsent:
+
+ m_curState = eSynchronizingOfflineImapChanges;
+ if (m_playbackOfflineImapOps)
+ return SynchronizeOfflineImapChanges();
+ else
+ AdvanceToNextState(NS_OK); // recurse to next state.
+ break;
+ case eSynchronizingOfflineImapChanges:
+ m_curState = eDone;
+ return StopRunning(exitStatus);
+ default:
+ NS_ASSERTION(false, "unhandled current state when going online");
+ }
+ } else if (m_curOperation == eDownloadingForOffline) {
+ switch (m_curState) {
+ case eNoState:
+ m_curState = eDownloadingNews;
+ if (m_downloadNews)
+ DownloadOfflineNewsgroups();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eSendingUnsent:
+ if (m_goOfflineWhenDone) {
+ SetOnlineState(false);
+ }
+ break;
+ case eDownloadingNews:
+ m_curState = eDownloadingMail;
+ if (m_downloadMail)
+ DownloadMail();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eDownloadingMail:
+ m_curState = eSendingUnsent;
+ if (m_sendUnsentMessages)
+ SendUnsentMessages();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ default:
+ NS_ASSERTION(false,
+ "unhandled current state when downloading for offline");
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::SynchronizeOfflineImapChanges() {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->PlaybackAllOfflineOperations(
+ m_window, this, getter_AddRefs(mOfflineImapSync));
+}
+
+nsresult nsMsgOfflineManager::SendUnsentMessages() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendLater> pMsgSendLater(do_GetService(kMsgSendLaterCID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // now we have to iterate over the identities, finding the *unique* unsent
+ // messages folder for each one, determine if they have unsent messages, and
+ // if so, add them to the list of identities to send unsent messages from.
+ // However, I think there's only ever one unsent messages folder at the
+ // moment, so I think we'll go with that for now.
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ rv = accountManager->GetAllIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCOMPtr<nsIMsgIdentity> identityToUse;
+ for (auto thisIdentity : identities) {
+ if (thisIdentity) {
+ nsCOMPtr<nsIMsgFolder> outboxFolder;
+ pMsgSendLater->GetUnsentMessagesFolder(thisIdentity,
+ getter_AddRefs(outboxFolder));
+ if (outboxFolder) {
+ int32_t numMessages;
+ outboxFolder->GetTotalMessages(false, &numMessages);
+ if (numMessages > 0) {
+ identityToUse = thisIdentity;
+ break;
+ }
+ }
+ }
+ }
+ if (identityToUse) {
+#ifdef MOZ_SUITE
+ if (m_statusFeedback) pMsgSendLater->SetStatusFeedback(m_statusFeedback);
+#endif
+
+ pMsgSendLater->AddListener(this);
+ rv = pMsgSendLater->SendUnsentMessages(identityToUse);
+ ShowStatus("sendingUnsent");
+ // if we succeeded, return - we'll run the next operation when the
+ // send finishes. Otherwise, advance to the next state.
+ if (NS_SUCCEEDED(rv)) return rv;
+ }
+ return AdvanceToNextState(rv);
+}
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+nsresult nsMsgOfflineManager::ShowStatus(const char* statusMsgName) {
+ if (!mStringBundle) {
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ sBundleService->CreateBundle(MESSENGER_STRING_URL,
+ getter_AddRefs(mStringBundle));
+ return NS_OK;
+ }
+
+ nsString statusString;
+ nsresult res = mStringBundle->GetStringFromName(statusMsgName, statusString);
+
+ if (NS_SUCCEEDED(res) && m_statusFeedback)
+ m_statusFeedback->ShowStatusString(statusString);
+
+ return res;
+}
+
+nsresult nsMsgOfflineManager::DownloadOfflineNewsgroups() {
+ nsresult rv;
+ ShowStatus("downloadingNewsgroups");
+ nsCOMPtr<nsINntpService> nntpService(
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv));
+ if (NS_SUCCEEDED(rv) && nntpService)
+ rv = nntpService->DownloadNewsgroupsForOffline(m_window, this);
+
+ if (NS_FAILED(rv)) return AdvanceToNextState(rv);
+ return rv;
+}
+
+nsresult nsMsgOfflineManager::DownloadMail() {
+ nsresult rv = NS_OK;
+ ShowStatus("downloadingMail");
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->DownloadAllOffineImapFolders(m_window, this);
+ // ### we should do get new mail on pop servers, and download imap messages
+ // for offline use.
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::GoOnline(bool sendUnsentMessages,
+ bool playbackOfflineImapOperations,
+ nsIMsgWindow* aMsgWindow) {
+ m_sendUnsentMessages = sendUnsentMessages;
+ m_playbackOfflineImapOps = playbackOfflineImapOperations;
+ m_curOperation = eGoingOnline;
+ m_curState = eNoState;
+ SetWindow(aMsgWindow);
+ SetOnlineState(true);
+ if (!m_sendUnsentMessages && !playbackOfflineImapOperations)
+ return NS_OK;
+ else
+ AdvanceToNextState(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::SynchronizeForOffline(
+ bool downloadNews, bool downloadMail, bool sendUnsentMessages,
+ bool goOfflineWhenDone, nsIMsgWindow* aMsgWindow) {
+ m_curOperation = eDownloadingForOffline;
+ m_downloadNews = downloadNews;
+ m_downloadMail = downloadMail;
+ m_sendUnsentMessages = sendUnsentMessages;
+ SetWindow(aMsgWindow);
+ m_goOfflineWhenDone = goOfflineWhenDone;
+ m_curState = eNoState;
+ if (!downloadNews && !downloadMail && !sendUnsentMessages) {
+ if (goOfflineWhenDone) return SetOnlineState(false);
+ } else
+ return AdvanceToNextState(NS_OK);
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::SetOnlineState(bool online) {
+ nsCOMPtr<nsIIOService> netService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(netService, NS_ERROR_UNEXPECTED);
+ return netService->SetOffline(!online);
+}
+
+// nsIUrlListener methods
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ mOfflineImapSync = nullptr;
+
+ AdvanceToNextState(aExitCode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ return NS_OK;
+}
+
+// nsIMsgSendLaterListener implementation
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStartSending(uint32_t aTotalMessageCount) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageStartSending(uint32_t aCurrentMessage,
+ uint32_t aTotalMessageCount,
+ nsIMsgDBHdr* aMessageHeader,
+ nsIMsgIdentity* aIdentity) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageSendProgress(uint32_t aCurrentMessage,
+ uint32_t aTotalMessageCount,
+ uint32_t aMessageSendPercent,
+ uint32_t aMessageCopyPercent) {
+ if (m_statusFeedback && aTotalMessageCount)
+ return m_statusFeedback->ShowProgress((100 * aCurrentMessage) /
+ aTotalMessageCount);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageSendError(uint32_t aCurrentMessage,
+ nsIMsgDBHdr* aMessageHeader,
+ nsresult aStatus,
+ const char16_t* aMsg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStopSending(nsresult aStatus, const char16_t* aMsg,
+ uint32_t aTotalTried, uint32_t aSuccessful) {
+#ifdef NS_DEBUG
+ if (NS_SUCCEEDED(aStatus))
+ printf(
+ "SendLaterListener::OnStopSending: Tried to send %d messages. %d "
+ "successful.\n",
+ aTotalTried, aSuccessful);
+#endif
+ return AdvanceToNextState(aStatus);
+}
diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.h b/comm/mailnews/base/src/nsMsgOfflineManager.h
new file mode 100644
index 0000000000..f340ee0fd1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgOfflineManager.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgOfflineManager_h__
+#define nsMsgOfflineManager_h__
+
+#include "nscore.h"
+#include "nsIMsgOfflineManager.h"
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSendLaterListener.h"
+#include "nsIStringBundle.h"
+
+class nsMsgOfflineManager : public nsIMsgOfflineManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIMsgSendLaterListener,
+ public nsIUrlListener {
+ public:
+ nsMsgOfflineManager();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* nsIMsgOfflineManager methods */
+
+ NS_DECL_NSIMSGOFFLINEMANAGER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSENDLATERLISTENER
+
+ typedef enum {
+ eStarting = 0,
+ eSynchronizingOfflineImapChanges = 1,
+ eDownloadingNews = 2,
+ eDownloadingMail = 3,
+ eSendingUnsent = 4,
+ eDone = 5,
+ eNoState = 6 // we're not doing anything
+ } offlineManagerState;
+
+ typedef enum {
+ eGoingOnline = 0,
+ eDownloadingForOffline = 1,
+ eNoOp = 2 // no operation in progress
+ } offlineManagerOperation;
+
+ private:
+ virtual ~nsMsgOfflineManager();
+
+ nsresult AdvanceToNextState(nsresult exitStatus);
+ nsresult SynchronizeOfflineImapChanges();
+ nsresult StopRunning(nsresult exitStatus);
+ nsresult SendUnsentMessages();
+ nsresult DownloadOfflineNewsgroups();
+ nsresult DownloadMail();
+
+ nsresult SetOnlineState(bool online);
+ nsresult ShowStatus(const char* statusMsgName);
+
+ bool m_inProgress;
+ bool m_sendUnsentMessages;
+ bool m_downloadNews;
+ bool m_downloadMail;
+ bool m_playbackOfflineImapOps;
+ bool m_goOfflineWhenDone;
+ offlineManagerState m_curState;
+ offlineManagerOperation m_curOperation;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+ nsCOMPtr<nsISupports> mOfflineImapSync;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgProgress.cpp b/comm/mailnews/base/src/nsMsgProgress.cpp
new file mode 100644
index 0000000000..a6349b5add
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProgress.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgProgress.h"
+
+#include "nsIBaseWindow.h"
+#include "nsXPCOM.h"
+#include "nsIMutableArray.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIComponentManager.h"
+#include "nsError.h"
+#include "nsIWindowWatcher.h"
+#include "nsPIDOMWindow.h"
+#include "mozIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/BrowsingContext.h"
+
+NS_IMPL_ISUPPORTS(nsMsgProgress, nsIMsgStatusFeedback, nsIMsgProgress,
+ nsIWebProgressListener, nsIProgressEventSink,
+ nsISupportsWeakReference)
+
+nsMsgProgress::nsMsgProgress() {
+ m_closeProgress = false;
+ m_processCanceled = false;
+ m_pendingStateFlags = -1;
+ m_pendingStateValue = NS_OK;
+}
+
+nsMsgProgress::~nsMsgProgress() { (void)ReleaseListeners(); }
+
+NS_IMETHODIMP nsMsgProgress::OpenProgressDialog(
+ mozIDOMWindowProxy* parentDOMWindow, nsIMsgWindow* aMsgWindow,
+ const char* dialogURL, bool inDisplayModal, nsISupports* parameters) {
+ nsresult rv;
+
+ if (aMsgWindow) {
+ SetMsgWindow(aMsgWindow);
+ aMsgWindow->SetStatusFeedback(this);
+ }
+
+ NS_ENSURE_ARG_POINTER(dialogURL);
+ NS_ENSURE_ARG_POINTER(parentDOMWindow);
+ nsCOMPtr<nsPIDOMWindowOuter> parent =
+ nsPIDOMWindowOuter::From(parentDOMWindow);
+
+ // Set up window.arguments[0]...
+ nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsInterfacePointer> ifptr =
+ do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ifptr->SetData(static_cast<nsIMsgProgress*>(this));
+ ifptr->SetDataIID(&NS_GET_IID(nsIMsgProgress));
+
+ array->AppendElement(ifptr);
+ array->AppendElement(parameters);
+
+ // Open the dialog.
+ RefPtr<mozilla::dom::BrowsingContext> newWindow;
+
+ nsString chromeOptions(u"chrome,dependent,centerscreen"_ns);
+ if (inDisplayModal) chromeOptions.AppendLiteral(",modal");
+
+ return parent->OpenDialog(NS_ConvertASCIItoUTF16(dialogURL), u"_blank"_ns,
+ chromeOptions, array, getter_AddRefs(newWindow));
+}
+
+NS_IMETHODIMP nsMsgProgress::CloseProgressDialog(bool forceClose) {
+ m_closeProgress = true;
+ return OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP,
+ forceClose ? NS_ERROR_FAILURE : NS_OK);
+}
+
+NS_IMETHODIMP nsMsgProgress::GetProcessCanceledByUser(
+ bool* aProcessCanceledByUser) {
+ NS_ENSURE_ARG_POINTER(aProcessCanceledByUser);
+ *aProcessCanceledByUser = m_processCanceled;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgProgress::SetProcessCanceledByUser(
+ bool aProcessCanceledByUser) {
+ m_processCanceled = aProcessCanceledByUser;
+ OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP,
+ NS_BINDING_ABORTED);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::RegisterListener(
+ nsIWebProgressListener* listener) {
+ if (!listener) // Nothing to do with a null listener!
+ return NS_OK;
+
+ NS_ENSURE_ARG(this != listener); // Check for self-reference (see bug 271700)
+
+ m_listenerList.AppendObject(listener);
+ if (m_closeProgress || m_processCanceled)
+ listener->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP, NS_OK);
+ else {
+ listener->OnStatusChange(nullptr, nullptr, NS_OK, m_pendingStatus.get());
+ if (m_pendingStateFlags != -1)
+ listener->OnStateChange(nullptr, nullptr, m_pendingStateFlags,
+ m_pendingStateValue);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::UnregisterListener(
+ nsIWebProgressListener* listener) {
+ if (listener) m_listenerList.RemoveObject(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus) {
+ m_pendingStateFlags = aStateFlags;
+ m_pendingStateValue = aStatus;
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindow));
+ if (aStateFlags == nsIWebProgressListener::STATE_STOP && msgWindow &&
+ NS_FAILED(aStatus)) {
+ msgWindow->StopUrls();
+ msgWindow->SetStatusFeedback(nullptr);
+ }
+
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnStateChange(aWebProgress, aRequest, aStateFlags,
+ aStatus);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI* location,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage) {
+ if (aMessage && *aMessage) m_pendingStatus = aMessage;
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnStatusChange(aWebProgress, aRequest, aStatus,
+ aMessage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t state) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProgress::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aEvent) {
+ return NS_OK;
+}
+
+nsresult nsMsgProgress::ReleaseListeners() {
+ m_listenerList.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::ShowStatusString(const nsAString& aStatus) {
+ return OnStatusChange(nullptr, nullptr, NS_OK,
+ PromiseFlatString(aStatus).get());
+}
+
+NS_IMETHODIMP nsMsgProgress::SetStatusString(const nsAString& aStatus) {
+ return OnStatusChange(nullptr, nullptr, NS_OK,
+ PromiseFlatString(aStatus).get());
+}
+
+NS_IMETHODIMP nsMsgProgress::StartMeteors() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP nsMsgProgress::StopMeteors() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP nsMsgProgress::ShowProgress(int32_t percent) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgProgress::SetWrappedStatusFeedback(
+ nsIMsgStatusFeedback* aJSStatusFeedback) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgProgress::SetMsgWindow(nsIMsgWindow* aMsgWindow) {
+ m_msgWindow = do_GetWeakReference(aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ if (m_msgWindow)
+ CallQueryReferent(m_msgWindow.get(), aMsgWindow);
+ else
+ *aMsgWindow = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnProgress(nsIRequest* request, int64_t aProgress,
+ int64_t aProgressMax) {
+ // XXX: What should the nsIWebProgress be?
+ // XXX: This truncates 64-bit to 32-bit
+ return OnProgressChange(nullptr, request, int32_t(aProgress),
+ int32_t(aProgressMax),
+ int32_t(aProgress) /* current total progress */,
+ int32_t(aProgressMax) /* max total progress */);
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStatus(nsIRequest* request, nsresult aStatus,
+ const char16_t* aStatusArg) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> sbs =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED);
+ nsString str;
+ rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowStatusString(str);
+}
diff --git a/comm/mailnews/base/src/nsMsgProgress.h b/comm/mailnews/base/src/nsMsgProgress.h
new file mode 100644
index 0000000000..57fae38aa9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProgress.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgProgress_h_
+#define nsMsgProgress_h_
+
+#include "nsIMsgProgress.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsString.h"
+#include "nsIMsgWindow.h"
+#include "nsIProgressEventSink.h"
+#include "nsIStringBundle.h"
+#include "nsWeakReference.h"
+
+class nsMsgProgress : public nsIMsgProgress,
+ public nsIMsgStatusFeedback,
+ public nsIProgressEventSink,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgProgress();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGPROGRESS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIMSGSTATUSFEEDBACK
+ NS_DECL_NSIPROGRESSEVENTSINK
+
+ private:
+ virtual ~nsMsgProgress();
+ nsresult ReleaseListeners(void);
+
+ bool m_closeProgress;
+ bool m_processCanceled;
+ nsString m_pendingStatus;
+ int32_t m_pendingStateFlags;
+ nsresult m_pendingStateValue;
+ nsWeakPtr m_msgWindow;
+ nsCOMArray<nsIWebProgressListener> m_listenerList;
+};
+
+#endif // nsMsgProgress_h_
diff --git a/comm/mailnews/base/src/nsMsgProtocol.cpp b/comm/mailnews/base/src/nsMsgProtocol.cpp
new file mode 100644
index 0000000000..7fc832758c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProtocol.cpp
@@ -0,0 +1,1512 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsMsgProtocol.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgMailSession.h"
+#include "nsIStreamTransportService.h"
+#include "nsISocketTransportService.h"
+#include "nsISocketTransport.h"
+#include "nsITLSSocketControl.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsILoadGroup.h"
+#include "nsILoadInfo.h"
+#include "nsIIOService.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIWebProgressListener.h"
+#include "nsIPipe.h"
+#include "nsIPrompt.h"
+#include "prprf.h"
+#include "plbase64.h"
+#include "nsIStringBundle.h"
+#include "nsIProxyInfo.h"
+#include "nsThreadUtils.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsMsgUtils.h"
+#include "nsILineInputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIInputStreamPump.h"
+#include "nsICancelable.h"
+#include "nsMimeTypes.h"
+#include "mozilla/Components.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsContentSecurityManager.h"
+#include "nsPrintfCString.h"
+
+#undef PostMessage // avoid to collision with WinUser.h
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgProtocol, nsHashPropertyBag, nsIMailChannel,
+ nsIChannel, nsIStreamListener, nsIRequestObserver,
+ nsIRequest, nsITransportEventSink)
+
+static char16_t* FormatStringWithHostNameByName(const char16_t* stringName,
+ nsIMsgMailNewsUrl* msgUri);
+
+nsMsgProtocol::nsMsgProtocol(nsIURI* aURL) {
+ m_flags = 0;
+ m_readCount = 0;
+ mLoadFlags = 0;
+ m_socketIsOpen = false;
+ mContentLength = -1;
+ m_isChannel = false;
+ mContentDisposition = nsIChannel::DISPOSITION_INLINE;
+
+ GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "tempMessage.eml",
+ getter_AddRefs(m_tempMsgFile));
+
+ mSuppressListenerNotifications = false;
+ InitFromURI(aURL);
+}
+
+nsresult nsMsgProtocol::InitFromURI(nsIURI* aUrl) {
+ m_url = aUrl;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) {
+ mailUrl->GetLoadGroup(getter_AddRefs(m_loadGroup));
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ mProgressEventSink = do_QueryInterface(statusFeedback);
+ }
+
+ // Reset channel data in case the object is reused and initialised again.
+ mCharset.Truncate();
+
+ return NS_OK;
+}
+
+nsMsgProtocol::~nsMsgProtocol() {}
+
+static bool gGotTimeoutPref;
+static int32_t gSocketTimeout = 60;
+
+nsresult nsMsgProtocol::GetQoSBits(uint8_t* aQoSBits) {
+ NS_ENSURE_ARG_POINTER(aQoSBits);
+ const char* protocol = GetType();
+
+ if (!protocol) return NS_ERROR_NOT_IMPLEMENTED;
+
+ nsAutoCString prefName("mail.");
+ prefName.Append(protocol);
+ prefName.AppendLiteral(".qos");
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t val;
+ rv = prefBranch->GetIntPref(prefName.get(), &val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aQoSBits = (uint8_t)clamped(val, 0, 0xff);
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::OpenNetworkSocketWithInfo(
+ const char* aHostName, int32_t aGetPort, const char* connectionType,
+ nsIProxyInfo* aProxyInfo, nsIInterfaceRequestor* callbacks) {
+ NS_ENSURE_ARG(aHostName);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsISocketTransportService> socketService(
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(socketService, NS_ERROR_FAILURE);
+
+ // with socket connections we want to read as much data as arrives
+ m_readCount = -1;
+
+ nsCOMPtr<nsISocketTransport> strans;
+ AutoTArray<nsCString, 1> connectionTypeArray;
+ if (connectionType) connectionTypeArray.AppendElement(connectionType);
+ rv = socketService->CreateTransport(
+ connectionTypeArray, nsDependentCString(aHostName), aGetPort, aProxyInfo,
+ nullptr, getter_AddRefs(strans));
+ if (NS_FAILED(rv)) return rv;
+
+ strans->SetSecurityCallbacks(callbacks);
+
+ // creates cyclic reference!
+ nsCOMPtr<nsIThread> currentThread(do_GetCurrentThread());
+ strans->SetEventSink(this, currentThread);
+
+ m_socketIsOpen = false;
+ m_transport = strans;
+
+ if (!gGotTimeoutPref) {
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (prefBranch) {
+ prefBranch->GetIntPref("mailnews.tcptimeout", &gSocketTimeout);
+ gGotTimeoutPref = true;
+ }
+ }
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, gSocketTimeout + 60);
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout);
+
+ uint8_t qos;
+ rv = GetQoSBits(&qos);
+ if (NS_SUCCEEDED(rv)) strans->SetQoSBits(qos);
+
+ return SetupTransportState();
+}
+
+nsresult nsMsgProtocol::GetFileFromURL(nsIURI* aURL, nsIFile** aResult) {
+ NS_ENSURE_ARG_POINTER(aURL);
+ NS_ENSURE_ARG_POINTER(aResult);
+ // extract the file path from the uri...
+ nsAutoCString urlSpec;
+ aURL->GetPathQueryRef(urlSpec);
+ urlSpec.InsertLiteral("file://", 0);
+ nsresult rv;
+
+ // dougt - there should be an easier way!
+ nsCOMPtr<nsIURI> uri;
+ if (NS_FAILED(rv = NS_NewURI(getter_AddRefs(uri), urlSpec.get()))) return rv;
+
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri);
+ if (!fileURL) return NS_ERROR_FAILURE;
+
+ return fileURL->GetFile(aResult);
+ // dougt
+}
+
+nsresult nsMsgProtocol::OpenFileSocket(nsIURI* aURL, uint64_t aStartPosition,
+ int64_t aReadCount) {
+ // mscott - file needs to be encoded directly into aURL. I should be able to
+ // get rid of this method completely.
+
+ nsresult rv = NS_OK;
+ m_readCount = aReadCount;
+ nsCOMPtr<nsIFile> file;
+
+ rv = GetFileFromURL(aURL, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
+ if (NS_FAILED(rv)) return rv;
+
+ // create input stream transport
+ nsCOMPtr<nsIStreamTransportService> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // This can be called with aReadCount == -1 which means "read as much as we
+ // can". We pass this on as UINT64_MAX, which is in fact uint64_t(-1).
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ stream.forget(), aStartPosition,
+ aReadCount == -1 ? UINT64_MAX : uint64_t(aReadCount));
+ rv = sts->CreateInputTransport(slicedStream, true,
+ getter_AddRefs(m_transport));
+
+ m_socketIsOpen = false;
+ return rv;
+}
+
+nsresult nsMsgProtocol::GetTopmostMsgWindow(nsIMsgWindow** aWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> mailSession(
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mailSession->GetTopmostMsgWindow(aWindow);
+}
+
+nsresult nsMsgProtocol::SetupTransportState() {
+ if (!m_socketIsOpen && m_transport) {
+ nsresult rv;
+
+ // open buffered, blocking output stream
+ rv = m_transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0,
+ getter_AddRefs(m_outputStream));
+ if (NS_FAILED(rv)) return rv;
+ // we want to open the stream
+ } // if m_transport
+
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::CloseSocket() {
+ nsresult rv = NS_OK;
+ // release all of our socket state
+ m_socketIsOpen = false;
+ m_outputStream = nullptr;
+ if (m_transport) {
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+ if (strans) {
+ strans->SetEventSink(nullptr, nullptr); // break cyclic reference!
+ }
+ }
+ // we need to call Cancel so that we remove the socket transport from the
+ // mActiveTransportList. see bug #30648
+ if (m_request) {
+ rv = m_request->Cancel(NS_BINDING_ABORTED);
+ }
+ m_request = nullptr;
+ if (m_transport) {
+ m_transport->Close(NS_BINDING_ABORTED);
+ m_transport = nullptr;
+ }
+
+ return rv;
+}
+
+/*
+ * Writes the data contained in dataBuffer into the current output stream. It
+ * also informs the transport layer that this data is now available for
+ * transmission. Returns a positive number for success, 0 for failure (not all
+ * the bytes were written to the stream, etc). We need to make another pass
+ * through this file to install an error system (mscott)
+ *
+ * No logging is done in the base implementation, so aSuppressLogging is
+ * ignored.
+ */
+
+nsresult nsMsgProtocol::SendData(const char* dataBuffer,
+ bool aSuppressLogging) {
+ uint32_t writeCount = 0;
+
+ if (dataBuffer && m_outputStream)
+ return m_outputStream->Write(dataBuffer, PL_strlen(dataBuffer),
+ &writeCount);
+ // TODO make sure all the bytes in PL_strlen(dataBuffer) were written
+ else
+ return NS_ERROR_INVALID_ARG;
+}
+
+// Whenever data arrives from the connection, core netlib notifices the protocol
+// by calling OnDataAvailable. We then read and process the incoming data from
+// the input stream.
+NS_IMETHODIMP nsMsgProtocol::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ // right now, this really just means turn around and churn through the state
+ // machine
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ return ProcessProtocolState(uri, inStr, sourceOffset, count);
+}
+
+NS_IMETHODIMP nsMsgProtocol::OnStartRequest(nsIRequest* request) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(uri);
+ rv = aMsgUrl->SetUrlState(true, NS_OK);
+ if (m_loadGroup)
+ m_loadGroup->AddRequest(static_cast<nsIRequest*>(this),
+ nullptr /* context isupports */);
+ }
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are starting... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (!mSuppressListenerNotifications && m_channelListener) {
+ m_isChannel = true;
+ rv = m_channelListener->OnStartRequest(this);
+ }
+
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+
+ if (strans)
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+void nsMsgProtocol::ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl,
+ nsresult aStatus) {
+ const char16_t* errorString = nullptr;
+ switch (aStatus) {
+ case NS_ERROR_UNKNOWN_HOST:
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ errorString = u"unknownHostError";
+ break;
+ case NS_ERROR_CONNECTION_REFUSED:
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ errorString = u"connectionRefusedError";
+ break;
+ case NS_ERROR_NET_TIMEOUT:
+ errorString = u"netTimeoutError";
+ break;
+ case NS_ERROR_NET_RESET:
+ errorString = u"netResetError";
+ break;
+ case NS_ERROR_NET_INTERRUPT:
+ errorString = u"netInterruptError";
+ break;
+ case NS_ERROR_OFFLINE:
+ // Don't alert when offline as that is already displayed in the UI.
+ return;
+ default:
+ nsPrintfCString msg(
+ "Unexpected status passed to ShowAlertMessage: %" PRIx32,
+ static_cast<uint32_t>(aStatus));
+ NS_WARNING(msg.get());
+ return;
+ }
+
+ nsString errorMsg;
+ errorMsg.Adopt(FormatStringWithHostNameByName(errorString, aMsgUrl));
+ if (errorMsg.IsEmpty()) {
+ errorMsg.AssignLiteral(u"[StringID ");
+ errorMsg.Append(errorString);
+ errorMsg.AppendLiteral(u"?]");
+ }
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (mailSession) mailSession->AlertUser(errorMsg, aMsgUrl);
+}
+
+// stop binding is a "notification" informing us that the stream associated with
+// aURL is going away.
+NS_IMETHODIMP nsMsgProtocol::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsresult rv = NS_OK;
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are starting... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (!mSuppressListenerNotifications && m_channelListener)
+ rv = m_channelListener->OnStopRequest(this, aStatus);
+
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(uri);
+ rv = msgUrl->SetUrlState(false, aStatus); // Always returns NS_OK.
+ if (m_loadGroup)
+ m_loadGroup->RemoveRequest(static_cast<nsIRequest*>(this), nullptr,
+ aStatus);
+
+ // !m_isChannel because if we're set up as a channel, then the remove
+ // request above will handle alerting the user, so we don't need to.
+ //
+ // !NS_BINDING_ABORTED because we don't want to see an alert if the user
+ // cancelled the operation. also, we'll get here because we call Cancel()
+ // to force removal of the nsSocketTransport. see CloseSocket()
+ // bugs #30775 and #30648 relate to this
+ if (!m_isChannel && NS_FAILED(aStatus) && (aStatus != NS_BINDING_ABORTED))
+ ShowAlertMessage(msgUrl, aStatus);
+ } // if we have a mailnews url.
+
+ // Drop notification callbacks to prevent cycles.
+ mCallbacks = nullptr;
+ mProgressEventSink = nullptr;
+ // Call CloseSocket(), in case we got here because the server dropped the
+ // connection while reading, and we never get a chance to get back into
+ // the protocol state machine via OnDataAvailable.
+ if (m_socketIsOpen) CloseSocket();
+
+ return rv;
+}
+
+nsresult nsMsgProtocol::LoadUrl(nsIURI* aURL, nsISupports* aConsumer) {
+ // nsMsgProtocol implements nsIChannel, and all channels are required to
+ // have non-null loadInfo. So if it's still unset, we've not been correctly
+ // initialised.
+ MOZ_ASSERT(m_loadInfo);
+
+ // okay now kick us off to the next state...
+ // our first state is a process state so drive the state machine...
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(aURL, &rv);
+
+ if (NS_SUCCEEDED(rv) && aMsgUrl) {
+ bool msgIsInLocalCache;
+ aMsgUrl->GetMsgIsInLocalCache(&msgIsInLocalCache);
+
+ // Set the url as a url currently being run...
+ rv = aMsgUrl->SetUrlState(true, NS_OK);
+
+ // if the url is given a stream consumer then we should use it to forward
+ // calls to...
+ if (!m_channelListener &&
+ aConsumer) // if we don't have a registered listener already
+ {
+ m_channelListener = do_QueryInterface(aConsumer);
+ m_isChannel = true;
+ }
+
+ if (!m_socketIsOpen) {
+ if (m_transport) {
+ // open buffered, asynchronous input stream
+ nsCOMPtr<nsIInputStream> stream;
+ rv = m_transport->OpenInputStream(0, 0, 0, getter_AddRefs(stream));
+ if (NS_FAILED(rv)) return rv;
+
+ // m_readCount can be -1 which means "read as much as we can".
+ // We pass this on as UINT64_MAX, which is in fact uint64_t(-1).
+ // We don't clone m_inputStream here, we simply give up ownership
+ // since otherwise the original would never be closed.
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ stream.forget(), 0,
+ m_readCount == -1 ? UINT64_MAX : uint64_t(m_readCount));
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), slicedStream.forget());
+ if (NS_FAILED(rv)) return rv;
+
+ m_request = pump; // keep a reference to the pump so we can cancel it
+
+ // Put us in a state where we are always notified of incoming data.
+ // OnDataAvailable() will be called when that happens, which will
+ // pass that data into ProcessProtocolState().
+ rv = pump->AsyncRead(this);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncRead failed");
+ m_socketIsOpen = true; // mark the channel as open
+ }
+ } else if (!msgIsInLocalCache) {
+ // The connection is already open so we should begin processing our url.
+ rv = ProcessProtocolState(aURL, nullptr, 0, 0);
+ }
+ }
+
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////
+// The rest of this file is mostly nsIChannel mumbo jumbo stuff
+///////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgProtocol::SetUrl(nsIURI* aURL) {
+ m_url = aURL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ m_loadGroup = aLoadGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetOriginalURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = m_originalUrl ? m_originalUrl : m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetOriginalURI(nsIURI* aURI) {
+ m_originalUrl = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Open(nsIInputStream** _retval) {
+ nsCOMPtr<nsIStreamListener> listener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_ImplementChannelOpen(this, _retval);
+}
+
+NS_IMETHODIMP nsMsgProtocol::AsyncOpen(nsIStreamListener* aListener) {
+ nsCOMPtr<nsIStreamListener> listener = aListener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = m_url->GetPort(&port);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString scheme;
+ rv = m_url->GetScheme(scheme);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = NS_CheckPortSafety(port, scheme.get());
+ if (NS_FAILED(rv)) return rv;
+
+ // set the stream listener and then load the url
+ m_isChannel = true;
+
+ m_channelListener = listener;
+ return LoadUrl(m_url, nullptr);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK; // don't fail when trying to set this
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentType(nsACString& aContentType) {
+ // as url dispatching matures, we'll be intelligent and actually start
+ // opening the url before specifying the content type. This will allow
+ // us to optimize the case where the message url actual refers to
+ // a part in the message that has a content type that is not message/rfc822
+
+ if (mContentType.IsEmpty())
+ aContentType.AssignLiteral("message/rfc822");
+ else
+ aContentType = mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentType(const nsACString& aContentType) {
+ nsAutoCString charset;
+ nsresult rv =
+ NS_ParseResponseContentType(aContentType, mContentType, charset);
+ if (NS_FAILED(rv) || mContentType.IsEmpty())
+ mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentCharset(nsACString& aContentCharset) {
+ aContentCharset.Assign(mCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentCharset(
+ const nsACString& aContentCharset) {
+ mCharset.Assign(aContentCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDisposition(uint32_t* aContentDisposition) {
+ *aContentDisposition = mContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetContentDisposition(uint32_t aContentDisposition) {
+ mContentDisposition = aContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentLength(int64_t* aContentLength) {
+ *aContentLength = mContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentLength(int64_t aContentLength) {
+ mContentLength = aContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetSecurityInfo(
+ nsITransportSecurityInfo** secInfo) {
+ *secInfo = nullptr;
+ if (m_transport) {
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+ if (strans) {
+ nsCOMPtr<nsITLSSocketControl> tlsSocketControl;
+ if (NS_SUCCEEDED(
+ strans->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)))) {
+ nsCOMPtr<nsITransportSecurityInfo> transportSecInfo;
+ if (NS_SUCCEEDED(tlsSocketControl->GetSecurityInfo(
+ getter_AddRefs(transportSecInfo)))) {
+ transportSecInfo.forget(secInfo);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetName(nsACString& result) {
+ if (m_url) return m_url->GetSpec(result);
+ result.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetOwner(nsISupports** aPrincipal) {
+ NS_IF_ADDREF(*aPrincipal = mOwner);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetOwner(nsISupports* aPrincipal) {
+ mOwner = aPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ NS_IF_ADDREF(*aLoadGroup = m_loadGroup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ NS_IF_ADDREF(*aLoadInfo = m_loadInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadInfo(nsILoadInfo* aLoadInfo) {
+ m_loadInfo = aLoadInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aNotificationCallbacks) {
+ NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aNotificationCallbacks) {
+ mCallbacks = aNotificationCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::OnTransportStatus(nsITransport* transport, nsresult status,
+ int64_t progress, int64_t progressMax) {
+ if ((mLoadFlags & LOAD_BACKGROUND) || !m_url) return NS_OK;
+
+ // these transport events should not generate any status messages
+ if (status == NS_NET_STATUS_RECEIVING_FROM ||
+ status == NS_NET_STATUS_SENDING_TO)
+ return NS_OK;
+
+ if (!mProgressEventSink) {
+ NS_QueryNotificationCallbacks(mCallbacks, m_loadGroup, mProgressEventSink);
+ if (!mProgressEventSink) return NS_OK;
+ }
+
+ nsAutoCString host;
+ m_url->GetHost(host);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ mailnewsUrl->GetServer(getter_AddRefs(server));
+ if (server) server->GetHostName(host);
+ }
+ mProgressEventSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetIsDocument(bool* aIsDocument) {
+ return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// From nsIRequest
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgProtocol::IsPending(bool* result) {
+ *result = m_channelListener != nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetStatus(nsresult* status) {
+ if (m_request) return m_request->GetStatus(status);
+
+ *status = NS_OK;
+ return *status;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::Cancel(nsresult status) {
+ if (m_proxyRequest) m_proxyRequest->Cancel(status);
+
+ if (m_request) return m_request->Cancel(status);
+
+ NS_WARNING("no request to cancel");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetCanceled(bool* aCanceled) {
+ nsresult status = NS_ERROR_FAILURE;
+ GetStatus(&status);
+ *aCanceled = NS_FAILED(status);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Suspend() {
+ if (m_request) return m_request->Suspend();
+
+ NS_WARNING("no request to suspend");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Resume() {
+ if (m_request) return m_request->Resume();
+
+ NS_WARNING("no request to resume");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsMsgProtocol::PostMessage(nsIURI* url, nsIFile* postFile) {
+ if (!url || !postFile) return NS_ERROR_NULL_POINTER;
+
+#define POST_DATA_BUFFER_SIZE 2048
+
+ // mscott -- this function should be re-written to use the file url code
+ // so it can be asynch
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv =
+ NS_NewLocalFileInputStream(getter_AddRefs(inputStream), postFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(inputStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsCString line;
+ nsCString outputBuffer;
+
+ do {
+ lineInputStream->ReadLine(line, &more);
+
+ /* escape starting periods
+ */
+ if (line.CharAt(0) == '.') line.Insert('.', 0);
+ line.AppendLiteral(CRLF);
+ outputBuffer.Append(line);
+ // test hack by mscott. If our buffer is almost full, then
+ // send it off & reset ourselves
+ // to make more room.
+ if (outputBuffer.Length() > POST_DATA_BUFFER_SIZE || !more) {
+ rv = SendData(outputBuffer.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ // does this keep the buffer around? That would be best.
+ // Maybe SetLength(0) instead?
+ outputBuffer.Truncate();
+ }
+ } while (more);
+
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::DoGSSAPIStep1(const nsACString& service,
+ const char* username,
+ nsCString& response) {
+ nsresult rv;
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 1 for service %s, username %s\n", service, username);
+#endif
+
+ // if this fails, then it means that we cannot do GSSAPI SASL.
+ m_authModule = nsIAuthModule::CreateInstance("sasl-gssapi");
+
+ m_authModule->Init(service, nsIAuthModule::REQ_DEFAULT, u""_ns,
+ NS_ConvertUTF8toUTF16(username), u""_ns);
+
+ void* outBuf;
+ uint32_t outBufLen;
+ rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ free(outBuf);
+ }
+
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 1 succeeded\n");
+#endif
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoGSSAPIStep2(nsCString& commandResponse,
+ nsCString& response) {
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 2\n");
+#endif
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen, outBufLen;
+ uint32_t len = commandResponse.Length();
+
+ // Cyrus SASL may send us zero length tokens (grrrr)
+ if (len > 0) {
+ // decode into the input secbuffer
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+ if (!inBuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // strip off any padding (see bug 230351)
+ const char* challenge = commandResponse.get();
+ while (challenge[len - 1] == '=') len--;
+
+ // We need to know the exact length of the decoded string to give to
+ // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable
+ // of telling us that. So, we figure it out for ourselves.
+
+ // For every 4 characters, add 3 to the destination
+ // If there are 3 remaining, add 2
+ // If there are 2 remaining, add 1
+ // 1 remaining is an error
+ inBufLen =
+ (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0);
+
+ rv = (PL_Base64Decode(challenge, len, (char*)inBuf))
+ ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen)
+ : NS_ERROR_FAILURE;
+
+ free(inBuf);
+ } else {
+ rv = m_authModule->GetNextToken(NULL, 0, &outBuf, &outBufLen);
+ }
+ if (NS_SUCCEEDED(rv)) {
+ // And in return, we may need to send Cyrus zero length tokens back
+ if (outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ } else
+ response.Adopt((char*)moz_xmemdup("", 1));
+ }
+
+#ifdef DEBUG_BenB
+ printf(NS_SUCCEEDED(rv) ? "GSSAPI step 2 succeeded\n"
+ : "GSSAPI step 2 failed\n");
+#endif
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoNtlmStep1(const nsACString& username,
+ const nsAString& password,
+ nsCString& response) {
+ nsresult rv;
+
+ m_authModule = nsIAuthModule::CreateInstance("ntlm");
+
+ m_authModule->Init(""_ns, 0, u""_ns, NS_ConvertUTF8toUTF16(username),
+ PromiseFlatString(password));
+
+ void* outBuf;
+ uint32_t outBufLen;
+ rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ free(outBuf);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoNtlmStep2(nsCString& commandResponse,
+ nsCString& response) {
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen, outBufLen;
+ uint32_t len = commandResponse.Length();
+
+ // decode into the input secbuffer
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+ if (!inBuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // strip off any padding (see bug 230351)
+ const char* challenge = commandResponse.get();
+ while (challenge[len - 1] == '=') len--;
+
+ rv = (PL_Base64Decode(challenge, len, (char*)inBuf))
+ ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen)
+ : NS_ERROR_FAILURE;
+
+ free(inBuf);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (NS_FAILED(rv)) response = "*";
+
+ return rv;
+}
+
+/////////////////////////////////////////////////////////////////////
+// nsMsgAsyncWriteProtocol subclass and related helper classes
+/////////////////////////////////////////////////////////////////////
+
+class nsMsgProtocolStreamProvider : public nsIOutputStreamCallback {
+ public:
+ // XXX this probably doesn't need to be threadsafe
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsMsgProtocolStreamProvider() {}
+
+ void Init(nsMsgAsyncWriteProtocol* aProtInstance,
+ nsIInputStream* aInputStream) {
+ mMsgProtocol =
+ do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance));
+ mInStream = aInputStream;
+ }
+
+ //
+ // nsIOutputStreamCallback implementation ...
+ //
+ NS_IMETHODIMP OnOutputStreamReady(nsIAsyncOutputStream* aOutStream) override {
+ NS_ASSERTION(mInStream, "not initialized");
+
+ nsresult rv;
+ uint64_t avail;
+
+ // Write whatever is available in the pipe. If the pipe is empty, then
+ // return NS_BASE_STREAM_WOULD_BLOCK; we will resume the write when there
+ // is more data.
+
+ rv = mInStream->Available(&avail);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mMsgProtocol);
+ if (!callback) return NS_ERROR_FAILURE;
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (avail == 0 && !protInst->mAsyncBuffer.Length()) {
+ // ok, stop writing...
+ protInst->mSuspendedWrite = true;
+ return NS_OK;
+ }
+ protInst->mSuspendedWrite = false;
+
+ uint32_t bytesWritten;
+
+ if (avail) {
+ rv = aOutStream->WriteFrom(mInStream,
+ std::min(avail, uint64_t(FILE_IO_BUFFER_SIZE)),
+ &bytesWritten);
+ // if were full at the time, the input stream may be backed up and we need
+ // to read any remains from the last ODA call before we'll get more ODA
+ // calls
+ if (protInst->mSuspendedRead) protInst->UnblockPostReader();
+ } else {
+ rv = aOutStream->Write(protInst->mAsyncBuffer.get(),
+ protInst->mAsyncBuffer.Length(), &bytesWritten);
+ protInst->mAsyncBuffer.Cut(0, bytesWritten);
+ }
+
+ protInst->UpdateProgress(bytesWritten);
+
+ // try to write again...
+ if (NS_SUCCEEDED(rv))
+ rv = aOutStream->AsyncWait(this, 0, 0, protInst->mProviderThread);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv) || rv == NS_BINDING_ABORTED,
+ "unexpected error writing stream");
+ return NS_OK;
+ }
+
+ protected:
+ virtual ~nsMsgProtocolStreamProvider() {}
+
+ nsWeakPtr mMsgProtocol;
+ nsCOMPtr<nsIInputStream> mInStream;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgProtocolStreamProvider, nsIOutputStreamCallback)
+
+class nsMsgFilePostHelper : public nsIStreamListener {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsMsgFilePostHelper() { mSuspendedPostFileRead = false; }
+ nsresult Init(nsIOutputStream* aOutStream,
+ nsMsgAsyncWriteProtocol* aProtInstance, nsIFile* aFileToPost);
+ nsCOMPtr<nsIRequest> mPostFileRequest;
+ bool mSuspendedPostFileRead;
+ void CloseSocket() { mProtInstance = nullptr; }
+
+ protected:
+ virtual ~nsMsgFilePostHelper() {}
+ nsCOMPtr<nsIOutputStream> mOutStream;
+ nsWeakPtr mProtInstance;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFilePostHelper, nsIStreamListener, nsIRequestObserver)
+
+nsresult nsMsgFilePostHelper::Init(nsIOutputStream* aOutStream,
+ nsMsgAsyncWriteProtocol* aProtInstance,
+ nsIFile* aFileToPost) {
+ nsresult rv = NS_OK;
+ mOutStream = aOutStream;
+ mProtInstance =
+ do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance));
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFileToPost);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget());
+ if (NS_FAILED(rv)) return rv;
+
+ rv = pump->AsyncRead(this);
+ if (NS_FAILED(rv)) return rv;
+
+ mPostFileRequest = pump;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnStartRequest(nsIRequest* aChannel) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnStopRequest(nsIRequest* aChannel,
+ nsresult aStatus) {
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance);
+ if (!callback) return NS_OK;
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (!mSuspendedPostFileRead) protInst->PostDataFinished();
+
+ mSuspendedPostFileRead = false;
+ protInst->mFilePostHelper = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnDataAvailable(nsIRequest* /* aChannel */,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance);
+ if (!callback) return NS_OK;
+
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (mSuspendedPostFileRead) {
+ protInst->UpdateSuspendedReadBytes(count, protInst->mInsertPeriodRequired);
+ return NS_OK;
+ }
+
+ protInst->ProcessIncomingPostData(inStr, count);
+
+ if (protInst->mSuspendedWrite) {
+ // if we got here then we had suspended the write 'cause we didn't have
+ // anymore data to write (i.e. the pipe went empty). So resume the channel
+ // to kick things off again.
+ protInst->mSuspendedWrite = false;
+ protInst->mAsyncOutStream->AsyncWait(protInst->mProvider, 0, 0,
+ protInst->mProviderThread);
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ADDREF_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol)
+NS_IMPL_RELEASE_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol)
+
+NS_INTERFACE_MAP_BEGIN(nsMsgAsyncWriteProtocol)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgProtocol)
+
+nsMsgAsyncWriteProtocol::nsMsgAsyncWriteProtocol(nsIURI* aURL)
+ : nsMsgProtocol(aURL) {
+ mSuspendedWrite = false;
+ mSuspendedReadBytes = 0;
+ mSuspendedRead = false;
+ mInsertPeriodRequired = false;
+ mGenerateProgressNotifications = false;
+ mSuspendedReadBytesPostPeriod = 0;
+ mFilePostHelper = nullptr;
+ mNumBytesPosted = 0;
+ mFilePostSize = 0;
+}
+
+nsMsgAsyncWriteProtocol::~nsMsgAsyncWriteProtocol() {}
+
+NS_IMETHODIMP nsMsgAsyncWriteProtocol::Cancel(nsresult status) {
+ mGenerateProgressNotifications = false;
+
+ if (m_proxyRequest) {
+ m_proxyRequest->Cancel(status);
+ }
+
+ if (m_request) m_request->Cancel(status);
+
+ if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(status);
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::PostMessage(nsIURI* url, nsIFile* file) {
+ nsCOMPtr<nsIStreamListener> listener = new nsMsgFilePostHelper();
+
+ if (!listener) return NS_ERROR_OUT_OF_MEMORY;
+
+ // be sure to initialize some state before posting
+ mSuspendedReadBytes = 0;
+ mNumBytesPosted = 0;
+ file->GetFileSize(&mFilePostSize);
+ mSuspendedRead = false;
+ mInsertPeriodRequired = false;
+ mSuspendedReadBytesPostPeriod = 0;
+ mGenerateProgressNotifications = true;
+
+ mFilePostHelper = static_cast<nsMsgFilePostHelper*>(
+ static_cast<nsIStreamListener*>(listener));
+
+ static_cast<nsMsgFilePostHelper*>(static_cast<nsIStreamListener*>(listener))
+ ->Init(m_outputStream, this, file);
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SuspendPostFileRead() {
+ if (mFilePostHelper && !mFilePostHelper->mSuspendedPostFileRead) {
+ // uhoh we need to pause reading in the file until we get unblocked...
+ mFilePostHelper->mPostFileRequest->Suspend();
+ mFilePostHelper->mSuspendedPostFileRead = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::ResumePostFileRead() {
+ if (mFilePostHelper) {
+ if (mFilePostHelper->mSuspendedPostFileRead) {
+ mFilePostHelper->mPostFileRequest->Resume();
+ mFilePostHelper->mSuspendedPostFileRead = false;
+ }
+ } else // we must be done with the download so send the '.'
+ {
+ PostDataFinished();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::UpdateSuspendedReadBytes(
+ uint32_t aNewBytes, bool aAddToPostPeriodByteCount) {
+ // depending on our current state, we'll either add aNewBytes to
+ // mSuspendedReadBytes or mSuspendedReadBytesAfterPeriod.
+
+ mSuspendedRead = true;
+ if (aAddToPostPeriodByteCount)
+ mSuspendedReadBytesPostPeriod += aNewBytes;
+ else
+ mSuspendedReadBytes += aNewBytes;
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::PostDataFinished() {
+ nsresult rv = SendData("." CRLF);
+ if (NS_FAILED(rv)) return rv;
+ mGenerateProgressNotifications = false;
+ mPostDataStream = nullptr;
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::ProcessIncomingPostData(nsIInputStream* inStr,
+ uint32_t count) {
+ if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled
+
+ // We need to quote any '.' that occur at the beginning of a line.
+ // but I don't want to waste time reading out the data into a buffer and
+ // searching let's try to leverage nsIBufferedInputStream and see if we can
+ // "peek" into the current contents for this particular case.
+
+ nsCOMPtr<nsISearchableInputStream> bufferInputStr = do_QueryInterface(inStr);
+ NS_ASSERTION(
+ bufferInputStr,
+ "i made a wrong assumption about the type of stream we are getting");
+ NS_ASSERTION(mSuspendedReadBytes == 0, "oops, I missed something");
+
+ if (!mPostDataStream) mPostDataStream = inStr;
+
+ if (bufferInputStr) {
+ uint32_t amountWritten;
+
+ while (count > 0) {
+ bool found = false;
+ uint32_t offset = 0;
+ bufferInputStr->Search("\012.", true, &found, &offset); // LF.
+
+ if (!found || offset > count) {
+ // push this data into the output stream
+ m_outputStream->WriteFrom(inStr, count, &amountWritten);
+ // store any remains which need read out at a later date
+ if (count > amountWritten) // stream will block
+ {
+ UpdateSuspendedReadBytes(count - amountWritten, false);
+ SuspendPostFileRead();
+ }
+ break;
+ } else {
+ // count points to the LF in a LF followed by a '.'
+ // go ahead and write up to offset..
+ m_outputStream->WriteFrom(inStr, offset + 1, &amountWritten);
+ count -= amountWritten;
+ if (offset + 1 > amountWritten) {
+ UpdateSuspendedReadBytes(offset + 1 - amountWritten, false);
+ mInsertPeriodRequired = true;
+ UpdateSuspendedReadBytes(count, mInsertPeriodRequired);
+ SuspendPostFileRead();
+ break;
+ }
+
+ // write out the extra '.'
+ m_outputStream->Write(".", 1, &amountWritten);
+ if (amountWritten != 1) {
+ mInsertPeriodRequired = true;
+ // once we do write out the '.', if we are now blocked we need to
+ // remember the remaining count that comes after the '.' so we can
+ // perform processing on that once we become unblocked.
+ UpdateSuspendedReadBytes(count, mInsertPeriodRequired);
+ SuspendPostFileRead();
+ break;
+ }
+ }
+ } // while count > 0
+ }
+
+ return NS_OK;
+}
+nsresult nsMsgAsyncWriteProtocol::UnblockPostReader() {
+ uint32_t amountWritten = 0;
+
+ if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled
+
+ if (mSuspendedRead) {
+ // (1) attempt to write out any remaining read bytes we need in order to
+ // unblock the reader
+ if (mSuspendedReadBytes > 0 && mPostDataStream) {
+ uint64_t avail = 0;
+ mPostDataStream->Available(&avail);
+
+ m_outputStream->WriteFrom(mPostDataStream,
+ std::min(avail, uint64_t(mSuspendedReadBytes)),
+ &amountWritten);
+ // hmm sometimes my mSuspendedReadBytes is getting out of whack...so for
+ // now, reset it if necessary.
+ if (mSuspendedReadBytes > avail) mSuspendedReadBytes = avail;
+
+ if (mSuspendedReadBytes > amountWritten)
+ mSuspendedReadBytes -= amountWritten;
+ else
+ mSuspendedReadBytes = 0;
+ }
+
+ // (2) if we are now unblocked, and we need to insert a '.' then do so
+ // now...
+ if (mInsertPeriodRequired && mSuspendedReadBytes == 0) {
+ amountWritten = 0;
+ m_outputStream->Write(".", 1, &amountWritten);
+ if (amountWritten == 1) // if we succeeded then clear pending '.' flag
+ mInsertPeriodRequired = false;
+ }
+
+ // (3) if we inserted a '.' and we still have bytes after the '.' which need
+ // processed before the stream is unblocked then fake an ODA call to handle
+ // this now...
+ if (!mInsertPeriodRequired && mSuspendedReadBytesPostPeriod > 0) {
+ // these bytes actually need processed for extra '.''s.....
+ uint32_t postbytes = mSuspendedReadBytesPostPeriod;
+ mSuspendedReadBytesPostPeriod = 0;
+ ProcessIncomingPostData(mPostDataStream, postbytes);
+ }
+
+ // (4) determine if we are out of the suspended read state...
+ if (mSuspendedReadBytes == 0 && !mInsertPeriodRequired &&
+ mSuspendedReadBytesPostPeriod == 0) {
+ mSuspendedRead = false;
+ ResumePostFileRead();
+ }
+
+ } // if we are in the suspended read state
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SetupTransportState() {
+ nsresult rv = NS_OK;
+
+ if (!m_outputStream && m_transport) {
+ // first create a pipe which we'll use to write the data we want to send
+ // into.
+ nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1");
+ rv = pipe->Init(true, true, 1024, 8);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIAsyncInputStream* inputStream = nullptr;
+ // This always succeeds because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetInputStream(&inputStream));
+ mInStream = dont_AddRef(static_cast<nsIInputStream*>(inputStream));
+
+ nsIAsyncOutputStream* outputStream = nullptr;
+ // This always succeeds because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetOutputStream(&outputStream));
+ m_outputStream = dont_AddRef(static_cast<nsIOutputStream*>(outputStream));
+
+ mProviderThread = do_GetCurrentThread();
+
+ nsMsgProtocolStreamProvider* provider = new nsMsgProtocolStreamProvider();
+
+ if (!provider) return NS_ERROR_OUT_OF_MEMORY;
+
+ provider->Init(this, mInStream);
+ mProvider = provider; // ADDREF
+
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = m_transport->OpenOutputStream(0, 0, 0, getter_AddRefs(stream));
+ if (NS_FAILED(rv)) return rv;
+
+ mAsyncOutStream = do_QueryInterface(stream, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // wait for the output stream to become writable
+ rv = mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread);
+ } // if m_transport
+
+ return rv;
+}
+
+nsresult nsMsgAsyncWriteProtocol::CloseSocket() {
+ nsresult rv = NS_OK;
+ if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(NS_BINDING_ABORTED);
+
+ nsMsgProtocol::CloseSocket();
+
+ if (mFilePostHelper) {
+ mFilePostHelper->CloseSocket();
+ mFilePostHelper = nullptr;
+ }
+
+ mAsyncOutStream = nullptr;
+ mProvider = nullptr;
+ mProviderThread = nullptr;
+ mAsyncBuffer.Truncate();
+ return rv;
+}
+
+void nsMsgAsyncWriteProtocol::UpdateProgress(uint32_t aNewBytes) {
+ if (!mGenerateProgressNotifications) return;
+
+ mNumBytesPosted += aNewBytes;
+ if (mFilePostSize > 0) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(m_url);
+ if (!mailUrl) return;
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (!statusFeedback) return;
+
+ nsCOMPtr<nsIWebProgressListener> webProgressListener(
+ do_QueryInterface(statusFeedback));
+ if (!webProgressListener) return;
+
+ // XXX not sure if m_request is correct here
+ webProgressListener->OnProgressChange(nullptr, m_request, mNumBytesPosted,
+ static_cast<uint32_t>(mFilePostSize),
+ mNumBytesPosted, mFilePostSize);
+ }
+
+ return;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SendData(const char* dataBuffer,
+ bool aSuppressLogging) {
+ this->mAsyncBuffer.Append(dataBuffer);
+ if (!mAsyncOutStream) return NS_ERROR_FAILURE;
+ return mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread);
+}
+
+char16_t* FormatStringWithHostNameByName(const char16_t* stringName,
+ nsIMsgMailNewsUrl* msgUri) {
+ if (!msgUri) return nullptr;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, nullptr);
+
+ nsCOMPtr<nsIStringBundle> sBundle;
+ rv = sBundleService->CreateBundle(MSGS_URL, getter_AddRefs(sBundle));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = msgUri->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCString hostName;
+ rv = server->GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ AutoTArray<nsString, 1> params;
+ CopyASCIItoUTF16(hostName, *params.AppendElement());
+ nsAutoString str;
+ rv = sBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(stringName).get(),
+ params, str);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return ToNewUnicode(str);
+}
+
+// vim: ts=2 sw=2
diff --git a/comm/mailnews/base/src/nsMsgProtocol.h b/comm/mailnews/base/src/nsMsgProtocol.h
new file mode 100644
index 0000000000..c4aa707300
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProtocol.h
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgProtocol_h__
+#define nsMsgProtocol_h__
+
+#include "mozilla/Attributes.h"
+#include "nsIStreamListener.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIChannel.h"
+#include "nsIURL.h"
+#include "nsIThread.h"
+#include "nsILoadGroup.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIProgressEventSink.h"
+#include "nsITransport.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAuthModule.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "nsHashPropertyBag.h"
+#include "nsMailChannel.h"
+
+class nsIMsgWindow;
+class nsIPrompt;
+class nsIMsgMailNewsUrl;
+class nsMsgFilePostHelper;
+class nsIProxyInfo;
+class nsICancelable;
+
+// This is a helper class used to encapsulate code shared between all of the
+// mailnews protocol objects (imap, news, pop, smtp, etc.) In particular,
+// it unifies the core networking code for the protocols. My hope is that
+// this will make unification with Necko easier as we'll only have to change
+// this class and not all of the mailnews protocols.
+class nsMsgProtocol : public nsIStreamListener,
+ public nsIChannel,
+ public nsITransportEventSink,
+ public nsMailChannel,
+ public nsHashPropertyBag {
+ public:
+ nsMsgProtocol(nsIURI* aURL);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ // nsIChannel support
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIREQUEST
+
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSITRANSPORTEVENTSINK
+
+ // LoadUrl -- A protocol typically overrides this function, sets up any local
+ // state for the url and then calls the base class which opens the socket if
+ // it needs opened. If the socket is already opened then we just call
+ // ProcessProtocolState to start the churning process. aConsumer is the
+ // consumer for the url. It can be null if this argument is not appropriate
+ virtual nsresult LoadUrl(nsIURI* aURL, nsISupports* aConsumer = nullptr);
+
+ virtual nsresult SetUrl(
+ nsIURI* aURL); // sometimes we want to set the url before we load it
+ void ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl, nsresult aStatus);
+
+ // Flag manipulators
+ virtual bool TestFlag(uint32_t flag) { return flag & m_flags; }
+ virtual void SetFlag(uint32_t flag) { m_flags |= flag; }
+ virtual void ClearFlag(uint32_t flag) { m_flags &= ~flag; }
+
+ protected:
+ virtual ~nsMsgProtocol();
+
+ // methods for opening and closing a socket with core netlib....
+ // mscott -okay this is lame. I should break this up into a file protocol and
+ // a socket based protocool class instead of cheating and putting both methods
+ // here...
+
+ // open a connection with a specific host and port
+ // aHostName must be UTF-8 encoded.
+ virtual nsresult OpenNetworkSocketWithInfo(const char* aHostName,
+ int32_t aGetPort,
+ const char* connectionType,
+ nsIProxyInfo* aProxyInfo,
+ nsIInterfaceRequestor* callbacks);
+ // helper routine
+ nsresult GetFileFromURL(nsIURI* aURL, nsIFile** aResult);
+ virtual nsresult OpenFileSocket(
+ nsIURI* aURL, uint64_t aStartPosition,
+ int64_t aReadCount); // used to open a file socket connection
+
+ nsresult GetTopmostMsgWindow(nsIMsgWindow** aWindow);
+
+ virtual const char* GetType() { return nullptr; }
+ nsresult GetQoSBits(uint8_t* aQoSBits);
+
+ // a Protocol typically overrides this method. They free any of their own
+ // connection state and then they call up into the base class to free the
+ // generic connection objects
+ virtual nsresult CloseSocket();
+
+ virtual nsresult
+ SetupTransportState(); // private method used by OpenNetworkSocket and
+ // OpenFileSocket
+
+ // ProcessProtocolState - This is the function that gets churned by calls to
+ // OnDataAvailable. As data arrives on the socket, OnDataAvailable calls
+ // ProcessProtocolState.
+
+ virtual nsresult ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) = 0;
+
+ // SendData -- Writes the data contained in dataBuffer into the current output
+ // stream. It also informs the transport layer that this data is now available
+ // for transmission. Returns a positive number for success, 0 for failure (not
+ // all the bytes were written to the stream, etc). aSuppressLogging is a hint
+ // that sensitive data is being sent and should not be logged
+ virtual nsresult SendData(const char* dataBuffer,
+ bool aSuppressLogging = false);
+
+ virtual nsresult PostMessage(nsIURI* url, nsIFile* aPostFile);
+
+ virtual nsresult InitFromURI(nsIURI* aUrl);
+
+ nsresult DoNtlmStep1(const nsACString& username, const nsAString& password,
+ nsCString& response);
+ nsresult DoNtlmStep2(nsCString& commandResponse, nsCString& response);
+
+ nsresult DoGSSAPIStep1(const nsACString& service, const char* username,
+ nsCString& response);
+ nsresult DoGSSAPIStep2(nsCString& commandResponse, nsCString& response);
+ // Output stream for writing commands to the socket
+ nsCOMPtr<nsIOutputStream>
+ m_outputStream; // this will be obtained from the transport interface
+
+ // Output stream for writing commands to the socket
+ nsCOMPtr<nsITransport> m_transport;
+ nsCOMPtr<nsIRequest> m_request;
+ nsCOMPtr<nsICancelable> m_proxyRequest;
+
+ bool m_socketIsOpen; // mscott: we should look into keeping this state in the
+ // nsSocketTransport... I'm using it to make sure I open
+ // the socket the first time a URL is loaded into the
+ // connection
+ uint32_t m_flags; // used to store flag information
+ // uint32_t m_startPosition;
+ int64_t m_readCount;
+
+ nsCOMPtr<nsIFile>
+ m_tempMsgFile; // we currently have a hack where displaying a msg
+ // involves writing it to a temp file first
+
+ // auth module for access to NTLM functions
+ nsCOMPtr<nsIAuthModule> m_authModule;
+
+ // the following is a catch all for nsIChannel related data
+ nsCOMPtr<nsIURI> m_originalUrl; // the original url
+ nsCOMPtr<nsIURI> m_url; // the running url
+ nsCOMPtr<nsISupports> m_consumer;
+ nsCOMPtr<nsIStreamListener> m_channelListener;
+ bool m_isChannel;
+ nsCOMPtr<nsILoadGroup> m_loadGroup;
+ nsLoadFlags mLoadFlags;
+ nsCOMPtr<nsIProgressEventSink> mProgressEventSink;
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsISupports> mOwner;
+ nsCString mContentType;
+ nsCString mCharset;
+ int64_t mContentLength;
+ nsCOMPtr<nsILoadInfo> m_loadInfo;
+
+ nsString m_lastPasswordSent; // used to prefill the password prompt
+
+ // if a url isn't going to result in any content then we want to suppress
+ // calls to OnStartRequest, OnDataAvailable and OnStopRequest
+ bool mSuppressListenerNotifications;
+
+ uint32_t mContentDisposition;
+};
+
+// This is is a subclass of nsMsgProtocol extends the parent class with
+// AsyncWrite support. Protocols like smtp and news want to leverage async
+// write. We don't want everyone who inherits from nsMsgProtocol to have to pick
+// up the extra overhead.
+class nsMsgAsyncWriteProtocol : public nsMsgProtocol,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMETHOD Cancel(nsresult status) override;
+
+ nsMsgAsyncWriteProtocol(nsIURI* aURL);
+
+ // temporary over ride...
+ virtual nsresult PostMessage(nsIURI* url, nsIFile* postFile) override;
+
+ // over ride the following methods from the base class
+ virtual nsresult SetupTransportState() override;
+ virtual nsresult SendData(const char* dataBuffer,
+ bool aSuppressLogging = false) override;
+ nsCString mAsyncBuffer;
+
+ // if we suspended the asynch write while waiting for more data to write then
+ // this will be TRUE
+ bool mSuspendedWrite;
+ nsCOMPtr<nsIRequest> m_WriteRequest;
+ nsCOMPtr<nsIAsyncOutputStream> mAsyncOutStream;
+ nsCOMPtr<nsIOutputStreamCallback> mProvider;
+ nsCOMPtr<nsIThread> mProviderThread;
+
+ // because we are reading the post data in asynchronously, it's possible that
+ // we aren't sending it out fast enough and the reading gets blocked. The
+ // following set of state variables are used to track this.
+ bool mSuspendedRead;
+ bool mInsertPeriodRequired; // do we need to insert a '.' as part of the
+ // unblocking process
+
+ nsresult ProcessIncomingPostData(nsIInputStream* inStr, uint32_t count);
+ nsresult UnblockPostReader();
+ nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes,
+ bool aAddToPostPeriodByteCount);
+ nsresult PostDataFinished(); // this is so we'll send out a closing '.' and
+ // release any state related to the post
+
+ // these two routines are used to pause and resume our loading of the file
+ // containing the contents we are trying to post. We call these routines when
+ // we aren't sending the bits out fast enough to keep up with the file read.
+ nsresult SuspendPostFileRead();
+ nsresult ResumePostFileRead();
+ nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes);
+ void UpdateProgress(uint32_t aNewBytes);
+ nsMsgFilePostHelper* mFilePostHelper; // needs to be a weak reference
+ protected:
+ virtual ~nsMsgAsyncWriteProtocol();
+
+ // the streams for the pipe used to queue up data for the async write calls to
+ // the server. we actually re-use the same mOutStream variable in our parent
+ // class for the output stream to the socket channel. So no need for a new
+ // variable here.
+ nsCOMPtr<nsIInputStream> mInStream;
+ nsCOMPtr<nsIInputStream> mPostDataStream;
+ uint32_t mSuspendedReadBytes; // remaining # of bytes we need to read before
+ // the input stream becomes unblocked
+ uint32_t mSuspendedReadBytesPostPeriod; // # of bytes which need processed
+ // after we insert a '.' before the
+ // input stream becomes unblocked.
+ int64_t
+ mFilePostSize; // used for file size, we post a single message in a file
+ uint32_t mNumBytesPosted; // used for determining progress on posting files
+ bool
+ mGenerateProgressNotifications; // set during a post operation after
+ // we've started sending the post data...
+
+ virtual nsresult CloseSocket() override;
+};
+
+#endif /* nsMsgProtocol_h__ */
diff --git a/comm/mailnews/base/src/nsMsgPurgeService.cpp b/comm/mailnews/base/src/nsMsgPurgeService.cpp
new file mode 100644
index 0000000000..0f72416c7b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgPurgeService.cpp
@@ -0,0 +1,496 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgPurgeService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgUtils.h"
+#include "nsMsgSearchCore.h"
+#include "msgCore.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "mozilla/Logging.h"
+#include "nsMsgFolderFlags.h"
+#include "nsITimer.h"
+#include <stdlib.h>
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+static mozilla::LazyLogModule MsgPurgeLogModule("MsgPurge");
+
+NS_IMPL_ISUPPORTS(nsMsgPurgeService, nsIMsgPurgeService, nsIMsgSearchNotify)
+
+void OnPurgeTimer(nsITimer* timer, void* aPurgeService) {
+ nsMsgPurgeService* purgeService = (nsMsgPurgeService*)aPurgeService;
+ purgeService->PerformPurge();
+}
+
+nsMsgPurgeService::nsMsgPurgeService() {
+ mHaveShutdown = false;
+ // never purge a folder more than once every 8 hours (60 min/hour * 8 hours.
+ mMinDelayBetweenPurges = 480;
+ // fire the purge timer every 5 minutes, starting 5 minutes after the service
+ // is created (when we load accounts).
+ mPurgeTimerInterval = 5;
+}
+
+nsMsgPurgeService::~nsMsgPurgeService() {
+ if (mPurgeTimer) mPurgeTimer->Cancel();
+
+ if (!mHaveShutdown) Shutdown();
+}
+
+NS_IMETHODIMP nsMsgPurgeService::Init() {
+ nsresult rv;
+
+ // these prefs are here to help QA test this feature
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ int32_t min_delay;
+ rv = prefBranch->GetIntPref("mail.purge.min_delay", &min_delay);
+ if (NS_SUCCEEDED(rv) && min_delay) mMinDelayBetweenPurges = min_delay;
+
+ int32_t purge_timer_interval;
+ rv = prefBranch->GetIntPref("mail.purge.timer_interval",
+ &purge_timer_interval);
+ if (NS_SUCCEEDED(rv) && purge_timer_interval)
+ mPurgeTimerInterval = purge_timer_interval;
+ }
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("mail.purge.min_delay=%d minutes", mMinDelayBetweenPurges));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("mail.purge.timer_interval=%d minutes", mPurgeTimerInterval));
+
+ // don't start purging right away.
+ // because the accounts aren't loaded and because the user might be trying to
+ // sign in or startup, etc.
+ SetupNextPurge();
+
+ mHaveShutdown = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::Shutdown() {
+ if (mPurgeTimer) {
+ mPurgeTimer->Cancel();
+ mPurgeTimer = nullptr;
+ }
+
+ mHaveShutdown = true;
+ return NS_OK;
+}
+
+nsresult nsMsgPurgeService::SetupNextPurge() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("setting to check again in %d minutes", mPurgeTimerInterval));
+
+ // Convert mPurgeTimerInterval into milliseconds
+ uint32_t timeInMSUint32 = mPurgeTimerInterval * 60000;
+
+ // Can't currently reset a timer when it's in the process of
+ // calling Notify. So, just release the timer here and create a new one.
+ if (mPurgeTimer) mPurgeTimer->Cancel();
+
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mPurgeTimer), OnPurgeTimer, (void*)this, timeInMSUint32,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgPurgeService::OnPurgeTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mPurgeTimer timer");
+ }
+
+ return NS_OK;
+}
+
+// This is the function that looks for the first folder to purge. It also
+// applies retention settings to any folder that hasn't had retention settings
+// applied in mMinDelayBetweenPurges minutes (default, 8 hours).
+// However, if we've spent more than .5 seconds in this loop, don't
+// apply any more retention settings because it might lock up the UI.
+// This might starve folders later on in the hierarchy, since we always
+// start at the top, but since we also apply retention settings when you
+// open a folder, or when you compact all folders, I think this will do
+// for now, until we have a cleanup on shutdown architecture.
+nsresult nsMsgPurgeService::PerformPurge() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("performing purge"));
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool keepApplyingRetentionSettings = true;
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountManager->GetAllServers(allServers);
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%d servers", (int)allServers.Length()));
+ nsCOMPtr<nsIMsgFolder> folderToPurge;
+ PRIntervalTime startTime = PR_IntervalNow();
+ int32_t purgeIntervalToUse = 0;
+ PRTime oldestPurgeTime =
+ 0; // we're going to pick the least-recently purged folder
+
+ // apply retention settings to folders that haven't had retention settings
+ // applied in mMinDelayBetweenPurges minutes (default 8 hours)
+ // Because we get last purge time from the folder cache,
+ // this code won't open db's for folders until it decides it needs
+ // to apply retention settings, and since
+ // nsIMsgFolder::ApplyRetentionSettings will close any db's it opens, this
+ // code won't leave db's open.
+ for (uint32_t serverIndex = 0; serverIndex < allServers.Length();
+ serverIndex++) {
+ nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
+ if (server) {
+ if (keepApplyingRetentionSettings) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgFolder>> childFolders;
+ rv = rootFolder->GetDescendants(childFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto childFolder : childFolders) {
+ uint32_t folderFlags;
+ (void)childFolder->GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Virtual) continue;
+ PRTime curFolderLastPurgeTime = 0;
+ nsCString curFolderLastPurgeTimeString, curFolderUri;
+ rv = childFolder->GetStringProperty("LastPurgeTime",
+ curFolderLastPurgeTimeString);
+ if (NS_FAILED(rv))
+ continue; // it is ok to fail, go on to next folder
+
+ if (!curFolderLastPurgeTimeString.IsEmpty()) {
+ PRTime theTime;
+ PR_ParseTimeString(curFolderLastPurgeTimeString.get(), false,
+ &theTime);
+ curFolderLastPurgeTime = theTime;
+ }
+
+ childFolder->GetURI(curFolderUri);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%s curFolderLastPurgeTime=%s (if blank, then never)",
+ curFolderUri.get(), curFolderLastPurgeTimeString.get()));
+
+ // check if this folder is due to purge
+ // has to have been purged at least mMinDelayBetweenPurges minutes
+ // ago we don't want to purge the folders all the time - once a
+ // day is good enough
+ int64_t minDelayBetweenPurges(mMinDelayBetweenPurges);
+ int64_t microSecondsPerMinute(60000000);
+ PRTime nextPurgeTime =
+ curFolderLastPurgeTime +
+ (minDelayBetweenPurges * microSecondsPerMinute);
+ if (nextPurgeTime < PR_Now()) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("purging %s", curFolderUri.get()));
+ childFolder->ApplyRetentionSettings();
+ }
+ PRIntervalTime elapsedTime = PR_IntervalNow() - startTime;
+ // check if more than 500 milliseconds have elapsed in this purge
+ // process
+ if (PR_IntervalToMilliseconds(elapsedTime) > 500) {
+ keepApplyingRetentionSettings = false;
+ break;
+ }
+ }
+ }
+ nsCString type;
+ nsresult rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostName;
+ server->GetHostName(hostName);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s (%s)", serverIndex, hostName.get(), type.get()));
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t spamLevel;
+ spamSettings->GetLevel(&spamLevel);
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] spamLevel=%d (if 0, don't purge)", serverIndex, spamLevel));
+ // clang-format on
+ if (!spamLevel) continue;
+
+ // check if we are set up to purge for this server
+ // if not, skip it.
+ bool purgeSpam;
+ spamSettings->GetPurge(&purgeSpam);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] purgeSpam=%s (if false, don't purge)", serverIndex,
+ purgeSpam ? "true" : "false"));
+ if (!purgeSpam) continue;
+
+ // check if the spam folder uri is set for this server
+ // if not skip it.
+ nsCString junkFolderURI;
+ rv = spamSettings->GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] junkFolderURI=%s (if empty, don't purge)", serverIndex,
+ junkFolderURI.get()));
+ if (junkFolderURI.IsEmpty()) continue;
+
+ // if the junk folder doesn't exist
+ // because the folder pane isn't built yet, for example
+ // skip this account
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ rv = FindFolder(junkFolderURI, getter_AddRefs(junkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s exists? %s (if doesn't exist, don't purge)", serverIndex,
+ junkFolderURI.get(), junkFolder ? "true" : "false"));
+ // clang-format on
+ if (!junkFolder) continue;
+
+ PRTime curJunkFolderLastPurgeTime = 0;
+ nsCString curJunkFolderLastPurgeTimeString;
+ rv = junkFolder->GetStringProperty("curJunkFolderLastPurgeTime",
+ curJunkFolderLastPurgeTimeString);
+ if (NS_FAILED(rv))
+ continue; // it is ok to fail, junk folder may not exist
+
+ if (!curJunkFolderLastPurgeTimeString.IsEmpty()) {
+ PRTime theTime;
+ PR_ParseTimeString(curJunkFolderLastPurgeTimeString.get(), false,
+ &theTime);
+ curJunkFolderLastPurgeTime = theTime;
+ }
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s curJunkFolderLastPurgeTime=%s (if blank, then never)",
+ serverIndex, junkFolderURI.get(),
+ curJunkFolderLastPurgeTimeString.get()));
+
+ // check if this account is due to purge
+ // has to have been purged at least mMinDelayBetweenPurges minutes ago
+ // we don't want to purge the folders all the time
+ PRTime nextPurgeTime =
+ curJunkFolderLastPurgeTime +
+ mMinDelayBetweenPurges * 60000000 // convert to microseconds.
+ ;
+ if (nextPurgeTime < PR_Now()) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] last purge greater than min delay", serverIndex));
+
+ nsCOMPtr<nsIMsgIncomingServer> junkFolderServer;
+ rv = junkFolder->GetServer(getter_AddRefs(junkFolderServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool serverBusy = false;
+ bool serverRequiresPassword = true;
+ bool passwordPromptRequired;
+ bool canSearchMessages = false;
+ junkFolderServer->GetPasswordPromptRequired(&passwordPromptRequired);
+ junkFolderServer->GetServerBusy(&serverBusy);
+ junkFolderServer->GetServerRequiresPasswordForBiff(
+ &serverRequiresPassword);
+ junkFolderServer->GetCanSearchMessages(&canSearchMessages);
+ // Make sure we're logged on before doing the search (assuming we need
+ // to be) and make sure the server isn't already in the middle of
+ // downloading new messages and make sure a search isn't already going
+ // on
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (search in progress? %s)", serverIndex,
+ mSearchSession ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (server busy? %s)", serverIndex,
+ serverBusy ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (serverRequiresPassword? %s)", serverIndex,
+ serverRequiresPassword ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (passwordPromptRequired? %s)", serverIndex,
+ passwordPromptRequired ? "true" : "false"));
+ if (canSearchMessages && !mSearchSession && !serverBusy &&
+ (!serverRequiresPassword || !passwordPromptRequired)) {
+ int32_t purgeInterval;
+ spamSettings->GetPurgeInterval(&purgeInterval);
+
+ if ((oldestPurgeTime == 0) ||
+ (curJunkFolderLastPurgeTime < oldestPurgeTime)) {
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] purging! searching for messages older than %d days",
+ serverIndex, purgeInterval));
+ // clang-format on
+ oldestPurgeTime = curJunkFolderLastPurgeTime;
+ purgeIntervalToUse = purgeInterval;
+ folderToPurge = junkFolder;
+ // if we've never purged this folder, do it...
+ if (curJunkFolderLastPurgeTime == 0) break;
+ }
+ } else {
+ NS_ASSERTION(canSearchMessages,
+ "unexpected, you should be able to search");
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] not a good time for this server, try again later",
+ serverIndex));
+ }
+ } else {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] last purge too recent", serverIndex));
+ }
+ }
+ }
+ if (folderToPurge && purgeIntervalToUse != 0)
+ rv = SearchFolderToPurge(folderToPurge, purgeIntervalToUse);
+ }
+
+ // set up timer to check accounts again
+ SetupNextPurge();
+ return rv;
+}
+
+nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder* folder,
+ int32_t purgeInterval) {
+ nsresult rv;
+ mSearchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSearchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
+
+ // update the time we attempted to purge this folder
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
+ &exploded);
+ folder->SetStringProperty("curJunkFolderLastPurgeTime",
+ nsDependentCString(dateBuf));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("curJunkFolderLastPurgeTime is now %s", dateBuf));
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // We need to get the folder's server scope because imap can have
+ // local junk folder.
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgSearchScopeValue searchScope;
+ server->GetSearchScope(&searchScope);
+
+ mSearchSession->AddScopeTerm(searchScope, folder);
+
+ // look for messages older than the cutoff
+ // you can't also search by junk status, see
+ // nsMsgPurgeService::OnSearchHit()
+ nsCOMPtr<nsIMsgSearchTerm> searchTerm;
+ mSearchSession->CreateTerm(getter_AddRefs(searchTerm));
+ if (searchTerm) {
+ searchTerm->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ searchTerm->SetOp(nsMsgSearchOp::IsGreaterThan);
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+ searchTerm->GetValue(getter_AddRefs(searchValue));
+ if (searchValue) {
+ searchValue->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ searchValue->SetAge((uint32_t)purgeInterval);
+ searchTerm->SetValue(searchValue);
+ }
+ searchTerm->SetBooleanAnd(false);
+ mSearchSession->AppendTerm(searchTerm);
+ }
+
+ // we are about to search
+ // create mHdrsToDelete array (if not previously created)
+ NS_ASSERTION(mHdrsToDelete.IsEmpty(), "mHdrsToDelete is not empty");
+
+ mSearchFolder = folder;
+ return mSearchSession->Search(nullptr);
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnNewSearch() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("on new search"));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+
+ nsCString messageId;
+ nsCString author;
+ nsCString subject;
+
+ aMsgHdr->GetMessageId(getter_Copies(messageId));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("messageId=%s", messageId.get()));
+ aMsgHdr->GetSubject(subject);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("subject=%s", subject.get()));
+ aMsgHdr->GetAuthor(getter_Copies(author));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("author=%s", author.get()));
+
+ // double check that the message is junk before adding to
+ // the list of messages to delete
+ //
+ // note, we can't just search for messages that are junk
+ // because not all imap server support keywords
+ // (which we use for the junk score)
+ // so the junk status would be in the message db.
+ //
+ // see bug #194090
+ nsCString junkScoreStr;
+ nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("junkScore=%s (if empty or != nsIJunkMailPlugin::IS_SPAM_SCORE, "
+ "don't add to list delete)",
+ junkScoreStr.get()));
+
+ // if "junkscore" is not set, don't delete the message
+ if (junkScoreStr.IsEmpty()) return NS_OK;
+
+ if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_SPAM_SCORE) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("added message to delete"));
+ mHdrsToDelete.AppendElement(aMsgHdr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnSearchDone(nsresult status) {
+ if (NS_SUCCEEDED(status)) {
+ uint32_t count = mHdrsToDelete.Length();
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%d messages to delete", count));
+
+ if (count > 0) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("delete messages"));
+ if (mSearchFolder)
+ mSearchFolder->DeleteMessages(
+ mHdrsToDelete, nullptr, false /*delete storage*/, false /*isMove*/,
+ nullptr, false /*allowUndo*/);
+ }
+ }
+ mHdrsToDelete.Clear();
+ if (mSearchSession) mSearchSession->UnregisterListener(this);
+ // don't cache the session
+ // just create another search session next time we search, rather than
+ // clearing scopes, terms etc. we also use mSearchSession to determine if the
+ // purge service is "busy"
+ mSearchSession = nullptr;
+ mSearchFolder = nullptr;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgPurgeService.h b/comm/mailnews/base/src/nsMsgPurgeService.h
new file mode 100644
index 0000000000..30643624d7
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgPurgeService.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NSMSGPURGESERVICE_H
+#define NSMSGPURGESERVICE_H
+
+#include "msgCore.h"
+#include "nsIMsgPurgeService.h"
+#include "nsIMsgSearchSession.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+
+class nsMsgPurgeService : public nsIMsgPurgeService, public nsIMsgSearchNotify {
+ public:
+ nsMsgPurgeService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPURGESERVICE
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ nsresult PerformPurge();
+
+ protected:
+ virtual ~nsMsgPurgeService();
+ int32_t FindServer(nsIMsgIncomingServer* server);
+ nsresult SetupNextPurge();
+ nsresult PurgeSurver(nsIMsgIncomingServer* server);
+ nsresult SearchFolderToPurge(nsIMsgFolder* folder, int32_t purgeInterval);
+
+ protected:
+ nsCOMPtr<nsITimer> mPurgeTimer;
+ nsCOMPtr<nsIMsgSearchSession> mSearchSession;
+ nsCOMPtr<nsIMsgFolder> mSearchFolder;
+ nsTArray<RefPtr<nsIMsgDBHdr>> mHdrsToDelete;
+ bool mHaveShutdown;
+
+ private:
+ // in minutes, how long must pass between two consecutive purges on the
+ // same junk folder?
+ int32_t mMinDelayBetweenPurges;
+ // in minutes, how often to check if we need to purge one of the junk folders?
+ int32_t mPurgeTimerInterval;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp
new file mode 100644
index 0000000000..97d412acc4
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp
@@ -0,0 +1,806 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgQuickSearchDBView.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsImapCore.h"
+#include "nsIMsgHdr.h"
+#include "nsIDBFolderInfo.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+
+nsMsgQuickSearchDBView::nsMsgQuickSearchDBView() {
+ m_usingCachedHits = false;
+ m_cacheEmpty = true;
+}
+
+nsMsgQuickSearchDBView::~nsMsgQuickSearchDBView() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgQuickSearchDBView, nsMsgDBView, nsIMsgDBView,
+ nsIMsgSearchNotify)
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::Open(nsIMsgFolder* folder,
+ nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags,
+ int32_t* pCount) {
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+ m_viewFolder = nullptr;
+
+ int32_t count;
+ rv = InitThreadedView(count);
+ if (pCount) *pCount = count;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgQuickSearchDBView* newMsgDBView = new nsMsgQuickSearchDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgThreadedDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgQuickSearchDBView* newMsgDBView = (nsMsgQuickSearchDBView*)aNewMsgDBView;
+
+ // now copy all of our private member data
+ newMsgDBView->m_origKeys = m_origKeys.Clone();
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::DeleteMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ RememberDeletedMsgHdr(msgHdr);
+ }
+ }
+ return nsMsgDBView::DeleteMessages(window, selection, deleteStorage);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::DoCommand(
+ nsMsgViewCommandTypeValue aCommand) {
+ if (aCommand == nsMsgViewCommandType::markAllRead) {
+ nsresult rv = NS_OK;
+ m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ false);
+
+ for (uint32_t i = 0; NS_SUCCEEDED(rv) && i < GetSize(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ m_db->GetMsgHdrForKey(m_keys[i], getter_AddRefs(msgHdr));
+ rv = m_db->MarkHdrRead(msgHdr, true, nullptr);
+ }
+
+ m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ true);
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder);
+ if (NS_SUCCEEDED(rv) && imapFolder)
+ rv = imapFolder->StoreImapFlags(kImapMsgSeenFlag, true, m_keys, nullptr);
+
+ m_db->SetSummaryValid(true);
+ return rv;
+ } else
+ return nsMsgDBView::DoCommand(aCommand);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowQuickSearchResults;
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ // protect against duplication.
+ if (m_origKeys.BinaryIndexOf(msgKey) == m_origKeys.NoIndex) {
+ nsMsgViewIndex insertIndex = GetInsertIndexHelper(
+ msgHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ m_origKeys.InsertElementAt(insertIndex, msgKey);
+ }
+ if (m_viewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay)) {
+ nsMsgKey parentKey;
+ msgHdr->GetThreadParent(&parentKey);
+ return nsMsgThreadedDBView::OnNewHeader(msgHdr, parentKey, true);
+ } else
+ return nsMsgDBView::AddHdr(msgHdr, resultIndex);
+}
+
+nsresult nsMsgQuickSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (newHdr) {
+ bool match = false;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match);
+ if (match) {
+ // put the new header in m_origKeys, so that expanding a thread will
+ // show the newly added header.
+ nsMsgKey newKey;
+ (void)newHdr->GetMessageKey(&newKey);
+ nsMsgViewIndex insertIndex = GetInsertIndexHelper(
+ newHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ m_origKeys.InsertElementAt(insertIndex, newKey);
+ nsMsgThreadedDBView::OnNewHeader(
+ newHdr, aParentKey,
+ ensureListed); // do not add a new message if there isn't a match.
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsresult rv = nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags,
+ aNewFlags, aInstigator);
+
+ if (m_viewFolder && (m_viewFolder != m_folder) &&
+ (aOldFlags & nsMsgMessageFlags::Read) !=
+ (aNewFlags & nsMsgMessageFlags::Read)) {
+ // if we're displaying a single folder virtual folder for an imap folder,
+ // the search criteria might be on message body, and we might not have the
+ // message body offline, in which case we can't tell if the message
+ // matched or not. But if the unread flag changed, we need to update the
+ // unread counts. Normally, VirtualFolderChangeListener::OnHdrFlagsChanged
+ // will handle this, but it won't work for body criteria when we don't have
+ // the body offline.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_viewFolder);
+ if (imapFolder) {
+ nsMsgViewIndex hdrIndex = FindHdr(aHdrChanged);
+ if (hdrIndex != nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ if (searchSession) {
+ bool oldMatch, newMatch;
+ rv = searchSession->MatchHdr(aHdrChanged, m_db, &newMatch);
+ aHdrChanged->SetFlags(aOldFlags);
+ rv = searchSession->MatchHdr(aHdrChanged, m_db, &oldMatch);
+ aHdrChanged->SetFlags(aNewFlags);
+ // if it doesn't match the criteria,
+ // VirtualFolderChangeListener::OnHdrFlagsChanged won't tweak the
+ // read/unread counts. So do it here:
+ if (!oldMatch && !newMatch) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbFolderInfo->ChangeNumUnreadMessages(
+ (aOldFlags & nsMsgMessageFlags::Read) ? 1 : -1);
+ m_viewFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrChanged,
+ const nsACString& property,
+ bool aPreChange, uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ // If the junk mail plugin just activated on a message, then
+ // we'll allow filters to remove from view.
+ // Otherwise, just update the view line.
+ //
+ // Note this will not add newly matched headers to the view. This is
+ // probably a bug that needs fixing.
+
+ NS_ENSURE_ARG_POINTER(aStatus);
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ if (index == nsMsgViewIndex_None) // message does not appear in view
+ return NS_OK;
+
+ nsCString originStr;
+ (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr);
+ // check for "plugin" with only first character for performance
+ bool plugin = (originStr.get()[0] == 'p');
+
+ if (aPreChange) {
+ // first call, done prior to the change
+ *aStatus = plugin;
+ return NS_OK;
+ }
+
+ // second call, done after the change
+ bool wasPlugin = *aStatus;
+
+ bool match = true;
+ nsCOMPtr<nsIMsgSearchSession> searchSession(
+ do_QueryReferent(m_searchSession));
+ if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match);
+
+ if (!match && plugin && !wasPlugin)
+ RemoveByIndex(index); // remove hdr from view
+ else
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::GetSearchSession(nsIMsgSearchSession** aSession) {
+ NS_ASSERTION(false, "GetSearchSession method is not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ m_searchSession = do_GetWeakReference(aSession);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+ // remember search hit and when search is done, reconcile cache
+ // with new hits;
+ m_hdrHits.AppendObject(aMsgHdr);
+ nsMsgKey key;
+ aMsgHdr->GetMessageKey(&key);
+ // Is FindKey going to be expensive here? A lot of hits could make
+ // it a little bit slow to search through the view for every hit.
+ if (m_cacheEmpty || FindKey(key, false) == nsMsgViewIndex_None)
+ return AddHdr(aMsgHdr);
+ else
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnSearchDone(nsresult status) {
+ // This batch began in OnNewSearch.
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ // We're a single-folder virtual folder if viewFolder != folder, and that is
+ // the only case in which we want to be messing about with a results cache
+ // or unread counts.
+ if (m_db && m_viewFolder && m_viewFolder != m_folder) {
+ nsTArray<nsMsgKey> keyArray;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ uint32_t count = m_hdrHits.Count();
+ // Build up message keys. The cache expects them to be sorted.
+ for (uint32_t i = 0; i < count; i++) {
+ nsMsgKey key;
+ m_hdrHits[i]->GetMessageKey(&key);
+ keyArray.AppendElement(key);
+ }
+ keyArray.Sort();
+ nsTArray<nsMsgKey> staleHits;
+ nsresult rv = m_db->RefreshCache(searchUri, keyArray, staleHits);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsMsgKey staleKey : staleHits) {
+ nsCOMPtr<nsIMsgDBHdr> hdrDeleted;
+ m_db->GetMsgHdrForKey(staleKey, getter_AddRefs(hdrDeleted));
+ if (hdrDeleted) OnHdrDeleted(hdrDeleted, nsMsgKey_None, 0, this);
+ }
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numUnread = 0;
+ size_t numTotal = m_origKeys.Length();
+
+ for (size_t i = 0; i < m_origKeys.Length(); i++) {
+ bool isRead;
+ m_db->IsRead(m_origKeys[i], &isRead);
+ if (!isRead) numUnread++;
+ }
+ dbFolderInfo->SetNumUnreadMessages(numUnread);
+ dbFolderInfo->SetNumMessages(numTotal);
+ m_viewFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ if (m_sortType !=
+ nsMsgViewSortType::byThread) // we do not find levels for the results.
+ {
+ m_sortValid = false; // sort the results
+ Sort(m_sortType, m_sortOrder);
+ }
+ if (m_viewFolder && (m_viewFolder != m_folder))
+ SetMRUTimeForFolder(m_viewFolder);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ m_hdrHits.Clear();
+ // this needs to happen after we remove all the keys, since RowCountChanged()
+ // will call our GetRowCount()
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ uint32_t folderFlags = 0;
+ if (m_viewFolder) m_viewFolder->GetFlags(&folderFlags);
+ // check if it's a virtual folder - if so, we should get the cached hits
+ // from the db, and set a flag saying that we're using cached values.
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ nsCOMPtr<nsIMsgEnumerator> cachedHits;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ m_db->GetCachedHits(searchUri, getter_AddRefs(cachedHits));
+ if (cachedHits) {
+ bool hasMore;
+
+ m_usingCachedHits = true;
+ cachedHits->HasMoreElements(&hasMore);
+ m_cacheEmpty = !hasMore;
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ while (hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ nsresult rv = cachedHits->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv))
+ AddHdr(header);
+ else
+ break;
+ cachedHits->HasMoreElements(&hasMore);
+ }
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+ }
+
+ // Prevent updates for every message found. This batch ends in OnSearchDone.
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) {
+ uint32_t numChildren;
+ nsresult rv = NS_OK;
+ uint8_t minLevel = 0xff;
+ threadHdr->GetNumChildren(&numChildren);
+ nsMsgKey threadRootKey;
+ nsCOMPtr<nsIMsgDBHdr> rootParent;
+ threadHdr->GetRootHdr(getter_AddRefs(rootParent));
+ if (rootParent)
+ rootParent->GetMessageKey(&threadRootKey);
+ else
+ threadHdr->GetThreadKey(&threadRootKey);
+
+ nsCOMPtr<nsIMsgDBHdr> retHdr;
+
+ // iterate over thread, finding mgsHdr in view with the lowest level.
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ // this works because we've already sorted m_keys by id.
+ nsMsgViewIndex keyIndex = m_origKeys.BinaryIndexOf(msgKey);
+ if (keyIndex != nsMsgViewIndex_None) {
+ // this is the root, so it's the best we're going to do.
+ if (msgKey == threadRootKey) {
+ retHdr = child;
+ break;
+ }
+ uint8_t level = 0;
+ nsMsgKey parentId;
+ child->GetThreadParent(&parentId);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ // count number of ancestors - that's our level
+ while (parentId != nsMsgKey_None) {
+ m_db->GetMsgHdrForKey(parentId, getter_AddRefs(parent));
+ if (parent) {
+ nsMsgKey saveParentId = parentId;
+ parent->GetThreadParent(&parentId);
+ // message is it's own parent - bad, let's break out of here.
+ // Or we've got some circular ancestry.
+ if (parentId == saveParentId || level > numChildren) break;
+ level++;
+ } else // if we can't find the parent, don't loop forever.
+ break;
+ }
+ if (level < minLevel) {
+ minLevel = level;
+ retHdr = child;
+ }
+ }
+ }
+ }
+ retHdr.forget(result);
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::SortThreads(
+ nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) {
+ // don't need to sort by threads for group view.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK;
+ // iterate over the messages in the view, getting the thread id's
+ // sort m_keys so we can quickly find if a key is in the view.
+ m_keys.Sort();
+ // array of the threads' root hdr keys.
+ nsTArray<nsMsgKey> threadRootIds;
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ if (threadHdr) {
+ nsMsgKey rootKey;
+ threadHdr->GetChildKeyAt(0, &rootKey);
+ nsMsgViewIndex threadRootIndex = threadRootIds.BinaryIndexOf(rootKey);
+ // if we already have that id in top level threads, ignore this msg.
+ if (threadRootIndex != nsMsgViewIndex_None) continue;
+ // it would be nice if GetInsertIndexHelper always found the hdr, but it
+ // doesn't.
+ threadHdr->GetChildHdrAt(0, getter_AddRefs(rootHdr));
+ if (!rootHdr) continue;
+ threadRootIndex = GetInsertIndexHelper(rootHdr, threadRootIds, nullptr,
+ nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ threadRootIds.InsertElementAt(threadRootIndex, rootKey);
+ }
+ }
+
+ m_sortType = nsMsgViewSortType::byNone; // sort from scratch
+ // need to sort the top level threads now by sort order, if it's not by id
+ // and ascending (which is the order per above).
+ if (!(sortType == nsMsgViewSortType::byId &&
+ sortOrder == nsMsgViewSortOrder::ascending)) {
+ m_keys.SwapElements(threadRootIds);
+ nsMsgDBView::Sort(sortType, sortOrder);
+ threadRootIds.SwapElements(m_keys);
+ }
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ // now we've build up the list of thread ids - need to build the view
+ // from that. So for each thread id, we need to list the messages in the
+ // thread.
+ uint32_t numThreads = threadRootIds.Length();
+ for (uint32_t threadIndex = 0; threadIndex < numThreads; threadIndex++) {
+ m_db->GetMsgHdrForKey(threadRootIds[threadIndex], getter_AddRefs(rootHdr));
+ if (rootHdr) {
+ nsCOMPtr<nsIMsgDBHdr> displayRootHdr;
+ m_db->GetThreadContainingMsgHdr(rootHdr, getter_AddRefs(threadHdr));
+ if (threadHdr) {
+ nsMsgKey rootKey;
+ uint32_t rootFlags;
+ GetFirstMessageHdrToDisplayInThread(threadHdr,
+ getter_AddRefs(displayRootHdr));
+ if (!displayRootHdr) continue;
+ displayRootHdr->GetMessageKey(&rootKey);
+ displayRootHdr->GetFlags(&rootFlags);
+ rootFlags |= MSG_VIEW_FLAG_ISTHREAD;
+ m_keys.AppendElement(rootKey);
+ m_flags.AppendElement(rootFlags);
+ m_levels.AppendElement(0);
+
+ nsMsgViewIndex startOfThreadViewIndex = m_keys.Length();
+ nsMsgViewIndex rootIndex = startOfThreadViewIndex - 1;
+ uint32_t numListed = 0;
+ ListIdsInThreadOrder(threadHdr, rootKey, 1, &startOfThreadViewIndex,
+ &numListed);
+ if (numListed > 0)
+ m_flags[rootIndex] = rootFlags | MSG_VIEW_FLAG_HASCHILDREN;
+ }
+ }
+ }
+
+ // The thread state is left expanded (despite viewFlags) so at least reflect
+ // the correct state.
+ m_viewFlags |= nsMsgViewFlagsType::kExpandAll;
+
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(viewIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (uint32_t i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ // if this hdr is in the original view, add it to new view.
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex)
+ messageArray.AppendElement(msgHdr);
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) {
+ nsMsgKey parentKey = m_keys[startOfThreadViewIndex++];
+ return ListIdsInThreadOrder(threadHdr, parentKey, 1,
+ &startOfThreadViewIndex, pNumListed);
+ }
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ uint32_t i;
+ uint32_t viewIndex = startOfThreadViewIndex + 1;
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ uint32_t rootFlags = m_flags[startOfThreadViewIndex];
+ *pNumListed = 0;
+ GetMsgHdrForViewIndex(startOfThreadViewIndex, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ nsMsgViewIndex threadRootIndex = m_origKeys.BinaryIndexOf(msgKey);
+ // if this hdr is in the original view, add it to new view.
+ if (threadRootIndex != nsMsgViewIndex_None) {
+ uint32_t childFlags;
+ msgHdr->GetFlags(&childFlags);
+ InsertMsgHdrAt(
+ viewIndex, msgHdr, msgKey, childFlags,
+ FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex));
+ if (!(rootFlags & MSG_VIEW_FLAG_HASCHILDREN))
+ m_flags[startOfThreadViewIndex] =
+ rootFlags | MSG_VIEW_FLAG_HASCHILDREN;
+
+ viewIndex++;
+ (*pNumListed)++;
+ }
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder(
+ nsIMsgThread* threadHdr, nsMsgKey parentKey, uint32_t level,
+ uint32_t callLevel, nsMsgKey keyToSkip, nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsCOMPtr<nsIMsgEnumerator> msgEnumerator;
+ nsresult rv =
+ threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use the numChildren as a sanity check on the thread structure.
+ uint32_t numChildren;
+ (void)threadHdr->GetNumChildren(&numChildren);
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = msgEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey == keyToSkip) continue;
+
+ // If we discover depths of more than numChildren, it means we have
+ // some sort of circular thread relationship and we bail out of the
+ // while loop before overflowing the stack with recursive calls.
+ // Technically, this is an error, but forcing a database rebuild
+ // is too destructive so we just return.
+ if (*pNumListed > numChildren || callLevel > numChildren) {
+ NS_ERROR("loop in message threading while listing children");
+ return NS_OK;
+ }
+
+ int32_t childLevel = level;
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex) {
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(*viewIndex, msgHdr, msgKey, msgFlags & ~MSG_VIEW_FLAGS,
+ level);
+ (*pNumListed)++;
+ (*viewIndex)++;
+ childLevel++;
+ }
+ rv = ListIdsInThreadOrder(threadHdr, msgKey, childLevel, callLevel + 1,
+ keyToSkip, viewIndex, pNumListed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey,
+ uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsresult rv = ListIdsInThreadOrder(threadHdr, parentKey, level, level,
+ nsMsgKey_None, viewIndex, pNumListed);
+ // Because a quick search view might not have the actual thread root
+ // as its root, and thus might have a message that potentially has siblings
+ // as its root, and the enumerator will miss the siblings, we might need to
+ // make a pass looking for the siblings of the non-root root. We'll put
+ // those after the potential children of the root. So we will list the
+ // children of the faux root's parent, ignoring the faux root.
+ if (level == 1) {
+ nsCOMPtr<nsIMsgDBHdr> root;
+ nsCOMPtr<nsIMsgDBHdr> rootParent;
+ nsMsgKey rootKey;
+ threadHdr->GetRootHdr(getter_AddRefs(rootParent));
+ if (rootParent) {
+ rootParent->GetMessageKey(&rootKey);
+ if (rootKey != parentKey)
+ rv = ListIdsInThreadOrder(threadHdr, rootKey, level, level, parentKey,
+ viewIndex, pNumListed);
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) {
+ *expansionDelta = 0;
+ if (index >= ((nsMsgViewIndex)m_keys.Length()))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ char flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK;
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ GetMsgHdrForViewIndex(index, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (uint32_t i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ // if this hdr is in the original view, add it to new view.
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex)
+ (*expansionDelta)++;
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ if (!(flags & nsMsgMessageFlags::Elided))
+ *expansionDelta = -(*expansionDelta);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ if (aViewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder,
+ aViewFlags, aCount);
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags;
+
+ bool hasMore;
+ nsCOMPtr<nsISupports> supports;
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ AddHdr(msgHdr);
+ } else {
+ break;
+ }
+ }
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::SetViewFlags(
+ nsMsgViewFlagsTypeValue aViewFlags) {
+ nsresult rv = NS_OK;
+ // if the grouping has changed, rebuild the view
+ if ((m_viewFlags & nsMsgViewFlagsType::kGroupBySort) ^
+ (aViewFlags & nsMsgViewFlagsType::kGroupBySort))
+ rv = RebuildView(aViewFlags);
+ nsMsgDBView::SetViewFlags(aViewFlags);
+
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ return GetViewEnumerator(enumerator);
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ NS_ENSURE_ARG_POINTER(aHdrDeleted);
+ nsMsgKey msgKey;
+ aHdrDeleted->GetMessageKey(&msgKey);
+ size_t keyIndex = m_origKeys.BinaryIndexOf(msgKey);
+ if (keyIndex != m_origKeys.NoIndex) m_origKeys.RemoveElementAt(keyIndex);
+ return nsMsgThreadedDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ *aNumMsgs = m_origKeys.Length();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.h b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h
new file mode 100644
index 0000000000..da10a3d9e1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsMsgQuickSearchDBView_H_
+#define _nsMsgQuickSearchDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgThreadedDBView.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgSearchSession.h"
+#include "nsCOMArray.h"
+#include "nsIMsgHdr.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgQuickSearchDBView : public nsMsgThreadedDBView,
+ public nsIMsgSearchNotify {
+ public:
+ nsMsgQuickSearchDBView();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual const char* GetViewName(void) override { return "QuickSearchView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue aCommand) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override;
+ NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override;
+ NS_IMETHOD GetSearchSession(nsIMsgSearchSession** aSearchSession) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+
+ protected:
+ virtual ~nsMsgQuickSearchDBView();
+ nsWeakPtr m_searchSession;
+ nsTArray<nsMsgKey> m_origKeys;
+ bool m_usingCachedHits;
+ bool m_cacheEmpty;
+ nsCOMArray<nsIMsgDBHdr> m_hdrHits;
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex = nullptr) override;
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) override;
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) override;
+ virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ virtual nsresult GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) override;
+ virtual nsresult ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) override;
+ virtual nsresult ListCollapsedChildren(
+ nsMsgViewIndex viewIndex,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) override;
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) override;
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) override;
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ uint32_t callLevel, nsMsgKey keyToSkip,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed);
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+ void SavePreSearchInfo();
+ void ClearPreSearchInfo();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.cpp b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp
new file mode 100644
index 0000000000..80cd0d0bd1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgReadStateTxn.h"
+
+#include "nsIMsgHdr.h"
+#include "nsComponentManagerUtils.h"
+
+nsMsgReadStateTxn::nsMsgReadStateTxn() {}
+
+nsMsgReadStateTxn::~nsMsgReadStateTxn() {}
+
+nsresult nsMsgReadStateTxn::Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys,
+ nsMsgKey* aMsgKeyArray) {
+ NS_ENSURE_ARG_POINTER(aParentFolder);
+
+ mParentFolder = aParentFolder;
+ mMarkedMessages.AppendElements(aMsgKeyArray, aNumKeys);
+
+ return nsMsgTxn::Init();
+}
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::UndoTransaction() { return MarkMessages(false); }
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::RedoTransaction() { return MarkMessages(true); }
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::MarkMessages(bool aAsRead) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages(mMarkedMessages.Length());
+ for (auto msgKey : mMarkedMessages) {
+ nsCOMPtr<nsIMsgDBHdr> curMsgHdr;
+ nsresult rv =
+ mParentFolder->GetMessageHeader(msgKey, getter_AddRefs(curMsgHdr));
+ if (NS_SUCCEEDED(rv) && curMsgHdr) {
+ messages.AppendElement(curMsgHdr);
+ }
+ }
+ return mParentFolder->MarkMessagesRead(messages, aAsRead);
+}
diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.h b/comm/mailnews/base/src/nsMsgReadStateTxn.h
new file mode 100644
index 0000000000..07bd4c8670
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgReadStateTxn.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgBaseUndoTxn_h_
+#define nsMsgBaseUndoTxn_h_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgTxn.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes.h"
+#include "nsIMsgFolder.h"
+
+#define NS_MSGREADSTATETXN_IID \
+ { /* 121FCE4A-3EA1-455C-8161-839E1557D0CF */ \
+ 0x121FCE4A, 0x3EA1, 0x455C, { \
+ 0x81, 0x61, 0x83, 0x9E, 0x15, 0x57, 0xD0, 0xCF \
+ } \
+ }
+
+//------------------------------------------------------------------------------
+// A mark-all transaction handler. Helper for redo/undo of message read states.
+//------------------------------------------------------------------------------
+class nsMsgReadStateTxn : public nsMsgTxn {
+ public:
+ nsMsgReadStateTxn();
+ virtual ~nsMsgReadStateTxn();
+
+ nsresult Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys,
+ nsMsgKey* aMsgKeyArray);
+ NS_IMETHOD UndoTransaction() override;
+ NS_IMETHOD RedoTransaction() override;
+
+ protected:
+ NS_IMETHOD MarkMessages(bool aAsRead);
+
+ private:
+ nsCOMPtr<nsIMsgFolder> mParentFolder;
+ nsTArray<nsMsgKey> mMarkedMessages;
+};
+
+#endif // nsMsgBaseUndoTxn_h_
diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.cpp b/comm/mailnews/base/src/nsMsgSearchDBView.cpp
new file mode 100644
index 0000000000..f036d0d304
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSearchDBView.cpp
@@ -0,0 +1,1344 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgSearchDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsQuickSort.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgCopyService.h"
+#include "nsMsgUtils.h"
+#include "nsTreeColumns.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgGroupThread.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgSearchSession.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+static bool gReferenceOnlyThreading;
+
+nsMsgSearchDBView::nsMsgSearchDBView() {
+ // Don't try to display messages for the search pane.
+ mSuppressMsgDisplay = true;
+ m_totalMessagesInView = 0;
+ m_nextThreadId = 1;
+ mCurIndex = 0;
+ mTotalIndices = 0;
+ mCommand = -1;
+}
+
+nsMsgSearchDBView::~nsMsgSearchDBView() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgSearchDBView, nsMsgDBView, nsIMsgDBView,
+ nsIMsgCopyServiceListener, nsIMsgSearchNotify)
+
+NS_IMETHODIMP
+nsMsgSearchDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ // DBViewWrapper.jsm likes to create search views with a sort order
+ // of byNone, in order to have the order be the order the search results
+ // are returned. But this doesn't work with threaded view, so make the
+ // sort order be byDate if we're threaded.
+
+ if (viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ sortType == nsMsgViewSortType::byNone)
+ sortType = nsMsgViewSortType::byDate;
+
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mail.strict_threading", &gReferenceOnlyThreading);
+
+ // Our sort is automatically valid because we have no contents at this point!
+ m_sortValid = true;
+
+ if (pCount) *pCount = 0;
+
+ m_folder = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgSearchDBView* newMsgDBView = new nsMsgSearchDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgGroupView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgSearchDBView* newMsgDBView = (nsMsgSearchDBView*)aNewMsgDBView;
+
+ // Now copy all of our private member data.
+ newMsgDBView->mDestFolder = mDestFolder;
+ newMsgDBView->mCommand = mCommand;
+ newMsgDBView->mTotalIndices = mTotalIndices;
+ newMsgDBView->mCurIndex = mCurIndex;
+ newMsgDBView->m_folders.InsertObjectsAt(m_folders, 0);
+ newMsgDBView->m_curCustomColumn = m_curCustomColumn;
+ for (auto const& hdrs : m_hdrsForEachFolder) {
+ newMsgDBView->m_hdrsForEachFolder.AppendElement(hdrs.Clone());
+ }
+ newMsgDBView->m_uniqueFoldersSelected.InsertObjectsAt(m_uniqueFoldersSelected,
+ 0);
+
+ int32_t count = m_dbToUseList.Count();
+ for (int32_t i = 0; i < count; i++) {
+ newMsgDBView->m_dbToUseList.AppendObject(m_dbToUseList[i]);
+ // Register the new view with the database so it gets notifications.
+ m_dbToUseList[i]->AddListener(newMsgDBView);
+ }
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ // We need to clone the thread and msg hdr hash tables.
+ for (auto iter = m_threadsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_threadsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ for (auto iter = m_hdrsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_hdrsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::Close() {
+ int32_t count = m_dbToUseList.Count();
+
+ for (int32_t i = 0; i < count; i++) m_dbToUseList[i]->RemoveListener(this);
+
+ m_dbToUseList.Clear();
+
+ return nsMsgGroupView::Close();
+}
+
+void nsMsgSearchDBView::InternalClose() {
+ m_threadsTable.Clear();
+ m_hdrsTable.Clear();
+ nsMsgGroupView::InternalClose();
+ m_folders.Clear();
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aValue) {
+ NS_ENSURE_TRUE(IsValidIndex(aRow), NS_MSG_INVALID_DBVIEW_INDEX);
+ NS_ENSURE_ARG_POINTER(aCol);
+
+ const nsAString& colID = aCol->GetId();
+ // The only thing we contribute is location; dummy rows have no location, so
+ // bail in that case. Otherwise, check if we are dealing with 'location'.
+ // 'location', need to check for "lo" not just "l" to avoid "label" column.
+ if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) && colID.Length() >= 2 &&
+ colID.First() == 'l' && colID.CharAt(1) == 'o')
+ return FetchLocation(aRow, aValue);
+ else
+ return nsMsgGroupView::GetCellText(aRow, aCol, aValue);
+}
+
+nsresult nsMsgSearchDBView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) {
+ if (m_sortType == nsMsgViewSortType::byLocation) {
+ aHashKey.Truncate();
+ nsCOMPtr<nsIMsgFolder> folder;
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ return folder->GetPrettyName(aHashKey);
+ }
+
+ return nsMsgGroupView::HashHdr(msgHdr, aHashKey);
+}
+
+nsresult nsMsgSearchDBView::FetchLocation(int32_t aRow,
+ nsAString& aLocationString) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folder->GetPrettyName(aLocationString);
+}
+
+nsresult nsMsgSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex deletedIndex = FindHdr(aHdrDeleted);
+ uint32_t savedFlags = 0;
+ if (deletedIndex != nsMsgViewIndex_None) {
+ // Check if this message is currently selected. If it is, tell the front
+ // end to be prepared for a delete.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ bool isMsgSelected = false;
+ if (mTreeSelection && commandUpdater) {
+ mTreeSelection->IsSelected(deletedIndex, &isMsgSelected);
+ if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete();
+ }
+
+ savedFlags = m_flags[deletedIndex];
+ RemoveByIndex(deletedIndex);
+
+ if (isMsgSelected) {
+ // Now tell the front end that the delete happened.
+ commandUpdater->SelectedMessageRemoved();
+ }
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ GetXFThreadFromMsgHdr(aHdrDeleted, getter_AddRefs(thread));
+ if (thread) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ viewThread->RemoveChildHdr(aHdrDeleted, nullptr);
+ if (deletedIndex == nsMsgViewIndex_None && viewThread->MsgCount() == 1) {
+ // Remove the last child of a collapsed thread. Need to find the root,
+ // and remove the thread flags on it.
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ thread->GetRootHdr(getter_AddRefs(rootHdr));
+ if (rootHdr) {
+ nsMsgViewIndex threadIndex = GetThreadRootIndex(rootHdr);
+ if (IsValidIndex(threadIndex))
+ AndExtraFlag(threadIndex,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+ }
+ } else if (savedFlags & MSG_VIEW_FLAG_HASCHILDREN) {
+ if (savedFlags & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsresult rv = thread->GetRootHdr(getter_AddRefs(rootHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ rootHdr->GetMessageKey(&msgKey);
+ rootHdr->GetFlags(&msgFlags);
+ // Promote the new thread root.
+ if (viewThread->MsgCount() > 1)
+ msgFlags |= MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN;
+ InsertMsgHdrAt(deletedIndex, rootHdr, msgKey, msgFlags, 0);
+ if (!m_deletingRows)
+ NoteChange(deletedIndex, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+ } else if (viewThread->MsgCount() > 1) {
+ OrExtraFlag(deletedIndex,
+ MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN);
+ }
+ }
+ }
+ } else {
+ return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged,
+ uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ // Defer to base class if we're grouped or not threaded at all.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ return nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ bool foundMessageId;
+ // Check if the hdr that changed is in a xf thread, and if the read flag
+ // changed, update the thread unread count. GetXFThreadFromMsgHdr returns
+ // the thread the header does or would belong to, so we need to also
+ // check that the header is actually in the thread.
+ GetXFThreadFromMsgHdr(aHdrChanged, getter_AddRefs(thread), &foundMessageId);
+ if (foundMessageId) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ if (viewThread->HdrIndex(aHdrChanged) != -1) {
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & nsMsgMessageFlags::Read)
+ thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read);
+ }
+ }
+
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+}
+
+void nsMsgSearchDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ if ((int32_t)index < 0) {
+ NS_ERROR("invalid insert index");
+ index = 0;
+ level = 0;
+ } else if (index > m_keys.Length()) {
+ NS_ERROR("inserting past end of array");
+ index = m_keys.Length();
+ }
+
+ m_keys.InsertElementAt(index, msgKey);
+ m_flags.InsertElementAt(index, flags);
+ m_levels.InsertElementAt(index, level);
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, index);
+}
+
+void nsMsgSearchDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ m_keys[index] = msgKey;
+ m_flags[index] = flags;
+ m_levels[index] = level;
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.ReplaceObjectAt(folder, index);
+}
+
+void nsMsgSearchDBView::InsertEmptyRows(nsMsgViewIndex viewIndex,
+ int32_t numRows) {
+ for (int32_t i = 0; i < numRows; i++) {
+ m_folders.InsertObjectAt(nullptr, viewIndex + i);
+ }
+
+ return nsMsgDBView::InsertEmptyRows(viewIndex, numRows);
+}
+
+void nsMsgSearchDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ nsMsgDBView::RemoveRows(viewIndex, numRows);
+ for (int32_t i = 0; i < numRows; i++) m_folders.RemoveObjectAt(viewIndex);
+}
+
+nsresult nsMsgSearchDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) {
+ if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count()) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ nsIMsgFolder* folder = m_folders[index];
+ if (folder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (db) {
+ return db->GetMsgHdrForKey(m_keys[index], msgHdr);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NS_IF_ADDREF(*aFolder = m_folders[index]);
+ return *aFolder ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+nsresult nsMsgSearchDBView::GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) {
+ nsCOMPtr<nsIMsgFolder> aFolder;
+ nsresult rv = GetFolderForViewIndex(index, getter_AddRefs(aFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return aFolder->GetMsgDatabase(db);
+}
+
+nsresult nsMsgSearchDBView::AddHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnNewHeader(msgHdr, nsMsgKey_None, true);
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsCOMPtr<nsIMsgThread> thread;
+ nsCOMPtr<nsIMsgDBHdr> threadRoot;
+ // If we find an xf thread in the hash table corresponding to the new msg's
+ // message id, a previous header must be a reference child of the new
+ // message, which means we need to reparent later.
+ bool msgIsReferredTo;
+ GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread), &msgIsReferredTo);
+ bool newThread = !thread;
+ nsMsgXFViewThread* viewThread;
+ if (!thread) {
+ viewThread = new nsMsgXFViewThread(this, m_nextThreadId++);
+ if (!viewThread) return NS_ERROR_OUT_OF_MEMORY;
+
+ thread = viewThread;
+ } else {
+ viewThread = static_cast<nsMsgXFViewThread*>(thread.get());
+ thread->GetChildHdrAt(0, getter_AddRefs(threadRoot));
+ }
+
+ AddMsgToHashTables(msgHdr, thread);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ uint32_t posInThread;
+ // We need to move threads in order to keep ourselves sorted
+ // correctly. We want the index of the original thread...we can do this by
+ // getting the root header before we add the new header, and finding that.
+ if (newThread || !viewThread->MsgCount()) {
+ viewThread->AddHdr(msgHdr, false, posInThread, getter_AddRefs(parent));
+ nsMsgViewIndex insertIndex = GetIndexForThread(msgHdr);
+ NS_ASSERTION(insertIndex == m_levels.Length() ||
+ (IsValidIndex(insertIndex) && !m_levels[insertIndex]),
+ "inserting into middle of thread");
+ if (insertIndex == nsMsgViewIndex_None)
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kExpandAll))
+ msgFlags |= nsMsgMessageFlags::Elided;
+
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0);
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ } else {
+ // Get the thread root index before we add the header, because adding
+ // the header can change the sort position.
+ nsMsgViewIndex threadIndex = GetThreadRootIndex(threadRoot);
+ viewThread->AddHdr(msgHdr, msgIsReferredTo, posInThread,
+ getter_AddRefs(parent));
+ if (!IsValidIndex(threadIndex)) {
+ NS_ERROR("couldn't find thread index for newly inserted header");
+ // Not really OK, but not failure exactly.
+ return NS_OK;
+ }
+
+ NS_ASSERTION(!m_levels[threadIndex],
+ "threadRoot incorrect, or level incorrect");
+
+ bool moveThread = false;
+ if (m_sortType == nsMsgViewSortType::byDate) {
+ uint32_t newestMsgInThread = 0, msgDate = 0;
+ viewThread->GetNewestMsgDate(&newestMsgInThread);
+ msgHdr->GetDateInSeconds(&msgDate);
+ moveThread = (msgDate == newestMsgInThread);
+ }
+
+ OrExtraFlag(threadIndex,
+ MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD);
+ if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) {
+ if (parent) {
+ // Since we know posInThread, we just want to insert the new hdr
+ // at threadIndex + posInThread, and then rebuild the view until we
+ // get to a sibling of the new hdr.
+ uint8_t newMsgLevel = viewThread->ChildLevelAt(posInThread);
+ InsertMsgHdrAt(threadIndex + posInThread, msgHdr, msgKey, msgFlags,
+ newMsgLevel);
+
+ NoteChange(threadIndex + posInThread, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+ for (nsMsgViewIndex viewIndex = threadIndex + ++posInThread;
+ posInThread < viewThread->MsgCount() &&
+ viewThread->ChildLevelAt(posInThread) > newMsgLevel;
+ viewIndex++) {
+ m_levels[viewIndex] = viewThread->ChildLevelAt(posInThread++);
+ }
+
+ } else {
+ // The new header is the root, so we need to adjust all the children.
+ InsertMsgHdrAt(threadIndex, msgHdr, msgKey, msgFlags, 0);
+
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ nsMsgViewIndex i;
+ for (i = threadIndex + 1;
+ i < m_keys.Length() && (i == threadIndex + 1 || m_levels[i]);
+ i++)
+ m_levels[i] = m_levels[i] + 1;
+ // Turn off thread flags on old root.
+ AndExtraFlag(threadIndex + 1,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+
+ NoteChange(threadIndex + 1, i - threadIndex + 1,
+ nsMsgViewNotificationCode::changed);
+ }
+ } else if (!parent) {
+ // New parent came into collapsed thread.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ msgHdr->GetFolder(getter_AddRefs(msgFolder));
+ m_keys[threadIndex] = msgKey;
+ m_folders.ReplaceObjectAt(msgFolder, threadIndex);
+ m_flags[threadIndex] = msgFlags | MSG_VIEW_FLAG_ISTHREAD |
+ nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ if (moveThread) MoveThreadAt(threadIndex);
+ }
+ } else {
+ m_folders.AppendObject(folder);
+ // nsMsgKey_None means it's not a valid hdr.
+ if (msgKey != nsMsgKey_None) {
+ msgHdr->GetFlags(&msgFlags);
+ m_keys.AppendElement(msgKey);
+ m_levels.AppendElement(0);
+ m_flags.AppendElement(msgFlags);
+ NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+ }
+
+ return NS_OK;
+}
+
+// This method removes the thread at threadIndex from the view
+// and puts it back in its new position, determined by the sort order.
+// And, if the selection is affected, save and restore the selection.
+void nsMsgSearchDBView::MoveThreadAt(nsMsgViewIndex threadIndex) {
+ bool updatesSuppressed = mSuppressChangeNotification;
+ // Turn off tree notifications so that we don't reload the current message.
+ if (!updatesSuppressed) SetSuppressChangeNotifications(true);
+
+ nsCOMPtr<nsIMsgDBHdr> threadHdr;
+ GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr));
+
+ uint32_t saveFlags = m_flags[threadIndex];
+ bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided);
+ int32_t childCount = 0;
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ int32_t selectionCount;
+ int32_t currentIndex;
+ bool hasSelection =
+ mTreeSelection &&
+ ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) ||
+ (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) &&
+ selectionCount > 0));
+ if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ if (threadIsExpanded) {
+ ExpansionDelta(threadIndex, &childCount);
+ childCount = -childCount;
+ }
+
+ nsTArray<nsMsgKey> threadKeys;
+ nsTArray<uint32_t> threadFlags;
+ nsTArray<uint8_t> threadLevels;
+ nsCOMArray<nsIMsgFolder> threadFolders;
+
+ if (threadIsExpanded) {
+ threadKeys.SetCapacity(childCount);
+ threadFlags.SetCapacity(childCount);
+ threadLevels.SetCapacity(childCount);
+ threadFolders.SetCapacity(childCount);
+ for (nsMsgViewIndex index = threadIndex + 1;
+ index < (nsMsgViewIndex)GetSize() && m_levels[index]; index++) {
+ threadKeys.AppendElement(m_keys[index]);
+ threadFlags.AppendElement(m_flags[index]);
+ threadLevels.AppendElement(m_levels[index]);
+ threadFolders.AppendObject(m_folders[index]);
+ }
+
+ uint32_t collapseCount;
+ CollapseByIndex(threadIndex, &collapseCount);
+ }
+
+ nsMsgDBView::RemoveByIndex(threadIndex);
+ m_folders.RemoveObjectAt(threadIndex);
+ nsMsgViewIndex newIndex = GetIndexForThread(threadHdr);
+ NS_ASSERTION(newIndex == m_levels.Length() ||
+ (IsValidIndex(newIndex) && !m_levels[newIndex]),
+ "inserting into middle of thread");
+ if (newIndex == nsMsgViewIndex_None) newIndex = 0;
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ threadHdr->GetMessageKey(&msgKey);
+ threadHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(newIndex, threadHdr, msgKey, msgFlags, 0);
+
+ if (threadIsExpanded) {
+ m_keys.InsertElementsAt(newIndex + 1, threadKeys);
+ m_flags.InsertElementsAt(newIndex + 1, threadFlags);
+ m_levels.InsertElementsAt(newIndex + 1, threadLevels);
+ m_folders.InsertObjectsAt(threadFolders, newIndex + 1);
+ }
+
+ m_flags[newIndex] = saveFlags;
+ // Unfreeze selection.
+ if (hasSelection) RestoreSelection(preservedKey, preservedSelection);
+
+ if (!updatesSuppressed) SetSuppressChangeNotifications(false);
+
+ nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex;
+ nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex;
+ NoteChange(lowIndex, highIndex - lowIndex + childCount + 1,
+ nsMsgViewNotificationCode::changed);
+}
+
+nsresult nsMsgSearchDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ // We do not have an m_db, so the default behavior (in nsMsgDBView) is not
+ // what we want (it will crash). We just want someone to enumerate the
+ // headers that we already have. Conveniently, nsMsgDBView already knows
+ // how to do this with its view enumerator, so we just use that.
+ return nsMsgDBView::GetViewEnumerator(enumerator);
+}
+
+nsresult nsMsgSearchDBView::InsertHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder) {
+ nsMsgViewIndex insertIndex = nsMsgViewIndex_None;
+ // Threaded view always needs to go through AddHdrFromFolder since
+ // it handles the xf view thread object creation.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ insertIndex = GetInsertIndex(msgHdr);
+
+ if (insertIndex == nsMsgViewIndex_None)
+ return AddHdrFromFolder(msgHdr, folder);
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0);
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr, nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG(folder);
+
+ if (m_folders.IndexOf(folder) < 0)
+ // Do this just for new folder.
+ {
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(dbToUse));
+ if (dbToUse) {
+ dbToUse->AddListener(this);
+ m_dbToUseList.AppendObject(dbToUse);
+ }
+ }
+
+ m_totalMessagesInView++;
+ if (m_sortValid)
+ return InsertHdrFromFolder(aMsgHdr, folder);
+ else
+ return AddHdrFromFolder(aMsgHdr, folder);
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnSearchDone(nsresult status) {
+ // We want to set imap delete model once the search is over because setting
+ // next message after deletion will happen before deleting the message and
+ // search scope can change with every search.
+
+ // Set to default in case it is non-imap folder.
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0);
+ if (curFolder) GetImapDeleteModel(curFolder);
+
+ return NS_OK;
+}
+
+// For now also acts as a way of resetting the search datasource.
+NS_IMETHODIMP
+nsMsgSearchDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ int32_t count = m_dbToUseList.Count();
+ for (int32_t j = 0; j < count; j++) m_dbToUseList[j]->RemoveListener(this);
+
+ m_dbToUseList.Clear();
+ m_folders.Clear();
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ m_totalMessagesInView = 0;
+
+ // Needs to happen after we remove the keys, since RowCountChanged() will
+ // call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ // mSearchResults->Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowSearch;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ m_searchSession = do_GetWeakReference(aSession);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ nsIMsgDatabase* db = static_cast<nsIMsgDatabase*>(instigator);
+ if (db) {
+ db->RemoveListener(this);
+ m_dbToUseList.RemoveObject(db);
+ }
+
+ return NS_OK;
+}
+
+nsCOMArray<nsIMsgFolder>* nsMsgSearchDBView::GetFolders() { return &m_folders; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetCommandStatus(
+ nsMsgViewCommandTypeValue command, bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) {
+ if (command != nsMsgViewCommandType::runJunkControls)
+ return nsMsgDBView::GetCommandStatus(command, selectable_p, selected_p);
+
+ *selectable_p = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) {
+ mCommand = command;
+ mDestFolder = destFolder;
+ return nsMsgDBView::DoCommandWithFolder(command, destFolder);
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ mCommand = command;
+ if (command == nsMsgViewCommandType::deleteMsg ||
+ command == nsMsgViewCommandType::deleteNoTrash ||
+ command == nsMsgViewCommandType::selectAll ||
+ command == nsMsgViewCommandType::selectThread ||
+ command == nsMsgViewCommandType::selectFlagged ||
+ command == nsMsgViewCommandType::expandAll ||
+ command == nsMsgViewCommandType::collapseAll)
+ return nsMsgDBView::DoCommand(command);
+
+ nsresult rv = NS_OK;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ // We need to break apart the selection by folders, and then call
+ // ApplyCommandToIndices with the command and the indices in the
+ // selection that are from that folder.
+
+ mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]> indexArrays;
+ int32_t numArrays;
+ rv = PartitionSelectionByFolder(selection, indexArrays, &numArrays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t folderIndex = 0; folderIndex < numArrays; folderIndex++) {
+ rv = ApplyCommandToIndices(command, (indexArrays.get())[folderIndex]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+// This method removes the specified line from the view, and adjusts the
+// various flags and levels of affected messages.
+nsresult nsMsgSearchDBView::RemoveByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> thread;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (thread) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ if (viewThread->MsgCount() == 2) {
+ // If we removed the next to last message in the thread,
+ // we need to adjust the flags on the first message in the thread.
+ nsMsgViewIndex threadIndex = m_levels[index] ? index - 1 : index;
+ if (threadIndex != nsMsgViewIndex_None) {
+ AndExtraFlag(threadIndex,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+ m_levels[threadIndex] = 0;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // Bump up the level of all the descendents of the message
+ // that was removed, if the thread was expanded.
+ uint8_t removedLevel = m_levels[index];
+ nsMsgViewIndex i = index + 1;
+ if (i < m_levels.Length() && m_levels[i] > removedLevel) {
+ // Promote the child of the removed message.
+ uint8_t promotedLevel = m_levels[i];
+ m_levels[i] = promotedLevel - 1;
+ i++;
+ // Now promote all the children of the promoted message.
+ for (; i < m_levels.Length() && m_levels[i] > promotedLevel; i++)
+ m_levels[i] = m_levels[i] - 1;
+ }
+ }
+ }
+
+ m_folders.RemoveObjectAt(index);
+ return nsMsgDBView::RemoveByIndex(index);
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::ApplyCommandToIndices(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) {
+ mCommand = command;
+ return nsMsgDBView::ApplyCommandToIndices(command, selection);
+}
+
+nsresult nsMsgSearchDBView::DeleteMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ nsresult rv = GetFoldersAndHdrsForSelection(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mDeleteModel != nsMsgImapDeleteModels::MoveToTrash) deleteStorage = true;
+
+ if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) m_deletingRows = true;
+
+ // Remember the deleted messages in case the user undoes the delete,
+ // and we want to restore the hdr to the view, even if it no
+ // longer matches the search criteria.
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ RememberDeletedMsgHdr(msgHdr);
+ }
+
+ // If we are deleting rows, save off the view indices.
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElement(viewIndex);
+ }
+ }
+ rv = deleteStorage ? ProcessRequestsInAllFolders(window)
+ : ProcessRequestsInOneFolder(window);
+ if (NS_FAILED(rv)) m_deletingRows = false;
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::CopyMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) {
+ GetFoldersAndHdrsForSelection(selection);
+ return ProcessRequestsInOneFolder(window);
+}
+
+nsresult nsMsgSearchDBView::PartitionSelectionByFolder(
+ nsTArray<nsMsgViewIndex> const& selection,
+ mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]>& indexArrays,
+ int32_t* numArrays) {
+ nsCOMArray<nsIMsgFolder> uniqueFoldersSelected;
+ nsTArray<uint32_t> numIndicesSelected;
+ mCurIndex = 0;
+
+ // Build unique folder list based on headers selected by the user.
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsIMsgFolder* curFolder = m_folders[viewIndex];
+ int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder);
+ if (folderIndex < 0) {
+ uniqueFoldersSelected.AppendObject(curFolder);
+ numIndicesSelected.AppendElement(1);
+ } else {
+ numIndicesSelected[folderIndex]++;
+ }
+ }
+
+ int32_t numFolders = uniqueFoldersSelected.Count();
+ indexArrays = mozilla::MakeUnique<nsTArray<nsMsgViewIndex>[]>(numFolders);
+ *numArrays = numFolders;
+ NS_ENSURE_TRUE(indexArrays, NS_ERROR_OUT_OF_MEMORY);
+ for (int32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ (indexArrays.get())[folderIndex].SetCapacity(
+ numIndicesSelected[folderIndex]);
+ }
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsIMsgFolder* curFolder = m_folders[viewIndex];
+ int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder);
+ (indexArrays.get())[folderIndex].AppendElement(viewIndex);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::GetFoldersAndHdrsForSelection(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ nsresult rv = NS_OK;
+ mCurIndex = 0;
+ m_uniqueFoldersSelected.Clear();
+ m_hdrsForEachFolder.Clear();
+
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ rv = GetHeadersFromSelection(selection, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build unique folder list based on headers selected by the user.
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsCOMPtr<nsIMsgFolder> curFolder;
+ hdr->GetFolder(getter_AddRefs(curFolder));
+ if (m_uniqueFoldersSelected.IndexOf(curFolder) < 0) {
+ m_uniqueFoldersSelected.AppendObject(curFolder);
+ }
+ }
+
+ // Group the headers selected by each folder.
+ uint32_t numFolders = m_uniqueFoldersSelected.Count();
+ for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex];
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsForOneFolder;
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ hdr->GetFolder(getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder && msgFolder == curFolder) {
+ msgHdrsForOneFolder.AppendElement(hdr);
+ }
+ }
+
+ m_hdrsForEachFolder.AppendElement(msgHdrsForOneFolder.Clone());
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) {
+ mCommand = command;
+ mDestFolder = destFolder;
+ return nsMsgDBView::ApplyCommandToIndicesWithFolder(command, selection,
+ destFolder);
+}
+
+// nsIMsgCopyServiceListener methods
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnStartCopy() { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+// Believe it or not, these next two are msgcopyservice listener methods!
+NS_IMETHODIMP
+nsMsgSearchDBView::SetMessageKey(nsMsgKey aMessageKey) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetMessageId(nsACString& messageId) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnStopCopy(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ mCurIndex++;
+ if ((int32_t)mCurIndex < m_uniqueFoldersSelected.Count()) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ ProcessRequestsInOneFolder(msgWindow);
+ }
+ }
+
+ return NS_OK;
+}
+
+// End nsIMsgCopyServiceListener methods.
+
+nsresult nsMsgSearchDBView::ProcessRequestsInOneFolder(nsIMsgWindow* window) {
+ nsresult rv = NS_OK;
+
+ // Folder operations like copy/move are not implemented for .eml files.
+ if (m_uniqueFoldersSelected.Count() == 0) return NS_ERROR_NOT_IMPLEMENTED;
+
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[mCurIndex];
+ NS_ASSERTION(curFolder, "curFolder is null");
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& msgs = m_hdrsForEachFolder[mCurIndex];
+
+ // called for delete with trash, copy and move
+ if (mCommand == nsMsgViewCommandType::deleteMsg)
+ curFolder->DeleteMessages(msgs, window, false /* delete storage */,
+ false /* is move*/, this, true /*allowUndo*/);
+ else {
+ NS_ASSERTION(!(curFolder == mDestFolder),
+ "The source folder and the destination folder are the same");
+ if (NS_SUCCEEDED(rv) && curFolder != mDestFolder) {
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ if (mCommand == nsMsgViewCommandType::moveMessages)
+ copyService->CopyMessages(curFolder, msgs, mDestFolder,
+ true /* isMove */, this, window,
+ true /*allowUndo*/);
+ else if (mCommand == nsMsgViewCommandType::copyMessages)
+ copyService->CopyMessages(curFolder, msgs, mDestFolder,
+ false /* isMove */, this, window,
+ true /*allowUndo*/);
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::ProcessRequestsInAllFolders(nsIMsgWindow* window) {
+ uint32_t numFolders = m_uniqueFoldersSelected.Count();
+ for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex];
+ NS_ASSERTION(curFolder, "curFolder is null");
+ curFolder->DeleteMessages(
+ m_hdrsForEachFolder[folderIndex], window, true /* delete storage */,
+ false /* is move*/, nullptr /*copyServListener*/, false /*allowUndo*/);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered())
+ return NS_OK;
+
+ int32_t rowCountBeforeSort = GetSize();
+
+ if (!rowCountBeforeSort) return NS_OK;
+
+ if (m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort)) {
+ // ### This forgets which threads were expanded, and is sub-optimal
+ // since it rebuilds the thread objects.
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ return RebuildView(m_viewFlags);
+ }
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ nsresult rv = nsMsgDBView::Sort(sortType, sortOrder);
+ // The sort may have changed the number of rows before we restore the
+ // selection, tell the tree do this before we call restore selection.
+ // This is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgSearchDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) {
+ NS_ENSURE_ARG_POINTER(hdr);
+ nsMsgViewIndex index;
+ nsresult rv = GetViewIndexForFirstSelectedMsg(&index);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetMsgHdrForViewIndex(index, hdr);
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ if (aViewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder,
+ aViewFlags, aCount);
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags;
+ SaveSortInfo(m_sortType, m_sortOrder);
+
+ bool hasMore;
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ AddHdrFromFolder(msgHdr, folder);
+ }
+ }
+
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgMessageService> msgMessageService;
+ nsresult rv =
+ GetMessageServiceFromURI(aMsgURI, getter_AddRefs(msgMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgMessageService->MessageURIToMsgHdr(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return msgHdr->GetFolder(aFolder);
+}
+
+nsMsgViewIndex nsMsgSearchDBView::FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex,
+ bool allowDummy) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ uint32_t index;
+ // It would be nice to take advantage of sorted views when possible.
+ for (index = startIndex; index < GetSize(); index++) {
+ GetMsgHdrForViewIndex(index, getter_AddRefs(curHdr));
+ if (curHdr == msgHdr &&
+ (allowDummy || !(m_flags[index] & MSG_VIEW_FLAG_DUMMY) ||
+ (m_flags[index] & nsMsgMessageFlags::Elided)))
+ break;
+ }
+
+ return index < GetSize() ? index : nsMsgViewIndex_None;
+}
+
+// This method looks for the XF thread that corresponds to this message hdr,
+// first by looking up the message id, then references, and finally, if subject
+// threading is turned on, the subject.
+nsresult nsMsgSearchDBView::GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread,
+ bool* foundByMessageId) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(pThread);
+
+ nsAutoCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ *pThread = nullptr;
+ m_threadsTable.Get(messageId, pThread);
+ // The caller may want to know if we found the thread by the msgHdr's
+ // messageId.
+ if (foundByMessageId) *foundByMessageId = *pThread != nullptr;
+
+ if (!*pThread) {
+ uint16_t numReferences = 0;
+ msgHdr->GetNumReferences(&numReferences);
+ for (int32_t i = numReferences - 1; i >= 0 && !*pThread; i--) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ m_threadsTable.Get(reference, pThread);
+ }
+ }
+
+ // If we're threading by subject, and we couldn't find the thread by ref,
+ // just treat subject as an other ref.
+ if (!*pThread && !gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // This is the raw rfc822 subject header, so this is OK.
+ m_threadsTable.Get(subject, pThread);
+ }
+
+ return (*pThread) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool nsMsgSearchDBView::GetMsgHdrFromHash(nsCString& reference,
+ nsIMsgDBHdr** hdr) {
+ return m_hdrsTable.Get(reference, hdr);
+}
+
+bool nsMsgSearchDBView::GetThreadFromHash(nsCString& reference,
+ nsIMsgThread** thread) {
+ return m_threadsTable.Get(reference, thread);
+}
+
+nsresult nsMsgSearchDBView::AddRefToHash(nsCString& reference,
+ nsIMsgThread* thread) {
+ // Check if this reference is already is associated with a thread;
+ // If so, don't overwrite that association.
+ nsCOMPtr<nsIMsgThread> oldThread;
+ m_threadsTable.Get(reference, getter_AddRefs(oldThread));
+ if (oldThread) return NS_OK;
+
+ m_threadsTable.InsertOrUpdate(reference, thread);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::AddMsgToHashTables(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread* thread) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ uint16_t numReferences = 0;
+ nsresult rv;
+
+ msgHdr->GetNumReferences(&numReferences);
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = AddRefToHash(reference, thread);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ m_hdrsTable.InsertOrUpdate(messageId, msgHdr);
+ if (!gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // if we're threading by subject, just treat subject as an other ref.
+ AddRefToHash(subject, thread);
+ }
+
+ return AddRefToHash(messageId, thread);
+}
+
+nsresult nsMsgSearchDBView::RemoveRefFromHash(nsCString& reference) {
+ m_threadsTable.Remove(reference);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ uint16_t numReferences = 0;
+ nsresult rv = NS_OK;
+
+ msgHdr->GetNumReferences(&numReferences);
+
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = RemoveRefFromHash(reference);
+ if (NS_FAILED(rv)) break;
+ }
+
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ m_hdrsTable.Remove(messageId);
+ RemoveRefFromHash(messageId);
+ if (!gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // If we're threading by subject, just treat subject as an other ref.
+ RemoveRefFromHash(subject);
+ }
+
+ return rv;
+}
+
+nsMsgGroupThread* nsMsgSearchDBView::CreateGroupThread(
+ nsIMsgDatabase* /* db */) {
+ return new nsMsgXFGroupThread();
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::GetThreadContainingMsgHdr(msgHdr, pThread);
+ else if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ return GetXFThreadFromMsgHdr(msgHdr, pThread);
+
+ // If not threaded, use the real thread.
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgDB->GetThreadContainingMsgHdr(msgHdr, pThread);
+}
+
+nsresult nsMsgSearchDBView::ListIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG_POINTER(threadHdr);
+ NS_ENSURE_ARG_POINTER(pNumListed);
+
+ // These children ids should be in thread order.
+ uint32_t i;
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+ InsertEmptyRows(viewIndex, numChildren);
+
+ bool threadedView = m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort);
+ nsMsgXFViewThread* viewThread;
+ if (threadedView) viewThread = static_cast<nsMsgXFViewThread*>(threadHdr);
+
+ for (i = 1; i <= numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ uint8_t level = (threadedView) ? viewThread->ChildLevelAt(i) : 1;
+ SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level);
+ (*pNumListed)++;
+ viewIndex++;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ *aNumMsgs = m_totalMessagesInView;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.h b/comm/mailnews/base/src/nsMsgSearchDBView.h
new file mode 100644
index 0000000000..3dd363b58f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSearchDBView.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSearchDBViews_H_
+#define _nsMsgSearchDBViews_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgGroupView.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsMsgXFViewThread.h"
+#include "nsCOMArray.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgSearchDBView : public nsMsgGroupView,
+ public nsIMsgCopyServiceListener,
+ public nsIMsgSearchNotify {
+ public:
+ nsMsgSearchDBView();
+
+ // these are tied together pretty intimately
+ friend class nsMsgXFViewThread;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGSEARCHNOTIFY
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override;
+
+ virtual const char* GetViewName(void) override { return "SearchView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ NS_IMETHOD GetCommandStatus(
+ nsMsgViewCommandTypeValue command, bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override;
+ NS_IMETHOD DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) override;
+ NS_IMETHOD GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ // override to get location
+ NS_IMETHOD GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aValue) override;
+ virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) override;
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey,
+ bool ensureListed) override;
+ NS_IMETHOD GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** folder) override;
+
+ NS_IMETHOD OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) override;
+
+ virtual nsCOMArray<nsIMsgFolder>* GetFolders() override;
+ virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) override;
+
+ NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) override;
+
+ NS_IMETHOD ApplyCommandToIndices(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) override;
+
+ protected:
+ virtual ~nsMsgSearchDBView();
+ virtual void InternalClose() override;
+ virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) override;
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) override;
+ nsresult FetchLocation(int32_t aRow, nsAString& aLocationString);
+ virtual nsresult AddHdrFromFolder(nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder);
+ virtual nsresult GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) override;
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index) override;
+ virtual nsresult CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) override;
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) override;
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) override;
+ virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) override;
+ virtual void InsertEmptyRows(nsMsgViewIndex viewIndex,
+ int32_t numRows) override;
+ virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) override;
+ virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex = 0,
+ bool allowDummy = false) override;
+ nsresult GetFoldersAndHdrsForSelection(
+ nsTArray<nsMsgViewIndex> const& selection);
+ nsresult GroupSearchResultsByFolder();
+ nsresult PartitionSelectionByFolder(
+ nsTArray<nsMsgViewIndex> const& selection,
+ mozilla::UniquePtr<nsTArray<uint32_t>[]>& indexArrays,
+ int32_t* numArrays);
+
+ virtual nsresult ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection,
+ nsIMsgFolder* destFolder) override;
+ void MoveThreadAt(nsMsgViewIndex threadIndex);
+
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+ virtual nsresult InsertHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder);
+
+ // Holds the original folder of each message in this view.
+ // Augments the existing arrays in nsMsgDBView (m_keys, m_flags and m_levels),
+ // and is kept in sync with them.
+ nsCOMArray<nsIMsgFolder> m_folders;
+
+ nsTArray<nsTArray<RefPtr<nsIMsgDBHdr>>> m_hdrsForEachFolder;
+ nsCOMArray<nsIMsgFolder> m_uniqueFoldersSelected;
+ uint32_t mCurIndex;
+
+ int32_t mTotalIndices;
+ nsCOMArray<nsIMsgDatabase> m_dbToUseList;
+ nsMsgViewCommandTypeValue mCommand;
+ nsCOMPtr<nsIMsgFolder> mDestFolder;
+ nsWeakPtr m_searchSession;
+
+ nsresult ProcessRequestsInOneFolder(nsIMsgWindow* window);
+ nsresult ProcessRequestsInAllFolders(nsIMsgWindow* window);
+ // these are for doing threading of the search hits
+
+ // used for assigning thread id's to xfview threads.
+ nsMsgKey m_nextThreadId;
+ // this maps message-ids and reference message ids to
+ // the corresponding nsMsgXFViewThread object. If we're
+ // doing subject threading, we would throw subjects
+ // into the same table.
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgThread> m_threadsTable;
+
+ // map message-ids to msg hdrs in the view, used for threading.
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgDBHdr> m_hdrsTable;
+ uint32_t m_totalMessagesInView;
+
+ virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db) override;
+ nsresult GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr, nsIMsgThread** pThread,
+ bool* foundByMessageId = nullptr);
+ bool GetThreadFromHash(nsCString& reference, nsIMsgThread** thread);
+ bool GetMsgHdrFromHash(nsCString& reference, nsIMsgDBHdr** hdr);
+ nsresult AddRefToHash(nsCString& reference, nsIMsgThread* thread);
+ nsresult AddMsgToHashTables(nsIMsgDBHdr* msgHdr, nsIMsgThread* thread);
+ nsresult RemoveRefFromHash(nsCString& reference);
+ nsresult RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr);
+ nsresult InitRefHash();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.cpp b/comm/mailnews/base/src/nsMsgSpecialViews.cpp
new file mode 100644
index 0000000000..6bd42176e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSpecialViews.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgSpecialViews.h"
+#include "nsIMsgThread.h"
+#include "nsMsgMessageFlags.h"
+
+nsMsgThreadsWithUnreadDBView::nsMsgThreadsWithUnreadDBView()
+ : m_totalUnwantedMessagesInView(0) {}
+
+nsMsgThreadsWithUnreadDBView::~nsMsgThreadsWithUnreadDBView() {}
+
+NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowThreadsWithUnread;
+ return NS_OK;
+}
+
+bool nsMsgThreadsWithUnreadDBView::WantsThisThread(nsIMsgThread* threadHdr) {
+ if (threadHdr) {
+ uint32_t numNewChildren;
+
+ threadHdr->GetNumUnreadChildren(&numNewChildren);
+ if (numNewChildren > 0) return true;
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ m_totalUnwantedMessagesInView += numChildren;
+ }
+ return false;
+}
+
+nsresult nsMsgThreadsWithUnreadDBView::AddMsgToThreadNotInView(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgDBHdr> parentHdr;
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr));
+ if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) {
+ nsMsgKey key;
+ uint32_t numMsgsInThread;
+ rv = AddHdr(parentHdr);
+ threadHdr->GetNumChildren(&numMsgsInThread);
+ if (numMsgsInThread > 1) {
+ parentHdr->GetMessageKey(&key);
+ nsMsgViewIndex viewIndex = FindViewIndex(key);
+ if (viewIndex != nsMsgViewIndex_None)
+ OrExtraFlag(viewIndex,
+ nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_HASCHILDREN);
+ }
+ m_totalUnwantedMessagesInView -= numMsgsInThread;
+ } else
+ m_totalUnwantedMessagesInView++;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadsWithUnreadDBView::CloneDBView(
+ nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) {
+ nsMsgThreadsWithUnreadDBView* newMsgDBView =
+ new nsMsgThreadsWithUnreadDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetNumMsgsInView(
+ int32_t* aNumMsgs) {
+ nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView;
+ return rv;
+}
+
+nsMsgWatchedThreadsWithUnreadDBView::nsMsgWatchedThreadsWithUnreadDBView()
+ : m_totalUnwantedMessagesInView(0) {}
+
+NS_IMETHODIMP nsMsgWatchedThreadsWithUnreadDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowWatchedThreadsWithUnread;
+ return NS_OK;
+}
+
+bool nsMsgWatchedThreadsWithUnreadDBView::WantsThisThread(
+ nsIMsgThread* threadHdr) {
+ if (threadHdr) {
+ uint32_t numNewChildren;
+ uint32_t threadFlags;
+
+ threadHdr->GetNumUnreadChildren(&numNewChildren);
+ threadHdr->GetFlags(&threadFlags);
+ if (numNewChildren > 0 && (threadFlags & nsMsgMessageFlags::Watched) != 0)
+ return true;
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ m_totalUnwantedMessagesInView += numChildren;
+ }
+ return false;
+}
+
+nsresult nsMsgWatchedThreadsWithUnreadDBView::AddMsgToThreadNotInView(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) {
+ nsresult rv = NS_OK;
+ uint32_t threadFlags;
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ threadHdr->GetFlags(&threadFlags);
+ if (threadFlags & nsMsgMessageFlags::Watched) {
+ nsCOMPtr<nsIMsgDBHdr> parentHdr;
+ GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr));
+ if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) {
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ rv = AddHdr(parentHdr);
+ if (numChildren > 1) {
+ nsMsgKey key;
+ parentHdr->GetMessageKey(&key);
+ nsMsgViewIndex viewIndex = FindViewIndex(key);
+ if (viewIndex != nsMsgViewIndex_None)
+ OrExtraFlag(viewIndex, nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_ISTHREAD |
+ MSG_VIEW_FLAG_HASCHILDREN |
+ nsMsgMessageFlags::Watched);
+ }
+ m_totalUnwantedMessagesInView -= numChildren;
+ return rv;
+ }
+ }
+ m_totalUnwantedMessagesInView++;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgWatchedThreadsWithUnreadDBView::CloneDBView(
+ nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) {
+ nsMsgWatchedThreadsWithUnreadDBView* newMsgDBView =
+ new nsMsgWatchedThreadsWithUnreadDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgWatchedThreadsWithUnreadDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView;
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.h b/comm/mailnews/base/src/nsMsgSpecialViews.h
new file mode 100644
index 0000000000..1503eec123
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSpecialViews.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSpecialViews_H_
+#define _nsMsgSpecialViews_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgThreadedDBView.h"
+
+class nsMsgThreadsWithUnreadDBView : public nsMsgThreadedDBView {
+ public:
+ nsMsgThreadsWithUnreadDBView();
+ virtual ~nsMsgThreadsWithUnreadDBView();
+ virtual const char* GetViewName(void) override {
+ return "ThreadsWithUnreadView";
+ }
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ virtual bool WantsThisThread(nsIMsgThread* threadHdr) override;
+
+ protected:
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) override;
+ uint32_t m_totalUnwantedMessagesInView;
+};
+
+class nsMsgWatchedThreadsWithUnreadDBView : public nsMsgThreadedDBView {
+ public:
+ nsMsgWatchedThreadsWithUnreadDBView();
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ virtual const char* GetViewName(void) override {
+ return "WatchedThreadsWithUnreadView";
+ }
+ virtual bool WantsThisThread(nsIMsgThread* threadHdr) override;
+
+ protected:
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) override;
+ uint32_t m_totalUnwantedMessagesInView;
+};
+#ifdef DOING_CACHELESS_VIEW
+// This view will initially be used for cacheless IMAP.
+class nsMsgCachelessView : public nsMsgDBView {
+ public:
+ nsMsgCachelessView();
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType);
+ virtual ~nsMsgCachelessView();
+ virtual const char* GetViewName(void) { return "nsMsgCachelessView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue viewType,
+ int32_t* count);
+ nsresult SetViewSize(int32_t setSize); // Override
+ virtual nsresult AddNewMessages();
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr);
+ // for news, xover line, potentially, for IMAP, imap line...
+ virtual nsresult AddHdrFromServerLine(char* line, nsMsgKey* msgId);
+ virtual void SetInitialSortState(void);
+ virtual nsresult Init(uint32_t* pCount);
+
+ protected:
+ void ClearPendingIds();
+
+ nsIMsgFolder* m_folder;
+ nsMsgViewIndex m_curStartSeq;
+ nsMsgViewIndex m_curEndSeq;
+ bool m_sizeInitialized;
+};
+
+#endif /* DOING_CACHELESS_VIEW */
+#endif
diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.cpp b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp
new file mode 100644
index 0000000000..3afa14de87
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+
+#include "nsIWebProgress.h"
+#include "nsIXULBrowserWindow.h"
+#include "nsMsgStatusFeedback.h"
+#include "mozilla/dom/Document.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIChannel.h"
+#include "prinrval.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgWindow.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgDBFolder.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+#include "nsMsgUtils.h"
+
+#define MSGFEEDBACK_TIMER_INTERVAL 500
+
+nsMsgStatusFeedback::nsMsgStatusFeedback()
+ : m_meteorsSpinning(false), m_lastPercent(0), m_lastProgressTime(0) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+
+ if (bundleService)
+ bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(mBundle));
+}
+
+nsMsgStatusFeedback::~nsMsgStatusFeedback() { mBundle = nullptr; }
+
+NS_IMPL_ISUPPORTS(nsMsgStatusFeedback, nsIMsgStatusFeedback,
+ nsIProgressEventSink, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+//////////////////////////////////////////////////////////////////////////////////
+// nsMsgStatusFeedback::nsIWebProgressListener
+//////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ int32_t percentage = 0;
+ if (aMaxTotalProgress > 0) {
+ percentage = (aCurTotalProgress * 100) / aMaxTotalProgress;
+ if (percentage) ShowProgress(percentage);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aProgressStateFlags,
+ nsresult aStatus) {
+ nsresult rv;
+
+ NS_ENSURE_TRUE(mBundle, NS_ERROR_NULL_POINTER);
+ if (aProgressStateFlags & STATE_IS_NETWORK) {
+ if (aProgressStateFlags & STATE_START) {
+ m_lastPercent = 0;
+ StartMeteors();
+ nsString loadingDocument;
+ rv = mBundle->GetStringFromName("documentLoading", loadingDocument);
+ if (NS_SUCCEEDED(rv)) ShowStatusString(loadingDocument);
+ } else if (aProgressStateFlags & STATE_STOP) {
+ // if we are loading message for display purposes, this STATE_STOP
+ // notification is the only notification we get when layout is actually
+ // done rendering the message. We need to fire the appropriate msgHdrSink
+ // notification in this particular case.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) {
+ // get the url type
+ bool messageDisplayUrl;
+ mailnewsUrl->IsUrlType(nsIMsgMailNewsUrl::eDisplay,
+ &messageDisplayUrl);
+
+ if (messageDisplayUrl) {
+ // get the folder and notify that the msg has been loaded. We're
+ // using NotifyPropertyFlagChanged. To be completely consistent,
+ // we'd send a similar notification that the old message was
+ // unloaded.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ mailnewsUrl->GetFolder(getter_AddRefs(msgFolder));
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(mailnewsUrl);
+ if (msgUrl) {
+ // not sending this notification is not a fatal error...
+ (void)msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (msgFolder && msgHdr)
+ msgFolder->NotifyPropertyFlagChanged(msgHdr, kMsgLoaded, 0, 1);
+ }
+ }
+ }
+ }
+ StopMeteors();
+ nsString documentDone;
+ rv = mBundle->GetStringFromName("documentDone", documentDone);
+ if (NS_SUCCEEDED(rv)) ShowStatusString(documentDone);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnLocationChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t state) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::ShowStatusString(const nsAString& aStatus) {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->ShowStatusString(aStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::SetStatusString(const nsAString& aStatus) {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->SetStatusString(aStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::ShowProgress(int32_t aPercentage) {
+ // if the percentage hasn't changed...OR if we are going from 0 to 100% in one
+ // step then don't bother....just fall out....
+ if (aPercentage == m_lastPercent ||
+ (m_lastPercent == 0 && aPercentage >= 100))
+ return NS_OK;
+
+ m_lastPercent = aPercentage;
+
+ int64_t nowMS = 0;
+ if (aPercentage < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS < m_lastProgressTime + 250) return NS_OK;
+ }
+
+ m_lastProgressTime = nowMS;
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->ShowProgress(aPercentage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::StartMeteors() {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->StartMeteors();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::StopMeteors() {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->StopMeteors();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::SetWrappedStatusFeedback(
+ nsIMsgStatusFeedback* aJSStatusFeedback) {
+ NS_ENSURE_ARG_POINTER(aJSStatusFeedback);
+ mJSStatusFeedbackWeak = do_GetWeakReference(aJSStatusFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnProgress(nsIRequest* request,
+ int64_t aProgress,
+ int64_t aProgressMax) {
+ // XXX: What should the nsIWebProgress be?
+ // XXX: this truncates 64-bit to 32-bit
+ return OnProgressChange(nullptr, request, int32_t(aProgress),
+ int32_t(aProgressMax),
+ int32_t(aProgress) /* current total progress */,
+ int32_t(aProgressMax) /* max total progress */);
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnStatus(nsIRequest* request,
+ nsresult aStatus,
+ const char16_t* aStatusArg) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri;
+ nsString accountName;
+ // fetching account name from nsIRequest
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> url(do_QueryInterface(uri));
+ if (url) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ url->GetServer(getter_AddRefs(server));
+ if (server) server->GetPrettyName(accountName);
+ }
+
+ // forming the status message
+ nsCOMPtr<nsIStringBundleService> sbs =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED);
+ nsString str;
+ rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // prefixing the account name to the status message if status message isn't
+ // blank and doesn't already contain the account name.
+ nsString statusMessage;
+ if (!str.IsEmpty() && str.Find(accountName) == kNotFound) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = sbs->CreateBundle(MSGS_URL, getter_AddRefs(bundle));
+ AutoTArray<nsString, 2> params = {accountName, str};
+ rv = bundle->FormatStringFromName("statusMessage", params, statusMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ statusMessage.Assign(str);
+ }
+ return ShowStatusString(statusMessage);
+}
diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.h b/comm/mailnews/base/src/nsMsgStatusFeedback.h
new file mode 100644
index 0000000000..6713eaea4c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgStatusFeedback.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgStatusFeedback_h
+#define _nsMsgStatusFeedback_h
+
+#include "nsIWebProgressListener.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIProgressEventSink.h"
+#include "nsIStringBundle.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgStatusFeedback : public nsIMsgStatusFeedback,
+ public nsIProgressEventSink,
+ public nsIWebProgressListener,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgStatusFeedback();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGSTATUSFEEDBACK
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIPROGRESSEVENTSINK
+
+ protected:
+ virtual ~nsMsgStatusFeedback();
+
+ bool m_meteorsSpinning;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+
+ void BeginObserving();
+ void EndObserving();
+
+ // the JS status feedback implementation object...eventually this object
+ // will replace this very C++ class you are looking at.
+ nsWeakPtr mJSStatusFeedbackWeak;
+
+ nsCOMPtr<nsIStringBundle> mBundle;
+};
+
+#endif // _nsMsgStatusFeedback_h
diff --git a/comm/mailnews/base/src/nsMsgTagService.cpp b/comm/mailnews/base/src/nsMsgTagService.cpp
new file mode 100644
index 0000000000..83ced5fdeb
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTagService.cpp
@@ -0,0 +1,458 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgTagService.h"
+#include "nsIPrefService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsMsgI18N.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsMsgDBView.h" // for labels migration
+#include "nsQuickSort.h"
+#include "nsMsgUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+
+#define STRLEN(s) (sizeof(s) - 1)
+
+#define TAG_PREF_VERSION "version"
+#define TAG_PREF_SUFFIX_TAG ".tag"
+#define TAG_PREF_SUFFIX_COLOR ".color"
+#define TAG_PREF_SUFFIX_ORDINAL ".ordinal"
+
+static bool gMigratingKeys = false;
+
+// Comparator to set sort order in GetAllTags().
+struct CompareMsgTags {
+ private:
+ int cmp(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ // Sort nsMsgTag objects by ascending order, using their ordinal or key.
+ // The "smallest" value will be first in the sorted array,
+ // thus being the most important element.
+
+ // Only use the key if the ordinal is not defined or empty.
+ nsAutoCString value1, value2;
+ element1->GetOrdinal(value1);
+ if (value1.IsEmpty()) element1->GetKey(value1);
+ element2->GetOrdinal(value2);
+ if (value2.IsEmpty()) element2->GetKey(value2);
+
+ return strcmp(value1.get(), value2.get());
+ }
+
+ public:
+ bool Equals(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ return cmp(element1, element2) == 0;
+ }
+ bool LessThan(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ return cmp(element1, element2) < 0;
+ }
+};
+
+//
+// nsMsgTag
+//
+NS_IMPL_ISUPPORTS(nsMsgTag, nsIMsgTag)
+
+nsMsgTag::nsMsgTag(const nsACString& aKey, const nsAString& aTag,
+ const nsACString& aColor, const nsACString& aOrdinal)
+ : mTag(aTag), mKey(aKey), mColor(aColor), mOrdinal(aOrdinal) {}
+
+nsMsgTag::~nsMsgTag() {}
+
+/* readonly attribute ACString key; */
+NS_IMETHODIMP nsMsgTag::GetKey(nsACString& aKey) {
+ aKey = mKey;
+ return NS_OK;
+}
+
+/* readonly attribute AString tag; */
+NS_IMETHODIMP nsMsgTag::GetTag(nsAString& aTag) {
+ aTag = mTag;
+ return NS_OK;
+}
+
+/* readonly attribute ACString color; */
+NS_IMETHODIMP nsMsgTag::GetColor(nsACString& aColor) {
+ aColor = mColor;
+ return NS_OK;
+}
+
+/* readonly attribute ACString ordinal; */
+NS_IMETHODIMP nsMsgTag::GetOrdinal(nsACString& aOrdinal) {
+ aOrdinal = mOrdinal;
+ return NS_OK;
+}
+
+//
+// nsMsgTagService
+//
+NS_IMPL_ISUPPORTS(nsMsgTagService, nsIMsgTagService)
+
+nsMsgTagService::nsMsgTagService() {
+ m_tagPrefBranch = nullptr;
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefService)
+ prefService->GetBranch("mailnews.tags.", getter_AddRefs(m_tagPrefBranch));
+ SetupLabelTags();
+ RefreshKeyCache();
+}
+
+nsMsgTagService::~nsMsgTagService() {} /* destructor code */
+
+/* wstring getTagForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::GetTagForKey(const nsACString& key,
+ nsAString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ return GetUnicharPref(prefName.get(), _retval);
+}
+
+/* void setTagForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::SetTagForKey(const nsACString& key,
+ const nsAString& tag) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ return SetUnicharPref(prefName.get(), tag);
+}
+
+/* void getKeyForTag (in wstring tag); */
+NS_IMETHODIMP nsMsgTagService::GetKeyForTag(const nsAString& aTag,
+ nsACString& aKey) {
+ nsTArray<nsCString> prefList;
+ nsresult rv = m_tagPrefBranch->GetChildList("", prefList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // traverse the list, and look for a pref with the desired tag value.
+ // XXXbz is there a good reason to reverse the list here, or did the
+ // old code do it just to be clever and save some characters in the
+ // for loop header?
+ for (auto& prefName : mozilla::Reversed(prefList)) {
+ // We are returned the tag prefs in the form "<key>.<tag_data_type>", but
+ // since we only want the tags, just check that the string ends with "tag".
+ if (StringEndsWith(prefName, nsLiteralCString(TAG_PREF_SUFFIX_TAG))) {
+ nsAutoString curTag;
+ GetUnicharPref(prefName.get(), curTag);
+ if (aTag.Equals(curTag)) {
+ aKey = Substring(prefName, 0,
+ prefName.Length() - STRLEN(TAG_PREF_SUFFIX_TAG));
+ break;
+ }
+ }
+ }
+ ToLowerCase(aKey);
+ return NS_OK;
+}
+
+/* ACString getTopKey (in ACString keylist); */
+NS_IMETHODIMP nsMsgTagService::GetTopKey(const nsACString& keyList,
+ nsACString& _retval) {
+ _retval.Truncate();
+ // find the most important key
+ nsTArray<nsCString> keyArray;
+ ParseString(keyList, ' ', keyArray);
+ uint32_t keyCount = keyArray.Length();
+ nsCString *topKey = nullptr, *key, topOrdinal, ordinal;
+ for (uint32_t i = 0; i < keyCount; ++i) {
+ key = &keyArray[i];
+ if (key->IsEmpty()) continue;
+
+ // ignore unknown keywords
+ nsAutoString tagValue;
+ nsresult rv = GetTagForKey(*key, tagValue);
+ if (NS_FAILED(rv) || tagValue.IsEmpty()) continue;
+
+ // new top key, judged by ordinal order?
+ rv = GetOrdinalForKey(*key, ordinal);
+ if (NS_FAILED(rv) || ordinal.IsEmpty()) ordinal = *key;
+ if ((ordinal < topOrdinal) || topOrdinal.IsEmpty()) {
+ topOrdinal = ordinal;
+ topKey = key; // copy actual result key only once - later
+ }
+ }
+ // return the most important key - if any
+ if (topKey) _retval = *topKey;
+ return NS_OK;
+}
+
+/* void addTagForKey (in string key, in wstring tag, in string color, in string
+ * ordinal); */
+NS_IMETHODIMP nsMsgTagService::AddTagForKey(const nsACString& key,
+ const nsAString& tag,
+ const nsACString& color,
+ const nsACString& ordinal) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ nsresult rv = SetUnicharPref(prefName.get(), tag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetColorForKey(key, color);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RefreshKeyCache();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetOrdinalForKey(key, ordinal);
+}
+
+/* void addTag (in wstring tag, in long color); */
+NS_IMETHODIMP nsMsgTagService::AddTag(const nsAString& tag,
+ const nsACString& color,
+ const nsACString& ordinal) {
+ // figure out key from tag. Apply transformation stripping out
+ // illegal characters like <SP> and then convert to imap mod utf7.
+ // Then, check if we have a tag with that key yet, and if so,
+ // make it unique by appending A, AA, etc.
+ // Should we use an iterator?
+ nsAutoString transformedTag(tag);
+ transformedTag.ReplaceChar(u" ()/{%*<>\\\"", u'_');
+ nsAutoCString key;
+ CopyUTF16toMUTF7(transformedTag, key);
+ // We have an imap server that converts keys to upper case so we're going
+ // to normalize all keys to lower case (upper case looks ugly in prefs.js)
+ ToLowerCase(key);
+ nsAutoCString prefName(key);
+ while (true) {
+ nsAutoString tagValue;
+ nsresult rv = GetTagForKey(prefName, tagValue);
+ if (NS_FAILED(rv) || tagValue.IsEmpty() || tagValue.Equals(tag))
+ return AddTagForKey(prefName, tag, color, ordinal);
+ prefName.Append('A');
+ }
+ NS_ASSERTION(false, "can't get here");
+ return NS_ERROR_FAILURE;
+}
+
+/* long getColorForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::GetColorForKey(const nsACString& key,
+ nsACString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
+ nsCString color;
+ nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), color);
+ if (NS_SUCCEEDED(rv)) _retval = color;
+ return NS_OK;
+}
+
+/* long getSelectorForKey (in ACString key, out AString selector); */
+NS_IMETHODIMP nsMsgTagService::GetSelectorForKey(const nsACString& key,
+ nsAString& _retval) {
+ // Our keys are the result of MUTF-7 encoding. For CSS selectors we need
+ // to reduce this to 0-9A-Za-z_ with a leading alpha character.
+ // We encode non-alphanumeric characters using _ as an escape character
+ // and start with a leading T in all cases. This way users defining tags
+ // "selected" or "focus" don't collide with inbuilt "selected" or "focus".
+
+ // Calculate length of selector string.
+ const char* in = key.BeginReading();
+ size_t outLen = 1;
+ while (*in) {
+ if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
+ ('a' <= *in && *in <= 'z')) {
+ outLen++;
+ } else {
+ outLen += 3;
+ }
+ in++;
+ }
+
+ // Now fill selector string.
+ _retval.SetCapacity(outLen);
+ _retval.Assign('T');
+ in = key.BeginReading();
+ while (*in) {
+ if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
+ ('a' <= *in && *in <= 'z')) {
+ _retval.Append(*in);
+ } else {
+ _retval.AppendPrintf("_%02x", *in);
+ }
+ in++;
+ }
+
+ return NS_OK;
+}
+
+/* void setColorForKey (in ACString key, in ACString color); */
+NS_IMETHODIMP nsMsgTagService::SetColorForKey(const nsACString& key,
+ const nsACString& color) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
+ if (color.IsEmpty()) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ return NS_OK;
+ }
+ return m_tagPrefBranch->SetCharPref(prefName.get(), color);
+}
+
+/* ACString getOrdinalForKey (in ACString key); */
+NS_IMETHODIMP nsMsgTagService::GetOrdinalForKey(const nsACString& key,
+ nsACString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
+ nsCString ordinal;
+ nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), ordinal);
+ _retval = ordinal;
+ return rv;
+}
+
+/* void setOrdinalForKey (in ACString key, in ACString ordinal); */
+NS_IMETHODIMP nsMsgTagService::SetOrdinalForKey(const nsACString& key,
+ const nsACString& ordinal) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
+ if (ordinal.IsEmpty()) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ return NS_OK;
+ }
+ return m_tagPrefBranch->SetCharPref(prefName.get(), ordinal);
+}
+
+/* void deleteTag (in wstring tag); */
+NS_IMETHODIMP nsMsgTagService::DeleteKey(const nsACString& key) {
+ // clear the associated prefs
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.Append('.');
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = m_tagPrefBranch->GetChildList(prefName.get(), prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return RefreshKeyCache();
+}
+
+/* Array<nsIMsgTag> getAllTags(); */
+NS_IMETHODIMP nsMsgTagService::GetAllTags(
+ nsTArray<RefPtr<nsIMsgTag>>& aTagArray) {
+ aTagArray.Clear();
+
+ // get the actual tag definitions
+ nsresult rv;
+ nsTArray<nsCString> prefList;
+ rv = m_tagPrefBranch->GetChildList("", prefList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // sort them by key for ease of processing
+ prefList.Sort();
+
+ nsString tag;
+ nsCString lastKey, color, ordinal;
+ for (auto& pref : mozilla::Reversed(prefList)) {
+ // extract just the key from <key>.<info=tag|color|ordinal>
+ int32_t dotLoc = pref.RFindChar('.');
+ if (dotLoc != kNotFound) {
+ auto& key = Substring(pref, 0, dotLoc);
+ if (key != lastKey) {
+ if (!key.IsEmpty()) {
+ // .tag MUST exist (but may be empty)
+ rv = GetTagForKey(key, tag);
+ if (NS_SUCCEEDED(rv)) {
+ // .color MAY exist
+ color.Truncate();
+ GetColorForKey(key, color);
+ // .ordinal MAY exist
+ rv = GetOrdinalForKey(key, ordinal);
+ if (NS_FAILED(rv)) ordinal.Truncate();
+ // store the tag info in our array
+ aTagArray.AppendElement(new nsMsgTag(key, tag, color, ordinal));
+ }
+ }
+ lastKey = key;
+ }
+ }
+ }
+
+ // sort the non-null entries by ordinal
+ aTagArray.Sort(CompareMsgTags());
+ return NS_OK;
+}
+
+nsresult nsMsgTagService::SetUnicharPref(const char* prefName,
+ const nsAString& val) {
+ nsresult rv = NS_OK;
+ if (!val.IsEmpty()) {
+ rv = m_tagPrefBranch->SetStringPref(prefName, NS_ConvertUTF16toUTF8(val));
+ } else {
+ m_tagPrefBranch->ClearUserPref(prefName);
+ }
+ return rv;
+}
+
+nsresult nsMsgTagService::GetUnicharPref(const char* prefName,
+ nsAString& prefValue) {
+ nsCString valueUtf8;
+ nsresult rv =
+ m_tagPrefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, prefValue);
+ return rv;
+}
+
+nsresult nsMsgTagService::SetupLabelTags() {
+ nsCString prefString;
+
+ int32_t prefVersion = 0;
+ nsresult rv = m_tagPrefBranch->GetIntPref(TAG_PREF_VERSION, &prefVersion);
+ if (NS_SUCCEEDED(rv) && prefVersion > 1) {
+ return rv;
+ }
+ nsCOMPtr<nsIPrefBranch> prefRoot(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ nsCOMPtr<nsIPrefLocalizedString> pls;
+ nsString ucsval;
+ nsAutoCString labelKey("$label1");
+ for (int32_t i = 0; i < 5;) {
+ prefString.AssignLiteral("mailnews.labels.description.");
+ prefString.AppendInt(i + 1);
+ rv = prefRoot->GetComplexValue(prefString.get(),
+ NS_GET_IID(nsIPrefLocalizedString),
+ getter_AddRefs(pls));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pls->ToString(getter_Copies(ucsval));
+
+ prefString.AssignLiteral("mailnews.labels.color.");
+ prefString.AppendInt(i + 1);
+ nsCString csval;
+ rv = prefRoot->GetCharPref(prefString.get(), csval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddTagForKey(labelKey, ucsval, csval, EmptyCString());
+ NS_ENSURE_SUCCESS(rv, rv);
+ labelKey.SetCharAt(++i + '1', 6);
+ }
+ m_tagPrefBranch->SetIntPref(TAG_PREF_VERSION, 2);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgTagService::IsValidKey(const nsACString& aKey,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_keys.Contains(aKey);
+ return NS_OK;
+}
+
+// refresh the local tag key array m_keys from preferences
+nsresult nsMsgTagService::RefreshKeyCache() {
+ nsTArray<RefPtr<nsIMsgTag>> tagArray;
+ nsresult rv = GetAllTags(tagArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_keys.Clear();
+
+ uint32_t numTags = tagArray.Length();
+ m_keys.SetCapacity(numTags);
+ for (uint32_t tagIndex = 0; tagIndex < numTags; tagIndex++) {
+ nsAutoCString key;
+ tagArray[tagIndex]->GetKey(key);
+ m_keys.InsertElementAt(tagIndex, key);
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgTagService.h b/comm/mailnews/base/src/nsMsgTagService.h
new file mode 100644
index 0000000000..7eee648271
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTagService.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgTagService_h__
+#define nsMsgTagService_h__
+
+#include "nsIMsgTagService.h"
+#include "nsIPrefBranch.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsMsgTag final : public nsIMsgTag {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTAG
+
+ nsMsgTag(const nsACString& aKey, const nsAString& aTag,
+ const nsACString& aColor, const nsACString& aOrdinal);
+
+ protected:
+ ~nsMsgTag();
+
+ nsString mTag;
+ nsCString mKey, mColor, mOrdinal;
+};
+
+class nsMsgTagService final : public nsIMsgTagService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTAGSERVICE
+
+ nsMsgTagService();
+
+ private:
+ ~nsMsgTagService();
+
+ protected:
+ nsresult SetUnicharPref(const char* prefName, const nsAString& prefValue);
+ nsresult GetUnicharPref(const char* prefName, nsAString& prefValue);
+ nsresult SetupLabelTags();
+ nsresult RefreshKeyCache();
+
+ nsCOMPtr<nsIPrefBranch> m_tagPrefBranch;
+ nsTArray<nsCString> m_keys;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.cpp b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp
new file mode 100644
index 0000000000..84105bc4fc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp
@@ -0,0 +1,899 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgThreadedDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgSearchSession.h"
+#include "nsMsgMessageFlags.h"
+
+// Allocate this more to avoid reallocation on new mail.
+#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25
+// Max msghdr cache entries.
+#define MSGHDR_CACHE_MAX_SIZE 8192
+#define MSGHDR_CACHE_DEFAULT_SIZE 100
+
+nsMsgThreadedDBView::nsMsgThreadedDBView() {
+ /* member initializers and constructor code */
+ m_havePrevView = false;
+}
+
+nsMsgThreadedDBView::~nsMsgThreadedDBView() {} /* destructor code */
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+
+ // Preset msg hdr cache size for performance reason.
+ int32_t totalMessages, unreadMessages;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ PersistFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save off sort type and order, view type and flags.
+ dbFolderInfo->GetNumUnreadMessages(&unreadMessages);
+ dbFolderInfo->GetNumMessages(&totalMessages);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) {
+ // Set unread msg size + extra entries to avoid reallocation on new mail.
+ totalMessages = (uint32_t)unreadMessages + MSGHDR_CACHE_LOOK_AHEAD_SIZE;
+ } else {
+ if (totalMessages > MSGHDR_CACHE_MAX_SIZE)
+ // Use max default.
+ totalMessages = MSGHDR_CACHE_MAX_SIZE;
+ else if (totalMessages > 0)
+ // Allocate extra entries to avoid reallocation on new mail.
+ totalMessages += MSGHDR_CACHE_LOOK_AHEAD_SIZE;
+ }
+
+ // If total messages is 0, then we probably don't have any idea how many
+ // headers are in the db so we have no business setting the cache size.
+ if (totalMessages > 0) m_db->SetMsgHdrCacheSize((uint32_t)totalMessages);
+
+ int32_t count;
+ rv = InitThreadedView(count);
+ if (pCount) *pCount = count;
+
+ // This is a hack, but we're trying to find a way to correct
+ // incorrect total and unread msg counts w/o paying a big
+ // performance penalty. So, if we're not threaded, just add
+ // up the total and unread messages in the view and see if that
+ // matches what the db totals say. Except ignored threads are
+ // going to throw us off...hmm. Unless we just look at the
+ // unread counts which is what mostly tweaks people anyway...
+ int32_t unreadMsgsInView = 0;
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ for (uint32_t i = m_flags.Length(); i > 0;) {
+ if (!(m_flags[--i] & nsMsgMessageFlags::Read)) ++unreadMsgsInView;
+ }
+
+ if (unreadMessages != unreadMsgsInView) m_db->SyncCounts();
+ }
+
+ m_db->SetMsgHdrCacheSize(MSGHDR_CACHE_DEFAULT_SIZE);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Close() { return nsMsgDBView::Close(); }
+
+// Populate the view with the ids of the first message in each thread.
+nsresult nsMsgThreadedDBView::InitThreadedView(int32_t& count) {
+ count = 0;
+ m_keys.Clear();
+ m_flags.Clear();
+ m_levels.Clear();
+ m_prevKeys.Clear();
+ m_prevFlags.Clear();
+ m_prevLevels.Clear();
+ m_havePrevView = false;
+
+ bool unreadOnly = (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly);
+
+ nsCOMPtr<nsIMsgThreadEnumerator> threads;
+ nsresult rv = m_db->EnumerateThreads(getter_AddRefs(threads));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = threads->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ rv = threads->GetNext(getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t numChildren;
+ if (unreadOnly)
+ threadHdr->GetNumUnreadChildren(&numChildren);
+ else
+ threadHdr->GetNumChildren(&numChildren);
+
+ if (numChildren == 0) {
+ continue; // An empty thread.
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (unreadOnly) {
+ rv = threadHdr->GetFirstUnreadChild(getter_AddRefs(msgHdr));
+ } else {
+ rv = threadHdr->GetRootHdr(getter_AddRefs(msgHdr));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Hook to allow derived classes to filter out unwanted threads.
+ if (!WantsThisThread(threadHdr)) {
+ continue;
+ }
+
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ // Turn off high byte of msg flags - used for view flags.
+ msgFlags &= ~MSG_VIEW_FLAGS;
+ // Turn off these flags on msg hdr - they belong in thread.
+ uint32_t newMsgFlagsUnused;
+ msgHdr->AndFlags(~(nsMsgMessageFlags::Watched), &newMsgFlagsUnused);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ // Try adding in MSG_VIEW_FLAG_ISTHREAD flag for unreadonly view.
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ msgFlags |= MSG_VIEW_FLAG_ISTHREAD | threadFlags;
+ if (numChildren > 1) {
+ msgFlags |= MSG_VIEW_FLAG_HASCHILDREN;
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ // Skip ignored threads.
+ if (msgFlags & nsMsgMessageFlags::Ignored) {
+ continue;
+ }
+ // Skip ignored subthreads
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) {
+ continue;
+ }
+ }
+
+ // By default, make threads collapsed unless we're only viewing new msgs.
+ if (msgFlags & MSG_VIEW_FLAG_HASCHILDREN) {
+ msgFlags |= nsMsgMessageFlags::Elided;
+ }
+
+ // OK, now add it to the view!
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ m_keys.AppendElement(msgKey);
+ m_flags.AppendElement(msgFlags);
+ m_levels.AppendElement(0);
+
+ // We expand as we build the view, which allows us to insert at the end
+ // of the key array, instead of the middle, and is much faster.
+ if ((!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ m_viewFlags & nsMsgViewFlagsType::kExpandAll) &&
+ msgFlags & nsMsgMessageFlags::Elided) {
+ ExpandByIndex(m_keys.Length() - 1, nullptr);
+ }
+
+ count++;
+ }
+
+ rv = InitSort(m_sortType, m_sortOrder);
+ SaveSortInfo(m_sortType, m_sortOrder);
+ return rv;
+}
+
+nsresult nsMsgThreadedDBView::SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ NS_ASSERTION(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay,
+ "trying to sort unthreaded threads");
+
+ uint32_t numThreads = 0;
+ // The idea here is that copy the current view, then build up an m_keys and
+ // m_flags array of just the top level messages in the view, and then call
+ // nsMsgDBView::Sort(sortType, sortOrder).
+ // Then, we expand the threads in the result array that were expanded in the
+ // original view (perhaps by copying from the original view, but more likely
+ // just be calling expand).
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD) {
+ if (numThreads < i) {
+ m_keys[numThreads] = m_keys[i];
+ m_flags[numThreads] = m_flags[i];
+ }
+
+ m_levels[numThreads] = 0;
+ numThreads++;
+ }
+ }
+
+ m_keys.SetLength(numThreads);
+ m_flags.SetLength(numThreads);
+ m_levels.SetLength(numThreads);
+ // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay;
+ m_sortType = nsMsgViewSortType::byNone; // sort from scratch
+ nsMsgDBView::Sort(sortType, sortOrder);
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ SetSuppressChangeNotifications(true);
+
+ // Loop through the original array, for each thread that's expanded,
+ // find it in the new array and expand the thread. We have to update
+ // MSG_VIEW_FLAG_HAS_CHILDREN because we may be going from a flat sort,
+ // which doesn't maintain that flag, to a threaded sort, which requires
+ // that flag.
+ for (uint32_t j = 0; j < m_keys.Length(); j++) {
+ uint32_t flags = m_flags[j];
+ if ((flags & (MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided)) ==
+ MSG_VIEW_FLAG_HASCHILDREN) {
+ uint32_t numExpanded;
+ m_flags[j] = flags | nsMsgMessageFlags::Elided;
+ ExpandByIndex(j, &numExpanded);
+ j += numExpanded;
+ if (numExpanded > 0)
+ m_flags[j - numExpanded] = flags | MSG_VIEW_FLAG_HASCHILDREN;
+ } else if (flags & MSG_VIEW_FLAG_ISTHREAD &&
+ !(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> pThread;
+ m_db->GetMsgHdrForKey(m_keys[j], getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (pThread) {
+ uint32_t numChildren;
+ pThread->GetNumChildren(&numChildren);
+ if (numChildren > 1)
+ m_flags[j] =
+ flags | MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided;
+ }
+ }
+ }
+ }
+
+ SetSuppressChangeNotifications(false);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ nsresult rv;
+
+ int32_t rowCountBeforeSort = GetSize();
+
+ if (!rowCountBeforeSort) {
+ // Still need to setup our flags even when no articles - bug 98183.
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ if (sortType == nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ SetViewFlags(m_viewFlags | nsMsgViewFlagsType::kThreadedDisplay);
+ }
+
+ SaveSortInfo(sortType, sortOrder);
+ return NS_OK;
+ }
+
+ if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered())
+ return NS_OK;
+
+ // Sort threads by sort order.
+ bool sortThreads = m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort);
+
+ // If sort type is by thread, and we're already threaded, change sort type
+ // to byId.
+ if (sortType == nsMsgViewSortType::byThread &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0) {
+ sortType = nsMsgViewSortType::byId;
+ }
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ SaveAndClearSelection(&preservedKey, preservedSelection);
+ // If the client wants us to forget our cached id arrays, they
+ // should build a new view. If this isn't good enough, we
+ // need a method to do that.
+ if (sortType != m_sortType || !m_sortValid || sortThreads) {
+ SaveSortInfo(sortType, sortOrder);
+ if (sortType == nsMsgViewSortType::byThread) {
+ m_sortType = sortType;
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort;
+ if (m_havePrevView) {
+ // Restore saved id array and flags array.
+ m_keys = m_prevKeys.Clone();
+ m_flags = m_prevFlags.Clone();
+ m_levels = m_prevLevels.Clone();
+ m_sortValid = true;
+
+ // The sort may have changed the number of rows
+ // before we restore the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ return NS_OK;
+ } else {
+ // Set sort info in anticipation of what Init will do.
+ // Build up thread list.
+ int32_t unused; // count.
+ InitThreadedView(unused);
+ if (sortOrder != nsMsgViewSortOrder::ascending)
+ Sort(sortType, sortOrder);
+
+ // The sort may have changed the number of rows
+ // before we update the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ return NS_OK;
+ }
+ } else if (sortType != nsMsgViewSortType::byThread &&
+ (m_sortType == nsMsgViewSortType::byThread || sortThreads)
+ /* && !m_havePrevView*/) {
+ if (sortThreads) {
+ SortThreads(sortType, sortOrder);
+ // Hack so base class won't do anything.
+ sortType = nsMsgViewSortType::byThread;
+ } else {
+ // Going from SortByThread to non-thread sort - must build new key,
+ // level, and flags arrays.
+ m_prevKeys = m_keys.Clone();
+ m_prevFlags = m_flags.Clone();
+ m_prevLevels = m_levels.Clone();
+ // Do this before we sort, so that we'll use the cheap method
+ // of expanding.
+ m_viewFlags &= ~(nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort);
+ ExpandAll();
+ // m_idArray.RemoveAll();
+ // m_flags.Clear();
+ m_havePrevView = true;
+ }
+ }
+ } else if (m_sortOrder != sortOrder) {
+ // Check for toggling the sort.
+ nsMsgDBView::Sort(sortType, sortOrder);
+ }
+
+ if (!sortThreads) {
+ // Call the base class in case we're not sorting by thread.
+ rv = nsMsgDBView::Sort(sortType, sortOrder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SaveSortInfo(sortType, sortOrder);
+ }
+
+ // The sort may have changed the number of rows
+ // before we restore the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+void nsMsgThreadedDBView::OnExtraFlagChanged(nsMsgViewIndex index,
+ uint32_t extraFlag) {
+ if (IsValidIndex(index)) {
+ if (m_havePrevView) {
+ nsMsgKey keyChanged = m_keys[index];
+ nsMsgViewIndex prevViewIndex = m_prevKeys.IndexOf(keyChanged);
+ if (prevViewIndex != nsMsgViewIndex_None) {
+ uint32_t prevFlag = m_prevFlags[prevViewIndex];
+ // Don't want to change the elided bit, or has children or is thread.
+ if (prevFlag & nsMsgMessageFlags::Elided)
+ extraFlag |= nsMsgMessageFlags::Elided;
+ else
+ extraFlag &= ~nsMsgMessageFlags::Elided;
+
+ if (prevFlag & MSG_VIEW_FLAG_ISTHREAD)
+ extraFlag |= MSG_VIEW_FLAG_ISTHREAD;
+ else
+ extraFlag &= ~MSG_VIEW_FLAG_ISTHREAD;
+
+ if (prevFlag & MSG_VIEW_FLAG_HASCHILDREN)
+ extraFlag |= MSG_VIEW_FLAG_HASCHILDREN;
+ else
+ extraFlag &= ~MSG_VIEW_FLAG_HASCHILDREN;
+
+ // Will this be right?
+ m_prevFlags[prevViewIndex] = extraFlag;
+ }
+ }
+ }
+
+ // We don't really know what's changed, but to be on the safe side, set the
+ // sort invalid so that reverse sort will pick it up.
+ if (m_sortType == nsMsgViewSortType::byStatus ||
+ m_sortType == nsMsgViewSortType::byFlagged ||
+ m_sortType == nsMsgViewSortType::byUnread ||
+ m_sortType == nsMsgViewSortType::byPriority) {
+ m_sortValid = false;
+ }
+}
+
+void nsMsgThreadedDBView::OnHeaderAddedOrDeleted() { ClearPrevIdArray(); }
+
+void nsMsgThreadedDBView::ClearPrevIdArray() {
+ m_prevKeys.Clear();
+ m_prevLevels.Clear();
+ m_prevFlags.Clear();
+ m_havePrevView = false;
+}
+
+nsresult nsMsgThreadedDBView::InitSort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ // Nothing to do.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK;
+
+ if (sortType == nsMsgViewSortType::byThread) {
+ // Sort top level threads by id.
+ nsMsgDBView::Sort(nsMsgViewSortType::byId, sortOrder);
+ m_sortType = nsMsgViewSortType::byThread;
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort;
+ // Persist the view flags.
+ SetViewFlags(m_viewFlags);
+ // m_db->SetSortInfo(m_sortType, sortOrder);
+ }
+ // else
+ // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay;
+
+ // By default, the unread only view should have all threads expanded.
+ if ((m_viewFlags &
+ (nsMsgViewFlagsType::kUnreadOnly | nsMsgViewFlagsType::kExpandAll)) &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ ExpandAll();
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // For now, expand all and do a flat sort.
+ ExpandAll();
+ }
+
+ Sort(sortType, sortOrder);
+ if (sortType != nsMsgViewSortType::byThread) {
+ // Forget prev view, since it has everything expanded.
+ ClearPrevIdArray();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgThreadedDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnNewHeader(newHdr, aParentKey, ensureListed);
+
+ NS_ENSURE_TRUE(newHdr, NS_MSG_MESSAGE_NOT_FOUND);
+
+ nsMsgKey newKey;
+ newHdr->GetMessageKey(&newKey);
+
+ // Views can override this behaviour, which is to append to view.
+ // This is the mail behaviour, but threaded views want
+ // to insert in order...
+ uint32_t msgFlags;
+ newHdr->GetFlags(&msgFlags);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly && !ensureListed &&
+ msgFlags & nsMsgMessageFlags::Read) {
+ return NS_OK;
+ }
+
+ // Currently, we only add the header in a threaded view if it's a thread.
+ // We used to check if this was the first header in the thread, but that's
+ // a bit harder in the unreadOnly view. But we'll catch it below.
+
+ // If not threaded display just add it to the view.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return AddHdr(newHdr);
+
+ // Need to find the thread we added this to so we can change the hasnew flag
+ // added message to existing thread, but not to view.
+ // Fix flags on thread header.
+ int32_t threadCount;
+ uint32_t threadFlags;
+ bool moveThread = false;
+ nsMsgViewIndex threadIndex =
+ ThreadIndexOfMsg(newKey, nsMsgViewIndex_None, &threadCount, &threadFlags);
+ bool threadRootIsDisplayed = false;
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ m_db->GetThreadContainingMsgHdr(newHdr, getter_AddRefs(threadHdr));
+ if (threadHdr && m_sortType == nsMsgViewSortType::byDate) {
+ uint32_t newestMsgInThread = 0, msgDate = 0;
+ threadHdr->GetNewestMsgDate(&newestMsgInThread);
+ newHdr->GetDateInSeconds(&msgDate);
+ moveThread = (msgDate == newestMsgInThread);
+ }
+
+ if (threadIndex != nsMsgViewIndex_None) {
+ threadRootIsDisplayed = (m_currentlyDisplayedViewIndex == threadIndex);
+ uint32_t flags = m_flags[threadIndex];
+ if (!(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ flags |= MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD;
+ if (!(m_viewFlags & nsMsgViewFlagsType::kUnreadOnly))
+ flags |= nsMsgMessageFlags::Elided;
+
+ m_flags[threadIndex] = flags;
+ }
+
+ if (!(flags & nsMsgMessageFlags::Elided)) {
+ // Thread is expanded.
+ // Insert child into thread.
+ // Levels of other hdrs may have changed!
+ uint32_t newFlags = msgFlags;
+ int32_t level = 0;
+ nsMsgViewIndex insertIndex = threadIndex;
+ if (aParentKey == nsMsgKey_None) {
+ newFlags |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN;
+ } else {
+ nsMsgViewIndex parentIndex =
+ FindParentInThread(aParentKey, threadIndex);
+ level = m_levels[parentIndex] + 1;
+ insertIndex = GetInsertInfoForNewHdr(newHdr, parentIndex, level);
+ }
+
+ InsertMsgHdrAt(insertIndex, newHdr, newKey, newFlags, level);
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+
+ if (aParentKey == nsMsgKey_None) {
+ // this header is the new king! try collapsing the existing thread,
+ // removing it, installing this header as king, and expanding it.
+ CollapseByIndex(threadIndex, nullptr);
+ // call base class, so child won't get promoted.
+ // nsMsgDBView::RemoveByIndex(threadIndex);
+ ExpandByIndex(threadIndex, nullptr);
+ }
+ } else if (aParentKey == nsMsgKey_None) {
+ // if we have a collapsed thread which just got a new
+ // top of thread, change the keys array.
+ m_keys[threadIndex] = newKey;
+ }
+
+ // If this message is new, the thread is collapsed, it is the
+ // root and it was displayed, expand it so that the user does
+ // not find that their message has magically turned into a summary.
+ if (msgFlags & nsMsgMessageFlags::New &&
+ m_flags[threadIndex] & nsMsgMessageFlags::Elided &&
+ threadRootIsDisplayed)
+ ExpandByIndex(threadIndex, nullptr);
+
+ if (moveThread)
+ MoveThreadAt(threadIndex);
+ else
+ // note change, to update the parent thread's unread and total counts
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ } else if (threadHdr) {
+ // Adding msg to thread that's not in view.
+ AddMsgToThreadNotInView(threadHdr, newHdr, ensureListed);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ // We need to adjust the level of the hdr whose parent changed, and
+ // invalidate that row, iff we're in threaded mode.
+#if 0
+ // This code never runs due to the if (false) and Clang complains about it
+ // so it is ifdefed out for now.
+ if (false && m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ {
+ nsMsgViewIndex childIndex = FindViewIndex(aKeyChanged);
+ if (childIndex != nsMsgViewIndex_None)
+ {
+ nsMsgViewIndex parentIndex = FindViewIndex(newParent);
+ int32_t newParentLevel =
+ (parentIndex == nsMsgViewIndex_None) ? -1 : m_levels[parentIndex];
+
+ nsMsgViewIndex oldParentIndex = FindViewIndex(oldParent);
+
+ int32_t oldParentLevel =
+ (oldParentIndex != nsMsgViewIndex_None ||
+ newParent == nsMsgKey_None) ? m_levels[oldParentIndex] : -1 ;
+
+ int32_t levelChanged = m_levels[childIndex];
+ int32_t parentDelta = oldParentLevel - newParentLevel;
+ m_levels[childIndex] = (newParent == nsMsgKey_None) ? 0 : newParentLevel + 1;
+ if (parentDelta > 0)
+ {
+ for (nsMsgViewIndex viewIndex = childIndex + 1;
+ viewIndex < GetSize() && m_levels[viewIndex] > levelChanged;
+ viewIndex++)
+ {
+ m_levels[viewIndex] = m_levels[viewIndex] - parentDelta;
+ NoteChange(viewIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ NoteChange(childIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+#endif
+ return NS_OK;
+}
+
+nsMsgViewIndex nsMsgThreadedDBView::GetInsertInfoForNewHdr(
+ nsIMsgDBHdr* newHdr, nsMsgViewIndex parentIndex, int32_t targetLevel) {
+ uint32_t viewSize = GetSize();
+ while (++parentIndex < viewSize) {
+ // Loop until we find a message at a level less than or equal to the
+ // parent level
+ if (m_levels[parentIndex] < targetLevel) break;
+ }
+
+ return parentIndex;
+}
+
+// This method removes the thread at threadIndex from the view
+// and puts it back in its new position, determined by the sort order.
+// And, if the selection is affected, save and restore the selection.
+void nsMsgThreadedDBView::MoveThreadAt(nsMsgViewIndex threadIndex) {
+ // We need to check if the thread is collapsed or not...
+ // We want to turn off tree notifications so that we don't
+ // reload the current message.
+ // We also need to invalidate the range between where the thread was
+ // and where it ended up.
+ bool changesDisabled = mSuppressChangeNotification;
+ if (!changesDisabled) SetSuppressChangeNotifications(true);
+
+ nsCOMPtr<nsIMsgDBHdr> threadHdr;
+
+ GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr));
+ int32_t childCount = 0;
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ int32_t selectionCount;
+ int32_t currentIndex;
+ bool hasSelection =
+ mTreeSelection &&
+ ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) ||
+ (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) &&
+ selectionCount > 0));
+ if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ uint32_t saveFlags = m_flags[threadIndex];
+ bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided);
+
+ if (threadIsExpanded) {
+ ExpansionDelta(threadIndex, &childCount);
+ childCount = -childCount;
+ }
+
+ nsTArray<nsMsgKey> threadKeys;
+ nsTArray<uint32_t> threadFlags;
+ nsTArray<uint8_t> threadLevels;
+
+ if (threadIsExpanded) {
+ threadKeys.SetCapacity(childCount);
+ threadFlags.SetCapacity(childCount);
+ threadLevels.SetCapacity(childCount);
+ for (nsMsgViewIndex index = threadIndex + 1;
+ index < GetSize() && m_levels[index]; index++) {
+ threadKeys.AppendElement(m_keys[index]);
+ threadFlags.AppendElement(m_flags[index]);
+ threadLevels.AppendElement(m_levels[index]);
+ }
+
+ uint32_t collapseCount;
+ CollapseByIndex(threadIndex, &collapseCount);
+ }
+
+ nsMsgDBView::RemoveByIndex(threadIndex);
+ nsMsgViewIndex newIndex = nsMsgViewIndex_None;
+ AddHdr(threadHdr, &newIndex);
+
+ // AddHdr doesn't always set newIndex, and getting it to do so
+ // is going to require some refactoring.
+ if (newIndex == nsMsgViewIndex_None) newIndex = FindHdr(threadHdr);
+
+ if (threadIsExpanded) {
+ m_keys.InsertElementsAt(newIndex + 1, threadKeys);
+ m_flags.InsertElementsAt(newIndex + 1, threadFlags);
+ m_levels.InsertElementsAt(newIndex + 1, threadLevels);
+ }
+
+ if (newIndex == nsMsgViewIndex_None) {
+ NS_WARNING("newIndex=-1 in MoveThreadAt");
+ newIndex = 0;
+ }
+
+ m_flags[newIndex] = saveFlags;
+ // Unfreeze selection.
+ if (hasSelection) RestoreSelection(preservedKey, preservedSelection);
+
+ if (!changesDisabled) SetSuppressChangeNotifications(false);
+
+ nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex;
+ nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex;
+
+ NoteChange(lowIndex, highIndex - lowIndex + childCount + 1,
+ nsMsgViewNotificationCode::changed);
+}
+
+nsresult nsMsgThreadedDBView::AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) {
+ nsresult rv = NS_OK;
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ if (!(threadFlags & nsMsgMessageFlags::Ignored)) {
+ bool msgKilled;
+ msgHdr->GetIsKilled(&msgKilled);
+ if (!msgKilled) rv = nsMsgDBView::AddHdr(msgHdr);
+ }
+
+ return rv;
+}
+
+// This method just removes the specified line from the view. It does
+// NOT delete it from the database.
+nsresult nsMsgThreadedDBView::RemoveByIndex(nsMsgViewIndex index) {
+ nsresult rv = NS_OK;
+ int32_t flags;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ OnHeaderAddedOrDeleted();
+
+ flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return nsMsgDBView::RemoveByIndex(index);
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ GetThreadContainingIndex(index, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numThreadChildren = 0;
+ // If we can't get a thread, it's already deleted and thus has 0 children.
+ if (threadHdr) threadHdr->GetNumChildren(&numThreadChildren);
+
+ // Check if we're the top level msg in the thread, and we're not collapsed.
+ if ((flags & MSG_VIEW_FLAG_ISTHREAD) &&
+ !(flags & nsMsgMessageFlags::Elided) &&
+ (flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ // Fix flags on thread header - newly promoted message should have
+ // flags set correctly.
+ if (threadHdr) {
+ nsMsgDBView::RemoveByIndex(index);
+ nsCOMPtr<nsIMsgThread> nextThreadHdr;
+ // Above RemoveByIndex may now make index out of bounds.
+ if (IsValidIndex(index) && numThreadChildren > 0) {
+ // unreadOnly
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ uint32_t flag = 0;
+ msgHdr->GetFlags(&flag);
+ if (numThreadChildren > 1)
+ flag |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN;
+
+ m_flags[index] = flag;
+ m_levels[index] = 0;
+ }
+ }
+ }
+
+ return rv;
+ } else if (!(flags & MSG_VIEW_FLAG_ISTHREAD)) {
+ // We're not deleting the top level msg, but top level msg might be the
+ // only msg in thread now.
+ if (threadHdr && numThreadChildren == 1) {
+ nsMsgKey msgKey;
+ rv = threadHdr->GetChildKeyAt(0, &msgKey);
+ if (NS_SUCCEEDED(rv)) {
+ nsMsgViewIndex threadIndex = FindViewIndex(msgKey);
+ if (IsValidIndex(threadIndex)) {
+ uint32_t flags = m_flags[threadIndex];
+ flags &= ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN);
+ m_flags[threadIndex] = flags;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+ }
+
+ return nsMsgDBView::RemoveByIndex(index);
+ }
+
+ // Deleting collapsed thread header is special case. Child will be promoted,
+ // so just tell FE that line changed, not that it was deleted.
+ // Header has already been deleted from thread.
+ if (threadHdr && numThreadChildren > 0) {
+ // Change the id array and flags array to reflect the child header.
+ // If we're not deleting the header, we want the second header,
+ // Otherwise, the first one (which just got promoted).
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ msgHdr->GetMessageKey(&m_keys[index]);
+ uint32_t flag = 0;
+ msgHdr->GetFlags(&flag);
+ flag |= MSG_VIEW_FLAG_ISTHREAD;
+
+ // If only hdr in thread (with one about to be deleted).
+ if (numThreadChildren == 1) {
+ // Adjust flags.
+ flag &= ~MSG_VIEW_FLAG_HASCHILDREN;
+ flag &= ~nsMsgMessageFlags::Elided;
+ // Tell FE that thread header needs to be repainted.
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ } else {
+ flag |= MSG_VIEW_FLAG_HASCHILDREN;
+ flag |= nsMsgMessageFlags::Elided;
+ }
+
+ m_flags[index] = flag;
+ mIndicesToNoteChange.RemoveElement(index);
+ } else {
+ NS_ASSERTION(false, "couldn't find thread child");
+ }
+
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ } else {
+ // We may have deleted a whole, collapsed thread - if so,
+ // ensure that the current index will be noted as changed.
+ if (!mIndicesToNoteChange.Contains(index))
+ mIndicesToNoteChange.AppendElement(index);
+
+ rv = nsMsgDBView::RemoveByIndex(index);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowAllThreads;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgThreadedDBView* newMsgDBView = new nsMsgThreadedDBView();
+
+ if (!newMsgDBView) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.h b/comm/mailnews/base/src/nsMsgThreadedDBView.h
new file mode 100644
index 0000000000..a33c98a3bf
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgThreadedDBView.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgThreadedDBView_H_
+#define _nsMsgThreadedDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgGroupView.h"
+
+class nsMsgThreadedDBView : public nsMsgGroupView {
+ public:
+ nsMsgThreadedDBView();
+ virtual ~nsMsgThreadedDBView();
+
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) override;
+
+ protected:
+ virtual const char* GetViewName(void) override { return "ThreadedDBView"; }
+ nsresult InitThreadedView(int32_t& count);
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) override;
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed);
+ nsresult InitSort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ virtual void OnExtraFlagChanged(nsMsgViewIndex index,
+ uint32_t extraFlag) override;
+ virtual void OnHeaderAddedOrDeleted() override;
+ void ClearPrevIdArray();
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index) override;
+ nsMsgViewIndex GetInsertInfoForNewHdr(nsIMsgDBHdr* newHdr,
+ nsMsgViewIndex threadIndex,
+ int32_t targetLevel);
+ void MoveThreadAt(nsMsgViewIndex threadIndex);
+
+ // these are used to save off the previous view so that bopping back and forth
+ // between two views is quick (e.g., threaded and flat sorted by date).
+ bool m_havePrevView;
+ nsTArray<nsMsgKey> m_prevKeys; // this is used for caching non-threaded view.
+ nsTArray<uint32_t> m_prevFlags;
+ nsTArray<uint8_t> m_prevLevels;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgTxn.cpp b/comm/mailnews/base/src/nsMsgTxn.cpp
new file mode 100644
index 0000000000..0074fc1960
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTxn.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgTxn.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgDatabase.h"
+#include "nsCOMArray.h"
+#include "nsArrayEnumerator.h"
+#include "nsComponentManagerUtils.h"
+#include "nsVariant.h"
+#include "nsIProperty.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFolder.h"
+
+NS_IMPL_ADDREF(nsMsgTxn)
+NS_IMPL_RELEASE(nsMsgTxn)
+NS_INTERFACE_MAP_BEGIN(nsMsgTxn)
+ NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIPropertyBag, nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY(nsITransaction)
+ NS_INTERFACE_MAP_ENTRY(nsIPropertyBag2)
+ NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag2)
+NS_INTERFACE_MAP_END
+
+nsMsgTxn::nsMsgTxn() { m_txnType = 0; }
+
+nsMsgTxn::~nsMsgTxn() {}
+
+nsresult nsMsgTxn::Init() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgTxn::HasKey(const nsAString& name, bool* aResult) {
+ *aResult = mPropertyHash.Get(name, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::Get(const nsAString& name, nsIVariant** _retval) {
+ mPropertyHash.Get(name, _retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::GetProperty(const nsAString& name,
+ nsIVariant** _retval) {
+ return mPropertyHash.Get(name, _retval) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgTxn::SetProperty(const nsAString& name, nsIVariant* value) {
+ NS_ENSURE_ARG_POINTER(value);
+ mPropertyHash.InsertOrUpdate(name, value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::DeleteProperty(const nsAString& name) {
+ if (!mPropertyHash.Get(name, nullptr)) return NS_ERROR_FAILURE;
+
+ mPropertyHash.Remove(name);
+ return mPropertyHash.Get(name, nullptr) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+//
+// nsMailSimpleProperty class and impl; used for GetEnumerator
+// This is same as nsSimpleProperty but for external API use.
+//
+
+class nsMailSimpleProperty final : public nsIProperty {
+ public:
+ nsMailSimpleProperty(const nsAString& aName, nsIVariant* aValue)
+ : mName(aName), mValue(aValue) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROPERTY
+ protected:
+ ~nsMailSimpleProperty() {}
+
+ nsString mName;
+ nsCOMPtr<nsIVariant> mValue;
+};
+
+NS_IMPL_ISUPPORTS(nsMailSimpleProperty, nsIProperty)
+
+NS_IMETHODIMP nsMailSimpleProperty::GetName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailSimpleProperty::GetValue(nsIVariant** aValue) {
+ NS_IF_ADDREF(*aValue = mValue);
+ return NS_OK;
+}
+
+// end nsMailSimpleProperty
+
+NS_IMETHODIMP nsMsgTxn::GetEnumerator(nsISimpleEnumerator** _retval) {
+ nsCOMArray<nsIProperty> propertyArray;
+ for (auto iter = mPropertyHash.Iter(); !iter.Done(); iter.Next()) {
+ nsMailSimpleProperty* sprop =
+ new nsMailSimpleProperty(iter.Key(), iter.Data());
+ propertyArray.AppendObject(sprop);
+ }
+ return NS_NewArrayEnumerator(_retval, propertyArray, NS_GET_IID(nsIProperty));
+}
+
+#define IMPL_GETSETPROPERTY_AS(Name, Type) \
+ NS_IMETHODIMP \
+ nsMsgTxn::GetPropertyAs##Name(const nsAString& prop, Type* _retval) { \
+ nsIVariant* v = mPropertyHash.GetWeak(prop); \
+ if (!v) return NS_ERROR_NOT_AVAILABLE; \
+ return v->GetAs##Name(_retval); \
+ } \
+ \
+ NS_IMETHODIMP \
+ nsMsgTxn::SetPropertyAs##Name(const nsAString& prop, Type value) { \
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant(); \
+ var->SetAs##Name(value); \
+ return SetProperty(prop, var); \
+ }
+
+IMPL_GETSETPROPERTY_AS(Int32, int32_t)
+IMPL_GETSETPROPERTY_AS(Uint32, uint32_t)
+IMPL_GETSETPROPERTY_AS(Int64, int64_t)
+IMPL_GETSETPROPERTY_AS(Uint64, uint64_t)
+IMPL_GETSETPROPERTY_AS(Double, double)
+IMPL_GETSETPROPERTY_AS(Bool, bool)
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsAString(const nsAString& prop,
+ nsAString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsAString(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsACString(const nsAString& prop,
+ nsACString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsACString(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsAUTF8String(const nsAString& prop,
+ nsACString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsAUTF8String(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsInterface(const nsAString& prop,
+ const nsIID& aIID,
+ void** _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ nsCOMPtr<nsISupports> val;
+ nsresult rv = v->GetAsISupports(getter_AddRefs(val));
+ if (NS_FAILED(rv)) return rv;
+ if (!val) {
+ // We have a value, but it's null
+ *_retval = nullptr;
+ return NS_OK;
+ }
+ return val->QueryInterface(aIID, _retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsAString(const nsAString& prop,
+ const nsAString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsAString(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsACString(const nsAString& prop,
+ const nsACString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsACString(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsAUTF8String(const nsAString& prop,
+ const nsACString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsAUTF8String(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsInterface(const nsAString& prop,
+ nsISupports* value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsISupports(value);
+ return SetProperty(prop, var);
+}
+
+/////////////////////// Transaction Stuff //////////////////
+NS_IMETHODIMP nsMsgTxn::DoTransaction(void) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgTxn::GetIsTransient(bool* aIsTransient) {
+ if (nullptr != aIsTransient)
+ *aIsTransient = false;
+ else
+ return NS_ERROR_NULL_POINTER;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::Merge(nsITransaction* aTransaction, bool* aDidMerge) {
+ *aDidMerge = false;
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::GetMsgWindow(nsIMsgWindow** msgWindow) {
+ if (!msgWindow || !m_msgWindow) return NS_ERROR_NULL_POINTER;
+ NS_ADDREF(*msgWindow = m_msgWindow);
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::SetMsgWindow(nsIMsgWindow* msgWindow) {
+ m_msgWindow = msgWindow;
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::SetTransactionType(uint32_t txnType) {
+ return SetPropertyAsUint32(u"type"_ns, txnType);
+}
+
+/*none of the callers pass null aFolder,
+ we always initialize aResult (before we pass in) for the case where the key is
+ not in the db*/
+nsresult nsMsgTxn::CheckForToggleDelete(nsIMsgFolder* aFolder,
+ const nsMsgKey& aMsgKey,
+ bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ nsCOMPtr<nsIMsgDBHdr> message;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = aFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ bool containsKey;
+ rv = db->ContainsKey(aMsgKey, &containsKey);
+ if (NS_FAILED(rv) || !containsKey) // the message has been deleted from db,
+ // so we cannot do toggle here
+ return NS_OK;
+ rv = db->GetMsgHdrForKey(aMsgKey, getter_AddRefs(message));
+ uint32_t flags;
+ if (NS_SUCCEEDED(rv) && message) {
+ message->GetFlags(&flags);
+ *aResult = (flags & nsMsgMessageFlags::IMAPDeleted) != 0;
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgTxn.h b/comm/mailnews/base/src/nsMsgTxn.h
new file mode 100644
index 0000000000..824b26993b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTxn.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsMsgTxn_h__
+#define nsMsgTxn_h__
+
+#include "mozilla/Attributes.h"
+#include "nsITransaction.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgWindow.h"
+#include "nsInterfaceHashtable.h"
+#include "MailNewsTypes2.h"
+#include "nsIVariant.h"
+#include "nsIWritablePropertyBag.h"
+#include "nsIWritablePropertyBag2.h"
+
+#include "mozilla/EditTransactionBase.h"
+
+using mozilla::EditTransactionBase;
+
+#define NS_MESSAGETRANSACTION_IID \
+ { /* da621b30-1efc-11d3-abe4-00805f8ac968 */ \
+ 0xda621b30, 0x1efc, 0x11d3, { \
+ 0xab, 0xe4, 0x00, 0x80, 0x5f, 0x8a, 0xc9, 0x68 \
+ } \
+ }
+/**
+ * base class for all message undo/redo transactions.
+ */
+
+class nsMsgTxn : public nsITransaction,
+ public nsIWritablePropertyBag,
+ public nsIWritablePropertyBag2 {
+ public:
+ nsMsgTxn();
+
+ nsresult Init();
+
+ NS_IMETHOD DoTransaction(void) override;
+
+ NS_IMETHOD UndoTransaction(void) override = 0;
+
+ NS_IMETHOD RedoTransaction(void) override = 0;
+
+ NS_IMETHOD GetIsTransient(bool* aIsTransient) override;
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ nsresult GetMsgWindow(nsIMsgWindow** msgWindow);
+ nsresult SetMsgWindow(nsIMsgWindow* msgWindow);
+ nsresult SetTransactionType(uint32_t txnType);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPROPERTYBAG
+ NS_DECL_NSIPROPERTYBAG2
+ NS_DECL_NSIWRITABLEPROPERTYBAG
+ NS_DECL_NSIWRITABLEPROPERTYBAG2
+
+ NS_IMETHOD GetAsEditTransactionBase(EditTransactionBase**) final {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ protected:
+ virtual ~nsMsgTxn();
+
+ // a hash table of string -> nsIVariant
+ nsInterfaceHashtable<nsStringHashKey, nsIVariant> mPropertyHash;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ uint32_t m_txnType;
+ nsresult CheckForToggleDelete(nsIMsgFolder* aFolder, const nsMsgKey& aMsgKey,
+ bool* aResult);
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgUtils.cpp b/comm/mailnews/base/src/nsMsgUtils.cpp
new file mode 100644
index 0000000000..661b4fb219
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgUtils.cpp
@@ -0,0 +1,1926 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgUtils.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsString.h"
+#include "nsIServiceManager.h"
+#include "nsCOMPtr.h"
+#include "nsIFolderLookupService.h"
+#include "nsIImapUrl.h"
+#include "nsIMailboxUrl.h"
+#include "nsINntpUrl.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsCharTraits.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsNetCID.h"
+#include "nsIIOService.h"
+#include "nsIMimeConverter.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIRelativeFilePref.h"
+#include "mozilla/nsRelativeFilePref.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsISpamSettings.h"
+#include "nsICryptoHash.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIRssIncomingServer.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIOutputStream.h"
+#include "nsMsgFileStream.h"
+#include "nsIFileURL.h"
+#include "nsNetUtil.h"
+#include "nsProtocolProxyService.h"
+#include "nsIProtocolProxyCallback.h"
+#include "nsICancelable.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgWindow.h"
+#include "nsIWindowWatcher.h"
+#include "nsIPrompt.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsTextFormatter.h"
+#include "nsIStreamListener.h"
+#include "nsReadLine.h"
+#include "nsILineInputStream.h"
+#include "nsIParserUtils.h"
+#include "nsICharsetConverterManager.h"
+#include "nsIDocumentEncoder.h"
+#include "mozilla/Components.h"
+#include "locale.h"
+#include "nsStringStream.h"
+#include "nsIInputStreamPump.h"
+#include "nsIInputStream.h"
+#include "nsIChannel.h"
+#include "nsIURIMutator.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/EncodingDetector.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/Buffer.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+/* for logging to Error Console */
+#include "nsIScriptError.h"
+#include "nsIConsoleService.h"
+
+// Log an error string to the error console
+// (adapted from nsContentUtils::LogSimpleConsoleError).
+// Flag can indicate error, warning or info.
+NS_MSG_BASE void MsgLogToConsole4(const nsAString& aErrorText,
+ const nsAString& aFilename,
+ uint32_t aLinenumber, uint32_t aFlag) {
+ nsCOMPtr<nsIScriptError> scriptError =
+ do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
+ if (NS_WARN_IF(!scriptError)) return;
+ nsCOMPtr<nsIConsoleService> console =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (NS_WARN_IF(!console)) return;
+ if (NS_FAILED(scriptError->Init(aErrorText, aFilename, EmptyString(),
+ aLinenumber, 0, aFlag, "mailnews"_ns, false,
+ false)))
+ return;
+ console->LogMessage(scriptError);
+ return;
+}
+
+using namespace mozilla;
+using namespace mozilla::net;
+
+#define ILLEGAL_FOLDER_CHARS ";#"
+#define ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER "."
+#define ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER ".~ "
+
+nsresult GetMessageServiceContractIDForURI(const char* uri,
+ nsCString& contractID) {
+ nsresult rv = NS_OK;
+ // Find protocol
+ nsAutoCString uriStr(uri);
+ int32_t pos = uriStr.FindChar(':');
+ if (pos == -1) return NS_ERROR_FAILURE;
+
+ nsAutoCString protocol(StringHead(uriStr, pos));
+
+ if (protocol.EqualsLiteral("file")) {
+ protocol.AssignLiteral("mailbox");
+ }
+ // Build message service contractid
+ contractID = "@mozilla.org/messenger/messageservice;1?type=";
+ contractID += protocol.get();
+
+ return rv;
+}
+
+// Note: This function is also implemented in JS, see MailServices.jsm.
+nsresult GetMessageServiceFromURI(const nsACString& uri,
+ nsIMsgMessageService** aMessageService) {
+ nsresult rv;
+
+ nsAutoCString contractID;
+ rv = GetMessageServiceContractIDForURI(PromiseFlatCString(uri).get(),
+ contractID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgService.forget(aMessageService);
+ return rv;
+}
+
+nsresult GetMsgDBHdrFromURI(const nsACString& uri, nsIMsgDBHdr** msgHdr) {
+ nsCOMPtr<nsIMsgMessageService> msgMessageService;
+ nsresult rv =
+ GetMessageServiceFromURI(uri, getter_AddRefs(msgMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!msgMessageService) return NS_ERROR_FAILURE;
+
+ return msgMessageService->MessageURIToMsgHdr(uri, msgHdr);
+}
+
+// Where should this live? It's a utility used to convert a string priority,
+// e.g., "High, Low, Normal" to an enum.
+// Perhaps we should have an interface that groups together all these
+// utilities...
+nsresult NS_MsgGetPriorityFromString(const char* const priority,
+ nsMsgPriorityValue& outPriority) {
+ if (!priority) return NS_ERROR_NULL_POINTER;
+
+ // Note: Checking the values separately and _before_ the names,
+ // hoping for a much faster match;
+ // Only _drawback_, as "priority" handling is not truly specified:
+ // some software may have the number meanings reversed (1=Lowest) !?
+ if (PL_strchr(priority, '1'))
+ outPriority = nsMsgPriority::highest;
+ else if (PL_strchr(priority, '2'))
+ outPriority = nsMsgPriority::high;
+ else if (PL_strchr(priority, '3'))
+ outPriority = nsMsgPriority::normal;
+ else if (PL_strchr(priority, '4'))
+ outPriority = nsMsgPriority::low;
+ else if (PL_strchr(priority, '5'))
+ outPriority = nsMsgPriority::lowest;
+ else if (PL_strcasestr(priority, "Highest"))
+ outPriority = nsMsgPriority::highest;
+ // Important: "High" must be tested after "Highest" !
+ else if (PL_strcasestr(priority, "High") || PL_strcasestr(priority, "Urgent"))
+ outPriority = nsMsgPriority::high;
+ else if (PL_strcasestr(priority, "Normal"))
+ outPriority = nsMsgPriority::normal;
+ else if (PL_strcasestr(priority, "Lowest"))
+ outPriority = nsMsgPriority::lowest;
+ // Important: "Low" must be tested after "Lowest" !
+ else if (PL_strcasestr(priority, "Low") ||
+ PL_strcasestr(priority, "Non-urgent"))
+ outPriority = nsMsgPriority::low;
+ else
+ // "Default" case gets default value.
+ outPriority = nsMsgPriority::Default;
+
+ return NS_OK;
+}
+
+nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p,
+ nsACString& outValueString) {
+ switch (p) {
+ case nsMsgPriority::highest:
+ outValueString.Assign('1');
+ break;
+ case nsMsgPriority::high:
+ outValueString.Assign('2');
+ break;
+ case nsMsgPriority::normal:
+ outValueString.Assign('3');
+ break;
+ case nsMsgPriority::low:
+ outValueString.Assign('4');
+ break;
+ case nsMsgPriority::lowest:
+ outValueString.Assign('5');
+ break;
+ case nsMsgPriority::none:
+ case nsMsgPriority::notSet:
+ // Note: '0' is a "fake" value; we expect to never be in this case.
+ outValueString.Assign('0');
+ break;
+ default:
+ NS_ASSERTION(false, "invalid priority value");
+ }
+
+ return NS_OK;
+}
+
+nsresult NS_MsgGetUntranslatedPriorityName(const nsMsgPriorityValue p,
+ nsACString& outName) {
+ switch (p) {
+ case nsMsgPriority::highest:
+ outName.AssignLiteral("Highest");
+ break;
+ case nsMsgPriority::high:
+ outName.AssignLiteral("High");
+ break;
+ case nsMsgPriority::normal:
+ outName.AssignLiteral("Normal");
+ break;
+ case nsMsgPriority::low:
+ outName.AssignLiteral("Low");
+ break;
+ case nsMsgPriority::lowest:
+ outName.AssignLiteral("Lowest");
+ break;
+ case nsMsgPriority::none:
+ case nsMsgPriority::notSet:
+ // Note: 'None' is a "fake" value; we expect to never be in this case.
+ outName.AssignLiteral("None");
+ break;
+ default:
+ NS_ASSERTION(false, "invalid priority value");
+ }
+
+ return NS_OK;
+}
+
+/* this used to be XP_StringHash2 from xp_hash.c */
+/* phong's linear congruential hash */
+static uint32_t StringHash(const char* ubuf, int32_t len = -1) {
+ unsigned char* buf = (unsigned char*)ubuf;
+ uint32_t h = 1;
+ unsigned char* end = buf + (len == -1 ? strlen(ubuf) : len);
+ while (buf < end) {
+ h = 0x63c63cd9 * h + 0x9c39c33d + (int32_t)*buf;
+ buf++;
+ }
+ return h;
+}
+
+inline uint32_t StringHash(const nsAutoString& str) {
+ const char16_t* strbuf = str.get();
+ return StringHash(reinterpret_cast<const char*>(strbuf), str.Length() * 2);
+}
+
+/* Utility functions used in a few places in mailnews */
+int32_t MsgFindCharInSet(const nsCString& aString, const char* aChars,
+ uint32_t aOffset) {
+ return aString.FindCharInSet(aChars, aOffset);
+}
+
+int32_t MsgFindCharInSet(const nsString& aString, const char16_t* aChars,
+ uint32_t aOffset) {
+ return aString.FindCharInSet(aChars, aOffset);
+}
+
+static bool ConvertibleToNative(const nsAutoString& str) {
+ nsAutoCString native;
+ nsAutoString roundTripped;
+ NS_CopyUnicodeToNative(str, native);
+ NS_CopyNativeToUnicode(native, roundTripped);
+ return str.Equals(roundTripped);
+}
+
+#if defined(XP_UNIX)
+const static uint32_t MAX_LEN = 55;
+#elif defined(XP_WIN)
+const static uint32_t MAX_LEN = 55;
+#else
+# error need_to_define_your_max_filename_length
+#endif
+
+nsresult NS_MsgHashIfNecessary(nsAutoCString& name) {
+ if (name.IsEmpty()) return NS_OK; // Nothing to do.
+ nsAutoCString str(name);
+
+ // Given a filename, make it safe for filesystem
+ // certain filenames require hashing because they
+ // are too long or contain illegal characters
+ int32_t illegalCharacterIndex = MsgFindCharInSet(
+ str, FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0);
+
+ // Need to check the first ('.') and last ('.', '~' and ' ') char
+ if (illegalCharacterIndex == -1) {
+ int32_t lastIndex = str.Length() - 1;
+ if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER)
+ .FindChar(str[0]) != -1)
+ illegalCharacterIndex = 0;
+ else if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER)
+ .FindChar(str[lastIndex]) != -1)
+ illegalCharacterIndex = lastIndex;
+ else
+ illegalCharacterIndex = -1;
+ }
+
+ char hashedname[MAX_LEN + 1];
+ if (illegalCharacterIndex == -1) {
+ // no illegal chars, it's just too long
+ // keep the initial part of the string, but hash to make it fit
+ if (str.Length() > MAX_LEN) {
+ PL_strncpy(hashedname, str.get(), MAX_LEN + 1);
+ PR_snprintf(hashedname + MAX_LEN - 8, 9, "%08lx",
+ (unsigned long)StringHash(str.get()));
+ name = hashedname;
+ }
+ } else {
+ // found illegal chars, hash the whole thing
+ // if we do substitution, then hash, two strings
+ // could hash to the same value.
+ // for example, on mac: "foo__bar", "foo:_bar", "foo::bar"
+ // would map to "foo_bar". this way, all three will map to
+ // different values
+ PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(str.get()));
+ name = hashedname;
+ }
+
+ return NS_OK;
+}
+
+// XXX : The number of UTF-16 2byte code units are half the number of
+// bytes in legacy encodings for CJK strings and non-Latin1 in UTF-8.
+// The ratio can be 1/3 for CJK strings in UTF-8. However, we can
+// get away with using the same MAX_LEN for nsCString and nsString
+// because MAX_LEN is defined rather conservatively in the first place.
+nsresult NS_MsgHashIfNecessary(nsAutoString& name) {
+ if (name.IsEmpty()) return NS_OK; // Nothing to do.
+ int32_t illegalCharacterIndex = MsgFindCharInSet(
+ name,
+ u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0);
+
+ // Need to check the first ('.') and last ('.', '~' and ' ') char
+ if (illegalCharacterIndex == -1) {
+ int32_t lastIndex = name.Length() - 1;
+ if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER)
+ .FindChar(name[0]) != -1)
+ illegalCharacterIndex = 0;
+ else if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER)
+ .FindChar(name[lastIndex]) != -1)
+ illegalCharacterIndex = lastIndex;
+ else
+ illegalCharacterIndex = -1;
+ }
+
+ char hashedname[9];
+ int32_t keptLength = -1;
+ if (illegalCharacterIndex != -1)
+ keptLength = illegalCharacterIndex;
+ else if (!ConvertibleToNative(name))
+ keptLength = 0;
+ else if (name.Length() > MAX_LEN) {
+ keptLength = MAX_LEN - 8;
+ // To avoid keeping only the high surrogate of a surrogate pair
+ if (NS_IS_HIGH_SURROGATE(name.CharAt(keptLength - 1))) --keptLength;
+ }
+
+ if (keptLength >= 0) {
+ PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(name));
+ name.SetLength(keptLength);
+ name.Append(NS_ConvertASCIItoUTF16(hashedname));
+ }
+
+ return NS_OK;
+}
+
+nsresult FormatFileSize(int64_t size, bool useKB, nsAString& formattedSize) {
+ const char* sizeAbbrNames[] = {
+ "byteAbbreviation2", "kiloByteAbbreviation2", "megaByteAbbreviation2",
+ "gigaByteAbbreviation2", "teraByteAbbreviation2", "petaByteAbbreviation2",
+ };
+
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ double unitSize = size < 0 ? 0.0 : size;
+ uint32_t unitIndex = 0;
+
+ if (useKB) {
+ // Start by formatting in kilobytes
+ unitSize /= 1024;
+ if (unitSize < 0.1 && unitSize != 0) unitSize = 0.1;
+ unitIndex++;
+ }
+
+ // Convert to next unit if it needs 4 digits (after rounding), but only if
+ // we know the name of the next unit
+ while ((unitSize >= 999.5) && (unitIndex < ArrayLength(sizeAbbrNames) - 1)) {
+ unitSize /= 1024;
+ unitIndex++;
+ }
+
+ // Grab the string for the appropriate unit
+ nsString sizeAbbr;
+ rv = bundle->GetStringFromName(sizeAbbrNames[unitIndex], sizeAbbr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get rid of insignificant bits by truncating to 1 or 0 decimal points
+ // 0.1 -> 0.1; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
+ nsTextFormatter::ssprintf(
+ formattedSize, sizeAbbr.get(),
+ (unitIndex != 0) && (unitSize < 99.95 && unitSize != 0) ? 1 : 0,
+ unitSize);
+
+ int32_t separatorPos = formattedSize.FindChar('.');
+ if (separatorPos != kNotFound) {
+ // The ssprintf returned a decimal number using a dot (.) as the decimal
+ // separator. Now we try to localize the separator.
+ // Try to get the decimal separator from the system's locale.
+ char* decimalPoint;
+#ifdef HAVE_LOCALECONV
+ struct lconv* locale = localeconv();
+ decimalPoint = locale->decimal_point;
+#else
+ decimalPoint = getenv("LOCALE_DECIMAL_POINT");
+#endif
+ NS_ConvertUTF8toUTF16 decimalSeparator(decimalPoint);
+ if (decimalSeparator.IsEmpty()) decimalSeparator.Assign('.');
+
+ formattedSize.Replace(separatorPos, 1, decimalSeparator);
+ }
+
+ return NS_OK;
+}
+
+nsresult NS_MsgCreatePathStringFromFolderURI(const char* aFolderURI,
+ nsCString& aPathCString,
+ const nsCString& aScheme,
+ bool aIsNewsFolder) {
+ // A file name has to be in native charset. Here we convert
+ // to UTF-16 and check for 'unsafe' characters before converting
+ // to native charset.
+ NS_ENSURE_TRUE(mozilla::IsUtf8(nsDependentCString(aFolderURI)),
+ NS_ERROR_UNEXPECTED);
+ NS_ConvertUTF8toUTF16 oldPath(aFolderURI);
+
+ nsAutoString pathPiece, path;
+
+ int32_t startSlashPos = oldPath.FindChar('/');
+ int32_t endSlashPos = (startSlashPos >= 0)
+ ? oldPath.FindChar('/', startSlashPos + 1) - 1
+ : oldPath.Length() - 1;
+ if (endSlashPos < 0) endSlashPos = oldPath.Length();
+#if defined(XP_UNIX) || defined(XP_MACOSX)
+ bool isLocalUri = aScheme.EqualsLiteral("none") ||
+ aScheme.EqualsLiteral("pop3") ||
+ aScheme.EqualsLiteral("rss");
+#endif
+ // trick to make sure we only add the path to the first n-1 folders
+ bool haveFirst = false;
+ while (startSlashPos != -1) {
+ pathPiece.Assign(
+ Substring(oldPath, startSlashPos + 1, endSlashPos - startSlashPos));
+ // skip leading '/' (and other // style things)
+ if (!pathPiece.IsEmpty()) {
+ // add .sbd onto the previous path
+ if (haveFirst) {
+ path.AppendLiteral(FOLDER_SUFFIX "/");
+ }
+
+ if (aIsNewsFolder) {
+ nsAutoCString tmp;
+ CopyUTF16toMUTF7(pathPiece, tmp);
+ CopyASCIItoUTF16(tmp, pathPiece);
+ }
+#if defined(XP_UNIX) || defined(XP_MACOSX)
+ // Don't hash path pieces because local mail folder uri's have already
+ // been hashed. We're only doing this on the mac to limit potential
+ // regressions.
+ if (!isLocalUri)
+#endif
+ NS_MsgHashIfNecessary(pathPiece);
+ path += pathPiece;
+ haveFirst = true;
+ }
+ // look for the next slash
+ startSlashPos = endSlashPos + 1;
+
+ endSlashPos = (startSlashPos >= 0)
+ ? oldPath.FindChar('/', startSlashPos + 1) - 1
+ : oldPath.Length() - 1;
+ if (endSlashPos < 0) endSlashPos = oldPath.Length();
+
+ if (startSlashPos >= endSlashPos) break;
+ }
+ return NS_CopyUnicodeToNative(path, aPathCString);
+}
+
+bool NS_MsgStripRE(const nsCString& subject, nsCString& modifiedSubject) {
+ bool result = false;
+
+ // Get localizedRe pref.
+ nsresult rv;
+ nsString utf16LocalizedRe;
+ NS_GetLocalizedUnicharPreferenceWithDefault(nullptr, "mailnews.localizedRe",
+ EmptyString(), utf16LocalizedRe);
+ NS_ConvertUTF16toUTF8 localizedRe(utf16LocalizedRe);
+
+ // Hardcoded "Re" so that no one can configure Mozilla standards incompatible.
+ nsAutoCString checkString("Re,RE,re,rE");
+ if (!localizedRe.IsEmpty()) {
+ checkString.Append(',');
+ checkString.Append(localizedRe);
+ }
+
+ // Decode the string.
+ nsCString decodedString;
+ nsCOMPtr<nsIMimeConverter> mimeConverter;
+ // We cannot strip "Re:" for RFC2047-encoded subject without modifying the
+ // original.
+ if (subject.Find("=?") != kNotFound) {
+ mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = mimeConverter->DecodeMimeHeaderToUTF8(subject, nullptr, false, true,
+ decodedString);
+ }
+
+ const char *s, *s_end;
+ if (decodedString.IsEmpty()) {
+ s = subject.BeginReading();
+ s_end = s + subject.Length();
+ } else {
+ s = decodedString.BeginReading();
+ s_end = s + decodedString.Length();
+ }
+
+AGAIN:
+ while (s < s_end && IS_SPACE(*s)) s++;
+
+ const char* tokPtr = checkString.get();
+ while (*tokPtr) {
+ // Tokenize the comma separated list.
+ size_t tokenLength = 0;
+ while (*tokPtr && *tokPtr != ',') {
+ tokenLength++;
+ tokPtr++;
+ }
+ // Check if the beginning of s is the actual token.
+ if (tokenLength && !strncmp(s, tokPtr - tokenLength, tokenLength)) {
+ if (s[tokenLength] == ':') {
+ s = s + tokenLength + 1; /* Skip over "Re:" */
+ result = true; /* Yes, we stripped it. */
+ goto AGAIN; /* Skip whitespace and try again. */
+ } else if (s[tokenLength] == '[' || s[tokenLength] == '(') {
+ const char* s2 = s + tokenLength + 1; /* Skip over "Re[" */
+
+ // Skip forward over digits after the "[".
+ while (s2 < (s_end - 2) && isdigit((unsigned char)*s2)) s2++;
+
+ // Now ensure that the following thing is "]:".
+ // Only if it is do we alter `s`.
+ if ((s2[0] == ']' || s2[0] == ')') && s2[1] == ':') {
+ s = s2 + 2; /* Skip over "]:" */
+ result = true; /* Yes, we stripped it. */
+ goto AGAIN; /* Skip whitespace and try again. */
+ }
+ }
+ }
+ if (*tokPtr) tokPtr++;
+ }
+
+ // If we didn't strip anything, we can return here.
+ if (!result) return false;
+
+ if (decodedString.IsEmpty()) {
+ // We didn't decode anything, so just return a new string.
+ modifiedSubject.Assign(s);
+ return true;
+ }
+
+ // We decoded the string, so we need to encode it again. We always encode in
+ // UTF-8.
+ mimeConverter->EncodeMimePartIIStr_UTF8(
+ nsDependentCString(s), false, sizeof("Subject:"),
+ nsIMimeConverter::MIME_ENCODED_WORD_SIZE, modifiedSubject);
+ return true;
+}
+
+/* Very similar to strdup except it free's too
+ */
+char* NS_MsgSACopy(char** destination, const char* source) {
+ if (*destination) {
+ PR_Free(*destination);
+ *destination = 0;
+ }
+ if (!source)
+ *destination = nullptr;
+ else {
+ *destination = (char*)PR_Malloc(PL_strlen(source) + 1);
+ if (*destination == nullptr) return (nullptr);
+
+ PL_strcpy(*destination, source);
+ }
+ return *destination;
+}
+
+/* Again like strdup but it concatenates and free's and uses Realloc.
+ */
+char* NS_MsgSACat(char** destination, const char* source) {
+ if (source && *source) {
+ int destLength = *destination ? PL_strlen(*destination) : 0;
+ char* newDestination =
+ (char*)PR_Realloc(*destination, destLength + PL_strlen(source) + 1);
+ if (newDestination == nullptr) return nullptr;
+
+ *destination = newDestination;
+ PL_strcpy(*destination + destLength, source);
+ }
+ return *destination;
+}
+
+nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr, nsCString& aResult) {
+ return MsgEscapeString(NS_ConvertUTF16toUTF8(aStr),
+ nsINetUtil::ESCAPE_URL_PATH, aResult);
+}
+
+nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath,
+ nsAString& aResult) {
+ nsAutoCString unescapedName;
+ MsgUnescapeString(
+ aPath,
+ nsINetUtil::ESCAPE_URL_FILE_BASENAME | nsINetUtil::ESCAPE_URL_FORCED,
+ unescapedName);
+ CopyUTF8toUTF16(unescapedName, aResult);
+ return NS_OK;
+}
+
+bool WeAreOffline() {
+ bool offline = false;
+
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ if (ioService) ioService->GetOffline(&offline);
+
+ return offline;
+}
+
+// Find a folder by URL. If it doesn't exist, null will be returned
+// via aFolder.
+nsresult FindFolder(const nsACString& aFolderURI, nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ *aFolder = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // GetFolderForURL returns NS_OK and null for non-existent folders
+ rv = fls->GetFolderForURL(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Fetch an existing folder by URL
+// The returned aFolder will be non-null if and only if result is NS_OK.
+// NS_OK - folder was found
+// NS_MSG_FOLDER_MISSING - if aFolderURI not found
+nsresult GetExistingFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder) {
+ nsresult rv = FindFolder(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return *aFolder ? NS_OK : NS_MSG_ERROR_FOLDER_MISSING;
+}
+
+nsresult GetOrCreateFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ *aFolder = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = fls->GetOrCreateFolderForURL(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return *aFolder ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool IsAFromSpaceLine(char* start, const char* end) {
+ bool rv = false;
+ while ((start < end) && (*start == '>')) start++;
+ // If the leading '>'s are followed by an 'F' then we have a possible case
+ // here.
+ if ((*start == 'F') && (end - start > 4) && !strncmp(start, "From ", 5))
+ rv = true;
+ return rv;
+}
+
+//
+// This function finds all lines starting with "From " or "From " preceding
+// with one or more '>' (ie, ">From", ">>From", etc) in the input buffer
+// (between 'start' and 'end') and prefix them with a ">" .
+//
+nsresult EscapeFromSpaceLine(nsIOutputStream* outputStream, char* start,
+ const char* end) {
+ nsresult rv;
+ char* pChar;
+ uint32_t written;
+
+ pChar = start;
+ while (start < end) {
+ while ((pChar < end) && (*pChar != '\r') && ((pChar + 1) < end) &&
+ (*(pChar + 1) != '\n'))
+ pChar++;
+ if ((pChar + 1) == end) pChar++;
+
+ if (pChar < end) {
+ // Found a line so check if it's a qualified "From " line.
+ if (IsAFromSpaceLine(start, pChar)) {
+ rv = outputStream->Write(">", 1, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ int32_t lineTerminatorCount = (*(pChar + 1) == '\n') ? 2 : 1;
+ rv = outputStream->Write(start, pChar - start + lineTerminatorCount,
+ &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pChar += lineTerminatorCount;
+ start = pChar;
+ } else if (start < end) {
+ // Check and flush out the remaining data and we're done.
+ if (IsAFromSpaceLine(start, end)) {
+ rv = outputStream->Write(">", 1, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = outputStream->Write(start, end - start, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+ uint32_t length = strlen(aHdr);
+ for (uint32_t i = 0; i < length; i++) {
+ char c = aHdr[i];
+ if (c < '!' || c == ':' || c > '~') {
+ *aResult = false;
+ return NS_OK;
+ }
+ }
+ *aResult = true;
+ return NS_OK;
+}
+
+// Warning, currently this routine only works for the Junk Folder
+nsresult GetOrCreateJunkFolder(const nsACString& aURI,
+ nsIUrlListener* aListener) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(aURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // don't check validity of folder - caller will handle creating it
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // make sure that folder hierarchy is built so that legitimate parent-child
+ // relationship is established
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!server) return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = server->GetMsgFolderFromURI(folder, aURI, getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = msgFolder->GetParent(getter_AddRefs(parent));
+ if (NS_FAILED(rv) || !parent) {
+ nsCOMPtr<nsIFile> folderPath;
+ // for local folders, path is to the berkeley mailbox.
+ // for imap folders, path needs to have .msf appended to the name
+ msgFolder->GetFilePath(getter_AddRefs(folderPath));
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = server->GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isAsyncFolder;
+ rv = protocolInfo->GetFoldersCreatedAsync(&isAsyncFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we can't get the path from the folder, then try to create the storage.
+ // for imap, it doesn't matter if the .msf file exists - it still might not
+ // exist on the server, so we should try to create it
+ bool exists = false;
+ if (!isAsyncFolder && folderPath) folderPath->Exists(&exists);
+ if (!exists) {
+ // Hack to work around a localization bug with the Junk Folder.
+ // Please see Bug #270261 for more information...
+ nsString localizedJunkName;
+ msgFolder->GetName(localizedJunkName);
+
+ // force the junk folder name to be Junk so it gets created on disk
+ // correctly...
+ msgFolder->SetName(u"Junk"_ns);
+ msgFolder->SetFlag(nsMsgFolderFlags::Junk);
+ rv = msgFolder->CreateStorageIfMissing(aListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now restore the localized folder name...
+ msgFolder->SetName(localizedJunkName);
+
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // ugh, I hate this hack
+ // we have to do this (for now)
+ // because imap and local are different (one creates folder asynch, the
+ // other synch) one will notify the listener, one will not. I blame
+ // nsMsgCopy. we should look into making it so no matter what the folder
+ // type we always call the listener this code should move into local
+ // folder's version of CreateStorageIfMissing()
+ if (!isAsyncFolder && aListener) {
+ rv = aListener->OnStartRunningUrl(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aListener->OnStopRunningUrl(nullptr, NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ } else {
+ // if the folder exists, we should set the junk flag on it
+ // which is what the listener will do
+ if (aListener) {
+ rv = aListener->OnStartRunningUrl(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aListener->OnStopRunningUrl(nullptr, NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle) {
+ nsresult rv;
+ *aIsRSSArticle = false;
+
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aMsgURI, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCString resourceURI;
+ msgUrl->GetUri(resourceURI);
+
+ // get the msg service for this URI
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ rv = GetMessageServiceFromURI(resourceURI, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if the message is a feed message, regardless of folder.
+ uint32_t flags;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgService->MessageURIToMsgHdr(resourceURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::FeedMsg) {
+ *aIsRSSArticle = true;
+ return rv;
+ }
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aMsgURI, &rv);
+ mozilla::Unused << mailnewsUrl;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the folder and the server from the msghdr
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ folder->GetServer(getter_AddRefs(server));
+ nsCOMPtr<nsIRssIncomingServer> rssServer = do_QueryInterface(server);
+
+ if (rssServer) *aIsRSSArticle = true;
+ }
+
+ return rv;
+}
+
+// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer
+nsresult MSGCramMD5(const char* text, int32_t text_len, const char* key,
+ int32_t key_len, unsigned char* digest) {
+ nsresult rv;
+
+ nsAutoCString hash;
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this code adapted from
+ // http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc2104.html
+
+ char innerPad[65]; /* inner padding - key XORd with innerPad */
+ char outerPad[65]; /* outer padding - key XORd with outerPad */
+ int i;
+ /* if key is longer than 64 bytes reset it to key=MD5(key) */
+ if (key_len > 64) {
+ rv = hasher->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)key, key_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Finish(false, hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ key = hash.get();
+ key_len = DIGEST_LENGTH;
+ }
+
+ /*
+ * the HMAC_MD5 transform looks like:
+ *
+ * MD5(K XOR outerPad, MD5(K XOR innerPad, text))
+ *
+ * where K is an n byte key
+ * innerPad is the byte 0x36 repeated 64 times
+ * outerPad is the byte 0x5c repeated 64 times
+ * and text is the data being protected
+ */
+
+ /* start out by storing key in pads */
+ memset(innerPad, 0, sizeof innerPad);
+ memset(outerPad, 0, sizeof outerPad);
+ memcpy(innerPad, key, key_len);
+ memcpy(outerPad, key, key_len);
+
+ /* XOR key with innerPad and outerPad values */
+ for (i = 0; i < 64; i++) {
+ innerPad[i] ^= 0x36;
+ outerPad[i] ^= 0x5c;
+ }
+ /*
+ * perform inner MD5
+ */
+ nsAutoCString result;
+ rv = hasher->Init(nsICryptoHash::MD5); /* init context for 1st pass */
+ rv = hasher->Update((const uint8_t*)innerPad, 64); /* start with inner pad */
+ rv = hasher->Update((const uint8_t*)text,
+ text_len); /* then text of datagram */
+ rv = hasher->Finish(false, result); /* finish up 1st pass */
+
+ /*
+ * perform outer MD5
+ */
+ hasher->Init(nsICryptoHash::MD5); /* init context for 2nd pass */
+ rv = hasher->Update((const uint8_t*)outerPad, 64); /* start with outer pad */
+ rv = hasher->Update((const uint8_t*)result.get(),
+ 16); /* then results of 1st hash */
+ rv = hasher->Finish(false, result); /* finish up 2nd pass */
+
+ if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED;
+
+ memcpy(digest, result.get(), DIGEST_LENGTH);
+
+ return rv;
+}
+
+// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer
+nsresult MSGApopMD5(const char* text, int32_t text_len, const char* password,
+ int32_t password_len, unsigned char* digest) {
+ nsresult rv;
+ nsAutoCString result;
+
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)text, text_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)password, password_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Finish(false, result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED;
+
+ memcpy(digest, result.get(), DIGEST_LENGTH);
+ return rv;
+}
+
+NS_MSG_BASE nsresult NS_GetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ const char* dirServiceProp,
+ bool& gotRelPref, nsIFile** aFile,
+ nsIPrefBranch* prefBranch) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ *aFile = nullptr;
+ NS_ENSURE_ARG(relPrefName);
+ NS_ENSURE_ARG(absPrefName);
+ gotRelPref = false;
+
+ nsCOMPtr<nsIPrefBranch> mainBranch;
+ if (!prefBranch) {
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!prefService) return NS_ERROR_FAILURE;
+ prefService->GetBranch(nullptr, getter_AddRefs(mainBranch));
+ if (!mainBranch) return NS_ERROR_FAILURE;
+ prefBranch = mainBranch;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+
+ // Get the relative first
+ nsCOMPtr<nsIRelativeFilePref> relFilePref;
+ prefBranch->GetComplexValue(relPrefName, NS_GET_IID(nsIRelativeFilePref),
+ getter_AddRefs(relFilePref));
+ if (relFilePref) {
+ relFilePref->GetFile(getter_AddRefs(localFile));
+ NS_ASSERTION(localFile, "An nsIRelativeFilePref has no file.");
+ if (localFile) gotRelPref = true;
+ }
+
+ // If not, get the old absolute
+ if (!localFile) {
+ prefBranch->GetComplexValue(absPrefName, NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+
+ // If not, and given a dirServiceProp, use directory service.
+ if (!localFile && dirServiceProp) {
+ nsCOMPtr<nsIProperties> dirService(
+ do_GetService("@mozilla.org/file/directory_service;1"));
+ if (!dirService) return NS_ERROR_FAILURE;
+ dirService->Get(dirServiceProp, NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+ if (!localFile) return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (localFile) {
+ localFile->Normalize();
+ localFile.forget(aFile);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ nsIFile* aFile,
+ nsIPrefBranch* prefBranch) {
+ NS_ENSURE_ARG(relPrefName);
+ NS_ENSURE_ARG(absPrefName);
+ NS_ENSURE_ARG(aFile);
+
+ nsCOMPtr<nsIPrefBranch> mainBranch;
+ if (!prefBranch) {
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!prefService) return NS_ERROR_FAILURE;
+ prefService->GetBranch(nullptr, getter_AddRefs(mainBranch));
+ if (!mainBranch) return NS_ERROR_FAILURE;
+ prefBranch = mainBranch;
+ }
+
+ // Write the absolute for backwards compatibilty's sake.
+ // Or, if aPath is on a different drive than the profile dir.
+ nsresult rv =
+ prefBranch->SetComplexValue(absPrefName, NS_GET_IID(nsIFile), aFile);
+
+ // Write the relative path.
+ nsCOMPtr<nsIRelativeFilePref> relFilePref = new nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(aFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ nsresult rv2 = prefBranch->SetComplexValue(
+ relPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ if (NS_FAILED(rv2) && NS_SUCCEEDED(rv))
+ prefBranch->ClearUserPref(relPrefName);
+
+ return rv;
+}
+
+NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue) {
+ NS_ENSURE_ARG(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCString valueUtf8;
+ nsresult rv =
+ prefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8);
+ if (NS_SUCCEEDED(rv))
+ CopyUTF8toUTF16(valueUtf8, prefValue);
+ else
+ prefValue = defValue;
+ return NS_OK;
+}
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue) {
+ NS_ENSURE_ARG(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCOMPtr<nsIPrefLocalizedString> str;
+ nsresult rv = prefBranch->GetComplexValue(
+ prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str));
+ if (NS_SUCCEEDED(rv)) {
+ nsString tmpValue;
+ str->ToString(getter_Copies(tmpValue));
+ prefValue.Assign(tmpValue);
+ } else
+ prefValue = defValue;
+ return NS_OK;
+}
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, nsAString& prefValue) {
+ NS_ENSURE_ARG_POINTER(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCOMPtr<nsIPrefLocalizedString> str;
+ nsresult rv = prefBranch->GetComplexValue(
+ prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString tmpValue;
+ str->ToString(getter_Copies(tmpValue));
+ prefValue.Assign(tmpValue);
+ return NS_OK;
+}
+
+void PRTime2Seconds(PRTime prTime, uint32_t* seconds) {
+ *seconds = (uint32_t)(prTime / PR_USEC_PER_SEC);
+}
+
+void PRTime2Seconds(PRTime prTime, int32_t* seconds) {
+ *seconds = (int32_t)(prTime / PR_USEC_PER_SEC);
+}
+
+void Seconds2PRTime(uint32_t seconds, PRTime* prTime) {
+ *prTime = (PRTime)seconds * PR_USEC_PER_SEC;
+}
+
+nsresult GetSummaryFileLocation(nsIFile* fileLocation,
+ nsIFile** summaryLocation) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> newSummaryLocation =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation->InitWithFile(fileLocation);
+ nsString fileName;
+
+ rv = newSummaryLocation->GetLeafName(fileName);
+ if (NS_FAILED(rv)) return rv;
+
+ fileName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = newSummaryLocation->SetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation.forget(summaryLocation);
+ return NS_OK;
+}
+
+void MsgGenerateNowStr(nsACString& nowStr) {
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
+ &exploded);
+ nowStr.Assign(dateBuf);
+}
+
+// Gets a special directory and appends the supplied file name onto it.
+nsresult GetSpecialDirectoryWithFileName(const char* specialDirName,
+ const char* fileName,
+ nsIFile** result) {
+ nsresult rv = NS_GetSpecialDirectory(specialDirName, result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return (*result)->AppendNative(nsDependentCString(fileName));
+}
+
+// Cleans up temp files with matching names
+nsresult MsgCleanupTempFiles(const char* fileName, const char* extension) {
+ nsCOMPtr<nsIFile> tmpFile;
+ nsCString rootName(fileName);
+ rootName.Append('.');
+ rootName.Append(extension);
+ nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, rootName.get(),
+ getter_AddRefs(tmpFile));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ int index = 1;
+ bool exists;
+ do {
+ tmpFile->Exists(&exists);
+ if (exists) {
+ tmpFile->Remove(false);
+ nsCString leafName(fileName);
+ leafName.Append('-');
+ leafName.AppendInt(index);
+ leafName.Append('.');
+ leafName.Append(extension);
+ // start with "Picture-1.jpg" after "Picture.jpg" exists
+ tmpFile->SetNativeLeafName(leafName);
+ }
+ } while (exists && index++ < 10000);
+ return NS_OK;
+}
+
+nsresult MsgGetFileStream(nsIFile* file, nsIOutputStream** fileStream) {
+ RefPtr<nsMsgFileStream> newFileStream = new nsMsgFileStream;
+ nsresult rv = newFileStream->InitWithFile(file);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newFileStream.forget(fileStream);
+ return NS_OK;
+}
+
+nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile, int32_t aIOFlags,
+ int32_t aPerm) {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile,
+ aIOFlags, aPerm);
+ if (NS_SUCCEEDED(rv))
+ rv = NS_NewBufferedOutputStream(aResult, stream.forget(),
+ FILE_IO_BUFFER_SIZE);
+ return rv;
+}
+
+nsresult MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile, int32_t aIOFlags,
+ int32_t aPerm) {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), aFile,
+ aIOFlags, aPerm);
+ if (NS_SUCCEEDED(rv))
+ rv = NS_NewBufferedOutputStream(aResult, stream.forget(),
+ FILE_IO_BUFFER_SIZE);
+ return rv;
+}
+
+bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords,
+ int32_t* aStartOfKeyword, int32_t* aLength) {
+// nsTString_CharT::Find(const nsCString& aString,
+// bool aIgnoreCase=false,
+// int32_t aOffset=0,
+// int32_t aCount=-1 ) const;
+#define FIND_KEYWORD(keywords, keyword, offset) \
+ ((keywords).Find((keyword), (offset)))
+ // 'keyword' is the single keyword we're looking for
+ // 'keywords' is a space delimited list of keywords to be searched,
+ // which may be just a single keyword or even be empty
+ const int32_t kKeywordLen = keyword.Length();
+ const char* start = keywords.BeginReading();
+ const char* end = keywords.EndReading();
+ *aStartOfKeyword = FIND_KEYWORD(keywords, keyword, 0);
+ while (*aStartOfKeyword >= 0) {
+ const char* matchStart = start + *aStartOfKeyword;
+ const char* matchEnd = matchStart + kKeywordLen;
+ // For a real match, matchStart must be the start of keywords or preceded
+ // by a space and matchEnd must be the end of keywords or point to a space.
+ if ((matchStart == start || *(matchStart - 1) == ' ') &&
+ (matchEnd == end || *matchEnd == ' ')) {
+ *aLength = kKeywordLen;
+ return true;
+ }
+ *aStartOfKeyword =
+ FIND_KEYWORD(keywords, keyword, *aStartOfKeyword + kKeywordLen);
+ }
+
+ *aLength = 0;
+ return false;
+#undef FIND_KEYWORD
+}
+
+bool MsgHostDomainIsTrusted(nsCString& host, nsCString& trustedMailDomains) {
+ const char* end;
+ uint32_t hostLen, domainLen;
+ bool domainIsTrusted = false;
+
+ const char* domain = trustedMailDomains.BeginReading();
+ const char* domainEnd = trustedMailDomains.EndReading();
+ const char* hostStart = host.BeginReading();
+ hostLen = host.Length();
+
+ do {
+ // skip any whitespace
+ while (*domain == ' ' || *domain == '\t') ++domain;
+
+ // find end of this domain in the string
+ end = strchr(domain, ',');
+ if (!end) end = domainEnd;
+
+ // to see if the hostname is in the domain, check if the domain
+ // matches the end of the hostname.
+ domainLen = end - domain;
+ if (domainLen && hostLen >= domainLen) {
+ const char* hostTail = hostStart + hostLen - domainLen;
+ if (PL_strncasecmp(domain, hostTail, domainLen) == 0) {
+ // now, make sure either that the hostname is a direct match or
+ // that the hostname begins with a dot.
+ if (hostLen == domainLen || *hostTail == '.' ||
+ *(hostTail - 1) == '.') {
+ domainIsTrusted = true;
+ break;
+ }
+ }
+ }
+
+ domain = end + 1;
+ } while (*end);
+ return domainIsTrusted;
+}
+
+nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path, nsIFile** aFile) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> argURI;
+ rv = NS_NewURI(getter_AddRefs(argURI), aUTF8Path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> argFileURL(do_QueryInterface(argURI, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> argFile;
+ rv = argFileURL->GetFile(getter_AddRefs(argFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ argFile.forget(aFile);
+ return NS_OK;
+}
+
+NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc) {
+ // decode quoted printable text in place
+
+ if (aSrc.IsEmpty()) return;
+
+ char* src = aSrc.BeginWriting();
+ char* dest = src;
+ int srcIdx = 0, destIdx = 0;
+
+ while (src[srcIdx] != 0) {
+ // Decode sequence of '=XY' into a character with code XY.
+ if (src[srcIdx] == '=') {
+ if (MsgIsHex((const char*)src + srcIdx + 1, 2)) {
+ // If we got here, we successfully decoded a quoted printable sequence,
+ // so bump each pointer past it and move on to the next char.
+ dest[destIdx++] = MsgUnhex((const char*)src + srcIdx + 1, 2);
+ srcIdx += 3;
+ } else {
+ // If first char after '=' isn't hex check if it's a normal char
+ // or a soft line break. If it's a soft line break, eat the
+ // CR/LF/CRLF.
+ if (src[srcIdx + 1] == '\r' || src[srcIdx + 1] == '\n') {
+ srcIdx++; // soft line break, ignore the '=';
+ if (src[srcIdx] == '\r' || src[srcIdx] == '\n') {
+ srcIdx++;
+ if (src[srcIdx] == '\n') srcIdx++;
+ }
+ } else // The first or second char after '=' isn't hex, just copy the
+ // '='.
+ {
+ dest[destIdx++] = src[srcIdx++];
+ }
+ continue;
+ }
+ } else
+ dest[destIdx++] = src[srcIdx++];
+ }
+
+ dest[destIdx] = src[srcIdx]; // null terminate
+ aSrc.SetLength(destIdx);
+}
+
+NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->EscapeString(aStr, aType, aResult);
+}
+
+NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->UnescapeString(aStr, aFlags, aResult);
+}
+
+NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->EscapeURL(aStr, aFlags, aResult);
+}
+
+NS_MSG_BASE nsresult
+MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aMsgKeys,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders) {
+ NS_ENSURE_ARG_POINTER(aDB);
+ aHeaders.Clear();
+ aHeaders.SetCapacity(aMsgKeys.Length());
+
+ for (auto key : aMsgKeys) {
+ // This function silently skips when the key is not found. This is an
+ // expected case.
+ bool hasKey;
+ nsresult rv = aDB->ContainsKey(key, &hasKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasKey) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = aDB->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ aHeaders.AppendElement(msgHdr);
+ }
+ }
+ return NS_OK;
+}
+
+bool MsgAdvanceToNextLine(const char* buffer, uint32_t& bufferOffset,
+ uint32_t maxBufferOffset) {
+ bool result = false;
+ for (; bufferOffset < maxBufferOffset; bufferOffset++) {
+ if (buffer[bufferOffset] == '\r' || buffer[bufferOffset] == '\n') {
+ bufferOffset++;
+ if (buffer[bufferOffset - 1] == '\r' && buffer[bufferOffset] == '\n')
+ bufferOffset++;
+ result = true;
+ break;
+ }
+ }
+ return result;
+}
+
+NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel,
+ nsIProtocolProxyCallback* listener,
+ nsICancelable** result) {
+ nsresult rv;
+
+#ifdef DEBUG
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && uri,
+ "The URI needs to be set before calling the proxy service");
+#endif
+
+ nsCOMPtr<nsIProtocolProxyService> pps =
+ do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return pps->AsyncResolve(channel, 0, listener, nullptr, result);
+}
+
+NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow,
+ const nsACString& aHostname,
+ const nsACString& aUsername,
+ const nsAString& aAccountname,
+ int32_t* aResult) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ if (aMsgWindow) {
+ aMsgWindow->GetDomWindow(getter_AddRefs(domWindow));
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString message;
+ AutoTArray<nsString, 2> formatStrings;
+ CopyUTF8toUTF16(aHostname, *formatStrings.AppendElement());
+ CopyUTF8toUTF16(aUsername, *formatStrings.AppendElement());
+
+ rv = bundle->FormatStringFromName("mailServerLoginFailed2", formatStrings,
+ message);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString title;
+ if (aAccountname.IsEmpty()) {
+ // Account name may be empty e.g. on a SMTP server.
+ rv = bundle->GetStringFromName("mailServerLoginFailedTitle", title);
+ } else {
+ AutoTArray<nsString, 1> formatStrings = {nsString(aAccountname)};
+ rv = bundle->FormatStringFromName("mailServerLoginFailedTitleWithAccount",
+ formatStrings, title);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString button0;
+ rv = bundle->GetStringFromName("mailServerLoginFailedRetryButton", button0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString button2;
+ rv = bundle->GetStringFromName("mailServerLoginFailedEnterNewPasswordButton",
+ button2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool dummyValue = false;
+ return dlgService->ConfirmEx(
+ domWindow, title.get(), message.get(),
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1) +
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2),
+ button0.get(), nullptr, button2.get(), nullptr, &dummyValue, aResult);
+}
+
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays) {
+ PRTime now = PR_Now();
+
+ return now - PR_USEC_PER_DAY * ageInDays;
+}
+
+NS_MSG_BASE nsresult
+MsgTermListToString(nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList,
+ nsCString& aOutString) {
+ nsresult rv = NS_OK;
+ for (nsIMsgSearchTerm* term : aTermList) {
+ nsAutoCString stream;
+
+ if (aOutString.Length() > 1) aOutString += ' ';
+
+ bool booleanAnd;
+ bool matchAll;
+ term->GetBooleanAnd(&booleanAnd);
+ term->GetMatchAll(&matchAll);
+ if (matchAll) {
+ aOutString += "ALL";
+ continue;
+ } else if (booleanAnd)
+ aOutString += "AND (";
+ else
+ aOutString += "OR (";
+
+ rv = term->GetTermAsString(stream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aOutString += stream;
+ aOutString += ')';
+ }
+ return rv;
+}
+
+NS_MSG_BASE uint64_t ParseUint64Str(const char* str) {
+#ifdef XP_WIN
+ {
+ char* endPtr;
+ return _strtoui64(str, &endPtr, 10);
+ }
+#else
+ return strtoull(str, nullptr, 10);
+#endif
+}
+
+NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars) {
+ // Large numbers will not fit into uint64_t.
+ NS_ASSERTION(aNumChars <= 16, "Hex literal too long to convert!");
+
+ uint64_t result = 0;
+ for (size_t i = 0; i < aNumChars; i++) {
+ unsigned char c = aHexString[i];
+ uint8_t digit;
+ if ((c >= '0') && (c <= '9'))
+ digit = (c - '0');
+ else if ((c >= 'a') && (c <= 'f'))
+ digit = ((c - 'a') + 10);
+ else if ((c >= 'A') && (c <= 'F'))
+ digit = ((c - 'A') + 10);
+ else
+ break;
+
+ result = (result << 4) | digit;
+ }
+
+ return result;
+}
+
+NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars) {
+ for (size_t i = 0; i < aNumChars; i++) {
+ if (!isxdigit(aHexString[i])) return false;
+ }
+ return true;
+}
+
+NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream,
+ nsIStreamListener* aConsumer) {
+ mozilla::UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>);
+
+ nsresult rv;
+
+ nsAutoCString msgHeaders;
+ nsAutoCString curLine;
+
+ bool more = true;
+
+ // We want to NS_ReadLine until we get to a blank line (the end of the
+ // headers)
+ while (more) {
+ rv = NS_ReadLine(aInputStream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (curLine.IsEmpty()) break;
+ msgHeaders.Append(curLine);
+ msgHeaders.AppendLiteral("\r\n");
+ }
+ lineBuffer.reset();
+ nsCOMPtr<nsIStringInputStream> hdrsStream =
+ do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdrsStream->SetData(msgHeaders.get(), msgHeaders.Length());
+
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), hdrsStream.forget());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return pump->AsyncRead(aConsumer);
+}
+
+NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile,
+ nsACString& aCharset) {
+ // We do the detection in this order:
+ // Check BOM.
+ // If no BOM, run localized detection (Russian, Ukrainian or Japanese).
+ // We need to run this first, since ISO-2022-JP is 7bit ASCII and would be
+ // detected as UTF-8. If ISO-2022-JP not detected, check for UTF-8. If no
+ // UTF-8, but detector detected something, use that, otherwise return an
+ // error.
+ aCharset.Truncate();
+
+ nsresult rv;
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check the BOM.
+ char sniffBuf[3];
+ uint32_t numRead;
+ rv = inputStream->Read(sniffBuf, sizeof(sniffBuf), &numRead);
+
+ if (numRead >= 2 && sniffBuf[0] == (char)0xfe && sniffBuf[1] == (char)0xff) {
+ aCharset = "UTF-16BE";
+ } else if (numRead >= 2 && sniffBuf[0] == (char)0xff &&
+ sniffBuf[1] == (char)0xfe) {
+ aCharset = "UTF-16LE";
+ } else if (numRead >= 3 && sniffBuf[0] == (char)0xef &&
+ sniffBuf[1] == (char)0xbb && sniffBuf[2] == (char)0xbf) {
+ aCharset = "UTF-8";
+ }
+ if (!aCharset.IsEmpty()) return NS_OK;
+
+ // Position back to the beginning.
+ nsCOMPtr<nsISeekableStream> seekStream = do_QueryInterface(inputStream);
+ if (seekStream) seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+
+ // Use detector.
+ mozilla::UniquePtr<mozilla::EncodingDetector> detector =
+ mozilla::EncodingDetector::Create();
+ char buffer[1024];
+ numRead = 0;
+ while (NS_SUCCEEDED(inputStream->Read(buffer, sizeof(buffer), &numRead))) {
+ mozilla::Span<const uint8_t> src =
+ mozilla::AsBytes(mozilla::Span(buffer, numRead));
+ Unused << detector->Feed(src, false);
+ if (numRead == 0) {
+ break;
+ }
+ }
+ Unused << detector->Feed(nullptr, true);
+ auto encoding = detector->Guess(nullptr, true);
+ encoding->Name(aCharset);
+ return NS_OK;
+}
+
+/*
+ * Converts a buffer to plain text. Some conversions may
+ * or may not work with certain end charsets which is why we
+ * need that as an argument to the function. If charset is
+ * unknown or deemed of no importance NULL could be passed.
+ */
+NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed,
+ bool formatOutput,
+ bool disallowBreaks) {
+ if (aConBuf.IsEmpty()) return NS_OK;
+
+ int32_t wrapWidth = 72;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ if (pPrefBranch) {
+ pPrefBranch->GetIntPref("mailnews.wraplength", &wrapWidth);
+ // Let sanity reign!
+ if (wrapWidth == 0 || wrapWidth > 990)
+ wrapWidth = 990;
+ else if (wrapWidth < 10)
+ wrapWidth = 10;
+ }
+
+ uint32_t converterFlags = nsIDocumentEncoder::OutputPersistNBSP;
+ if (formatFlowed) converterFlags |= nsIDocumentEncoder::OutputFormatFlowed;
+ if (formatOutput) converterFlags |= nsIDocumentEncoder::OutputFormatted;
+ if (disallowBreaks)
+ converterFlags |= nsIDocumentEncoder::OutputDisallowLineBreaking;
+
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(aConBuf, converterFlags, wrapWidth, aConBuf);
+}
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue) { return aValue; }
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue) {
+ NS_ASSERTION(aValue <= PR_UINT32_MAX, "Msg key value too big!");
+ return aValue;
+}
+
+NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey) { return (uint32_t)aMsgKey; }
+
+// Helper function to extract a query qualifier.
+nsCString MsgExtractQueryPart(const nsACString& spec,
+ const char* queryToExtract) {
+ nsCString queryPart;
+ int32_t queryIndex = PromiseFlatCString(spec).Find(queryToExtract);
+ if (queryIndex == kNotFound) return queryPart;
+
+ int32_t queryEnd = spec.FindChar('&', queryIndex + 1);
+ if (queryEnd == kNotFound) queryEnd = spec.FindChar('?', queryIndex + 1);
+ if (queryEnd == kNotFound) {
+ // Nothing follows, so return from where the query qualifier started.
+ queryPart.Assign(Substring(spec, queryIndex));
+ } else {
+ // Return the substring that represents the query qualifier.
+ queryPart.Assign(Substring(spec, queryIndex, queryEnd - queryIndex));
+ }
+ return queryPart;
+}
+
+// Helper function to remove query part from URL spec or path.
+void MsgRemoveQueryPart(nsCString& aSpec) {
+ // Sadly the query part can have different forms, these were seen
+ // "in the wild", even with two ?:
+ // /;section=2?part=1.2&filename=A01.JPG
+ // ?section=2?part=1.2&filename=A01.JPG&type=image/jpeg&filename=A01.JPG
+ // ?header=quotebody/;section=2.2?part=1.2.2&filename=lijbmghmkilicioj.png
+ // ?part=1.2&type=image/jpeg&filename=IMG_C0030.jpg
+ // ?header=quotebody&part=1.2&filename=lijbmghmkilicioj.png
+
+ // Truncate path at the first of /; or ?
+ int32_t ind = aSpec.FindChar('?');
+ if (ind != kNotFound) aSpec.SetLength(ind);
+ ind = aSpec.Find("/;");
+ if (ind != kNotFound) aSpec.SetLength(ind);
+}
+
+// Perform C-style string escaping.
+// e.g. "foo\r\n" => "foo\\r\\n"
+// (See also CEscape(), in protobuf, for similar function).
+nsCString CEscapeString(nsACString const& s) {
+ nsCString out;
+ for (size_t i = 0; i < s.Length(); ++i) {
+ char c = s[i];
+ if (c & 0x80) {
+ out.AppendPrintf("\\x%02x", (uint8_t)c);
+ continue;
+ }
+ switch (c) {
+ case '\a':
+ out += "\\a";
+ break;
+ case '\b':
+ out += "\\b";
+ break;
+ case '\f':
+ out += "\\f";
+ break;
+ case '\n':
+ out += "\\n";
+ break;
+ case '\r':
+ out += "\\r";
+ break;
+ case '\t':
+ out += "\\t";
+ break;
+ case '\v':
+ out += "\\v";
+ break;
+ default:
+ if (c < ' ') {
+ out.AppendPrintf("\\x%02x", (uint8_t)c);
+ } else {
+ out += c;
+ }
+ break;
+ }
+ }
+ return out;
+}
+
+nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest,
+ uint64_t& bytesCopied, size_t bufSize) {
+ mozilla::Buffer<char> buf(bufSize);
+ nsresult rv;
+
+ bytesCopied = 0;
+ while (1) {
+ uint32_t numRead;
+ rv = src->Read(buf.Elements(), buf.Length(), &numRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (numRead == 0) {
+ break; // EOF.
+ }
+ uint32_t pos = 0;
+ while (pos < numRead) {
+ uint32_t n;
+ rv = dest->Write(&buf[pos], numRead - pos, &n);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pos += n;
+ bytesCopied += n;
+ }
+ }
+ return NS_OK;
+}
+
+// Used for "@mozilla.org/network/sync-stream-listener;1".
+already_AddRefed<nsIStreamListener> SyncStreamListenerCreate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIStreamListener> listener;
+ nsCOMPtr<nsIInputStream> stream;
+ nsresult rv = NS_NewSyncStreamListener(getter_AddRefs(listener),
+ getter_AddRefs(stream));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ return listener.forget();
+}
+
+// Determine if folder1 and folder2 reside on the same server
+nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2,
+ bool* sameServer) {
+ NS_ENSURE_ARG_POINTER(folder1);
+ NS_ENSURE_ARG_POINTER(folder2);
+ NS_ENSURE_ARG_POINTER(sameServer);
+
+ nsCOMPtr<nsIMsgIncomingServer> server1;
+ nsresult rv = folder1->GetServer(getter_AddRefs(server1));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsIMsgIncomingServer> server2;
+ rv = folder2->GetServer(getter_AddRefs(server2));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ NS_ENSURE_TRUE(server2, NS_ERROR_NULL_POINTER);
+ return server2->Equals(server1, sameServer);
+}
diff --git a/comm/mailnews/base/src/nsMsgUtils.h b/comm/mailnews/base/src/nsMsgUtils.h
new file mode 100644
index 0000000000..6f289d4b37
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgUtils.h
@@ -0,0 +1,462 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _NSMSGUTILS_H
+#define _NSMSGUTILS_H
+
+#include "nsIURL.h"
+#include "nsString.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes2.h"
+#include "nsTArray.h"
+#include "nsInterfaceRequestorAgg.h"
+#include "nsILoadGroup.h"
+#include "nsINetUtil.h"
+#include "nsIRequest.h"
+#include "nsILoadInfo.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIFile.h"
+
+class nsIChannel;
+class nsIFile;
+class nsIPrefBranch;
+class nsIMsgFolder;
+class nsIMsgMessageService;
+class nsIUrlListener;
+class nsIOutputStream;
+class nsIInputStream;
+class nsIMsgDatabase;
+class nsIProxyInfo;
+class nsIMsgWindow;
+class nsIStreamListener;
+class nsICancelable;
+class nsIProtocolProxyCallback;
+class nsIMsgSearchTerm;
+
+#define FILE_IO_BUFFER_SIZE (16 * 1024)
+#define MSGS_URL "chrome://messenger/locale/messenger.properties"
+
+enum nsDateFormatSelectorComm : long {
+ kDateFormatNone = 0,
+ kDateFormatLong = 1,
+ kDateFormatShort = 2,
+ kDateFormatUnused = 3,
+ kDateFormatWeekday = 4
+};
+
+// These are utility functions that can used throughout the mailnews code
+
+NS_MSG_BASE nsresult GetMessageServiceContractIDForURI(const char* uri,
+ nsCString& contractID);
+
+NS_MSG_BASE nsresult GetMessageServiceFromURI(
+ const nsACString& uri, nsIMsgMessageService** aMessageService);
+
+NS_MSG_BASE nsresult GetMsgDBHdrFromURI(const nsACString& uri,
+ nsIMsgDBHdr** msgHdr);
+
+NS_MSG_BASE nsresult NS_MsgGetPriorityFromString(
+ const char* const priority, nsMsgPriorityValue& outPriority);
+
+NS_MSG_BASE nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p,
+ nsACString& outValueString);
+
+NS_MSG_BASE nsresult NS_MsgGetUntranslatedPriorityName(
+ const nsMsgPriorityValue p, nsACString& outName);
+
+NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoString& name);
+NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoCString& name);
+
+NS_MSG_BASE nsresult FormatFileSize(int64_t size, bool useKB,
+ nsAString& formattedSize);
+
+/**
+ * given a folder uri, return the path to folder in the user profile directory.
+ *
+ * @param aFolderURI uri of folder we want the path to, without the scheme
+ * @param[out] aPathString result path string
+ * @param aScheme scheme of the uri
+ * @param[optional] aIsNewsFolder is this a news folder?
+ */
+NS_MSG_BASE nsresult NS_MsgCreatePathStringFromFolderURI(
+ const char* aFolderURI, nsCString& aPathString, const nsCString& aScheme,
+ bool aIsNewsFolder = false);
+
+/**
+ * Given a string and a length, removes any "Re:" strings from the front.
+ * It also deals with that dumbass "Re[2]:" thing that some losing mailers do.
+ *
+ * If mailnews.localizedRe is set, it will also remove localized "Re:" strings.
+ *
+ * @return true if it made a change (in which case the caller should look to
+ * modifiedSubject for the result) and false otherwise (in which
+ * case the caller should look at subject for the result)
+ */
+NS_MSG_BASE bool NS_MsgStripRE(const nsCString& subject,
+ nsCString& modifiedSubject);
+
+NS_MSG_BASE char* NS_MsgSACopy(char** destination, const char* source);
+
+NS_MSG_BASE char* NS_MsgSACat(char** destination, const char* source);
+
+NS_MSG_BASE nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr,
+ nsCString& aResult);
+
+NS_MSG_BASE nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath,
+ nsAString& aResult);
+
+NS_MSG_BASE bool WeAreOffline();
+
+// Get a folder by Uri, returning null if it doesn't exist (or if some
+// error occurs). A missing folder is not considered an error.
+NS_MSG_BASE nsresult FindFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Get a folder by Uri.
+// A missing folder is considered to be an error.
+// Returns a non-null folder if and only if result is NS_OK.
+NS_MSG_BASE nsresult GetExistingFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Get a folder by Uri, creating it if it doesn't already exist.
+// An error is returned if a folder cannot be found or created.
+// Created folders will be 'dangling' folders (ie not connected to a
+// parent).
+NS_MSG_BASE nsresult GetOrCreateFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Escape lines starting with "From ", ">From ", etc. in a buffer.
+NS_MSG_BASE nsresult EscapeFromSpaceLine(nsIOutputStream* ouputStream,
+ char* start, const char* end);
+NS_MSG_BASE bool IsAFromSpaceLine(char* start, const char* end);
+
+NS_MSG_BASE nsresult NS_GetPersistentFile(
+ const char* relPrefName, const char* absPrefName,
+ const char* dirServiceProp, // Can be NULL
+ bool& gotRelPref, nsIFile** aFile, nsIPrefBranch* prefBranch = nullptr);
+
+NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ nsIFile* aFile,
+ nsIPrefBranch* prefBranch = nullptr);
+
+NS_MSG_BASE nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult);
+
+NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue);
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue);
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, nsAString& prefValue);
+
+/**
+ * this needs a listener, because we might have to create the folder
+ * on the server, and that is asynchronous
+ */
+NS_MSG_BASE nsresult GetOrCreateJunkFolder(const nsACString& aURI,
+ nsIUrlListener* aListener);
+
+// Returns true if the nsIURI is a message under an RSS account
+NS_MSG_BASE nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle);
+
+// digest needs to be a pointer to a 16 byte buffer
+#define DIGEST_LENGTH 16
+
+NS_MSG_BASE nsresult MSGCramMD5(const char* text, int32_t text_len,
+ const char* key, int32_t key_len,
+ unsigned char* digest);
+NS_MSG_BASE nsresult MSGApopMD5(const char* text, int32_t text_len,
+ const char* password, int32_t password_len,
+ unsigned char* digest);
+
+// helper functions to convert a 64bits PRTime into a 32bits value (compatible
+// time_t) and vice versa.
+NS_MSG_BASE void PRTime2Seconds(PRTime prTime, uint32_t* seconds);
+NS_MSG_BASE void PRTime2Seconds(PRTime prTime, int32_t* seconds);
+NS_MSG_BASE void Seconds2PRTime(uint32_t seconds, PRTime* prTime);
+// helper function to generate current date+time as a string
+NS_MSG_BASE void MsgGenerateNowStr(nsACString& nowStr);
+
+// Appends the correct summary file extension onto the supplied fileLocation
+// and returns it in summaryLocation.
+NS_MSG_BASE nsresult GetSummaryFileLocation(nsIFile* fileLocation,
+ nsIFile** summaryLocation);
+
+// Gets a special directory and appends the supplied file name onto it.
+NS_MSG_BASE nsresult GetSpecialDirectoryWithFileName(const char* specialDirName,
+ const char* fileName,
+ nsIFile** result);
+
+// cleanup temp files with the given filename and extension, including
+// the consecutive -NNNN ones that we can find. If there are holes, e.g.,
+// <filename>-1-10,12.<extension> exist, but <filename>-11.<extension> does not
+// we'll clean up 1-10. If the leaks are common, I think the gaps will tend to
+// be filled.
+NS_MSG_BASE nsresult MsgCleanupTempFiles(const char* fileName,
+ const char* extension);
+
+NS_MSG_BASE nsresult MsgGetFileStream(nsIFile* file,
+ nsIOutputStream** fileStream);
+
+// Automatically creates an output stream with a suitable buffer
+NS_MSG_BASE nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile,
+ int32_t aIOFlags = -1,
+ int32_t aPerm = -1);
+
+// Automatically creates an output stream with a suitable buffer, but write to a
+// temporary file first, then rename to aFile
+NS_MSG_BASE nsresult
+MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult, nsIFile* aFile,
+ int32_t aIOFlags = -1, int32_t aPerm = -1);
+
+// fills in the position of the passed in keyword in the passed in keyword list
+// and returns false if the keyword isn't present
+NS_MSG_BASE bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords,
+ int32_t* aStartOfKeyword, int32_t* aLength);
+
+NS_MSG_BASE bool MsgHostDomainIsTrusted(nsCString& host,
+ nsCString& trustedMailDomains);
+
+// gets an nsIFile from a UTF-8 file:// path
+NS_MSG_BASE nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path,
+ nsIFile** aFile);
+
+NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc);
+
+/*
+ * Utility functions that call functions from nsINetUtil
+ */
+
+NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType,
+ nsACString& aResult);
+
+NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult);
+
+NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult);
+
+// Given a message db and a set of keys, fetch the corresponding message
+// headers.
+NS_MSG_BASE nsresult
+MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aKeys,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders);
+
+NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel,
+ nsIProtocolProxyCallback* listener,
+ nsICancelable** result);
+
+NS_MSG_BASE int32_t MsgFindCharInSet(const nsCString& aString,
+ const char* aChars, uint32_t aOffset = 0);
+NS_MSG_BASE int32_t MsgFindCharInSet(const nsString& aString,
+ const char16_t* aChars,
+ uint32_t aOffset = 0);
+
+// advances bufferOffset to the beginning of the next line, if we don't
+// get to maxBufferOffset first. Returns false if we didn't get to the
+// next line.
+NS_MSG_BASE bool MsgAdvanceToNextLine(const char* buffer,
+ uint32_t& bufferOffset,
+ uint32_t maxBufferOffset);
+
+/**
+ * Alerts the user that the login to the server failed. Asks whether the
+ * connection should: retry, cancel, or request a new password.
+ *
+ * @param aMsgWindow The message window associated with this action (cannot
+ * be null).
+ * @param aHostname The hostname of the server for which the login failed.
+ * @param aResult The button pressed. 0 for retry, 1 for cancel,
+ * 2 for enter a new password.
+ * @return NS_OK for success, NS_ERROR_* if there was a failure in
+ * creating the dialog.
+ */
+NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow,
+ const nsACString& aHostname,
+ const nsACString& aUsername,
+ const nsAString& aAccountname,
+ int32_t* aResult);
+
+/**
+ * Calculate a PRTime value used to determine if a date is XX
+ * days ago. This is used by various retention setting algorithms.
+ */
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays);
+
+/**
+ * Converts the passed in term list to its string representation.
+ *
+ * @param aTermList Array of nsIMsgSearchTerms
+ * @param[out] aOutString result representation of search terms.
+ *
+ */
+NS_MSG_BASE nsresult MsgTermListToString(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList, nsCString& aOutString);
+
+NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream,
+ nsIStreamListener* aConsumer);
+
+/**
+ * convert string to uint64_t
+ *
+ * @param str converted string
+ * @returns uint64_t value for success, 0 for parse failure
+ */
+NS_MSG_BASE uint64_t ParseUint64Str(const char* str);
+
+/**
+ * Detect charset of file
+ *
+ * @param aFile The target of nsIFile
+ * @param[out] aCharset The charset string
+ */
+NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile,
+ nsACString& aCharset);
+
+/*
+ * Converts a buffer to plain text. Some conversions may
+ * or may not work with certain end charsets which is why we
+ * need that as an argument to the function. If charset is
+ * unknown or deemed of no importance NULL could be passed.
+ * @param[in/out] aConBuf Variable with the text to convert
+ * @param formatFlowed Use format flowed?
+ * @param formatOutput Reformat the output?
+ & @param disallowBreaks Disallow breaks when formatting
+ */
+NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed,
+ bool formatOutput,
+ bool disallowBreaks);
+
+#include "nsEscape.h"
+
+/**
+ * Converts a hex string into an integer.
+ * Processes up to aNumChars characters or the first non-hex char.
+ * It is not an error if less than aNumChars valid hex digits are found.
+ */
+NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars);
+
+/**
+ * Checks if a string is a valid hex literal containing at least aNumChars
+ * digits.
+ */
+NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars);
+
+/**
+ * Convert an uint32_t to a nsMsgKey.
+ * Currently they are mostly the same but we need to preserve the notion that
+ * nsMsgKey is an opaque value that can't be treated as a generic integer
+ * (except when storing it into the database). It enables type safety checks and
+ * may prevent coding errors.
+ */
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue);
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue);
+
+NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey);
+
+/**
+ * Helper function to extract query part from URL spec.
+ */
+nsCString MsgExtractQueryPart(const nsACString& spec,
+ const char* queryToExtract);
+/**
+ * Helper function to remove query part from URL spec or path.
+ */
+void MsgRemoveQueryPart(nsCString& aSpec);
+
+/**
+ * Helper macro for defining getter/setters. Ported from nsISupportsObsolete.h
+ */
+#define NS_IMPL_GETSET(clazz, attr, type, member) \
+ NS_IMETHODIMP clazz::Get##attr(type* result) { \
+ NS_ENSURE_ARG_POINTER(result); \
+ *result = member; \
+ return NS_OK; \
+ } \
+ NS_IMETHODIMP clazz::Set##attr(type aValue) { \
+ member = aValue; \
+ return NS_OK; \
+ }
+
+/**
+ * Macro and helper function for reporting an error, warning or
+ * informational message to the Error Console
+ *
+ * This will require the inclusion of the following files in the source file
+ * #include "nsIScriptError.h"
+ * #include "nsIConsoleService.h"
+ *
+ */
+
+NS_MSG_BASE
+void MsgLogToConsole4(const nsAString& aErrorText, const nsAString& aFilename,
+ uint32_t aLine, uint32_t flags);
+
+// Macro with filename and line number
+#define MSG_LOG_TO_CONSOLE(_text, _flag) \
+ MsgLogToConsole4(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, _flag)
+#define MSG_LOG_ERR_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::errorFlag)
+#define MSG_LOG_WARN_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::warningFlag)
+#define MSG_LOG_INFO_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::infoFlag)
+
+// Helper macros to cope with shoddy I/O error reporting (or lack thereof)
+#define MSG_NS_ERROR(_txt) \
+ do { \
+ NS_ERROR(_txt); \
+ MSG_LOG_ERR_TO_CONSOLE(_txt); \
+ } while (0)
+#define MSG_NS_WARNING(_txt) \
+ do { \
+ NS_WARNING(_txt); \
+ MSG_LOG_WARN_TO_CONSOLE(_txt); \
+ } while (0)
+#define MSG_NS_WARN_IF_FALSE(_val, _txt) \
+ do { \
+ if (!(_val)) { \
+ NS_WARNING(_txt); \
+ MSG_LOG_WARN_TO_CONSOLE(_txt); \
+ } \
+ } while (0)
+#define MSG_NS_INFO(_txt) \
+ do { \
+ MSG_LOCAL_INFO_TO_CONSOLE(_txt); \
+ fprintf(stderr, "(info) %s (%s:%d)\n", _txt, __FILE__, __LINE__); \
+ } while (0)
+
+/**
+ * Perform C-style string escaping. E.g. "foo\r\n" => "foo\\r\\n"
+ * This is primarily intended for debuggin purposes.
+ */
+nsCString CEscapeString(nsACString const& s);
+
+/**
+ * Synchronously copy the contents of src to dest, until EOF is encountered
+ * or an error occurs.
+ * The total number of bytes copied is returned in bytesCopied.
+ */
+nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest,
+ uint64_t& bytesCopied,
+ size_t bufSize = FILE_IO_BUFFER_SIZE);
+
+// Used for "@mozilla.org/network/sync-stream-listener;1".
+already_AddRefed<nsIStreamListener> SyncStreamListenerCreate();
+
+nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2,
+ bool* sameServer);
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgWindow.cpp b/comm/mailnews/base/src/nsMsgWindow.cpp
new file mode 100644
index 0000000000..1b0129d8f8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgWindow.cpp
@@ -0,0 +1,327 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgWindow.h"
+#include "nsIURILoader.h"
+#include "nsCURILoader.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "mozIDOMWindow.h"
+#include "nsTransactionManagerCID.h"
+#include "nsIComponentManager.h"
+#include "nsILoadGroup.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsPIDOMWindow.h"
+#include "nsIPrompt.h"
+#include "nsICharsetConverterManager.h"
+#include "nsIChannel.h"
+#include "nsIRequestObserver.h"
+#include "netCore.h"
+#include "prmem.h"
+#include "plbase64.h"
+#include "nsMsgI18N.h"
+#include "nsIWebNavigation.h"
+#include "nsContentUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIAuthPrompt.h"
+#include "nsMsgUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/TransactionManager.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "nsFrameLoader.h"
+
+NS_IMPL_ISUPPORTS(nsMsgWindow, nsIMsgWindow, nsIURIContentListener,
+ nsISupportsWeakReference)
+
+nsMsgWindow::nsMsgWindow() {
+ mCharsetOverride = false;
+ m_stopped = false;
+}
+
+nsMsgWindow::~nsMsgWindow() { CloseWindow(); }
+
+nsresult nsMsgWindow::Init() {
+ // create Undo/Redo Transaction Manager
+ mTransactionManager = new mozilla::TransactionManager();
+ return mTransactionManager->SetMaxTransactionCount(-1);
+}
+
+NS_IMETHODIMP nsMsgWindow::GetMessageWindowDocShell(nsIDocShell** aDocShell) {
+ *aDocShell = nullptr;
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryReferent(mMessageWindowDocShellWeak));
+ nsCOMPtr<nsIDocShell> rootShell(do_QueryReferent(mRootDocShellWeak));
+ if (rootShell) {
+ // There seem to be some issues with shutdown (see Bug 1610406).
+ // This workaround should prevent the GetElementById() call dying horribly
+ // but really, we shouldn't even get here in such cases.
+ bool doomed;
+ rootShell->IsBeingDestroyed(&doomed);
+ if (doomed) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ RefPtr<mozilla::dom::Element> el =
+ rootShell->GetDocument()->GetElementById(u"messagepane"_ns);
+ RefPtr<mozilla::dom::XULFrameElement> frame =
+ mozilla::dom::XULFrameElement::FromNodeOrNull(el);
+ NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+ RefPtr<mozilla::dom::Document> doc = frame->GetContentDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+ docShell = doc->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ // we don't own mMessageWindowDocShell so don't try to keep a reference to
+ // it!
+ mMessageWindowDocShellWeak = do_GetWeakReference(docShell);
+ }
+
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ docShell.forget(aDocShell);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::CloseWindow() {
+ mStatusFeedback = nullptr;
+
+ StopUrls();
+
+ nsCOMPtr<nsIDocShell> messagePaneDocShell(
+ do_QueryReferent(mMessageWindowDocShellWeak));
+ if (messagePaneDocShell) {
+ nsCOMPtr<nsIURIContentListener> listener(
+ do_GetInterface(messagePaneDocShell));
+ if (listener) listener->SetParentContentListener(nullptr);
+ SetRootDocShell(nullptr);
+ mMessageWindowDocShellWeak = nullptr;
+ }
+
+ // in case nsMsgWindow leaks, make sure other stuff doesn't leak.
+ mTransactionManager = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetStatusFeedback(
+ nsIMsgStatusFeedback** aStatusFeedback) {
+ NS_ENSURE_ARG_POINTER(aStatusFeedback);
+ NS_IF_ADDREF(*aStatusFeedback = mStatusFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetStatusFeedback(
+ nsIMsgStatusFeedback* aStatusFeedback) {
+ mStatusFeedback = aStatusFeedback;
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+
+ // register our status feedback object as a web progress listener
+ nsCOMPtr<nsIWebProgress> webProgress(do_GetInterface(messageWindowDocShell));
+ if (webProgress && mStatusFeedback && messageWindowDocShell) {
+ nsCOMPtr<nsIWebProgressListener> webProgressListener =
+ do_QueryInterface(mStatusFeedback);
+ webProgress->AddProgressListener(webProgressListener,
+ nsIWebProgress::NOTIFY_ALL);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetTransactionManager(
+ nsITransactionManager** aTransactionManager) {
+ NS_ENSURE_ARG_POINTER(aTransactionManager);
+ NS_IF_ADDREF(*aTransactionManager = mTransactionManager);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetTransactionManager(
+ nsITransactionManager* aTransactionManager) {
+ mTransactionManager = aTransactionManager;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetOpenFolder(nsIMsgFolder** aOpenFolder) {
+ NS_ENSURE_ARG_POINTER(aOpenFolder);
+ NS_IF_ADDREF(*aOpenFolder = mOpenFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetOpenFolder(nsIMsgFolder* aOpenFolder) {
+ mOpenFolder = aOpenFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetRootDocShell(nsIDocShell** aDocShell) {
+ if (mRootDocShellWeak)
+ CallQueryReferent(mRootDocShellWeak.get(), aDocShell);
+ else
+ *aDocShell = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetRootDocShell(nsIDocShell* aDocShell) {
+ // Query for the doc shell and release it
+ mRootDocShellWeak = nullptr;
+ if (aDocShell) {
+ mRootDocShellWeak = do_GetWeakReference(aDocShell);
+
+ nsCOMPtr<nsIDocShell> messagePaneDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messagePaneDocShell));
+ nsCOMPtr<nsIURIContentListener> listener(
+ do_GetInterface(messagePaneDocShell));
+ if (listener) listener->SetParentContentListener(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetDomWindow(mozIDOMWindowProxy** aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+ if (mDomWindow)
+ CallQueryReferent(mDomWindow.get(), aWindow);
+ else
+ *aWindow = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetDomWindow(mozIDOMWindowProxy* aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+ mDomWindow = do_GetWeakReference(aWindow);
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWindow);
+ nsIDocShell* docShell = nullptr;
+ if (win) docShell = win->GetDocShell();
+
+ nsCOMPtr<nsIDocShellTreeItem> docShellAsItem(docShell);
+
+ if (docShellAsItem) {
+ nsCOMPtr<nsIDocShellTreeItem> rootAsItem;
+ docShellAsItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootAsItem));
+
+ nsCOMPtr<nsIDocShell> rootAsShell(do_QueryInterface(rootAsItem));
+ SetRootDocShell(rootAsShell);
+
+ // force ourselves to figure out the message pane
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aNotificationCallbacks) {
+ mNotificationCallbacks = aNotificationCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aNotificationCallbacks) {
+ NS_ENSURE_ARG_POINTER(aNotificationCallbacks);
+ NS_IF_ADDREF(*aNotificationCallbacks = mNotificationCallbacks);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::StopUrls() {
+ m_stopped = true;
+ nsCOMPtr<nsIWebNavigation> webnav(do_QueryReferent(mRootDocShellWeak));
+ return webnav ? webnav->Stop(nsIWebNavigation::STOP_NETWORK)
+ : NS_ERROR_FAILURE;
+}
+
+// nsIURIContentListener support
+
+NS_IMETHODIMP nsMsgWindow::DoContent(const nsACString& aContentType,
+ bool aIsContentPreferred,
+ nsIRequest* request,
+ nsIStreamListener** aContentHandler,
+ bool* aAbortProcess) {
+ if (!aContentType.IsEmpty()) {
+ // forward the DoContent call to our docshell
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ nsCOMPtr<nsIURIContentListener> ctnListener =
+ do_QueryInterface(messageWindowDocShell);
+ if (ctnListener) {
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ if (!aChannel) return NS_ERROR_FAILURE;
+
+ // get the url for the channel...let's hope it is a mailnews url so we can
+ // set our msg hdr sink on it.. right now, this is the only way I can
+ // think of to force the msg hdr sink into the mime converter so it can
+ // get too it later...
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) mailnewsUrl->SetMsgWindow(this);
+ }
+ return ctnListener->DoContent(aContentType, aIsContentPreferred, request,
+ aContentHandler, aAbortProcess);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgWindow::IsPreferred(const char* aContentType, char** aDesiredContentType,
+ bool* aCanHandleContent) {
+ // We don't want to handle opening any attachments inside the
+ // message pane, but want to let nsIExternalHelperAppService take care.
+ *aCanHandleContent = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::CanHandleContent(const char* aContentType,
+ bool aIsContentPreferred,
+ char** aDesiredContentType,
+ bool* aCanHandleContent)
+
+{
+ // the mail window knows nothing about the default content types
+ // its docshell can handle...ask the content area if it can handle
+ // the content type...
+
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ nsCOMPtr<nsIURIContentListener> ctnListener(
+ do_GetInterface(messageWindowDocShell));
+ if (ctnListener)
+ return ctnListener->CanHandleContent(aContentType, aIsContentPreferred,
+ aDesiredContentType,
+ aCanHandleContent);
+ else
+ *aCanHandleContent = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetParentContentListener(
+ nsIURIContentListener** aParent) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ *aParent = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetParentContentListener(
+ nsIURIContentListener* aParent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetLoadCookie(nsISupports** aLoadCookie) {
+ NS_ENSURE_ARG_POINTER(aLoadCookie);
+ *aLoadCookie = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetLoadCookie(nsISupports* aLoadCookie) {
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgWindow, Stopped, bool, m_stopped)
diff --git a/comm/mailnews/base/src/nsMsgWindow.h b/comm/mailnews/base/src/nsMsgWindow.h
new file mode 100644
index 0000000000..1ca2370552
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgWindow.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsMsgWindow_h
+#define _nsMsgWindow_h
+
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsITransactionManager.h"
+#include "nsIMsgFolder.h"
+#include "nsCOMPtr.h"
+#include "nsIDocShell.h"
+#include "nsIURIContentListener.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIInterfaceRequestor.h"
+
+class nsMsgWindow : public nsIMsgWindow,
+ public nsIURIContentListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsMsgWindow();
+ nsresult Init();
+ NS_DECL_NSIMSGWINDOW
+ NS_DECL_NSIURICONTENTLISTENER
+
+ protected:
+ virtual ~nsMsgWindow();
+ nsCOMPtr<nsIMsgStatusFeedback> mStatusFeedback;
+ nsCOMPtr<nsITransactionManager> mTransactionManager;
+ nsCOMPtr<nsIMsgFolder> mOpenFolder;
+ // These are used by the backend protocol code to attach
+ // notification callbacks to channels, e.g., nsIBadCertListner2.
+ nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks;
+ // authorization prompt used during testing only
+ nsCOMPtr<nsIAuthPrompt> mAuthPrompt;
+
+ // let's not make this a strong ref - we don't own it.
+ nsWeakPtr mRootDocShellWeak;
+ nsWeakPtr mMessageWindowDocShellWeak;
+ nsWeakPtr mDomWindow;
+
+ nsCString mMailCharacterSet;
+ bool mCharsetOverride;
+ bool m_stopped;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.cpp b/comm/mailnews/base/src/nsMsgXFViewThread.cpp
new file mode 100644
index 0000000000..0180a7c910
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFViewThread.cpp
@@ -0,0 +1,444 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgXFViewThread.h"
+#include "nsMsgSearchDBView.h"
+#include "nsMsgMessageFlags.h"
+
+NS_IMPL_ISUPPORTS(nsMsgXFViewThread, nsIMsgThread)
+
+nsMsgXFViewThread::nsMsgXFViewThread(nsMsgSearchDBView* view,
+ nsMsgKey threadId) {
+ m_numUnreadChildren = 0;
+ m_numChildren = 0;
+ m_flags = 0;
+ m_newestMsgDate = 0;
+ m_view = view;
+ m_threadId = threadId;
+}
+
+nsMsgXFViewThread::~nsMsgXFViewThread() {}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetThreadKey(nsMsgKey threadKey) {
+ m_threadId = threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetThreadKey(nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_threadId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetFlags(uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetFlags(uint32_t aFlags) {
+ m_flags = aFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetSubject(const nsACString& aSubject) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetSubject(nsACString& result) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_keys.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNumUnreadChildren(uint32_t* aNumUnreadChildren) {
+ NS_ENSURE_ARG_POINTER(aNumUnreadChildren);
+ *aNumUnreadChildren = m_numUnreadChildren;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::AddChild(nsIMsgDBHdr* aNewHdr, nsIMsgDBHdr* aInReplyTo,
+ bool aThreadInThread,
+ nsIDBChangeAnnouncer* aAnnouncer) {
+ uint32_t whereInserted;
+ return AddHdr(aNewHdr, false, whereInserted, nullptr);
+}
+
+// Returns the parent of the newly added header. If reparentChildren
+// is true, we believe that the new header is a parent of an existing
+// header, and we should find it, and reparent it.
+nsresult nsMsgXFViewThread::AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren,
+ uint32_t& whereInserted,
+ nsIMsgDBHdr** outParent) {
+ nsCOMPtr<nsIMsgFolder> newHdrFolder;
+ newHdr->GetFolder(getter_AddRefs(newHdrFolder));
+
+ uint32_t newHdrFlags = 0;
+ uint32_t msgDate;
+ nsMsgKey newHdrKey = 0;
+
+ newHdr->GetMessageKey(&newHdrKey);
+ newHdr->GetDateInSeconds(&msgDate);
+ newHdr->GetFlags(&newHdrFlags);
+ if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate);
+
+ if (newHdrFlags & nsMsgMessageFlags::Watched)
+ SetFlags(m_flags | nsMsgMessageFlags::Watched);
+
+ ChangeChildCount(1);
+ if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1);
+
+ if (m_numChildren == 1) {
+ m_keys.InsertElementAt(0, newHdrKey);
+ m_levels.InsertElementAt(0, 0);
+ m_folders.InsertObjectAt(newHdrFolder, 0);
+ if (outParent) *outParent = nullptr;
+
+ whereInserted = 0;
+ return NS_OK;
+ }
+
+ // Find our parent, if any, in the thread. Starting at the newest
+ // reference, and working our way back, see if we've mapped that reference
+ // to this thread.
+ uint16_t numReferences;
+ newHdr->GetNumReferences(&numReferences);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ int32_t parentIndex = -1;
+
+ for (int32_t i = numReferences - 1; i >= 0; i--) {
+ nsAutoCString reference;
+ newHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ // I could look for the thread from the reference, but getting
+ // the header directly should be fine. If it's not, that means
+ // that the parent isn't in this thread, though it should be.
+ m_view->GetMsgHdrFromHash(reference, getter_AddRefs(parent));
+ if (parent) {
+ parentIndex = HdrIndex(parent);
+ if (parentIndex == -1) {
+ NS_ERROR("how did we get in the wrong thread?");
+ parent = nullptr;
+ }
+
+ break;
+ }
+ }
+
+ if (parent) {
+ uint32_t parentLevel = m_levels[parentIndex];
+ nsMsgKey parentKey;
+ parent->GetMessageKey(&parentKey);
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ parent->GetFolder(getter_AddRefs(parentFolder));
+
+ if (outParent) parent.forget(outParent);
+
+ // Iterate over our parents' children until we find one we're older than,
+ // and insert ourselves before it, or as the last child. In other words,
+ // insert, sorted by date.
+ uint32_t msgDate, childDate;
+ newHdr->GetDateInSeconds(&msgDate);
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsMsgViewIndex i;
+ nsMsgViewIndex insertIndex = m_keys.Length();
+ uint32_t insertLevel = parentLevel + 1;
+ for (i = parentIndex;
+ i < m_keys.Length() &&
+ (i == (nsMsgViewIndex)parentIndex || m_levels[i] >= parentLevel);
+ i++) {
+ GetChildHdrAt(i, getter_AddRefs(child));
+ if (child) {
+ if (reparentChildren && IsHdrParentOf(newHdr, child)) {
+ insertIndex = i;
+ // Bump all the children of the current child, and the child.
+ nsMsgViewIndex j = insertIndex;
+ uint8_t childLevel = m_levels[insertIndex];
+ do {
+ m_levels[j] = m_levels[j] + 1;
+ j++;
+ } while (j < m_keys.Length() && m_levels[j] > childLevel);
+ break;
+ } else if (m_levels[i] == parentLevel + 1) {
+ // Possible sibling.
+ child->GetDateInSeconds(&childDate);
+ if (msgDate < childDate) {
+ // If we think we need to reparent, remember this insert index,
+ // but keep looking for children.
+ insertIndex = i;
+ insertLevel = m_levels[i];
+ // If the sibling we're inserting after has children, we need
+ // to go after the children.
+ while (insertIndex + 1 < m_keys.Length() &&
+ m_levels[insertIndex + 1] > insertLevel) {
+ insertIndex++;
+ }
+
+ if (!reparentChildren) break;
+ }
+ }
+ }
+ }
+
+ m_keys.InsertElementAt(insertIndex, newHdrKey);
+ m_levels.InsertElementAt(insertIndex, insertLevel);
+ m_folders.InsertObjectAt(newHdrFolder, insertIndex);
+ whereInserted = insertIndex;
+ } else {
+ if (outParent) *outParent = nullptr;
+
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ GetChildHdrAt(0, getter_AddRefs(rootHdr));
+ // If the new header is a parent of the root then it should be promoted.
+ if (rootHdr && IsHdrParentOf(newHdr, rootHdr)) {
+ m_keys.InsertElementAt(0, newHdrKey);
+ m_levels.InsertElementAt(0, 0);
+ m_folders.InsertObjectAt(newHdrFolder, 0);
+ whereInserted = 0;
+ // Adjust level of old root hdr and its children
+ for (nsMsgViewIndex i = 1; i < m_keys.Length(); i++)
+ m_levels[i] = m_levels[1] + 1;
+ } else {
+ m_keys.AppendElement(newHdrKey);
+ m_levels.AppendElement(1);
+ m_folders.AppendObject(newHdrFolder);
+ if (outParent) rootHdr.forget(outParent);
+
+ whereInserted = m_keys.Length() - 1;
+ }
+ }
+
+ // ### TODO handle the case where the root header starts
+ // with Re, and the new one doesn't, and is earlier. In that
+ // case, we want to promote the new header to root.
+ // PRTime newHdrDate;
+ // newHdr->GetDate(&newHdrDate);
+ // if (numChildren > 0 && !(newHdrFlags & nsMsgMessageFlags::HasRe)) {
+ // PRTime topLevelHdrDate;
+ // nsCOMPtr<nsIMsgDBHdr> topLevelHdr;
+ // rv = GetRootHdr(getter_AddRefs(topLevelHdr));
+ // if (NS_SUCCEEDED(rv) && topLevelHdr) {
+ // topLevelHdr->GetDate(&topLevelHdrDate);
+ // if (newHdrDate < topLevelHdrDate) ?? and now ??
+ // }
+ // }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_folders[aIndex]->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return db->GetMsgHdrForKey(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::RemoveChildAt(uint32_t aIndex) {
+ m_keys.RemoveElementAt(aIndex);
+ m_levels.RemoveElementAt(aIndex);
+ m_folders.RemoveObjectAt(aIndex);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::RemoveChildHdr(nsIMsgDBHdr* child,
+ nsIDBChangeAnnouncer* announcer) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ child->GetMessageKey(&msgKey);
+ child->GetFlags(&msgFlags);
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ child->GetFolder(getter_AddRefs(msgFolder));
+ // If this was the newest msg, clear the newest msg date so we'll recalc.
+ uint32_t date;
+ child->GetDateInSeconds(&date);
+ if (date == m_newestMsgDate) SetNewestMsgDate(0);
+
+ for (uint32_t childIndex = 0; childIndex < m_keys.Length(); childIndex++) {
+ if (m_keys[childIndex] == msgKey && m_folders[childIndex] == msgFolder) {
+ uint8_t levelRemoved = m_keys[childIndex];
+ // Adjust the levels of all the children of this header.
+ nsMsgViewIndex i;
+ for (i = childIndex + 1;
+ i < m_keys.Length() && m_levels[i] > levelRemoved; i++) {
+ m_levels[i] = m_levels[i] - 1;
+ }
+
+ m_view->NoteChange(childIndex + 1, i - childIndex + 1,
+ nsMsgViewNotificationCode::changed);
+ m_keys.RemoveElementAt(childIndex);
+ m_levels.RemoveElementAt(childIndex);
+ m_folders.RemoveObjectAt(childIndex);
+ if (!(msgFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1);
+
+ ChangeChildCount(-1);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetRootHdr(nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ return GetChildHdrAt(0, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChild(nsMsgKey msgKey, nsIMsgDBHdr** aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+int32_t nsMsgXFViewThread::HdrIndex(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetMessageKey(&msgKey);
+ hdr->GetFolder(getter_AddRefs(folder));
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ if (m_keys[i] == msgKey && m_folders[i] == folder) return i;
+ }
+
+ return -1;
+}
+
+void nsMsgXFViewThread::ChangeUnreadChildCount(int32_t delta) {
+ m_numUnreadChildren += delta;
+}
+
+void nsMsgXFViewThread::ChangeChildCount(int32_t delta) {
+ m_numChildren += delta;
+}
+
+bool nsMsgXFViewThread::IsHdrParentOf(nsIMsgDBHdr* possibleParent,
+ nsIMsgDBHdr* possibleChild) {
+ uint16_t referenceToCheck = 0;
+ possibleChild->GetNumReferences(&referenceToCheck);
+ nsAutoCString reference;
+
+ nsCString messageId;
+ possibleParent->GetMessageId(getter_Copies(messageId));
+
+ while (referenceToCheck > 0) {
+ possibleChild->GetStringReference(referenceToCheck - 1, reference);
+
+ if (reference.Equals(messageId)) return true;
+
+ // If reference didn't match, check if this ref is for a non-existent
+ // header. If it is, continue looking at ancestors.
+ nsCOMPtr<nsIMsgDBHdr> refHdr;
+ m_view->GetMsgHdrFromHash(reference, getter_AddRefs(refHdr));
+ if (refHdr) break;
+
+ referenceToCheck--;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNewestMsgDate(uint32_t* aResult) {
+ // If this hasn't been set, figure it out by enumerating the msgs in the
+ // thread.
+ if (!m_newestMsgDate) {
+ uint32_t numChildren;
+ nsresult rv = NS_OK;
+
+ GetNumChildren(&numChildren);
+
+ if ((int32_t)numChildren < 0) numChildren = 0;
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ uint32_t msgDate;
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate;
+ }
+ }
+ }
+
+ *aResult = m_newestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetNewestMsgDate(uint32_t aNewestMsgDate) {
+ m_newestMsgDate = aNewestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::MarkChildRead(bool aRead) {
+ ChangeUnreadChildCount(aRead ? -1 : 1);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetFirstUnreadChild(nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG(aResult);
+ uint32_t numChildren;
+ GetNumChildren(&numChildren);
+
+ if ((int32_t)numChildren < 0) numChildren = 0;
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ bool isRead;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_folders[childIndex]->GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv)) rv = db->IsRead(msgKey, &isRead);
+
+ if (NS_SUCCEEDED(rv) && !isRead) {
+ child.forget(aResult);
+ break;
+ }
+ }
+ }
+
+ return (*aResult) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::EnumerateMessages(nsMsgKey aParentKey,
+ nsIMsgEnumerator** aResult) {
+ NS_ERROR("shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.h b/comm/mailnews/base/src/nsMsgXFViewThread.h
new file mode 100644
index 0000000000..ff3276bf3f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFViewThread.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsMsgXFViewThread_h__
+#define nsMsgXFViewThread_h__
+
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgThread.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgDBView.h"
+
+class nsMsgSearchDBView;
+
+class nsMsgXFViewThread : public nsIMsgThread {
+ public:
+ nsMsgXFViewThread(nsMsgSearchDBView* view, nsMsgKey threadId);
+
+ NS_DECL_NSIMSGTHREAD
+ NS_DECL_ISUPPORTS
+
+ bool IsHdrParentOf(nsIMsgDBHdr* possibleParent, nsIMsgDBHdr* possibleChild);
+
+ void ChangeUnreadChildCount(int32_t delta);
+ void ChangeChildCount(int32_t delta);
+
+ nsresult AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren,
+ uint32_t& whereInserted, nsIMsgDBHdr** outParent);
+ int32_t HdrIndex(nsIMsgDBHdr* hdr);
+ uint32_t ChildLevelAt(uint32_t msgIndex) { return m_levels[msgIndex]; }
+ uint32_t MsgCount() { return m_numChildren; };
+
+ protected:
+ virtual ~nsMsgXFViewThread();
+
+ nsMsgSearchDBView* m_view;
+ uint32_t m_numUnreadChildren;
+ uint32_t m_numChildren;
+ uint32_t m_flags;
+ uint32_t m_newestMsgDate;
+ nsMsgKey m_threadId;
+ nsTArray<nsMsgKey> m_keys;
+ nsCOMArray<nsIMsgFolder> m_folders;
+ nsTArray<uint8_t> m_levels;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp
new file mode 100644
index 0000000000..fc2a1886eb
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp
@@ -0,0 +1,514 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgXFVirtualFolderDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsQuickSort.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgCopyService.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsMsgMessageFlags.h"
+#include "nsServiceManagerUtils.h"
+
+nsMsgXFVirtualFolderDBView::nsMsgXFVirtualFolderDBView() {
+ mSuppressMsgDisplay = false;
+ m_doingSearch = false;
+ m_doingQuickSearch = false;
+ m_totalMessagesInView = 0;
+ m_curFolderHasCachedHits = false;
+}
+
+nsMsgXFVirtualFolderDBView::~nsMsgXFVirtualFolderDBView() {}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::Open(nsIMsgFolder* folder,
+ nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags,
+ int32_t* pCount) {
+ m_viewFolder = folder;
+ return nsMsgSearchDBView::Open(folder, sortType, sortOrder, viewFlags,
+ pCount);
+}
+
+void nsMsgXFVirtualFolderDBView::RemovePendingDBListeners() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+
+ // UnregisterPendingListener will return an error when there are no more
+ // instances of this object registered as pending listeners.
+ while (NS_SUCCEEDED(rv)) rv = msgDBService->UnregisterPendingListener(this);
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::Close() {
+ RemovePendingDBListeners();
+ return nsMsgSearchDBView::Close();
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgXFVirtualFolderDBView* newMsgDBView = new nsMsgXFVirtualFolderDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::CopyDBView(
+ nsMsgDBView* aNewMsgDBView, nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow, nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgSearchDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+
+ nsMsgXFVirtualFolderDBView* newMsgDBView =
+ (nsMsgXFVirtualFolderDBView*)aNewMsgDBView;
+
+ newMsgDBView->m_viewFolder = m_viewFolder;
+ newMsgDBView->m_searchSession = m_searchSession;
+
+ int32_t scopeCount;
+ nsresult rv;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession, &rv);
+ // It's OK not to have a search session.
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ searchSession->CountSearchScopes(&scopeCount);
+ for (int32_t i = 0; i < scopeCount; i++) {
+ nsMsgSearchScopeValue scopeId;
+ nsCOMPtr<nsIMsgFolder> searchFolder;
+ searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder));
+ if (searchFolder)
+ msgDBService->RegisterPendingListener(searchFolder, newMsgDBView);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowVirtualFolderResults;
+ return NS_OK;
+}
+
+nsresult nsMsgXFVirtualFolderDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ if (newHdr) {
+ bool match = false;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+
+ if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match);
+
+ if (!match) match = WasHdrRecentlyDeleted(newHdr);
+
+ if (match) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ newHdr->GetFolder(getter_AddRefs(folder));
+ bool saveDoingSearch = m_doingSearch;
+ m_doingSearch = false;
+ OnSearchHit(newHdr, folder);
+ m_doingSearch = saveDoingSearch;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnHdrPropertyChanged(
+ nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange,
+ uint32_t* aStatus, nsIDBChangeListener* aInstigator) {
+ // If the junk mail plugin just activated on a message, then
+ // we'll allow filters to remove from view.
+ // Otherwise, just update the view line.
+ //
+ // Note this will not add newly matched headers to the view. This is
+ // probably a bug that needs fixing.
+
+ NS_ENSURE_ARG_POINTER(aStatus);
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ // Message does not appear in view.
+ if (index == nsMsgViewIndex_None) return NS_OK;
+
+ nsCString originStr;
+ (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr);
+ // Check for "plugin" with only first character for performance.
+ bool plugin = (originStr.get()[0] == 'p');
+
+ if (aPreChange) {
+ // First call, done prior to the change.
+ *aStatus = plugin;
+ return NS_OK;
+ }
+
+ // Second call, done after the change.
+ bool wasPlugin = *aStatus;
+
+ bool match = true;
+ nsCOMPtr<nsIMsgSearchSession> searchSession(
+ do_QueryReferent(m_searchSession));
+ if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match);
+
+ if (!match && plugin && !wasPlugin)
+ // Remove hdr from view.
+ RemoveByIndex(index);
+ else
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ return NS_OK;
+}
+
+void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForFolder(
+ nsIMsgFolder* folder, nsTArray<nsMsgKey> const& newHits) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ nsTArray<nsMsgKey> badHits;
+ rv = db->RefreshCache(searchUri, newHits, badHits);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDBHdr> badHdr;
+ for (nsMsgKey badKey : badHits) {
+ // ### of course, this isn't quite right, since we should be
+ // using FindHdr, and we shouldn't be expanding the threads.
+ db->GetMsgHdrForKey(badKey, getter_AddRefs(badHdr));
+ // Let nsMsgSearchDBView decide what to do about this header
+ // getting removed.
+ if (badHdr) OnHdrDeleted(badHdr, nsMsgKey_None, 0, this);
+ }
+ }
+ }
+}
+
+void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForPrevSearchedFolders(
+ nsIMsgFolder* curSearchFolder) {
+ // Handle the most recent folder with hits, if any.
+ if (m_curFolderGettingHits) {
+ uint32_t count = m_hdrHits.Count();
+ nsTArray<nsMsgKey> newHits;
+ newHits.SetLength(count);
+ for (uint32_t i = 0; i < count; i++)
+ m_hdrHits[i]->GetMessageKey(&newHits[i]);
+
+ newHits.Sort();
+ UpdateCacheAndViewForFolder(m_curFolderGettingHits, newHits);
+ m_foldersSearchingOver.RemoveObject(m_curFolderGettingHits);
+ }
+
+ while (m_foldersSearchingOver.Count() > 0) {
+ // This new folder has cached hits.
+ if (m_foldersSearchingOver[0] == curSearchFolder) {
+ m_curFolderHasCachedHits = true;
+ m_foldersSearchingOver.RemoveObjectAt(0);
+ break;
+ } else {
+ // This must be a folder that had no hits with the current search.
+ // So all cached hits, if any, need to be removed.
+ nsTArray<nsMsgKey> noHits;
+ UpdateCacheAndViewForFolder(m_foldersSearchingOver[0], noHits);
+ m_foldersSearchingOver.RemoveObjectAt(0);
+ }
+ }
+}
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG(aFolder);
+
+ if (m_curFolderGettingHits != aFolder && m_doingSearch &&
+ !m_doingQuickSearch) {
+ m_curFolderHasCachedHits = false;
+ // Since we've gotten a hit for a new folder, the searches for
+ // any previous folders are done, so deal with stale cached hits
+ // for those folders now.
+ UpdateCacheAndViewForPrevSearchedFolders(aFolder);
+ m_curFolderGettingHits = aFolder;
+ m_hdrHits.Clear();
+ m_curFolderStartKeyIndex = m_keys.Length();
+ }
+
+ bool hdrInCache = false;
+ if (!m_doingQuickSearch) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ nsCOMPtr<nsIDBFolderInfo> dummyInfo;
+ nsresult rv = aFolder->GetDBFolderInfoAndDB(getter_AddRefs(dummyInfo),
+ getter_AddRefs(dbToUse));
+ if (NS_SUCCEEDED(rv)) {
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ dbToUse->HdrIsInCache(searchUri, aMsgHdr, &hdrInCache);
+ }
+ }
+
+ if (!m_doingSearch || !m_curFolderHasCachedHits || !hdrInCache) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ nsMsgGroupView::OnNewHeader(aMsgHdr, nsMsgKey_None, true);
+ else if (m_sortValid)
+ InsertHdrFromFolder(aMsgHdr, aFolder);
+ else
+ AddHdrFromFolder(aMsgHdr, aFolder);
+ }
+
+ m_hdrHits.AppendObject(aMsgHdr);
+ m_totalMessagesInView++;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnSearchDone(nsresult status) {
+ // This batch began in OnNewSearch.
+ if (mJSTree) mJSTree->EndUpdateBatch();
+
+ NS_ENSURE_TRUE(m_viewFolder, NS_ERROR_NOT_INITIALIZED);
+
+ // Handle any non verified hits we haven't handled yet.
+ if (NS_SUCCEEDED(status) && !m_doingQuickSearch &&
+ status != NS_MSG_SEARCH_INTERRUPTED)
+ UpdateCacheAndViewForPrevSearchedFolders(nullptr);
+
+ m_doingSearch = false;
+ // We want to set imap delete model once the search is over because setting
+ // next message after deletion will happen before deleting the message and
+ // search scope can change with every search.
+
+ // Set to default in case it is non-imap folder.
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0);
+ if (curFolder) GetImapDeleteModel(curFolder);
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Count up the number of unread and total messages from the view, and set
+ // those in the folder - easier than trying to keep the count up to date in
+ // the face of search hits coming in while the user is reading/deleting
+ // messages.
+ uint32_t numUnread = 0;
+ for (uint32_t i = 0; i < m_flags.Length(); i++) {
+ if (m_flags[i] & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingIndex(i, getter_AddRefs(thread));
+ if (thread) {
+ uint32_t unreadInThread;
+ thread->GetNumUnreadChildren(&unreadInThread);
+ numUnread += unreadInThread;
+ }
+ } else {
+ if (!(m_flags[i] & nsMsgMessageFlags::Read)) numUnread++;
+ }
+ }
+
+ dbFolderInfo->SetNumUnreadMessages(numUnread);
+ dbFolderInfo->SetNumMessages(m_totalMessagesInView);
+ // Force update from db.
+ m_viewFolder->UpdateSummaryTotals(true);
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ if (!m_sortValid && m_sortType != nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // Sort the results.
+ m_sortValid = false;
+ Sort(m_sortType, m_sortOrder);
+ }
+
+ m_foldersSearchingOver.Clear();
+ m_curFolderGettingHits = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ RemovePendingDBListeners();
+ m_doingSearch = true;
+ m_totalMessagesInView = 0;
+ m_folders.Clear();
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+
+ // Needs to happen after we remove the keys, since RowCountChanged() will
+ // call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ // To use the search results cache, we'll need to iterate over the scopes
+ // in the search session, calling getNthSearchScope
+ // for i = 0; i < searchSession.countSearchScopes; i++
+ // and for each folder, then open the db and pull out the cached hits,
+ // add them to the view. For each hit in a new folder, we'll then clean up
+ // the stale hits from the previous folder(s).
+
+ int32_t scopeCount;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ // Just ignore.
+ NS_ENSURE_TRUE(searchSession, NS_OK);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1");
+ searchSession->CountSearchScopes(&scopeCount);
+
+ // Figure out how many search terms the virtual folder has.
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString terms;
+ dbFolderInfo->GetCharProperty("searchStr", terms);
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = searchSession->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString curSearchAsString;
+
+ rv = MsgTermListToString(searchTerms, curSearchAsString);
+ // Trim off the initial AND/OR, which is irrelevant and inconsistent between
+ // what SearchSpec.jsm generates, and what's in virtualFolders.dat.
+ curSearchAsString.Cut(0,
+ StringBeginsWith(curSearchAsString, "AND"_ns) ? 3 : 2);
+ terms.Cut(0, StringBeginsWith(terms, "AND"_ns) ? 3 : 2);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ // If the search session search string doesn't match the vf search str,
+ // then we're doing quick search, which means we don't want to invalidate
+ // cached results, or used cached results.
+ m_doingQuickSearch = !curSearchAsString.Equals(terms);
+
+ if (!m_doingQuickSearch) {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ }
+
+ for (int32_t i = 0; i < scopeCount; i++) {
+ nsMsgSearchScopeValue scopeId;
+ nsCOMPtr<nsIMsgFolder> searchFolder;
+ searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder));
+ if (searchFolder) {
+ nsCOMPtr<nsIMsgDatabase> searchDB;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ nsresult rv = searchFolder->GetMsgDatabase(getter_AddRefs(searchDB));
+ if (NS_SUCCEEDED(rv) && searchDB) {
+ if (msgDBService)
+ msgDBService->RegisterPendingListener(searchFolder, this);
+
+ m_foldersSearchingOver.AppendObject(searchFolder);
+ // Ignore cached hits in quick search case.
+ if (m_doingQuickSearch) continue;
+
+ nsCOMPtr<nsIMsgEnumerator> cachedHits;
+ searchDB->GetCachedHits(searchUri, getter_AddRefs(cachedHits));
+ bool hasMore;
+ if (cachedHits) {
+ cachedHits->HasMoreElements(&hasMore);
+ if (hasMore) {
+ mozilla::DebugOnly<nsMsgKey> prevKey = nsMsgKey_None;
+ while (hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ nsresult rv = cachedHits->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ NS_ASSERTION(prevKey == nsMsgKey_None || msgKey > prevKey,
+ "cached Hits not sorted");
+#ifdef DEBUG
+ prevKey = msgKey;
+#endif
+ AddHdrFromFolder(header, searchFolder);
+ } else {
+ break;
+ }
+
+ cachedHits->HasMoreElements(&hasMore);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!m_doingQuickSearch) {
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+
+ m_curFolderStartKeyIndex = 0;
+ m_curFolderGettingHits = nullptr;
+ m_curFolderHasCachedHits = false;
+
+ // If we have cached hits, sort them.
+ if (GetSize() > 0) {
+ // Currently, we keep threaded views sorted while we build them.
+ if (m_sortType != nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // Sort the results.
+ m_sortValid = false;
+ Sort(m_sortType, m_sortOrder);
+ } else if (mJSTree) {
+ mJSTree->Invalidate();
+ }
+ }
+
+ // Prevent updates for every message found. This batch ends in OnSearchDone.
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ return nsMsgSearchDBView::DoCommand(command);
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_viewFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) {
+ nsresult rv = NS_OK;
+ // If the grouping/threading has changed, rebuild the view.
+ if ((m_viewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay)) !=
+ (aViewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay))) {
+ rv = RebuildView(aViewFlags);
+ }
+
+ nsMsgDBView::SetViewFlags(aViewFlags);
+ return rv;
+}
+
+nsresult nsMsgXFVirtualFolderDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ return GetViewEnumerator(enumerator);
+}
diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h
new file mode 100644
index 0000000000..5b8b627347
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgXFVirtualFolderDBView_H_
+#define _nsMsgXFVirtualFolderDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgSearchDBView.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsCOMArray.h"
+
+class nsMsgGroupThread;
+
+class nsMsgXFVirtualFolderDBView : public nsMsgSearchDBView {
+ public:
+ nsMsgXFVirtualFolderDBView();
+ virtual ~nsMsgXFVirtualFolderDBView();
+
+ // we override all the methods, currently. Might change...
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual const char* GetViewName(void) override {
+ return "XFVirtualFolderView";
+ }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override;
+ NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override;
+ NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetMsgFolder(nsIMsgFolder** aMsgFolder) override;
+
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey,
+ bool ensureListed) override;
+ void UpdateCacheAndViewForPrevSearchedFolders(nsIMsgFolder* curSearchFolder);
+ void UpdateCacheAndViewForFolder(nsIMsgFolder* folder,
+ nsTArray<nsMsgKey> const& newHits);
+ void RemovePendingDBListeners();
+
+ protected:
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+
+ nsCOMArray<nsIMsgFolder> m_foldersSearchingOver;
+ nsCOMArray<nsIMsgDBHdr> m_hdrHits;
+ nsCOMPtr<nsIMsgFolder> m_curFolderGettingHits;
+ // keeps track of the index of the first hit from the cur folder
+ uint32_t m_curFolderStartKeyIndex;
+ bool m_curFolderHasCachedHits;
+ bool m_doingSearch;
+ // Are we doing a quick search on top of the virtual folder search?
+ bool m_doingQuickSearch;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.cpp b/comm/mailnews/base/src/nsNewMailnewsURI.cpp
new file mode 100644
index 0000000000..305e9273a4
--- /dev/null
+++ b/comm/mailnews/base/src/nsNewMailnewsURI.cpp
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsNewMailnewsURI.h"
+#include "nsURLHelper.h"
+#include "nsSimpleURI.h"
+#include "nsStandardURL.h"
+#include "nsThreadUtils.h"
+#include "MainThreadUtils.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsIMsgProtocolHandler.h"
+#include "nsIComponentRegistrar.h"
+#include "nsXULAppAPI.h"
+
+#include "../../local/src/nsPop3URL.h"
+#include "../../local/src/nsMailboxService.h"
+#include "../../compose/src/nsSmtpUrl.h"
+#include "../../addrbook/src/nsLDAPURL.h"
+#include "../../imap/src/nsImapService.h"
+#include "../../news/src/nsNntpUrl.h"
+#include "../src/nsCidProtocolHandler.h"
+
+nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec,
+ const char* aCharset /* = nullptr */,
+ nsIURI* aBaseURI /* = nullptr */) {
+ // Mailnews URIs aren't allowed in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_UNKNOWN_PROTOCOL;
+ }
+
+ nsAutoCString scheme;
+ nsresult rv = net_ExtractURLScheme(aSpec, scheme);
+ if (NS_FAILED(rv)) {
+ // then aSpec is relative
+ if (!aBaseURI) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ rv = aBaseURI->GetScheme(scheme);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Creating IMAP/mailbox URIs off the main thread can lead to crashes.
+ // Seems to happen when viewing PDFs.
+ if (scheme.EqualsLiteral("mailbox") ||
+ scheme.EqualsLiteral("mailbox-message")) {
+ if (NS_IsMainThread()) {
+ return nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("imap") || scheme.EqualsLiteral("imap-message")) {
+ if (NS_IsMainThread()) {
+ return nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("smtp") || scheme.EqualsLiteral("smtps")) {
+ return nsSmtpUrl::NewSmtpURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("mailto")) {
+ if (NS_IsMainThread()) {
+ return nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI);
+ }
+ // If we're for some reason not on the main thread, dispatch to main
+ // or else we'll crash.
+ auto NewURI = [&aSpec, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("pop") || scheme.EqualsLiteral("pop3")) {
+ return nsPop3URL::NewURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews") ||
+ scheme.EqualsLiteral("news-message") || scheme.EqualsLiteral("nntp")) {
+ return nsNntpUrl::NewURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("cid")) {
+ return nsCidProtocolHandler::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("ldap") || scheme.EqualsLiteral("ldaps")) {
+ nsCOMPtr<nsILDAPURL> url = do_CreateInstance(NS_LDAPURL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = url->Init(nsIStandardURL::URLTYPE_STANDARD,
+ scheme.EqualsLiteral("ldap") ? 389 : 636, aSpec, aCharset,
+ aBaseURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ url.forget(aURI);
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("smile")) {
+ return NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+ if (scheme.EqualsLiteral("moz-cal-handle-itip")) {
+ return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+ if (scheme.EqualsLiteral("webcal") || scheme.EqualsLiteral("webcals")) {
+ return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+
+ rv = NS_ERROR_UNKNOWN_PROTOCOL; // Let M-C handle it by default.
+
+ nsCOMPtr<nsIComponentRegistrar> compMgr;
+ NS_GetComponentRegistrar(getter_AddRefs(compMgr));
+ if (compMgr) {
+ nsAutoCString contractID(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX);
+ contractID += scheme;
+ bool isRegistered = false;
+ compMgr->IsContractIDRegistered(contractID.get(), &isRegistered);
+ if (isRegistered) {
+ auto NewURI =
+ [&aSpec, &aCharset, &aBaseURI, aURI, &contractID, &rv ]() -> auto{
+ nsCOMPtr<nsIMsgProtocolHandler> handler(
+ do_GetService(contractID.get()));
+ if (handler) {
+ // We recognise this URI. Use the protocol handler's result.
+ rv = handler->NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ };
+ if (NS_IsMainThread()) {
+ NewURI();
+ } else {
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ }
+ }
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.h b/comm/mailnews/base/src/nsNewMailnewsURI.h
new file mode 100644
index 0000000000..a5775d1978
--- /dev/null
+++ b/comm/mailnews/base/src/nsNewMailnewsURI.h
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsNewMailnewsURI_h__
+#define nsNewMailnewsURI_h__
+
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec,
+ const char* aCharset /* = nullptr */,
+ nsIURI* aBaseURI /* = nullptr */);
+#endif
diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp
new file mode 100644
index 0000000000..1f325edadf
--- /dev/null
+++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsQuarantinedOutputStream.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIInputStream.h"
+#include "nsISeekableStream.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "mozilla/UniquePtr.h"
+
+NS_IMPL_ISUPPORTS(nsQuarantinedOutputStream, nsIOutputStream,
+ nsISafeOutputStream)
+
+nsQuarantinedOutputStream::~nsQuarantinedOutputStream() { Close(); }
+
+// Initialise mTempFile and open it for writing (mTempStream).
+nsresult nsQuarantinedOutputStream::InitTemp() {
+ MOZ_ASSERT(mState == eUninitialized);
+ MOZ_ASSERT(!mTempFile);
+ MOZ_ASSERT(!mTempStream);
+ // Create a unique temp file.
+ {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = file->Append(u"newmsg"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mTempFile = std::move(file);
+ }
+
+ // Open the temp file for writing.
+ {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTempFile,
+ -1, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mTempStream = std::move(stream);
+ }
+
+ return NS_OK;
+}
+
+// Put us into the error state and clean up (by deleting the temp file
+// if it exists).
+void nsQuarantinedOutputStream::EnterErrorState(nsresult status) {
+ mState = eError;
+ mError = status;
+ mTarget = nullptr;
+
+ if (mTempStream) {
+ mTempStream = nullptr;
+ }
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ }
+}
+
+// copyStream copies all the data in the input stream to the output stream.
+// It keeps going until it sees an EOF on the input.
+static nsresult copyStream(nsIInputStream* in, nsIOutputStream* out) {
+ constexpr uint32_t BUFSIZE = 8192;
+ auto buf = mozilla::MakeUnique<char[]>(BUFSIZE);
+ while (true) {
+ // Read input stream into buf.
+ uint32_t bufCnt;
+ nsresult rv = in->Read(buf.get(), BUFSIZE, &bufCnt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bufCnt == 0) {
+ break; // EOF. We're all done!
+ }
+ // Write buf to output stream.
+ uint32_t pos = 0;
+ while (pos < bufCnt) {
+ uint32_t writeCnt;
+ rv = out->Write(buf.get() + pos, bufCnt - pos, &writeCnt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pos += writeCnt;
+ }
+ }
+ return NS_OK;
+}
+
+// copyStreamSafely() wraps copyStream(). If the output stream is seekable,
+// it will try to roll it back if an error occurs during the copy.
+static nsresult copyStreamSafely(nsIInputStream* in, nsIOutputStream* out) {
+ nsCOMPtr<nsISeekableStream> outSeekable = do_QueryInterface(out);
+ if (!outSeekable) {
+ // It's not seekable, so we jump out without a parachute.
+ return copyStream(in, out);
+ }
+ int64_t initialOffset;
+ nsresult rv = outSeekable->Tell(&initialOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStream(in, out);
+ if (NS_FAILED(rv)) {
+ // Uhoh... the copy failed! Try to remove the partially-written data.
+ rv = outSeekable->Seek(nsISeekableStream::NS_SEEK_SET, initialOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = outSeekable->SetEOF();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Close() {
+ if (mState != eOpen) {
+ // Already failed or closed or no data written. That's OK.
+ return NS_OK;
+ }
+ nsresult rv = NS_OK;
+ if (mTempStream) {
+ rv = mTempStream->Close();
+ mTempStream = nullptr;
+ }
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ }
+ mTarget->Close();
+ mTarget = nullptr;
+ mState = eClosed;
+ return rv;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Finish() {
+ // Fail here if there was a previous error.
+ if (mState == eError) {
+ return mError;
+ }
+ if (mState != eOpen) {
+ // Already closed or no data written. That's OK.
+ return NS_OK;
+ }
+
+ // Flush and close the temp file. Hopefully any virus checker will now act
+ // and prevent us reopening any suspicious-looking file.
+ MOZ_ASSERT(mTempStream);
+ MOZ_ASSERT(mTempFile);
+ mTempStream->Flush();
+ nsresult rv = mTempStream->Close();
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ mTempStream = nullptr;
+
+ // Write the tempfile out to the target stream
+ {
+ nsCOMPtr<nsIInputStream> ins;
+ // If a virus checker smells something bad, it should show up here as a
+ // failure to (re)open the temp file.
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(ins), mTempFile);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ rv = copyStreamSafely(ins, mTarget);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ }
+
+ // All done!
+ mTarget->Close();
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ mState = eClosed;
+ mTarget = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Flush() {
+ if (mState != eOpen) {
+ return NS_OK; // Don't rock the boat.
+ }
+ nsresult rv = mTempStream->Flush();
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Write(const char* buf, uint32_t count,
+ uint32_t* result) {
+ if (mState == eUninitialized) {
+ // Lazy open.
+ nsresult rv = InitTemp();
+ if NS_FAILED (rv) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ mState = eOpen;
+ }
+
+ if (mState != eOpen) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv = mTempStream->Write(buf, count, result);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::WriteFrom(nsIInputStream* fromStream,
+ uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::WriteSegments(nsReadSegmentFun reader,
+ void* closure,
+ uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::IsNonBlocking(bool* nonBlocking) {
+ *nonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::StreamStatus() {
+ return mState == eOpen ? NS_OK : NS_BASE_STREAM_CLOSED;
+}
diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.h b/comm/mailnews/base/src/nsQuarantinedOutputStream.h
new file mode 100644
index 0000000000..515440494b
--- /dev/null
+++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsQuarantinedOutputStream_h__
+#define nsQuarantinedOutputStream_h__
+
+// #include "nsISupports.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsCOMPtr.h"
+class nsIFile;
+
+/**
+ * nsQuarantinedOutputStream layers on top of an existing target output stream.
+ * The idea is to let an OS virus checker quarantine individual messages
+ * _before_ they hit the mbox. You don't want entire mboxes embargoed if
+ * you can avoid it.
+ *
+ * It works by buffering all writes to a temporary file.
+ * When finish() is called the temporary file is closed, reopened,
+ * then copied into a pre-existing target stream. There's no special OS
+ * virus-checker integration - the assumption is that the checker will hook
+ * into the filesystem and prevent us from opening a file it has flagged as
+ * dodgy. Hence the temp file close/reopen before the final write.
+ *
+ * If the nsQuarantinedOutputStream is closed (or released) without calling
+ * finish(), the write is discarded (as per nsISafeOutputStream requirements).
+ *
+ * Upon close() or finish(), the underlying target file is also closed.
+ */
+class nsQuarantinedOutputStream : public nsIOutputStream, nsISafeOutputStream {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+ NS_DECL_NSISAFEOUTPUTSTREAM
+
+ /**
+ * Pass the target output stream in during construction. Upon Close(),
+ * the written data will be copied here.
+ */
+ explicit nsQuarantinedOutputStream(nsIOutputStream* target)
+ : mTarget(target) {}
+ nsQuarantinedOutputStream() = delete;
+
+ protected:
+ virtual ~nsQuarantinedOutputStream();
+
+ // Set up mTempFile and mTempStream (called at
+ // (lazily set up, upon first write).
+ nsresult InitTemp();
+ nsresult PerformAppend();
+ void EnterErrorState(nsresult status);
+
+ // The temporary file and stream we're writing to.
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIOutputStream> mTempStream;
+
+ // The stream we'll be appending to if it all succeeds.
+ nsCOMPtr<nsIOutputStream> mTarget;
+
+ enum {
+ eUninitialized, // No temp file yet.
+ eOpen, // We're up and running.
+ eClosed, // The file has been closed.
+ eError // An error has occurred (stored in mError).
+ } mState{eUninitialized};
+ nsresult mError{NS_OK};
+};
+
+#endif // nsQuarantinedOutputStream_h__
diff --git a/comm/mailnews/base/src/nsSpamSettings.cpp b/comm/mailnews/base/src/nsSpamSettings.cpp
new file mode 100644
index 0000000000..61dc1e8f43
--- /dev/null
+++ b/comm/mailnews/base/src/nsSpamSettings.cpp
@@ -0,0 +1,806 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsSpamSettings.h"
+#include "nsIFile.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "nsIMsgHdr.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgUtils.h"
+#include "nsMsgFolderFlags.h"
+#include "nsImapCore.h"
+#include "nsIImapIncomingServer.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIStringBundle.h"
+#include "mozilla/Components.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIAbCard.h"
+#include "nsIAbManager.h"
+#include "nsIMsgAccountManager.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+using namespace mozilla::mailnews;
+
+nsSpamSettings::nsSpamSettings() {
+ mLevel = 0;
+ mMoveOnSpam = false;
+ mMoveTargetMode = nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT;
+ mPurge = false;
+ mPurgeInterval = 14; // 14 days
+
+ mServerFilterTrustFlags = 0;
+ mInhibitWhiteListingIdentityUser = false;
+ mInhibitWhiteListingIdentityDomain = false;
+ mUseWhiteList = false;
+ mUseServerFilter = false;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mLogFile));
+ if (NS_SUCCEEDED(rv)) mLogFile->Append(u"junklog.html"_ns);
+}
+
+nsSpamSettings::~nsSpamSettings() {}
+
+NS_IMPL_ISUPPORTS(nsSpamSettings, nsISpamSettings, nsIUrlListener)
+
+NS_IMETHODIMP
+nsSpamSettings::GetLevel(int32_t* aLevel) {
+ NS_ENSURE_ARG_POINTER(aLevel);
+ *aLevel = mLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetLevel(int32_t aLevel) {
+ NS_ASSERTION((aLevel >= 0 && aLevel <= 100), "bad level");
+ mLevel = aLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpamSettings::GetMoveTargetMode(int32_t* aMoveTargetMode) {
+ NS_ENSURE_ARG_POINTER(aMoveTargetMode);
+ *aMoveTargetMode = mMoveTargetMode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetMoveTargetMode(int32_t aMoveTargetMode) {
+ NS_ASSERTION((aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER ||
+ aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT),
+ "bad move target mode");
+ mMoveTargetMode = aMoveTargetMode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetManualMark(bool* aManualMark) {
+ NS_ENSURE_ARG_POINTER(aManualMark);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.manualMark", aManualMark);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetManualMarkMode(int32_t* aManualMarkMode) {
+ NS_ENSURE_ARG_POINTER(aManualMarkMode);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetIntPref("mail.spam.manualMarkMode", aManualMarkMode);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetLoggingEnabled(bool* aLoggingEnabled) {
+ NS_ENSURE_ARG_POINTER(aLoggingEnabled);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.logging.enabled", aLoggingEnabled);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetMarkAsReadOnSpam(bool* aMarkAsReadOnSpam) {
+ NS_ENSURE_ARG_POINTER(aMarkAsReadOnSpam);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.markAsReadOnSpam",
+ aMarkAsReadOnSpam);
+}
+
+NS_IMPL_GETSET(nsSpamSettings, MoveOnSpam, bool, mMoveOnSpam)
+NS_IMPL_GETSET(nsSpamSettings, Purge, bool, mPurge)
+NS_IMPL_GETSET(nsSpamSettings, UseWhiteList, bool, mUseWhiteList)
+NS_IMPL_GETSET(nsSpamSettings, UseServerFilter, bool, mUseServerFilter)
+
+NS_IMETHODIMP nsSpamSettings::GetWhiteListAbURI(nsACString& aWhiteListAbURI) {
+ aWhiteListAbURI = mWhiteListAbURI;
+ return NS_OK;
+}
+NS_IMETHODIMP nsSpamSettings::SetWhiteListAbURI(
+ const nsACString& aWhiteListAbURI) {
+ mWhiteListAbURI = aWhiteListAbURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetActionTargetAccount(
+ nsACString& aActionTargetAccount) {
+ aActionTargetAccount = mActionTargetAccount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetActionTargetAccount(
+ const nsACString& aActionTargetAccount) {
+ mActionTargetAccount = aActionTargetAccount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetActionTargetFolder(
+ nsACString& aActionTargetFolder) {
+ aActionTargetFolder = mActionTargetFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetActionTargetFolder(
+ const nsACString& aActionTargetFolder) {
+ mActionTargetFolder = aActionTargetFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetPurgeInterval(int32_t* aPurgeInterval) {
+ NS_ENSURE_ARG_POINTER(aPurgeInterval);
+ *aPurgeInterval = mPurgeInterval;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetPurgeInterval(int32_t aPurgeInterval) {
+ NS_ASSERTION(aPurgeInterval >= 0, "bad purge interval");
+ mPurgeInterval = aPurgeInterval;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpamSettings::SetLogStream(nsIOutputStream* aLogStream) {
+ // if there is a log stream already, close it
+ if (mLogStream) {
+ // will flush
+ nsresult rv = mLogStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mLogStream = aLogStream;
+ return NS_OK;
+}
+
+#define LOG_HEADER \
+ "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style " \
+ "type=\"text/css\">body{font-family:Consolas,\"Lucida " \
+ "Console\",Monaco,\"Courier " \
+ "New\",Courier,monospace;font-size:small}</style>\n</head>\n<body>\n"
+#define LOG_HEADER_LEN (strlen(LOG_HEADER))
+
+NS_IMETHODIMP
+nsSpamSettings::GetLogStream(nsIOutputStream** aLogStream) {
+ NS_ENSURE_ARG_POINTER(aLogStream);
+
+ nsresult rv;
+
+ if (!mLogStream) {
+ // append to the end of the log file
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mLogStream), mLogFile,
+ PR_CREATE_FILE | PR_WRONLY | PR_APPEND,
+ 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t fileSize;
+ rv = mLogFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the header at the start
+ if (fileSize == 0) {
+ uint32_t writeCount;
+
+ rv = mLogStream->Write(LOG_HEADER, LOG_HEADER_LEN, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_HEADER_LEN,
+ "failed to write out log header");
+ }
+ }
+
+ NS_ADDREF(*aLogStream = mLogStream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::Initialize(nsIMsgIncomingServer* aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsresult rv;
+ int32_t spamLevel;
+ rv = aServer->GetIntValue("spamLevel", &spamLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetLevel(spamLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool moveOnSpam;
+ rv = aServer->GetBoolValue("moveOnSpam", &moveOnSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetMoveOnSpam(moveOnSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t moveTargetMode;
+ rv = aServer->GetIntValue("moveTargetMode", &moveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetMoveTargetMode(moveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString spamActionTargetAccount;
+ rv =
+ aServer->GetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetActionTargetAccount(spamActionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString spamActionTargetFolder;
+ rv = aServer->GetUnicharValue("spamActionTargetFolder",
+ spamActionTargetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetActionTargetFolder(NS_ConvertUTF16toUTF8(spamActionTargetFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useWhiteList;
+ rv = aServer->GetBoolValue("useWhiteList", &useWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetUseWhiteList(useWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString whiteListAbURI;
+ rv = aServer->GetCharValue("whiteListAbURI", whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetWhiteListAbURI(whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool purgeSpam;
+ rv = aServer->GetBoolValue("purgeSpam", &purgeSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPurge(purgeSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t purgeSpamInterval;
+ rv = aServer->GetIntValue("purgeSpamInterval", &purgeSpamInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPurgeInterval(purgeSpamInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useServerFilter;
+ rv = aServer->GetBoolValue("useServerFilter", &useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetUseServerFilter(useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverFilterName;
+ rv = aServer->GetCharValue("serverFilterName", serverFilterName);
+ if (NS_SUCCEEDED(rv)) SetServerFilterName(serverFilterName);
+ int32_t serverFilterTrustFlags = 0;
+ rv = aServer->GetIntValue("serverFilterTrustFlags", &serverFilterTrustFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetServerFilterTrustFlags(serverFilterTrustFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (prefBranch)
+ prefBranch->GetCharPref("mail.trusteddomains", mTrustedMailDomains);
+
+ mWhiteListDirArray.Clear();
+ if (!mWhiteListAbURI.IsEmpty()) {
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsCString> whiteListArray;
+ ParseString(mWhiteListAbURI, ' ', whiteListArray);
+
+ for (uint32_t index = 0; index < whiteListArray.Length(); index++) {
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(whiteListArray[index],
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (directory) mWhiteListDirArray.AppendObject(directory);
+ }
+ }
+
+ // the next two preferences affect whether we try to whitelist our own
+ // address or domain. Spammers send emails with spoofed from address matching
+ // either the email address of the recipient, or the recipient's domain,
+ // hoping to get whitelisted.
+ //
+ // The terms to describe this get wrapped up in chains of negatives. A full
+ // definition of the boolean inhibitWhiteListingIdentityUser is "Suppress
+ // address book whitelisting if the sender matches an identity's email
+ // address"
+
+ rv = aServer->GetBoolValue("inhibitWhiteListingIdentityUser",
+ &mInhibitWhiteListingIdentityUser);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aServer->GetBoolValue("inhibitWhiteListingIdentityDomain",
+ &mInhibitWhiteListingIdentityDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // collect lists of identity users if needed
+ if (mInhibitWhiteListingIdentityDomain || mInhibitWhiteListingIdentityUser) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager(
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString accountKey;
+ if (account) account->GetKey(accountKey);
+
+ // Loop through all accounts, adding emails from this account, as well as
+ // from any accounts that defer to this account.
+ mEmails.Clear();
+ nsTArray<RefPtr<nsIMsgAccount>> accounts;
+ // No sense scanning accounts if we've nothing to match.
+ if (account) {
+ rv = accountManager->GetAccounts(accounts);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ for (auto loopAccount : accounts) {
+ if (!loopAccount) continue;
+ nsAutoCString loopAccountKey;
+ loopAccount->GetKey(loopAccountKey);
+ nsCOMPtr<nsIMsgIncomingServer> loopServer;
+ loopAccount->GetIncomingServer(getter_AddRefs(loopServer));
+ nsAutoCString deferredToAccountKey;
+ if (loopServer)
+ loopServer->GetCharValue("deferred_to_account", deferredToAccountKey);
+
+ // Add the emails for any account that defers to this one, or for the
+ // account itself.
+ if (accountKey.Equals(deferredToAccountKey) ||
+ accountKey.Equals(loopAccountKey)) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ loopAccount->GetIdentities(identities);
+ for (auto identity : identities) {
+ nsAutoCString email;
+ identity->GetEmail(email);
+ if (!email.IsEmpty()) mEmails.AppendElement(email);
+ }
+ }
+ }
+ }
+
+ return UpdateJunkFolderState();
+}
+
+nsresult nsSpamSettings::UpdateJunkFolderState() {
+ nsresult rv;
+
+ // if the spam folder uri changed on us, we need to unset the junk flag
+ // on the old spam folder
+ nsCString newJunkFolderURI;
+ rv = GetSpamFolderURI(newJunkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mCurrentJunkFolderURI.IsEmpty() &&
+ !mCurrentJunkFolderURI.Equals(newJunkFolderURI)) {
+ nsCOMPtr<nsIMsgFolder> oldJunkFolder;
+ rv = FindFolder(mCurrentJunkFolderURI, getter_AddRefs(oldJunkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (oldJunkFolder) {
+ // remove the nsMsgFolderFlags::Junk on the old junk folder
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // (in ClearFlag?) we need to make sure that this folder
+ // is not the junk folder for another account
+ // the same goes for set flag. have fun with all that.
+ oldJunkFolder->ClearFlag(nsMsgFolderFlags::Junk);
+ }
+ }
+
+ mCurrentJunkFolderURI = newJunkFolderURI;
+
+ // only try to create the junk folder if we are moving junk
+ // and we have a non-empty uri
+ if (mMoveOnSpam && !mCurrentJunkFolderURI.IsEmpty()) {
+ // as the url listener, the spam settings will set the
+ // nsMsgFolderFlags::Junk folder flag on the junk mail folder, after it is
+ // created
+ rv = GetOrCreateJunkFolder(mCurrentJunkFolderURI, this);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::Clone(nsISpamSettings* aSpamSettings) {
+ NS_ENSURE_ARG_POINTER(aSpamSettings);
+
+ nsresult rv = aSpamSettings->GetUseWhiteList(&mUseWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (void)aSpamSettings->GetMoveOnSpam(&mMoveOnSpam);
+ (void)aSpamSettings->GetPurge(&mPurge);
+ (void)aSpamSettings->GetUseServerFilter(&mUseServerFilter);
+
+ rv = aSpamSettings->GetPurgeInterval(&mPurgeInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aSpamSettings->GetLevel(&mLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aSpamSettings->GetMoveTargetMode(&mMoveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString actionTargetAccount;
+ rv = aSpamSettings->GetActionTargetAccount(actionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mActionTargetAccount = actionTargetAccount;
+
+ nsCString actionTargetFolder;
+ rv = aSpamSettings->GetActionTargetFolder(actionTargetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mActionTargetFolder = actionTargetFolder;
+
+ nsCString whiteListAbURI;
+ rv = aSpamSettings->GetWhiteListAbURI(whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mWhiteListAbURI = whiteListAbURI;
+
+ aSpamSettings->GetServerFilterName(mServerFilterName);
+ aSpamSettings->GetServerFilterTrustFlags(&mServerFilterTrustFlags);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetSpamFolderURI(nsACString& aSpamFolderURI) {
+ if (mMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER)
+ return GetActionTargetFolder(aSpamFolderURI);
+
+ // if the mode is nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT
+ // the spam folder URI = account uri + "/Junk"
+ nsCString folderURI;
+ nsresult rv = GetActionTargetAccount(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we might be trying to get the old spam folder uri
+ // in order to clear the flag
+ // if we didn't have one, bail out.
+ if (folderURI.IsEmpty()) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // see nsMsgFolder::SetPrettyName() for where the pretty name is set.
+
+ // Check for an existing junk folder - this will do a case-insensitive
+ // search by URI - if we find a junk folder, use its URI.
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ folderURI.AppendLiteral("/Junk");
+ if (NS_SUCCEEDED(server->GetMsgFolderFromURI(nullptr, folderURI,
+ getter_AddRefs(junkFolder))) &&
+ junkFolder)
+ junkFolder->GetURI(folderURI);
+
+ // XXX todo
+ // better not to make base depend in imap
+ // but doing it here, like in nsMsgCopy.cpp
+ // one day, we'll fix this (and nsMsgCopy.cpp) to use GetMsgFolderFromURI()
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ if (imapServer) {
+ // Make sure an specific IMAP folder has correct personal namespace
+ // see bug #197043
+ nsCString folderUriWithNamespace;
+ (void)imapServer->GetUriWithNamespacePrefixIfNecessary(
+ kPersonalNamespace, folderURI, folderUriWithNamespace);
+ if (!folderUriWithNamespace.IsEmpty()) folderURI = folderUriWithNamespace;
+ }
+
+ aSpamFolderURI = folderURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetServerFilterName(nsACString& aFilterName) {
+ aFilterName = mServerFilterName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetServerFilterName(
+ const nsACString& aFilterName) {
+ mServerFilterName = aFilterName;
+ mServerFilterFile = nullptr; // clear out our stored location value
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetServerFilterFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ if (!mServerFilterFile) {
+ nsresult rv;
+ nsAutoCString serverFilterFileName;
+ GetServerFilterName(serverFilterFileName);
+ serverFilterFileName.AppendLiteral(".sfd");
+
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Walk through the list of isp directories
+ nsCOMPtr<nsISimpleEnumerator> ispDirectories;
+ rv = dirSvc->Get(ISP_DIRECTORY_LIST, NS_GET_IID(nsISimpleEnumerator),
+ getter_AddRefs(ispDirectories));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ nsCOMPtr<nsIFile> file;
+ while (NS_SUCCEEDED(ispDirectories->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> elem;
+ ispDirectories->GetNext(getter_AddRefs(elem));
+ file = do_QueryInterface(elem);
+
+ if (file) {
+ // append our desired leaf name then test to see if the file exists. If
+ // it does, we've found mServerFilterFile.
+ file->AppendNative(serverFilterFileName);
+ bool exists;
+ if (NS_SUCCEEDED(file->Exists(&exists)) && exists) {
+ file.swap(mServerFilterFile);
+ break;
+ }
+ } // if file
+ } // until we find the location of mServerFilterName
+ } // if we haven't already stored mServerFilterFile
+
+ NS_IF_ADDREF(*aFile = mServerFilterFile);
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsSpamSettings, ServerFilterTrustFlags, int32_t,
+ mServerFilterTrustFlags)
+
+#define LOG_ENTRY_START_TAG "<p>\n"
+#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG))
+#define LOG_ENTRY_END_TAG "</p>\n"
+#define LOG_ENTRY_END_TAG_LEN (strlen(LOG_ENTRY_END_TAG))
+// Does this need to be localizable?
+#define LOG_ENTRY_TIMESTAMP "[$S] "
+
+NS_IMETHODIMP nsSpamSettings::LogJunkHit(nsIMsgDBHdr* aMsgHdr,
+ bool aMoveMessage) {
+ bool loggingEnabled;
+ nsresult rv = GetLoggingEnabled(&loggingEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!loggingEnabled) return NS_OK;
+
+ PRTime date;
+
+ nsString authorValue;
+ nsString subjectValue;
+ nsString dateValue;
+
+ (void)aMsgHdr->GetDate(&date);
+ PRExplodedTime exploded;
+ PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded);
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ (void)aMsgHdr->GetMime2DecodedAuthor(authorValue);
+ (void)aMsgHdr->GetMime2DecodedSubject(subjectValue);
+
+ nsCString buffer;
+ // this is big enough to hold a log entry.
+ // do this so we avoid growing and copying as we append to the log.
+ buffer.SetCapacity(512);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 3> junkLogDetectFormatStrings = {
+ authorValue, subjectValue, dateValue};
+ nsString junkLogDetectStr;
+ rv = bundle->FormatStringFromName(
+ "junkLogDetectStr", junkLogDetectFormatStrings, junkLogDetectStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += NS_ConvertUTF16toUTF8(junkLogDetectStr);
+ buffer += "\n";
+
+ if (aMoveMessage) {
+ nsCString msgId;
+ aMsgHdr->GetMessageId(getter_Copies(msgId));
+
+ nsCString junkFolderURI;
+ rv = GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 2> logMoveFormatStrings;
+ CopyASCIItoUTF16(msgId, *logMoveFormatStrings.AppendElement());
+ CopyASCIItoUTF16(junkFolderURI, *logMoveFormatStrings.AppendElement());
+ nsString logMoveStr;
+ rv = bundle->FormatStringFromName("logMoveStr", logMoveFormatStrings,
+ logMoveStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += NS_ConvertUTF16toUTF8(logMoveStr);
+ buffer += "\n";
+ }
+
+ return LogJunkString(buffer.get());
+}
+
+NS_IMETHODIMP nsSpamSettings::LogJunkString(const char* string) {
+ bool loggingEnabled;
+ nsresult rv = GetLoggingEnabled(&loggingEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!loggingEnabled) return NS_OK;
+
+ nsString dateValue;
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ nsCString timestampString(LOG_ENTRY_TIMESTAMP);
+ timestampString.ReplaceSubstring("$S",
+ NS_ConvertUTF16toUTF8(dateValue).get());
+
+ nsCOMPtr<nsIOutputStream> logStream;
+ rv = GetLogStream(getter_AddRefs(logStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t writeCount;
+
+ rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN,
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN,
+ "failed to write out start log tag");
+
+ rv = logStream->Write(timestampString.get(), timestampString.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == timestampString.Length(),
+ "failed to write out timestamp");
+
+ // HTML-escape the log for security reasons.
+ // We don't want someone to send us a message with a subject with
+ // HTML tags, especially <script>.
+ nsCString escapedBuffer;
+ nsAppendEscapedHTML(nsDependentCString(string), escapedBuffer);
+
+ uint32_t escapedBufferLen = escapedBuffer.Length();
+ rv = logStream->Write(escapedBuffer.get(), escapedBufferLen, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == escapedBufferLen, "failed to write out log hit");
+
+ rv = logStream->Write(LOG_ENTRY_END_TAG, LOG_ENTRY_END_TAG_LEN, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_END_TAG_LEN,
+ "failed to write out end log tag");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::OnStartRunningUrl(nsIURI* aURL) {
+ // do nothing
+ // all the action happens in OnStopRunningUrl()
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::OnStopRunningUrl(nsIURI* aURL,
+ nsresult exitCode) {
+ nsCString junkFolderURI;
+ nsresult rv = GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (junkFolderURI.IsEmpty()) return NS_ERROR_UNEXPECTED;
+
+ // when we get here, the folder should exist.
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ rv = GetExistingFolder(junkFolderURI, getter_AddRefs(junkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = junkFolder->SetFlag(nsMsgFolderFlags::Junk);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::CheckWhiteList(nsIMsgDBHdr* aMsgHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false; // default in case of error or no whitelisting
+
+ if (!mUseWhiteList ||
+ (!mWhiteListDirArray.Count() && mTrustedMailDomains.IsEmpty()))
+ return NS_OK;
+
+ // do per-message processing
+
+ nsCString author;
+ aMsgHdr->GetAuthor(getter_Copies(author));
+
+ nsAutoCString authorEmailAddress;
+ ExtractEmail(EncodedHeader(author), authorEmailAddress);
+
+ if (authorEmailAddress.IsEmpty()) return NS_OK;
+
+ // should we skip whitelisting for the identity email?
+ if (mInhibitWhiteListingIdentityUser) {
+ for (uint32_t i = 0; i < mEmails.Length(); ++i) {
+ if (mEmails[i].Equals(authorEmailAddress,
+ nsCaseInsensitiveCStringComparator))
+ return NS_OK;
+ }
+ }
+
+ if (!mTrustedMailDomains.IsEmpty() || mInhibitWhiteListingIdentityDomain) {
+ nsAutoCString domain;
+ int32_t atPos = authorEmailAddress.FindChar('@');
+ if (atPos >= 0) domain = Substring(authorEmailAddress, atPos + 1);
+ if (!domain.IsEmpty()) {
+ if (!mTrustedMailDomains.IsEmpty() &&
+ MsgHostDomainIsTrusted(domain, mTrustedMailDomains)) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ if (mInhibitWhiteListingIdentityDomain) {
+ for (uint32_t i = 0; i < mEmails.Length(); ++i) {
+ nsAutoCString identityDomain;
+ int32_t atPos = mEmails[i].FindChar('@');
+ if (atPos >= 0) {
+ identityDomain = Substring(mEmails[i], atPos + 1);
+ if (identityDomain.Equals(domain,
+ nsCaseInsensitiveCStringComparator))
+ return NS_OK; // don't whitelist
+ }
+ }
+ }
+ }
+ }
+
+ if (mWhiteListDirArray.Count()) {
+ nsCOMPtr<nsIAbCard> cardForAddress;
+ for (int32_t index = 0;
+ index < mWhiteListDirArray.Count() && !cardForAddress; index++) {
+ mWhiteListDirArray[index]->CardForEmailAddress(
+ authorEmailAddress, getter_AddRefs(cardForAddress));
+ }
+ if (cardForAddress) {
+ *aResult = true;
+ return NS_OK;
+ }
+ }
+ return NS_OK; // default return is false
+}
diff --git a/comm/mailnews/base/src/nsSpamSettings.h b/comm/mailnews/base/src/nsSpamSettings.h
new file mode 100644
index 0000000000..56725f7fe4
--- /dev/null
+++ b/comm/mailnews/base/src/nsSpamSettings.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsSpamSettings_h__
+#define nsSpamSettings_h__
+
+#include "nsCOMPtr.h"
+#include "nsISpamSettings.h"
+#include "nsString.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIUrlListener.h"
+#include "nsCOMArray.h"
+#include "nsIAbDirectory.h"
+#include "nsTArray.h"
+
+class nsSpamSettings : public nsISpamSettings, public nsIUrlListener {
+ public:
+ nsSpamSettings();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISPAMSETTINGS
+ NS_DECL_NSIURLLISTENER
+
+ private:
+ virtual ~nsSpamSettings();
+
+ nsCOMPtr<nsIOutputStream> mLogStream;
+ nsCOMPtr<nsIFile> mLogFile;
+
+ int32_t mLevel;
+ int32_t mPurgeInterval;
+ int32_t mMoveTargetMode;
+
+ bool mPurge;
+ bool mUseWhiteList;
+ bool mMoveOnSpam;
+ bool mUseServerFilter;
+
+ nsCString mActionTargetAccount;
+ nsCString mActionTargetFolder;
+ nsCString mWhiteListAbURI;
+ // used to detect changes to the spam folder in ::initialize
+ nsCString mCurrentJunkFolderURI;
+
+ nsCString mServerFilterName;
+ nsCOMPtr<nsIFile> mServerFilterFile;
+ int32_t mServerFilterTrustFlags;
+
+ // array of address directories to use in junk whitelisting
+ nsCOMArray<nsIAbDirectory> mWhiteListDirArray;
+ // mail domains to use in junk whitelisting
+ nsCString mTrustedMailDomains;
+ // should we inhibit whitelisting address of identity?
+ bool mInhibitWhiteListingIdentityUser;
+ // should we inhibit whitelisting domain of identity?
+ bool mInhibitWhiteListingIdentityDomain;
+ // email addresses associated with this server
+ nsTArray<nsCString> mEmails;
+
+ // helper routine used by Initialize which unsets the junk flag on the
+ // previous junk folder for this account, and sets it on the new junk folder.
+ nsresult UpdateJunkFolderState();
+};
+
+#endif /* nsSpamSettings_h__ */
diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.cpp b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp
new file mode 100644
index 0000000000..df85a7bcee
--- /dev/null
+++ b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp
@@ -0,0 +1,254 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsStatusBarBiffManager.h"
+#include "nsMsgBiffManager.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIObserverService.h"
+#include "nsIWindowMediator.h"
+#include "nsIMsgMailSession.h"
+#include "MailNewsTypes.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsMsgDBFolder.h"
+#include "nsIFileChannel.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIFile.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Services.h"
+
+// QueryInterface, AddRef, and Release
+//
+NS_IMPL_ISUPPORTS(nsStatusBarBiffManager, nsIStatusBarBiffManager,
+ nsIFolderListener, nsIObserver)
+
+nsStatusBarBiffManager::nsStatusBarBiffManager()
+ : mInitialized(false),
+ mCurrentBiffState(nsIMsgFolder::nsMsgBiffState_Unknown) {}
+
+nsStatusBarBiffManager::~nsStatusBarBiffManager() {}
+
+#define NEW_MAIL_PREF_BRANCH "mail.biff."
+#define CHAT_PREF_BRANCH "mail.chat."
+#define FEED_PREF_BRANCH "mail.feed."
+#define PREF_PLAY_SOUND "play_sound"
+#define PREF_SOUND_URL "play_sound.url"
+#define PREF_SOUND_TYPE "play_sound.type"
+#define SYSTEM_SOUND_TYPE 0
+#define CUSTOM_SOUND_TYPE 1
+#define PREF_CHAT_ENABLED "mail.chat.enabled"
+#define PLAY_CHAT_NOTIFICATION_SOUND "play-chat-notification-sound"
+
+nsresult nsStatusBarBiffManager::Init() {
+ if (mInitialized) return NS_ERROR_ALREADY_INITIALIZED;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ mailSession->AddFolderListener(this, nsIFolderListener::intPropertyChanged);
+
+ nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool chatEnabled = false;
+ if (NS_SUCCEEDED(rv)) rv = pref->GetBoolPref(PREF_CHAT_ENABLED, &chatEnabled);
+ if (NS_SUCCEEDED(rv) && chatEnabled) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService)
+ observerService->AddObserver(this, PLAY_CHAT_NOTIFICATION_SOUND, false);
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+nsresult nsStatusBarBiffManager::PlayBiffSound(const char* aPrefBranch) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefSvc =
+ (do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrefBranch> pref;
+ rv = prefSvc->GetBranch(aPrefBranch, getter_AddRefs(pref));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool playSound;
+ if (mServerType.EqualsLiteral("rss")) {
+ nsCOMPtr<nsIPrefBranch> prefFeed;
+ rv = prefSvc->GetBranch(FEED_PREF_BRANCH, getter_AddRefs(prefFeed));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = prefFeed->GetBoolPref(PREF_PLAY_SOUND, &playSound);
+ } else {
+ rv = pref->GetBoolPref(PREF_PLAY_SOUND, &playSound);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!playSound) return NS_OK;
+
+ // lazily create the sound instance
+ if (!mSound) mSound = do_CreateInstance("@mozilla.org/sound;1");
+
+ int32_t soundType = SYSTEM_SOUND_TYPE;
+ rv = pref->GetIntPref(PREF_SOUND_TYPE, &soundType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifndef XP_MACOSX
+ bool customSoundPlayed = false;
+#endif
+
+ if (soundType == CUSTOM_SOUND_TYPE) {
+ nsCString soundURLSpec;
+ rv = pref->GetCharPref(PREF_SOUND_URL, soundURLSpec);
+
+ if (NS_SUCCEEDED(rv) && !soundURLSpec.IsEmpty()) {
+ if (!strncmp(soundURLSpec.get(), "file://", 7)) {
+ nsCOMPtr<nsIURI> fileURI;
+ rv = NS_NewURI(getter_AddRefs(fileURI), soundURLSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> soundURL = do_QueryInterface(fileURI, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> soundFile;
+ rv = soundURL->GetFile(getter_AddRefs(soundFile));
+ if (NS_SUCCEEDED(rv)) {
+ bool soundFileExists = false;
+ rv = soundFile->Exists(&soundFileExists);
+ if (NS_SUCCEEDED(rv) && soundFileExists) {
+ rv = mSound->Play(soundURL);
+#ifndef XP_MACOSX
+ if (NS_SUCCEEDED(rv)) customSoundPlayed = true;
+#endif
+ }
+ }
+ }
+ }
+ // XXX TODO: See if we can create a nsIFile using the string as a native
+ // path.
+ }
+ }
+#ifndef XP_MACOSX
+ // if nothing played, play the default system sound
+ if (!customSoundPlayed) {
+ rv = mSound->PlayEventSound(nsISound::EVENT_NEW_MAIL_RECEIVED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+#endif
+ return rv;
+}
+
+// nsIFolderListener methods....
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnMessageAdded(nsIMsgFolder* parent, nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ const nsACString& oldValue,
+ const nsACString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderIntPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ int64_t oldValue,
+ int64_t newValue) {
+ if (property.Equals(kBiffState) && mCurrentBiffState != newValue) {
+ // if we got new mail, attempt to play a sound.
+ // if we fail along the way, don't return.
+ // we still need to update the UI.
+ if (newValue == nsIMsgFolder::nsMsgBiffState_NewMail) {
+ // Get the folder's server type.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = folder->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) server->GetType(mServerType);
+
+ // if we fail to play the biff sound, keep going.
+ (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH);
+ }
+ mCurrentBiffState = newValue;
+
+ // don't care if notification fails
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (observerService)
+ observerService->NotifyObservers(
+ static_cast<nsIStatusBarBiffManager*>(this),
+ "mail:biff-state-changed", nullptr);
+ } else if (property.Equals(kNewMailReceived)) {
+ (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderBoolPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ bool oldValue,
+ bool newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderPropertyFlagChanged(nsIMsgDBHdr* msg,
+ const nsACString& property,
+ uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderEvent(nsIMsgFolder* folder,
+ const nsACString& event) {
+ return NS_OK;
+}
+
+// nsIObserver implementation
+NS_IMETHODIMP
+nsStatusBarBiffManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ return PlayBiffSound(CHAT_PREF_BRANCH);
+}
+
+// nsIStatusBarBiffManager method....
+NS_IMETHODIMP
+nsStatusBarBiffManager::GetBiffState(int32_t* aBiffState) {
+ NS_ENSURE_ARG_POINTER(aBiffState);
+ *aBiffState = mCurrentBiffState;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.h b/comm/mailnews/base/src/nsStatusBarBiffManager.h
new file mode 100644
index 0000000000..bd4d5d5bed
--- /dev/null
+++ b/comm/mailnews/base/src/nsStatusBarBiffManager.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsStatusBarBiffManager_h__
+#define nsStatusBarBiffManager_h__
+
+#include "nsIStatusBarBiffManager.h"
+
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsISound.h"
+#include "nsIObserver.h"
+
+class nsStatusBarBiffManager : public nsIStatusBarBiffManager,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFOLDERLISTENER
+ NS_DECL_NSISTATUSBARBIFFMANAGER
+ NS_DECL_NSIOBSERVER
+
+ nsStatusBarBiffManager();
+ nsresult Init();
+
+ private:
+ virtual ~nsStatusBarBiffManager();
+
+ bool mInitialized;
+ int32_t mCurrentBiffState;
+ nsCString mServerType;
+ nsCOMPtr<nsISound> mSound;
+ nsresult PlayBiffSound(const char* aPrefBranch);
+};
+
+#endif // nsStatusBarBiffManager_h__
diff --git a/comm/mailnews/base/src/nsStopwatch.cpp b/comm/mailnews/base/src/nsStopwatch.cpp
new file mode 100644
index 0000000000..585450d113
--- /dev/null
+++ b/comm/mailnews/base/src/nsStopwatch.cpp
@@ -0,0 +1,164 @@
+/* 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/. */
+
+#include "nsStopwatch.h"
+
+#include <stdio.h>
+#include <time.h>
+#if defined(XP_UNIX)
+# include <unistd.h>
+# include <sys/times.h>
+# include <sys/time.h>
+# include <errno.h>
+#elif defined(XP_WIN)
+# include "windows.h"
+#endif // elif defined(XP_WIN)
+
+#include "nsMemory.h"
+/*
+ * This basis for the logic in this file comes from (will used to come from):
+ * (mozilla/)modules/libutil/public/stopwatch.cpp.
+ *
+ * It was no longer used in the mozilla tree, and is being migrated to
+ * comm-central where we actually have a need for it. ("Being" in the sense
+ * that it will not be removed immediately from mozilla-central.)
+ *
+ * Simplification and general clean-up has been performed and the fix for
+ * bug 96669 has been integrated.
+ */
+
+NS_IMPL_ISUPPORTS(nsStopwatch, nsIStopwatch)
+
+#if defined(XP_UNIX)
+/** the number of ticks per second */
+static double gTicks = 0;
+# define MICRO_SECONDS_TO_SECONDS_MULT static_cast<double>(1.0e-6)
+#elif defined(WIN32)
+# ifdef DEBUG
+# include "nsPrintfCString.h"
+# endif
+// 1 tick per 100ns = 10 per us = 10 * 1,000 per ms = 10 * 1,000 * 1,000 per
+// sec.
+# define WIN32_TICK_RESOLUTION static_cast<double>(1.0e-7)
+// subtract off to get to the unix epoch
+# define UNIX_EPOCH_IN_FILE_TIME 116444736000000000L
+#endif // elif defined(WIN32)
+
+nsStopwatch::nsStopwatch()
+ : fTotalRealTimeSecs(0.0), fTotalCpuTimeSecs(0.0), fRunning(false) {
+#if defined(XP_UNIX)
+ // idempotent in the event of a race under all coherency models
+ if (!gTicks) {
+ // we need to clear errno because sysconf's spec says it leaves it the same
+ // on success and only sets it on failure.
+ errno = 0;
+ gTicks = (clock_t)sysconf(_SC_CLK_TCK);
+ // in event of failure, pick an arbitrary value so we don't divide by zero.
+ if (errno) gTicks = 1000000L;
+ }
+#endif
+}
+
+nsStopwatch::~nsStopwatch() {}
+
+NS_IMETHODIMP nsStopwatch::Start() {
+ fTotalRealTimeSecs = 0.0;
+ fTotalCpuTimeSecs = 0.0;
+ return Resume();
+}
+
+NS_IMETHODIMP nsStopwatch::Stop() {
+ fStopRealTimeSecs = GetRealTime();
+ fStopCpuTimeSecs = GetCPUTime();
+ if (fRunning) {
+ fTotalCpuTimeSecs += fStopCpuTimeSecs - fStartCpuTimeSecs;
+ fTotalRealTimeSecs += fStopRealTimeSecs - fStartRealTimeSecs;
+ }
+ fRunning = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::Resume() {
+ if (!fRunning) {
+ fStartRealTimeSecs = GetRealTime();
+ fStartCpuTimeSecs = GetCPUTime();
+ }
+ fRunning = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::GetCpuTimeSeconds(double* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = fTotalCpuTimeSecs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::GetRealTimeSeconds(double* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = fTotalRealTimeSecs;
+ return NS_OK;
+}
+
+double nsStopwatch::GetRealTime() {
+#if defined(XP_UNIX)
+ struct timeval t;
+ gettimeofday(&t, NULL);
+ return t.tv_sec + t.tv_usec * MICRO_SECONDS_TO_SECONDS_MULT;
+#elif defined(WIN32)
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftRealTime; // time the process has spent in kernel mode
+ SYSTEMTIME st;
+ GetSystemTime(&st);
+ SystemTimeToFileTime(&st, &ftRealTime.ftFileTime);
+ return (ftRealTime.ftInt64 - UNIX_EPOCH_IN_FILE_TIME) * WIN32_TICK_RESOLUTION;
+#else
+# error "nsStopwatch not supported on this platform."
+#endif
+}
+
+double nsStopwatch::GetCPUTime() {
+#if defined(XP_UNIX)
+ struct tms cpt;
+ times(&cpt);
+ return (double)(cpt.tms_utime + cpt.tms_stime) / gTicks;
+#elif defined(WIN32)
+ FILETIME ftCreate, // when the process was created
+ ftExit; // when the process exited
+
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftKernel; // time the process has spent in kernel mode
+
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftUser; // time the process has spent in user mode
+
+ HANDLE hProcess = GetCurrentProcess();
+# ifdef DEBUG
+ BOOL ret =
+# endif
+ GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel.ftFileTime,
+ &ftUser.ftFileTime);
+# ifdef DEBUG
+ if (!ret)
+ NS_ERROR(nsPrintfCString("GetProcessTimes() failed, error=0x%lx.",
+ GetLastError())
+ .get());
+# endif
+
+ /*
+ * Process times are returned in a 64-bit structure, as the number of
+ * 100 nanosecond ticks since 1 January 1601. User mode and kernel mode
+ * times for this process are in separate 64-bit structures.
+ * Add them and convert the result to seconds.
+ */
+ return (ftKernel.ftInt64 + ftUser.ftInt64) * WIN32_TICK_RESOLUTION;
+#else
+# error "nsStopwatch not supported on this platform."
+#endif
+}
diff --git a/comm/mailnews/base/src/nsStopwatch.h b/comm/mailnews/base/src/nsStopwatch.h
new file mode 100644
index 0000000000..9873519809
--- /dev/null
+++ b/comm/mailnews/base/src/nsStopwatch.h
@@ -0,0 +1,51 @@
+/* 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/. */
+
+#ifndef _nsStopwatch_h_
+#define _nsStopwatch_h_
+
+#include "nsIStopwatch.h"
+
+#include "msgCore.h"
+
+#define NS_STOPWATCH_CID \
+ { \
+ 0x6ef7eafd, 0x72d0, 0x4c56, { \
+ 0x94, 0x09, 0x67, 0xe1, 0x6d, 0x0f, 0x25, 0x5b \
+ } \
+ }
+
+#define NS_STOPWATCH_CONTRACTID "@mozilla.org/stopwatch;1"
+
+class nsStopwatch : public nsIStopwatch {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTOPWATCH
+
+ nsStopwatch();
+
+ private:
+ virtual ~nsStopwatch();
+
+ /// Wall-clock start time in seconds since unix epoch.
+ double fStartRealTimeSecs;
+ /// Wall-clock stop time in seconds since unix epoch.
+ double fStopRealTimeSecs;
+ /// CPU-clock start time in seconds (of CPU time used since app start)
+ double fStartCpuTimeSecs;
+ /// CPU-clock stop time in seconds (of CPU time used since app start)
+ double fStopCpuTimeSecs;
+ /// Total wall-clock time elapsed in seconds.
+ double fTotalRealTimeSecs;
+ /// Total CPU time elapsed in seconds.
+ double fTotalCpuTimeSecs;
+
+ /// Is the timer running?
+ bool fRunning;
+
+ static double GetRealTime();
+ static double GetCPUTime();
+};
+
+#endif // _nsStopwatch_h_
diff --git a/comm/mailnews/base/src/nsSubscribableServer.cpp b/comm/mailnews/base/src/nsSubscribableServer.cpp
new file mode 100644
index 0000000000..043ef2e4d3
--- /dev/null
+++ b/comm/mailnews/base/src/nsSubscribableServer.cpp
@@ -0,0 +1,867 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsSubscribableServer.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIServiceManager.h"
+#include "nsMsgI18N.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTreeColumns.h"
+#include "mozilla/dom/DataTransfer.h"
+
+/**
+ * The basic structure for the tree of the implementation.
+ *
+ * These elements are stored in reverse alphabetical order.
+ * Each node owns its children and subsequent siblings.
+ */
+struct SubscribeTreeNode {
+ nsCString name;
+ nsCString path;
+ bool isSubscribed;
+ SubscribeTreeNode* prevSibling;
+ SubscribeTreeNode* nextSibling;
+ SubscribeTreeNode* firstChild;
+ SubscribeTreeNode* lastChild;
+ SubscribeTreeNode* parent;
+ // Stores the child considered most likely to be next searched for - usually
+ // the most recently-added child. If names match the search can early-out.
+ SubscribeTreeNode* cachedChild;
+ bool isSubscribable;
+ bool isOpen;
+};
+
+nsSubscribableServer::nsSubscribableServer(void) {
+ mDelimiter = '.';
+ mShowFullName = true;
+ mTreeRoot = nullptr;
+ mStopped = false;
+}
+
+nsresult nsSubscribableServer::Init() { return NS_OK; }
+
+nsSubscribableServer::~nsSubscribableServer(void) {
+ FreeRows();
+ if (mTreeRoot) {
+ FreeSubtree(mTreeRoot);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsSubscribableServer, nsISubscribableServer, nsITreeView)
+
+NS_IMETHODIMP
+nsSubscribableServer::SetIncomingServer(nsIMsgIncomingServer* aServer) {
+ if (!aServer) {
+ mIncomingServerUri.AssignLiteral("");
+ mServerType.Truncate();
+ return NS_OK;
+ }
+ aServer->GetType(mServerType);
+
+ // We intentionally do not store a pointer to the aServer here
+ // as it would create reference loops, because nsIImapIncomingServer
+ // and nsINntpIncomingServer keep a reference to an internal
+ // nsISubscribableServer object.
+ // We only need the URI of the server anyway.
+ return aServer->GetServerURI(mIncomingServerUri);
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetDelimiter(char* aDelimiter) {
+ NS_ENSURE_ARG_POINTER(aDelimiter);
+ *aDelimiter = mDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetDelimiter(char aDelimiter) {
+ mDelimiter = aDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetAsSubscribed(const nsACString& path) {
+ nsresult rv = NS_OK;
+
+ SubscribeTreeNode* node = nullptr;
+ rv = FindAndCreateNode(path, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+ node->isSubscribable = true;
+ node->isSubscribed = true;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::AddTo(const nsACString& aName, bool aAddAsSubscribed,
+ bool aSubscribable, bool aChangeIfExists) {
+ nsresult rv = NS_OK;
+
+ if (mStopped) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SubscribeTreeNode* node = nullptr;
+
+ // todo, shouldn't we pass in aAddAsSubscribed, for the
+ // default value if we create it?
+ rv = FindAndCreateNode(aName, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ if (aChangeIfExists) {
+ node->isSubscribed = aAddAsSubscribed;
+ }
+
+ node->isSubscribable = aSubscribable;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetState(const nsACString& aPath, bool aState,
+ bool* aStateChanged) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(!aPath.IsEmpty() && aStateChanged, "no path or stateChanged");
+ if (aPath.IsEmpty() || !aStateChanged) return NS_ERROR_NULL_POINTER;
+
+ NS_ASSERTION(mozilla::IsUtf8(aPath), "aPath is not in UTF-8");
+
+ *aStateChanged = false;
+
+ SubscribeTreeNode* node = nullptr;
+ rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(node->isSubscribable, "fix this");
+ if (!node->isSubscribable) {
+ return NS_OK;
+ }
+
+ if (node->isSubscribed == aState) {
+ return NS_OK;
+ } else {
+ node->isSubscribed = aState;
+ *aStateChanged = true;
+
+ // Repaint the tree row to show/clear the check mark.
+ if (mTree) {
+ bool dummy;
+ int32_t index = GetRow(node, &dummy);
+ if (index >= 0) mTree->InvalidateRow(index);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSubscribeListener(nsISubscribeListener* aListener) {
+ mSubscribeListener = aListener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSubscribeListener(nsISubscribeListener** aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ NS_IF_ADDREF(*aListener = mSubscribeListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SubscribeCleanup() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StartPopulatingWithUri(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer,
+ const nsACString& uri) {
+ mStopped = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StartPopulating(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer,
+ bool aGetOnlyNew /*ignored*/) {
+ mStopped = false;
+
+ FreeRows();
+ if (mTreeRoot) {
+ FreeSubtree(mTreeRoot);
+ mTreeRoot = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StopPopulating(nsIMsgWindow* aMsgWindow) {
+ mStopped = true;
+
+ FreeRows();
+
+ if (mTreeRoot) {
+ SubscribeTreeNode* node = mTreeRoot->lastChild;
+ // Add top level items as closed.
+ while (node) {
+ node->isOpen = false;
+ mRowMap.AppendElement(node);
+ node = node->prevSibling;
+ }
+
+ // Invalidate the whole thing.
+ if (mTree) mTree->RowCountChanged(0, mRowMap.Length());
+
+ // Open all the top level items if they are containers.
+ uint32_t topRows = mRowMap.Length();
+ for (int32_t i = topRows - 1; i >= 0; i--) {
+ bool isContainer = false;
+ IsContainer(i, &isContainer);
+ if (isContainer) ToggleOpenState(i);
+ }
+ }
+
+ if (mSubscribeListener) mSubscribeListener->OnDonePopulating();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::UpdateSubscribed() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Subscribe(const char16_t* aName) {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Unsubscribe(const char16_t* aName) {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetShowFullName(bool showFullName) {
+ mShowFullName = showFullName;
+ return NS_OK;
+}
+
+void nsSubscribableServer::FreeSubtree(SubscribeTreeNode* node) {
+ // NOTE: this doesn't cleanly detach each node, but that's not an issue
+ // here. This is a nuking, not a surgical removal.
+
+ // recursively free the children
+ if (node->firstChild) {
+ // will free node->firstChild
+ FreeSubtree(node->firstChild);
+ node->firstChild = nullptr;
+ }
+
+ // recursively free the siblings
+ if (node->nextSibling) {
+ FreeSubtree(node->nextSibling);
+ node->nextSibling = nullptr;
+ }
+
+ delete node;
+}
+
+void nsSubscribableServer::FreeRows() {
+ int32_t rowCount = mRowMap.Length();
+ mRowMap.Clear();
+ if (mTree) mTree->RowCountChanged(0, -rowCount);
+}
+
+SubscribeTreeNode* nsSubscribableServer::CreateNode(SubscribeTreeNode* parent,
+ nsACString const& name,
+ nsACString const& path) {
+ SubscribeTreeNode* node = new SubscribeTreeNode();
+
+ node->name.Assign(name);
+ node->path.Assign(path);
+ node->parent = parent;
+ node->prevSibling = nullptr;
+ node->nextSibling = nullptr;
+ node->firstChild = nullptr;
+ node->lastChild = nullptr;
+ node->isSubscribed = false;
+ node->isSubscribable = false;
+ node->isOpen = true;
+ node->cachedChild = nullptr;
+ if (parent) {
+ parent->cachedChild = node;
+ }
+ return node;
+}
+
+nsresult nsSubscribableServer::AddChildNode(SubscribeTreeNode* parent,
+ nsACString const& name,
+ const nsACString& path,
+ SubscribeTreeNode** child) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(parent && child && !name.IsEmpty() && !path.IsEmpty(),
+ "parent, child or name is null");
+ if (!parent || !child || name.IsEmpty() || path.IsEmpty())
+ return NS_ERROR_NULL_POINTER;
+
+ // Adding to a node with no children?
+ if (!parent->firstChild) {
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+ parent->firstChild = *child;
+ parent->lastChild = *child;
+ return NS_OK;
+ }
+
+ // Did we just add a child of this name?
+ if (parent->cachedChild) {
+ if (parent->cachedChild->name.Equals(name)) {
+ *child = parent->cachedChild;
+ return NS_OK;
+ }
+ }
+
+ SubscribeTreeNode* current = parent->firstChild;
+
+ /*
+ * Insert in reverse alphabetical order.
+ * This will reduce the # of strcmps since this is faster assuming:
+ * 1) the hostinfo.dat feeds us the groups in alphabetical order
+ * since we control the hostinfo.dat file, we can guarantee this.
+ * 2) the server gives us the groups in alphabetical order
+ * we can't guarantee this, but it seems to be a common thing.
+ *
+ * Because we have firstChild, lastChild, nextSibling, prevSibling,
+ * we can efficiently reverse the order when dumping to hostinfo.dat
+ * or to GetTargets().
+ */
+ int32_t compare = Compare(current->name, name);
+
+ while (current && (compare != 0)) {
+ if (compare < 0) {
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*child)->nextSibling = current;
+ (*child)->prevSibling = current->prevSibling;
+ current->prevSibling = (*child);
+ if (!(*child)->prevSibling) {
+ parent->firstChild = (*child);
+ } else {
+ (*child)->prevSibling->nextSibling = (*child);
+ }
+
+ return NS_OK;
+ }
+ current = current->nextSibling;
+ if (current) {
+ NS_ASSERTION(!current->name.IsEmpty(), "no name!");
+ compare = Compare(current->name, name);
+ } else {
+ compare = -1; // anything but 0, since that would be a match
+ }
+ }
+
+ if (compare == 0) {
+ // already exists;
+ *child = current;
+ parent->cachedChild = *child;
+ return NS_OK;
+ }
+
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+
+ (*child)->prevSibling = parent->lastChild;
+ (*child)->nextSibling = nullptr;
+ parent->lastChild->nextSibling = *child;
+ parent->lastChild = *child;
+
+ return NS_OK;
+}
+
+nsresult nsSubscribableServer::FindAndCreateNode(const nsACString& aPath,
+ SubscribeTreeNode** aResult) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(aResult, "no result");
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!mTreeRoot) {
+ // the root has no parent, and its name is server uri
+ mTreeRoot = CreateNode(nullptr, mIncomingServerUri, EmptyCString());
+ }
+
+ if (aPath.IsEmpty()) {
+ *aResult = mTreeRoot;
+ return NS_OK;
+ }
+
+ *aResult = nullptr;
+
+ SubscribeTreeNode* parent = mTreeRoot;
+ SubscribeTreeNode* child = nullptr;
+
+ uint32_t tokenStart = 0;
+ // Special case paths that start with the hierarchy delimiter.
+ // We want to include that delimiter in the first token name.
+ // So start from position 1.
+ int32_t tokenEnd = aPath.FindChar(mDelimiter, tokenStart + 1);
+ while (true) {
+ if (tokenEnd == kNotFound) {
+ if (tokenStart >= aPath.Length()) break;
+ tokenEnd = aPath.Length();
+ }
+ nsCString token(Substring(aPath, tokenStart, tokenEnd - tokenStart));
+ rv = AddChildNode(parent, token, Substring(aPath, 0, tokenEnd), &child);
+ if (NS_FAILED(rv)) return rv;
+ tokenStart = tokenEnd + 1;
+ tokenEnd = aPath.FindChar(mDelimiter, tokenStart);
+ parent = child;
+ }
+
+ // the last child we add is the result
+ *aResult = child;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::HasChildren(const nsACString& aPath, bool* aHasChildren) {
+ NS_ASSERTION(aHasChildren, "no hasChildren");
+ NS_ENSURE_ARG_POINTER(aHasChildren);
+
+ *aHasChildren = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aHasChildren = (node->firstChild != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSubscribed(const nsACString& aPath,
+ bool* aIsSubscribed) {
+ NS_ENSURE_ARG_POINTER(aIsSubscribed);
+
+ *aIsSubscribed = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aIsSubscribed = node->isSubscribed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSubscribable(const nsACString& aPath,
+ bool* aIsSubscribable) {
+ NS_ENSURE_ARG_POINTER(aIsSubscribable);
+
+ *aIsSubscribable = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aIsSubscribable = node->isSubscribable;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetLeafName(const nsACString& aPath,
+ nsAString& aLeafName) {
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ // XXX TODO FIXME
+ // I'm assuming that mShowFullName is true for NNTP, false for IMAP.
+ // For imap, the node name is in MUTF-7; for news, the path is escaped UTF-8.
+ // When we switch to using the tree, this hack will go away.
+ if (mShowFullName) {
+ return NS_MsgDecodeUnescapeURLPath(aPath, aLeafName);
+ }
+
+ return CopyFolderNameToUTF16(node->name, aLeafName);
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetFirstChildURI(const nsACString& aPath,
+ nsACString& aResult) {
+ aResult.Truncate();
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ // no children
+ if (!node->firstChild) return NS_ERROR_FAILURE;
+
+ aResult.Assign(node->firstChild->path);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetChildURIs(const nsACString& aPath,
+ nsTArray<nsCString>& aResult) {
+ aResult.Clear();
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(mTreeRoot, "no tree root!");
+ if (!mTreeRoot) return NS_ERROR_UNEXPECTED;
+
+ // We inserted them in reverse alphabetical order.
+ // So pull them out in reverse to get the right order
+ // in the subscribe dialog.
+ SubscribeTreeNode* current = node->lastChild;
+ // return failure if there are no children.
+ if (!current) return NS_ERROR_FAILURE;
+
+ while (current) {
+ NS_ASSERTION(!current->name.IsEmpty(), "no name");
+ if (current->name.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.AppendElement(current->path);
+ current = current->prevSibling;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CommitSubscribeChanges() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSearchValue(const nsAString& aSearchValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSupportsSubscribeSearch(bool* retVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetFolderView(nsITreeView** aView) {
+ NS_ENSURE_ARG_POINTER(aView);
+ return this->QueryInterface(NS_GET_IID(nsITreeView), (void**)aView);
+}
+
+int32_t nsSubscribableServer::GetRow(SubscribeTreeNode* node, bool* open) {
+ int32_t parentRow = -1;
+ if (node->parent) parentRow = GetRow(node->parent, open);
+
+ // If the parent wasn't opened, we're not in the row map
+ if (open && *open == false) return -1;
+
+ if (open) *open = node->isOpen;
+
+ for (uint32_t row = parentRow + 1; row < mRowMap.Length(); row++) {
+ if (mRowMap[row] == node) return row;
+ }
+
+ // Apparently, we're not in the map
+ return -1;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSelection(nsITreeSelection** selection) {
+ NS_IF_ADDREF(*selection = mSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSelection(nsITreeSelection* selection) {
+ mSelection = selection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetRowCount(int32_t* rowCount) {
+ *rowCount = mRowMap.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetTree(mozilla::dom::XULTreeElement* aTree) {
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainer(int32_t aIndex, bool* retval) {
+ *retval = !!mRowMap[aIndex]->firstChild;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainerEmpty(int32_t aIndex, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainerOpen(int32_t aIndex, bool* retval) {
+ *retval = mRowMap[aIndex]->isOpen;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetParentIndex(int32_t aIndex, int32_t* retval) {
+ SubscribeTreeNode* parent = mRowMap[aIndex]->parent;
+ if (!parent) {
+ *retval = -1;
+ return NS_OK;
+ }
+
+ int32_t index;
+ for (index = aIndex - 1; index >= 0; index--) {
+ if (mRowMap[index] == parent) {
+ *retval = index;
+ return NS_OK;
+ }
+ }
+ *retval = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* retval) {
+ // This looks odd, but is correct. Using ->nextSibling gives a bad tree.
+ *retval = !!mRowMap[aRowIndex]->prevSibling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetLevel(int32_t aIndex, int32_t* retval) {
+ // When starting with -2, we increase twice and return 0 for a top level node.
+ int32_t level = -2;
+ SubscribeTreeNode* node = mRowMap[aIndex];
+ while (node) {
+ node = node->parent;
+ level++;
+ }
+
+ *retval = level;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::ToggleOpenState(int32_t aIndex) {
+ SubscribeTreeNode* node = mRowMap[aIndex];
+ if (node->isOpen) {
+ node->isOpen = false;
+
+ // Remove subtree by deleting elements from array up to next sibling.
+ int32_t count = 0;
+ do {
+ // Find next sibling or the beginning of the next subtree.
+ if (node->prevSibling) {
+ count = mRowMap.IndexOf(node->prevSibling, aIndex) - aIndex - 1;
+ } else {
+ node = node->parent;
+ // When node reaches the root, delete the rest of the array.
+ if (!node->parent) {
+ count = mRowMap.Length() - aIndex - 1;
+ }
+ }
+ } while (!count && node->parent);
+ mRowMap.RemoveElementsAt(aIndex + 1, count);
+ if (mTree) {
+ mTree->RowCountChanged(aIndex + 1, -count);
+ mTree->InvalidateRow(aIndex);
+ }
+ } else {
+ // Recursively add the children nodes (i.e., remember open)
+ node->isOpen = true;
+ int32_t total = 0;
+ node = node->lastChild;
+ while (node) {
+ total += AddSubtree(node, aIndex + 1 + total);
+ node = node->prevSibling;
+ }
+ if (mTree) {
+ mTree->RowCountChanged(aIndex + 1, total);
+ mTree->InvalidateRow(aIndex);
+ }
+ }
+ return NS_OK;
+}
+
+int32_t nsSubscribableServer::AddSubtree(SubscribeTreeNode* node,
+ int32_t index) {
+ mRowMap.InsertElementAt(index, node);
+ int32_t total = 1;
+ if (node->isOpen) {
+ node = node->lastChild;
+ while (node) {
+ total += AddSubtree(node, index + total);
+ node = node->prevSibling;
+ }
+ }
+ return total;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("nameColumn")) {
+ nsCString path(mRowMap[aRow]->path);
+ GetLeafName(path, retval);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("nameColumn"))
+ retval = NS_ConvertUTF8toUTF16(mRowMap[aRow]->path);
+ if (colId.EqualsLiteral("subscribedColumn")) {
+ retval = mRowMap[aRow]->isSubscribed ? u"true"_ns : u"false"_ns;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetCellText(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aText) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aText) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProps) {
+ SubscribeTreeNode* node = mRowMap[aRow];
+ if (node->isSubscribable)
+ aProps.AssignLiteral("subscribable-true");
+ else
+ aProps.AssignLiteral("subscribable-false");
+
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("subscribedColumn")) {
+ if (node->isSubscribed)
+ aProps.AppendLiteral(" subscribed-true");
+ else
+ aProps.AppendLiteral(" subscribed-false");
+ } else if (colId.EqualsLiteral("nameColumn")) {
+ aProps.AppendLiteral(" serverType-");
+ aProps.Append(NS_ConvertUTF8toUTF16(mServerType));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetRowProperties(int32_t aRow, nsAString& aProps) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetColumnProperties(nsTreeColumn* aCol,
+ nsAString& aProps) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsEditable(int32_t aRow, nsTreeColumn* aCol,
+ bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSeparator(int32_t aRowIndex, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSorted(bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CanDrop(int32_t aIndex, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aData, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Drop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aData) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetImageSrc(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CycleHeader(nsTreeColumn* aCol) { return NS_OK; }
+
+NS_IMETHODIMP
+nsSubscribableServer::SelectionChangedXPCOM() { return NS_OK; }
+
+NS_IMETHODIMP
+nsSubscribableServer::CycleCell(int32_t aRow, nsTreeColumn* aCol) {
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsSubscribableServer.h b/comm/mailnews/base/src/nsSubscribableServer.h
new file mode 100644
index 0000000000..7fbafd0838
--- /dev/null
+++ b/comm/mailnews/base/src/nsSubscribableServer.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsSubscribableServer_h__
+#define nsSubscribableServer_h__
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsITreeSelection.h"
+#include "nsITreeView.h"
+#include "nsISubscribableServer.h"
+#include "nsTArray.h"
+
+struct SubscribeTreeNode;
+
+class nsSubscribableServer : public nsISubscribableServer, public nsITreeView {
+ public:
+ nsSubscribableServer();
+
+ nsresult Init();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISUBSCRIBABLESERVER
+ NS_DECL_NSITREEVIEW
+
+ private:
+ virtual ~nsSubscribableServer();
+
+ nsresult ConvertNameToUnichar(const char* inStr, char16_t** outStr);
+ nsCOMPtr<nsISubscribeListener> mSubscribeListener;
+ nsCString mIncomingServerUri;
+ char mDelimiter;
+ bool mShowFullName;
+ bool mStopped;
+ nsCString mServerType;
+
+ // root of the folder tree while items are discovered on the server
+ SubscribeTreeNode* mTreeRoot;
+ // array of nodes representing the rows for the tree element
+ nsTArray<SubscribeTreeNode*> mRowMap;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ void FreeSubtree(SubscribeTreeNode* node);
+ void FreeRows();
+ SubscribeTreeNode* CreateNode(SubscribeTreeNode* parent,
+ nsACString const& name, nsACString const& path);
+ nsresult AddChildNode(SubscribeTreeNode* parent, nsACString const& name,
+ const nsACString& aPath, SubscribeTreeNode** child);
+ nsresult FindAndCreateNode(const nsACString& aPath,
+ SubscribeTreeNode** aResult);
+
+ int32_t GetRow(SubscribeTreeNode* node, bool* open);
+ int32_t AddSubtree(SubscribeTreeNode* node, int32_t index);
+};
+
+#endif // nsSubscribableServer_h__
diff --git a/comm/mailnews/base/src/nsUserInfo.h b/comm/mailnews/base/src/nsUserInfo.h
new file mode 100644
index 0000000000..df3f81adef
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfo.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef __nsUserInfo_h
+#define __nsUserInfo_h
+
+#include "nsIUserInfo.h"
+
+class nsUserInfo : public nsIUserInfo
+
+{
+ public:
+ nsUserInfo(void);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUSERINFO
+
+ protected:
+ virtual ~nsUserInfo();
+};
+
+#endif /* __nsUserInfo_h */
diff --git a/comm/mailnews/base/src/nsUserInfoMac.mm b/comm/mailnews/base/src/nsUserInfoMac.mm
new file mode 100644
index 0000000000..a98d1f72ea
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoMac.mm
@@ -0,0 +1,70 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUserInfo.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "nsCocoaUtils.h"
+
+#import <Cocoa/Cocoa.h>
+#import <AddressBook/AddressBook.h>
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ nsCocoaUtils::GetStringForNSString(NSFullUserName(), aFullname);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ nsCocoaUtils::GetStringForNSString(NSUserName(), aUsername);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ aEmailAddress.Truncate();
+ // Try to get this user's primary email from the system addressbook's "me card"
+ // (if they've filled it)
+ ABPerson* me = [[ABAddressBook sharedAddressBook] me];
+ ABMultiValue* emailAddresses = [me valueForProperty:kABEmailProperty];
+ if ([emailAddresses count] > 0) {
+ // get the index of the primary email, in case there are more than one
+ int primaryEmailIndex = [emailAddresses indexForIdentifier:[emailAddresses primaryIdentifier]];
+ nsCocoaUtils::GetStringForNSString([emailAddresses valueAtIndex:primaryEmailIndex],
+ aEmailAddress);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ GetEmailAddress(aDomain);
+ int32_t index = aDomain.FindChar('@');
+ if (index != -1) {
+ // chop off everything before, and including the '@'
+ aDomain.Cut(0, index + 1);
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsUserInfoUnix.cpp b/comm/mailnews/base/src/nsUserInfoUnix.cpp
new file mode 100644
index 0000000000..559b68f062
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoUnix.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUserInfo.h"
+#include "nsCRT.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsNativeCharsetUtils.h"
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ aFullname.Truncate();
+ struct passwd* pw = nullptr;
+
+ pw = getpwuid(geteuid());
+
+ if (!pw || !pw->pw_gecos) return NS_OK;
+
+ nsAutoCString fullname(pw->pw_gecos);
+
+ // now try to parse the GECOS information, which will be in the form
+ // Full Name, <other stuff> - eliminate the ", <other stuff>
+ // also, sometimes GECOS uses "&" to mean "the user name" so do
+ // the appropriate substitution
+
+ // truncate at first comma (field delimiter)
+ int32_t index;
+ if ((index = fullname.Find(",")) != kNotFound) fullname.Truncate(index);
+
+ // replace ampersand with username
+ if (pw->pw_name) {
+ nsAutoCString username(pw->pw_name);
+ if (!username.IsEmpty())
+ username.SetCharAt(nsCRT::ToUpper(username.CharAt(0)), 0);
+
+ fullname.ReplaceSubstring("&", username.get());
+ }
+
+ CopyUTF8toUTF16(fullname, aFullname);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ aUsername.Truncate();
+ struct passwd* pw = nullptr;
+
+ // is this portable? those are POSIX compliant calls, but I need to check
+ pw = getpwuid(geteuid());
+
+ if (!pw || !pw->pw_name) return NS_OK;
+
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(pw->pw_name), aUsername);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ aDomain.Truncate();
+ struct utsname buf;
+ char* domainname = nullptr;
+
+ if (uname(&buf) < 0) {
+ return NS_OK;
+ }
+
+#if defined(__linux__)
+ domainname = buf.domainname;
+#endif
+
+ if (domainname && domainname[0]) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(domainname), aDomain);
+ } else {
+ // try to get the hostname from the nodename
+ // on machines that use DHCP, domainname may not be set
+ // but the nodename might.
+ if (buf.nodename[0]) {
+ // if the nodename is foo.bar.org, use bar.org as the domain
+ char* pos = strchr(buf.nodename, '.');
+ if (pos) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(pos + 1), aDomain);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ // use username + "@" + domain for the email address
+
+ nsString username;
+ nsString domain;
+
+ GetUsername(username);
+ GetDomain(domain);
+
+ if (!username.IsEmpty() && !domain.IsEmpty()) {
+ aEmailAddress = username;
+ aEmailAddress.Append('@');
+ aEmailAddress += domain;
+ } else {
+ aEmailAddress.Truncate();
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsUserInfoWin.cpp b/comm/mailnews/base/src/nsUserInfoWin.cpp
new file mode 100644
index 0000000000..a5b69cf9a2
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoWin.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUserInfo.h"
+
+#include "mozilla/ArrayUtils.h" // ArrayLength
+#include "nsString.h"
+#include "windows.h"
+#include "nsCRT.h"
+
+#define SECURITY_WIN32
+#include "lm.h"
+#include "security.h"
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ aUsername.Truncate();
+
+ // UNLEN is the max username length as defined in lmcons.h
+ wchar_t username[UNLEN + 1];
+ DWORD size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size)) return NS_OK;
+
+ aUsername.Assign(username);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ aFullname.Truncate();
+
+ wchar_t fullName[512];
+ DWORD size = mozilla::ArrayLength(fullName);
+
+ if (GetUserNameExW(NameDisplay, fullName, &size)) {
+ aFullname.Assign(fullName);
+ } else {
+ // Try to use the net APIs regardless of the error because it may be
+ // able to obtain the information.
+ wchar_t username[UNLEN + 1];
+ size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size)) {
+ return NS_OK;
+ }
+
+ const DWORD level = 2;
+ LPBYTE info;
+ // If the NetUserGetInfo function has no full name info it will return
+ // success with an empty string.
+ NET_API_STATUS status = NetUserGetInfo(nullptr, username, level, &info);
+ if (status != NERR_Success) {
+ return NS_OK;
+ }
+
+ aFullname.Assign(reinterpret_cast<USER_INFO_2*>(info)->usri2_full_name);
+ NetApiBufferFree(info);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ aDomain.Truncate();
+
+ const DWORD level = 100;
+ LPBYTE info;
+ NET_API_STATUS status = NetWkstaGetInfo(nullptr, level, &info);
+ if (status == NERR_Success) {
+ aDomain.Assign(reinterpret_cast<WKSTA_INFO_100*>(info)->wki100_langroup);
+ NetApiBufferFree(info);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ aEmailAddress.Truncate();
+
+ // RFC3696 says max length of an email address is 254
+ wchar_t emailAddress[255];
+ DWORD size = mozilla::ArrayLength(emailAddress);
+
+ if (!GetUserNameExW(NameUserPrincipal, emailAddress, &size)) {
+ return NS_OK;
+ }
+
+ aEmailAddress.Assign(emailAddress);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/test/.eslintrc.js b/comm/mailnews/base/test/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/base/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/base/test/TestMsgStripRE.cpp b/comm/mailnews/base/test/TestMsgStripRE.cpp
new file mode 100644
index 0000000000..eae217dcb2
--- /dev/null
+++ b/comm/mailnews/base/test/TestMsgStripRE.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISupportsPrimitives.h"
+#include "nsString.h"
+
+#define STRING_SIZE 255
+struct testInfo {
+ char encodedInput[STRING_SIZE];
+ char expectedOutput[STRING_SIZE];
+ bool expectedDidModify;
+};
+
+int testStripRe(const char* encodedInput, char* expectedOutput,
+ bool expectedDidModify) {
+ // call NS_StripRE with the appropriate args
+ nsCString modifiedSubject;
+ bool didModify;
+ didModify = NS_MsgStripRE(nsDependentCString(encodedInput), modifiedSubject);
+
+ // make sure we got the right results
+ if (didModify != expectedDidModify) return 2;
+
+ if (didModify) {
+ if (strcmp(expectedOutput, modifiedSubject.get())) {
+ return 3;
+ }
+ } else if (strcmp(expectedOutput, encodedInput)) {
+ return 4;
+ }
+
+ // test passed
+ return 0;
+}
+
+// int main(int argc, char** argv)
+TEST(TestMsgStripRE, TestMsgStripREMain)
+{
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ // set localizedRe pref, value "SV,ÆØÅ",
+ // \xC3\x86, \xC3\x98 and \xC3\x85 are the UTF-8 encodings of Æ, Ø and Å.
+ rv = prefBranch->SetStringPref("mailnews.localizedRe",
+ "SV,\xC3\x86\xC3\x98\xC3\x85"_ns);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ // run our tests
+ struct testInfo testInfoStructs[] = {
+ // Note that re-encoding always happens in UTF-8.
+ {"SV: =?ISO-8859-1?Q?=C6blegr=F8d?=", "=?UTF-8?B?w4ZibGVncsO4ZA==?=",
+ true},
+ {"=?ISO-8859-1?Q?SV=3A=C6blegr=F8d?=", "=?UTF-8?B?w4ZibGVncsO4ZA==?=",
+ true},
+
+ // Note that in the next two tests, the only ISO-8859-1 chars are in the
+ // localizedRe piece, so once they've been stripped, the re-encoding
+ // process simply writes out ASCII rather than an ISO-8859-1 encoded
+ // string with no actual ISO-8859-1 special characters, which seems
+ // reasonable.
+ {"=?ISO-8859-1?Q?=C6=D8=C5=3A_Foo_bar?=", "Foo bar", true},
+ {"=?ISO-8859-1?Q?=C6=D8=C5=3AFoo_bar?=", "Foo bar", true}};
+
+ bool allTestsPassed = true;
+ int result;
+ for (unsigned int i = 0; i < MOZ_ARRAY_LENGTH(testInfoStructs); i++) {
+ result = testStripRe(testInfoStructs[i].encodedInput,
+ testInfoStructs[i].expectedOutput,
+ testInfoStructs[i].expectedDidModify);
+ if (result) {
+ printf("Failed: %s, i=%d | result=%d\n", __FILE__, i, result);
+ allTestsPassed = false;
+ }
+ EXPECT_TRUE(result == 0);
+ }
+
+ if (allTestsPassed) {
+ printf("all tests passed\n");
+ }
+ EXPECT_TRUE(allTestsPassed);
+}
diff --git a/comm/mailnews/base/test/gtest/TestHeaderReader.cpp b/comm/mailnews/base/test/gtest/TestHeaderReader.cpp
new file mode 100644
index 0000000000..326279655a
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/TestHeaderReader.cpp
@@ -0,0 +1,205 @@
+#include "gtest/gtest.h"
+#include "nsString.h"
+#include "HeaderReader.h"
+#include "nsTArray.h"
+#include "mozilla/ArrayUtils.h"
+
+// Invocation:
+// $ ./mach gtest "TestHeaderReader.*"
+
+TEST(TestHeaderReader, Basic)
+{
+ struct hdr {
+ nsCString name;
+ nsCString rawValue;
+ };
+
+ struct {
+ nsCString raw;
+ nsTArray<hdr> expected;
+ bool isComplete; // expect to find complete header block?
+ nsCString leftOver; // Data which should be left unprocessed.
+ } testMsgs[] = {
+ // Simple case works?
+ {
+ "Message-ID: one\r\n"
+ "To: alice@example.com\r\n"
+ "\r\n"
+ "Body here. HeaderReader should have stopped by now.\r\n"
+ "Note: this line looks like a header, but it isn't!\r\n"_ns,
+ {
+ {"Message-ID"_ns, "one"_ns},
+ {"To"_ns, "alice@example.com"_ns},
+ },
+ true,
+ "Body here. HeaderReader should have stopped by now.\r\n"
+ "Note: this line looks like a header, but it isn't!\r\n"_ns,
+ },
+ // Handle folded header values correctly?
+ {
+ "To: bob@example.com\r\n"
+ "Subject: Do\r\n"
+ " we\r\n"
+ " handle\r\n"
+ "\tfolded\r\n" // Can fold with tabs too.
+ " fields OK?\r\n"
+ "Message-ID: two\r\n"
+ "\r\n"
+ "...message body here...\r\n"_ns,
+ {
+ {"To"_ns, "bob@example.com"_ns},
+ {"Subject"_ns,
+ "Do\r\n we\r\n handle\r\n\tfolded\r\n fields OK?"_ns},
+ {"Message-ID"_ns, "two"_ns},
+ },
+ true,
+ "...message body here...\r\n"_ns,
+ },
+ // Handle no whitespace after colon?
+ {
+ "Foo:bar\r\n"
+ "\r\n"_ns,
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ true,
+ ""_ns,
+ },
+ // Folding with no text on first line?
+ // (I _think_ this is legal...)
+ {
+ "Foo: \r\n"
+ " bar\r\n"
+ "\r\n"_ns,
+ {
+ {"Foo"_ns, "\r\n bar"_ns},
+ },
+ true,
+ ""_ns,
+ },
+ // Folded line with no end of header block.
+ // Input could be truncated. So we don't want this to output any headers
+ // (The missing next line could be folded or not - we just don't know).
+ {
+ "Foo:\r\n"
+ " bar\r\n"
+ " wibble\r\n"_ns,
+ {},
+ false,
+ "Foo:\r\n bar\r\n wibble\r\n"_ns,
+ },
+ // Ignore incomplete lines as expected?.
+ {
+ "Foo: bar\r\n"
+ "Wibble: this is a part"_ns, // ... "ial line".
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ false,
+ "Wibble: this is a part"_ns,
+ },
+ // Ignore incomplete folded lines?
+ {
+ "Foo: bar\r\n"
+ "Wibble: this\r\n"
+ " value is not co"_ns, // ... "mplete".
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ false,
+ "Wibble: this\r\n value is not co"_ns,
+ },
+ // Handle empty input without crashing?
+ {
+ ""_ns,
+ {},
+ false,
+ ""_ns,
+ }};
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testMsgs); ++i) {
+ auto const& t = testMsgs[i];
+ // Collect all the headers.
+ nsTArray<HeaderReader::Hdr> gotHeaders;
+ HeaderReader rdr;
+ auto handler = [&](HeaderReader::Hdr const& hdr) {
+ gotHeaders.AppendElement(hdr);
+ return true; // Keep going.
+ };
+
+ // Simulate multiple passes over a growing buffer - we'll add a quarter
+ // of the data each pass.
+ for (uint32_t i = 1; i < 4; ++i) {
+ // Encourage each pass to use different memory.
+ nsCString fudge(t.raw.BeginReading(), i * t.raw.Length() / 4);
+ rdr.Parse(fudge, handler);
+ }
+
+ // Last pass - give the reader the entire input.
+ auto leftOver = rdr.Parse(t.raw, handler);
+
+ // Did we get all the headers we expected?
+ ASSERT_EQ(gotHeaders.Length(), t.expected.Length());
+ for (size_t i = 0; i < t.expected.Length(); ++i) {
+ auto const& expect = t.expected[i];
+ auto const& got = gotHeaders[i];
+ ASSERT_EQ(expect.name, nsCString(got.Name(t.raw)));
+ ASSERT_EQ(expect.rawValue, nsCString(got.RawValue(t.raw)));
+ }
+
+ // Correctly detected the end of the header block?
+ ASSERT_EQ(t.isComplete, rdr.IsComplete());
+
+ // Make sure processing stopped where expected.
+ ASSERT_EQ(t.leftOver, nsCString(leftOver));
+ }
+}
+
+// Check that callback can halt processing, and that it can be correctly
+// resumed.
+TEST(TestHeaderReader, Stop)
+{
+ // We'll stop each time we find a header name containing "STOP".
+ struct {
+ nsCString raw;
+ int expectedCount; // Number of headers we expect to find.
+ int expectedPasses; // Number of passes we expect to run.
+ } testMsgs[] = {
+ {"foo: bar\r\n\r\n"_ns, 1, 1},
+ {"Message-ID: one\r\n"
+ "STOP: pause here please\r\n"
+ "foo-One: nothing to see here...\r\n"
+ "foo-Two: still nothing...\r\n"
+ "\r\n"_ns,
+ 4, 2},
+ {"A: eh\r\n"
+ "B: bee\r\n"
+ "STOP_1: pause here.\r\n"
+ "C: sea\r\n"
+ "STOP_2: another pause.\r\n"
+ "E: eee\r\n"
+ "STOP_3: last one.\r\n"
+ "\r\n"_ns,
+ 7, 3},
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testMsgs); ++i) {
+ auto const& t = testMsgs[i];
+ HeaderReader rdr;
+ int gotCount = 0;
+ auto handler = [&](HeaderReader::Hdr const& hdr) {
+ ++gotCount;
+ return nsCString(hdr.Name(t.raw)).Find("STOP") == kNotFound;
+ };
+
+ int passes = 0;
+ // All our examples have a complete header block.
+ while (!rdr.IsComplete()) {
+ rdr.Parse(t.raw, handler);
+ ++passes;
+ }
+ ASSERT_EQ(gotCount, t.expectedCount);
+ ASSERT_EQ(passes, t.expectedPasses);
+ ASSERT_TRUE(rdr.IsComplete());
+ }
+}
diff --git a/comm/mailnews/base/test/gtest/TestLineReader.cpp b/comm/mailnews/base/test/gtest/TestLineReader.cpp
new file mode 100644
index 0000000000..e84274fcb0
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/TestLineReader.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "nsString.h"
+#include "LineReader.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Span.h"
+
+// Invocation:
+// $ ./mach gtest "TestLineReader.*"
+
+TEST(TestLineReader, Basic)
+{
+ struct {
+ nsTArray<nsCString> chunks;
+ int expectedLines;
+ } testCases[] = {
+ // Split lines as expected?
+ {
+ {"Line one\r\nLine two\r\n"_ns},
+ 2,
+ },
+
+ // Plain LFs should be accepted.
+ {
+ {"Line one\nLine two\n"_ns},
+ 2,
+ },
+
+ // Empty lines appear as expected?
+ {
+ {"\r\n\r\n\r\n"_ns},
+ 3,
+ },
+
+ // Empty case handled?
+ {
+ {""_ns},
+ 0,
+ },
+
+ // Split in mid-CRLF.
+ {
+ {
+ "EOL split across\r"_ns,
+ "\nchunks.\r\n"_ns,
+ },
+ 2,
+ },
+
+ // Chunks join up correctly?
+ {
+ {"This single line is "_ns, "fed in as "_ns,
+ "multiple chunks\r\n"_ns},
+ 1,
+ },
+ // Handle an empty chunk OK?
+ {
+ {"foo"_ns, ""_ns,
+ "bar\n"_ns
+ "wibble\n"_ns},
+ 2,
+ },
+ // Handle lines without EOL?
+ {
+ {"This line has no EOL and relies on Flush()."_ns},
+ 1,
+ },
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testCases); ++i) {
+ auto const& t = testCases[i];
+
+ // Join chunks into one string - we expect the output to be
+ // this, byte-for-byte.
+ nsCString expectedText;
+ for (auto chunk : t.chunks) {
+ expectedText.Append(chunk);
+ }
+
+ // Callback to collect the lines and count them.
+ int gotLineCount = 0;
+ nsCString gotText;
+ auto callback = [&](mozilla::Span<const char> line) {
+ ++gotLineCount;
+ gotText.Append(line);
+ return true;
+ };
+ // Parse the chunks.
+ LineReader chopper;
+ for (auto chunk : t.chunks) {
+ chopper.Feed(chunk, callback);
+ }
+ chopper.Flush(callback);
+
+ ASSERT_EQ(t.expectedLines, gotLineCount);
+ ASSERT_EQ(expectedText, gotText);
+ }
+}
+
+// Check that processing is aborted when callback returns false.
+TEST(TestLineReader, Stop)
+{
+ struct {
+ nsTArray<nsCString> chunks;
+ int expectedLines;
+ } testCases[] = {
+ // Stop at 2 lines.
+ {
+ {"Line one\nSTOP\nThis line is never seen.\n"_ns},
+ 2,
+ },
+
+ // Line split up over multiple chunks.
+ {
+ {"one\r\nST"_ns, "OP\r\nblah blah\r\n"_ns},
+ 2,
+ },
+
+ // Empty string -> no lines.
+ {
+ {""_ns},
+ 0,
+ },
+
+ // No EOL, relies on Flush().
+ {
+ {
+ "STOP"_ns,
+ },
+ 1,
+ },
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testCases); ++i) {
+ auto const& t = testCases[i];
+
+ // Callback to collect the lines and count them, stopping when
+ // we find a line containing "STOP".
+ int gotLineCount = 0;
+ auto callback = [&](mozilla::Span<const char> line) -> bool {
+ ++gotLineCount;
+ return nsCString(line).Find("STOP"_ns) == kNotFound;
+ };
+ // Parse.
+ LineReader chopper;
+ for (auto chunk : t.chunks) {
+ chopper.Feed(chunk, callback);
+ }
+ chopper.Flush(callback);
+ ASSERT_EQ(t.expectedLines, gotLineCount);
+ }
+}
+
+// Test the SplitLines() fn.
+TEST(TestLineReader, SplitLines)
+{
+ struct {
+ nsCString input;
+ nsTArray<nsCString> expect; // The lines we expect to see.
+ nsCString expectLeftover; // The unconsumed data we expect at the end.
+ } testCases[] = {
+ // Empty string -> no lines.
+ {""_ns, {}, ""_ns},
+ // Incomplete line
+ {"foo"_ns, {}, "foo"_ns},
+ // Blank lines split as expected?
+ {"\r\n\r\n\r\n"_ns, {"\r\n"_ns, "\r\n"_ns, "\r\n"_ns}, ""_ns},
+ // A couple of normal-looking lines.
+ {"one\r\ntwo\r\n"_ns, {"one\r\n"_ns, "two\r\n"_ns}, ""_ns},
+ // Handles bare LFs?
+ {"one\ntwo\n"_ns, {"one\n"_ns, "two\n"_ns}, ""_ns},
+ // Ignores bare CRs?
+ {"one\rtwo\r"_ns, {}, "one\rtwo\r"_ns},
+
+ // Early-out works?
+ {"one\r\nSTOP\r\n3\r\n4\r\n"_ns,
+ {"one\r\n"_ns, "STOP\r\n"_ns},
+ "3\r\n4\r\n"_ns},
+ };
+
+ for (auto const& t : testCases) {
+ nsTArray<nsCString> got;
+ auto fn = [&](mozilla::Span<const char> line) -> bool {
+ got.AppendElement(line);
+ // Finish early if line contains "STOP".
+ return nsCString(line).Find("STOP"_ns) == kNotFound;
+ };
+ mozilla::Span<const char> leftover = SplitLines(t.input, fn);
+
+ ASSERT_EQ(t.expect.Length(), got.Length());
+ for (size_t i = 0; i < t.expect.Length(); ++i) {
+ ASSERT_EQ(t.expect[i], got[i]);
+ }
+ ASSERT_EQ(t.expectLeftover, nsCString(leftover));
+ }
+}
diff --git a/comm/mailnews/base/test/gtest/moz.build b/comm/mailnews/base/test/gtest/moz.build
new file mode 100644
index 0000000000..a0b6129efd
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+FINAL_LIBRARY = "xul-gtest"
+
+UNIFIED_SOURCES += [
+ "TestHeaderReader.cpp",
+ "TestLineReader.cpp",
+]
+
+# LOCAL_INCLUDES += [
+# "../../src",
+# ]
diff --git a/comm/mailnews/base/test/moz.build b/comm/mailnews/base/test/moz.build
new file mode 100644
index 0000000000..4bb676ffb3
--- /dev/null
+++ b/comm/mailnews/base/test/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell-imap.ini", "unit/xpcshell.ini"]
+
+FINAL_LIBRARY = "xul-gtest"
+
+UNIFIED_SOURCES += [
+ "TestMsgStripRE.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/test",
+ "/xpcom/tests",
+]
+
+TEST_DIRS += ["gtest"]
diff --git a/comm/mailnews/base/test/unit/data/folderCache.json b/comm/mailnews/base/test/unit/data/folderCache.json
new file mode 100644
index 0000000000..e593e6cbb9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/folderCache.json
@@ -0,0 +1,206 @@
+{
+ "/foo/bar/profile-default/ImapMail/imap.localhost": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": -1,
+ "totalUnreadMsgs": -1
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Archives.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Archives",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Drafts.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Drafts",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/INBOX.msf": {
+ "MRMTime": "1628202592",
+ "MRUTime": "1628202593",
+ "aclFlags": 0,
+ "boxFlags": 2097216,
+ "expungedBytes": 11536,
+ "flags": 2282237972,
+ "folderSize": 22906,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 2,
+ "onlineName": "INBOX",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 2,
+ "serverUnseen": 0,
+ "totalMsgs": 2,
+ "totalUnreadMsgs": 1
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Junk.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Junk",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Sent.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Sent",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Templates.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Templates",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Trash.msf": {
+ "aclFlags": 0,
+ "boxFlags": 262224,
+ "expungedBytes": 0,
+ "flags": 532756,
+ "folderSize": -1,
+ "hierDelim": 46,
+ "onlineName": "Trash",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/blah.msf": {
+ "MRUTime": "1628202572",
+ "aclFlags": 0,
+ "boxFlags": 262208,
+ "expungedBytes": 0,
+ "flags": 134750228,
+ "folderSize": 11872,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 3,
+ "onlineName": "blah",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 2,
+ "serverUnseen": 0,
+ "totalMsgs": 2,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/foo.msf": {
+ "MRUTime": "1628202573",
+ "aclFlags": 0,
+ "boxFlags": 262208,
+ "expungedBytes": 11536,
+ "flags": 134750228,
+ "folderSize": 10707667,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 5,
+ "onlineName": "foo",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 4,
+ "serverUnseen": 0,
+ "totalMsgs": 3,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/Mail/Local Folders": {
+ "expungedBytes": 0,
+ "flags": 28,
+ "folderName": "Local Folders",
+ "folderSize": -1,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": -1,
+ "totalUnreadMsgs": -1
+ },
+ "/foo/bar/profile-default/Mail/Local Folders/Trash.msf": {
+ "MRUTime": "1628202569",
+ "expungedBytes": 0,
+ "flags": 260,
+ "folderName": "Trash",
+ "folderSize": 0,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0,
+ "useServerRetention": "1"
+ },
+ "/foo/bar/profile-default/Mail/Local Folders/Unsent Messages.msf": {
+ "MRUTime": "1628202570",
+ "expungedBytes": 0,
+ "flags": 2052,
+ "folderName": "Outbox",
+ "folderSize": 0,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0,
+ "useServerRetention": "1"
+ }
+}
diff --git a/comm/mailnews/base/test/unit/data/panacea.dat b/comm/mailnews/base/test/unit/data/panacea.dat
new file mode 100644
index 0000000000..214176e31a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/panacea.dat
@@ -0,0 +1,70 @@
+// <!-- <mdb:mork:z v="1.4"/> -->
+< <(a=c)> // (f=iso-8859-1)
+ (8A=boxFlags)(8B=hierDelim)(8C=onlineName)(8D=aclFlags)(8E=serverTotal)
+ (8F=serverUnseen)(90=serverRecent)(91=indexingPriority)(92=folderName)
+ (93=applyIncomingFilters.empty)(94=applyIncomingFilters)(95=MRUTime)
+ (96=dobayes.mailnews@mozilla.org#junk.empty)
+ (97=dobayes.mailnews@mozilla.org#junk)(98=nextUID)
+ (99=lastSyncTimeInSec)(9A=useServerRetention)(9B=MRMTime)
+ (80=ns:msg:db:row:scope:folders:all)(81=ns:msg:db:table:kind:folders)
+ (82=key)(83=flags)(84=totalMsgs)(85=totalUnreadMsgs)
+ (86=pendingUnreadMsgs)(87=pendingMsgs)(88=expungedBytes)(89=folderSize)>
+
+<(80
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/INBOX.msf)(A6=88083014)(AA=2)(A8=1)(81=0)(B8=2d10)(BB=597a)
+ (A7=200040)(9D=2e)(84=INBOX)(BA=1628202593)(B5=1628202592)(86
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders/Trash.msf)(87=104)(88=Trash)(AB=1628202569)(89
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders/Unsent Messages.msf)(8A=804)(8C=Outbox)(AC=1628202570)(8D
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders)(8E=1c)(8F=ffffffff)(82=ffffffffffffffff)(90=Local Folders)
+ (91
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Drafts.msf)(83=5e)(92=Drafts)(93
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Templates.msf)(94=Templates)(95
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Sent.msf)(96=Sent)(97
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Archives.msf)(98=Archives)(9A
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Trash.msf)(9B=82114)(9C=40050)(9E
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/blah.msf)(A0=8082014)(AF=2e60)(A1=40040)(9F=blah)(AE
+ =1628202572)(B0=3)(A2
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/foo.msf)(B9=a362d3)(A3=foo)(B2=4)(B1=1628202573)(B4=5)
+ (A4
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Junk.msf)(A5=Junk)(BC
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost)(BD=)>
+{1:^80 {(k^81:c)(s=9)}
+ [1(^82^80)(^83^A6)(^84=2)(^85=1)(^86=0)(^87=0)(^88^B8)(^89^BB)(^8A^A7)
+ (^8B=2e)(^8C^84)(^8D=0)(^8E=2)(^8F=0)(^90=0)(^95^BA)(^98=2)(^99=0)
+ (^9B^B5)]
+ [2(^82^86)(^83^87)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89=0)(^92^88)
+ (^95^AB)(^9A=1)]
+ [3(^82^89)(^83^8A)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89=0)(^92^8C)
+ (^95^AC)(^9A=1)]
+ [4(^82^8D)(^83=1c)(^84^8F)(^85^8F)(^86=0)(^87=0)(^88=0)(^89^82)(^92^90)]
+ [5(^82^91)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^92)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [6(^82^93)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^94)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [7(^82^95)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^96)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [8(^82^97)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^98)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [9(^82^9A)(^83^9B)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A^9C)
+ (^8B=2e)(^8C^88)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [A(^82^9E)(^83^A0)(^84=2)(^85=0)(^86=0)(^87=0)(^88=0)(^89^AF)(^8A^A1)
+ (^8B=2e)(^8C^9F)(^8D=0)(^8E=2)(^8F=0)(^90=0)(^95^AE)(^98=3)(^99=0)]
+ [B(^82^A2)(^83^A0)(^84=3)(^85=0)(^86=0)(^87=0)(^88^B8)(^89^B9)(^8A^A1)
+ (^8B=2e)(^8C^A3)(^8D=0)(^8E=4)(^8F=0)(^90=0)(^95^B1)(^98=5)(^99=0)]
+ [C(^82^A4)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^A5)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [D(^82^BC)(^83=0)(^84^8F)(^85^8F)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C=)(^8D=0)(^8E=0)(^8F=0)(^90=0)]}
diff --git a/comm/mailnews/base/test/unit/data/panacea_empty.dat b/comm/mailnews/base/test/unit/data/panacea_empty.dat
new file mode 100644
index 0000000000..b169b34da9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/panacea_empty.dat
@@ -0,0 +1 @@
+// <!-- <mdb:mork:z v="1.4"/> -->
diff --git a/comm/mailnews/base/test/unit/data/remoteContent.sql b/comm/mailnews/base/test/unit/data/remoteContent.sql
new file mode 100644
index 0000000000..042cfefbd9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/remoteContent.sql
@@ -0,0 +1,41 @@
+-- Address book with remote content permissions for use in test_accountMigration.js.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 1),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 2),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 3);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PrimaryEmail', 'no@test.invalid'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PhotoType', 'generic'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'LowercasePrimaryEmail', 'no@test.invalid'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PopularityIndex', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PreferMailFormat', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'LastModifiedDate', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PreferDisplayName', '1'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'AllowRemoteContent', '0'),
+
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PrimaryEmail', 'yes@test.invalid'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PhotoType', 'generic'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'LowercasePrimaryEmail', 'yes@test.invalid'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PopularityIndex', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PreferMailFormat', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'LastModifiedDate', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PreferDisplayName', '1'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'AllowRemoteContent', '0'),
+
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'LastModifiedDate', '1397383824'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PrimaryEmail', 'yes@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PhotoType', 'generic'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'SecondEmail', 'yes2@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'LowercasePrimaryEmail', 'yes@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PopularityIndex', '0'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PreferMailFormat', '0'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PreferDisplayName', '1'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'AllowRemoteContent', '1');
diff --git a/comm/mailnews/base/test/unit/head_mailbase.js b/comm/mailnews/base/test/unit/head_mailbase.js
new file mode 100644
index 0000000000..9f37623291
--- /dev/null
+++ b/comm/mailnews/base/test/unit/head_mailbase.js
@@ -0,0 +1,23 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/base/test/unit/nodelist_test.xml b/comm/mailnews/base/test/unit/nodelist_test.xml
new file mode 100644
index 0000000000..e1640b5b5b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/nodelist_test.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" ?>
+
+<rootnode>
+ <node id="childnode1" />
+ <node id="childnode2" />
+ <node id="childnode3" />
+</rootnode>
diff --git a/comm/mailnews/base/test/unit/test_MsgIncomingServer.js b/comm/mailnews/base/test/unit/test_MsgIncomingServer.js
new file mode 100644
index 0000000000..184d2d4072
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_MsgIncomingServer.js
@@ -0,0 +1,224 @@
+/* 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/. */
+
+Services.prefs.setBoolPref("mailnews.imap.jsmodule", true);
+
+registerCleanupFunction(() => {
+ Services.logins.removeAllLogins();
+ Services.prefs.clearUserPref("mailnews.imap.jsmodule");
+});
+
+/**
+ * Test password is migrated when changing hostname/username.
+ */
+add_task(function testMigratePasswordOnChangeUsernameHostname() {
+ // Add two logins.
+ let loginItems = [
+ ["news://news.localhost", "user-nntp", "password-nntp"],
+ ["mailbox://pop3.localhost", "user-pop", "password-pop"],
+ ];
+ for (let [uri, username, password] of loginItems) {
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(uri, null, uri, username, password, "", "");
+ Services.logins.addLogin(login);
+ }
+
+ // Create a nntp server, check the password can be found correctly.
+ let nntpIncomingServer = MailServices.accounts.createIncomingServer(
+ "user-nntp",
+ "news.localhost",
+ "nntp"
+ );
+ nntpIncomingServer.getPasswordWithUI("", "");
+ equal(nntpIncomingServer.password, "password-nntp");
+
+ // Change the username, check password can be found using the new username.
+ nntpIncomingServer.username = "nntp";
+ let password;
+ let serverUri = "news://news.localhost";
+ for (let login of Services.logins.findLogins(serverUri, "", serverUri)) {
+ if (login.username == "nntp") {
+ password = login.password;
+ }
+ }
+ equal(password, "password-nntp");
+
+ // Create a pop3 server, check the password can be found correctly.
+ let pop3IncomingServer = MailServices.accounts.createIncomingServer(
+ "user-pop",
+ "pop3.localhost",
+ "pop3"
+ );
+ pop3IncomingServer.getPasswordWithUI("", "");
+ equal(pop3IncomingServer.password, "password-pop");
+
+ // Change the hostname, check password can be found using the new hostname.
+ pop3IncomingServer.hostName = "localhost";
+ serverUri = "mailbox://localhost";
+ for (let login of Services.logins.findLogins(serverUri, "", serverUri)) {
+ if (login.username == "user-pop") {
+ password = login.password;
+ }
+ }
+ equal(password, "password-pop");
+});
+
+/**
+ * Test identity folders are migrated when changing hostname/username.
+ */
+add_task(function testMigrateIdentitiesOnChangeUsernameHostname() {
+ // Create an imap server.
+ let incomingServer1 = MailServices.accounts.createIncomingServer(
+ "user-imap",
+ "imap.localhost",
+ "imap"
+ );
+ // Create a pop server.
+ let incomingServer2 = MailServices.accounts.createIncomingServer(
+ "user-pop",
+ "pop3.localhost",
+ "pop3"
+ );
+
+ // Create an identity and point folders to incomingServer1.
+ let identity1 = MailServices.accounts.createIdentity();
+ identity1.fccFolder = incomingServer1.serverURI + "/Sent";
+ identity1.draftFolder = incomingServer1.serverURI + "/Drafts";
+ identity1.archiveFolder = incomingServer1.serverURI + "/Archives";
+ identity1.stationeryFolder = incomingServer1.serverURI + "/Templates";
+ let account1 = MailServices.accounts.createAccount();
+ account1.addIdentity(identity1);
+ // Create another identity and point folders to both servers.
+ let identity2 = MailServices.accounts.createIdentity();
+ identity2.fccFolder = incomingServer1.serverURI + "/Sent";
+ identity2.draftFolder = incomingServer2.serverURI + "/Drafts";
+ let account2 = MailServices.accounts.createAccount();
+ account2.addIdentity(identity2);
+
+ // Check folders were correctly set.
+ equal(identity1.fccFolder, "imap://user-imap@imap.localhost/Sent");
+ equal(identity1.draftFolder, "imap://user-imap@imap.localhost/Drafts");
+ equal(identity1.archiveFolder, "imap://user-imap@imap.localhost/Archives");
+ equal(
+ identity1.stationeryFolder,
+ "imap://user-imap@imap.localhost/Templates"
+ );
+ equal(identity2.fccFolder, "imap://user-imap@imap.localhost/Sent");
+ equal(identity2.draftFolder, "mailbox://user-pop@pop3.localhost/Drafts");
+
+ // Change the hostname.
+ incomingServer1.hostName = "localhost";
+
+ // Check folders were correctly updated.
+ identity1 = MailServices.accounts.getIdentity(identity1.key);
+ equal(identity1.fccFolder, "imap://user-imap@localhost/Sent");
+ equal(identity1.draftFolder, "imap://user-imap@localhost/Drafts");
+ equal(identity1.archiveFolder, "imap://user-imap@localhost/Archives");
+ equal(identity1.stationeryFolder, "imap://user-imap@localhost/Templates");
+ equal(identity2.fccFolder, "imap://user-imap@localhost/Sent");
+ equal(identity2.draftFolder, "mailbox://user-pop@pop3.localhost/Drafts");
+});
+
+/**
+ * Test spam action prefs are migrated when changing hostname/username.
+ */
+add_task(function testMigrateSpamActionsOnChangeUsernameHostname() {
+ // Create an imap server.
+ let incomingServer1 = MailServices.accounts.createIncomingServer(
+ "user-imap",
+ "imap.localhost",
+ "imap"
+ );
+ incomingServer1.setUnicharValue(
+ "spamActionTargetFolder",
+ incomingServer1.serverURI + "/Спам"
+ );
+
+ equal(
+ incomingServer1.spamSettings.actionTargetAccount,
+ "imap://user-imap@imap.localhost"
+ );
+ equal(
+ incomingServer1.spamSettings.actionTargetFolder,
+ "imap://user-imap@imap.localhost/Спам"
+ );
+
+ // Change the username.
+ incomingServer1.username = "user";
+
+ equal(
+ incomingServer1.spamSettings.actionTargetAccount,
+ "imap://user@imap.localhost"
+ );
+ equal(
+ incomingServer1.spamSettings.actionTargetFolder,
+ "imap://user@imap.localhost/Спам"
+ );
+});
+
+/**
+ * Test filters are migrated when changing hostname/username.
+ */
+add_task(function testMigrateFiltersOnChangeUsernameHostname() {
+ // Create a nntp server.
+ let nntpIncomingServer = MailServices.accounts.createIncomingServer(
+ "user-nntp",
+ "news.localhost",
+ "nntp"
+ );
+ let filterList = nntpIncomingServer.getFilterList(null);
+
+ // Insert a CopyToFolder filter.
+ let filter = filterList.createFilter("filter1");
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.CopyToFolder;
+ action.targetFolderUri = "news://user-nntp@news.localhost/dest1";
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Insert a MarkRead filter.
+ filter = filterList.createFilter("filter2");
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Insert a MoveToFolder filter.
+ filter = filterList.createFilter("filter3");
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = "news://user-nntp@news.localhost/dest2";
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Change the hostname, test targetFolderUri of filters are changed accordingly.
+ nntpIncomingServer.hostName = "localhost";
+ filterList = nntpIncomingServer.getFilterList(null);
+ filter = filterList.getFilterAt(0);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://user-nntp@localhost/dest1"
+ );
+ filter = filterList.getFilterAt(2);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://user-nntp@localhost/dest2"
+ );
+
+ // Change the username, test targetFolderUri of filters are changed accordingly.
+ nntpIncomingServer.username = "nntp";
+ filterList = nntpIncomingServer.getFilterList(null);
+ filter = filterList.getFilterAt(0);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://nntp@localhost/dest1"
+ );
+ filter = filterList.getFilterAt(2);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://nntp@localhost/dest2"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_MsgKeySet.js b/comm/mailnews/base/test/unit/test_MsgKeySet.js
new file mode 100644
index 0000000000..7ae1eea938
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_MsgKeySet.js
@@ -0,0 +1,85 @@
+/* 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 { MsgKeySet } = ChromeUtils.import("resource:///modules/MsgKeySet.jsm");
+
+/**
+ * Test MsgKeySet.addRange works correctly.
+ */
+add_task(function testAddRange() {
+ // Init an empty set.
+ let keySet = new MsgKeySet();
+ ok(!keySet.has(1));
+
+ // Add two ranges.
+ keySet.addRange(90, 99);
+ keySet.addRange(2, 19);
+
+ // Test members.
+ ok(!keySet.has(1));
+ ok(keySet.has(2));
+ ok(keySet.has(16));
+ ok(!keySet.has(20));
+ ok(keySet.has(99));
+ equal(keySet.toString(), "2-19,90-99");
+
+ // Init a set from a string.
+ keySet = new MsgKeySet("102,199");
+ ok(!keySet.has(22));
+ ok(keySet.has(199));
+
+ // Add two ranges.
+ keySet.addRange(2, 19);
+ keySet.addRange(12, 29);
+
+ // Test members.
+ ok(keySet.has(2));
+ ok(keySet.has(22));
+ ok(keySet.has(199));
+ equal(keySet.toString(), "2-29,102,199");
+});
+
+/**
+ * Test MsgKeySet.add works correctly.
+ */
+add_task(function testAdd() {
+ // Init an empty set.
+ let keySet = new MsgKeySet();
+ ok(!keySet.has(1));
+
+ // Add three values.
+ keySet.add(1);
+ keySet.add(2);
+ keySet.add(4);
+
+ // Test members.
+ ok(keySet.has(1));
+ ok(keySet.has(2));
+ ok(!keySet.has(3));
+ ok(keySet.has(4));
+ equal(keySet.toString(), "1-2,4");
+});
+
+/**
+ * Test MsgKeySet.getLastMissingRange works correctly.
+ */
+add_task(function testGetLastMissingRange() {
+ // Init a set.
+ let keySet = new MsgKeySet("2-9,12-29");
+
+ // Test `start` should be a value not already in keySet.
+ let [start, end] = keySet.getLastMissingRange(2, 33);
+ equal(start, 30);
+ equal(end, 33);
+
+ // Test `start` should be the lowest input value.
+ [start, end] = keySet.getLastMissingRange(33, 33);
+ equal(start, 33);
+ equal(end, 33);
+
+ // Test get missing old messages range works.
+ [start, end] = keySet.getLastMissingRange(3, 23);
+ equal(start, 10);
+ equal(end, 11);
+});
diff --git a/comm/mailnews/base/test/unit/test_accountMgr.js b/comm/mailnews/base/test/unit/test_accountMgr.js
new file mode 100644
index 0000000000..6036dccfa9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgr.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+/**
+ * This tests that we cleanup the account prefs when the account manager is
+ * loaded. This entails removing duplicate accounts from
+ * mail.accountmanager.accounts list, and removing duplicate accounts with
+ * the same server.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with both kinds of duplication.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server2.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server2.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server2.directory-rel",
+ "[ProfD]Mail/Local Folders-1"
+ );
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account4,account5,account5,account6,account1,account2"
+ );
+ // Set the default account to one we're going to get rid of. The account
+ // manager should recover relatively gracefully.
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account6");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ // Here all the accounts are local but the first account will behave as
+ // an actual local account and will be kept last always.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account4,account5,account1"
+ );
+ let server5 = MailServices.accounts
+ .getIncomingServer("server5")
+ .QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account1");
+
+ // Just make sure this doesn't throw an exception, because we did remove the
+ // default account.
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ Assert.equal(defaultAccount, null);
+
+ // Remove an account, and verify that the account list pref looks OK:
+ let server = MailServices.accounts.getIncomingServer("server4");
+
+ // We need to get the root folder to read from the folder cache
+ // before it gets removed or else we'll assert, because we're
+ // not completely initialized...
+ server.rootFolder.flags;
+
+ MailServices.accounts.removeAccount(
+ MailServices.accounts.FindAccountForServer(server)
+ );
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account5,account1"
+ );
+ // Make sure cleaning up duplicate accounts didn't hork accounts.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account1.server"),
+ "server1"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account5.server"),
+ "server5"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgr2.js b/comm/mailnews/base/test/unit/test_accountMgr2.js
new file mode 100644
index 0000000000..a48685da00
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgr2.js
@@ -0,0 +1,199 @@
+/* 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/. */
+
+/**
+ * This tests various methods and attributes on nsIMsgAccountManager.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(async function () {
+ // Create a couple of test accounts.
+ let acc1 = MailServices.accounts.createAccount();
+ acc1.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap",
+ "imap.example.com",
+ "imap"
+ );
+ let id1 = MailServices.accounts.createIdentity();
+ id1.email = "bob_imap@example.com";
+ acc1.addIdentity(id1);
+
+ let acc2 = MailServices.accounts.createAccount();
+ acc2.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_pop3",
+ "pop3.EXAMPLE.com.",
+ "pop3"
+ );
+ let id2 = MailServices.accounts.createIdentity();
+ id2.email = "bob_pop3@example.com";
+ acc2.addIdentity(id2);
+
+ // Add an identity shared by both accounts.
+ let id3 = MailServices.accounts.createIdentity();
+ id3.email = "bob_common@example.com";
+ acc1.addIdentity(id3);
+ acc2.addIdentity(id3);
+
+ // The special "Local Folders" account and server (server type is "none").
+ MailServices.accounts.createLocalMailAccount();
+
+ // Setup done. Now check that things are as we expect.
+
+ // At this point we should have 3 accounts and servers (imap, pop, local).
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(MailServices.accounts.allServers.length, 3);
+
+ // The identities we explicitly created.
+ Assert.equal(MailServices.accounts.allIdentities.length, 3);
+
+ // Check we find the right number of identities associated with each server.
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(acc1.incomingServer).length,
+ 2
+ );
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(acc2.incomingServer).length,
+ 2
+ );
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(
+ MailServices.accounts.localFoldersServer
+ ).length,
+ 0
+ );
+
+ // id1 and id2 are on separate accounts (and servers).
+ Assert.equal(MailServices.accounts.getServersForIdentity(id1).length, 1);
+ Assert.equal(MailServices.accounts.getServersForIdentity(id2).length, 1);
+ // id3 is shared.
+ Assert.equal(MailServices.accounts.getServersForIdentity(id3).length, 2);
+
+ // Does allFolders return the default folders we'd expect?
+ // IMAP has Inbox only.
+ // POP3 and local accounts both have Inbox and Trash.
+ Assert.equal(MailServices.accounts.allFolders.length, 1 + 2 + 2);
+
+ // Let's ditch the IMAP account.
+ MailServices.accounts.removeAccount(acc1);
+
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(MailServices.accounts.allServers.length, 2);
+
+ // It should have taken the imap-specific identity with it.
+ Assert.equal(MailServices.accounts.allIdentities.length, 2);
+
+ // Test a special hostname.
+ const acc4 = MailServices.accounts.createAccount();
+ acc4.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_unavail",
+ "0.0.0.0.", // Note ending dot which would not do anything for an IP.
+ "pop3"
+ );
+ let id4 = MailServices.accounts.createIdentity();
+ id4.email = "bob_unavail@example.com";
+ acc4.addIdentity(id4);
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 3,
+ "acc4 should be in accounts"
+ );
+
+ // Test that an account with empty server hostname doesn't even get listed.
+ const serverKey = acc4.incomingServer.key;
+ Services.prefs.setStringPref(`mail.server.${serverKey}.hostname`, "");
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 2,
+ "invalid acc4 should have been removed"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account3",
+ "listed accounts should be correct after testing blank host"
+ );
+
+ // Test that an account that had punycode hostname entered is found.
+ const acc5 = MailServices.accounts.createAccount();
+ acc5.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap5",
+ "xn--thnderbird-beb.test",
+ "imap"
+ );
+ const id5 = MailServices.accounts.createIdentity();
+ id5.email = "bob_imap5@xn--thnderbird-beb.test";
+ acc5.addIdentity(id5);
+
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 3,
+ "added acc5 should still be listed"
+ );
+
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account5,account3",
+ "listed accounts should be correct after testing punycode host"
+ );
+ const punyServer = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://xn--thnderbird-beb.test:143/INBOX")
+ );
+ Assert.ok(
+ punyServer?.hostName,
+ "should find server by uri for punycode hostname"
+ );
+
+ const punyServer2 = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://thünderbird.test:143/INBOX")
+ );
+ Assert.ok(
+ punyServer2?.hostName,
+ "should find ACE server by normalized IDN hostname"
+ );
+
+ // Test that an account with IDN hostname entered is found.
+ const acc6 = MailServices.accounts.createAccount();
+ acc6.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap6",
+ "thünderbird.example",
+ "imap"
+ );
+ const id6 = MailServices.accounts.createIdentity();
+ id6.email = "bob_imap6@thünderbird.example";
+ acc6.addIdentity(id6);
+
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 4,
+ "added acc6 should still be listed"
+ );
+
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account5,account6,account3",
+ "listed accounts should be correct after testing IDN host"
+ );
+ const idnServer = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://thünderbird.example:143/INBOX")
+ );
+ Assert.ok(idnServer?.hostName, "should find server by uri for IDN hostname");
+
+ const idnServer2 = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://xn--thnderbird-beb.example:143/INBOX")
+ );
+ Assert.ok(
+ idnServer2?.hostName,
+ "should find idn server by by ACE encodeed uri"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js b/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js
new file mode 100644
index 0000000000..f8b8f707c7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+/**
+ * This tests that accounts with invalid types, such as could be created
+ * from an extension, do not disappear immediately when the extension
+ * is unloaded.
+ *
+ * Adapted from test_AccountMgr.js by Kent James <kent@caspia.com>
+ */
+
+function run_test() {
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+
+ // Here we are simulating a server and account that is added by an
+ // extension, but that extension is currently unloaded. The extension
+ // added "secondsToLeaveUnavailable" (though a typical value would be
+ // one month, not 2 seconds!) to tell the core code to leave this alone
+ // for awhile if the extension is unloaded.
+ Services.prefs.setCharPref("mail.server.server2.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server2.type", "invalid");
+ Services.prefs.setIntPref("mail.server.server2.secondsToLeaveUnavailable", 2);
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account2,account1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account1");
+
+ // This will force the load of the accounts setup above.
+ // We don't see the invalid account.
+ Assert.equal(MailServices.accounts.accounts.length, 1);
+
+ // But it is really there.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account1"
+ );
+
+ // Add a new account (so that we can check if this clobbers the existing
+ // inactive account or its server).
+ let newAccount = MailServices.accounts.createAccount();
+ let newIdentity = MailServices.accounts.createIdentity();
+ newAccount.addIdentity(newIdentity);
+ newAccount.defaultIdentity = newIdentity;
+ newAccount.incomingServer = MailServices.accounts.createIncomingServer(
+ "somename",
+ "somehost.example.com",
+ "pop3"
+ );
+
+ // No collisions with the inactive account.
+ Assert.notEqual(newIdentity.key, "id2");
+ Assert.notEqual(newAccount.incomingServer.key, "server2");
+ Assert.notEqual(newAccount.key, "account2");
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+
+ MailServices.accounts.unloadAccounts();
+
+ // Set the unavailable account to a valid type, and watch it appear.
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+
+ // Make it bad again, and reload it to restart the timeout before delete.
+ MailServices.accounts.unloadAccounts();
+ Services.prefs.setCharPref("mail.server.server2.type", "invalid");
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ MailServices.accounts.unloadAccounts();
+
+ // Now let the bad type timeout, and watch it magically disappear!
+ do_test_pending();
+ do_timeout(3000, function () {
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+
+ // It is now gone.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ newAccount.key + ",account1"
+ );
+
+ do_test_finished();
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js b/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js
new file mode 100644
index 0000000000..5972012e85
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js
@@ -0,0 +1,43 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with Local Folders in the middle
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account4,account1,account5"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account4");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account4,account1,account5"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js b/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js
new file mode 100644
index 0000000000..640c314499
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+/**
+ * This tests that if the default account is removed, the default becomes
+ * another account or null. The removed account must not remain the default.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account3.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account3.server", "server3");
+
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "host2.invalid");
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server3.hostname", "host3.invalid");
+ Services.prefs.setCharPref("mail.server.server3.type", "pop3");
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account2,account3,account1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account3");
+
+ // Load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account3,account1"
+ );
+ Assert.equal(MailServices.accounts.defaultAccount?.key, "account3");
+
+ // Remove the default account, account3. The default should be set to a
+ // sensible replacement, account2.
+
+ MailServices.accounts.removeAccount(MailServices.accounts.defaultAccount);
+ Assert.equal(MailServices.accounts.defaultAccount?.key, "account2");
+
+ // Remove the default account, account2. No remaining accounts can be the
+ // default, so it should become null.
+
+ MailServices.accounts.removeAccount(MailServices.accounts.defaultAccount);
+ Assert.equal(MailServices.accounts.defaultAccount, null);
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMigration.js b/comm/mailnews/base/test/unit/test_accountMigration.js
new file mode 100644
index 0000000000..d2bb801c49
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMigration.js
@@ -0,0 +1,184 @@
+/* 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/. */
+
+/**
+ * This tests that we don't try to reset the mail.server.server<n>.authMethod
+ * preference every time we run the migration code, and other migration stuff
+ */
+
+var { migrateMailnews } = ChromeUtils.import(
+ "resource:///modules/MailnewsMigrator.jsm"
+);
+
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+function testPermission(aURI) {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ aURI,
+ {}
+ );
+ return Services.perms.testPermissionFromPrincipal(principal, "image");
+}
+
+function run_test() {
+ // Set up some basic accounts with limited prefs - enough to satisfy the
+ // migrator.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account3.server", "server3");
+
+ // Server1 has nothing set.
+
+ // Server2 has useSecAuth set to true, auth_login unset
+ Services.prefs.setBoolPref("mail.server.server2.useSecAuth", true);
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2,account3"
+ );
+
+ // Set server1 and server2 username and hostname to test clientid population.
+ Services.prefs.setCharPref("mail.server.server1.userName", "testuser1");
+ Services.prefs.setCharPref("mail.server.server2.userName", "testuser2");
+ Services.prefs.setCharPref("mail.server.server1.type", "imap");
+ Services.prefs.setCharPref("mail.server.server2.type", "imap");
+ Services.prefs.setCharPref(
+ "mail.server.server1.hostname",
+ "mail.sampledomain1.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.server.server2.hostname",
+ "mail.sampledomain2.com"
+ );
+
+ loadABFile("data/remoteContent", kPABData.fileName);
+
+ let uriAllowed = Services.io.newURI(
+ "chrome://messenger/content/email=yes@test.invalid"
+ );
+ let uriAllowed2 = Services.io.newURI(
+ "chrome://messenger/content/email=yes2@test.invalid"
+ );
+ let uriDisallowed = Services.io.newURI(
+ "chrome://messenger/content/email=no@test.invalid"
+ );
+
+ // Check that this email that according to the ab data has (had!)
+ // remote content premissions, has no premissions pre migration.
+ Assert.equal(testPermission(uriAllowed), Services.perms.UNKNOWN_ACTION);
+ Assert.equal(testPermission(uriAllowed2), Services.perms.UNKNOWN_ACTION);
+ Assert.equal(testPermission(uriDisallowed), Services.perms.UNKNOWN_ACTION);
+
+ // Now migrate the prefs.
+ migrateMailnews();
+
+ // Check that server1 and server2 have the same clientid.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server1.clientid"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.clientid"));
+
+ // Check that server3 didn't get a clientid, since it has no userName.
+ // This is the case for nntp.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server3.clientid"));
+
+ // Check what has been set.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server1.authMethod"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.authMethod"));
+ Assert.equal(
+ Services.prefs.getIntPref("mail.server.server2.authMethod"),
+ Ci.nsMsgAuthMethod.secure
+ );
+
+ // Now clear the authMethod for set for server2. This simulates the user
+ // setting the value back to "3", i.e. Ci.nsMsgAuthMethod.passwordCleartext.
+ Services.prefs.clearUserPref("mail.server.server2.authMethod");
+
+ // Now attempt migration again, e.g. a second load of TB
+ migrateMailnews();
+
+ // This time around, both of these should not be set.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server1.authMethod"));
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server2.authMethod"));
+
+ //
+ // Now check SMTP
+ //
+
+ Services.prefs.setCharPref("mail.smtpservers", "smtp1,smtp2,smtp3");
+
+ // smtp1 has nothing set.
+
+ // smtp2 has useSecAuth set to true, auth_method unset
+ Services.prefs.setBoolPref("mail.smtpserver.smtp2.useSecAuth", true);
+
+ // Set server1 and server2 username and hostname to test clientid population.
+ Services.prefs.setCharPref("mail.smtpserver.smtp1.username", "testuser1");
+ Services.prefs.setCharPref("mail.smtpserver.smtp2.username", "testuser2");
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp1.hostname",
+ "mail.sampledomain1.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp2.hostname",
+ "mail.sampledomain2.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp3.hostname",
+ "mail.nousername.example.com"
+ );
+
+ // Migration should now have added permissions for the address that had them
+ // and not for the one that didn't have them.
+ Assert.ok(Services.prefs.getIntPref("mail.ab_remote_content.migrated") > 0);
+ Assert.equal(testPermission(uriAllowed), Services.perms.ALLOW_ACTION);
+ Assert.equal(testPermission(uriAllowed2), Services.perms.ALLOW_ACTION);
+ Assert.equal(testPermission(uriDisallowed), Services.perms.UNKNOWN_ACTION);
+
+ // Now migrate the prefs
+ migrateMailnews();
+
+ // Check that smtpserver 1 and smtpserver 2 now have a clientid.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.clientid"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.clientid"));
+
+ // Check that the smtp3server 2 got a clientid, even if it has no
+ // username. All SMTP servers don't require a username.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp3.clientid"));
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
+ );
+ Assert.ok(
+ Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
+ );
+ Assert.equal(
+ Services.prefs.getIntPref("mail.smtpserver.smtp2.authMethod"),
+ Ci.nsMsgAuthMethod.secure
+ );
+
+ // Now clear the authMethod for set for smtp2. This simulates the user
+ // setting the value back to "3", i.e. Ci.nsMsgAuthMethod.passwordCleartext.
+ Services.prefs.clearUserPref("mail.smtpserver.smtp2.authMethod");
+
+ // Now clear the mail.server.server1.clientid to test re-population.
+ Services.prefs.clearUserPref("mail.server.server2.clientid");
+
+ // Now attempt migration again, e.g. a second load of TB
+ migrateMailnews();
+
+ // This time around, both of these should not be set.
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
+ );
+
+ // The server2 clientid should be the same as the smtpserver2 now since
+ // they are for the same mail.sampledomain2.com domain.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.smtpserver.smtp2.clientid"),
+ Services.prefs.getCharPref("mail.server.server2.clientid")
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_acctRepair.js b/comm/mailnews/base/test/unit/test_acctRepair.js
new file mode 100644
index 0000000000..b702dcfd61
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_acctRepair.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+/**
+ * This tests that we recover from having a local folders server
+ * without having an account that points at it.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with both kinds of duplication.
+
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server1");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref("mail.accountmanager.accounts", "account6");
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account6");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ // This will force the load of the accounts setup above.
+ // We should have created an account for the local folders.
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account6,account7"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account7.server"),
+ "server1"
+ );
+ let server5 = MailServices.accounts
+ .getIncomingServer("server5")
+ .QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account7");
+}
diff --git a/comm/mailnews/base/test/unit/test_bccInDatabase.js b/comm/mailnews/base/test/unit/test_bccInDatabase.js
new file mode 100644
index 0000000000..ad9ff039b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bccInDatabase.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+/*
+ * Testing of bcc in message summary file added in bug 481667
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var hdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continueTest();
+ },
+ };
+
+ // Get a message into the local filestore.
+ var draft = do_get_file("../../../data/draft1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ draft,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function continueTest() {
+ // dump("\nbccList >" + hdr.bccList);
+ // dump("\nccList >" + hdr.ccList);
+ // dump("\n");
+ Assert.ok(hdr.bccList.includes("Another Person"));
+ Assert.ok(hdr.bccList.includes("<u1@example.com>"));
+ Assert.ok(!hdr.bccList.includes("IDoNotExist"));
+ hdr = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/base/test/unit/test_bug428427.js b/comm/mailnews/base/test/unit/test_bug428427.js
new file mode 100644
index 0000000000..f00f2bd571
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug428427.js
@@ -0,0 +1,249 @@
+/* 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/. */
+
+// Test of message count changes in virtual folder views
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var bugmail1 = do_get_file("../../../data/bugmail1");
+// main test
+
+// the headers for the test messages. All messages are identical, but
+// have different properties set on them.
+var hdrs = [];
+
+// how many identical messages to load
+var messageCount = 5;
+
+// tag used with test messages
+var tag1 = "istag";
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Get messageCount messages into the local filestore.
+ do_test_pending();
+
+ // function setupVirtualFolder() continues the testing after copyFileMessage.
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ return true;
+}
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdrs.push(localAccountUtils.inboxFolder.GetMessageHeader(aKey));
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ if (--messageCount) {
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ } else {
+ try {
+ setupVirtualFolder();
+ } catch (ex) {
+ dump(ex);
+ }
+ }
+ },
+};
+
+var virtualFolder;
+var numTotalMessages;
+var numUnreadMessages;
+
+// virtual folder setup
+function setupVirtualFolder() {
+ // add as valid tag tag1, though probably not really necessary
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+
+ // add tag1 to 4 messages
+ let messages0to3 = [hdrs[0], hdrs[1], hdrs[2], hdrs[3]];
+ localAccountUtils.inboxFolder.addKeywordsToMessages(messages0to3, tag1);
+
+ // set 3 messages unread, 2 messages read
+ let messages0to2 = [hdrs[0], hdrs[1], hdrs[2]];
+ localAccountUtils.inboxFolder.markMessagesRead(messages0to2, false);
+
+ let messages3to4 = [hdrs[3], hdrs[4]];
+ localAccountUtils.inboxFolder.markMessagesRead(messages3to4, true);
+
+ // search will look for tag tag1 in the inbox folder
+ var searchTerm = makeSearchTerm(
+ localAccountUtils.inboxFolder,
+ tag1,
+ Ci.nsMsgSearchAttrib.Keywords,
+ Ci.nsMsgSearchOp.Contains
+ );
+
+ dump("creating virtual folder\n");
+ var rootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ virtualFolder = CreateVirtualFolder(
+ "VfTest",
+ rootFolder,
+ localAccountUtils.inboxFolder.URI,
+ searchTerm,
+ false
+ );
+
+ // Setup search session. Execution continues with testVirtualFolder()
+ // after search is done.
+
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ localAccountUtils.inboxFolder
+ );
+ searchSession.appendTerm(searchTerm, false);
+ searchSession.registerListener(searchListener);
+ dump("starting search of vf\n");
+ searchSession.search(null);
+}
+
+// partially based on gSearchNotificationListener in searchBar.js
+// nsIMsgSearchNotify implementation
+var searchListener = {
+ onNewSearch() {
+ dump("in onnewsearch\n");
+ numTotalMessages = 0;
+ numUnreadMessages = 0;
+ },
+ onSearchHit(dbHdr, folder) {
+ print("Search hit, isRead is " + dbHdr.isRead);
+ numTotalMessages++;
+ if (!dbHdr.isRead) {
+ numUnreadMessages++;
+ }
+ },
+ onSearchDone(status) {
+ print("Finished search hitCount = " + numTotalMessages);
+ var db = virtualFolder.msgDatabase;
+ var dbFolderInfo = db.dBFolderInfo;
+ dbFolderInfo.numMessages = numTotalMessages;
+ dbFolderInfo.numUnreadMessages = numUnreadMessages;
+ virtualFolder.updateSummaryTotals(true);
+ print("virtual folder unread is " + virtualFolder.getNumUnread(false));
+ testVirtualFolder();
+ },
+};
+
+function testVirtualFolder() {
+ // basic functionality tests
+
+ // total messages matching search
+ Assert.equal(4, virtualFolder.getTotalMessages(false));
+
+ // total unread messages in search
+ Assert.equal(3, virtualFolder.getNumUnread(false));
+
+ // change unread of one item in search to decrease count
+ localAccountUtils.inboxFolder.markMessagesRead([hdrs[0]], true);
+ virtualFolder.updateSummaryTotals(true);
+
+ Assert.equal(2, virtualFolder.getNumUnread(false));
+
+ // failures fixed in this bug
+
+ // remove tag from one item to decrease count
+ var message1 = [hdrs[1]];
+ localAccountUtils.inboxFolder.removeKeywordsFromMessages(message1, tag1);
+ virtualFolder.updateSummaryTotals(true);
+ Assert.equal(3, virtualFolder.getTotalMessages(false));
+ Assert.equal(1, virtualFolder.getNumUnread(false));
+
+ // End of test, so release our header references
+ hdrs = null;
+
+ do_test_finished();
+ return true;
+}
+
+// helper functions
+
+// adapted from commandglue.js
+function CreateVirtualFolder(
+ newName,
+ parentFolder,
+ searchFolderURIs,
+ searchTerm,
+ searchOnline
+) {
+ var newFolder = parentFolder.addSubfolder(newName);
+ newFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
+ var vfdb = newFolder.msgDatabase;
+ var searchTermString = getSearchTermString(searchTerm);
+
+ var dbFolderInfo = vfdb.dBFolderInfo;
+ // set the view string as a property of the db folder info
+ // set the original folder name as well.
+ dbFolderInfo.setCharProperty("searchStr", searchTermString);
+ dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
+ dbFolderInfo.setBooleanProperty("searchOnline", searchOnline);
+ // This fails because the folder doesn't exist - why were we doing it?
+ // vfdb.summaryValid = true;
+ vfdb.close(true);
+ // use acctMgr to setup the virtual folder listener
+ var acctMgr = MailServices.accounts.QueryInterface(Ci.nsIFolderListener);
+ // print(acctMgr);
+ acctMgr.onFolderAdded(parentFolder, newFolder);
+ return newFolder;
+}
+
+function getSearchTermString(term) {
+ var condition = "";
+
+ if (condition.length > 1) {
+ condition += " ";
+ }
+
+ if (term.matchAll) {
+ condition = "ALL";
+ }
+ condition += term.booleanAnd ? "AND (" : "OR (";
+ condition += term.termAsString + ")";
+ return condition;
+}
+
+// Create a search term for searching aFolder
+// using aAttrib, aOp, and string aStrValue
+function makeSearchTerm(aFolder, aStrValue, aAttrib, aOp) {
+ // use a temporary search session
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder);
+ var searchTerm = searchSession.createTerm();
+ var value = searchTerm.value;
+ value.str = aStrValue;
+ searchTerm.value = value;
+ searchTerm.attrib = aAttrib;
+ searchTerm.op = aOp;
+ searchTerm.booleanAnd = false;
+ searchSession = null;
+ return searchTerm;
+}
diff --git a/comm/mailnews/base/test/unit/test_bug434810.js b/comm/mailnews/base/test/unit/test_bug434810.js
new file mode 100644
index 0000000000..f41eea82d9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug434810.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+// Test of setup of localMailFolders
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var rootFolder = localAccountUtils.incomingServer.rootFolder;
+
+ var msgProps = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+
+ var expectedFolders = ["Inbox"]; // Inbox hard-coded in LocalAccountUtils.jsm
+
+ // These two MailNews adds by default
+ expectedFolders.push(msgProps.GetStringFromName("outboxFolderName"));
+ expectedFolders.push(msgProps.GetStringFromName("trashFolderName"));
+
+ Assert.equal(rootFolder.numSubFolders, expectedFolders.length);
+ for (var i = 0; i < expectedFolders.length; ++i) {
+ Assert.ok(rootFolder.containsChildNamed(expectedFolders[i]));
+ }
+ Assert.ok(rootFolder.isAncestorOf(localAccountUtils.inboxFolder));
+}
diff --git a/comm/mailnews/base/test/unit/test_bug471682.js b/comm/mailnews/base/test/unit/test_bug471682.js
new file mode 100644
index 0000000000..73e431d84f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug471682.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+/*
+ * Test of message database validity on local copy from bug 471682. What
+ * we want to do here is to copy a couple of message to a new folder, and
+ * then compare the date and filesize of the folder file with the
+ * stored result in dbfolderinfo. If they don't match, that's bad.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var bugmail1 = do_get_file("../../../data/bugmail1");
+var gHdr; // header of test message in local folder
+
+localAccountUtils.loadLocalMailAccount();
+// create a subfolder as a target for copies
+var gSubfolder =
+ localAccountUtils.inboxFolder.createLocalSubfolder("subfolder");
+
+function run_test() {
+ // make sure we're using berkeley mailbox format here since this test
+ // assumes berkeley mailbox format.
+ if (
+ Services.prefs.getCharPref("mail.serverDefaultStoreContractID") !=
+ "@mozilla.org/msgstore/berkeleystore;1"
+ ) {
+ return;
+ }
+
+ do_test_pending();
+ // step 1: copy a message into the local inbox
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ step2,
+ null
+ );
+}
+
+// step 2: copy one message into a subfolder to establish an
+// mbox file time and size
+// nsIMsgCopyServiceListener implementation
+var step2 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ dump("in set message key\n");
+ gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ Assert.notEqual(gHdr, null);
+ // copy the message into the subfolder
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ [gHdr],
+ gSubfolder,
+ false,
+ step3,
+ null,
+ false
+ );
+ },
+};
+
+// step 3: after the copy, delay to allow copy to complete and allow possible
+// file error time
+// nsIMsgCopyServiceListener implementation
+var step3 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ do_timeout(2000, step4);
+ },
+};
+
+// step 4: start a second copy
+function step4() {
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ [gHdr],
+ gSubfolder,
+ false,
+ step5,
+ null,
+ false
+ );
+}
+
+// step 5: actual tests of file size and date
+// nsIMsgCopyServiceListener implementation
+var step5 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ var dbSize = gSubfolder.msgDatabase.dBFolderInfo.folderSize;
+ var dbDate = gSubfolder.msgDatabase.dBFolderInfo.folderDate;
+ var filePath = gSubfolder.filePath;
+ var date = parseInt(filePath.lastModifiedTime / 1000);
+ var size = filePath.fileSize;
+ Assert.equal(size, dbSize);
+ Assert.equal(date, dbDate);
+ // End of test, so release our header reference
+ gHdr = null;
+ do_test_finished();
+ },
+};
diff --git a/comm/mailnews/base/test/unit/test_bug514945.js b/comm/mailnews/base/test/unit/test_bug514945.js
new file mode 100644
index 0000000000..90ec8084dd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug514945.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/*
+ * Testing of GetAvailable crashes in bug 514945
+ */
+
+function run_test() {
+ const kValidityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+ ].getService(Ci.nsIMsgSearchValidityManager);
+
+ let validityTable = kValidityManager.getTable(
+ Ci.nsMsgSearchScope.offlineMail
+ );
+
+ // When we try to access a bad value of getAvailable, it should give an error,
+ // not crash.
+ let BAD_VALUE = 1000000; // some large value that is beyond the array bounds
+ let haveExpectedError = false;
+ try {
+ validityTable.getAvailable(Ci.nsMsgSearchAttrib.Subject, BAD_VALUE);
+ } catch (e) {
+ dump("Error but no crash, this is what we want:" + e + "\n");
+ haveExpectedError = true;
+ }
+
+ Assert.ok(haveExpectedError);
+
+ // One of the causes of this is that search term operators are not being
+ // initialized, resulting in random values of the operator. Make sure that is
+ // fixed.
+
+ const kSearchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerm = kSearchSession.createTerm();
+ Assert.equal(searchTerm.op, Ci.nsMsgSearchOp.Contains);
+}
diff --git a/comm/mailnews/base/test/unit/test_closedDB.js b/comm/mailnews/base/test/unit/test_closedDB.js
new file mode 100644
index 0000000000..fd752bf81b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_closedDB.js
@@ -0,0 +1,104 @@
+/* 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/. */
+
+/**
+ * Tests that an nsMsgDBFolder's database connection and listeners are
+ * restored when called by certain functions.
+ */
+
+const { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+add_task(async function () {
+ // Create a folder and some messages.
+
+ let generator = new MessageGenerator();
+
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let testFolder = rootFolder.createLocalSubfolder("testFolder");
+ testFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ testFolder.addMessageBatch(
+ generator.makeMessages({ count: 5 }).map(message => message.toMboxString())
+ );
+ let testMessages = [...testFolder.messages];
+
+ // Listen for notifications.
+
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ notifications: [],
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (property != "TotalUnreadMessages") {
+ return;
+ }
+
+ this.notifications.push({ folder, property, oldValue, newValue });
+ },
+ consumeNotification(expectedFolder, expectedOldValue, expectedNewValue) {
+ let { folder, oldValue, newValue } = this.notifications.shift();
+ Assert.equal(folder, expectedFolder, "notification folder");
+ Assert.equal(oldValue, expectedOldValue, "notification oldValue");
+ Assert.equal(newValue, expectedNewValue, "notification newValue");
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Clear the database reference, then mark some messages as read. We should
+ // see the unread count change and get two notifications about it. We could
+ // check `testFolder.msgDatabase` is not null afterwards, but that would be
+ // pointless, because the getter restores the database.
+
+ testFolder.msgDatabase = null;
+ testFolder.markMessagesRead([testMessages[0], testMessages[4]], true);
+ Assert.equal(
+ testFolder.getNumUnread(false),
+ 3,
+ "unread message count should be updated"
+ );
+ Assert.equal(
+ folderListener.notifications.length,
+ 2,
+ "two folder notifications should have fired"
+ );
+ folderListener.consumeNotification(testFolder, 5, 4);
+ folderListener.consumeNotification(testFolder, 4, 3);
+
+ // Clear the database reference, then mark some messages as flagged. This
+ // doesn't prove much except that nothing exploded.
+
+ testFolder.msgDatabase = null;
+ testFolder.markMessagesFlagged([testMessages[1], testMessages[3]], true);
+
+ // Clear the database reference, then mark all messages as read. We should
+ // see the unread count change to zero and get a notification about it.
+
+ testFolder.msgDatabase = null;
+ testFolder.markAllMessagesRead(null);
+ Assert.equal(
+ testFolder.getNumUnread(false),
+ 0,
+ "unread message count should be updated"
+ );
+ Assert.equal(
+ folderListener.notifications.length,
+ 1,
+ "a folder notifications should have fired"
+ );
+ folderListener.consumeNotification(testFolder, 3, 0);
+
+ // Clean up.
+
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ MailServices.accounts.removeAccount(account, false);
+});
diff --git a/comm/mailnews/base/test/unit/test_compactFailure.js b/comm/mailnews/base/test/unit/test_compactFailure.js
new file mode 100644
index 0000000000..300c8abda7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_compactFailure.js
@@ -0,0 +1,134 @@
+/* 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 { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+var { addMessagesToFolder, MessageGenerator, MessageScenarioFactory } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+var gTargetFolder;
+var gCid;
+
+// Allow certain xpcom errors.
+logHelperAllowedErrors.push("NS_ERROR_FILE_NOT_FOUND");
+
+var MsgDBServiceFailure = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgDBService"]),
+
+ openMailDBFromFile(file, folder, create, leaveInvalidDB) {
+ if (folder.name == "ShouldFail") {
+ throw Components.Exception("", Cr.NS_ERROR_FILE_NOT_FOUND);
+ }
+ return this._genuine.openMailDBFromFile(
+ file,
+ folder,
+ create,
+ leaveInvalidDB
+ );
+ },
+
+ openFolderDB(folder, leaveInvalidDB) {
+ return this._genuine.openFolderDB(folder, leaveInvalidDB);
+ },
+ createNewDB(folder) {
+ return this._genuine.createNewDB(folder);
+ },
+ registerPendingListener(folder, listener) {
+ this._genuine.registerPendingListener(folder, listener);
+ },
+ unregisterPendingListener(listener) {
+ this._genuine.unregisterPendingListener(listener);
+ },
+ cachedDBForFolder(folder) {
+ return this._genuine.cachedDBFolder(folder);
+ },
+ get openDBs() {
+ return this._genuine.openDBs;
+ },
+};
+
+function generate_messages() {
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(10));
+ return messages;
+}
+
+async function compact_with_exception(expectedException) {
+ let compactor = Cc["@mozilla.org/messenger/foldercompactor;1"].createInstance(
+ Ci.nsIMsgFolderCompactor
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ compactor.compactFolders([gTargetFolder], listener, null);
+ try {
+ await listener.promise;
+ do_throw(
+ "nsIMsgFolderCompactor listener wasn't called with a failure code."
+ );
+ } catch (failureCode) {
+ Assert.equal(expectedException, failureCode);
+ }
+}
+
+function create_local_folders() {
+ let rootFolder = localAccountUtils.rootFolder;
+ let localTrashFolder = rootFolder.getChildNamed("Trash");
+ localTrashFolder.setFlag(Ci.nsMsgFolderFlags.Trash);
+}
+
+async function delete_all_messages() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ gTargetFolder.deleteMessages(
+ [...gTargetFolder.messages],
+ null,
+ false, // Do not delete storage.
+ true, // Is a move.
+ promiseCopyListener,
+ false // Do not allow undo, currently leaks.
+ );
+ await promiseCopyListener.promise;
+}
+
+add_setup(function () {
+ localAccountUtils.loadLocalMailAccount();
+ create_local_folders();
+});
+
+add_task(async function test_compact_without_failure() {
+ // Setup open failure folder.
+ gTargetFolder =
+ localAccountUtils.rootFolder.createLocalSubfolder("ShouldFail");
+ addMessagesToFolder(generate_messages(), gTargetFolder);
+
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(gTargetFolder, resolve);
+ });
+ // Delete messages.
+ await delete_all_messages();
+ // Setup db service mock.
+ gCid = MockRegistrar.register(
+ "@mozilla.org/msgDatabase/msgDBService;1",
+ MsgDBServiceFailure
+ );
+ // Test compact without failure.
+ await compact_with_exception(Cr.NS_ERROR_FILE_NOT_FOUND);
+ // Teardown db service mock.
+ MockRegistrar.unregister(gCid);
+});
diff --git a/comm/mailnews/base/test/unit/test_converterDeferredAccount.js b/comm/mailnews/base/test/unit/test_converterDeferredAccount.js
new file mode 100644
index 0000000000..effc6d57e2
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_converterDeferredAccount.js
@@ -0,0 +1,206 @@
+/* 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 { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { convertMailStoreTo } = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+const { FolderUtils } = ChromeUtils.import(
+ "resource:///modules/FolderUtils.jsm"
+);
+
+// XXX: merge into test_converter.js
+
+var log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// No. of messages/files and folders copied.
+var gMsgHdrs = [];
+// {nsIMsgLocalMailFolder} folder carrying messages for the pop server.
+var gInbox;
+// {nsIMsgIncomingServer} server for first deferred pop account.
+var gServer1;
+// {nsIMsgIncomingServer} server for second deferred pop account.
+var gServer2;
+// {nsIMsgIncomingServer} server to convert.
+var gServer;
+
+var copyListenerWrap = {
+ SetMessageKey(aKey) {
+ let hdr = gInbox.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ },
+};
+
+var EventTarget = function () {
+ this.dispatchEvent = function (event) {
+ if (event.type == "progress") {
+ log.trace("Progress: " + event.detail);
+ }
+ };
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ listener,
+ null
+ );
+ return listener.promise;
+}
+
+/**
+ * Check that conversion worked for the given source.
+ *
+ * @param {nsIFile} source - mbox source directory.
+ * @param {nsIFile} target - maildir target directory.
+ */
+function checkConversion(source, target) {
+ for (let sourceContent of source.directoryEntries) {
+ let sourceContentName = sourceContent.leafName;
+ let ext = sourceContentName.substr(-4);
+ let targetFile = FileUtils.File(
+ PathUtils.join(target.path, sourceContentName)
+ );
+ log.debug("Checking path: " + targetFile.path);
+ if (ext == ".dat") {
+ Assert.ok(targetFile.exists());
+ } else if (sourceContent.isDirectory()) {
+ Assert.ok(targetFile.exists());
+ checkConversion(sourceContent, targetFile);
+ } else if (ext != ".msf") {
+ Assert.ok(targetFile.exists());
+ let cur = FileUtils.File(PathUtils.join(targetFile.path, "cur"));
+ Assert.ok(cur.exists());
+ let tmp = FileUtils.File(PathUtils.join(targetFile.path, "tmp"));
+ Assert.ok(tmp.exists());
+ if (targetFile.leafName == "Inbox") {
+ let curContents = cur.directoryEntries;
+ let curContentsCount = [...curContents].length;
+ Assert.equal(curContentsCount, 1000);
+ }
+ }
+ }
+}
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Set up two deferred pop accounts.
+ gServer1 = MailServices.accounts.createIncomingServer(
+ "test1",
+ "localhost1",
+ "pop3"
+ );
+ gServer2 = MailServices.accounts.createIncomingServer(
+ "test2",
+ "localhost2",
+ "pop3"
+ );
+ var accountPop1 = MailServices.accounts.createAccount();
+ var accountPop2 = MailServices.accounts.createAccount();
+
+ // Set incoming servers.
+ accountPop1.incomingServer = gServer1;
+ gServer1.QueryInterface(Ci.nsIPop3IncomingServer);
+ gServer1.valid = true;
+ accountPop2.incomingServer = gServer2;
+ gServer2.QueryInterface(Ci.nsIPop3IncomingServer);
+ gServer2.valid = true;
+
+ // Defer accounts to Local Folders.
+ gServer1.deferredToAccount = localAccountUtils.msgAccount.key;
+ gServer2.deferredToAccount = localAccountUtils.msgAccount.key;
+
+ // 'gServer1' should be deferred. Get the path of the root folder to which
+ // other accounts are deferred.
+ ok(gServer1.rootFolder.filePath.path != gServer1.rootMsgFolder.filePath.path);
+ let deferredToRootFolder = gServer1.rootMsgFolder.filePath.path;
+
+ // Account to which other accounts have been deferred.
+ let deferredToAccount;
+ // String to hold names of accounts to convert.
+ let accountsToConvert = "";
+
+ let accounts = FolderUtils.allAccountsSorted(true);
+ for (let account of accounts) {
+ if (
+ account.incomingServer.rootFolder.filePath.path == deferredToRootFolder
+ ) {
+ // Other accounts may be deferred to this account.
+ deferredToAccount = account;
+ } else if (
+ account.incomingServer.rootMsgFolder.filePath.path == deferredToRootFolder
+ ) {
+ // This is a deferred account.
+ accountsToConvert += account.incomingServer.username + ", ";
+ }
+ }
+
+ accountsToConvert =
+ accountsToConvert + deferredToAccount.incomingServer.username;
+ log.info(accountsToConvert + " will be converted");
+
+ gInbox = localAccountUtils.inboxFolder;
+ gServer = deferredToAccount.incomingServer;
+
+ run_next_test();
+}
+
+add_setup(async function () {
+ let msgFile = do_get_file("../../../data/bugmail10");
+ // Add 1000 messages to the "Inbox" folder.
+ for (let i = 0; i < 1000; i++) {
+ await copyFileMessage(msgFile, gInbox, false);
+ }
+});
+
+add_task(function testMaildirConversion() {
+ let mailstoreContractId = Services.prefs.getCharPref(
+ "mail.server." + gServer.key + ".storeContractID"
+ );
+
+ do_test_pending();
+ let pConverted = convertMailStoreTo(
+ mailstoreContractId,
+ gServer,
+ new EventTarget()
+ );
+ let originalRootFolder = gServer.rootFolder.filePath;
+
+ pConverted
+ .then(function (val) {
+ log.debug("Conversion done: " + originalRootFolder.path + " => " + val);
+ let newRootFolder = gServer.rootFolder.filePath;
+ checkConversion(originalRootFolder, newRootFolder);
+ do_test_finished();
+ })
+ .catch(function (reason) {
+ log.error("Conversion failed: " + reason.error);
+ ok(false); // Fail the test!
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_copyChaining.js b/comm/mailnews/base/test/unit/test_copyChaining.js
new file mode 100644
index 0000000000..b014049ed3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_copyChaining.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+// Test of chaining copies between the same folders
+
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gCopySource;
+var gCopyDest;
+var gMessages;
+var gCurTestNum = 1;
+
+// main test
+
+var gTestArray = [
+ function copyMsg1() {
+ gMessages = [...gCopySource.msgDatabase.enumerateMessages()];
+ CopyNextMessage();
+ },
+ function copyMsg2() {
+ CopyNextMessage();
+ },
+ function copyMsg3() {
+ CopyNextMessage();
+ },
+ function copyMsg4() {
+ CopyNextMessage();
+ },
+];
+
+function CopyNextMessage() {
+ if (gMessages.length > 0) {
+ let msgHdr = gMessages.shift();
+ MailServices.copy.copyMessages(
+ gCopySource,
+ [msgHdr],
+ gCopyDest,
+ true,
+ copyListener,
+ null,
+ false
+ );
+ } else {
+ do_throw("TEST FAILED - out of messages");
+ }
+}
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of
+ // all the operations.
+ do_test_pending();
+
+ gCopyDest = localAccountUtils.inboxFolder.createLocalSubfolder("copyDest");
+ // build up a diverse list of messages
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(10));
+ gCopySource = localAccountUtils.rootFolder.createLocalSubfolder("copySource");
+ addMessagesToFolder(messages, gCopySource);
+
+ mailTestUtils.updateFolderAndNotify(gCopySource, doTest);
+ return true;
+}
+
+function doTest() {
+ var test = gCurTestNum;
+ if (test <= gTestArray.length) {
+ var testFn = gTestArray[test - 1];
+ dump("Doing test " + test + " " + testFn.name + "\n");
+
+ try {
+ testFn();
+ } catch (ex) {
+ do_throw("TEST FAILED " + ex);
+ }
+ } else {
+ endTest();
+ }
+}
+
+function endTest() {
+ // Cleanup, null out everything
+ dump(" Exiting mail tests\n");
+ gMessages = null;
+ do_test_finished(); // for the one in run_test()
+}
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ ++gCurTestNum;
+ doTest();
+ },
+};
diff --git a/comm/mailnews/base/test/unit/test_copyToInvalidDB.js b/comm/mailnews/base/test/unit/test_copyToInvalidDB.js
new file mode 100644
index 0000000000..2c0dd85d9f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_copyToInvalidDB.js
@@ -0,0 +1,106 @@
+/*
+ * Simple tests for copying local messages to a folder whose db is missing
+ * or invalid
+ */
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+async function setup() {
+ let createSubfolder = async function (parentFolder, name) {
+ let promiseAdded = PromiseTestUtils.promiseFolderAdded(name);
+ parentFolder.createSubfolder(name, null);
+ await promiseAdded;
+ return parentFolder.getChildNamed(name);
+ };
+
+ // Create account.
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+ let root = account.incomingServer.rootFolder;
+
+ // Add a couple of folders containing some test messages.
+ let folder1 = await createSubfolder(root, "test1");
+ folder1.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let folder2 = await createSubfolder(root, "test2");
+ folder2.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let gen = new MessageGenerator();
+ let msg1 = gen.makeMessage();
+ let msg2 = gen.makeMessage({ inReplyTo: msg1 });
+ folder1.addMessageBatch([msg1, msg2].map(m => m.toMboxString()));
+
+ let msg3 = gen.makeMessage();
+ folder2.addMessage(msg3.toMboxString());
+
+ return [folder1, folder2];
+}
+
+add_task(async function test_copyToInvalidDB() {
+ let [folder1, folder2] = await setup();
+
+ // folder1 contains [msg1, msg2].
+ // folder2 contains [msg3].
+
+ // Take note of the message we're going to move (first msg in folder1).
+ let msgHdr = Array.from(folder1.msgDatabase.enumerateMessages())[0];
+ let expectedID = msgHdr.messageId;
+ let expectedMsg = mailTestUtils.loadMessageToString(folder1, msgHdr);
+
+ // Sabotage the destination folder2 database.
+ folder2.msgDatabase.summaryValid = false;
+ folder2.msgDatabase = null;
+ folder2.ForceDBClosed();
+ // In fact, delete the .msf file entirely.
+ folder2.summaryFile.remove(false);
+
+ // So folder2 has no trace of a DB.
+ Assert.equal(folder2.databaseOpen, false);
+ Assert.equal(folder2.summaryFile.exists(), false);
+
+ // Move the message from folder1 to folder2.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ folder1,
+ [msgHdr],
+ folder2,
+ true, // isMove
+ copyListener,
+ null, // window
+ false // allowUndo
+ );
+ await copyListener.promise;
+
+ // Current behaviour:
+ // After the move, there's still no sign of a DB file.
+ // Yet the copy didn't fail (see Bug 1737203).
+ Assert.equal(folder2.databaseOpen, false);
+ Assert.equal(folder2.summaryFile.exists(), false);
+
+ // Rebuild the the database.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ try {
+ folder2.getDatabaseWithReparse(urlListener, null);
+ } catch (ex) {
+ // We expect this - it indicates the DB is not valid. But it will have
+ // kicked off an async reparse, so we need to wait for the listener.
+ Assert.equal(ex.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ await urlListener.promise;
+ }
+
+ // Check that the message moved over intact.
+ let gotHdr = folder2.msgDatabase.getMsgHdrForMessageID(expectedID);
+ let gotMsg = mailTestUtils.loadMessageToString(folder2, gotHdr);
+ // NOTE: With maildir store, the message seems to gain an extra trailing
+ // "\n" during the copy. See Bug 1716651.
+ // For now, use .trim() as a workaround.
+ Assert.equal(gotMsg.trim(), expectedMsg.trim());
+});
diff --git a/comm/mailnews/base/test/unit/test_detachToFile.js b/comm/mailnews/base/test/unit/test_detachToFile.js
new file mode 100644
index 0000000000..3c1913a9a8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_detachToFile.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+/*
+ * Tests nsIMessenger's detachAttachmentsWOPrompts
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+function SaveAttachmentCallback() {
+ this.attachments = null;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+}
+
+SaveAttachmentCallback.prototype = {
+ callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) {
+ this.attachments = aMimeMessage.allAttachments;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var gCallbackObject = new SaveAttachmentCallback();
+
+add_setup(async function () {
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+});
+
+add_task(async function startCopy() {
+ // Get a message into the local filestore.
+ let mailFile = do_get_file("../../../data/external-attach-test");
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ mailFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ listener,
+ null
+ );
+ await listener.promise;
+});
+
+// process the message through mime
+add_task(async function startMime() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ gCallbackObject,
+ gCallbackObject.callback,
+ true // allowDownload
+ );
+
+ await gCallbackObject.promise;
+});
+
+// detach any found attachments
+add_task(async function startDetach() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+ let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey);
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let attachment = gCallbackObject.attachments[0];
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+
+ messenger.detachAttachmentsWOPrompts(
+ do_get_profile(),
+ [attachment.contentType],
+ [attachment.url],
+ [attachment.name],
+ [msgURI],
+ listener
+ );
+ await listener.promise;
+});
+
+/**
+ * Test that the detachment was successful.
+ */
+add_task(async function testDetach() {
+ // The message contained a file "check.pdf" which should
+ // now exist in the profile directory.
+ let checkFile = do_get_profile().clone();
+ checkFile.append("check.pdf");
+ Assert.ok(checkFile.exists());
+
+ // The message should now have a detached attachment. Read the message,
+ // and search for "AttachmentDetached" which is added on detachment.
+
+ // Get the message header
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ let messageContent = await getContentFromMessage(msgHdr);
+ Assert.ok(messageContent.includes("AttachmentDetached"));
+});
+
+/**
+ * Get the full message content.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - Message whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_emptyTrash.js b/comm/mailnews/base/test/unit/test_emptyTrash.js
new file mode 100644
index 0000000000..528691117e
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_emptyTrash.js
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for empty trash
+ *
+ * Currently tested:
+ * - Empty local trash
+ * TODO
+ * - Empty imap trash
+ */
+
+// Globals
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gMsgFile1;
+var gLocalTrashFolder;
+var gCurTestNum;
+var gMsgHdrs = [];
+var gRootFolder;
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ let hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, function () {
+ doTest(++gCurTestNum);
+ });
+ },
+};
+
+var urlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully copied.
+ Assert.equal(aExitCode, 0);
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, function () {
+ doTest(++gCurTestNum);
+ });
+ },
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function deleteMessages(srcFolder, items) {
+ srcFolder.deleteMessages(items, null, false, true, copyListener, true);
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Copying message from file
+ function testCopyFileMessage1() {
+ copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+
+ // Delete message
+ function testDeleteMessage() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ let inboxDB = localAccountUtils.inboxFolder.msgDatabase;
+ gMsgHdrs[0].hdr = inboxDB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Now delete the message
+ deleteMessages(localAccountUtils.inboxFolder, [gMsgHdrs[0].hdr]);
+ },
+ function emptyTrash() {
+ gRootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ gLocalTrashFolder = gRootFolder.getChildNamed("Trash");
+ // hold onto a db to make sure that empty trash deals with the case
+ // of someone holding onto the db, but the trash folder has a null db.
+ let gLocalTrashDB = gLocalTrashFolder.msgDatabase; // eslint-disable-line no-unused-vars
+ gLocalTrashFolder.msgDatabase = null;
+ // this is synchronous
+ gLocalTrashFolder.emptyTrash(null);
+ // check that the trash folder is 0 size, that the db has a 0 message count
+ // and has no messages.
+ Assert.equal(0, gLocalTrashFolder.filePath.fileSize);
+ Assert.equal(0, gLocalTrashFolder.msgDatabase.dBFolderInfo.numMessages);
+ let msgs = [...gLocalTrashFolder.msgDatabase.enumerateMessages()];
+ Assert.equal(0, msgs.length);
+ urlListener.OnStopRunningUrl(null, 0);
+ },
+];
+
+// Our listener, which captures events.
+function gMFListener() {}
+gMFListener.prototype = {
+ folderDeleted(aFolder) {
+ aFolder.msgDatabase = null;
+ },
+};
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ // Load up a message so that we can copy it in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ // our front end code clears the msg db when it gets told the folder for
+ // an open view has been deleted - so simulate that.
+ var folderDeletedListener = new gMFListener();
+ MailServices.mfn.addListener(
+ folderDeletedListener,
+ nsIMFNService.folderDeleted
+ );
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of all the operations.
+ do_test_pending();
+
+ // Do the test.
+ doTest(1);
+}
+
+function doTest(test) {
+ if (test <= gTestArray.length) {
+ gCurTestNum = test;
+
+ var testFn = gTestArray[test - 1];
+ // Set a limit of three seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gCurTestNum == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " + testFn.name
+ );
+ }
+ });
+ try {
+ testFn();
+ } catch (ex) {
+ dump(ex);
+ }
+ } else {
+ gMsgHdrs = null;
+ do_test_finished(); // for the one in run_test()
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js b/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js
new file mode 100644
index 0000000000..0fd09a543a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+/**
+ * This tests that we cleanup the account prefs when a pop3 account has
+ * been deferred to a hidden account.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with a pop3 account deferred to a hidden account.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "Smart Mailboxes");
+ Services.prefs.setCharPref("mail.server.server2.type", "none");
+ Services.prefs.setBoolPref("mail.server.server2.hidden", true);
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server4.deferred_to_account",
+ "account2"
+ );
+ Services.prefs.setCharPref("mail.server.server5.hostname", "mail.host5.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2,account4,account5"
+ );
+ // Set the default account to one we're going to get rid of. The account manager
+ // should recover relatively gracefully.
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account1");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3); // hidden account not included
+ let server4 = MailServices.accounts
+ .getAccount("account4")
+ .incomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server4.deferredToAccount, "account1");
+ let server5 = MailServices.accounts
+ .getAccount("account5")
+ .incomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account1");
+}
diff --git a/comm/mailnews/base/test/unit/test_folderCompact.js b/comm/mailnews/base/test/unit/test_folderCompact.js
new file mode 100644
index 0000000000..876672c0e5
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderCompact.js
@@ -0,0 +1,335 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for folder compaction
+ *
+ * Currently tested:
+ * - Compacting local folders
+ * TODO
+ * - Compacting imap offline stores.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Globals
+var gMsgFile1, gMsgFile2, gMsgFile3;
+var gMsg2ID = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+var gMsg3ID = "4849BF7B.2030800@example.com";
+var gLocalFolder2;
+var gLocalFolder3;
+// After a compact (or other operation), this is what we expect the
+// folder size to be.
+var gExpectedFolderSize;
+var gMsgHdrs = [];
+var gExpectedInboxSize;
+var gExpectedFolder2Size;
+var gExpectedFolder3Size;
+// Use this to account for the size of the separating line after the message
+// body. The compactor tries to preserve the convention already in the mbox,
+// and we know that our test messages have CRLFs, so that's what we'll use.
+var gSeparatorLine = "\r\n";
+
+// Transfer message keys between function calls.
+var gMsgKeys = [];
+
+// nsIMsgCopyServiceListener implementation
+var copyListenerWrap = {
+ SetMessageKey(aKey) {
+ let hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ },
+};
+
+var urlListenerWrap = {
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully copied.
+ Assert.equal(aExitCode, 0);
+
+ if (gMsgKeys.length > 0) {
+ // Bug 854798: Check if the new message keys are the same as before compaction.
+ let folderMsgs = [...gMsgKeys.folder.messages];
+ // First message was deleted so skip it in the old array.
+ let expectedKeys = [...gMsgKeys].slice(1);
+ Assert.equal(folderMsgs.length, expectedKeys.length);
+ for (let i = 1; i < expectedKeys.length; i++) {
+ Assert.equal(folderMsgs[i], expectedKeys[i]);
+ }
+ gMsgKeys.length = 0;
+ }
+ },
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ listener,
+ null
+ );
+ return listener.promise;
+}
+
+function copyMessages(items, isMove, srcFolder, destFolder) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyMessages(
+ srcFolder,
+ items,
+ destFolder,
+ isMove,
+ listener,
+ null,
+ true
+ );
+ return listener.promise;
+}
+
+function deleteMessages(srcFolder, items) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ srcFolder.deleteMessages(items, null, false, true, listener, true);
+ return listener.promise;
+}
+
+function calculateFolderSize(folder) {
+ let msgDB = folder.msgDatabase;
+ let totalSize = 0;
+ for (let header of msgDB.enumerateMessages()) {
+ totalSize += header.messageSize + gSeparatorLine.length;
+ }
+ return totalSize;
+}
+
+function verifyMsgOffsets(folder) {
+ let msgDB = folder.msgDatabase;
+ let enumerator = msgDB.enumerateMessages();
+ if (enumerator) {
+ for (let header of enumerator) {
+ if (header instanceof Ci.nsIMsgDBHdr) {
+ let storeToken = header.getStringProperty("storeToken");
+ Assert.equal(storeToken, header.messageOffset);
+ }
+ }
+ }
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Copying messages from files
+ async function testCopyFileMessage1() {
+ await copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+ async function testCopyFileMessage2() {
+ await copyFileMessage(gMsgFile2, localAccountUtils.inboxFolder, false);
+ },
+ async function testCopyFileMessage3() {
+ await copyFileMessage(gMsgFile3, localAccountUtils.inboxFolder, true);
+ showMessages(
+ localAccountUtils.inboxFolder,
+ "after initial 3 messages copy to inbox"
+ );
+ },
+
+ // Moving/copying messages
+ async function testCopyMessages1() {
+ await copyMessages(
+ [gMsgHdrs[0].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ async function testCopyMessages2() {
+ await copyMessages(
+ [gMsgHdrs[1].hdr, gMsgHdrs[2].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ showMessages(gLocalFolder2, "after copying 3 messages");
+ },
+ async function testMoveMessages1() {
+ await copyMessages(
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalFolder3
+ );
+
+ showMessages(localAccountUtils.inboxFolder, "after moving 2 messages");
+ showMessages(gLocalFolder3, "after moving 2 messages");
+ },
+
+ // Deleting messages
+ async function testDeleteMessages1() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ var folder3DB = gLocalFolder3.msgDatabase;
+ gMsgHdrs[0].hdr = folder3DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Store message keys before deletion and compaction.
+ gMsgKeys.folder = gLocalFolder3;
+ for (let header of gLocalFolder3.messages) {
+ gMsgKeys.push(header.messageKey);
+ }
+
+ // Now delete the message
+ await deleteMessages(gLocalFolder3, [gMsgHdrs[0].hdr]);
+
+ showMessages(gLocalFolder3, "after deleting 1 message to trash");
+ },
+ async function compactFolder() {
+ gExpectedFolderSize = calculateFolderSize(gLocalFolder3);
+ Assert.notEqual(gLocalFolder3.expungedBytes, 0);
+ let listener = new PromiseTestUtils.PromiseUrlListener(urlListenerWrap);
+ gLocalFolder3.compact(listener, null);
+ await listener.promise;
+
+ showMessages(gLocalFolder3, "after compact");
+ },
+ async function testDeleteMessages2() {
+ Assert.equal(gExpectedFolderSize, gLocalFolder3.filePath.fileSize);
+ verifyMsgOffsets(gLocalFolder3);
+ var folder2DB = gLocalFolder2.msgDatabase;
+ gMsgHdrs[0].hdr = folder2DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Store message keys before deletion and compaction.
+ gMsgKeys.folder = gLocalFolder2;
+ for (let header of gLocalFolder2.messages) {
+ gMsgKeys.push(header.messageKey);
+ }
+
+ // Now delete the message
+ await deleteMessages(gLocalFolder2, [gMsgHdrs[0].hdr]);
+
+ showMessages(gLocalFolder2, "after deleting 1 message");
+ },
+ async function compactAllFolders() {
+ gExpectedInboxSize = calculateFolderSize(localAccountUtils.inboxFolder);
+ gExpectedFolder2Size = calculateFolderSize(gLocalFolder2);
+ gExpectedFolder3Size = calculateFolderSize(gLocalFolder3);
+
+ // Save the first message key, which will change after compact with
+ // rebuild.
+ let f2m2Key =
+ gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID).messageKey;
+
+ // force expunged bytes count to get cached.
+ gLocalFolder2.expungedBytes;
+ // mark localFolder2 as having an invalid db, and remove it
+ // for good measure.
+ gLocalFolder2.msgDatabase.summaryValid = false;
+ gLocalFolder2.msgDatabase = null;
+ gLocalFolder2.ForceDBClosed();
+ let dbPath = gLocalFolder2.filePath;
+ dbPath.leafName = dbPath.leafName + ".msf";
+ dbPath.remove(false);
+
+ showMessages(localAccountUtils.inboxFolder, "before compactAll");
+ // Save the key for the inbox message, we'll check after compact that it
+ // did not change.
+ let preInboxMsg3Key =
+ localAccountUtils.inboxFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsg3ID
+ ).messageKey;
+
+ // We used to check here that the keys did not change during rebuild.
+ // But that is no true in general, it was only conicidental since the
+ // checked folder had never been compacted, so the key equaled the offset.
+ // We do not in guarantee that, indeed after rebuild we expect the keys
+ // to change.
+ let checkResult = {
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully compacted.
+ Assert.equal(aExitCode, 0);
+ },
+ };
+ let listener = new PromiseTestUtils.PromiseUrlListener(checkResult);
+ localAccountUtils.inboxFolder.compactAll(listener, null);
+ await listener.promise;
+
+ showMessages(localAccountUtils.inboxFolder, "after compactAll");
+ showMessages(gLocalFolder2, "after compactAll");
+
+ // For the inbox, which was compacted but not rebuild, key is unchanged.
+ let postInboxMsg3Key =
+ localAccountUtils.inboxFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsg3ID
+ ).messageKey;
+ Assert.equal(preInboxMsg3Key, postInboxMsg3Key);
+
+ // For folder2, which was rebuilt, keys change but all messages should exist.
+ let message2 = gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID);
+ Assert.ok(message2);
+ Assert.ok(gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg3ID));
+
+ // In folder2, gMsg2ID is the first message. After compact with database
+ // rebuild, that key has now changed.
+ Assert.notEqual(message2.messageKey, f2m2Key);
+ },
+ function lastTestCheck() {
+ Assert.equal(
+ gExpectedInboxSize,
+ localAccountUtils.inboxFolder.filePath.fileSize
+ );
+ Assert.equal(gExpectedFolder2Size, gLocalFolder2.filePath.fileSize);
+ Assert.equal(gExpectedFolder3Size, gLocalFolder3.filePath.fileSize);
+ verifyMsgOffsets(gLocalFolder2);
+ verifyMsgOffsets(gLocalFolder3);
+ verifyMsgOffsets(localAccountUtils.inboxFolder);
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ // Load up some messages so that we can copy them in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ gMsgFile2 = do_get_file("../../../data/bugmail11");
+ gMsgFile3 = do_get_file("../../../data/draft1");
+
+ // Create another folder to move and copy messages around, and force initialization.
+ gLocalFolder2 = localAccountUtils.rootFolder.createLocalSubfolder("folder2");
+
+ // Create a third folder for more testing.
+ gLocalFolder3 = localAccountUtils.rootFolder.createLocalSubfolder("folder3");
+
+ gTestArray.forEach(x => add_task(x));
+ run_next_test();
+}
+
+// debug utility to show the key/offset/ID relationship of messages in a folder
+function showMessages(folder, text) {
+ dump(`***** Show messages for folder <${folder.name}> "${text} *****\n`);
+ for (let hdr of folder.messages) {
+ dump(
+ ` key: ${hdr.messageKey} offset: ${hdr.messageOffset} size: ${hdr.messageSize} ID: ${hdr.messageId}\n`
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_folderCompact2.js b/comm/mailnews/base/test/unit/test_folderCompact2.js
new file mode 100644
index 0000000000..dc9d0b865a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderCompact2.js
@@ -0,0 +1,300 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/*
+ * A few more specific mbox compaction tests for local folders:
+ * - mbox file contains exactly what we expect after compaction?
+ * test_folderCompact.js tends to rely on checking values in the msgDB.
+ * - works with messages larger than compaction code internal buffer?
+ * - X-Mozilla-Status/Status2/Keys headers handled as expected?
+ *
+ * Note: all these tests perform a seemingly-arbitrary delete.
+ * This is to trigger the folder compactor to actually do work. Without that
+ * delete it tends to think that compaction isn't required and does nothing.
+ */
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Escape a string for more useful debug output - show EOLs and spaces.
+function esc(s) {
+ s = s.replace(/\r/g, "\\r");
+ s = s.replace(/\n/g, "\\n\n");
+ s = s.replace(/ /g, "\u2420"); // U+2420 SYMBOL FOR SPACE
+ return s;
+}
+
+// Load raw messages into folder (and sanitycheck them).
+function loadMsgs(folder, inputMsgs) {
+ folder.addMessageBatch(inputMsgs);
+ // Make sure all the loaded messages are the expected size.
+ // If this fails, it probably means addMessageBatch() no longer assumes input
+ // data is mbox format, which is good! See Bug 1763263.
+ let hdrs = Array.from(folder.messages);
+ for (let i = 0; i < hdrs.length; ++i) {
+ Assert.equal(
+ inputMsgs[i].length,
+ hdrs[i].messageSize,
+ `Loaded message ${i} should match size in msgDB`
+ );
+ }
+}
+
+// Delete the specified messages.
+async function deleteMsgs(folder, indexesToDelete) {
+ let hdrs = Array.from(folder.messages);
+ let doomed = indexesToDelete.map(i => hdrs[i]);
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(doomed, null, false, true, listener, true);
+ await listener.promise;
+}
+
+// Check the raw mbox file against what we expect to see.
+async function checkMbox(folder, outputMsgs) {
+ let bytes = await IOUtils.read(folder.filePath.path);
+ let mbox = "";
+ for (let b of bytes) {
+ mbox += String.fromCharCode(b);
+ }
+
+ let expected = "";
+ for (let raw of outputMsgs) {
+ // mbox has blank line between messages
+ expected += raw + "\n";
+ }
+
+ // Force all EOLs to linefeeds for comparisons.
+ // EOLs are handled inconsistently - most code will just leave EOLs as they
+ // come in, but new EOLs (e.g. added between messages in mbox) will use
+ // platform native EOLs. So our cheap and cheerful hack here is to just
+ // ditch all CRs and use pure LFs.
+ mbox = mbox.replace(/\r/g, "");
+ expected = expected.replace(/\r/g, "");
+
+ if (mbox != expected) {
+ // Pretty-print before we assert. Makes life so much easier.
+ dump(`=======mbox=========\n${esc(mbox)}\n============\n`);
+ dump(`=======expected=====\n${esc(expected)}\n============\n`);
+ }
+ Assert.ok(mbox == expected, "mbox should contain expected data");
+}
+
+// Some chunks from which we'll construct test messages.
+
+// These are the default X-Mozilla- headers for local folders (they are
+// re-written in place when flags and keywords are modified).
+let xhdrs =
+ `X-Mozilla-Status: 0000\r\n` +
+ `X-Mozilla-Status2: 00010000\r\n` + // 'New' flag is set
+ `X-Mozilla-Keys: \r\n`;
+
+let hdrs1 =
+ "Date: Fri, 21 Nov 1997 09:26:06 -0600\r\n" +
+ "From: bob@invalid\r\n" +
+ "Subject: Test message 1\r\n" +
+ "Message-ID: <blah1@invalid>\r\n";
+
+let bod1 = "Body of message 1.\r\n";
+
+let hdrs2 =
+ "Date: Fri, 21 Nov 1997 10:55:32 -0600\r\n" +
+ "From: bob@invalid\r\n" +
+ "Subject: Test message 2\r\n" +
+ "Message-ID: <blah2@invalid>\r\n";
+
+let bod2 = "Body of message2.\r\n";
+
+let hdrs3 =
+ `Date: Fri, 21 Nov 1997 11:09:14 -0600\r\n` +
+ `From: bob@invalid\r\n` +
+ `Message-ID: <blah3@invalid>\r\n` +
+ `Subject: Test message 3\r\n`;
+
+let bod3 = `message\r\nthree\r\nis multiple\r\nlines.\r\n`;
+
+let from = "From \r\n";
+
+// Check compact works after a simple delete.
+add_task(async function testSimple() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ let inMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete message msg2.
+ // Out expected output:
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact adds missing X-Mozilla- headers.
+add_task(async function testMissingXMozillaHdrs() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // No X-Mozilla-* headers on input.
+ let inMsgs = [
+ `${from}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+ // Out expected output.
+ // Compact should have added X-Mozilla-* headers.
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check localfolder compact works on messages are bigger than internal read buffer.
+add_task(async function testBigMessages() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // Compaction uses buffer of around 16KB, so we'll go way bigger.
+ let targSize = 256 * 1024;
+
+ let inMsgs = [];
+ let outMsgs = [];
+ let doomed = [0, 1]; // We'll delete the first 2 messages.
+ for (let i = 0; i < 5; ++i) {
+ let raw =
+ `From \r\n` +
+ xhdrs +
+ `Date: Fri, 21 Nov 1997 09:55:06 -0600\r\n` +
+ `From: bob${i}@invalid\r\n` +
+ `Message-ID: <blah${i}@invalid>\r\n` +
+ `\r\n`;
+ while (raw.length < targSize) {
+ raw +=
+ "BlahBlahBlahBlahBlahBlahBlahBlahBlah" +
+ "BlahBlahBlahBlahBlahBlahBlahBlahBlah\r\n";
+ }
+ raw += `\r\n`;
+ inMsgs.push(raw);
+ if (!doomed.includes(i)) {
+ outMsgs.push(raw);
+ }
+ }
+
+ loadMsgs(inbox, inMsgs);
+
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ // outMsgs is what we expect to see in the mbox.
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact moves X-Mozilla-* headers to start of
+// header block.
+add_task(async function testMoveXMozillaHdrs() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // These have X-Mozilla-* headers after all the other headers.
+ let inMsgs = [
+ `${from}${hdrs1}${xhdrs}\r\n${bod1}\r\n`,
+ `${from}${hdrs2}${xhdrs}\r\n${bod2}\r\n`,
+ `${from}${hdrs3}${xhdrs}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+ // The messages we expect to see in the final mbox.
+ // Compact should have moved the X-Mozilla-* headers to the front.
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact handles large X-Mozilla-Keys value.
+add_task(async function testBigXMozillaKeys() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ let bigKeyword =
+ "HugeGreatBigStupidlyLongKeywordNameWhichWillDefinitelyOverflowThe80" +
+ "CharactersUsuallyReservedInTheKeywordsHeaderForInPlaceEditing";
+
+ let inMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+
+ let bigxhdrs =
+ `X-Mozilla-Status: 0000\r\n` +
+ `X-Mozilla-Status2: 00010000\r\n` + // 'New' flag is set
+ `X-Mozilla-Keys: ${bigKeyword}\r\n`;
+
+ // The messages we expect to see in the final mbox:
+ let outMsgs = [
+ `${from}${bigxhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+
+ let msgs = Array.from(inbox.messages);
+ inbox.addKeywordsToMessages([msgs[0]], bigKeyword);
+
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
diff --git a/comm/mailnews/base/test/unit/test_folderLookupService.js b/comm/mailnews/base/test/unit/test_folderLookupService.js
new file mode 100644
index 0000000000..039fa2a383
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderLookupService.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+/**
+ * This tests that nsIFolderLookupService works according to specification.
+ */
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var kRootURI = "mailbox://nobody@Local%20Folders";
+
+add_task(async function test_fls_basics() {
+ let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService(
+ Ci.nsIFolderLookupService
+ );
+
+ // Make sure that the local mail account exists.
+ localAccountUtils.loadLocalMailAccount();
+
+ // There should exist an inbox.
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Inbox"),
+ localAccountUtils.inboxFolder
+ );
+
+ // Can we get the root folder?
+ let root = localAccountUtils.rootFolder;
+ Assert.equal(fls.getFolderForURL(kRootURI), root);
+
+ // The child folder Child doesn't exist yet... make sure that fls doesn't
+ // return it yet
+ Assert.equal(fls.getFolderForURL(kRootURI + "/Child"), null);
+
+ // Create the Child folder, and test we can find it.
+ // (NOTE: createSubFolder() can be async for IMAP folders. But for now
+ // we know we're using local folders and it'll happen right away).
+ root.createSubfolder("Child", null);
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Child"),
+ root.getChildNamed("Child")
+ );
+
+ // Try it again... it should load from cache this time.
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Child"),
+ root.getChildNamed("Child")
+ );
+
+ // Now delete the folder; we should be unable to find it
+ root.propagateDelete(root.getChildNamed("Child"), true);
+ Assert.equal(fls.getFolderForURL(kRootURI + "/Child"), null);
+
+ Assert.equal(fls.getFolderForURL(kRootURI + "/"), null);
+ Assert.equal(
+ fls.getFolderForURL("mailbox://idonotexist@Local%20Folders"),
+ null
+ );
+});
+
+add_task(async function test_unicode_uris() {
+ let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService(
+ Ci.nsIFolderLookupService
+ );
+ localAccountUtils.loadLocalMailAccount();
+ let root = localAccountUtils.rootFolder;
+
+ // Create a folder with non-ASCII characters.
+ // Unicode abuse - dotless letter i and a metal umlaut over the n.
+ let tapName = "Sp\u0131n\u0308al Tap";
+ root.createSubfolder(tapName, null);
+
+ // Make sure we can find it.
+ // (URI is percent-escaped utf-8)
+ let tapNameEscaped = "Sp%C4%B1n%CC%88al%20Tap";
+ if (AppConstants.platform == "win") {
+ // For !ConvertibleToNative(), folder name is hashed on Windows.
+ tapNameEscaped = "a2d874f7";
+ }
+ let tapURI = kRootURI + "/" + tapNameEscaped;
+ let tap = fls.getFolderForURL(tapURI);
+ Assert.equal(tap.URI, tapURI);
+});
diff --git a/comm/mailnews/base/test/unit/test_folderStringProperties.js b/comm/mailnews/base/test/unit/test_folderStringProperties.js
new file mode 100644
index 0000000000..8f508625b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderStringProperties.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+/*
+ * Test the nsIMsgFolder .(get|set)StringProperty methods.
+ */
+
+add_task(function test_string_properties() {
+ localAccountUtils.loadLocalMailAccount();
+ let root = localAccountUtils.incomingServer.rootMsgFolder;
+
+ // Ensure unset properties return an error.
+ Assert.throws(function () {
+ root.getStringProperty("this-property-doesnt-exist");
+ }, /NS_ERROR_.*/);
+
+ // Check basic set/get operation.
+ root.setStringProperty("test-property", "wibble");
+ Assert.equal(root.getStringProperty("test-property"), "wibble");
+
+ // Keys are case-sensitive.
+ Assert.throws(function () {
+ root.getStringProperty("TEST-PROPERTY");
+ }, /NS_ERROR_.*/);
+
+ // Values with non-latin chars?
+ root.setStringProperty("test-property", "日本語");
+ Assert.equal(root.getStringProperty("test-property"), "日本語");
+
+ // Check that things stay as strings, even if they are values that could
+ // be misinterpreted in JSON.
+ root.setStringProperty("test-property", "");
+ Assert.equal(root.getStringProperty("test-property"), "");
+
+ root.setStringProperty("test-property", "null");
+ Assert.equal(root.getStringProperty("test-property"), "null");
+
+ root.setStringProperty("test-property", "0");
+ Assert.equal(root.getStringProperty("test-property"), "0");
+});
diff --git a/comm/mailnews/base/test/unit/test_formatFileSize.js b/comm/mailnews/base/test/unit/test_formatFileSize.js
new file mode 100644
index 0000000000..1269fd33c3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_formatFileSize.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/*
+ * Tests for the formatFileSize method.
+ */
+
+var gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+);
+
+var gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+function isDigit(c) {
+ return "0123456789".includes(c);
+}
+
+function test_formatFileSize(aArgs) {
+ const strings = {
+ b: "byteAbbreviation2",
+ kb: "kiloByteAbbreviation2",
+ mb: "megaByteAbbreviation2",
+ gb: "gigaByteAbbreviation2",
+ tb: "teraByteAbbreviation2",
+ pb: "petaByteAbbreviation2",
+ };
+
+ let actual = gMessenger.formatFileSize(aArgs.bytes, aArgs.useKB);
+ let expected = gStringBundle
+ .GetStringFromName(strings[aArgs.units])
+ .replace("%.*f", aArgs.mantissa);
+
+ // If the actual string contains a non-numeric character at the position
+ // where we'd expect a decimal separator, assume it is a localized separator
+ // and just convert it to a dot for easy comparing.
+ let separatorPos = aArgs.mantissa.indexOf(".");
+ if (!isDigit(actual.charAt(separatorPos))) {
+ actual =
+ actual.substring(0, separatorPos) + "." + actual.substr(separatorPos + 1);
+ }
+
+ Assert.equal(actual, expected);
+}
+
+var test_data = [
+ { bytes: 0, useKB: false, mantissa: "0", units: "b" },
+ { bytes: 1, useKB: false, mantissa: "1", units: "b" },
+ { bytes: 10, useKB: false, mantissa: "10", units: "b" },
+ { bytes: 999, useKB: false, mantissa: "999", units: "b" },
+ { bytes: 1000, useKB: false, mantissa: "1.0", units: "kb" },
+ { bytes: 1024, useKB: false, mantissa: "1.0", units: "kb" },
+ { bytes: 10 * 1024, useKB: false, mantissa: "10.0", units: "kb" },
+ { bytes: 999 * 1024, useKB: false, mantissa: "999", units: "kb" },
+ { bytes: 1000 * 1024, useKB: false, mantissa: "1.0", units: "mb" },
+ { bytes: 1024 * 1024, useKB: false, mantissa: "1.0", units: "mb" },
+ { bytes: 10 * 1024 * 1024, useKB: false, mantissa: "10.0", units: "mb" },
+ { bytes: 999 * 1024 * 1024, useKB: false, mantissa: "999", units: "mb" },
+ { bytes: 1000 * 1024 * 1024, useKB: false, mantissa: "1.0", units: "gb" },
+ { bytes: 1024 * 1024 * 1024, useKB: false, mantissa: "1.0", units: "gb" },
+ {
+ bytes: 10 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "gb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "gb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "tb",
+ },
+ {
+ bytes: 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "tb",
+ },
+ {
+ bytes: 10 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "tb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "tb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 10 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "pb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "pb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1000",
+ units: "pb",
+ },
+
+ { bytes: 0, useKB: true, mantissa: "0", units: "kb" },
+ { bytes: 1, useKB: true, mantissa: "0.1", units: "kb" },
+ { bytes: 500, useKB: true, mantissa: "0.5", units: "kb" },
+ { bytes: 999, useKB: true, mantissa: "1.0", units: "kb" },
+];
+
+add_task(function test_format_file_size() {
+ test_data.map(entry => {
+ test_formatFileSize(entry);
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js b/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js
new file mode 100644
index 0000000000..b803619e21
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test suite for GetMsgTextFromStream.
+ *
+ * Currently tests: text/plain, text/html -- with tags stripped and without,
+ * base64, multipart.
+ * Does not currently test: quoted-printable, stripping quotes, UTF-8, small values of
+ * bytesToRead.
+ */
+var kDataRoot = "../../../data/";
+
+function create(fileName, bytes, compressQuotes, stripHTML, outContentType) {
+ return {
+ name: fileName,
+ bytesToRead: bytes,
+ compressQuotes,
+ stripHTML,
+ contentType: outContentType,
+ };
+}
+
+var gTestFiles = [
+ create("basic1", 1024, false, false, "text/plain"), // Simple plain text
+ create("basic1", 1024, false, true, "text/plain"), // should be same as above
+ create("basic2", 1024, false, false, "text/html"), // Simple HTML
+ create("basic3", 1024, false, true, "text/html"), // HTML with tags stripped out
+ create("basic4", 1024, false, false, "text/plain"), // No content type, should be assumed to be text/plain
+ create("basic4", 1024, false, true, "text/plain"),
+ create("basic5", 1024, false, false, "text/plain"), // HTML content in text/plain
+ create("basic5", 1024, false, true, "text/plain"),
+ create("base64-1", 1024, false, false, "text/plain"), // base64 text/plain
+ create("base64-1", 1024, false, true, "text/plain"),
+ create("base64-2", 1024, false, false, "text/html"), // base64 text/html
+ create("base64-3", 1024, false, true, "text/html"), // strip out tags here
+ create("multipart1", 1024, false, false, "text/plain"), // basic multipart message
+ create("multipart1", 1024, false, true, "text/plain"),
+ create("multipart2", 1024, false, false, "text/html"), // multipart HTML
+ create("multipart3", 1024, false, true, "text/html"),
+ create("multipart4", 1024, false, false, "text/plain"), // text with no headers
+ create("multipart4", 1024, false, true, "text/plain"),
+ create("multipart-base64-1", 1024, false, false, "text/plain"), // base64 encoded text
+ create("multipart-base64-1", 1024, false, true, "text/plain"),
+ create("multipart-base64-2", 1024, false, false, "text/html"),
+ create("multipart-base64-3", 1024, false, true, "text/html"),
+ create("multipart-complex1", 1024, false, true, "text/html"), // Things get more complex here
+ create("multipart-complex2", 1024, false, false, "text/plain"),
+ create("multipart-complex2", 1024, false, true, "text/plain"),
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ var folder = localAccountUtils.incomingServer.rootMsgFolder;
+
+ gTestFiles.forEach(function (test) {
+ dump("Testing " + test.name + "\n");
+ var inFile = do_get_file(kDataRoot + test.name);
+ var inStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ inStream.init(inFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+ // Now get the message body using getMsgTextFromStream
+ var contentType = {};
+ var body = folder.getMsgTextFromStream(
+ inStream,
+ "",
+ test.bytesToRead,
+ 65536,
+ test.compressQuotes,
+ test.stripHTML,
+ contentType
+ );
+
+ // Now we need to compare the output
+ Assert.equal(test.contentType, contentType.value);
+
+ var resultFile = do_get_file(kDataRoot + test.name + ".out");
+ var actualBody = mailTestUtils.loadFileToString(resultFile, "UTF-8");
+ Assert.equal(body, actualBody);
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js b/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js
new file mode 100644
index 0000000000..c3165ba720
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+/*
+ * Testing header folding in nsParseMailMessageState::ParseHeaders(),
+ * see bug 1454257 and bug 1456001.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var hdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continueTest();
+ },
+ };
+
+ // Get a message into the local filestore.
+ var message = do_get_file("../../../data/badly-folded-headers.eml");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ message,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function continueTest() {
+ Assert.equal(hdr.author, "sender@example.com");
+ Assert.equal(
+ hdr.recipients,
+ '"Recipient with spaces" <recipient@example.com>'
+ );
+ Assert.equal(
+ hdr.subject,
+ "Badly folded headers, one line with space between To and From"
+ );
+ hdr = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/base/test/unit/test_hostnameUtils.js b/comm/mailnews/base/test/unit/test_hostnameUtils.js
new file mode 100644
index 0000000000..5f8839165d
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_hostnameUtils.js
@@ -0,0 +1,276 @@
+/* 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/. */
+
+/*
+ * Tests for hostnameUtils.jsm.
+ */
+
+var {
+ isLegalHostName,
+ isLegalHostNameOrIP,
+ isLegalIPAddress,
+ isLegalIPv4Address,
+ isLegalIPv6Address,
+ isLegalLocalIPAddress,
+} = ChromeUtils.import("resource:///modules/hostnameUtils.jsm");
+
+/**
+ * Checks if valid and invalid IPs are properly allowed or rejected.
+ */
+function test_IPaddresses() {
+ const kIPsToTest = [
+ // isValid, IP addr. isIPv6, isLocal, extend, result
+ // IPv4
+ [true, "1.2.3.4", false, false, false],
+ [true, "123.245.111.222", false, false, false],
+ [true, "255.255.255.255", false, false, false],
+ [true, "1.2.0.4", false, false, false],
+ [true, "1.2.3.4", false, false, false],
+ [true, "127.1.2.3", false, true, false],
+ [true, "10.1.2.3", false, true, false],
+ [true, "192.168.2.3", false, true, false],
+
+ [false, "1.2.3.4.5", false, false, false],
+ [false, "1.2.3", false, false, false],
+ [false, "1.2.3.", false, false, false],
+ [false, ".1.2.3", false, false, false],
+ [false, "1.2.3.256", false, false, false],
+ [false, "1.2.3.12345", false, false, false],
+ [false, "1.2..123", false, false, false],
+ [false, "1", false, false, false],
+ [false, "", false, false, false],
+ [false, "0.1.2.3", false, false, false],
+ [false, "0.0.2.3", false, false, false],
+ [false, "0.0.0.0", false, false, false],
+ [false, "1.2.3.d", false, false, false],
+ [false, "a.b.c.d", false, false, false],
+ [false, "a.b.c.d", false, false, true],
+ // IPv6
+ [
+ true,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:db8:85a3:0:0:8a2e:370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:db8:85a3::8a2e:370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:0000",
+ ],
+ [
+ true,
+ "::ffff:c000:0280",
+ true,
+ false,
+ false,
+ "0000:0000:0000:0000:0000:ffff:c000:0280",
+ ],
+ [
+ true,
+ "::ffff:192.0.2.128",
+ true,
+ false,
+ false,
+ "0000:0000:0000:0000:0000:ffff:c000:0280",
+ ],
+ [
+ true,
+ "2001:db8::1",
+ true,
+ false,
+ false,
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ ],
+ [
+ true,
+ "2001:DB8::1",
+ true,
+ false,
+ false,
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ ],
+ [
+ true,
+ "1:2:3:4:5:6:7:8",
+ true,
+ false,
+ false,
+ "0001:0002:0003:0004:0005:0006:0007:0008",
+ ],
+
+ [true, "::1", true, true, false, "0000:0000:0000:0000:0000:0000:0000:0001"],
+ [
+ true,
+ "::0000:0000:1",
+ true,
+ true,
+ false,
+ "0000:0000:0000:0000:0000:0000:0000:0001",
+ ],
+
+ [false, "::", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:73346", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:7334:1", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:7334x", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:03707334", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000x8a2e:0370:7334", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:::1", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:0000:some:junk", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:0000::192.0.2.359", true, false, false],
+ [false, "some::junk", true, false, false],
+ [false, "some_junk", true, false, false],
+
+ // Extended formats of IPv4, hex, octal, decimal up to DWORD
+ [true, "0xff.0x12.0x45.0x78", false, false, true, "255.18.69.120"],
+ [true, "01.0123.056.077", false, false, true, "1.83.46.63"],
+ [true, "0xff.2.3.4", false, false, true, "255.2.3.4"],
+ [true, "0xff.2.3.077", false, false, true, "255.2.3.63"],
+ [true, "0x7f.2.3.077", false, true, true, "127.2.3.63"],
+
+ [false, "0xZZ.1.2.3", false, false, true],
+ [false, "0x00.0123.056.077", false, false, true],
+ [false, "0x11.0123.056.078", false, false, true],
+ [false, "0x11.0123.056.0789", false, false, true],
+
+ [true, "1234566945", false, false, true, "73.149.255.33"],
+ [false, "12345", false, false, true],
+ [false, "123456789123456", false, false, true],
+
+ [true, "127.1", false, true, true, "127.0.0.1"],
+ [true, "0x7f.100", false, true, true, "127.0.0.100"],
+ [true, "0x7f.100.1000", false, true, true, "127.100.3.232"],
+ [true, "0xff.100.1024", false, false, true, "255.100.4.0"],
+ [true, "0xC0.0xA8.0x2A48", false, true, true, "192.168.42.72"],
+ [true, "0xC0.0xA82A48", false, true, true, "192.168.42.72"],
+ [true, "0xC0A82A48", false, true, true, "192.168.42.72"],
+ [true, "0324.062477106", false, false, true, "212.202.126.70"],
+
+ [false, "0.0.1000", false, false, true],
+ [false, "0324.06247710677", false, false, true],
+ ];
+
+ for (let item of kIPsToTest) {
+ let result = null;
+ let [isValid, address, isIPv6, isLocal, isExtended, wantedResult] = item;
+ if (!wantedResult) {
+ wantedResult = isValid ? address : null;
+ }
+
+ if (isIPv6) {
+ result = isLegalIPv6Address(address);
+ Assert.equal(result, wantedResult);
+ if (isValid) {
+ // If this is valid IPv6, it can't be valid IPv4. The opposite is unknown.
+ result = isLegalIPv4Address(address);
+ Assert.equal(result, null);
+ }
+ } else {
+ result = isLegalIPv4Address(address, isExtended);
+ Assert.equal(result, wantedResult);
+ if (isValid) {
+ // If this is valid IPv4, it can't be valid IPv6. The opposite is unknown.
+ result = isLegalIPv6Address(address);
+ Assert.equal(result, null);
+ }
+ }
+
+ result = isLegalIPAddress(address, isExtended);
+ Assert.equal(result, wantedResult);
+
+ if (isValid) {
+ // isLegalLocalIPAddress operates on a normalized address,
+ // not the original one.
+ result = isLegalLocalIPAddress(result);
+ Assert.equal(result, isLocal);
+ }
+
+ // If something is a valid IP, it also passes isLegalHostNameOrIP.
+ // However, an invalid IP string may still be a valid hostname.
+ // So only check success if the IP is valid.
+ result = isLegalHostNameOrIP(address, isExtended);
+ if (isValid) {
+ Assert.equal(result, wantedResult);
+ }
+ }
+}
+/**
+ * Checks if valid and invalid host names are properly allowed or rejected.
+ */
+function test_hostnames() {
+ const kHostsToTest = [
+ // isValid, hostname
+ [true, "localhost"],
+ [true, "some-server"],
+ [true, "server.company.invalid"],
+ [true, "server.comp-any.invalid"],
+ [true, "server.123.invalid"],
+ [true, "1server.123.invalid"],
+ [true, "1.2.3.4.5"],
+ [true, "very.log.sub.domain.name.invalid"],
+ [true, "1234567890"],
+ [true, "1234567890."], // FQDN
+ [true, "server.company.invalid."], // FQDN
+
+ [false, ""],
+ [false, "server.badcompany!.invalid"],
+ [false, "server._badcompany.invalid"],
+ [false, "server.bad_company.invalid"],
+ [false, "server.badcompany-.invalid"],
+ [false, "server.bad company.invalid"],
+ [false, "server.b…dcompany.invalid"],
+ [false, ".server.badcompany.invalid"],
+ [
+ false,
+ "make-this-a-long-host-name-component-that-is-over-63-characters-long.invalid",
+ ],
+ [
+ false,
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid",
+ ],
+ ];
+
+ for (let item of kHostsToTest) {
+ let result = null;
+ let [wantedResult, hostname] = item;
+ wantedResult = wantedResult ? hostname : null;
+
+ result = isLegalHostName(hostname);
+ Assert.equal(result, wantedResult);
+
+ result = isLegalHostNameOrIP(hostname, false);
+ Assert.equal(result, wantedResult);
+ }
+}
+
+var gTests = [test_IPaddresses, test_hostnames];
+
+function run_test() {
+ for (let test of gTests) {
+ test();
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_identity.js b/comm/mailnews/base/test/unit/test_identity.js
new file mode 100644
index 0000000000..c28615451a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_identity.js
@@ -0,0 +1,69 @@
+/* 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/. */
+
+/**
+ * Tests the UID attribute of identities.
+ */
+add_task(async function testUID() {
+ const UUID_REGEXP =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+ // Create an identity and check it the UID is set when accessed.
+
+ let identityA = MailServices.accounts.createIdentity();
+ Assert.stringMatches(
+ identityA.UID,
+ UUID_REGEXP,
+ "identity A's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityA.key}.uid`),
+ identityA.UID,
+ "identity A's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (identityA.UID = "00001111-2222-3333-4444-555566667777"),
+ /NS_ERROR_ABORT/,
+ "identity A's UID should be unchangeable after it is set"
+ );
+
+ // Create a second identity and check the two UIDs don't match.
+
+ let identityB = MailServices.accounts.createIdentity();
+ Assert.stringMatches(
+ identityB.UID,
+ UUID_REGEXP,
+ "identity B's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityB.key}.uid`),
+ identityB.UID,
+ "identity B's UID should be saved to the preferences"
+ );
+ Assert.notEqual(
+ identityB.UID,
+ identityA.UID,
+ "identity B's UID should not be the same as identity A's"
+ );
+
+ // Create a third identity and set the UID before it is accessed.
+
+ let identityC = MailServices.accounts.createIdentity();
+ identityC.UID = "11112222-3333-4444-5555-666677778888";
+ Assert.equal(
+ identityC.UID,
+ "11112222-3333-4444-5555-666677778888",
+ "identity C's UID set correctly"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityC.key}.uid`),
+ "11112222-3333-4444-5555-666677778888",
+ "identity C's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (identityC.UID = "22223333-4444-5555-6666-777788889999"),
+ /NS_ERROR_ABORT/,
+ "identity C's UID should be unchangeable after it is set"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_imapPump.js b/comm/mailnews/base/test/unit/test_imapPump.js
new file mode 100644
index 0000000000..ccc34bae60
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_imapPump.js
@@ -0,0 +1,81 @@
+/* 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/. */
+
+/**
+ * Simple demonstration of the imap pump test method.
+ */
+
+// async support
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+
+var { fsDebugAll } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+
+// Globals
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail10"; // message file used as the test message
+
+// Definition of tests
+
+// load and update a message in the imap fake server
+
+var gTestArray = [
+ // initial setup of IMAP environment
+ setupIMAPPump,
+
+ // optionally set server parameters, here enabling debug messages
+ function serverParms() {
+ IMAPPump.server.setDebugLevel(fsDebugAll);
+ },
+
+ // the main test
+ async function loadImapMessage() {
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+ },
+
+ // all done
+ teardownIMAPPump,
+];
+
+add_setup(() => {
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ gTestArray.forEach(x => add_task(x));
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file(gDEPTH + "mailnews/data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/base/test/unit/test_incomingServer.js b/comm/mailnews/base/test/unit/test_incomingServer.js
new file mode 100644
index 0000000000..9093ab4806
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_incomingServer.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+function subtestUID(type) {
+ const UUID_REGEXP =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+ // Create a server and check it the UID is set when accessed.
+
+ let serverA = MailServices.accounts.createIncomingServer(
+ "userA",
+ "hostA",
+ type
+ );
+ Assert.stringMatches(
+ serverA.UID,
+ UUID_REGEXP,
+ "server A's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverA.key}.uid`),
+ serverA.UID,
+ "server A's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverA.UID = "00001111-2222-3333-4444-555566667777"),
+ /NS_ERROR_ABORT/,
+ "server A's UID should be unchangeable after it is set"
+ );
+
+ // Create a second server and check the two UIDs don't match.
+
+ let serverB = MailServices.accounts.createIncomingServer(
+ "userB",
+ "hostB",
+ type
+ );
+ Assert.stringMatches(
+ serverB.UID,
+ UUID_REGEXP,
+ "server B's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverB.key}.uid`),
+ serverB.UID,
+ "server B's UID should be saved to the preferences"
+ );
+ Assert.notEqual(
+ serverB.UID,
+ serverA.UID,
+ "server B's UID should not be the same as server A's"
+ );
+
+ // Create a third server and set the UID before it is accessed.
+
+ let serverC = MailServices.accounts.createIncomingServer(
+ "userC",
+ "hostC",
+ type
+ );
+ serverC.UID = "11112222-3333-4444-5555-666677778888";
+ Assert.equal(
+ serverC.UID,
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID set correctly"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverC.key}.uid`),
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverC.UID = "22223333-4444-5555-6666-777788889999"),
+ /NS_ERROR_ABORT/,
+ "server C's UID should be unchangeable after it is set"
+ );
+}
+
+/**
+ * Tests the UID attribute of IMAP servers.
+ */
+add_task(function testUID_IMAP() {
+ subtestUID("imap");
+});
+
+/**
+ * Tests the UID attribute of NNTP servers.
+ */
+add_task(function testUID_NNTP() {
+ subtestUID("nntp");
+});
+
+/**
+ * Tests the UID attribute of POP3 servers.
+ */
+add_task(function testUID_POP3() {
+ subtestUID("pop3");
+});
diff --git a/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js b/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js
new file mode 100644
index 0000000000..55fb1da6c8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js
@@ -0,0 +1,183 @@
+/* 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/. */
+
+/*
+ * Testing of inherited folder properties
+ */
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ var rootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+
+ // add subfolders to the inbox
+ const subFolder11 = localAccountUtils.inboxFolder
+ .createLocalSubfolder("subfolder11")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ const subFolder12 = localAccountUtils.inboxFolder
+ .createLocalSubfolder("subfolder12")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ const subFolder21 = subFolder11.createLocalSubfolder("subfolder21");
+ const subFolder22 = subFolder12.createLocalSubfolder("subfolder22");
+
+ // add a global preference
+ const propertyName = "iexist";
+ const invalidName = "idontexist";
+ const globalPref = "mail.server.default." + propertyName;
+ const globalValue = "iAmGlobal";
+ const folderValue = "iAmFolder";
+ const folderValue2 = "iAmFolder2";
+ const rootValue = "iAmRoot";
+ Services.prefs.setCharPref(globalPref, globalValue);
+
+ // test that the global preference is honored
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(rootFolder.getInheritedStringProperty(invalidName), null);
+ Assert.equal(subFolder11.getInheritedStringProperty(invalidName), null);
+ Assert.equal(subFolder22.getInheritedStringProperty(invalidName), null);
+
+ // set a value on a subfolder and check
+ subFolder11.setStringProperty(propertyName, folderValue);
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder12.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+
+ // set a root folder value and check
+ localAccountUtils.incomingServer.setCharValue(propertyName, rootValue);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // force an empty string
+ subFolder12.setForcePropertyEmpty(propertyName, true);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), "");
+
+ // reset a folder to allow inheritance
+ subFolder12.setForcePropertyEmpty(propertyName, false);
+ subFolder12.setStringProperty(propertyName, "");
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // force an empty string on the server
+ localAccountUtils.incomingServer.setForcePropertyEmpty(propertyName, true);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), "");
+
+ // reset a server to allow inheritance from the global
+ localAccountUtils.incomingServer.setCharValue(propertyName, "");
+ localAccountUtils.incomingServer.setForcePropertyEmpty(propertyName, false);
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder12.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+
+ // check with all levels populated
+ subFolder21.setStringProperty(propertyName, folderValue2);
+ localAccountUtils.incomingServer.setCharValue(propertyName, rootValue);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue2
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // clear the global value and the root value
+ Services.prefs.clearUserPref(globalPref);
+ localAccountUtils.incomingServer.setCharValue(propertyName, "");
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), null);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), null);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue2
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), null);
+}
diff --git a/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js b/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js
new file mode 100644
index 0000000000..b6a103b069
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js
@@ -0,0 +1,176 @@
+/* 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/. */
+
+/*
+ * Test that junk actions work even when the bayes filtering of incoming
+ * messages is disabled, as fixed in bug 487610. Test developed by Kent
+ * James using test_nsMsgDBView.js as a base.
+ */
+
+const { TreeSelection } = ChromeUtils.importESModule(
+ "chrome://messenger/content/tree-selection.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+// fake objects needed to get nsMsgDBView to operate on selected messages.
+// Warning: these are partial implementations. If someone adds additional
+// calls to these objects in nsMsgDBView and friends, it will also
+// be necessary to add fake versions of those calls here.
+
+var gFakeSelection = new TreeSelection(null);
+
+// Items used to add messages to the folder
+
+var gMessageGenerator = new MessageGenerator();
+
+var messageInjection = new MessageInjection(
+ { mode: "local" },
+ gMessageGenerator
+);
+
+var gLocalInboxFolder = messageInjection.getInboxFolder();
+var gListener;
+var gCommandUpdater;
+
+var gDBView;
+var gTreeView;
+
+var CommandUpdaterWithPromise = function () {
+ this.deferred = PromiseUtils.defer();
+};
+CommandUpdaterWithPromise.prototype = {
+ async promiseSelectionSummarized() {
+ await this.deferred.promise;
+ this.deferred = PromiseUtils.defer();
+ return this.deferred.promise;
+ },
+
+ updateCommandStatus() {
+ // the back end is smart and is only telling us to update command status
+ // when the # of items in the selection has actually changed.
+ },
+
+ displayMessageChanged(aFolder, aSubject, aKeywords) {},
+
+ updateNextMessageAfterDelete() {},
+ summarizeSelection() {
+ this.deferred.resolve();
+ },
+};
+
+// Our listener, which captures events and does the real tests.
+function gMFListener() {
+ this._promiseMsgsMoveCopyCompleted = new Promise(resolve => {
+ this._resolveMsgsMoveCopyCompleted = resolve;
+ });
+ this._promiseFolderAdded = new Promise(resolve => {
+ this._resolveFolderAdded = resolve;
+ });
+}
+gMFListener.prototype = {
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ Assert.ok(aDestFolder.getFlag(Ci.nsMsgFolderFlags.Junk));
+ // I tried to test this by counting messages in the folder, didn't work.
+ // Maybe all updates are not completed yet. Anyway I do it by just
+ // making sure there is something in the destination array.
+ Assert.ok(aDestMsgs.length > 0);
+ this._resolveMsgsMoveCopyCompleted();
+ },
+
+ folderAdded(aFolder) {
+ // this should be a junk folder
+ Assert.ok(aFolder.getFlag(Ci.nsMsgFolderFlags.Junk));
+ this._resolveFolderAdded();
+ },
+ get promiseMsgsMoveCopyCompleted() {
+ return this._promiseMsgsMoveCopyCompleted;
+ },
+ get promiseFolderAdded() {
+ return this._promiseFolderAdded;
+ },
+};
+
+add_setup(async function () {
+ // Set option so that when messages are marked as junk, they move to the junk folder
+ Services.prefs.setBoolPref("mail.spam.manualMark", true);
+
+ // 0 == "move to junk folder", 1 == "delete"
+ Services.prefs.setIntPref("mail.spam.manualMarkMode", 0);
+
+ // Disable bayes filtering on the local account. That's the whole point of this test,
+ // to make sure that the junk move happens anyway.
+ gLocalInboxFolder.server.spamSettings.level = 0;
+
+ // Add folder listeners that will capture async events.
+ let flags = nsIMFNService.msgsMoveCopyCompleted | nsIMFNService.folderAdded;
+ gListener = new gMFListener();
+ MailServices.mfn.addListener(gListener, flags);
+
+ // Build up a message.
+ await messageInjection.makeNewSetsInFolders([gLocalInboxFolder], [{}]);
+ let view_type = "threaded";
+ let view_flag = Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + view_type;
+
+ // Always start out fully expanded.
+ view_flag |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gCommandUpdater = new CommandUpdaterWithPromise();
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ gLocalInboxFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ view_flag,
+ outCount
+ );
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gTreeView.selection = gFakeSelection;
+ gFakeSelection.view = gTreeView;
+});
+
+add_task(async function test_first_junking_create_folder() {
+ // In the proposed fix for bug 487610, the first call to junk messages
+ // only creates the junk folder, it does not actually successfully move
+ // messages. So we junk messages twice so we can really see a move. But
+ // if that gets fixed and the messages actually move on the first call,
+ // I want this test to succeed as well. So I don't actually count how
+ // many messages get moved, just that some do on the second move.
+
+ // Select and junk all messages.
+ gDBView.doCommand(Ci.nsMsgViewCommandType.selectAll);
+ gDBView.doCommand(Ci.nsMsgViewCommandType.junk);
+ await gCommandUpdater.promiseSelectionSummarized;
+ await gListener.promiseFolderAdded;
+});
+
+add_task(async function test_add_further_message() {
+ // Add another message in case the first one moved.
+ await messageInjection.makeNewSetsInFolders([gLocalInboxFolder], [{}]);
+});
+
+add_task(async function test_second_junking_move_msgs() {
+ // Select and junk all messages.
+ gDBView.doCommand(Ci.nsMsgViewCommandType.selectAll);
+ gDBView.doCommand(Ci.nsMsgViewCommandType.junk);
+ await gCommandUpdater.promiseSelectionSummarized;
+ await gListener.promiseMsgsMoveCopyCompleted;
+});
diff --git a/comm/mailnews/base/test/unit/test_loadVirtualFolders.js b/comm/mailnews/base/test/unit/test_loadVirtualFolders.js
new file mode 100644
index 0000000000..13afd3f03b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_loadVirtualFolders.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+// Test loading of virtualFolders.dat, including verification of the search
+// scopes, i.e., folder uri's.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// As currently written, this test will only work with Berkeley store.
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// main test
+
+function run_test() {
+ let vfdat = do_get_file("../../../data/test_virtualFolders.dat");
+
+ vfdat.copyTo(do_get_profile(), "virtualFolders.dat");
+ localAccountUtils.loadLocalMailAccount();
+ let localMailDir = do_get_profile().clone();
+ localMailDir.append("Mail");
+ localMailDir.append("Local Folders");
+ localMailDir.append("unread-local");
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ localMailDir.leafName = "invalidserver-local";
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ localMailDir.leafName = "$label1";
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ MailServices.accounts.loadVirtualFolders();
+ let unreadLocal =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed(
+ "unread-local"
+ );
+ let searchScope =
+ unreadLocal.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri");
+ Assert.equal(
+ searchScope,
+ "mailbox://nobody@Local%20Folders/Inbox|mailbox://nobody@Local%20Folders/Trash"
+ );
+ let invalidServer =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed(
+ "invalidserver-local"
+ );
+ searchScope =
+ invalidServer.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri");
+ Assert.equal(searchScope, "mailbox://nobody@Local%20Folders/Inbox");
+
+ let tagsFolder =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed("$label1");
+ Assert.equal(
+ tagsFolder.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri"),
+ "*"
+ );
+ Assert.equal(
+ tagsFolder.msgDatabase.dBFolderInfo.getCharProperty("searchStr"),
+ "AND (tag,contains,$label1)"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_mailServices.js b/comm/mailnews/base/test/unit/test_mailServices.js
new file mode 100644
index 0000000000..1c8299b4f5
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mailServices.js
@@ -0,0 +1,67 @@
+/* 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/. */
+
+/*
+ * Tests for the MailServices module.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(function test_services() {
+ function check_service(service, serviceInterface) {
+ Assert.ok(
+ service in MailServices,
+ `${service} should be a member of MailServices`
+ );
+ Assert.ok(
+ MailServices[service] instanceof serviceInterface,
+ `MailServices.${service} should implement Ci.${serviceInterface.name}`
+ );
+ }
+
+ check_service("mailSession", Ci.nsIMsgMailSession);
+ check_service("accounts", Ci.nsIMsgAccountManager);
+ check_service("pop3", Ci.nsIPop3Service);
+ check_service("imap", Ci.nsIImapService);
+ check_service("nntp", Ci.nsINntpService);
+ check_service("smtp", Ci.nsISmtpService);
+ check_service("compose", Ci.nsIMsgComposeService);
+ check_service("ab", Ci.nsIAbManager);
+ check_service("copy", Ci.nsIMsgCopyService);
+ check_service("mfn", Ci.nsIMsgFolderNotificationService);
+ check_service("headerParser", Ci.nsIMsgHeaderParser);
+ check_service("mimeConverter", Ci.nsIMimeConverter);
+ check_service("tags", Ci.nsIMsgTagService);
+ check_service("filters", Ci.nsIMsgFilterService);
+ check_service("junk", Ci.nsIJunkMailPlugin);
+});
+
+add_task(function test_message_services() {
+ function check_message_service(uri) {
+ let service = MailServices.messageServiceFromURI(uri);
+ Assert.ok(
+ service instanceof Ci.nsIMsgMessageService,
+ `message service for ${uri.substring(
+ 0,
+ uri.indexOf(":")
+ )} URIs should exist`
+ );
+ }
+
+ check_message_service("file://it.does.not.matter/");
+ check_message_service("imap://it.does.not.matter/");
+ check_message_service("imap-message://it.does.not.matter/");
+ check_message_service("mailbox://it.does.not.matter/");
+ check_message_service("mailbox-message://it.does.not.matter/");
+ check_message_service("news://it.does.not.matter/");
+ check_message_service("news-message://it.does.not.matter/");
+
+ Assert.throws(
+ () => MailServices.messageServiceFromURI("fake://not.going.to.work/"),
+ () => true, // Accept any exception.
+ "message service for other URIs should not exist"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_mailstoreConverter.js b/comm/mailnews/base/test/unit/test_mailstoreConverter.js
new file mode 100644
index 0000000000..f440b9c8cd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mailstoreConverter.js
@@ -0,0 +1,376 @@
+/* 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 { convertMailStoreTo } = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Test data for round-trip test.
+let testEmails = [
+ // Base64 encoded bodies.
+ "../../../data/01-plaintext.eml",
+ "../../../data/02-plaintext+attachment.eml",
+ "../../../data/03-HTML.eml",
+ "../../../data/04-HTML+attachment.eml",
+ "../../../data/05-HTML+embedded-image.eml",
+ "../../../data/06-plaintext+HMTL.eml",
+ "../../../data/07-plaintext+(HTML+embedded-image).eml",
+ "../../../data/08-plaintext+HTML+attachment.eml",
+ "../../../data/09-(HTML+embedded-image)+attachment.eml",
+ "../../../data/10-plaintext+(HTML+embedded-image)+attachment.eml",
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets.
+ "../../../data/11-plaintext.eml",
+ "../../../data/12-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/13-HTML.eml",
+ "../../../data/14-HTML+attachment.eml",
+ "../../../data/15-HTML+embedded-image.eml",
+ "../../../data/16-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/17-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/18-plaintext+HTML+attachment.eml",
+ "../../../data/19-(HTML+embedded-image)+attachment.eml",
+ "../../../data/20-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets, all encoded with quoted printable.
+ "../../../data/21-plaintext.eml",
+ "../../../data/22-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/23-HTML.eml",
+ "../../../data/24-HTML+attachment.eml",
+ "../../../data/25-HTML+embedded-image.eml",
+ "../../../data/26-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/27-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/28-plaintext+HTML+attachment.eml",
+ "../../../data/29-(HTML+embedded-image)+attachment.eml",
+ "../../../data/30-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ add_task(async function () {
+ await doMboxTest("test1", "../../../data/mbox_modern", 2);
+ await doMboxTest("test2", "../../../data/mbox_mboxrd", 2);
+ await doMboxTest("test3", "../../../data/mbox_unquoted", 2);
+ await roundTripTest();
+ // Ideas for more tests:
+ // - check a really big mbox
+ // - check with really huge message (larger than one chunk)
+ // - check mbox with "From " line on chunk boundary
+ // - add tests for maildir->mbox conversion
+ // - check that conversions preserve message body (ie that the
+ // "From " line escaping scheme is reversible)
+ });
+
+ run_next_test();
+}
+
+/**
+ * Helper to create a server, account and inbox, and install an
+ * mbox file.
+ *
+ * @param {string} srvName - A unique server name to use for the test.
+ * @param {string} mboxFilename - mbox file to install and convert.
+ * @returns {nsIMsgIncomingServer} a server.
+ */
+function setupServer(srvName, mboxFilename) {
+ // {nsIMsgIncomingServer} pop server for the test.
+ let server = MailServices.accounts.createIncomingServer(
+ srvName,
+ "localhost",
+ "pop3"
+ );
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+ server.QueryInterface(Ci.nsIPop3IncomingServer);
+ server.valid = true;
+
+ let inbox = account.incomingServer.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+
+ // install the mbox file
+ let mboxFile = do_get_file(mboxFilename);
+ mboxFile.copyTo(inbox.filePath.parent, inbox.filePath.leafName);
+
+ // TODO: is there some way to make folder rescan the mbox?
+ // We don't need it for this, but would be nice to do things properly.
+ return server;
+}
+
+/**
+ * Perform an mbox->maildir conversion test.
+ *
+ * @param {string} srvName - A unique server name to use for the test.
+ * @param {string} mboxFilename - mbox file to install and convert.
+ * @param {number} expectCnt - Number of messages expected.
+ * @returns {nsIMsgIncomingServer} a server.
+ */
+async function doMboxTest(srvName, mboxFilename, expectCnt) {
+ // set up an account+server+inbox and copy in the test mbox file
+ let server = setupServer(srvName, mboxFilename);
+
+ let mailstoreContractId = Services.prefs.getCharPref(
+ "mail.server." + server.key + ".storeContractID"
+ );
+
+ await convertMailStoreTo(mailstoreContractId, server, new EventTarget());
+
+ // Converted. Now find resulting Inbox/cur directory so
+ // we can count the messages there.
+
+ let inbox = server.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ // NOTE: the conversion updates the path of the root folder,
+ // but _not_ the path of the inbox...
+ // Ideally, we'd just use inbox.filePath here, but
+ // instead we'll have compose the path manually.
+
+ let curDir = server.rootFolder.filePath;
+ curDir.append(inbox.filePath.leafName);
+ curDir.append("cur");
+
+ // Sanity check.
+ Assert.ok(curDir.isDirectory(), "'cur' directory created");
+
+ // Check number of messages in Inbox/cur is what we expect.
+ let cnt = [...curDir.directoryEntries].length;
+
+ Assert.equal(
+ cnt,
+ expectCnt,
+ "expected number of messages (" + mboxFilename + ")"
+ );
+}
+
+/**
+ * Create a temporary directory. The caller is responsible for deleting it.
+ *
+ * @param {string} prefix - Generated dir name will be of the form:
+ * "<prefix><random_sequence>".
+ * @returns {string} full path of new directory.
+ */
+async function tempDir(prefix) {
+ if (!prefix) {
+ prefix = "";
+ }
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+ while (true) {
+ let name = prefix + Math.floor(Math.random() * 0xffffffff).toString(16);
+ let fullPath = PathUtils.join(tmpDir, name);
+ try {
+ await IOUtils.makeDirectory(fullPath, { ignoreExisting: false });
+ return fullPath;
+ } catch (e) {
+ // If directory already exists, try another name. Else bail out.
+ if (
+ !(DOMException.isInstance(e) && e.name === "NoModificationAllowedError")
+ ) {
+ throw e;
+ }
+ }
+ }
+}
+
+/**
+ * Test that messages survive unscathed in a roundtrip conversion,
+ * maildir -> mbox -> maildir.
+ * The final mailbox should have an identical set of files to the initial one,
+ * albeit with different filenames.
+ * Purely filesystem based.
+ *
+ * Would be nice to do a mbox->maildir->mbox roundtrip too, but that'd involve
+ * parsing the mbox files to compare them (can't just compare mbox files because
+ * message order and "From " lines can change).
+ */
+async function roundTripTest() {
+ // Set up initial maildir structure
+ let initialRoot = await tempDir("initial");
+
+ let inbox = PathUtils.join(initialRoot, "INBOX");
+ await IOUtils.makeDirectory(inbox);
+ // Create a couple of subdirs under INBOX
+ let subdir = PathUtils.join(initialRoot, "INBOX.sbd");
+ await IOUtils.makeDirectory(subdir);
+ let foodir = PathUtils.join(subdir, "foo");
+ await IOUtils.makeDirectory(foodir);
+ let bardir = PathUtils.join(subdir, "bar");
+ await IOUtils.makeDirectory(bardir);
+
+ // Populate all the folders with some test emails.
+ const absolutePaths = testEmails.map(path => do_get_file(path).path);
+ await populateMaildir(inbox, absolutePaths);
+ await populateMaildir(foodir, absolutePaths);
+ await populateMaildir(bardir, absolutePaths);
+
+ // Add a pick of "special" files, which should survive the trip verbatim.
+ for (let special of ["filterlog.html", "feeds.json", "rules.dat"]) {
+ let f = PathUtils.join(initialRoot, special);
+ await IOUtils.writeUTF8(f, f); // Use the filename for content.
+ }
+
+ // Create root dirs for intermediate and final result.
+ let mboxRoot = await tempDir("mbox");
+ let finalRoot = await tempDir("final");
+
+ // Convert: maildir -> mbox -> maildir
+ await doConvert("maildir", initialRoot, "mbox", mboxRoot);
+ await doConvert("mbox", mboxRoot, "maildir", finalRoot);
+
+ // compare results - use checksums, because filenames will differ.
+ await recursiveMaildirCompare(initialRoot, finalRoot);
+}
+
+/**
+ * Helper to adapt the callbacks from converterWorker into a promise.
+ *
+ * @param {string} srcType - type of source ("maildir", "mbox")
+ * @param {string} srcRoot - root directory containing the src folders.
+ * @param {string} destType - type of destination ("maildir", "mbox")
+ * @param {string} destRoot - root directory to place converted store.
+ * @returns {Promise} resolved when when conversion is complete.
+ */
+function doConvert(srcType, srcRoot, destType, destRoot) {
+ return new Promise(function (resolve, reject) {
+ let worker = new ChromeWorker("resource:///modules/converterWorker.js");
+ worker.addEventListener("message", function (ev) {
+ if (ev.data.msg == "success") {
+ resolve();
+ }
+ });
+ worker.addEventListener("error", function (ev) {
+ reject(ev.message);
+ });
+ // Go.
+ worker.postMessage({
+ srcType,
+ destType,
+ srcRoot,
+ destRoot,
+ });
+ });
+}
+
+/**
+ * Copy a list of email files (.eml) files into a maildir, creating "cur"
+ * and "tmp" subdirs if required.
+ *
+ * @param {string} maildir - Path to the maildir directory.
+ * @param {Array<string>} emailFiles - paths of source .eml files to copy.
+ */
+async function populateMaildir(maildir, emailFiles) {
+ let cur = PathUtils.join(maildir, "cur");
+ await IOUtils.makeDirectory(cur);
+ await IOUtils.makeDirectory(PathUtils.join(maildir, "tmp"));
+
+ // Normally maildir files would have a name derived from their msg-id field,
+ // but here we'll just use a timestamp-based one to save parsing them.
+ let ident = Date.now();
+ for (let src of emailFiles) {
+ let dest = PathUtils.join(cur, ident.toString() + ".eml");
+ ident += 1;
+ await IOUtils.copy(src, dest);
+ }
+}
+
+/*
+ * List files in a directory (excludes subdirectories).
+ *
+ * @param {String} dirPath - Full path of directory.
+ * @returns {Array<String} full paths of the files.
+ */
+async function listFiles(dirPath) {
+ let files = [];
+ // Note: IOUtils has no dir iterator at time of writing.
+ for (const path of await IOUtils.getChildren(dirPath)) {
+ let fileInfo = await IOUtils.stat(path);
+ if (fileInfo.type !== "directory") {
+ files.push(path);
+ }
+ }
+ return files;
+}
+
+/*
+ * Calculate md5 checksum for a file.
+ *
+ * @param {String} fileName - Full path to file.
+ * @returns {String} checksum of the file contents.
+ */
+async function md5Sum(fileName) {
+ let md5 = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ md5.init(Ci.nsICryptoHash.MD5);
+ let raw = await IOUtils.read(fileName);
+ md5.update(raw, raw.byteLength);
+ return md5.finish(true);
+}
+
+/**
+ * Compare all maildir directories in two directory trees.
+ * The comparison is per-maildir, by looking at the checksums of their emails.
+ * Asserts a test fail if any differences are found.
+ *
+ * @param {string} rootA - path to root of maildir store A.
+ * @param {string} rootB - path to root of maildir store B.
+ */
+async function recursiveMaildirCompare(rootA, rootB) {
+ let subdirs = [];
+ let maildirs = [];
+ let otherFiles = [];
+ for (let path of await IOUtils.getChildren(rootA)) {
+ let stat = await IOUtils.stat(path);
+ let name = PathUtils.filename(path);
+ if (stat.type === "directory") {
+ if (name.endsWith(".sbd")) {
+ subdirs.push(name);
+ } else {
+ // Assume all other dirs are maildirs.
+ maildirs.push(name);
+ }
+ } else {
+ otherFiles.push(name);
+ }
+ }
+
+ // Compare the maildirs we found here.
+ let md5DirContents = async function (dirPath) {
+ let checksums = [];
+ for (let f of await listFiles(dirPath)) {
+ checksums.push(await md5Sum(f));
+ }
+ return checksums;
+ };
+
+ for (let name of maildirs) {
+ let checksumsA = await md5DirContents(PathUtils.join(rootA, name, "cur"));
+ let checksumsB = await md5DirContents(PathUtils.join(rootB, name, "cur"));
+
+ checksumsA.sort();
+ checksumsB.sort();
+ let match = checksumsA.length == checksumsB.length;
+ for (let i = 0; match && i < checksumsA.length; i++) {
+ match = checksumsA[i] == checksumsB[i];
+ }
+ Assert.ok(match, "roundtrip preserves messages in maildir " + name);
+ }
+
+ // Make sure any "special" files survived the trip intact.
+ for (let name of otherFiles) {
+ let checksumA = await md5Sum(PathUtils.join(rootA, name));
+ let pathB = PathUtils.join(rootB, name);
+ let checksumB = (await IOUtils.exists(pathB)) ? await md5Sum(pathB) : null;
+ Assert.equal(checksumA, checksumB, "roundtrip preserves " + name);
+ }
+
+ // Recurse down into .sbd dirs.
+ for (let name of subdirs) {
+ await recursiveMaildirCompare(
+ PathUtils.join(rootA, name),
+ PathUtils.join(rootB, name)
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_mimemaltdetach.js b/comm/mailnews/base/test/unit/test_mimemaltdetach.js
new file mode 100644
index 0000000000..1f90566726
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mimemaltdetach.js
@@ -0,0 +1,160 @@
+/* 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/. */
+
+/*
+ * Tests nsIMessenger's detachAttachmentsWOPrompts of Mime multi-part
+ * alternative messages.
+ */
+
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+function SaveAttachmentCallback() {
+ this.attachments = null;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+}
+
+SaveAttachmentCallback.prototype = {
+ callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) {
+ this.attachments = aMimeMessage.allAttachments;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var gCallbackObject = new SaveAttachmentCallback();
+
+add_setup(async function () {
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+});
+
+add_task(async function startCopy() {
+ // Get a message into the local filestore.
+ let mailFile = do_get_file("../../../data/multipartmalt-detach");
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ mailFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ listener,
+ null
+ );
+ await listener.promise;
+});
+
+// process the message through mime
+add_task(async function startMime() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ gCallbackObject,
+ gCallbackObject.callback,
+ true // allowDownload
+ );
+
+ await gCallbackObject.promise;
+});
+
+// detach any found attachments
+add_task(async function startDetach() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+ let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey);
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let attachment = gCallbackObject.attachments[0];
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+
+ messenger.detachAttachmentsWOPrompts(
+ do_get_profile(),
+ [attachment.contentType],
+ [attachment.url],
+ [attachment.name],
+ [msgURI],
+ listener
+ );
+ await listener.promise;
+});
+
+// test that the detachment was successful
+add_task(async function testDetach() {
+ // The message contained a file "head_update.txt" which should
+ // now exist in the profile directory.
+ let checkFile = do_get_profile().clone();
+ checkFile.append("head_update.txt");
+ Assert.ok(checkFile.exists());
+ Assert.ok(checkFile.fileSize > 0);
+
+ // The message should now have a detached attachment. Read the message,
+ // and search for "AttachmentDetached" which is added on detachment.
+
+ // Get the message header
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ let messageContent = await getContentFromMessage(msgHdr);
+ Assert.ok(messageContent.includes("AttachmentDetached"));
+ // Make sure the body survived the detach.
+ Assert.ok(messageContent.includes("body hello"));
+});
+
+/**
+ * Get the full message content.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - Message object whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_newMailNotification.js b/comm/mailnews/base/test/unit/test_newMailNotification.js
new file mode 100644
index 0000000000..d6e111d86a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_newMailNotification.js
@@ -0,0 +1,203 @@
+/* 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/. */
+
+/* Tests for platform-independent code to count new and unread messages and pass the
+ * information to platform-specific notification modules */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Register listener for a particular event, make sure it shows up in the right lists
+ * of listeners (and not the wrong ones) and doesn't show up after being removed
+ */
+add_test(function testListeners() {
+ let notif = MailServices.newMailNotification.wrappedJSObject;
+ let listener = { onCountChanged: () => {} };
+
+ notif.addListener(listener, Ci.mozINewMailNotificationService.count);
+ let list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 1);
+ Assert.equal(list[0], listener);
+
+ let newlist = notif.listenersForFlag(
+ Ci.mozINewMailNotificationService.messages
+ );
+ Assert.equal(newlist.length, 0);
+
+ notif.removeListener(listener);
+ list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 0);
+
+ run_next_test();
+});
+
+/*
+ * Register a listener for two types and another for one type, make sure they show up,
+ * remove one and make sure the other stays put
+ */
+add_test(function testMultiListeners() {
+ let notif = MailServices.newMailNotification.wrappedJSObject;
+ let l1 = { onCountChanged: () => {} };
+ let l2 = { b: 2 };
+
+ notif.addListener(
+ l1,
+ Ci.mozINewMailNotificationService.count |
+ Ci.mozINewMailNotificationService.messages
+ );
+ // do_check_eq(notif._listeners.length, 1);
+ notif.addListener(l2, Ci.mozINewMailNotificationService.messages);
+ // do_check_eq(notif._listeners.length, 2);
+ let list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 1);
+ Assert.equal(list[0], l1);
+
+ let newlist = notif.listenersForFlag(
+ Ci.mozINewMailNotificationService.messages
+ );
+ Assert.equal(newlist.length, 2);
+
+ notif.removeListener(l1);
+ list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 0);
+ newlist = notif.listenersForFlag(Ci.mozINewMailNotificationService.messages);
+ Assert.equal(newlist.length, 1);
+ Assert.equal(newlist[0], l2);
+ notif.removeListener(l2);
+
+ run_next_test();
+});
+
+/* Make sure we get a notification call when the unread count changes on an Inbox */
+add_test(function testNotifyInbox() {
+ let notified = false;
+ let count = 0;
+ let mockListener = {
+ onCountChanged: function TNU_onCountChanged(updatedCount) {
+ notified = true;
+ count = updatedCount;
+ },
+ };
+ let folder = {
+ URI: "Test Inbox",
+ flags: Ci.nsMsgFolderFlags.Mail | Ci.nsMsgFolderFlags.Inbox,
+ };
+
+ const notificationService = MailServices.newMailNotification.wrappedJSObject;
+
+ // Set up the notification service to start with a non-zero unread count to
+ // verify this value is correctly passed to new listeners. Do this before any
+ // listeners are added.
+ const startCount = 3;
+ notificationService.unreadCount = startCount;
+
+ // Add a listener for count updates.
+ notificationService.addListener(
+ mockListener,
+ Ci.mozINewMailNotificationService.count
+ );
+
+ // Verify that a new listener is notified of the current count.
+ Assert.ok(notified, "New listeners should be notified of count when added.");
+ Assert.equal(
+ count,
+ startCount,
+ "New listener notification should contain the current unread count."
+ );
+
+ // Verify that listeners are notified of subsequent changes.
+ notified = false;
+ const updatedInboxCount = 5;
+ notificationService.onFolderIntPropertyChanged(
+ folder,
+ "TotalUnreadMessages",
+ startCount,
+ updatedInboxCount
+ );
+ Assert.ok(
+ notified,
+ "Listeners should be notified of changes in inbox unread count."
+ );
+ Assert.equal(
+ count,
+ updatedInboxCount,
+ "Notification should contain updated inbox unread count."
+ );
+
+ // Sanity check.
+ Assert.ok(
+ Services.prefs.getBoolPref("mail.notification.count.inbox_only", false),
+ "`inbox_only` pref should be true for test."
+ );
+
+ // Verify that listeners are not notified of changes outside of the inbox.
+ let nonInbox = {
+ URI: "Test Non-Inbox",
+ flags: Ci.nsMsgFolderFlags.Mail,
+ };
+ notified = false;
+ notificationService.onFolderIntPropertyChanged(
+ nonInbox,
+ "TotalUnreadMessages",
+ 0,
+ 2
+ );
+ Assert.ok(
+ !notified,
+ "Listeners should not be notified of changes in unread count outside of inbox by default."
+ );
+ Assert.equal(
+ count,
+ updatedInboxCount,
+ "Total unread message count should not have changed."
+ );
+
+ // Verify that, when `inbox_only` is false, unread messages outside of the
+ // inbox are counted.
+ Services.prefs.setBoolPref("mail.notification.count.inbox_only", false);
+ notified = false;
+ const updatedNonInboxCount = 2;
+ const updatedTotalCount = updatedInboxCount + updatedNonInboxCount;
+ notificationService.onFolderIntPropertyChanged(
+ nonInbox,
+ "TotalUnreadMessages",
+ 0,
+ updatedNonInboxCount
+ );
+ Assert.ok(
+ notified,
+ "Listeners should be notified of changes in unread count outside of inbox when pref is set."
+ );
+ Assert.equal(
+ count,
+ updatedTotalCount,
+ "Notification should contain total unread count for all counted folders."
+ );
+
+ // Verify that listeners are never informed of updates in special folders.
+ let special = {
+ URI: "Test Special",
+ flags: Ci.nsMsgFolderFlags.Mail | Ci.nsMsgFolderFlags.Junk,
+ };
+ notified = false;
+ notificationService.onFolderIntPropertyChanged(
+ special,
+ "TotalUnreadMessages",
+ 0,
+ 2
+ );
+ Assert.ok(
+ !notified,
+ "Listeners should not be notified of changes in special folder unread count."
+ );
+ Assert.equal(
+ count,
+ updatedTotalCount,
+ "Total unread message count should not have changed."
+ );
+
+ run_next_test();
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIFolderListener.js b/comm/mailnews/base/test/unit/test_nsIFolderListener.js
new file mode 100644
index 0000000000..aa1b19ecfb
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIFolderListener.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/*
+ * Test that adding nsIFolderListener in js does not cause any crash.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var folderListener = {
+ onFolderAdded() {},
+ onMessageAdded() {},
+ onFolderRemoved() {},
+ onMessageRemoved() {},
+ onFolderPropertyChanged() {},
+ onFolderIntPropertyChanged() {},
+ onFolderBoolPropertyChanged() {},
+ onFolderUnicharPropertyChanged() {},
+ onFolderPropertyFlagChanged() {},
+ onFolderEvent() {},
+};
+
+var targetFolder;
+var messageInjection;
+
+add_setup(async function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+
+ targetFolder = await messageInjection.makeEmptyFolder();
+ targetFolder.AddFolderListener(folderListener);
+ registerCleanupFunction(function () {
+ targetFolder.RemoveFolderListener(folderListener);
+ });
+});
+
+add_task(async function create_new_message() {
+ await messageInjection.makeNewSetsInFolders([targetFolder], [{ count: 1 }]);
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js b/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js
new file mode 100644
index 0000000000..de49d5b7eb
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js
@@ -0,0 +1,68 @@
+/* -*- mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgContentPolicy to check we could add/remove customized protocol to
+ * nsMsgContentPolicy.
+ */
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+function makeURI(aURL) {
+ return Services.io.newURI(aURL);
+}
+
+function run_test() {
+ var content_policy = Cc["@mozilla.org/messenger/content-policy;1"].getService(
+ Ci.nsIContentPolicy
+ );
+
+ Assert.ok(content_policy);
+
+ var msg_content_policy = content_policy.QueryInterface(
+ Ci.nsIMsgContentPolicy
+ );
+
+ Assert.ok(msg_content_policy);
+
+ var req_uri = makeURI("custom-scheme://custom_url/1.emal");
+ Assert.ok(req_uri);
+
+ var content_uri = makeURI("custom-scheme://custom_content_url/1.jsp");
+ Assert.ok(content_uri);
+
+ let tmpChannel = NetUtil.newChannel({
+ uri: content_uri,
+ // Needs one of 'loadingNode', 'loadingPrincipal' or 'loadUsingSystemPrincipal' which we don't have.
+ // Even with `loadUsingSystemPrincipal: true` this fails with "unknown protocol". See bug 1446587.
+ securityFlags: Ci.nsILoadInfo.SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ });
+ let tmpLoadInfo = tmpChannel.loadInfo;
+
+ var decision = content_policy.shouldLoad(
+ content_uri,
+ tmpLoadInfo,
+ "img/jpeg"
+ );
+ Assert.notEqual(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should not load"
+ );
+
+ msg_content_policy.addExposedProtocol("custom-scheme");
+
+ decision = content_policy.shouldLoad(content_uri, tmpLoadInfo, "img/jpeg");
+ Assert.equal(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should load"
+ );
+
+ msg_content_policy.removeExposedProtocol("custom-scheme");
+
+ decision = content_policy.shouldLoad(content_uri, tmpLoadInfo, "img/jpeg");
+ Assert.notEqual(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should not load"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolder.js b/comm/mailnews/base/test/unit/test_nsIMsgFolder.js
new file mode 100644
index 0000000000..9cfb57a07c
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolder.js
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgFolder functions.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create a local mail account (we need this first)
+ MailServices.accounts.createLocalMailAccount();
+
+ // Get the account
+ let account = MailServices.accounts.accounts[0];
+
+ // Get the root folder
+ var root = account.incomingServer.rootFolder;
+
+ // Add a sub folder to ensure that we have some folders created
+ root.createSubfolder("folder1", null);
+
+ // Test - getChildNamed
+
+ var caught = false;
+ try {
+ root.getChildNamed("folder");
+ } catch (e) {
+ caught = true;
+ }
+ Assert.equal(caught, true);
+
+ caught = false;
+ try {
+ root.getChildNamed("Trash1");
+ } catch (e) {
+ caught = true;
+ }
+ Assert.equal(caught, true);
+
+ var folder1 = root.getChildNamed("folder1");
+
+ Assert.notEqual(folder1, folder2);
+ Assert.equal(folder1.prettyName, "folder1");
+
+ var folder2 = root.getChildNamed("FOLDER1");
+
+ Assert.equal(folder1, folder2);
+
+ // Check special folders aren't deletable, and that normal folders are.
+ if (!root.containsChildNamed("Inbox")) {
+ root.createSubfolder("Inbox", null);
+ }
+ var inbox = root.getChildNamed("Inbox");
+ inbox.setFlag(Ci.nsMsgFolderFlags.Inbox);
+ Assert.ok(!inbox.deletable);
+
+ if (!root.containsChildNamed("Drafts")) {
+ root.createSubfolder("Drafts", null);
+ }
+ var drafts = root.getChildNamed("Drafts");
+ drafts.setFlag(Ci.nsMsgFolderFlags.Drafts);
+ Assert.ok(!drafts.deletable);
+
+ if (!root.containsChildNamed("Templates")) {
+ root.createSubfolder("Templates", null);
+ }
+ var templates = root.getChildNamed("Templates");
+ templates.setFlag(Ci.nsMsgFolderFlags.Templates);
+ Assert.ok(!templates.deletable);
+
+ if (!root.containsChildNamed("Sent")) {
+ root.createSubfolder("Sent", null);
+ }
+ var sent = root.getChildNamed("Sent");
+ sent.setFlag(Ci.nsMsgFolderFlags.SentMail);
+ Assert.ok(!sent.deletable);
+
+ if (!root.containsChildNamed("Archives")) {
+ root.createSubfolder("Archives", null);
+ }
+ var archives = root.getChildNamed("Archives");
+ archives.setFlag(Ci.nsMsgFolderFlags.Archive);
+ Assert.ok(!archives.deletable);
+
+ if (!root.containsChildNamed("Trash")) {
+ root.createSubfolder("Trash", null);
+ }
+ var trash = root.getChildNamed("Trash");
+ trash.setFlag(Ci.nsMsgFolderFlags.Trash);
+ Assert.ok(!trash.deletable);
+
+ if (!root.containsChildNamed("Outbox")) {
+ root.createSubfolder("Outbox", null);
+ }
+ var outbox = root.getChildNamed("Outbox");
+ outbox.setFlag(Ci.nsMsgFolderFlags.Queue);
+ Assert.ok(!outbox.deletable);
+
+ // test a normal folder is deletable
+ Assert.ok(folder1.deletable);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js
new file mode 100644
index 0000000000..9fd9688b53
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js
@@ -0,0 +1,228 @@
+/* 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/. */
+
+/**
+ * Sanity checks for nsIMsgFolderCache/nsIMsgFolderCacheElement.
+ */
+add_task(function test_basics() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ // Create an empty cache object and start poking it.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // Neither of these files exist, and that's fine.
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+ cache.init(jsonFile, legacyFile);
+
+ // getCacheElement has to be told to create non-existent keys.
+ Assert.throws(function () {
+ cache.getCacheElement("a/non/existent/key", false);
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ let e1 = cache.getCacheElement("/made/up/path/Inbox", true);
+
+ // Can set, get and modify Int32 values?
+ e1.setCachedInt32("wibble", -1);
+ Assert.equal(e1.getCachedInt32("wibble"), -1);
+ e1.setCachedInt32("wibble", 42);
+ Assert.equal(e1.getCachedInt32("wibble"), 42);
+
+ // Check some allowed conversions from Int32.
+ Assert.equal(e1.getCachedUInt32("wibble"), 42);
+ Assert.equal(e1.getCachedInt64("wibble"), 42);
+ Assert.equal(e1.getCachedString("wibble"), "42");
+
+ // Can set, get and modify UInt32 values?
+ e1.setCachedUInt32("pibble", 0xffffffff);
+ Assert.equal(e1.getCachedUInt32("pibble"), 0xffffffff);
+ e1.setCachedUInt32("pibble", 42);
+ Assert.equal(e1.getCachedUInt32("pibble"), 42);
+
+ // Check some allowed conversions from UInt32.
+ Assert.equal(e1.getCachedInt32("pibble"), 42);
+ Assert.equal(e1.getCachedInt64("pibble"), 42);
+ Assert.equal(e1.getCachedString("pibble"), "42");
+
+ // Can set, get and modify Int64 values?
+ e1.setCachedInt64("foo", 2305843009213694000);
+ Assert.equal(e1.getCachedInt64("foo"), 2305843009213694000);
+ e1.setCachedInt64("foo", -2305843009213694000);
+ Assert.equal(e1.getCachedInt64("foo"), -2305843009213694000);
+ e1.setCachedInt64("foo", 42);
+ Assert.equal(e1.getCachedInt64("foo"), 42);
+
+ // Check some allowed conversions from Int64.
+ Assert.equal(e1.getCachedInt32("foo"), 42);
+ Assert.equal(e1.getCachedUInt32("foo"), 42);
+ Assert.equal(e1.getCachedString("foo"), "42");
+
+ // Can set, get and modify String values?
+ e1.setCachedString("bar", "Before");
+ Assert.equal(e1.getCachedString("bar"), "Before");
+ e1.setCachedString("bar", "After");
+ Assert.equal(e1.getCachedString("bar"), "After");
+ e1.setCachedString("bar", "日本語");
+ Assert.equal(e1.getCachedString("bar"), "日本語");
+
+ // Check some disallowed conversions from String.
+ Assert.throws(function () {
+ e1.getCachedInt32("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedUInt32("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedInt64("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+
+ // Trying to read missing properties is an error.
+ Assert.throws(function () {
+ e1.getCachedInt32("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedUInt32("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedInt64("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedString("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+
+ // Force a save to jsonFile. The changes we made will have queued up a
+ // cache autosave but we don't want to wait that long. The cache dtor
+ // would also save, but we don't want to second-guess JS garbage
+ // collection here.
+ cache.flush();
+ }
+
+ // Create a new cache object, reload jsonFile and make sure all the expected
+ // values are there.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // jsonFile is there now.
+ Assert.ok(jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+ cache.init(jsonFile, legacyFile);
+ // Make sure all the values we previously set are intact.
+ let e1 = cache.getCacheElement("/made/up/path/Inbox", true);
+ Assert.equal(e1.getCachedInt32("wibble"), 42);
+ Assert.equal(e1.getCachedUInt32("pibble"), 42);
+ Assert.equal(e1.getCachedInt64("foo"), 42);
+ Assert.equal(e1.getCachedString("bar"), "日本語");
+ }
+
+ // clean up for next test
+ jsonFile.remove(false);
+});
+
+add_task(async function test_null_entries() {
+ // Write out a trivial foldercache file with a null value.
+ let data = { "a-folder-key": { foo: null } };
+ let jsonFilename = PathUtils.join(PathUtils.tempDir, "foo.json");
+ await IOUtils.writeJSON(jsonFilename, data);
+
+ // Load it into an msIMsgFolderCache
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ let jsonFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ jsonFile.initWithPath(jsonFilename);
+ let morkFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ morkFile.initWithPath(
+ PathUtils.join(PathUtils.tempDir, "non-existent-file.dat")
+ );
+ cache.init(jsonFile, morkFile);
+
+ //
+ let e1 = cache.getCacheElement("a-folder-key", false);
+
+ // Make sure all accessors convert the null appropriately.
+ Assert.equal(e1.getCachedInt32("foo"), 0);
+ Assert.equal(e1.getCachedUInt32("foo"), 0);
+ Assert.equal(e1.getCachedInt64("foo"), 0);
+ Assert.equal(e1.getCachedString("foo"), "");
+});
+
+/**
+ * Test foldercache migration from mork DB (panacea.dat) to JSON.
+ */
+add_task(async function test_migration() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Install our test legacy file.
+ do_get_file("data/panacea.dat").copyTo(profileDir, legacyFile.leafName);
+
+ // Set up the cache.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ cache.init(jsonFile, legacyFile);
+
+ // Migration should have occurred.
+ Assert.ok(jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Done with the cache now.
+ }
+
+ // Compare the migrated json to the json we expect.
+ let raw = await IOUtils.readUTF8(jsonFile.path);
+ let got = JSON.parse(raw);
+
+ raw = await IOUtils.readUTF8(do_get_file("data/folderCache.json").path);
+ let expect = JSON.parse(raw);
+
+ Assert.deepEqual(got, expect);
+
+ // clean up for next test
+ jsonFile.remove(false);
+});
+
+/**
+ * Test foldercache migration doesn't crash with a dud panacea.dat.
+ */
+add_task(async function test_bad_pancea_dat() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Install our bad panacea.dat. It has only the first line - the mork magic
+ // cookie - so it's valid enough for mork to open, but doesn't have
+ // anything the migration is looking for.
+ do_get_file("data/panacea_empty.dat").copyTo(profileDir, legacyFile.leafName);
+
+ // Set up the cache.
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // init() returns OK even if migration fails - the show must go on!
+ cache.init(jsonFile, legacyFile);
+
+ // If we get this far, we didn't crash, which is good.
+ // The migration should have left everything as it was.
+ Assert.ok(legacyFile.exists());
+ Assert.ok(!jsonFile.exists());
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js
new file mode 100644
index 0000000000..c0a9b72e64
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for basic functionality with nsIMsgFolderListeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+var gIndividualFlags = [
+ nsIMFNService.msgAdded,
+ nsIMFNService.msgsClassified,
+ nsIMFNService.msgsJunkStatusChanged,
+ nsIMFNService.msgsDeleted,
+ nsIMFNService.msgsMoveCopyCompleted,
+ nsIMFNService.msgKeyChanged,
+ nsIMFNService.msgUnincorporatedMoved,
+ nsIMFNService.folderAdded,
+ nsIMFNService.folderDeleted,
+ nsIMFNService.folderMoveCopyCompleted,
+ nsIMFNService.folderRenamed,
+ nsIMFNService.folderCompactStart,
+ nsIMFNService.folderCompactFinish,
+ nsIMFNService.folderReindexTriggered,
+];
+
+// Our listener, which captures events.
+function gMFListener() {}
+gMFListener.prototype = {
+ mReceived: 0,
+ mRemoveSelf: false,
+ msgAdded(aMsg) {
+ Assert.equal(this.mReceived & nsIMFNService.msgAdded, 0);
+ this.mReceived |= nsIMFNService.msgAdded;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsClassified(aMsgs, aJunkProcessed, aTraitProcessed) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsClassified, 0);
+ this.mReceived |= nsIMFNService.msgsClassified;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsJunkStatusChanged(messages) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsJunkStatusChanged, 0);
+ this.mReceived |= nsIMFNService.msgsJunkStatusChanged;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsDeleted(aMsgs) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsDeleted, 0);
+ this.mReceived |= nsIMFNService.msgsDeleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsMoveCopyCompleted, 0);
+ this.mReceived |= nsIMFNService.msgsMoveCopyCompleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgKeyChanged(aOldMsgKey, aNewMsgHdr) {
+ Assert.equal(this.mReceived & nsIMFNService.msgKeyChanged, 0);
+ this.mReceived |= nsIMFNService.msgKeyChanged;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgUnincorporatedMoved(srcFolder, msg) {
+ Assert.equal(this.mReceived & nsIMFNService.msgUnincorporatedMoved, 0);
+ this.mReceived |= nsIMFNService.msgUnincorporatedMoved;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderAdded(aFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderAdded, 0);
+ this.mReceived |= nsIMFNService.folderAdded;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderDeleted(aFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderDeleted, 0);
+ this.mReceived |= nsIMFNService.folderDeleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderMoveCopyCompleted, 0);
+ this.mReceived |= nsIMFNService.folderMoveCopyCompleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderRenamed, 0);
+ this.mReceived |= nsIMFNService.folderRenamed;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderCompactStart(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderCompactStart, 0);
+ this.mReceived |= nsIMFNService.folderCompactStart;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderCompactFinish(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderCompactFinish, 0);
+ this.mReceived |= nsIMFNService.folderCompactFinish;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderReindexTriggered(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderReindexTriggered, 0);
+ this.mReceived |= nsIMFNService.folderReindexTriggered;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+};
+
+function NotifyMsgFolderListeners() {
+ MailServices.mfn.notifyMsgAdded(null);
+ MailServices.mfn.notifyMsgsClassified([], null, null);
+ MailServices.mfn.notifyMsgsJunkStatusChanged([]);
+ MailServices.mfn.notifyMsgsDeleted([]);
+ MailServices.mfn.notifyMsgsMoveCopyCompleted(null, [], null, []);
+ MailServices.mfn.notifyMsgKeyChanged(null, null);
+ MailServices.mfn.notifyMsgUnincorporatedMoved(null, null);
+ MailServices.mfn.notifyFolderAdded(null);
+ MailServices.mfn.notifyFolderDeleted(null);
+ MailServices.mfn.notifyFolderMoveCopyCompleted(null, null, null);
+ MailServices.mfn.notifyFolderRenamed(null, null);
+ MailServices.mfn.notifyFolderCompactStart(null);
+ MailServices.mfn.notifyFolderCompactFinish(null);
+ MailServices.mfn.notifyFolderReindexTriggered(null);
+}
+
+function run_test() {
+ // Test: Add listeners
+ var singleListeners = [];
+
+ var addAListener = function (flag) {
+ var listener = new gMFListener();
+ MailServices.mfn.addListener(listener, flag);
+ singleListeners.push(listener);
+ };
+
+ gIndividualFlags.forEach(addAListener);
+
+ // Test: Notify the listeners of all events.
+ NotifyMsgFolderListeners();
+
+ // Test: check whether the correct number of notifications have been received.
+ // Then remove the listeners
+ var checkFlag = function (flag) {
+ var listener = singleListeners.shift();
+ Assert.equal(listener.mReceived, flag);
+ listener.mRemoveSelf = true;
+ listener.mReceived = 0;
+ singleListeners.push(listener);
+ };
+ gIndividualFlags.forEach(checkFlag);
+
+ // We'll do one more set of notifications, and remove ourselves in the middle of them
+ NotifyMsgFolderListeners();
+
+ // Test: all listeners should be removed at this point
+ Assert.ok(!MailServices.mfn.hasListeners);
+
+ // Test: Send notifications again. Check that we don't receive any notifications.
+ singleListeners.forEach(function (listener) {
+ listener.mReceived = 0;
+ });
+
+ NotifyMsgFolderListeners();
+
+ var checkNotReceived = function () {
+ Assert.equal(singleListeners.shift().mReceived, 0);
+ };
+ gIndividualFlags.forEach(checkNotReceived);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js
new file mode 100644
index 0000000000..aadbd75b8a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js
@@ -0,0 +1,444 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for nsIMsgFolderListener events due to local mail folder
+ * operations.
+ *
+ * Currently tested:
+ * - Adding new folders
+ * - Copy messages from files into the db
+ * - Moving and copying one or more messages from one local folder to another
+ * - Moving folders, with and without subfolders
+ * - Renaming folders
+ * - Deleting messages and folders, to trash and from trash (permanently)
+ */
+
+/* import-globals-from ../../../test/resources/msgFolderListenerSetup.js */
+load("../../../resources/msgFolderListenerSetup.js");
+
+// Globals
+var gMsgFile1, gMsgFile2, gMsgFile3;
+var gRootFolder;
+var gLocalFolder2;
+var gLocalFolder3;
+var gLocalTrashFolder;
+
+// storeIn takes a string containing the variable to store the new folder in
+function addFolder(parent, folderName, storeIn) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderAdded, parent, folderName, storeIn],
+ ];
+ // We won't receive a copy listener notification for this
+ gCurrStatus |= kStatus.onStopCopyDone;
+ parent.createSubfolder(folderName, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+/**
+ * This will introduce a new message to the system which will generate an added
+ * notification and subsequently a classification notification. For the
+ * classification because no messages have yet been marked as junk and there
+ * are no traits configured, aJunkProcessed and aTraitProcessed will be false.
+ */
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ copyListener.mFolderStoredIn = destFolder;
+ gExpectedEvents = [
+ [MailServices.mfn.msgAdded, gHdrsReceived],
+ [MailServices.mfn.msgsClassified, gHdrsReceived, false, false],
+ ];
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function copyMessages(items, isMove, srcFolder, destFolder) {
+ gExpectedEvents = [
+ [MailServices.mfn.msgsMoveCopyCompleted, isMove, items, destFolder, true],
+ ];
+ MailServices.copy.copyMessages(
+ srcFolder,
+ items,
+ destFolder,
+ isMove,
+ copyListener,
+ null,
+ true
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function copyFolder(srcFolder, isMove, destFolder) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderMoveCopyCompleted, isMove, [srcFolder], destFolder],
+ ];
+ MailServices.copy.copyFolder(
+ srcFolder,
+ destFolder,
+ isMove,
+ copyListener,
+ null
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function deleteMessages(srcFolder, items, deleteStorage, isMove) {
+ // We should only get the delete notification only if we are not moving, and are deleting from
+ // the storage/trash. We should get only the move/copy notification if we aren't.
+ var isTrashFolder = srcFolder.getFlag(Ci.nsMsgFolderFlags.Trash);
+ if (!isMove && (deleteStorage || isTrashFolder)) {
+ // We won't be getting any OnStopCopy notification in this case
+ gCurrStatus = kStatus.onStopCopyDone;
+ gExpectedEvents = [[MailServices.mfn.msgsDeleted, items]];
+ } else {
+ // We have to be getting a move notification, even if isMove is false
+ gExpectedEvents = [
+ [
+ MailServices.mfn.msgsMoveCopyCompleted,
+ true,
+ items,
+ gLocalTrashFolder,
+ true,
+ ],
+ ];
+ }
+
+ srcFolder.deleteMessages(
+ items,
+ null,
+ deleteStorage,
+ isMove,
+ copyListener,
+ true
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function renameFolder(folder, newName) {
+ gExpectedEvents = [[MailServices.mfn.folderRenamed, [folder], newName]];
+ gCurrStatus = kStatus.onStopCopyDone;
+ folder.rename(newName, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function deleteFolder(folder, child) {
+ // We won't be getting any OnStopCopy notification at all
+ // XXX delete to trash should get one, but we'll need to pass the listener
+ // somehow to deleteSelf
+ gCurrStatus = kStatus.onStopCopyDone;
+ // If ancestor is trash, expect a folderDeleted, otherwise expect
+ // a folderMoveCopyCompleted.
+ if (gLocalTrashFolder.isAncestorOf(folder)) {
+ if (child) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderDeleted, [child]],
+ [MailServices.mfn.folderDeleted, [folder]],
+ ];
+ } else {
+ gExpectedEvents = [[MailServices.mfn.folderDeleted, [folder]]];
+ }
+ } else {
+ gExpectedEvents = [
+ [
+ MailServices.mfn.folderMoveCopyCompleted,
+ true,
+ [folder],
+ gLocalTrashFolder,
+ ],
+ ];
+ }
+
+ folder.deleteSelf(null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function compactFolder(folder) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderCompactStart, folder],
+ [MailServices.mfn.folderCompactFinish, folder],
+ ];
+ // We won't receive a copy listener notification for this
+ gCurrStatus |= kStatus.onStopCopyDone;
+ folder.compact(null, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Adding folders
+ // Create another folder to move and copy messages around, and force initialization.
+ function addFolder1() {
+ addFolder(gRootFolder, "folder2", function (folder) {
+ gLocalFolder2 = folder;
+ });
+ },
+ // Create a third folder for more testing.
+ function addFolder2() {
+ addFolder(gRootFolder, "folder3", function (folder) {
+ gLocalFolder3 = folder;
+ });
+ },
+ // Folder structure is now
+ // Inbox
+ // Trash
+ // folder2
+ // folder3
+ // Copying messages from files
+ function testCopyFileMessage1() {
+ copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+ function testCopyFileMessage2() {
+ copyFileMessage(gMsgFile2, localAccountUtils.inboxFolder, false);
+ },
+ function testCopyFileMessage3() {
+ copyFileMessage(gMsgFile3, localAccountUtils.inboxFolder, true);
+ },
+
+ // Moving/copying messages
+ function testCopyMessages1() {
+ copyMessages(
+ [gMsgHdrs[0].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ function testCopyMessages2() {
+ copyMessages(
+ [gMsgHdrs[1].hdr, gMsgHdrs[2].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ function testMoveMessages1() {
+ copyMessages(
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalFolder3
+ );
+ },
+ function testMoveMessages2() {
+ copyMessages(
+ [gMsgHdrs[2].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalTrashFolder
+ );
+ },
+ function testMoveMessages3() {
+ // This is to test whether the notification is correct for moving from trash
+ gMsgHdrs[2].hdr = gLocalTrashFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsgHdrs[2].ID
+ );
+ copyMessages([gMsgHdrs[2].hdr], true, gLocalTrashFolder, gLocalFolder3);
+ },
+ // Moving/copying folders
+ function testCopyFolder1() {
+ copyFolder(gLocalFolder3, false, gLocalFolder2);
+ },
+ function testMoveFolder1() {
+ copyFolder(gLocalFolder3, true, localAccountUtils.inboxFolder);
+ },
+ function testMoveFolder2() {
+ copyFolder(gLocalFolder2, true, localAccountUtils.inboxFolder);
+ },
+ // Folder structure should now be
+ // Inbox
+ // -folder2
+ // --folder3
+ // -folder3
+ // Trash
+
+ // Deleting messages
+ function testDeleteMessages1() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ gLocalFolder2 = localAccountUtils.inboxFolder.getChildNamed("folder2");
+ gLocalFolder3 = gLocalFolder2.getChildNamed("folder3");
+ var folder3DB = gLocalFolder3.msgDatabase;
+ for (var i = 0; i < gMsgHdrs.length; i++) {
+ gMsgHdrs[i].hdr = folder3DB.getMsgHdrForMessageID(gMsgHdrs[i].ID);
+ }
+
+ // Now delete the message
+ deleteMessages(
+ gLocalFolder3,
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ false,
+ false
+ );
+ },
+ // shift delete
+ function testDeleteMessages2() {
+ deleteMessages(gLocalFolder3, [gMsgHdrs[2].hdr], true, false);
+ },
+ function testDeleteMessages3() {
+ // normal delete from trash
+ var trashDB = gLocalTrashFolder.msgDatabase;
+ for (var i = 0; i < gMsgHdrs.length; i++) {
+ gMsgHdrs[i].hdr = trashDB.getMsgHdrForMessageID(gMsgHdrs[i].ID);
+ }
+ deleteMessages(gLocalTrashFolder, [gMsgHdrs[0].hdr], false, false);
+ },
+ // shift delete from trash
+ function testDeleteMessages4() {
+ deleteMessages(gLocalTrashFolder, [gMsgHdrs[1].hdr], true, false);
+ },
+
+ // Renaming folders
+ function testRename1() {
+ renameFolder(gLocalFolder3, "folder4");
+ },
+ function testRename2() {
+ renameFolder(gLocalFolder2.getChildNamed("folder4"), "folder3");
+ },
+ function testRename3() {
+ renameFolder(gLocalFolder2, "folder4");
+ },
+ function testRename4() {
+ renameFolder(
+ localAccountUtils.inboxFolder.getChildNamed("folder4"),
+ "folder2"
+ );
+ },
+
+ // Folder structure should still be
+ // Inbox
+ // -folder2
+ // --folder3
+ // -folder3
+ // Trash
+
+ // Deleting folders (currently only one folder delete is supported through the UI)
+ function deleteFolder1() {
+ deleteFolder(localAccountUtils.inboxFolder.getChildNamed("folder3"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // -folder2
+ // --folder3
+ // Trash
+ // -folder3
+ function deleteFolder2() {
+ deleteFolder(localAccountUtils.inboxFolder.getChildNamed("folder2"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // Trash
+ // -folder2
+ // --folder3
+ // -folder3
+ function deleteFolder3() {
+ deleteFolder(gLocalTrashFolder.getChildNamed("folder3"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // Trash
+ // -folder2
+ // --folder3
+ function deleteFolder4() {
+ // Let's take a moment to re-initialize stuff that got moved
+ gLocalFolder2 = gLocalTrashFolder.getChildNamed("folder2");
+ gLocalFolder3 = gLocalFolder2.getChildNamed("folder3");
+ deleteFolder(gLocalFolder2, gLocalFolder3);
+ },
+ function compactInbox() {
+ if (localAccountUtils.inboxFolder.msgStore.supportsCompaction) {
+ compactFolder(localAccountUtils.inboxFolder);
+ } else {
+ doTest(++gTest);
+ }
+ },
+];
+// Folder structure should just be
+// Inbox
+// Trash
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Add a listener.
+ MailServices.mfn.addListener(gMFListener, allTestedEvents);
+
+ // Load up some messages so that we can copy them in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ gMsgFile2 = do_get_file("../../../data/bugmail11");
+ gMsgFile3 = do_get_file("../../../data/draft1");
+
+ // "Trash" folder
+ gRootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ gLocalTrashFolder = gRootFolder.getChildNamed("Trash");
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of all the operations.
+ do_test_pending();
+
+ // Do the test.
+ doTest(1);
+}
+
+function doTest(test) {
+ if (test <= gTestArray.length) {
+ var testFn = gTestArray[test - 1];
+ // Set a limit of 10 seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gTest == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " +
+ testFn.name +
+ ", current status is " +
+ gCurrStatus
+ );
+ }
+ });
+ dump("=== Test: " + testFn.name + "\n");
+ testFn();
+ } else {
+ gHdrsReceived = null;
+ gMsgHdrs = null;
+ MailServices.mfn.removeListener(gMFListener);
+ do_test_finished(); // for the one in run_test()
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgTagService.js b/comm/mailnews/base/test/unit/test_nsIMsgTagService.js
new file mode 100644
index 0000000000..52e36056ef
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgTagService.js
@@ -0,0 +1,113 @@
+/* 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/. */
+
+/*
+ * Tests of nsIMsgTagService.
+ *
+ * Specifically tests changes implemented in bug 217034
+ * Does not do comprehensive testing.
+ *
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // These are both tags and keys. Note keys are forced to be lower case
+ const tag1 = "istag";
+ const tag2 = "notistag";
+ const tag3 = "istagnot";
+ const tag4 = "istagtoo";
+
+ // add a tag
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+
+ // delete any existing tags
+ let tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; i++) {
+ MailServices.tags.deleteKey(tagArray[i].key);
+ }
+
+ // make sure added tag is now gone
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+
+ // add single tag, and check again
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+ Assert.ok(MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag4));
+
+ // add second tag and check
+ MailServices.tags.addTagForKey(tag4, tag4, null, null);
+ Assert.ok(MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ // delete a tag and check
+ MailServices.tags.deleteKey(tag1);
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ // add many tags and check again
+ for (i = 0; i < 100; i++) {
+ MailServices.tags.addTagForKey(i, "lotsatags" + i, null, null);
+ }
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ for (i = 0; i < 100; i++) {
+ Assert.ok(MailServices.tags.isValidKey(i));
+ // make sure it knows the difference betweens tags and keys
+ Assert.ok(!MailServices.tags.isValidKey("lotsatags" + i));
+ // are we confused by key at start of tag?
+ Assert.ok(!MailServices.tags.isValidKey(i + "lotsatags"));
+ }
+
+ // Test sort ordering for getAllTags() without ordinal.
+ for (let tag of MailServices.tags.getAllTags()) {
+ MailServices.tags.deleteKey(tag.key);
+ }
+ MailServices.tags.addTag("grapefruit", null, null);
+ MailServices.tags.addTag("orange", null, null);
+ MailServices.tags.addTag("lime", null, null);
+ MailServices.tags.addTag("lemon", null, null);
+
+ // Should be sorted by tag name.
+ let tagNames = MailServices.tags.getAllTags().map(t => t.tag);
+ Assert.deepEqual(
+ tagNames,
+ ["grapefruit", "lemon", "lime", "orange"],
+ "Sort without ordinals"
+ );
+
+ // Test sort ordering for getAllTags() with (some) ordinals.
+ for (let tag of MailServices.tags.getAllTags()) {
+ MailServices.tags.deleteKey(tag.key);
+ }
+ MailServices.tags.addTag("grapefruit", null, "3");
+ MailServices.tags.addTag("orange", null, "1");
+ MailServices.tags.addTag("lime", null, null);
+ MailServices.tags.addTag("lemon", null, "2");
+
+ // Should be sorted by ordinal, then tag name.
+ tagNames = MailServices.tags.getAllTags().map(t => t.tag);
+ Assert.deepEqual(
+ tagNames,
+ ["orange", "lemon", "grapefruit", "lime"],
+ "Sort with ordinals"
+ );
+}
+
+/*
+function printTags() {
+ for (let tag of MailServices.tags.getAllTags()) {
+ print(`# key [${tag.key}] tag [${tag.tag}] ordinal [${tag.ordinal}]`);
+ }
+}
+*/
diff --git a/comm/mailnews/base/test/unit/test_nsMailDirProvider.js b/comm/mailnews/base/test/unit/test_nsMailDirProvider.js
new file mode 100644
index 0000000000..19b848f3fd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMailDirProvider.js
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMailDirProvider to check we get the right directories and
+ * files.
+ */
+
+function run_test() {
+ const items = [
+ { key: "MailD", value: "Mail" },
+ { key: "IMapMD", value: "ImapMail" },
+ { key: "NewsD", value: "News" },
+ { key: "MLFCaF", value: "panacea.dat" }, // Legacy folder cache.
+ { key: "MFCaF", value: "folderCache.json" },
+ ];
+
+ items.forEach(function (item) {
+ var dir = Services.dirsvc.get(item.key, Ci.nsIFile);
+ dump(do_get_profile().path + " " + dir.path + "\n");
+ Assert.ok(do_get_profile().equals(dir.parent));
+
+ Assert.equal(dir.leafName, item.value);
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgDBView.js b/comm/mailnews/base/test/unit/test_nsMsgDBView.js
new file mode 100644
index 0000000000..cb5527deab
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgDBView.js
@@ -0,0 +1,1212 @@
+/* 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/. */
+
+/*
+ * Attempt to test nsMsgDBView and descendents. Right now this means we:
+ * - Ensure sorting and grouping sorta works, including using custom columns.
+ *
+ * Things we really should do:
+ * - Test that secondary sorting works, especially when the primary column is
+ * a custom column.
+ *
+ * You may also want to look into the test_viewWrapper_*.js tests as well.
+ */
+
+var { MessageGenerator, MessageScenarioFactory, SyntheticMessageSet } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+const { TreeSelection } = ChromeUtils.importESModule(
+ "chrome://messenger/content/tree-selection.mjs"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { dump_view_contents } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ViewHelpers.jsm"
+);
+
+// Items used to add messages to the folder
+var gMessageGenerator = new MessageGenerator();
+var gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
+var messageInjection = new MessageInjection({ mode: "local" });
+
+var gTestFolder;
+var gSiblingsMissingParentsSubject;
+var gMessages;
+
+function setup_messages() {
+ // build up a diverse list of messages
+ let messages = [];
+ messages = messages.concat(gScenarioFactory.directReply(10));
+ // the message generator uses a constanty incrementing counter, so we need to
+ // mix up the order of messages ourselves to ensure that the timestamp
+ // ordering is not already in order. (a poor test of sorting otherwise.)
+ messages = gScenarioFactory.directReply(6).concat(messages);
+
+ messages = messages.concat(gScenarioFactory.fullPyramid(3, 3));
+ let siblingMessages = gScenarioFactory.siblingsMissingParent();
+ // cut off "Re: " part
+ gSiblingsMissingParentsSubject = siblingMessages[0].subject.slice(4);
+ dump("siblings subect = " + gSiblingsMissingParentsSubject + "\n");
+ messages = messages.concat(siblingMessages);
+ messages = messages.concat(gScenarioFactory.missingIntermediary());
+ // This next line was found to be faulty during linting, but fixing it breaks the test.
+ // messages.concat(gMessageGenerator.makeMessage({age: {days: 2, hours: 1}}));
+
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 1
+ // 2
+ // 4
+ // 3
+ let msg1 = gMessageGenerator.makeMessage();
+ let msg2 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
+ let msg3 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
+ let msg4 = gMessageGenerator.makeMessage({ inReplyTo: msg2 });
+ messages = messages.concat([msg1, msg2, msg3, msg4]);
+
+ // test bug 600140, make a thread that Reply message has smaller MsgKey
+ let msgBiggerKey = gMessageGenerator.makeMessage();
+ let msgSmallerKey = gMessageGenerator.makeMessage({
+ inReplyTo: msgBiggerKey,
+ });
+ messages = messages.concat([msgSmallerKey, msgBiggerKey]);
+ let msgSet = new SyntheticMessageSet(messages);
+ return msgSet;
+}
+
+/**
+ * Sets gTestFolder with msgSet. Ensure that gTestFolder is clean for each test.
+ *
+ * @param {SyntheticMessageSet} msgSet
+ */
+async function set_gTestFolder(msgSet) {
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+}
+
+/**
+ * Create a synthetic message by passing the provided aMessageArgs to
+ * the message generator, then add the resulting message to the given
+ * folder (or gTestFolder if no folder is provided).
+ */
+async function make_and_add_message(aMessageArgs) {
+ // create the message
+ let synMsg = gMessageGenerator.makeMessage(aMessageArgs);
+ let msgSet = new SyntheticMessageSet([synMsg]);
+ // this is synchronous for local stuff.
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+
+ return [synMsg, msgSet];
+}
+
+function view_throw(why) {
+ dump_view_contents();
+ do_throw(why);
+}
+
+/**
+ * Throw if gDBView has any rows.
+ */
+function assert_view_empty() {
+ if (gTreeView.rowCount != 0) {
+ view_throw(
+ "Expected view to be empty, but it was not! (" +
+ gTreeView.rowCount +
+ " rows)"
+ );
+ }
+}
+
+/**
+ * Throw if gDBView does not have aCount rows.
+ */
+function assert_view_row_count(aCount) {
+ if (gTreeView.rowCount != aCount) {
+ view_throw(
+ "Expected view to have " +
+ aCount +
+ " rows, but it had " +
+ gTreeView.rowCount +
+ " rows!"
+ );
+ }
+}
+
+/**
+ * Throw if any of the arguments (as view indices) do not correspond to dummy
+ * rows in gDBView.
+ */
+function assert_view_index_is_dummy(...aArgs) {
+ for (let viewIndex of aArgs) {
+ let flags = gDBView.getFlagsAt(viewIndex);
+ if (!(flags & MSG_VIEW_FLAG_DUMMY)) {
+ view_throw("Expected index " + viewIndex + " to be a dummy!");
+ }
+ }
+}
+
+/**
+ * Throw if any of the arguments (as view indices) correspond to dummy rows in
+ * gDBView.
+ */
+function assert_view_index_is_not_dummy(...aArgs) {
+ for (let viewIndex of aArgs) {
+ let flags = gDBView.getFlagsAt(viewIndex);
+ if (flags & MSG_VIEW_FLAG_DUMMY) {
+ view_throw("Expected index " + viewIndex + " to not be a dummy!");
+ }
+ }
+}
+
+function assert_view_level_is(index, level) {
+ if (gDBView.getLevel(index) != level) {
+ view_throw(
+ "Expected index " +
+ index +
+ " to be level " +
+ level +
+ " not " +
+ gDBView.getLevel(index)
+ );
+ }
+}
+
+/**
+ * Given a message, assert that it is present at the given indices.
+ *
+ * Usage:
+ * assert_view_message_at_indices(synMsg, 0);
+ * assert_view_message_at_indices(synMsg, 0, 1);
+ * assert_view_message_at_indices(aMsg, 0, bMsg, 1);
+ */
+function assert_view_message_at_indices(...aArgs) {
+ let curHdr;
+ for (let thing of aArgs) {
+ if (typeof thing == "number") {
+ let hdrAt = gDBView.getMsgHdrAt(thing);
+ if (curHdr != hdrAt) {
+ view_throw(
+ "Expected hdr at " +
+ thing +
+ " to be " +
+ curHdr.messageKey +
+ ":" +
+ curHdr.mime2DecodedSubject.substr(0, 30) +
+ " not " +
+ hdrAt.messageKey +
+ ":" +
+ hdrAt.mime2DecodedSubject.substr(0, 30)
+ );
+ }
+ } else {
+ // synthetic message, get the header...
+ curHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(thing.messageId);
+ }
+ }
+}
+
+var authorFirstLetterCustomColumn = {
+ getCellText(row, col) {
+ let msgHdr = this.dbView.getMsgHdrAt(row);
+ return msgHdr.mime2DecodedAuthor.charAt(0).toUpperCase() || "?";
+ },
+ getSortStringForRow(msgHdr) {
+ // charAt(0) is a quote, charAt(1) is the first letter!
+ return msgHdr.mime2DecodedAuthor.charAt(1).toUpperCase() || "?";
+ },
+ isString() {
+ return true;
+ },
+
+ getCellProperties(row, col) {
+ return "";
+ },
+ getRowProperties(row) {
+ return "";
+ },
+ getImageSrc(row, col) {
+ return null;
+ },
+ getSortLongForRow(hdr) {
+ return 0;
+ },
+};
+
+var gDBView;
+var gTreeView;
+
+var MSG_VIEW_FLAG_DUMMY = 0x20000000;
+
+var gFakeSelection = new TreeSelection(null);
+
+function setup_view(aViewType, aViewFlags, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + aViewType;
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // always start out fully expanded
+ aViewFlags |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ aViewType != "search" ? aTestFolder : null,
+ Ci.nsMsgViewSortType.byDate,
+ aViewType != "search"
+ ? Ci.nsMsgViewSortOrder.ascending
+ : Ci.nsMsgViewSortOrder.descending,
+ aViewFlags,
+ outCount
+ );
+ // outCount is 0 if byCustom; view is built by addColumnHandler()
+ dump(" View Out Count: " + outCount.value + "\n");
+
+ // we need to cram messages into the search via nsIMsgSearchNotify interface
+ if (
+ aViewType == "search" ||
+ aViewType == "quicksearch" ||
+ aViewType == "xfvf"
+ ) {
+ let searchNotify = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ searchNotify.onNewSearch();
+ for (let msgHdr of aTestFolder.msgDatabase.enumerateMessages()) {
+ searchNotify.onSearchHit(msgHdr, msgHdr.folder);
+ }
+ searchNotify.onSearchDone(Cr.NS_OK);
+ }
+
+ gDBView.addColumnHandler(
+ "authorFirstLetterCol",
+ authorFirstLetterCustomColumn
+ );
+ // XXX this sets the custom column to use for sorting by the custom column.
+ // It has been argued (and is generally accepted) that this should not be
+ // so limited.
+ gDBView.curCustomColumn = "authorFirstLetterCol";
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gTreeView.selection = gFakeSelection;
+ gFakeSelection.view = gTreeView;
+}
+
+function setup_group_view(aSortType, aSortOrder, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=group";
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // grouped view uses these flags
+ let viewFlags =
+ Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kExpandAll |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(aTestFolder, aSortType, aSortOrder, viewFlags, outCount);
+
+ gDBView.addColumnHandler(
+ "authorFirstLetterCol",
+ authorFirstLetterCustomColumn
+ );
+ gDBView.curCustomColumn = "authorFirstLetterCol";
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gFakeSelection.view = gTreeView;
+ gTreeView.selection = gFakeSelection;
+}
+
+/**
+ * Comparison func for built-in types (including strings, so no subtraction.)
+ */
+function generalCmp(a, b) {
+ if (a < b) {
+ return -1;
+ } else if (a > b) {
+ return 1;
+ }
+ return 0;
+}
+
+/**
+ * Check that sort order and grouping logic (if applicable) are doing the right
+ * thing.
+ *
+ * In the case of groups (indicated by dummy headers), we want to ignore the
+ * dummies and 1) make sure all the values in the group have the same value,
+ * 2) verify that the headers meet our total ordering.
+ * In the case of threads, we want to ensure that each level of the hierarchy
+ * meets our ordering demands, recursing into children. Because the tree
+ * representation is rather quite horrible, the easiest thing for us is to
+ * track a per-level list of comparison values we have seen, nuking older
+ * values when changes in levels indicate closure of a level. (Namely,
+ * if we see a node at level N, then all levels >N are no longer valid.)
+ *
+ * @param {nsMsgViewType} aSortBy - The sort type.
+ * @param {nsMsgViewSortOrder} aDirection - The sort direction.
+ * @param {string|Function} aKeyOrValueGetter - A string naming the attribute on
+ * the message headerto retrieve, or if that is not sufficient a function that
+ * takes a message header and returns the sort value for it.
+ * @param {Function} [aGetGroupValue] - An optional function that takes a
+ * message header and returns the grouping value for the header.
+ * If omitted, it is assumed that the sort value is the grouping value.
+ */
+function ensure_view_ordering(
+ aSortBy,
+ aDirection,
+ aKeyOrValueGetter,
+ aGetGroupValue
+) {
+ if (!gTreeView.rowCount) {
+ do_throw("There are no rows in my folder! I can't test anything!");
+ }
+ dump(
+ " Ensuring sort order for " +
+ aSortBy +
+ " (Row count: " +
+ gTreeView.rowCount +
+ ")\n"
+ );
+ dump(" cur view flags: " + gDBView.viewFlags + "\n");
+
+ // standard grouping doesn't re-group when you sort. so we need to actually
+ // re-initialize the view.
+ // but search mode is special and does the right thing because asuth didn't
+ // realize that it shouldn't do the right thing, so it can just change the
+ // sort. (of course, under the hood, it is actually creating a new view...)
+ if (
+ gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort &&
+ gDBView.viewType != Ci.nsMsgViewType.eShowSearch
+ ) {
+ // we must close to re-open (or we could just use a new view)
+ let msgFolder = gDBView.msgFolder;
+ gDBView.close();
+ gDBView.open(msgFolder, aSortBy, aDirection, gDBView.viewFlags, {});
+ } else {
+ gDBView.sort(aSortBy, aDirection);
+ }
+
+ let comparisonValuesByLevel = [];
+ let expectedLevel0CmpResult =
+ aDirection == Ci.nsMsgViewSortOrder.ascending ? 1 : -1;
+ let comparator = generalCmp;
+
+ let dummyCount = 0,
+ emptyDummyCount = 0;
+
+ let valueGetter =
+ typeof aKeyOrValueGetter == "string"
+ ? function (msgHdr) {
+ return msgHdr[aKeyOrValueGetter];
+ }
+ : aKeyOrValueGetter;
+ let groupValueGetter = aGetGroupValue || valueGetter;
+
+ // don't do group testing until we see a dummy header (which we will see
+ // before we see any grouped headers, so it's fine to do this)
+ let inGroup = false;
+ // the current grouping value for the current group. this allows us to
+ // detect erroneous grouping of different group values together.
+ let curGroupValue = null;
+ // the set of group values observed before the current group. this allows
+ // us to detect improper grouping where there are multiple groups with the
+ // same grouping value.
+ let previouslySeenGroupValues = {};
+
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let msgHdr = gDBView.getMsgHdrAt(iViewIndex);
+ let msgViewFlags = gDBView.getFlagsAt(iViewIndex);
+
+ // ignore dummy headers; testing grouping logic happens elsewhere
+ if (msgViewFlags & MSG_VIEW_FLAG_DUMMY) {
+ if (dummyCount && curGroupValue == null) {
+ emptyDummyCount++;
+ }
+ dummyCount++;
+ if (curGroupValue != null) {
+ previouslySeenGroupValues[curGroupValue] = true;
+ }
+ curGroupValue = null;
+ inGroup = true;
+ continue;
+ }
+
+ // level is 0-based
+ let level = gTreeView.getLevel(iViewIndex);
+ // nuke existing comparison levels
+ if (level < comparisonValuesByLevel.length - 1) {
+ comparisonValuesByLevel.splice(level);
+ }
+
+ // get the value for comparison
+ let curValue = valueGetter(msgHdr);
+ if (inGroup) {
+ let groupValue = groupValueGetter(msgHdr);
+ if (groupValue in previouslySeenGroupValues) {
+ do_throw(`Group value ${groupValue} observed in more than one group!`);
+ }
+ if (curGroupValue == null) {
+ curGroupValue = groupValue;
+ } else if (curGroupValue != groupValue) {
+ do_throw(
+ "Inconsistent grouping! " + groupValue + " != " + curGroupValue
+ );
+ }
+ }
+
+ // is this level new to our comparisons? then track it...
+ if (level >= comparisonValuesByLevel.length) {
+ // null-fill any gaps (due to, say, dummy nodes)
+ while (comparisonValuesByLevel.length <= level) {
+ comparisonValuesByLevel.push(null);
+ }
+ comparisonValuesByLevel.push(curValue);
+ } else {
+ // otherwise compare it
+ let prevValue = comparisonValuesByLevel[level - 1];
+ let cmpResult = comparator(curValue, prevValue);
+ let expectedCmpResult = level > 0 ? 1 : expectedLevel0CmpResult;
+ if (cmpResult && cmpResult != expectedCmpResult) {
+ do_throw(
+ "Ordering failure on key " +
+ msgHdr.messageKey +
+ ". " +
+ curValue +
+ " should have been " +
+ (expectedCmpResult == 1 ? ">=" : "<=") +
+ " " +
+ prevValue +
+ " but was not."
+ );
+ }
+ }
+ }
+
+ if (inGroup && curGroupValue == null) {
+ emptyDummyCount++;
+ }
+ if (dummyCount) {
+ dump(
+ " saw " +
+ dummyCount +
+ " dummy headers (" +
+ emptyDummyCount +
+ " empty).\n"
+ );
+ }
+}
+
+/**
+ * Test sorting functionality.
+ */
+function test_sort_columns() {
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.descending,
+ "date",
+ function getDateAgeBucket(msgHdr) {
+ // so, this is a cop-out, but we know that the date age bucket for our
+ // generated messages is always more than 2-weeks ago!
+ return 5;
+ }
+ );
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ "date",
+ function getDateAgeBucket(msgHdr) {
+ // so, this is a cop-out, but we know that the date age bucket for our
+ // generated messages is always more than 2-weeks ago!
+ return 5;
+ }
+ );
+ // (note, subject doesn't use dummy groups and so won't have grouping tested)
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.bySubject,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedSubject"
+ );
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byAuthor,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedAuthor"
+ );
+ // Id
+ // Thread
+ // Priority
+ // Status
+ // Size
+ // Flagged
+ // Unread
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byRecipient,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedRecipients"
+ );
+ // Location
+ // Tags
+ // JunkStatus
+ // Attachments
+ // Account
+ // Custom
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byCustom,
+ Ci.nsMsgViewSortOrder.ascending,
+ function (msgHdr) {
+ return authorFirstLetterCustomColumn.getSortStringForRow(msgHdr);
+ }
+ );
+ // Received
+}
+
+function test_number_of_messages() {
+ // Bug 574799
+ if (gDBView.numMsgsInView != gTestFolder.getTotalMessages(false)) {
+ do_throw(
+ "numMsgsInView is " +
+ gDBView.numMsgsInView +
+ " but should be " +
+ gTestFolder.getTotalMessages(false) +
+ "\n"
+ );
+ }
+ // Bug 600140
+ // Maybe elided so open it, now only consider the first one
+ if (gDBView.isContainer(0) && !gDBView.isContainerOpen(0)) {
+ gDBView.toggleOpenState(0);
+ }
+ let numMsgInTree = gTreeView.rowCount;
+ if (gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort) {
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let flags = gDBView.getFlagsAt(iViewIndex);
+ if (flags & MSG_VIEW_FLAG_DUMMY) {
+ numMsgInTree--;
+ }
+ }
+ }
+ if (gDBView.numMsgsInView != numMsgInTree) {
+ view_throw(
+ "message in tree is " +
+ numMsgInTree +
+ " but should be " +
+ gDBView.numMsgsInView +
+ "\n"
+ );
+ }
+}
+
+function test_selected_messages() {
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+
+ // Select one message
+ gTreeView.selection.select(1);
+ let selectedMessages = gDBView.getSelectedMsgHdrs();
+
+ if (selectedMessages.length != 1) {
+ do_throw(
+ "getSelectedMsgHdrs.length is " +
+ selectedMessages.length +
+ " but should be 1\n"
+ );
+ }
+
+ let firstSelectedMsg = gDBView.hdrForFirstSelectedMessage;
+ if (selectedMessages[0] != firstSelectedMsg) {
+ do_throw(
+ "getSelectedMsgHdrs[0] is " +
+ selectedMessages[0].messageKey +
+ " but should be " +
+ firstSelectedMsg.messageKey +
+ "\n"
+ );
+ }
+
+ // Select all messages
+ gTreeView.selection.selectAll();
+ if (gDBView.numSelected != gTreeView.rowCount) {
+ do_throw(
+ "numSelected is " +
+ gDBView.numSelected +
+ " but should be " +
+ gTreeView.rowCount +
+ "\n"
+ );
+ }
+
+ selectedMessages = gDBView.getSelectedMsgHdrs();
+ if (selectedMessages.length != gTestFolder.getTotalMessages(false)) {
+ do_throw(
+ "getSelectedMsgHdrs.length is " +
+ selectedMessages.length +
+ " but should be " +
+ gTestFolder.getTotalMessages(false) +
+ "\n"
+ );
+ }
+
+ for (let i = 0; i < selectedMessages.length; i++) {
+ let expectedHdr = gDBView.getMsgHdrAt(i);
+ if (!selectedMessages.includes(expectedHdr)) {
+ view_throw(
+ "Expected " +
+ expectedHdr.messageKey +
+ ":" +
+ expectedHdr.mime2DecodedSubject.substr(0, 30) +
+ " to be selected, but it wasn't\n"
+ );
+ }
+ }
+
+ gTreeView.selection.clearSelection();
+}
+
+function test_insert_remove_view_rows() {
+ // Test insertion/removal into m_keys.
+ let startCount = gTreeView.rowCount;
+ let index = 0;
+ let rows = 3;
+ let msgKey = rows * 1000;
+ let flags = 0;
+ let level = 0;
+ let folder = null;
+ let xfview =
+ gDBView.viewType == Ci.nsMsgViewType.eShowSearch ||
+ gDBView.viewType == Ci.nsMsgViewType.eShowVirtualFolderResults;
+ if (xfview) {
+ folder = gDBView.getFolderForViewIndex(index);
+ }
+
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder);
+ assert_view_row_count(startCount + rows);
+ let key = gDBView.getKeyAt(rows - 1);
+ if (key != msgKey) {
+ view_throw("msgKey is " + key + " but should be " + msgKey + "\n");
+ }
+ gDBView.removeTreeRows(index, rows);
+ assert_view_row_count(startCount);
+
+ // These should fail.
+ try {
+ gDBView.insertTreeRows(startCount + 10, rows, msgKey, flags, level, folder);
+ view_throw("expected exception not caught; inserting at illegal index \n");
+ } catch (ex) {}
+ try {
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder);
+ gDBView.removeTreeRows(index, gTreeView.rowCount + 10);
+ view_throw("expected exception not caught; removing illegal rows \n");
+ } catch (ex) {
+ gDBView.removeTreeRows(index, rows);
+ }
+ if (xfview) {
+ try {
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, null);
+ view_throw(
+ "expected exception not caught; folder required for xfvf view \n"
+ );
+ } catch (ex) {}
+ }
+}
+
+async function test_msg_added_to_search_view() {
+ // if the view is a non-grouped search view, test adding a header to
+ // the search results, and verify it gets put at top.
+ if (!(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort)) {
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ let [synMsg] = await make_and_add_message();
+ let msgHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(
+ synMsg.messageId
+ );
+ gDBView
+ .QueryInterface(Ci.nsIMsgSearchNotify)
+ .onSearchHit(msgHdr, msgHdr.folder);
+ assert_view_message_at_indices(synMsg, 0);
+ }
+}
+
+function IsHdrChildOf(possibleParent, possibleChild) {
+ let parentHdrId = possibleParent.messageId;
+ let numRefs = possibleChild.numReferences;
+ for (let refIndex = 0; refIndex < numRefs; refIndex++) {
+ if (parentHdrId == possibleChild.getStringReference(refIndex)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// This could be part of ensure_view_ordering() but I don't want to make that
+// function any harder to read.
+function test_threading_levels() {
+ if (!gTreeView.rowCount) {
+ do_throw("There are no rows in my folder! I can't test anything!");
+ }
+ // only look at threaded, non-grouped views.
+ if (
+ gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort ||
+ !(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay)
+ ) {
+ return;
+ }
+
+ let prevLevel = 1;
+ let prevMsgHdr;
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let msgHdr = gDBView.getMsgHdrAt(iViewIndex);
+ let level = gTreeView.getLevel(iViewIndex);
+ if (level > prevLevel && msgHdr.subject != gSiblingsMissingParentsSubject) {
+ if (!IsHdrChildOf(prevMsgHdr, msgHdr)) {
+ view_throw("indented message not child of parent");
+ }
+ }
+ prevLevel = level;
+ prevMsgHdr = msgHdr;
+ }
+}
+
+function test_expand_collapse() {
+ let oldRowCount = gDBView.rowCount;
+ let thirdChild = gDBView.getMsgHdrAt(3);
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount - 9) {
+ view_throw("collapsing first item should have removed 9 items");
+ }
+
+ // test that expand/collapse works with killed sub-thread.
+ oldRowCount = gDBView.rowCount;
+ gTestFolder.msgDatabase.markHeaderKilled(thirdChild, true, null);
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount + 2) {
+ view_throw("expanding first item should have aded 2 items");
+ }
+ gTestFolder.msgDatabase.markHeaderKilled(thirdChild, false, null);
+ oldRowCount = gDBView.rowCount;
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount - 2) {
+ view_throw("collapsing first item should have removed 2 items");
+ }
+}
+
+function test_qs_results() {
+ // This just tests that bug 505967 hasn't regressed.
+ if (gTreeView.getLevel(0) != 0) {
+ view_throw("first message should be at level 0");
+ }
+ if (gTreeView.getLevel(1) != 1) {
+ view_throw("second message should be at level 1");
+ }
+ if (gTreeView.getLevel(2) != 2) {
+ view_throw("third message should be at level 2");
+ }
+ test_threading_levels();
+}
+
+async function test_group_sort_collapseAll_expandAll_threading() {
+ // - start with an empty folder
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create a normal unthreaded view
+ setup_view("threaded", 0);
+
+ // - ensure it's empty
+ assert_view_empty();
+
+ // - add 3 messages:
+ // msg1: from A, custom column val A, to be starred
+ // msg2: from A, custom column val A
+ // msg3: from B, custom column val B
+ let [smsg1] = await make_and_add_message({ from: ["A", "A@a.invalid"] });
+ await make_and_add_message({ from: ["A", "A@a.invalid"] });
+ let [smsg3] = await make_and_add_message({ from: ["B", "B@b.invalid"] });
+
+ assert_view_row_count(3);
+ gDBView.getMsgHdrAt(0).markFlagged(true);
+ if (!gDBView.getMsgHdrAt(0).isFlagged) {
+ view_throw("Expected smsg1 to be flagged");
+ }
+
+ // - create grouped view; open folder in byFlagged AZ sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byFlagged,
+ Ci.nsMsgViewSortOrder.ascending,
+ gTestFolder
+ );
+ // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is flagged message,
+ // 3-4 are messages
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg1, 1);
+ if (!gDBView.getMsgHdrAt(1).isFlagged) {
+ view_throw("Expected grouped smsg1 to be flagged");
+ }
+ assert_view_index_is_dummy(2);
+ assert_view_index_is_not_dummy(3);
+ assert_view_index_is_not_dummy(4);
+
+ // - collapse the grouped threads; there should be 2 dummy rows
+ gDBView.doCommand(Ci.nsMsgViewCommandType.collapseAll);
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_dummy(1);
+
+ // - expand the grouped threads; there should be 5 rows
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_dummy(2);
+
+ // - reverse sort; create grouped view; open folder in byFlagged ZA sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byFlagged,
+ Ci.nsMsgViewSortOrder.descending,
+ gTestFolder
+ );
+ // - make sure there are 5 rows; index 0 and 3 are dummy, 1-2 are messages,
+ // 4 is flagged message
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_index_is_not_dummy(2);
+ assert_view_index_is_dummy(3);
+ assert_view_index_is_not_dummy(4);
+ assert_view_message_at_indices(smsg1, 4);
+ if (!gDBView.getMsgHdrAt(4).isFlagged) {
+ view_throw("Expected reverse sorted grouped smsg1 to be flagged");
+ }
+
+ // - test grouped by custom column; the custCol is first letter of author
+ // - create grouped view; open folder in byCustom ZA sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byCustom,
+ Ci.nsMsgViewSortOrder.descending,
+ gTestFolder
+ );
+
+ // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is B value message,
+ // 3-4 are messages with A value
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg3, 1);
+ if (
+ authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(1)) !=
+ "B"
+ ) {
+ view_throw(
+ "Expected grouped by custom column, ZA sortOrder smsg3 value to be B"
+ );
+ }
+ assert_view_index_is_dummy(2);
+ assert_view_index_is_not_dummy(3);
+ assert_view_index_is_not_dummy(4);
+ if (
+ authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(4)) !=
+ "A"
+ ) {
+ view_throw(
+ "Expected grouped by custom column, ZA sortOrder smsg2 value to be A"
+ );
+ }
+}
+
+async function test_group_dummies_under_mutation_by_date() {
+ // - start with an empty folder
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create the view
+ setup_view("group", Ci.nsMsgViewFlagsType.kGroupBySort);
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.ascending);
+
+ // - ensure it's empty
+ assert_view_empty();
+
+ // - add a message from this week
+ // (we want to make sure all the messages end up in the same bucket and that
+ // the current day changing as we run the test does not change buckets
+ // either. bucket 1 is same day, bucket 2 is yesterday, bucket 3 is last
+ // week, so 2 days ago or older is always last week, even if we roll over
+ // and it becomes 3 days ago.)
+ let [smsg, synSet] = await make_and_add_message({
+ age: { days: 2, hours: 1 },
+ });
+
+ // - make sure the message and a dummy appear
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg, 0, 1);
+
+ // we used to display total in tag column - make sure we don't do that.
+ if (gDBView.cellTextForColumn(0, "tags") != "") {
+ view_throw("tag column shouldn't display total count in group view");
+ }
+
+ // - move the messages to the trash
+ await messageInjection.trashMessages(synSet);
+
+ // - make sure the message and dummy disappear
+ assert_view_empty();
+
+ // - add two messages from this week (same date bucket concerns)
+ let [newer, newerSet] = await make_and_add_message({
+ age: { days: 2, hours: 1 },
+ });
+ let [older] = await make_and_add_message({ age: { days: 2, hours: 2 } });
+
+ // - sanity check addition
+ assert_view_row_count(3); // 2 messages + 1 dummy
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1, 2);
+ // the dummy should be based off the older guy
+ assert_view_message_at_indices(older, 0, 1);
+ assert_view_message_at_indices(newer, 2);
+
+ // - delete the message right under the dummy
+ // (this will be the newer one)
+ await messageInjection.trashMessages(newerSet);
+
+ // - ensure we still have the dummy and the right child node
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ // now the dummy should be based off the remaining older one
+ assert_view_message_at_indices(older, 0, 1);
+}
+
+async function test_xfvf_threading() {
+ // - start with an empty folder
+ let save_gTestFolder = gTestFolder;
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ let messages = [];
+ // Add messages such that ancestors arrive after their descendents in
+ // various interesting ways.
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 3
+ // 1
+ // 4
+ // 2
+ // 5
+ let msg3 = gMessageGenerator.makeMessage({ age: { days: 2, hours: 5 } });
+ let msg1 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 4 },
+ inReplyTo: msg3,
+ });
+ let msg4 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 3 },
+ inReplyTo: msg1,
+ });
+ let msg2 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 1 },
+ inReplyTo: msg4,
+ });
+ let msg5 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 2 },
+ inReplyTo: msg1,
+ });
+ messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create the view
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+ setup_view("xfvf", Ci.nsMsgViewFlagsType.kThreadedDisplay);
+ assert_view_row_count(5);
+ gDBView.toggleOpenState(0);
+ gDBView.toggleOpenState(0);
+
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg4, 2);
+ assert_view_message_at_indices(msg2, 3);
+ assert_view_message_at_indices(msg5, 4);
+ assert_view_level_is(0, 0);
+ assert_view_level_is(1, 1);
+ assert_view_level_is(2, 2);
+ assert_view_level_is(3, 3);
+ assert_view_level_is(4, 2);
+ gTestFolder = save_gTestFolder;
+}
+
+/*
+ * Tests the sorting order of collapsed threads, not of messages within
+ * threads. Currently limited to testing the sort-threads-by-date case,
+ * sorting both by thread root and by newest message.
+ */
+async function test_thread_sorting() {
+ let save_gTestFolder = gTestFolder;
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ let messages = [];
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 1
+ // 4
+ // 2
+ // 5
+ // 3
+ let msg1 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 10 } });
+ let msg2 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 9 } });
+ let msg3 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 8 } });
+ let msg4 = gMessageGenerator.makeMessage({
+ age: { days: 1, hours: 7 },
+ inReplyTo: msg1,
+ });
+ let msg5 = gMessageGenerator.makeMessage({
+ age: { days: 1, hours: 6 },
+ inReplyTo: msg2,
+ });
+ messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+
+ // test the non-default pref state first, so the pref gets left with its
+ // default value at the end
+ Services.prefs.setBoolPref("mailnews.sort_threads_by_root", true);
+ gDBView.open(
+ gTestFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ Ci.nsMsgViewFlagsType.kThreadedDisplay,
+ {}
+ );
+
+ assert_view_row_count(3);
+ assert_view_message_at_indices(msg1, 0);
+ assert_view_message_at_indices(msg2, 1);
+ assert_view_message_at_indices(msg3, 2);
+
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg2, 1);
+ assert_view_message_at_indices(msg1, 2);
+
+ Services.prefs.clearUserPref("mailnews.sort_threads_by_root");
+ gDBView.open(
+ gTestFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ Ci.nsMsgViewFlagsType.kThreadedDisplay,
+ {}
+ );
+
+ assert_view_row_count(3);
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg2, 2);
+
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ assert_view_message_at_indices(msg2, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg3, 2);
+
+ gDBView.close();
+ gTestFolder = save_gTestFolder;
+}
+
+const VIEW_TYPES = [
+ ["threaded", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["quicksearch", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["search", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["search", Ci.nsMsgViewFlagsType.kGroupBySort],
+ ["xfvf", Ci.nsMsgViewFlagsType.kNone],
+ // group does unspeakable things to gTestFolder, so put it last.
+ ["group", Ci.nsMsgViewFlagsType.kGroupBySort],
+];
+
+/**
+ * These are tests which are for every test configuration.
+ */
+function tests_for_all_views() {
+ test_sort_columns();
+ test_number_of_messages();
+ test_selected_messages();
+ test_insert_remove_view_rows();
+}
+
+add_setup(function () {
+ gMessages = setup_messages();
+});
+
+add_task(async function test_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[0];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for threaded.
+ test_expand_collapse();
+ await test_thread_sorting();
+});
+
+add_task(async function test_quicksearch_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[1];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for quicksearch threaded.
+ test_qs_results();
+});
+
+add_task(async function test_search_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[2];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for search threaded.
+ await test_msg_added_to_search_view();
+});
+
+add_task(async function test_search_group_by_sort() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[3];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for search group by sort.
+ await test_msg_added_to_search_view();
+});
+
+add_task(async function test_xfvf() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[4];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for xfvf.
+ await test_xfvf_threading();
+});
+
+add_task(async function test_group() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[5];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for group.
+ await test_group_sort_collapseAll_expandAll_threading;
+ await test_group_dummies_under_mutation_by_date;
+});
+
+add_task(function test_teardown() {
+ // Delete view reference to avoid a cycle leak.
+ gFakeSelection.view = null;
+});
diff --git a/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js b/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js
new file mode 100644
index 0000000000..796257a8a8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/**
+ * Test that nsMsgDBView properly reports the values of messages in the display.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var messageInjection = new MessageInjection({ mode: "local" });
+
+// This is an array of the actual test data. Each test datum is an array of two
+// elements: the first element is the argument into a simple message generator,
+// and the second element is a map of column names to expected values when
+// requesting the cell text for a given column name.
+var tests = [
+ [{ from: "John Doe <db@tinderbox.invalid>" }, { senderCol: "John Doe" }],
+ [{ from: '"Doe, John" <db@tinderbox.invalid>' }, { senderCol: "Doe, John" }],
+ // Multiple senders are indicated with 'et al.' suffix.
+ [
+ { from: "John Doe <db@tinderbox.invalid>, Sally Ann <db@null.invalid>" },
+ { senderCol: "John Doe et al." },
+ ],
+ [
+ { from: "=?UTF-8?Q?David_H=C3=A5s=C3=A4ther?= <db@null.invalid>" },
+ { senderCol: "David Håsäther" },
+ ],
+ [
+ { from: "=?UTF-8?Q?H=C3=A5s=C3=A4ther=2C_David?= <db@null.invalid>" },
+ { senderCol: "Håsäther, David" },
+ ],
+ [
+ { from: '"Håsäther, David" <db@null.invalid>' },
+ { senderCol: "Håsäther, David" },
+ ],
+ [
+ { from: "David Håsäther <db@null.invalid>" },
+ { senderCol: "David Håsäther" },
+ ],
+ [
+ {
+ from: "\xC2\xAB\xCE\xA0\xCE\x9F\xCE\x9B\xCE\x99\xCE\xA4\xCE\x97\xCE\xA3\xC2\xBB",
+ },
+ { senderCol: "«ΠΟΛΙΤΗΣ»" },
+ ],
+ [
+ {
+ from: "John Doe \xF5 <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-1" },
+ },
+ { senderCol: "John Doe õ" },
+ ],
+ [
+ {
+ from: "John Doe \xF5 <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-2" },
+ },
+ { senderCol: "John Doe Å‘" },
+ ],
+ [
+ {
+ from: "=?UTF-8?Q?H=C3=A5s=C3=A4ther=2C_David?= <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-2" },
+ },
+ { senderCol: "Håsäther, David" },
+ ],
+];
+
+add_task(async function test_nsMsgDBView_headValues() {
+ // Add the messages to the folder
+ let msgGenerator = new MessageGenerator();
+ let genMessages = tests.map(data => msgGenerator.makeMessage(data[0]));
+ let folder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders(
+ [folder],
+ [new SyntheticMessageSet(genMessages)]
+ );
+
+ // Make the DB view
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=threaded";
+ let dbView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ dbView.init(null, null, null);
+ let outCount = {};
+ dbView.open(
+ folder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ 0,
+ outCount
+ );
+
+ // Did we add all the messages properly?
+ let treeView = dbView.QueryInterface(Ci.nsITreeView);
+ Assert.equal(treeView.rowCount, tests.length);
+
+ // For each test, make sure that the display is correct.
+ tests.forEach(function (data, i) {
+ info("Checking data for " + uneval(data));
+ let expected = data[1];
+ for (let column in expected) {
+ Assert.equal(dbView.cellTextForColumn(i, column), expected[column]);
+ }
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js
new file mode 100644
index 0000000000..61c1cf3fe9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgMailSession functions relating to alerts and their
+ * listeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var gDialogTitle = null;
+var gText = null;
+
+function reset() {
+ gDialogTitle = null;
+ gText = null;
+}
+
+/* exported alert */
+// Used in alertTestUtils.
+function alert(aDialogTitle, aText) {
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ gDialogTitle = aDialogTitle;
+ gText = aText;
+}
+
+var msgWindow = {
+ get promptDialog() {
+ return alertUtilsPrompts;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgWindow"]),
+};
+
+var msgUrl = {
+ _msgWindow: null,
+
+ get msgWindow() {
+ return this._msgWindow;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgMailNewsUrl"]),
+};
+
+function alertListener() {}
+
+alertListener.prototype = {
+ mReturn: false,
+ mMessage: null,
+ mMsgWindow: null,
+
+ reset() {
+ this.mMessage = null;
+ this.mMsgWindow = null;
+ },
+
+ onAlert(aMessage, aMsgWindow) {
+ Assert.equal(this.mMessage, null);
+ Assert.equal(this.mMsgWindow, null);
+
+ this.mMessage = aMessage;
+ this.mMsgWindow = aMsgWindow;
+
+ return this.mReturn;
+ },
+ QueryInferface: ChromeUtils.generateQI([Ci.nsIMsgMailNewsUrl]),
+};
+
+function run_test() {
+ // Test - No listeners, check alert tries to alert the user.
+
+ reset();
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("test message", msgUrl);
+
+ // The dialog title doesn't get set at the moment.
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "test message");
+
+ // Test - No listeners and no msgWindow, check no alerts.
+
+ reset();
+
+ msgUrl._msgWindow = null;
+
+ MailServices.mailSession.alertUser("test no message", msgUrl);
+
+ // The dialog title doesn't get set at the moment.
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ // Test - One listener, returning false (prompt should still happen).
+
+ reset();
+
+ var listener1 = new alertListener();
+ listener1.mReturn = false;
+
+ MailServices.mailSession.addUserFeedbackListener(listener1);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("message test", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "message test");
+
+ Assert.equal(listener1.mMessage, "message test");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ // Test - One listener, returning false, no msg window (prompt shouldn't
+ // happen).
+
+ reset();
+ listener1.reset();
+
+ MailServices.mailSession.alertUser("message test no prompt", null);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, "message test no prompt");
+ Assert.equal(listener1.mMsgWindow, null);
+
+ // Test - Two listeners, both returning false (prompt should happen).
+
+ reset();
+ listener1.reset();
+
+ var listener2 = new alertListener();
+ listener2.mReturn = false;
+
+ MailServices.mailSession.addUserFeedbackListener(listener2);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("two listeners", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "two listeners");
+
+ Assert.equal(listener1.mMessage, "two listeners");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "two listeners");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Two listeners, one returning true (prompt shouldn't happen).
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ listener2.mReturn = true;
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("no prompt", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, "no prompt");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "no prompt");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Remove a listener.
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ MailServices.mailSession.removeUserFeedbackListener(listener1);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("remove listener", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, null);
+ Assert.equal(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "remove listener");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Remove the other listener.
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ MailServices.mailSession.removeUserFeedbackListener(listener2);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("no listeners", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "no listeners");
+
+ Assert.equal(listener1.mMessage, null);
+ Assert.equal(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, null);
+ Assert.equal(listener2.mMsgWindow, null);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js
new file mode 100644
index 0000000000..e6fa785d79
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgMailSession functions relating to listeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var numListenerFunctions = 8;
+
+// The MailSession also implements nsIFolderListener - used to relay
+// notifications onward to all the registered listeners.
+var gMailSessionNotifier = MailServices.mailSession.QueryInterface(
+ Ci.nsIFolderListener
+);
+
+var gFLAll;
+var gFLSingle = new Array(numListenerFunctions);
+
+function fL() {}
+
+fL.prototype = {
+ mReceived: 0,
+ mAutoRemoveItem: false,
+
+ onFolderAdded(parentFolder, child) {
+ this.mReceived |= Ci.nsIFolderListener.added;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onMessageAdded(parentFolder, msg) {
+ this.mReceived |= Ci.nsIFolderListener.added;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderRemoved(parentFolder, child) {
+ this.mReceived |= Ci.nsIFolderListener.removed;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onMessageRemoved(parentFolder, msg) {
+ this.mReceived |= Ci.nsIFolderListener.removed;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.propertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderIntPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.intPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderBoolPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.boolPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderUnicharPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.unicharPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderPropertyFlagChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.propertyFlagChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderEvent(parentItem, item) {
+ this.mReceived |= Ci.nsIFolderListener.event;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+};
+
+function NotifyMailSession() {
+ gMailSessionNotifier.onFolderAdded(null, null);
+ gMailSessionNotifier.onMessageAdded(null, null);
+ gMailSessionNotifier.onFolderRemoved(null, null);
+ gMailSessionNotifier.onMessageRemoved(null, null);
+ gMailSessionNotifier.onFolderPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderIntPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderBoolPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderUnicharPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderPropertyFlagChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderEvent(null, null);
+}
+
+function run_test() {
+ var i;
+
+ Assert.ok(MailServices.mailSession != null);
+
+ // Test - Add a listener
+
+ gFLAll = new fL();
+
+ MailServices.mailSession.AddFolderListener(gFLAll, Ci.nsIFolderListener.all);
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ gFLSingle[i] = new fL();
+ MailServices.mailSession.AddFolderListener(gFLSingle[i], Math.pow(2, i));
+ }
+
+ // Test - Notify listener on all available items
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, Math.pow(2, i));
+ gFLSingle[i].mReceived = 0;
+
+ // And prepare for test 3.
+ gFLSingle[i].mAutoRemoveItem = true;
+ }
+
+ // Test - Remove Single Listeners as we go through the functions
+
+ // Check the for loop above for changes to the single listeners.
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, Math.pow(2, i));
+ gFLSingle[i].mReceived = 0;
+ }
+
+ // Test - Ensure the single listeners have been removed.
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, 0);
+ }
+
+ // Test - Remove main listener
+
+ MailServices.mailSession.RemoveFolderListener(gFLAll);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgTraitService.js b/comm/mailnews/base/test/unit/test_nsMsgTraitService.js
new file mode 100644
index 0000000000..f75b76ada6
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgTraitService.js
@@ -0,0 +1,130 @@
+/* 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 ts = Cc["@mozilla.org/msg-trait-service;1"].getService(
+ Ci.nsIMsgTraitService
+);
+
+// junk-related traits set by default
+var kJunkId = "mailnews@mozilla.org#junk";
+var kGoodId = "mailnews@mozilla.org#good";
+var kGoodIndex = Ci.nsIJunkMailPlugin.GOOD_TRAIT;
+var kJunkIndex = Ci.nsIJunkMailPlugin.JUNK_TRAIT;
+
+// a dummy set of traits
+var proId = "TheProTrait";
+var proName = "ProName";
+var antiId = "TheAntiTrait";
+
+function run_test() {
+ // Check lastIndex prior to adding, 3 - 1000 are reserved for mailnews
+ Assert.equal(ts.lastIndex, 1000);
+
+ // basic junk as traits should be setup automatically
+ Assert.equal(
+ kGoodId,
+ Services.prefs.getCharPref("mailnews.traits.id." + kGoodIndex)
+ );
+ Assert.equal(
+ kJunkId,
+ Services.prefs.getCharPref("mailnews.traits.id." + kJunkIndex)
+ );
+ Assert.equal(
+ kGoodId,
+ Services.prefs.getCharPref("mailnews.traits.antiId." + kJunkIndex)
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref("mailnews.traits.enabled." + kJunkIndex)
+ );
+
+ // add the pro and anti test traits
+ Assert.ok(!ts.isRegistered(proId));
+ var proIndex = ts.registerTrait(proId);
+ Assert.ok(ts.isRegistered(proId));
+ Assert.equal(proIndex, 1001);
+ Assert.equal(proIndex, ts.getIndex(proId));
+ Assert.equal(proId, ts.getId(proIndex));
+ var antiIndex = ts.registerTrait(antiId);
+ Assert.equal(proIndex, 1001);
+ Assert.equal(antiIndex, 1002);
+
+ // check setting and getting things through the service
+ ts.setName(proId, proName);
+ Assert.equal(proName, ts.getName(proId));
+ Assert.ok(!ts.getEnabled(proId));
+ ts.setEnabled(proId, true);
+ Assert.ok(ts.getEnabled(proId));
+ ts.setAntiId(proId, antiId);
+ Assert.equal(antiId, ts.getAntiId(proId));
+ let proArray = ts.getEnabledProIndices();
+ let antiArray = ts.getEnabledAntiIndices();
+ Assert.equal(proArray.length, 2);
+ Assert.equal(antiArray.length, 2);
+ Assert.equal(proArray[1], proIndex);
+ Assert.equal(antiArray[1], antiIndex);
+
+ // check of aliases
+ // add three random aliases
+ ts.addAlias(1, 501);
+ ts.addAlias(1, 502);
+ ts.addAlias(1, 601);
+ let aliases = ts.getAliases(1);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 502);
+ Assert.equal(aliases[2], 601);
+
+ // remove the middle one
+ ts.removeAlias(1, 502);
+ aliases = ts.getAliases(1);
+ Assert.equal(aliases.length, 2);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 601);
+
+ // try to add an existing value
+ ts.addAlias(1, 501);
+ aliases = ts.getAliases(1);
+ Assert.equal(aliases.length, 2);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 601);
+
+ // now let's make sure this got saved in preferences
+ Assert.equal(
+ proId,
+ Services.prefs.getCharPref("mailnews.traits.id." + proIndex)
+ );
+ Assert.equal(
+ proName,
+ Services.prefs.getCharPref("mailnews.traits.name." + proIndex)
+ );
+ Assert.ok(Services.prefs.getBoolPref("mailnews.traits.enabled." + proIndex));
+ Assert.equal(
+ antiId,
+ Services.prefs.getCharPref("mailnews.traits.antiId." + proIndex)
+ );
+
+ // remove the pro trait
+ ts.unRegisterTrait(proId);
+ Assert.ok(!ts.isRegistered(proId));
+
+ // check that this is also removed from prefs. The get calls should fail
+ try {
+ Services.prefs.getCharPref("mailnews.traits.id." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getCharPref("mailnews.traits.name." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getBoolPref("mailnews.traits.enabled." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getCharPref("mailnews.traits.antiId." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+}
diff --git a/comm/mailnews/base/test/unit/test_postPluginFilters.js b/comm/mailnews/base/test/unit/test_postPluginFilters.js
new file mode 100644
index 0000000000..de75307886
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_postPluginFilters.js
@@ -0,0 +1,223 @@
+/* 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/. */
+
+/*
+ * tests post-plugin message filters as implemented in bug 198100
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+// Globals
+
+var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+);
+
+// command functions for test data
+var kTrain = 0; // train a file as a trait
+var kClass = 1; // classify files with traits
+
+var gTest; // currently active test
+var gMsgHdr; // current message header
+
+var kJunkFile = "../../../data/bugmail1";
+var kGoodFile = "../../../data/draft1";
+
+var kPriorityLow = 3;
+var kPriorityHigh = 5;
+var gInboxListener; // database listener object
+
+var gTests = [
+ {
+ // train two different messages
+ command: kTrain,
+ fileName: kGoodFile,
+ traitId: MailServices.junk.GOOD_TRAIT,
+ },
+ {
+ command: kTrain,
+ fileName: kJunkFile,
+ traitId: MailServices.junk.JUNK_TRAIT,
+ },
+ {
+ // test a filter that acts on GOOD messages
+ command: kClass,
+ fileName: kGoodFile,
+ test() {
+ Assert.equal(kPriorityHigh, gMsgHdr.priority);
+ },
+ },
+ {
+ // test a filter that acts on JUNK messages
+ command: kClass,
+ fileName: kJunkFile,
+ test() {
+ Assert.equal(kPriorityLow, gMsgHdr.priority);
+ },
+ },
+];
+
+// main test
+function run_test() {
+ // Setup some incoming filters, setting junk priority low, and good high.
+
+ // Can't use the fake server, must use the deferredTo local server!
+ let filterList = localAccountUtils.incomingServer.getFilterList(null);
+
+ // junkIsLow filter
+ let filter = filterList.createFilter("junkIsLow");
+ let searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ let value = searchTerm.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value.junkStatus = MailServices.junk.JUNK;
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ searchTerm.booleanAnd = false;
+ filter.appendTerm(searchTerm);
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.ChangePriority;
+ action.priority = kPriorityLow;
+ filter.appendAction(action);
+ filter.filterType = Ci.nsMsgFilterType.PostPlugin;
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+
+ // goodIsHigh filter
+ filter = filterList.createFilter("goodIsHigh");
+ searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value = searchTerm.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value.junkStatus = MailServices.junk.GOOD;
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ searchTerm.booleanAnd = false;
+ filter.appendTerm(searchTerm);
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.ChangePriority;
+ action.priority = kPriorityHigh;
+ filter.appendAction(action);
+ filter.filterType = Ci.nsMsgFilterType.PostPlugin;
+ filter.enabled = true;
+ filterList.insertFilterAt(1, filter);
+
+ // setup a db listener to grab the message headers. There's probably an
+ // easier way, but this works.
+ gInboxListener = new DBListener();
+ gDbService.registerPendingListener(
+ localAccountUtils.inboxFolder,
+ gInboxListener
+ );
+
+ do_test_pending();
+
+ startCommand();
+}
+
+function endTest() {
+ // Cleanup
+ dump(" Exiting mail tests\n");
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+
+ gPOP3Pump = null;
+
+ do_test_finished(); // for the one in run_test()
+}
+
+var classifyListener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ // print("Message URI is " + aMsgURI);
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+
+ startCommand();
+ },
+};
+
+/** @implements {nsIDBChangeListener} */
+function DBListener() {}
+
+DBListener.prototype = {
+ onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) {},
+
+ onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) {},
+
+ onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) {
+ gMsgHdr = aHdrChanged;
+ },
+
+ onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) {},
+
+ onAnnouncerGoingAway(instigator) {
+ if (gInboxListener) {
+ try {
+ POP3Pump.inbox.msgDatabase.removeListener(gInboxListener);
+ } catch (e) {
+ dump("listener not found\n");
+ }
+ }
+ },
+
+ onReadChanged(aInstigator) {},
+
+ onJunkScoreChanged(aInstigator) {},
+
+ onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) {},
+ onEvent(aDB, aEvent) {},
+};
+
+// start the next test command
+function startCommand() {
+ if (gTest && gTest.test) {
+ dump("doing test " + gTest.test.name + "\n");
+ gTest.test();
+ }
+ if (!gTests.length) {
+ // Do we have more commands?
+ // no, all done
+ endTest();
+ return;
+ }
+
+ gTest = gTests.shift();
+ switch (gTest.command) {
+ case kTrain:
+ // train message
+ var proArray = [];
+ proArray.push(gTest.traitId);
+
+ MailServices.junk.setMsgTraitClassification(
+ getSpec(gTest.fileName), // aMsgURI
+ [], // aOldTraits
+ proArray, // aNewTraits
+ classifyListener // aTraitListener
+ );
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ break;
+
+ case kClass:
+ // classify message
+ gPOP3Pump.files = [gTest.fileName];
+ gPOP3Pump.onDone = function () {
+ do_timeout(100, startCommand);
+ };
+ gPOP3Pump.run();
+ break;
+ }
+}
+
+function getSpec(aFileName) {
+ var file = do_get_file(aFileName);
+ var uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIURL);
+ uri = uri.mutate().setQuery("type=application/x-message-display").finalize();
+ return uri.spec;
+}
diff --git a/comm/mailnews/base/test/unit/test_retention.js b/comm/mailnews/base/test/unit/test_retention.js
new file mode 100644
index 0000000000..38038015b1
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_retention.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+/*
+ * Simple tests for retention settings. In particular, we'd like to make
+ * sure that applying retention settings works with the new code that avoids
+ * opening db's to apply retention settings if the folder doesn't override
+ * the server defaults.
+ */
+
+var { MessageGenerator, MessageScenarioFactory, SyntheticMessageSet } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var gMessageGenerator = new MessageGenerator();
+var gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
+var messageInjection = new MessageInjection({ mode: "local" });
+
+var gTestFolder;
+
+add_setup(async function () {
+ // Add 10 messages
+ let messages = [];
+ messages = messages.concat(gScenarioFactory.directReply(10));
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+});
+
+add_task(function test_retention() {
+ let numMessages = 10;
+ gTestFolder.msgDatabase = null;
+ gTestFolder.applyRetentionSettings();
+ const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+ // adding messages leaves some headers around as garbage - make sure
+ // those are cleaned up so the db will get closed.
+ Cu.forceGC();
+ Cu.forceCC();
+ Assert.equal(gDbService.cachedDBForFolder(gTestFolder), null);
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(numMessages, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+ let serverSettings = gTestFolder.server.retentionSettings;
+ serverSettings.retainByPreference =
+ Ci.nsIMsgRetentionSettings.nsMsgRetainByNumHeaders;
+ serverSettings.numHeadersToKeep = 9;
+ gTestFolder.server.retentionSettings = serverSettings;
+ gTestFolder.applyRetentionSettings();
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(9, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+ let folderSettings = gTestFolder.retentionSettings;
+ folderSettings.retainByPreference =
+ Ci.nsIMsgRetentionSettings.nsMsgRetainByNumHeaders;
+ folderSettings.numHeadersToKeep = 8;
+ folderSettings.useServerDefaults = false;
+ gTestFolder.retentionSettings = folderSettings;
+ gTestFolder.applyRetentionSettings();
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(8, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+});
diff --git a/comm/mailnews/base/test/unit/test_saveAs.js b/comm/mailnews/base/test/unit/test_saveAs.js
new file mode 100644
index 0000000000..39263d3854
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_saveAs.js
@@ -0,0 +1,172 @@
+/* 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/. */
+
+/**
+ * Invokes nsMessenger::saveAs with the bypass of the File Picker to check if
+ * the saved file as .eml .html or .txt contains certain strings (header, body, ...).
+ *
+ * See `checkedContent` for the compared strings.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+var { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+setupIMAPPump();
+
+registerCleanupFunction(function () {
+ teardownIMAPPump();
+});
+
+/**
+ * Creates a SyntheticMessage and prepares it for loading it
+ * into a fake IMAP inbox.
+ *
+ * @returns {object[]}
+ * [0] is an {ImapMessage}
+ * [1] is an {SyntheticMessage}
+ */
+async function createMessage() {
+ let gMessageGenerator = new MessageGenerator();
+ let synthMessage = gMessageGenerator.makeMessage();
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(synthMessage.toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ let ImapMessageFromSynthMsg = new ImapMessage(
+ msgURI.spec,
+ imapInbox.uidnext++,
+ []
+ );
+ return [ImapMessageFromSynthMsg, synthMessage];
+}
+
+/**
+ * Adds a fake msg to a fake IMAP.
+ *
+ * @param {ImapMessage} fooMessage
+ */
+async function addImapMessage(fooMessage) {
+ IMAPPump.mailbox.addMessage(fooMessage);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+}
+
+/**
+ * Saves the file and waits until its able to load.
+ * nsMessenger doesn't have an event for the loaded File.
+ *
+ * @param {string} fileEnding
+ * @returns {string}
+ */
+async function saveAndLoad(fileEnding) {
+ // getTempFile guarantees that the file doesn't exist.
+ let tmpFile = FileTestUtils.getTempFile(`someprefix${fileEnding}`);
+ Assert.ok(
+ tmpFile.path.endsWith(fileEnding),
+ "Sanity check if the file ending is intact"
+ );
+
+ // Get the ImapMessage.
+ let hdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let uri = IMAPPump.inbox.getUriForMsg(hdr);
+
+ // nsMessenger::saveAs
+ messenger.saveAs(uri, true, null, tmpFile.path, true);
+ info(`File saved at ${tmpFile.path}`);
+
+ await TestUtils.waitForCondition(
+ () => IOUtils.exists(tmpFile.path),
+ "wait for nsMessenger::saveAs file exists"
+ );
+ let fileContent = await IOUtils.readUTF8(tmpFile.path);
+ return fileContent;
+}
+
+/**
+ * All of these strings must appear in the saved file.
+ *
+ * @param {SyntheticMessage} synthMessage
+ * This message is the original message.
+ * @returns {object}
+ */
+function checkedContent(synthMessage) {
+ return {
+ // Skip dateCheck because of Formatting and Timezone.
+ // date: synthMessage.date.toString(),
+ fromName: synthMessage.from[0],
+ fromEmail: synthMessage.from[1],
+ subject: synthMessage.subject,
+ toName: synthMessage.toName,
+ toAddress: synthMessage.toAddress,
+ body: synthMessage.bodyPart.body,
+ };
+}
+
+async function emlTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".eml");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .eml should contain ${msgPart}`
+ );
+ }
+}
+
+async function htmlTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".html");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .html should contain ${msgPart}`
+ );
+ }
+}
+
+async function txtTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".txt");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .txt should contain ${msgPart}`
+ );
+ }
+}
+
+add_task(async function test_saveAs() {
+ let [fakedImapMessage, synthMessage] = await createMessage();
+ await addImapMessage(fakedImapMessage);
+ await emlTest(synthMessage);
+ await txtTest(synthMessage);
+ await htmlTest(synthMessage);
+});
diff --git a/comm/mailnews/base/test/unit/test_testsuite_base64.js b/comm/mailnews/base/test/unit/test_testsuite_base64.js
new file mode 100644
index 0000000000..a6faae3640
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_base64.js
@@ -0,0 +1,22 @@
+/**
+ * Tests functions atob() and btoa() in mailnews/test/resources/MailTestUtils.jsm .
+ *
+ * Note:
+ * btoa() = base64 encode
+ * atob() = base64 decode
+ * (i.e. "binary" = plain, and "ascii" = encoded)
+ */
+
+function run_test() {
+ var plain = "testtesttest";
+ var encoded = "dGVzdHRlc3R0ZXN0";
+
+ // correct encoding according to spec
+ Assert.equal(btoa(plain), encoded); // encode
+ Assert.equal(atob(encoded), plain); // decode
+
+ // roundtrip works
+ Assert.equal(atob(btoa(plain)), plain);
+ Assert.equal(btoa(atob(encoded)), encoded);
+ return true;
+}
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js
new file mode 100644
index 0000000000..02181ae20b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js
@@ -0,0 +1,58 @@
+/**
+ * Tests functions in mailnews/test/fakeserver/Auth.jsm
+ * which are responsible for the authentication in the
+ * fakeserver.
+ *
+ * Do NOT essentially re-code the auth schemes here,
+ * just check roundtrips, against static values etc..
+ */
+
+var { AuthPLAIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+var kUsername = "fred1";
+var kPassword = "wilma2";
+
+function run_test() {
+ authPLAIN();
+ authCRAMMD5();
+ return true;
+}
+
+/**
+ * Test AUTH PLAIN
+ */
+function authPLAIN() {
+ // roundtrip works
+ var line = AuthPLAIN.encodeLine(kUsername, kPassword);
+ var req = AuthPLAIN.decodeLine(line);
+ Assert.equal(req.username, kUsername);
+ Assert.equal(req.password, kPassword);
+
+ // correct encoding
+ Assert.equal(line, "AGZyZWQxAHdpbG1hMg==");
+}
+
+/**
+ * Test AUTH CRAM-MD5
+ */
+function authCRAMMD5() {
+ // AuthCRAM.createChallenge() creates a different challenge each time
+ var hardcodedChallenge = btoa("<123@fake.invalid>");
+ var hardcodedResponse =
+ "ZnJlZDEgOTA5YjgwMmM3NTI5NTJlYzI2NjgyMTNmYTdjNWU0ZjQ=";
+
+ // correct encoding
+ var req = AuthCRAM.decodeLine(hardcodedResponse);
+ Assert.equal(req.username, kUsername);
+ var expectedDigest = AuthCRAM.encodeCRAMMD5(hardcodedChallenge, kPassword);
+ Assert.equal(req.digest, expectedDigest);
+
+ var challenge = AuthCRAM.createChallenge("fake.invalid");
+ challenge = atob(challenge); // decode. function currently returns it already encoded
+ var challengeSplit = challenge.split("@");
+ Assert.equal(challengeSplit.length, 2);
+ Assert.equal(challengeSplit[1], "fake.invalid>");
+ Assert.equal(challengeSplit[0][0], "<");
+}
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js
new file mode 100644
index 0000000000..a9eb9ae2d7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+// Test that Imapd.jsm fakeserver correctly emulates GMail server
+// That means X-GM-EXT-1 capability and GMail flavor XLIST
+// per https://developers.google.com/google-apps/gmail/imap_extensions
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+setupIMAPPump("GMail");
+// create our own handler so that we can call imapd functions directly
+var handler;
+
+add_setup(function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// mbox mailboxes cannot contain both child mailboxes and messages, so this will
+// be one test case.
+add_setup(async function () {
+ IMAPPump.mailbox.specialUseFlag = "\\Inbox";
+ IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\Noselect"] });
+ IMAPPump.daemon.createMailbox("[Gmail]/All Mail", {
+ specialUseFlag: "\\AllMail",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Drafts", {
+ specialUseFlag: "\\Drafts",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Sent", { specialUseFlag: "\\Sent" });
+ IMAPPump.daemon.createMailbox("[Gmail]/Spam", { specialUseFlag: "\\Spam" });
+ IMAPPump.daemon.createMailbox("[Gmail]/Starred", {
+ specialUseFlag: "\\Starred",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Trash", { specialUseFlag: "\\Trash" });
+ IMAPPump.daemon.createMailbox("test", {});
+
+ handler = IMAPPump.server._handlerCreator(IMAPPump.daemon);
+ let response = handler.onError("1", "LOGIN user password");
+ Assert.ok(response.includes("OK"));
+ // wait for imap pump to do its thing or else we get memory leaks
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// test that 'XLIST "" "*"' returns the proper responses
+add_task(function testXlist() {
+ let response = handler.onError("2", 'XLIST "" "*"');
+
+ Assert.ok(response.includes('* LIST (\\HasNoChildren \\Inbox) "/" "INBOX"'));
+ Assert.ok(
+ response.includes('* LIST (\\Noselect \\HasChildren) "/" "[Gmail]"')
+ );
+ Assert.ok(
+ response.includes(
+ '* LIST (\\HasNoChildren \\AllMail) "/" "[Gmail]/All Mail"'
+ )
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Drafts) "/" "[Gmail]/Drafts"')
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Sent) "/" "[Gmail]/Sent"')
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Spam) "/" "[Gmail]/Spam"')
+ );
+ Assert.ok(
+ response.includes(
+ '* LIST (\\HasNoChildren \\Starred) "/" "[Gmail]/Starred"'
+ )
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Trash) "/" "[Gmail]/Trash"')
+ );
+ Assert.ok(response.includes('* LIST (\\HasNoChildren) "/" "test"'));
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js
new file mode 100644
index 0000000000..89214b2b51
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js
@@ -0,0 +1,150 @@
+/* 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/. */
+
+// Test that Imapd.jsm fakeserver correctly implements LIST-EXTENDED imap
+// extension (RFC 5258 - http://tools.ietf.org/html/rfc5258)
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Globals
+
+// Dovecot is one of the servers that supports LIST-EXTENDED
+setupIMAPPump("Dovecot");
+// create our own handler so that we can call imapd functions directly
+var handler;
+
+add_setup(function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// mbox mailboxes cannot contain both child mailboxes and messages, so this will
+// be one test case.
+add_setup(async function () {
+ IMAPPump.mailbox.flags = ["\\Marked", "\\NoInferiors"];
+ IMAPPump.mailbox.subscribed = true;
+ IMAPPump.daemon.createMailbox("Fruit", {});
+ IMAPPump.daemon.createMailbox("Fruit/Apple", {});
+ IMAPPump.daemon.createMailbox("Fruit/Banana", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Fruit/Peach", {
+ nonExistent: true,
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("Tofu", {});
+ IMAPPump.daemon.createMailbox("Vegetable", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Vegetable/Broccoli", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Vegetable/Corn", {});
+
+ handler = IMAPPump.server._handlerCreator(IMAPPump.daemon);
+ let response = handler.onError("1", "LOGIN user password");
+ Assert.ok(response.includes("OK"));
+ // wait for imap pump to do it's thing or else we get memory leaks
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// test that 'LIST "" "*"' returns the proper responses (standard LIST usage)
+add_task(function testList() {
+ let response = handler.onError("2", 'LIST "" "*"');
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Apple"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Banana"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Broccoli"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// test that 'LIST (SUBSCRIBED) "" "*"' returns the proper responses
+add_task(function testListSelectSubscribed() {
+ let response = handler.onError("3", 'LIST (SUBSCRIBED) "" "*"');
+
+ Assert.ok(
+ response.includes(
+ '* LIST (\\Marked \\NoInferiors \\Subscribed) "/" "INBOX"'
+ )
+ );
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Fruit/Banana"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed \\NonExistent) "/" "Fruit/Peach"')
+ );
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Vegetable"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed) "/" "Vegetable/Broccoli"')
+ );
+ Assert.ok(!response.includes('"Fruit"'));
+ Assert.ok(!response.includes("Apple"));
+ Assert.ok(!response.includes("Tofu"));
+ Assert.ok(!response.includes("Corn"));
+});
+
+// test that 'LIST "" "%" RETURN (CHILDEREN)' returns the proper responses
+add_task(function testListReturnChilderen() {
+ let response = handler.onError("4", 'LIST "" "%" RETURN (CHILDREN)');
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST (\\HasChildren) "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST (\\HasNoChildren) "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST (\\HasChildren) "/" "Vegetable"'));
+ Assert.ok(!response.includes("Apple"));
+ Assert.ok(!response.includes("Banana"));
+ Assert.ok(!response.includes("Peach"));
+ Assert.ok(!response.includes("Broccoli"));
+ Assert.ok(!response.includes("Corn"));
+});
+
+// test that 'LIST "" "*" RETURN (SUBSCRIBED)' returns the proper responses
+add_task(function testListReturnSubscribed() {
+ let response = handler.onError("5", 'LIST "" "*" RETURN (SUBSCRIBED)');
+
+ Assert.ok(
+ response.includes(
+ '* LIST (\\Marked \\NoInferiors \\Subscribed) "/" "INBOX"'
+ )
+ );
+ Assert.ok(response.includes('* LIST () "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Apple"'));
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Fruit/Banana"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Vegetable"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed) "/" "Vegetable/Broccoli"')
+ );
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// test that 'LIST "" ("INBOX" "Tofu" "Vegetable/%")' returns the proper responses
+add_task(function testListSelectMultiple() {
+ let response = handler._dispatchCommand("LIST", [
+ "",
+ '("INBOX" "Tofu" "Vegetable/%")',
+ ]);
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Broccoli"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes('"Vegetable"'));
+ Assert.ok(!response.includes("Fruit"));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ handler = null;
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/base/test/unit/test_viewSortByAddresses.js b/comm/mailnews/base/test/unit/test_viewSortByAddresses.js
new file mode 100644
index 0000000000..7235723bfd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_viewSortByAddresses.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/*
+ * Attempt to test nsMsgDBView's handling of sorting by sender/recipients
+ * when using a display name from the address book.
+ */
+
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { dump_view_contents } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ViewHelpers.jsm"
+);
+
+var gMessageGenerator = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+
+Services.prefs.setBoolPref("mail.showCondensedAddresses", true);
+
+var gTestFolder;
+
+// Setup the display name to be opposite of alphabetic order of e-mail address.
+var cards = [
+ { email: "aaa@b.invalid", displayName: "4" },
+ { email: "ccc@d.invalid", displayName: "3" },
+ { email: "eee@f.invalid", displayName: "2" },
+ { email: "ggg@h.invalid", displayName: "1" },
+];
+
+add_setup(async function () {
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+
+ ab.addCard(card);
+ }
+
+ // Add address to addressbook so we can set display name and verify that
+ // the view uses the display name for display and sorting.
+ cards.forEach(createAndAddCard);
+
+ // build up a couple message with addresses in the ab.
+ let messages = [];
+ messages = messages.concat(
+ gMessageGenerator.makeMessage({
+ from: ["aaa", "aaa@b.invalid"],
+ to: [["ccc", "ccc@d.invalid"]],
+ })
+ );
+ messages = messages.concat(
+ gMessageGenerator.makeMessage({
+ from: ["eee", "eee@f.invalid"],
+ to: [["ggg", "ggg@h.invalid"]],
+ })
+ );
+
+ let msgSet = new SyntheticMessageSet(messages);
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+});
+
+add_task(function test_view_sort_by_addresses() {
+ // - create the view
+ setup_view("threaded", Ci.nsMsgViewFlagsType.kNone);
+ // Check that sorting by sender uses the display name
+ gDBView.sort(Ci.nsMsgViewSortType.byAuthor, Ci.nsMsgViewSortOrder.ascending);
+ let sender1 = gDBView.cellTextForColumn(0, "senderCol");
+ let sender2 = gDBView.cellTextForColumn(1, "senderCol");
+
+ if (sender1 != 2) {
+ view_throw("expected sender 1 to be 2");
+ }
+ if (sender2 != 4) {
+ view_throw("expected sender 2 to be 4");
+ }
+
+ gDBView.sort(
+ Ci.nsMsgViewSortType.byRecipient,
+ Ci.nsMsgViewSortOrder.ascending
+ );
+ let recip1 = gDBView.cellTextForColumn(0, "recipientCol");
+ let recip2 = gDBView.cellTextForColumn(1, "recipientCol");
+
+ if (recip1 != 1) {
+ view_throw("expected recip 1 to be 1");
+ }
+ if (recip2 != 3) {
+ view_throw("expected recip 2 to be 3");
+ }
+});
+
+function view_throw(why) {
+ dump_view_contents();
+ do_throw(why);
+}
+var gDBView;
+var gTreeView;
+
+function setup_view(aViewType, aViewFlags, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + aViewType;
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // always start out fully expanded
+ aViewFlags |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ aViewType != "search" ? aTestFolder : null,
+ Ci.nsMsgViewSortType.byDate,
+ aViewType != "search"
+ ? Ci.nsMsgViewSortOrder.ascending
+ : Ci.nsMsgViewSortOrder.descending,
+ aViewFlags,
+ outCount
+ );
+ dump(" View Out Count: " + outCount.value + "\n");
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+}
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders1.js b/comm/mailnews/base/test/unit/test_virtualFolders1.js
new file mode 100644
index 0000000000..5fe32de237
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders1.js
@@ -0,0 +1,205 @@
+/* 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/. */
+
+/**
+ * Tests that subfolders added to searched folders are also searched.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+add_task(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ let rootFolder = account.incomingServer.rootFolder;
+
+ // Create some real folders to test.
+ rootFolder.createSubfolder("test A", null);
+ let testFolderA = rootFolder.getChildNamed("test A");
+ rootFolder.createSubfolder("test B", null);
+ let testFolderB = rootFolder.getChildNamed("test B");
+ rootFolder.createSubfolder("test C", null);
+ let testFolderC = rootFolder.getChildNamed("test C");
+
+ // Create a virtual folder with no search folders.
+ let wrappedFolderZ = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual Z",
+ rootFolder,
+ [],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderZ.virtualFolder,
+ rootFolder.getChildNamed("virtual Z")
+ );
+ Assert.equal(wrappedFolderZ.searchFolderURIs, "");
+ Assert.deepEqual(wrappedFolderZ.searchFolders, []);
+
+ // Create a virtual folder with one search folder.
+ let wrappedFolderY = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual Y",
+ rootFolder,
+ [testFolderA],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderY.virtualFolder,
+ rootFolder.getChildNamed("virtual Y")
+ );
+ Assert.equal(wrappedFolderY.searchFolderURIs, testFolderA.URI);
+ Assert.deepEqual(wrappedFolderY.searchFolders, [testFolderA]);
+
+ // Create a virtual folder with two search folders.
+ let wrappedFolderX = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual X",
+ rootFolder,
+ [testFolderB, testFolderC],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderX.virtualFolder,
+ rootFolder.getChildNamed("virtual X")
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [testFolderB, testFolderC]);
+
+ // Add a subfolder to real folder B. Check it is added to virtual folder X.
+ testFolderB.createSubfolder("test BB", null);
+ let testFolderBB = testFolderB.getChildNamed("test BB");
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}|${testFolderBB.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBB,
+ testFolderC,
+ ]);
+
+ // Add a subfolder to real folder BB. Check it is added to virtual folder X.
+ testFolderBB.createSubfolder("test BBB", null);
+ let testFolderBBB = testFolderBB.getChildNamed("test BBB");
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}|${testFolderBB.URI}|${testFolderBBB.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+
+ // Remove BB from virtual folder X.
+ wrappedFolderX.searchFolders = [testFolderB, testFolderBBB, testFolderC];
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderBBB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+
+ // Add a second subfolder to the removed folder. Check it is not added to
+ // virtual folder X, because the parent folder is not in X.
+ testFolderBB.createSubfolder("test BBB two", null);
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderBBB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+});
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders2.js b/comm/mailnews/base/test/unit/test_virtualFolders2.js
new file mode 100644
index 0000000000..07c75db97f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders2.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/**
+ * Tests problems emptying a Trash folder that is searched by a virtual folder.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+add_task(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ let rootFolder = account.incomingServer.rootFolder;
+
+ // Create a real folders inside the trash folder.
+ let trashFolder = rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
+ trashFolder.createSubfolder("deleted", null);
+ let deletedFolder = trashFolder.getChildNamed("deleted");
+
+ // Create a virtual folder that searches the trash folder.
+ let wrappedVirtualFolder = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual",
+ rootFolder,
+ [trashFolder, deletedFolder],
+ "ANY",
+ false
+ );
+ let virtualFolder = wrappedVirtualFolder.virtualFolder;
+ Assert.equal(virtualFolder, rootFolder.getChildNamed("virtual"));
+ Assert.equal(
+ wrappedVirtualFolder.searchFolderURIs,
+ `${trashFolder.URI}|${deletedFolder.URI}`
+ );
+ Assert.deepEqual(wrappedVirtualFolder.searchFolders, [
+ trashFolder,
+ deletedFolder,
+ ]);
+
+ // Create a smart virtual folder that searches the trash folder. This is the
+ // same as before except we'll set the searchFolderFlag property, as we do
+ // for the Unified Folders section of the UI.
+ let wrappedSmartFolder = VirtualFolderHelper.createNewVirtualFolder(
+ "smart",
+ rootFolder,
+ [trashFolder, deletedFolder],
+ "ANY",
+ false
+ );
+ let smartFolder = wrappedSmartFolder.virtualFolder;
+ smartFolder.msgDatabase.dBFolderInfo.setUint32Property(
+ "searchFolderFlag",
+ Ci.nsMsgFolderFlags.Trash
+ );
+ Assert.equal(smartFolder, rootFolder.getChildNamed("smart"));
+ Assert.equal(
+ wrappedSmartFolder.searchFolderURIs,
+ `${trashFolder.URI}|${deletedFolder.URI}`
+ );
+ Assert.deepEqual(wrappedSmartFolder.searchFolders, [
+ trashFolder,
+ deletedFolder,
+ ]);
+
+ // Empty the trash. The normal virtual folder should disappear, but the
+ // smart folder shouldn't.
+ trashFolder.emptyTrash(null);
+ Assert.equal(
+ virtualFolder.parent,
+ null,
+ "virtual folder should be removed with last search folder"
+ );
+ Assert.equal(
+ smartFolder.parent,
+ rootFolder,
+ "smart virtual folder should NOT be removed with last search folder"
+ );
+ Assert.equal(
+ wrappedSmartFolder.searchFolderURIs,
+ trashFolder.URI,
+ "smart virtual folder should still search the trash folder (only)"
+ );
+ Assert.deepEqual(
+ wrappedSmartFolder.searchFolders,
+ [trashFolder],
+ "smart virtual folder should still search the trash folder (only)"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders3.js b/comm/mailnews/base/test/unit/test_virtualFolders3.js
new file mode 100644
index 0000000000..e3d1b9c73c
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders3.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+/**
+ * Tests that subfolders added to searched folders are also searched.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+let rootFolder;
+
+add_setup(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ rootFolder = account.incomingServer.rootFolder;
+ rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ // Listen to folder events for debugging purposes.
+
+ MailServices.mailSession.AddFolderListener(
+ {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderAdded(parentFolder, childFolder) {
+ console.log(
+ `onFolderAdded: ${
+ childFolder?.URI
+ } (flags: ${childFolder.flags.toString(16)}) added to ${
+ parentFolder?.URI
+ }`
+ );
+ },
+ onMessageAdded(parentFolder, msg) {},
+ onFolderRemoved(parentFolder, childFolder) {
+ console.log(
+ `onFolderRemoved: ${
+ childFolder?.URI
+ } (flags: ${childFolder.flags.toString(16)}) removed from ${
+ parentFolder?.URI
+ }`
+ );
+ },
+ onMessageRemoved(parentFolder, msg) {},
+ onFolderPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (property == "FolderFlag") {
+ console.log(
+ `onFolderIntPropertyChanged: ${
+ folder.URI
+ } changed flags from ${oldValue.toString(
+ 16
+ )} to ${newValue.toString(16)}`
+ );
+ }
+ },
+ onFolderBoolPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderUnicharPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderPropertyFlagChanged(folder, property, oldFlag, newFlag) {},
+ onFolderEvent(folder, event) {},
+ },
+ Ci.nsIFolderListener.all
+ );
+});
+
+// Test each of the folder types.
+add_task(function testInbox() {
+ subtest("Inbox");
+});
+add_task(function testDrafts() {
+ subtest("Drafts");
+});
+add_task(function testTemplates() {
+ subtest("Templates");
+});
+add_task(function testSentMail() {
+ subtest("SentMail");
+});
+add_task(function testArchive() {
+ subtest("Archive");
+});
+add_task(function testJunk() {
+ subtest("Junk");
+});
+add_task(function testTrash() {
+ subtest("Trash");
+});
+
+function subtest(flag) {
+ // Create a virtual folder. This is very similar to the code in about3Pane.js.
+
+ let virtualFolder = rootFolder.createLocalSubfolder(`virtual${flag}`);
+ virtualFolder.flags |=
+ Ci.nsMsgFolderFlags.Virtual | Ci.nsMsgFolderFlags[flag];
+
+ let msgDatabase = virtualFolder.msgDatabase;
+ let folderInfo = msgDatabase.dBFolderInfo;
+
+ folderInfo.setCharProperty("searchStr", "ALL");
+ folderInfo.setUint32Property("searchFolderFlag", Ci.nsMsgFolderFlags[flag]);
+ folderInfo.setBooleanProperty("searchOnline", true);
+ msgDatabase.summaryValid = true;
+ msgDatabase.close(true);
+
+ function checkVirtualFolder(searchFolders, message) {
+ let wrappedVirtualFolder =
+ VirtualFolderHelper.wrapVirtualFolder(virtualFolder);
+ Assert.deepEqual(
+ wrappedVirtualFolder.searchFolderURIs.split("|").filter(Boolean).sort(),
+ searchFolders.map(f => f.URI).sort(),
+ message
+ );
+ Assert.deepEqual(
+ wrappedVirtualFolder.searchFolders,
+ searchFolders,
+ message
+ );
+ }
+
+ rootFolder.notifyFolderAdded(virtualFolder);
+ checkVirtualFolder([], "new virtual folder should have no search folders");
+
+ // Create a disconnected folder with some descendants, add the flag and then
+ // add it to the parent. The folder and descendants should all be added to
+ // the virtual folder.
+
+ let parent = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}`
+ );
+ parent.setFlag(Ci.nsMsgFolderFlags[flag]);
+ let child = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/child`
+ );
+ parent.addSubfolder(child.name);
+ let grandchild = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/child/grandchild`
+ );
+ child.addSubfolder(grandchild.name);
+
+ rootFolder.addSubfolder(parent.name);
+ rootFolder.notifyFolderAdded(parent);
+ parent.notifyFolderAdded(child);
+ child.notifyFolderAdded(grandchild);
+
+ checkVirtualFolder(
+ [parent, child, grandchild],
+ "added folder and descendants should be added to the virtual folder"
+ );
+
+ // Create a subfolder of a real folder with some descendants, then set the
+ // flag. The folder and descendants should all be added to the virtual folder.
+
+ let more = rootFolder.createLocalSubfolder(`more${flag}`);
+ more.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let evenMore = more.createLocalSubfolder("even more");
+ evenMore.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let yetMore = evenMore.createLocalSubfolder("yet more");
+ more.setFlag(Ci.nsMsgFolderFlags[flag]);
+
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild],
+ "folder with changed flag and descendants should be added to the virtual folder"
+ );
+
+ // Test what happens if a folder of one type is not added to a parent in the
+ // virtual folder of another type. This should really only matter for inboxes
+ // containing other types of folders, which happens in some configurations.
+ // Other combinations shouldn't really exist, but let's test them anyway.
+
+ if (!["SentMail", "Archive"].includes(flag)) {
+ for (let otherFlag of [
+ "Inbox",
+ "Drafts",
+ "Templates",
+ "SentMail",
+ "Archive",
+ "Junk",
+ "Trash",
+ ]) {
+ if (otherFlag == flag) {
+ continue;
+ }
+ let otherFlagChild = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/other${otherFlag}Child`
+ );
+ otherFlagChild.setFlag(Ci.nsMsgFolderFlags[otherFlag]);
+ parent.addSubfolder(otherFlagChild.name);
+ parent.notifyFolderAdded(otherFlagChild);
+
+ if (flag == "Trash") {
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild, otherFlagChild],
+ `folder with ${otherFlag} flag should be added to the virtual ${flag} folder`
+ );
+ } else {
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild],
+ `folder with ${otherFlag} flag should not be added to the virtual ${flag} folder`
+ );
+ }
+
+ parent.propagateDelete(otherFlagChild, false);
+ }
+ }
+
+ // Now reverse the additions.
+
+ rootFolder.propagateDelete(parent, false);
+ checkVirtualFolder(
+ [more, evenMore, yetMore],
+ "deleted folder and descendants should be removed from the virtual folder"
+ );
+
+ if (flag != "Inbox") {
+ // Clearing the inbox flag from a folder that has it throws an assertion.
+ more.clearFlag(Ci.nsMsgFolderFlags[flag]);
+ checkVirtualFolder(
+ [],
+ "folder with changed flag and descendants should be removed from the virtual folder"
+ );
+
+ Assert.equal(
+ virtualFolder.parent,
+ rootFolder,
+ "smart virtual folder should NOT be removed with last search folder"
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/xpcshell-imap.ini b/comm/mailnews/base/test/unit/xpcshell-imap.ini
new file mode 100644
index 0000000000..2b343b30b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/xpcshell-imap.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head_mailbase.js
+tail =
+prefs =
+ mailnews.imap.jsmodule=true
+dupe-manifest =
+
+[test_incomingServer.js]
diff --git a/comm/mailnews/base/test/unit/xpcshell.ini b/comm/mailnews/base/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..0658d7d1b3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/xpcshell.ini
@@ -0,0 +1,77 @@
+[DEFAULT]
+head = head_mailbase.js
+tail =
+dupe-manifest =
+prefs =
+ mail.biff.use_new_count_in_badge=false
+support-files = nodelist_test.xml data/*
+
+[test_accountMgr.js]
+[test_accountMgr2.js]
+[test_accountMgrCustomTypes.js]
+[test_accountMgrMovedLocalFolders.js]
+[test_accountMgrRemoveDefault.js]
+[test_accountMigration.js]
+[test_acctRepair.js]
+[test_bccInDatabase.js]
+[test_bug428427.js]
+[test_bug434810.js]
+[test_bug471682.js]
+[test_bug514945.js]
+[test_closedDB.js]
+[test_compactColumnSave.js]
+[test_compactFailure.js]
+[test_converterDeferredAccount.js]
+[test_copyChaining.js]
+[test_copyToInvalidDB.js]
+[test_detachToFile.js]
+[test_emptyTrash.js]
+[test_fix_deferred_accounts.js]
+[test_folderCompact.js]
+[test_folderCompact2.js]
+[test_folderLookupService.js]
+[test_folderStringProperties.js]
+[test_formatFileSize.js]
+[test_getMsgTextFromStream.js]
+[test_headerFoldingInDatabase.js]
+[test_hostnameUtils.js]
+[test_identity.js]
+[test_imapPump.js]
+[test_incomingServer.js]
+[test_inheritedFolderProperties.js]
+[test_junkingWhenDisabled.js]
+[test_loadVirtualFolders.js]
+[test_mailServices.js]
+[test_mailstoreConverter.js]
+[test_mimemaltdetach.js]
+[test_MsgIncomingServer.js]
+[test_MsgKeySet.js]
+[test_newMailNotification.js]
+# Not yet working for non-Mac OS
+skip-if = os != 'mac'
+[test_nsIFolderListener.js]
+[test_nsIMsgContentPolicy.js]
+skip-if = true # See bug 1446587.
+[test_nsIMsgFolder.js]
+[test_nsIMsgFolderCache.js]
+[test_nsIMsgFolderListener.js]
+[test_nsIMsgFolderListenerLocal.js]
+[test_nsIMsgTagService.js]
+[test_nsMailDirProvider.js]
+[test_nsMsgDBView.js]
+[test_nsMsgDBView_headerValues.js]
+[test_nsMsgMailSession_Alerts.js]
+skip-if = true # See bug 1418063.
+[test_nsMsgMailSession_Listeners.js]
+[test_nsMsgTraitService.js]
+[test_postPluginFilters.js]
+[test_retention.js]
+[test_saveAs.js]
+[test_testsuite_base64.js]
+[test_testsuite_fakeserver_imapd_gmail.js]
+[test_testsuite_fakeserver_imapd_list-extended.js]
+[test_testsuite_fakeserverAuth.js]
+[test_viewSortByAddresses.js]
+[test_virtualFolders1.js]
+[test_virtualFolders2.js]
+[test_virtualFolders3.js]
diff --git a/comm/mailnews/compose/.eslintrc.js b/comm/mailnews/compose/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/compose/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/compose/content/sendProgress.js b/comm/mailnews/compose/content/sendProgress.js
new file mode 100644
index 0000000000..fbc05451a7
--- /dev/null
+++ b/comm/mailnews/compose/content/sendProgress.js
@@ -0,0 +1,174 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// dialog is just an array we'll use to store various properties from the dialog document...
+var dialog;
+
+// the msgProgress is a nsIMsgProgress object
+var msgProgress = null;
+
+// random global variables...
+var itsASaveOperation = false;
+var gBundle;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+window.addEventListener("unload", onUnload);
+document.addEventListener("dialogcancel", onCancel);
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ // Set progress meter to show indeterminate.
+ dialog.progress.removeAttribute("value");
+ dialog.progressText.value = "";
+ }
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (Components.isSuccessCode(aStatus)) {
+ // we are done sending/saving the message...
+ // Indicate completion in status area.
+ let msg;
+ if (itsASaveOperation) {
+ msg = gBundle.GetStringFromName("messageSaved");
+ } else {
+ msg = gBundle.GetStringFromName("messageSent");
+ }
+ dialog.status.setAttribute("value", msg);
+
+ // Put progress meter at 100%.
+ dialog.progress.setAttribute("value", 100);
+ dialog.progressText.setAttribute(
+ "value",
+ gBundle.formatStringFromName("percentMsg", [100])
+ );
+ }
+
+ // Note: Without some delay closing the window the "msg" string above may
+ // never be visible. Example: setTimeout(() => window.close(), 1000);
+ // Windows requires other delays. The delays also cause test failures.
+ window.close();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ // Calculate percentage.
+ var percent;
+ if (aMaxTotalProgress > 0) {
+ percent = Math.round((aCurTotalProgress / aMaxTotalProgress) * 100);
+ if (percent > 100) {
+ percent = 100;
+ }
+
+ // Advance progress meter.
+ dialog.progress.value = percent;
+
+ // Update percentage label on progress meter.
+ dialog.progressText.value = gBundle.formatStringFromName("percentMsg", [
+ percent,
+ ]);
+ } else {
+ // Have progress meter show indeterminate with denominator <= 0.
+ dialog.progress.removeAttribute("value");
+ dialog.progressText.value = "";
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // we can ignore this notification
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (aMessage != "") {
+ dialog.status.setAttribute("value", aMessage);
+ }
+ },
+
+ onSecurityChange(aWebProgress, aRequest, state) {
+ // we can ignore this notification
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
+ // we can ignore this notification
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function onLoad() {
+ // Set global variables.
+ gBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/sendProgress.properties"
+ );
+
+ msgProgress = window.arguments[0];
+ if (!msgProgress) {
+ console.error("Invalid argument to sendProgress.xhtml.");
+ window.close();
+ return;
+ }
+
+ let subject = "";
+ if (window.arguments[1]) {
+ let progressParams = window.arguments[1].QueryInterface(
+ Ci.nsIMsgComposeProgressParams
+ );
+ if (progressParams) {
+ itsASaveOperation =
+ progressParams.deliveryMode != Ci.nsIMsgCompDeliverMode.Now;
+ subject = progressParams.subject;
+ }
+ }
+
+ if (subject) {
+ let title = itsASaveOperation
+ ? "titleSaveMsgSubject"
+ : "titleSendMsgSubject";
+ document.title = gBundle.formatStringFromName(title, [subject]);
+ } else {
+ let title = itsASaveOperation ? "titleSaveMsg" : "titleSendMsg";
+ document.title = gBundle.GetStringFromName(title);
+ }
+
+ dialog = {};
+ dialog.status = document.getElementById("dialog.status");
+ dialog.progress = document.getElementById("dialog.progress");
+ dialog.progressText = document.getElementById("dialog.progressText");
+
+ // set our web progress listener on the helper app launcher
+ msgProgress.registerListener(progressListener);
+}
+
+function onUnload() {
+ if (msgProgress) {
+ try {
+ msgProgress.unregisterListener(progressListener);
+ msgProgress = null;
+ } catch (e) {}
+ }
+}
+
+// If the user presses cancel, tell the app launcher and close the dialog...
+function onCancel(event) {
+ // Cancel app launcher.
+ try {
+ msgProgress.processCanceledByUser = true;
+ } catch (e) {
+ return;
+ }
+
+ // Don't close up dialog, the backend will close the dialog when everything will be aborted.
+ event.preventDefault();
+}
diff --git a/comm/mailnews/compose/content/sendProgress.xhtml b/comm/mailnews/compose/content/sendProgress.xhtml
new file mode 100644
index 0000000000..cf46d0c8cc
--- /dev/null
+++ b/comm/mailnews/compose/content/sendProgress.xhtml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/messengercompose/sendProgress.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 56ch; min-height: 8em"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&sendDialog.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/sendProgress.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="sendProgress" buttons="cancel">
+ <hbox flex="1">
+ <vbox align="end">
+ <hbox flex="1" align="center">
+ <label value="&status.label;" />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label value="&progress.label;" />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <hbox flex="1" align="center">
+ <label id="dialog.status" crop="center" />
+ </hbox>
+ <hbox
+ class="thin-separator"
+ flex="1"
+ style="display: flex; align-items: center"
+ >
+ <html:progress
+ id="dialog.progress"
+ value="0"
+ max="100"
+ style="flex: 1"
+ />
+ <label id="dialog.progressText" value="" />
+ </hbox>
+ </vbox>
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/compose/moz.build b/comm/mailnews/compose/moz.build
new file mode 100644
index 0000000000..a49689ab64
--- /dev/null
+++ b/comm/mailnews/compose/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/compose/public/moz.build b/comm/mailnews/compose/public/moz.build
new file mode 100644
index 0000000000..b4e027ef05
--- /dev/null
+++ b/comm/mailnews/compose/public/moz.build
@@ -0,0 +1,30 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIMsgAttachment.idl",
+ "nsIMsgCompFields.idl",
+ "nsIMsgCompose.idl",
+ "nsIMsgComposeParams.idl",
+ "nsIMsgComposeProgressParams.idl",
+ "nsIMsgComposeSecure.idl",
+ "nsIMsgComposeService.idl",
+ "nsIMsgCompUtils.idl",
+ "nsIMsgCopy.idl",
+ "nsIMsgQuote.idl",
+ "nsIMsgQuotingOutputStreamListener.idl",
+ "nsIMsgSend.idl",
+ "nsIMsgSendLater.idl",
+ "nsIMsgSendLaterListener.idl",
+ "nsIMsgSendListener.idl",
+ "nsIMsgSendReport.idl",
+ "nsISmtpServer.idl",
+ "nsISmtpService.idl",
+ "nsISmtpUrl.idl",
+]
+
+XPIDL_MODULE = "msgcompose"
+
+EXPORTS += []
diff --git a/comm/mailnews/compose/public/nsIMsgAttachment.idl b/comm/mailnews/compose/public/nsIMsgAttachment.idl
new file mode 100644
index 0000000000..71356d6686
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgAttachment.idl
@@ -0,0 +1,144 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(d17d2d60-ec3a-46de-8bd1-24c77dd9b87b)]
+interface nsIMsgAttachment : nsISupports {
+
+ /**
+ * name attribute
+ *
+ * @Attachment real name, will be sent with the attachment's header.
+ * @If no name has been provided, a name will be generated using the url.
+ */
+ attribute AString name;
+
+ /**
+ * url attribute
+ *
+ * @specify where the attachment live (locally or remotely)
+ */
+ attribute AUTF8String url;
+
+ /**
+ * msgUri attribute
+ *
+ * @specify the uri of the message this attachment belongs to
+ */
+ attribute AUTF8String msgUri;
+
+ /**
+ * urlCharset attribute
+ *
+ * @specify the Charset of url (used to convert url to Unicode after
+ * unescaping)
+ */
+ attribute ACString urlCharset;
+
+
+ /**
+ * temporary attribute
+ *
+ * @If set to true, the file pointed by the url will be destroyed when this object is destroyed.
+ * @This is only for local attachment.
+ */
+ attribute boolean temporary;
+
+ /**
+ * Are we storing this attachment via a cloud provider and linking to it?
+ */
+ attribute boolean sendViaCloud;
+
+ /**
+ * Cloud provider account key for this attachment, if any.
+ */
+ attribute ACString cloudFileAccountKey;
+
+ /**
+ * A data string stored in the x-mozilla-cloud-part header of draft messages,
+ * to be able to restore cloudFile information of re-opened drafts.
+ */
+ attribute AUTF8String cloudPartHeaderData;
+
+ /**
+ * This allows the compose front end code to put whatever html annotation
+ * it wants for the cloud part, e.g., with expiration time, etc.
+ */
+ attribute AUTF8String htmlAnnotation;
+
+ /**
+ * contentLocation attribute
+ *
+ * @Specify the origin url of the attachment, used normally when attaching
+ * a locally saved html document, but also used for cloud files and to store
+ * the original mailbox:// url of attachments, after they have been saves as
+ * temporary files.
+ */
+ attribute ACString contentLocation;
+
+ /**
+ * contentType attribute
+ *
+ * @Specify the content-type of the attachment, this does not include extra content-type parameters. If
+ * @you need to specify extra information, use contentTypeParam, charset, macType or macCreator.
+ * @If omitted, it will be determined base on either the name, the url or the content of the file.
+ */
+ attribute string contentType;
+
+ /**
+ * contentTypeParam attribute
+ *
+ * @Specify the any content-type parameter (other than the content-type itself, charset, macType or macCreator).
+ * @It will be added to the content-type during the send/save operation.
+ */
+ attribute string contentTypeParam;
+
+ /**
+ * Content-ID for embedded attachments inside a multipart/related container.
+ */
+ attribute AUTF8String contentId;
+
+ /**
+ * charset attribute
+ *
+ * @Specify the charset of the attachment. It will be added to the content-type during the
+ * @send/save operation
+ * @If omitted, will be determined automatically (if possible).
+ */
+ attribute string charset;
+
+ /**
+ * size attribute
+ *
+ * @Specify the size of the attachment.
+ */
+ attribute int64_t size;
+
+ /**
+ * macType attribute
+ *
+ * @Specify the Mac file type of the attachment. It will be added to the content-type during the
+ * @send/save operation
+ * @If omitted, will be determined automatically on Macintosh OS.
+ */
+ attribute string macType;
+
+ /**
+ * macCreator attribute
+ *
+ * @Specify the Mac file creator of the attachment. It will be added to the content-type during the
+ * @send/save operation
+ * @If omitted, will be determined automatically on Macintosh OS.
+ */
+ attribute string macCreator;
+
+ /**
+ * equalsUrl
+ *
+ * @ determines if both attachments have the same url.
+ */
+ boolean equalsUrl(in nsIMsgAttachment attachment);
+};
diff --git a/comm/mailnews/compose/public/nsIMsgCompFields.idl b/comm/mailnews/compose/public/nsIMsgCompFields.idl
new file mode 100644
index 0000000000..7ed51f4737
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgCompFields.idl
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgIStructuredHeaders.idl"
+
+interface nsIMsgAttachment;
+interface nsIMsgComposeSecure;
+
+/**
+ * A collection of headers and other attributes for building a mail message.
+ */
+[scriptable, uuid(10928477-4F24-4357-9397-FBD847F46F0A)]
+interface nsIMsgCompFields : msgIWritableStructuredHeaders {
+
+ attribute AString from;
+ attribute AString replyTo;
+ attribute AString to;
+ attribute AString cc;
+ attribute AString bcc;
+ readonly attribute bool hasRecipients;
+
+ attribute AString fcc;
+ attribute AString fcc2;
+
+ attribute AString newsgroups;
+ attribute string newspostUrl;
+ attribute AString followupTo;
+
+ attribute AString subject;
+
+ attribute AString organization;
+ attribute string references;
+ attribute string priority;
+ attribute string messageId;
+
+ attribute AString templateName;
+ // The so-called draft/template ID is a URI in reality.
+ attribute AUTF8String draftId;
+ attribute AUTF8String templateId;
+
+ attribute boolean returnReceipt;
+ attribute long receiptHeaderType;
+ attribute boolean DSN;
+ attribute boolean attachVCard;
+ attribute boolean forcePlainText;
+ attribute boolean useMultipartAlternative;
+ attribute boolean bodyIsAsciiOnly;
+ attribute boolean forceMsgEncoding;
+ /// Status of manually-activated attachment reminder.
+ attribute boolean attachmentReminder;
+ /// Delivery format for the mail being composed
+ /// (auto = 4, text = 1, html = 2, text and html = 3).
+ attribute long deliveryFormat;
+ attribute string contentLanguage;
+ /// This is populated with the key of the identity which created the draft or template.
+ attribute string creatorIdentityKey;
+
+ /**
+ * Beware that when setting this property, your body must be properly wrapped,
+ * and the line endings must match MSG_LINEBREAK, namely "\r\n" on Windows
+ * and "\n" on Linux and OSX.
+ */
+ attribute AString body;
+
+ readonly attribute Array<nsIMsgAttachment> attachments;
+ void addAttachment(in nsIMsgAttachment attachment);
+ void removeAttachment(in nsIMsgAttachment attachment);
+ void removeAttachments();
+
+ /**
+ * Values for other headers. Headers in order, from the
+ * mail.compose.other.header pref.
+ */
+ attribute Array<AString> otherHeaders;
+
+ /**
+ * This function will split the recipients into an array.
+ *
+ * @param aRecipients The recipients list to split.
+ * @param aEmailAddressOnly Set to true to drop display names from the results
+ * array.
+ *
+ * @return An array of the recipients.
+ */
+ Array<AString> splitRecipients(in AString aRecipients,
+ in boolean aEmailAddressOnly);
+
+ void ConvertBodyToPlainText();
+
+ /**
+ * Indicates whether we need to check if the current |DocumentCharset|
+ * can represent all the characters in the message body. It should be
+ * initialized to true and set to false when 'Send Anyway' is selected
+ * by a user. (bug 249530)
+ */
+ attribute boolean needToCheckCharset;
+
+ /**
+ * Object implementing encryption/signing functionality (e.g. S/MIME, PGP/MIME)
+ */
+ attribute nsIMsgComposeSecure composeSecure;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgCompUtils.idl b/comm/mailnews/compose/public/nsIMsgCompUtils.idl
new file mode 100644
index 0000000000..18749e1f9d
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgCompUtils.idl
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIdentity.idl"
+
+[scriptable, uuid(00b4569a-077e-4236-b993-980fd82bb948)]
+interface nsIMsgCompUtils : nsISupports {
+ string mimeMakeSeparator(in string prefix);
+
+ /**
+ * Try to use the provided identity and/or host name to generate a message ID.
+ *
+ * To identify the host name to use in the message ID, this will:
+ * - if the attribute "FQDN" of the identity is set to a valid host name, use it
+ * - otherwise, if the provided host name is valid, use it
+ * - otherwise, if the identity's email address includes a valid host name after
+ * an '@' symbol, use it
+ * - otherwise, bail without generating a message ID (returns with an empty value)
+ *
+ * @param nsIMsgIdentity The identity to use to generate the message ID.
+ * @param string The host to use to generate the message ID. Ignored if empty.
+ *
+ * @returns A message ID usable in a Message-ID header, or an empty string
+ * if no message ID could be generated.
+ */
+ AUTF8String msgGenerateMessageId(in nsIMsgIdentity identity, in AUTF8String host);
+
+ readonly attribute boolean msgMimeConformToStandard;
+
+ /**
+ * Detect the text encoding of an input string. This is a wrapper of
+ * mozilla::EncodingDetector to be used by JavaScript code. For C++, use
+ * MsgDetectCharsetFromFile from nsMsgUtils.cpp instead.
+ *
+ * @param aContent The string to detect charset.
+ *
+ * @returns Detected charset.
+ */
+ ACString detectCharset(in ACString aContent);
+};
diff --git a/comm/mailnews/compose/public/nsIMsgCompose.idl b/comm/mailnews/compose/public/nsIMsgCompose.idl
new file mode 100644
index 0000000000..7af2405e84
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgCompose.idl
@@ -0,0 +1,305 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgCompFields.idl"
+#include "nsIMsgComposeParams.idl"
+#include "nsIMsgSendListener.idl"
+
+%{C++
+#include "nsString.h"
+%}
+
+interface nsIMsgSend;
+interface nsIMsgIdentity;
+interface nsIMsgProgress;
+interface nsIDocShell;
+interface mozIDOMWindowProxy;
+interface nsIEditor;
+interface nsIMsgWindow;
+
+webidl Element;
+
+typedef long MSG_ComposeSaveType;
+
+[scriptable, uuid(6953e50a-7531-11d3-85fe-006008948010)]
+interface nsIMsgCompSaveType : nsISupports {
+ const long File = 0;
+ const long Template = 1;
+ const long Draft = 2;
+};
+
+typedef long MSG_DeliverMode;
+
+[scriptable, uuid(a9f27dd7-8f89-4de3-8fbf-41b789c16ee5)]
+interface nsIMsgCompDeliverMode : nsISupports {
+ const long Now = 0;
+ const long Later = 1;
+ const long Save = 2;
+ const long SaveAs = 3;
+ const long SaveAsDraft = 4;
+ const long SaveAsTemplate = 5;
+ const long SendUnsent = 6;
+ const long AutoSaveAsDraft = 7;
+ const long Background = 8;
+};
+
+[scriptable, uuid(f38ea280-e090-11d3-a449-e3153319347c)]
+interface nsIMsgCompSendFormat : nsISupports {
+ /* Send only plain text if the message is free of any rich formatting or
+ * inserted elements. Otherwise send both a HTML part and plain text
+ * alternative part. */
+ const long Auto = 0;
+ /* Only send a plain text part, losing any rich formatting or inserted
+ * elements. */
+ const long PlainText = 1;
+ /* Only send a HTML part. */
+ const long HTML = 2;
+ /* Send both the HTML part and the plain text alternative part. */
+ const long Both = 3;
+ /* An unset value, to be set with mail.default_send_format on loading the
+ * message in the compose window. */
+ const long Unset = 4;
+};
+
+[scriptable, uuid(9638af92-1dd1-11b2-bef1-ca5fee0abc62)]
+interface nsIMsgCompConvertible : nsISupports /*ToTXT*/ {
+ const long Plain = 1; // Like 4.x: Only <html>, <p>, <br>, ...
+ const long Yes = 2; // *Minor* alterations of the look: <ol>, <dd>, ...
+ const long Altering = 3; /* Look altered: <strong>, <i>, <h1>, ...
+ Can be expressed in plaintext, but not in
+ the way it looked in the HTML composer. */
+ const long No = 4; /* Will lose data: <font>, ...
+ Really *requires* visual formatting or
+ is not supported by our HTML->TXT converter. */
+ /* The values here have meaning, they are "levels":
+ convertible({a; b}) == max(convertible({a}), convertible({b}))
+ must be true, i.e. the higher value counts. */
+};
+
+[scriptable, uuid(6ce49b2a-07dc-4783-b307-9a355423163f)]
+interface nsIMsgComposeStateListener : nsISupports
+{
+ /* ... */
+ void NotifyComposeFieldsReady();
+ void ComposeProcessDone(in nsresult aResult);
+ void SaveInFolderDone(in string folderName);
+ void NotifyComposeBodyReady();
+};
+
+[scriptable, uuid(061aae23-7e0a-4818-9a15-1b5db3ceb7f4)]
+interface nsIMsgComposeNotificationType : nsISupports
+{
+ const long ComposeFieldsReady = 0;
+ const long ComposeProcessDone = 1;
+ const long SaveInFolderDone = 2;
+ const long ComposeBodyReady = 3;
+};
+
+native nsString(nsString);
+[ref] native nsStringRef(nsString);
+
+[scriptable, uuid(c6544b6b-06dd-43ac-89b5-949d7c81bb7b)]
+interface nsIMsgCompose : nsIMsgSendListener {
+
+ /**
+ * Initializes the msg compose object.
+ *
+ * @param aParams An nsIMsgComposeParams object containing the initial
+ * details for the compose.
+ * @param aWindow The optional window associated with this compose object.
+ * @param aDocShell The optional docShell of the editor element that is used
+ * for composing.
+ */
+ void initialize(in nsIMsgComposeParams aParams,
+ [optional] in mozIDOMWindowProxy aWindow,
+ [optional] in nsIDocShell aDocShell);
+
+ /* ... */
+ void RegisterStateListener(in nsIMsgComposeStateListener stateListener);
+
+ /* ... */
+ void UnregisterStateListener(in nsIMsgComposeStateListener stateListener);
+
+ /* ... */
+ Promise sendMsg(in MSG_DeliverMode deliverMode, in nsIMsgIdentity identity, in string accountKey, in nsIMsgWindow aMsgWindow, in nsIMsgProgress progress);
+
+ /**
+ * After all Compose preparations are complete, send the prepared message to
+ * the server. This exists primarily to allow an override of the sending to
+ * use a non-SMTP method for send.
+ *
+ * @param deliverMode One of the nsIMsgCompDeliverMode values.
+ * @param identity The message identity.
+ * @param accountKey The message account key.
+ */
+ Promise sendMsgToServer(in MSG_DeliverMode deliverMode,
+ in nsIMsgIdentity identity,
+ in string accountKey);
+
+ /* ... */
+ void CloseWindow();
+
+ /* ... */
+ void abort();
+
+ /* ... */
+ void quoteMessage(in AUTF8String msgURI);
+
+ /*
+ AttachmentPrettyName will return only the leafName if the it's a file URL.
+ It will also convert the filename to Unicode assuming it's in the file system
+ charset. In case of URL, |charset| parameter will be used in the conversion.
+ This UI utility function should probably go into it's own class
+ */
+ AUTF8String AttachmentPrettyName(in AUTF8String url, in string charset);
+
+ /**
+ * Expand all mailing lists in the relevant compose fields to include the
+ * members of their output. This method will additionally update the
+ * popularity field of cards in the addressing header.
+ */
+ void expandMailingLists();
+
+ /**
+ * The level of "convertibility" of the message body (whole HTML document)
+ * to plaintext.
+ *
+ * @return a value from nsIMsgCompConvertible.
+ */
+ long bodyConvertible();
+
+ /**
+ * The level of "convertibility" of the provided node to plaintext.
+ *
+ * @return a value from nsIMsgCompConvertible.
+ */
+ long nodeTreeConvertible(in Element aNode);
+
+ /**
+ * The identity currently selected for the message compose object. When set
+ * this may change the signature on a message being composed. Note that
+ * typically SendMsg will be called with the same identity as is set here, but
+ * if it is different the SendMsg version will overwrite this identity.
+ */
+ attribute nsIMsgIdentity identity;
+
+ /* Check if the composing mail headers (and identity) can be converted to a mail charset.
+ */
+ boolean checkCharsetConversion(in nsIMsgIdentity identity, out string fallbackCharset);
+
+ /* The message send object. This is created by default to be the SMTP server
+ * in sendMsgToServer, but if that method is overridden, set the actual
+ * value used here.
+ */
+ attribute nsIMsgSend messageSend;
+
+ /*
+ * Clear the messageSend object to break any circular references
+ */
+ void clearMessageSend();
+
+ /* ... */
+ attribute nsIEditor editor;
+
+ /* ... */
+ readonly attribute mozIDOMWindowProxy domWindow;
+
+ /* ... */
+ readonly attribute nsIMsgCompFields compFields;
+
+ /* ... */
+ readonly attribute boolean composeHTML;
+
+ /* ... */
+ attribute MSG_ComposeType type;
+
+ /* ... */
+ readonly attribute long wrapLength;
+
+ /* by reading this value, you can determine if yes or not the message has been modified
+ by the user. When you set this value to false, you reset the modification count
+ of the body to 0 (clean).
+ */
+ attribute boolean bodyModified;
+
+ /**
+ * Init the editor THIS USED TO BE [noscript]
+ * Now, this is called after editor is created,
+ * which is triggered by loading startup url from JS.
+ * The completion of document loading is detected by observing
+ * the "obs_documentCreated" command
+ */
+ void initEditor(in nsIEditor editor, in mozIDOMWindowProxy contentWindow);
+
+ /* The following functions are for internal use, essentially for the listener */
+
+ /* ... */
+ [noscript] void setCiteReference(in nsString citeReference);
+
+ /* Set the URI of the folder where the message has been saved */
+ attribute AUTF8String savedFolderURI;
+
+ /* Append the signature defined in the identity to the msgBody */
+ [noscript] void processSignature(in nsIMsgIdentity identity,
+ in boolean aQuoted,
+ inout nsString aMsgBody);
+
+ /* set any reply flags on the original message's folder */
+ [noscript] void processReplyFlags();
+ [noscript] void rememberQueuedDisposition();
+
+ /* ... */
+ [noscript]
+ void convertAndLoadComposeWindow(in nsStringRef aPrefix,
+ in nsStringRef aBuf,
+ in nsStringRef aSignature,
+ in boolean aQuoted,
+ in boolean aHTMLEditor);
+
+ /* Tell the doc state listeners that the doc state has changed
+ * aNotificationType is from nsIMsgComposeNotificationType
+ */
+ [noscript] void notifyStateListeners(in long aNotificationType, in nsresult aResult);
+
+ /* Retrieve the progress object */
+ readonly attribute nsIMsgProgress progress;
+
+ /* ... */
+ [noscript]
+ void buildBodyMessageAndSignature();
+
+ /* ... */
+ [noscript] void buildQuotedMessageAndSignature();
+
+ /* ... */
+ [noscript] void getQuotingToFollow(out boolean quotingToFollow);
+
+ readonly attribute AUTF8String originalMsgURI;
+
+ attribute boolean deleteDraft;
+
+ /** Set to true when remote content can load in the editor. E.g for pasting. */
+ attribute boolean allowRemoteContent;
+
+ /* for easier use of nsIMsgSendListener */
+ void addMsgSendListener(in nsIMsgSendListener sendListener);
+
+ /* for easier use of nsIMsgSendListener */
+ void removeMsgSendListener(in nsIMsgSendListener sendListener);
+
+ /// Access during mail-set-sender observer if needed, see nsIMsgCompDeliverMode.
+ readonly attribute MSG_DeliverMode deliverMode;
+
+};
+
+/* send listener interface */
+[scriptable, uuid(ad6ee068-b225-47f9-a50e-8e48440282ca)]
+interface nsIMsgComposeSendListener : nsISupports {
+
+ void setMsgCompose(in nsIMsgCompose msgCompose);
+ void setDeliverMode(in MSG_DeliverMode deliverMode);
+
+};
diff --git a/comm/mailnews/compose/public/nsIMsgComposeParams.idl b/comm/mailnews/compose/public/nsIMsgComposeParams.idl
new file mode 100644
index 0000000000..bafe7ef4e0
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgComposeParams.idl
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgCompFields.idl"
+#include "nsIMsgSendListener.idl"
+
+interface nsIMsgDBHdr;
+typedef long MSG_ComposeType;
+
+[scriptable, uuid(c7035852-7531-11d3-9a73-006008948010)]
+interface nsIMsgCompType : nsISupports {
+ const long New = 0;
+ const long Reply = 1;
+ const long ReplyAll = 2;
+ const long ForwardAsAttachment = 3;
+ const long ForwardInline = 4;
+ const long NewsPost = 5;
+ const long ReplyToSender = 6;
+ const long ReplyToGroup = 7;
+ const long ReplyToSenderAndGroup = 8;
+ const long Draft = 9;
+ const long Template = 10; // New message from template.
+ const long MailToUrl = 11;
+ const long ReplyWithTemplate = 12;
+ const long ReplyToList = 13;
+
+ /**
+ * Will resend the original message keeping the Subject and the body the
+ * same, and will set the Reply-To: header to the sender of the original
+ * message. This gets the redirector "out of the loop" because replies
+ * to the message will go to the original sender. This is not the same
+ * as the Resent mechanism described in section 3.6.6 of RFC 2822, and
+ * so therefore does not use Resent-* headers.
+ */
+ const long Redirect = 14;
+
+ /**
+ * Used to compose a new message from an existing message. Links
+ * are sanitized since the message could be from external sources.
+ */
+ const long EditAsNew = 15;
+
+ /**
+ * Used to edit an existing template.
+ */
+ const long EditTemplate = 16;
+};
+
+
+typedef long MSG_ComposeFormat;
+
+[scriptable, uuid(a28325e8-7531-11d3-8f1c-006008948010)]
+interface nsIMsgCompFormat : nsISupports{
+ const long Default = 0;
+ const long HTML = 1;
+ const long PlainText = 2;
+ const long OppositeOfDefault = 3;
+};
+
+
+[scriptable, uuid(930895f2-d610-43f4-9e3c-25e1d1fe4143)]
+interface nsIMsgComposeParams : nsISupports {
+ attribute MSG_ComposeType type;
+ attribute MSG_ComposeFormat format;
+ attribute AUTF8String originalMsgURI;
+ attribute nsIMsgIdentity identity;
+
+ attribute nsIMsgCompFields composeFields;
+ attribute boolean bodyIsLink;
+
+ attribute nsIMsgSendListener sendListener;
+ attribute AString smtpPassword;
+ attribute nsIMsgDBHdr origMsgHdr;
+ attribute boolean autodetectCharset;
+
+ /**
+ * HTML-formatted content to quote in the body of the message.
+ * Set this to get different content than what would normally
+ * appear in the body, e.g. the original message body in a reply.
+ */
+ attribute AUTF8String htmlToQuote;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl b/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl
new file mode 100644
index 0000000000..9a77012c5c
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "nsIMsgCompose.idl"
+
+[scriptable, uuid(1e0e7c00-3e4c-11d5-9daa-f88d288130fc)]
+interface nsIMsgComposeProgressParams: nsISupports {
+
+ /* message subject */
+ attribute wstring subject;
+
+ /* delivery mode */
+ attribute MSG_DeliverMode deliveryMode;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgComposeSecure.idl b/comm/mailnews/compose/public/nsIMsgComposeSecure.idl
new file mode 100644
index 0000000000..cbebdb9495
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgComposeSecure.idl
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsIMsgSendReport.idl"
+#include "nsISupports.idl"
+
+interface nsIMsgCompFields;
+interface nsIMsgIdentity;
+interface nsIOutputStream;
+interface nsIX509Cert;
+
+/**
+ * Callback type for use with asyncFindCertByEmailAddr.
+ */
+[scriptable, function, uuid(6149d7d3-14bf-4280-8451-60fb48263894)]
+interface nsIDoneFindCertForEmailCallback : nsISupports {
+ /**
+ * Called after a searching for a certificate is done.
+ *
+ * @param emailAddress - The email address that was used as the key
+ * to find this certificate.
+ * @param cert - The valid certificate that was found,
+ * or null, if no valid cert was found.
+ */
+ void findCertDone(in AUTF8String emailAddress,
+ in nsIX509Cert cert);
+};
+
+/**
+ * An instance of this type is related to exactly one email message
+ * while the user is composing it.
+ * Besides remembering flags and providing helper code, it is used to
+ * cache information about valid S/MIME encryption certificates that
+ * were found and which may be used at send time.
+ */
+[scriptable, uuid(245f2adc-410e-4bdb-91e2-a7bb42d61787)]
+interface nsIMsgComposeSecure : nsISupports
+{
+ /**
+ * Set to true if the outgoing message shall be signed.
+ */
+ attribute boolean signMessage;
+
+ /**
+ * Set to true if the outgoing message shall be encrypted.
+ */
+ attribute boolean requireEncryptMessage;
+
+ /***************************************************************************
+ * The following functions are called during message creation by nsMsgSend,
+ * after the message source is completely prepared.
+ ***************************************************************************/
+
+ /**
+ * Determine if encryption and/or signing is required.
+ *
+ * @param aIdentity - The sender's identity
+ * @param compFields - Attributes of the composed message
+ *
+ * @return - Returns true if the creation of the message requires us to go through
+ * some encryption work, and false otherwise.
+ */
+ boolean requiresCryptoEncapsulation(in nsIMsgIdentity aIdentity, in nsIMsgCompFields aCompFields);
+
+ /**
+ * Start encryption work. Called before the encrypted data is processed.
+ *
+ * @param aStream - Output stream that takes the resulting data
+ * @param aRecipients - RFC 2047-encoded list of all recipients (To:, Cc:, Bcc:, ... fields), separated by "," or ", "
+ * Recipients contain name and email addresses, just like they will be put into the message headers
+ * @param compFields - Attributes of the composed message
+ * @param aIdentity - The sender's identity
+ * @param sendReport - Report feedback to the user
+ * @param aIsDraft - True if send operation saves draft/template/etc., false if message is really sent (or sent later)
+ */
+ void beginCryptoEncapsulation(in nsIOutputStream aStream, in string aRecipients, in nsIMsgCompFields aCompFields, in nsIMsgIdentity aIdentity, in nsIMsgSendReport sendReport, in boolean aIsDraft);
+
+ /**
+ * Process a part of the message data. Called multiple times, usually for every
+ * line of the data to be encrypted
+ *
+ * @param aBuf - Buffer holding the data to be processed
+ * @param aLen - Length of the buffer (number of characters)
+ */
+ void mimeCryptoWriteBlock(in string aBuf, in long aLen);
+
+ /**
+ * End encryption work. Called after the encrypted data is processed.
+ *
+ * @param aAbort - True if the send operation was aborted
+ * @param sendReport - Report feedback to the user
+ */
+ void finishCryptoEncapsulation(in boolean aAbort, in nsIMsgSendReport sendReport);
+
+ /**
+ * Is information about a valid encryption certificate for the given
+ * email address already available in the cache?
+ *
+ * @param emailAddress - The email address to check.
+ *
+ * @return - True if a valid cert is known by the cache.
+ */
+ boolean haveValidCertForEmail(in AUTF8String emailAddress);
+
+ /**
+ * If a valid encryption certificate for the given email address
+ * is already known by the cache, then return the NSS database
+ * key of that certificate.
+ *
+ * @param emailAddress - The email address to check.
+ *
+ * @return - NSS db key of the valid cert.
+ */
+ ACString getCertDBKeyForEmail(in AUTF8String emailAddress);
+
+ /**
+ * Remember the given certificate database key in our cache. The
+ * given certDBey (as used with nsIX509CertDB) must reference a
+ * valid encryption certificate for the given email address.
+ *
+ * @param emailAddress - The email address that is related to
+ * the given certDBKey.
+ * @param certDBKey - The certificate database key.
+ */
+ void cacheValidCertForEmail(in AUTF8String emailAddress,
+ in ACString certDBKey);
+
+ /*
+ * Asynchronously find an encryption certificate by email address. Calls
+ * `findCertDone` function on the provided `nsIDoneFindCertForEmailCallback`
+ * with the results of the operation.
+ *
+ * @param emailAddress - The email address to be used as the key
+ * to find the certificate.
+ * @param callback - A callback of type nsIDoneFindCertForEmailCallback,
+ * function findCertDone will be called with
+ * the result of the operation.
+ */
+ [must_use]
+ void asyncFindCertByEmailAddr(in AUTF8String emailAddress,
+ in nsIDoneFindCertForEmailCallback callback);
+};
diff --git a/comm/mailnews/compose/public/nsIMsgComposeService.idl b/comm/mailnews/compose/public/nsIMsgComposeService.idl
new file mode 100644
index 0000000000..068bb7bd46
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgComposeService.idl
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+#include "nsIMsgCompose.idl"
+#include "nsIMsgComposeParams.idl"
+
+interface nsIURI;
+interface nsIDocShell;
+interface nsIMsgWindow;
+interface nsIMsgIdentity;
+interface nsIMsgIncomingServer;
+interface nsIMsgDBHdr;
+
+webidl Selection;
+
+[scriptable, uuid(041782bf-e523-444b-a268-d90868fd2b50)]
+interface nsIMsgComposeService : nsISupports {
+
+ /**
+ * Open a compose window given a mailto url and other options.
+ *
+ * @param msgComposeWindowURL Can be null in most cases. If you have your
+ * own chrome url you want to use in bringing up a
+ * compose window, pass it in here.
+ * @param msgHdr The header of the original message.
+ * @param originalMsgURI The URI of the original message.
+ * @param type The message compose type: new/reply/forward/..
+ * @param format The message compose format: text/html/..
+ * @param identity The identity to send the message from.
+ * @param from The email address of the sender.
+ * @param aMsgWindow The message window to use.
+ * @param suppressReplyQuote An optional boolean flag to ignore or include
+ selected content in aMsgWindow as quote in the
+ new compose window.
+ */
+ [can_run_script]
+ void OpenComposeWindow(in AUTF8String msgComposeWindowURL,
+ in nsIMsgDBHdr msgHdr,
+ in AUTF8String originalMsgURI,
+ in MSG_ComposeType type,
+ in MSG_ComposeFormat format,
+ in nsIMsgIdentity identity,
+ in AUTF8String from,
+ in nsIMsgWindow aMsgWindow,
+ [optional] in Selection aSelection,
+ [optional] in boolean autodetectCharset);
+
+ /**
+ * Open a compose window given a mailto url and (optionally) an identity.
+ *
+ * @param aMsgComposeWindowURL Can be null in most cases. If you have your
+ * own chrome url you want to use in bringing up a
+ * compose window, pass it in here.
+ * @param aURI The mailto url you want to use as the
+ * foundation for the data inside the compose
+ * window.
+ * @param aIdentity An optional identity to send the message from.
+ */
+ void OpenComposeWindowWithURI(in string msgComposeWindowURL,
+ in nsIURI aURI,
+ [optional] in nsIMsgIdentity aIdentity);
+
+ /* ... */
+ void OpenComposeWindowWithParams(in string msgComposeWindowURL, in nsIMsgComposeParams params);
+
+ /**
+ * Creates an nsIMsgCompose instance and initializes it.
+ *
+ * @param aParams An nsIMsgComposeParams object containing the initial
+ * details for the compose.
+ * @param aWindow The optional window associated with this compose object.
+ * @param aDocShell The optional docShell of the editor element that is used
+ * for composing.
+ */
+ nsIMsgCompose initCompose(in nsIMsgComposeParams aParams,
+ [optional] in mozIDOMWindowProxy aWindow,
+ [optional] in nsIDocShell aDocShell);
+
+ /**
+ * defaultIdentity
+ *
+ * @return the default identity, in case no identity has been setup yet, will return null
+ */
+ readonly attribute nsIMsgIdentity defaultIdentity;
+
+ /* This function is use for debugging purpose only and may go away at anytime without warning */
+ void TimeStamp(in string label, in boolean resetTime);
+
+ /* This attribute is use for debugging purposes for determining whether to PR_LOG or not */
+ readonly attribute boolean logComposePerformance;
+
+ [noscript] boolean determineComposeHTML(in nsIMsgIdentity aIdentity, in MSG_ComposeFormat aFormat);
+
+ /**
+ * given a mailto url, parse the attributes and turn them into a nsIMsgComposeParams object
+ * @return nsIMsgComposeParams which corresponds to the passed in mailto url
+ */
+ nsIMsgComposeParams getParamsForMailto(in nsIURI aURI);
+
+ /**
+ * @{
+ * These constants control how to forward messages in forwardMessage.
+ * kForwardAsDefault uses value of pref "mail.forward_message_mode".
+ */
+ const unsigned long kForwardAsDefault = 0;
+ const unsigned long kForwardAsAttachment = 1;
+ const unsigned long kForwardInline = 2;
+ /** @} */
+
+ /**
+ * Allow filters to automatically forward a message to the given address(es).
+ * @param forwardTo the address(es) to forward to
+ * @param msgHdr the header of the message being replied to
+ * @param msgWindow message window to use
+ * @param server server to use for determining which account to send from
+ * @param aForwardType - How to forward the message one of 3 values:
+ * kForwardAsDefault, kForwardInline, or
+ * kForwardAsAttachment.
+ */
+ void forwardMessage(in AString forwardTo, in nsIMsgDBHdr msgHdr,
+ in nsIMsgWindow msgWindow, in nsIMsgIncomingServer server,
+ in unsigned long aForwardType);
+
+ /**
+ * Allow filters to automatically reply to a message. The reply message is
+ * based on the given template.
+ * @param msgHdr the header of the message being replied to
+ * @param templateUri uri of the template to base ther reply on
+ * @param msgWindow message window to use
+ * @param server server to use for determining which account to send from
+ */
+ void replyWithTemplate(in nsIMsgDBHdr msgHdr, in AUTF8String templateUri,
+ in nsIMsgWindow msgWindow, in nsIMsgIncomingServer server);
+
+ /**
+ * The docShell of each editor element used for composing should be registered
+ * with this service. docShells passed to initCompose get registered
+ * automatically. The registrations are typically used to get the msgCompose
+ * window when determining what remote content to allow to be displayed.
+ *
+ * @param aDocShell The nsIDocShell of the editor element.
+ * @param aMsgCompose The compose object associated with the compose window
+ */
+ void registerComposeDocShell(in nsIDocShell aDocShell,
+ in nsIMsgCompose aMsgCompose);
+
+ /**
+ * When an editor docShell is being closed, you should
+ * unregister it from this service. nsIMsgCompose normally calls this
+ * automatically for items passed to initCompose.
+ *
+ * @param aDocShell The nsIDocShell of the editor element.
+ */
+ void unregisterComposeDocShell(in nsIDocShell aDocShell);
+
+ /**
+ * For a given docShell, returns the nsIMsgCompose object associated with it.
+ *
+ * @param aDocShell The nsIDocShell of the editor element.
+ *
+ * @return NS_ERROR_FAILURE if we could not find a nsIMsgCompose for
+ * the passed in docShell.
+ */
+ nsIMsgCompose getMsgComposeForDocShell(in nsIDocShell aDocShell);
+};
diff --git a/comm/mailnews/compose/public/nsIMsgCopy.idl b/comm/mailnews/compose/public/nsIMsgCopy.idl
new file mode 100644
index 0000000000..338e7234de
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgCopy.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgFolder.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgSend.idl"
+
+/**
+ * The contract ID for this component is @mozilla.org/messengercompose/msgcopy;1.
+ */
+[scriptable, uuid(de03b16f-3a41-40d0-a487-ca21abcf2bee)]
+interface nsIMsgCopy : nsISupports {
+ /**
+ * Start the process of copying a message file to a message folder. The
+ * destinationfolder depends on pref and deliver mode.
+ *
+ * @param aUserIdentity The identity of the sender
+ * @param aFile The message file
+ * @param aMode The deliver mode
+ * @param aMsgSendObj The nsIMsgSend instance that listens to copy events
+ * @param aSavePref The folder uri on server
+ * @param aMsgToReplace The message to replace when copying
+ */
+ void startCopyOperation(in nsIMsgIdentity aUserIdentity,
+ in nsIFile aFile,
+ in nsMsgDeliverMode aMode,
+ in nsIMsgSend aMsgSendObj,
+ in AUTF8String aSavePref,
+ in nsIMsgDBHdr aMsgToReplace);
+
+ /**
+ * Destination folder of the copy operation. Used when aborting copy operation.
+ */
+ readonly attribute nsIMsgFolder dstFolder;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgQuote.idl b/comm/mailnews/compose/public/nsIMsgQuote.idl
new file mode 100644
index 0000000000..ae8fef2ab9
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgQuote.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "nsIMsgQuotingOutputStreamListener.idl"
+#include "nsIChannel.idl"
+#include "nsIMimeStreamConverter.idl"
+
+interface nsIMsgDBHdr;
+
+[scriptable, uuid(f79b1d55-f546-4ed5-9f75-9428e35c4eff)]
+interface nsIMsgQuote : nsISupports {
+
+ /**
+ * Quote a particular message specified by its URI.
+ *
+ * @param charset optional parameter - if set, force the message to be
+ * quoted using this particular charset
+ */
+ void quoteMessage(in AUTF8String msgURI, in boolean quoteHeaders,
+ in nsIMsgQuotingOutputStreamListener streamListener,
+ in bool autodetectCharset, in boolean headersOnly,
+ in nsIMsgDBHdr aOrigHdr);
+
+ readonly attribute nsIMimeStreamConverterListener quoteListener;
+ readonly attribute nsIChannel quoteChannel;
+ readonly attribute nsIMsgQuotingOutputStreamListener streamListener;
+};
+
+[scriptable, uuid(1EC75AD9-88DE-11d3-989D-001083010E9B)]
+interface nsIMsgQuoteListener : nsIMimeStreamConverterListener
+{
+ attribute nsIMsgQuote msgQuote;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl b/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl
new file mode 100644
index 0000000000..ac86361ab5
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsIStreamListener.idl"
+
+interface nsIMimeHeaders;
+
+[scriptable, uuid(1fe345e6-2428-4a43-a0c6-d2acea0d4da4)]
+interface nsIMsgQuotingOutputStreamListener : nsIStreamListener {
+
+ // The headers are used to fill in the reply's compose fields
+ void setMimeHeaders(in nsIMimeHeaders headers);
+
+};
diff --git a/comm/mailnews/compose/public/nsIMsgSend.idl b/comm/mailnews/compose/public/nsIMsgSend.idl
new file mode 100644
index 0000000000..9d4639925d
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgSend.idl
@@ -0,0 +1,374 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * The nsIMsgSend method will create an RFC822 message and send it all in one operation
+ * as well as providing the ability to save disk files for later use. The mode of delivery
+ * can also be specified for the "Send Later", "Drafts" and "Templates" operations. (NOTE:
+ * This method could easily be broken in to a few different calls. Currently, this method
+ * does several functions depending on the arguments passed in, but this could easily lead
+ * to confusion. This is something that very well may change as time allows).
+ */
+#include "nsISupports.idl"
+#include "nsrootidl.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgCompFields.idl"
+#include "nsIMsgSendListener.idl"
+#include "nsIMsgSendReport.idl"
+#include "domstubs.idl"
+#include "nsIPrompt.idl"
+#include "MailNewsTypes2.idl"
+#include "nsIMsgComposeParams.idl"
+
+interface nsIMsgProgress;
+interface nsIURI;
+interface nsIRequest;
+interface nsIMsgDBHdr;
+interface nsIMsgHdr;
+interface nsIFile;
+interface nsIOutputStream;
+interface nsIMsgComposeSecure;
+interface nsIMsgStatusFeedback;
+interface nsIEditor;
+interface mozIDOMWindowProxy;
+
+typedef long nsMsgDeliverMode;
+
+[scriptable, uuid(c658cd1f-dc4a-43c0-911c-c6d3e569ca7e)]
+interface nsIMsgAttachmentData : nsISupports
+{
+ /// The URL to attach.
+ attribute nsIURI url;
+
+ /**
+ * The type to which this document should be
+ * converted. Legal values are NULL, TEXT_PLAIN
+ * and APPLICATION_POSTSCRIPT (which are macros
+ * defined in net.h); other values are ignored.
+ */
+ attribute ACString desiredType;
+
+ /**
+ * The type of the URL if known, otherwise empty. For example, if
+ * you were attaching a temp file which was known to contain HTML data,
+ * you would pass in TEXT_HTML as the realType, to override whatever type
+ * the name of the tmp file might otherwise indicate.
+ */
+ attribute ACString realType;
+
+ /// Goes along with real_type.
+ attribute ACString realEncoding;
+
+ /**
+ * The original name of this document, which will eventually show up in the
+ * Content-Disposition header. For example, if you had copied a document to a
+ * tmp file, this would be the original, human-readable name of the document.
+ */
+ attribute ACString realName;
+ /**
+ * If you put a string here, it will show up as the Content-Description
+ * header. This can be any explanatory text; it's not a file name.
+ */
+ attribute ACString description;
+
+ /// mac-specific info
+ attribute ACString xMacType;
+
+ /// mac-specific info
+ attribute ACString xMacCreator;
+};
+
+/**
+ * When we have downloaded a URL to a tmp file for attaching, this
+ * represents everything we learned about it (and did to it) in the
+ * process.
+ */
+[scriptable, uuid(c552345d-c74b-40b0-a673-79bb461e920b)]
+interface nsIMsgAttachedFile : nsISupports
+{
+ /// Where it came from on the network (or even elsewhere on the local disk.)
+ attribute nsIURI origUrl;
+
+ /// The tmp file in which the (possibly converted) data now resides.
+ attribute nsIFile tmpFile;
+
+ /// The type of the data in file_name (not necessarily the same as the type of orig_url.)
+ attribute ACString type;
+
+ /**
+ * The encoding of the tmp file. This will be set only if the original
+ * document had an encoding already; we don't do base64 encoding and so forth
+ * until it's time to assemble a full MIME message of all parts.
+ */
+ attribute ACString encoding;
+ /// For Content-Description header.
+ attribute ACString description;
+
+ /// X-Mozilla-Cloud-Part, if any.
+ attribute ACString cloudPartInfo;
+
+ attribute ACString xMacType; // mac-specific info
+ attribute ACString xMacCreator; // mac-specific info
+ attribute ACString realName; // The real name of the file.
+
+ /**
+ * Some statistics about the data that was written to the file, so that when
+ * it comes time to compose a MIME message, we can make an informed decision
+ * about what Content-Transfer-Encoding would be best for this attachment.
+ * (If it's encoded already, we ignore this information and ship it as-is.)
+ */
+ attribute unsigned long size;
+ attribute unsigned long unprintableCount;
+ attribute unsigned long highbitCount;
+ attribute unsigned long ctlCount;
+ attribute unsigned long nullCount;
+ attribute unsigned long maxLineLength;
+};
+
+/**
+ * This interface is used by Outlook import to shuttle embedded
+ * image information over to nsIMsgSend's createRFC822Message method via
+ * the aEmbbeddedObjects parameter.
+ */
+[scriptable, uuid(5d2c6554-b4c8-4d68-b864-50e0df929707)]
+interface nsIMsgEmbeddedImageData : nsISupports
+{
+ attribute nsIURI uri;
+ attribute ACString cid;
+ attribute ACString name;
+};
+
+[ptr] native nsMsgAttachedFile(nsMsgAttachedFile);
+
+[scriptable, uuid(747fdfa2-1754-4282-ab26-1e55fd8de13c)]
+interface nsIMsgSend : nsISupports
+{
+ //
+ // This is the primary interface for creating and sending RFC822 messages
+ // in the new architecture. Currently, this method supports many arguments
+ // that change the behavior of the operation. This will change in time to
+ // be separate calls that will be more singluar in nature.
+ //
+ // NOTE: when aEditor is non-null, a multipart related MHTML message will
+ // be created
+ //
+
+ /// Send the message straight away.
+ const nsMsgDeliverMode nsMsgDeliverNow = 0;
+ /**
+ * Queue the message for sending later, but then wait for the user to
+ * request to send it.
+ */
+ const nsMsgDeliverMode nsMsgQueueForLater = 1;
+ const nsMsgDeliverMode nsMsgSave = 2;
+ const nsMsgDeliverMode nsMsgSaveAs = 3;
+ const nsMsgDeliverMode nsMsgSaveAsDraft = 4;
+ const nsMsgDeliverMode nsMsgSaveAsTemplate = 5;
+ const nsMsgDeliverMode nsMsgSendUnsent = 6;
+
+ /// Queue the message in the unsent folder and send it in the background.
+ const nsMsgDeliverMode nsMsgDeliverBackground = 8;
+
+ /**
+ * Create an rfc822 message and send it.
+ * @param aEditor nsIEditor instance that contains message. May be a dummy,
+ * especially in the case of import.
+ * @param aUserIdentity identity to send from.
+ * @param aAccountKey account we're sending message from. May be null.
+ * @param aFields composition fields from addressing widget
+ * @param aIsDigest is this a digest message?
+ * @param aDontDeliver Set to false by the import code - used when we're
+ * trying to create a message from parts.
+ * @param aMode delivery mode
+ * @param aMsgToReplace e.g., when saving a draft over an old draft. May be 0
+ * @param aBodyType content type of message body
+ * @param aBody message body text (should have native line endings)
+ * @param aParentWindow compose window; may be null.
+ * @param aProgress where to send progress info; may be null.
+ * @param aListener optional listener for send progress
+ * @param aPassword optional smtp server password
+ * @param aOriginalMsgURI may be null.
+ * @param aType see nsIMsgComposeParams.idl
+ */
+ Promise createAndSendMessage(in nsIEditor aEditor,
+ in nsIMsgIdentity aUserIdentity,
+ in string aAccountKey,
+ in nsIMsgCompFields aFields,
+ in boolean aIsDigest,
+ in boolean aDontDeliver,
+ in nsMsgDeliverMode aMode,
+ in nsIMsgDBHdr aMsgToReplace,
+ in string aBodyType,
+ in AString aBody,
+ in mozIDOMWindowProxy aParentWindow,
+ in nsIMsgProgress aProgress,
+ in nsIMsgSendListener aListener,
+ in AString aPassword,
+ in AUTF8String aOriginalMsgURI,
+ in MSG_ComposeType aType);
+
+ /**
+ * Creates a file containing an rfc822 message, using the passed information.
+ * aListener's OnStopSending method will get called with the file the message
+ * was stored in. OnStopSending may be called sync or async, depending on
+ * content, so you need to handle both cases.
+ *
+ * @param aUserIdentity The user identity to use for sending this email.
+ * @param aFields An nsIMsgCompFields object containing information
+ * on who to send the message to.
+ * @param aBodyType content type of message body
+ * @param aBody message body text (should have native line endings)
+ * @param aCreateAsDraft If true, this message will be put in a drafts folder
+ * @param aAttachments Array of nsIMsgAttachedFile objects
+ * @param aEmbeddedObjects Array of nsIMsgEmbeddedImageData objects for
+ * MHTML messages.
+ * @param aListener listener for msg creation progress and resulting file.
+ */
+ void createRFC822Message(in nsIMsgIdentity aUserIdentity,
+ in nsIMsgCompFields aFields,
+ in string aBodyType,
+ in ACString aBody,
+ in boolean aCreateAsDraft,
+ in Array<nsIMsgAttachedFile> aAttachments,
+ in Array<nsIMsgEmbeddedImageData> aEmbeddedObjects,
+ in nsIMsgSendListener aListener);
+
+ /**
+ * Sends a file to the specified composition fields, via the user identity
+ * provided.
+ *
+ * @param aUserIdentity The user identity to use for sending this email.
+ * @param aAccountKey The key of the account that this message relates
+ * to.
+ * @param aFields An nsIMsgCompFields object containing information
+ * on who to send the message to.
+ * @param aSendIFile A reference to the file to send.
+ * @param aDeleteSendFileOnCompletion
+ * Set to true if you want the send file deleted once
+ * the message has been sent.
+ * @param aDigest If this is a multipart message, this param
+ * specifies whether the message is in digest or mixed
+ * format.
+ * @param aMode The delivery mode for sending the message (see
+ * above for values).
+ * @param aMsgToReplace A message header representing a message to be
+ * replaced by the one sent, this param may be null.
+ * @param aListener An nsIMsgSendListener to receive feedback on the
+ * current send status. This parameter can also
+ * support the nsIMsgCopyServiceListener interface to
+ * receive notifications of copy finishing e.g. after
+ * saving a message to the sent mail folder.
+ * This param may be null.
+ * @param aStatusFeedback A feedback listener for slightly different feedback
+ * on the message send status. This param may be null.
+ * @param aPassword Pass this in to prevent a dialog if the password
+ * is needed for secure transmission.
+ */
+ Promise sendMessageFile(in nsIMsgIdentity aUserIdentity,
+ in string aAccountKey,
+ in nsIMsgCompFields aFields,
+ in nsIFile aSendIFile,
+ in boolean aDeleteSendFileOnCompletion,
+ in boolean aDigest,
+ in nsMsgDeliverMode aMode,
+ in nsIMsgDBHdr aMsgToReplace,
+ in nsIMsgSendListener aListener,
+ in nsIMsgStatusFeedback aStatusFeedback,
+ in wstring aPassword
+ );
+
+ /* Abort current send/save operation */
+ void abort();
+
+ /**
+ * Report a send failure.
+ *
+ * @param aFailureCode The failure code of the send operation. See
+ * nsComposeStrings.h for possible values. NS_OK is a possible
+ * value as well; if passed, the function won't prompt the user * but will still about the session.
+ * @param aErrorMsg The appropriate error string for the failure.
+ * @result A modified result value in the case a user action results in
+ * a different way to handle the failure.
+ */
+ nsresult fail(in nsresult aFailureCode, in wstring aErrorMsg);
+
+ /* Disable UI notification (alert message) */
+ void setGUINotificationState(in boolean aEnableFlag);
+
+ /* Crypto */
+ void BeginCryptoEncapsulation();
+
+ /* retrieve the last send process report*/
+ readonly attribute nsIMsgSendReport sendReport;
+
+ /* methods for send listener ... */
+ void notifyListenerOnStartSending(in string aMsgID, in unsigned long aMsgSize);
+ void notifyListenerOnProgress(in string aMsgID, in unsigned long aProgress, in unsigned long aProgressMax);
+ void notifyListenerOnStatus(in string aMsgID, in wstring aMsg);
+ void notifyListenerOnStopSending(in string aMsgID, in nsresult aStatus, in wstring aMsg, in nsIFile returnFile);
+ void notifyListenerOnTransportSecurityError(in string msgID, in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location);
+ void deliverAsMailExit(in nsIURI aUrl, in nsresult aExitCode);
+ void deliverAsNewsExit(in nsIURI aUrl, in nsresult aExitCode);
+
+ void sendDeliveryCallback(in nsIURI aUrl, in boolean inIsNewsDelivery, in nsresult aExitCode);
+
+ /* methods for copy listener ... */
+ void notifyListenerOnStartCopy();
+ void notifyListenerOnProgressCopy(in unsigned long aProgress, in unsigned long aProgressMax);
+ void notifyListenerOnStopCopy(in nsresult aStatus);
+ void getMessageId(out ACString messageID);
+ /// When saving as draft, the folder uri we saved to.
+ readonly attribute AUTF8String folderUri;
+
+ /**
+ * After a draft is saved, use this to get the mime part number for the dom
+ * node in the editor embedded object list with the passed in index.
+ *
+ * @param aDomIndex - index in the editor dom embedded object list of
+ * the part we're interested in. These are generally images.
+ *
+ * @return the mime part number for that object.
+ */
+ ACString getPartForDomIndex(in long aDomIndex);
+
+ attribute nsMsgKey messageKey;
+
+ /* process attachment */
+ void gatherMimeAttachments();
+ readonly attribute boolean processAttachmentsSynchronously;
+ readonly attribute unsigned long attachmentCount;
+ attribute unsigned long pendingAttachmentCount;
+ readonly attribute nsMsgDeliverMode deliveryMode;
+
+ nsIMsgProgress getProgress();
+
+ nsIOutputStream getOutputStream();
+
+ attribute nsIRequest runningRequest;
+
+ attribute nsresult status;
+
+ attribute nsIMsgComposeSecure cryptoclosure;
+
+ /// Access the local copy of the composition fields.
+ readonly attribute nsIMsgCompFields sendCompFields;
+
+ /// The message body.
+ readonly attribute AString sendBody;
+
+ /// The type of the message body (typically text/plain or text/html).
+ readonly attribute ACString sendBodyType;
+
+ /// The identity to use to send the message.
+ readonly attribute nsIMsgIdentity identity;
+
+ /// The folder name to which the message will be saved,
+ /// used by error reporting.
+ attribute AString savedToFolderName;
+
+ /// Should we deliver this message (versus saving as a file)?
+ attribute boolean dontDeliver;
+
+};
diff --git a/comm/mailnews/compose/public/nsIMsgSendLater.idl b/comm/mailnews/compose/public/nsIMsgSendLater.idl
new file mode 100644
index 0000000000..65fecebced
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgSendLater.idl
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIStreamListener.idl"
+
+interface nsIMsgStatusFeedback;
+interface nsIMsgIdentity;
+interface nsIMsgSendLaterListener;
+interface nsIMsgFolder;
+
+/**
+ * nsIMsgSendLater is a service used for sending messages in the background.
+ * Messages should be saved to an identity's unsent messages folder, and then
+ * can be sent by calling sendUnsentMessages.
+ *
+ * Although the service supports passing identities as parameters, until bug
+ * 317803 is fixed, all identities use the same folder, and hence the option
+ * currently doesn't work.
+ */
+[scriptable, uuid(fa324a4b-4b87-4e9a-a3c0-af9071a358df)]
+interface nsIMsgSendLater : nsIStreamListener
+{
+ /// Used to obtain status feedback for when messages are sent.
+ attribute nsIMsgStatusFeedback statusFeedback;
+
+ /**
+ * Sends any unsent messages in the identity's unsent messages folder.
+ *
+ * @param aIdentity The identity to send messages for.
+ */
+ void sendUnsentMessages(in nsIMsgIdentity aIdentity);
+
+ /**
+ * Adds an listener to the service to receive notifications.
+ *
+ * @param aListener The listener to add.
+ */
+ void addListener(in nsIMsgSendLaterListener aListener);
+
+ /**
+ * Removes a listener from the service.
+ *
+ * @param aListener The listener to remove.
+ * @exception NS_ERROR_INVALID_ARG If the listener was not already added to
+ * the service.
+ */
+ void removeListener(in nsIMsgSendLaterListener aListener);
+
+ /**
+ * Returns the unsent messages folder for the identity.
+ */
+ nsIMsgFolder getUnsentMessagesFolder(in nsIMsgIdentity userIdentity);
+
+ /**
+ * Returns true if there are any unsent messages to send.
+ *
+ * @param aIdentity The identity whose folder to check for unsent messages.
+ * If not specified, all unsent message folders are checked.
+ */
+ boolean hasUnsentMessages([optional] in nsIMsgIdentity aIdentity);
+
+ /// Returns true if the service is currently sending messages.
+ readonly attribute boolean sendingMessages;
+};
diff --git a/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl b/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl
new file mode 100644
index 0000000000..6e6e29a794
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgIdentity;
+
+/**
+ * Implement this interface and add to nsIMsgSendLater to receive notifications
+ * of send later actions.
+ */
+[scriptable, uuid(a7bc603b-d0da-4959-a82f-4b99c138b9f4)]
+interface nsIMsgSendLaterListener : nsISupports {
+ /**
+ * Notify the observer that the operation of sending messages later has
+ * started.
+ *
+ * @param aTotalMessageCount Number of messages to be sent. This will not
+ * change over the time we are doing this sequence.
+ */
+ void onStartSending(in unsigned long aTotalMessageCount);
+
+ /**
+ * Notify the observer that the next message send/copy is starting and
+ * provide details about the message.
+ *
+ * @param aCurrentMessage The current message number that is being sent.
+ * @param aTotalMessageCount The total number of messages that we are
+ * trying to send.
+ * @param aMessageHeader The header information for the message that is
+ * being sent.
+ * @param aMessageIdentity The identity being used to send the message.
+ */
+ void onMessageStartSending(in unsigned long aCurrentMessage,
+ in unsigned long aTotalMessageCount,
+ in nsIMsgDBHdr aMessageHeader,
+ in nsIMsgIdentity aIdentity);
+
+ /**
+ * Notify the observer of the current progress of sending a message. The one
+ * function covers sending the message over the network and copying to the
+ * appropriate sent folder.
+ *
+ * @param aCurrentMessage The current message number that is being sent.
+ * @param aTotalMessageCount The total number of messages that we are
+ * trying to send.
+ * @param aMessageSendPercent The percentage of the message sent (0 to 100)
+ * @param aMessageCopyPercent The percentage of the copy completed (0 to
+ * 100). If there is no copy for this message,
+ * this may be set to 100 at the same time as
+ * aMessageSendPercent.
+ */
+ void onMessageSendProgress(in unsigned long aCurrentMessage,
+ in unsigned long aTotalMessageCount,
+ in unsigned long aMessageSendPercent,
+ in unsigned long aMessageCopyPercent);
+
+ /**
+ * Notify the observer of an error in the send message later function.
+ *
+ * @param aCurrentMessage The current message number that is being sent.
+ * @param aMessageHeader The header information for the message that is
+ * being sent.
+ * @param aStatus The error status code.
+ * @param aMsg A text string describing the error.
+ */
+ void onMessageSendError(in unsigned long aCurrentMessage,
+ in nsIMsgDBHdr aMessageHeader,
+ in nsresult aStatus,
+ in wstring aMsg);
+
+ /**
+ * Notify the observer that the send unsent messages operation has finished.
+ * This is called regardless of the success/failure of the operation.
+ *
+ * @param aStatus Status code for the message send.
+ * @param aMsg A text string describing the error.
+ * @param aTotalTried Total number of messages that were attempted to be sent.
+ * @param aSuccessful How many messages were successfully sent.
+ */
+ void onStopSending(in nsresult aStatus, in wstring aMsg,
+ in unsigned long aTotalTried, in unsigned long aSuccessful);
+};
diff --git a/comm/mailnews/compose/public/nsIMsgSendListener.idl b/comm/mailnews/compose/public/nsIMsgSendListener.idl
new file mode 100644
index 0000000000..0bca8cceb0
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgSendListener.idl
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsITransportSecurityInfo;
+
+[scriptable, uuid(D34DC178-5E78-45E8-8658-A8F52D9CCF5F)]
+interface nsIMsgSendListener : nsISupports {
+
+ /**
+ * Notify the observer that the message has started to be delivered. This method is
+ * called only once, at the beginning of a message send operation.
+ *
+ * @return The return value is currently ignored. In the future it may be
+ * used to cancel the URL load..
+ */
+ void onStartSending(in string aMsgID, in uint32_t aMsgSize);
+
+ /**
+ * Notify the observer that progress as occurred for the message send
+ */
+ void onProgress(in string aMsgID, in uint32_t aProgress, in uint32_t aProgressMax);
+
+ /**
+ * Notify the observer with a status message for the message send
+ */
+ void onStatus(in string aMsgID, in wstring aMsg);
+
+ /**
+ * Notify the observer that the message has been sent. This method is
+ * called once when the networking library has finished processing the
+ * message.
+ *
+ * This method is called regardless of whether the the operation was successful.
+ * aMsgID The message id for the mail message
+ * status Status code for the message send.
+ * msg A text string describing the error.
+ * returnFileSpec The returned file spec for save to file operations.
+ */
+ void onStopSending(in string aMsgID, in nsresult aStatus, in wstring aMsg,
+ in nsIFile aReturnFile);
+
+ /**
+ * Notify the observer with the message id and the folder uri before the draft
+ * is copied.
+ */
+ void onGetDraftFolderURI(in string aMsgID, in AUTF8String aFolderURI);
+
+ /**
+ * Notify the observer when the user aborts the send without actually doing the send
+ * eg : by closing the compose window without Send.
+ */
+ void onSendNotPerformed(in string aMsgID, in nsresult aStatus);
+
+ /**
+ * Notify that an NSS security error has occurred during the send
+ * (e.g. Bad Certificate or SSL version failure).
+ * This callback is invoked before onStopSending(), in case a listener
+ * needs the securityInfo - most likely to get at a failed certificate,
+ * allowing the user to add an exception.
+ * onStopSending() will still be called after this, so a listener
+ * which doesn't need special NSS handling can just leave this callback as
+ * an empty function and leave the handling to onStopSending().
+ *
+ * @param {string} msgID - The message ID.
+ * @param {nsresult} status - The error code (it will be in the NSS error
+ * code range).
+ * @param {nsITransportSecurityInfo} secInfo
+ * - Security info for the failed operation.
+ * @param {ACString} location - The location of the failed operation
+ * ("<host>:<port>")
+ */
+ void onTransportSecurityError(in string msgID, in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location);
+
+};
diff --git a/comm/mailnews/compose/public/nsIMsgSendReport.idl b/comm/mailnews/compose/public/nsIMsgSendReport.idl
new file mode 100644
index 0000000000..bcdf8e7265
--- /dev/null
+++ b/comm/mailnews/compose/public/nsIMsgSendReport.idl
@@ -0,0 +1,47 @@
+/* -*- Mode: idl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+[scriptable, uuid(2ec81175-bc65-44b9-ba87-462bc3f938db)]
+interface nsIMsgProcessReport : nsISupports {
+
+ attribute boolean proceeded;
+ attribute nsresult error;
+ attribute wstring message;
+
+ void reset();
+};
+
+[scriptable, uuid(428c5bde-29f5-4bfe-830a-ec795a1c2975)]
+interface nsIMsgSendReport : nsISupports {
+
+ const long process_Current = -1;
+ const long process_BuildMessage = 0;
+ const long process_NNTP = 1;
+ const long process_SMTP = 2;
+ const long process_Copy = 3;
+ const long process_Filter = 4;
+ const long process_FCC = 5;
+
+ attribute long deliveryMode; /* see nsMsgDeliverMode in nsIMsgSend.idl for valid value */
+ attribute long currentProcess;
+
+ void reset();
+
+ void setProceeded(in long process, in boolean proceeded);
+ void setError(in long process, in nsresult error, in boolean overwriteError);
+ void setMessage(in long process, in wstring message, in boolean overwriteMessage);
+
+ nsIMsgProcessReport getProcessReport(in long process);
+
+ /* Display Report will ananlyze data collected during the send and will show the most appropriate error.
+ Also it will return the error code. In case of no error or if the error has been canceld, it will return
+ NS_OK.
+ */
+ nsresult displayReport(in mozIDOMWindowProxy prompt, in boolean showErrorOnly, in boolean dontShowReportTwice);
+};
diff --git a/comm/mailnews/compose/public/nsISmtpServer.idl b/comm/mailnews/compose/public/nsISmtpServer.idl
new file mode 100644
index 0000000000..6180e9289d
--- /dev/null
+++ b/comm/mailnews/compose/public/nsISmtpServer.idl
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIAuthPrompt;
+interface nsIUrlListener;
+interface nsIURI;
+interface nsIMsgWindow;
+
+/**
+ * This interface represents a single SMTP Server. A SMTP server instance may be
+ * created/obtained from nsIMsgAccountManager.
+ *
+ * Most of the attributes will set/get preferences from the main preferences
+ * file.
+ */
+[scriptable, uuid(a53dce6c-cd81-495c-83bc-45a65df1f08e)]
+interface nsISmtpServer : nsISupports {
+
+ /// A unique identifier for the server.
+ attribute string key;
+
+ /**
+ * A unique identifier for this server that can be used for the same
+ * server synced across multiple profiles. Auto-generated on first use.
+ */
+ attribute AUTF8String UID;
+
+ /// A user supplied description for the server.
+ attribute AUTF8String description;
+
+ /// The server's hostname.
+ attribute AUTF8String hostname;
+
+ /// The server's port.
+ attribute int32_t port;
+
+ /// The username to access the server with (if required)
+ attribute ACString username;
+
+ /**
+ * The CLIENTID to use for this server (if required).
+ * @see https://tools.ietf.org/html/draft-storey-smtp-client-id-05
+ */
+ attribute ACString clientid;
+
+ /**
+ * Whether the CLIENTID feature above is enabled.
+ */
+ attribute boolean clientidEnabled;
+
+ /**
+ * The password to access the server with (if required).
+ *
+ * @note this is stored within the server instance but not within preferences.
+ * It can be specified/saved here to avoid prompting the user constantly for
+ * the sending password.
+ */
+ attribute AString password;
+
+ /// Returns a displayname of the format hostname:port or just hostname
+ readonly attribute string displayname;
+
+ /**
+ * Authentication mechanism.
+ *
+ * @see nsMsgAuthMethod (in MailNewsTypes2.idl)
+ * Same as "mail.smtpserver...authMethod" pref
+ *
+ * Compatibility note: This attribute had a different meaning in TB < 3.1
+ */
+ attribute nsMsgAuthMethodValue authMethod;
+
+ /**
+ * Whether to SSL or STARTTLS or not
+ *
+ * @see nsMsgSocketType (in MailNewsTypes2.idl)
+ * Same as "mail.smtpserver...try_ssl" pref
+ */
+ attribute nsMsgSocketTypeValue socketType;
+
+ /**
+ * May contain an alternative argument to EHLO or HELO to provide to the
+ * server. Reflects the value of the mail.smtpserver.*.hello_argument pref.
+ * This is mainly useful where ISPs don't bother providing PTR records for
+ * their servers and therefore users get an error on sending. See bug 244030
+ * for more discussion.
+ */
+ readonly attribute ACString helloArgument;
+
+ /// Returns the URI of the server (smtp:///)
+ readonly attribute AUTF8String serverURI;
+
+ /** Limit of concurrent connections to a server. */
+ attribute long maximumConnectionsNumber;
+
+ /** Close cached server connections. */
+ void closeCachedConnections();
+
+ /**
+ * Gets a password for this server, using a UI prompt if necessary.
+ *
+ * @param promptString The string to prompt the user with when asking for
+ * the password.
+ * @param promptTitle The title of the prompt.
+ * @return The password to use (may be null if no password was
+ * obtained).
+ */
+ AString getPasswordWithUI(in wstring promptString, in wstring promptTitle);
+
+ /**
+ * Gets a username and password for this server, using a UI prompt if
+ * necessary.
+ *
+ * @param promptString The string to prompt the user with when asking for
+ * the password.
+ * @param promptTitle The title of the prompt.
+ * @param netPrompt An nsIAuthPrompt instance to use for the password
+ * prompt.
+ * @param userid The username to use (may be null if no password was
+ * obtained).
+ * @param password The password to use (may be empty if no password was
+ * obtained).
+ */
+ void getUsernamePasswordWithUI(in wstring promptString, in wstring promptTitle,
+ in nsIAuthPrompt netPrompt, out ACString userid,
+ out AString password);
+
+ /**
+ * Calling this will *remove* the saved password for this server from the
+ * password manager and from the stored value.
+ */
+ void forgetPassword();
+
+ /**
+ * Verify that we can logon
+ *
+ * @param aPassword - password to use
+ * @param aUrlListener - gets called back with success or failure.
+ * @return - the url that we run.
+ *
+ */
+ nsIURI verifyLogon(in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ /// Call this to clear all preference values for this server.
+ void clearAllValues();
+};
diff --git a/comm/mailnews/compose/public/nsISmtpService.idl b/comm/mailnews/compose/public/nsISmtpService.idl
new file mode 100644
index 0000000000..0fc306fe11
--- /dev/null
+++ b/comm/mailnews/compose/public/nsISmtpService.idl
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsISmtpServer;
+interface nsIURI;
+interface nsIUrlListener;
+interface nsIMsgIdentity;
+interface nsIInterfaceRequestor;
+interface nsIFile;
+interface nsIMsgStatusFeedback;
+interface nsIRequest;
+interface nsIMsgWindow;
+
+[scriptable, uuid(1b11b532-1527-4fc0-a00f-4ce7e6886419)]
+interface nsISmtpService : nsISupports {
+ /**
+ * Sends a mail message via the given parameters. This function builds an
+ * SMTP URL and makes an SMTP connection, and then runs the url.
+ * The SMTP server defined
+ * in the aSenderIdentity object (see nsIMsgIdentity) will be used to send
+ * the message. If there is no SMTP server defined in aSenderIdentity, the
+ * default SMTP server will be used.
+ *
+ * @note The file to send must be in the format specified by RFC 2822 for
+ * sending data. This includes having the correct CRLF line endings
+ * throughout the file, and the <CRLF>.<CRLF> at the end of the file.
+ * sendMailMessage does no processing/additions on the file.
+ *
+ * @param aFilePath The file to send.
+ * @param aRecipients A comma delimited list of recipients.
+ * @param aSenderIdentity The identity of the sender.
+ * @param aSender The senders email address.
+ * @param aPassword Pass this in to prevent a dialog if the
+ * password is needed for secure transmission.
+ * @param aUrlListener A listener to listen to the URL being run,
+ * this parameter may be null.
+ * @param aStatusListener A feedback listener for slightly different
+ * feedback on the message send status. This
+ * parameter may be null.
+ * @param aNotificationCallbacks More notification callbacks
+ * @param aRequestDSN Pass true to request Delivery Status
+ * Notification.
+ * @param aMessageId The message id can be used as ENVID for DSN.
+ * @param aURL Provides a handle on the running url. You
+ * can later interrupt the action by asking the
+ * netlib service manager to interrupt the url
+ * you are given back. This parameter may be
+ * null.
+ * @param aRequest Provides a handle to the running request.
+ * This parameter may be null.
+ */
+ void sendMailMessage(in nsIFile aFilePath, in string aRecipients,
+ in nsIMsgIdentity aSenderIdentity,
+ in string aSender,
+ in AString aPassword,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgStatusFeedback aStatusListener,
+ in nsIInterfaceRequestor aNotificationCallbacks,
+ in boolean aRequestDSN,
+ in ACString aMessageId,
+ out nsIURI aURL,
+ out nsIRequest aRequest);
+
+ /**
+ * Verifies that we can logon to the server with given password
+ *
+ * @param aSmtpServer Server to try to logon to.
+ * @param aUrlListener Listener that will get notified whether logon
+ * was successful or not.
+ * @param aMsgWindow nsIMsgWindow to use for notification callbacks.
+ * @return - the url that we run.
+ */
+ nsIURI verifyLogon(in nsISmtpServer aServer, in nsIUrlListener aListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Return the SMTP server that is associated with an identity.
+ * @param aSenderIdentity the identity
+ */
+ nsISmtpServer getServerByIdentity(in nsIMsgIdentity aSenderIdentity);
+
+ /**
+ * A copy of the array of SMTP servers, as stored in the preferences
+ */
+ readonly attribute Array<nsISmtpServer> servers;
+
+ /**
+ * The default server, across sessions of the app
+ * (eventually there will be a session default which does not
+ * persist past shutdown)
+ */
+ attribute nsISmtpServer defaultServer;
+
+ /**
+ * The "session default" server - this is never saved, and only used
+ * for the current session. Always falls back to the default server
+ * unless explicitly set.
+ */
+ attribute nsISmtpServer sessionDefaultServer;
+
+ /**
+ * Create a new SMTP server.
+ * Use this instead of createInstance(), so that the SMTP Service can
+ * be aware of this server
+ */
+ nsISmtpServer createServer();
+
+ /**
+ * Find the first server with the given hostname and/or username.
+ * Note: if either username or hostname is empty, then that parameter will
+ * not be used in the matching process.
+ * @param username the username for the server
+ * @param hostname the hostname of the server
+ * @returns null if no server is found
+ */
+ nsISmtpServer findServer(in string username, in string hostname);
+
+ /**
+ * Look up the server with the given key.
+ */
+ nsISmtpServer getServerByKey(in string key);
+
+ /**
+ * Delete the given server from the server list - does nothing if the server
+ * does not exist
+ * @param server the server to delete. Use findServer() if you only know
+ * the hostname
+ */
+ void deleteServer(in nsISmtpServer server);
+};
diff --git a/comm/mailnews/compose/public/nsISmtpUrl.idl b/comm/mailnews/compose/public/nsISmtpUrl.idl
new file mode 100644
index 0000000000..c03bc7f4e6
--- /dev/null
+++ b/comm/mailnews/compose/public/nsISmtpUrl.idl
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgComposeParams.idl"
+
+interface nsIMsgIdentity;
+interface nsIPrompt;
+interface nsIAuthPrompt;
+interface nsISmtpServer;
+interface nsIInterfaceRequestor;
+interface nsIFile;
+
+[scriptable, uuid(da22b8ac-059d-4f82-bf99-f5f3d3c8202d)]
+interface nsISmtpUrl : nsISupports {
+ /**
+ * SMTP Parse specific getters.
+ * These retrieve various parts from the url.
+ */
+
+ /**
+ * This list is a list of all recipients to send the email to.
+ * Each name is NULL terminated.
+ */
+ attribute string recipients;
+
+ attribute boolean PostMessage;
+
+ /**
+ * The message can be stored in a file, to allow accessors for getting and
+ * setting the file name to post.
+ */
+ attribute nsIFile postMessageFile;
+
+ attribute boolean requestDSN;
+
+ /**
+ * The envid which is used in the DSN.
+ */
+ attribute ACString dsnEnvid;
+
+ /**
+ * The sender of this mail (can be different from identity).
+ */
+ attribute string sender;
+
+ /**
+ * SMTP Url instance specific getters and setters
+ * Information the protocol needs to know in order to run the url.
+ * These are NOT event sinks which are things the caller needs to know.
+ */
+
+ /**
+ * By default the url is really a bring up the compose window mailto url.
+ * You need to call this function if you want to force the message to be
+ * posted to the mailserver.
+ */
+
+ /**
+ * The user's full name and user's email address are encapsulated in the
+ * senderIdentity.
+ * (the user's domain name can be glopped from the user's email address)
+ *
+ * NOTE: the SMTP username and SMTP server are in the smtp url
+ * smtp://sspitzer@tintin/...
+ */
+ attribute nsIMsgIdentity senderIdentity;
+ attribute nsIPrompt prompt;
+ attribute nsIAuthPrompt authPrompt;
+ attribute nsIInterfaceRequestor notificationCallbacks;
+ attribute nsISmtpServer smtpServer;
+
+ attribute boolean verifyLogon; // we're just verifying the ability to logon
+
+ /// Constant for the default SMTP port number
+ const int32_t DEFAULT_SMTP_PORT = 25;
+
+ /// Constant for the default SMTP over ssl port number
+ const int32_t DEFAULT_SMTPS_PORT = 465;
+};
+
+[scriptable, uuid(87c36c23-4bc2-4992-b338-69f88f6ed0a1)]
+interface nsIMailtoUrl : nsISupports {
+ /**
+ * mailto: parse specific getters
+ *
+ * All of these fields are things we can effectively extract from a
+ * mailto url if it contains all of these values
+ *
+ * Note: Attachments aren't available because that would expose a potential
+ * security hole (see bug 99055).
+ *
+ * These items are in one function as we only ever get them from the one
+ * place and all at the same time.
+ */
+ void getMessageContents(out AUTF8String aToPart, out AUTF8String aCcPart,
+ out AUTF8String aBccPart, out AUTF8String aSubjectPart,
+ out AUTF8String aBodyPart, out AUTF8String aHtmlPart,
+ out ACString aReferencePart,
+ out AUTF8String aNewsgroupPart,
+ out MSG_ComposeFormat aFormat);
+
+ /**
+ * These attributes are available should mailnews or extensions want them
+ * but aren't used by standard in mailnews.
+ */
+ readonly attribute AUTF8String fromPart;
+ readonly attribute AUTF8String followUpToPart;
+ readonly attribute AUTF8String organizationPart;
+ readonly attribute AUTF8String replyToPart;
+ readonly attribute AUTF8String priorityPart;
+ readonly attribute AUTF8String newsHostPart;
+};
diff --git a/comm/mailnews/compose/src/MailtoProtocolHandler.jsm b/comm/mailnews/compose/src/MailtoProtocolHandler.jsm
new file mode 100644
index 0000000000..a129e7d750
--- /dev/null
+++ b/comm/mailnews/compose/src/MailtoProtocolHandler.jsm
@@ -0,0 +1,38 @@
+/* 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 = ["MailtoProtocolHandler"];
+
+/**
+ * Protocol handler for mailto: url.
+ *
+ * @implements {nsIProtocolHandler}
+ */
+class MailtoProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolHandler]);
+
+ scheme = "mailto";
+ allowPort = false;
+
+ newChannel(uri, loadInfo) {
+ // Create an empty pipe to get an inputStream.
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ pipe.outputStream.close();
+
+ // Create a channel so that we can set contentType onto it.
+ let streamChannel = Cc[
+ "@mozilla.org/network/input-stream-channel;1"
+ ].createInstance(Ci.nsIInputStreamChannel);
+ streamChannel.setURI(uri);
+ streamChannel.contentStream = pipe.inputStream;
+
+ let channel = streamChannel.QueryInterface(Ci.nsIChannel);
+ // With this set, a nsIContentHandler instance will take over to open a
+ // compose window.
+ channel.contentType = "application/x-mailto";
+ channel.loadInfo = loadInfo;
+ return channel;
+ }
+}
diff --git a/comm/mailnews/compose/src/MessageSend.jsm b/comm/mailnews/compose/src/MessageSend.jsm
new file mode 100644
index 0000000000..914694fa79
--- /dev/null
+++ b/comm/mailnews/compose/src/MessageSend.jsm
@@ -0,0 +1,1434 @@
+/* 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 = ["MessageSend"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+ MimeMessage: "resource:///modules/MimeMessage.jsm",
+ MsgUtils: "resource:///modules/MimeMessageUtils.jsm",
+});
+
+// nsMsgKey_None from MailNewsTypes.h.
+const nsMsgKey_None = 0xffffffff;
+
+/**
+ * A class to manage sending processes.
+ *
+ * @implements {nsIMsgSend}
+ * @implements {nsIWebProgressListener}
+ */
+class MessageSend {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgSend",
+ "nsIWebProgressListener",
+ ]);
+ classID = Components.ID("{028b9c1e-8d0a-4518-80c2-842e07846eaa}");
+
+ async createAndSendMessage(
+ editor,
+ userIdentity,
+ accountKey,
+ compFields,
+ isDigest,
+ dontDeliver,
+ deliverMode,
+ msgToReplace,
+ bodyType,
+ body,
+ parentWindow,
+ progress,
+ listener,
+ smtpPassword,
+ originalMsgURI,
+ compType
+ ) {
+ this._userIdentity = userIdentity;
+ this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity);
+ this._compFields = compFields;
+ this._dontDeliver = dontDeliver;
+ this._deliverMode = deliverMode;
+ this._msgToReplace = msgToReplace;
+ this._sendProgress = progress;
+ this._smtpPassword = smtpPassword;
+ this._sendListener = listener;
+ this._parentWindow = parentWindow;
+ this._originalMsgURI = originalMsgURI;
+ this._shouldRemoveMessageFile = true;
+
+ this._sendReport = Cc[
+ "@mozilla.org/messengercompose/sendreport;1"
+ ].createInstance(Ci.nsIMsgSendReport);
+ this._composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+
+ // Initialize the error reporting mechanism.
+ this.sendReport.reset();
+ this.sendReport.deliveryMode = deliverMode;
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("assemblingMailInformation")
+ );
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage;
+
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("assemblingMessage")
+ );
+
+ this._fcc = lazy.MsgUtils.getFcc(
+ userIdentity,
+ compFields,
+ originalMsgURI,
+ compType
+ );
+ let { embeddedAttachments, embeddedObjects } =
+ this._gatherEmbeddedAttachments(editor);
+
+ let bodyText = this._getBodyFromEditor(editor) || body;
+ // Convert to a binary string. This is because MimeMessage requires it and:
+ // 1. An attachment content is BinaryString.
+ // 2. Body text and attachment contents are handled in the same way by
+ // MimeEncoder to pick encoding and encode.
+ bodyText = lazy.jsmime.mimeutils.typedArrayToString(
+ new TextEncoder().encode(bodyText)
+ );
+
+ this._restoreEditorContent(embeddedObjects);
+ this._message = new lazy.MimeMessage(
+ userIdentity,
+ compFields,
+ this._fcc,
+ bodyType,
+ bodyText,
+ deliverMode,
+ originalMsgURI,
+ compType,
+ embeddedAttachments,
+ this.sendReport
+ );
+
+ this._messageKey = nsMsgKey_None;
+
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("creatingMailMessage")
+ );
+ lazy.MsgUtils.sendLogger.debug("Creating message file");
+ let messageFile;
+ try {
+ // Create a local file from MimeMessage, then pass it to _deliverMessage.
+ messageFile = await this._message.createMessageFile();
+ } catch (e) {
+ lazy.MsgUtils.sendLogger.error(e);
+ let errorMsg = "";
+ if (e.result == lazy.MsgUtils.NS_MSG_ERROR_ATTACHING_FILE) {
+ errorMsg = this._composeBundle.formatStringFromName(
+ "errorAttachingFile",
+ [e.data.name || e.data.url]
+ );
+ }
+ this.fail(e.result || Cr.NS_ERROR_FAILURE, errorMsg);
+ this.notifyListenerOnStopSending(null, e.result, null, null);
+ return null;
+ }
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("assemblingMessageDone")
+ );
+ lazy.MsgUtils.sendLogger.debug("Message file created");
+ return this._deliverMessage(messageFile);
+ }
+
+ sendMessageFile(
+ userIdentity,
+ accountKey,
+ compFields,
+ messageFile,
+ deleteSendFileOnCompletion,
+ digest,
+ deliverMode,
+ msgToReplace,
+ listener,
+ statusFeedback,
+ smtpPassword
+ ) {
+ this._userIdentity = userIdentity;
+ this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity);
+ this._compFields = compFields;
+ this._deliverMode = deliverMode;
+ this._msgToReplace = msgToReplace;
+ this._smtpPassword = smtpPassword;
+ this._sendListener = listener;
+ this._statusFeedback = statusFeedback;
+ this._shouldRemoveMessageFile = deleteSendFileOnCompletion;
+
+ this._sendReport = Cc[
+ "@mozilla.org/messengercompose/sendreport;1"
+ ].createInstance(Ci.nsIMsgSendReport);
+ this._composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+
+ // Initialize the error reporting mechanism.
+ this.sendReport.reset();
+ this.sendReport.deliveryMode = deliverMode;
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("assemblingMailInformation")
+ );
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage;
+
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("assemblingMessage")
+ );
+
+ this._fcc = lazy.MsgUtils.getFcc(
+ userIdentity,
+ compFields,
+ null,
+ Ci.nsIMsgCompType.New
+ );
+
+ // nsMsgKey_None from MailNewsTypes.h.
+ this._messageKey = 0xffffffff;
+
+ return this._deliverMessage(messageFile);
+ }
+
+ // @see nsIMsgSend
+ createRFC822Message(
+ userIdentity,
+ compFields,
+ bodyType,
+ bodyText,
+ isDraft,
+ attachedFiles,
+ embeddedObjects,
+ listener
+ ) {
+ this._userIdentity = userIdentity;
+ this._compFields = compFields;
+ this._dontDeliver = true;
+ this._sendListener = listener;
+
+ this._sendReport = Cc[
+ "@mozilla.org/messengercompose/sendreport;1"
+ ].createInstance(Ci.nsIMsgSendReport);
+ this._composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+
+ // Initialize the error reporting mechanism.
+ this.sendReport.reset();
+ let deliverMode = isDraft
+ ? Ci.nsIMsgSend.nsMsgSaveAsDraft
+ : Ci.nsIMsgSend.nsMsgDeliverNow;
+ this.sendReport.deliveryMode = deliverMode;
+
+ // Convert nsIMsgAttachedFile[] to nsIMsgAttachment[]
+ for (let file of attachedFiles) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.name = file.realName;
+ attachment.url = file.origUrl.spec;
+ attachment.contentType = file.type;
+ compFields.addAttachment(attachment);
+ }
+
+ // Convert nsIMsgEmbeddedImageData[] to nsIMsgAttachment[]
+ let embeddedAttachments = embeddedObjects.map(obj => {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.name = obj.name;
+ attachment.contentId = obj.cid;
+ attachment.url = obj.uri.spec;
+ return attachment;
+ });
+
+ this._message = new lazy.MimeMessage(
+ userIdentity,
+ compFields,
+ null,
+ bodyType,
+ bodyText,
+ deliverMode,
+ null,
+ Ci.nsIMsgCompType.New,
+ embeddedAttachments,
+ this.sendReport
+ );
+
+ this._messageKey = nsMsgKey_None;
+
+ // Create a local file from MimeMessage, then pass it to _deliverMessage.
+ this._message
+ .createMessageFile()
+ .then(messageFile => this._deliverMessage(messageFile));
+ }
+
+ // nsIWebProgressListener.
+ onLocationChange(webProgress, request, location, flags) {}
+ onProgressChange(
+ webProgress,
+ request,
+ curSelfProgress,
+ maxSelfProgress,
+ curTotalProgress,
+ maxTotalProgress
+ ) {}
+ onStatusChange(webProgress, request, status, message) {}
+ onSecurityChange(webProgress, request, state) {}
+ onContentBlockingEvent(webProgress, request, event) {}
+ onStateChange(webProgress, request, stateFlags, status) {
+ if (
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ !Components.isSuccessCode(status)
+ ) {
+ lazy.MsgUtils.sendLogger.debug("onStateChange with failure. Aborting.");
+ this._isRetry = false;
+ this.abort();
+ }
+ }
+
+ abort() {
+ if (this._aborting) {
+ return;
+ }
+ this._aborting = true;
+ if (this._smtpRequest?.value) {
+ this._smtpRequest.value.cancel(Cr.NS_ERROR_ABORT);
+ this._smtpRequest = null;
+ }
+ if (this._msgCopy) {
+ MailServices.copy.notifyCompletion(
+ this._copyFile,
+ this._msgCopy.dstFolder,
+ Cr.NS_ERROR_ABORT
+ );
+ } else {
+ // If already in the fcc step, notifyListenerOnStopCopy will do the clean up.
+ this._cleanup();
+ }
+ if (!this._failed) {
+ // Emit stopsending event if the sending is cancelled by user, so that
+ // listeners can do necessary clean up, e.g. reset the sending button.
+ this.notifyListenerOnStopSending(null, Cr.NS_ERROR_ABORT, null, null);
+ }
+ this._aborting = false;
+ }
+
+ fail(exitCode, errorMsg) {
+ this._failed = true;
+ if (!Components.isSuccessCode(exitCode) && exitCode != Cr.NS_ERROR_ABORT) {
+ lazy.MsgUtils.sendLogger.error(
+ `Sending failed; ${errorMsg}, exitCode=${exitCode}, originalMsgURI=${this._originalMsgURI}`
+ );
+ this._sendReport.setError(
+ Ci.nsIMsgSendReport.process_Current,
+ exitCode,
+ false
+ );
+ if (errorMsg) {
+ this._sendReport.setMessage(
+ Ci.nsIMsgSendReport.process_Current,
+ errorMsg,
+ false
+ );
+ }
+ exitCode = this._sendReport.displayReport(this._parentWindow, true, true);
+ }
+ this.abort();
+
+ return exitCode;
+ }
+
+ getPartForDomIndex(domIndex) {
+ throw Components.Exception(
+ "getPartForDomIndex not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ getProgress() {
+ return this._sendProgress;
+ }
+
+ /**
+ * NOTE: This is a copy of the C++ code, msgId and msgSize are only
+ * placeholders. Maybe refactor this after nsMsgSend is gone.
+ */
+ notifyListenerOnStartSending(msgId, msgSize) {
+ lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartSending");
+ if (this._sendListener) {
+ this._sendListener.onStartSending(msgId, msgSize);
+ }
+ }
+
+ notifyListenerOnStartCopy() {
+ lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartCopy");
+ if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) {
+ this._sendListener.OnStartCopy();
+ }
+ }
+
+ notifyListenerOnProgressCopy(progress, progressMax) {
+ lazy.MsgUtils.sendLogger.debug("notifyListenerOnProgressCopy");
+ if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) {
+ this._sendListener.OnProgress(progress, progressMax);
+ }
+ }
+
+ notifyListenerOnStopCopy(status) {
+ lazy.MsgUtils.sendLogger.debug(
+ `notifyListenerOnStopCopy; status=${status}`
+ );
+ this._msgCopy = null;
+
+ if (!this._isRetry) {
+ let statusMsgEntry = Components.isSuccessCode(status)
+ ? "copyMessageComplete"
+ : "copyMessageFailed";
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName(statusMsgEntry)
+ );
+ } else if (Components.isSuccessCode(status)) {
+ // We got here via retry and the save to sent, drafts or template
+ // succeeded so take down our progress dialog. We don't need it any more.
+ this._sendProgress.unregisterListener(this);
+ this._sendProgress.closeProgressDialog(false);
+ this._isRetry = false;
+ }
+
+ if (!Components.isSuccessCode(status)) {
+ let localFoldersAccountName =
+ MailServices.accounts.localFoldersServer.prettyName;
+ let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
+ let accountName = folder?.server.prettyName;
+ if (!this._fcc || !localFoldersAccountName || !accountName) {
+ this.fail(Cr.NS_OK, null);
+ return;
+ }
+
+ let params = [folder.name, accountName, localFoldersAccountName];
+ let promptMsg;
+ switch (this._deliverMode) {
+ case Ci.nsIMsgSend.nsMsgDeliverNow:
+ case Ci.nsIMsgSend.nsMsgSendUnsent:
+ promptMsg = this._composeBundle.formatStringFromName(
+ "promptToSaveSentLocally2",
+ params
+ );
+ break;
+ case Ci.nsIMsgSend.nsMsgSaveAsDraft:
+ promptMsg = this._composeBundle.formatStringFromName(
+ "promptToSaveDraftLocally2",
+ params
+ );
+ break;
+ case Ci.nsIMsgSend.nsMsgSaveAsTemplate:
+ promptMsg = this._composeBundle.formatStringFromName(
+ "promptToSaveTemplateLocally2",
+ params
+ );
+ break;
+ }
+ if (promptMsg) {
+ let showCheckBox = { value: false };
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING +
+ Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE +
+ Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_SAVE;
+ let dialogTitle =
+ this._composeBundle.GetStringFromName("SaveDialogTitle");
+ let buttonLabelRety =
+ this._composeBundle.GetStringFromName("buttonLabelRetry2");
+ let buttonPressed = Services.prompt.confirmEx(
+ this._parentWindow,
+ dialogTitle,
+ promptMsg,
+ buttonFlags,
+ buttonLabelRety,
+ null,
+ null,
+ null,
+ showCheckBox
+ );
+ if (buttonPressed == 0) {
+ // retry button clicked
+ // Check we have a progress dialog.
+ if (
+ this._sendProgress.processCanceledByUser &&
+ Services.prefs.getBoolPref("mailnews.show_send_progress")
+ ) {
+ let progress = Cc[
+ "@mozilla.org/messenger/progress;1"
+ ].createInstance(Ci.nsIMsgProgress);
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeprogressparameters;1"
+ ].createInstance(Ci.nsIMsgComposeProgressParams);
+ params.subject = this._parentWindow.gMsgCompose.compFields.subject;
+ params.deliveryMode = this._deliverMode;
+
+ progress.openProgressDialog(
+ this._parentWindow,
+ this._sendProgress.msgWindow,
+ "chrome://messenger/content/messengercompose/sendProgress.xhtml",
+ false,
+ params
+ );
+
+ progress.onStateChange(
+ null,
+ null,
+ Ci.nsIWebProgressListener.STATE_START,
+ Cr.NS_OK
+ );
+
+ // We want to hear when this is cancelled.
+ progress.registerListener(this);
+
+ this._sendProgress = progress;
+ this._isRetry = true;
+ }
+ // Ensure statusFeedback is set so progress percent bargraph occurs.
+ this._sendProgress.msgWindow.statusFeedback = this._sendProgress;
+
+ this._mimeDoFcc();
+ return;
+ } else if (buttonPressed == 2) {
+ try {
+ // Try to save to Local Folders/<account name>. Pass null to save
+ // to local folders and not the configured fcc.
+ this._mimeDoFcc(null, true, Ci.nsIMsgSend.nsMsgDeliverNow);
+ return;
+ } catch (e) {
+ Services.prompt.alert(
+ this._parentWindow,
+ null,
+ this._composeBundle.GetStringFromName("saveToLocalFoldersFailed")
+ );
+ }
+ }
+ }
+ this.fail(Cr.NS_OK, null);
+ }
+
+ if (
+ !this._fcc2Handled &&
+ this._messageKey != nsMsgKey_None &&
+ [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes(
+ this._deliverMode
+ )
+ ) {
+ try {
+ this._filterSentMessage();
+ } catch (e) {
+ this.onStopOperation(e.result);
+ }
+ return;
+ }
+
+ this._doFcc2();
+ }
+
+ notifyListenerOnStopSending(msgId, status, msg, returnFile) {
+ lazy.MsgUtils.sendLogger.debug(
+ `notifyListenerOnStopSending; status=${status}`
+ );
+ try {
+ this._sendListener?.onStopSending(msgId, status, msg, returnFile);
+ } catch (e) {}
+ }
+
+ notifyListenerOnTransportSecurityError(msgId, status, secInfo, location) {
+ lazy.MsgUtils.sendLogger.debug(
+ `notifyListenerOnTransportSecurityError; status=${status}, location=${location}`
+ );
+ if (!this._sendListener) {
+ return;
+ }
+ try {
+ this._sendListener.onTransportSecurityError(
+ msgId,
+ status,
+ secInfo,
+ location
+ );
+ } catch (e) {}
+ }
+
+ /**
+ * Called by nsIMsgFilterService.
+ */
+ onStopOperation(status) {
+ lazy.MsgUtils.sendLogger.debug(`onStopOperation; status=${status}`);
+ if (Components.isSuccessCode(status)) {
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("filterMessageComplete")
+ );
+ } else {
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("filterMessageFailed")
+ );
+ Services.prompt.alert(
+ this._parentWindow,
+ null,
+ this._composeBundle.GetStringFromName("errorFilteringMsg")
+ );
+ }
+
+ this._doFcc2();
+ }
+
+ /**
+ * Handle the exit code of message delivery.
+ *
+ * @param {nsIURI} url - The delivered message uri.
+ * @param {boolean} isNewsDelivery - The message was delivered to newsgroup.
+ * @param {nsreault} exitCode - The exit code of message delivery.
+ */
+ _deliveryExitProcessing(url, isNewsDelivery, exitCode) {
+ lazy.MsgUtils.sendLogger.debug(
+ `Delivery exit processing; exitCode=${exitCode}`
+ );
+ if (!Components.isSuccessCode(exitCode)) {
+ let isNSSError = false;
+ let errorName = lazy.MsgUtils.getErrorStringName(exitCode);
+ let errorMsg;
+ if (
+ [
+ lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER,
+ lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED,
+ lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED,
+ lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT,
+ lazy.MsgUtils.NS_ERROR_SMTP_PASSWORD_UNDEFINED,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_GSSAPI,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL,
+ lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT,
+ lazy.MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS,
+ ].includes(exitCode)
+ ) {
+ errorMsg = lazy.MsgUtils.formatStringWithSMTPHostName(
+ this._userIdentity,
+ this._composeBundle,
+ errorName
+ );
+ } else {
+ let nssErrorsService = Cc[
+ "@mozilla.org/nss_errors_service;1"
+ ].getService(Ci.nsINSSErrorsService);
+ try {
+ // This is a server security issue as determined by the Mozilla
+ // platform. To the Mozilla security message string, appended a string
+ // having additional information with the server name encoded.
+ errorMsg = nssErrorsService.getErrorMessage(exitCode);
+ errorMsg +=
+ "\n" +
+ lazy.MsgUtils.formatStringWithSMTPHostName(
+ this._userIdentity,
+ this._composeBundle,
+ "smtpSecurityIssue"
+ );
+ isNSSError = true;
+ } catch (e) {
+ if (url.errorMessage) {
+ // url.errorMessage is an already localized message, usually
+ // combined with the error message from SMTP server.
+ errorMsg = url.errorMessage;
+ } else if (errorName != "sendFailed") {
+ // Not the default string. A mailnews error occurred that does not
+ // require the server name to be encoded. Just print the descriptive
+ // string.
+ errorMsg = this._composeBundle.GetStringFromName(errorName);
+ } else {
+ errorMsg = this._composeBundle.GetStringFromName(
+ "sendFailedUnexpected"
+ );
+ // nsIStringBundle.formatStringFromName doesn't work with %X.
+ errorMsg.replace("%X", `0x${exitCode.toString(16)}`);
+ errorMsg =
+ "\n" +
+ lazy.MsgUtils.formatStringWithSMTPHostName(
+ this._userIdentity,
+ this._composeBundle,
+ "smtpSendFailedUnknownReason"
+ );
+ }
+ }
+ }
+ if (isNSSError) {
+ let u = url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ this.notifyListenerOnTransportSecurityError(
+ null,
+ exitCode,
+ u.failedSecInfo,
+ u.asciiHostPort
+ );
+ }
+ this.notifyListenerOnStopSending(null, exitCode, null, null);
+ this.fail(exitCode, errorMsg);
+ return;
+ }
+
+ if (
+ isNewsDelivery &&
+ (this._compFields.to || this._compFields.cc || this._compFields.bcc)
+ ) {
+ this._deliverAsMail();
+ return;
+ }
+
+ this.notifyListenerOnStopSending(
+ this._compFields.messageId,
+ exitCode,
+ null,
+ null
+ );
+
+ this._doFcc();
+ }
+
+ sendDeliveryCallback(url, isNewsDelivery, exitCode) {
+ if (isNewsDelivery) {
+ if (
+ !Components.isSuccessCode(exitCode) &&
+ exitCode != Cr.NS_ERROR_ABORT &&
+ !lazy.MsgUtils.isMsgError(exitCode)
+ ) {
+ exitCode = lazy.MsgUtils.NS_ERROR_POST_FAILED;
+ }
+ return this._deliveryExitProcessing(url, isNewsDelivery, exitCode);
+ }
+ if (!Components.isSuccessCode(exitCode)) {
+ switch (exitCode) {
+ case Cr.NS_ERROR_UNKNOWN_HOST:
+ case Cr.NS_ERROR_UNKNOWN_PROXY_HOST:
+ exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER;
+ break;
+ case Cr.NS_ERROR_CONNECTION_REFUSED:
+ case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
+ exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED;
+ break;
+ case Cr.NS_ERROR_NET_INTERRUPT:
+ exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED;
+ break;
+ case Cr.NS_ERROR_NET_TIMEOUT:
+ case Cr.NS_ERROR_NET_RESET:
+ exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT;
+ break;
+ default:
+ break;
+ }
+ }
+ return this._deliveryExitProcessing(url, isNewsDelivery, exitCode);
+ }
+
+ get folderUri() {
+ return this._folderUri;
+ }
+
+ /**
+ * @type {nsMsgKey}
+ */
+ set messageKey(key) {
+ this._messageKey = key;
+ }
+
+ /**
+ * @type {nsMsgKey}
+ */
+ get messageKey() {
+ return this._messageKey;
+ }
+
+ get sendReport() {
+ return this._sendReport;
+ }
+
+ _setStatusMessage(msg) {
+ if (this._sendProgress) {
+ this._sendProgress.onStatusChange(null, null, Cr.NS_OK, msg);
+ }
+ }
+
+ /**
+ * Deliver a message.
+ *
+ * @param {nsIFile} file - The message file to deliver.
+ */
+ async _deliverMessage(file) {
+ if (this._dontDeliver) {
+ this.notifyListenerOnStopSending(null, Cr.NS_OK, null, file);
+ return;
+ }
+
+ this._messageFile = file;
+ if (
+ [
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ ].includes(this._deliverMode)
+ ) {
+ await this._mimeDoFcc();
+ return;
+ }
+
+ let warningSize = Services.prefs.getIntPref(
+ "mailnews.message_warning_size"
+ );
+ if (warningSize > 0 && file.fileSize > warningSize) {
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let msg = this._composeBundle.formatStringFromName(
+ "largeMessageSendWarning",
+ [messenger.formatFileSize(file.fileSize)]
+ );
+ if (!Services.prompt.confirm(this._parentWindow, null, msg)) {
+ this.fail(lazy.MsgUtils.NS_ERROR_BUT_DONT_SHOW_ALERT, msg);
+ throw Components.Exception(
+ "Cancelled sending large message",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ }
+
+ this._deliveryFile = await this._createDeliveryFile();
+ if (this._compFields.newsgroups) {
+ this._deliverAsNews();
+ return;
+ }
+ await this._deliverAsMail();
+ }
+
+ /**
+ * Strip Bcc header, create the file to be actually delivered.
+ *
+ * @returns {nsIFile}
+ */
+ async _createDeliveryFile() {
+ if (!this._compFields.bcc) {
+ return this._messageFile;
+ }
+ let deliveryFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ deliveryFile.append("nsemail.tmp");
+ deliveryFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ let content = await IOUtils.read(this._messageFile.path);
+ let bodyIndex = content.findIndex(
+ (el, index) =>
+ // header and body are separated by \r\n\r\n
+ el == 13 &&
+ content[index + 1] == 10 &&
+ content[index + 2] == 13 &&
+ content[index + 3] == 10
+ );
+ let header = new TextDecoder("UTF-8").decode(content.slice(0, bodyIndex));
+ let lastLinePruned = false;
+ let headerToWrite = "";
+ for (let line of header.split("\r\n")) {
+ if (line.startsWith("Bcc") || (line.startsWith(" ") && lastLinePruned)) {
+ lastLinePruned = true;
+ continue;
+ }
+ lastLinePruned = false;
+ headerToWrite += `${line}\r\n`;
+ }
+ let encodedHeader = new TextEncoder().encode(headerToWrite);
+ // Prevent extra \r\n, which was already added to the last head line.
+ let body = content.slice(bodyIndex + 2);
+ let combinedContent = new Uint8Array(encodedHeader.length + body.length);
+ combinedContent.set(encodedHeader);
+ combinedContent.set(body, encodedHeader.length);
+ await IOUtils.write(deliveryFile.path, combinedContent);
+ return deliveryFile;
+ }
+
+ /**
+ * Create the file to be copied to the Sent folder, add X-Mozilla-Status and
+ * X-Mozilla-Status2 if needed.
+ *
+ * @returns {nsIFile}
+ */
+ async _createCopyFile() {
+ if (!this._folderUri.startsWith("mailbox:")) {
+ return this._messageFile;
+ }
+
+ // Add a `From - Date` line, so that nsLocalMailFolder.cpp won't add a
+ // dummy envelope. The date string will be parsed by PR_ParseTimeString.
+ // TODO: this should not be added to Maildir, see bug 1686852.
+ let contentToWrite = `From - ${new Date().toUTCString()}\r\n`;
+ let xMozillaStatus = lazy.MsgUtils.getXMozillaStatus(this._deliverMode);
+ let xMozillaStatus2 = lazy.MsgUtils.getXMozillaStatus2(this._deliverMode);
+ if (xMozillaStatus) {
+ contentToWrite += `X-Mozilla-Status: ${xMozillaStatus}\r\n`;
+ }
+ if (xMozillaStatus2) {
+ contentToWrite += `X-Mozilla-Status2: ${xMozillaStatus2}\r\n`;
+ }
+
+ // Create a separate copy file when there are extra headers.
+ let copyFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ copyFile.append("nscopy.tmp");
+ copyFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ await IOUtils.writeUTF8(copyFile.path, contentToWrite);
+ await IOUtils.write(
+ copyFile.path,
+ await IOUtils.read(this._messageFile.path),
+ {
+ mode: "append",
+ }
+ );
+ return copyFile;
+ }
+
+ /**
+ * Start copy operation according to this._fcc value.
+ */
+ async _doFcc() {
+ if (!this._fcc || !lazy.MsgUtils.canSaveToFolder(this._fcc)) {
+ this.notifyListenerOnStopCopy(Cr.NS_OK);
+ return;
+ }
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Copy;
+ this._mimeDoFcc(this._fcc, false, Ci.nsIMsgSend.nsMsgDeliverNow);
+ }
+
+ /**
+ * Copy a message to a folder, or fallback to a folder depending on pref and
+ * deliverMode, usually Drafts/Sent.
+ *
+ * @param {string} [fccHeader=this._fcc] - The target folder uri to copy the
+ * message to.
+ * @param {boolean} [throwOnError=false] - By default notifyListenerOnStopCopy
+ * is called on error. When throwOnError is true, the caller can handle the
+ * error by itself.
+ * @param {nsMsgDeliverMode} [deliverMode=this._deliverMode] - The deliver mode.
+ */
+ async _mimeDoFcc(
+ fccHeader = this._fcc,
+ throwOnError = false,
+ deliverMode = this._deliverMode
+ ) {
+ let folder;
+ let folderUri;
+ if (fccHeader) {
+ folder = lazy.MailUtils.getExistingFolder(fccHeader);
+ }
+ if (
+ [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes(
+ deliverMode
+ ) &&
+ folder
+ ) {
+ this._folderUri = fccHeader;
+ } else if (fccHeader == null) {
+ // Set fcc_header to a special folder in Local Folders "account" since can't
+ // save to Sent mbox, typically because imap connection is down. This
+ // folder is created if it doesn't yet exist.
+ let rootFolder = MailServices.accounts.localFoldersServer.rootMsgFolder;
+ folderUri = rootFolder.URI + "/";
+
+ // Now append the special folder name folder to the local folder uri.
+ if (
+ [
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ Ci.nsIMsgSend.nsMsgSendUnsent,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ ].includes(this._deliverMode)
+ ) {
+ // Typically, this appends "Sent-", "Drafts-" or "Templates-" to folder
+ // and then has the account name appended, e.g., .../Sent-MyImapAccount.
+ let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
+ folderUri += folder.name + "-";
+ }
+ if (this._fcc) {
+ // Get the account name where the "save to" failed.
+ let accountName = lazy.MailUtils.getOrCreateFolder(this._fcc).server
+ .prettyName;
+
+ // Now append the imap account name (escaped) to the folder uri.
+ folderUri += accountName;
+ this._folderUri = folderUri;
+ }
+ } else {
+ this._folderUri = lazy.MsgUtils.getMsgFolderURIFromPrefs(
+ this._userIdentity,
+ this._deliverMode
+ );
+ if (
+ (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft &&
+ this._compFields.draftId) ||
+ (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate &&
+ this._compFields.templateId)
+ ) {
+ // Turn the draft/template ID into a folder URI string.
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ try {
+ // This can fail if the user renames/removed/moved the folder.
+ folderUri = messenger.msgHdrFromURI(
+ this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft
+ ? this._compFields.draftId
+ : this._compFields.templateId
+ ).folder.URI;
+ } catch (ex) {
+ console.warn(ex);
+ }
+ // Only accept it if it's a subfolder of the identity's draft/template folder.
+ if (folderUri?.startsWith(this._folderUri)) {
+ this._folderUri = folderUri;
+ }
+ }
+ }
+ lazy.MsgUtils.sendLogger.debug(
+ `Processing fcc; folderUri=${this._folderUri}`
+ );
+
+ this._msgCopy = Cc[
+ "@mozilla.org/messengercompose/msgcopy;1"
+ ].createInstance(Ci.nsIMsgCopy);
+ this._copyFile = await this._createCopyFile();
+ lazy.MsgUtils.sendLogger.debug("fcc file created");
+
+ // Notify nsMsgCompose about the saved folder.
+ if (this._sendListener) {
+ this._sendListener.onGetDraftFolderURI(
+ this._compFields.messageId,
+ this._folderUri
+ );
+ }
+ folder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
+ let statusMsg = this._composeBundle.formatStringFromName(
+ "copyMessageStart",
+ [folder?.name || "?"]
+ );
+ this._setStatusMessage(statusMsg);
+ lazy.MsgUtils.sendLogger.debug("startCopyOperation");
+ try {
+ this._msgCopy.startCopyOperation(
+ this._userIdentity,
+ this._copyFile,
+ this._deliverMode,
+ this,
+ this._folderUri,
+ this._msgToReplace
+ );
+ } catch (e) {
+ lazy.MsgUtils.sendLogger.warn(
+ `startCopyOperation failed with ${e.result}`
+ );
+ if (throwOnError) {
+ throw Components.Exception("startCopyOperation failed", e.result);
+ }
+ this.notifyListenerOnStopCopy(e.result);
+ }
+ }
+
+ /**
+ * Handle the fcc2 field. Then notify OnStopCopy and clean up.
+ */
+ _doFcc2() {
+ // Handle fcc2 only once.
+ if (!this._fcc2Handled && this._compFields.fcc2) {
+ lazy.MsgUtils.sendLogger.debug("Processing fcc2");
+ this._fcc2Handled = true;
+ this._mimeDoFcc(
+ this._compFields.fcc2,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverNow
+ );
+ return;
+ }
+
+ // NOTE: When nsMsgCompose receives OnStopCopy, it will release nsIMsgSend
+ // instance and close the compose window, which prevents the Promise from
+ // resolving in MsgComposeCommands.js. Use setTimeout to work around it.
+ lazy.setTimeout(() => {
+ try {
+ if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) {
+ this._sendListener.OnStopCopy(0);
+ }
+ } catch (e) {
+ // Ignore the return value of OnStopCopy. Non-zero nsresult will throw
+ // when going through XPConnect. In this case, we don't care about it.
+ console.warn(
+ `OnStopCopy failed with 0x${e.result.toString(16)}\n${e.stack}`
+ );
+ }
+ this._cleanup();
+ });
+ }
+
+ /**
+ * Run filters on the just sent message.
+ */
+ _filterSentMessage() {
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Filter;
+ let folder = lazy.MailUtils.getExistingFolder(this._folderUri);
+ let msgHdr = folder.GetMessageHeader(this._messageKey);
+ let msgWindow = this._sendProgress?.msgWindow;
+ return MailServices.filters.applyFilters(
+ Ci.nsMsgFilterType.PostOutgoing,
+ [msgHdr],
+ folder,
+ msgWindow,
+ this
+ );
+ }
+
+ _cleanup() {
+ lazy.MsgUtils.sendLogger.debug("Clean up temporary files");
+ if (this._copyFile && this._copyFile != this._messageFile) {
+ IOUtils.remove(this._copyFile.path).catch(console.error);
+ this._copyFile = null;
+ }
+ if (this._deliveryFile && this._deliveryFile != this._messageFile) {
+ IOUtils.remove(this._deliveryFile.path).catch(console.error);
+ this._deliveryFile = null;
+ }
+ if (this._messageFile && this._shouldRemoveMessageFile) {
+ IOUtils.remove(this._messageFile.path).catch(console.error);
+ this._messageFile = null;
+ }
+ }
+
+ /**
+ * Send this._deliveryFile to smtp service.
+ */
+ async _deliverAsMail() {
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_SMTP;
+ this._setStatusMessage(
+ this._composeBundle.GetStringFromName("sendingMessage")
+ );
+ let recipients = [
+ this._compFields.to,
+ this._compFields.cc,
+ this._compFields.bcc,
+ ].filter(Boolean);
+ this._collectAddressesToAddressBook(recipients);
+ let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService(
+ Ci.nsIMimeConverter
+ );
+ let encodedRecipients = encodeURIComponent(
+ converter.encodeMimePartIIStr_UTF8(
+ recipients.join(","),
+ true,
+ 0,
+ Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE
+ )
+ );
+ lazy.MsgUtils.sendLogger.debug(
+ `Delivering mail message <${this._compFields.messageId}>`
+ );
+ let deliveryListener = new MsgDeliveryListener(this, false);
+ let msgStatus =
+ this._sendProgress instanceof Ci.nsIMsgStatusFeedback
+ ? this._sendProgress
+ : this._statusFeedback;
+ this._smtpRequest = {};
+ // Do async call. This is necessary to ensure _smtpRequest is set so that
+ // cancel function can be obtained.
+ await MailServices.smtp.wrappedJSObject.sendMailMessage(
+ this._deliveryFile,
+ encodedRecipients,
+ this._userIdentity,
+ this._compFields.from,
+ this._smtpPassword,
+ deliveryListener,
+ msgStatus,
+ null,
+ this._compFields.DSN,
+ this._compFields.messageId,
+ {},
+ this._smtpRequest
+ );
+ }
+
+ /**
+ * Send this._deliveryFile to nntp service.
+ */
+ _deliverAsNews() {
+ this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_NNTP;
+ lazy.MsgUtils.sendLogger.debug("Delivering news message");
+ let deliveryListener = new MsgDeliveryListener(this, true);
+ let msgWindow;
+ try {
+ msgWindow =
+ this._sendProgress?.msgWindow ||
+ MailServices.mailSession.topmostMsgWindow;
+ } catch (e) {}
+ MailServices.nntp.postMessage(
+ this._deliveryFile,
+ this._compFields.newsgroups,
+ this._accountKey,
+ deliveryListener,
+ msgWindow,
+ null
+ );
+ }
+
+ /**
+ * Collect outgoing addresses to address book.
+ *
+ * @param {string[]} recipients - Outgoing addresses including to/cc/bcc.
+ */
+ _collectAddressesToAddressBook(recipients) {
+ let createCard = Services.prefs.getBoolPref(
+ "mail.collect_email_address_outgoing",
+ false
+ );
+
+ let addressCollector = Cc[
+ "@mozilla.org/addressbook/services/addressCollector;1"
+ ].getService(Ci.nsIAbAddressCollector);
+ for (let recipient of recipients) {
+ addressCollector.collectAddress(recipient, createCard);
+ }
+ }
+
+ /**
+ * Check if link text is equivalent to the href.
+ *
+ * @param {string} text - The innerHTML of a <a> element.
+ * @param {string} href - The href of a <a> element.
+ * @returns {boolean} true if text is equivalent to href.
+ */
+ _isLinkFreeText(text, href) {
+ href = href.trim();
+ if (href.startsWith("mailto:")) {
+ return this._isLinkFreeText(text, href.slice("mailto:".length));
+ }
+ text = text.trim();
+ return (
+ text == href ||
+ (text.endsWith("/") && text.slice(0, -1) == href) ||
+ (href.endsWith("/") && href.slice(0, -1) == text)
+ );
+ }
+
+ /**
+ * Collect embedded objects as attachments.
+ *
+ * @returns {object} collected
+ * @returns {nsIMsgAttachment[]} collected.embeddedAttachments
+ * @returns {object[]} collected.embeddedObjects objects {element, url}
+ */
+ _gatherEmbeddedAttachments(editor) {
+ let embeddedAttachments = [];
+ let embeddedObjects = [];
+
+ if (!editor || !editor.document) {
+ return { embeddedAttachments, embeddedObjects };
+ }
+ let nodes = [];
+ nodes.push(...editor.document.querySelectorAll("img"));
+ nodes.push(...editor.document.querySelectorAll("a"));
+ let body = editor.document.querySelector("body[background]");
+ if (body) {
+ nodes.push(body);
+ }
+
+ let urlCidCache = {};
+ for (let element of nodes) {
+ if (element.tagName == "A" && element.href) {
+ if (this._isLinkFreeText(element.innerHTML, element.href)) {
+ // Set this special classname, which is recognized by nsIParserUtils,
+ // so that links are not duplicated in text/plain.
+ element.classList.add("moz-txt-link-freetext");
+ }
+ }
+ let isImage = false;
+ let url;
+ let name;
+ let mozDoNotSend = element.getAttribute("moz-do-not-send");
+ if (mozDoNotSend && mozDoNotSend != "false") {
+ // Only empty or moz-do-not-send="false" may be accepted later.
+ continue;
+ }
+ if (element.tagName == "BODY" && element.background) {
+ isImage = true;
+ url = element.background;
+ } else if (element.tagName == "IMG" && element.src) {
+ isImage = true;
+ url = element.src;
+ name = element.name;
+ } else if (element.tagName == "A" && element.href) {
+ url = element.href;
+ name = element.name;
+ } else {
+ continue;
+ }
+ let acceptObject = false;
+ // Before going further, check what scheme we're dealing with. Files need to
+ // be converted to data URLs during composition. "Attaching" means
+ // sending as a cid: part instead of original URL.
+ if (/^https?:\/\//i.test(url)) {
+ acceptObject =
+ (isImage &&
+ Services.prefs.getBoolPref(
+ "mail.compose.attach_http_images",
+ false
+ )) ||
+ mozDoNotSend == "false";
+ }
+ if (/^(data|news|snews|nntp):/i.test(url)) {
+ acceptObject = true;
+ }
+ if (!acceptObject) {
+ continue;
+ }
+
+ let cid;
+ if (urlCidCache[url]) {
+ // If an url has already been inserted as MimePart, just reuse the cid.
+ cid = urlCidCache[url];
+ } else {
+ cid = lazy.MsgUtils.makeContentId(
+ this._userIdentity,
+ embeddedAttachments.length + 1
+ );
+ urlCidCache[url] = cid;
+
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.name = name || lazy.MsgUtils.pickFileNameFromUrl(url);
+ attachment.contentId = cid;
+ attachment.url = url;
+ embeddedAttachments.push(attachment);
+ }
+ embeddedObjects.push({
+ element,
+ url,
+ });
+
+ let newUrl = `cid:${cid}`;
+ if (element.tagName == "BODY") {
+ element.background = newUrl;
+ } else if (element.tagName == "IMG") {
+ element.src = newUrl;
+ } else if (element.tagName == "A") {
+ element.href = newUrl;
+ }
+ }
+ return { embeddedAttachments, embeddedObjects };
+ }
+
+ /**
+ * Restore embedded objects in editor to their original urls.
+ *
+ * @param {object[]} embeddedObjects - An array of embedded objects.
+ * @param {Element} embeddedObjects.element
+ * @param {string} embeddedObjects.url
+ */
+ _restoreEditorContent(embeddedObjects) {
+ for (let { element, url } of embeddedObjects) {
+ if (element.tagName == "BODY") {
+ element.background = url;
+ } else if (element.tagName == "IMG") {
+ element.src = url;
+ } else if (element.tagName == "A") {
+ element.href = url;
+ }
+ }
+ }
+
+ /**
+ * Get the message body from an editor.
+ *
+ * @param {nsIEditor} editor - The editor instance.
+ * @returns {string}
+ */
+ _getBodyFromEditor(editor) {
+ if (!editor) {
+ return "";
+ }
+
+ let flags =
+ Ci.nsIDocumentEncoder.OutputFormatted |
+ Ci.nsIDocumentEncoder.OutputNoFormattingInPre |
+ Ci.nsIDocumentEncoder.OutputDisallowLineBreaking;
+ // bodyText is UTF-16 string.
+ let bodyText = editor.outputToString("text/html", flags);
+
+ // No need to do conversion if forcing plain text.
+ if (!this._compFields.forcePlainText) {
+ let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(
+ Ci.mozITXTToHTMLConv
+ );
+ let csFlags = Ci.mozITXTToHTMLConv.kURLs;
+ if (Services.prefs.getBoolPref("mail.send_struct", false)) {
+ csFlags |= Ci.mozITXTToHTMLConv.kStructPhrase;
+ }
+ bodyText = cs.scanHTML(bodyText, csFlags);
+ }
+
+ return bodyText;
+ }
+
+ /**
+ * Get the first account key of an identity.
+ *
+ * @param {nsIMsgIdentity} identity - The identity.
+ * @returns {string}
+ */
+ _accountKeyForIdentity(identity) {
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ return servers.length
+ ? MailServices.accounts.FindAccountForServer(servers[0])?.key
+ : null;
+ }
+}
+
+/**
+ * A listener to be passed to the SMTP service.
+ *
+ * @implements {nsIUrlListener}
+ */
+class MsgDeliveryListener {
+ QueryInterface = ChromeUtils.generateQI(["nsIUrlListener"]);
+
+ /**
+ * @param {nsIMsgSend} msgSend - Send instance to use.
+ * @param {boolean} isNewsDelivery - Whether this is an nntp message delivery.
+ */
+ constructor(msgSend, isNewsDelivery) {
+ this._msgSend = msgSend;
+ this._isNewsDelivery = isNewsDelivery;
+ }
+
+ OnStartRunningUrl(url) {
+ this._msgSend.notifyListenerOnStartSending(null, 0);
+ }
+
+ OnStopRunningUrl(url, exitCode) {
+ lazy.MsgUtils.sendLogger.debug(`OnStopRunningUrl; exitCode=${exitCode}`);
+ let mailUrl = url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ mailUrl.UnRegisterListener(this);
+
+ this._msgSend.sendDeliveryCallback(url, this._isNewsDelivery, exitCode);
+ }
+}
diff --git a/comm/mailnews/compose/src/MimeEncoder.jsm b/comm/mailnews/compose/src/MimeEncoder.jsm
new file mode 100644
index 0000000000..ab4c60de42
--- /dev/null
+++ b/comm/mailnews/compose/src/MimeEncoder.jsm
@@ -0,0 +1,430 @@
+/* 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 = ["MimeEncoder"];
+
+const LINELENGTH_ENCODING_THRESHOLD = 990;
+const MESSAGE_RFC822 = "message/rfc822";
+
+/**
+ * A class to pick Content-Transfer-Encoding for a MimePart, and encode MimePart
+ * body accordingly.
+ */
+class MimeEncoder {
+ /**
+ * Create a MimeEncoder.
+ *
+ * @param {string} charset
+ * @param {string} contentType
+ * @param {boolean} forceMsgEncoding
+ * @param {boolean} isMainBody
+ * @param {string} content
+ */
+ constructor(charset, contentType, forceMsgEncoding, isMainBody, content) {
+ this._charset = charset;
+ this._contentType = contentType.toLowerCase();
+ this._forceMsgEncoding = forceMsgEncoding;
+ this._isMainBody = isMainBody;
+ this._body = content;
+ this._bodySize = content.length;
+
+ // The encoding value will be used to set Content-Transfer-Encoding header
+ // and encode this._body.
+ this._encoding = "";
+
+ // Flags used to pick encoding.
+ this._highBitCount = 0;
+ this._unPrintableCount = 0;
+ this._ctrlCount = 0;
+ this._nullCount = 0;
+ this._hasCr = 0;
+ this._hasLf = 0;
+ this._hasCrLf = 0;
+ this._maxColumn = 0;
+ }
+
+ /**
+ * @type {string}
+ */
+ get encoding() {
+ return this._encoding;
+ }
+
+ /**
+ * Use the combination of charset, content type and scanning this._body to
+ * decide what encoding it should have.
+ */
+ pickEncoding() {
+ this._analyzeBody();
+
+ let strictlyMime = Services.prefs.getBoolPref("mail.strictly_mime");
+ let needsB64 = false;
+ let isUsingQP = false;
+
+ // Allow users to override our percentage-wise guess on whether
+ // the file is text or binary.
+ let forceB64 = Services.prefs.getBoolPref("mail.file_attach_binary");
+
+ // If the content-type is "image/" or something else known to be binary or
+ // several flavors of newlines are present, use base64 unless we're attaching
+ // a message (so that we don't get confused by newline conversions).
+ if (
+ !this._isMainBody &&
+ (forceB64 ||
+ this._requiresB64() ||
+ this._hasCr + this._hasLf + this._hasCrLf != 1) &&
+ this._contentType != MESSAGE_RFC822
+ ) {
+ needsB64 = true;
+ } else {
+ // Otherwise, we need to pick an encoding based on the contents of the
+ // document.
+ let encodeP = false;
+
+ // Force quoted-printable if the sender does not allow conversion to 7bit.
+ if (
+ this._forceMsgEncoding ||
+ this._maxColumn > LINELENGTH_ENCODING_THRESHOLD ||
+ (strictlyMime && this._unPrintableCount) ||
+ this._nullCount
+ ) {
+ if (
+ this._isMainBody &&
+ this._contentType == "text/plain" &&
+ // From rfc3676#section-4.2, Quoted-Printable encoding SHOULD NOT be
+ // used with Format=Flowed unless absolutely necessary.
+ Services.prefs.getBoolPref("mailnews.send_plaintext_flowed")
+ ) {
+ needsB64 = true;
+ } else {
+ encodeP = true;
+ }
+ }
+
+ // MIME requires a special case that these types never be encoded.
+ if (
+ this._contentType.startsWith("message") ||
+ this._contentType.startsWith("multipart")
+ ) {
+ encodeP = false;
+ }
+
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ let isCharsetMultiByte = false;
+ try {
+ isCharsetMultiByte =
+ manager.getCharsetData(this._charset, ".isMultibyte") == "true";
+ } catch {}
+
+ // If the Mail charset is multibyte, we force it to use Base64 for
+ // attachments.
+ if (
+ !this._isMainBody &&
+ this._charset &&
+ isCharsetMultiByte &&
+ (this._contentType.startsWith("text") ||
+ // text/vcard synonym
+ this._contentType == "application/directory")
+ ) {
+ needsB64 = true;
+ } else if (this._charset == "ISO-2022-JP") {
+ this._encoding = "7bit";
+ } else if (encodeP && this._unPrintableCount > this._bodySize / 10) {
+ // If the document contains more than 10% unprintable characters,
+ // then that seems like a good candidate for base64 instead of
+ // quoted-printable.
+ needsB64 = true;
+ } else if (encodeP) {
+ this._encoding = "quoted-printable";
+ isUsingQP = true;
+ } else if (this._highBitCount > 0) {
+ this._encoding = "8bit";
+ } else {
+ this._encoding = "7bit";
+ }
+ }
+
+ // Always base64 binary data.
+ if (needsB64) {
+ this._encoding = "base64";
+ }
+
+ // According to RFC 821 we must always have lines shorter than 998 bytes.
+ // To encode "long lines" use a CTE that will transmit shorter lines.
+ // Switch to base64 if we are not already using "quoted printable".
+
+ // We don't do this for message/rfc822 attachments, since we can't
+ // change the original Content-Transfer-Encoding of the message we're
+ // attaching. We rely on the original message complying with RFC 821,
+ // if it doesn't we won't either. Not ideal.
+ if (
+ this._contentType != MESSAGE_RFC822 &&
+ this._maxColumn > LINELENGTH_ENCODING_THRESHOLD &&
+ !isUsingQP
+ ) {
+ this._encoding = "base64";
+ }
+ }
+
+ /**
+ * Encode this._body according to the value of this.encoding.
+ */
+ encode() {
+ let output;
+ if (this.encoding == "base64") {
+ output = this._encodeBase64();
+ } else if (this.encoding == "quoted-printable") {
+ output = this._encodeQP();
+ } else {
+ output = this._body.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n");
+ }
+ if (!output.endsWith("\r\n")) {
+ output += "\r\n";
+ }
+ return output;
+ }
+
+ /**
+ * Scan this._body to set flags that will be used by pickEncoding.
+ */
+ _analyzeBody() {
+ let currentColumn = 0;
+ let prevCharWasCr = false;
+
+ for (let i = 0; i < this._bodySize; i++) {
+ let ch = this._body.charAt(i);
+ let charCode = this._body.charCodeAt(i);
+ if (charCode > 126) {
+ this._highBitCount++;
+ this._unPrintableCount++;
+ } else if (ch < " " && !"\t\r\n".includes(ch)) {
+ this._unPrintableCount++;
+ this._ctrlCount++;
+ if (ch == "\0") {
+ this._nullCount++;
+ }
+ }
+
+ if ("\r\n".includes(ch)) {
+ if (ch == "\r") {
+ if (prevCharWasCr) {
+ this._hasCr = 1;
+ } else {
+ prevCharWasCr = true;
+ }
+ } else if (prevCharWasCr) {
+ if (currentColumn == 0) {
+ this._hasCrLf = 1;
+ } else {
+ this._hasCr = 1;
+ this._hasLf = 1;
+ }
+ prevCharWasCr = false;
+ } else {
+ this._hasLf = 1;
+ }
+
+ if (this._maxColumn < currentColumn) {
+ this._maxColumn = currentColumn;
+ }
+ currentColumn = 0;
+ } else {
+ currentColumn++;
+ }
+ }
+
+ if (this._maxColumn < currentColumn) {
+ this._maxColumn = currentColumn;
+ }
+ }
+
+ /**
+ * Determine if base64 is required according to contentType.
+ */
+ _requiresB64() {
+ if (this._contentType == "application/x-unknown-content-type") {
+ // Unknown types don't necessarily require encoding. (Note that
+ // "unknown" and "application/octet-stream" aren't the same.)
+ return false;
+ }
+ if (
+ this._contentType.startsWith("image/") ||
+ this._contentType.startsWith("audio/") ||
+ this._contentType.startsWith("video/") ||
+ this._contentType.startsWith("application/")
+ ) {
+ // The following types are application/ or image/ types that are actually
+ // known to contain textual data (meaning line-based, not binary, where
+ // CRLF conversion is desired rather than disastrous.) So, if the type
+ // is any of these, it does not *require* base64, and if we do need to
+ // encode it for other reasons, we'll probably use quoted-printable.
+ // But, if it's not one of these types, then we assume that any subtypes
+ // of the non-"text/" types are binary data, where CRLF conversion would
+ // corrupt it, so we use base64 right off the bat.
+ // The reason it's desirable to ship these as text instead of just using
+ // base64 all the time is mainly to preserve the readability of them for
+ // non-MIME users: if I mail a /bin/sh script to someone, it might not
+ // need to be encoded at all, so we should leave it readable if we can.
+ // This list of types was derived from the comp.mail.mime FAQ, section
+ // 10.2.2, "List of known unregistered MIME types" on 2-Feb-96.
+ const typesWhichAreReallyText = [
+ "application/mac-binhex40", // APPLICATION_BINHEX
+ "application/pgp", // APPLICATION_PGP
+ "application/pgp-keys",
+ "application/x-pgp-message", // APPLICATION_PGP2
+ "application/postscript", // APPLICATION_POSTSCRIPT
+ "application/x-uuencode", // APPLICATION_UUENCODE
+ "application/x-uue", // APPLICATION_UUENCODE2
+ "application/uue", // APPLICATION_UUENCODE4
+ "application/uuencode", // APPLICATION_UUENCODE3
+ "application/sgml",
+ "application/x-csh",
+ "application/javascript",
+ "application/ecmascript",
+ "application/x-javascript",
+ "application/x-latex",
+ "application/x-macbinhex40",
+ "application/x-ns-proxy-autoconfig",
+ "application/x-www-form-urlencoded",
+ "application/x-perl",
+ "application/x-sh",
+ "application/x-shar",
+ "application/x-tcl",
+ "application/x-tex",
+ "application/x-texinfo",
+ "application/x-troff",
+ "application/x-troff-man",
+ "application/x-troff-me",
+ "application/x-troff-ms",
+ "application/x-troff-ms",
+ "application/x-wais-source",
+ "image/x-bitmap",
+ "image/x-pbm",
+ "image/x-pgm",
+ "image/x-portable-anymap",
+ "image/x-portable-bitmap",
+ "image/x-portable-graymap",
+ "image/x-portable-pixmap", // IMAGE_PPM
+ "image/x-ppm",
+ "image/x-xbitmap", // IMAGE_XBM
+ "image/x-xbm", // IMAGE_XBM2
+ "image/xbm", // IMAGE_XBM3
+ "image/x-xpixmap",
+ "image/x-xpm",
+ ];
+ if (typesWhichAreReallyText.includes(this._contentType)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Base64 encoding. See RFC 2045 6.8. We use the built-in `btoa`, then ensure
+ * line width is no more than 72.
+ */
+ _encodeBase64() {
+ let encoded = btoa(this._body);
+ let ret = "";
+ let length = encoded.length;
+ let i = 0;
+ let limit = 72;
+ while (true) {
+ if (i * limit > length) {
+ break;
+ }
+ ret += encoded.substr(i * limit, limit) + "\r\n";
+ i++;
+ }
+ return ret;
+ }
+
+ /**
+ * Quoted-printable encoding. See RFC 2045 6.7.
+ */
+ _encodeQP() {
+ let currentColumn = 0;
+ let hexdigits = "0123456789ABCDEF";
+ let white = false;
+ let out = "";
+
+ function encodeChar(ch) {
+ let charCode = ch.charCodeAt(0);
+ let ret = "=";
+ ret += hexdigits[charCode >> 4];
+ ret += hexdigits[charCode & 0xf];
+ return ret;
+ }
+
+ for (let i = 0; i < this._bodySize; i++) {
+ let ch = this._body.charAt(i);
+ let charCode = this._body.charCodeAt(i);
+ if (ch == "\r" || ch == "\n") {
+ // If it's CRLF, swallow two chars instead of one.
+ if (i + 1 < this._bodySize && ch == "\r" && this._body[i + 1] == "\n") {
+ i++;
+ }
+
+ // Whitespace cannot be allowed to occur at the end of the line, so we
+ // back up and replace the whitespace with its code.
+ if (white) {
+ let whiteChar = out.slice(-1);
+ out = out.slice(0, -1);
+ out += encodeChar(whiteChar);
+ }
+
+ // Now write out the newline.
+ out += "\r";
+ out += "\n";
+ white = false;
+ currentColumn = 0;
+ } else if (
+ currentColumn == 0 &&
+ (ch == "." ||
+ (ch == "F" &&
+ (i >= this._bodySize - 1 || this._body[i + 1] == "r") &&
+ (i >= this._bodySize - 2 || this._body[i + 2] == "o") &&
+ (i >= this._bodySize - 3 || this._body[i + 3] == "m") &&
+ (i >= this._bodySize - 4 || this._body[i + 4] == " ")))
+ ) {
+ // Just to be SMTP-safe, if "." appears in column 0, encode it.
+ // If this line begins with "From " (or it could but we don't have enough
+ // data in the buffer to be certain), encode the 'F' in hex to avoid
+ // potential problems with BSD mailbox formats.
+ white = false;
+ out += encodeChar(ch);
+ currentColumn += 3;
+ } else if (
+ (charCode >= 33 && charCode <= 60) ||
+ (charCode >= 62 && charCode <= 126)
+ ) {
+ // Printable characters except for '='
+ white = false;
+ out += ch;
+ currentColumn++;
+ } else if (ch == " " || ch == "\t") {
+ // Whitespace
+ white = true;
+ out += ch;
+ currentColumn++;
+ } else {
+ white = false;
+ out += encodeChar(ch);
+ currentColumn += 3;
+ }
+
+ if (currentColumn >= 73) {
+ // Soft line break for readability
+ out += "=\r\n";
+ white = false;
+ currentColumn = 0;
+ }
+ }
+
+ return out;
+ }
+}
diff --git a/comm/mailnews/compose/src/MimeMessage.jsm b/comm/mailnews/compose/src/MimeMessage.jsm
new file mode 100644
index 0000000000..9423e84004
--- /dev/null
+++ b/comm/mailnews/compose/src/MimeMessage.jsm
@@ -0,0 +1,625 @@
+/* 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 = ["MimeMessage"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+let { MimeMultiPart, MimePart } = ChromeUtils.import(
+ "resource:///modules/MimePart.jsm"
+);
+let { MsgUtils } = ChromeUtils.import(
+ "resource:///modules/MimeMessageUtils.jsm"
+);
+let { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+
+/**
+ * A class to create a top MimePart and write to a tmp file. It works like this:
+ * 1. collect top level MIME headers (_gatherMimeHeaders)
+ * 2. collect HTML/plain main body as MimePart[] (_gatherMainParts)
+ * 3. collect attachments as MimePart[] (_gatherAttachmentParts)
+ * 4. construct a top MimePart with above headers and MimePart[] (_initMimePart)
+ * 5. write the top MimePart to a tmp file (createMessageFile)
+ * NOTE: It's possible we will want to replace nsIMsgSend with the interfaces of
+ * MimeMessage. As a part of it, we will add a `send` method to this class.
+ */
+class MimeMessage {
+ /**
+ * Construct a MimeMessage.
+ *
+ * @param {nsIMsgIdentity} userIdentity
+ * @param {nsIMsgCompFields} compFields
+ * @param {string} fcc - The FCC header value.
+ * @param {string} bodyType
+ * @param {BinaryString} bodyText - This is ensured to be a 8-bit string, to
+ * be handled the same as attachment content.
+ * @param {nsMsgDeliverMode} deliverMode
+ * @param {string} originalMsgURI
+ * @param {MSG_ComposeType} compType
+ * @param {nsIMsgAttachment[]} embeddedAttachments - Usually Embedded images.
+ * @param {nsIMsgSendReport} sendReport - Used by _startCryptoEncapsulation.
+ */
+ constructor(
+ userIdentity,
+ compFields,
+ fcc,
+ bodyType,
+ bodyText,
+ deliverMode,
+ originalMsgURI,
+ compType,
+ embeddedAttachments,
+ sendReport
+ ) {
+ this._userIdentity = userIdentity;
+ this._compFields = compFields;
+ this._fcc = fcc;
+ this._bodyType = bodyType;
+ this._bodyText = bodyText;
+ this._deliverMode = deliverMode;
+ this._compType = compType;
+ this._embeddedAttachments = embeddedAttachments;
+ this._sendReport = sendReport;
+ }
+
+ /**
+ * Write a MimeMessage to a tmp file.
+ *
+ * @returns {nsIFile}
+ */
+ async createMessageFile() {
+ let topPart = this._initMimePart();
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("nsemail.eml");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let fstream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ this._fstream = Cc[
+ "@mozilla.org/network/buffered-output-stream;1"
+ ].createInstance(Ci.nsIBufferedOutputStream);
+ fstream.init(file, -1, -1, 0);
+ this._fstream.init(fstream, 16 * 1024);
+
+ this._composeSecure = this._getComposeSecure();
+ if (this._composeSecure) {
+ await this._writePart(topPart);
+ this._composeSecure.finishCryptoEncapsulation(false, this._sendReport);
+ } else {
+ await this._writePart(topPart);
+ }
+
+ this._fstream.close();
+ fstream.close();
+
+ return file;
+ }
+
+ /**
+ * Create a top MimePart to represent the full message.
+ *
+ * @returns {MimePart}
+ */
+ _initMimePart() {
+ let { plainPart, htmlPart } = this._gatherMainParts();
+ let embeddedParts = this._gatherEmbeddedParts();
+ let attachmentParts = this._gatherAttachmentParts();
+
+ let relatedPart = htmlPart;
+ if (htmlPart && embeddedParts.length > 0) {
+ relatedPart = new MimeMultiPart("related");
+ relatedPart.addPart(htmlPart);
+ relatedPart.addParts(embeddedParts);
+ }
+ let mainParts = [plainPart, relatedPart].filter(Boolean);
+ let topPart;
+ if (attachmentParts.length > 0) {
+ // Use multipart/mixed as long as there is at least one attachment.
+ topPart = new MimeMultiPart("mixed");
+ if (plainPart && relatedPart) {
+ // Wrap mainParts inside a multipart/alternative MimePart.
+ let alternativePart = new MimeMultiPart("alternative");
+ alternativePart.addParts(mainParts);
+ topPart.addPart(alternativePart);
+ } else {
+ topPart.addParts(mainParts);
+ }
+ topPart.addParts(attachmentParts);
+ } else {
+ if (mainParts.length > 1) {
+ // Mark the topPart as multipart/alternative.
+ topPart = new MimeMultiPart("alternative");
+ } else {
+ topPart = new MimePart();
+ }
+ topPart.addParts(mainParts);
+ }
+
+ topPart.setHeaders(this._gatherMimeHeaders());
+
+ return topPart;
+ }
+
+ /**
+ * Collect top level headers like From/To/Subject into a Map.
+ */
+ _gatherMimeHeaders() {
+ let messageId = this._compFields.messageId;
+ if (
+ !messageId &&
+ (this._compFields.to ||
+ this._compFields.cc ||
+ this._compFields.bcc ||
+ !this._compFields.newsgroups ||
+ this._userIdentity.getBoolAttribute("generate_news_message_id"))
+ ) {
+ // Try to use the domain name of the From header to generate the message ID. We
+ // specifically don't use the nsIMsgIdentity associated with the account, because
+ // the user might have changed the address in the From header to use a different
+ // domain, and we don't want to leak the relationship between the domains.
+ const fromHdr = MailServices.headerParser.parseEncodedHeaderW(
+ this._compFields.from
+ );
+ const fromAddr = fromHdr[0].email;
+
+ // Extract the host from the address, if any, and generate a message ID from it.
+ // If we can't get a host for the message ID, let SMTP populate the header.
+ const atIndex = fromAddr.indexOf("@");
+ if (atIndex >= 0) {
+ messageId = Cc["@mozilla.org/messengercompose/computils;1"]
+ .createInstance(Ci.nsIMsgCompUtils)
+ .msgGenerateMessageId(
+ this._userIdentity,
+ fromAddr.slice(atIndex + 1)
+ );
+ }
+
+ this._compFields.messageId = messageId;
+ }
+ let headers = new Map([
+ ["message-id", messageId],
+ ["date", new Date()],
+ ["mime-version", "1.0"],
+ ]);
+
+ if (Services.prefs.getBoolPref("mailnews.headers.sendUserAgent")) {
+ if (Services.prefs.getBoolPref("mailnews.headers.useMinimalUserAgent")) {
+ headers.set(
+ "user-agent",
+ Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandFullName")
+ );
+ } else {
+ headers.set(
+ "user-agent",
+ Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ ).userAgent
+ );
+ }
+ }
+
+ for (let headerName of [...this._compFields.headerNames]) {
+ let headerContent = this._compFields.getRawHeader(headerName);
+ if (headerContent) {
+ headers.set(headerName, headerContent);
+ }
+ }
+ let isDraft = [
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ ].includes(this._deliverMode);
+
+ let undisclosedRecipients = MsgUtils.getUndisclosedRecipients(
+ this._compFields,
+ this._deliverMode
+ );
+ if (undisclosedRecipients) {
+ headers.set("to", undisclosedRecipients);
+ }
+
+ if (isDraft) {
+ headers
+ .set(
+ "x-mozilla-draft-info",
+ MsgUtils.getXMozillaDraftInfo(this._compFields)
+ )
+ .set("x-identity-key", this._userIdentity.key)
+ .set("fcc", this._fcc);
+ }
+
+ if (messageId) {
+ // MDN request header requires to have MessageID header presented in the
+ // message in order to coorelate the MDN reports to the original message.
+ headers
+ .set(
+ "disposition-notification-to",
+ MsgUtils.getDispositionNotificationTo(
+ this._compFields,
+ this._deliverMode
+ )
+ )
+ .set(
+ "return-receipt-to",
+ MsgUtils.getReturnReceiptTo(this._compFields, this._deliverMode)
+ );
+ }
+
+ for (let { headerName, headerValue } of MsgUtils.getDefaultCustomHeaders(
+ this._userIdentity
+ )) {
+ headers.set(headerName, headerValue);
+ }
+
+ let rawMftHeader = headers.get("mail-followup-to");
+ // If there's already a Mail-Followup-To header, don't need to do anything.
+ if (!rawMftHeader) {
+ headers.set(
+ "mail-followup-to",
+ MsgUtils.getMailFollowupToHeader(this._compFields, this._userIdentity)
+ );
+ }
+
+ let rawMrtHeader = headers.get("mail-reply-to");
+ // If there's already a Mail-Reply-To header, don't need to do anything.
+ if (!rawMrtHeader) {
+ headers.set(
+ "mail-reply-to",
+ MsgUtils.getMailReplyToHeader(
+ this._compFields,
+ this._userIdentity,
+ rawMrtHeader
+ )
+ );
+ }
+
+ let rawPriority = headers.get("x-priority");
+ if (rawPriority) {
+ headers.set("x-priority", MsgUtils.getXPriority(rawPriority));
+ }
+
+ let rawReferences = headers.get("references");
+ if (rawReferences) {
+ let references = MsgUtils.getReferences(rawReferences);
+ // Don't reset "references" header if references is undefined.
+ if (references) {
+ headers.set("references", references);
+ }
+ headers.set("in-reply-to", MsgUtils.getInReplyTo(rawReferences));
+ }
+ if (
+ rawReferences &&
+ [
+ Ci.nsIMsgCompType.ForwardInline,
+ Ci.nsIMsgCompType.ForwardAsAttachment,
+ ].includes(this._compType)
+ ) {
+ headers.set("x-forwarded-message-id", rawReferences);
+ }
+
+ let rawNewsgroups = headers.get("newsgroups");
+ if (rawNewsgroups) {
+ let { newsgroups, newshost } = MsgUtils.getNewsgroups(
+ this._deliverMode,
+ rawNewsgroups
+ );
+ // Don't reset "newsgroups" header if newsgroups is undefined.
+ if (newsgroups) {
+ headers.set("newsgroups", newsgroups);
+ }
+ headers.set("x-mozilla-news-host", newshost);
+ }
+
+ return headers;
+ }
+
+ /**
+ * Determine if the message should include an HTML part, a plain part or both.
+ *
+ * @returns {{plainPart: MimePart, htmlPart: MimePart}}
+ */
+ _gatherMainParts() {
+ let formatFlowed = Services.prefs.getBoolPref(
+ "mailnews.send_plaintext_flowed"
+ );
+ let formatParam = "";
+ if (formatFlowed) {
+ // Set format=flowed as in RFC 2646 according to the preference.
+ formatParam += "; format=flowed";
+ }
+
+ let htmlPart = null;
+ let plainPart = null;
+ let parts = {};
+
+ if (this._bodyType === "text/html") {
+ htmlPart = new MimePart(
+ this._bodyType,
+ this._compFields.forceMsgEncoding,
+ true
+ );
+ htmlPart.setHeader("content-type", "text/html; charset=UTF-8");
+ htmlPart.bodyText = this._bodyText;
+ } else if (this._bodyType === "text/plain") {
+ plainPart = new MimePart(
+ this._bodyType,
+ this._compFields.forceMsgEncoding,
+ true
+ );
+ plainPart.setHeader(
+ "content-type",
+ `text/plain; charset=UTF-8${formatParam}`
+ );
+ plainPart.bodyText = this._bodyText;
+ parts.plainPart = plainPart;
+ }
+
+ // Assemble a multipart/alternative message.
+ if (
+ (this._compFields.forcePlainText ||
+ this._compFields.useMultipartAlternative) &&
+ plainPart === null &&
+ htmlPart !== null
+ ) {
+ plainPart = new MimePart(
+ "text/plain",
+ this._compFields.forceMsgEncoding,
+ true
+ );
+ plainPart.setHeader(
+ "content-type",
+ `text/plain; charset=UTF-8${formatParam}`
+ );
+ // nsIParserUtils.convertToPlainText expects unicode string.
+ let plainUnicode = MsgUtils.convertToPlainText(
+ new TextDecoder().decode(
+ jsmime.mimeutils.stringToTypedArray(this._bodyText)
+ ),
+ formatFlowed
+ );
+ // MimePart.bodyText should be binary string.
+ plainPart.bodyText = jsmime.mimeutils.typedArrayToString(
+ new TextEncoder().encode(plainUnicode)
+ );
+
+ parts.plainPart = plainPart;
+ }
+
+ // If useMultipartAlternative is true, send multipart/alternative message.
+ // Otherwise, send the plainPart only.
+ if (htmlPart) {
+ if (
+ (plainPart && this._compFields.useMultipartAlternative) ||
+ !plainPart
+ ) {
+ parts.htmlPart = htmlPart;
+ }
+ }
+
+ return parts;
+ }
+
+ /**
+ * Collect local attachments.
+ *
+ * @returns {MimePart[]}
+ */
+ _gatherAttachmentParts() {
+ let attachments = [...this._compFields.attachments];
+ let cloudParts = [];
+ let localParts = [];
+
+ for (let attachment of attachments) {
+ let part;
+ if (attachment.htmlAnnotation) {
+ part = new MimePart();
+ // MimePart.bodyText should be binary string.
+ part.bodyText = jsmime.mimeutils.typedArrayToString(
+ new TextEncoder().encode(attachment.htmlAnnotation)
+ );
+ part.setHeader("content-type", "text/html; charset=utf-8");
+
+ let suffix = /\.html$/i.test(attachment.name) ? "" : ".html";
+ let encodedFilename = MsgUtils.rfc2231ParamFolding(
+ "filename",
+ `${attachment.name}${suffix}`
+ );
+ part.setHeader("content-disposition", `attachment; ${encodedFilename}`);
+ } else {
+ part = new MimePart(null, this._compFields.forceMsgEncoding, false);
+ part.setBodyAttachment(attachment);
+ }
+
+ let cloudPartHeader = MsgUtils.getXMozillaCloudPart(
+ this._deliverMode,
+ attachment
+ );
+ if (cloudPartHeader) {
+ part.setHeader("x-mozilla-cloud-part", cloudPartHeader);
+ }
+
+ localParts.push(part);
+ }
+ // Cloud attachments are handled before local attachments in the C++
+ // implementation. We follow it here so that no need to change tests.
+ return cloudParts.concat(localParts);
+ }
+
+ /**
+ * Collect embedded objects as attachments.
+ *
+ * @returns {MimePart[]}
+ */
+ _gatherEmbeddedParts() {
+ return this._embeddedAttachments.map(attachment => {
+ let part = new MimePart(null, this._compFields.forceMsgEncoding, false);
+ part.setBodyAttachment(attachment, "inline", attachment.contentId);
+ return part;
+ });
+ }
+
+ /**
+ * If crypto encapsulation is required, returns an nsIMsgComposeSecure instance.
+ *
+ * @returns {nsIMsgComposeSecure}
+ */
+ _getComposeSecure() {
+ let secureCompose = this._compFields.composeSecure;
+ if (!secureCompose) {
+ return null;
+ }
+
+ if (
+ this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft &&
+ !this._userIdentity.getBoolAttribute("autoEncryptDrafts")
+ ) {
+ return null;
+ }
+
+ if (
+ !secureCompose.requiresCryptoEncapsulation(
+ this._userIdentity,
+ this._compFields
+ )
+ ) {
+ return null;
+ }
+ return secureCompose;
+ }
+
+ /**
+ * Pass a stream and other params to this._composeSecure to start crypto
+ * encapsulation.
+ */
+ _startCryptoEncapsulation() {
+ let recipients = [
+ this._compFields.to,
+ this._compFields.cc,
+ this._compFields.bcc,
+ this._compFields.newsgroups,
+ ]
+ .filter(Boolean)
+ .join(",");
+
+ this._composeSecure.beginCryptoEncapsulation(
+ this._fstream,
+ recipients,
+ this._compFields,
+ this._userIdentity,
+ this._sendReport,
+ this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft
+ );
+ this._cryptoEncapsulationStarted = true;
+ }
+
+ /**
+ * Recursively write an MimePart and its parts to a this._fstream.
+ *
+ * @param {MimePart} curPart - The MimePart to write out.
+ * @param {number} [depth=0] - Nested level of a part.
+ */
+ async _writePart(curPart, depth = 0) {
+ let bodyString;
+ try {
+ // `getEncodedBodyString()` returns a binary string.
+ bodyString = await curPart.getEncodedBodyString();
+ } catch (e) {
+ if (e.data && /^data:/i.test(e.data.url)) {
+ // Invalid data uri should not prevent sending message.
+ return;
+ }
+ throw e;
+ }
+
+ if (depth == 0 && this._composeSecure) {
+ // Crypto encapsulation will add a new content-type header.
+ curPart.deleteHeader("content-type");
+ if (curPart.parts.length > 1) {
+ // Move child parts one layer deeper so that the message is still well
+ // formed after crypto encapsulation.
+ let newChild = new MimeMultiPart(curPart.subtype);
+ newChild.parts = curPart._parts;
+ curPart.parts = [newChild];
+ }
+ }
+
+ // Write out headers, there could be non-ASCII in the headers
+ // which we need to encode into UTF-8.
+ this._writeString(curPart.getHeaderString());
+
+ // Start crypto encapsulation if needed.
+ if (depth == 0 && this._composeSecure) {
+ this._startCryptoEncapsulation();
+ }
+
+ // Recursively write out parts.
+ if (curPart.parts.length) {
+ // single part message
+ if (curPart.parts.length === 1) {
+ await this._writePart(curPart.parts[0], depth + 1);
+ this._writeBinaryString(bodyString);
+ return;
+ }
+
+ // We can safely use `_writeBinaryString()` for ASCII strings.
+ this._writeBinaryString("\r\n");
+ if (depth == 0) {
+ // Current part is a top part and multipart container.
+ this._writeBinaryString(
+ "This is a multi-part message in MIME format.\r\n"
+ );
+ }
+
+ // multipart message
+ for (let part of curPart.parts) {
+ this._writeBinaryString(`--${curPart.separator}\r\n`);
+ await this._writePart(part, depth + 1);
+ }
+ this._writeBinaryString(`\r\n--${curPart.separator}--\r\n`);
+ if (depth > 1) {
+ // If more separators follow, make sure there is a blank line after
+ // this one.
+ this._writeBinaryString("\r\n");
+ }
+ } else {
+ this._writeBinaryString(`\r\n`);
+ }
+
+ // Ensure there is exactly one blank line after a part and before
+ // the boundary, and exactly one blank line between boundary lines.
+ // This works around bugs in other software that erroneously remove
+ // additional blank lines, thereby causing verification failures of
+ // OpenPGP or S/MIME signatures. For example see bug 1731529.
+
+ // Write out body.
+ this._writeBinaryString(bodyString);
+ }
+
+ /**
+ * Write a binary string to this._fstream.
+ *
+ * @param {BinaryString} str - The binary string to write.
+ */
+ _writeBinaryString(str) {
+ this._cryptoEncapsulationStarted
+ ? this._composeSecure.mimeCryptoWriteBlock(str, str.length)
+ : this._fstream.write(str, str.length);
+ }
+
+ /**
+ * Write a string to this._fstream.
+ *
+ * @param {string} str - The string to write.
+ */
+ _writeString(str) {
+ this._writeBinaryString(
+ jsmime.mimeutils.typedArrayToString(new TextEncoder().encode(str))
+ );
+ }
+}
diff --git a/comm/mailnews/compose/src/MimeMessageUtils.jsm b/comm/mailnews/compose/src/MimeMessageUtils.jsm
new file mode 100644
index 0000000000..65682b6d4c
--- /dev/null
+++ b/comm/mailnews/compose/src/MimeMessageUtils.jsm
@@ -0,0 +1,1058 @@
+/* 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 = ["MsgUtils"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+
+// Defined in ErrorList.h.
+const NS_ERROR_MODULE_BASE_OFFSET = 69;
+const NS_ERROR_MODULE_MAILNEWS = 16;
+
+/**
+ * Generate an NS_ERROR code from a MAILNEWS error code. See NS_ERROR_GENERATE
+ * in nsError.h and NS_MSG_GENERATE_FAILURE in nsComposeStrings.h.
+ *
+ * @param {number} code - The error code in MAILNEWS module.
+ * @returns {number}
+ */
+function generateNSError(code) {
+ return (
+ ((1 << 31) |
+ ((NS_ERROR_MODULE_MAILNEWS + NS_ERROR_MODULE_BASE_OFFSET) << 16) |
+ code) >>>
+ 0
+ );
+}
+
+/**
+ * Collection of helper functions for message sending process.
+ */
+var MsgUtils = {
+ /**
+ * Error codes defined in nsComposeStrings.h
+ */
+ NS_MSG_UNABLE_TO_OPEN_FILE: generateNSError(12500),
+ NS_MSG_UNABLE_TO_OPEN_TMP_FILE: generateNSError(12501),
+ NS_MSG_UNABLE_TO_SAVE_TEMPLATE: generateNSError(12502),
+ NS_MSG_UNABLE_TO_SAVE_DRAFT: generateNSError(12503),
+ NS_MSG_COULDNT_OPEN_FCC_FOLDER: generateNSError(12506),
+ NS_MSG_NO_SENDER: generateNSError(12510),
+ NS_MSG_NO_RECIPIENTS: generateNSError(12511),
+ NS_MSG_ERROR_WRITING_FILE: generateNSError(12512),
+ NS_ERROR_SENDING_FROM_COMMAND: generateNSError(12514),
+ NS_ERROR_SENDING_DATA_COMMAND: generateNSError(12516),
+ NS_ERROR_SENDING_MESSAGE: generateNSError(12517),
+ NS_ERROR_POST_FAILED: generateNSError(12518),
+ NS_ERROR_SMTP_SERVER_ERROR: generateNSError(12524),
+ NS_MSG_UNABLE_TO_SEND_LATER: generateNSError(12525),
+ NS_ERROR_COMMUNICATIONS_ERROR: generateNSError(12526),
+ NS_ERROR_BUT_DONT_SHOW_ALERT: generateNSError(12527),
+ NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS: generateNSError(12529),
+ NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY: generateNSError(12530),
+ NS_ERROR_MIME_MPART_ATTACHMENT_ERROR: generateNSError(12531),
+
+ // 12554 is taken by NS_ERROR_NNTP_NO_CROSS_POSTING. use 12555 as the next one
+
+ // For message sending report
+ NS_MSG_ERROR_READING_FILE: generateNSError(12563),
+
+ NS_MSG_ERROR_ATTACHING_FILE: generateNSError(12570),
+
+ NS_ERROR_SMTP_GREETING: generateNSError(12572),
+
+ NS_ERROR_SENDING_RCPT_COMMAND: generateNSError(12575),
+
+ NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS: generateNSError(12582),
+
+ NS_ERROR_SMTP_PASSWORD_UNDEFINED: generateNSError(12584),
+ NS_ERROR_SMTP_SEND_NOT_ALLOWED: generateNSError(12585),
+ NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED: generateNSError(12586),
+ NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2: generateNSError(12588),
+
+ NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER: generateNSError(12589),
+ NS_ERROR_SMTP_SEND_FAILED_REFUSED: generateNSError(12590),
+ NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED: generateNSError(12591),
+ NS_ERROR_SMTP_SEND_FAILED_TIMEOUT: generateNSError(12592),
+ NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON: generateNSError(12593),
+
+ NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL: generateNSError(12594),
+ NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL: generateNSError(12595),
+ NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT: generateNSError(12596),
+ NS_ERROR_SMTP_AUTH_FAILURE: generateNSError(12597),
+ NS_ERROR_SMTP_AUTH_GSSAPI: generateNSError(12598),
+ NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED: generateNSError(12599),
+
+ NS_ERROR_ILLEGAL_LOCALPART: generateNSError(12601),
+
+ NS_ERROR_CLIENTID: generateNSError(12610),
+ NS_ERROR_CLIENTID_PERMISSION: generateNSError(12611),
+
+ sendLogger: console.createInstance({
+ prefix: "mailnews.send",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.send.loglevel",
+ }),
+
+ smtpLogger: console.createInstance({
+ prefix: "mailnews.smtp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.smtp.loglevel",
+ }),
+
+ /**
+ * NS_IS_MSG_ERROR in msgCore.h.
+ *
+ * @param {nsresult} err - The nsresult value.
+ * @returns {boolean}
+ */
+ isMsgError(err) {
+ return (
+ (((err >> 16) - NS_ERROR_MODULE_BASE_OFFSET) & 0x1fff) ==
+ NS_ERROR_MODULE_MAILNEWS
+ );
+ },
+
+ /**
+ * Convert html to text to form a multipart/alternative message. The output
+ * depends on preference.
+ *
+ * @param {string} input - The HTML text to convert.
+ * @param {boolean} formatFlowed - A flag to enable OutputFormatFlowed.
+ * @returns {string}
+ */
+ convertToPlainText(input, formatFlowed) {
+ let wrapWidth = Services.prefs.getIntPref("mailnews.wraplength", 72);
+ if (wrapWidth == 0 || wrapWidth > 990) {
+ wrapWidth = 990;
+ } else if (wrapWidth < 10) {
+ wrapWidth = 10;
+ }
+
+ let flags =
+ Ci.nsIDocumentEncoder.OutputPersistNBSP |
+ Ci.nsIDocumentEncoder.OutputFormatted |
+ Ci.nsIDocumentEncoder.OutputDisallowLineBreaking;
+ if (formatFlowed) {
+ flags |= Ci.nsIDocumentEncoder.OutputFormatFlowed;
+ }
+
+ let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
+ Ci.nsIParserUtils
+ );
+ return parserUtils.convertToPlainText(input, flags, wrapWidth);
+ },
+
+ /**
+ * Get the list of default custom headers.
+ *
+ * @param {nsIMsgIdentity} userIdentity - User identity.
+ * @returns {{headerName: string, headerValue: string}[]}
+ */
+ getDefaultCustomHeaders(userIdentity) {
+ // mail.identity.<id#>.headers pref is a comma separated value of pref names
+ // containing headers to add headers are stored in
+ let headerAttributes = userIdentity
+ .getUnicharAttribute("headers")
+ .split(",");
+ let headers = [];
+ for (let attr of headerAttributes) {
+ // mail.identity.<id#>.header.<header name> grab all the headers
+ let attrValue = userIdentity.getUnicharAttribute(`header.${attr}`);
+ if (attrValue) {
+ let colonIndex = attrValue.indexOf(":");
+ headers.push({
+ headerName: attrValue.slice(0, colonIndex),
+ headerValue: attrValue.slice(colonIndex + 1).trim(),
+ });
+ }
+ }
+ return headers;
+ },
+
+ /**
+ * Get the fcc value.
+ *
+ * @param {nsIMsgIdentity} userIdentity - The user identity.
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {string} originalMsgURI - The original message uri, can be null.
+ * @param {MSG_ComposeType} compType - The compose type.
+ * @returns {string}
+ */
+ getFcc(userIdentity, compFields, originalMsgURI, compType) {
+ // Check if the default fcc has been overridden.
+ let fcc = "";
+ let useDefaultFcc = true;
+ if (compFields.fcc) {
+ if (compFields.fcc.startsWith("nocopy://")) {
+ useDefaultFcc = false;
+ fcc = "";
+ } else {
+ let folder = MailUtils.getExistingFolder(compFields.fcc);
+ if (folder) {
+ useDefaultFcc = false;
+ fcc = compFields.fcc;
+ }
+ }
+ }
+
+ // If the identity pref "fcc" is set to false, then we will not do the default
+ // FCC operation but still allow the override.
+ if (!userIdentity.doFcc) {
+ return fcc;
+ }
+
+ // We use default FCC setting if it's not set or was set to an invalid
+ // folder.
+ if (useDefaultFcc) {
+ // Only check whether the user wants the message in the original message
+ // folder if the msgcomptype is some kind of a reply.
+ if (
+ originalMsgURI &&
+ [
+ Ci.nsIMsgCompType.Reply,
+ Ci.nsIMsgCompType.ReplyAll,
+ Ci.nsIMsgCompType.ReplyToGroup,
+ Ci.nsIMsgCompType.ReplyToSender,
+ Ci.nsIMsgCompType.ReplyToSenderAndGroup,
+ Ci.nsIMsgCompType.ReplyToList,
+ Ci.nsIMsgCompType.ReplyWithTemplate,
+ ].includes(compType)
+ ) {
+ let msgHdr;
+ try {
+ msgHdr =
+ MailServices.messageServiceFromURI(
+ originalMsgURI
+ ).messageURIToMsgHdr(originalMsgURI);
+ } catch (e) {
+ console.warn(
+ `messageServiceFromURI failed for ${originalMsgURI}\n${e.stack}`
+ );
+ }
+ if (msgHdr) {
+ let folder = msgHdr.folder;
+ if (
+ folder &&
+ folder.canFileMessages &&
+ folder.server &&
+ folder.server.getCharValue("type") != "rss" &&
+ userIdentity.fccReplyFollowsParent
+ ) {
+ fcc = folder.URI;
+ useDefaultFcc = false;
+ }
+ }
+ }
+
+ if (useDefaultFcc) {
+ let uri = this.getMsgFolderURIFromPrefs(
+ userIdentity,
+ Ci.nsIMsgSend.nsMsgDeliverNow
+ );
+ fcc = uri == "nocopy://" ? "" : uri;
+ }
+ }
+
+ return fcc;
+ },
+
+ canSaveToFolder(folderUri) {
+ let folder = MailUtils.getOrCreateFolder(folderUri);
+ if (folder.server) {
+ return folder.server.canFileMessagesOnServer;
+ }
+ return false;
+ },
+
+ /**
+ * Get the To header value. When we don't have disclosed recipient but only
+ * Bcc, use the undisclosedRecipients entry from composeMsgs.properties as the
+ * To header value to prevent problem with some servers.
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {string}
+ */
+ getUndisclosedRecipients(compFields, deliverMode) {
+ // Newsgroups count as recipients.
+ let hasDisclosedRecipient =
+ compFields.to || compFields.cc || compFields.newsgroups;
+ // If we are saving the message as a draft, don't bother inserting the
+ // undisclosed recipients field. We'll take care of that when we really send
+ // the message.
+ if (
+ hasDisclosedRecipient ||
+ [
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ ].includes(deliverMode) ||
+ !Services.prefs.getBoolPref("mail.compose.add_undisclosed_recipients")
+ ) {
+ return "";
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+ let undisclosedRecipients = composeBundle.GetStringFromName(
+ "undisclosedRecipients"
+ );
+ let recipients = MailServices.headerParser.makeGroupObject(
+ undisclosedRecipients,
+ []
+ );
+ return recipients.toString();
+ },
+
+ /**
+ * Get the Mail-Followup-To header value.
+ * See bug #204339 and http://cr.yp.to/proto/replyto.html for details
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {nsIMsgIdentity} userIdentity - The user identity.
+ * @returns {string}
+ */
+ getMailFollowupToHeader(compFields, userIdentity) {
+ let mailLists = userIdentity.getUnicharAttribute(
+ "subscribed_mailing_lists"
+ );
+ if (!mailLists || !(compFields.to || compFields.cc)) {
+ return "";
+ }
+ let recipients = compFields.to;
+ if (recipients) {
+ if (compFields.cc) {
+ recipients += `,${compFields.cc}`;
+ }
+ } else {
+ recipients = compFields.cc;
+ }
+ let recipientsDedup =
+ MailServices.headerParser.removeDuplicateAddresses(recipients);
+ let recipientsWithoutMailList =
+ MailServices.headerParser.removeDuplicateAddresses(
+ recipientsDedup,
+ mailLists
+ );
+ if (recipientsDedup != recipientsWithoutMailList) {
+ return recipients;
+ }
+ return "";
+ },
+
+ /**
+ * Get the Mail-Reply-To header value.
+ * See bug #204339 and http://cr.yp.to/proto/replyto.html for details
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {nsIMsgIdentity} userIdentity - The user identity.
+ * @returns {string}
+ */
+ getMailReplyToHeader(compFields, userIdentity) {
+ let mailLists = userIdentity.getUnicharAttribute(
+ "replyto_mangling_mailing_lists"
+ );
+ if (
+ !mailLists ||
+ mailLists[0] == "*" ||
+ !(compFields.to || compFields.cc)
+ ) {
+ return "";
+ }
+ let recipients = compFields.to;
+ if (recipients) {
+ if (compFields.cc) {
+ recipients += `,${compFields.cc}`;
+ }
+ } else {
+ recipients = compFields.cc;
+ }
+ let recipientsDedup =
+ MailServices.headerParser.removeDuplicateAddresses(recipients);
+ let recipientsWithoutMailList =
+ MailServices.headerParser.removeDuplicateAddresses(
+ recipientsDedup,
+ mailLists
+ );
+ if (recipientsDedup != recipientsWithoutMailList) {
+ return compFields.replyTo || compFields.from;
+ }
+ return "";
+ },
+
+ /**
+ * Get the X-Mozilla-Draft-Info header value.
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @returns {string}
+ */
+ getXMozillaDraftInfo(compFields) {
+ let getCompField = (property, key) => {
+ let value = compFields[property] ? 1 : 0;
+ return `${key}=${value}; `;
+ };
+ let draftInfo = "internal/draft; ";
+ draftInfo += getCompField("attachVCard", "vcard");
+
+ let receiptValue = 0;
+ if (compFields.returnReceipt) {
+ // slight change compared to 4.x; we used to use receipt= to tell
+ // whether the draft/template has request for either MDN or DNS or both
+ // return receipt; since the DNS is out of the picture we now use the
+ // header type + 1 to tell whether user has requested the return receipt
+ receiptValue = compFields.receiptHeaderType + 1;
+ }
+ draftInfo += `receipt=${receiptValue}; `;
+
+ draftInfo += getCompField("DSN", "DSN");
+ draftInfo += "uuencode=0; ";
+ draftInfo += getCompField("attachmentReminder", "attachmentreminder");
+ draftInfo += `deliveryformat=${compFields.deliveryFormat}`;
+
+ return draftInfo;
+ },
+
+ /**
+ * Get the X-Mozilla-Cloud-Part header value.
+ *
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @param {nsIMsgAttachment} attachment - The cloud attachment.
+ * @returns {string}
+ */
+ getXMozillaCloudPart(deliverMode, attachment) {
+ let value = "";
+ if (attachment.sendViaCloud && attachment.contentLocation) {
+ value += `cloudFile; url=${attachment.contentLocation}`;
+
+ if (
+ (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft ||
+ deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) &&
+ attachment.cloudFileAccountKey &&
+ attachment.cloudPartHeaderData
+ ) {
+ value += `; provider=${attachment.cloudFileAccountKey}`;
+ value += `; ${this.rfc2231ParamFolding(
+ "data",
+ attachment.cloudPartHeaderData
+ )}`;
+ }
+ }
+ return value;
+ },
+
+ /**
+ * Get the X-Mozilla-Status header value. The header value will be used to set
+ * some nsMsgMessageFlags. Including the Read flag for message in a local
+ * folder.
+ *
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {string}
+ */
+ getXMozillaStatus(deliverMode) {
+ if (
+ ![
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ Ci.nsIMsgSend.nsMsgSendUnsent,
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ ].includes(deliverMode)
+ ) {
+ return "";
+ }
+ let flags = 0;
+ if (deliverMode == Ci.nsIMsgSend.nsMsgQueueForLater) {
+ flags |= Ci.nsMsgMessageFlags.Queued;
+ } else if (
+ deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft &&
+ deliverMode != Ci.nsIMsgSend.nsMsgDeliverBackground
+ ) {
+ flags |= Ci.nsMsgMessageFlags.Read;
+ }
+ return flags.toString(16).padStart(4, "0");
+ },
+
+ /**
+ * Get the X-Mozilla-Status2 header value. The header value will be used to
+ * set some nsMsgMessageFlags.
+ *
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {string}
+ */
+ getXMozillaStatus2(deliverMode) {
+ if (
+ ![
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ Ci.nsIMsgSend.nsMsgSendUnsent,
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ ].includes(deliverMode)
+ ) {
+ return "";
+ }
+ let flags = 0;
+ if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) {
+ flags |= Ci.nsMsgMessageFlags.Template;
+ } else if (
+ deliverMode == Ci.nsIMsgSend.nsMsgDeliverNow ||
+ deliverMode == Ci.nsIMsgSend.nsMsgSendUnsent
+ ) {
+ flags &= ~Ci.nsMsgMessageFlags.MDNReportNeeded;
+ flags |= Ci.nsMsgMessageFlags.MDNReportSent;
+ }
+ return flags.toString(16).padStart(8, "0");
+ },
+
+ /**
+ * Get the Disposition-Notification-To header value.
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {{dnt: string, rrt: string}}
+ */
+ getDispositionNotificationTo(compFields, deliverMode) {
+ if (
+ compFields.returnReceipt &&
+ deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft &&
+ deliverMode != Ci.nsIMsgSend.nsMsgSaveAsTemplate &&
+ compFields.receiptHeaderType != Ci.nsIMsgMdnGenerator.eRrtType
+ ) {
+ return compFields.from;
+ }
+ return "";
+ },
+
+ /**
+ * Get the Return-Receipt-To header value.
+ *
+ * @param {nsIMsgCompFields} compFields - The compose fields.
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {{dnt: string, rrt: string}}
+ */
+ getReturnReceiptTo(compFields, deliverMode) {
+ if (
+ compFields.returnReceipt &&
+ deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft &&
+ deliverMode != Ci.nsIMsgSend.nsMsgSaveAsTemplate &&
+ compFields.receiptHeaderType != Ci.nsIMsgMdnGenerator.eDntType
+ ) {
+ return compFields.from;
+ }
+ return "";
+ },
+
+ /**
+ * Get the value of X-Priority header.
+ *
+ * @param {string} rawPriority - Raw X-Priority content.
+ * @returns {string}
+ */
+ getXPriority(rawPriority) {
+ rawPriority = rawPriority.toLowerCase();
+ let priorityValue = Ci.nsMsgPriority.Default;
+ let priorityValueString = "0";
+ let priorityName = "None";
+ if (rawPriority.startsWith("1") || rawPriority.startsWith("highest")) {
+ priorityValue = Ci.nsMsgPriority.highest;
+ priorityValueString = "1";
+ priorityName = "Highest";
+ } else if (
+ rawPriority.startsWith("2") ||
+ // "high" must be tested after "highest".
+ rawPriority.startsWith("high") ||
+ rawPriority.startsWith("urgent")
+ ) {
+ priorityValue = Ci.nsMsgPriority.high;
+ priorityValueString = "2";
+ priorityName = "High";
+ } else if (
+ rawPriority.startsWith("3") ||
+ rawPriority.startsWith("normal")
+ ) {
+ priorityValue = Ci.nsMsgPriority.normal;
+ priorityValueString = "3";
+ priorityName = "Normal";
+ } else if (
+ rawPriority.startsWith("5") ||
+ rawPriority.startsWith("lowest")
+ ) {
+ priorityValue = Ci.nsMsgPriority.lowest;
+ priorityValueString = "5";
+ priorityName = "Lowest";
+ } else if (
+ rawPriority.startsWith("4") ||
+ // "low" must be tested after "lowest".
+ rawPriority.startsWith("low")
+ ) {
+ priorityValue = Ci.nsMsgPriority.low;
+ priorityValueString = "4";
+ priorityName = "Low";
+ }
+ if (priorityValue == Ci.nsMsgPriority.Default) {
+ return "";
+ }
+ return `${priorityValueString} (${priorityName})`;
+ },
+
+ /**
+ * Get the References header value.
+ *
+ * @param {string} references - Raw References header content.
+ * @returns {string}
+ */
+ getReferences(references) {
+ if (references.length <= 986) {
+ return "";
+ }
+ // The References header should be kept under 998 characters: if it's too
+ // long, trim out the earliest references to make it smaller.
+ let newReferences = "";
+ let firstRef = references.indexOf("<");
+ let secondRef = references.indexOf("<", firstRef + 1);
+ if (secondRef > 0) {
+ newReferences = references.slice(0, secondRef);
+ let bracket = references.indexOf(
+ "<",
+ references.length + newReferences.length - 986
+ );
+ if (bracket > 0) {
+ newReferences += references.slice(bracket);
+ }
+ }
+ return newReferences;
+ },
+
+ /**
+ * Get the In-Reply-To header value.
+ *
+ * @param {string} references - Raw References header content.
+ * @returns {string}
+ */
+ getInReplyTo(references) {
+ // The In-Reply-To header is the last entry in the references header...
+ let bracket = references.lastIndexOf("<");
+ if (bracket >= 0) {
+ return references.slice(bracket);
+ }
+ return "";
+ },
+
+ /**
+ * Get the value of Newsgroups and X-Mozilla-News-Host header.
+ *
+ * @param {nsMsgDeliverMode} deliverMode - Message deliver mode.
+ * @param {string} newsgroups - Raw newsgroups header content.
+ * @returns {{newsgroups: string, newshost: string}}
+ */
+ getNewsgroups(deliverMode, newsgroups) {
+ let nntpService = Cc["@mozilla.org/messenger/nntpservice;1"].getService(
+ Ci.nsINntpService
+ );
+ let newsgroupsHeaderVal = {};
+ let newshostHeaderVal = {};
+ nntpService.generateNewsHeaderValsForPosting(
+ newsgroups,
+ newsgroupsHeaderVal,
+ newshostHeaderVal
+ );
+
+ // If we are here, we are NOT going to send this now. (i.e. it is a Draft,
+ // Send Later file, etc...). Because of that, we need to store what the user
+ // typed in on the original composition window for use later when rebuilding
+ // the headers
+ if (
+ deliverMode == Ci.nsIMsgSend.nsMsgDeliverNow ||
+ deliverMode == Ci.nsIMsgSend.nsMsgSendUnsent
+ ) {
+ // This is going to be saved for later, that means we should just store
+ // what the user typed into the "Newsgroup" line in the
+ // HEADER_X_MOZILLA_NEWSHOST header for later use by "Send Unsent
+ // Messages", "Drafts" or "Templates"
+ newshostHeaderVal.value = "";
+ }
+ return {
+ newsgroups: newsgroupsHeaderVal.value,
+ newshost: newshostHeaderVal.value,
+ };
+ },
+
+ /**
+ * Get the Content-Location header value.
+ *
+ * @param {string} baseUrl - The base url of an HTML attachment.
+ * @returns {string}
+ */
+ getContentLocation(baseUrl) {
+ let lowerBaseUrl = baseUrl.toLowerCase();
+ if (
+ !baseUrl.includes(":") ||
+ lowerBaseUrl.startsWith("news:") ||
+ lowerBaseUrl.startsWith("snews:") ||
+ lowerBaseUrl.startsWith("imap:") ||
+ lowerBaseUrl.startsWith("file:") ||
+ lowerBaseUrl.startsWith("mailbox:")
+ ) {
+ return "";
+ }
+ let transformMap = {
+ " ": "%20",
+ "\t": "%09",
+ "\n": "%0A",
+ "\r": "%0D",
+ };
+ let value = "";
+ for (let char of baseUrl) {
+ value += transformMap[char] || char;
+ }
+ return value;
+ },
+
+ /**
+ * Given a string, convert it to 'qtext' (quoted text) for RFC822 header
+ * purposes.
+ */
+ makeFilenameQtext(srcText, stripCRLFs) {
+ let size = srcText.length;
+ let ret = "";
+ for (let i = 0; i < size; i++) {
+ let char = srcText.charAt(i);
+ if (
+ char == "\\" ||
+ char == '"' ||
+ (!stripCRLFs &&
+ char == "\r" &&
+ (srcText[i + 1] != "\n" ||
+ (srcText[i + 1] == "\n" && i + 2 < size && srcText[i + 2] != " ")))
+ ) {
+ ret += "\\";
+ }
+
+ if (
+ stripCRLFs &&
+ char == "\r" &&
+ srcText[i + 1] == "\n" &&
+ i + 2 < size &&
+ srcText[i + 2] == " "
+ ) {
+ i += 3;
+ } else {
+ ret += char;
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * Encode parameter value according to RFC 2047.
+ *
+ * @param {string} value - The parameter value.
+ * @returns {string}
+ */
+ rfc2047EncodeParam(value) {
+ let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService(
+ Ci.nsIMimeConverter
+ );
+
+ let encoded = converter.encodeMimePartIIStr_UTF8(
+ value,
+ false,
+ 0,
+ Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE
+ );
+
+ return this.makeFilenameQtext(encoded, false);
+ },
+
+ /**
+ * Encode parameter value according to RFC 2231.
+ *
+ * @param {string} paramName - The parameter name.
+ * @param {string} paramValue - The parameter value.
+ * @returns {string}
+ */
+ rfc2231ParamFolding(paramName, paramValue) {
+ // this is to guarantee the folded line will never be greater
+ // than 78 = 75 + CRLFLWSP
+ const PR_MAX_FOLDING_LEN = 75;
+
+ let needsEscape = false;
+ let encoder = new TextEncoder();
+ let dupParamValue = jsmime.mimeutils.typedArrayToString(
+ encoder.encode(paramValue)
+ );
+
+ if (/[\x80-\xff]/.test(dupParamValue)) {
+ needsEscape = true;
+ dupParamValue = Services.io.escapeString(
+ dupParamValue,
+ Ci.nsINetUtil.ESCAPE_ALL
+ );
+ } else {
+ dupParamValue = this.makeFilenameQtext(dupParamValue, true);
+ }
+
+ let paramNameLen = paramName.length;
+ let paramValueLen = dupParamValue.length;
+ paramNameLen += 5; // *=__'__'___ or *[0]*=__'__'__ or *[1]*=___ or *[0]="___"
+ let foldedParam = "";
+
+ if (paramValueLen + paramNameLen + "UTF-8".length < PR_MAX_FOLDING_LEN) {
+ foldedParam = paramName;
+ if (needsEscape) {
+ foldedParam += "*=UTF-8''";
+ } else {
+ foldedParam += '="';
+ }
+ foldedParam += dupParamValue;
+ if (!needsEscape) {
+ foldedParam += '"';
+ }
+ } else {
+ let curLineLen = 0;
+ let counter = 0;
+ let start = 0;
+ let end = null;
+
+ while (paramValueLen > 0) {
+ curLineLen = 0;
+ if (counter == 0) {
+ foldedParam = paramName;
+ } else {
+ foldedParam += `;\r\n ${paramName}`;
+ }
+ foldedParam += `*${counter}`;
+ curLineLen += `*${counter}`.length;
+ if (needsEscape) {
+ foldedParam += "*=";
+ if (counter == 0) {
+ foldedParam += "UTF-8''";
+ curLineLen += "UTF-8".length;
+ }
+ } else {
+ foldedParam += '="';
+ }
+ counter++;
+ curLineLen += paramNameLen;
+ if (paramValueLen <= PR_MAX_FOLDING_LEN - curLineLen) {
+ end = start + paramValueLen;
+ } else {
+ end = start + (PR_MAX_FOLDING_LEN - curLineLen);
+ }
+
+ if (end && needsEscape) {
+ // Check to see if we are in the middle of escaped char.
+ // We use ESCAPE_ALL, so every third character is a '%'.
+ if (end - 1 > start && dupParamValue[end - 1] == "%") {
+ end -= 1;
+ } else if (end - 2 > start && dupParamValue[end - 2] == "%") {
+ end -= 2;
+ }
+ // *end is now a '%'.
+ // Check if the following UTF-8 octet is a continuation.
+ while (end - 3 > start && "89AB".includes(dupParamValue[end + 1])) {
+ end -= 3;
+ }
+ }
+ foldedParam += dupParamValue.slice(start, end);
+ if (!needsEscape) {
+ foldedParam += '"';
+ }
+ paramValueLen -= end - start;
+ start = end;
+ }
+ }
+
+ return foldedParam;
+ },
+
+ /**
+ * Get the target message folder to copy to.
+ *
+ * @param {nsIMsgIdentity} userIdentity - The user identity.
+ * @param {nsMsgDeliverMode} deliverMode - The deliver mode.
+ * @returns {string}
+ */
+ getMsgFolderURIFromPrefs(userIdentity, deliverMode) {
+ if (
+ deliverMode == Ci.nsIMsgSend.nsMsgQueueForLater ||
+ deliverMode == Ci.nsIMsgSend.nsMsgDeliverBackground
+ ) {
+ let uri = Services.prefs.getCharPref("mail.default_sendlater_uri");
+ // check if uri is unescaped, and if so, escape it and reset the pef.
+ if (!uri) {
+ return "anyfolder://";
+ } else if (uri.includes(" ")) {
+ uri.replaceAll(" ", "%20");
+ Services.prefs.setCharPref("mail.default_sendlater_uri", uri);
+ }
+ return uri;
+ } else if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft) {
+ return userIdentity.draftFolder;
+ } else if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) {
+ return userIdentity.stationeryFolder;
+ }
+ if (userIdentity.doFcc) {
+ return userIdentity.fccFolder;
+ }
+ return "";
+ },
+
+ /**
+ * Get the error string name of an exit code. The name will corresponds to an
+ * entry in composeMsgs.properties.
+ *
+ * @param {nsresult} exitCode - Exit code of sending mail process.
+ * @returns {string}
+ */
+ getErrorStringName(exitCode) {
+ let codeNameMap = {
+ [this.NS_MSG_UNABLE_TO_OPEN_FILE]: "unableToOpenFile",
+ [this.NS_MSG_UNABLE_TO_OPEN_TMP_FILE]: "unableToOpenTmpFile",
+ [this.NS_MSG_UNABLE_TO_SAVE_TEMPLATE]: "unableToSaveTemplate",
+ [this.NS_MSG_UNABLE_TO_SAVE_DRAFT]: "unableToSaveDraft",
+ [this.NS_MSG_COULDNT_OPEN_FCC_FOLDER]: "couldntOpenFccFolder",
+ [this.NS_MSG_NO_SENDER]: "noSender",
+ [this.NS_MSG_NO_RECIPIENTS]: "noRecipients",
+ [this.NS_MSG_ERROR_WRITING_FILE]: "errorWritingFile",
+ [this.NS_ERROR_SENDING_FROM_COMMAND]: "errorSendingFromCommand",
+ [this.NS_ERROR_SENDING_DATA_COMMAND]: "errorSendingDataCommand",
+ [this.NS_ERROR_SENDING_MESSAGE]: "errorSendingMessage",
+ [this.NS_ERROR_POST_FAILED]: "postFailed",
+ [this.NS_ERROR_SMTP_SERVER_ERROR]: "smtpServerError",
+ [this.NS_MSG_UNABLE_TO_SEND_LATER]: "unableToSendLater",
+ [this.NS_ERROR_COMMUNICATIONS_ERROR]: "communicationsError",
+ [this.NS_ERROR_BUT_DONT_SHOW_ALERT]: "dontShowAlert",
+ [this.NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS]:
+ "couldNotGetUsersMailAddress2",
+ [this.NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY]:
+ "couldNotGetSendersIdentity",
+ [this.NS_ERROR_MIME_MPART_ATTACHMENT_ERROR]: "mimeMpartAttachmentError",
+ [this.NS_ERROR_NNTP_NO_CROSS_POSTING]: "nntpNoCrossPosting",
+ [this.NS_MSG_ERROR_READING_FILE]: "errorReadingFile",
+ [this.NS_MSG_ERROR_ATTACHING_FILE]: "errorAttachingFile",
+ [this.NS_ERROR_SMTP_GREETING]: "incorrectSmtpGreeting",
+ [this.NS_ERROR_SENDING_RCPT_COMMAND]: "errorSendingRcptCommand",
+ [this.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS]: "startTlsFailed",
+ [this.NS_ERROR_SMTP_PASSWORD_UNDEFINED]: "smtpPasswordUndefined",
+ [this.NS_ERROR_SMTP_SEND_NOT_ALLOWED]: "smtpSendNotAllowed",
+ [this.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED]: "smtpTooManyRecipients",
+ [this.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2]: "smtpPermSizeExceeded2",
+ [this.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER]:
+ "smtpSendFailedUnknownServer",
+ [this.NS_ERROR_SMTP_SEND_FAILED_REFUSED]: "smtpSendRequestRefused",
+ [this.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED]: "smtpSendInterrupted",
+ [this.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT]: "smtpSendTimeout",
+ [this.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON]:
+ "smtpSendFailedUnknownReason",
+ [this.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL]:
+ "smtpHintAuthEncryptToPlainNoSsl",
+ [this.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL]:
+ "smtpHintAuthEncryptToPlainSsl",
+ [this.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT]:
+ "smtpHintAuthPlainToEncrypt",
+ [this.NS_ERROR_SMTP_AUTH_FAILURE]: "smtpAuthFailure",
+ [this.NS_ERROR_SMTP_AUTH_GSSAPI]: "smtpAuthGssapi",
+ [this.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED]: "smtpAuthMechNotSupported",
+ [this.NS_ERROR_ILLEGAL_LOCALPART]: "errorIllegalLocalPart2",
+ [this.NS_ERROR_CLIENTID]: "smtpClientid",
+ [this.NS_ERROR_CLIENTID_PERMISSION]: "smtpClientidPermission",
+ };
+ return codeNameMap[exitCode] || "sendFailed";
+ },
+
+ /**
+ * Format the error message that will be shown to the user.
+ *
+ * @param {nsIMsgIdentity} userIdentity - User identity.
+ * @param {nsIStringBundle} composeBundle - Localized string bundle.
+ * @param {string} errorName - The error name derived from an exit code.
+ * @returns {string}
+ */
+ formatStringWithSMTPHostName(userIdentity, composeBundle, errorName) {
+ let smtpServer = MailServices.smtp.getServerByIdentity(userIdentity);
+ let smtpHostname = smtpServer.hostname;
+ return composeBundle.formatStringFromName(errorName, [smtpHostname]);
+ },
+
+ /**
+ * Generate random alphanumeric string.
+ *
+ * @param {number} size - The length of generated string.
+ * @returns {string}
+ */
+ randomString(size) {
+ let length = Math.round((size * 3) / 4);
+ return btoa(
+ String.fromCharCode(
+ ...[...Array(length)].map(() => Math.floor(Math.random() * 256))
+ )
+ )
+ .slice(0, size)
+ .replaceAll(/[+/=]/g, "0");
+ },
+
+ /**
+ * Generate a content id to be used by embedded images.
+ *
+ * @param {nsIMsgIdentity} userIdentity - User identity.
+ * @param {number} partNum - The number of embedded MimePart.
+ * @returns {string}
+ */
+ makeContentId(userIdentity, partNum) {
+ let domain = userIdentity.email.split("@")[1];
+ return `part${partNum}.${this.randomString(8)}.${this.randomString(
+ 8
+ )}@${domain}`;
+ },
+
+ /**
+ * Pick a file name from the file URL.
+ *
+ * @param {string} url - The file URL.
+ * @returns {string}
+ */
+ pickFileNameFromUrl(url) {
+ if (/^(news|snews|imap|mailbox):/i.test(url)) {
+ // No sensible file name in it,
+ return "";
+ }
+ if (/^data:/i.test(url)) {
+ let matches = /filename=(.*);/.exec(url);
+ if (matches && matches[1]) {
+ return decodeURIComponent(matches[1]);
+ }
+ let mimeType = url.slice(5, url.indexOf(";"));
+ let extname = "";
+ try {
+ extname = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getPrimaryExtension(mimeType, null);
+ if (!extname) {
+ return "";
+ }
+ } catch (e) {
+ return "";
+ }
+ return `${this.randomString(16)}.${extname}`;
+ }
+ // Take the part after the last / or \.
+ let lastSlash = url.lastIndexOf("\\");
+ if (lastSlash == -1) {
+ lastSlash = url.lastIndexOf("/");
+ }
+ // Strip any search or anchor.
+ return url
+ .slice(lastSlash + 1)
+ .split("?")[0]
+ .split("#")[0];
+ },
+};
diff --git a/comm/mailnews/compose/src/MimePart.jsm b/comm/mailnews/compose/src/MimePart.jsm
new file mode 100644
index 0000000000..a22c0527af
--- /dev/null
+++ b/comm/mailnews/compose/src/MimePart.jsm
@@ -0,0 +1,378 @@
+/* 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 = ["MimePart", "MimeMultiPart"];
+
+let { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+let { MimeEncoder } = ChromeUtils.import("resource:///modules/MimeEncoder.jsm");
+let { MsgUtils } = ChromeUtils.import(
+ "resource:///modules/MimeMessageUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * A class to represent a RFC2045 message. MimePart can be nested, each MimePart
+ * can contain a list of MimePart. HTML and plain text are parts as well. Use
+ * class MimeMultiPart for multipart/*, that's why this class doesn't expose an
+ * addPart method
+ */
+class MimePart {
+ /**
+ * @param {string} contentType - Content type of the part, e.g. text/plain.
+ * @param {boolean} forceMsgEncoding - A flag used to determine Content-Transfer-Encoding.
+ * @param {boolean} isMainBody - The part is main part or an attachment part.
+ */
+ constructor(contentType = "", forceMsgEncoding = false, isMainBody = false) {
+ this._charset = "UTF-8";
+ this._contentType = contentType;
+ this._forceMsgEncoding = forceMsgEncoding;
+ this._isMainBody = isMainBody;
+
+ this._headers = new Map();
+ // 8-bit string to avoid converting back and forth.
+ this._bodyText = "";
+ this._bodyAttachment = null;
+ this._contentDisposition = null;
+ this._contentId = null;
+ this._separator = "";
+ this._parts = [];
+ }
+
+ /**
+ * @type {BinaryString} text - The string to use as body.
+ */
+ set bodyText(text) {
+ this._bodyText = text.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n");
+ }
+
+ /**
+ * @type {MimePart[]} - The child parts.
+ */
+ get parts() {
+ return this._parts;
+ }
+
+ /**
+ * @type {MimePart[]} parts - The child parts.
+ */
+ set parts(parts) {
+ this._parts = parts;
+ }
+
+ /**
+ * @type {string} - The separator string.
+ */
+ get separator() {
+ return this._separator;
+ }
+
+ /**
+ * Set a header.
+ *
+ * @param {string} name - The header name, e.g. "content-type".
+ * @param {string} content - The header content, e.g. "text/plain".
+ */
+ setHeader(name, content) {
+ if (!content) {
+ return;
+ }
+ // There is no Content-Type encoder in jsmime yet. If content is not string,
+ // assume it's already a structured header.
+ if (name == "content-type" || typeof content != "string") {
+ // _headers will be passed to jsmime, which requires header content to be
+ // an array.
+ this._headers.set(name, [content]);
+ return;
+ }
+ try {
+ this._headers.set(name, [
+ jsmime.headerparser.parseStructuredHeader(name, content),
+ ]);
+ } catch (e) {
+ this._headers.set(name, [content.trim()]);
+ }
+ }
+
+ /**
+ * Delete a header.
+ *
+ * @param {string} name - The header name to delete, e.g. "content-type".
+ */
+ deleteHeader(name) {
+ this._headers.delete(name);
+ }
+
+ /**
+ * Set headers by an iterable.
+ *
+ * @param {Iterable.<string, string>} entries - The header entries.
+ */
+ setHeaders(entries) {
+ for (let [name, content] of entries) {
+ this.setHeader(name, content);
+ }
+ }
+
+ /**
+ * Set an attachment as body, with optional contentDisposition and contentId.
+ *
+ * @param {nsIMsgAttachment} attachment - The attachment to use as body.
+ * @param {string} [contentDisposition=attachment] - "attachment" or "inline".
+ * @param {string} [contentId] - The url of an embedded object is cid:contentId.
+ */
+ setBodyAttachment(
+ attachment,
+ contentDisposition = "attachment",
+ contentId = null
+ ) {
+ this._bodyAttachment = attachment;
+ this._contentDisposition = contentDisposition;
+ this._contentId = contentId;
+ }
+
+ /**
+ * Add a child part.
+ *
+ * @param {MimePart} part - A MimePart.
+ */
+ addPart(part) {
+ this._parts.push(part);
+ }
+
+ /**
+ * Add child parts.
+ *
+ * @param {MimePart[]} parts - An array of MimePart.
+ */
+ addParts(parts) {
+ this._parts.push(...parts);
+ }
+
+ /**
+ * Pick an encoding according to _bodyText or _bodyAttachment content. Set
+ * content-transfer-encoding header, then return the encoded value.
+ *
+ * @returns {BinaryString}
+ */
+ async getEncodedBodyString() {
+ let bodyString = this._bodyText;
+ // If this is an attachment part, use the attachment content as bodyString.
+ if (this._bodyAttachment) {
+ try {
+ bodyString = await this._fetchAttachment();
+ } catch (e) {
+ MsgUtils.sendLogger.error(
+ `Failed to fetch attachment; name=${this._bodyAttachment.name}, url=${this._bodyAttachment.url}, error=${e}`
+ );
+ throw Components.Exception(
+ "Failed to fetch attachment",
+ MsgUtils.NS_MSG_ERROR_ATTACHING_FILE,
+ e.stack,
+ this._bodyAttachment
+ );
+ }
+ }
+ if (bodyString) {
+ let encoder = new MimeEncoder(
+ this._charset,
+ this._contentType,
+ this._forceMsgEncoding,
+ this._isMainBody,
+ bodyString
+ );
+ encoder.pickEncoding();
+ this.setHeader("content-transfer-encoding", encoder.encoding);
+ bodyString = encoder.encode();
+ } else if (this._isMainBody) {
+ this.setHeader("content-transfer-encoding", "7bit");
+ }
+ return bodyString;
+ }
+
+ /**
+ * Use jsmime to convert _headers to string.
+ *
+ * @returns {string}
+ */
+ getHeaderString() {
+ return jsmime.headeremitter.emitStructuredHeaders(this._headers, {
+ useASCII: true,
+ sanitizeDate: Services.prefs.getBoolPref(
+ "mail.sanitize_date_header",
+ false
+ ),
+ });
+ }
+
+ /**
+ * Fetch the attached message file to get its content.
+ *
+ * @returns {string}
+ */
+ async _fetchMsgAttachment() {
+ let msgService = MailServices.messageServiceFromURI(
+ this._bodyAttachment.url
+ );
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ _data: "",
+ _stream: null,
+ onDataAvailable(request, inputStream, offset, count) {
+ if (!this._stream) {
+ this._stream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ this._stream.init(inputStream);
+ }
+ this._data += this._stream.read(count);
+ },
+ onStartRequest() {},
+ onStopRequest(request, status) {
+ if (Components.isSuccessCode(status)) {
+ resolve(this._data);
+ } else {
+ reject(`Fetch message attachment failed with status=${status}`);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ };
+
+ msgService.streamMessage(
+ this._bodyAttachment.url,
+ streamListener,
+ null, // msgWindow
+ null, // urlListener
+ false, // convertData
+ "" // additionalHeader
+ );
+ });
+ }
+
+ /**
+ * Fetch the attachment file to get its content type and content.
+ *
+ * Previously, we used the Fetch API to download all attachments, but Fetch
+ * doesn't support url with embedded credentials (imap://name@server). As a
+ * result, it's unreliable when having two mail accounts on the same IMAP
+ * server.
+ *
+ * @returns {string}
+ */
+ async _fetchAttachment() {
+ let url = this._bodyAttachment.url;
+ MsgUtils.sendLogger.debug(`Fetching ${url}`);
+
+ let content = "";
+ if (/^[^:]+-message:/i.test(url)) {
+ content = await this._fetchMsgAttachment();
+ if (!content) {
+ // Message content is empty usually means it's (re)moved.
+ throw new Error("Message is gone");
+ }
+ this._contentType = "message/rfc822";
+ } else {
+ let channel = Services.io.newChannelFromURI(
+ Services.io.newURI(url),
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ content = await new Promise((resolve, reject) =>
+ NetUtil.asyncFetch(channel, (stream, status, request) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(`asyncFetch failed with status=${status}`);
+ return;
+ }
+ let data = "";
+ try {
+ data = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (e) {
+ // stream.available() throws if the file is empty.
+ }
+ resolve(data);
+ })
+ );
+ this._contentType =
+ this._bodyAttachment.contentType || channel.contentType;
+ }
+
+ let parmFolding = Services.prefs.getIntPref(
+ "mail.strictly_mime.parm_folding",
+ 2
+ );
+ // File name can contain non-ASCII chars, encode according to RFC 2231.
+ let encodedName, encodedFileName;
+ if (this._bodyAttachment.name) {
+ encodedName = MsgUtils.rfc2047EncodeParam(this._bodyAttachment.name);
+ encodedFileName = MsgUtils.rfc2231ParamFolding(
+ "filename",
+ this._bodyAttachment.name
+ );
+ }
+ this._charset = this._contentType.startsWith("text/")
+ ? MailStringUtils.detectCharset(content)
+ : "";
+
+ let contentTypeParams = "";
+ if (this._charset) {
+ contentTypeParams += `; charset=${this._charset}`;
+ }
+ if (encodedName && parmFolding != 2) {
+ contentTypeParams += `; name="${encodedName}"`;
+ }
+ this.setHeader("content-type", `${this._contentType}${contentTypeParams}`);
+ if (encodedFileName) {
+ this.setHeader(
+ "content-disposition",
+ `${this._contentDisposition}; ${encodedFileName}`
+ );
+ }
+ if (this._contentId) {
+ this.setHeader("content-id", `<${this._contentId}>`);
+ }
+ if (this._contentType == "text/html") {
+ let contentLocation = MsgUtils.getContentLocation(
+ this._bodyAttachment.url
+ );
+ this.setHeader("content-location", contentLocation);
+ } else if (this._contentType == "application/pgp-keys") {
+ this.setHeader("content-description", "OpenPGP public key");
+ }
+
+ return content;
+ }
+}
+
+/**
+ * A class to represent a multipart/* part inside a RFC2045 message.
+ */
+class MimeMultiPart extends MimePart {
+ /**
+ * @param {string} subtype - The multipart subtype, e.g. "alternative" or "mixed".
+ */
+ constructor(subtype) {
+ super();
+ this.subtype = subtype;
+ this._separator = this._makePartSeparator();
+ this.setHeader(
+ "content-type",
+ `multipart/${subtype}; boundary="${this._separator}"`
+ );
+ }
+
+ /**
+ * Use 12 hyphen characters and 24 random base64 characters as separator.
+ */
+ _makePartSeparator() {
+ return "------------" + MsgUtils.randomString(24);
+ }
+}
diff --git a/comm/mailnews/compose/src/SMTPProtocolHandler.jsm b/comm/mailnews/compose/src/SMTPProtocolHandler.jsm
new file mode 100644
index 0000000000..d139e15784
--- /dev/null
+++ b/comm/mailnews/compose/src/SMTPProtocolHandler.jsm
@@ -0,0 +1,39 @@
+/* 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 EXPORTED_SYMBOLS = ["SMTPProtocolHandler", "SMTPSProtocolHandler"];
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class SMTPProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "smtp";
+
+ newChannel(aURI, aLoadInfo) {
+ throw Components.Exception(
+ `${this.constructor.name}.newChannel not implemented`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ allowPort(port, scheme) {
+ return port == Ci.nsISmtpUrl.DEFAULT_SMTP_PORT;
+ }
+}
+SMTPProtocolHandler.prototype.classID = Components.ID(
+ "{b14c2b67-8680-4c11-8d63-9403c7d4f757}"
+);
+
+class SMTPSProtocolHandler extends SMTPProtocolHandler {
+ scheme = "smtps";
+
+ allowPort(port, scheme) {
+ return port == Ci.nsISmtpUrl.DEFAULT_SMTPS_PORT;
+ }
+}
+SMTPSProtocolHandler.prototype.classID = Components.ID(
+ "{057d0997-9e3a-411e-b4ee-2602f53fe05f}"
+);
diff --git a/comm/mailnews/compose/src/SmtpClient.jsm b/comm/mailnews/compose/src/SmtpClient.jsm
new file mode 100644
index 0000000000..2eb13985e3
--- /dev/null
+++ b/comm/mailnews/compose/src/SmtpClient.jsm
@@ -0,0 +1,1344 @@
+/* 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/.
+ *
+ * Based on https://github.com/emailjs/emailjs-smtp-client
+ *
+ * Copyright (c) 2013 Andris Reinman
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+const EXPORTED_SYMBOLS = ["SmtpClient"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+var { SmtpAuthenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+);
+var { MsgUtils } = ChromeUtils.import(
+ "resource:///modules/MimeMessageUtils.jsm"
+);
+
+class SmtpClient {
+ /**
+ * The number of RCPT TO commands sent on the connection by this client.
+ * This can count-up over multiple messages.
+ */
+ rcptCount = 0;
+
+ /**
+ * Set true only when doing a retry.
+ */
+ isRetry = false;
+
+ /**
+ * Creates a connection object to a SMTP server and allows to send mail through it.
+ * Call `connect` method to inititate the actual connection, the constructor only
+ * defines the properties but does not actually connect.
+ *
+ * @class
+ *
+ * @param {nsISmtpServer} server - The associated nsISmtpServer instance.
+ */
+ constructor(server) {
+ this.options = {
+ alwaysSTARTTLS:
+ server.socketType == Ci.nsMsgSocketType.trySTARTTLS ||
+ server.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS,
+ requireTLS: server.socketType == Ci.nsMsgSocketType.SSL,
+ };
+
+ this.socket = false; // Downstream TCP socket to the SMTP server, created with TCPSocket
+ this.waitDrain = false; // Keeps track if the downstream socket is currently full and a drain event should be waited for or not
+
+ // Private properties
+
+ this._server = server;
+ this._authenticator = new SmtpAuthenticator(server);
+ this._authenticating = false;
+ // A list of auth methods detected from the EHLO response.
+ this._supportedAuthMethods = [];
+ // A list of auth methods that worth a try.
+ this._possibleAuthMethods = [];
+ // Auth method set by user preference.
+ this._preferredAuthMethods =
+ {
+ [Ci.nsMsgAuthMethod.passwordCleartext]: ["PLAIN", "LOGIN"],
+ [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"],
+ [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"],
+ [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"],
+ [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"],
+ [Ci.nsMsgAuthMethod.secure]: ["CRAM-MD5", "XOAUTH2"],
+ }[server.authMethod] || [];
+ // The next auth method to try if the current failed.
+ this._nextAuthMethod = null;
+
+ // A list of capabilities detected from the EHLO response.
+ this._capabilities = [];
+
+ this._dataMode = false; // If true, accepts data from the upstream to be passed directly to the downstream socket. Used after the DATA command
+ this._lastDataBytes = ""; // Keep track of the last bytes to see how the terminating dot should be placed
+ this._envelope = null; // Envelope object for tracking who is sending mail to whom
+ this._currentAction = null; // Stores the function that should be run after a response has been received from the server
+
+ this._parseBlock = { data: [], statusCode: null };
+ this._parseRemainder = ""; // If the complete line is not received yet, contains the beginning of it
+
+ this.logger = MsgUtils.smtpLogger;
+
+ // Event placeholders
+ this.onerror = (e, failedSecInfo) => {}; // Will be run when an error occurs. The `onclose` event will fire subsequently.
+ this.ondrain = () => {}; // More data can be buffered in the socket.
+ this.onclose = () => {}; // The connection to the server has been closed
+ this.onidle = () => {}; // The connection is established and idle, you can send mail now
+ this.onready = failedRecipients => {}; // Waiting for mail body, lists addresses that were not accepted as recipients
+ this.ondone = success => {}; // The mail has been sent. Wait for `onidle` next. Indicates if the message was queued by the server.
+ // Callback when this client is ready to be reused.
+ this.onFree = () => {};
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ if (this.socket?.readyState == "open") {
+ this.logger.debug("Reusing a connection");
+ this.onidle();
+ } else {
+ let hostname = this._server.hostname.toLowerCase();
+ let port = this._server.port || (this.options.requireTLS ? 465 : 587);
+ this.logger.debug(`Connecting to smtp://${hostname}:${port}`);
+ this._secureTransport = this.options.requireTLS;
+ this.socket = new TCPSocket(hostname, port, {
+ binaryType: "arraybuffer",
+ useSecureTransport: this._secureTransport,
+ });
+
+ this.socket.onerror = this._onError;
+ this.socket.onopen = this._onOpen;
+ }
+ this._freed = false;
+ }
+
+ /**
+ * Sends QUIT
+ */
+ quit() {
+ this._authenticating = false;
+ this._freed = true;
+ this._sendCommand("QUIT");
+ this._currentAction = this.close;
+ }
+
+ /**
+ * Closes the connection to the server
+ *
+ * @param {boolean} [immediately] - Close the socket without waiting for
+ * unsent data.
+ */
+ close(immediately) {
+ if (this.socket && this.socket.readyState === "open") {
+ if (immediately) {
+ this.logger.debug(
+ `Closing connection to ${this._server.hostname} immediately!`
+ );
+ this.socket.closeImmediately();
+ } else {
+ this.logger.debug(`Closing connection to ${this._server.hostname}...`);
+ this.socket.close();
+ }
+ } else {
+ this.logger.debug(`Connection to ${this._server.hostname} closed`);
+ this._free();
+ }
+ }
+
+ // Mail related methods
+
+ /**
+ * Initiates a new message by submitting envelope data, starting with
+ * `MAIL FROM:` command. Use after `onidle` event
+ *
+ * @param {object} envelope - The envelope object.
+ * @param {string} envelope.from - The from address.
+ * @param {string[]} envelope.to - The to addresses.
+ * @param {number} envelope.size - The file size.
+ * @param {boolean} envelope.requestDSN - Whether to request Delivery Status Notifications.
+ * @param {boolean} envelope.messageId - The message id.
+ */
+ useEnvelope(envelope) {
+ this._envelope = envelope || {};
+ this._envelope.from = [].concat(
+ this._envelope.from || "anonymous@" + this._getHelloArgument()
+ )[0];
+
+ if (!this._capabilities.includes("SMTPUTF8")) {
+ // If server doesn't support SMTPUTF8, check if addresses contain invalid
+ // characters.
+
+ let recipients = this._envelope.to;
+ this._envelope.to = [];
+
+ for (let recipient of recipients) {
+ let lastAt = null;
+ let firstInvalid = null;
+ for (let i = 0; i < recipient.length; i++) {
+ let ch = recipient[i];
+ if (ch == "@") {
+ lastAt = i;
+ } else if ((ch < " " || ch > "~") && ch != "\t") {
+ firstInvalid = i;
+ break;
+ }
+ }
+ if (!recipient || firstInvalid != null) {
+ if (!lastAt) {
+ // Invalid char found in the localpart, throw error until we implement RFC 6532.
+ this._onNsError(MsgUtils.NS_ERROR_ILLEGAL_LOCALPART, recipient);
+ return;
+ }
+ // Invalid char found in the domainpart, convert it to ACE.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ let domain = idnService.convertUTF8toACE(recipient.slice(lastAt + 1));
+ recipient = `${recipient.slice(0, lastAt)}@${domain}`;
+ }
+ this._envelope.to.push(recipient);
+ }
+ }
+
+ // clone the recipients array for latter manipulation
+ this._envelope.rcptQueue = [...new Set(this._envelope.to)];
+ this._envelope.rcptFailed = [];
+ this._envelope.responseQueue = [];
+
+ if (!this._envelope.rcptQueue.length) {
+ this._onNsError(MsgUtils.NS_MSG_NO_RECIPIENTS);
+ return;
+ }
+
+ this._currentAction = this._actionMAIL;
+ let cmd = `MAIL FROM:<${this._envelope.from}>`;
+ if (
+ this._capabilities.includes("8BITMIME") &&
+ !Services.prefs.getBoolPref("mail.strictly_mime", false)
+ ) {
+ cmd += " BODY=8BITMIME";
+ }
+ if (this._capabilities.includes("SMTPUTF8")) {
+ // Should not send SMTPUTF8 if all ascii, see RFC6531.
+ // eslint-disable-next-line no-control-regex
+ let ascii = /^[\x00-\x7F]+$/;
+ if ([envelope.from, ...envelope.to].some(x => !ascii.test(x))) {
+ cmd += " SMTPUTF8";
+ }
+ }
+ if (this._capabilities.includes("SIZE")) {
+ cmd += ` SIZE=${this._envelope.size}`;
+ }
+ if (this._capabilities.includes("DSN") && this._envelope.requestDSN) {
+ let ret = Services.prefs.getBoolPref("mail.dsn.ret_full_on")
+ ? "FULL"
+ : "HDRS";
+ cmd += ` RET=${ret} ENVID=${envelope.messageId}`;
+ }
+ this._sendCommand(cmd);
+ }
+
+ /**
+ * Send ASCII data to the server. Works only in data mode (after `onready` event), ignored
+ * otherwise
+ *
+ * @param {string} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
+ * @returns {boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
+ */
+ send(chunk) {
+ // works only in data mode
+ if (!this._dataMode) {
+ // this line should never be reached but if it does,
+ // act like everything's normal.
+ return true;
+ }
+
+ // TODO: if the chunk is an arraybuffer, use a separate function to send the data
+ return this._sendString(chunk);
+ }
+
+ /**
+ * Indicates that a data stream for the socket is ended. Works only in data
+ * mode (after `onready` event), ignored otherwise. Use it when you are done
+ * with sending the mail. This method does not close the socket. Once the mail
+ * has been queued by the server, `ondone` and `onidle` are emitted.
+ *
+ * @param {Buffer} [chunk] Chunk of data to be sent to the server
+ */
+ end(chunk) {
+ // works only in data mode
+ if (!this._dataMode) {
+ // this line should never be reached but if it does,
+ // act like everything's normal.
+ return true;
+ }
+
+ if (chunk && chunk.length) {
+ this.send(chunk);
+ }
+
+ // redirect output from the server to _actionStream
+ this._currentAction = this._actionStream;
+
+ // indicate that the stream has ended by sending a single dot on its own line
+ // if the client already closed the data with \r\n no need to do it again
+ if (this._lastDataBytes === "\r\n") {
+ this.waitDrain = this._send(new Uint8Array([0x2e, 0x0d, 0x0a]).buffer); // .\r\n
+ } else if (this._lastDataBytes.substr(-1) === "\r") {
+ this.waitDrain = this._send(
+ new Uint8Array([0x0a, 0x2e, 0x0d, 0x0a]).buffer
+ ); // \n.\r\n
+ } else {
+ this.waitDrain = this._send(
+ new Uint8Array([0x0d, 0x0a, 0x2e, 0x0d, 0x0a]).buffer
+ ); // \r\n.\r\n
+ }
+
+ // End data mode.
+ this._dataMode = false;
+
+ return this.waitDrain;
+ }
+
+ // PRIVATE METHODS
+
+ /**
+ * Queue some data from the server for parsing.
+ *
+ * @param {string} chunk Chunk of data received from the server
+ */
+ _parse(chunk) {
+ // Lines should always end with <CR><LF> but you never know, might be only <LF> as well
+ var lines = (this._parseRemainder + (chunk || "")).split(/\r?\n/);
+ this._parseRemainder = lines.pop(); // not sure if the line has completely arrived yet
+
+ for (let i = 0, len = lines.length; i < len; i++) {
+ if (!lines[i].trim()) {
+ // nothing to check, empty line
+ continue;
+ }
+
+ // possible input strings for the regex:
+ // 250-MULTILINE REPLY
+ // 250 LAST LINE OF REPLY
+ // 250 1.2.3 MESSAGE
+
+ const match = lines[i].match(
+ /^(\d{3})([- ])(?:(\d+\.\d+\.\d+)(?: ))?(.*)/
+ );
+
+ if (match) {
+ this._parseBlock.data.push(match[4]);
+
+ if (match[2] === "-") {
+ // this is a multiline reply
+ this._parseBlock.statusCode =
+ this._parseBlock.statusCode || Number(match[1]);
+ } else {
+ const statusCode = Number(match[1]) || 0;
+ const response = {
+ statusCode,
+ data: this._parseBlock.data.join("\n"),
+ // Success means can move to the next step. Though 3xx is not
+ // failure, we don't consider it success here.
+ success: statusCode >= 200 && statusCode < 300,
+ };
+
+ this._onCommand(response);
+ this._parseBlock = {
+ data: [],
+ statusCode: null,
+ };
+ }
+ } else {
+ this._onCommand({
+ success: false,
+ statusCode: this._parseBlock.statusCode || null,
+ data: [lines[i]].join("\n"),
+ });
+ this._parseBlock = {
+ data: [],
+ statusCode: null,
+ };
+ }
+ }
+ }
+
+ // EVENT HANDLERS FOR THE SOCKET
+
+ /**
+ * Connection listener that is run when the connection to the server is opened.
+ * Sets up different event handlers for the opened socket
+ */
+ _onOpen = () => {
+ this.logger.debug("Connected");
+
+ this.socket.ondata = this._onData;
+ this.socket.onclose = this._onClose;
+ this.socket.ondrain = this._onDrain;
+
+ this._currentAction = this._actionGreeting;
+ this.socket.transport.setTimeout(
+ Ci.nsISocketTransport.TIMEOUT_READ_WRITE,
+ Services.prefs.getIntPref("mailnews.tcptimeout")
+ );
+ };
+
+ /**
+ * Data listener for chunks of data emitted by the server
+ *
+ * @param {Event} evt - Event object. See `evt.data` for the chunk received
+ */
+ _onData = async evt => {
+ let stringPayload = new TextDecoder("UTF-8").decode(
+ new Uint8Array(evt.data)
+ );
+ // "S: " to denote that this is data from the Server.
+ this.logger.debug(`S: ${stringPayload}`);
+
+ // Prevent blocking the main thread, otherwise onclose/onerror may not be
+ // called in time. test_smtpPasswordFailure3 is such a case, the server
+ // rejects AUTH PLAIN then closes the connection, the client then sends AUTH
+ // LOGIN. This line guarantees onclose is called before sending AUTH LOGIN.
+ await new Promise(resolve => setTimeout(resolve));
+ this._parse(stringPayload);
+ };
+
+ /**
+ * More data can be buffered in the socket, `waitDrain` is reset to false
+ */
+ _onDrain = () => {
+ this.waitDrain = false;
+ this.ondrain();
+ };
+
+ /**
+ * Error handler. Emits an nsresult value.
+ *
+ * @param {Error|TCPSocketErrorEvent} event - An Error or TCPSocketErrorEvent object.
+ */
+ _onError = async event => {
+ this.logger.error(`${event.name}: a ${event.message} error occurred`);
+ if (this._freed) {
+ // Ignore socket errors if already freed.
+ return;
+ }
+
+ this._free();
+ this.quit();
+
+ let nsError = Cr.NS_ERROR_FAILURE;
+ let secInfo = null;
+ if (TCPSocketErrorEvent.isInstance(event)) {
+ nsError = event.errorCode;
+ secInfo =
+ await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo();
+ if (secInfo) {
+ this.logger.error(`SecurityError info: ${secInfo.errorCodeString}`);
+ if (secInfo.failedCertChain.length) {
+ let chain = secInfo.failedCertChain.map(c => {
+ return c.commonName + "; serial# " + c.serialNumber;
+ });
+ this.logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`);
+ }
+ this._server.closeCachedConnections();
+ }
+ }
+
+ // Use nsresult to integrate with other parts of sending process, e.g.
+ // MessageSend.jsm will show an error message depending on the nsresult.
+ this.onerror(nsError, "", secInfo);
+ };
+
+ /**
+ * Error handler. Emits an nsresult value.
+ *
+ * @param {nsresult} nsError - A nsresult.
+ * @param {string} errorParam - Param to form the error message.
+ * @param {string} [extra] - Some messages take two arguments to format.
+ * @param {number} [statusCode] - Only needed when checking need to retry.
+ */
+ _onNsError(nsError, errorParam, extra, statusCode) {
+ // First check if handling an error response that might need a retry.
+ if ([this._actionMAIL, this._actionRCPT].includes(this._currentAction)) {
+ if (statusCode >= 400 && statusCode < 500) {
+ // Possibly too many recipients, too many messages, to much data
+ // or too much time has elapsed on this connection.
+ if (!this.isRetry) {
+ // Now seeing error 4xx meaning that the current message can't be
+ // accepted. We close the connection and try again to send on a new
+ // connection using this same client instance. If the retry also
+ // fails on the new connection, we give up and report the error.
+ this.logger.debug("Retry send on new connection.");
+ this.quit();
+ this.isRetry = true; // flag that we will retry on new connection
+ this.close(true);
+ this.connect();
+ return; // return without reporting the error yet
+ }
+ }
+ }
+
+ let errorName = MsgUtils.getErrorStringName(nsError);
+ let errorMessage = "";
+ if (
+ [
+ MsgUtils.NS_ERROR_SMTP_SERVER_ERROR,
+ MsgUtils.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED,
+ MsgUtils.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2,
+ MsgUtils.NS_ERROR_SENDING_FROM_COMMAND,
+ MsgUtils.NS_ERROR_SENDING_RCPT_COMMAND,
+ MsgUtils.NS_ERROR_SENDING_DATA_COMMAND,
+ MsgUtils.NS_ERROR_SENDING_MESSAGE,
+ MsgUtils.NS_ERROR_ILLEGAL_LOCALPART,
+ ].includes(nsError)
+ ) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+ if (nsError == MsgUtils.NS_ERROR_ILLEGAL_LOCALPART) {
+ errorMessage = bundle
+ .GetStringFromName(errorName)
+ .replace("%s", errorParam);
+ } else {
+ errorMessage = bundle.formatStringFromName(errorName, [
+ errorParam,
+ extra,
+ ]);
+ }
+ }
+ this.onerror(nsError, errorMessage);
+ this.close();
+ }
+
+ /**
+ * Indicates that the socket has been closed
+ */
+ _onClose = () => {
+ this.logger.debug("Socket closed.");
+ this._free();
+ this.rcptCount = 0;
+ if (this._authenticating) {
+ // In some cases, socket is closed for invalid username/password.
+ this._onAuthFailed({ data: "Socket closed." });
+ }
+ };
+
+ /**
+ * This is not a socket data handler but the handler for data emitted by the parser,
+ * so this data is safe to use as it is always complete (server might send partial chunks)
+ *
+ * @param {object} command - Parsed data.
+ */
+ _onCommand(command) {
+ if (command.statusCode < 200 || command.statusCode >= 400) {
+ // @see https://datatracker.ietf.org/doc/html/rfc5321#section-3.8
+ // 421: SMTP service shutting down and closing transmission channel.
+ // When that happens during idle, just close the connection.
+ if (
+ command.statusCode == 421 &&
+ this._currentAction == this._actionIdle
+ ) {
+ this.close(true);
+ return;
+ }
+
+ this.logger.error(
+ `Command failed: ${command.statusCode} ${command.data}; currentAction=${this._currentAction?.name}`
+ );
+ }
+ if (typeof this._currentAction === "function") {
+ this._currentAction(command);
+ }
+ }
+
+ /**
+ * This client has finished the current process and ready to be reused.
+ */
+ _free() {
+ if (!this._freed) {
+ this._freed = true;
+ this.onFree();
+ }
+ }
+
+ /**
+ * Sends a string to the socket.
+ *
+ * @param {string} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
+ * @returns {boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
+ */
+ _sendString(chunk) {
+ // escape dots
+ if (!this.options.disableEscaping) {
+ chunk = chunk.replace(/\n\./g, "\n..");
+ if (
+ (this._lastDataBytes.substr(-1) === "\n" || !this._lastDataBytes) &&
+ chunk.charAt(0) === "."
+ ) {
+ chunk = "." + chunk;
+ }
+ }
+
+ // Keeping eye on the last bytes sent, to see if there is a <CR><LF> sequence
+ // at the end which is needed to end the data stream
+ if (chunk.length > 2) {
+ this._lastDataBytes = chunk.substr(-2);
+ } else if (chunk.length === 1) {
+ this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk;
+ }
+
+ this.logger.debug("Sending " + chunk.length + " bytes of payload");
+
+ // pass the chunk to the socket
+ this.waitDrain = this._send(
+ MailStringUtils.byteStringToUint8Array(chunk).buffer
+ );
+ return this.waitDrain;
+ }
+
+ /**
+ * Send a string command to the server, also append CRLF if needed.
+ *
+ * @param {string} str - String to be sent to the server.
+ * @param {boolean} [suppressLogging=false] - If true and not in dev mode,
+ * do not log the str. For non-release builds output won't be suppressed,
+ * so that debugging auth problems is easier.
+ */
+ _sendCommand(str, suppressLogging = false) {
+ if (this.socket.readyState !== "open") {
+ if (str != "QUIT") {
+ this.logger.warn(
+ `Failed to send "${str}" because socket state is ${this.socket.readyState}`
+ );
+ }
+ return;
+ }
+ // "C: " is used to denote that this is data from the Client.
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this.logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ this.logger.debug(`C: ${str}`);
+ }
+ this.waitDrain = this._send(
+ new TextEncoder().encode(str + (str.substr(-2) !== "\r\n" ? "\r\n" : ""))
+ .buffer
+ );
+ }
+
+ _send(buffer) {
+ return this.socket.send(buffer);
+ }
+
+ /**
+ * Intitiate authentication sequence if needed
+ *
+ * @param {boolean} forceNewPassword - Discard cached password.
+ */
+ async _authenticateUser(forceNewPassword) {
+ if (
+ this._preferredAuthMethods.length == 0 ||
+ this._supportedAuthMethods.length == 0
+ ) {
+ // no need to authenticate, at least no data given
+ this._currentAction = this._actionIdle;
+ this.onidle(); // ready to take orders
+ return;
+ }
+
+ if (!this._nextAuthMethod) {
+ this._onAuthFailed({ data: "No available auth method." });
+ return;
+ }
+
+ this._authenticating = true;
+
+ this._currentAuthMethod = this._nextAuthMethod;
+ this._nextAuthMethod =
+ this._possibleAuthMethods[
+ this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1
+ ];
+ this.logger.debug(`Current auth method: ${this._currentAuthMethod}`);
+
+ switch (this._currentAuthMethod) {
+ case "LOGIN":
+ // LOGIN is a 3 step authentication process
+ // C: AUTH LOGIN
+ // C: BASE64(USER)
+ // C: BASE64(PASS)
+ this.logger.debug("Authentication via AUTH LOGIN");
+ this._currentAction = this._actionAUTH_LOGIN_USER;
+ this._sendCommand("AUTH LOGIN");
+ return;
+ case "PLAIN":
+ // AUTH PLAIN is a 1 step authentication process
+ // C: AUTH PLAIN BASE64(\0 USER \0 PASS)
+ this.logger.debug("Authentication via AUTH PLAIN");
+ this._currentAction = this._actionAUTHComplete;
+ this._sendCommand(
+ "AUTH PLAIN " + this._authenticator.getPlainToken(),
+ true
+ );
+ return;
+ case "CRAM-MD5":
+ this.logger.debug("Authentication via AUTH CRAM-MD5");
+ this._currentAction = this._actionAUTH_CRAM;
+ this._sendCommand("AUTH CRAM-MD5");
+ return;
+ case "XOAUTH2":
+ // See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange
+ this.logger.debug("Authentication via AUTH XOAUTH2");
+ this._currentAction = this._actionAUTH_XOAUTH2;
+ let oauthToken = await this._authenticator.getOAuthToken();
+ this._sendCommand("AUTH XOAUTH2 " + oauthToken, true);
+ return;
+ case "GSSAPI": {
+ this.logger.debug("Authentication via AUTH GSSAPI");
+ this._currentAction = this._actionAUTH_GSSAPI;
+ this._authenticator.initGssapiAuth("smtp");
+ let token;
+ try {
+ token = this._authenticator.getNextGssapiToken("");
+ } catch (e) {
+ this.logger.error(e);
+ this._actionAUTHComplete({ success: false, data: "AUTH GSSAPI" });
+ return;
+ }
+ this._sendCommand(`AUTH GSSAPI ${token}`, true);
+ return;
+ }
+ case "NTLM": {
+ this.logger.debug("Authentication via AUTH NTLM");
+ this._currentAction = this._actionAUTH_NTLM;
+ this._authenticator.initNtlmAuth("smtp");
+ let token;
+ try {
+ token = this._authenticator.getNextNtlmToken("");
+ } catch (e) {
+ this.logger.error(e);
+ this._actionAUTHComplete({ success: false, data: "AUTH NTLM" });
+ return;
+ }
+ this._sendCommand(`AUTH NTLM ${token}`, true);
+ return;
+ }
+ }
+
+ this._onAuthFailed({
+ data: `Unknown authentication method ${this._currentAuthMethod}`,
+ });
+ }
+
+ _onAuthFailed(command) {
+ this.logger.error(`Authentication failed: ${command.data}`);
+ if (!this._freed) {
+ if (this._nextAuthMethod) {
+ // Try the next auth method.
+ this._authenticateUser();
+ return;
+ } else if (!this._currentAuthMethod) {
+ // No auth method was even tried.
+ let err;
+ if (
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordEncrypted &&
+ (this._supportedAuthMethods.includes("PLAIN") ||
+ this._supportedAuthMethods.includes("LOGIN"))
+ ) {
+ // Pref has encrypted password, server claims to support plaintext
+ // password.
+ err = [
+ Ci.nsMsgSocketType.alwaysSTARTTLS,
+ Ci.nsMsgSocketType.SSL,
+ ].includes(this._server.socketType)
+ ? MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL
+ : MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL;
+ } else if (
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext &&
+ this._supportedAuthMethods.includes("CRAM-MD5")
+ ) {
+ // Pref has plaintext password, server claims to support encrypted
+ // password.
+ err = MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT;
+ } else {
+ err = MsgUtils.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED;
+ }
+ this._onNsError(err);
+ return;
+ }
+ }
+
+ // Ask user what to do.
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this.logger.error(`Authentication failed: ${command.data}`);
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE);
+ return;
+ } else if (action == 2) {
+ // 'New password' button pressed. Forget cached password, new password
+ // will be asked.
+ this._authenticator.forgetPassword();
+ }
+
+ if (this._freed) {
+ // If connection is lost, reconnect.
+ this.connect();
+ return;
+ }
+
+ // Reset _nextAuthMethod to start again.
+ this._nextAuthMethod = this._possibleAuthMethods[0];
+ if (action == 2 || action == 0) {
+ // action = 0 means retry button pressed.
+ this._authenticateUser();
+ }
+ }
+
+ _getHelloArgument() {
+ let helloArgument = this._server.helloArgument;
+ if (helloArgument) {
+ return helloArgument;
+ }
+
+ try {
+ // The address format follows rfc5321#section-4.1.3.
+ let netAddr = this.socket?.transport.getScriptableSelfAddr();
+ let address = netAddr.address;
+ if (netAddr.family === Ci.nsINetAddr.FAMILY_INET6) {
+ return `[IPV6:${address}]`;
+ }
+ return `[${address}]`;
+ } catch (e) {}
+
+ return "[127.0.0.1]";
+ }
+
+ // ACTIONS FOR RESPONSES FROM THE SMTP SERVER
+
+ /**
+ * Initial response from the server, must have a status 220
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionGreeting(command) {
+ if (command.statusCode !== 220) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+
+ if (this.options.lmtp) {
+ this._currentAction = this._actionLHLO;
+ this._sendCommand("LHLO " + this._getHelloArgument());
+ } else {
+ this._currentAction = this._actionEHLO;
+ this._sendCommand("EHLO " + this._getHelloArgument());
+ }
+ }
+
+ /**
+ * Response to LHLO
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionLHLO(command) {
+ if (!command.success) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+
+ // Process as EHLO response
+ this._actionEHLO(command);
+ }
+
+ /**
+ * Response to EHLO. If the response is an error, try HELO instead
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionEHLO(command) {
+ if ([500, 502].includes(command.statusCode)) {
+ // EHLO is not implemented by the server.
+ if (this.options.alwaysSTARTTLS) {
+ // If alwaysSTARTTLS is set by the user, EHLO is required to advertise it.
+ this._onNsError(MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS);
+ return;
+ }
+
+ // Try HELO instead
+ this.logger.warn(
+ "EHLO not successful, trying HELO " + this._getHelloArgument()
+ );
+ this._currentAction = this._actionHELO;
+ this._sendCommand("HELO " + this._getHelloArgument());
+ return;
+ } else if (!command.success) {
+ // 501 Syntax error or some other error.
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+
+ this._supportedAuthMethods = [];
+
+ let lines = command.data.toUpperCase().split("\n");
+ // Skip the first greeting line.
+ for (let line of lines.slice(1)) {
+ if (line.startsWith("AUTH ")) {
+ this._supportedAuthMethods = line.slice(5).split(" ");
+ } else {
+ this._capabilities.push(line.split(" ")[0]);
+ }
+ }
+
+ if (!this._secureTransport && this.options.alwaysSTARTTLS) {
+ // STARTTLS is required by the user. Detect if the server supports it.
+ if (this._capabilities.includes("STARTTLS")) {
+ this._currentAction = this._actionSTARTTLS;
+ this._sendCommand("STARTTLS");
+ return;
+ }
+ // STARTTLS is required but not advertised.
+ this._onNsError(MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS);
+ return;
+ }
+
+ // If a preferred method is not supported by the server, no need to try it.
+ this._possibleAuthMethods = this._preferredAuthMethods.filter(x =>
+ this._supportedAuthMethods.includes(x)
+ );
+ this.logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`);
+ this._nextAuthMethod = this._possibleAuthMethods[0];
+
+ if (
+ this._capabilities.includes("CLIENTID") &&
+ (this._secureTransport ||
+ // For test purpose.
+ ["localhost", "127.0.0.1", "::1"].includes(this._server.hostname)) &&
+ this._server.clientidEnabled &&
+ this._server.clientid
+ ) {
+ // Client identity extension, still a draft.
+ this._currentAction = this._actionCLIENTID;
+ this._sendCommand("CLIENTID UUID " + this._server.clientid, true);
+ } else {
+ this._authenticateUser();
+ }
+ }
+
+ /**
+ * Handles server response for STARTTLS command. If there's an error
+ * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
+ * succeeds restart the EHLO
+ *
+ * @param {string} command - Message from the server.
+ */
+ _actionSTARTTLS(command) {
+ if (!command.success) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+
+ this.socket.upgradeToSecure();
+ this._secureTransport = true;
+
+ // restart protocol flow
+ this._currentAction = this._actionEHLO;
+ this._sendCommand("EHLO " + this._getHelloArgument());
+ }
+
+ /**
+ * Response to HELO
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionHELO(command) {
+ if (!command.success) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+ this._authenticateUser();
+ }
+
+ /**
+ * Handles server response for CLIENTID command. If successful then will
+ * initiate the authenticateUser process.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionCLIENTID(command) {
+ if (!command.success) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ return;
+ }
+ this._authenticateUser();
+ }
+
+ /**
+ * Returns the saved/cached server password, or show a password dialog. If the
+ * user cancels the dialog, abort sending.
+ *
+ * @returns {string} The server password.
+ */
+ _getPassword() {
+ try {
+ return this._authenticator.getPassword();
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ this.quit();
+ this.onerror(e.result);
+ } else {
+ throw e;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Response to AUTH LOGIN, if successful expects base64 encoded username
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionAUTH_LOGIN_USER(command) {
+ if (command.statusCode !== 334 || command.data !== "VXNlcm5hbWU6") {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data);
+ return;
+ }
+ this.logger.debug("AUTH LOGIN USER");
+ this._currentAction = this._actionAUTH_LOGIN_PASS;
+ this._sendCommand(btoa(this._authenticator.username), true);
+ }
+
+ /**
+ * Process the response to AUTH LOGIN with a username. If successful, expects
+ * a base64-encoded password.
+ *
+ * @param {{statusCode: number, data: string}} command - Parsed command from
+ * the server.
+ */
+ _actionAUTH_LOGIN_PASS(command) {
+ if (
+ command.statusCode !== 334 ||
+ (command.data !== btoa("Password:") && command.data !== btoa("password:"))
+ ) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data);
+ return;
+ }
+ this.logger.debug("AUTH LOGIN PASS");
+ this._currentAction = this._actionAUTHComplete;
+ let password = this._getPassword();
+ if (
+ !Services.prefs.getBoolPref(
+ "mail.smtp_login_pop3_user_pass_auth_is_latin1",
+ true
+ ) ||
+ !/^[\x00-\xFF]+$/.test(password) // eslint-disable-line no-control-regex
+ ) {
+ // Unlike PLAIN auth, the payload of LOGIN auth is not standardized. When
+ // `mail.smtp_login_pop3_user_pass_auth_is_latin1` is true, we apply
+ // base64 encoding directly. Otherwise, we convert it to UTF-8
+ // BinaryString first.
+ password = MailStringUtils.stringToByteString(password);
+ }
+ this._sendCommand(btoa(password), true);
+ }
+
+ /**
+ * Response to AUTH CRAM, if successful expects base64 encoded challenge.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ async _actionAUTH_CRAM(command) {
+ if (command.statusCode !== 334) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data);
+ return;
+ }
+ this._currentAction = this._actionAUTHComplete;
+ this._sendCommand(
+ this._authenticator.getCramMd5Token(this._getPassword(), command.data),
+ true
+ );
+ }
+
+ /**
+ * Response to AUTH XOAUTH2 token, if error occurs send empty response
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionAUTH_XOAUTH2(command) {
+ if (!command.success) {
+ this.logger.warn("Error during AUTH XOAUTH2, sending empty response");
+ this._sendCommand("");
+ this._currentAction = this._actionAUTHComplete;
+ } else {
+ this._actionAUTHComplete(command);
+ }
+ }
+
+ /**
+ * Response to AUTH GSSAPI, if successful expects a base64 encoded challenge.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionAUTH_GSSAPI(command) {
+ // GSSAPI auth can be multiple steps. We exchange tokens with the server
+ // until success or failure.
+ if (command.success) {
+ this._actionAUTHComplete(command);
+ return;
+ }
+ if (command.statusCode !== 334) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_GSSAPI, command.data);
+ return;
+ }
+ let token = this._authenticator.getNextGssapiToken(command.data);
+ this._currentAction = this._actionAUTH_GSSAPI;
+ this._sendCommand(token, true);
+ }
+
+ /**
+ * Response to AUTH NTLM, if successful expects a base64 encoded challenge.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionAUTH_NTLM(command) {
+ // NTLM auth can be multiple steps. We exchange tokens with the server
+ // until success or failure.
+ if (command.success) {
+ this._actionAUTHComplete(command);
+ return;
+ }
+ if (command.statusCode !== 334) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data);
+ return;
+ }
+ let token = this._authenticator.getNextNtlmToken(command.data);
+ this._currentAction = this._actionAUTH_NTLM;
+ this._sendCommand(token, true);
+ }
+
+ /**
+ * Checks if authentication succeeded or not. If successfully authenticated
+ * emit `idle` to indicate that an e-mail can be sent using this connection
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionAUTHComplete(command) {
+ this._authenticating = false;
+ if (!command.success) {
+ this._onAuthFailed(command);
+ return;
+ }
+
+ this.logger.debug("Authentication successful.");
+
+ this._currentAction = this._actionIdle;
+ this.onidle(); // ready to take orders
+ }
+
+ /**
+ * Used when the connection is idle, not expecting anything from the server.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionIdle(command) {
+ this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data);
+ }
+
+ /**
+ * Response to MAIL FROM command. Proceed to defining RCPT TO list if successful
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionMAIL(command) {
+ if (!command.success) {
+ let errorCode = MsgUtils.NS_ERROR_SENDING_FROM_COMMAND; // default code
+ if (command.statusCode == 552) {
+ // Too much mail data indicated by "size" parameter of MAIL FROM.
+ // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.9
+ errorCode = MsgUtils.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2;
+ }
+ if (command.statusCode == 452 || command.statusCode == 451) {
+ // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.10
+ errorCode = MsgUtils.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED;
+ }
+ this._onNsError(errorCode, command.data, null, command.statusCode);
+ return;
+ }
+ this.logger.debug(
+ "MAIL FROM successful, proceeding with " +
+ this._envelope.rcptQueue.length +
+ " recipients"
+ );
+ this.logger.debug("Adding recipient...");
+ this._envelope.curRecipient = this._envelope.rcptQueue.shift();
+ this._currentAction = this._actionRCPT;
+ this._sendCommand(
+ `RCPT TO:<${this._envelope.curRecipient}>${this._getRCPTParameters()}`
+ );
+ }
+
+ /**
+ * Prepare the RCPT params, currently only DSN params. If the server supports
+ * DSN and sender requested DSN, append DSN params to each RCPT TO command.
+ */
+ _getRCPTParameters() {
+ if (this._capabilities.includes("DSN") && this._envelope.requestDSN) {
+ let notify = [];
+ if (Services.prefs.getBoolPref("mail.dsn.request_never_on")) {
+ notify.push("NEVER");
+ } else {
+ if (Services.prefs.getBoolPref("mail.dsn.request_on_success_on")) {
+ notify.push("SUCCESS");
+ }
+ if (Services.prefs.getBoolPref("mail.dsn.request_on_failure_on")) {
+ notify.push("FAILURE");
+ }
+ if (Services.prefs.getBoolPref("mail.dsn.request_on_delay_on")) {
+ notify.push("DELAY");
+ }
+ }
+ if (notify.length > 0) {
+ return ` NOTIFY=${notify.join(",")}`;
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Response to a RCPT TO command. If the command is unsuccessful, emit an
+ * error to abort the sending.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionRCPT(command) {
+ if (!command.success) {
+ this._onNsError(
+ MsgUtils.NS_ERROR_SENDING_RCPT_COMMAND,
+ command.data,
+ this._envelope.curRecipient,
+ command.statusCode
+ );
+ return;
+ }
+ this.rcptCount++;
+ this._envelope.responseQueue.push(this._envelope.curRecipient);
+
+ if (this._envelope.rcptQueue.length) {
+ // Send the next recipient.
+ this._envelope.curRecipient = this._envelope.rcptQueue.shift();
+ this._currentAction = this._actionRCPT;
+ this._sendCommand(
+ `RCPT TO:<${this._envelope.curRecipient}>${this._getRCPTParameters()}`
+ );
+ } else {
+ this.logger.debug(
+ `Total RCPTs during this connection: ${this.rcptCount}`
+ );
+ this.logger.debug("RCPT TO done. Proceeding with payload.");
+ this._currentAction = this._actionDATA;
+ this._sendCommand("DATA");
+ }
+ }
+
+ /**
+ * Response to the DATA command. Server is now waiting for a message, so emit `onready`
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionDATA(command) {
+ // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
+ // some servers might use 250 instead
+ if (![250, 354].includes(command.statusCode)) {
+ this._onNsError(MsgUtils.NS_ERROR_SENDING_DATA_COMMAND, command.data);
+ return;
+ }
+
+ this._dataMode = true;
+ this._currentAction = this._actionIdle;
+ this.onready(this._envelope.rcptFailed);
+ }
+
+ /**
+ * Response from the server, once the message stream has ended with <CR><LF>.<CR><LF>
+ * Emits `ondone`.
+ *
+ * @param {object} command Parsed command from the server {statusCode, data}
+ */
+ _actionStream(command) {
+ var rcpt;
+
+ if (this.options.lmtp) {
+ // LMTP returns a response code for *every* successfully set recipient
+ // For every recipient the message might succeed or fail individually
+
+ rcpt = this._envelope.responseQueue.shift();
+ if (!command.success) {
+ this.logger.error("Local delivery to " + rcpt + " failed.");
+ this._envelope.rcptFailed.push(rcpt);
+ } else {
+ this.logger.error("Local delivery to " + rcpt + " succeeded.");
+ }
+
+ if (this._envelope.responseQueue.length) {
+ this._currentAction = this._actionStream;
+ return;
+ }
+
+ this._currentAction = this._actionIdle;
+ this.ondone(0);
+ } else {
+ // For SMTP the message either fails or succeeds, there is no information
+ // about individual recipients
+
+ if (!command.success) {
+ this.logger.error("Message sending failed.");
+ } else {
+ this.logger.debug("Message sent successfully.");
+ this.isRetry = false;
+ }
+
+ this._currentAction = this._actionIdle;
+ if (command.success) {
+ this.ondone(0);
+ } else {
+ this._onNsError(MsgUtils.NS_ERROR_SENDING_MESSAGE, command.data);
+ }
+ }
+
+ this._freed = true;
+ this.onFree();
+ }
+}
diff --git a/comm/mailnews/compose/src/SmtpServer.jsm b/comm/mailnews/compose/src/SmtpServer.jsm
new file mode 100644
index 0000000000..3ce81ff936
--- /dev/null
+++ b/comm/mailnews/compose/src/SmtpServer.jsm
@@ -0,0 +1,519 @@
+/* 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 = ["SmtpServer"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ SmtpClient: "resource:///modules/SmtpClient.jsm",
+});
+
+/**
+ * This class represents a single SMTP server.
+ *
+ * @implements {nsISmtpServer}
+ * @implements {nsIObserver}
+ */
+
+class SmtpServer {
+ QueryInterface = ChromeUtils.generateQI(["nsISmtpServer", "nsIObserver"]);
+
+ constructor() {
+ this._key = "";
+ this._loadPrefs();
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed");
+ }
+
+ /**
+ * Observe() receives notifications for all accounts, not just this SMTP
+ * server's * account. So we ignore all notifications not intended for this
+ * server. When the state of the password manager changes we need to clear the
+ * this server's password from the cache in case the user just changed or
+ * removed the password or username.
+ * OAuth2 servers often automatically change the password manager's stored
+ * password (the token).
+ */
+ observe(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ // Check that the notification is for this server and user.
+ let otherFullName = "";
+ let otherUsername = "";
+ if (subject instanceof Ci.nsILoginInfo) {
+ // The login info for a server has been removed with aData being
+ // "removeLogin" or "removeAllLogins".
+ otherFullName = subject.origin;
+ otherUsername = subject.username;
+ } else if (subject instanceof Ci.nsIArray) {
+ // Probably a 2 element array containing old and new login info due to
+ // aData being "modifyLogin". E.g., a user has modified the password or
+ // username in the password manager or an OAuth2 token string has
+ // automatically changed. Only need to look at names in first array
+ // element (login info before any modification) since the user might
+ // have changed the username as found in the 2nd elements. (The
+ // hostname can't be modified in the password manager.
+ otherFullName = subject.queryElementAt(0, Ci.nsISupports).origin;
+ otherUsername = subject.queryElementAt(0, Ci.nsISupports).username;
+ }
+ if (otherFullName) {
+ if (
+ otherFullName != "smtp://" + this.hostname ||
+ otherUsername != this.username
+ ) {
+ // Not for this account; keep this account's password.
+ return;
+ }
+ } else if (data != "hostSavingDisabled") {
+ // "hostSavingDisabled" only occurs during test_smtpServer.js and
+ // expects the password to be removed from memory cache. Otherwise, we
+ // don't have enough information to decide to remove the cached
+ // password, so keep it.
+ return;
+ }
+ // Remove the password for this server cached in memory.
+ this.password = "";
+ }
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ set key(key) {
+ this._key = key;
+ this._loadPrefs();
+ }
+
+ get UID() {
+ let uid = this._prefs.getStringPref("uid", "");
+ if (uid) {
+ return uid;
+ }
+ return (this.UID = Services.uuid
+ .generateUUID()
+ .toString()
+ .substring(1, 37));
+ }
+
+ set UID(uid) {
+ if (this._prefs.prefHasUserValue("uid")) {
+ throw new Components.Exception("uid is already set", Cr.NS_ERROR_ABORT);
+ }
+ this._prefs.setStringPref("uid", uid);
+ }
+
+ get description() {
+ return this._prefs.getStringPref("description", "");
+ }
+
+ set description(value) {
+ this._prefs.setStringPref("description", value);
+ }
+
+ get hostname() {
+ return this._prefs.getStringPref("hostname", "");
+ }
+
+ set hostname(value) {
+ if (value.toLowerCase() != this.hostname.toLowerCase()) {
+ // Reset password so that users are prompted for new password for the new
+ // host.
+ this.forgetPassword();
+ }
+ this._prefs.setStringPref("hostname", value);
+ }
+
+ get port() {
+ return this._prefs.getIntPref("port", 0);
+ }
+
+ set port(value) {
+ if (value) {
+ this._prefs.setIntPref("port", value);
+ } else {
+ this._prefs.clearUserPref("port");
+ }
+ }
+
+ get displayname() {
+ return `${this.hostname}` + (this.port ? `:${this.port}` : "");
+ }
+
+ get username() {
+ return this._prefs.getCharPref("username", "");
+ }
+
+ set username(value) {
+ if (value != this.username) {
+ // Reset password so that users are prompted for new password for the new
+ // username.
+ this.forgetPassword();
+ }
+ this._setCharPref("username", value);
+ }
+
+ get clientid() {
+ return this._getCharPrefWithDefault("clientid");
+ }
+
+ set clientid(value) {
+ this._setCharPref("clientid", value);
+ }
+
+ get clientidEnabled() {
+ try {
+ return this._prefs.getBoolPref("clientidEnabled");
+ } catch (e) {
+ return this._defaultPrefs.getBoolPref("clientidEnabled", false);
+ }
+ }
+
+ set clientidEnabled(value) {
+ this._prefs.setBoolPref("clientidEnabled", value);
+ }
+
+ get authMethod() {
+ return this._getIntPrefWithDefault("authMethod", 3);
+ }
+
+ set authMethod(value) {
+ this._prefs.setIntPref("authMethod", value);
+ }
+
+ get socketType() {
+ return this._getIntPrefWithDefault("try_ssl", 0);
+ }
+
+ set socketType(value) {
+ this._prefs.setIntPref("try_ssl", value);
+ }
+
+ get helloArgument() {
+ return this._getCharPrefWithDefault("hello_argument");
+ }
+
+ get serverURI() {
+ return this._getServerURI(true);
+ }
+
+ /**
+ * If pref max_cached_connection is set to less than 1, allow only one
+ * connection and one message to be sent on that connection. Otherwise, allow
+ * up to max_cached_connection (default to 3) with each connection allowed to
+ * send multiple messages.
+ */
+ get maximumConnectionsNumber() {
+ let maxConnections = this._getIntPrefWithDefault(
+ "max_cached_connections",
+ 3
+ );
+ // Always return a value >= 0.
+ return maxConnections > 0 ? maxConnections : 0;
+ }
+
+ set maximumConnectionsNumber(value) {
+ this._prefs.setIntPref("max_cached_connections", value);
+ }
+
+ get password() {
+ if (this._password) {
+ return this._password;
+ }
+ let incomingAccountKey = this._prefs.getCharPref("incomingAccount", "");
+ let incomingServer;
+ if (incomingAccountKey) {
+ incomingServer =
+ MailServices.accounts.getIncomingServer(incomingAccountKey);
+ } else {
+ let useMatchingHostNameServer = Services.prefs.getBoolPref(
+ "mail.smtp.useMatchingHostNameServer"
+ );
+ let useMatchingDomainServer = Services.prefs.getBoolPref(
+ "mail.smtp.useMatchingDomainServer"
+ );
+ if (useMatchingHostNameServer || useMatchingDomainServer) {
+ if (useMatchingHostNameServer) {
+ // Pass in empty type and port=0, to match imap and pop3.
+ incomingServer = MailServices.accounts.findServer(
+ this.username,
+ this.hostname,
+ "",
+ 0
+ );
+ }
+ if (
+ !incomingServer &&
+ useMatchingDomainServer &&
+ this.hostname.includes(".")
+ ) {
+ let newHostname = this.hostname.slice(0, this.hostname.indexOf("."));
+ for (let server of MailServices.accounts.allServers) {
+ if (server.username == this.username) {
+ let serverHostName = server.hostName;
+ if (
+ serverHostName.includes(".") &&
+ serverHostName.slice(0, serverHostName.indexOf(".")) ==
+ newHostname
+ ) {
+ incomingServer = server;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ return incomingServer?.password || "";
+ }
+
+ set password(password) {
+ this._password = password;
+ }
+
+ getPasswordWithUI(promptMessage, promptTitle) {
+ let authPrompt;
+ try {
+ // This prompt has a checkbox for saving password.
+ authPrompt = Cc["@mozilla.org/messenger/msgAuthPrompt;1"].getService(
+ Ci.nsIAuthPrompt
+ );
+ } catch (e) {
+ // Often happens in tests. This prompt has no checkbox for saving password.
+ authPrompt = Services.ww.getNewAuthPrompter(null);
+ }
+ let password = this._getPasswordWithoutUI();
+ if (password) {
+ this.password = password;
+ return this.password;
+ }
+ let outUsername = {};
+ let outPassword = {};
+ let ok;
+ if (this.username) {
+ ok = authPrompt.promptPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outPassword
+ );
+ } else {
+ ok = authPrompt.promptUsernameAndPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outUsername,
+ outPassword
+ );
+ }
+ if (ok) {
+ if (outUsername.value) {
+ this.username = outUsername.value;
+ }
+ this.password = outPassword.value;
+ } else {
+ throw Components.Exception("Password dialog canceled", Cr.NS_ERROR_ABORT);
+ }
+ return this.password;
+ }
+
+ forgetPassword() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ Services.logins.removeLogin(login);
+ }
+ }
+ this.password = "";
+ }
+
+ verifyLogon(urlListener, msgWindow) {
+ return MailServices.smtp.verifyLogon(this, urlListener, msgWindow);
+ }
+
+ clearAllValues() {
+ for (let prefName of this._prefs.getChildList("")) {
+ this._prefs.clearUserPref(prefName);
+ }
+ }
+
+ /**
+ * @returns {string}
+ */
+ _getPasswordWithoutUI() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ return login.password;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get server URI in the form of smtp://[user@]hostname.
+ *
+ * @param {boolean} includeUsername - Whether to include the username.
+ * @returns {string}
+ */
+ _getServerURI(includeUsername) {
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ let hostname = this.hostname.includes(":")
+ ? `[${this.hostname}]`
+ : this.hostname;
+ return (
+ "smtp://" +
+ (includeUsername && this.username
+ ? `${encodeURIComponent(this.username)}@`
+ : "") +
+ hostname
+ );
+ }
+
+ /**
+ * Get the associated pref branch and the default SMTP server branch.
+ */
+ _loadPrefs() {
+ this._prefs = Services.prefs.getBranch(`mail.smtpserver.${this._key}.`);
+ this._defaultPrefs = Services.prefs.getBranch("mail.smtpserver.default.");
+ }
+
+ /**
+ * Set or clear a string preference.
+ *
+ * @param {string} name - The preference name.
+ * @param {string} value - The preference value.
+ */
+ _setCharPref(name, value) {
+ if (value) {
+ this._prefs.setCharPref(name, value);
+ } else {
+ this._prefs.clearUserPref(name);
+ }
+ }
+
+ /**
+ * Get the value of a char preference from this or default SMTP server.
+ *
+ * @param {string} name - The preference name.
+ * @param {number} [defaultValue=""] - The default value to return.
+ * @returns {string}
+ */
+ _getCharPrefWithDefault(name, defaultValue = "") {
+ try {
+ return this._prefs.getCharPref(name);
+ } catch (e) {
+ return this._defaultPrefs.getCharPref(name, defaultValue);
+ }
+ }
+
+ /**
+ * Get the value of an integer preference from this or default SMTP server.
+ *
+ * @param {string} name - The preference name.
+ * @param {number} defaultValue - The default value to return.
+ * @returns {number}
+ */
+ _getIntPrefWithDefault(name, defaultValue) {
+ try {
+ return this._prefs.getIntPref(name);
+ } catch (e) {
+ return this._defaultPrefs.getIntPref(name, defaultValue);
+ }
+ }
+
+ get wrappedJSObject() {
+ return this;
+ }
+
+ // @type {SmtpClient[]} - An array of connections can be used.
+ _freeConnections = [];
+ // @type {SmtpClient[]} - An array of connections in use.
+ _busyConnections = [];
+ // @type {Function[]} - An array of Promise.resolve functions.
+ _connectionWaitingQueue = [];
+
+ closeCachedConnections() {
+ // Close all connections.
+ for (let client of [...this._freeConnections, ...this._busyConnections]) {
+ client.quit();
+ }
+ // Cancel all waitings in queue.
+ for (let resolve of this._connectionWaitingQueue) {
+ resolve(false);
+ }
+ this._freeConnections = [];
+ this._busyConnections = [];
+ }
+
+ /**
+ * Get an idle connection that can be used.
+ *
+ * @returns {SmtpClient}
+ */
+ async _getNextClient() {
+ // The newest connection is the least likely to have timed out.
+ let client = this._freeConnections.pop();
+ if (client) {
+ this._busyConnections.push(client);
+ return client;
+ }
+ const maxConns = this.maximumConnectionsNumber
+ ? this.maximumConnectionsNumber
+ : 1;
+ if (
+ this._freeConnections.length + this._busyConnections.length <
+ maxConns
+ ) {
+ // Create a new client if the pool is not full.
+ client = new lazy.SmtpClient(this);
+ this._busyConnections.push(client);
+ return client;
+ }
+ // Wait until a connection is available.
+ await new Promise(resolve => this._connectionWaitingQueue.push(resolve));
+ return this._getNextClient();
+ }
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {Function} handler - A callback function to take a SmtpClient
+ * instance, and do some actions.
+ */
+ async withClient(handler) {
+ let client = await this._getNextClient();
+ client.onFree = () => {
+ this._busyConnections = this._busyConnections.filter(c => c != client);
+ // Per RFC, the minimum total number of recipients that MUST be buffered
+ // is 100 recipients.
+ // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.8
+ // So use a new connection for the next message to avoid running into
+ // recipient limits.
+ // If user has set SMTP pref max_cached_connection to less than 1,
+ // use a new connection for each message.
+ if (this.maximumConnectionsNumber == 0 || client.rcptCount > 99) {
+ // Send QUIT, server will then terminate the connection
+ client.quit();
+ } else {
+ // Keep using this connection
+ this._freeConnections.push(client);
+ // Resolve the first waiting in queue.
+ this._connectionWaitingQueue.shift()?.();
+ }
+ };
+ handler(client);
+ client.connect();
+ }
+}
diff --git a/comm/mailnews/compose/src/SmtpService.jsm b/comm/mailnews/compose/src/SmtpService.jsm
new file mode 100644
index 0000000000..d90983a213
--- /dev/null
+++ b/comm/mailnews/compose/src/SmtpService.jsm
@@ -0,0 +1,350 @@
+/* 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 = ["SmtpService"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ SmtpClient: "resource:///modules/SmtpClient.jsm",
+ MsgUtils: "resource:///modules/MimeMessageUtils.jsm",
+});
+
+/**
+ * The SMTP service.
+ *
+ * @implements {nsISmtpService}
+ */
+class SmtpService {
+ QueryInterface = ChromeUtils.generateQI(["nsISmtpService"]);
+
+ constructor() {
+ this._servers = [];
+ this._logger = lazy.MsgUtils.smtpLogger;
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ get defaultServer() {
+ let defaultServerKey = Services.prefs.getCharPref(
+ "mail.smtp.defaultserver",
+ ""
+ );
+ if (defaultServerKey) {
+ // Get it from the prefs.
+ return this.getServerByKey(defaultServerKey);
+ }
+
+ // No pref set, so set the first one as default, and return it.
+ if (this.servers.length > 0) {
+ this.defaultServer = this.servers[0];
+ return this.servers[0];
+ }
+ return null;
+ }
+
+ set defaultServer(server) {
+ Services.prefs.setCharPref("mail.smtp.defaultserver", server.key);
+ }
+
+ get servers() {
+ if (!this._servers.length) {
+ // Load SMTP servers from prefs.
+ this._servers = this._getSmtpServerKeys().map(key =>
+ this._keyToServer(key)
+ );
+ }
+ return this._servers;
+ }
+
+ get wrappedJSObject() {
+ return this;
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ async sendMailMessage(
+ messageFile,
+ recipients,
+ userIdentity,
+ sender,
+ password,
+ deliveryListener,
+ statusListener,
+ notificationCallbacks,
+ requestDSN,
+ messageId,
+ outURI,
+ outRequest
+ ) {
+ this._logger.debug(`Sending message ${messageId}`);
+ let server = this.getServerByIdentity(userIdentity);
+ if (!server) {
+ // Occurs for at least one unit test, but test does not fail if return
+ // here. This check for "server" can be removed if tests are fixed.
+ console.log(
+ `No server found for identity with email ${userIdentity.email} and ` +
+ `smtpServerKey ${userIdentity.smtpServerKey}`
+ );
+ return;
+ }
+ if (password) {
+ server.password = password;
+ }
+ let runningUrl = this._getRunningUri(server);
+ await server.wrappedJSObject.withClient(client => {
+ deliveryListener?.OnStartRunningUrl(runningUrl, 0);
+ let fresh = true;
+ client.onidle = () => {
+ // onidle can occur multiple times, but we should only init sending
+ // when sending a new message(fresh is true) or when a new connection
+ // replaces the original connection due to error 4xx response
+ // (client.isRetry is true).
+ if (!fresh && !client.isRetry) {
+ return;
+ }
+ // Init when fresh==true OR re-init sending when client.isRetry==true.
+ fresh = false;
+ let from = sender;
+ let to = MailServices.headerParser
+ .parseEncodedHeaderW(decodeURIComponent(recipients))
+ .map(rec => rec.email);
+
+ if (
+ !Services.prefs.getBoolPref(
+ "mail.smtp.useSenderForSmtpMailFrom",
+ false
+ )
+ ) {
+ from = userIdentity.email;
+ }
+ if (!messageId) {
+ messageId = Cc["@mozilla.org/messengercompose/computils;1"]
+ .createInstance(Ci.nsIMsgCompUtils)
+ .msgGenerateMessageId(userIdentity, null);
+ }
+ client.useEnvelope({
+ from: MailServices.headerParser.parseEncodedHeaderW(
+ decodeURIComponent(from)
+ )[0].email,
+ to,
+ size: messageFile.fileSize,
+ requestDSN,
+ messageId,
+ });
+ };
+ let socketOnDrain;
+ client.onready = async () => {
+ let fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ // PR_RDONLY
+ fstream.init(messageFile, 0x01, 0, 0);
+
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sstream.init(fstream);
+
+ let sentSize = 0;
+ let totalSize = messageFile.fileSize;
+ let progressListener = statusListener?.QueryInterface(
+ Ci.nsIWebProgressListener
+ );
+
+ while (sstream.available()) {
+ let chunk = sstream.read(65536);
+ let canSendMore = client.send(chunk);
+ if (!canSendMore) {
+ // Socket buffer is full, wait for the ondrain event.
+ await new Promise(resolve => (socketOnDrain = resolve));
+ }
+ // In practice, chunks are buffered by TCPSocket, progress reaches 100%
+ // almost immediately unless message is larger than chunk size.
+ sentSize += chunk.length;
+ progressListener?.onProgressChange(
+ null,
+ null,
+ sentSize,
+ totalSize,
+ sentSize,
+ totalSize
+ );
+ }
+ sstream.close();
+ fstream.close();
+ client.end();
+
+ // Set progress to indeterminate.
+ progressListener?.onProgressChange(null, null, 0, -1, 0, -1);
+ };
+ client.ondrain = () => {
+ // Socket buffer is empty, safe to continue sending.
+ socketOnDrain();
+ };
+ client.ondone = exitCode => {
+ if (!AppConstants.MOZ_SUITE) {
+ Services.telemetry.scalarAdd("tb.mails.sent", 1);
+ }
+ deliveryListener?.OnStopRunningUrl(runningUrl, exitCode);
+ };
+ client.onerror = (nsError, errorMessage, secInfo) => {
+ runningUrl.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ if (secInfo) {
+ // TODO(emilio): Passing the failed security info as part of the URI is
+ // quite a smell, but monkey see monkey do...
+ runningUrl.failedSecInfo = secInfo;
+ }
+ runningUrl.errorMessage = errorMessage;
+ deliveryListener?.OnStopRunningUrl(runningUrl, nsError);
+ };
+
+ outRequest.value = {
+ cancel() {
+ client.close(true);
+ },
+ };
+ });
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ verifyLogon(server, urlListener, msgWindow) {
+ let client = new lazy.SmtpClient(server);
+ client.connect();
+ let runningUrl = this._getRunningUri(server);
+ client.onerror = (nsError, errorMessage, secInfo) => {
+ runningUrl.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ if (secInfo) {
+ runningUrl.failedSecInfo = secInfo;
+ }
+ runningUrl.errorMessage = errorMessage;
+ urlListener.OnStopRunningUrl(runningUrl, nsError);
+ };
+ client.onready = () => {
+ urlListener.OnStopRunningUrl(runningUrl, 0);
+ client.close();
+ };
+ return runningUrl;
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ getServerByIdentity(userIdentity) {
+ return userIdentity.smtpServerKey
+ ? this.getServerByKey(userIdentity.smtpServerKey)
+ : this.defaultServer;
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ getServerByKey(key) {
+ return this.servers.find(s => s.key == key);
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ createServer() {
+ let serverKeys = this._getSmtpServerKeys();
+ let i = 1;
+ let key;
+ do {
+ key = `smtp${i++}`;
+ } while (serverKeys.includes(key));
+
+ serverKeys.push(key);
+ this._saveSmtpServerKeys(serverKeys);
+ this._servers = []; // Reset to force repopulation of this.servers.
+ return this.servers.at(-1);
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ deleteServer(server) {
+ let serverKeys = this._getSmtpServerKeys().filter(k => k != server.key);
+ this._servers = this.servers.filter(s => s.key != server.key);
+ this._saveSmtpServerKeys(serverKeys);
+ }
+
+ /**
+ * @see nsISmtpService
+ */
+ findServer(username, hostname) {
+ username = username?.toLowerCase();
+ hostname = hostname?.toLowerCase();
+ return this.servers.find(server => {
+ if (
+ (username && server.username.toLowerCase() != username) ||
+ (hostname && server.hostname.toLowerCase() != hostname)
+ ) {
+ return false;
+ }
+ return true;
+ });
+ }
+
+ /**
+ * Get all SMTP server keys from prefs.
+ *
+ * @returns {string[]}
+ */
+ _getSmtpServerKeys() {
+ return Services.prefs
+ .getCharPref("mail.smtpservers", "")
+ .split(",")
+ .filter(Boolean);
+ }
+
+ /**
+ * Save SMTP server keys to prefs.
+ *
+ * @param {string[]} keys - The key list to save.
+ */
+ _saveSmtpServerKeys(keys) {
+ return Services.prefs.setCharPref("mail.smtpservers", keys.join(","));
+ }
+
+ /**
+ * Create an nsISmtpServer from a key.
+ *
+ * @param {string} key - The key for the SmtpServer.
+ * @returns {nsISmtpServer}
+ */
+ _keyToServer(key) {
+ let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance(
+ Ci.nsISmtpServer
+ );
+ // Setting the server key will set up all of its other properties by
+ // reading them from the prefs.
+ server.key = key;
+ return server;
+ }
+
+ /**
+ * Get the server URI in the form of smtp://user@hostname:port.
+ *
+ * @param {nsISmtpServer} server - The SMTP server.
+ * @returns {nsIURI}
+ */
+ _getRunningUri(server) {
+ let spec = server.serverURI + (server.port ? `:${server.port}` : "");
+ return Services.io.newURI(spec);
+ }
+}
diff --git a/comm/mailnews/compose/src/components.conf b/comm/mailnews/compose/src/components.conf
new file mode 100644
index 0000000000..44754289f5
--- /dev/null
+++ b/comm/mailnews/compose/src/components.conf
@@ -0,0 +1,197 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{588595fe-1ada-11d3-a715-0060b0eb39b5}",
+ "contract_ids": [
+ "@mozilla.org/messengercompose;1",
+ "@mozilla.org/commandlinehandler/general-startup;1?type=compose",
+ ],
+ "type": "nsMsgComposeService",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/compose/src/nsMsgComposeService.h"],
+ "categories": {"command-line-handler": "m-compose"},
+ "name": "Compose",
+ "interfaces": ["nsIMsgComposeService"],
+ },
+ {
+ "cid": "{0b63fb80-bbba-11d4-9daa-91b657eb313c}",
+ "contract_ids": [
+ "@mozilla.org/uriloader/content-handler;1?type=application/x-mailto"
+ ],
+ "type": "nsMsgComposeContentHandler",
+ "headers": ["/comm/mailnews/compose/src/nsMsgComposeContentHandler.h"],
+ },
+ {
+ "cid": "{eb5bdaf8-bbc6-11d2-a6ec-0060b0eb39b5}",
+ "contract_ids": ["@mozilla.org/messengercompose/compose;1"],
+ "type": "nsMsgCompose",
+ "headers": ["/comm/mailnews/compose/src/nsMsgCompose.h"],
+ },
+ {
+ "cid": "{cb998a00-c079-11d4-9daa-8df64bab2efc}",
+ "contract_ids": ["@mozilla.org/messengercompose/composeparams;1"],
+ "type": "nsMsgComposeParams",
+ "headers": ["/comm/mailnews/compose/src/nsMsgComposeParams.h"],
+ },
+ {
+ "cid": "{acc72781-2cea-11d5-9daa-bacdeac1eefc}",
+ "contract_ids": ["@mozilla.org/messengercompose/composesendlistener;1"],
+ "type": "nsMsgComposeSendListener",
+ "headers": ["/comm/mailnews/compose/src/nsMsgCompose.h"],
+ },
+ {
+ "cid": "{1e0e7c01-3e4c-11d5-9daa-f88d288130fc}",
+ "contract_ids": ["@mozilla.org/messengercompose/composeprogressparameters;1"],
+ "type": "nsMsgComposeProgressParams",
+ "headers": ["/comm/mailnews/compose/src/nsMsgComposeProgressParams.h"],
+ },
+ {
+ "cid": "{e64b0f51-0d7b-4e2f-8c60-3862ee8c174f}",
+ "contract_ids": ["@mozilla.org/messengercompose/composefields;1"],
+ "type": "nsMsgCompFields",
+ "headers": ["/comm/mailnews/compose/src/nsMsgCompFields.h"],
+ },
+ {
+ "cid": "{27b8d045-8d9f-4fa8-bfb6-8a0f8d09ce89}",
+ "contract_ids": ["@mozilla.org/messengercompose/attachment;1"],
+ "type": "nsMsgAttachment",
+ "headers": ["/comm/mailnews/compose/src/nsMsgAttachment.h"],
+ },
+ {
+ "cid": "{9e16958d-d9e9-4cae-b723-a5bccf104998}",
+ "contract_ids": ["@mozilla.org/messengercompose/attachmentdata;1"],
+ "type": "nsMsgAttachmentData",
+ "headers": ["/comm/mailnews/compose/src/nsMsgAttachmentData.h"],
+ },
+ {
+ "cid": "{ef173501-4e14-42b9-ae1f-7770de235c29}",
+ "contract_ids": ["@mozilla.org/messengercompose/attachedfile;1"],
+ "type": "nsMsgAttachedFile",
+ "headers": ["/comm/mailnews/compose/src/nsMsgAttachedFile.h"],
+ },
+ {
+ "cid": "{e15c83f1-1cf4-11d3-8ef0-00a024a7d144}",
+ "contract_ids": ["@mozilla.org/messengercompose/sendlater;1"],
+ "type": "nsMsgSendLater",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/compose/src/nsMsgSendLater.h"],
+ },
+ {
+ "cid": "{be59dbf0-2812-11d3-80a3-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messengercompose/smtpurl;1"],
+ "type": "nsSmtpUrl",
+ "headers": ["/comm/mailnews/compose/src/nsSmtpUrl.h"],
+ },
+ {
+ "cid": "{05bab5e7-9c7d-11d3-98a3-001083010e9b}",
+ "contract_ids": ["@mozilla.org/messengercompose/mailtourl;1"],
+ "type": "nsMailtoUrl",
+ "headers": ["/comm/mailnews/compose/src/nsSmtpUrl.h"],
+ },
+ {
+ "cid": "{1c7abf0c-21e5-11d3-8ef1-00a024a7d144}",
+ "contract_ids": ["@mozilla.org/messengercompose/quoting;1"],
+ "type": "nsMsgQuote",
+ "headers": ["/comm/mailnews/compose/src/nsMsgQuote.h"],
+ },
+ {
+ "cid": "{683728ac-88df-11d3-989d-001083010e9b}",
+ "contract_ids": ["@mozilla.org/messengercompose/quotinglistener;1"],
+ "type": "nsMsgQuoteListener",
+ "headers": ["/comm/mailnews/compose/src/nsMsgQuote.h"],
+ },
+ {
+ "cid": "{ceb0dca2-5e7d-4204-94d4-2ab925921fae}",
+ "contract_ids": ["@mozilla.org/messengercompose/computils;1"],
+ "type": "nsMsgCompUtils",
+ "headers": ["/comm/mailnews/compose/src/nsMsgCompUtils.h"],
+ },
+ {
+ "cid": "{0874c3b5-317d-11d3-8efb-00a024a7d144}",
+ "contract_ids": ["@mozilla.org/messengercompose/msgcopy;1"],
+ "type": "nsMsgCopy",
+ "headers": ["/comm/mailnews/compose/src/nsMsgCopy.h"],
+ },
+ {
+ "cid": "{e5872045-a87b-4ea0-b366-45ebd7dc89d9}",
+ "contract_ids": ["@mozilla.org/messengercompose/sendreport;1"],
+ "type": "nsMsgSendReport",
+ "headers": ["/comm/mailnews/compose/src/nsMsgSendReport.h"],
+ },
+ {
+ "cid": "{028b9c1e-8d0a-4518-80c2-842e07846eaa}",
+ "contract_ids": ["@mozilla.org/messengercompose/send;1"],
+ "jsm": "resource:///modules/MessageSend.jsm",
+ "constructor": "MessageSend",
+ },
+ {
+ "cid": "{b14c2b67-8680-4c11-8d63-9403c7d4f757}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=smtp"],
+ "jsm": "resource:///modules/SMTPProtocolHandler.jsm",
+ "constructor": "SMTPProtocolHandler",
+ "protocol_config": {
+ "scheme": "smtp",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_DANGEROUS_TO_LOAD",
+ "ALLOWS_PROXY",
+ "URI_NON_PERSISTABLE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ ],
+ "default_port": 25,
+ },
+ },
+ {
+ "cid": "{057d0997-9e3a-411e-b4ee-2602f53fe05f}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=smtps"],
+ "jsm": "resource:///modules/SMTPProtocolHandler.jsm",
+ "constructor": "SMTPSProtocolHandler",
+ "protocol_config": {
+ "scheme": "smtps",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_DANGEROUS_TO_LOAD",
+ "ALLOWS_PROXY",
+ "URI_NON_PERSISTABLE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ ],
+ "default_port": 465,
+ },
+ },
+ {
+ "cid": "{af314bd9-0b28-4f69-9bea-592ab4dc6811}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=mailto"],
+ "jsm": "resource:///modules/MailtoProtocolHandler.jsm",
+ "constructor": "MailtoProtocolHandler",
+ "protocol_config": {
+ "scheme": "mailto",
+ "flags": [
+ "URI_NORELATIVE",
+ "ALLOWS_PROXY",
+ "URI_LOADABLE_BY_ANYONE",
+ "URI_NON_PERSISTABLE",
+ "URI_DOES_NOT_RETURN_DATA",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ ],
+ },
+ },
+ {
+ "cid": "{acda6039-8b17-46c1-a8ed-ad50aa80f412}",
+ "contract_ids": ["@mozilla.org/messengercompose/smtp;1"],
+ "jsm": "resource:///modules/SmtpService.jsm",
+ "constructor": "SmtpService",
+ "name": "Smtp",
+ "interfaces": ["nsISmtpService"],
+ },
+ {
+ "cid": "{3a75f5ea-651e-4696-9813-848c03da8bbd}",
+ "contract_ids": ["@mozilla.org/messenger/smtp/server;1"],
+ "jsm": "resource:///modules/SmtpServer.jsm",
+ "constructor": "SmtpServer",
+ },
+]
diff --git a/comm/mailnews/compose/src/moz.build b/comm/mailnews/compose/src/moz.build
new file mode 100644
index 0000000000..60ff148540
--- /dev/null
+++ b/comm/mailnews/compose/src/moz.build
@@ -0,0 +1,60 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "nsComposeStrings.h",
+ "nsMsgAttachmentData.h",
+ "nsMsgCompFields.h",
+ "nsMsgCompose.h",
+]
+
+SOURCES += [
+ "nsComposeStrings.cpp",
+ "nsMsgAttachedFile.cpp",
+ "nsMsgAttachment.cpp",
+ "nsMsgAttachmentData.cpp",
+ "nsMsgCompFields.cpp",
+ "nsMsgCompose.cpp",
+ "nsMsgComposeContentHandler.cpp",
+ "nsMsgComposeParams.cpp",
+ "nsMsgComposeProgressParams.cpp",
+ "nsMsgComposeService.cpp",
+ "nsMsgCompUtils.cpp",
+ "nsMsgCopy.cpp",
+ "nsMsgPrompts.cpp",
+ "nsMsgQuote.cpp",
+ "nsMsgSendLater.cpp",
+ "nsMsgSendReport.cpp",
+ "nsSmtpUrl.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "mail"
+
+# clang-cl rightly complains about switch on nsresult.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-switch"]
+
+EXTRA_JS_MODULES += [
+ "MailtoProtocolHandler.jsm",
+ "MessageSend.jsm",
+ "MimeEncoder.jsm",
+ "MimeMessage.jsm",
+ "MimeMessageUtils.jsm",
+ "MimePart.jsm",
+ "SmtpClient.jsm",
+ "SMTPProtocolHandler.jsm",
+ "SmtpServer.jsm",
+ "SmtpService.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/compose/src/nsComposeStrings.cpp b/comm/mailnews/compose/src/nsComposeStrings.cpp
new file mode 100644
index 0000000000..666cd14e07
--- /dev/null
+++ b/comm/mailnews/compose/src/nsComposeStrings.cpp
@@ -0,0 +1,106 @@
+/* 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/. */
+
+#include "nsComposeStrings.h"
+
+const char* errorStringNameForErrorCode(nsresult aCode) {
+#ifdef __GNUC__
+// Temporary workaround until bug 783526 is fixed.
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wswitch"
+#endif
+ switch (aCode) {
+ case NS_MSG_UNABLE_TO_OPEN_FILE:
+ return "unableToOpenFile";
+ case NS_MSG_UNABLE_TO_OPEN_TMP_FILE:
+ return "unableToOpenTmpFile";
+ case NS_MSG_UNABLE_TO_SAVE_TEMPLATE:
+ return "unableToSaveTemplate";
+ case NS_MSG_UNABLE_TO_SAVE_DRAFT:
+ return "unableToSaveDraft";
+ case NS_MSG_COULDNT_OPEN_FCC_FOLDER:
+ return "couldntOpenFccFolder";
+ case NS_MSG_NO_SENDER:
+ return "noSender";
+ case NS_MSG_NO_RECIPIENTS:
+ return "noRecipients";
+ case NS_MSG_ERROR_WRITING_FILE:
+ return "errorWritingFile";
+ case NS_ERROR_SENDING_FROM_COMMAND:
+ return "errorSendingFromCommand";
+ case NS_ERROR_SENDING_DATA_COMMAND:
+ return "errorSendingDataCommand";
+ case NS_ERROR_SENDING_MESSAGE:
+ return "errorSendingMessage";
+ case NS_ERROR_POST_FAILED:
+ return "postFailed";
+ case NS_ERROR_SMTP_SERVER_ERROR:
+ return "smtpServerError";
+ case NS_MSG_UNABLE_TO_SEND_LATER:
+ return "unableToSendLater";
+ case NS_ERROR_COMMUNICATIONS_ERROR:
+ return "communicationsError";
+ case NS_ERROR_BUT_DONT_SHOW_ALERT:
+ return "dontShowAlert";
+ case NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS:
+ return "couldNotGetUsersMailAddress2";
+ case NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY:
+ return "couldNotGetSendersIdentity";
+ case NS_ERROR_MIME_MPART_ATTACHMENT_ERROR:
+ return "mimeMpartAttachmentError";
+ case NS_ERROR_NNTP_NO_CROSS_POSTING:
+ return "nntpNoCrossPosting";
+ case NS_MSG_ERROR_READING_FILE:
+ return "errorReadingFile";
+ case NS_MSG_ERROR_ATTACHING_FILE:
+ return "errorAttachingFile";
+ case NS_ERROR_SMTP_GREETING:
+ return "incorrectSmtpGreeting";
+ case NS_ERROR_SENDING_RCPT_COMMAND:
+ return "errorSendingRcptCommand";
+ case NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS:
+ return "startTlsFailed";
+ case NS_ERROR_SMTP_PASSWORD_UNDEFINED:
+ return "smtpPasswordUndefined";
+ case NS_ERROR_SMTP_SEND_NOT_ALLOWED:
+ return "smtpSendNotAllowed";
+ case NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED:
+ return "smtpTooManyRecipients";
+ case NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2:
+ return "smtpPermSizeExceeded2";
+ case NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER:
+ return "smtpSendFailedUnknownServer";
+ case NS_ERROR_SMTP_SEND_FAILED_REFUSED:
+ return "smtpSendRequestRefused";
+ case NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED:
+ return "smtpSendInterrupted";
+ case NS_ERROR_SMTP_SEND_FAILED_TIMEOUT:
+ return "smtpSendTimeout";
+ case NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON:
+ return "smtpSendFailedUnknownReason";
+ case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL:
+ return "smtpHintAuthEncryptToPlainNoSsl";
+ case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL:
+ return "smtpHintAuthEncryptToPlainSsl";
+ case NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT:
+ return "smtpHintAuthPlainToEncrypt";
+ case NS_ERROR_SMTP_AUTH_FAILURE:
+ return "smtpAuthFailure";
+ case NS_ERROR_SMTP_AUTH_GSSAPI:
+ return "smtpAuthGssapi";
+ case NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED:
+ return "smtpAuthMechNotSupported";
+ case NS_ERROR_ILLEGAL_LOCALPART:
+ return "errorIllegalLocalPart2";
+ case NS_ERROR_CLIENTID:
+ return "smtpClientid";
+ case NS_ERROR_CLIENTID_PERMISSION:
+ return "smtpClientidPermission";
+ default:
+ return "sendFailed";
+ }
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+}
diff --git a/comm/mailnews/compose/src/nsComposeStrings.h b/comm/mailnews/compose/src/nsComposeStrings.h
new file mode 100644
index 0000000000..3d6a24516f
--- /dev/null
+++ b/comm/mailnews/compose/src/nsComposeStrings.h
@@ -0,0 +1,76 @@
+/* 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/. */
+
+// clang-format off
+/**
+ String Ids used by mailnews\compose
+ To Do: Convert the callers to use names instead of ids and then make this file obsolete.
+ */
+
+#ifndef _nsComposeStrings_H__
+#define _nsComposeStrings_H__
+
+#include "msgCore.h"
+
+#define NS_MSG_UNABLE_TO_OPEN_FILE NS_MSG_GENERATE_FAILURE(12500)
+#define NS_MSG_UNABLE_TO_OPEN_TMP_FILE NS_MSG_GENERATE_FAILURE(12501)
+#define NS_MSG_UNABLE_TO_SAVE_TEMPLATE NS_MSG_GENERATE_FAILURE(12502)
+#define NS_MSG_UNABLE_TO_SAVE_DRAFT NS_MSG_GENERATE_FAILURE(12503)
+#define NS_MSG_COULDNT_OPEN_FCC_FOLDER NS_MSG_GENERATE_FAILURE(12506)
+#define NS_MSG_NO_SENDER NS_MSG_GENERATE_FAILURE(12510)
+#define NS_MSG_NO_RECIPIENTS NS_MSG_GENERATE_FAILURE(12511)
+#define NS_MSG_ERROR_WRITING_FILE NS_MSG_GENERATE_FAILURE(12512)
+#define NS_ERROR_SENDING_FROM_COMMAND NS_MSG_GENERATE_FAILURE(12514)
+#define NS_ERROR_SENDING_DATA_COMMAND NS_MSG_GENERATE_FAILURE(12516)
+#define NS_ERROR_SENDING_MESSAGE NS_MSG_GENERATE_FAILURE(12517)
+#define NS_ERROR_POST_FAILED NS_MSG_GENERATE_FAILURE(12518)
+#define NS_ERROR_SMTP_SERVER_ERROR NS_MSG_GENERATE_FAILURE(12524)
+#define NS_MSG_UNABLE_TO_SEND_LATER NS_MSG_GENERATE_FAILURE(12525)
+#define NS_ERROR_COMMUNICATIONS_ERROR NS_MSG_GENERATE_FAILURE(12526)
+#define NS_ERROR_BUT_DONT_SHOW_ALERT NS_MSG_GENERATE_FAILURE(12527)
+#define NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS NS_MSG_GENERATE_FAILURE(12529)
+#define NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY NS_MSG_GENERATE_FAILURE(12530)
+#define NS_ERROR_MIME_MPART_ATTACHMENT_ERROR NS_MSG_GENERATE_FAILURE(12531)
+
+/* 12554 is taken by NS_ERROR_NNTP_NO_CROSS_POSTING. use 12555 as the next one */
+
+// For message sending report
+#define NS_MSG_ERROR_READING_FILE NS_MSG_GENERATE_FAILURE(12563)
+
+#define NS_MSG_ERROR_ATTACHING_FILE NS_MSG_GENERATE_FAILURE(12570)
+
+#define NS_ERROR_SMTP_GREETING NS_MSG_GENERATE_FAILURE(12572)
+
+#define NS_ERROR_SENDING_RCPT_COMMAND NS_MSG_GENERATE_FAILURE(12575)
+
+#define NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS NS_MSG_GENERATE_FAILURE(12582)
+
+#define NS_ERROR_SMTP_PASSWORD_UNDEFINED NS_MSG_GENERATE_FAILURE(12584)
+#define NS_ERROR_SMTP_SEND_NOT_ALLOWED NS_MSG_GENERATE_FAILURE(12585)
+#define NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED NS_MSG_GENERATE_FAILURE(12586)
+#define NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2 NS_MSG_GENERATE_FAILURE(12588)
+
+#define NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER NS_MSG_GENERATE_FAILURE(12589)
+#define NS_ERROR_SMTP_SEND_FAILED_REFUSED NS_MSG_GENERATE_FAILURE(12590)
+#define NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED NS_MSG_GENERATE_FAILURE(12591)
+#define NS_ERROR_SMTP_SEND_FAILED_TIMEOUT NS_MSG_GENERATE_FAILURE(12592)
+#define NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON NS_MSG_GENERATE_FAILURE(12593)
+
+#define NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL NS_MSG_GENERATE_FAILURE(12594)
+#define NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL NS_MSG_GENERATE_FAILURE(12595)
+#define NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT NS_MSG_GENERATE_FAILURE(12596)
+#define NS_ERROR_SMTP_AUTH_FAILURE NS_MSG_GENERATE_FAILURE(12597)
+#define NS_ERROR_SMTP_AUTH_GSSAPI NS_MSG_GENERATE_FAILURE(12598)
+#define NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED NS_MSG_GENERATE_FAILURE(12599)
+
+#define NS_ERROR_ILLEGAL_LOCALPART NS_MSG_GENERATE_FAILURE(12601)
+
+#define NS_ERROR_CLIENTID NS_MSG_GENERATE_FAILURE(12610)
+#define NS_ERROR_CLIENTID_PERMISSION NS_MSG_GENERATE_FAILURE(12611)
+
+const char* errorStringNameForErrorCode(nsresult aCode);
+
+#endif /* _nsComposeStrings_H__ */
+
+// clang-format on
diff --git a/comm/mailnews/compose/src/nsMsgAttachedFile.cpp b/comm/mailnews/compose/src/nsMsgAttachedFile.cpp
new file mode 100644
index 0000000000..cb293ee964
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachedFile.cpp
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMsgAttachedFile.h"
+
+NS_IMPL_ISUPPORTS(nsMsgAttachedFile, nsIMsgAttachedFile)
+
+nsMsgAttachedFile::nsMsgAttachedFile()
+ : m_size(0),
+ m_unprintableCount(0),
+ m_highbitCount(0),
+ m_ctlCount(0),
+ m_nullCount(0),
+ m_maxLineLength(0) {}
+
+nsMsgAttachedFile::~nsMsgAttachedFile() {}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetOrigUrl(nsIURI** aOrigUrl) {
+ NS_ENSURE_ARG_POINTER(aOrigUrl);
+ NS_IF_ADDREF(*aOrigUrl = m_origUrl);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetOrigUrl(nsIURI* aOrigUrl) {
+ m_origUrl = aOrigUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetTmpFile(nsIFile** aTmpFile) {
+ NS_ENSURE_ARG_POINTER(aTmpFile);
+ NS_IF_ADDREF(*aTmpFile = m_tmpFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetTmpFile(nsIFile* aTmpFile) {
+ m_tmpFile = aTmpFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetType(nsACString& aType) {
+ aType = m_type;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetType(const nsACString& aType) {
+ m_type = aType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetEncoding(nsACString& aEncoding) {
+ aEncoding = m_encoding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetEncoding(const nsACString& aEncoding) {
+ m_encoding = aEncoding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetDescription(nsACString& aDescription) {
+ aDescription = m_description;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetDescription(
+ const nsACString& aDescription) {
+ m_description = aDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetCloudPartInfo(nsACString& aCloudPartInfo) {
+ aCloudPartInfo = m_cloudPartInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetCloudPartInfo(
+ const nsACString& aCloudPartInfo) {
+ m_cloudPartInfo = aCloudPartInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetXMacType(nsACString& aXMacType) {
+ aXMacType = m_xMacType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetXMacType(const nsACString& aXMacType) {
+ m_xMacType = aXMacType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetXMacCreator(nsACString& aXMacCreator) {
+ aXMacCreator = m_xMacCreator;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetXMacCreator(
+ const nsACString& aXMacCreator) {
+ m_xMacCreator = aXMacCreator;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetRealName(nsACString& aRealName) {
+ aRealName = m_realName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetRealName(const nsACString& aRealName) {
+ m_realName = aRealName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetSize(uint32_t* aSize) {
+ NS_ENSURE_ARG_POINTER(aSize);
+ *aSize = m_size;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetSize(uint32_t aSize) {
+ m_size = aSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetUnprintableCount(
+ uint32_t* aUnprintableCount) {
+ NS_ENSURE_ARG_POINTER(aUnprintableCount);
+ *aUnprintableCount = m_unprintableCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetUnprintableCount(
+ uint32_t aUnprintableCount) {
+ m_unprintableCount = aUnprintableCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetHighbitCount(uint32_t* aHighbitCount) {
+ NS_ENSURE_ARG_POINTER(aHighbitCount);
+ *aHighbitCount = m_highbitCount;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgAttachedFile::SetHighbitCount(uint32_t aHighbitCount) {
+ m_highbitCount = aHighbitCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetCtlCount(uint32_t* aCtlCount) {
+ NS_ENSURE_ARG_POINTER(aCtlCount);
+ *aCtlCount = m_ctlCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetCtlCount(uint32_t aCtlCount) {
+ m_ctlCount = aCtlCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetNullCount(uint32_t* aNullCount) {
+ NS_ENSURE_ARG_POINTER(aNullCount);
+ *aNullCount = m_nullCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetNullCount(uint32_t aNullCount) {
+ m_nullCount = aNullCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::GetMaxLineLength(uint32_t* aMaxLineLength) {
+ NS_ENSURE_ARG_POINTER(aMaxLineLength);
+ *aMaxLineLength = m_maxLineLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachedFile::SetMaxLineLength(uint32_t aMaxLineLength) {
+ m_maxLineLength = aMaxLineLength;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgAttachedFile.h b/comm/mailnews/compose/src/nsMsgAttachedFile.h
new file mode 100644
index 0000000000..5f68731927
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachedFile.h
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgAttachedFile_H_
+#define _nsMsgAttachedFile_H_
+
+#include "nsCOMPtr.h"
+#include "nsIMsgSend.h"
+#include "nsMsgAttachmentData.h"
+
+#endif /* _nsMsgAttachedFile_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgAttachment.cpp b/comm/mailnews/compose/src/nsMsgAttachment.cpp
new file mode 100644
index 0000000000..55edd348ca
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachment.cpp
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgAttachment.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsMsgCompUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgAttachment, nsIMsgAttachment)
+
+nsMsgAttachment::nsMsgAttachment() {
+ mTemporary = false;
+ mSendViaCloud = false;
+ mSize = -1;
+}
+
+nsMsgAttachment::~nsMsgAttachment() {
+ MOZ_LOG(Compose, mozilla::LogLevel::Debug, ("~nsMsgAttachment()"));
+}
+
+/* attribute wstring name; */
+NS_IMETHODIMP nsMsgAttachment::GetName(nsAString& aName) {
+ aName = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetName(const nsAString& aName) {
+ mName = aName;
+ return NS_OK;
+}
+
+/* attribute string url; */
+NS_IMETHODIMP nsMsgAttachment::GetUrl(nsACString& aUrl) {
+ aUrl = mUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetUrl(const nsACString& aUrl) {
+ mUrl = aUrl;
+ return NS_OK;
+}
+
+/* attribute string msgUri; */
+NS_IMETHODIMP nsMsgAttachment::GetMsgUri(nsACString& aMsgUri) {
+ aMsgUri = mMsgUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetMsgUri(const nsACString& aMsgUri) {
+ mMsgUri = aMsgUri;
+ return NS_OK;
+}
+
+/* attribute string urlCharset; */
+NS_IMETHODIMP nsMsgAttachment::GetUrlCharset(nsACString& aUrlCharset) {
+ aUrlCharset = mUrlCharset;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgAttachment::SetUrlCharset(const nsACString& aUrlCharset) {
+ mUrlCharset = aUrlCharset;
+ return NS_OK;
+}
+
+/* attribute boolean temporary; */
+NS_IMETHODIMP nsMsgAttachment::GetTemporary(bool* aTemporary) {
+ NS_ENSURE_ARG_POINTER(aTemporary);
+
+ *aTemporary = mTemporary;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgAttachment::SetTemporary(bool aTemporary) {
+ mTemporary = aTemporary;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetSendViaCloud(bool* aSendViaCloud) {
+ NS_ENSURE_ARG_POINTER(aSendViaCloud);
+
+ *aSendViaCloud = mSendViaCloud;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgAttachment::SetSendViaCloud(bool aSendViaCloud) {
+ mSendViaCloud = aSendViaCloud;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetHtmlAnnotation(
+ const nsACString& aAnnotation) {
+ mHtmlAnnotation = aAnnotation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetHtmlAnnotation(nsACString& aAnnotation) {
+ aAnnotation = mHtmlAnnotation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAttachment::SetCloudFileAccountKey(
+ const nsACString& aCloudFileAccountKey) {
+ mCloudFileAccountKey = aCloudFileAccountKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAttachment::GetCloudFileAccountKey(nsACString& aCloudFileAccountKey) {
+ aCloudFileAccountKey = mCloudFileAccountKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetCloudPartHeaderData(
+ nsACString& aCloudPartHeaderData) {
+ aCloudPartHeaderData = mCloudPartHeaderData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetCloudPartHeaderData(
+ const nsACString& aCloudPartHeaderData) {
+ mCloudPartHeaderData = aCloudPartHeaderData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetContentLocation(
+ nsACString& aContentLocation) {
+ aContentLocation = mContentLocation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetContentLocation(
+ const nsACString& aContentLocation) {
+ mContentLocation = aContentLocation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetContentType(char** aContentType) {
+ NS_ENSURE_ARG_POINTER(aContentType);
+
+ *aContentType = ToNewCString(mContentType);
+ return (*aContentType ? NS_OK : NS_ERROR_OUT_OF_MEMORY);
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetContentType(const char* aContentType) {
+ mContentType = aContentType;
+ // a full content type could also contains parameters but we need to
+ // keep only the content type alone. Therefore we need to cleanup it.
+ int32_t offset = mContentType.FindChar(';');
+ if (offset >= 0) mContentType.SetLength(offset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetContentTypeParam(char** aContentTypeParam) {
+ NS_ENSURE_ARG_POINTER(aContentTypeParam);
+
+ *aContentTypeParam = ToNewCString(mContentTypeParam);
+ return (*aContentTypeParam ? NS_OK : NS_ERROR_OUT_OF_MEMORY);
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetContentTypeParam(
+ const char* aContentTypeParam) {
+ if (aContentTypeParam)
+ while (*aContentTypeParam == ';' || *aContentTypeParam == ' ')
+ aContentTypeParam++;
+ mContentTypeParam = aContentTypeParam;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::GetContentId(nsACString& aContentId) {
+ aContentId = mContentId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachment::SetContentId(const nsACString& aContentId) {
+ mContentId = aContentId;
+ return NS_OK;
+}
+
+/* attribute string charset; */
+NS_IMETHODIMP nsMsgAttachment::GetCharset(char** aCharset) {
+ NS_ENSURE_ARG_POINTER(aCharset);
+
+ *aCharset = ToNewCString(mCharset);
+ return (*aCharset ? NS_OK : NS_ERROR_OUT_OF_MEMORY);
+}
+NS_IMETHODIMP nsMsgAttachment::SetCharset(const char* aCharset) {
+ mCharset = aCharset;
+ return NS_OK;
+}
+
+/* attribute string macType; */
+NS_IMETHODIMP nsMsgAttachment::GetMacType(char** aMacType) {
+ NS_ENSURE_ARG_POINTER(aMacType);
+
+ *aMacType = ToNewCString(mMacType);
+ return (*aMacType ? NS_OK : NS_ERROR_OUT_OF_MEMORY);
+}
+NS_IMETHODIMP nsMsgAttachment::SetMacType(const char* aMacType) {
+ mMacType = aMacType;
+ return NS_OK;
+}
+
+/* attribute string macCreator; */
+NS_IMETHODIMP nsMsgAttachment::GetMacCreator(char** aMacCreator) {
+ NS_ENSURE_ARG_POINTER(aMacCreator);
+
+ *aMacCreator = ToNewCString(mMacCreator);
+ return (*aMacCreator ? NS_OK : NS_ERROR_OUT_OF_MEMORY);
+}
+NS_IMETHODIMP nsMsgAttachment::SetMacCreator(const char* aMacCreator) {
+ mMacCreator = aMacCreator;
+ return NS_OK;
+}
+
+/* attribute int64_t size; */
+NS_IMETHODIMP nsMsgAttachment::GetSize(int64_t* aSize) {
+ NS_ENSURE_ARG_POINTER(aSize);
+
+ *aSize = mSize;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgAttachment::SetSize(int64_t aSize) {
+ mSize = aSize;
+ return NS_OK;
+}
+
+/* boolean equalsUrl (in nsIMsgAttachment attachment); */
+NS_IMETHODIMP nsMsgAttachment::EqualsUrl(nsIMsgAttachment* attachment,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(attachment);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsAutoCString url;
+ attachment->GetUrl(url);
+
+ *_retval = mUrl.Equals(url);
+ return NS_OK;
+}
+
+nsresult nsMsgAttachment::DeleteAttachment() {
+ nsresult rv;
+ bool isAFile = false;
+
+ nsCOMPtr<nsIFile> urlFile;
+ rv = NS_GetFileFromURLSpec(mUrl, getter_AddRefs(urlFile));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Can't nsIFile from URL string");
+ if (NS_SUCCEEDED(rv)) {
+ bool bExists = false;
+ rv = urlFile->Exists(&bExists);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Exists() call failed!");
+ if (NS_SUCCEEDED(rv) && bExists) {
+ rv = urlFile->IsFile(&isAFile);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "IsFile() call failed!");
+ }
+ }
+
+ // remove it if it's a valid file
+ if (isAFile) rv = urlFile->Remove(false);
+
+ return rv;
+}
diff --git a/comm/mailnews/compose/src/nsMsgAttachment.h b/comm/mailnews/compose/src/nsMsgAttachment.h
new file mode 100644
index 0000000000..d9e0de696e
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachment.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgAttachment_H_
+#define _nsMsgAttachment_H_
+
+#include "nsIMsgAttachment.h"
+#include "nsString.h"
+
+class nsMsgAttachment : public nsIMsgAttachment {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGATTACHMENT
+
+ nsMsgAttachment();
+
+ private:
+ virtual ~nsMsgAttachment();
+ nsresult DeleteAttachment();
+
+ nsString mName;
+ nsCString mUrl;
+ nsCString mMsgUri;
+ nsCString mUrlCharset;
+ bool mTemporary;
+ bool mSendViaCloud;
+ nsCString mCloudFileAccountKey;
+ nsCString mCloudPartHeaderData;
+ nsCString mContentLocation;
+ nsCString mContentType;
+ nsCString mContentTypeParam;
+ nsCString mContentId;
+ nsCString mCharset;
+ nsCString mMacType;
+ nsCString mMacCreator;
+ nsCString mHtmlAnnotation;
+ int64_t mSize;
+};
+
+#endif /* _nsMsgAttachment_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgAttachmentData.cpp b/comm/mailnews/compose/src/nsMsgAttachmentData.cpp
new file mode 100644
index 0000000000..c32b31d0c1
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachmentData.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMsgAttachmentData.h"
+
+NS_IMPL_ISUPPORTS(nsMsgAttachmentData, nsIMsgAttachmentData)
+
+nsMsgAttachmentData::nsMsgAttachmentData()
+ : m_size(0),
+ m_sizeExternalStr("-1"),
+ m_isExternalAttachment(false),
+ m_isExternalLinkAttachment(false),
+ m_isDownloaded(false),
+ m_hasFilename(false),
+ m_displayableInline(false) {}
+
+nsMsgAttachmentData::~nsMsgAttachmentData() {}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetUrl(nsIURI** aUrl) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ NS_IF_ADDREF(*aUrl = m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetUrl(nsIURI* aUrl) {
+ m_url = aUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetDesiredType(nsACString& aDesiredType) {
+ aDesiredType = m_desiredType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetDesiredType(
+ const nsACString& aDesiredType) {
+ m_desiredType = aDesiredType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetRealType(nsACString& aRealType) {
+ aRealType = m_realType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetRealType(const nsACString& aRealType) {
+ m_realType = aRealType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetRealEncoding(nsACString& aRealEncoding) {
+ aRealEncoding = m_realEncoding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetRealEncoding(
+ const nsACString& aRealEncoding) {
+ m_realEncoding = aRealEncoding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetRealName(nsACString& aRealName) {
+ aRealName = m_realName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetRealName(const nsACString& aRealName) {
+ m_realName = aRealName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetDescription(nsACString& aDescription) {
+ aDescription = m_description;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetDescription(
+ const nsACString& aDescription) {
+ m_description = aDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetXMacType(nsACString& aXMacType) {
+ aXMacType = m_xMacType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetXMacType(const nsACString& aXMacType) {
+ m_xMacType = aXMacType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::GetXMacCreator(nsACString& aXMacCreator) {
+ aXMacCreator = m_xMacCreator;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAttachmentData::SetXMacCreator(
+ const nsACString& aXMacCreator) {
+ m_xMacCreator = aXMacCreator;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgAttachmentData.h b/comm/mailnews/compose/src/nsMsgAttachmentData.h
new file mode 100644
index 0000000000..763a4377e9
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgAttachmentData.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __MSGATTACHMENTDATA_H__
+#define __MSGATTACHMENTDATA_H__
+
+#include "nsIURL.h"
+#include "nsString.h"
+#include "nsIMsgSend.h"
+
+// Attachment file/URL structures - we're letting libmime use this directly
+class nsMsgAttachmentData final : public nsIMsgAttachmentData {
+ public:
+ NS_DECL_NSIMSGATTACHMENTDATA
+ NS_DECL_ISUPPORTS
+
+ nsMsgAttachmentData();
+ virtual ~nsMsgAttachmentData();
+
+ nsCOMPtr<nsIURI> m_url; // The URL to attach.
+
+ nsCString m_desiredType; // The type to which this document should be
+ // converted. Legal values are NULL, TEXT_PLAIN
+ // and APPLICATION_POSTSCRIPT (which are macros
+ // defined in net.h); other values are ignored.
+
+ nsCString
+ m_realType; // The type of the URL if known, otherwise NULL. For example,
+ // if you were attaching a temp file which was known to
+ // contain HTML data, you would pass in TEXT_HTML as the
+ // real_type, to override whatever type the name of the tmp
+ // file might otherwise indicate.
+
+ nsCString m_realEncoding; // Goes along with real_type
+
+ nsCString
+ m_realName; // The original name of this document, which will eventually
+ // show up in the Content-Disposition header. For example, if
+ // you had copied a document to a tmp file, this would be the
+ // original, human-readable name of the document.
+
+ nsCString m_description; // If you put a string here, it will show up as the
+ // Content-Description header. This can be any
+ // explanatory text; it's not a file name.
+
+ nsCString m_disposition; // The Content-Disposition header (if any). a
+ // nsMsgAttachmentData can very well have
+ // Content-Disposition: inline value, instead of
+ // "attachment".
+ nsCString m_cloudPartInfo; // For X-Mozilla-Cloud-Part header, if any
+
+ // Mac-specific data that should show up as optional parameters
+ // to the content-type header.
+ nsCString m_xMacType;
+ nsCString m_xMacCreator;
+
+ int32_t m_size; // The size of the attachment. May be 0.
+ nsCString
+ m_sizeExternalStr; // The reported size of an external attachment.
+ // Originally set at "-1" to mean an unknown value.
+ bool m_isExternalAttachment; // Flag for determining if the attachment is
+ // external
+ bool m_isExternalLinkAttachment; // Flag for determining if the attachment is
+ // external and an http link.
+ bool m_isDownloaded; // Flag for determining if the attachment has already
+ // been downloaded
+ bool m_hasFilename; // Tells whether the name is provided by us or if it's a
+ // Part 1.2-like attachment
+ bool m_displayableInline; // Tells whether the attachment could be displayed
+ // inline
+};
+
+class nsMsgAttachedFile final : public nsIMsgAttachedFile {
+ public:
+ NS_DECL_NSIMSGATTACHEDFILE
+ NS_DECL_ISUPPORTS
+
+ nsMsgAttachedFile();
+ virtual ~nsMsgAttachedFile();
+
+ nsCOMPtr<nsIURI> m_origUrl; // Where it came from on the network (or even
+ // elsewhere on the local disk.)
+
+ nsCOMPtr<nsIFile> m_tmpFile; // The tmp file in which the (possibly
+ // converted) data now resides.
+
+ nsCString m_type; // The type of the data in file_name (not necessarily the
+ // same as the type of orig_url.)
+
+ nsCString
+ m_encoding; // Likewise, the encoding of the tmp file. This will be set
+ // only if the original document had an encoding already; we
+ // don't do base64 encoding and so forth until it's time to
+ // assemble a full MIME message of all parts.
+
+ nsCString m_description; // For Content-Description header
+ nsCString m_cloudPartInfo; // For X-Mozilla-Cloud-Part header, if any
+ nsCString m_xMacType; // mac-specific info
+ nsCString m_xMacCreator; // mac-specific info
+ nsCString m_realName; // The real name of the file.
+
+ // Some statistics about the data that was written to the file, so that when
+ // it comes time to compose a MIME message, we can make an informed decision
+ // about what Content-Transfer-Encoding would be best for this attachment.
+ // (If it's encoded already, we ignore this information and ship it as-is.)
+ uint32_t m_size;
+ uint32_t m_unprintableCount;
+ uint32_t m_highbitCount;
+ uint32_t m_ctlCount;
+ uint32_t m_nullCount;
+ uint32_t m_maxLineLength;
+};
+
+#undef MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING
+#ifdef MOZ_IS_DESTRUCTIBLE
+# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) \
+ static_assert( \
+ !MOZ_IS_DESTRUCTIBLE(X) || \
+ mozilla::IsSame<X, nsMsgAttachmentData>::value || \
+ mozilla::IsSame<X, nsMsgAttachedFile>::value, \
+ "Reference-counted class " #X \
+ " should not have a public destructor. " \
+ "Try to make this class's destructor non-public. If that is really " \
+ "not possible, you can whitelist this class by providing a " \
+ "HasDangerousPublicDestructor specialization for it.");
+#else
+# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X)
+#endif
+#endif
diff --git a/comm/mailnews/compose/src/nsMsgCompFields.cpp b/comm/mailnews/compose/src/nsMsgCompFields.cpp
new file mode 100644
index 0000000000..9bfb66a253
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompFields.cpp
@@ -0,0 +1,559 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgCompose.h"
+#include "nsMsgCompFields.h"
+#include "nsMsgI18N.h"
+#include "nsMsgCompUtils.h"
+#include "nsMsgUtils.h"
+#include "prmem.h"
+#include "nsIFileChannel.h"
+#include "nsIMsgAttachment.h"
+#include "nsIMsgMdnGenerator.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+
+using namespace mozilla::mailnews;
+
+struct HeaderInfo {
+ /// Header name
+ const char* mName;
+ /// If true, nsMsgCompFields should reflect the raw header value instead of
+ /// the unstructured header value.
+ bool mStructured;
+};
+
+// This is a mapping of the m_headers local set to the actual header name we
+// store on the structured header object.
+static HeaderInfo kHeaders[] = {
+ {"From", true},
+ {"Reply-To", true},
+ {"To", true},
+ {"Cc", true},
+ {"Bcc", true},
+ {nullptr, false}, // FCC
+ {nullptr, false}, // FCC2
+ {"Newsgroups", true},
+ {"Followup-To", true},
+ {"Subject", false},
+ {"Organization", false},
+ {"References", true},
+ {"X-Mozilla-News-Host", false},
+ {"X-Priority", false},
+ {nullptr, false}, // CHARACTER_SET
+ {"Message-Id", true},
+ {"X-Template", true},
+ {nullptr, false}, // DRAFT_ID
+ {nullptr, false}, // TEMPLATE_ID
+ {"Content-Language", true},
+ {nullptr, false} // CREATOR IDENTITY KEY
+};
+
+static_assert(
+ MOZ_ARRAY_LENGTH(kHeaders) == nsMsgCompFields::MSG_MAX_HEADERS,
+ "These two arrays need to be kept in sync or bad things will happen!");
+
+NS_IMPL_ISUPPORTS(nsMsgCompFields, nsIMsgCompFields, msgIStructuredHeaders,
+ msgIWritableStructuredHeaders)
+
+nsMsgCompFields::nsMsgCompFields()
+ : mStructuredHeaders(do_CreateInstance(NS_ISTRUCTUREDHEADERS_CONTRACTID)) {
+ m_body.Truncate();
+
+ m_attachVCard = false;
+ m_forcePlainText = false;
+ m_useMultipartAlternative = false;
+ m_returnReceipt = false;
+ m_receiptHeaderType = nsIMsgMdnGenerator::eDntType;
+ m_DSN = false;
+ m_bodyIsAsciiOnly = false;
+ m_forceMsgEncoding = false;
+ m_needToCheckCharset = true;
+ m_attachmentReminder = false;
+ m_deliveryFormat = nsIMsgCompSendFormat::Unset;
+}
+
+nsMsgCompFields::~nsMsgCompFields() {
+ MOZ_LOG(Compose, mozilla::LogLevel::Debug, ("~nsMsgCompFields()"));
+}
+
+nsresult nsMsgCompFields::SetAsciiHeader(MsgHeaderID header,
+ const char* value) {
+ NS_ASSERTION(header >= 0 && header < MSG_MAX_HEADERS,
+ "Invalid message header index!");
+
+ // If we are storing this on the structured header object, we need to set the
+ // value on that object as well. Note that the value may be null, which we'll
+ // take as an attempt to delete the header.
+ const char* headerName = kHeaders[header].mName;
+ if (headerName) {
+ if (!value || !*value) return mStructuredHeaders->DeleteHeader(headerName);
+
+ return mStructuredHeaders->SetRawHeader(headerName,
+ nsDependentCString(value));
+ }
+
+ // Not on the structurd header object, so save it locally.
+ m_headers[header] = value;
+
+ return NS_OK;
+}
+
+const char* nsMsgCompFields::GetAsciiHeader(MsgHeaderID header) {
+ NS_ASSERTION(header >= 0 && header < MSG_MAX_HEADERS,
+ "Invalid message header index!");
+
+ const char* headerName = kHeaders[header].mName;
+ if (headerName) {
+ // We may be out of sync with the structured header object. Retrieve the
+ // header value.
+ if (kHeaders[header].mStructured) {
+ mStructuredHeaders->GetRawHeader(headerName, m_headers[header]);
+ } else {
+ nsString value;
+ mStructuredHeaders->GetUnstructuredHeader(headerName, value);
+ CopyUTF16toUTF8(value, m_headers[header]);
+ }
+ }
+
+ return m_headers[header].get();
+}
+
+nsresult nsMsgCompFields::SetUnicodeHeader(MsgHeaderID header,
+ const nsAString& value) {
+ return SetAsciiHeader(header, NS_ConvertUTF16toUTF8(value).get());
+}
+
+nsresult nsMsgCompFields::GetUnicodeHeader(MsgHeaderID header,
+ nsAString& aResult) {
+ CopyUTF8toUTF16(nsDependentCString(GetAsciiHeader(header)), aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetFrom(const nsAString& value) {
+ return SetUnicodeHeader(MSG_FROM_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetFrom(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_FROM_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetReplyTo(const nsAString& value) {
+ return SetUnicodeHeader(MSG_REPLY_TO_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetReplyTo(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_REPLY_TO_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetTo(const nsAString& value) {
+ return SetUnicodeHeader(MSG_TO_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetTo(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_TO_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetCc(const nsAString& value) {
+ return SetUnicodeHeader(MSG_CC_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetCc(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_CC_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetBcc(const nsAString& value) {
+ return SetUnicodeHeader(MSG_BCC_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetBcc(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_BCC_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetFcc(const nsAString& value) {
+ return SetUnicodeHeader(MSG_FCC_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetFcc(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_FCC_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetFcc2(const nsAString& value) {
+ return SetUnicodeHeader(MSG_FCC2_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetFcc2(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_FCC2_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetNewsgroups(const nsAString& aValue) {
+ return SetUnicodeHeader(MSG_NEWSGROUPS_HEADER_ID, aValue);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetNewsgroups(nsAString& aGroup) {
+ return GetUnicodeHeader(MSG_NEWSGROUPS_HEADER_ID, aGroup);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetFollowupTo(const nsAString& aValue) {
+ return SetUnicodeHeader(MSG_FOLLOWUP_TO_HEADER_ID, aValue);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetFollowupTo(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_FOLLOWUP_TO_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetHasRecipients(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = NS_SUCCEEDED(mime_sanity_check_fields_recipients(
+ GetTo(), GetCc(), GetBcc(), GetNewsgroups()));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetCreatorIdentityKey(const char* value) {
+ return SetAsciiHeader(MSG_CREATOR_IDENTITY_KEY_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetCreatorIdentityKey(char** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = strdup(GetAsciiHeader(MSG_CREATOR_IDENTITY_KEY_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetSubject(const nsAString& value) {
+ return SetUnicodeHeader(MSG_SUBJECT_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetSubject(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_SUBJECT_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetOrganization(const nsAString& value) {
+ return SetUnicodeHeader(MSG_ORGANIZATION_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetOrganization(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_ORGANIZATION_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetReferences(const char* value) {
+ return SetAsciiHeader(MSG_REFERENCES_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetReferences(char** _retval) {
+ *_retval = strdup(GetAsciiHeader(MSG_REFERENCES_HEADER_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetNewspostUrl(const char* value) {
+ return SetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetNewspostUrl(char** _retval) {
+ *_retval = strdup(GetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetPriority(const char* value) {
+ return SetAsciiHeader(MSG_PRIORITY_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetPriority(char** _retval) {
+ *_retval = strdup(GetAsciiHeader(MSG_PRIORITY_HEADER_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetMessageId(const char* value) {
+ return SetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetMessageId(char** _retval) {
+ *_retval = strdup(GetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetTemplateName(const nsAString& value) {
+ return SetUnicodeHeader(MSG_X_TEMPLATE_HEADER_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetTemplateName(nsAString& _retval) {
+ return GetUnicodeHeader(MSG_X_TEMPLATE_HEADER_ID, _retval);
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetDraftId(const nsACString& value) {
+ return SetAsciiHeader(MSG_DRAFT_ID_HEADER_ID,
+ PromiseFlatCString(value).get());
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetDraftId(nsACString& _retval) {
+ _retval.Assign(GetAsciiHeader(MSG_DRAFT_ID_HEADER_ID));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetTemplateId(const nsACString& value) {
+ return SetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID,
+ PromiseFlatCString(value).get());
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetTemplateId(nsACString& _retval) {
+ _retval.Assign(GetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetReturnReceipt(bool value) {
+ m_returnReceipt = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetReturnReceipt(bool* _retval) {
+ *_retval = m_returnReceipt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetReceiptHeaderType(int32_t value) {
+ m_receiptHeaderType = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetReceiptHeaderType(int32_t* _retval) {
+ *_retval = m_receiptHeaderType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetDSN(bool value) {
+ m_DSN = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetDSN(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_DSN;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetAttachVCard(bool value) {
+ m_attachVCard = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetAttachVCard(bool* _retval) {
+ *_retval = m_attachVCard;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetAttachmentReminder(bool* _retval) {
+ *_retval = m_attachmentReminder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetAttachmentReminder(bool value) {
+ m_attachmentReminder = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetDeliveryFormat(int32_t value) {
+ switch (value) {
+ case nsIMsgCompSendFormat::Auto:
+ case nsIMsgCompSendFormat::PlainText:
+ case nsIMsgCompSendFormat::HTML:
+ case nsIMsgCompSendFormat::Both:
+ m_deliveryFormat = value;
+ break;
+ case nsIMsgCompSendFormat::Unset:
+ default:
+ m_deliveryFormat = nsIMsgCompSendFormat::Unset;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetDeliveryFormat(int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_deliveryFormat;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetContentLanguage(const char* value) {
+ return SetAsciiHeader(MSG_CONTENT_LANGUAGE_ID, value);
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetContentLanguage(char** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = strdup(GetAsciiHeader(MSG_CONTENT_LANGUAGE_ID));
+ return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetForcePlainText(bool value) {
+ m_forcePlainText = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetForcePlainText(bool* _retval) {
+ *_retval = m_forcePlainText;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetForceMsgEncoding(bool value) {
+ m_forceMsgEncoding = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetForceMsgEncoding(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_forceMsgEncoding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetUseMultipartAlternative(bool value) {
+ m_useMultipartAlternative = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetUseMultipartAlternative(bool* _retval) {
+ *_retval = m_useMultipartAlternative;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetBodyIsAsciiOnly(bool value) {
+ m_bodyIsAsciiOnly = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetBodyIsAsciiOnly(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = m_bodyIsAsciiOnly;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetBody(const nsAString& value) {
+ m_body = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetBody(nsAString& _retval) {
+ _retval = m_body;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetAttachments(
+ nsTArray<RefPtr<nsIMsgAttachment>>& attachments) {
+ attachments = m_attachments.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::AddAttachment(nsIMsgAttachment* attachment) {
+ // Don't add the same attachment twice.
+ for (nsIMsgAttachment* a : m_attachments) {
+ bool sameUrl;
+ a->EqualsUrl(attachment, &sameUrl);
+ if (sameUrl) return NS_OK;
+ }
+ m_attachments.AppendElement(attachment);
+ return NS_OK;
+}
+
+/* void removeAttachment (in nsIMsgAttachment attachment); */
+NS_IMETHODIMP nsMsgCompFields::RemoveAttachment(nsIMsgAttachment* attachment) {
+ for (uint32_t i = 0; i < m_attachments.Length(); i++) {
+ bool sameUrl;
+ m_attachments[i]->EqualsUrl(attachment, &sameUrl);
+ if (sameUrl) {
+ m_attachments.RemoveElementAt(i);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetOtherHeaders(
+ const nsTArray<nsString>& headers) {
+ m_otherHeaders = headers.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetOtherHeaders(nsTArray<nsString>& headers) {
+ headers = m_otherHeaders.Clone();
+ return NS_OK;
+}
+
+/* void removeAttachments (); */
+NS_IMETHODIMP nsMsgCompFields::RemoveAttachments() {
+ m_attachments.Clear();
+ return NS_OK;
+}
+
+// This method is called during the creation of a new window.
+NS_IMETHODIMP
+nsMsgCompFields::SplitRecipients(const nsAString& aRecipients,
+ bool aEmailAddressOnly,
+ nsTArray<nsString>& aResult) {
+ nsCOMArray<msgIAddressObject> header(EncodedHeaderW(aRecipients));
+ if (aEmailAddressOnly)
+ ExtractEmails(header, aResult);
+ else
+ ExtractDisplayAddresses(header, aResult);
+
+ return NS_OK;
+}
+
+// This method is called during the sending of message from
+// nsMsgCompose::CheckAndPopulateRecipients()
+nsresult nsMsgCompFields::SplitRecipientsEx(const nsAString& recipients,
+ nsTArray<nsMsgRecipient>& aResult) {
+ nsTArray<nsString> names, addresses;
+ ExtractAllAddresses(EncodedHeaderW(recipients), names, addresses);
+
+ uint32_t numAddresses = names.Length();
+ for (uint32_t i = 0; i < numAddresses; ++i) {
+ nsMsgRecipient msgRecipient;
+ msgRecipient.mEmail = addresses[i];
+ msgRecipient.mName = names[i];
+ aResult.AppendElement(msgRecipient);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::ConvertBodyToPlainText() {
+ nsresult rv = NS_OK;
+
+ if (!m_body.IsEmpty()) {
+ if (NS_SUCCEEDED(rv)) {
+ bool flowed, formatted;
+ GetSerialiserFlags(&flowed, &formatted);
+ rv = ConvertBufToPlainText(m_body, flowed, formatted, true);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetComposeSecure(
+ nsIMsgComposeSecure** aComposeSecure) {
+ NS_ENSURE_ARG_POINTER(aComposeSecure);
+ NS_IF_ADDREF(*aComposeSecure = mSecureCompFields);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetComposeSecure(
+ nsIMsgComposeSecure* aComposeSecure) {
+ mSecureCompFields = aComposeSecure;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::GetNeedToCheckCharset(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_needToCheckCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompFields::SetNeedToCheckCharset(bool aCheck) {
+ m_needToCheckCharset = aCheck;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgCompFields.h b/comm/mailnews/compose/src/nsMsgCompFields.h
new file mode 100644
index 0000000000..312d19192c
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompFields.h
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MsgCompFields_H_
+#define _MsgCompFields_H_
+
+#include "nsIMsgCompFields.h"
+#include "msgCore.h"
+#include "nsIAbCard.h"
+#include "nsIAbDirectory.h"
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIMsgComposeSecure.h"
+
+struct nsMsgRecipient {
+ nsString mName;
+ nsString mEmail;
+ nsCOMPtr<nsIAbCard> mCard;
+ nsCOMPtr<nsIAbDirectory> mDirectory;
+};
+
+/* Note that all the "Get" methods never return NULL (except in case of serious
+ error, like an illegal parameter); rather, they return "" if things were set
+ to NULL. This makes it real handy for the callers. */
+
+class nsMsgCompFields : public nsIMsgCompFields {
+ public:
+ nsMsgCompFields();
+
+ /* this macro defines QueryInterface, AddRef and Release for this class */
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_FORWARD_MSGISTRUCTUREDHEADERS(mStructuredHeaders->)
+ NS_FORWARD_MSGIWRITABLESTRUCTUREDHEADERS(mStructuredHeaders->)
+ NS_DECL_NSIMSGCOMPFIELDS
+
+ typedef enum MsgHeaderID {
+ MSG_FROM_HEADER_ID = 0,
+ MSG_REPLY_TO_HEADER_ID,
+ MSG_TO_HEADER_ID,
+ MSG_CC_HEADER_ID,
+ MSG_BCC_HEADER_ID,
+ MSG_FCC_HEADER_ID,
+ MSG_FCC2_HEADER_ID,
+ MSG_NEWSGROUPS_HEADER_ID,
+ MSG_FOLLOWUP_TO_HEADER_ID,
+ MSG_SUBJECT_HEADER_ID,
+ MSG_ORGANIZATION_HEADER_ID,
+ MSG_REFERENCES_HEADER_ID,
+ MSG_NEWSPOSTURL_HEADER_ID,
+ MSG_PRIORITY_HEADER_ID,
+ MSG_CHARACTER_SET_HEADER_ID,
+ MSG_MESSAGE_ID_HEADER_ID,
+ MSG_X_TEMPLATE_HEADER_ID,
+ MSG_DRAFT_ID_HEADER_ID,
+ MSG_TEMPLATE_ID_HEADER_ID,
+ MSG_CONTENT_LANGUAGE_ID,
+ MSG_CREATOR_IDENTITY_KEY_ID,
+
+ MSG_MAX_HEADERS // Must be the last one.
+ } MsgHeaderID;
+
+ nsresult SetAsciiHeader(MsgHeaderID header, const char* value);
+ const char* GetAsciiHeader(
+ MsgHeaderID header); // just return the address of the internal header
+ // variable, don't dispose it
+
+ nsresult SetUnicodeHeader(MsgHeaderID header, const nsAString& value);
+ nsresult GetUnicodeHeader(MsgHeaderID header, nsAString& _retval);
+
+ /* Convenience routines to get and set header's value...
+
+ IMPORTANT:
+ all routines const char* GetXxx(void) will return a pointer to the header,
+ please don't free it.
+ */
+
+ nsresult SetFrom(const char* value) {
+ return SetAsciiHeader(MSG_FROM_HEADER_ID, value);
+ }
+ const char* GetFrom(void) { return GetAsciiHeader(MSG_FROM_HEADER_ID); }
+
+ nsresult SetReplyTo(const char* value) {
+ return SetAsciiHeader(MSG_REPLY_TO_HEADER_ID, value);
+ }
+ const char* GetReplyTo() { return GetAsciiHeader(MSG_REPLY_TO_HEADER_ID); }
+
+ nsresult SetTo(const char* value) {
+ return SetAsciiHeader(MSG_TO_HEADER_ID, value);
+ }
+ const char* GetTo() { return GetAsciiHeader(MSG_TO_HEADER_ID); }
+
+ nsresult SetCc(const char* value) {
+ return SetAsciiHeader(MSG_CC_HEADER_ID, value);
+ }
+ const char* GetCc() { return GetAsciiHeader(MSG_CC_HEADER_ID); }
+
+ nsresult SetBcc(const char* value) {
+ return SetAsciiHeader(MSG_BCC_HEADER_ID, value);
+ }
+ const char* GetBcc() { return GetAsciiHeader(MSG_BCC_HEADER_ID); }
+
+ nsresult SetFcc(const char* value) {
+ return SetAsciiHeader(MSG_FCC_HEADER_ID, value);
+ }
+ const char* GetFcc() { return GetAsciiHeader(MSG_FCC_HEADER_ID); }
+
+ nsresult SetFcc2(const char* value) {
+ return SetAsciiHeader(MSG_FCC2_HEADER_ID, value);
+ }
+ const char* GetFcc2() { return GetAsciiHeader(MSG_FCC2_HEADER_ID); }
+
+ nsresult SetNewsgroups(const char* aValue) {
+ return SetAsciiHeader(MSG_NEWSGROUPS_HEADER_ID, aValue);
+ }
+ const char* GetNewsgroups() {
+ return GetAsciiHeader(MSG_NEWSGROUPS_HEADER_ID);
+ }
+
+ nsresult SetFollowupTo(const char* aValue) {
+ return SetAsciiHeader(MSG_FOLLOWUP_TO_HEADER_ID, aValue);
+ }
+ const char* GetFollowupTo() {
+ return GetAsciiHeader(MSG_FOLLOWUP_TO_HEADER_ID);
+ }
+
+ nsresult SetSubject(const char* value) {
+ return SetAsciiHeader(MSG_SUBJECT_HEADER_ID, value);
+ }
+ const char* GetSubject() { return GetAsciiHeader(MSG_SUBJECT_HEADER_ID); }
+
+ nsresult SetOrganization(const char* value) {
+ return SetAsciiHeader(MSG_ORGANIZATION_HEADER_ID, value);
+ }
+ const char* GetOrganization() {
+ return GetAsciiHeader(MSG_ORGANIZATION_HEADER_ID);
+ }
+
+ const char* GetReferences() {
+ return GetAsciiHeader(MSG_REFERENCES_HEADER_ID);
+ }
+
+ const char* GetNewspostUrl() {
+ return GetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID);
+ }
+
+ const char* GetPriority() { return GetAsciiHeader(MSG_PRIORITY_HEADER_ID); }
+
+ const char* GetCharacterSet() {
+ return GetAsciiHeader(MSG_CHARACTER_SET_HEADER_ID);
+ }
+
+ const char* GetMessageId() {
+ return GetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID);
+ }
+
+ nsresult SetTemplateName(const char* value) {
+ return SetAsciiHeader(MSG_X_TEMPLATE_HEADER_ID, value);
+ }
+ const char* GetTemplateName() {
+ return GetAsciiHeader(MSG_X_TEMPLATE_HEADER_ID);
+ }
+
+ const char* GetDraftId() { return GetAsciiHeader(MSG_DRAFT_ID_HEADER_ID); }
+ const char* GetTemplateId() {
+ return GetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID);
+ }
+
+ const char* GetContentLanguage() {
+ return GetAsciiHeader(MSG_CONTENT_LANGUAGE_ID);
+ }
+
+ bool GetReturnReceipt() { return m_returnReceipt; }
+ bool GetDSN() { return m_DSN; }
+ bool GetAttachVCard() { return m_attachVCard; }
+ bool GetAttachmentReminder() { return m_attachmentReminder; }
+ int32_t GetDeliveryFormat() { return m_deliveryFormat; }
+ bool GetForcePlainText() { return m_forcePlainText; }
+ bool GetUseMultipartAlternative() { return m_useMultipartAlternative; }
+ bool GetBodyIsAsciiOnly() { return m_bodyIsAsciiOnly; }
+ bool GetForceMsgEncoding() { return m_forceMsgEncoding; }
+
+ nsresult SplitRecipientsEx(const nsAString& recipients,
+ nsTArray<nsMsgRecipient>& aResult);
+
+ protected:
+ virtual ~nsMsgCompFields();
+ nsCString m_headers[MSG_MAX_HEADERS];
+ nsString m_body;
+ nsTArray<RefPtr<nsIMsgAttachment>> m_attachments;
+ nsTArray<nsString> m_otherHeaders;
+ bool m_attachVCard;
+ bool m_attachmentReminder;
+ int32_t m_deliveryFormat;
+ bool m_forcePlainText;
+ bool m_useMultipartAlternative;
+ bool m_returnReceipt;
+ bool m_DSN;
+ bool m_bodyIsAsciiOnly;
+ bool m_forceMsgEncoding;
+ int32_t m_receiptHeaderType; /* receipt header type */
+ nsCString m_DefaultCharacterSet;
+ bool m_needToCheckCharset;
+
+ nsCOMPtr<nsIMsgComposeSecure> mSecureCompFields;
+ nsCOMPtr<msgIWritableStructuredHeaders> mStructuredHeaders;
+};
+
+#endif /* _MsgCompFields_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgCompUtils.cpp b/comm/mailnews/compose/src/nsMsgCompUtils.cpp
new file mode 100644
index 0000000000..50ce86497b
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompUtils.cpp
@@ -0,0 +1,1164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "nsMsgCompUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsStringFwd.h"
+#include "prmem.h"
+#include "nsIStringBundle.h"
+#include "nsIIOService.h"
+#include "nsIHttpProtocolHandler.h"
+#include "nsMailHeaders.h"
+#include "nsMsgI18N.h"
+#include "nsINntpService.h"
+#include "nsMimeTypes.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsMsgPrompts.h"
+#include "nsMsgUtils.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIMIMEService.h"
+#include "nsComposeStrings.h"
+#include "nsIMsgCompUtils.h"
+#include "nsIMsgMdnGenerator.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMemory.h"
+#include "nsCRTGlue.h"
+#include <ctype.h>
+#include "mozilla/dom/Element.h"
+#include "mozilla/EncodingDetector.h"
+#include "mozilla/Components.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/dom/Document.h"
+#include "nsIMIMEInfo.h"
+#include "nsIMsgHeaderParser.h"
+#include "nsIMutableArray.h"
+#include "nsIRandomGenerator.h"
+#include "nsID.h"
+
+void msg_generate_message_id(nsIMsgIdentity* identity,
+ const nsACString& customHost,
+ nsACString& messageID);
+
+NS_IMPL_ISUPPORTS(nsMsgCompUtils, nsIMsgCompUtils)
+
+nsMsgCompUtils::nsMsgCompUtils() {}
+
+nsMsgCompUtils::~nsMsgCompUtils() {}
+
+NS_IMETHODIMP nsMsgCompUtils::MimeMakeSeparator(const char* prefix,
+ char** _retval) {
+ NS_ENSURE_ARG_POINTER(prefix);
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mime_make_separator(prefix);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompUtils::MsgGenerateMessageId(nsIMsgIdentity* identity,
+ const nsACString& host,
+ nsACString& messageID) {
+ // We don't check `host` because it's allowed to be a null pointer (which
+ // means we should ignore it for message ID generation).
+ NS_ENSURE_ARG_POINTER(identity);
+ msg_generate_message_id(identity, host, messageID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompUtils::GetMsgMimeConformToStandard(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = nsMsgMIMEGetConformToStandard();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompUtils::DetectCharset(const nsACString& aContent,
+ nsACString& aCharset) {
+ mozilla::UniquePtr<mozilla::EncodingDetector> detector =
+ mozilla::EncodingDetector::Create();
+ mozilla::Span<const uint8_t> src = mozilla::AsBytes(
+ mozilla::Span(ToNewCString(aContent), aContent.Length()));
+ mozilla::Unused << detector->Feed(src, true);
+ auto encoding = detector->Guess(nullptr, true);
+ encoding->Name(aCharset);
+ return NS_OK;
+}
+
+//
+// Create a file for the a unique temp file
+// on the local machine. Caller must free memory
+//
+nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile) {
+ if ((!tFileName) || (!*tFileName)) tFileName = "nsmail.tmp";
+
+ nsresult rv =
+ GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, tFileName, tFile);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = (*tFile)->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ if (NS_FAILED(rv)) NS_RELEASE(*tFile);
+
+ return rv;
+}
+
+// This is the value a caller will Get if they don't Set first (like MDN
+// sending a return receipt), so init to the default value of the
+// mail.strictly_mime_headers preference.
+static bool mime_headers_use_quoted_printable_p = true;
+
+bool nsMsgMIMEGetConformToStandard(void) {
+ return mime_headers_use_quoted_printable_p;
+}
+
+void nsMsgMIMESetConformToStandard(bool conform_p) {
+ /*
+ * If we are conforming to mime standard no matter what we set
+ * for the headers preference when generating mime headers we should
+ * also conform to the standard. Otherwise, depends the preference
+ * we set. For now, the headers preference is not accessible from UI.
+ */
+ if (conform_p)
+ mime_headers_use_quoted_printable_p = true;
+ else {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ prefs->GetBoolPref("mail.strictly_mime_headers",
+ &mime_headers_use_quoted_printable_p);
+ }
+ }
+}
+
+/**
+ * Checks if the recipient fields have sane values for message send.
+ */
+nsresult mime_sanity_check_fields_recipients(const char* to, const char* cc,
+ const char* bcc,
+ const char* newsgroups) {
+ if (to)
+ while (IS_SPACE(*to)) to++;
+ if (cc)
+ while (IS_SPACE(*cc)) cc++;
+ if (bcc)
+ while (IS_SPACE(*bcc)) bcc++;
+ if (newsgroups)
+ while (IS_SPACE(*newsgroups)) newsgroups++;
+
+ if ((!to || !*to) && (!cc || !*cc) && (!bcc || !*bcc) &&
+ (!newsgroups || !*newsgroups))
+ return NS_MSG_NO_RECIPIENTS;
+
+ return NS_OK;
+}
+
+/**
+ * Checks if the compose fields have sane values for message send.
+ */
+nsresult mime_sanity_check_fields(
+ const char* from, const char* reply_to, const char* to, const char* cc,
+ const char* bcc, const char* fcc, const char* newsgroups,
+ const char* followup_to, const char* /*subject*/,
+ const char* /*references*/, const char* /*organization*/,
+ const char* /*other_random_headers*/) {
+ if (from)
+ while (IS_SPACE(*from)) from++;
+ if (reply_to)
+ while (IS_SPACE(*reply_to)) reply_to++;
+ if (fcc)
+ while (IS_SPACE(*fcc)) fcc++;
+ if (followup_to)
+ while (IS_SPACE(*followup_to)) followup_to++;
+
+ // TODO: sanity check other_random_headers for newline conventions
+ if (!from || !*from) return NS_MSG_NO_SENDER;
+
+ return mime_sanity_check_fields_recipients(to, cc, bcc, newsgroups);
+}
+
+// Helper macro for generating the X-Mozilla-Draft-Info header.
+#define APPEND_BOOL(method, param) \
+ do { \
+ bool val = false; \
+ fields->Get##method(&val); \
+ if (val) \
+ draftInfo.AppendLiteral(param "=1"); \
+ else \
+ draftInfo.AppendLiteral(param "=0"); \
+ } while (false)
+
+nsresult mime_generate_headers(nsIMsgCompFields* fields,
+ nsMsgDeliverMode deliver_mode,
+ msgIWritableStructuredHeaders* finalHeaders) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDraft = deliver_mode == nsIMsgSend::nsMsgSaveAsDraft ||
+ deliver_mode == nsIMsgSend::nsMsgSaveAsTemplate ||
+ deliver_mode == nsIMsgSend::nsMsgQueueForLater ||
+ deliver_mode == nsIMsgSend::nsMsgDeliverBackground;
+
+ bool hasDisclosedRecipient = false;
+
+ MOZ_ASSERT(fields, "null fields");
+ NS_ENSURE_ARG_POINTER(fields);
+
+ nsTArray<RefPtr<msgIAddressObject>> from;
+ fields->GetAddressingHeader("From", true, from);
+
+ // Copy all headers from the original compose field.
+ rv = finalHeaders->AddAllHeaders(fields);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMessageId = false;
+ if (NS_SUCCEEDED(fields->HasHeader("Message-ID", &hasMessageId)) &&
+ hasMessageId) {
+ /* MDN request header requires to have MessageID header presented
+ * in the message in order to
+ * coorelate the MDN reports to the original message. Here will be
+ * the right place
+ */
+
+ bool returnReceipt = false;
+ fields->GetReturnReceipt(&returnReceipt);
+ if (returnReceipt && (deliver_mode != nsIMsgSend::nsMsgSaveAsDraft &&
+ deliver_mode != nsIMsgSend::nsMsgSaveAsTemplate)) {
+ int32_t receipt_header_type = nsIMsgMdnGenerator::eDntType;
+ fields->GetReceiptHeaderType(&receipt_header_type);
+
+ // nsIMsgMdnGenerator::eDntType = MDN Disposition-Notification-To: ;
+ // nsIMsgMdnGenerator::eRrtType = Return-Receipt-To: ;
+ // nsIMsgMdnGenerator::eDntRrtType = both MDN DNT and RRT headers .
+ if (receipt_header_type != nsIMsgMdnGenerator::eRrtType)
+ finalHeaders->SetAddressingHeader("Disposition-Notification-To", from);
+ if (receipt_header_type != nsIMsgMdnGenerator::eDntType)
+ finalHeaders->SetAddressingHeader("Return-Receipt-To", from);
+ }
+ }
+
+ PRExplodedTime now;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);
+ int gmtoffset =
+ (now.tm_params.tp_gmt_offset + now.tm_params.tp_dst_offset) / 60;
+
+ // Use PR_FormatTimeUSEnglish() to format the date in US English format,
+ // then figure out what our local GMT offset is, and append it (since
+ // PR_FormatTimeUSEnglish() can't do that.) Generate four digit years as
+ // per RFC 1123 (superseding RFC 822.)
+ char dateString[130];
+ PR_FormatTimeUSEnglish(dateString, sizeof(dateString),
+ "%a, %d %b %Y %H:%M:%S ", &now);
+
+ char* entryPoint = dateString + strlen(dateString);
+ PR_snprintf(entryPoint, sizeof(dateString) - (entryPoint - dateString),
+ "%c%02d%02d" CRLF, (gmtoffset >= 0 ? '+' : '-'),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) / 60),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) % 60));
+ finalHeaders->SetRawHeader("Date", nsDependentCString(dateString));
+
+ // X-Mozilla-Draft-Info
+ if (isDraft) {
+ nsAutoCString draftInfo;
+ draftInfo.AppendLiteral("internal/draft; ");
+ APPEND_BOOL(AttachVCard, "vcard");
+ draftInfo.AppendLiteral("; ");
+ bool hasReturnReceipt = false;
+ fields->GetReturnReceipt(&hasReturnReceipt);
+ if (hasReturnReceipt) {
+ // slight change compared to 4.x; we used to use receipt= to tell
+ // whether the draft/template has request for either MDN or DNS or both
+ // return receipt; since the DNS is out of the picture we now use the
+ // header type + 1 to tell whether user has requested the return receipt
+ int32_t headerType = 0;
+ fields->GetReceiptHeaderType(&headerType);
+ draftInfo.AppendLiteral("receipt=");
+ draftInfo.AppendInt(headerType + 1);
+ } else
+ draftInfo.AppendLiteral("receipt=0");
+ draftInfo.AppendLiteral("; ");
+ APPEND_BOOL(DSN, "DSN");
+ draftInfo.AppendLiteral("; ");
+ draftInfo.AppendLiteral("uuencode=0");
+ draftInfo.AppendLiteral("; ");
+ APPEND_BOOL(AttachmentReminder, "attachmentreminder");
+ draftInfo.AppendLiteral("; ");
+ int32_t deliveryFormat;
+ fields->GetDeliveryFormat(&deliveryFormat);
+ draftInfo.AppendLiteral("deliveryformat=");
+ draftInfo.AppendInt(deliveryFormat);
+
+ finalHeaders->SetRawHeader(HEADER_X_MOZILLA_DRAFT_INFO, draftInfo);
+ }
+
+ bool sendUserAgent = false;
+ if (prefs) {
+ prefs->GetBoolPref("mailnews.headers.sendUserAgent", &sendUserAgent);
+ }
+ if (sendUserAgent) {
+ bool useMinimalUserAgent = false;
+ if (prefs) {
+ prefs->GetBoolPref("mailnews.headers.useMinimalUserAgent",
+ &useMinimalUserAgent);
+ }
+ if (useMinimalUserAgent) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (bundleService) {
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties",
+ getter_AddRefs(brandBundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsString brandName;
+ brandBundle->GetStringFromName("brandFullName", brandName);
+ if (!brandName.IsEmpty())
+ finalHeaders->SetUnstructuredHeader("User-Agent", brandName);
+ }
+ }
+ } else {
+ nsCOMPtr<nsIHttpProtocolHandler> pHTTPHandler =
+ do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http", &rv);
+ if (NS_SUCCEEDED(rv) && pHTTPHandler) {
+ nsAutoCString userAgentString;
+ // Ignore error since we're testing the return value.
+ mozilla::Unused << pHTTPHandler->GetUserAgent(userAgentString);
+
+ if (!userAgentString.IsEmpty())
+ finalHeaders->SetUnstructuredHeader(
+ "User-Agent", NS_ConvertUTF8toUTF16(userAgentString));
+ }
+ }
+ }
+
+ finalHeaders->SetUnstructuredHeader("MIME-Version", u"1.0"_ns);
+
+ nsAutoCString newsgroups;
+ finalHeaders->GetRawHeader("Newsgroups", newsgroups);
+ if (!newsgroups.IsEmpty()) {
+ // Since the newsgroup header can contain data in the form of:
+ // "news://news.mozilla.org/netscape.test,news://news.mozilla.org/netscape.junk"
+ // we need to turn that into: "netscape.test,netscape.junk"
+ // (XXX: can it really?)
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString newsgroupsHeaderVal;
+ nsCString newshostHeaderVal;
+ rv = nntpService->GenerateNewsHeaderValsForPosting(
+ newsgroups, getter_Copies(newsgroupsHeaderVal),
+ getter_Copies(newshostHeaderVal));
+ NS_ENSURE_SUCCESS(rv, rv);
+ finalHeaders->SetRawHeader("Newsgroups", newsgroupsHeaderVal);
+
+ // If we are here, we are NOT going to send this now. (i.e. it is a Draft,
+ // Send Later file, etc...). Because of that, we need to store what the user
+ // typed in on the original composition window for use later when rebuilding
+ // the headers
+ if (deliver_mode != nsIMsgSend::nsMsgDeliverNow &&
+ deliver_mode != nsIMsgSend::nsMsgSendUnsent) {
+ // This is going to be saved for later, that means we should just store
+ // what the user typed into the "Newsgroup" line in the
+ // HEADER_X_MOZILLA_NEWSHOST header for later use by "Send Unsent
+ // Messages", "Drafts" or "Templates"
+ finalHeaders->SetRawHeader(HEADER_X_MOZILLA_NEWSHOST, newshostHeaderVal);
+ }
+
+ // Newsgroups are a recipient...
+ hasDisclosedRecipient = true;
+ }
+
+ nsTArray<RefPtr<msgIAddressObject>> recipients;
+ finalHeaders->GetAddressingHeader("To", false, recipients);
+ hasDisclosedRecipient |= !recipients.IsEmpty();
+ finalHeaders->GetAddressingHeader("Cc", false, recipients);
+ hasDisclosedRecipient |= !recipients.IsEmpty();
+
+ // If we don't have disclosed recipient (only Bcc), address the message to
+ // undisclosed-recipients to prevent problem with some servers
+
+ // If we are saving the message as a draft, don't bother inserting the
+ // undisclosed recipients field. We'll take care of that when we really send
+ // the message.
+ if (!hasDisclosedRecipient &&
+ (!isDraft || deliver_mode == nsIMsgSend::nsMsgQueueForLater)) {
+ bool bAddUndisclosedRecipients = true;
+ prefs->GetBoolPref("mail.compose.add_undisclosed_recipients",
+ &bAddUndisclosedRecipients);
+ if (bAddUndisclosedRecipients) {
+ bool hasBcc = false;
+ fields->HasHeader("Bcc", &hasBcc);
+ if (hasBcc) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (stringService) {
+ nsCOMPtr<nsIStringBundle> composeStringBundle;
+ rv = stringService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/"
+ "composeMsgs.properties",
+ getter_AddRefs(composeStringBundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsString undisclosedRecipients;
+ rv = composeStringBundle->GetStringFromName("undisclosedRecipients",
+ undisclosedRecipients);
+ if (NS_SUCCEEDED(rv) && !undisclosedRecipients.IsEmpty()) {
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ mozilla::components::HeaderParser::Service());
+ nsCOMPtr<msgIAddressObject> group;
+ nsTArray<RefPtr<msgIAddressObject>> noRecipients;
+ headerParser->MakeGroupObject(undisclosedRecipients, noRecipients,
+ getter_AddRefs(group));
+ recipients.AppendElement(group);
+ finalHeaders->SetAddressingHeader("To", recipients);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // We don't want to emit a Bcc header to the output. If we are saving this to
+ // Drafts/Sent, this is re-added later in nsMsgSend.cpp.
+ finalHeaders->DeleteHeader("bcc");
+
+ // Skip no or empty priority.
+ nsAutoCString priority;
+ rv = fields->GetRawHeader("X-Priority", priority);
+ if (NS_SUCCEEDED(rv) && !priority.IsEmpty()) {
+ nsMsgPriorityValue priorityValue;
+
+ NS_MsgGetPriorityFromString(priority.get(), priorityValue);
+
+ // Skip default priority.
+ if (priorityValue != nsMsgPriority::Default) {
+ nsAutoCString priorityName;
+ nsAutoCString priorityValueString;
+
+ NS_MsgGetPriorityValueString(priorityValue, priorityValueString);
+ NS_MsgGetUntranslatedPriorityName(priorityValue, priorityName);
+
+ // Output format: [X-Priority: <pValue> (<pName>)]
+ priorityValueString.AppendLiteral(" (");
+ priorityValueString += priorityName;
+ priorityValueString.Append(')');
+ finalHeaders->SetRawHeader("X-Priority", priorityValueString);
+ }
+ }
+
+ nsAutoCString references;
+ finalHeaders->GetRawHeader("References", references);
+ if (!references.IsEmpty()) {
+ // The References header should be kept under 998 characters: if it's too
+ // long, trim out the earliest references to make it smaller.
+ if (references.Length() > 986) {
+ int32_t firstRef = references.FindChar('<');
+ int32_t secondRef = references.FindChar('<', firstRef + 1);
+ if (secondRef > 0) {
+ nsAutoCString newReferences(StringHead(references, secondRef));
+ int32_t bracket = references.FindChar(
+ '<', references.Length() + newReferences.Length() - 986);
+ if (bracket > 0) {
+ newReferences.Append(Substring(references, bracket));
+ finalHeaders->SetRawHeader("References", newReferences);
+ }
+ }
+ }
+ // The In-Reply-To header is the last entry in the references header...
+ int32_t bracket = references.RFind("<");
+ if (bracket >= 0)
+ finalHeaders->SetRawHeader("In-Reply-To", Substring(references, bracket));
+ }
+
+ return NS_OK;
+}
+
+#undef APPEND_BOOL // X-Mozilla-Draft-Info helper macro
+
+static void GenerateGlobalRandomBytes(unsigned char* buf, int32_t len) {
+ // Attempt to generate bytes from system entropy-based RNG.
+ nsCOMPtr<nsIRandomGenerator> randomGenerator(
+ do_GetService("@mozilla.org/security/random-generator;1"));
+ MOZ_ASSERT(randomGenerator, "nsIRandomGenerator service not retrievable");
+ uint8_t* tempBuffer;
+ nsresult rv = randomGenerator->GenerateRandomBytes(len, &tempBuffer);
+ if (NS_SUCCEEDED(rv)) {
+ memcpy(buf, tempBuffer, len);
+ free(tempBuffer);
+ return;
+ }
+ // nsIRandomGenerator failed -- fall back to low entropy PRNG.
+ static bool firstTime = true;
+ if (firstTime) {
+ // Seed the random-number generator with current time so that
+ // the numbers will be different every time we run.
+ srand((unsigned)PR_Now());
+ firstTime = false;
+ }
+
+ for (int32_t i = 0; i < len; i++) buf[i] = rand() % 256;
+}
+
+char* mime_make_separator(const char* prefix) {
+ unsigned char rand_buf[13];
+ GenerateGlobalRandomBytes(rand_buf, 12);
+
+ return PR_smprintf(
+ "------------%s"
+ "%02X%02X%02X%02X"
+ "%02X%02X%02X%02X"
+ "%02X%02X%02X%02X",
+ prefix, rand_buf[0], rand_buf[1], rand_buf[2], rand_buf[3], rand_buf[4],
+ rand_buf[5], rand_buf[6], rand_buf[7], rand_buf[8], rand_buf[9],
+ rand_buf[10], rand_buf[11]);
+}
+
+// Tests if the content of a string is a valid host name.
+// In this case, a valid host name is any non-empty string that only contains
+// letters (a-z + A-Z), numbers (0-9) and the characters '-', '_' and '.'.
+static bool isValidHost(const nsCString& host) {
+ if (host.IsEmpty()) {
+ return false;
+ }
+
+ const auto* cur = host.BeginReading();
+ const auto* end = host.EndReading();
+ for (; cur < end; ++cur) {
+ if (!isalpha(*cur) && !isdigit(*cur) && *cur != '-' && *cur != '_' &&
+ *cur != '.') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Extract the domain name from an address.
+// If none could be found (i.e. the address does not contain an '@' sign, or
+// the value following it is not a valid domain), then nullptr is returned.
+void msg_domain_name_from_address(const nsACString& address, nsACString& host) {
+ auto atIndex = address.FindChar('@');
+
+ if (address.IsEmpty() || atIndex == kNotFound) {
+ return;
+ }
+
+ // Substring() should handle cases where we would go out of bounds (by
+ // preventing the index from exceeding the length of the source string), so we
+ // don't need to handle this here.
+ host = Substring(address, atIndex + 1);
+}
+
+// Generate a value for a Message-Id header using the identity and optional
+// hostname provided.
+void msg_generate_message_id(nsIMsgIdentity* identity,
+ const nsACString& customHost,
+ nsACString& messageID) {
+ nsCString host;
+
+ // Check if the identity forces host name. This is sometimes the case when
+ // using newsgroup.
+ nsCString forcedFQDN;
+ nsresult rv = identity->GetCharAttribute("FQDN", forcedFQDN);
+ if (NS_SUCCEEDED(rv) && !forcedFQDN.IsEmpty()) {
+ host = forcedFQDN;
+ }
+
+ // If no valid host name has been set, try using the value defined by the
+ // caller, if any.
+ if (!isValidHost(host)) {
+ host = customHost;
+ }
+
+ // If no valid host name has been set, try extracting one from the email
+ // address associated with the identity.
+ if (!isValidHost(host)) {
+ nsCString from;
+ rv = identity->GetEmail(from);
+ if (NS_SUCCEEDED(rv) && !from.IsEmpty()) {
+ msg_domain_name_from_address(from, host);
+ }
+ }
+
+ // If we still couldn't find a valid host name to use, we can't generate a
+ // valid message ID, so bail, and let NNTP and SMTP generate them.
+ if (!isValidHost(host)) {
+ return;
+ }
+
+ // Generate 128-bit UUID for the local part of the ID. `nsID` provides us with
+ // cryptographically-secure generation.
+ nsID uuid = nsID::GenerateUUID();
+ char uuidString[NSID_LENGTH];
+ uuid.ToProvidedString(uuidString);
+ // Drop first and last characters (curly braces).
+ uuidString[NSID_LENGTH - 2] = 0;
+
+ messageID.AppendPrintf("<%s@%s>", uuidString + 1, host.get());
+}
+
+// this is to guarantee the folded line will never be greater
+// than 78 = 75 + CRLFLWSP
+#define PR_MAX_FOLDING_LEN 75
+
+/*static */ char* RFC2231ParmFolding(const char* parmName,
+ const char* parmValue) {
+ NS_ENSURE_TRUE(parmName && *parmName && parmValue && *parmValue, nullptr);
+
+ bool needEscape;
+ nsCString dupParm;
+ nsCString charset("UTF-8");
+
+ if (!mozilla::IsAsciiNullTerminated(parmValue)) {
+ needEscape = true;
+ dupParm.Assign(parmValue);
+ MsgEscapeString(dupParm, nsINetUtil::ESCAPE_ALL, dupParm);
+ } else {
+ needEscape = false;
+ dupParm.Adopt(msg_make_filename_qtext(parmValue, true));
+ }
+
+ int32_t parmNameLen = PL_strlen(parmName);
+ int32_t parmValueLen = dupParm.Length();
+
+ parmNameLen += 5; // *=__'__'___ or *[0]*=__'__'__ or *[1]*=___ or *[0]="___"
+
+ char* foldedParm = nullptr;
+
+ if ((parmValueLen + parmNameLen + strlen("UTF-8")) < PR_MAX_FOLDING_LEN) {
+ foldedParm = PL_strdup(parmName);
+ if (needEscape) {
+ NS_MsgSACat(&foldedParm, "*=");
+ NS_MsgSACat(&foldedParm, "UTF-8");
+ NS_MsgSACat(&foldedParm, "''"); // We don't support language.
+ } else
+ NS_MsgSACat(&foldedParm, "=\"");
+ NS_MsgSACat(&foldedParm, dupParm.get());
+ if (!needEscape) NS_MsgSACat(&foldedParm, "\"");
+ } else {
+ int curLineLen = 0;
+ int counter = 0;
+ char digits[32];
+ char* start = dupParm.BeginWriting();
+ char* end = NULL;
+ char tmp = 0;
+
+ while (parmValueLen > 0) {
+ curLineLen = 0;
+ if (counter == 0) {
+ PR_FREEIF(foldedParm)
+ foldedParm = PL_strdup(parmName);
+ } else {
+ NS_MsgSACat(&foldedParm, ";\r\n ");
+ NS_MsgSACat(&foldedParm, parmName);
+ }
+ PR_snprintf(digits, sizeof(digits), "*%d", counter);
+ NS_MsgSACat(&foldedParm, digits);
+ curLineLen += PL_strlen(digits);
+ if (needEscape) {
+ NS_MsgSACat(&foldedParm, "*=");
+ if (counter == 0) {
+ NS_MsgSACat(&foldedParm, "UTF-8");
+ NS_MsgSACat(&foldedParm, "''"); // We don't support language.
+ curLineLen += strlen("UTF-8");
+ }
+ } else {
+ NS_MsgSACat(&foldedParm, "=\"");
+ }
+ counter++;
+ curLineLen += parmNameLen;
+ if (parmValueLen <= PR_MAX_FOLDING_LEN - curLineLen)
+ end = start + parmValueLen;
+ else
+ end = start + (PR_MAX_FOLDING_LEN - curLineLen);
+
+ tmp = 0;
+ if (*end && needEscape) {
+ // Check to see if we are in the middle of escaped char.
+ // We use ESCAPE_ALL, so every third character is a '%'.
+ if (end - 1 > start && *(end - 1) == '%') {
+ end -= 1;
+ } else if (end - 2 > start && *(end - 2) == '%') {
+ end -= 2;
+ }
+ // *end is now a '%'.
+ // Check if the following UTF-8 octet is a continuation.
+ while (end - 3 > start && (*(end + 1) == '8' || *(end + 1) == '9' ||
+ *(end + 1) == 'A' || *(end + 1) == 'B')) {
+ end -= 3;
+ }
+ tmp = *end;
+ *end = 0;
+ } else {
+ tmp = *end;
+ *end = 0;
+ }
+ NS_MsgSACat(&foldedParm, start);
+ if (!needEscape) NS_MsgSACat(&foldedParm, "\"");
+
+ parmValueLen -= (end - start);
+ if (tmp) *end = tmp;
+ start = end;
+ }
+ }
+
+ return foldedParm;
+}
+
+bool mime_7bit_data_p(const char* string, uint32_t size) {
+ if ((!string) || (!*string)) return true;
+
+ char* ptr = (char*)string;
+ for (uint32_t i = 0; i < size; i++) {
+ if ((unsigned char)ptr[i] > 0x7F) return false;
+ }
+ return true;
+}
+
+// Strips whitespace, and expands newlines into newline-tab for use in
+// mail headers. Returns a new string or 0 (if it would have been empty.)
+// If addr_p is true, the addresses will be parsed and reemitted as
+// rfc822 mailboxes.
+char* mime_fix_header_1(const char* string, bool addr_p, bool news_p) {
+ char* new_string;
+ const char* in;
+ char* out;
+ int32_t i, old_size, new_size;
+
+ if (!string || !*string) return 0;
+
+ if (addr_p) {
+ return strdup(string);
+ }
+
+ old_size = PL_strlen(string);
+ new_size = old_size;
+ for (i = 0; i < old_size; i++)
+ if (string[i] == '\r' || string[i] == '\n') new_size += 2;
+
+ new_string = (char*)PR_Malloc(new_size + 1);
+ if (!new_string) return 0;
+
+ in = string;
+ out = new_string;
+
+ /* strip leading whitespace. */
+ while (IS_SPACE(*in)) in++;
+
+ /* replace CR, LF, or CRLF with CRLF-TAB. */
+ while (*in) {
+ if (*in == '\r' || *in == '\n') {
+ if (*in == '\r' && in[1] == '\n') in++;
+ in++;
+ *out++ = '\r';
+ *out++ = '\n';
+ *out++ = '\t';
+ } else if (news_p && *in == ',') {
+ *out++ = *in++;
+ /* skip over all whitespace after a comma. */
+ while (IS_SPACE(*in)) in++;
+ } else
+ *out++ = *in++;
+ }
+ *out = 0;
+
+ /* strip trailing whitespace. */
+ while (out > in && IS_SPACE(out[-1])) *out-- = 0;
+
+ /* If we ended up throwing it all away, use 0 instead of "". */
+ if (!*new_string) {
+ PR_Free(new_string);
+ new_string = 0;
+ }
+
+ return new_string;
+}
+
+char* mime_fix_header(const char* string) {
+ return mime_fix_header_1(string, false, false);
+}
+
+char* mime_fix_addr_header(const char* string) {
+ return mime_fix_header_1(string, true, false);
+}
+
+char* mime_fix_news_header(const char* string) {
+ return mime_fix_header_1(string, false, true);
+}
+
+bool mime_type_requires_b64_p(const char* type) {
+ if (!type || !PL_strcasecmp(type, UNKNOWN_CONTENT_TYPE))
+ // Unknown types don't necessarily require encoding. (Note that
+ // "unknown" and "application/octet-stream" aren't the same.)
+ return false;
+
+ else if (!PL_strncasecmp(type, "image/", 6) ||
+ !PL_strncasecmp(type, "audio/", 6) ||
+ !PL_strncasecmp(type, "video/", 6) ||
+ !PL_strncasecmp(type, "application/", 12)) {
+ // The following types are application/ or image/ types that are actually
+ // known to contain textual data (meaning line-based, not binary, where
+ // CRLF conversion is desired rather than disastrous.) So, if the type
+ // is any of these, it does not *require* base64, and if we do need to
+ // encode it for other reasons, we'll probably use quoted-printable.
+ // But, if it's not one of these types, then we assume that any subtypes
+ // of the non-"text/" types are binary data, where CRLF conversion would
+ // corrupt it, so we use base64 right off the bat.
+
+ // The reason it's desirable to ship these as text instead of just using
+ // base64 all the time is mainly to preserve the readability of them for
+ // non-MIME users: if I mail a /bin/sh script to someone, it might not
+ // need to be encoded at all, so we should leave it readable if we can.
+
+ // This list of types was derived from the comp.mail.mime FAQ, section
+ // 10.2.2, "List of known unregistered MIME types" on 2-Feb-96.
+ static const char* app_and_image_types_which_are_really_text[] = {
+ "application/mac-binhex40", /* APPLICATION_BINHEX */
+ "application/pgp", /* APPLICATION_PGP */
+ "application/pgp-keys",
+ "application/x-pgp-message", /* APPLICATION_PGP2 */
+ "application/postscript", /* APPLICATION_POSTSCRIPT */
+ "application/x-uuencode", /* APPLICATION_UUENCODE */
+ "application/x-uue", /* APPLICATION_UUENCODE2 */
+ "application/uue", /* APPLICATION_UUENCODE4 */
+ "application/uuencode", /* APPLICATION_UUENCODE3 */
+ "application/sgml",
+ "application/x-csh",
+ "application/javascript",
+ "application/ecmascript",
+ "application/x-javascript",
+ "application/x-latex",
+ "application/x-macbinhex40",
+ "application/x-ns-proxy-autoconfig",
+ "application/x-www-form-urlencoded",
+ "application/x-perl",
+ "application/x-sh",
+ "application/x-shar",
+ "application/x-tcl",
+ "application/x-tex",
+ "application/x-texinfo",
+ "application/x-troff",
+ "application/x-troff-man",
+ "application/x-troff-me",
+ "application/x-troff-ms",
+ "application/x-troff-ms",
+ "application/x-wais-source",
+ "image/x-bitmap",
+ "image/x-pbm",
+ "image/x-pgm",
+ "image/x-portable-anymap",
+ "image/x-portable-bitmap",
+ "image/x-portable-graymap",
+ "image/x-portable-pixmap", /* IMAGE_PPM */
+ "image/x-ppm",
+ "image/x-xbitmap", /* IMAGE_XBM */
+ "image/x-xbm", /* IMAGE_XBM2 */
+ "image/xbm", /* IMAGE_XBM3 */
+ "image/x-xpixmap",
+ "image/x-xpm",
+ 0};
+ const char** s;
+ for (s = app_and_image_types_which_are_really_text; *s; s++)
+ if (!PL_strcasecmp(type, *s)) return false;
+
+ /* All others must be assumed to be binary formats, and need Base64. */
+ return true;
+ }
+
+ else
+ return false;
+}
+
+//
+// Some types should have a "charset=" parameter, and some shouldn't.
+// This is what decides.
+//
+bool mime_type_needs_charset(const char* type) {
+ /* Only text types should have charset. */
+ if (!type || !*type)
+ return false;
+ else if (!PL_strncasecmp(type, "text", 4))
+ return true;
+ else
+ return false;
+}
+
+// Given a string, convert it to 'qtext' (quoted text) for RFC822 header
+// purposes.
+char* msg_make_filename_qtext(const char* srcText, bool stripCRLFs) {
+ /* newString can be at most twice the original string (every char quoted). */
+ char* newString = (char*)PR_Malloc(PL_strlen(srcText) * 2 + 1);
+ if (!newString) return NULL;
+
+ const char* s = srcText;
+ const char* end = srcText + PL_strlen(srcText);
+ char* d = newString;
+
+ while (*s) {
+ // Put backslashes in front of existing backslashes, or double quote
+ // characters.
+ // If stripCRLFs is true, don't write out CRs or LFs. Otherwise,
+ // write out a backslash followed by the CR but not
+ // linear-white-space.
+ // We might already have quoted pair of "\ " or "\\t" skip it.
+ if (*s == '\\' || *s == '"' ||
+ (!stripCRLFs &&
+ (*s == '\r' && (s[1] != '\n' ||
+ (s[1] == '\n' && (s + 2) < end && !IS_SPACE(s[2]))))))
+ *d++ = '\\';
+
+ if (stripCRLFs && *s == '\r' && s[1] == '\n' && (s + 2) < end &&
+ IS_SPACE(s[2])) {
+ s += 3; // skip CRLFLWSP
+ } else {
+ *d++ = *s++;
+ }
+ }
+ *d = 0;
+
+ return newString;
+}
+
+// Utility to create a nsIURI object...
+nsresult nsMsgNewURL(nsIURI** aInstancePtrResult, const nsCString& aSpec) {
+ nsresult rv = NS_OK;
+ if (nullptr == aInstancePtrResult) return NS_ERROR_NULL_POINTER;
+ nsCOMPtr<nsIIOService> pNetService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(pNetService, NS_ERROR_UNEXPECTED);
+ if (aSpec.Find("://") == kNotFound && !StringBeginsWith(aSpec, "data:"_ns)) {
+ // XXXjag Temporary fix for bug 139362 until the real problem(bug 70083) get
+ // fixed
+ nsAutoCString uri("http://"_ns);
+ uri.Append(aSpec);
+ rv = pNetService->NewURI(uri, nullptr, nullptr, aInstancePtrResult);
+ } else
+ rv = pNetService->NewURI(aSpec, nullptr, nullptr, aInstancePtrResult);
+ return rv;
+}
+
+char* nsMsgGetLocalFileFromURL(const char* url) {
+ char* finalPath;
+ NS_ASSERTION(PL_strncasecmp(url, "file://", 7) == 0, "invalid url");
+ finalPath = (char*)PR_Malloc(strlen(url));
+ if (finalPath == NULL) return NULL;
+ strcpy(finalPath, url + 6 + 1);
+ return finalPath;
+}
+
+char* nsMsgParseURLHost(const char* url) {
+ nsIURI* workURI = nullptr;
+ nsresult rv;
+
+ rv = nsMsgNewURL(&workURI, nsDependentCString(url));
+ if (NS_FAILED(rv) || !workURI) return nullptr;
+
+ nsAutoCString host;
+ rv = workURI->GetHost(host);
+ NS_IF_RELEASE(workURI);
+ if (NS_FAILED(rv)) return nullptr;
+
+ return ToNewCString(host);
+}
+
+char* GenerateFileNameFromURI(nsIURI* aURL) {
+ nsresult rv;
+ nsCString file;
+ nsCString spec;
+ char* returnString;
+ char* cp = nullptr;
+ char* cp1 = nullptr;
+
+ rv = aURL->GetPathQueryRef(file);
+ if (NS_SUCCEEDED(rv) && !file.IsEmpty()) {
+ char* newFile = ToNewCString(file);
+ if (!newFile) return nullptr;
+
+ // strip '/'
+ cp = PL_strrchr(newFile, '/');
+ if (cp)
+ ++cp;
+ else
+ cp = newFile;
+
+ if (*cp) {
+ if ((cp1 = PL_strchr(cp, '/'))) *cp1 = 0;
+ if ((cp1 = PL_strchr(cp, '?'))) *cp1 = 0;
+ if ((cp1 = PL_strchr(cp, '>'))) *cp1 = 0;
+ if (*cp != '\0') {
+ returnString = PL_strdup(cp);
+ PR_FREEIF(newFile);
+ return returnString;
+ }
+ } else
+ return nullptr;
+ }
+
+ cp = nullptr;
+ cp1 = nullptr;
+
+ rv = aURL->GetSpec(spec);
+ if (NS_SUCCEEDED(rv) && !spec.IsEmpty()) {
+ char* newSpec = ToNewCString(spec);
+ if (!newSpec) return nullptr;
+
+ char *cp2 = NULL, *cp3 = NULL;
+
+ // strip '"'
+ cp2 = newSpec;
+ while (*cp2 == '"') cp2++;
+ if ((cp3 = PL_strchr(cp2, '"'))) *cp3 = 0;
+
+ char* hostStr = nsMsgParseURLHost(cp2);
+ if (!hostStr) hostStr = PL_strdup(cp2);
+
+ bool isHTTP = false;
+ if (NS_SUCCEEDED(aURL->SchemeIs("http", &isHTTP)) && isHTTP) {
+ returnString = PR_smprintf("%s.html", hostStr);
+ PR_FREEIF(hostStr);
+ } else
+ returnString = hostStr;
+
+ PR_FREEIF(newSpec);
+ return returnString;
+ }
+
+ return nullptr;
+}
+
+//
+// This routine will generate a content id for use in a mail part.
+// It will take the part number passed in as well as the email
+// address. If the email address is null or invalid, we will simply
+// use netscape.com for the interesting part. The content ID's will
+// look like the following:
+//
+// Content-ID: <part1.36DF1DCE.73B5A330@netscape.com>
+//
+char* mime_gen_content_id(uint32_t aPartNum, const char* aEmailAddress) {
+ int32_t randLen = 5;
+ unsigned char rand_buf1[5];
+ unsigned char rand_buf2[5];
+ const char* domain = nullptr;
+ const char* defaultDomain = "@netscape.com";
+
+ memset(rand_buf1, 0, randLen - 1);
+ memset(rand_buf2, 0, randLen - 1);
+
+ GenerateGlobalRandomBytes(rand_buf1, randLen);
+ GenerateGlobalRandomBytes(rand_buf2, randLen);
+
+ // Find the @domain.com string...
+ if (aEmailAddress && *aEmailAddress)
+ domain = const_cast<const char*>(PL_strchr(aEmailAddress, '@'));
+
+ if (!domain) domain = defaultDomain;
+
+ char* retVal = PR_smprintf(
+ "part%d."
+ "%02X%02X%02X%02X"
+ "."
+ "%02X%02X%02X%02X"
+ "%s",
+ aPartNum, rand_buf1[0], rand_buf1[1], rand_buf1[2], rand_buf1[3],
+ rand_buf2[0], rand_buf2[1], rand_buf2[2], rand_buf2[3], domain);
+
+ return retVal;
+}
+
+void GetFolderURIFromUserPrefs(nsMsgDeliverMode aMode, nsIMsgIdentity* identity,
+ nsCString& uri) {
+ nsresult rv;
+ uri.Truncate();
+
+ // QueueForLater (Outbox)
+ if (aMode == nsIMsgSend::nsMsgQueueForLater ||
+ aMode == nsIMsgSend::nsMsgDeliverBackground) {
+ nsCOMPtr<nsIPrefBranch> prefs(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return;
+ rv = prefs->GetCharPref("mail.default_sendlater_uri", uri);
+ if (NS_FAILED(rv) || uri.IsEmpty())
+ uri.AssignLiteral(ANY_SERVER);
+ else {
+ // check if uri is unescaped, and if so, escape it and reset the pef.
+ if (uri.FindChar(' ') != kNotFound) {
+ uri.ReplaceSubstring(" ", "%20");
+ prefs->SetCharPref("mail.default_sendlater_uri", uri);
+ }
+ }
+ return;
+ }
+
+ if (!identity) return;
+
+ if (aMode == nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts)
+ rv = identity->GetDraftFolder(uri);
+ else if (aMode ==
+ nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates)
+ rv = identity->GetStationeryFolder(uri);
+ else {
+ bool doFcc = false;
+ rv = identity->GetDoFcc(&doFcc);
+ if (doFcc) rv = identity->GetFccFolder(uri);
+ }
+ return;
+}
+
+/**
+ * Check if we should use format=flowed (RFC 2646) for a mail.
+ * We will use format=flowed unless the preference tells us not to do so.
+ * In this function we set all the serialiser flags.
+ * 'formatted' is always 'true'.
+ */
+void GetSerialiserFlags(bool* flowed, bool* formatted) {
+ *flowed = false;
+ *formatted = true;
+
+ // Set format=flowed as in RFC 2646 according to the preference.
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ prefs->GetBoolPref("mailnews.send_plaintext_flowed", flowed);
+ }
+}
+
+already_AddRefed<nsIArray> GetEmbeddedObjects(
+ mozilla::dom::Document* aDocument) {
+ nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ if (NS_WARN_IF(!nodes)) {
+ return nullptr;
+ }
+
+ mozilla::PostContentIterator iter;
+ nsresult rv = iter.Init(aDocument->GetRootElement());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ // Loop through the content iterator for each content node.
+ while (!iter.IsDone()) {
+ nsINode* node = iter.GetCurrentNode();
+ if (node->IsElement()) {
+ mozilla::dom::Element* element = node->AsElement();
+
+ // See if it's an image or also include all links.
+ // Let mail decide which link to send or not
+ if (element->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::a) ||
+ (element->IsHTMLElement(nsGkAtoms::body) &&
+ element->HasAttr(kNameSpaceID_None, nsGkAtoms::background))) {
+ nodes->AppendElement(node);
+ }
+ }
+ iter.Next();
+ }
+
+ return nodes.forget();
+}
diff --git a/comm/mailnews/compose/src/nsMsgCompUtils.h b/comm/mailnews/compose/src/nsMsgCompUtils.h
new file mode 100644
index 0000000000..7197cde459
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompUtils.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgCompUtils_H_
+#define _nsMsgCompUtils_H_
+
+#include "nscore.h"
+#include "mozilla/dom/Document.h"
+#include "nsMsgCompFields.h"
+#include "nsIMsgSend.h"
+#include "nsIMsgCompUtils.h"
+
+class nsIArray;
+class nsIDocument;
+class nsIPrompt;
+
+#define ANY_SERVER "anyfolder://"
+
+// these are msg hdr property names for storing the original
+// msg uri's and disposition(replied/forwarded) when queuing
+// messages to send later.
+#define ORIG_URI_PROPERTY "origURIs"
+#define QUEUED_DISPOSITION_PROPERTY "queuedDisposition"
+
+extern mozilla::LazyLogModule Compose;
+
+class nsMsgCompUtils : public nsIMsgCompUtils {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOMPUTILS
+
+ nsMsgCompUtils();
+
+ private:
+ virtual ~nsMsgCompUtils();
+};
+
+already_AddRefed<nsIArray> GetEmbeddedObjects(
+ mozilla::dom::Document* aDocument);
+
+PR_BEGIN_EXTERN_C
+
+//
+// Create a file spec or file name using the name passed
+// in as a template
+//
+nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile);
+char* nsMsgCreateTempFileName(const char* tFileName);
+
+//
+// Various utilities for building parts of MIME encoded
+// messages during message composition
+//
+
+nsresult mime_sanity_check_fields_recipients(const char* to, const char* cc,
+ const char* bcc,
+ const char* newsgroups);
+
+nsresult mime_sanity_check_fields(
+ const char* from, const char* reply_to, const char* to, const char* cc,
+ const char* bcc, const char* fcc, const char* newsgroups,
+ const char* followup_to, const char* /*subject*/,
+ const char* /*references*/, const char* /*organization*/,
+ const char* /*other_random_headers*/);
+
+nsresult mime_generate_headers(nsIMsgCompFields* fields,
+ nsMsgDeliverMode deliver_mode,
+ msgIWritableStructuredHeaders* headers);
+
+char* mime_make_separator(const char* prefix);
+char* mime_gen_content_id(uint32_t aPartNum, const char* aEmailAddress);
+
+bool mime_7bit_data_p(const char* string, uint32_t size);
+
+char* mime_fix_header_1(const char* string, bool addr_p, bool news_p);
+char* mime_fix_header(const char* string);
+char* mime_fix_addr_header(const char* string);
+char* mime_fix_news_header(const char* string);
+
+bool mime_type_requires_b64_p(const char* type);
+bool mime_type_needs_charset(const char* type);
+
+char* msg_make_filename_qtext(const char* srcText, bool stripCRLFs);
+
+char* RFC2231ParmFolding(const char* parmName, const char* parmValue);
+
+//
+// Informational calls...
+//
+void nsMsgMIMESetConformToStandard(bool conform_p);
+bool nsMsgMIMEGetConformToStandard(void);
+
+//
+// network service type calls...
+//
+nsresult nsMsgNewURL(nsIURI** aInstancePtrResult, const nsCString& aSpec);
+char* nsMsgGetLocalFileFromURL(const char* url);
+
+char* nsMsgParseURLHost(const char* url);
+
+char* GenerateFileNameFromURI(nsIURI* aURL);
+
+//
+// Folder calls...
+//
+void GetFolderURIFromUserPrefs(nsMsgDeliverMode aMode, nsIMsgIdentity* identity,
+ nsCString& uri);
+
+// Check if we should use format=flowed
+void GetSerialiserFlags(bool* flowed, bool* formatted);
+
+PR_END_EXTERN_C
+
+#endif /* _nsMsgCompUtils_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgCompose.cpp b/comm/mailnews/compose/src/nsMsgCompose.cpp
new file mode 100644
index 0000000000..2630852f5a
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompose.cpp
@@ -0,0 +1,5094 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgCompose.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIDOMWindow.h"
+#include "mozIDOMWindow.h"
+#include "nsISelectionController.h"
+#include "nsMsgI18N.h"
+#include "nsMsgQuote.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIDocumentEncoder.h" // for editor output flags
+#include "nsMsgCompUtils.h"
+#include "nsComposeStrings.h"
+#include "nsIMsgSend.h"
+#include "nsMailHeaders.h"
+#include "nsMsgPrompts.h"
+#include "nsMimeTypes.h"
+#include "nsICharsetConverterManager.h"
+#include "nsTextFormatter.h"
+#include "nsIHTMLEditor.h"
+#include "nsIEditor.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "nsIDocShell.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIMIMEService.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIWindowMediator.h"
+#include "nsIURL.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+#include "nsIMsgComposeService.h"
+#include "nsIMsgComposeProgressParams.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsImapCore.h"
+#include "nsUnicharUtils.h"
+#include "nsNetUtil.h"
+#include "nsIContentViewer.h"
+#include "nsIMsgMdnGenerator.h"
+#include "plbase64.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgAttachment.h"
+#include "nsIMsgProgress.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgDatabase.h"
+#include "nsStringStream.h"
+#include "nsArrayUtils.h"
+#include "nsIMsgWindow.h"
+#include "nsITextToSubURI.h"
+#include "nsIAbManager.h"
+#include "nsCRT.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/Utf8.h"
+#include "nsStreamConverter.h"
+#include "nsIObserverService.h"
+#include "nsIProtocolHandler.h"
+#include "nsContentUtils.h"
+#include "nsStreamUtils.h"
+#include "nsIFileURL.h"
+#include "nsTextNode.h" // from dom/base
+#include "nsIParserUtils.h"
+#include "nsIStringBundle.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::mailnews;
+
+LazyLogModule Compose("Compose");
+
+static nsresult GetReplyHeaderInfo(int32_t* reply_header_type,
+ nsString& reply_header_authorwrote,
+ nsString& reply_header_ondateauthorwrote,
+ nsString& reply_header_authorwroteondate,
+ nsString& reply_header_originalmessage) {
+ nsresult rv;
+ *reply_header_type = 0;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If fetching any of the preferences fails,
+ // we return early with header_type = 0 meaning "no header".
+ rv = NS_GetLocalizedUnicharPreference(
+ prefBranch, "mailnews.reply_header_authorwrotesingle",
+ reply_header_authorwrote);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_GetLocalizedUnicharPreference(
+ prefBranch, "mailnews.reply_header_ondateauthorwrote",
+ reply_header_ondateauthorwrote);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_GetLocalizedUnicharPreference(
+ prefBranch, "mailnews.reply_header_authorwroteondate",
+ reply_header_authorwroteondate);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_GetLocalizedUnicharPreference(prefBranch,
+ "mailnews.reply_header_originalmessage",
+ reply_header_originalmessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return prefBranch->GetIntPref("mailnews.reply_header_type",
+ reply_header_type);
+}
+
+static void TranslateLineEnding(nsString& data) {
+ char16_t* rPtr; // Read pointer
+ char16_t* wPtr; // Write pointer
+ char16_t* sPtr; // Start data pointer
+ char16_t* ePtr; // End data pointer
+
+ rPtr = wPtr = sPtr = data.BeginWriting();
+ ePtr = rPtr + data.Length();
+
+ while (rPtr < ePtr) {
+ if (*rPtr == nsCRT::CR) {
+ *wPtr = nsCRT::LF;
+ if (rPtr + 1 < ePtr && *(rPtr + 1) == nsCRT::LF) rPtr++;
+ } else
+ *wPtr = *rPtr;
+
+ rPtr++;
+ wPtr++;
+ }
+
+ data.SetLength(wPtr - sPtr);
+}
+
+nsMsgCompose::nsMsgCompose() {
+ mQuotingToFollow = false;
+ mAllowRemoteContent = false;
+ mWhatHolder = 1;
+ m_window = nullptr;
+ m_editor = nullptr;
+ mQuoteStreamListener = nullptr;
+ mAutodetectCharset = false;
+ mDeleteDraft = false;
+ m_compFields =
+ nullptr; // m_compFields will be set during nsMsgCompose::Initialize
+ mType = nsIMsgCompType::New;
+
+ // For TagConvertible
+ // Read and cache pref
+ mConvertStructs = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch)
+ prefBranch->GetBoolPref("converter.html2txt.structs", &mConvertStructs);
+
+ m_composeHTML = false;
+
+ mTmpAttachmentsDeleted = false;
+ mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_None;
+ mDeliverMode = 0;
+}
+
+nsMsgCompose::~nsMsgCompose() {
+ MOZ_LOG(Compose, LogLevel::Debug, ("~nsMsgCompose()"));
+ if (!m_compFields) {
+ // Uhoh. We're in an uninitialized state. Maybe initialize() failed, or
+ // was never even called.
+ return;
+ }
+ m_window = nullptr;
+ if (!mMsgSend) {
+ // This dtor can be called before mMsgSend->CreateAndSendMessage returns,
+ // tmp attachments are needed to create the message, so don't delete them.
+ DeleteTmpAttachments();
+ }
+}
+
+/* the following macro actually implement addref, release and query interface
+ * for our component. */
+NS_IMPL_ISUPPORTS(nsMsgCompose, nsIMsgCompose, nsIMsgSendListener,
+ nsISupportsWeakReference)
+
+//
+// Once we are here, convert the data which we know to be UTF-8 to UTF-16
+// for insertion into the editor
+//
+nsresult GetChildOffset(nsINode* aChild, nsINode* aParent, int32_t& aOffset) {
+ NS_ASSERTION((aChild && aParent), "bad args");
+
+ if (!aChild || !aParent) return NS_ERROR_NULL_POINTER;
+
+ nsINodeList* childNodes = aParent->ChildNodes();
+ for (uint32_t i = 0; i < childNodes->Length(); i++) {
+ nsINode* childNode = childNodes->Item(i);
+ if (childNode == aChild) {
+ aOffset = i;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NULL_POINTER;
+}
+
+nsresult GetNodeLocation(nsINode* inChild, nsCOMPtr<nsINode>* outParent,
+ int32_t* outOffset) {
+ NS_ASSERTION((outParent && outOffset), "bad args");
+ nsresult result = NS_ERROR_NULL_POINTER;
+ if (inChild && outParent && outOffset) {
+ nsCOMPtr<nsINode> inChild2 = inChild;
+ *outParent = inChild2->GetParentNode();
+ if (*outParent) {
+ result = GetChildOffset(inChild2, *outParent, *outOffset);
+ }
+ }
+
+ return result;
+}
+
+bool nsMsgCompose::IsEmbeddedObjectSafe(const char* originalScheme,
+ const char* originalHost,
+ const char* originalPath,
+ Element* element) {
+ nsresult rv;
+
+ nsAutoString objURL;
+
+ if (!originalScheme || !originalPath) // Having a null host is OK.
+ return false;
+
+ RefPtr<HTMLImageElement> image = HTMLImageElement::FromNode(element);
+ RefPtr<HTMLAnchorElement> anchor = HTMLAnchorElement::FromNode(element);
+
+ if (image)
+ image->GetSrc(objURL);
+ else if (anchor)
+ anchor->GetHref(objURL);
+ else
+ return false;
+
+ if (!objURL.IsEmpty()) {
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), objURL);
+ if (NS_SUCCEEDED(rv) && uri) {
+ nsAutoCString scheme;
+ rv = uri->GetScheme(scheme);
+ if (NS_SUCCEEDED(rv) &&
+ scheme.Equals(originalScheme, nsCaseInsensitiveCStringComparator)) {
+ nsAutoCString host;
+ rv = uri->GetAsciiHost(host);
+ // mailbox url don't have a host therefore don't be too strict.
+ if (NS_SUCCEEDED(rv) &&
+ (host.IsEmpty() || originalHost ||
+ host.Equals(originalHost, nsCaseInsensitiveCStringComparator))) {
+ nsAutoCString path;
+ rv = uri->GetPathQueryRef(path);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString orgPath(originalPath);
+ MsgRemoveQueryPart(orgPath);
+ MsgRemoveQueryPart(path);
+ // mailbox: and JS Account URLs have a message number in
+ // the query part of "path query ref". We removed this so
+ // we're not comparing down to the message but down to the folder.
+ // Code in the frontend (in the "error" event listener in
+ // MsgComposeCommands.js that deals with unblocking images) will
+ // prompt if a part of another message is referenced.
+ // A saved message opened for reply or forwarding has a
+ // mailbox: URL.
+ // imap: URLs don't have the message number in the query, so we do
+ // compare it here.
+ // news: URLs use group and key in the query, but it's OK to compare
+ // without them.
+ return path.Equals(orgPath, nsCaseInsensitiveCStringComparator);
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+/* The purpose of this function is to mark any embedded object that wasn't a
+ RFC822 part of the original message as moz-do-not-send. That will prevent us
+ to attach data not specified by the user or not present in the original
+ message.
+*/
+nsresult nsMsgCompose::TagEmbeddedObjects(nsIEditor* aEditor) {
+ nsresult rv = NS_OK;
+ uint32_t count;
+ uint32_t i;
+
+ if (!aEditor) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<Document> document;
+ aEditor->GetDocument(getter_AddRefs(document));
+ if (!document) return NS_ERROR_FAILURE;
+ nsCOMPtr<nsIArray> aNodeList = GetEmbeddedObjects(document);
+ if (!aNodeList) return NS_ERROR_FAILURE;
+
+ if (NS_FAILED(aNodeList->GetLength(&count))) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIURI> originalUrl;
+ nsCString originalScheme;
+ nsCString originalHost;
+ nsCString originalPath;
+
+ // first, convert the rdf original msg uri into a url that represents the
+ // message...
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ rv = GetMessageServiceFromURI(mOriginalMsgURI, getter_AddRefs(msgService));
+ if (NS_SUCCEEDED(rv)) {
+ rv = msgService->GetUrlForUri(mOriginalMsgURI, nullptr,
+ getter_AddRefs(originalUrl));
+ if (NS_SUCCEEDED(rv) && originalUrl) {
+ originalUrl->GetScheme(originalScheme);
+ originalUrl->GetAsciiHost(originalHost);
+ originalUrl->GetPathQueryRef(originalPath);
+ }
+ }
+
+ // Then compare the url of each embedded objects with the original message.
+ // If they a not coming from the original message, they should not be sent
+ // with the message.
+ for (i = 0; i < count; i++) {
+ nsCOMPtr<Element> domElement = do_QueryElementAt(aNodeList, i);
+ if (!domElement) continue;
+ if (IsEmbeddedObjectSafe(originalScheme.get(), originalHost.get(),
+ originalPath.get(), domElement))
+ continue; // Don't need to tag this object, it's safe to send it.
+
+ // The source of this object should not be sent with the message.
+ IgnoredErrorResult rv2;
+ domElement->SetAttribute(u"moz-do-not-send"_ns, u"true"_ns, rv2);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::GetAllowRemoteContent(bool* aAllowRemoteContent) {
+ NS_ENSURE_ARG_POINTER(aAllowRemoteContent);
+ *aAllowRemoteContent = mAllowRemoteContent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::SetAllowRemoteContent(bool aAllowRemoteContent) {
+ mAllowRemoteContent = aAllowRemoteContent;
+ return NS_OK;
+}
+
+void nsMsgCompose::InsertDivWrappedTextAtSelection(const nsAString& aText,
+ const nsAString& classStr) {
+ NS_ASSERTION(m_editor,
+ "InsertDivWrappedTextAtSelection called, but no editor exists");
+ if (!m_editor) return;
+
+ RefPtr<Element> divElem;
+ nsCOMPtr<nsIHTMLEditor> htmlEditor(do_QueryInterface(m_editor));
+
+ nsresult rv =
+ htmlEditor->CreateElementWithDefaults(u"div"_ns, getter_AddRefs(divElem));
+
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // We need the document
+ nsCOMPtr<Document> doc;
+ rv = m_editor->GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Break up the text by newlines, and then insert text nodes followed
+ // by <br> nodes.
+ int32_t start = 0;
+ int32_t end = aText.Length();
+
+ for (;;) {
+ int32_t delimiter = aText.FindChar('\n', start);
+ if (delimiter == kNotFound) delimiter = end;
+
+ RefPtr<nsTextNode> textNode =
+ doc->CreateTextNode(Substring(aText, start, delimiter - start));
+
+ IgnoredErrorResult rv2;
+ divElem->AppendChild(*textNode, rv2);
+ if (rv2.Failed()) {
+ return;
+ }
+
+ // Now create and insert a BR
+ RefPtr<Element> brElem;
+ rv =
+ htmlEditor->CreateElementWithDefaults(u"br"_ns, getter_AddRefs(brElem));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ divElem->AppendChild(*brElem, rv2);
+ if (rv2.Failed()) {
+ return;
+ }
+
+ if (delimiter == end) break;
+ start = ++delimiter;
+ if (start == end) break;
+ }
+
+ htmlEditor->InsertElementAtSelection(divElem, true);
+ nsCOMPtr<nsINode> parent;
+ int32_t offset;
+
+ rv = GetNodeLocation(divElem, address_of(parent), &offset);
+ if (NS_SUCCEEDED(rv)) {
+ RefPtr<Selection> selection;
+ m_editor->GetSelection(getter_AddRefs(selection));
+
+ if (selection) selection->CollapseInLimiter(parent, offset + 1);
+ }
+ if (divElem) {
+ RefPtr<Element> divElem2 = divElem;
+ IgnoredErrorResult rv2;
+ divElem2->SetAttribute(u"class"_ns, classStr, rv2);
+ }
+}
+
+/*
+ * The following function replaces <plaintext> tags with <x-plaintext>.
+ * <plaintext> is a funny beast: It leads to everything following it
+ * being displayed verbatim, even a </plaintext> tag is ignored.
+ */
+static void remove_plaintext_tag(nsString& body) {
+ // Replace all <plaintext> and </plaintext> tags.
+ int32_t index = 0;
+ bool replaced = false;
+ while ((index = body.LowerCaseFindASCII("<plaintext", index)) != kNotFound) {
+ body.Insert(u"x-", index + 1);
+ index += 12;
+ replaced = true;
+ }
+ if (replaced) {
+ index = 0;
+ while ((index = body.LowerCaseFindASCII("</plaintext", index)) !=
+ kNotFound) {
+ body.Insert(u"x-", index + 2);
+ index += 13;
+ }
+ }
+}
+
+static void remove_conditional_CSS(const nsAString& in, nsAString& out) {
+ nsCOMPtr<nsIParserUtils> parserUtils =
+ do_GetService(NS_PARSERUTILS_CONTRACTID);
+ parserUtils->RemoveConditionalCSS(in, out);
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMsgCompose::ConvertAndLoadComposeWindow(nsString& aPrefix, nsString& aBuf,
+ nsString& aSignature, bool aQuoted,
+ bool aHTMLEditor) {
+ NS_ASSERTION(m_editor, "ConvertAndLoadComposeWindow but no editor");
+ NS_ENSURE_TRUE(m_editor && m_identity, NS_ERROR_NOT_INITIALIZED);
+
+ // First, get the nsIEditor interface for future use
+ nsCOMPtr<nsINode> nodeInserted;
+
+ TranslateLineEnding(aPrefix);
+ TranslateLineEnding(aBuf);
+ TranslateLineEnding(aSignature);
+
+ m_editor->EnableUndo(false);
+
+ // Ok - now we need to figure out the charset of the aBuf we are going to send
+ // into the editor shell. There are I18N calls to sniff the data and then we
+ // need to call the new routine in the editor that will allow us to send in
+ // the charset
+ //
+
+ // Now, insert it into the editor...
+ RefPtr<HTMLEditor> htmlEditor = m_editor->AsHTMLEditor();
+ int32_t reply_on_top = 0;
+ bool sig_bottom = true;
+ m_identity->GetReplyOnTop(&reply_on_top);
+ m_identity->GetSigBottom(&sig_bottom);
+ bool sigOnTop = (reply_on_top == 1 && !sig_bottom);
+ bool isForwarded = (mType == nsIMsgCompType::ForwardInline);
+
+ // When in paragraph mode, don't call InsertLineBreak() since that inserts
+ // a full paragraph instead of just a line break since we switched
+ // the default paragraph separator to "p".
+ bool paragraphMode =
+ mozilla::Preferences::GetBool("mail.compose.default_to_paragraph", false);
+
+ if (aQuoted) {
+ if (!aPrefix.IsEmpty()) {
+ if (!aHTMLEditor) aPrefix.AppendLiteral("\n");
+
+ int32_t reply_on_top = 0;
+ m_identity->GetReplyOnTop(&reply_on_top);
+ if (reply_on_top == 1) {
+ // HTML editor eats one line break but not a whole paragraph.
+ if (aHTMLEditor && !paragraphMode) htmlEditor->InsertLineBreak();
+
+ // add one newline if a signature comes before the quote, two otherwise
+ bool includeSignature = true;
+ bool sig_bottom = true;
+ bool attachFile = false;
+ nsString prefSigText;
+
+ m_identity->GetSigOnReply(&includeSignature);
+ m_identity->GetSigBottom(&sig_bottom);
+ m_identity->GetHtmlSigText(prefSigText);
+ nsresult rv = m_identity->GetAttachSignature(&attachFile);
+ if (!paragraphMode || !aHTMLEditor) {
+ if (includeSignature && !sig_bottom &&
+ ((NS_SUCCEEDED(rv) && attachFile) || !prefSigText.IsEmpty()))
+ htmlEditor->InsertLineBreak();
+ else {
+ htmlEditor->InsertLineBreak();
+ htmlEditor->InsertLineBreak();
+ }
+ }
+ }
+
+ InsertDivWrappedTextAtSelection(aPrefix, u"moz-cite-prefix"_ns);
+ }
+
+ if (!aBuf.IsEmpty()) {
+ // This leaves the caret at the right place to insert a bottom signature.
+ if (aHTMLEditor) {
+ nsAutoString body(aBuf);
+ remove_plaintext_tag(body);
+ htmlEditor->InsertAsCitedQuotation(body, mCiteReference, true,
+ getter_AddRefs(nodeInserted));
+ } else {
+ htmlEditor->InsertAsQuotation(aBuf, getter_AddRefs(nodeInserted));
+ }
+ }
+
+ (void)TagEmbeddedObjects(htmlEditor);
+
+ if (!aSignature.IsEmpty()) {
+ // we cannot add it on top earlier, because TagEmbeddedObjects will mark
+ // all images in the signature as "moz-do-not-send"
+ if (sigOnTop) MoveToBeginningOfDocument();
+
+ if (aHTMLEditor) {
+ bool oldAllow;
+ GetAllowRemoteContent(&oldAllow);
+ SetAllowRemoteContent(true);
+ htmlEditor->InsertHTML(aSignature);
+ SetAllowRemoteContent(oldAllow);
+ } else {
+ htmlEditor->InsertLineBreak();
+ InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns);
+ }
+
+ if (sigOnTop) htmlEditor->EndOfDocument();
+ }
+ } else {
+ if (aHTMLEditor) {
+ if (isForwarded &&
+ Substring(aBuf, 0, sizeof(MIME_FORWARD_HTML_PREFIX) - 1)
+ .EqualsLiteral(MIME_FORWARD_HTML_PREFIX)) {
+ // We assign the opening tag inside "<HTML><BODY><BR><BR>" before the
+ // two <br> elements.
+ // This is a bit hacky but we know that the MIME code prepares the
+ // forwarded content like this:
+ // <HTML><BODY><BR><BR> + forwarded header + header table.
+ // Note: We only do this when we prepare the message to be forwarded,
+ // a re-opened saved draft of a forwarded message does not repeat this.
+ nsString divTag;
+ divTag.AssignLiteral("<div class=\"moz-forward-container\">");
+ aBuf.Insert(divTag, sizeof(MIME_FORWARD_HTML_PREFIX) - 1 - 8);
+ }
+ remove_plaintext_tag(aBuf);
+
+ bool stripConditionalCSS = mozilla::Preferences::GetBool(
+ "mail.html_sanitize.drop_conditional_css", true);
+
+ if (stripConditionalCSS) {
+ nsString newBody;
+ remove_conditional_CSS(aBuf, newBody);
+ htmlEditor->RebuildDocumentFromSource(newBody);
+ } else {
+ htmlEditor->RebuildDocumentFromSource(aBuf);
+ }
+
+ // When forwarding a message as inline, or editing as new (which could
+ // contain unsanitized remote content), tag any embedded objects
+ // with moz-do-not-send=true so they don't get attached upon send.
+ if (isForwarded || mType == nsIMsgCompType::EditAsNew)
+ (void)TagEmbeddedObjects(htmlEditor);
+
+ if (!aSignature.IsEmpty()) {
+ if (isForwarded && sigOnTop) {
+ // Use our own function, nsEditor::BeginningOfDocument() would
+ // position into the <div class="moz-forward-container"> we've just
+ // created.
+ MoveToBeginningOfDocument();
+ } else {
+ // Use our own function, nsEditor::EndOfDocument() would position
+ // into the <div class="moz-forward-container"> we've just created.
+ MoveToEndOfDocument();
+ }
+
+ bool oldAllow;
+ GetAllowRemoteContent(&oldAllow);
+ SetAllowRemoteContent(true);
+ htmlEditor->InsertHTML(aSignature);
+ SetAllowRemoteContent(oldAllow);
+
+ if (isForwarded && sigOnTop) htmlEditor->EndOfDocument();
+ } else
+ htmlEditor->EndOfDocument();
+ } else {
+ bool sigOnTopInserted = false;
+ if (isForwarded && sigOnTop && !aSignature.IsEmpty()) {
+ htmlEditor->InsertLineBreak();
+ InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns);
+ htmlEditor->EndOfDocument();
+ sigOnTopInserted = true;
+ }
+
+ if (!aBuf.IsEmpty()) {
+ nsresult rv;
+ RefPtr<Element> divElem;
+ RefPtr<Element> extraBr;
+
+ if (isForwarded) {
+ // Special treatment for forwarded messages: Part 1.
+ // Create a <div> of the required class.
+ rv = htmlEditor->CreateElementWithDefaults(u"div"_ns,
+ getter_AddRefs(divElem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString attributeName;
+ nsAutoString attributeValue;
+ attributeName.AssignLiteral("class");
+ attributeValue.AssignLiteral("moz-forward-container");
+ IgnoredErrorResult rv1;
+ divElem->SetAttribute(attributeName, attributeValue, rv1);
+
+ // We can't insert an empty <div>, so fill it with something.
+ rv = htmlEditor->CreateElementWithDefaults(u"br"_ns,
+ getter_AddRefs(extraBr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ErrorResult rv2;
+ divElem->AppendChild(*extraBr, rv2);
+ if (rv2.Failed()) {
+ return rv2.StealNSResult();
+ }
+
+ // Insert the non-empty <div> into the DOM.
+ rv = htmlEditor->InsertElementAtSelection(divElem, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Position into the div, so out content goes there.
+ RefPtr<Selection> selection;
+ htmlEditor->GetSelection(getter_AddRefs(selection));
+ rv = selection->CollapseInLimiter(divElem, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = htmlEditor->InsertTextWithQuotations(aBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isForwarded) {
+ // Special treatment for forwarded messages: Part 2.
+ if (sigOnTopInserted) {
+ // Sadly the M-C editor inserts a <br> between the <div> for the
+ // signature and this <div>, so remove the <br> we don't want.
+ nsCOMPtr<nsINode> brBeforeDiv;
+ nsAutoString tagLocalName;
+ brBeforeDiv = divElem->GetPreviousSibling();
+ if (brBeforeDiv) {
+ tagLocalName = brBeforeDiv->LocalName();
+ if (tagLocalName.EqualsLiteral("br")) {
+ rv = htmlEditor->DeleteNode(brBeforeDiv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ // Clean up the <br> we inserted.
+ rv = htmlEditor->DeleteNode(extraBr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Use our own function instead of nsEditor::EndOfDocument() because
+ // we don't want to position at the end of the div we've just created.
+ // It's OK to use, even if we're not forwarding and didn't create a
+ // <div>.
+ rv = MoveToEndOfDocument();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if ((!isForwarded || !sigOnTop) && !aSignature.IsEmpty()) {
+ htmlEditor->InsertLineBreak();
+ InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns);
+ }
+ }
+ }
+
+ if (aBuf.IsEmpty())
+ htmlEditor->BeginningOfDocument();
+ else {
+ switch (reply_on_top) {
+ // This should set the cursor after the body but before the sig
+ case 0: {
+ if (!htmlEditor) {
+ htmlEditor->BeginningOfDocument();
+ break;
+ }
+
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsINode> parent;
+ int32_t offset;
+ nsresult rv;
+
+ // get parent and offset of mailcite
+ rv = GetNodeLocation(nodeInserted, address_of(parent), &offset);
+ if (NS_FAILED(rv) || (!parent)) {
+ htmlEditor->BeginningOfDocument();
+ break;
+ }
+
+ // get selection
+ htmlEditor->GetSelection(getter_AddRefs(selection));
+ if (!selection) {
+ htmlEditor->BeginningOfDocument();
+ break;
+ }
+
+ // place selection after mailcite
+ selection->CollapseInLimiter(parent, offset + 1);
+
+ // insert a break at current selection
+ if (!paragraphMode || !aHTMLEditor) htmlEditor->InsertLineBreak();
+
+ // i'm not sure if you need to move the selection back to before the
+ // break. expirement.
+ selection->CollapseInLimiter(parent, offset + 1);
+
+ break;
+ }
+
+ case 2: {
+ nsCOMPtr<nsIEditor> editor(htmlEditor); // Strong reference.
+ editor->SelectAll();
+ break;
+ }
+
+ // This should set the cursor to the top!
+ default: {
+ MoveToBeginningOfDocument();
+ break;
+ }
+ }
+ }
+
+ nsCOMPtr<nsISelectionController> selCon;
+ htmlEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (selCon)
+ selCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_ANCHOR_REGION, true);
+
+ htmlEditor->EnableUndo(true);
+ SetBodyModified(false);
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ nsCOMPtr<nsIMsgComposeService> composeService(
+ do_GetService("@mozilla.org/messengercompose;1"));
+ composeService->TimeStamp(
+ "Finished inserting data into the editor. The window is finally ready!",
+ false);
+#endif
+ return NS_OK;
+}
+
+/**
+ * Check the identity pref to include signature on replies and forwards.
+ */
+bool nsMsgCompose::CheckIncludeSignaturePrefs(nsIMsgIdentity* identity) {
+ bool includeSignature = true;
+ switch (mType) {
+ case nsIMsgCompType::ForwardInline:
+ case nsIMsgCompType::ForwardAsAttachment:
+ identity->GetSigOnForward(&includeSignature);
+ break;
+ case nsIMsgCompType::Reply:
+ case nsIMsgCompType::ReplyAll:
+ case nsIMsgCompType::ReplyToList:
+ case nsIMsgCompType::ReplyToGroup:
+ case nsIMsgCompType::ReplyToSender:
+ case nsIMsgCompType::ReplyToSenderAndGroup:
+ identity->GetSigOnReply(&includeSignature);
+ break;
+ }
+ return includeSignature;
+}
+
+nsresult nsMsgCompose::SetQuotingToFollow(bool aVal) {
+ mQuotingToFollow = aVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::GetQuotingToFollow(bool* quotingToFollow) {
+ NS_ENSURE_ARG(quotingToFollow);
+ *quotingToFollow = mQuotingToFollow;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::Initialize(nsIMsgComposeParams* aParams,
+ mozIDOMWindowProxy* aWindow, nsIDocShell* aDocShell) {
+ NS_ENSURE_ARG_POINTER(aParams);
+ nsresult rv;
+
+ aParams->GetIdentity(getter_AddRefs(m_identity));
+
+ if (aWindow) {
+ m_window = aWindow;
+ nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow);
+ NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDocShellTreeItem> treeItem = window->GetDocShell();
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ rv = treeItem->GetTreeOwner(getter_AddRefs(treeOwner));
+ if (NS_FAILED(rv)) return rv;
+
+ m_baseWindow = do_QueryInterface(treeOwner);
+ }
+
+ aParams->GetAutodetectCharset(&mAutodetectCharset);
+
+ MSG_ComposeFormat format;
+ aParams->GetFormat(&format);
+
+ MSG_ComposeType type;
+ aParams->GetType(&type);
+
+ nsCString originalMsgURI;
+ aParams->GetOriginalMsgURI(originalMsgURI);
+ aParams->GetOrigMsgHdr(getter_AddRefs(mOrigMsgHdr));
+
+ nsCOMPtr<nsIMsgCompFields> composeFields;
+ aParams->GetComposeFields(getter_AddRefs(composeFields));
+
+ nsCOMPtr<nsIMsgComposeService> composeService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = composeService->DetermineComposeHTML(m_identity, format, &m_composeHTML);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifndef MOZ_SUITE
+ if (m_composeHTML) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TB_COMPOSE_FORMAT_HTML, 1);
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TB_COMPOSE_FORMAT_PLAIN_TEXT, 1);
+ }
+ Telemetry::Accumulate(Telemetry::TB_COMPOSE_TYPE, type);
+#endif
+
+ if (composeFields) {
+ nsAutoCString draftId; // will get set for drafts and templates
+ rv = composeFields->GetDraftId(draftId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set return receipt flag and type, and if we should attach a vCard
+ // by checking the identity prefs - but don't clobber the values for
+ // drafts and templates as they were set up already by mime when
+ // initializing the message.
+ if (m_identity && draftId.IsEmpty() && type != nsIMsgCompType::Template) {
+ bool requestReturnReceipt = false;
+ rv = m_identity->GetRequestReturnReceipt(&requestReturnReceipt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = composeFields->SetReturnReceipt(requestReturnReceipt);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t receiptType = nsIMsgMdnGenerator::eDntType;
+ rv = m_identity->GetReceiptHeaderType(&receiptType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = composeFields->SetReceiptHeaderType(receiptType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool requestDSN = false;
+ rv = m_identity->GetRequestDSN(&requestDSN);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = composeFields->SetDSN(requestDSN);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool attachVCard;
+ rv = m_identity->GetAttachVCard(&attachVCard);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = composeFields->SetAttachVCard(attachVCard);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+ aParams->GetSendListener(getter_AddRefs(externalSendListener));
+ if (externalSendListener) AddMsgSendListener(externalSendListener);
+
+ nsString smtpPassword;
+ aParams->GetSmtpPassword(smtpPassword);
+ mSmtpPassword = smtpPassword;
+
+ aParams->GetHtmlToQuote(mHtmlToQuote);
+
+ if (aDocShell) {
+ mDocShell = aDocShell;
+ // register the compose object with the compose service
+ rv = composeService->RegisterComposeDocShell(aDocShell, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return CreateMessage(originalMsgURI, type, composeFields);
+}
+
+NS_IMETHODIMP
+nsMsgCompose::RegisterStateListener(
+ nsIMsgComposeStateListener* aStateListener) {
+ NS_ENSURE_ARG_POINTER(aStateListener);
+ mStateListeners.AppendElement(aStateListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::UnregisterStateListener(
+ nsIMsgComposeStateListener* aStateListener) {
+ NS_ENSURE_ARG_POINTER(aStateListener);
+ return mStateListeners.RemoveElement(aStateListener) ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+// Added to allow easier use of the nsIMsgSendListener
+NS_IMETHODIMP nsMsgCompose::AddMsgSendListener(
+ nsIMsgSendListener* aMsgSendListener) {
+ NS_ENSURE_ARG_POINTER(aMsgSendListener);
+ mExternalSendListeners.AppendElement(aMsgSendListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::RemoveMsgSendListener(
+ nsIMsgSendListener* aMsgSendListener) {
+ NS_ENSURE_ARG_POINTER(aMsgSendListener);
+ return mExternalSendListeners.RemoveElement(aMsgSendListener)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::SendMsgToServer(MSG_DeliverMode deliverMode,
+ nsIMsgIdentity* identity, const char* accountKey,
+ Promise** aPromise) {
+ nsresult rv = NS_OK;
+
+ // clear saved message id if sending, so we don't send out the same
+ // message-id.
+ if (deliverMode == nsIMsgCompDeliverMode::Now ||
+ deliverMode == nsIMsgCompDeliverMode::Later ||
+ deliverMode == nsIMsgCompDeliverMode::Background)
+ m_compFields->SetMessageId("");
+
+ if (m_compFields && identity) {
+ // Pref values are supposed to be stored as UTF-8, so no conversion
+ nsCString email;
+ nsString fullName;
+ nsString organization;
+
+ identity->GetEmail(email);
+ identity->GetFullName(fullName);
+ identity->GetOrganization(organization);
+
+ const char* pFrom = m_compFields->GetFrom();
+ if (!pFrom || !*pFrom) {
+ nsCString sender;
+ MakeMimeAddress(NS_ConvertUTF16toUTF8(fullName), email, sender);
+ m_compFields->SetFrom(sender.IsEmpty() ? email.get() : sender.get());
+ }
+
+ m_compFields->SetOrganization(organization);
+
+ // We need an nsIMsgSend instance to send the message. Allow extensions
+ // to override the default SMTP sender by observing mail-set-sender.
+ mMsgSend = nullptr;
+ mDeliverMode = deliverMode; // save for possible access by observer.
+
+ // Allow extensions to specify an outgoing server.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(observerService);
+
+ // Assemble a string with sending parameters.
+ nsAutoString sendParms;
+
+ // First parameter: account key. This may be null.
+ sendParms.AppendASCII(accountKey && *accountKey ? accountKey : "");
+ sendParms.Append(',');
+
+ // Second parameter: deliverMode.
+ sendParms.AppendInt(deliverMode);
+ sendParms.Append(',');
+
+ // Third parameter: identity (as identity key).
+ nsAutoCString identityKey;
+ identity->GetKey(identityKey);
+ sendParms.AppendASCII(identityKey.get());
+
+ observerService->NotifyObservers(NS_ISUPPORTS_CAST(nsIMsgCompose*, this),
+ "mail-set-sender", sendParms.get());
+
+ if (!mMsgSend)
+ mMsgSend = do_CreateInstance("@mozilla.org/messengercompose/send;1");
+
+ if (mMsgSend) {
+ nsString bodyString;
+ rv = m_compFields->GetBody(bodyString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the listener for the send operation...
+ nsCOMPtr<nsIMsgComposeSendListener> composeSendListener =
+ do_CreateInstance(
+ "@mozilla.org/messengercompose/composesendlistener;1");
+ if (!composeSendListener) return NS_ERROR_OUT_OF_MEMORY;
+
+ // right now, AutoSaveAsDraft is identical to SaveAsDraft as
+ // far as the msg send code is concerned. This way, we don't have
+ // to add an nsMsgDeliverMode for autosaveasdraft, and add cases for
+ // it in the msg send code.
+ if (deliverMode == nsIMsgCompDeliverMode::AutoSaveAsDraft)
+ deliverMode = nsIMsgCompDeliverMode::SaveAsDraft;
+
+ RefPtr<nsIMsgCompose> msgCompose(this);
+ composeSendListener->SetMsgCompose(msgCompose);
+ composeSendListener->SetDeliverMode(deliverMode);
+
+ if (mProgress) {
+ nsCOMPtr<nsIWebProgressListener> progressListener =
+ do_QueryInterface(composeSendListener);
+ mProgress->RegisterListener(progressListener);
+ }
+
+ // If we are composing HTML, then this should be sent as
+ // multipart/related which means we pass the editor into the
+ // backend...if not, just pass nullptr
+ //
+ nsCOMPtr<nsIMsgSendListener> sendListener =
+ do_QueryInterface(composeSendListener);
+ RefPtr<mozilla::dom::Promise> promise;
+ rv = mMsgSend->CreateAndSendMessage(
+ m_composeHTML ? m_editor.get() : nullptr, identity, accountKey,
+ m_compFields, false, false, (nsMsgDeliverMode)deliverMode, nullptr,
+ m_composeHTML ? TEXT_HTML : TEXT_PLAIN, bodyString, m_window,
+ mProgress, sendListener, mSmtpPassword, mOriginalMsgURI, mType,
+ getter_AddRefs(promise));
+ promise.forget(aPromise);
+ } else
+ rv = NS_ERROR_FAILURE;
+ } else
+ rv = NS_ERROR_NOT_INITIALIZED;
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgCompose::SendMsg(MSG_DeliverMode deliverMode,
+ nsIMsgIdentity* identity,
+ const char* accountKey,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgProgress* progress,
+ Promise** aPromise) {
+ NS_ENSURE_TRUE(m_compFields, NS_ERROR_NOT_INITIALIZED);
+ nsresult rv = NS_OK;
+
+ // Set content type based on which type of compose window we had.
+ nsString contentType = (m_composeHTML) ? u"text/html"_ns : u"text/plain"_ns;
+ nsString msgBody;
+ if (m_editor) {
+ // Reset message body previously stored in the compose fields
+ m_compFields->SetBody(EmptyString());
+
+ uint32_t flags = nsIDocumentEncoder::OutputCRLineBreak |
+ nsIDocumentEncoder::OutputLFLineBreak;
+
+ if (m_composeHTML) {
+ flags |= nsIDocumentEncoder::OutputFormatted |
+ nsIDocumentEncoder::OutputDisallowLineBreaking;
+ } else {
+ bool flowed, formatted;
+ GetSerialiserFlags(&flowed, &formatted);
+ if (flowed) flags |= nsIDocumentEncoder::OutputFormatFlowed;
+ if (formatted) flags |= nsIDocumentEncoder::OutputFormatted;
+ flags |= nsIDocumentEncoder::OutputDisallowLineBreaking;
+ // Don't lose NBSP in the plain text encoder.
+ flags |= nsIDocumentEncoder::OutputPersistNBSP;
+ }
+ nsresult rv = m_editor->OutputToString(contentType, flags, msgBody);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ m_compFields->GetBody(msgBody);
+ }
+ if (!msgBody.IsEmpty()) {
+ // Ensure body ends in CRLF to avoid SMTP server timeout when sent.
+ if (!StringEndsWith(msgBody, u"\r\n"_ns)) msgBody.AppendLiteral("\r\n");
+ bool isAsciiOnly = mozilla::IsAsciiNullTerminated(
+ static_cast<const char16_t*>(msgBody.get()));
+
+ if (m_compFields->GetForceMsgEncoding()) {
+ isAsciiOnly = false;
+ }
+
+ m_compFields->SetBodyIsAsciiOnly(isAsciiOnly);
+ m_compFields->SetBody(msgBody);
+ }
+
+ // Let's open the progress dialog
+ if (progress) {
+ mProgress = progress;
+
+ if (deliverMode != nsIMsgCompDeliverMode::AutoSaveAsDraft) {
+ nsAutoString msgSubject;
+ m_compFields->GetSubject(msgSubject);
+
+ bool showProgress = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch) {
+ prefBranch->GetBoolPref("mailnews.show_send_progress", &showProgress);
+ if (showProgress) {
+ nsCOMPtr<nsIMsgComposeProgressParams> params = do_CreateInstance(
+ "@mozilla.org/messengercompose/composeprogressparameters;1", &rv);
+ if (NS_FAILED(rv) || !params) return NS_ERROR_FAILURE;
+
+ params->SetSubject(msgSubject.get());
+ params->SetDeliveryMode(deliverMode);
+
+ mProgress->OpenProgressDialog(
+ m_window, aMsgWindow,
+ "chrome://messenger/content/messengercompose/sendProgress.xhtml",
+ false, params);
+ }
+ }
+ }
+
+ mProgress->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_START, NS_OK);
+ }
+
+ bool attachVCard = false;
+ m_compFields->GetAttachVCard(&attachVCard);
+
+ if (attachVCard && identity &&
+ (deliverMode == nsIMsgCompDeliverMode::Now ||
+ deliverMode == nsIMsgCompDeliverMode::Later ||
+ deliverMode == nsIMsgCompDeliverMode::Background)) {
+ nsCString escapedVCard;
+ // make sure, if there is no card, this returns an empty string, or
+ // NS_ERROR_FAILURE
+ rv = identity->GetEscapedVCard(escapedVCard);
+
+ if (NS_SUCCEEDED(rv) && !escapedVCard.IsEmpty()) {
+ nsCString vCardUrl;
+ vCardUrl = "data:text/vcard;charset=utf-8;base64,";
+ nsCString unescapedData;
+ MsgUnescapeString(escapedVCard, 0, unescapedData);
+ char* result = PL_Base64Encode(unescapedData.get(), 0, nullptr);
+ vCardUrl += result;
+ PR_Free(result);
+
+ nsCOMPtr<nsIMsgAttachment> attachment =
+ do_CreateInstance("@mozilla.org/messengercompose/attachment;1", &rv);
+ if (NS_SUCCEEDED(rv) && attachment) {
+ // [comment from 4.x]
+ // Send the vCard out with a filename which distinguishes this user.
+ // e.g. jsmith.vcf The main reason to do this is for interop with
+ // Eudora, which saves off the attachments separately from the message
+ // body
+ nsCString userid;
+ (void)identity->GetEmail(userid);
+ int32_t index = userid.FindChar('@');
+ if (index != kNotFound) userid.SetLength(index);
+
+ if (userid.IsEmpty())
+ attachment->SetName(u"vcard.vcf"_ns);
+ else {
+ // Replace any dot with underscore to stop vCards
+ // generating false positives with some heuristic scanners
+ userid.ReplaceChar('.', '_');
+ userid.AppendLiteral(".vcf");
+ attachment->SetName(NS_ConvertASCIItoUTF16(userid));
+ }
+
+ attachment->SetUrl(vCardUrl);
+ m_compFields->AddAttachment(attachment);
+ }
+ }
+ }
+
+ // Save the identity being sent for later use.
+ m_identity = identity;
+
+ RefPtr<mozilla::dom::Promise> promise;
+ rv = SendMsgToServer(deliverMode, identity, accountKey,
+ getter_AddRefs(promise));
+
+ RefPtr<nsMsgCompose> self = this;
+ auto handleFailure = [self = std::move(self), deliverMode](nsresult rv) {
+ self->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, rv);
+ nsCOMPtr<nsIMsgSendReport> sendReport;
+ if (self->mMsgSend)
+ self->mMsgSend->GetSendReport(getter_AddRefs(sendReport));
+ if (sendReport) {
+ nsresult theError;
+ sendReport->DisplayReport(self->m_window, true, true, &theError);
+ } else {
+ // If we come here it's because we got an error before we could initialize
+ // a send report! Let's try our best...
+ switch (deliverMode) {
+ case nsIMsgCompDeliverMode::Later:
+ nsMsgDisplayMessageByName(self->m_window, "unableToSendLater");
+ break;
+ case nsIMsgCompDeliverMode::AutoSaveAsDraft:
+ case nsIMsgCompDeliverMode::SaveAsDraft:
+ nsMsgDisplayMessageByName(self->m_window, "unableToSaveDraft");
+ break;
+ case nsIMsgCompDeliverMode::SaveAsTemplate:
+ nsMsgDisplayMessageByName(self->m_window, "unableToSaveTemplate");
+ break;
+
+ default:
+ nsMsgDisplayMessageByName(self->m_window, "sendFailed");
+ break;
+ }
+ }
+ if (self->mProgress) self->mProgress->CloseProgressDialog(true);
+
+ self->DeleteTmpAttachments();
+ };
+ if (promise) {
+ RefPtr<DomPromiseListener> listener = new DomPromiseListener(
+ [&](JSContext*, JS::Handle<JS::Value>) { DeleteTmpAttachments(); },
+ handleFailure);
+ promise->AppendNativeHandler(listener);
+ promise.forget(aPromise);
+ } else if (NS_FAILED(rv)) {
+ handleFailure(rv);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetDeleteDraft(bool* aDeleteDraft) {
+ NS_ENSURE_ARG_POINTER(aDeleteDraft);
+ *aDeleteDraft = mDeleteDraft;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::SetDeleteDraft(bool aDeleteDraft) {
+ mDeleteDraft = aDeleteDraft;
+ return NS_OK;
+}
+
+bool nsMsgCompose::IsLastWindow() {
+ nsresult rv;
+ bool more;
+ nsCOMPtr<nsIWindowMediator> windowMediator =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsISimpleEnumerator> windowEnumerator;
+ rv = windowMediator->GetEnumerator(nullptr,
+ getter_AddRefs(windowEnumerator));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsISupports> isupports;
+
+ if (NS_SUCCEEDED(windowEnumerator->GetNext(getter_AddRefs(isupports))))
+ if (NS_SUCCEEDED(windowEnumerator->HasMoreElements(&more)))
+ return !more;
+ }
+ }
+ return true;
+}
+
+NS_IMETHODIMP nsMsgCompose::CloseWindow(void) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgComposeService> composeService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // unregister the compose object with the compose service
+ rv = composeService->UnregisterComposeDocShell(mDocShell);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mDocShell = nullptr;
+
+ // ensure that the destructor of nsMsgSend is invoked to remove
+ // temporary files.
+ mMsgSend = nullptr;
+
+ // We are going away for real, we need to do some clean up first
+ if (m_baseWindow) {
+ if (m_editor) {
+ // The editor will be destroyed during the close window.
+ // Set it to null to be sure we won't use it anymore.
+ m_editor = nullptr;
+ }
+ nsCOMPtr<nsIBaseWindow> window = m_baseWindow.forget();
+ rv = window->Destroy();
+ }
+
+ m_window = nullptr;
+ return rv;
+}
+
+nsresult nsMsgCompose::Abort() {
+ if (mMsgSend) mMsgSend->Abort();
+
+ if (mProgress) mProgress->CloseProgressDialog(true);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetEditor(nsIEditor** aEditor) {
+ NS_IF_ADDREF(*aEditor = m_editor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::SetEditor(nsIEditor* aEditor) {
+ m_editor = aEditor;
+ return NS_OK;
+}
+
+// This used to be called BEFORE editor was created
+// (it did the loadURI that triggered editor creation)
+// It is called from JS after editor creation
+// (loadURI is done in JS)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsMsgCompose::InitEditor(
+ nsIEditor* aEditor, mozIDOMWindowProxy* aContentWindow) {
+ NS_ENSURE_ARG_POINTER(aEditor);
+ NS_ENSURE_ARG_POINTER(aContentWindow);
+ nsresult rv;
+
+ m_editor = aEditor;
+
+ aEditor->SetDocumentCharacterSet("UTF-8"_ns);
+
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ nsPIDOMWindowOuter::From(aContentWindow);
+
+ nsIDocShell* docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_UNEXPECTED);
+
+ bool quotingToFollow = false;
+ GetQuotingToFollow(&quotingToFollow);
+ if (quotingToFollow)
+ return BuildQuotedMessageAndSignature();
+ else {
+ NotifyStateListeners(nsIMsgComposeNotificationType::ComposeFieldsReady,
+ NS_OK);
+ rv = BuildBodyMessageAndSignature();
+ NotifyStateListeners(nsIMsgComposeNotificationType::ComposeBodyReady,
+ NS_OK);
+ return rv;
+ }
+}
+
+nsresult nsMsgCompose::GetBodyModified(bool* modified) {
+ nsresult rv;
+
+ if (!modified) return NS_ERROR_NULL_POINTER;
+
+ *modified = true;
+
+ if (m_editor) {
+ rv = m_editor->GetDocumentModified(modified);
+ if (NS_FAILED(rv)) *modified = true;
+ }
+
+ return NS_OK;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+nsMsgCompose::SetBodyModified(bool modified) {
+ nsresult rv = NS_OK;
+
+ if (m_editor) {
+ nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference.
+ if (modified) {
+ int32_t modCount = 0;
+ editor->GetModificationCount(&modCount);
+ if (modCount == 0) editor->IncrementModificationCount(1);
+ } else
+ editor->ResetModificationCount();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::GetDomWindow(mozIDOMWindowProxy** aDomWindow) {
+ NS_IF_ADDREF(*aDomWindow = m_window);
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::GetCompFields(nsIMsgCompFields** aCompFields) {
+ NS_IF_ADDREF(*aCompFields = (nsIMsgCompFields*)m_compFields);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetComposeHTML(bool* aComposeHTML) {
+ *aComposeHTML = m_composeHTML;
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::GetWrapLength(int32_t* aWrapLength) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ return prefBranch->GetIntPref("mailnews.wraplength", aWrapLength);
+}
+
+nsresult nsMsgCompose::CreateMessage(const nsACString& originalMsgURI,
+ MSG_ComposeType type,
+ nsIMsgCompFields* compFields) {
+ nsresult rv = NS_OK;
+ mType = type;
+ mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_None;
+
+ mDeleteDraft = (type == nsIMsgCompType::Draft);
+ nsAutoCString msgUri(originalMsgURI);
+ bool fileUrl = StringBeginsWith(msgUri, "file:"_ns);
+ int32_t typeIndex = msgUri.Find("type=application/x-message-display");
+ if (typeIndex != kNotFound && typeIndex > 0) {
+ // Strip out type=application/x-message-display because it confuses libmime.
+ msgUri.Cut(typeIndex, sizeof("type=application/x-message-display"));
+ if (fileUrl) // we're dealing with an .eml file msg
+ {
+ // We have now removed the type from the uri. Make sure we don't have
+ // an uri with "&&" now. If we do, remove the second '&'.
+ if (msgUri.CharAt(typeIndex) == '&') msgUri.Cut(typeIndex, 1);
+ // Remove possible trailing '?'.
+ if (msgUri.CharAt(msgUri.Length() - 1) == '?')
+ msgUri.Cut(msgUri.Length() - 1, 1);
+ } else // we're dealing with a message/rfc822 attachment
+ {
+ // nsURLFetcher will check for "realtype=message/rfc822" and will set the
+ // content type to message/rfc822 in the forwarded message.
+ msgUri.AppendLiteral("&realtype=message/rfc822");
+ }
+ }
+
+ if (compFields) {
+ m_compFields = reinterpret_cast<nsMsgCompFields*>(compFields);
+ } else {
+ m_compFields = new nsMsgCompFields();
+ }
+
+ if (m_identity && mType != nsIMsgCompType::Draft) {
+ // Setup reply-to field.
+ nsCString replyTo;
+ m_identity->GetReplyTo(replyTo);
+ if (!replyTo.IsEmpty()) {
+ nsCString resultStr;
+ RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetReplyTo()),
+ replyTo, resultStr);
+ if (!resultStr.IsEmpty()) {
+ replyTo.Append(',');
+ replyTo.Append(resultStr);
+ }
+ m_compFields->SetReplyTo(replyTo.get());
+ }
+
+ // Setup auto-Cc field.
+ bool doCc;
+ m_identity->GetDoCc(&doCc);
+ if (doCc) {
+ nsCString ccList;
+ m_identity->GetDoCcList(ccList);
+
+ nsCString resultStr;
+ RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetCc()),
+ ccList, resultStr);
+ if (!resultStr.IsEmpty()) {
+ ccList.Append(',');
+ ccList.Append(resultStr);
+ }
+ m_compFields->SetCc(ccList.get());
+ }
+
+ // Setup auto-Bcc field.
+ bool doBcc;
+ m_identity->GetDoBcc(&doBcc);
+ if (doBcc) {
+ nsCString bccList;
+ m_identity->GetDoBccList(bccList);
+
+ nsCString resultStr;
+ RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetBcc()),
+ bccList, resultStr);
+ if (!resultStr.IsEmpty()) {
+ bccList.Append(',');
+ bccList.Append(resultStr);
+ }
+ m_compFields->SetBcc(bccList.get());
+ }
+ }
+
+ if (mType == nsIMsgCompType::Draft) {
+ nsCString curDraftIdURL;
+ rv = m_compFields->GetDraftId(curDraftIdURL);
+ // Skip if no draft id (probably a new draft msg).
+ if (NS_SUCCEEDED(rv) && !curDraftIdURL.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> msgDBHdr;
+ rv = GetMsgDBHdrFromURI(curDraftIdURL, getter_AddRefs(msgDBHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "CreateMessage can't get msg header DB interface pointer.");
+ if (msgDBHdr) {
+ nsCString queuedDisposition;
+ msgDBHdr->GetStringProperty(QUEUED_DISPOSITION_PROPERTY,
+ queuedDisposition);
+ // We need to retrieve the original URI from the database so we can
+ // set the disposition flags correctly if the draft is a reply or
+ // forwarded message.
+ nsCString originalMsgURIfromDB;
+ msgDBHdr->GetStringProperty(ORIG_URI_PROPERTY, originalMsgURIfromDB);
+ mOriginalMsgURI = originalMsgURIfromDB;
+ if (!queuedDisposition.IsEmpty()) {
+ if (queuedDisposition.EqualsLiteral("replied"))
+ mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Replied;
+ else if (queuedDisposition.EqualsLiteral("forward"))
+ mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Forwarded;
+ else if (queuedDisposition.EqualsLiteral("redirected"))
+ mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Redirected;
+ }
+ }
+ } else {
+ NS_WARNING("CreateMessage can't get draft id");
+ }
+ }
+
+ // If we don't have an original message URI, nothing else to do...
+ if (msgUri.IsEmpty()) return NS_OK;
+
+ // store the original message URI so we can extract it after we send the
+ // message to properly mark any disposition flags like replied or forwarded on
+ // the message.
+ if (mOriginalMsgURI.IsEmpty()) mOriginalMsgURI = msgUri;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // "Forward inline" and "Reply with template" processing.
+ // Note the early return at the end of the block.
+ if (type == nsIMsgCompType::ForwardInline ||
+ type == nsIMsgCompType::ReplyWithTemplate) {
+ // We want to treat this message as a reference too
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(msgUri, getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+
+ nsAutoCString reference;
+ // When forwarding we only use the original message for "References:" -
+ // recipients don't have the other messages anyway.
+ // For reply with template we want to preserve all the references.
+ if (type == nsIMsgCompType::ReplyWithTemplate) {
+ uint16_t numReferences = 0;
+ msgHdr->GetNumReferences(&numReferences);
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString ref;
+ msgHdr->GetStringReference(i, ref);
+ if (!ref.IsEmpty()) {
+ reference.Append('<');
+ reference.Append(ref);
+ reference.AppendLiteral("> ");
+ }
+ }
+ reference.Trim(" ", false, true);
+ }
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ reference.Append('<');
+ reference.Append(messageId);
+ reference.Append('>');
+ m_compFields->SetReferences(reference.get());
+
+ if (type == nsIMsgCompType::ForwardInline) {
+ nsString subject;
+ msgHdr->GetMime2DecodedSubject(subject);
+ nsCString fwdPrefix;
+ prefs->GetCharPrefWithDefault("mail.forward_subject_prefix", "Fwd"_ns,
+ 1, fwdPrefix);
+ nsString unicodeFwdPrefix;
+ CopyUTF8toUTF16(fwdPrefix, unicodeFwdPrefix);
+ unicodeFwdPrefix.AppendLiteral(": ");
+ subject.Insert(unicodeFwdPrefix, 0);
+ m_compFields->SetSubject(subject);
+ }
+ }
+
+ // Early return for "ForwardInline" and "ReplyWithTemplate" processing.
+ return NS_OK;
+ }
+
+ // All other processing.
+
+ // Note the following:
+ // LoadDraftOrTemplate() is run in nsMsgComposeService::OpenComposeWindow()
+ // for five compose types: ForwardInline, ReplyWithTemplate (both covered
+ // in the code block above) and Draft, Template and Redirect. For these
+ // compose types, the charset is already correct (incl. MIME-applied override)
+ // unless the default charset should be used.
+
+ bool isFirstPass = true;
+ char* uriList = ToNewCString(msgUri);
+ char* uri = uriList;
+ char* nextUri;
+ do {
+ nextUri = strstr(uri, "://");
+ if (nextUri) {
+ // look for next ://, and then back up to previous ','
+ nextUri = strstr(nextUri + 1, "://");
+ if (nextUri) {
+ *nextUri = '\0';
+ char* saveNextUri = nextUri;
+ nextUri = strrchr(uri, ',');
+ if (nextUri) *nextUri = '\0';
+ *saveNextUri = ':';
+ }
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (mOrigMsgHdr)
+ msgHdr = mOrigMsgHdr;
+ else {
+ rv = GetMsgDBHdrFromURI(nsDependentCString(uri), getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (msgHdr) {
+ nsString subject;
+ rv = msgHdr->GetMime2DecodedSubject(subject);
+ if (NS_FAILED(rv)) return rv;
+
+ // Check if (was: is present in the subject
+ int32_t wasOffset = subject.RFind(u" (was:"_ns);
+ bool strip = true;
+
+ if (wasOffset >= 0) {
+ // Check the number of references, to check if was: should be stripped
+ // First, assume that it should be stripped; the variable will be set to
+ // false later if stripping should not happen.
+ uint16_t numRef;
+ msgHdr->GetNumReferences(&numRef);
+ if (numRef) {
+ // If there are references, look for the first message in the thread
+ // firstly, get the database via the folder
+ nsCOMPtr<nsIMsgFolder> folder;
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ if (folder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ folder->GetMsgDatabase(getter_AddRefs(db));
+
+ if (db) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(0, reference);
+
+ nsCOMPtr<nsIMsgDBHdr> refHdr;
+ db->GetMsgHdrForMessageID(reference.get(),
+ getter_AddRefs(refHdr));
+
+ if (refHdr) {
+ nsCString refSubject;
+ rv = refHdr->GetSubject(refSubject);
+ if (NS_SUCCEEDED(rv)) {
+ if (refSubject.Find(" (was:") >= 0) strip = false;
+ }
+ }
+ }
+ }
+ } else
+ strip = false;
+ }
+
+ if (strip && wasOffset >= 0) {
+ // Strip off the "(was: old subject)" part
+ subject.Assign(Substring(subject, 0, wasOffset));
+ }
+
+ switch (type) {
+ default:
+ break;
+ case nsIMsgCompType::Draft:
+ case nsIMsgCompType::Template:
+ case nsIMsgCompType::EditTemplate:
+ case nsIMsgCompType::EditAsNew: {
+ // If opening from file, preseve the subject already present, since
+ // we can't get a subject from db there.
+ if (mOriginalMsgURI.Find("&realtype=message/rfc822") != -1) {
+ break;
+ }
+ // Otherwise, set up the subject from db, with possible modifications.
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::HasRe) {
+ subject.InsertLiteral(u"Re: ", 0);
+ }
+ // Set subject from db, where it's already decrypted. The raw
+ // header may be encrypted.
+ m_compFields->SetSubject(subject);
+ break;
+ }
+ case nsIMsgCompType::Reply:
+ case nsIMsgCompType::ReplyAll:
+ case nsIMsgCompType::ReplyToList:
+ case nsIMsgCompType::ReplyToGroup:
+ case nsIMsgCompType::ReplyToSender:
+ case nsIMsgCompType::ReplyToSenderAndGroup: {
+ if (!isFirstPass) // safeguard, just in case...
+ {
+ PR_Free(uriList);
+ return rv;
+ }
+ mQuotingToFollow = true;
+
+ subject.InsertLiteral(u"Re: ", 0);
+ m_compFields->SetSubject(subject);
+
+ // Setup quoting callbacks for later...
+ mWhatHolder = 1;
+ break;
+ }
+ case nsIMsgCompType::ForwardAsAttachment: {
+ // Add the forwarded message in the references, first
+ nsAutoCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ if (isFirstPass) {
+ nsAutoCString reference;
+ reference.Append('<');
+ reference.Append(messageId);
+ reference.Append('>');
+ m_compFields->SetReferences(reference.get());
+ } else {
+ nsAutoCString references;
+ m_compFields->GetReferences(getter_Copies(references));
+ references.AppendLiteral(" <");
+ references.Append(messageId);
+ references.Append('>');
+ m_compFields->SetReferences(references.get());
+ }
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::HasRe)
+ subject.InsertLiteral(u"Re: ", 0);
+
+ // Setup quoting callbacks for later...
+ mQuotingToFollow =
+ false; // We don't need to quote the original message.
+ nsCOMPtr<nsIMsgAttachment> attachment = do_CreateInstance(
+ "@mozilla.org/messengercompose/attachment;1", &rv);
+ if (NS_SUCCEEDED(rv) && attachment) {
+ bool addExtension = true;
+ nsString sanitizedSubj;
+ prefs->GetBoolPref("mail.forward_add_extension", &addExtension);
+
+ // copy subject string to sanitizedSubj, use default if empty
+ if (subject.IsEmpty()) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> composeBundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/"
+ "composeMsgs.properties",
+ getter_AddRefs(composeBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ composeBundle->GetStringFromName("messageAttachmentSafeName",
+ sanitizedSubj);
+ } else
+ sanitizedSubj.Assign(subject);
+
+ // set the file size
+ uint32_t messageSize;
+ msgHdr->GetMessageSize(&messageSize);
+ attachment->SetSize(messageSize);
+
+ // change all '.' to '_' see bug #271211
+ sanitizedSubj.ReplaceChar(u".", u'_');
+ if (addExtension) sanitizedSubj.AppendLiteral(".eml");
+ attachment->SetName(sanitizedSubj);
+ attachment->SetUrl(nsDependentCString(uri));
+ m_compFields->AddAttachment(attachment);
+ }
+
+ if (isFirstPass) {
+ nsCString fwdPrefix;
+ prefs->GetCharPrefWithDefault("mail.forward_subject_prefix",
+ "Fwd"_ns, 1, fwdPrefix);
+ nsString unicodeFwdPrefix;
+ CopyUTF8toUTF16(fwdPrefix, unicodeFwdPrefix);
+ unicodeFwdPrefix.AppendLiteral(": ");
+ subject.Insert(unicodeFwdPrefix, 0);
+ m_compFields->SetSubject(subject);
+ }
+ break;
+ }
+ case nsIMsgCompType::Redirect: {
+ // For a redirect, set the Reply-To: header to what was in the
+ // original From: header...
+ nsAutoCString author;
+ msgHdr->GetAuthor(getter_Copies(author));
+ m_compFields->SetSubject(subject);
+ m_compFields->SetReplyTo(author.get());
+
+ // ... and empty out the various recipient headers
+ nsAutoString empty;
+ m_compFields->SetTo(empty);
+ m_compFields->SetCc(empty);
+ m_compFields->SetBcc(empty);
+ m_compFields->SetNewsgroups(empty);
+ m_compFields->SetFollowupTo(empty);
+
+ // Add the redirected message in the references so that threading
+ // will work when the new recipient eventually replies to the
+ // original sender.
+ nsAutoCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ if (isFirstPass) {
+ nsAutoCString reference;
+ reference.Append('<');
+ reference.Append(messageId);
+ reference.Append('>');
+ m_compFields->SetReferences(reference.get());
+ } else {
+ nsAutoCString references;
+ m_compFields->GetReferences(getter_Copies(references));
+ references.AppendLiteral(" <");
+ references.Append(messageId);
+ references.Append('>');
+ m_compFields->SetReferences(references.get());
+ }
+ break;
+ }
+ }
+ }
+ isFirstPass = false;
+ uri = nextUri + 1;
+ } while (nextUri);
+ PR_Free(uriList);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetProgress(nsIMsgProgress** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ NS_IF_ADDREF(*_retval = mProgress);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetMessageSend(nsIMsgSend** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ NS_IF_ADDREF(*_retval = mMsgSend);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::SetMessageSend(nsIMsgSend* aMsgSend) {
+ mMsgSend = aMsgSend;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::ClearMessageSend() {
+ mMsgSend = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::SetCiteReference(nsString citeReference) {
+ mCiteReference = citeReference;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::SetSavedFolderURI(const nsACString& folderURI) {
+ m_folderName = folderURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetSavedFolderURI(nsACString& folderURI) {
+ folderURI = m_folderName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetOriginalMsgURI(nsACString& originalMsgURI) {
+ originalMsgURI = mOriginalMsgURI;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// THIS IS THE CLASS THAT IS THE STREAM CONSUMER OF THE HTML OUTPUT
+// FROM LIBMIME. THIS IS FOR QUOTING
+////////////////////////////////////////////////////////////////////////////////////
+QuotingOutputStreamListener::~QuotingOutputStreamListener() {}
+
+QuotingOutputStreamListener::QuotingOutputStreamListener(
+ nsIMsgDBHdr* originalMsgHdr, bool quoteHeaders, bool headersOnly,
+ nsIMsgIdentity* identity, nsIMsgQuote* msgQuote, bool quoteOriginal,
+ const nsACString& htmlToQuote) {
+ nsresult rv;
+ mQuoteHeaders = quoteHeaders;
+ mHeadersOnly = headersOnly;
+ mIdentity = identity;
+ mOrigMsgHdr = originalMsgHdr;
+ mUnicodeBufferCharacterLength = 0;
+ mQuoteOriginal = quoteOriginal;
+ mHtmlToQuote = htmlToQuote;
+ mQuote = msgQuote;
+
+ if (!mHeadersOnly || !mHtmlToQuote.IsEmpty()) {
+ // Get header type, locale and strings from pref.
+ int32_t replyHeaderType;
+ nsString replyHeaderAuthorWrote;
+ nsString replyHeaderOnDateAuthorWrote;
+ nsString replyHeaderAuthorWroteOnDate;
+ nsString replyHeaderOriginalmessage;
+ GetReplyHeaderInfo(
+ &replyHeaderType, replyHeaderAuthorWrote, replyHeaderOnDateAuthorWrote,
+ replyHeaderAuthorWroteOnDate, replyHeaderOriginalmessage);
+
+ // For the built message body...
+ if (originalMsgHdr && !quoteHeaders) {
+ // Setup the cite information....
+ nsCString myGetter;
+ if (NS_SUCCEEDED(originalMsgHdr->GetMessageId(getter_Copies(myGetter)))) {
+ if (!myGetter.IsEmpty()) {
+ nsAutoCString buf;
+ mCiteReference.AssignLiteral("mid:");
+ MsgEscapeURL(myGetter,
+ nsINetUtil::ESCAPE_URL_FILE_BASENAME |
+ nsINetUtil::ESCAPE_URL_FORCED,
+ buf);
+ mCiteReference.Append(NS_ConvertASCIItoUTF16(buf));
+ }
+ }
+
+ bool citingHeader; // Do we have a header needing to cite any info from
+ // original message?
+ bool headerDate; // Do we have a header needing to cite date/time from
+ // original message?
+ switch (replyHeaderType) {
+ case 0: // No reply header at all (actually the "---- original message
+ // ----" string, which is kinda misleading. TODO: Should there
+ // be a "really no header" option?
+ mCitePrefix.Assign(replyHeaderOriginalmessage);
+ citingHeader = false;
+ headerDate = false;
+ break;
+
+ case 2: // Insert both the original author and date in the reply header
+ // (date followed by author)
+ mCitePrefix.Assign(replyHeaderOnDateAuthorWrote);
+ citingHeader = true;
+ headerDate = true;
+ break;
+
+ case 3: // Insert both the original author and date in the reply header
+ // (author followed by date)
+ mCitePrefix.Assign(replyHeaderAuthorWroteOnDate);
+ citingHeader = true;
+ headerDate = true;
+ break;
+
+ case 4: // TODO bug 107884: implement a more featureful user specified
+ // header
+ case 1:
+ default: // Default is to only show the author.
+ mCitePrefix.Assign(replyHeaderAuthorWrote);
+ citingHeader = true;
+ headerDate = false;
+ break;
+ }
+
+ if (citingHeader) {
+ int32_t placeholderIndex = kNotFound;
+
+ if (headerDate) {
+ PRTime originalMsgDate;
+ rv = originalMsgHdr->GetDate(&originalMsgDate);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString citeDatePart;
+ if ((placeholderIndex = mCitePrefix.Find(u"#2")) != kNotFound) {
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date =
+ mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(
+ style, originalMsgDate, citeDatePart);
+ if (NS_SUCCEEDED(rv))
+ mCitePrefix.Replace(placeholderIndex, 2, citeDatePart);
+ }
+ if ((placeholderIndex = mCitePrefix.Find(u"#3")) != kNotFound) {
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.time =
+ mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(
+ style, originalMsgDate, citeDatePart);
+ if (NS_SUCCEEDED(rv))
+ mCitePrefix.Replace(placeholderIndex, 2, citeDatePart);
+ }
+ }
+ }
+
+ if ((placeholderIndex = mCitePrefix.Find(u"#1")) != kNotFound) {
+ nsAutoCString author;
+ rv = originalMsgHdr->GetAuthor(getter_Copies(author));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString citeAuthor;
+ ExtractName(EncodedHeader(author), citeAuthor);
+ mCitePrefix.Replace(placeholderIndex, 2, citeAuthor);
+ }
+ }
+ }
+ }
+
+ // This should not happen, but just in case.
+ if (mCitePrefix.IsEmpty()) {
+ mCitePrefix.AppendLiteral("\n\n");
+ mCitePrefix.Append(replyHeaderOriginalmessage);
+ mCitePrefix.AppendLiteral("\n");
+ }
+ }
+}
+
+/**
+ * The formatflowed parameter directs if formatflowed should be used in the
+ * conversion. format=flowed (RFC 2646) is a way to represent flow in a plain
+ * text mail, without disturbing the plain text.
+ */
+nsresult QuotingOutputStreamListener::ConvertToPlainText(bool formatflowed,
+ bool formatted,
+ bool disallowBreaks) {
+ nsresult rv =
+ ConvertBufToPlainText(mMsgBody, formatflowed, formatted, disallowBreaks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ConvertBufToPlainText(mSignature, formatflowed, formatted,
+ disallowBreaks);
+}
+
+NS_IMETHODIMP QuotingOutputStreamListener::OnStartRequest(nsIRequest* request) {
+ return NS_OK;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+QuotingOutputStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ nsresult rv = NS_OK;
+
+ if (!mHtmlToQuote.IsEmpty()) {
+ // If we had a selection in the original message to quote, we can add
+ // it now that we are done ignoring the original body of the message
+ mHeadersOnly = false;
+ rv = AppendToMsgBody(mHtmlToQuote);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgCompose> compose = do_QueryReferent(mWeakComposeObj);
+ NS_ENSURE_TRUE(compose, NS_ERROR_NULL_POINTER);
+
+ MSG_ComposeType type;
+ compose->GetType(&type);
+
+ // Assign cite information if available...
+ if (!mCiteReference.IsEmpty()) compose->SetCiteReference(mCiteReference);
+
+ bool overrideReplyTo =
+ mozilla::Preferences::GetBool("mail.override_list_reply_to", true);
+
+ if (mHeaders &&
+ (type == nsIMsgCompType::Reply || type == nsIMsgCompType::ReplyAll ||
+ type == nsIMsgCompType::ReplyToList ||
+ type == nsIMsgCompType::ReplyToSender ||
+ type == nsIMsgCompType::ReplyToGroup ||
+ type == nsIMsgCompType::ReplyToSenderAndGroup) &&
+ mQuoteOriginal) {
+ nsCOMPtr<nsIMsgCompFields> compFields;
+ compose->GetCompFields(getter_AddRefs(compFields));
+ if (compFields) {
+ nsAutoString from;
+ nsAutoString to;
+ nsAutoString cc;
+ nsAutoString bcc;
+ nsAutoString replyTo;
+ nsAutoString mailReplyTo;
+ nsAutoString mailFollowupTo;
+ nsAutoString newgroups;
+ nsAutoString followUpTo;
+ nsAutoString messageId;
+ nsAutoString references;
+ nsAutoString listPost;
+
+ nsCString outCString; // Temp helper string.
+
+ bool needToRemoveDup = false;
+ if (!mMimeConverter) {
+ mMimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCString charset("UTF-8");
+
+ mHeaders->ExtractHeader(HEADER_FROM, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, from);
+
+ mHeaders->ExtractHeader(HEADER_TO, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, to);
+
+ mHeaders->ExtractHeader(HEADER_CC, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, cc);
+
+ mHeaders->ExtractHeader(HEADER_BCC, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, bcc);
+
+ mHeaders->ExtractHeader(HEADER_MAIL_FOLLOWUP_TO, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, mailFollowupTo);
+
+ mHeaders->ExtractHeader(HEADER_REPLY_TO, false, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, replyTo);
+
+ mHeaders->ExtractHeader(HEADER_MAIL_REPLY_TO, true, outCString);
+ nsMsgI18NConvertRawBytesToUTF16(outCString, charset, mailReplyTo);
+
+ mHeaders->ExtractHeader(HEADER_NEWSGROUPS, false, outCString);
+ if (!outCString.IsEmpty())
+ mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false,
+ true, newgroups);
+
+ mHeaders->ExtractHeader(HEADER_FOLLOWUP_TO, false, outCString);
+ if (!outCString.IsEmpty())
+ mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false,
+ true, followUpTo);
+
+ mHeaders->ExtractHeader(HEADER_MESSAGE_ID, false, outCString);
+ if (!outCString.IsEmpty())
+ mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false,
+ true, messageId);
+
+ mHeaders->ExtractHeader(HEADER_REFERENCES, false, outCString);
+ if (!outCString.IsEmpty())
+ mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false,
+ true, references);
+
+ mHeaders->ExtractHeader(HEADER_LIST_POST, true, outCString);
+ if (!outCString.IsEmpty())
+ mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false,
+ true, listPost);
+ if (!listPost.IsEmpty()) {
+ int32_t startPos = listPost.Find(u"<mailto:");
+ int32_t endPos = listPost.FindChar('>', startPos);
+ // Extract the e-mail address.
+ if (endPos > startPos) {
+ const uint32_t mailtoLen = strlen("<mailto:");
+ listPost = Substring(listPost, startPos + mailtoLen,
+ endPos - (startPos + mailtoLen));
+ }
+ }
+
+ nsCString fromEmailAddress;
+ ExtractEmail(EncodedHeaderW(from), fromEmailAddress);
+
+ nsTArray<nsCString> toEmailAddresses;
+ ExtractEmails(EncodedHeaderW(to), UTF16ArrayAdapter<>(toEmailAddresses));
+
+ nsTArray<nsCString> ccEmailAddresses;
+ ExtractEmails(EncodedHeaderW(cc), UTF16ArrayAdapter<>(ccEmailAddresses));
+
+ nsCOMPtr<nsIPrefBranch> prefs(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool replyToSelfCheckAll = false;
+ prefs->GetBoolPref("mailnews.reply_to_self_check_all_ident",
+ &replyToSelfCheckAll);
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ nsCString accountKey;
+ mOrigMsgHdr->GetAccountKey(getter_Copies(accountKey));
+ if (replyToSelfCheckAll) {
+ // Check all available identities if the pref was set.
+ accountManager->GetAllIdentities(identities);
+ } else if (!accountKey.IsEmpty()) {
+ // Check headers to see which account the message came in from
+ // (only works for pop3).
+ nsCOMPtr<nsIMsgAccount> account;
+ accountManager->GetAccount(accountKey, getter_AddRefs(account));
+ if (account) {
+ rv = account->GetIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Check identities only for the server of the folder that the message
+ // is in.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = mOrigMsgHdr->GetFolder(getter_AddRefs(msgFolder));
+
+ if (NS_SUCCEEDED(rv) && msgFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> nsIMsgIncomingServer;
+ rv = msgFolder->GetServer(getter_AddRefs(nsIMsgIncomingServer));
+
+ if (NS_SUCCEEDED(rv) && nsIMsgIncomingServer) {
+ rv = accountManager->GetIdentitiesForServer(nsIMsgIncomingServer,
+ identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ bool isReplyToSelf = false;
+ nsCOMPtr<nsIMsgIdentity> selfIdentity;
+ if (!identities.IsEmpty()) {
+ nsTArray<nsCString> toEmailAddressesLower(toEmailAddresses.Length());
+ for (auto email : toEmailAddresses) {
+ ToLowerCase(email);
+ toEmailAddressesLower.AppendElement(email);
+ }
+ nsTArray<nsCString> ccEmailAddressesLower(ccEmailAddresses.Length());
+ for (auto email : ccEmailAddresses) {
+ ToLowerCase(email);
+ ccEmailAddressesLower.AppendElement(email);
+ }
+
+ // Go through the identities to see if any of them is the author of
+ // the email.
+ for (auto lookupIdentity : identities) {
+ selfIdentity = lookupIdentity;
+
+ nsCString curIdentityEmail;
+ lookupIdentity->GetEmail(curIdentityEmail);
+
+ // See if it's a reply to own message, but not a reply between
+ // identities.
+ if (curIdentityEmail.Equals(fromEmailAddress,
+ nsCaseInsensitiveCStringComparator)) {
+ isReplyToSelf = true;
+ // For a true reply-to-self, none of your identities are normally in
+ // To or Cc. We need to avoid doing a reply-to-self for people that
+ // have multiple identities set and sometimes *uses* the other
+ // identity and sometimes *mails* the other identity.
+ // E.g. husband+wife or own-email+company-role-mail.
+ for (auto lookupIdentity2 : identities) {
+ nsCString curIdentityEmail2;
+ lookupIdentity2->GetEmail(curIdentityEmail2);
+ ToLowerCase(curIdentityEmail2);
+ if (toEmailAddressesLower.Contains(curIdentityEmail2)) {
+ // However, "From:me To:me" should be treated as
+ // reply-to-self if we have a Bcc. If we don't have a Bcc we
+ // might have the case of a generated mail of the style
+ // "From:me To:me Reply-To:customer". Then we need to to do a
+ // normal reply to the customer.
+ isReplyToSelf = !bcc.IsEmpty(); // true if bcc is set
+ break;
+ } else if (ccEmailAddressesLower.Contains(curIdentityEmail2)) {
+ // If you auto-Cc yourself your email would be in Cc - but we
+ // can't detect why it is in Cc so lets just treat it like a
+ // normal reply.
+ isReplyToSelf = false;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+ if (type == nsIMsgCompType::ReplyToSenderAndGroup ||
+ type == nsIMsgCompType::ReplyToSender ||
+ type == nsIMsgCompType::Reply) {
+ if (isReplyToSelf) {
+ // Cast to concrete class. We *only* what to change m_identity, not
+ // all the things compose->SetIdentity would do.
+ nsMsgCompose* _compose = static_cast<nsMsgCompose*>(compose.get());
+ _compose->m_identity = selfIdentity;
+ compFields->SetFrom(from);
+ compFields->SetTo(to);
+ compFields->SetReplyTo(replyTo);
+ } else if (!mailReplyTo.IsEmpty()) {
+ // handle Mail-Reply-To (http://cr.yp.to/proto/replyto.html)
+ compFields->SetTo(mailReplyTo);
+ needToRemoveDup = true;
+ } else if (!replyTo.IsEmpty()) {
+ // default reply behaviour then
+
+ if (overrideReplyTo && !listPost.IsEmpty() &&
+ replyTo.Find(listPost) != kNotFound) {
+ // Reply-To munging in this list post. Reply to From instead,
+ // as the user can choose Reply List if that's what he wants.
+ compFields->SetTo(from);
+ } else {
+ compFields->SetTo(replyTo);
+ }
+ needToRemoveDup = true;
+ } else {
+ compFields->SetTo(from);
+ }
+ } else if (type == nsIMsgCompType::ReplyAll) {
+ if (isReplyToSelf) {
+ // Cast to concrete class. We *only* what to change m_identity, not
+ // all the things compose->SetIdentity would do.
+ nsMsgCompose* _compose = static_cast<nsMsgCompose*>(compose.get());
+ _compose->m_identity = selfIdentity;
+ compFields->SetFrom(from);
+ compFields->SetTo(to);
+ compFields->SetCc(cc);
+ // In case it's a reply to self, but it's not the actual source of the
+ // sent message, then we won't know the Bcc header. So set it only if
+ // it's not empty. If you have auto-bcc and removed the auto-bcc for
+ // the original mail, you will have to do it manually for this reply
+ // too.
+ if (!bcc.IsEmpty()) compFields->SetBcc(bcc);
+ compFields->SetReplyTo(replyTo);
+ needToRemoveDup = true;
+ } else if (mailFollowupTo.IsEmpty()) {
+ // default reply-all behaviour then
+
+ nsAutoString allTo;
+ if (!replyTo.IsEmpty()) {
+ allTo.Assign(replyTo);
+ needToRemoveDup = true;
+ if (overrideReplyTo && !listPost.IsEmpty() &&
+ replyTo.Find(listPost) != kNotFound) {
+ // Reply-To munging in this list. Add From to recipients, it's the
+ // lesser evil...
+ allTo.AppendLiteral(", ");
+ allTo.Append(from);
+ }
+ } else {
+ allTo.Assign(from);
+ }
+
+ allTo.AppendLiteral(", ");
+ allTo.Append(to);
+ compFields->SetTo(allTo);
+
+ nsAutoString allCc;
+ compFields->GetCc(allCc); // auto-cc
+ if (!allCc.IsEmpty()) allCc.AppendLiteral(", ");
+ allCc.Append(cc);
+ compFields->SetCc(allCc);
+
+ needToRemoveDup = true;
+ } else {
+ // Handle Mail-Followup-To (http://cr.yp.to/proto/replyto.html)
+ compFields->SetTo(mailFollowupTo);
+ needToRemoveDup = true; // To remove possible self from To.
+
+ // If Cc is set a this point it's auto-Ccs, so we'll just keep those.
+ }
+ } else if (type == nsIMsgCompType::ReplyToList) {
+ compFields->SetTo(listPost);
+ }
+
+ if (!newgroups.IsEmpty()) {
+ if ((type != nsIMsgCompType::Reply) &&
+ (type != nsIMsgCompType::ReplyToSender))
+ compFields->SetNewsgroups(newgroups);
+ if (type == nsIMsgCompType::ReplyToGroup)
+ compFields->SetTo(EmptyString());
+ }
+
+ if (!followUpTo.IsEmpty()) {
+ // Handle "followup-to: poster" magic keyword here
+ if (followUpTo.EqualsLiteral("poster")) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ compose->GetDomWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
+ nsMsgDisplayMessageByName(domWindow, "followupToSenderMessage");
+
+ if (!replyTo.IsEmpty()) {
+ compFields->SetTo(replyTo);
+ } else {
+ // If reply-to is empty, use the From header to fetch the original
+ // sender's email.
+ compFields->SetTo(from);
+ }
+
+ // Clear the newsgroup: header field, because followup-to: poster
+ // only follows up to the original sender
+ if (!newgroups.IsEmpty()) compFields->SetNewsgroups(EmptyString());
+ } else // Process "followup-to: newsgroup-content" here
+ {
+ if (type != nsIMsgCompType::ReplyToSender)
+ compFields->SetNewsgroups(followUpTo);
+ if (type == nsIMsgCompType::Reply) {
+ compFields->SetTo(EmptyString());
+ }
+ }
+ }
+
+ if (!references.IsEmpty()) references.Append(char16_t(' '));
+ references += messageId;
+ compFields->SetReferences(NS_LossyConvertUTF16toASCII(references).get());
+
+ nsAutoCString resultStr;
+
+ // Cast interface to concrete class that has direct field getters etc.
+ nsMsgCompFields* _compFields =
+ static_cast<nsMsgCompFields*>(compFields.get());
+
+ // Remove duplicate addresses between To && Cc.
+ if (needToRemoveDup) {
+ nsCString addressesToRemoveFromCc;
+ if (mIdentity) {
+ bool removeMyEmailInCc = true;
+ nsCString myEmail;
+ // Get senders address from composeField or from identity,
+ nsAutoCString sender(_compFields->GetFrom());
+ ExtractEmail(EncodedHeader(sender), myEmail);
+ if (myEmail.IsEmpty()) mIdentity->GetEmail(myEmail);
+
+ // Remove my own address from To, unless it's a reply to self.
+ if (!isReplyToSelf) {
+ RemoveDuplicateAddresses(nsDependentCString(_compFields->GetTo()),
+ myEmail, resultStr);
+ _compFields->SetTo(resultStr.get());
+ }
+ addressesToRemoveFromCc.Assign(_compFields->GetTo());
+
+ // Remove own address from CC unless we want it in there
+ // through the automatic-CC-to-self (see bug 584962). There are
+ // three cases:
+ // - user has no automatic CC
+ // - user has automatic CC but own email is not in it
+ // - user has automatic CC and own email in it
+ // Only in the last case do we want our own email address to stay
+ // in the CC list.
+ bool automaticCc;
+ mIdentity->GetDoCc(&automaticCc);
+ if (automaticCc) {
+ nsCString autoCcList;
+ mIdentity->GetDoCcList(autoCcList);
+ nsTArray<nsCString> autoCcEmailAddresses;
+ ExtractEmails(EncodedHeader(autoCcList),
+ UTF16ArrayAdapter<>(autoCcEmailAddresses));
+ if (autoCcEmailAddresses.Contains(myEmail)) {
+ removeMyEmailInCc = false;
+ }
+ }
+
+ if (removeMyEmailInCc) {
+ addressesToRemoveFromCc.AppendLiteral(", ");
+ addressesToRemoveFromCc.Append(myEmail);
+ }
+ }
+ RemoveDuplicateAddresses(nsDependentCString(_compFields->GetCc()),
+ addressesToRemoveFromCc, resultStr);
+ _compFields->SetCc(resultStr.get());
+ if (_compFields->GetBcc()) {
+ // Remove addresses already in Cc from Bcc.
+ RemoveDuplicateAddresses(nsDependentCString(_compFields->GetBcc()),
+ nsDependentCString(_compFields->GetCc()),
+ resultStr);
+ if (!resultStr.IsEmpty()) {
+ // Remove addresses already in To from Bcc.
+ RemoveDuplicateAddresses(
+ resultStr, nsDependentCString(_compFields->GetTo()), resultStr);
+ }
+ _compFields->SetBcc(resultStr.get());
+ }
+ }
+ }
+ }
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ nsCOMPtr<nsIMsgComposeService> composeService(
+ do_GetService("@mozilla.org/messengercompose;1"));
+ composeService->TimeStamp(
+ "Done with MIME. Now we're updating the UI elements", false);
+#endif
+
+ if (mQuoteOriginal)
+ compose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeFieldsReady, NS_OK);
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ composeService->TimeStamp(
+ "Addressing widget, window title and focus are now set, time to insert "
+ "the body",
+ false);
+#endif
+
+ if (!mHeadersOnly) mMsgBody.AppendLiteral("</html>");
+
+ // Now we have an HTML representation of the quoted message.
+ // If we are in plain text mode, we need to convert this to plain
+ // text before we try to insert it into the editor. If we don't, we
+ // just get lots of HTML text in the message...not good.
+ //
+ // XXX not m_composeHTML? /BenB
+ bool composeHTML = true;
+ compose->GetComposeHTML(&composeHTML);
+ if (!composeHTML) {
+ // Downsampling.
+
+ // In plain text quotes we always allow line breaking to not end up with
+ // long lines. The quote is inserted into a span with style
+ // "white-space: pre;" which isn't be wrapped.
+ // Update: Bug 387687 changed this to "white-space: pre-wrap;".
+ // Note that the body of the plain text message is wrapped since it uses
+ // "white-space: pre-wrap; width: 72ch;".
+ // Look at it in the DOM Inspector to see it.
+ //
+ // If we're using format flowed, we need to pass it so the encoder
+ // can add a space at the end.
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ bool flowed = false;
+ if (pPrefBranch) {
+ pPrefBranch->GetBoolPref("mailnews.send_plaintext_flowed", &flowed);
+ }
+
+ rv = ConvertToPlainText(flowed,
+ true, // formatted
+ false); // allow line breaks
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ compose->ProcessSignature(mIdentity, true, &mSignature);
+
+ nsCOMPtr<nsIEditor> editor;
+ if (NS_SUCCEEDED(compose->GetEditor(getter_AddRefs(editor))) && editor) {
+ if (mQuoteOriginal)
+ compose->ConvertAndLoadComposeWindow(mCitePrefix, mMsgBody, mSignature,
+ true, composeHTML);
+ else
+ InsertToCompose(editor, composeHTML);
+ }
+
+ if (mQuoteOriginal)
+ compose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeBodyReady, NS_OK);
+ return rv;
+}
+
+NS_IMETHODIMP QuotingOutputStreamListener::OnDataAvailable(
+ nsIRequest* request, nsIInputStream* inStr, uint64_t sourceOffset,
+ uint32_t count) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG(inStr);
+
+ if (mHeadersOnly) return rv;
+
+ char* newBuf = (char*)PR_Malloc(count + 1);
+ if (!newBuf) return NS_ERROR_FAILURE;
+
+ uint32_t numWritten = 0;
+ rv = inStr->Read(newBuf, count, &numWritten);
+ if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK;
+ newBuf[numWritten] = '\0';
+ if (NS_SUCCEEDED(rv) && numWritten > 0) {
+ rv = AppendToMsgBody(nsDependentCString(newBuf, numWritten));
+ }
+
+ PR_FREEIF(newBuf);
+ return rv;
+}
+
+nsresult QuotingOutputStreamListener::AppendToMsgBody(const nsCString& inStr) {
+ nsresult rv = NS_OK;
+ if (!inStr.IsEmpty()) {
+ nsAutoString tmp;
+ rv = UTF_8_ENCODING->DecodeWithoutBOMHandling(inStr, tmp);
+ if (NS_SUCCEEDED(rv)) mMsgBody.Append(tmp);
+ }
+ return rv;
+}
+
+nsresult QuotingOutputStreamListener::SetComposeObj(nsIMsgCompose* obj) {
+ mWeakComposeObj = do_GetWeakReference(obj);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotingOutputStreamListener::SetMimeHeaders(nsIMimeHeaders* headers) {
+ mHeaders = headers;
+ return NS_OK;
+}
+
+nsresult QuotingOutputStreamListener::InsertToCompose(nsIEditor* aEditor,
+ bool aHTMLEditor) {
+ NS_ENSURE_ARG(aEditor);
+ nsCOMPtr<nsINode> nodeInserted;
+
+ TranslateLineEnding(mMsgBody);
+
+ // Now, insert it into the editor...
+ aEditor->EnableUndo(true);
+
+ nsCOMPtr<nsIMsgCompose> compose = do_QueryReferent(mWeakComposeObj);
+ if (!mMsgBody.IsEmpty() && compose) {
+ compose->SetAllowRemoteContent(true);
+ if (!mCitePrefix.IsEmpty()) {
+ if (!aHTMLEditor) mCitePrefix.AppendLiteral("\n");
+ aEditor->InsertText(mCitePrefix);
+ }
+
+ RefPtr<mozilla::HTMLEditor> htmlEditor = aEditor->AsHTMLEditor();
+ if (aHTMLEditor) {
+ nsAutoString body(mMsgBody);
+ remove_plaintext_tag(body);
+ htmlEditor->InsertAsCitedQuotation(body, EmptyString(), true,
+ getter_AddRefs(nodeInserted));
+ } else {
+ htmlEditor->InsertAsQuotation(mMsgBody, getter_AddRefs(nodeInserted));
+ }
+ compose->SetAllowRemoteContent(false);
+ }
+
+ RefPtr<Selection> selection;
+ nsCOMPtr<nsINode> parent;
+ int32_t offset;
+ nsresult rv;
+
+ // get parent and offset of mailcite
+ rv = GetNodeLocation(nodeInserted, address_of(parent), &offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get selection
+ aEditor->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ // place selection after mailcite
+ selection->CollapseInLimiter(parent, offset + 1);
+ // insert a break at current selection
+ aEditor->InsertLineBreak();
+ selection->CollapseInLimiter(parent, offset + 1);
+ }
+ nsCOMPtr<nsISelectionController> selCon;
+ aEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (selCon)
+ // After ScrollSelectionIntoView(), the pending notifications might be
+ // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
+ selCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_ANCHOR_REGION, true);
+
+ return NS_OK;
+}
+
+/**
+ * Returns true if the domain is a match for the given the domain list.
+ * Subdomains are also considered to match.
+ * @param aDomain - the domain name to check
+ * @param aDomainList - a comma separated string of domain names
+ */
+bool IsInDomainList(const nsAString& aDomain, const nsAString& aDomainList) {
+ if (aDomain.IsEmpty() || aDomainList.IsEmpty()) return false;
+
+ // Check plain text domains.
+ int32_t left = 0;
+ int32_t right = 0;
+ while (right != (int32_t)aDomainList.Length()) {
+ right = aDomainList.FindChar(',', left);
+ if (right == kNotFound) right = aDomainList.Length();
+ nsDependentSubstring domain = Substring(aDomainList, left, right);
+
+ if (aDomain.Equals(domain, nsCaseInsensitiveStringComparator)) return true;
+
+ nsAutoString dotDomain;
+ dotDomain.Assign(u'.');
+ dotDomain.Append(domain);
+ if (StringEndsWith(aDomain, dotDomain, nsCaseInsensitiveStringComparator))
+ return true;
+
+ left = right + 1;
+ }
+ return false;
+}
+
+NS_IMPL_ISUPPORTS(QuotingOutputStreamListener,
+ nsIMsgQuotingOutputStreamListener, nsIRequestObserver,
+ nsIStreamListener, nsISupportsWeakReference)
+
+////////////////////////////////////////////////////////////////////////////////////
+// END OF QUOTING LISTENER
+////////////////////////////////////////////////////////////////////////////////////
+
+/* attribute MSG_ComposeType type; */
+NS_IMETHODIMP nsMsgCompose::SetType(MSG_ComposeType aType) {
+ mType = aType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetType(MSG_ComposeType* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+
+ *aType = mType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::QuoteMessage(const nsACString& msgURI) {
+ nsresult rv;
+ mQuotingToFollow = false;
+
+ // Create a mime parser (nsIStreamConverter)!
+ mQuote = do_CreateInstance("@mozilla.org/messengercompose/quoting;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(msgURI, getter_AddRefs(msgHdr));
+
+ // Create the consumer output stream.. this will receive all the HTML from
+ // libmime
+ mQuoteStreamListener =
+ new QuotingOutputStreamListener(msgHdr, false, !mHtmlToQuote.IsEmpty(),
+ m_identity, mQuote, false, mHtmlToQuote);
+
+ mQuoteStreamListener->SetComposeObj(this);
+
+ rv = mQuote->QuoteMessage(msgURI, false, mQuoteStreamListener,
+ mAutodetectCharset, false, msgHdr);
+ return rv;
+}
+
+nsresult nsMsgCompose::QuoteOriginalMessage() // New template
+{
+ nsresult rv;
+
+ mQuotingToFollow = false;
+
+ // Create a mime parser (nsIStreamConverter)!
+ mQuote = do_CreateInstance("@mozilla.org/messengercompose/quoting;1", &rv);
+ if (NS_FAILED(rv) || !mQuote) return NS_ERROR_FAILURE;
+
+ bool bAutoQuote = true;
+ m_identity->GetAutoQuote(&bAutoQuote);
+
+ nsCOMPtr<nsIMsgDBHdr> originalMsgHdr = mOrigMsgHdr;
+ if (!originalMsgHdr) {
+ rv = GetMsgDBHdrFromURI(mOriginalMsgURI, getter_AddRefs(originalMsgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoCString msgUri(mOriginalMsgURI);
+ bool fileUrl = StringBeginsWith(msgUri, "file:"_ns);
+ if (fileUrl) {
+ msgUri.Replace(0, 5, "mailbox:"_ns);
+ msgUri.AppendLiteral("?number=0");
+ }
+
+ // Create the consumer output stream.. this will receive all the HTML from
+ // libmime
+ mQuoteStreamListener = new QuotingOutputStreamListener(
+ originalMsgHdr, mWhatHolder != 1, !bAutoQuote || !mHtmlToQuote.IsEmpty(),
+ m_identity, mQuote, true, mHtmlToQuote);
+
+ mQuoteStreamListener->SetComposeObj(this);
+
+ rv = mQuote->QuoteMessage(msgUri, mWhatHolder != 1, mQuoteStreamListener,
+ mAutodetectCharset, !bAutoQuote, originalMsgHdr);
+ return rv;
+}
+
+// CleanUpRecipient will remove un-necessary "<>" when a recipient as an address
+// without name
+void nsMsgCompose::CleanUpRecipients(nsString& recipients) {
+ uint16_t i;
+ bool startANewRecipient = true;
+ bool removeBracket = false;
+ nsAutoString newRecipient;
+ char16_t aChar;
+
+ for (i = 0; i < recipients.Length(); i++) {
+ aChar = recipients[i];
+ switch (aChar) {
+ case '<':
+ if (startANewRecipient)
+ removeBracket = true;
+ else
+ newRecipient += aChar;
+ startANewRecipient = false;
+ break;
+
+ case '>':
+ if (removeBracket)
+ removeBracket = false;
+ else
+ newRecipient += aChar;
+ break;
+
+ case ' ':
+ newRecipient += aChar;
+ break;
+
+ case ',':
+ newRecipient += aChar;
+ startANewRecipient = true;
+ removeBracket = false;
+ break;
+
+ default:
+ newRecipient += aChar;
+ startANewRecipient = false;
+ break;
+ }
+ }
+ recipients = newRecipient;
+}
+
+NS_IMETHODIMP nsMsgCompose::RememberQueuedDisposition() {
+ // need to find the msg hdr in the saved folder and then set a property on
+ // the header that we then look at when we actually send the message.
+ nsresult rv;
+ nsAutoCString dispositionSetting;
+
+ if (mType == nsIMsgCompType::Reply || mType == nsIMsgCompType::ReplyAll ||
+ mType == nsIMsgCompType::ReplyToList ||
+ mType == nsIMsgCompType::ReplyToGroup ||
+ mType == nsIMsgCompType::ReplyToSender ||
+ mType == nsIMsgCompType::ReplyToSenderAndGroup) {
+ dispositionSetting.AssignLiteral("replied");
+ } else if (mType == nsIMsgCompType::ForwardAsAttachment ||
+ mType == nsIMsgCompType::ForwardInline) {
+ dispositionSetting.AssignLiteral("forwarded");
+ } else if (mType == nsIMsgCompType::Redirect) {
+ dispositionSetting.AssignLiteral("redirected");
+ } else if (mType == nsIMsgCompType::Draft) {
+ nsAutoCString curDraftIdURL;
+ rv = m_compFields->GetDraftId(curDraftIdURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!curDraftIdURL.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> draftHdr;
+ rv = GetMsgDBHdrFromURI(curDraftIdURL, getter_AddRefs(draftHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ draftHdr->GetStringProperty(QUEUED_DISPOSITION_PROPERTY,
+ dispositionSetting);
+ }
+ }
+
+ nsMsgKey msgKey;
+ if (mMsgSend) {
+ mMsgSend->GetMessageKey(&msgKey);
+ nsCString identityKey;
+
+ m_identity->GetKey(identityKey);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(m_folderName, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = folder->GetMessageHeader(msgKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t pseudoHdrProp = 0;
+ msgHdr->GetUint32Property("pseudoHdr", &pseudoHdrProp);
+ if (pseudoHdrProp) {
+ // Use SetAttributeOnPendingHdr for IMAP pseudo headers, as those
+ // will get deleted (and properties set using SetStringProperty lost.)
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ rv = folder->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString messageId;
+ mMsgSend->GetMessageId(messageId);
+ msgHdr->SetMessageId(messageId.get());
+ if (!mOriginalMsgURI.IsEmpty()) {
+ msgDB->SetAttributeOnPendingHdr(msgHdr, ORIG_URI_PROPERTY,
+ mOriginalMsgURI.get());
+ if (!dispositionSetting.IsEmpty())
+ msgDB->SetAttributeOnPendingHdr(msgHdr, QUEUED_DISPOSITION_PROPERTY,
+ dispositionSetting.get());
+ }
+ msgDB->SetAttributeOnPendingHdr(msgHdr, HEADER_X_MOZILLA_IDENTITY_KEY,
+ identityKey.get());
+ } else if (msgHdr) {
+ if (!mOriginalMsgURI.IsEmpty()) {
+ msgHdr->SetStringProperty(ORIG_URI_PROPERTY, mOriginalMsgURI);
+ if (!dispositionSetting.IsEmpty())
+ msgHdr->SetStringProperty(QUEUED_DISPOSITION_PROPERTY,
+ dispositionSetting);
+ }
+ msgHdr->SetStringProperty(HEADER_X_MOZILLA_IDENTITY_KEY, identityKey);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::ProcessReplyFlags() {
+ nsresult rv;
+ // check to see if we were doing a reply or a forward, if we were, set the
+ // answered field flag on the message folder for this URI.
+ if (mType == nsIMsgCompType::Reply || mType == nsIMsgCompType::ReplyAll ||
+ mType == nsIMsgCompType::ReplyToList ||
+ mType == nsIMsgCompType::ReplyToGroup ||
+ mType == nsIMsgCompType::ReplyToSender ||
+ mType == nsIMsgCompType::ReplyToSenderAndGroup ||
+ mType == nsIMsgCompType::ForwardAsAttachment ||
+ mType == nsIMsgCompType::ForwardInline ||
+ mType == nsIMsgCompType::Redirect ||
+ mDraftDisposition != nsIMsgFolder::nsMsgDispositionState_None) {
+ if (!mOriginalMsgURI.IsEmpty()) {
+ nsCString msgUri(mOriginalMsgURI);
+ char* newStr = msgUri.BeginWriting();
+ char* uri;
+ while (nullptr != (uri = NS_strtok(",", &newStr))) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv =
+ GetMsgDBHdrFromURI(nsDependentCString(uri), getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) {
+ // get the folder for the message resource
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ msgHdr->GetFolder(getter_AddRefs(msgFolder));
+ if (msgFolder) {
+ // If it's a draft with disposition, default to replied, otherwise,
+ // check if it's a forward.
+ nsMsgDispositionState dispositionSetting =
+ nsIMsgFolder::nsMsgDispositionState_Replied;
+ if (mDraftDisposition != nsIMsgFolder::nsMsgDispositionState_None)
+ dispositionSetting = mDraftDisposition;
+ else if (mType == nsIMsgCompType::ForwardAsAttachment ||
+ mType == nsIMsgCompType::ForwardInline)
+ dispositionSetting =
+ nsIMsgFolder::nsMsgDispositionState_Forwarded;
+ else if (mType == nsIMsgCompType::Redirect)
+ dispositionSetting =
+ nsIMsgFolder::nsMsgDispositionState_Redirected;
+
+ msgFolder->AddMessageDispositionState(msgHdr, dispositionSetting);
+ if (mType != nsIMsgCompType::ForwardAsAttachment)
+ break; // just safeguard
+ }
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgCompose::OnStartSending(const char* aMsgID,
+ uint32_t aMsgSize) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnStartSending(aMsgID, aMsgSize);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::OnProgress(const char* aMsgID, uint32_t aProgress,
+ uint32_t aProgressMax) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnProgress(aMsgID, aProgress, aProgressMax);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::OnStatus(const char* aMsgID, const char16_t* aMsg) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnStatus(aMsgID, aMsg);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::OnStopSending(const char* aMsgID, nsresult aStatus,
+ const char16_t* aMsg,
+ nsIFile* returnFile) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnStopSending(aMsgID, aStatus, aMsg, returnFile);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::OnTransportSecurityError(const char* msgID, nsresult status,
+ nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnTransportSecurityError(msgID, status, secInfo,
+ location);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::OnSendNotPerformed(const char* aMsgID,
+ nsresult aStatus) {
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnSendNotPerformed(aMsgID, aStatus);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::OnGetDraftFolderURI(const char* aMsgID,
+ const nsACString& aFolderURI) {
+ m_folderName = aFolderURI;
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter(
+ mExternalSendListeners);
+ nsCOMPtr<nsIMsgSendListener> externalSendListener;
+
+ while (iter.HasMore()) {
+ externalSendListener = iter.GetNext();
+ externalSendListener->OnGetDraftFolderURI(aMsgID, aFolderURI);
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for both the send operation and the copy
+// operation. We have to create this class to listen for message send completion
+// and deal with failures in both send and copy operations
+////////////////////////////////////////////////////////////////////////////////////
+NS_IMPL_ADDREF(nsMsgComposeSendListener)
+NS_IMPL_RELEASE(nsMsgComposeSendListener)
+
+/*
+NS_IMPL_QUERY_INTERFACE(nsMsgComposeSendListener,
+ nsIMsgComposeSendListener,
+ nsIMsgSendListener,
+ nsIMsgCopyServiceListener,
+ nsIWebProgressListener)
+*/
+NS_INTERFACE_MAP_BEGIN(nsMsgComposeSendListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgComposeSendListener)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgComposeSendListener)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgSendListener)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgCopyServiceListener)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener)
+NS_INTERFACE_MAP_END
+
+nsMsgComposeSendListener::nsMsgComposeSendListener(void) { mDeliverMode = 0; }
+
+nsMsgComposeSendListener::~nsMsgComposeSendListener(void) {}
+
+NS_IMETHODIMP nsMsgComposeSendListener::SetMsgCompose(nsIMsgCompose* obj) {
+ mWeakComposeObj = do_GetWeakReference(obj);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::SetDeliverMode(
+ MSG_DeliverMode deliverMode) {
+ mDeliverMode = deliverMode;
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnStartSending(const char* aMsgID,
+ uint32_t aMsgSize) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnStartSending(aMsgID, aMsgSize);
+
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnProgress(const char* aMsgID,
+ uint32_t aProgress,
+ uint32_t aProgressMax) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnProgress(aMsgID, aProgress, aProgressMax);
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnStatus(const char* aMsgID,
+ const char16_t* aMsg) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnStatus(aMsgID, aMsg);
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnSendNotPerformed(const char* aMsgID,
+ nsresult aStatus) {
+ // since OnSendNotPerformed is called in the case where the user aborts the
+ // operation by closing the compose window, we need not do the stuff required
+ // for closing the windows. However we would need to do the other operations
+ // as below.
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv);
+ if (msgCompose)
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, aStatus);
+
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnSendNotPerformed(aMsgID, aStatus);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgComposeSendListener::OnTransportSecurityError(
+ const char* msgID, nsresult status, nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnTransportSecurityError(msgID, status, secInfo,
+ location);
+
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnStopSending(const char* aMsgID,
+ nsresult aStatus,
+ const char16_t* aMsg,
+ nsIFile* returnFile) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv);
+ if (msgCompose) {
+ nsCOMPtr<nsIMsgProgress> progress;
+ msgCompose->GetProgress(getter_AddRefs(progress));
+
+ if (NS_SUCCEEDED(aStatus)) {
+ nsCOMPtr<nsIMsgCompFields> compFields;
+ msgCompose->GetCompFields(getter_AddRefs(compFields));
+
+ // only process the reply flags if we successfully sent the message
+ msgCompose->ProcessReplyFlags();
+
+ // See if there is a composer window
+ bool hasDomWindow = true;
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ rv = msgCompose->GetDomWindow(getter_AddRefs(domWindow));
+ if (NS_FAILED(rv) || !domWindow) hasDomWindow = false;
+
+ // Close the window ONLY if we are not going to do a save operation
+ nsAutoString fieldsFCC;
+ if (NS_SUCCEEDED(compFields->GetFcc(fieldsFCC))) {
+ if (!fieldsFCC.IsEmpty()) {
+ if (fieldsFCC.LowerCaseEqualsLiteral("nocopy://")) {
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, NS_OK);
+ if (progress) {
+ progress->UnregisterListener(this);
+ progress->CloseProgressDialog(false);
+ }
+ if (hasDomWindow) msgCompose->CloseWindow();
+ }
+ }
+ } else {
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, NS_OK);
+ if (progress) {
+ progress->UnregisterListener(this);
+ progress->CloseProgressDialog(false);
+ }
+ if (hasDomWindow)
+ msgCompose->CloseWindow(); // if we fail on the simple GetFcc call,
+ // close the window to be safe and avoid
+ // windows hanging around to prevent the
+ // app from exiting.
+ }
+
+ // Remove the current draft msg when sending draft is done.
+ bool deleteDraft;
+ msgCompose->GetDeleteDraft(&deleteDraft);
+ if (deleteDraft) RemoveCurrentDraftMessage(msgCompose, false, false);
+ } else {
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, aStatus);
+ if (progress) {
+ progress->CloseProgressDialog(true);
+ progress->UnregisterListener(this);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnStopSending(aMsgID, aStatus, aMsg, returnFile);
+
+ return rv;
+}
+
+nsresult nsMsgComposeSendListener::OnGetDraftFolderURI(
+ const char* aMsgID, const nsACString& aFolderURI) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendListener> composeSendListener =
+ do_QueryReferent(mWeakComposeObj, &rv);
+ if (NS_SUCCEEDED(rv) && composeSendListener)
+ composeSendListener->OnGetDraftFolderURI(aMsgID, aFolderURI);
+
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnStartCopy() { return NS_OK; }
+
+nsresult nsMsgComposeSendListener::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::OnStopCopy(nsresult aStatus) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv);
+ if (msgCompose) {
+ if (mDeliverMode == nsIMsgSend::nsMsgQueueForLater ||
+ mDeliverMode == nsIMsgSend::nsMsgDeliverBackground ||
+ mDeliverMode == nsIMsgSend::nsMsgSaveAsDraft) {
+ msgCompose->RememberQueuedDisposition();
+ }
+
+ // Ok, if we are here, we are done with the send/copy operation so
+ // we have to do something with the window....SHOW if failed, Close
+ // if succeeded
+
+ nsCOMPtr<nsIMsgProgress> progress;
+ msgCompose->GetProgress(getter_AddRefs(progress));
+ if (progress) {
+ // Unregister ourself from msg compose progress
+ progress->UnregisterListener(this);
+ progress->CloseProgressDialog(NS_FAILED(aStatus));
+ }
+
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::ComposeProcessDone, aStatus);
+
+ if (NS_SUCCEEDED(aStatus)) {
+ // We should only close the window if we are done. Things like templates
+ // and drafts aren't done so their windows should stay open
+ if (mDeliverMode == nsIMsgSend::nsMsgSaveAsDraft ||
+ mDeliverMode == nsIMsgSend::nsMsgSaveAsTemplate) {
+ msgCompose->NotifyStateListeners(
+ nsIMsgComposeNotificationType::SaveInFolderDone, aStatus);
+ // Remove the current draft msg when saving as draft/template is done.
+ msgCompose->SetDeleteDraft(true);
+ RemoveCurrentDraftMessage(
+ msgCompose, true, mDeliverMode == nsIMsgSend::nsMsgSaveAsTemplate);
+ } else {
+ // Remove (possible) draft if we're in send later mode
+ if (mDeliverMode == nsIMsgSend::nsMsgQueueForLater ||
+ mDeliverMode == nsIMsgSend::nsMsgDeliverBackground) {
+ msgCompose->SetDeleteDraft(true);
+ RemoveCurrentDraftMessage(msgCompose, true, false);
+ }
+ msgCompose->CloseWindow();
+ }
+ }
+ msgCompose->ClearMessageSend();
+ }
+
+ return rv;
+}
+
+nsresult nsMsgComposeSendListener::GetMsgFolder(nsIMsgCompose* compObj,
+ nsIMsgFolder** msgFolder) {
+ nsCString folderUri;
+
+ nsresult rv = compObj->GetSavedFolderURI(folderUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetOrCreateFolder(folderUri, msgFolder);
+}
+
+nsresult nsMsgComposeSendListener::RemoveDraftOrTemplate(nsIMsgCompose* compObj,
+ nsCString msgURI,
+ bool isSaveTemplate) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nsCOMPtr<nsIMsgDBHdr> msgDBHdr;
+ rv = GetMsgDBHdrFromURI(msgURI, getter_AddRefs(msgDBHdr));
+ NS_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RemoveDraftOrTemplate can't get msg header DB interface pointer");
+ if (NS_SUCCEEDED(rv) && msgDBHdr) {
+ do { // Break on failure or removal not needed.
+ // Get the folder for the message resource.
+ rv = msgDBHdr->GetFolder(getter_AddRefs(msgFolder));
+ NS_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RemoveDraftOrTemplate can't get msg folder interface pointer");
+ if (NS_FAILED(rv) || !msgFolder) break;
+
+ // Only do this if it's a drafts or templates folder.
+ uint32_t flags;
+ msgFolder->GetFlags(&flags);
+ if (!(flags & (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates)))
+ break;
+ // Only delete a template when saving a new one, never delete a template
+ // when sending.
+ if (!isSaveTemplate && (flags & nsMsgFolderFlags::Templates)) break;
+
+ // Only remove if the message is actually in the db. It might have only
+ // been in the use cache.
+ nsMsgKey key;
+ rv = msgDBHdr->GetMessageKey(&key);
+ if (NS_FAILED(rv)) break;
+ nsCOMPtr<nsIMsgDatabase> db;
+ msgFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (!db) break;
+ bool containsKey = false;
+ db->ContainsKey(key, &containsKey);
+ if (!containsKey) break;
+
+ // Ready to delete the msg.
+ rv = msgFolder->DeleteMessages({&*msgDBHdr}, nullptr, true, false,
+ nullptr, false /*allowUndo*/);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "RemoveDraftOrTemplate can't delete message");
+ } while (false);
+ } else {
+ // If we get here we have the case where the draft folder is on the server
+ // and it's not currently open (in thread pane), so draft msgs are saved to
+ // the server but they're not in our local DB. In this case,
+ // GetMsgDBHdrFromURI() will never find the msg. If the draft folder is a
+ // local one then we'll not get here because the draft msgs are saved to the
+ // local folder and are in local DB. Make sure the msg folder is imap. Even
+ // if we get here due to DB errors (worst case), we should still try to
+ // delete msg on the server because that's where the master copy of the msgs
+ // are stored, if draft folder is on the server. For local case, since DB is
+ // bad we can't do anything with it anyway so it'll be noop in this case.
+ rv = GetMsgFolder(compObj, getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder);
+ NS_ASSERTION(imapFolder,
+ "The draft folder MUST be an imap folder in order to mark "
+ "the msg delete!");
+ if (NS_SUCCEEDED(rv) && imapFolder) {
+ // Only do this if it's a drafts or templates folder.
+ uint32_t flags;
+ msgFolder->GetFlags(&flags);
+ if (!(flags & (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates)))
+ return NS_OK;
+ // Only delete a template when saving a new one, never delete a template
+ // when sending.
+ if (!isSaveTemplate && (flags & nsMsgFolderFlags::Templates))
+ return NS_OK;
+
+ const char* str = PL_strchr(msgURI.get(), '#');
+ NS_ASSERTION(str, "Failed to get current draft id url");
+ if (str) {
+ nsAutoCString srcStr(str + 1);
+ nsresult err;
+ nsMsgKey messageID = srcStr.ToInteger(&err);
+ if (messageID != nsMsgKey_None) {
+ rv = imapFolder->StoreImapFlags(kImapMsgDeletedFlag, true,
+ {messageID}, nullptr);
+ }
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+/**
+ * Remove the current draft message since a new one will be saved.
+ * When we're coming to save a template, also delete the original template.
+ * This is necessary since auto-save doesn't delete the original template.
+ */
+nsresult nsMsgComposeSendListener::RemoveCurrentDraftMessage(
+ nsIMsgCompose* compObj, bool calledByCopy, bool isSaveTemplate) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgCompFields> compFields = nullptr;
+
+ rv = compObj->GetCompFields(getter_AddRefs(compFields));
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "RemoveCurrentDraftMessage can't get compose fields");
+ if (NS_FAILED(rv) || !compFields) return rv;
+
+ nsCString curDraftIdURL;
+ rv = compFields->GetDraftId(curDraftIdURL);
+
+ // Skip if no draft id (probably a new draft msg).
+ if (NS_SUCCEEDED(rv) && !curDraftIdURL.IsEmpty()) {
+ rv = RemoveDraftOrTemplate(compObj, curDraftIdURL, isSaveTemplate);
+ if (NS_FAILED(rv)) NS_WARNING("Removing current draft failed");
+ } else {
+ NS_WARNING("RemoveCurrentDraftMessage can't get draft id");
+ }
+
+ if (isSaveTemplate) {
+ nsCString templateIdURL;
+ rv = compFields->GetTemplateId(templateIdURL);
+ if (NS_SUCCEEDED(rv) && !templateIdURL.Equals(curDraftIdURL)) {
+ // Above we deleted an auto-saved draft, so here we need to delete
+ // the original template.
+ rv = RemoveDraftOrTemplate(compObj, templateIdURL, isSaveTemplate);
+ if (NS_FAILED(rv)) NS_WARNING("Removing original template failed");
+ }
+ }
+
+ // Now get the new uid so that next save will remove the right msg
+ // regardless whether or not the exiting msg can be deleted.
+ if (calledByCopy) {
+ nsMsgKey newUid = 0;
+ nsCOMPtr<nsIMsgFolder> savedToFolder;
+ nsCOMPtr<nsIMsgSend> msgSend;
+ rv = compObj->GetMessageSend(getter_AddRefs(msgSend));
+ NS_ASSERTION(msgSend, "RemoveCurrentDraftMessage msgSend is null.");
+ if (NS_FAILED(rv) || !msgSend) return rv;
+
+ rv = msgSend->GetMessageKey(&newUid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make sure we have a folder interface pointer
+ rv = GetMsgFolder(compObj, getter_AddRefs(savedToFolder));
+
+ // Reset draft (uid) url with the new uid.
+ if (savedToFolder && newUid != nsMsgKey_None) {
+ uint32_t folderFlags;
+ savedToFolder->GetFlags(&folderFlags);
+ if (folderFlags &
+ (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates)) {
+ nsCString newDraftIdURL;
+ rv = savedToFolder->GenerateMessageURI(newUid, newDraftIdURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ compFields->SetDraftId(newDraftIdURL);
+ if (isSaveTemplate) compFields->SetTemplateId(newDraftIdURL);
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgComposeSendListener::SetMessageKey(nsMsgKey aMessageKey) {
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSendListener::GetMessageId(nsACString& messageId) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::OnStateChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) {
+ if (aStateFlags == nsIWebProgressListener::STATE_STOP) {
+ nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj);
+ if (msgCompose) {
+ nsCOMPtr<nsIMsgProgress> progress;
+ msgCompose->GetProgress(getter_AddRefs(progress));
+
+ // Time to stop any pending operation...
+ if (progress) {
+ // Unregister ourself from msg compose progress
+ progress->UnregisterListener(this);
+
+ bool bCanceled = false;
+ progress->GetProcessCanceledByUser(&bCanceled);
+ if (bCanceled) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/"
+ "composeMsgs.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString msg;
+ bundle->GetStringFromName("msgCancelling", msg);
+ progress->OnStatusChange(nullptr, nullptr, NS_OK, msg.get());
+ }
+ }
+
+ nsCOMPtr<nsIMsgSend> msgSend;
+ msgCompose->GetMessageSend(getter_AddRefs(msgSend));
+ if (msgSend) msgSend->Abort();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::OnProgressChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress, int32_t aMaxTotalProgress) {
+ /* Ignore this call */
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::OnLocationChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* location,
+ uint32_t aFlags) {
+ /* Ignore this call */
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::OnStatusChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ /* Ignore this call */
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSendListener::OnSecurityChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t state) {
+ /* Ignore this call */
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeSendListener::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ /* Ignore this call */
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::ConvertHTMLToText(nsIFile* aSigFile,
+ nsString& aSigData) {
+ nsAutoString origBuf;
+
+ nsresult rv = LoadDataFromFile(aSigFile, origBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ConvertBufToPlainText(origBuf, false, true, true);
+ aSigData = origBuf;
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::ConvertTextToHTML(nsIFile* aSigFile,
+ nsString& aSigData) {
+ nsresult rv;
+ nsAutoString origBuf;
+
+ rv = LoadDataFromFile(aSigFile, origBuf);
+ if (NS_FAILED(rv)) return rv;
+
+ // Ok, once we are here, we need to escape the data to make sure that
+ // we don't do HTML stuff with plain text sigs.
+ nsCString escapedUTF8;
+ nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(origBuf), escapedUTF8);
+ aSigData.Append(NS_ConvertUTF8toUTF16(escapedUTF8));
+
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::LoadDataFromFile(nsIFile* file, nsString& sigData,
+ bool aAllowUTF8, bool aAllowUTF16) {
+ bool isDirectory = false;
+ file->IsDirectory(&isDirectory);
+ if (isDirectory) {
+ NS_ERROR("file is a directory");
+ return NS_MSG_ERROR_READING_FILE;
+ }
+
+ nsAutoCString data;
+ nsresult rv = nsMsgCompose::SlurpFileToString(file, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* readBuf = data.get();
+ int32_t readSize = data.Length();
+
+ nsAutoCString sigEncoding(nsMsgI18NParseMetaCharset(file));
+ bool removeSigCharset = !sigEncoding.IsEmpty() && m_composeHTML;
+
+ if (sigEncoding.IsEmpty()) {
+ if (aAllowUTF8 && mozilla::IsUtf8(nsDependentCString(readBuf))) {
+ sigEncoding.AssignLiteral("UTF-8");
+ } else if (sigEncoding.IsEmpty() && aAllowUTF16 && readSize % 2 == 0 &&
+ readSize >= 2 &&
+ ((readBuf[0] == char(0xFE) && readBuf[1] == char(0xFF)) ||
+ (readBuf[0] == char(0xFF) && readBuf[1] == char(0xFE)))) {
+ sigEncoding.AssignLiteral("UTF-16");
+ } else {
+ // Autodetect encoding for plain text files w/o meta charset
+ nsAutoCString textFileCharset;
+ rv = MsgDetectCharsetFromFile(file, textFileCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ sigEncoding.Assign(textFileCharset);
+ }
+ }
+
+ if (NS_FAILED(nsMsgI18NConvertToUnicode(sigEncoding, data, sigData)))
+ CopyASCIItoUTF16(data, sigData);
+
+ // remove sig meta charset to allow user charset override during composition
+ if (removeSigCharset) {
+ nsAutoCString metaCharset("charset=");
+ metaCharset.Append(sigEncoding);
+ int32_t pos = sigData.LowerCaseFindASCII(metaCharset);
+ if (pos != kNotFound) sigData.Cut(pos, metaCharset.Length());
+ }
+ return NS_OK;
+}
+
+/**
+ * If the data contains file URLs, convert them to data URLs instead.
+ * This is intended to be used in for signature files, so that we can make sure
+ * images loaded into the editor are available on send.
+ */
+nsresult nsMsgCompose::ReplaceFileURLs(nsString& aData) {
+ // XXX This code is rather incomplete since it looks for "file://" even
+ // outside tags.
+
+ int32_t offset = 0;
+ while (true) {
+ int32_t fPos = aData.LowerCaseFindASCII("file://", offset);
+ if (fPos == kNotFound) {
+ break; // All done.
+ }
+ bool quoted = false;
+ char16_t q = 'x'; // initialise to anything to keep compilers happy.
+ if (fPos > 0) {
+ q = aData.CharAt(fPos - 1);
+ quoted = (q == '"' || q == '\'');
+ }
+ int32_t end = kNotFound;
+ if (quoted) {
+ end = aData.FindChar(q, fPos);
+ } else {
+ int32_t spacePos = aData.FindChar(' ', fPos);
+ int32_t gtPos = aData.FindChar('>', fPos);
+ if (gtPos != kNotFound && spacePos != kNotFound) {
+ end = (spacePos < gtPos) ? spacePos : gtPos;
+ } else if (gtPos == kNotFound && spacePos != kNotFound) {
+ end = spacePos;
+ } else if (gtPos != kNotFound && spacePos == kNotFound) {
+ end = gtPos;
+ }
+ }
+ if (end == kNotFound) {
+ break;
+ }
+ nsString fileURL;
+ fileURL = Substring(aData, fPos, end - fPos);
+ nsString dataURL;
+ nsresult rv = DataURLForFileURL(fileURL, dataURL);
+ if (NS_SUCCEEDED(rv)) {
+ aData.Replace(fPos, fileURL.Length(), dataURL);
+ offset = fPos + dataURL.Length();
+ } else {
+ // If this one failed, maybe because the file wasn't found,
+ // continue to process the next one.
+ offset = fPos + fileURL.Length();
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::DataURLForFileURL(const nsAString& aFileURL,
+ nsAString& aDataURL) {
+ nsresult rv;
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> fileUri;
+ rv =
+ NS_NewURI(getter_AddRefs(fileUri), NS_ConvertUTF16toUTF8(aFileURL).get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(fileUri, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> file;
+ rv = fileUrl->GetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString type;
+ rv = mime->GetTypeFromFile(file, type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString data;
+ rv = nsMsgCompose::SlurpFileToString(file, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aDataURL.AssignLiteral("data:");
+ AppendUTF8toUTF16(type, aDataURL);
+
+ nsAutoString filename;
+ rv = file->GetLeafName(filename);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString fn;
+ MsgEscapeURL(
+ NS_ConvertUTF16toUTF8(filename),
+ nsINetUtil::ESCAPE_URL_FILE_BASENAME | nsINetUtil::ESCAPE_URL_FORCED,
+ fn);
+ if (!fn.IsEmpty()) {
+ aDataURL.AppendLiteral(";filename=");
+ aDataURL.Append(NS_ConvertUTF8toUTF16(fn));
+ }
+ }
+
+ aDataURL.AppendLiteral(";base64,");
+ char* result = PL_Base64Encode(data.get(), data.Length(), nullptr);
+ nsDependentCString base64data(result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AppendUTF8toUTF16(base64data, aDataURL);
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::SlurpFileToString(nsIFile* aFile, nsACString& aString) {
+ aString.Truncate();
+
+ nsCOMPtr<nsIURI> fileURI;
+ nsresult rv = NS_NewFileURI(getter_AddRefs(fileURI), aFile);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), fileURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = channel->Open(getter_AddRefs(stream));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = NS_ConsumeStream(stream, UINT32_MAX, aString);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = stream->Close();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::BuildQuotedMessageAndSignature(void) {
+ //
+ // This should never happen...if it does, just bail out...
+ //
+ NS_ASSERTION(m_editor, "BuildQuotedMessageAndSignature but no editor!");
+ if (!m_editor) return NS_ERROR_FAILURE;
+
+ // We will fire off the quote operation and wait for it to
+ // finish before we actually do anything with Ender...
+ return QuoteOriginalMessage();
+}
+
+//
+// This will process the signature file for the user. This method
+// will always append the results to the mMsgBody member variable.
+//
+nsresult nsMsgCompose::ProcessSignature(nsIMsgIdentity* identity, bool aQuoted,
+ nsString* aMsgBody) {
+ nsresult rv = NS_OK;
+
+ // Now, we can get sort of fancy. This is the time we need to check
+ // for all sorts of user defined stuff, like signatures and editor
+ // types and the like!
+ //
+ // user_pref(".....sig_file", "y:\\sig.html");
+ // user_pref(".....attach_signature", true);
+ // user_pref(".....htmlSigText", "unicode sig");
+ //
+ // Note: We will have intelligent signature behavior in that we
+ // look at the signature file first...if the extension is .htm or
+ // .html, we assume its HTML, otherwise, we assume it is plain text
+ //
+ // ...and that's not all! What we will also do now is look and see if
+ // the file is an image file. If it is an image file, then we should
+ // insert the correct HTML into the composer to have it work, but if we
+ // are doing plain text compose, we should insert some sort of message
+ // saying "Image Signature Omitted" or something (not done yet).
+ //
+ // If there's a sig pref, it will only be used if there is no sig file
+ // defined, thus if attach_signature is checked, htmlSigText is ignored (bug
+ // 324495). Plain-text signatures may or may not have a trailing line break
+ // (bug 428040).
+
+ bool attachFile = false;
+ bool useSigFile = false;
+ bool htmlSig = false;
+ bool imageSig = false;
+ nsAutoString sigData;
+ nsAutoString sigOutput;
+ int32_t reply_on_top = 0;
+ bool sig_bottom = true;
+ bool suppressSigSep = false;
+
+ nsCOMPtr<nsIFile> sigFile;
+ if (identity) {
+ if (!CheckIncludeSignaturePrefs(identity)) return NS_OK;
+
+ identity->GetReplyOnTop(&reply_on_top);
+ identity->GetSigBottom(&sig_bottom);
+ identity->GetSuppressSigSep(&suppressSigSep);
+
+ rv = identity->GetAttachSignature(&attachFile);
+ if (NS_SUCCEEDED(rv) && attachFile) {
+ rv = identity->GetSignature(getter_AddRefs(sigFile));
+ if (NS_SUCCEEDED(rv) && sigFile) {
+ if (!sigFile->NativePath().IsEmpty()) {
+ bool exists = false;
+ sigFile->Exists(&exists);
+ if (exists) {
+ useSigFile = true; // ok, there's a signature file
+
+ // Now, most importantly, we need to figure out what the content
+ // type is for this signature...if we can't, we assume text
+ nsAutoCString sigContentType;
+ nsresult rv2; // don't want to clobber the other rv
+ nsCOMPtr<nsIMIMEService> mimeFinder(
+ do_GetService(NS_MIMESERVICE_CONTRACTID, &rv2));
+ if (NS_SUCCEEDED(rv2)) {
+ rv2 = mimeFinder->GetTypeFromFile(sigFile, sigContentType);
+ if (NS_SUCCEEDED(rv2)) {
+ if (StringBeginsWith(sigContentType, "image/"_ns,
+ nsCaseInsensitiveCStringComparator))
+ imageSig = true;
+ else if (sigContentType.Equals(
+ TEXT_HTML, nsCaseInsensitiveCStringComparator))
+ htmlSig = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Unless signature to be attached from file, use preference value;
+ // the htmlSigText value is always going to be treated as html if
+ // the htmlSigFormat pref is true, otherwise it is considered text
+ nsAutoString prefSigText;
+ if (identity && !attachFile) identity->GetHtmlSigText(prefSigText);
+ // Now, if they didn't even want to use a signature, we should
+ // just return nicely.
+ //
+ if ((!useSigFile && prefSigText.IsEmpty()) || NS_FAILED(rv)) return NS_OK;
+
+ static const char htmlBreak[] = "<br>";
+ static const char dashes[] = "-- ";
+ static const char htmlsigopen[] = "<div class=\"moz-signature\">";
+ static const char htmlsigclose[] = "</div>"; /* XXX: Due to a bug in
+ 4.x' HTML editor, it will not be able to
+ break this HTML sig, if quoted (for the user to
+ interleave a comment). */
+ static const char _preopen[] = "<pre class=\"moz-signature\" cols=%d>";
+ char* preopen;
+ static const char preclose[] = "</pre>";
+
+ int32_t wrapLength = 72; // setup default value in case GetWrapLength failed
+ GetWrapLength(&wrapLength);
+ preopen = PR_smprintf(_preopen, wrapLength);
+ if (!preopen) return NS_ERROR_OUT_OF_MEMORY;
+
+ bool paragraphMode =
+ mozilla::Preferences::GetBool("mail.compose.default_to_paragraph", false);
+
+ if (imageSig) {
+ // We have an image signature. If we're using the in HTML composer, we
+ // should put in the appropriate HTML for inclusion, otherwise, do nothing.
+ if (m_composeHTML) {
+ if (!paragraphMode) sigOutput.AppendLiteral(htmlBreak);
+ sigOutput.AppendLiteral(htmlsigopen);
+ if ((mType == nsIMsgCompType::NewsPost || !suppressSigSep) &&
+ (reply_on_top != 1 || sig_bottom || !aQuoted)) {
+ sigOutput.AppendLiteral(dashes);
+ }
+
+ sigOutput.AppendLiteral(htmlBreak);
+ sigOutput.AppendLiteral("<img src='");
+
+ nsCOMPtr<nsIURI> fileURI;
+ nsresult rv = NS_NewFileURI(getter_AddRefs(fileURI), sigFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString fileURL;
+ fileURI->GetSpec(fileURL);
+
+ nsString dataURL;
+ rv = DataURLForFileURL(NS_ConvertUTF8toUTF16(fileURL), dataURL);
+ if (NS_SUCCEEDED(rv)) {
+ sigOutput.Append(dataURL);
+ }
+ sigOutput.AppendLiteral("' border=0>");
+ sigOutput.AppendLiteral(htmlsigclose);
+ }
+ } else if (useSigFile) {
+ // is this a text sig with an HTML editor?
+ if ((m_composeHTML) && (!htmlSig)) {
+ ConvertTextToHTML(sigFile, sigData);
+ }
+ // is this a HTML sig with a text window?
+ else if ((!m_composeHTML) && (htmlSig)) {
+ ConvertHTMLToText(sigFile, sigData);
+ } else { // We have a match...
+ LoadDataFromFile(sigFile, sigData); // Get the data!
+ ReplaceFileURLs(sigData);
+ }
+ }
+
+ // if we have a prefSigText, append it to sigData.
+ if (!prefSigText.IsEmpty()) {
+ // set htmlSig if the pref is supposed to contain HTML code, defaults to
+ // false
+ rv = identity->GetHtmlSigFormat(&htmlSig);
+ if (NS_FAILED(rv)) htmlSig = false;
+
+ if (!m_composeHTML) {
+ if (htmlSig) ConvertBufToPlainText(prefSigText, false, true, true);
+ sigData.Append(prefSigText);
+ } else {
+ if (!htmlSig) {
+ nsCString escapedUTF8;
+ nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(prefSigText), escapedUTF8);
+ sigData.Append(NS_ConvertUTF8toUTF16(escapedUTF8));
+ } else {
+ ReplaceFileURLs(prefSigText);
+ sigData.Append(prefSigText);
+ }
+ }
+ }
+
+ // post-processing for plain-text signatures to ensure we end in CR, LF, or
+ // CRLF
+ if (!htmlSig && !m_composeHTML) {
+ int32_t sigLength = sigData.Length();
+ if (sigLength > 0 && !(sigData.CharAt(sigLength - 1) == '\r') &&
+ !(sigData.CharAt(sigLength - 1) == '\n'))
+ sigData.AppendLiteral(CRLF);
+ }
+
+ // Now that sigData holds data...if any, append it to the body in a nice
+ // looking manner
+ if (!sigData.IsEmpty()) {
+ if (m_composeHTML) {
+ if (!paragraphMode) sigOutput.AppendLiteral(htmlBreak);
+
+ if (htmlSig)
+ sigOutput.AppendLiteral(htmlsigopen);
+ else
+ sigOutput.Append(NS_ConvertASCIItoUTF16(preopen));
+ }
+
+ if ((reply_on_top != 1 || sig_bottom || !aQuoted) &&
+ sigData.Find(u"\r-- \r") < 0 && sigData.Find(u"\n-- \n") < 0 &&
+ sigData.Find(u"\n-- \r") < 0) {
+ nsDependentSubstring firstFourChars(sigData, 0, 4);
+
+ if ((mType == nsIMsgCompType::NewsPost || !suppressSigSep) &&
+ !(firstFourChars.EqualsLiteral("-- \n") ||
+ firstFourChars.EqualsLiteral("-- \r"))) {
+ sigOutput.AppendLiteral(dashes);
+
+ if (!m_composeHTML || !htmlSig)
+ sigOutput.AppendLiteral(CRLF);
+ else if (m_composeHTML)
+ sigOutput.AppendLiteral(htmlBreak);
+ }
+ }
+
+ // add CRLF before signature for plain-text mode if signature comes before
+ // quote
+ if (!m_composeHTML && reply_on_top == 1 && !sig_bottom && aQuoted)
+ sigOutput.AppendLiteral(CRLF);
+
+ sigOutput.Append(sigData);
+
+ if (m_composeHTML) {
+ if (htmlSig)
+ sigOutput.AppendLiteral(htmlsigclose);
+ else
+ sigOutput.AppendLiteral(preclose);
+ }
+ }
+
+ aMsgBody->Append(sigOutput);
+ PR_Free(preopen);
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::BuildBodyMessageAndSignature() {
+ nsresult rv = NS_OK;
+
+ //
+ // This should never happen...if it does, just bail out...
+ //
+ if (!m_editor) return NS_ERROR_FAILURE;
+
+ //
+ // Now, we have the body so we can just blast it into the
+ // composition editor window.
+ //
+ nsAutoString body;
+ m_compFields->GetBody(body);
+
+ // Some time we want to add a signature and sometime we won't.
+ // Let's figure that out now...
+ bool addSignature;
+ bool isQuoted = false;
+ switch (mType) {
+ case nsIMsgCompType::ForwardInline:
+ addSignature = true;
+ isQuoted = true;
+ break;
+ case nsIMsgCompType::New:
+ case nsIMsgCompType::MailToUrl: /* same as New */
+ case nsIMsgCompType::Reply: /* should not happen! but just in case */
+ case nsIMsgCompType::ReplyAll: /* should not happen! but just in case */
+ case nsIMsgCompType::ReplyToList: /* should not happen! but just in case */
+ case nsIMsgCompType::ForwardAsAttachment: /* should not happen! but just in
+ case */
+ case nsIMsgCompType::NewsPost:
+ case nsIMsgCompType::ReplyToGroup:
+ case nsIMsgCompType::ReplyToSender:
+ case nsIMsgCompType::ReplyToSenderAndGroup:
+ addSignature = true;
+ break;
+
+ case nsIMsgCompType::Draft:
+ case nsIMsgCompType::Template:
+ case nsIMsgCompType::Redirect:
+ case nsIMsgCompType::EditAsNew:
+ addSignature = false;
+ break;
+
+ default:
+ addSignature = false;
+ break;
+ }
+
+ nsAutoString tSignature;
+ if (addSignature) ProcessSignature(m_identity, isQuoted, &tSignature);
+
+ // if type is new, but we have body, this is probably a mapi send, so we need
+ // to replace '\n' with <br> so that the line breaks won't be lost by html. if
+ // mailtourl, do the same.
+ if (m_composeHTML &&
+ (mType == nsIMsgCompType::New || mType == nsIMsgCompType::MailToUrl))
+ body.ReplaceSubstring(u"\n"_ns, u"<br>"_ns);
+
+ // Restore flowed text wrapping for Drafts/Templates.
+ // Look for unquoted lines - if we have an unquoted line
+ // that ends in a space, join this line with the next one
+ // by removing the end of line char(s).
+ int32_t wrapping_enabled = 0;
+ GetWrapLength(&wrapping_enabled);
+ if (!m_composeHTML && wrapping_enabled) {
+ bool quote = false;
+ for (uint32_t i = 0; i < body.Length(); i++) {
+ if (i == 0 || body[i - 1] == '\n') // newline
+ {
+ if (body[i] == '>') {
+ quote = true;
+ continue;
+ }
+ nsString s(Substring(body, i, 10));
+ if (StringBeginsWith(s, u"-- \r"_ns) ||
+ StringBeginsWith(s, u"-- \n"_ns)) {
+ i += 4;
+ continue;
+ }
+ if (StringBeginsWith(s, u"- -- \r"_ns) ||
+ StringBeginsWith(s, u"- -- \n"_ns)) {
+ i += 6;
+ continue;
+ }
+ }
+ if (body[i] == '\n' && i > 1) {
+ if (quote) {
+ quote = false;
+ continue; // skip quoted lines
+ }
+ uint32_t j = i - 1; // look backward for space
+ if (body[j] == '\r') j--;
+ if (body[j] == ' ') // join this line with next one
+ body.Cut(j + 1, i - j); // remove CRLF
+ }
+ }
+ }
+
+ nsString empty;
+ rv = ConvertAndLoadComposeWindow(empty, body, tSignature, false,
+ m_composeHTML);
+
+ return rv;
+}
+
+nsresult nsMsgCompose::NotifyStateListeners(int32_t aNotificationType,
+ nsresult aResult) {
+ nsTObserverArray<nsCOMPtr<nsIMsgComposeStateListener>>::ForwardIterator iter(
+ mStateListeners);
+ nsCOMPtr<nsIMsgComposeStateListener> thisListener;
+
+ while (iter.HasMore()) {
+ thisListener = iter.GetNext();
+
+ switch (aNotificationType) {
+ case nsIMsgComposeNotificationType::ComposeFieldsReady:
+ thisListener->NotifyComposeFieldsReady();
+ break;
+
+ case nsIMsgComposeNotificationType::ComposeProcessDone:
+ thisListener->ComposeProcessDone(aResult);
+ break;
+
+ case nsIMsgComposeNotificationType::SaveInFolderDone:
+ thisListener->SaveInFolderDone(m_folderName.get());
+ break;
+
+ case nsIMsgComposeNotificationType::ComposeBodyReady:
+ thisListener->NotifyComposeBodyReady();
+ break;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown notification");
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgCompose::AttachmentPrettyName(const nsACString& scheme,
+ const char* charset,
+ nsACString& _retval) {
+ nsresult rv;
+
+ if (StringHead(scheme, 5).LowerCaseEqualsLiteral("file:")) {
+ nsCOMPtr<nsIFile> file;
+ rv = NS_GetFileFromURLSpec(scheme, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString leafName;
+ rv = file->GetLeafName(leafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF16toUTF8(leafName, _retval);
+ return rv;
+ }
+
+ nsCOMPtr<nsITextToSubURI> textToSubURI =
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString retUrl;
+ rv = textToSubURI->UnEscapeURIForUI(scheme, retUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(retUrl, _retval);
+ } else {
+ _retval.Assign(scheme);
+ }
+ if (StringHead(scheme, 5).LowerCaseEqualsLiteral("http:")) _retval.Cut(0, 7);
+
+ return NS_OK;
+}
+
+/**
+ * Retrieve address book directories and mailing lists.
+ *
+ * @param aDirUri directory URI
+ * @param allDirectoriesArray retrieved directories and sub-directories
+ * @param allMailListArray retrieved maillists
+ */
+nsresult nsMsgCompose::GetABDirAndMailLists(
+ const nsACString& aDirUri, nsCOMArray<nsIAbDirectory>& aDirArray,
+ nsTArray<nsMsgMailList>& aMailListArray) {
+ static bool collectedAddressbookFound = false;
+
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDirUri.Equals(kAllDirectoryRoot)) {
+ nsTArray<RefPtr<nsIAbDirectory>> directories;
+ rv = abManager->GetDirectories(directories);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t count = directories.Length();
+ nsCString uri;
+ for (uint32_t i = 0; i < count; i++) {
+ rv = directories[i]->GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t pos;
+ if (uri.EqualsLiteral(kPersonalAddressbookUri)) {
+ pos = 0;
+ } else {
+ uint32_t count = aDirArray.Count();
+
+ if (uri.EqualsLiteral(kCollectedAddressbookUri)) {
+ collectedAddressbookFound = true;
+ pos = count;
+ } else {
+ if (collectedAddressbookFound && count > 1) {
+ pos = count - 1;
+ } else {
+ pos = count;
+ }
+ }
+ }
+
+ aDirArray.InsertObjectAt(directories[i], pos);
+ rv = GetABDirAndMailLists(uri, aDirArray, aMailListArray);
+ }
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(aDirUri, getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIAbDirectory>> subDirectories;
+ rv = directory->GetChildNodes(subDirectories);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsIAbDirectory* subDirectory : subDirectories) {
+ bool bIsMailList;
+ if (NS_SUCCEEDED(subDirectory->GetIsMailList(&bIsMailList)) &&
+ bIsMailList) {
+ aMailListArray.AppendElement(subDirectory);
+ }
+ }
+ return rv;
+}
+
+/**
+ * Comparator for use with nsTArray::IndexOf to find a recipient.
+ * This comparator will check if an "address" is a mail list or not.
+ */
+struct nsMsgMailListComparator {
+ // A mail list will have one of the formats
+ // 1) "mName <mDescription>" when the list has a description
+ // 2) "mName <mName>" when the list lacks description
+ // A recipient is of the form "mName <mEmail>" - for equality the list
+ // name must be the same. The recipient "email" must match the list name for
+ // case 1, and the list description for case 2.
+ bool Equals(const nsMsgMailList& mailList,
+ const nsMsgRecipient& recipient) const {
+ if (!mailList.mName.Equals(recipient.mName,
+ nsCaseInsensitiveStringComparator))
+ return false;
+ return mailList.mDescription.IsEmpty()
+ ? mailList.mName.Equals(recipient.mEmail,
+ nsCaseInsensitiveStringComparator)
+ : mailList.mDescription.Equals(
+ recipient.mEmail, nsCaseInsensitiveStringComparator);
+ }
+};
+
+/**
+ * Comparator for use with nsTArray::IndexOf to find a recipient.
+ */
+struct nsMsgRecipientComparator {
+ bool Equals(const nsMsgRecipient& recipient,
+ const nsMsgRecipient& recipientToFind) const {
+ if (!recipient.mEmail.Equals(recipientToFind.mEmail,
+ nsCaseInsensitiveStringComparator))
+ return false;
+
+ if (!recipient.mName.Equals(recipientToFind.mName,
+ nsCaseInsensitiveStringComparator))
+ return false;
+
+ return true;
+ }
+};
+
+/**
+ * This function recursively resolves a mailing list and returns individual
+ * email addresses. Nested lists are supported. It maintains an array of
+ * already visited mailing lists to avoid endless recursion.
+ *
+ * @param aMailList the list
+ * @param allDirectoriesArray all directories
+ * @param allMailListArray all maillists
+ * @param mailListProcessed maillists processed (to avoid recursive lists)
+ * @param aListMembers list members
+ */
+nsresult nsMsgCompose::ResolveMailList(
+ nsIAbDirectory* aMailList, nsCOMArray<nsIAbDirectory>& allDirectoriesArray,
+ nsTArray<nsMsgMailList>& allMailListArray,
+ nsTArray<nsMsgMailList>& mailListProcessed,
+ nsTArray<nsMsgRecipient>& aListMembers) {
+ nsresult rv = NS_OK;
+
+ nsTArray<RefPtr<nsIAbCard>> mailListAddresses;
+ rv = aMailList->GetChildCards(mailListAddresses);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIAbCard* existingCard : mailListAddresses) {
+ nsMsgRecipient newRecipient;
+
+ rv = existingCard->GetDisplayName(newRecipient.mName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = existingCard->GetPrimaryEmail(newRecipient.mEmail);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (newRecipient.mName.IsEmpty() && newRecipient.mEmail.IsEmpty()) {
+ continue;
+ }
+
+ // First check if it's a mailing list.
+ size_t index =
+ allMailListArray.IndexOf(newRecipient, 0, nsMsgMailListComparator());
+ if (index != allMailListArray.NoIndex &&
+ allMailListArray[index].mDirectory) {
+ // Check if maillist processed.
+ if (mailListProcessed.Contains(newRecipient, nsMsgMailListComparator())) {
+ continue;
+ }
+
+ nsCOMPtr<nsIAbDirectory> directory2(allMailListArray[index].mDirectory);
+
+ // Add mailList to mailListProcessed.
+ mailListProcessed.AppendElement(directory2);
+
+ // Resolve mailList members.
+ rv = ResolveMailList(directory2, allDirectoriesArray, allMailListArray,
+ mailListProcessed, aListMembers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ continue;
+ }
+
+ // Check if recipient is in aListMembers.
+ if (aListMembers.Contains(newRecipient, nsMsgRecipientComparator())) {
+ continue;
+ }
+
+ // Now we need to insert the new address into the list of recipients.
+ newRecipient.mCard = existingCard;
+ newRecipient.mDirectory = aMailList;
+
+ aListMembers.AppendElement(newRecipient);
+ }
+
+ return rv;
+}
+
+/**
+ * Lookup the recipients as specified in the compose fields (To, Cc, Bcc)
+ * in the address books and return an array of individual recipients.
+ * Mailing lists are replaced by the cards they contain, nested and recursive
+ * lists are taken care of, recipients contained in multiple lists are only
+ * added once.
+ *
+ * @param recipientsList (out) recipient array
+ */
+nsresult nsMsgCompose::LookupAddressBook(RecipientsArray& recipientsList) {
+ nsresult rv = NS_OK;
+
+ // First, build some arrays with the original recipients.
+
+ nsAutoString originalRecipients[MAX_OF_RECIPIENT_ARRAY];
+ m_compFields->GetTo(originalRecipients[0]);
+ m_compFields->GetCc(originalRecipients[1]);
+ m_compFields->GetBcc(originalRecipients[2]);
+
+ for (uint32_t i = 0; i < MAX_OF_RECIPIENT_ARRAY; ++i) {
+ if (originalRecipients[i].IsEmpty()) continue;
+
+ rv = m_compFields->SplitRecipientsEx(originalRecipients[i],
+ recipientsList[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Then look them up in the Addressbooks
+ bool stillNeedToSearch = true;
+ nsCOMPtr<nsIAbDirectory> abDirectory;
+ nsCOMPtr<nsIAbCard> existingCard;
+ nsTArray<nsMsgMailList> mailListArray;
+ nsTArray<nsMsgMailList> mailListProcessed;
+
+ nsCOMArray<nsIAbDirectory> addrbookDirArray;
+ rv = GetABDirAndMailLists(nsLiteralCString(kAllDirectoryRoot),
+ addrbookDirArray, mailListArray);
+ if (NS_FAILED(rv)) return rv;
+
+ nsString dirPath;
+ uint32_t nbrAddressbook = addrbookDirArray.Count();
+
+ for (uint32_t k = 0; k < nbrAddressbook && stillNeedToSearch; ++k) {
+ // Avoid recursive mailing lists.
+ if (abDirectory && (addrbookDirArray[k] == abDirectory)) {
+ stillNeedToSearch = false;
+ break;
+ }
+
+ abDirectory = addrbookDirArray[k];
+ if (!abDirectory) continue;
+
+ stillNeedToSearch = false;
+ for (uint32_t i = 0; i < MAX_OF_RECIPIENT_ARRAY; i++) {
+ mailListProcessed.Clear();
+
+ // Note: We check this each time to allow for length changes.
+ for (uint32_t j = 0; j < recipientsList[i].Length(); j++) {
+ nsMsgRecipient& recipient = recipientsList[i][j];
+ if (!recipient.mDirectory) {
+ // First check if it's a mailing list.
+ size_t index =
+ mailListArray.IndexOf(recipient, 0, nsMsgMailListComparator());
+ if (index != mailListArray.NoIndex &&
+ mailListArray[index].mDirectory) {
+ // Check mailList Processed.
+ if (mailListProcessed.Contains(recipient,
+ nsMsgMailListComparator())) {
+ // Remove from recipientsList.
+ recipientsList[i].RemoveElementAt(j--);
+ continue;
+ }
+
+ nsCOMPtr<nsIAbDirectory> directory(mailListArray[index].mDirectory);
+
+ // Add mailList to mailListProcessed.
+ mailListProcessed.AppendElement(directory);
+
+ // Resolve mailList members.
+ nsTArray<nsMsgRecipient> members;
+ rv = ResolveMailList(directory, addrbookDirArray, mailListArray,
+ mailListProcessed, members);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove mailList from recipientsList.
+ recipientsList[i].RemoveElementAt(j);
+
+ // Merge members into recipientsList[i].
+ uint32_t pos = 0;
+ for (uint32_t c = 0; c < members.Length(); c++) {
+ nsMsgRecipient& member = members[c];
+ if (!recipientsList[i].Contains(member,
+ nsMsgRecipientComparator())) {
+ recipientsList[i].InsertElementAt(j + pos, member);
+ pos++;
+ }
+ }
+ } else {
+ // Find a card that contains this e-mail address.
+ rv = abDirectory->CardForEmailAddress(
+ NS_ConvertUTF16toUTF8(recipient.mEmail),
+ getter_AddRefs(existingCard));
+ if (NS_SUCCEEDED(rv) && existingCard) {
+ recipient.mCard = existingCard;
+ recipient.mDirectory = abDirectory;
+ } else {
+ stillNeedToSearch = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::ExpandMailingLists() {
+ RecipientsArray recipientsList;
+ nsresult rv = LookupAddressBook(recipientsList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Reset the final headers with the expanded mailing lists.
+ nsAutoString recipientsStr;
+
+ for (int i = 0; i < MAX_OF_RECIPIENT_ARRAY; ++i) {
+ uint32_t nbrRecipients = recipientsList[i].Length();
+ if (nbrRecipients == 0) continue;
+ recipientsStr.Truncate();
+
+ // Note: We check this each time to allow for length changes.
+ for (uint32_t j = 0; j < recipientsList[i].Length(); ++j) {
+ nsMsgRecipient& recipient = recipientsList[i][j];
+
+ if (!recipientsStr.IsEmpty()) recipientsStr.Append(char16_t(','));
+ nsAutoString address;
+ MakeMimeAddress(recipient.mName, recipient.mEmail, address);
+ recipientsStr.Append(address);
+
+ if (recipient.mCard) {
+ bool readOnly;
+ rv = recipient.mDirectory->GetReadOnly(&readOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bump the popularity index for this card since we are about to send
+ // e-mail to it.
+ if (!readOnly) {
+ uint32_t popularityIndex = 0;
+ if (NS_FAILED(recipient.mCard->GetPropertyAsUint32(
+ kPopularityIndexProperty, &popularityIndex))) {
+ // TB 2 wrote the popularity value as hex, so if we get here,
+ // then we've probably got a hex value. We'll convert it back
+ // to decimal, as that's the best we can do.
+
+ nsCString hexPopularity;
+ if (NS_SUCCEEDED(recipient.mCard->GetPropertyAsAUTF8String(
+ kPopularityIndexProperty, hexPopularity))) {
+ nsresult errorCode = NS_OK;
+ popularityIndex = hexPopularity.ToInteger(&errorCode, 16);
+ if (NS_FAILED(errorCode))
+ // We failed, just set it to zero.
+ popularityIndex = 0;
+ } else
+ // We couldn't get it as a string either, so just reset to zero.
+ popularityIndex = 0;
+ }
+
+ recipient.mCard->SetPropertyAsUint32(kPopularityIndexProperty,
+ ++popularityIndex);
+ recipient.mDirectory->ModifyCard(recipient.mCard);
+ }
+ }
+ }
+
+ switch (i) {
+ case 0:
+ m_compFields->SetTo(recipientsStr);
+ break;
+ case 1:
+ m_compFields->SetCc(recipientsStr);
+ break;
+ case 2:
+ m_compFields->SetBcc(recipientsStr);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Decides which tags trigger which convertible mode,
+ * i.e. here is the logic for BodyConvertible.
+ * Note: Helper function. Parameters are not checked.
+ */
+void nsMsgCompose::TagConvertible(Element* node, int32_t* _retval) {
+ *_retval = nsIMsgCompConvertible::No;
+
+ nsAutoString element;
+ element = node->NodeName();
+
+ // A style attribute on any element can change layout in any way,
+ // so that is not convertible.
+ nsAutoString attribValue;
+ node->GetAttribute(u"style"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ *_retval = nsIMsgCompConvertible::No;
+ return;
+ }
+
+ // moz-* classes are used internally by the editor and mail composition
+ // (like moz-cite-prefix or moz-signature). Those can be discarded.
+ // But any other ones are unconvertible. Style can be attached to them or any
+ // other context (e.g. in microformats).
+ node->GetAttribute(u"class"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ if (StringBeginsWith(attribValue, u"moz-"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ // We assume that anything with a moz-* class is convertible regardless of
+ // the tag, because we add, for example, class="moz-signature" to HTML
+ // messages and we still want to be able to downgrade them.
+ *_retval = nsIMsgCompConvertible::Plain;
+ } else {
+ *_retval = nsIMsgCompConvertible::No;
+ }
+
+ return;
+ }
+
+ // ID attributes can contain attached style/context or be target of links
+ // so we should preserve them.
+ node->GetAttribute(u"id"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ *_retval = nsIMsgCompConvertible::No;
+ return;
+ }
+
+ // Alignment is not convertible to plaintext; editor currently uses this.
+ node->GetAttribute(u"align"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ *_retval = nsIMsgCompConvertible::No;
+ return;
+ }
+
+ // Title attribute is not convertible to plaintext;
+ // this also preserves any links with titles.
+ node->GetAttribute(u"title"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ *_retval = nsIMsgCompConvertible::No;
+ return;
+ }
+
+ // Treat <font face="monospace"> as converible to plaintext.
+ if (element.LowerCaseEqualsLiteral("font")) {
+ node->GetAttribute(u"size"_ns, attribValue);
+ if (!attribValue.IsEmpty()) {
+ *_retval = nsIMsgCompConvertible::No;
+ return;
+ }
+ node->GetAttribute(u"face"_ns, attribValue);
+ if (attribValue.LowerCaseEqualsLiteral("monospace")) {
+ *_retval = nsIMsgCompConvertible::Plain;
+ }
+ }
+
+ if ( // Considered convertible to plaintext: Some "simple" elements
+ // without non-convertible attributes like style, class, id,
+ // or align (see above).
+ element.LowerCaseEqualsLiteral("br") ||
+ element.LowerCaseEqualsLiteral("p") ||
+ element.LowerCaseEqualsLiteral("tt") ||
+ element.LowerCaseEqualsLiteral("html") ||
+ element.LowerCaseEqualsLiteral("head") ||
+ element.LowerCaseEqualsLiteral("meta") ||
+ element.LowerCaseEqualsLiteral("title")) {
+ *_retval = nsIMsgCompConvertible::Plain;
+ } else if (
+ // element.LowerCaseEqualsLiteral("blockquote") || // see below
+ element.LowerCaseEqualsLiteral("ul") ||
+ element.LowerCaseEqualsLiteral("ol") ||
+ element.LowerCaseEqualsLiteral("li") ||
+ element.LowerCaseEqualsLiteral("dl") ||
+ element.LowerCaseEqualsLiteral("dt") ||
+ element.LowerCaseEqualsLiteral("dd")) {
+ *_retval = nsIMsgCompConvertible::Yes;
+ } else if (
+ // element.LowerCaseEqualsLiteral("a") || // see below
+ element.LowerCaseEqualsLiteral("h1") ||
+ element.LowerCaseEqualsLiteral("h2") ||
+ element.LowerCaseEqualsLiteral("h3") ||
+ element.LowerCaseEqualsLiteral("h4") ||
+ element.LowerCaseEqualsLiteral("h5") ||
+ element.LowerCaseEqualsLiteral("h6") ||
+ element.LowerCaseEqualsLiteral("hr") ||
+ element.LowerCaseEqualsLiteral("pre") ||
+ (mConvertStructs && (element.LowerCaseEqualsLiteral("em") ||
+ element.LowerCaseEqualsLiteral("strong") ||
+ element.LowerCaseEqualsLiteral("code") ||
+ element.LowerCaseEqualsLiteral("b") ||
+ element.LowerCaseEqualsLiteral("i") ||
+ element.LowerCaseEqualsLiteral("u")))) {
+ *_retval = nsIMsgCompConvertible::Altering;
+ } else if (element.LowerCaseEqualsLiteral("body")) {
+ *_retval = nsIMsgCompConvertible::Plain;
+
+ if (node->HasAttribute(u"background"_ns) || // There is a background image
+ node->HasAttribute(
+ u"dir"_ns)) { // dir=rtl attributes should not downconvert
+ *_retval = nsIMsgCompConvertible::No;
+ } else {
+ nsAutoString color;
+ if (node->HasAttribute(u"text"_ns)) {
+ node->GetAttribute(u"text"_ns, color);
+ if (!color.EqualsLiteral("#000000"))
+ *_retval = nsIMsgCompConvertible::Altering;
+ }
+ if (*_retval != nsIMsgCompConvertible::Altering && // small optimization
+ node->HasAttribute(u"bgcolor"_ns)) {
+ node->GetAttribute(u"bgcolor"_ns, color);
+ if (!color.LowerCaseEqualsLiteral("#ffffff"))
+ *_retval = nsIMsgCompConvertible::Altering;
+ }
+ }
+
+ // ignore special color setting for link, vlink and alink at this point.
+ } else if (element.LowerCaseEqualsLiteral("blockquote")) {
+ // Skip <blockquote type="cite">
+ *_retval = nsIMsgCompConvertible::Yes;
+
+ node->GetAttribute(u"type"_ns, attribValue);
+ if (attribValue.LowerCaseEqualsLiteral("cite")) {
+ *_retval = nsIMsgCompConvertible::Plain;
+ }
+ } else if (element.LowerCaseEqualsLiteral("div") ||
+ element.LowerCaseEqualsLiteral("span") ||
+ element.LowerCaseEqualsLiteral("a")) {
+ // Do some special checks for these tags. They are inside this |else if|
+ // for performance reasons.
+
+ // Maybe, it's an <a> element inserted by another recognizer (e.g. 4.x')
+ if (element.LowerCaseEqualsLiteral("a")) {
+ // Ignore anchor tag, if the URI is the same as the text
+ // (as inserted by recognizers).
+ *_retval = nsIMsgCompConvertible::Altering;
+
+ nsAutoString hrefValue;
+ node->GetAttribute(u"href"_ns, hrefValue);
+ nsINodeList* children = node->ChildNodes();
+ if (children->Length() > 0) {
+ nsINode* pItem = children->Item(0);
+ nsAutoString textValue;
+ pItem->GetNodeValue(textValue);
+ if (textValue == hrefValue) *_retval = nsIMsgCompConvertible::Plain;
+ }
+ }
+
+ // Lastly, test, if it is just a "simple" <div> or <span>
+ else if (element.LowerCaseEqualsLiteral("div") ||
+ element.LowerCaseEqualsLiteral("span")) {
+ *_retval = nsIMsgCompConvertible::Plain;
+ }
+ }
+}
+
+/**
+ * Note: Helper function. Parameters are not checked.
+ */
+NS_IMETHODIMP
+nsMsgCompose::NodeTreeConvertible(Element* node, int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ int32_t result;
+
+ // Check this node
+ TagConvertible(node, &result);
+
+ // Walk tree recursively to check the children.
+ nsINodeList* children = node->ChildNodes();
+ for (uint32_t i = 0; i < children->Length(); i++) {
+ nsINode* pItem = children->Item(i);
+ // We assume all nodes that are not elements are convertible,
+ // so only test elements.
+ nsCOMPtr<Element> domElement = do_QueryInterface(pItem);
+ if (domElement) {
+ int32_t curresult;
+ NodeTreeConvertible(domElement, &curresult);
+
+ if (curresult > result) result = curresult;
+ }
+ }
+
+ *_retval = result;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::BodyConvertible(int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ NS_ENSURE_STATE(m_editor);
+
+ nsCOMPtr<Document> rootDocument;
+ nsresult rv = m_editor->GetDocument(getter_AddRefs(rootDocument));
+ if (NS_FAILED(rv)) return rv;
+ if (!rootDocument) return NS_ERROR_UNEXPECTED;
+
+ // get the top level element, which contains <html>
+ nsCOMPtr<Element> rootElement = rootDocument->GetDocumentElement();
+ if (!rootElement) return NS_ERROR_UNEXPECTED;
+ NodeTreeConvertible(rootElement, _retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompose::GetIdentity(nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ NS_IF_ADDREF(*aIdentity = m_identity);
+ return NS_OK;
+}
+
+/**
+ * Position above the quote, that is either <blockquote> or
+ * <div class="moz-cite-prefix"> or <div class="moz-forward-container">
+ * in an inline-forwarded message.
+ */
+nsresult nsMsgCompose::MoveToAboveQuote(void) {
+ RefPtr<Element> rootElement;
+ nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement));
+ if (NS_FAILED(rv) || !rootElement) {
+ return rv;
+ }
+
+ nsCOMPtr<nsINode> node;
+ nsAutoString attributeName;
+ nsAutoString attributeValue;
+ nsAutoString tagLocalName;
+ attributeName.AssignLiteral("class");
+
+ RefPtr<nsINode> rootElement2 = rootElement;
+ node = rootElement2->GetFirstChild();
+ while (node) {
+ nsCOMPtr<Element> element = do_QueryInterface(node);
+ if (element) {
+ // First check for <blockquote>. This will most likely not trigger
+ // since well-behaved quotes are preceded by a cite prefix.
+ tagLocalName = node->LocalName();
+ if (tagLocalName.EqualsLiteral("blockquote")) {
+ break;
+ }
+
+ // Get the class value.
+ element->GetAttribute(attributeName, attributeValue);
+
+ // Now check for the cite prefix, so an element with
+ // class="moz-cite-prefix".
+ if (attributeValue.LowerCaseFindASCII("moz-cite-prefix") != kNotFound) {
+ break;
+ }
+
+ // Next check for forwarded content.
+ // The forwarded part is inside an element with
+ // class="moz-forward-container".
+ if (attributeValue.LowerCaseFindASCII("moz-forward-container") !=
+ kNotFound) {
+ break;
+ }
+ }
+
+ node = node->GetNextSibling();
+ if (!node) {
+ // No further siblings found, so we didn't find what we were looking for.
+ rv = NS_OK;
+ break;
+ }
+ }
+
+ // Now position. If no quote was found, we position to the very front.
+ int32_t offset = 0;
+ if (node) {
+ rv = GetChildOffset(node, rootElement2, offset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ RefPtr<Selection> selection;
+ m_editor->GetSelection(getter_AddRefs(selection));
+ if (selection) rv = selection->CollapseInLimiter(rootElement, offset);
+
+ return rv;
+}
+
+/**
+ * nsEditor::BeginningOfDocument() will position to the beginning of the
+ * document before the first editable element. It will position into a
+ * container. We need to be at the very front.
+ */
+nsresult nsMsgCompose::MoveToBeginningOfDocument(void) {
+ RefPtr<Element> rootElement;
+ nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement));
+ if (NS_FAILED(rv) || !rootElement) {
+ return rv;
+ }
+
+ RefPtr<Selection> selection;
+ m_editor->GetSelection(getter_AddRefs(selection));
+ if (selection) rv = selection->CollapseInLimiter(rootElement, 0);
+
+ return rv;
+}
+
+/**
+ * M-C's nsEditor::EndOfDocument() will position to the end of the document
+ * but it will position into a container. We really need to position
+ * after the last container so we don't accidentally position into a
+ * <blockquote>. That's why we use our own function.
+ */
+nsresult nsMsgCompose::MoveToEndOfDocument(void) {
+ int32_t offset;
+ RefPtr<Element> rootElement;
+ nsCOMPtr<nsINode> lastNode;
+ nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement));
+ if (NS_FAILED(rv) || !rootElement) {
+ return rv;
+ }
+
+ RefPtr<nsINode> rootElement2 = rootElement;
+ lastNode = rootElement2->GetLastChild();
+ if (!lastNode) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ rv = GetChildOffset(lastNode, rootElement2, offset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ RefPtr<Selection> selection;
+ m_editor->GetSelection(getter_AddRefs(selection));
+ if (selection) rv = selection->CollapseInLimiter(rootElement, offset + 1);
+
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMsgCompose::SetIdentity(nsIMsgIdentity* aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+
+ m_identity = aIdentity;
+
+ nsresult rv;
+
+ if (!m_editor) return NS_ERROR_FAILURE;
+
+ RefPtr<Element> rootElement;
+ rv = m_editor->GetRootElement(getter_AddRefs(rootElement));
+ if (NS_FAILED(rv) || !rootElement) return rv;
+
+ // First look for the current signature, if we have one
+ nsCOMPtr<nsINode> lastNode;
+ nsCOMPtr<nsINode> node;
+ nsCOMPtr<nsINode> tempNode;
+ nsAutoString tagLocalName;
+
+ RefPtr<nsINode> rootElement2 = rootElement;
+ lastNode = rootElement2->GetLastChild();
+ if (lastNode) {
+ node = lastNode;
+ // In html, the signature is inside an element with
+ // class="moz-signature"
+ bool signatureFound = false;
+ nsAutoString attributeName;
+ attributeName.AssignLiteral("class");
+
+ while (node) {
+ nsCOMPtr<Element> element = do_QueryInterface(node);
+ if (element) {
+ nsAutoString attributeValue;
+
+ element->GetAttribute(attributeName, attributeValue);
+
+ if (attributeValue.LowerCaseFindASCII("moz-signature") != kNotFound) {
+ signatureFound = true;
+ break;
+ }
+ }
+ node = node->GetPreviousSibling();
+ }
+
+ if (signatureFound) {
+ nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference.
+ editor->BeginTransaction();
+ tempNode = node->GetPreviousSibling();
+ rv = editor->DeleteNode(node);
+ if (NS_FAILED(rv)) {
+ editor->EndTransaction();
+ return rv;
+ }
+
+ // Also, remove the <br> right before the signature.
+ if (tempNode) {
+ tagLocalName = tempNode->LocalName();
+ if (tagLocalName.EqualsLiteral("br")) editor->DeleteNode(tempNode);
+ }
+ editor->EndTransaction();
+ }
+ }
+
+ if (!CheckIncludeSignaturePrefs(aIdentity)) return NS_OK;
+
+ // Then add the new one if needed
+ nsAutoString aSignature;
+
+ // No delimiter needed if not a compose window
+ bool isQuoted;
+ switch (mType) {
+ case nsIMsgCompType::New:
+ case nsIMsgCompType::NewsPost:
+ case nsIMsgCompType::MailToUrl:
+ case nsIMsgCompType::ForwardAsAttachment:
+ isQuoted = false;
+ break;
+ default:
+ isQuoted = true;
+ break;
+ }
+
+ ProcessSignature(aIdentity, isQuoted, &aSignature);
+
+ if (!aSignature.IsEmpty()) {
+ TranslateLineEnding(aSignature);
+ nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference.
+
+ editor->BeginTransaction();
+ int32_t reply_on_top = 0;
+ bool sig_bottom = true;
+ aIdentity->GetReplyOnTop(&reply_on_top);
+ aIdentity->GetSigBottom(&sig_bottom);
+ bool sigOnTop = (reply_on_top == 1 && !sig_bottom);
+ if (sigOnTop && isQuoted) {
+ rv = MoveToAboveQuote();
+ } else {
+ // Note: New messages aren't quoted so we always move to the end.
+ rv = MoveToEndOfDocument();
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ if (m_composeHTML) {
+ bool oldAllow;
+ GetAllowRemoteContent(&oldAllow);
+ SetAllowRemoteContent(true);
+ rv = MOZ_KnownLive(editor->AsHTMLEditor())->InsertHTML(aSignature);
+ SetAllowRemoteContent(oldAllow);
+ } else {
+ rv = editor->InsertLineBreak();
+ InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns);
+ }
+ }
+ editor->EndTransaction();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgCompose::CheckCharsetConversion(nsIMsgIdentity* identity,
+ char** fallbackCharset,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(identity);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ // Kept around for legacy reasons. This method is supposed to check that the
+ // headers can be converted to the appropriate charset, but we don't support
+ // encoding headers to non-UTF-8, so this is now moot.
+ if (fallbackCharset) *fallbackCharset = nullptr;
+ *_retval = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompose::GetDeliverMode(MSG_DeliverMode* aDeliverMode) {
+ NS_ENSURE_ARG_POINTER(aDeliverMode);
+ *aDeliverMode = mDeliverMode;
+ return NS_OK;
+}
+
+void nsMsgCompose::DeleteTmpAttachments() {
+ if (mTmpAttachmentsDeleted || m_window) {
+ // Don't delete tmp attachments if compose window is still open, e.g. saving
+ // a draft.
+ return;
+ }
+ mTmpAttachmentsDeleted = true;
+ // Remove temporary attachment files, e.g. key.asc when attaching public key.
+ nsTArray<RefPtr<nsIMsgAttachment>> attachments;
+ m_compFields->GetAttachments(attachments);
+ for (nsIMsgAttachment* attachment : attachments) {
+ bool isTemporary;
+ attachment->GetTemporary(&isTemporary);
+ bool sentViaCloud;
+ attachment->GetSendViaCloud(&sentViaCloud);
+ if (isTemporary && !sentViaCloud) {
+ nsCString url;
+ attachment->GetUrl(url);
+ nsCOMPtr<nsIFile> urlFile;
+ nsresult rv = NS_GetFileFromURLSpec(url, getter_AddRefs(urlFile));
+ if (NS_SUCCEEDED(rv)) {
+ urlFile->Remove(false);
+ }
+ }
+ }
+}
+
+nsMsgMailList::nsMsgMailList(nsIAbDirectory* directory)
+ : mDirectory(directory) {
+ mDirectory->GetDirName(mName);
+ mDirectory->GetDescription(mDescription);
+
+ if (mDescription.IsEmpty()) mDescription = mName;
+
+ mDirectory = directory;
+}
diff --git a/comm/mailnews/compose/src/nsMsgCompose.h b/comm/mailnews/compose/src/nsMsgCompose.h
new file mode 100644
index 0000000000..fe9b9724c8
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCompose.h
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgCompose_H_
+#define _nsMsgCompose_H_
+
+#include "nsIMsgCompose.h"
+#include "nsCOMArray.h"
+#include "nsTObserverArray.h"
+#include "nsWeakReference.h"
+#include "nsMsgCompFields.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgQuote.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIBaseWindow.h"
+#include "nsIAbDirectory.h"
+#include "nsIWebProgressListener.h"
+#include "nsIMimeConverter.h"
+#include "nsIMsgFolder.h"
+#include "mozIDOMWindow.h"
+#include "mozilla/dom/Element.h"
+
+// Forward declares
+class QuotingOutputStreamListener;
+class nsMsgComposeSendListener;
+class nsIEditor;
+class nsIArray;
+struct nsMsgMailList;
+
+class nsMsgCompose : public nsIMsgCompose, public nsSupportsWeakReference {
+ public:
+ nsMsgCompose();
+
+ /* this macro defines QueryInterface, AddRef and Release for this class */
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /*** nsIMsgCompose pure virtual functions */
+ NS_DECL_NSIMSGCOMPOSE
+
+ /* nsIMsgSendListener interface */
+ NS_DECL_NSIMSGSENDLISTENER
+
+ protected:
+ virtual ~nsMsgCompose();
+
+ // Deal with quoting issues...
+ nsresult QuoteOriginalMessage(); // New template
+ nsresult SetQuotingToFollow(bool aVal);
+ nsresult ConvertHTMLToText(nsIFile* aSigFile, nsString& aSigData);
+ nsresult ConvertTextToHTML(nsIFile* aSigFile, nsString& aSigData);
+ bool IsEmbeddedObjectSafe(const char* originalScheme,
+ const char* originalHost, const char* originalPath,
+ mozilla::dom::Element* element);
+ nsresult TagEmbeddedObjects(nsIEditor* aEditor);
+
+ nsCString mOriginalMsgURI; // used so we can mark message disposition flags
+ // after we send the message
+
+ int32_t mWhatHolder;
+
+ nsresult LoadDataFromFile(nsIFile* file, nsString& sigData,
+ bool aAllowUTF8 = true, bool aAllowUTF16 = true);
+
+ bool CheckIncludeSignaturePrefs(nsIMsgIdentity* identity);
+ // m_folderName to store the value of the saved drafts folder.
+ nsCString m_folderName;
+ MOZ_CAN_RUN_SCRIPT void InsertDivWrappedTextAtSelection(
+ const nsAString& aText, const nsAString& classStr);
+
+ protected:
+ nsresult CreateMessage(const nsACString& originalMsgURI, MSG_ComposeType type,
+ nsIMsgCompFields* compFields);
+ void CleanUpRecipients(nsString& recipients);
+ nsresult GetABDirAndMailLists(const nsACString& aDirUri,
+ nsCOMArray<nsIAbDirectory>& aDirArray,
+ nsTArray<nsMsgMailList>& aMailListArray);
+ nsresult ResolveMailList(nsIAbDirectory* aMailList,
+ nsCOMArray<nsIAbDirectory>& allDirectoriesArray,
+ nsTArray<nsMsgMailList>& allMailListArray,
+ nsTArray<nsMsgMailList>& mailListResolved,
+ nsTArray<nsMsgRecipient>& aListMembers);
+ void TagConvertible(mozilla::dom::Element* node, int32_t* _retval);
+ MOZ_CAN_RUN_SCRIPT nsresult MoveToAboveQuote(void);
+ MOZ_CAN_RUN_SCRIPT nsresult MoveToBeginningOfDocument(void);
+ MOZ_CAN_RUN_SCRIPT nsresult MoveToEndOfDocument(void);
+ nsresult ReplaceFileURLs(nsString& sigData);
+ nsresult DataURLForFileURL(const nsAString& aFileURL, nsAString& aDataURL);
+
+ /**
+ * Given an nsIFile, attempts to read it into aString.
+ *
+ * Note: Use sparingly! This causes main-thread I/O, which causes jank and all
+ * other bad things.
+ */
+ static nsresult SlurpFileToString(nsIFile* aFile, nsACString& aString);
+
+// 3 = To, Cc, Bcc
+#define MAX_OF_RECIPIENT_ARRAY 3
+ typedef nsTArray<nsMsgRecipient> RecipientsArray[MAX_OF_RECIPIENT_ARRAY];
+ /**
+ * This method parses the compose fields and associates email addresses with
+ * the relevant cards from the address books.
+ */
+ nsresult LookupAddressBook(RecipientsArray& recipientList);
+ bool IsLastWindow();
+
+ // Helper function. Parameters are not checked.
+ bool mConvertStructs; // for TagConvertible
+
+ nsCOMPtr<nsIEditor> m_editor;
+ mozIDOMWindowProxy* m_window;
+ nsCOMPtr<nsIDocShell> mDocShell;
+ nsCOMPtr<nsIBaseWindow> m_baseWindow;
+ RefPtr<nsMsgCompFields> m_compFields;
+ nsCOMPtr<nsIMsgIdentity> m_identity;
+ bool m_composeHTML;
+ RefPtr<QuotingOutputStreamListener> mQuoteStreamListener;
+ nsCOMPtr<nsIOutputStream> mBaseStream;
+
+ nsCOMPtr<nsIMsgSend> mMsgSend; // for composition back end
+ nsCOMPtr<nsIMsgProgress>
+ mProgress; // use by the back end to report progress to the front end
+
+ // Deal with quoting issues...
+ nsString mCiteReference;
+ nsCOMPtr<nsIMsgQuote> mQuote;
+ bool mQuotingToFollow; // Quoting indicator
+ MSG_ComposeType mType; // Message type
+ bool mAutodetectCharset;
+ bool mDeleteDraft;
+ nsMsgDispositionState mDraftDisposition;
+ nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr;
+
+ nsString mSmtpPassword;
+ nsCString mHtmlToQuote;
+
+ nsTObserverArray<nsCOMPtr<nsIMsgComposeStateListener> > mStateListeners;
+ nsTObserverArray<nsCOMPtr<nsIMsgSendListener> > mExternalSendListeners;
+
+ bool mAllowRemoteContent;
+ MSG_DeliverMode mDeliverMode; // nsIMsgCompDeliverMode long.
+
+ friend class QuotingOutputStreamListener;
+ friend class nsMsgComposeSendListener;
+
+ private:
+ void DeleteTmpAttachments();
+ bool mTmpAttachmentsDeleted;
+};
+
+////////////////////////////////////////////////////////////////////////////////////
+// THIS IS THE CLASS THAT IS THE STREAM Listener OF THE HTML OUTPUT
+// FROM LIBMIME. THIS IS FOR QUOTING
+////////////////////////////////////////////////////////////////////////////////////
+class QuotingOutputStreamListener : public nsIMsgQuotingOutputStreamListener,
+ public nsSupportsWeakReference {
+ public:
+ QuotingOutputStreamListener(nsIMsgDBHdr* origMsgHdr, bool quoteHeaders,
+ bool headersOnly, nsIMsgIdentity* identity,
+ nsIMsgQuote* msgQuote, bool quoteOriginal,
+ const nsACString& htmlToQuote);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIMSGQUOTINGOUTPUTSTREAMLISTENER
+
+ nsresult SetComposeObj(nsIMsgCompose* obj);
+ nsresult ConvertToPlainText(bool formatflowed, bool formatted,
+ bool disallowBreaks);
+ MOZ_CAN_RUN_SCRIPT nsresult InsertToCompose(nsIEditor* aEditor,
+ bool aHTMLEditor);
+ nsresult AppendToMsgBody(const nsCString& inStr);
+
+ private:
+ virtual ~QuotingOutputStreamListener();
+ nsWeakPtr mWeakComposeObj;
+ nsString mMsgBody;
+ nsString mCitePrefix;
+ nsString mSignature;
+ bool mQuoteHeaders;
+ bool mHeadersOnly;
+ nsCOMPtr<nsIMsgQuote> mQuote;
+ nsCOMPtr<nsIMimeHeaders> mHeaders;
+ nsCOMPtr<nsIMsgIdentity> mIdentity;
+ nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr;
+ nsString mCiteReference;
+ nsCOMPtr<nsIMimeConverter> mMimeConverter;
+ int32_t mUnicodeBufferCharacterLength;
+ bool mQuoteOriginal;
+ nsCString mHtmlToQuote;
+};
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for the send operation. We have to create this
+// class to listen for message send completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+class nsMsgComposeSendListener : public nsIMsgComposeSendListener,
+ public nsIMsgSendListener,
+ public nsIMsgCopyServiceListener,
+ public nsIWebProgressListener {
+ public:
+ nsMsgComposeSendListener(void);
+
+ // nsISupports interface
+ NS_DECL_ISUPPORTS
+
+ // nsIMsgComposeSendListener interface
+ NS_DECL_NSIMSGCOMPOSESENDLISTENER
+
+ // nsIMsgSendListener interface
+ NS_DECL_NSIMSGSENDLISTENER
+
+ // nsIMsgCopyServiceListener interface
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ // nsIWebProgressListener interface
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ nsresult RemoveDraftOrTemplate(nsIMsgCompose* compObj, nsCString msgURI,
+ bool isSaveTemplate);
+ nsresult RemoveCurrentDraftMessage(nsIMsgCompose* compObj, bool calledByCopy,
+ bool isSaveTemplate);
+ nsresult GetMsgFolder(nsIMsgCompose* compObj, nsIMsgFolder** msgFolder);
+
+ private:
+ virtual ~nsMsgComposeSendListener();
+ nsWeakPtr mWeakComposeObj;
+ MSG_DeliverMode mDeliverMode;
+};
+
+/******************************************************************************
+ * nsMsgMailList
+ ******************************************************************************/
+struct nsMsgMailList {
+ explicit nsMsgMailList(nsIAbDirectory* directory);
+
+ nsString mName;
+ nsString mDescription;
+ nsCOMPtr<nsIAbDirectory> mDirectory;
+};
+
+#endif /* _nsMsgCompose_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp b/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp
new file mode 100644
index 0000000000..7683d04d89
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgComposeContentHandler.h"
+#include "nsMsgComposeService.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+#include "plstr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsPIDOMWindow.h"
+#include "mozIDOMWindow.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIMsgAccountManager.h"
+
+#define NS_MSGCOMPOSESERVICE_CID \
+ { /* 588595FE-1ADA-11d3-A715-0060B0EB39B5 */ \
+ 0x588595fe, 0x1ada, 0x11d3, { \
+ 0xa7, 0x15, 0x0, 0x60, 0xb0, 0xeb, 0x39, 0xb5 \
+ } \
+ }
+static NS_DEFINE_CID(kMsgComposeServiceCID, NS_MSGCOMPOSESERVICE_CID);
+
+nsMsgComposeContentHandler::nsMsgComposeContentHandler() {}
+
+// The following macro actually implement addref, release and query interface
+// for our component.
+NS_IMPL_ISUPPORTS(nsMsgComposeContentHandler, nsIContentHandler)
+
+nsMsgComposeContentHandler::~nsMsgComposeContentHandler() {}
+
+// Try to get an appropriate nsIMsgIdentity by going through the window, getting
+// the document's URI, then the corresponding nsIMsgDBHdr. Then find the server
+// associated with that header and get the first identity for it.
+nsresult nsMsgComposeContentHandler::GetBestIdentity(
+ nsIInterfaceRequestor* aWindowContext, nsIMsgIdentity** aIdentity) {
+ nsresult rv;
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow = do_GetInterface(aWindowContext);
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(domWindow);
+
+ nsAutoString documentURIString;
+ rv = window->GetDoc()->GetDocumentURI(documentURIString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> documentURI;
+ rv = NS_NewURI(getter_AddRefs(documentURI), documentURIString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageUrl> msgURI = do_QueryInterface(documentURI);
+ if (!msgURI) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgURI->GetMessageHeader(getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nsIMsgDBHdrs from .eml messages have a null folder, so bail out if that's
+ // the case.
+ if (!folder) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = accountManager->GetFirstIdentityForServer(server, aIdentity);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgComposeContentHandler::HandleContent(
+ const char* aContentType, nsIInterfaceRequestor* aWindowContext,
+ nsIRequest* request) {
+ nsresult rv = NS_OK;
+ if (!request) return NS_ERROR_NULL_POINTER;
+
+ // First of all, get the content type and make sure it is a content type we
+ // know how to handle!
+ if (PL_strcasecmp(aContentType, "application/x-mailto") == 0) {
+ nsCOMPtr<nsIMsgIdentity> identity;
+
+ if (aWindowContext)
+ GetBestIdentity(aWindowContext, getter_AddRefs(identity));
+
+ nsCOMPtr<nsIURI> aUri;
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ if (!aChannel) return NS_ERROR_FAILURE;
+
+ rv = aChannel->GetURI(getter_AddRefs(aUri));
+ if (aUri) {
+ nsCOMPtr<nsIMsgComposeService> composeService =
+ do_GetService(kMsgComposeServiceCID, &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = composeService->OpenComposeWindowWithURI(nullptr, aUri, identity);
+ }
+ } else {
+ // The content-type was not application/x-mailto...
+ return NS_ERROR_WONT_HANDLE_CONTENT;
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/compose/src/nsMsgComposeContentHandler.h b/comm/mailnews/compose/src/nsMsgComposeContentHandler.h
new file mode 100644
index 0000000000..158edfa09b
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeContentHandler.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIContentHandler.h"
+#include "nsIMsgIdentity.h"
+
+class nsMsgComposeContentHandler : public nsIContentHandler {
+ public:
+ nsMsgComposeContentHandler();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTHANDLER
+ private:
+ virtual ~nsMsgComposeContentHandler();
+ nsresult GetBestIdentity(nsIInterfaceRequestor* aWindowContext,
+ nsIMsgIdentity** identity);
+};
diff --git a/comm/mailnews/compose/src/nsMsgComposeParams.cpp b/comm/mailnews/compose/src/nsMsgComposeParams.cpp
new file mode 100644
index 0000000000..653ed11f46
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeParams.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgComposeParams.h"
+
+nsMsgComposeParams::nsMsgComposeParams()
+ : mType(nsIMsgCompType::New),
+ mFormat(nsIMsgCompFormat::Default),
+ mBodyIsLink(false),
+ mAutodetectCharset(false) {}
+
+/* the following macro actually implement addref, release and query interface
+ * for our component. */
+NS_IMPL_ISUPPORTS(nsMsgComposeParams, nsIMsgComposeParams)
+
+nsMsgComposeParams::~nsMsgComposeParams() {}
+
+/* attribute MSG_ComposeType type; */
+NS_IMETHODIMP nsMsgComposeParams::GetType(MSG_ComposeType* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+
+ *aType = mType;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetType(MSG_ComposeType aType) {
+ mType = aType;
+ return NS_OK;
+}
+
+/* attribute MSG_ComposeFormat format; */
+NS_IMETHODIMP nsMsgComposeParams::GetFormat(MSG_ComposeFormat* aFormat) {
+ NS_ENSURE_ARG_POINTER(aFormat);
+
+ *aFormat = mFormat;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetFormat(MSG_ComposeFormat aFormat) {
+ mFormat = aFormat;
+ return NS_OK;
+}
+
+/* attribute string originalMsgURI; */
+NS_IMETHODIMP nsMsgComposeParams::GetOriginalMsgURI(
+ nsACString& aOriginalMsgURI) {
+ aOriginalMsgURI = mOriginalMsgUri;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetOriginalMsgURI(
+ const nsACString& aOriginalMsgURI) {
+ mOriginalMsgUri = aOriginalMsgURI;
+ return NS_OK;
+}
+
+/* attribute nsIMsgIdentity identity; */
+NS_IMETHODIMP nsMsgComposeParams::GetIdentity(nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ NS_IF_ADDREF(*aIdentity = mIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeParams::SetIdentity(nsIMsgIdentity* aIdentity) {
+ mIdentity = aIdentity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeParams::SetOrigMsgHdr(nsIMsgDBHdr* aMsgHdr) {
+ mOrigMsgHdr = aMsgHdr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeParams::GetOrigMsgHdr(nsIMsgDBHdr** aMsgHdr) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_IF_ADDREF(*aMsgHdr = mOrigMsgHdr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeParams::GetAutodetectCharset(
+ bool* aAutodetectCharset) {
+ NS_ENSURE_ARG_POINTER(aAutodetectCharset);
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetAutodetectCharset(
+ bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+/* attribute ACString htmlToQuote; */
+NS_IMETHODIMP nsMsgComposeParams::GetHtmlToQuote(nsACString& aHtmlToQuote) {
+ aHtmlToQuote = mHtmlToQuote;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetHtmlToQuote(
+ const nsACString& aHtmlToQuote) {
+ mHtmlToQuote = aHtmlToQuote;
+ return NS_OK;
+}
+
+/* attribute nsIMsgCompFields composeFields; */
+NS_IMETHODIMP nsMsgComposeParams::GetComposeFields(
+ nsIMsgCompFields** aComposeFields) {
+ NS_ENSURE_ARG_POINTER(aComposeFields);
+
+ if (mComposeFields) {
+ NS_ADDREF(*aComposeFields = mComposeFields);
+ } else
+ *aComposeFields = nullptr;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetComposeFields(
+ nsIMsgCompFields* aComposeFields) {
+ mComposeFields = aComposeFields;
+ return NS_OK;
+}
+
+/* attribute boolean bodyIsLink; */
+NS_IMETHODIMP nsMsgComposeParams::GetBodyIsLink(bool* aBodyIsLink) {
+ NS_ENSURE_ARG_POINTER(aBodyIsLink);
+
+ *aBodyIsLink = mBodyIsLink;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetBodyIsLink(bool aBodyIsLink) {
+ mBodyIsLink = aBodyIsLink;
+ return NS_OK;
+}
+
+/* attribute nsIMsgSendLisneter sendListener; */
+NS_IMETHODIMP nsMsgComposeParams::GetSendListener(
+ nsIMsgSendListener** aSendListener) {
+ NS_ENSURE_ARG_POINTER(aSendListener);
+
+ if (mSendListener) {
+ NS_ADDREF(*aSendListener = mSendListener);
+ } else
+ *aSendListener = nullptr;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetSendListener(
+ nsIMsgSendListener* aSendListener) {
+ mSendListener = aSendListener;
+ return NS_OK;
+}
+
+/* attribute string smtpPassword; */
+NS_IMETHODIMP nsMsgComposeParams::GetSmtpPassword(nsAString& aSmtpPassword) {
+ aSmtpPassword = mSMTPPassword;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeParams::SetSmtpPassword(
+ const nsAString& aSmtpPassword) {
+ mSMTPPassword = aSmtpPassword;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgComposeParams.h b/comm/mailnews/compose/src/nsMsgComposeParams.h
new file mode 100644
index 0000000000..b447304e30
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeParams.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIMsgComposeParams.h"
+#include "nsString.h"
+#include "nsIMsgHdr.h"
+#include "nsCOMPtr.h"
+class nsMsgComposeParams : public nsIMsgComposeParams {
+ public:
+ nsMsgComposeParams();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOMPOSEPARAMS
+
+ private:
+ virtual ~nsMsgComposeParams();
+ MSG_ComposeType mType;
+ MSG_ComposeFormat mFormat;
+ nsCString mOriginalMsgUri;
+ nsCOMPtr<nsIMsgIdentity> mIdentity;
+ nsCOMPtr<nsIMsgCompFields> mComposeFields;
+ bool mBodyIsLink;
+ nsCOMPtr<nsIMsgSendListener> mSendListener;
+ nsString mSMTPPassword;
+ nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr;
+ bool mAutodetectCharset;
+ nsCString mHtmlToQuote;
+};
diff --git a/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp b/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp
new file mode 100644
index 0000000000..d54355ab8f
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgComposeProgressParams.h"
+#include "nsServiceManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgComposeProgressParams, nsIMsgComposeProgressParams)
+
+nsMsgComposeProgressParams::nsMsgComposeProgressParams()
+ : m_deliveryMode(nsIMsgCompDeliverMode::Now) {}
+
+nsMsgComposeProgressParams::~nsMsgComposeProgressParams() {}
+
+/* attribute wstring subject; */
+NS_IMETHODIMP nsMsgComposeProgressParams::GetSubject(char16_t** aSubject) {
+ NS_ENSURE_ARG(aSubject);
+
+ *aSubject = ToNewUnicode(m_subject);
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeProgressParams::SetSubject(const char16_t* aSubject) {
+ m_subject = aSubject;
+ return NS_OK;
+}
+
+/* attribute MSG_DeliverMode deliveryMode; */
+NS_IMETHODIMP nsMsgComposeProgressParams::GetDeliveryMode(
+ MSG_DeliverMode* aDeliveryMode) {
+ NS_ENSURE_ARG(aDeliveryMode);
+
+ *aDeliveryMode = m_deliveryMode;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgComposeProgressParams::SetDeliveryMode(
+ MSG_DeliverMode aDeliveryMode) {
+ m_deliveryMode = aDeliveryMode;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgComposeProgressParams.h b/comm/mailnews/compose/src/nsMsgComposeProgressParams.h
new file mode 100644
index 0000000000..141d870195
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeProgressParams.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIMsgComposeProgressParams.h"
+
+class nsMsgComposeProgressParams : public nsIMsgComposeProgressParams {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOMPOSEPROGRESSPARAMS
+
+ nsMsgComposeProgressParams();
+
+ private:
+ virtual ~nsMsgComposeProgressParams();
+ nsString m_subject;
+ MSG_DeliverMode m_deliveryMode;
+};
diff --git a/comm/mailnews/compose/src/nsMsgComposeService.cpp b/comm/mailnews/compose/src/nsMsgComposeService.cpp
new file mode 100644
index 0000000000..be12fbbef4
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeService.cpp
@@ -0,0 +1,1411 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgComposeService.h"
+#include "nsIMsgSend.h"
+#include "nsIServiceManager.h"
+#include "nsIObserverService.h"
+#include "nsIMsgIdentity.h"
+#include "nsISmtpUrl.h"
+#include "nsIURI.h"
+#include "nsMsgI18N.h"
+#include "nsIMsgComposeParams.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIWindowWatcher.h"
+#include "mozIDOMWindow.h"
+#include "nsIContentViewer.h"
+#include "nsIMsgWindow.h"
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/Document.h"
+#include "nsIAppWindow.h"
+#include "nsIWindowMediator.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIBaseWindow.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIStreamConverter.h"
+#include "nsToolkitCompsCID.h"
+#include "nsNetUtil.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIMsgDatabase.h"
+#include "nsIDocumentEncoder.h"
+#include "nsContentCID.h"
+#include "mozilla/dom/Selection.h"
+#include "nsUTF8Utils.h"
+#include "mozilla/intl/LineBreaker.h"
+#include "mimemoz2.h"
+#include "nsIURIMutator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "nsFrameLoader.h"
+#include "nsSmtpUrl.h"
+#include "nsUnicharUtils.h"
+#include "mozilla/NullPrincipal.h"
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+# include "mozilla/Logging.h"
+# include "nsIMsgHdr.h"
+# include "nsIMsgMessageService.h"
+# include "nsMsgUtils.h"
+#endif
+
+#include "nsICommandLine.h"
+#include "nsIAppStartup.h"
+#include "nsMsgUtils.h"
+#include "nsIPrincipal.h"
+#include "nsIMutableArray.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#ifdef XP_WIN
+# include <windows.h>
+# include <shellapi.h>
+# include "nsIWidget.h"
+#endif
+
+#define DEFAULT_CHROME \
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml"_ns
+
+#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION "mailnews.reply_quoting_selection"
+#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION_MULTI_WORD \
+ "mailnews.reply_quoting_selection.multi_word"
+#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION_ONLY_IF \
+ "mailnews.reply_quoting_selection.only_if_chars"
+
+#define MAIL_ROOT_PREF "mail."
+#define MAILNEWS_ROOT_PREF "mailnews."
+#define HTMLDOMAINUPDATE_VERSION_PREF_NAME "global_html_domains.version"
+#define HTMLDOMAINUPDATE_DOMAINLIST_PREF_NAME "global_html_domains"
+#define USER_CURRENT_HTMLDOMAINLIST_PREF_NAME "html_domains"
+#define USER_CURRENT_PLAINTEXTDOMAINLIST_PREF_NAME "plaintext_domains"
+#define DOMAIN_DELIMITER ','
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+static mozilla::LazyLogModule MsgComposeLogModule("MsgCompose");
+
+static uint32_t GetMessageSizeFromURI(const nsACString& originalMsgURI) {
+ uint32_t msgSize = 0;
+
+ if (!originalMsgURI.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> originalMsgHdr;
+ GetMsgDBHdrFromURI(originalMsgURI, getter_AddRefs(originalMsgHdr));
+ if (originalMsgHdr) originalMsgHdr->GetMessageSize(&msgSize);
+ }
+
+ return msgSize;
+}
+#endif
+
+nsMsgComposeService::nsMsgComposeService() {
+ // Defaulting the value of mLogComposePerformance to FALSE to prevent logging.
+ mLogComposePerformance = false;
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ mStartTime = PR_IntervalNow();
+ mPreviousTime = mStartTime;
+#endif
+}
+
+NS_IMPL_ISUPPORTS(nsMsgComposeService, nsIMsgComposeService,
+ ICOMMANDLINEHANDLER, nsISupportsWeakReference)
+
+nsMsgComposeService::~nsMsgComposeService() { mOpenComposeWindows.Clear(); }
+
+nsresult nsMsgComposeService::Init() {
+ nsresult rv = NS_OK;
+
+ Reset();
+
+ AddGlobalHtmlDomains();
+ // Since the compose service should only be initialized once, we can
+ // be pretty sure there aren't any existing compose windows open.
+ MsgCleanupTempFiles("nsmail", "tmp");
+ MsgCleanupTempFiles("nscopy", "tmp");
+ MsgCleanupTempFiles("nsemail", "eml");
+ MsgCleanupTempFiles("nsemail", "tmp");
+ MsgCleanupTempFiles("nsqmail", "tmp");
+ return rv;
+}
+
+void nsMsgComposeService::Reset() {
+ mOpenComposeWindows.Clear();
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs)
+ prefs->GetBoolPref("mailnews.logComposePerformance",
+ &mLogComposePerformance);
+}
+
+// Function to open a message compose window and pass an nsIMsgComposeParams
+// parameter to it.
+NS_IMETHODIMP
+nsMsgComposeService::OpenComposeWindowWithParams(const char* chrome,
+ nsIMsgComposeParams* params) {
+ NS_ENSURE_ARG_POINTER(params);
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ if (mLogComposePerformance) {
+ TimeStamp("Start opening the window", true);
+ }
+#endif
+
+ nsresult rv;
+
+ NS_ENSURE_ARG_POINTER(params);
+
+ // Use default identity if no identity has been specified
+ nsCOMPtr<nsIMsgIdentity> identity;
+ params->GetIdentity(getter_AddRefs(identity));
+ if (!identity) {
+ GetDefaultIdentity(getter_AddRefs(identity));
+ params->SetIdentity(identity);
+ }
+
+ // Create a new window.
+ nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (!wwatch) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsISupportsInterfacePointer> msgParamsWrapper =
+ do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgParamsWrapper->SetData(params);
+ msgParamsWrapper->SetDataIID(&NS_GET_IID(nsIMsgComposeParams));
+
+ nsCOMPtr<mozIDOMWindowProxy> newWindow;
+ nsAutoCString chromeURL;
+ if (chrome && *chrome) {
+ chromeURL = nsDependentCString(chrome);
+ } else {
+ chromeURL = DEFAULT_CHROME;
+ }
+ rv = wwatch->OpenWindow(0, chromeURL, "_blank"_ns,
+ "all,chrome,dialog=no,status,toolbar"_ns,
+ msgParamsWrapper, getter_AddRefs(newWindow));
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::DetermineComposeHTML(nsIMsgIdentity* aIdentity,
+ MSG_ComposeFormat aFormat,
+ bool* aComposeHTML) {
+ NS_ENSURE_ARG_POINTER(aComposeHTML);
+
+ *aComposeHTML = true;
+ switch (aFormat) {
+ case nsIMsgCompFormat::HTML:
+ *aComposeHTML = true;
+ break;
+ case nsIMsgCompFormat::PlainText:
+ *aComposeHTML = false;
+ break;
+
+ default:
+ nsCOMPtr<nsIMsgIdentity> identity = aIdentity;
+ if (!identity) GetDefaultIdentity(getter_AddRefs(identity));
+
+ if (identity) {
+ identity->GetComposeHtml(aComposeHTML);
+ if (aFormat == nsIMsgCompFormat::OppositeOfDefault)
+ *aComposeHTML = !*aComposeHTML;
+ } else {
+ // default identity not found. Use the mail.html_compose pref to
+ // determine message compose type (HTML or PlainText).
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ nsresult rv;
+ bool useHTMLCompose;
+ rv = prefs->GetBoolPref(MAIL_ROOT_PREF "html_compose",
+ &useHTMLCompose);
+ if (NS_SUCCEEDED(rv)) *aComposeHTML = useHTMLCompose;
+ }
+ }
+ break;
+ }
+
+ return NS_OK;
+}
+
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION nsresult
+nsMsgComposeService::GetOrigWindowSelection(MSG_ComposeType type,
+ mozilla::dom::Selection* selection,
+ nsACString& aSelHTML) {
+ nsresult rv;
+
+ // Good hygiene
+ aSelHTML.Truncate();
+
+ // Get the pref to see if we even should do reply quoting selection
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool replyQuotingSelection;
+ rv = prefs->GetBoolPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION,
+ &replyQuotingSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!replyQuotingSelection) return NS_ERROR_ABORT;
+
+ bool requireMultipleWords = true;
+ nsAutoCString charsOnlyIf;
+ prefs->GetBoolPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION_MULTI_WORD,
+ &requireMultipleWords);
+ prefs->GetCharPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION_ONLY_IF,
+ charsOnlyIf);
+ if (requireMultipleWords || !charsOnlyIf.IsEmpty()) {
+ nsAutoString selPlain;
+ selection->Stringify(selPlain);
+
+ // If "mailnews.reply_quoting_selection.multi_word" is on, then there must
+ // be at least two words selected in order to quote just the selected text
+ if (requireMultipleWords) {
+ if (selPlain.IsEmpty()) return NS_ERROR_ABORT;
+
+ if (NS_SUCCEEDED(rv)) {
+ const uint32_t length = selPlain.Length();
+ const char16_t* unicodeStr = selPlain.get();
+ int32_t endWordPos =
+ mozilla::intl::LineBreaker::Next(unicodeStr, length, 0);
+
+ // If there's not even one word, then there's not multiple words
+ if (endWordPos == NS_LINEBREAKER_NEED_MORE_TEXT) return NS_ERROR_ABORT;
+
+ // If after the first word is only space, then there's not multiple
+ // words
+ const char16_t* end;
+ for (end = unicodeStr + endWordPos; mozilla::intl::NS_IsSpace(*end);
+ end++)
+ ;
+ if (!*end) return NS_ERROR_ABORT;
+ }
+ }
+
+ if (!charsOnlyIf.IsEmpty()) {
+ if (selPlain.FindCharInSet(NS_ConvertUTF8toUTF16(charsOnlyIf)) ==
+ kNotFound) {
+ return NS_ERROR_ABORT;
+ }
+ }
+ }
+
+ nsAutoString selHTML;
+ IgnoredErrorResult rv2;
+ selection->ToStringWithFormat(u"text/html"_ns,
+ nsIDocumentEncoder::SkipInvisibleContent, 0,
+ selHTML, rv2);
+ if (rv2.Failed()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now remove <span class="moz-txt-citetags">&gt; </span>.
+ nsAutoCString html(NS_ConvertUTF16toUTF8(selHTML).get());
+ int32_t spanInd = html.Find("<span class=\"moz-txt-citetags\">");
+ while (spanInd != kNotFound) {
+ nsAutoCString right0(Substring(html, spanInd));
+ int32_t endInd = right0.Find("</span>");
+ if (endInd == kNotFound) break; // oops, where is the closing tag gone?
+ nsAutoCString right1(Substring(html, spanInd + endInd + 7));
+ html.SetLength(spanInd);
+ html.Append(right1);
+ spanInd = html.Find("<span class=\"moz-txt-citetags\">");
+ }
+
+ aSelHTML.Assign(html);
+
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION NS_IMETHODIMP
+nsMsgComposeService::OpenComposeWindow(
+ const nsACString& msgComposeWindowURL, nsIMsgDBHdr* origMsgHdr,
+ const nsACString& originalMsgURI, MSG_ComposeType type,
+ MSG_ComposeFormat format, nsIMsgIdentity* aIdentity, const nsACString& from,
+ nsIMsgWindow* aMsgWindow, mozilla::dom::Selection* selection,
+ bool autodetectCharset) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIdentity> identity = aIdentity;
+ if (!identity) GetDefaultIdentity(getter_AddRefs(identity));
+
+ /* Actually, the only way to implement forward inline is to simulate a
+ template message. Maybe one day when we will have more time we can change
+ that
+ */
+ if (type == nsIMsgCompType::ForwardInline || type == nsIMsgCompType::Draft ||
+ type == nsIMsgCompType::EditTemplate ||
+ type == nsIMsgCompType::Template ||
+ type == nsIMsgCompType::ReplyWithTemplate ||
+ type == nsIMsgCompType::Redirect || type == nsIMsgCompType::EditAsNew) {
+ nsAutoCString uriToOpen(originalMsgURI);
+ char sep = (uriToOpen.FindChar('?') == kNotFound) ? '?' : '&';
+
+ // The compose type that gets transmitted to a compose window open in mime
+ // is communicated using url query parameters here.
+ if (type == nsIMsgCompType::Redirect) {
+ uriToOpen += sep;
+ uriToOpen.AppendLiteral("redirect=true");
+ } else if (type == nsIMsgCompType::EditAsNew) {
+ uriToOpen += sep;
+ uriToOpen.AppendLiteral("editasnew=true");
+ } else if (type == nsIMsgCompType::EditTemplate) {
+ uriToOpen += sep;
+ uriToOpen.AppendLiteral("edittempl=true");
+ }
+
+ return LoadDraftOrTemplate(
+ uriToOpen,
+ type == nsIMsgCompType::ForwardInline || type == nsIMsgCompType::Draft
+ ? nsMimeOutput::nsMimeMessageDraftOrTemplate
+ : nsMimeOutput::nsMimeMessageEditorTemplate,
+ identity, originalMsgURI, origMsgHdr,
+ type == nsIMsgCompType::ForwardInline,
+ format == nsIMsgCompFormat::OppositeOfDefault, aMsgWindow,
+ autodetectCharset);
+ }
+
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv));
+ if (NS_SUCCEEDED(rv) && pMsgComposeParams) {
+ nsCOMPtr<nsIMsgCompFields> pMsgCompFields(do_CreateInstance(
+ "@mozilla.org/messengercompose/composefields;1", &rv));
+ if (NS_SUCCEEDED(rv) && pMsgCompFields) {
+ pMsgComposeParams->SetType(type);
+ pMsgComposeParams->SetFormat(format);
+ pMsgComposeParams->SetIdentity(identity);
+ pMsgComposeParams->SetAutodetectCharset(autodetectCharset);
+
+ // When doing a reply (except with a template) see if there's a selection
+ // that we should quote
+ if (selection &&
+ (type == nsIMsgCompType::Reply || type == nsIMsgCompType::ReplyAll ||
+ type == nsIMsgCompType::ReplyToSender ||
+ type == nsIMsgCompType::ReplyToGroup ||
+ type == nsIMsgCompType::ReplyToSenderAndGroup ||
+ type == nsIMsgCompType::ReplyToList)) {
+ nsAutoCString selHTML;
+ if (NS_SUCCEEDED(GetOrigWindowSelection(type, selection, selHTML))) {
+ nsCOMPtr<nsINode> node = selection->GetFocusNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+ IgnoredErrorResult er;
+
+ if ((node->LocalName().IsEmpty() ||
+ node->LocalName().EqualsLiteral("pre")) &&
+ node->OwnerDoc()->QuerySelector(
+ "body > div:first-of-type.moz-text-plain"_ns, er)) {
+ // Treat the quote as <pre> for selections in moz-text-plain bodies.
+ // If focusNode.localName isn't empty, we had e.g. body selected
+ // and should not add <pre>.
+ pMsgComposeParams->SetHtmlToQuote("<pre>"_ns + selHTML +
+ "</pre>"_ns);
+ } else {
+ pMsgComposeParams->SetHtmlToQuote(selHTML);
+ }
+ }
+ }
+
+ if (!originalMsgURI.IsEmpty()) {
+ if (type == nsIMsgCompType::NewsPost) {
+ nsAutoCString newsURI(originalMsgURI);
+ nsAutoCString group;
+ nsAutoCString host;
+
+ int32_t slashpos = newsURI.RFindChar('/');
+ if (slashpos > 0) {
+ // uri is "[s]news://host[:port]/group"
+ host = StringHead(newsURI, slashpos);
+ group = Substring(newsURI, slashpos + 1);
+
+ } else
+ group = originalMsgURI;
+
+ nsAutoCString unescapedName;
+ MsgUnescapeString(group,
+ nsINetUtil::ESCAPE_URL_FILE_BASENAME |
+ nsINetUtil::ESCAPE_URL_FORCED,
+ unescapedName);
+ pMsgCompFields->SetNewsgroups(NS_ConvertUTF8toUTF16(unescapedName));
+ pMsgCompFields->SetNewspostUrl(host.get());
+ } else {
+ pMsgComposeParams->SetOriginalMsgURI(originalMsgURI);
+ pMsgComposeParams->SetOrigMsgHdr(origMsgHdr);
+ pMsgCompFields->SetFrom(NS_ConvertUTF8toUTF16(from));
+ }
+ }
+
+ pMsgComposeParams->SetComposeFields(pMsgCompFields);
+
+ if (mLogComposePerformance) {
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ // ducarroz, properly fix this in the case of new message (not a reply)
+ if (type != nsIMsgCompType::NewsPost) {
+ char buff[256];
+ sprintf(buff, "Start opening the window, message size = %d",
+ GetMessageSizeFromURI(originalMsgURI));
+ TimeStamp(buff, true);
+ }
+#endif
+ } // end if(mLogComposePerformance)
+
+ rv = OpenComposeWindowWithParams(
+ PromiseFlatCString(msgComposeWindowURL).get(), pMsgComposeParams);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgComposeService::GetParamsForMailto(
+ nsIURI* aURI, nsIMsgComposeParams** aParams) {
+ nsresult rv = NS_OK;
+ if (aURI) {
+ nsCString spec;
+ aURI->GetSpec(spec);
+
+ nsCOMPtr<nsIURI> url;
+ rv = nsMailtoUrl::NewMailtoURI(spec, nullptr, getter_AddRefs(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMailtoUrl> aMailtoUrl = do_QueryInterface(url, &rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ MSG_ComposeFormat requestedComposeFormat = nsIMsgCompFormat::Default;
+ nsCString toPart;
+ nsCString ccPart;
+ nsCString bccPart;
+ nsCString subjectPart;
+ nsCString bodyPart;
+ nsCString newsgroup;
+ nsCString refPart;
+ nsCString HTMLBodyPart;
+
+ aMailtoUrl->GetMessageContents(toPart, ccPart, bccPart, subjectPart,
+ bodyPart, HTMLBodyPart, refPart, newsgroup,
+ &requestedComposeFormat);
+
+ nsAutoString sanitizedBody;
+
+ bool composeHTMLFormat;
+ DetermineComposeHTML(NULL, requestedComposeFormat, &composeHTMLFormat);
+
+ // If there was an 'html-body' param, finding it will have requested
+ // HTML format in GetMessageContents, so we try to use it first. If it's
+ // empty, but we are composing in HTML because of the user's prefs, the
+ // 'body' param needs to be escaped, since it's supposed to be plain
+ // text, but it then doesn't need to sanitized.
+ nsString rawBody;
+ if (HTMLBodyPart.IsEmpty()) {
+ if (composeHTMLFormat) {
+ nsCString escaped;
+ nsAppendEscapedHTML(bodyPart, escaped);
+ CopyUTF8toUTF16(escaped, sanitizedBody);
+ } else
+ CopyUTF8toUTF16(bodyPart, rawBody);
+ } else
+ CopyUTF8toUTF16(HTMLBodyPart, rawBody);
+
+ if (!rawBody.IsEmpty() && composeHTMLFormat) {
+ // For security reason, we must sanitize the message body before
+ // accepting any html...
+
+ rv = HTMLSanitize(rawBody, sanitizedBody); // from mimemoz2.h
+
+ if (NS_FAILED(rv)) {
+ // Something went horribly wrong with parsing for html format
+ // in the body. Set composeHTMLFormat to false so we show the
+ // plain text mail compose.
+ composeHTMLFormat = false;
+ }
+ }
+
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(do_CreateInstance(
+ "@mozilla.org/messengercompose/composeparams;1", &rv));
+ if (NS_SUCCEEDED(rv) && pMsgComposeParams) {
+ pMsgComposeParams->SetType(nsIMsgCompType::MailToUrl);
+ pMsgComposeParams->SetFormat(composeHTMLFormat
+ ? nsIMsgCompFormat::HTML
+ : nsIMsgCompFormat::PlainText);
+
+ nsCOMPtr<nsIMsgCompFields> pMsgCompFields(do_CreateInstance(
+ "@mozilla.org/messengercompose/composefields;1", &rv));
+ if (pMsgCompFields) {
+ // ugghh more conversion work!!!!
+ pMsgCompFields->SetTo(NS_ConvertUTF8toUTF16(toPart));
+ pMsgCompFields->SetCc(NS_ConvertUTF8toUTF16(ccPart));
+ pMsgCompFields->SetBcc(NS_ConvertUTF8toUTF16(bccPart));
+ pMsgCompFields->SetNewsgroups(NS_ConvertUTF8toUTF16(newsgroup));
+ pMsgCompFields->SetReferences(refPart.get());
+ pMsgCompFields->SetSubject(NS_ConvertUTF8toUTF16(subjectPart));
+ pMsgCompFields->SetBody(composeHTMLFormat ? sanitizedBody : rawBody);
+ pMsgComposeParams->SetComposeFields(pMsgCompFields);
+
+ NS_ADDREF(*aParams = pMsgComposeParams);
+ return NS_OK;
+ }
+ } // if we created msg compose params....
+ } // if we had a mailto url
+ } // if we had a url...
+
+ // if we got here we must have encountered an error
+ *aParams = nullptr;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgComposeService::OpenComposeWindowWithURI(
+ const char* aMsgComposeWindowURL, nsIURI* aURI, nsIMsgIdentity* identity) {
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams;
+ nsresult rv = GetParamsForMailto(aURI, getter_AddRefs(pMsgComposeParams));
+ if (NS_SUCCEEDED(rv)) {
+ pMsgComposeParams->SetIdentity(identity);
+ rv = OpenComposeWindowWithParams(aMsgComposeWindowURL, pMsgComposeParams);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgComposeService::InitCompose(nsIMsgComposeParams* aParams,
+ mozIDOMWindowProxy* aWindow,
+ nsIDocShell* aDocShell,
+ nsIMsgCompose** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgCompose> msgCompose =
+ do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = msgCompose->Initialize(aParams, aWindow, aDocShell);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = msgCompose);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::GetDefaultIdentity(nsIMsgIdentity** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> defaultAccount;
+ rv = accountManager->GetDefaultAccount(getter_AddRefs(defaultAccount));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return defaultAccount ? defaultAccount->GetDefaultIdentity(_retval) : NS_OK;
+}
+
+/* readonly attribute boolean logComposePerformance; */
+NS_IMETHODIMP nsMsgComposeService::GetLogComposePerformance(
+ bool* aLogComposePerformance) {
+ *aLogComposePerformance = mLogComposePerformance;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeService::TimeStamp(const char* label,
+ bool resetTime) {
+ if (!mLogComposePerformance) return NS_OK;
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+
+ PRIntervalTime now;
+
+ if (resetTime) {
+ MOZ_LOG(MsgComposeLogModule, mozilla::LogLevel::Info,
+ ("\n[process]: [totalTime][deltaTime]\n--------------------\n"));
+
+ mStartTime = PR_IntervalNow();
+ mPreviousTime = mStartTime;
+ now = mStartTime;
+ } else
+ now = PR_IntervalNow();
+
+ PRIntervalTime totalTime = PR_IntervalToMilliseconds(now - mStartTime);
+ PRIntervalTime deltaTime = PR_IntervalToMilliseconds(now - mPreviousTime);
+
+ MOZ_LOG(MsgComposeLogModule, mozilla::LogLevel::Info,
+ ("[%3.2f][%3.2f] - %s\n", ((double)totalTime / 1000.0) + 0.005,
+ ((double)deltaTime / 1000.0) + 0.005, label));
+
+ mPreviousTime = now;
+#endif
+ return NS_OK;
+}
+
+class nsMsgTemplateReplyHelper final : public nsIStreamListener,
+ public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ nsMsgTemplateReplyHelper();
+
+ nsCOMPtr<nsIMsgDBHdr> mHdrToReplyTo;
+ nsCOMPtr<nsIMsgDBHdr> mTemplateHdr;
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ nsCOMPtr<nsIMsgIdentity> mIdentity;
+ nsCString mTemplateBody;
+ bool mInMsgBody;
+ char mLastBlockChars[3];
+
+ private:
+ ~nsMsgTemplateReplyHelper();
+};
+
+NS_IMPL_ISUPPORTS(nsMsgTemplateReplyHelper, nsIStreamListener,
+ nsIRequestObserver, nsIUrlListener)
+
+nsMsgTemplateReplyHelper::nsMsgTemplateReplyHelper() {
+ mInMsgBody = false;
+ memset(mLastBlockChars, 0, sizeof(mLastBlockChars));
+}
+
+nsMsgTemplateReplyHelper::~nsMsgTemplateReplyHelper() {}
+
+NS_IMETHODIMP nsMsgTemplateReplyHelper::OnStartRunningUrl(nsIURI* aUrl) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTemplateReplyHelper::OnStopRunningUrl(nsIURI* aUrl,
+ nsresult aExitCode) {
+ NS_ENSURE_SUCCESS(aExitCode, aExitCode);
+ nsresult rv;
+ nsCOMPtr<nsPIDOMWindowOuter> parentWindow;
+ if (mMsgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ rv = mMsgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ parentWindow = do_GetInterface(docShell);
+ NS_ENSURE_TRUE(parentWindow, NS_ERROR_FAILURE);
+ }
+
+ // create the compose params object
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv));
+ if (NS_FAILED(rv) || (!pMsgComposeParams)) return rv;
+ nsCOMPtr<nsIMsgCompFields> compFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+
+ nsCString replyTo;
+ mHdrToReplyTo->GetStringProperty("replyTo", replyTo);
+ if (replyTo.IsEmpty()) mHdrToReplyTo->GetAuthor(getter_Copies(replyTo));
+ compFields->SetTo(NS_ConvertUTF8toUTF16(replyTo));
+
+ nsString body;
+ nsString templateSubject, replySubject;
+
+ mHdrToReplyTo->GetMime2DecodedSubject(replySubject);
+ mTemplateHdr->GetMime2DecodedSubject(templateSubject);
+ nsString subject(u"Auto: "_ns); // RFC 3834 3.1.5.
+ subject.Append(templateSubject);
+ if (!replySubject.IsEmpty()) {
+ subject.AppendLiteral(u" (was: ");
+ subject.Append(replySubject);
+ subject.Append(u')');
+ }
+
+ compFields->SetSubject(subject);
+ compFields->SetRawHeader("Auto-Submitted", "auto-replied"_ns);
+
+ nsCString charset;
+ rv = mTemplateHdr->GetCharset(getter_Copies(charset));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsMsgI18NConvertToUnicode(charset, mTemplateBody, body);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "couldn't convert templ body to unicode");
+ compFields->SetBody(body);
+
+ nsCString msgUri;
+ nsCOMPtr<nsIMsgFolder> folder;
+ mHdrToReplyTo->GetFolder(getter_AddRefs(folder));
+ folder->GetUriForMsg(mHdrToReplyTo, msgUri);
+ // populate the compose params
+ pMsgComposeParams->SetType(nsIMsgCompType::ReplyWithTemplate);
+ pMsgComposeParams->SetFormat(nsIMsgCompFormat::Default);
+ pMsgComposeParams->SetIdentity(mIdentity);
+ pMsgComposeParams->SetComposeFields(compFields);
+ pMsgComposeParams->SetOriginalMsgURI(msgUri);
+
+ // create the nsIMsgCompose object to send the object
+ nsCOMPtr<nsIMsgCompose> pMsgCompose(
+ do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /** initialize nsIMsgCompose, Send the message, wait for send completion
+ * response **/
+
+ rv = pMsgCompose->Initialize(pMsgComposeParams, parentWindow, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::dom::Promise> promise;
+ return pMsgCompose->SendMsg(nsIMsgSend::nsMsgDeliverNow, mIdentity, nullptr,
+ nullptr, nullptr, getter_AddRefs(promise));
+}
+
+NS_IMETHODIMP
+nsMsgTemplateReplyHelper::OnStartRequest(nsIRequest* request) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgTemplateReplyHelper::OnStopRequest(nsIRequest* request, nsresult status) {
+ if (NS_SUCCEEDED(status)) {
+ // now we've got the message body in mTemplateBody -
+ // need to set body in compose params and send the reply.
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgTemplateReplyHelper::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStream,
+ uint64_t srcOffset, uint32_t count) {
+ nsresult rv = NS_OK;
+
+ char readBuf[1024];
+
+ uint64_t available;
+ uint32_t readCount;
+ uint32_t maxReadCount = sizeof(readBuf) - 1;
+
+ rv = inStream->Available(&available);
+ while (NS_SUCCEEDED(rv) && available > 0) {
+ uint32_t bodyOffset = 0, readOffset = 0;
+ if (!mInMsgBody && mLastBlockChars[0]) {
+ memcpy(readBuf, mLastBlockChars, 3);
+ readOffset = 3;
+ maxReadCount -= 3;
+ }
+ if (maxReadCount > available) maxReadCount = (uint32_t)available;
+ memset(readBuf, 0, sizeof(readBuf));
+ rv = inStream->Read(readBuf + readOffset, maxReadCount, &readCount);
+ available -= readCount;
+ readCount += readOffset;
+ // we're mainly interested in the msg body, so we need to
+ // find the header/body delimiter of a blank line. A blank line
+ // looks like <CR><CR>, <LF><LF>, or <CRLF><CRLF>
+ if (!mInMsgBody) {
+ for (uint32_t charIndex = 0; charIndex < readCount && !bodyOffset;
+ charIndex++) {
+ if (readBuf[charIndex] == '\r' || readBuf[charIndex] == '\n') {
+ if (charIndex + 1 < readCount) {
+ if (readBuf[charIndex] == readBuf[charIndex + 1]) {
+ // got header+body separator
+ bodyOffset = charIndex + 2;
+ break;
+ } else if ((charIndex + 3 < readCount) &&
+ !strncmp(readBuf + charIndex, "\r\n\r\n", 4)) {
+ bodyOffset = charIndex + 4;
+ break;
+ }
+ }
+ }
+ }
+ mInMsgBody = bodyOffset != 0;
+ if (!mInMsgBody && readCount > 3) // still in msg hdrs
+ strncpy(mLastBlockChars, readBuf + readCount - 3, 3);
+ }
+ mTemplateBody.Append(readBuf + bodyOffset);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeService::ReplyWithTemplate(
+ nsIMsgDBHdr* aMsgHdr, const nsACString& templateUri,
+ nsIMsgWindow* aMsgWindow, nsIMsgIncomingServer* aServer) {
+ // To reply with template, we need the message body of the template.
+ // I think we're going to need to stream the template message to ourselves,
+ // and construct the body, and call setBody on the compFields.
+ nsresult rv;
+ const nsPromiseFlatCString& templateUriFlat = PromiseFlatCString(templateUri);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = account->GetIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString recipients;
+ aMsgHdr->GetRecipients(getter_Copies(recipients));
+
+ nsAutoCString ccList;
+ aMsgHdr->GetCcList(getter_Copies(ccList));
+
+ // Go through the identities to see to whom this was addressed.
+ // In case we get no match, this is likely a list/bulk/bcc/spam mail and we
+ // shouldn't reply. RFC 3834 2.
+ nsCOMPtr<nsIMsgIdentity> identity; // identity to reply from
+ for (auto anIdentity : identities) {
+ nsAutoCString identityEmail;
+ anIdentity->GetEmail(identityEmail);
+
+ if (FindInReadable(identityEmail, recipients,
+ nsCaseInsensitiveCStringComparator) ||
+ FindInReadable(identityEmail, ccList,
+ nsCaseInsensitiveCStringComparator)) {
+ identity = anIdentity;
+ break;
+ }
+ }
+ if (!identity) // Found no match -> don't reply.
+ return NS_ERROR_ABORT;
+
+ RefPtr<nsMsgTemplateReplyHelper> helper = new nsMsgTemplateReplyHelper;
+
+ helper->mHdrToReplyTo = aMsgHdr;
+ helper->mMsgWindow = aMsgWindow;
+ helper->mIdentity = identity;
+
+ nsAutoCString replyTo;
+ aMsgHdr->GetStringProperty("replyTo", replyTo);
+ if (replyTo.IsEmpty()) aMsgHdr->GetAuthor(getter_Copies(replyTo));
+ if (replyTo.IsEmpty()) return NS_ERROR_FAILURE; // nowhere to send the reply
+
+ nsCOMPtr<nsIMsgFolder> templateFolder;
+ nsCOMPtr<nsIMsgDatabase> templateDB;
+ nsCString templateMsgHdrUri;
+ const char* query = PL_strstr(templateUriFlat.get(), "?messageId=");
+ if (!query) return NS_ERROR_FAILURE;
+
+ nsAutoCString folderUri(Substring(templateUriFlat.get(), query));
+ rv = GetExistingFolder(folderUri, getter_AddRefs(templateFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = templateFolder->GetMsgDatabase(getter_AddRefs(templateDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* subject = PL_strstr(templateUriFlat.get(), "&subject=");
+ if (subject) {
+ const char* subjectEnd = subject + strlen(subject);
+ nsAutoCString messageId(Substring(query + 11, subject));
+ nsAutoCString subjectString(Substring(subject + 9, subjectEnd));
+ templateDB->GetMsgHdrForMessageID(messageId.get(),
+ getter_AddRefs(helper->mTemplateHdr));
+ if (helper->mTemplateHdr)
+ templateFolder->GetUriForMsg(helper->mTemplateHdr, templateMsgHdrUri);
+ // to use the subject, we'd need to expose a method to find a message by
+ // subject, or painfully iterate through messages...We'll try to make the
+ // message-id not change when saving a template first.
+ }
+ if (templateMsgHdrUri.IsEmpty()) {
+ // ### probably want to return a specific error and
+ // have the calling code disable the filter.
+ NS_ASSERTION(false, "failed to get msg hdr");
+ return NS_ERROR_FAILURE;
+ }
+ // we need to convert the template uri, which is of the form
+ // <folder uri>?messageId=<messageId>&subject=<subject>
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ rv = GetMessageServiceFromURI(templateMsgHdrUri, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> listenerSupports;
+ helper->QueryInterface(NS_GET_IID(nsISupports),
+ getter_AddRefs(listenerSupports));
+
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = msgService->StreamMessage(
+ templateMsgHdrUri, listenerSupports, aMsgWindow, helper,
+ false, // convert data
+ EmptyCString(), false, getter_AddRefs(dummyNull));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ aMsgHdr->GetFolder(getter_AddRefs(folder));
+ if (!folder) return NS_ERROR_NULL_POINTER;
+
+ // We're sending a new message. Conceptually it's a reply though, so mark the
+ // original message as replied.
+ return folder->AddMessageDispositionState(
+ aMsgHdr, nsIMsgFolder::nsMsgDispositionState_Replied);
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::ForwardMessage(const nsAString& forwardTo,
+ nsIMsgDBHdr* aMsgHdr,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgIncomingServer* aServer,
+ uint32_t aForwardType) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+
+ nsresult rv;
+ if (aForwardType == nsIMsgComposeService::kForwardAsDefault) {
+ int32_t forwardPref = 0;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetIntPref("mail.forward_message_mode", &forwardPref);
+ // 0=default as attachment 2=forward as inline with attachments,
+ // (obsolete 4.x value)1=forward as quoted (mapped to 2 in mozilla)
+ aForwardType = forwardPref == 0 ? nsIMsgComposeService::kForwardAsAttachment
+ : nsIMsgComposeService::kForwardInline;
+ }
+ nsCString msgUri;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ aMsgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_TRUE(folder, NS_ERROR_NULL_POINTER);
+
+ folder->GetUriForMsg(aMsgHdr, msgUri);
+
+ nsAutoCString uriToOpen(msgUri);
+
+ // get the MsgIdentity for the above key using AccountManager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ nsCOMPtr<nsIMsgIdentity> identity;
+
+ rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = account->GetDefaultIdentity(getter_AddRefs(identity));
+ // Use default identity if no identity has been found on this account
+ if (NS_FAILED(rv) || !identity) {
+ rv = GetDefaultIdentity(getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (aForwardType == nsIMsgComposeService::kForwardInline)
+ return RunMessageThroughMimeDraft(
+ uriToOpen, nsMimeOutput::nsMimeMessageDraftOrTemplate, identity,
+ uriToOpen, aMsgHdr, true, forwardTo, false, aMsgWindow, false);
+
+ nsCOMPtr<mozIDOMWindowProxy> parentWindow;
+ if (aMsgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ rv = aMsgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+ parentWindow = do_GetInterface(docShell);
+ NS_ENSURE_TRUE(parentWindow, NS_ERROR_FAILURE);
+ }
+ // create the compose params object
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgCompFields> compFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+
+ compFields->SetTo(forwardTo);
+ // populate the compose params
+ pMsgComposeParams->SetType(nsIMsgCompType::ForwardAsAttachment);
+ pMsgComposeParams->SetFormat(nsIMsgCompFormat::Default);
+ pMsgComposeParams->SetIdentity(identity);
+ pMsgComposeParams->SetComposeFields(compFields);
+ pMsgComposeParams->SetOriginalMsgURI(uriToOpen);
+ // create the nsIMsgCompose object to send the object
+ nsCOMPtr<nsIMsgCompose> pMsgCompose(
+ do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /** initialize nsIMsgCompose, Send the message, wait for send completion
+ * response **/
+ rv = pMsgCompose->Initialize(pMsgComposeParams, parentWindow, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Promise> promise;
+ rv = pMsgCompose->SendMsg(nsIMsgSend::nsMsgDeliverNow, identity, nullptr,
+ nullptr, nullptr, getter_AddRefs(promise));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nsMsgCompose::ProcessReplyFlags usually takes care of marking messages
+ // as forwarded. ProcessReplyFlags is normally called from
+ // nsMsgComposeSendListener::OnStopSending but for this case the msgCompose
+ // object is not set so ProcessReplyFlags won't get called.
+ // Therefore, let's just mark it here instead.
+ return folder->AddMessageDispositionState(
+ aMsgHdr, nsIMsgFolder::nsMsgDispositionState_Forwarded);
+}
+
+nsresult nsMsgComposeService::AddGlobalHtmlDomains() {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefs->GetBranch(MAILNEWS_ROOT_PREF, getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> defaultsPrefBranch;
+ rv = prefs->GetDefaultBranch(MAILNEWS_ROOT_PREF,
+ getter_AddRefs(defaultsPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /**
+ * Check to see if we need to add any global domains.
+ * If so, make sure the following prefs are added to mailnews.js
+ *
+ * 1. pref("mailnews.global_html_domains.version", version number);
+ * This pref registers the current version in the user prefs file. A default
+ * value is stored in mailnews file. Depending the changes we plan to make we
+ * can move the default version number. Comparing version number from user's
+ * prefs file and the default one from mailnews.js, we can effect ppropriate
+ * changes.
+ *
+ * 2. pref("mailnews.global_html_domains", <comma separated domain list>);
+ * This pref contains the list of html domains that ISP can add to make that
+ * user's contain all of these under the HTML domains in the
+ * Mail&NewsGrpus|Send Format under global preferences.
+ */
+ int32_t htmlDomainListCurrentVersion, htmlDomainListDefaultVersion;
+ rv = prefBranch->GetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME,
+ &htmlDomainListCurrentVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultsPrefBranch->GetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME,
+ &htmlDomainListDefaultVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update the list as needed
+ if (htmlDomainListCurrentVersion <= htmlDomainListDefaultVersion) {
+ // Get list of global domains need to be added
+ nsCString globalHtmlDomainList;
+ rv = prefBranch->GetCharPref(HTMLDOMAINUPDATE_DOMAINLIST_PREF_NAME,
+ globalHtmlDomainList);
+
+ if (NS_SUCCEEDED(rv) && !globalHtmlDomainList.IsEmpty()) {
+ nsTArray<nsCString> domainArray;
+
+ // Get user's current HTML domain set for send format
+ nsCString currentHtmlDomainList;
+ rv = prefBranch->GetCharPref(USER_CURRENT_HTMLDOMAINLIST_PREF_NAME,
+ currentHtmlDomainList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString newHtmlDomainList(currentHtmlDomainList);
+ // Get the current html domain list into new list var
+ ParseString(currentHtmlDomainList, DOMAIN_DELIMITER, domainArray);
+
+ // Get user's current Plaintext domain set for send format
+ nsCString currentPlaintextDomainList;
+ rv = prefBranch->GetCharPref(USER_CURRENT_PLAINTEXTDOMAINLIST_PREF_NAME,
+ currentPlaintextDomainList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current plaintext domain list into new list var
+ ParseString(currentPlaintextDomainList, DOMAIN_DELIMITER, domainArray);
+
+ size_t i = domainArray.Length();
+ if (i > 0) {
+ // Append each domain in the preconfigured html domain list
+ globalHtmlDomainList.StripWhitespace();
+ ParseString(globalHtmlDomainList, DOMAIN_DELIMITER, domainArray);
+
+ // Now add each domain that does not already appear in
+ // the user's current html or plaintext domain lists
+ for (; i < domainArray.Length(); i++) {
+ if (domainArray.IndexOf(domainArray[i]) == i) {
+ if (!newHtmlDomainList.IsEmpty())
+ newHtmlDomainList += DOMAIN_DELIMITER;
+ newHtmlDomainList += domainArray[i];
+ }
+ }
+ } else {
+ // User has no domains listed either in html or plain text category.
+ // Assign the global list to be the user's current html domain list
+ newHtmlDomainList = globalHtmlDomainList;
+ }
+
+ // Set user's html domain pref with the updated list
+ rv = prefBranch->SetCharPref(USER_CURRENT_HTMLDOMAINLIST_PREF_NAME,
+ newHtmlDomainList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Increase the version to avoid running the update code unless needed
+ // (based on default version)
+ rv = prefBranch->SetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME,
+ htmlDomainListCurrentVersion + 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::RegisterComposeDocShell(nsIDocShell* aDocShell,
+ nsIMsgCompose* aComposeObject) {
+ NS_ENSURE_ARG_POINTER(aDocShell);
+ NS_ENSURE_ARG_POINTER(aComposeObject);
+
+ nsresult rv;
+
+ // add the msg compose / dom window mapping to our hash table
+ nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsWeakPtr weakMsgComposePtr = do_GetWeakReference(aComposeObject);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mOpenComposeWindows.InsertOrUpdate(weakDocShell, weakMsgComposePtr);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::UnregisterComposeDocShell(nsIDocShell* aDocShell) {
+ NS_ENSURE_ARG_POINTER(aDocShell);
+
+ nsresult rv;
+ nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mOpenComposeWindows.Remove(weakDocShell);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::GetMsgComposeForDocShell(nsIDocShell* aDocShell,
+ nsIMsgCompose** aComposeObject) {
+ NS_ENSURE_ARG_POINTER(aDocShell);
+ NS_ENSURE_ARG_POINTER(aComposeObject);
+
+ if (!mOpenComposeWindows.Count()) return NS_ERROR_FAILURE;
+
+ // get the weak reference for our dom window
+ nsresult rv;
+ nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsWeakPtr weakMsgComposePtr;
+
+ if (!mOpenComposeWindows.Get(weakDocShell, getter_AddRefs(weakMsgComposePtr)))
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(weakMsgComposePtr, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*aComposeObject = msgCompose);
+ return rv;
+}
+
+/**
+ * LoadDraftOrTemplate
+ * Helper routine used to run msgURI through libmime in order to fetch the
+ * contents for a draft or template.
+ */
+nsresult nsMsgComposeService::LoadDraftOrTemplate(
+ const nsACString& aMsgURI, nsMimeOutputType aOutType,
+ nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI,
+ nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, bool overrideComposeFormat,
+ nsIMsgWindow* aMsgWindow, bool autodetectCharset) {
+ return RunMessageThroughMimeDraft(
+ aMsgURI, aOutType, aIdentity, aOriginalMsgURI, aOrigMsgHdr,
+ aForwardInline, EmptyString(), overrideComposeFormat, aMsgWindow,
+ autodetectCharset);
+}
+
+/**
+ * Run the aMsgURI message through libmime. We set various attributes of the
+ * nsIMimeStreamConverter so mimedrft.cpp will know what to do with the message
+ * when its done streaming. Usually that will be opening a compose window
+ * with the contents of the message, but if forwardTo is non-empty, mimedrft.cpp
+ * will forward the contents directly.
+ *
+ * @param aMsgURI URI to stream, which is the msgUri + any extra terms, e.g.,
+ * "redirect=true".
+ * @param aOutType nsMimeOutput::nsMimeMessageDraftOrTemplate or
+ * nsMimeOutput::nsMimeMessageEditorTemplate
+ * @param aIdentity identity to use for the new message
+ * @param aOriginalMsgURI msgURI w/o any extra terms
+ * @param aOrigMsgHdr nsIMsgDBHdr corresponding to aOriginalMsgURI
+ * @param aForwardInline true if doing a forward inline
+ * @param aForwardTo e-mail address to forward msg to. This is used for
+ * forward inline message filter actions.
+ * @param aOverrideComposeFormat True if the user had shift key down when
+ doing a command that opens the compose window,
+ * which means we switch the compose window used
+ * from the default.
+ * @param aMsgWindow msgWindow to pass into LoadMessage.
+ */
+nsresult nsMsgComposeService::RunMessageThroughMimeDraft(
+ const nsACString& aMsgURI, nsMimeOutputType aOutType,
+ nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI,
+ nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, const nsAString& aForwardTo,
+ bool aOverrideComposeFormat, nsIMsgWindow* aMsgWindow,
+ bool autodetectCharset) {
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsresult rv =
+ GetMessageServiceFromURI(aMsgURI, getter_AddRefs(messageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create a mime parser (nsIMimeStreamConverter)to do the conversion.
+ nsCOMPtr<nsIMimeStreamConverter> mimeConverter = do_CreateInstance(
+ "@mozilla.org/streamconv;1?from=message/rfc822&to=application/xhtml+xml",
+ &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mimeConverter->SetMimeOutputType(
+ aOutType); // Set the type of output for libmime
+ mimeConverter->SetForwardInline(aForwardInline);
+ if (!aForwardTo.IsEmpty()) {
+ mimeConverter->SetForwardInlineFilter(true);
+ mimeConverter->SetForwardToAddress(aForwardTo);
+ }
+ mimeConverter->SetOverrideComposeFormat(aOverrideComposeFormat);
+ mimeConverter->SetIdentity(aIdentity);
+ mimeConverter->SetOriginalMsgURI(aOriginalMsgURI);
+ mimeConverter->SetOrigMsgHdr(aOrigMsgHdr);
+
+ nsCOMPtr<nsIURI> url;
+ bool fileUrl = StringBeginsWith(aMsgURI, "file:"_ns);
+ nsCString mailboxUri(aMsgURI);
+ if (fileUrl) {
+ // We loaded a .eml file from a file: url. Construct equivalent mailbox url.
+ mailboxUri.Replace(0, 5, "mailbox:"_ns);
+ mailboxUri.AppendLiteral("&number=0");
+ // Need this to prevent nsMsgCompose::TagEmbeddedObjects from setting
+ // inline images as moz-do-not-send.
+ mimeConverter->SetOriginalMsgURI(mailboxUri);
+ }
+ if (fileUrl || PromiseFlatCString(aMsgURI).Find(
+ "&type=application/x-message-display") >= 0)
+ rv = NS_NewURI(getter_AddRefs(url), mailboxUri);
+ else
+ rv = messageService->GetUrlForUri(aMsgURI, aMsgWindow, getter_AddRefs(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(url);
+ if (!mailnewsurl) {
+ NS_WARNING(
+ "Trying to run a message through MIME which doesn't have a "
+ "nsIMsgMailNewsUrl?");
+ return NS_ERROR_UNEXPECTED;
+ }
+ // SetSpecInternal must not fail, or else the URL won't have a base URL and
+ // we'll crash later.
+ rv = mailnewsurl->SetSpecInternal(mailboxUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we are forwarding a message and that message used a charset override
+ // then forward that as auto-detect flag, too.
+ nsCOMPtr<nsIMsgI18NUrl> i18nUrl(do_QueryInterface(url));
+ if (i18nUrl) (void)i18nUrl->SetAutodetectCharset(autodetectCharset);
+
+ nsCOMPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewInputStreamChannel(
+ getter_AddRefs(channel), url, nullptr, nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed.");
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIStreamConverter> converter = do_QueryInterface(mimeConverter);
+ rv = converter->AsyncConvertData(nullptr, nullptr, nullptr, channel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now, just plug the two together and get the hell out of the way!
+ nsCOMPtr<nsIStreamListener> streamListener = do_QueryInterface(mimeConverter);
+ return messageService->LoadMessage(aMsgURI, streamListener, aMsgWindow,
+ nullptr, autodetectCharset);
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::Handle(nsICommandLine* aCmdLine) {
+ NS_ENSURE_ARG_POINTER(aCmdLine);
+
+ nsresult rv;
+ int32_t found, end, count;
+ nsAutoString uristr;
+ bool composeShouldHandle = true;
+
+ rv = aCmdLine->FindFlag(u"compose"_ns, false, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifndef MOZ_SUITE
+ // MAC OS X passes in -url mailto:mscott@mozilla.org into the command line
+ // instead of -compose.
+ if (found == -1) {
+ rv = aCmdLine->FindFlag(u"url"_ns, false, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we don't want to consume the argument for -url unless we're sure it is a
+ // mailto url and we'll figure that out shortly.
+ composeShouldHandle = false;
+ }
+#endif
+
+ if (found == -1) return NS_OK;
+
+ end = found;
+
+ rv = aCmdLine->GetLength(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (count > found + 1) {
+ aCmdLine->GetArgument(found + 1, uristr);
+ if (StringBeginsWith(uristr, u"mailto:"_ns) ||
+ StringBeginsWith(uristr, u"preselectid="_ns) ||
+ StringBeginsWith(uristr, u"to="_ns) ||
+ StringBeginsWith(uristr, u"cc="_ns) ||
+ StringBeginsWith(uristr, u"bcc="_ns) ||
+ StringBeginsWith(uristr, u"newsgroups="_ns) ||
+ StringBeginsWith(uristr, u"subject="_ns) ||
+ StringBeginsWith(uristr, u"format="_ns) ||
+ StringBeginsWith(uristr, u"body="_ns) ||
+ StringBeginsWith(uristr, u"attachment="_ns) ||
+ StringBeginsWith(uristr, u"message="_ns) ||
+ StringBeginsWith(uristr, u"from="_ns)) {
+ composeShouldHandle = true; // the -url argument looks like mailto
+ end++;
+ // mailto: URIs are frequently passed with spaces in them. They should be
+ // escaped with %20, but we hack around broken clients. See bug 231032.
+ while (end + 1 < count) {
+ nsAutoString curarg;
+ aCmdLine->GetArgument(end + 1, curarg);
+ if (curarg.First() == '-') break;
+
+ uristr.Append(' ');
+ uristr.Append(curarg);
+ ++end;
+ }
+ } else {
+ uristr.Truncate();
+ }
+ }
+ if (composeShouldHandle) {
+ aCmdLine->RemoveArguments(found, end);
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ NS_ENSURE_TRUE(wwatch, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsISupportsString> arg(
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ if (arg) arg->SetData(uristr);
+
+ nsCOMPtr<nsIMutableArray> params(do_CreateInstance(NS_ARRAY_CONTRACTID));
+ params->AppendElement(arg);
+ params->AppendElement(aCmdLine);
+
+ nsCOMPtr<mozIDOMWindowProxy> opened;
+ wwatch->OpenWindow(nullptr, DEFAULT_CHROME, "_blank"_ns,
+ "chrome,dialog=no,all"_ns, params,
+ getter_AddRefs(opened));
+
+ aCmdLine->SetPreventDefault(true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeService::GetHelpInfo(nsACString& aResult) {
+ // clang-format off
+ aResult.AssignLiteral(
+ " -compose [ <options> ] Compose a mail or news message. Options are specified\n"
+ " as string \"option='value,...',option=value,...\" and\n"
+ " include: from, to, cc, bcc, newsgroups, subject, body,\n"
+ " message (file), attachment (file), format (html | text).\n"
+ " Example: \"to=john@example.com,subject='Dinner tonight?'\"\n");
+ return NS_OK;
+ // clang-format on
+}
diff --git a/comm/mailnews/compose/src/nsMsgComposeService.h b/comm/mailnews/compose/src/nsMsgComposeService.h
new file mode 100644
index 0000000000..97911fb71f
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgComposeService.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#define MSGCOMP_TRACE_PERFORMANCE 1
+
+#include "nsIMsgComposeService.h"
+#include "nsCOMPtr.h"
+#include "mozIDOMWindow.h"
+#include "nsIAppWindow.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReference.h"
+#include "nsIMimeStreamConverter.h"
+#include "nsInterfaceHashtable.h"
+
+#include "nsICommandLineHandler.h"
+#define ICOMMANDLINEHANDLER nsICommandLineHandler
+
+class nsMsgComposeService : public nsIMsgComposeService,
+ public ICOMMANDLINEHANDLER,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgComposeService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOMPOSESERVICE
+ NS_DECL_NSICOMMANDLINEHANDLER
+
+ nsresult Init();
+ void Reset();
+ void DeleteCachedWindows();
+ nsresult AddGlobalHtmlDomains();
+
+ private:
+ virtual ~nsMsgComposeService();
+ bool mLogComposePerformance;
+
+ nsresult LoadDraftOrTemplate(
+ const nsACString& aMsgURI, nsMimeOutputType aOutType,
+ nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI,
+ nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, bool overrideComposeFormat,
+ nsIMsgWindow* aMsgWindow, bool autodetectCharset);
+
+ nsresult RunMessageThroughMimeDraft(
+ const nsACString& aMsgURI, nsMimeOutputType aOutType,
+ nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI,
+ nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, const nsAString& forwardTo,
+ bool overrideComposeFormat, nsIMsgWindow* aMsgWindow,
+ bool autodetectCharset);
+
+ // hash table mapping dom windows to nsIMsgCompose objects
+ nsInterfaceHashtable<nsISupportsHashKey, nsIWeakReference>
+ mOpenComposeWindows;
+
+ // When doing a reply and the settings are enabled, get the HTML of the
+ // selected text in the original message window so that it can be quoted
+ // instead of the entire message.
+ nsresult GetOrigWindowSelection(MSG_ComposeType type,
+ mozilla::dom::Selection* selection,
+ nsACString& aSelHTML);
+
+#ifdef MSGCOMP_TRACE_PERFORMANCE
+ PRIntervalTime mStartTime;
+ PRIntervalTime mPreviousTime;
+#endif
+};
diff --git a/comm/mailnews/compose/src/nsMsgCopy.cpp b/comm/mailnews/compose/src/nsMsgCopy.cpp
new file mode 100644
index 0000000000..0cccc108f9
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCopy.cpp
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMsgCopy.h"
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsIThread.h"
+#include "nscore.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/RefCountType.h"
+#include "mozilla/RefPtr.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsISupports.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsMsgCompUtils.h"
+#include "prcmon.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsThreadUtils.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgProgress.h"
+#include "nsComposeStrings.h"
+#include "prmem.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsIURIMutator.h"
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for the copy operation. We have to create this
+// class to listen for message copy completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+NS_IMPL_ISUPPORTS(CopyListener, nsIMsgCopyServiceListener)
+
+CopyListener::CopyListener(void) { mCopyInProgress = false; }
+
+CopyListener::~CopyListener(void) {}
+
+nsresult CopyListener::OnStartCopy() {
+#ifdef NS_DEBUG
+ printf("CopyListener::OnStartCopy()\n");
+#endif
+
+ if (mComposeAndSend) mComposeAndSend->NotifyListenerOnStartCopy();
+ return NS_OK;
+}
+
+nsresult CopyListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+#ifdef NS_DEBUG
+ printf("CopyListener::OnProgress() %d of %d\n", aProgress, aProgressMax);
+#endif
+
+ if (mComposeAndSend)
+ mComposeAndSend->NotifyListenerOnProgressCopy(aProgress, aProgressMax);
+
+ return NS_OK;
+}
+
+nsresult CopyListener::SetMessageKey(nsMsgKey aMessageKey) {
+ if (mComposeAndSend) mComposeAndSend->SetMessageKey(aMessageKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CopyListener::GetMessageId(nsACString& aMessageId) {
+ if (mComposeAndSend) mComposeAndSend->GetMessageId(aMessageId);
+ return NS_OK;
+}
+
+nsresult CopyListener::OnStopCopy(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+#ifdef NS_DEBUG
+ printf("CopyListener: SUCCESSFUL ON THE COPY OPERATION!\n");
+#endif
+ } else {
+#ifdef NS_DEBUG
+ printf("CopyListener: COPY OPERATION FAILED!\n");
+#endif
+ }
+
+ if (mCopyInProgress) {
+ PR_CEnterMonitor(this);
+ PR_CNotifyAll(this);
+ mCopyInProgress = false;
+ PR_CExitMonitor(this);
+ }
+ if (mComposeAndSend) mComposeAndSend->NotifyListenerOnStopCopy(aStatus);
+
+ return NS_OK;
+}
+
+nsresult CopyListener::SetMsgComposeAndSendObject(nsIMsgSend* obj) {
+ if (obj) mComposeAndSend = obj;
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// END END END END END END END END END END END END END END END
+// This is the listener class for the copy operation. We have to create this
+// class to listen for message copy completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMPL_ISUPPORTS(nsMsgCopy, nsIMsgCopy, nsIUrlListener)
+
+nsMsgCopy::nsMsgCopy() {
+ mFile = nullptr;
+ mMode = nsIMsgSend::nsMsgDeliverNow;
+ mSavePref = nullptr;
+ mIsDraft = false;
+ mMsgFlags = nsMsgMessageFlags::Read;
+}
+
+nsMsgCopy::~nsMsgCopy() { PR_Free(mSavePref); }
+
+NS_IMETHODIMP
+nsMsgCopy::StartCopyOperation(nsIMsgIdentity* aUserIdentity, nsIFile* aFile,
+ nsMsgDeliverMode aMode, nsIMsgSend* aMsgSendObj,
+ const nsACString& aSavePref,
+ nsIMsgDBHdr* aMsgToReplace) {
+ nsCOMPtr<nsIMsgFolder> dstFolder;
+ bool isDraft = false;
+ uint32_t msgFlags = nsMsgMessageFlags::Read;
+ bool waitForUrl = false;
+ nsresult rv;
+
+ if (!aMsgSendObj) return NS_ERROR_INVALID_ARG;
+
+ // Store away the server location...
+ if (!aSavePref.IsEmpty()) mSavePref = ToNewCString(aSavePref);
+
+ //
+ // Vars for implementation...
+ //
+
+ // QueueForLater (Outbox)
+ if (aMode == nsIMsgSend::nsMsgQueueForLater ||
+ aMode == nsIMsgSend::nsMsgDeliverBackground) {
+ rv = GetUnsentMessagesFolder(aUserIdentity, getter_AddRefs(dstFolder),
+ &waitForUrl);
+ isDraft = false;
+ // Do not mark outgoing messages as read.
+ msgFlags = 0;
+ if (!dstFolder || NS_FAILED(rv)) {
+ return NS_MSG_UNABLE_TO_SEND_LATER;
+ }
+ } else if (aMode == nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts)
+ {
+ rv = GetDraftsFolder(aUserIdentity, getter_AddRefs(dstFolder), &waitForUrl);
+ isDraft = true;
+ // Do not mark drafts as read.
+ msgFlags = 0;
+ if (!dstFolder || NS_FAILED(rv)) return NS_MSG_UNABLE_TO_SAVE_DRAFT;
+ } else if (aMode ==
+ nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates)
+ {
+ rv = GetTemplatesFolder(aUserIdentity, getter_AddRefs(dstFolder),
+ &waitForUrl);
+ // Mark saved templates as read.
+ isDraft = false;
+ msgFlags = nsMsgMessageFlags::Read;
+ if (!dstFolder || NS_FAILED(rv)) return NS_MSG_UNABLE_TO_SAVE_TEMPLATE;
+ } else // SaveInSentFolder (Sent) - nsMsgDeliverNow or nsMsgSendUnsent
+ {
+ rv = GetSentFolder(aUserIdentity, getter_AddRefs(dstFolder), &waitForUrl);
+ // Mark send messages as read.
+ isDraft = false;
+ msgFlags = nsMsgMessageFlags::Read;
+ if (!dstFolder || NS_FAILED(rv)) return NS_MSG_COULDNT_OPEN_FCC_FOLDER;
+ }
+
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+
+ if (aMsgSendObj) {
+ nsCOMPtr<nsIMsgProgress> progress;
+ aMsgSendObj->GetProgress(getter_AddRefs(progress));
+ if (progress) progress->GetMsgWindow(getter_AddRefs(msgWindow));
+ }
+
+ mMode = aMode;
+ mFile = aFile;
+ mDstFolder = dstFolder;
+ mMsgToReplace = aMsgToReplace;
+ mIsDraft = isDraft;
+ mMsgSendObj = aMsgSendObj;
+ mMsgFlags = msgFlags;
+ if (!waitForUrl) {
+ // cache info needed for DoCopy and call DoCopy when OnStopUrl is called.
+ rv = DoCopy(aFile, dstFolder, aMsgToReplace, isDraft, msgFlags, msgWindow,
+ aMsgSendObj);
+ // N.B. "this" may be deleted when this call returns.
+ }
+ return rv;
+}
+
+nsresult nsMsgCopy::DoCopy(nsIFile* aDiskFile, nsIMsgFolder* dstFolder,
+ nsIMsgDBHdr* aMsgToReplace, bool aIsDraft,
+ uint32_t aMsgFlags, nsIMsgWindow* msgWindow,
+ nsIMsgSend* aMsgSendObj) {
+ nsresult rv = NS_OK;
+
+ // Check sanity
+ if ((!aDiskFile) || (!dstFolder)) return NS_ERROR_INVALID_ARG;
+
+ // Call copyservice with dstFolder, disk file, and txnManager
+ if (NS_SUCCEEDED(rv)) {
+ RefPtr<CopyListener> copyListener = new CopyListener();
+ if (!copyListener) return NS_ERROR_OUT_OF_MEMORY;
+
+ copyListener->SetMsgComposeAndSendObject(aMsgSendObj);
+ nsCOMPtr<nsIThread> thread;
+
+ if (aIsDraft) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(dstFolder);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+ bool shutdownInProgress = false;
+ rv = accountManager->GetShutdownInProgress(&shutdownInProgress);
+
+ if (NS_SUCCEEDED(rv) && shutdownInProgress && imapFolder) {
+ // set the following only when we were in the middle of shutdown
+ // process
+ copyListener->mCopyInProgress = true;
+ thread = do_GetCurrentThread();
+ }
+ }
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyService->CopyFileMessage(aDiskFile, dstFolder, aMsgToReplace,
+ aIsDraft, aMsgFlags, EmptyCString(),
+ copyListener, msgWindow);
+ // copyListener->mCopyInProgress can only be set when we are in the
+ // middle of the shutdown process
+ while (copyListener->mCopyInProgress) {
+ PR_CEnterMonitor(copyListener);
+ PR_CWait(copyListener, PR_MicrosecondsToInterval(1000UL));
+ PR_CExitMonitor(copyListener);
+ if (thread) NS_ProcessPendingEvents(thread);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCopy::GetDstFolder(nsIMsgFolder** aDstFolder) {
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+ NS_IF_ADDREF(*aDstFolder = mDstFolder);
+ return NS_OK;
+}
+
+// nsIUrlListener methods
+NS_IMETHODIMP
+nsMsgCopy::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgCopy::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ nsresult rv = aExitCode;
+ if (NS_SUCCEEDED(aExitCode)) {
+ rv = DoCopy(mFile, mDstFolder, mMsgToReplace, mIsDraft, mMsgFlags, nullptr,
+ mMsgSendObj);
+ }
+ return rv;
+}
+
+nsresult nsMsgCopy::GetUnsentMessagesFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** folder,
+ bool* waitForUrl) {
+ nsresult ret = LocateMessageFolder(
+ userIdentity, nsIMsgSend::nsMsgQueueForLater, mSavePref, folder);
+ if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Queue);
+ CreateIfMissing(folder, waitForUrl);
+ return ret;
+}
+
+nsresult nsMsgCopy::GetDraftsFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** folder, bool* waitForUrl) {
+ nsresult ret = LocateMessageFolder(userIdentity, nsIMsgSend::nsMsgSaveAsDraft,
+ mSavePref, folder);
+ if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Drafts);
+ CreateIfMissing(folder, waitForUrl);
+ return ret;
+}
+
+nsresult nsMsgCopy::GetTemplatesFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** folder,
+ bool* waitForUrl) {
+ nsresult ret = LocateMessageFolder(
+ userIdentity, nsIMsgSend::nsMsgSaveAsTemplate, mSavePref, folder);
+ if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Templates);
+ CreateIfMissing(folder, waitForUrl);
+ return ret;
+}
+
+nsresult nsMsgCopy::GetSentFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** folder, bool* waitForUrl) {
+ nsresult ret = LocateMessageFolder(userIdentity, nsIMsgSend::nsMsgDeliverNow,
+ mSavePref, folder);
+ if (*folder) {
+ // If mSavePref is the same as the identity's fcc folder, set the sent flag.
+ nsCString identityFccUri;
+ userIdentity->GetFccFolder(identityFccUri);
+ if (identityFccUri.Equals(mSavePref))
+ (*folder)->SetFlag(nsMsgFolderFlags::SentMail);
+ }
+ CreateIfMissing(folder, waitForUrl);
+ return ret;
+}
+
+nsresult nsMsgCopy::CreateIfMissing(nsIMsgFolder** folder, bool* waitForUrl) {
+ nsresult rv = NS_OK;
+ if (folder && *folder) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ (*folder)->GetParent(getter_AddRefs(parent));
+ if (!parent) {
+ nsCOMPtr<nsIFile> folderPath;
+ // for local folders, path is to the berkeley mailbox.
+ // for imap folders, path needs to have .msf appended to the name
+ (*folder)->GetFilePath(getter_AddRefs(folderPath));
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = (*folder)->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = server->GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isAsyncFolder;
+ rv = protocolInfo->GetFoldersCreatedAsync(&isAsyncFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we can't get the path from the folder, then try to create the
+ // storage. for imap, it doesn't matter if the .msf file exists - it still
+ // might not exist on the server, so we should try to create it
+ bool exists = false;
+ if (!isAsyncFolder && folderPath) folderPath->Exists(&exists);
+ if (!exists) {
+ (*folder)->CreateStorageIfMissing(this);
+ if (isAsyncFolder) *waitForUrl = true;
+
+ rv = NS_OK;
+ }
+ }
+ }
+ return rv;
+}
+////////////////////////////////////////////////////////////////////////////////////
+// Utility Functions for MsgFolders
+////////////////////////////////////////////////////////////////////////////////////
+nsresult LocateMessageFolder(nsIMsgIdentity* userIdentity,
+ nsMsgDeliverMode aFolderType,
+ const char* aFolderURI, nsIMsgFolder** msgFolder) {
+ nsresult rv = NS_OK;
+
+ if (!msgFolder) return NS_ERROR_NULL_POINTER;
+ *msgFolder = nullptr;
+
+ if (!aFolderURI || !*aFolderURI) return NS_ERROR_INVALID_ARG;
+
+ // as long as it doesn't start with anyfolder://
+ if (PL_strncasecmp(ANY_SERVER, aFolderURI, strlen(aFolderURI)) != 0) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(nsDependentCString(aFolderURI),
+ getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Don't check validity of folder - caller will handle creating it.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // make sure that folder hierarchy is built so that legitimate parent-child
+ // relationship is established.
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetMsgFolderFromURI(folder, nsDependentCString(aFolderURI),
+ msgFolder);
+ } else {
+ if (!userIdentity) return NS_ERROR_INVALID_ARG;
+
+ // get the account manager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If any folder will do, go look for one.
+ nsTArray<RefPtr<nsIMsgIncomingServer>> servers;
+ rv = accountManager->GetServersForIdentity(userIdentity, servers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ok, we have to look through the servers and try to find the server that
+ // has a valid folder of the type that interests us...
+ for (auto inServer : servers) {
+ // Now that we have the server...we need to get the named message folder
+
+ // If aFolderURI is passed in, then the user has chosen a specific
+ // mail folder to save the message, but if it is null, just find the
+ // first one and make that work. The folder is specified as a URI, like
+ // the following:
+ //
+ // mailbox://nobody@Local Folders/Sent
+ // imap://rhp@nsmail-2/Drafts
+ // newsgroup://news.mozilla.org/netscape.test
+ //
+ nsCString serverURI;
+ rv = inServer->GetServerURI(serverURI);
+ if (NS_FAILED(rv) || serverURI.IsEmpty()) continue;
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = inServer->GetRootFolder(getter_AddRefs(rootFolder));
+
+ if (NS_FAILED(rv) || (!rootFolder)) continue;
+
+ // use the defaults by getting the folder by flags
+ if (aFolderType == nsIMsgSend::nsMsgQueueForLater ||
+ aFolderType == nsIMsgSend::nsMsgDeliverBackground) {
+ // QueueForLater (Outbox)
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Queue, msgFolder);
+ } else if (aFolderType ==
+ nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts)
+ {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Drafts, msgFolder);
+ } else if (aFolderType ==
+ nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates)
+ {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Templates, msgFolder);
+ } else // SaveInSentFolder (Sent) - nsMsgDeliverNow or nsMsgSendUnsent
+ {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::SentMail, msgFolder);
+ }
+
+ if (*msgFolder) {
+ return NS_OK;
+ }
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+//
+// Figure out if a folder is local or not and return a boolean to
+// say so.
+//
+nsresult MessageFolderIsLocal(nsIMsgIdentity* userIdentity,
+ nsMsgDeliverMode aFolderType,
+ const char* aFolderURI, bool* aResult) {
+ nsresult rv;
+
+ if (!aFolderURI) return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(nsDependentCString(aFolderURI))
+ .Finalize(url);
+ if (NS_FAILED(rv)) return rv;
+
+ /* mailbox:/ means its local (on disk) */
+ rv = url->SchemeIs("mailbox", aResult);
+ if (NS_FAILED(rv)) return rv;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgCopy.h b/comm/mailnews/compose/src/nsMsgCopy.h
new file mode 100644
index 0000000000..4fa50bd8ac
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgCopy.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgCopy_H_
+#define _nsMsgCopy_H_
+
+#include "mozilla/Attributes.h"
+#include "nscore.h"
+#include "nsIFile.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsITransactionManager.h"
+#include "nsIMsgCopy.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgCopyService.h"
+
+// Forward declarations...
+class nsMsgCopy;
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for the copy operation. We have to create this
+// class to listen for message copy completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+class CopyListener : public nsIMsgCopyServiceListener {
+ public:
+ CopyListener(void);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD OnStartCopy() override;
+
+ NS_IMETHOD OnProgress(uint32_t aProgress, uint32_t aProgressMax) override;
+
+ NS_IMETHOD SetMessageKey(nsMsgKey aMessageKey) override;
+
+ NS_IMETHOD GetMessageId(nsACString& aMessageId) override;
+
+ NS_IMETHOD OnStopCopy(nsresult aStatus) override;
+
+ NS_IMETHOD SetMsgComposeAndSendObject(nsIMsgSend* obj);
+
+ bool mCopyInProgress;
+
+ private:
+ virtual ~CopyListener();
+ nsCOMPtr<nsIMsgSend> mComposeAndSend;
+};
+
+//
+// This is a class that deals with processing remote attachments. It implements
+// an nsIStreamListener interface to deal with incoming data
+//
+class nsMsgCopy : public nsIMsgCopy, public nsIUrlListener {
+ public:
+ nsMsgCopy();
+
+ // nsISupports interface
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOPY
+ NS_DECL_NSIURLLISTENER
+
+ //////////////////////////////////////////////////////////////////////
+ // Object methods...
+ //////////////////////////////////////////////////////////////////////
+ //
+ nsresult DoCopy(nsIFile* aDiskFile, nsIMsgFolder* dstFolder,
+ nsIMsgDBHdr* aMsgToReplace, bool aIsDraft, uint32_t aMsgFlags,
+ nsIMsgWindow* msgWindow, nsIMsgSend* aMsgSendObj);
+
+ nsresult GetUnsentMessagesFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** msgFolder, bool* waitForUrl);
+ nsresult GetDraftsFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** msgFolder, bool* waitForUrl);
+ nsresult GetTemplatesFolder(nsIMsgIdentity* userIdentity,
+ nsIMsgFolder** msgFolder, bool* waitForUrl);
+ nsresult GetSentFolder(nsIMsgIdentity* userIdentity, nsIMsgFolder** msgFolder,
+ bool* waitForUrl);
+ nsresult CreateIfMissing(nsIMsgFolder** folder, bool* waitForUrl);
+
+ //
+ // Vars for implementation...
+ //
+ nsIFile* mFile; // the file we are sending...
+ nsMsgDeliverMode mMode;
+ nsCOMPtr<nsIMsgFolder> mDstFolder;
+ nsCOMPtr<nsIMsgDBHdr> mMsgToReplace;
+ bool mIsDraft;
+ uint32_t mMsgFlags;
+ nsCOMPtr<nsIMsgSend> mMsgSendObj;
+ char* mSavePref;
+
+ private:
+ virtual ~nsMsgCopy();
+};
+
+// Useful function for the back end...
+nsresult LocateMessageFolder(nsIMsgIdentity* userIdentity,
+ nsMsgDeliverMode aFolderType, const char* aSaveURI,
+ nsIMsgFolder** msgFolder);
+
+nsresult MessageFolderIsLocal(nsIMsgIdentity* userIdentity,
+ nsMsgDeliverMode aFolderType,
+ const char* aSaveURI, bool* aResult);
+
+#endif /* _nsMsgCopy_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgPrompts.cpp b/comm/mailnews/compose/src/nsMsgPrompts.cpp
new file mode 100644
index 0000000000..ff0f133285
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgPrompts.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMsgPrompts.h"
+
+#include "nsMsgCopy.h"
+#include "nsIPrompt.h"
+#include "nsIWindowWatcher.h"
+#include "nsComposeStrings.h"
+#include "nsIStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+nsresult nsMsgGetMessageByName(const char* aName, nsString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return bundle->GetStringFromName(aName, aResult);
+}
+
+static nsresult nsMsgBuildMessageByName(const char* aName, nsIFile* aFile,
+ nsString& aResult) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString path;
+ aFile->GetPath(path);
+
+ AutoTArray<nsString, 1> params = {path};
+ return bundle->FormatStringFromName(aName, params, aResult);
+}
+
+nsresult nsMsgBuildMessageWithFile(nsIFile* aFile, nsString& aResult) {
+ return nsMsgBuildMessageByName("unableToOpenFile", aFile, aResult);
+}
+
+nsresult nsMsgBuildMessageWithTmpFile(nsIFile* aFile, nsString& aResult) {
+ return nsMsgBuildMessageByName("unableToOpenTmpFile", aFile, aResult);
+}
+
+nsresult nsMsgDisplayMessageByName(mozIDOMWindowProxy* window,
+ const char* aName,
+ const char16_t* windowTitle) {
+ nsString msg;
+ nsMsgGetMessageByName(aName, msg);
+ return nsMsgDisplayMessageByString(window, msg.get(), windowTitle);
+}
+
+nsresult nsMsgDisplayMessageByString(mozIDOMWindowProxy* window,
+ const char16_t* msg,
+ const char16_t* windowTitle) {
+ NS_ENSURE_ARG_POINTER(msg);
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dlgService->Alert(window, windowTitle, msg);
+}
+
+nsresult nsMsgAskBooleanQuestionByString(mozIDOMWindowProxy* window,
+ const char16_t* msg, bool* answer,
+ const char16_t* windowTitle) {
+ NS_ENSURE_TRUE(msg && *msg, NS_ERROR_INVALID_ARG);
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dlgService->Confirm(window, windowTitle, msg, answer);
+}
diff --git a/comm/mailnews/compose/src/nsMsgPrompts.h b/comm/mailnews/compose/src/nsMsgPrompts.h
new file mode 100644
index 0000000000..300b6ffee2
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgPrompts.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgPrompts_H_
+#define _nsMsgPrompts_H_
+
+#include "nscore.h"
+#include "nsError.h"
+#include "nsString.h"
+
+class mozIDOMWindowProxy;
+
+nsresult nsMsgGetMessageByName(const char* aName, nsString& aResult);
+nsresult nsMsgBuildMessageWithFile(nsIFile* aFile, nsString& aResult);
+nsresult nsMsgBuildMessageWithTmpFile(nsIFile* aFile, nsString& aResult);
+nsresult nsMsgDisplayMessageByName(mozIDOMWindowProxy* window,
+ const char* aName,
+ const char16_t* windowTitle = nullptr);
+nsresult nsMsgDisplayMessageByString(mozIDOMWindowProxy* window,
+ const char16_t* msg,
+ const char16_t* windowTitle = nullptr);
+nsresult nsMsgAskBooleanQuestionByString(mozIDOMWindowProxy* window,
+ const char16_t* msg, bool* answer,
+ const char16_t* windowTitle = nullptr);
+
+#endif /* _nsMsgPrompts_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgQuote.cpp b/comm/mailnews/compose/src/nsMsgQuote.cpp
new file mode 100644
index 0000000000..75efbf67ca
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgQuote.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIURL.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIServiceManager.h"
+#include "nsIStreamListener.h"
+#include "nsIStreamConverter.h"
+#include "nsIStreamConverterService.h"
+#include "nsIMimeStreamConverter.h"
+#include "nsMimeTypes.h"
+#include "nsICharsetConverterManager.h"
+#include "prprf.h"
+#include "nsMsgQuote.h"
+#include "nsMsgCompUtils.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsNetUtil.h"
+#include "nsMsgCompose.h"
+#include "nsMsgMailNewsUrl.h"
+#include "mozilla/Components.h"
+#include "nsContentUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgQuoteListener, nsIMsgQuoteListener,
+ nsIMimeStreamConverterListener)
+
+nsMsgQuoteListener::nsMsgQuoteListener() {}
+
+nsMsgQuoteListener::~nsMsgQuoteListener() {}
+
+NS_IMETHODIMP nsMsgQuoteListener::SetMsgQuote(nsIMsgQuote* msgQuote) {
+ mMsgQuote = do_GetWeakReference(msgQuote);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuoteListener::GetMsgQuote(nsIMsgQuote** aMsgQuote) {
+ nsresult rv = NS_OK;
+ if (aMsgQuote) {
+ nsCOMPtr<nsIMsgQuote> msgQuote = do_QueryReferent(mMsgQuote);
+ msgQuote.forget(aMsgQuote);
+ } else
+ rv = NS_ERROR_NULL_POINTER;
+
+ return rv;
+}
+
+nsresult nsMsgQuoteListener::OnHeadersReady(nsIMimeHeaders* headers) {
+ nsCOMPtr<nsIMsgQuotingOutputStreamListener> quotingOutputStreamListener;
+ nsCOMPtr<nsIMsgQuote> msgQuote = do_QueryReferent(mMsgQuote);
+
+ if (msgQuote)
+ msgQuote->GetStreamListener(getter_AddRefs(quotingOutputStreamListener));
+
+ if (quotingOutputStreamListener)
+ quotingOutputStreamListener->SetMimeHeaders(headers);
+ return NS_OK;
+}
+
+//
+// Implementation...
+//
+nsMsgQuote::nsMsgQuote() {
+ mQuoteHeaders = false;
+ mQuoteListener = nullptr;
+}
+
+nsMsgQuote::~nsMsgQuote() {}
+
+NS_IMPL_ISUPPORTS(nsMsgQuote, nsIMsgQuote, nsISupportsWeakReference)
+
+NS_IMETHODIMP nsMsgQuote::GetStreamListener(
+ nsIMsgQuotingOutputStreamListener** aStreamListener) {
+ if (!aStreamListener) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsCOMPtr<nsIMsgQuotingOutputStreamListener> streamListener =
+ do_QueryReferent(mStreamListener);
+ if (!streamListener) {
+ return NS_ERROR_FAILURE;
+ }
+ NS_IF_ADDREF(*aStreamListener = streamListener);
+ return NS_OK;
+}
+
+nsresult nsMsgQuote::QuoteMessage(
+ const nsACString& msgURI, bool quoteHeaders,
+ nsIMsgQuotingOutputStreamListener* aQuoteMsgStreamListener,
+ bool aAutodetectCharset, bool headersOnly, nsIMsgDBHdr* aMsgHdr) {
+ nsresult rv;
+
+ mQuoteHeaders = quoteHeaders;
+ mStreamListener = do_GetWeakReference(aQuoteMsgStreamListener);
+
+ nsAutoCString msgUri(msgURI);
+ bool fileUrl = StringBeginsWith(msgUri, "file:"_ns);
+ bool forwardedMessage = msgUri.Find("&realtype=message/rfc822") >= 0;
+ nsCOMPtr<nsIURI> newURI;
+ if (fileUrl) {
+ msgUri.Replace(0, 5, "mailbox:"_ns);
+ msgUri.AppendLiteral("?number=0");
+ rv = NS_NewURI(getter_AddRefs(newURI), msgUri);
+ } else if (forwardedMessage)
+ rv = NS_NewURI(getter_AddRefs(newURI), msgURI);
+ else {
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ rv = GetMessageServiceFromURI(msgURI, getter_AddRefs(msgService));
+ if (NS_FAILED(rv)) return rv;
+ rv = msgService->GetUrlForUri(msgURI, nullptr, getter_AddRefs(newURI));
+ }
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString queryPart;
+ rv = newURI->GetQuery(queryPart);
+ if (!queryPart.IsEmpty()) queryPart.Append('&');
+
+ if (headersOnly) /* We don't need to quote the message body but we still need
+ to extract the headers */
+ queryPart.AppendLiteral("header=only");
+ else if (quoteHeaders)
+ queryPart.AppendLiteral("header=quote");
+ else
+ queryPart.AppendLiteral("header=quotebody");
+ rv = NS_MutateURI(newURI).SetQuery(queryPart).Finalize(newURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we were told to auto-detect the charset, pass that on.
+ if (aAutodetectCharset) {
+ nsCOMPtr<nsIMsgI18NUrl> i18nUrl(do_QueryInterface(newURI));
+ if (i18nUrl) i18nUrl->SetAutodetectCharset(true);
+ }
+
+ mQuoteListener =
+ do_CreateInstance("@mozilla.org/messengercompose/quotinglistener;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+ mQuoteListener->SetMsgQuote(this);
+
+ // funky magic go get the isupports for this class which inherits from
+ // multiple interfaces.
+ nsISupports* supports;
+ QueryInterface(NS_GET_IID(nsISupports), (void**)&supports);
+ nsCOMPtr<nsISupports> quoteSupport = supports;
+ NS_IF_RELEASE(supports);
+
+ // now we want to create a necko channel for this url and we want to open it
+ mQuoteChannel = nullptr;
+ nsCOMPtr<nsIIOService> netService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(netService, NS_ERROR_UNEXPECTED);
+ rv = netService->NewChannelFromURI(
+ newURI, nullptr, nsContentUtils::GetSystemPrincipal(), nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER, getter_AddRefs(mQuoteChannel));
+
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIStreamConverterService> streamConverterService =
+ do_GetService("@mozilla.org/streamConverters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStreamListener> convertedListener;
+ nsCOMPtr<nsIMsgQuotingOutputStreamListener> streamListener =
+ do_QueryReferent(mStreamListener);
+ rv = streamConverterService->AsyncConvertData(
+ "message/rfc822", "application/xhtml+xml", streamListener, quoteSupport,
+ getter_AddRefs(convertedListener));
+ if (NS_FAILED(rv)) return rv;
+
+ // now try to open the channel passing in our display consumer as the
+ // listener
+ rv = mQuoteChannel->AsyncOpen(convertedListener);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgQuote::GetQuoteListener(nsIMimeStreamConverterListener** aQuoteListener) {
+ if (!aQuoteListener || !mQuoteListener) return NS_ERROR_NULL_POINTER;
+ NS_ADDREF(*aQuoteListener = mQuoteListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuote::GetQuoteChannel(nsIChannel** aQuoteChannel) {
+ if (!aQuoteChannel || !mQuoteChannel) return NS_ERROR_NULL_POINTER;
+ NS_ADDREF(*aQuoteChannel = mQuoteChannel);
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgQuote.h b/comm/mailnews/compose/src/nsMsgQuote.h
new file mode 100644
index 0000000000..27525a6dc1
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgQuote.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#ifndef __nsMsgQuote_h__
+#define __nsMsgQuote_h__
+
+#include "nsIMsgQuote.h"
+#include "nsIMsgMessageService.h"
+#include "nsIStreamListener.h"
+#include "nsIMimeStreamConverter.h"
+#include "nsIChannel.h"
+#include "nsCOMPtr.h"
+#include "nsWeakReference.h"
+
+class nsMsgQuote;
+
+class nsMsgQuoteListener : public nsIMsgQuoteListener {
+ public:
+ nsMsgQuoteListener();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIMimeStreamConverterListener support
+ NS_DECL_NSIMIMESTREAMCONVERTERLISTENER
+ NS_DECL_NSIMSGQUOTELISTENER
+
+ private:
+ virtual ~nsMsgQuoteListener();
+ nsWeakPtr mMsgQuote;
+};
+
+class nsMsgQuote : public nsIMsgQuote, public nsSupportsWeakReference {
+ public:
+ nsMsgQuote();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGQUOTE
+
+ private:
+ virtual ~nsMsgQuote();
+ //
+ // Implementation data...
+ //
+ nsWeakPtr mStreamListener;
+ bool mQuoteHeaders;
+ nsCOMPtr<nsIMsgQuoteListener> mQuoteListener;
+ nsCOMPtr<nsIChannel> mQuoteChannel;
+};
+
+#endif /* __nsMsgQuote_h__ */
diff --git a/comm/mailnews/compose/src/nsMsgSendLater.cpp b/comm/mailnews/compose/src/nsMsgSendLater.cpp
new file mode 100644
index 0000000000..8e3b8b0a5d
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgSendLater.cpp
@@ -0,0 +1,1406 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsMsgSendLater.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsMsgCopy.h"
+#include "nsIMsgSend.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgCompUtils.h"
+#include "nsMsgUtils.h"
+#include "nsMailHeaders.h"
+#include "nsMsgPrompts.h"
+#include "nsISmtpUrl.h"
+#include "nsIChannel.h"
+#include "nsNetUtil.h"
+#include "prlog.h"
+#include "prmem.h"
+#include "nsIMimeConverter.h"
+#include "nsComposeStrings.h"
+#include "nsIObserverService.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgDatabase.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgWindow.h"
+#include "nsMsgMessageFlags.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Services.h"
+
+// Consts for checking and sending mail in milliseconds
+
+// 1 second from mail into the unsent messages folder to initially trying to
+// send it.
+const uint32_t kInitialMessageSendTime = 1000;
+
+NS_IMPL_ISUPPORTS(nsMsgSendLater, nsIMsgSendLater, nsIFolderListener,
+ nsIRequestObserver, nsIStreamListener, nsIObserver,
+ nsIUrlListener, nsIMsgShutdownTask)
+
+nsMsgSendLater::nsMsgSendLater() {
+ mSendingMessages = false;
+ mTimerSet = false;
+ mTotalSentSuccessfully = 0;
+ mTotalSendCount = 0;
+ mLeftoverBuffer = nullptr;
+
+ m_to = nullptr;
+ m_bcc = nullptr;
+ m_fcc = nullptr;
+ m_newsgroups = nullptr;
+ m_newshost = nullptr;
+ m_headers = nullptr;
+ m_flags = 0;
+ m_headersFP = 0;
+ m_inhead = true;
+ m_headersPosition = 0;
+
+ m_bytesRead = 0;
+ m_position = 0;
+ m_flagsPosition = 0;
+ m_headersSize = 0;
+
+ mIdentityKey = nullptr;
+ mAccountKey = nullptr;
+
+ mUserInitiated = false;
+}
+
+nsMsgSendLater::~nsMsgSendLater() {
+ PR_Free(m_to);
+ PR_Free(m_fcc);
+ PR_Free(m_bcc);
+ PR_Free(m_newsgroups);
+ PR_Free(m_newshost);
+ PR_Free(m_headers);
+ PR_Free(mLeftoverBuffer);
+ PR_Free(mIdentityKey);
+ PR_Free(mAccountKey);
+}
+
+nsresult nsMsgSendLater::Init() {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool sendInBackground;
+ rv = prefs->GetBoolPref("mailnews.sendInBackground", &sendInBackground);
+ // If we're not sending in the background, don't do anything else
+ if (NS_FAILED(rv) || !sendInBackground) return NS_OK;
+
+ // We need to know when we're shutting down.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ rv = observerService->AddObserver(this, "xpcom-shutdown", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = observerService->AddObserver(this, "quit-application", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = observerService->AddObserver(this, "msg-shutdown", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Subscribe to the unsent messages folder
+ // XXX This code should be set up for multiple unsent folders, however we
+ // don't support that at the moment, so for now just assume one folder.
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder));
+ // There doesn't have to be a nsMsgQueueForLater flagged folder.
+ if (NS_FAILED(rv) || !folder) return NS_OK;
+
+ rv = folder->AddFolderListener(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX may want to send messages X seconds after startup if there are any.
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (aSubject == mTimer && !strcmp(aTopic, "timer-callback")) {
+ if (mTimer)
+ mTimer->Cancel();
+ else
+ NS_ERROR("mTimer was null in nsMsgSendLater::Observe");
+
+ mTimerSet = false;
+ // If we've already started a send since the timer fired, don't start
+ // another
+ if (!mSendingMessages) InternalSendMessages(false, nullptr);
+ } else if (!strcmp(aTopic, "quit-application")) {
+ // If the timer is set, cancel it - we're quitting, the shutdown service
+ // interfaces will sort out sending etc.
+ if (mTimer) mTimer->Cancel();
+
+ mTimerSet = false;
+ } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ // We're shutting down. Unsubscribe from the unsentFolder notifications
+ // they aren't any use to us now, we don't want to start sending more
+ // messages.
+ nsresult rv;
+ if (mMessageFolder) {
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ if (folder) {
+ rv = folder->RemoveFolderListener(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ folder->ForceDBClosed();
+ }
+ }
+
+ // Now remove ourselves from the observer service as well.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ rv = observerService->RemoveObserver(this, "xpcom-shutdown");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = observerService->RemoveObserver(this, "quit-application");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = observerService->RemoveObserver(this, "msg-shutdown");
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::SetStatusFeedback(nsIMsgStatusFeedback* aFeedback) {
+ mFeedback = aFeedback;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::GetStatusFeedback(nsIMsgStatusFeedback** aFeedback) {
+ NS_ENSURE_ARG_POINTER(aFeedback);
+ NS_IF_ADDREF(*aFeedback = mFeedback);
+ return NS_OK;
+}
+
+// Stream is done...drive on!
+NS_IMETHODIMP
+nsMsgSendLater::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsresult rv;
+
+ // First, this shouldn't happen, but if
+ // it does, flush the buffer and move on.
+ if (mLeftoverBuffer) {
+ DeliverQueuedLine(mLeftoverBuffer, PL_strlen(mLeftoverBuffer));
+ }
+
+ if (mOutFile) mOutFile->Close();
+
+ // See if we succeeded on reading the message from the message store?
+ //
+ if (NS_SUCCEEDED(status)) {
+ // Message is done...send it!
+ rv = CompleteMailFileSend();
+
+#ifdef NS_DEBUG
+ printf("nsMsgSendLater: Success on getting message...\n");
+#endif
+
+ // If the send operation failed..try the next one...
+ if (NS_FAILED(rv)) {
+ rv = StartNextMailFileSend(rv);
+ if (NS_FAILED(rv))
+ EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully);
+ }
+ } else {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+ if (!channel) return NS_ERROR_FAILURE;
+
+ // extract the prompt object to use for the alert from the url....
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ if (channel) {
+ channel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(uri));
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ if (msgUrl) msgUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ if (msgWindow) msgWindow->GetDomWindow(getter_AddRefs(domWindow));
+ }
+
+ nsMsgDisplayMessageByName(domWindow, "errorQueuedDeliveryFailed");
+
+ // Getting the data failed, but we will still keep trying to send the
+ // rest...
+ rv = StartNextMailFileSend(status);
+ if (NS_FAILED(rv))
+ EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully);
+ }
+
+ return rv;
+}
+
+char* FindEOL(char* inBuf, char* buf_end) {
+ char* buf = inBuf;
+ char* findLoc = nullptr;
+
+ while (buf <= buf_end)
+ if (*buf == 0)
+ return buf;
+ else if ((*buf == '\n') || (*buf == '\r')) {
+ findLoc = buf;
+ break;
+ } else
+ ++buf;
+
+ if (!findLoc)
+ return nullptr;
+ else if ((findLoc + 1) > buf_end)
+ return buf;
+
+ if ((*findLoc == '\n' && *(findLoc + 1) == '\r') ||
+ (*findLoc == '\r' && *(findLoc + 1) == '\n'))
+ findLoc++; // possibly a pair.
+ return findLoc;
+}
+
+nsresult nsMsgSendLater::RebufferLeftovers(char* startBuf, uint32_t aLen) {
+ PR_FREEIF(mLeftoverBuffer);
+ mLeftoverBuffer = (char*)PR_Malloc(aLen + 1);
+ if (!mLeftoverBuffer) return NS_ERROR_OUT_OF_MEMORY;
+
+ memcpy(mLeftoverBuffer, startBuf, aLen);
+ mLeftoverBuffer[aLen] = '\0';
+ return NS_OK;
+}
+
+nsresult nsMsgSendLater::BuildNewBuffer(const char* aBuf, uint32_t aCount,
+ uint32_t* totalBufSize) {
+ // Only build a buffer when there are leftovers...
+ if (!mLeftoverBuffer) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t leftoverSize = PL_strlen(mLeftoverBuffer);
+ char* newBuffer = (char*)PR_Realloc(mLeftoverBuffer, aCount + leftoverSize);
+ NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
+ mLeftoverBuffer = newBuffer;
+
+ memcpy(mLeftoverBuffer + leftoverSize, aBuf, aCount);
+ *totalBufSize = aCount + leftoverSize;
+ return NS_OK;
+}
+
+// Got data?
+NS_IMETHODIMP
+nsMsgSendLater::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) {
+ NS_ENSURE_ARG_POINTER(inStr);
+
+ // This is a little bit tricky since we have to chop random
+ // buffers into lines and deliver the lines...plus keeping the
+ // leftovers for next time...some fun, eh?
+ //
+ nsresult rv = NS_OK;
+ char* startBuf;
+ char* endBuf;
+ char* lineEnd;
+ char* newbuf = nullptr;
+ uint32_t size;
+
+ uint32_t aCount = count;
+ char* aBuf = (char*)PR_Malloc(aCount + 1);
+
+ inStr->Read(aBuf, count, &aCount);
+
+ // First, create a new work buffer that will
+ if (NS_FAILED(BuildNewBuffer(aBuf, aCount, &size))) // no leftovers...
+ {
+ startBuf = (char*)aBuf;
+ endBuf = (char*)(aBuf + aCount - 1);
+ } else // yum, leftovers...new buffer created...sitting in mLeftoverBuffer
+ {
+ newbuf = mLeftoverBuffer;
+ startBuf = newbuf;
+ endBuf = startBuf + size - 1;
+ mLeftoverBuffer = nullptr; // null out this
+ }
+
+ while (startBuf <= endBuf) {
+ lineEnd = FindEOL(startBuf, endBuf);
+ if (!lineEnd) {
+ rv = RebufferLeftovers(startBuf, (endBuf - startBuf) + 1);
+ break;
+ }
+
+ rv = DeliverQueuedLine(startBuf, (lineEnd - startBuf) + 1);
+ if (NS_FAILED(rv)) break;
+
+ startBuf = lineEnd + 1;
+ }
+
+ PR_Free(newbuf);
+ PR_Free(aBuf);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSendLater::OnStopRunningUrl(nsIURI* url, nsresult aExitCode) {
+ if (NS_SUCCEEDED(aExitCode)) InternalSendMessages(mUserInitiated, mIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnStartRequest(nsIRequest* request) { return NS_OK; }
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for the send operation. We have to create this
+// class to listen for message send completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+NS_IMPL_ISUPPORTS(SendOperationListener, nsIMsgSendListener,
+ nsIMsgCopyServiceListener)
+
+SendOperationListener::SendOperationListener(nsMsgSendLater* aSendLater)
+ : mSendLater(aSendLater) {}
+
+SendOperationListener::~SendOperationListener(void) {}
+
+NS_IMETHODIMP
+SendOperationListener::OnGetDraftFolderURI(const char* aMsgID,
+ const nsACString& aFolderURI) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnStartSending(const char* aMsgID, uint32_t aMsgSize) {
+#ifdef NS_DEBUG
+ printf("SendOperationListener::OnStartSending()\n");
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnProgress(const char* aMsgID, uint32_t aProgress,
+ uint32_t aProgressMax) {
+#ifdef NS_DEBUG
+ printf("SendOperationListener::OnProgress()\n");
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnStatus(const char* aMsgID, const char16_t* aMsg) {
+#ifdef NS_DEBUG
+ printf("SendOperationListener::OnStatus()\n");
+#endif
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnSendNotPerformed(const char* aMsgID,
+ nsresult aStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnStopSending(const char* aMsgID, nsresult aStatus,
+ const char16_t* aMsg,
+ nsIFile* returnFile) {
+ if (mSendLater && !mSendLater->OnSendStepFinished(aStatus))
+ mSendLater = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnTransportSecurityError(
+ const char* msgID, nsresult status, nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ return NS_OK;
+}
+
+// nsIMsgCopyServiceListener
+
+NS_IMETHODIMP
+SendOperationListener::OnStartCopy(void) { return NS_OK; }
+
+NS_IMETHODIMP
+SendOperationListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendOperationListener::SetMessageKey(nsMsgKey aKey) {
+ MOZ_ASSERT_UNREACHABLE("SendOperationListener::SetMessageKey()");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+SendOperationListener::GetMessageId(nsACString& messageId) {
+ MOZ_ASSERT_UNREACHABLE("SendOperationListener::GetMessageId()");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+SendOperationListener::OnStopCopy(nsresult aStatus) {
+ if (mSendLater) {
+ mSendLater->OnCopyStepFinished(aStatus);
+ mSendLater = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgSendLater::CompleteMailFileSend() {
+ // get the identity from the key
+ // if no key, or we fail to find the identity
+ // use the default identity on the default account
+ nsCOMPtr<nsIMsgIdentity> identity;
+ nsresult rv = GetIdentityFromKey(mIdentityKey, getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!identity) return NS_ERROR_UNEXPECTED;
+
+ // If for some reason the tmp file didn't get created, we've failed here
+ bool created;
+ mTempFile->Exists(&created);
+ if (!created) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgCompFields> compFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgSend> pMsgSend =
+ do_CreateInstance("@mozilla.org/messengercompose/send;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Since we have already parsed all of the headers, we are simply going to
+ // set the composition fields and move on.
+ nsCString author;
+ mMessage->GetAuthor(getter_Copies(author));
+
+ nsMsgCompFields* fields = (nsMsgCompFields*)compFields.get();
+
+ fields->SetFrom(author.get());
+
+ if (m_to) {
+ fields->SetTo(m_to);
+ }
+
+ if (m_bcc) {
+ fields->SetBcc(m_bcc);
+ }
+
+ if (m_fcc) {
+ fields->SetFcc(m_fcc);
+ }
+
+ if (m_newsgroups) fields->SetNewsgroups(m_newsgroups);
+
+#if 0
+ // needs cleanup. Is this needed?
+ if (m_newshost)
+ fields->SetNewspostUrl(m_newshost);
+#endif
+
+ // Create the listener for the send operation...
+ RefPtr<SendOperationListener> sendListener = new SendOperationListener(this);
+
+ RefPtr<mozilla::dom::Promise> promise;
+ rv = pMsgSend->SendMessageFile(
+ identity, mAccountKey,
+ compFields, // nsIMsgCompFields *fields,
+ mTempFile, // nsIFile *sendFile,
+ true, // bool deleteSendFileOnCompletion,
+ false, // bool digest_p,
+ nsIMsgSend::nsMsgSendUnsent, // nsMsgDeliverMode mode,
+ nullptr, // nsIMsgDBHdr *msgToReplace,
+ sendListener, mFeedback, nullptr, getter_AddRefs(promise));
+ return rv;
+}
+
+nsresult nsMsgSendLater::StartNextMailFileSend(nsresult prevStatus) {
+ if (mTotalSendCount >= (uint32_t)mMessagesToSend.Count()) {
+ // Notify that this message has finished being sent.
+ NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100,
+ 100);
+
+ // EndSendMessages resets everything for us
+ EndSendMessages(prevStatus, nullptr, mTotalSendCount,
+ mTotalSentSuccessfully);
+
+ // XXX Should we be releasing references so that we don't hold onto items
+ // unnecessarily.
+ return NS_OK;
+ }
+
+ // If we've already sent a message, and are sending more, send out a progress
+ // update with 100% for both send and copy as we must have finished by now.
+ if (mTotalSendCount > 0) {
+ NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100,
+ 100);
+ }
+
+ mMessage = mMessagesToSend[mTotalSendCount++];
+
+ if (!mMessageFolder) return NS_ERROR_UNEXPECTED;
+
+ nsCString messageURI;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ folder->GetUriForMsg(mMessage, messageURI);
+
+ rv = nsMsgCreateTempFile("nsqmail.tmp", getter_AddRefs(mTempFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ rv = GetMessageServiceFromURI(messageURI, getter_AddRefs(messageService));
+ if (NS_FAILED(rv) && !messageService) return NS_ERROR_FACTORY_NOT_LOADED;
+
+ nsCString identityKey;
+ rv = mMessage->GetStringProperty(HEADER_X_MOZILLA_IDENTITY_KEY, identityKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = GetIdentityFromKey(identityKey.get(), getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!identity) return NS_ERROR_UNEXPECTED;
+
+ // Notify that we're just about to start sending this message
+ NotifyListenersOnMessageStartSending(mTotalSendCount, mMessagesToSend.Count(),
+ identity);
+
+ // Setup what we need to parse the data stream correctly
+ m_inhead = true;
+ m_headersFP = 0;
+ m_headersPosition = 0;
+ m_bytesRead = 0;
+ m_position = 0;
+ m_flagsPosition = 0;
+ m_headersSize = 0;
+ PR_FREEIF(mLeftoverBuffer);
+
+ // Now, get our stream listener interface and plug it into the LoadMessage
+ // operation
+ rv = messageService->LoadMessage(messageURI,
+ static_cast<nsIStreamListener*>(this),
+ nullptr, nullptr, false);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::GetUnsentMessagesFolder(nsIMsgIdentity* aIdentity,
+ nsIMsgFolder** aFolder) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder);
+ if (!folder) {
+ nsCString uri;
+ GetFolderURIFromUserPrefs(nsIMsgSend::nsMsgQueueForLater, aIdentity, uri);
+ rv = LocateMessageFolder(aIdentity, nsIMsgSend::nsMsgQueueForLater,
+ uri.get(), getter_AddRefs(folder));
+ mMessageFolder = do_GetWeakReference(folder);
+ if (!mMessageFolder) return NS_ERROR_FAILURE;
+ }
+ if (folder) folder.forget(aFolder);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::HasUnsentMessages(nsIMsgIdentity* aIdentity, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgAccount>> accounts;
+ rv = accountManager->GetAccounts(accounts);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (accounts.IsEmpty())
+ return NS_OK; // no account set up -> no unsent messages
+
+ // XXX This code should be set up for multiple unsent folders, however we
+ // don't support that at the moment, so for now just assume one folder.
+ if (!mMessageFolder) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder));
+ // There doesn't have to be a nsMsgQueueForLater flagged folder.
+ if (NS_FAILED(rv) || !folder) return NS_OK;
+ }
+ rv = ReparseDBIfNeeded(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t totalMessages;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetTotalMessages(false, &totalMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = totalMessages > 0;
+ return NS_OK;
+}
+
+//
+// To really finalize this capability, we need to have the ability to get
+// the message from the mail store in a stream for processing. The flow
+// would be something like this:
+//
+// foreach (message in Outbox folder)
+// get stream of Nth message
+// if (not done with headers)
+// Tack on to current buffer of headers
+// when done with headers
+// BuildHeaders()
+// Write Headers to Temp File
+// after done with headers
+// write rest of message body to temp file
+//
+// when done with the message
+// do send operation
+//
+// when send is complete
+// Copy from Outbox to FCC folder
+// Delete from Outbox folder
+//
+//
+NS_IMETHODIMP
+nsMsgSendLater::SendUnsentMessages(nsIMsgIdentity* aIdentity) {
+ return InternalSendMessages(true, aIdentity);
+}
+
+// Returns NS_OK if the db is OK, an error otherwise, e.g., we had to reparse.
+nsresult nsMsgSendLater::ReparseDBIfNeeded(nsIUrlListener* aListener) {
+ // This will kick off a reparse, if needed. So the next time we check if
+ // there are unsent messages, the db will be up to date.
+ nsCOMPtr<nsIMsgDatabase> unsentDB;
+ nsresult rv;
+ nsCOMPtr<nsIMsgLocalMailFolder> locFolder(
+ do_QueryReferent(mMessageFolder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return locFolder->GetDatabaseWithReparse(aListener, nullptr,
+ getter_AddRefs(unsentDB));
+}
+
+nsresult nsMsgSendLater::InternalSendMessages(bool aUserInitiated,
+ nsIMsgIdentity* aIdentity) {
+ if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE;
+
+ // Protect against being called whilst we're already sending.
+ if (mSendingMessages) {
+ NS_ERROR("nsMsgSendLater is already sending messages");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+ // XXX This code should be set up for multiple unsent folders, however we
+ // don't support that at the moment, so for now just assume one folder.
+ if (!mMessageFolder) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder));
+ if (NS_FAILED(rv) || !folder) return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIMsgDatabase> unsentDB;
+ // Remember these in case we need to reparse the db.
+ mUserInitiated = aUserInitiated;
+ mIdentity = aIdentity;
+ rv = ReparseDBIfNeeded(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mIdentity = nullptr; // don't hold onto the identity since we're a service.
+
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = folder->GetMessages(getter_AddRefs(enumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build mMessagesToSend array.
+ bool hasMoreElements = false;
+ while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreElements)) &&
+ hasMoreElements) {
+ nsCOMPtr<nsIMsgDBHdr> messageHeader;
+ rv = enumerator->GetNext(getter_AddRefs(messageHeader));
+ if (NS_SUCCEEDED(rv)) {
+ if (aUserInitiated) {
+ // If the user initiated the send, add all messages
+ mMessagesToSend.AppendObject(messageHeader);
+ } else {
+ // Else just send those that are NOT marked as Queued.
+ uint32_t flags;
+ rv = messageHeader->GetFlags(&flags);
+ if (NS_SUCCEEDED(rv) && !(flags & nsMsgMessageFlags::Queued))
+ mMessagesToSend.AppendObject(messageHeader);
+ }
+ }
+ }
+
+ // We're now sending messages so its time to signal that and reset our counts.
+ mSendingMessages = true;
+ mTotalSentSuccessfully = 0;
+ mTotalSendCount = 0;
+
+ // Notify the listeners that we are starting a send.
+ NotifyListenersOnStartSending(mMessagesToSend.Count());
+
+ return StartNextMailFileSend(NS_OK);
+}
+
+nsresult nsMsgSendLater::SetOrigMsgDisposition() {
+ if (!mMessage) return NS_ERROR_NULL_POINTER;
+
+ // We're finished sending a queued message. We need to look at mMessage
+ // and see if we need to set replied/forwarded
+ // flags for the original message that this message might be a reply to
+ // or forward of.
+ nsCString originalMsgURIs;
+ nsCString queuedDisposition;
+ mMessage->GetStringProperty(ORIG_URI_PROPERTY, originalMsgURIs);
+ mMessage->GetStringProperty(QUEUED_DISPOSITION_PROPERTY, queuedDisposition);
+ if (!queuedDisposition.IsEmpty()) {
+ nsTArray<nsCString> uriArray;
+ ParseString(originalMsgURIs, ',', uriArray);
+ for (uint32_t i = 0; i < uriArray.Length(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgDBHdrFromURI(uriArray[i], getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) {
+ // get the folder for the message resource
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ msgHdr->GetFolder(getter_AddRefs(msgFolder));
+ if (msgFolder) {
+ nsMsgDispositionState dispositionSetting =
+ nsIMsgFolder::nsMsgDispositionState_None;
+ if (queuedDisposition.EqualsLiteral("replied"))
+ dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Replied;
+ else if (queuedDisposition.EqualsLiteral("forwarded"))
+ dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Forwarded;
+ else if (queuedDisposition.EqualsLiteral("redirected"))
+ dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Redirected;
+
+ msgFolder->AddMessageDispositionState(msgHdr, dispositionSetting);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSendLater::DeleteCurrentMessage() {
+ if (!mMessage) {
+ NS_ERROR("nsMsgSendLater: Attempt to delete an already deleted message");
+ return NS_OK;
+ }
+
+ // Get the composition fields interface
+ if (!mMessageFolder) return NS_ERROR_UNEXPECTED;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->DeleteMessages({&*mMessage}, nullptr, true, false, nullptr,
+ false /*allowUndo*/);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ // Null out the message so we don't try and delete it again.
+ mMessage = nullptr;
+
+ return NS_OK;
+}
+
+//
+// This function parses the headers, and also deletes from the header block
+// any headers which should not be delivered in mail, regardless of whether
+// they were present in the queue file. Such headers include: BCC, FCC,
+// Sender, X-Mozilla-Status, X-Mozilla-News-Host, and Content-Length.
+// (Content-Length is for the disk file only, and must not be allowed to
+// escape onto the network, since it depends on the local linebreak
+// representation. Arguably, we could allow Lines to escape, but it's not
+// required by NNTP.)
+//
+nsresult nsMsgSendLater::BuildHeaders() {
+ char* buf = m_headers;
+ char* buf_end = buf + m_headersFP;
+
+ PR_FREEIF(m_to);
+ PR_FREEIF(m_bcc);
+ PR_FREEIF(m_newsgroups);
+ PR_FREEIF(m_newshost);
+ PR_FREEIF(m_fcc);
+ PR_FREEIF(mIdentityKey);
+ PR_FREEIF(mAccountKey);
+ m_flags = 0;
+
+ while (buf < buf_end) {
+ bool prune_p = false;
+ bool do_flags_p = false;
+ char* colon = PL_strchr(buf, ':');
+ char* end;
+ char* value = 0;
+ char** header = 0;
+ char* header_start = buf;
+
+ if (!colon) break;
+
+ end = colon;
+ while (end > buf && (*end == ' ' || *end == '\t')) end--;
+
+ switch (buf[0]) {
+ case 'B':
+ case 'b':
+ if (!PL_strncasecmp("BCC", buf, end - buf)) {
+ header = &m_bcc;
+ }
+ break;
+ case 'C':
+ case 'c':
+ if (!PL_strncasecmp("CC", buf, end - buf))
+ header = &m_to;
+ else if (!PL_strncasecmp(HEADER_CONTENT_LENGTH, buf, end - buf))
+ prune_p = true;
+ break;
+ case 'F':
+ case 'f':
+ if (!PL_strncasecmp("FCC", buf, end - buf)) {
+ header = &m_fcc;
+ prune_p = true;
+ }
+ break;
+ case 'L':
+ case 'l':
+ if (!PL_strncasecmp("Lines", buf, end - buf)) prune_p = true;
+ break;
+ case 'N':
+ case 'n':
+ if (!PL_strncasecmp("Newsgroups", buf, end - buf))
+ header = &m_newsgroups;
+ break;
+ case 'S':
+ case 's':
+ if (!PL_strncasecmp("Sender", buf, end - buf)) prune_p = true;
+ break;
+ case 'T':
+ case 't':
+ if (!PL_strncasecmp("To", buf, end - buf)) header = &m_to;
+ break;
+ case 'X':
+ case 'x': {
+ if (buf + strlen(HEADER_X_MOZILLA_STATUS2) == end &&
+ !PL_strncasecmp(HEADER_X_MOZILLA_STATUS2, buf, end - buf))
+ prune_p = true;
+ else if (buf + strlen(HEADER_X_MOZILLA_STATUS) == end &&
+ !PL_strncasecmp(HEADER_X_MOZILLA_STATUS, buf, end - buf))
+ prune_p = do_flags_p = true;
+ else if (!PL_strncasecmp(HEADER_X_MOZILLA_DRAFT_INFO, buf, end - buf))
+ prune_p = true;
+ else if (!PL_strncasecmp(HEADER_X_MOZILLA_KEYWORDS, buf, end - buf))
+ prune_p = true;
+ else if (!PL_strncasecmp(HEADER_X_MOZILLA_NEWSHOST, buf, end - buf)) {
+ prune_p = true;
+ header = &m_newshost;
+ } else if (!PL_strncasecmp(HEADER_X_MOZILLA_IDENTITY_KEY, buf,
+ end - buf)) {
+ prune_p = true;
+ header = &mIdentityKey;
+ } else if (!PL_strncasecmp(HEADER_X_MOZILLA_ACCOUNT_KEY, buf,
+ end - buf)) {
+ prune_p = true;
+ header = &mAccountKey;
+ }
+ break;
+ }
+ }
+
+ buf = colon + 1;
+ while (*buf == ' ' || *buf == '\t') buf++;
+
+ value = buf;
+
+ SEARCH_NEWLINE:
+ while (*buf != 0 && *buf != '\r' && *buf != '\n') buf++;
+
+ if (buf + 1 >= buf_end)
+ ;
+ // If "\r\n " or "\r\n\t" is next, that doesn't terminate the header.
+ else if (buf + 2 < buf_end && (buf[0] == '\r' && buf[1] == '\n') &&
+ (buf[2] == ' ' || buf[2] == '\t')) {
+ buf += 3;
+ goto SEARCH_NEWLINE;
+ }
+ // If "\r " or "\r\t" or "\n " or "\n\t" is next, that doesn't terminate
+ // the header either.
+ else if ((buf[0] == '\r' || buf[0] == '\n') &&
+ (buf[1] == ' ' || buf[1] == '\t')) {
+ buf += 2;
+ goto SEARCH_NEWLINE;
+ }
+
+ if (header) {
+ int L = buf - value;
+ if (*header) {
+ char* newh = (char*)PR_Realloc((*header), PL_strlen(*header) + L + 10);
+ if (!newh) return NS_ERROR_OUT_OF_MEMORY;
+ *header = newh;
+ newh = (*header) + PL_strlen(*header);
+ *newh++ = ',';
+ *newh++ = ' ';
+ memcpy(newh, value, L);
+ newh[L] = 0;
+ } else {
+ *header = (char*)PR_Malloc(L + 1);
+ if (!*header) return NS_ERROR_OUT_OF_MEMORY;
+ memcpy((*header), value, L);
+ (*header)[L] = 0;
+ }
+ } else if (do_flags_p) {
+ char* s = value;
+ PR_ASSERT(*s != ' ' && *s != '\t');
+ NS_ASSERTION(MsgIsHex(s, 4), "Expected 4 hex digits for flags.");
+ m_flags = MsgUnhex(s, 4);
+ }
+
+ if (*buf == '\r' || *buf == '\n') {
+ if (*buf == '\r' && buf[1] == '\n') buf++;
+ buf++;
+ }
+
+ if (prune_p) {
+ char* to = header_start;
+ char* from = buf;
+ while (from < buf_end) *to++ = *from++;
+ buf = header_start;
+ buf_end = to;
+ m_headersFP = buf_end - m_headers;
+ }
+ }
+
+ m_headers[m_headersFP++] = '\r';
+ m_headers[m_headersFP++] = '\n';
+
+ // Now we have parsed out all of the headers we need and we
+ // can proceed.
+ return NS_OK;
+}
+
+nsresult DoGrowBuffer(int32_t desired_size, int32_t element_size,
+ int32_t quantum, char** buffer, int32_t* size) {
+ if (*size <= desired_size) {
+ char* new_buf;
+ int32_t increment = desired_size - *size;
+ if (increment < quantum) // always grow by a minimum of N bytes
+ increment = quantum;
+
+ new_buf =
+ (*buffer ? (char*)PR_Realloc(*buffer, (*size + increment) *
+ (element_size / sizeof(char)))
+ : (char*)PR_Malloc((*size + increment) *
+ (element_size / sizeof(char))));
+ if (!new_buf) return NS_ERROR_OUT_OF_MEMORY;
+ *buffer = new_buf;
+ *size += increment;
+ }
+ return NS_OK;
+}
+
+#define do_grow_headers(desired_size) \
+ (((desired_size) >= m_headersSize) \
+ ? DoGrowBuffer((desired_size), sizeof(char), 1024, &m_headers, \
+ &m_headersSize) \
+ : NS_OK)
+
+nsresult nsMsgSendLater::DeliverQueuedLine(const char* line, int32_t length) {
+ int32_t flength = length;
+
+ m_bytesRead += length;
+
+ // convert existing newline to CRLF
+ // Don't need this because the calling routine is taking care of it.
+ // if (length > 0 && (line[length-1] == '\r' ||
+ // (line[length-1] == '\n' && (length < 2 || line[length-2] != '\r'))))
+ // {
+ // line[length-1] = '\r';
+ // line[length++] = '\n';
+ // }
+ //
+ //
+ // We are going to check if we are looking at a "From - " line. If so,
+ // then just eat it and return NS_OK
+ //
+ if (!PL_strncasecmp(line, "From - ", 7)) return NS_OK;
+
+ if (m_inhead) {
+ if (m_headersPosition == 0) {
+ // This line is the first line in a header block.
+ // Remember its position.
+ m_headersPosition = m_position;
+
+ // Also, since we're now processing the headers, clear out the
+ // slots which we will parse data into, so that the values that
+ // were used the last time around do not persist.
+
+ // We must do that here, and not in the previous clause of this
+ // `else' (the "I've just seen a `From ' line clause") because
+ // that clause happens before delivery of the previous message is
+ // complete, whereas this clause happens after the previous msg
+ // has been delivered. If we did this up there, then only the
+ // last message in the folder would ever be able to be both
+ // mailed and posted (or fcc'ed.)
+ PR_FREEIF(m_to);
+ PR_FREEIF(m_bcc);
+ PR_FREEIF(m_newsgroups);
+ PR_FREEIF(m_newshost);
+ PR_FREEIF(m_fcc);
+ PR_FREEIF(mIdentityKey);
+ }
+
+ if (line[0] == '\r' || line[0] == '\n' || line[0] == 0) {
+ // End of headers. Now parse them; open the temp file;
+ // and write the appropriate subset of the headers out.
+ m_inhead = false;
+
+ nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mOutFile),
+ mTempFile, -1, 00600);
+ if (NS_FAILED(rv)) return NS_MSG_ERROR_WRITING_FILE;
+
+ nsresult status = BuildHeaders();
+ if (NS_FAILED(status)) return status;
+
+ uint32_t n;
+ rv = mOutFile->Write(m_headers, m_headersFP, &n);
+ if (NS_FAILED(rv) || n != (uint32_t)m_headersFP)
+ return NS_MSG_ERROR_WRITING_FILE;
+ } else {
+ // Otherwise, this line belongs to a header. So append it to the
+ // header data.
+
+ if (!PL_strncasecmp(line, HEADER_X_MOZILLA_STATUS,
+ PL_strlen(HEADER_X_MOZILLA_STATUS)))
+ // Notice the position of the flags.
+ m_flagsPosition = m_position;
+ else if (m_headersFP == 0)
+ m_flagsPosition = 0;
+
+ nsresult status = do_grow_headers(length + m_headersFP + 10);
+ if (NS_FAILED(status)) return status;
+
+ memcpy(m_headers + m_headersFP, line, length);
+ m_headersFP += length;
+ }
+ } else {
+ // This is a body line. Write it to the file.
+ PR_ASSERT(mOutFile);
+ if (mOutFile) {
+ uint32_t wrote;
+ nsresult rv = mOutFile->Write(line, length, &wrote);
+ if (NS_FAILED(rv) || wrote < (uint32_t)length)
+ return NS_MSG_ERROR_WRITING_FILE;
+ }
+ }
+
+ m_position += flength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::AddListener(nsIMsgSendLaterListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ mListenerArray.AppendElement(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::RemoveListener(nsIMsgSendLaterListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ return mListenerArray.RemoveElement(aListener) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::GetSendingMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mSendingMessages;
+ return NS_OK;
+}
+
+#define NOTIFY_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIMsgSendLaterListener>>::ForwardIterator iter( \
+ mListenerArray); \
+ nsCOMPtr<nsIMsgSendLaterListener> listener; \
+ while (iter.HasMore()) { \
+ listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+void nsMsgSendLater::NotifyListenersOnStartSending(
+ uint32_t aTotalMessageCount) {
+ NOTIFY_LISTENERS(OnStartSending, (aTotalMessageCount));
+}
+
+void nsMsgSendLater::NotifyListenersOnMessageStartSending(
+ uint32_t aCurrentMessage, uint32_t aTotalMessage,
+ nsIMsgIdentity* aIdentity) {
+ NOTIFY_LISTENERS(OnMessageStartSending,
+ (aCurrentMessage, aTotalMessage, mMessage, aIdentity));
+}
+
+void nsMsgSendLater::NotifyListenersOnProgress(uint32_t aCurrentMessage,
+ uint32_t aTotalMessage,
+ uint32_t aSendPercent,
+ uint32_t aCopyPercent) {
+ NOTIFY_LISTENERS(OnMessageSendProgress, (aCurrentMessage, aTotalMessage,
+ aSendPercent, aCopyPercent));
+}
+
+void nsMsgSendLater::NotifyListenersOnMessageSendError(uint32_t aCurrentMessage,
+ nsresult aStatus,
+ const char16_t* aMsg) {
+ NOTIFY_LISTENERS(OnMessageSendError,
+ (aCurrentMessage, mMessage, aStatus, aMsg));
+}
+
+/**
+ * This function is called to end sending of messages, it resets the send later
+ * system and notifies the relevant parties that we have finished.
+ */
+void nsMsgSendLater::EndSendMessages(nsresult aStatus, const char16_t* aMsg,
+ uint32_t aTotalTried,
+ uint32_t aSuccessful) {
+ // Catch-all, we may have had an issue sending, so we may not be calling
+ // StartNextMailFileSend to fully finish the sending. Therefore set
+ // mSendingMessages to false here so that we don't think we're still trying
+ // to send messages
+ mSendingMessages = false;
+
+ // Clear out our array of messages.
+ mMessagesToSend.Clear();
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, );
+ // We don't need to keep hold of the database now we've finished sending.
+ (void)folder->SetMsgDatabase(nullptr);
+
+ // or temp file or output stream
+ mTempFile = nullptr;
+ mOutFile = nullptr;
+
+ NOTIFY_LISTENERS(OnStopSending, (aStatus, aMsg, aTotalTried, aSuccessful));
+
+ // If we've got a shutdown listener, notify it that we've finished.
+ if (mShutdownListener) {
+ mShutdownListener->OnStopRunningUrl(nullptr, NS_OK);
+ mShutdownListener = nullptr;
+ }
+}
+
+/**
+ * Called when the send part of sending a message is finished. This will set up
+ * for the next step or "end" depending on the status.
+ *
+ * @param aStatus The success or fail result of the send step.
+ * @return True if the copy process will continue, false otherwise.
+ */
+bool nsMsgSendLater::OnSendStepFinished(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ SetOrigMsgDisposition();
+ DeleteCurrentMessage();
+
+ // Send finished, so that is now 100%, copy to proceed...
+ NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100, 0);
+
+ ++mTotalSentSuccessfully;
+ return true;
+ } else {
+ // XXX we don't currently get a message string from the send service.
+ NotifyListenersOnMessageSendError(mTotalSendCount, aStatus, nullptr);
+ nsresult rv = StartNextMailFileSend(aStatus);
+ // if this is the last message we're sending, we should report
+ // the status failure.
+ if (NS_FAILED(rv))
+ EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully);
+ }
+ return false;
+}
+
+/**
+ * Called when the copy part of sending a message is finished. This will send
+ * the next message or handle failure as appropriate.
+ *
+ * @param aStatus The success or fail result of the copy step.
+ */
+void nsMsgSendLater::OnCopyStepFinished(nsresult aStatus) {
+ // Regardless of the success of the copy we will still keep trying
+ // to send the rest...
+ nsresult rv = StartNextMailFileSend(aStatus);
+ if (NS_FAILED(rv))
+ EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully);
+}
+
+// XXX todo
+// maybe this should just live in the account manager?
+nsresult nsMsgSendLater::GetIdentityFromKey(const char* aKey,
+ nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aKey) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ if (NS_SUCCEEDED(accountManager->GetAllIdentities(identities))) {
+ for (auto lookupIdentity : identities) {
+ nsCString key;
+ lookupIdentity->GetKey(key);
+ if (key.Equals(aKey)) {
+ lookupIdentity.forget(aIdentity);
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ // If no aKey, or we failed to find the identity from the key
+ // use the identity from the default account.
+ nsCOMPtr<nsIMsgAccount> defaultAccount;
+ rv = accountManager->GetDefaultAccount(getter_AddRefs(defaultAccount));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (defaultAccount)
+ rv = defaultAccount->GetDefaultIdentity(aIdentity);
+ else
+ *aIdentity = nullptr;
+
+ return rv;
+}
+
+nsresult nsMsgSendLater::StartTimer() {
+ // No need to trigger if timer is already set
+ if (mTimerSet) return NS_OK;
+
+ // XXX only trigger for non-queued headers
+
+ // Items from this function return NS_OK because the callee won't care about
+ // the result anyway.
+ nsresult rv;
+ if (!mTimer) {
+ mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ }
+
+ rv = mTimer->Init(static_cast<nsIObserver*>(this), kInitialMessageSendTime,
+ nsITimer::TYPE_ONE_SHOT);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ mTimerSet = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderAdded(nsIMsgFolder* /*parent*/,
+ nsIMsgFolder* /*child*/) {
+ return StartTimer();
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnMessageAdded(nsIMsgFolder* /*parent*/, nsIMsgDBHdr* /*msg*/) {
+ return StartTimer();
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderRemoved(nsIMsgFolder* /*parent*/,
+ nsIMsgFolder* /*child*/) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnMessageRemoved(nsIMsgFolder* /*parent*/,
+ nsIMsgDBHdr* /*msg*/) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ const nsACString& aOldValue,
+ const nsACString& aNewValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderIntPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ int64_t aOldValue,
+ int64_t aNewValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderBoolPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ bool aOldValue, bool aNewValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderUnicharPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderPropertyFlagChanged(nsIMsgDBHdr* aMsg,
+ const nsACString& aProperty,
+ uint32_t aOldValue,
+ uint32_t aNewValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::OnFolderEvent(nsIMsgFolder* aFolder, const nsACString& aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::GetNeedsToRunTask(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mSendingMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::DoShutdownTask(nsIUrlListener* aListener, nsIMsgWindow* aWindow,
+ bool* aResult) {
+ if (mTimer) mTimer->Cancel();
+ // If we're already sending messages, nothing to do, but save the shutdown
+ // listener until we've finished.
+ if (mSendingMessages) {
+ mShutdownListener = aListener;
+ return NS_OK;
+ }
+ // Else we have pending messages, we need to throw up a dialog to find out
+ // if to send them or not.
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgSendLater::GetCurrentTaskName(nsAString& aResult) {
+ // XXX Bug 440794 will localize this, left as non-localized whilst we decide
+ // on the actual strings and try out the UI.
+ aResult = u"Sending Messages"_ns;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgSendLater.h b/comm/mailnews/compose/src/nsMsgSendLater.h
new file mode 100644
index 0000000000..fcd8c53e38
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgSendLater.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSendLater_H_
+#define _nsMsgSendLater_H_
+
+#include "nsCOMArray.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgSendListener.h"
+#include "nsIMsgSendLaterListener.h"
+#include "nsIMsgSendLater.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsTObserverArray.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgShutdown.h"
+#include "nsIWeakReferenceUtils.h"
+
+////////////////////////////////////////////////////////////////////////////////////
+// This is the listener class for the send operation. We have to create this
+// class to listen for message send completion and eventually notify the caller
+////////////////////////////////////////////////////////////////////////////////////
+class nsMsgSendLater;
+
+class SendOperationListener : public nsIMsgSendListener,
+ public nsIMsgCopyServiceListener {
+ public:
+ explicit SendOperationListener(nsMsgSendLater* aSendLater);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSENDLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ private:
+ virtual ~SendOperationListener();
+ RefPtr<nsMsgSendLater> mSendLater;
+};
+
+class nsMsgSendLater : public nsIMsgSendLater,
+ public nsIFolderListener,
+ public nsIObserver,
+ public nsIUrlListener,
+ public nsIMsgShutdownTask
+
+{
+ public:
+ nsMsgSendLater();
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSENDLATER
+ NS_DECL_NSIFOLDERLISTENER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSHUTDOWNTASK
+
+ // Methods needed for implementing interface...
+ nsresult StartNextMailFileSend(nsresult prevStatus);
+ nsresult CompleteMailFileSend();
+
+ nsresult DeleteCurrentMessage();
+ nsresult SetOrigMsgDisposition();
+ // Necessary for creating a valid list of recipients
+ nsresult BuildHeaders();
+ nsresult DeliverQueuedLine(const char* line, int32_t length);
+ nsresult RebufferLeftovers(char* startBuf, uint32_t aLen);
+ nsresult BuildNewBuffer(const char* aBuf, uint32_t aCount,
+ uint32_t* totalBufSize);
+
+ // methods for listener array processing...
+ void NotifyListenersOnStartSending(uint32_t aTotalMessageCount);
+ void NotifyListenersOnMessageStartSending(uint32_t aCurrentMessage,
+ uint32_t aTotalMessage,
+ nsIMsgIdentity* aIdentity);
+ void NotifyListenersOnProgress(uint32_t aCurrentMessage,
+ uint32_t aTotalMessage, uint32_t aSendPercent,
+ uint32_t aCopyPercent);
+ void NotifyListenersOnMessageSendError(uint32_t aCurrentMessage,
+ nsresult aStatus,
+ const char16_t* aMsg);
+ void EndSendMessages(nsresult aStatus, const char16_t* aMsg,
+ uint32_t aTotalTried, uint32_t aSuccessful);
+
+ bool OnSendStepFinished(nsresult aStatus);
+ void OnCopyStepFinished(nsresult aStatus);
+
+ private:
+ // counters and things for enumeration
+ uint32_t mTotalSentSuccessfully;
+ uint32_t mTotalSendCount;
+ nsCOMArray<nsIMsgDBHdr> mMessagesToSend;
+ nsWeakPtr mMessageFolder;
+ nsCOMPtr<nsIMsgStatusFeedback> mFeedback;
+
+ virtual ~nsMsgSendLater();
+ nsresult GetIdentityFromKey(const char* aKey, nsIMsgIdentity** aIdentity);
+ nsresult ReparseDBIfNeeded(nsIUrlListener* aListener);
+ nsresult InternalSendMessages(bool aUserInitiated, nsIMsgIdentity* aIdentity);
+ nsresult StartTimer();
+
+ nsTObserverArray<nsCOMPtr<nsIMsgSendLaterListener> > mListenerArray;
+ nsCOMPtr<nsIMsgDBHdr> mMessage;
+ nsCOMPtr<nsITimer> mTimer;
+ bool mTimerSet;
+ nsCOMPtr<nsIUrlListener> mShutdownListener;
+
+ //
+ // File output stuff...
+ //
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIOutputStream> mOutFile;
+
+ // For building headers and stream parsing...
+ char* m_to;
+ char* m_bcc;
+ char* m_fcc;
+ char* m_newsgroups;
+ char* m_newshost;
+ char* m_headers;
+ int32_t m_flags;
+ int32_t m_headersFP;
+ bool m_inhead;
+ int32_t m_headersPosition;
+ int32_t m_bytesRead;
+ int32_t m_position;
+ int32_t m_flagsPosition;
+ int32_t m_headersSize;
+ char* mLeftoverBuffer;
+ char* mIdentityKey;
+ char* mAccountKey;
+
+ bool mSendingMessages;
+ bool mUserInitiated;
+ nsCOMPtr<nsIMsgIdentity> mIdentity;
+};
+
+#endif /* _nsMsgSendLater_H_ */
diff --git a/comm/mailnews/compose/src/nsMsgSendReport.cpp b/comm/mailnews/compose/src/nsMsgSendReport.cpp
new file mode 100644
index 0000000000..5cab724810
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgSendReport.cpp
@@ -0,0 +1,385 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgSendReport.h"
+
+#include "msgCore.h"
+#include "nsIMsgCompose.h"
+#include "nsMsgPrompts.h"
+#include "nsError.h"
+#include "nsComposeStrings.h"
+#include "nsIStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+
+NS_IMPL_ISUPPORTS(nsMsgProcessReport, nsIMsgProcessReport)
+
+nsMsgProcessReport::nsMsgProcessReport() { Reset(); }
+
+nsMsgProcessReport::~nsMsgProcessReport() {}
+
+/* attribute boolean proceeded; */
+NS_IMETHODIMP nsMsgProcessReport::GetProceeded(bool* aProceeded) {
+ NS_ENSURE_ARG_POINTER(aProceeded);
+ *aProceeded = mProceeded;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgProcessReport::SetProceeded(bool aProceeded) {
+ mProceeded = aProceeded;
+ return NS_OK;
+}
+
+/* attribute nsresult error; */
+NS_IMETHODIMP nsMsgProcessReport::GetError(nsresult* aError) {
+ NS_ENSURE_ARG_POINTER(aError);
+ *aError = mError;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgProcessReport::SetError(nsresult aError) {
+ mError = aError;
+ return NS_OK;
+}
+
+/* attribute wstring message; */
+NS_IMETHODIMP nsMsgProcessReport::GetMessage(char16_t** aMessage) {
+ NS_ENSURE_ARG_POINTER(aMessage);
+ *aMessage = ToNewUnicode(mMessage);
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgProcessReport::SetMessage(const char16_t* aMessage) {
+ mMessage = aMessage;
+ return NS_OK;
+}
+
+/* void Reset (); */
+NS_IMETHODIMP nsMsgProcessReport::Reset() {
+ mProceeded = false;
+ mError = NS_OK;
+ mMessage.Truncate();
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgSendReport, nsIMsgSendReport)
+
+nsMsgSendReport::nsMsgSendReport() {
+ uint32_t i;
+ for (i = 0; i <= SEND_LAST_PROCESS; i++)
+ mProcessReport[i] = new nsMsgProcessReport();
+
+ Reset();
+}
+
+nsMsgSendReport::~nsMsgSendReport() {
+ uint32_t i;
+ for (i = 0; i <= SEND_LAST_PROCESS; i++) mProcessReport[i] = nullptr;
+}
+
+/* attribute long currentProcess; */
+NS_IMETHODIMP nsMsgSendReport::GetCurrentProcess(int32_t* aCurrentProcess) {
+ NS_ENSURE_ARG_POINTER(aCurrentProcess);
+ *aCurrentProcess = mCurrentProcess;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgSendReport::SetCurrentProcess(int32_t aCurrentProcess) {
+ if (aCurrentProcess < 0 || aCurrentProcess > SEND_LAST_PROCESS)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ mCurrentProcess = aCurrentProcess;
+ if (mProcessReport[mCurrentProcess])
+ mProcessReport[mCurrentProcess]->SetProceeded(true);
+
+ return NS_OK;
+}
+
+/* attribute long deliveryMode; */
+NS_IMETHODIMP nsMsgSendReport::GetDeliveryMode(int32_t* aDeliveryMode) {
+ NS_ENSURE_ARG_POINTER(aDeliveryMode);
+ *aDeliveryMode = mDeliveryMode;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgSendReport::SetDeliveryMode(int32_t aDeliveryMode) {
+ mDeliveryMode = aDeliveryMode;
+ return NS_OK;
+}
+
+/* void Reset (); */
+NS_IMETHODIMP nsMsgSendReport::Reset() {
+ uint32_t i;
+ for (i = 0; i <= SEND_LAST_PROCESS; i++)
+ if (mProcessReport[i]) mProcessReport[i]->Reset();
+
+ mCurrentProcess = 0;
+ mDeliveryMode = 0;
+ mAlreadyDisplayReport = false;
+
+ return NS_OK;
+}
+
+/* void setProceeded (in long process, in boolean proceeded); */
+NS_IMETHODIMP nsMsgSendReport::SetProceeded(int32_t process, bool proceeded) {
+ if (process < process_Current || process > SEND_LAST_PROCESS)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (process == process_Current) process = mCurrentProcess;
+
+ if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED;
+
+ return mProcessReport[process]->SetProceeded(proceeded);
+}
+
+/* void setError (in long process, in nsresult error, in boolean
+ * overwriteError); */
+NS_IMETHODIMP nsMsgSendReport::SetError(int32_t process, nsresult newError,
+ bool overwriteError) {
+ if (process < process_Current || process > SEND_LAST_PROCESS)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (process == process_Current) {
+ if (mCurrentProcess == process_Current)
+ // We don't know what we're currently trying to do
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ process = mCurrentProcess;
+ }
+
+ if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult currError = NS_OK;
+ mProcessReport[process]->GetError(&currError);
+ if (overwriteError || NS_SUCCEEDED(currError))
+ return mProcessReport[process]->SetError(newError);
+ else
+ return NS_OK;
+}
+
+/* void setMessage (in long process, in wstring message, in boolean
+ * overwriteMessage); */
+NS_IMETHODIMP nsMsgSendReport::SetMessage(int32_t process,
+ const char16_t* message,
+ bool overwriteMessage) {
+ if (process < process_Current || process > SEND_LAST_PROCESS)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (process == process_Current) {
+ if (mCurrentProcess == process_Current)
+ // We don't know what we're currently trying to do
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ process = mCurrentProcess;
+ }
+
+ if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED;
+
+ nsString currMessage;
+ mProcessReport[process]->GetMessage(getter_Copies(currMessage));
+ if (overwriteMessage || currMessage.IsEmpty())
+ return mProcessReport[process]->SetMessage(message);
+ else
+ return NS_OK;
+}
+
+/* nsIMsgProcessReport getProcessReport (in long process); */
+NS_IMETHODIMP nsMsgSendReport::GetProcessReport(int32_t process,
+ nsIMsgProcessReport** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ if (process < process_Current || process > SEND_LAST_PROCESS)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (process == process_Current) {
+ if (mCurrentProcess == process_Current)
+ // We don't know what we're currently trying to do
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ process = mCurrentProcess;
+ }
+
+ NS_IF_ADDREF(*_retval = mProcessReport[process]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSendReport::DisplayReport(mozIDOMWindowProxy* window,
+ bool showErrorOnly,
+ bool dontShowReportTwice,
+ nsresult* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ NS_ENSURE_TRUE(mCurrentProcess >= 0 && mCurrentProcess <= SEND_LAST_PROCESS,
+ NS_ERROR_NOT_INITIALIZED);
+
+ nsresult currError = NS_OK;
+ mProcessReport[mCurrentProcess]->GetError(&currError);
+ *_retval = currError;
+
+ if (dontShowReportTwice && mAlreadyDisplayReport) return NS_OK;
+
+ if (showErrorOnly && NS_SUCCEEDED(currError)) return NS_OK;
+
+ nsString currMessage;
+ mProcessReport[mCurrentProcess]->GetMessage(getter_Copies(currMessage));
+
+ nsresult rv; // don't step on currError.
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties",
+ getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) {
+ // TODO need to display a generic hardcoded message
+ mAlreadyDisplayReport = true;
+ return NS_OK;
+ }
+
+ nsString dialogTitle;
+ nsString dialogMessage;
+
+ if (NS_SUCCEEDED(currError)) {
+ // TODO display a success error message
+ return NS_OK;
+ }
+
+ // Do we have an explanation of the error? if no, try to build one...
+ if (currMessage.IsEmpty()) {
+#ifdef __GNUC__
+// Temporary workaround until bug 783526 is fixed.
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wswitch"
+#endif
+ switch (currError) {
+ case NS_BINDING_ABORTED:
+ case NS_MSG_UNABLE_TO_SEND_LATER:
+ case NS_MSG_UNABLE_TO_SAVE_DRAFT:
+ case NS_MSG_UNABLE_TO_SAVE_TEMPLATE:
+ // Ignore, don't need to repeat ourself.
+ break;
+ default:
+ const char* errorString = errorStringNameForErrorCode(currError);
+ nsMsgGetMessageByName(errorString, currMessage);
+ break;
+ }
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+ }
+
+ if (mDeliveryMode == nsIMsgCompDeliverMode::Now ||
+ mDeliveryMode == nsIMsgCompDeliverMode::SendUnsent) {
+ // SMTP is taking care of it's own error message and will return
+ // NS_ERROR_BUT_DONT_SHOW_ALERT as error code. In that case, we must not
+ // show an alert ourself.
+ if (currError == NS_ERROR_BUT_DONT_SHOW_ALERT) {
+ mAlreadyDisplayReport = true;
+ return NS_OK;
+ }
+
+ bundle->GetStringFromName("sendMessageErrorTitle", dialogTitle);
+
+ const char* preStrName = "sendFailed";
+ bool askToGoBackToCompose = false;
+ switch (mCurrentProcess) {
+ case process_BuildMessage:
+ preStrName = "sendFailed";
+ askToGoBackToCompose = false;
+ break;
+ case process_NNTP:
+ preStrName = "sendFailed";
+ askToGoBackToCompose = false;
+ break;
+ case process_SMTP:
+ bool nntpProceeded;
+ mProcessReport[process_NNTP]->GetProceeded(&nntpProceeded);
+ if (nntpProceeded)
+ preStrName = "sendFailedButNntpOk";
+ else
+ preStrName = "sendFailed";
+ askToGoBackToCompose = false;
+ break;
+ case process_Copy:
+ preStrName = "failedCopyOperation";
+ askToGoBackToCompose = (mDeliveryMode == nsIMsgCompDeliverMode::Now);
+ break;
+ case process_FCC:
+ preStrName = "failedCopyOperation";
+ askToGoBackToCompose = (mDeliveryMode == nsIMsgCompDeliverMode::Now);
+ break;
+ }
+ bundle->GetStringFromName(preStrName, dialogMessage);
+
+ // Do we already have an error message?
+ if (!askToGoBackToCompose && currMessage.IsEmpty()) {
+ // we don't have an error description but we can put a generic explanation
+ bundle->GetStringFromName("genericFailureExplanation", currMessage);
+ }
+
+ if (!currMessage.IsEmpty()) {
+ // Don't need to repeat ourself!
+ if (!currMessage.Equals(dialogMessage)) {
+ if (!dialogMessage.IsEmpty()) dialogMessage.Append(char16_t('\n'));
+ dialogMessage.Append(currMessage);
+ }
+ }
+
+ if (askToGoBackToCompose) {
+ bool oopsGiveMeBackTheComposeWindow = true;
+ nsString text1;
+ bundle->GetStringFromName("returnToComposeWindowQuestion", text1);
+ if (!dialogMessage.IsEmpty()) dialogMessage.AppendLiteral("\n");
+ dialogMessage.Append(text1);
+ nsMsgAskBooleanQuestionByString(window, dialogMessage.get(),
+ &oopsGiveMeBackTheComposeWindow,
+ dialogTitle.get());
+ if (!oopsGiveMeBackTheComposeWindow) *_retval = NS_OK;
+ } else
+ nsMsgDisplayMessageByString(window, dialogMessage.get(),
+ dialogTitle.get());
+ } else {
+ const char* title;
+ const char* messageName;
+
+ switch (mDeliveryMode) {
+ case nsIMsgCompDeliverMode::Later:
+ title = "sendLaterErrorTitle";
+ messageName = "unableToSendLater";
+ break;
+
+ case nsIMsgCompDeliverMode::AutoSaveAsDraft:
+ case nsIMsgCompDeliverMode::SaveAsDraft:
+ title = "saveDraftErrorTitle";
+ messageName = "unableToSaveDraft";
+ break;
+
+ case nsIMsgCompDeliverMode::SaveAsTemplate:
+ title = "saveTemplateErrorTitle";
+ messageName = "unableToSaveTemplate";
+ break;
+
+ default:
+ /* This should never happen! */
+ title = "sendMessageErrorTitle";
+ messageName = "sendFailed";
+ break;
+ }
+
+ bundle->GetStringFromName(title, dialogTitle);
+ bundle->GetStringFromName(messageName, dialogMessage);
+
+ // Do we have an error message...
+ if (currMessage.IsEmpty()) {
+ // we don't have an error description but we can put a generic explanation
+ bundle->GetStringFromName("genericFailureExplanation", currMessage);
+ }
+
+ if (!currMessage.IsEmpty()) {
+ if (!dialogMessage.IsEmpty()) dialogMessage.Append(char16_t('\n'));
+ dialogMessage.Append(currMessage);
+ }
+ nsMsgDisplayMessageByString(window, dialogMessage.get(), dialogTitle.get());
+ }
+
+ mAlreadyDisplayReport = true;
+ return NS_OK;
+}
diff --git a/comm/mailnews/compose/src/nsMsgSendReport.h b/comm/mailnews/compose/src/nsMsgSendReport.h
new file mode 100644
index 0000000000..0fcc2f1c9f
--- /dev/null
+++ b/comm/mailnews/compose/src/nsMsgSendReport.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMsgSendReport_h__
+#define __nsMsgSendReport_h__
+
+#include "nsIMsgSendReport.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+class nsMsgProcessReport : public nsIMsgProcessReport {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPROCESSREPORT
+
+ nsMsgProcessReport();
+
+ private:
+ virtual ~nsMsgProcessReport();
+ bool mProceeded;
+ nsresult mError;
+ nsString mMessage;
+};
+
+class nsMsgSendReport : public nsIMsgSendReport {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSENDREPORT
+
+ nsMsgSendReport();
+
+ protected:
+ virtual ~nsMsgSendReport();
+
+ private:
+#define SEND_LAST_PROCESS process_FCC
+ nsCOMPtr<nsIMsgProcessReport> mProcessReport[SEND_LAST_PROCESS + 1];
+ int32_t mDeliveryMode;
+ int32_t mCurrentProcess;
+ bool mAlreadyDisplayReport;
+};
+
+#endif
diff --git a/comm/mailnews/compose/src/nsSmtpUrl.cpp b/comm/mailnews/compose/src/nsSmtpUrl.cpp
new file mode 100644
index 0000000000..b9ac451650
--- /dev/null
+++ b/comm/mailnews/compose/src/nsSmtpUrl.cpp
@@ -0,0 +1,755 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsSmtpUrl.h"
+#include "nsString.h"
+#include "nsMsgUtils.h"
+#include "nsIMimeConverter.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsCRT.h"
+#include "mozilla/Encoding.h"
+
+/////////////////////////////////////////////////////////////////////////////////////
+// mailto url definition
+/////////////////////////////////////////////////////////////////////////////////////
+nsMailtoUrl::nsMailtoUrl() { mFormat = nsIMsgCompFormat::Default; }
+
+nsMailtoUrl::~nsMailtoUrl() {}
+
+NS_IMPL_ISUPPORTS(nsMailtoUrl, nsIMailtoUrl, nsIURI)
+
+static void UnescapeAndConvert(nsIMimeConverter* mimeConverter,
+ const nsACString& escaped, nsACString& out) {
+ NS_ASSERTION(mimeConverter, "Set mimeConverter before calling!");
+ // If the string is empty, do absolutely nothing.
+ if (escaped.IsEmpty()) return;
+
+ MsgUnescapeString(escaped, 0, out);
+ nsAutoCString decodedString;
+ nsresult rv = mimeConverter->DecodeMimeHeaderToUTF8(out, "UTF_8", false, true,
+ decodedString);
+ if (NS_SUCCEEDED(rv) && !decodedString.IsEmpty()) out = decodedString;
+}
+
+nsresult nsMailtoUrl::ParseMailtoUrl(char* searchPart) {
+ char* rest = searchPart;
+ nsCString escapedInReplyToPart;
+ nsCString escapedToPart;
+ nsCString escapedCcPart;
+ nsCString escapedSubjectPart;
+ nsCString escapedNewsgroupPart;
+ nsCString escapedNewsHostPart;
+ nsCString escapedReferencePart;
+ nsCString escapedBodyPart;
+ nsCString escapedBccPart;
+ nsCString escapedFollowUpToPart;
+ nsCString escapedFromPart;
+ nsCString escapedHtmlPart;
+ nsCString escapedOrganizationPart;
+ nsCString escapedReplyToPart;
+ nsCString escapedPriorityPart;
+
+ // okay, first, free up all of our old search part state.....
+ CleanupMailtoState();
+ // m_toPart has the escaped address from before the query string, copy it
+ // over so we can add on any additional to= addresses and unescape them all.
+ escapedToPart = m_toPart;
+
+ if (rest && *rest == '?') {
+ /* start past the '?' */
+ rest++;
+ }
+
+ if (rest) {
+ char* token = NS_strtok("&", &rest);
+ while (token && *token) {
+ char* value = 0;
+ char* eq = PL_strchr(token, '=');
+ if (eq) {
+ value = eq + 1;
+ *eq = 0;
+ }
+
+ nsCString decodedName;
+ MsgUnescapeString(nsDependentCString(token), 0, decodedName);
+
+ if (decodedName.IsEmpty()) break;
+
+ switch (NS_ToUpper(decodedName.First())) {
+ /* DO NOT support attachment= in mailto urls. This poses a security
+ fire hole!!! case 'A': if (!PL_strcasecmp (token, "attachment"))
+ m_attachmentPart = value;
+ break;
+ */
+ case 'B':
+ if (decodedName.LowerCaseEqualsLiteral("bcc")) {
+ if (!escapedBccPart.IsEmpty()) {
+ escapedBccPart += ", ";
+ escapedBccPart += value;
+ } else
+ escapedBccPart = value;
+ } else if (decodedName.LowerCaseEqualsLiteral("body")) {
+ if (!escapedBodyPart.IsEmpty()) {
+ escapedBodyPart += "\n";
+ escapedBodyPart += value;
+ } else
+ escapedBodyPart = value;
+ }
+ break;
+ case 'C':
+ if (decodedName.LowerCaseEqualsLiteral("cc")) {
+ if (!escapedCcPart.IsEmpty()) {
+ escapedCcPart += ", ";
+ escapedCcPart += value;
+ } else
+ escapedCcPart = value;
+ }
+ break;
+ case 'F':
+ if (decodedName.LowerCaseEqualsLiteral("followup-to"))
+ escapedFollowUpToPart = value;
+ else if (decodedName.LowerCaseEqualsLiteral("from"))
+ escapedFromPart = value;
+ break;
+ case 'H':
+ if (decodedName.LowerCaseEqualsLiteral("html-part") ||
+ decodedName.LowerCaseEqualsLiteral("html-body")) {
+ // escapedHtmlPart holds the body for both html-part and html-body.
+ escapedHtmlPart = value;
+ mFormat = nsIMsgCompFormat::HTML;
+ }
+ break;
+ case 'I':
+ if (decodedName.LowerCaseEqualsLiteral("in-reply-to"))
+ escapedInReplyToPart = value;
+ break;
+
+ case 'N':
+ if (decodedName.LowerCaseEqualsLiteral("newsgroups"))
+ escapedNewsgroupPart = value;
+ else if (decodedName.LowerCaseEqualsLiteral("newshost"))
+ escapedNewsHostPart = value;
+ break;
+ case 'O':
+ if (decodedName.LowerCaseEqualsLiteral("organization"))
+ escapedOrganizationPart = value;
+ break;
+ case 'R':
+ if (decodedName.LowerCaseEqualsLiteral("references"))
+ escapedReferencePart = value;
+ else if (decodedName.LowerCaseEqualsLiteral("reply-to"))
+ escapedReplyToPart = value;
+ break;
+ case 'S':
+ if (decodedName.LowerCaseEqualsLiteral("subject"))
+ escapedSubjectPart = value;
+ break;
+ case 'P':
+ if (decodedName.LowerCaseEqualsLiteral("priority"))
+ escapedPriorityPart = PL_strdup(value);
+ break;
+ case 'T':
+ if (decodedName.LowerCaseEqualsLiteral("to")) {
+ if (!escapedToPart.IsEmpty()) {
+ escapedToPart += ", ";
+ escapedToPart += value;
+ } else
+ escapedToPart = value;
+ }
+ break;
+ default:
+ break;
+ } // end of switch statement...
+
+ if (eq) *eq = '='; /* put it back */
+ token = NS_strtok("&", &rest);
+ } // while we still have part of the url to parse...
+ } // if rest && *rest
+
+ nsresult rv;
+ // Get a global converter
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now unescape everything, and mime-decode the things that can be encoded.
+ UnescapeAndConvert(mimeConverter, escapedToPart, m_toPart);
+ UnescapeAndConvert(mimeConverter, escapedCcPart, m_ccPart);
+ UnescapeAndConvert(mimeConverter, escapedBccPart, m_bccPart);
+ UnescapeAndConvert(mimeConverter, escapedSubjectPart, m_subjectPart);
+ UnescapeAndConvert(mimeConverter, escapedNewsgroupPart, m_newsgroupPart);
+ UnescapeAndConvert(mimeConverter, escapedReferencePart, m_referencePart);
+ if (!escapedBodyPart.IsEmpty())
+ MsgUnescapeString(escapedBodyPart, 0, m_bodyPart);
+ if (!escapedHtmlPart.IsEmpty())
+ MsgUnescapeString(escapedHtmlPart, 0, m_htmlPart);
+ UnescapeAndConvert(mimeConverter, escapedNewsHostPart, m_newsHostPart);
+ UnescapeAndConvert(mimeConverter, escapedFollowUpToPart, m_followUpToPart);
+ UnescapeAndConvert(mimeConverter, escapedFromPart, m_fromPart);
+ UnescapeAndConvert(mimeConverter, escapedOrganizationPart,
+ m_organizationPart);
+ UnescapeAndConvert(mimeConverter, escapedReplyToPart, m_replyToPart);
+ UnescapeAndConvert(mimeConverter, escapedPriorityPart, m_priorityPart);
+
+ nsCString inReplyToPart; // Not a member like the others...
+ UnescapeAndConvert(mimeConverter, escapedInReplyToPart, inReplyToPart);
+
+ if (!inReplyToPart.IsEmpty()) {
+ // Ensure that References and In-Reply-To are consistent... The last
+ // reference will be used as In-Reply-To header.
+ if (m_referencePart.IsEmpty()) {
+ // If References is not set, set it to be the In-Reply-To.
+ m_referencePart = inReplyToPart;
+ } else {
+ // References is set. Add the In-Reply-To as last header unless it's
+ // set as last reference already.
+ int32_t lastRefStart = m_referencePart.RFindChar('<');
+ nsAutoCString lastReference;
+ if (lastRefStart != -1)
+ lastReference = StringTail(m_referencePart, lastRefStart);
+ else
+ lastReference = m_referencePart;
+
+ if (lastReference != inReplyToPart) {
+ m_referencePart += " ";
+ m_referencePart += inReplyToPart;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMailtoUrl::SetSpecInternal(const nsACString& aSpec) {
+ nsresult rv = NS_MutateURI(NS_SIMPLEURIMUTATOR_CONTRACTID)
+ .SetSpec(aSpec)
+ .Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+nsresult nsMailtoUrl::CleanupMailtoState() {
+ m_ccPart = "";
+ m_subjectPart = "";
+ m_newsgroupPart = "";
+ m_newsHostPart = "";
+ m_referencePart = "";
+ m_bodyPart = "";
+ m_bccPart = "";
+ m_followUpToPart = "";
+ m_fromPart = "";
+ m_htmlPart = "";
+ m_organizationPart = "";
+ m_replyToPart = "";
+ m_priorityPart = "";
+ return NS_OK;
+}
+
+nsresult nsMailtoUrl::ParseUrl() {
+ // we can get the path from the simple url.....
+ nsCString escapedPath;
+ m_baseURL->GetPathQueryRef(escapedPath);
+
+ int32_t startOfSearchPart = escapedPath.FindChar('?');
+ if (startOfSearchPart >= 0) {
+ // now parse out the search field...
+ nsAutoCString searchPart(Substring(escapedPath, startOfSearchPart));
+
+ if (!searchPart.IsEmpty()) {
+ // now we need to strip off the search part from the
+ // to part....
+ escapedPath.SetLength(startOfSearchPart);
+ MsgUnescapeString(escapedPath, 0, m_toPart);
+ ParseMailtoUrl(searchPart.BeginWriting());
+ }
+ } else if (!escapedPath.IsEmpty()) {
+ MsgUnescapeString(escapedPath, 0, m_toPart);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetMessageContents(nsACString& aToPart, nsACString& aCcPart,
+ nsACString& aBccPart, nsACString& aSubjectPart,
+ nsACString& aBodyPart, nsACString& aHtmlPart,
+ nsACString& aReferencePart,
+ nsACString& aNewsgroupPart,
+ MSG_ComposeFormat* aFormat) {
+ NS_ENSURE_ARG_POINTER(aFormat);
+
+ aToPart = m_toPart;
+ aCcPart = m_ccPart;
+ aBccPart = m_bccPart;
+ aSubjectPart = m_subjectPart;
+ aBodyPart = m_bodyPart;
+ aHtmlPart = m_htmlPart;
+ aReferencePart = m_referencePart;
+ aNewsgroupPart = m_newsgroupPart;
+ *aFormat = mFormat;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetFromPart(nsACString& aResult) {
+ aResult = m_fromPart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetFollowUpToPart(nsACString& aResult) {
+ aResult = m_followUpToPart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetOrganizationPart(nsACString& aResult) {
+ aResult = m_organizationPart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetReplyToPart(nsACString& aResult) {
+ aResult = m_replyToPart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetPriorityPart(nsACString& aResult) {
+ aResult = m_priorityPart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetNewsHostPart(nsACString& aResult) {
+ aResult = m_newsHostPart;
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Begin nsIURI support
+//////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMailtoUrl::GetSpec(nsACString& aSpec) {
+ return m_baseURL->GetSpec(aSpec);
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetPrePath(aPrePath);
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetScheme(nsACString& aScheme) {
+ return m_baseURL->GetScheme(aScheme);
+}
+
+nsresult nsMailtoUrl::SetScheme(const nsACString& aScheme) {
+ nsresult rv = NS_MutateURI(m_baseURL).SetScheme(aScheme).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetUserPass(nsACString& aUserPass) {
+ return m_baseURL->GetUserPass(aUserPass);
+}
+
+nsresult nsMailtoUrl::SetUserPass(const nsACString& aUserPass) {
+ nsresult rv =
+ NS_MutateURI(m_baseURL).SetUserPass(aUserPass).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetUsername(nsACString& aUsername) {
+ return m_baseURL->GetUsername(aUsername);
+}
+
+nsresult nsMailtoUrl::SetUsername(const nsACString& aUsername) {
+ nsresult rv =
+ NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetPassword(nsACString& aPassword) {
+ return m_baseURL->GetPassword(aPassword);
+}
+
+nsresult nsMailtoUrl::SetPassword(const nsACString& aPassword) {
+ nsresult rv =
+ NS_MutateURI(m_baseURL).SetPassword(aPassword).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetHostPort(nsACString& aHostPort) {
+ return m_baseURL->GetHost(aHostPort);
+}
+
+nsresult nsMailtoUrl::SetHostPort(const nsACString& aHostPort) {
+ nsresult rv =
+ NS_MutateURI(m_baseURL).SetHostPort(aHostPort).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetHost(nsACString& aHost) {
+ return m_baseURL->GetHost(aHost);
+}
+
+nsresult nsMailtoUrl::SetHost(const nsACString& aHost) {
+ nsresult rv = NS_MutateURI(m_baseURL).SetHost(aHost).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetPort(int32_t* aPort) {
+ return m_baseURL->GetPort(aPort);
+}
+
+nsresult nsMailtoUrl::SetPort(int32_t aPort) {
+ nsresult rv = NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetPathQueryRef(nsACString& aPath) {
+ return m_baseURL->GetPathQueryRef(aPath);
+}
+
+nsresult nsMailtoUrl::SetPathQueryRef(const nsACString& aPath) {
+ nsresult rv =
+ NS_MutateURI(m_baseURL).SetPathQueryRef(aPath).Finalize(m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ParseUrl();
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetAsciiHost(nsACString& aHostA) {
+ return m_baseURL->GetAsciiHost(aHostA);
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetAsciiHostPort(nsACString& aHostPortA) {
+ return m_baseURL->GetAsciiHostPort(aHostPortA);
+}
+
+NS_IMETHODIMP nsMailtoUrl::GetAsciiSpec(nsACString& aSpecA) {
+ return m_baseURL->GetAsciiSpec(aSpecA);
+}
+
+NS_IMETHODIMP nsMailtoUrl::SchemeIs(const char* aScheme, bool* _retval) {
+ return m_baseURL->SchemeIs(aScheme, _retval);
+}
+
+NS_IMETHODIMP nsMailtoUrl::Equals(nsIURI* other, bool* _retval) {
+ // The passed-in URI might be an nsMailtoUrl. Pass our inner URL to its
+ // Equals method. The other nsMailtoUrl will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->Equals(m_baseURL, _retval);
+
+ return m_baseURL->Equals(other, _retval);
+}
+
+nsresult nsMailtoUrl::Clone(nsIURI** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ RefPtr<nsMailtoUrl> clone = new nsMailtoUrl();
+
+ NS_ENSURE_TRUE(clone, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = NS_MutateURI(m_baseURL).Finalize(clone->m_baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ clone->ParseUrl();
+ clone.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailtoUrl::Resolve(const nsACString& relativePath,
+ nsACString& result) {
+ return m_baseURL->Resolve(relativePath, result);
+}
+
+nsresult nsMailtoUrl::SetRef(const nsACString& aRef) {
+ return NS_MutateURI(m_baseURL).SetRef(aRef).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetRef(nsACString& result) { return m_baseURL->GetRef(result); }
+
+NS_IMETHODIMP nsMailtoUrl::EqualsExceptRef(nsIURI* other, bool* result) {
+ // The passed-in URI might be an nsMailtoUrl. Pass our inner URL to its
+ // Equals method. The other nsMailtoUrl will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->EqualsExceptRef(m_baseURL, result);
+
+ return m_baseURL->EqualsExceptRef(other, result);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetSpecIgnoringRef(nsACString& result) {
+ return m_baseURL->GetSpecIgnoringRef(result);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetDisplaySpec(nsACString& aUnicodeSpec) {
+ return m_baseURL->GetDisplaySpec(aUnicodeSpec);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetDisplayHostPort(nsACString& aUnicodeHostPort) {
+ return m_baseURL->GetDisplayHostPort(aUnicodeHostPort);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetDisplayHost(nsACString& aUnicodeHost) {
+ return m_baseURL->GetDisplayHost(aUnicodeHost);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetDisplayPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetDisplayPrePath(aPrePath);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetHasRef(bool* result) { return m_baseURL->GetHasRef(result); }
+
+NS_IMETHODIMP
+nsMailtoUrl::GetFilePath(nsACString& aFilePath) {
+ return m_baseURL->GetFilePath(aFilePath);
+}
+
+nsresult nsMailtoUrl::SetFilePath(const nsACString& aFilePath) {
+ return NS_MutateURI(m_baseURL).SetFilePath(aFilePath).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP
+nsMailtoUrl::GetQuery(nsACString& aQuery) {
+ return m_baseURL->GetQuery(aQuery);
+}
+
+nsresult nsMailtoUrl::SetQuery(const nsACString& aQuery) {
+ return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL);
+}
+
+nsresult nsMailtoUrl::SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding) {
+ return NS_MutateURI(m_baseURL)
+ .SetQueryWithEncoding(aQuery, aEncoding)
+ .Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP_(void)
+nsMailtoUrl::Serialize(mozilla::ipc::URIParams& aParams) {
+ m_baseURL->Serialize(aParams);
+}
+
+NS_IMPL_ISUPPORTS(nsMailtoUrl::Mutator, nsIURISetters, nsIURIMutator)
+
+NS_IMETHODIMP
+nsMailtoUrl::Mutate(nsIURIMutator** aMutator) {
+ RefPtr<nsMailtoUrl::Mutator> mutator = new nsMailtoUrl::Mutator();
+ nsresult rv = mutator->InitFromURI(this);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mutator.forget(aMutator);
+ return NS_OK;
+}
+
+nsresult nsMailtoUrl::NewMailtoURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> mailtoUrl;
+ rv = NS_MutateURI(new Mutator()).SetSpec(aSpec).Finalize(mailtoUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mailtoUrl.forget(_retval);
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////
+// smtp url definition
+/////////////////////////////////////////////////////////////////////////////////////
+
+nsSmtpUrl::nsSmtpUrl() : nsMsgMailNewsUrl() {
+ // nsISmtpUrl specific state...
+
+ m_isPostMessage = true;
+ m_requestDSN = false;
+ m_verifyLogon = false;
+}
+
+nsSmtpUrl::~nsSmtpUrl() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsSmtpUrl, nsMsgMailNewsUrl, nsISmtpUrl)
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsISmtpUrl specific support
+
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsSmtpUrl::SetRecipients(const char* aRecipientsList) {
+ NS_ENSURE_ARG(aRecipientsList);
+ MsgUnescapeString(nsDependentCString(aRecipientsList), 0, m_toPart);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetRecipients(char** aRecipientsList) {
+ NS_ENSURE_ARG_POINTER(aRecipientsList);
+ if (aRecipientsList) *aRecipientsList = ToNewCString(m_toPart);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetSender(const char* aSender) {
+ NS_ENSURE_ARG(aSender);
+ MsgUnescapeString(nsDependentCString(aSender), 0, m_fromPart);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetSender(char** aSender) {
+ NS_ENSURE_ARG_POINTER(aSender);
+ if (aSender) *aSender = ToNewCString(m_fromPart);
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsSmtpUrl, PostMessage, bool, m_isPostMessage)
+
+NS_IMPL_GETSET(nsSmtpUrl, VerifyLogon, bool, m_verifyLogon)
+
+// the message can be stored in a file....allow accessors for getting and
+// setting the file name to post...
+NS_IMETHODIMP nsSmtpUrl::SetPostMessageFile(nsIFile* aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ m_fileName = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSmtpUrl::GetPostMessageFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ if (m_fileName) {
+ // Clone the file so nsLocalFile stat caching doesn't make the caller get
+ // the wrong file size.
+ m_fileName->Clone(aFile);
+ return *aFile ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMPL_GETSET(nsSmtpUrl, RequestDSN, bool, m_requestDSN)
+
+NS_IMETHODIMP
+nsSmtpUrl::SetDsnEnvid(const nsACString& aDsnEnvid) {
+ m_dsnEnvid = aDsnEnvid;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetDsnEnvid(nsACString& aDsnEnvid) {
+ aDsnEnvid = m_dsnEnvid;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetSenderIdentity(nsIMsgIdentity** aSenderIdentity) {
+ NS_ENSURE_ARG_POINTER(aSenderIdentity);
+ NS_ADDREF(*aSenderIdentity = m_senderIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetSenderIdentity(nsIMsgIdentity* aSenderIdentity) {
+ NS_ENSURE_ARG_POINTER(aSenderIdentity);
+ m_senderIdentity = aSenderIdentity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetPrompt(nsIPrompt* aNetPrompt) {
+ NS_ENSURE_ARG_POINTER(aNetPrompt);
+ m_netPrompt = aNetPrompt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetPrompt(nsIPrompt** aNetPrompt) {
+ NS_ENSURE_ARG_POINTER(aNetPrompt);
+ NS_ENSURE_TRUE(m_netPrompt, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aNetPrompt = m_netPrompt);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetAuthPrompt(nsIAuthPrompt* aNetAuthPrompt) {
+ NS_ENSURE_ARG_POINTER(aNetAuthPrompt);
+ m_netAuthPrompt = aNetAuthPrompt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetAuthPrompt(nsIAuthPrompt** aNetAuthPrompt) {
+ NS_ENSURE_ARG_POINTER(aNetAuthPrompt);
+ NS_ENSURE_TRUE(m_netAuthPrompt, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aNetAuthPrompt = m_netAuthPrompt);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) {
+ NS_ENSURE_ARG_POINTER(aCallbacks);
+ m_callbacks = aCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) {
+ NS_ENSURE_ARG_POINTER(aCallbacks);
+ NS_ENSURE_TRUE(m_callbacks, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aCallbacks = m_callbacks);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::SetSmtpServer(nsISmtpServer* aSmtpServer) {
+ NS_ENSURE_ARG_POINTER(aSmtpServer);
+ m_smtpServer = aSmtpServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpUrl::GetSmtpServer(nsISmtpServer** aSmtpServer) {
+ NS_ENSURE_ARG_POINTER(aSmtpServer);
+ NS_ENSURE_TRUE(m_smtpServer, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aSmtpServer = m_smtpServer);
+ return NS_OK;
+}
+
+nsresult nsSmtpUrl::NewSmtpURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = 0;
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> aSmtpUri = new nsSmtpUrl();
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ rv = aBaseURI->Resolve(aSpec, newSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aSmtpUri->SetSpecInternal(newSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = aSmtpUri->SetSpecInternal(aSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ aSmtpUri.forget(_retval);
+
+ return rv;
+}
diff --git a/comm/mailnews/compose/src/nsSmtpUrl.h b/comm/mailnews/compose/src/nsSmtpUrl.h
new file mode 100644
index 0000000000..bd205f119c
--- /dev/null
+++ b/comm/mailnews/compose/src/nsSmtpUrl.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsSmtpUrl_h__
+#define nsSmtpUrl_h__
+
+#include "nsISmtpUrl.h"
+#include "nsIURI.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsIMsgIdentity.h"
+#include "nsCOMPtr.h"
+#include "nsIPrompt.h"
+#include "nsIAuthPrompt.h"
+#include "nsISmtpServer.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIURIMutator.h"
+
+class nsMailtoUrl : public nsIMailtoUrl, public nsIURI {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURI
+ NS_DECL_NSIMAILTOURL
+
+ nsMailtoUrl();
+ static nsresult NewMailtoURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+
+ protected:
+ virtual nsresult Clone(nsIURI** _retval);
+ virtual nsresult SetSpecInternal(const nsACString& aSpec);
+ virtual nsresult SetScheme(const nsACString& aScheme);
+ virtual nsresult SetUserPass(const nsACString& aUserPass);
+ virtual nsresult SetUsername(const nsACString& aUsername);
+ virtual nsresult SetPassword(const nsACString& aPassword);
+ virtual nsresult SetHostPort(const nsACString& aHostPort);
+ virtual nsresult SetHost(const nsACString& aHost);
+ virtual nsresult SetPort(int32_t aPort);
+ virtual nsresult SetPathQueryRef(const nsACString& aPath);
+ virtual nsresult SetRef(const nsACString& aRef);
+ virtual nsresult SetFilePath(const nsACString& aFilePath);
+ virtual nsresult SetQuery(const nsACString& aQuery);
+ virtual nsresult SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding);
+
+ public:
+ class Mutator : public nsIURIMutator, public BaseURIMutator<nsMailtoUrl> {
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
+
+ NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD Finalize(nsIURI** aURI) override {
+ mURI.forget(aURI);
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetSpec(const nsACString& aSpec,
+ nsIURIMutator** aMutator) override {
+ if (aMutator) NS_ADDREF(*aMutator = this);
+ return InitFromSpec(aSpec);
+ }
+
+ explicit Mutator() {}
+
+ private:
+ virtual ~Mutator() {}
+
+ friend class nsMailtoUrl;
+ };
+ friend BaseURIMutator<nsMailtoUrl>;
+
+ protected:
+ virtual ~nsMailtoUrl();
+ nsresult ParseUrl();
+ nsresult CleanupMailtoState();
+ nsresult ParseMailtoUrl(char* searchPart);
+
+ nsCOMPtr<nsIURI> m_baseURL;
+
+ // data retrieved from parsing the url: (Note the url could be a post from
+ // file or it could be in the url)
+ nsCString m_toPart;
+ nsCString m_ccPart;
+ nsCString m_subjectPart;
+ nsCString m_newsgroupPart;
+ nsCString m_newsHostPart;
+ nsCString m_referencePart;
+ nsCString m_bodyPart;
+ nsCString m_bccPart;
+ nsCString m_followUpToPart;
+ nsCString m_fromPart;
+ nsCString m_htmlPart;
+ nsCString m_organizationPart;
+ nsCString m_replyToPart;
+ nsCString m_priorityPart;
+
+ MSG_ComposeFormat mFormat;
+};
+
+class nsSmtpUrl : public nsISmtpUrl, public nsMsgMailNewsUrl {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // From nsISmtpUrl
+ NS_DECL_NSISMTPURL
+
+ // nsSmtpUrl
+ nsSmtpUrl();
+ static nsresult NewSmtpURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+
+ protected:
+ virtual ~nsSmtpUrl();
+
+ // data retrieved from parsing the url: (Note the url could be a post from
+ // file or it could be in the url)
+ nsCString m_toPart;
+ nsCString m_fromPart;
+
+ bool m_isPostMessage;
+ bool m_requestDSN;
+ nsCString m_dsnEnvid;
+ bool m_verifyLogon;
+
+ // Smtp specific event sinks
+ nsCOMPtr<nsIFile> m_fileName;
+ nsCOMPtr<nsIMsgIdentity> m_senderIdentity;
+ nsCOMPtr<nsIPrompt> m_netPrompt;
+ nsCOMPtr<nsIAuthPrompt> m_netAuthPrompt;
+ nsCOMPtr<nsIInterfaceRequestor> m_callbacks;
+ nsCOMPtr<nsISmtpServer> m_smtpServer;
+
+ // it is possible to encode the message to parse in the form of a url.
+ // This function is used to decompose the search and path part into the bare
+ // message components (to, fcc, bcc, etc.)
+ nsresult ParseMessageToPost(char* searchPart);
+};
+
+#endif // nsSmtpUrl_h__
diff --git a/comm/mailnews/compose/test/moz.build b/comm/mailnews/compose/test/moz.build
new file mode 100644
index 0000000000..512bee25fc
--- /dev/null
+++ b/comm/mailnews/compose/test/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell.ini",
+]
diff --git a/comm/mailnews/compose/test/unit/data/429891_testcase.eml b/comm/mailnews/compose/test/unit/data/429891_testcase.eml
new file mode 100644
index 0000000000..b4fb4164c9
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/429891_testcase.eml
@@ -0,0 +1,384 @@
+From: Invalid User <from_A@foo.invalid>
+To: =?UTF-8?B?RnLDqcOpZGxlLCBUZXN0?= <to_A@foo.invalid>
+Subject: Big email
+
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
diff --git a/comm/mailnews/compose/test/unit/data/binary-after-plain.txt b/comm/mailnews/compose/test/unit/data/binary-after-plain.txt
new file mode 100644
index 0000000000..cec0697428
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/binary-after-plain.txt
Binary files differ
diff --git a/comm/mailnews/compose/test/unit/data/listexpansion.sql b/comm/mailnews/compose/test/unit/data/listexpansion.sql
new file mode 100644
index 0000000000..6c4f6491be
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/listexpansion.sql
@@ -0,0 +1,126 @@
+-- Address book with nested mailing lists for use in test_expandMailingLists.js.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 1), -- homer
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 2), -- marge
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 3), -- bart
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 4), -- lisa
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 5), -- maggie
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 6), --simpson
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 7), --marge
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', 8), --family
+ ('4808121d-ebad-4564-864d-8f1149aa053b', 9), --kids
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', 10), --parents
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 11), --older-kids
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 12), --bad-kids
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', 13); --bad-younger-kids
+
+INSERT INTO properties (card, name, value) VALUES
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PrimaryEmail', 'homer@example.com'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PhotoType', 'generic'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'LowercasePrimaryEmail', 'homer@example.com'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'DisplayName', 'Simpson'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'LastModifiedDate', '1473722922'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PopularityIndex', '0'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PreferMailFormat', '0'),
+ ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PreferDisplayName', '1'),
+
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'DisplayName', 'Marge'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PrimaryEmail', 'marge@example.com'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PhotoType', 'generic'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'LowercasePrimaryEmail', 'marge@example.com'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'LastModifiedDate', '1473723020'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PopularityIndex', '0'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PreferMailFormat', '0'),
+ ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PreferDisplayName', '1'),
+
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PhotoType', 'generic'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PopularityIndex', '0'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PreferMailFormat', '0'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PreferDisplayName', '1'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'DisplayName', 'Bart'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PrimaryEmail', 'bart@foobar.invalid'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LowercasePrimaryEmail', 'bart@foobar.invalid'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'SecondEmail', 'bart@example.com'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LowercaseSecondEmail', 'bart@example.com'),
+ ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LastModifiedDate', '1473716192'),
+
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PrimaryEmail', 'lisa@example.com'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PhotoType', 'generic'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'LowercasePrimaryEmail', 'lisa@example.com'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'DisplayName', 'lisa@example.com'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PopularityIndex', '0'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PreferMailFormat', '0'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'LastModifiedDate', '0'),
+ ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PreferDisplayName', '1'),
+
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'DisplayName', 'Maggie'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'LastModifiedDate', '1473723047'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PrimaryEmail', 'maggie@example.com'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PhotoType', 'generic'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'LowercasePrimaryEmail', 'maggie@example.com'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PopularityIndex', '0'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PreferMailFormat', '0'),
+ ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PreferDisplayName', '1'),
+
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'DisplayName', 'simpson'),
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'PrimaryEmail', 'simpson'),
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'DisplayName', 'marge'),
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'PrimaryEmail', 'marge'),
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', 'DisplayName', 'family'),
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', 'PrimaryEmail', 'family'),
+ ('4808121d-ebad-4564-864d-8f1149aa053b', 'DisplayName', 'kids'),
+ ('4808121d-ebad-4564-864d-8f1149aa053b', 'PrimaryEmail', 'kids'),
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', 'DisplayName', 'parents'),
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', 'PrimaryEmail', 'parents'),
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 'PrimaryEmail', 'older-kids'),
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 'DisplayName', 'older-kids'),
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 'DisplayName', 'bad-kids'),
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 'PrimaryEmail', 'bad-kids'),
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', 'DisplayName', 'bad-younger-kids'),
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', 'PrimaryEmail', 'bad-younger-kids');
+
+INSERT INTO lists (uid, localId, name, nickName, description) VALUES
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 1, 'simpson', '', ''),
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 2, 'marge', '', 'marges own list'),
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', 3, 'family', '', ''),
+ ('4808121d-ebad-4564-864d-8f1149aa053b', 4, 'kids', '', ''),
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', 5, 'parents', '', ''),
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 6, 'older-kids', '', ''),
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 7, 'bad-kids', '', ''),
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', 8, 'bad-younger-kids', '', '');
+
+INSERT INTO list_cards (list, card) VALUES
+ -- simpson
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '979f194e-49f2-4bbb-b364-598cdc6a7d11'), -- bart
+ ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '4dd13a79-b70c-4b43-bdba-bacd4e977c1b'), -- lisa
+ -- marge
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer
+ ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge
+ -- family
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', '4926ff7a-e929-475a-8aa8-2baac994390c'), -- parents
+ ('ad305609-3535-4d51-8c96-cd82d93aed46', '4808121d-ebad-4564-864d-8f1149aa053b'), -- kids
+ -- parents
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge
+ ('4926ff7a-e929-475a-8aa8-2baac994390c', '4926ff7a-e929-475a-8aa8-2baac994390c'), -- parents
+ -- kids
+ ('4808121d-ebad-4564-864d-8f1149aa053b', '84fa4513-9b60-4379-ade7-1e4b48d67c84'), -- older-kids
+ ('4808121d-ebad-4564-864d-8f1149aa053b', 'c96402d7-1c7b-4242-a35c-b92c8ec9dfa2'), -- maggie
+ -- older-kids
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', '4dd13a79-b70c-4b43-bdba-bacd4e977c1b'), -- lisa
+ ('84fa4513-9b60-4379-ade7-1e4b48d67c84', '979f194e-49f2-4bbb-b364-598cdc6a7d11'), -- bart
+ -- bad-kids
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', '84fa4513-9b60-4379-ade7-1e4b48d67c84'), -- older-kids
+ ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', '34e60324-4fb6-4f10-ab1b-333b07680228'), -- bad-younger-kids
+ -- bad-younger-kids
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', 'c96402d7-1c7b-4242-a35c-b92c8ec9dfa2'), -- maggie
+ ('34e60324-4fb6-4f10-ab1b-333b07680228', '8e88b9a4-2500-48e0-bcea-b1fa4eab6b72'); -- bad-kids
diff --git a/comm/mailnews/compose/test/unit/data/message1.eml b/comm/mailnews/compose/test/unit/data/message1.eml
new file mode 100644
index 0000000000..7913f5f262
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/message1.eml
@@ -0,0 +1,7 @@
+From: from_B@foo.invalid
+To: to_B@foo.invalid
+Subject: test mail
+
+this email is in dos format because that is what the interface requires
+
+test message
diff --git a/comm/mailnews/compose/test/unit/data/shift-jis.eml b/comm/mailnews/compose/test/unit/data/shift-jis.eml
new file mode 100644
index 0000000000..58f583907d
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/shift-jis.eml
@@ -0,0 +1,13 @@
+To: test@example.com
+From: test@example.com
+Subject: ISO-2022-JP and 7bit containing =67 and hence looking like quoted-printable
+Message-ID: <10a2aa17-e92f-417c-864e-575d4e371702@example.com>
+Date: Tue, 3 Apr 2018 19:09:16 +0900
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
+ Thunderbird/52.6.0
+MIME-Version: 1.0
+Content-Type: text/plain; charset=SHIFT-JIS; format=flowed
+Content-Language: ja-JP
+Content-Transfer-Encoding: 7bit
+
+Œ»‹µ
diff --git a/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt b/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt
new file mode 100644
index 0000000000..cd370be3f8
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt
@@ -0,0 +1 @@
+$B%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H(B
diff --git a/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt b/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt
new file mode 100644
index 0000000000..91f77cae45
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt
@@ -0,0 +1,2 @@
+îÁ ÀÇÅ òÏÓÓÉÑ ÇÒÁÎÉÞÉÔ Ó ëÉÔÁÅÍ, íÏÎÇÏÌÉÅÊ, ëÏÒÅÅÊ, ëÁÚÁÈÓÔÁÎÏÍ, çÒÕÚÉÅÊ
+É áÚÅÒÂÁÊÄÖÁÎÏÍ.
diff --git a/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt b/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt
new file mode 100644
index 0000000000..7a7f267540
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt
@@ -0,0 +1 @@
+‚±‚ê‚ÍShift_JIS‚̃eƒLƒXƒgƒtƒ@ƒCƒ‹‚Å‚·B
diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt b/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt
new file mode 100644
index 0000000000..dd5fd39ed2
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt
Binary files differ
diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt b/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt
new file mode 100644
index 0000000000..a13a8f09e1
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt
Binary files differ
diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-8.txt b/comm/mailnews/compose/test/unit/data/test-UTF-8.txt
new file mode 100644
index 0000000000..b5e9df9a45
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-UTF-8.txt
@@ -0,0 +1 @@
+测试文件
diff --git a/comm/mailnews/compose/test/unit/data/test-windows-1252.txt b/comm/mailnews/compose/test/unit/data/test-windows-1252.txt
new file mode 100644
index 0000000000..a98046517a
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/data/test-windows-1252.txt
@@ -0,0 +1,2 @@
+Buenos días - François a été à Paris - Budapester Straße, Berlin.
+This is text in windows-1252.
diff --git a/comm/mailnews/compose/test/unit/head_compose.js b/comm/mailnews/compose/test/unit/head_compose.js
new file mode 100644
index 0000000000..6f874335e5
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/head_compose.js
@@ -0,0 +1,280 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo.
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+// Import the required setup scripts.
+
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+// Import the smtp server scripts
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+var gDraftFolder;
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+function getBasicSmtpServer(port = 1, hostname = "localhost") {
+ let server = localAccountUtils.create_outgoing_server(
+ port,
+ "user",
+ "password",
+ hostname
+ );
+
+ // Override the default greeting so we get something predicitable
+ // in the ELHO message
+ Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test");
+
+ return server;
+}
+
+function getSmtpIdentity(senderName, smtpServer) {
+ // Set up the identity
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = senderName;
+ identity.smtpServerKey = smtpServer.key;
+
+ return identity;
+}
+
+var test;
+
+function do_check_transaction(real, expected) {
+ if (Array.isArray(real)) {
+ real = real.at(-1);
+ }
+ // real.them may have an extra QUIT on the end, where the stream is only
+ // closed after we have a chance to process it and not them. We therefore
+ // excise this from the list
+ if (real.them[real.them.length - 1] == "QUIT") {
+ real.them.pop();
+ }
+
+ Assert.equal(real.them.join(","), expected.join(","));
+ dump("Passed test " + test + "\n");
+}
+
+// This listener is designed just to call OnStopCopy() when its OnStopCopy
+// function is called - the rest of the functions are unneeded for a lot of
+// tests (but we can't use asyncCopyListener because we need the
+// nsIMsgSendListener interface as well).
+var copyListener = {
+ // nsIMsgSendListener
+ onStartSending(aMsgID, aMsgSize) {},
+ onProgress(aMsgID, aProgress, aProgressMax) {},
+ onStatus(aMsgID, aMsg) {},
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {},
+ onGetDraftFolderURI(aMsgID, aFolderURI) {},
+ onSendNotPerformed(aMsgID, aStatus) {},
+ onTransportSecurityError(msgID, status, secInfo, location) {},
+
+ // nsIMsgCopyServiceListener
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ GetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ /* globals OnStopCopy */
+ OnStopCopy(aStatus);
+ },
+
+ // QueryInterface
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgSendListener",
+ "nsIMsgCopyServiceListener",
+ ]),
+};
+
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ this.resolve(gDraftFolder && mailTestUtils.firstMsgHdr(gDraftFolder));
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function createMessage(aAttachment) {
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.from = "Nobody <nobody@tinderbox.test>";
+
+ let attachments = [];
+ if (aAttachment) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ if (aAttachment instanceof Ci.nsIFile) {
+ attachment.url = "file://" + aAttachment.path;
+ attachment.contentType = "text/plain";
+ attachment.name = aAttachment.leafName;
+ } else {
+ attachment.url = "data:,sometext";
+ attachment.name = aAttachment;
+ }
+ attachments = [attachment];
+ }
+ return richCreateMessage(fields, attachments);
+}
+
+function richCreateMessage(
+ fields,
+ attachments = [],
+ identity = null,
+ account = null
+) {
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+
+ let msgCompose = MailServices.compose.initCompose(params);
+ if (identity === null) {
+ identity = getSmtpIdentity(null, getBasicSmtpServer());
+ }
+
+ let rootFolder = localAccountUtils.rootFolder;
+ gDraftFolder = null;
+ // Make sure the drafts folder is empty
+ try {
+ gDraftFolder = rootFolder.getChildNamed("Drafts");
+ } catch (e) {
+ // we don't have to remove the folder because it doesn't exist yet
+ gDraftFolder = rootFolder.createLocalSubfolder("Drafts");
+ }
+ // Clear all messages
+ let msgs = [...gDraftFolder.msgDatabase.enumerateMessages()];
+ if (msgs.length > 0) {
+ gDraftFolder.deleteMessages(msgs, null, true, false, null, false);
+ }
+
+ // Set attachment
+ fields.removeAttachments();
+ for (let attachment of attachments) {
+ fields.addAttachment(attachment);
+ }
+
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ identity,
+ account ? account.key : "",
+ null,
+ progress
+ );
+ return promise;
+}
+
+function getAttachmentFromContent(aContent) {
+ function getBoundaryStringFromContent() {
+ let found = aContent.match(
+ /Content-Type: multipart\/mixed;\s+boundary="(.*?)"/
+ );
+ Assert.notEqual(found, null);
+ Assert.equal(found.length, 2);
+
+ return found[1];
+ }
+
+ let boundary = getBoundaryStringFromContent(aContent);
+ let regex = new RegExp(
+ "\\r\\n\\r\\n--" +
+ boundary +
+ "\\r\\n" +
+ "([\\s\\S]*?)\\r\\n" +
+ "--" +
+ boundary +
+ "--",
+ "m"
+ );
+ let attachments = aContent.match(regex);
+ Assert.notEqual(attachments, null);
+ Assert.equal(attachments.length, 2);
+ return attachments[1];
+}
+
+/**
+ * Get the body part of an MIME message.
+ *
+ * @param {string} content - The message content.
+ * @returns {string}
+ */
+function getMessageBody(content) {
+ let separatorIndex = content.indexOf("\r\n\r\n");
+ Assert.equal(content.slice(-2), "\r\n", "Should end with a line break.");
+ return content.slice(separatorIndex + 4, -2);
+}
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/compose/test/unit/test_accountKey.js b/comm/mailnews/compose/test/unit/test_accountKey.js
new file mode 100644
index 0000000000..440b2ea78a
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_accountKey.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+let MockNntpService = {
+ QueryInterface: ChromeUtils.generateQI(["nsINntpService"]),
+ postMessage(messageFile, groupNames, accountKey, urlListener, msgWindow) {
+ this.messageFile = messageFile;
+ this.groupNames = groupNames;
+ this.accountKey = accountKey;
+ },
+};
+
+let MockNntpServiceFactory = {
+ createInstance(aIID) {
+ return MockNntpService;
+ },
+};
+
+add_setup(async function () {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(
+ Components.ID("{4816dd44-fe15-4719-8cfb-a2f8ee46d787}"),
+ "Mock NntpService",
+ "@mozilla.org/messenger/nntpservice;1",
+ MockNntpServiceFactory
+ );
+});
+
+/**
+ * Test that when accountKey is not passed to sendMessageFile, MessageSend can
+ * get the right account key from identity.
+ */
+add_task(async function testAccountKey() {
+ // Set up the servers.
+ let server = setupServerDaemon();
+ localAccountUtils.loadLocalMailAccount();
+ server.start();
+ let smtpServer = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity("from@foo.invalid", smtpServer);
+ let account = MailServices.accounts.createAccount();
+ account.addIdentity(identity);
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ // Init nsIMsgSend and fields.
+ let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+ let compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ compFields.from = identity.email;
+ // Set the newsgroups filed so that the message will be passed to NntpService.
+ compFields.newsgroups = "foo.test";
+
+ let testFile = do_get_file("data/message1.eml");
+ // Notice the second argument is accountKey.
+ await msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ null,
+ copyListener,
+ null,
+ null
+ );
+
+ // Make sure the messageFile passed to NntpService is the file we set above.
+ equal(MockNntpService.messageFile, testFile);
+ // Test accountKey passed to NntpService is correct.
+ equal(MockNntpService.accountKey, account.key);
+});
diff --git a/comm/mailnews/compose/test/unit/test_attachment.js b/comm/mailnews/compose/test/unit/test_attachment.js
new file mode 100644
index 0000000000..f0c5a4d91d
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_attachment.js
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for attachment file name.
+ */
+
+var input0 =
+ " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
+ "`abcdefghijklmnopqrstuvwxyz{|}~" +
+ "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" +
+ "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf" +
+ "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf" +
+ "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf" +
+ "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef" +
+ "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff.txt";
+
+// ascii only
+var input1 =
+ "x!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
+ "`abcdefghijklmnopqrstuvwxyz{|}~.txt";
+
+var expectedCD0 = [
+ "Content-Disposition: attachment;",
+ " filename*0*=UTF-8''%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31;",
+ " filename*1*=%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45;",
+ " filename*2*=%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59;",
+ " filename*3*=%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D;",
+ " filename*4*=%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%C2%A0;",
+ " filename*5*=%C2%A1%C2%A2%C2%A3%C2%A4%C2%A5%C2%A6%C2%A7%C2%A8%C2%A9%C2%AA;",
+ " filename*6*=%C2%AB%C2%AC%C2%AD%C2%AE%C2%AF%C2%B0%C2%B1%C2%B2%C2%B3%C2%B4;",
+ " filename*7*=%C2%B5%C2%B6%C2%B7%C2%B8%C2%B9%C2%BA%C2%BB%C2%BC%C2%BD%C2%BE;",
+ " filename*8*=%C2%BF%C3%80%C3%81%C3%82%C3%83%C3%84%C3%85%C3%86%C3%87%C3%88;",
+ " filename*9*=%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90%C3%91%C3%92;",
+ " filename*10*=%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%9B;",
+ " filename*11*=%C3%9C%C3%9D%C3%9E%C3%9F%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4;",
+ " filename*12*=%C3%A5%C3%A6%C3%A7%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD;",
+ " filename*13*=%C3%AE%C3%AF%C3%B0%C3%B1%C3%B2%C3%B3%C3%B4%C3%B5%C3%B6;",
+ " filename*14*=%C3%B7%C3%B8%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BE%C3%BF%2E;",
+ " filename*15*=%74%78%74",
+ "",
+].join("\r\n");
+
+var expectedCD1 =
+ "Content-Disposition: attachment;\r\n" +
+ ' filename*0="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ";\r\n' +
+ ' filename*1="[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n';
+
+var ParamFoldingPref = {
+ // RFC2047: 0,
+ RFC2047WithCRLF: 1,
+ RFC2231: 2,
+};
+
+var expectedCTList0 = {
+ RFC2047:
+ "Content-Type: text/plain; charset=UTF-8;\r\n" +
+ ' name="=?UTF-8?B?ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJ?=' +
+ "=?UTF-8?Q?JKLMNOPQRSTUVWXYZ=5b=5c=5d=5e=5f=60abcdefghijklmnopqrstuvwx?=" +
+ "=?UTF-8?B?eXp7fH1+wqDCocKiwqPCpMKlwqbCp8KowqnCqsKrwqzCrcKuwq/CsMKx?=" +
+ "=?UTF-8?B?wrLCs8K0wrXCtsK3wrjCucK6wrvCvMK9wr7Cv8OAw4HDgsODw4TDhcOG?=" +
+ "=?UTF-8?B?w4fDiMOJw4rDi8OMw43DjsOPw5DDkcOSw5PDlMOVw5bDl8OYw5nDmsOb?=" +
+ "=?UTF-8?B?w5zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Ow?=" +
+ '=?UTF-8?B?w7HDssOzw7TDtcO2w7fDuMO5w7rDu8O8w73DvsO/LnR4dA==?="\r\n',
+
+ RFC2047WithCRLF:
+ "Content-Type: text/plain; charset=UTF-8;\r\n" +
+ ' name="=?UTF-8?B?ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJ?=\r\n' +
+ " =?UTF-8?Q?JKLMNOPQRSTUVWXYZ=5b=5c=5d=5e=5f=60abcdefghijklmnopqrstuvwx?=\r\n" +
+ " =?UTF-8?B?eXp7fH1+wqDCocKiwqPCpMKlwqbCp8KowqnCqsKrwqzCrcKuwq/CsMKx?=\r\n" +
+ " =?UTF-8?B?wrLCs8K0wrXCtsK3wrjCucK6wrvCvMK9wr7Cv8OAw4HDgsODw4TDhcOG?=\r\n" +
+ " =?UTF-8?B?w4fDiMOJw4rDi8OMw43DjsOPw5DDkcOSw5PDlMOVw5bDl8OYw5nDmsOb?=\r\n" +
+ " =?UTF-8?B?w5zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Ow?=\r\n" +
+ ' =?UTF-8?B?w7HDssOzw7TDtcO2w7fDuMO5w7rDu8O8w73DvsO/LnR4dA==?="\r\n',
+
+ RFC2231: "Content-Type: text/plain; charset=UTF-8\r\n",
+};
+
+var expectedCTList1 = {
+ RFC2047:
+ "Content-Type: text/plain; charset=UTF-8;\r\n" +
+ ' name="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n',
+
+ RFC2047WithCRLF:
+ "Content-Type: text/plain; charset=UTF-8;\r\n" +
+ ' name="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n',
+
+ RFC2231: "Content-Type: text/plain; charset=UTF-8\r\n",
+};
+
+function checkAttachment(expectedCD, expectedCT) {
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ let pos = msgData.indexOf("Content-Disposition:");
+ Assert.notEqual(pos, -1);
+ let contentDisposition = msgData.substr(pos);
+ pos = 0;
+ do {
+ pos = contentDisposition.indexOf("\n", pos);
+ Assert.notEqual(pos, -1);
+ pos++;
+ } while (contentDisposition.startsWith(" ", pos));
+ contentDisposition = contentDisposition.substr(0, pos);
+ Assert.equal(contentDisposition, expectedCD);
+
+ pos = msgData.indexOf("Content-Type:"); // multipart
+ Assert.notEqual(pos, -1);
+ msgData = msgData.substr(pos + 13);
+ pos = msgData.indexOf("Content-Type:"); // body
+ Assert.notEqual(pos, -1);
+ msgData = msgData.substr(pos + 13);
+ pos = msgData.indexOf("Content-Type:"); // first attachment
+ Assert.notEqual(pos, -1);
+ var contentType = msgData.substr(pos);
+ pos = 0;
+ do {
+ pos = contentType.indexOf("\n", pos);
+ Assert.notEqual(pos, -1);
+ pos++;
+ } while (contentType.startsWith(" ", pos));
+ contentType = contentType.substr(0, pos);
+ Assert.equal(contentType.toLowerCase(), expectedCT.toLowerCase());
+}
+
+async function testInput0() {
+ for (let folding in ParamFoldingPref) {
+ Services.prefs.setIntPref(
+ "mail.strictly_mime.parm_folding",
+ ParamFoldingPref[folding]
+ );
+ await createMessage(input0);
+ checkAttachment(expectedCD0, expectedCTList0[folding]);
+ }
+}
+
+async function testInput1() {
+ for (let folding in ParamFoldingPref) {
+ Services.prefs.setIntPref(
+ "mail.strictly_mime.parm_folding",
+ ParamFoldingPref[folding]
+ );
+ await createMessage(input1);
+ checkAttachment(expectedCD1, expectedCTList1[folding]);
+ }
+}
+
+var tests = [testInput0, testInput1];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
+
+/**
+ * Test that the full attachment content is used to pick the CTE.
+ */
+add_task(async function testBinaryAfterPlainTextAttachment() {
+ let testFile = do_get_file("data/binary-after-plain.txt");
+ await createMessage(testFile);
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ // If only the first few chars are used, encoding will be incorrectly 7bit.
+ Assert.ok(
+ msgData.includes(
+ 'Content-Disposition: attachment; filename="binary-after-plain.txt"\r\nContent-Transfer-Encoding: base64\r\n'
+ )
+ );
+});
diff --git a/comm/mailnews/compose/test/unit/test_attachment_intl.js b/comm/mailnews/compose/test/unit/test_attachment_intl.js
new file mode 100644
index 0000000000..6ce352d4df
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_attachment_intl.js
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * attachment test using non-ascii character
+ */
+
+let nonAsciiUrl = "http://\u65e5\u672c\u8a9e.jp";
+let prettyResult = "\u65e5\u672c\u8a9e.jp";
+
+function doAttachmentUrlTest() {
+ // handles non-ascii url in nsIMsgAttachment
+
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.url = nonAsciiUrl;
+
+ Assert.equal(attachment.url, nonAsciiUrl);
+}
+
+function doPrettyNameTest() {
+ // handles non-ascii url in nsIMsgCompose
+
+ let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ msgCompose.initialize(params);
+
+ Assert.equal(
+ msgCompose.AttachmentPrettyName(nonAsciiUrl, null),
+ prettyResult
+ );
+}
+
+function run_test() {
+ doAttachmentUrlTest();
+ doPrettyNameTest();
+
+ do_test_finished();
+}
diff --git a/comm/mailnews/compose/test/unit/test_autoReply.js b/comm/mailnews/compose/test/unit/test_autoReply.js
new file mode 100644
index 0000000000..a81dc7bcef
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_autoReply.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests messages generated by ReplyWithTemplate.
+ */
+
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+load("../../../resources/logHelper.js"); // watch for errors in the error console
+
+const kSender = "from@foo.invalid";
+
+var gIncomingMailFile = do_get_file("../../../data/bugmail10"); // mail to reply to
+// reply-filter-testmail: mail to reply to (but not really)
+var gIncomingMailFile2 = do_get_file("../../../data/reply-filter-testmail");
+// mail to reply to (but not really, no from)
+var gIncomingMailFile3 = do_get_file("../../../data/mail-without-from");
+var gTemplateMailFile = do_get_file("../../../data/template-latin1"); // template
+var gTemplateMailFile2 = do_get_file("../../../data/template-utf8"); // template2
+var gTemplateFolder;
+
+var gServer;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ gTemplateFolder =
+ localAccountUtils.rootFolder.createLocalSubfolder("Templates");
+
+ gServer = setupServerDaemon();
+ gServer.start();
+
+ run_next_test();
+}
+
+add_task(async function copy_gIncomingMailFile() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ // Copy gIncomingMailFile into the Inbox.
+ MailServices.copy.copyFileMessage(
+ gIncomingMailFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+});
+
+add_task(async function copy_gIncomingMailFile2() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ // Copy gIncomingMailFile2 into the Inbox.
+ MailServices.copy.copyFileMessage(
+ gIncomingMailFile2,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+});
+
+add_task(async function copy_gIncomingMailFile3() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ // Copy gIncomingMailFile3 into the Inbox.
+ MailServices.copy.copyFileMessage(
+ gIncomingMailFile3,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+});
+
+add_task(async function copy_gTemplateMailFile() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ // Copy gTemplateMailFile into the Templates folder.
+ MailServices.copy.copyFileMessage(
+ gTemplateMailFile,
+ gTemplateFolder,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+});
+
+add_task(async function copy_gTemplateMailFile2() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ // Copy gTemplateMailFile2 into the Templates folder.
+ MailServices.copy.copyFileMessage(
+ gTemplateMailFile2,
+ gTemplateFolder,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+});
+
+// Test that a reply is NOT sent when the message is not addressed to "me".
+add_task(async function testReplyingToUnaddressedFails() {
+ try {
+ await testReply(0); // mail 0 is not to us!
+ do_throw("Replied to a message not addressed to us!");
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_ABORT) {
+ throw e;
+ }
+ // Ok! We didn't reply to the message not specifically addressed to
+ // us (from@foo.invalid).
+ }
+});
+
+// Test that a reply is sent when the message is addressed to "me".
+add_task(async function testReplyingToAdressedWorksLatin1() {
+ try {
+ await testReply(1); // mail 1 is addressed to us, using template-latin1
+ } catch (e) {
+ do_throw("Didn't reply properly to a message addressed to us! " + e);
+ }
+});
+
+// Test that a reply is sent when the message is addressed to "me".
+add_task(async function testReplyingToAdressedWorksUTF8() {
+ try {
+ await testReply(1, 1); // mail 1 is addressed to us, template-utf8
+ } catch (e) {
+ do_throw("Didn't reply properly to a message addressed to us! " + e);
+ }
+});
+
+// Test that a reply is NOT even tried when the message has no From.
+add_task(async function testReplyingToMailWithNoFrom() {
+ try {
+ await testReply(2); // mail 2 has no From
+ do_throw(
+ "Shouldn't even have tried to reply reply to the message " +
+ "with no From and no Reply-To"
+ );
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+});
+
+// Test reply with template.
+async function testReply(aHrdIdx, aTemplateHdrIdx = 0) {
+ let smtpServer = getBasicSmtpServer();
+ smtpServer.port = gServer.port;
+
+ let identity = getSmtpIdentity(kSender, smtpServer);
+ localAccountUtils.msgAccount.addIdentity(identity);
+
+ let msgHdr = mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, aHrdIdx);
+ info(
+ "Msg#" +
+ aHrdIdx +
+ " author=" +
+ msgHdr.author +
+ ", recipients=" +
+ msgHdr.recipients
+ );
+ let templateHdr = mailTestUtils.getMsgHdrN(gTemplateFolder, aTemplateHdrIdx);
+
+ // See <method name="getTemplates"> in searchWidgets.xml
+ let msgTemplateUri =
+ gTemplateFolder.URI +
+ "?messageId=" +
+ templateHdr.messageId +
+ "&subject=" +
+ templateHdr.mime2DecodedSubject;
+ MailServices.compose.replyWithTemplate(
+ msgHdr,
+ msgTemplateUri,
+ null,
+ localAccountUtils.incomingServer
+ );
+
+ await TestUtils.waitForCondition(() => gServer._daemon.post);
+ let headers, body;
+ [headers, body] = MimeParser.extractHeadersAndBody(gServer._daemon.post);
+ Assert.ok(headers.get("Subject").startsWith("Auto: "));
+ Assert.equal(headers.get("Auto-submitted"), "auto-replied");
+ Assert.equal(headers.get("In-Reply-To"), "<" + msgHdr.messageId + ">");
+ Assert.equal(headers.get("References"), "<" + msgHdr.messageId + ">");
+ // XXX: something's wrong with how the fake server gets the data.
+ // The text gets converted to UTF-8 (regardless of what it is) at some point.
+ // Suspect a bug with how BinaryInputStream handles the strings.
+ if (templateHdr.charset == "windows-1252") {
+ // XXX: should really check for "åäö xlatin1"
+ if (!body.includes("åäö xlatin1")) {
+ // template-latin1 contains this
+ do_throw(
+ "latin1 body didn't go through! hdr msgid=" +
+ templateHdr.messageId +
+ ", msgbody=" +
+ body
+ );
+ }
+ } else if (templateHdr.charset == "utf-8") {
+ // XXX: should really check for "åäö xutf8"
+ if (!body.includes("åäö xutf8")) {
+ // template-utf8 contains this
+ do_throw(
+ "utf8 body didn't go through! hdr msgid=" +
+ templateHdr.messageId +
+ ", msgbody=" +
+ body
+ );
+ }
+ } else if (templateHdr.charset) {
+ do_throw(
+ "unexpected msg charset: " +
+ templateHdr.charset +
+ ", hdr msgid=" +
+ templateHdr.messageId
+ );
+ } else {
+ do_throw("didn't find a msg charset! hdr msgid=" + templateHdr.messageId);
+ }
+ gServer.resetTest();
+}
+
+add_task(function teardown() {
+ // fake server cleanup
+ gServer.stop();
+});
diff --git a/comm/mailnews/compose/test/unit/test_bcc.js b/comm/mailnews/compose/test/unit/test_bcc.js
new file mode 100644
index 0000000000..3689b920e7
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_bcc.js
@@ -0,0 +1,330 @@
+/* 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/. */
+
+/**
+ * Test that when bcc field is set, bcc header should not exist in the sent
+ * mail, but should exist in the mail copy (e.g. Sent folder).
+ */
+
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gServer;
+var gSentFolder;
+
+function cleanUpSent() {
+ let messages = [...gSentFolder.msgDatabase.enumerateMessages()];
+ if (messages.length) {
+ gSentFolder.deleteMessages(messages, null, true, false, null, false);
+ }
+}
+
+/**
+ * Load local mail account and start fake SMTP server.
+ */
+add_setup(async function setup() {
+ localAccountUtils.loadLocalMailAccount();
+ gServer = setupServerDaemon();
+ gServer.start();
+ registerCleanupFunction(() => {
+ gServer.stop();
+ });
+ gSentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+});
+
+/**
+ * Send a msg with bcc field set, then check the sent mail doesn't contain bcc
+ * header, but the mail saved to the Sent folder contains bcc header.
+ */
+add_task(async function testBcc() {
+ gServer.resetTest();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+
+ // Prepare the comp fields, including the bcc field.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.to = "Nobody <to@tinderbox.invalid>";
+ fields.subject = "Test bcc";
+ fields.bcc = "bcc@tinderbox.invalid";
+ fields.body = "A\r\nBcc: \r\n mail body\r\n.";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+
+ // Send the mail.
+ let msgCompose = MailServices.compose.initCompose(params);
+ msgCompose.type = Ci.nsIMsgCompType.New;
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await promise;
+
+ let expectedBody = `\r\n\r\n${fields.body}`;
+ // Should not contain extra \r\n between head and body.
+ let notExpectedBody = `\r\n\r\n\r\n${fields.body}`;
+
+ Assert.ok(gServer._daemon.post.includes("Subject: Test bcc"));
+ // Check that bcc header doesn't exist in the sent mail.
+ Assert.ok(!gServer._daemon.post.includes("Bcc: bcc@tinderbox.invalid"));
+ Assert.ok(gServer._daemon.post.includes(expectedBody));
+ Assert.ok(!gServer._daemon.post.includes(notExpectedBody));
+
+ let msgData = mailTestUtils.loadMessageToString(
+ gSentFolder,
+ mailTestUtils.getMsgHdrN(gSentFolder, 0)
+ );
+ Assert.ok(msgData.includes("Subject: Test bcc"));
+ // Check that bcc header exists in the mail copy.
+ Assert.ok(msgData.includes("Bcc: bcc@tinderbox.invalid"));
+ Assert.ok(msgData.includes(fields.body));
+ Assert.ok(msgData.includes(expectedBody));
+ Assert.ok(!msgData.includes(notExpectedBody));
+});
+
+/**
+ * Test that non-utf8 eml attachment is intact after sent to a bcc recipient.
+ */
+add_task(async function testBccWithNonUtf8EmlAttachment() {
+ gServer.resetTest();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+
+ // Prepare the comp fields, including the bcc field.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.to = "Nobody <to@tinderbox.invalid>";
+ fields.subject = "Test bcc with non-utf8 eml attachment";
+ fields.bcc = "bcc@tinderbox.invalid";
+
+ let testFile = do_get_file("data/shift-jis.eml");
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ attachment.url = "file://" + testFile.path;
+ attachment.contentType = "message/rfc822";
+ attachment.name = testFile.leafName;
+ fields.addAttachment(attachment);
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+
+ // Send the mail.
+ let msgCompose = MailServices.compose.initCompose(params);
+ msgCompose.type = Ci.nsIMsgCompType.New;
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await promise;
+
+ Assert.ok(
+ gServer._daemon.post.includes(
+ "Subject: Test bcc with non-utf8 eml attachment"
+ )
+ );
+ // \x8C\xBB\x8B\xB5 is ç¾æ³ in SHIFT-JIS.
+ Assert.ok(gServer._daemon.post.includes("\r\n\r\n\x8C\xBB\x8B\xB5\r\n"));
+});
+
+add_task(async function testBccWithSendLater() {
+ gServer.resetTest();
+ cleanUpSent();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ let account = MailServices.accounts.createAccount();
+ account.addIdentity(identity);
+
+ // Prepare the comp fields, including the bcc field.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.to = "Nobody <to@tinderbox.invalid>";
+ fields.subject = "Test bcc with send later";
+ fields.bcc = "bcc@tinderbox.invalid";
+ fields.body = "A\r\nBcc: \r\n mail body\r\n.";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+
+ // Queue the mail to send later.
+ let msgCompose = MailServices.compose.initCompose(params);
+ msgCompose.type = Ci.nsIMsgCompType.New;
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await promise;
+
+ let onStopSendingPromise = PromiseUtils.defer();
+ let msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+ );
+ let sendLaterListener = {
+ onStartSending() {},
+ onMessageStartSending() {},
+ onMessageSendProgress() {},
+ onMessageSendError() {},
+ onStopSending() {
+ let expectedBody = `\r\n\r\n${fields.body}`;
+ // Should not contain extra \r\n between head and body.
+ let notExpectedBody = `\r\n\r\n\r\n${fields.body}`;
+
+ Assert.ok(gServer._daemon.post.includes(`Subject: ${fields.subject}`));
+ // Check that bcc header doesn't exist in the sent mail.
+ Assert.ok(!gServer._daemon.post.includes("Bcc: bcc@tinderbox.invalid"));
+ Assert.ok(gServer._daemon.post.includes(expectedBody));
+ Assert.ok(!gServer._daemon.post.includes(notExpectedBody));
+
+ let msgData = mailTestUtils.loadMessageToString(
+ gSentFolder,
+ mailTestUtils.getMsgHdrN(gSentFolder, 0)
+ );
+ Assert.ok(msgData.includes(`Subject: ${fields.subject}`));
+ // Check that bcc header exists in the mail copy.
+ Assert.ok(msgData.includes("Bcc: bcc@tinderbox.invalid"));
+ Assert.ok(msgData.includes(fields.body));
+ Assert.ok(msgData.includes(expectedBody));
+ Assert.ok(!msgData.includes(notExpectedBody));
+
+ msgSendLater.removeListener(sendLaterListener);
+ onStopSendingPromise.resolve();
+ },
+ };
+
+ msgSendLater.addListener(sendLaterListener);
+
+ // Actually send the message.
+ msgSendLater.sendUnsentMessages(identity);
+ await onStopSendingPromise.promise;
+});
+
+/**
+ * Test that sending bcc only message from Outbox works. With a bcc only
+ * message, nsMsgSendLater passes `To: undisclosed-recipients: ;` to
+ * SmtpService, but it should not be sent to the SMTP server.
+ */
+add_task(async function testBccOnlyWithSendLater() {
+ gServer.resetTest();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ let account = MailServices.accounts.createAccount();
+ account.addIdentity(identity);
+
+ // Prepare the comp fields, including the bcc field.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.subject = "Test bcc only with send later";
+ fields.bcc = "bcc@tinderbox.invalid";
+ fields.body = "A\r\nBcc: \r\n mail body\r\n.";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+
+ // Queue the mail to send later.
+ let msgCompose = MailServices.compose.initCompose(params);
+ msgCompose.type = Ci.nsIMsgCompType.New;
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await promise;
+
+ let onStopSendingPromise = PromiseUtils.defer();
+ let msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+ );
+ let sendLaterListener = {
+ onStartSending() {},
+ onMessageStartSending() {},
+ onMessageSendProgress() {},
+ onMessageSendError() {},
+ onStopSending() {
+ // Should not include RCPT TO:<undisclosed-recipients: ;>
+ do_check_transaction(gServer.playTransaction(), [
+ "EHLO test",
+ `MAIL FROM:<from@tinderbox.invalid> BODY=8BITMIME SIZE=${gServer._daemon.post.length}`,
+ "RCPT TO:<bcc@tinderbox.invalid>",
+ "DATA",
+ ]);
+
+ msgSendLater.removeListener(sendLaterListener);
+ onStopSendingPromise.resolve();
+ },
+ };
+
+ msgSendLater.addListener(sendLaterListener);
+
+ // Actually send the message.
+ msgSendLater.sendUnsentMessages(identity);
+ await onStopSendingPromise.promise;
+});
diff --git a/comm/mailnews/compose/test/unit/test_bug155172.js b/comm/mailnews/compose/test/unit/test_bug155172.js
new file mode 100644
index 0000000000..06c14416a7
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_bug155172.js
@@ -0,0 +1,140 @@
+/**
+ * Authentication tests for SMTP.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gNewPassword = null;
+
+// for alertTestUtils.js
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ // Just return 2 which will is pressing button 2 - enter a new password.
+ return 2;
+}
+
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ aPassword.value = gNewPassword;
+ return true;
+}
+
+var server;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "test.smtp@fakeserver";
+// kPasswordSaved is the one defined in signons-smtp.json, the other one
+// is intentionally wrong.
+var kPasswordWrong = "wrong";
+var kPasswordSaved = "smtptest";
+
+add_task(async function () {
+ registerAlertTestUtils();
+
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ // Username needs to match the login information stored in the signons json
+ // file.
+ handler.kUsername = kUsername;
+ handler.kPassword = kPasswordWrong;
+ handler.kAuthRequired = true;
+ handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below
+ return handler;
+ }
+
+ server = setupServerDaemon(createHandler);
+ server.setDebugLevel(fsDebugAll);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-smtp.json");
+
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // This time with auth
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ // Set the new password for when we get a prompt
+ gNewPassword = kPasswordWrong;
+
+ await urlListener.promise;
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPasswordSaved),
+ "AUTH LOGIN",
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPasswordWrong),
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_bug474774.js b/comm/mailnews/compose/test/unit/test_bug474774.js
new file mode 100644
index 0000000000..ba0c20667c
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_bug474774.js
@@ -0,0 +1,253 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests bug 474774 - assertions when saving send later and when sending with
+ * FCC switched off.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var server;
+var smtpServer;
+var originalData;
+var finished = false;
+var identity = null;
+
+var testFile = do_get_file("data/429891_testcase.eml");
+
+var kTestFileSender = "from_A@foo.invalid";
+var kTestFileRecipient = "to_A@foo.invalid";
+
+var kIdentityMail = "identity@foo.invalid";
+
+var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+);
+
+// This listener handles the post-sending of the actual message and checks the
+// sequence and ensures the data is correct.
+function msll() {}
+
+msll.prototype = {
+ _initialTotal: 0,
+
+ // nsIMsgSendLaterListener
+ onStartSending(aTotalMessageCount) {
+ this._initialTotal = 1;
+ Assert.equal(msgSendLater.sendingMessages, true);
+ },
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {},
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {
+ // XXX Enable this function
+ },
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ do_throw(
+ "onMessageSendError should not have been called, status: " + aStatus
+ );
+ },
+ onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {
+ print("msll onStopSending\n");
+ try {
+ Assert.equal(aSuccessful, 1);
+ Assert.equal(aStatus, 0);
+ Assert.equal(aTotalTried, 1);
+ Assert.equal(this._initialTotal, 1);
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kTestFileSender +
+ "> BODY=8BITMIME SIZE=" +
+ originalData.length,
+ "RCPT TO:<" + kTestFileRecipient + ">",
+ "DATA",
+ ]);
+
+ // Compare data file to what the server received
+ Assert.equal(originalData, server._daemon.post);
+
+ // Now wait till the copy is finished for the sent message
+ do_test_pending();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+ do_test_finished();
+ },
+};
+
+/* exported OnStopCopy */
+// for head_compose.js
+function OnStopCopy(aStatus) {
+ do_test_finished();
+
+ try {
+ Assert.equal(aStatus, 0);
+
+ // Check this is false before we start sending
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ let folder = msgSendLater.getUnsentMessagesFolder(identity);
+
+ // Check we have a message in the unsent message folder
+ Assert.equal(folder.getTotalMessages(false), 1);
+
+ // Now do a comparison of what is in the sent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ folder,
+ mailTestUtils.firstMsgHdr(folder)
+ );
+
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("From:");
+ Assert.notEqual(pos, -1);
+
+ msgData = msgData.substr(pos);
+
+ // Check the data is matching.
+ Assert.equal(originalData, msgData);
+
+ do_test_pending();
+ sendMessageLater();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ finished = true;
+ }
+}
+
+// This function does the actual send later
+function sendMessageLater() {
+ do_test_finished();
+
+ // Set up the SMTP server.
+ server = setupServerDaemon();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ smtpServer.port = server.port;
+
+ // A test to check that we are sending files correctly, including checking
+ // what the server receives and what we output.
+ test = "sendMessageLater";
+
+ var messageListener = new msll();
+
+ msgSendLater.addListener(messageListener);
+
+ // Send the unsent message
+ msgSendLater.sendUnsentMessages(identity);
+
+ server.performTest();
+
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+
+ do_test_pending();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function run_the_test() {
+ // Test file - for bug 429891
+ originalData = await IOUtils.readUTF8(testFile.path);
+
+ // Ensure we have a local mail account, an normal account and appropriate
+ // servers and identities.
+ localAccountUtils.loadLocalMailAccount();
+
+ MailServices.accounts.setSpecialFolders();
+
+ let account = MailServices.accounts.createAccount();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ smtpServer = getBasicSmtpServer(0);
+ identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ account.incomingServer = incomingServer;
+ MailServices.accounts.defaultAccount = account;
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ identity.doFcc = false;
+
+ // Now prepare to actually "send" the message later, i.e. dump it in the
+ // unsent messages folder.
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // Setting the compFields sender and recipient to any value is required to
+ // survive mime_sanity_check_fields in nsMsgCompUtils.cpp.
+ // Sender and recipient are required for sendMessageFile but SMTP
+ // transaction values will be used directly from mail body.
+ compFields.from = "irrelevant@foo.invalid";
+ compFields.to = "irrelevant@foo.invalid";
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ null,
+ copyListener,
+ null,
+ null
+ );
+
+ // Now we wait till we get copy notification of completion.
+ do_test_pending();
+});
diff --git a/comm/mailnews/compose/test/unit/test_createAndSendMessage.js b/comm/mailnews/compose/test/unit/test_createAndSendMessage.js
new file mode 100644
index 0000000000..41daecf2cc
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_createAndSendMessage.js
@@ -0,0 +1,170 @@
+/* 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/. */
+
+/**
+ * Test createAndSendMessage creates a mail file when not using the editor.
+ */
+
+var server;
+var sentFolder;
+const originalData = "createAndSendMessage utf-8 test åäöÅÄÖ";
+// This is the originalData converted to a byte string.
+const expectedData = "createAndSendMessage utf-8 test åäöÃ\x85Ã\x84Ã\x96";
+const expectedContentTypeHeaders =
+ "Content-Type: text/plain; charset=UTF-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\n\r\n";
+var finished = false;
+
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+
+function checkData(msgData) {
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("Content-Type:");
+ Assert.notEqual(pos, -1);
+
+ msgData = msgData.substr(pos);
+
+ Assert.equal(msgData, expectedContentTypeHeaders + expectedData + "\r\n");
+}
+
+function MessageListener() {}
+
+MessageListener.prototype = {
+ // nsIMsgSendListener
+ onStartSending(aMsgID, aMsgSize) {},
+ onProgress(aMsgID, aProgress, aProgressMax) {},
+ onStatus(aMsgID, aMsg) {},
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {
+ try {
+ Assert.equal(aStatus, 0);
+
+ // Compare data file to what the server received
+ checkData(server._daemon.post);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(false);
+ }
+ }
+ },
+ onGetDraftFolderURI(aMsgID, aFolderURI) {},
+ onSendNotPerformed(aMsgID, aStatus) {},
+ onTransportSecurityError(msgID, status, secInfo, location) {},
+
+ // nsIMsgCopyServiceListener
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ GetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ Assert.equal(aStatus, 0);
+ try {
+ // Now do a comparison of what is in the sent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ sentFolder,
+ mailTestUtils.firstMsgHdr(sentFolder)
+ );
+
+ checkData(msgData);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ finished = true;
+ do_test_finished();
+ }
+ },
+
+ // QueryInterface
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgSendListener",
+ "nsIMsgCopyServiceListener",
+ ]),
+};
+
+/**
+ * Call createAndSendMessage, expect onStopSending to be called.
+ */
+add_task(async function testCreateAndSendMessage() {
+ server = setupServerDaemon();
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ MailServices.accounts.setSpecialFolders();
+
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kSender, smtpServer);
+
+ sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ Assert.equal(identity.doFcc, true);
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // A test to check that we are sending files correctly, including checking
+ // what the server receives and what we output.
+ test = "sendMessageFile";
+
+ // Msg Comp Fields
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ compFields.from = identity.email;
+ compFields.to = kTo;
+
+ var messageListener = new MessageListener();
+
+ msgSend.createAndSendMessage(
+ null,
+ identity,
+ "",
+ compFields,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ null,
+ "text/plain",
+ // The following parameter is the message body, test that utf-8 is handled
+ // correctly.
+ originalData,
+ null,
+ null,
+ messageListener,
+ null,
+ null,
+ Ci.nsIMsgCompType.New
+ );
+
+ server.performTest();
+
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+
+ do_test_pending();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_createRFC822Message.js b/comm/mailnews/compose/test/unit/test_createRFC822Message.js
new file mode 100644
index 0000000000..9502031484
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_createRFC822Message.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+/**
+ * Test createRFC822Message creates a mail file.
+ */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+let customSendListener = {
+ ...copyListener,
+ OnStopCopy() {},
+
+ /**
+ * Test a mail file is created and has correct content.
+ */
+ async onStopSending(msgId, status, msg, returnFile) {
+ ok(returnFile.exists(), "createRFC822Message should create a mail file");
+ let content = await IOUtils.read(returnFile.path);
+ content = String.fromCharCode(...content);
+ ok(
+ content.includes("Subject: Test createRFC822Message\r\n"),
+ "Mail file should contain correct subject line"
+ );
+ ok(
+ content.includes(
+ "createRFC822Message is used by nsImportService \xe4\xe9"
+ ),
+ "Mail file should contain correct body"
+ );
+ do_test_finished();
+ },
+};
+
+/**
+ * Call createRFC822Message, expect onStopSending to be called.
+ */
+add_task(async function testCreateRFC822Message() {
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.from = "Somebody <somebody@tinderbox.invalid>";
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test createRFC822Message";
+
+ let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+ msgSend.createRFC822Message(
+ identity,
+ fields,
+ "text/plain",
+ // The following parameter is the message body that can contain arbitrary
+ // binary data, let's try some windows-1252 data (äé).
+ "createRFC822Message is used by nsImportService \xe4\xe9",
+ true, // isDraft
+ [],
+ [],
+ customSendListener
+ );
+ do_test_pending();
+});
diff --git a/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js b/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js
new file mode 100644
index 0000000000..27e879d018
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for auto-detecting attachment file charset.
+ */
+
+function checkAttachmentCharset(expectedCharset) {
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ let attachmentData = getAttachmentFromContent(msgData);
+
+ Assert.equal(expectedCharset, getContentCharset(attachmentData));
+}
+
+function getContentCharset(aContent) {
+ let found = aContent.match(/^Content-Type: text\/plain; charset=(.*?);/);
+ if (found) {
+ Assert.equal(found.length, 2);
+ return found[1];
+ }
+ return null;
+}
+
+async function testUTF8() {
+ await createMessage(do_get_file("data/test-UTF-8.txt"));
+ checkAttachmentCharset("UTF-8");
+}
+
+async function testUTF16BE() {
+ await createMessage(do_get_file("data/test-UTF-16BE.txt"));
+ checkAttachmentCharset("UTF-16BE");
+}
+
+async function testUTF16LE() {
+ await createMessage(do_get_file("data/test-UTF-16LE.txt"));
+ checkAttachmentCharset("UTF-16LE");
+}
+
+async function testShiftJIS() {
+ await createMessage(do_get_file("data/test-SHIFT_JIS.txt"));
+ checkAttachmentCharset("Shift_JIS");
+}
+
+async function testISO2022JP() {
+ await createMessage(do_get_file("data/test-ISO-2022-JP.txt"));
+ checkAttachmentCharset("ISO-2022-JP");
+}
+
+async function testKOI8R() {
+ // NOTE: KOI8-R is detected as KOI8-U which is a superset covering both
+ // Russian and Ukrainian (a few box-drawing characters are repurposed).
+ await createMessage(do_get_file("data/test-KOI8-R.txt"));
+ checkAttachmentCharset("KOI8-U");
+}
+
+async function testWindows1252() {
+ await createMessage(do_get_file("data/test-windows-1252.txt"));
+ checkAttachmentCharset("windows-1252");
+}
+
+var tests = [
+ testUTF8,
+ testUTF16BE,
+ testUTF16LE,
+ testShiftJIS,
+ testISO2022JP,
+ testKOI8R,
+ testWindows1252,
+];
+
+function run_test() {
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ Services.prefs.setIntPref("mail.strictly_mime.parm_folding", 0);
+
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/compose/test/unit/test_expandMailingLists.js b/comm/mailnews/compose/test/unit/test_expandMailingLists.js
new file mode 100644
index 0000000000..aa5998196f
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_expandMailingLists.js
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/**
+ * Tests nsMsgCompose expandMailingLists.
+ */
+
+var MsgComposeContractID = "@mozilla.org/messengercompose/compose;1";
+var MsgComposeParamsContractID =
+ "@mozilla.org/messengercompose/composeparams;1";
+var MsgComposeFieldsContractID =
+ "@mozilla.org/messengercompose/composefields;1";
+var nsIMsgCompose = Ci.nsIMsgCompose;
+var nsIMsgComposeParams = Ci.nsIMsgComposeParams;
+var nsIMsgCompFields = Ci.nsIMsgCompFields;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Helper to check population worked as expected.
+ *
+ * @param {string} aTo - Text in the To field.
+ * @param {string} aCheckTo - The expected To addresses (after possible list population).
+ */
+function checkPopulate(aTo, aCheckTo) {
+ let msgCompose = Cc[MsgComposeContractID].createInstance(nsIMsgCompose);
+
+ // Set up some basic fields for compose.
+ let fields = Cc[MsgComposeFieldsContractID].createInstance(nsIMsgCompFields);
+
+ fields.to = aTo;
+
+ // Set up some params
+ let params =
+ Cc[MsgComposeParamsContractID].createInstance(nsIMsgComposeParams);
+
+ params.composeFields = fields;
+
+ msgCompose.initialize(params);
+
+ msgCompose.expandMailingLists();
+ equal(fields.to, aCheckTo);
+}
+
+function run_test() {
+ loadABFile("data/listexpansion", kPABData.fileName);
+
+ // XXX Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ // Test expansion of list with no description.
+ checkPopulate(
+ "simpson <simpson>",
+ 'Simpson <homer@example.com>, Marge <marge@example.com>, Bart <bart@foobar.invalid>, "lisa@example.com" <lisa@example.com>'
+ );
+
+ // Test expansion fo list with description.
+ checkPopulate(
+ "marge <marges own list>",
+ "Simpson <homer@example.com>, Marge <marge@example.com>"
+ );
+
+ // Special tests for bug 1287726: Lists in list. This is what the data looks like:
+ // 1) family (list) = parents (list) + kids (list).
+ // 2) parents (list) = homer + marge + parents (list recursion).
+ // 3) kids (list) = older-kids (list) + maggie.
+ // 4) older-kids (list) = bart + lisa.
+ // 5) bad-kids (list) = older-kids + bad-younger-kids (list).
+ // 6) bad-younger-kids (list) = maggie + bad-kids (list recursion).
+ checkPopulate(
+ "family <family>",
+ "Simpson <homer@example.com>, Marge <marge@example.com>, " +
+ '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, Maggie <maggie@example.com>'
+ );
+ checkPopulate(
+ "parents <parents>",
+ "Simpson <homer@example.com>, Marge <marge@example.com>"
+ );
+ checkPopulate(
+ "kids <kids>",
+ '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, ' +
+ "Maggie <maggie@example.com>"
+ );
+ checkPopulate(
+ "older-kids <older-kids>",
+ '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>'
+ );
+ checkPopulate(
+ "bad-kids <bad-kids>",
+ '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, ' +
+ "Maggie <maggie@example.com>"
+ );
+ checkPopulate(
+ "bad-younger-kids <bad-younger-kids>",
+ "Maggie <maggie@example.com>, " +
+ '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>'
+ );
+
+ // Test we don't mistake an email address for a list, with a few variations.
+ checkPopulate("Simpson <homer@example.com>", "Simpson <homer@example.com>");
+ checkPopulate("simpson <homer@example.com>", "simpson <homer@example.com>");
+ checkPopulate(
+ "simpson <homer@not-in-ab.invalid>",
+ "simpson <homer@not-in-ab.invalid>"
+ );
+
+ checkPopulate("Marge <marge@example.com>", "Marge <marge@example.com>");
+ checkPopulate("marge <marge@example.com>", "marge <marge@example.com>");
+ checkPopulate(
+ "marge <marge@not-in-ab.invalid>",
+ "marge <marge@not-in-ab.invalid>"
+ );
+}
diff --git a/comm/mailnews/compose/test/unit/test_fcc2.js b/comm/mailnews/compose/test/unit/test_fcc2.js
new file mode 100644
index 0000000000..e7f8d7aadf
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_fcc2.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+/*
+ * Test that when fcc2 field is set, the mail is copied to the fcc2 folder.
+ */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+let fcc2Folder;
+
+add_setup(async function () {
+ localAccountUtils.loadLocalMailAccount();
+ fcc2Folder = localAccountUtils.rootFolder.createLocalSubfolder("fcc2");
+});
+
+/**
+ * Send a message with the fcc2 field set, then check the message in the fcc2
+ * folder.
+ */
+add_task(async function testFcc2() {
+ let CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+ );
+ let fields = new CompFields();
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test fcc2";
+ fields.fcc2 = fcc2Folder.URI;
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ await richCreateMessage(fields, [], identity);
+
+ // Check the message shows up correctly in the fcc2 folder.
+ let msgData = mailTestUtils.loadMessageToString(
+ fcc2Folder,
+ mailTestUtils.firstMsgHdr(fcc2Folder)
+ );
+ Assert.ok(msgData.includes("Subject: Test fcc2"));
+});
+
+add_task(async function cleanup() {
+ fcc2Folder.deleteSelf(null);
+});
diff --git a/comm/mailnews/compose/test/unit/test_fccReply.js b/comm/mailnews/compose/test/unit/test_fccReply.js
new file mode 100644
index 0000000000..b7e44bce17
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_fccReply.js
@@ -0,0 +1,140 @@
+/* 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/. */
+
+/*
+ * Test that when nsIMsgIdentity.fccReplyFollowsParent is true, the reply mail
+ * is copied to the same folder as the original mail.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+var gServer;
+
+/**
+ * Send a reply to originalMsgURI.
+ */
+async function sendReply(identity, fields, originalMsgURI, compType) {
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+ params.originalMsgURI = originalMsgURI;
+ let msgCompose = MailServices.compose.initCompose(params);
+ msgCompose.type = compType;
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ identity,
+ "",
+ null,
+ progress
+ );
+ return promise;
+}
+
+/**
+ * Load local mail account and start fake SMTP server.
+ */
+add_setup(function () {
+ localAccountUtils.loadLocalMailAccount();
+ gServer = setupServerDaemon();
+ gServer.start();
+ registerCleanupFunction(() => {
+ gServer.stop();
+ });
+});
+
+/**
+ * With fccReplyFollowsParent enabled, send a few replies then check the replies
+ * exists in the Inbox folder.
+ */
+add_task(async function testFccReply() {
+ // Turn on fccReplyFollowsParent.
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer(gServer.port)
+ );
+ identity.fccReplyFollowsParent = true;
+
+ // Copy a test mail into the Inbox.
+ let file = do_get_file("data/message1.eml"); // mail to reply to
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+
+ let CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+ );
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+ let originalMsgURI = msgHdr.folder.getUriForMsg(msgHdr);
+
+ // Test nsIMsgCompFields.Reply.
+ let fields = new CompFields();
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test fcc reply";
+ await sendReply(identity, fields, originalMsgURI, Ci.nsIMsgCompType.Reply);
+ await TestUtils.waitForCondition(() => gServer._daemon.post);
+ let msgData = mailTestUtils.loadMessageToString(
+ localAccountUtils.inboxFolder,
+ mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 1)
+ );
+ Assert.ok(msgData.includes("Subject: Test fcc reply"));
+
+ // Test nsIMsgCompFields.ReplyToGroup.
+ gServer.resetTest();
+ fields.subject = "Test fccReplyToGroup";
+ await sendReply(
+ identity,
+ fields,
+ originalMsgURI,
+ Ci.nsIMsgCompType.ReplyToGroup
+ );
+ await TestUtils.waitForCondition(() => gServer._daemon.post);
+ msgData = mailTestUtils.loadMessageToString(
+ localAccountUtils.inboxFolder,
+ mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 2)
+ );
+ Assert.ok(msgData.includes("Subject: Test fccReplyToGroup"));
+
+ // Test nsIMsgCompFields.ReplyToList.
+ gServer.resetTest();
+ fields.subject = "Test fccReplyToList";
+ await sendReply(
+ identity,
+ fields,
+ originalMsgURI,
+ Ci.nsIMsgCompType.ReplyToList
+ );
+ await TestUtils.waitForCondition(() => gServer._daemon.post);
+ msgData = mailTestUtils.loadMessageToString(
+ localAccountUtils.inboxFolder,
+ mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 3)
+ );
+ Assert.ok(msgData.includes("Subject: Test fccReplyToList"));
+});
diff --git a/comm/mailnews/compose/test/unit/test_longLines.js b/comm/mailnews/compose/test/unit/test_longLines.js
new file mode 100644
index 0000000000..cd75e75d38
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_longLines.js
@@ -0,0 +1,232 @@
+/*
+ * Test ensuring that messages with "long lines" are transmitted correctly.
+ * Most of this test was copied from test_messageHeaders.js.
+ */
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+var CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+);
+
+// Copied from jsmime.js.
+function stringToTypedArray(buffer) {
+ var typedarray = new Uint8Array(buffer.length);
+ for (var i = 0; i < buffer.length; i++) {
+ typedarray[i] = buffer.charCodeAt(i);
+ }
+ return typedarray;
+}
+
+function checkDraftHeadersAndBody(
+ expectedHeaders,
+ expectedBody,
+ charset = "UTF-8"
+) {
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ checkMessageHeaders(msgData, expectedHeaders);
+
+ // Get the message body, decode from base64 and check.
+ let endOfHeaders = msgData.indexOf("\r\n\r\n");
+ let body = msgData.slice(endOfHeaders + 4);
+ let endOfBody = body.indexOf("\r\n\r\n");
+
+ if (endOfBody > 0) {
+ body = body.slice(0, endOfBody);
+ } else {
+ body = body.slice(0, body.length);
+ }
+
+ // Remove line breaks and decode from base64 if required.
+ if (expectedHeaders["Content-Transfer-Encoding"] == "base64") {
+ body = atob(body.replace(/\r\n/g, ""));
+ }
+
+ if (charset == "UTF-8") {
+ let expectedBinary = String.fromCharCode.apply(
+ undefined,
+ new TextEncoder("UTF-8").encode(expectedBody)
+ );
+ Assert.equal(body, expectedBinary);
+ } else {
+ let strView = stringToTypedArray(body);
+ let decodedBody = new TextDecoder(charset).decode(strView);
+ Assert.equal(decodedBody, expectedBody);
+ }
+}
+
+function checkMessageHeaders(msgData, expectedHeaders, partNum = "") {
+ let seen = false;
+ let handler = {
+ startPart(part, headers) {
+ if (part != partNum) {
+ return;
+ }
+ seen = true;
+ for (let header in expectedHeaders) {
+ let expected = expectedHeaders[header];
+ if (expected === undefined) {
+ Assert.ok(!headers.has(header));
+ } else {
+ let value = headers.getRawHeader(header);
+ Assert.equal(value.length, 1);
+ value[0] = value[0].replace(/boundary=[^;]*(;|$)/, "boundary=.");
+ Assert.equal(value[0], expected);
+ }
+ }
+ },
+ };
+ MimeParser.parseSync(msgData, handler, {
+ onerror(e) {
+ throw e;
+ },
+ });
+ Assert.ok(seen);
+}
+
+// Create a line with 600 letters 'a' with acute accent, encoded as
+// two bytes c3a1 in UTF-8.
+let longMultibyteLine = "\u00E1".repeat(600);
+
+// And here a line with a Korean character, encoded as three bytes
+// ec9588 in UTF-8.
+let longMultibyteLineCJK = "안".repeat(400);
+
+// And some Japanese.
+let longMultibyteLineJapanese = "語".repeat(450);
+
+async function testBodyWithLongLine() {
+ // Lines in the message body are split by CRLF according to RFC 5322, should
+ // be independent of the system.
+ let newline = "\r\n";
+
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ identity.fullName = "Me";
+ identity.organization = "World Destruction Committee";
+ fields.from = "Nobody <nobody@tinderbox.invalid>";
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Message with 1200 byte line in body";
+ let htmlMessage =
+ "<html><head>" +
+ '<meta http-equiv="content-type" content="text/html; charset=utf-8">' +
+ "</head><body>" +
+ longMultibyteLine +
+ "</body></html>\r\n\r\n";
+ fields.body = htmlMessage;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "base64",
+ },
+ htmlMessage
+ );
+
+ // Again, but this time as plain text.
+ fields.body = htmlMessage;
+ fields.forcePlainText = true;
+ fields.useMultipartAlternative = false;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "base64",
+ },
+ longMultibyteLine + " " + newline + newline // Expected body: The message without the tags.
+ );
+
+ // Now CJK.
+ fields.forcePlainText = false;
+ htmlMessage =
+ "<html><head>" +
+ '<meta http-equiv="content-type" content="text/html; charset=utf-8">' +
+ "</head><body>" +
+ longMultibyteLineCJK +
+ "</body></html>\r\n\r\n";
+ fields.body = htmlMessage;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "base64",
+ },
+ htmlMessage
+ );
+
+ // Again, but this time as plain text.
+ fields.body = htmlMessage;
+ fields.forcePlainText = true;
+ fields.useMultipartAlternative = false;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "base64",
+ },
+ longMultibyteLineCJK + " " + newline + newline // Expected body: The message without the tags.
+ );
+
+ // Now a test for ISO-2022-JP.
+ fields.forcePlainText = false;
+ htmlMessage =
+ "<html><head>" +
+ '<meta http-equiv="content-type" content="text/html; charset=ISO-2022-JP">' +
+ "</head><body>" +
+ longMultibyteLineJapanese +
+ "</body></html>\r\n\r\n";
+ fields.body = htmlMessage;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "base64",
+ },
+ htmlMessage
+ );
+
+ // Again, but this time as plain text.
+ fields.body = htmlMessage;
+ fields.forcePlainText = true;
+ fields.useMultipartAlternative = false;
+ await richCreateMessage(fields, [], identity);
+
+ let expectedBody = longMultibyteLineJapanese + " " + newline + newline;
+
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "base64",
+ },
+ expectedBody
+ );
+
+ // Again, but this time not flowed.
+ fields.body = htmlMessage;
+ Services.prefs.setBoolPref("mailnews.send_plaintext_flowed", false);
+
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeadersAndBody(
+ {
+ "Content-Type": "text/plain; charset=UTF-8",
+ "Content-Transfer-Encoding": "base64",
+ },
+ expectedBody.replace(/ /g, "") // No spaces expected this time.
+ );
+}
+
+var tests = [testBodyWithLongLine];
+
+function run_test() {
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/compose/test/unit/test_mailTelemetry.js b/comm/mailnews/compose/test/unit/test_mailTelemetry.js
new file mode 100644
index 0000000000..28959d508f
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_mailTelemetry.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test telemetry related to mails sent.
+ */
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+let server;
+
+let kIdentityMail = "identity@foo.invalid";
+let kSender = "from@foo.invalid";
+let kTo = "to@foo.invalid";
+
+const NUM_MAILS = 3;
+
+let deliveryListener = {
+ count: 0,
+ OnStartRunningUrl() {},
+ OnStopRunningUrl() {
+ if (++this.count == NUM_MAILS) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars["tb.mails.sent"],
+ NUM_MAILS,
+ "Count of mails sent must be correct."
+ );
+ }
+ },
+};
+
+/**
+ * Check that we're counting mails sent.
+ */
+add_task(async function test_mails_sent() {
+ Services.telemetry.clearScalars();
+
+ server = setupServerDaemon();
+ registerCleanupFunction(() => {
+ server.stop();
+ });
+
+ // Test file
+ let testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ let smtpServer = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ for (let i = 0; i < NUM_MAILS; i++) {
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ deliveryListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+ }
+ } catch (e) {
+ do_throw(e);
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_mailtoURL.js b/comm/mailnews/compose/test/unit/test_mailtoURL.js
new file mode 100644
index 0000000000..a793c99974
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_mailtoURL.js
@@ -0,0 +1,810 @@
+/*
+ * Test suite for mailto: URLs
+ */
+
+var COMPOSE_HTML = Ci.nsIMsgCompFormat.HTML;
+var COMPOSE_DEFAULT = Ci.nsIMsgCompFormat.Default;
+
+function run_test() {
+ function test(aTest) {
+ var uri = Services.io.newURI(aTest.url);
+ uri = uri.QueryInterface(Ci.nsIMailtoUrl);
+
+ var to = {},
+ cc = {},
+ bcc = {},
+ subject = {},
+ body = {},
+ html = {},
+ reference = {},
+ newsgroup = {},
+ composeformat = {};
+ uri.getMessageContents(
+ to,
+ cc,
+ bcc,
+ subject,
+ body,
+ html,
+ reference,
+ newsgroup,
+ composeformat
+ );
+ Assert.equal(aTest.to, to.value);
+ Assert.equal(aTest.cc, cc.value);
+ Assert.equal(aTest.bcc, bcc.value);
+ Assert.equal(aTest.subject, subject.value);
+ Assert.equal(aTest.body, body.value);
+ Assert.equal(aTest.html, html.value);
+ Assert.equal(aTest.reference, reference.value);
+ Assert.equal(aTest.newsgroup, newsgroup.value);
+ Assert.equal(aTest.composeformat, composeformat.value);
+ Assert.equal(aTest.from, uri.fromPart);
+ Assert.equal(aTest.followupto, uri.followUpToPart);
+ Assert.equal(aTest.organization, uri.organizationPart);
+ Assert.equal(aTest.replyto, uri.replyToPart);
+ Assert.equal(aTest.priority, uri.priorityPart);
+ Assert.equal(aTest.newshost, uri.newsHostPart);
+ Assert.ok(uri.equals(uri));
+ }
+
+ for (var i = 0; i < tests.length; i++) {
+ test(tests[i]);
+ }
+
+ // Test cloning reparses the url by checking the to field.
+ let uri = Services.io.newURI(tests[0].url).QueryInterface(Ci.nsIMailtoUrl);
+ var to = {},
+ cc = {},
+ bcc = {},
+ subject = {},
+ body = {},
+ html = {},
+ reference = {},
+ newsgroup = {},
+ composeformat = {};
+ uri.getMessageContents(
+ to,
+ cc,
+ bcc,
+ subject,
+ body,
+ html,
+ reference,
+ newsgroup,
+ composeformat
+ );
+ Assert.equal(to.value, tests[0].to);
+}
+
+var tests = [
+ {
+ url: "mailto:one@example.com",
+ to: "one@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:two@example.com?",
+ to: "two@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ /* the heirarchical-part address shouldn't be mime-decoded */
+ {
+ url: "mailto:%3D%3FUTF-8%3FQ%3Fthree%3F%3D@example.com",
+ to: "=?UTF-8?Q?three?=@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ /* a to=address should be mime-decoded */
+ {
+ url: "mailto:?to=%3D%3FUTF-8%3FQ%3Ffour%3F%3D@example.com",
+ to: "four@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:fivea@example.com?to=%3D%3FUTF-8%3FQ%3Ffiveb%3F%3D@example.com",
+ to: "fivea@example.com, fiveb@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:sixa@example.com?to=sixb@example.com&to=sixc@example.com",
+ to: "sixa@example.com, sixb@example.com, sixc@example.com",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?cc=seven@example.com",
+ to: "",
+ cc: "seven@example.com",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?cc=%3D%3FUTF-8%3FQ%3Feight%3F%3D@example.com",
+ to: "",
+ cc: "eight@example.com",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?bcc=nine@example.com",
+ to: "",
+ cc: "",
+ bcc: "nine@example.com",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?bcc=%3D%3FUTF-8%3FQ%3Ften%3F%3D@example.com",
+ to: "",
+ cc: "",
+ bcc: "ten@example.com",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?subject=foo",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "foo",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?subject=%62%61%72",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "bar",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?subject=%3D%3Futf-8%3FQ%3F%3DC2%3DA1encoded_subject%21%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "¡encoded subject!",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?body=one%20body",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "one body",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?body=two%20bodies&body=two%20lines",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "two bodies\ntwo lines",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?html-part=html%20part",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "html part",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_HTML,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?html-body=html%20body",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "html body",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_HTML,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?html-part=html%20part&html-body=html-body%20trumps%20earlier%20html-part",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "html-body trumps earlier html-part",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_HTML,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?references=%3Cref1%40example.com%3E",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "<ref1@example.com>",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?in-reply-to=%3Crepl1%40example.com%3E",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "<repl1@example.com>",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url:
+ "mailto:?references=%3Cref2%40example.com%3E" +
+ "&in-reply-to=%3Crepl2%40example.com%3E",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "<ref2@example.com> <repl2@example.com>",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url:
+ "mailto:?references=%3Cref3%40example.com%3E%20%3Crepl3%40example.com%3E" +
+ "&in-reply-to=%3Crepl3%40example.com%3E",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "<ref3@example.com> <repl3@example.com>",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?newsgroups=mozilla.dev.apps.thunderbird",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "mozilla.dev.apps.thunderbird",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?newsgroups=%3D%3FUTF-8%3FQ%3Fmozilla.test.multimedia%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "mozilla.test.multimedia",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?from=notlikely@example.com",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "notlikely@example.com",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?from=%3D%3FUTF-8%3FQ%3Fme@example.com%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "me@example.com",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?followup-to=mozilla.dev.planning",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "mozilla.dev.planning",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?followup-to=%3D%3FUTF-8%3FQ%3Fmozilla.test%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "mozilla.test",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?organization=very%20little",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "very little",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?organization=%3D%3FUTF-8%3FQ%3Fmicroscopic%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "microscopic",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?reply-to=notme@example.com",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "notme@example.com",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?reply-to=%3D%3FUTF-8%3FB%3Fw4VrZQ%3D%3D%3F%3D%20%3Cake@example.org%3E",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "Ã…ke <ake@example.org>",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?priority=1%20(People%20Are%20Dying!!1!)",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "1 (People Are Dying!!1!)",
+ newshost: "",
+ },
+ {
+ url: "mailto:?priority=%3D%3FUTF-8%3FQ%3F4%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "4",
+ newshost: "",
+ },
+ {
+ url: "mailto:?newshost=news.mozilla.org",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "news.mozilla.org",
+ },
+ {
+ url: "mailto:?newshost=%3D%3FUTF-8%3FQ%3Fnews.example.org%3F%3D",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "news.example.org",
+ },
+ {
+ url: "mailto:?%74%4F=to&%73%55%62%4A%65%43%74=subject&%62%4F%64%59=body&%63%43=cc&%62%43%63=bcc",
+ to: "to",
+ cc: "cc",
+ bcc: "bcc",
+ subject: "subject",
+ body: "body",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:to1?%74%4F=to2&to=to3&subject=&%73%55%62%4A%65%43%74=subject&%62%4F%64%59=line1&body=line2&%63%43=cc1&cc=cc2&%62%43%63=bcc1&bcc=bcc2",
+ to: "to1, to2, to3",
+ cc: "cc1, cc2",
+ bcc: "bcc1, bcc2",
+ subject: "subject",
+ body: "line1\nline2",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url: "mailto:?nto=1&nsubject=2&nbody=3&ncc=4&nbcc=5",
+ to: "",
+ cc: "",
+ bcc: "",
+ subject: "",
+ body: "",
+ html: "",
+ reference: "",
+ newsgroup: "",
+ composeformat: COMPOSE_DEFAULT,
+ from: "",
+ followupto: "",
+ organization: "",
+ replyto: "",
+ priority: "",
+ newshost: "",
+ },
+ {
+ url:
+ "mailto:%CE%B1?cc=%CE%B2&bcc=%CE%B3&subject=%CE%B4&body=%CE%B5" +
+ "&html-body=%CE%BE&newsgroups=%CE%B6&from=%CE%B7&followup-to=%CE%B8" +
+ "&organization=%CE%B9&reply-to=%CE%BA&priority=%CE%BB&newshost=%CE%BC",
+ to: "α",
+ cc: "β",
+ bcc: "γ",
+ subject: "δ",
+ body: "ε",
+ html: "ξ",
+ reference: "", // we expect this field to be ASCII-only
+ newsgroup: "ζ",
+ composeformat: COMPOSE_HTML,
+ from: "η",
+ followupto: "θ",
+ organization: "ι",
+ replyto: "κ",
+ priority: "λ",
+ newshost: "μ",
+ },
+];
diff --git a/comm/mailnews/compose/test/unit/test_messageBody.js b/comm/mailnews/compose/test/unit/test_messageBody.js
new file mode 100644
index 0000000000..14c44b59f8
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_messageBody.js
@@ -0,0 +1,206 @@
+/* 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/. */
+
+/**
+ * Test suite for message body.
+ */
+
+localAccountUtils.loadLocalMailAccount();
+
+/**
+ * Test trailing whitespace is QP encoded.
+ */
+add_task(async function testQP() {
+ // Together with fields.forceMsgEncoding, force quote-printable encoding.
+ Services.prefs.setBoolPref("mail.strictly_mime", true);
+
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ let CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+ );
+
+ // Test QP works for ascii text.
+
+ let fields = new CompFields();
+ fields.forceMsgEncoding = true;
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test QP encoding for trailing whitespace";
+ fields.body = "A line with trailing whitespace\t ";
+ await richCreateMessage(fields, [], identity);
+
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.ok(
+ msgData.includes("A line with trailing whitespace\t=20"),
+ "QP for ascii should work"
+ );
+
+ // Test QP works for non-ascii text.
+
+ fields = new CompFields();
+ fields.forceMsgEncoding = true;
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test QP encoding for non-ascii and trailing tab";
+ fields.body = "記: base64 is used if unprintable > 10% \t";
+ await richCreateMessage(fields, [], identity);
+
+ msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.ok(
+ msgData.includes("=E8=A8=98: base64 is used if unprintable > 10% =09"),
+ "QP for non-ascii should work"
+ );
+
+ // Test leading space is preserved.
+
+ fields = new CompFields();
+ fields.forceMsgEncoding = true;
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Leading space is valid in a quoted printable message";
+ fields.body = "123456789" + " 123456789".repeat(6) + "1234 56789";
+ await richCreateMessage(fields, [], identity);
+
+ msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ let endOfHeaders = msgData.indexOf("\r\n\r\n");
+ let body = msgData.slice(endOfHeaders + 4);
+
+ Assert.equal(
+ body.trimRight("\r\n"),
+ "123456789 123456789 123456789 123456789 123456789 123456789 1234567891234=\r\n 56789"
+ );
+
+ Services.prefs.clearUserPref("mail.strictly_mime");
+});
+
+/**
+ * Test QP is not used together with format=flowed.
+ */
+add_task(async function testNoQPWithFormatFlowed() {
+ // Together with fields.forceMsgEncoding, force quote-printable encoding.
+ Services.prefs.setBoolPref("mail.strictly_mime", true);
+
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.forceMsgEncoding = true;
+ fields.forcePlainText = true;
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test QP encoding for trailing whitespace";
+ fields.body = "A line with trailing whitespace\t ";
+ await richCreateMessage(fields, [], identity);
+
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.ok(
+ msgData.includes(
+ "Content-Type: text/plain; charset=UTF-8; format=flowed\r\nContent-Transfer-Encoding: base64"
+ ),
+ "format=flowed should be used"
+ );
+ Assert.ok(
+ !msgData.includes("quoted-printable"),
+ "quoted-printable should not be used"
+ );
+
+ Services.prefs.clearUserPref("mail.strictly_mime");
+});
+
+/**
+ * Test plain text body is wrapped correctly with different mailnews.wraplength
+ * pref value.
+ */
+add_task(async function testWrapLength() {
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ let CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+ );
+
+ let word = "abcd ";
+ let body = word.repeat(20);
+
+ let fields = new CompFields();
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test text wrapping";
+ fields.body = `<html><body>${body}</body></html>`;
+ fields.forcePlainText = true;
+ await richCreateMessage(fields, [], identity);
+
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.equal(
+ getMessageBody(msgData),
+ // Default wrap length is 72.
+ word.repeat(14) + "\r\n" + word.repeat(6).trim(),
+ "Text wraps at 72 by default"
+ );
+
+ // 0 means no wrap.
+ Services.prefs.setIntPref("mailnews.wraplength", 0);
+
+ await richCreateMessage(fields, [], identity);
+
+ msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.equal(
+ getMessageBody(msgData),
+ body.trim(),
+ "Should not wrap when wraplength is 0"
+ );
+
+ Services.prefs.clearUserPref("mailnews.wraplength");
+});
+
+/**
+ * Test handling of trailing NBSP.
+ */
+add_task(async function testNBSP() {
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.subject = "Test text wrapping";
+ // The character after `test` is NBSP.
+ fields.body = "<html><body>åäö test <br></body></html>";
+ fields.forcePlainText = true;
+ await richCreateMessage(fields, [], identity);
+
+ let msgData = mailTestUtils.loadMessageToUTF16String(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ Assert.equal(
+ getMessageBody(msgData),
+ "åäö test",
+ "Trailing NBSP should be removed"
+ );
+});
diff --git a/comm/mailnews/compose/test/unit/test_messageHeaders.js b/comm/mailnews/compose/test/unit/test_messageHeaders.js
new file mode 100644
index 0000000000..58765e219f
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_messageHeaders.js
@@ -0,0 +1,812 @@
+/*
+ * Test suite for ensuring that the headers of messages are set properly.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+var CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+);
+
+function makeAttachment(opts = {}) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ for (let key in opts) {
+ attachment[key] = opts[key];
+ }
+ return attachment;
+}
+
+function sendMessage(fieldParams, identity, opts = {}, attachments = []) {
+ // Initialize compose fields
+ let fields = new CompFields();
+ for (let key in fieldParams) {
+ fields[key] = fieldParams[key];
+ }
+ for (let attachment of attachments) {
+ fields.addAttachment(attachment);
+ }
+
+ // Initialize compose params
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+ for (let key in opts) {
+ params[key] = opts[key];
+ }
+
+ // Send the message
+ let msgCompose = MailServices.compose.initCompose(params);
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let promise = new Promise((resolve, reject) => {
+ progressListener.resolve = resolve;
+ progressListener.reject = reject;
+ });
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ identity,
+ "",
+ null,
+ progress
+ );
+ return promise;
+}
+
+function checkDraftHeaders(expectedHeaders, partNum = "") {
+ let msgData = mailTestUtils.loadMessageToString(
+ gDraftFolder,
+ mailTestUtils.firstMsgHdr(gDraftFolder)
+ );
+ checkMessageHeaders(msgData, expectedHeaders, partNum);
+}
+
+function checkMessageHeaders(msgData, expectedHeaders, partNum = "") {
+ let seen = false;
+ let handler = {
+ startPart(part, headers) {
+ if (part != partNum) {
+ return;
+ }
+ seen = true;
+ for (let header in expectedHeaders) {
+ let expected = expectedHeaders[header];
+ if (expected === undefined) {
+ Assert.ok(
+ !headers.has(header),
+ `Should not have header named "${header}"`
+ );
+ } else {
+ let value = headers.getRawHeader(header);
+ Assert.equal(
+ value && value.length,
+ 1,
+ `Should have exactly one header named "${header}"`
+ );
+ value[0] = value[0].replace(/boundary=[^;]*(;|$)/, "boundary=.");
+ Assert.equal(value[0], expected);
+ }
+ }
+ },
+ };
+ MimeParser.parseSync(msgData, handler, {
+ onerror(e) {
+ throw e;
+ },
+ });
+ Assert.ok(seen);
+}
+
+async function testEnvelope() {
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ identity.fullName = "Me";
+ identity.organization = "World Destruction Committee";
+ fields.from = "Nobody <nobody@tinderbox.invalid>";
+ fields.to = "Nobody <nobody@tinderbox.invalid>";
+ fields.cc = "Alex <alex@tinderbox.invalid>";
+ fields.bcc = "Boris <boris@tinderbox.invalid>";
+ fields.replyTo = "Charles <charles@tinderbox.invalid>";
+ fields.organization = "World Salvation Committee";
+ fields.subject = "This is an obscure reference";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ // As of bug 87987, the identity does not override the from header.
+ From: "Nobody <nobody@tinderbox.invalid>",
+ // The identity should override the organization field here.
+ Organization: "World Destruction Committee",
+ To: "Nobody <nobody@tinderbox.invalid>",
+ Cc: "Alex <alex@tinderbox.invalid>",
+ Bcc: "Boris <boris@tinderbox.invalid>",
+ "Reply-To": "Charles <charles@tinderbox.invalid>",
+ Subject: "This is an obscure reference",
+ });
+}
+
+async function testI18NEnvelope() {
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ identity.fullName = "ケツァルコアトル";
+ identity.organization = "Comité de la destruction du monde";
+ fields.to = "Émile <nobody@tinderbox.invalid>";
+ fields.cc = "André Chopin <alex@tinderbox.invalid>";
+ fields.bcc = "Étienne <boris@tinderbox.invalid>";
+ fields.replyTo = "Frédéric <charles@tinderbox.invalid>";
+ fields.subject = "Ceci n'est pas un référence obscure";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ From: "=?UTF-8?B?44Kx44OE44Kh44Or44Kz44Ki44OI44Or?= <from@tinderbox.invalid>",
+ Organization: "=?UTF-8?Q?Comit=C3=A9_de_la_destruction_du_monde?=",
+ To: "=?UTF-8?B?w4ltaWxl?= <nobody@tinderbox.invalid>",
+ Cc: "=?UTF-8?Q?Andr=C3=A9_Chopin?= <alex@tinderbox.invalid>",
+ Bcc: "=?UTF-8?Q?=C3=89tienne?= <boris@tinderbox.invalid>",
+ "Reply-To": "=?UTF-8?B?RnLDqWTDqXJpYw==?= <charles@tinderbox.invalid>",
+ Subject: "=?UTF-8?Q?Ceci_n=27est_pas_un_r=C3=A9f=C3=A9rence_obscure?=",
+ });
+}
+
+async function testIDNEnvelope() {
+ let fields = new CompFields();
+ let domain = "ケツァルコアトル.invalid";
+ // We match against rawHeaderText, so we need to encode the string as a binary
+ // string instead of a unicode string.
+ let utf8Domain = String.fromCharCode.apply(
+ undefined,
+ new TextEncoder("UTF-8").encode(domain)
+ );
+ // Bug 1034658: nsIMsgIdentity doesn't like IDN in its email addresses.
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ fields.to = "Nobody <nobody@" + domain + ">";
+ fields.cc = "Alex <alex@" + domain + ">";
+ fields.bcc = "Boris <boris@" + domain + ">";
+ fields.replyTo = "Charles <charles@" + domain + ">";
+ fields.subject = "This is an obscure reference";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ // The identity sets the from field here.
+ From: "from@tinderbox.invalid",
+ To: "Nobody <nobody@" + utf8Domain + ">",
+ Cc: "Alex <alex@" + utf8Domain + ">",
+ Bcc: "Boris <boris@" + utf8Domain + ">",
+ "Reply-To": "Charles <charles@" + utf8Domain + ">",
+ Subject: "This is an obscure reference",
+ });
+}
+
+async function testDraftInfo() {
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ FCC: identity.fccFolder,
+ "X-Identity-Key": identity.key,
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=0; receipt=0; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4",
+ });
+
+ fields.attachVCard = true;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=1; receipt=0; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4",
+ });
+
+ fields.returnReceipt = true;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=1; receipt=1; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4",
+ });
+
+ fields.DSN = true;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=0; deliveryformat=4",
+ });
+
+ fields.attachmentReminder = true;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=1; deliveryformat=4",
+ });
+
+ fields.deliveryFormat = Ci.nsIMsgCompSendFormat.Both;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Mozilla-Draft-Info":
+ "internal/draft; " +
+ "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=1; deliveryformat=3",
+ });
+}
+
+async function testOtherHeadersAgentParam(sendAgent, minimalAgent) {
+ Services.prefs.setBoolPref("mailnews.headers.sendUserAgent", sendAgent);
+ if (sendAgent) {
+ Services.prefs.setBoolPref(
+ "mailnews.headers.useMinimalUserAgent",
+ minimalAgent
+ );
+ }
+
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ fields.priority = "high";
+ fields.references = "<fake@tinderbox.invalid> <more@test.invalid>";
+ fields.setHeader("X-Fake-Header", "124");
+ let before = Date.now();
+ let msgHdr = await richCreateMessage(fields, [], identity);
+ let after = Date.now();
+ let msgData = mailTestUtils.loadMessageToString(msgHdr.folder, msgHdr);
+ let expectedAgent = undefined; // !sendAgent
+ if (sendAgent) {
+ if (minimalAgent) {
+ expectedAgent = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandFullName");
+ } else {
+ expectedAgent = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler).userAgent;
+ }
+ }
+ checkMessageHeaders(msgData, {
+ "Mime-Version": "1.0",
+ "User-Agent": expectedAgent,
+ "X-Priority": "2 (High)",
+ References: "<fake@tinderbox.invalid> <more@test.invalid>",
+ "In-Reply-To": "<more@test.invalid>",
+ "X-Fake-Header": "124",
+ });
+
+ // Check headers with dynamic content
+ let headers = MimeParser.extractHeaders(msgData);
+ Assert.ok(headers.has("Message-Id"));
+ Assert.ok(
+ headers.getRawHeader("Message-Id")[0].endsWith("@tinderbox.invalid>")
+ );
+ // This is a very special crafted check. We don't know when the message was
+ // actually created, but we have bounds on it, from above. From
+ // experimentation, there are a few ways you can create dates that Date.parse
+ // can't handle (specifically related to how 2-digit years). However, the
+ // optimal RFC 5322 form is supported by Date.parse. If Date.parse fails, we
+ // have a form that we shouldn't be using anyways.
+ let date = new Date(headers.getRawHeader("Date")[0]);
+ // If we have clock skew within the test, then our results are going to be
+ // meaningless. Hopefully, this is only rarely the case.
+ if (before > after) {
+ info("Clock skew detected, skipping date check");
+ } else {
+ // In case this all took place within one second, remove sub-millisecond
+ // timing (Date headers only carry second-level precision).
+ before = before - (before % 1000);
+ after = after - (after % 1000);
+ info(before + " <= " + date + " <= " + after + "?");
+ Assert.ok(before <= date && date <= after);
+ }
+
+ // We truncate too-long References. Check this.
+ let references = [];
+ for (let i = 0; i < 100; i++) {
+ references.push("<" + i + "@test.invalid>");
+ }
+ let expected = references.slice(47);
+ expected.unshift(references[0]);
+ fields.references = references.join(" ");
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ References: expected.join(" "),
+ "In-Reply-To": references[references.length - 1],
+ });
+}
+
+/**
+ * Tests that the domain for the Message-Id header defaults to the domain of the
+ * identity's address.
+ */
+async function testMessageIdUseIdentityAddress() {
+ const expectedMessageIdHostname = "tinderbox.test";
+
+ const identity = getSmtpIdentity(
+ `from@${expectedMessageIdHostname}`,
+ getBasicSmtpServer()
+ );
+
+ await createMsgAndCompareMessageId(identity, null, expectedMessageIdHostname);
+}
+
+/**
+ * Tests that if a custom address (with a custom domain) is used when composing a
+ * message, the domain in this address takes precendence over the domain of the
+ * identity's address to generate the value for the Message-Id header.
+ */
+async function testMessageIdUseFromDomain() {
+ const expectedMessageIdHostname = "another-tinderbox.test";
+
+ const identity = getSmtpIdentity("from@tinderbox.test", getBasicSmtpServer());
+
+ // Set the From header to an address that uses a different domain than
+ // the identity.
+ const fields = new CompFields();
+ fields.from = `Nobody <nobody@${expectedMessageIdHostname}>`;
+
+ await createMsgAndCompareMessageId(
+ identity,
+ fields,
+ expectedMessageIdHostname
+ );
+}
+
+/**
+ * Tests that if the identity has a "FQDN" attribute, it takes precedence to use as the
+ * domain for the Message-Id header over any other domain or address.
+ */
+async function testMessageIdUseIdentityAttribute() {
+ const expectedMessageIdHostname = "my-custom-fqdn.test";
+
+ const identity = getSmtpIdentity("from@tinderbox.test", getBasicSmtpServer());
+ identity.setCharAttribute("FQDN", expectedMessageIdHostname);
+
+ // Set the From header to an address that uses a different domain than
+ // the identity.
+ const fields = new CompFields();
+ fields.from = "Nobody <nobody@another-tinderbox.test>";
+
+ await createMsgAndCompareMessageId(
+ identity,
+ fields,
+ expectedMessageIdHostname
+ );
+}
+
+/**
+ * Util function to create a message using the given identity and fields,
+ * and test that the message ID that was generated for it has the correct
+ * host name.
+ *
+ * @param {nsIMsgIdentity} identity - The identity to use to create the message.
+ * @param {?nsIMsgCompFields} fields - The compose fields to use. If not provided,
+ * default fields are used.
+ * @param {string} expectedMessageIdHostname - The expected host name of the
+ * Message-Id header.
+ */
+async function createMsgAndCompareMessageId(
+ identity,
+ fields,
+ expectedMessageIdHostname
+) {
+ if (!fields) {
+ fields = new CompFields();
+ }
+
+ let msgHdr = await richCreateMessage(fields, [], identity);
+ let msgData = mailTestUtils.loadMessageToString(msgHdr.folder, msgHdr);
+ let headers = MimeParser.extractHeaders(msgData);
+
+ // As of bug 1727181, the identity does not override the message-id header.
+ Assert.ok(headers.has("Message-Id"), "the message has a Message-Id header");
+ Assert.ok(
+ headers
+ .getRawHeader("Message-Id")[0]
+ .endsWith(`@${expectedMessageIdHostname}>`),
+ `the hostname for the Message-Id header should be ${expectedMessageIdHostname}`
+ );
+}
+
+async function testOtherHeadersFullAgent() {
+ await testOtherHeadersAgentParam(true, false);
+}
+
+async function testOtherHeadersMinimalAgent() {
+ await testOtherHeadersAgentParam(true, true);
+}
+
+async function testOtherHeadersNoAgent() {
+ await testOtherHeadersAgentParam(false, undefined);
+}
+
+async function testNewsgroups() {
+ let fields = new CompFields();
+ let nntpServer = localAccountUtils.create_incoming_server(
+ "nntp",
+ 534,
+ "",
+ ""
+ );
+ nntpServer
+ .QueryInterface(Ci.nsINntpIncomingServer)
+ .subscribeToNewsgroup("mozilla.test");
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ fields.newsgroups = "mozilla.test, mozilla.test.multimedia";
+ fields.followupTo = "mozilla.test";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ // The identity should override the compose fields here.
+ Newsgroups: "mozilla.test,mozilla.test.multimedia",
+ "Followup-To": "mozilla.test",
+ "X-Mozilla-News-Host": "localhost",
+ });
+}
+
+async function testSendHeaders() {
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ identity.setCharAttribute("headers", "bah,humbug");
+ identity.setCharAttribute(
+ "header.bah",
+ "X-Custom-1: A header value: with a colon"
+ );
+ identity.setUnicharAttribute("header.humbug", "X-Custom-2: Enchanté");
+ identity.setCharAttribute("subscribed_mailing_lists", "list@test.invalid");
+ identity.setCharAttribute(
+ "replyto_mangling_mailing_lists",
+ "replyto@test.invalid"
+ );
+ fields.to = "list@test.invalid";
+ fields.cc = "not-list@test.invalid";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Custom-1": "A header value: with a colon",
+ "X-Custom-2": "=?UTF-8?B?RW5jaGFudMOp?=",
+ "Mail-Followup-To": "list@test.invalid, not-list@test.invalid",
+ "Mail-Reply-To": undefined,
+ });
+
+ // Don't set the M-F-T header if there's no list.
+ fields.to = "replyto@test.invalid";
+ fields.cc = "";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "X-Custom-1": "A header value: with a colon",
+ "X-Custom-2": "=?UTF-8?B?RW5jaGFudMOp?=",
+ "Mail-Reply-To": "from@tinderbox.invalid",
+ "Mail-Followup-To": undefined,
+ });
+}
+
+async function testContentHeaders() {
+ // Disable RFC 2047 fallback
+ Services.prefs.setIntPref("mail.strictly_mime.parm_folding", 2);
+ let fields = new CompFields();
+ fields.body = "A body";
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "7bit",
+ });
+
+ // non-ASCII body should be 8-bit...
+ fields.body = "Archæologist";
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "8bit",
+ });
+
+ // Attachments
+ fields.body = "";
+ let plainAttachment = makeAttachment({
+ url: "data:text/plain,oïl",
+ name: "attachment.txt",
+ });
+ let plainAttachmentHeaders = {
+ "Content-Type": "text/plain; charset=UTF-8",
+ "Content-Transfer-Encoding": "base64",
+ "Content-Disposition": 'attachment; filename="attachment.txt"',
+ };
+ await richCreateMessage(fields, [plainAttachment], identity);
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1"
+ );
+ checkDraftHeaders(plainAttachmentHeaders, "2");
+
+ plainAttachment.name = "oïl.txt";
+ plainAttachmentHeaders["Content-Disposition"] =
+ "attachment; filename*=UTF-8''%6F%C3%AF%6C%2E%74%78%74";
+ await richCreateMessage(fields, [plainAttachment], identity);
+ checkDraftHeaders(plainAttachmentHeaders, "2");
+
+ plainAttachment.name = "\ud83d\udca9.txt";
+ plainAttachmentHeaders["Content-Disposition"] =
+ "attachment; filename*=UTF-8''%F0%9F%92%A9%2E%74%78%74";
+ await richCreateMessage(fields, [plainAttachment], identity);
+ checkDraftHeaders(plainAttachmentHeaders, "2");
+
+ let httpAttachment = makeAttachment({
+ url: "data:text/html,<html></html>",
+ name: "attachment.html",
+ });
+ let httpAttachmentHeaders = {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Disposition": 'attachment; filename="attachment.html"',
+ "Content-Location": "data:text/html,<html></html>",
+ };
+ await richCreateMessage(fields, [httpAttachment], identity);
+ checkDraftHeaders(
+ {
+ "Content-Location": undefined,
+ },
+ "1"
+ );
+ checkDraftHeaders(httpAttachmentHeaders, "2");
+
+ let cloudAttachment = makeAttachment({
+ url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec,
+ sendViaCloud: true,
+ htmlAnnotation:
+ "<html><body>This is an html placeholder file.</body></html>",
+ cloudFileAccountKey: "akey",
+ cloudPartHeaderData: "0123456789ABCDE",
+ name: "attachment.html",
+ contentLocation: "http://localhost.invalid/",
+ });
+ let cloudAttachmentHeaders = {
+ "Content-Type": "text/html; charset=utf-8",
+ "X-Mozilla-Cloud-Part":
+ "cloudFile; " +
+ "url=http://localhost.invalid/; " +
+ "provider=akey; " +
+ 'data="0123456789ABCDE"',
+ };
+ await richCreateMessage(fields, [cloudAttachment], identity);
+ checkDraftHeaders(cloudAttachmentHeaders, "2");
+
+ // Cloud attachment with non-ascii file name.
+ cloudAttachment = makeAttachment({
+ url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec,
+ sendViaCloud: true,
+ htmlAnnotation:
+ "<html><body>This is an html placeholder file.</body></html>",
+ cloudFileAccountKey: "akey",
+ cloudPartHeaderData: "0123456789ABCDE",
+ name: "ファイル.txt",
+ contentLocation: "http://localhost.invalid/",
+ });
+ cloudAttachmentHeaders = {
+ "Content-Type": "text/html; charset=utf-8",
+ "X-Mozilla-Cloud-Part":
+ "cloudFile; " +
+ "url=http://localhost.invalid/; " +
+ "provider=akey; " +
+ 'data="0123456789ABCDE"',
+ };
+ await richCreateMessage(fields, [cloudAttachment], identity);
+ checkDraftHeaders(cloudAttachmentHeaders, "2");
+
+ // Some multipart/alternative tests.
+ fields.body = "Some text";
+ fields.forcePlainText = false;
+ fields.useMultipartAlternative = true;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "Content-Type": "multipart/alternative; boundary=.",
+ });
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1"
+ );
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "2"
+ );
+
+ // multipart/mixed
+ // + multipart/alternative
+ // + text/plain
+ // + text/html
+ // + text/plain attachment
+ await richCreateMessage(fields, [plainAttachment], identity);
+ checkDraftHeaders({
+ "Content-Type": "multipart/mixed; boundary=.",
+ });
+ checkDraftHeaders(
+ {
+ "Content-Type": "multipart/alternative; boundary=.",
+ },
+ "1"
+ );
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1.1"
+ );
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1.2"
+ );
+ checkDraftHeaders(plainAttachmentHeaders, "2");
+
+ // Three attachments, and a multipart/alternative. Oh the humanity!
+ await richCreateMessage(
+ fields,
+ [plainAttachment, httpAttachment, cloudAttachment],
+ identity
+ );
+ checkDraftHeaders({
+ "Content-Type": "multipart/mixed; boundary=.",
+ });
+ checkDraftHeaders(
+ {
+ "Content-Type": "multipart/alternative; boundary=.",
+ },
+ "1"
+ );
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1.1"
+ );
+ checkDraftHeaders(
+ {
+ "Content-Type": "text/html; charset=UTF-8",
+ "Content-Transfer-Encoding": "7bit",
+ },
+ "1.2"
+ );
+ checkDraftHeaders(plainAttachmentHeaders, "2");
+ checkDraftHeaders(httpAttachmentHeaders, "3");
+ checkDraftHeaders(cloudAttachmentHeaders, "4");
+
+ // Test a request for plain text with text/html.
+ fields.forcePlainText = true;
+ fields.useMultipartAlternative = false;
+ await richCreateMessage(fields, [], identity);
+ checkDraftHeaders({
+ "Content-Type": "text/plain; charset=UTF-8; format=flowed",
+ "Content-Transfer-Encoding": "7bit",
+ });
+}
+
+async function testSentMessage() {
+ let server = setupServerDaemon();
+ let daemon = server._daemon;
+ server.start();
+ try {
+ let localserver = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity("test@tinderbox.invalid", localserver);
+ await sendMessage(
+ {
+ to: "Nobody <nobody@tinderbox.invalid>",
+ cc: "Alex <alex@tinderbox.invalid>",
+ bcc: "Boris <boris@tinderbox.invalid>",
+ replyTo: "Charles <charles@tinderbox.invalid>",
+ },
+ identity,
+ {},
+ []
+ );
+ checkMessageHeaders(daemon.post, {
+ From: "test@tinderbox.invalid",
+ To: "Nobody <nobody@tinderbox.invalid>",
+ Cc: "Alex <alex@tinderbox.invalid>",
+ Bcc: undefined,
+ "Reply-To": "Charles <charles@tinderbox.invalid>",
+ "X-Mozilla-Status": undefined,
+ "X-Mozilla-Keys": undefined,
+ "X-Mozilla-Draft-Info": undefined,
+ Fcc: undefined,
+ });
+ server.resetTest();
+ await sendMessage({ bcc: "Somebody <test@tinderbox.invalid" }, identity);
+ checkMessageHeaders(daemon.post, {
+ To: "undisclosed-recipients: ;",
+ });
+ server.resetTest();
+ await sendMessage(
+ {
+ to: "Somebody <test@tinderbox.invalid>",
+ returnReceipt: true,
+ receiptHeaderType: Ci.nsIMsgMdnGenerator.eDntRrtType,
+ },
+ identity
+ );
+ checkMessageHeaders(daemon.post, {
+ "Disposition-Notification-To": "test@tinderbox.invalid",
+ "Return-Receipt-To": "test@tinderbox.invalid",
+ });
+ server.resetTest();
+ let cloudAttachment = makeAttachment({
+ url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec,
+ sendViaCloud: true,
+ htmlAnnotation:
+ "<html><body>This is an html placeholder file.</body></html>",
+ cloudFileAccountKey: "akey",
+ cloudPartHeaderData: "0123456789ABCDE",
+ name: "attachment.html",
+ contentLocation: "http://localhost.invalid/",
+ });
+ await sendMessage({ to: "test@tinderbox.invalid" }, identity, {}, [
+ cloudAttachment,
+ ]);
+ checkMessageHeaders(
+ daemon.post,
+ {
+ "Content-Type": "text/html; charset=utf-8",
+ "X-Mozilla-Cloud-Part": "cloudFile; url=http://localhost.invalid/",
+ },
+ "2"
+ );
+ } finally {
+ server.stop();
+ }
+}
+
+var tests = [
+ testEnvelope,
+ testI18NEnvelope,
+ testIDNEnvelope,
+ testDraftInfo,
+ testOtherHeadersFullAgent,
+ testOtherHeadersMinimalAgent,
+ testOtherHeadersNoAgent,
+ testNewsgroups,
+ testSendHeaders,
+ testContentHeaders,
+ testSentMessage,
+ testMessageIdUseIdentityAddress,
+ testMessageIdUseFromDomain,
+ testMessageIdUseIdentityAttribute,
+];
+
+function run_test() {
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js b/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js
new file mode 100644
index 0000000000..69c1b753b6
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+// Test that nsIMsgCompFields works properly
+
+var nsMsgCompFields = Components.Constructor(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+);
+
+function check_headers(enumerator, container) {
+ let checkValues = new Set(container.map(header => header.toLowerCase()));
+ for (let value of enumerator) {
+ value = value.toLowerCase();
+ Assert.ok(checkValues.has(value));
+ checkValues.delete(value);
+ }
+ Assert.equal(checkValues.size, 0);
+}
+
+function run_test() {
+ let fields = new nsMsgCompFields();
+ Assert.ok(fields instanceof Ci.nsIMsgCompFields);
+ Assert.ok(fields instanceof Ci.msgIStructuredHeaders);
+ Assert.ok(fields instanceof Ci.msgIWritableStructuredHeaders);
+ check_headers(fields.headerNames, []);
+ Assert.ok(!fields.hasRecipients);
+
+ // Try some basic headers
+ fields.setHeader("From", [{ name: "", email: "a@test.invalid" }]);
+ let from = fields.getHeader("from");
+ Assert.equal(from.length, 1);
+ Assert.equal(from[0].email, "a@test.invalid");
+ check_headers(fields.headerNames, ["From"]);
+ Assert.ok(!fields.hasRecipients);
+
+ // Add a To header
+ fields.setHeader("To", [{ name: "", email: "b@test.invalid" }]);
+ check_headers(fields.headerNames, ["From", "To"]);
+ Assert.ok(fields.hasRecipients);
+
+ // Delete a header...
+ fields.deleteHeader("from");
+ Assert.equal(fields.getHeader("From"), undefined);
+ check_headers(fields.headerNames, ["To"]);
+
+ // Subject should work and not convert to RFC 2047.
+ fields.subject = "\u79c1\u306f\u4ef6\u540d\u5348\u524d";
+ Assert.equal(fields.subject, "\u79c1\u306f\u4ef6\u540d\u5348\u524d");
+ Assert.equal(
+ fields.getHeader("Subject"),
+ "\u79c1\u306f\u4ef6\u540d\u5348\u524d"
+ );
+
+ // Check header synchronization.
+ fields.from = "a@test.invalid";
+ Assert.equal(fields.from, "a@test.invalid");
+ Assert.equal(fields.getHeader("From")[0].email, "a@test.invalid");
+ fields.from = null;
+ Assert.equal(fields.getHeader("From"), undefined);
+}
diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js
new file mode 100644
index 0000000000..700232b46e
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/**
+ * Tests nsMsgCompose expandMailingLists.
+ */
+
+/**
+ * Helper to check population worked as expected.
+ *
+ * @param {string} aTo - Text in the To field.
+ * @param {string} aCheckTo - The expected To addresses (after possible list population)
+ */
+function checkPopulate(aTo, aCheckTo) {
+ var msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+
+ // Set up some basic fields for compose.
+ var fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ fields.to = aTo;
+
+ // Set up some params
+ var params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ params.composeFields = fields;
+
+ msgCompose.initialize(params);
+
+ msgCompose.expandMailingLists();
+ let addresses = fields.getHeader("To");
+ let checkEmails = MailServices.headerParser.parseDecodedHeader(aCheckTo);
+ Assert.equal(addresses.length, checkEmails.length);
+ for (let i = 0; i < addresses.length; i++) {
+ Assert.equal(addresses[i].name, checkEmails[i].name);
+ Assert.equal(addresses[i].email, checkEmails[i].email);
+ }
+}
+
+function run_test() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+ loadABFile("../../../data/abLists2", kCABData.fileName);
+
+ // Test - Check we can initialize with fewest specified
+ // parameters and don't fail/crash like we did in bug 411646.
+
+ var msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+
+ // Set up some params
+ var params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ msgCompose.initialize(params);
+
+ // Test - expandMailingLists basic functionality.
+
+ // Re-initialize
+ msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+
+ // Set up some basic fields for compose.
+ var fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // These aren't in the address book copied above.
+ fields.from = "test1@foo1.invalid";
+ fields.to = "test2@foo1.invalid";
+ fields.cc = "test3@foo1.invalid";
+ fields.bcc = "test4@foo1.invalid";
+
+ // Set up some params
+ params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(
+ Ci.nsIMsgComposeParams
+ );
+
+ params.composeFields = fields;
+
+ msgCompose.initialize(params);
+
+ msgCompose.expandMailingLists();
+ Assert.equal(fields.to, "test2@foo1.invalid");
+ Assert.equal(fields.cc, "test3@foo1.invalid");
+ Assert.equal(fields.bcc, "test4@foo1.invalid");
+
+ // Test - expandMailingLists with plain text.
+
+ checkPopulate("test4@foo.invalid", "test4@foo.invalid");
+
+ // Test - expandMailingLists with html.
+
+ checkPopulate("test5@foo.invalid", "test5@foo.invalid");
+
+ // Test - expandMailingLists with a list of three items.
+
+ checkPopulate(
+ "TestList1 <TestList1>",
+ "test1@foo.invalid,test2@foo.invalid,test3@foo.invalid"
+ );
+
+ // Test - expandMailingLists with a list of one item.
+
+ checkPopulate("TestList2 <TestList2>", "test4@foo.invalid");
+
+ checkPopulate("TestList3 <TestList3>", "test5@foo.invalid");
+
+ // Test - expandMailingLists with items from multiple address books.
+
+ checkPopulate(
+ "TestList1 <TestList1>, test3@com.invalid",
+ "test1@foo.invalid,test2@foo.invalid,test3@foo.invalid,test3@com.invalid"
+ );
+
+ checkPopulate(
+ "TestList2 <TestList2>, ListTest2 <ListTest2>",
+ "test4@foo.invalid,test4@com.invalid"
+ );
+
+ checkPopulate(
+ "TestList3 <TestList3>, ListTest1 <ListTest1>",
+ "test5@foo.invalid,test1@com.invalid,test2@com.invalid,test3@com.invalid"
+ );
+
+ // test bug 254519 rfc 2047 encoding
+ checkPopulate(
+ "=?iso-8859-1?Q?Sure=F6name=2C_Forename_Dr=2E?= <pb@bieringer.invalid>",
+ '"Sure\u00F6name, Forename Dr." <pb@bieringer.invalid>'
+ );
+}
diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js
new file mode 100644
index 0000000000..5f234444b8
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgCompose functions relating to send listeners.
+ */
+
+let gMsgCompose = null;
+let numSendListenerFunctions = 7;
+
+let gSLAll = new Array(numSendListenerFunctions + 1);
+
+function sendListener() {}
+
+sendListener.prototype = {
+ mReceived: 0,
+ mAutoRemoveItem: 0,
+
+ onStartSending(aMsgID, aMsgSize) {
+ this.mReceived |= 0x01;
+ if (this.mAutoRemoveItem == 0x01) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onProgress(aMsgID, aProgress, aProgressMax) {
+ this.mReceived |= 0x02;
+ if (this.mAutoRemoveItem == 0x02) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onStatus(aMsgID, aMsg) {
+ this.mReceived |= 0x04;
+ if (this.mAutoRemoveItem == 0x04) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {
+ this.mReceived |= 0x08;
+ if (this.mAutoRemoveItem == 0x08) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onGetDraftFolderURI(aMsgID, aFolderURI) {
+ this.mReceived |= 0x10;
+ if (this.mAutoRemoveItem == 0x10) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onSendNotPerformed(aMsgID, aStatus) {
+ this.mReceived |= 0x20;
+ if (this.mAutoRemoveItem == 0x20) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+ onTransportSecurityError(msgID, status, secInfo, location) {
+ this.mReceived |= 0x40;
+ if (this.mAutoRemoveItem == 0x40) {
+ gMsgCompose.removeMsgSendListener(this);
+ }
+ },
+};
+
+function NotifySendListeners() {
+ gMsgCompose.onStartSending(null, null);
+ gMsgCompose.onProgress(null, null, null);
+ gMsgCompose.onStatus(null, null);
+ gMsgCompose.onStopSending(null, null, null, null);
+ gMsgCompose.onGetDraftFolderURI(null, null);
+ gMsgCompose.onSendNotPerformed(null, null);
+ gMsgCompose.onTransportSecurityError(null, null, null, "");
+}
+
+function run_test() {
+ gMsgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ gMsgCompose.initialize(params);
+
+ Assert.ok(gMsgCompose != null);
+
+ // Test - Add a listener
+
+ for (let i = 0; i < numSendListenerFunctions + 1; ++i) {
+ gSLAll[i] = new sendListener();
+ gMsgCompose.addMsgSendListener(gSLAll[i]);
+ }
+
+ // Test - Notify all listeners
+
+ NotifySendListeners();
+
+ const bitMask = (1 << numSendListenerFunctions) - 1;
+ for (let i = 0; i < numSendListenerFunctions + 1; ++i) {
+ Assert.equal(gSLAll[i].mReceived, bitMask);
+ gSLAll[i].mReceived = 0;
+
+ // And prepare for test 3.
+ gSLAll[i].mAutoRemoveItem = 1 << i;
+ }
+
+ // Test - Remove some listeners as we go
+
+ NotifySendListeners();
+
+ let currentReceived = 0;
+
+ for (let i = 0; i < numSendListenerFunctions + 1; ++i) {
+ if (i < numSendListenerFunctions) {
+ currentReceived += 1 << i;
+ }
+
+ Assert.equal(gSLAll[i].mReceived, currentReceived);
+ gSLAll[i].mReceived = 0;
+ }
+
+ // Test - Ensure the listeners have been removed.
+
+ NotifySendListeners();
+
+ for (let i = 0; i < numSendListenerFunctions + 1; ++i) {
+ if (i < numSendListenerFunctions) {
+ Assert.equal(gSLAll[i].mReceived, 0);
+ } else {
+ Assert.equal(gSLAll[i].mReceived, bitMask);
+ }
+ }
+
+ // Test - Remove main listener
+
+ gMsgCompose.removeMsgSendListener(gSLAll[numSendListenerFunctions]);
+}
diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js
new file mode 100644
index 0000000000..71521abff4
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for increasing the popularity of contacts via
+ * expandMailingLists.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var TESTS = [
+ {
+ email: "em@test.invalid",
+ // TB 2 stored popularity as hex, so we need to check correct handling.
+ prePopularity: "a",
+ postPopularity: "11",
+ },
+ {
+ email: "e@test.invalid",
+ prePopularity: "0",
+ postPopularity: "1",
+ },
+ {
+ email: "e@test.invalid",
+ prePopularity: "1",
+ postPopularity: "2",
+ },
+ {
+ email: "em@test.invalid",
+ prePopularity: "11",
+ postPopularity: "12",
+ },
+];
+
+function checkPopulate(aTo, aCheckTo) {
+ let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+
+ // Set up some basic fields for compose.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ fields.to = aTo;
+
+ // Set up some params
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ params.composeFields = fields;
+
+ msgCompose.initialize(params);
+
+ Assert.ok(!msgCompose.expandMailingLists());
+
+ Assert.equal(fields.to, aCheckTo);
+}
+
+function run_test() {
+ loadABFile("../../../data/tb2hexpopularity", kPABData.fileName);
+
+ // Check the popularity index on a couple of cards.
+ let AB = MailServices.ab.getDirectory(kPABData.URI);
+
+ for (let i = 0; i < TESTS.length; ++i) {
+ let card = AB.cardForEmailAddress(TESTS[i].email);
+ Assert.ok(!!card);
+
+ // Thunderbird 2 stored its popularityIndexes as hex, hence when we read it
+ // now we're going to get a hex value. The AB has a value of "a".
+ Assert.equal(
+ card.getProperty("PopularityIndex", -1),
+ TESTS[i].prePopularity
+ );
+
+ // Call the check populate function.
+ checkPopulate(TESTS[i].email, TESTS[i].email);
+
+ // Now we've run check populate, check the popularityIndex has increased.
+ card = AB.cardForEmailAddress(TESTS[i].email);
+ Assert.ok(!!card);
+
+ // Thunderbird 2 stored its popularityIndexes as hex, hence when we read it
+ // now we're going to get a hex value. The AB has a value of "a".
+ Assert.equal(
+ card.getProperty("PopularityIndex", -1),
+ TESTS[i].postPopularity
+ );
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_nsSmtpService1.js b/comm/mailnews/compose/test/unit/test_nsSmtpService1.js
new file mode 100644
index 0000000000..d062ef3f1e
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_nsSmtpService1.js
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsSmtpService
+ */
+
+var SmtpServiceContractID = "@mozilla.org/messengercompose/smtp;1";
+var nsISmtpService = Ci.nsISmtpService;
+
+function run_test() {
+ var smtpService = Cc[SmtpServiceContractID].getService(nsISmtpService);
+
+ // Test - no servers
+
+ var smtpServers = smtpService.servers;
+ Assert.equal(smtpServers.length, 0);
+
+ Assert.equal(smtpService.defaultServer, null);
+
+ // Test - add single server, and check
+
+ var smtpServer = smtpService.createServer();
+
+ smtpServer.hostname = "localhost";
+ smtpServer.description = "test";
+
+ smtpService.defaultServer = smtpServer;
+
+ // Test - Check to see there is only one element in the server list
+ smtpServers = smtpService.servers;
+ Assert.ok(smtpServers.length == 1);
+
+ // Test - Find the server in different ways
+ Assert.equal(smtpServer, smtpService.findServer("", "localhost"));
+ Assert.equal(smtpServer, smtpService.getServerByKey(smtpServer.key));
+
+ // Test - Try finding one that doesn't exist.
+ Assert.equal(null, smtpService.findServer("", "test"));
+
+ // Test - Check default server is still ok
+ Assert.equal(smtpServer, smtpService.defaultServer);
+
+ // Test - Delete the only server
+ smtpService.deleteServer(smtpServer);
+
+ smtpServers = smtpService.servers;
+ Assert.ok(smtpServers.length == 0);
+
+ // do_check_eq(null, smtpService.defaultServer);
+
+ // Test - add multiple servers
+
+ var smtpServerArray = new Array(3);
+
+ for (let i = 0; i < 3; ++i) {
+ smtpServerArray[i] = smtpService.createServer();
+ }
+
+ smtpServerArray[0].hostname = "localhost";
+ smtpServerArray[0].description = "test";
+ smtpServerArray[0].username = "user";
+
+ smtpServerArray[1].hostname = "localhost";
+ smtpServerArray[1].description = "test1";
+ smtpServerArray[1].username = "user1";
+
+ smtpServerArray[2].hostname = "localhost1";
+ smtpServerArray[2].description = "test2";
+ smtpServerArray[2].username = "";
+
+ // Now check them
+ smtpServers = smtpService.servers;
+
+ var found = [false, false, false];
+
+ for (smtpServer of smtpServers) {
+ for (let i = 0; i < 3; ++i) {
+ if (smtpServer.key == smtpServerArray[i].key) {
+ found[i] = true;
+ }
+ }
+ }
+
+ Assert.equal(found, "true,true,true");
+
+ // Test - Find the servers.
+
+ Assert.equal(
+ smtpServerArray[0].key,
+ smtpService.findServer("user", "localhost").key
+ );
+ Assert.equal(
+ smtpServerArray[1].key,
+ smtpService.findServer("user1", "localhost").key
+ );
+ Assert.equal(
+ smtpServerArray[2].key,
+ smtpService.findServer("", "localhost1").key
+ );
+
+ Assert.equal(null, smtpService.findServer("user2", "localhost"));
+
+ // XXX: FIXME
+ // do_check_eq(null, smtpService.findServer("", "localhost"));
+
+ for (let i = 0; i < 3; ++i) {
+ Assert.equal(
+ smtpServerArray[i].key,
+ smtpService.getServerByKey(smtpServerArray[i].key).key
+ );
+ }
+
+ smtpService.defaultServer = smtpServerArray[2];
+ Assert.equal(
+ smtpService.defaultServer.key,
+ smtpServerArray[2].key,
+ "Default server should be correctly set"
+ );
+
+ // Test - Delete the servers
+
+ for (let i = 0; i < 3; ++i) {
+ smtpService.deleteServer(smtpServerArray[i]);
+ }
+
+ smtpServers = smtpService.servers;
+ Assert.ok(smtpServers.length == 0);
+}
diff --git a/comm/mailnews/compose/test/unit/test_saveDraft.js b/comm/mailnews/compose/test/unit/test_saveDraft.js
new file mode 100644
index 0000000000..b3f7029bab
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_saveDraft.js
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for checking correctly saved as draft with unread.
+ */
+
+add_task(async function checkDraft() {
+ await createMessage();
+ Assert.equal(gDraftFolder.getTotalMessages(false), 1);
+ Assert.equal(gDraftFolder.getNumUnread(false), 1);
+});
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ run_next_test();
+}
diff --git a/comm/mailnews/compose/test/unit/test_sendBackground.js b/comm/mailnews/compose/test/unit/test_sendBackground.js
new file mode 100644
index 0000000000..6d0a59f4f9
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendBackground.js
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests sending a message in the background (checks auto-send works).
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var server;
+var originalData;
+var finished = false;
+var identity = null;
+var testFile1 = do_get_file("data/429891_testcase.eml");
+var testFile2 = do_get_file("data/message1.eml");
+
+var kTestFile1Sender = "from_A@foo.invalid";
+var kTestFile1Recipient = "to_A@foo.invalid";
+
+var kIdentityMail = "identity@foo.invalid";
+
+var gMsgSendLater;
+
+// This listener handles the post-sending of the actual message and checks the
+// sequence and ensures the data is correct.
+function msll() {}
+
+msll.prototype = {
+ _initialTotal: 0,
+
+ // nsIMsgSendLaterListener
+ onStartSending(aTotal) {
+ this._initialTotal = 1;
+ Assert.equal(gMsgSendLater.sendingMessages, true);
+ Assert.equal(aTotal, 1);
+ },
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {},
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {},
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ do_throw(
+ "onMessageSendError should not have been called, status: " + aStatus
+ );
+ },
+ onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {
+ do_test_finished();
+ print("msll onStopSending\n");
+ try {
+ Assert.equal(aStatus, 0);
+ Assert.equal(aTotalTried, 1);
+ Assert.equal(aSuccessful, 1);
+ Assert.equal(this._initialTotal, 1);
+ Assert.equal(gMsgSendLater.sendingMessages, false);
+
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kTestFile1Sender +
+ "> BODY=8BITMIME SIZE=" +
+ originalData.length,
+ "RCPT TO:<" + kTestFile1Recipient + ">",
+ "DATA",
+ ]);
+
+ // Compare data file to what the server received
+ Assert.equal(originalData, server._daemon.post);
+
+ // check there's still one message left in the folder
+ Assert.equal(
+ gMsgSendLater.getUnsentMessagesFolder(null).getTotalMessages(false),
+ 1
+ );
+
+ finished = true;
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+ },
+};
+
+add_task(async function run_the_test() {
+ // The point of this test - send in background.
+ Services.prefs.setBoolPref("mailnews.sendInBackground", true);
+
+ // Ensure we have a local mail account, an normal account and appropriate
+ // servers and identities.
+ localAccountUtils.loadLocalMailAccount();
+
+ // Now load (and internally initialize) the send later service
+ gMsgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+ );
+
+ // Test file - for bug 429891
+ originalData = await IOUtils.readUTF8(testFile1.path);
+
+ // Check that the send later service thinks we don't have messages to send
+ Assert.equal(gMsgSendLater.hasUnsentMessages(identity), false);
+
+ MailServices.accounts.setSpecialFolders();
+
+ let account = MailServices.accounts.createAccount();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ // Start the fake SMTP server
+ server = setupServerDaemon();
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ account.incomingServer = incomingServer;
+ MailServices.accounts.defaultAccount = account;
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ Assert.equal(identity.doFcc, true);
+
+ // Now prepare to actually "send" the message later, i.e. dump it in the
+ // unsent messages folder.
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // Setting the compFields sender and recipient to any value is required to
+ // survive mime_sanity_check_fields in nsMsgCompUtils.cpp.
+ // Sender and recipient are required for sendMessageFile but SMTP
+ // transaction values will be used directly from mail body.
+ compFields.from = "irrelevant@foo.invalid";
+ compFields.to = "irrelevant@foo.invalid";
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+ var msgSend2 = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // A test to check that we are sending files correctly, including checking
+ // what the server receives and what we output.
+ test = "sendMessageLater";
+
+ var messageListener = new msll();
+
+ gMsgSendLater.addListener(messageListener);
+
+ // Send this message later - it shouldn't get sent
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile2,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ null,
+ null,
+ null,
+ null
+ );
+
+ // Send the unsent message in the background, because we have
+ // mailnews.sendInBackground set, nsMsgSendLater should just send it for
+ // us.
+ msgSend2.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile1,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverBackground,
+ null,
+ null,
+ null,
+ null
+ );
+
+ server.performTest();
+
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+
+ do_test_pending();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js b/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js
new file mode 100644
index 0000000000..56ab77c303
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js
@@ -0,0 +1,231 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests sending messages to addresses with non-ASCII characters.
+ */
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var test = null;
+var server;
+var finished = false;
+
+var sentFolder;
+
+var kSender = "from@foo.invalid";
+var kToASCII = "to@foo.invalid";
+var kToValid = "to@v\u00E4lid.foo.invalid";
+var kToValidACE = "to@xn--vlid-loa.foo.invalid";
+var kToInvalid = "b\u00F8rken.to@invalid.foo.invalid";
+var kToInvalidWithoutDomain = "b\u00F8rken.to";
+var NS_ERROR_ILLEGAL_LOCALPART = 0x80553139;
+
+// for alertTestUtils.js
+let resolveAlert;
+function alertPS(parent, aDialogText, aText) {
+ var composeProps = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+ var expectedAlertMessage =
+ composeProps.GetStringFromName("sendFailed") +
+ "\n" +
+ composeProps
+ .GetStringFromName("errorIllegalLocalPart2")
+ // Without the domain, we currently don't display any name in the
+ // message part.
+ .replace("%s", test == kToInvalidWithoutDomain ? "" : test);
+
+ // we should only get here for the kToInvalid test case
+ Assert.equal(aText, expectedAlertMessage);
+ resolveAlert();
+}
+
+// message listener implementations
+function MsgSendListener(aRecipient, originalData) {
+ this.rcpt = aRecipient;
+ this.originalData = originalData;
+}
+
+/**
+ * @implements {nsIMsgSendListener}
+ * @implements {nsIMsgCopyServiceListener}
+ */
+MsgSendListener.prototype = {
+ // nsIMsgSendListener
+ onStartSending(aMsgID, aMsgSize) {},
+ onProgress(aMsgID, aProgress, aProgressMax) {},
+ onStatus(aMsgID, aMsg) {},
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {
+ try {
+ if (test == kToValid || test == kToASCII) {
+ Assert.equal(aStatus, 0);
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kSender +
+ "> BODY=8BITMIME SIZE=" +
+ this.originalData.length,
+ "RCPT TO:<" + this.rcpt + ">",
+ "DATA",
+ ]);
+ // Compare data file to what the server received
+ Assert.equal(this.originalData, server._daemon.post);
+ } else {
+ Assert.equal(aStatus, NS_ERROR_ILLEGAL_LOCALPART);
+ do_check_transaction(server.playTransaction(), ["EHLO test"]);
+ // Local address (before the @) has non-ascii char(s) or the @ is
+ // missing from the address. An alert is triggered after the EHLO is
+ // sent. Nothing else occurs so we "finish" the test to avoid
+ // NS_ERROR_ABORT test failure due to timeout waiting for the send
+ // (which doesn't occurs) to complete.
+ }
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(false);
+ }
+ do_test_finished();
+ }
+ },
+ onGetDraftFolderURI(aMsgID, aFolderURI) {},
+ onSendNotPerformed(aMsgID, aStatus) {},
+ onTransportSecurityError(msgID, status, secInfo, location) {},
+
+ // nsIMsgCopyServiceListener
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ GetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ Assert.equal(aStatus, 0);
+ try {
+ // Now do a comparison of what is in the sent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ sentFolder,
+ mailTestUtils.firstMsgHdr(sentFolder)
+ );
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("From:");
+ Assert.notEqual(pos, -1);
+ msgData = msgData.substr(pos);
+ Assert.equal(this.originalData, msgData);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ finished = true;
+ }
+ },
+
+ // QueryInterface
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgSendListener",
+ "nsIMsgCopyServiceListener",
+ ]),
+};
+
+async function doSendTest(aRecipient, aRecipientExpected, waitForPrompt) {
+ info(`Testing send to ${aRecipient} will get sent to ${aRecipientExpected}`);
+ let promiseAlertReceived = new Promise(resolve => {
+ resolveAlert = resolve;
+ });
+ test = aRecipient;
+ server = setupServerDaemon();
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kSender, smtpServer);
+ Assert.equal(identity.doFcc, true);
+
+ // Random test file with data we don't actually care about. ;-)
+ var testFile = do_get_file("data/message1.eml");
+ var originalData = await IOUtils.readUTF8(testFile.path);
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ do_test_pending();
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ compFields.from = identity.email;
+ compFields.to = aRecipient;
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ null,
+ new MsgSendListener(aRecipientExpected, originalData),
+ null,
+ null
+ );
+
+ server.performTest();
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+ if (waitForPrompt) {
+ await promiseAlertReceived;
+ }
+ } catch (e) {
+ Assert.ok(false, "Send fail: " + e);
+ } finally {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_setup(function () {
+ registerAlertTestUtils();
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ MailServices.accounts.setSpecialFolders();
+ sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+});
+
+add_task(async function plainASCIIRecipient() {
+ // Test 1:
+ // Plain ASCII recipient address.
+ await doSendTest(kToASCII, kToASCII, false);
+});
+
+add_task(async function domainContainsNonAscii() {
+ // Test 2:
+ // The recipient's domain part contains a non-ASCII character, hence the
+ // address needs to be converted to ACE before sending.
+ // The old code would just strip the non-ASCII character and try to send
+ // the message to the remaining - wrong! - address.
+ // The new code will translate the domain part to ACE for the SMTP
+ // transaction (only), i.e. the To: header will stay as stated by the sender.
+ await doSendTest(kToValid, kToValidACE, false);
+});
+
+add_task(async function localContainsNonAscii() {
+ // Test 3:
+ // The recipient's local part contains a non-ASCII character, which is not
+ // allowed with unextended SMTP.
+ // The old code would just strip the invalid character and try to send the
+ // message to the remaining - wrong! - address.
+ // The new code will present an informational message box and deny sending.
+ await doSendTest(kToInvalid, kToInvalid, true);
+});
+
+add_task(async function invalidCharNoAt() {
+ // Test 4:
+ // Bug 856506. invalid char without '@' causes crash.
+ await doSendTest(kToInvalidWithoutDomain, kToInvalidWithoutDomain, true);
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendMailMessage.js b/comm/mailnews/compose/test/unit/test_sendMailMessage.js
new file mode 100644
index 0000000000..ea294a0b92
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMailMessage.js
@@ -0,0 +1,189 @@
+/**
+ * Protocol tests for SMTP.
+ *
+ * This test currently consists of verifying the correct protocol sequence
+ * between mailnews and SMTP server. It does not check the data of the message
+ * either side of the link, it will be extended later to do that.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "testsmtp";
+var kPassword = "smtptest";
+
+async function test_RFC2821() {
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Just a basic test to check we're sending mail correctly.
+ test = "Basic sendMailMessage";
+
+ // First do test with identity email address used for smtp MAIL FROM.
+ Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", false);
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "MAIL FROM:<" + kIdentityMail + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ smtpServer.closeCachedConnections();
+ server.resetTest();
+
+ // Now do the same test with sender's email address used for smtp MAIL FROM.
+ Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", true);
+
+ urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ smtpServer.closeCachedConnections();
+ server.resetTest();
+
+ // This time with auth.
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+ smtpServer.password = kPassword;
+
+ // First do test with identity email address used for smtp MAIL FROM.
+ Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", false);
+
+ urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword),
+ "MAIL FROM:<" + kIdentityMail + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ smtpServer.closeCachedConnections();
+ server.resetTest();
+
+ // Now do the same test with sender's email address used for smtp MAIL FROM.
+ Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", true);
+
+ urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword),
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function run() {
+ server = setupServerDaemon();
+ await test_RFC2821();
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendMessageFile.js b/comm/mailnews/compose/test/unit/test_sendMessageFile.js
new file mode 100644
index 0000000000..cb2882e88f
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMessageFile.js
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Protocol tests for SMTP.
+ *
+ * This test verifies:
+ * - Sending a message to an SMTP server (which is also covered elsewhere).
+ * - Correct reception of the message by the SMTP server.
+ * - Correct saving of the message to the sent folder.
+ *
+ * Originally written to test bug 429891 where saving to the sent folder was
+ * mangling the message.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var server;
+var sentFolder;
+var originalData;
+var finished = false;
+
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+
+function msl() {}
+
+msl.prototype = {
+ // nsIMsgSendListener
+ onStartSending(aMsgID, aMsgSize) {},
+ onProgress(aMsgID, aProgress, aProgressMax) {},
+ onStatus(aMsgID, aMsg) {},
+ onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {
+ try {
+ Assert.equal(aStatus, 0);
+
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=" + originalData.length,
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ // Compare data file to what the server received
+ Assert.equal(originalData, server._daemon.post);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(false);
+ }
+ }
+ },
+ onGetDraftFolderURI(aMsgID, aFolderURI) {},
+ onSendNotPerformed(aMsgID, aStatus) {},
+ onTransportSecurityError(msgID, status, secInfo, location) {},
+
+ // nsIMsgCopyServiceListener
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ GetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ Assert.equal(aStatus, 0);
+ try {
+ // Now do a comparison of what is in the sent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ sentFolder,
+ mailTestUtils.firstMsgHdr(sentFolder)
+ );
+
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("From:");
+ Assert.notEqual(pos, -1);
+
+ msgData = msgData.substr(pos);
+
+ Assert.equal(originalData, msgData);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ finished = true;
+ do_test_finished();
+ }
+ },
+
+ // QueryInterface
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgSendListener",
+ "nsIMsgCopyServiceListener",
+ ]),
+};
+
+add_task(async function run_the_test() {
+ server = setupServerDaemon();
+
+ // Test file - for bug 429891
+ var testFile = do_get_file("data/429891_testcase.eml");
+ originalData = await IOUtils.readUTF8(testFile.path);
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ MailServices.accounts.setSpecialFolders();
+
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kSender, smtpServer);
+
+ sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ Assert.equal(identity.doFcc, true);
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // A test to check that we are sending files correctly, including checking
+ // what the server receives and what we output.
+ test = "sendMessageFile";
+
+ // Msg Comp Fields
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ compFields.from = identity.email;
+ compFields.to = kTo;
+
+ var messageListener = new msl();
+
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgDeliverNow,
+ null,
+ messageListener,
+ null,
+ null
+ );
+
+ server.performTest();
+
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+
+ do_test_pending();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater.js b/comm/mailnews/compose/test/unit/test_sendMessageLater.js
new file mode 100644
index 0000000000..7dcaf8ec32
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMessageLater.js
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Protocol tests for SMTP.
+ *
+ * This test verifies:
+ * - Sending a message to an SMTP server (which is also covered elsewhere).
+ * - Correct reception of the message by the SMTP server.
+ * - Correct saving of the message to the sent folder.
+ *
+ * Originally written to test bug 429891 where saving to the sent folder was
+ * mangling the message.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var server;
+var smtpServer;
+var originalData;
+var finished = false;
+var identity = null;
+var testFile = do_get_file("data/429891_testcase.eml");
+var kTestFileSender = "from_A@foo.invalid";
+var kTestFileRecipient = "to_A@foo.invalid";
+
+var kIdentityMail = "identity@foo.invalid";
+
+var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+);
+
+// This listener handles the post-sending of the actual message and checks the
+// sequence and ensures the data is correct.
+function msll() {}
+
+msll.prototype = {
+ _initialTotal: 0,
+ _startedSending: false,
+
+ // nsIMsgSendLaterListener
+ onStartSending(aTotalMessageCount) {
+ this._initialTotal = 1;
+ Assert.equal(msgSendLater.sendingMessages, true);
+ },
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {
+ this._startedSending = true;
+ },
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {
+ // XXX Enable this function
+ },
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ do_throw(
+ "onMessageSendError should not have been called, status: " + aStatus
+ );
+ },
+ onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {
+ do_test_finished();
+ print("msll onStopSending\n");
+ try {
+ Assert.equal(this._startedSending, true);
+ Assert.equal(aStatus, 0);
+ Assert.equal(aTotalTried, 1);
+ Assert.equal(aSuccessful, 1);
+ Assert.equal(this._initialTotal, 1);
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kTestFileSender +
+ "> BODY=8BITMIME SIZE=" +
+ originalData.length,
+ "RCPT TO:<" + kTestFileRecipient + ">",
+ "DATA",
+ ]);
+
+ // Compare data file to what the server received
+ Assert.equal(originalData, server._daemon.post);
+
+ finished = true;
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+ },
+};
+
+/* exported OnStopCopy */
+// for head_compose.js
+function OnStopCopy(aStatus) {
+ dump("OnStopCopy()\n");
+
+ try {
+ Assert.equal(aStatus, 0);
+
+ // Check this is false before we start sending
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ let folder = msgSendLater.getUnsentMessagesFolder(identity);
+
+ // Check we have a message in the unsent message folder
+ Assert.equal(folder.getTotalMessages(false), 1);
+
+ // Check that the send later service thinks we have messages to send
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), true);
+
+ // Now do a comparison of what is in the sent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ folder,
+ mailTestUtils.firstMsgHdr(folder)
+ );
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("From:");
+ Assert.notEqual(pos, -1);
+
+ msgData = msgData.substr(pos);
+
+ // Check the data is matching.
+ Assert.equal(originalData, msgData);
+
+ sendMessageLater();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ finished = true;
+ }
+}
+
+// This function does the actual send later
+function sendMessageLater() {
+ // Set up the SMTP server.
+ server = setupServerDaemon();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ smtpServer.port = server.port;
+
+ // A test to check that we are sending files correctly, including checking
+ // what the server receives and what we output.
+ test = "sendMessageLater";
+
+ var messageListener = new msll();
+
+ msgSendLater.addListener(messageListener);
+
+ // Send the unsent message
+ msgSendLater.sendUnsentMessages(identity);
+
+ server.performTest();
+
+ do_timeout(10000, function () {
+ if (!finished) {
+ do_throw("Notifications of message send/copy not received");
+ }
+ });
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function run_the_test() {
+ // Test file - for bug 429891
+ originalData = await IOUtils.readUTF8(testFile.path);
+
+ // Ensure we have a local mail account, an normal account and appropriate
+ // servers and identities.
+ localAccountUtils.loadLocalMailAccount();
+
+ // Check that the send later service thinks we don't have messages to send
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), false);
+
+ MailServices.accounts.setSpecialFolders();
+
+ let account = MailServices.accounts.createAccount();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ smtpServer = getBasicSmtpServer(1);
+ identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ account.incomingServer = incomingServer;
+ MailServices.accounts.defaultAccount = account;
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ Assert.equal(identity.doFcc, true);
+
+ // Now prepare to actually "send" the message later, i.e. dump it in the
+ // unsent messages folder.
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // Setting the compFields sender and recipient to any value is required to
+ // survive mime_sanity_check_fields in nsMsgCompUtils.cpp.
+ // Sender and recipient are required for sendMessageFile but SMTP
+ // transaction values will be used directly from mail body.
+ compFields.from = "irrelevant@foo.invalid";
+ compFields.to = "irrelevant@foo.invalid";
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ null,
+ copyListener,
+ null,
+ null
+ );
+
+ // Now we wait till we get copy notification of completion.
+ do_test_pending();
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater2.js b/comm/mailnews/compose/test/unit/test_sendMessageLater2.js
new file mode 100644
index 0000000000..bd0b974400
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMessageLater2.js
@@ -0,0 +1,301 @@
+/* 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/. */
+
+/**
+ * Complex test for the send message later function - including sending multiple
+ * times in the same session.
+ *
+ * XXX: This test is intended to additionally test sending of multiple messages
+ * from one send later instance, however due to the fact we use one connection
+ * per message sent, it is very difficult to consistently get the fake server
+ * reconnected in time for the next connection. Thus, sending of multiple
+ * messages is currently disabled (but commented out for local testing if
+ * required), when we fix bug 136871 we should be able to enable the multiple
+ * messages option.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+var server = null;
+var smtpServer;
+var gSentFolder;
+var identity = null;
+var gMsgFile = [
+ do_get_file("data/message1.eml"),
+ do_get_file("data/429891_testcase.eml"),
+];
+var kTestFileSender = ["from_B@foo.invalid", "from_A@foo.invalid"];
+var kTestFileRecipient = ["to_B@foo.invalid", "to_A@foo.invalid"];
+
+var gMsgFileData = [];
+var gMsgOrder = [];
+var gLastSentMessage = 0;
+
+var kIdentityMail = "identity@foo.invalid";
+
+var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+);
+
+var messageListener;
+var onStopCopyPromise = PromiseUtils.defer();
+
+/* exported OnStopCopy */
+// for head_compose.js
+// This function is used to find out when the copying of the message to the
+// unsent message folder is completed, and hence can fire off the actual
+// sending of the message.
+function OnStopCopy(aStatus) {
+ Assert.equal(aStatus, 0);
+
+ // Check this is false before we start sending.
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ // Check that the send later service thinks we have messages to send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), true);
+
+ // Check we have a message in the unsent message folder.
+ Assert.equal(gSentFolder.getTotalMessages(false), gMsgOrder.length);
+
+ // Start the next step after a brief time so that functions can finish
+ // properly.
+ onStopCopyPromise.resolve();
+}
+
+add_setup(async function () {
+ // Load in the test files so we have a record of length and their data.
+ for (var i = 0; i < gMsgFile.length; ++i) {
+ gMsgFileData[i] = await IOUtils.readUTF8(gMsgFile[i].path);
+ }
+
+ // Ensure we have a local mail account, an normal account and appropriate
+ // servers and identities.
+ localAccountUtils.loadLocalMailAccount();
+
+ // Check that the send later service thinks we don't have messages to send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), false);
+
+ MailServices.accounts.setSpecialFolders();
+
+ let account = MailServices.accounts.createAccount();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ smtpServer = getBasicSmtpServer(1);
+ identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ account.incomingServer = incomingServer;
+ MailServices.accounts.defaultAccount = account;
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ gSentFolder = msgSendLater.getUnsentMessagesFolder(identity);
+
+ // Don't copy messages to sent folder for this test.
+ identity.doFcc = false;
+
+ // Create and add a listener.
+ messageListener = new MsgSendLaterListener();
+
+ msgSendLater.addListener(messageListener);
+
+ // Set up the server.
+ server = setupServerDaemon();
+ server.setDebugLevel(fsDebugRecv);
+});
+
+add_task(async function test_sendMessageLater2_message1() {
+ // Copy Message from file to folder.
+ await sendMessageLater(0);
+
+ // Send unsent message.
+ await sendUnsentMessages();
+
+ // Check sent folder is now empty.
+ Assert.equal(gSentFolder.getTotalMessages(false), 0);
+
+ // Reset the server.
+ server.stop();
+ server.resetTest();
+
+ // Reset counts.
+ resetCounts();
+});
+
+add_task(async function test_sendMessageLater2_429891_testcase() {
+ // Copy more messages.
+ await sendMessageLater(1);
+
+ // XXX Only do one the second time round, as described at the start of the
+ // file.
+ // await sendMessageLater(0);
+
+ // Test send again.
+ await sendUnsentMessages();
+});
+
+async function sendMessageLater(aTestFileIndex) {
+ gMsgOrder.push(aTestFileIndex);
+
+ // Prepare to actually "send" the message later, i.e. dump it in the
+ // unsent messages folder.
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // Setting the compFields sender and recipient to any value is required to
+ // survive mime_sanity_check_fields in nsMsgCompUtils.cpp.
+ // Sender and recipient are required for sendMessageFile but SMTP
+ // transaction values will be used directly from mail body.
+ compFields.from = "irrelevant@foo.invalid";
+ compFields.to = "irrelevant@foo.invalid";
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ gMsgFile[aTestFileIndex],
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ null,
+ copyListener,
+ null,
+ null
+ );
+ await onStopCopyPromise.promise;
+ // Reset onStopCopyPromise.
+ onStopCopyPromise = PromiseUtils.defer();
+}
+
+function resetCounts() {
+ gMsgOrder = [];
+ gLastSentMessage = 0;
+}
+
+// This function does the actual send later.
+async function sendUnsentMessages() {
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server.
+ server.start();
+ smtpServer.port = server.port;
+
+ // Send the unsent message.
+ msgSendLater.sendUnsentMessages(identity);
+ } catch (e) {
+ throw new Error(e);
+ }
+ await messageListener.promise;
+ messageListener.deferPromise();
+}
+
+// This listener handles the post-sending of the actual message and checks the
+// sequence and ensures the data is correct.
+class MsgSendLaterListener {
+ constructor() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ checkMessageSend(aCurrentMessage) {
+ do_check_transaction(server.playTransaction(), [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kTestFileSender[gMsgOrder[aCurrentMessage - 1]] +
+ "> BODY=8BITMIME SIZE=" +
+ gMsgFileData[gMsgOrder[aCurrentMessage - 1]].length,
+ "RCPT TO:<" + kTestFileRecipient[gMsgOrder[aCurrentMessage - 1]] + ">",
+ "DATA",
+ ]);
+
+ // Compare data file to what the server received.
+ Assert.equal(
+ gMsgFileData[gMsgOrder[aCurrentMessage - 1]],
+ server._daemon.post
+ );
+ }
+
+ // nsIMsgSendLaterListener
+ onStartSending(aTotalMessageCount) {
+ Assert.equal(aTotalMessageCount, gMsgOrder.length);
+ Assert.equal(msgSendLater.sendingMessages, true);
+ }
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {
+ if (gLastSentMessage > 0) {
+ this.checkMessageSend(aCurrentMessage);
+ }
+ Assert.equal(gLastSentMessage + 1, aCurrentMessage);
+ gLastSentMessage = aCurrentMessage;
+ }
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {
+ Assert.equal(aTotalMessageCount, gMsgOrder.length);
+ Assert.equal(gLastSentMessage, aCurrentMessage);
+ Assert.equal(msgSendLater.sendingMessages, true);
+ }
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ throw new Error(
+ "onMessageSendError should not have been called, status: " + aStatus
+ );
+ }
+ onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {
+ try {
+ Assert.equal(aStatus, 0);
+ Assert.equal(aTotalTried, aSuccessful);
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ // Check that the send later service now thinks we don't have messages to
+ // send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), false);
+
+ this.checkMessageSend(gLastSentMessage);
+ } catch (e) {
+ throw new Error(e);
+ }
+ // The extra timeout here is to work around an issue where sometimes
+ // the sendUnsentMessages is completely synchronous up until onStopSending
+ // and sometimes it isn't. This protects us for the synchronous case to
+ // allow the sendUnsentMessages function to complete and exit before we
+ // resolve the promise.
+ PromiseTestUtils.promiseDelay(0).then(resolve => {
+ this._deferredPromise.resolve(true);
+ });
+ }
+
+ deferPromise() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ get promise() {
+ return this._deferredPromise.promise;
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater3.js b/comm/mailnews/compose/test/unit/test_sendMessageLater3.js
new file mode 100644
index 0000000000..08e32481c6
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendMessageLater3.js
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Protocol tests for SMTP.
+ *
+ * For trying to send a message later with no server connected, this test
+ * verifies:
+ * - A correct status response.
+ * - A correct state at the end of attempting to send.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var originalData;
+var identity = null;
+var testFile = do_get_file("data/429891_testcase.eml");
+
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+
+var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService(
+ Ci.nsIMsgSendLater
+);
+
+// for alertTestUtils.js
+function alertPS(parent, aDialogTitle, aText) {
+ dump("Hiding Alert {\n" + aText + "\n} End Alert\n");
+}
+
+// This listener handles the post-sending of the actual message and checks the
+// sequence and ensures the data is correct.
+function msll() {}
+
+msll.prototype = {
+ _initialTotal: 0,
+ _errorRaised: false,
+
+ // nsIMsgSendLaterListener
+ onStartSending(aTotal) {
+ this._initialTotal = 1;
+ Assert.equal(msgSendLater.sendingMessages, true);
+ },
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {},
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {},
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ this._errorRaised = true;
+ },
+ onStopSending(aStatus, aMsg, aTotal, aSuccessful) {
+ print("msll onStopSending\n");
+
+ // NS_ERROR_SMTP_SEND_FAILED_REFUSED is 2153066798
+ Assert.equal(aStatus, 2153066798);
+ Assert.equal(aTotal, 1);
+ Assert.equal(aSuccessful, 0);
+ Assert.equal(this._initialTotal, 1);
+ Assert.equal(this._errorRaised, true);
+ Assert.equal(msgSendLater.sendingMessages, false);
+ // Check that the send later service still thinks we have messages to send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), true);
+
+ do_test_finished();
+ },
+};
+
+/* exported OnStopCopy */
+// for head_compose.js
+function OnStopCopy(aStatus) {
+ Assert.equal(aStatus, 0);
+
+ // Check this is false before we start sending
+ Assert.equal(msgSendLater.sendingMessages, false);
+
+ let folder = msgSendLater.getUnsentMessagesFolder(identity);
+
+ // Check that the send later service thinks we have messages to send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), true);
+
+ // Check we have a message in the unsent message folder
+ Assert.equal(folder.getTotalMessages(false), 1);
+
+ // Now do a comparison of what is in the unsent mail folder
+ let msgData = mailTestUtils.loadMessageToString(
+ folder,
+ mailTestUtils.firstMsgHdr(folder)
+ );
+
+ // Skip the headers etc that mailnews adds
+ var pos = msgData.indexOf("From:");
+ Assert.notEqual(pos, -1);
+
+ msgData = msgData.substr(pos);
+
+ // Check the data is matching.
+ Assert.equal(originalData, msgData);
+
+ do_timeout(0, sendMessageLater);
+}
+
+// This function does the actual send later
+function sendMessageLater() {
+ // No server for this test, just attempt to send unsent and wait.
+ var messageListener = new msll();
+
+ msgSendLater.addListener(messageListener);
+
+ // Send the unsent message
+ msgSendLater.sendUnsentMessages(identity);
+}
+
+add_task(async function run_the_test() {
+ registerAlertTestUtils();
+
+ // Test file - for bug 429891
+ originalData = await IOUtils.readUTF8(testFile.path);
+
+ // Ensure we have a local mail account, an normal account and appropriate
+ // servers and identities.
+ localAccountUtils.loadLocalMailAccount();
+
+ // Check that the send later service thinks we don't have messages to send.
+ Assert.equal(msgSendLater.hasUnsentMessages(identity), false);
+
+ MailServices.accounts.setSpecialFolders();
+
+ let account = MailServices.accounts.createAccount();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "localhost",
+ "pop3"
+ );
+
+ var smtpServer = getBasicSmtpServer();
+ identity = getSmtpIdentity(kSender, smtpServer);
+
+ account.addIdentity(identity);
+ account.defaultIdentity = identity;
+ account.incomingServer = incomingServer;
+ MailServices.accounts.defaultAccount = account;
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Sent");
+
+ identity.doFcc = false;
+
+ // Now prepare to actually "send" the message later, i.e. dump it in the
+ // unsent messages folder.
+
+ var compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ compFields.from = identity.email;
+ compFields.to = kTo;
+
+ var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(
+ Ci.nsIMsgSend
+ );
+
+ msgSend.sendMessageFile(
+ identity,
+ "",
+ compFields,
+ testFile,
+ false,
+ false,
+ Ci.nsIMsgSend.nsMsgQueueForLater,
+ null,
+ copyListener,
+ null,
+ null
+ );
+
+ // Now we wait till we get copy notification of completion.
+ do_test_pending();
+});
diff --git a/comm/mailnews/compose/test/unit/test_sendObserver.js b/comm/mailnews/compose/test/unit/test_sendObserver.js
new file mode 100644
index 0000000000..3640d1ca02
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_sendObserver.js
@@ -0,0 +1,52 @@
+/*
+ * Tests that the mail-set-sender observer, used by extensions to modify the
+ * outgoing server, works.
+ *
+ * This is adapted from test_messageHeaders.js
+ */
+
+var CompFields = CC(
+ "@mozilla.org/messengercompose/composefields;1",
+ Ci.nsIMsgCompFields
+);
+
+// nsIObserver implementation.
+var gData = "";
+var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "mail-set-sender") {
+ Assert.ok(aSubject instanceof Ci.nsIMsgCompose);
+ gData = aData;
+ }
+ },
+};
+
+add_task(async function testObserver() {
+ let fields = new CompFields();
+ let identity = getSmtpIdentity(
+ "from@tinderbox.invalid",
+ getBasicSmtpServer()
+ );
+ identity.fullName = "Observer Tester";
+ fields.to = "Emile <nobody@tinderbox.invalid>";
+ fields.cc = "Alex <alex@tinderbox.invalid>";
+ fields.subject = "Let's test the observer";
+
+ await richCreateMessage(fields, [], identity);
+ // observer data should have:
+ // (no account), Ci.nsIMsgSend.nsMsgSaveAsDraft, identity.key
+ Assert.equal(gData, ",4,id1");
+
+ // Now try with an account
+ await richCreateMessage(fields, [], identity, localAccountUtils.msgAccount);
+ // observer data should have:
+ // (local account key), Ci.nsIMsgSend.nsMsgSaveAsDraft, identity.key
+ Assert.equal(gData, "account1,4,id1");
+});
+
+function run_test() {
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ Services.obs.addObserver(observer, "mail-set-sender");
+ run_next_test();
+}
diff --git a/comm/mailnews/compose/test/unit/test_smtp8bitMime.js b/comm/mailnews/compose/test/unit/test_smtp8bitMime.js
new file mode 100644
index 0000000000..d763947154
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtp8bitMime.js
@@ -0,0 +1,105 @@
+/**
+ * 8BITMIME tests for SMTP.
+ *
+ * This test verifies that 8BITMIME is sent to the server only if the server
+ * advertises it AND if mail.strictly_mime doesn't force us to send 7bit.
+ * It does not check the data of the message on either side of the link.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+
+// aStrictMime: Test if mail.strictly_mime omits the BODY=8BITMIME attribute.
+// aServer8bit: Test if BODY=8BITMIME is only sent if advertised by the server.
+
+async function test_8bitmime(aStrictMime, aServer8bit) {
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ test =
+ "Strictly MIME" +
+ (aStrictMime ? "on (7bit" : "off (8bit") +
+ ", 8BITMIME " +
+ (aServer8bit ? "" : "not ") +
+ "advertised)";
+
+ Services.prefs.setBoolPref("mail.strictly_mime", aStrictMime);
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "MAIL FROM:<" +
+ kSender +
+ (!aStrictMime && aServer8bit
+ ? "> BODY=8BITMIME SIZE=159"
+ : "> SIZE=159"),
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ server.resetTest();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function run() {
+ // The default SMTP server advertises 8BITMIME capability.
+ server = setupServerDaemon();
+ await test_8bitmime(true, true);
+ await test_8bitmime(false, true);
+
+ // Now we need a server which does not advertise 8BITMIME capability.
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ handler.kCapabilities = ["SIZE"];
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+ await test_8bitmime(true, false);
+ await test_8bitmime(false, false);
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js b/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js
new file mode 100644
index 0000000000..24d5c6d554
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js
@@ -0,0 +1,166 @@
+/**
+ * Authentication tests for SMTP.
+ *
+ * Test code <copied from="test_pop3AuthMethods.js">
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+var kAuthSchemes;
+var smtpServer;
+var testFile;
+var identity;
+
+var kUsername = "fred";
+var kPassword = "wilma";
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var MAILFROM = "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159";
+var RCPTTO = "RCPT TO:<" + kTo + ">";
+var AUTHPLAIN = "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword);
+
+var tests = [
+ {
+ title:
+ "Cleartext password, with server supporting AUTH PLAIN, LOGIN, and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["EHLO test", AUTHPLAIN, MAILFROM, RCPTTO, "DATA"],
+ },
+ {
+ title: "Cleartext password, with server only supporting AUTH LOGIN",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["LOGIN"],
+ expectSuccess: true,
+ transaction: ["EHLO test", "AUTH LOGIN", MAILFROM, RCPTTO, "DATA"],
+ },
+ {
+ title:
+ "Encrypted password, with server supporting AUTH PLAIN, LOGIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["EHLO test", "AUTH CRAM-MD5", MAILFROM, RCPTTO, "DATA"],
+ },
+ {
+ title:
+ "Encrypted password, with server only supporting AUTH PLAIN (must fail)",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN"],
+ expectSuccess: false,
+ transaction: ["EHLO test"],
+ },
+ {
+ title:
+ "Any secure method, with server supporting AUTH PLAIN, LOGIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["EHLO test", "AUTH CRAM-MD5", MAILFROM, RCPTTO, "DATA"],
+ },
+ {
+ title:
+ "Any secure method, with server only supporting AUTH PLAIN (must fail)",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["PLAIN"],
+ expectSuccess: false,
+ transaction: ["EHLO test"],
+ },
+];
+
+function nextTest() {
+ if (tests.length == 0) {
+ // this is sync, so we run into endTest() at the end of run_test() now
+ return;
+ }
+ server.resetTest();
+
+ var curTest = tests.shift();
+ test = curTest.title;
+ dump("NEXT test: " + curTest.title + "\n");
+
+ // Adapt to curTest
+ kAuthSchemes = curTest.serverAuthMethods;
+ smtpServer.authMethod = curTest.clientAuthMethod;
+
+ // Run test
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+ let resolved = false;
+ urlListener.promise.catch(e => {}).finally(() => (resolved = true));
+ Services.tm.spinEventLoopUntil("wait for sending", () => resolved);
+
+ do_check_transaction(server.playTransaction(), curTest.transaction);
+
+ smtpServer.closeCachedConnections();
+ nextTest();
+}
+
+function run_test() {
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ handler.kUsername = kUsername;
+ handler.kPassword = kPassword;
+ handler.kAuthRequired = true;
+ handler.kAuthSchemes = kAuthSchemes;
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+ dump("AUTH PLAIN = " + AUTHPLAIN + "\n");
+ server.start();
+
+ localAccountUtils.loadLocalMailAccount();
+ smtpServer = getBasicSmtpServer(server.port);
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+ smtpServer.password = kPassword;
+ identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ testFile = do_get_file("data/message1.eml");
+
+ nextTest();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ endTest();
+ }
+}
+
+function endTest() {
+ dump("endTest()\n");
+ server.stop();
+
+ dump("emptying event loop\n");
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ dump("next event\n");
+ thread.processNextEvent(true);
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_smtpClient.js b/comm/mailnews/compose/test/unit/test_smtpClient.js
new file mode 100644
index 0000000000..b06ec48560
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpClient.js
@@ -0,0 +1,136 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+let server = setupServerDaemon();
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Test sending is aborted when alwaysSTARTTLS is set, but the server doesn't
+ * support STARTTLS.
+ */
+add_task(async function testAbort() {
+ server.resetTest();
+ let smtpServer = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity("identity@foo.invalid", smtpServer);
+ // Set to always use STARTTLS.
+ smtpServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+ do_test_pending();
+
+ let urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, status) {
+ // Test sending is aborted with NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS.
+ Assert.equal(status, 0x80553126);
+ do_test_finished();
+ },
+ };
+
+ // Send a message.
+ let testFile = do_get_file("data/message1.eml");
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ "to@foo.invalid",
+ identity,
+ "from@foo.invalid",
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+ server.performTest();
+});
+
+/**
+ * Test client identity extension works.
+ */
+add_task(async function testClientIdentityExtension() {
+ server.resetTest();
+ let smtpServer = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity("identity@foo.invalid", smtpServer);
+ // Enable and set clientid to the smtp server.
+ smtpServer.clientidEnabled = true;
+ smtpServer.clientid = "uuid-111";
+
+ // Send a message.
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ let testFile = do_get_file("data/message1.eml");
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ "to@foo.invalid",
+ identity,
+ "from@foo.invalid",
+ null,
+ asyncUrlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await asyncUrlListener.promise;
+
+ // Check CLIENTID command is sent.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "CLIENTID UUID uuid-111",
+ "MAIL FROM:<from@foo.invalid> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<to@foo.invalid>",
+ "DATA",
+ ]);
+});
+
+/**
+ * Test that when To and Cc/Bcc contain the same address, should send only
+ * one RCPT TO per address.
+ */
+add_task(async function testDeduplicateRecipients() {
+ server.resetTest();
+ let smtpServer = getBasicSmtpServer(server.port);
+ let identity = getSmtpIdentity("identity@foo.invalid", smtpServer);
+
+ // Send a message, notice to1 appears twice in the recipients argument.
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ let testFile = do_get_file("data/message1.eml");
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ "to1@foo.invalid,to2@foo.invalid,to1@foo.invalid",
+ identity,
+ "from@foo.invalid",
+ null,
+ asyncUrlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await asyncUrlListener.promise;
+
+ // Check only one RCPT TO is sent for to1.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "MAIL FROM:<from@foo.invalid> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<to1@foo.invalid>",
+ "RCPT TO:<to2@foo.invalid>",
+ "DATA",
+ ]);
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpPassword.js b/comm/mailnews/compose/test/unit/test_smtpPassword.js
new file mode 100644
index 0000000000..f4b8515df7
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpPassword.js
@@ -0,0 +1,97 @@
+/**
+ * Authentication tests for SMTP.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var server;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "testsmtp";
+// Password needs to match the login information stored in the signons json
+// file.
+var kPassword = "smtptest";
+
+add_task(async function () {
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ // Username needs to match the login information stored in the signons json
+ // file.
+ handler.kUsername = kUsername;
+ handler.kPassword = kPassword;
+ handler.kAuthRequired = true;
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // This time with auth
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword),
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpPassword2.js b/comm/mailnews/compose/test/unit/test_smtpPassword2.js
new file mode 100644
index 0000000000..a0445ad0a3
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpPassword2.js
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Extra tests for SMTP passwords (forgetPassword)
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var kUser1 = "testsmtp";
+var kUser2 = "testsmtpa";
+var kProtocol = "smtp";
+var kHostname = "localhost";
+var kServerUrl = kProtocol + "://" + kHostname;
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-multiple.json");
+
+ // Set up the basic accounts and folders.
+ localAccountUtils.loadLocalMailAccount();
+
+ var smtpServer1 = getBasicSmtpServer();
+ var smtpServer2 = getBasicSmtpServer();
+
+ smtpServer1.authMethod = 3;
+ smtpServer1.username = kUser1;
+ smtpServer2.authMethod = 3;
+ smtpServer2.username = kUser2;
+
+ // Test - Check there are two logins to begin with.
+ let logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ Assert.equal(logins.length, 2);
+
+ // These will either be one way around or the other.
+ if (logins[0].username == kUser1) {
+ Assert.equal(logins[1].username, kUser2);
+ } else {
+ Assert.equal(logins[0].username, kUser2);
+ Assert.equal(logins[1].username, kUser1);
+ }
+
+ // Test - Remove a login via the incoming server
+ smtpServer1.forgetPassword();
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // should be one login left for kUser2
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUser2);
+
+ // Test - Remove the other login via the incoming server
+ smtpServer2.forgetPassword();
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // There should be no login left.
+ Assert.equal(logins.length, 0);
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js
new file mode 100644
index 0000000000..b7d7f1ef43
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js
@@ -0,0 +1,151 @@
+/**
+ * This test checks to see if the smtp password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ *
+ * XXX Due to problems with the fakeserver + smtp not using one connection for
+ * multiple sends, the rest of this test is in test_smtpPasswordFailure2.js.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var server;
+var attempt = 0;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "testsmtp";
+// Login information needs to match the login information stored in the signons
+// json file.
+var kInvalidPassword = "smtptest";
+var kValidPassword = "smtptest1";
+
+/* exported alert, confirmEx */
+// for alertTestUtils.js
+function alert(aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ dump("Alert Title: " + aDialogText + "\nAlert Text: " + aText + "\n");
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting retry\n");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ dump("\nCancelling login attempt\n");
+ return 1;
+ default:
+ do_throw("unexpected attempt number " + attempt);
+ return 1;
+ }
+}
+
+add_task(async function () {
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ // Username needs to match the login information stored in the signons json
+ // file.
+ handler.kUsername = kUsername;
+ handler.kPassword = kValidPassword;
+ handler.kAuthRequired = true;
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // This time with auth
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+
+ dump("Send\n");
+
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ null,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ server.performTest();
+
+ dump("End Send\n");
+
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgetton the login even though we've retried and cancelled.
+ let logins = Services.logins.findLogins(
+ "smtp://localhost",
+ null,
+ "smtp://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUsername);
+ Assert.equal(logins[0].password, kInvalidPassword);
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js
new file mode 100644
index 0000000000..f394db434d
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js
@@ -0,0 +1,178 @@
+/**
+ * This test checks to see if the pop3 password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ *
+ * XXX Due to problems with the fakeserver + smtp not using one connection for
+ * multiple sends, the first part of this test is in
+ * test_smtpPasswordFailure2.js.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var server;
+var attempt = 0;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "testsmtp";
+// Password needs to match the login information stored in the signons json
+// file.
+var kInvalidPassword = "smtptest";
+var kValidPassword = "smtptest1";
+
+function confirmExPS(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting Retry\n");
+ return 0;
+ // Second attempt, enter a new password.
+ case 2:
+ dump("\nEnter new password\n");
+ return 2;
+ default:
+ do_throw("unexpected attempt number " + attempt);
+ return 1;
+ }
+}
+
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 2) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
+
+add_task(async function () {
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ // Username needs to match the login information stored in the signons json
+ // file.
+ handler.kUsername = kUsername;
+ handler.kPassword = kValidPassword;
+ handler.kAuthRequired = true;
+ handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ // Start the fake SMTP server
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // This time with auth
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+
+ dump("Send\n");
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ await urlListener.promise;
+
+ dump("End Send\n");
+
+ Assert.equal(attempt, 2);
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "EHLO test",
+ // attempt 3 invalid password
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kInvalidPassword),
+ "AUTH LOGIN",
+ // attempt 4 which retries
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kInvalidPassword),
+ "AUTH LOGIN",
+ // then we enter the correct password
+ "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kValidPassword),
+ "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159",
+ "RCPT TO:<" + kTo + ">",
+ "DATA",
+ ]);
+
+ // Now check the new one has been saved.
+ let logins = Services.logins.findLogins(
+ "smtp://localhost",
+ null,
+ "smtp://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUsername);
+ Assert.equal(logins[0].password, kValidPassword);
+ do_test_finished();
+ } catch (e) {
+ do_throw(e);
+ } finally {
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js
new file mode 100644
index 0000000000..27312b47a4
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js
@@ -0,0 +1,154 @@
+/**
+ * This test checks to see if the smtp password failure is handled correctly
+ * when the server drops the connection on an authentication error.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ *
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var server;
+var attempt = 0;
+
+var kIdentityMail = "identity@foo.invalid";
+var kSender = "from@foo.invalid";
+var kTo = "to@foo.invalid";
+var kUsername = "testsmtp";
+// Password needs to match the login information stored in the signons json
+// file.
+var kValidPassword = "smtptest1";
+
+function confirmExPS(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting Retry\n");
+ return 0;
+ // Second attempt, enter a new password.
+ case 2:
+ dump("\nEnter new password\n");
+ return 2;
+ default:
+ do_throw("unexpected attempt number " + attempt);
+ return 1;
+ }
+}
+
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 2) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
+
+add_task(async function () {
+ function createHandler(d) {
+ var handler = new SMTP_RFC2821_handler(d);
+ handler.dropOnAuthFailure = true;
+ // Username needs to match the login information stored in the signons json
+ // file.
+ handler.kUsername = kUsername;
+ handler.kPassword = kValidPassword;
+ handler.kAuthRequired = true;
+ handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below
+ return handler;
+ }
+ server = setupServerDaemon(createHandler);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Test file
+ var testFile = do_get_file("data/message1.eml");
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ // Start the fake SMTP server
+ server.start();
+ var smtpServer = getBasicSmtpServer(server.port);
+ var identity = getSmtpIdentity(kIdentityMail, smtpServer);
+
+ // This time with auth
+ test = "Auth sendMailMessage";
+
+ smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext;
+ smtpServer.socketType = Ci.nsMsgSocketType.plain;
+ smtpServer.username = kUsername;
+
+ do_test_pending();
+
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ kTo,
+ identity,
+ kSender,
+ null,
+ URLListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+
+ server.performTest();
+});
+
+var URLListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, rc) {
+ // Check for ok status.
+ Assert.equal(rc, 0);
+ // Now check the new password has been saved.
+ let logins = Services.logins.findLogins(
+ "smtp://localhost",
+ null,
+ "smtp://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUsername);
+ Assert.equal(logins[0].password, kValidPassword);
+
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ },
+};
diff --git a/comm/mailnews/compose/test/unit/test_smtpProtocols.js b/comm/mailnews/compose/test/unit/test_smtpProtocols.js
new file mode 100644
index 0000000000..bba7d55b6b
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpProtocols.js
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for getting smtp urls via the protocol handler.
+ */
+
+var defaultProtocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.URI_NON_PERSISTABLE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT;
+
+var protocols = [
+ {
+ protocol: "smtp",
+ urlSpec: "smtp://user@localhost/",
+ defaultPort: Ci.nsISmtpUrl.DEFAULT_SMTP_PORT,
+ },
+ {
+ protocol: "smtps",
+ urlSpec: "smtps://user@localhost/",
+ defaultPort: Ci.nsISmtpUrl.DEFAULT_SMTPS_PORT,
+ },
+];
+
+function run_test() {
+ for (var part = 0; part < protocols.length; ++part) {
+ print("protocol: " + protocols[part].protocol);
+
+ var pH = Cc[
+ "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol
+ ].createInstance(Ci.nsIProtocolHandler);
+
+ Assert.equal(pH.scheme, protocols[part].protocol);
+ Assert.equal(
+ Services.io.getDefaultPort(pH.scheme),
+ protocols[part].defaultPort
+ );
+ Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags);
+
+ // Whip through some of the ports to check we get the right results.
+ for (let i = 0; i < 1024; ++i) {
+ Assert.equal(pH.allowPort(i, ""), i == protocols[part].defaultPort);
+ }
+
+ // Check we get a URI when we ask for one
+ var uri = Services.io.newURI(protocols[part].urlSpec);
+
+ uri.QueryInterface(Ci.nsISmtpUrl);
+
+ Assert.equal(uri.spec, protocols[part].urlSpec);
+
+ try {
+ // This call should throw NS_ERROR_NOT_IMPLEMENTED. If it doesn't,
+ // then we should implement a new test for it.
+ pH.newChannel(uri, null);
+ // If it didn't throw, then shout about it.
+ do_throw("newChannel not throwing NS_ERROR_NOT_IMPLEMENTED.");
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_smtpProxy.js b/comm/mailnews/compose/test/unit/test_smtpProxy.js
new file mode 100644
index 0000000000..7a008be001
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpProxy.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Tests that SMTP over a SOCKS proxy works.
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const PORT = 25;
+var daemon, localserver, server;
+
+add_setup(function () {
+ localAccountUtils.loadLocalMailAccount();
+ server = setupServerDaemon();
+ daemon = server._daemon;
+ server.start();
+ NetworkTestUtils.configureProxy("smtp.tinderbox.invalid", PORT, server.port);
+ localserver = getBasicSmtpServer(PORT, "smtp.tinderbox.invalid");
+});
+
+add_task(async function sendMessage() {
+ equal(daemon.post, undefined);
+ let identity = getSmtpIdentity("test@tinderbox.invalid", localserver);
+ var testFile = do_get_file("data/message1.eml");
+ var urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.smtp.sendMailMessage(
+ testFile,
+ "somebody@example.org",
+ identity,
+ "me@example.org",
+ null,
+ urlListener,
+ null,
+ null,
+ false,
+ "",
+ {},
+ {}
+ );
+ await urlListener.promise;
+ notEqual(daemon.post, "");
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpServer.js b/comm/mailnews/compose/test/unit/test_smtpServer.js
new file mode 100644
index 0000000000..5e252a44f0
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpServer.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests for nsISmtpServer implementation.
+ */
+
+/**
+ * Test that cached server password is cleared when password storage changed.
+ */
+add_task(async function test_passwordmgr_change() {
+ // Create an nsISmtpServer instance and set a password.
+ let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance(
+ Ci.nsISmtpServer
+ );
+ server.password = "smtp-pass";
+ equal(server.password, "smtp-pass", "Password should be cached.");
+
+ // Trigger the change event of password manager.
+ Services.logins.setLoginSavingEnabled("smtp://localhost", false);
+ equal(server.password, "", "Password should be cleared.");
+});
+
+/**
+ * Test getter/setter of attributes.
+ */
+add_task(async function test_attributes() {
+ // Create an nsISmtpServer instance and set a password.
+ let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance(
+ Ci.nsISmtpServer
+ );
+
+ server.description = "アイウ";
+ equal(server.description, "アイウ", "Description should be correctly set.");
+
+ server.hostname = "サービス.jp";
+ equal(server.hostname, "サービス.jp", "Hostname should be correctly set.");
+});
+
+/**
+ * Tests the UID attribute of servers.
+ */
+add_task(async function testUID() {
+ const UUID_REGEXP =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+ // Create a server and check it the UID is set when accessed.
+
+ let serverA = MailServices.smtp.createServer();
+ Assert.stringMatches(
+ serverA.UID,
+ UUID_REGEXP,
+ "server A's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.smtpserver.${serverA.key}.uid`),
+ serverA.UID,
+ "server A's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverA.UID = "00001111-2222-3333-4444-555566667777"),
+ /NS_ERROR_ABORT/,
+ "server A's UID should be unchangeable after it is set"
+ );
+
+ // Create a second server and check the two UIDs don't match.
+
+ let serverB = MailServices.smtp.createServer();
+ Assert.stringMatches(
+ serverB.UID,
+ UUID_REGEXP,
+ "server B's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.smtpserver.${serverB.key}.uid`),
+ serverB.UID,
+ "server B's UID should be saved to the preferences"
+ );
+ Assert.notEqual(
+ serverB.UID,
+ serverA.UID,
+ "server B's UID should not be the same as server A's"
+ );
+
+ // Create a third server and set the UID before it is accessed.
+
+ let serverC = MailServices.smtp.createServer();
+ serverC.UID = "11112222-3333-4444-5555-666677778888";
+ Assert.equal(
+ serverC.UID,
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID set correctly"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.smtpserver.${serverC.key}.uid`),
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverC.UID = "22223333-4444-5555-6666-777788889999"),
+ /NS_ERROR_ABORT/,
+ "server C's UID should be unchangeable after it is set"
+ );
+});
diff --git a/comm/mailnews/compose/test/unit/test_smtpURL.js b/comm/mailnews/compose/test/unit/test_smtpURL.js
new file mode 100644
index 0000000000..833ca91817
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_smtpURL.js
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for checking SMTP URLs are working as expected.
+ * XXX this test needs extending as we fix up nsSmtpUrl.
+ */
+
+var smtpURLs = [
+ {
+ url: "smtp://user@localhost/",
+ spec: "smtp://user@localhost/",
+ username: "user",
+ },
+ {
+ url: "smtps://user@localhost/",
+ spec: "smtps://user@localhost/",
+ username: "user",
+ },
+];
+
+function run_test() {
+ var url;
+ for (var part = 0; part < smtpURLs.length; ++part) {
+ print("url: " + smtpURLs[part].url);
+
+ url = Services.io.newURI(smtpURLs[part].url);
+
+ Assert.equal(url.spec, smtpURLs[part].spec);
+ Assert.equal(url.username, smtpURLs[part].username);
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_splitRecipients.js b/comm/mailnews/compose/test/unit/test_splitRecipients.js
new file mode 100644
index 0000000000..b51da1c7a2
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_splitRecipients.js
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgCompFields functions.
+ * Currently only tests nsIMsgCompFields::SplitRecipients
+ */
+
+var splitRecipientsTests = [
+ {
+ recipients: "me@foo.invalid",
+ emailAddressOnly: false,
+ count: 1,
+ result: ["me@foo.invalid"],
+ },
+ {
+ recipients: "me@foo.invalid, me2@foo.invalid",
+ emailAddressOnly: false,
+ count: 2,
+ result: ["me@foo.invalid", "me2@foo.invalid"],
+ },
+ {
+ recipients: '"foo bar" <me@foo.invalid>',
+ emailAddressOnly: false,
+ count: 1,
+ result: ["foo bar <me@foo.invalid>"],
+ },
+ {
+ recipients: '"foo bar" <me@foo.invalid>',
+ emailAddressOnly: true,
+ count: 1,
+ result: ["me@foo.invalid"],
+ },
+ {
+ recipients: '"foo bar" <me@foo.invalid>, "bar foo" <me2@foo.invalid>',
+ emailAddressOnly: false,
+ count: 2,
+ result: ["foo bar <me@foo.invalid>", "bar foo <me2@foo.invalid>"],
+ },
+ {
+ recipients: '"foo bar" <me@foo.invalid>, "bar foo" <me2@foo.invalid>',
+ emailAddressOnly: true,
+ count: 2,
+ result: ["me@foo.invalid", "me2@foo.invalid"],
+ },
+ {
+ recipients:
+ "A Group:Ed Jones <c@a.invalid>,joe@where.invalid,John <jdoe@one.invalid>;",
+ emailAddressOnly: false,
+ count: 3,
+ result: [
+ "Ed Jones <c@a.invalid>",
+ "joe@where.invalid",
+ "John <jdoe@one.invalid>",
+ ],
+ },
+ {
+ recipients:
+ "mygroup:;, empty:;, foo@foo.invalid, othergroup:bar@foo.invalid, bar2@foo.invalid;, y@y.invalid, empty:;",
+ emailAddressOnly: true,
+ count: 4,
+ result: [
+ "foo@foo.invalid",
+ "bar@foo.invalid",
+ "bar2@foo.invalid",
+ "y@y.invalid",
+ ],
+ },
+ {
+ recipients: "Undisclosed recipients:;;;;;;;;;;;;;;;;,,,,,,,,,,,,,,,,",
+ emailAddressOnly: true,
+ count: 0,
+ result: [],
+ },
+ {
+ recipients: "a@xxx.invalid; b@xxx.invalid",
+ emailAddressOnly: true,
+ count: 2,
+ result: ["a@xxx.invalid", "b@xxx.invalid"],
+ },
+ {
+ recipients: "a@xxx.invalid; B <b@xxx.invalid>",
+ emailAddressOnly: false,
+ count: 2,
+ result: ["a@xxx.invalid", "B <b@xxx.invalid>"],
+ },
+ {
+ recipients: '"A " <a@xxx.invalid>; b@xxx.invalid',
+ emailAddressOnly: false,
+ count: 2,
+ result: ["A <a@xxx.invalid>", "b@xxx.invalid"],
+ },
+ {
+ recipients: "A <a@xxx.invalid>; B <b@xxx.invalid>",
+ emailAddressOnly: false,
+ count: 2,
+ result: ["A <a@xxx.invalid>", "B <b@xxx.invalid>"],
+ },
+ {
+ recipients:
+ "A (this: is, a comment;) <a.invalid>; g: (this: is, <a> comment;) C <c.invalid>, d.invalid;",
+ emailAddressOnly: false,
+ count: 3,
+ result: [
+ "A (this: is, a comment;) <a.invalid>",
+ "(this: is, <a> comment;) C <c.invalid>",
+ "d.invalid <>",
+ ],
+ },
+ {
+ recipients:
+ 'Mary Smith <mary@x.invalid>, extra:;, group:jdoe@example.invalid; Who? <one@y.invalid>; <boss@nil.invalid>, "Giant; \\"Big\\" Box" <sysservices@example.invalid>, ',
+ emailAddressOnly: false,
+ count: 5,
+ result: [
+ "Mary Smith <mary@x.invalid>",
+ "jdoe@example.invalid",
+ "Who? <one@y.invalid>",
+ "boss@nil.invalid",
+ 'Giant; "Big" Box <sysservices@example.invalid>',
+ ],
+ },
+ {
+ recipients: "Undisclosed recipients: a@foo.invalid ;;extra:;",
+ emailAddressOnly: true,
+ count: 1,
+ result: ["a@foo.invalid"],
+ },
+ {
+ recipients: "Undisclosed recipients:;;extra:a@foo.invalid;",
+ emailAddressOnly: true,
+ count: 1,
+ result: ["a@foo.invalid"],
+ },
+ {
+ recipients: "",
+ emailAddressOnly: false,
+ count: 0,
+ result: [],
+ },
+];
+
+function run_test() {
+ var fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ // As most of SplitRecipients functionality is in the nsIMsgHeaderParser
+ // functionality, here (at least initially), we're just interested in checking
+ // the basic argument/return combinations.
+
+ for (var part = 0; part < splitRecipientsTests.length; ++part) {
+ print("Test: " + splitRecipientsTests[part].recipients);
+ var result = fields.splitRecipients(
+ splitRecipientsTests[part].recipients,
+ splitRecipientsTests[part].emailAddressOnly
+ );
+
+ Assert.equal(splitRecipientsTests[part].count, result.length);
+
+ for (var item = 0; item < result.length; ++item) {
+ Assert.equal(splitRecipientsTests[part].result[item], result[item]);
+ }
+ }
+}
diff --git a/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js b/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js
new file mode 100644
index 0000000000..427c101914
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+/*
+ * Test that stale temporary files are cleaned up when the msg compose service
+ * is initialized.
+ */
+
+var gExpectedFiles;
+
+function create_temporary_files_for(name) {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(name);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ return file;
+}
+
+function collect_expected_temporary_files() {
+ let files = [];
+
+ files.push(create_temporary_files_for("nsmail.tmp"));
+ files.push(create_temporary_files_for("nsmail.tmp"));
+ files.push(create_temporary_files_for("nsmail.tmp"));
+ files.push(create_temporary_files_for("nsemail.eml"));
+ files.push(create_temporary_files_for("nsemail.tmp"));
+ files.push(create_temporary_files_for("nsqmail.tmp"));
+ files.push(create_temporary_files_for("nscopy.tmp"));
+ files.push(create_temporary_files_for("nscopy.tmp"));
+
+ return files;
+}
+
+function check_files_not_exist(files) {
+ files.forEach(function (file) {
+ Assert.ok(!file.exists());
+ });
+}
+
+function run_test() {
+ gExpectedFiles = collect_expected_temporary_files();
+ registerCleanupFunction(function () {
+ gExpectedFiles.forEach(function (file) {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+ });
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+ MailServices.compose; // Initialise the compose service.
+ do_test_pending();
+ check_files_not_exist(gExpectedFiles);
+ do_test_finished();
+}
diff --git a/comm/mailnews/compose/test/unit/test_telemetry_compose.js b/comm/mailnews/compose/test/unit/test_telemetry_compose.js
new file mode 100644
index 0000000000..f48db07293
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_telemetry_compose.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test telemetry related to message composition.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const HTML_SCALAR = "tb.compose.format_html";
+const PLAIN_TEXT_SCALAR = "tb.compose.format_plain_text";
+
+/**
+ * Check that we're counting HTML or Plain text when composing.
+ */
+add_task(async function test_compose_format() {
+ Services.telemetry.clearScalars();
+
+ // Bare-bones code to initiate composing a message in given format.
+ let createCompose = function (fmt) {
+ let msgCompose = Cc[
+ "@mozilla.org/messengercompose/compose;1"
+ ].createInstance(Ci.nsIMsgCompose);
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ params.format = fmt;
+ msgCompose.initialize(params);
+ };
+
+ // Start composing arbitrary numbers of messages in each format.
+ const NUM_HTML = 7;
+ const NUM_PLAIN = 13;
+ for (let i = 0; i < NUM_HTML; i++) {
+ createCompose(Ci.nsIMsgCompFormat.HTML);
+ }
+ for (let i = 0; i < NUM_PLAIN; i++) {
+ createCompose(Ci.nsIMsgCompFormat.PlainText);
+ }
+
+ // Did we count them correctly?
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[HTML_SCALAR],
+ NUM_HTML,
+ HTML_SCALAR + " must have the correct value."
+ );
+ Assert.equal(
+ scalars[PLAIN_TEXT_SCALAR],
+ NUM_PLAIN,
+ PLAIN_TEXT_SCALAR + " must have the correct value."
+ );
+});
+
+/**
+ * Check that we're counting compose type (new/reply/fwd etc) when composing.
+ */
+add_task(async function test_compose_type() {
+ // Bare-bones code to initiate composing a message in given type.
+ let createCompose = function (type) {
+ let msgCompose = Cc[
+ "@mozilla.org/messengercompose/compose;1"
+ ].createInstance(Ci.nsIMsgCompose);
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ params.type = type;
+ msgCompose.initialize(params);
+ };
+ const histogram = TelemetryTestUtils.getAndClearHistogram("TB_COMPOSE_TYPE");
+
+ // Start composing arbitrary numbers of messages in each format.
+ const NUM_NEW = 4;
+ const NUM_DRAFT = 7;
+ const NUM_EDIT_TEMPLATE = 3;
+ for (let i = 0; i < NUM_NEW; i++) {
+ createCompose(Ci.nsIMsgCompType.New);
+ }
+ for (let i = 0; i < NUM_DRAFT; i++) {
+ createCompose(Ci.nsIMsgCompType.Draft);
+ }
+ for (let i = 0; i < NUM_EDIT_TEMPLATE; i++) {
+ createCompose(Ci.nsIMsgCompType.EditTemplate);
+ }
+
+ // Did we count them correctly?
+ const snapshot = histogram.snapshot();
+ Assert.equal(
+ snapshot.values[Ci.nsIMsgCompType.New],
+ NUM_NEW,
+ "nsIMsgCompType.New count must be correct"
+ );
+ Assert.equal(
+ snapshot.values[Ci.nsIMsgCompType.Draft],
+ NUM_DRAFT,
+ "nsIMsgCompType.Draft count must be correct"
+ );
+ Assert.equal(
+ snapshot.values[Ci.nsIMsgCompType.EditTemplate],
+ NUM_EDIT_TEMPLATE,
+ "nsIMsgCompType.EditTemplate count must be correct"
+ );
+});
diff --git a/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js b/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js
new file mode 100644
index 0000000000..6a350ac64e
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+/*
+ * Test that temporary files for draft are surely removed.
+ */
+
+var gMsgCompose;
+var gExpectedFiles;
+
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ do_timeout(0, checkResult);
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+/**
+ * Get the count of temporary files. Because nsIFile.createUnique creates a random
+ * file name, we iterate the tmp dir and count the files that match filename
+ * patterns.
+ */
+async function getTemporaryFilesCount() {
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+ let entries = await IOUtils.getChildren(tmpDir);
+ let tempFiles = {
+ "nsmail.tmp": 0,
+ "nscopy.tmp": 0,
+ "nsemail.eml": 0,
+ "nsemail.tmp": 0,
+ "nsqmail.tmp": 0,
+ };
+ for (const path of entries) {
+ for (let pattern of Object.keys(tempFiles)) {
+ let [name, extName] = pattern.split(".");
+ if (PathUtils.filename(path).startsWith(name) && path.endsWith(extName)) {
+ tempFiles[pattern]++;
+ }
+ }
+ }
+ return tempFiles;
+}
+
+/**
+ * Temp files should be deleted as soon as the draft is finished saving, so the
+ * counts should be the same as before.
+ */
+async function checkResult() {
+ let filesCount = await getTemporaryFilesCount();
+ for (let [pattern, count] of Object.entries(filesCount)) {
+ Assert.equal(
+ count,
+ gExpectedFiles[pattern],
+ `${pattern} should not exists`
+ );
+ }
+ do_test_finished();
+}
+
+add_task(async function () {
+ gExpectedFiles = await getTemporaryFilesCount();
+
+ // Ensure we have at least one mail account
+ localAccountUtils.loadLocalMailAccount();
+
+ gMsgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ fields.from = "Nobody <nobody@tinderbox.test>";
+ fields.body = "body text";
+ fields.useMultipartAlternative = true;
+
+ params.composeFields = fields;
+ params.format = Ci.nsIMsgCompFormat.HTML;
+
+ gMsgCompose.initialize(params, null, null);
+
+ let identity = getSmtpIdentity(null, getBasicSmtpServer());
+
+ localAccountUtils.rootFolder.createLocalSubfolder("Drafts");
+
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ progress.registerListener(progressListener);
+
+ do_test_pending();
+
+ gMsgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ identity,
+ "",
+ null,
+ progress
+ );
+});
diff --git a/comm/mailnews/compose/test/unit/xpcshell.ini b/comm/mailnews/compose/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..687259c42a
--- /dev/null
+++ b/comm/mailnews/compose/test/unit/xpcshell.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+head = head_compose.js
+tail =
+support-files = data/*
+
+[test_accountKey.js]
+[test_attachment.js]
+[test_attachment_intl.js]
+[test_autoReply.js]
+skip-if = os == 'mac'
+[test_bcc.js]
+[test_bug155172.js]
+[test_bug474774.js]
+[test_createAndSendMessage.js]
+[test_createRFC822Message.js]
+[test_detectAttachmentCharset.js]
+[test_expandMailingLists.js]
+[test_fcc2.js]
+[test_fccReply.js]
+[test_longLines.js]
+[test_mailTelemetry.js]
+[test_mailtoURL.js]
+[test_messageBody.js]
+[test_messageHeaders.js]
+[test_nsIMsgCompFields.js]
+[test_nsMsgCompose1.js]
+[test_nsMsgCompose2.js]
+[test_nsMsgCompose3.js]
+[test_nsSmtpService1.js]
+[test_saveDraft.js]
+[test_sendBackground.js]
+[test_sendMailAddressIDN.js]
+[test_sendMailMessage.js]
+[test_sendMessageFile.js]
+[test_sendMessageLater.js]
+[test_sendMessageLater2.js]
+[test_sendMessageLater3.js]
+[test_sendObserver.js]
+[test_smtp8bitMime.js]
+[test_smtpAuthMethods.js]
+[test_smtpClient.js]
+[test_smtpPassword.js]
+[test_smtpPassword2.js]
+[test_smtpPasswordFailure1.js]
+[test_smtpPasswordFailure2.js]
+[test_smtpPasswordFailure3.js]
+[test_smtpProtocols.js]
+[test_smtpProxy.js]
+[test_smtpServer.js]
+[test_smtpURL.js]
+[test_splitRecipients.js]
+[test_telemetry_compose.js]
+[test_staleTemporaryFileCleanup.js]
+[test_temporaryFilesRemoved.js]
diff --git a/comm/mailnews/db/gloda/.project b/comm/mailnews/db/gloda/.project
new file mode 100644
index 0000000000..08f9557936
--- /dev/null
+++ b/comm/mailnews/db/gloda/.project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>gloda</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ </buildSpec>
+ <natures>
+ </natures>
+</projectDescription>
diff --git a/comm/mailnews/db/gloda/components/GlodaAutoComplete.jsm b/comm/mailnews/db/gloda/components/GlodaAutoComplete.jsm
new file mode 100644
index 0000000000..98f67eadda
--- /dev/null
+++ b/comm/mailnews/db/gloda/components/GlodaAutoComplete.jsm
@@ -0,0 +1,576 @@
+/* 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/. */
+
+/**
+ * glautocomp.js decides which autocomplete item type to
+ * use when one enters text in global search box. There are
+ * following types of autocomplete item: gloda-contact-chunk-richlistitem,
+ * gloda-fulltext-all-richlistitem, gloda-fulltext-single-richlistitem, gloda-multi-richlistitem,
+ * gloda-single-identity-richlistitem, gloda-single-tag-richlistitem.
+ */
+
+var EXPORTED_SYMBOLS = ["GlodaAutoComplete"];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+var Gloda = null;
+var MultiSuffixTree = null;
+var TagNoun = null;
+var FreeTagNoun = null;
+
+function ResultRowFullText(aItem, words, typeForStyle) {
+ this.item = aItem;
+ this.words = words;
+ this.typeForStyle = "gloda-fulltext-" + typeForStyle + "-richlistitem";
+}
+ResultRowFullText.prototype = {
+ multi: false,
+ fullText: true,
+};
+
+function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
+ this.nounID = aExplicitNounID || aItem.NOUN_ID;
+ this.nounDef = Gloda._nounIDToDef[this.nounID];
+ this.criteriaType = aCriteriaType;
+ this.criteria = aCriteria;
+ this.item = aItem;
+ this.typeForStyle = "gloda-single-" + this.nounDef.name + "-richlistitem";
+}
+ResultRowSingle.prototype = {
+ multi: false,
+ fullText: false,
+};
+
+function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
+ this.nounID = aNounID;
+ this.nounDef = Gloda._nounIDToDef[aNounID];
+ this.criteriaType = aCriteriaType;
+ this.criteria = aCriteria;
+ this.collection = aQuery.getCollection(this);
+ this.collection.becomeExplicit();
+ this.renderer = null;
+}
+ResultRowMulti.prototype = {
+ multi: true,
+ typeForStyle: "gloda-multi-richlistitem",
+ fullText: false,
+ onItemsAdded(aItems) {
+ if (this.renderer) {
+ for (let [, item] of aItems.entries()) {
+ this.renderer.renderItem(item);
+ }
+ }
+ },
+ onItemsModified(aItems) {},
+ onItemsRemoved(aItems) {},
+ onQueryCompleted() {},
+};
+
+function nsAutoCompleteGlodaResult(aListener, aCompleter, aString) {
+ this.listener = aListener;
+ this.completer = aCompleter;
+ this.searchString = aString;
+ this._results = [];
+ this._pendingCount = 0;
+ this._problem = false;
+ // Track whether we have reported anything to the complete controller so
+ // that we know not to send notifications to it during calls to addRows
+ // prior to that point.
+ this._initiallyReported = false;
+
+ this.wrappedJSObject = this;
+}
+nsAutoCompleteGlodaResult.prototype = {
+ getObjectAt(aIndex) {
+ return this._results[aIndex] || null;
+ },
+ markPending(aCompleter) {
+ this._pendingCount++;
+ },
+ markCompleted(aCompleter) {
+ if (--this._pendingCount == 0 && this.active) {
+ this.listener.onSearchResult(this.completer, this);
+ }
+ },
+ announceYourself() {
+ this._initiallyReported = true;
+ this.listener.onSearchResult(this.completer, this);
+ },
+ addRows(aRows) {
+ if (!aRows.length) {
+ return;
+ }
+ this._results.push.apply(this._results, aRows);
+ if (this._initiallyReported && this.active) {
+ this.listener.onSearchResult(this.completer, this);
+ }
+ },
+ // ==== nsIAutoCompleteResult
+ searchString: null,
+ get searchResult() {
+ if (this._problem) {
+ return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
+ }
+ if (this._results.length) {
+ return !this._pendingCount
+ ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
+ : Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING;
+ }
+ return !this._pendingCount
+ ? Ci.nsIAutoCompleteResult.RESULT_NOMATCH
+ : Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
+ },
+ active: false,
+ defaultIndex: -1,
+ errorDescription: null,
+ get matchCount() {
+ return this._results === null ? 0 : this._results.length;
+ },
+ // this is the lower text, (shows the url in firefox)
+ // we try and show the contact's name here.
+ getValueAt(aIndex) {
+ let thing = this._results[aIndex];
+ return thing.name || thing.value || thing.subject || null;
+ },
+ getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+ // rich uses this to be the "title". it is the upper text
+ // we try and show the identity here.
+ getCommentAt(aIndex) {
+ let thing = this._results[aIndex];
+ if (thing.value) {
+ // identity
+ return thing.contact.name;
+ }
+ return thing.name || thing.subject;
+ },
+ // rich uses this to be the "type"
+ getStyleAt(aIndex) {
+ let row = this._results[aIndex];
+ return row.typeForStyle;
+ },
+ // rich uses this to be the icon
+ getImageAt(aIndex) {
+ let thing = this._results[aIndex];
+ if (!thing.value) {
+ return null;
+ }
+
+ return ""; // we don't want to use gravatars as is.
+ /*
+ let md5hash = GlodaUtils.md5HashString(thing.value);
+ let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
+ "?d=identicon&s=32&r=g";
+ return gravURL;
+ */
+ },
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+ removeValueAt() {},
+ _stop() {},
+};
+
+var MAX_POPULAR_CONTACTS = 200;
+
+/**
+ * Complete contacts/identities based on name/email. Instant phase is based on
+ * a suffix-tree built of popular contacts/identities. Delayed phase relies
+ * on a LIKE search of all known contacts.
+ */
+function ContactIdentityCompleter() {
+ // get all the contacts
+ let contactQuery = Gloda.newQuery(GlodaConstants.NOUN_CONTACT);
+ contactQuery.orderBy("-popularity").limit(MAX_POPULAR_CONTACTS);
+ this.contactCollection = contactQuery.getCollection(this, null);
+ this.contactCollection.becomeExplicit();
+}
+ContactIdentityCompleter.prototype = {
+ _popularitySorter(a, b) {
+ return b.popularity - a.popularity;
+ },
+ complete(aResult, aString) {
+ if (aString.length < 3) {
+ // In CJK, first name or last name is sometime used as 1 character only.
+ // So we allow autocompleted search even if 1 character.
+ //
+ // [U+3041 - U+9FFF ... Full-width Katakana, Hiragana
+ // and CJK Ideograph
+ // [U+AC00 - U+D7FF ... Hangul
+ // [U+F900 - U+FFDC ... CJK compatibility ideograph
+ if (!aString.match(/[\u3041-\u9fff\uac00-\ud7ff\uf900-\uffdc]/)) {
+ return false;
+ }
+ }
+
+ let matches;
+ if (this.suffixTree) {
+ matches = this.suffixTree.findMatches(aString.toLowerCase());
+ } else {
+ matches = [];
+ }
+
+ // let's filter out duplicates due to identity/contact double-hits by
+ // establishing a map based on the contact id for these guys.
+ // let's also favor identities as we do it, because that gets us the
+ // most accurate gravat, potentially
+ let contactToThing = {};
+ for (let iMatch = 0; iMatch < matches.length; iMatch++) {
+ let thing = matches[iMatch];
+ if (
+ thing.NOUN_ID == GlodaConstants.NOUN_CONTACT &&
+ !(thing.id in contactToThing)
+ ) {
+ contactToThing[thing.id] = thing;
+ } else if (thing.NOUN_ID == GlodaConstants.NOUN_IDENTITY) {
+ contactToThing[thing.contactID] = thing;
+ }
+ }
+ // and since we can now map from contacts down to identities, map contacts
+ // to the first identity for them that we find...
+ matches = Object.keys(contactToThing)
+ .map(id => contactToThing[id])
+ .map(val =>
+ val.NOUN_ID == GlodaConstants.NOUN_IDENTITY ? val : val.identities[0]
+ );
+
+ let rows = matches.map(
+ match => new ResultRowSingle(match, "text", aResult.searchString)
+ );
+ aResult.addRows(rows);
+
+ // - match against database contacts / identities
+ let pending = { contactToThing, pendingCount: 2 };
+
+ let contactQuery = Gloda.newQuery(GlodaConstants.NOUN_CONTACT);
+ contactQuery.nameLike(
+ contactQuery.WILDCARD,
+ aString,
+ contactQuery.WILDCARD
+ );
+ pending.contactColl = contactQuery.getCollection(this, aResult);
+ pending.contactColl.becomeExplicit();
+
+ let identityQuery = Gloda.newQuery(GlodaConstants.NOUN_IDENTITY);
+ identityQuery
+ .kind("email")
+ .valueLike(identityQuery.WILDCARD, aString, identityQuery.WILDCARD);
+ pending.identityColl = identityQuery.getCollection(this, aResult);
+ pending.identityColl.becomeExplicit();
+
+ aResult._contactCompleterPending = pending;
+
+ return true;
+ },
+ onItemsAdded(aItems, aCollection) {},
+ onItemsModified(aItems, aCollection) {},
+ onItemsRemoved(aItems, aCollection) {},
+ onQueryCompleted(aCollection) {
+ // handle the initial setup case...
+ if (aCollection.data == null) {
+ // cheat and explicitly add our own contact...
+ if (
+ Gloda.myContact &&
+ !(Gloda.myContact.id in this.contactCollection._idMap)
+ ) {
+ this.contactCollection._onItemsAdded([Gloda.myContact]);
+ }
+
+ // the set of identities owned by the contacts is automatically loaded as part
+ // of the contact loading...
+ // (but only if we actually have any contacts)
+ this.identityCollection =
+ this.contactCollection.subCollections[GlodaConstants.NOUN_IDENTITY];
+
+ let contactNames = this.contactCollection.items.map(
+ c => c.name.replace(" ", "").toLowerCase() || "x"
+ );
+ // if we had no contacts, we will have no identity collection!
+ let identityMails;
+ if (this.identityCollection) {
+ identityMails = this.identityCollection.items.map(i =>
+ i.value.toLowerCase()
+ );
+ }
+
+ // The suffix tree takes two parallel lists; the first contains strings
+ // while the second contains objects that correspond to those strings.
+ // In the degenerate case where identityCollection does not exist, it will
+ // be undefined. Calling concat with an argument of undefined simply
+ // duplicates the list we called concat on, and is thus harmless. Our
+ // use of && on identityCollection allows its undefined value to be
+ // passed through to concat. identityMails will likewise be undefined.
+ this.suffixTree = new MultiSuffixTree(
+ contactNames.concat(identityMails),
+ this.contactCollection.items.concat(
+ this.identityCollection && this.identityCollection.items
+ )
+ );
+
+ return;
+ }
+
+ // handle the completion case
+ let result = aCollection.data;
+ let pending = result._contactCompleterPending;
+
+ if (--pending.pendingCount == 0) {
+ let possibleDudes = [];
+
+ let contactToThing = pending.contactToThing;
+
+ let items;
+
+ // check identities first because they are better than contacts in terms
+ // of display
+ items = pending.identityColl.items;
+ for (let iIdentity = 0; iIdentity < items.length; iIdentity++) {
+ let identity = items[iIdentity];
+ if (!(identity.contactID in contactToThing)) {
+ contactToThing[identity.contactID] = identity;
+ possibleDudes.push(identity);
+ // augment the identity with its contact's popularity
+ identity.popularity = identity.contact.popularity;
+ }
+ }
+ items = pending.contactColl.items;
+ for (let iContact = 0; iContact < items.length; iContact++) {
+ let contact = items[iContact];
+ if (!(contact.id in contactToThing)) {
+ contactToThing[contact.id] = contact;
+ possibleDudes.push(contact.identities[0]);
+ }
+ }
+
+ // sort in order of descending popularity
+ possibleDudes.sort(this._popularitySorter);
+ let rows = possibleDudes.map(
+ dude => new ResultRowSingle(dude, "text", result.searchString)
+ );
+ result.addRows(rows);
+ result.markCompleted(this);
+
+ // the collections no longer care about the result, make it clear.
+ delete pending.identityColl.data;
+ delete pending.contactColl.data;
+ // the result object no longer needs us or our data
+ delete result._contactCompleterPending;
+ }
+ },
+};
+
+/**
+ * Complete tags that are used on contacts.
+ */
+function ContactTagCompleter() {
+ FreeTagNoun.populateKnownFreeTags();
+ this._buildSuffixTree();
+ FreeTagNoun.addListener(this);
+}
+ContactTagCompleter.prototype = {
+ _buildSuffixTree() {
+ let tagNames = [],
+ tags = [];
+ for (let [tagName, tag] of Object.entries(FreeTagNoun.knownFreeTags)) {
+ tagNames.push(tagName.toLowerCase());
+ tags.push(tag);
+ }
+ this._suffixTree = new MultiSuffixTree(tagNames, tags);
+ this._suffixTreeDirty = false;
+ },
+ onFreeTagAdded(aTag) {
+ this._suffixTreeDirty = true;
+ },
+ complete(aResult, aString) {
+ // now is not the best time to do this; have onFreeTagAdded use a timer.
+ if (this._suffixTreeDirty) {
+ this._buildSuffixTree();
+ }
+
+ if (aString.length < 2) {
+ // No async mechanism that will add new rows.
+ return false;
+ }
+
+ let tags = this._suffixTree.findMatches(aString.toLowerCase());
+ let rows = [];
+ for (let tag of tags) {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_CONTACT);
+ query.freeTags(tag);
+ let resRow = new ResultRowMulti(
+ GlodaConstants.NOUN_CONTACT,
+ "tag",
+ tag.name,
+ query
+ );
+ rows.push(resRow);
+ }
+ aResult.addRows(rows);
+
+ return false; // no async mechanism that will add new rows
+ },
+};
+
+/**
+ * Complete tags that are used on messages
+ */
+function MessageTagCompleter() {
+ this._buildSuffixTree();
+}
+MessageTagCompleter.prototype = {
+ _buildSuffixTree() {
+ let tagNames = [],
+ tags = [];
+ let tagArray = TagNoun.getAllTags();
+ for (let iTag = 0; iTag < tagArray.length; iTag++) {
+ let tag = tagArray[iTag];
+ tagNames.push(tag.tag.toLowerCase());
+ tags.push(tag);
+ }
+ this._suffixTree = new MultiSuffixTree(tagNames, tags);
+ this._suffixTreeDirty = false;
+ },
+ complete(aResult, aString) {
+ if (aString.length < 2) {
+ return false;
+ }
+
+ let tags = this._suffixTree.findMatches(aString.toLowerCase());
+ let rows = [];
+ for (let tag of tags) {
+ let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
+ rows.push(resRow);
+ }
+ aResult.addRows(rows);
+
+ return false; // no async mechanism that will add new rows
+ },
+};
+
+/**
+ * Complete with helpful hints about full-text search
+ */
+function FullTextCompleter() {}
+FullTextCompleter.prototype = {
+ complete(aResult, aSearchString) {
+ if (aSearchString.length < 4) {
+ return false;
+ }
+ // We use code very similar to that in GlodaMsgSearcher.jsm, except that we
+ // need to detect when we found phrases, as well as strip commas.
+ aSearchString = aSearchString.trim();
+ let terms = [];
+ let phraseFound = false;
+ while (aSearchString) {
+ let term = "";
+ if (aSearchString.startsWith('"')) {
+ let endIndex = aSearchString.indexOf(aSearchString[0], 1);
+ // eat the quote if it has no friend
+ if (endIndex == -1) {
+ aSearchString = aSearchString.substring(1);
+ continue;
+ }
+ phraseFound = true;
+ term = aSearchString.substring(1, endIndex).trim();
+ if (term) {
+ terms.push(term);
+ }
+ aSearchString = aSearchString.substring(endIndex + 1);
+ continue;
+ }
+
+ let spaceIndex = aSearchString.indexOf(" ");
+ if (spaceIndex == -1) {
+ terms.push(aSearchString.replace(/,/g, ""));
+ break;
+ }
+
+ term = aSearchString.substring(0, spaceIndex).replace(/,/g, "");
+ if (term) {
+ terms.push(term);
+ }
+ aSearchString = aSearchString.substring(spaceIndex + 1);
+ }
+
+ if (terms.length == 1 && !phraseFound) {
+ aResult.addRows([new ResultRowFullText(aSearchString, terms, "single")]);
+ } else {
+ aResult.addRows([new ResultRowFullText(aSearchString, terms, "all")]);
+ }
+
+ return false; // no async mechanism that will add new rows
+ },
+};
+
+function GlodaAutoComplete() {
+ this.wrappedJSObject = this;
+ try {
+ // set up our awesome globals!
+ if (Gloda === null) {
+ let loadNS = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaPublic.jsm"
+ );
+ Gloda = loadNS.Gloda;
+
+ loadNS = ChromeUtils.import("resource:///modules/gloda/GlodaUtils.jsm");
+ loadNS = ChromeUtils.import("resource:///modules/gloda/SuffixTree.jsm");
+ MultiSuffixTree = loadNS.MultiSuffixTree;
+ loadNS = ChromeUtils.import("resource:///modules/gloda/NounTag.jsm");
+ TagNoun = loadNS.TagNoun;
+ loadNS = ChromeUtils.import("resource:///modules/gloda/NounFreetag.jsm");
+ FreeTagNoun = loadNS.FreeTagNoun;
+ }
+
+ this.completers = [];
+ this.curResult = null;
+
+ this.completers.push(new FullTextCompleter()); // not async.
+ this.completers.push(new ContactIdentityCompleter()); // potentially async.
+ this.completers.push(new ContactTagCompleter()); // not async.
+ this.completers.push(new MessageTagCompleter()); // not async.
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+GlodaAutoComplete.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+
+ startSearch(aString, aParam, aResult, aListener) {
+ try {
+ let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
+ // save this for hacky access to the search. I somewhat suspect we simply
+ // should not be using the formal autocomplete mechanism at all.
+ // Used in glodacomplete.xml.
+ this.curResult = result;
+
+ // Guard against late async results being sent.
+ this.curResult.active = true;
+
+ if (aParam == "global") {
+ for (let completer of this.completers) {
+ // they will return true if they have something pending.
+ if (completer.complete(result, aString)) {
+ result.markPending(completer);
+ }
+ }
+ // } else {
+ // It'd be nice to do autocomplete in the quicksearch modes based
+ // on the specific values for that mode in the current view.
+ // But we don't do that yet.
+ }
+
+ result.announceYourself();
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ stopSearch() {
+ this.curResult.active = false;
+ },
+};
diff --git a/comm/mailnews/db/gloda/components/MimeMessageEmitter.jsm b/comm/mailnews/db/gloda/components/MimeMessageEmitter.jsm
new file mode 100644
index 0000000000..0ee1737f16
--- /dev/null
+++ b/comm/mailnews/db/gloda/components/MimeMessageEmitter.jsm
@@ -0,0 +1,501 @@
+/* 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 EXPORTED_SYMBOLS = ["MimeMessageEmitter"];
+
+var kStateUnknown = 0;
+var kStateInHeaders = 1;
+var kStateInBody = 2;
+var kStateInAttachment = 3;
+
+/**
+ * When the saneBodySize flag is active, limit body parts to at most this many
+ * bytes. See |MsgHdrToMimeMessage| for more information on the flag.
+ *
+ * The choice of 20k was made on the very scientific basis of running a query
+ * against my indexed e-mail and finding the point where these things taper
+ * off. I chose 20 because things had tapered off pretty firmly by 16, so
+ * 20 gave it some space and it was also the end of a mini-plateau.
+ */
+var MAX_SANE_BODY_PART_SIZE = 20 * 1024;
+
+/**
+ * Custom nsIMimeEmitter to build a sub-optimal javascript representation of a
+ * MIME message. The intent is that a better mechanism than is evolved to
+ * provide a javascript-accessible representation of the message.
+ *
+ * Processing occurs in two passes. During the first pass, libmime is parsing
+ * the stream it is receiving, and generating header and body events for all
+ * MimeMessage instances it encounters. This provides us with the knowledge
+ * of each nested message in addition to the top level message, their headers
+ * and sort-of their bodies. The sort-of is that we may get more than
+ * would normally be displayed in cases involving multipart/alternatives.
+ * We have augmented libmime to have a notify_nested_options parameter which
+ * is enabled when we are the consumer. This option causes MimeMultipart to
+ * always emit a content-type header (via addHeaderField), defaulting to
+ * text/plain when an explicit value is not present. Additionally,
+ * addHeaderField is called with a custom "x-jsemitter-part-path" header with
+ * the value being the part path (ex: 1.2.2). Having the part path greatly
+ * simplifies our life for building the part hierarchy.
+ * During the second pass, the libmime object model is traversed, generating
+ * attachment notifications for all leaf nodes. From our perspective, this
+ * means file attachments and embedded messages (message/rfc822). We use this
+ * pass to create the attachment objects proper, which we then substitute into
+ * the part tree we have already built.
+ */
+function MimeMessageEmitter() {
+ this._mimeMsg = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+ );
+ this._utils = ChromeUtils.import("resource:///modules/gloda/GlodaUtils.jsm");
+
+ this._url = null;
+ this._partRE = this._utils.GlodaUtils.PART_RE;
+
+ this._outputListener = null;
+
+ this._curPart = null;
+ this._curAttachment = null;
+ this._partMap = {};
+ this._bogusPartTranslation = {};
+
+ this._state = kStateUnknown;
+
+ this._writeBody = false;
+}
+
+var deathToNewlines = /\n/g;
+
+MimeMessageEmitter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMimeEmitter"]),
+
+ initialize(aUrl, aChannel, aFormat) {
+ this._url = aUrl;
+ this._curPart = new this._mimeMsg.MimeMessage();
+ // the partName is intentionally ""! not a place-holder!
+ this._curPart.partName = "";
+ this._curAttachment = "";
+ this._partMap[""] = this._curPart;
+
+ // pull options across...
+ let options = this._mimeMsg.MsgHdrToMimeMessage.OPTION_TUNNEL;
+ this._saneBodySize =
+ options && "saneBodySize" in options ? options.saneBodySize : false;
+
+ this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
+ this._curPart;
+ },
+
+ complete() {
+ this._url = null;
+
+ this._outputListener = null;
+
+ this._curPart = null;
+ this._curAttachment = null;
+ this._partMap = null;
+ this._bogusPartTranslation = null;
+ },
+
+ setPipe(aInputStream, aOutputStream) {
+ // we do not care about these
+ },
+ set outputListener(aListener) {
+ this._outputListener = aListener;
+ },
+ get outputListener() {
+ return this._outputListener;
+ },
+
+ _stripParams(aValue) {
+ let indexSemi = aValue.indexOf(";");
+ if (indexSemi >= 0) {
+ aValue = aValue.substring(0, indexSemi);
+ }
+ return aValue;
+ },
+
+ _beginPayload(aContentType) {
+ let contentTypeNoParams = this._stripParams(aContentType).toLowerCase();
+ if (
+ contentTypeNoParams == "text/plain" ||
+ contentTypeNoParams == "text/html" ||
+ contentTypeNoParams == "text/enriched"
+ ) {
+ this._curPart = new this._mimeMsg.MimeBody(contentTypeNoParams);
+ this._writeBody = true;
+ } else if (contentTypeNoParams == "message/rfc822") {
+ // startHeader will take care of this
+ this._curPart = new this._mimeMsg.MimeMessage();
+ // do not fall through into the content-type setting case; this
+ // content-type needs to get clobbered by the actual content-type of
+ // the enclosed message.
+ this._writeBody = false;
+ return;
+ } else if (contentTypeNoParams.startsWith("multipart/")) {
+ // this is going to fall-down with TNEF encapsulation and such, we really
+ // need to just be consuming the object model.
+ this._curPart = new this._mimeMsg.MimeContainer(contentTypeNoParams);
+ this._writeBody = false;
+ } else {
+ this._curPart = new this._mimeMsg.MimeUnknown(contentTypeNoParams);
+ this._writeBody = false;
+ }
+ // put the full content-type in the headers and normalize out any newlines
+ this._curPart.headers["content-type"] = [
+ aContentType.replace(deathToNewlines, ""),
+ ];
+ },
+
+ // ----- Header Routines
+ /**
+ * StartHeader provides the base case for our processing. It is the first
+ * notification we receive when processing begins on the outer rfc822
+ * message. We do not receive an x-jsemitter-part-path notification for the
+ * message, but the aIsRootMailHeader tells us everything we need to know.
+ * (Or it would if we hadn't already set everything up in initialize.)
+ *
+ * When dealing with nested RFC822 messages, we will receive the
+ * addHeaderFields for the content-type and the x-jsemitter-part-path
+ * prior to the startHeader call. This is because the MIME multipart
+ * container that holds the message is the one generating the notification.
+ * For that reason, we do not process them here, but instead in
+ * addHeaderField and _beginPayload.
+ *
+ * We do need to track our state for addHeaderField's benefit though.
+ */
+ startHeader(aIsRootMailHeader, aIsHeaderOnly, aMsgID, aOutputCharset) {
+ this._state = kStateInHeaders;
+ },
+ /**
+ * Receives a header field name and value for the current MIME part, which
+ * can be an rfc822/message or one of its sub-parts.
+ *
+ * The emitter architecture treats rfc822/messages as special because it was
+ * architected around presentation. In that case, the organizing concept
+ * is the single top-level rfc822/message. (It did not 'look into' nested
+ * messages in most cases.)
+ * As a result the interface is biased towards being 'in the headers' or
+ * 'in the body', corresponding to calls to startHeader and startBody,
+ * respectively.
+ * This information is interesting to us because the message itself is an
+ * odd pseudo-mime-part. Because it has only one child, its headers are,
+ * in a way, its payload, but they also serve as the description of its
+ * MIME child part. This introduces a complication in that we see the
+ * content-type for the message's "body" part before we actually see any
+ * of the headers. To deal with this, we punt on the construction of the
+ * body part to the call to startBody() and predicate our logic on the
+ * _state field.
+ */
+ addHeaderField(aField, aValue) {
+ if (this._state == kStateInBody) {
+ aField = aField.toLowerCase();
+ if (aField == "content-type") {
+ this._beginPayload(aValue, true);
+ } else if (aField == "x-jsemitter-part-path") {
+ // This is either naming the current part, or referring to an already
+ // existing part (in the case of multipart/related on its second pass).
+ // As such, check if the name already exists in our part map.
+ let partName = this._stripParams(aValue);
+ // if it does, then make the already-existing part at that path current
+ if (partName in this._partMap) {
+ this._curPart = this._partMap[partName];
+ this._writeBody = "body" in this._curPart;
+ } else {
+ // otherwise, name the part we are holding onto and place it.
+ this._curPart.partName = partName;
+ this._placePart(this._curPart);
+ }
+ } else if (aField == "x-jsemitter-encrypted" && aValue == "1") {
+ this._curPart.isEncrypted = true;
+ }
+ // There is no other field to be emitted in the body case other than the
+ // ones we just handled. (They were explicitly added for the js
+ // emitter.)
+ } else if (this._state == kStateInHeaders) {
+ let lowerField = aField.toLowerCase();
+ if (lowerField in this._curPart.headers) {
+ this._curPart.headers[lowerField].push(aValue);
+ } else {
+ this._curPart.headers[lowerField] = [aValue];
+ }
+ }
+ },
+ addAllHeaders(aAllHeaders, aHeaderSize) {
+ // This is called by the parsing code after the calls to AddHeaderField (or
+ // AddAttachmentField if the part is an attachment), and seems to serve
+ // a specialized, quasi-redundant purpose. (nsMimeBaseEmitter creates a
+ // nsIMimeHeaders instance and hands it to the nsIMsgMailNewsUrl.)
+ // nop
+ },
+ writeHTMLHeaders(aName) {
+ // It doesn't look like this should even be part of the interface; I think
+ // only the nsMimeHtmlDisplayEmitter::EndHeader call calls this signature.
+ // nop
+ },
+ endHeader(aName) {},
+ updateCharacterSet(aCharset) {
+ // we do not need to worry about this. it turns out this notification is
+ // exclusively for the benefit of the UI. libmime, believe it or not,
+ // is actually doing the right thing under the hood and handles all the
+ // encoding issues for us.
+ // so, get ready for the only time you will ever hear this:
+ // three cheers for libmime!
+ },
+
+ /**
+ * Place a part in its proper location; requires the parent to be present.
+ * However, we no longer require in-order addition of children. (This is
+ * currently a hedge against extension code doing wacky things. Our
+ * motivating use-case is multipart/related which actually does generate
+ * everything in order on its first pass, but has a wacky second pass. It
+ * does not actually trigger the out-of-order code because we have
+ * augmented the libmime code to generate its x-jsemitter-part-path info
+ * a second time, in which case we reuse the part we already created.)
+ *
+ * @param aPart Part to place.
+ */
+ _placePart(aPart) {
+ let partName = aPart.partName;
+ this._partMap[partName] = aPart;
+
+ let [storagePartName, , parentPart] = this._findOrCreateParent(partName);
+ let lastDotIndex = storagePartName.lastIndexOf(".");
+ if (parentPart !== undefined) {
+ let indexInParent =
+ parseInt(storagePartName.substring(lastDotIndex + 1)) - 1;
+ // handle out-of-order notification...
+ if (indexInParent < parentPart.parts.length) {
+ parentPart.parts[indexInParent] = aPart;
+ } else {
+ while (indexInParent > parentPart.parts.length) {
+ parentPart.parts.push(null);
+ }
+ parentPart.parts.push(aPart);
+ }
+ }
+ },
+
+ /**
+ * In case the MIME structure is wrong, (i.e. we have no parent to add the
+ * current part to), this function recursively makes sure we create the
+ * missing bits in the hierarchy.
+ * What happens in the case of encrypted emails (mimecryp.cpp):
+ * 1. is the message
+ * 1.1 doesn't exist
+ * 1.1.1 is the multipart/alternative that holds the text/plain and text/html
+ * 1.1.1.1 is text/plain
+ * 1.1.1.2 is text/html
+ * This function fills the missing bits.
+ */
+ _findOrCreateParent(aPartName) {
+ let partName = aPartName + "";
+ let parentName = partName.substring(0, partName.lastIndexOf("."));
+ let parentPart;
+ if (parentName in this._partMap) {
+ parentPart = this._partMap[parentName];
+ let lastDotIndex = partName.lastIndexOf(".");
+ let indexInParent = parseInt(partName.substring(lastDotIndex + 1)) - 1;
+ if (
+ "parts" in parentPart &&
+ indexInParent == parentPart.parts.length - 1
+ ) {
+ return [partName, parentName, parentPart];
+ }
+ return this._findAnotherContainer(aPartName);
+ }
+
+ // Find the grandparent
+ let [, , grandParentPart] = this._findOrCreateParent(parentName);
+ // Create the missing part.
+ parentPart = new this._mimeMsg.MimeContainer("multipart/fake-container");
+ // Add it to the grandparent, remember we added it in the hierarchy.
+ grandParentPart.parts.push(parentPart);
+ this._partMap[parentName] = parentPart;
+ return [partName, parentName, parentPart];
+ },
+
+ /**
+ * In the case of UUEncoded attachments, libmime tells us about the attachment
+ * as a child of a MimeBody. This obviously doesn't make us happy, so in case
+ * libmime wants us to attach an attachment to something that's not a
+ * container, we walk up the mime tree to find a suitable container to hold
+ * the attachment.
+ * The results are cached so that they're consistent across calls — this
+ * ensures the call to _replacePart works fine.
+ */
+ _findAnotherContainer(aPartName) {
+ if (aPartName in this._bogusPartTranslation) {
+ return this._bogusPartTranslation[aPartName];
+ }
+
+ let parentName = aPartName + "";
+ let parentPart;
+ while (!(parentPart && "parts" in parentPart) && parentName.length) {
+ parentName = parentName.substring(0, parentName.lastIndexOf("."));
+ parentPart = this._partMap[parentName];
+ }
+ let childIndex = parentPart.parts.length;
+ let fallbackPartName =
+ (parentName ? parentName + "." : "") + (childIndex + 1);
+ return (this._bogusPartTranslation[aPartName] = [
+ fallbackPartName,
+ parentName,
+ parentPart,
+ ]);
+ },
+
+ /**
+ * In the case of attachments, we need to replace an existing part with a
+ * more representative part...
+ *
+ * @param aPart Part to place.
+ */
+ _replacePart(aPart) {
+ // _partMap always maps the libmime names to parts
+ let partName = aPart.partName;
+ this._partMap[partName] = aPart;
+
+ let [storagePartName, , parentPart] = this._findOrCreateParent(partName);
+
+ let childNamePart = storagePartName.substring(
+ storagePartName.lastIndexOf(".") + 1
+ );
+ let childIndex = parseInt(childNamePart) - 1;
+
+ // The attachment has been encapsulated properly in a MIME part (most of
+ // the cases). This does not hold for UUencoded-parts for instance (see
+ // test_mime_attachments_size.js for instance).
+ if (childIndex < parentPart.parts.length) {
+ let oldPart = parentPart.parts[childIndex];
+ parentPart.parts[childIndex] = aPart;
+ // copy over information from the original part
+ aPart.parts = oldPart.parts;
+ aPart.headers = oldPart.headers;
+ aPart.isEncrypted = oldPart.isEncrypted;
+ } else {
+ parentPart.parts[childIndex] = aPart;
+ }
+ },
+
+ // ----- Attachment Routines
+ // The attachment processing happens after the initial streaming phase (during
+ // which time we receive the messages, both bodies and headers). Our caller
+ // traverses the libmime child object hierarchy, emitting an attachment for
+ // each leaf object or sub-message.
+ startAttachment(aName, aContentType, aUrl, aIsExternalAttachment) {
+ this._state = kStateInAttachment;
+
+ // we need to strip our magic flags from the URL; this regexp matches all
+ // the specific flags that the jsmimeemitter understands (we abuse the URL
+ // parameters to pass information all the way to here)
+ aUrl = aUrl.replace(
+ /((header=filter|emitter=js|examineEncryptedParts=(true|false)))&?/g,
+ ""
+ );
+ // the url should contain a part= piece that tells us the part name, which
+ // we then use to figure out where to place that part if it's a real
+ // attachment.
+ let partMatch, partName;
+ if (aUrl.startsWith("http") || aUrl.startsWith("file")) {
+ // if we have a remote url, unlike non external mail part urls, it may also
+ // contain query strings starting with ?; PART_RE does not handle this.
+ partMatch = aUrl.match(/[?&]part=[^&]+$/);
+ partMatch = partMatch && partMatch[0];
+ partName = partMatch && partMatch.split("part=")[1];
+ } else {
+ partMatch = this._partRE.exec(aUrl);
+ partName = partMatch && partMatch[1];
+ }
+ this._curAttachment = partName;
+
+ if (aContentType == "message/rfc822") {
+ // we want to offer extension authors a way to see attachments as the
+ // message readers sees them, which means attaching an extra url property
+ // to the part that was already created before
+ if (partName) {
+ // we disguise this MimeMessage into something that can be used as a
+ // MimeAttachment so that it is transparent for the user code
+ this._partMap[partName].url = aUrl;
+ this._partMap[partName].isExternal = aIsExternalAttachment;
+ this._partMap[partName].name = aName;
+ }
+ } else if (partName) {
+ let part = new this._mimeMsg.MimeMessageAttachment(
+ partName,
+ aName,
+ aContentType,
+ aUrl,
+ aIsExternalAttachment
+ );
+ // replace the existing part with the attachment...
+ this._replacePart(part);
+ }
+ },
+ addAttachmentField(aField, aValue) {
+ // What gets passed in here is X-Mozilla-PartURL with a value that
+ // is completely identical to aUrl from the call to startAttachment.
+ // (it's the same variable they use in each case). As such, there is
+ // no reason to handle that here.
+ // However, we also pass information about the size of the attachment, and
+ // that we want to handle
+ if (
+ aField == "X-Mozilla-PartSize" &&
+ this._curAttachment in this._partMap
+ ) {
+ this._partMap[this._curAttachment].size = parseInt(aValue);
+ }
+ },
+ endAttachment() {
+ // don't need to do anything here, since we don't care about the headers.
+ },
+ endAllAttachments() {
+ // nop
+ },
+
+ // ----- Body Routines
+ /**
+ * We don't get an x-jsemitter-part-path for the message body, and we ignored
+ * our body part's content-type in addHeaderField, so this serves as our
+ * notice to set up the part (giving it a name).
+ */
+ startBody(aIsBodyOnly, aMsgID, aOutCharset) {
+ this._state = kStateInBody;
+
+ let subPartName =
+ this._curPart.partName == "" ? "1" : this._curPart.partName + ".1";
+ this._beginPayload(this._curPart.get("content-type", "text/plain"));
+ this._curPart.partName = subPartName;
+ this._placePart(this._curPart);
+ },
+
+ /**
+ * Write to the body. When saneBodySize is active, we stop adding if we are
+ * already at the limit for this body part.
+ */
+ writeBody(aBuf, aSize, aOutAmountWritten) {
+ if (
+ this._writeBody &&
+ (!this._saneBodySize || this._curPart.size < MAX_SANE_BODY_PART_SIZE)
+ ) {
+ this._curPart.appendBody(aBuf);
+ }
+ },
+
+ endBody() {},
+
+ // ----- Generic Write (confusing)
+ // (binary data writing...)
+ write(aBuf, aSize, aOutAmountWritten) {
+ // we don't actually ever get called because we don't have the attachment
+ // binary payloads pass through us, but we do the following just in case
+ // we did get called (otherwise the caller gets mad and throws exceptions).
+ aOutAmountWritten.value = aSize;
+ },
+
+ // (string writing)
+ utilityWrite(aBuf) {
+ this.write(aBuf, aBuf.length, {});
+ },
+};
diff --git a/comm/mailnews/db/gloda/components/components.conf b/comm/mailnews/db/gloda/components/components.conf
new file mode 100644
index 0000000000..52d2739bcd
--- /dev/null
+++ b/comm/mailnews/db/gloda/components/components.conf
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{3bbe4d77-3f70-4252-9500-bc00c26f476d}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=gloda"],
+ "jsm": "resource:///modules/GlodaAutoComplete.jsm",
+ "constructor": "GlodaAutoComplete",
+ },
+ {
+ "cid": "{8cddbbbc-7ced-46b0-a936-8cddd1928c24}",
+ "contract_ids": [
+ "@mozilla.org/gloda/jsmimeemitter;1",
+ ],
+ "jsm": "resource:///modules/MimeMessageEmitter.jsm",
+ "constructor": "MimeMessageEmitter",
+ "categories": {
+ "mime-emitter": "@mozilla.org/messenger/mimeemitter;1?type=application/x-js-mime-message"
+ },
+ },
+]
diff --git a/comm/mailnews/db/gloda/components/moz.build b/comm/mailnews/db/gloda/components/moz.build
new file mode 100644
index 0000000000..c2f151a815
--- /dev/null
+++ b/comm/mailnews/db/gloda/components/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "GlodaAutoComplete.jsm",
+ "MimeMessageEmitter.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js b/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js
new file mode 100644
index 0000000000..916c6ef5d5
--- /dev/null
+++ b/comm/mailnews/db/gloda/content/autocomplete-richlistitem.js
@@ -0,0 +1,644 @@
+/* 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/. */
+
+"use strict";
+
+/* global MozXULElement, MozElements */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const gGlodaCompleteStrings = Services.strings.createBundle(
+ "chrome://messenger/locale/glodaComplete.properties"
+ );
+
+ /**
+ * The MozGlodacompleteBaseRichlistitem widget is the
+ * abstract base class for all the gloda autocomplete items.
+ *
+ * @abstract
+ * @augments {MozElements.MozRichlistitem}
+ */
+ class MozGlodacompleteBaseRichlistitem extends MozElements.MozRichlistitem {
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this._boundaryCutoff = null;
+ }
+
+ get boundaryCutoff() {
+ if (!this._boundaryCutoff) {
+ this._boundaryCutoff = Services.prefs.getIntPref(
+ "toolkit.autocomplete.richBoundaryCutoff"
+ );
+ }
+ return this._boundaryCutoff;
+ }
+
+ _getBoundaryIndices(aText, aSearchTokens) {
+ // Short circuit for empty search ([""] == "")
+ if (aSearchTokens == "") {
+ return [0, aText.length];
+ }
+
+ // Find which regions of text match the search terms.
+ let regions = [];
+ for (let search of aSearchTokens) {
+ let matchIndex;
+ let startIndex = 0;
+ let searchLen = search.length;
+
+ // Find all matches of the search terms, but stop early for perf.
+ let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff);
+ while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) {
+ // Start the next search from where this one finished.
+ startIndex = matchIndex + searchLen;
+ regions.push([matchIndex, startIndex]);
+ }
+ }
+
+ // Sort the regions by start position then end position.
+ regions = regions.sort(function (a, b) {
+ let start = a[0] - b[0];
+ return start == 0 ? a[1] - b[1] : start;
+ });
+
+ // Generate the boundary indices from each region.
+ let start = 0;
+ let end = 0;
+ let boundaries = [];
+ for (let i = 0; i < regions.length; i++) {
+ // We have a new boundary if the start of the next is past the end.
+ let region = regions[i];
+ if (region[0] > end) {
+ // First index is the beginning of match.
+ boundaries.push(start);
+ // Second index is the beginning of non-match.
+ boundaries.push(end);
+
+ // Track the new region now that we've stored the previous one.
+ start = region[0];
+ }
+
+ // Push back the end index for the current or new region.
+ end = Math.max(end, region[1]);
+ }
+
+ // Add the last region.
+ boundaries.push(start);
+ boundaries.push(end);
+
+ // Put on the end boundary if necessary.
+ if (end < aText.length) {
+ boundaries.push(aText.length);
+ }
+
+ // Skip the first item because it's always 0.
+ return boundaries.slice(1);
+ }
+
+ _getSearchTokens(aSearch) {
+ let search = aSearch.toLowerCase();
+ return search.split(/\s+/);
+ }
+
+ _needsAlternateEmphasis(aText) {
+ for (let i = aText.length - 1; i >= 0; i--) {
+ let charCode = aText.charCodeAt(i);
+ // Arabic, Syriac, Indic languages are likely to have ligatures
+ // that are broken when using the main emphasis styling.
+ if (0x0600 <= charCode && charCode <= 0x109f) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ _setUpDescription(aDescriptionElement, aText) {
+ // Get rid of all previous text.
+ while (aDescriptionElement.hasChildNodes()) {
+ aDescriptionElement.lastChild.remove();
+ }
+
+ // Get the indices that separate match and non-match text.
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(aText, tokens);
+
+ // If we're searching for something that needs alternate emphasis,
+ // we'll need to check the text that we match.
+ let checkAlt = this._needsAlternateEmphasis(search);
+
+ let next;
+ let start = 0;
+ let len = indices.length;
+ // Even indexed boundaries are matches, so skip the 0th if it's empty.
+ for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+ next = indices[i];
+ let text = aText.substr(start, next - start);
+ start = next;
+
+ if (i % 2 == 0) {
+ // Emphasize the text for even indices
+ let span = aDescriptionElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span")
+ );
+ span.className =
+ checkAlt && this._needsAlternateEmphasis(text)
+ ? "ac-emphasize-alt"
+ : "ac-emphasize-text";
+ span.textContent = text;
+ } else {
+ // Otherwise, it's plain text
+ aDescriptionElement.appendChild(document.createTextNode(text));
+ }
+ }
+ }
+
+ _setUpOverflow(aParentBox, aEllipsis) {
+ // Hide the ellipsis in case there's just enough to not underflow.
+ aEllipsis.hidden = true;
+
+ // Start with the parent's width and subtract off its children.
+ let tooltip = [];
+ let children = aParentBox.children;
+ let widthDiff = aParentBox.getBoundingClientRect().width;
+
+ for (let i = 0; i < children.length; i++) {
+ // Only consider a child if it actually takes up space.
+ let childWidth = children[i].getBoundingClientRect().width;
+ if (childWidth > 0) {
+ // Subtract a little less to account for subpixel rounding.
+ widthDiff -= childWidth - 0.5;
+
+ // Add to the tooltip if it's not hidden and has text.
+ let childText = children[i].textContent;
+ if (childText) {
+ tooltip.push(childText);
+ }
+ }
+ }
+
+ // If the children take up more space than the parent.. overflow!
+ if (widthDiff < 0) {
+ // Re-show the ellipsis now that we know it's needed.
+ aEllipsis.hidden = false;
+
+ // Separate text components with a ndash --
+ aParentBox.tooltipText = tooltip.join(" \u2013 ");
+ }
+ }
+
+ _doUnderflow(aName) {
+ // Hide the ellipsis right when we know we're underflowing instead of
+ // waiting for the timeout to trigger the _setUpOverflow calculations.
+ this[aName + "Box"].tooltipText = "";
+ this[aName + "OverflowEllipsis"].hidden = true;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodacompleteBaseRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ /**
+ * The MozGlodaContactChunkRichlistitem widget displays an autocomplete item with
+ * contact chunk: e.g. image, name and description of the contact.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaContactChunkRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ static get inheritedAttributes() {
+ return {
+ "description.ac-comment": "selected",
+ "label.ac-comment": "selected",
+ "description.ac-url-text": "selected",
+ "label.ac-url-text": "selected",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-contact-chunk-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <vbox>
+ <hbox>
+ <hbox class="ac-title"
+ flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <description class="ac-normal-text ac-comment"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-comment"
+ hidden="true"></label>
+ </hbox>
+ <hbox>
+ <hbox class="ac-url"
+ flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <description class="ac-normal-text ac-url-text"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-url-text"
+ hidden="true"></label>
+ </hbox>
+ </vbox>
+ `)
+ );
+
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default.
+ }
+
+ this._identityOverflowEllipsis = this.querySelector("label.ac-url-text");
+ this._nameOverflowEllipsis = this.querySelector("label.ac-comment");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._identityBox = this.querySelector(".ac-url");
+ this._identity = this.querySelector("description.ac-url-text");
+
+ this._nameBox = this.querySelector(".ac-title");
+ this._name = this.querySelector("description.ac-comment");
+
+ this._adjustAcItem();
+
+ this.initializeAttributeInheritance();
+ }
+
+ get label() {
+ let identity = this.obj;
+ return identity.accessibleLabel;
+ }
+
+ _adjustAcItem() {
+ let contact = this.obj;
+
+ if (contact == null) {
+ return;
+ }
+
+ let identity = contact.identities[0];
+
+ // Emphasize the matching search terms for the description.
+ this._setUpDescription(this._name, contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them.
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._nameBox,
+ this._nameOverflowEllipsis
+ );
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._identityBox,
+ this._identityOverflowEllipsis
+ );
+ }
+ }
+
+ customElements.define(
+ "gloda-contact-chunk-richlistitem",
+ MozGlodaContactChunkRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaFulltextAllRichlistitem widget displays an autocomplete full text of
+ * all the items: e.g. full text explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaFulltextAllRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-fulltext-all-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation");
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesMentioningMany.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.words.join(", "))
+ );
+ this.appendChild(this._explanation);
+ }
+
+ get label() {
+ return "full text search: " + this.row.item; // what is this for? l10n?
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaFulltextAllRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-fulltext-all-richlistitem",
+ MozGlodaFulltextAllRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaFulltextAllRichlistitem widget displays an autocomplete full text
+ * of single item: e.g. full text explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaFulltextSingleRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-fulltext-single-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation", "gloda-fulltext-single");
+ this._parameters = document.createXULElement("description");
+
+ this.appendChild(this._explanation);
+ this.appendChild(this._parameters);
+
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesMentioning.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.item)
+ );
+ }
+
+ get label() {
+ return "full text search: " + this.row.item;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaFulltextSingleRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-fulltext-single-richlistitem",
+ MozGlodaFulltextSingleRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaMultiRichlistitem widget displays an autocomplete description of multiple
+ * type items: e.g. explanation of the items.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaMultiRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-multi-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._identityHolder = document.createXULElement("hbox");
+ this._identityHolder.setAttribute("flex", "1");
+
+ this.appendChild(this._explanation);
+ this.appendChild(this._identityHolder);
+ this._adjustAcItem();
+ }
+
+ get label() {
+ return this._explanation.value;
+ }
+
+ renderItem(aObj) {
+ let node = document.createXULElement("richlistitem");
+
+ node.obj = aObj;
+ node.setAttribute(
+ "type",
+ "gloda-" + this.row.nounDef.name + "-chunk-richlistitem"
+ );
+
+ this._identityHolder.appendChild(node);
+ }
+
+ _adjustAcItem() {
+ // clear out any lingering children.
+ while (this._identityHolder.hasChildNodes()) {
+ this._identityHolder.lastChild.remove();
+ }
+
+ let row = this.row;
+ if (row == null) {
+ return;
+ }
+
+ this._explanation.value =
+ row.nounDef.name + "s " + row.criteriaType + "ed " + row.criteria;
+
+ // render anyone already in there.
+ for (let item of row.collection.items) {
+ this.renderItem(item);
+ }
+ // listen up, yo.
+ row.renderer = this;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaMultiRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define("gloda-multi-richlistitem", MozGlodaMultiRichlistitem, {
+ extends: "richlistitem",
+ });
+
+ /**
+ * The MozGlodaSingleIdentityRichlistitem widget displays an autocomplete item with
+ * single identity: e.g. image, name and description of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaSingleIdentityRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ static get inheritedAttributes() {
+ return {
+ "description.ac-comment": "selected",
+ "label.ac-comment": "selected",
+ "description.ac-url-text": "selected",
+ "label.ac-url-text": "selected",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ this.setAttribute("is", "gloda-single-identity-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(`
+ <hbox class="gloda-single-identity">
+ <vbox>
+ <hbox>
+ <hbox class="ac-title"
+ flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <description class="ac-normal-text ac-comment"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-comment"
+ hidden="true"></label>
+ </hbox>
+ <hbox>
+ <hbox class="ac-url"
+ flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <description class="ac-normal-text ac-url-text"
+ inherits="selected"></description>
+ </hbox>
+ <label class="ac-ellipsis-after ac-url-text"
+ hidden="true"></label>
+ </hbox>
+ </vbox>
+ </hbox>
+ `)
+ );
+
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default.
+ }
+
+ this._identityOverflowEllipsis = this.querySelector("label.ac-url-text");
+ this._nameOverflowEllipsis = this.querySelector("label.ac-comment");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._identityBox = this.querySelector(".ac-url");
+ this._identity = this.querySelector("description.ac-url-text");
+
+ this._nameBox = this.querySelector(".ac-title");
+ this._name = this.querySelector("description.ac-comment");
+
+ this._adjustAcItem();
+
+ this.initializeAttributeInheritance();
+ }
+
+ get label() {
+ let identity = this.row.item;
+ return identity.accessibleLabel;
+ }
+
+ _adjustAcItem() {
+ let identity = this.row.item;
+
+ if (identity == null) {
+ return;
+ }
+
+ // Emphasize the matching search terms for the description.
+ this._setUpDescription(this._name, identity.contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them.
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._nameBox,
+ this._nameOverflowEllipsis
+ );
+ setTimeout(
+ this._setUpOverflow,
+ 0,
+ this._identityBox,
+ this._identityOverflowEllipsis
+ );
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaSingleIdentityRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-single-identity-richlistitem",
+ MozGlodaSingleIdentityRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+
+ /**
+ * The MozGlodaSingleTagRichlistitem widget displays an autocomplete item with
+ * single tag: e.g. explanation of the item.
+ *
+ * @augments MozGlodacompleteBaseRichlistitem
+ */
+ class MozGlodaSingleTagRichlistitem extends MozGlodacompleteBaseRichlistitem {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "gloda-single-tag-richlistitem");
+ this._explanation = document.createXULElement("description");
+ this._explanation.classList.add("explanation", "gloda-single");
+ this.appendChild(this._explanation);
+ let label = gGlodaCompleteStrings.GetStringFromName(
+ "glodaComplete.messagesTagged.label"
+ );
+ this._explanation.setAttribute(
+ "value",
+ label.replace("#1", this.row.item.tag)
+ );
+ }
+
+ get label() {
+ return "tag " + this.row.item.tag;
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodaSingleTagRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define(
+ "gloda-single-tag-richlistitem",
+ MozGlodaSingleTagRichlistitem,
+ {
+ extends: "richlistitem",
+ }
+ );
+}
diff --git a/comm/mailnews/db/gloda/content/glodacomplete.js b/comm/mailnews/db/gloda/content/glodacomplete.js
new file mode 100644
index 0000000000..64578d4143
--- /dev/null
+++ b/comm/mailnews/db/gloda/content/glodacomplete.js
@@ -0,0 +1,466 @@
+/* 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/. */
+
+/* globals MozElements, MozXULElement */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const MozPopupElement = MozElements.MozElementMixin(XULPopupElement);
+
+ /**
+ * The MozGlodacompleteRichResultPopup class creates the panel
+ * to append all the results for the gloda search autocomplete.
+ *
+ * @augments {MozPopupElement}
+ */
+ class MozGlodacompleteRichResultPopup extends MozPopupElement {
+ constructor() {
+ super();
+
+ this.addEventListener("popupshowing", event => {
+ // If normalMaxRows wasn't already set by the input, then set it here
+ // so that we restore the correct number when the popup is hidden.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this._normalMaxRows < 0 && this.mInput) {
+ this._normalMaxRows = this.mInput.maxRows;
+ }
+
+ this.mPopupOpen = true;
+ });
+
+ this.addEventListener("popupshown", event => {
+ if (this._adjustHeightOnPopupShown) {
+ delete this._adjustHeightOnPopupShown;
+ this.adjustHeight();
+ }
+ });
+
+ this.addEventListener("popuphiding", event => {
+ let isListActive = true;
+ if (this.selectedIndex == -1) {
+ isListActive = false;
+ }
+ this.mInput.controller.stopSearch();
+ this.mPopupOpen = false;
+
+ // Reset the maxRows property to the cached "normal" value (if there's
+ // any), and reset normalMaxRows so that we can detect whether it was set
+ // by the input when the popupshowing handler runs.
+
+ // Null-check this.mInput; see bug 1017914
+ if (this.mInput && this._normalMaxRows > 0) {
+ this.mInput.maxRows = this._normalMaxRows;
+ }
+ this._normalMaxRows = -1;
+ // If the list was being navigated and then closed, make sure
+ // we fire accessible focus event back to textbox
+
+ // Null-check this.mInput; see bug 1017914
+ if (isListActive && this.mInput) {
+ this.mInput.mIgnoreFocus = true;
+ this.mInput._focus();
+ this.mInput.mIgnoreFocus = false;
+ }
+ });
+
+ this.attachShadow({ mode: "open" });
+
+ let slot = document.createElement("slot");
+ slot.part = "content";
+ this.shadowRoot.appendChild(slot);
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this.textContent = "";
+
+ this.mInput = null;
+
+ this.mPopupOpen = false;
+
+ this._currentIndex = 0;
+
+ /**
+ * This is the default number of rows that we give the autocomplete
+ * popup when the textbox doesn't have a "maxrows" attribute
+ * for us to use.
+ */
+ this.defaultMaxRows = 6;
+
+ /**
+ * In some cases (e.g. when the input's dropmarker button is clicked),
+ * the input wants to display a popup with more rows. In that case, it
+ * should increase its maxRows property and store the "normal" maxRows
+ * in this field. When the popup is hidden, we restore the input's
+ * maxRows to the value stored in this field.
+ *
+ * This field is set to -1 between uses so that we can tell when it's
+ * been set by the input and when we need to set it in the popupshowing
+ * handler.
+ */
+ this._normalMaxRows = -1;
+
+ this._previousSelectedIndex = -1;
+
+ this.mLastMoveTime = Date.now();
+
+ this.mousedOverIndex = -1;
+
+ this.richlistbox = document.createXULElement("richlistbox");
+ this.richlistbox.setAttribute("flex", "1");
+ this.richlistbox.classList.add("autocomplete-richlistbox");
+
+ this.appendChild(this.richlistbox);
+
+ if (!this.listEvents) {
+ this.listEvents = {
+ handleEvent: event => {
+ if (!this.parentNode) {
+ return;
+ }
+
+ switch (event.type) {
+ case "mouseup":
+ // Don't call onPopupClick for the scrollbar buttons, thumb,
+ // slider, etc. If we hit the richlistbox and not a
+ // richlistitem, we ignore the event.
+ if (
+ event.target.closest("richlistbox, richlistitem").localName ==
+ "richlistitem"
+ ) {
+ this.onPopupClick(event);
+ }
+ break;
+ case "mousemove":
+ if (Date.now() - this.mLastMoveTime <= 30) {
+ return;
+ }
+
+ let item = event.target.closest("richlistbox, richlistitem");
+
+ // If we hit the richlistbox and not a richlistitem, we ignore
+ // the event.
+ if (item.localName == "richlistbox") {
+ return;
+ }
+
+ let index = this.richlistbox.getIndexOfItem(item);
+
+ this.mousedOverIndex = index;
+
+ if (item.selectedByMouseOver) {
+ this.richlistbox.selectedIndex = index;
+ }
+
+ this.mLastMoveTime = Date.now();
+ break;
+ }
+ },
+ };
+ this.richlistbox.addEventListener("mouseup", this.listEvents);
+ this.richlistbox.addEventListener("mousemove", this.listEvents);
+ }
+ }
+
+ // nsIAutoCompletePopup
+ get input() {
+ return this.mInput;
+ }
+
+ get overrideValue() {
+ return null;
+ }
+
+ get popupOpen() {
+ return this.mPopupOpen;
+ }
+
+ get maxRows() {
+ return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
+ }
+
+ set selectedIndex(val) {
+ if (val != this.richlistbox.selectedIndex) {
+ this._previousSelectedIndex = this.richlistbox.selectedIndex;
+ }
+ this.richlistbox.selectedIndex = val;
+ // Since ensureElementIsVisible may cause an expensive Layout flush,
+ // invoke it only if there may be a scrollbar, so if we could fetch
+ // more results than we can show at once.
+ // maxResults is the maximum number of fetched results, maxRows is the
+ // maximum number of rows we show at once, without a scrollbar.
+ if (this.mPopupOpen && this.maxResults > this.maxRows) {
+ // when clearing the selection (val == -1, so selectedItem will be
+ // null), we want to scroll back to the top. see bug #406194
+ this.richlistbox.ensureElementIsVisible(
+ this.richlistbox.selectedItem || this.richlistbox.firstElementChild
+ );
+ }
+ }
+
+ get selectedIndex() {
+ return this.richlistbox.selectedIndex;
+ }
+
+ get maxResults() {
+ // This is how many richlistitems will be kept around.
+ // Note, this getter may be overridden, or instances
+ // can have the nomaxresults attribute set to have no
+ // limit.
+ if (this.getAttribute("nomaxresults") == "true") {
+ return Infinity;
+ }
+
+ return 20;
+ }
+
+ get matchCount() {
+ return Math.min(this.mInput.controller.matchCount, this.maxResults);
+ }
+
+ get overflowPadding() {
+ return Number(this.getAttribute("overflowpadding"));
+ }
+
+ set view(val) {}
+
+ get view() {
+ return this.mInput.controller;
+ }
+
+ closePopup() {
+ if (this.mPopupOpen) {
+ this.hidePopup();
+ this.style.removeProperty("--panel-width");
+ }
+ }
+
+ getNextIndex(aReverse, aAmount, aIndex, aMaxRow) {
+ if (aMaxRow < 0) {
+ return -1;
+ }
+
+ let newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
+ if (
+ (aReverse && aIndex == -1) ||
+ (newIdx > aMaxRow && aIndex != aMaxRow)
+ ) {
+ newIdx = aMaxRow;
+ } else if ((!aReverse && aIndex == -1) || (newIdx < 0 && aIndex != 0)) {
+ newIdx = 0;
+ }
+
+ if (
+ (newIdx < 0 && aIndex == 0) ||
+ (newIdx > aMaxRow && aIndex == aMaxRow)
+ ) {
+ aIndex = -1;
+ } else {
+ aIndex = newIdx;
+ }
+
+ return aIndex;
+ }
+
+ onPopupClick(aEvent) {
+ this.input.controller.handleEnter(true, aEvent);
+ }
+
+ onSearchBegin() {
+ this.mousedOverIndex = -1;
+
+ if (typeof this._onSearchBegin == "function") {
+ this._onSearchBegin();
+ }
+ }
+
+ openAutocompletePopup(aInput, aElement) {
+ // until we have "baseBinding", (see bug #373652) this allows
+ // us to override openAutocompletePopup(), but still call
+ // the method on the base class
+ this._openAutocompletePopup(aInput, aElement);
+ }
+
+ _openAutocompletePopup(aInput, aElement) {
+ if (!this.mPopupOpen) {
+ // It's possible that the panel is hidden initially
+ // to avoid impacting startup / new window performance
+ aInput.popup.hidden = false;
+
+ this.mInput = aInput;
+ // clear any previous selection, see bugs 400671 and 488357
+ this.selectedIndex = -1;
+
+ let width = aElement.getBoundingClientRect().width;
+ this.style.setProperty(
+ "--panel-width",
+ (width > 100 ? width : 100) + "px"
+ );
+ // invalidate() depends on the width attribute
+ this._invalidate();
+
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ }
+ }
+
+ invalidate(reason) {
+ // Don't bother doing work if we're not even showing
+ if (!this.mPopupOpen) {
+ return;
+ }
+
+ this._invalidate(reason);
+ }
+
+ _invalidate(reason) {
+ setTimeout(() => this.adjustHeight(), 0);
+
+ // remove all child nodes because we never want to reuse them.
+ while (this.richlistbox.hasChildNodes()) {
+ this.richlistbox.lastChild.remove();
+ }
+
+ this._currentIndex = 0;
+ this._appendCurrentResult();
+ }
+
+ _collapseUnusedItems() {
+ let existingItemsCount = this.richlistbox.children.length;
+ for (let i = this.matchCount; i < existingItemsCount; ++i) {
+ let item = this.richlistbox.children[i];
+
+ item.collapsed = true;
+ if (typeof item._onCollapse == "function") {
+ item._onCollapse();
+ }
+ }
+ }
+
+ adjustHeight() {
+ // Figure out how many rows to show
+ let rows = this.richlistbox.children;
+ let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
+
+ // Default the height to 0 if we have no rows to show
+ let height = 0;
+ if (numRows) {
+ let firstRowRect = rows[0].getBoundingClientRect();
+ if (this._rlbPadding == undefined) {
+ let style = window.getComputedStyle(this.richlistbox);
+ let paddingTop = parseInt(style.paddingTop) || 0;
+ let paddingBottom = parseInt(style.paddingBottom) || 0;
+ this._rlbPadding = paddingTop + paddingBottom;
+ }
+
+ // The class `forceHandleUnderflow` is for the item might need to
+ // handle OverUnderflow or Overflow when the height of an item will
+ // be changed dynamically.
+ for (let i = 0; i < numRows; i++) {
+ if (rows[i].classList.contains("forceHandleUnderflow")) {
+ rows[i].handleOverUnderflow();
+ }
+ }
+
+ let lastRowRect = rows[numRows - 1].getBoundingClientRect();
+ // Calculate the height to have the first row to last row shown
+ height = lastRowRect.bottom - firstRowRect.top + this._rlbPadding;
+ }
+
+ let currentHeight = this.richlistbox.getBoundingClientRect().height;
+ if (height <= currentHeight) {
+ this._collapseUnusedItems();
+ }
+ this.richlistbox.style.removeProperty("height");
+ // We need to get the ceiling of the calculated value to ensure that the box fully contains
+ // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a
+ // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a
+ // scrollbar for the extra 0.5px.
+ this.richlistbox.height = Math.ceil(height);
+ }
+
+ _appendCurrentResult() {
+ let controller = this.mInput.controller;
+ let glodaCompleter = Cc[
+ "@mozilla.org/autocomplete/search;1?name=gloda"
+ ].getService(Ci.nsIAutoCompleteSearch).wrappedJSObject;
+
+ // Process maxRows per chunk to improve performance and user experience
+ for (let i = 0; i < this.maxRows; i++) {
+ if (this._currentIndex >= this.matchCount) {
+ return;
+ }
+
+ let item;
+
+ // trim the leading/trailing whitespace
+ let trimmedSearchString = controller.searchString.trim();
+ let result = glodaCompleter.curResult;
+
+ item = document.createXULElement("richlistitem", {
+ is: result.getStyleAt(this._currentIndex),
+ });
+
+ // set these attributes before we set the class
+ // so that we can use them from the constructor
+ let row = result.getObjectAt(this._currentIndex);
+ item.setAttribute("text", trimmedSearchString);
+ item.setAttribute("type", result.getStyleAt(this._currentIndex));
+
+ item.row = row;
+
+ // set the class at the end so we can use the attributes
+ // in the xbl constructor
+ item.className = "autocomplete-richlistitem";
+ this.richlistbox.appendChild(item);
+ this._currentIndex++;
+ }
+
+ // yield after each batch of items so that typing the url bar is responsive
+ setTimeout(() => this._appendCurrentResult(), 0);
+ }
+
+ selectBy(aReverse, aPage) {
+ try {
+ let amount = aPage ? 5 : 1;
+
+ // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
+ this.selectedIndex = this.getNextIndex(
+ aReverse,
+ amount,
+ this.selectedIndex,
+ this.matchCount - 1
+ );
+ if (this.selectedIndex == -1) {
+ this.input._focus();
+ }
+ } catch (ex) {
+ // do nothing - occasionally timer-related js errors happen here
+ // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+ // navigation key before this popup has opened
+ }
+ }
+
+ disconnectedCallback() {
+ if (this.listEvents) {
+ this.richlistbox.removeEventListener("mouseup", this.listEvents);
+ this.richlistbox.removeEventListener("mousemove", this.listEvents);
+ delete this.listEvents;
+ }
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozGlodacompleteRichResultPopup, [
+ Ci.nsIAutoCompletePopup,
+ ]);
+ customElements.define(
+ "glodacomplete-rich-result-popup",
+ MozGlodacompleteRichResultPopup,
+ { extends: "panel" }
+ );
+}
diff --git a/comm/mailnews/db/gloda/jar.mn b/comm/mailnews/db/gloda/jar.mn
new file mode 100644
index 0000000000..6dbf20d9c3
--- /dev/null
+++ b/comm/mailnews/db/gloda/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+gloda.jar:
+% content gloda %content/
+ content/glodacomplete.js (content/glodacomplete.js)
+ content/autocomplete-richlistitem.js (content/autocomplete-richlistitem.js)
diff --git a/comm/mailnews/db/gloda/modules/Collection.jsm b/comm/mailnews/db/gloda/modules/Collection.jsm
new file mode 100644
index 0000000000..e229161fc9
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/Collection.jsm
@@ -0,0 +1,834 @@
+/* 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 = ["GlodaCollection", "GlodaCollectionManager"];
+
+var LOG = console.createInstance({
+ prefix: "gloda.collection",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * @namespace Central registry and logic for all collections.
+ *
+ * The collection manager is a singleton that has the following tasks:
+ * - Let views of objects (nouns) know when their objects have changed. For
+ * example, an attribute has changed due to user action.
+ * - Let views of objects based on queries know when new objects match their
+ * query, or when their existing objects no longer match due to changes.
+ * - Caching/object-identity maintenance. It is ideal if we only ever have
+ * one instance of an object at a time. (More specifically, only one instance
+ * per database row 'id'.) The collection mechanism lets us find existing
+ * instances to this end. Caching can be directly integrated by being treated
+ * as a special collection.
+ */
+var GlodaCollectionManager = {
+ _collectionsByNoun: {},
+ _cachesByNoun: {},
+
+ /**
+ * Registers the existence of a collection with the collection manager. This
+ * is done using a weak reference so that the collection can go away if it
+ * wants to.
+ */
+ registerCollection(aCollection) {
+ let collections;
+ let nounID = aCollection.query._nounDef.id;
+ if (!(nounID in this._collectionsByNoun)) {
+ collections = this._collectionsByNoun[nounID] = [];
+ } else {
+ // purge dead weak references while we're at it
+ collections = this._collectionsByNoun[nounID].filter(aRef => aRef.get());
+ this._collectionsByNoun[nounID] = collections;
+ }
+ collections.push(Cu.getWeakReference(aCollection));
+ },
+
+ getCollectionsForNounID(aNounID) {
+ if (!(aNounID in this._collectionsByNoun)) {
+ return [];
+ }
+
+ // generator would be nice, but I suspect get() is too expensive to use
+ // twice (guard/predicate and value)
+ let weakCollections = this._collectionsByNoun[aNounID];
+ let collections = [];
+ for (let iColl = 0; iColl < weakCollections.length; iColl++) {
+ let collection = weakCollections[iColl].get();
+ if (collection) {
+ collections.push(collection);
+ }
+ }
+ return collections;
+ },
+
+ defineCache(aNounDef, aCacheSize) {
+ this._cachesByNoun[aNounDef.id] = new GlodaLRUCacheCollection(
+ aNounDef,
+ aCacheSize
+ );
+ },
+
+ /**
+ * Attempt to locate an instance of the object of the given noun type with the
+ * given id. Counts as a cache hit if found. (And if it wasn't in a cache,
+ * but rather a collection, it is added to the cache.)
+ */
+ cacheLookupOne(aNounID, aID, aDoCache) {
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ if (aID in cache._idMap) {
+ let item = cache._idMap[aID];
+ return cache.hit(item);
+ }
+ }
+
+ if (aDoCache === false) {
+ cache = null;
+ }
+
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ if (aID in collection._idMap) {
+ let item = collection._idMap[aID];
+ if (cache) {
+ cache.add([item]);
+ }
+ return item;
+ }
+ }
+
+ LOG.debug("cacheLookupOne:\nhit null");
+ return null;
+ },
+
+ /**
+ * Lookup multiple nouns by ID from the cache/existing collections.
+ *
+ * @param aNounID The kind of noun identified by its ID.
+ * @param aIDMap A dictionary/map whose keys must be gloda noun ids for the
+ * given noun type and whose values are ignored.
+ * @param aTargetMap An object to hold the noun id's (key) and noun instances
+ * (value) for the noun instances that were found available in memory
+ * because they were cached or in existing query collections.
+ * @param [aDoCache=true] Should we add any items to the cache that we found
+ * in collections that were in memory but not in the cache? You would
+ * likely want to pass false if you are only updating in-memory
+ * representations rather than performing a new query.
+ *
+ * @returns [The number that were found, the number that were not found,
+ * a dictionary whose keys are the ids of noun instances that
+ * were not found.]
+ */
+ cacheLookupMany(aNounID, aIDMap, aTargetMap, aDoCache) {
+ let foundCount = 0,
+ notFoundCount = 0,
+ notFound = {};
+
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ for (let key in aIDMap) {
+ let cacheValue = cache._idMap[key];
+ if (cacheValue === undefined) {
+ notFoundCount++;
+ notFound[key] = null;
+ } else {
+ foundCount++;
+ aTargetMap[key] = cacheValue;
+ cache.hit(cacheValue);
+ }
+ }
+ }
+
+ if (aDoCache === false) {
+ cache = null;
+ }
+
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ for (let key in notFound) {
+ let collValue = collection._idMap[key];
+ if (collValue !== undefined) {
+ aTargetMap[key] = collValue;
+ delete notFound[key];
+ foundCount++;
+ notFoundCount--;
+ if (cache) {
+ cache.add([collValue]);
+ }
+ }
+ }
+ }
+
+ return [foundCount, notFoundCount, notFound];
+ },
+
+ /**
+ * Friendlier version of |cacheLookupMany|; takes a list of ids and returns
+ * an object whose keys and values are the gloda id's and instances of the
+ * instances that were found. We don't tell you who we didn't find. The
+ * assumption is this is being used for in-memory updates where we only need
+ * to tweak what is in memory.
+ */
+ cacheLookupManyList(aNounID, aIds) {
+ let checkMap = {},
+ targetMap = {};
+ for (let id of aIds) {
+ checkMap[id] = null;
+ }
+ // do not promote found items into the cache
+ this.cacheLookupMany(aNounID, checkMap, targetMap, false);
+ return targetMap;
+ },
+
+ /**
+ * Attempt to locate an instance of the object of the given noun type with the
+ * given id. Counts as a cache hit if found. (And if it wasn't in a cache,
+ * but rather a collection, it is added to the cache.)
+ */
+ cacheLookupOneByUniqueValue(aNounID, aUniqueValue, aDoCache) {
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ if (aUniqueValue in cache._uniqueValueMap) {
+ let item = cache._uniqueValueMap[aUniqueValue];
+ return cache.hit(item);
+ }
+ }
+
+ if (aDoCache === false) {
+ cache = null;
+ }
+
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ if (aUniqueValue in collection._uniqueValueMap) {
+ let item = collection._uniqueValueMap[aUniqueValue];
+ if (cache) {
+ cache.add([item]);
+ }
+ return item;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Checks whether the provided item with the given id is actually a duplicate
+ * of an instance that already exists in the cache/a collection. If it is,
+ * the pre-existing instance is returned and counts as a cache hit. If it
+ * is not, the passed-in instance is added to the cache and returned.
+ */
+ cacheLoadUnifyOne(aItem) {
+ let items = [aItem];
+ this.cacheLoadUnify(aItem.NOUN_ID, items);
+ return items[0];
+ },
+
+ /**
+ * Given a list of items, check if any of them already have duplicate,
+ * canonical, instances in the cache or collections. Items with pre-existing
+ * instances are replaced by those instances in the provided list, and each
+ * counts as a cache hit. Items without pre-existing instances are added
+ * to the cache and left intact.
+ */
+ cacheLoadUnify(aNounID, aItems, aCacheIfMissing) {
+ let cache = this._cachesByNoun[aNounID];
+ if (aCacheIfMissing === undefined) {
+ aCacheIfMissing = true;
+ }
+
+ // track the items we haven't yet found in a cache/collection (value) and
+ // their index in aItems (key). We're somewhat abusing the dictionary
+ // metaphor with the intent of storing tuples here. We also do it because
+ // it allows random-access deletion theoretically without cost. (Since
+ // we delete during iteration, that may be wrong, but it sounds like the
+ // semantics still work?)
+ let unresolvedIndexToItem = {};
+ let numUnresolved = 0;
+
+ if (cache) {
+ for (let iItem = 0; iItem < aItems.length; iItem++) {
+ let item = aItems[iItem];
+
+ if (item.id in cache._idMap) {
+ let realItem = cache._idMap[item.id];
+ // update the caller's array with the reference to the 'real' item
+ aItems[iItem] = realItem;
+ cache.hit(realItem);
+ } else {
+ unresolvedIndexToItem[iItem] = item;
+ numUnresolved++;
+ }
+ }
+
+ // we're done if everyone was a hit.
+ if (numUnresolved == 0) {
+ return;
+ }
+ } else {
+ for (let iItem = 0; iItem < aItems.length; iItem++) {
+ unresolvedIndexToItem[iItem] = aItems[iItem];
+ }
+ numUnresolved = aItems.length;
+ }
+
+ let needToCache = [];
+ // next, let's fall back to our collections
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ for (let [iItem, item] of Object.entries(unresolvedIndexToItem)) {
+ if (item.id in collection._idMap) {
+ let realItem = collection._idMap[item.id];
+ // update the caller's array to now have the 'real' object
+ aItems[iItem] = realItem;
+ // flag that we need to cache this guy (we use an inclusive cache)
+ needToCache.push(realItem);
+ // we no longer need to resolve this item...
+ delete unresolvedIndexToItem[iItem];
+ // stop checking collections if we got everybody
+ if (--numUnresolved == 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ // anything left in unresolvedIndexToItem should be added to the cache
+ // unless !aCacheIfMissing. plus, we already have 'needToCache'
+ if (cache && aCacheIfMissing) {
+ cache.add(
+ needToCache.concat(
+ Object.keys(unresolvedIndexToItem).map(
+ key => unresolvedIndexToItem[key]
+ )
+ )
+ );
+ }
+ },
+
+ cacheCommitDirty() {
+ for (let id in this._cachesByNoun) {
+ let cache = this._cachesByNoun[id];
+ cache.commitDirty();
+ }
+ },
+
+ /**
+ * Notifies the collection manager that an item has been loaded and should
+ * be cached, assuming caching is active.
+ */
+ itemLoaded(aItem) {
+ let cache = this._cachesByNoun[aItem.NOUN_ID];
+ if (cache) {
+ cache.add([aItem]);
+ }
+ },
+
+ /**
+ * Notifies the collection manager that multiple items has been loaded and
+ * should be cached, assuming caching is active.
+ */
+ itemsLoaded(aNounID, aItems) {
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ cache.add(aItems);
+ }
+ },
+
+ /**
+ * This should be called when items are added to the global database. This
+ * should generally mean during indexing by indexers or an attribute
+ * provider.
+ * We walk all existing collections for the given noun type and add the items
+ * to the collection if the item meets the query that defines the collection.
+ */
+ itemsAdded(aNounID, aItems) {
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ cache.add(aItems);
+ }
+
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ let addItems = aItems.filter(item => collection.query.test(item));
+ if (addItems.length) {
+ collection._onItemsAdded(addItems);
+ }
+ }
+ },
+ /**
+ * This should be called when items in the global database are modified. For
+ * example, as a result of indexing. This should generally only be called
+ * by indexers or by attribute providers.
+ * We walk all existing collections for the given noun type. For items
+ * currently included in each collection but should no longer be (per the
+ * collection's defining query) we generate onItemsRemoved events. For items
+ * not currently included in the collection but should now be, we generate
+ * onItemsAdded events. For items included that still match the query, we
+ * generate onItemsModified events.
+ */
+ itemsModified(aNounID, aItems) {
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ let added = [],
+ modified = [],
+ removed = [];
+ for (let item of aItems) {
+ if (item.id in collection._idMap) {
+ // currently in... but should it still be there?
+ if (collection.query.test(item)) {
+ modified.push(item); // yes, keep it
+ } else if (!collection.query.frozen) {
+ // oy, so null queries really don't want any notifications, and they
+ // sorta fit into our existing model, except for the removal bit.
+ // so we need a specialized check for them, and we're using the
+ // frozen attribute to this end.
+ removed.push(item); // no, bin it
+ }
+ } else if (collection.query.test(item)) {
+ // not in, should it be?
+ added.push(item); // yep, add it
+ }
+ }
+ if (added.length) {
+ collection._onItemsAdded(added);
+ }
+ if (modified.length) {
+ collection._onItemsModified(modified);
+ }
+ if (removed.length) {
+ collection._onItemsRemoved(removed);
+ }
+ }
+ },
+ /**
+ * This should be called when items in the global database are permanently-ish
+ * deleted. (This is distinct from concepts like message deletion which may
+ * involved trash folders or other modified forms of existence. Deleted
+ * means the data is gone and if it were to come back, it would come back
+ * via an itemsAdded event.)
+ * We walk all existing collections for the given noun type. For items
+ * currently in the collection, we generate onItemsRemoved events.
+ *
+ * @param aItemIds A list of item ids that are being deleted.
+ */
+ itemsDeleted(aNounID, aItemIds) {
+ // cache
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ for (let itemId of aItemIds) {
+ if (itemId in cache._idMap) {
+ cache.deleted(cache._idMap[itemId]);
+ }
+ }
+ }
+
+ // collections
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ let removeItems = aItemIds
+ .filter(itemId => itemId in collection._idMap)
+ .map(itemId => collection._idMap[itemId]);
+ if (removeItems.length) {
+ collection._onItemsRemoved(removeItems);
+ }
+ }
+ },
+ /**
+ * Like |itemsDeleted| but for the case where the deletion is based on an
+ * attribute that SQLite can more efficiently check than we can and where the
+ * cost of scanning the in-memory items is presumably much cheaper than
+ * trying to figure out what actually got deleted.
+ *
+ * Since we are doing an in-memory walk, this is obviously O(n) where n is the
+ * number of noun instances of a given type in-memory. We are assuming this
+ * is a reasonable number of things and that this type of deletion call is
+ * not going to happen all that frequently. If these assumptions are wrong,
+ * callers are advised to re-think the whole situation.
+ *
+ * @param aNounID Type of noun we are talking about here.
+ * @param aFilter A filter function that returns true when the item should be
+ * thought of as deleted, or false if the item is still good. Screw this
+ * up and you will get some seriously wacky bugs, yo.
+ */
+ itemsDeletedByAttribute(aNounID, aFilter) {
+ // cache
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ for (let id in cache._idMap) {
+ let item = cache._idMap[id];
+ if (aFilter(item)) {
+ cache.deleted(item);
+ }
+ }
+ }
+
+ // collections
+ for (let collection of this.getCollectionsForNounID(aNounID)) {
+ let removeItems = collection.items.filter(aFilter);
+ if (removeItems.length) {
+ collection._onItemsRemoved(removeItems);
+ }
+ }
+ },
+};
+
+/**
+ * @class A current view of the set of first-class nouns meeting a given query.
+ * Assuming a listener is present, events are
+ * generated when new objects meet the query, existing objects no longer meet
+ * the query, or existing objects have experienced a change in attributes that
+ * does not affect their ability to be present (but the listener may care about
+ * because it is exposing those attributes).
+ * @class
+ */
+function GlodaCollection(
+ aNounDef,
+ aItems,
+ aQuery,
+ aListener,
+ aMasterCollection
+) {
+ // if aNounDef is null, we are just being invoked for subclassing
+ if (aNounDef === undefined) {
+ return;
+ }
+
+ this._nounDef = aNounDef;
+ // should we also maintain a unique value mapping...
+ if (this._nounDef.usesUniqueValue) {
+ this._uniqueValueMap = {};
+ }
+
+ this.pendingItems = [];
+ this._pendingIdMap = {};
+ this.items = [];
+ this._idMap = {};
+
+ // force the listener to null for our call to _onItemsAdded; no events for
+ // the initial load-out.
+ this._listener = null;
+ if (aItems && aItems.length) {
+ this._onItemsAdded(aItems);
+ }
+
+ this.query = aQuery || null;
+ if (this.query) {
+ this.query.collection = this;
+ if (this.query.options.stashColumns) {
+ this.stashedColumns = {};
+ }
+ }
+ this._listener = aListener || null;
+
+ this.deferredCount = 0;
+ this.resolvedCount = 0;
+
+ if (aMasterCollection) {
+ this.masterCollection = aMasterCollection.masterCollection;
+ } else {
+ this.masterCollection = this;
+ /** a dictionary of dictionaries. at the top level, the keys are noun IDs.
+ * each of these sub-dictionaries maps the IDs of desired noun instances to
+ * the actual instance, or null if it has not yet been loaded.
+ */
+ this.referencesByNounID = {};
+ /**
+ * a dictionary of dictionaries. at the top level, the keys are noun IDs.
+ * each of the sub-dictionaries maps the IDs of the _recognized parent
+ * noun_ to the list of children, or null if the list has not yet been
+ * populated.
+ *
+ * So if we have a noun definition A with ID 1 who is the recognized parent
+ * noun of noun definition B with ID 2, AND we have an instance A(1) with
+ * two children B(10), B(11), then an example might be: {2: {1: [10, 11]}}.
+ */
+ this.inverseReferencesByNounID = {};
+ this.subCollections = {};
+ }
+}
+
+GlodaCollection.prototype = {
+ get listener() {
+ return this._listener;
+ },
+ set listener(aListener) {
+ this._listener = aListener;
+ },
+
+ /**
+ * If this collection still has a query associated with it, drop the query
+ * and replace it with an 'explicit query'. This means that the Collection
+ * Manager will not attempt to match new items indexed to the system against
+ * our query criteria.
+ * Once you call this method, your collection's listener will no longer
+ * receive onItemsAdded notifications that are not the result of your
+ * initial database query. It will, however, receive onItemsModified
+ * notifications if items in the collection are re-indexed.
+ */
+ becomeExplicit() {
+ if (!(this.query instanceof this._nounDef.explicitQueryClass)) {
+ this.query = new this._nounDef.explicitQueryClass(this);
+ }
+ },
+
+ /**
+ * Clear the contents of this collection. This only makes sense for explicit
+ * collections or wildcard collections. (Actual query-based collections
+ * should represent the state of the query, so unless we're going to delete
+ * all the items, clearing the collection would violate that constraint.)
+ */
+ clear() {
+ this._idMap = {};
+ if (this._uniqueValueMap) {
+ this._uniqueValueMap = {};
+ }
+ this.items = [];
+ },
+
+ _onItemsAdded(aItems) {
+ this.items.push.apply(this.items, aItems);
+ if (this._uniqueValueMap) {
+ for (let item of this.items) {
+ this._idMap[item.id] = item;
+ this._uniqueValueMap[item.uniqueValue] = item;
+ }
+ } else {
+ for (let item of this.items) {
+ this._idMap[item.id] = item;
+ }
+ }
+ if (this._listener) {
+ try {
+ this._listener.onItemsAdded(aItems, this);
+ } catch (ex) {
+ LOG.error(
+ "caught exception from listener in onItemsAdded: " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ": " +
+ ex
+ );
+ }
+ }
+ },
+
+ _onItemsModified(aItems) {
+ if (this._listener) {
+ try {
+ this._listener.onItemsModified(aItems, this);
+ } catch (ex) {
+ LOG.error(
+ "caught exception from listener in onItemsModified: " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ": " +
+ ex
+ );
+ }
+ }
+ },
+
+ /**
+ * Given a list of items that definitely no longer belong in this collection,
+ * remove them from the collection and notify the listener. The 'tricky'
+ * part is that we need to remove the deleted items from our list of items.
+ */
+ _onItemsRemoved(aItems) {
+ // we want to avoid the O(n^2) deletion performance case, and deletion
+ // should be rare enough that the extra cost of building the deletion map
+ // should never be a real problem.
+ let deleteMap = {};
+ // build the delete map while also nuking from our id map/unique value map
+ for (let item of aItems) {
+ deleteMap[item.id] = true;
+ delete this._idMap[item.id];
+ if (this._uniqueValueMap) {
+ delete this._uniqueValueMap[item.uniqueValue];
+ }
+ }
+ let items = this.items;
+ // in-place filter. probably needless optimization.
+ let iWrite = 0;
+ for (let iRead = 0; iRead < items.length; iRead++) {
+ let item = items[iRead];
+ if (!(item.id in deleteMap)) {
+ items[iWrite++] = item;
+ }
+ }
+ items.splice(iWrite);
+
+ if (this._listener) {
+ try {
+ this._listener.onItemsRemoved(aItems, this);
+ } catch (ex) {
+ LOG.error(
+ "caught exception from listener in onItemsRemoved: " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ": " +
+ ex
+ );
+ }
+ }
+ },
+
+ _onQueryCompleted() {
+ this.query.completed = true;
+ if (this._listener && this._listener.onQueryCompleted) {
+ this._listener.onQueryCompleted(this);
+ }
+ },
+};
+
+/**
+ * Create an LRU cache collection for the given noun with the given size.
+ *
+ * @class
+ */
+function GlodaLRUCacheCollection(aNounDef, aCacheSize) {
+ GlodaCollection.call(this, aNounDef, null, null, null);
+
+ this._head = null; // aka oldest!
+ this._tail = null; // aka newest!
+ this._size = 0;
+ // let's keep things sane, and simplify our logic a little...
+ if (aCacheSize < 32) {
+ aCacheSize = 32;
+ }
+ this._maxCacheSize = aCacheSize;
+}
+/**
+ * @class A LRU-discard cache. We use a doubly linked-list for the eviction
+ * tracking. Since we require that there is at most one LRU-discard cache per
+ * noun class, we simplify our lives by adding our own attributes to the
+ * cached objects.
+ * @augments GlodaCollection
+ */
+GlodaLRUCacheCollection.prototype = new GlodaCollection();
+GlodaLRUCacheCollection.prototype.add = function (aItems) {
+ for (let item of aItems) {
+ if (item.id in this._idMap) {
+ // DEBUGME so, we're dealing with this, but it shouldn't happen. need
+ // trace-debuggage.
+ continue;
+ }
+ this._idMap[item.id] = item;
+ if (this._uniqueValueMap) {
+ this._uniqueValueMap[item.uniqueValue] = item;
+ }
+
+ item._lruPrev = this._tail;
+ // we do have to make sure that we will set _head the first time we insert
+ // something
+ if (this._tail !== null) {
+ this._tail._lruNext = item;
+ } else {
+ this._head = item;
+ }
+ item._lruNext = null;
+ this._tail = item;
+
+ this._size++;
+ }
+
+ while (this._size > this._maxCacheSize) {
+ let item = this._head;
+
+ // we never have to deal with the possibility of needing to make _head/_tail
+ // null.
+ this._head = item._lruNext;
+ this._head._lruPrev = null;
+ // (because we are nice, we will delete the properties...)
+ delete item._lruNext;
+ delete item._lruPrev;
+
+ // nuke from our id map
+ delete this._idMap[item.id];
+ if (this._uniqueValueMap) {
+ delete this._uniqueValueMap[item.uniqueValue];
+ }
+
+ // flush dirty items to disk (they may not have this attribute, in which
+ // case, this returns false, which is fine.)
+ if (item.dirty) {
+ this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+ delete item.dirty;
+ }
+
+ this._size--;
+ }
+};
+
+GlodaLRUCacheCollection.prototype.hit = function (aItem) {
+ // don't do anything in the 0 or 1 items case, or if we're already
+ // the last item
+ if (this._head === this._tail || this._tail === aItem) {
+ return aItem;
+ }
+
+ // - unlink the item
+ if (aItem._lruPrev !== null) {
+ aItem._lruPrev._lruNext = aItem._lruNext;
+ } else {
+ this._head = aItem._lruNext;
+ }
+ // (_lruNext cannot be null)
+ aItem._lruNext._lruPrev = aItem._lruPrev;
+ // - link it in to the end
+ this._tail._lruNext = aItem;
+ aItem._lruPrev = this._tail;
+ aItem._lruNext = null;
+ // update tail tracking
+ this._tail = aItem;
+
+ return aItem;
+};
+
+GlodaLRUCacheCollection.prototype.deleted = function (aItem) {
+ // unlink the item
+ if (aItem._lruPrev !== null) {
+ aItem._lruPrev._lruNext = aItem._lruNext;
+ } else {
+ this._head = aItem._lruNext;
+ }
+ if (aItem._lruNext !== null) {
+ aItem._lruNext._lruPrev = aItem._lruPrev;
+ } else {
+ this._tail = aItem._lruPrev;
+ }
+
+ // (because we are nice, we will delete the properties...)
+ delete aItem._lruNext;
+ delete aItem._lruPrev;
+
+ // nuke from our id map
+ delete this._idMap[aItem.id];
+ if (this._uniqueValueMap) {
+ delete this._uniqueValueMap[aItem.uniqueValue];
+ }
+
+ this._size--;
+};
+
+/**
+ * If any of the cached items are dirty, commit them, and make them no longer
+ * dirty.
+ */
+GlodaLRUCacheCollection.prototype.commitDirty = function () {
+ // we can only do this if there is an update method available...
+ if (!this._nounDef.objUpdate) {
+ return;
+ }
+
+ for (let iItem in this._idMap) {
+ let item = this._idMap[iItem];
+ if (item.dirty) {
+ LOG.debug("flushing dirty: " + item);
+ this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+ delete item.dirty;
+ }
+ }
+};
diff --git a/comm/mailnews/db/gloda/modules/Everybody.jsm b/comm/mailnews/db/gloda/modules/Everybody.jsm
new file mode 100644
index 0000000000..4f33134ef9
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/Everybody.jsm
@@ -0,0 +1,23 @@
+/* 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 = [];
+
+const { GlodaFundAttr } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaFundAttr.jsm"
+);
+GlodaFundAttr.init();
+const { GlodaExplicitAttr } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaExplicitAttr.jsm"
+);
+GlodaExplicitAttr.init();
+
+ChromeUtils.import("resource:///modules/gloda/NounTag.jsm");
+ChromeUtils.import("resource:///modules/gloda/NounFreetag.jsm");
+ChromeUtils.import("resource:///modules/gloda/NounMimetype.jsm");
+ChromeUtils.import("resource:///modules/gloda/IndexMsg.jsm");
+const { GlodaABAttrs } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaMsgIndexer.jsm"
+);
+GlodaABAttrs.init();
diff --git a/comm/mailnews/db/gloda/modules/Facet.jsm b/comm/mailnews/db/gloda/modules/Facet.jsm
new file mode 100644
index 0000000000..96425b8838
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/Facet.jsm
@@ -0,0 +1,599 @@
+/* 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/. */
+
+/*
+ * This file provides faceting logic.
+ */
+
+var EXPORTED_SYMBOLS = ["FacetDriver", "FacetUtils"];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Gloda",
+ "resource:///modules/gloda/GlodaPublic.jsm"
+);
+
+/**
+ * Decides the appropriate faceters for the noun type and drives the faceting
+ * process. This class and the faceters are intended to be reusable so that
+ * you only need one instance per faceting session. (Although each faceting
+ * pass is accordingly destructive to previous results.)
+ *
+ * Our strategy for faceting is to process one attribute at a time across all
+ * the items in the provided set. The alternative would be to iterate over
+ * the items and then iterate over the attributes on each item. While both
+ * approaches have caching downsides
+ */
+function FacetDriver(aNounDef, aWindow) {
+ this.nounDef = aNounDef;
+ this._window = aWindow;
+
+ this._makeFaceters();
+}
+FacetDriver.prototype = {
+ /**
+ * Populate |this.faceters| with a set of faceters appropriate to the noun
+ * definition associated with this instance.
+ */
+ _makeFaceters() {
+ let faceters = (this.faceters = []);
+
+ function makeFaceter(aAttrDef, aFacetDef) {
+ let facetType = aFacetDef.type;
+
+ if (aAttrDef.singular) {
+ if (facetType == "date") {
+ faceters.push(new DateFaceter(aAttrDef, aFacetDef));
+ } else {
+ faceters.push(new DiscreteFaceter(aAttrDef, aFacetDef));
+ }
+ } else if (facetType == "nonempty?") {
+ faceters.push(new NonEmptySetFaceter(aAttrDef, aFacetDef));
+ } else {
+ faceters.push(new DiscreteSetFaceter(aAttrDef, aFacetDef));
+ }
+ }
+
+ for (let key in this.nounDef.attribsByBoundName) {
+ let attrDef = this.nounDef.attribsByBoundName[key];
+ // ignore attributes that do not want to be faceted
+ if (!attrDef.facet) {
+ continue;
+ }
+
+ makeFaceter(attrDef, attrDef.facet);
+
+ if ("extraFacets" in attrDef) {
+ for (let facetDef of attrDef.extraFacets) {
+ makeFaceter(attrDef, facetDef);
+ }
+ }
+ }
+ },
+ /**
+ * Asynchronously facet the provided items, calling the provided callback when
+ * completed.
+ */
+ go(aItems, aCallback, aCallbackThis) {
+ this.items = aItems;
+ this.callback = aCallback;
+ this.callbackThis = aCallbackThis;
+
+ this._nextFaceter = 0;
+ this._drive();
+ },
+
+ _MAX_FACETING_TIMESLICE_MS: 100,
+ _FACETING_YIELD_DURATION_MS: 0,
+ _driveWrapper(aThis) {
+ aThis._drive();
+ },
+ _drive() {
+ let start = Date.now();
+
+ while (this._nextFaceter < this.faceters.length) {
+ let faceter = this.faceters[this._nextFaceter++];
+ // for now we facet in one go, but the long-term plan allows for them to
+ // be generators.
+ faceter.facetItems(this.items);
+
+ let delta = Date.now() - start;
+ if (delta > this._MAX_FACETING_TIMESLICE_MS) {
+ this._window.setTimeout(
+ this._driveWrapper,
+ this._FACETING_YIELD_DURATION_MS,
+ this
+ );
+ return;
+ }
+ }
+
+ // we only get here once we are done with the faceters
+ this.callback.call(this.callbackThis);
+ },
+};
+
+var FacetUtils = {
+ _groupSizeComparator(a, b) {
+ return b[1].length - a[1].length;
+ },
+
+ /**
+ * Given a list where each entry is a tuple of [group object, list of items
+ * belonging to that group], produce a new list of the top grouped items. We
+ * used to also produce an "other" aggregation, but that turned out to be
+ * conceptually difficult to deal with, so that's gone, leaving this method
+ * with much less to do.
+ *
+ * @param aAttrDef The attribute for the facet we are working with.
+ * @param aGroups The list of groups built for the facet.
+ * @param aMaxCount The number of result rows you want back.
+ */
+ makeTopGroups(aAttrDef, aGroups, aMaxCount) {
+ let nounDef = aAttrDef.objectNounDef;
+ let realGroupsToUse = aMaxCount;
+
+ let orderedBySize = aGroups.concat();
+ orderedBySize.sort(this._groupSizeComparator);
+
+ // - get the real groups to use and order them by the attribute comparator
+ let outGroups = orderedBySize.slice(0, realGroupsToUse);
+ let comparator = nounDef.comparator;
+ function comparatorHelper(a, b) {
+ return comparator(a[0], b[0]);
+ }
+ outGroups.sort(comparatorHelper);
+
+ return outGroups;
+ },
+};
+
+/**
+ * Facet discrete things like message authors, boolean values, etc. Only
+ * appropriate for use on singular values. Use |DiscreteSetFaceter| for
+ * non-singular values.
+ */
+function DiscreteFaceter(aAttrDef, aFacetDef) {
+ this.attrDef = aAttrDef;
+ this.facetDef = aFacetDef;
+}
+DiscreteFaceter.prototype = {
+ type: "discrete",
+ /**
+ * Facet the given set of items, deferring to the appropriate helper method
+ */
+ facetItems(aItems) {
+ if (this.attrDef.objectNounDef.isPrimitive) {
+ return this.facetPrimitiveItems(aItems);
+ }
+ return this.facetComplexItems(aItems);
+ },
+ /**
+ * Facet an attribute whose value is primitive, meaning that it is a raw
+ * numeric value or string, rather than a complex object.
+ */
+ facetPrimitiveItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+ let filter = this.facetDef.filter;
+
+ let valStrToVal = {};
+ let groups = (this.groups = {});
+ this.groupCount = 0;
+
+ for (let item of aItems) {
+ let val = attrKey in item ? item[attrKey] : null;
+ if (val === GlodaConstants.IGNORE_FACET) {
+ continue;
+ }
+
+ // skip items the filter tells us to ignore
+ if (filter && !filter(val)) {
+ continue;
+ }
+
+ // We need to use hasOwnProperty because we cannot guarantee that the
+ // contents of val won't collide with the attributes in Object.prototype.
+ if (groups.hasOwnProperty(val)) {
+ groups[val].push(item);
+ } else {
+ groups[val] = [item];
+ valStrToVal[val] = val;
+ this.groupCount++;
+ }
+ }
+
+ let orderedGroups = Object.keys(groups).map(key => [
+ valStrToVal[key],
+ groups[key],
+ ]);
+ let comparator = this.facetDef.groupComparator;
+ function comparatorHelper(a, b) {
+ return comparator(a[0], b[0]);
+ }
+ orderedGroups.sort(comparatorHelper);
+ this.orderedGroups = orderedGroups;
+ },
+ /**
+ * Facet an attribute whose value is a complex object that can be identified
+ * by its 'id' attribute. This is the case where the value is itself a noun
+ * instance.
+ */
+ facetComplexItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+ let filter = this.facetDef.filter;
+ let idAttr = this.facetDef.groupIdAttr;
+
+ let groups = (this.groups = {});
+ let groupMap = (this.groupMap = {});
+ this.groupCount = 0;
+
+ for (let item of aItems) {
+ let val = attrKey in item ? item[attrKey] : null;
+ if (val === GlodaConstants.IGNORE_FACET) {
+ continue;
+ }
+
+ // skip items the filter tells us to ignore
+ if (filter && !filter(val)) {
+ continue;
+ }
+
+ let valId = val == null ? null : val[idAttr];
+ // We need to use hasOwnProperty because tag nouns are complex objects
+ // with id's that are non-numeric and so can collide with the contents
+ // of Object.prototype. (Note: the "tags" attribute is actually handled
+ // by the DiscreteSetFaceter.)
+ if (groupMap.hasOwnProperty(valId)) {
+ groups[valId].push(item);
+ } else {
+ groupMap[valId] = val;
+ groups[valId] = [item];
+ this.groupCount++;
+ }
+ }
+
+ let orderedGroups = Object.keys(groups).map(key => [
+ groupMap[key],
+ groups[key],
+ ]);
+ let comparator = this.facetDef.groupComparator;
+ function comparatorHelper(a, b) {
+ return comparator(a[0], b[0]);
+ }
+ orderedGroups.sort(comparatorHelper);
+ this.orderedGroups = orderedGroups;
+ },
+};
+
+/**
+ * Facet sets of discrete items. For example, tags applied to messages.
+ *
+ * The main differences between us and |DiscreteFaceter| are:
+ * - The empty set is notable.
+ * - Specific set configurations could be interesting, but are not low-hanging
+ * fruit.
+ */
+function DiscreteSetFaceter(aAttrDef, aFacetDef) {
+ this.attrDef = aAttrDef;
+ this.facetDef = aFacetDef;
+}
+DiscreteSetFaceter.prototype = {
+ type: "discrete",
+ /**
+ * Facet the given set of items, deferring to the appropriate helper method
+ */
+ facetItems(aItems) {
+ if (this.attrDef.objectNounDef.isPrimitive) {
+ return this.facetPrimitiveItems(aItems);
+ }
+ return this.facetComplexItems(aItems);
+ },
+ /**
+ * Facet an attribute whose value is primitive, meaning that it is a raw
+ * numeric value or string, rather than a complex object.
+ */
+ facetPrimitiveItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+ let filter = this.facetDef.filter;
+
+ let groups = (this.groups = {});
+ let valStrToVal = {};
+ this.groupCount = 0;
+
+ for (let item of aItems) {
+ let vals = attrKey in item ? item[attrKey] : null;
+ if (vals === GlodaConstants.IGNORE_FACET) {
+ continue;
+ }
+
+ if (vals == null || vals.length == 0) {
+ vals = [null];
+ }
+ for (let val of vals) {
+ // skip items the filter tells us to ignore
+ if (filter && !filter(val)) {
+ continue;
+ }
+
+ // We need to use hasOwnProperty because we cannot guarantee that the
+ // contents of val won't collide with the attributes in
+ // Object.prototype.
+ if (groups.hasOwnProperty(val)) {
+ groups[val].push(item);
+ } else {
+ groups[val] = [item];
+ valStrToVal[val] = val;
+ this.groupCount++;
+ }
+ }
+ }
+
+ let orderedGroups = Object.keys(groups).map(key => [
+ valStrToVal[key],
+ groups[key],
+ ]);
+ let comparator = this.facetDef.groupComparator;
+ function comparatorHelper(a, b) {
+ return comparator(a[0], b[0]);
+ }
+ orderedGroups.sort(comparatorHelper);
+ this.orderedGroups = orderedGroups;
+ },
+ /**
+ * Facet an attribute whose value is a complex object that can be identified
+ * by its 'id' attribute. This is the case where the value is itself a noun
+ * instance.
+ */
+ facetComplexItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+ let filter = this.facetDef.filter;
+ let idAttr = this.facetDef.groupIdAttr;
+
+ let groups = (this.groups = {});
+ let groupMap = (this.groupMap = {});
+ this.groupCount = 0;
+
+ for (let item of aItems) {
+ let vals = attrKey in item ? item[attrKey] : null;
+ if (vals === GlodaConstants.IGNORE_FACET) {
+ continue;
+ }
+
+ if (vals == null || vals.length == 0) {
+ vals = [null];
+ }
+ for (let val of vals) {
+ // skip items the filter tells us to ignore
+ if (filter && !filter(val)) {
+ continue;
+ }
+
+ let valId = val == null ? null : val[idAttr];
+ // We need to use hasOwnProperty because tag nouns are complex objects
+ // with id's that are non-numeric and so can collide with the contents
+ // of Object.prototype.
+ if (groupMap.hasOwnProperty(valId)) {
+ groups[valId].push(item);
+ } else {
+ groupMap[valId] = val;
+ groups[valId] = [item];
+ this.groupCount++;
+ }
+ }
+ }
+
+ let orderedGroups = Object.keys(groups).map(key => [
+ groupMap[key],
+ groups[key],
+ ]);
+ let comparator = this.facetDef.groupComparator;
+ function comparatorHelper(a, b) {
+ return comparator(a[0], b[0]);
+ }
+ orderedGroups.sort(comparatorHelper);
+ this.orderedGroups = orderedGroups;
+ },
+};
+
+/**
+ * Given a non-singular attribute, facet it as if it were a boolean based on
+ * whether there is anything in the list (set).
+ */
+function NonEmptySetFaceter(aAttrDef, aFacetDef) {
+ this.attrDef = aAttrDef;
+ this.facetDef = aFacetDef;
+}
+NonEmptySetFaceter.prototype = {
+ type: "boolean",
+ /**
+ * Facet the given set of items, deferring to the appropriate helper method
+ */
+ facetItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+
+ let trueValues = [];
+ let falseValues = [];
+
+ this.groupCount = 0;
+
+ for (let item of aItems) {
+ let vals = attrKey in item ? item[attrKey] : null;
+ if (vals == null || vals.length == 0) {
+ falseValues.push(item);
+ } else {
+ trueValues.push(item);
+ }
+ }
+
+ this.orderedGroups = [];
+ if (trueValues.length) {
+ this.orderedGroups.push([true, trueValues]);
+ }
+ if (falseValues.length) {
+ this.orderedGroups.push([false, falseValues]);
+ }
+ this.groupCount = this.orderedGroups.length;
+ },
+ makeQuery(aGroupValues, aInclusive) {
+ let query = (this.query = lazy.Gloda.newQuery(GlodaConstants.NOUN_MESSAGE));
+
+ let constraintFunc = query[this.attrDef.boundName];
+ constraintFunc.call(query);
+
+ // Our query is always for non-empty lists (at this time), so we want to
+ // invert if they're excluding 'true' or including 'false', which means !=.
+ let invert = aGroupValues[0] != aInclusive;
+
+ return [query, invert];
+ },
+};
+
+/**
+ * Facet dates. We build a hierarchical nested structure of year, month, and
+ * day nesting levels. This decision was made speculatively in the hopes that
+ * it would allow us to do clustered analysis and that there might be a benefit
+ * for that. For example, if you search for "Christmas", we might notice
+ * clusters of messages around December of each year. We could then present
+ * these in a list as likely candidates, rather than a graphical timeline.
+ * Alternately, it could be used to inform a non-linear visualization. As it
+ * stands (as of this writing), it's just a complicating factor.
+ */
+function DateFaceter(aAttrDef, aFacetDef) {
+ this.attrDef = aAttrDef;
+ this.facetDef = aFacetDef;
+}
+DateFaceter.prototype = {
+ type: "date",
+ /**
+ *
+ */
+ facetItems(aItems) {
+ let attrKey = this.attrDef.boundName;
+
+ let years = (this.years = { _subCount: 0 });
+ // generally track the time range
+ let oldest = null,
+ newest = null;
+
+ this.validItems = [];
+
+ // just cheat and put us at the front...
+ this.groupCount = aItems.length ? 1000 : 0;
+ this.orderedGroups = null;
+
+ /** The number of items with a null/missing attribute. */
+ this.missing = 0;
+
+ /**
+ * The number of items with a date that is unreasonably far in the past or
+ * in the future. Old-wise, we are concerned about incorrectly formatted
+ * messages (spam) that end up placed around the UNIX epoch. New-wise,
+ * we are concerned about messages that can't be explained by users who
+ * don't know how to set their clocks (both the current user and people
+ * sending them mail), mainly meaning spam.
+ * We want to avoid having our clever time-scale logic being made useless by
+ * these unreasonable messages.
+ */
+ this.unreasonable = 0;
+ // feb 1, 1970
+ let tooOld = new Date(1970, 1, 1);
+ // 3 days from now
+ let tooNew = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
+
+ for (let item of aItems) {
+ let val = attrKey in item ? item[attrKey] : null;
+ // -- missing
+ if (val == null) {
+ this.missing++;
+ continue;
+ }
+
+ // -- unreasonable
+ if (val < tooOld || val > tooNew) {
+ this.unreasonable++;
+ continue;
+ }
+
+ this.validItems.push(item);
+
+ // -- time range
+ if (oldest == null) {
+ oldest = newest = val;
+ } else if (val < oldest) {
+ oldest = val;
+ } else if (val > newest) {
+ newest = val;
+ }
+
+ // -- bucket
+ // - year
+ let year,
+ valYear = val.getYear();
+ if (valYear in years) {
+ year = years[valYear];
+ year._dateCount++;
+ } else {
+ year = years[valYear] = {
+ _dateCount: 1,
+ _subCount: 0,
+ };
+ years._subCount++;
+ }
+
+ // - month
+ let month,
+ valMonth = val.getMonth();
+ if (valMonth in year) {
+ month = year[valMonth];
+ month._dateCount++;
+ } else {
+ month = year[valMonth] = {
+ _dateCount: 1,
+ _subCount: 0,
+ };
+ year._subCount++;
+ }
+
+ // - day
+ let valDate = val.getDate();
+ if (valDate in month) {
+ month[valDate].push(item);
+ } else {
+ month[valDate] = [item];
+ }
+ }
+
+ this.oldest = oldest;
+ this.newest = newest;
+ },
+
+ _unionMonth(aMonthObj) {
+ let dayItemLists = [];
+ for (let key in aMonthObj) {
+ let dayItemList = aMonthObj[key];
+ if (typeof key == "string" && key.startsWith("_")) {
+ continue;
+ }
+ dayItemLists.push(dayItemList);
+ }
+ return dayItemLists;
+ },
+
+ _unionYear(aYearObj) {
+ let monthItemLists = [];
+ for (let key in aYearObj) {
+ let monthObj = aYearObj[key];
+ if (typeof key == "string" && key.startsWith("_")) {
+ continue;
+ }
+ monthItemLists.push(this._unionMonth(monthObj));
+ }
+ return monthItemLists;
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/Gloda.jsm b/comm/mailnews/db/gloda/modules/Gloda.jsm
new file mode 100644
index 0000000000..77b2288e53
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/Gloda.jsm
@@ -0,0 +1,2275 @@
+/* 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 = ["Gloda"];
+
+const { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+const {
+ GlodaAttributeDBDef,
+ GlodaAccount,
+ GlodaConversation,
+ GlodaFolder,
+ GlodaMessage,
+ GlodaContact,
+ GlodaIdentity,
+ GlodaAttachment,
+} = ChromeUtils.import("resource:///modules/gloda/GlodaDataModel.jsm");
+const { GlodaCollection, GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { whittlerRegistry, mimeMsgToContentAndMeta } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaContent.jsm"
+);
+const { GlodaQueryClassFactory } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaQueryClassFactory.jsm"
+);
+const { GlodaUtils } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaUtils.jsm"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * @see |Gloda.BadItemContentsError|
+ */
+function BadItemContentsError(aMessage) {
+ this.message = aMessage;
+}
+BadItemContentsError.prototype = {
+ toString() {
+ return this.message;
+ },
+};
+
+/**
+ * Provides the user-visible (and extension visible) global database
+ * functionality. There is currently a dependency/ordering
+ * problem in that the concept of 'gloda' also includes some logic that is
+ * contributed by built-in extensions, if you will. Those built-in extensions
+ * (fundattr.js, GlodaExplicitAttr.jsm) also import this file. To avoid a circular
+ * dependency, those built-in extensions are loaded by Everybody.jsm. The
+ * simplest/best solution is probably to move Everybody.jsm to be Gloda.jsm and
+ * have it re-export only 'Gloda'. Gloda.jsm (this file) can then move to be
+ * gloda_int.js (or whatever our eventual naming scheme is), which built-in
+ * extensions can explicitly rely upon.
+ *
+ * === Concepts
+ *
+ * == Nouns
+ *
+ * Inspired by reasonable uses of triple-stores, I have tried to leverage
+ * existing model and terminology rather than rolling out own for everything.
+ * The idea with triple-stores is that you have a subject, a predicate, and an
+ * object. For example, if we are talking about a message, that is the
+ * subject, the predicate could roughly be sent-by, and the object a person.
+ * We can generalize this idea to say that the subject and objects are nouns.
+ * Since we want to be more flexible than only dealing with messages, we
+ * therefore introduce the concept of nouns as an organizing principle.
+ *
+ * == Attributes
+ *
+ * Our attributes definitions are basically our predicates. When we define
+ * an attribute, it's a label with a bunch of meta-data. Our attribute
+ * instances are basically a 'triple' in a triple-store. The attributes
+ * are stored in database rows that imply a specific noun-type (ex: the
+ * messageAttributes table), with an ID identifying the message which is our
+ * subject, an attribute ID which identifies the attribute definition in use
+ * (and therefore the predicate), plus an object ID (given context aka the
+ * noun type by the attribute's meta-data) which identifies the 'object'.
+ *
+ * == But...
+ *
+ * Things aren't entirely as clear as they could be right now, terminology/
+ * concept/implementation-wise. Some work is probably still in order.
+ *
+ * === Implementation
+ *
+ * == Nouns
+ *
+ * So, we go and define the nouns that are roughly the classes in our data
+ * model. Every 'class' we define in GlodaDataModel.jsm is a noun that gets defined
+ * here in the Gloda core. We provide sufficient meta-data about the noun to
+ * serialize/deserialize its representation from our database representation.
+ * Nouns do not have to be defined in this class, but can also be contributed
+ * by external code.
+ * We have a concept of 'first class' nouns versus non-first class nouns. The
+ * distinction is meant to be whether we can store meta-information about those
+ * nouns using attributes. Right now, only message are real first-class nouns,
+ * but we want to expand that to include contacts and eventually events and
+ * tasks as lightning-integration occurs. In practice, we are stretching the
+ * definition of first-class nouns slightly to include things we can't store
+ * meta-data about, but want to be able to query about. We do want to resolve
+ * this.
+ *
+ * == Attributes
+ *
+ * Attributes are defined by "attribute providers" who are responsible for
+ * taking an instance of a first-class noun (for which they are registered)
+ * plus perhaps some other meta-data, and returning a list of attributes
+ * extracted from that noun. For now, this means messages. Attribute
+ * providers may create new data records as a side-effect of the indexing
+ * process, although we have not yet fully dealt with the problem of deleting
+ * these records should they become orphaned in the database due to the
+ * purging of a message and its attributes.
+ * All of the 'core' gloda attributes are provided by the GlodaFundAttr.jsm and
+ * GlodaExplicitAttr.jsm providers.
+ *
+ * === (Notable) Future Work
+ *
+ * == Attributes
+ *
+ * Attribute mechanisms currently lack any support for 'overriding' attributes
+ * provided by other attribute providers. For example, the fundattr provider
+ * tells us who a message is 'from' based on the e-mail address present.
+ * However, other plugins may actually know better. For example, the bugzilla
+ * daemon e-mails based on bug activity although the daemon gets the credit
+ * as the official sender. A bugzilla plugin can easily extract the actual
+ * person/e-mail addressed who did something on the bug to cause the
+ * notification to be sent. In practice, we would like that person to be
+ * the 'sender' of the bugmail. But we can't really do that right, yet.
+ *
+ * @namespace
+ */
+var Gloda = {
+ /**
+ * Initialize logging, the datastore (SQLite database), the core nouns and
+ * attributes, and the contact and identities that belong to the presumed
+ * current user (based on accounts).
+ *
+ * Additional nouns and the core attribute providers are initialized by the
+ * Everybody.jsm module which ensures all of those dependencies are loaded
+ * (and initialized).
+ */
+ _init() {
+ this._initLogging();
+ GlodaDatastore._init(this._nounIDToDef);
+ this._initAttributes();
+ this._initMyIdentities();
+ },
+
+ _log: null,
+ /**
+ * Initialize logging; the error console window gets Warning/Error, and stdout
+ * (via dump) gets everything.
+ */
+ _initLogging() {
+ this._log = console.createInstance({
+ prefix: "gloda",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+ this._log.info("Logging Initialized");
+ },
+
+ /**
+ * Callers should access the unique ID for the GlodaDatastore
+ * with this getter. If the GlodaDatastore has not been
+ * initialized, this value is null.
+ *
+ * @returns a UUID as a string, ex: "c4dd0159-9287-480f-a648-a4613e147fdb"
+ */
+ get datastoreID() {
+ return GlodaDatastore._datastoreID;
+ },
+
+ /**
+ * Lookup a gloda message from an nsIMsgDBHdr, with the result returned as a
+ * collection. Keep in mind that the message may not be indexed, so you
+ * may end up with an empty collection. (Also keep in mind that this query
+ * is asynchronous, so you will want your action-taking logic to be found
+ * in your listener's onQueryCompleted method; the result will not be in
+ * the collection when this method returns.)
+ *
+ * @param aMsgHdr The header of the message you want the gloda message for.
+ * @param aListener The listener that should be registered with the collection
+ * @param aData The (optional) value to set as the data attribute on the
+ * collection.
+ *
+ * @returns The collection that will receive the results.
+ *
+ * @testpoint gloda.ns.getMessageCollectionForHeader()
+ */
+ getMessageCollectionForHeader(aMsgHdr, aListener, aData) {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.folder(aMsgHdr.folder).messageKey(aMsgHdr.messageKey);
+ return query.getCollection(aListener, aData);
+ },
+
+ /**
+ * Given a list of message headers, return a collection containing the gloda
+ * messages that correspond to those headers. Keep in mind that gloda may
+ * not have indexed all the messages, so the returned collection may not have
+ * a message for each header you provide. (Also keep in mind that this query
+ * is asynchronous, so you will want your action-taking logic to be found
+ * in your listener's onQueryCompleted method; no results will be present in
+ * the collection when this method returns.)
+ *
+ * @param aHeaders An array of headers
+ * @param aListener The listener that should be registered with the collection
+ * @param aData The (optional) value to set as the data attribute on the
+ * collection.
+ *
+ * @returns The collection that will receive the results.
+ *
+ * @testpoint gloda.ns.getMessageCollectionForHeaders()
+ */
+ getMessageCollectionForHeaders(aHeaders, aListener, aData) {
+ // group the headers by the folder they are found in
+ let headersByFolder = {};
+ for (let header of aHeaders) {
+ let folderURI = header.folder.URI;
+ let headersForFolder = headersByFolder[folderURI];
+ if (headersForFolder === undefined) {
+ headersByFolder[folderURI] = [header];
+ } else {
+ headersForFolder.push(header);
+ }
+ }
+
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ let clause;
+ // build a query, using a separate union clause for each folder.
+ for (let folderURI in headersByFolder) {
+ let headersForFolder = headersByFolder[folderURI];
+ let folder = this.getFolderForFolder(headersForFolder[0].folder);
+ // if this is the first or clause, just use the query itself
+ if (!clause) {
+ clause = query;
+ } else {
+ // Create a new query clause via the 'or' command.
+ clause = query.or();
+ }
+
+ clause.folder(folder);
+ let messageKeys = headersForFolder.map(hdr => hdr.messageKey);
+ clause.messageKey.apply(clause, messageKeys);
+ }
+
+ return query.getCollection(aListener, aData);
+ },
+
+ /**
+ * @testpoint gloda.ns.getMessageContent
+ */
+ getMessageContent(aGlodaMessage, aMimeMsg) {
+ return mimeMsgToContentAndMeta(
+ aMimeMsg,
+ aGlodaMessage.folderMessage.folder
+ )[0];
+ },
+
+ getFolderForFolder(aMsgFolder) {
+ return GlodaDatastore._mapFolder(aMsgFolder);
+ },
+
+ /**
+ * Takes one or more strings containing lists of comma-delimited e-mail
+ * addresses with optional display names, and returns a list of sub-lists of
+ * identities, where each sub-list corresponds to each of the strings passed
+ * as arguments. These identities are loaded from the database if they
+ * already exist, or created if they do not yet exist.
+ * If the identities need to be created, they will also result in the
+ * creation of a gloda contact. If a display name was provided with the
+ * e-mail address, it will become the name of the gloda contact. If a
+ * display name was not provided, the e-mail address will also serve as the
+ * contact name.
+ * This method uses the indexer's callback handle mechanism, and does not
+ * obey traditional return semantics.
+ *
+ * We normalize all e-mail addresses to be lowercase as a normative measure.
+ *
+ * @param aCallbackHandle The GlodaIndexer callback handle (or equivalent)
+ * that you are operating under.
+ * @param aAddrGroups... One or more strings. Each string can contain zero or more
+ * e-mail addresses with display name. If more than one address is given,
+ * they should be comma-delimited. For example
+ * '"Bob Smith" <bob@example.com>' is an address with display name. Mime
+ * header decoding is performed, but is ignorant of any folder-level
+ * character set overrides.
+ * @returns via the callback handle mechanism, a list containing one sub-list
+ * for each string argument passed. Each sub-list contains zero or more
+ * GlodaIdentity instances corresponding to the addresses provided.
+ */
+ *getOrCreateMailIdentities(aCallbackHandle, ...aAddrGroups) {
+ let addresses = {};
+ let resultLists = [];
+
+ // parse the strings
+ for (let aMailAddresses of aAddrGroups) {
+ let parsed = GlodaUtils.parseMailAddresses(aMailAddresses);
+
+ let resultList = [];
+ resultLists.push(resultList);
+
+ for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
+ let address = parsed.addresses[iAddress].toLowerCase();
+ if (address in addresses) {
+ addresses[address].push(resultList);
+ } else {
+ addresses[address] = [parsed.names[iAddress], resultList];
+ }
+ }
+ }
+
+ let addressList = Object.keys(addresses);
+ if (addressList.length == 0) {
+ yield aCallbackHandle.doneWithResult(resultLists);
+ // we should be stopped before we reach this point, but safety first.
+ return;
+ }
+
+ let query = this.newQuery(GlodaConstants.NOUN_IDENTITY);
+ query.kind("email");
+ query.value.apply(query, addressList);
+ let collection = query.getCollection(aCallbackHandle);
+ yield GlodaConstants.kWorkAsync;
+
+ // put the identities in the appropriate result lists
+ for (let identity of collection.items) {
+ let nameAndResultLists = addresses[identity.value];
+ this._log.debug(
+ " found identity for '" +
+ nameAndResultLists[0] +
+ "' (" +
+ identity.value +
+ ")"
+ );
+ // index 0 is the name, skip it
+ for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+ nameAndResultLists[iResList].push(identity);
+ }
+ delete addresses[identity.value];
+ }
+
+ // create the identities that did not exist yet
+ for (let address in addresses) {
+ let nameAndResultLists = addresses[address];
+ let name = nameAndResultLists[0];
+
+ this._log.debug(" creating contact for '" + name + "' (" + address + ")");
+
+ // try and find an existing address book contact.
+ let card = MailServices.ab.cardForEmailAddress(address);
+ // XXX when we have the address book GUID stuff, we need to use that to
+ // find existing contacts... (this will introduce a new query phase
+ // where we batch all the GUIDs for an async query)
+ // XXX when the address book supports multiple e-mail addresses, we
+ // should also just create identities for any that don't yet exist
+
+ // if there is no name, just use the e-mail (the ab indexer actually
+ // processes the card's displayName for synchronization, so we don't
+ // need to do that.)
+ if (!name) {
+ name = address;
+ }
+
+ let contact = GlodaDatastore.createContact(null, null, name, 0, 0);
+
+ // we must create the identity. use a blank description because there's
+ // nothing to differentiate it from other identities, as this contact
+ // only has one initially (us).
+ // XXX when we have multiple e-mails and there is a meaning associated
+ // with each e-mail, try and use that to populate the description.
+ // XXX we are creating the identity here before we insert the contact.
+ // conceptually it is good for us to be creating the identity before
+ // exposing it to the address-book indexer, but we could get our id's
+ // in a bad way from not deferring the identity insertion until after
+ // the contact insertion.
+ let identity = GlodaDatastore.createIdentity(
+ contact.id,
+ contact,
+ "email",
+ address,
+ /* description */ "",
+ /* relay? */ false
+ );
+ contact._identities = [identity];
+
+ // give the address book indexer a chance if we have a card.
+ // (it will fix-up the name based on the card as appropriate)
+ if (card) {
+ yield aCallbackHandle.pushAndGo(
+ Gloda.grokNounItem(contact, { card }, true, true, aCallbackHandle)
+ );
+ } else {
+ // grokNounItem will issue the insert for us...
+ GlodaDatastore.insertContact(contact);
+ }
+
+ for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+ nameAndResultLists[iResList].push(identity);
+ }
+ }
+
+ yield aCallbackHandle.doneWithResult(resultLists);
+ },
+
+ /**
+ * Dictionary of the user's known identities; key is the identity id, value
+ * is the actual identity. This is populated by _initMyIdentities based on
+ * the accounts defined.
+ */
+ myIdentities: {},
+ /**
+ * The contact corresponding to the current user. We are assuming that only
+ * a single user/human being uses the current profile. This is known to be
+ * a flawed assumption, but is the best first approximation available.
+ * The contact is based on the default account's default identity. The user
+ * can change both, if desired, in Account Settings.
+ *
+ * @TODO attempt to deal with multiple people using the same profile
+ */
+ myContact: null,
+ /**
+ * Populate myIdentities with all of our identities. Currently we do this
+ * by assuming that there is one human/user per profile, and that all of the
+ * accounts defined in the profile belong to them. The single contact is
+ * stored on myContact.
+ *
+ * @TODO deal with account addition/modification/removal
+ * @TODO attempt to deal with multiple people using the same profile
+ */
+ _initMyIdentities() {
+ let myContact = null;
+ let myIdentities = {};
+ // Process each email at most once; stored here.
+ let myEmailAddresses = new Set();
+
+ let fullName, fallbackName;
+ let existingIdentities = [];
+ let identitiesToCreate = [];
+
+ let allIdentities = MailServices.accounts.allIdentities;
+ let defaultMsgIdentity = MailServices.accounts.defaultAccount
+ ? MailServices.accounts.defaultAccount.defaultIdentity
+ : null;
+ let defaultMsgIdentityKey = defaultMsgIdentity
+ ? defaultMsgIdentity.key
+ : null;
+ let defaultIdentity;
+
+ // Nothing to do if there are no accounts/identities.
+ if (allIdentities.length == 0) {
+ return;
+ }
+
+ for (let msgIdentity of allIdentities) {
+ let emailAddress = msgIdentity.email;
+ let replyTo = msgIdentity.replyTo;
+ let msgIdentityDescription = msgIdentity.fullName || msgIdentity.email;
+ let isDefaultMsgIdentity = msgIdentity.key == defaultMsgIdentityKey;
+
+ if (!fullName || isDefaultMsgIdentity) {
+ fullName = msgIdentity.fullName;
+ }
+ if (!fallbackName || isDefaultMsgIdentity) {
+ fallbackName = msgIdentity.email;
+ }
+
+ // Find the identities if they exist, flag to create them if they don't.
+ for (let address of [emailAddress, replyTo]) {
+ if (!address) {
+ continue;
+ }
+ let parsed = GlodaUtils.parseMailAddresses(address);
+ if (myEmailAddresses.has(parsed.addresses[0])) {
+ continue;
+ }
+ let identity = GlodaDatastore.getIdentity("email", parsed.addresses[0]);
+ if (identity) {
+ if (identity.description != msgIdentityDescription) {
+ // If the user changed the identity name, update the db.
+ identity._description = msgIdentityDescription;
+ GlodaDatastore.updateIdentity(identity);
+ }
+ existingIdentities.push(identity);
+ if (isDefaultMsgIdentity) {
+ defaultIdentity = identity;
+ }
+ } else {
+ identitiesToCreate.push([
+ parsed.addresses[0],
+ msgIdentityDescription,
+ ]);
+ }
+ myEmailAddresses.add(parsed.addresses[0]);
+ }
+ }
+ // We need to establish the identity.contact portions of the relationship.
+ for (let identity of existingIdentities) {
+ identity._contact = GlodaDatastore.getContactByID(identity.contactID);
+ if (defaultIdentity && defaultIdentity.id == identity.id) {
+ if (identity.contact.name != (fullName || fallbackName)) {
+ // If the user changed the default identity, update the db.
+ identity.contact.name = fullName || fallbackName;
+ GlodaDatastore.updateContact(identity.contact);
+ }
+ defaultIdentity._contact = identity.contact;
+ }
+ }
+
+ if (defaultIdentity) {
+ // The contact is based on the default account's default identity.
+ myContact = defaultIdentity.contact;
+ } else if (existingIdentities.length) {
+ // Just use the first guy's contact.
+ myContact = existingIdentities[0].contact;
+ } else {
+ // Create a new contact.
+ myContact = GlodaDatastore.createContact(
+ null,
+ null,
+ fullName || fallbackName,
+ 0,
+ 0
+ );
+ GlodaDatastore.insertContact(myContact);
+ }
+
+ for (let emailAndDescription of identitiesToCreate) {
+ // XXX This won't always be of type "email" as we add new account types.
+ let identity = GlodaDatastore.createIdentity(
+ myContact.id,
+ myContact,
+ "email",
+ emailAndDescription[0],
+ emailAndDescription[1],
+ false
+ );
+ existingIdentities.push(identity);
+ }
+
+ for (let identity of existingIdentities) {
+ myIdentities[identity.id] = identity;
+ }
+
+ this.myContact = myContact;
+ this.myIdentities = myIdentities;
+ myContact._identities = Object.keys(myIdentities).map(
+ id => myIdentities[id]
+ );
+
+ // We need contacts to make these objects reachable via the collection
+ // manager.
+ this._myContactCollection = this.explicitCollection(
+ GlodaConstants.NOUN_CONTACT,
+ [this.myContact]
+ );
+ this._myIdentitiesCollection = this.explicitCollection(
+ GlodaConstants.NOUN_IDENTITY,
+ this.myContact._identities
+ );
+ },
+
+ /** Next Noun ID to hand out, these don't need to be persisted (for now). */
+ _nextNounID: 1000,
+
+ /**
+ * Maps noun names to noun IDs.
+ */
+ _nounNameToNounID: {},
+ /**
+ * Maps noun IDs to noun definition dictionaries. (Noun definition
+ * dictionaries provided to us at the time a noun was defined, plus some
+ * additional stuff we put in there.)
+ */
+ _nounIDToDef: {},
+
+ _managedToJSON(aItem) {
+ return aItem.id;
+ },
+
+ /**
+ * Define a noun. Takes a dictionary with the following keys/values:
+ *
+ * @param aNounDef.name The name of the noun. This is not a display name
+ * (anything being displayed needs to be localized, after all), but simply
+ * the canonical name for debugging purposes and for people to pass to
+ * lookupNoun. The suggested convention is lower-case-dash-delimited,
+ * with names being singular (since it's a single noun we are referring
+ * to.)
+ * @param aNounDef.class The 'class' to which an instance of the noun will
+ * belong (aka will pass an instanceof test). You may also provide this
+ * as 'clazz' if the keyword makes your IDE angry.
+ * @param aNounDef.allowsArbitraryAttrs Is this a 'first class noun'/can it be
+ * a subject, AKA can this noun have attributes stored on it that relate
+ * it to other things? For example, a message is first-class; we store
+ * attributes of messages. A date is not first-class now, nor is it
+ * likely to be; we will not store attributes about a date, although dates
+ * will be the objects of other subjects. (For example: we might
+ * associate a date with a calendar event, but the date is an attribute of
+ * the calendar event and not vice versa.)
+ * @param aNounDef.usesParameter A boolean indicating whether this noun
+ * requires use of the 'parameter' BLOB storage field on the attribute
+ * bindings in the database to persist itself. Use of parameters should
+ * be limited to a reasonable number of values (16-32 is okay, more than
+ * that is pushing it and 256 should be considered an absolute upper
+ * bound) because of the database organization. When false, your
+ * toParamAndValue function is expected to return null for the parameter
+ * and likewise your fromParamAndValue should expect ignore and generally
+ * ignore the argument.
+ * @param aNounDef.toParamAndValue A function that takes an instantiated noun
+ * instance and returns a 2-element list of [parameter, value] where
+ * parameter may only be non-null if you passed a usesParameter of true.
+ * Parameter may be of any type (BLOB), and value must be numeric (pass
+ * 0 if you don't need the value).
+ *
+ * @param aNounDef.isPrimitive True when the noun instance is a raw numeric
+ * value/string/boolean. False when the instance is an object. When
+ * false, it is assumed the attribute that serves as a unique identifier
+ * for the value is "id" unless 'idAttr' is provided.
+ * @param [aNounDef.idAttr="id"] For non-primitive nouns, this is the
+ * attribute on the object that uniquely identifies it.
+ *
+ * @param aNounDef.schema Unsupported mechanism by which you can define a
+ * table that corresponds to this noun. The table will be created if it
+ * does not exist.
+ * - name The table name; don't conflict with other things!
+ * - columns A list of [column name, sqlite type] tuples. You should
+ * always include a definition like ["id", "INTEGER PRIMARY KEY"] for
+ * now (and it should be the first column name too.) If you care about
+ * how the attributes are poked into your object (for example, you want
+ * underscores used for some of them because the attributes should be
+ * immutable), then you can include a third string that is the name of
+ * the attribute to use.
+ * - indices A dictionary of lists of column names, where the key name
+ * becomes the index name. Ex: {foo: ["bar"]} results in an index on
+ * the column "bar" where the index is named "foo".
+ */
+ defineNoun(aNounDef, aNounID) {
+ this._log.info("Defining noun: " + aNounDef.name);
+ if (aNounID === undefined) {
+ aNounID = this._nextNounID++;
+ }
+ aNounDef.id = aNounID;
+
+ // Let people whose editors get angry about illegal attribute names use
+ // clazz instead of class.
+ if (aNounDef.clazz) {
+ aNounDef.class = aNounDef.clazz;
+ }
+
+ if (!("idAttr" in aNounDef)) {
+ aNounDef.idAttr = "id";
+ }
+ if (!("comparator" in aNounDef)) {
+ aNounDef.comparator = function () {
+ throw new Error(
+ "Noun type '" + aNounDef.name + "' lacks a real comparator."
+ );
+ };
+ }
+
+ // We allow nouns to have data tables associated with them where we do all
+ // the legwork. The schema attribute is the gateway to this magical world
+ // of functionality. Said door is officially unsupported.
+ if (aNounDef.schema) {
+ if (!aNounDef.tableName) {
+ if (aNounDef.schema.name) {
+ aNounDef.tableName = "ext_" + aNounDef.schema.name;
+ } else {
+ aNounDef.tableName = "ext_" + aNounDef.name;
+ }
+ }
+ // this creates the data table and binder and hooks everything up
+ GlodaDatastore.createNounTable(aNounDef);
+
+ if (!aNounDef.toParamAndValue) {
+ aNounDef.toParamAndValue = function (aThing) {
+ if (aThing instanceof aNounDef.class) {
+ return [null, aThing.id];
+ }
+ // assume they're just passing the id directly
+ return [null, aThing];
+ };
+ }
+ }
+
+ // if it has a table, you can query on it. seems straight-forward.
+ if (aNounDef.tableName) {
+ [
+ aNounDef.queryClass,
+ aNounDef.nullQueryClass,
+ aNounDef.explicitQueryClass,
+ aNounDef.wildcardQueryClass,
+ ] = GlodaQueryClassFactory(aNounDef);
+ aNounDef._dbMeta = {};
+ aNounDef.class.prototype.NOUN_ID = aNounDef.id;
+ aNounDef.class.prototype.NOUN_DEF = aNounDef;
+ aNounDef.toJSON = this._managedToJSON;
+
+ aNounDef.specialLoadAttribs = [];
+
+ // - define the 'id' constrainer
+ let idConstrainer = function (...aArgs) {
+ let constraint = [GlodaConstants.kConstraintIdIn, null, ...aArgs];
+ this._constraints.push(constraint);
+ return this;
+ };
+ aNounDef.queryClass.prototype.id = idConstrainer;
+ }
+ if (aNounDef.cache) {
+ let cacheCost = aNounDef.cacheCost || 1024;
+ let cacheBudget = aNounDef.cacheBudget || 128 * 1024;
+ let cacheSize = Math.floor(cacheBudget / cacheCost);
+ if (cacheSize) {
+ GlodaCollectionManager.defineCache(aNounDef, cacheSize);
+ }
+ }
+ aNounDef.attribsByBoundName = {};
+ aNounDef.domExposeAttribsByBoundName = {};
+
+ aNounDef.objectNounOfAttributes = [];
+
+ this._nounNameToNounID[aNounDef.name] = aNounID;
+ this._nounIDToDef[aNounID] = aNounDef;
+ aNounDef.actions = [];
+
+ this._attrProviderOrderByNoun[aNounDef.id] = [];
+ this._attrOptimizerOrderByNoun[aNounDef.id] = [];
+ this._attrProvidersByNoun[aNounDef.id] = {};
+
+ return aNounDef;
+ },
+
+ /**
+ * Lookup a noun (ID) suitable for passing to defineAttribute's various
+ * noun arguments. Throws an exception if the noun with the given name
+ * cannot be found; the assumption is that you can't live without the noun.
+ */
+ lookupNoun(aNounName) {
+ if (aNounName in this._nounNameToNounID) {
+ return this._nounNameToNounID[aNounName];
+ }
+
+ throw Error(
+ "Unable to locate noun with name '" +
+ aNounName +
+ "', but I " +
+ "do know about: " +
+ Object.keys(this._nounNameToNounID).join(", ")
+ );
+ },
+
+ /**
+ * Lookup a noun def given a name.
+ */
+ lookupNounDef(aNounName) {
+ return this._nounIDToDef[this.lookupNoun(aNounName)];
+ },
+
+ /**
+ * Define an action on a noun. During the prototype stage, this was conceived
+ * of as a way to expose all the constraints possible given a noun. For
+ * example, if you have an identity or a contact, you could use this to
+ * see all the messages sent from/to a given contact. It was likewise
+ * thought potentially usable for future expansion. For example, you could
+ * also decide to send an e-mail to a contact when you have the contact
+ * instance available.
+ * Outside of the 'expmess' checkbox-happy prototype, this functionality is
+ * not used. As such, this functionality should be considered in flux and
+ * subject to changes. Also, very open to specific suggestsions motivated
+ * by use cases.
+ * One conceptual issue raised by this mechanism is the interaction of actions
+ * with facts like "this message is read". We currently implement the 'fact'
+ * by defining an attribute with a 'boolean' noun type. To deal with this,
+ * in various places we pass-in the attribute as well as the noun value.
+ * Since the relationships for booleans and integers in these cases is
+ * standard and well-defined, this works out pretty well, but suggests we
+ * need to think things through.
+ *
+ * @param aNounID The ID of the noun you want to define an action on.
+ * @param aActionMeta The dictionary describing the noun. The dictionary
+ * should have the following fields:
+ * - actionType: a string indicating the type of action. Currently, only
+ * "filter" is a legal value.
+ * - actionTarget: the noun ID of the noun type on which this action is
+ * applicable. For example,
+ *
+ * The following should be present for actionType=="filter";
+ * - shortName: The name that should be used to display this constraint. For
+ * example, a checkbox-heavy UI might display a checkbox for each constraint
+ * using shortName as the label.
+ * - makeConstraint: A function that takes the attribute that is the source
+ * of the noun and the noun instance as arguments, and returns APV-style
+ * constraints. Since the APV-style query mechanism is now deprecated,
+ * this signature is deprecated. Probably the way to update this would be
+ * to pass in the query instance that constraints should be contributed to.
+ */
+ defineNounAction(aNounID, aActionMeta) {
+ let nounDef = this._nounIDToDef[aNounID];
+ nounDef.actions.push(aActionMeta);
+ },
+
+ /**
+ * Retrieve all of the actions (as defined using defineNounAction) for the
+ * given noun type (via noun ID) with the given action type (ex: filter).
+ */
+ getNounActions(aNounID, aActionType) {
+ let nounDef = this._nounIDToDef[aNounID];
+ if (!nounDef) {
+ return [];
+ }
+ return nounDef.actions.filter(
+ action => !aActionType || action.actionType == aActionType
+ );
+ },
+
+ /** Attribute providers in the sequence to process them. */
+ _attrProviderOrderByNoun: {},
+ /** Attribute providers that provide optimizers, in the sequence to proc. */
+ _attrOptimizerOrderByNoun: {},
+ /** Maps attribute providers to the list of attributes they provide */
+ _attrProviders: {},
+ /**
+ * Maps nouns to their attribute providers to a list of the attributes they
+ * provide for the noun.
+ */
+ _attrProvidersByNoun: {},
+
+ /**
+ * Define the core nouns (that are not defined elsewhere) and a few noun
+ * actions. Core nouns could be defined in other files, assuming dependency
+ * issues are resolved via the Everybody.jsm mechanism or something else.
+ * Right now, noun_tag defines the tag noun. If we broke more of these out,
+ * we would probably want to move the 'class' code from GlodaDataModel.jsm, the
+ * SQL table def and helper code from GlodaDatastore.jsm (and this code) to their
+ * own noun_*.js files. There are some trade-offs to be made, and I think
+ * we can deal with those once we start to integrate lightning/calendar and
+ * our noun space gets large and more heterogeneous.
+ */
+ _initAttributes() {
+ this.defineNoun(
+ {
+ name: "bool",
+ clazz: Boolean,
+ allowsArbitraryAttrs: false,
+ isPrimitive: true,
+ // favor true before false
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return b - a;
+ },
+ toParamAndValue(aBool) {
+ return [null, aBool ? 1 : 0];
+ },
+ },
+ GlodaConstants.NOUN_BOOLEAN
+ );
+ this.defineNoun(
+ {
+ name: "number",
+ clazz: Number,
+ allowsArbitraryAttrs: false,
+ continuous: true,
+ isPrimitive: true,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a - b;
+ },
+ toParamAndValue(aNum) {
+ return [null, aNum];
+ },
+ },
+ GlodaConstants.NOUN_NUMBER
+ );
+ this.defineNoun(
+ {
+ name: "string",
+ clazz: String,
+ allowsArbitraryAttrs: false,
+ isPrimitive: true,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.localeCompare(b);
+ },
+ toParamAndValue(aString) {
+ return [null, aString];
+ },
+ },
+ GlodaConstants.NOUN_STRING
+ );
+ this.defineNoun(
+ {
+ name: "date",
+ clazz: Date,
+ allowsArbitraryAttrs: false,
+ continuous: true,
+ isPrimitive: true,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a - b;
+ },
+ toParamAndValue(aDate) {
+ return [null, aDate.valueOf() * 1000];
+ },
+ },
+ GlodaConstants.NOUN_DATE
+ );
+ this.defineNoun(
+ {
+ name: "fulltext",
+ clazz: String,
+ allowsArbitraryAttrs: false,
+ continuous: false,
+ isPrimitive: true,
+ comparator(a, b) {
+ throw new Error("Fulltext nouns are not comparable!");
+ },
+ // as noted on NOUN_FULLTEXT, we just pass the string around. it never
+ // hits the database, so it's okay.
+ toParamAndValue(aString) {
+ return [null, aString];
+ },
+ },
+ GlodaConstants.NOUN_FULLTEXT
+ );
+
+ this.defineNoun(
+ {
+ name: "folder",
+ clazz: GlodaFolder,
+ allowsArbitraryAttrs: false,
+ isPrimitive: false,
+ queryHelpers: {
+ /**
+ * Query for accounts based on the account associated with folders. We
+ * walk all of the folders associated with an account and put them in
+ * the list of folders that match if gloda would index them. This is
+ * unsuitable for producing a persistable constraint since it does not
+ * adapt for added/deleted folders. However, it is sufficient for
+ * faceting. Also, we don't persist constraints yet.
+ *
+ * @TODO The long-term solution is to move towards using arithmetic
+ * encoding on folder-id's like we use for MIME types and friends.
+ */
+ Account(aAttrDef, aArguments) {
+ let folderValues = [];
+ let seenRootFolders = {};
+ for (let iArg = 0; iArg < aArguments.length; iArg++) {
+ let givenFolder = aArguments[iArg];
+ let givenMsgFolder = givenFolder.getXPCOMFolder(
+ givenFolder.kActivityFolderOnlyNoData
+ );
+ let rootFolder = givenMsgFolder.rootFolder;
+
+ // skip processing this folder if we have already processed its
+ // root folder.
+ if (rootFolder.URI in seenRootFolders) {
+ continue;
+ }
+ seenRootFolders[rootFolder.URI] = true;
+
+ for (let folder of rootFolder.descendants) {
+ let folderFlags = folder.flags;
+
+ // Ignore virtual folders, non-mail folders.
+ // XXX this is derived from GlodaIndexer's shouldIndexFolder.
+ // This should probably just use centralized code or the like.
+ if (
+ !(folderFlags & Ci.nsMsgFolderFlags.Mail) ||
+ folderFlags & Ci.nsMsgFolderFlags.Virtual
+ ) {
+ continue;
+ }
+ // we only index local or IMAP folders
+ if (
+ !(folder instanceof Ci.nsIMsgLocalMailFolder) &&
+ !(folder instanceof Ci.nsIMsgImapMailFolder)
+ ) {
+ continue;
+ }
+
+ let glodaFolder = Gloda.getFolderForFolder(folder);
+ folderValues.push(glodaFolder);
+ }
+ }
+ return this._inConstraintHelper(aAttrDef, folderValues);
+ },
+ },
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.name.localeCompare(b.name);
+ },
+ toParamAndValue(aFolderOrGlodaFolder) {
+ if (aFolderOrGlodaFolder instanceof GlodaFolder) {
+ return [null, aFolderOrGlodaFolder.id];
+ }
+ return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
+ },
+ },
+ GlodaConstants.NOUN_FOLDER
+ );
+ this.defineNoun(
+ {
+ name: "account",
+ clazz: GlodaAccount,
+ allowsArbitraryAttrs: false,
+ isPrimitive: false,
+ equals(a, b) {
+ if ((a && !b) || (!a && b)) {
+ return false;
+ }
+ if (!a && !b) {
+ return true;
+ }
+ return a.id == b.id;
+ },
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.name.localeCompare(b.name);
+ },
+ },
+ GlodaConstants.NOUN_ACCOUNT
+ );
+ this.defineNoun(
+ {
+ name: "conversation",
+ clazz: GlodaConversation,
+ allowsArbitraryAttrs: false,
+ isPrimitive: false,
+ cache: true,
+ cacheCost: 512,
+ tableName: "conversations",
+ attrTableName: "messageAttributes",
+ attrIDColumnName: "conversationID",
+ datastore: GlodaDatastore,
+ objFromRow: GlodaDatastore._conversationFromRow,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.subject.localeCompare(b.subject);
+ },
+ toParamAndValue(aConversation) {
+ if (aConversation instanceof GlodaConversation) {
+ return [null, aConversation.id];
+ }
+ // assume they're just passing the id directly
+ return [null, aConversation];
+ },
+ },
+ GlodaConstants.NOUN_CONVERSATION
+ );
+ this.defineNoun(
+ {
+ name: "message",
+ clazz: GlodaMessage,
+ allowsArbitraryAttrs: true,
+ isPrimitive: false,
+ cache: true,
+ cacheCost: 2048,
+ tableName: "messages",
+ // we will always have a fulltext row, even for messages where we don't
+ // have the body available. this is because we want the subject indexed.
+ dbQueryJoinMagic:
+ " INNER JOIN messagesText ON messages.id = messagesText.rowid",
+ attrTableName: "messageAttributes",
+ attrIDColumnName: "messageID",
+ datastore: GlodaDatastore,
+ objFromRow: GlodaDatastore._messageFromRow,
+ dbAttribAdjuster: GlodaDatastore.adjustMessageAttributes,
+ dbQueryValidityConstraintSuffix:
+ " AND +deleted = 0 AND +folderID IS NOT NULL AND +messageKey IS NOT NULL",
+ // This is what's used when we have no validity constraints, i.e. we allow
+ // for ghost messages, which do not have a row in the messagesText table.
+ dbQueryJoinMagicWithNoValidityConstraints:
+ " LEFT JOIN messagesText ON messages.id = messagesText.rowid",
+ objInsert: GlodaDatastore.insertMessage,
+ objUpdate: GlodaDatastore.updateMessage,
+ toParamAndValue(aMessage) {
+ if (aMessage instanceof GlodaMessage) {
+ return [null, aMessage.id];
+ }
+ // assume they're just passing the id directly
+ return [null, aMessage];
+ },
+ },
+ GlodaConstants.NOUN_MESSAGE
+ );
+ this.defineNoun(
+ {
+ name: "contact",
+ clazz: GlodaContact,
+ allowsArbitraryAttrs: true,
+ isPrimitive: false,
+ cache: true,
+ cacheCost: 128,
+ tableName: "contacts",
+ attrTableName: "contactAttributes",
+ attrIDColumnName: "contactID",
+ datastore: GlodaDatastore,
+ objFromRow: GlodaDatastore._contactFromRow,
+ dbAttribAdjuster: GlodaDatastore.adjustAttributes,
+ objInsert: GlodaDatastore.insertContact,
+ objUpdate: GlodaDatastore.updateContact,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.name.localeCompare(b.name);
+ },
+ toParamAndValue(aContact) {
+ if (aContact instanceof GlodaContact) {
+ return [null, aContact.id];
+ }
+ // assume they're just passing the id directly
+ return [null, aContact];
+ },
+ },
+ GlodaConstants.NOUN_CONTACT
+ );
+ this.defineNoun(
+ {
+ name: "identity",
+ clazz: GlodaIdentity,
+ allowsArbitraryAttrs: false,
+ isPrimitive: false,
+ cache: true,
+ cacheCost: 128,
+ usesUniqueValue: true,
+ tableName: "identities",
+ datastore: GlodaDatastore,
+ objFromRow: GlodaDatastore._identityFromRow,
+ /**
+ * Short string is the contact name, long string includes the identity
+ * value too, delimited by a colon. Not tremendously localizable.
+ */
+ userVisibleString(aIdentity, aLong) {
+ if (!aLong) {
+ return aIdentity.contact.name;
+ }
+ if (aIdentity.contact.name == aIdentity.value) {
+ return aIdentity.value;
+ }
+ return aIdentity.contact.name + " (" + aIdentity.value + ")";
+ },
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.contact.name.localeCompare(b.contact.name);
+ },
+ toParamAndValue(aIdentity) {
+ if (aIdentity instanceof GlodaIdentity) {
+ return [null, aIdentity.id];
+ }
+ // assume they're just passing the id directly
+ return [null, aIdentity];
+ },
+ },
+ GlodaConstants.NOUN_IDENTITY
+ );
+ this.defineNoun(
+ {
+ name: "attachment-infos",
+ clazz: GlodaAttachment,
+ allowsArbitraryAttrs: false,
+ isPrimitive: false,
+ toJSON(x) {
+ return [
+ x._name,
+ x._contentType,
+ x._size,
+ x._part,
+ x._externalUrl,
+ x._isExternal,
+ ];
+ },
+ fromJSON(x, aGlodaMessage) {
+ let [name, contentType, size, _part, _externalUrl, isExternal] = x;
+ return new GlodaAttachment(
+ aGlodaMessage,
+ name,
+ contentType,
+ size,
+ _part,
+ _externalUrl,
+ isExternal
+ );
+ },
+ },
+ GlodaConstants.NOUN_ATTACHMENT
+ );
+
+ // parameterized identity is just two identities; we store the first one
+ // (whose value set must be very constrainted, like the 'me' identities)
+ // as the parameter, the second (which does not need to be constrained)
+ // as the value.
+ this.defineNoun(
+ {
+ name: "parameterized-identity",
+ clazz: null,
+ allowsArbitraryAttrs: false,
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ // First sort by the first identity in the tuple
+ // Since our general use-case is for the first guy to be "me", we only
+ // compare the identity value, not the name.
+ let fic = a[0].value.localeCompare(b[0].value);
+ if (fic) {
+ return fic;
+ }
+ // Next compare the second identity in the tuple, but use the contact
+ // this time to be consistent with our identity comparator.
+ return a[1].contact.name.localeCompare(b[1].contact.name);
+ },
+ computeDelta(aCurValues, aOldValues) {
+ let oldMap = {};
+ for (let tupe of aOldValues) {
+ let [originIdentity, targetIdentity] = tupe;
+ let targets = oldMap[originIdentity];
+ if (targets === undefined) {
+ targets = oldMap[originIdentity] = {};
+ }
+ targets[targetIdentity] = true;
+ }
+
+ let added = [],
+ removed = [];
+ for (let tupe of aCurValues) {
+ let [originIdentity, targetIdentity] = tupe;
+ let targets = oldMap[originIdentity];
+ if (targets === undefined || !(targetIdentity in targets)) {
+ added.push(tupe);
+ } else {
+ delete targets[targetIdentity];
+ }
+ }
+
+ for (let originIdentity in oldMap) {
+ let targets = oldMap[originIdentity];
+ for (let targetIdentity in targets) {
+ removed.push([originIdentity, targetIdentity]);
+ }
+ }
+
+ return [added, removed];
+ },
+ contributeObjDependencies(
+ aJsonValues,
+ aReferencesByNounID,
+ aInverseReferencesByNounID
+ ) {
+ // nothing to do with a zero-length list
+ if (aJsonValues.length == 0) {
+ return false;
+ }
+
+ let nounIdentityDef =
+ Gloda._nounIDToDef[GlodaConstants.NOUN_IDENTITY];
+ let references = aReferencesByNounID[nounIdentityDef.id];
+ if (references === undefined) {
+ references = aReferencesByNounID[nounIdentityDef.id] = {};
+ }
+
+ for (let tupe of aJsonValues) {
+ let [originIdentityID, targetIdentityID] = tupe;
+ if (!(originIdentityID in references)) {
+ references[originIdentityID] = null;
+ }
+ if (!(targetIdentityID in references)) {
+ references[targetIdentityID] = null;
+ }
+ }
+
+ return true;
+ },
+ resolveObjDependencies(
+ aJsonValues,
+ aReferencesByNounID,
+ aInverseReferencesByNounID
+ ) {
+ let references = aReferencesByNounID[GlodaConstants.NOUN_IDENTITY];
+
+ let results = [];
+ for (let tupe of aJsonValues) {
+ let [originIdentityID, targetIdentityID] = tupe;
+ results.push([
+ references[originIdentityID],
+ references[targetIdentityID],
+ ]);
+ }
+
+ return results;
+ },
+ toJSON(aIdentityTuple) {
+ return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+ },
+ toParamAndValue(aIdentityTuple) {
+ return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+ },
+ },
+ GlodaConstants.NOUN_PARAM_IDENTITY
+ );
+
+ GlodaDatastore.getAllAttributes();
+ },
+
+ /**
+ * Create accessor functions to 'bind' an attribute to underlying normalized
+ * attribute storage, as well as creating the appropriate query object
+ * constraint helper functions. This name is somewhat of a misnomer because
+ * special attributes are not 'bound' (because specific/non-generic per-class
+ * code provides the properties) but still depend on this method to
+ * establish their constraint helper methods.
+ *
+ * @XXX potentially rename to not suggest binding is required.
+ */
+ _bindAttribute(aAttrDef, aSubjectNounDef) {
+ let objectNounDef = aAttrDef.objectNounDef;
+
+ // -- the query constraint helpers
+ if (aSubjectNounDef.queryClass !== undefined) {
+ let constrainer;
+ let canQuery = true;
+ if (
+ "special" in aAttrDef &&
+ aAttrDef.special == GlodaConstants.kSpecialFulltext
+ ) {
+ constrainer = function (...aArgs) {
+ let constraint = [
+ GlodaConstants.kConstraintFulltext,
+ aAttrDef,
+ ...aArgs,
+ ];
+ this._constraints.push(constraint);
+ return this;
+ };
+ } else if (aAttrDef.canQuery || aAttrDef.attributeName.startsWith("_")) {
+ constrainer = function (...aArgs) {
+ let constraint = [GlodaConstants.kConstraintIn, aAttrDef, ...aArgs];
+ this._constraints.push(constraint);
+ return this;
+ };
+ } else {
+ constrainer = function () {
+ throw new Error(
+ "Cannot query on attribute " +
+ aAttrDef.attributeName +
+ " because its canQuery parameter hasn't been set to true." +
+ " Reading the comments about Gloda.defineAttribute may be a" +
+ " sensible thing to do now."
+ );
+ };
+ canQuery = false;
+ }
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;
+
+ // Don't bind extra query-able attributes if we're unable to perform a
+ // search on the attribute.
+ if (!canQuery) {
+ return;
+ }
+
+ // - ranged value helper: fooRange
+ if (objectNounDef.continuous) {
+ // takes one or more tuples of [lower bound, upper bound]
+ let rangedConstrainer = function (...aArgs) {
+ let constraint = [
+ GlodaConstants.kConstraintRanges,
+ aAttrDef,
+ ...aArgs,
+ ];
+ this._constraints.push(constraint);
+ return this;
+ };
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Range"] =
+ rangedConstrainer;
+ }
+
+ // - string LIKE helper for special on-row attributes: fooLike
+ // (it is impossible to store a string as an indexed attribute, which is
+ // why we do this for on-row only.)
+ if (
+ "special" in aAttrDef &&
+ aAttrDef.special == GlodaConstants.kSpecialString
+ ) {
+ let likeConstrainer = function (...aArgs) {
+ let constraint = [
+ GlodaConstants.kConstraintStringLike,
+ aAttrDef,
+ ...aArgs,
+ ];
+ this._constraints.push(constraint);
+ return this;
+ };
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
+ likeConstrainer;
+ }
+
+ // - Custom helpers provided by the noun type...
+ if ("queryHelpers" in objectNounDef) {
+ for (let name in objectNounDef.queryHelpers) {
+ let helper = objectNounDef.queryHelpers[name];
+ // we need a new closure...
+ let helperFunc = helper;
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + name] =
+ function (...aArgs) {
+ return helperFunc.call(this, aAttrDef, ...aArgs);
+ };
+ }
+ }
+ }
+ },
+
+ /**
+ * Names of attribute-specific localized strings and the JS attribute they are
+ * exposed as in the attribute's "strings" attribute (if the provider has a
+ * string bundle exposed on its "strings" attribute). They are rooted at
+ * "gloda.SUBJECT-NOUN-NAME.attr.ATTR-NAME.*".
+ *
+ * Please consult the localization notes in gloda.properties to understand
+ * what these are used for.
+ */
+ _ATTR_LOCALIZED_STRINGS: {
+ /* - Faceting */
+ facetNameLabel: "facetNameLabel",
+ noneLabel: "noneLabel",
+ includeLabel: "includeLabel",
+ excludeLabel: "excludeLabel",
+ remainderLabel: "remainderLabel",
+ mustMatchLabel: "mustMatchLabel",
+ cantMatchLabel: "cantMatchLabel",
+ mayMatchLabel: "mayMatchLabel",
+ mustMatchNoneLabel: "mustMatchNoneLabel",
+ mustMatchSomeLabel: "mustMatchSomeLabel",
+ mayMatchAnyLabel: "mayMatchAnyLabel",
+ },
+ /**
+ * Define an attribute and all its meta-data. Takes a single dictionary as
+ * its argument, with the following required properties:
+ *
+ * @param aAttrDef.provider The object instance providing a 'process' method.
+ * @param aAttrDef.extensionName The name of the extension providing these
+ * attributes.
+ * @param aAttrDef.attributeType The type of attribute, one of the values from
+ * the kAttr* enumeration.
+ * @param aAttrDef.attributeName The name of the attribute, which also doubles
+ * as the bound property name if you pass 'bind' a value of true. You are
+ * responsible for avoiding collisions, which presumably will mean
+ * checking/updating a wiki page in the future, or just prefixing your
+ * attribute name with your extension name or something like that.
+ * @param aAttrDef.bind Should this attribute be 'bound' as a convenience
+ * attribute on the subject's object (true/false)? For example, with an
+ * attributeName of "foo" and passing true for 'bind' with a subject noun
+ * of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter that
+ * returns the value of the attribute. If 'singular' is true, this means
+ * an instance of the object class corresponding to the noun type or null
+ * if the attribute does not exist. If 'singular' is false, this means a
+ * list of instances of the object class corresponding to the noun type,
+ * where the list may be empty if no instances of the attribute are
+ * present.
+ * @param aAttrDef.bindName Optional override of attributeName for purposes of
+ * the binding property's name.
+ * @param aAttrDef.singular Is the attribute going to happen at most once
+ * (true), or potentially multiple times (false). This affects whether
+ * the binding returns a list or just a single item (which is null when
+ * the attribute is not present).
+ * @param [aAttrDef.emptySetIsSignificant=false] Should we
+ * @param aAttrDef.subjectNouns A list of object types (NOUNs) that this
+ * attribute can be set on. Each element in the list should be one of the
+ * NOUN_* constants or a dynamically registered noun type.
+ * @param aAttrDef.objectNoun The object type (one of the NOUN_* constants or
+ * a dynamically registered noun types) that is the 'object' in the
+ * traditional RDF triple. More pragmatically, in the database row used
+ * to represent an attribute, we store the subject (ex: message ID),
+ * attribute ID, and an integer which is the integer representation of the
+ * 'object' whose type you are defining right here.
+ */
+ defineAttribute(aAttrDef) {
+ // ensure required properties exist on aAttrDef
+ if (
+ !("provider" in aAttrDef) ||
+ !("extensionName" in aAttrDef) ||
+ !("attributeType" in aAttrDef) ||
+ !("attributeName" in aAttrDef) ||
+ !("singular" in aAttrDef) ||
+ !("subjectNouns" in aAttrDef) ||
+ !("objectNoun" in aAttrDef)
+ ) {
+ // perhaps we should have a list of required attributes, perchance with
+ // and explanation of what it holds, and use that to be friendlier?
+ throw Error(
+ "You omitted a required attribute defining property, please" +
+ " consult the documentation as penance."
+ );
+ }
+
+ // -- Fill in defaults
+ if (!("emptySetIsSignificant" in aAttrDef)) {
+ aAttrDef.emptySetIsSignificant = false;
+ }
+
+ if (!("canQuery" in aAttrDef)) {
+ aAttrDef.canQuery = !!aAttrDef.facet;
+ }
+
+ // return if the attribute has already been defined
+ if (aAttrDef.dbDef) {
+ return aAttrDef;
+ }
+
+ // - first time we've seen a provider init logic
+ if (!(aAttrDef.provider.providerName in this._attrProviders)) {
+ this._attrProviders[aAttrDef.provider.providerName] = [];
+ if (aAttrDef.provider.contentWhittle) {
+ whittlerRegistry.registerWhittler(aAttrDef.provider);
+ }
+ }
+
+ let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
+ // -- Database Definition
+ let attrDBDef;
+ if (compoundName in GlodaDatastore._attributeDBDefs) {
+ // the existence of the GlodaAttributeDBDef means that either it has
+ // already been fully defined, or has been loaded from the database but
+ // not yet 'bound' to a provider (and had important meta-info that
+ // doesn't go in the db copied over)
+ attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
+ } else {
+ // we need to create the attribute definition in the database
+ let attrID = null;
+ attrID = GlodaDatastore._createAttributeDef(
+ aAttrDef.attributeType,
+ aAttrDef.extensionName,
+ aAttrDef.attributeName,
+ null
+ );
+
+ attrDBDef = new GlodaAttributeDBDef(
+ GlodaDatastore,
+ attrID,
+ compoundName,
+ aAttrDef.attributeType,
+ aAttrDef.extensionName,
+ aAttrDef.attributeName
+ );
+ GlodaDatastore._attributeDBDefs[compoundName] = attrDBDef;
+ GlodaDatastore._attributeIDToDBDefAndParam[attrID] = [attrDBDef, null];
+ }
+
+ aAttrDef.dbDef = attrDBDef;
+ attrDBDef.attrDef = aAttrDef;
+
+ aAttrDef.id = aAttrDef.dbDef.id;
+
+ if ("bindName" in aAttrDef) {
+ aAttrDef.boundName = aAttrDef.bindName;
+ } else {
+ aAttrDef.boundName = aAttrDef.attributeName;
+ }
+
+ aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
+ aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);
+
+ // -- Facets
+ function normalizeFacetDef(aFacetDef) {
+ if (!("groupIdAttr" in aFacetDef)) {
+ aFacetDef.groupIdAttr = aAttrDef.objectNounDef.idAttr;
+ }
+ if (!("groupComparator" in aFacetDef)) {
+ aFacetDef.groupComparator = aAttrDef.objectNounDef.comparator;
+ }
+ if (!("filter" in aFacetDef)) {
+ aFacetDef.filter = null;
+ }
+ }
+ // No facet attribute means no facet desired; set an explicit null so that
+ // code can check without doing an "in" check.
+ if (!("facet" in aAttrDef)) {
+ aAttrDef.facet = null;
+ } else if (aAttrDef.facet === true) {
+ // Promote "true" facet values to the defaults. Where attributes have
+ // specified values, make sure we fill in any missing defaults.
+ aAttrDef.facet = {
+ type: "default",
+ groupIdAttr: aAttrDef.objectNounDef.idAttr,
+ groupComparator: aAttrDef.objectNounDef.comparator,
+ filter: null,
+ };
+ } else {
+ normalizeFacetDef(aAttrDef.facet);
+ }
+ if ("extraFacets" in aAttrDef) {
+ for (let facetDef of aAttrDef.extraFacets) {
+ normalizeFacetDef(facetDef);
+ }
+ }
+
+ function gatherLocalizedStrings(aBundle, aPropRoot, aStickIn) {
+ for (let propName in Gloda._ATTR_LOCALIZED_STRINGS) {
+ let attrName = Gloda._ATTR_LOCALIZED_STRINGS[propName];
+ try {
+ aStickIn[attrName] = aBundle.GetStringFromName(aPropRoot + propName);
+ } catch (ex) {
+ // do nothing. nsIStringBundle throws exceptions when not found
+ }
+ }
+ }
+
+ // -- L10n.
+ // If the provider has a string bundle, populate a "strings" attribute with
+ // our standard attribute strings that can be UI exposed.
+ if ("strings" in aAttrDef.provider && aAttrDef.facet) {
+ let bundle = aAttrDef.provider.strings;
+
+ // -- attribute strings
+ let attrStrings = (aAttrDef.facet.strings = {});
+ // we use the first subject the attribute applies to as the basis of
+ // where to get the string from. Mainly because we currently don't have
+ // any attributes with multiple subjects nor a use-case where we expose
+ // multiple noun types via the UI. (Just messages right now.)
+ let canonicalSubject = this._nounIDToDef[aAttrDef.subjectNouns[0]];
+ let propRoot =
+ "gloda." +
+ canonicalSubject.name +
+ ".attr." +
+ aAttrDef.attributeName +
+ ".";
+ gatherLocalizedStrings(bundle, propRoot, attrStrings);
+
+ // -- alias strings for synthetic facets
+ if ("extraFacets" in aAttrDef) {
+ for (let facetDef of aAttrDef.extraFacets) {
+ facetDef.strings = {};
+ let aliasPropRoot =
+ "gloda." + canonicalSubject.name + ".attr." + facetDef.alias + ".";
+ gatherLocalizedStrings(bundle, aliasPropRoot, facetDef.strings);
+ }
+ }
+ }
+
+ // -- Subject Noun Binding
+ for (
+ let iSubject = 0;
+ iSubject < aAttrDef.subjectNouns.length;
+ iSubject++
+ ) {
+ let subjectType = aAttrDef.subjectNouns[iSubject];
+ let subjectNounDef = this._nounIDToDef[subjectType];
+ this._bindAttribute(aAttrDef, subjectNounDef);
+
+ // update the provider maps...
+ if (
+ !this._attrProviderOrderByNoun[subjectType].includes(aAttrDef.provider)
+ ) {
+ this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
+ if (aAttrDef.provider.optimize) {
+ this._attrOptimizerOrderByNoun[subjectType].push(aAttrDef.provider);
+ }
+ this._attrProvidersByNoun[subjectType][aAttrDef.provider.providerName] =
+ [];
+ }
+ this._attrProvidersByNoun[subjectType][
+ aAttrDef.provider.providerName
+ ].push(aAttrDef);
+
+ subjectNounDef.attribsByBoundName[aAttrDef.boundName] = aAttrDef;
+ if (aAttrDef.domExpose) {
+ subjectNounDef.domExposeAttribsByBoundName[aAttrDef.boundName] =
+ aAttrDef;
+ }
+
+ if (
+ "special" in aAttrDef &&
+ aAttrDef.special & GlodaConstants.kSpecialColumn
+ ) {
+ subjectNounDef.specialLoadAttribs.push(aAttrDef);
+ }
+
+ // if this is a parent column attribute, make note of it so that if we
+ // need to do an inverse references lookup, we know what column we are
+ // issuing against.
+ if (
+ "special" in aAttrDef &&
+ aAttrDef.special === GlodaConstants.kSpecialColumnParent
+ ) {
+ subjectNounDef.parentColumnAttr = aAttrDef;
+ }
+
+ if (
+ aAttrDef.objectNounDef.tableName ||
+ aAttrDef.objectNounDef.contributeObjDependencies
+ ) {
+ subjectNounDef.hasObjDependencies = true;
+ }
+ }
+
+ this._attrProviders[aAttrDef.provider.providerName].push(aAttrDef);
+ return aAttrDef;
+ },
+
+ /**
+ * Retrieve the attribute provided by the given extension with the given
+ * attribute name. The original idea was that plugins would effectively
+ * name-space attributes, helping avoid collisions. Since we are leaning
+ * towards using binding heavily, this doesn't really help, as the collisions
+ * will just occur on the attribute name instead. Also, this can turn
+ * extensions into liars as name changes/moves to core/etc. happen.
+ *
+ * @TODO consider removing the extension name argument parameter requirement
+ */
+ getAttrDef(aPluginName, aAttrName) {
+ let compoundName = aPluginName + ":" + aAttrName;
+ return GlodaDatastore._attributeDBDefs[compoundName];
+ },
+
+ /**
+ * Create a new query instance for the given noun-type. This provides
+ * a generic way to provide constraint-based queries of any first-class
+ * nouns supported by the system.
+ *
+ * The idea is that every attribute on an object can be used to express
+ * a constraint on the query object. Constraints implicitly 'AND' together,
+ * but providing multiple arguments to a constraint function results in an
+ * 'OR'ing of those values. Additionally, you can call or() on the returned
+ * query to create an alternate query that is effectively a giant OR against
+ * all the constraints you create on the main query object (or any other
+ * alternate queries returned by or()). (Note: there is no nesting of these
+ * alternate queries. query.or().or() is equivalent to query.or())
+ * For each attribute, there is a constraint with the same name that takes
+ * one or more arguments. The arguments represent a set of OR values that
+ * objects matching the query can have. (If you want the constraint
+ * effectively ANDed together, just invoke the constraint function
+ * multiple times.) For example, newQuery(NOUN_PERSON).age(25) would
+ * constraint to all the people aged 25, while age(25, 26) would constrain
+ * to all the people age 25 or 26.
+ * For each attribute with a 'continuous' noun, there is a constraint with the
+ * attribute name with "Range" appended. It takes two arguments which are an
+ * inclusive lower bound and an inclusive lower bound for values in the
+ * range. If you would like an open-ended range on either side, pass null
+ * for that argument. If you would like to specify multiple ranges that
+ * should be ORed together, simply pass additional (pairs of) arguments.
+ * For example, newQuery(NOUN_PERSON).age(25,100) would constraint to all
+ * the people who are >= 25 and <= 100. Likewise age(25, null) would just
+ * return all the people who are 25 or older. And age(25,30,35,40) would
+ * return people who are either 25-30 or 35-30.
+ * There are also full-text constraint columns. In a nutshell, their
+ * arguments are the strings that should be passed to the SQLite FTS3
+ * MATCH clause.
+ *
+ * @param aNounID The (integer) noun-id of the noun you want to query on.
+ * @param aOptions an optional dictionary of query options, see the GlodaQuery
+ * class documentation.
+ */
+ newQuery(aNounID, aOptions) {
+ let nounDef = this._nounIDToDef[aNounID];
+ return new nounDef.queryClass(aOptions);
+ },
+
+ /**
+ * Create a collection/query for the given noun-type that only matches the
+ * provided items. This is to be used when you have an explicit set of items
+ * that you would still like to receive updates for.
+ */
+ explicitCollection(aNounID, aItems) {
+ let nounDef = this._nounIDToDef[aNounID];
+ let collection = new GlodaCollection(nounDef, aItems, null, null);
+ let query = new nounDef.explicitQueryClass(collection);
+ collection.query = query;
+ GlodaCollectionManager.registerCollection(collection);
+ return collection;
+ },
+
+ /**
+ * Debugging 'wildcard' collection creation support. A wildcard collection
+ * will 'accept' any new item instances presented to the collection manager
+ * as new. The result is that it allows you to be notified as new items
+ * as they are indexed, existing items as they are loaded from the database,
+ * etc.
+ * Because the items are added to the collection without limit, this will
+ * result in a leak if you don't do something to clean up after the
+ * collection. (Forgetting about the collection will suffice, as it is still
+ * weakly held.)
+ */
+ _wildcardCollection(aNounID, aItems) {
+ let nounDef = this._nounIDToDef[aNounID];
+ let collection = new GlodaCollection(nounDef, aItems, null, null);
+ let query = new nounDef.wildcardQueryClass(collection);
+ collection.query = query;
+ GlodaCollectionManager.registerCollection(collection);
+ return collection;
+ },
+
+ /**
+ * Attribute providers attempting to index something that experience a fatal
+ * problem should throw one of these. For example:
+ * "throw new Gloda.BadItemContentsError('Message lacks an author.');".
+ *
+ * We're not really taking advantage of this yet, but it's a good idea.
+ */
+ BadItemContentsError,
+
+ /* eslint-disable complexity */
+ /**
+ * Populate a gloda representation of an item given the thus-far built
+ * representation, the previous representation, and one or more raw
+ * representations. The attribute providers/optimizers for the given noun
+ * type are invoked, allowing them to contribute/alter things. Following
+ * that, we build and persist our attribute representations.
+ *
+ * The result of the processing ends up with attributes in 3 different forms:
+ * - Database attribute rows (to be added and removed).
+ * - In-memory representation.
+ * - JSON-able representation.
+ *
+ * @param aItem The noun instance you want processed.
+ * @param aRawReps A dictionary that we pass to the attribute providers.
+ * There is a(n implied) contract between the caller of grokNounItem for a
+ * given noun type and the attribute providers for that noun type, and we
+ * have nothing to do with it OTHER THAN inserting a 'trueGlodaRep'
+ * value into it. In the event of reindexing an existing object, the
+ * gloda representation we pass to the indexers is actually a clone that
+ * allows the asynchronous indexers to mutate the object without
+ * causing visible changes in the existing representation of the gloda
+ * object. We patch the changes back onto the original item atomically
+ * once indexing completes. The 'trueGlodaRep' is then useful for
+ * objects that hang off of the gloda instance that need a reference
+ * back to their containing object for API convenience purposes.
+ * @param aIsConceptuallyNew Is the item "new" in the sense that it would
+ * never have been visible from within user code? This translates into
+ * whether this should trigger an itemAdded notification or an
+ * itemModified notification.
+ * @param aIsRecordNew Is the item "new" in the sense that we should INSERT
+ * a record rather than UPDATE-ing a record. For example, when dealing
+ * with messages where we may have a ghost, the ghost message is not a
+ * new record, but is conceptually new.
+ * @param aCallbackHandle The GlodaIndexer-style callback handle that is being
+ * used to drive this processing in an async fashion. (See
+ * GlodaIndexer._callbackHandle).
+ * @param aDoCache Should we allow this item to be contributed to its noun
+ * cache?
+ */
+ *grokNounItem(
+ aItem,
+ aRawReps,
+ aIsConceptuallyNew,
+ aIsRecordNew,
+ aCallbackHandle,
+ aDoCache
+ ) {
+ let itemNounDef = aItem.NOUN_DEF;
+ let attribsByBoundName = itemNounDef.attribsByBoundName;
+
+ this._log.info(" ** grokNounItem: " + itemNounDef.name);
+
+ let addDBAttribs = [];
+ let removeDBAttribs = [];
+
+ let jsonDict = {};
+
+ let aOldItem;
+ aRawReps.trueGlodaRep = aItem;
+ if (aIsConceptuallyNew) {
+ // there is no old item if we are new.
+ aOldItem = {};
+ } else {
+ aOldItem = aItem;
+ // we want to create a clone of the existing item so that we can know the
+ // deltas that happened for indexing purposes
+ aItem = aItem._clone();
+ }
+
+ // Have the attribute providers directly set properties on the aItem
+ let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
+ for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
+ this._log.info(" * provider: " + attrProviders[iProvider].providerName);
+ yield aCallbackHandle.pushAndGo(
+ attrProviders[iProvider].process(
+ aItem,
+ aRawReps,
+ aIsConceptuallyNew,
+ aCallbackHandle
+ )
+ );
+ }
+
+ let attrOptimizers = this._attrOptimizerOrderByNoun[itemNounDef.id];
+ for (let iProvider = 0; iProvider < attrOptimizers.length; iProvider++) {
+ this._log.info(
+ " * optimizer: " + attrOptimizers[iProvider].providerName
+ );
+ yield aCallbackHandle.pushAndGo(
+ attrOptimizers[iProvider].optimize(
+ aItem,
+ aRawReps,
+ aIsConceptuallyNew,
+ aCallbackHandle
+ )
+ );
+ }
+ this._log.info(" ** done with providers.");
+
+ // Iterate over the attributes on the item
+ for (let key of Object.keys(aItem)) {
+ let value = aItem[key];
+ // ignore keys that start with underscores, they are private and not
+ // persisted by our attribute mechanism. (they are directly handled by
+ // the object implementation.)
+ if (key.startsWith("_")) {
+ continue;
+ }
+ // find the attribute definition that corresponds to this key
+ let attrib = attribsByBoundName[key];
+ // if there's no attribute, that's not good, but not horrible.
+ if (attrib === undefined) {
+ this._log.warn("new proc ignoring attrib: " + key);
+ continue;
+ }
+
+ let attribDB = attrib.dbDef;
+ let objectNounDef = attrib.objectNounDef;
+
+ // - translate for our JSON rep
+ if (attrib.singular) {
+ if (objectNounDef.toJSON) {
+ jsonDict[attrib.id] = objectNounDef.toJSON(value);
+ } else {
+ jsonDict[attrib.id] = value;
+ }
+ } else if (objectNounDef.toJSON) {
+ let toJSON = objectNounDef.toJSON;
+ jsonDict[attrib.id] = [];
+ for (let subValue of value) {
+ jsonDict[attrib.id].push(toJSON(subValue));
+ }
+ } else {
+ jsonDict[attrib.id] = value;
+ }
+
+ let oldValue = aOldItem[key];
+
+ // the 'old' item is still the canonical one; update it
+ // do the update now, because we may skip operations on addDBAttribs and
+ // removeDBattribs, if the attribute is not to generate entries in
+ // messageAttributes
+ if (oldValue !== undefined || !aIsConceptuallyNew) {
+ aOldItem[key] = value;
+ }
+
+ // the new canQuery property has to be set to true to generate entries
+ // in the messageAttributes table. Any other truthy value (like a non
+ // empty string), will still make the message query-able but without
+ // using the database.
+ if (attrib.canQuery !== true) {
+ continue;
+ }
+
+ // - database index attributes
+
+ // perform a delta analysis against the old value, if we have one
+ if (oldValue !== undefined) {
+ // in the singular case if they don't match, it's one add and one remove
+ if (attrib.singular) {
+ // test for identicality, failing that, see if they have explicit
+ // equals support.
+ if (
+ value !== oldValue &&
+ (!value.equals || !value.equals(oldValue))
+ ) {
+ addDBAttribs.push(attribDB.convertValuesToDBAttributes([value])[0]);
+ removeDBAttribs.push(
+ attribDB.convertValuesToDBAttributes([oldValue])[0]
+ );
+ }
+ } else if (objectNounDef.computeDelta) {
+ // in the plural case, we have to figure the deltas accounting for
+ // possible changes in ordering (which is insignificant from an
+ // indexing perspective)
+ // some nouns may not meet === equivalence needs, so must provide a
+ // custom computeDelta method to help us out
+ let [valuesAdded, valuesRemoved] = objectNounDef.computeDelta(
+ value,
+ oldValue
+ );
+ // convert the values to database-style attribute rows
+ addDBAttribs.push.apply(
+ addDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesAdded)
+ );
+ removeDBAttribs.push.apply(
+ removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesRemoved)
+ );
+ } else {
+ // build a map of the previous values; we will delete the values as
+ // we see them so that we will know what old values are no longer
+ // present in the current set of values.
+ let oldValueMap = {};
+ for (let anOldValue of oldValue) {
+ // remember, the key is just the toString'ed value, so we need to
+ // store and use the actual value as the value!
+ oldValueMap[anOldValue] = anOldValue;
+ }
+ // traverse the current values...
+ let valuesAdded = [];
+ for (let curValue of value) {
+ if (curValue in oldValueMap) {
+ delete oldValueMap[curValue];
+ } else {
+ valuesAdded.push(curValue);
+ }
+ }
+ // anything still on oldValueMap was removed.
+ let valuesRemoved = Object.keys(oldValueMap).map(
+ key => oldValueMap[key]
+ );
+ // convert the values to database-style attribute rows
+ addDBAttribs.push.apply(
+ addDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesAdded)
+ );
+ removeDBAttribs.push.apply(
+ removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesRemoved)
+ );
+ }
+
+ // Add/remove the empty set indicator as appropriate.
+ if (attrib.emptySetIsSignificant) {
+ // if we are now non-zero but previously were zero, remove.
+ if (value.length && !oldValue.length) {
+ removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
+ } else if (!value.length && oldValue.length) {
+ // We are now zero length but previously were not, add.
+ addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
+ }
+ }
+ } else {
+ // no old value, all values are new
+ // add the db reps on the new values
+ if (attrib.singular) {
+ value = [value];
+ }
+ addDBAttribs.push.apply(
+ addDBAttribs,
+ attribDB.convertValuesToDBAttributes(value)
+ );
+ // Add the empty set indicator for the attribute id if appropriate.
+ if (!value.length && attrib.emptySetIsSignificant) {
+ addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
+ }
+ }
+ }
+
+ // Iterate over any remaining values in old items for purge purposes.
+ for (let key of Object.keys(aOldItem)) {
+ let value = aOldItem[key];
+ // ignore keys that start with underscores, they are private and not
+ // persisted by our attribute mechanism. (they are directly handled by
+ // the object implementation.)
+ if (key.startsWith("_")) {
+ continue;
+ }
+ // ignore things we saw in the new guy
+ if (key in aItem) {
+ continue;
+ }
+
+ // find the attribute definition that corresponds to this key
+ let attrib = attribsByBoundName[key];
+ // if there's no attribute, that's not good, but not horrible.
+ if (attrib === undefined) {
+ continue;
+ }
+
+ // delete these from the old item, as the old item is canonical, and
+ // should no longer have these values
+ delete aOldItem[key];
+
+ if (attrib.canQuery !== true) {
+ this._log.debug(
+ "Not inserting attribute " +
+ attrib.attributeName +
+ " into the db, since we don't plan on querying on it"
+ );
+ continue;
+ }
+
+ if (attrib.singular) {
+ value = [value];
+ }
+ let attribDB = attrib.dbDef;
+ removeDBAttribs.push.apply(
+ removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(value)
+ );
+ // remove the empty set marker if there should have been one
+ if (!value.length && attrib.emptySetIsSignificant) {
+ removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
+ }
+ }
+
+ aItem._jsonText = JSON.stringify(jsonDict);
+ this._log.debug(" json text: " + aItem._jsonText);
+
+ if (aIsRecordNew) {
+ this._log.debug(" inserting item");
+ itemNounDef.objInsert.call(itemNounDef.datastore, aItem);
+ } else {
+ this._log.debug(" updating item");
+ itemNounDef.objUpdate.call(itemNounDef.datastore, aItem);
+ }
+
+ this._log.debug(
+ " adjusting attributes, add: " + addDBAttribs + " rem: " + removeDBAttribs
+ );
+ itemNounDef.dbAttribAdjuster.call(
+ itemNounDef.datastore,
+ aItem,
+ addDBAttribs,
+ removeDBAttribs
+ );
+
+ if (!aIsConceptuallyNew && "_declone" in aOldItem) {
+ aOldItem._declone(aItem);
+ }
+
+ // Cache ramifications...
+ if (aDoCache === undefined || aDoCache) {
+ if (aIsConceptuallyNew) {
+ GlodaCollectionManager.itemsAdded(aItem.NOUN_ID, [aItem]);
+ } else {
+ GlodaCollectionManager.itemsModified(aOldItem.NOUN_ID, [aOldItem]);
+ }
+ }
+
+ this._log.debug(" done grokking.");
+
+ yield GlodaConstants.kWorkDone;
+ },
+ /* eslint-enable complexity */
+
+ /**
+ * Processes a list of noun instances for their score within a given context.
+ * This is primarily intended for use by search ranking mechanisms, but could
+ * be used elsewhere too. (It does, however, depend on the complicity of the
+ * score method implementations to not get confused.)
+ *
+ * @param aItems The non-empty list of items to score.
+ * @param aContext A noun-specific dictionary that we just pass to the funcs.
+ * @param aExtraScoreFuncs A list of extra scoring functions to apply.
+ * @returns A list of integer scores equal in length to aItems.
+ */
+ scoreNounItems(aItems, aContext, aExtraScoreFuncs) {
+ let scores = [];
+ // bail if there is nothing to score
+ if (!aItems.length) {
+ return scores;
+ }
+
+ let itemNounDef = aItems[0].NOUN_DEF;
+ if (aExtraScoreFuncs == null) {
+ aExtraScoreFuncs = [];
+ }
+
+ for (let item of aItems) {
+ let score = 0;
+ let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
+ for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
+ let provider = attrProviders[iProvider];
+ if (provider.score) {
+ score += provider.score(item);
+ }
+ }
+ for (let extraScoreFunc of aExtraScoreFuncs) {
+ score += extraScoreFunc(item, aContext);
+ }
+ scores.push(score);
+ }
+
+ return scores;
+ },
+};
+
+/* and initialize the Gloda object/NS before we return... */
+try {
+ Gloda._init();
+} catch (ex) {
+ Gloda._log.debug(
+ "Exception during Gloda init (" +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ "): " +
+ ex
+ );
+}
+/* but don't forget that we effectively depend on Everybody.jsm too, and
+ currently on our importer to be importing that if they need us fully armed
+ and operational. */
diff --git a/comm/mailnews/db/gloda/modules/GlodaConstants.jsm b/comm/mailnews/db/gloda/modules/GlodaConstants.jsm
new file mode 100644
index 0000000000..1e6d253f09
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaConstants.jsm
@@ -0,0 +1,250 @@
+/* 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/. */
+
+/**
+ * The constants used by Gloda files. Avoid importing anything into this file.
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaConstants"];
+
+var GlodaConstants = {
+ /**
+ * The indexer is idle.
+ */
+ kIndexerIdle: 0,
+ /**
+ * The indexer is doing something. We used to have other specific states, but
+ * they have been rendered irrelevant and wiped from existence.
+ */
+ kIndexerIndexing: 1,
+
+ /**
+ * Synchronous activities performed that can be thought of as one processing
+ * token. Potentially yield the event-loop and re-schedule for later based
+ * on how long we've actually taken/etc. The goal here is that code that
+ * is doing stuff synchronously yields with kWorkSync periodically to make
+ * sure that it doesn't dominate the event-loop. Unless the processing
+ * in question is particularly intensive, it should be reasonable to apply
+ * some decimation factor (ex: 32 or 64) with the general goal of yielding
+ * every 3-10 milliseconds.
+ */
+ kWorkSync: 0,
+ /**
+ * Asynchronous activity performed, you need to relinquish flow control and
+ * trust us to call callbackDriver later.
+ */
+ kWorkAsync: 1,
+ /**
+ * We are all done with our task, close us and figure out something else to do.
+ */
+ kWorkDone: 2,
+ /**
+ * We are not done with our task, but we think it's a good idea to take a
+ * breather because we believe we have tied up the event loop for a
+ * non-trivial amount of time. So please re-schedule us in the future.
+ *
+ * This is currently only used internally by the indexer's batching logic;
+ * minor changes may be required if used by actual indexers.
+ */
+ kWorkPause: 3,
+ /**
+ * We are done with our task, and have a result that we are returning. This
+ * should only be used by your callback handler's doneWithResult method.
+ * Ex: you are passed aCallbackHandle, and you do
+ * "yield aCallbackHandle.doneWithResult(myResult);".
+ */
+ kWorkDoneWithResult: 4,
+
+ /**
+ * An attribute that is a defining characteristic of the subject.
+ */
+ kAttrFundamental: 0,
+ /**
+ * An attribute that is an optimization derived from two or more fundamental
+ * attributes and exists solely to improve database query performance.
+ */
+ kAttrOptimization: 1,
+ /**
+ * An attribute that is derived from the content of the subject. For example,
+ * a message that references a bugzilla bug could have a "derived" attribute
+ * that captures the bugzilla reference. This is not
+ */
+ kAttrDerived: 2,
+ /**
+ * An attribute that is the result of an explicit and intentional user action
+ * upon the subject. For example, a tag placed on a message by a user (or
+ * at the user's request by a filter) is explicit.
+ */
+ kAttrExplicit: 3,
+ /**
+ * An attribute that is indirectly the result of a user's behaviour. For
+ * example, if a user consults a message multiple times, we may conclude that
+ * the user finds the message interesting. It is "implied", if you will,
+ * that the message is interesting.
+ */
+ kAttrImplicit: 4,
+
+ /**
+ * This attribute is not 'special'; it is stored as a (thing id, attribute id,
+ * attribute id) tuple in the database rather than on thing's row or on
+ * thing's fulltext row. (Where "thing" could be a message or any other
+ * first class noun.)
+ */
+ kSpecialNotAtAll: 0,
+ /**
+ * This attribute is stored as a numeric column on the row for the noun. The
+ * attribute definition should include this value as 'special' and the
+ * column name that stores the attribute as 'specialColumnName'.
+ */
+ kSpecialColumn: 16,
+ kSpecialColumnChildren: 16 | 1,
+ kSpecialColumnParent: 16 | 2,
+ /**
+ * This attribute is stored as a string column on the row for the noun. It
+ * differs from kSpecialColumn in that it is a string, which once had
+ * query ramifications and one day may have them again.
+ */
+ kSpecialString: 32,
+ /**
+ * This attribute is stored as a fulltext column on the fulltext table for
+ * the noun. The attribute definition should include this value as 'special'
+ * and the column name that stores the table as 'specialColumnName'.
+ */
+ kSpecialFulltext: 64,
+
+ /**
+ * The extensionName used for the attributes defined by core gloda plugins
+ * such as GlodaFundAttr.jsm and GlodaExplicitAttr.jsm.
+ */
+ BUILT_IN: "built-in",
+
+ /**
+ * Special sentinel value that will cause facets to skip a noun instance
+ * when an attribute has this value.
+ */
+ IGNORE_FACET: "ignore-facet",
+
+ /*
+ * The following are explicit noun IDs. While most extension-provided nouns
+ * will have dynamically allocated id's that are looked up by name, these
+ * id's can be relied upon to exist and be accessible via these
+ * pseudo-constants. It's not really clear that we need these, although it
+ * does potentially simplify code to not have to look up all of their nouns
+ * at initialization time.
+ */
+ /**
+ * Boolean values, expressed as 0/1 in the database and non-continuous for
+ * constraint purposes. Like numbers, such nouns require their attributes
+ * to provide them with context, lacking any of their own.
+ * Having this as a noun type may be a bad idea; a change of nomenclature
+ * (so that we are not claiming a boolean value is a noun, but still using
+ * it in the same way) or implementation to require each boolean noun
+ * actually be its own noun may be in order.
+ */
+ NOUN_BOOLEAN: 1,
+ /**
+ * A number, which could mean an integer or floating point values. We treat
+ * these as continuous, meaning that queries on them can have ranged
+ * constraints expressed on them. Lacking any inherent context, numbers
+ * depend on their attributes to parameterize them as required.
+ * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+ */
+ NOUN_NUMBER: 2,
+ /**
+ * A (non-fulltext) string.
+ * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+ */
+ NOUN_STRING: 3,
+ /** A date, encoded as a PRTime, represented as a js Date object. */
+ NOUN_DATE: 10,
+ /**
+ * Fulltext search support, somewhat magical. This is only intended to be
+ * used for kSpecialFulltext attributes, and exclusively as a constraint
+ * mechanism. The values are always represented as strings. It is presumed
+ * that the user of this functionality knows how to generate SQLite FTS3
+ * style MATCH queries, or is okay with us just gluing them together with
+ * " OR " when used in an or-constraint case. Gloda's query mechanism
+ * currently lacks the ability to to compile Gloda-style and-constraints
+ * into a single MATCH query, but it will turn out okay, just less
+ * efficiently than it could.
+ */
+ NOUN_FULLTEXT: 20,
+ /**
+ * Represents a MIME Type. We currently lack any human-intelligible
+ * descriptions of mime types.
+ */
+ NOUN_MIME_TYPE: 40,
+ /**
+ * Captures a message tag as well as when the tag's presence was observed,
+ * hoping to approximate when the tag was applied. It's a somewhat dubious
+ * attempt to not waste our opporunity to store a value along with the tag.
+ * (The tag is actually stored as an attribute parameter on the attribute
+ * definition, rather than a value in the attribute 'instance' for the
+ * message.)
+ */
+ NOUN_TAG: 50,
+ /**
+ * Doesn't actually work owing to a lack of an object to represent a folder.
+ * We do expose the folderURI and folderID of a message, but need to map that
+ * to a good abstraction. Probably something thin around a SteelFolder or
+ * the like; we would contribute the functionality to easily move from a
+ * folder to the list of gloda messages in that folder, as well as the
+ * indexing preferences for that folder.
+ *
+ * @TODO folder noun and related abstraction
+ */
+ NOUN_FOLDER: 100,
+ /**
+ * All messages belong to a conversation. See GlodaDataModel.jsm for the
+ * definition of the GlodaConversation class.
+ */
+ NOUN_CONVERSATION: 101,
+ /**
+ * A one-to-one correspondence with underlying (indexed) nsIMsgDBHdr
+ * instances. See GlodaDataModel.jsm for the definition of the GlodaMessage class.
+ */
+ NOUN_MESSAGE: 102,
+ /**
+ * Corresponds to a human being, who may have multiple electronic identities
+ * (a la NOUN_IDENTITY). There is no requirement for association with an
+ * address book contact, although when the address book contact exists,
+ * we want to be associated with it. See GlodaDataModel.jsm for the definition
+ * of the GlodaContact class.
+ */
+ NOUN_CONTACT: 103,
+ /**
+ * A single identity of a contact, who may have one or more. E-mail accounts,
+ * instant messaging accounts, social network site accounts, etc. are each
+ * identities. See GlodaDataModel.jsm for the definition of the GlodaIdentity
+ * class.
+ */
+ NOUN_IDENTITY: 104,
+ /**
+ * An attachment to a message. A message may have many different attachments.
+ */
+ NOUN_ATTACHMENT: 105,
+ /**
+ * An account related to a message. A message can have only one account.
+ */
+ NOUN_ACCOUNT: 106,
+
+ /**
+ * Parameterized identities, for use in the from-me, to-me, cc-me optimization
+ * cases. Not for reuse without some thought. These nouns use the parameter
+ * to store the 'me' identity that we are talking about, and the value to
+ * store the identity of the other party. So in both the from-me and to-me
+ * cases involving 'me' and 'foo@bar', the 'me' identity is always stored via
+ * the attribute parameter, and the 'foo@bar' identity is always stored as
+ * the attribute value. See GlodaFundAttr.jsm for more information on this, but
+ * you probably shouldn't be touching this unless you are fundattr.
+ */
+ NOUN_PARAM_IDENTITY: 200,
+
+ kConstraintIdIn: 0,
+ kConstraintIn: 1,
+ kConstraintRanges: 2,
+ kConstraintEquals: 3,
+ kConstraintStringLike: 4,
+ kConstraintFulltext: 5,
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaContent.jsm b/comm/mailnews/db/gloda/modules/GlodaContent.jsm
new file mode 100644
index 0000000000..5f1daf5e9c
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaContent.jsm
@@ -0,0 +1,285 @@
+/* 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 = [
+ "GlodaContent",
+ "whittlerRegistry",
+ "mimeMsgToContentAndMeta",
+ "mimeMsgToContentSnippetAndMeta",
+];
+
+/**
+ * Given a MimeMsg and the corresponding folder, return the GlodaContent object.
+ *
+ * @param aMimeMsg: the MimeMessage instance
+ * @param folder: the nsIMsgDBFolder
+ * @returns an array containing the GlodaContent instance, and the meta dictionary
+ * that the Gloda content providers may have filled with useful data.
+ */
+
+function mimeMsgToContentAndMeta(aMimeMsg, folder) {
+ let content = new GlodaContent();
+ let meta = { subject: aMimeMsg.get("subject") };
+ let bodyLines = aMimeMsg.coerceBodyToPlaintext(folder).split(/\r?\n/);
+
+ for (let whittler of whittlerRegistry.getWhittlers()) {
+ whittler.contentWhittle(meta, bodyLines, content);
+ }
+
+ return [content, meta];
+}
+
+/**
+ * Given a MimeMsg, return the whittled content string, suitable for summarizing
+ * a message.
+ *
+ * @param aMimeMsg: the MimeMessage instance
+ * @param folder: the nsIMsgDBFolder
+ * @param length: optional number of characters to trim the whittled content.
+ * If the actual length of the message is greater than |length|, then the return
+ * value is the first (length-1) characters with an ellipsis appended.
+ * @returns an array containing the text of the snippet, and the meta dictionary
+ * that the Gloda content providers may have filled with useful data.
+ */
+
+function mimeMsgToContentSnippetAndMeta(aMimeMsg, folder, length) {
+ let [content, meta] = mimeMsgToContentAndMeta(aMimeMsg, folder);
+
+ let text = content.getContentSnippet(length + 1);
+ if (length && text.length > length) {
+ text = text.substring(0, length - 1) + "\u2026"; // ellipsis
+ }
+ return [text, meta];
+}
+
+/**
+ * A registry of gloda providers that have contentWhittle() functions.
+ * used by mimeMsgToContentSnippet, but populated by the Gloda object as it's
+ * processing providers.
+ */
+function WhittlerRegistry() {
+ this._whittlers = [];
+}
+
+WhittlerRegistry.prototype = {
+ /**
+ * Add a provider as a content whittler.
+ */
+ registerWhittler(provider) {
+ this._whittlers.push(provider);
+ },
+ /**
+ * get the list of content whittlers, sorted from the most specific to
+ * the most generic
+ */
+ getWhittlers() {
+ // Use the concat() trick to avoid mutating the internal object and
+ // leaking an internal representation.
+ return this._whittlers.concat().reverse();
+ },
+};
+
+const whittlerRegistry = new WhittlerRegistry();
+
+function GlodaContent() {
+ this._contentPriority = null;
+ this._producing = false;
+ this._hunks = [];
+}
+
+GlodaContent.prototype = {
+ kPriorityBase: 0,
+ kPriorityPerfect: 100,
+
+ kHunkMeta: 1,
+ kHunkQuoted: 2,
+ kHunkContent: 3,
+
+ _resetContent() {
+ this._keysAndValues = [];
+ this._keysAndDeltaValues = [];
+ this._hunks = [];
+ this._curHunk = null;
+ },
+
+ /* ===== Consumer API ===== */
+ hasContent() {
+ return this._contentPriority != null;
+ },
+
+ /**
+ * Return content suitable for snippet display. This means that no quoting
+ * or meta-data should be returned.
+ *
+ * @param aMaxLength The maximum snippet length desired.
+ */
+ getContentSnippet(aMaxLength) {
+ let content = this.getContentString();
+ if (aMaxLength) {
+ content = content.substring(0, aMaxLength);
+ }
+ return content;
+ },
+
+ getContentString(aIndexingPurposes) {
+ let data = "";
+ for (let hunk of this._hunks) {
+ if (hunk.hunkType == this.kHunkContent) {
+ if (data) {
+ data += "\n" + hunk.data;
+ } else {
+ data = hunk.data;
+ }
+ }
+ }
+
+ if (aIndexingPurposes) {
+ // append the values for indexing. we assume the keywords are cruft.
+ // this may be crazy, but things that aren't a science aren't an exact
+ // science.
+ for (let kv of this._keysAndValues) {
+ data += "\n" + kv[1];
+ }
+ for (let kon of this._keysAndValues) {
+ data += "\n" + kon[1] + "\n" + kon[2];
+ }
+ }
+
+ return data;
+ },
+
+ /* ===== Producer API ===== */
+ /**
+ * Called by a producer with the priority they believe their interpretation
+ * of the content comes in at.
+ *
+ * @returns true if we believe the producer's interpretation will be
+ * interesting and they should go ahead and generate events. We return
+ * false if we don't think they are interesting, in which case they should
+ * probably not issue calls to us, although we don't care. (We will
+ * ignore their calls if we return false, this allows the simplification
+ * of code that needs to run anyways.)
+ */
+ volunteerContent(aPriority) {
+ if (this._contentPriority === null || this._contentPriority < aPriority) {
+ this._contentPriority = aPriority;
+ this._resetContent();
+ this._producing = true;
+ return true;
+ }
+ this._producing = false;
+ return false;
+ },
+
+ keyValue(aKey, aValue) {
+ if (!this._producing) {
+ return;
+ }
+
+ this._keysAndValues.push([aKey, aValue]);
+ },
+ keyValueDelta(aKey, aOldValue, aNewValue) {
+ if (!this._producing) {
+ return;
+ }
+
+ this._keysAndDeltaValues.push([aKey, aOldValue, aNewValue]);
+ },
+
+ /**
+ * Meta lines are lines that have to do with the content but are not the
+ * content and can generally be related to an attribute that has been derived
+ * and stored on the item.
+ * For example, a bugzilla bug may note that an attachment was created; this
+ * is not content and wouldn't be desired in a snippet, but is still
+ * potentially interesting meta-data.
+ *
+ * @param aLineOrLines The line or list of lines that are meta-data.
+ * @param aAttr The attribute this meta-data is associated with.
+ * @param aIndex If the attribute is non-singular, indicate the specific
+ * index of the item in the attribute's bound list that the meta-data
+ * is associated with.
+ */
+ meta(aLineOrLines, aAttr, aIndex) {
+ if (!this._producing) {
+ return;
+ }
+
+ let data;
+ if (typeof aLineOrLines == "string") {
+ data = aLineOrLines;
+ } else {
+ data = aLineOrLines.join("\n");
+ }
+
+ this._curHunk = {
+ hunkType: this.kHunkMeta,
+ attr: aAttr,
+ index: aIndex,
+ data,
+ };
+ this._hunks.push(this._curHunk);
+ },
+ /**
+ * Quoted lines reference previous messages or what not.
+ *
+ * @param aLineOrLiens The line or list of lines that are quoted.
+ * @param aDepth The depth of the quoting.
+ * @param aOrigin The item that originated the original content, if known.
+ * For example, perhaps a GlodaMessage?
+ * @param aTarget A reference to the location in the original content, if
+ * known. For example, the index of a line in a message or something?
+ */
+ quoted(aLineOrLines, aDepth, aOrigin, aTarget) {
+ if (!this._producing) {
+ return;
+ }
+
+ let data;
+ if (typeof aLineOrLines == "string") {
+ data = aLineOrLines;
+ } else {
+ data = aLineOrLines.join("\n");
+ }
+
+ if (
+ !this._curHunk ||
+ this._curHunk.hunkType != this.kHunkQuoted ||
+ this._curHunk.depth != aDepth ||
+ this._curHunk.origin != aOrigin ||
+ this._curHunk.target != aTarget
+ ) {
+ this._curHunk = {
+ hunkType: this.kHunkQuoted,
+ data,
+ depth: aDepth,
+ origin: aOrigin,
+ target: aTarget,
+ };
+ this._hunks.push(this._curHunk);
+ } else {
+ this._curHunk.data += "\n" + data;
+ }
+ },
+
+ content(aLineOrLines) {
+ if (!this._producing) {
+ return;
+ }
+
+ let data;
+ if (typeof aLineOrLines == "string") {
+ data = aLineOrLines;
+ } else {
+ data = aLineOrLines.join("\n");
+ }
+
+ if (!this._curHunk || this._curHunk.hunkType != this.kHunkContent) {
+ this._curHunk = { hunkType: this.kHunkContent, data };
+ this._hunks.push(this._curHunk);
+ } else {
+ this._curHunk.data += "\n" + data;
+ }
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm b/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm
new file mode 100644
index 0000000000..d9361c079c
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaDataModel.jsm
@@ -0,0 +1,1020 @@
+/* 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 = [
+ "GlodaAttributeDBDef",
+ "GlodaAccount",
+ "GlodaConversation",
+ "GlodaFolder",
+ "GlodaMessage",
+ "GlodaContact",
+ "GlodaIdentity",
+ "GlodaAttachment",
+];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var LOG = console.createInstance({
+ prefix: "gloda.datamodel",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * @class Represents a gloda attribute definition's DB form. This class
+ * stores the information in the database relating to this attribute
+ * definition. Access its attrDef attribute to get at the really juicy data.
+ * This main interesting thing this class does is serve as the keeper of the
+ * mapping from parameters to attribute ids in the database if this is a
+ * parameterized attribute.
+ */
+function GlodaAttributeDBDef(
+ aDatastore,
+ aID,
+ aCompoundName,
+ aAttrType,
+ aPluginName,
+ aAttrName
+) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._compoundName = aCompoundName;
+ this._attrType = aAttrType;
+ this._pluginName = aPluginName;
+ this._attrName = aAttrName;
+
+ this.attrDef = null;
+
+ /** Map parameter values to the underlying database id. */
+ this._parameterBindings = {};
+}
+
+GlodaAttributeDBDef.prototype = {
+ // set by GlodaDatastore
+ _datastore: null,
+ get id() {
+ return this._id;
+ },
+ get attributeName() {
+ return this._attrName;
+ },
+
+ get parameterBindings() {
+ return this._parameterBindings;
+ },
+
+ /**
+ * Bind a parameter value to the attribute definition, allowing use of the
+ * attribute-parameter as an attribute.
+ *
+ * @returns
+ */
+ bindParameter(aValue) {
+ // people probably shouldn't call us with null, but handle it
+ if (aValue == null) {
+ return this._id;
+ }
+ if (aValue in this._parameterBindings) {
+ return this._parameterBindings[aValue];
+ }
+ // no database entry exists if we are here, so we must create it...
+ let id = this._datastore._createAttributeDef(
+ this._attrType,
+ this._pluginName,
+ this._attrName,
+ aValue
+ );
+ this._parameterBindings[aValue] = id;
+ this._datastore.reportBinding(id, this, aValue);
+ return id;
+ },
+
+ /**
+ * Given a list of values, return a list (regardless of plurality) of
+ * database-ready [attribute id, value] tuples. This is intended to be used
+ * to directly convert the value of a property on an object that corresponds
+ * to a bound attribute.
+ *
+ * @param {Array} aInstanceValues An array of instance values regardless of
+ * whether or not the attribute is singular.
+ */
+ convertValuesToDBAttributes(aInstanceValues) {
+ let nounDef = this.attrDef.objectNounDef;
+ let dbAttributes = [];
+ if (nounDef.usesParameter) {
+ for (let instanceValue of aInstanceValues) {
+ let [param, dbValue] = nounDef.toParamAndValue(instanceValue);
+ dbAttributes.push([this.bindParameter(param), dbValue]);
+ }
+ } else if ("toParamAndValue" in nounDef) {
+ // Not generating any attributes is ok. This basically means the noun is
+ // just an informative property on the Gloda Message and has no real
+ // indexing purposes.
+ for (let instanceValue of aInstanceValues) {
+ dbAttributes.push([
+ this._id,
+ nounDef.toParamAndValue(instanceValue)[1],
+ ]);
+ }
+ }
+ return dbAttributes;
+ },
+
+ toString() {
+ return this._compoundName;
+ },
+};
+
+var GlodaHasAttributesMixIn = {
+ *enumerateAttributes() {
+ let nounDef = this.NOUN_DEF;
+ for (let key in this) {
+ let value = this[key];
+ let attrDef = nounDef.attribsByBoundName[key];
+ // we expect to not have attributes for underscore prefixed values (those
+ // are managed by the instance's logic. we also want to not explode
+ // should someone crap other values in there, we get both birds with this
+ // one stone.
+ if (attrDef === undefined) {
+ continue;
+ }
+ if (attrDef.singular) {
+ // ignore attributes with null values
+ if (value != null) {
+ yield [attrDef, [value]];
+ }
+ } else if (value.length) {
+ // ignore attributes with no values
+ yield [attrDef, value];
+ }
+ }
+ },
+
+ domContribute(aDomNode) {
+ let nounDef = this.NOUN_DEF;
+ for (let attrName in nounDef.domExposeAttribsByBoundName) {
+ let attr = nounDef.domExposeAttribsByBoundName[attrName];
+ if (this[attrName]) {
+ aDomNode.setAttribute(attr.domExpose, this[attrName]);
+ }
+ }
+ },
+};
+
+function MixIn(aConstructor, aMixIn) {
+ let proto = aConstructor.prototype;
+ for (let [name, func] of Object.entries(aMixIn)) {
+ if (name.startsWith("get_")) {
+ proto.__defineGetter__(name.substring(4), func);
+ } else {
+ proto[name] = func;
+ }
+ }
+}
+
+/**
+ * @class A gloda wrapper around nsIMsgIncomingServer.
+ */
+function GlodaAccount(aIncomingServer) {
+ this._incomingServer = aIncomingServer;
+}
+
+GlodaAccount.prototype = {
+ NOUN_ID: 106,
+ get id() {
+ return this._incomingServer.key;
+ },
+ get name() {
+ return this._incomingServer.prettyName;
+ },
+ get incomingServer() {
+ return this._incomingServer;
+ },
+ toString() {
+ return "Account: " + this.id;
+ },
+
+ toLocaleString() {
+ return this.name;
+ },
+};
+
+/**
+ * @class A gloda conversation (thread) exists so that messages can belong.
+ */
+function GlodaConversation(
+ aDatastore,
+ aID,
+ aSubject,
+ aOldestMessageDate,
+ aNewestMessageDate
+) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._subject = aSubject;
+ this._oldestMessageDate = aOldestMessageDate;
+ this._newestMessageDate = aNewestMessageDate;
+}
+
+GlodaConversation.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_CONVERSATION,
+ // set by GlodaDatastore
+ _datastore: null,
+ get id() {
+ return this._id;
+ },
+ get subject() {
+ return this._subject;
+ },
+ get oldestMessageDate() {
+ return this._oldestMessageDate;
+ },
+ get newestMessageDate() {
+ return this._newestMessageDate;
+ },
+
+ getMessagesCollection(aListener, aData) {
+ let query = new GlodaMessage.prototype.NOUN_DEF.queryClass();
+ query.conversation(this._id).orderBy("date");
+ return query.getCollection(aListener, aData);
+ },
+
+ toString() {
+ return "Conversation:" + this._id;
+ },
+
+ toLocaleString() {
+ return this._subject;
+ },
+};
+
+function GlodaFolder(
+ aDatastore,
+ aID,
+ aURI,
+ aDirtyStatus,
+ aPrettyName,
+ aIndexingPriority
+) {
+ // _datastore is now set by GlodaDatastore
+ this._id = aID;
+ this._uri = aURI;
+ this._dirtyStatus = aDirtyStatus;
+ this._prettyName = aPrettyName;
+ this._account = null;
+ this._activeIndexing = false;
+ this._indexingPriority = aIndexingPriority;
+ this._deleted = false;
+ this._compacting = false;
+}
+
+GlodaFolder.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_FOLDER,
+ // set by GlodaDatastore
+ _datastore: null,
+
+ /** The folder is believed to be up-to-date */
+ kFolderClean: 0,
+ /** The folder has some un-indexed or dirty messages */
+ kFolderDirty: 1,
+ /** The folder needs to be entirely re-indexed, regardless of the flags on
+ * the messages in the folder. This state will be downgraded to dirty */
+ kFolderFilthy: 2,
+
+ _kFolderDirtyStatusMask: 0x7,
+ /**
+ * The (local) folder has been compacted and all of its message keys are
+ * potentially incorrect. This is not a possible state for IMAP folders
+ * because their message keys are based on UIDs rather than offsets into
+ * the mbox file.
+ */
+ _kFolderCompactedFlag: 0x8,
+
+ /** The folder should never be indexed. */
+ kIndexingNeverPriority: -1,
+ /** The lowest priority assigned to a folder. */
+ kIndexingLowestPriority: 0,
+ /** The highest priority assigned to a folder. */
+ kIndexingHighestPriority: 100,
+
+ /** The indexing priority for a folder if no other priority is assigned. */
+ kIndexingDefaultPriority: 20,
+ /** Folders marked check new are slightly more important I guess. */
+ kIndexingCheckNewPriority: 30,
+ /** Favorite folders are more interesting to the user, presumably. */
+ kIndexingFavoritePriority: 40,
+ /** The indexing priority for inboxes. */
+ kIndexingInboxPriority: 50,
+ /** The indexing priority for sent mail folders. */
+ kIndexingSentMailPriority: 60,
+
+ get id() {
+ return this._id;
+ },
+ get uri() {
+ return this._uri;
+ },
+ get dirtyStatus() {
+ return this._dirtyStatus & this._kFolderDirtyStatusMask;
+ },
+ /**
+ * Mark a folder as dirty if it was clean. Do nothing if it was already dirty
+ * or filthy. For use by GlodaMsgIndexer only. And maybe rkent and his
+ * marvelous extensions.
+ */
+ _ensureFolderDirty() {
+ if (this.dirtyStatus == this.kFolderClean) {
+ this._dirtyStatus =
+ (this.kFolderDirty & this._kFolderDirtyStatusMask) |
+ (this._dirtyStatus & ~this._kFolderDirtyStatusMask);
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+ /**
+ * Definitely for use only by GlodaMsgIndexer to downgrade the dirty status of
+ * a folder.
+ */
+ _downgradeDirtyStatus(aNewStatus) {
+ if (this.dirtyStatus != aNewStatus) {
+ this._dirtyStatus =
+ (aNewStatus & this._kFolderDirtyStatusMask) |
+ (this._dirtyStatus & ~this._kFolderDirtyStatusMask);
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+ /**
+ * Indicate whether this folder is currently being compacted. The
+ * |GlodaMsgIndexer| keeps this in-memory-only value up-to-date.
+ */
+ get compacting() {
+ return this._compacting;
+ },
+ /**
+ * Set whether this folder is currently being compacted. This is really only
+ * for the |GlodaMsgIndexer| to set.
+ */
+ set compacting(aCompacting) {
+ this._compacting = aCompacting;
+ },
+ /**
+ * Indicate whether this folder was compacted and has not yet been
+ * compaction processed.
+ */
+ get compacted() {
+ return Boolean(this._dirtyStatus & this._kFolderCompactedFlag);
+ },
+ /**
+ * For use only by GlodaMsgIndexer to set/clear the compaction state of this
+ * folder.
+ */
+ _setCompactedState(aCompacted) {
+ if (this.compacted != aCompacted) {
+ if (aCompacted) {
+ this._dirtyStatus |= this._kFolderCompactedFlag;
+ } else {
+ this._dirtyStatus &= ~this._kFolderCompactedFlag;
+ }
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+
+ get name() {
+ return this._prettyName;
+ },
+ toString() {
+ return "Folder:" + this._id;
+ },
+
+ toLocaleString() {
+ let xpcomFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ if (!xpcomFolder) {
+ return this._prettyName;
+ }
+ return (
+ xpcomFolder.prettyName + " (" + xpcomFolder.rootFolder.prettyName + ")"
+ );
+ },
+
+ get indexingPriority() {
+ return this._indexingPriority;
+ },
+
+ /** We are going to index this folder. */
+ kActivityIndexing: 0,
+ /** Asking for the folder to perform header retrievals. */
+ kActivityHeaderRetrieval: 1,
+ /** We only want the folder for its metadata but are not going to open it. */
+ kActivityFolderOnlyNoData: 2,
+
+ /** Is this folder known to be actively used for indexing? */
+ _activeIndexing: false,
+ /** Get our indexing status. */
+ get indexing() {
+ return this._activeIndexing;
+ },
+ /**
+ * Set our indexing status. Normally, this will be enabled through passing
+ * an activity type of kActivityIndexing (which will set us), but we will
+ * still need to be explicitly disabled by the indexing code.
+ * When disabling indexing, we will call forgetFolderIfUnused to take care of
+ * shutting things down.
+ * We are not responsible for committing changes to the message database!
+ * That is on you!
+ */
+ set indexing(aIndexing) {
+ this._activeIndexing = aIndexing;
+ },
+
+ /**
+ * Retrieve the nsIMsgFolder instance corresponding to this folder, providing
+ * an explanation of why you are requesting it for tracking/cleanup purposes.
+ *
+ * @param aActivity One of the kActivity* constants. If you pass
+ * kActivityIndexing, we will set indexing for you, but you will need to
+ * clear it when you are done.
+ * @returns The nsIMsgFolder if available, null on failure.
+ */
+ getXPCOMFolder(aActivity) {
+ switch (aActivity) {
+ case this.kActivityIndexing:
+ // mark us as indexing, but don't bother with live tracking. we do
+ // that independently and only for header retrieval.
+ this.indexing = true;
+ break;
+ case this.kActivityHeaderRetrieval:
+ case this.kActivityFolderOnlyNoData:
+ // we don't have to do anything here.
+ break;
+ }
+
+ return MailServices.folderLookup.getFolderForURL(this.uri);
+ },
+
+ /**
+ * Retrieve a GlodaAccount instance corresponding to this folder.
+ *
+ * @returns The GlodaAccount instance.
+ */
+ getAccount() {
+ if (!this._account) {
+ let msgFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+ this._account = new GlodaAccount(msgFolder.server);
+ }
+ return this._account;
+ },
+};
+
+/**
+ * @class A message representation.
+ */
+function GlodaMessage(
+ aDatastore,
+ aID,
+ aFolderID,
+ aMessageKey,
+ aConversationID,
+ aConversation,
+ aDate,
+ aHeaderMessageID,
+ aDeleted,
+ aJsonText,
+ aNotability,
+ aSubject,
+ aIndexedBodyText,
+ aAttachmentNames
+) {
+ // _datastore is now set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._folderID = aFolderID;
+ this._messageKey = aMessageKey;
+ this._conversationID = aConversationID;
+ this._conversation = aConversation;
+ this._date = aDate;
+ this._headerMessageID = aHeaderMessageID;
+ this._jsonText = aJsonText;
+ this._notability = aNotability;
+ this._subject = aSubject;
+ this._indexedBodyText = aIndexedBodyText;
+ this._attachmentNames = aAttachmentNames;
+
+ // only set _deleted if we're deleted, otherwise the undefined does our
+ // speaking for us.
+ if (aDeleted) {
+ this._deleted = aDeleted;
+ }
+}
+
+GlodaMessage.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_MESSAGE,
+ // set by GlodaDatastore
+ _datastore: null,
+ get id() {
+ return this._id;
+ },
+ get folderID() {
+ return this._folderID;
+ },
+ get messageKey() {
+ return this._messageKey;
+ },
+ get conversationID() {
+ return this._conversationID;
+ },
+ // conversation is special
+ get headerMessageID() {
+ return this._headerMessageID;
+ },
+ get notability() {
+ return this._notability;
+ },
+ set notability(aNotability) {
+ this._notability = aNotability;
+ },
+
+ get subject() {
+ return this._subject;
+ },
+ get indexedBodyText() {
+ return this._indexedBodyText;
+ },
+ get attachmentNames() {
+ return this._attachmentNames;
+ },
+
+ get date() {
+ return this._date;
+ },
+ set date(aNewDate) {
+ this._date = aNewDate;
+ },
+
+ get folder() {
+ // XXX due to a deletion bug it is currently possible to get in a state
+ // where we have an illegal folderID value. This will result in an
+ // exception. As a workaround, let's just return null in that case.
+ try {
+ if (this._folderID != null) {
+ return this._datastore._mapFolderID(this._folderID);
+ }
+ } catch (ex) {}
+ return null;
+ },
+ get folderURI() {
+ // XXX just like for folder, handle mapping failures and return null
+ try {
+ if (this._folderID != null) {
+ return this._datastore._mapFolderID(this._folderID).uri;
+ }
+ } catch (ex) {}
+ return null;
+ },
+ get account() {
+ // XXX due to a deletion bug it is currently possible to get in a state
+ // where we have an illegal folderID value. This will result in an
+ // exception. As a workaround, let's just return null in that case.
+ try {
+ if (this._folderID == null) {
+ return null;
+ }
+ let folder = this._datastore._mapFolderID(this._folderID);
+ return folder.getAccount();
+ } catch (ex) {}
+ return null;
+ },
+ get conversation() {
+ return this._conversation;
+ },
+
+ toString() {
+ // uh, this is a tough one...
+ return "Message:" + this._id;
+ },
+
+ _clone() {
+ return new GlodaMessage(
+ /* datastore */ null,
+ this._id,
+ this._folderID,
+ this._messageKey,
+ this._conversationID,
+ this._conversation,
+ this._date,
+ this._headerMessageID,
+ "_deleted" in this ? this._deleted : undefined,
+ "_jsonText" in this ? this._jsonText : undefined,
+ this._notability,
+ this._subject,
+ this._indexedBodyText,
+ this._attachmentNames
+ );
+ },
+
+ /**
+ * Provide a means of propagating changed values on our clone back to
+ * ourselves. This is required because of an object identity trick gloda
+ * does; when indexing an already existing object, all mutations happen on
+ * a clone of the existing object so that
+ */
+ _declone(aOther) {
+ if ("_content" in aOther) {
+ this._content = aOther._content;
+ }
+
+ // The _indexedAuthor/_indexedRecipients fields don't get updated on
+ // fulltext update so we don't need to propagate.
+ this._indexedBodyText = aOther._indexedBodyText;
+ this._attachmentNames = aOther._attachmentNames;
+ },
+
+ /**
+ * Mark this message as a ghost. Ghosts are characterized by having no folder
+ * id and no message key. They also are not deleted or they would be of
+ * absolutely no use to us.
+ *
+ * These changes are suitable for persistence.
+ */
+ _ghost() {
+ this._folderID = null;
+ this._messageKey = null;
+ if ("_deleted" in this) {
+ delete this._deleted;
+ }
+ },
+
+ /**
+ * Are we a ghost (which implies not deleted)? We are not a ghost if we have
+ * a definite folder location (we may not know our message key in the case
+ * of IMAP moves not fully completed) and are not deleted.
+ */
+ get _isGhost() {
+ return this._folderID == null && !this._isDeleted;
+ },
+
+ /**
+ * If we were dead, un-dead us.
+ */
+ _ensureNotDeleted() {
+ if ("_deleted" in this) {
+ delete this._deleted;
+ }
+ },
+
+ /**
+ * Are we deleted? This is private because deleted gloda messages are not
+ * visible to non-core-gloda code.
+ */
+ get _isDeleted() {
+ return "_deleted" in this && this._deleted;
+ },
+
+ /**
+ * Trash this message's in-memory representation because it should no longer
+ * be reachable by any code. The database record is gone, it's not coming
+ * back.
+ */
+ _objectPurgedMakeYourselfUnpleasant() {
+ this._id = null;
+ this._folderID = null;
+ this._messageKey = null;
+ this._conversationID = null;
+ this._conversation = null;
+ this.date = null;
+ this._headerMessageID = null;
+ },
+
+ /**
+ * Return the underlying nsIMsgDBHdr from the folder storage for this, or
+ * null if the message does not exist for one reason or another. We may log
+ * to our logger in the failure cases.
+ *
+ * This method no longer caches the result, so if you need to hold onto it,
+ * hold onto it.
+ *
+ * In the process of retrieving the underlying message header, we may have to
+ * open the message header database associated with the folder. This may
+ * result in blocking while the load happens, so you may want to try and find
+ * an alternate way to initiate the load before calling us.
+ * We provide hinting to the GlodaDatastore via the GlodaFolder so that it
+ * knows when it's a good time for it to go and detach from the database.
+ *
+ * @returns The nsIMsgDBHdr associated with this message if available, null on
+ * failure.
+ */
+ get folderMessage() {
+ if (this._folderID === null || this._messageKey === null) {
+ return null;
+ }
+
+ // XXX like for folder and folderURI, return null if we can't map the folder
+ let glodaFolder;
+ try {
+ glodaFolder = this._datastore._mapFolderID(this._folderID);
+ } catch (ex) {
+ return null;
+ }
+ let folder = glodaFolder.getXPCOMFolder(
+ glodaFolder.kActivityHeaderRetrieval
+ );
+ if (folder) {
+ let folderMessage;
+ try {
+ folderMessage = folder.GetMessageHeader(this._messageKey);
+ } catch (ex) {
+ folderMessage = null;
+ }
+ if (folderMessage !== null) {
+ // verify the message-id header matches what we expect...
+ if (folderMessage.messageId != this._headerMessageID) {
+ LOG.info(
+ "Message with message key " +
+ this._messageKey +
+ " in folder '" +
+ folder.URI +
+ "' does not match expected " +
+ "header! (" +
+ this._headerMessageID +
+ " expected, got " +
+ folderMessage.messageId +
+ ")"
+ );
+ folderMessage = null;
+ }
+ }
+ return folderMessage;
+ }
+
+ // this only gets logged if things have gone very wrong. we used to throw
+ // here, but it's unlikely our caller can do anything more meaningful than
+ // treating this as a disappeared message.
+ LOG.info(
+ "Unable to locate folder message for: " +
+ this._folderID +
+ ":" +
+ this._messageKey
+ );
+ return null;
+ },
+ get folderMessageURI() {
+ let folderMessage = this.folderMessage;
+ if (folderMessage) {
+ return folderMessage.folder.getUriForMsg(folderMessage);
+ }
+ return null;
+ },
+};
+MixIn(GlodaMessage, GlodaHasAttributesMixIn);
+
+/**
+ * @class Contacts correspond to people (one per person), and may own multiple
+ * identities (e-mail address, IM account, etc.)
+ */
+function GlodaContact(
+ aDatastore,
+ aID,
+ aDirectoryUUID,
+ aContactUUID,
+ aName,
+ aPopularity,
+ aFrecency,
+ aJsonText
+) {
+ // _datastore set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._directoryUUID = aDirectoryUUID;
+ this._contactUUID = aContactUUID;
+ this._name = aName;
+ this._popularity = aPopularity;
+ this._frecency = aFrecency;
+ if (aJsonText) {
+ this._jsonText = aJsonText;
+ }
+
+ this._identities = null;
+}
+
+GlodaContact.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_CONTACT,
+ // set by GlodaDatastore
+ _datastore: null,
+
+ get id() {
+ return this._id;
+ },
+ get directoryUUID() {
+ return this._directoryUUID;
+ },
+ get contactUUID() {
+ return this._contactUUID;
+ },
+ get name() {
+ return this._name;
+ },
+ set name(aName) {
+ this._name = aName;
+ },
+
+ get popularity() {
+ return this._popularity;
+ },
+ set popularity(aPopularity) {
+ this._popularity = aPopularity;
+ this.dirty = true;
+ },
+
+ get frecency() {
+ return this._frecency;
+ },
+ set frecency(aFrecency) {
+ this._frecency = aFrecency;
+ this.dirty = true;
+ },
+
+ get identities() {
+ return this._identities;
+ },
+
+ toString() {
+ return "Contact:" + this._id;
+ },
+
+ get accessibleLabel() {
+ return "Contact: " + this._name;
+ },
+
+ _clone() {
+ return new GlodaContact(
+ /* datastore */ null,
+ this._id,
+ this._directoryUUID,
+ this._contactUUID,
+ this._name,
+ this._popularity,
+ this._frecency
+ );
+ },
+};
+MixIn(GlodaContact, GlodaHasAttributesMixIn);
+
+/**
+ * @class A specific means of communication for a contact.
+ */
+function GlodaIdentity(
+ aDatastore,
+ aID,
+ aContactID,
+ aContact,
+ aKind,
+ aValue,
+ aDescription,
+ aIsRelay
+) {
+ // _datastore set on the prototype by GlodaDatastore
+ this._id = aID;
+ this._contactID = aContactID;
+ this._contact = aContact;
+ this._kind = aKind;
+ this._value = aValue;
+ this._description = aDescription;
+ this._isRelay = aIsRelay;
+ // Cached indication of whether there is an address book card for this
+ // identity. We keep this up-to-date via address book listener
+ // notifications in |GlodaABIndexer|.
+ this._hasAddressBookCard = undefined;
+}
+
+GlodaIdentity.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_IDENTITY,
+ // set by GlodaDatastore
+ _datastore: null,
+ get id() {
+ return this._id;
+ },
+ get contactID() {
+ return this._contactID;
+ },
+ get contact() {
+ return this._contact;
+ },
+ get kind() {
+ return this._kind;
+ },
+ get value() {
+ return this._value;
+ },
+ get description() {
+ return this._description;
+ },
+ get isRelay() {
+ return this._isRelay;
+ },
+
+ get uniqueValue() {
+ return this._kind + "@" + this._value;
+ },
+
+ toString() {
+ return "Identity:" + this._kind + ":" + this._value;
+ },
+
+ toLocaleString() {
+ if (this.contact.name == this.value) {
+ return this.value;
+ }
+ return this.contact.name + " : " + this.value;
+ },
+
+ get abCard() {
+ // for our purposes, the address book only speaks email
+ if (this._kind != "email") {
+ return false;
+ }
+ let card = MailServices.ab.cardForEmailAddress(this._value);
+ this._hasAddressBookCard = card != null;
+ return card;
+ },
+
+ /**
+ * Indicates whether we have an address book card for this identity. This
+ * value is cached once looked-up and kept up-to-date by |GlodaABIndexer|
+ * and its notifications.
+ */
+ get inAddressBook() {
+ if (this._hasAddressBookCard !== undefined) {
+ return this._hasAddressBookCard;
+ }
+ return (this.abCard && true) || false;
+ },
+};
+
+/**
+ * An attachment, with as much information as we can gather on it
+ */
+function GlodaAttachment(
+ aGlodaMessage,
+ aName,
+ aContentType,
+ aSize,
+ aPart,
+ aExternalUrl,
+ aIsExternal
+) {
+ // _datastore set on the prototype by GlodaDatastore
+ this._glodaMessage = aGlodaMessage;
+ this._name = aName;
+ this._contentType = aContentType;
+ this._size = aSize;
+ this._part = aPart;
+ this._externalUrl = aExternalUrl;
+ this._isExternal = aIsExternal;
+}
+
+GlodaAttachment.prototype = {
+ NOUN_ID: GlodaConstants.NOUN_ATTACHMENT,
+ // set by GlodaDatastore
+ get name() {
+ return this._name;
+ },
+ get contentType() {
+ return this._contentType;
+ },
+ get size() {
+ return this._size;
+ },
+ get url() {
+ if (this.isExternal) {
+ return this._externalUrl;
+ }
+
+ let uri = this._glodaMessage.folderMessageURI;
+ if (!uri) {
+ throw new Error(
+ "The message doesn't exist anymore, unable to rebuild attachment URL"
+ );
+ }
+ let msgService = MailServices.messageServiceFromURI(uri);
+ let neckoURL = msgService.getUrlForUri(uri);
+ let url = neckoURL.spec;
+ let hasParamAlready = url.match(/\?[a-z]+=[^\/]+$/);
+ let sep = hasParamAlready ? "&" : "?";
+ return (
+ url +
+ sep +
+ "part=" +
+ this._part +
+ "&filename=" +
+ encodeURIComponent(this._name)
+ );
+ },
+ get isExternal() {
+ return this._isExternal;
+ },
+
+ toString() {
+ return "attachment: " + this._name + ":" + this._contentType;
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaDatabind.jsm b/comm/mailnews/db/gloda/modules/GlodaDatabind.jsm
new file mode 100644
index 0000000000..eda41cb91a
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaDatabind.jsm
@@ -0,0 +1,210 @@
+/* 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 = ["GlodaDatabind"];
+
+function GlodaDatabind(aNounDef, aDatastore) {
+ this._nounDef = aNounDef;
+ this._tableName = aNounDef.tableName;
+ this._tableDef = aNounDef.schema;
+ this._datastore = aDatastore;
+ this._log = console.createInstance({
+ prefix: `gloda.databind.${this._tableName}`,
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+
+ // process the column definitions and make sure they have an attribute mapping
+ for (let [iColDef, coldef] of this._tableDef.columns.entries()) {
+ // default to the other dude's thing.
+ if (coldef.length < 3) {
+ coldef[2] = coldef[0];
+ }
+ if (coldef[0] == "id") {
+ this._idAttr = coldef[2];
+ }
+ // colDef[3] is the index of us in our SQL bindings, storage-numbering
+ coldef[3] = iColDef;
+ }
+
+ // XXX This is obviously synchronous and not perfectly async. Since we are
+ // doing this, we don't actually need to move to ordinal binding below
+ // since we could just as well compel creation of the name map and thereby
+ // avoid ever acquiring the mutex after bootstrap.
+ // However, this specific check can be cleverly avoided with future work.
+ // Namely, at startup we can scan for extension-defined tables and get their
+ // maximum id so that we don't need to do it here. The table will either
+ // be brand new and thus have a maximum id of 1 or we will already know it
+ // because of that scan.
+ this._nextId = 1;
+ let stmt = this._datastore._createSyncStatement(
+ "SELECT MAX(id) FROM " + this._tableName,
+ true
+ );
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ this._nextId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+
+ let insertColumns = [];
+ let insertValues = [];
+ let updateItems = [];
+ for (let [iColDef, coldef] of this._tableDef.columns.entries()) {
+ let column = coldef[0];
+ let placeholder = "?" + (iColDef + 1);
+ insertColumns.push(column);
+ insertValues.push(placeholder);
+ if (column != "id") {
+ updateItems.push(column + " = " + placeholder);
+ }
+ }
+
+ let insertSql =
+ "INSERT INTO " +
+ this._tableName +
+ " (" +
+ insertColumns.join(", ") +
+ ") VALUES (" +
+ insertValues.join(", ") +
+ ")";
+
+ // For the update, we want the 'id' to be a constraint and not a value
+ // that gets set...
+ let updateSql =
+ "UPDATE " +
+ this._tableName +
+ " SET " +
+ updateItems.join(", ") +
+ " WHERE id = ?1";
+ this._insertStmt = aDatastore._createAsyncStatement(insertSql);
+ this._updateStmt = aDatastore._createAsyncStatement(updateSql);
+
+ if (this._tableDef.fulltextColumns) {
+ for (let [iColDef, coldef] of this._tableDef.fulltextColumns.entries()) {
+ if (coldef.length < 3) {
+ coldef[2] = coldef[0];
+ }
+ // colDef[3] is the index of us in our SQL bindings, storage-numbering
+ coldef[3] = iColDef + 1;
+ }
+
+ let insertColumns = [];
+ let insertValues = [];
+ let updateItems = [];
+ for (var [iColDef, coldef] of this._tableDef.fulltextColumns.entries()) {
+ let column = coldef[0];
+ // +2 instead of +1 because docid is implied
+ let placeholder = "?" + (iColDef + 2);
+ insertColumns.push(column);
+ insertValues.push(placeholder);
+ if (column != "id") {
+ updateItems.push(column + " = " + placeholder);
+ }
+ }
+
+ let insertFulltextSql =
+ "INSERT INTO " +
+ this._tableName +
+ "Text (docid," +
+ insertColumns.join(", ") +
+ ") VALUES (?1," +
+ insertValues.join(", ") +
+ ")";
+
+ // For the update, we want the 'id' to be a constraint and not a value
+ // that gets set...
+ let updateFulltextSql =
+ "UPDATE " +
+ this._tableName +
+ "Text SET " +
+ updateItems.join(", ") +
+ " WHERE docid = ?1";
+
+ this._insertFulltextStmt =
+ aDatastore._createAsyncStatement(insertFulltextSql);
+ this._updateFulltextStmt =
+ aDatastore._createAsyncStatement(updateFulltextSql);
+ }
+}
+
+GlodaDatabind.prototype = {
+ /**
+ * Perform appropriate binding coercion based on the schema provided to us.
+ * Although we end up effectively coercing JS Date objects to numeric values,
+ * we should not be provided with JS Date objects! There is no way for us
+ * to know to turn them back into JS Date objects on the way out.
+ * Additionally, there is the small matter of storage's bias towards
+ * PRTime representations which may not always be desirable.
+ */
+ bindByType(aStmt, aColDef, aValue) {
+ aStmt.bindByIndex(aColDef[3], aValue);
+ },
+
+ objFromRow(aRow) {
+ let getVariant = this._datastore._getVariant;
+ let obj = new this._nounDef.class();
+ for (let [iCol, colDef] of this._tableDef.columns.entries()) {
+ obj[colDef[2]] = getVariant(aRow, iCol);
+ }
+ return obj;
+ },
+
+ objInsert(aThing) {
+ let bindByType = this.bindByType;
+ if (!aThing[this._idAttr]) {
+ aThing[this._idAttr] = this._nextId++;
+ }
+
+ let stmt = this._insertStmt;
+ for (let colDef of this._tableDef.columns) {
+ bindByType(stmt, colDef, aThing[colDef[2]]);
+ }
+
+ stmt.executeAsync(this._datastore.trackAsync());
+
+ if (this._insertFulltextStmt) {
+ stmt = this._insertFulltextStmt;
+ stmt.bindByIndex(0, aThing[this._idAttr]);
+ for (let colDef of this._tableDef.fulltextColumns) {
+ bindByType(stmt, colDef, aThing[colDef[2]]);
+ }
+ stmt.executeAsync(this._datastore.trackAsync());
+ }
+ },
+
+ objUpdate(aThing) {
+ let bindByType = this.bindByType;
+ let stmt = this._updateStmt;
+ // note, we specially bound the location of 'id' for the insert, but since
+ // we're using named bindings, there is nothing special about setting it
+ for (let colDef of this._tableDef.columns) {
+ bindByType(stmt, colDef, aThing[colDef[2]]);
+ }
+ stmt.executeAsync(this._datastore.trackAsync());
+
+ if (this._updateFulltextStmt) {
+ stmt = this._updateFulltextStmt;
+ // fulltextColumns doesn't include id/docid, need to explicitly set it
+ stmt.bindByIndex(0, aThing[this._idAttr]);
+ for (let colDef of this._tableDef.fulltextColumns) {
+ bindByType(stmt, colDef, aThing[colDef[2]]);
+ }
+ stmt.executeAsync(this._datastore.trackAsync());
+ }
+ },
+
+ adjustAttributes(...aArgs) {
+ // just proxy the call over to the datastore... we have to do this for
+ // 'this' reasons. we don't refactor things to avoid this because it does
+ // make some sense to have all the methods exposed from a single object,
+ // even if the implementation does live elsewhere.
+ return this._datastore.adjustAttributes(...aArgs);
+ },
+
+ // also proxied...
+ queryFromQuery(...aArgs) {
+ return this._datastore.queryFromQuery(...aArgs);
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaDatastore.jsm b/comm/mailnews/db/gloda/modules/GlodaDatastore.jsm
new file mode 100644
index 0000000000..1391ceaaf2
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaDatastore.jsm
@@ -0,0 +1,4402 @@
+/* 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/. */
+
+/* This file looks to Myk Melez <myk@mozilla.org>'s Mozilla Labs snowl
+ * project's (https://hg.mozilla.org/labs/snowl/) modules/GlodaDatastore.jsm
+ * for inspiration and idioms (and also a name :).
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaDatastore"];
+
+const {
+ GlodaAttributeDBDef,
+ GlodaConversation,
+ GlodaFolder,
+ GlodaMessage,
+ GlodaContact,
+ GlodaIdentity,
+} = ChromeUtils.import("resource:///modules/gloda/GlodaDataModel.jsm");
+const { GlodaDatabind } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatabind.jsm"
+);
+const { GlodaCollection, GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+var MIN_CACHE_SIZE = 8 * 1048576;
+var MAX_CACHE_SIZE = 64 * 1048576;
+var MEMSIZE_FALLBACK_BYTES = 256 * 1048576;
+
+var PCH_LOG = console.createInstance({
+ prefix: "gloda.ds.pch",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * Commit async handler; hands off the notification to
+ * |GlodaDatastore._asyncCompleted|.
+ */
+function PostCommitHandler(aCallbacks) {
+ this.callbacks = aCallbacks;
+ GlodaDatastore._pendingAsyncStatements++;
+}
+
+PostCommitHandler.prototype = {
+ handleResult(aResultSet) {},
+
+ handleError(aError) {
+ PCH_LOG.error("database error:" + aError);
+ },
+
+ handleCompletion(aReason) {
+ // just outright bail if we are shutdown
+ if (GlodaDatastore.datastoreIsShutdown) {
+ return;
+ }
+
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ for (let callback of this.callbacks) {
+ try {
+ callback();
+ } catch (ex) {
+ PCH_LOG.error(
+ "PostCommitHandler callback (" +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ") threw: " +
+ ex
+ );
+ }
+ }
+ }
+ try {
+ GlodaDatastore._asyncCompleted();
+ } catch (e) {
+ PCH_LOG.error("Exception in handleCompletion:", e);
+ }
+ },
+};
+
+var QFQ_LOG = console.createInstance({
+ prefix: "gloda.ds.qfq",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * Singleton collection listener used by |QueryFromQueryCallback| to assist in
+ * the loading of referenced noun instances. Which is to say, messages have
+ * identities (specific e-mail addresses) associated with them via attributes.
+ * And these identities in turn reference / are referenced by contacts (the
+ * notion of a person).
+ *
+ * This listener is primarily concerned with fixing up the references in each
+ * noun instance to its referenced instances once they have been loaded. It
+ * also deals with caching so that our identity invariant is maintained: user
+ * code should only ever see one distinct instance of a thing at a time.
+ */
+var QueryFromQueryResolver = {
+ onItemsAdded(aIgnoredItems, aCollection, aFake) {
+ let originColl = aCollection.dataStack
+ ? aCollection.dataStack.pop()
+ : aCollection.data;
+ // QFQ_LOG.debug("QFQR: originColl: " + originColl);
+ if (aCollection.completionShifter) {
+ aCollection.completionShifter.push(originColl);
+ } else {
+ aCollection.completionShifter = [originColl];
+ }
+
+ if (!aFake) {
+ originColl.deferredCount--;
+ originColl.resolvedCount++;
+ }
+
+ // bail if we are still pending on some other load completion
+ if (originColl.deferredCount > 0) {
+ // QFQ_LOG.debug("QFQR: bailing " + originColl._nounDef.name);
+ return;
+ }
+
+ let referencesByNounID = originColl.masterCollection.referencesByNounID;
+ let inverseReferencesByNounID =
+ originColl.masterCollection.inverseReferencesByNounID;
+
+ if (originColl.pendingItems) {
+ for (let item of originColl.pendingItems) {
+ // QFQ_LOG.debug("QFQR: loading deferred " + item.NOUN_ID + ":" + item.id);
+ GlodaDatastore.loadNounDeferredDeps(
+ item,
+ referencesByNounID,
+ inverseReferencesByNounID
+ );
+ }
+
+ // we need to consider the possibility that we are racing a collection very
+ // much like our own. as such, this means we need to perform cache
+ // unification as our last step.
+ GlodaCollectionManager.cacheLoadUnify(
+ originColl._nounDef.id,
+ originColl.pendingItems,
+ false
+ );
+
+ // just directly tell the collection about the items. we know the query
+ // matches (at least until we introduce predicates that we cannot express
+ // in SQL.)
+ // QFQ_LOG.debug(" QFQR: about to trigger listener: " + originColl._listener +
+ // "with collection: " + originColl._nounDef.name);
+ originColl._onItemsAdded(originColl.pendingItems);
+ delete originColl.pendingItems;
+ delete originColl._pendingIdMap;
+ }
+ },
+ onItemsModified() {},
+ onItemsRemoved() {},
+ onQueryCompleted(aCollection) {
+ let originColl = aCollection.completionShifter
+ ? aCollection.completionShifter.shift()
+ : aCollection.data;
+ // QFQ_LOG.debug(" QFQR about to trigger completion with collection: " +
+ // originColl._nounDef.name);
+ if (originColl.deferredCount <= 0) {
+ originColl._onQueryCompleted();
+ }
+ },
+};
+
+/**
+ * Handles the results from a GlodaDatastore.queryFromQuery call in cooperation
+ * with the |QueryFromQueryResolver| collection listener. We do a lot of
+ * legwork related to satisfying references to other noun instances on the
+ * noun instances the user directly queried. Messages reference identities
+ * reference contacts which in turn (implicitly) reference identities again.
+ * We have to spin up those other queries and stitch things together.
+ *
+ * While the code is generally up to the existing set of tasks it is called to
+ * handle, I would not be surprised for it to fall down if things get more
+ * complex. Some of the logic here 'evolved' a bit and could benefit from
+ * additional documentation and a fresh go-through.
+ */
+function QueryFromQueryCallback(aStatement, aNounDef, aCollection) {
+ this.statement = aStatement;
+ this.nounDef = aNounDef;
+ this.collection = aCollection;
+
+ // QFQ_LOG.debug("Creating QFQCallback for noun: " + aNounDef.name);
+
+ // the master collection holds the referencesByNounID
+ this.referencesByNounID = {};
+ this.masterReferencesByNounID =
+ this.collection.masterCollection.referencesByNounID;
+ this.inverseReferencesByNounID = {};
+ this.masterInverseReferencesByNounID =
+ this.collection.masterCollection.inverseReferencesByNounID;
+ // we need to contribute our references as we load things; we need this
+ // because of the potential for circular dependencies and our inability to
+ // put things into the caching layer (or collection's _idMap) until we have
+ // fully resolved things.
+ if (this.nounDef.id in this.masterReferencesByNounID) {
+ this.selfReferences = this.masterReferencesByNounID[this.nounDef.id];
+ } else {
+ this.selfReferences = this.masterReferencesByNounID[this.nounDef.id] = {};
+ }
+ if (this.nounDef.parentColumnAttr) {
+ if (this.nounDef.id in this.masterInverseReferencesByNounID) {
+ this.selfInverseReferences =
+ this.masterInverseReferencesByNounID[this.nounDef.id];
+ } else {
+ this.selfInverseReferences = this.masterInverseReferencesByNounID[
+ this.nounDef.id
+ ] = {};
+ }
+ }
+
+ this.needsLoads = false;
+
+ GlodaDatastore._pendingAsyncStatements++;
+}
+
+QueryFromQueryCallback.prototype = {
+ handleResult(aResultSet) {
+ try {
+ // just outright bail if we are shutdown
+ if (GlodaDatastore.datastoreIsShutdown) {
+ return;
+ }
+
+ let pendingItems = this.collection.pendingItems;
+ let pendingIdMap = this.collection._pendingIdMap;
+ let row;
+ let nounDef = this.nounDef;
+ let nounID = nounDef.id;
+ while ((row = aResultSet.getNextRow())) {
+ let item = nounDef.objFromRow.call(nounDef.datastore, row);
+ if (this.collection.stashedColumns) {
+ let stashed = (this.collection.stashedColumns[item.id] = []);
+ for (let iCol of this.collection.query.options.stashColumns) {
+ stashed.push(GlodaDatastore._getVariant(row, iCol));
+ }
+ }
+ // try and replace the item with one from the cache, if we can
+ let cachedItem = GlodaCollectionManager.cacheLookupOne(
+ nounID,
+ item.id,
+ false
+ );
+
+ // if we already have a copy in the pending id map, skip it
+ if (item.id in pendingIdMap) {
+ continue;
+ }
+
+ // QFQ_LOG.debug("loading item " + nounDef.id + ":" + item.id + " existing: " +
+ // this.selfReferences[item.id] + " cached: " + cachedItem);
+ if (cachedItem) {
+ item = cachedItem;
+ } else if (this.selfReferences[item.id] != null) {
+ // We may already have been loaded by this process.
+ item = this.selfReferences[item.id];
+ } else {
+ // Perform loading logic which may produce reference dependencies.
+ this.needsLoads =
+ GlodaDatastore.loadNounItem(
+ item,
+ this.referencesByNounID,
+ this.inverseReferencesByNounID
+ ) || this.needsLoads;
+ }
+
+ // add ourself to the references by our id
+ // QFQ_LOG.debug("saving item " + nounDef.id + ":" + item.id + " to self-refs");
+ this.selfReferences[item.id] = item;
+
+ // if we're tracking it, add ourselves to our parent's list of children
+ // too
+ if (this.selfInverseReferences) {
+ let parentID = item[nounDef.parentColumnAttr.idStorageAttributeName];
+ let childrenList = this.selfInverseReferences[parentID];
+ if (childrenList === undefined) {
+ childrenList = this.selfInverseReferences[parentID] = [];
+ }
+ childrenList.push(item);
+ }
+
+ pendingItems.push(item);
+ pendingIdMap[item.id] = item;
+ }
+ } catch (e) {
+ GlodaDatastore._log.error("Exception in handleResult:", e);
+ }
+ },
+
+ handleError(aError) {
+ GlodaDatastore._log.error(
+ "Async queryFromQuery error: " + aError.result + ": " + aError.message
+ );
+ },
+
+ handleCompletion(aReason) {
+ try {
+ try {
+ this.statement.finalize();
+ this.statement = null;
+
+ // just outright bail if we are shutdown
+ if (GlodaDatastore.datastoreIsShutdown) {
+ return;
+ }
+
+ // QFQ_LOG.debug("handleCompletion: " + this.collection._nounDef.name);
+
+ if (this.needsLoads) {
+ for (let nounID in this.referencesByNounID) {
+ let references = this.referencesByNounID[nounID];
+ if (nounID == this.nounDef.id) {
+ continue;
+ }
+ let nounDef = GlodaDatastore._nounIDToDef[nounID];
+ // QFQ_LOG.debug(" have references for noun: " + nounDef.name);
+ // try and load them out of the cache/existing collections. items in the
+ // cache will be fully formed, which is nice for us.
+ // XXX this mechanism will get dubious when we have multiple paths to a
+ // single noun-type. For example, a -> b -> c, a-> c; two paths to c
+ // and we're looking at issuing two requests to c, the latter of which
+ // will be a superset of the first one. This does not currently pose
+ // a problem because we only have a -> b -> c -> b, and sequential
+ // processing means no alarms and no surprises.
+ let masterReferences = this.masterReferencesByNounID[nounID];
+ if (masterReferences === undefined) {
+ masterReferences = this.masterReferencesByNounID[nounID] = {};
+ }
+ let outReferences;
+ if (nounDef.parentColumnAttr) {
+ outReferences = {};
+ } else {
+ outReferences = masterReferences;
+ }
+ let [, notFoundCount, notFound] =
+ GlodaCollectionManager.cacheLookupMany(
+ nounDef.id,
+ references,
+ outReferences
+ );
+
+ if (nounDef.parentColumnAttr) {
+ let inverseReferences;
+ if (nounDef.id in this.masterInverseReferencesByNounID) {
+ inverseReferences =
+ this.masterInverseReferencesByNounID[nounDef.id];
+ } else {
+ inverseReferences = this.masterInverseReferencesByNounID[
+ nounDef.id
+ ] = {};
+ }
+
+ for (let key in outReferences) {
+ let item = outReferences[key];
+ masterReferences[item.id] = item;
+ let parentID =
+ item[nounDef.parentColumnAttr.idStorageAttributeName];
+ let childrenList = inverseReferences[parentID];
+ if (childrenList === undefined) {
+ childrenList = inverseReferences[parentID] = [];
+ }
+ childrenList.push(item);
+ }
+ }
+
+ // QFQ_LOG.debug(" found: " + foundCount + " not found: " + notFoundCount);
+ if (notFoundCount === 0) {
+ this.collection.resolvedCount++;
+ } else {
+ this.collection.deferredCount++;
+ let query = new nounDef.queryClass();
+ query.id.apply(query, Object.keys(notFound));
+
+ // we fully expect/allow for there being no such subcollection yet.
+ let subCollection =
+ nounDef.id in this.collection.masterCollection.subCollections
+ ? this.collection.masterCollection.subCollections[nounDef.id]
+ : undefined;
+ this.collection.masterCollection.subCollections[nounDef.id] =
+ GlodaDatastore.queryFromQuery(
+ query,
+ QueryFromQueryResolver,
+ this.collection,
+ subCollection,
+ this.collection.masterCollection,
+ { becomeExplicit: true }
+ );
+ }
+ }
+
+ for (let nounID in this.inverseReferencesByNounID) {
+ let inverseReferences = this.inverseReferencesByNounID[nounID];
+ this.collection.deferredCount++;
+ let nounDef = GlodaDatastore._nounIDToDef[nounID];
+
+ // QFQ_LOG.debug("Want to load inverse via " + nounDef.parentColumnAttr.boundName);
+
+ let query = new nounDef.queryClass();
+ // we want to constrain using the parent column
+ let queryConstrainer = query[nounDef.parentColumnAttr.boundName];
+ queryConstrainer.apply(query, Object.keys(inverseReferences));
+ // we fully expect/allow for there being no such subcollection yet.
+ let subCollection =
+ nounDef.id in this.collection.masterCollection.subCollections
+ ? this.collection.masterCollection.subCollections[nounDef.id]
+ : undefined;
+ this.collection.masterCollection.subCollections[nounDef.id] =
+ GlodaDatastore.queryFromQuery(
+ query,
+ QueryFromQueryResolver,
+ this.collection,
+ subCollection,
+ this.collection.masterCollection,
+ { becomeExplicit: true }
+ );
+ }
+ } else {
+ this.collection.deferredCount--;
+ this.collection.resolvedCount++;
+ }
+
+ // QFQ_LOG.debug(" defer: " + this.collection.deferredCount +
+ // " resolved: " + this.collection.resolvedCount);
+
+ // process immediately and kick-up to the master collection...
+ if (this.collection.deferredCount <= 0) {
+ // this guy will resolve everyone using referencesByNounID and issue the
+ // call to this.collection._onItemsAdded to propagate things to the
+ // next concerned subCollection or the actual listener if this is the
+ // master collection. (Also, call _onQueryCompleted).
+ QueryFromQueryResolver.onItemsAdded(
+ null,
+ { data: this.collection },
+ true
+ );
+ QueryFromQueryResolver.onQueryCompleted({ data: this.collection });
+ }
+ } catch (e) {
+ console.error(e);
+ QFQ_LOG.error("Exception:", e);
+ }
+ } finally {
+ GlodaDatastore._asyncCompleted();
+ }
+ },
+};
+
+/**
+ * Used by |GlodaDatastore.folderCompactionPassBlockFetch| to accumulate the
+ * results and pass them back in to the compaction process in
+ * |GlodaMsgIndexer._worker_folderCompactionPass|.
+ */
+function CompactionBlockFetcherHandler(aCallback) {
+ this.callback = aCallback;
+ this.idsAndMessageKeys = [];
+ GlodaDatastore._pendingAsyncStatements++;
+}
+CompactionBlockFetcherHandler.prototype = {
+ handleResult(aResultSet) {
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ this.idsAndMessageKeys.push([
+ row.getInt64(0), // id
+ row.getInt64(1), // messageKey
+ row.getString(2), // headerMessageID
+ ]);
+ }
+ },
+ handleError(aError) {
+ GlodaDatastore._log.error(
+ "CompactionBlockFetcherHandler error: " +
+ aError.result +
+ ": " +
+ aError.message
+ );
+ },
+ handleCompletion(aReason) {
+ GlodaDatastore._asyncCompleted();
+ this.callback(this.idsAndMessageKeys);
+ },
+};
+
+/**
+ * Use this as the callback handler when you have a SQL query that returns a
+ * single row with a single integer column value, like a COUNT() query.
+ */
+function SingletonResultValueHandler(aCallback) {
+ this.callback = aCallback;
+ this.result = null;
+ GlodaDatastore._pendingAsyncStatements++;
+}
+SingletonResultValueHandler.prototype = {
+ handleResult(aResultSet) {
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ this.result = row.getInt64(0);
+ }
+ },
+ handleError(aError) {
+ GlodaDatastore._log.error(
+ "SingletonResultValueHandler error: " +
+ aError.result +
+ ": " +
+ aError.message
+ );
+ },
+ handleCompletion(aReason) {
+ GlodaDatastore._asyncCompleted();
+ this.callback(this.result);
+ },
+};
+
+/**
+ * Wrapper that duplicates actions taken on a real statement to an explain
+ * statement. Currently only fires an explain statement once.
+ */
+function ExplainedStatementWrapper(
+ aRealStatement,
+ aExplainStatement,
+ aSQLString,
+ aExplainHandler
+) {
+ this.real = aRealStatement;
+ this.explain = aExplainStatement;
+ this.sqlString = aSQLString;
+ this.explainHandler = aExplainHandler;
+ this.done = false;
+}
+ExplainedStatementWrapper.prototype = {
+ bindByIndex(aColIndex, aValue) {
+ this.real.bindByIndex(aColIndex, aValue);
+ if (!this.done) {
+ this.explain.bindByIndex(aColIndex, aValue);
+ }
+ },
+ executeAsync(aCallback) {
+ if (!this.done) {
+ this.explainHandler.sqlEnRoute(this.sqlString);
+ this.explain.executeAsync(this.explainHandler);
+ this.explain.finalize();
+ this.done = true;
+ }
+ return this.real.executeAsync(aCallback);
+ },
+ finalize() {
+ if (!this.done) {
+ this.explain.finalize();
+ }
+ this.real.finalize();
+ },
+};
+
+/**
+ * Writes a single JSON document to the provide file path in a streaming
+ * fashion. At startup we open an array to place the queries in and at
+ * shutdown we close it.
+ */
+function ExplainedStatementProcessor(aDumpPath) {
+ Services.obs.addObserver(this, "quit-application");
+
+ this._sqlStack = [];
+ this._curOps = [];
+ this._objsWritten = 0;
+
+ let filePath = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ filePath.initWithPath(aDumpPath);
+
+ this._ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ this._ostream.init(filePath, -1, -1, 0);
+
+ let s = '{"queries": [';
+ this._ostream.write(s, s.length);
+}
+ExplainedStatementProcessor.prototype = {
+ sqlEnRoute(aSQLString) {
+ this._sqlStack.push(aSQLString);
+ },
+ handleResult(aResultSet) {
+ let row;
+ // addr opcode (s) p1 p2 p3 p4 (s) p5 comment (s)
+ while ((row = aResultSet.getNextRow())) {
+ this._curOps.push([
+ row.getInt64(0), // addr
+ row.getString(1), // opcode
+ row.getInt64(2), // p1
+ row.getInt64(3), // p2
+ row.getInt64(4), // p3
+ row.getString(5), // p4
+ row.getString(6), // p5
+ row.getString(7), // comment
+ ]);
+ }
+ },
+ handleError(aError) {
+ console.error("Unexpected error in EXPLAIN handler: " + aError);
+ },
+ handleCompletion(aReason) {
+ let obj = {
+ sql: this._sqlStack.shift(),
+ operations: this._curOps,
+ };
+ let s = (this._objsWritten++ ? ", " : "") + JSON.stringify(obj, null, 2);
+ this._ostream.write(s, s.length);
+
+ this._curOps = [];
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this.shutdown();
+ }
+ },
+
+ shutdown() {
+ let s = "]}";
+ this._ostream.write(s, s.length);
+ this._ostream.close();
+
+ Services.obs.removeObserver(this, "quit-application");
+ },
+};
+
+// See the documentation on GlodaDatastore._schemaVersion to understand these:
+var DB_SCHEMA_ACCEPT_LEAVE_LOW = 31,
+ DB_SCHEMA_ACCEPT_LEAVE_HIGH = 34,
+ DB_SCHEMA_ACCEPT_DOWNGRADE_LOW = 35,
+ DB_SCHEMA_ACCEPT_DOWNGRADE_HIGH = 39,
+ DB_SCHEMA_DOWNGRADE_DELTA = 5;
+
+/**
+ * Database abstraction layer. Contains explicit SQL schemas for our
+ * fundamental representations (core 'nouns', if you will) as well as
+ * specialized functions for then dealing with each type of object. At the
+ * same time, we are beginning to support extension-provided tables, which
+ * call into question whether we really need our hand-rolled code, or could
+ * simply improve the extension-provided table case to work for most of our
+ * hand-rolled cases.
+ * For now, the argument can probably be made that our explicit schemas and code
+ * is readable/intuitive (not magic) and efficient (although generic stuff
+ * could also be made efficient, if slightly evil through use of eval or some
+ * other code generation mechanism.)
+ *
+ * === Data Model Interaction / Dependencies
+ *
+ * Dependent on and assumes limited knowledge of the GlodaDataModel.jsm
+ * implementations. GlodaDataModel.jsm actually has an implicit dependency on
+ * our implementation, reaching back into the datastore via the _datastore
+ * attribute which we pass into every instance we create.
+ * We pass a reference to ourself as we create the GlodaDataModel.jsm instances (and
+ * they store it as _datastore) because of a half-implemented attempt to make
+ * it possible to live in a world where we have multiple datastores. This
+ * would be desirable in the cases where we are dealing with multiple SQLite
+ * databases. This could be because of per-account global databases or
+ * some other segmentation. This was abandoned when the importance of
+ * per-account databases was diminished following public discussion, at least
+ * for the short-term, but no attempted was made to excise the feature or
+ * preclude it. (Merely a recognition that it's too much to try and implement
+ * correct right now, especially because our solution might just be another
+ * (aggregating) layer on top of things, rather than complicating the lower
+ * levels.)
+ *
+ * === Object Identity / Caching
+ *
+ * The issue of object identity is handled by integration with the Collection.jsm
+ * provided GlodaCollectionManager. By "Object Identity", I mean that we only
+ * should ever have one object instance alive at a time that corresponds to
+ * an underlying database row in the database. Where possible we avoid
+ * performing database look-ups when we can check if the object is already
+ * present in memory; in practice, this means when we are asking for an object
+ * by ID. When we cannot avoid a database query, we attempt to make sure that
+ * we do not return a duplicate object instance, instead replacing it with the
+ * 'live' copy of the object. (Ideally, we would avoid any redundant
+ * construction costs, but that is not currently the case.)
+ * Although you should consult the GlodaCollectionManager for details, the
+ * general idea is that we have 'collections' which represent views of the
+ * database (based on a query) which use a single mechanism for double duty.
+ * The collections are registered with the collection manager via weak
+ * reference. The first 'duty' is that since the collections may be desired
+ * to be 'live views' of the data, we want them to update as changes occur.
+ * The weak reference allows the collection manager to track the 'live'
+ * collections and update them. The second 'duty' is the caching/object
+ * identity duty. In theory, every live item should be referenced by at least
+ * one collection, making it reachable for object identity/caching purposes.
+ * There is also an explicit (inclusive) caching layer present to both try and
+ * avoid poor performance from some of the costs of this strategy, as well as
+ * to try and keep track of objects that are being worked with that are not
+ * (yet) tracked by a collection. Using a size-bounded cache is clearly not
+ * a guarantee of correctness for this, but is suspected will work quite well.
+ * (Well enough to be dangerous because the inevitable failure case will not be
+ * expected.)
+ *
+ * The current strategy may not be the optimal one, feel free to propose and/or
+ * implement better ones, especially if you have numbers.
+ * The current strategy is not fully implemented in this file, but the common
+ * cases are believed to be covered. (Namely, we fail to purge items from the
+ * cache as they are purged from the database.)
+ *
+ * === Things That May Not Be Obvious (Gotchas)
+ *
+ * Although the schema includes "triggers", they are currently not used
+ * and were added when thinking about implementing the feature. We will
+ * probably implement this feature at some point, which is why they are still
+ * in there.
+ *
+ * We, and the layers above us, are not sufficiently thorough at cleaning out
+ * data from the database, and may potentially orphan it _as new functionality
+ * is added in the future at layers above us_. That is, currently we should
+ * not be leaking database rows, but we may in the future. This is because
+ * we/the layers above us lack a mechanism to track dependencies based on
+ * attributes. Say a plugin exists that extracts recipes from messages and
+ * relates them via an attribute. To do so, it must create new recipe rows
+ * in its own table as new recipes are discovered. No automatic mechanism
+ * will purge recipes as their source messages are purged, nor does any
+ * event-driven mechanism explicitly inform the plugin. (It could infer
+ * such an event from the indexing/attribute-providing process, or poll the
+ * states of attributes to accomplish this, but that is not desirable.) This
+ * needs to be addressed, and may be best addressed at layers above
+ * GlodaDatastore.jsm.
+ *
+ * @namespace
+ */
+var GlodaDatastore = {
+ _log: null,
+
+ /* ******************* SCHEMA ******************* */
+
+ /**
+ * Schema version policy. IMPORTANT! We expect the following potential things
+ * to happen in the life of gloda that can impact our schema and the ability
+ * to move between different versions of Thunderbird:
+ *
+ * - Fundamental changes to the schema so that two versions of Thunderbird
+ * cannot use the same global database. To wit, Thunderbird N+1 needs to
+ * blow away the database of Thunderbird N and reindex from scratch.
+ * Likewise, Thunderbird N will need to blow away Thunderbird N+1's
+ * database because it can't understand it. And we can't simply use a
+ * different file because there would be fatal bookkeeping losses.
+ *
+ * - Bidirectional minor schema changes (rare).
+ * Thunderbird N+1 does something that does not affect Thunderbird N's use
+ * of the database, and a user switching back to Thunderbird N will not be
+ * negatively impacted. It will also be fine when they go back to N+1 and
+ * N+1 will not be missing any vital data. The historic example of this is
+ * when we added a missing index that was important for performance. In
+ * that case, Thunderbird N could have potentially left the schema revision
+ * intact (if there was a safe revision), rather than swapping it on the
+ * downgrade, compelling N+1 to redo the transform on upgrade.
+ *
+ * - Backwards compatible, upgrade-transition minor schema changes.
+ * Thunderbird N+1 does something that does not require nuking the
+ * database / a full re-index, but does require processing on upgrade from
+ * a version of the database previously used by Thunderbird. These changes
+ * do not impact N's ability to use the database. For example, adding a
+ * new indexed attribute that affects a small number of messages could be
+ * handled by issuing a query on upgrade to dirty/index those messages.
+ * However, if the user goes back to N from N+1, when they upgrade to N+1
+ * again, we need to re-index. In this case N would need to have downgrade
+ * the schema revision.
+ *
+ * - Backwards incompatible, minor schema changes.
+ * Thunderbird N+1 does something that does not require nuking the database
+ * but will break Thunderbird N's ability to use the database.
+ *
+ * - Regression fixes. Sometimes we may land something that screws up
+ * databases, or the platform changes in a way that breaks our code and we
+ * had insufficient unit test coverage and so don't detect it until some
+ * databases have gotten messed up.
+ *
+ * Accordingly, every version of Thunderbird has a concept of potential schema
+ * versions with associated semantics to prepare for the minor schema upgrade
+ * cases were inter-op is possible. These ranges and their semantics are:
+ * - accepts and leaves intact. Covers:
+ * - regression fixes that no longer exist with the landing of the upgrade
+ * code as long as users never go back a build in the given channel.
+ * - bidirectional minor schema changes.
+ * - accepts but downgrades version to self. Covers:
+ * - backwards compatible, upgrade-transition minor schema changes.
+ * - nuke range (anything beyond a specific revision needs to be nuked):
+ * - backwards incompatible, minor scheme changes
+ * - fundamental changes
+ *
+ *
+ * SO, YOU WANT TO CHANGE THE SCHEMA?
+ *
+ * Use the ranges below for Thunderbird 11 as a guide, bumping things as little
+ * as possible. If we start to use up the "accepts and leaves intact" range
+ * without majorly changing things up, re-do the numbering acceptance range
+ * to give us additional runway.
+ *
+ * Also, if we keep needing non-nuking upgrades, consider adding an additional
+ * table to the database that can tell older versions of Thunderbird what to
+ * do when confronted with a newer database and where it can set flags to tell
+ * the newer Thunderbird what the older Thunderbird got up to. For example,
+ * it would be much easier if we just tell Thunderbird N what to do when it's
+ * confronted with the database.
+ *
+ *
+ * CURRENT STATE OF THE MIGRATION LOGIC:
+ *
+ * Thunderbird 11: uses 30 (regression fix from 26)
+ * - accepts and leaves intact: 31-34
+ * - accepts and downgrades by 5: 35-39
+ * - nukes: 40+
+ */
+ _schemaVersion: 30,
+ // what is the schema in the database right now?
+ _actualSchemaVersion: 0,
+ _schema: {
+ tables: {
+ // ----- Messages
+ folderLocations: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["folderURI", "TEXT NOT NULL"],
+ ["dirtyStatus", "INTEGER NOT NULL"],
+ ["name", "TEXT NOT NULL"],
+ ["indexingPriority", "INTEGER NOT NULL"],
+ ],
+
+ triggers: {
+ delete: "DELETE from messages WHERE folderID = OLD.id",
+ },
+ },
+
+ conversations: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["subject", "TEXT NOT NULL"],
+ ["oldestMessageDate", "INTEGER"],
+ ["newestMessageDate", "INTEGER"],
+ ],
+
+ indices: {
+ subject: ["subject"],
+ oldestMessageDate: ["oldestMessageDate"],
+ newestMessageDate: ["newestMessageDate"],
+ },
+
+ fulltextColumns: [["subject", "TEXT"]],
+
+ triggers: {
+ delete: "DELETE from messages WHERE conversationID = OLD.id",
+ },
+ },
+
+ /**
+ * A message record correspond to an actual message stored in a folder
+ * somewhere, or is a ghost record indicating a message that we know
+ * should exist, but which we have not seen (and which we may never see).
+ * We represent these ghost messages by storing NULL values in the
+ * folderID and messageKey fields; this may need to change to other
+ * sentinel values if this somehow impacts performance.
+ */
+ messages: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["folderID", "INTEGER"],
+ ["messageKey", "INTEGER"],
+ // conversationID used to have a REFERENCES but I'm losing it for
+ // presumed performance reasons and it doesn't do anything for us.
+ ["conversationID", "INTEGER NOT NULL"],
+ ["date", "INTEGER"],
+ // we used to have the parentID, but because of the very real
+ // possibility of multiple copies of a message with a given
+ // message-id, the parentID concept is unreliable.
+ ["headerMessageID", "TEXT"],
+ ["deleted", "INTEGER NOT NULL default 0"],
+ ["jsonAttributes", "TEXT"],
+ // Notability attempts to capture the static 'interestingness' of a
+ // message as a result of being starred/flagged, labeled, read
+ // multiple times, authored by someone in your address book or that
+ // you converse with a lot, etc.
+ ["notability", "INTEGER NOT NULL default 0"],
+ ],
+
+ indices: {
+ messageLocation: ["folderID", "messageKey"],
+ headerMessageID: ["headerMessageID"],
+ conversationID: ["conversationID"],
+ date: ["date"],
+ deleted: ["deleted"],
+ },
+
+ // note: if reordering the columns, you need to change this file's
+ // row-loading logic, GlodaMsgSearcher.jsm's ranking usages and also the
+ // column saturations in nsGlodaRankerFunction
+ fulltextColumns: [
+ ["body", "TEXT"],
+ ["subject", "TEXT"],
+ ["attachmentNames", "TEXT"],
+ ["author", "TEXT"],
+ ["recipients", "TEXT"],
+ ],
+
+ triggers: {
+ delete: "DELETE FROM messageAttributes WHERE messageID = OLD.id",
+ },
+ },
+
+ // ----- Attributes
+ attributeDefinitions: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["attributeType", "INTEGER NOT NULL"],
+ ["extensionName", "TEXT NOT NULL"],
+ ["name", "TEXT NOT NULL"],
+ ["parameter", "BLOB"],
+ ],
+
+ triggers: {
+ delete: "DELETE FROM messageAttributes WHERE attributeID = OLD.id",
+ },
+ },
+
+ messageAttributes: {
+ columns: [
+ // conversationID and messageID used to have REFERENCES back to their
+ // appropriate types. I removed it when removing attributeID for
+ // better reasons and because the code is not capable of violating
+ // this constraint, so the check is just added cost. (And we have
+ // unit tests that sanity check my assertions.)
+ ["conversationID", "INTEGER NOT NULL"],
+ ["messageID", "INTEGER NOT NULL"],
+ // This used to be REFERENCES attributeDefinitions(id) but then we
+ // introduced sentinel values and it's hard to justify the effort
+ // to compel injection of the record or the overhead to do the
+ // references checking.
+ ["attributeID", "INTEGER NOT NULL"],
+ ["value", "NUMERIC"],
+ ],
+
+ indices: {
+ attribQuery: [
+ "attributeID",
+ "value",
+ /* covering: */ "conversationID",
+ "messageID",
+ ],
+ // This is required for deletion of a message's attributes to be
+ // performant. We could optimize this index away if we changed our
+ // deletion logic to issue specific attribute deletions based on the
+ // information it already has available in the message's JSON blob.
+ // The rub there is that if we screwed up we could end up leaking
+ // attributes and there is a non-trivial performance overhead to
+ // the many requests it would cause (which can also be reduced in
+ // the future by changing our SQL dispatch code.)
+ messageAttribFastDeletion: ["messageID"],
+ },
+ },
+
+ // ----- Contacts / Identities
+
+ /**
+ * Corresponds to a human being and roughly to an address book entry.
+ * Contrast with an identity, which is a specific e-mail address, IRC
+ * nick, etc. Identities belong to contacts, and this relationship is
+ * expressed on the identityAttributes table.
+ */
+ contacts: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["directoryUUID", "TEXT"],
+ ["contactUUID", "TEXT"],
+ ["popularity", "INTEGER"],
+ ["frecency", "INTEGER"],
+ ["name", "TEXT"],
+ ["jsonAttributes", "TEXT"],
+ ],
+ indices: {
+ popularity: ["popularity"],
+ frecency: ["frecency"],
+ },
+ },
+
+ contactAttributes: {
+ columns: [
+ ["contactID", "INTEGER NOT NULL"],
+ ["attributeID", "INTEGER NOT NULL"],
+ ["value", "NUMERIC"],
+ ],
+ indices: {
+ contactAttribQuery: [
+ "attributeID",
+ "value",
+ /* covering: */ "contactID",
+ ],
+ },
+ },
+
+ /**
+ * Identities correspond to specific e-mail addresses, IRC nicks, etc.
+ */
+ identities: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["contactID", "INTEGER NOT NULL"],
+ ["kind", "TEXT NOT NULL"], // ex: email, irc, etc.
+ ["value", "TEXT NOT NULL"], // ex: e-mail address, irc nick/handle...
+ ["description", "NOT NULL"], // what makes this identity different
+ // from the others? (ex: home, work, etc.)
+ ["relay", "INTEGER NOT NULL"], // is the identity just a relay
+ // mechanism? (ex: mailing list, twitter 'bouncer', IRC gateway, etc.)
+ ],
+
+ indices: {
+ contactQuery: ["contactID"],
+ valueQuery: ["kind", "value"],
+ },
+ },
+ },
+ },
+
+ /* ******************* LOGIC ******************* */
+ /**
+ * We only have one connection; this name exists for legacy reasons but helps
+ * track when we are intentionally doing synchronous things during startup.
+ * We do nothing synchronous once our setup has completed.
+ */
+ syncConnection: null,
+ /**
+ * We only have one connection and we only do asynchronous things after setup;
+ * this name still exists mainly for legacy reasons.
+ */
+ asyncConnection: null,
+
+ /**
+ * Our "mailnews.database.global.datastore." preferences branch for debug
+ * notification handling. We register as an observer against this.
+ */
+ _prefBranch: null,
+
+ /**
+ * The unique ID assigned to an index when it has been built. This value
+ * changes once the index has been rebuilt.
+ */
+ _datastoreID: null,
+
+ /**
+ * Initialize logging, create the database if it doesn't exist, "upgrade" it
+ * if it does and it's not up-to-date, fill our authoritative folder uri/id
+ * mapping.
+ */
+ _init(aNounIDToDef) {
+ this._log = console.createInstance({
+ prefix: "gloda.datastore",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+ this._log.debug("Beginning datastore initialization.");
+
+ this._nounIDToDef = aNounIDToDef;
+
+ let branch = Services.prefs.getBranch(
+ "mailnews.database.global.datastore."
+ );
+ this._prefBranch = branch;
+
+ // Not sure the weak reference really makes a difference given that we are a
+ // GC root.
+ branch.addObserver("", this);
+ // claim the pref changed so we can centralize our logic there.
+ this.observe(null, "nsPref:changed", "explainToPath");
+
+ // Get the path to our global database
+ var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("global-messages-db.sqlite");
+
+ var dbConnection;
+
+ // Report about the size of the database through telemetry (if there's a
+ // database, naturally).
+ if (dbFile.exists()) {
+ try {
+ let h = Services.telemetry.getHistogramById(
+ "THUNDERBIRD_GLODA_SIZE_MB"
+ );
+ h.add(dbFile.fileSize / 1048576);
+ } catch (e) {
+ this._log.warn("Couldn't report telemetry", e);
+ }
+ }
+
+ // Create the file if it does not exist
+ if (!dbFile.exists()) {
+ this._log.debug("Creating database because it doesn't exist.");
+ dbConnection = this._createDB(dbFile);
+ } else {
+ // It does exist, but we (someday) might need to upgrade the schema
+ // (Exceptions may be thrown if the database is corrupt)
+ try {
+ dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+ let cacheSize = this._determineCachePages(dbConnection);
+ // see _createDB...
+ dbConnection.executeSimpleSQL("PRAGMA cache_size = " + cacheSize);
+ dbConnection.executeSimpleSQL("PRAGMA synchronous = FULL");
+
+ // Register custom tokenizer to index all language text
+ var tokenizer = Cc["@mozilla.org/messenger/fts3tokenizer;1"].getService(
+ Ci.nsIFts3Tokenizer
+ );
+ tokenizer.registerTokenizer(dbConnection);
+
+ // -- database schema changes
+ let dbSchemaVersion = (this._actualSchemaVersion =
+ dbConnection.schemaVersion);
+ // - database from the future!
+ if (dbSchemaVersion > this._schemaVersion) {
+ if (
+ dbSchemaVersion >= DB_SCHEMA_ACCEPT_LEAVE_LOW &&
+ dbSchemaVersion <= DB_SCHEMA_ACCEPT_LEAVE_HIGH
+ ) {
+ this._log.debug(
+ "db from the future in acceptable range; leaving " +
+ "version at: " +
+ dbSchemaVersion
+ );
+ } else if (
+ dbSchemaVersion >= DB_SCHEMA_ACCEPT_DOWNGRADE_LOW &&
+ dbSchemaVersion <= DB_SCHEMA_ACCEPT_DOWNGRADE_HIGH
+ ) {
+ let newVersion = dbSchemaVersion - DB_SCHEMA_DOWNGRADE_DELTA;
+ this._log.debug(
+ "db from the future in downgrade range; setting " +
+ "version to " +
+ newVersion +
+ " down from " +
+ dbSchemaVersion
+ );
+ dbConnection.schemaVersion = this._actualSchemaVersion = newVersion;
+ } else {
+ // too far from the future, nuke it.
+ dbConnection = this._nukeMigration(dbFile, dbConnection);
+ }
+ } else if (dbSchemaVersion < this._schemaVersion) {
+ // - database from the past! migrate it, possibly.
+ this._log.debug(
+ "Need to migrate database. (DB version: " +
+ this._actualSchemaVersion +
+ " desired version: " +
+ this._schemaVersion
+ );
+ dbConnection = this._migrate(
+ dbFile,
+ dbConnection,
+ this._actualSchemaVersion,
+ this._schemaVersion
+ );
+ this._log.debug("Migration call completed.");
+ }
+ // else: this database is juuust right.
+
+ // If we never had a datastore ID, make sure to create one now.
+ if (!this._prefBranch.prefHasUserValue("id")) {
+ this._datastoreID = this._generateDatastoreID();
+ this._prefBranch.setCharPref("id", this._datastoreID);
+ } else {
+ this._datastoreID = this._prefBranch.getCharPref("id");
+ }
+ } catch (ex) {
+ // Handle corrupt databases, other oddities
+ if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ this._log.warn("Database was corrupt, removing the old one.");
+ dbFile.remove(false);
+ this._log.warn("Removed old database, creating a new one.");
+ dbConnection = this._createDB(dbFile);
+ } else {
+ this._log.error(
+ "Unexpected error when trying to open the database:",
+ ex
+ );
+ throw ex;
+ }
+ }
+ }
+
+ this.syncConnection = dbConnection;
+ this.asyncConnection = dbConnection;
+
+ this._log.debug("Initializing folder mappings.");
+ this._getAllFolderMappings();
+ // we need to figure out the next id's for all of the tables where we
+ // manage that.
+ this._log.debug("Populating managed id counters.");
+ this._populateAttributeDefManagedId();
+ this._populateConversationManagedId();
+ this._populateMessageManagedId();
+ this._populateContactManagedId();
+ this._populateIdentityManagedId();
+
+ this._log.debug("Completed datastore initialization.");
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ if (aData == "explainToPath") {
+ let explainToPath = null;
+ try {
+ explainToPath = this._prefBranch.getCharPref("explainToPath");
+ if (explainToPath.trim() == "") {
+ explainToPath = null;
+ }
+ } catch (ex) {
+ // don't care if the pref is not there.
+ }
+
+ // It is conceivable that the name is changing and this isn't a boolean
+ // toggle, so always clean out the explain processor.
+ if (this._explainProcessor) {
+ this._explainProcessor.shutdown();
+ this._explainProcessor = null;
+ }
+
+ if (explainToPath) {
+ this._createAsyncStatement = this._createExplainedAsyncStatement;
+ this._explainProcessor = new ExplainedStatementProcessor(explainToPath);
+ } else {
+ this._createAsyncStatement = this._realCreateAsyncStatement;
+ }
+ }
+ },
+
+ datastoreIsShutdown: false,
+
+ /**
+ * Perform datastore shutdown.
+ */
+ shutdown() {
+ // Clear out any pending transaction by committing it.
+ // The indexer has been shutdown by this point; it no longer has any active
+ // indexing logic and it no longer has active event listeners capable of
+ // generating new activity.
+ // Semantic consistency of the database is guaranteed by the indexer's
+ // strategy of only yielding control at coherent times. Although it takes
+ // multiple calls and multiple SQL operations to update the state of our
+ // database representations, the generator does not yield until it has
+ // issued all the database statements required for said update. As such,
+ // this commit will leave us in a good way (and the commit will happen
+ // because closing the connection will drain the async execution queue.)
+ while (this._transactionDepth) {
+ this._log.info("Closing pending transaction out for shutdown.");
+ // just schedule this function to be run again once the transaction has
+ // been closed out.
+ this._commitTransaction();
+ }
+
+ this.datastoreIsShutdown = true;
+
+ this._log.info("Closing db connection");
+
+ // we do not expect exceptions, but it's a good idea to avoid having our
+ // shutdown process explode.
+ try {
+ this._cleanupAsyncStatements();
+ this._cleanupSyncStatements();
+ } catch (ex) {
+ this._log.debug("Unexpected exception during statement cleanup: " + ex);
+ }
+
+ // it's conceivable we might get a spurious exception here, but we really
+ // shouldn't get one. again, we want to ensure shutdown runs to completion
+ // and doesn't break our caller.
+ try {
+ // This currently causes all pending asynchronous operations to be run to
+ // completion. this simplifies things from a correctness perspective,
+ // and, honestly, is a lot easier than us tracking all of the async
+ // event tasks so that we can explicitly cancel them.
+ // This is a reasonable thing to do because we don't actually ever have
+ // a huge number of statements outstanding. The indexing process needs
+ // to issue async requests periodically, so the most we have in-flight
+ // from a write perspective is strictly less than the work required to
+ // update the database state for a single message.
+ // However, the potential for multiple pending expensive queries does
+ // exist, and it may be advisable to attempt to track and cancel those.
+ // For simplicity we don't currently do this, and I expect this should
+ // not pose a major problem, but those are famous last words.
+ // Note: asyncClose does not spin a nested event loop, but the thread
+ // manager shutdown code will spin the async thread's event loop, so it
+ // nets out to be the same.
+ this.asyncConnection.asyncClose();
+ } catch (ex) {
+ this._log.debug(
+ "Potentially expected exception during connection closure: " + ex
+ );
+ }
+
+ this.asyncConnection = null;
+ this.syncConnection = null;
+ },
+
+ /**
+ * Generates and returns a UUID.
+ *
+ * @returns a UUID as a string, ex: "c4dd0159-9287-480f-a648-a4613e147fdb"
+ */
+ _generateDatastoreID() {
+ let uuid = Services.uuid.generateUUID().toString();
+ // We snip off the { and } from each end of the UUID.
+ return uuid.substring(1, uuid.length - 2);
+ },
+
+ _determineCachePages(aDBConn) {
+ try {
+ // For the details of the computations, one should read
+ // nsNavHistory::InitDB. We're slightly diverging from them in the sense
+ // that we won't allow gloda to use insane amounts of memory cache, and
+ // we start with 1% instead of 6% like them.
+ let pageStmt = aDBConn.createStatement("PRAGMA page_size");
+ pageStmt.executeStep();
+ let pageSize = pageStmt.row.page_size;
+ pageStmt.finalize();
+ let cachePermillage = this._prefBranch.getIntPref(
+ "cache_to_memory_permillage"
+ );
+ cachePermillage = Math.min(cachePermillage, 50);
+ cachePermillage = Math.max(cachePermillage, 0);
+ let physMem = Services.sysinfo.getPropertyAsInt64("memsize");
+ if (physMem == 0) {
+ physMem = MEMSIZE_FALLBACK_BYTES;
+ }
+ let cacheSize = Math.round((physMem * cachePermillage) / 1000);
+ cacheSize = Math.max(cacheSize, MIN_CACHE_SIZE);
+ cacheSize = Math.min(cacheSize, MAX_CACHE_SIZE);
+ let cachePages = Math.round(cacheSize / pageSize);
+ return cachePages;
+ } catch (ex) {
+ this._log.warn("Error determining cache size: " + ex);
+ // A little bit lower than on my personal machine, will result in ~40M.
+ return 1000;
+ }
+ },
+
+ /**
+ * Create our database; basically a wrapper around _createSchema.
+ */
+ _createDB(aDBFile) {
+ var dbConnection = Services.storage.openUnsharedDatabase(aDBFile);
+ // We now follow the Firefox strategy for places, which mainly consists in
+ // picking a default 32k page size, and then figuring out the amount of
+ // cache accordingly. The default 32k come from mozilla/toolkit/storage,
+ // but let's get it directly from sqlite in case they change it.
+ let cachePages = this._determineCachePages(dbConnection);
+ // This is a maximum number of pages to be used. If the database does not
+ // get this large, then the memory does not get used.
+ // Do not forget to update the code in _init if you change this value.
+ dbConnection.executeSimpleSQL("PRAGMA cache_size = " + cachePages);
+ // The mozStorage default is NORMAL which shaves off some fsyncs in the
+ // interest of performance. Since everything we do after bootstrap is
+ // async, we do not care about the performance, but we really want the
+ // correctness. Bug reports and support avenues indicate a non-zero number
+ // of corrupt databases. Note that this may not fix everything; OS X
+ // also supports an F_FULLSYNC flag enabled by PRAGMA fullfsync that we are
+ // not enabling that is much more comprehensive. We can think about
+ // turning that on after we've seen how this reduces our corruption count.
+ dbConnection.executeSimpleSQL("PRAGMA synchronous = FULL");
+ // Register custom tokenizer to index all language text
+ var tokenizer = Cc["@mozilla.org/messenger/fts3tokenizer;1"].getService(
+ Ci.nsIFts3Tokenizer
+ );
+ tokenizer.registerTokenizer(dbConnection);
+
+ // We're creating a new database, so let's generate a new ID for this
+ // version of the datastore. This way, indexers can know when the index
+ // has been rebuilt in the event that they need to rebuild dependent data.
+ this._datastoreID = this._generateDatastoreID();
+ this._prefBranch.setCharPref("id", this._datastoreID);
+
+ dbConnection.beginTransaction();
+ try {
+ this._createSchema(dbConnection);
+ dbConnection.commitTransaction();
+ } catch (ex) {
+ dbConnection.rollbackTransaction();
+ throw ex;
+ }
+
+ return dbConnection;
+ },
+
+ _createTableSchema(aDBConnection, aTableName, aTableDef) {
+ // - Create the table
+ this._log.info("Creating table: " + aTableName);
+ let columnDefs = [];
+ for (let [column, type] of aTableDef.columns) {
+ columnDefs.push(column + " " + type);
+ }
+ aDBConnection.createTable(aTableName, columnDefs.join(", "));
+
+ // - Create the fulltext table if applicable
+ if (aTableDef.fulltextColumns) {
+ let columnDefs = [];
+ for (let [column, type] of aTableDef.fulltextColumns) {
+ columnDefs.push(column + " " + type);
+ }
+ let createFulltextSQL =
+ "CREATE VIRTUAL TABLE " +
+ aTableName +
+ "Text" +
+ " USING fts3(tokenize mozporter, " +
+ columnDefs.join(", ") +
+ ")";
+ this._log.info("Creating fulltext table: " + createFulltextSQL);
+ aDBConnection.executeSimpleSQL(createFulltextSQL);
+ }
+
+ // - Create its indices
+ if (aTableDef.indices) {
+ for (let indexName in aTableDef.indices) {
+ let indexColumns = aTableDef.indices[indexName];
+ aDBConnection.executeSimpleSQL(
+ "CREATE INDEX " +
+ indexName +
+ " ON " +
+ aTableName +
+ "(" +
+ indexColumns.join(", ") +
+ ")"
+ );
+ }
+ }
+
+ // - Create the attributes table if applicable
+ if (aTableDef.genericAttributes) {
+ aTableDef.genericAttributes = {
+ columns: [
+ ["nounID", "INTEGER NOT NULL"],
+ ["attributeID", "INTEGER NOT NULL"],
+ ["value", "NUMERIC"],
+ ],
+ indices: {},
+ };
+ aTableDef.genericAttributes.indices[aTableName + "AttribQuery"] = [
+ "attributeID",
+ "value",
+ /* covering: */ "nounID",
+ ];
+ // let's use this very function! (since we created genericAttributes,
+ // explodey recursion is avoided.)
+ this._createTableSchema(
+ aDBConnection,
+ aTableName + "Attributes",
+ aTableDef.genericAttributes
+ );
+ }
+ },
+
+ /**
+ * Create our database schema assuming a newly created database. This
+ * comes down to creating normal tables, their full-text variants (if
+ * applicable), and their indices.
+ */
+ _createSchema(aDBConnection) {
+ // -- For each table...
+ for (let tableName in this._schema.tables) {
+ let tableDef = this._schema.tables[tableName];
+ this._createTableSchema(aDBConnection, tableName, tableDef);
+ }
+
+ aDBConnection.schemaVersion = this._actualSchemaVersion =
+ this._schemaVersion;
+ },
+
+ /**
+ * Create a table for a noun, replete with data binding.
+ */
+ createNounTable(aNounDef) {
+ // give it a _jsonText attribute if appropriate...
+ if (aNounDef.allowsArbitraryAttrs) {
+ aNounDef.schema.columns.push(["jsonAttributes", "STRING", "_jsonText"]);
+ }
+ // check if the table exists
+ if (!this.asyncConnection.tableExists(aNounDef.tableName)) {
+ // it doesn't! create it (and its potentially many variants)
+ try {
+ this._createTableSchema(
+ this.asyncConnection,
+ aNounDef.tableName,
+ aNounDef.schema
+ );
+ } catch (ex) {
+ this._log.error(
+ "Problem creating table " +
+ aNounDef.tableName +
+ " " +
+ "because: " +
+ ex +
+ " at " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber
+ );
+ return;
+ }
+ }
+
+ aNounDef._dataBinder = new GlodaDatabind(aNounDef, this);
+ aNounDef.datastore = aNounDef._dataBinder;
+ aNounDef.objFromRow = aNounDef._dataBinder.objFromRow;
+ aNounDef.objInsert = aNounDef._dataBinder.objInsert;
+ aNounDef.objUpdate = aNounDef._dataBinder.objUpdate;
+ aNounDef.dbAttribAdjuster = aNounDef._dataBinder.adjustAttributes;
+
+ if (aNounDef.schema.genericAttributes) {
+ aNounDef.attrTableName = aNounDef.tableName + "Attributes";
+ aNounDef.attrIDColumnName = "nounID";
+ }
+ },
+
+ _nukeMigration(aDBFile, aDBConnection) {
+ aDBConnection.close();
+ aDBFile.remove(false);
+ this._log.warn(
+ "Global database has been purged due to schema change. " +
+ "old version was " +
+ this._actualSchemaVersion +
+ ", new version is: " +
+ this._schemaVersion
+ );
+ return this._createDB(aDBFile);
+ },
+
+ /**
+ * Migrate the database _to the latest version_ from an older version. We
+ * only keep enough logic around to get us to the recent version. This code
+ * is not a time machine! If we need to blow away the database to get to the
+ * most recent version, then that's the sum total of the migration!
+ */
+ _migrate(aDBFile, aDBConnection, aCurVersion, aNewVersion) {
+ // version 12:
+ // - notability column added
+ // version 13:
+ // - we are adding a new fulltext index column. blow away!
+ // - note that I screwed up and failed to mark the schema change; apparently
+ // no database will claim to be version 13...
+ // version 14ish, still labeled 13?:
+ // - new attributes: forwarded, repliedTo, bcc, recipients
+ // - altered fromMeTo and fromMeCc to fromMe
+ // - altered toMe and ccMe to just be toMe
+ // - exposes bcc to cc-related attributes
+ // - MIME type DB schema overhaul
+ // version 15ish, still labeled 13:
+ // - change tokenizer to mozporter to support CJK
+ // (We are slip-streaming this so that only people who want to test CJK
+ // have to test it. We will properly bump the schema revision when the
+ // gloda correctness patch lands.)
+ // version 16ish, labeled 14 and now 16
+ // - gloda message id's start from 32 now
+ // - all kinds of correctness changes (blow away)
+ // version 17
+ // - more correctness fixes. (blow away)
+ // version 18
+ // - significant empty set support (blow away)
+ // version 19
+ // - there was a typo that was resulting in deleted getting set to the
+ // numeric value of the javascript undefined value. (migrate-able)
+ // version 20
+ // - tokenizer changes to provide for case/accent-folding. (blow away)
+ // version 21
+ // - add the messagesAttribFastDeletion index we thought was already covered
+ // by an index we removed a while ago (migrate-able)
+ // version 26
+ // - bump page size and also cache size (blow away)
+ // version 30
+ // - recover from bug 732372 that affected TB 11 beta / TB 12 alpha / TB 13
+ // trunk. The fix is bug 734507. The revision bump happens
+ // asynchronously. (migrate-able)
+
+ // nuke if prior to 26
+ if (aCurVersion < 26) {
+ return this._nukeMigration(aDBFile, aDBConnection);
+ }
+
+ // They must be desiring our "a.contact is undefined" fix!
+ // This fix runs asynchronously as the first indexing job the indexer ever
+ // performs. It is scheduled by the enabling of the message indexer and
+ // it is the one that updates the schema version when done.
+
+ // return the same DB connection since we didn't create a new one or do
+ // anything.
+ return aDBConnection;
+ },
+
+ /**
+ * Asynchronously update the schema version; only for use by in-tree callers
+ * who asynchronously perform migration work triggered by their initial
+ * indexing sweep and who have properly updated the schema version in all
+ * the appropriate locations in this file.
+ *
+ * This is done without doing anything about the current transaction state,
+ * which is desired.
+ */
+ _updateSchemaVersion(newSchemaVersion) {
+ this._actualSchemaVersion = newSchemaVersion;
+ let stmt = this._createAsyncStatement(
+ // we need to concat; pragmas don't like "?1" binds
+ "PRAGMA user_version = " + newSchemaVersion,
+ true
+ );
+ stmt.executeAsync(this.trackAsync());
+ stmt.finalize();
+ },
+
+ _outstandingAsyncStatements: [],
+
+ /**
+ * Unless debugging, this is just _realCreateAsyncStatement, but in some
+ * debugging modes this is instead the helpful wrapper
+ * _createExplainedAsyncStatement.
+ */
+ _createAsyncStatement: null,
+
+ _realCreateAsyncStatement(aSQLString, aWillFinalize) {
+ let statement = null;
+ try {
+ statement = this.asyncConnection.createAsyncStatement(aSQLString);
+ } catch (ex) {
+ throw new Error(
+ "error creating async statement " +
+ aSQLString +
+ " - " +
+ this.asyncConnection.lastError +
+ ": " +
+ this.asyncConnection.lastErrorString +
+ " - " +
+ ex
+ );
+ }
+
+ if (!aWillFinalize) {
+ this._outstandingAsyncStatements.push(statement);
+ }
+
+ return statement;
+ },
+
+ /**
+ * The ExplainedStatementProcessor instance used by
+ * _createExplainedAsyncStatement. This will be null if
+ * _createExplainedAsyncStatement is not being used as _createAsyncStatement.
+ */
+ _explainProcessor: null,
+
+ /**
+ * Wrapped version of _createAsyncStatement that EXPLAINs the statement. When
+ * used this decorates _createAsyncStatement, in which case we are found at
+ * that name and the original is at _orig_createAsyncStatement. This is
+ * controlled by the explainToPath preference (see |_init|).
+ */
+ _createExplainedAsyncStatement(aSQLString, aWillFinalize) {
+ let realStatement = this._realCreateAsyncStatement(
+ aSQLString,
+ aWillFinalize
+ );
+ // don't wrap transaction control statements.
+ if (
+ aSQLString == "COMMIT" ||
+ aSQLString == "BEGIN TRANSACTION" ||
+ aSQLString == "ROLLBACK"
+ ) {
+ return realStatement;
+ }
+
+ let explainSQL = "EXPLAIN " + aSQLString;
+ let explainStatement = this._realCreateAsyncStatement(explainSQL);
+
+ return new ExplainedStatementWrapper(
+ realStatement,
+ explainStatement,
+ aSQLString,
+ this._explainProcessor
+ );
+ },
+
+ _cleanupAsyncStatements() {
+ this._outstandingAsyncStatements.forEach(stmt => stmt.finalize());
+ },
+
+ _outstandingSyncStatements: [],
+
+ _createSyncStatement(aSQLString, aWillFinalize) {
+ let statement = null;
+ try {
+ statement = this.syncConnection.createStatement(aSQLString);
+ } catch (ex) {
+ throw new Error(
+ "error creating sync statement " +
+ aSQLString +
+ " - " +
+ this.syncConnection.lastError +
+ ": " +
+ this.syncConnection.lastErrorString +
+ " - " +
+ ex
+ );
+ }
+
+ if (!aWillFinalize) {
+ this._outstandingSyncStatements.push(statement);
+ }
+
+ return statement;
+ },
+
+ _cleanupSyncStatements() {
+ this._outstandingSyncStatements.forEach(stmt => stmt.finalize());
+ },
+
+ /**
+ * Perform a synchronous executeStep on the statement, handling any
+ * SQLITE_BUSY fallout that could conceivably happen from a collision on our
+ * read with the async writes.
+ * Basically we keep trying until we succeed or run out of tries.
+ * We believe this to be a reasonable course of action because we don't
+ * expect this to happen much.
+ */
+ _syncStep(aStatement) {
+ let tries = 0;
+ while (tries < 32000) {
+ try {
+ return aStatement.executeStep();
+ } catch (e) {
+ // SQLITE_BUSY becomes NS_ERROR_FAILURE
+ if (e.result == Cr.NS_ERROR_FAILURE) {
+ tries++;
+ // we really need to delay here, somehow. unfortunately, we can't
+ // allow event processing to happen, and most of the things we could
+ // do to delay ourselves result in event processing happening. (Use
+ // of a timer, a synchronous dispatch, etc.)
+ // in theory, nsIThreadEventFilter could allow us to stop other events
+ // that aren't our timer from happening, but it seems slightly
+ // dangerous and 'notxpcom' suggests it ain't happening anyways...
+ // so, let's just be dumb and hope that the underlying file I/O going
+ // on makes us more likely to yield to the other thread so it can
+ // finish what it is doing...
+ } else {
+ throw e;
+ }
+ }
+ }
+ this._log.error("Synchronous step gave up after " + tries + " tries.");
+ return false;
+ },
+
+ _bindVariant(aStatement, aIndex, aVariant) {
+ aStatement.bindByIndex(aIndex, aVariant);
+ },
+
+ /**
+ * Helper that uses the appropriate getter given the data type; should be
+ * mooted once we move to 1.9.2 and can use built-in variant support.
+ */
+ _getVariant(aRow, aIndex) {
+ let typeOfIndex = aRow.getTypeOfIndex(aIndex);
+ if (typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ // XPConnect would just end up going through an intermediary double stage
+ // for the int64 case anyways...
+ return null;
+ }
+ if (
+ typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER ||
+ typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_DOUBLE
+ ) {
+ return aRow.getDouble(aIndex);
+ }
+ // typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_TEXT
+ return aRow.getString(aIndex);
+ },
+
+ /** Simple nested transaction support as a performance optimization. */
+ _transactionDepth: 0,
+ _transactionGood: false,
+
+ /**
+ * Self-memoizing BEGIN TRANSACTION statement.
+ */
+ get _beginTransactionStatement() {
+ let statement = this._createAsyncStatement("BEGIN TRANSACTION");
+ this.__defineGetter__("_beginTransactionStatement", () => statement);
+ return this._beginTransactionStatement;
+ },
+
+ /**
+ * Self-memoizing COMMIT statement.
+ */
+ get _commitTransactionStatement() {
+ let statement = this._createAsyncStatement("COMMIT");
+ this.__defineGetter__("_commitTransactionStatement", () => statement);
+ return this._commitTransactionStatement;
+ },
+
+ /**
+ * Self-memoizing ROLLBACK statement.
+ */
+ get _rollbackTransactionStatement() {
+ let statement = this._createAsyncStatement("ROLLBACK");
+ this.__defineGetter__("_rollbackTransactionStatement", () => statement);
+ return this._rollbackTransactionStatement;
+ },
+
+ _pendingPostCommitCallbacks: null,
+ /**
+ * Register a callback to be invoked when the current transaction's commit
+ * completes.
+ */
+ runPostCommit(aCallback) {
+ this._pendingPostCommitCallbacks.push(aCallback);
+ },
+
+ /**
+ * Begin a potentially nested transaction; only the outermost transaction gets
+ * to be an actual transaction, and the failure of any nested transaction
+ * results in a rollback of the entire outer transaction. If you really
+ * need an atomic transaction
+ */
+ _beginTransaction() {
+ if (this._transactionDepth == 0) {
+ this._pendingPostCommitCallbacks = [];
+ this._beginTransactionStatement.executeAsync(this.trackAsync());
+ this._transactionGood = true;
+ }
+ this._transactionDepth++;
+ },
+ /**
+ * Commit a potentially nested transaction; if we are the outer-most
+ * transaction and no sub-transaction issues a rollback
+ * (via _rollbackTransaction) then we commit, otherwise we rollback.
+ */
+ _commitTransaction() {
+ this._transactionDepth--;
+ if (this._transactionDepth == 0) {
+ try {
+ if (this._transactionGood) {
+ this._commitTransactionStatement.executeAsync(
+ new PostCommitHandler(this._pendingPostCommitCallbacks)
+ );
+ } else {
+ this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+ }
+ } catch (ex) {
+ this._log.error("Commit problem:", ex);
+ }
+ this._pendingPostCommitCallbacks = [];
+ }
+ },
+ /**
+ * Abort the commit of the potentially nested transaction. If we are not the
+ * outermost transaction, we set a flag that tells the outermost transaction
+ * that it must roll back.
+ */
+ _rollbackTransaction() {
+ this._transactionDepth--;
+ this._transactionGood = false;
+ if (this._transactionDepth == 0) {
+ try {
+ this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+ } catch (ex) {
+ this._log.error("Rollback problem:", ex);
+ }
+ }
+ },
+
+ _pendingAsyncStatements: 0,
+ /**
+ * The function to call, if any, when we hit 0 pending async statements.
+ */
+ _pendingAsyncCompletedListener: null,
+ _asyncCompleted() {
+ if (--this._pendingAsyncStatements == 0) {
+ if (this._pendingAsyncCompletedListener !== null) {
+ this._pendingAsyncCompletedListener();
+ this._pendingAsyncCompletedListener = null;
+ }
+ }
+ },
+ _asyncTrackerListener: {
+ handleResult() {},
+ handleError(aError) {
+ GlodaDatastore._log.error(
+ "got error in _asyncTrackerListener.handleError(): " +
+ aError.result +
+ ": " +
+ aError.message
+ );
+ },
+ handleCompletion() {
+ try {
+ // the helper method exists because the other classes need to call it too
+ GlodaDatastore._asyncCompleted();
+ } catch (e) {
+ this._log.error("Exception in handleCompletion:", e);
+ }
+ },
+ },
+ /**
+ * Increments _pendingAsyncStatements and returns a listener that will
+ * decrement the value when the statement completes.
+ */
+ trackAsync() {
+ this._pendingAsyncStatements++;
+ return this._asyncTrackerListener;
+ },
+
+ /* ********** Attribute Definitions ********** */
+ /** Maps (attribute def) compound names to the GlodaAttributeDBDef objects. */
+ _attributeDBDefs: {},
+ /** Map attribute ID to the definition and parameter value that produce it. */
+ _attributeIDToDBDefAndParam: {},
+
+ /**
+ * This attribute id indicates that we are encoding that a non-singular
+ * attribute has an empty set. The value payload that goes with this should
+ * the attribute id of the attribute we are talking about.
+ */
+ kEmptySetAttrId: 1,
+
+ /**
+ * We maintain the attributeDefinitions next id counter mainly because we can.
+ * Since we mediate the access, there's no real risk to doing so, and it
+ * allows us to keep the writes on the async connection without having to
+ * wait for a completion notification.
+ *
+ * Start from 32 so we can have a number of sentinel values.
+ */
+ _nextAttributeId: 32,
+
+ _populateAttributeDefManagedId() {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM attributeDefinitions",
+ true
+ );
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ // 0 gets returned even if there are no messages...
+ let highestSeen = stmt.getInt64(0);
+ if (highestSeen != 0) {
+ this._nextAttributeId = highestSeen + 1;
+ }
+ }
+ stmt.finalize();
+ },
+
+ get _insertAttributeDefStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO attributeDefinitions (id, attributeType, extensionName, \
+ name, parameter) \
+ VALUES (?1, ?2, ?3, ?4, ?5)"
+ );
+ this.__defineGetter__("_insertAttributeDefStatement", () => statement);
+ return this._insertAttributeDefStatement;
+ },
+
+ /**
+ * Create an attribute definition and return the row ID. Special/atypical
+ * in that it doesn't directly return a GlodaAttributeDBDef; we leave that up
+ * to the caller since they know much more than actually needs to go in the
+ * database.
+ *
+ * @returns The attribute id allocated to this attribute.
+ */
+ _createAttributeDef(aAttrType, aExtensionName, aAttrName, aParameter) {
+ let attributeId = this._nextAttributeId++;
+
+ let iads = this._insertAttributeDefStatement;
+ iads.bindByIndex(0, attributeId);
+ iads.bindByIndex(1, aAttrType);
+ iads.bindByIndex(2, aExtensionName);
+ iads.bindByIndex(3, aAttrName);
+ this._bindVariant(iads, 4, aParameter);
+
+ iads.executeAsync(this.trackAsync());
+
+ return attributeId;
+ },
+
+ /**
+ * Sync-ly look-up all the attribute definitions, populating our authoritative
+ * _attributeDBDefss and _attributeIDToDBDefAndParam maps. (In other words,
+ * once this method is called, those maps should always be in sync with the
+ * underlying database.)
+ */
+ getAllAttributes() {
+ let stmt = this._createSyncStatement(
+ "SELECT id, attributeType, extensionName, name, parameter \
+ FROM attributeDefinitions",
+ true
+ );
+
+ // map compound name to the attribute
+ let attribs = {};
+ // map the attribute id to [attribute, parameter] where parameter is null
+ // in cases where parameter is unused.
+ let idToAttribAndParam = {};
+
+ this._log.info("loading all attribute defs");
+
+ while (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ let rowId = stmt.getInt64(0);
+ let rowAttributeType = stmt.getInt64(1);
+ let rowExtensionName = stmt.getString(2);
+ let rowName = stmt.getString(3);
+ let rowParameter = this._getVariant(stmt, 4);
+
+ let compoundName = rowExtensionName + ":" + rowName;
+
+ let attrib;
+ if (compoundName in attribs) {
+ attrib = attribs[compoundName];
+ } else {
+ attrib = new GlodaAttributeDBDef(
+ this,
+ /* aID */ null,
+ compoundName,
+ rowAttributeType,
+ rowExtensionName,
+ rowName
+ );
+ attribs[compoundName] = attrib;
+ }
+ // if the parameter is null, the id goes on the attribute def, otherwise
+ // it is a parameter binding and goes in the binding map.
+ if (rowParameter == null) {
+ this._log.debug(compoundName + " primary: " + rowId);
+ attrib._id = rowId;
+ idToAttribAndParam[rowId] = [attrib, null];
+ } else {
+ this._log.debug(
+ compoundName + " binding: " + rowParameter + " = " + rowId
+ );
+ attrib._parameterBindings[rowParameter] = rowId;
+ idToAttribAndParam[rowId] = [attrib, rowParameter];
+ }
+ }
+ stmt.finalize();
+
+ this._log.info("done loading all attribute defs");
+
+ this._attributeDBDefs = attribs;
+ this._attributeIDToDBDefAndParam = idToAttribAndParam;
+ },
+
+ /**
+ * Helper method for GlodaAttributeDBDef to tell us when their bindParameter
+ * method is called and they have created a new binding (using
+ * GlodaDatastore._createAttributeDef). In theory, that method could take
+ * an additional argument and obviate the need for this method.
+ */
+ reportBinding(aID, aAttrDef, aParamValue) {
+ this._attributeIDToDBDefAndParam[aID] = [aAttrDef, aParamValue];
+ },
+
+ /* ********** Folders ********** */
+ /** next folder (row) id to issue, populated by _getAllFolderMappings. */
+ _nextFolderId: 1,
+
+ get _insertFolderLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO folderLocations (id, folderURI, dirtyStatus, name, \
+ indexingPriority) VALUES \
+ (?1, ?2, ?3, ?4, ?5)"
+ );
+ this.__defineGetter__("_insertFolderLocationStatement", () => statement);
+ return this._insertFolderLocationStatement;
+ },
+
+ /**
+ * Authoritative map from folder URI to folder ID. (Authoritative in the
+ * sense that this map exactly represents the state of the underlying
+ * database. If it does not, it's a bug in updating the database.)
+ */
+ _folderByURI: {},
+ /** Authoritative map from folder ID to folder URI */
+ _folderByID: {},
+
+ /** Initialize our _folderByURI/_folderByID mappings, called by _init(). */
+ _getAllFolderMappings() {
+ let stmt = this._createSyncStatement(
+ "SELECT id, folderURI, dirtyStatus, name, indexingPriority \
+ FROM folderLocations",
+ true
+ );
+
+ while (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ let folderID = stmt.getInt64(0);
+ let folderURI = stmt.getString(1);
+ let dirtyStatus = stmt.getInt32(2);
+ let folderName = stmt.getString(3);
+ let indexingPriority = stmt.getInt32(4);
+
+ let folder = new GlodaFolder(
+ this,
+ folderID,
+ folderURI,
+ dirtyStatus,
+ folderName,
+ indexingPriority
+ );
+
+ this._folderByURI[folderURI] = folder;
+ this._folderByID[folderID] = folder;
+
+ if (folderID >= this._nextFolderId) {
+ this._nextFolderId = folderID + 1;
+ }
+ }
+ stmt.finalize();
+ },
+
+ _folderKnown(aFolder) {
+ let folderURI = aFolder.URI;
+ return folderURI in this._folderByURI;
+ },
+
+ _folderIdKnown(aFolderID) {
+ return aFolderID in this._folderByID;
+ },
+
+ /**
+ * Return the default messaging priority for a folder of this type, based
+ * on the folder's flags. If aAllowSpecialFolderIndexing is true, then
+ * folders suchs as Trash and Junk will be indexed.
+ *
+ * @param {nsIMsgFolder} aFolder
+ * @param {boolean} aAllowSpecialFolderIndexing
+ * @returns {number}
+ */
+ getDefaultIndexingPriority(aFolder, aAllowSpecialFolderIndexing) {
+ let indexingPriority = GlodaFolder.prototype.kIndexingDefaultPriority;
+ // Do not walk into trash/junk folders, unless the user is explicitly
+ // telling us to do so.
+ let specialFolderFlags =
+ Ci.nsMsgFolderFlags.Trash | Ci.nsMsgFolderFlags.Junk;
+ if (aFolder.isSpecialFolder(specialFolderFlags, true)) {
+ indexingPriority = aAllowSpecialFolderIndexing
+ ? GlodaFolder.prototype.kIndexingDefaultPriority
+ : GlodaFolder.prototype.kIndexingNeverPriority;
+ } else if (
+ aFolder.flags &
+ (Ci.nsMsgFolderFlags.Queue | Ci.nsMsgFolderFlags.Newsgroup)
+ // In unit testing at least folders can be
+ // confusingly labeled ImapPublic when they
+ // should not be. Or at least I don't think they
+ // should be. So they're legit for now.
+ // | Ci.nsMsgFolderFlags.ImapPublic
+ // | Ci.nsMsgFolderFlags.ImapOtherUser
+ ) {
+ // Queue folders should always be ignored just because messages should not
+ // spend much time in there.
+ // We hate newsgroups, and public IMAP folders are similar.
+ // Other user IMAP folders should be ignored because it's not this user's
+ // mail.
+ indexingPriority = GlodaFolder.prototype.kIndexingNeverPriority;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.Inbox) {
+ indexingPriority = GlodaFolder.prototype.kIndexingInboxPriority;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.SentMail) {
+ indexingPriority = GlodaFolder.prototype.kIndexingSentMailPriority;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.Favorite) {
+ indexingPriority = GlodaFolder.prototype.kIndexingFavoritePriority;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.CheckNew) {
+ indexingPriority = GlodaFolder.prototype.kIndexingCheckNewPriority;
+ }
+
+ return indexingPriority;
+ },
+
+ /**
+ * Map a folder URI to a GlodaFolder instance, creating the mapping if it does
+ * not yet exist.
+ *
+ * @param aFolder The nsIMsgFolder instance you would like the GlodaFolder
+ * instance for.
+ * @returns The existing or newly created GlodaFolder instance.
+ */
+ _mapFolder(aFolder) {
+ let folderURI = aFolder.URI;
+ if (folderURI in this._folderByURI) {
+ return this._folderByURI[folderURI];
+ }
+
+ let folderID = this._nextFolderId++;
+
+ // If there's an indexingPriority stored on the folder, just use that.
+ // Otherwise, fall back to the default for folders of this type.
+ let indexingPriority = NaN;
+ try {
+ let pri = aFolder.getStringProperty("indexingPriority"); // Might throw.
+ indexingPriority = parseInt(pri); // Might return NaN.
+ } catch (ex) {}
+ if (isNaN(indexingPriority)) {
+ indexingPriority = this.getDefaultIndexingPriority(aFolder);
+ }
+
+ // If there are messages in the folder, it is filthy. If there are no
+ // messages, it can be clean.
+ let dirtyStatus = aFolder.getTotalMessages(false)
+ ? GlodaFolder.prototype.kFolderFilthy
+ : GlodaFolder.prototype.kFolderClean;
+ let folder = new GlodaFolder(
+ this,
+ folderID,
+ folderURI,
+ dirtyStatus,
+ aFolder.prettyName,
+ indexingPriority
+ );
+
+ this._insertFolderLocationStatement.bindByIndex(0, folder.id);
+ this._insertFolderLocationStatement.bindByIndex(1, folder.uri);
+ this._insertFolderLocationStatement.bindByIndex(2, folder.dirtyStatus);
+ this._insertFolderLocationStatement.bindByIndex(3, folder.name);
+ this._insertFolderLocationStatement.bindByIndex(4, folder.indexingPriority);
+ this._insertFolderLocationStatement.executeAsync(this.trackAsync());
+
+ this._folderByURI[folderURI] = folder;
+ this._folderByID[folderID] = folder;
+ this._log.debug("!! mapped " + folder.id + " from " + folderURI);
+ return folder;
+ },
+
+ /**
+ * Map an integer gloda folder ID to the corresponding GlodaFolder instance.
+ *
+ * @param aFolderID The known valid gloda folder ID for which you would like
+ * a GlodaFolder instance.
+ * @returns The GlodaFolder instance with the given id. If no such instance
+ * exists, we will throw an exception.
+ */
+ _mapFolderID(aFolderID) {
+ if (aFolderID === null) {
+ return null;
+ }
+ if (aFolderID in this._folderByID) {
+ return this._folderByID[aFolderID];
+ }
+ throw new Error("Got impossible folder ID: " + aFolderID);
+ },
+
+ /**
+ * Mark the gloda folder as deleted for any outstanding references to it and
+ * remove it from our tables so we don't hand out any new references. The
+ * latter is especially important in the case a folder with the same name
+ * is created afterwards; we don't want to confuse the new one with the old
+ * one!
+ */
+ _killGlodaFolderIntoTombstone(aGlodaFolder) {
+ aGlodaFolder._deleted = true;
+ delete this._folderByURI[aGlodaFolder.uri];
+ delete this._folderByID[aGlodaFolder.id];
+ },
+
+ get _updateFolderDirtyStatusStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE folderLocations SET dirtyStatus = ?1 \
+ WHERE id = ?2"
+ );
+ this.__defineGetter__("_updateFolderDirtyStatusStatement", () => statement);
+ return this._updateFolderDirtyStatusStatement;
+ },
+
+ updateFolderDirtyStatus(aFolder) {
+ let ufds = this._updateFolderDirtyStatusStatement;
+ ufds.bindByIndex(1, aFolder.id);
+ ufds.bindByIndex(0, aFolder.dirtyStatus);
+ ufds.executeAsync(this.trackAsync());
+ },
+
+ get _updateFolderIndexingPriorityStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE folderLocations SET indexingPriority = ?1 \
+ WHERE id = ?2"
+ );
+ this.__defineGetter__(
+ "_updateFolderIndexingPriorityStatement",
+ () => statement
+ );
+ return this._updateFolderIndexingPriorityStatement;
+ },
+
+ updateFolderIndexingPriority(aFolder) {
+ let ufip = this._updateFolderIndexingPriorityStatement;
+ ufip.bindByIndex(1, aFolder.id);
+ ufip.bindByIndex(0, aFolder.indexingPriority);
+ ufip.executeAsync(this.trackAsync());
+ },
+
+ get _updateFolderLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE folderLocations SET folderURI = ?1 \
+ WHERE id = ?2"
+ );
+ this.__defineGetter__("_updateFolderLocationStatement", () => statement);
+ return this._updateFolderLocationStatement;
+ },
+
+ /**
+ * Non-recursive asynchronous folder renaming based on the URI.
+ *
+ * @TODO provide a mechanism for recursive folder renames or have a higher
+ * layer deal with it and remove this note.
+ */
+ renameFolder(aOldFolder, aNewURI) {
+ if (!(aOldFolder.URI in this._folderByURI)) {
+ return;
+ }
+ let folder = this._mapFolder(aOldFolder); // ensure the folder is mapped
+ let oldURI = folder.uri;
+ this._folderByURI[aNewURI] = folder;
+ folder._uri = aNewURI;
+ this._log.info("renaming folder URI " + oldURI + " to " + aNewURI);
+ this._updateFolderLocationStatement.bindByIndex(1, folder.id);
+ this._updateFolderLocationStatement.bindByIndex(0, aNewURI);
+ this._updateFolderLocationStatement.executeAsync(this.trackAsync());
+
+ delete this._folderByURI[oldURI];
+ },
+
+ get _deleteFolderByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM folderLocations WHERE id = ?1"
+ );
+ this.__defineGetter__("_deleteFolderByIDStatement", () => statement);
+ return this._deleteFolderByIDStatement;
+ },
+
+ deleteFolderByID(aFolderID) {
+ let dfbis = this._deleteFolderByIDStatement;
+ dfbis.bindByIndex(0, aFolderID);
+ dfbis.executeAsync(this.trackAsync());
+ },
+
+ /* ********** Conversation ********** */
+ /** The next conversation id to allocate. Initialize at startup. */
+ _nextConversationId: 1,
+
+ _populateConversationManagedId() {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM conversations",
+ true
+ );
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ this._nextConversationId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertConversationStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO conversations (id, subject, oldestMessageDate, \
+ newestMessageDate) \
+ VALUES (?1, ?2, ?3, ?4)"
+ );
+ this.__defineGetter__("_insertConversationStatement", () => statement);
+ return this._insertConversationStatement;
+ },
+
+ get _insertConversationTextStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO conversationsText (docid, subject) \
+ VALUES (?1, ?2)"
+ );
+ this.__defineGetter__("_insertConversationTextStatement", () => statement);
+ return this._insertConversationTextStatement;
+ },
+
+ /**
+ * Asynchronously create a conversation.
+ */
+ createConversation(aSubject, aOldestMessageDate, aNewestMessageDate) {
+ // create the data row
+ let conversationID = this._nextConversationId++;
+ let ics = this._insertConversationStatement;
+ ics.bindByIndex(0, conversationID);
+ ics.bindByIndex(1, aSubject);
+ if (aOldestMessageDate == null) {
+ ics.bindByIndex(2, null);
+ } else {
+ ics.bindByIndex(2, aOldestMessageDate);
+ }
+ if (aNewestMessageDate == null) {
+ ics.bindByIndex(3, null);
+ } else {
+ ics.bindByIndex(3, aNewestMessageDate);
+ }
+ ics.executeAsync(this.trackAsync());
+
+ // create the fulltext row, using the same rowid/docid
+ let icts = this._insertConversationTextStatement;
+ icts.bindByIndex(0, conversationID);
+ icts.bindByIndex(1, aSubject);
+ icts.executeAsync(this.trackAsync());
+
+ // create it
+ let conversation = new GlodaConversation(
+ this,
+ conversationID,
+ aSubject,
+ aOldestMessageDate,
+ aNewestMessageDate
+ );
+ // it's new! let the collection manager know about it.
+ GlodaCollectionManager.itemsAdded(conversation.NOUN_ID, [conversation]);
+ // return it
+ return conversation;
+ },
+
+ get _deleteConversationByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM conversations WHERE id = ?1"
+ );
+ this.__defineGetter__("_deleteConversationByIDStatement", () => statement);
+ return this._deleteConversationByIDStatement;
+ },
+
+ /**
+ * Asynchronously delete a conversation given its ID.
+ */
+ deleteConversationByID(aConversationID) {
+ let dcbids = this._deleteConversationByIDStatement;
+ dcbids.bindByIndex(0, aConversationID);
+ dcbids.executeAsync(this.trackAsync());
+
+ GlodaCollectionManager.itemsDeleted(GlodaConversation.prototype.NOUN_ID, [
+ aConversationID,
+ ]);
+ },
+
+ _conversationFromRow(aStmt) {
+ let oldestMessageDate, newestMessageDate;
+ if (aStmt.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ oldestMessageDate = null;
+ } else {
+ oldestMessageDate = aStmt.getInt64(2);
+ }
+ if (aStmt.getTypeOfIndex(3) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ newestMessageDate = null;
+ } else {
+ newestMessageDate = aStmt.getInt64(3);
+ }
+ return new GlodaConversation(
+ this,
+ aStmt.getInt64(0),
+ aStmt.getString(1),
+ oldestMessageDate,
+ newestMessageDate
+ );
+ },
+
+ /* ********** Message ********** */
+ /**
+ * Next message id, managed because of our use of asynchronous inserts.
+ * Initialized by _populateMessageManagedId called by _init.
+ *
+ * Start from 32 to leave us all kinds of magical sentinel values at the
+ * bottom.
+ */
+ _nextMessageId: 32,
+
+ _populateMessageManagedId() {
+ let stmt = this._createSyncStatement("SELECT MAX(id) FROM messages", true);
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ // 0 gets returned even if there are no messages...
+ let highestSeen = stmt.getInt64(0);
+ if (highestSeen != 0) {
+ this._nextMessageId = highestSeen + 1;
+ }
+ }
+ stmt.finalize();
+ },
+
+ get _insertMessageStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messages (id, folderID, messageKey, conversationID, date, \
+ headerMessageID, jsonAttributes, notability) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
+ );
+ this.__defineGetter__("_insertMessageStatement", () => statement);
+ return this._insertMessageStatement;
+ },
+
+ get _insertMessageTextStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messagesText (docid, subject, body, attachmentNames, \
+ author, recipients) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
+ );
+ this.__defineGetter__("_insertMessageTextStatement", () => statement);
+ return this._insertMessageTextStatement;
+ },
+
+ /**
+ * Create a GlodaMessage with the given properties. Because this is only half
+ * of the process of creating a message (the attributes still need to be
+ * completed), it's on the caller's head to call GlodaCollectionManager's
+ * itemAdded method once the message is fully created.
+ *
+ * This method uses the async connection, any downstream logic that depends on
+ * this message actually existing in the database must be done using an
+ * async query.
+ */
+ createMessage(
+ aFolder,
+ aMessageKey,
+ aConversationID,
+ aDatePRTime,
+ aHeaderMessageID
+ ) {
+ let folderID;
+ if (aFolder != null) {
+ folderID = this._mapFolder(aFolder).id;
+ } else {
+ folderID = null;
+ }
+
+ let messageID = this._nextMessageId++;
+
+ let message = new GlodaMessage(
+ this,
+ messageID,
+ folderID,
+ aMessageKey,
+ aConversationID,
+ /* conversation */ null,
+ aDatePRTime ? new Date(aDatePRTime / 1000) : null,
+ aHeaderMessageID,
+ /* deleted */ false,
+ /* jsonText */ undefined,
+ /* notability*/ 0
+ );
+
+ // We would love to notify the collection manager about the message at this
+ // point (at least if it's not a ghost), but we can't yet. We need to wait
+ // until the attributes have been indexed, which means it's out of our
+ // hands. (Gloda.processMessage does it.)
+
+ return message;
+ },
+
+ insertMessage(aMessage) {
+ this._log.debug("insertMessage " + aMessage);
+ let ims = this._insertMessageStatement;
+ ims.bindByIndex(0, aMessage.id);
+ if (aMessage.folderID == null) {
+ ims.bindByIndex(1, null);
+ } else {
+ ims.bindByIndex(1, aMessage.folderID);
+ }
+ if (aMessage.messageKey == null) {
+ ims.bindByIndex(2, null);
+ } else {
+ ims.bindByIndex(2, aMessage.messageKey);
+ }
+ ims.bindByIndex(3, aMessage.conversationID);
+ if (aMessage.date == null) {
+ ims.bindByIndex(4, null);
+ } else {
+ ims.bindByIndex(4, aMessage.date * 1000);
+ }
+ ims.bindByIndex(5, aMessage.headerMessageID);
+ if (aMessage._jsonText) {
+ ims.bindByIndex(6, aMessage._jsonText);
+ } else {
+ ims.bindByIndex(6, null);
+ }
+ ims.bindByIndex(7, aMessage.notability);
+
+ try {
+ ims.executeAsync(this.trackAsync());
+ } catch (ex) {
+ throw new Error(
+ "error executing statement... " +
+ this.asyncConnection.lastError +
+ ": " +
+ this.asyncConnection.lastErrorString +
+ " - " +
+ ex
+ );
+ }
+
+ // we create the full-text row for any message that isn't a ghost,
+ // whether we have the body or not
+ if (aMessage.folderID !== null) {
+ this._insertMessageText(aMessage);
+ }
+ },
+
+ /**
+ * Inserts a full-text row. This should only be called if you're sure you want
+ * to insert a row into the table.
+ */
+ _insertMessageText(aMessage) {
+ if (aMessage._content && aMessage._content.hasContent()) {
+ aMessage._indexedBodyText = aMessage._content.getContentString(true);
+ } else if (aMessage._bodyLines) {
+ aMessage._indexedBodyText = aMessage._bodyLines.join("\n");
+ } else {
+ aMessage._indexedBodyText = null;
+ }
+
+ let imts = this._insertMessageTextStatement;
+ imts.bindByIndex(0, aMessage.id);
+ imts.bindByIndex(1, aMessage._subject);
+ if (aMessage._indexedBodyText == null) {
+ imts.bindByIndex(2, null);
+ } else {
+ imts.bindByIndex(2, aMessage._indexedBodyText);
+ }
+ if (aMessage._attachmentNames === null) {
+ imts.bindByIndex(3, null);
+ } else {
+ imts.bindByIndex(3, aMessage._attachmentNames.join("\n"));
+ }
+
+ // if (aMessage._indexAuthor)
+ imts.bindByIndex(4, aMessage._indexAuthor);
+ // if (aMessage._indexRecipients)
+ imts.bindByIndex(5, aMessage._indexRecipients);
+
+ try {
+ imts.executeAsync(this.trackAsync());
+ } catch (ex) {
+ throw new Error(
+ "error executing fulltext statement... " +
+ this.asyncConnection.lastError +
+ ": " +
+ this.asyncConnection.lastErrorString +
+ " - " +
+ ex
+ );
+ }
+ },
+
+ get _updateMessageStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = ?1, \
+ messageKey = ?2, \
+ conversationID = ?3, \
+ date = ?4, \
+ headerMessageID = ?5, \
+ jsonAttributes = ?6, \
+ notability = ?7, \
+ deleted = ?8 \
+ WHERE id = ?9"
+ );
+ this.__defineGetter__("_updateMessageStatement", () => statement);
+ return this._updateMessageStatement;
+ },
+
+ get _updateMessageTextStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messagesText SET body = ?1, \
+ attachmentNames = ?2 \
+ WHERE docid = ?3"
+ );
+
+ this.__defineGetter__("_updateMessageTextStatement", () => statement);
+ return this._updateMessageTextStatement;
+ },
+
+ /**
+ * Update the database row associated with the message. If the message is
+ * not a ghost and has _isNew defined, messagesText is affected.
+ *
+ * aMessage._isNew is currently equivalent to the fact that there is no
+ * full-text row associated with this message, and we work with this
+ * assumption here. Note that if aMessage._isNew is not defined, then
+ * we don't do anything.
+ */
+ updateMessage(aMessage) {
+ this._log.debug("updateMessage " + aMessage);
+ let ums = this._updateMessageStatement;
+ ums.bindByIndex(8, aMessage.id);
+ if (aMessage.folderID === null) {
+ ums.bindByIndex(0, null);
+ } else {
+ ums.bindByIndex(0, aMessage.folderID);
+ }
+ if (aMessage.messageKey === null) {
+ ums.bindByIndex(1, null);
+ } else {
+ ums.bindByIndex(1, aMessage.messageKey);
+ }
+ ums.bindByIndex(2, aMessage.conversationID);
+ if (aMessage.date === null) {
+ ums.bindByIndex(3, null);
+ } else {
+ ums.bindByIndex(3, aMessage.date * 1000);
+ }
+ ums.bindByIndex(4, aMessage.headerMessageID);
+ if (aMessage._jsonText) {
+ ums.bindByIndex(5, aMessage._jsonText);
+ } else {
+ ums.bindByIndex(5, null);
+ }
+ ums.bindByIndex(6, aMessage.notability);
+ ums.bindByIndex(7, aMessage._isDeleted ? 1 : 0);
+
+ ums.executeAsync(this.trackAsync());
+
+ if (aMessage.folderID !== null) {
+ if ("_isNew" in aMessage && aMessage._isNew === true) {
+ this._insertMessageText(aMessage);
+ } else {
+ this._updateMessageText(aMessage);
+ }
+ }
+ },
+
+ /**
+ * Updates the full-text row associated with this message. This only performs
+ * the UPDATE query if the indexed body text has changed, which means that if
+ * the body hasn't changed but the attachments have, we don't update.
+ */
+ _updateMessageText(aMessage) {
+ let newIndexedBodyText;
+ if (aMessage._content && aMessage._content.hasContent()) {
+ newIndexedBodyText = aMessage._content.getContentString(true);
+ } else if (aMessage._bodyLines) {
+ newIndexedBodyText = aMessage._bodyLines.join("\n");
+ } else {
+ newIndexedBodyText = null;
+ }
+
+ // If the body text matches, don't perform an update
+ if (newIndexedBodyText == aMessage._indexedBodyText) {
+ this._log.debug(
+ "in _updateMessageText, skipping update because body matches"
+ );
+ return;
+ }
+
+ aMessage._indexedBodyText = newIndexedBodyText;
+ let umts = this._updateMessageTextStatement;
+ umts.bindByIndex(2, aMessage.id);
+
+ if (aMessage._indexedBodyText == null) {
+ umts.bindByIndex(0, null);
+ } else {
+ umts.bindByIndex(0, aMessage._indexedBodyText);
+ }
+
+ if (aMessage._attachmentNames == null) {
+ umts.bindByIndex(1, null);
+ } else {
+ umts.bindByIndex(1, aMessage._attachmentNames.join("\n"));
+ }
+
+ try {
+ umts.executeAsync(this.trackAsync());
+ } catch (ex) {
+ throw new Error(
+ "error executing fulltext statement... " +
+ this.asyncConnection.lastError +
+ ": " +
+ this.asyncConnection.lastErrorString +
+ " - " +
+ ex
+ );
+ }
+ },
+
+ get _updateMessageLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = ?1, messageKey = ?2 WHERE id = ?3"
+ );
+ this.__defineGetter__("_updateMessageLocationStatement", () => statement);
+ return this._updateMessageLocationStatement;
+ },
+
+ /**
+ * Given a list of gloda message ids, and a list of their new message keys in
+ * the given new folder location, asynchronously update the message's
+ * database locations. Also, update the in-memory representations.
+ */
+ updateMessageLocations(
+ aMessageIds,
+ aNewMessageKeys,
+ aDestFolder,
+ aDoNotNotify
+ ) {
+ this._log.debug(
+ "updateMessageLocations:\n" +
+ "ids: " +
+ aMessageIds +
+ "\n" +
+ "keys: " +
+ aNewMessageKeys +
+ "\n" +
+ "dest folder: " +
+ aDestFolder +
+ "\n" +
+ "do not notify?" +
+ aDoNotNotify +
+ "\n"
+ );
+ let statement = this._updateMessageLocationStatement;
+ let destFolderID =
+ typeof aDestFolder == "number"
+ ? aDestFolder
+ : this._mapFolder(aDestFolder).id;
+
+ // map gloda id to the new message key for in-memory rep transform below
+ let cacheLookupMap = {};
+
+ for (let iMsg = 0; iMsg < aMessageIds.length; iMsg++) {
+ let id = aMessageIds[iMsg],
+ msgKey = aNewMessageKeys[iMsg];
+ statement.bindByIndex(0, destFolderID);
+ statement.bindByIndex(1, msgKey);
+ statement.bindByIndex(2, id);
+ statement.executeAsync(this.trackAsync());
+
+ cacheLookupMap[id] = msgKey;
+ }
+
+ // - perform the cache lookup so we can update in-memory representations
+ // found in memory items, and converted to list form for notification
+ let inMemoryItems = {},
+ modifiedItems = [];
+ GlodaCollectionManager.cacheLookupMany(
+ GlodaMessage.prototype.NOUN_ID,
+ cacheLookupMap,
+ inMemoryItems,
+ /* do not cache */ false
+ );
+ for (let glodaId in inMemoryItems) {
+ let glodaMsg = inMemoryItems[glodaId];
+ glodaMsg._folderID = destFolderID;
+ glodaMsg._messageKey = cacheLookupMap[glodaId];
+ modifiedItems.push(glodaMsg);
+ }
+
+ // tell the collection manager about the modified messages so it can update
+ // any existing views...
+ if (!aDoNotNotify && modifiedItems.length) {
+ GlodaCollectionManager.itemsModified(
+ GlodaMessage.prototype.NOUN_ID,
+ modifiedItems
+ );
+ }
+ },
+
+ get _updateMessageKeyStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET messageKey = ?1 WHERE id = ?2"
+ );
+ this.__defineGetter__("_updateMessageKeyStatement", () => statement);
+ return this._updateMessageKeyStatement;
+ },
+
+ /**
+ * Update the message keys for the gloda messages with the given id's. This
+ * is to be used in response to msgKeyChanged notifications and is similar to
+ * `updateMessageLocations` except that we do not update the folder and we
+ * do not perform itemsModified notifications (because message keys are not
+ * intended to be relevant to the gloda message abstraction).
+ */
+ updateMessageKeys(aMessageIds, aNewMessageKeys) {
+ this._log.debug(
+ "updateMessageKeys:\n" +
+ "ids: " +
+ aMessageIds +
+ "\n" +
+ "keys:" +
+ aNewMessageKeys +
+ "\n"
+ );
+ let statement = this._updateMessageKeyStatement;
+
+ // map gloda id to the new message key for in-memory rep transform below
+ let cacheLookupMap = {};
+
+ for (let iMsg = 0; iMsg < aMessageIds.length; iMsg++) {
+ let id = aMessageIds[iMsg],
+ msgKey = aNewMessageKeys[iMsg];
+ statement.bindByIndex(0, msgKey);
+ statement.bindByIndex(1, id);
+ statement.executeAsync(this.trackAsync());
+
+ cacheLookupMap[id] = msgKey;
+ }
+
+ // - perform the cache lookup so we can update in-memory representations
+ let inMemoryItems = {};
+ GlodaCollectionManager.cacheLookupMany(
+ GlodaMessage.prototype.NOUN_ID,
+ cacheLookupMap,
+ inMemoryItems,
+ /* do not cache */ false
+ );
+ for (let glodaId in inMemoryItems) {
+ let glodaMsg = inMemoryItems[glodaId];
+ glodaMsg._messageKey = cacheLookupMap[glodaId];
+ }
+ },
+
+ /**
+ * Asynchronously mutate message folder id/message keys for the given
+ * messages, indicating that we are moving them to the target folder, but
+ * don't yet know their target message keys.
+ *
+ * Updates in-memory representations too.
+ */
+ updateMessageFoldersByKeyPurging(aGlodaIds, aDestFolder) {
+ let destFolderID = this._mapFolder(aDestFolder).id;
+
+ let sqlStr =
+ "UPDATE messages SET folderID = ?1, \
+ messageKey = ?2 \
+ WHERE id IN (" +
+ aGlodaIds.join(", ") +
+ ")";
+ let statement = this._createAsyncStatement(sqlStr, true);
+ statement.bindByIndex(0, destFolderID);
+ statement.bindByIndex(1, null);
+ statement.executeAsync(this.trackAsync());
+ statement.finalize();
+
+ let cached = GlodaCollectionManager.cacheLookupManyList(
+ GlodaMessage.prototype.NOUN_ID,
+ aGlodaIds
+ );
+ for (let id in cached) {
+ let glodaMsg = cached[id];
+ glodaMsg._folderID = destFolderID;
+ glodaMsg._messageKey = null;
+ }
+ },
+
+ _messageFromRow(aRow) {
+ this._log.debug("_messageFromRow " + aRow);
+ let folderId,
+ messageKey,
+ date,
+ jsonText,
+ subject,
+ indexedBodyText,
+ attachmentNames;
+ if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ folderId = null;
+ } else {
+ folderId = aRow.getInt64(1);
+ }
+ if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ messageKey = null;
+ } else {
+ messageKey = aRow.getInt64(2);
+ }
+ if (aRow.getTypeOfIndex(4) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ date = null;
+ } else {
+ date = new Date(aRow.getInt64(4) / 1000);
+ }
+ if (aRow.getTypeOfIndex(7) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ jsonText = undefined;
+ } else {
+ jsonText = aRow.getString(7);
+ }
+ // only queryFromQuery queries will have these columns
+ if (aRow.numEntries >= 14) {
+ if (aRow.getTypeOfIndex(10) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ subject = undefined;
+ } else {
+ subject = aRow.getString(10);
+ }
+ if (aRow.getTypeOfIndex(9) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ indexedBodyText = undefined;
+ } else {
+ indexedBodyText = aRow.getString(9);
+ }
+ if (aRow.getTypeOfIndex(11) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ attachmentNames = null;
+ } else {
+ attachmentNames = aRow.getString(11);
+ if (attachmentNames) {
+ attachmentNames = attachmentNames.split("\n");
+ } else {
+ attachmentNames = null;
+ }
+ }
+ // we ignore 12, author
+ // we ignore 13, recipients
+ }
+ return new GlodaMessage(
+ this,
+ aRow.getInt64(0),
+ folderId,
+ messageKey,
+ aRow.getInt64(3),
+ null,
+ date,
+ aRow.getString(5),
+ aRow.getInt64(6),
+ jsonText,
+ aRow.getInt64(8),
+ subject,
+ indexedBodyText,
+ attachmentNames
+ );
+ },
+
+ get _updateMessagesMarkDeletedByFolderID() {
+ // When marking deleted clear the folderID and messageKey so that the
+ // indexing process can reuse it without any location constraints.
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = NULL, messageKey = NULL, \
+ deleted = 1 WHERE folderID = ?1"
+ );
+ this.__defineGetter__(
+ "_updateMessagesMarkDeletedByFolderID",
+ () => statement
+ );
+ return this._updateMessagesMarkDeletedByFolderID;
+ },
+
+ /**
+ * Efficiently mark all the messages in a folder as deleted. Unfortunately,
+ * we obviously do not know the id's of the messages affected by this which
+ * complicates in-memory updates. The options are sending out to the SQL
+ * database for a list of the message id's or some form of in-memory
+ * traversal. I/O costs being what they are, users having a propensity to
+ * have folders with tens of thousands of messages, and the unlikeliness
+ * of all of those messages being gloda-memory-resident, we go with the
+ * in-memory traversal.
+ */
+ markMessagesDeletedByFolderID(aFolderID) {
+ let statement = this._updateMessagesMarkDeletedByFolderID;
+ statement.bindByIndex(0, aFolderID);
+ statement.executeAsync(this.trackAsync());
+
+ // Have the collection manager generate itemsRemoved events for any
+ // in-memory messages in that folder.
+ GlodaCollectionManager.itemsDeletedByAttribute(
+ GlodaMessage.prototype.NOUN_ID,
+ aMsg => aMsg._folderID == aFolderID
+ );
+ },
+
+ /**
+ * Mark all the gloda messages as deleted blind-fire. Check if any of the
+ * messages are known to the collection manager and update them to be deleted
+ * along with the requisite collection notifications.
+ */
+ markMessagesDeletedByIDs(aMessageIDs) {
+ // When marking deleted clear the folderID and messageKey so that the
+ // indexing process can reuse it without any location constraints.
+ let sqlString =
+ "UPDATE messages SET folderID = NULL, messageKey = NULL, " +
+ "deleted = 1 WHERE id IN (" +
+ aMessageIDs.join(",") +
+ ")";
+
+ let statement = this._createAsyncStatement(sqlString, true);
+ statement.executeAsync(this.trackAsync());
+ statement.finalize();
+
+ GlodaCollectionManager.itemsDeleted(
+ GlodaMessage.prototype.NOUN_ID,
+ aMessageIDs
+ );
+ },
+
+ get _countDeletedMessagesStatement() {
+ let statement = this._createAsyncStatement(
+ "SELECT COUNT(*) FROM messages WHERE deleted = 1"
+ );
+ this.__defineGetter__("_countDeletedMessagesStatement", () => statement);
+ return this._countDeletedMessagesStatement;
+ },
+
+ /**
+ * Count how many messages are currently marked as deleted in the database.
+ */
+ countDeletedMessages(aCallback) {
+ let cms = this._countDeletedMessagesStatement;
+ cms.executeAsync(new SingletonResultValueHandler(aCallback));
+ },
+
+ get _deleteMessageByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messages WHERE id = ?1"
+ );
+ this.__defineGetter__("_deleteMessageByIDStatement", () => statement);
+ return this._deleteMessageByIDStatement;
+ },
+
+ get _deleteMessageTextByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messagesText WHERE docid = ?1"
+ );
+ this.__defineGetter__("_deleteMessageTextByIDStatement", () => statement);
+ return this._deleteMessageTextByIDStatement;
+ },
+
+ /**
+ * Delete a message and its fulltext from the database. It is assumed that
+ * the message was already marked as deleted and so is not visible to the
+ * collection manager and so nothing needs to be done about that.
+ */
+ deleteMessageByID(aMessageID) {
+ let dmbids = this._deleteMessageByIDStatement;
+ dmbids.bindByIndex(0, aMessageID);
+ dmbids.executeAsync(this.trackAsync());
+
+ this.deleteMessageTextByID(aMessageID);
+ },
+
+ deleteMessageTextByID(aMessageID) {
+ let dmt = this._deleteMessageTextByIDStatement;
+ dmt.bindByIndex(0, aMessageID);
+ dmt.executeAsync(this.trackAsync());
+ },
+
+ get _folderCompactionStatement() {
+ let statement = this._createAsyncStatement(
+ "SELECT id, messageKey, headerMessageID FROM messages \
+ WHERE folderID = ?1 AND \
+ messageKey >= ?2 AND +deleted = 0 ORDER BY messageKey LIMIT ?3"
+ );
+ this.__defineGetter__("_folderCompactionStatement", () => statement);
+ return this._folderCompactionStatement;
+ },
+
+ folderCompactionPassBlockFetch(
+ aFolderID,
+ aStartingMessageKey,
+ aLimit,
+ aCallback
+ ) {
+ let fcs = this._folderCompactionStatement;
+ fcs.bindByIndex(0, aFolderID);
+ fcs.bindByIndex(1, aStartingMessageKey);
+ fcs.bindByIndex(2, aLimit);
+ fcs.executeAsync(new CompactionBlockFetcherHandler(aCallback));
+ },
+
+ /* ********** Message Attributes ********** */
+ get _insertMessageAttributeStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messageAttributes (conversationID, messageID, attributeID, \
+ value) \
+ VALUES (?1, ?2, ?3, ?4)"
+ );
+ this.__defineGetter__("_insertMessageAttributeStatement", () => statement);
+ return this._insertMessageAttributeStatement;
+ },
+
+ get _deleteMessageAttributeStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messageAttributes WHERE attributeID = ?1 AND value = ?2 \
+ AND conversationID = ?3 AND messageID = ?4"
+ );
+ this.__defineGetter__("_deleteMessageAttributeStatement", () => statement);
+ return this._deleteMessageAttributeStatement;
+ },
+
+ /**
+ * Insert and remove attributes relating to a GlodaMessage. This is performed
+ * inside a pseudo-transaction (we create one if we aren't in one, using
+ * our _beginTransaction wrapper, but if we are in one, no additional
+ * meaningful semantics are added).
+ * No attempt is made to verify uniqueness of inserted attributes, either
+ * against the current database or within the provided list of attributes.
+ * The caller is responsible for ensuring that unwanted duplicates are
+ * avoided.
+ *
+ * @param aMessage The GlodaMessage the attributes belong to. This is used
+ * to provide the message id and conversation id.
+ * @param aAddDBAttributes A list of attribute tuples to add, where each tuple
+ * contains an attribute ID and a value. Lest you forget, an attribute ID
+ * corresponds to a row in the attribute definition table. The attribute
+ * definition table stores the 'parameter' for the attribute, if any.
+ * (Which is to say, our frequent Attribute-Parameter-Value triple has
+ * the Attribute-Parameter part distilled to a single attribute id.)
+ * @param aRemoveDBAttributes A list of attribute tuples to remove.
+ */
+ adjustMessageAttributes(aMessage, aAddDBAttributes, aRemoveDBAttributes) {
+ let imas = this._insertMessageAttributeStatement;
+ let dmas = this._deleteMessageAttributeStatement;
+ this._beginTransaction();
+ try {
+ for (let iAttrib = 0; iAttrib < aAddDBAttributes.length; iAttrib++) {
+ let attribValueTuple = aAddDBAttributes[iAttrib];
+
+ imas.bindByIndex(0, aMessage.conversationID);
+ imas.bindByIndex(1, aMessage.id);
+ imas.bindByIndex(2, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null) {
+ imas.bindByIndex(3, 0);
+ } else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1]) {
+ imas.bindByIndex(3, attribValueTuple[1]);
+ } else {
+ imas.bindByIndex(3, attribValueTuple[1]);
+ }
+ imas.executeAsync(this.trackAsync());
+ }
+
+ for (let iAttrib = 0; iAttrib < aRemoveDBAttributes.length; iAttrib++) {
+ let attribValueTuple = aRemoveDBAttributes[iAttrib];
+
+ dmas.bindByIndex(0, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null) {
+ dmas.bindByIndex(1, 0);
+ } else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1]) {
+ dmas.bindByIndex(1, attribValueTuple[1]);
+ } else {
+ dmas.bindByIndex(1, attribValueTuple[1]);
+ }
+ dmas.bindByIndex(2, aMessage.conversationID);
+ dmas.bindByIndex(3, aMessage.id);
+ dmas.executeAsync(this.trackAsync());
+ }
+
+ this._commitTransaction();
+ } catch (ex) {
+ this._log.error("adjustMessageAttributes:", ex);
+ this._rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ get _deleteMessageAttributesByMessageIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messageAttributes WHERE messageID = ?1"
+ );
+ this.__defineGetter__(
+ "_deleteMessageAttributesByMessageIDStatement",
+ () => statement
+ );
+ return this._deleteMessageAttributesByMessageIDStatement;
+ },
+
+ /**
+ * Clear all the message attributes for a given GlodaMessage. No changes
+ * are made to the in-memory representation of the message; it is up to the
+ * caller to ensure that it handles things correctly.
+ *
+ * @param aMessage The GlodaMessage whose database attributes should be
+ * purged.
+ */
+ clearMessageAttributes(aMessage) {
+ if (aMessage.id != null) {
+ this._deleteMessageAttributesByMessageIDStatement.bindByIndex(
+ 0,
+ aMessage.id
+ );
+ this._deleteMessageAttributesByMessageIDStatement.executeAsync(
+ this.trackAsync()
+ );
+ }
+ },
+
+ _stringSQLQuoter(aString) {
+ return "'" + aString.replace(/\'/g, "''") + "'";
+ },
+ _numberQuoter(aNum) {
+ return aNum;
+ },
+
+ /* ===== Generic Attribute Support ===== */
+ adjustAttributes(aItem, aAddDBAttributes, aRemoveDBAttributes) {
+ let nounDef = aItem.NOUN_DEF;
+ let dbMeta = nounDef._dbMeta;
+ if (dbMeta.insertAttrStatement === undefined) {
+ dbMeta.insertAttrStatement = this._createAsyncStatement(
+ "INSERT INTO " +
+ nounDef.attrTableName +
+ " (" +
+ nounDef.attrIDColumnName +
+ ", attributeID, value) " +
+ " VALUES (?1, ?2, ?3)"
+ );
+ // we always create this at the same time (right here), no need to check
+ dbMeta.deleteAttrStatement = this._createAsyncStatement(
+ "DELETE FROM " +
+ nounDef.attrTableName +
+ " WHERE " +
+ " attributeID = ?1 AND value = ?2 AND " +
+ nounDef.attrIDColumnName +
+ " = ?3"
+ );
+ }
+
+ let ias = dbMeta.insertAttrStatement;
+ let das = dbMeta.deleteAttrStatement;
+ this._beginTransaction();
+ try {
+ for (let iAttr = 0; iAttr < aAddDBAttributes.length; iAttr++) {
+ let attribValueTuple = aAddDBAttributes[iAttr];
+
+ ias.bindByIndex(0, aItem.id);
+ ias.bindByIndex(1, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null) {
+ ias.bindByIndex(2, 0);
+ } else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1]) {
+ ias.bindByIndex(2, attribValueTuple[1]);
+ } else {
+ ias.bindByIndex(2, attribValueTuple[1]);
+ }
+ ias.executeAsync(this.trackAsync());
+ }
+
+ for (let iAttr = 0; iAttr < aRemoveDBAttributes.length; iAttr++) {
+ let attribValueTuple = aRemoveDBAttributes[iAttr];
+
+ das.bindByIndex(0, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null) {
+ das.bindByIndex(1, 0);
+ } else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1]) {
+ das.bindByIndex(1, attribValueTuple[1]);
+ } else {
+ das.bindByIndex(1, attribValueTuple[1]);
+ }
+ das.bindByIndex(2, aItem.id);
+ das.executeAsync(this.trackAsync());
+ }
+
+ this._commitTransaction();
+ } catch (ex) {
+ this._log.error("adjustAttributes:", ex);
+ this._rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ clearAttributes(aItem) {
+ let nounDef = aItem.NOUN_DEF;
+ let dbMeta = nounDef._dbMeta;
+ if (dbMeta.clearAttrStatement === undefined) {
+ dbMeta.clearAttrStatement = this._createAsyncStatement(
+ "DELETE FROM " +
+ nounDef.attrTableName +
+ " WHERE " +
+ nounDef.attrIDColumnName +
+ " = ?1"
+ );
+ }
+
+ if (aItem.id != null) {
+ dbMeta.clearAttrstatement.bindByIndex(0, aItem.id);
+ dbMeta.clearAttrStatement.executeAsync(this.trackAsync());
+ }
+ },
+
+ /**
+ * escapeStringForLIKE is only available on statements, and sometimes we want
+ * to use it before we create our statement, so we create a statement just
+ * for this reason.
+ */
+ get _escapeLikeStatement() {
+ let statement = this._createAsyncStatement("SELECT 0");
+ this.__defineGetter__("_escapeLikeStatement", () => statement);
+ return this._escapeLikeStatement;
+ },
+
+ *_convertToDBValuesAndGroupByAttributeID(aAttrDef, aValues) {
+ let objectNounDef = aAttrDef.objectNounDef;
+ if (!objectNounDef.usesParameter) {
+ let dbValues = [];
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let value = aValues[iValue];
+ // If the empty set is significant and it's an empty signifier, emit
+ // the appropriate dbvalue.
+ if (value == null && aAttrDef.emptySetIsSignificant) {
+ yield [this.kEmptySetAttrId, [aAttrDef.id]];
+ // Bail if the only value was us; we don't want to add a
+ // value-posessing wildcard into the mix.
+ if (aValues.length == 1) {
+ return;
+ }
+ continue;
+ }
+ let dbValue = objectNounDef.toParamAndValue(value)[1];
+ if (dbValue != null) {
+ dbValues.push(dbValue);
+ }
+ }
+ yield [aAttrDef.special ? undefined : aAttrDef.id, dbValues];
+ return;
+ }
+
+ let curParam, attrID, dbValues;
+ let attrDBDef = aAttrDef.dbDef;
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let value = aValues[iValue];
+ // If the empty set is significant and it's an empty signifier, emit
+ // the appropriate dbvalue.
+ if (value == null && aAttrDef.emptySetIsSignificant) {
+ yield [this.kEmptySetAttrId, [aAttrDef.id]];
+ // Bail if the only value was us; we don't want to add a
+ // value-posessing wildcard into the mix.
+ if (aValues.length == 1) {
+ return;
+ }
+ continue;
+ }
+ let [dbParam, dbValue] = objectNounDef.toParamAndValue(value);
+ if (curParam === undefined) {
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ if (dbValue != null) {
+ dbValues = [dbValue];
+ } else {
+ dbValues = [];
+ }
+ } else if (curParam == dbParam) {
+ if (dbValue != null) {
+ dbValues.push(dbValue);
+ }
+ } else {
+ yield [attrID, dbValues];
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ if (dbValue != null) {
+ dbValues = [dbValue];
+ } else {
+ dbValues = [];
+ }
+ }
+ }
+ if (dbValues !== undefined) {
+ yield [attrID, dbValues];
+ }
+ },
+
+ *_convertRangesToDBStringsAndGroupByAttributeID(
+ aAttrDef,
+ aValues,
+ aValueColumnName
+ ) {
+ let objectNounDef = aAttrDef.objectNounDef;
+ if (!objectNounDef.usesParameter) {
+ let dbStrings = [];
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let [lowerVal, upperVal] = aValues[iValue];
+ // they both can't be null. that is the law.
+ if (lowerVal == null) {
+ dbStrings.push(
+ aValueColumnName +
+ " <= " +
+ objectNounDef.toParamAndValue(upperVal)[1]
+ );
+ } else if (upperVal == null) {
+ dbStrings.push(
+ aValueColumnName +
+ " >= " +
+ objectNounDef.toParamAndValue(lowerVal)[1]
+ );
+ } else {
+ // No one is null!
+ dbStrings.push(
+ aValueColumnName +
+ " BETWEEN " +
+ objectNounDef.toParamAndValue(lowerVal)[1] +
+ " AND " +
+ objectNounDef.toParamAndValue(upperVal)[1]
+ );
+ }
+ }
+ yield [aAttrDef.special ? undefined : aAttrDef.id, dbStrings];
+ return;
+ }
+
+ let curParam, attrID, dbStrings;
+ let attrDBDef = aAttrDef.dbDef;
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let [lowerVal, upperVal] = aValues[iValue];
+
+ let dbString, dbParam, lowerDBVal, upperDBVal;
+ // they both can't be null. that is the law.
+ if (lowerVal == null) {
+ [dbParam, upperDBVal] = objectNounDef.toParamAndValue(upperVal);
+ dbString = aValueColumnName + " <= " + upperDBVal;
+ } else if (upperVal == null) {
+ [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+ dbString = aValueColumnName + " >= " + lowerDBVal;
+ } else {
+ // no one is null!
+ [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+ dbString =
+ aValueColumnName +
+ " BETWEEN " +
+ lowerDBVal +
+ " AND " +
+ objectNounDef.toParamAndValue(upperVal)[1];
+ }
+
+ if (curParam === undefined) {
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ dbStrings = [dbString];
+ } else if (curParam === dbParam) {
+ dbStrings.push(dbString);
+ } else {
+ yield [attrID, dbStrings];
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ dbStrings = [dbString];
+ }
+ }
+ if (dbStrings !== undefined) {
+ yield [attrID, dbStrings];
+ }
+ },
+
+ /* eslint-disable complexity */
+ /**
+ * Perform a database query given a GlodaQueryClass instance that specifies
+ * a set of constraints relating to the noun type associated with the query.
+ * A GlodaCollection is returned containing the results of the look-up.
+ * By default the collection is "live", and will mutate (generating events to
+ * its listener) as the state of the database changes.
+ * This functionality is made user/extension visible by the Query's
+ * getCollection (asynchronous).
+ *
+ * @param [aArgs] See |GlodaQuery.getCollection| for info.
+ */
+ queryFromQuery(
+ aQuery,
+ aListener,
+ aListenerData,
+ aExistingCollection,
+ aMasterCollection,
+ aArgs
+ ) {
+ // when changing this method, be sure that GlodaQuery's testMatch function
+ // likewise has its changes made.
+ let nounDef = aQuery._nounDef;
+
+ let whereClauses = [];
+ let unionQueries = [aQuery].concat(aQuery._unions);
+ let boundArgs = [];
+
+ // Use the dbQueryValidityConstraintSuffix to provide constraints that
+ // filter items down to those that are valid for the query mechanism to
+ // return. For example, in the case of messages, deleted or ghost
+ // messages should not be returned by this query layer. We require
+ // hand-rolled SQL to do that for now.
+ let validityConstraintSuffix;
+ if (
+ nounDef.dbQueryValidityConstraintSuffix &&
+ !aQuery.options.noDbQueryValidityConstraints
+ ) {
+ validityConstraintSuffix = nounDef.dbQueryValidityConstraintSuffix;
+ } else {
+ validityConstraintSuffix = "";
+ }
+
+ for (let iUnion = 0; iUnion < unionQueries.length; iUnion++) {
+ let curQuery = unionQueries[iUnion];
+ let selects = [];
+
+ let lastConstraintWasSpecial = false;
+ let curConstraintIsSpecial;
+
+ for (
+ let iConstraint = 0;
+ iConstraint < curQuery._constraints.length;
+ iConstraint++
+ ) {
+ let constraint = curQuery._constraints[iConstraint];
+ let [constraintType, attrDef] = constraint;
+ let constraintValues = constraint.slice(2);
+
+ let tableName, idColumnName, valueColumnName;
+ if (constraintType == GlodaConstants.kConstraintIdIn) {
+ // we don't need any of the next cases' setup code, and we especially
+ // would prefer that attrDef isn't accessed since it's null for us.
+ } else if (attrDef.special) {
+ tableName = nounDef.tableName;
+ idColumnName = "id"; // canonical id for a table is "id".
+ valueColumnName = attrDef.specialColumnName;
+ curConstraintIsSpecial = true;
+ } else {
+ tableName = nounDef.attrTableName;
+ idColumnName = nounDef.attrIDColumnName;
+ valueColumnName = "value";
+ curConstraintIsSpecial = false;
+ }
+
+ let select = null,
+ test = null;
+ if (constraintType === GlodaConstants.kConstraintIdIn) {
+ // this is somewhat of a trick. this does mean that this can be the
+ // only constraint. Namely, our idiom is:
+ // SELECT * FROM blah WHERE id IN (a INTERSECT b INTERSECT c)
+ // but if we only have 'a', then that becomes "...IN (a)", and if
+ // 'a' is not a select but a list of id's... tricky, no?
+ select = constraintValues.join(",");
+ } else if (constraintType === GlodaConstants.kConstraintIn) {
+ // @testpoint gloda.datastore.sqlgen.kConstraintIn
+ let clauses = [];
+ for (let [
+ attrID,
+ values,
+ ] of this._convertToDBValuesAndGroupByAttributeID(
+ attrDef,
+ constraintValues
+ )) {
+ let clausePart;
+ if (attrID !== undefined) {
+ clausePart =
+ "(attributeID = " + attrID + (values.length ? " AND " : "");
+ } else {
+ clausePart = "(";
+ }
+ if (values.length) {
+ // strings need to be escaped, we would use ? binding, except
+ // that gets mad if we have too many strings... so we use our
+ // own escaping logic. correctly escaping is easy, but it still
+ // feels wrong to do it. (just double the quote character...)
+ if (
+ "special" in attrDef &&
+ attrDef.special == GlodaConstants.kSpecialString
+ ) {
+ clausePart +=
+ valueColumnName +
+ " IN (" +
+ values
+ .map(v => "'" + v.replace(/\'/g, "''") + "'")
+ .join(",") +
+ "))";
+ } else {
+ clausePart +=
+ valueColumnName + " IN (" + values.join(",") + "))";
+ }
+ } else {
+ clausePart += ")";
+ }
+ clauses.push(clausePart);
+ }
+ test = clauses.join(" OR ");
+ } else if (constraintType === GlodaConstants.kConstraintRanges) {
+ // @testpoint gloda.datastore.sqlgen.kConstraintRanges
+ let clauses = [];
+ for (let [
+ attrID,
+ dbStrings,
+ ] of this._convertRangesToDBStringsAndGroupByAttributeID(
+ attrDef,
+ constraintValues,
+ valueColumnName
+ )) {
+ if (attrID !== undefined) {
+ clauses.push(
+ "(attributeID = " +
+ attrID +
+ " AND (" +
+ dbStrings.join(" OR ") +
+ "))"
+ );
+ } else {
+ clauses.push("(" + dbStrings.join(" OR ") + ")");
+ }
+ }
+ test = clauses.join(" OR ");
+ } else if (constraintType === GlodaConstants.kConstraintEquals) {
+ // @testpoint gloda.datastore.sqlgen.kConstraintEquals
+ let clauses = [];
+ for (let [
+ attrID,
+ values,
+ ] of this._convertToDBValuesAndGroupByAttributeID(
+ attrDef,
+ constraintValues
+ )) {
+ if (attrID !== undefined) {
+ clauses.push(
+ "(attributeID = " +
+ attrID +
+ " AND (" +
+ values.map(_ => valueColumnName + " = ?").join(" OR ") +
+ "))"
+ );
+ } else {
+ clauses.push(
+ "(" +
+ values.map(_ => valueColumnName + " = ?").join(" OR ") +
+ ")"
+ );
+ }
+ boundArgs.push.apply(boundArgs, values);
+ }
+ test = clauses.join(" OR ");
+ } else if (constraintType === GlodaConstants.kConstraintStringLike) {
+ // @testpoint gloda.datastore.sqlgen.kConstraintStringLike
+ let likePayload = "";
+ for (let valuePart of constraintValues) {
+ if (typeof valuePart == "string") {
+ likePayload += this._escapeLikeStatement.escapeStringForLIKE(
+ valuePart,
+ "/"
+ );
+ } else {
+ likePayload += "%";
+ }
+ }
+ test = valueColumnName + " LIKE ? ESCAPE '/'";
+ boundArgs.push(likePayload);
+ } else if (constraintType === GlodaConstants.kConstraintFulltext) {
+ // @testpoint gloda.datastore.sqlgen.kConstraintFulltext
+ let matchStr = constraintValues[0];
+ select =
+ "SELECT docid FROM " +
+ nounDef.tableName +
+ "Text" +
+ " WHERE " +
+ attrDef.specialColumnName +
+ " MATCH ?";
+ boundArgs.push(matchStr);
+ }
+
+ if (curConstraintIsSpecial && lastConstraintWasSpecial && test) {
+ selects[selects.length - 1] += " AND " + test;
+ } else if (select) {
+ selects.push(select);
+ } else if (test) {
+ select =
+ "SELECT " + idColumnName + " FROM " + tableName + " WHERE " + test;
+ selects.push(select);
+ } else {
+ this._log.warn(
+ "Unable to translate constraint of type " +
+ constraintType +
+ " on attribute bound as " +
+ nounDef.name
+ );
+ }
+
+ lastConstraintWasSpecial = curConstraintIsSpecial;
+ }
+
+ if (selects.length) {
+ whereClauses.push(
+ "id IN (" +
+ selects.join(" INTERSECT ") +
+ ")" +
+ validityConstraintSuffix
+ );
+ }
+ }
+
+ let sqlString = "SELECT * FROM " + nounDef.tableName;
+ if (!aQuery.options.noMagic) {
+ if (
+ aQuery.options.noDbQueryValidityConstraints &&
+ nounDef.dbQueryJoinMagicWithNoValidityConstraints
+ ) {
+ sqlString += nounDef.dbQueryJoinMagicWithNoValidityConstraints;
+ } else if (nounDef.dbQueryJoinMagic) {
+ sqlString += nounDef.dbQueryJoinMagic;
+ }
+ }
+
+ if (whereClauses.length) {
+ sqlString += " WHERE (" + whereClauses.join(") OR (") + ")";
+ }
+
+ if (aQuery.options.explicitSQL) {
+ sqlString = aQuery.options.explicitSQL;
+ }
+
+ if (aQuery.options.outerWrapColumns) {
+ sqlString =
+ "SELECT *, " +
+ aQuery.options.outerWrapColumns.join(", ") +
+ " FROM (" +
+ sqlString +
+ ")";
+ }
+
+ if (aQuery._order.length) {
+ let orderClauses = [];
+ for (let colName of aQuery._order) {
+ if (colName.startsWith("-")) {
+ orderClauses.push(colName.substring(1) + " DESC");
+ } else {
+ orderClauses.push(colName + " ASC");
+ }
+ }
+ sqlString += " ORDER BY " + orderClauses.join(", ");
+ }
+
+ if (aQuery._limit) {
+ if (!("limitClauseAlreadyIncluded" in aQuery.options)) {
+ sqlString += " LIMIT ?";
+ }
+ boundArgs.push(aQuery._limit);
+ }
+
+ this._log.debug("QUERY FROM QUERY: " + sqlString + " ARGS: " + boundArgs);
+
+ // if we want to become explicit, replace the query (which has already
+ // provided our actual SQL query) with an explicit query. This will be
+ // what gets attached to the collection in the event we create a new
+ // collection. If we are reusing one, we assume that the explicitness,
+ // if desired, already happened.
+ // (we do not need to pass an argument to the explicitQueryClass constructor
+ // because it will be passed in to the collection's constructor, which will
+ // ensure that the collection attribute gets set.)
+ if (aArgs && "becomeExplicit" in aArgs && aArgs.becomeExplicit) {
+ aQuery = new nounDef.explicitQueryClass();
+ } else if (aArgs && "becomeNull" in aArgs && aArgs.becomeNull) {
+ aQuery = new nounDef.nullQueryClass();
+ }
+
+ return this._queryFromSQLString(
+ sqlString,
+ boundArgs,
+ nounDef,
+ aQuery,
+ aListener,
+ aListenerData,
+ aExistingCollection,
+ aMasterCollection
+ );
+ },
+ /* eslint-enable complexity */
+
+ _queryFromSQLString(
+ aSqlString,
+ aBoundArgs,
+ aNounDef,
+ aQuery,
+ aListener,
+ aListenerData,
+ aExistingCollection,
+ aMasterCollection
+ ) {
+ let statement = this._createAsyncStatement(aSqlString, true);
+ for (let [iBinding, bindingValue] of aBoundArgs.entries()) {
+ this._bindVariant(statement, iBinding, bindingValue);
+ }
+
+ let collection;
+ if (aExistingCollection) {
+ collection = aExistingCollection;
+ } else {
+ collection = new GlodaCollection(
+ aNounDef,
+ [],
+ aQuery,
+ aListener,
+ aMasterCollection
+ );
+ GlodaCollectionManager.registerCollection(collection);
+ // we don't want to overwrite the existing listener or its data, but this
+ // does raise the question about what should happen if we get passed in
+ // a different listener and/or data.
+ if (aListenerData !== undefined) {
+ collection.data = aListenerData;
+ }
+ }
+ if (aListenerData) {
+ if (collection.dataStack) {
+ collection.dataStack.push(aListenerData);
+ } else {
+ collection.dataStack = [aListenerData];
+ }
+ }
+
+ statement.executeAsync(
+ new QueryFromQueryCallback(statement, aNounDef, collection)
+ );
+ statement.finalize();
+ return collection;
+ },
+
+ /* eslint-disable complexity */
+ loadNounItem(aItem, aReferencesByNounID, aInverseReferencesByNounID) {
+ let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+
+ let hadDeps = aItem._deps != null;
+ let deps = aItem._deps || {};
+ let hasDeps = false;
+
+ for (let attrib of aItem.NOUN_DEF.specialLoadAttribs) {
+ let objectNounDef = attrib.objectNounDef;
+
+ if (
+ "special" in attrib &&
+ attrib.special === GlodaConstants.kSpecialColumnChildren
+ ) {
+ let invReferences = aInverseReferencesByNounID[objectNounDef.id];
+ if (invReferences === undefined) {
+ invReferences = aInverseReferencesByNounID[objectNounDef.id] = {};
+ }
+ // only contribute if it's not already pending or there
+ if (
+ !(attrib.id in deps) &&
+ aItem[attrib.storageAttributeName] == null
+ ) {
+ // this._log.debug(" Adding inv ref for: " + aItem.id);
+ if (!(aItem.id in invReferences)) {
+ invReferences[aItem.id] = null;
+ }
+ deps[attrib.id] = null;
+ hasDeps = true;
+ }
+ } else if (
+ "special" in attrib &&
+ attrib.special === GlodaConstants.kSpecialColumnParent
+ ) {
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (references === undefined) {
+ references = aReferencesByNounID[objectNounDef.id] = {};
+ }
+ // nothing to contribute if it's already there
+ if (
+ !(attrib.id in deps) &&
+ aItem[attrib.valueStorageAttributeName] == null
+ ) {
+ let parentID = aItem[attrib.idStorageAttributeName];
+ if (!(parentID in references)) {
+ references[parentID] = null;
+ }
+ // this._log.debug(" Adding parent ref for: " +
+ // aItem[attrib.idStorageAttributeName]);
+ deps[attrib.id] = null;
+ hasDeps = true;
+ } else {
+ this._log.debug(
+ " paranoia value storage: " +
+ aItem[attrib.valueStorageAttributeName]
+ );
+ }
+ }
+ }
+
+ // bail here if arbitrary values are not allowed, there just is no
+ // encoded json, or we already had dependencies for this guy, implying
+ // the json pass has already been performed
+ if (!aItem.NOUN_DEF.allowsArbitraryAttrs || !aItem._jsonText || hadDeps) {
+ if (hasDeps) {
+ aItem._deps = deps;
+ }
+ return hasDeps;
+ }
+
+ // this._log.debug(" load json: " + aItem._jsonText);
+ let jsonDict = JSON.parse(aItem._jsonText);
+ delete aItem._jsonText;
+
+ // Iterate over the attributes on the item
+ for (let attribId in jsonDict) {
+ let jsonValue = jsonDict[attribId];
+ // It is technically impossible for attribute ids to go away at this
+ // point in time. This would require someone to monkey around with
+ // our schema. But we will introduce this functionality one day, so
+ // prepare for it now.
+ if (!(attribId in attribIDToDBDefAndParam)) {
+ continue;
+ }
+ // find the attribute definition that corresponds to this key
+ let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+
+ let attrib = dbAttrib.attrDef;
+ // The attribute definition will fail to exist if no one defines the
+ // attribute anymore. This can happen for many reasons: an extension
+ // was uninstalled, an extension was changed and no longer defines the
+ // attribute, or patches are being applied/unapplied. Ignore this
+ // attribute if missing.
+ if (attrib == null) {
+ continue;
+ }
+ let objectNounDef = attrib.objectNounDef;
+
+ // If it has a tableName member but no fromJSON, then it's a persistent
+ // object that needs to be loaded, which also means we need to hold it in
+ // a collection owned by our collection.
+ // (If it has a fromJSON method, then it's a special case like
+ // MimeTypeNoun where it is authoritatively backed by a table but caches
+ // everything into memory. There is no case where fromJSON would be
+ // implemented but we should still be doing database lookups.)
+ if (objectNounDef.tableName && !objectNounDef.fromJSON) {
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (references === undefined) {
+ references = aReferencesByNounID[objectNounDef.id] = {};
+ }
+
+ if (attrib.singular) {
+ if (!(jsonValue in references)) {
+ references[jsonValue] = null;
+ }
+ } else {
+ for (let key in jsonValue) {
+ let anID = jsonValue[key];
+ if (!(anID in references)) {
+ references[anID] = null;
+ }
+ }
+ }
+
+ deps[attribId] = jsonValue;
+ hasDeps = true;
+ } else if (objectNounDef.contributeObjDependencies) {
+ /* if it has custom contribution logic, use it */
+ if (
+ objectNounDef.contributeObjDependencies(
+ jsonValue,
+ aReferencesByNounID,
+ aInverseReferencesByNounID
+ )
+ ) {
+ deps[attribId] = jsonValue;
+ hasDeps = true;
+ } else {
+ // just propagate the value, it's some form of simple sentinel
+ aItem[attrib.boundName] = jsonValue;
+ }
+ } else if (objectNounDef.fromJSON) {
+ // otherwise, the value just needs to be de-persisted, or...
+ if (attrib.singular) {
+ // For consistency with the non-singular case, we don't assign the
+ // attribute if undefined is returned.
+ let deserialized = objectNounDef.fromJSON(jsonValue, aItem);
+ if (deserialized !== undefined) {
+ aItem[attrib.boundName] = deserialized;
+ }
+ } else {
+ // Convert all the entries in the list filtering out any undefined
+ // values. (TagNoun will do this if the tag is now dead.)
+ let outList = [];
+ for (let key in jsonValue) {
+ let val = jsonValue[key];
+ let deserialized = objectNounDef.fromJSON(val, aItem);
+ if (deserialized !== undefined) {
+ outList.push(deserialized);
+ }
+ }
+ // Note: It's possible if we filtered things out that this is an empty
+ // list. This is acceptable because this is somewhat of an unusual
+ // case and I don't think we want to further complicate our
+ // semantics.
+ aItem[attrib.boundName] = outList;
+ }
+ } else {
+ // it's fine as is
+ aItem[attrib.boundName] = jsonValue;
+ }
+ }
+
+ if (hasDeps) {
+ aItem._deps = deps;
+ }
+ return hasDeps;
+ },
+ /* eslint-enable complexity */
+
+ loadNounDeferredDeps(aItem, aReferencesByNounID, aInverseReferencesByNounID) {
+ if (aItem._deps === undefined) {
+ return;
+ }
+
+ let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+
+ for (let [attribId, jsonValue] of Object.entries(aItem._deps)) {
+ let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+ let attrib = dbAttrib.attrDef;
+
+ let objectNounDef = attrib.objectNounDef;
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (attrib.special) {
+ if (attrib.special === GlodaConstants.kSpecialColumnChildren) {
+ let inverseReferences = aInverseReferencesByNounID[objectNounDef.id];
+ // this._log.info("inverse assignment: " + objectNounDef.id +
+ // " of " + aItem.id)
+ aItem[attrib.storageAttributeName] = inverseReferences[aItem.id];
+ } else if (attrib.special === GlodaConstants.kSpecialColumnParent) {
+ // this._log.info("parent column load: " + objectNounDef.id +
+ // " storage value: " + aItem[attrib.idStorageAttributeName]);
+ aItem[attrib.valueStorageAttributeName] =
+ references[aItem[attrib.idStorageAttributeName]];
+ }
+ } else if (objectNounDef.tableName) {
+ if (attrib.singular) {
+ aItem[attrib.boundName] = references[jsonValue];
+ } else {
+ aItem[attrib.boundName] = Object.keys(jsonValue).map(
+ key => references[jsonValue[key]]
+ );
+ }
+ } else if (objectNounDef.contributeObjDependencies) {
+ aItem[attrib.boundName] = objectNounDef.resolveObjDependencies(
+ jsonValue,
+ aReferencesByNounID,
+ aInverseReferencesByNounID
+ );
+ }
+ // there is no other case
+ }
+
+ delete aItem._deps;
+ },
+
+ /* ********** Contact ********** */
+ _nextContactId: 1,
+
+ _populateContactManagedId() {
+ let stmt = this._createSyncStatement("SELECT MAX(id) FROM contacts", true);
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ this._nextContactId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertContactStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO contacts (id, directoryUUID, contactUUID, name, popularity,\
+ frecency, jsonAttributes) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
+ );
+ this.__defineGetter__("_insertContactStatement", () => statement);
+ return this._insertContactStatement;
+ },
+
+ createContact(aDirectoryUUID, aContactUUID, aName, aPopularity, aFrecency) {
+ let contactID = this._nextContactId++;
+ let contact = new GlodaContact(
+ this,
+ contactID,
+ aDirectoryUUID,
+ aContactUUID,
+ aName,
+ aPopularity,
+ aFrecency
+ );
+ return contact;
+ },
+
+ insertContact(aContact) {
+ let ics = this._insertContactStatement;
+ ics.bindByIndex(0, aContact.id);
+ if (aContact.directoryUUID == null) {
+ ics.bindByIndex(1, null);
+ } else {
+ ics.bindByIndex(1, aContact.directoryUUID);
+ }
+ if (aContact.contactUUID == null) {
+ ics.bindByIndex(2, null);
+ } else {
+ ics.bindByIndex(2, aContact.contactUUID);
+ }
+ ics.bindByIndex(3, aContact.name);
+ ics.bindByIndex(4, aContact.popularity);
+ ics.bindByIndex(5, aContact.frecency);
+ if (aContact._jsonText) {
+ ics.bindByIndex(6, aContact._jsonText);
+ } else {
+ ics.bindByIndex(6, null);
+ }
+
+ ics.executeAsync(this.trackAsync());
+
+ return aContact;
+ },
+
+ get _updateContactStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE contacts SET directoryUUID = ?1, \
+ contactUUID = ?2, \
+ name = ?3, \
+ popularity = ?4, \
+ frecency = ?5, \
+ jsonAttributes = ?6 \
+ WHERE id = ?7"
+ );
+ this.__defineGetter__("_updateContactStatement", () => statement);
+ return this._updateContactStatement;
+ },
+
+ updateContact(aContact) {
+ let ucs = this._updateContactStatement;
+ ucs.bindByIndex(6, aContact.id);
+ ucs.bindByIndex(0, aContact.directoryUUID);
+ ucs.bindByIndex(1, aContact.contactUUID);
+ ucs.bindByIndex(2, aContact.name);
+ ucs.bindByIndex(3, aContact.popularity);
+ ucs.bindByIndex(4, aContact.frecency);
+ if (aContact._jsonText) {
+ ucs.bindByIndex(5, aContact._jsonText);
+ } else {
+ ucs.bindByIndex(5, null);
+ }
+
+ ucs.executeAsync(this.trackAsync());
+ },
+
+ _contactFromRow(aRow) {
+ let directoryUUID, contactUUID, jsonText;
+ if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ directoryUUID = null;
+ } else {
+ directoryUUID = aRow.getString(1);
+ }
+ if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ contactUUID = null;
+ } else {
+ contactUUID = aRow.getString(2);
+ }
+ if (aRow.getTypeOfIndex(6) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL) {
+ jsonText = undefined;
+ } else {
+ jsonText = aRow.getString(6);
+ }
+
+ return new GlodaContact(
+ this,
+ aRow.getInt64(0),
+ directoryUUID,
+ contactUUID,
+ aRow.getString(5),
+ aRow.getInt64(3),
+ aRow.getInt64(4),
+ jsonText
+ );
+ },
+
+ get _selectContactByIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM contacts WHERE id = ?1"
+ );
+ this.__defineGetter__("_selectContactByIDStatement", () => statement);
+ return this._selectContactByIDStatement;
+ },
+
+ /**
+ * Synchronous contact lookup currently only for use by gloda's creation
+ * of the concept of "me". It is okay for it to be doing synchronous work
+ * because it is part of the startup process before any user code could
+ * have gotten a reference to Gloda, but no one else should do this.
+ */
+ getContactByID(aContactID) {
+ let contact = GlodaCollectionManager.cacheLookupOne(
+ GlodaContact.prototype.NOUN_ID,
+ aContactID
+ );
+
+ if (contact === null) {
+ let scbi = this._selectContactByIDStatement;
+ scbi.bindByIndex(0, aContactID);
+ if (this._syncStep(scbi)) {
+ contact = this._contactFromRow(scbi);
+ GlodaCollectionManager.itemLoaded(contact);
+ }
+ scbi.reset();
+ }
+
+ return contact;
+ },
+
+ /* ********** Identity ********** */
+ /** next identity id, managed for async use reasons. */
+ _nextIdentityId: 1,
+ _populateIdentityManagedId() {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM identities",
+ true
+ );
+ if (stmt.executeStep()) {
+ // no chance of this SQLITE_BUSY on this call
+ this._nextIdentityId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertIdentityStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO identities (id, contactID, kind, value, description, relay) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
+ );
+ this.__defineGetter__("_insertIdentityStatement", () => statement);
+ return this._insertIdentityStatement;
+ },
+
+ createIdentity(aContactID, aContact, aKind, aValue, aDescription, aIsRelay) {
+ let identityID = this._nextIdentityId++;
+ let iis = this._insertIdentityStatement;
+ iis.bindByIndex(0, identityID);
+ iis.bindByIndex(1, aContactID);
+ iis.bindByIndex(2, aKind);
+ iis.bindByIndex(3, aValue);
+ iis.bindByIndex(4, aDescription);
+ iis.bindByIndex(5, aIsRelay ? 1 : 0);
+ iis.executeAsync(this.trackAsync());
+
+ let identity = new GlodaIdentity(
+ this,
+ identityID,
+ aContactID,
+ aContact,
+ aKind,
+ aValue,
+ aDescription,
+ aIsRelay
+ );
+ GlodaCollectionManager.itemsAdded(identity.NOUN_ID, [identity]);
+ return identity;
+ },
+
+ get _updateIdentityStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE identities SET contactID = ?1, \
+ kind = ?2, \
+ value = ?3, \
+ description = ?4, \
+ relay = ?5 \
+ WHERE id = ?6"
+ );
+ this.__defineGetter__("_updateIdentityStatement", () => statement);
+ return this._updateIdentityStatement;
+ },
+
+ updateIdentity(aIdentity) {
+ let ucs = this._updateIdentityStatement;
+ ucs.bindByIndex(5, aIdentity.id);
+ ucs.bindByIndex(0, aIdentity.contactID);
+ ucs.bindByIndex(1, aIdentity.kind);
+ ucs.bindByIndex(2, aIdentity.value);
+ ucs.bindByIndex(3, aIdentity.description);
+ ucs.bindByIndex(4, aIdentity.relay ? 1 : 0);
+
+ ucs.executeAsync(this.trackAsync());
+ },
+
+ _identityFromRow(aRow) {
+ return new GlodaIdentity(
+ this,
+ aRow.getInt64(0),
+ aRow.getInt64(1),
+ null,
+ aRow.getString(2),
+ aRow.getString(3),
+ aRow.getString(4),
+ !!aRow.getInt32(5)
+ );
+ },
+
+ get _selectIdentityByKindValueStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM identities WHERE kind = ?1 AND value = ?2"
+ );
+ this.__defineGetter__(
+ "_selectIdentityByKindValueStatement",
+ () => statement
+ );
+ return this._selectIdentityByKindValueStatement;
+ },
+
+ /**
+ * Synchronous lookup of an identity by kind and value, only for use by
+ * the legacy gloda core code that creates a concept of "me".
+ * Ex: (email, foo@example.com)
+ */
+ getIdentity(aKind, aValue) {
+ let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
+ GlodaIdentity.prototype.NOUN_ID,
+ aKind + "@" + aValue
+ );
+
+ let ibkv = this._selectIdentityByKindValueStatement;
+ ibkv.bindByIndex(0, aKind);
+ ibkv.bindByIndex(1, aValue);
+ if (this._syncStep(ibkv)) {
+ identity = this._identityFromRow(ibkv);
+ GlodaCollectionManager.itemLoaded(identity);
+ }
+ ibkv.reset();
+
+ return identity;
+ },
+};
+GlodaAttributeDBDef.prototype._datastore = GlodaDatastore;
+GlodaConversation.prototype._datastore = GlodaDatastore;
+GlodaFolder.prototype._datastore = GlodaDatastore;
+GlodaMessage.prototype._datastore = GlodaDatastore;
+GlodaContact.prototype._datastore = GlodaDatastore;
+GlodaIdentity.prototype._datastore = GlodaDatastore;
diff --git a/comm/mailnews/db/gloda/modules/GlodaExplicitAttr.jsm b/comm/mailnews/db/gloda/modules/GlodaExplicitAttr.jsm
new file mode 100644
index 0000000000..7a10b4112e
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaExplicitAttr.jsm
@@ -0,0 +1,188 @@
+/* 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/. */
+
+/*
+ * This file provides the "explicit attribute" provider for messages. It is
+ * concerned with attributes that are the result of user actions. For example,
+ * whether a message is starred (flagged), message tags, whether it is
+ * read/unread, etc.
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaExplicitAttr"];
+
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { TagNoun } = ChromeUtils.import("resource:///modules/gloda/NounTag.jsm");
+
+/**
+ * @namespace Explicit attribute provider. Indexes/defines attributes that are
+ * explicitly a result of user action. This dubiously includes marking a
+ * message as read.
+ */
+var GlodaExplicitAttr = {
+ providerName: "gloda.explattr",
+ strings: Services.strings.createBundle(
+ "chrome://messenger/locale/gloda.properties"
+ ),
+ _log: null,
+
+ init() {
+ this._log = console.createInstance({
+ prefix: "gloda.explattr",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+
+ try {
+ this.defineAttributes();
+ } catch (ex) {
+ this._log.error("Error in init: " + ex);
+ throw ex;
+ }
+ },
+
+ /** Boost for starred messages. */
+ NOTABILITY_STARRED: 16,
+ /** Boost for tagged messages, first tag. */
+ NOTABILITY_TAGGED_FIRST: 8,
+ /** Boost for tagged messages, each additional tag. */
+ NOTABILITY_TAGGED_ADDL: 1,
+
+ defineAttributes() {
+ // Tag
+ this._attrTag = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "tag",
+ bindName: "tags",
+ singular: false,
+ emptySetIsSignificant: true,
+ facet: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_TAG,
+ parameterNoun: null,
+ // Property change notifications that we care about:
+ propertyChanges: ["keywords"],
+ }); // not-tested
+
+ // Star
+ this._attrStar = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "star",
+ bindName: "starred",
+ singular: true,
+ facet: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+ // Read/Unread
+ this._attrRead = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "read",
+ // Make the message query-able but without using the database.
+ canQuery: "truthy-but-not-true",
+ singular: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+
+ /**
+ * Has this message been replied to by the user.
+ */
+ this._attrRepliedTo = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "repliedTo",
+ singular: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+
+ /**
+ * Has this user forwarded this message to someone.
+ */
+ this._attrForwarded = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "forwarded",
+ singular: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+ },
+
+ *process(aGlodaMessage, aRawReps, aIsNew, aCallbackHandle) {
+ let aMsgHdr = aRawReps.header;
+
+ aGlodaMessage.starred = aMsgHdr.isFlagged;
+ if (aGlodaMessage.starred) {
+ aGlodaMessage.notability += this.NOTABILITY_STARRED;
+ }
+
+ aGlodaMessage.read = aMsgHdr.isRead;
+
+ let flags = aMsgHdr.flags;
+ aGlodaMessage.repliedTo = Boolean(flags & Ci.nsMsgMessageFlags.Replied);
+ aGlodaMessage.forwarded = Boolean(flags & Ci.nsMsgMessageFlags.Forwarded);
+
+ let tags = (aGlodaMessage.tags = []);
+
+ // -- Tag
+ // build a map of the keywords
+ let keywords = aMsgHdr.getStringProperty("keywords");
+ let keywordList = keywords.split(" ");
+ let keywordMap = {};
+ for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
+ let keyword = keywordList[iKeyword];
+ keywordMap[keyword] = true;
+ }
+
+ let tagArray = TagNoun.getAllTags();
+ for (let iTag = 0; iTag < tagArray.length; iTag++) {
+ let tag = tagArray[iTag];
+ if (tag.key in keywordMap) {
+ tags.push(tag);
+ }
+ }
+
+ if (tags.length) {
+ aGlodaMessage.notability +=
+ this.NOTABILITY_TAGGED_FIRST +
+ (tags.length - 1) * this.NOTABILITY_TAGGED_ADDL;
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Duplicates the notability logic from process(). Arguably process should
+ * be factored to call us, grokNounItem should be factored to call us, or we
+ * should get sufficiently fancy that our code wildly diverges.
+ */
+ score(aMessage, aContext) {
+ let score = 0;
+ if (aMessage.starred) {
+ score += this.NOTABILITY_STARRED;
+ }
+ if (aMessage.tags.length) {
+ score +=
+ this.NOTABILITY_TAGGED_FIRST +
+ (aMessage.tags.length - 1) * this.NOTABILITY_TAGGED_ADDL;
+ }
+ return score;
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaFundAttr.jsm b/comm/mailnews/db/gloda/modules/GlodaFundAttr.jsm
new file mode 100644
index 0000000000..364ea61bb0
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaFundAttr.jsm
@@ -0,0 +1,947 @@
+/* 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 = ["GlodaFundAttr"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { GlodaUtils } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaUtils.jsm"
+);
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaAttachment } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDataModel.jsm"
+);
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { MimeTypeNoun } = ChromeUtils.import(
+ "resource:///modules/gloda/NounMimetype.jsm"
+);
+const { GlodaContent } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaContent.jsm"
+);
+
+/**
+ * @namespace The Gloda Fundamental Attribute provider is a special attribute
+ * provider; it provides attributes that the rest of the providers should be
+ * able to assume exist. Also, it may end up accessing things at a lower level
+ * than most extension providers should do. In summary, don't mimic this code
+ * unless you won't complain when your code breaks.
+ */
+var GlodaFundAttr = {
+ providerName: "gloda.fundattr",
+ strings: Services.strings.createBundle(
+ "chrome://messenger/locale/gloda.properties"
+ ),
+ _log: null,
+
+ init() {
+ this._log = console.createInstance({
+ prefix: "gloda.fundattr",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+
+ try {
+ this.defineAttributes();
+ } catch (ex) {
+ this._log.error("Error in init: " + ex);
+ throw ex;
+ }
+ },
+
+ POPULARITY_FROM_ME_TO: 10,
+ POPULARITY_FROM_ME_CC: 4,
+ POPULARITY_FROM_ME_BCC: 3,
+ POPULARITY_TO_ME: 5,
+ POPULARITY_CC_ME: 1,
+ POPULARITY_BCC_ME: 1,
+
+ /** Boost for messages 'I' sent */
+ NOTABILITY_FROM_ME: 10,
+ /** Boost for messages involving 'me'. */
+ NOTABILITY_INVOLVING_ME: 1,
+ /** Boost for message from someone in 'my' address book. */
+ NOTABILITY_FROM_IN_ADDR_BOOK: 10,
+ /** Boost for the first person involved in my address book. */
+ NOTABILITY_INVOLVING_ADDR_BOOK_FIRST: 8,
+ /** Boost for each additional person involved in my address book. */
+ NOTABILITY_INVOLVING_ADDR_BOOK_ADDL: 2,
+
+ defineAttributes() {
+ /* ***** Conversations ***** */
+ // conversation: subjectMatches
+ this._attrConvSubject = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "subjectMatches",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "subject",
+ subjectNouns: [GlodaConstants.NOUN_CONVERSATION],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ /* ***** Messages ***** */
+ // folder
+ this._attrFolder = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "folder",
+ singular: true,
+ facet: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "folderID",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FOLDER,
+ }); // tested-by: test_attributes_fundamental
+ this._attrAccount = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "account",
+ canQuery: "memory",
+ singular: true,
+ facet: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_ACCOUNT,
+ });
+ this._attrMessageKey = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "messageKey",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "messageKey",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+
+ // We need to surface the deleted attribute for querying, but there is no
+ // reason for user code, so let's call it "_deleted" rather than deleted.
+ // (In fact, our validity constraints require a special query formulation
+ // that user code should have no clue exists. That's right user code,
+ // that's a dare.)
+ Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "_deleted",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "deleted",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ });
+
+ // -- fulltext search helpers
+ // fulltextMatches. Match over message subject, body, and attachments
+ // @testpoint gloda.noun.message.attr.fulltextMatches
+ this._attrFulltext = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "fulltextMatches",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "messagesText",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // subjectMatches. Fulltext match on subject
+ // @testpoint gloda.noun.message.attr.subjectMatches
+ this._attrSubjectText = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "subjectMatches",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "subject",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // bodyMatches. super-synthetic full-text matching...
+ // @testpoint gloda.noun.message.attr.bodyMatches
+ this._attrBody = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "bodyMatches",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "body",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // attachmentNamesMatch
+ // @testpoint gloda.noun.message.attr.attachmentNamesMatch
+ this._attrAttachmentNames = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "attachmentNamesMatch",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "attachmentNames",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // @testpoint gloda.noun.message.attr.authorMatches
+ this._attrAuthorFulltext = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "authorMatches",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "author",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // @testpoint gloda.noun.message.attr.recipientsMatch
+ this._attrRecipientsFulltext = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "recipientsMatch",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "recipients",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ });
+
+ // --- synthetic stuff for some reason
+ // conversation
+ // @testpoint gloda.noun.message.attr.conversation
+ this._attrConversation = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "conversation",
+ singular: true,
+ special: GlodaConstants.kSpecialColumnParent,
+ specialColumnName: "conversationID",
+ idStorageAttributeName: "_conversationID",
+ valueStorageAttributeName: "_conversation",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_CONVERSATION,
+ canQuery: true,
+ });
+
+ // --- Fundamental
+ // From
+ this._attrFrom = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "from",
+ singular: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // tested-by: test_attributes_fundamental
+ // To
+ this._attrTo = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "to",
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // tested-by: test_attributes_fundamental
+ // Cc
+ this._attrCc = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "cc",
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // not-tested
+ /**
+ * Bcc'ed recipients; only makes sense for sent messages.
+ */
+ this._attrBcc = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "bcc",
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // not-tested
+
+ // Date. now lives on the row.
+ this._attrDate = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "date",
+ singular: true,
+ facet: {
+ type: "date",
+ },
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "date",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_DATE,
+ }); // tested-by: test_attributes_fundamental
+
+ // Header message ID.
+ this._attrHeaderMessageID = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "headerMessageID",
+ singular: true,
+ special: GlodaConstants.kSpecialString,
+ specialColumnName: "headerMessageID",
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_STRING,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+
+ // Attachment MIME Types
+ this._attrAttachmentTypes = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "attachmentTypes",
+ singular: false,
+ emptySetIsSignificant: true,
+ facet: {
+ type: "default",
+ // This will group the MIME types by their category.
+ groupIdAttr: "category",
+ queryHelper: "Category",
+ },
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_MIME_TYPE,
+ });
+
+ // Attachment infos
+ this._attrIsEncrypted = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "isEncrypted",
+ singular: true,
+ emptySetIsSignificant: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ });
+
+ // Attachment infos
+ this._attrAttachmentInfos = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "attachmentInfos",
+ singular: false,
+ emptySetIsSignificant: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_ATTACHMENT,
+ });
+
+ // --- Optimization
+ /**
+ * Involves means any of from/to/cc/bcc. The queries get ugly enough
+ * without this that it seems to justify the cost, especially given the
+ * frequent use case. (In fact, post-filtering for the specific from/to/cc
+ * is probably justifiable rather than losing this attribute...)
+ */
+ this._attrInvolves = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrOptimization,
+ attributeName: "involves",
+ singular: false,
+ facet: {
+ type: "default",
+ /**
+ * Filter out 'me', as we have other facets that deal with that, and the
+ * 'me' identities are so likely that they distort things.
+ *
+ * @returns true if the identity is not one of my identities, false if it
+ * is.
+ */
+ filter(aItem) {
+ return !(aItem.id in Gloda.myIdentities);
+ },
+ },
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // not-tested
+
+ /**
+ * Any of to/cc/bcc.
+ */
+ this._attrRecipients = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrOptimization,
+ attributeName: "recipients",
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // not-tested
+
+ // From Me (To/Cc/Bcc)
+ this._attrFromMe = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrOptimization,
+ attributeName: "fromMe",
+ singular: false,
+ // The interesting thing to a facet is whether the message is from me.
+ facet: {
+ type: "nonempty?",
+ },
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+ // To/Cc/Bcc Me
+ this._attrToMe = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "toMe",
+ // The interesting thing to a facet is whether the message is to me.
+ facet: {
+ type: "nonempty?",
+ },
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+
+ // -- Mailing List
+ // Non-singular, but a hard call. Namely, it is obvious that a message can
+ // be addressed to multiple mailing lists. However, I don't see how you
+ // could receive a message with more than one set of List-* headers,
+ // since each list-serve would each send you a copy. Based on our current
+ // decision to treat each physical message as separate, it almost seems
+ // right to limit the list attribute to the copy that originated at the
+ // list. That may sound entirely wrong, but keep in mind that until we
+ // have seen a message from the list with the List headers, we can't
+ // definitely know it's a mailing list (although heuristics could take us
+ // pretty far). As such, the quasi-singular thing is appealing.
+ // Of course, the reality is that we really want to know if a message was
+ // sent to multiple mailing lists and be able to query on that.
+ // Additionally, our implicit-to logic needs to work on messages that
+ // weren't relayed by the list-serve, especially messages sent to the list
+ // by the user.
+ this._attrList = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "mailing-list",
+ bindName: "mailingLists",
+ singular: false,
+ emptySetIsSignificant: true,
+ facet: true,
+ subjectNouns: [GlodaConstants.NOUN_MESSAGE],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // not-tested, not-implemented
+ },
+
+ RE_LIST_POST: /<mailto:([^>]+)>/,
+
+ /**
+ *
+ * Specializations:
+ * - Mailing Lists. Replies to a message on a mailing list frequently only
+ * have the list-serve as the 'to', so we try to generate a synthetic 'to'
+ * based on the author of the parent message when possible. (The 'possible'
+ * part is that we may not have a copy of the parent message at the time of
+ * processing.)
+ * - Newsgroups. Same deal as mailing lists.
+ */
+ *process(aGlodaMessage, aRawReps, aIsNew, aCallbackHandle) {
+ let aMsgHdr = aRawReps.header;
+ let aMimeMsg = aRawReps.mime;
+
+ // -- From
+ // Let's use replyTo if available.
+ // er, since we are just dealing with mailing lists for now, forget the
+ // reply-to...
+ // TODO: deal with default charset issues
+ let author = null;
+ /*
+ try {
+ author = aMsgHdr.getStringProperty("replyTo");
+ }
+ catch (ex) {
+ }
+ */
+ if (author == null || author == "") {
+ author = aMsgHdr.author;
+ }
+
+ let normalizedListPost = "";
+ if (aMimeMsg && aMimeMsg.has("list-post")) {
+ let match = this.RE_LIST_POST.exec(aMimeMsg.get("list-post"));
+ if (match) {
+ normalizedListPost = "<" + match[1] + ">";
+ }
+ }
+
+ // Do not use the MIME decoded variants of any of the email addresses
+ // because if name is encoded and has a comma in it, it will break the
+ // address parser (which already knows how to do the decoding anyways).
+ let [
+ authorIdentities,
+ toIdentities,
+ ccIdentities,
+ bccIdentities,
+ listIdentities,
+ ] = yield aCallbackHandle.pushAndGo(
+ Gloda.getOrCreateMailIdentities(
+ aCallbackHandle,
+ author,
+ aMsgHdr.recipients,
+ aMsgHdr.ccList,
+ aMsgHdr.bccList,
+ normalizedListPost
+ )
+ );
+
+ if (authorIdentities.length != 1) {
+ throw new Gloda.BadItemContentsError(
+ "Message with subject '" +
+ aMsgHdr.mime2DecodedSubject +
+ "' somehow lacks a valid author. Bailing."
+ );
+ }
+ let authorIdentity = authorIdentities[0];
+ aGlodaMessage.from = authorIdentity;
+
+ // -- To, Cc, Bcc
+ aGlodaMessage.to = toIdentities;
+ aGlodaMessage.cc = ccIdentities;
+ aGlodaMessage.bcc = bccIdentities;
+
+ // -- Mailing List
+ if (listIdentities.length) {
+ aGlodaMessage.mailingLists = listIdentities;
+ }
+
+ let findIsEncrypted = x =>
+ x.isEncrypted || (x.parts ? x.parts.some(findIsEncrypted) : false);
+
+ // -- Encryption
+ aGlodaMessage.isEncrypted = false;
+ if (aMimeMsg) {
+ aGlodaMessage.isEncrypted = findIsEncrypted(aMimeMsg);
+ }
+
+ // -- Attachments
+ if (aMimeMsg) {
+ // nsParseMailbox.cpp puts the attachment flag on msgHdrs as soon as it
+ // finds a multipart/mixed part. This is a good heuristic, but if it turns
+ // out the part has no filename, then we don't treat it as an attachment.
+ // We just streamed the message, and we have all the information to figure
+ // that out, so now is a good place to clear the flag if needed.
+ let attachmentTypes = new Set();
+ for (let attachment of aMimeMsg.allAttachments) {
+ // getMimeType expects the content type to contain at least a "/".
+ if (!attachment.contentType.includes("/")) {
+ continue;
+ }
+ attachmentTypes.add(MimeTypeNoun.getMimeType(attachment.contentType));
+ }
+ if (attachmentTypes.size) {
+ aGlodaMessage.attachmentTypes = Array.from(attachmentTypes);
+ }
+
+ let aMsgHdr = aRawReps.header;
+ let wasStreamed =
+ aMsgHdr &&
+ !aGlodaMessage.isEncrypted &&
+ (aMsgHdr.flags & Ci.nsMsgMessageFlags.Offline ||
+ aMsgHdr.folder instanceof Ci.nsIMsgLocalMailFolder);
+
+ // Clear the flag if it turns out there's no attachment after all and we
+ // streamed completely the message (if we didn't, then we have no
+ // knowledge of attachments, unless bug 673370 is fixed).
+ if (wasStreamed && !aMimeMsg.allAttachments.length) {
+ aMsgHdr.markHasAttachments(false);
+ }
+
+ // This is not the same kind of attachments as above. Now, we want to
+ // provide convenience attributes to Gloda consumers, so that they can run
+ // through the list of attachments of a given message, to possibly build a
+ // visualization on top of it. We still reject bogus mime types, which
+ // means yencode won't be supported. Oh, I feel really bad.
+ let attachmentInfos = [];
+ for (let att of aMimeMsg.allUserAttachments) {
+ attachmentInfos.push(
+ this.glodaAttFromMimeAtt(aRawReps.trueGlodaRep, att)
+ );
+ }
+ aGlodaMessage.attachmentInfos = attachmentInfos;
+ }
+
+ // TODO: deal with mailing lists, including implicit-to. this will require
+ // convincing the indexer to pass us in the previous message if it is
+ // available. (which we'll simply pass to everyone... it can help body
+ // logic for quoting purposes, etc. too.)
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ glodaAttFromMimeAtt(aGlodaMessage, aAtt) {
+ // So we don't want to store the URL because it can change over time if
+ // the message is moved. What we do is store the full URL if it's a
+ // detached attachment, otherwise just keep the part information, and
+ // rebuild the URL according to where the message is sitting.
+ let part, externalUrl;
+ if (aAtt.isExternal) {
+ externalUrl = aAtt.url;
+ } else {
+ let matches = aAtt.url.match(GlodaUtils.PART_RE);
+ if (matches && matches.length) {
+ part = matches[1];
+ } else {
+ this._log.error("Error processing attachment: " + aAtt.url);
+ }
+ }
+ return new GlodaAttachment(
+ aGlodaMessage,
+ aAtt.name,
+ aAtt.contentType,
+ aAtt.size,
+ part,
+ externalUrl,
+ aAtt.isExternal
+ );
+ },
+
+ *optimize(aGlodaMessage, aRawReps, aIsNew, aCallbackHandle) {
+ let aMsgHdr = aRawReps.header;
+
+ // for simplicity this is used for both involves and recipients
+ let involvesIdentities = {};
+ let involves = aGlodaMessage.involves || [];
+ let recipients = aGlodaMessage.recipients || [];
+
+ // 'me' specialization optimizations
+ let toMe = aGlodaMessage.toMe || [];
+ let fromMe = aGlodaMessage.fromMe || [];
+
+ let myIdentities = Gloda.myIdentities; // needless optimization?
+ let authorIdentity = aGlodaMessage.from;
+ let isFromMe = authorIdentity.id in myIdentities;
+
+ // The fulltext search column for the author. We want to have in here:
+ // - The e-mail address and display name as enclosed on the message.
+ // - The name per the address book card for this e-mail address, if we have
+ // one.
+ aGlodaMessage._indexAuthor = aMsgHdr.mime2DecodedAuthor;
+ // The fulltext search column for the recipients. (same deal)
+ aGlodaMessage._indexRecipients = aMsgHdr.mime2DecodedRecipients;
+
+ if (isFromMe) {
+ aGlodaMessage.notability += this.NOTABILITY_FROM_ME;
+ } else {
+ let authorDisplayName = MailServices.ab.cardForEmailAddress(
+ authorIdentity.value
+ )?.displayName;
+ if (authorDisplayName !== null) {
+ aGlodaMessage.notability += this.NOTABILITY_FROM_IN_ADDR_BOOK;
+ // @testpoint gloda.noun.message.attr.authorMatches
+ aGlodaMessage._indexAuthor += " " + authorDisplayName;
+ }
+ }
+
+ involves.push(authorIdentity);
+ involvesIdentities[authorIdentity.id] = true;
+
+ let involvedAddrBookCount = 0;
+
+ for (let toIdentity of aGlodaMessage.to) {
+ if (!(toIdentity.id in involvesIdentities)) {
+ involves.push(toIdentity);
+ recipients.push(toIdentity);
+ involvesIdentities[toIdentity.id] = true;
+ let toDisplayName = MailServices.ab.cardForEmailAddress(
+ toIdentity.value
+ )?.displayName;
+ if (toDisplayName !== null) {
+ involvedAddrBookCount++;
+ // @testpoint gloda.noun.message.attr.recipientsMatch
+ aGlodaMessage._indexRecipients += " " + toDisplayName;
+ }
+ }
+
+ // optimization attribute to-me ('I' am the parameter)
+ if (toIdentity.id in myIdentities) {
+ toMe.push([toIdentity, authorIdentity]);
+ if (aIsNew) {
+ authorIdentity.contact.popularity += this.POPULARITY_TO_ME;
+ }
+ }
+ // optimization attribute from-me-to ('I' am the parameter)
+ if (isFromMe) {
+ fromMe.push([authorIdentity, toIdentity]);
+ // also, popularity
+ if (aIsNew) {
+ toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
+ }
+ }
+ }
+ for (let ccIdentity of aGlodaMessage.cc) {
+ if (!(ccIdentity.id in involvesIdentities)) {
+ involves.push(ccIdentity);
+ recipients.push(ccIdentity);
+ involvesIdentities[ccIdentity.id] = true;
+ let ccDisplayName = MailServices.ab.cardForEmailAddress(
+ ccIdentity.value
+ )?.displayName;
+ if (ccDisplayName !== null) {
+ involvedAddrBookCount++;
+ // @testpoint gloda.noun.message.attr.recipientsMatch
+ aGlodaMessage._indexRecipients += " " + ccDisplayName;
+ }
+ }
+ // optimization attribute cc-me ('I' am the parameter)
+ if (ccIdentity.id in myIdentities) {
+ toMe.push([ccIdentity, authorIdentity]);
+ if (aIsNew) {
+ authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
+ }
+ }
+ // optimization attribute from-me-to ('I' am the parameter)
+ if (isFromMe) {
+ fromMe.push([authorIdentity, ccIdentity]);
+ // also, popularity
+ if (aIsNew) {
+ ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
+ }
+ }
+ }
+ // just treat bcc like cc; the intent is the same although the exact
+ // semantics differ.
+ for (let bccIdentity of aGlodaMessage.bcc) {
+ if (!(bccIdentity.id in involvesIdentities)) {
+ involves.push(bccIdentity);
+ recipients.push(bccIdentity);
+ involvesIdentities[bccIdentity.id] = true;
+ let bccDisplayName = MailServices.ab.cardForEmailAddress(
+ bccIdentity.value
+ )?.displayName;
+ if (bccDisplayName !== null) {
+ involvedAddrBookCount++;
+ // @testpoint gloda.noun.message.attr.recipientsMatch
+ aGlodaMessage._indexRecipients += " " + bccDisplayName;
+ }
+ }
+ // optimization attribute cc-me ('I' am the parameter)
+ if (bccIdentity.id in myIdentities) {
+ toMe.push([bccIdentity, authorIdentity]);
+ if (aIsNew) {
+ authorIdentity.contact.popularity += this.POPULARITY_BCC_ME;
+ }
+ }
+ // optimization attribute from-me-to ('I' am the parameter)
+ if (isFromMe) {
+ fromMe.push([authorIdentity, bccIdentity]);
+ // also, popularity
+ if (aIsNew) {
+ bccIdentity.contact.popularity += this.POPULARITY_FROM_ME_BCC;
+ }
+ }
+ }
+
+ if (involvedAddrBookCount) {
+ aGlodaMessage.notability +=
+ this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
+ (involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
+ }
+
+ aGlodaMessage.involves = involves;
+ aGlodaMessage.recipients = recipients;
+ if (toMe.length) {
+ aGlodaMessage.toMe = toMe;
+ aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ME;
+ }
+ if (fromMe.length) {
+ aGlodaMessage.fromMe = fromMe;
+ }
+
+ // Content
+ if (aRawReps.bodyLines) {
+ aGlodaMessage._content = aRawReps.content = new GlodaContent();
+ if (this.contentWhittle({}, aRawReps.bodyLines, aGlodaMessage._content)) {
+ // we were going to do something here?
+ }
+ } else {
+ aRawReps.content = null;
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Duplicates the notability logic from optimize(). Arguably optimize should
+ * be factored to call us, grokNounItem should be factored to call us, or we
+ * should get sufficiently fancy that our code wildly diverges.
+ */
+ score(aMessage, aContext) {
+ let score = 0;
+
+ let authorIdentity = aMessage.from;
+ if (authorIdentity.id in Gloda.myIdentities) {
+ score += this.NOTABILITY_FROM_ME;
+ } else if (authorIdentity.inAddressBook) {
+ score += this.NOTABILITY_FROM_IN_ADDR_BOOK;
+ }
+ if (aMessage.toMe) {
+ score += this.NOTABILITY_INVOLVING_ME;
+ }
+
+ let involvedAddrBookCount = 0;
+ for (let identity of aMessage.to) {
+ if (identity.inAddressBook) {
+ involvedAddrBookCount++;
+ }
+ }
+ for (let identity of aMessage.cc) {
+ if (identity.inAddressBook) {
+ involvedAddrBookCount++;
+ }
+ }
+ if (involvedAddrBookCount) {
+ score +=
+ this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
+ (involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
+ }
+ return score;
+ },
+
+ _countQuoteDepthAndNormalize(aLine) {
+ let count = 0;
+ let lastStartOffset = 0;
+
+ for (let i = 0; i < aLine.length; i++) {
+ let c = aLine[i];
+ if (c == ">") {
+ count++;
+ lastStartOffset = i + 1;
+ } else if (c != " ") {
+ return [
+ count,
+ lastStartOffset ? aLine.substring(lastStartOffset) : aLine,
+ ];
+ }
+ }
+
+ return [count, lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
+ },
+
+ /**
+ * Attempt to understand simple quoting constructs that use ">" with
+ * obvious phrases to enter the quoting block. No support for other types
+ * of quoting at this time. Also no support for piercing the wrapper of
+ * forwarded messages to actually be the content of the forwarded message.
+ */
+ contentWhittle(aMeta, aBodyLines, aContent) {
+ if (!aContent.volunteerContent(aContent.kPriorityBase)) {
+ return false;
+ }
+
+ // duplicate the list; we mutate somewhat...
+ let bodyLines = aBodyLines.concat();
+
+ // lastNonBlankLine originally was just for detecting quoting idioms where
+ // the "wrote" line was separated from the quoted block by a blank line.
+ // Now we also use it for whitespace suppression at the boundaries of
+ // quoted and un-quoted text. (We keep blank lines within the same
+ // 'block' of quoted or non-quoted text.)
+ // Because we now have two goals for it, and we still want to suppress blank
+ // lines when there is a 'wrote' line involved, we introduce...
+ // prevLastNonBlankLine! This arguably suggests refactoring should be the
+ // next step, but things work for now.
+ let rangeStart = 0,
+ lastNonBlankLine = null,
+ prevLastNonBlankLine = null;
+ let inQuoteDepth = 0;
+ for (let [iLine, line] of bodyLines.entries()) {
+ if (!line || line == "\xa0") {
+ /* unicode non breaking space */
+ continue;
+ }
+
+ if (line.startsWith(">")) {
+ if (!inQuoteDepth) {
+ let rangeEnd = iLine - 1;
+ let quoteRangeStart = iLine;
+ // see if the last non-blank-line was a lead-in...
+ if (lastNonBlankLine != null) {
+ // TODO: localize quote range start detection
+ if (aBodyLines[lastNonBlankLine].includes("wrote")) {
+ quoteRangeStart = lastNonBlankLine;
+ rangeEnd = lastNonBlankLine - 1;
+ // we 'used up' lastNonBlankLine, let's promote the prev guy to
+ // be the new lastNonBlankLine for the next logic block
+ lastNonBlankLine = prevLastNonBlankLine;
+ }
+ // eat the trailing whitespace...
+ if (lastNonBlankLine != null) {
+ rangeEnd = Math.min(rangeEnd, lastNonBlankLine);
+ }
+ }
+ if (rangeEnd >= rangeStart) {
+ aContent.content(aBodyLines.slice(rangeStart, rangeEnd + 1));
+ }
+
+ [inQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+ bodyLines[iLine] = line;
+ rangeStart = quoteRangeStart;
+ } else {
+ let curQuoteDepth;
+ [curQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+ bodyLines[iLine] = line;
+
+ if (curQuoteDepth != inQuoteDepth) {
+ // we could do some "wrote" compensation here, but it's not really
+ // as important. let's wait for a more clever algorithm.
+ aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+ inQuoteDepth = curQuoteDepth;
+ rangeStart = iLine;
+ }
+ }
+ } else if (inQuoteDepth) {
+ aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+ inQuoteDepth = 0;
+ rangeStart = iLine;
+ }
+
+ prevLastNonBlankLine = lastNonBlankLine;
+ lastNonBlankLine = iLine;
+ }
+
+ if (inQuoteDepth) {
+ aContent.quoted(aBodyLines.slice(rangeStart), inQuoteDepth);
+ } else {
+ aContent.content(aBodyLines.slice(rangeStart, lastNonBlankLine + 1));
+ }
+
+ return true;
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaIndexer.jsm b/comm/mailnews/db/gloda/modules/GlodaIndexer.jsm
new file mode 100644
index 0000000000..05919e4d67
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaIndexer.jsm
@@ -0,0 +1,1491 @@
+/* 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/. */
+
+/*
+ * This file currently contains a fairly general implementation of asynchronous
+ * indexing with a very explicit message indexing implementation. As gloda
+ * will eventually want to index more than just messages, the message-specific
+ * things should ideally lose their special hold on this file. This will
+ * benefit readability/size as well.
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaIndexer", "IndexingJob"];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "GlodaCollectionManager",
+ "resource:///modules/gloda/Collection.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "GlodaDatastore",
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+
+/**
+ * @class Capture the indexing batch concept explicitly.
+ *
+ * @param aJobType The type of thing we are indexing. Current choices are:
+ * "folder" and "message". Previous choices included "account". The indexer
+ * currently knows too much about these; they should be de-coupled.
+ * @param aID Specific to the job type, but for now only used to hold folder
+ * IDs.
+ *
+ * @ivar items The list of items to process during this job/batch. (For
+ * example, if this is a "messages" job, this would be the list of messages
+ * to process, although the specific representation is determined by the
+ * job.) The list will only be mutated through the addition of extra items.
+ * @ivar offset The current offset into the 'items' list (if used), updated as
+ * processing occurs. If 'items' is not used, the processing code can also
+ * update this in a similar fashion. This is used by the status
+ * notification code in conjunction with goal.
+ * @ivar goal The total number of items to index/actions to perform in this job.
+ * This number may increase during the life of the job, but should not
+ * decrease. This is used by the status notification code in conjunction
+ * with the goal.
+ */
+function IndexingJob(aJobType, aID, aItems) {
+ this.jobType = aJobType;
+ this.id = aID;
+ this.items = aItems != null ? aItems : [];
+ this.offset = 0;
+ this.goal = null;
+ this.callback = null;
+ this.callbackThis = null;
+}
+IndexingJob.prototype = {
+ /**
+ * Invoke the callback associated with this job, passing through all arguments
+ * received by this function to the callback function.
+ */
+ safelyInvokeCallback(...aArgs) {
+ if (!this.callback) {
+ return;
+ }
+ try {
+ this.callback.apply(this.callbackThis, aArgs);
+ } catch (ex) {
+ GlodaIndexer._log.warn("job callback invocation problem:", ex);
+ }
+ },
+ toString() {
+ return (
+ "[job:" +
+ this.jobType +
+ " id:" +
+ this.id +
+ " items:" +
+ (this.items ? this.items.length : "no") +
+ " offset:" +
+ this.offset +
+ " goal:" +
+ this.goal +
+ "]"
+ );
+ },
+};
+
+/**
+ * @namespace Core indexing logic, plus message-specific indexing logic.
+ *
+ * === Indexing Goals
+ * We have the following goals:
+ *
+ * Responsiveness
+ * - When the user wants to quit, we should be able to stop and quit in a timely
+ * fashion.
+ * - We should not interfere with the user's thunderbird usage.
+ *
+ * Correctness
+ * - Quitting should not result in any information loss; we should (eventually)
+ * end up at the same indexed state regardless of whether a user lets
+ * indexing run to completion or restarts thunderbird in the middle of the
+ * process. (It is okay to take slightly longer in the latter case.)
+ *
+ * Worst Case Scenario Avoidance
+ * - We should try to be O(1) memory-wise regardless of what notifications
+ * are thrown at us.
+ *
+ * === Indexing Throttling
+ *
+ * Adaptive Indexing
+ * - The indexer tries to stay out of the way of other running code in
+ * Thunderbird (autosync) and other code on the system. We try and target
+ * some number of milliseconds of activity between intentional inactive
+ * periods. The number of milliseconds of activity varies based on whether we
+ * believe the user to be actively using the computer or idle. We use our
+ * inactive periods as a way to measure system load; if we receive our
+ * notification promptly at the end of our inactive period, we believe the
+ * system is not heavily loaded. If we do not get notified promptly, we
+ * assume there is other stuff going on and back off.
+ *
+ */
+var GlodaIndexer = {
+ /**
+ * A partial attempt to generalize to support multiple databases. Each
+ * database would have its own datastore would have its own indexer. But
+ * we rather inter-mingle our use of this field with the singleton global
+ * GlodaDatastore.
+ */
+ _log: console.createInstance({
+ prefix: "gloda.indexer",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ }),
+ /**
+ * Our nsITimer that we use to schedule ourselves on the main thread
+ * intermittently. The timer always exists but may not always be active.
+ */
+ _timer: null,
+ /**
+ * Our nsITimer that we use to schedule events in the "far" future. For now,
+ * this means not compelling an initial indexing sweep until some number of
+ * seconds after startup.
+ */
+ _longTimer: null,
+
+ /**
+ * Periodic performance adjustment parameters: The overall goal is to adjust
+ * our rate of work so that we don't interfere with the user's activities
+ * when they are around (non-idle), and the system in general (when idle).
+ * Being nice when idle isn't quite as important, but is a good idea so that
+ * when the user un-idles we are able to back off nicely. Also, we give
+ * other processes on the system a chance to do something.
+ *
+ * We do this by organizing our work into discrete "tokens" of activity,
+ * then processing the number of tokens that we have determined will
+ * not impact the UI. Then we pause to give other activities a chance to get
+ * some work done, and we measure whether anything happened during our pause.
+ * If something else is going on in our application during that pause, we
+ * give it priority (up to a point) by delaying further indexing.
+ *
+ * Keep in mind that many of our operations are actually asynchronous, so we
+ * aren't entirely starving the event queue. However, a lot of the async
+ * stuff can end up not having any actual delay between events. For
+ * example, we only index offline message bodies, so there's no network
+ * latency involved, just disk IO; the only meaningful latency will be the
+ * initial disk seek (if there is one... pre-fetching may seriously be our
+ * friend).
+ *
+ * In order to maintain responsiveness, I assert that we want to minimize the
+ * length of the time we are dominating the event queue. This suggests
+ * that we want break up our blocks of work frequently. But not so
+ * frequently that there is a lot of waste. Accordingly our algorithm is
+ * basically:
+ *
+ * - Estimate the time that it takes to process a token, and schedule the
+ * number of tokens that should fit into that time.
+ * - Detect user activity, and back off immediately if found.
+ * - Try to delay commits and garbage collection until the user is inactive,
+ * as these tend to cause a brief pause in the UI.
+ */
+
+ /**
+ * The number of milliseconds before we declare the user idle and step up our
+ * indexing.
+ */
+ _INDEX_IDLE_ADJUSTMENT_TIME: 5000,
+
+ /**
+ * The time delay in milliseconds before we should schedule our initial sweep.
+ */
+ _INITIAL_SWEEP_DELAY: 10000,
+
+ /**
+ * How many milliseconds in the future should we schedule indexing to start
+ * when turning on indexing (and it was not previously active).
+ */
+ _INDEX_KICKOFF_DELAY: 200,
+
+ /**
+ * The time interval, in milliseconds, of pause between indexing batches. The
+ * maximum processor consumption is determined by this constant and the
+ * active |_cpuTargetIndexTime|.
+ *
+ * For current constants, that puts us at 50% while the user is active and 83%
+ * when idle.
+ */
+ _INDEX_INTERVAL: 32,
+
+ /**
+ * Number of indexing 'tokens' we are allowed to consume before yielding for
+ * each incremental pass. Consider a single token equal to indexing a single
+ * medium-sized message. This may be altered by user session (in)activity.
+ * Because we fetch message bodies, which is potentially asynchronous, this
+ * is not a precise knob to twiddle.
+ */
+ _indexTokens: 2,
+
+ /**
+ * Stopwatches used to measure performance during indexing, and during
+ * pauses between indexing. These help us adapt our indexing constants so
+ * as to not explode your computer. Kind of us, no?
+ */
+ _perfIndexStopwatch: null,
+ _perfPauseStopwatch: null,
+ /**
+ * Do we have an uncommitted indexer transaction that idle callback should commit?
+ */
+ _idleToCommit: false,
+ /**
+ * Target CPU time per batch of tokens, current value (milliseconds).
+ */
+ _cpuTargetIndexTime: 32,
+ /**
+ * Target CPU time per batch of tokens, during non-idle (milliseconds).
+ */
+ _CPU_TARGET_INDEX_TIME_ACTIVE: 32,
+ /**
+ * Target CPU time per batch of tokens, during idle (milliseconds).
+ */
+ _CPU_TARGET_INDEX_TIME_IDLE: 160,
+ /**
+ * Average CPU time per processed token (milliseconds).
+ */
+ _cpuAverageTimePerToken: 16,
+ /**
+ * Damping factor for _cpuAverageTimePerToken, as an approximate
+ * number of tokens to include in the average time.
+ */
+ _CPU_AVERAGE_TIME_DAMPING: 200,
+ /**
+ * Maximum tokens per batch. This is normally just a sanity check.
+ */
+ _CPU_MAX_TOKENS_PER_BATCH: 100,
+ /**
+ * CPU usage during a pause to declare that system was busy (milliseconds).
+ * This is typically set as 1.5 times the minimum resolution of the cpu
+ * usage clock, which is 16 milliseconds on Windows systems, and (I think)
+ * smaller on other systems, so we take the worst case.
+ */
+ _CPU_IS_BUSY_TIME: 24,
+ /**
+ * Time that return from pause may be late before the system is declared
+ * busy, in milliseconds. (Same issues as _CPU_IS_BUSY_TIME).
+ */
+ _PAUSE_LATE_IS_BUSY_TIME: 24,
+ /**
+ * Number of times that we will repeat a pause while waiting for a
+ * free CPU.
+ */
+ _PAUSE_REPEAT_LIMIT: 10,
+ /**
+ * Minimum time delay between commits, in milliseconds.
+ */
+ _MINIMUM_COMMIT_TIME: 5000,
+ /**
+ * Maximum time delay between commits, in milliseconds.
+ */
+ _MAXIMUM_COMMIT_TIME: 20000,
+
+ /**
+ * Unit testing hook to get us to emit additional logging that verges on
+ * inane for general usage but is helpful in unit test output to get a lay
+ * of the land and for paranoia reasons.
+ */
+ _unitTestSuperVerbose: false,
+ /**
+ * Unit test vector to get notified when a worker has a problem and it has
+ * a recover helper associated. This gets called with an argument
+ * indicating whether the recovery helper indicates recovery was possible.
+ */
+ _unitTestHookRecover: null,
+ /**
+ * Unit test vector to get notified when a worker runs into an exceptional
+ * situation (an exception propagates or gets explicitly killed) and needs
+ * to be cleaned up. This gets called with an argument indicating if there
+ * was a helper that was used or if we just did the default cleanup thing.
+ */
+ _unitTestHookCleanup: null,
+
+ /**
+ * Last commit time. Tracked to try and only commit at reasonable intervals.
+ */
+ _lastCommitTime: Date.now(),
+
+ _inited: false,
+ /**
+ * Initialize the indexer.
+ */
+ _init() {
+ if (this._inited) {
+ return;
+ }
+
+ this._inited = true;
+
+ this._callbackHandle.init();
+
+ if (Services.io.offline) {
+ this._suppressIndexing = true;
+ }
+
+ // create the timer that drives our intermittent indexing
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ // create the timer for larger offsets independent of indexing
+ this._longTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ this._idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(
+ Ci.nsIUserIdleService
+ );
+
+ // create our performance stopwatches
+ try {
+ this._perfIndexStopwatch = Cc["@mozilla.org/stopwatch;1"].createInstance(
+ Ci.nsIStopwatch
+ );
+ this._perfPauseStopwatch = Cc["@mozilla.org/stopwatch;1"].createInstance(
+ Ci.nsIStopwatch
+ );
+ } catch (ex) {
+ this._log.error("problem creating stopwatch!: " + ex);
+ }
+
+ // register for shutdown notifications
+ Services.obs.addObserver(this, "quit-application");
+
+ // figure out if event-driven indexing should be enabled...
+ let branch = Services.prefs.getBranch("mailnews.database.global.indexer.");
+ let eventDrivenEnabled = branch.getBoolPref("enabled", false);
+ let performInitialSweep = branch.getBoolPref("perform_initial_sweep", true);
+ // pretend we have already performed an initial sweep...
+ if (!performInitialSweep) {
+ this._initialSweepPerformed = true;
+ }
+
+ this.enabled = eventDrivenEnabled;
+ },
+
+ /**
+ * When shutdown, indexing immediately ceases and no further progress should
+ * be made. This flag goes true once, and never returns to false. Being
+ * in this state is a destructive thing from whence we cannot recover.
+ */
+ _indexerIsShutdown: false,
+
+ /**
+ * Shutdown the indexing process and datastore as quickly as possible in
+ * a synchronous fashion.
+ */
+ _shutdown() {
+ // no more timer events, please
+ try {
+ this._timer.cancel();
+ } catch (ex) {}
+ this._timer = null;
+ try {
+ this._longTimer.cancel();
+ } catch (ex) {}
+ this._longTimer = null;
+
+ this._perfIndexStopwatch = null;
+ this._perfPauseStopwatch = null;
+
+ // Remove listeners to avoid reference cycles on the off chance one of them
+ // holds a reference to the indexer object.
+ this._indexListeners = [];
+
+ this._indexerIsShutdown = true;
+
+ if (this.enabled) {
+ this._log.info("Shutting Down");
+ }
+
+ // don't let anything try and convince us to start indexing again
+ this.suppressIndexing = true;
+
+ // If there is an active job and it has a cleanup handler, run it.
+ if (this._curIndexingJob) {
+ let workerDef = this._curIndexingJob._workerDef;
+ try {
+ if (workerDef.cleanup) {
+ workerDef.cleanup.call(workerDef.indexer, this._curIndexingJob);
+ }
+ } catch (ex) {
+ this._log.error("problem during worker cleanup during shutdown.");
+ }
+ }
+ // Definitely clean out the async call stack and any associated data
+ this._callbackHandle.cleanup();
+ this._workBatchData = undefined;
+
+ // disable ourselves and all of the specific indexers
+ this.enabled = false;
+
+ lazy.GlodaDatastore.shutdown();
+ },
+
+ /**
+ * The list of indexers registered with us. If you are a core gloda indexer
+ * (you ship with gloda), then you can import this file directly and should
+ * make sure your indexer is imported in 'Everybody.jsm' in the right order.
+ * If you are not core gloda, then you should import 'GlodaPublic.jsm' and only
+ * then should you import 'GlodaIndexer.jsm' to get at GlodaIndexer.
+ */
+ _indexers: [],
+ /**
+ * Register an indexer with the Gloda indexing mechanism.
+ *
+ * @param aIndexer.name The name of your indexer.
+ * @param aIndexer.enable Your enable function. This will be called during
+ * the call to registerIndexer if Gloda indexing is already enabled. If
+ * indexing is not yet enabled, you will be called
+ * @param aIndexer.disable Your disable function. This will be called when
+ * indexing is disabled or we are shutting down. This will only be called
+ * if enable has already been called.
+ * @param aIndexer.workers A list of tuples of the form [worker type code,
+ * worker generator function, optional scheduling trigger function]. The
+ * type code is the string used to uniquely identify the job type. If you
+ * are not core gloda, your job type must start with your extension's name
+ * and a colon; you can collow that with anything you want. The worker
+ * generator is not easily explained in here. The trigger function is
+ * invoked immediately prior to calling the generator to create it. The
+ * trigger function takes the job as an argument and should perform any
+ * finalization required on the job. Most workers should not need to use
+ * the trigger function.
+ * @param aIndexer.initialSweep We call this to tell each indexer when it is
+ * its turn to run its indexing sweep. The idea of the indexing sweep is
+ * that this is when you traverse things eligible for indexing to make
+ * sure they are indexed. Right now we just call everyone at the same
+ * time and hope that their jobs don't fight too much.
+ */
+ registerIndexer(aIndexer) {
+ this._log.info("Registering indexer: " + aIndexer.name);
+ this._indexers.push(aIndexer);
+
+ try {
+ for (let workerInfo of aIndexer.workers) {
+ let workerCode = workerInfo[0];
+ let workerDef = workerInfo[1];
+ workerDef.name = workerCode;
+ workerDef.indexer = aIndexer;
+ this._indexerWorkerDefs[workerCode] = workerDef;
+ if (!("recover" in workerDef)) {
+ workerDef.recover = null;
+ }
+ if (!("cleanup" in workerDef)) {
+ workerDef.cleanup = null;
+ }
+ if (!("onSchedule" in workerDef)) {
+ workerDef.onSchedule = null;
+ }
+ if (!("jobCanceled" in workerDef)) {
+ workerDef.jobCanceled = null;
+ }
+ }
+ } catch (ex) {
+ this._log.warn("Helper indexer threw exception on worker enum.");
+ }
+
+ if (this._enabled) {
+ try {
+ aIndexer.enable();
+ } catch (ex) {
+ this._log.warn("Helper indexer threw exception on enable: " + ex);
+ }
+ }
+ },
+
+ /**
+ * Are we enabled, read: are we processing change events?
+ */
+ _enabled: false,
+ get enabled() {
+ return this._enabled;
+ },
+ set enabled(aEnable) {
+ if (!this._enabled && aEnable) {
+ // register for offline notifications
+ Services.obs.addObserver(this, "network:offline-status-changed");
+
+ // register for idle notification
+ this._idleService.addIdleObserver(this, this._indexIdleThresholdSecs);
+
+ this._enabled = true;
+
+ for (let indexer of this._indexers) {
+ try {
+ indexer.enable();
+ } catch (ex) {
+ this._log.warn("Helper indexer threw exception on enable: " + ex);
+ }
+ }
+
+ // if we have an accumulated desire to index things, kick it off again.
+ if (this._indexingDesired) {
+ this._indexingDesired = false; // it's edge-triggered for now
+ this.indexing = true;
+ }
+
+ // if we have not done an initial sweep, schedule scheduling one.
+ if (!this._initialSweepPerformed) {
+ this._longTimer.initWithCallback(
+ this._scheduleInitialSweep,
+ this._INITIAL_SWEEP_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ } else if (this._enabled && !aEnable) {
+ for (let indexer of this._indexers) {
+ try {
+ indexer.disable();
+ } catch (ex) {
+ this._log.warn("Helper indexer threw exception on disable: " + ex);
+ }
+ }
+
+ // remove offline observer
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+
+ // remove idle
+ this._idleService.removeIdleObserver(this, this._indexIdleThresholdSecs);
+
+ this._enabled = false;
+ }
+ },
+
+ /** Track whether indexing is desired (we have jobs to prosecute). */
+ _indexingDesired: false,
+ /**
+ * Track whether we have an actively pending callback or timer event. We do
+ * this so we don't experience a transient suppression and accidentally
+ * get multiple event-chains driving indexing at the same time (which the
+ * code will not handle correctly).
+ */
+ _indexingActive: false,
+ /**
+ * Indicates whether indexing is currently ongoing. This may return false
+ * while indexing activities are still active, but they will quiesce shortly.
+ */
+ get indexing() {
+ return this._indexingDesired && !this._suppressIndexing;
+ },
+ /** Indicates whether indexing is desired. */
+ get indexingDesired() {
+ return this._indexingDesired;
+ },
+ /**
+ * Set this to true to indicate there is indexing work to perform. This does
+ * not mean indexing will begin immediately (if it wasn't active), however.
+ * If suppressIndexing has been set, we won't do anything until indexing is
+ * no longer suppressed.
+ */
+ set indexing(aShouldIndex) {
+ if (!this._indexingDesired && aShouldIndex) {
+ this._indexingDesired = true;
+ if (this.enabled && !this._indexingActive && !this._suppressIndexing) {
+ this._log.info("+++ Indexing Queue Processing Commencing");
+ this._indexingActive = true;
+ this._timer.initWithCallback(
+ this._timerCallbackDriver,
+ this._INDEX_KICKOFF_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ }
+ },
+
+ _suppressIndexing: false,
+ /**
+ * Set whether or not indexing should be suppressed. This is to allow us to
+ * avoid running down a laptop's battery when it is not on AC. Only code
+ * in charge of regulating that tracking should be setting this variable; if
+ * other factors want to contribute to such a decision, this logic needs to
+ * be changed to track that, since last-write currently wins.
+ */
+ set suppressIndexing(aShouldSuppress) {
+ this._suppressIndexing = aShouldSuppress;
+
+ // re-start processing if we are no longer suppressing, there is work yet
+ // to do, and the indexing process had actually stopped.
+ if (
+ !this._suppressIndexing &&
+ this._indexingDesired &&
+ !this._indexingActive
+ ) {
+ this._log.info("+++ Indexing Queue Processing Resuming");
+ this._indexingActive = true;
+ this._timer.initWithCallback(
+ this._timerCallbackDriver,
+ this._INDEX_KICKOFF_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ },
+
+ /**
+ * Track whether an initial sweep has been performed. This mainly exists so
+ * that unit testing can stop us from performing an initial sweep.
+ */
+ _initialSweepPerformed: false,
+ /**
+ * Our timer-driven callback to schedule our first initial indexing sweep.
+ * Because it is invoked by an nsITimer it operates without the benefit of
+ * a 'this' context and must use GlodaIndexer instead of this.
+ * Since an initial sweep could have been performed before we get invoked,
+ * we need to check whether an initial sweep is still desired before trying
+ * to schedule one. We don't need to worry about whether one is active
+ * because the indexingSweepNeeded takes care of that.
+ */
+ _scheduleInitialSweep() {
+ if (GlodaIndexer._initialSweepPerformed) {
+ return;
+ }
+ GlodaIndexer._initialSweepPerformed = true;
+ for (let indexer of GlodaIndexer._indexers) {
+ indexer.initialSweep();
+ }
+ },
+
+ /**
+ * Our current job number. Meaningless value that increments with every job
+ * we process that resets to 0 when we run out of jobs. Currently used by
+ * the activity manager's gloda listener to tell when we have changed jobs.
+ * We really need a better listener mechanism.
+ */
+ _indexingJobCount: 0,
+
+ /**
+ * A list of IndexingJob instances to process.
+ */
+ _indexQueue: [],
+
+ /**
+ * The current indexing job.
+ */
+ _curIndexingJob: null,
+
+ /**
+ * The number of seconds before we declare the user idle and commit if
+ * needed.
+ */
+ _indexIdleThresholdSecs: 3,
+
+ _indexListeners: [],
+ /**
+ * Add an indexing progress listener. The listener will be notified of at
+ * least all major status changes (idle -> indexing, indexing -> idle), plus
+ * arbitrary progress updates during the indexing process.
+ * If indexing is not active when the listener is added, a synthetic idle
+ * notification will be generated.
+ *
+ * @param aListener A listener function, taking arguments: status (Gloda.
+ * kIndexer*), the folder name if a folder is involved (string or null),
+ * current zero-based job number (int),
+ * current item number being indexed in this job (int), total number
+ * of items in this job to be indexed (int).
+ *
+ * @TODO should probably allow for a 'this' value to be provided
+ * @TODO generalize to not be folder/message specific. use nouns!
+ */
+ addListener(aListener) {
+ // should we weakify?
+ if (!this._indexListeners.includes(aListener)) {
+ this._indexListeners.push(aListener);
+ }
+ // if we aren't indexing, give them an idle indicator, otherwise they can
+ // just be happy when we hit the next actual status point.
+ if (!this.indexing) {
+ aListener(GlodaConstants.kIndexerIdle, null, 0, 0, 1);
+ }
+ return aListener;
+ },
+ /**
+ * Remove the given listener so that it no longer receives indexing progress
+ * updates.
+ */
+ removeListener(aListener) {
+ let index = this._indexListeners.indexOf(aListener);
+ if (index != -1) {
+ this._indexListeners.splice(index, 1);
+ }
+ },
+ /**
+ * Helper method to tell listeners what we're up to. For code simplicity,
+ * the caller is just deciding when to send this update (preferably at
+ * reasonable intervals), and doesn't need to provide any indication of
+ * state... we figure that out ourselves.
+ *
+ * This was not pretty but got ugly once we moved the message indexing out
+ * to its own indexer. Some generalization is required but will likely
+ * require string hooks.
+ */
+ _notifyListeners() {
+ let status, prettyName, jobIndex, jobItemIndex, jobItemGoal, jobType;
+
+ if (this.indexing && this._curIndexingJob) {
+ let job = this._curIndexingJob;
+ status = GlodaConstants.kIndexerIndexing;
+
+ let indexer = this._indexerWorkerDefs[job.jobType].indexer;
+ if ("_indexingFolder" in indexer) {
+ prettyName =
+ indexer._indexingFolder != null
+ ? indexer._indexingFolder.prettyName
+ : null;
+ } else {
+ prettyName = null;
+ }
+
+ jobIndex = this._indexingJobCount - 1;
+ jobItemIndex = job.offset;
+ jobItemGoal = job.goal;
+ jobType = job.jobType;
+ } else {
+ status = GlodaConstants.kIndexerIdle;
+ prettyName = null;
+ jobIndex = 0;
+ jobItemIndex = 0;
+ jobItemGoal = 1;
+ jobType = null;
+ }
+
+ // Some people ascribe to the belief that the most you can give is 100%.
+ // We know better, but let's humor them.
+ if (jobItemIndex > jobItemGoal) {
+ jobItemGoal = jobItemIndex;
+ }
+
+ for (
+ let iListener = this._indexListeners.length - 1;
+ iListener >= 0;
+ iListener--
+ ) {
+ let listener = this._indexListeners[iListener];
+ try {
+ listener(
+ status,
+ prettyName,
+ jobIndex,
+ jobItemIndex,
+ jobItemGoal,
+ jobType
+ );
+ } catch (ex) {
+ this._log.error(ex);
+ }
+ }
+ },
+
+ /**
+ * A wrapped callback driver intended to be used by timers that provide
+ * arguments we really do not care about.
+ */
+ _timerCallbackDriver() {
+ GlodaIndexer.callbackDriver();
+ },
+
+ /**
+ * A simple callback driver wrapper to provide 'this'.
+ */
+ _wrapCallbackDriver(...aArgs) {
+ GlodaIndexer.callbackDriver(...aArgs);
+ },
+
+ /**
+ * The current processing 'batch' generator, produced by a call to workBatch()
+ * and used by callbackDriver to drive execution.
+ */
+ _batch: null,
+ _inCallback: false,
+ _savedCallbackArgs: null,
+ /**
+ * The root work-driver. callbackDriver creates workBatch generator instances
+ * (stored in _batch) which run until they are done (kWorkDone) or they
+ * (really the embedded activeIterator) encounter something asynchronous.
+ * The convention is that all the callback handlers end up calling us,
+ * ensuring that control-flow properly resumes. If the batch completes,
+ * we re-schedule ourselves after a time delay (controlled by _INDEX_INTERVAL)
+ * and return. (We use one-shot timers because repeating-slack does not
+ * know enough to deal with our (current) asynchronous nature.)
+ */
+ callbackDriver(...aArgs) {
+ // just bail if we are shutdown
+ if (this._indexerIsShutdown) {
+ return;
+ }
+
+ // it is conceivable that someone we call will call something that in some
+ // cases might be asynchronous, and in other cases immediately generate
+ // events without returning. In the interest of (stack-depth) sanity,
+ // let's handle this by performing a minimal time-delay callback.
+ // this is also now a good thing sequencing-wise. if we get our callback
+ // with data before the underlying function has yielded, we obviously can't
+ // cram the data in yet. Our options in this case are to either mark the
+ // fact that the callback has already happened and immediately return to
+ // the iterator when it does bubble up the kWorkAsync, or we can do as we
+ // have been doing, but save the
+ if (this._inCallback) {
+ this._savedCallbackArgs = aArgs;
+ this._timer.initWithCallback(
+ this._timerCallbackDriver,
+ 0,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return;
+ }
+ this._inCallback = true;
+
+ try {
+ if (this._batch === null) {
+ this._batch = this.workBatch();
+ }
+
+ // kWorkAsync, kWorkDone, kWorkPause are allowed out; kWorkSync is not
+ // On kWorkDone, we want to schedule another timer to fire on us if we are
+ // not done indexing. (On kWorkAsync, we don't care what happens, because
+ // someone else will be receiving the callback, and they will call us when
+ // they are done doing their thing.
+ let args;
+ if (this._savedCallbackArgs != null) {
+ args = this._savedCallbackArgs;
+ this._savedCallbackArgs = null;
+ } else {
+ args = aArgs;
+ }
+
+ let result;
+ if (args.length == 0) {
+ result = this._batch.next().value;
+ } else if (args.length == 1) {
+ result = this._batch.next(args[0]).value;
+ } else {
+ // Arguments works with destructuring assignment.
+ result = this._batch.next(args).value;
+ }
+ switch (result) {
+ // job's done, close the batch and re-schedule ourselves if there's more
+ // to do.
+ case GlodaConstants.kWorkDone:
+ this._batch.return();
+ this._batch = null;
+ // the batch wants to get re-scheduled, do so.
+ // (intentional fall-through to re-scheduling logic)
+ case GlodaConstants.kWorkPause:
+ if (this.indexing) {
+ this._timer.initWithCallback(
+ this._timerCallbackDriver,
+ this._INDEX_INTERVAL,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ } else {
+ // it's important to indicate no more callbacks are in flight
+ this._indexingActive = false;
+ }
+ break;
+ case GlodaConstants.kWorkAsync:
+ // there is nothing to do. some other code is now responsible for
+ // calling us.
+ break;
+ }
+ } finally {
+ this._inCallback = false;
+ }
+ },
+
+ _callbackHandle: {
+ init() {
+ this.wrappedCallback = GlodaIndexer._wrapCallbackDriver;
+ this.callbackThis = GlodaIndexer;
+ this.callback = GlodaIndexer.callbackDriver;
+ },
+ /**
+ * The stack of generators we are processing. The (numerically) last one is
+ * also the |activeIterator|.
+ */
+ activeStack: [],
+ /**
+ * The generator at the top of the |activeStack| and that we will call next
+ * or send on next if nothing changes.
+ */
+ activeIterator: null,
+ /**
+ * Meta-information about the generators at each level of the stack.
+ */
+ contextStack: [],
+ /**
+ * Push a new generator onto the stack. It becomes the active generator.
+ */
+ push(aIterator, aContext) {
+ this.activeStack.push(aIterator);
+ this.contextStack.push(aContext);
+ this.activeIterator = aIterator;
+ },
+ /**
+ * For use by generators that want to call another asynchronous process
+ * implemented as a generator. They should do
+ * "yield aCallbackHandle.pushAndGo(someGenerator(arg1, arg2));".
+ *
+ * @public
+ */
+ pushAndGo(aIterator, aContext) {
+ this.push(aIterator, aContext);
+ return GlodaConstants.kWorkSync;
+ },
+ /**
+ * Pop the active generator off the stack.
+ */
+ pop() {
+ this.activeIterator.return();
+ this.activeStack.pop();
+ this.contextStack.pop();
+ if (this.activeStack.length) {
+ this.activeIterator = this.activeStack[this.activeStack.length - 1];
+ } else {
+ this.activeIterator = null;
+ }
+ },
+ /**
+ * Someone propagated an exception and we need to clean-up all the active
+ * logic as best we can. Which is not really all that well.
+ *
+ * @param [aOptionalStopAtDepth=0] The length the stack should be when this
+ * method completes. Pass 0 or omit for us to clear everything out.
+ * Pass 1 to leave just the top-level generator intact.
+ */
+ cleanup(aOptionalStopAtDepth) {
+ if (aOptionalStopAtDepth === undefined) {
+ aOptionalStopAtDepth = 0;
+ }
+ while (this.activeStack.length > aOptionalStopAtDepth) {
+ this.pop();
+ }
+ },
+ /**
+ * For use when a generator finishes up by calling |doneWithResult| on us;
+ * the async driver calls this to pop that generator off the stack
+ * and get the result it passed in to its call to |doneWithResult|.
+ *
+ * @protected
+ */
+ popWithResult() {
+ this.pop();
+ let result = this._result;
+ this._result = null;
+ return result;
+ },
+ _result: null,
+ /**
+ * For use by generators that want to return a result to the calling
+ * asynchronous generator. Specifically, they should do
+ * "yield aCallbackHandle.doneWithResult(RESULT);".
+ *
+ * @public
+ */
+ doneWithResult(aResult) {
+ this._result = aResult;
+ return GlodaConstants.kWorkDoneWithResult;
+ },
+
+ /* be able to serve as a collection listener, resuming the active iterator's
+ last yield kWorkAsync */
+ onItemsAdded() {},
+ onItemsModified() {},
+ onItemsRemoved() {},
+ onQueryCompleted(aCollection) {
+ GlodaIndexer.callbackDriver();
+ },
+ },
+ _workBatchData: undefined,
+ /* eslint-disable complexity */
+ /**
+ * The workBatch generator handles a single 'batch' of processing, managing
+ * the database transaction and keeping track of "tokens". It drives the
+ * activeIterator generator which is doing the work.
+ * workBatch will only produce kWorkAsync, kWorkPause, and kWorkDone
+ * notifications. If activeIterator returns kWorkSync and there are still
+ * tokens available, workBatch will keep driving the activeIterator until it
+ * encounters a kWorkAsync (which workBatch will yield to callbackDriver), or
+ * it runs out of tokens and yields a kWorkPause or kWorkDone.
+ */
+ *workBatch() {
+ // Do we still have an open transaction? If not, start a new one.
+ if (!this._idleToCommit) {
+ lazy.GlodaDatastore._beginTransaction();
+ } else {
+ // We'll manage commit ourself while this routine is active.
+ this._idleToCommit = false;
+ }
+
+ this._perfIndexStopwatch.start();
+ let batchCount;
+ let haveMoreWork = true;
+ let transactionToCommit = true;
+ let inIdle;
+
+ let notifyDecimator = 0;
+
+ while (haveMoreWork) {
+ // Both explicit work activity points (sync + async) and transfer of
+ // control return (via kWorkDone*) results in a token being eaten. The
+ // idea now is to make tokens less precious so that the adaptive logic
+ // can adjust them with less impact. (Before this change, doing 1
+ // token's work per cycle ended up being an entire non-idle time-slice's
+ // work.)
+ // During this loop we track the clock real-time used even though we
+ // frequently yield to asynchronous operations. These asynchronous
+ // operations are either database queries or message streaming requests.
+ // Both may involve disk I/O but no network I/O (since we only stream
+ // messages that are already available offline), but in an ideal
+ // situation will come from cache and so the work this function kicks off
+ // will dominate.
+ // We do not use the CPU time to this end because...
+ // 1) Our timer granularity on linux is worse for CPU than for wall time.
+ // 2) That can fail to account for our I/O cost.
+ // 3) If something with a high priority / low latency need (like playing
+ // a video) is fighting us, although using CPU time will accurately
+ // express how much time we are actually spending to index, our goal
+ // is to control the duration of our time slices, not be "right" about
+ // the actual CPU cost. In that case, if we attempted to take on more
+ // work, we would likely interfere with the higher priority process or
+ // make ourselves less responsive by drawing out the period of time we
+ // are dominating the main thread.
+ this._perfIndexStopwatch.start();
+ // For telemetry purposes, we want to know how many messages we've been
+ // processing during that batch, and how long it took, pauses included.
+ let t0 = Date.now();
+ this._indexedMessageCount = 0;
+ batchCount = 0;
+ while (batchCount < this._indexTokens) {
+ if (
+ this._callbackHandle.activeIterator === null &&
+ !this._hireJobWorker()
+ ) {
+ haveMoreWork = false;
+ break;
+ }
+ batchCount++;
+
+ // XXX for performance, we may want to move the try outside the for loop
+ // with a quasi-redundant outer loop that shunts control back inside
+ // if we left the loop due to an exception (without consuming all the
+ // tokens.)
+ try {
+ switch (
+ this._callbackHandle.activeIterator.next(this._workBatchData).value
+ ) {
+ case GlodaConstants.kWorkSync:
+ this._workBatchData = undefined;
+ break;
+ case GlodaConstants.kWorkAsync:
+ this._workBatchData = yield GlodaConstants.kWorkAsync;
+ break;
+ case GlodaConstants.kWorkDone:
+ this._callbackHandle.pop();
+ this._workBatchData = undefined;
+ break;
+ case GlodaConstants.kWorkDoneWithResult:
+ this._workBatchData = this._callbackHandle.popWithResult();
+ break;
+ default:
+ break;
+ }
+ } catch (ex) {
+ this._log.debug("Exception in batch processing:", ex);
+ let workerDef = this._curIndexingJob._workerDef;
+ if (workerDef.recover) {
+ let recoverToDepth;
+ try {
+ recoverToDepth = workerDef.recover.call(
+ workerDef.indexer,
+ this._curIndexingJob,
+ this._callbackHandle.contextStack,
+ ex
+ );
+ } catch (ex2) {
+ this._log.error(
+ "Worker '" +
+ workerDef.name +
+ "' recovery function itself failed:",
+ ex2
+ );
+ }
+ if (this._unitTestHookRecover) {
+ this._unitTestHookRecover(
+ recoverToDepth,
+ ex,
+ this._curIndexingJob,
+ this._callbackHandle
+ );
+ }
+
+ if (recoverToDepth) {
+ this._callbackHandle.cleanup(recoverToDepth);
+ continue;
+ }
+ }
+ // (we either did not have a recover handler or it couldn't recover)
+ // call the cleanup helper if there is one
+ if (workerDef.cleanup) {
+ try {
+ workerDef.cleanup.call(workerDef.indexer, this._curIndexingJob);
+ } catch (ex2) {
+ this._log.error(
+ "Worker '" +
+ workerDef.name +
+ "' cleanup function itself failed:",
+ ex2
+ );
+ }
+ if (this._unitTestHookCleanup) {
+ this._unitTestHookCleanup(
+ true,
+ ex,
+ this._curIndexingJob,
+ this._callbackHandle
+ );
+ }
+ } else if (this._unitTestHookCleanup) {
+ this._unitTestHookCleanup(
+ false,
+ ex,
+ this._curIndexingJob,
+ this._callbackHandle
+ );
+ }
+
+ // Clean out everything on the async stack, warn about the job, kill.
+ // We do not log this warning lightly; it will break unit tests and
+ // be visible to users. Anything expected should likely have a
+ // recovery function or the cleanup logic should be extended to
+ // indicate that the failure is acceptable.
+ this._callbackHandle.cleanup();
+ this._log.warn(
+ "Problem during " + this._curIndexingJob + ", bailing:",
+ ex
+ );
+ this._curIndexingJob = null;
+ // the data must now be invalid
+ this._workBatchData = undefined;
+ }
+ }
+ this._perfIndexStopwatch.stop();
+
+ // idleTime can throw if there is no idle-provider available, such as an
+ // X session without the relevant extensions available. In this case
+ // we assume that the user is never idle.
+ try {
+ // We want to stop ASAP when leaving idle, so we can't rely on the
+ // standard polled callback. We do the polling ourselves.
+ if (this._idleService.idleTime < this._INDEX_IDLE_ADJUSTMENT_TIME) {
+ inIdle = false;
+ this._cpuTargetIndexTime = this._CPU_TARGET_INDEX_TIME_ACTIVE;
+ } else {
+ inIdle = true;
+ this._cpuTargetIndexTime = this._CPU_TARGET_INDEX_TIME_IDLE;
+ }
+ } catch (ex) {
+ inIdle = false;
+ }
+
+ // take a breather by having the caller re-schedule us sometime in the
+ // future, but only if we're going to perform another loop iteration.
+ if (haveMoreWork) {
+ notifyDecimator = (notifyDecimator + 1) % 32;
+ if (!notifyDecimator) {
+ this._notifyListeners();
+ }
+
+ for (
+ let pauseCount = 0;
+ pauseCount < this._PAUSE_REPEAT_LIMIT;
+ pauseCount++
+ ) {
+ this._perfPauseStopwatch.start();
+
+ yield GlodaConstants.kWorkPause;
+
+ this._perfPauseStopwatch.stop();
+ // We repeat the pause if the pause was longer than
+ // we expected, or if it used a significant amount
+ // of cpu, either of which indicate significant other
+ // activity.
+ if (
+ this._perfPauseStopwatch.cpuTimeSeconds * 1000 <
+ this._CPU_IS_BUSY_TIME &&
+ this._perfPauseStopwatch.realTimeSeconds * 1000 -
+ this._INDEX_INTERVAL <
+ this._PAUSE_LATE_IS_BUSY_TIME
+ ) {
+ break;
+ }
+ }
+ }
+
+ // All pauses have been taken, how effective were we? Report!
+ // XXX: there's possibly a lot of fluctuation since we go through here
+ // every 5 messages or even less
+ if (this._indexedMessageCount > 0) {
+ let delta = (Date.now() - t0) / 1000; // in seconds
+ let v = Math.round(this._indexedMessageCount / delta);
+ try {
+ let h = Services.telemetry.getHistogramById(
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S"
+ );
+ h.add(v);
+ } catch (e) {
+ this._log.warn("Couldn't report telemetry", e, v);
+ }
+ }
+
+ if (batchCount > 0) {
+ let totalTime = this._perfIndexStopwatch.realTimeSeconds * 1000;
+ let timePerToken = totalTime / batchCount;
+ // Damp the average time since it is a rough estimate only.
+ this._cpuAverageTimePerToken =
+ (totalTime +
+ this._CPU_AVERAGE_TIME_DAMPING * this._cpuAverageTimePerToken) /
+ (batchCount + this._CPU_AVERAGE_TIME_DAMPING);
+ // We use the larger of the recent or the average time per token, so
+ // that we can respond quickly to slow down indexing if there
+ // is a sudden increase in time per token.
+ let bestTimePerToken = Math.max(
+ timePerToken,
+ this._cpuAverageTimePerToken
+ );
+ // Always index at least one token!
+ this._indexTokens = Math.max(
+ 1,
+ this._cpuTargetIndexTime / bestTimePerToken
+ );
+ // But no more than the a maximum limit, just for sanity's sake.
+ this._indexTokens = Math.min(
+ this._CPU_MAX_TOKENS_PER_BATCH,
+ this._indexTokens
+ );
+ this._indexTokens = Math.ceil(this._indexTokens);
+ }
+
+ // Should we try to commit now?
+ let elapsed = Date.now() - this._lastCommitTime;
+ // Commit tends to cause a brief UI pause, so we try to delay it (but not
+ // forever) if the user is active. If we're done and idling, we'll also
+ // commit, otherwise we'll let the idle callback do it.
+ let doCommit =
+ transactionToCommit &&
+ (elapsed > this._MAXIMUM_COMMIT_TIME ||
+ (inIdle && (elapsed > this._MINIMUM_COMMIT_TIME || !haveMoreWork)));
+ if (doCommit) {
+ lazy.GlodaCollectionManager.cacheCommitDirty();
+ // Set up an async notification to happen after the commit completes so
+ // that we can avoid the indexer doing something with the database that
+ // causes the main thread to block against the completion of the commit
+ // (which can be a while) on 1.9.1.
+ lazy.GlodaDatastore.runPostCommit(this._callbackHandle.wrappedCallback);
+ // kick off the commit
+ lazy.GlodaDatastore._commitTransaction();
+ yield GlodaConstants.kWorkAsync;
+ this._lastCommitTime = Date.now();
+ // Restart the transaction if we still have work.
+ if (haveMoreWork) {
+ lazy.GlodaDatastore._beginTransaction();
+ } else {
+ transactionToCommit = false;
+ }
+ }
+ }
+
+ this._notifyListeners();
+
+ // If we still have a transaction to commit, tell idle to do the commit
+ // when it gets around to it.
+ if (transactionToCommit) {
+ this._idleToCommit = true;
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+ /* eslint-enable complexity */
+
+ /**
+ * Maps indexing job type names to a worker definition.
+ * The worker definition is an object with the following attributes where
+ * only worker is required:
+ * - worker:
+ * - onSchedule: A function to be invoked when the worker is scheduled. The
+ * job is passed as an argument.
+ * - recover:
+ * - cleanup:
+ */
+ _indexerWorkerDefs: {},
+ /**
+ * Perform the initialization step and return a generator if there is any
+ * steady-state processing to be had.
+ */
+ _hireJobWorker() {
+ // In no circumstances should there be data bouncing around from previous
+ // calls if we are here. |killActiveJob| depends on this.
+ this._workBatchData = undefined;
+
+ if (this._indexQueue.length == 0) {
+ this._log.info("--- Done indexing, disabling timer renewal.");
+
+ this._curIndexingJob = null;
+ this._indexingDesired = false;
+ this._indexingJobCount = 0;
+ return false;
+ }
+
+ let job = (this._curIndexingJob = this._indexQueue.shift());
+ this._indexingJobCount++;
+
+ let generator = null;
+
+ if (job.jobType in this._indexerWorkerDefs) {
+ let workerDef = this._indexerWorkerDefs[job.jobType];
+ job._workerDef = workerDef;
+
+ // Prior to creating the worker, call the scheduling trigger function
+ // if there is one. This is so that jobs can be finalized. The
+ // initial use case is event-driven message indexing that accumulates
+ // a list of messages to index but wants it locked down once we start
+ // processing the list.
+ if (workerDef.onSchedule) {
+ workerDef.onSchedule.call(workerDef.indexer, job);
+ }
+
+ generator = workerDef.worker.call(
+ workerDef.indexer,
+ job,
+ this._callbackHandle
+ );
+ } else {
+ // Nothing we can do about this. Be loud about it and try to schedule
+ // something else.
+ this._log.error("Unknown job type: " + job.jobType);
+ return this._hireJobWorker();
+ }
+
+ if (this._unitTestSuperVerbose) {
+ this._log.debug("Hired job of type: " + job.jobType);
+ }
+
+ this._notifyListeners();
+
+ if (generator) {
+ this._callbackHandle.push(generator);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Schedule a job for indexing.
+ */
+ indexJob(aJob) {
+ this._log.info("Queue-ing job for indexing: " + aJob.jobType);
+
+ this._indexQueue.push(aJob);
+ this.indexing = true;
+ },
+
+ /**
+ * Kill the active job. This means a few things:
+ * - Kill all the generators in the callbackHandle stack.
+ * - If we are currently waiting on an async return, we need to make sure it
+ * does not screw us up.
+ * - Make sure the job's cleanup function gets called if appropriate.
+ *
+ * The async return case is actually not too troublesome. Since there is an
+ * active indexing job and we are not (by fiat) in that call stack, we know
+ * that the callback driver is guaranteed to get triggered again somehow.
+ * The only issue is to make sure that _workBatchData does not end up with
+ * the data. We compel |_hireJobWorker| to erase it to this end.
+ *
+ * @note You MUST NOT call this function from inside a job or an async function
+ * on the callbackHandle's stack of generators. If you are in that
+ * situation, you should just throw an exception. At the very least,
+ * use a timeout to trigger us.
+ */
+ killActiveJob() {
+ // There is nothing to do if we have no job
+ if (!this._curIndexingJob) {
+ return;
+ }
+
+ // -- Blow away the stack with cleanup.
+ let workerDef = this._curIndexingJob._workerDef;
+ if (this._unitTestSuperVerbose) {
+ this._log.debug("Killing job of type: " + this._curIndexingJob.jobType);
+ }
+ if (this._unitTestHookCleanup) {
+ this._unitTestHookCleanup(
+ !!workerDef.cleanup,
+ "no exception, this was killActiveJob",
+ this._curIndexingJob,
+ this._callbackHandle
+ );
+ }
+ this._callbackHandle.cleanup();
+ if (workerDef.cleanup) {
+ workerDef.cleanup.call(workerDef.indexer, this._curIndexingJob);
+ }
+
+ // Eliminate the job.
+ this._curIndexingJob = null;
+ },
+
+ /**
+ * Purge all jobs that the filter function returns true for. This does not
+ * kill the active job, use |killActiveJob| to do that.
+ *
+ * Make sure to call this function before killActiveJob
+ *
+ * @param aFilterElimFunc A filter function that takes an |IndexingJob| and
+ * returns true if the job should be purged, false if it should not be.
+ * The filter sees the jobs in the order they are scheduled.
+ */
+ purgeJobsUsingFilter(aFilterElimFunc) {
+ for (let iJob = 0; iJob < this._indexQueue.length; iJob++) {
+ let job = this._indexQueue[iJob];
+
+ // If the filter says to, splice the job out of existence (and make sure
+ // to fixup iJob to compensate.)
+ if (aFilterElimFunc(job)) {
+ if (this._unitTestSuperVerbose) {
+ this._log.debug("Purging job of type: " + job.jobType);
+ }
+ this._indexQueue.splice(iJob--, 1);
+ let workerDef = this._indexerWorkerDefs[job.jobType];
+ if (workerDef.jobCanceled) {
+ workerDef.jobCanceled.call(workerDef.indexer, job);
+ }
+ }
+ }
+ },
+
+ /* *********** Event Processing *********** */
+ observe(aSubject, aTopic, aData) {
+ // idle
+ if (aTopic == "idle") {
+ // Do we need to commit an indexer transaction?
+ if (this._idleToCommit) {
+ this._idleToCommit = false;
+ lazy.GlodaCollectionManager.cacheCommitDirty();
+ lazy.GlodaDatastore._commitTransaction();
+ this._lastCommitTime = Date.now();
+ this._notifyListeners();
+ }
+ } else if (aTopic == "network:offline-status-changed") {
+ // offline status
+ if (aData == "offline") {
+ this.suppressIndexing = true;
+ } else {
+ // online
+ this.suppressIndexing = false;
+ }
+ } else if (aTopic == "quit-application") {
+ // shutdown fallback
+ this._shutdown();
+ }
+ },
+};
+// we used to initialize here; now we have GlodaPublic.jsm do it for us after the
+// indexers register themselves so we know about all our built-in indexers
+// at init-time.
diff --git a/comm/mailnews/db/gloda/modules/GlodaMsgIndexer.jsm b/comm/mailnews/db/gloda/modules/GlodaMsgIndexer.jsm
new file mode 100644
index 0000000000..54ceacb59a
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaMsgIndexer.jsm
@@ -0,0 +1,310 @@
+/* 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 = ["GlodaABIndexer", "GlodaABAttrs"];
+
+const { GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { GlodaIndexer, IndexingJob } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+const { FreeTagNoun } = ChromeUtils.import(
+ "resource:///modules/gloda/NounFreetag.jsm"
+);
+
+var GlodaABIndexer = {
+ _log: null,
+ _notifications: [
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-deleted",
+ ],
+
+ name: "index_ab",
+ enable() {
+ if (this._log == null) {
+ this._log = console.createInstance({
+ prefix: "gloda.index_ab",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+ }
+
+ for (let topic of this._notifications) {
+ Services.obs.addObserver(this, topic);
+ }
+ },
+
+ disable() {
+ for (let topic of this._notifications) {
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ // it's a getter so we can reference 'this'
+ get workers() {
+ return [
+ [
+ "ab-card",
+ {
+ worker: this._worker_index_card,
+ },
+ ],
+ ];
+ },
+
+ *_worker_index_card(aJob, aCallbackHandle) {
+ let card = aJob.id;
+
+ if (card.primaryEmail) {
+ // load the identity
+ let query = Gloda.newQuery(GlodaConstants.NOUN_IDENTITY);
+ query.kind("email");
+ // we currently normalize all e-mail addresses to be lowercase
+ query.value(card.primaryEmail.toLowerCase());
+ let identityCollection = query.getCollection(aCallbackHandle);
+ yield GlodaConstants.kWorkAsync;
+
+ if (identityCollection.items.length) {
+ let identity = identityCollection.items[0];
+ // force the identity to know it has an associated ab card.
+ identity._hasAddressBookCard = true;
+
+ this._log.debug("Found identity, processing card.");
+ yield aCallbackHandle.pushAndGo(
+ Gloda.grokNounItem(
+ identity.contact,
+ { card },
+ false,
+ false,
+ aCallbackHandle
+ )
+ );
+ this._log.debug("Done processing card.");
+ }
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ initialSweep() {},
+
+ observe(subject, topic, data) {
+ subject.QueryInterface(Ci.nsIAbCard);
+
+ switch (topic) {
+ case "addrbook-contact-created": {
+ // When an address book card is added, update the cached GlodaIdentity
+ // object's cached idea of whether the identity has an ab card.
+ this._log.debug("Received Card Add Notification");
+
+ let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
+ GlodaConstants.NOUN_IDENTITY,
+ "email@" + subject.primaryEmail.toLowerCase()
+ );
+ if (identity) {
+ identity._hasAddressBookCard = true;
+ }
+ break;
+ }
+ case "addrbook-contact-updated": {
+ this._log.debug("Received Card Change Notification");
+
+ let job = new IndexingJob("ab-card", subject);
+ GlodaIndexer.indexJob(job);
+ break;
+ }
+ case "addrbook-contact-deleted": {
+ // When an address book card is added, update the cached GlodaIdentity
+ // object's cached idea of whether the identity has an ab card.
+ this._log.debug("Received Card Removal Notification");
+
+ let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
+ GlodaConstants.NOUN_IDENTITY,
+ "email@" + subject.primaryEmail.toLowerCase()
+ );
+ if (identity) {
+ identity._hasAddressBookCard = false;
+ }
+ break;
+ }
+ }
+ },
+};
+GlodaIndexer.registerIndexer(GlodaABIndexer);
+
+var GlodaABAttrs = {
+ providerName: "gloda.ab_attr",
+ _log: null,
+
+ init() {
+ this._log = console.createInstance({
+ prefix: "gloda.abattrs",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ });
+
+ try {
+ this.defineAttributes();
+ } catch (ex) {
+ this._log.error("Error in init: " + ex);
+ throw ex;
+ }
+ },
+
+ defineAttributes() {
+ /* ***** Contacts ***** */
+ this._attrIdentityContact = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "identities",
+ singular: false,
+ special: GlodaConstants.kSpecialColumnChildren,
+ // specialColumnName: "contactID",
+ storageAttributeName: "_identities",
+ subjectNouns: [GlodaConstants.NOUN_CONTACT],
+ objectNoun: GlodaConstants.NOUN_IDENTITY,
+ }); // tested-by: test_attributes_fundamental
+ this._attrContactName = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "name",
+ singular: true,
+ special: GlodaConstants.kSpecialString,
+ specialColumnName: "name",
+ subjectNouns: [GlodaConstants.NOUN_CONTACT],
+ objectNoun: GlodaConstants.NOUN_STRING,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+ this._attrContactPopularity = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "popularity",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "popularity",
+ subjectNouns: [GlodaConstants.NOUN_CONTACT],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ }); // not-tested
+ this._attrContactFrecency = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "frecency",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "frecency",
+ subjectNouns: [GlodaConstants.NOUN_CONTACT],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ }); // not-tested
+
+ /* ***** Identities ***** */
+ this._attrIdentityContact = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrDerived,
+ attributeName: "contact",
+ singular: true,
+ special: GlodaConstants.kSpecialColumnParent,
+ specialColumnName: "contactID", // the column in the db
+ idStorageAttributeName: "_contactID",
+ valueStorageAttributeName: "_contact",
+ subjectNouns: [GlodaConstants.NOUN_IDENTITY],
+ objectNoun: GlodaConstants.NOUN_CONTACT,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+ this._attrIdentityKind = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "kind",
+ singular: true,
+ special: GlodaConstants.kSpecialString,
+ specialColumnName: "kind",
+ subjectNouns: [GlodaConstants.NOUN_IDENTITY],
+ objectNoun: GlodaConstants.NOUN_STRING,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+ this._attrIdentityValue = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "value",
+ singular: true,
+ special: GlodaConstants.kSpecialString,
+ specialColumnName: "value",
+ subjectNouns: [GlodaConstants.NOUN_IDENTITY],
+ objectNoun: GlodaConstants.NOUN_STRING,
+ canQuery: true,
+ }); // tested-by: test_attributes_fundamental
+
+ /* ***** Contact Meta ***** */
+ // Freeform tags; not explicit like thunderbird's fundamental tags.
+ // we differentiate for now because of fundamental implementation
+ // differences.
+ this._attrFreeTag = Gloda.defineAttribute({
+ provider: this,
+ extensionName: GlodaConstants.BUILT_IN,
+ attributeType: GlodaConstants.kAttrExplicit,
+ attributeName: "freetag",
+ bind: true,
+ bindName: "freeTags",
+ singular: false,
+ subjectNouns: [GlodaConstants.NOUN_CONTACT],
+ objectNoun: Gloda.lookupNoun("freetag"),
+ parameterNoun: null,
+ canQuery: true,
+ }); // not-tested
+ // we need to find any existing bound freetag attributes, and use them to
+ // populate to FreeTagNoun's understanding
+ if ("parameterBindings" in this._attrFreeTag) {
+ for (let freeTagName in this._attrFreeTag.parameterBindings) {
+ this._log.debug("Telling FreeTagNoun about: " + freeTagName);
+ FreeTagNoun.getFreeTag(freeTagName);
+ }
+ }
+ },
+
+ *process(aContact, aRawReps, aIsNew, aCallbackHandle) {
+ let card = aRawReps.card;
+ if (aContact.NOUN_ID != GlodaConstants.NOUN_CONTACT) {
+ this._log.warn("Somehow got a non-contact: " + aContact);
+ return; // this will produce an exception; we like.
+ }
+
+ // update the name
+ if (card.displayName && card.displayName != aContact.name) {
+ aContact.name = card.displayName;
+ }
+
+ aContact.freeTags = [];
+
+ let tags = null;
+ try {
+ tags = card.getProperty("Categories", null);
+ } catch (ex) {
+ this._log.error("Problem accessing property: " + ex);
+ }
+ if (tags) {
+ for (let tagName of tags.split(",")) {
+ tagName = tagName.trim();
+ if (tagName) {
+ aContact.freeTags.push(FreeTagNoun.getFreeTag(tagName));
+ }
+ }
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaMsgSearcher.jsm b/comm/mailnews/db/gloda/modules/GlodaMsgSearcher.jsm
new file mode 100644
index 0000000000..f81def2560
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaMsgSearcher.jsm
@@ -0,0 +1,361 @@
+/* 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 = ["GlodaMsgSearcher"];
+
+const { Gloda } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaPublic.jsm"
+);
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+/**
+ * How much time boost should a 'score point' amount to? The authoritative,
+ * incontrivertible answer, across all time and space, is a week.
+ * Note that gloda stores timestamps as PRTimes for no exceedingly good
+ * reason.
+ */
+var FUZZSCORE_TIMESTAMP_FACTOR = 1000 * 1000 * 60 * 60 * 24 * 7;
+
+var RANK_USAGE = "glodaRank(matchinfo(messagesText), 1.0, 2.0, 2.0, 1.5, 1.5)";
+
+var DASCORE =
+ "(((" +
+ RANK_USAGE +
+ " + messages.notability) * " +
+ FUZZSCORE_TIMESTAMP_FACTOR +
+ ") + messages.date)";
+
+/**
+ * A new optimization decision we are making is that we do not want to carry
+ * around any data in our ephemeral tables that is not used for whittling the
+ * result set. The idea is that the btree page cache or OS cache is going to
+ * save us from the disk seeks and carrying around the extra data is just going
+ * to be CPU/memory churn that slows us down.
+ *
+ * Additionally, we try and avoid row lookups that would have their results
+ * discarded by the LIMIT. Because of limitations in FTS3 (which might
+ * be addressed in FTS4 by a feature request), we can't avoid the 'messages'
+ * lookup since that has the message's date and static notability but we can
+ * defer the 'messagesText' lookup.
+ *
+ * This is the access pattern we are after here:
+ * 1) Order the matches with minimized lookup and result storage costs.
+ * - The innermost MATCH does the doclist magic and provides us with
+ * matchinfo() support which does not require content row retrieval
+ * from messagesText. Unfortunately, this is not enough to whittle anything
+ * because we still need static interestingness, so...
+ * - Based on the match we retrieve the date and notability for that row from
+ * 'messages' using this in conjunction with matchinfo() to provide a score
+ * that we can then use to LIMIT our results.
+ * 2) We reissue the MATCH query so that we will be able to use offsets(), but
+ * we intersect the results of this MATCH against our LIMITed results from
+ * step 1.
+ * - We use 'docid IN (phase 1 query)' to accomplish this because it results in
+ * efficient lookup. If we just use a join, we get O(mn) performance because
+ * a cartesian join ends up being performed where either we end up performing
+ * the fulltext query M times and table scan intersect with the results from
+ * phase 1 or we do the fulltext once but traverse the entire result set from
+ * phase 1 N times.
+ * - We believe that the re-execution of the MATCH query should have no disk
+ * costs because it should still be cached by SQLite or the OS. In the case
+ * where memory is so constrained this is not true our behavior is still
+ * probably preferable than the old way because that would have caused lots
+ * of swapping.
+ * - This part of the query otherwise resembles the basic gloda query but with
+ * the inclusion of the offsets() invocation. The messages table lookup
+ * should not involve any disk traffic because the pages should still be
+ * cached (SQLite or OS) from phase 1. The messagesText lookup is new, and
+ * this is the major disk-seek reduction optimization we are making. (Since
+ * we avoid this lookup for all of the documents that were excluded by the
+ * LIMIT.) Since offsets() also needs to retrieve the row from messagesText
+ * there is a nice synergy there.
+ */
+var NUEVO_FULLTEXT_SQL =
+ "SELECT messages.*, messagesText.*, offsets(messagesText) AS osets " +
+ "FROM messagesText, messages " +
+ "WHERE" +
+ " messagesText MATCH ?1 " +
+ " AND messagesText.docid IN (" +
+ "SELECT docid " +
+ "FROM messagesText JOIN messages ON messagesText.docid = messages.id " +
+ "WHERE messagesText MATCH ?1 " +
+ "ORDER BY " +
+ DASCORE +
+ " DESC " +
+ "LIMIT ?2" +
+ " )" +
+ " AND messages.id = messagesText.docid " +
+ " AND +messages.deleted = 0" +
+ " AND +messages.folderID IS NOT NULL" +
+ " AND +messages.messageKey IS NOT NULL";
+
+function identityFunc(x) {
+ return x;
+}
+
+function oneLessMaxZero(x) {
+ if (x <= 1) {
+ return 0;
+ }
+ return x - 1;
+}
+
+function reduceSum(accum, curValue) {
+ return accum + curValue;
+}
+
+/*
+ * Columns are: body, subject, attachment names, author, recipients
+ */
+
+/**
+ * Scores if all search terms match in a column. We bias against author
+ * slightly and recipient a bit more in this case because a search that
+ * entirely matches just on a person should give a mention of that person
+ * in the subject or attachment a fighting chance.
+ * Keep in mind that because of our indexing in the face of address book
+ * contacts (namely, we index the name used in the e-mail as well as the
+ * display name on the address book card associated with the e-mail address)
+ * a contact is going to bias towards matching multiple times.
+ */
+var COLUMN_ALL_MATCH_SCORES = [4, 20, 20, 16, 12];
+/**
+ * Score for each distinct term that matches in the column. This is capped
+ * by COLUMN_ALL_SCORES.
+ */
+var COLUMN_PARTIAL_PER_MATCH_SCORES = [1, 4, 4, 4, 3];
+/**
+ * If a term matches multiple times, what is the marginal score for each
+ * additional match. We count the total number of matches beyond the
+ * first match for each term. In other words, if we have 3 terms which
+ * matched 5, 3, and 0 times, then the total from our perspective is
+ * (5 - 1) + (3 - 1) + 0 = 4 + 2 + 0 = 6. We take the minimum of that value
+ * and the value in COLUMN_MULTIPLE_MATCH_LIMIT and multiply by the value in
+ * COLUMN_MULTIPLE_MATCH_SCORES.
+ */
+var COLUMN_MULTIPLE_MATCH_SCORES = [1, 0, 0, 0, 0];
+var COLUMN_MULTIPLE_MATCH_LIMIT = [10, 0, 0, 0, 0];
+
+/**
+ * Score the message on its offsets (from stashedColumns).
+ */
+function scoreOffsets(aMessage, aContext) {
+ let score = 0;
+
+ let termTemplate = aContext.terms.map(_ => 0);
+ // for each column, a list of the incidence of each term
+ let columnTermIncidence = [
+ termTemplate.concat(),
+ termTemplate.concat(),
+ termTemplate.concat(),
+ termTemplate.concat(),
+ termTemplate.concat(),
+ ];
+
+ // we need a friendlyParseInt because otherwise the radix stuff happens
+ // because of the extra arguments map parses. curse you, map!
+ let offsetNums = aContext.stashedColumns[aMessage.id][0]
+ .split(" ")
+ .map(x => parseInt(x));
+ for (let i = 0; i < offsetNums.length; i += 4) {
+ let columnIndex = offsetNums[i];
+ let termIndex = offsetNums[i + 1];
+ columnTermIncidence[columnIndex][termIndex]++;
+ }
+
+ for (let iColumn = 0; iColumn < COLUMN_ALL_MATCH_SCORES.length; iColumn++) {
+ let termIncidence = columnTermIncidence[iColumn];
+ if (termIncidence.every(identityFunc)) {
+ // Bestow all match credit.
+ score += COLUMN_ALL_MATCH_SCORES[iColumn];
+ } else if (termIncidence.some(identityFunc)) {
+ // Bestow partial match credit.
+ score += Math.min(
+ COLUMN_ALL_MATCH_SCORES[iColumn],
+ COLUMN_PARTIAL_PER_MATCH_SCORES[iColumn] *
+ termIncidence.filter(identityFunc).length
+ );
+ }
+ // Bestow multiple match credit.
+ score +=
+ Math.min(
+ termIncidence.map(oneLessMaxZero).reduce(reduceSum, 0),
+ COLUMN_MULTIPLE_MATCH_LIMIT[iColumn]
+ ) * COLUMN_MULTIPLE_MATCH_SCORES[iColumn];
+ }
+
+ return score;
+}
+
+/**
+ * The searcher basically looks like a query, but is specialized for fulltext
+ * search against messages. Most of the explicit specialization involves
+ * crafting a SQL query that attempts to order the matches by likelihood that
+ * the user was looking for it. This is based on full-text matches combined
+ * with an explicit (generic) interest score value placed on the message at
+ * indexing time. This is followed by using the more generic gloda scoring
+ * mechanism to explicitly score the messages given the search context in
+ * addition to the more generic score adjusting rules.
+ */
+function GlodaMsgSearcher(aListener, aSearchString, aAndTerms) {
+ this.listener = aListener;
+
+ this.searchString = aSearchString;
+ this.fulltextTerms = this.parseSearchString(aSearchString);
+ this.andTerms = aAndTerms != null ? aAndTerms : true;
+
+ this.query = null;
+ this.collection = null;
+
+ this.scores = null;
+}
+GlodaMsgSearcher.prototype = {
+ /**
+ * Number of messages to retrieve initially.
+ */
+ get retrievalLimit() {
+ return Services.prefs.getIntPref(
+ "mailnews.database.global.search.msg.limit"
+ );
+ },
+
+ /**
+ * Parse the string into terms/phrases by finding matching double-quotes.
+ */
+ parseSearchString(aSearchString) {
+ aSearchString = aSearchString.trim();
+ let terms = [];
+
+ /*
+ * Add the term as long as the trim on the way in didn't obliterate it.
+ *
+ * In the future this might have other helper logic; it did once before.
+ */
+ function addTerm(aTerm) {
+ if (aTerm) {
+ terms.push(aTerm);
+ }
+ }
+
+ while (aSearchString) {
+ if (aSearchString.startsWith('"')) {
+ let endIndex = aSearchString.indexOf(aSearchString[0], 1);
+ // eat the quote if it has no friend
+ if (endIndex == -1) {
+ aSearchString = aSearchString.substring(1);
+ continue;
+ }
+
+ addTerm(aSearchString.substring(1, endIndex).trim());
+ aSearchString = aSearchString.substring(endIndex + 1);
+ continue;
+ }
+
+ let spaceIndex = aSearchString.indexOf(" ");
+ if (spaceIndex == -1) {
+ addTerm(aSearchString);
+ break;
+ }
+
+ addTerm(aSearchString.substring(0, spaceIndex));
+ aSearchString = aSearchString.substring(spaceIndex + 1);
+ }
+
+ return terms;
+ },
+
+ buildFulltextQuery() {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noMagic: true,
+ explicitSQL: NUEVO_FULLTEXT_SQL,
+ limitClauseAlreadyIncluded: true,
+ // osets is 0-based column number 14 (volatile to column changes)
+ // save the offset column for extra analysis
+ stashColumns: [14],
+ });
+
+ let fulltextQueryString = "";
+
+ for (let [iTerm, term] of this.fulltextTerms.entries()) {
+ if (iTerm) {
+ fulltextQueryString += this.andTerms ? " " : " OR ";
+ }
+
+ // Put our term in quotes. This is needed for the tokenizer to be able
+ // to do useful things. The exception is people clever enough to use
+ // NEAR.
+ if (/^NEAR(\/\d+)?$/.test(term)) {
+ fulltextQueryString += term;
+ } else if (term.length == 1 && term.charCodeAt(0) >= 0x2000) {
+ // This is a single-character CJK search query, so add a wildcard.
+ // Our tokenizer treats anything at/above 0x2000 as CJK for now.
+ fulltextQueryString += term + "*";
+ } else if (
+ (term.length == 2 &&
+ term.charCodeAt(0) >= 0x2000 &&
+ term.charCodeAt(1) >= 0x2000) ||
+ term.length >= 3
+ ) {
+ fulltextQueryString += '"' + term + '"';
+ }
+ }
+
+ query.fulltextMatches(fulltextQueryString);
+ query.limit(this.retrievalLimit);
+
+ return query;
+ },
+
+ getCollection(aListenerOverride, aData) {
+ if (aListenerOverride) {
+ this.listener = aListenerOverride;
+ }
+
+ this.query = this.buildFulltextQuery();
+ this.collection = this.query.getCollection(this, aData);
+ this.completed = false;
+
+ return this.collection;
+ },
+
+ sortBy: "-dascore",
+
+ onItemsAdded(aItems, aCollection) {
+ let newScores = Gloda.scoreNounItems(
+ aItems,
+ {
+ terms: this.fulltextTerms,
+ stashedColumns: aCollection.stashedColumns,
+ },
+ [scoreOffsets]
+ );
+ if (this.scores) {
+ this.scores = this.scores.concat(newScores);
+ } else {
+ this.scores = newScores;
+ }
+
+ if (this.listener) {
+ this.listener.onItemsAdded(aItems, aCollection);
+ }
+ },
+ onItemsModified(aItems, aCollection) {
+ if (this.listener) {
+ this.listener.onItemsModified(aItems, aCollection);
+ }
+ },
+ onItemsRemoved(aItems, aCollection) {
+ if (this.listener) {
+ this.listener.onItemsRemoved(aItems, aCollection);
+ }
+ },
+ onQueryCompleted(aCollection) {
+ this.completed = true;
+ if (this.listener) {
+ this.listener.onQueryCompleted(aCollection);
+ }
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaPublic.jsm b/comm/mailnews/db/gloda/modules/GlodaPublic.jsm
new file mode 100644
index 0000000000..555a6d8921
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaPublic.jsm
@@ -0,0 +1,45 @@
+/* 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 = ["Gloda"];
+
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+/* nothing to import, just run some code */ ChromeUtils.import(
+ "resource:///modules/gloda/Everybody.jsm"
+);
+const { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+// initialize the indexer! (who was actually imported as a nested dep by the
+// things Everybody.jsm imported.) We waited until now so it could know about
+// its indexers.
+GlodaIndexer._init();
+const { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+
+/**
+ * Expose some junk
+ */
+function proxy(aSourceObj, aSourceAttr, aDestObj, aDestAttr) {
+ aDestObj[aDestAttr] = function (...aArgs) {
+ return aSourceObj[aSourceAttr](...aArgs);
+ };
+}
+
+proxy(GlodaIndexer, "addListener", Gloda, "addIndexerListener");
+proxy(GlodaIndexer, "removeListener", Gloda, "removeIndexerListener");
+proxy(GlodaMsgIndexer, "isMessageIndexed", Gloda, "isMessageIndexed");
+proxy(
+ GlodaMsgIndexer,
+ "setFolderIndexingPriority",
+ Gloda,
+ "setFolderIndexingPriority"
+);
+proxy(
+ GlodaMsgIndexer,
+ "resetFolderIndexingPriority",
+ Gloda,
+ "resetFolderIndexingPriority"
+);
diff --git a/comm/mailnews/db/gloda/modules/GlodaQueryClassFactory.jsm b/comm/mailnews/db/gloda/modules/GlodaQueryClassFactory.jsm
new file mode 100644
index 0000000000..2e53cf5925
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaQueryClassFactory.jsm
@@ -0,0 +1,642 @@
+/* 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 = ["GlodaQueryClassFactory"];
+
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+/**
+ * @class Query class core; each noun gets its own sub-class where attributes
+ * have helper methods bound.
+ *
+ * @param aOptions A dictionary of options. Current legal options are:
+ * - noMagic: Indicates that the noun's dbQueryJoinMagic should be ignored.
+ * Currently, this means that messages will not have their
+ * full-text indexed values re-attached. This is planned to be
+ * offset by having queries/cache lookups that do not request
+ * noMagic to ensure that their data does get loaded.
+ * - explicitSQL: A hand-rolled alternate representation for the core
+ * SELECT portion of the SQL query. The queryFromQuery logic still
+ * generates its normal query, we just ignore its result in favor of
+ * your provided value. This means that the positional parameter
+ * list is still built and you should/must rely on those bound
+ * parameters (using '?'). The replacement occurs prior to the
+ * outerWrapColumns, ORDER BY, and LIMIT contributions to the query.
+ * - outerWrapColumns: If provided, wraps the query in a "SELECT *,blah
+ * FROM (actual query)" where blah is your list of outerWrapColumns
+ * made comma-delimited. The idea is that this allows you to
+ * reference the result of expressions inside the query using their
+ * names rather than having to duplicate the logic. In practice,
+ * this makes things more readable but is unlikely to improve
+ * performance. (Namely, my use of 'offsets' for full-text stuff
+ * ends up in the EXPLAIN plan twice despite this.)
+ * - noDbQueryValidityConstraints: Indicates that any validity constraints
+ * should be ignored. This should be used when you need to get every
+ * match regardless of whether it's valid.
+ *
+ * @property _owner The query instance that holds the list of unions...
+ * @property _constraints A list of (lists of OR constraints) that are ANDed
+ * together. For example [[FROM bob, FROM jim], [DATE last week]] would
+ * be requesting us to find all the messages from either bob or jim, and
+ * sent in the last week.
+ * @property _unions A list of other queries whose results are unioned with our
+ * own. There is no concept of nesting or sub-queries apart from this
+ * mechanism.
+ */
+function GlodaQueryClass(aOptions) {
+ this.options = aOptions != null ? aOptions : {};
+
+ // if we are an 'or' clause, who is our parent whom other 'or' clauses should
+ // spawn from...
+ this._owner = null;
+ // our personal chain of and-ing.
+ this._constraints = [];
+ // the other instances we union with
+ this._unions = [];
+
+ this._order = [];
+ this._limit = 0;
+}
+
+GlodaQueryClass.prototype = {
+ WILDCARD: {},
+
+ get constraintCount() {
+ return this._constraints.length;
+ },
+
+ or() {
+ let owner = this._owner || this;
+ let orQuery = new this._queryClass();
+ orQuery._owner = owner;
+ owner._unions.push(orQuery);
+ return orQuery;
+ },
+
+ orderBy(...aArgs) {
+ this._order.push(...aArgs);
+ return this;
+ },
+
+ limit(aLimit) {
+ this._limit = aLimit;
+ return this;
+ },
+
+ /**
+ * Return a collection asynchronously populated by this collection. You must
+ * provide a listener to receive notifications from the collection as it
+ * receives updates. The listener object should implement onItemsAdded,
+ * onItemsModified, and onItemsRemoved methods, all of which take a single
+ * argument which is the list of items which have been added, modified, or
+ * removed respectively.
+ *
+ * @param aListener The collection listener.
+ * @param [aData] The data attribute to set on the collection.
+ * @param [aArgs.becomeExplicit] Make the collection explicit so that the
+ * collection will only ever contain results found from the database
+ * query and the query will not be updated as new items are indexed that
+ * also match the query.
+ * @param [aArgs.becomeNull] Change the collection's query to a null query so
+ * that it will never receive any additional added/modified/removed events
+ * apart from the underlying database query. This is really only intended
+ * for gloda internal use but may be acceptable for non-gloda use. Please
+ * ask on mozilla.dev.apps.thunderbird first to make sure there isn't a
+ * better solution for your use-case. (Note: removals will still happen
+ * when things get fully deleted.)
+ */
+ getCollection(aListener, aData, aArgs) {
+ this.completed = false;
+ return this._nounDef.datastore.queryFromQuery(
+ this,
+ aListener,
+ aData,
+ /* aExistingCollection */ null,
+ /* aMasterCollection */ null,
+ aArgs
+ );
+ },
+
+ /* eslint-disable complexity */
+ /**
+ * Test whether the given first-class noun instance satisfies this query.
+ *
+ * @testpoint gloda.query.test
+ */
+ test(aObj) {
+ // when changing this method, be sure that GlodaDatastore's queryFromQuery
+ // method likewise has any required changes made.
+ let unionQueries = [this].concat(this._unions);
+
+ for (let iUnion = 0; iUnion < unionQueries.length; iUnion++) {
+ let curQuery = unionQueries[iUnion];
+
+ // assume success until a specific (or) constraint proves us wrong
+ let querySatisfied = true;
+ for (
+ let iConstraint = 0;
+ iConstraint < curQuery._constraints.length;
+ iConstraint++
+ ) {
+ let constraint = curQuery._constraints[iConstraint];
+ let [constraintType, attrDef] = constraint;
+ let boundName = attrDef ? attrDef.boundName : "id";
+ if (
+ boundName in aObj &&
+ aObj[boundName] === GlodaConstants.IGNORE_FACET
+ ) {
+ querySatisfied = false;
+ break;
+ }
+
+ let constraintValues = constraint.slice(2);
+
+ if (constraintType === GlodaConstants.kConstraintIdIn) {
+ if (!constraintValues.includes(aObj.id)) {
+ querySatisfied = false;
+ break;
+ }
+ } else if (
+ constraintType === GlodaConstants.kConstraintIn ||
+ constraintType === GlodaConstants.kConstraintEquals
+ ) {
+ // @testpoint gloda.query.test.kConstraintIn
+ let objectNounDef = attrDef.objectNounDef;
+
+ // if they provide an equals comparator, use that.
+ // (note: the next case has better optimization possibilities than
+ // this mechanism, but of course has higher initialization costs or
+ // code complexity costs...)
+ if (objectNounDef.equals) {
+ let testValues;
+ if (!(boundName in aObj)) {
+ testValues = [];
+ } else if (attrDef.singular) {
+ testValues = [aObj[boundName]];
+ } else {
+ testValues = aObj[boundName];
+ }
+
+ // If there are no constraints, then we are just testing for there
+ // being a value. Succeed (continue) in that case.
+ if (
+ constraintValues.length == 0 &&
+ testValues.length &&
+ testValues[0] != null
+ ) {
+ continue;
+ }
+
+ // If there are no test values and the empty set is significant,
+ // then check if any of the constraint values are null (our
+ // empty indicator.)
+ if (testValues.length == 0 && attrDef.emptySetIsSignificant) {
+ let foundEmptySetSignifier = false;
+ for (let constraintValue of constraintValues) {
+ if (constraintValue == null) {
+ foundEmptySetSignifier = true;
+ break;
+ }
+ }
+ if (foundEmptySetSignifier) {
+ continue;
+ }
+ }
+
+ let foundMatch = false;
+ for (let testValue of testValues) {
+ for (let value of constraintValues) {
+ if (objectNounDef.equals(testValue, value)) {
+ foundMatch = true;
+ break;
+ }
+ }
+ if (foundMatch) {
+ break;
+ }
+ }
+ if (!foundMatch) {
+ querySatisfied = false;
+ break;
+ }
+ } else {
+ // otherwise, we need to convert everyone to their param/value form
+ // in order to test for equality
+ // let's just do the simple, obvious thing for now. which is
+ // what we did in the prior case but exploding values using
+ // toParamAndValue, and then comparing.
+ let testValues;
+ if (!(boundName in aObj)) {
+ testValues = [];
+ } else if (attrDef.singular) {
+ testValues = [aObj[boundName]];
+ } else {
+ testValues = aObj[boundName];
+ }
+
+ // If there are no constraints, then we are just testing for there
+ // being a value. Succeed (continue) in that case.
+ if (
+ constraintValues.length == 0 &&
+ testValues.length &&
+ testValues[0] != null
+ ) {
+ continue;
+ }
+ // If there are no test values and the empty set is significant,
+ // then check if any of the constraint values are null (our
+ // empty indicator.)
+ if (testValues.length == 0 && attrDef.emptySetIsSignificant) {
+ let foundEmptySetSignifier = false;
+ for (let constraintValue of constraintValues) {
+ if (constraintValue == null) {
+ foundEmptySetSignifier = true;
+ break;
+ }
+ }
+ if (foundEmptySetSignifier) {
+ continue;
+ }
+ }
+
+ let foundMatch = false;
+ for (let testValue of testValues) {
+ let [aParam, aValue] = objectNounDef.toParamAndValue(testValue);
+ for (let value of constraintValues) {
+ // skip empty set check sentinel values
+ if (value == null && attrDef.emptySetIsSignificant) {
+ continue;
+ }
+ let [bParam, bValue] = objectNounDef.toParamAndValue(value);
+ if (aParam == bParam && aValue == bValue) {
+ foundMatch = true;
+ break;
+ }
+ }
+ if (foundMatch) {
+ break;
+ }
+ }
+ if (!foundMatch) {
+ querySatisfied = false;
+ break;
+ }
+ }
+ } else if (constraintType === GlodaConstants.kConstraintRanges) {
+ // @testpoint gloda.query.test.kConstraintRanges
+ let objectNounDef = attrDef.objectNounDef;
+
+ let testValues;
+ if (!(boundName in aObj)) {
+ testValues = [];
+ } else if (attrDef.singular) {
+ testValues = [aObj[boundName]];
+ } else {
+ testValues = aObj[boundName];
+ }
+
+ let foundMatch = false;
+ for (let testValue of testValues) {
+ let [tParam, tValue] = objectNounDef.toParamAndValue(testValue);
+ for (let rangeTuple of constraintValues) {
+ let [lowerRValue, upperRValue] = rangeTuple;
+ if (lowerRValue == null) {
+ let [upperParam, upperValue] =
+ objectNounDef.toParamAndValue(upperRValue);
+ if (tParam == upperParam && tValue <= upperValue) {
+ foundMatch = true;
+ break;
+ }
+ } else if (upperRValue == null) {
+ let [lowerParam, lowerValue] =
+ objectNounDef.toParamAndValue(lowerRValue);
+ if (tParam == lowerParam && tValue >= lowerValue) {
+ foundMatch = true;
+ break;
+ }
+ } else {
+ // no one is null
+ let [upperParam, upperValue] =
+ objectNounDef.toParamAndValue(upperRValue);
+ let [lowerParam, lowerValue] =
+ objectNounDef.toParamAndValue(lowerRValue);
+ if (
+ tParam == lowerParam &&
+ tValue >= lowerValue &&
+ tParam == upperParam &&
+ tValue <= upperValue
+ ) {
+ foundMatch = true;
+ break;
+ }
+ }
+ }
+ if (foundMatch) {
+ break;
+ }
+ }
+ if (!foundMatch) {
+ querySatisfied = false;
+ break;
+ }
+ } else if (constraintType === GlodaConstants.kConstraintStringLike) {
+ // @testpoint gloda.query.test.kConstraintStringLike
+ let curIndex = 0;
+ let value = boundName in aObj ? aObj[boundName] : "";
+ // the attribute must be singular, we don't support arrays of strings.
+ for (let valuePart of constraintValues) {
+ if (typeof valuePart == "string") {
+ let index = value.indexOf(valuePart);
+ // if curIndex is null, we just need any match
+ // if it's not null, it must match the offset of our found match
+ if (curIndex === null) {
+ if (index == -1) {
+ querySatisfied = false;
+ } else {
+ curIndex = index + valuePart.length;
+ }
+ } else if (index != curIndex) {
+ querySatisfied = false;
+ } else {
+ curIndex = index + valuePart.length;
+ }
+ if (!querySatisfied) {
+ break;
+ }
+ } else {
+ // wild!
+ curIndex = null;
+ }
+ }
+ // curIndex must be null or equal to the length of the string
+ if (querySatisfied && curIndex !== null && curIndex != value.length) {
+ querySatisfied = false;
+ }
+ } else if (constraintType === GlodaConstants.kConstraintFulltext) {
+ // @testpoint gloda.query.test.kConstraintFulltext
+ // this is beyond our powers. Even if we have the fulltext content in
+ // memory, which we may not, the tokenization and such to perform
+ // the testing gets very complicated in the face of i18n, etc.
+ // so, let's fail if the item is not already in the collection, and
+ // let the testing continue if it is. (some other constraint may no
+ // longer apply...)
+ if (!(aObj.id in this.collection._idMap)) {
+ querySatisfied = false;
+ }
+ }
+
+ if (!querySatisfied) {
+ break;
+ }
+ }
+
+ if (querySatisfied) {
+ return true;
+ }
+ }
+ return false;
+ },
+ /* eslint-enable complexity */
+
+ /**
+ * Helper code for noun definitions of queryHelpers that want to build a
+ * traditional in/equals constraint. The goal is to let them build a range
+ * without having to know how we structure |_constraints|.
+ *
+ * @protected
+ */
+ _inConstraintHelper(aAttrDef, aValues) {
+ let constraint = [GlodaConstants.kConstraintIn, aAttrDef].concat(aValues);
+ this._constraints.push(constraint);
+ return this;
+ },
+
+ /**
+ * Helper code for noun definitions of queryHelpers that want to build a
+ * range. The goal is to let them build a range without having to know how
+ * we structure |_constraints| or requiring them to mark themselves as
+ * continuous to get a "Range".
+ *
+ * @protected
+ */
+ _rangedConstraintHelper(aAttrDef, aRanges) {
+ let constraint = [GlodaConstants.kConstraintRanges, aAttrDef].concat(
+ aRanges
+ );
+ this._constraints.push(constraint);
+ return this;
+ },
+};
+
+/**
+ * @class A query that never matches anything.
+ *
+ * Collections corresponding to this query are intentionally frozen in time and
+ * do not want to be notified of any updates. We need the collection to be
+ * registered with the collection manager so that the noun instances in the
+ * collection are always 'reachable' via the collection for as long as we might
+ * be handing out references to the instances. (The other way to avoid updates
+ * would be to not register the collection, but then items might not be
+ * reachable.)
+ * This is intended to be used in implementation details behind the gloda
+ * abstraction barrier. For example, the message indexer likes to be able
+ * to represent 'ghost' and deleted messages, but these should never be exposed
+ * to the user. For code simplicity, it wants to be able to use the query
+ * mechanism. But it doesn't want updates that are effectively
+ * nonsensical. For example, a ghost message that is reused by message
+ * indexing may already be present in a collection; when the collection manager
+ * receives an itemsAdded event, a GlodaExplicitQueryClass would result in
+ * an item added notification in that case, which would wildly not be desired.
+ */
+function GlodaNullQueryClass() {}
+
+GlodaNullQueryClass.prototype = {
+ /**
+ * No options; they are currently only needed for SQL query generation, which
+ * does not happen for null queries.
+ */
+ options: {},
+
+ /**
+ * Provide a duck-typing way of indicating to GlodaCollectionManager that our
+ * associated collection just doesn't want anything to change. Our test
+ * function is able to convey most of it, but special-casing has to happen
+ * somewhere, so it happens here.
+ */
+ frozen: true,
+
+ /**
+ * Since our query never matches anything, it doesn't make sense to let
+ * someone attempt to construct a boolean OR involving us.
+ *
+ * @returns null
+ */
+ or() {
+ return null;
+ },
+
+ /**
+ * Return nothing (null) because it does not make sense to create a collection
+ * based on a null query. This method is normally used (on a normal query)
+ * to return a collection populated by the constraints of the query. We
+ * match nothing, so we should return nothing. More importantly, you are
+ * currently doing something wrong if you try and do this, so null is
+ * appropriate. It may turn out that it makes sense for us to return an
+ * empty collection in the future for sentinel value purposes, but we'll
+ * cross that bridge when we come to it.
+ *
+ * @returns null
+ */
+ getCollection() {
+ return null;
+ },
+
+ /**
+ * Never matches anything.
+ *
+ * @param aObj The object someone wants us to test for relevance to our
+ * associated collection. But we don't care! Not a fig!
+ * @returns false
+ */
+ test(aObj) {
+ return false;
+ },
+};
+
+/**
+ * @class A query that only 'tests' for already belonging to the collection.
+ *
+ * This type of collection is useful for when you (or rather your listener)
+ * are interested in hearing about modifications to your collection or removals
+ * from your collection because of deletion, but do not want to be notified
+ * about newly indexed items matching your normal query constraints.
+ *
+ * @param aCollection The collection this query belongs to. This needs to be
+ * passed-in here or the collection should set the attribute directly when
+ * the query is passed in to a collection's constructor.
+ */
+function GlodaExplicitQueryClass(aCollection) {
+ this.collection = aCollection;
+}
+
+GlodaExplicitQueryClass.prototype = {
+ /**
+ * No options; they are currently only needed for SQL query generation, which
+ * does not happen for explicit queries.
+ */
+ options: {},
+
+ /**
+ * Since our query is intended to only match the contents of our collection,
+ * it doesn't make sense to let someone attempt to construct a boolean OR
+ * involving us.
+ *
+ * @returns null
+ */
+ or() {
+ return null;
+ },
+
+ /**
+ * Return nothing (null) because it does not make sense to create a collection
+ * based on an explicit query. This method is normally used (on a normal
+ * query) to return a collection populated by the constraints of the query.
+ * In the case of an explicit query, we expect it will be associated with
+ * either a hand-created collection or the results of a normal query that is
+ * immediately converted into an explicit query. In all likelihood, calling
+ * this method on an instance of this type is an error, so it is helpful to
+ * return null because people will error hard.
+ *
+ * @returns null
+ */
+ getCollection() {
+ return null;
+ },
+
+ /**
+ * Matches only items that are already in the collection associated with this
+ * query (by id).
+ *
+ * @param aObj The object/item to test for already being in the associated
+ * collection.
+ * @returns true when the object is in the associated collection, otherwise
+ * false.
+ */
+ test(aObj) {
+ return aObj.id in this.collection._idMap;
+ },
+};
+
+/**
+ * @class A query that 'tests' true for everything. Intended for debugging purposes
+ * only.
+ */
+function GlodaWildcardQueryClass() {}
+
+GlodaWildcardQueryClass.prototype = {
+ /**
+ * No options; they are currently only needed for SQL query generation.
+ */
+ options: {},
+
+ // don't let people try and mess with us
+ or() {
+ return null;
+ },
+ // don't let people try and query on us (until we have a real use case for
+ // that...)
+ getCollection() {
+ return null;
+ },
+ /**
+ * Everybody wins!
+ */
+ test(aObj) {
+ return true;
+ },
+};
+
+/**
+ * Factory method to effectively create per-noun subclasses of GlodaQueryClass,
+ * GlodaNullQueryClass, GlodaExplicitQueryClass, and GlodaWildcardQueryClass.
+ * For GlodaQueryClass this allows us to add per-noun helpers. For the others,
+ * this is merely a means of allowing us to attach the (per-noun) nounDef to
+ * the 'class'.
+ */
+function GlodaQueryClassFactory(aNounDef) {
+ let newQueryClass = function (aOptions) {
+ GlodaQueryClass.call(this, aOptions);
+ };
+ newQueryClass.prototype = new GlodaQueryClass();
+ newQueryClass.prototype._queryClass = newQueryClass;
+ newQueryClass.prototype._nounDef = aNounDef;
+
+ let newNullClass = function (aCollection) {
+ GlodaNullQueryClass.call(this);
+ this.collection = aCollection;
+ };
+ newNullClass.prototype = new GlodaNullQueryClass();
+ newNullClass.prototype._queryClass = newNullClass;
+ newNullClass.prototype._nounDef = aNounDef;
+
+ let newExplicitClass = function (aCollection) {
+ GlodaExplicitQueryClass.call(this);
+ this.collection = aCollection;
+ };
+ newExplicitClass.prototype = new GlodaExplicitQueryClass();
+ newExplicitClass.prototype._queryClass = newExplicitClass;
+ newExplicitClass.prototype._nounDef = aNounDef;
+
+ let newWildcardClass = function (aCollection) {
+ GlodaWildcardQueryClass.call(this);
+ this.collection = aCollection;
+ };
+ newWildcardClass.prototype = new GlodaWildcardQueryClass();
+ newWildcardClass.prototype._queryClass = newWildcardClass;
+ newWildcardClass.prototype._nounDef = aNounDef;
+
+ return [newQueryClass, newNullClass, newExplicitClass, newWildcardClass];
+}
diff --git a/comm/mailnews/db/gloda/modules/GlodaSyntheticView.jsm b/comm/mailnews/db/gloda/modules/GlodaSyntheticView.jsm
new file mode 100644
index 0000000000..2e0fb7b5be
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaSyntheticView.jsm
@@ -0,0 +1,175 @@
+/* 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/. */
+
+/*
+ * This file is charged with providing you a way to have a pretty gloda-backed
+ * nsIMsgDBView.
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaSyntheticView"];
+
+/**
+ * Create a synthetic view suitable for passing to |FolderDisplayWidget.show|.
+ * You must pass a query, collection, or conversation in.
+ *
+ * @param {GlodaQuery} [aArgs.query] A gloda query to run.
+ * @param {GlodaCollection} [aArgs.collection] An already-populated collection
+ * to display. Do not call getCollection on a query and hand us that. We
+ * will not register ourselves as a listener and things will not work.
+ * @param {GlodaConversation} [aArgs.conversation] A conversation whose messages
+ * you want to display.
+ */
+function GlodaSyntheticView(aArgs) {
+ if ("query" in aArgs) {
+ this.query = aArgs.query;
+ this.collection = this.query.getCollection(this);
+ this.completed = false;
+ this.viewType = "global";
+ } else if ("collection" in aArgs) {
+ this.query = null;
+ this.collection = aArgs.collection;
+ this.completed = true;
+ this.viewType = "global";
+ } else if ("conversation" in aArgs) {
+ this.collection = aArgs.conversation.getMessagesCollection(this);
+ this.query = this.collection.query;
+ this.completed = false;
+ this.viewType = "conversation";
+ this.selectedMessage = aArgs.message.folderMessage;
+ } else {
+ throw new Error("You need to pass a query or collection");
+ }
+
+ this.customColumns = [];
+}
+GlodaSyntheticView.prototype = {
+ defaultSort: [
+ [Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending],
+ ],
+
+ /**
+ * Request the search be performed and notification provided to
+ * aSearchListener. If results are already available, they should
+ * be provided to aSearchListener without re-performing the search.
+ */
+ search(aSearchListener, aCompletionCallback) {
+ this.searchListener = aSearchListener;
+ this.completionCallback = aCompletionCallback;
+
+ this.searchListener.onNewSearch();
+ if (this.completed) {
+ this.reportResults(this.collection.items);
+ // we're not really aborting, but it closes things out nicely
+ this.abortSearch();
+ }
+ },
+
+ abortSearch() {
+ if (this.searchListener) {
+ this.searchListener.onSearchDone(Cr.NS_OK);
+ }
+ if (this.completionCallback) {
+ this.completionCallback();
+ }
+ this.searchListener = null;
+ this.completionCallback = null;
+ },
+
+ reportResults(aItems) {
+ for (let item of aItems) {
+ let hdr = item.folderMessage;
+ if (hdr) {
+ this.searchListener.onSearchHit(hdr, hdr.folder);
+ }
+ }
+ },
+
+ /**
+ * Helper function used by |DBViewWrapper.getMsgHdrForMessageID| since there
+ * are no actual backing folders for it to check.
+ */
+ getMsgHdrForMessageID(aMessageId) {
+ for (let item of this.collection.items) {
+ if (item.headerMessageID == aMessageId) {
+ let hdr = item.folderMessage;
+ if (hdr) {
+ return hdr;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * The default set of columns to show.
+ */
+ DEFAULT_COLUMN_STATES: {
+ threadCol: {
+ visible: true,
+ },
+ flaggedCol: {
+ visible: true,
+ },
+ subjectCol: {
+ visible: true,
+ },
+ correspondentCol: {
+ visible: Services.prefs.getBoolPref("mail.threadpane.use_correspondents"),
+ },
+ senderCol: {
+ visible: !Services.prefs.getBoolPref(
+ "mail.threadpane.use_correspondents"
+ ),
+ },
+ dateCol: {
+ visible: true,
+ },
+ locationCol: {
+ visible: true,
+ },
+ },
+
+ // --- settings persistence
+ getPersistedSetting(aSetting) {
+ try {
+ return JSON.parse(
+ Services.prefs.getCharPref(
+ "mailnews.database.global.views." + this.viewType + "." + aSetting
+ )
+ );
+ } catch (e) {
+ return this.getDefaultSetting(aSetting);
+ }
+ },
+ setPersistedSetting(aSetting, aValue) {
+ Services.prefs.setCharPref(
+ "mailnews.database.global.views." + this.viewType + "." + aSetting,
+ JSON.stringify(aValue)
+ );
+ },
+ getDefaultSetting(aSetting) {
+ if (aSetting == "columns") {
+ return this.DEFAULT_COLUMN_STATES;
+ }
+ return undefined;
+ },
+
+ // --- collection listener
+ onItemsAdded(aItems, aCollection) {
+ if (this.searchListener) {
+ this.reportResults(aItems);
+ }
+ },
+ onItemsModified(aItems, aCollection) {},
+ onItemsRemoved(aItems, aCollection) {},
+ onQueryCompleted(aCollection) {
+ this.completed = true;
+ if (this.searchListener) {
+ this.searchListener.onSearchDone(Cr.NS_OK);
+ }
+ if (this.completionCallback) {
+ this.completionCallback();
+ }
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/GlodaUtils.jsm b/comm/mailnews/db/gloda/modules/GlodaUtils.jsm
new file mode 100644
index 0000000000..a2b7fe4174
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/GlodaUtils.jsm
@@ -0,0 +1,84 @@
+/* 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 = ["GlodaUtils"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * @namespace A holding place for logic that is not gloda-specific and should
+ * reside elsewhere.
+ */
+var GlodaUtils = {
+ /**
+ * This Regexp is super-complicated and used at least in two different parts of
+ * the code, so let's expose it from one single location.
+ */
+ PART_RE: new RegExp(
+ "^[^?]+\\?(?:/;section=\\d+\\?)?(?:[^&]+&)*part=([^&]+)(?:&[^&]+)*$"
+ ),
+
+ deMime(aString) {
+ return MailServices.mimeConverter.decodeMimeHeader(
+ aString,
+ null,
+ false,
+ true
+ );
+ },
+
+ _headerParser: MailServices.headerParser,
+
+ /**
+ * Parses an RFC 2822 list of e-mail addresses and returns an object with
+ * 4 attributes, as described below. We will use the example of the user
+ * passing an argument of '"Bob Smith" <bob@example.com>'.
+ *
+ * This method (by way of nsIMsgHeaderParser) takes care of decoding mime
+ * headers, but is not aware of folder-level character set overrides.
+ *
+ * count: the number of addresses parsed. (ex: 1)
+ * addresses: a list of e-mail addresses (ex: ["bob@example.com"])
+ * names: a list of names (ex: ["Bob Smith"])
+ * fullAddresses: aka the list of name and e-mail together (ex: ['"Bob Smith"
+ * <bob@example.com>']).
+ *
+ * This method is a convenience wrapper around nsIMsgHeaderParser.
+ */
+ parseMailAddresses(aMailAddresses) {
+ let addresses = this._headerParser.parseEncodedHeader(aMailAddresses);
+ return {
+ names: addresses.map(a => a.name || null),
+ addresses: addresses.map(a => a.email),
+ fullAddresses: addresses.map(a => a.toString()),
+ count: addresses.length,
+ };
+ },
+
+ /**
+ * MD5 hash a string and return the hex-string result. Impl from nsICryptoHash
+ * docs.
+ */
+ md5HashString(aString) {
+ let data = [...new TextEncoder().encode(aString)];
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(data, data.length);
+ let hash = hasher.finish(false);
+
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+ }
+
+ // convert the binary hash data to a hex string.
+ let hex = Object.keys(hash).map(i => toHexString(hash.charCodeAt(i)));
+ return hex.join("");
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/IndexMsg.jsm b/comm/mailnews/db/gloda/modules/IndexMsg.jsm
new file mode 100644
index 0000000000..9a4add589e
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/IndexMsg.jsm
@@ -0,0 +1,3464 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * This file currently contains a fairly general implementation of asynchronous
+ * indexing with a very explicit message indexing implementation. As gloda
+ * will eventually want to index more than just messages, the message-specific
+ * things should ideally lose their special hold on this file. This will
+ * benefit readability/size as well.
+ */
+
+const EXPORTED_SYMBOLS = ["GlodaMsgIndexer"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+const { GlodaContact, GlodaFolder } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDataModel.jsm"
+);
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+const { GlodaIndexer, IndexingJob } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+const { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+);
+
+// Cr does not have mailnews error codes!
+var NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE = 0x80550005;
+
+var GLODA_MESSAGE_ID_PROPERTY = "gloda-id";
+/**
+ * Message header property to track dirty status; one of
+ * |GlodaIndexer.kMessageClean|, |GlodaIndexer.kMessageDirty|,
+ * |GlodaIndexer.kMessageFilthy|.
+ */
+var GLODA_DIRTY_PROPERTY = "gloda-dirty";
+
+/**
+ * The sentinel GLODA_MESSAGE_ID_PROPERTY value indicating that a message fails
+ * to index and we should not bother trying again, at least not until a new
+ * release is made.
+ *
+ * This should ideally just flip between 1 and 2, with GLODA_OLD_BAD_MESSAGE_ID
+ * flipping in the other direction. If we start having more trailing badness,
+ * _indexerGetEnumerator and GLODA_OLD_BAD_MESSAGE_ID will need to be altered.
+ *
+ * When flipping this, be sure to update glodaTestHelper.js's copy.
+ */
+var GLODA_BAD_MESSAGE_ID = 2;
+/**
+ * The gloda id we used to use to mark messages as bad, but now should be
+ * treated as eligible for indexing. This is only ever used for consideration
+ * when creating msg header enumerators with `_indexerGetEnumerator` which
+ * means we only will re-index such messages in an indexing sweep. Accordingly
+ * event-driven indexing will still treat such messages as unindexed (and
+ * unindexable) until an indexing sweep picks them up.
+ */
+var GLODA_OLD_BAD_MESSAGE_ID = 1;
+var GLODA_FIRST_VALID_MESSAGE_ID = 32;
+
+var JUNK_SCORE_PROPERTY = "junkscore";
+var JUNK_SPAM_SCORE_STR = Ci.nsIJunkMailPlugin.IS_SPAM_SCORE.toString();
+
+/**
+ * The processing flags that tell us that a message header has not yet been
+ * reported to us via msgsClassified. If it has one of these flags, it is
+ * still being processed.
+ */
+var NOT_YET_REPORTED_PROCESSING_FLAGS =
+ Ci.nsMsgProcessingFlags.NotReportedClassified |
+ Ci.nsMsgProcessingFlags.ClassifyJunk;
+
+// for list comprehension fun
+function* range(begin, end) {
+ for (let i = begin; i < end; ++i) {
+ yield i;
+ }
+}
+
+/**
+ * We do not set properties on the messages until we perform a DB commit; this
+ * helper class tracks messages that we have indexed but are not yet marked
+ * as such on their header.
+ */
+var PendingCommitTracker = {
+ /**
+ * Maps message URIs to their gloda ids.
+ *
+ * I am not entirely sure why I chose the URI for the key rather than
+ * gloda folder ID + message key. Most likely it was to simplify debugging
+ * since the gloda folder ID is opaque while the URI is very informative. It
+ * is also possible I was afraid of IMAP folder renaming triggering a UID
+ * renumbering?
+ */
+ _indexedMessagesPendingCommitByKey: {},
+ /**
+ * Map from the pending commit gloda id to a tuple of [the corresponding
+ * message header, dirtyState].
+ */
+ _indexedMessagesPendingCommitByGlodaId: {},
+ /**
+ * Do we have a post-commit handler registered with this transaction yet?
+ */
+ _pendingCommit: false,
+
+ /**
+ * The function gets called when the commit actually happens to flush our
+ * message id's.
+ *
+ * It is very possible that by the time this call happens we have left the
+ * folder and nulled out msgDatabase on the folder. Since nulling it out
+ * is what causes the commit, if we set the headers here without somehow
+ * forcing a commit, we will lose. Badly.
+ * Accordingly, we make a list of all the folders that the headers belong to
+ * as we iterate, make sure to re-attach their msgDatabase before forgetting
+ * the headers, then make sure to zero the msgDatabase again, triggering a
+ * commit. If there were a way to directly get the nsIMsgDatabase from the
+ * header we could do that and call commit directly. We don't track
+ * databases along with the headers since the headers can change because of
+ * moves and that would increase the number of moving parts.
+ */
+ _commitCallback() {
+ let foldersByURI = {};
+ let lastFolder = null;
+
+ for (let glodaId in PendingCommitTracker._indexedMessagesPendingCommitByGlodaId) {
+ let [msgHdr, dirtyState] =
+ PendingCommitTracker._indexedMessagesPendingCommitByGlodaId[glodaId];
+ // Mark this message as indexed.
+ // It's conceivable the database could have gotten blown away, in which
+ // case the message headers are going to throw exceptions when we try
+ // and touch them. So we wrap this in a try block that complains about
+ // this unforeseen circumstance. (noteFolderDatabaseGettingBlownAway
+ // should have been called and avoided this situation in all known
+ // situations.)
+ try {
+ let curGlodaId = msgHdr.getUint32Property(GLODA_MESSAGE_ID_PROPERTY);
+ if (curGlodaId != glodaId) {
+ msgHdr.setUint32Property(GLODA_MESSAGE_ID_PROPERTY, glodaId);
+ }
+ let headerDirty = msgHdr.getUint32Property(GLODA_DIRTY_PROPERTY);
+ if (headerDirty != dirtyState) {
+ msgHdr.setUint32Property(GLODA_DIRTY_PROPERTY, dirtyState);
+ }
+
+ // Make sure this folder is in our foldersByURI map.
+ if (lastFolder == msgHdr.folder) {
+ continue;
+ }
+ lastFolder = msgHdr.folder;
+ let folderURI = lastFolder.URI;
+ if (!(folderURI in foldersByURI)) {
+ foldersByURI[folderURI] = lastFolder;
+ }
+ } catch (ex) {
+ GlodaMsgIndexer._log.error(
+ "Exception while attempting to mark message with gloda state after" +
+ "db commit",
+ ex
+ );
+ }
+ }
+
+ // it is vitally important to do this before we forget about the headers!
+ for (let uri in foldersByURI) {
+ let folder = foldersByURI[uri];
+ // This will not cause a parse. The database is in-memory since we have
+ // a header that belongs to it. This just causes the folder to
+ // re-acquire a reference from the database manager.
+ folder.msgDatabase;
+ // And this will cause a commit. (And must be done since we don't want
+ // to cause a leak.)
+ folder.msgDatabase = null;
+ }
+
+ PendingCommitTracker._indexedMessagesPendingCommitByGlodaId = {};
+ PendingCommitTracker._indexedMessagesPendingCommitByKey = {};
+
+ PendingCommitTracker._pendingCommit = false;
+ },
+
+ /**
+ * Track a message header that should be marked with the given gloda id when
+ * the database commits.
+ */
+ track(aMsgHdr, aGlodaId) {
+ let pendingKey = aMsgHdr.folder.URI + "#" + aMsgHdr.messageKey;
+ this._indexedMessagesPendingCommitByKey[pendingKey] = aGlodaId;
+ this._indexedMessagesPendingCommitByGlodaId[aGlodaId] = [
+ aMsgHdr,
+ GlodaMsgIndexer.kMessageClean,
+ ];
+
+ if (!this._pendingCommit) {
+ GlodaDatastore.runPostCommit(this._commitCallback);
+ this._pendingCommit = true;
+ }
+ },
+
+ /**
+ * Get the current state of a message header given that we cannot rely on just
+ * looking at the header's properties because we defer setting those
+ * until the SQLite commit happens.
+ *
+ * @returns Tuple of [gloda id, dirty status].
+ */
+ getGlodaState(aMsgHdr) {
+ // If it's in the pending commit table, then the message is basically
+ // clean. Return that info.
+ let pendingKey = aMsgHdr.folder.URI + "#" + aMsgHdr.messageKey;
+ if (pendingKey in this._indexedMessagesPendingCommitByKey) {
+ let glodaId =
+ PendingCommitTracker._indexedMessagesPendingCommitByKey[pendingKey];
+ return [glodaId, this._indexedMessagesPendingCommitByGlodaId[glodaId][1]];
+ }
+
+ // Otherwise the header's concept of state is correct.
+ let glodaId = aMsgHdr.getUint32Property(GLODA_MESSAGE_ID_PROPERTY);
+ let glodaDirty = aMsgHdr.getUint32Property(GLODA_DIRTY_PROPERTY);
+ return [glodaId, glodaDirty];
+ },
+
+ /**
+ * Update our structure to reflect moved headers. Moves are currently
+ * treated as weakly interesting and do not require a reindexing
+ * although collections will get notified. So our job is to to fix-up
+ * the pending commit information if the message has a pending commit.
+ */
+ noteMove(aOldHdr, aNewHdr) {
+ let oldKey = aOldHdr.folder.URI + "#" + aOldHdr.messageKey;
+ if (!(oldKey in this._indexedMessagesPendingCommitByKey)) {
+ return;
+ }
+
+ let glodaId = this._indexedMessagesPendingCommitByKey[oldKey];
+ delete this._indexedMessagesPendingCommitByKey[oldKey];
+
+ let newKey = aNewHdr.folder.URI + "#" + aNewHdr.messageKey;
+ this._indexedMessagesPendingCommitByKey[newKey] = glodaId;
+
+ // only clobber the header, not the dirty state
+ this._indexedMessagesPendingCommitByGlodaId[glodaId][0] = aNewHdr;
+ },
+
+ /**
+ * A blind move is one where we have the source header but not the destination
+ * header. This happens for IMAP messages that do not involve offline fake
+ * headers.
+ * XXX Since IMAP moves will propagate the gloda-id/gloda-dirty bits for us,
+ * we could detect the other side of the move when it shows up as a
+ * msgsClassified event and restore the mapping information. Since the
+ * offline fake header case should now cover the bulk of IMAP move
+ * operations, we probably do not need to pursue this.
+ *
+ * We just re-dispatch to noteDirtyHeader because we can't do anything more
+ * clever.
+ */
+ noteBlindMove(aOldHdr) {
+ this.noteDirtyHeader(aOldHdr);
+ },
+
+ /**
+ * If a message is dirty we should stop tracking it for post-commit
+ * purposes. This is not because we don't want to write to its header
+ * when we commit as much as that we want to avoid |getHeaderGlodaState|
+ * reporting that the message is clean. We could complicate our state
+ * by storing that information, but this is easier and ends up the same
+ * in the end.
+ */
+ noteDirtyHeader(aMsgHdr) {
+ let pendingKey = aMsgHdr.folder.URI + "#" + aMsgHdr.messageKey;
+ if (!(pendingKey in this._indexedMessagesPendingCommitByKey)) {
+ return;
+ }
+
+ // (It is important that we get the gloda id from our own structure!)
+ let glodaId = this._indexedMessagesPendingCommitByKey[pendingKey];
+ this._indexedMessagesPendingCommitByGlodaId[glodaId][1] =
+ GlodaMsgIndexer.kMessageDirty;
+ },
+
+ /**
+ * Sometimes a folder database gets blown away. This happens for one of two
+ * expected reasons right now:
+ * - Folder compaction.
+ * - Explicit reindexing of a folder via the folder properties "rebuild index"
+ * button.
+ *
+ * When this happens, we are basically out of luck and need to discard
+ * everything about the folder. The good news is that the folder compaction
+ * pass is clever enough to re-establish the linkages that are being lost
+ * when we drop these things on the floor. Reindexing of a folder is not
+ * clever enough to deal with this but is an exceptional case of last resort
+ * (the user should not normally be performing a reindex as part of daily
+ * operation), so we accept that messages may be redundantly indexed.
+ */
+ noteFolderDatabaseGettingBlownAway(aMsgFolder) {
+ let uri = aMsgFolder.URI + "#";
+ for (let key of Object.keys(this._indexedMessagesPendingCommitByKey)) {
+ // this is not as efficient as it could be, but compaction is relatively
+ // rare and the number of pending headers is generally going to be
+ // small.
+ if (key.indexOf(uri) == 0) {
+ delete this._indexedMessagesPendingCommitByKey[key];
+ }
+ }
+ },
+};
+
+/**
+ * This callback handles processing the asynchronous query results of
+ * |GlodaMsgIndexer.getMessagesByMessageID|.
+ */
+function MessagesByMessageIdCallback(
+ aMsgIDToIndex,
+ aResults,
+ aCallback,
+ aCallbackThis
+) {
+ this.msgIDToIndex = aMsgIDToIndex;
+ this.results = aResults;
+ this.callback = aCallback;
+ this.callbackThis = aCallbackThis;
+}
+
+MessagesByMessageIdCallback.prototype = {
+ _log: console.createInstance({
+ prefix: "gloda.index_msg.mbm",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ }),
+
+ onItemsAdded(aItems, aCollection) {
+ // just outright bail if we are shutdown
+ if (GlodaDatastore.datastoreIsShutdown) {
+ return;
+ }
+
+ this._log.debug("getting results...");
+ for (let message of aItems) {
+ this.results[this.msgIDToIndex[message.headerMessageID]].push(message);
+ }
+ },
+ onItemsModified() {},
+ onItemsRemoved() {},
+ onQueryCompleted(aCollection) {
+ // just outright bail if we are shutdown
+ if (GlodaDatastore.datastoreIsShutdown) {
+ return;
+ }
+
+ this._log.debug("query completed, notifying... " + this.results);
+
+ this.callback.call(this.callbackThis, this.results);
+ },
+};
+
+/**
+ * The message indexer!
+ *
+ * === Message Indexing Strategy
+ * To these ends, we implement things like so:
+ *
+ * Message State Tracking
+ * - We store a property on all indexed headers indicating their gloda message
+ * id. This allows us to tell whether a message is indexed from the header,
+ * without having to consult the SQL database.
+ * - When we receive an event that indicates that a message's meta-data has
+ * changed and gloda needs to re-index the message, we set a property on the
+ * header that indicates the message is dirty. This property can indicate
+ * that the message needs to be re-indexed but the gloda-id is valid (dirty)
+ * or that the message's gloda-id is invalid (filthy) because the gloda
+ * database has been blown away.
+ * - We track whether a folder is up-to-date on our GlodaFolder representation
+ * using a concept of dirtiness, just like messages. Like messages, a folder
+ * can be dirty or filthy. A dirty folder has at least one dirty message in
+ * it which means we should scan the folder. A filthy folder means that
+ * every message in the folder should be considered filthy. Folders start
+ * out filthy when Gloda is first told about them indicating we cannot
+ * trust any of the gloda-id's in the folders. Filthy folders are downgraded
+ * to dirty folders after we mark all of the headers with gloda-id's filthy.
+ *
+ * Indexing Message Control
+ * - We index the headers of all IMAP messages. We index the bodies of all IMAP
+ * messages that are offline. We index all local messages. We plan to avoid
+ * indexing news messages.
+ * - We would like a way to express desires about indexing that either don't
+ * confound offline storage with indexing, or actually allow some choice.
+ *
+ * Indexing Messages
+ * - We have two major modes of indexing: sweep and event-driven. When we
+ * start up we kick off an indexing sweep. We use event-driven indexing
+ * as we receive events for eligible messages, but if we get too many
+ * events we start dropping them on the floor and just flag that an indexing
+ * sweep is required.
+ * - The sweep initiates folder indexing jobs based on the priorities assigned
+ * to folders. Folder indexing uses a filtered message enumerator to find
+ * messages that need to be indexed, minimizing wasteful exposure of message
+ * headers to XPConnect that we would not end up indexing.
+ * - For local folders, we use GetDatabaseWithReparse to ensure that the .msf
+ * file exists. For IMAP folders, we simply use GetDatabase because we know
+ * the auto-sync logic will make sure that the folder is up-to-date and we
+ * want to avoid creating problems through use of updateFolder.
+ *
+ * Junk Mail
+ * - We do not index junk. We do not index messages until the junk/non-junk
+ * determination has been made. If a message gets marked as junk, we act like
+ * it was deleted.
+ * - We know when a message is actively queued for junk processing thanks to
+ * folder processing flags. nsMsgDBFolder::CallFilterPlugins does this
+ * prior to initiating spam processing. Unfortunately, this method does not
+ * get called until after we receive the notification about the existence of
+ * the header. How long after can vary on different factors. The longest
+ * delay is in the IMAP case where there is a filter that requires the
+ * message body to be present; the method does not get called until all the
+ * bodies are downloaded.
+ *
+ */
+var GlodaMsgIndexer = {
+ /**
+ * A partial attempt to generalize to support multiple databases. Each
+ * database would have its own datastore would have its own indexer. But
+ * we rather inter-mingle our use of this field with the singleton global
+ * GlodaDatastore.
+ */
+ _datastore: GlodaDatastore,
+ _log: console.createInstance({
+ prefix: "gloda.index_msg",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ }),
+
+ _junkService: MailServices.junk,
+
+ name: "index_msg",
+ /**
+ * Are we enabled, read: are we processing change events?
+ */
+ _enabled: false,
+ get enabled() {
+ return this._enabled;
+ },
+
+ enable() {
+ // initialize our listeners' this pointers
+ this._databaseAnnouncerListener.indexer = this;
+ this._msgFolderListener.indexer = this;
+
+ // register for:
+ // - folder loaded events, so we know when getDatabaseWithReparse has
+ // finished updating the index/what not (if it wasn't immediately
+ // available)
+ // - property changes (so we know when a message's read/starred state have
+ // changed.)
+ this._folderListener._init(this);
+ MailServices.mailSession.AddFolderListener(
+ this._folderListener,
+ Ci.nsIFolderListener.intPropertyChanged |
+ Ci.nsIFolderListener.propertyFlagChanged |
+ Ci.nsIFolderListener.event
+ );
+
+ MailServices.mfn.addListener(
+ this._msgFolderListener,
+ // note: intentionally no msgAdded or msgUnincorporatedMoved.
+ Ci.nsIMsgFolderNotificationService.msgsClassified |
+ Ci.nsIMsgFolderNotificationService.msgsJunkStatusChanged |
+ Ci.nsIMsgFolderNotificationService.msgsDeleted |
+ Ci.nsIMsgFolderNotificationService.msgsMoveCopyCompleted |
+ Ci.nsIMsgFolderNotificationService.msgKeyChanged |
+ Ci.nsIMsgFolderNotificationService.folderAdded |
+ Ci.nsIMsgFolderNotificationService.folderDeleted |
+ Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted |
+ Ci.nsIMsgFolderNotificationService.folderRenamed |
+ Ci.nsIMsgFolderNotificationService.folderCompactStart |
+ Ci.nsIMsgFolderNotificationService.folderCompactFinish |
+ Ci.nsIMsgFolderNotificationService.folderReindexTriggered
+ );
+
+ this._enabled = true;
+
+ this._considerSchemaMigration();
+
+ this._log.info("Event-Driven Indexing is now " + this._enabled);
+ },
+ disable() {
+ // remove FolderLoaded notification listener
+ MailServices.mailSession.RemoveFolderListener(this._folderListener);
+
+ MailServices.mfn.removeListener(this._msgFolderListener);
+
+ this._indexerLeaveFolder(); // nop if we aren't "in" a folder
+
+ this._enabled = false;
+
+ this._log.info("Event-Driven Indexing is now " + this._enabled);
+ },
+
+ /**
+ * Indicates that we have pending deletions to process, meaning that there
+ * are gloda message rows flagged for deletion. If this value is a boolean,
+ * it means the value is known reliably. If this value is null, it means
+ * that we don't know, likely because we have started up and have not checked
+ * the database.
+ */
+ pendingDeletions: null,
+
+ /**
+ * The message (or folder state) is believed up-to-date.
+ */
+ kMessageClean: 0,
+ /**
+ * The message (or folder) is known to not be up-to-date. In the case of
+ * folders, this means that some of the messages in the folder may be dirty.
+ * However, because of the way our indexing works, it is possible there may
+ * actually be no dirty messages in a folder. (We attempt to process
+ * messages in an event-driven fashion for a finite number of messages, but
+ * because we can quit without completing processing of the queue, we need to
+ * mark the folder dirty, just-in-case.) (We could do some extra leg-work
+ * and do a better job of marking the folder clean again.)
+ */
+ kMessageDirty: 1,
+ /**
+ * We have not indexed the folder at all, but messages in the folder think
+ * they are indexed. We downgrade the folder to just kMessageDirty after
+ * marking all the messages in the folder as dirty. We do this so that if we
+ * have to stop indexing the folder we can still build on our progress next
+ * time we enter the folder.
+ * We mark all folders filthy when (re-)creating the database because there
+ * may be previous state left over from an earlier database.
+ */
+ kMessageFilthy: 2,
+
+ /**
+ * A message addition job yet to be (completely) processed. Since message
+ * addition events come to us one-by-one, in order to aggregate them into a
+ * job, we need something like this. It's up to the indexing loop to
+ * decide when to null this out; it can either do it when it first starts
+ * processing it, or when it has processed the last thing. It's really a
+ * question of whether we want retrograde motion in the folder progress bar
+ * or the message progress bar.
+ */
+ _pendingAddJob: null,
+
+ /**
+ * The number of messages that we should queue for processing before letting
+ * them fall on the floor and relying on our folder-walking logic to ensure
+ * that the messages are indexed.
+ * The reason we allow for queueing messages in an event-driven fashion is
+ * that once we have reached a steady-state, it is preferable to be able to
+ * deal with new messages and modified meta-data in a prompt fashion rather
+ * than having to (potentially) walk every folder in the system just to find
+ * the message that the user changed the tag on.
+ */
+ _indexMaxEventQueueMessages: 20,
+
+ /**
+ * Unit testing hook to get us to emit additional logging that verges on
+ * inane for general usage but is helpful in unit test output to get a lay
+ * of the land and for paranoia reasons.
+ */
+ _unitTestSuperVerbose: false,
+
+ /** The GlodaFolder corresponding to the folder we are indexing. */
+ _indexingGlodaFolder: null,
+ /** The nsIMsgFolder we are currently indexing. */
+ _indexingFolder: null,
+ /** The nsIMsgDatabase we are currently indexing. */
+ _indexingDatabase: null,
+ /**
+ * The iterator we are using to iterate over the headers in
+ * this._indexingDatabase.
+ */
+ _indexingIterator: null,
+
+ /** folder whose entry we are pending on */
+ _pendingFolderEntry: null,
+
+ /**
+ * Async common logic that we want to deal with the given folder ID. Besides
+ * cutting down on duplicate code, this ensures that we are listening on
+ * the folder in case it tries to go away when we are using it.
+ *
+ * @returns true when the folder was successfully entered, false when we need
+ * to pend on notification of updating of the folder (due to re-parsing
+ * or what have you). In the event of an actual problem, an exception
+ * will escape.
+ */
+ _indexerEnterFolder(aFolderID) {
+ // leave the folder if we haven't explicitly left it.
+ if (this._indexingFolder !== null) {
+ this._indexerLeaveFolder();
+ }
+
+ this._indexingGlodaFolder = GlodaDatastore._mapFolderID(aFolderID);
+ this._indexingFolder = this._indexingGlodaFolder.getXPCOMFolder(
+ this._indexingGlodaFolder.kActivityIndexing
+ );
+
+ if (this._indexingFolder) {
+ this._log.debug("Entering folder: " + this._indexingFolder.URI);
+ }
+
+ try {
+ // The msf may need to be created or otherwise updated for local folders.
+ // This may require yielding until such time as the msf has been created.
+ try {
+ if (this._indexingFolder instanceof Ci.nsIMsgLocalMailFolder) {
+ this._indexingDatabase = this._indexingFolder.getDatabaseWithReparse(
+ null,
+ null
+ );
+ }
+ // we need do nothing special for IMAP, news, or other
+ } catch (e) {
+ // getDatabaseWithReparse can return either NS_ERROR_NOT_INITIALIZED or
+ // NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE if the net result is that it
+ // is going to send us a notification when the reparse has completed.
+ // (note that although internally NS_MSG_ERROR_FOLDER_SUMMARY_MISSING
+ // might get flung around, it won't make it out to us, and will instead
+ // be permuted into an NS_ERROR_NOT_INITIALIZED.)
+ if (
+ e.result == Cr.NS_ERROR_NOT_INITIALIZED ||
+ e.result == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE
+ ) {
+ // this means that we need to pend on the update; the listener for
+ // FolderLoaded events will call _indexerCompletePendingFolderEntry.
+ this._log.debug("Pending on folder load...");
+ this._pendingFolderEntry = this._indexingFolder;
+ return GlodaConstants.kWorkAsync;
+ }
+ throw e;
+ }
+ // we get an nsIMsgDatabase out of this (unsurprisingly) which
+ // explicitly inherits from nsIDBChangeAnnouncer, which has the
+ // addListener call we want.
+ if (this._indexingDatabase == null) {
+ this._indexingDatabase = this._indexingFolder.msgDatabase;
+ }
+ this._indexingDatabase.addListener(this._databaseAnnouncerListener);
+ } catch (ex) {
+ this._log.error(
+ "Problem entering folder: " +
+ (this._indexingFolder ? this._indexingFolder.prettyName : "unknown") +
+ ", skipping. Error was: " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ": " +
+ ex
+ );
+ this._indexingGlodaFolder.indexing = false;
+ this._indexingFolder = null;
+ this._indexingGlodaFolder = null;
+ this._indexingDatabase = null;
+ this._indexingEnumerator = null;
+
+ // re-throw, we just wanted to make sure this junk is cleaned up and
+ // get localized error logging...
+ throw ex;
+ }
+
+ return GlodaConstants.kWorkSync;
+ },
+
+ /**
+ * If the folder was still parsing/updating when we tried to enter, then this
+ * handler will get called by the listener who got the FolderLoaded message.
+ * All we need to do is get the database reference, register a listener on
+ * the db, and retrieve an iterator if desired.
+ */
+ _indexerCompletePendingFolderEntry() {
+ this._indexingDatabase = this._indexingFolder.msgDatabase;
+ this._indexingDatabase.addListener(this._databaseAnnouncerListener);
+ this._log.debug("...Folder Loaded!");
+
+ // the load is no longer pending; we certainly don't want more notifications
+ this._pendingFolderEntry = null;
+ // indexerEnterFolder returned kWorkAsync, which means we need to notify
+ // the callback driver to get things going again.
+ GlodaIndexer.callbackDriver();
+ },
+
+ /**
+ * Enumerate all messages in the folder.
+ */
+ kEnumAllMsgs: 0,
+ /**
+ * Enumerate messages that look like they need to be indexed.
+ */
+ kEnumMsgsToIndex: 1,
+ /**
+ * Enumerate messages that are already indexed.
+ */
+ kEnumIndexedMsgs: 2,
+
+ /**
+ * Synchronous helper to get an enumerator for the current folder (as found
+ * in |_indexingFolder|.
+ *
+ * @param aEnumKind One of |kEnumAllMsgs|, |kEnumMsgsToIndex|, or
+ * |kEnumIndexedMsgs|.
+ * @param [aAllowPreBadIds=false] Only valid for |kEnumIndexedMsgs|, tells us
+ * that we should treat message with any gloda-id as dirty, not just
+ * messages that have non-bad message id's.
+ */
+ _indexerGetEnumerator(aEnumKind, aAllowPreBadIds) {
+ if (aEnumKind == this.kEnumMsgsToIndex) {
+ // We need to create search terms for messages to index. Messages should
+ // be indexed if they're indexable (local or offline and not expunged)
+ // and either: haven't been indexed, are dirty, or are marked with with
+ // a former GLODA_BAD_MESSAGE_ID that is no longer our bad marker. (Our
+ // bad marker can change on minor schema revs so that we can try and
+ // reindex those messages exactly once and without needing to go through
+ // a pass to mark them as needing one more try.)
+ // The basic search expression is:
+ // ((GLODA_MESSAGE_ID_PROPERTY Is 0) ||
+ // (GLODA_MESSAGE_ID_PROPERTY Is GLODA_OLD_BAD_MESSAGE_ID) ||
+ // (GLODA_DIRTY_PROPERTY Isnt 0)) &&
+ // (JUNK_SCORE_PROPERTY Isnt 100)
+ // If the folder !isLocal we add the terms:
+ // - if the folder is offline -- && (Status Is nsMsgMessageFlags.Offline)
+ // - && (Status Isnt nsMsgMessageFlags.Expunged)
+
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerms = [];
+ let isLocal = this._indexingFolder instanceof Ci.nsIMsgLocalMailFolder;
+
+ searchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ this._indexingFolder
+ );
+ let nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ let nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+ // first term: (GLODA_MESSAGE_ID_PROPERTY Is 0
+ let searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = false; // actually don't care here
+ searchTerm.beginsGrouping = true;
+ searchTerm.attrib = nsMsgSearchAttrib.Uint32HdrProperty;
+ searchTerm.op = nsMsgSearchOp.Is;
+ let value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = 0;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = GLODA_MESSAGE_ID_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ // second term: || GLODA_MESSAGE_ID_PROPERTY Is GLODA_OLD_BAD_MESSAGE_ID
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = false; // OR
+ searchTerm.attrib = nsMsgSearchAttrib.Uint32HdrProperty;
+ searchTerm.op = nsMsgSearchOp.Is;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = GLODA_OLD_BAD_MESSAGE_ID;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = GLODA_MESSAGE_ID_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ // third term: || GLODA_DIRTY_PROPERTY Isnt 0 )
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = false;
+ searchTerm.endsGrouping = true;
+ searchTerm.attrib = nsMsgSearchAttrib.Uint32HdrProperty;
+ searchTerm.op = nsMsgSearchOp.Isnt;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = 0;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = GLODA_DIRTY_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ // JUNK_SCORE_PROPERTY Isnt 100
+ // For symmetry with our event-driven stuff, we just directly deal with
+ // the header property.
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = nsMsgSearchAttrib.HdrProperty;
+ searchTerm.op = nsMsgSearchOp.Isnt;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.str = JUNK_SPAM_SCORE_STR;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = JUNK_SCORE_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ if (!isLocal) {
+ // If the folder is offline, then the message should be too
+ if (this._indexingFolder.getFlag(Ci.nsMsgFolderFlags.Offline)) {
+ // third term: && Status Is nsMsgMessageFlags.Offline
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = nsMsgSearchAttrib.MsgStatus;
+ searchTerm.op = nsMsgSearchOp.Is;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = Ci.nsMsgMessageFlags.Offline;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+ }
+
+ // fourth term: && Status Isnt nsMsgMessageFlags.Expunged
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = nsMsgSearchAttrib.MsgStatus;
+ searchTerm.op = nsMsgSearchOp.Isnt;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = Ci.nsMsgMessageFlags.Expunged;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+ }
+
+ this._indexingEnumerator = this._indexingDatabase.getFilterEnumerator(
+ searchTerms,
+ true
+ );
+ } else if (aEnumKind == this.kEnumIndexedMsgs) {
+ // Enumerate only messages that are already indexed. This comes out to:
+ // ((GLODA_MESSAGE_ID_PROPERTY > GLODA_FIRST_VALID_MESSAGE_ID-1) &&
+ // (GLODA_DIRTY_PROPERTY Isnt kMessageFilthy))
+ // In English, a message is indexed if (by clause):
+ // 1) The message has a gloda-id and that gloda-id is in the valid range
+ // (and not in the bad message marker range).
+ // 2) The message has not been marked filthy (which invalidates the
+ // gloda-id.) We also assume that the folder would not have been
+ // entered at all if it was marked filthy.
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerms = [];
+
+ searchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ this._indexingFolder
+ );
+ let nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ let nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+ // first term: (GLODA_MESSAGE_ID_PROPERTY > GLODA_FIRST_VALID_MESSAGE_ID-1
+ let searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = false; // actually don't care here
+ searchTerm.beginsGrouping = true;
+ searchTerm.attrib = nsMsgSearchAttrib.Uint32HdrProperty;
+ // use != 0 if we're allow pre-bad ids.
+ searchTerm.op = aAllowPreBadIds
+ ? nsMsgSearchOp.Isnt
+ : nsMsgSearchOp.IsGreaterThan;
+ let value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = aAllowPreBadIds ? 0 : GLODA_FIRST_VALID_MESSAGE_ID - 1;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = GLODA_MESSAGE_ID_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ // second term: && GLODA_DIRTY_PROPERTY Isnt kMessageFilthy)
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.endsGrouping = true;
+ searchTerm.attrib = nsMsgSearchAttrib.Uint32HdrProperty;
+ searchTerm.op = nsMsgSearchOp.Isnt;
+ value = searchTerm.value;
+ value.attrib = searchTerm.attrib;
+ value.status = this.kMessageFilthy;
+ searchTerm.value = value;
+ searchTerm.hdrProperty = GLODA_DIRTY_PROPERTY;
+ searchTerms.push(searchTerm);
+
+ // The use-case of already indexed messages does not want them reversed;
+ // we care about seeing the message keys in order.
+ this._indexingEnumerator = this._indexingDatabase.getFilterEnumerator(
+ searchTerms,
+ false
+ );
+ } else if (aEnumKind == this.kEnumAllMsgs) {
+ this._indexingEnumerator =
+ this._indexingDatabase.reverseEnumerateMessages();
+ } else {
+ throw new Error("Unknown enumerator type requested:" + aEnumKind);
+ }
+ },
+
+ _indexerLeaveFolder() {
+ if (this._indexingFolder !== null) {
+ if (this._indexingDatabase) {
+ this._indexingDatabase.commit(Ci.nsMsgDBCommitType.kLargeCommit);
+ // remove our listener!
+ this._indexingDatabase.removeListener(this._databaseAnnouncerListener);
+ }
+ // let the gloda folder know we are done indexing
+ this._indexingGlodaFolder.indexing = false;
+ // null everyone out
+ this._indexingFolder = null;
+ this._indexingGlodaFolder = null;
+ this._indexingDatabase = null;
+ this._indexingEnumerator = null;
+ }
+ },
+
+ /**
+ * Event fed to us by our nsIFolderListener when a folder is loaded. We use
+ * this event to know when a folder we were trying to open to index is
+ * actually ready to be indexed. (The summary may have not existed, may have
+ * been out of date, or otherwise.)
+ *
+ * @param aFolder An nsIMsgFolder, already QI'd.
+ */
+ _onFolderLoaded(aFolder) {
+ if (
+ this._pendingFolderEntry !== null &&
+ aFolder.URI == this._pendingFolderEntry.URI
+ ) {
+ this._indexerCompletePendingFolderEntry();
+ }
+ },
+
+ // it's a getter so we can reference 'this'. we could memoize.
+ get workers() {
+ return [
+ [
+ "folderSweep",
+ {
+ worker: this._worker_indexingSweep,
+ jobCanceled: this._cleanup_indexingSweep,
+ cleanup: this._cleanup_indexingSweep,
+ },
+ ],
+ [
+ "folder",
+ {
+ worker: this._worker_folderIndex,
+ recover: this._recover_indexMessage,
+ cleanup: this._cleanup_indexing,
+ },
+ ],
+ [
+ "folderCompact",
+ {
+ worker: this._worker_folderCompactionPass,
+ // compaction enters the folder so needs to know how to leave
+ cleanup: this._cleanup_indexing,
+ },
+ ],
+ [
+ "message",
+ {
+ worker: this._worker_messageIndex,
+ onSchedule: this._schedule_messageIndex,
+ jobCanceled: this._canceled_messageIndex,
+ recover: this._recover_indexMessage,
+ cleanup: this._cleanup_indexing,
+ },
+ ],
+ [
+ "delete",
+ {
+ worker: this._worker_processDeletes,
+ },
+ ],
+
+ [
+ "fixMissingContacts",
+ {
+ worker: this._worker_fixMissingContacts,
+ },
+ ],
+ ];
+ },
+
+ _schemaMigrationInitiated: false,
+ _considerSchemaMigration() {
+ if (
+ !this._schemaMigrationInitiated &&
+ GlodaDatastore._actualSchemaVersion === 26
+ ) {
+ let job = new IndexingJob("fixMissingContacts", null);
+ GlodaIndexer.indexJob(job);
+ this._schemaMigrationInitiated = true;
+ }
+ },
+
+ initialSweep() {
+ this.indexingSweepNeeded = true;
+ },
+
+ _indexingSweepActive: false,
+ /**
+ * Indicate that an indexing sweep is desired. We kick-off an indexing
+ * sweep at start-up and whenever we receive an event-based notification
+ * that we either can't process as an event or that we normally handle
+ * during the sweep pass anyways.
+ */
+ set indexingSweepNeeded(aNeeded) {
+ if (!this._indexingSweepActive && aNeeded) {
+ let job = new IndexingJob("folderSweep", null);
+ job.mappedFolders = false;
+ GlodaIndexer.indexJob(job);
+ this._indexingSweepActive = true;
+ }
+ },
+
+ /**
+ * Performs the folder sweep, locating folders that should be indexed, and
+ * creating a folder indexing job for them, and rescheduling itself for
+ * execution after that job is completed. Once it indexes all the folders,
+ * if we believe we have deletions to process (or just don't know), it kicks
+ * off a deletion processing job.
+ *
+ * Folder traversal logic is based off the spotlight/vista indexer code; we
+ * retrieve the list of servers and folders each time want to find a new
+ * folder to index. This avoids needing to maintain a perfect model of the
+ * folder hierarchy at all times. (We may eventually want to do that, but
+ * this is sufficient and safe for now.) Although our use of dirty flags on
+ * the folders allows us to avoid tracking the 'last folder' we processed,
+ * we do so to avoid getting 'trapped' in a folder with a high rate of
+ * changes.
+ */
+ *_worker_indexingSweep(aJob) {
+ if (!aJob.mappedFolders) {
+ // Walk the folders and make sure all the folders we would want to index
+ // are mapped. Build up a list of GlodaFolders as we go, so that we can
+ // sort them by their indexing priority.
+ let foldersToProcess = (aJob.foldersToProcess = []);
+
+ for (let folder of MailServices.accounts.allFolders) {
+ if (this.shouldIndexFolder(folder)) {
+ foldersToProcess.push(Gloda.getFolderForFolder(folder));
+ }
+ }
+
+ // sort the folders by priority (descending)
+ foldersToProcess.sort(function (a, b) {
+ return b.indexingPriority - a.indexingPriority;
+ });
+
+ aJob.mappedFolders = true;
+ }
+
+ // -- process the folders (in sorted order)
+ while (aJob.foldersToProcess.length) {
+ let glodaFolder = aJob.foldersToProcess.shift();
+ // ignore folders that:
+ // - have been deleted out of existence!
+ // - are not dirty/have not been compacted
+ // - are actively being compacted
+ if (
+ glodaFolder._deleted ||
+ (!glodaFolder.dirtyStatus && !glodaFolder.compacted) ||
+ glodaFolder.compacting
+ ) {
+ continue;
+ }
+
+ // If the folder is marked as compacted, give it a compaction job.
+ if (glodaFolder.compacted) {
+ GlodaIndexer.indexJob(new IndexingJob("folderCompact", glodaFolder.id));
+ }
+
+ // add a job for the folder indexing if it was dirty
+ if (glodaFolder.dirtyStatus) {
+ GlodaIndexer.indexJob(new IndexingJob("folder", glodaFolder.id));
+ }
+
+ // re-schedule this job (although this worker will die)
+ GlodaIndexer.indexJob(aJob);
+ yield GlodaConstants.kWorkDone;
+ }
+
+ // consider deletion
+ if (this.pendingDeletions || this.pendingDeletions === null) {
+ GlodaIndexer.indexJob(new IndexingJob("delete", null));
+ }
+
+ // we don't have any more work to do...
+ this._indexingSweepActive = false;
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * The only state we need to cleanup is that there is no longer an active
+ * indexing sweep.
+ */
+ _cleanup_indexingSweep(aJob) {
+ this._indexingSweepActive = false;
+ },
+
+ /**
+ * The number of headers to look at before yielding with kWorkSync. This
+ * is for time-slicing purposes so we still yield to the UI periodically.
+ */
+ HEADER_CHECK_SYNC_BLOCK_SIZE: 25,
+
+ FOLDER_COMPACTION_PASS_BATCH_SIZE: 512,
+ /**
+ * Special indexing pass for (local) folders than have been compacted. The
+ * compaction can cause message keys to change because message keys in local
+ * folders are simply offsets into the mbox file. Accordingly, we need to
+ * update the gloda records/objects to point them at the new message key.
+ *
+ * Our general algorithm is to perform two traversals in parallel. The first
+ * is a straightforward enumeration of the message headers in the folder that
+ * apparently have been already indexed. These provide us with the message
+ * key and the "gloda-id" property.
+ * The second is a list of tuples containing a gloda message id, its current
+ * message key per the gloda database, and the message-id header. We re-fill
+ * the list with batches on-demand. This allows us to both avoid dispatching
+ * needless UPDATEs as well as deal with messages that were tracked by the
+ * PendingCommitTracker but were discarded by the compaction notification.
+ *
+ * We end up processing two streams of gloda-id's and some extra info. In
+ * the normal case we expect these two streams to line up exactly and all
+ * we need to do is update the message key if it has changed.
+ *
+ * There are a few exceptional cases where things do not line up:
+ * 1) The gloda database knows about a message that the enumerator does not
+ * know about...
+ * a) This message exists in the folder (identified using its message-id
+ * header). This means the message got indexed but PendingCommitTracker
+ * had to forget about the info when the compaction happened. We
+ * re-establish the link and track the message in PendingCommitTracker
+ * again.
+ * b) The message does not exist in the folder. This means the message got
+ * indexed, PendingCommitTracker had to forget about the info, and
+ * then the message either got moved or deleted before now. We mark
+ * the message as deleted; this allows the gloda message to be reused
+ * if the move target has not yet been indexed or purged if it already
+ * has been and the gloda message is a duplicate. And obviously, if the
+ * event that happened was actually a delete, then the delete is the
+ * right thing to do.
+ * 2) The enumerator knows about a message that the gloda database does not
+ * know about. This is unexpected and should not happen. We log a
+ * warning. We are able to differentiate this case from case #1a by
+ * retrieving the message header associated with the next gloda message
+ * (using the message-id header per 1a again). If the gloda message's
+ * message key is after the enumerator's message key then we know this is
+ * case #2. (It implies an insertion in the enumerator stream which is how
+ * we define the unexpected case.)
+ *
+ * Besides updating the database rows, we also need to make sure that
+ * in-memory representations are updated. Immediately after dispatching
+ * UPDATE changes to the database we use the same set of data to walk the
+ * live collections and update any affected messages. We are then able to
+ * discard the information. Although this means that we will have to
+ * potentially walk the live collections multiple times, unless something
+ * has gone horribly wrong, the number of collections should be reasonable
+ * and the lookups are cheap. We bias batch sizes accordingly.
+ *
+ * Because we operate based on chunks we need to make sure that when we
+ * actually deal with multiple chunks that we don't step on our own feet with
+ * our database updates. Since compaction of message key K results in a new
+ * message key K' such that K' <= K, we can reliably issue database
+ * updates for all values <= K. Which means our feet are safe no matter
+ * when we issue the update command. For maximum cache benefit, we issue
+ * our updates prior to our new query since they should still be maximally
+ * hot at that point.
+ */
+ *_worker_folderCompactionPass(aJob, aCallbackHandle) {
+ yield this._indexerEnterFolder(aJob.id);
+
+ // It's conceivable that with a folder sweep we might end up trying to
+ // compact a folder twice. Bail early in this case.
+ if (!this._indexingGlodaFolder.compacted) {
+ yield GlodaConstants.kWorkDone;
+ }
+
+ // this is a forward enumeration (sometimes we reverse enumerate; not here)
+ this._indexerGetEnumerator(this.kEnumIndexedMsgs);
+
+ const HEADER_CHECK_SYNC_BLOCK_SIZE = this.HEADER_CHECK_SYNC_BLOCK_SIZE;
+ const FOLDER_COMPACTION_PASS_BATCH_SIZE =
+ this.FOLDER_COMPACTION_PASS_BATCH_SIZE;
+
+ // Tuples of [gloda id, message key, message-id header] from
+ // folderCompactionPassBlockFetch
+ let glodaIdsMsgKeysHeaderIds = [];
+ // Unpack each tuple from glodaIdsMsgKeysHeaderIds into these guys.
+ // (Initialize oldMessageKey because we use it to kickstart our query.)
+ let oldGlodaId,
+ oldMessageKey = -1,
+ oldHeaderMessageId;
+ // parallel lists of gloda ids and message keys to pass to
+ // GlodaDatastore.updateMessageLocations
+ let updateGlodaIds = [];
+ let updateMessageKeys = [];
+ // list of gloda id's to mark deleted
+ let deleteGlodaIds = [];
+
+ // for GC reasons we need to track the number of headers seen
+ let numHeadersSeen = 0;
+
+ // We are consuming two lists; our loop structure has to reflect that.
+ let headerIter = this._indexingEnumerator[Symbol.iterator]();
+ let mayHaveMoreGlodaMessages = true;
+ let keepIterHeader = false;
+ let keepGlodaTuple = false;
+ let msgHdr = null;
+ while (headerIter || mayHaveMoreGlodaMessages) {
+ let glodaId;
+ if (headerIter) {
+ if (!keepIterHeader) {
+ let result = headerIter.next();
+ if (result.done) {
+ headerIter = null;
+ msgHdr = null;
+ // do the loop check again
+ continue;
+ }
+ msgHdr = result.value;
+ } else {
+ keepIterHeader = false;
+ }
+ }
+
+ if (msgHdr) {
+ numHeadersSeen++;
+ if (numHeadersSeen % HEADER_CHECK_SYNC_BLOCK_SIZE == 0) {
+ yield GlodaConstants.kWorkSync;
+ }
+
+ // There is no need to check with PendingCommitTracker. If a message
+ // somehow got indexed between the time the compaction killed
+ // everything and the time we run, that is a bug.
+ glodaId = msgHdr.getUint32Property(GLODA_MESSAGE_ID_PROPERTY);
+ // (there is also no need to check for gloda dirty since the enumerator
+ // filtered that for us.)
+ }
+
+ // get more [gloda id, message key, message-id header] tuples if out
+ if (!glodaIdsMsgKeysHeaderIds.length && mayHaveMoreGlodaMessages) {
+ // Since we operate on blocks, getting a new block implies we should
+ // flush the last block if applicable.
+ if (updateGlodaIds.length) {
+ GlodaDatastore.updateMessageLocations(
+ updateGlodaIds,
+ updateMessageKeys,
+ aJob.id,
+ true
+ );
+ updateGlodaIds = [];
+ updateMessageKeys = [];
+ }
+
+ if (deleteGlodaIds.length) {
+ GlodaDatastore.markMessagesDeletedByIDs(deleteGlodaIds);
+ deleteGlodaIds = [];
+ }
+
+ GlodaDatastore.folderCompactionPassBlockFetch(
+ aJob.id,
+ oldMessageKey + 1,
+ FOLDER_COMPACTION_PASS_BATCH_SIZE,
+ aCallbackHandle.wrappedCallback
+ );
+ glodaIdsMsgKeysHeaderIds = yield GlodaConstants.kWorkAsync;
+ // Reverse so we can use pop instead of shift and I don't need to be
+ // paranoid about performance.
+ glodaIdsMsgKeysHeaderIds.reverse();
+
+ if (!glodaIdsMsgKeysHeaderIds.length) {
+ mayHaveMoreGlodaMessages = false;
+
+ // We shouldn't be in the loop anymore if headerIter is dead now.
+ if (!headerIter) {
+ break;
+ }
+ }
+ }
+
+ if (!keepGlodaTuple) {
+ if (mayHaveMoreGlodaMessages) {
+ [oldGlodaId, oldMessageKey, oldHeaderMessageId] =
+ glodaIdsMsgKeysHeaderIds.pop();
+ } else {
+ oldGlodaId = oldMessageKey = oldHeaderMessageId = null;
+ }
+ } else {
+ keepGlodaTuple = false;
+ }
+
+ // -- normal expected case
+ if (glodaId == oldGlodaId) {
+ // only need to do something if the key is not right
+ if (msgHdr.messageKey != oldMessageKey) {
+ updateGlodaIds.push(glodaId);
+ updateMessageKeys.push(msgHdr.messageKey);
+ }
+ } else {
+ // -- exceptional cases
+ // This should always return a value unless something is very wrong.
+ // We do not want to catch the exception if one happens.
+ let idBasedHeader = oldHeaderMessageId
+ ? this._indexingDatabase.getMsgHdrForMessageID(oldHeaderMessageId)
+ : false;
+ // - Case 1b.
+ // We want to mark the message as deleted.
+ if (idBasedHeader == null) {
+ deleteGlodaIds.push(oldGlodaId);
+ } else if (
+ idBasedHeader &&
+ ((msgHdr && idBasedHeader.messageKey < msgHdr.messageKey) || !msgHdr)
+ ) {
+ // - Case 1a
+ // The expected case is that the message referenced by the gloda
+ // database precedes the header the enumerator told us about. This
+ // is expected because if PendingCommitTracker did not mark the
+ // message as indexed/clean then the enumerator would not tell us
+ // about it.
+ // Also, if we ran out of headers from the enumerator, this is a dead
+ // giveaway that this is the expected case.
+ // tell the pending commit tracker about the gloda database one
+ PendingCommitTracker.track(idBasedHeader, oldGlodaId);
+ // and we might need to update the message key too
+ if (idBasedHeader.messageKey != oldMessageKey) {
+ updateGlodaIds.push(oldGlodaId);
+ updateMessageKeys.push(idBasedHeader.messageKey);
+ }
+ // Take another pass through the loop so that we check the
+ // enumerator header against the next message in the gloda
+ // database.
+ keepIterHeader = true;
+ } else if (msgHdr) {
+ // - Case 2
+ // Whereas if the message referenced by gloda has a message key
+ // greater than the one returned by the enumerator, then we have a
+ // header claiming to be indexed by gloda that gloda does not
+ // actually know about. This is exceptional and gets a warning.
+ this._log.warn(
+ "Observed header that claims to be gloda indexed " +
+ "but that gloda has never heard of during " +
+ "compaction." +
+ " In folder: " +
+ msgHdr.folder.URI +
+ " sketchy key: " +
+ msgHdr.messageKey +
+ " subject: " +
+ msgHdr.mime2DecodedSubject
+ );
+ // Keep this tuple around for the next enumerator provided header
+ keepGlodaTuple = true;
+ }
+ }
+ }
+ // If we don't flush the update, no one will!
+ if (updateGlodaIds.length) {
+ GlodaDatastore.updateMessageLocations(
+ updateGlodaIds,
+ updateMessageKeys,
+ aJob.id,
+ true
+ );
+ }
+ if (deleteGlodaIds.length) {
+ GlodaDatastore.markMessagesDeletedByIDs(deleteGlodaIds);
+ }
+
+ this._indexingGlodaFolder._setCompactedState(false);
+
+ this._indexerLeaveFolder();
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Index the contents of a folder.
+ */
+ *_worker_folderIndex(aJob, aCallbackHandle) {
+ yield this._indexerEnterFolder(aJob.id);
+
+ if (!this.shouldIndexFolder(this._indexingFolder)) {
+ aJob.safelyInvokeCallback(true);
+ yield GlodaConstants.kWorkDone;
+ }
+
+ // Make sure listeners get notified about this job.
+ GlodaIndexer._notifyListeners();
+
+ // there is of course a cost to all this header investigation even if we
+ // don't do something. so we will yield with kWorkSync for every block.
+ const HEADER_CHECK_SYNC_BLOCK_SIZE = this.HEADER_CHECK_SYNC_BLOCK_SIZE;
+
+ // we can safely presume if we are here that this folder has been selected
+ // for offline processing...
+
+ // -- Filthy Folder
+ // A filthy folder may have misleading properties on the message that claim
+ // the message is indexed. They are misleading because the database, for
+ // whatever reason, does not have the messages (accurately) indexed.
+ // We need to walk all the messages and mark them filthy if they have a
+ // dirty property. Once we have done this, we can downgrade the folder's
+ // dirty status to plain dirty. We do this rather than trying to process
+ // everyone in one go in a filthy context because if we have to terminate
+ // indexing before we quit, we don't want to have to re-index messages next
+ // time. (This could even lead to never completing indexing in a
+ // pathological situation.)
+ let glodaFolder = GlodaDatastore._mapFolder(this._indexingFolder);
+ if (glodaFolder.dirtyStatus == glodaFolder.kFolderFilthy) {
+ this._indexerGetEnumerator(this.kEnumIndexedMsgs, true);
+ let count = 0;
+ for (let msgHdr of this._indexingEnumerator) {
+ // we still need to avoid locking up the UI, pause periodically...
+ if (++count % HEADER_CHECK_SYNC_BLOCK_SIZE == 0) {
+ yield GlodaConstants.kWorkSync;
+ }
+
+ let glodaMessageId = msgHdr.getUint32Property(
+ GLODA_MESSAGE_ID_PROPERTY
+ );
+ // if it has a gloda message id, we need to mark it filthy
+ if (glodaMessageId != 0) {
+ msgHdr.setUint32Property(GLODA_DIRTY_PROPERTY, this.kMessageFilthy);
+ }
+ // if it doesn't have a gloda message id, we will definitely index it,
+ // so no action is required.
+ }
+ // Commit the filthy status changes to the message database.
+ this._indexingDatabase.commit(Ci.nsMsgDBCommitType.kLargeCommit);
+
+ // this will automatically persist to the database
+ glodaFolder._downgradeDirtyStatus(glodaFolder.kFolderDirty);
+ }
+
+ // Figure out whether we're supposed to index _everything_ or just what
+ // has not yet been indexed.
+ let force = "force" in aJob && aJob.force;
+ let enumeratorType = force ? this.kEnumAllMsgs : this.kEnumMsgsToIndex;
+
+ // Pass 1: count the number of messages to index.
+ // We do this in order to be able to report to the user what we're doing.
+ // TODO: give up after reaching a certain number of messages in folders
+ // with ridiculous numbers of messages and make the interface just say
+ // something like "over N messages to go."
+
+ this._indexerGetEnumerator(enumeratorType);
+
+ let numMessagesToIndex = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (let ignore of this._indexingEnumerator) {
+ // We're only counting, so do bigger chunks on this pass.
+ ++numMessagesToIndex;
+ if (numMessagesToIndex % (HEADER_CHECK_SYNC_BLOCK_SIZE * 8) == 0) {
+ yield GlodaConstants.kWorkSync;
+ }
+ }
+
+ aJob.goal = numMessagesToIndex;
+
+ if (numMessagesToIndex > 0) {
+ // We used up the iterator, get a new one.
+ this._indexerGetEnumerator(enumeratorType);
+
+ // Pass 2: index the messages.
+ let count = 0;
+ for (let msgHdr of this._indexingEnumerator) {
+ // per above, we want to periodically release control while doing all
+ // this header traversal/investigation.
+ if (++count % HEADER_CHECK_SYNC_BLOCK_SIZE == 0) {
+ yield GlodaConstants.kWorkSync;
+ }
+
+ // To keep our counts more accurate, increment the offset before
+ // potentially skipping any messages.
+ ++aJob.offset;
+
+ // Skip messages that have not yet been reported to us as existing via
+ // msgsClassified.
+ if (
+ this._indexingFolder.getProcessingFlags(msgHdr.messageKey) &
+ NOT_YET_REPORTED_PROCESSING_FLAGS
+ ) {
+ continue;
+ }
+
+ // Because the gloda id could be in-flight, we need to double-check the
+ // enumerator here since it can't know about our in-memory stuff.
+ let [glodaId, glodaDirty] = PendingCommitTracker.getGlodaState(msgHdr);
+ // if the message seems valid and we are not forcing indexing, skip it.
+ // (that means good gloda id and not dirty)
+ if (
+ !force &&
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ glodaDirty == this.kMessageClean
+ ) {
+ continue;
+ }
+
+ this._log.debug(">>> calling _indexMessage");
+ yield aCallbackHandle.pushAndGo(
+ this._indexMessage(msgHdr, aCallbackHandle),
+ { what: "indexMessage", msgHdr }
+ );
+ GlodaIndexer._indexedMessageCount++;
+ this._log.debug("<<< back from _indexMessage");
+ }
+ }
+
+ // This will trigger an (async) db update which cannot hit the disk prior to
+ // the actual database records that constitute the clean state.
+ // XXX There is the slight possibility that, in the event of a crash, this
+ // will hit the disk but the gloda-id properties on the headers will not
+ // get set. This should ideally be resolved by detecting a non-clean
+ // shutdown and marking all folders as dirty.
+ glodaFolder._downgradeDirtyStatus(glodaFolder.kFolderClean);
+
+ // by definition, it's not likely we'll visit this folder again anytime soon
+ this._indexerLeaveFolder();
+
+ aJob.safelyInvokeCallback(true);
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Invoked when a "message" job is scheduled so that we can clear
+ * _pendingAddJob if that is the job. We do this so that work items are not
+ * added to _pendingAddJob while it is being processed.
+ */
+ _schedule_messageIndex(aJob, aCallbackHandle) {
+ // we do not want new work items to be added as we are processing, so
+ // clear _pendingAddJob. A new job will be created as needed.
+ if (aJob === this._pendingAddJob) {
+ this._pendingAddJob = null;
+ }
+ // update our goal from the items length
+ aJob.goal = aJob.items.length;
+ },
+ /**
+ * If the job gets canceled, we need to make sure that we clear out pending
+ * add job or our state will get wonky.
+ */
+ _canceled_messageIndex(aJob) {
+ if (aJob === this._pendingAddJob) {
+ this._pendingAddJob = null;
+ }
+ },
+
+ /**
+ * Index a specific list of messages that we know to index from
+ * event-notification hints.
+ */
+ *_worker_messageIndex(aJob, aCallbackHandle) {
+ // if we are already in the correct folder, our "get in the folder" clause
+ // will not execute, so we need to make sure this value is accurate in
+ // that case. (and we want to avoid multiple checks...)
+ for (; aJob.offset < aJob.items.length; aJob.offset++) {
+ let item = aJob.items[aJob.offset];
+ // item is either [folder ID, message key] or
+ // [folder ID, message ID]
+
+ let glodaFolderId = item[0];
+ // If the folder has been deleted since we queued, skip this message
+ if (!GlodaDatastore._folderIdKnown(glodaFolderId)) {
+ continue;
+ }
+ let glodaFolder = GlodaDatastore._mapFolderID(glodaFolderId);
+
+ // Stay out of folders that:
+ // - are compacting / compacted and not yet processed
+ // - got deleted (this would be redundant if we had a stance on id nukage)
+ // (these things could have changed since we queued the event)
+ if (
+ glodaFolder.compacting ||
+ glodaFolder.compacted ||
+ glodaFolder._deleted
+ ) {
+ continue;
+ }
+
+ // get in the folder
+ if (this._indexingGlodaFolder != glodaFolder) {
+ yield this._indexerEnterFolder(glodaFolderId);
+
+ // Now that we have the real nsIMsgFolder, sanity-check that we should
+ // be indexing it. (There are some checks that require the
+ // nsIMsgFolder.)
+ if (!this.shouldIndexFolder(this._indexingFolder)) {
+ continue;
+ }
+ }
+
+ let msgHdr;
+ // GetMessageHeader can be affected by the use cache, so we need to check
+ // ContainsKey first to see if the header is really actually there.
+ if (typeof item[1] == "number") {
+ msgHdr =
+ this._indexingDatabase.containsKey(item[1]) &&
+ this._indexingFolder.GetMessageHeader(item[1]);
+ } else {
+ // Same deal as in move processing.
+ // TODO fixme to not assume singular message-id's.
+ msgHdr = this._indexingDatabase.getMsgHdrForMessageID(item[1]);
+ }
+
+ if (msgHdr) {
+ yield aCallbackHandle.pushAndGo(
+ this._indexMessage(msgHdr, aCallbackHandle),
+ { what: "indexMessage", msgHdr }
+ );
+ } else {
+ yield GlodaConstants.kWorkSync;
+ }
+ }
+
+ // There is no real reason to stay 'in' the folder. If we are going to get
+ // more events from the folder, its database would have to be open for us
+ // to get the events, so it's not like we're creating an efficiency
+ // problem where we unload a folder just to load it again in 2 seconds.
+ // (Well, at least assuming the views are good about holding onto the
+ // database references even though they go out of their way to avoid
+ // holding onto message header references.)
+ this._indexerLeaveFolder();
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Recover from a "folder" or "message" job failing inside a call to
+ * |_indexMessage|, marking the message bad. If we were not in an
+ * |_indexMessage| call, then fail to recover.
+ *
+ * @param aJob The job that was being worked. We ignore this for now.
+ * @param aContextStack The callbackHandle mechanism's context stack. When we
+ * invoke pushAndGo for _indexMessage we put something in so we can
+ * detect when it is on the async stack.
+ * @param aException The exception that is necessitating we attempt to
+ * recover.
+ *
+ * @returns 1 if we were able to recover (because we want the call stack
+ * popped down to our worker), false if we can't.
+ */
+ _recover_indexMessage(aJob, aContextStack, aException) {
+ // See if indexMessage is on the stack...
+ if (
+ aContextStack.length >= 2 &&
+ aContextStack[1] &&
+ "what" in aContextStack[1] &&
+ aContextStack[1].what == "indexMessage"
+ ) {
+ // it is, so this is probably recoverable.
+
+ this._log.debug(
+ "Exception while indexing message, marking it bad (gloda id of 1)."
+ );
+
+ // -- Mark the message as bad
+ let msgHdr = aContextStack[1].msgHdr;
+ // (In the worst case, the header is no longer valid, which will result in
+ // exceptions. We need to be prepared for that.)
+ try {
+ msgHdr.setUint32Property(
+ GLODA_MESSAGE_ID_PROPERTY,
+ GLODA_BAD_MESSAGE_ID
+ );
+ // clear the dirty bit if it has one
+ if (msgHdr.getUint32Property(GLODA_DIRTY_PROPERTY)) {
+ msgHdr.setUint32Property(GLODA_DIRTY_PROPERTY, 0);
+ }
+ } catch (ex) {
+ // If we are indexing a folder and the message header is no longer
+ // valid, then it's quite likely the whole folder is no longer valid.
+ // But since in the event-driven message indexing case we could have
+ // other valid things to look at, let's try and recover. The folder
+ // indexing case will come back to us shortly and we will indicate
+ // recovery is not possible at that point.
+ // So do nothing here since by popping the indexing of the specific
+ // message out of existence we are recovering.
+ }
+ return 1;
+ }
+ return false;
+ },
+
+ /**
+ * Cleanup after an aborted "folder" or "message" job.
+ */
+ _cleanup_indexing(aJob) {
+ this._indexerLeaveFolder();
+ aJob.safelyInvokeCallback(false);
+ },
+
+ /**
+ * Maximum number of deleted messages to process at a time. Arbitrary; there
+ * are no real known performance constraints at this point.
+ */
+ DELETED_MESSAGE_BLOCK_SIZE: 32,
+
+ /**
+ * Process pending deletes...
+ */
+ *_worker_processDeletes(aJob, aCallbackHandle) {
+ // Count the number of messages we will eventually process. People freak
+ // out when the number is constantly increasing because they think gloda
+ // has gone rogue. (Note: new deletions can still accumulate during
+ // our execution, so we may 'expand' our count a little still.)
+ this._datastore.countDeletedMessages(aCallbackHandle.wrappedCallback);
+ aJob.goal = yield GlodaConstants.kWorkAsync;
+ this._log.debug(
+ "There are currently " +
+ aJob.goal +
+ " messages awaiting" +
+ " deletion processing."
+ );
+
+ // get a block of messages to delete.
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ query._deleted(1);
+ query.limit(this.DELETED_MESSAGE_BLOCK_SIZE);
+ let deletedCollection = query.getCollection(aCallbackHandle);
+ yield GlodaConstants.kWorkAsync;
+
+ while (deletedCollection.items.length) {
+ for (let message of deletedCollection.items) {
+ // If it turns out our count is wrong (because some new deletions
+ // happened since we entered this worker), let's issue a new count
+ // and use that to accurately update our goal.
+ if (aJob.offset >= aJob.goal) {
+ this._datastore.countDeletedMessages(aCallbackHandle.wrappedCallback);
+ aJob.goal += yield GlodaConstants.kWorkAsync;
+ }
+
+ yield aCallbackHandle.pushAndGo(
+ this._deleteMessage(message, aCallbackHandle)
+ );
+ aJob.offset++;
+ yield GlodaConstants.kWorkSync;
+ }
+
+ deletedCollection = query.getCollection(aCallbackHandle);
+ yield GlodaConstants.kWorkAsync;
+ }
+ this.pendingDeletions = false;
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ *_worker_fixMissingContacts(aJob, aCallbackHandle) {
+ let identityContactInfos = [];
+
+ // -- asynchronously get a list of all identities without contacts
+ // The upper bound on the number of messed up contacts is the number of
+ // contacts in the user's address book. This should be small enough
+ // (and the data size small enough) that this won't explode thunderbird.
+ let queryStmt = GlodaDatastore._createAsyncStatement(
+ "SELECT identities.id, identities.contactID, identities.value " +
+ "FROM identities " +
+ "LEFT JOIN contacts ON identities.contactID = contacts.id " +
+ "WHERE identities.kind = 'email' AND contacts.id IS NULL",
+ true
+ );
+ queryStmt.executeAsync({
+ handleResult(aResultSet) {
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ identityContactInfos.push({
+ identityId: row.getInt64(0),
+ contactId: row.getInt64(1),
+ email: row.getString(2),
+ });
+ }
+ },
+ handleError(aError) {},
+ handleCompletion(aReason) {
+ GlodaDatastore._asyncCompleted();
+ aCallbackHandle.wrappedCallback();
+ },
+ });
+ queryStmt.finalize();
+ GlodaDatastore._pendingAsyncStatements++;
+ yield GlodaConstants.kWorkAsync;
+
+ // -- perform fixes only if there were missing contacts
+ if (identityContactInfos.length) {
+ const yieldEvery = 64;
+ // - create the missing contacts
+ for (let i = 0; i < identityContactInfos.length; i++) {
+ if (i % yieldEvery === 0) {
+ yield GlodaConstants.kWorkSync;
+ }
+
+ let info = identityContactInfos[i],
+ card = MailServices.ab.cardForEmailAddress(info.email),
+ contact = new GlodaContact(
+ GlodaDatastore,
+ info.contactId,
+ null,
+ null,
+ card ? card.displayName || info.email : info.email,
+ 0,
+ 0
+ );
+ GlodaDatastore.insertContact(contact);
+
+ // update the in-memory rep of the identity to know about the contact
+ // if there is one.
+ let identity = GlodaCollectionManager.cacheLookupOne(
+ GlodaConstants.NOUN_IDENTITY,
+ info.identityId,
+ false
+ );
+ if (identity) {
+ // Unfortunately, although this fixes the (reachable) Identity and
+ // exposes the Contact, it does not make the Contact reachable from
+ // the collection manager. This will make explicit queries that look
+ // up the contact potentially see the case where
+ // contact.identities[0].contact !== contact. Alternately, that
+ // may not happen and instead the "contact" object we created above
+ // may become unlinked. (I'd have to trace some logic I don't feel
+ // like tracing.) Either way, The potential fallout is minimal
+ // since the object identity invariant will just lapse and popularity
+ // on the contact may become stale, and neither of those meaningfully
+ // affect the operation of anything in Thunderbird.
+ // If we really cared, we could find all the dominant collections
+ // that reference the identity and update their corresponding
+ // contact collection to make it reachable. That use-case does not
+ // exist outside of here, which is why we're punting.
+ identity._contact = contact;
+ contact._identities = [identity];
+ }
+
+ // NOTE: If the addressbook indexer did anything useful other than
+ // adapting to name changes, we could schedule indexing of the cards at
+ // this time. However, as of this writing, it doesn't, and this task
+ // is a one-off relevant only to the time of this writing.
+ }
+
+ // - mark all folders as dirty, initiate indexing sweep
+ this.dirtyAllKnownFolders();
+ this.indexingSweepNeeded = true;
+ }
+
+ // -- mark the schema upgrade, be done
+ GlodaDatastore._updateSchemaVersion(GlodaDatastore._schemaVersion);
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Determine whether a folder is suitable for indexing.
+ *
+ * @param aMsgFolder An nsIMsgFolder you want to see if we should index.
+ *
+ * @returns true if we want to index messages in this type of folder, false if
+ * we do not.
+ */
+ shouldIndexFolder(aMsgFolder) {
+ let folderFlags = aMsgFolder.flags;
+ // Completely ignore non-mail and virtual folders. They should never even
+ // get to be GlodaFolder instances.
+ if (
+ !(folderFlags & Ci.nsMsgFolderFlags.Mail) ||
+ folderFlags & Ci.nsMsgFolderFlags.Virtual
+ ) {
+ return false;
+ }
+
+ // Some folders do not really exist; we can detect this by getStringProperty
+ // exploding when we call it. This is primarily a concern because
+ // _mapFolder calls said exploding method, but we also don't want to
+ // even think about indexing folders that don't exist. (Such folders are
+ // likely the result of a messed up profile.)
+ try {
+ // flags is used because it should always be in the cache avoiding a miss
+ // which would compel an msf open.
+ aMsgFolder.getStringProperty("flags");
+ } catch (ex) {
+ return false;
+ }
+
+ // Now see what our gloda folder information has to say about the folder.
+ let glodaFolder = GlodaDatastore._mapFolder(aMsgFolder);
+ return glodaFolder.indexingPriority != glodaFolder.kIndexingNeverPriority;
+ },
+
+ /**
+ * Sets the indexing priority for this folder and persists it both to Gloda,
+ * and, for backup purposes, to the nsIMsgFolder via string property as well.
+ *
+ * Setting this priority may cause the indexer to either reindex this folder,
+ * or remove this folder from the existing index.
+ *
+ * @param {nsIMsgFolder} aFolder
+ * @param {number} aPriority (one of the priority constants from GlodaFolder)
+ */
+ setFolderIndexingPriority(aFolder, aPriority) {
+ let glodaFolder = GlodaDatastore._mapFolder(aFolder);
+
+ // if there's been no change, we're done
+ if (aPriority == glodaFolder.indexingPriority) {
+ return;
+ }
+
+ // save off the old priority, and set the new one
+ let previousPrio = glodaFolder.indexingPriority;
+ glodaFolder._indexingPriority = aPriority;
+
+ // persist the new priority
+ GlodaDatastore.updateFolderIndexingPriority(glodaFolder);
+ aFolder.setStringProperty("indexingPriority", Number(aPriority).toString());
+
+ // if we've been told never to index this folder...
+ if (aPriority == glodaFolder.kIndexingNeverPriority) {
+ // stop doing so
+ if (this._indexingFolder == aFolder) {
+ GlodaIndexer.killActiveJob();
+ }
+
+ // mark all existing messages as deleted
+ GlodaDatastore.markMessagesDeletedByFolderID(glodaFolder.id);
+
+ // re-index
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ } else if (previousPrio == glodaFolder.kIndexingNeverPriority) {
+ // there's no existing index, but the user now wants one
+ glodaFolder._dirtyStatus = glodaFolder.kFolderFilthy;
+ GlodaDatastore.updateFolderDirtyStatus(glodaFolder);
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ }
+ },
+
+ /**
+ * Resets the indexing priority on the given folder to whatever the default
+ * is for folders of that type.
+ *
+ * @note Calls setFolderIndexingPriority under the hood, so has identical
+ * potential reindexing side-effects
+ *
+ * @param {nsIMsgFolder} aFolder
+ * @param {boolean} aAllowSpecialFolderIndexing
+ */
+ resetFolderIndexingPriority(aFolder, aAllowSpecialFolderIndexing) {
+ this.setFolderIndexingPriority(
+ aFolder,
+ GlodaDatastore.getDefaultIndexingPriority(
+ aFolder,
+ aAllowSpecialFolderIndexing
+ )
+ );
+ },
+
+ /**
+ * Queue all of the folders of all of the accounts of the current profile
+ * for indexing. We traverse all folders and queue them immediately to try
+ * and have an accurate estimate of the number of folders that need to be
+ * indexed. (We previously queued accounts rather than immediately
+ * walking their list of folders.)
+ */
+ indexEverything() {
+ this._log.info("Queueing all accounts for indexing.");
+
+ GlodaDatastore._beginTransaction();
+ for (let account of MailServices.accounts.accounts) {
+ this.indexAccount(account);
+ }
+ GlodaDatastore._commitTransaction();
+ },
+
+ /**
+ * Queue all of the folders belonging to an account for indexing.
+ */
+ indexAccount(aAccount) {
+ let rootFolder = aAccount.incomingServer.rootFolder;
+ if (rootFolder instanceof Ci.nsIMsgFolder) {
+ this._log.info("Queueing account folders for indexing: " + aAccount.key);
+
+ for (let folder of rootFolder.descendants) {
+ if (this.shouldIndexFolder(folder)) {
+ GlodaIndexer.indexJob(
+ new IndexingJob("folder", GlodaDatastore._mapFolder(folder).id)
+ );
+ }
+ }
+ } else {
+ this._log.info("Skipping Account, root folder not nsIMsgFolder");
+ }
+ },
+
+ /**
+ * Queue a single folder for indexing given an nsIMsgFolder.
+ *
+ * @param [aOptions.callback] A callback to invoke when the folder finishes
+ * indexing. First argument is true if the task ran to completion
+ * successfully, false if we had to abort for some reason.
+ * @param [aOptions.force=false] Should we force the indexing of all messages
+ * in the folder (true) or just index what hasn't been indexed (false).
+ * @returns true if we are going to index the folder, false if not.
+ */
+ indexFolder(aMsgFolder, aOptions) {
+ if (!this.shouldIndexFolder(aMsgFolder)) {
+ return false;
+ }
+ let glodaFolder = GlodaDatastore._mapFolder(aMsgFolder);
+ // stay out of compacting/compacted folders
+ if (glodaFolder.compacting || glodaFolder.compacted) {
+ return false;
+ }
+
+ this._log.info("Queue-ing folder for indexing: " + aMsgFolder.prettyName);
+ let job = new IndexingJob("folder", glodaFolder.id);
+ if (aOptions) {
+ if ("callback" in aOptions) {
+ job.callback = aOptions.callback;
+ }
+ if ("force" in aOptions) {
+ job.force = true;
+ }
+ }
+ GlodaIndexer.indexJob(job);
+ return true;
+ },
+
+ /**
+ * Queue a list of messages for indexing.
+ *
+ * @param aFoldersAndMessages List of [nsIMsgFolder, message key] tuples.
+ */
+ indexMessages(aFoldersAndMessages) {
+ let job = new IndexingJob("message", null);
+ job.items = aFoldersAndMessages.map(fm => [
+ GlodaDatastore._mapFolder(fm[0]).id,
+ fm[1],
+ ]);
+ GlodaIndexer.indexJob(job);
+ },
+
+ /**
+ * Mark all known folders as dirty so that the next indexing sweep goes
+ * into all folders and checks their contents to see if they need to be
+ * indexed.
+ *
+ * This is being added for the migration case where we want to try and reindex
+ * all of the messages that had been marked with GLODA_BAD_MESSAGE_ID but
+ * which is now GLODA_OLD_BAD_MESSAGE_ID and so we should attempt to reindex
+ * them.
+ */
+ dirtyAllKnownFolders() {
+ // Just iterate over the datastore's folder map and tell each folder to
+ // be dirty if its priority is not disabled.
+ for (let folderID in GlodaDatastore._folderByID) {
+ let glodaFolder = GlodaDatastore._folderByID[folderID];
+ if (glodaFolder.indexingPriority !== glodaFolder.kIndexingNeverPriority) {
+ glodaFolder._ensureFolderDirty();
+ }
+ }
+ },
+
+ /**
+ * Given a message header, return whether this message is likely to have
+ * been indexed or not.
+ *
+ * This means the message must:
+ * - Be in a folder eligible for gloda indexing. (Not News, etc.)
+ * - Be in a non-filthy folder.
+ * - Be gloda-indexed and non-filthy.
+ *
+ * @param aMsgHdr A message header.
+ * @returns true if the message is likely to have been indexed.
+ */
+ isMessageIndexed(aMsgHdr) {
+ // If it's in a folder that we flat out do not index, say no.
+ if (!this.shouldIndexFolder(aMsgHdr.folder)) {
+ return false;
+ }
+ let glodaFolder = GlodaDatastore._mapFolder(aMsgHdr.folder);
+ let [glodaId, glodaDirty] = PendingCommitTracker.getGlodaState(aMsgHdr);
+ return (
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ glodaDirty != GlodaMsgIndexer.kMessageFilthy &&
+ glodaFolder &&
+ glodaFolder.dirtyStatus != glodaFolder.kFolderFilthy
+ );
+ },
+
+ /* *********** Event Processing *********** */
+
+ /**
+ * Tracks messages we have received msgKeyChanged notifications for in order
+ * to provide batching and to suppress needless reindexing when we receive
+ * the expected follow-up msgsClassified notification.
+ *
+ * The entries in this dictionary should be extremely short-lived as we
+ * receive the msgKeyChanged notification as the offline fake header is
+ * converted into a real header (which is accompanied by a msgAdded
+ * notification we don't pay attention to). Once the headers finish
+ * updating, the message classifier will get its at-bat and should likely
+ * find that the messages have already been classified and so fast-path
+ * them.
+ *
+ * The keys in this dictionary are chosen to be consistent with those of
+ * PendingCommitTracker: the folder.URI + "#" + the (new) message key.
+ * The values in the dictionary are either an object with "id" (the gloda
+ * id), "key" (the new message key), and "dirty" (is it dirty and so
+ * should still be queued for indexing) attributes, or null indicating that
+ * no change in message key occurred and so no database changes are required.
+ */
+ _keyChangedBatchInfo: {},
+
+ /**
+ * Common logic for things that want to feed event-driven indexing. This gets
+ * called by both |_msgFolderListener.msgsClassified| when we are first
+ * seeing a message as well as by |_folderListener| when things happen to
+ * existing messages. Although we could slightly specialize for the
+ * new-to-us case, it works out to be cleaner to just treat them the same
+ * and take a very small performance hit.
+ *
+ * @param aMsgHdrs array of messages to treat as potentially changed.
+ * @param aDirtyingEvent Is this event inherently dirtying? Receiving a
+ * msgsClassified notification is not inherently dirtying because it is
+ * just telling us that a message exists. We use this knowledge to
+ * ignore the msgsClassified notifications for messages we have received
+ * msgKeyChanged notifications for and fast-pathed. Since it is possible
+ * for user action to do something that dirties the message between the
+ * time we get the msgKeyChanged notification and when we receive the
+ * msgsClassified notification, we want to make sure we don't get
+ * confused. (Although since we remove the message from our ignore-set
+ * after the first notification, we would likely just mistakenly treat
+ * the msgsClassified notification as something dirtying, so it would
+ * still work out...)
+ */
+ _reindexChangedMessages(aMsgHdrs, aDirtyingEvent) {
+ let glodaIdsNeedingDeletion = null;
+ let messageKeyChangedIds = null,
+ messageKeyChangedNewKeys = null;
+ for (let msgHdr of aMsgHdrs) {
+ // -- Index this folder?
+ let msgFolder = msgHdr.folder;
+ if (!this.shouldIndexFolder(msgFolder)) {
+ continue;
+ }
+ // -- Ignore messages in filthy folders!
+ // A filthy folder can only be processed by an indexing sweep, and at
+ // that point the message will get indexed.
+ let glodaFolder = GlodaDatastore._mapFolder(msgHdr.folder);
+ if (glodaFolder.dirtyStatus == glodaFolder.kFolderFilthy) {
+ continue;
+ }
+
+ // -- msgKeyChanged event follow-up
+ if (!aDirtyingEvent) {
+ let keyChangedKey = msgHdr.folder.URI + "#" + msgHdr.messageKey;
+ if (keyChangedKey in this._keyChangedBatchInfo) {
+ var keyChangedInfo = this._keyChangedBatchInfo[keyChangedKey];
+ delete this._keyChangedBatchInfo[keyChangedKey];
+
+ // Null means to ignore this message because the key did not change
+ // (and the message was not dirty so it is safe to ignore.)
+ if (keyChangedInfo == null) {
+ continue;
+ }
+ // (the key may be null if we only generated the entry because the
+ // message was dirty)
+ if (keyChangedInfo.key !== null) {
+ if (messageKeyChangedIds == null) {
+ messageKeyChangedIds = [];
+ messageKeyChangedNewKeys = [];
+ }
+ messageKeyChangedIds.push(keyChangedInfo.id);
+ messageKeyChangedNewKeys.push(keyChangedInfo.key);
+ }
+ // ignore the message because it was not dirty
+ if (!keyChangedInfo.isDirty) {
+ continue;
+ }
+ }
+ }
+
+ // -- Index this message?
+ // We index local messages, IMAP messages that are offline, and IMAP
+ // messages that aren't offline but whose folders aren't offline either
+ let isFolderLocal = msgFolder instanceof Ci.nsIMsgLocalMailFolder;
+ if (!isFolderLocal) {
+ if (
+ !(msgHdr.flags & Ci.nsMsgMessageFlags.Offline) &&
+ msgFolder.getFlag(Ci.nsMsgFolderFlags.Offline)
+ ) {
+ continue;
+ }
+ }
+ // Ignore messages whose processing flags indicate it has not yet been
+ // classified. In the IMAP case if the Offline flag is going to get set
+ // we are going to see it before the msgsClassified event so this is
+ // very important.
+ if (
+ msgFolder.getProcessingFlags(msgHdr.messageKey) &
+ NOT_YET_REPORTED_PROCESSING_FLAGS
+ ) {
+ continue;
+ }
+
+ let [glodaId, glodaDirty] = PendingCommitTracker.getGlodaState(msgHdr);
+
+ let isSpam =
+ msgHdr.getStringProperty(JUNK_SCORE_PROPERTY) == JUNK_SPAM_SCORE_STR;
+
+ // -- Is the message currently gloda indexed?
+ if (
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ glodaDirty != this.kMessageFilthy
+ ) {
+ // - Is the message spam?
+ if (isSpam) {
+ // Treat this as a deletion...
+ if (!glodaIdsNeedingDeletion) {
+ glodaIdsNeedingDeletion = [];
+ }
+ glodaIdsNeedingDeletion.push(glodaId);
+ // and skip to the next message
+ continue;
+ }
+
+ // - Mark the message dirty if it is clean.
+ // (This is the only case in which we need to mark dirty so that the
+ // indexing sweep takes care of things if we don't process this in
+ // an event-driven fashion. If the message has no gloda-id or does
+ // and it's already dirty or filthy, it is already marked for
+ // indexing.)
+ if (glodaDirty == this.kMessageClean) {
+ msgHdr.setUint32Property(GLODA_DIRTY_PROPERTY, this.kMessageDirty);
+ }
+ // if the message is pending clean, this change invalidates that.
+ PendingCommitTracker.noteDirtyHeader(msgHdr);
+ } else if (isSpam) {
+ // If it's not indexed but is spam, ignore it.
+ continue;
+ }
+ // (we want to index the message if we are here)
+
+ // mark the folder dirty too, so we know to look inside
+ glodaFolder._ensureFolderDirty();
+
+ if (this._pendingAddJob == null) {
+ this._pendingAddJob = new IndexingJob("message", null);
+ GlodaIndexer.indexJob(this._pendingAddJob);
+ }
+ // only queue the message if we haven't overflowed our event-driven budget
+ if (this._pendingAddJob.items.length < this._indexMaxEventQueueMessages) {
+ this._pendingAddJob.items.push([
+ GlodaDatastore._mapFolder(msgFolder).id,
+ msgHdr.messageKey,
+ ]);
+ } else {
+ this.indexingSweepNeeded = true;
+ }
+ }
+
+ // Process any message key changes (from earlier msgKeyChanged events)
+ if (messageKeyChangedIds != null) {
+ GlodaDatastore.updateMessageKeys(
+ messageKeyChangedIds,
+ messageKeyChangedNewKeys
+ );
+ }
+
+ // If we accumulated any deletions in there, batch them off now.
+ if (glodaIdsNeedingDeletion) {
+ GlodaDatastore.markMessagesDeletedByIDs(glodaIdsNeedingDeletion);
+ this.pendingDeletions = true;
+ }
+ },
+
+ /* ***** Folder Changes ***** */
+ /**
+ * All additions and removals are queued for processing. Indexing messages
+ * is potentially phenomenally expensive, and deletion can still be
+ * relatively expensive due to our need to delete the message, its
+ * attributes, and all attributes that reference it. Additionally,
+ * attribute deletion costs are higher than attribute look-up because
+ * there is the actual row plus its 3 indices, and our covering indices are
+ * no help there.
+ *
+ */
+ _msgFolderListener: {
+ indexer: null,
+
+ /**
+ * We no longer use the msgAdded notification, instead opting to wait until
+ * junk/trait classification has run (or decided not to run) and all
+ * filters have run. The msgsClassified notification provides that for us.
+ */
+ msgAdded(aMsgHdr) {
+ // we are never called! we do not enable this bit!
+ },
+
+ /**
+ * Process (apparently newly added) messages that have been looked at by
+ * the message classifier. This ensures that if the message was going
+ * to get marked as spam, this will have already happened.
+ *
+ * Besides truly new (to us) messages, We will also receive this event for
+ * messages that are the result of IMAP message move/copy operations,
+ * including both moves that generated offline fake headers and those that
+ * did not. In the offline fake header case, however, we are able to
+ * ignore their msgsClassified events because we will have received a
+ * msgKeyChanged notification sometime in the recent past.
+ */
+ msgsClassified(aMsgHdrs, aJunkClassified, aTraitClassified) {
+ this.indexer._log.debug("msgsClassified notification");
+ try {
+ GlodaMsgIndexer._reindexChangedMessages(aMsgHdrs, false);
+ } catch (ex) {
+ this.indexer._log.error("Explosion in msgsClassified handling:", ex);
+ }
+ },
+
+ /**
+ * Any messages which have had their junk state changed are marked for
+ * reindexing.
+ */
+ msgsJunkStatusChanged(messages) {
+ this.indexer._log.debug("JunkStatusChanged notification");
+ GlodaMsgIndexer._reindexChangedMessages(messages, true);
+ },
+
+ /**
+ * Handle real, actual deletion (move to trash and IMAP deletion model
+ * don't count); we only see the deletion here when it becomes forever,
+ * or rather _just before_ it becomes forever. Because the header is
+ * going away, we need to either process things immediately or extract the
+ * information required to purge it later without the header.
+ * To this end, we mark all messages that were indexed in the gloda message
+ * database as deleted. We set our pending deletions flag to let our
+ * indexing logic know that after its next wave of folder traversal, it
+ * should perform a deletion pass. If it turns out the messages are coming
+ * back, the fact that deletion is thus deferred can be handy, as we can
+ * reuse the existing gloda message.
+ */
+ msgsDeleted(aMsgHdrs) {
+ this.indexer._log.debug("msgsDeleted notification");
+ let glodaMessageIds = [];
+
+ for (let msgHdr of aMsgHdrs) {
+ let [glodaId, glodaDirty] = PendingCommitTracker.getGlodaState(msgHdr);
+ if (
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ glodaDirty != GlodaMsgIndexer.kMessageFilthy
+ ) {
+ glodaMessageIds.push(glodaId);
+ }
+ }
+
+ if (glodaMessageIds.length) {
+ GlodaMsgIndexer._datastore.markMessagesDeletedByIDs(glodaMessageIds);
+ GlodaMsgIndexer.pendingDeletions = true;
+ }
+ },
+
+ /**
+ * Process a move or copy.
+ *
+ * Moves to a local folder or an IMAP folder where we are generating offline
+ * fake headers are dealt with efficiently because we get both the source
+ * and destination headers. The main ingredient to having offline fake
+ * headers is that allowUndo was true when the operation was performance.
+ * The only non-obvious thing is that we need to make sure that we deal
+ * with the impact of filthy folders and messages on gloda-id's (they
+ * invalidate the gloda-id).
+ *
+ * Moves to an IMAP folder that do not generate offline fake headers do not
+ * provide us with the target header, but the IMAP SetPendingAttributes
+ * logic will still attempt to propagate the properties on the message
+ * header so when we eventually see it in the msgsClassified notification,
+ * it should have the properties of the source message copied over.
+ * We make sure that gloda-id's do not get propagated when messages are
+ * moved from IMAP folders that are marked filthy or are marked as not
+ * supposed to be indexed by clearing the pending attributes for the header
+ * being tracked by the destination IMAP folder.
+ * We could fast-path the IMAP move case in msgsClassified by noticing that
+ * a message is showing up with a gloda-id header already and just
+ * performing an async location update.
+ *
+ * Moves that occur involving 'compacted' folders are fine and do not
+ * require special handling here. The one tricky super-edge-case that
+ * can happen (and gets handled by the compaction pass) is the move of a
+ * message that got gloda indexed that did not already have a gloda-id and
+ * PendingCommitTracker did not get to flush the gloda-id before the
+ * compaction happened. In that case our move logic cannot know to do
+ * anything and the gloda database still thinks the message lives in our
+ * folder. The compaction pass will deal with this by marking the message
+ * as deleted. The rationale being that marking it deleted allows the
+ * message to be re-used if it gets indexed in the target location, or if
+ * the target location has already been indexed, we no longer need the
+ * duplicate and it should be deleted. (Also, it is unable to distinguish
+ * between a case where the message got deleted versus moved.)
+ *
+ * Because copied messages are, by their nature, duplicate messages, we
+ * do not particularly care about them. As such, we defer their processing
+ * to the automatic sync logic that will happen much later on. This is
+ * potentially desirable in case the user deletes some of the original
+ * messages, allowing us to reuse the gloda message representations when
+ * we finally get around to indexing the messages. We do need to mark the
+ * folder as dirty, though, to clue in the sync logic.
+ */
+ msgsMoveCopyCompleted(aMove, aSrcMsgHdrs, aDestFolder, aDestMsgHdrs) {
+ this.indexer._log.debug("MoveCopy notification. Move: " + aMove);
+ try {
+ // ---- Move
+ if (aMove) {
+ // -- Effectively a deletion?
+ // If the destination folder is not indexed, it's like these messages
+ // are being deleted.
+ if (!GlodaMsgIndexer.shouldIndexFolder(aDestFolder)) {
+ this.msgsDeleted(aSrcMsgHdrs);
+ return;
+ }
+
+ // -- Avoid propagation of filthy gloda-id's.
+ // If the source folder is filthy or should not be indexed (and so
+ // any gloda-id's found in there are gibberish), our only job is to
+ // strip the gloda-id's off of all the destination headers because
+ // none of the gloda-id's are valid (and so we certainly don't want
+ // to try and use them as a basis for updating message keys.)
+ let srcMsgFolder = aSrcMsgHdrs[0].folder;
+ if (
+ !this.indexer.shouldIndexFolder(srcMsgFolder) ||
+ GlodaDatastore._mapFolder(srcMsgFolder).dirtyStatus ==
+ GlodaFolder.prototype.kFolderFilthy
+ ) {
+ // Local case, just modify the destination headers directly.
+ if (aDestMsgHdrs.length > 0) {
+ for (let destMsgHdr of aDestMsgHdrs) {
+ // zero it out if it exists
+ // (no need to deal with pending commit issues here; a filthy
+ // folder by definition has nothing indexed in it.)
+ let glodaId = destMsgHdr.getUint32Property(
+ GLODA_MESSAGE_ID_PROPERTY
+ );
+ if (glodaId) {
+ destMsgHdr.setUint32Property(GLODA_MESSAGE_ID_PROPERTY, 0);
+ }
+ }
+
+ // Since we are moving messages from a folder where they were
+ // effectively not indexed, it is up to us to make sure the
+ // messages now get indexed.
+ this.indexer._reindexChangedMessages(aDestMsgHdrs);
+ return;
+ }
+
+ // IMAP move case, we need to operate on the pending headers using
+ // the source header to get the pending header and as the
+ // indication of what has been already set on the pending header.
+ let destDb;
+ // so, this can fail, and there's not much we can do about it.
+ try {
+ destDb = aDestFolder.msgDatabase;
+ } catch (ex) {
+ this.indexer._log.warn(
+ "Destination database for " +
+ aDestFolder.prettyName +
+ " not ready on IMAP move." +
+ " Gloda corruption possible."
+ );
+ return;
+ }
+ for (let srcMsgHdr of aSrcMsgHdrs) {
+ // zero it out if it exists
+ // (no need to deal with pending commit issues here; a filthy
+ // folder by definition has nothing indexed in it.)
+ let glodaId = srcMsgHdr.getUint32Property(
+ GLODA_MESSAGE_ID_PROPERTY
+ );
+ if (glodaId) {
+ destDb.setUint32AttributeOnPendingHdr(
+ srcMsgHdr,
+ GLODA_MESSAGE_ID_PROPERTY,
+ 0
+ );
+ }
+ }
+
+ // Nothing remains to be done. The msgClassified event will take
+ // care of making sure the message gets indexed.
+ return;
+ }
+
+ // --- Have destination headers (local case):
+ if (aDestMsgHdrs.length > 0) {
+ // -- Update message keys for valid gloda-id's.
+ // (Which means ignore filthy gloda-id's.)
+ let glodaIds = [];
+ let newMessageKeys = [];
+ // Track whether we see any messages that are not gloda indexed so
+ // we know if we have to mark the destination folder dirty.
+ let sawNonGlodaMessage = false;
+ for (let iMsg = 0; iMsg < aSrcMsgHdrs.length; iMsg++) {
+ let srcMsgHdr = aSrcMsgHdrs[iMsg];
+ let destMsgHdr = aDestMsgHdrs[iMsg];
+
+ let [glodaId, dirtyStatus] =
+ PendingCommitTracker.getGlodaState(srcMsgHdr);
+ if (
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ dirtyStatus != GlodaMsgIndexer.kMessageFilthy
+ ) {
+ // we may need to update the pending commit map (it checks)
+ PendingCommitTracker.noteMove(srcMsgHdr, destMsgHdr);
+ // but we always need to update our database
+ glodaIds.push(glodaId);
+ newMessageKeys.push(destMsgHdr.messageKey);
+ } else {
+ sawNonGlodaMessage = true;
+ }
+ }
+
+ // this method takes care to update the in-memory representations
+ // too; we don't need to do anything
+ if (glodaIds.length) {
+ GlodaDatastore.updateMessageLocations(
+ glodaIds,
+ newMessageKeys,
+ aDestFolder
+ );
+ }
+
+ // Mark the destination folder dirty if we saw any messages that
+ // were not already gloda indexed.
+ if (sawNonGlodaMessage) {
+ let destGlodaFolder = GlodaDatastore._mapFolder(aDestFolder);
+ destGlodaFolder._ensureFolderDirty();
+ this.indexer.indexingSweepNeeded = true;
+ }
+ } else {
+ // --- No dest headers (IMAP case):
+ // Update any valid gloda indexed messages into their new folder to
+ // make the indexer's life easier when it sees the messages in their
+ // new folder.
+ let glodaIds = [];
+
+ let srcFolderIsLocal =
+ srcMsgFolder instanceof Ci.nsIMsgLocalMailFolder;
+ for (let msgHdr of aSrcMsgHdrs) {
+ let [glodaId, dirtyStatus] =
+ PendingCommitTracker.getGlodaState(msgHdr);
+ if (
+ glodaId >= GLODA_FIRST_VALID_MESSAGE_ID &&
+ dirtyStatus != GlodaMsgIndexer.kMessageFilthy
+ ) {
+ // we may need to update the pending commit map (it checks)
+ PendingCommitTracker.noteBlindMove(msgHdr);
+ // but we always need to update our database
+ glodaIds.push(glodaId);
+
+ // XXX UNDO WORKAROUND
+ // This constitutes a move from a local folder to an IMAP
+ // folder. Undo does not currently do the right thing for us,
+ // but we have a chance of not orphaning the message if we
+ // mark the source header as dirty so that when the message
+ // gets re-added we see it. (This does require that we enter
+ // the folder; we set the folder dirty after the loop to
+ // increase the probability of this but it's not foolproof
+ // depending on when the next indexing sweep happens and when
+ // the user performs an undo.)
+ msgHdr.setUint32Property(
+ GLODA_DIRTY_PROPERTY,
+ GlodaMsgIndexer.kMessageDirty
+ );
+ }
+ }
+ // XXX ALSO UNDO WORKAROUND
+ if (srcFolderIsLocal) {
+ let srcGlodaFolder = GlodaDatastore._mapFolder(srcMsgFolder);
+ srcGlodaFolder._ensureFolderDirty();
+ }
+
+ // quickly move them to the right folder, zeroing their message keys
+ GlodaDatastore.updateMessageFoldersByKeyPurging(
+ glodaIds,
+ aDestFolder
+ );
+ // we _do not_ need to mark the folder as dirty, because the
+ // message added events will cause that to happen.
+ }
+ } else {
+ // ---- Copy case
+ // -- Do not propagate gloda-id's for copies
+ // (Only applies if we have the destination header, which means local)
+ for (let destMsgHdr of aDestMsgHdrs) {
+ let glodaId = destMsgHdr.getUint32Property(
+ GLODA_MESSAGE_ID_PROPERTY
+ );
+ if (glodaId) {
+ destMsgHdr.setUint32Property(GLODA_MESSAGE_ID_PROPERTY, 0);
+ }
+ }
+
+ // mark the folder as dirty; we'll get to it later.
+ let destGlodaFolder = GlodaDatastore._mapFolder(aDestFolder);
+ destGlodaFolder._ensureFolderDirty();
+ this.indexer.indexingSweepNeeded = true;
+ }
+ } catch (ex) {
+ this.indexer._log.error(
+ "Problem encountered during message move/copy:",
+ ex.stack
+ );
+ }
+ },
+
+ /**
+ * Queue up message key changes that are a result of offline fake headers
+ * being made real for the actual update during the msgsClassified
+ * notification that is expected after this. We defer the
+ * actual work (if there is any to be done; the fake header might have
+ * guessed the right UID correctly) so that we can batch our work.
+ *
+ * The expectation is that there will be no meaningful time window between
+ * this notification and the msgsClassified notification since the message
+ * classifier should not actually need to classify the messages (they
+ * should already have been classified) and so can fast-path them.
+ */
+ msgKeyChanged(aOldMsgKey, aNewMsgHdr) {
+ try {
+ let val = null,
+ newKey = aNewMsgHdr.messageKey;
+ let [glodaId, glodaDirty] =
+ PendingCommitTracker.getGlodaState(aNewMsgHdr);
+ // If we haven't indexed this message yet, take no action, and leave it
+ // up to msgsClassified to take proper action.
+ if (glodaId < GLODA_FIRST_VALID_MESSAGE_ID) {
+ return;
+ }
+ // take no action on filthy messages,
+ // generate an entry if dirty or the keys don't match.
+ if (
+ glodaDirty !== GlodaMsgIndexer.kMessageFilthy &&
+ (glodaDirty === GlodaMsgIndexer.kMessageDirty ||
+ aOldMsgKey !== newKey)
+ ) {
+ val = {
+ id: glodaId,
+ key: aOldMsgKey !== newKey ? newKey : null,
+ isDirty: glodaDirty === GlodaMsgIndexer.kMessageDirty,
+ };
+ }
+
+ let key = aNewMsgHdr.folder.URI + "#" + aNewMsgHdr.messageKey;
+ this.indexer._keyChangedBatchInfo[key] = val;
+ } catch (ex) {
+ // this is more for the unit test to fail rather than user error reporting
+ this.indexer._log.error(
+ "Problem encountered during msgKeyChanged" +
+ " notification handling: " +
+ ex +
+ "\n\n" +
+ ex.stack +
+ " \n\n"
+ );
+ }
+ },
+
+ /**
+ * Detect newly added folders before they get messages so we map them before
+ * they get any messages added to them. If we only hear about them after
+ * they get their 1st message, then we will mark them filthy, but if we mark
+ * them before that, they get marked clean.
+ */
+ folderAdded(aMsgFolder) {
+ // This is invoked for its side-effect of invoking _mapFolder and doing so
+ // only after filtering out folders we don't care about.
+ GlodaMsgIndexer.shouldIndexFolder(aMsgFolder);
+ },
+
+ /**
+ * Handles folder no-longer-exists-ence. We mark all messages as deleted
+ * and remove the folder from our URI table. Currently, if a folder that
+ * contains other folders is deleted, we may either receive one
+ * notification for the folder that is deleted, or a notification for the
+ * folder and one for each of its descendents. This depends upon the
+ * underlying account implementation, so we explicitly handle each case.
+ * Namely, we treat it as if we're only planning on getting one, but we
+ * handle if the children are already gone for some reason.
+ */
+ folderDeleted(aFolder) {
+ this.indexer._log.debug("folderDeleted notification");
+ try {
+ let delFunc = function (aFolder, indexer) {
+ if (indexer._datastore._folderKnown(aFolder)) {
+ indexer._log.info(
+ "Processing deletion of folder " + aFolder.prettyName + "."
+ );
+ let glodaFolder = GlodaDatastore._mapFolder(aFolder);
+ indexer._datastore.markMessagesDeletedByFolderID(glodaFolder.id);
+ indexer._datastore.deleteFolderByID(glodaFolder.id);
+ GlodaDatastore._killGlodaFolderIntoTombstone(glodaFolder);
+ } else {
+ indexer._log.info(
+ "Ignoring deletion of folder " +
+ aFolder.prettyName +
+ " because it is unknown to gloda."
+ );
+ }
+ };
+
+ let descendentFolders = aFolder.descendants;
+ // (the order of operations does not matter; child, non-child, whatever.)
+ // delete the parent
+ delFunc(aFolder, this.indexer);
+ // delete all its descendents
+ for (let folder of descendentFolders) {
+ delFunc(folder, this.indexer);
+ }
+
+ this.indexer.pendingDeletions = true;
+ } catch (ex) {
+ this.indexer._log.error(
+ "Problem encountered during folder deletion" +
+ ": " +
+ ex +
+ "\n\n" +
+ ex.stack +
+ "\n\n"
+ );
+ }
+ },
+
+ /**
+ * Handle a folder being copied or moved.
+ * Moves are handled by a helper function shared with _folderRenameHelper
+ * (which takes care of any nesting involved).
+ * Copies are actually ignored, because our periodic indexing traversal
+ * should discover these automatically. We could hint ourselves into
+ * action, but arguably a set of completely duplicate messages is not
+ * a high priority for indexing.
+ */
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ this.indexer._log.debug(
+ "folderMoveCopy notification (Move: " + aMove + ")"
+ );
+ if (aMove) {
+ let srcURI = aSrcFolder.URI;
+ let targetURI =
+ aDestFolder.URI + srcURI.substring(srcURI.lastIndexOf("/"));
+ this._folderRenameHelper(aSrcFolder, targetURI);
+ } else {
+ this.indexer.indexingSweepNeeded = true;
+ }
+ },
+
+ /**
+ * We just need to update the URI <-> ID maps and the row in the database,
+ * all of which is actually done by the datastore for us.
+ * This method needs to deal with the complexity where local folders will
+ * generate a rename notification for each sub-folder, but IMAP folders
+ * will generate only a single notification. Our logic primarily handles
+ * this by not exploding if the original folder no longer exists.
+ */
+ _folderRenameHelper(aOrigFolder, aNewURI) {
+ let newFolder = lazy.MailUtils.getOrCreateFolder(aNewURI);
+ let specialFolderFlags =
+ Ci.nsMsgFolderFlags.Trash | Ci.nsMsgFolderFlags.Junk;
+ if (newFolder.isSpecialFolder(specialFolderFlags, true)) {
+ let descendentFolders = newFolder.descendants;
+
+ // First thing to do: make sure we don't index the resulting folder and
+ // its descendents.
+ GlodaMsgIndexer.resetFolderIndexingPriority(newFolder);
+ for (let folder of descendentFolders) {
+ GlodaMsgIndexer.resetFolderIndexingPriority(folder);
+ }
+
+ // Remove from the index messages from the original folder
+ this.folderDeleted(aOrigFolder);
+ } else {
+ let descendentFolders = aOrigFolder.descendants;
+
+ let origURI = aOrigFolder.URI;
+ // this rename is straightforward.
+ GlodaDatastore.renameFolder(aOrigFolder, aNewURI);
+
+ for (let folder of descendentFolders) {
+ let oldSubURI = folder.URI;
+ // mangle a new URI from the old URI. we could also try and do a
+ // parallel traversal of the new folder hierarchy, but that seems like
+ // more work.
+ let newSubURI = aNewURI + oldSubURI.substring(origURI.length);
+ this.indexer._datastore.renameFolder(oldSubURI, newSubURI);
+ }
+
+ this.indexer._log.debug(
+ "folder renamed: " + origURI + " to " + aNewURI
+ );
+ }
+ },
+
+ /**
+ * Handle folder renames, dispatching to our rename helper (which also
+ * takes care of any nested folder issues.)
+ */
+ folderRenamed(aOrigFolder, aNewFolder) {
+ this._folderRenameHelper(aOrigFolder, aNewFolder.URI);
+ },
+
+ /**
+ * Helper used by folderCompactStart/folderReindexTriggered.
+ */
+ _reindexFolderHelper(folder, isCompacting) {
+ // ignore folders we ignore...
+ if (!GlodaMsgIndexer.shouldIndexFolder(folder)) {
+ return;
+ }
+
+ let glodaFolder = GlodaDatastore._mapFolder(folder);
+ if (isCompacting) {
+ glodaFolder.compacting = true;
+ }
+
+ // Purge any explicit indexing of said folder.
+ GlodaIndexer.purgeJobsUsingFilter(function (aJob) {
+ return aJob.jobType == "folder" && aJob.id == folder.id;
+ });
+
+ // Abort the active job if it's in the folder (this covers both
+ // event-driven indexing that happens to be in the folder as well
+ // explicit folder indexing of the folder).
+ if (GlodaMsgIndexer._indexingFolder == folder) {
+ GlodaIndexer.killActiveJob();
+ }
+
+ // Tell the PendingCommitTracker to throw away anything it is tracking
+ // about the folder. We will pick up the pieces in the compaction
+ // pass.
+ PendingCommitTracker.noteFolderDatabaseGettingBlownAway(folder);
+
+ // (We do not need to mark the folder dirty because if we were indexing
+ // it, it already must have been marked dirty.)
+ },
+
+ /**
+ * folderCompactStart: Mark the folder as compacting in our in-memory
+ * representation. This should keep any new indexing out of the folder
+ * until it is done compacting. Also, kill any active or existing jobs
+ * to index the folder.
+ */
+ folderCompactStart(folder) {
+ this._reindexFolderHelper(folder, true);
+ },
+
+ /**
+ * folderReindexTriggered: We do the same thing as folderCompactStart
+ * but don't mark the folder as compacting.
+ */
+ folderReindexTriggered(folder) {
+ this._reindexFolderHelper(folder, false);
+ },
+
+ /**
+ * folderCompactFinish: Mark the folder as done compacting in our
+ * in-memory representation. Assuming the folder was known to us and
+ * not marked filthy, queue a compaction job.
+ */
+ folderCompactFinish(folder) {
+ // ignore folders we ignore...
+ if (!GlodaMsgIndexer.shouldIndexFolder(folder)) {
+ return;
+ }
+
+ let glodaFolder = GlodaDatastore._mapFolder(folder);
+ glodaFolder.compacting = false;
+ glodaFolder._setCompactedState(true);
+
+ // Queue compaction unless the folder was filthy (in which case there
+ // are no valid gloda-id's to update.)
+ if (glodaFolder.dirtyStatus != glodaFolder.kFolderFilthy) {
+ GlodaIndexer.indexJob(new IndexingJob("folderCompact", glodaFolder.id));
+ }
+
+ // Queue indexing of the folder if it is dirty. We are doing this
+ // mainly in case we were indexing it before the compaction started.
+ // It should be reasonably harmless if we weren't.
+ // (It would probably be better to just make sure that there is an
+ // indexing sweep queued or active, and if it's already active that
+ // this folder is in the queue to be processed.)
+ if (glodaFolder.dirtyStatus == glodaFolder.kFolderDirty) {
+ GlodaIndexer.indexJob(new IndexingJob("folder", glodaFolder.id));
+ }
+ },
+ },
+
+ /**
+ * A nsIFolderListener (listening on nsIMsgMailSession so we get all of
+ * these events) PRIMARILY to get folder loaded notifications. Because of
+ * deficiencies in the nsIMsgFolderListener's events at this time, we also
+ * get our folder-added and newsgroup notifications from here for now. (This
+ * will be rectified.)
+ */
+ _folderListener: {
+ indexer: null,
+
+ _init(aIndexer) {
+ this.indexer = aIndexer;
+ },
+
+ onFolderAdded(parentFolder, child) {},
+ onMessageAdded(parentFolder, msg) {},
+ onFolderRemoved(parentFolder, child) {},
+ onMessageRemoved(parentFolder, msg) {},
+ onFolderPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {},
+ /**
+ * Detect changes to folder flags and reset our indexing priority. This
+ * is important because (all?) folders start out without any flags and
+ * then get their flags added to them.
+ */
+ onFolderIntPropertyChanged(aFolderItem, aProperty, aOldValue, aNewValue) {
+ if (aProperty !== "FolderFlag") {
+ return;
+ }
+ if (!GlodaMsgIndexer.shouldIndexFolder(aFolderItem)) {
+ return;
+ }
+ // Only reset priority if folder Special Use changes.
+ if (
+ (aOldValue & Ci.nsMsgFolderFlags.SpecialUse) ==
+ (aNewValue & Ci.nsMsgFolderFlags.SpecialUse)
+ ) {
+ return;
+ }
+ GlodaMsgIndexer.resetFolderIndexingPriority(aFolderItem);
+ },
+ onFolderBoolPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {},
+ onFolderUnicharPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {},
+ /**
+ * Notice when user activity adds/removes tags or changes a message's
+ * status.
+ */
+ onFolderPropertyFlagChanged(aMsgHdr, aProperty, aOldValue, aNewValue) {
+ if (
+ aProperty == "Keywords" ||
+ // We could care less about the new flag changing.
+ (aProperty == "Status" &&
+ (aOldValue ^ aNewValue) != Ci.nsMsgMessageFlags.New &&
+ // We do care about IMAP deletion, but msgsDeleted tells us that, so
+ // ignore IMAPDeleted too...
+ (aOldValue ^ aNewValue) != Ci.nsMsgMessageFlags.IMAPDeleted) ||
+ aProperty == "Flagged"
+ ) {
+ GlodaMsgIndexer._reindexChangedMessages([aMsgHdr], true);
+ }
+ },
+
+ /**
+ * Get folder loaded notifications for folders that had to do some
+ * (asynchronous) processing before they could be opened.
+ */
+ onFolderEvent(aFolder, aEvent) {
+ if (aEvent == "FolderLoaded") {
+ this.indexer._onFolderLoaded(aFolder);
+ }
+ },
+ },
+
+ /* ***** Rebuilding / Reindexing ***** */
+ /**
+ * Allow us to invalidate an outstanding folder traversal because the
+ * underlying database is going away. We use other means for detecting
+ * modifications of the message (labeling, marked (un)read, starred, etc.)
+ *
+ * This is an nsIDBChangeListener listening to an nsIDBChangeAnnouncer. To
+ * add ourselves, we get us a nice nsMsgDatabase, query it to the announcer,
+ * then call addListener.
+ */
+ _databaseAnnouncerListener: {
+ indexer: null,
+ /**
+ * XXX We really should define the operations under which we expect this to
+ * occur. While we know this must be happening as the result of a
+ * ForceClosed call, we don't have a comprehensive list of when this is
+ * expected to occur. Some reasons:
+ * - Compaction (although we should already have killed the job thanks to
+ * our compaction notification)
+ * - UID validity rolls.
+ * - Folder Rename
+ * - Folder Delete
+ * The fact that we already have the database open when getting this means
+ * that it had to be valid before we opened it, which hopefully rules out
+ * modification of the mbox file by an external process (since that is
+ * forbidden when we are running) and many other exotic things.
+ *
+ * So this really ends up just being a correctness / safety protection
+ * mechanism. At least now that we have better compaction support.
+ */
+ onAnnouncerGoingAway(aDBChangeAnnouncer) {
+ // The fact that we are getting called means we have an active folder and
+ // that we therefore are the active job. As such, we must kill the
+ // active job.
+ // XXX In the future, when we support interleaved event-driven indexing
+ // that bumps long-running indexing tasks, the semantics of this will
+ // have to change a bit since we will want to maintain being active in a
+ // folder even when bumped. However, we will probably have a more
+ // complex notion of indexing contexts on a per-job basis.
+ GlodaIndexer.killActiveJob();
+ },
+
+ onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) {},
+ onHdrDeleted(aHdrChanged, aParentKey, aFlags, aInstigator) {},
+ onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) {},
+ onParentChanged(aKeyChanged, aOldParent, aNewParent, aInstigator) {},
+ onReadChanged(aInstigator) {},
+ onJunkScoreChanged(aInstigator) {},
+ onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) {},
+ onEvent(aDB, aEvent) {},
+ },
+
+ /**
+ * Given a list of Message-ID's, return a matching list of lists of messages
+ * matching those Message-ID's. So if you pass an array with three
+ * Message-ID's ["a", "b", "c"], you would get back an array containing
+ * 3 lists, where the first list contains all the messages with a message-id
+ * of "a", and so forth. The reason a list is returned rather than null/a
+ * message is that we accept the reality that we have multiple copies of
+ * messages with the same ID.
+ * This call is asynchronous because it depends on previously created messages
+ * to be reflected in our results, which requires us to execute on the async
+ * thread where all our writes happen. This also turns out to be a
+ * reasonable thing because we could imagine pathological cases where there
+ * could be a lot of message-id's and/or a lot of messages with those
+ * message-id's.
+ *
+ * The returned collection will include both 'ghost' messages (messages
+ * that exist for conversation-threading purposes only) as well as deleted
+ * messages in addition to the normal 'live' messages that non-privileged
+ * queries might return.
+ */
+ getMessagesByMessageID(aMessageIDs, aCallback, aCallbackThis) {
+ let msgIDToIndex = {};
+ let results = [];
+ for (let iID = 0; iID < aMessageIDs.length; ++iID) {
+ let msgID = aMessageIDs[iID];
+ results.push([]);
+ msgIDToIndex[msgID] = iID;
+ }
+
+ // (Note: although we are performing a lookup with no validity constraints
+ // and using the same object-relational-mapper-ish layer used by things
+ // that do have constraints, we are not at risk of exposing deleted
+ // messages to other code and getting it confused. The only way code
+ // can find a message is if it shows up in their queries or gets announced
+ // via GlodaCollectionManager.itemsAdded, neither of which will happen.)
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ query.headerMessageID.apply(query, aMessageIDs);
+ query.frozen = true;
+
+ let listener = new MessagesByMessageIdCallback(
+ msgIDToIndex,
+ results,
+ aCallback,
+ aCallbackThis
+ );
+ return query.getCollection(listener, null, { becomeNull: true });
+ },
+
+ /**
+ * A reference to MsgHdrToMimeMessage that unit testing can clobber when it
+ * wants to cause us to hang or inject a fault. If you are not
+ * glodaTestHelper.js then _do not touch this_.
+ */
+ _MsgHdrToMimeMessageFunc: MsgHdrToMimeMessage,
+ /**
+ * Primary message indexing logic. This method is mainly concerned with
+ * getting all the information about the message required for threading /
+ * conversation building and subsequent processing. It is responsible for
+ * determining whether to reuse existing gloda messages or whether a new one
+ * should be created. Most attribute stuff happens in fund_attr.js or
+ * expl_attr.js.
+ *
+ * Prior to calling this method, the caller must have invoked
+ * |_indexerEnterFolder|, leaving us with the following true invariants
+ * below.
+ *
+ * @pre aMsgHdr.folder == this._indexingFolder
+ * @pre aMsgHdr.folder.msgDatabase == this._indexingDatabase
+ */
+ *_indexMessage(aMsgHdr, aCallbackHandle) {
+ this._log.debug(
+ "*** Indexing message: " + aMsgHdr.messageKey + " : " + aMsgHdr.subject
+ );
+
+ // If the message is offline, then get the message body as well
+ let aMimeMsg;
+ if (
+ aMsgHdr.flags & Ci.nsMsgMessageFlags.Offline ||
+ aMsgHdr.folder instanceof Ci.nsIMsgLocalMailFolder
+ ) {
+ this._MsgHdrToMimeMessageFunc(
+ aMsgHdr,
+ aCallbackHandle.callbackThis,
+ aCallbackHandle.callback,
+ false,
+ {
+ saneBodySize: true,
+ }
+ );
+ aMimeMsg = (yield GlodaConstants.kWorkAsync)[1];
+ } else {
+ this._log.debug(" * Message is not offline -- only headers indexed");
+ }
+
+ this._log.debug(" * Got message, subject " + aMsgHdr.subject);
+
+ if (this._unitTestSuperVerbose) {
+ if (aMimeMsg) {
+ this._log.debug(" * Got Mime " + aMimeMsg.prettyString());
+ } else {
+ this._log.debug(" * NO MIME MESSAGE!!!\n");
+ }
+ }
+
+ // -- Find/create the conversation the message belongs to.
+ // Our invariant is that all messages that exist in the database belong to
+ // a conversation.
+
+ // - See if any of the ancestors exist and have a conversationID...
+ // (references are ordered from old [0] to new [n-1])
+ let references = Array.from(range(0, aMsgHdr.numReferences)).map(i =>
+ aMsgHdr.getStringReference(i)
+ );
+ // also see if we already know about the message...
+ references.push(aMsgHdr.messageId);
+
+ this.getMessagesByMessageID(
+ references,
+ aCallbackHandle.callback,
+ aCallbackHandle.callbackThis
+ );
+ // (ancestorLists has a direct correspondence to the message ids)
+ let ancestorLists = yield GlodaConstants.kWorkAsync;
+
+ this._log.debug("ancestors raw: " + ancestorLists);
+ this._log.debug(
+ "ref len: " + references.length + " anc len: " + ancestorLists.length
+ );
+ this._log.debug("references: " + references);
+ this._log.debug("ancestors: " + ancestorLists);
+
+ // pull our current message lookup results off
+ references.pop();
+ let candidateCurMsgs = ancestorLists.pop();
+
+ let conversationID = null;
+ let conversation = null;
+ // -- figure out the conversation ID
+ // if we have a clone/already exist, just use his conversation ID
+ if (candidateCurMsgs.length > 0) {
+ conversationID = candidateCurMsgs[0].conversationID;
+ conversation = candidateCurMsgs[0].conversation;
+ } else {
+ // otherwise check out our ancestors
+ // (walk from closest to furthest ancestor)
+ for (
+ let iAncestor = ancestorLists.length - 1;
+ iAncestor >= 0;
+ --iAncestor
+ ) {
+ let ancestorList = ancestorLists[iAncestor];
+
+ if (ancestorList.length > 0) {
+ // we only care about the first instance of the message because we are
+ // able to guarantee the invariant that all messages with the same
+ // message id belong to the same conversation.
+ let ancestor = ancestorList[0];
+ if (conversationID === null) {
+ conversationID = ancestor.conversationID;
+ conversation = ancestor.conversation;
+ } else if (conversationID != ancestor.conversationID) {
+ // XXX this inconsistency is known and understood and tracked by
+ // bug 478162 https://bugzilla.mozilla.org/show_bug.cgi?id=478162
+ // this._log.error("Inconsistency in conversations invariant on " +
+ // ancestor.headerMessageID + ". It has conv id " +
+ // ancestor.conversationID + " but expected " +
+ // conversationID + ". ID: " + ancestor.id);
+ }
+ }
+ }
+ }
+
+ // nobody had one? create a new conversation
+ if (conversationID === null) {
+ // (the create method could issue the id, making the call return
+ // without waiting for the database...)
+ conversation = this._datastore.createConversation(
+ aMsgHdr.mime2DecodedSubject,
+ null,
+ null
+ );
+ conversationID = conversation.id;
+ }
+
+ // Walk from furthest to closest ancestor, creating the ancestors that don't
+ // exist. (This is possible if previous messages that were consumed in this
+ // thread only had an in-reply-to or for some reason did not otherwise
+ // provide the full references chain.)
+ for (let iAncestor = 0; iAncestor < ancestorLists.length; ++iAncestor) {
+ let ancestorList = ancestorLists[iAncestor];
+
+ if (ancestorList.length == 0) {
+ this._log.debug(
+ "creating message with: null, " +
+ conversationID +
+ ", " +
+ references[iAncestor] +
+ ", null."
+ );
+ let ancestor = this._datastore.createMessage(
+ null,
+ null, // ghost
+ conversationID,
+ null,
+ references[iAncestor],
+ null, // no subject
+ null, // no body
+ null
+ ); // no attachments
+ this._datastore.insertMessage(ancestor);
+ ancestorLists[iAncestor].push(ancestor);
+ }
+ }
+ // now all our ancestors exist, though they may be ghost-like...
+
+ // find if there's a ghost version of our message or we already have indexed
+ // this message.
+ let curMsg = null;
+ this._log.debug(candidateCurMsgs.length + " candidate messages");
+ for (let iCurCand = 0; iCurCand < candidateCurMsgs.length; iCurCand++) {
+ let candMsg = candidateCurMsgs[iCurCand];
+
+ this._log.debug(
+ "candidate folderID: " +
+ candMsg.folderID +
+ " messageKey: " +
+ candMsg.messageKey
+ );
+
+ if (candMsg.folderURI == this._indexingFolder.URI) {
+ // if we are in the same folder and we have the same message key, we
+ // are definitely the same, stop looking.
+ if (candMsg.messageKey == aMsgHdr.messageKey) {
+ curMsg = candMsg;
+ break;
+ }
+ // if (we are in the same folder and) the candidate message has a null
+ // message key, we treat it as our best option unless we find an exact
+ // key match. (this would happen because the 'move' notification case
+ // has to deal with not knowing the target message key. this case
+ // will hopefully be somewhat improved in the future to not go through
+ // this path which mandates re-indexing of the message in its entirety)
+ if (candMsg.messageKey === null) {
+ curMsg = candMsg;
+ } else if (
+ curMsg === null &&
+ !this._indexingDatabase.containsKey(candMsg.messageKey)
+ ) {
+ // (We are in the same folder and) the candidate message's underlying
+ // message no longer exists/matches. Assume we are the same but
+ // were betrayed by a re-indexing or something, but we have to make
+ // sure a perfect match doesn't turn up.
+ curMsg = candMsg;
+ }
+ } else if (curMsg === null && candMsg.folderID === null) {
+ // a ghost/deleted message is fine
+ curMsg = candMsg;
+ }
+ }
+
+ let attachmentNames = aMimeMsg?.allAttachments.map(att => att.name) || null;
+
+ let isConceptuallyNew, isRecordNew, insertFulltext;
+ if (curMsg === null) {
+ curMsg = this._datastore.createMessage(
+ aMsgHdr.folder,
+ aMsgHdr.messageKey,
+ conversationID,
+ aMsgHdr.date,
+ aMsgHdr.messageId
+ );
+ curMsg._conversation = conversation;
+ isConceptuallyNew = isRecordNew = insertFulltext = true;
+ } else {
+ isRecordNew = false;
+ // the message is conceptually new if it was a ghost or dead.
+ isConceptuallyNew = curMsg._isGhost || curMsg._isDeleted;
+ // insert fulltext if it was a ghost
+ insertFulltext = curMsg._isGhost;
+ curMsg._folderID = this._datastore._mapFolder(aMsgHdr.folder).id;
+ curMsg._messageKey = aMsgHdr.messageKey;
+ curMsg.date = new Date(aMsgHdr.date / 1000);
+ // the message may have been deleted; tell it to make sure it's not.
+ curMsg._ensureNotDeleted();
+ // note: we are assuming that our matching logic is flawless in that
+ // if this message was not a ghost, we are assuming the 'body'
+ // associated with the id is still exactly the same. It is conceivable
+ // that there are cases where this is not true.
+ }
+
+ if (aMimeMsg) {
+ let bodyPlain = aMimeMsg.coerceBodyToPlaintext(aMsgHdr.folder);
+ if (bodyPlain) {
+ curMsg._bodyLines = bodyPlain.split(/\r?\n/);
+ // curMsg._content gets set by GlodaFundAttr.jsm
+ }
+ }
+
+ // Mark the message as new (for the purposes of fulltext insertion)
+ if (insertFulltext) {
+ curMsg._isNew = true;
+ }
+
+ curMsg._subject = aMsgHdr.mime2DecodedSubject;
+ curMsg._attachmentNames = attachmentNames;
+
+ // curMsg._indexAuthor gets set by GlodaFundAttr.jsm
+ // curMsg._indexRecipients gets set by GlodaFundAttr.jsm
+
+ // zero the notability so everything in grokNounItem can just increment
+ curMsg.notability = 0;
+
+ yield aCallbackHandle.pushAndGo(
+ Gloda.grokNounItem(
+ curMsg,
+ { header: aMsgHdr, mime: aMimeMsg, bodyLines: curMsg._bodyLines },
+ isConceptuallyNew,
+ isRecordNew,
+ aCallbackHandle
+ )
+ );
+
+ delete curMsg._bodyLines;
+ delete curMsg._content;
+ delete curMsg._isNew;
+ delete curMsg._indexAuthor;
+ delete curMsg._indexRecipients;
+
+ // we want to update the header for messages only after the transaction
+ // irrevocably hits the disk. otherwise we could get confused if the
+ // transaction rolls back or what not.
+ PendingCommitTracker.track(aMsgHdr, curMsg.id);
+
+ yield GlodaConstants.kWorkDone;
+ },
+
+ /**
+ * Wipe a message out of existence from our index. This is slightly more
+ * tricky than one would first expect because there are potentially
+ * attributes not immediately associated with this message that reference
+ * the message. Not only that, but deletion of messages may leave a
+ * conversation possessing only ghost messages, which we don't want, so we
+ * need to nuke the moot conversation and its moot ghost messages.
+ * For now, we are actually punting on that trickiness, and the exact
+ * nuances aren't defined yet because we have not decided whether to store
+ * such attributes redundantly. For example, if we have subject-pred-object,
+ * we could actually store this as attributes (subject, id, object) and
+ * (object, id, subject). In such a case, we could query on (subject, *)
+ * and use the results to delete the (object, id, subject) case. If we
+ * don't redundantly store attributes, we can deal with the problem by
+ * collecting up all the attributes that accept a message as their object
+ * type and issuing a delete against that. For example, delete (*, [1,2,3],
+ * message id).
+ * (We are punting because we haven't implemented support for generating
+ * attributes like that yet.)
+ *
+ * @TODO: implement deletion of attributes that reference (deleted) messages
+ */
+ *_deleteMessage(aMessage, aCallbackHandle) {
+ this._log.debug("*** Deleting message: " + aMessage);
+
+ // -- delete our attributes
+ // delete the message's attributes (if we implement the cascade delete, that
+ // could do the honors for us... right now we define the trigger in our
+ // schema but the back-end ignores it)
+ GlodaDatastore.clearMessageAttributes(aMessage);
+
+ // -- delete our message or ghost us, and maybe nuke the whole conversation
+ // Look at the other messages in the conversation.
+ // (Note: although we are performing a lookup with no validity constraints
+ // and using the same object-relational-mapper-ish layer used by things
+ // that do have constraints, we are not at risk of exposing deleted
+ // messages to other code and getting it confused. The only way code
+ // can find a message is if it shows up in their queries or gets announced
+ // via GlodaCollectionManager.itemsAdded, neither of which will happen.)
+ let convPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ convPrivQuery.conversation(aMessage.conversation);
+ let conversationCollection = convPrivQuery.getCollection(aCallbackHandle);
+ yield GlodaConstants.kWorkAsync;
+
+ let conversationMsgs = conversationCollection.items;
+
+ // Count the number of ghosts messages we see to determine if we are
+ // the last message alive.
+ let ghostCount = 0;
+ let twinMessageExists = false;
+ for (let convMsg of conversationMsgs) {
+ // ignore our own message
+ if (convMsg.id == aMessage.id) {
+ continue;
+ }
+
+ if (convMsg._isGhost) {
+ ghostCount++;
+ } else if (
+ // This message is our (living) twin if it is not a ghost, not deleted,
+ // and has the same message-id header.
+ !convMsg._isDeleted &&
+ convMsg.headerMessageID == aMessage.headerMessageID
+ ) {
+ twinMessageExists = true;
+ }
+ }
+
+ // -- If everyone else is a ghost, blow away the conversation.
+ // If there are messages still alive or deleted but we have not yet gotten
+ // to them yet _deleteMessage, then do not do this. (We will eventually
+ // hit this case if they are all deleted.)
+ if (conversationMsgs.length - 1 == ghostCount) {
+ // - Obliterate each message
+ for (let msg of conversationMsgs) {
+ GlodaDatastore.deleteMessageByID(msg.id);
+ }
+ // - Obliterate the conversation
+ GlodaDatastore.deleteConversationByID(aMessage.conversationID);
+ // *no one* should hold a reference or use aMessage after this point,
+ // trash it so such ne'er do'wells are made plain.
+ aMessage._objectPurgedMakeYourselfUnpleasant();
+ } else if (twinMessageExists) {
+ // -- Ghost or purge us as appropriate
+ // Purge us if we have a (living) twin; no ghost required.
+ GlodaDatastore.deleteMessageByID(aMessage.id);
+ // *no one* should hold a reference or use aMessage after this point,
+ // trash it so such ne'er do'wells are made plain.
+ aMessage._objectPurgedMakeYourselfUnpleasant();
+ } else {
+ // No twin, a ghost is required, we become the ghost.
+ aMessage._ghost();
+ GlodaDatastore.updateMessage(aMessage);
+ // ghosts don't have fulltext. purge it.
+ GlodaDatastore.deleteMessageTextByID(aMessage.id);
+ }
+
+ yield GlodaConstants.kWorkDone;
+ },
+};
+GlodaIndexer.registerIndexer(GlodaMsgIndexer);
diff --git a/comm/mailnews/db/gloda/modules/MimeMessage.jsm b/comm/mailnews/db/gloda/modules/MimeMessage.jsm
new file mode 100644
index 0000000000..8859f10877
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/MimeMessage.jsm
@@ -0,0 +1,821 @@
+/* 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 = [
+ "MsgHdrToMimeMessage",
+ "MimeMessage",
+ "MimeContainer",
+ "MimeBody",
+ "MimeUnknown",
+ "MimeMessageAttachment",
+];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * The URL listener is surplus because the CallbackStreamListener ends up
+ * getting the same set of events, effectively.
+ */
+var dumbUrlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {},
+};
+
+/**
+ * Maintain a list of all active stream listeners so that we can cancel them all
+ * during shutdown. If we don't cancel them, we risk calls into javascript
+ * from C++ after the various XPConnect contexts have already begun their
+ * teardown process.
+ */
+var activeStreamListeners = {};
+
+var shutdownCleanupObserver = {
+ _initialized: false,
+ ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "quit-application");
+
+ this._initialized = true;
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ Services.obs.removeObserver(this, "quit-application");
+
+ for (let uri in activeStreamListeners) {
+ let streamListener = activeStreamListeners[uri];
+ if (streamListener._request) {
+ streamListener._request.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+ }
+ },
+};
+
+function CallbackStreamListener(aMsgHdr, aCallbackThis, aCallback) {
+ this._msgHdr = aMsgHdr;
+ // Messages opened from file or attachments do not have a folder property, but
+ // have their url stored as a string property.
+ let hdrURI = aMsgHdr.folder
+ ? aMsgHdr.folder.getUriForMsg(aMsgHdr)
+ : aMsgHdr.getStringProperty("dummyMsgUrl");
+
+ this._request = null;
+ this._stream = null;
+ if (aCallback === undefined) {
+ this._callbacksThis = [null];
+ this._callbacks = [aCallbackThis];
+ } else {
+ this._callbacksThis = [aCallbackThis];
+ this._callbacks = [aCallback];
+ }
+ activeStreamListeners[hdrURI] = this;
+}
+
+/**
+ * @implements {nsIRequestObserver}
+ * @implements {nsIStreamListener}
+ */
+CallbackStreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ // nsIRequestObserver part
+ onStartRequest(aRequest) {
+ this._request = aRequest;
+ },
+ onStopRequest(aRequest, aStatusCode) {
+ // Messages opened from file or attachments do not have a folder property,
+ // but have their url stored as a string property.
+ let msgURI = this._msgHdr.folder
+ ? this._msgHdr.folder.getUriForMsg(this._msgHdr)
+ : this._msgHdr.getStringProperty("dummyMsgUrl");
+ delete activeStreamListeners[msgURI];
+
+ aRequest.QueryInterface(Ci.nsIChannel);
+ let message = MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec];
+ if (message === undefined) {
+ message = null;
+ }
+
+ delete MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec];
+
+ for (let i = 0; i < this._callbacksThis.length; i++) {
+ try {
+ this._callbacks[i].call(this._callbacksThis[i], this._msgHdr, message);
+ } catch (e) {
+ // Most of the time, exceptions will silently disappear into the endless
+ // deeps of XPConnect, and never reach the surface ever again. At least
+ // warn the user if he has dump enabled.
+ dump(
+ "The MsgHdrToMimeMessage callback threw an exception: " + e + "\n"
+ );
+ // That one will probably never make it to the original caller.
+ throw e;
+ }
+ }
+
+ this._msgHdr = null;
+ this._request = null;
+ this._stream = null;
+ this._callbacksThis = null;
+ this._callbacks = null;
+ },
+
+ // nsIStreamListener part
+
+ /**
+ * Our onDataAvailable should actually never be called. The stream converter
+ * is actually eating everything except the start and stop notification.
+ */
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ throw new Error(
+ `The stream converter should have grabbed the data for ${aRequest?.URI.spec}`
+ );
+ },
+};
+
+function stripEncryptedParts(aPart) {
+ if (aPart.parts && aPart.isEncrypted) {
+ aPart.parts = []; // Show an empty container.
+ } else if (aPart.parts) {
+ aPart.parts = aPart.parts.map(stripEncryptedParts);
+ }
+ return aPart;
+}
+
+/**
+ * Starts retrieval of a MimeMessage instance for the given message header.
+ * Your callback will be called with the message header you provide and the
+ *
+ * @param aMsgHdr The message header to retrieve the body for and build a MIME
+ * representation of the message.
+ * @param aCallbackThis The (optional) 'this' to use for your callback function.
+ * @param aCallback The callback function to invoke on completion of message
+ * parsing or failure. The first argument passed will be the nsIMsgDBHdr
+ * you passed to this function. The second argument will be the MimeMessage
+ * instance resulting from the processing on success, and null on failure.
+ * @param [aAllowDownload=false] Should we allow the message to be downloaded
+ * for this streaming request? The default is false, which means that we
+ * require that the message be available offline. If false is passed and
+ * the message is not available offline, we will propagate an exception
+ * thrown by the underlying code.
+ * @param [aOptions] Optional options.
+ * @param [aOptions.saneBodySize] Limit body sizes to a 'reasonable' size in
+ * order to combat corrupt offline/message stores creating pathological
+ * situations where we have erroneously multi-megabyte messages. This
+ * also likely reduces the impact of legitimately ridiculously large
+ * messages.
+ * @param [aOptions.examineEncryptedParts] By default, we won't reveal the
+ * contents of multipart/encrypted parts to the consumers, unless explicitly
+ * requested. In the case of MIME/PGP messages, for instance, the message
+ * will appear as an empty multipart/encrypted container, unless this option
+ * is used.
+ */
+function MsgHdrToMimeMessage(
+ aMsgHdr,
+ aCallbackThis,
+ aCallback,
+ aAllowDownload,
+ aOptions
+) {
+ shutdownCleanupObserver.ensureInitialized();
+
+ let requireOffline = !aAllowDownload;
+ // Messages opened from file or attachments do not have a folder property, but
+ // have their url stored as a string property.
+ let msgURI = aMsgHdr.folder
+ ? aMsgHdr.folder.getUriForMsg(aMsgHdr)
+ : aMsgHdr.getStringProperty("dummyMsgUrl");
+
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ MsgHdrToMimeMessage.OPTION_TUNNEL = aOptions;
+ // By default, Enigmail only decrypts a message streamed via libmime if it's
+ // the one currently on display in the message reader. With this option, we're
+ // letting Enigmail know that it should decrypt the message since the client
+ // explicitly asked for it.
+ let encryptedStr =
+ aOptions && aOptions.examineEncryptedParts
+ ? "&examineEncryptedParts=true"
+ : "";
+
+ // S/MIME, our other encryption backend, is not that smart, and always
+ // decrypts data. In order to protect sensitive data (e.g. not index it in
+ // Gloda), unless the client asked for encrypted data, we pass to the client
+ // callback a stripped-down version of the MIME structure where encrypted
+ // parts have been removed.
+ let wrapCallback = function (aCallback, aCallbackThis) {
+ if (aOptions && aOptions.examineEncryptedParts) {
+ return aCallback;
+ }
+ return (aMsgHdr, aMimeMsg) =>
+ aCallback.call(aCallbackThis, aMsgHdr, stripEncryptedParts(aMimeMsg));
+ };
+
+ // Apparently there used to be an old syntax where the callback was the second
+ // argument...
+ let callback = aCallback ? aCallback : aCallbackThis;
+ let callbackThis = aCallback ? aCallbackThis : null;
+
+ // if we're already streaming this msg, just add the callback
+ // to the listener.
+ let listenerForURI = activeStreamListeners[msgURI];
+ if (listenerForURI != undefined) {
+ listenerForURI._callbacks.push(wrapCallback(callback, callbackThis));
+ listenerForURI._callbacksThis.push(callbackThis);
+ return;
+ }
+ let streamListener = new CallbackStreamListener(
+ aMsgHdr,
+ callbackThis,
+ wrapCallback(callback, callbackThis)
+ );
+
+ try {
+ msgService.streamMessage(
+ msgURI,
+ streamListener, // consumer
+ null, // nsIMsgWindow
+ dumbUrlListener, // nsIUrlListener
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter&emitter=js" + encryptedStr,
+ requireOffline
+ );
+ } catch (ex) {
+ // If streamMessage throws an exception, we should make sure to clear the
+ // activeStreamListener, or any subsequent attempt at sreaming this URI
+ // will silently fail
+ if (activeStreamListeners[msgURI]) {
+ delete activeStreamListeners[msgURI];
+ }
+ MsgHdrToMimeMessage.OPTION_TUNNEL = null;
+ throw ex;
+ }
+
+ MsgHdrToMimeMessage.OPTION_TUNNEL = null;
+}
+
+/**
+ * Let the jsmimeemitter provide us with results. The poor emitter (if I am
+ * understanding things correctly) is evaluated outside of the C.u.import
+ * world, so if we were to import him, we would not see him, but rather a new
+ * copy of him. This goes for his globals, etc. (and is why we live in this
+ * file right here). Also, it appears that the XPCOM JS wrappers aren't
+ * magically unified so that we can try and pass data as expando properties
+ * on things like the nsIUri instances either. So we have the jsmimeemitter
+ * import us and poke things into RESULT_RENDEVOUZ. We put it here on this
+ * function to try and be stealthy and avoid polluting the namespaces (or
+ * encouraging bad behaviour) of our importers.
+ *
+ * If you can come up with a prettier way to shuttle this data, please do.
+ */
+MsgHdrToMimeMessage.RESULT_RENDEVOUZ = {};
+/**
+ * Cram rich options here for the MimeMessageEmitter to grab from. We
+ * leverage the known control-flow to avoid needing a whole dictionary here.
+ * We set this immediately before constructing the emitter and clear it
+ * afterwards. Control flow is never yielded during the process and reentrancy
+ * cannot happen via any other means.
+ */
+MsgHdrToMimeMessage.OPTION_TUNNEL = null;
+
+var HeaderHandlerBase = {
+ /**
+ * Look-up a header that should be present at most once.
+ *
+ * @param aHeaderName The header name to retrieve, case does not matter.
+ * @param aDefaultValue The value to return if the header was not found, null
+ * if left unspecified.
+ * @returns the value of the header if present, and the default value if not
+ * (defaults to null). If the header was present multiple times, the first
+ * instance of the header is returned. Use getAll if you want all of the
+ * values for the multiply-defined header.
+ */
+ get(aHeaderName, aDefaultValue) {
+ if (aDefaultValue === undefined) {
+ aDefaultValue = null;
+ }
+ let lowerHeader = aHeaderName.toLowerCase();
+ if (lowerHeader in this.headers) {
+ // we require that the list cannot be empty if present
+ return this.headers[lowerHeader][0];
+ }
+ return aDefaultValue;
+ },
+ /**
+ * Look-up a header that can be present multiple times. Use get for headers
+ * that you only expect to be present at most once.
+ *
+ * @param aHeaderName The header name to retrieve, case does not matter.
+ * @returns An array containing the values observed, which may mean a zero
+ * length array.
+ */
+ getAll(aHeaderName) {
+ let lowerHeader = aHeaderName.toLowerCase();
+ if (lowerHeader in this.headers) {
+ return this.headers[lowerHeader];
+ }
+ return [];
+ },
+ /**
+ * @param aHeaderName Header name to test for its presence.
+ * @returns true if the message has (at least one value for) the given header
+ * name.
+ */
+ has(aHeaderName) {
+ let lowerHeader = aHeaderName.toLowerCase();
+ return lowerHeader in this.headers;
+ },
+ _prettyHeaderString(aIndent) {
+ if (aIndent === undefined) {
+ aIndent = "";
+ }
+ let s = "";
+ for (let header in this.headers) {
+ let values = this.headers[header];
+ s += "\n " + aIndent + header + ": " + values;
+ }
+ return s;
+ },
+};
+
+/**
+ * @ivar partName The MIME part, ex "1.2.2.1". The partName of a (top-level)
+ * message is "1", its first child is "1.1", its second child is "1.2",
+ * its first child's first child is "1.1.1", etc.
+ * @ivar headers Maps lower-cased header field names to a list of the values
+ * seen for the given header. Use get or getAll as convenience helpers.
+ * @ivar parts The list of the MIME part children of this message. Children
+ * will be either MimeMessage instances, MimeMessageAttachment instances,
+ * MimeContainer instances, or MimeUnknown instances. The latter two are
+ * the result of limitations in the Javascript representation generation
+ * at this time, combined with the need to most accurately represent the
+ * MIME structure.
+ */
+function MimeMessage() {
+ this.partName = null;
+ this.headers = {};
+ this.parts = [];
+ this.isEncrypted = false;
+}
+
+MimeMessage.prototype = {
+ __proto__: HeaderHandlerBase,
+ contentType: "message/rfc822",
+
+ /**
+ * @returns a list of all attachments contained in this message and all its
+ * sub-messages. Only MimeMessageAttachment instances will be present in
+ * the list (no sub-messages).
+ */
+ get allAttachments() {
+ let results = []; // messages are not attachments, don't include self
+ for (let iChild = 0; iChild < this.parts.length; iChild++) {
+ let child = this.parts[iChild];
+ results = results.concat(child.allAttachments);
+ }
+ return results;
+ },
+
+ /**
+ * @returns a list of all attachments contained in this message and all its
+ * sub-messages, including the sub-messages.
+ */
+ get allInlineAttachments() {
+ // Do not include the top message, but only sub-messages.
+ let results = this.partName ? [this] : [];
+ for (let iChild = 0; iChild < this.parts.length; iChild++) {
+ let child = this.parts[iChild];
+ results = results.concat(child.allInlineAttachments);
+ }
+ return results;
+ },
+
+ /**
+ * @returns a list of all attachments contained in this message, with
+ * included/forwarded messages treated as real attachments. Attachments
+ * contained in inner messages won't be shown.
+ */
+ get allUserAttachments() {
+ if (this.url) {
+ // The jsmimeemitter camouflaged us as a MimeAttachment
+ return [this];
+ }
+ return this.parts
+ .map(child => child.allUserAttachments)
+ .reduce((a, b) => a.concat(b), []);
+ },
+
+ /**
+ * @returns the total size of this message, that is, the size of all subparts
+ */
+ get size() {
+ return this.parts
+ .map(child => child.size)
+ .reduce((a, b) => a + Math.max(b, 0), 0);
+ },
+
+ /**
+ * In the case of attached messages, libmime considers them as attachments,
+ * and if the body is, say, quoted-printable encoded, then libmime will start
+ * counting bytes and notify the js mime emitter about it. The JS mime emitter
+ * being a nice guy, it will try to set a size on us. While this is the
+ * expected behavior for MimeMsgAttachments, we must make sure we can handle
+ * that (failing to write a setter results in exceptions being thrown).
+ */
+ set size(whatever) {
+ // nop
+ },
+
+ /**
+ * @param aMsgFolder A message folder, any message folder. Because this is
+ * a hack.
+ * @returns The concatenation of all of the body parts where parts
+ * available as text/plain are pulled as-is, and parts only available
+ * as text/html are converted to plaintext form first. In other words,
+ * if we see a multipart/alternative with a text/plain, we take the
+ * text/plain. If we see a text/html without an alternative, we convert
+ * that to text.
+ */
+ coerceBodyToPlaintext(aMsgFolder) {
+ let bodies = [];
+ for (let part of this.parts) {
+ // an undefined value for something not having the method is fine
+ let body =
+ part.coerceBodyToPlaintext && part.coerceBodyToPlaintext(aMsgFolder);
+ if (body) {
+ bodies.push(body);
+ }
+ }
+ if (bodies) {
+ return bodies.join("");
+ }
+ return "";
+ },
+
+ /**
+ * 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, attachments have their filename and
+ * content-type (ex: image/jpeg) displayed. "Filler" classes simply have
+ * their class displayed.
+ */
+ prettyString(aVerbose, aIndent, aDumpBody) {
+ if (aIndent === undefined) {
+ aIndent = "";
+ }
+ let nextIndent = aIndent + " ";
+
+ let s =
+ "Message " +
+ (this.isEncrypted ? "[encrypted] " : "") +
+ "(" +
+ this.size +
+ " bytes): " +
+ "subject" in
+ this.headers
+ ? this.headers.subject
+ : "";
+ if (aVerbose) {
+ s += this._prettyHeaderString(nextIndent);
+ }
+
+ for (let iPart = 0; iPart < this.parts.length; iPart++) {
+ let part = this.parts[iPart];
+ s +=
+ "\n" +
+ nextIndent +
+ (iPart + 1) +
+ " " +
+ part.prettyString(aVerbose, nextIndent, aDumpBody);
+ }
+
+ return s;
+ },
+};
+
+/**
+ * @ivar contentType The content-type of this container.
+ * @ivar parts The parts held by this container. These can be instances of any
+ * of the classes found in this file.
+ */
+function MimeContainer(aContentType) {
+ this.partName = null;
+ this.contentType = aContentType;
+ this.headers = {};
+ this.parts = [];
+ this.isEncrypted = false;
+}
+
+MimeContainer.prototype = {
+ __proto__: HeaderHandlerBase,
+ get allAttachments() {
+ let results = [];
+ for (let iChild = 0; iChild < this.parts.length; iChild++) {
+ let child = this.parts[iChild];
+ results = results.concat(child.allAttachments);
+ }
+ return results;
+ },
+ get allInlineAttachments() {
+ let results = [];
+ for (let iChild = 0; iChild < this.parts.length; iChild++) {
+ let child = this.parts[iChild];
+ results = results.concat(child.allInlineAttachments);
+ }
+ return results;
+ },
+ get allUserAttachments() {
+ return this.parts
+ .map(child => child.allUserAttachments)
+ .reduce((a, b) => a.concat(b), []);
+ },
+ get size() {
+ return this.parts
+ .map(child => child.size)
+ .reduce((a, b) => a + Math.max(b, 0), 0);
+ },
+ set size(whatever) {
+ // nop
+ },
+ coerceBodyToPlaintext(aMsgFolder) {
+ if (this.contentType == "multipart/alternative") {
+ let htmlPart;
+ // pick the text/plain if we can find one, otherwise remember the HTML one
+ for (let part of this.parts) {
+ if (part.contentType == "text/plain") {
+ return part.body;
+ }
+ if (part.contentType == "text/html") {
+ htmlPart = part;
+ } else if (!htmlPart && part.contentType == "text/enriched") {
+ // text/enriched gets transformed into HTML, so use it if we don't
+ // already have an HTML part.
+ htmlPart = part;
+ }
+ }
+ // convert the HTML part if we have one
+ if (htmlPart) {
+ return aMsgFolder.convertMsgSnippetToPlainText(htmlPart.body);
+ }
+ }
+ // if it's not alternative, recurse/aggregate using MimeMessage logic
+ return MimeMessage.prototype.coerceBodyToPlaintext.call(this, aMsgFolder);
+ },
+ prettyString(aVerbose, aIndent, aDumpBody) {
+ let nextIndent = aIndent + " ";
+
+ let s =
+ "Container " +
+ (this.isEncrypted ? "[encrypted] " : "") +
+ "(" +
+ this.size +
+ " bytes): " +
+ this.contentType;
+ if (aVerbose) {
+ s += this._prettyHeaderString(nextIndent);
+ }
+
+ for (let iPart = 0; iPart < this.parts.length; iPart++) {
+ let part = this.parts[iPart];
+ s +=
+ "\n" +
+ nextIndent +
+ (iPart + 1) +
+ " " +
+ part.prettyString(aVerbose, nextIndent, aDumpBody);
+ }
+
+ return s;
+ },
+ toString() {
+ return "Container: " + this.contentType;
+ },
+};
+
+/**
+ * @class Represents a body portion that we understand and do not believe to be
+ * a proper attachment. This means text/plain or text/html and it has no
+ * filename. (A filename suggests an attachment.)
+ *
+ * @ivar contentType The content type of this body materal; text/plain or
+ * text/html.
+ * @ivar body The actual body content.
+ */
+function MimeBody(aContentType) {
+ this.partName = null;
+ this.contentType = aContentType;
+ this.headers = {};
+ this.body = "";
+ this.isEncrypted = false;
+}
+
+MimeBody.prototype = {
+ __proto__: HeaderHandlerBase,
+ get allAttachments() {
+ return []; // we are a leaf
+ },
+ get allInlineAttachments() {
+ return []; // we are a leaf
+ },
+ get allUserAttachments() {
+ return []; // we are a leaf
+ },
+ get size() {
+ return this.body.length;
+ },
+ set size(whatever) {
+ // nop
+ },
+ appendBody(aBuf) {
+ this.body += aBuf;
+ },
+ coerceBodyToPlaintext(aMsgFolder) {
+ if (this.contentType == "text/plain") {
+ return this.body;
+ }
+ // text/enriched gets transformed into HTML by libmime
+ if (
+ this.contentType == "text/html" ||
+ this.contentType == "text/enriched"
+ ) {
+ return aMsgFolder.convertMsgSnippetToPlainText(this.body);
+ }
+ return "";
+ },
+ prettyString(aVerbose, aIndent, aDumpBody) {
+ let s =
+ "Body: " +
+ (this.isEncrypted ? "[encrypted] " : "") +
+ "" +
+ this.contentType +
+ " (" +
+ this.body.length +
+ " bytes" +
+ (aDumpBody ? ": '" + this.body + "'" : "") +
+ ")";
+ if (aVerbose) {
+ s += this._prettyHeaderString(aIndent + " ");
+ }
+ return s;
+ },
+ toString() {
+ return "Body: " + this.contentType + " (" + this.body.length + " bytes)";
+ },
+};
+
+/**
+ * @class A MIME Leaf node that doesn't have a filename so we assume it's not
+ * intended to be an attachment proper. This is probably meant for inline
+ * display or is the result of someone amusing themselves by composing messages
+ * by hand or a bad client. This class should probably be renamed or we should
+ * introduce a better named class that we try and use in preference to this
+ * class.
+ *
+ * @ivar contentType The content type of this part.
+ */
+function MimeUnknown(aContentType) {
+ this.partName = null;
+ this.contentType = aContentType;
+ this.headers = {};
+ // Looks like libmime does not always interpret us as an attachment, which
+ // means we'll have to have a default size. Returning undefined would cause
+ // the recursive size computations to fail.
+ this._size = 0;
+ this.isEncrypted = false;
+ // We want to make sure MimeUnknown has a part property: S/MIME encrypted
+ // messages have a topmost MimeUnknown part, with the encrypted bit set to 1,
+ // and we need to ensure all other encrypted parts are children of this
+ // topmost part.
+ this.parts = [];
+}
+
+MimeUnknown.prototype = {
+ __proto__: HeaderHandlerBase,
+ get allAttachments() {
+ return this.parts
+ .map(child => child.allAttachments)
+ .reduce((a, b) => a.concat(b), []);
+ },
+ get allInlineAttachments() {
+ return this.parts
+ .map(child => child.allInlineAttachments)
+ .reduce((a, b) => a.concat(b), []);
+ },
+ get allUserAttachments() {
+ return this.parts
+ .map(child => child.allUserAttachments)
+ .reduce((a, b) => a.concat(b), []);
+ },
+ get size() {
+ return (
+ this._size +
+ this.parts
+ .map(child => child.size)
+ .reduce((a, b) => a + Math.max(b, 0), 0)
+ );
+ },
+ set size(aSize) {
+ this._size = aSize;
+ },
+ prettyString(aVerbose, aIndent, aDumpBody) {
+ let nextIndent = aIndent + " ";
+
+ let s =
+ "Unknown: " +
+ (this.isEncrypted ? "[encrypted] " : "") +
+ "" +
+ this.contentType +
+ " (" +
+ this.size +
+ " bytes)";
+ if (aVerbose) {
+ s += this._prettyHeaderString(aIndent + " ");
+ }
+
+ for (let iPart = 0; iPart < this.parts.length; iPart++) {
+ let part = this.parts[iPart];
+ s +=
+ "\n" +
+ nextIndent +
+ (iPart + 1) +
+ " " +
+ (part ? part.prettyString(aVerbose, nextIndent, aDumpBody) : "NULL");
+ }
+ return s;
+ },
+ toString() {
+ return "Unknown: " + this.contentType;
+ },
+};
+
+/**
+ * @class An attachment proper. We think it's an attachment because it has a
+ * filename that libmime was able to figure out.
+ *
+ * @ivar partName @see{MimeMessage.partName}
+ * @ivar name The filename of this attachment.
+ * @ivar contentType The MIME content type of this part.
+ * @ivar url The URL to stream if you want the contents of this part.
+ * @ivar isExternal Is the attachment stored someplace else than in the message?
+ * @ivar size The size of the attachment if available, -1 otherwise (size is set
+ * after initialization by jsmimeemitter.js)
+ */
+function MimeMessageAttachment(
+ aPartName,
+ aName,
+ aContentType,
+ aUrl,
+ aIsExternal
+) {
+ this.partName = aPartName;
+ this.name = aName;
+ this.contentType = aContentType;
+ this.url = aUrl;
+ this.isExternal = aIsExternal;
+ this.headers = {};
+ this.isEncrypted = false;
+ // parts is copied over from the part instance that preceded us
+ // headers is copied over from the part instance that preceded us
+ // isEncrypted is copied over from the part instance that preceded us
+}
+
+MimeMessageAttachment.prototype = {
+ __proto__: HeaderHandlerBase,
+ get allAttachments() {
+ return [this]; // we are a leaf, so just us.
+ },
+ get allInlineAttachments() {
+ return [this]; // we are a leaf, so just us.
+ },
+ get allUserAttachments() {
+ return [this];
+ },
+ prettyString(aVerbose, aIndent, aDumpBody) {
+ let s =
+ "Attachment " +
+ (this.isEncrypted ? "[encrypted] " : "") +
+ "(" +
+ this.size +
+ " bytes): " +
+ this.name +
+ ", " +
+ this.contentType;
+ if (aVerbose) {
+ s += this._prettyHeaderString(aIndent + " ");
+ }
+ return s;
+ },
+ toString() {
+ return this.prettyString(false, "");
+ },
+};
diff --git a/comm/mailnews/db/gloda/modules/NounFreetag.jsm b/comm/mailnews/db/gloda/modules/NounFreetag.jsm
new file mode 100644
index 0000000000..cb169645f1
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/NounFreetag.jsm
@@ -0,0 +1,91 @@
+/* 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 = ["FreeTag", "FreeTagNoun"];
+
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+
+function FreeTag(aTagName) {
+ this.name = aTagName;
+}
+
+FreeTag.prototype = {
+ toString() {
+ return this.name;
+ },
+};
+
+/**
+ * @namespace Tag noun provider. Since the tag unique value is stored as a
+ * parameter, we are an odd case and semantically confused.
+ */
+var FreeTagNoun = {
+ _log: console.createInstance({
+ prefix: "gloda.noun.freetag",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+ }),
+
+ name: "freetag",
+ clazz: FreeTag,
+ allowsArbitraryAttrs: false,
+ usesParameter: true,
+
+ _listeners: [],
+ addListener(aListener) {
+ this._listeners.push(aListener);
+ },
+ removeListener(aListener) {
+ let index = this._listeners.indexOf(aListener);
+ if (index >= 0) {
+ this._listeners.splice(index, 1);
+ }
+ },
+
+ populateKnownFreeTags() {
+ for (let attr of this.objectNounOfAttributes) {
+ let attrDB = attr.dbDef;
+ for (let param in attrDB.parameterBindings) {
+ this.getFreeTag(param);
+ }
+ }
+ },
+
+ knownFreeTags: {},
+ getFreeTag(aTagName) {
+ let tag = this.knownFreeTags[aTagName];
+ if (!tag) {
+ tag = this.knownFreeTags[aTagName] = new FreeTag(aTagName);
+ for (let listener of this._listeners) {
+ listener.onFreeTagAdded(tag);
+ }
+ }
+ return tag;
+ },
+
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.name.localeCompare(b.name);
+ },
+
+ toParamAndValue(aTag) {
+ return [aTag.name, null];
+ },
+
+ toJSON(aTag) {
+ return aTag.name;
+ },
+ fromJSON(aTagName) {
+ return this.getFreeTag(aTagName);
+ },
+};
+
+Gloda.defineNoun(FreeTagNoun);
diff --git a/comm/mailnews/db/gloda/modules/NounMimetype.jsm b/comm/mailnews/db/gloda/modules/NounMimetype.jsm
new file mode 100644
index 0000000000..fef1a33bc7
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/NounMimetype.jsm
@@ -0,0 +1,582 @@
+/* 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 = ["MimeType", "MimeTypeNoun"];
+
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+var LOG = console.createInstance({
+ prefix: "gloda.noun.mimetype",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+var CategoryStringMap = {};
+
+/**
+ * Input data structure to allow us to build a fast mapping from mime type to
+ * category name. The keys in MimeCategoryMapping are the top-level
+ * categories. Each value can either be a list of MIME types or a nested
+ * object which recursively defines sub-categories. We currently do not use
+ * the sub-categories. They are just there to try and organize the MIME types
+ * a little and open the door to future enhancements.
+ *
+ * Do _not_ add additional top-level categories unless you have added
+ * corresponding entries to gloda.properties under the
+ * "gloda.mimetype.category" branch and are making sure localizers are aware
+ * of the change and have time to localize it.
+ *
+ * Entries with wildcards in them are part of a fallback strategy by the
+ * |mimeTypeNoun| and do not actually use regular expressions or anything like
+ * that. Everything is a straight string lookup. Given "foo/bar" we look for
+ * "foo/bar", then "foo/*", and finally "*".
+ */
+var MimeCategoryMapping = {
+ archives: [
+ "application/java-archive",
+ "application/x-java-archive",
+ "application/x-jar",
+ "application/x-java-jnlp-file",
+
+ "application/mac-binhex40",
+ "application/vnd.ms-cab-compressed",
+
+ "application/x-arc",
+ "application/x-arj",
+ "application/x-compress",
+ "application/x-compressed-tar",
+ "application/x-cpio",
+ "application/x-cpio-compressed",
+ "application/x-deb",
+
+ "application/x-bittorrent",
+
+ "application/x-rar",
+ "application/x-rar-compressed",
+ "application/x-7z-compressed",
+ "application/zip",
+ "application/x-zip-compressed",
+ "application/x-zip",
+
+ "application/x-bzip",
+ "application/x-bzip-compressed-tar",
+ "application/x-bzip2",
+ "application/x-gzip",
+ "application/x-tar",
+ "application/x-tar-gz",
+ "application/x-tarz",
+ ],
+ documents: {
+ database: [
+ "application/vnd.ms-access",
+ "application/x-msaccess",
+ "application/msaccess",
+ "application/vnd.msaccess",
+ "application/x-msaccess",
+ "application/mdb",
+ "application/x-mdb",
+
+ "application/vnd.oasis.opendocument.database",
+ ],
+ graphics: [
+ "application/postscript",
+ "application/x-bzpostscript",
+ "application/x-dvi",
+ "application/x-gzdvi",
+
+ "application/illustrator",
+
+ "application/vnd.corel-draw",
+ "application/cdr",
+ "application/coreldraw",
+ "application/x-cdr",
+ "application/x-coreldraw",
+ "image/cdr",
+ "image/x-cdr",
+ "zz-application/zz-winassoc-cdr",
+
+ "application/vnd.oasis.opendocument.graphics",
+ "application/vnd.oasis.opendocument.graphics-template",
+ "application/vnd.oasis.opendocument.image",
+
+ "application/x-dia-diagram",
+ ],
+ presentation: [
+ "application/vnd.ms-powerpoint.presentation.macroenabled.12",
+ "application/vnd.ms-powerpoint.template.macroenabled.12",
+ "application/vnd.ms-powerpoint",
+ "application/powerpoint",
+ "application/mspowerpoint",
+ "application/x-mspowerpoint",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
+
+ "application/vnd.oasis.opendocument.presentation",
+ "application/vnd.oasis.opendocument.presentation-template",
+ ],
+ spreadsheet: [
+ "application/vnd.lotus-1-2-3",
+ "application/x-lotus123",
+ "application/x-123",
+ "application/lotus123",
+ "application/wk1",
+
+ "application/x-quattropro",
+
+ "application/vnd.ms-excel.sheet.binary.macroenabled.12",
+ "application/vnd.ms-excel.sheet.macroenabled.12",
+ "application/vnd.ms-excel.template.macroenabled.12",
+ "application/vnd.ms-excel",
+ "application/msexcel",
+ "application/x-msexcel",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+
+ "application/vnd.oasis.opendocument.formula",
+ "application/vnd.oasis.opendocument.formula-template",
+ "application/vnd.oasis.opendocument.chart",
+ "application/vnd.oasis.opendocument.chart-template",
+ "application/vnd.oasis.opendocument.spreadsheet",
+ "application/vnd.oasis.opendocument.spreadsheet-template",
+
+ "application/x-gnumeric",
+ ],
+ wordProcessor: [
+ "application/msword",
+ "application/vnd.ms-word",
+ "application/x-msword",
+ "application/msword-template",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ "application/vnd.ms-word.document.macroenabled.12",
+ "application/vnd.ms-word.template.macroenabled.12",
+ "application/x-mswrite",
+ "application/x-pocket-word",
+
+ "application/rtf",
+ "text/rtf",
+
+ "application/vnd.oasis.opendocument.text",
+ "application/vnd.oasis.opendocument.text-master",
+ "application/vnd.oasis.opendocument.text-template",
+ "application/vnd.oasis.opendocument.text-web",
+
+ "application/vnd.wordperfect",
+
+ "application/x-abiword",
+ "application/x-amipro",
+ ],
+ suite: ["application/vnd.ms-works"],
+ },
+ images: ["image/*"],
+ media: {
+ audio: ["audio/*"],
+ video: ["video/*"],
+ container: [
+ "application/ogg",
+
+ "application/smil",
+ "application/vnd.ms-asf",
+ "application/vnd.rn-realmedia",
+ "application/x-matroska",
+ "application/x-quicktime-media-link",
+ "application/x-quicktimeplayer",
+ ],
+ },
+ other: ["*"],
+ pdf: [
+ "application/pdf",
+ "application/x-pdf",
+ "image/pdf",
+ "file/pdf",
+ "application/x-bzpdf",
+ "application/x-gzpdf",
+ ],
+};
+
+/**
+ * Mime type abstraction that exists primarily so we can map mime types to
+ * integer id's.
+ *
+ * Instances of this class should only be retrieved via |MimeTypeNoun|; no one
+ * should ever create an instance directly.
+ */
+function MimeType(aID, aType, aSubType, aFullType, aCategory) {
+ this._id = aID;
+ this._type = aType;
+ this._subType = aSubType;
+ this._fullType = aFullType;
+ this._category = aCategory;
+}
+
+MimeType.prototype = {
+ /**
+ * The integer id we have associated with the mime type. This is stable for
+ * the lifetime of the database, which means that anything in the Gloda
+ * database can use this without fear. Things not persisted in the database
+ * should use the actual string mime type, retrieval via |fullType|.
+ */
+ get id() {
+ return this._id;
+ },
+ /**
+ * The first part of the MIME type; "text/plain" gets you "text".
+ */
+ get type() {
+ return this._type;
+ },
+ set fullType(aFullType) {
+ if (!this._fullType) {
+ this._fullType = aFullType;
+ [this._type, this._subType] = this._fullType.split("/");
+ this._category = MimeTypeNoun._getCategoryForMimeType(
+ aFullType,
+ this._type
+ );
+ }
+ },
+ /**
+ * If the |fullType| is "text/plain", subType is "plain".
+ */
+ get subType() {
+ return this._subType;
+ },
+ /**
+ * The full MIME type; "text/plain" returns "text/plain".
+ */
+ get fullType() {
+ return this._fullType;
+ },
+ toString() {
+ return this.fullType;
+ },
+
+ /**
+ * @returns the category we believe this mime type belongs to. This category
+ * name should never be shown directly to the user. Instead, use
+ * |categoryLabel| to get the localized name for the category. The
+ * category mapping comes from mimeTypesCategories.js.
+ */
+ get category() {
+ return this._category;
+ },
+ /**
+ * @returns The localized label for the category from gloda.properties in the
+ * "gloda.mimetype.category.CATEGORY.label" definition using the value
+ * from |category|.
+ */
+ get categoryLabel() {
+ return CategoryStringMap[this._category];
+ },
+};
+
+/**
+ * Mime type noun provider.
+ *
+ * The set of MIME Types is sufficiently limited that we can keep them all in
+ * memory. In theory it is also sufficiently limited that we could use the
+ * parameter mechanism in the database. However, it is more efficient, for
+ * both space and performance reasons, to store the specific mime type as a
+ * value. For future-proofing reasons, we opt to use a database table to
+ * persist the mapping rather than a hard-coded list. A preferences file or
+ * other text file would arguably suffice, but for consistency reasons, the
+ * database is not a bad thing.
+ */
+var MimeTypeNoun = {
+ name: "mime-type",
+ clazz: MimeType, // gloda supports clazz as well as class
+ allowsArbitraryAttrs: false,
+
+ _strings: Services.strings.createBundle(
+ "chrome://messenger/locale/gloda.properties"
+ ),
+
+ // note! update test_noun_mimetype if you change our internals!
+ _mimeTypes: {},
+ _mimeTypesByID: {},
+ TYPE_BLOCK_SIZE: 16384,
+ _mimeTypeHighID: {},
+ _mimeTypeRangeDummyObjects: {},
+ _highID: 0,
+
+ // we now use the exciting 'schema' mechanism of defineNoun to get our table
+ // created for us, plus some helper methods that we simply don't use.
+ schema: {
+ name: "mimeTypes",
+ columns: [
+ ["id", "INTEGER PRIMARY KEY", "_id"],
+ ["mimeType", "TEXT", "fullType"],
+ ],
+ },
+
+ _init() {
+ LOG.debug("loading MIME types");
+ this._loadCategoryMapping();
+ this._loadMimeTypes();
+ },
+
+ /**
+ * A map from MIME type to category name.
+ */
+ _mimeTypeToCategory: {},
+ /**
+ * Load the contents of MimeTypeCategories and populate
+ */
+ _loadCategoryMapping() {
+ let mimeTypeToCategory = this._mimeTypeToCategory;
+
+ function procMapObj(aSubTree, aCategories) {
+ for (let key in aSubTree) {
+ let value = aSubTree[key];
+ // Add this category to our nested categories list. Use concat since
+ // the list will be long-lived and each list needs to be distinct.
+ let categories = aCategories.concat();
+ categories.push(key);
+
+ if (categories.length == 1) {
+ CategoryStringMap[key] = MimeTypeNoun._strings.GetStringFromName(
+ "gloda.mimetype.category." + key + ".label"
+ );
+ }
+
+ // Is it an array? If so, just process this depth
+ if (Array.isArray(value)) {
+ for (let mimeTypeStr of value) {
+ mimeTypeToCategory[mimeTypeStr] = categories;
+ }
+ } else {
+ // it's yet another sub-tree branch
+ procMapObj(value, categories);
+ }
+ }
+ }
+ procMapObj(MimeCategoryMapping, []);
+ },
+
+ /**
+ * Lookup the category associated with a MIME type given its full type and
+ * type. (So, "foo/bar" and "foo" for "foo/bar".)
+ */
+ _getCategoryForMimeType(aFullType, aType) {
+ if (aFullType in this._mimeTypeToCategory) {
+ return this._mimeTypeToCategory[aFullType][0];
+ }
+ let wildType = aType + "/*";
+ if (wildType in this._mimeTypeToCategory) {
+ return this._mimeTypeToCategory[wildType][0];
+ }
+ return this._mimeTypeToCategory["*"][0];
+ },
+
+ /**
+ * In order to allow the gloda query mechanism to avoid hitting the database,
+ * we need to either define the noun type as cacheable and have a super-large
+ * cache or simply have a collection with every MIME type in it that stays
+ * alive forever.
+ * This is that collection. It is initialized by |_loadMimeTypes|. As new
+ * MIME types are created, we add them to the collection.
+ */
+ _universalCollection: null,
+
+ /**
+ * Kick off a query of all the mime types in our database, leaving
+ * |_processMimeTypes| to actually do the legwork.
+ */
+ _loadMimeTypes() {
+ // get all the existing mime types!
+ let query = Gloda.newQuery(this.id);
+ let nullFunc = function () {};
+ this._universalCollection = query.getCollection(
+ {
+ onItemsAdded: nullFunc,
+ onItemsModified: nullFunc,
+ onItemsRemoved: nullFunc,
+ onQueryCompleted(aCollection) {
+ MimeTypeNoun._processMimeTypes(aCollection.items);
+ },
+ },
+ null
+ );
+ },
+
+ /**
+ * For the benefit of our Category queryHelper, we need dummy ranged objects
+ * that cover the numerical address space allocated to the category. We
+ * can't use a real object for the upper-bound because the upper-bound is
+ * constantly growing and there is the chance the query might get persisted,
+ * which means these values need to be long-lived. Unfortunately, our
+ * solution to this problem (dummy objects) complicates the second case,
+ * should it ever occur. (Because the dummy objects cannot be persisted
+ * on their own... but there are other issues that will come up that we will
+ * just have to deal with then.)
+ */
+ _createCategoryDummies(aId, aCategory) {
+ let blockBottom = aId - (aId % this.TYPE_BLOCK_SIZE);
+ let blockTop = blockBottom + this.TYPE_BLOCK_SIZE - 1;
+ this._mimeTypeRangeDummyObjects[aCategory] = [
+ new MimeType(
+ blockBottom,
+ "!category-dummy!",
+ aCategory,
+ "!category-dummy!/" + aCategory,
+ aCategory
+ ),
+ new MimeType(
+ blockTop,
+ "!category-dummy!",
+ aCategory,
+ "!category-dummy!/" + aCategory,
+ aCategory
+ ),
+ ];
+ },
+
+ _processMimeTypes(aMimeTypes) {
+ for (let mimeType of aMimeTypes) {
+ if (mimeType.id > this._highID) {
+ this._highID = mimeType.id;
+ }
+ this._mimeTypes[mimeType] = mimeType;
+ this._mimeTypesByID[mimeType.id] = mimeType;
+
+ let blockHighID =
+ mimeType.category in this._mimeTypeHighID
+ ? this._mimeTypeHighID[mimeType.category]
+ : undefined;
+ // create the dummy range objects
+ if (blockHighID === undefined) {
+ this._createCategoryDummies(mimeType.id, mimeType.category);
+ }
+ if (blockHighID === undefined || mimeType.id > blockHighID) {
+ this._mimeTypeHighID[mimeType.category] = mimeType.id;
+ }
+ }
+ },
+
+ _addNewMimeType(aMimeTypeName) {
+ let [typeName, subTypeName] = aMimeTypeName.split("/");
+ let category = this._getCategoryForMimeType(aMimeTypeName, typeName);
+
+ if (!(category in this._mimeTypeHighID)) {
+ let nextID =
+ this._highID -
+ (this._highID % this.TYPE_BLOCK_SIZE) +
+ this.TYPE_BLOCK_SIZE;
+ this._mimeTypeHighID[category] = nextID;
+ this._createCategoryDummies(nextID, category);
+ }
+
+ let nextID = ++this._mimeTypeHighID[category];
+
+ let mimeType = new MimeType(
+ nextID,
+ typeName,
+ subTypeName,
+ aMimeTypeName,
+ category
+ );
+ if (mimeType.id > this._highID) {
+ this._highID = mimeType.id;
+ }
+
+ this._mimeTypes[aMimeTypeName] = mimeType;
+ this._mimeTypesByID[nextID] = mimeType;
+
+ // As great as the gloda extension mechanisms are, we don't think it makes
+ // a lot of sense to use them in this case. So we directly trigger object
+ // insertion without any of the grokNounItem stuff.
+ this.objInsert.call(this.datastore, mimeType);
+ // Since we bypass grokNounItem and its fun, we need to explicitly add the
+ // new MIME-type to _universalCollection ourselves. Don't try this at
+ // home, kids.
+ this._universalCollection._onItemsAdded([mimeType]);
+
+ return mimeType;
+ },
+
+ /**
+ * Map a mime type to a |MimeType| instance, creating it if necessary.
+ *
+ * @param aMimeTypeName The mime type. It may optionally include parameters
+ * (which will be ignored). A mime type is of the form "type/subtype".
+ * A type with parameters would look like 'type/subtype; param="value"'.
+ */
+ getMimeType(aMimeTypeName) {
+ // first, lose any parameters
+ let semiIndex = aMimeTypeName.indexOf(";");
+ if (semiIndex >= 0) {
+ aMimeTypeName = aMimeTypeName.substring(0, semiIndex);
+ }
+ aMimeTypeName = aMimeTypeName.trim().toLowerCase();
+
+ if (aMimeTypeName in this._mimeTypes) {
+ return this._mimeTypes[aMimeTypeName];
+ }
+ return this._addNewMimeType(aMimeTypeName);
+ },
+
+ /**
+ * Query helpers contribute additional functions to the query object for the
+ * attributes that use the noun type. For example, we define Category, so
+ * for the "attachmentTypes" attribute, "attachmentTypesCategory" would be
+ * exposed.
+ */
+ queryHelpers: {
+ /**
+ * Query for MIME type categories based on one or more MIME type objects
+ * passed in. We want the range to span the entire block allocated to the
+ * category.
+ *
+ * @param aAttrDef The attribute that is using us.
+ * @param aArguments The actual arguments object that
+ */
+ Category(aAttrDef, aArguments) {
+ let rangePairs = [];
+ // If there are no arguments then we want to fall back to the 'in'
+ // constraint which matches on any attachment.
+ if (!aArguments || aArguments.length == 0) {
+ return this._inConstraintHelper(aAttrDef, []);
+ }
+
+ for (let iArg = 0; iArg < aArguments.length; iArg++) {
+ let arg = aArguments[iArg];
+ rangePairs.push(MimeTypeNoun._mimeTypeRangeDummyObjects[arg.category]);
+ }
+ return this._rangedConstraintHelper(aAttrDef, rangePairs);
+ },
+ },
+
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.fullType.localeCompare(b.fullType);
+ },
+
+ toParamAndValue(aMimeType) {
+ return [null, aMimeType.id];
+ },
+ toJSON(aMimeType) {
+ return aMimeType.id;
+ },
+ fromJSON(aMimeTypeID) {
+ return this._mimeTypesByID[aMimeTypeID];
+ },
+};
+Gloda.defineNoun(MimeTypeNoun, GlodaConstants.NOUN_MIME_TYPE);
+try {
+ MimeTypeNoun._init();
+} catch (ex) {
+ LOG.error(
+ "problem init-ing: " + ex.fileName + ":" + ex.lineNumber + ": " + ex
+ );
+}
diff --git a/comm/mailnews/db/gloda/modules/NounTag.jsm b/comm/mailnews/db/gloda/modules/NounTag.jsm
new file mode 100644
index 0000000000..1e5db85a42
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/NounTag.jsm
@@ -0,0 +1,97 @@
+/* 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 = ["TagNoun"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+const { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+
+/**
+ * @namespace Tag noun provider.
+ */
+var TagNoun = {
+ name: "tag",
+ clazz: Ci.nsIMsgTag,
+ usesParameter: true,
+ allowsArbitraryAttrs: false,
+ idAttr: "key",
+ _msgTagService: null,
+ _tagMap: null,
+ _tagList: null,
+
+ _init() {
+ // This reference can be substituted for testing purposes.
+ this._msgTagService = MailServices.tags;
+ this._updateTagMap();
+ },
+
+ getAllTags() {
+ if (this._tagList == null) {
+ this._updateTagMap();
+ }
+ return this._tagList;
+ },
+
+ _updateTagMap() {
+ this._tagMap = {};
+ let tagArray = (this._tagList = this._msgTagService.getAllTags());
+ for (let iTag = 0; iTag < tagArray.length; iTag++) {
+ let tag = tagArray[iTag];
+ this._tagMap[tag.key] = tag;
+ }
+ },
+
+ comparator(a, b) {
+ if (a == null) {
+ if (b == null) {
+ return 0;
+ }
+ return 1;
+ } else if (b == null) {
+ return -1;
+ }
+ return a.tag.localeCompare(b.tag);
+ },
+ userVisibleString(aTag) {
+ return aTag.tag;
+ },
+
+ // we cannot be an attribute value
+
+ toParamAndValue(aTag) {
+ return [aTag.key, null];
+ },
+ toJSON(aTag) {
+ return aTag.key;
+ },
+ fromJSON(aTagKey, aIgnored) {
+ let tag = this._tagMap.hasOwnProperty(aTagKey)
+ ? this._tagMap[aTagKey]
+ : undefined;
+ // you will note that if a tag is removed, we are unable to aggressively
+ // deal with this. we are okay with this, but it would be nice to be able
+ // to listen to the message tag service to know when we should rebuild.
+ if (tag === undefined && this._msgTagService.isValidKey(aTagKey)) {
+ this._updateTagMap();
+ tag = this._tagMap[aTagKey];
+ }
+ // we intentionally are returning undefined if the tag doesn't exist
+ return tag;
+ },
+ /**
+ * Convenience helper to turn a tag key into a tag name.
+ */
+ getTag(aTagKey) {
+ return this.fromJSON(aTagKey);
+ },
+};
+
+TagNoun._init();
+Gloda.defineNoun(TagNoun, GlodaConstants.NOUN_TAG);
diff --git a/comm/mailnews/db/gloda/modules/SuffixTree.jsm b/comm/mailnews/db/gloda/modules/SuffixTree.jsm
new file mode 100644
index 0000000000..239993e180
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/SuffixTree.jsm
@@ -0,0 +1,381 @@
+/* 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 = ["SuffixTree", "MultiSuffixTree"];
+
+/**
+ * Given a list of strings and a corresponding map of items that those strings
+ * correspond to, build a suffix tree.
+ */
+function MultiSuffixTree(aStrings, aItems) {
+ if (aStrings.length != aItems.length) {
+ throw new Error("Array lengths need to be the same.");
+ }
+
+ let s = "";
+ let offsetsToItems = [];
+ let lastLength = 0;
+ for (let i = 0; i < aStrings.length; i++) {
+ s += aStrings[i];
+ offsetsToItems.push(lastLength, s.length, aItems[i]);
+ lastLength = s.length;
+ }
+
+ this._construct(s);
+ this._offsetsToItems = offsetsToItems;
+ this._numItems = aItems.length;
+}
+
+/**
+ * @class
+ */
+function State(aStartIndex, aEndIndex, aSuffix) {
+ this.start = aStartIndex;
+ this.end = aEndIndex;
+ this.suffix = aSuffix;
+}
+
+/**
+ * Since objects are basically hash-tables anyways, we simply create an
+ * attribute whose name is the first letter of the edge string. (So, the
+ * edge string can conceptually be a multi-letter string, but since we would
+ * split it were there any ambiguity, it's okay to just use the single letter.)
+ * This avoids having to update the attribute name or worry about tripping our
+ * implementation up.
+ */
+State.prototype = {
+ get isExplicit() {
+ // our end is not inclusive...
+ return this.end <= this.start;
+ },
+ get isImplicit() {
+ // our end is not inclusive...
+ return this.end > this.start;
+ },
+
+ get length() {
+ return this.end - this.start;
+ },
+
+ toString() {
+ return (
+ "[Start: " +
+ this.start +
+ " End: " +
+ this.end +
+ (this.suffix ? " non-null suffix]" : " null suffix]")
+ );
+ },
+};
+
+/**
+ * Suffix tree implemented using Ukkonen's algorithm.
+ *
+ * @class
+ */
+function SuffixTree(aStr) {
+ this._construct(aStr);
+}
+
+/**
+ * States are
+ */
+SuffixTree.prototype = {
+ /**
+ * Find all items matching the provided substring.
+ */
+ findMatches(aSubstring) {
+ let results = [];
+ let state = this._root;
+ let index = 0;
+ let end = aSubstring.length;
+ while (index < end) {
+ state = state[aSubstring[index]];
+ // bail if there was no edge
+ if (state === undefined) {
+ return results;
+ }
+ // bail if the portion of the edge we traversed is not equal to that
+ // portion of our pattern
+ let actualTraverseLength = Math.min(state.length, end - index);
+ if (
+ this._str.substring(state.start, state.start + actualTraverseLength) !=
+ aSubstring.substring(index, index + actualTraverseLength)
+ ) {
+ return results;
+ }
+ index += state.length;
+ }
+
+ // state should now be the node which itself and all its children match...
+ // The delta is to adjust us to the offset of the last letter of our match;
+ // the edge we traversed to get here may have found us traversing more
+ // than we wanted.
+ // index - end captures the over-shoot of the edge traversal,
+ // index - end + 1 captures the fact that we want to find the last letter
+ // that matched, not just the first letter beyond it
+ // However, if this state is a leaf node (end == 'infinity'), then 'end'
+ // isn't describing an edge at all and we want to avoid accounting for it.
+ let delta;
+ /*
+ if (state.end != this._infinity)
+ //delta = index - end + 1;
+ delta = end - (index - state.length);
+ else */
+ delta = index - state.length - end + 1;
+
+ this._resultGather(state, results, {}, end, delta, true);
+ return results;
+ },
+
+ _resultGather(
+ aState,
+ aResults,
+ aPresence,
+ aPatLength,
+ aDelta,
+ alreadyAdjusted
+ ) {
+ // find the item that this state originated from based on the state's
+ // start character. offsetToItem holds [string start index, string end
+ // index (exclusive), item reference]. So we want to binary search to
+ // find the string whose start/end index contains the state's start index.
+ let low = 0;
+ let high = this._numItems - 1;
+ let mid, stringStart, stringEnd;
+
+ let patternLast = aState.start - aDelta;
+ while (low <= high) {
+ mid = low + Math.floor((high - low) / 2); // excessive, especially with js nums
+ stringStart = this._offsetsToItems[mid * 3];
+ let startDelta = stringStart - patternLast;
+ stringEnd = this._offsetsToItems[mid * 3 + 1];
+ let endDelta = stringEnd - patternLast;
+ if (startDelta > 0) {
+ high = mid - 1;
+ } else if (endDelta <= 0) {
+ low = mid + 1;
+ } else {
+ break;
+ }
+ }
+
+ // - The match occurred completely inside a source string. Success.
+ // - The match spans more than one source strings, and is therefore not
+ // a match.
+
+ // at this point, we have located the origin string that corresponds to the
+ // start index of this state.
+ // - The match terminated with the end of the preceding string, and does
+ // not match us at all. We, and potentially our children, are merely
+ // serving as a unique terminal.
+ // - The
+
+ let patternFirst = patternLast - (aPatLength - 1);
+
+ if (patternFirst >= stringStart) {
+ if (!(stringStart in aPresence)) {
+ aPresence[stringStart] = true;
+ aResults.push(this._offsetsToItems[mid * 3 + 2]);
+ }
+ }
+
+ // bail if we had it coming OR
+ // if the result terminates at/part-way through this state, meaning any
+ // of its children are not going to be actual results, just hangers
+ // on.
+ /*
+ if (bail || (end <= aState.end)) {
+dump(" bailing! (bail was: " + bail + ")\n");
+ return;
+ }
+*/
+ // process our children...
+ for (let key in aState) {
+ // edges have attributes of length 1...
+ if (key.length == 1) {
+ let statePrime = aState[key];
+ this._resultGather(
+ statePrime,
+ aResults,
+ aPresence,
+ aPatLength,
+ aDelta + aState.length, // (alreadyAdjusted ? 0 : aState.length),
+ false
+ );
+ }
+ }
+ },
+
+ /**
+ * Given a reference 'pair' of a state and a string (may be 'empty'=explicit,
+ * which means no work to do and we return immediately) follow that state
+ * (and then the successive states)'s transitions until we run out of
+ * transitions. This happens either when we find an explicit state, or
+ * find ourselves partially along an edge (conceptually speaking). In
+ * the partial case, we return the state prior to the edge traversal.
+ * (The information about the 'edge' is contained on its target State;
+ * we can do this because a state is only referenced by one other state.)
+ */
+ _canonize(aState, aStart, aEnd) {
+ if (aEnd <= aStart) {
+ return [aState, aStart];
+ }
+
+ let statePrime;
+ // we treat an aState of null as 'bottom', which has transitions for every
+ // letter in the alphabet to 'root'. rather than create all those
+ // transitions, we special-case here.
+ if (aState === null) {
+ statePrime = this._root;
+ } else {
+ statePrime = aState[this._str[aStart]];
+ }
+ while (statePrime.length <= aEnd - aStart) {
+ // (no 1 adjustment required)
+ aStart += statePrime.length;
+ aState = statePrime;
+ if (aStart < aEnd) {
+ statePrime = aState[this._str[aStart]];
+ }
+ }
+ return [aState, aStart];
+ },
+
+ /**
+ * Given a reference 'pair' whose state may or may not be explicit (and for
+ * which we will perform the required splitting to make it explicit), test
+ * whether it already possesses a transition corresponding to the provided
+ * character.
+ *
+ * @returns A list of: whether we had to make it explicit, the (potentially)
+ * new explicit state.
+ */
+ _testAndSplit(aState, aStart, aEnd, aChar) {
+ if (aStart < aEnd) {
+ // it's not explicit
+ let statePrime = aState[this._str[aStart]];
+ let length = aEnd - aStart;
+ if (aChar == this._str[statePrime.start + length]) {
+ return [true, aState];
+ }
+
+ // do splitting... aState -> rState -> statePrime
+ let rState = new State(statePrime.start, statePrime.start + length);
+ aState[this._str[statePrime.start]] = rState;
+ statePrime.start += length;
+ rState[this._str[statePrime.start]] = statePrime;
+ return [false, rState];
+ }
+
+ // it's already explicit
+ if (aState === null) {
+ // bottom case... shouldn't happen, but hey.
+ return [true, aState];
+ }
+ return [aChar in aState, aState];
+ },
+
+ _update(aState, aStart, aIndex) {
+ let oldR = this._root;
+ let textAtIndex = this._str[aIndex]; // T sub i (0-based corrected...)
+ // because of the way we store the 'end' value as a one-past form, we do
+ // not need to subtract 1 off of aIndex.
+ let [endPoint, rState] = this._testAndSplit(
+ aState,
+ aStart,
+ aIndex, // no -1
+ textAtIndex
+ );
+ while (!endPoint) {
+ let rPrime = new State(aIndex, this._infinity);
+ rState[textAtIndex] = rPrime;
+ if (oldR !== this._root) {
+ oldR.suffix = rState;
+ }
+ oldR = rState;
+ [aState, aStart] = this._canonize(aState.suffix, aStart, aIndex); // no -1
+ [endPoint, rState] = this._testAndSplit(
+ aState,
+ aStart,
+ aIndex, // no -1
+ textAtIndex
+ );
+ }
+ if (oldR !== this._root) {
+ oldR.suffix = aState;
+ }
+
+ return [aState, aStart];
+ },
+
+ _construct(aStr) {
+ this._str = aStr;
+ // just needs to be longer than the string.
+ this._infinity = aStr.length + 1;
+
+ // this._bottom = new State(0, -1, null);
+ this._root = new State(-1, 0, null); // null === bottom
+ let state = this._root;
+ let start = 0;
+
+ for (let i = 0; i < aStr.length; i++) {
+ [state, start] = this._update(state, start, i); // treat as flowing -1...
+ [state, start] = this._canonize(state, start, i + 1); // 1-length string
+ }
+ },
+
+ dump(aState, aIndent, aKey) {
+ if (aState === undefined) {
+ aState = this._root;
+ }
+ if (aIndent === undefined) {
+ aIndent = "";
+ aKey = ".";
+ }
+
+ if (aState.isImplicit) {
+ let snip;
+ if (aState.length > 10) {
+ snip =
+ this._str.slice(
+ aState.start,
+ Math.min(aState.start + 10, this._str.length)
+ ) + "...";
+ } else {
+ snip = this._str.slice(
+ aState.start,
+ Math.min(aState.end, this._str.length)
+ );
+ }
+ dump(
+ aIndent +
+ aKey +
+ ":" +
+ snip +
+ "(" +
+ aState.start +
+ ":" +
+ aState.end +
+ ")\n"
+ );
+ } else {
+ dump(
+ aIndent +
+ aKey +
+ ": (explicit:" +
+ aState.start +
+ ":" +
+ aState.end +
+ ")\n"
+ );
+ }
+ let nextIndent = aIndent + " ";
+ let keys = Object.keys(aState).filter(c => c.length == 1);
+ for (let key of keys) {
+ this.dump(aState[key], nextIndent, key);
+ }
+ },
+};
+MultiSuffixTree.prototype = SuffixTree.prototype;
diff --git a/comm/mailnews/db/gloda/modules/moz.build b/comm/mailnews/db/gloda/modules/moz.build
new file mode 100644
index 0000000000..54978c24ea
--- /dev/null
+++ b/comm/mailnews/db/gloda/modules/moz.build
@@ -0,0 +1,31 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES.gloda += [
+ "Collection.jsm",
+ "Everybody.jsm",
+ "Facet.jsm",
+ "Gloda.jsm",
+ "GlodaConstants.jsm",
+ "GlodaContent.jsm",
+ "GlodaDatabind.jsm",
+ "GlodaDataModel.jsm",
+ "GlodaDatastore.jsm",
+ "GlodaExplicitAttr.jsm",
+ "GlodaFundAttr.jsm",
+ "GlodaIndexer.jsm",
+ "GlodaMsgIndexer.jsm",
+ "GlodaMsgSearcher.jsm",
+ "GlodaPublic.jsm",
+ "GlodaQueryClassFactory.jsm",
+ "GlodaSyntheticView.jsm",
+ "GlodaUtils.jsm",
+ "IndexMsg.jsm",
+ "MimeMessage.jsm",
+ "NounFreetag.jsm",
+ "NounMimetype.jsm",
+ "NounTag.jsm",
+ "SuffixTree.jsm",
+]
diff --git a/comm/mailnews/db/gloda/moz.build b/comm/mailnews/db/gloda/moz.build
new file mode 100644
index 0000000000..4c7d35cca3
--- /dev/null
+++ b/comm/mailnews/db/gloda/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "modules",
+ "components",
+]
+
+TEST_DIRS += ["test"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/mailnews/db/gloda/test/moz.build b/comm/mailnews/db/gloda/test/moz.build
new file mode 100644
index 0000000000..c16fdd2b6c
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
+
+TESTING_JS_MODULES.gloda += [
+ "unit/resources/GlodaQueryHelper.jsm",
+ "unit/resources/GlodaTestHelper.jsm",
+ "unit/resources/GlodaTestHelperFunctions.jsm",
+]
diff --git a/comm/mailnews/db/gloda/test/unit/base_gloda_content.js b/comm/mailnews/db/gloda/test/unit/base_gloda_content.js
new file mode 100644
index 0000000000..d106015b48
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/base_gloda_content.js
@@ -0,0 +1,226 @@
+/* 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/. */
+
+/*
+ * Tests the operation of the GlodaContent (in GlodaContent.jsm) and its exposure
+ * via Gloda.getMessageContent. This may also be implicitly tested by indexing
+ * and fulltext query tests (on messages), but the buck stops here for the
+ * content stuff.
+ *
+ * Currently, we just test quoting removal and that the content turns out right.
+ * We do not actually verify that the quoted blocks are correct (aka we might
+ * screw up eating the greater-than signs). (We have no known consumers who
+ * care about the quoted blocks.)
+ */
+
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { assertExpectedMessagesIndexed, waitForGlodaIndexer } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+// We need to be able to get at GlodaFundAttr to check the number of whittler
+// invocations.
+var { GlodaFundAttr } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaFundAttr.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var { SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* ===== Data ===== */
+var messageInfos = [
+ {
+ name: "no quoting",
+ bode: [
+ [true, "I like hats"],
+ [true, "yes I do!"],
+ [true, "I like hats!"],
+ [true, "How bout you?"],
+ ],
+ },
+ {
+ name: "no quoting, whitespace removal",
+ bode: [
+ [true, "robots are nice..."],
+ [true, ""],
+ [true, "except for the bloodlust"],
+ ],
+ },
+ {
+ name: "bottom posting",
+ bode: [
+ [false, "John wrote:"],
+ [false, "> I like hats"],
+ [false, ">"], // This quoted blank line is significant! no lose!
+ [false, "> yes I do!"],
+ [false, ""],
+ [true, "I do enjoy them as well."],
+ [true, ""],
+ [true, "Bob"],
+ ],
+ },
+ {
+ name: "top posting",
+ bode: [
+ [true, "Hats are where it's at."],
+ [false, ""],
+ [false, "John wrote:"],
+ [false, "> I like hats"],
+ [false, "> yes I do!"],
+ ],
+ },
+ {
+ name: "top posting with trailing whitespace, no intro",
+ bode: [
+ [true, "Hats are where it's at."],
+ [false, ""],
+ [false, "> I like hats"],
+ [false, "> yes I do!"],
+ [false, ""],
+ [false, ""],
+ ],
+ },
+ {
+ name: "interspersed quoting",
+ bode: [
+ [false, "John wrote:"],
+ [false, "> I like hats"],
+ [true, "I concur with this point."],
+ [false, "> yes I do!"],
+ [false, ""],
+ [true, "this point also resonates with me."],
+ [false, ""],
+ [false, "> I like hats!"],
+ [false, "> How bout you?"],
+ [false, ""],
+ [true, "Verily!"],
+ ],
+ },
+ {
+ name: "german style",
+ bode: [
+ [false, "Mark Banner <bugzilla@standard8.plus.invalid> wrote:"],
+ [false, "\xa0"],
+ [
+ false,
+ "> We haven't nailed anything down in detail yet, depending on how we are ",
+ ],
+ [
+ true,
+ "That sounds great and would definitely be appreciated by localizers.",
+ ],
+ [false, ""],
+ ],
+ },
+ {
+ name: "tortuous interference",
+ bode: [
+ [false, "> wrote"],
+ [true, "running all the time"],
+ [false, "> wrote"],
+ [true, "cheese"],
+ [false, ""],
+ ],
+ },
+];
+
+function setup_create_message(info) {
+ info.body = { body: info.bode.map(tupe => tupe[1]).join("\r\n") };
+ info.expected = info.bode
+ .filter(tupe => tupe[0])
+ .map(tupe => tupe[1])
+ .join("\n");
+
+ info._synMsg = msgGen.makeMessage(info);
+}
+
+/**
+ * To save ourselves some lookup trouble, pretend to be a verification
+ * function so we get easy access to the gloda translations of the messages so
+ * we can cram this in various places.
+ */
+function glodaInfoStasher(aSynthMessage, aGlodaMessage) {
+ // Let's not assume an ordering.
+ for (let iMsg = 0; iMsg < messageInfos.length; iMsg++) {
+ if (messageInfos[iMsg]._synMsg == aSynthMessage) {
+ messageInfos[iMsg]._glodaMsg = aGlodaMessage;
+ }
+ }
+}
+
+/**
+ * Actually inject all the messages we created above.
+ */
+async function setup_inject_messages() {
+ // Create the messages from messageInfo.
+ messageInfos.forEach(info => {
+ setup_create_message(info);
+ });
+ let msgSet = new SyntheticMessageSet(messageInfos.map(info => info._synMsg));
+ let folder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([folder], [msgSet]);
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet], { verifier: glodaInfoStasher })
+ );
+}
+
+function test_stream_message(info) {
+ // Currying the function for simpler usage with `base_gloda_content_tests`.
+ return () => {
+ let msgHdr = info._glodaMsg.folderMessage;
+
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ verify_message_content(
+ info,
+ info._synMsg,
+ info._glodaMsg,
+ aMsgHdr,
+ aMimeMsg
+ );
+ });
+ };
+}
+
+// Instrument GlodaFundAttr so we can check the count.
+var originalWhittler = GlodaFundAttr.contentWhittle;
+var whittleCount = 0;
+GlodaFundAttr.contentWhittle = function (...aArgs) {
+ whittleCount++;
+ return originalWhittler.apply(this, aArgs);
+};
+
+function verify_message_content(aInfo, aSynMsg, aGlodaMsg, aMsgHdr, aMimeMsg) {
+ if (aMimeMsg == null) {
+ throw new Error(
+ "Message streaming should work; check test_mime_emitter.js first"
+ );
+ }
+
+ whittleCount = 0;
+ let content = Gloda.getMessageContent(aGlodaMsg, aMimeMsg);
+ if (whittleCount != 1) {
+ throw new Error("Whittle count is " + whittleCount + " but should be 1!");
+ }
+
+ Assert.equal(content.getContentString(), aInfo.expected, "Message streamed");
+}
+
+function test_sanity_test_environment() {
+ Assert.ok(msgGen, "Sanity that msgGen is set.");
+ Assert.ok(messageInjection, "Sanity that messageInjection is set.");
+}
+
+var base_gloda_content_tests = [
+ test_sanity_test_environment,
+ setup_inject_messages,
+ ...messageInfos.map(e => {
+ return test_stream_message(e);
+ }),
+];
diff --git a/comm/mailnews/db/gloda/test/unit/base_index_junk.js b/comm/mailnews/db/gloda/test/unit/base_index_junk.js
new file mode 100644
index 0000000000..8529f24a56
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/base_index_junk.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+/*
+ * Test indexing in the face of junk classification and junk folders. It is
+ * gloda policy not to index junk mail.
+ *
+ * A similar test that moving things to the trash folder is deletion happens in
+ * base_index_messages.js.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { assertExpectedMessagesIndexed, waitForGlodaIndexer } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var messageInjection;
+
+const SPAM_BODY = { body: "superspam superspam superspam eevil eevil eevil" };
+const HAM_BODY = { body: "ham ham ham nice nice nice happy happy happy" };
+
+/**
+ * Make SPAM_BODY be known as spammy and HAM_BODY be known as hammy.
+ */
+async function setup_spam_filter() {
+ let [, spamSet, hamSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, body: SPAM_BODY },
+ { count: 1, body: HAM_BODY },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([spamSet, hamSet], []));
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ let junkListener = {
+ onMessageClassified() {
+ promiseResolve();
+ },
+ };
+
+ // Ham.
+ dump(`Marking message: ${hamSet.getMsgHdr(0)} as ham.`);
+ MailServices.junk.setMessageClassification(
+ hamSet.getMsgURI(0),
+ null, // no old classification
+ MailServices.junk.GOOD,
+ null,
+ junkListener
+ );
+ await promise;
+
+ // Reset promise for junkListener.
+ promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ // Spam.
+ dump(`Marking message: ${spamSet.getMsgHdr(0)} as spam.`);
+ MailServices.junk.setMessageClassification(
+ spamSet.getMsgURI(0),
+ null, // No old classification.
+ MailServices.junk.JUNK,
+ null,
+ junkListener
+ );
+ await promise;
+}
+
+/**
+ * Because gloda defers indexing until after junk, we should never index a
+ * message that gets marked as junk. So if we inject a message that will
+ * definitely be marked as junk (thanks to use of terms that guarantee it),
+ * the indexer should never index it.
+ *
+ * ONLY THIS TEST ACTUALLY RELIES ON THE BAYESIAN CLASSIFIER.
+ */
+async function test_never_indexes_a_message_marked_as_junk() {
+ // Event-driven does not index junk.
+
+ // Make a message that will be marked as junk from the get-go.
+ await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, body: SPAM_BODY },
+ ]);
+ // Since the message is junk, gloda should not index it!
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // Folder sweep does not index junk.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+}
+
+/**
+ * Reset the training data so the bayesian classifier stops doing things.
+ */
+function reset_spam_filter() {
+ MailServices.junk.resetTrainingData();
+}
+
+/**
+ * Marking a message as junk is equivalent to deleting the message, un-mark it
+ * and it should go back to being a happy message (with the same gloda-id!).
+ *
+ * THIS TEST DOES NOT RELY ON THE BAYESIAN CLASSIFIER.
+ */
+
+async function test_mark_as_junk_is_deletion_mark_as_not_junk_is_exposure() {
+ // Mark as junk is deletion.
+ // Create a message; it should get indexed.
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let glodaId = msgSet.glodaMessages[0].id;
+ // Mark it as junk.
+ msgSet.setJunk(true);
+ // It will appear deleted after the event.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] }));
+ // Mark as non-junk gets indexed.
+ msgSet.setJunk(false);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ // We should have reused the existing gloda message so it should keep the id.
+ Assert.equal(glodaId, msgSet.glodaMessages[0].id);
+}
+
+/**
+ * Moving a message to the junk folder is equivalent to deletion. Gloda does
+ * not index junk folders at all, which is why this is an important and
+ * independent determination from marking a message directly as junk.
+ *
+ * The move to the junk folder is performed without using any explicit junk
+ * support code. This ends up being effectively the same underlying logic test
+ * as base_index_messages' test of moving a message to the trash folder.
+ */
+async function test_message_moving_to_junk_folder_is_deletion() {
+ // Create and index two messages in a conversation.
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 2, msgsPerThread: 2 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ let convId = msgSet.glodaMessages[0].conversation.id;
+ let firstGlodaId = msgSet.glodaMessages[0].id;
+ let secondGlodaId = msgSet.glodaMessages[1].id;
+
+ // Move them to the junk folder.
+ await messageInjection.moveMessages(
+ msgSet,
+ await messageInjection.getJunkFolder()
+ );
+
+ // They will appear deleted after the events.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] }));
+
+ // We do not index the junk folder so this should actually make them appear
+ // deleted to an unprivileged query.
+ let msgQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ msgQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgQuery, []);
+
+ // Force a sweep.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ // There should be no apparent change as the result of this pass.
+ // (Well, the conversation will die, but we can't see that.)
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // The conversation should be gone.
+ let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION);
+ convQuery.id(convId);
+ await queryExpect(convQuery, []);
+
+ // The messages should be entirely gone.
+ let msgPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ msgPrivQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgPrivQuery, []);
+}
+
+function test_sanity_test_environment() {
+ Assert.ok(messageInjection, "Sanity that messageInjection is set.");
+ Assert.ok(messageInjection.messageGenerator, "Sanity that msgGen is set.");
+}
+
+/* exported tests */
+var base_index_junk_tests = [
+ test_sanity_test_environment,
+ setup_spam_filter,
+ test_never_indexes_a_message_marked_as_junk,
+ reset_spam_filter,
+ test_mark_as_junk_is_deletion_mark_as_not_junk_is_exposure,
+ test_message_moving_to_junk_folder_is_deletion,
+];
diff --git a/comm/mailnews/db/gloda/test/unit/base_index_messages.js b/comm/mailnews/db/gloda/test/unit/base_index_messages.js
new file mode 100644
index 0000000000..bea2337d7f
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/base_index_messages.js
@@ -0,0 +1,1461 @@
+/* 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/. */
+
+/*
+ * This file tests our indexing prowess. This includes both our ability to
+ * properly be triggered by events taking place in thunderbird as well as our
+ * ability to correctly extract/index the right data.
+ * In general, if these tests pass, things are probably working quite well.
+ *
+ * This test has local, IMAP online, IMAP offline, and IMAP online-become-offline
+ * variants. See the text_index_messages_*.js files.
+ *
+ * Things we don't test that you think we might test:
+ * - Full-text search. Happens in query testing.
+ */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+var { queryExpect, sqlExpectCount } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var {
+ assertExpectedMessagesIndexed,
+ waitForGlodaIndexer,
+ nukeGlodaCachesAndCollections,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var {
+ configureGlodaIndexing,
+ waitForGlodaDBFlush,
+ waitForIndexingHang,
+ resumeFromSimulatedHang,
+ permuteMessages,
+ makeABCardForAddressPair,
+} = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { SyntheticMessageSet, SyntheticPartMultiMixed, SyntheticPartLeaf } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+var { TagNoun } = ChromeUtils.import("resource:///modules/gloda/NounTag.jsm");
+
+// Whether we can expect fulltext results
+var expectFulltextResults = true;
+
+/**
+ * Should we force our folders offline after we have indexed them once. We do
+ * this in the online_to_offline test variant.
+ */
+var goOffline = false;
+
+var messageInjection;
+var msgGen;
+var scenarios;
+
+/* ===== Indexing Basics ===== */
+
+/**
+ * Index a message, wait for a commit, make sure the header gets the property
+ * set correctly. Then modify the message, verify the dirty property shows
+ * up, flush again, and make sure the dirty property goes clean again.
+ */
+async function test_pending_commit_tracker_flushes_correctly() {
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ // Before the flush, there should be no gloda-id property.
+ let msgHdr = msgSet.getMsgHdr(0);
+ // Get it as a string to make sure it's empty rather than possessing a value.
+ Assert.equal(msgHdr.getStringProperty("gloda-id"), "");
+
+ await waitForGlodaDBFlush();
+
+ // After the flush there should be a gloda-id property and it should
+ // equal the gloda id.
+ let gmsg = msgSet.glodaMessages[0];
+ Assert.equal(msgHdr.getUint32Property("gloda-id"), gmsg.id);
+
+ // Make sure no dirty property was written.
+ Assert.equal(msgHdr.getStringProperty("gloda-dirty"), "");
+
+ // Modify the message.
+ msgSet.setRead(true);
+ await waitForGlodaIndexer(msgSet);
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+
+ // Now there should be a dirty property and it should be 1.
+ Assert.equal(
+ msgHdr.getUint32Property("gloda-dirty"),
+ GlodaMsgIndexer.kMessageDirty
+ );
+
+ // Flush.
+ await waitForGlodaDBFlush();
+
+ // Now dirty should be 0 and the gloda id should still be the same.
+ Assert.equal(
+ msgHdr.getUint32Property("gloda-dirty"),
+ GlodaMsgIndexer.kMessageClean
+ );
+ Assert.equal(msgHdr.getUint32Property("gloda-id"), gmsg.id);
+}
+
+/**
+ * Make sure that PendingCommitTracker causes a msgdb commit to occur so that
+ * if the nsIMsgFolder's msgDatabase attribute has already been nulled
+ * (which is normally how we force a msgdb commit), that the changes to the
+ * header actually hit the disk.
+ */
+async function test_pending_commit_causes_msgdb_commit() {
+ // New message, index it.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ // Force the msgDatabase closed; the sqlite commit will not yet have occurred.
+ messageInjection.getRealInjectionFolder(folder).msgDatabase = null;
+ // Make the commit happen, this causes the header to get set.
+ await waitForGlodaDBFlush();
+
+ // Force a GC. This will kill off the header and the database, losing data
+ // if we are not protecting it.
+ Cu.forceGC();
+
+ // Now retrieve the header and make sure it has the gloda id set!
+ let msgHdr = msgSet.getMsgHdr(0);
+ Assert.equal(
+ msgHdr.getUint32Property("gloda-id"),
+ msgSet.glodaMessages[0].id
+ );
+}
+
+/**
+ * Give the indexing sweep a workout.
+ *
+ * This includes:
+ * - Basic indexing sweep across never-before-indexed folders.
+ * - Indexing sweep across folders with just some changes.
+ * - Filthy pass.
+ */
+async function test_indexing_sweep() {
+ // -- Never-before-indexed folders.
+ // Turn off event-driven indexing.
+ configureGlodaIndexing({ event: false });
+
+ let [[folderA], setA1, setA2] = await messageInjection.makeFoldersWithSets(
+ 1,
+ [{ count: 3 }, { count: 2 }]
+ );
+ let [, setB1, setB2] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 3 },
+ { count: 2 },
+ ]);
+ let [[folderC], setC1, setC2] = await messageInjection.makeFoldersWithSets(
+ 1,
+ [{ count: 3 }, { count: 2 }]
+ );
+
+ // Make sure that event-driven job gets nuked out of existence
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+
+ // Turn on event-driven indexing again; this will trigger a sweep.
+ configureGlodaIndexing({ event: true });
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([setA1, setA2, setB1, setB2, setC1, setC2])
+ );
+
+ // -- Folders with some changes, pending commits.
+ // Indexing off.
+ configureGlodaIndexing({ event: false });
+
+ setA1.setRead(true);
+ setB2.setRead(true);
+
+ // Indexing on, killing all outstanding jobs, trigger sweep.
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+ configureGlodaIndexing({ event: true });
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setA1, setB2]));
+
+ // -- Folders with some changes, no pending commits.
+ // Force a commit to clear out our pending commits.
+ await waitForGlodaDBFlush();
+ // Indexing off.
+ configureGlodaIndexing({ event: false });
+
+ setA2.setRead(true);
+ setB1.setRead(true);
+
+ // Indexing on, killing all outstanding jobs, trigger sweep.
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+ configureGlodaIndexing({ event: true });
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setA2, setB1]));
+
+ // -- Filthy foldering indexing.
+ // Just mark the folder filthy and make sure that we reindex everyone.
+ // IMPORTANT! The trick of marking the folder filthy only works because
+ // we flushed/committed the database above; the PendingCommitTracker
+ // is not aware of bogus filthy-marking of folders.
+ // We leave the verification of the implementation details to
+ // test_index_sweep_folder.js.
+ let glodaFolderC = Gloda.getFolderForFolder(
+ messageInjection.getRealInjectionFolder(folderC)
+ );
+ // Marked gloda folder dirty.
+ glodaFolderC._dirtyStatus = glodaFolderC.kFolderFilthy;
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setC1, setC2]));
+
+ // -- Forced folder indexing.
+ var callbackInvoked = false;
+ GlodaMsgIndexer.indexFolder(
+ messageInjection.getRealInjectionFolder(folderA),
+ {
+ force: true,
+ callback() {
+ callbackInvoked = true;
+ },
+ }
+ );
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setA1, setA2]));
+ Assert.ok(callbackInvoked);
+}
+
+/**
+ * We used to screw up and downgrade filthy folders to dirty if we saw an event
+ * happen in the folder before we got to the folder; this tests that we no
+ * longer do that.
+ */
+async function test_event_driven_indexing_does_not_mess_with_filthy_folders() {
+ // Add a folder with a message.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+
+ // Fake marking the folder filthy.
+ let glodaFolder = Gloda.getFolderForFolder(
+ messageInjection.getRealInjectionFolder(folder)
+ );
+ glodaFolder._dirtyStatus = glodaFolder.kFolderFilthy;
+
+ // Generate an event in the folder.
+ msgSet.setRead(true);
+ // Make sure the indexer did not do anything and the folder is still filthy.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+ Assert.equal(glodaFolder._dirtyStatus, glodaFolder.kFolderFilthy);
+ // Also, the message should not have actually gotten marked dirty.
+ Assert.equal(msgSet.getMsgHdr(0).getUint32Property("gloda-dirty"), 0);
+
+ // Let's make the message un-read again for consistency with the gloda state.
+ msgSet.setRead(false);
+ // Make the folder dirty and let an indexing sweep take care of this so we
+ // don't get extra events in subsequent tests.
+ glodaFolder._dirtyStatus = glodaFolder.kFolderDirty;
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ // The message won't get indexed though.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+}
+
+async function test_indexing_never_priority() {
+ // Add a folder with a bunch of messages.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ // Index it, and augment the msgSet with the glodaMessages array
+ // for later use by sqlExpectCount.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ // Explicitly tell gloda to never index this folder.
+ let XPCOMFolder = messageInjection.getRealInjectionFolder(folder);
+ let glodaFolder = Gloda.getFolderForFolder(XPCOMFolder);
+ GlodaMsgIndexer.setFolderIndexingPriority(
+ XPCOMFolder,
+ glodaFolder.kIndexingNeverPriority
+ );
+
+ // Verify that the setter and getter do the right thing.
+ Assert.equal(
+ glodaFolder.indexingPriority,
+ glodaFolder.kIndexingNeverPriority
+ );
+
+ // Check that existing message is marked as deleted.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] }));
+
+ // Make sure the deletion hit the database.
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?",
+ glodaFolder.id,
+ glodaFolder.kIndexingNeverPriority
+ );
+
+ // Add another message.
+ await messageInjection.makeNewSetsInFolders([folder], [{ count: 1 }]);
+
+ // Make sure that indexing returns nothing.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+}
+
+async function test_setting_indexing_priority_never_while_indexing() {
+ if (!messageInjection.messageInjectionIsLocal()) {
+ return;
+ }
+
+ // Configure the gloda indexer to hang while streaming the message.
+ configureGlodaIndexing({ hangWhile: "streaming" });
+
+ // Create a folder with a message inside.
+ let [[folder]] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ await waitForIndexingHang();
+
+ // Explicitly tell gloda to never index this folder.
+ let XPCOMFolder = messageInjection.getRealInjectionFolder(folder);
+ let glodaFolder = Gloda.getFolderForFolder(XPCOMFolder);
+ GlodaMsgIndexer.setFolderIndexingPriority(
+ XPCOMFolder,
+ glodaFolder.kIndexingNeverPriority
+ );
+
+ // Reset indexing to not hang.
+ configureGlodaIndexing({});
+
+ // Sorta get the event chain going again.
+ await resumeFromSimulatedHang(true);
+
+ // Because the folder was dirty it should actually end up getting indexed,
+ // so in the end the message will get indexed. Also, make sure a cleanup
+ // was observed.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { cleanedUp: 1 }));
+}
+
+/* ===== Threading / Conversation Grouping ===== */
+
+var gSynMessages = [];
+function allMessageInSameConversation(aSynthMessage, aGlodaMessage, aConvID) {
+ if (aConvID === undefined) {
+ return aGlodaMessage.conversationID;
+ }
+ Assert.equal(aConvID, aGlodaMessage.conversationID);
+ // Cheat and stash the synthetic message (we need them for one of the IMAP
+ // tests).
+ gSynMessages.push(aSynthMessage);
+ return aConvID;
+}
+
+/**
+ * Test our conversation/threading logic in the straight-forward direct
+ * reply case, the missing intermediary case, and the siblings with missing
+ * parent case. We also test all permutations of receipt of those messages.
+ * (Also tests that we index new messages.)
+ */
+async function test_threading_direct_reply() {
+ let permutationMessages = await permuteMessages(
+ scenarios.directReply,
+ messageInjection
+ );
+ for (const preparedMessage of permutationMessages) {
+ let message = await preparedMessage();
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([message], allMessageInSameConversation)
+ );
+ }
+}
+
+async function test_threading_missing_intermediary() {
+ let permutationMessages = await permuteMessages(
+ scenarios.missingIntermediary,
+ messageInjection
+ );
+ for (const preparedMessage of permutationMessages) {
+ let message = await preparedMessage();
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([message], allMessageInSameConversation)
+ );
+ }
+}
+async function test_threading_siblings_missing_parent() {
+ let permutationMessages = await permuteMessages(
+ scenarios.siblingsMissingParent,
+ messageInjection
+ );
+ for (const preparedMessage of permutationMessages) {
+ let message = await preparedMessage();
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([message], allMessageInSameConversation)
+ );
+ }
+}
+
+/**
+ * Test the bit that says "if we're fulltext-indexing the message and we
+ * discover it didn't have any attachments, clear the attachment bit from the
+ * message header".
+ */
+async function test_attachment_flag() {
+ // Create a synthetic message with an attachment that won't normally be listed
+ // in the attachment pane (Content-Disposition: inline, no filename, and
+ // displayable inline).
+ let smsg = msgGen.makeMessage({
+ name: "test message with part 1.2 attachment",
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ format: "",
+ },
+ ],
+ });
+ // Save it off for test_attributes_fundamental_from_disk.
+ let msgSet = new SyntheticMessageSet([smsg]);
+ let folder = (fundamentalFolderHandle =
+ await messageInjection.makeEmptyFolder());
+ await messageInjection.addSetsToFolders([folder], [msgSet]);
+
+ // If we need to go offline, let the indexing pass run, then force us offline.
+ if (goOffline) {
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ await messageInjection.makeFolderAndContentsOffline(folder);
+ // Now the next indexer wait will wait for the next indexing pass.
+ }
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet], {
+ verifier: verify_attachment_flag,
+ })
+ );
+}
+
+function verify_attachment_flag(smsg, gmsg) {
+ // -- Attachments. We won't have these if we don't have fulltext results.
+ if (expectFulltextResults) {
+ Assert.equal(gmsg.attachmentNames.length, 0);
+ Assert.equal(gmsg.attachmentInfos.length, 0);
+ Assert.equal(
+ false,
+ gmsg.folderMessage.flags & Ci.nsMsgMessageFlags.Attachment
+ );
+ }
+}
+/* ===== Fundamental Attributes (per GlodaFundAttr.jsm) ===== */
+
+/**
+ * Save the synthetic message created in test_attributes_fundamental for the
+ * benefit of test_attributes_fundamental_from_disk.
+ */
+var fundamentalSyntheticMessage;
+var fundamentalFolderHandle;
+/**
+ * We're saving this one so that we can move the message later and verify that
+ * the attributes are consistent.
+ */
+var fundamentalMsgSet;
+var fundamentalGlodaMsgAttachmentUrls;
+/**
+ * Save the resulting gloda message id corresponding to the
+ * fundamentalSyntheticMessage so we can use it to query the message from disk.
+ */
+var fundamentalGlodaMessageId;
+
+/**
+ * Test that we extract the 'fundamental attributes' of a message properly
+ * 'Fundamental' in this case is talking about the attributes defined/extracted
+ * by gloda's GlodaFundAttr.jsm and perhaps the core message indexing logic itself
+ * (which show up as kSpecial* attributes in GlodaFundAttr.jsm anyways.)
+ */
+async function test_attributes_fundamental() {
+ // Create a synthetic message with attachment.
+ let smsg = msgGen.makeMessage({
+ name: "test message",
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartLeaf({ body: "I like cheese!" }),
+ msgGen.makeMessage({ body: { body: "I like wine!" } }), // That's one attachment.
+ ]),
+ attachments: [
+ { filename: "bob.txt", body: "I like bread!" }, // And that's another one.
+ ],
+ });
+ // Save it off for test_attributes_fundamental_from_disk.
+ fundamentalSyntheticMessage = smsg;
+ let msgSet = new SyntheticMessageSet([smsg]);
+ fundamentalMsgSet = msgSet;
+ let folder = (fundamentalFolderHandle =
+ await messageInjection.makeEmptyFolder());
+ await messageInjection.addSetsToFolders([folder], [msgSet]);
+
+ // If we need to go offline, let the indexing pass run, then force us offline.
+ if (goOffline) {
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ await messageInjection.makeFolderAndContentsOffline(folder);
+ // Now the next indexer wait will wait for the next indexing pass.
+ }
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet], {
+ verifier: verify_attributes_fundamental,
+ })
+ );
+}
+
+function verify_attributes_fundamental(smsg, gmsg) {
+ // Save off the message id for test_attributes_fundamental_from_disk.
+ fundamentalGlodaMessageId = gmsg.id;
+ if (gmsg.attachmentInfos) {
+ fundamentalGlodaMsgAttachmentUrls = gmsg.attachmentInfos.map(
+ att => att.url
+ );
+ } else {
+ fundamentalGlodaMsgAttachmentUrls = [];
+ }
+
+ Assert.equal(
+ gmsg.folderURI,
+ messageInjection.getRealInjectionFolder(fundamentalFolderHandle).URI
+ );
+
+ // -- Subject
+ Assert.equal(smsg.subject, gmsg.conversation.subject);
+ Assert.equal(smsg.subject, gmsg.subject);
+
+ // -- Contact/identity information.
+ // - From
+ // Check the e-mail address.
+ Assert.equal(gmsg.from.kind, "email");
+ Assert.equal(smsg.fromAddress, gmsg.from.value);
+ // Check the name.
+ Assert.equal(smsg.fromName, gmsg.from.contact.name);
+
+ // - To
+ Assert.equal(smsg.toAddress, gmsg.to[0].value);
+ Assert.equal(smsg.toName, gmsg.to[0].contact.name);
+
+ // Date
+ Assert.equal(smsg.date.valueOf(), gmsg.date.valueOf());
+
+ // -- Message ID
+ Assert.equal(smsg.messageId, gmsg.headerMessageID);
+
+ // -- Attachments. We won't have these if we don't have fulltext results.
+ if (expectFulltextResults) {
+ Assert.equal(gmsg.attachmentTypes.length, 1);
+ Assert.equal(gmsg.attachmentTypes[0], "text/plain");
+ Assert.equal(gmsg.attachmentNames.length, 1);
+ Assert.equal(gmsg.attachmentNames[0], "bob.txt");
+
+ let expectedInfos = [
+ // The name for that one is generated randomly.
+ { contentType: "message/rfc822" },
+ { name: "bob.txt", contentType: "text/plain" },
+ ];
+ let expectedSize = 14;
+ Assert.equal(gmsg.attachmentInfos.length, 2);
+ for (let [i, attInfos] of gmsg.attachmentInfos.entries()) {
+ for (let k in expectedInfos[i]) {
+ Assert.equal(attInfos[k], expectedInfos[i][k]);
+ }
+ // Because it's unreliable and depends on the platform.
+ Assert.ok(Math.abs(attInfos.size - expectedSize) <= 2);
+ // Check that the attachment URLs are correct.
+ let channel = NetUtil.newChannel({
+ uri: attInfos.url,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ try {
+ // Will throw if the URL is invalid.
+ channel.asyncOpen(new PromiseTestUtils.PromiseStreamListener());
+ } catch (e) {
+ do_throw(new Error("Invalid attachment URL"));
+ }
+ }
+ } else {
+ // Make sure we don't actually get attachments!
+ Assert.equal(gmsg.attachmentTypes, null);
+ Assert.equal(gmsg.attachmentNames, null);
+ }
+}
+
+/**
+ * We now move the message into another folder, wait for it to be indexed,
+ * and make sure the magic url getter for GlodaAttachment returns a proper
+ * URL.
+ */
+async function test_moved_message_attributes() {
+ if (!expectFulltextResults) {
+ return;
+ }
+
+ // Don't ask me why, let destFolder = MessageInjection.make_empty_folder would result in a
+ // random error when running test_index_messages_imap_offline.js ...
+ let [[destFolder], ignoreSet] = await messageInjection.makeFoldersWithSets(
+ 1,
+ [{ count: 2 }]
+ );
+ fundamentalFolderHandle = destFolder;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([ignoreSet]));
+
+ // This is a fast move (third parameter set to true).
+ await messageInjection.moveMessages(fundamentalMsgSet, destFolder, true);
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([fundamentalMsgSet], {
+ verifier(newSynMsg, newGlodaMsg) {
+ // Verify we still have the same number of attachments.
+ Assert.equal(
+ fundamentalGlodaMsgAttachmentUrls.length,
+ newGlodaMsg.attachmentInfos.length
+ );
+ for (let [i, attInfos] of newGlodaMsg.attachmentInfos.entries()) {
+ // Verify the url has changed.
+ Assert.notEqual(fundamentalGlodaMsgAttachmentUrls[i], attInfos.url);
+ // And verify that the new url is still valid.
+ let channel = NetUtil.newChannel({
+ uri: attInfos.url,
+ loadingPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+ try {
+ channel.asyncOpen(new PromiseTestUtils.PromiseStreamListener());
+ } catch (e) {
+ new Error("Invalid attachment URL");
+ }
+ }
+ },
+ fullyIndexed: 0,
+ })
+ );
+}
+
+/**
+ * We want to make sure that all of the fundamental properties also are there
+ * when we load them from disk. Nuke our cache, query the message back up.
+ * We previously used getMessagesByMessageID to get the message back, but he
+ * does not perform a full load-out like a query does, so we need to use our
+ * query mechanism for this.
+ */
+async function test_attributes_fundamental_from_disk() {
+ nukeGlodaCachesAndCollections();
+
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE).id(
+ fundamentalGlodaMessageId
+ );
+ await queryExpect(
+ query,
+ [fundamentalSyntheticMessage],
+ verify_attributes_fundamental_from_disk,
+ function (smsg) {
+ return smsg.messageId;
+ }
+ );
+}
+
+/**
+ * We are just a wrapper around verify_attributes_fundamental, adapting the
+ * return callback from getMessagesByMessageID.
+ *
+ * @param aGlodaMessageLists This should be [[theGlodaMessage]].
+ */
+function verify_attributes_fundamental_from_disk(aGlodaMessage) {
+ // Teturn the message id for test_attributes_fundamental_from_disk's benefit.
+ verify_attributes_fundamental(fundamentalSyntheticMessage, aGlodaMessage);
+ return aGlodaMessage.headerMessageID;
+}
+
+/* ===== Explicit Attributes (per GlodaExplicitAttr.jsm) ===== */
+
+/**
+ * Test the attributes defined by GlodaExplicitAttr.jsm.
+ */
+async function test_attributes_explicit() {
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let gmsg = msgSet.glodaMessages[0];
+
+ // -- Star
+ msgSet.setStarred(true);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.starred, true);
+
+ msgSet.setStarred(false);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.starred, false);
+
+ // -- Read / Unread
+ msgSet.setRead(true);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.read, true);
+
+ msgSet.setRead(false);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.read, false);
+
+ // -- Tags
+ // Note that the tag service does not guarantee stable nsIMsgTag references,
+ // nor does noun_tag go too far out of its way to provide stability.
+ // However, it is stable as long as we don't spook it by bringing new tags
+ // into the equation.
+ let tagOne = TagNoun.getTag("$label1");
+ let tagTwo = TagNoun.getTag("$label2");
+
+ msgSet.addTag(tagOne.key);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.notEqual(gmsg.tags.indexOf(tagOne), -1);
+
+ msgSet.addTag(tagTwo.key);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.notEqual(gmsg.tags.indexOf(tagOne), -1);
+ Assert.notEqual(gmsg.tags.indexOf(tagTwo), -1);
+
+ msgSet.removeTag(tagOne.key);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.tags.indexOf(tagOne), -1);
+ Assert.notEqual(gmsg.tags.indexOf(tagTwo), -1);
+
+ msgSet.removeTag(tagTwo.key);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.tags.indexOf(tagOne), -1);
+ Assert.equal(gmsg.tags.indexOf(tagTwo), -1);
+
+ // -- Replied To
+
+ // -- Forwarded
+}
+
+/**
+ * Test non-query-able attributes
+ */
+async function test_attributes_cant_query() {
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let gmsg = msgSet.glodaMessages[0];
+
+ // -- Star
+ msgSet.setStarred(true);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.starred, true);
+
+ msgSet.setStarred(false);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.starred, false);
+
+ // -- Read / Unread
+ msgSet.setRead(true);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.read, true);
+
+ msgSet.setRead(false);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.read, false);
+
+ let readDbAttr = Gloda.getAttrDef(GlodaConstants.BUILT_IN, "read");
+ let readId = readDbAttr.id;
+
+ await sqlExpectCount(
+ 0,
+ "SELECT COUNT(*) FROM messageAttributes WHERE attributeID = ?1",
+ readId
+ );
+
+ // -- Replied To
+
+ // -- Forwarded
+}
+
+/**
+ * Have the participants be in our addressbook prior to indexing so that we can
+ * verify that the hand-off to the addressbook indexer does not cause breakage.
+ */
+async function test_people_in_addressbook() {
+ var senderPair = msgGen.makeNameAndAddress(),
+ recipPair = msgGen.makeNameAndAddress();
+
+ // - Add both people to the address book.
+ makeABCardForAddressPair(senderPair);
+ makeABCardForAddressPair(recipPair);
+
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, to: [recipPair], from: senderPair },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let gmsg = msgSet.glodaMessages[0],
+ senderIdentity = gmsg.from,
+ recipIdentity = gmsg.to[0];
+
+ Assert.notEqual(senderIdentity.contact, null);
+ Assert.ok(senderIdentity.inAddressBook);
+
+ Assert.notEqual(recipIdentity.contact, null);
+ Assert.ok(recipIdentity.inAddressBook);
+}
+
+/* ===== Fulltexts Indexing ===== */
+
+/**
+ * Make sure that we are using the saneBodySize flag. This is basically the
+ * test_sane_bodies test from test_mime_emitter but we pull the indexedBodyText
+ * off the message to check and also make sure that the text contents slice
+ * off the end rather than the beginning.
+ */
+async function test_streamed_bodies_are_size_capped() {
+ if (!expectFulltextResults) {
+ return;
+ }
+
+ let hugeString =
+ "qqqqxxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx \r\n";
+ const powahsOfTwo = 10;
+ for (let i = 0; i < powahsOfTwo; i++) {
+ hugeString = hugeString + hugeString;
+ }
+ let bodyString = "aabb" + hugeString + "xxyy";
+
+ let synMsg = msgGen.makeMessage({
+ body: { body: bodyString, contentType: "text/plain" },
+ });
+ let msgSet = new SyntheticMessageSet([synMsg]);
+ let folder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([folder], [msgSet]);
+
+ if (goOffline) {
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ await messageInjection.makeFolderAndContentsOffline(folder);
+ }
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let gmsg = msgSet.glodaMessages[0];
+ Assert.ok(gmsg.indexedBodyText.startsWith("aabb"));
+ Assert.ok(!gmsg.indexedBodyText.includes("xxyy"));
+
+ if (gmsg.indexedBodyText.length > 20 * 1024 + 58 + 10) {
+ do_throw(
+ "Indexed body text is too big! (" + gmsg.indexedBodyText.length + ")"
+ );
+ }
+}
+
+/* ===== Message Deletion ===== */
+/**
+ * Test actually deleting a message on a per-message basis (not just nuking the
+ * folder like emptying the trash does.)
+ *
+ * Logic situations:
+ * - Non-last message in a conversation, twin.
+ * - Non-last message in a conversation, not a twin.
+ * - Last message in a conversation
+ */
+async function test_message_deletion() {
+ // Non-last message in conv, twin.
+ // Create and index two messages in a conversation.
+ let [, convSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 2, msgsPerThread: 2 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([convSet], { augment: true }));
+
+ // Twin the first message in a different folder owing to our reliance on
+ // message-id's in the SyntheticMessageSet logic. (This is also why we broke
+ // up the indexing waits too.)
+ let twinFolder = await messageInjection.makeEmptyFolder();
+ let twinSet = new SyntheticMessageSet([convSet.synMessages[0]]);
+ await messageInjection.addSetsToFolders([twinFolder], [twinSet]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([twinSet], { augment: true }));
+
+ // Split the conv set into two helper sets.
+ let firstSet = convSet.slice(0, 1); // The twinned first message in the thread.
+ let secondSet = convSet.slice(1, 2); // The un-twinned second thread message.
+
+ // Make sure we can find the message (paranoia).
+ let firstQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ firstQuery.id(firstSet.glodaMessages[0].id);
+ let firstColl = await queryExpect(firstQuery, firstSet);
+
+ // Delete it (not trash! delete!).
+ await MessageInjection.deleteMessages(firstSet);
+ // Which should result in an apparent deletion.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [firstSet] }));
+ // And our collection from that query should now be empty.
+ Assert.equal(firstColl.items.length, 0);
+
+ // Make sure it no longer shows up in a standard query.
+ firstColl = await queryExpect(firstQuery, []);
+
+ // Make sure it shows up in a privileged query.
+ let privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ let firstGlodaId = firstSet.glodaMessages[0].id;
+ privQuery.id(firstGlodaId);
+ await queryExpect(privQuery, firstSet);
+
+ // Force a deletion pass.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // Make sure it no longer shows up in a privileged query; since it has a twin
+ // we don't need to leave it as a ghost.
+ await queryExpect(privQuery, []);
+
+ // Make sure that the messagesText entry got blown away.
+ await sqlExpectCount(
+ 0,
+ "SELECT COUNT(*) FROM messagesText WHERE docid = ?1",
+ firstGlodaId
+ );
+
+ // Make sure the conversation still exists.
+ let conv = twinSet.glodaMessages[0].conversation;
+ let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION);
+ convQuery.id(conv.id);
+ let convColl = await queryExpect(convQuery, [conv]);
+
+ // -- Non-last message, no longer a twin => ghost.
+
+ // Make sure nuking the twin didn't somehow kill them both.
+ let twinQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ // Let's search on the message-id now that there is no ambiguity.
+ twinQuery.headerMessageID(twinSet.synMessages[0].messageId);
+ let twinColl = await queryExpect(twinQuery, twinSet);
+
+ // Delete the twin.
+ await MessageInjection.deleteMessages(twinSet);
+ // Which should result in an apparent deletion.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [twinSet] }));
+ // It should disappear from the collection.
+ Assert.equal(twinColl.items.length, 0);
+
+ // No longer show up in the standard query.
+ twinColl = await queryExpect(twinQuery, []);
+
+ // Still show up in a privileged query.
+ privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ privQuery.headerMessageID(twinSet.synMessages[0].messageId);
+ await queryExpect(privQuery, twinSet);
+
+ // Force a deletion pass.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // The message should be marked as a ghost now that the deletion pass.
+ // Ghosts have no fulltext rows, so check for that.
+ await sqlExpectCount(
+ 0,
+ "SELECT COUNT(*) FROM messagesText WHERE docid = ?1",
+ twinSet.glodaMessages[0].id
+ );
+
+ // It still should show up in the privileged query; it's a ghost!
+ let privColl = await queryExpect(privQuery, twinSet);
+ // Make sure it looks like a ghost.
+ let twinGhost = privColl.items[0];
+ Assert.equal(twinGhost._folderID, null);
+ Assert.equal(twinGhost._messageKey, null);
+
+ // Make sure the conversation still exists.
+ await queryExpect(convQuery, [conv]);
+
+ // -- Non-last message, not a twin.
+ // This should blow away the message, the ghosts, and the conversation.
+
+ // Second message should still be around.
+ let secondQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ secondQuery.headerMessageID(secondSet.synMessages[0].messageId);
+ let secondColl = await queryExpect(secondQuery, secondSet);
+
+ // Delete it and make sure it gets marked deleted appropriately.
+ await MessageInjection.deleteMessages(secondSet);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [secondSet] }));
+ Assert.equal(secondColl.items.length, 0);
+
+ // Still show up in a privileged query.
+ privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ privQuery.headerMessageID(secondSet.synMessages[0].messageId);
+ await queryExpect(privQuery, secondSet);
+
+ // Force a deletion pass.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // It should no longer show up in a privileged query; we killed the ghosts.
+ await queryExpect(privQuery, []);
+
+ // - The conversation should have disappeared too.
+ // (we have no listener to watch for it to have disappeared from convQuery but
+ // this is basically how glodaTestHelper does its thing anyways.)
+ Assert.equal(convColl.items.length, 0);
+
+ // Make sure the query fails to find it too.
+ await queryExpect(convQuery, []);
+
+ // -- Identity culling verification.
+ // The identities associated with that message should no longer exist, nor
+ // should their contacts.
+}
+
+async function test_moving_to_trash_marks_deletion() {
+ // Create and index two messages in a conversation.
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 2, msgsPerThread: 2 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ let convId = msgSet.glodaMessages[0].conversation.id;
+ let firstGlodaId = msgSet.glodaMessages[0].id;
+ let secondGlodaId = msgSet.glodaMessages[1].id;
+
+ // Move them to the trash.
+ await messageInjection.trashMessages(msgSet);
+
+ // We do not index the trash folder so this should actually make them appear
+ // deleted to an unprivileged query.
+ let msgQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ msgQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgQuery, []);
+
+ // They will appear deleted after the events.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] }));
+
+ // Force a sweep.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ // There should be no apparent change as the result of this pass.
+ // Well, the conversation will die, but we can't see that.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // The conversation should be gone.
+ let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION);
+ convQuery.id(convId);
+ await queryExpect(convQuery, []);
+
+ // The messages should be entirely gone.
+ let msgPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ msgPrivQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgPrivQuery, []);
+}
+
+/**
+ * Deletion that occurs because a folder got deleted.
+ * There is no hand-holding involving the headers that were in the folder.
+ */
+async function test_folder_nuking_message_deletion() {
+ // Create and index two messages in a conversation.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 2, msgsPerThread: 2 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+
+ let convId = msgSet.glodaMessages[0].conversation.id;
+ let firstGlodaId = msgSet.glodaMessages[0].id;
+ let secondGlodaId = msgSet.glodaMessages[1].id;
+
+ // Delete the folder.
+ messageInjection.deleteFolder(folder);
+ // That does generate the deletion events if the messages were in-memory,
+ // which these are.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] }));
+
+ // This should have caused us to mark all the messages as deleted; the
+ // messages should no longer show up in an unprivileged query.
+ let msgQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ msgQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgQuery, []);
+
+ // Force a sweep.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ // There should be no apparent change as the result of this pass.
+ // Well, the conversation will die, but we can't see that.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // The conversation should be gone.
+ let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION);
+ convQuery.id(convId);
+ await queryExpect(convQuery, []);
+
+ // The messages should be entirely gone.
+ let msgPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, {
+ noDbQueryValidityConstraints: true,
+ });
+ msgPrivQuery.id(firstGlodaId, secondGlodaId);
+ await queryExpect(msgPrivQuery, []);
+}
+
+/* ===== Folder Move/Rename/Copy (Single and Nested) ===== */
+
+async function test_folder_deletion_nested() {
+ // Add a folder with a bunch of messages.
+ let [[folder1], msgSet1] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ let [[folder2], msgSet2] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ // Index these folders, and augment the msgSet with the glodaMessages array
+ // for later use by sqlExpectCount.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet1, msgSet2], { augment: true })
+ );
+ // The move has to be performed after the indexing, because otherwise, on
+ // IMAP, the moved message header are different entities and it's not msgSet2
+ // that ends up indexed, but the fresh headers
+ await MessageInjection.moveFolder(folder2, folder1);
+
+ // Add a trash folder, and move folder1 into it.
+ let trash = await messageInjection.makeEmptyFolder(null, [
+ Ci.nsMsgFolderFlags.Trash,
+ ]);
+ await MessageInjection.moveFolder(folder1, trash);
+
+ let folders = MessageInjection.get_nsIMsgFolder(trash).descendants;
+ Assert.equal(folders.length, 2);
+ let [newFolder1, newFolder2] = folders;
+
+ let glodaFolder1 = Gloda.getFolderForFolder(newFolder1);
+ let glodaFolder2 = Gloda.getFolderForFolder(newFolder2);
+
+ // Verify that Gloda properly marked this folder as not to be indexed anymore.
+ Assert.equal(
+ glodaFolder1.indexingPriority,
+ glodaFolder1.kIndexingNeverPriority
+ );
+
+ // Check that existing message is marked as deleted.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], { deleted: [msgSet1, msgSet2] })
+ );
+
+ // Make sure the deletion hit the database.
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?",
+ glodaFolder1.id,
+ glodaFolder1.kIndexingNeverPriority
+ );
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?",
+ glodaFolder2.id,
+ glodaFolder2.kIndexingNeverPriority
+ );
+
+ if (messageInjection.messageInjectionIsLocal()) {
+ // Add another message.
+ await messageInjection.makeNewSetsInFolders([newFolder1], [{ count: 1 }]);
+ await messageInjection.makeNewSetsInFolders([newFolder2], [{ count: 1 }]);
+
+ // Make sure that indexing returns nothing.
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+ }
+}
+
+/* ===== IMAP Nuances ===== */
+
+/**
+ * Verify that for IMAP folders we still see an index a message that is added
+ * as read.
+ */
+async function test_imap_add_unread_to_folder() {
+ if (messageInjection.messageInjectionIsLocal()) {
+ return;
+ }
+
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, read: true },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+}
+
+/* ===== Message Moving ===== */
+
+/**
+ * Moving a message between folders should result in us knowing that the message
+ * is in the target location.
+ */
+async function test_message_moving() {
+ // - Inject and insert.
+ // Source folder with the message we care about.
+ let [[srcFolder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ // Dest folder with some messages in it to test some wacky local folder moving
+ // logic. (Local moves try and update the correspondence immediately.)
+ let [[destFolder], ignoreSet] = await messageInjection.makeFoldersWithSets(
+ 1,
+ [{ count: 2 }]
+ );
+
+ // We want the gloda message mapping.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet, ignoreSet], { augment: true })
+ );
+ let gmsg = msgSet.glodaMessages[0];
+ // Save off the message key so we can make sure it changes.
+ let oldMessageKey = msgSet.getMsgHdr(0).messageKey;
+
+ // - Fastpath (offline) move it to a new folder.
+ // Initial move.
+ await messageInjection.moveMessages(msgSet, destFolder, true);
+
+ // - Make sure gloda sees it in the new folder.
+ // Since we are doing offline IMAP moves, the fast-path should be taken and
+ // so we should receive an itemsModified notification without a call to
+ // Gloda.grokNounItem.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { fullyIndexed: 0 }));
+
+ Assert.equal(
+ gmsg.folderURI,
+ messageInjection.getRealInjectionFolder(destFolder).URI
+ );
+
+ // - Make sure the message key is correct!
+ Assert.equal(gmsg.messageKey, msgSet.getMsgHdr(0).messageKey);
+ // Sanity check that the messageKey actually changed for the message.
+ Assert.notEqual(gmsg.messageKey, oldMessageKey);
+
+ // - Make sure the indexer's _keyChangedBatchInfo dict is empty.
+ for (let evilKey in GlodaMsgIndexer._keyChangedBatchInfo) {
+ let evilValue = GlodaMsgIndexer._keyChangedBatchInfo[evilKey];
+ throw new Error(
+ "GlodaMsgIndexer._keyChangedBatchInfo should be empty but" +
+ "has key:\n" +
+ evilKey +
+ "\nAnd value:\n",
+ evilValue + "."
+ );
+ }
+
+ // - Slowpath (IMAP online) move it back to its origin folder.
+ // Move it back.
+ await messageInjection.moveMessages(msgSet, srcFolder, false);
+ // In the IMAP case we will end up reindexing the message because we will
+ // not be able to fast-path, but the local case will still be fast-pathed.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([msgSet], {
+ fullyIndexed: messageInjection.messageInjectionIsLocal() ? 0 : 1,
+ })
+ );
+ Assert.equal(
+ gmsg.folderURI,
+ messageInjection.getRealInjectionFolder(srcFolder).URI
+ );
+ Assert.equal(gmsg.messageKey, msgSet.getMsgHdr(0).messageKey);
+}
+
+/**
+ * Moving a gloda-indexed message out of a filthy folder should result in the
+ * destination message not having a gloda-id.
+ */
+
+/* ===== Message Copying ===== */
+
+/* ===== Sweep Complications ==== */
+
+/**
+ * Make sure that a message indexed by event-driven indexing does not
+ * get reindexed by sweep indexing that follows.
+ */
+async function test_sweep_indexing_does_not_reindex_event_indexed() {
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ // Wait for the event sweep to complete.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+
+ // Force a sweep of the folder.
+ GlodaMsgIndexer.indexFolder(messageInjection.getRealInjectionFolder(folder));
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+}
+
+/**
+ * Verify that moving apparently gloda-indexed messages from a filthy folder or
+ * one that simply should not be gloda indexed does not result in the target
+ * messages having the gloda-id property on them. To avoid messing with too
+ * many invariants we do the 'folder should not be gloda indexed' case.
+ * Uh, and of course, the message should still get indexed once we clear the
+ * filthy gloda-id off of it given that it is moving from a folder that is not
+ * indexed to one that is indexed.
+ */
+async function test_filthy_moves_slash_move_from_unindexed_to_indexed() {
+ // - Inject.
+ // The source folder needs a flag so we don't index it.
+ let srcFolder = await messageInjection.makeEmptyFolder(null, [
+ Ci.nsMsgFolderFlags.Junk,
+ ]);
+ // The destination folder has to be something we want to index though.
+ let destFolder = await messageInjection.makeEmptyFolder();
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [srcFolder],
+ [{ count: 1 }]
+ );
+
+ // - Mark with a bogus gloda-id.
+ msgSet.getMsgHdr(0).setUint32Property("gloda-id", 9999);
+
+ // - Disable event driven indexing so we don't get interference from indexing.
+ configureGlodaIndexing({ event: false });
+
+ // - Move.
+ await messageInjection.moveMessages(msgSet, destFolder);
+
+ // - Verify the target has no gloda-id!
+ dump(`checking ${msgSet.getMsgHdr(0)}`);
+ Assert.equal(msgSet.getMsgHdr(0).getUint32Property("gloda-id"), 0);
+
+ // - Re-enable indexing and let the indexer run.
+ // We don't want to affect other tests.
+ configureGlodaIndexing({});
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+}
+
+function test_sanity_test_environment() {
+ Assert.ok(msgGen, "Sanity that msgGen is set.");
+ Assert.ok(scenarios, "Sanity that scenarios is set");
+ Assert.ok(messageInjection, "Sanity that messageInjection is set.");
+}
+
+var base_index_messages_tests = [
+ test_sanity_test_environment,
+ test_pending_commit_tracker_flushes_correctly,
+ test_pending_commit_causes_msgdb_commit,
+ test_indexing_sweep,
+ test_event_driven_indexing_does_not_mess_with_filthy_folders,
+
+ test_threading_direct_reply,
+ test_threading_missing_intermediary,
+ test_threading_siblings_missing_parent,
+ test_attachment_flag,
+ test_attributes_fundamental,
+ test_moved_message_attributes,
+ test_attributes_fundamental_from_disk,
+ test_attributes_explicit,
+ test_attributes_cant_query,
+
+ test_people_in_addressbook,
+
+ test_streamed_bodies_are_size_capped,
+
+ test_imap_add_unread_to_folder,
+ test_message_moving,
+
+ test_message_deletion,
+ test_moving_to_trash_marks_deletion,
+ test_folder_nuking_message_deletion,
+
+ test_sweep_indexing_does_not_reindex_event_indexed,
+
+ test_filthy_moves_slash_move_from_unindexed_to_indexed,
+
+ test_indexing_never_priority,
+ test_setting_indexing_priority_never_while_indexing,
+
+ test_folder_deletion_nested,
+];
diff --git a/comm/mailnews/db/gloda/test/unit/base_query_messages.js b/comm/mailnews/db/gloda/test/unit/base_query_messages.js
new file mode 100644
index 0000000000..02b8cceb1a
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/base_query_messages.js
@@ -0,0 +1,729 @@
+/* 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/. */
+
+/*
+ * This file tests our querying support. We build up a deterministic little
+ * 'world' of messages spread across multiple conversations, multiple folders
+ * and multiple authors. To verify expected negative results, in addition to
+ * the 'peoples' in our world clique, we also have 'outlier' contacts that do
+ * not communicate with the others (but are also spread across folders).
+ *
+ * This is broadly intended to test all of our query features and mechanisms
+ * (apart from our specialized search implementation, which is tested by
+ * test_search_messages.js), but is probably not the place to test specific
+ * edge-cases if they do not easily fit into the 'world' data set.
+ *
+ * I feel like having the 'world' mishmash as a data source may muddle things
+ * more than it should, but it is hard to deny the benefit of not having to
+ * define a bunch of message corpuses entirely specialized for each test.
+ */
+
+var { assertExpectedMessagesIndexed, waitForGlodaIndexer } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/**
+ * Whether we expect fulltext results. IMAP folders that are offline shouldn't
+ * have their bodies indexed.
+ */
+var expectFulltextResults = true;
+
+/**
+ * Should we force our folders offline after we have indexed them once. We do
+ * this in the online_to_offline test variant.
+ */
+var goOffline = false;
+
+/* ===== Populate ===== */
+var world = {
+ phase: 0,
+
+ // A list of tuples of [name, email] of length NUM_AUTHORS.
+ peoples: null,
+ NUM_AUTHORS: 5,
+ // Maps each author (as defined by their email address) to the list of
+ // (synthetic) messages they have 'authored'.
+ authorGroups: {},
+
+ NUM_CONVERSATIONS: 3,
+ // The last message (so far) in each conversation.
+ lastMessagesInConvos: [],
+ // Maps the message-id of the root message in a conversation to the list of
+ // synthetic messages in the conversation.
+ conversationGroups: {},
+ // A list of lists of synthetic messages, organized by the conversation they
+ // belong to.
+ conversationLists: [],
+ // A list of gloda conversation id's, each corresponding to the entries in
+ // converastionLists.
+ glodaConversationIds: [],
+
+ NUM_FOLDERS: 2,
+ MESSAGES_PER_FOLDER: 11,
+ // A list of lists of synthetic messages, one list per folder.
+ folderClumps: [],
+ // A list of nsIMsgFolders, with each folder containing the messages in the
+ // corresponding list in folderClumps.
+ glodaFolders: [],
+
+ outlierAuthor: null,
+ outlierFriend: null,
+
+ // Messages authored by contacts in the "peoples" group.
+ peoplesMessages: [],
+ // Messages authored by outlierAuthor and outlierFriend.
+ outlierMessages: [],
+};
+
+/**
+ * Given a number, provide a unique term. This is for the benefit of the search
+ * logic. This entails using a unique prefix to avoid accidental collision
+ * with terms outside our control and then just generating unique character
+ * strings in a vaguely base-26 style. To avoid the porter stemmer causing odd
+ * things to happen we actually double every numerically driven character.
+ */
+function uniqueTermGenerator(aNum) {
+ let s = "uniq";
+ do {
+ let l = String.fromCharCode(97 + (aNum % 26));
+ s += l + l;
+ aNum = Math.floor(aNum / 26);
+ } while (aNum);
+ return s;
+}
+
+var UNIQUE_OFFSET_CONV = 0;
+var UNIQUE_OFFSET_AUTHOR = 26;
+var UNIQUE_OFFSET_BODY = 0;
+var UNIQUE_OFFSET_SUBJECT = 26 * 26;
+var UNIQUE_OFFSET_ATTACHMENT = 26 * 26 * 26;
+
+/**
+ * Categorize a synthetic message by conversation/folder/people in the 'world'
+ * structure. This is then used by the test code to generate and verify query
+ * data.
+ *
+ * @param aSynthMessage The synthetic message.
+ */
+function categorizeMessage(aSynthMessage) {
+ // Lump by author.
+ let author = aSynthMessage.fromAddress;
+ if (!(author in world.authorGroups)) {
+ world.authorGroups[author] = [];
+ }
+ world.authorGroups[author].push(aSynthMessage);
+
+ // Lump by conversation, keying off of the originator's message id.
+ let originator = aSynthMessage;
+ while (originator.parent) {
+ originator = originator.parent;
+ }
+ if (!(originator.messageId in world.conversationGroups)) {
+ world.conversationGroups[originator.messageId] = [];
+ }
+ world.conversationGroups[originator.messageId].push(aSynthMessage);
+ world.conversationLists[aSynthMessage.iConvo].push(aSynthMessage);
+
+ // Folder lumping happens in a big glob.
+}
+
+/**
+ * Generate messages in a single folder, categorizing them as we go.
+ *
+ * Key message characteristics:
+ * - Whenever a 'peoples' sends a message, they send it to all 'peoples',
+ * including themselves.
+ */
+function generateFolderMessages() {
+ let messages = [],
+ smsg;
+
+ let iAuthor = 0;
+ for (let iMessage = 0; iMessage < world.MESSAGES_PER_FOLDER; iMessage++) {
+ let iConvo = iMessage % world.NUM_CONVERSATIONS;
+
+ // We need missing messages to create ghosts, so periodically add an extra
+ // unknown into the equation. we do this prior to the below step because
+ // then we don't hose up all the fancy body creation the next step does.
+ if (iMessage % 3 == 1) {
+ smsg = msgGen.makeMessage({ inReplyTo: smsg });
+ }
+
+ let convUniqueSubject = uniqueTermGenerator(
+ UNIQUE_OFFSET_SUBJECT + UNIQUE_OFFSET_CONV + iConvo
+ );
+ let convUniqueBody = uniqueTermGenerator(
+ UNIQUE_OFFSET_BODY + UNIQUE_OFFSET_CONV + iConvo
+ );
+ let authorUniqueBody = uniqueTermGenerator(
+ UNIQUE_OFFSET_BODY + UNIQUE_OFFSET_AUTHOR + iAuthor
+ );
+ let convUniqueAttachment = uniqueTermGenerator(
+ UNIQUE_OFFSET_ATTACHMENT + UNIQUE_OFFSET_CONV + iConvo
+ );
+ smsg = msgGen.makeMessage({
+ inReplyTo: world.lastMessagesInConvos[iConvo],
+ // Note that the reply-logic will ignore our subject, luckily that does
+ // not matter! (since it will just copy the subject)
+ subject: convUniqueSubject,
+ body: {
+ body: convUniqueBody + " " + authorUniqueBody,
+ },
+ attachments: [
+ {
+ filename: convUniqueAttachment + ".conv",
+ body: "content does not matter. only life matters.",
+ contentType: "application/x-test",
+ },
+ ],
+ });
+
+ // MakeMessage is not exceedingly clever right now, we need to overwrite
+ // From and To.
+ smsg.from = world.peoples[iAuthor];
+ iAuthor = (iAuthor + iConvo + 1) % world.NUM_AUTHORS;
+ // So, everyone is talking to everyone for this stuff.
+ smsg.to = world.peoples;
+ world.lastMessagesInConvos[iConvo] = smsg;
+ // Simplify categorizeMessage and glodaInfoStasher's life.
+ smsg.iConvo = iConvo;
+
+ categorizeMessage(smsg);
+ messages.push(smsg);
+ world.peoplesMessages.push(smsg);
+ }
+
+ smsg = msgGen.makeMessage();
+ smsg.from = world.outlierAuthor;
+ smsg.to = [world.outlierFriend];
+ // Do not lump it.
+ messages.push(smsg);
+ world.outlierMessages.push(smsg);
+
+ world.folderClumps.push(messages);
+
+ return new SyntheticMessageSet(messages);
+}
+
+/**
+ * To save ourselves some lookup trouble, pretend to be a verification
+ * function so we get easy access to the gloda translations of the messages so
+ * we can cram this in various places.
+ */
+function glodaInfoStasher(aSynthMessage, aGlodaMessage) {
+ if (aSynthMessage.iConvo !== undefined) {
+ world.glodaConversationIds[aSynthMessage.iConvo] =
+ aGlodaMessage.conversation.id;
+ }
+ if (world.glodaFolders.length <= world.phase) {
+ world.glodaFolders.push(aGlodaMessage.folder);
+ }
+}
+
+// We override these for the IMAP tests.
+var pre_setup_populate_hook = function default_pre_setup_populate_hook() {};
+var post_setup_populate_hook = function default_post_setup_populate_hook() {};
+
+// First, we must populate our message store with delicious messages.
+async function setup_populate() {
+ world.glodaHolderCollection = Gloda.explicitCollection(
+ GlodaConstants.NOUN_MESSAGE,
+ []
+ );
+
+ world.peoples = msgGen.makeNamesAndAddresses(world.NUM_AUTHORS);
+ world.outlierAuthor = msgGen.makeNameAndAddress();
+ world.outlierFriend = msgGen.makeNameAndAddress();
+ // Set up the per-conversation values with blanks initially.
+ for (let iConvo = 0; iConvo < world.NUM_CONVERSATIONS; iConvo++) {
+ world.lastMessagesInConvos.push(null);
+ world.conversationLists.push([]);
+ world.glodaConversationIds.push(null);
+ }
+
+ let setOne = generateFolderMessages();
+ let folderOne = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([folderOne], [setOne]);
+ // If this is the online_to_offline variant (indicated by goOffline) we want
+ // to make the messages available offline. This should trigger an event
+ // driven re-indexing of the messages which should make the body available
+ // for fulltext queries.
+ if (goOffline) {
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setOne]));
+ await messageInjection.makeFolderAndContentsOffline(folderOne);
+ }
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([setOne], { verifier: glodaInfoStasher })
+ );
+
+ world.phase++;
+ let setTwo = generateFolderMessages();
+ let folderTwo = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([folderTwo], [setTwo]);
+ if (goOffline) {
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([setTwo]));
+ await messageInjection.makeFolderAndContentsOffline(folderTwo);
+ }
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([setTwo], { verifier: glodaInfoStasher })
+ );
+}
+
+/* ===== Non-text queries ===== */
+
+/* === messages === */
+
+/**
+ * Takes a list of mutually exclusive queries and a list of the resulting
+ * collections and ensures that the collections from one query do not pass the
+ * query.test() method of one of the other queries. To restate, the queries
+ * must not have any overlapping results, or we will get angry without
+ * justification.
+ */
+function verify_nonMatches(aQueries, aCollections) {
+ for (let i = 0; i < aCollections.length; i++) {
+ let testQuery = aQueries[i];
+ let nonmatches = aCollections[(i + 1) % aCollections.length].items;
+
+ for (let item of nonmatches) {
+ if (testQuery.test(item)) {
+ dump("item: " + JSON.stringify(item) + "\n");
+ dump("constraints: " + JSON.stringify(testQuery._constraints) + "\n");
+ do_throw(
+ "Something should not match query.test(), but it does: " + item
+ );
+ }
+ }
+ }
+}
+
+var ts_convNum = 0;
+/* preserved state for the non-match testing performed by
+ * test_query_messages_by_conversation_nonmatches.
+ */
+var ts_convQueries = [];
+var ts_convCollections = [];
+/**
+ * Query conversations by gloda conversation-id, saving the queries and
+ * resulting collections in ts_convQueries and ts_convCollections for the
+ * use of test_query_messages_by_conversation_nonmatches who verifies the
+ * query.test() logic doesn't match on things it should not match on.
+ *
+ * @tests gloda.noun.message.attr.conversation
+ * @tests gloda.datastore.sqlgen.kConstraintIn
+ */
+async function test_query_messages_by_conversation() {
+ let convNum = ts_convNum++;
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.conversation(world.glodaConversationIds[convNum]);
+
+ ts_convQueries.push(query);
+ ts_convCollections.push(
+ await queryExpect(query, world.conversationLists[convNum])
+ );
+}
+
+/**
+ * @tests gloda.query.test.kConstraintIn
+ */
+function test_query_messages_by_conversation_nonmatches() {
+ verify_nonMatches(ts_convQueries, ts_convCollections);
+}
+
+var ts_folderNum = 0;
+var ts_folderQueries = [];
+var ts_folderCollections = [];
+/**
+ * @tests gloda.noun.message.attr.folder
+ * @tests gloda.datastore.sqlgen.kConstraintIn
+ */
+async function test_query_messages_by_folder() {
+ let folderNum = ts_folderNum++;
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.folder(world.glodaFolders[folderNum]);
+
+ ts_folderQueries.push(query);
+ ts_folderCollections.push(
+ await queryExpect(query, world.folderClumps[folderNum])
+ );
+}
+
+/**
+ * @tests gloda.query.test.kConstraintIn
+ */
+function test_query_messages_by_folder_nonmatches() {
+ verify_nonMatches(ts_folderQueries, ts_folderCollections);
+}
+
+/**
+ * @tests Gloda.ns.getMessageCollectionForHeader()
+ */
+async function test_get_message_for_header() {
+ // Pick an arbitrary message.
+ let glodaMessage = ts_convCollections[1].items[0];
+ // Find the synthetic message that matches (ordering must not be assumed).
+ let synthMessage = world.conversationLists[1].find(
+ sm => sm.messageId == glodaMessage.headerMessageID
+ );
+ await queryExpect(
+ {
+ queryFunc: Gloda.getMessageCollectionForHeader,
+ queryThis: Gloda,
+ args: [glodaMessage.folderMessage],
+ nounId: GlodaConstants.NOUN_MESSAGE,
+ },
+ [synthMessage]
+ );
+}
+
+/**
+ * @tests Gloda.ns.getMessageCollectionForHeaders()
+ */
+async function test_get_messages_for_headers() {
+ let messageCollection = ts_convCollections[0];
+ let headers = messageCollection.items.map(m => m.folderMessage);
+ await queryExpect(
+ {
+ queryFunc: Gloda.getMessageCollectionForHeaders,
+ queryThis: Gloda,
+ args: [headers],
+ nounId: GlodaConstants.NOUN_MESSAGE,
+ },
+ world.conversationLists[0]
+ );
+}
+
+// At this point we go run the identity and contact tests for side-effects.
+
+var ts_messageIdentityQueries = [];
+var ts_messageIdentityCollections = [];
+/**
+ * @tests gloda.noun.message.attr.involves
+ * @tests gloda.datastore.sqlgen.kConstraintIn
+ */
+async function test_query_messages_by_identity_peoples() {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.involves(peoplesIdentityCollection.items[0]);
+
+ ts_messageIdentityQueries.push(query);
+ ts_messageIdentityCollections.push(
+ await queryExpect(query, world.peoplesMessages)
+ );
+}
+
+/**
+ * @tests gloda.noun.message.attr.involves
+ */
+async function test_query_messages_by_identity_outlier() {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.involves(outlierIdentityCollection.items[0]);
+ // This also tests our ability to have two intersecting constraints! hooray!.
+ query.involves(outlierIdentityCollection.items[1]);
+
+ ts_messageIdentityQueries.push(query);
+ ts_messageIdentityCollections.push(
+ await queryExpect(query, world.outlierMessages)
+ );
+}
+
+/**
+ * @tests gloda.query.test.kConstraintIn
+ */
+function test_query_messages_by_identity_nonmatches() {
+ verify_nonMatches(ts_messageIdentityQueries, ts_messageIdentityCollections);
+}
+
+/* exported test_query_messages_by_contact */
+function test_query_messages_by_contact() {
+ // IOU
+}
+
+var ts_messagesDateQuery;
+/**
+ * @tests gloda.noun.message.attr.date
+ * @tests gloda.datastore.sqlgen.kConstraintRanges
+ */
+async function test_query_messages_by_date() {
+ ts_messagesDateQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ // We are clearly relying on knowing the generation sequence here,
+ // fuggedaboutit.
+ ts_messagesDateQuery.dateRange([
+ world.peoplesMessages[1].date,
+ world.peoplesMessages[2].date,
+ ]);
+ await queryExpect(ts_messagesDateQuery, world.peoplesMessages.slice(1, 3));
+}
+
+/**
+ * @tests gloda.query.test.kConstraintRanges
+ */
+function test_query_messages_by_date_nonmatches() {
+ if (
+ ts_messagesDateQuery.test(world.peoplesMessages[0]) ||
+ ts_messagesDateQuery.test(world.peoplesMessages[3])
+ ) {
+ do_throw("The date testing mechanism is busted.");
+ }
+}
+
+/* === contacts === */
+/* exported test_query_contacts_by_popularity */
+function test_query_contacts_by_popularity() {
+ // IOU
+}
+
+/* === identities === */
+
+/* ===== Text-based queries ===== */
+
+/* === conversations === */
+
+/* exported test_query_conversations_by_subject_text */
+function test_query_conversations_by_subject_text() {}
+
+/* === messages === */
+
+/**
+ * Test subject searching using the conversation unique subject term.
+ *
+ * @tests gloda.noun.message.attr.subjectMatches
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_subject_text() {
+ // We only need to use one conversation.
+ let convNum = 0;
+
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ let convSubjectTerm = uniqueTermGenerator(
+ UNIQUE_OFFSET_SUBJECT + UNIQUE_OFFSET_CONV + convNum
+ );
+ query.subjectMatches(convSubjectTerm);
+ await queryExpect(query, world.conversationLists[convNum]);
+}
+
+/**
+ * Test body searching using the conversation unique body term.
+ *
+ * @tests gloda.noun.message.attr.bodyMatches
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_body_text() {
+ // We only need to use one conversation.
+ let convNum = 0;
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ let convBodyTerm = uniqueTermGenerator(
+ UNIQUE_OFFSET_BODY + UNIQUE_OFFSET_CONV + convNum
+ );
+ query.bodyMatches(convBodyTerm);
+ await queryExpect(
+ query,
+ expectFulltextResults ? world.conversationLists[convNum] : []
+ );
+}
+
+/**
+ * Test attachment name searching using the conversation unique attachment term.
+ *
+ * @tests gloda.noun.message.attr.attachmentNamesMatch
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_attachment_names() {
+ let convNum = 0;
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ let convUniqueAttachment = uniqueTermGenerator(
+ UNIQUE_OFFSET_ATTACHMENT + UNIQUE_OFFSET_CONV + convNum
+ );
+ query.attachmentNamesMatch(convUniqueAttachment);
+ await queryExpect(
+ query,
+ expectFulltextResults ? world.conversationLists[convNum] : []
+ );
+}
+
+/**
+ * Test author name fulltext searching using an arbitrary author.
+ *
+ * @tests gloda.noun.message.attr.authorMatches
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_authorMatches_name() {
+ let [authorName, authorMail] = world.peoples[0];
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.authorMatches(authorName);
+ await queryExpect(query, world.authorGroups[authorMail]);
+}
+
+/**
+ * Test author mail address fulltext searching using an arbitrary author.
+ *
+ * @tests gloda.noun.message.attr.authorMatches
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_authorMatches_email() {
+ let [, authorMail] = world.peoples[0];
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.authorMatches(authorMail);
+ await queryExpect(query, world.authorGroups[authorMail]);
+}
+
+/**
+ * Test recipient name fulltext searching using an arbitrary recipient. Since
+ * all 'peoples' messages are sent to all of them, any choice from peoples
+ * gets us all 'peoplesMessages'.
+ *
+ * @tests gloda.noun.message.attr.recipientsMatch
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_recipients_name() {
+ let name = world.peoples[0][0];
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.recipientsMatch(name);
+ await queryExpect(query, world.peoplesMessages);
+}
+
+/**
+ * Test recipient mail fulltext searching using an arbitrary recipient. Since
+ * all 'peoples' messages are sent to all of them, any choice from peoples
+ * gets us all 'peoplesMessages'.
+ *
+ * @tests gloda.noun.message.attr.recipientsMatch
+ * @tests gloda.datastore.sqlgen.kConstraintFulltext
+ */
+async function test_query_messages_by_recipients_email() {
+ let [, mail] = world.peoples[0];
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.recipientsMatch(mail);
+ await queryExpect(query, world.peoplesMessages);
+}
+
+/* === contacts === */
+
+var contactLikeQuery;
+/**
+ * @tests gloda.noun.contact.attr.name
+ * @tests gloda.datastore.sqlgen.kConstraintStringLike
+ */
+async function test_query_contacts_by_name() {
+ // Let's use like... we need to test that.
+ contactLikeQuery = Gloda.newQuery(GlodaConstants.NOUN_CONTACT);
+ let personName = world.peoples[0][0];
+ // Chop off the first and last letter... this isn't the most edge-case
+ // handling way to roll, but LOOK OVER THERE? IS THAT ELVIS?
+ let personNameSubstring = personName.substring(1, personName.length - 1);
+ contactLikeQuery.nameLike(
+ contactLikeQuery.WILDCARD,
+ personNameSubstring,
+ contactLikeQuery.WILDCARD
+ );
+
+ await queryExpect(contactLikeQuery, [personName]);
+}
+
+/**
+ * @tests gloda.query.test.kConstraintStringLike
+ */
+function test_query_contacts_by_name_nonmatch() {
+ let otherContact = outlierIdentityCollection.items[0].contact;
+ if (contactLikeQuery.test(otherContact)) {
+ do_throw("The string LIKE mechanism as applied to contacts does not work.");
+ }
+}
+
+/* === identities === */
+
+var peoplesIdentityQuery;
+var peoplesIdentityCollection;
+async function test_query_identities_for_peoples() {
+ peoplesIdentityQuery = Gloda.newQuery(GlodaConstants.NOUN_IDENTITY);
+ peoplesIdentityQuery.kind("email");
+ let peopleAddrs = world.peoples.map(nameAndAddr => nameAndAddr[1]);
+ peoplesIdentityQuery.value.apply(peoplesIdentityQuery, peopleAddrs);
+ peoplesIdentityCollection = await queryExpect(
+ peoplesIdentityQuery,
+ peopleAddrs
+ );
+}
+
+var outlierIdentityQuery;
+var outlierIdentityCollection;
+async function test_query_identities_for_outliers() {
+ outlierIdentityQuery = Gloda.newQuery(GlodaConstants.NOUN_IDENTITY);
+ outlierIdentityQuery.kind("email");
+ let outlierAddrs = [world.outlierAuthor[1], world.outlierFriend[1]];
+ outlierIdentityQuery.value.apply(outlierIdentityQuery, outlierAddrs);
+ outlierIdentityCollection = await queryExpect(
+ outlierIdentityQuery,
+ outlierAddrs
+ );
+}
+
+function test_query_identities_by_kind_and_value_nonmatches() {
+ verify_nonMatches(
+ [peoplesIdentityQuery, outlierIdentityQuery],
+ [peoplesIdentityCollection, outlierIdentityCollection]
+ );
+}
+
+function test_sanity_test_environment() {
+ Assert.ok(msgGen, "Sanity that msgGen is set.");
+ Assert.ok(messageInjection, "Sanity that messageInjection is set.");
+}
+
+var base_query_messages_tests = [
+ test_sanity_test_environment,
+ function pre_setup_populate() {
+ pre_setup_populate_hook();
+ },
+ setup_populate,
+ function post_setup_populate() {
+ post_setup_populate_hook();
+ },
+ test_query_messages_by_conversation,
+ test_query_messages_by_conversation,
+ test_query_messages_by_conversation_nonmatches,
+ test_query_messages_by_folder,
+ test_query_messages_by_folder,
+ test_query_messages_by_folder_nonmatches,
+ test_get_message_for_header,
+ test_get_messages_for_headers,
+ // Need to do the identity and contact lookups so we can have their results
+ // for the other message-related queries.
+ test_query_identities_for_peoples,
+ test_query_identities_for_outliers,
+ test_query_identities_by_kind_and_value_nonmatches,
+ // Back to messages!
+ test_query_messages_by_identity_peoples,
+ test_query_messages_by_identity_outlier,
+ test_query_messages_by_identity_nonmatches,
+ test_query_messages_by_date,
+ test_query_messages_by_date_nonmatches,
+ // Fulltext
+ test_query_messages_by_subject_text,
+ test_query_messages_by_body_text,
+ test_query_messages_by_attachment_names,
+ test_query_messages_by_authorMatches_name,
+ test_query_messages_by_authorMatches_email,
+ test_query_messages_by_recipients_name,
+ test_query_messages_by_recipients_email,
+ // Like
+ test_query_contacts_by_name,
+ test_query_contacts_by_name_nonmatch,
+];
diff --git a/comm/mailnews/db/gloda/test/unit/head_gloda.js b/comm/mailnews/db/gloda/test/unit/head_gloda.js
new file mode 100644
index 0000000000..fb8edbd24e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/head_gloda.js
@@ -0,0 +1,19 @@
+/* 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 { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../../";
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/db/gloda/test/unit/resources/GlodaQueryHelper.jsm b/comm/mailnews/db/gloda/test/unit/resources/GlodaQueryHelper.jsm
new file mode 100644
index 0000000000..e8234f1a97
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/resources/GlodaQueryHelper.jsm
@@ -0,0 +1,431 @@
+/* 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 = ["queryExpect", "sqlExpectCount", "sqlRun"];
+
+/*
+ * This file provides gloda query helpers for the test infrastructure.
+ */
+
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+
+var log = console.createInstance({
+ prefix: "gloda.queryHelper",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+var _defaultExpectationExtractors = {};
+_defaultExpectationExtractors[GlodaConstants.NOUN_MESSAGE] = [
+ function expectExtract_message_gloda(aGlodaMessage) {
+ return aGlodaMessage.headerMessageID;
+ },
+ function expectExtract_message_synth(aSynthMessage) {
+ return aSynthMessage.messageId;
+ },
+];
+_defaultExpectationExtractors[GlodaConstants.NOUN_CONTACT] = [
+ function expectExtract_contact_gloda(aGlodaContact) {
+ return aGlodaContact.name;
+ },
+ function expectExtract_contact_name(aName) {
+ return aName;
+ },
+];
+_defaultExpectationExtractors[GlodaConstants.NOUN_IDENTITY] = [
+ function expectExtract_identity_gloda(aGlodaIdentity) {
+ return aGlodaIdentity.value;
+ },
+ function expectExtract_identity_address(aAddress) {
+ return aAddress;
+ },
+];
+
+function expectExtract_default_toString(aThing) {
+ return aThing.toString();
+}
+
+/**
+ * @see queryExpect for info on what we do.
+ */
+class QueryExpectationListener {
+ constructor(
+ aExpectedSet,
+ aGlodaExtractor,
+ aOrderVerifier,
+ aCallerStackFrame
+ ) {
+ this.expectedSet = aExpectedSet;
+ this.glodaExtractor = aGlodaExtractor;
+ this.orderVerifier = aOrderVerifier;
+ this.completed = false;
+ this.callerStackFrame = aCallerStackFrame;
+ // Track our current 'index' in the results for the (optional) order verifier,
+ // but also so we can provide slightly more useful debug output.
+ this.nextIndex = 0;
+
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ }
+ onItemsAdded(aItems, aCollection) {
+ log.debug("QueryExpectationListener onItemsAdded received.");
+ for (let item of aItems) {
+ let glodaStringRep;
+ try {
+ glodaStringRep = this.glodaExtractor(item);
+ } catch (ex) {
+ this._reject(
+ new Error(
+ "Gloda extractor threw during query expectation.\n" +
+ "Item:\n" +
+ item +
+ "\nException:\n" +
+ ex
+ )
+ );
+ return; // We don't have to continue for more checks.
+ }
+
+ // Make sure we were expecting this guy.
+ if (glodaStringRep in this.expectedSet) {
+ delete this.expectedSet[glodaStringRep];
+ } else {
+ this._reject(
+ new Error(
+ "Query returned unexpected result!\n" +
+ "Item:\n" +
+ item +
+ "\nExpected set:\n" +
+ this.expectedSet +
+ "\nCaller:\n" +
+ this.callerStackFrame
+ )
+ );
+ return; // We don't have to continue for more checks.
+ }
+
+ if (this.orderVerifier) {
+ try {
+ this.orderVerifier(this.nextIndex, item, aCollection);
+ } catch (ex) {
+ // If the order was wrong, we could probably go for an output of what
+ // we actually got...
+ dump("Order Problem detected. Dump of data:\n");
+ for (let [iThing, thing] of aItems.entries()) {
+ dump(
+ iThing +
+ ": " +
+ thing +
+ (aCollection.stashedColumns
+ ? ". " + aCollection.stashedColumns[thing.id].join(", ")
+ : "") +
+ "\n"
+ );
+ }
+ this._reject(ex);
+ return; // We don't have to continue for more checks.
+ }
+ }
+ this.nextIndex++;
+
+ // Make sure the query's test method agrees with the database about this.
+ if (!aCollection.query.test(item)) {
+ this._reject(
+ new Error(
+ "Query test returned false when it should have been true on.\n" +
+ "Extracted:\n" +
+ glodaStringRep +
+ "\nItem:\n" +
+ item
+ )
+ );
+ }
+ }
+ }
+ onItemsModified(aItems, aCollection) {
+ log.debug(
+ "QueryExpectationListener onItemsModified received. Nothing done."
+ );
+ }
+ onItemsRemoved(aItems, aCollection) {
+ log.debug(
+ "QueryExpectationListener onItemsRemoved received. Nothing done."
+ );
+ }
+ onQueryCompleted(aCollection) {
+ log.debug("QueryExpectationListener onQueryCompleted received.");
+ // We may continue to match newly added items if we leave our query as it
+ // is, so let's become explicit to avoid related troubles.
+ aCollection.becomeExplicit();
+
+ // `expectedSet` should now be empty.
+ for (let key in this.expectedSet) {
+ let value = this.expectedSet[key];
+ this._reject(
+ new Error(
+ "Query should have returned:\n" +
+ key +
+ " (" +
+ value +
+ ").\n" +
+ "But " +
+ this.nextIndex +
+ " was seen."
+ )
+ );
+ return; // We don't have to continue for more checks.
+ }
+
+ // If no error is thrown then we're fine here.
+ this._resolve();
+ }
+
+ get promise() {
+ return this._promise;
+ }
+}
+
+/**
+ * Execute the given query, verifying that the result set contains exactly the
+ * contents of the expected set; no more, no less. Since we expect that the
+ * query will result in gloda objects, but your expectations will not be posed
+ * in terms of gloda objects (though they could be), we rely on extractor
+ * functions to take the gloda result objects and the expected result objects
+ * into the same string.
+ * If you don't provide extractor functions, we will use our defaults (based on
+ * the query noun type) if available, or assume that calling toString is
+ * sufficient.
+ *
+ * @param aQuery Either a query to execute, or a dict with the following keys:
+ * - queryFunc: The function to call that returns a function.
+ * - queryThis: The 'this' to use for the invocation of queryFunc.
+ * - args: A list (possibly empty) or arguments to precede the traditional
+ * arguments to query.getCollection.
+ * - nounId: The (numeric) noun id of the noun type expected to be returned.
+ * @param aExpectedSet The list of expected results from the query where each
+ * item is suitable for extraction using aExpectedExtractor. We have a soft
+ * spot for SyntheticMessageSets and automatically unbox them.
+ * @param aGlodaExtractor The extractor function to take an instance of the
+ * gloda representation and return a string for comparison/equivalence
+ * against that returned by the expected extractor (against the input
+ * instance in aExpectedSet.) The value returned must be unique for all
+ * of the expected gloda representations of the expected set. If omitted,
+ * the default extractor for the gloda noun type is used. If no default
+ * extractor exists, toString is called on the item.
+ * @param aExpectedExtractor The extractor function to take an instance from the
+ * values in the aExpectedSet and return a string for comparison/equivalence
+ * against that returned by the gloda extractor. The value returned must
+ * be unique for all of the values in the expected set. If omitted, the
+ * default extractor for the presumed input type based on the gloda noun
+ * type used for the query is used, failing over to toString.
+ * @param aOrderVerifier Optional function to verify the order the results are
+ * received in. Function signature should be of the form (aZeroBasedIndex,
+ * aItem, aCollectionResultIsFor).
+ */
+async function queryExpect(
+ aQuery,
+ aExpectedSet,
+ aGlodaExtractor,
+ aExpectedExtractor,
+ aOrderVerifier
+) {
+ if (aQuery.test) {
+ aQuery = {
+ queryFunc: aQuery.getCollection,
+ queryThis: aQuery,
+ args: [],
+ nounId: aQuery._nounDef.id,
+ };
+ }
+
+ if ("synMessages" in aExpectedSet) {
+ aExpectedSet = aExpectedSet.synMessages;
+ }
+
+ // - set extractor functions to defaults if omitted
+ if (aGlodaExtractor == null) {
+ if (_defaultExpectationExtractors[aQuery.nounId] !== undefined) {
+ aGlodaExtractor = _defaultExpectationExtractors[aQuery.nounId][0];
+ } else {
+ aGlodaExtractor = expectExtract_default_toString;
+ }
+ }
+ if (aExpectedExtractor == null) {
+ if (_defaultExpectationExtractors[aQuery.nounId] !== undefined) {
+ aExpectedExtractor = _defaultExpectationExtractors[aQuery.nounId][1];
+ } else {
+ aExpectedExtractor = expectExtract_default_toString;
+ }
+ }
+
+ // - build the expected set
+ let expectedSet = {};
+ for (let item of aExpectedSet) {
+ try {
+ expectedSet[aExpectedExtractor(item)] = item;
+ } catch (ex) {
+ throw new Error(
+ "Expected extractor threw during query expectation for item:\n" +
+ item +
+ "\nException:\n" +
+ ex
+ );
+ }
+ }
+
+ // - create the listener...
+ let listener = new QueryExpectationListener(
+ expectedSet,
+ aGlodaExtractor,
+ aOrderVerifier,
+ Components.stack.caller
+ );
+ aQuery.args.push(listener);
+ let queryValue = aQuery.queryFunc.apply(aQuery.queryThis, aQuery.args);
+ // Wait for the QueryListener to finish.
+ await listener.promise;
+ return queryValue;
+}
+
+/**
+ * Asynchronously run a SQL statement against the gloda database. This can grow
+ * binding logic and data returning as needed.
+ *
+ * We run the statement asynchronously to get a consistent view of the database.
+ */
+async function sqlRun(sql) {
+ let conn = GlodaDatastore.asyncConnection;
+ let stmt = conn.createAsyncStatement(sql);
+ let rows = null;
+
+ let promiseResolve;
+ let promiseReject;
+ let promise = new Promise((resolve, reject) => {
+ promiseResolve = resolve;
+ promiseReject = reject;
+ });
+ // Running SQL.
+ stmt.executeAsync({
+ handleResult(aResultSet) {
+ if (!rows) {
+ rows = [];
+ }
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ rows.push(row);
+ }
+ },
+ handleError(aError) {
+ promiseReject(
+ new Error("SQL error!\nResult:\n" + aError + "\nSQL:\n" + sql)
+ );
+ },
+ handleCompletion() {
+ promiseResolve(rows);
+ },
+ });
+ stmt.finalize();
+ return promise;
+}
+
+/**
+ * Run an (async) SQL statement against the gloda database. The statement
+ * should be a SELECT COUNT; we check the count against aExpectedCount.
+ * Any additional arguments are positionally bound to the statement.
+ *
+ * We run the statement asynchronously to get a consistent view of the database.
+ */
+async function sqlExpectCount(aExpectedCount, aSQLString, ...params) {
+ let conn = GlodaDatastore.asyncConnection;
+ let stmt = conn.createStatement(aSQLString);
+
+ for (let iArg = 0; iArg < params.length; iArg++) {
+ GlodaDatastore._bindVariant(stmt, iArg, params[iArg]);
+ }
+
+ let desc = [aSQLString, ...params];
+ // Running SQL count.
+ let listener = new SqlExpectationListener(
+ aExpectedCount,
+ desc,
+ Components.stack.caller
+ );
+ stmt.executeAsync(listener);
+ // We don't need the statement anymore.
+ stmt.finalize();
+
+ await listener.promise;
+}
+
+class SqlExpectationListener {
+ constructor(aExpectedCount, aDesc, aCallerStackFrame) {
+ this.actualCount = null;
+ this.expectedCount = aExpectedCount;
+ this.sqlDesc = aDesc;
+ this.callerStackFrame = aCallerStackFrame;
+
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ }
+ handleResult(aResultSet) {
+ let row = aResultSet.getNextRow();
+ if (!row) {
+ this._reject(
+ new Error(
+ "No result row returned from caller:\n" +
+ this.callerStackFrame +
+ "\nSQL:\n" +
+ this.sqlDesc
+ )
+ );
+ return; // We don't have to continue for more checks.
+ }
+ this.actualCount = row.getInt64(0);
+ }
+
+ handleError(aError) {
+ this._reject(
+ new Error(
+ "SQL error from caller:\n" +
+ this.callerStackFrame +
+ "\nResult:\n" +
+ aError +
+ "\nSQL:\n" +
+ this.sqlDesc
+ )
+ );
+ }
+
+ handleCompletion(aReason) {
+ if (this.actualCount != this.expectedCount) {
+ this._reject(
+ new Error(
+ "Actual count of " +
+ this.actualCount +
+ "does not match expected count of:\n" +
+ this.expectedCount +
+ "\nFrom caller:" +
+ this.callerStackFrame +
+ "\nSQL:\n" +
+ this.sqlDesc
+ )
+ );
+ return; // We don't have to continue for more checks.
+ }
+ this._resolve();
+ }
+
+ get promise() {
+ return this._promise;
+ }
+}
diff --git a/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelper.jsm b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelper.jsm
new file mode 100644
index 0000000000..a4c092400b
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelper.jsm
@@ -0,0 +1,847 @@
+/* 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/. */
+
+/*
+ * This file provides gloda testing infrastructure.
+ *
+ * A few words about how tests should expect to interact with indexing:
+ *
+ * By default, we enable only event-driven indexing with an infinite work queue
+ * length. This means that all messages will be queued for indexing as they
+ * are added or modified. You should await to |waitForGlodaIndexer| to wait
+ * until the indexer completes. If you want to assert that certain messages
+ * will have been indexed during that pass, you can pass them as arguments to
+ * |assertExpectedMessagesIndexed|.
+ * There is no need to tell us to expect the messages to be indexed prior to the
+ * waiting as long as nothing spins the event loop after you perform the action
+ * that triggers indexing. None of our existing xpcshell tests do this, but it
+ * is part of the mozmill idiom for its waiting mechanism, so be sure to not
+ * perform a mozmill wait without first telling us to expect the messages.
+ */
+
+const EXPORTED_SYMBOLS = [
+ "assertExpectedMessagesIndexed",
+ "glodaTestHelperInitialize",
+ "nukeGlodaCachesAndCollections",
+ "prepareIndexerForTesting",
+ "waitForGlodaIndexer",
+];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+
+var log = console.createInstance({
+ prefix: "gloda.testHelper",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+var indexMessageState;
+
+/**
+ * Create a 'me' identity of "me@localhost" for the benefit of Gloda. At the
+ * time of this writing, Gloda only initializes Gloda.myIdentities and
+ * Gloda.myContact at startup with no event-driven updates. As such, this
+ * function needs to be called prior to gloda startup.
+ */
+function createMeIdentity() {
+ let identity = MailServices.accounts.createIdentity;
+ identity.email = "me@localhost";
+ identity.fullName = "Me";
+}
+// And run it now.
+createMeIdentity();
+
+// Set the gloda prefs.
+// "yes" to indexing.
+Services.prefs.setBoolPref("mailnews.database.global.indexer.enabled", true);
+// "no" to a sweep we don't control.
+Services.prefs.setBoolPref(
+ "mailnews.database.global.indexer.perform_initial_sweep",
+ false
+);
+
+var ENVIRON_MAPPINGS = [
+ {
+ envVar: "GLODA_DATASTORE_EXPLAIN_TO_PATH",
+ prefName: "mailnews.database.global.datastore.explainToPath",
+ },
+];
+
+// Propagate environment variables to prefs as appropriate:
+for (let { envVar, prefName } of ENVIRON_MAPPINGS) {
+ if (Services.env.exists(envVar)) {
+ Services.prefs.setCharPref(prefName, Services.env.get(envVar));
+ }
+}
+
+/**
+ * Side note:
+ * Keep them in the global scope so that a Cu.forceGC() call won't purge them.
+ */
+var collectionListener;
+
+/**
+ * Registers MessageInjection listeners and Gloda listeners for our tests.
+ *
+ * @param {MessageInjection} messageInjection Instance of MessageInjection
+ * to register Events to.
+ */
+function glodaTestHelperInitialize(messageInjection) {
+ // Initialize the message state if we are dealing with messages. At some
+ // point we probably want to just completely generalize the indexing state.
+ // That point is likely when our testing infrastructure needs the support
+ // provided by `indexMessageState` for things other than messages.
+ indexMessageState = new IndexMessageState();
+
+ collectionListener = new GlodaCollectionListener();
+ new TestAttributeProvider();
+ new MsgsClassifiedListener();
+
+ // Add a hook that makes folders not filthy when we first see them.
+ messageInjection.registerMessageInjectionListener({
+ /**
+ * By default all folders start out filthy. This is great in the real world
+ * but I went and wrote all the unit tests without entirely thinking about
+ * how this affected said unit tests. So we add a listener so that we can
+ * force the folders to be clean.
+ * This is okay and safe because messageInjection always creates the folders
+ * without any messages in them.
+ */
+ onRealFolderCreated(aRealFolder) {
+ log.debug(
+ `onRealFolderCreated through MessageInjection received. ` +
+ `Make folder: ${aRealFolder.name} clean for Gloda.`
+ );
+ let glodaFolder = Gloda.getFolderForFolder(aRealFolder);
+ glodaFolder._downgradeDirtyStatus(glodaFolder.kFolderClean);
+ },
+
+ /**
+ * Make waitForGlodaIndexer know that it should wait for a msgsClassified
+ * event whenever messages have been injected, at least if event-driven
+ * indexing is enabled.
+ */
+ onInjectingMessages() {
+ log.debug(
+ "onInjectingMessages through MessageInjection received. Pushing to intrestestingEvents."
+ );
+ indexMessageState.interestingEvents.push("msgsClassified");
+ },
+
+ /**
+ * This basically translates to "we are triggering an IMAP move" and has
+ * the ramification that we should expect a msgsClassified event because
+ * the destination will see the header get added at some point.
+ */
+ onMovingMessagesWithoutDestHeaders() {
+ log.debug(
+ "onMovingMessagesWithoutDestHeaders through MessageInjection received. Pushing to intrestestingEvents."
+ );
+ indexMessageState.interestingEvents.push("msgsClassified");
+ },
+ });
+ log.debug("glodaTestHelperInitialize finished.");
+}
+
+class IndexMessageState {
+ data = new GlodaIndexerData();
+
+ constructor() {
+ prepareIndexerForTesting();
+ // Continue the preparing by assigning the hook recover and hook cleanup.
+ GlodaIndexer._unitTestHookRecover = this._testHookRecover;
+ GlodaIndexer._unitTestHookCleanup = this._testHookCleanup;
+ }
+
+ resetData() {
+ this.data = new GlodaIndexerData();
+ }
+
+ // The synthetic message sets passed in to |assertExpectedMessagesIndexed|.
+ synMessageSets = [];
+ // The user-specified accumulate-style verification function.
+ verifier() {
+ return this.data.data.verifier;
+ }
+ // Should we augment the synthetic sets with gloda message info?
+ augmentSynSets() {
+ return this.data.data.augment;
+ }
+ deletionSynSets() {
+ return this.data.data.deleted;
+ }
+
+ // Expected value of |_workerRecoveredCount| at assertion time.
+ expectedWorkerRecoveredCount() {
+ return this.data.data.recovered;
+ }
+ // Expected value of |_workerFailedToRecoverCount| at assertion time.
+ expectedFailedToRecoverCount() {
+ return this.data.data.failedToRecover;
+ }
+ // Expected value of |_workerCleanedUpCount| at assertion time.
+ expectedCleanedUpCount() {
+ return this.data.data.cleanedUp;
+ }
+ // Expected value of |_workerHadNoCleanUpCount| at assertion time.
+ expectedHadNoCleanUpCount() {
+ return this.data.data.hadNoCleanUp;
+ }
+ /**
+ * The number of messages that were fully (re)indexed using
+ * Gloda.grokNounItem.
+ */
+ _numFullIndexed = 0;
+ // Expected value of |_numFullIndexed| at assertion time.
+ expectedNumFullIndexed() {
+ return this.data.data.fullyIndexed;
+ }
+
+ // The number of times a worker had a recover helper and it recovered.
+ _workerRecoveredCount = 0;
+ // The number of times a worker had a recover helper and it did not recover.
+ _workerFailedToRecoverCount = 0;
+ // The number of times a worker had a cleanup helper and it cleaned up.
+ _workerCleanedUpCount = 0;
+ // The number of times a worker had no cleanup helper but there was a cleanup.
+ _workerHadNoCleanUpCount = 0;
+
+ /**
+ * Beware this scoping for this class is lost where _testHookRecover is used.
+ *
+ * @param aRecoverResult
+ * @param aOriginEx
+ * @param aActiveJob
+ * @param aCallbackHandle
+ */
+ _testHookRecover(aRecoverResult, aOriginEx, aActiveJob, aCallbackHandle) {
+ log.debug(
+ "indexer recovery hook fired" +
+ "\nrecover result:\n" +
+ aRecoverResult +
+ "\noriginating exception:\n" +
+ aOriginEx +
+ "\nactive job:\n" +
+ aActiveJob +
+ "\ncallbackHandle:\n" +
+ indexMessageState._jsonifyCallbackHandleState(aCallbackHandle)
+ );
+ if (aRecoverResult) {
+ indexMessageState._workerRecoveredCount++;
+ } else {
+ indexMessageState._workerFailedToRecoverCount++;
+ }
+ }
+
+ /**
+ * Beware this scoping for this class is lost where _testHookCleanup is used.
+ *
+ * @param aHadCleanupFunc
+ * @param aOriginEx
+ * @param aActiveJob
+ * @param aCallbackHandle
+ */
+ _testHookCleanup(aHadCleanupFunc, aOriginEx, aActiveJob, aCallbackHandle) {
+ log.debug(
+ "indexer cleanup hook fired" +
+ "\nhad cleanup?\n" +
+ aHadCleanupFunc +
+ "\noriginating exception:\n" +
+ aOriginEx +
+ "\nactive job:\n" +
+ aActiveJob +
+ "\ncallbackHandle\n" +
+ indexMessageState._jsonifyCallbackHandleState(aCallbackHandle)
+ );
+ if (aHadCleanupFunc) {
+ indexMessageState._workerCleanedUpCount++;
+ } else {
+ indexMessageState._workerHadNoCleanUpCount++;
+ }
+ }
+ _jsonifyCallbackHandleState(aCallbackHandle) {
+ return {
+ _stringRep: aCallbackHandle.activeStack.length + " active generators",
+ activeStackLength: aCallbackHandle.activeStack.length,
+ contextStack: aCallbackHandle.contextStack,
+ };
+ }
+
+ /**
+ * The gloda messages indexed since the last call to |waitForGlodaIndexer|.
+ */
+ _glodaMessagesByMessageId = [];
+ _glodaDeletionsByMessageId = [];
+
+ _numItemsAdded = 0;
+
+ applyGlodaIndexerData(data) {
+ this.data.applyData(data);
+ }
+
+ /**
+ * A list of events that we need to see before we allow ourselves to perform
+ * the indexer check. For example, if "msgsClassified" is in here, it means
+ * that whether the indexer is active or not is irrelevant until we have
+ * seen that msgsClassified event.
+ */
+ interestingEvents = [];
+}
+
+function prepareIndexerForTesting() {
+ if (!GlodaIndexer.enabled) {
+ throw new Error(
+ "The gloda indexer is somehow not enabled. This is problematic."
+ );
+ }
+ // Make the indexer be more verbose about indexing for us.
+ GlodaIndexer._unitTestSuperVerbose = true;
+ GlodaMsgIndexer._unitTestSuperVerbose = true;
+ // Lobotomize the adaptive indexer.
+ // The indexer doesn't need to worry about load; zero his rescheduling time.
+ GlodaIndexer._INDEX_INTERVAL = 0;
+ // The indexer already registered for the idle service; we must remove this
+ // or "idle" notifications will still get sent via the observer mechanism.
+ let realIdleService = GlodaIndexer._idleService;
+ realIdleService.removeIdleObserver(
+ GlodaIndexer,
+ GlodaIndexer._indexIdleThresholdSecs
+ );
+ // Pretend we are always idle.
+ GlodaIndexer._idleService = {
+ idleTime: 1000,
+ addIdleObserver() {
+ // There is no actual need to register with the idle observer, and if
+ // we do, the stupid "idle" notification will trigger commits.
+ },
+ removeIdleObserver() {},
+ };
+ // We want the event-driven indexer to always handle indexing and never spill
+ // to an indexing sweep unless a test intentionally does so.
+ GlodaIndexer._indexMaxEventQueueMessages = 10000;
+ // Lobotomize the adaptive indexer's constants.
+ GlodaIndexer._cpuTargetIndexTime = 10000000;
+ GlodaIndexer._CPU_TARGET_INDEX_TIME_ACTIVE = 10000000;
+ GlodaIndexer._CPU_TARGET_INDEX_TIME_IDLE = 10000000;
+ GlodaIndexer._CPU_IS_BUSY_TIME = 10000000;
+ GlodaIndexer._PAUSE_LATE_IS_BUSY_TIME = 10000000;
+
+ delete GlodaIndexer._indexTokens;
+ GlodaIndexer.__defineGetter__("_indexTokens", function () {
+ return GlodaIndexer._CPU_MAX_TOKENS_PER_BATCH;
+ });
+ GlodaIndexer.__defineSetter__("_indexTokens", function () {});
+
+ // This includes making commits only happen when we the unit tests explicitly
+ // tell them to.
+ GlodaIndexer._MINIMUM_COMMIT_TIME = 10000000;
+ GlodaIndexer._MAXIMUM_COMMIT_TIME = 10000000;
+}
+
+class GlodaIndexerData {
+ data = {
+ verifier: null,
+ augment: false,
+ deleted: [],
+ fullyIndexed: null,
+
+ // Things should not be recovering or failing and cleaning up unless the test
+ // is expecting it.
+ recovered: 0,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ };
+
+ /**
+ * Applies data shallow.
+ * Only the first level of keys are applied and replaced complete
+ * if given via param data. No deep merge.
+ *
+ * @param {*} data
+ */
+ applyData(data) {
+ this.data = {
+ ...this.data,
+ ...data,
+ };
+ }
+}
+
+/**
+ * Note that if the indexer is not currently active we assume it has already
+ * completed; we do not entertain the possibility that it has not yet started.
+ * Since the indexer is 'active' as soon as it sees an event, this does mean
+ * that you need to wait to make sure the indexing event has happened before
+ * calling us. This is reasonable.
+ */
+async function waitForGlodaIndexer() {
+ let eventsPending = TestUtils.waitForCondition(() => {
+ if (indexMessageState.interestingEvents.length > 1) {
+ // Events still pending. See msgClassified event and
+ // messageInjection.registerMessageInjectionListener.
+ return false;
+ }
+ // Events finished.
+ return true;
+ });
+ let indexerRunning = TestUtils.waitForCondition(() => {
+ if (GlodaIndexer.indexing) {
+ // Still indexing.
+ return false;
+ }
+ // Indexing finished.
+ return true;
+ });
+
+ log.debug(
+ "waitForGlodaIndexer waiting for intrestingEvents and GlodaIndexer.indexing."
+ );
+
+ // If we are waiting on certain events to occur first, block on those.
+ await Promise.all([eventsPending, indexerRunning]);
+}
+
+/**
+ * Each time a msgClassified Event is fired and it is present
+ * in IndexMessageState.interestingEvents it will be removed.
+ */
+class MsgsClassifiedListener {
+ /**
+ * Events pending for the tests.
+ * (we want this to happen after gloda registers its own listener, and it
+ * does.)
+ */
+ constructor() {
+ MailServices.mfn.addListener(
+ this,
+ Ci.nsIMsgFolderNotificationService.msgsClassified
+ );
+ }
+ /**
+ * If this was an expected interesting event, remove it from the list.
+ * If an event happens that we did not expect, it does not matter. We know
+ * this because we add events we care about to interestingEvents before they
+ * can possibly be fired.
+ */
+ msgsClassified(aMsgHdrs, aJunkClassified, aTraitClassified) {
+ log.debug("MsgsClassifiedListener msgsClassified received.");
+ let idx = indexMessageState.interestingEvents.indexOf("msgsClassified");
+ if (idx != -1) {
+ log.debug("Remove intrestingEvent through msgsClassified.");
+ // Remove the interesting Event as we received it here.
+ indexMessageState.interestingEvents.splice(idx, 1);
+ }
+ }
+}
+
+/**
+ * This AttributeProvider helps us testing Gloda.
+ * With the `process` method the Collections will be noticed
+ * through listeners.
+ * (onItemsAdded, onItemsModified, onItemsRemoved, onQueryComplete)
+ */
+class TestAttributeProvider {
+ providerName = "glodaTestHelper:fakeProvider";
+ constructor() {
+ // Register us with gloda as an attribute provider so that we can
+ // distinguish between fully reindexed messages and fastpath indexed
+ // messages.
+ Gloda._attrProviderOrderByNoun[GlodaConstants.NOUN_MESSAGE].push({
+ providerName: this.providerName,
+ process: this.process,
+ });
+ }
+ /**
+ * Fake attribute provider processing function so we can distinguish
+ * between fully reindexed messages and fast-path modified messages.
+ * Process has to be invoked for the GlodaCollectionListener
+ */
+ *process(aItem, aRawReps, aIsConceptuallyNew, aCallbackHandle) {
+ indexMessageState._numFullIndexed++;
+
+ yield GlodaConstants.kWorkDone;
+ }
+}
+
+/**
+ * This class tracks a GlodaCollection (created by Gloda._wildcardCollection).
+ * The listeners for this collection which will notify our IndexMessageState
+ * are defined here.
+ */
+class GlodaCollectionListener {
+ // Our catch-all message collection that nets us all messages passing by.
+ catchAllCollection = null;
+ constructor() {
+ this.catchAllCollection = Gloda._wildcardCollection(
+ GlodaConstants.NOUN_MESSAGE
+ );
+ this.catchAllCollection.listener = this;
+ }
+ /*
+ * Our catch-all collection listener. Any time a new message gets indexed,
+ * we should receive an onItemsAdded call. Any time an existing message
+ * gets reindexed, we should receive an onItemsModified call. Any time an
+ * existing message actually gets purged from the system, we should receive
+ * an onItemsRemoved call.
+ */
+ onItemsAdded(aItems) {
+ log.debug("GlodaCollectionListener onItemsAdded received.");
+ for (let item of aItems) {
+ if (item.headerMessageID in indexMessageState._glodaMessagesByMessageId) {
+ throw new Error(
+ "Gloda message" +
+ item.folderMessage +
+ "already indexed once since the last waitForGlodaIndexer call!"
+ );
+ }
+ log.debug(
+ "GlodaCollectionListener save item to indexMessageState._glodaMessagesByMessageId."
+ );
+ indexMessageState._glodaMessagesByMessageId[item.headerMessageID] = item;
+ }
+
+ // Simulate some other activity clearing out the the current folder's
+ // cached database, which used to kill the indexer's enumerator.
+ if (++indexMessageState._numItemsAdded == 3) {
+ log.debug("GlodaCollectionListener simulate other activity.");
+ GlodaMsgIndexer._indexingFolder.msgDatabase = null;
+ }
+ }
+
+ onItemsModified(aItems) {
+ log.debug("GlodaCollectionListener onItemsModified received.");
+ for (let item of aItems) {
+ if (item.headerMessageID in indexMessageState._glodaMessagesByMessageId) {
+ throw new Error(
+ "Gloda message" +
+ item +
+ "already indexed once since the last waitForGlodaIndexer call!"
+ );
+ }
+ log.debug(
+ "GlodaCollectionListener save item to indexMessageState._glodaMessagesByMessageId."
+ );
+ indexMessageState._glodaMessagesByMessageId[item.headerMessageID] = item;
+ }
+ }
+
+ onItemsRemoved(aItems) {
+ log.debug("GlodaCollectionListener onItemsRemoved received.");
+ for (let item of aItems) {
+ if (
+ item.headerMessageID in indexMessageState._glodaDeletionsByMessageId
+ ) {
+ throw new Error(
+ "Gloda message " +
+ item +
+ "already deleted once since the last waitForGlodaIndexer call!"
+ );
+ }
+ log.debug(
+ "GlodaCollectionListener save item to indexMessageState._glodaDeletionsByMessageId."
+ );
+ indexMessageState._glodaDeletionsByMessageId[item.headerMessageID] = item;
+ }
+ }
+ onQueryComplete(aCollection) {
+ log.debug(
+ "GlodaCollectionListener onQueryComplete received. Nothing done."
+ );
+ }
+}
+
+/**
+ * Assert that the set of messages indexed is exactly the set passed in.
+ * If a verification function is provided, use it on a per-message basis
+ * to make sure the resulting gloda message looks like it should given the
+ * synthetic message.
+ *
+ * Throws Errors if something is not according and returns always [true, string]
+ * for `Assert.ok` in your tests. This ensures proper testing output.
+ *
+ * @param {SyntheticMessage[]} aSynMessageSets A list of SyntheticMessageSets
+ * containing exactly the messages we should expect to see.
+ * @param [aConfig.verifier] The function to call to verify that the indexing
+ * had the desired result. Takes arguments aSynthMessage (the synthetic
+ * message just indexed), aGlodaMessage (the gloda message representation of
+ * the indexed message), and aPreviousResult (the value last returned by the
+ * verifier function for this given set of messages, or undefined if it is
+ * the first message.)
+ * @param [aConfig.augment=false] Should we augment the synthetic message sets
+ * with references to their corresponding gloda messages? The messages
+ * will show up in a 'glodaMessages' list on the syn set.
+ * @param {SyntheticMessageSet[]} [aConfig.deleted] A list of SyntheticMessageSets
+ * containing messages that should be recognized as deleted by the gloda
+ * indexer in this pass.
+ * @param [aConfig.fullyIndexed] A count of the number of messages we expect
+ * to observe being fully indexed. This is relevant because in the case
+ * of message moves, gloda may generate an onItemsModified notification but
+ * not reindex the message. This attribute allows the tests to distinguish
+ * between the two cases.
+ * @returns {[true, string]}
+ */
+function assertExpectedMessagesIndexed(aSynMessageSets, aConfig) {
+ indexMessageState.synMessageSets = aSynMessageSets;
+
+ indexMessageState.applyGlodaIndexerData(aConfig);
+
+ // Check that we have a gloda message for every syn message and verify.
+ for (let msgSet of indexMessageState.synMessageSets) {
+ if (indexMessageState.augmentSynSets()) {
+ msgSet.glodaMessages = [];
+ }
+ for (let [iSynMsg, synMsg] of msgSet.synMessages.entries()) {
+ if (!(synMsg.messageId in indexMessageState._glodaMessagesByMessageId)) {
+ let msgHdr = msgSet.getMsgHdr(iSynMsg);
+ throw new Error(
+ "Header " +
+ msgHdr.messageId +
+ " in folder: " +
+ (msgHdr ? msgHdr.folder.name : "no header?") +
+ " should have been indexed."
+ );
+ }
+
+ let glodaMsg =
+ indexMessageState._glodaMessagesByMessageId[synMsg.messageId];
+ if (indexMessageState.augmentSynSets()) {
+ msgSet.glodaMessages.push(glodaMsg);
+ }
+
+ indexMessageState._glodaMessagesByMessageId[synMsg.messageId] = null;
+
+ let verifier = indexMessageState.verifier();
+ let previousValue = undefined;
+ if (verifier) {
+ try {
+ // Looking if a previous value have been present.
+ previousValue = verifier(synMsg, glodaMsg, previousValue);
+ } catch (ex) {
+ throw new Error(
+ "Verification failure: " +
+ synMsg +
+ " is not close enough to " +
+ glodaMsg +
+ "; basing this on exception: " +
+ ex
+ );
+ }
+ }
+ }
+ }
+
+ // Check that we don't have any extra gloda messages. (lacking syn msgs)
+ for (let messageId in indexMessageState._glodaMessagesByMessageId) {
+ let glodaMsg = indexMessageState._glodaMessagesByMessageId[messageId];
+ if (glodaMsg != null) {
+ throw new Error(
+ "Gloda message:\n" +
+ glodaMsg +
+ "\nShould not have been indexed.\n" +
+ "Source header:\n" +
+ glodaMsg.folderMessage
+ );
+ }
+ }
+
+ if (indexMessageState.deletionSynSets()) {
+ for (let msgSet of indexMessageState.deletionSynSets()) {
+ for (let synMsg of msgSet.synMessages) {
+ if (
+ !(synMsg.messageId in indexMessageState._glodaDeletionsByMessageId)
+ ) {
+ throw new Error(
+ "Synthetic message " + synMsg + " did not get deleted!"
+ );
+ }
+
+ indexMessageState._glodaDeletionsByMessageId[synMsg.messageId] = null;
+ }
+ }
+ }
+
+ // Check that we don't have unexpected deletions.
+ for (let messageId in indexMessageState._glodaDeletionsByMessageId) {
+ let glodaMsg = indexMessageState._glodaDeletionsByMessageId[messageId];
+ if (glodaMsg != null) {
+ throw new Error(
+ "Gloda message with message id " +
+ messageId +
+ " was " +
+ "unexpectedly deleted!"
+ );
+ }
+ }
+
+ if (
+ indexMessageState.expectedWorkerRecoveredCount() != null &&
+ indexMessageState.expectedWorkerRecoveredCount() !=
+ indexMessageState._workerRecoveredCount
+ ) {
+ throw new Error(
+ "Expected worker-recovered count did not match actual!\n" +
+ "Expected:\n" +
+ indexMessageState.expectedWorkerRecoveredCount() +
+ "\nActual:\n" +
+ indexMessageState._workerRecoveredCount
+ );
+ }
+ if (
+ indexMessageState.expectedFailedToRecoverCount() != null &&
+ indexMessageState.expectedFailedToRecoverCount() !=
+ indexMessageState._workerFailedToRecoverCount
+ ) {
+ throw new Error(
+ "Expected worker-failed-to-recover count did not match actual!\n" +
+ "Expected:\n" +
+ indexMessageState.expectedFailedToRecoverCount() +
+ "\nActual:\n" +
+ indexMessageState._workerFailedToRecoverCount
+ );
+ }
+ if (
+ indexMessageState.expectedCleanedUpCount() != null &&
+ indexMessageState.expectedCleanedUpCount() !=
+ indexMessageState._workerCleanedUpCount
+ ) {
+ throw new Error(
+ "Expected worker-cleaned-up count did not match actual!\n" +
+ "Expected:\n" +
+ indexMessageState.expectedCleanedUpCount() +
+ "\nActual:\n" +
+ indexMessageState._workerCleanedUpCount
+ );
+ }
+ if (
+ indexMessageState.expectedHadNoCleanUpCount() != null &&
+ indexMessageState.expectedHadNoCleanUpCount() !=
+ indexMessageState._workerHadNoCleanUpCount
+ ) {
+ throw new Error(
+ "Expected worker-had-no-cleanup count did not match actual!\n" +
+ "Expected:\n" +
+ indexMessageState.expectedHadNoCleanUpCount() +
+ "\nActual\n" +
+ indexMessageState._workerHadNoCleanUpCount
+ );
+ }
+
+ if (
+ indexMessageState.expectedNumFullIndexed() != null &&
+ indexMessageState.expectedNumFullIndexed() !=
+ indexMessageState._numFullIndexed
+ ) {
+ throw new Error(
+ "Expected number of fully indexed messages did not match.\n" +
+ "Expected:\n" +
+ indexMessageState.expectedNumFullIndexed() +
+ "\nActual:\n" +
+ indexMessageState._numFullIndexed
+ );
+ }
+
+ // Cleanup of internal tracking values in the IndexMessageState
+ // for new tests.
+ resetIndexMessageState();
+
+ // If no error has been thrown till here were fine!
+ // Return values for Assert.ok.
+ // Using like Assert.ok(...assertExpectedMessagesIndexed()).
+ return [true, "Expected messages were indexed."];
+}
+
+/**
+ * Resets the IndexMessageState
+ *
+ * @TODO more docs
+ */
+function resetIndexMessageState() {
+ indexMessageState.synMessageSets = [];
+ indexMessageState._glodaMessagesByMessageId = [];
+ indexMessageState._glodaDeletionsByMessageId = [];
+
+ indexMessageState._workerRecoveredCount = 0;
+ indexMessageState._workerFailedToRecoverCount = 0;
+ indexMessageState._workerCleanedUpCount = 0;
+ indexMessageState._workerHadNoCleanUpCount = 0;
+
+ indexMessageState._numFullIndexed = 0;
+ indexMessageState.resetData();
+}
+
+/**
+ * Wipe out almost everything from the clutches of the GlodaCollectionManager.
+ * By default, it is caching things and knows about all the non-GC'ed
+ * collections. Tests may want to ensure that their data is loaded from disk
+ * rather than relying on the cache, and so, we exist.
+ * The exception to everything is that Gloda's concept of myContact and
+ * myIdentities needs to have its collections still be reachable or invariants
+ * are in danger of being "de-invarianted".
+ * The other exception to everything are any catch-all-collections used by our
+ * testing/indexing process. We don't scan for them, we just hard-code their
+ * addition if they exist.
+ */
+function nukeGlodaCachesAndCollections() {
+ // Explode if the GlodaCollectionManager somehow doesn't work like we think it
+ // should. (I am reluctant to put this logic in there, especially because
+ // knowledge of the Gloda contact/identity collections simply can't be known
+ // by the colleciton manager.)
+ if (
+ GlodaCollectionManager._collectionsByNoun === undefined ||
+ GlodaCollectionManager._cachesByNoun === undefined
+ ) {
+ // We don't check the Gloda contact/identities things because they might not
+ // get initialized if there are no identities, which is the case for our
+ // unit tests right now...
+ throw new Error(
+ "Try and remember to update the testing infrastructure when you " +
+ "change things!"
+ );
+ }
+
+ // We can just blow away the known collections.
+ GlodaCollectionManager._collectionsByNoun = {};
+ // But then we have to put the myContact / myIdentities junk back.
+ if (Gloda._myContactCollection) {
+ GlodaCollectionManager.registerCollection(Gloda._myContactCollection);
+ GlodaCollectionManager.registerCollection(Gloda._myIdentitiesCollection);
+ }
+ // Don't forget our testing catch-all collection.
+ if (collectionListener.catchAllCollection) {
+ // Empty it out in case it has anything in it.
+ collectionListener.catchAllCollection.clear();
+ // And now we can register it.
+ GlodaCollectionManager.registerCollection(
+ collectionListener.catchAllCollection
+ );
+ }
+
+ // Caches aren't intended to be cleared, but we also don't want to lose our
+ // caches, so we need to create new ones from the ashes of the old ones.
+ let oldCaches = GlodaCollectionManager._cachesByNoun;
+ GlodaCollectionManager._cachesByNoun = {};
+ for (let nounId in oldCaches) {
+ let cache = oldCaches[nounId];
+ GlodaCollectionManager.defineCache(cache._nounDef, cache._maxCacheSize);
+ }
+}
diff --git a/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm
new file mode 100644
index 0000000000..f7a5199ba3
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm
@@ -0,0 +1,293 @@
+/* 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 = [
+ "configureGlodaIndexing",
+ "waitForGlodaDBFlush",
+ "waitForIndexingHang",
+ "resumeFromSimulatedHang",
+ "permuteMessages",
+ "makeABCardForAddressPair",
+];
+
+/*
+ * This file provides gloda testing infrastructure functions which are not coupled
+ * with the IndexMessageState from GlodaTestHelper.jsm
+ */
+
+var { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+var { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var { SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var log = console.createInstance({
+ prefix: "gloda.helperFunctions",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.loglevel",
+});
+
+/**
+ * Resume execution when the db has run all the async statements whose execution
+ * was queued prior to this call. We trigger a commit to accomplish this,
+ * although this could also be accomplished without a commit. (Though we would
+ * have to reach into GlodaDatastore.jsm and get at the raw connection or extend
+ * datastore to provide a way to accomplish this.)
+ */
+async function waitForGlodaDBFlush() {
+ // We already have a mechanism to do this by forcing a commit. Arguably,
+ // it would be better to use a mechanism that does not induce an fsync.
+ var savedDepth = GlodaDatastore._transactionDepth;
+ if (!savedDepth) {
+ GlodaDatastore._beginTransaction();
+ }
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ GlodaDatastore.runPostCommit(promiseResolve);
+ // We don't actually need to run things to zero. We can just wait for the
+ // outer transaction to close itself.
+ GlodaDatastore._commitTransaction();
+ if (savedDepth) {
+ GlodaDatastore._beginTransaction();
+ }
+ await promise;
+}
+
+/**
+ * An injected fault exception.
+ */
+function InjectedFault(aWhy) {
+ this.message = aWhy;
+}
+InjectedFault.prototype = {
+ toString() {
+ return "[InjectedFault: " + this.message + "]";
+ },
+};
+
+function _inject_failure_on_MsgHdrToMimeMessage() {
+ throw new InjectedFault("MsgHdrToMimeMessage");
+}
+
+let hangResolve;
+let hangPromise = new Promise(resolve => {
+ hangResolve = resolve;
+});
+
+function _simulate_hang_on_MsgHdrToMimeMessage(...aArgs) {
+ hangResolve([MsgHdrToMimeMessage, null, aArgs]);
+}
+
+/**
+ * If you have configured gloda to hang while indexing, this is the thing
+ * you wait on to make sure the indexer actually gets to the point where it
+ * hangs.
+ */
+async function waitForIndexingHang() {
+ await hangPromise;
+}
+
+/**
+ * Configure gloda indexing. For most settings, the settings get clobbered by
+ * the next time this method is called. Omitted settings reset to the defaults.
+ * However, anything labeled as a 'sticky' setting stays that way until
+ * explicitly changed.
+ *
+ * @param {boolean} [aArgs.event=true] Should event-driven indexing be enabled
+ * (true) or disabled (false)? Right now, this actually suppresses
+ * indexing... the semantics will be ironed out as-needed.
+ * @param [aArgs.hangWhile] Must be either omitted (for don't force a hang) or
+ * "streaming" indicating that we should do a no-op instead of performing
+ * the message streaming. This will manifest as a hang until
+ * |resumeFromSimulatedHang| is invoked or the test explicitly causes the
+ * indexer to abort (in which case you do not need to call the resume
+ * function.) You must omit injectFaultIn if you use hangWhile.
+ * @param [aArgs.injectFaultIn=null] Must be omitted (for don't inject a
+ * failure) or "streaming" indicating that we should inject a failure when
+ * the message indexer attempts to stream a message. The fault will be an
+ * appropriate exception. You must omit hangWhile if you use injectFaultIn.
+ */
+function configureGlodaIndexing(aArgs) {
+ let shouldSuppress = "event" in aArgs ? !aArgs.event : false;
+ if (shouldSuppress != GlodaIndexer.suppressIndexing) {
+ log.debug(`Setting suppress indexing to ${shouldSuppress}.`);
+ GlodaIndexer.suppressIndexing = shouldSuppress;
+ }
+
+ if ("hangWhile" in aArgs) {
+ log.debug(`Enabling hang injection in ${aArgs.hangWhile}.`);
+ switch (aArgs.hangWhile) {
+ case "streaming":
+ GlodaMsgIndexer._MsgHdrToMimeMessageFunc =
+ _simulate_hang_on_MsgHdrToMimeMessage;
+ break;
+ default:
+ throw new Error(
+ aArgs.hangWhile + " is not a legal choice for hangWhile"
+ );
+ }
+ } else if ("injectFaultIn" in aArgs) {
+ log.debug(`Enabling fault injection in ${aArgs.hangWhile}.`);
+ switch (aArgs.injectFaultIn) {
+ case "streaming":
+ GlodaMsgIndexer._MsgHdrToMimeMessageFunc =
+ _inject_failure_on_MsgHdrToMimeMessage;
+ break;
+ default:
+ throw new Error(
+ aArgs.injectFaultIn + " is not a legal choice for injectFaultIn"
+ );
+ }
+ } else {
+ if (GlodaMsgIndexer._MsgHdrToMimeMessageFunc != MsgHdrToMimeMessage) {
+ log.debug("Clearing hang/fault injection.");
+ }
+ GlodaMsgIndexer._MsgHdrToMimeMessageFunc = MsgHdrToMimeMessage;
+ }
+}
+
+/**
+ * Call this to resume from the hang induced by configuring the indexer with
+ * a "hangWhile" argument to |configureGlodaIndexing|.
+ *
+ * @param [aJustResumeExecution=false] Should we just poke the callback driver
+ * for the indexer rather than continuing the call. You would likely want
+ * to do this if you committed a lot of violence while in the simulated
+ * hang and proper resumption would throw exceptions all over the place.
+ * (For example; if you hang before streaming and destroy the message
+ * header while suspended, resuming the attempt to stream will throw.)
+ */
+async function resumeFromSimulatedHang(aJustResumeExecution) {
+ if (aJustResumeExecution) {
+ log.debug("Resuming from simulated hang with direct wrapper callback.");
+ GlodaIndexer._wrapCallbackDriver();
+ } else {
+ let [func, dis, args] = await hangPromise;
+ log.debug(`Resuming from simulated hang with call to: ${func.name}.`);
+ func.apply(dis, args);
+ }
+ // Reset the promise for the hang.
+ hangPromise = new Promise(resolve => {
+ hangResolve = resolve;
+ });
+}
+
+/**
+ * Prepares permutations for messages with aScenarioMaker. Be sure to wait for the indexer
+ * for every permutation and verify the result.
+ *
+ * This process is executed once for each possible permutation of observation
+ * of the synthetic messages. (Well, we cap it; brute-force test your logic
+ * on your own time; you should really only be feeding us minimal scenarios.)
+ *
+ * @param aScenarioMaker A function that, when called, will generate a series
+ * of SyntheticMessage instances. Each call to this method should generate
+ * a new set of conceptually equivalent, but not identical, messages. This
+ * allows us to process without having to reset our state back to nothing each
+ * time. (This is more to try and make sure we run the system with a 'dirty'
+ * state than a bid for efficiency.)
+ * @param {MessageInjection} messageInjection An instance to use for permuting
+ * the messages and creating folders.
+ *
+ * @returns {[async () => SyntheticMessageSet]} Await it sequentially with a for...of loop.
+ * Wait for each element for the Indexer and assert afterwards.
+ */
+async function permuteMessages(aScenarioMaker, messageInjection) {
+ let folder = await messageInjection.makeEmptyFolder();
+
+ // To calculate the permutations, we need to actually see what gets produced.
+ let scenarioMessages = aScenarioMaker();
+ let numPermutations = Math.min(factorial(scenarioMessages.length), 32);
+
+ let permutations = [];
+ for (let iPermutation = 0; iPermutation < numPermutations; iPermutation++) {
+ permutations.push(async () => {
+ log.debug(`Run permutation: ${iPermutation + 1} / ${numPermutations}`);
+ // If this is not the first time through, we need to create a new set.
+ if (iPermutation) {
+ scenarioMessages = aScenarioMaker();
+ }
+ scenarioMessages = permute(scenarioMessages, iPermutation);
+ let scenarioSet = new SyntheticMessageSet(scenarioMessages);
+ await messageInjection.addSetsToFolders([folder], [scenarioSet]);
+ return scenarioSet;
+ });
+ }
+ return permutations;
+}
+
+/**
+ * A simple factorial function used to calculate the number of permutations
+ * possible for a given set of messages.
+ */
+function factorial(i, rv) {
+ if (i <= 1) {
+ return rv || 1;
+ }
+ return factorial(i - 1, (rv || 1) * i); // tail-call capable
+}
+
+/**
+ * Permute an array given a 'permutation id' that is an integer that fully
+ * characterizes the permutation through the decisions that need to be made
+ * at each step.
+ *
+ * @param aArray Source array that is destructively processed.
+ * @param aPermutationId The permutation id. A permutation id of 0 results in
+ * the original array's sequence being maintained.
+ */
+function permute(aArray, aPermutationId) {
+ let out = [];
+ for (let i = aArray.length; i > 0; i--) {
+ let offset = aPermutationId % i;
+ out.push(aArray[offset]);
+ aArray.splice(offset, 1);
+ aPermutationId = Math.floor(aPermutationId / i);
+ }
+ return out;
+}
+
+/**
+ * Add a name-and-address pair as generated by `makeNameAndAddress` to the
+ * personal address book.
+ */
+function makeABCardForAddressPair(nameAndAddress) {
+ // XXX bug 314448 demands that we trigger creation of the ABs... If we don't
+ // do this, then the call to addCard will fail if someone else hasn't tickled
+ // this.
+ MailServices.ab.directories;
+
+ // kPABData is copied from abSetup.js
+ let kPABData = {
+ URI: "jsaddrbook://abook.sqlite",
+ };
+ let addressBook = MailServices.ab.getDirectory(kPABData.URI);
+
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = nameAndAddress[0];
+ card.primaryEmail = nameAndAddress[1];
+
+ // Just save the new node straight away.
+ addressBook.addCard(card);
+
+ log.debug(`Adding address book card for: ${nameAndAddress}`);
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_corrupt_database.js b/comm/mailnews/db/gloda/test/unit/test_corrupt_database.js
new file mode 100644
index 0000000000..ff186e871a
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_corrupt_database.js
@@ -0,0 +1,86 @@
+/* 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/. */
+
+/*
+ * This test does not use glodaTestHelper because:
+ * 1) We need to do things as part of the test without gloda having remotely
+ * thought about opening the database.
+ * 2) We expect and desire that the logger produce a warning and glodaTestHelper
+ * takes the view that warnings = death.
+ *
+ * We do use the rest of the test infrastructure though.
+ */
+
+// -- Do configure the gloda prefs though...
+// Yes to indexing.
+Services.prefs.setBoolPref("mailnews.database.global.indexer.enabled", true);
+// No to a sweep we don't control.
+Services.prefs.setBoolPref(
+ "mailnews.database.global.indexer.perform_initial_sweep",
+ false
+);
+
+// We'll start with this datastore ID, and make sure it gets overwritten
+// when the index is rebuilt.
+var kDatastoreIDPref = "mailnews.database.global.datastore.id";
+var kOriginalDatastoreID = "47e4bad6-fedc-4931-bf3f-d2f4146ac63e";
+Services.prefs.setCharPref(kDatastoreIDPref, kOriginalDatastoreID);
+
+/**
+ * Create an illegal=corrupt database and make sure that we log a message and
+ * still end up happy.
+ */
+add_task(function test_corrupt_databases_get_reported_and_blown_away() {
+ // - Get the file path.
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("global-messages-db.sqlite");
+
+ // - Protect dangerous people from themselves.
+ // (There should not be a database at this point; if there is one, we are
+ // not in the sandbox profile we expect. I wouldn't bother except we're
+ // going out of our way to write gibberish whereas gloda accidentally
+ // opening a valid database is bad but not horrible.)
+ if (dbFile.exists()) {
+ do_throw("There should not be a database at this point.");
+ }
+
+ // - Create the file.
+ dump("Creating gibberish file\n");
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(dbFile, -1, -1, 0);
+ let fileContents = "I'm in ur database not being a database.\n";
+ ostream.write(fileContents, fileContents.length);
+ ostream.close();
+
+ // - Init gloda, get warnings.
+ dump("Init gloda\n");
+ var { Gloda } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaPublic.jsm"
+ );
+ dump("Gloda inited, checking\n");
+
+ // - Make sure the datastore has an actual database.
+ let { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+ );
+
+ // Make sure that the datastoreID was overwritten
+ Assert.notEqual(Gloda.datastoreID, kOriginalDatastoreID);
+ // And for good measure, make sure that the pref was also overwritten
+ let currentDatastoreID = Services.prefs.getCharPref(kDatastoreIDPref);
+ Assert.notEqual(currentDatastoreID, kOriginalDatastoreID);
+ // We'll also ensure that the Gloda.datastoreID matches the one stashed
+ // in prefs...
+ Assert.equal(currentDatastoreID, Gloda.datastoreID);
+ // And finally, we'll make sure that the datastoreID is a string with length
+ // greater than 0.
+ Assert.equal(typeof Gloda.datastoreID, "string");
+ Assert.ok(Gloda.datastoreID.length > 0);
+
+ if (!GlodaDatastore.asyncConnection) {
+ do_throw("No database connection suggests no database!");
+ }
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_folder_logic.js b/comm/mailnews/db/gloda/test/unit/test_folder_logic.js
new file mode 100644
index 0000000000..6625258daa
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_folder_logic.js
@@ -0,0 +1,60 @@
+/* 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/. */
+
+/**
+ * Tests the gloda folder logic.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ // Tests in this file assume that returned folders are nsIMsgFolders and not
+ // handles which currently only local injection supports.
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Newly created folders should not be filthy (at least as long as they have
+ * nothing in them.)
+ */
+add_task(async function test_newly_created_folders_start_clean() {
+ let msgFolder = await messageInjection.makeEmptyFolder();
+ let glodaFolder = Gloda.getFolderForFolder(msgFolder);
+ Assert.equal(glodaFolder.dirtyStatus, glodaFolder.kFolderClean);
+});
+
+/**
+ * Deleted folders should not leave behind any mapping, and that mapping
+ * definitely should not interfere with a newly created folder of the same
+ * name.
+ */
+add_task(async function test_deleted_folder_tombstones_get_forgotten() {
+ let oldFolder = await messageInjection.makeEmptyFolder("volver");
+ let oldGlodaFolder = Gloda.getFolderForFolder(oldFolder);
+ messageInjection.deleteFolder(oldFolder);
+
+ // The tombstone needs to know it is deleted.
+ Assert.ok(oldGlodaFolder._deleted);
+
+ let newFolder = await messageInjection.makeEmptyFolder("volver");
+ let newGlodaFolder = Gloda.getFolderForFolder(newFolder);
+
+ // This folder better not be the same and better not think it is deleted.
+ Assert.notEqual(oldGlodaFolder, newGlodaFolder);
+ Assert.ok(!newGlodaFolder._deleted);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_fts3_tokenizer.js b/comm/mailnews/db/gloda/test/unit/test_fts3_tokenizer.js
new file mode 100644
index 0000000000..d938208c9b
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_fts3_tokenizer.js
@@ -0,0 +1,299 @@
+/* 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/. */
+
+/*
+ * This test file recycles part of test_intl.js. What we do is insert into the
+ * fulltext index two messages:
+ * - one has tokens 'aa' and 'bbb',
+ * - one is from a previous test and has CJK characters in it.
+ *
+ * We want to test that the behavior of the tokenizer is as expected (namely,
+ * that it drops two-letter tokens unless they're CJK bigrams), and that
+ * GlodaMsgSearcher.jsm properly drops two-letter tokens (unless CJK) from the search
+ * terms to avoid issuing a query that will definitely return no results.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { waitForGlodaDBFlush } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { queryExpect, sqlExpectCount } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+var { GlodaFolder } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDataModel.jsm"
+);
+var { GlodaMsgSearcher } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaMsgSearcher.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* ===== Tests ===== */
+
+/**
+ * To make the encoding pairs:
+ * - For the subject bit:
+ * import email
+ * h = email.Header.Header(charset=CHARSET)
+ * h.append(STRING)
+ * h.encode()
+ * - For the body bit
+ * s.encode(CHARSET)
+ */
+var intlPhrases = [
+ // -- CJK case
+ {
+ name: "CJK: Vending Machine",
+ actual: "\u81ea\u52d5\u552e\u8ca8\u6a5f",
+ encodings: {
+ "utf-8": [
+ "=?utf-8?b?6Ieq5YuV5ZSu6LKo5qmf?=",
+ "\xe8\x87\xaa\xe5\x8b\x95\xe5\x94\xae\xe8\xb2\xa8\xe6\xa9\x9f",
+ ],
+ },
+ searchPhrases: [
+ // Match bi-gram driven matches starting from the front.
+ { body: '"\u81ea\u52d5"', match: true },
+ ],
+ },
+ // -- Regular case. Make sure two-letter tokens do not match, since the
+ // tokenizer is supposed to drop them. Also make sure that a three-letter
+ // token matches.
+ {
+ name: "Boring ASCII",
+ actual: "aa bbb",
+ encodings: {
+ "utf-8": ["=?utf-8?q?aa_bbb?=", "aa bbb"],
+ },
+ searchPhrases: [
+ { body: "aa", match: false },
+ { body: "bbb", match: true },
+ ],
+ },
+];
+
+var msgGen;
+var messageInjection;
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+add_task(async function test_index_cjk() {
+ await indexPhrase(intlPhrases[0]);
+});
+
+add_task(async function test_index_regular() {
+ await indexPhrase(intlPhrases[1]);
+});
+
+/**
+ * - Check that the 'aa' token was never emitted (we don't emit two-letter
+ * tokens unless they're CJK).
+ * - Check that the '\u81ea\u52d5' token was emitted, because it's CJK.
+ * - Check that the 'bbb' token was duly emitted (three letters is more than two
+ * letters so it's tokenized).
+ */
+add_task(async function test_token_count() {
+ // Force a db flush so I can investigate the database if I want.
+ await waitForGlodaDBFlush();
+ await sqlExpectCount(
+ 0,
+ "SELECT COUNT(*) FROM messagesText where messagesText MATCH 'aa'"
+ );
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) FROM messagesText where messagesText MATCH 'bbb'"
+ );
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) FROM messagesText where messagesText MATCH '\u81ea\u52d5'"
+ );
+});
+
+add_task(async function test_fulltextsearch_cjk() {
+ await test_fulltextsearch(intlPhrases[0]);
+});
+
+add_task(async function test_fulltextsearch_regular() {
+ await test_fulltextsearch(intlPhrases[1]);
+});
+
+/**
+ * We make sure that the Gloda module that builds the query drops two-letter
+ * tokens, otherwise this would result in an empty search (no matches for
+ * two-letter tokens).
+ */
+add_task(async function test_query_builder() {
+ // aa should be dropped, and we have one message containing the bbb token.
+ await msgSearchExpectCount(1, "aa bbb");
+ // The CJK part should not be dropped, and match message 1; the bbb token
+ // should not be dropped, and match message 2; 0 results returned because no
+ // message has the two tokens in it.
+ await msgSearchExpectCount(0, "\u81ea\u52d5 bbb");
+});
+
+/**
+ * For each phrase in the intlPhrases array (we are parameterized over it using
+ * parameterizeTest in the 'tests' declaration), create a message where the
+ * subject, body, and attachment name are populated using the encodings in
+ * the phrase's "encodings" attribute, one encoding per message. Make sure
+ * that the strings as exposed by the gloda representation are equal to the
+ * expected/actual value.
+ * Stash each created synthetic message in a resultList list on the phrase so
+ * that we can use them as expected query results in
+ * |test_fulltextsearch|.
+ */
+async function indexPhrase(aPhrase) {
+ // Create a synthetic message for each of the delightful encoding types.
+ let messages = [];
+ aPhrase.resultList = [];
+ for (let charset in aPhrase.encodings) {
+ let [quoted, bodyEncoded] = aPhrase.encodings[charset];
+
+ let smsg = msgGen.makeMessage({
+ subject: quoted,
+ body: { charset, encoding: "8bit", body: bodyEncoded },
+ attachments: [{ filename: quoted, body: "gabba gabba hey" }],
+ // Save off the actual value for checking.
+ callerData: [charset, aPhrase.actual],
+ });
+
+ messages.push(smsg);
+ aPhrase.resultList.push(smsg);
+ }
+ let synSet = new SyntheticMessageSet(messages);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([synSet], { verifier: verify_index })
+ );
+}
+
+/**
+ * Does the per-message verification for indexPhrase. Knows what is right for
+ * each message because of the callerData attribute on the synthetic message.
+ */
+function verify_index(smsg, gmsg) {
+ let [charset, actual] = smsg.callerData;
+ let subject = gmsg.subject;
+ let indexedBodyText = gmsg.indexedBodyText.trim();
+ let attachmentName = gmsg.attachmentNames[0];
+ dump("Using character set:\n" + charset + "\nActual:\n" + actual + "\n");
+ dump("Subject:\n" + subject + "\nSubject length:\n" + subject.length + "\n");
+ Assert.equal(actual, subject);
+ dump("Body: " + indexedBodyText + " (len: " + indexedBodyText.length + ")\n");
+ Assert.equal(actual, indexedBodyText);
+ dump(
+ "Attachment name:" +
+ attachmentName +
+ " (len: " +
+ attachmentName.length +
+ ")\n"
+ );
+ Assert.equal(actual, attachmentName);
+}
+
+/**
+ * For each phrase, make sure that all of the searchPhrases either match or fail
+ * to match as appropriate.
+ */
+async function test_fulltextsearch(aPhrase) {
+ for (let searchPhrase of aPhrase.searchPhrases) {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.bodyMatches(searchPhrase.body);
+ await queryExpect(query, searchPhrase.match ? aPhrase.resultList : []);
+ }
+}
+
+/**
+ * Pass a query string to the GlodaMsgSearcher, run the corresponding SQL query,
+ * and check the resulted count is what we want.
+ *
+ * Use like so:
+ * await msgSearchExpectCount(1, "I like cheese");
+ */
+async function msgSearchExpectCount(aCount, aFulltextStr) {
+ // Let the GlodaMsgSearcher build its query
+ let searcher = new GlodaMsgSearcher(null, aFulltextStr);
+ let conn = GlodaDatastore.asyncConnection;
+ let query = searcher.buildFulltextQuery();
+
+ // Brace yourself, brutal monkey-patching NOW
+ let sql, args;
+ let oldFunc = GlodaDatastore._queryFromSQLString;
+ GlodaDatastore._queryFromSQLString = function (aSql, aArgs) {
+ sql = aSql;
+ args = aArgs;
+ };
+ query.getCollection();
+ GlodaDatastore._queryFromSQLString = oldFunc;
+
+ // Bind the parameters
+ let stmt = conn.createStatement(sql);
+ for (let [iBinding, bindingValue] of args.entries()) {
+ GlodaDatastore._bindVariant(stmt, iBinding, bindingValue);
+ }
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ let i = 0;
+ stmt.executeAsync({
+ handleResult(aResultSet) {
+ for (
+ let row = aResultSet.getNextRow();
+ row;
+ row = aResultSet.getNextRow()
+ ) {
+ i++;
+ }
+ },
+
+ handleError(aError) {
+ do_throw(new Error("Error: " + aError.message));
+ },
+
+ handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ do_throw(new Error("Query canceled or aborted!"));
+ }
+
+ if (i != aCount) {
+ throw new Error(
+ "Didn't get the expected number of rows: got " +
+ i +
+ " expected " +
+ aCount +
+ " SQL: " +
+ sql
+ );
+ }
+ promiseResolve();
+ },
+ });
+ stmt.finalize();
+ await promise;
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_gloda_content_imap_offline.js b/comm/mailnews/db/gloda/test/unit/test_gloda_content_imap_offline.js
new file mode 100644
index 0000000000..3c59de4233
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_gloda_content_imap_offline.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+/**
+ * Tests the operation of the GlodaContent (in GlodaContent.jsm) and its exposure
+ * via Gloda.getMessageContent for IMAP messages that are offline.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_gloda_content.js */
+load("base_gloda_content.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: true },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_gloda_content_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_gloda_content_local.js b/comm/mailnews/db/gloda/test/unit/test_gloda_content_local.js
new file mode 100644
index 0000000000..f02a6750b4
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_gloda_content_local.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+/**
+ * Tests the operation of the GlodaContent (in GlodaContent.jsm) and its exposure
+ * via Gloda.getMessageContent for local messages.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_gloda_content.js */
+load("base_gloda_content.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_gloda_content_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_addressbook.js b/comm/mailnews/db/gloda/test/unit/test_index_addressbook.js
new file mode 100644
index 0000000000..9d0b0d4103
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_addressbook.js
@@ -0,0 +1,139 @@
+/* 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/. */
+
+/**
+ * Check that events update identity._hasAddressBookCard correctly.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ nukeGlodaCachesAndCollections,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaCollectionManager } = ChromeUtils.import(
+ "resource:///modules/gloda/Collection.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var EMAIL_ADDRESS = "all.over@the.world.invalid";
+var DISPLAY_NAME = "every day";
+
+var messageInjection;
+
+add_setup(function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Create an e-mail so the identity can exist.
+ */
+add_setup(async function () {
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [messageInjection.getInboxFolder()],
+ [{ count: 1, from: [DISPLAY_NAME, EMAIL_ADDRESS] }]
+ );
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+
+ // Okay, but it knows it has no card because indexing thinks stuff.
+ // So let's flush all caches and create a query that just knows about the
+ // identity.
+ nukeGlodaCachesAndCollections();
+
+ let identQuery = Gloda.newQuery(GlodaConstants.NOUN_IDENTITY);
+ identQuery.kind("email");
+ identQuery.value(EMAIL_ADDRESS);
+ await queryExpect(identQuery, [EMAIL_ADDRESS]);
+
+ // Now the identity exists. Make sure it is in cache.
+ let identity = get_cached_gloda_identity_for_email(EMAIL_ADDRESS);
+ Assert.notEqual(identity, null);
+
+ // And make sure it has no idea what the current state of the card is.
+ if (identity._hasAddressBookCard !== undefined) {
+ do_throw(
+ "We should have no idea about the state of the ab card, but " +
+ "it's: " +
+ identity._hasAddressBookCard
+ );
+ }
+});
+
+/**
+ * Add a card for that e-mail, make sure we update the cached identity ab
+ * card state.
+ */
+add_task(function test_add_card_cache_indication() {
+ add_card(EMAIL_ADDRESS, DISPLAY_NAME);
+
+ let identity = get_cached_gloda_identity_for_email(EMAIL_ADDRESS);
+ Assert.equal(identity._hasAddressBookCard, true);
+});
+
+/**
+ * Remove the card we added in setup, make sure we update the cached identity
+ * ab card state.
+ */
+add_task(function test_remove_card_cache_indication() {
+ delete_card(EMAIL_ADDRESS);
+
+ let identity = get_cached_gloda_identity_for_email(EMAIL_ADDRESS);
+ Assert.equal(identity._hasAddressBookCard, false);
+});
+
+/**
+ * Add again a card for that e-mail, make sure we update the cached identity ab
+ * card state.
+ */
+add_task(function test_add_card_cache_indication() {
+ add_card(EMAIL_ADDRESS, DISPLAY_NAME);
+
+ let identity = get_cached_gloda_identity_for_email(EMAIL_ADDRESS);
+ Assert.equal(identity._hasAddressBookCard, true);
+});
+
+function add_card(aEmailAddress, aDisplayName) {
+ Cc["@mozilla.org/addressbook/services/addressCollector;1"]
+ .getService(Ci.nsIAbAddressCollector)
+ .collectSingleAddress(aEmailAddress, aDisplayName, true, true);
+}
+
+function get_card_for_email(aEmailAddress) {
+ for (let book of MailServices.ab.directories) {
+ let card = book.cardForEmailAddress(aEmailAddress);
+ if (card) {
+ return [book, card];
+ }
+ }
+ return [null, null];
+}
+
+function delete_card(aEmailAddress) {
+ let [book, card] = get_card_for_email(aEmailAddress);
+
+ MailServices.ab.getDirectory(book.URI).deleteCards([card]);
+}
+
+function get_cached_gloda_identity_for_email(aEmailAddress) {
+ return GlodaCollectionManager.cacheLookupOneByUniqueValue(
+ GlodaConstants.NOUN_IDENTITY,
+ "email@" + aEmailAddress.toLowerCase()
+ );
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_bad_messages.js b/comm/mailnews/db/gloda/test/unit/test_index_bad_messages.js
new file mode 100644
index 0000000000..5920ac981e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_bad_messages.js
@@ -0,0 +1,210 @@
+/* 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/. */
+
+/*
+ * Test that we fail on bad messages by marking the messages as bad rather than
+ * exploding or something bad like that.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { configureGlodaIndexing } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+const GLODA_BAD_MESSAGE_ID = 2;
+
+var illegalMessageTemplates = [
+ // -- Authors
+ {
+ name: "no author",
+ clobberHeaders: {
+ From: "",
+ },
+ },
+ {
+ name: "too many authors (> 1)",
+ clobberHeaders: {
+ From: "Tweedle Dee <dee@example.com>, Tweedle Dum <dum@example.com>",
+ },
+ },
+];
+
+var messageInjection;
+
+add_setup(function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+add_task(async function test_illegal_message_no_author() {
+ await illegal_message(illegalMessageTemplates[0]);
+});
+add_task(async function test_illegal_message_too_many_authors() {
+ await illegal_message(illegalMessageTemplates[1]);
+});
+
+/**
+ * A byzantine failure to stream should not sink us. Fake a failure.
+ */
+add_task(async function test_streaming_failure() {
+ configureGlodaIndexing({ injectFaultIn: "streaming" });
+
+ // Inject the messages.
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [messageInjection.getInboxFolder()],
+ [{ count: 1 }]
+ );
+
+ // Indexing should complete without actually indexing the message.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 1,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+
+ // Make sure the header has the expected gloda bad message state.
+ let msgHdr = msgSet.getMsgHdr(0);
+ Assert.equal(msgHdr.getUint32Property("gloda-id"), GLODA_BAD_MESSAGE_ID);
+
+ // Make sure gloda does not think the message is indexed
+ Assert.equal(Gloda.isMessageIndexed(msgHdr), false);
+
+ configureGlodaIndexing({});
+});
+
+/**
+ * If we have one bad message followed by a good message, the good message
+ * should still get indexed. Additionally, if we do a sweep on the folder,
+ * we should not attempt to index the message again.
+ */
+add_task(async function test_recovery_and_no_second_attempts() {
+ let [, goodSet] = await messageInjection.makeNewSetsInFolders(
+ [messageInjection.getInboxFolder()],
+ [{ count: 1, clobberHeaders: { From: "" } }, { count: 1 }]
+ );
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([goodSet], { recovered: 1 }));
+
+ // Index the folder; no messages should get indexed and there should be no
+ // failure things.
+ GlodaMsgIndexer.indexFolder(messageInjection.getInboxFolder());
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 0,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+});
+
+/**
+ * Make sure that we attempt to reindex a dirty bad message and that when we
+ * fail that we clear the dirty bit.
+ */
+add_task(async function test_reindex_on_dirty_clear_dirty_on_fail() {
+ // Inject a new illegal message
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [messageInjection.getInboxFolder()],
+ [
+ {
+ count: 1,
+ clobberHeaders: illegalMessageTemplates[0].clobberHeaders,
+ },
+ ]
+ );
+
+ // Indexing should complete without actually indexing the message.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 1,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+
+ // Mark the message dirty, force the folder to be indexed.
+ let msgHdr = msgSet.getMsgHdr(0);
+ msgHdr.setUint32Property("gloda-dirty", 1);
+ GlodaMsgIndexer.indexFolder(messageInjection.getInboxFolder());
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 1,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+ // Now the message should be clean.
+ Assert.equal(msgHdr.getUint32Property("gloda-dirty"), 0);
+
+ // Check again with filthy.
+ msgHdr.setUint32Property("gloda-dirty", 2);
+ GlodaMsgIndexer.indexFolder(messageInjection.getInboxFolder());
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 1,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+ // Now the message should be clean.
+ Assert.equal(msgHdr.getUint32Property("gloda-dirty"), 0);
+});
+
+/**
+ * Using exciting templates from |illegalMessageTemplates|, verify that gloda
+ * fails to index them and marks the messages bad.
+ */
+async function illegal_message(aInfo) {
+ // Inject the messages.
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [messageInjection.getInboxFolder()],
+ [{ count: 1, clobberHeaders: aInfo.clobberHeaders }]
+ );
+
+ // Indexing should complete without actually indexing the message.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([], {
+ recovered: 1,
+ failedToRecover: 0,
+ cleanedUp: 0,
+ hadNoCleanUp: 0,
+ })
+ );
+
+ // Make sure the header has the expected gloda bad message state.
+ let msgHdr = msgSet.getMsgHdr(0);
+ Assert.equal(msgHdr.getUint32Property("gloda-id"), GLODA_BAD_MESSAGE_ID);
+
+ // Make sure gloda does not think the message is indexed.
+ Assert.equal(Gloda.isMessageIndexed(msgHdr), false);
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_compaction.js b/comm/mailnews/db/gloda/test/unit/test_index_compaction.js
new file mode 100644
index 0000000000..7b6923ab61
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_compaction.js
@@ -0,0 +1,395 @@
+/* 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/. */
+
+/*
+ * Test that gloda does the right things in terms of compaction. Major cases:
+ *
+ * - Compaction occurs while we are in the process of indexing a folder. We
+ * want to make sure we stop indexing cleanly
+ *
+ * - A folder that we have already indexed gets compacted. We want to make sure
+ * that we update the message keys for all involved. This means verifying
+ * that both the on-disk representations and in-memory representations are
+ * correct.
+ *
+ * - Make sure that an indexing sweep performs a compaction pass if we kill the
+ * compaction job automatically scheduled by the conclusion of the
+ * compaction. (Simulating the user quitting before all compactions have
+ * been processed.)
+ *
+ * - Moves/deletes that happen after a compaction but before we process the
+ * compaction generate a special type of edge case that we need to check.
+ *
+ * There is also a less interesting case:
+ *
+ * - Make sure that the indexer does not try and start indexing a folder that is
+ * in the process of being compacted.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var {
+ configureGlodaIndexing,
+ resumeFromSimulatedHang,
+ waitForGlodaDBFlush,
+ waitForIndexingHang,
+} = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+add_setup(function () {
+ /*
+ * All the rest of the gloda tests (should) work with maildir, but this test
+ * only works/makes sense with mbox, so force it to always use mbox. This
+ * allows developers to manually change the default to maildir and have the
+ * gloda tests run with that.
+ */
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+ );
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+add_task(async function compaction_indexing_pass_none_pending_commit() {
+ await compaction_indexing_pass({
+ name: "none pending commit",
+ forceCommit: true,
+ });
+});
+add_task(async function compaction_indexing_pass_all_pending_commit() {
+ await compaction_indexing_pass({
+ name: "all pending commit",
+ forceCommit: false,
+ });
+});
+
+/**
+ * Make sure that an indexing sweep performs a compaction pass if we kill the
+ * compaction job automatically scheduled by the conclusion of the compaction.
+ * (Simulating the user quitting before all compactions have been processed.)
+ */
+add_task(async function test_sweep_performs_compaction() {
+ let [[folder], moveSet, staySet] = await messageInjection.makeFoldersWithSets(
+ 1,
+ [{ count: 1 }, { count: 1 }]
+ );
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([moveSet, staySet], { augment: true })
+ );
+
+ // Move the message to another folder.
+ let otherFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.moveMessages(moveSet, otherFolder);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([moveSet]));
+
+ // Disable event-driven indexing so there is no way the compaction job can
+ // get worked.
+ configureGlodaIndexing({ event: false });
+
+ // Compact.
+ let msgFolder = messageInjection.getRealInjectionFolder(folder);
+ dump(
+ "Triggering compaction " +
+ "Folder: " +
+ msgFolder.name +
+ " Gloda folder: " +
+ Gloda.getFolderForFolder(msgFolder) +
+ "\n"
+ );
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.compact(urlListener, null);
+ await urlListener.promise;
+
+ // Erase the compaction job.
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+
+ // Make sure the folder is marked compacted.
+ let glodaFolder = Gloda.getFolderForFolder(msgFolder);
+ Assert.ok(glodaFolder.compacted);
+
+ // Re-enable indexing and fire up an indexing pass.
+ configureGlodaIndexing({ event: true });
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // Make sure the compaction happened.
+ verify_message_keys(staySet);
+});
+
+/**
+ * Make sure that if we compact a folder then move messages out of it and/or
+ * delete messages from it before its compaction pass happens that the
+ * compaction pass properly marks the messages deleted.
+ */
+add_task(
+ async function test_moves_and_deletions_on_compacted_folder_edge_case() {
+ let [[folder], compactMoveSet, moveSet, delSet, staySet] =
+ await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ { count: 1 },
+ { count: 1 },
+ { count: 1 },
+ ]);
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed(
+ [compactMoveSet, moveSet, delSet, staySet],
+ {
+ augment: true,
+ }
+ )
+ );
+
+ // Move the message to another folder.
+ let otherFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.moveMessages(compactMoveSet, otherFolder);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([compactMoveSet]));
+
+ // Disable indexing because we don't want to process the compaction.
+ configureGlodaIndexing({ event: false });
+
+ // Compact the folder.
+ let msgFolder = messageInjection.getRealInjectionFolder(folder);
+ dump(
+ "Triggering compaction " +
+ "Folder: " +
+ msgFolder.name +
+ " Gloda folder: " +
+ Gloda.getFolderForFolder(msgFolder) +
+ "\n"
+ );
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.compact(urlListener, null);
+ await urlListener.promise;
+
+ // Erase the compaction job.
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+
+ // - Delete
+ // Because of the compaction, the PendingCommitTracker forgot that the message
+ // we are deleting got indexed; we will receive no event.
+ await MessageInjection.deleteMessages(delSet);
+
+ // - Move
+ // Same deal on the move, except that it will try and trigger event-based
+ // indexing in the target folder...
+ await messageInjection.moveMessages(moveSet, otherFolder);
+ // Kill the event-based indexing job of the target; we want the indexing sweep
+ // to see it as a move.
+ dump("killing all indexing jobs\n");
+ GlodaIndexer.purgeJobsUsingFilter(() => true);
+
+ // - Indexing pass
+ // Re-enable indexing so we can do a sweep.
+ configureGlodaIndexing({ event: true });
+
+ // This will trigger compaction (per the previous unit test) which should mark
+ // moveSet and delSet as deleted. Then it should happen in to the next
+ // folder and add moveSet again...
+ dump("triggering indexing sweep\n");
+ GlodaMsgIndexer.indexingSweepNeeded = true;
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([moveSet], {
+ deleted: [moveSet, delSet],
+ })
+ );
+
+ // Sanity check the compaction for giggles.
+ verify_message_keys(staySet);
+ }
+);
+
+/**
+ * Induce a compaction while we are in the middle of indexing. Make sure we
+ * clean up and that the folder ends
+ *
+ * Note that in order for compaction to happen there has to be something for
+ * compaction to do, so our prep involves moving a message to another folder.
+ * (Deletion actually produces more legwork for gloda whereas a local move is
+ * almost entirely free.)
+ */
+add_task(async function test_compaction_interrupting_indexing() {
+ // Create a folder with a message inside.
+ let [[folder], compactionFodderSet] =
+ await messageInjection.makeFoldersWithSets(1, [{ count: 1 }]);
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([compactionFodderSet]));
+
+ // Move that message to another folder.
+ let otherFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.moveMessages(compactionFodderSet, otherFolder);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([compactionFodderSet]));
+
+ // Configure the gloda indexer to hang while streaming the message.
+ configureGlodaIndexing({ hangWhile: "streaming" });
+
+ // Create a folder with a message inside.
+ let [msgSet] = await messageInjection.makeNewSetsInFolders(
+ [folder],
+ [{ count: 1 }]
+ );
+
+ await waitForIndexingHang();
+
+ // Compact! This should kill the job and because of the compaction; no other
+ // reason should be able to do this.
+ let msgFolder = messageInjection.getRealInjectionFolder(folder);
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.compact(urlListener, null);
+ await urlListener.promise;
+
+ // Reset indexing to not hang.
+ configureGlodaIndexing({});
+
+ // Sorta get the event chain going again.
+ await resumeFromSimulatedHang(true);
+
+ // Because the folder was dirty it should actually end up getting indexed,
+ // so in the end the message will get indexed.
+ // Also, make sure a cleanup was observed.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { cleanedUp: 1 }));
+});
+
+/**
+ *
+ */
+add_task(async function test_do_not_enter_compacting_folders() {
+ // Turn off indexing.
+ configureGlodaIndexing({ event: false });
+
+ // Create a folder with a message inside.
+ let [[folder]] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ // Lie and claim we are compacting that folder.
+ let glodaFolder = Gloda.getFolderForFolder(
+ messageInjection.getRealInjectionFolder(folder)
+ );
+ glodaFolder.compacting = true;
+
+ // Now try and force ourselves to index that folder and its message.
+ // Turn back on indexing.
+ configureGlodaIndexing({ event: true });
+
+ // Verify that the indexer completes without having indexed anything.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+});
+
+/**
+ * Verify that the message keys match between the message headers and the
+ * (augmented on) gloda messages that correspond to the headers.
+ */
+function verify_message_keys(aSynSet) {
+ let iMsg = 0;
+ for (let msgHdr of aSynSet.msgHdrs()) {
+ let glodaMsg = aSynSet.glodaMessages[iMsg++];
+ if (msgHdr.messageKey != glodaMsg.messageKey) {
+ throw new Error(
+ "Message header " +
+ msgHdr +
+ " should have message key " +
+ msgHdr.messageKey +
+ " but has key " +
+ glodaMsg.messageKey +
+ " per gloda msg " +
+ glodaMsg
+ );
+ }
+ }
+ dump("verified message keys after compaction\n");
+}
+
+/**
+ * Compact a folder that we were not indexing. Make sure gloda's representations
+ * get updated to the new message keys.
+ *
+ * This is parameterized because the logic has special cases to deal with
+ * messages that were pending commit that got blown away.
+ */
+async function compaction_indexing_pass(aParam) {
+ // Create 5 messages. We will move just the third message so the first two
+ // message keep their keys and the last two change. (We want 2 for both
+ // cases to avoid edge cases.)
+ let [[folder], sameSet, moveSet, shiftSet] =
+ await messageInjection.makeFoldersWithSets(1, [
+ { count: 2 },
+ { count: 1 },
+ { count: 2 },
+ ]);
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([sameSet, moveSet, shiftSet], {
+ augment: true,
+ })
+ );
+
+ // Move the message to another folder.
+ let otherFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.moveMessages(moveSet, otherFolder);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([moveSet]));
+
+ if (aParam.forceCommit) {
+ await waitForGlodaDBFlush();
+ }
+
+ // Compact the folder.
+ let msgFolder = messageInjection.getRealInjectionFolder(folder);
+ dump(
+ "Triggering compaction " +
+ "Folder: " +
+ msgFolder.name +
+ " Gloda folder: " +
+ Gloda.getFolderForFolder(msgFolder) +
+ "\n"
+ );
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.compact(urlListener, null);
+ await urlListener.promise;
+ // Wait for the compaction job to complete.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ verify_message_keys(sameSet);
+ verify_message_keys(shiftSet);
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_offline.js b/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_offline.js
new file mode 100644
index 0000000000..0004373f7a
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_offline.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+/**
+ * Test indexing support for offline IMAP junk.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_index_junk.js */
+load("base_index_junk.js");
+
+add_setup(function () {
+ // Set these preferences to stop the cache value "cachePDir" being fetched. This
+ // avoids errors on the javascript console, for which the test would otherwise fail.
+ // See bug 903402 for follow-up information.
+ Services.prefs.setComplexValue(
+ "browser.cache.disk.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+ Services.prefs.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: true },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_junk_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_online.js b/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_online.js
new file mode 100644
index 0000000000..c144155799
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_junk_imap_online.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/**
+ * Test indexing support for online IMAP junk.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_index_junk.js */
+load("base_index_junk.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: false },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_junk_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_junk_local.js b/comm/mailnews/db/gloda/test/unit/test_index_junk_local.js
new file mode 100644
index 0000000000..788b630d5b
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_junk_local.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+/**
+ * Test indexing support for local junk.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_index_junk.js */
+load("base_index_junk.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_junk_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_offline.js b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_offline.js
new file mode 100644
index 0000000000..a340122ef0
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_offline.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+/**
+ * Tests how well gloda indexes IMAP messages that are offline from the start.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_index_messages.js */
+load("base_index_messages.js");
+
+var msgGen;
+var scenarios;
+var messageInjection;
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ scenarios = new MessageScenarioFactory(msgGen);
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: true },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_messages_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online.js b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online.js
new file mode 100644
index 0000000000..4977dd5521
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/**
+ * Tests how well gloda indexes IMAP messages that aren't offline.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_index_messages.js */
+load("base_index_messages.js");
+
+expectFulltextResults = false;
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ scenarios = new MessageScenarioFactory(msgGen);
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: false },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_messages_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online_to_offline.js b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online_to_offline.js
new file mode 100644
index 0000000000..85031ec0ac
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_messages_imap_online_to_offline.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/**
+ * Tests how well gloda indexes IMAP messages that are not offline at first, but
+ * are made offline later.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_index_messages.js */
+load("base_index_messages.js");
+
+// We want to go offline once the messages have already been indexed online.
+goOffline = true;
+
+var msgGen;
+var scenarios;
+var messageInjection;
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ scenarios = new MessageScenarioFactory(msgGen);
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: false },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_index_messages_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_messages_local.js b/comm/mailnews/db/gloda/test/unit/test_index_messages_local.js
new file mode 100644
index 0000000000..5441a3062c
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_messages_local.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+/*
+ * Test indexing support for local messages.
+ */
+
+var {
+ glodaTestHelperInitialize,
+ assertExpectedMessagesIndexed,
+ waitForGlodaIndexer,
+ messageInjection,
+ nukeGlodaCachesAndCollections,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { waitForGlodaDBFlush } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/* import-globals-from base_index_messages.js */
+load("base_index_messages.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ scenarios = new MessageScenarioFactory(msgGen);
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Make sure that if we have to reparse a local folder we do not hang or
+ * anything. (We had a regression where we would hang.)
+ */
+add_task(async function test_reparse_of_local_folder_works() {
+ // Index a folder.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+
+ // Force a db flush so we do not have any outstanding references to the
+ // folder or its headers.
+ await waitForGlodaDBFlush();
+
+ // Mark the summary invalid.
+ folder.msgDatabase.summaryValid = false;
+ // Clear the database so next time we have to reparse.
+ folder.msgDatabase.forceClosed();
+
+ // Force gloda to re-parse the folder again.
+ GlodaMsgIndexer.indexFolder(folder);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+});
+
+/**
+ * Ensure that fromJSON for a non-singular attribute properly filters out
+ * "undefined" return values, specifically as it relates to tags. When the
+ * user removes them Gloda doesn't actually re-index the messages so the
+ * values will still be there when we next load the message.
+ *
+ * We directly monkey with the state of NounTag for no really good reason, but
+ * maybe it cuts down on disk I/O because we don't have to touch prefs.
+ */
+add_task(async function test_fromjson_of_removed_tag() {
+ // -- Inject
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true }));
+ let gmsg = msgSet.glodaMessages[0];
+
+ // -- Tag
+ let tag = TagNoun.getTag("$label4");
+ msgSet.addTag(tag.key);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+ Assert.equal(gmsg.tags.length, 1);
+ Assert.equal(gmsg.tags[0].key, tag.key);
+
+ // -- Forget about the tag, TagNoun!
+ delete TagNoun._tagMap[tag.key];
+ // This also means we have to replace the tag service with a liar.
+ let realTagService = TagNoun._msgTagService;
+ TagNoun._msgTagService = {
+ isValidKey() {
+ return false;
+ }, // Lies!
+ };
+
+ // -- Forget about the message, gloda!
+ let glodaId = gmsg.id;
+ nukeGlodaCachesAndCollections();
+
+ // -- Re-load the message.
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.id(glodaId);
+ let coll = await queryExpect(query, msgSet);
+
+ // -- Put the tag back in TagNoun before we check and possibly explode.
+ TagNoun._tagMap[tag.key] = tag;
+ TagNoun._msgTagService = realTagService;
+
+ // -- Verify the message apparently has no tags (despite no reindex).
+ gmsg = coll.items[0];
+ Assert.equal(gmsg.tags.length, 0);
+});
+
+/**
+ * Test that we are using hasOwnProperty or a properly guarding dict for
+ * NounTag so that if someone created a tag called "watch" and then deleted
+ * it, we don't end up exposing the watch function as the tag.
+ *
+ * Strictly speaking, this does not really belong here, but it's a matched set
+ * with the previous test.
+ */
+add_task(
+ function test_nountag_does_not_think_it_has_watch_tag_when_it_does_not() {
+ Assert.equal(TagNoun.fromJSON("watch"), undefined);
+ }
+);
+
+base_index_messages_tests.forEach(e => {
+ add_task(e);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_index_sweep_folder.js b/comm/mailnews/db/gloda/test/unit/test_index_sweep_folder.js
new file mode 100644
index 0000000000..c3f79f0c21
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_index_sweep_folder.js
@@ -0,0 +1,265 @@
+/* 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/. */
+
+/*
+ * This file tests the folder indexing logic of Gloda._worker_folderIndex in
+ * the greater context of the sweep indexing mechanism in a whitebox fashion.
+ *
+ * Automated indexing is suppressed for the duration of this file.
+ *
+ * In order to test the phases of the logic we inject failures into
+ * GlodaIndexer._indexerGetEnumerator with a wrapper to control how far
+ * indexing gets. We also clobber or wrap other functions as needed.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { configureGlodaIndexing } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { sqlExpectCount } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+/**
+ * We want to stop the GlodaMsgIndexer._indexerGetEnumerator after a
+ * set amount of folder indexing.
+ */
+const ENUMERATOR_SIGNAL_WORD = "STOP Me!";
+/**
+ * How many more enumerations before we should throw; 0 means don't throw.
+ */
+var stop_enumeration_after = 0;
+/**
+ * We hide the error in the promise chain. But we do have to know if it happens
+ * at another cycle.
+ */
+var error_is_thrown = false;
+/**
+ * Inject GlodaMsgIndexer._indexerGetEnumerator with our test indexerGetEnumerator.
+ */
+GlodaMsgIndexer._original_indexerGetEnumerator =
+ GlodaMsgIndexer._indexerGetEnumerator;
+/**
+ * Wrapper for GlodaMsgIndexer._indexerGetEnumerator to cause explosions.
+ */
+GlodaMsgIndexer._indexerGetEnumerator = function (...aArgs) {
+ if (stop_enumeration_after && !--stop_enumeration_after) {
+ error_is_thrown = true;
+ throw new Error(ENUMERATOR_SIGNAL_WORD);
+ }
+
+ return GlodaMsgIndexer._original_indexerGetEnumerator(...aArgs);
+};
+
+var messageInjection;
+
+add_setup(function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ // We do not want the event-driven indexer crimping our style.
+ configureGlodaIndexing({ event: false });
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * The value itself does not matter; it just needs to be present and be in a
+ * certain range for our logic testing.
+ */
+var arbitraryGlodaId = 4096;
+
+/**
+ * When we enter a filthy folder we should be marking all the messages as filthy
+ * that have gloda-id's and committing.
+ */
+add_task(async function test_propagate_filthy_from_folder_to_messages() {
+ // Mark the folder as filthy.
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 3 },
+ ]);
+ let glodaFolder = Gloda.getFolderForFolder(folder);
+ glodaFolder._dirtyStatus = glodaFolder.kFolderFilthy;
+
+ // Mark each header with a gloda-id so they can get marked filthy.
+ for (let msgHdr of msgSet.msgHdrs()) {
+ msgHdr.setUint32Property("gloda-id", arbitraryGlodaId);
+ }
+
+ // Force the database to see it as filthy so we can verify it changes.
+ glodaFolder._datastore.updateFolderDirtyStatus(glodaFolder);
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) FROM folderLocations WHERE id = ? " +
+ "AND dirtyStatus = ?",
+ glodaFolder.id,
+ glodaFolder.kFolderFilthy
+ );
+
+ // Index the folder, aborting at the second get enumerator request.
+ stop_enumeration_after = 2;
+
+ await spin_folder_indexer(folder);
+
+ // The folder should only be dirty.
+ Assert.equal(glodaFolder.dirtyStatus, glodaFolder.kFolderDirty);
+ // Make sure the database sees it as dirty.
+ await sqlExpectCount(
+ 1,
+ "SELECT COUNT(*) FROM folderLocations WHERE id = ? " +
+ "AND dirtyStatus = ?",
+ glodaFolder.id,
+ glodaFolder.kFolderDirty
+ );
+
+ // The messages should be filthy per the headers.
+ // We force a commit of the database.
+ for (let msgHdr of msgSet.msgHdrs()) {
+ Assert.equal(
+ msgHdr.getUint32Property("gloda-dirty"),
+ GlodaMsgIndexer.kMessageFilthy
+ );
+ }
+});
+
+/**
+ * Make sure our counting pass and our indexing passes gets it right. We test
+ * with 0,1,2 messages matching.
+ */
+add_task(async function test_count_pass() {
+ let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 2 },
+ ]);
+
+ let hdrs = msgSet.msgHdrList;
+
+ // - (clean) messages with gloda-id's do not get indexed
+ // Nothing is indexed at this point, so all 2.
+ error_is_thrown = false;
+ stop_enumeration_after = 2;
+ await spin_folder_indexer(folder, 2);
+
+ // Pretend the first is indexed, leaving a count of 1.
+ hdrs[0].setUint32Property("gloda-id", arbitraryGlodaId);
+ error_is_thrown = false;
+ stop_enumeration_after = 2;
+ await spin_folder_indexer(folder, 1);
+
+ // Pretend both are indexed, count of 0.
+ hdrs[1].setUint32Property("gloda-id", arbitraryGlodaId);
+ // No explosion should happen since we should never get to the second
+ // enumerator.
+ error_is_thrown = false;
+ await spin_folder_indexer(folder, 0);
+
+ // - Dirty messages get indexed.
+ hdrs[0].setUint32Property("gloda-dirty", GlodaMsgIndexer.kMessageDirty);
+ stop_enumeration_after = 2;
+ error_is_thrown = false;
+ await spin_folder_indexer(folder, 1);
+
+ hdrs[1].setUint32Property("gloda-dirty", GlodaMsgIndexer.kMessageDirty);
+ stop_enumeration_after = 2;
+ error_is_thrown = false;
+ await spin_folder_indexer(folder, 2);
+});
+
+/**
+ * Create a folder indexing job for the given injection folder handle and
+ * run it until completion.
+ *
+ * The folder indexer will continue running on its own if we dont throw an Error in the
+ * GlodaMsgIndexer._indexerGetEnumerator
+ */
+async function spin_folder_indexer(aFolderHandle, aExpectedJobGoal) {
+ let msgFolder = messageInjection.getRealInjectionFolder(aFolderHandle);
+
+ // Cheat and use indexFolder to build the job for us.
+ GlodaMsgIndexer.indexFolder(msgFolder);
+ // Steal that job.
+ let job = GlodaIndexer._indexQueue.pop();
+ GlodaIndexer._indexingJobGoal--;
+
+ // Create the callbackHandle.
+ let callbackHandle = new CallbackHandle();
+ // Create the worker.
+ let worker = GlodaMsgIndexer._worker_folderIndex(job, callbackHandle);
+ try {
+ callbackHandle.pushAndGo(worker, null);
+ await Promise.race([
+ callbackHandle.promise,
+ TestUtils.waitForCondition(() => {
+ return error_is_thrown;
+ }),
+ ]);
+ } catch (ex) {
+ do_throw(ex);
+ }
+
+ if (aExpectedJobGoal !== undefined) {
+ Assert.equal(job.goal, aExpectedJobGoal);
+ }
+}
+
+/**
+ * Implements GlodaIndexer._callbackHandle's interface adapted to our async
+ * test driver. This allows us to run indexing workers directly in tests
+ * or support code.
+ *
+ * We do not do anything with the context stack or recovery. Use the actual
+ * indexer callback handler for that!
+ *
+ * Actually, we do very little at all right now. This will fill out as needs
+ * arise.
+ */
+class CallbackHandle {
+ constructor() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+ }
+
+ pushAndGo(aIterator, aContext) {
+ this.glodaWorkerAdapter(aIterator, this._resolve).catch(reason => {
+ if (!reason.message.match(ENUMERATOR_SIGNAL_WORD)) {
+ throw reason;
+ }
+ });
+ }
+
+ async glodaWorkerAdapter(aIter, resolve) {
+ while (!error_is_thrown) {
+ switch (aIter.next().value) {
+ case GlodaConstants.kWorkSync:
+ break;
+ case GlodaConstants.kWorkDone:
+ case GlodaConstants.kWorkDoneWithResult:
+ resolve();
+ return;
+ default:
+ break;
+ }
+ }
+ }
+ get promise() {
+ return this._promise;
+ }
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_intl.js b/comm/mailnews/db/gloda/test/unit/test_intl.js
new file mode 100644
index 0000000000..e6e9868189
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_intl.js
@@ -0,0 +1,355 @@
+/* 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/. */
+
+/*
+ * Sanity check our encoding transforms and make sure the mozporter tokenizer
+ * is resulting in the expected fulltext search results. Specifically:
+ * - Check that subject, body, and attachment names are properly indexed;
+ * previously we screwed up at least one of these in terms of handling
+ * encodings properly.
+ * - Check that we can fulltext search on those things afterwards.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { waitForGlodaDBFlush } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/**
+ * To make the encoding pairs:
+ * - For the subject bit:
+ * import email
+ * h = email.Header.Header(charset=CHARSET)
+ * h.append(STRING)
+ * h.encode()
+ * - For the body bit
+ * s.encode(CHARSET)
+ */
+var intlPhrases = [
+ // -- CJK case
+ {
+ name: "CJK: Vending Machine",
+ actual: "\u81ea\u52d5\u552e\u8ca8\u6a5f",
+ encodings: {
+ "utf-8": [
+ "=?utf-8?b?6Ieq5YuV5ZSu6LKo5qmf?=",
+ "\xe8\x87\xaa\xe5\x8b\x95\xe5\x94\xae\xe8\xb2\xa8\xe6\xa9\x9f",
+ ],
+ "euc-jp": [
+ "=?shift-jis?b?jqmTrppTid2LQA==?=",
+ "\xbc\xab\xc6\xb0\xd3\xb4\xb2\xdf\xb5\xa1",
+ ],
+ "shift-jis": [
+ "=?shift-jis?b?jqmTrppTid2LQA==?=",
+ "\x8e\xa9\x93\xae\x9aS\x89\xdd\x8b@",
+ ],
+ },
+ searchPhrases: [
+ // Match bi-gram driven matches starting from the front.
+ { body: '"\u81ea\u52d5"', match: true },
+ { body: '"\u81ea\u52d5\u552e"', match: true },
+ { body: '"\u81ea\u52d5\u552e\u8ca8"', match: true },
+ { body: '"\u81ea\u52d5\u552e\u8ca8\u6a5f"', match: true },
+ // Now match from the back (bi-gram based).
+ { body: '"\u52d5\u552e\u8ca8\u6a5f"', match: true },
+ { body: '"\u552e\u8ca8\u6a5f"', match: true },
+ { body: '"\u8ca8\u6a5f"', match: true },
+ // Now everybody in the middle!
+ { body: '"\u52d5\u552e\u8ca8"', match: true },
+ { body: '"\u552e\u8ca8"', match: true },
+ { body: '"\u52d5\u552e"', match: true },
+ // -- Now match nobody!
+ // Nothing in common with the right answer.
+ { body: '"\u81eb\u52dc"', match: false },
+ // Too long, no match!
+ { body: '"\u81ea\u52d5\u552e\u8ca8\u6a5f\u6a5f"', match: false },
+ // Minor change at the end.
+ { body: '"\u81ea\u52d5\u552e\u8ca8\u6a5e"', match: false },
+ ],
+ },
+ // Use two words where the last character is a multi-byte sequence and one of
+ // them is the last word in the string. This helps test an off-by-one error
+ // in both the asymmetric case (query's last character is last character in
+ // the tokenized string but it is not the last character in the body string)
+ // and symmetric case (last character in the query and the body).
+ {
+ name: "Czech diacritics",
+ actual: "Slov\u00e1cko Moravsk\u00e9 rodin\u011b",
+ encodings: {
+ "utf-8": [
+ "=?utf-8?b?U2xvdsOhY2tvIE1vcmF2c2vDqSByb2RpbsSb?=",
+ "Slov\xc3\xa1cko Moravsk\xc3\xa9 rodin\xc4\x9b",
+ ],
+ },
+ searchPhrases: [
+ // -- Desired
+ // Match on exact for either word should work
+ { body: "Slov\u00e1cko", match: true },
+ { body: "Moravsk\u00e9", match: true },
+ { body: "rodin\u011b", match: true },
+ // The ASCII uppercase letters get case-folded
+ { body: "slov\u00e1cko", match: true },
+ { body: "moravsk\u00e9", match: true },
+ { body: "rODIN\u011b", match: true },
+ ],
+ },
+ // Ignore accent search!
+ {
+ name: "having accent: Paris",
+ actual: "Par\u00eds",
+ encodings: {
+ "utf-8": ["=?UTF-8?B?UGFyw61z?=", "Par\xc3\xads"],
+ },
+ searchPhrases: [{ body: "paris", match: true }],
+ },
+ // Case insensitive case for non-ASCII characters.
+ {
+ name: "Russian: new",
+ actual: "\u041d\u043e\u0432\u043e\u0435",
+ encodings: {
+ "utf-8": [
+ "=?UTF-8?B?0J3QvtCy0L7QtQ==?=",
+ "\xd0\x9d\xd0\xbe\xd0\xb2\xd0\xbe\xd0\xb5",
+ ],
+ },
+ searchPhrases: [{ body: "\u043d\u043e\u0432\u043e\u0435", match: true }],
+ },
+ // Case-folding happens after decomposition.
+ {
+ name: "Awesome where A has a bar over it",
+ actual: "\u0100wesome",
+ encodings: {
+ "utf-8": ["=?utf-8?q?=C4=80wesome?=", "\xc4\x80wesome"],
+ },
+ searchPhrases: [
+ { body: "\u0100wesome", match: true }, // Upper A-bar
+ { body: "\u0101wesome", match: true }, // Lower a-bar
+ { body: "Awesome", match: true }, // Upper A
+ { body: "awesome", match: true }, // Lower a
+ ],
+ },
+ // Deep decomposition happens and after that, case folding.
+ {
+ name: "Upper case upsilon with diaeresis and hook goes to small upsilon",
+ actual: "\u03d4esterday",
+ encodings: {
+ "utf-8": ["=?utf-8?q?=CF=94esterday?=", "\xcf\x94esterday"],
+ },
+ searchPhrases: [
+ { body: "\u03d4esterday", match: true }, // Y_: 03d4 => 03d2 (decomposed)
+ { body: "\u03d3esterday", match: true }, // Y_' 03d3 => 03d2 (decomposed)
+ { body: "\u03d2esterday", match: true }, // Y_ 03d2 => 03a5 (decomposed)
+ { body: "\u03a5esterday", match: true }, // Y 03a5 => 03c5 (lowercase)
+ { body: "\u03c5esterday", match: true }, // y 03c5 (final state)
+ ],
+ },
+ // Full-width alphabet.
+ // Even if search phrases are ASCII, it has to hit.
+ {
+ name: "Full-width Thunderbird",
+ actual:
+ "\uff34\uff48\uff55\uff4e\uff44\uff45\uff52\uff42\uff49\uff52\uff44",
+ encodings: {
+ "utf-8": [
+ "=?UTF-8?B?77y0772I772V772O772E772F772S772C772J772S772E?=",
+ "\xef\xbc\xb4\xef\xbd\x88\xef\xbd\x95\xef\xbd\x8e\xef\xbd\x84\xef\xbd\x85\xef\xbd\x92\xef\xbd\x82\xef\xbd\x89\xef\xbd\x92\xef\xbd\x84",
+ ],
+ },
+ searchPhrases: [
+ // Full-width lower.
+ {
+ body: "\uff34\uff28\uff35\uff2e\uff24\uff25\uff32\uff22\uff29\uff32\uff24",
+ match: true,
+ },
+ // Half-width.
+ { body: "Thunderbird", match: true },
+ ],
+ },
+ // Half-width Katakana with voiced sound mark.
+ // Even if search phrases are full-width, it has to hit.
+ {
+ name: "Half-width Katakana: Thunderbird (SANDAABAADO)",
+ actual: "\uff7b\uff9d\uff80\uff9e\uff70\uff8a\uff9e\uff70\uff84\uff9e",
+ encodings: {
+ "utf-8": [
+ "=?UTF-8?B?7727776d776A776e772w776K776e772w776E776e?=",
+ "\xef\xbd\xbb\xef\xbe\x9d\xef\xbe\x80\xef\xbe\x9e\xef\xbd\xb0\xef\xbe\x8a\xef\xbe\x9e\xef\xbd\xb0\xef\xbe\x84\xef\xbe\x9e",
+ ],
+ },
+ searchPhrases: [
+ { body: "\u30b5\u30f3\u30c0\u30fc\u30d0\u30fc\u30c9", match: true },
+ ],
+ },
+ // Thai: Would you like to see the movie?
+ {
+ name: "Thai: query movie word into Thai language content",
+ actual:
+ "\u0e04\u0e38\u0e13\u0e2d\u0e22\u0e32\u0e01\u0e44\u0e1b\u0e14\u0e39\u0e2b\u0e19\u0e31\u0e07",
+ encodings: {
+ "utf-8": [
+ "=?UTF-8?B?4LiE4Li44LiT4Lit4Lii4Liy4LiB4LmE4Lib4LiU4Li54Lir4LiZ4Lix4LiH?=",
+ "\xe0\xb8\x84\xe0\xb8\xb8\xe0\xb8\x93\xe0\xb8\xad\xe0\xb8\xa2\xe0\xb8\xb2\xe0\xb8\x81\xe0\xb9\x84\xe0\xb8\x9b\xe0\xb8\x94\xe0\xb8\xb9\xe0\xb8\xab\xe0\xb8\x99\xe0\xb8\xb1\xe0\xb8\x87",
+ ],
+ },
+ searchPhrases: [{ body: "\u0e2b\u0e19\u0e31\u0e07", match: true }],
+ },
+];
+
+var msgGen;
+var messageInjection;
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ // Use mbox injection because the fake server chokes sometimes right now.
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+add_task(async function test_index_all_phrases() {
+ for (let phrase of intlPhrases) {
+ await indexPhrase(phrase);
+ }
+});
+
+add_task(async function flush_db() {
+ // Force a db flush so I can investigate the database if I want.
+ await waitForGlodaDBFlush();
+});
+
+add_task(async function test_fulltextsearch_all_phrases() {
+ for (let phrase of intlPhrases) {
+ await fulltextsearchPhrase(phrase);
+ }
+});
+
+/**
+ * Names with encoded commas in them can screw up our mail address parsing if
+ * we perform the mime decoding prior to handing the mail address off for
+ * parsing.
+ */
+add_task(async function test_encoding_complications_with_mail_addresses() {
+ let basePair = msgGen.makeNameAndAddress();
+ // The =2C encodes a comma!
+ let encodedCommaPair = ["=?iso-8859-1?Q?=DFnake=2C_=DFammy?=", basePair[1]];
+ // "Snake, Sammy", but with a much cooler looking S-like character!
+ let decodedName = "\u00dfnake, \u00dfammy";
+ // Use the thing with the comma in it for all cases; previously there was an
+ // asymmetry between to and cc...
+ let smsg = msgGen.makeMessage({
+ from: encodedCommaPair,
+ to: [encodedCommaPair],
+ cc: [encodedCommaPair],
+ });
+ function verify_sammy_snake(unused, gmsg) {
+ Assert.equal(gmsg.from.contact.name, decodedName);
+ Assert.equal(gmsg.to.length, 1);
+ Assert.equal(gmsg.to[0].id, gmsg.from.id);
+ Assert.equal(gmsg.cc.length, 1);
+ Assert.equal(gmsg.cc[0].id, gmsg.from.id);
+ }
+
+ let synSet = new SyntheticMessageSet([smsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([synSet], { verifier: verify_sammy_snake })
+ );
+});
+
+/**
+ * For each phrase in the intlPhrases array (we are parameterized over it using
+ * parameterizeTest in the 'tests' declaration), create a message where the
+ * subject, body, and attachment name are populated using the encodings in
+ * the phrase's "encodings" attribute, one encoding per message. Make sure
+ * that the strings as exposed by the gloda representation are equal to the
+ * expected/actual value.
+ * Stash each created synthetic message in a resultList list on the phrase so
+ * that we can use them as expected query results in
+ * |fulltextsearchPhrase|.
+ */
+async function indexPhrase(aPhrase) {
+ // Create a synthetic message for each of the delightful encoding types.
+ let messages = [];
+ aPhrase.resultList = [];
+ for (let charset in aPhrase.encodings) {
+ let [quoted, bodyEncoded] = aPhrase.encodings[charset];
+
+ let smsg = msgGen.makeMessage({
+ subject: quoted,
+ body: { charset, encoding: "8bit", body: bodyEncoded },
+ attachments: [{ filename: quoted, body: "gabba gabba hey" }],
+ // Save off the actual value for checking.
+ callerData: [charset, aPhrase.actual],
+ });
+
+ messages.push(smsg);
+ aPhrase.resultList.push(smsg);
+ }
+ let synSet = new SyntheticMessageSet(messages);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([synSet], { verifier: verify_index })
+ );
+}
+
+/**
+ * Does the per-message verification for indexPhrase. Knows what is right for
+ * each message because of the callerData attribute on the synthetic message.
+ */
+function verify_index(smsg, gmsg) {
+ let [charset, actual] = smsg.callerData;
+ let subject = gmsg.subject;
+ let indexedBodyText = gmsg.indexedBodyText.trim();
+ let attachmentName = gmsg.attachmentNames[0];
+ dump("using character set: " + charset + " actual: " + actual + "\n");
+ dump("subject: " + subject + " (len: " + subject.length + ")\n");
+ Assert.equal(actual, subject);
+ dump("Body: " + indexedBodyText + " (len: " + indexedBodyText.length + ")\n");
+ Assert.equal(actual, indexedBodyText);
+ dump(
+ "Attachment name: " +
+ attachmentName +
+ " (len: " +
+ attachmentName.length +
+ ")\n"
+ );
+ Assert.equal(actual, attachmentName);
+}
+
+/**
+ * For each phrase, make sure that all of the searchPhrases either match or fail
+ * to match as appropriate.
+ */
+async function fulltextsearchPhrase(aPhrase) {
+ for (let searchPhrase of aPhrase.searchPhrases) {
+ let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE);
+ query.bodyMatches(searchPhrase.body);
+ await queryExpect(query, searchPhrase.match ? aPhrase.resultList : []);
+ }
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_migration.js b/comm/mailnews/db/gloda/test/unit/test_migration.js
new file mode 100644
index 0000000000..f7e1bc334d
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_migration.js
@@ -0,0 +1,151 @@
+/* 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/. */
+
+/**
+ * Test migration logic by artificially inducing or simulating the problem, then
+ * trigger the migration logic, then verify things ended up correct, including
+ * the schema version so a second pass of the logic doesn't happen. (As
+ * opposed to checking in an example of a broken database and running against
+ * that.)
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ nukeGlodaCachesAndCollections,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { waitForGlodaDBFlush, makeABCardForAddressPair } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { sqlRun } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { GlodaMsgIndexer } = ChromeUtils.import(
+ "resource:///modules/gloda/IndexMsg.jsm"
+);
+var { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+const GLODA_OLD_BAD_MESSAGE_ID = 1;
+
+var msgGen;
+var messageInjection;
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Fix the fallout from bug 732372 (with this patch for bug 734507) which left
+ * identities whose e-mails were in the address book without contacts and then
+ * broke messages involving them.
+ */
+add_task(async function test_fix_missing_contacts_and_fallout() {
+ // -- Setup
+
+ // - Create 4 e-mail addresses, 2 of which are in the address book. (We want
+ // to make sure we have to iterate, hence >1).
+ let abPeeps = msgGen.makeNamesAndAddresses(2);
+ let nonAbPeeps = msgGen.makeNamesAndAddresses(2);
+ makeABCardForAddressPair(abPeeps[0]);
+ makeABCardForAddressPair(abPeeps[1]);
+
+ // - Create messages of the genres [from, to]: [inAB, inAB], [inAB, !inAB],
+ // [!inAB, inAB], [!inAB, !inAB]. The permutations are black box overkill.
+ // Smear the messages over multiple folders for realism.
+ let [, yesyesMsgSet, yesnoMsgSet, noyesMsgSet, nonoMsgSet] =
+ await messageInjection.makeFoldersWithSets(3, [
+ { count: 2, from: abPeeps[0], to: [abPeeps[1]] },
+ { count: 2, from: abPeeps[1], to: nonAbPeeps },
+ { count: 2, from: nonAbPeeps[0], to: abPeeps },
+ { count: 2, from: nonAbPeeps[1], to: [nonAbPeeps[0]] },
+ ]);
+
+ // Union the yeses together; we don't care about their composition.
+ let yesMsgSet = yesyesMsgSet.union(yesnoMsgSet).union(noyesMsgSet),
+ noMsgSet = nonoMsgSet;
+
+ // - Let gloda index the messages so the identities get created.
+ await waitForGlodaIndexer();
+ Assert.ok(
+ ...assertExpectedMessagesIndexed([yesMsgSet, noMsgSet], { augment: true })
+ );
+ // The messages are now indexed and the contacts created.
+
+ // - Compel an indexing sweep so the folder's dirty statuses get cleared
+ GlodaMsgIndexer.initialSweep();
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([])); // (no new messages to index)
+
+ // - Force a DB commit so the pending commit tracker gets emptied out
+ // (otherwise we need to worry about its state overriding our clobbering)
+ await waitForGlodaDBFlush();
+
+ // - Delete the contact records for the people in the address book.
+ await sqlRun(
+ "DELETE FROM contacts WHERE id IN (" +
+ yesMsgSet.glodaMessages[0].from.contact.id +
+ ", " +
+ yesMsgSet.glodaMessages[0].to[0].contact.id +
+ ")"
+ );
+
+ // - Nuke the gloda caches so we totally forget those contact records.
+ nukeGlodaCachesAndCollections();
+
+ // - Manually mark the messages involving the inAB people with the _old_ bad
+ // id marker so that our scan will see them.
+ for (let msgHdr of yesMsgSet.msgHdrs()) {
+ msgHdr.setUint32Property("gloda-id", GLODA_OLD_BAD_MESSAGE_ID);
+ }
+
+ // - Mark the db schema version to the version with the bug (26).
+ // Sanity check that gloda actually populates the value with the current
+ // version correctly.
+ Assert.equal(
+ GlodaDatastore._actualSchemaVersion,
+ GlodaDatastore._schemaVersion
+ );
+ GlodaDatastore._actualSchemaVersion = 26;
+ await sqlRun("PRAGMA user_version = 26");
+ // Make sure that took, since we check it below as a success indicator.
+ let verRows = await sqlRun("PRAGMA user_version");
+ Assert.equal(verRows[0].getInt64(0), 26);
+
+ // -- Test
+ // - Trigger the migration logic and request an indexing sweep.
+ GlodaMsgIndexer.disable();
+ GlodaMsgIndexer.enable();
+ GlodaMsgIndexer.initialSweep();
+
+ // - Wait for the indexer to complete, expecting that the messages that we
+ // marked bad will get indexed but not the good messages.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([yesMsgSet], { augment: true }));
+
+ // - Verify that the identities have contacts again.
+ // Must have the contact object.
+ Assert.notEqual(yesMsgSet.glodaMessages[0].from.contact, undefined);
+ // The contact's name should come from the address book card
+ Assert.equal(yesMsgSet.glodaMessages[0].from.contact.name, abPeeps[0][0]);
+
+ // - Verify that the schema version changed from gloda's perspective and from
+ // the db's perspective.
+ verRows = await sqlRun("PRAGMA user_version");
+ Assert.equal(verRows[0].getInt64(0), GlodaDatastore._schemaVersion);
+ Assert.equal(
+ GlodaDatastore._actualSchemaVersion,
+ GlodaDatastore._schemaVersion
+ );
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_mime_attachments_size.js b/comm/mailnews/db/gloda/test/unit/test_mime_attachments_size.js
new file mode 100644
index 0000000000..2e18fbe11f
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_mime_attachments_size.js
@@ -0,0 +1,445 @@
+/* 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/. */
+
+/*
+ * General testing of the byte-counting libmime facility, to make sure that what
+ * is streamed to us is actually labeled with the right size.
+ */
+
+/*
+ * Do not include glodaTestHelper because we do not want gloda loaded and it
+ * adds a lot of runtime overhead which makes certain debugging strategies like
+ * using chronicle-recorder impractical.
+ */
+
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var {
+ MessageGenerator,
+ SyntheticPartLeaf,
+ SyntheticPartMultiMixed,
+ SyntheticPartMultiRelated,
+ SyntheticMessageSet,
+} = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen = new MessageGenerator();
+var messageInjection;
+
+add_setup(function () {
+ // Sanity check: figure out how many bytes the original text occupies in UTF-8 encoding
+ Assert.equal(
+ new TextEncoder().encode(originalText).length,
+ originalTextByteCount
+ );
+
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+});
+
+var htmlText = "<html><head></head><body>I am HTML! Woo! </body></html>";
+
+var partHtml = new SyntheticPartLeaf(htmlText, {
+ contentType: "text/html",
+});
+
+// This text is 168 characters long, and occupies 173 bytes when encoded in
+// UTF-8. (We make sure it occupies 173 bytes in run_test below). Note that
+// you cannot use this text directly because it isn't pure ASCII. You must use
+// one of the encoded forms below.
+var originalText =
+ "Longtemps, je me suis couché de bonne heure. Parfois, à " +
+ "peine ma bougie éteinte, mes yeux se fermaient si vite que je n'avais pas le " +
+ "temps de me dire : « Je m'endors. »";
+var originalTextByteCount = 173;
+
+var b64Text =
+ "TG9uZ3RlbXBzLCBqZSBtZSBzdWlzIGNvdWNow6kgZGUgYm9ubmUgaGV1cmUuIFBhcmZvaXMs\n" +
+ "IMOgIHBlaW5lIG1hIGJvdWdpZSDDqXRlaW50ZSwgbWVzIHlldXggc2UgZmVybWFpZW50IHNp\n" +
+ "IHZpdGUgcXVlIGplIG4nYXZhaXMgcGFzIGxlIHRlbXBzIGRlIG1lIGRpcmUgOiDCqyBKZSBt\n" +
+ "J2VuZG9ycy4gwrsK";
+
+var qpText =
+ "Longtemps,=20je=20me=20suis=20couch=C3=A9=20de=20bonne=20heure.=20Parfois,=\n" +
+ "=20=C3=A0=20peine=20ma=20bougie=20=C3=A9teinte,=20mes=20yeux=20se=20fermaie=\n" +
+ "nt=20si=20vite=20que=20je=20n'avais=20pas=20le=20temps=20de=20me=20dire=20:=\n" +
+ "=20=C2=AB=20Je=20m'endors.=20=C2=BB";
+
+var uuText =
+ "begin 666 -\n" +
+ 'M3&]N9W1E;7!S+"!J92!M92!S=6ES(&-O=6-HPZD@9&4@8F]N;F4@:&5U<F4N\n' +
+ "M(%!A<F9O:7,L(,.@('!E:6YE(&UA(&)O=6=I92##J71E:6YT92P@;65S('EE\n" +
+ "M=7@@<V4@9F5R;6%I96YT('-I('9I=&4@<75E(&IE(&XG879A:7,@<&%S(&QE\n" +
+ "G('1E;7!S(&1E(&UE(&1I<F4@.B#\"JR!*92!M)V5N9&]R<RX@PKL*\n" +
+ "\n" +
+ "end";
+
+var yencText =
+ "Hello there --\n" +
+ "=ybegin line=128 size=174 name=jane.doe\n" +
+ "\x76\x99\x98\x91\x9e\x8f\x97\x9a\x9d\x56\x4a\x94\x8f\x4a\x97\x8f" +
+ "\x4a\x9d\x9f\x93\x9d\x4a\x8d\x99\x9f\x8d\x92\xed\xd3\x4a\x8e\x8f" +
+ "\x4a\x8c\x99\x98\x98\x8f\x4a\x92\x8f\x9f\x9c\x8f\x58\x4a\x7a\x8b" +
+ "\x9c\x90\x99\x93\x9d\x56\x4a\xed\xca\x4a\x9a\x8f\x93\x98\x8f\x4a" +
+ "\x97\x8b\x4a\x8c\x99\x9f\x91\x93\x8f\x4a\xed\xd3\x9e\x8f\x93\x98" +
+ "\x9e\x8f\x56\x4a\x97\x8f\x9d\x4a\xa3\x8f\x9f\xa2\x4a\x9d\x8f\x4a" +
+ "\x90\x8f\x9c\x97\x8b\x93\x8f\x98\x9e\x4a\x9d\x93\x4a\xa0\x93\x9e" +
+ "\x8f\x4a\x9b\x9f\x8f\x4a\x94\x8f\x4a\x98\x51\x8b\xa0\x8b\x93\x9d" +
+ "\x0d\x0a\x4a\x9a\x8b\x9d\x4a\x96\x8f\x4a\x9e\x8f\x97\x9a\x9d\x4a" +
+ "\x8e\x8f\x4a\x97\x8f\x4a\x8e\x93\x9c\x8f\x4a\x64\x4a\xec\xd5\x4a" +
+ "\x74\x8f\x4a\x97\x51\x8f\x98\x8e\x99\x9c\x9d\x58\x4a\xec\xe5\x34" +
+ "\x0d\x0a" +
+ "=yend size=174 crc32=7efccd8e\n";
+
+// That completely exotic encoding is only detected if there is no content type
+// on the message, which is usually the case in newsgroups. I hate you yencode!
+// var partYencText = new SyntheticPartLeaf("I am text! Woo!\n\n" + yencText, {
+// contentType: "",
+// charset: "",
+// format: "",
+// });
+
+var partUUText = new SyntheticPartLeaf(
+ "I am text! With uuencode... noes...\n\n" + uuText,
+ {
+ contentType: "",
+ charset: "",
+ format: "",
+ }
+);
+
+var tachText = {
+ filename: "bob.txt",
+ body: qpText,
+ charset: "utf-8",
+ encoding: "quoted-printable",
+};
+
+var tachInlineText = {
+ filename: "foo.txt",
+ body: qpText,
+ format: null,
+ charset: "utf-8",
+ encoding: "quoted-printable",
+ disposition: "inline",
+};
+
+// Images have a different behavior than other attachments: they are displayed
+// inline most of the time, so there are two different code paths that need to
+// enable streaming and byte counting to the JS mime emitter.
+
+var tachImage = {
+ filename: "bob.png",
+ contentType: "image/png",
+ encoding: "base64",
+ charset: null,
+ format: null,
+ body: b64Text,
+};
+
+var tachPdf = {
+ filename: "bob.pdf",
+ contentType: "application/pdf",
+ encoding: "base64",
+ charset: null,
+ format: null,
+ body: b64Text,
+};
+
+var tachUU = {
+ filename: "john.doe",
+ contentType: "application/x-uuencode",
+ encoding: "uuencode",
+ charset: null,
+ format: null,
+ body: uuText,
+};
+
+var tachApplication = {
+ filename: "funky.funk",
+ contentType: "application/x-funky",
+ encoding: "base64",
+ body: b64Text,
+};
+
+var relImage = {
+ contentType: "image/png",
+ encoding: "base64",
+ charset: null,
+ format: null,
+ contentId: "part1.foo@bar.invalid",
+ body: b64Text,
+};
+
+var tachVCard = {
+ filename: "bob.vcf",
+ contentType: "text/vcard",
+ encoding: "7bit",
+ body: "begin:vcard\nfn:Bob\nend:vcard\n",
+};
+var partTachVCard = new SyntheticPartLeaf(tachVCard.body, tachVCard);
+
+new SyntheticPartLeaf(relImage.body, relImage);
+
+var messageInfos = [
+ {
+ name: "uuencode inline",
+ bodyPart: partUUText,
+ subject: "duh",
+ epsilon: 1,
+ checkTotalSize: false,
+ },
+ // Encoding type specific to newsgroups, not interested, gloda doesn't even
+ // treat this as an attachment (probably because gloda requires an attachment
+ // to have a content-type, which these yencoded parts don't have), but size IS
+ // counted properly nonetheless.
+ /* {
+ name: 'text/plain with yenc inline',
+ bodyPart: partYencText,
+ subject: "yEnc-Prefix: \"jane.doe\" 174 yEnc bytes - yEnc test (1)",
+ },*/
+ // Inline image, not interested either, gloda doesn't keep that as an
+ // attachment (probably a deliberate choice), size is NOT counted properly.
+ // (don't want to investigate, I doubt it's a useful information anyway.)
+ /* {
+ name: 'multipart/related',
+ bodyPart: new SyntheticPartMultiRelated([partHtml, partRelImage]),
+ },*/
+ // This doesn't really make sense because it returns the length of the
+ // encoded blob without the envelope. Disabling as part of bug 711980.
+ /* {
+ name: '.eml attachment',
+ bodyPart: new SyntheticPartMultiMixed([
+ partHtml,
+ msgGen.makeMessage({ body: { body: qpText,
+ charset: "UTF-8",
+ encoding: "quoted-printable" } }),
+ ]),
+ epsilon: 1,
+ },*/
+ // All of the other common cases work fine.
+ {
+ name: 'all sorts of "real" attachments',
+ bodyPart: partHtml,
+ attachments: [
+ tachImage,
+ tachPdf,
+ tachUU,
+ tachApplication,
+ tachText,
+ tachInlineText,
+ ],
+ epsilon: 2,
+ },
+];
+
+add_task(async function test_message_attachments() {
+ for (let messageInfo of messageInfos) {
+ await message_attachments(messageInfo);
+ }
+});
+
+var bogusMessage = msgGen.makeMessage({ body: { body: originalText } });
+bogusMessage._contentType = "woooooo"; // Breaking abstraction boundaries. Bad.
+
+var bogusMessageInfos = [
+ // In this case, the wooooo part is not an attachment, so its bytes won't be
+ // counted (size will end up being 0 bytes). We don't check the size, but
+ // check_bogus_parts makes sure we're able to come up with a resulting size
+ // for the MimeMessage.
+ //
+ // In that very case, since message M is an attachment, libmime will count M's
+ // bytes, and we could have MimeMessages prefer the size libmime tells them
+ // (when they have it), rather than recursively computing their sizes. I'm not
+ // sure changing jsmimeemitter.js is worth the trouble just for buggy
+ // messages...
+ {
+ name: ".eml attachment with inner MimeUnknown",
+ bodyPart: new SyntheticPartMultiMixed([
+ partHtml,
+ msgGen.makeMessage({
+ // <--- M
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartMultiRelated([
+ partHtml,
+ new SyntheticPartLeaf(htmlText, { contentType: "woooooo" }),
+ ]),
+ ]),
+ }),
+ ]),
+ epsilon: 6,
+ checkSize: false,
+ },
+];
+
+add_task(async function test_bogus_messages(info) {
+ for (let bogusMessageInfo of bogusMessageInfos) {
+ await bogus_messages(bogusMessageInfo);
+ }
+});
+
+add_task(async function test_have_attachments() {
+ // The goal here is to explicitly check that these messages have attachments.
+ let number = 1;
+ let synMsg = msgGen.makeMessage({
+ name: "multipart/related",
+ bodyPart: new SyntheticPartMultiMixed([partHtml, partTachVCard]),
+ number,
+ });
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ try {
+ Assert.equal(aMimeMsg.allUserAttachments.length, number);
+ promiseResolve();
+ } catch (e) {
+ do_throw(e);
+ }
+ });
+
+ await promise;
+});
+
+async function message_attachments(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ try {
+ check_attachments(
+ aMimeMsg,
+ info.epsilon,
+ "checkTotalSize" in info ? info.checkTotalSize : undefined
+ );
+ promiseResolve();
+ } catch (e) {
+ do_throw(e);
+ }
+ });
+
+ await promise;
+}
+
+function check_attachments(aMimeMsg, epsilon, checkTotalSize) {
+ if (aMimeMsg == null) {
+ do_throw("We really should have gotten a result!");
+ }
+
+ /* It is hard to get a byte count that's perfectly accurate. When composing
+ * the message, the MIME structure goes like this (for an encoded attachment):
+ *
+ * XXXXXXXXXX
+ * XXXXXXXXXX <-- encoded block
+ * XXXXXXXXXX
+ * <-- newline
+ * --chopchop <-- MIME separator
+ *
+ * libmime counts bytes all the way up to the separator, which means it counts
+ * the bytes for the extra line. Since newlines in emails are \n, most of the
+ * time we get att.size = 174 instead of 173.
+ *
+ * The good news is, it's just a fixed extra cost. There no issues with the
+ * inner contents of the attachment, you can add as many newlines as you want
+ * in it, Unix or Windows, the count won't get past the bounds.
+ */
+
+ Assert.ok(aMimeMsg.allUserAttachments.length > 0);
+
+ let totalSize = htmlText.length;
+
+ for (let att of aMimeMsg.allUserAttachments) {
+ dump("*** Attachment now is " + att.name + " " + att.size + "\n");
+ Assert.ok(Math.abs(att.size - originalTextByteCount) <= epsilon);
+ totalSize += att.size;
+ }
+
+ // Undefined means true.
+ if (checkTotalSize !== false) {
+ dump(
+ "*** Total size comparison: " + totalSize + " vs " + aMimeMsg.size + "\n"
+ );
+ Assert.ok(Math.abs(aMimeMsg.size - totalSize) <= epsilon);
+ }
+}
+
+function check_bogus_parts(aMimeMsg, { epsilon, checkSize }) {
+ if (aMimeMsg == null) {
+ do_throw("We really should have gotten a result!");
+ }
+
+ // First make sure the size is computed properly
+ let x = parseInt(aMimeMsg.size);
+ Assert.ok(!isNaN(x));
+
+ let sep = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n";
+
+ if (checkSize) {
+ let partSize = 0;
+ // The attachment, although a MimeUnknown part, is actually plain/text that
+ // contains the whole attached message, including headers. Count them.
+ for (let k in bogusMessage.headers) {
+ let v = bogusMessage.headers[k];
+ partSize += (k + ": " + v + sep).length;
+ }
+ // That's the newline between the headers and the message body.
+ partSize += sep.length;
+ // That's the message body.
+ partSize += originalTextByteCount;
+ // That's the total length that's to be returned by the MimeMessage abstraction.
+ let totalSize = htmlText.length + partSize;
+ dump(totalSize + " vs " + aMimeMsg.size + "\n");
+ Assert.ok(Math.abs(aMimeMsg.size - totalSize) <= epsilon);
+ }
+}
+
+async function bogus_messages(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ try {
+ check_bogus_parts(aMimeMsg, info);
+ promiseResolve();
+ } catch (e) {
+ do_throw(e);
+ }
+ });
+
+ await promise;
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_mime_emitter.js b/comm/mailnews/db/gloda/test/unit/test_mime_emitter.js
new file mode 100644
index 0000000000..3380a0937e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_mime_emitter.js
@@ -0,0 +1,746 @@
+/* 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/. */
+
+/*
+ * General testing of the JS Mime Emitter to make sure it doesn't choke on any
+ * scenarios.
+ *
+ * We do not test, but should consider testing:
+ * - MimeEncryptedPKCS7, whatever that translates to.
+ * - apple double
+ * - sun attachment
+ */
+
+/*
+ * Do not include GlodaTestHelper because we do not want gloda loaded and it
+ * adds a lot of runtime overhead which makes certain debugging strategies like
+ * using chronicle-recorder impractical.
+ */
+
+var { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var {
+ MessageGenerator,
+ SyntheticPartLeaf,
+ SyntheticPartMultiAlternative,
+ SyntheticDegeneratePartEmpty,
+ SyntheticPartMultiSignedSMIME,
+ SyntheticPartMultiMixed,
+ SyntheticPartMultiSignedPGP,
+ SyntheticPartMultiRelated,
+ SyntheticPartMultiDigest,
+ SyntheticPartMultiParallel,
+ SyntheticMessageSet,
+} = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+// While we're at it, we'll also test the correctness of the GlodaAttachment
+// representation, esp. its "I just need the part information to rebuild the
+// URLs" claim.
+var { GlodaFundAttr } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaFundAttr.jsm"
+);
+
+const DEATH_TO_NEWLINE_TYPE_THINGS = /[\r\n]+/g;
+var msgGen = new MessageGenerator();
+var messageInjection;
+
+var partText = new SyntheticPartLeaf("I am text! Woo!");
+var partHtml = new SyntheticPartLeaf(
+ "<html><head></head><body>I am HTML! Woo! </body></html>",
+ {
+ contentType: "text/html",
+ }
+);
+var partEnriched = new SyntheticPartLeaf(
+ "<bold><italic>I am not a popular format! sad woo :(</italic></bold>",
+ {
+ contentType: "text/enriched",
+ }
+);
+var partAlternative = new SyntheticPartMultiAlternative([partText, partHtml]);
+var partMailingListFooter = new SyntheticPartLeaf("I am an annoying footer!");
+
+// We need to make sure a part that has content-disposition: attachment, even
+// though it doesn't have any filename, still is treated as an attachment.
+var tachNoFilename = {
+ body: "I like Bordeaux wine",
+ contentType: "text/plain",
+ disposition: "attachment",
+};
+
+// This is an external attachment, i.e. a mime part that basically says "go find
+// the attachment on disk, assuming it still exists, here's the path to the file
+// on disk". It turns out feed enclosures are presented in the exact same way,
+// so this covers this case as well.
+var tachExternal = {
+ body:
+ "You deleted an attachment from this message. The original MIME headers for the attachment were:\n" +
+ "Content-Type: image/png;\n" +
+ ' name="conversations-bug1.png"\n' +
+ "Content-Transfer-Encoding: base64\n" +
+ "Content-Disposition: attachment;\n" +
+ ' filename="conversations-bug1.png"',
+ contentType: "image/png",
+ filename: "conversations-bug1.png",
+ charset: null,
+ format: null,
+ encoding: "base64",
+ extraHeaders: {
+ "X-Mozilla-External-Attachment-URL": "file:///tmp/conversations-bug1.png",
+ "X-Mozilla-Altered": 'AttachmentDetached; date="Wed Aug 03 11:11:33 2011"',
+ },
+};
+var tachText = { filename: "bob.txt", body: "I like cheese!" };
+var partTachText = new SyntheticPartLeaf(tachText.body, tachText);
+var tachInlineText = {
+ filename: "foo.txt",
+ body: "Rock the mic",
+ format: null,
+ charset: null,
+ disposition: "inline",
+};
+new SyntheticPartLeaf(tachInlineText.body, tachInlineText);
+
+var tachImage = {
+ filename: "bob.png",
+ contentType: "image/png",
+ encoding: "base64",
+ charset: null,
+ format: null,
+ body: "YWJj\n",
+};
+var partTachImage = new SyntheticPartLeaf(tachImage.body, tachImage);
+
+var relImage = {
+ contentType: "image/png",
+ encoding: "base64",
+ charset: null,
+ format: null,
+ contentId: "part1.foo@bar.invalid",
+ body: "YWJj\n",
+};
+var partRelImage = new SyntheticPartLeaf(relImage.body, relImage);
+
+var tachVCard = {
+ filename: "bob.vcf",
+ contentType: "text/vcard",
+ encoding: "7bit",
+ body: "begin:vcard\nfn:Bob\nend:vcard\n",
+};
+var partTachVCard = new SyntheticPartLeaf(tachVCard.body, tachVCard);
+
+var tachApplication = {
+ filename: "funky.funk",
+ contentType: "application/x-funky",
+ body: "funk!",
+};
+var partTachApplication = new SyntheticPartLeaf(
+ tachApplication.body,
+ tachApplication
+);
+
+var partTachMessages = [msgGen.makeMessage(), msgGen.makeMessage()];
+
+var partEmpty = new SyntheticDegeneratePartEmpty();
+
+var messageInfos = [
+ // -- Simple
+ {
+ name: "text/plain",
+ bodyPart: partText,
+ },
+ {
+ name: "text/html",
+ bodyPart: partHtml,
+ },
+ // -- Simply ugly
+ {
+ name: "text/enriched",
+ bodyPart: partEnriched,
+ },
+ // -- Simple w/attachment
+ {
+ name: "text/plain w/text attachment (=> multipart/mixed)",
+ bodyPart: partText,
+ attachments: [tachText],
+ },
+ {
+ name: "text/plain w/image attachment (=> multipart/mixed)",
+ bodyPart: partText,
+ attachments: [tachImage],
+ },
+ {
+ name: "text/plain w/vcard attachment (=> multipart/mixed)",
+ bodyPart: partText,
+ attachments: [tachVCard],
+ },
+ {
+ name: "text/plain w/app attachment (=> multipart/mixed)",
+ bodyPart: partText,
+ attachments: [tachApplication],
+ },
+ {
+ name: "text/html w/text attachment (=> multipart/mixed)",
+ bodyPart: partHtml,
+ attachments: [tachText],
+ },
+ {
+ name: "text/html w/image attachment (=> multipart/mixed)",
+ bodyPart: partHtml,
+ attachments: [tachImage],
+ },
+ {
+ name: "text/html w/vcard attachment (=> multipart/mixed)",
+ bodyPart: partHtml,
+ attachments: [tachVCard],
+ },
+ {
+ name: "text/html w/app attachment (=> multipart/mixed)",
+ bodyPart: partHtml,
+ attachments: [tachApplication],
+ },
+ // -- Alternatives
+ {
+ name: "multipart/alternative: text/plain, text/html",
+ bodyPart: partAlternative,
+ },
+ {
+ name: "multipart/alternative plain/html w/text attachment",
+ bodyPart: partAlternative,
+ attachments: [tachText],
+ },
+ {
+ name: "multipart/alternative plain/html w/image attachment",
+ bodyPart: partAlternative,
+ attachments: [tachImage],
+ },
+ {
+ name: "multipart/alternative plain/html w/vcard attachment",
+ bodyPart: partAlternative,
+ attachments: [tachVCard],
+ },
+ {
+ name: "multipart/alternative plain/html w/app attachment",
+ bodyPart: partAlternative,
+ attachments: [tachApplication],
+ },
+ // -- S/MIME.
+ {
+ name: "S/MIME alternative",
+ bodyPart: new SyntheticPartMultiSignedSMIME(partAlternative),
+ },
+ {
+ name: "S/MIME alternative with text attachment inside",
+ // We have to do the attachment packing ourselves on this one.
+ bodyPart: new SyntheticPartMultiSignedSMIME(
+ new SyntheticPartMultiMixed([partAlternative, partTachText])
+ ),
+ },
+ {
+ name: "S/MIME alternative with image attachment inside",
+ // We have to do the attachment packing ourselves on this one.
+ bodyPart: new SyntheticPartMultiSignedSMIME(
+ new SyntheticPartMultiMixed([partAlternative, partTachImage])
+ ),
+ },
+ {
+ name: "S/MIME alternative with image attachment inside",
+ // We have to do the attachment packing ourselves on this one.
+ bodyPart: new SyntheticPartMultiSignedSMIME(
+ new SyntheticPartMultiMixed([partAlternative, partTachVCard])
+ ),
+ },
+ {
+ name: "S/MIME alternative with app attachment inside",
+ // We have to do the attachment packing ourselves on this one.
+ bodyPart: new SyntheticPartMultiSignedSMIME(
+ new SyntheticPartMultiMixed([partAlternative, partTachApplication])
+ ),
+ },
+ {
+ name: "S/MIME alternative wrapped in mailing list",
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartMultiSignedSMIME(partAlternative),
+ partMailingListFooter,
+ ]),
+ },
+ // -- PGP signature
+ // We mainly care that all the content-type parameters show up.
+ {
+ name: "PGP signed alternative",
+ bodyPart: new SyntheticPartMultiSignedPGP(partAlternative),
+ },
+ // -- Attached RFC822
+ {
+ // Not your average attachment, pack ourselves for now.
+ name: "attached rfc822",
+ bodyPart: new SyntheticPartMultiMixed([
+ partAlternative,
+ partTachMessages[0],
+ ]),
+ },
+ // -- Multipart/related
+ {
+ name: "multipart/related",
+ bodyPart: new SyntheticPartMultiRelated([partHtml, partRelImage]),
+ },
+ {
+ name: "multipart/related inside multipart/alternative",
+ bodyPart: new SyntheticPartMultiAlternative([
+ partText,
+ new SyntheticPartMultiRelated([partHtml, partRelImage]),
+ ]),
+ },
+ // -- Multipart/digest
+ {
+ name: "multipart/digest",
+ bodyPart: new SyntheticPartMultiDigest(partTachMessages.concat()),
+ },
+ // -- Multipart/parallel (allegedly the same as mixed)
+ {
+ name: "multipart/parallel",
+ bodyPart: new SyntheticPartMultiParallel([partText, partTachImage]),
+ },
+ // --- Previous bugs
+ // -- Bug 495057, text/enriched was being dumb
+ {
+ name: "text/enriched inside related",
+ bodyPart: new SyntheticPartMultiRelated([partEnriched]),
+ },
+ // -- Empty sections
+ // This was a crasher because the empty part made us try and close the
+ // child preceding the empty part a second time. The nested multipart led
+ // to the crash providing evidence of the double-close bug but there was
+ // nothing inherently nested-multipart-requiring to trigger the double-close
+ // bug.
+ {
+ name: "nested multipart with empty multipart section",
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartMultiRelated([partAlternative, partTachText]),
+ partEmpty,
+ ]),
+ },
+ {
+ name: "empty multipart section produces no child",
+ bodyPart: new SyntheticPartMultiMixed([partText, partEmpty, partTachText]),
+ },
+];
+
+add_setup(async function () {
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ registerCleanupFunction(function () {
+ GlodaDatastore.shutdown();
+ });
+});
+
+add_task(async function test_stream_message() {
+ for (let messageInfo of messageInfos) {
+ await stream_message(messageInfo);
+ }
+});
+
+/**
+ * Stream
+ */
+add_task(async function test_sane_bodies() {
+ // 60 bytes long... (becomes 59 on the other side when \r is dropped)
+ let hugeString =
+ "don't know what you want but I can't stream it anymore...\r\n";
+ const powahsOfTwo = 10;
+ for (let i = 0; i < powahsOfTwo; i++) {
+ hugeString = hugeString + hugeString;
+ }
+ // This will come out to be 60k, of course.
+ Assert.equal(hugeString.length, 60 * Math.pow(2, powahsOfTwo));
+
+ let synMsg = msgGen.makeMessage({
+ body: { body: hugeString, contentType: "text/plain" },
+ });
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ null,
+ function (aMsgHdr, aMimeMsg) {
+ let bodyPart = aMimeMsg.parts[0];
+ // (the \r gets gone, so it's only 59 per line)
+ if (bodyPart.body.length > 20 * 1024 + 59) {
+ do_throw(
+ "Mime body length is " +
+ bodyPart.body.length +
+ " bytes long but should not be!"
+ );
+ }
+ promiseResolve();
+ },
+ false,
+ { saneBodySize: true }
+ );
+
+ await promise;
+});
+
+// Additional testing for the correctness of allAttachments and
+// allUserAttachments representation
+
+var partTachNestedMessages = [
+ // Looks like the synthetic part generator appends the charset=ISO-8859-1 part
+ // all by itself. That allows us to create a non-UTF-8 subject, and ensure the
+ // resulting attachment name is indeed São Paulo.eml.
+ msgGen.makeMessage({
+ subject: "S" + String.fromCharCode(0xe3) + "o Paulo",
+ bodyPart: new SyntheticPartLeaf(
+ "<html><head></head><body>I am HTML! Woo! </body></html>",
+ {
+ contentType: "text/html",
+ }
+ ),
+ }),
+ msgGen.makeMessage({
+ attachments: [tachImage],
+ }),
+ msgGen.makeMessage({
+ attachments: [tachImage, tachApplication],
+ }),
+];
+
+var attMessagesParams = [
+ {
+ attachments: [tachNoFilename],
+ },
+ {
+ attachments: [tachExternal],
+ },
+ {
+ name: "attached rfc822",
+ bodyPart: new SyntheticPartMultiMixed([
+ partAlternative,
+ partTachNestedMessages[0],
+ ]),
+ },
+ {
+ name: "attached rfc822 w. image inside",
+ bodyPart: new SyntheticPartMultiMixed([
+ partAlternative,
+ partTachNestedMessages[1],
+ ]),
+ },
+ {
+ name: "attached x/funky + attached rfc822 w. (image + x/funky) inside",
+ bodyPart: new SyntheticPartMultiMixed([
+ partAlternative,
+ partTachApplication,
+ partTachNestedMessages[2],
+ ]),
+ },
+];
+
+var expectedAttachmentsInfo = [
+ {
+ allAttachmentsContentTypes: ["text/plain"],
+ allUserAttachmentsContentTypes: ["text/plain"],
+ },
+ {
+ allAttachmentsContentTypes: ["image/png"],
+ allUserAttachmentsContentTypes: ["image/png"],
+ },
+ {
+ allAttachmentsContentTypes: [],
+ allUserAttachmentsContentTypes: ["message/rfc822"],
+ firstAttachmentName: "S\u00e3o Paulo.eml",
+ },
+ {
+ allAttachmentsContentTypes: ["image/png"],
+ allUserAttachmentsContentTypes: ["message/rfc822"],
+ },
+ {
+ allAttachmentsContentTypes: [
+ "application/x-funky",
+ "image/png",
+ "application/x-funky",
+ ],
+ allUserAttachmentsContentTypes: ["application/x-funky", "message/rfc822"],
+ },
+];
+
+add_task(async function test_attachments_correctness() {
+ for (let [i, params] of attMessagesParams.entries()) {
+ let synMsg = msgGen.makeMessage(params);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ null,
+ function (aMsgHdr, aMimeMsg) {
+ try {
+ let expected = expectedAttachmentsInfo[i];
+ if ("firstAttachmentName" in expected) {
+ let att = aMimeMsg.allUserAttachments[0];
+ Assert.equal(att.name.length, expected.firstAttachmentName.length);
+ for (let j = 0; j < att.name.length; ++j) {
+ Assert.equal(
+ att.name.charCodeAt(j),
+ expected.firstAttachmentName.charCodeAt(j)
+ );
+ }
+ }
+
+ Assert.equal(
+ aMimeMsg.allAttachments.length,
+ expected.allAttachmentsContentTypes.length
+ );
+ for (let [j, att] of aMimeMsg.allAttachments.entries()) {
+ Assert.equal(
+ att.contentType,
+ expected.allAttachmentsContentTypes[j]
+ );
+ }
+
+ Assert.equal(
+ aMimeMsg.allUserAttachments.length,
+ expected.allUserAttachmentsContentTypes.length
+ );
+ for (let [j, att] of aMimeMsg.allUserAttachments.entries()) {
+ Assert.equal(
+ att.contentType,
+ expected.allUserAttachmentsContentTypes[j]
+ );
+ }
+
+ // Test
+ for (let att of aMimeMsg.allUserAttachments) {
+ let uri = aMsgHdr.folder.getUriForMsg(aMsgHdr);
+ let glodaAttachment = GlodaFundAttr.glodaAttFromMimeAtt(
+ { folderMessageURI: uri },
+ att
+ );
+ // The GlodaAttachment appends the filename, which is not always
+ // present
+ Assert.ok(glodaAttachment.url.startsWith(att.url));
+ }
+ } catch (e) {
+ dump(aMimeMsg.prettyString() + "\n");
+ do_throw(e);
+ }
+
+ promiseResolve();
+ },
+ false
+ );
+
+ await promise;
+ }
+});
+
+var bogusMessage = msgGen.makeMessage({ body: { body: "whatever" } });
+bogusMessage._contentType = "woooooo"; // Breaking abstraction boundaries. Bad.
+
+var weirdMessageInfos = [
+ // This message has an unnamed part as an attachment (with
+ // Content-Disposition: inline and which is displayable inline). Previously,
+ // libmime would emit notifications for this to be treated as an attachment,
+ // name Part 1.2. Now it's not the case anymore, so we should ensure this
+ // message has no attachments.
+ {
+ name: "test message with part 1.2 attachment",
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ format: "",
+ },
+ ],
+ },
+];
+
+add_task(async function test_part12_not_an_attachment() {
+ let synMsg = msgGen.makeMessage(weirdMessageInfos[0]);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ try {
+ Assert.ok(aMimeMsg.allUserAttachments.length == 0);
+ Assert.ok(aMimeMsg.allAttachments.length == 0);
+ } catch (e) {
+ do_throw(e);
+ }
+ promiseResolve();
+ });
+
+ await promise;
+});
+
+async function stream_message(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ verify_stream_message(info, synMsg, aMsgHdr, aMimeMsg);
+ promiseResolve();
+ });
+
+ await promise;
+}
+/**
+ * Verify the streamed results are what we wanted. For now, this just means
+ * receiving a representation; we don't check it for correctness.
+ */
+function verify_stream_message(aInfo, aSynMsg, aMsgHdr, aMimeMsg) {
+ if (aMimeMsg == null) {
+ do_throw("We really should have gotten a result!");
+ }
+ try {
+ // aMimeMsg is normalized; it only ever actually gets one child.
+ verify_body_part_equivalence(aSynMsg.bodyPart, aMimeMsg.parts[0]);
+ } catch (ex) {
+ dump("Something was wrong with the MIME rep!\n!!!!!!!!\n");
+ dump("Synthetic looks like:\n " + aSynMsg.prettyString() + "\n\n");
+ dump(
+ "MIME looks like: \n" + aMimeMsg.prettyString(true, " ", true) + "\n\n"
+ );
+ do_throw(ex);
+ }
+
+ dump("Everything is just fine.\n");
+ dump("Synthetic looks like:\n " + aSynMsg.prettyString() + "\n\n");
+ dump(
+ "MIME looks like:\n " + aMimeMsg.prettyString(true, " ", false) + "\n\n"
+ );
+}
+
+/**
+ * Applies any transformations to the synthetic body part that we would expect
+ * to happen to a message during its libmime journey. It may be better to
+ * just put the expected translations in the synthetic body part instead of
+ * trying to make this method do anything complex.
+ */
+function synTransformBody(aSynBodyPart) {
+ let text = aSynBodyPart.body.trim();
+ // This transforms things into HTML apparently.
+ if (aSynBodyPart._contentType == "text/enriched") {
+ // Our job here is just to transform just enough for our example above.
+ // We also could have provided a manual translation on the body part.
+ text = text.replace(/bold/g, "B").replace(/italic/g, "I");
+ }
+ return text;
+}
+
+function verify_body_part_equivalence(aSynBodyPart, aMimePart) {
+ // The content-type devoid of parameters should match.
+ Assert.equal(aSynBodyPart._contentType, aMimePart.contentType);
+
+ // The header representation of the content-type should also match unless
+ // this is an rfc822 part, in which case it should only match for the
+ // actual contents.
+ if (aMimePart.contentType != "message/rfc822") {
+ Assert.equal(
+ aSynBodyPart.contentTypeHeaderValue.replace(
+ DEATH_TO_NEWLINE_TYPE_THINGS,
+ ""
+ ),
+ aMimePart.get("content-type").replace(DEATH_TO_NEWLINE_TYPE_THINGS, "")
+ );
+ }
+
+ // XXX body part checking will get brittle if we ever actually encode things!
+ if (
+ aSynBodyPart.body &&
+ !aSynBodyPart._filename &&
+ aSynBodyPart._contentType.startsWith("text/")
+ ) {
+ Assert.equal(
+ synTransformBody(aSynBodyPart),
+ aMimePart.body
+ .trim()
+ .replace(/\r/g, "")
+ // Remove stuff added by libmime for HTML parts.
+ .replace(
+ /[\n]*<meta http-equiv="content-type" content="text\/html; .*">[\n]*/g,
+ ""
+ )
+ .replace(/[\n]+<\/body>/, "</body>")
+ );
+ }
+ if (aSynBodyPart.parts) {
+ let iPart;
+ let realPartOffsetCompensator = 0;
+ for (iPart = 0; iPart < aSynBodyPart.parts.length; iPart++) {
+ let subSyn = aSynBodyPart.parts[iPart];
+ // If this is a degenerate empty, it should not produce output, so
+ // compensate for the offset drift and get on with our lives.
+ if (subSyn instanceof SyntheticDegeneratePartEmpty) {
+ realPartOffsetCompensator--;
+ continue;
+ }
+ let subMime = aMimePart.parts[iPart + realPartOffsetCompensator];
+ // Our special case is the signature, which libmime does not expose to us.
+ // Ignore! (Also, have our too-many-part checker below not trip on this.)
+ if (subSyn._contentType != "application/x-pkcs7-signature") {
+ if (subMime == null) {
+ do_throw(
+ "No MIME part matching " + subSyn.contentTypeHeaderValue + "\n"
+ );
+ }
+ verify_body_part_equivalence(subSyn, subMime);
+ }
+ }
+ // Only check if there are still more mime parts; don't check for a count
+ // mismatch (the PKCS case from above needs to be handled).
+ if (iPart < aMimePart.parts.length) {
+ do_throw("MIME part has more sub-parts than syn part?");
+ }
+ }
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_msg_search.js b/comm/mailnews/db/gloda/test/unit/test_msg_search.js
new file mode 100644
index 0000000000..2c8ea1c528
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_msg_search.js
@@ -0,0 +1,155 @@
+/* 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/. */
+
+/*
+ * Test GlodaMsgSearcher.jsm our heuristic-based fulltext search mechanism. Things we
+ * generally want to verify:
+ * - fulltext weighting by where the match happened works.
+ * - static interestingness impacts things appropriately.
+ *
+ * Our general strategy is to create two messages each with a unique string
+ * placed in controlled places and whatever intentional message manipulation
+ * is required to set things up. Then we query using a GlodaMsgSearcher with
+ * the limit set to 1. Only the message we expect should come back.
+ * Keep in mind in all tests that our underlying ranking mechanism is based on
+ * time so the date of each message is relevant but should not be significant
+ * because our score boost factor should always be well in excess of the one
+ * hour increment between messages.
+ *
+ * Previously, we relied on the general equivalence of the logic in
+ * test_query_core to our message search logic.
+ */
+
+var {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { GlodaMsgSearcher } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaMsgSearcher.jsm"
+);
+var { waitForGlodaDBFlush } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var uniqueCounter = 0;
+var messageInjection;
+
+add_setup(async function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Verify that the ranking function is using the weights as expected. We do not
+ * need to test all the permutations
+ */
+add_task(async function test_fulltext_weighting_by_column() {
+ let ustr = unique_string();
+ let [, subjSet, bodySet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, subject: ustr },
+ { count: 1, body: { body: ustr } },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([subjSet, bodySet]));
+ await asyncMsgSearcherExpect(ustr, subjSet);
+});
+
+/**
+ * A term mentioned 3 times in the body is worth more than twice in the subject.
+ * (This is because the subject saturates at one occurrence worth 2.0 and the
+ * body does not saturate until 10, each worth 1.0.)
+ */
+add_task(async function test_fulltext_weighting_saturation() {
+ let ustr = unique_string();
+ let double_ustr = ustr + " " + ustr;
+ let thrice_ustr = ustr + " " + ustr + " " + ustr;
+ let [, subjSet, bodySet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, subject: double_ustr },
+ { count: 1, body: { body: thrice_ustr } },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([subjSet, bodySet]));
+ await asyncMsgSearcherExpect(ustr, bodySet);
+});
+
+/**
+ * Use a starred message with the same fulltext match characteristics as another
+ * message to verify the preference goes the right way. Have the starred
+ * message be the older message for safety.
+ */
+add_task(async function test_static_interestingness_boost_works() {
+ let ustr = unique_string();
+ let [, starred, notStarred] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, subject: ustr },
+ { count: 1, subject: ustr },
+ ]);
+ // Index in their native state.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([starred, notStarred]));
+ // Star and index.
+ starred.setStarred(true);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([starred]));
+ // Stars upon thars wins.
+ await asyncMsgSearcherExpect(ustr, starred);
+});
+
+/**
+ * Make sure that the query does not retrieve more than actually matches.
+ */
+add_task(async function test_joins_do_not_return_everybody() {
+ let ustr = unique_string();
+ let [, subjSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1, subject: ustr },
+ ]);
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([subjSet]));
+ await asyncMsgSearcherExpect(ustr, subjSet, 2);
+});
+
+/**
+ * Generate strings like "aaaaa", "aabaa", "aacaa", etc. The idea with the
+ * suffix is to avoid the porter stemmer from doing something weird that
+ * collapses things.
+ */
+function unique_string() {
+ let uval = uniqueCounter++;
+ let s =
+ String.fromCharCode(97 + Math.floor(uval / (26 * 26))) +
+ String.fromCharCode(97 + (Math.floor(uval / 26) % 26)) +
+ String.fromCharCode(97 + (uval % 26)) +
+ "aa";
+ return s;
+}
+
+/**
+ * Wrap the construction of a GlodaMsgSearcher with a limit of 1 and feed it to
+ * queryExpect.
+ *
+ * @param aFulltextStr The fulltext query string which GlodaMsgSearcher will
+ * parse.
+ * @param aExpectedSet The expected result set. Make sure that the size of the
+ * set is consistent with aLimit.
+ * @param [aLimit=1]
+ *
+ * Use like so:
+ * await asyncMsgSearchExpect("foo bar", someSynMsgSet);
+ */
+async function asyncMsgSearcherExpect(aFulltextStr, aExpectedSet, aLimit) {
+ let limit = aLimit ? aLimit : 1;
+ Services.prefs.setIntPref("mailnews.database.global.search.msg.limit", limit);
+ let searcher = new GlodaMsgSearcher(null, aFulltextStr);
+ await queryExpect(searcher.buildFulltextQuery(), aExpectedSet);
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_noun_mimetype.js b/comm/mailnews/db/gloda/test/unit/test_noun_mimetype.js
new file mode 100644
index 0000000000..128720ee76
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_noun_mimetype.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/*
+ * Test noun_mimetype. Exists because I just changed its implementation and I'm
+ * afraid I may have damaged it and it's hard to tell, so ironically a unit test
+ * is the easiest solution. (Don't you hate it when the right thing to do is
+ * also the easy thing to do?)
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { waitForGlodaDBFlush } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { MimeTypeNoun } = ChromeUtils.import(
+ "resource:///modules/gloda/NounMimetype.jsm"
+);
+
+var passResults = [];
+var curPassResults;
+
+add_setup(async function () {
+ let msgGen = new MessageGenerator();
+ let messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+add_task(async function test_new_pass_first_time() {
+ await new_pass();
+});
+
+add_task(function test_basics_first_time() {
+ test_basics();
+});
+
+/**
+ * Do two passes of test_basics making sure that persisted values really
+ * persist.
+ */
+add_task(async function test_new_pass_second_time() {
+ await new_pass();
+});
+
+add_task(function test_basics_second_time() {
+ test_basics();
+});
+
+add_task(function verify_passes_are_the_same() {
+ var firstPassResults = passResults[0];
+ for (let iType = 0; iType < curPassResults.length; iType++) {
+ for (let iPass = 1; iPass < passResults.length; iPass++) {
+ Assert.equal(firstPassResults[iType].id, passResults[iPass][iType].id);
+ }
+ }
+});
+
+add_task(function test_parameters() {
+ let plain = MimeTypeNoun.getMimeType("text/plain");
+ Assert.equal(plain, MimeTypeNoun.getMimeType('text/plain; charset="UTF-8"'));
+});
+
+/**
+ * Setup a new 'pass' by nuking the MimeTypeNoun's state if it has any. The
+ * goal here is to verify that the database persistence is actually working,
+ * and we can only do that if we convince it to nuke its authoritative 'cache'
+ * and grab a new copy.
+ */
+async function new_pass() {
+ // We have to nuke if it has already happened.
+ if (passResults.length) {
+ MimeTypeNoun._mimeTypes = {};
+ MimeTypeNoun._mimeTypesByID = {};
+ MimeTypeNoun._mimeTypeHighID = {};
+ MimeTypeNoun._highID = 0;
+ MimeTypeNoun._init();
+ }
+ curPassResults = [];
+ passResults.push(curPassResults);
+
+ // The mime type does some async stuff... make sure we don't advance until
+ // it is done with said stuff.
+ await waitForGlodaDBFlush();
+}
+
+function test_basics() {
+ let python;
+ // If this is not the first pass, check for python before other things to
+ // make sure we're not just relying on consistent logic rather than actual
+ // persistence.
+ if (passResults.length) {
+ python = MimeTypeNoun.getMimeType("text/x-python");
+ }
+
+ let jpeg = MimeTypeNoun.getMimeType("image/jpeg");
+ curPassResults.push(jpeg);
+
+ let png = MimeTypeNoun.getMimeType("image/png");
+ curPassResults.push(png);
+
+ let html = MimeTypeNoun.getMimeType("text/html");
+ curPassResults.push(html);
+
+ let plain = MimeTypeNoun.getMimeType("text/plain");
+ curPassResults.push(plain);
+
+ // If this is for the first time, check for python now (see above).
+ if (!passResults.length) {
+ python = MimeTypeNoun.getMimeType("text/x-python");
+ }
+ // But always add it to the results now, as we need consistent ordering
+ // since we use a list.
+ curPassResults.push(python);
+
+ // Sanity-checking the parsing.
+ Assert.equal(jpeg.type, "image");
+ Assert.equal(jpeg.subType, "jpeg");
+
+ // - Make sure the numeric trickiness for the block stuff is actually doing
+ // the right thing!
+ const BLOCK_SIZE = MimeTypeNoun.TYPE_BLOCK_SIZE;
+ // Same blocks.
+ Assert.equal(
+ Math.floor(jpeg.id / BLOCK_SIZE),
+ Math.floor(png.id / BLOCK_SIZE)
+ );
+ Assert.equal(
+ Math.floor(html.id / BLOCK_SIZE),
+ Math.floor(plain.id / BLOCK_SIZE)
+ );
+ // Different blocks.
+ Assert.notEqual(
+ Math.floor(jpeg.id / BLOCK_SIZE),
+ Math.floor(html.id / BLOCK_SIZE)
+ );
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_nuke_migration.js b/comm/mailnews/db/gloda/test/unit/test_nuke_migration.js
new file mode 100644
index 0000000000..e47eac75bc
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_nuke_migration.js
@@ -0,0 +1,62 @@
+/**
+ * Atypical gloda unit test that tests nuke migration. Gloda is not designed
+ * to be shutdown and started up again in the same process lifetime. It tries
+ * to be clever with caching accessors that clobber themselves out of existence
+ * which are hard to make come back to life, and probably other things.
+ *
+ * So what we do is create a global-messages-db.sqlite with an unacceptably
+ * old schema version before tickling gloda to startup. If gloda comes up
+ * with a database connection and it has the right schema version, we declare
+ * that gloda has successfully loaded. Our only historical screw-up here was
+ * very blatant (and was actually a result of trying to avoid complexity in
+ * the nuke path! oh the irony!) so we don't need to get all hardcore.
+ */
+
+/**
+ * The DB version to use. We set this as a non-const variable so that
+ * test_nuke_migration_from_future.js can change it.
+ */
+var BAD_DB_VERSION_TO_USE = 2;
+
+/**
+ * Synchronously create and close the out-of-date database. Because we are
+ * only using synchronous APIs, we know everything is in fact dead. GC being
+ * what it is, the various C++ objects will probably stay alive through the
+ * next test, but will be inert because we have closed the database.
+ */
+function make_out_of_date_database() {
+ // Get the path to our global database
+ var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("global-messages-db.sqlite");
+
+ // Create the database
+ var dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+ dbConnection.schemaVersion = BAD_DB_VERSION_TO_USE;
+
+ // Close the database (will throw if there's a problem closing)
+ dbConnection.close();
+}
+
+// some copied and pasted preference setup from glodaTestHelper that is
+// appropriate here.
+// yes to indexing
+Services.prefs.setBoolPref("mailnews.database.global.indexer.enabled", true);
+// no to a sweep we don't control
+Services.prefs.setBoolPref(
+ "mailnews.database.global.indexer.perform_initial_sweep",
+ false
+);
+
+function run_test() {
+ // - make the old database
+ make_out_of_date_database();
+
+ // - tickle gloda
+ // GlodaPublic.jsm loads Gloda.jsm which self-initializes and initializes the datastore
+ ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+ let { GlodaDatastore } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaDatastore.jsm"
+ );
+
+ Assert.notEqual(GlodaDatastore.asyncConnection, null);
+}
diff --git a/comm/mailnews/db/gloda/test/unit/test_nuke_migration_from_future.js b/comm/mailnews/db/gloda/test/unit/test_nuke_migration_from_future.js
new file mode 100644
index 0000000000..f60c1dd29e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_nuke_migration_from_future.js
@@ -0,0 +1,12 @@
+/**
+ * There are actually two ways the nuke migration can be invoked. From
+ * a database too far from the future, and too far from the past. This
+ * one is the future one. We must keep ourselves safe from time-traveling
+ * grandchildren!
+ */
+
+/* import-globals-from test_nuke_migration.js */
+load("test_nuke_migration.js");
+
+// pick something so far forward it will never get used!
+BAD_DB_VERSION_TO_USE = 100000000;
diff --git a/comm/mailnews/db/gloda/test/unit/test_query_core.js b/comm/mailnews/db/gloda/test/unit/test_query_core.js
new file mode 100644
index 0000000000..0849a62d50
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_query_core.js
@@ -0,0 +1,658 @@
+/* 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/. */
+
+/*
+ * Test the mechanics our query functionality. Tests in this file are intended
+ * to cover extreme boundary cases and things that are just unlikely to happen
+ * in reasonable message use-cases. (Which is to say, it could be hard to
+ * formulate a set of synthetic messages that result in the situation we want
+ * to test for.)
+ */
+
+var { prepareIndexerForTesting } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { queryExpect } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaQueryHelper.jsm"
+);
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+var { GlodaConstants } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaConstants.jsm"
+);
+var { GlodaIndexer, IndexingJob } = ChromeUtils.import(
+ "resource:///modules/gloda/GlodaIndexer.jsm"
+);
+
+/* ===== Test Noun ===== */
+/*
+ * Introduce a simple noun type for our testing so that we can avoid having to
+ * deal with the semantics of messages/friends and all their complexity.
+ */
+
+var WidgetProvider = {
+ providerName: "widget",
+ *process() {
+ yield GlodaConstants.kWorkDone;
+ },
+};
+
+add_setup(function () {
+ // Don't initialize the index message state
+ prepareIndexerForTesting();
+ GlodaIndexer.registerIndexer(GenericIndexer);
+ Gloda.addIndexerListener(genericIndexerCallback);
+});
+
+var WidgetNoun;
+add_task(function setup_test_noun_and_attributes() {
+ // --- noun
+ WidgetNoun = Gloda.defineNoun({
+ name: "widget",
+ clazz: Widget,
+ allowsArbitraryAttrs: true,
+ // It is vitally important to our correctness that we allow caching
+ // otherwise our in-memory representations will not be canonical and the db
+ // will load some. Or we could add things to collections as we index them.
+ cache: true,
+ cacheCost: 32,
+ schema: {
+ columns: [
+ ["id", "INTEGER PRIMARY KEY"],
+ ["intCol", "NUMBER", "inum"],
+ // datePRTime is special and creates a Date object.
+ ["dateCol", "NUMBER", "datePRTime"],
+ ["strCol", "STRING", "str"],
+ ["notabilityCol", "NUMBER", "notability"],
+ ["textOne", "STRING", "text1"],
+ ["textTwo", "STRING", "text2"],
+ ],
+ indices: {
+ intCol: ["intCol"],
+ strCol: ["strCol"],
+ },
+ fulltextColumns: [
+ ["fulltextOne", "TEXT", "text1"],
+ ["fulltextTwo", "TEXT", "text2"],
+ ],
+ genericAttributes: true,
+ },
+ });
+
+ const EXT_NAME = "test";
+
+ // --- special (on-row) attributes
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "inum",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "intCol",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ });
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "date",
+ singular: true,
+ special: GlodaConstants.kSpecialColumn,
+ specialColumnName: "dateCol",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_DATE,
+ canQuery: true,
+ });
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "str",
+ singular: true,
+ special: GlodaConstants.kSpecialString,
+ specialColumnName: "strCol",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_STRING,
+ canQuery: true,
+ });
+
+ // --- fulltext attributes
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "text1",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "fulltextOne",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ canQuery: true,
+ });
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "text2",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: "fulltextTwo",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ canQuery: true,
+ });
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "fulltextAll",
+ singular: true,
+ special: GlodaConstants.kSpecialFulltext,
+ specialColumnName: WidgetNoun.tableName + "Text",
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_FULLTEXT,
+ canQuery: true,
+ });
+
+ // --- external (attribute-storage) attributes
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "singleIntAttr",
+ singular: true,
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ });
+
+ Gloda.defineAttribute({
+ provider: WidgetProvider,
+ extensionName: EXT_NAME,
+ attributeType: GlodaConstants.kAttrFundamental,
+ attributeName: "multiIntAttr",
+ singular: false,
+ emptySetIsSignificant: true,
+ subjectNouns: [WidgetNoun.id],
+ objectNoun: GlodaConstants.NOUN_NUMBER,
+ canQuery: true,
+ });
+});
+
+/* ===== Tests ===== */
+
+const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
+add_task(async function test_lots_of_string_constraints() {
+ let stringConstraints = [];
+ for (let i = 0; i < 2049; i++) {
+ stringConstraints.push(
+ ALPHABET[Math.floor(i / (ALPHABET.length * 2)) % ALPHABET.length] +
+ ALPHABET[Math.floor(i / ALPHABET.length) % ALPHABET.length] +
+ ALPHABET[i % ALPHABET.length] +
+ // Throw in something that will explode if not quoted
+ // and use an uneven number of things so if we fail
+ // to quote it won't get quietly eaten.
+ "'\""
+ );
+ }
+
+ let query = Gloda.newQuery(WidgetNoun.id);
+ query.str.apply(query, stringConstraints);
+
+ await queryExpect(query, []);
+});
+
+/* === Query === */
+
+/**
+ * Use a counter so that each test can have its own unique value for intCol so
+ * that it can use that as a constraint. Otherwise we would need to purge
+ * between every test. That's not an unreasonable alternative, but this works.
+ * Every test should increment this before using it.
+ */
+var testUnique = 100;
+
+/**
+ * Widgets with multiIntAttr populated with one or more values.
+ */
+var nonSingularWidgets;
+/**
+ * Widgets with multiIntAttr unpopulated.
+ */
+var singularWidgets;
+
+add_task(async function setup_non_singular_values() {
+ testUnique++;
+ let origin = new Date("2007/01/01");
+ nonSingularWidgets = [
+ new Widget(testUnique, origin, "ns1", 0, "", ""),
+ new Widget(testUnique, origin, "ns2", 0, "", ""),
+ ];
+ singularWidgets = [
+ new Widget(testUnique, origin, "s1", 0, "", ""),
+ new Widget(testUnique, origin, "s2", 0, "", ""),
+ ];
+ nonSingularWidgets[0].multiIntAttr = [1, 2];
+ nonSingularWidgets[1].multiIntAttr = [3];
+ singularWidgets[0].multiIntAttr = [];
+ // And don't bother setting it on singularWidgets[1].
+
+ GenericIndexer.indexObjects(nonSingularWidgets.concat(singularWidgets));
+ await promiseGenericIndexerCallback;
+
+ // Reset promise.
+ promiseGenericIndexerCallback = new Promise(resolve => {
+ promiseGenericIndexerCallbackResolve = resolve;
+ });
+});
+
+add_task(async function test_query_has_value_for_non_singular() {
+ let query = Gloda.newQuery(WidgetNoun.id);
+ query.inum(testUnique);
+ query.multiIntAttr();
+ await queryExpect(query, nonSingularWidgets);
+});
+
+/**
+ * We should find the one singular object where we set the multiIntAttr to an
+ * empty set. We don't find the one without the attribute since that's
+ * actually something different.
+ * We also want to test that re-indexing properly adds/removes the attribute
+ * so change the object and make sure everything happens correctly.
+ *
+ * @tests gloda.datastore.sqlgen.kConstraintIn.emptySet
+ * @tests gloda.query.test.kConstraintIn.emptySet
+ */
+add_task(async function test_empty_set_logic() {
+ // - Initial query based on the setup previously.
+ dump("Initial index case\n");
+ let query = Gloda.newQuery(WidgetNoun.id);
+ query.inum(testUnique);
+ query.multiIntAttr(null);
+ await queryExpect(query, [singularWidgets[0]]);
+
+ // - Make one of the non-singulars move to empty and move the guy who matched
+ // to no longer match.
+ dump("Incremental index case\n");
+ nonSingularWidgets[0].multiIntAttr = [];
+ singularWidgets[0].multiIntAttr = [4, 5];
+
+ GenericIndexer.indexObjects([nonSingularWidgets[0], singularWidgets[0]]);
+ await promiseGenericIndexerCallback;
+
+ // Reset promise;
+ promiseGenericIndexerCallback = new Promise(resolve => {
+ promiseGenericIndexerCallbackResolve = resolve;
+ });
+
+ query = Gloda.newQuery(WidgetNoun.id);
+ query.inum(testUnique);
+ query.multiIntAttr(null);
+ await queryExpect(query, [nonSingularWidgets[0]]);
+
+ // Make sure that the query doesn't explode when it has to handle a case
+ // that's not supposed to match.
+ Assert.ok(!query.test(singularWidgets[0]));
+});
+
+/* === Search === */
+/*
+ * The conceit of our search is that more recent messages are better than older
+ * messages. But at the same time, we care about some messages more than
+ * others (in general), and we care about messages that match search terms
+ * more strongly too. So we introduce a general 'score' heuristic which we
+ * then apply to message timestamps to make them appear more recent. We
+ * then order by this 'date score' hybrid, which we dub "dascore". Such a
+ * flattening heuristic is over-simple, but believed to be sufficient to
+ * generally get us the messages we want. Post-processing based can then
+ * be more multi-dimensional and what not, but that is beyond the scope of
+ * this unit test.
+ */
+
+/**
+ * How much time boost should a 'score point' amount to? The authoritative,
+ * incontrivertible answer, across all time and space, is a week.
+ * Gloda and storage like to store things as PRTime and so we do it too,
+ * even though milliseconds are the actual granularity of JS Date instances.
+ */
+const SCORE_TIMESTAMP_FACTOR = 1000 * 1000 * 60 * 60 * 24 * 7;
+
+/**
+ * How many score points for each fulltext match?
+ */
+const SCORE_FOR_FULLTEXT_MATCH = 1;
+
+/**
+ * Roughly how many characters are in each offset match.
+ */
+const OFFSET_CHARS_PER_FULLTEXT_MATCH = 8;
+
+var fooWidgets = null;
+var barBazWidgets = null;
+
+add_task(async function setup_search_ranking_idiom() {
+ // --- Build some widgets for testing.
+ // Use inum to represent the expected result sequence
+ // Setup a base date.
+ let origin = new Date("2008/01/01");
+ let daymore = new Date("2008/01/02");
+ let monthmore = new Date("2008/02/01");
+ fooWidgets = [
+ // -- Setup the term "foo" to do frequency tests.
+ new Widget(5, origin, "", 0, "", "foo"),
+ new Widget(4, origin, "", 0, "", "foo foo"),
+ new Widget(3, origin, "", 0, "foo", "foo foo"),
+ new Widget(2, origin, "", 0, "foo foo", "foo foo"),
+ new Widget(1, origin, "", 0, "foo foo", "foo foo foo"),
+ new Widget(0, origin, "", 0, "foo foo foo", "foo foo foo"),
+ ];
+ barBazWidgets = [
+ // -- Setup score and matches to boost older messages over newer messages.
+ new Widget(7, origin, "", 0, "", "bar"), // score boost: 1 + date: 0
+ new Widget(6, daymore, "", 0, "", "bar"), // 1 + 0+
+ new Widget(5, origin, "", 1, "", "bar"), // 2 + 0
+ new Widget(4, daymore, "", 0, "bar", "bar"), // 2 + 0+
+ new Widget(3, origin, "", 1, "bar", "baz"), // 3 + 0
+ new Widget(2, monthmore, "", 0, "", "bar"), // 1 + 4
+ new Widget(1, origin, "", 0, "bar baz", "bar baz bar bar"), // 6 + 0
+ new Widget(0, origin, "", 1, "bar baz", "bar baz bar bar"), // 7 + 0
+ ];
+
+ GenericIndexer.indexObjects(fooWidgets.concat(barBazWidgets));
+ await promiseGenericIndexerCallback;
+
+ // Reset promise.
+ promiseGenericIndexerCallback = new Promise(resolve => {
+ promiseGenericIndexerCallbackResolve = resolve;
+ });
+});
+
+// Add one because the last snippet shouldn't have a trailing space.
+const OFFSET_SCORE_SQL_SNIPPET =
+ "(((length(osets) + 1) / " +
+ OFFSET_CHARS_PER_FULLTEXT_MATCH +
+ ") * " +
+ SCORE_FOR_FULLTEXT_MATCH +
+ ")";
+
+const SCORE_SQL_SNIPPET = "(" + OFFSET_SCORE_SQL_SNIPPET + " + notabilityCol)";
+
+const DASCORE_SQL_SNIPPET =
+ "((" + SCORE_SQL_SNIPPET + " * " + SCORE_TIMESTAMP_FACTOR + ") + dateCol)";
+
+const WIDGET_FULLTEXT_QUERY_EXPLICIT_SQL =
+ "SELECT ext_widget.*, offsets(ext_widgetText) AS osets " +
+ "FROM ext_widget, ext_widgetText WHERE ext_widgetText MATCH ?" +
+ " AND ext_widget.id == ext_widgetText.docid";
+
+/**
+ * Used by queryExpect to verify
+ */
+function verify_widget_order_and_stashing(
+ aZeroBasedIndex,
+ aWidget,
+ aCollection
+) {
+ Assert.equal(aZeroBasedIndex, aWidget.inum);
+ if (
+ !aCollection.stashedColumns[aWidget.id] ||
+ !aCollection.stashedColumns[aWidget.id].length
+ ) {
+ do_throw("no stashed information for widget: " + aWidget);
+ }
+}
+
+/**
+ * Test the fundamentals of the search ranking idiom we use elsewhere. This
+ * is primarily a simplified
+ */
+add_task(async function test_search_ranking_idiom_offsets() {
+ let query = Gloda.newQuery(WidgetNoun.id, {
+ explicitSQL: WIDGET_FULLTEXT_QUERY_EXPLICIT_SQL,
+ // osets becomes 0-based column number 7.
+ // dascore becomes 0-based column number 8.
+ outerWrapColumns: [DASCORE_SQL_SNIPPET + " AS dascore"],
+ // Save our extra columns for analysis and debugging.
+ stashColumns: [7, 8],
+ });
+ query.fulltextAll("foo");
+ query.orderBy("-dascore");
+ await queryExpect(
+ query,
+ fooWidgets,
+ null,
+ null,
+ verify_widget_order_and_stashing
+ );
+});
+
+add_task(async function test_search_ranking_idiom_score() {
+ let query = Gloda.newQuery(WidgetNoun.id, {
+ explicitSQL: WIDGET_FULLTEXT_QUERY_EXPLICIT_SQL,
+ // osets becomes 0-based column number 7
+ // dascore becomes 0-based column number 8
+ outerWrapColumns: [
+ DASCORE_SQL_SNIPPET + " AS dascore",
+ SCORE_SQL_SNIPPET + " AS dabore",
+ "dateCol",
+ ],
+ // Save our extra columns for analysis and debugging.
+ stashColumns: [7, 8, 9, 10],
+ });
+ query.fulltextAll("bar OR baz");
+ query.orderBy("-dascore");
+ await queryExpect(
+ query,
+ barBazWidgets,
+ null,
+ null,
+ verify_widget_order_and_stashing
+ );
+});
+
+/**
+ * Generic indexing mechanism; does nothing special, just uses
+ * Gloda.grokNounItem. Call GenericIndexer.indexNewObjects() to queue
+ * queue your objects for initial indexing.
+ */
+var GenericIndexer = {
+ _log: console.createInstance({
+ prefix: "gloda.test",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "gloda.test.loglevel",
+ }),
+ /* public interface */
+ name: "generic_indexer",
+ enable() {
+ this.enabled = true;
+ },
+ disable() {
+ this.enabled = false;
+ },
+ get workers() {
+ return [
+ [
+ "generic",
+ {
+ worker: this._worker_index_generic,
+ },
+ ],
+ ];
+ },
+ initialSweep() {},
+ /* mock interface */
+ enabled: false,
+ initialSweepCalled: false,
+ indexObjects(aObjects) {
+ indexingInProgress = true;
+ this._log.debug(
+ "enqueuing " +
+ aObjects.length +
+ " generic objects with id: " +
+ aObjects[0].NOUN_ID
+ );
+ GlodaIndexer.indexJob(new IndexingJob("generic", null, aObjects.concat()));
+ },
+ /* implementation */
+ *_worker_index_generic(aJob, aCallbackHandle) {
+ this._log.debug(
+ "Beginning indexing " + aJob.items.length + " generic items"
+ );
+ for (let item of aJob.items) {
+ this._log.debug("Indexing: " + item);
+ yield aCallbackHandle.pushAndGo(
+ Gloda.grokNounItem(
+ item,
+ {},
+ item.id === undefined,
+ item.id === undefined,
+ aCallbackHandle,
+ item.NOUN_DEF.cache
+ )
+ );
+ item._stash();
+ }
+
+ yield GlodaConstants.kWorkDone;
+ this._log.debug("Done indexing");
+ },
+};
+
+var indexingInProgress = false;
+var promiseGenericIndexerCallbackResolve;
+var promiseGenericIndexerCallback = new Promise(resolve => {
+ promiseGenericIndexerCallbackResolve = resolve;
+});
+function genericIndexerCallback(aStatus) {
+ // If indexingInProgress is false, we've received the synthetic
+ // notification, so ignore it.
+ if (indexingInProgress && aStatus == GlodaConstants.kIndexerIdle) {
+ indexingInProgress = false;
+ promiseGenericIndexerCallbackResolve();
+ }
+}
+
+/**
+ * Simple test object.
+ *
+ * Has some tricks for gloda indexing to deal with gloda's general belief that
+ * things are immutable. When we get indexed we stash all of our attributes
+ * at that time in _indexStash. Then when we get cloned we propagate our
+ * current attributes over to the cloned object and restore _indexStash. This
+ * sets things up the way gloda expects them as long as we never de-persist
+ * from the db.
+ */
+function Widget(inum, date, str, notability, text1, text2) {
+ this._id = undefined;
+ this._inum = inum;
+ this._date = date;
+ this._str = str;
+ this._notability = notability;
+ this._text1 = text1;
+ this._text2 = text2;
+
+ this._indexStash = null;
+ this._restoreStash = null;
+}
+Widget.prototype = {
+ _clone() {
+ let clonus = new Widget(
+ this._inum,
+ this._date,
+ this._str,
+ this._notability,
+ this._text1,
+ this._text2
+ );
+ clonus._id = this._id;
+ clonus._iAmAClone = true;
+
+ for (let key of Object.keys(this)) {
+ let value = this[key];
+ if (key.startsWith("_")) {
+ continue;
+ }
+ clonus[key] = value;
+ if (key in this._indexStash) {
+ this[key] = this._indexStash[key];
+ }
+ }
+
+ return clonus;
+ },
+ _stash() {
+ this._indexStash = {};
+ for (let key of Object.keys(this)) {
+ let value = this[key];
+ if (key[0].startsWith("_")) {
+ continue;
+ }
+ this._indexStash[key] = value;
+ }
+ },
+
+ get id() {
+ return this._id;
+ },
+ set id(aVal) {
+ this._id = aVal;
+ },
+
+ // Gloda's attribute idiom demands that row attributes be prefixed with a '_'
+ // (Because Gloda.grokNounItem detects attributes by just walking.). This
+ // could be resolved by having the special attributes moot these dudes, but
+ // that's not how things are right now.
+ get inum() {
+ return this._inum;
+ },
+ set inum(aVal) {
+ this._inum = aVal;
+ },
+ get date() {
+ return this._date;
+ },
+ set date(aVal) {
+ this._date = aVal;
+ },
+
+ get datePRTime() {
+ return this._date.valueOf() * 1000;
+ },
+ // We need a special setter to convert back from PRTime to an actual
+ // date object.
+ set datePRTime(aVal) {
+ this._date = new Date(aVal / 1000);
+ },
+
+ get str() {
+ return this._str;
+ },
+ set str(aVal) {
+ this._str = aVal;
+ },
+ get notability() {
+ return this._notability;
+ },
+ set notability(aVal) {
+ this._notability = aVal;
+ },
+ get text1() {
+ return this._text1;
+ },
+ set text1(aVal) {
+ this._text1 = aVal;
+ },
+ get text2() {
+ return this._text2;
+ },
+ set text2(aVal) {
+ this._text2 = aVal;
+ },
+
+ toString() {
+ return "" + this.id;
+ },
+};
diff --git a/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_offline.js b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_offline.js
new file mode 100644
index 0000000000..93b4a9ec34
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_offline.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+/**
+ * Test query support for IMAP messages that were offline before they were
+ * indexed.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_query_messages.js */
+load("base_query_messages.js");
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: true },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_query_messages_tests.forEach(test => {
+ add_task(test);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online.js b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online.js
new file mode 100644
index 0000000000..368252a5e6
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+/**
+ * Test query support for IMAP messages that aren't offline.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_query_messages.js */
+load("base_query_messages.js");
+
+expectFulltextResults = false;
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: false },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_query_messages_tests.forEach(test => {
+ add_task(test);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online_to_offline.js b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online_to_offline.js
new file mode 100644
index 0000000000..0788c15ff7
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_query_messages_imap_online_to_offline.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/**
+ * Test query support for IMAP messages that were indexed, then made available
+ * offline.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_query_messages.js */
+load("base_query_messages.js");
+
+// We want to go offline once the messages have already been indexed online.
+goOffline = true;
+
+add_setup(function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection(
+ { mode: "imap", offline: false },
+ msgGen
+ );
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_query_messages_tests.forEach(test => {
+ add_task(test);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_query_messages_local.js b/comm/mailnews/db/gloda/test/unit/test_query_messages_local.js
new file mode 100644
index 0000000000..c88fe1aa4e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_query_messages_local.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+/**
+ * Test query support for local messages.
+ */
+
+var { glodaTestHelperInitialize } = ChromeUtils.import(
+ "resource://testing-common/gloda/GlodaTestHelper.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+/* import-globals-from base_query_messages.js */
+load("base_query_messages.js");
+
+add_setup(async function () {
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+base_query_messages_tests.forEach(test => {
+ add_task(test);
+});
diff --git a/comm/mailnews/db/gloda/test/unit/test_smime_mimemsg_representation.js b/comm/mailnews/db/gloda/test/unit/test_smime_mimemsg_representation.js
new file mode 100644
index 0000000000..efe489974e
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_smime_mimemsg_representation.js
@@ -0,0 +1,894 @@
+/* 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/. */
+
+/*
+ * Test that S/MIME messages are properly displayed and that the MimeMessage
+ * representation is correct.
+ */
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+var msgGen;
+var messageInjection;
+
+function initNSS() {
+ // Copy the NSS database files over.
+ let profile = FileUtils.getDir("ProfD", []);
+ let files = ["cert9.db", "key4.db"];
+ let directory = do_get_file("../../../../data/db-tinderbox-invalid");
+ for (let f of files) {
+ let keydb = directory.clone();
+ keydb.append(f);
+ keydb.copyTo(profile, f);
+ }
+
+ // Ensure NSS is initialized.
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+}
+
+add_setup(async function () {
+ initNSS();
+ msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+});
+
+add_task(async function test_smime_mimemsg() {
+ let msg = msgGen.makeEncryptedSMimeMessage({
+ from: ["Tinderbox", "tinderbox@foo.invalid"],
+ to: [["Tinderbox", "tinderbox@foo.invalid"]],
+ subject: "Albertine disparue (La Fugitive)",
+ body: { body: encrypted_blurb },
+ });
+ let synSet = new SyntheticMessageSet([msg]);
+ await messageInjection.addSetsToFolders(
+ [messageInjection.getInboxFolder()],
+ [synSet]
+ );
+
+ let msgHdr = synSet.getMsgHdr(0);
+
+ let promiseResolve;
+ let promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+ // Make sure by default, MimeMessages do not include encrypted parts
+ MsgHdrToMimeMessage(
+ msgHdr,
+ null,
+ function (aMsgHdr, aMimeMsg) {
+ // First make sure the MIME structure is as we expect it to be.
+ Assert.equal(aMimeMsg.parts.length, 1);
+ // Then, make sure the MimeUnknown part there has the encrypted flag
+ Assert.ok(aMimeMsg.parts[0].isEncrypted);
+ // And that we can't "see through" the MimeUnknown container
+ Assert.equal(aMimeMsg.parts[0].parts.length, 0);
+ // Make sure we can't see the attachment
+ Assert.equal(aMimeMsg.allUserAttachments.length, 0);
+ promiseResolve();
+ },
+ true,
+ {}
+ );
+
+ await promise;
+
+ // Reset promise.
+ promise = new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+
+ // Now what about we specifically ask to "see" the encrypted parts?
+ MsgHdrToMimeMessage(
+ msgHdr,
+ null,
+ function (aMsgHdr, aMimeMsg) {
+ // First make sure the MIME structure is as we expect it to be.
+ Assert.equal(aMimeMsg.parts.length, 1);
+ // Then, make sure the MimeUnknown part there has the encrypted flag
+ Assert.ok(aMimeMsg.parts[0].isEncrypted);
+ // And that we can "see through" the MimeUnknown container
+ Assert.equal(aMimeMsg.parts[0].parts.length, 1);
+ Assert.equal(aMimeMsg.parts[0].parts[0].parts.length, 1);
+ Assert.equal(aMimeMsg.parts[0].parts[0].parts[0].parts.length, 2);
+ // Make sure we can see the attachment
+ Assert.equal(aMimeMsg.allUserAttachments.length, 1);
+ Assert.equal(aMimeMsg.allUserAttachments[0].contentType, "image/jpeg");
+ promiseResolve();
+ // Extra little bit of testing
+ },
+ true,
+ {
+ examineEncryptedParts: true,
+ }
+ );
+ await promise;
+});
+
+var encrypted_blurb =
+ "MIAGCSqGSIb3DQEHA6CAMIACAQAxgf8wgfwCAQAwZTBgMQswCQYDVQQGEwJTVzETMBEGA1UE\n" +
+ "CBMKVGVzdCBTdGF0ZTERMA8GA1UEBxMIVGVzdCBMb2MxETAPBgNVBAoTCFRlc3QgT3JnMRYw\n" +
+ "FAYDVQQDEw1TTUlNRSBUZXN0IENBAgEFMA0GCSqGSIb3DQEBAQUABIGAJ6gUwBMmtiIIF4ii\n" +
+ "SzkMP5vh6kCztLuF7yy/To27ZUlNOjBZZRuiwcQHiZx0aZXVhtAZcLgQKRcDwwGGd0xGvBIW\n" +
+ "dHO/gJlVX0frePMALZx/NIUtbN1cjtwDAezcTmTshiosYmlzzpPnTkgPDNDezxbN4bdBfWRu\n" +
+ "vA7aVTWGn/YwgAYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAgV77BzGUrfiqCABIIgAGLhaWnP\n" +
+ "VOgC/TGjXhAk+kjv2g4Oi8qJIJ9CWXGnBjqMAAkTgUBspqc6rxY23gIrnYbLxX3Ik+YM9je0\n" +
+ "XP/ECiY44C8lGTKIOYAE5S58w9HCrtHn3tWid8h9Yc4TJrlJ8DRv0AnpOIsob1oqkDGuIjSt\n" +
+ "sKkr2tR8t632ARoEqyWdoHIVdKVkCE7gIICHn03e/0e5Aye4dLWttTNcCwqClXR9W6QsNPuA\n" +
+ "ZWvxBCBzN8SmqkdJilFFbFusup2ON69oFTFpX8CzaUYoXI6LgxuX435fWsXJUfDI077NWQrB\n" +
+ "LbnqM6UAoYkLPYRL+hTtYE4Z8o8sU/3n5yaq6WtCRUWz+ukQWKfDq2MDWqTVI12CCy505npv\n" +
+ "2bvNUxZHInfmSzbdmTty2aaSWnuGzWI8jnA/LdPS+0ly8fkZV9tU5n46uAYOFzcVGfA94iIr\n" +
+ "8+ftcVSSLCu5qpjOdYi1iVg/sR2sjhq3gcS+CxOGjdR1s+UWmWdBnulQ0yks7/PTjlztGVvV\n" +
+ "PYkmJQ/1io3whu0UPGdUOINTFKyfca8OHnPtkAqsTBqqxnEaXVsaD4QI859u7ZiKfUL08vC2\n" +
+ "cmwHTN7iVGyMe9IfaKxXPDi3WWbOi5Aafc5KDeX3sgzC01LoIaWqTrm756GEj7dJ9vsKzlxO\n" +
+ "Xfz95oVq1/pDwUcPtTtDLWPtQHRmBl711qzVvUozT9p3GCmvzDHETlMQa45/m5jp4jEHlA1j\n" +
+ "GFX/Y0G8Y5Zziv9JD2sYc+78H5f7IMrHibKRlnsIuCvcxazUB0CfiUO5Q4Xe82bSS09C1IvJ\n" +
+ "/I79HN0KNGN4es+x/0eyIlYD3dcm3uqDpsv0ghMEPBKogqDLMzZUwW3bQxn8bMqB/zL+6hLm\n" +
+ "1197EESFEYrs6yzVnuap+vnfaqk+vprwe2Kasl1vIl1h3K+PZvsjdQHqX1WfZRWQ41eKHX/M\n" +
+ "cR5Kn8fhi/4ddt8IK2i+OeCbkRsRnBIhGpcP2pkVaH0EtZ45nbxbs1qlFbWC4nWAJ3UlmnSe\n" +
+ "eO5QOErFgwJX9W1hUWiAgyDqMWcdWLYPQJ4Gw9yqwrEP6baILArF1oZyc9XgSBzZn/7kTw6h\n" +
+ "TeCSKu0QCK1jQXUKbftl76ftFh6L/mEPWG8CZP02GnDQx5eEoUhEIS4tf3Ltc/8ey6k62R8C\n" +
+ "gMLsUdOusI61w18bNW0ffVc+N+C8j8uWbc8w4dL4DHnfz/oFUjuk0AlpZE8ii7GNqszBgirq\n" +
+ "wQ3WdXwpD4Q/j/hru040ONElMJr7HO6ipL1oP7nbIR7JHoJmht4G39pXJ86XfJmtzMuu0MxC\n" +
+ "UTcLt1Sz87HzrMO9eWdApGo6qvwSwapAQC48nXY/WDRHgxjji6EQLwO0wF4Rlwlo4SsW3nwm\n" +
+ "NtOBsjKsEQ6/WILvRAziAPlp7+v13QfLrrzmnWFwKE6h9KQ/wpLL9/TAoy76FHoRvZgT3x20\n" +
+ "Vo9Fe7nZbc6qEc9/DbwShxWMbsU8vlzrxm4pgOC7I4jftUgolQ+NE78sQHH4XefHDKXWxRvx\n" +
+ "H8HVU/TPsj+2cEHM2WlVOXlYdtlobx20DSiOvhWdkW45Zw+9SaVkGw/IhCVkLi0UKuQV1gou\n" +
+ "lA4FeTVs0WY7jUdZB6c3DYgu4o5gxVvpRKOmwNp7rVIjsGuAjC91FN3DGQYlsyItLlZd8Yli\n" +
+ "FqGL6B2HTehmOGwtc6pfzbUJj9X9biZlQBigS3waDC0ei7HUq5M0ztyZv71dg+ZA39F0ZlVD\n" +
+ "CszjUNp847Lvt91JVmQdH0HTPu7Qfb/l3qX6LARTCgFfLGzjdcthzxyWEU/oCurUj9E1MwxX\n" +
+ "pfr8AX9/ajgCCS9bBvV0luYe/+0xqrzbnZw3m3ljfpxx5k78HFVuYhXt4iEsgtbXhJuLr/EJ\n" +
+ "B+Cu2YaQhXrvtyfi4EkOLoOcIzu5hs8V4hPebDbhDQKDcF3EhzYZ0k2YlfXnUx2Uk1Xw/x7n\n" +
+ "bLKVIpw0xSnVWdj3XeHLwEwh+T6/uthhi99iiXNQikxwbrEU4Y5IVAjh/JfKywIgPcXnaDqR\n" +
+ "1anwP8a+QQcD3U9neOvIZVx4fA/Ide5svJEkJ6gccel7kMAGD3/R14VfasqjBc0XhoEZT4PN\n" +
+ "xuW8fZIKPkxU4KEgM2VlzB9ZgTTcfUbUMmaCWioQEwfF7J2PhmIl6pBUiBFUdPv9+TnE4riG\n" +
+ "Cm5myUQEap9SFIjWRbLidy4+ZOK1rA34zNT4CnknLWFruygn8EzpgQVlru5no+qppchbOjyH\n" +
+ "O+Yz9VGs+SjoQlMl1HjtM2GQeNizP7AsLLd/R+jQ0Al4+KmM0Z8obTtYKUjG5rlwtNzcxyjv\n" +
+ "tvEhXeWjD4xGkWN8Xhf7VQX2dM7APMIiyyMNaNDVZvWxU9DpJjt4F+mhQFk4Yk5ao+Bs23MV\n" +
+ "XI4b0GanjnGzu5bHMUngkHLISMNGcDicT5JzfVYMbiM2pDakaaZWQ/ztQW5gWzjYFpj/Yffg\n" +
+ "ThvYXUi71gTZqHZiybqu6UI4iBOXc3mXbKzN3XwBSfCODFHJj5A9Lzh4pVBrndC7APfgn+Cm\n" +
+ "6ga7DmPZI1igomTOiIcF5+i7AOW/8hnv9hlsxN3D7mrIiJuRAkCD56kGBkCEMnZ1EA5nk49+\n" +
+ "k1s+XKPKPskxz8XrD2vhPL9ToSXQl/i+b+bh7jBIi+2KJh5XoiM9CCqP3B7bjxwx9qvtq7nD\n" +
+ "/+Zn4B2qCxxGI5d92mV4d1KGanbzHSZh1PJyQHrRcMMdoHMEVl1AW+YPffkwQrnRef1AZm9D\n" +
+ "ZB8B5LJvvjyNXsVGicPYM+RZwthk9Eko0W17u8fC3I/TST8c+kNqJihNhJW3y70plmSe/na4\n" +
+ "G4XeSHdbHsOWHq8CkRW83jk+2G0BE+1Y7YQt9jLOgVlIm6qYr1ov629575zV3ebyxXtkQY0g\n" +
+ "mjoal1nGJCrCp7GAl/c5KMK66T03RXEY+sBZZ2sbv6FiB6+xHreUI7k+JCUJ/uoW6c/8ithM\n" +
+ "L0gMRpxZrhksRcaBDXa8Mp4lyrqf3QWiowznSIyKPm7i0FjGGul/SESz7cKe/8RjJbKnx4TP\n" +
+ "dZ5G/+dhOZwXoisiGSj4CdXq6KKY62C1Pfvnf9elYJMo7GT8+6REYXrCQEoTIAw9zkQGD/FJ\n" +
+ "L6PvXunheXSHY454jau9JqqQdYaroYVrIHD9AINJPKluaToyT62oOL2CcG3dB0Yw1SZfUASa\n" +
+ "P36CevQjjs9GhLeFrqXXYx9ItqbYZKMiHDarjf3KgOzRhFS97n4OaZgn7Yc/tOvtXTMlYSAy\n" +
+ "M4pw2vISXcuaSl6mQzbllYuWk2sqt+rpt+/l0Hd/TfLVzp4mMq84cKerXSL271oc/2Sary/l\n" +
+ "wRHj50Wz0gIxjyfg1FgegnDmaeDCuMwSTFjrlUaV7FSKPZqaVr4LBQbyL5fsd2VrO4mQfmdO\n" +
+ "rwd7+CojtVraeyrNcwC6inBoPOa07A1aYB+bGKhwn/7n6YJEdX8AtTtir1u4r9rIPeUyv+nA\n" +
+ "QpPkPie5R481ZEgApFhyvFy6+etmHBPEpr5PguDzX1Una8sOBfBxDMVCLdn6lHA/ebDCDrLn\n" +
+ "JobzOLmW8G8cXwTmgxr1r5KbvoUaWfSZtJYL6M3b4Ix73GfAhbH30eAbgRya+IHrTx2Nhy0q\n" +
+ "pU1mgbM1aV4OhZ3wZXga8tpWnohVcTIXUfQhBYwJXCxVj6lR6mVd+4WKZT5Tz1twrYxI1ZGD\n" +
+ "HRIatLWeshiULj2KNVtTkc0w4HqIw6gVEwYSojzduuhrtXZMsBVImyV9151ZFL/oDgMQEOEm\n" +
+ "qIill8STDIz2bFF+FzkLLW+l5TeJ9rS4mrO1ffKdVWWL/PFlBvP39PHTkSv7+MYKhobbzccA\n" +
+ "ydjzdauQVn28lXVIMpF9UWmMeyWZlogGNECxb7NAPwvzONGvak//dBKEsqnGquNfNHfFJoMZ\n" +
+ "S5Ts8Br8rc0LW0zyLpLls3p+AnyJQQArteqraSodGk6X18BIbJc2avhbzGJnegacFhTr+e6a\n" +
+ "7niVgn1/P9PNo/SfMYZLWTIUKLkHq9GDhuniHqGM7tcdujI+Orit/uLVYaHDEMVKUDvJuJGj\n" +
+ "z+EybiUvIvpWjY7nWRjmtwTzR8JFUnltTGoLbcnA0Fmtu3rQCOuECYbUvH2bbtJBjatmA38+\n" +
+ "AotExnchuqDI13HVm9OY2CjyD4cJonvmjpz60xwFnr3HGp8pZNNFmvY2udGKUYhNF1X8mb9c\n" +
+ "vgs8SiT3Lf1HNXfayy+F+kLkXqBNZLnGfRHWKOAWSEj8dXiJ0ScLmAvoJTbC18s3yYoK3o2X\n" +
+ "z1sY+RERhyJ3UmFHuQ5q75w2mKz4l0kzHA6bfwHvLbTps7sNkkhT403KU8RbxNmsQDgFMCfw\n" +
+ "BaJnTNyQFJTVgljTEnFsaUAhEOgyoCAFvwe7eKTGO2NqqX9hrWcEoXSa6FgnLQvT49SZHrYC\n" +
+ "poVRVZdJ6sqnjSy7OxT+WbuQufc44TEYeGuHjH444yS7ZCMVyjNaQDRvWPYuXmFp8Anw5lO+\n" +
+ "xLb+LMEgeFKcVMjtnYLZTTgY6UtqMr18BzwHKft6+ATzyUc1zsHv9Ap7mmdRakLFa+8QbXvc\n" +
+ "+AfVbOsmcY8Bmin0nKIL9nfOUPahEMQBN1NN3dOWM/5qa3REk1Cx3rIaB/jsU9f9zUztg9MV\n" +
+ "kvplfOVYoxUsBoAhCjjzPmCgVbp6Gnr/Ebd2vFvDsokp0yHw7Cgb0mBznsntRnkb2cEB0tvw\n" +
+ "fBhK7YeETx8W3A+PKSkn1AwvoG6WeiM6CFh2yp/VYjabwfrTrfVbXpk4epzCLU8WTyPvuxv3\n" +
+ "DDH4s/Zl0CMIqL2oydtEN11ARdwXi3AImYsix/cWLzV5r1UN6NN0B0y9zmT5BrCElrJKJxYb\n" +
+ "NmafkxyrCFGnjWFIRzw4s/GGm/dBx0DGBizioedTttqjnF0rfF2pM/MVf/udCdd6uQyYlGZz\n" +
+ "AxW6ZKX0TPj7bvPyRgzqXBXTfd23kYVH/lvHEsKxnMb2F9A9LYun63jPFSiHXCahU4WcuzZK\n" +
+ "aH6h+cnY3xJn8+P2e4m4pTDMHdsgBQs4upMTxrxhH01MnUgbKz6IA2KV9y8H24PzzqJawh02\n" +
+ "xhdMHVuV396LvvjICg4OWzvFdEFdWDEZ4ph4nYTHN62TsQUwa8t3MBbKeW4mlIQXqGNAhfN6\n" +
+ "UR8nqf4H56oAMTvsvNS8EoCgcu/L9C5TrDnldYf3Zhyx51A0ufvpSNR6onWOKzVF/qwtyn/C\n" +
+ "y5l9X4c/0uCbff2nkYUqVAkfgD/hdEXiO0kdku6ptnWbNUPU76pQDQ5vD6sfe/8ZsRF68Eay\n" +
+ "XhvbZYmXCVn7azZeEps3EiOKCL4cazE508fLyjC/fNc1WMdyIve1lhXGI8uJ7/lB6tJ6CucL\n" +
+ "WT4OX6kHZh4I7mXy2+lezAELmrP3eU7YduHemlXqqlOrnw8pwGEVCsxGmCv6DdJNehk3wCJv\n" +
+ "GcdygTynL5d5fGe1mP2zxZjW9kscNX1nwf1+sz6chZ3jXpiBTRXICh66vk3UbyS3eZk8NKYL\n" +
+ "dY+/cN1O4jtipgHGq8EPUefBVRH+DmjTqFA05qHAaV/fZ53xLWm8YVTI/DS9fbbPZprOBeib\n" +
+ "GoMdA+a0Sqh6RdIWlaFXYYJUspp+rI1FlOBZvgy8Z5K5oGajE6RM06EeB7DPtI1/K+jRXa5O\n" +
+ "YXacRu/lgDlZvevVsSj27Oy6A+rbfo5oafhMMCLArtGlY4ENMk+u/ztvoxPlos9vCUV6NSFj\n" +
+ "znenH7iv5TUvv5gm4n1NCSZ9Db+zW5DQS8Gm5iGUsRj6VX5hZ1pMl2df43B6I5BwCKnq2eYn\n" +
+ "mpDzvUXUku9C/RkTxf/xfaIG30+whnY9Id4MWzWNNIJicvEdJkDgE5iRfwsVntbQYGwctmxs\n" +
+ "209aIk/KjeGWPOyg6TFYF5ZJMe/0XVSr2Bci3cj7GWeFc2FrFB/5nfExErrT4+e+9GMCyXcz\n" +
+ "bIbj45WCoA3Lgo2vh7bZV7xy6iXv358kl7bahH2/IvjUPGn3EKQY8ApoTNrRXvKAt7P4Q7zM\n" +
+ "HrRSQ+iDYZ3BCmoWfXMzRmRJbAzvC1akeduykIwQkL8QP7z7n33ntPlP2n1rDLI+LoDSOC3o\n" +
+ "bJzafHOOAH2J/MWOI61Tj7+FWyGIPihUf4rZqFXnoZkBpy/fRb/+qmSmIZ3YPiDdwICnCerU\n" +
+ "0BLeaWRD4aie51FyZ5fR+tXmTu7JDC+GRKp4EARokJgL4CTnuSGY9TaYKsoKrwST/9kKQrlM\n" +
+ "ISOGV8yTnLTzhs01EijkNEJZkJwg7QYxsJ8x9zLDL44fCL+KALLpkHEmUQdkLwy5DQV97qL+\n" +
+ "/6bSyxgLBiEHRJQns3HHGlUvNt2naUPCukRO7ieIlrPPSaL199yPcgjmFIBiXptTm9fZJRzE\n" +
+ "rkwkIeGlXzxhSpLHApOnDZaeNlVE+5NyNHOxbbBxfrc5Xmg68ZESXwxKeZAF4GM11OBLzj/f\n" +
+ "r6iGBayidg/uYZ5D0CCSyTDT1Y5RKFFe1DieQey1bj9oIuE+jo9coYLc7XUK8cnlOqLRl9Kt\n" +
+ "kcB4t5JAqob/ZttXhHnZ8J3QUpprXYYQ9c4NrYf4KEy1+femS8uGnuBZgUM1Tun5EjSeKxMB\n" +
+ "cY8gGkXcsuLzRpAtwifgHM2R6dgOq7g2nwB4wQYiILSqAsSH0QKNb+tS3NKyfNsg1tJK1PSI\n" +
+ "vOjRQCkzaII1IureIWrUikWCbQWqTDW/PazEr3HG9+BMs1JMUbEviA6ljNZz478Xbc+mA9yI\n" +
+ "RsqILUos/MCjKEhYn/qq+BsKtKmSC0nsZ3KXQcLbq7O/RZU85Dr+N+wyhieT8vu+4hb0mqrn\n" +
+ "FZwyMQt2WpnqaNk5tw92/Gw/Ad5q6ACt3PZiG4GrG3NNaKxadwkN9POzyN4zn+7gq3cyF/uN\n" +
+ "imAv6aVHaiD002PMWHIMKUOFwmS9AV3iskmW+swH9UyLPnWDejvUs8jW6mmeD3TOR8sRQv8q\n" +
+ "KwcvrscKtEXmBvFDYh3UcIcu/j5wb7WLwhNi3XOpGHEgg2MjDf5ti0kkrR68VEc+XBvnAYV7\n" +
+ "5EIrxI1qfkNcgXKRdOg6msLv6a9QSgJunwjACXM7Zv96MHMEETgkNr7DO+woHjWcPl4AYV4k\n" +
+ "HgPGUISEGUQr6/c1penqLiExW+iVj8Y5uLj3c/PNQLMhnttckHWVCz6wlqxmvoUQHgEl3Qd5\n" +
+ "pODBWHyC2FZku+Xuyu2o+GHxj10hYfsEl/qoDqqvW4TGlTz16MQrSV3SMs/i6SHmq5eiuhMf\n" +
+ "Hj6nkt3hljgHA1YawbFL58hj4x2DAyeYFfLY1YEBMH3K6JLxUdD0c02lecUDOqUxBrp+/qp2\n" +
+ "4KIqFLZ3+z7Wzx8WI0DzKYyZK79+VV7+Imv+DpOTaLFLu7nymvPeOgbzTsrJbJQo560EXpLl\n" +
+ "wID5Z36x9P/A54q0i/mhTzK/RtYYhqgaV4+GmP7XxA58zulNAJIVcsmgXKiD1GpmOR8c8EDm\n" +
+ "kMGEcrACXBOkpEJHp07J5vD8gfWublIG3MzeoTjeBhUJM7G9H5r6tNHdB4Ak+TMVfjcN0vbZ\n" +
+ "UtVCiQJqR8USTwNCumY3EtcMiXGVM3CRTTLai+IZVmLqED7SL3kpOdFcthMk5K0L0j1Ootan\n" +
+ "wFE2QhcmMVP6x8kH9cJVhbhLHWYbO/vg1AcLE7YOPRD3DVId+3dTZo0JVDC6RQKpOuUBolbH\n" +
+ "P8GpxBg4IcKqyMAA/1+FzaLicvXPzk7rKFkXjL5cgervdWF8Xx6gaihVXRfR7AiWOy38I0GH\n" +
+ "RJI8WC8NruvGHN71Oi0VKiyGD8o4tlGZyQoeRU02Z7cM1X493wCEVUuBEXYI5ax7wIcl25AD\n" +
+ "+WAv2iBZ3gHNNyCSJZM/Tqk2/2B35pfotVMgs67fnUy9tpm3n9nOdm/FgReSu3CBM3JZmYtf\n" +
+ "tOfqq3Xpu/3WnhWjkqDVmgaQ42PWtxYU32ah3M+EHHhkYSIG/csaSkVlyGYul3BsfeZ4jCvK\n" +
+ "MvVFFD2Kzkyt8zKKQlA7Zzyf900aFNhU5SkX70s94Bk3WXHXD5DRQRYHWmruCFVkFJXyaiZj\n" +
+ "qWBVKP3Gv6OXSc9IRimu6p0l0TaDxxjNoPskg6dXHTV5uTcgOKfRohgudjQC20VmamOp8IGd\n" +
+ "1muj9L82CT7elonqA0E6HFZfJqJIfxq/wSFVG7wiB9Gwjoj1xgB7bSzbglpOV/ReBPcv1ivl\n" +
+ "KsJmK9nlmfS4Y9MPWuctSROg9QVEOWq/XowOm6+Y4bpKpDhmmpsUpMsDtOJnrvSWJwcwWRRB\n" +
+ "+2Z3H6kIEUXDq1cjLsrBIWRTwb//h0Sbb2Kb1cUHnQQAjlhkSlOpaEMTzQb7GMojunx8Yeb9\n" +
+ "ff/1l4/1tqVSxX61AJuJyywGyk9AIsDIm1WW6P+P5AVRsy5xu61qrL60GHlMxtfm7ZSLAeR7\n" +
+ "GvBOgDitOE+llhzZSjwdaESxSAvnhFfM5TOCSj5YNBfLaI8bVxn4Br342GV7nufFqOLkp4rr\n" +
+ "3pcNbQvsb+k7kkdyNMNtOQfG/Ojf8YTGoanvDYrtB/0Euu0TXR86ljXPIJOT/4nhue4149SO\n" +
+ "9lboxBH6iaP8AGxn+2/pzCbcOXjDzcD/i1DoQXVcwfniiMf6S+CHb38Os3KTO49YsMYjrDPP\n" +
+ "9L2IurXfUHONlljI1T6GFV1RfRCBfO5XklduPaR4+4B0JLhU6+UKl9vdTphhwrYTuJ8I3wkD\n" +
+ "6DO4hvktTjl/IPLyYPU1w48W3cZ++P/wJNtIYl5I/ZSNfAzefc8SQh7kcnVnDoocElfWHfg6\n" +
+ "oZL0MSe088uFDAxaJTLxDaDIbzjBkwaiRYSBQ+SQVBmUlP1EjLbrwdayi2IidFj2Mr6nv4KZ\n" +
+ "4HUlmmVMSvg4K2Iv5NGgAmAjYngYSveCdDkYXQgOXldxnzVTzRRP+nEAtFepLx6TZjSjawqL\n" +
+ "nZ+N0/BCJ5UkldplLALg+5kdHCLwcdkz+H4YsB2sLE8zULM9JJW88DGBKXKue4J8GkhJlY9i\n" +
+ "1y1pdTW6mvC0J0oMAe2ULkrakIdyGgNghwjnDMaf85niB1A4+qjN0K3uGGjRyWddJH/Pnv+Z\n" +
+ "7A9dmkRNnYMFEkyFYTkbfmE2fHr4MY+YwlwjE7f69LmKEcai/is9L/Lqv5Onb8W6N06l54s1\n" +
+ "iYKzFFqo/gc0UJsiBhPmSKMNvoeoUpi0yUgXDPtw5+9HD/hqFSXqWGh2uR1vOUi85k0f1eOe\n" +
+ "zzkIBzcL3on0y03D74cB1QtjBAS2lwTXzjyEbitB4AxHyp5L13tPJs4l2uo8JXpL8u0HmJVR\n" +
+ "w6AOL/rV6elTYkuxnq5aOq8WQcm+1cYY4fPdT7ZRwVy0ZfHpN6VsqmMNIoAUyRgy86sYU2E+\n" +
+ "UMTeKZzD1+T2LbbV38AQh2kaLlSNuNkoFIjFZZvth/vubqIjHlmsw2MeZqXZIs3dBeA/1GL8\n" +
+ "s0k5ix2Obdy1t+w1e0d+y/ei1IzsxHRdBvrn1YDqdFw4xdUreJ3FSTrsTePlSWVjJXKGm13h\n" +
+ "hFjuCqELnR05+au1dFSbiAlbMPM6W/cebi+/0GmvIvfRaqrbvRJoUWxfgaFcanrlin7a11Pb\n" +
+ "6pFV47mIKHxWQiYq0z3kq+QQ1YqXvxMdM7eIg0PEOygB4Wp2FwIG0ZcEFfdq5CPveormJ/EZ\n" +
+ "NOFrIHZXkFl8fT4x8LFLWNmlQwoVqeQGOs51CYQF7YPXjFx64mV0RXz/umA5/Un6fHjKS5Yq\n" +
+ "7ZIhx+JPX4+s3RrxbUjbq4hCCa2MSBBQONhdmXtKKIf+TNvnimm9je5bt3Nu79A2OYbAzvb3\n" +
+ "cOEcQqieXzqj358oIxwd7BL2xLEMbe2Z+1bDXK+YwyJpXNF0Ech6Vbh4PSLHpW1jCoIn5HCP\n" +
+ "4K28TdrXOwwKkac1WjaQCw0RztZEatpJW1PyhQ0n8xcegTqT+6nyifeTbEKuUYXhCaJa0spg\n" +
+ "Xx1yv6G+ieBg5owSZ3DQSQ4GmaZ4GBgFePkqroihA4C1bs2FbrRWRFVRWAAEZYdcgHOyBWNG\n" +
+ "KLGntWv2VWf7yid8+oSQLExsYHBGdYMTJCbU53fuAnJYE4DJJ15Vztj0TO74KqKrkTtxfog7\n" +
+ "5CdFia/OvcCruLblCFLcrRyhsW3YKUxHmgpAPoSN4/46Bz+ob+CCkd6RJzwjnhfIgbXqKRLE\n" +
+ "8KfsCqksHp1p3hEgvm3iDuqHfBP/7O/T5V753HBhuAzFZlaOzQsjBfzK+BMXP3zp+DEpGwUL\n" +
+ "Pd/DG0fa6odMTqPs/TUblpHeANF88+XRkgB/hucv+K7h13bfRRYPMM4zephlWBzBDIaoazv9\n" +
+ "SvRyy21B4vRTXrwbTkSZXTtEFCb2027l+ycCayD9XXCLQUhjSrsI8SB+9qC/i827HcLF7X20\n" +
+ "L/8Na6qnRTinmwkBUDk+o6APUlR6sDpX+uf1bOyiV6oF0wy59+kXi9oCjupzPBOatSM8ka47\n" +
+ "6tcHJ6na0wJ+Z7EjcaOqy26OYcPT2m3wvquK00JLHCaTDisK3cQ9178FxZmpD8i09AsLVWuz\n" +
+ "r/dmucYAxjKMQzV6+q94S42EThtTbw3LJURF/8QNLk8AZKwVuaw7zz5+8F/bc2qtrUr762t1\n" +
+ "KN+9Ul8Kc2N5IxAS+klFXPfA1isfvbm88737wa3Tk1N54QIvDXVLBJg4OzvjkQAPai9lPqUK\n" +
+ "Tj3LrtYGPDTaRyRXpsH0ehIZ66TRobSaBBrL4VeopHzoWOutlTLlSSjZ1Grn6SFGdH/i998Q\n" +
+ "64ucbkyejUbFT6SgOzDN3rnl9ppqnDPOCk60WAeosAJdf4tndoYGGQQnQpsBh8uLCkyyu4z2\n" +
+ "di/om5c1yNSJsv6j2jQQiPsMX+ef+27mdAj9pUXQSRnl3oZRvQMQ7VmKsa8NBByU05MwSvOn\n" +
+ "vuEKgPq5CL+2Spnjcll+wWQsDF6OZMb2cM7PmLTGTI9LKnPnDPEhz4borQfch3jHR/EVtsmg\n" +
+ "BX6xmoD7gQdXPWBFTvwT7ljRJ4v5O0v/4p56rTneZZwBBIIgAOfncYVNGur0g1ZaFAujgzEG\n" +
+ "/PLpgIqn2rjHU+zmUuf28MvHdWxVNgSar7qMRp67M6UM6RExfuv1vzWw+ogYWeiQOYMYcBqP\n" +
+ "4p1Dm0ZxwWaqgllea7MCmniOrEGNizUMlvYIJoYcKJFVHz4Jbxy9pzGVL58Kbmwa1ZDwSXqC\n" +
+ "YHcVLer9yxoZpuDnIhRXHUnDx6Iw6QDiKpMQqJcFKf0YJTUrhN2M17kUaOD7TY0zHhDznFHY\n" +
+ "Oe0hlEu1y/FEwNxueg8tpjGVivXTX5E/81RMpUHKenlM8WbA7GQepFiIrcZTsnZ9jBCXLPGu\n" +
+ "CI00YwbNnzV/EsYsHAcvwIQBlBDVjSjkxoBaBmDsVpLawCh/SGEAl99Fe1/08OKHceGPDxko\n" +
+ "wZ3Sge2vC2ydyu4+LVnypr29R3sv53cnApKlt0uplnF4rbpBSbTCgH6IR0Aq/aYQUW032HtX\n" +
+ "wuPhxgIp7Yf5mi5rd3MwyLhsTQ7dFhXZT1kecAXAMo2x2BAo98yJfmvXM90hIwlXHwp11ped\n" +
+ "MTzc47I5XC+dR3YTHbxUKC7RCo2OjiLsT6UocM0vqyxkkJrUWHuC9vGHNEA3wmJuj/Tncr+r\n" +
+ "/bLYzx9TWcN0st02kCC4wUjQJuNlCZLjmnCrr6Y8Yrm592pv3ztcVD+cbgjwptpxN4OXTreI\n" +
+ "7Py1P0BRRC7N3I+W8OVsszHpjGsEqxFDdyRL7VtUWMR85c1cJKvmYWeSSVX0YlNsbMtVledB\n" +
+ "ViJg/2Qa6vU5lB3WXIyXOuJVEo9B6ua60Fg+HlKDHEl/5bOqOzW5pgTz2BclmAb+NvhEdl6a\n" +
+ "SzNSHFrCqmmG6Nb9DCT9wcvvs74PN2QFHm1vxPymLoEQYZ1o0oI9puayLFpMykIK4N8Kinp5\n" +
+ "iUWxh3t+V3L/yz6jHXiL2pR3UYBrfzRb+bOumTD5ENLil/3P8BngPSCvYAfRMOrBj6EAIoZi\n" +
+ "HTaCqKN2K7LefPum/AQXfE5oHHJXWkS5Zx+DiKVmwJcQuzqO5j+sJxuUlZXQnSR28g++33WK\n" +
+ "zZsrMU1MolLmEFArfC2Z1o2dxtk2FIQVq/mNhq79sfU+xmCEaGyUV84NCFpXMTe2z0m8gQA9\n" +
+ "/v+Arqi7hCbtq2AyUFNwlUlBjdAxtoPNUj5E9iPfpQVZLUTGM8H4C5kJkOXYtb+XKeoKRLx5\n" +
+ "VCESit4KBnFfx4Egptm58q2CDUOb441YhMQKUR2TCCgLPJFZBKexz0jJpWHoCBBNj5lbAeQk\n" +
+ "3Hrpj2ErGttnVxL/pFEOY0u22FWHeXdELaBs0bvbQ/8WHGUg9THFzhZtvo+utuFGpmU+gK+a\n" +
+ "XCvYMtSxSBFoSSwA4v/YTc12QBO/Dm5xINzupyx9cfkbUgrRRbP/ORXB+KIkL3uQEa6UwRzo\n" +
+ "NdZlGOySsXHLmMkICx1TxWHiTjVbrk0tAvSjIiCgdW3kFVAqGNovgl259anhCkXxbnLUMMsc\n" +
+ "sAVW0cdy3DPLjbab5tCSjbpLE8g7KxGTX6jgwjZVEDEkvhk2JwqaxQdhp6JsZIOMSxOmhhSa\n" +
+ "+zZ2V3amEkQs6Ks+3MOPRF233G33dfkmkaq8oPNOXzROimZod7RaYTJYlfl3kBHx2Gd33ID3\n" +
+ "OR2Z5ZywURCEUZ1tmidgJaChiT42hfkTNI+Y11S0DKHoQZfDQQ4gOpoGo8qn8GntVyVx25nA\n" +
+ "VpxqsbddA6diporOmNx76M7+tuSKN8KpqHpv1K1+Bv180rqa/oZ+PXxO1nu3Iv+drzvMuSXs\n" +
+ "ityJ/DRhzg3Hdz8ZJOUuKb06AfhMDcFGOpCAz5KVN6wr9/bD53Ig0KU/gUKDd2vBsPemKSL5\n" +
+ "FKKKuHf5LYVMELEfgEwhcnen5tT+wvh+UOVit6YLHSQ3uoNW9REzBwEsBcSM2xHRlg+oPw7V\n" +
+ "K6CoW1SZdRt3P6ixVDbU5IAz9oH3owqC27FK1poBSXTEg6+AodSdKD2TOqyAaP3a5+/QoGya\n" +
+ "uQntOxj2mU9rtGP2p7wQuL48ya6waALfx+8N/P18hlILF8x7K+JPBZ+0BWhMNEF9BgPOau//\n" +
+ "THHwFMvjc0yVlRtChlhzEjhAhvcK9WpM7c0R6N5vBm7M9477PbGkNZzMFqduJxTw+hxja2oZ\n" +
+ "gjcm9JXGFbYb1ATE/8WDh5dy4H49azbAb70mf9XxzvllCUCdor8TXkjqTp8qyof7P81BUknL\n" +
+ "g8vYzpY3D8eoKFwyS/f0QQic0t0/wbRVZ/tiW9qzzKaAppKINddPfVXlGUKbSKsXy5rjcg+f\n" +
+ "rD4WKauGPgTs+kOpCOAOxAd46wEP0CoLnjALeVsP6q+yNic2Mxa2FUN2fQ7Am8IWV73cnkP0\n" +
+ "RK/tcmGOmFkg73KJSl/FC3yNxG8HLQmcY/IeW+Z0PVLTj5tzWer2cey9/JTHzzOLvqEjDpZH\n" +
+ "bbsS7lOi+oxEEHHRlOM7PECSsMc9C/AQohyDyHNYPEqo1XjRmTUSU6ozbgcLDucrpAIjvVYm\n" +
+ "8Cz1icS3xZCO97XtqSGd6LsMYWlCHvQ6RJAcuBxL8sasJHkz5QZ4TG1xArSRDdz+bO/4Df+Q\n" +
+ "R5HTXGqY6cFs9CLG6O/vpzGKCaeaIjKVIZTTl93Ql988Y3Rk/NQFpWRoIWtrMC0Lpu04Vmop\n" +
+ "qYLPJCFEdCctbhiD/SXjUR6unYXHPAPGWwpRmUF8gQChRng+R5bzpmMXGAUOP8W7lvthh5+g\n" +
+ "66o+0kvtxImNox3up83hSnsU9xv5n37j9T3pttub3ozQIJTudiHS6uNLbKwDCbCvrdvY9vMu\n" +
+ "8D2LSmNC1b7QHkU7R7Bq6R8DWdvm+T+LKqgqodpoInMsN/p70ShybyVQAOg7RNUzw7k8RJKV\n" +
+ "TdxHFAxEVpS/PiBs3JFwL8QpOMVhmgK3O6Ictn/TW249fQ5qEEA7LLMY6H/TZmYWg/EWfTzL\n" +
+ "wd4bGfdMoY+IRjMsxfX4Z1vLVAo83VbtgvbKFpLb1EO7Kc7zuCS0w1BeQ5++eAnZCy3GaTUk\n" +
+ "vFAkjZkU64NaObuZ1/4hyMMGnzNZYnNraZ0+wNOFdLquhi6F5wjsbep9kf/VZfNJscWNIhsd\n" +
+ "+okxW4QlBC9smcIQJfpYx+ycVttGXQ7acP6U5NmVKf/TCu30Ltev6/SXtLlWVzFMFO6ZgKrG\n" +
+ "4xlUqiSn2L0P8AmjvWEPAyL7f3E8iarGS8mKnAq+h/LeyQPD5M8sDhrBDsBweQJghnRavj5/\n" +
+ "kg9MalKxnbYxB79uzRi3Cqz1nNJxP/sAyUi4/c7+PU4T0xQkoU3BioXURhCXZMcOOBSwSEGy\n" +
+ "LCpJbPMRSnX6gveGth2ba7os14cRSG44LPe9BDjrJwSvVV4Pv12OeNPqwH/tvyaVi5V2UvGn\n" +
+ "J9t8EK+rYLlZJs65g7oxaTIcBpkRIzElLMGNmXsEHkGc5PQeJC48C+yho5cKq84lDq0XHlMv\n" +
+ "atYV/u5N/w7Ta+nOQGn41GTOyZmAqddNwpabhszmzx32klOHwNWdM/xoqXze0SHBEMSYaXfW\n" +
+ "cOecJNbWpmIoFs+gxt6AKnOYWC/UdaBN+NPUmyQh56LNBPXHInMGc+TJpJR2BhLryKYbMRiG\n" +
+ "3KcysiWiSOujHeMhohFMUm/DUfy1LgMT8T+bQGrCIvhAjpQn5uqtB2xBMtnD4Rc6KxTyY/HT\n" +
+ "VhVtQqITCY4wy3yv15lIGxe0LLGGnVtYJqo5EEe6hQg9eXOhH6dhCDKMQ8InV+H55fAB7dnq\n" +
+ "7gZhYwjUh3+cbQHnamh/qovVNY/4sTHOP0i+13ekbw/Q7zTq27bWPGyWrfa1vsMFqBZD4vVQ\n" +
+ "1/dkZvzpdWc0uJqqSw1p0vVaHddjAwaoBqqYLwIbhrhDPYqpkQuBnNLxSoYf296ut3Z6tcxX\n" +
+ "PSOt9Z5XGK0f3XdQQSOyP2ujB9KI8sNgPCC3BpXcqb0shalUXwltnRpAsLzRnxjOujR48rxA\n" +
+ "li/1wGpRxFPNsA0dG9/kGGN/FKdYW9J38fC8YVM1gpFDrvENuiGqKxdTnAQqwNTQ4YMZKgIU\n" +
+ "spsCCOA26YRsJwRYRn3Ajw9wpTR22OG9SwmZlhgsvFxVRiDRa0KlysJVpF4n5C3F7oQtroiD\n" +
+ "86oThYaQN3ylOr8qpf4ks/rl5QHoY7j72FAaqn/9hef1C3kAh6vF85ZliGXKY4tV3gBLMgZX\n" +
+ "L08CCTUsBQG+1qeRY3UKaigBTfsbYfxU/CLayCoEV95Y4j6yFV1GDG/OuYN6hSIjw9hl3p5t\n" +
+ "4iSmAuH4jkdQFWAile59e5ewt9KuJwxjyCFpn2gREx6LBImTDAQ9YW1AManPRtvriv0mnmG/\n" +
+ "x3Pm826Jteq8pd0Vi6pLLATWjzAz+GyrtmMjk1InY0sUXdMzMfWczWBedZKCLzd2WB1tCoUt\n" +
+ "g2ZnQO3nBV/+t3yTH15cNtkG74Kk/3itRBxz1kPvLjMMwQrlErfIF5zOQ/SFXaJoiC18jIFp\n" +
+ "1aDng/elbbjpz8Y6ZQdYlwZAJt14Pgmd9oCiT8nw7cNzJkzhPw1g3MSjHiqndHNeP3J16Rp2\n" +
+ "wGnvYwGTWA2sbPgtPSv61mstrs0ZW8+JbqknLn6lRxfnODqiwH8jR723GrJGHWRwwFOLN0SY\n" +
+ "eKO7T6OPsxdiWSnDb587DzdcPV8UjwU92sdtxJPJTE5AP3ER/GFlrRtJWoJNEc4FPQPEbxSI\n" +
+ "kf8ziZWlEcwztvZyeKv/iOqmGBULuXXjFVRYn+PLXJ7rXIMo/FC4rp8wOpVy1Kr82UoJdriE\n" +
+ "+KRpOMZAqyBoQhnzqT3KSI2fzfKlKLg4XFajzjKgvA25Lt4t0FiTX0oPjT5xXy3nLMPqJkSa\n" +
+ "1xk8jA/WhFzm1H7KPjttN3Cl7Q4II+NnbxXrZ3jxZ0pAQkbR1goH3QrBDkr888Gxp4RpyUqd\n" +
+ "sgplw5FdAIGLuPZD20JkSAtJI9MuYJtndWYm1xO6aIrpCsG05E2NVSr7ziyaEEuiL1Xc8TlT\n" +
+ "//v4JMO9As9x/Pcik1mD8f7a8qLibt4+yboD1/Vra4SgfWyWaniG326q5Upk8Bl1hksCKKTO\n" +
+ "7vSEp32TaP90SOuH054HQc4Ki0ffye0aBJMifV77RVz6GErggO6iyIsFjSVpCi+bwQZ6wrkk\n" +
+ "lV3znF1li5e8dGkfMv8G/F7rCpecpvYQPD4+8PPmIELFAoRXw/PKsFXf2z9Jj3KxCirGmnWa\n" +
+ "6pV7BuKiXH2ir11ZD4zrZ8Qi2SlAJ4VfY3BIgt2nkZ8FRkmT0wroc+Basp8PDcuKzgT2HBgX\n" +
+ "r9ZhanQBsf1OZxaU33jeGUd03f4Kgf22xawruBhcdwlfRybZSUQHGpiTbhflPn6n1L697/xv\n" +
+ "kr4StZ2YIb2UHppAWbDBxZOvBct4tBi7L3A5hr+/TQr2em7kYbyrDn1x8wgNxvk7mJ2s58Dk\n" +
+ "b8Sw+XG0UnmuLhrPBF6Q7juOHN2BTaSn2X8IPtOmf5Md3KCBwb8xoIz1VUMGlgyQpvu6dL6p\n" +
+ "DDFkeCWmZloPz5tlZfwDtvgzrPxykz5sl9nwu3T5nQeufx8z76FmN1ACbxbKP4cUD29WPVRX\n" +
+ "fXQOdkzT2ogLgDkVXvOMZgeiLJ8Ws2nWPXKct4EsrykjhPvkdFLv5D65hvAnWYXBldq4DUfz\n" +
+ "tYYzorGqiyQT0p27FA6z/ohsOzkrYT5DHmOcgMJCItgnifuFh2LnXPpmW+PGPtHY4Ij7hAaC\n" +
+ "XCE++XLdlHsrEpx0Fv2f3zjmdLYRRLFkYq/g5jMWw0xAhTx9MyLBNSOTELeEZ1gOMyEUBMkg\n" +
+ "64uTVRkSZCNjOMj8QuzozG0QT8zKXUPZufka7ltYMt/LrJvUx1PqeX/Hf5hd7ZTj/2xdOZlA\n" +
+ "DcaB5H8jclPjsFn2HoLeVHnaKt1ImdQMmJpktGzC20rT1ZVqg/jIhm1hEC6rhIgXI6UaXxl7\n" +
+ "9sun22kYio6itWgJFtlQvdEgiTHlYF5Agq6Yeiv9/gw2HTnd0BFL1RHrYeUHHBxvM4Nfalu0\n" +
+ "kVRhhnJBpa5kvP74Ck1DpSaSQ6ftLOmbJ0LBZQbWxPuH1bOcztDPxW5s7F5dPqfKRfzD57s+\n" +
+ "CktZTUI5jCkxGUdsLboqCaX/9ne6mr/KQqWNbkJ6Vpl/uBMa3Iuk4UdbVdLPa9QB37vxLChI\n" +
+ "E0iRpbPCa9GBdvyf1iTlvSEAJ+xkKaxKf3DFt4ro2+CcUllEG51wegf09GacjX0vtmrJVsZm\n" +
+ "rMnt96KXdL+DtJicFFovTuu4ssf6lV2cIrKLbHBrcNuHjCAuuhsF/r3p5kewh2ZZFfkqfQ73\n" +
+ "T6XlHrAB7+jVKRPCavljLiiU/mWIZ5caadS1wDlf0Yoor76bIpr5Ifn6QiV7O1zOuboZwL4k\n" +
+ "QhLgRCCD2wn4BkeGccn4quAZSFvEpL4G9vjl5efeEI71WegKBIwEqL4w8eJCitufg1I47Bz7\n" +
+ "k8/tPa2R3qZmoS5pTW4ObX84i+nbTpuVanJ6BmaLqS/Imti8pOnu/+Nk57DYAqz/+PboZNqo\n" +
+ "wQ+d9/s7/ORYYxD085yJvTZdTsldaslunLviDXPE6WUVtt3XzNxCR7cUxNcIh8kOwbxPkwhF\n" +
+ "nLdqQVDHs8KWUb/mJPUkipwWxnwlb/nSjs+6T2P6ansxq4FNFQJeXVCLF3Mnc4ZDeC3GB/KK\n" +
+ "21Z3JYUAynAWuK5y4N3Ed3GUHhJjUReBvW6T+3MsgHapQbzvHvKTmueIuxa9nHXsUaxojV7V\n" +
+ "PNxp9TRvUX5KLJ+OPZsVhut32zpe0/HdSHeUVawdIun1chs73Gb67bZA0vnhirbASCStNnyB\n" +
+ "gTaw4o53N99N/11/i3zurK5bxqnAhEfe+H8cY5qwVOf3zksctxdjBO6OyfG7EyEbFLgxt3MK\n" +
+ "rRzwleobPeYBAp0Lotu+iBngfg9EcoC3kh7XTx2Kqc8OGISjRF7Vsf42AVWxNZc6Y1Z2kfcm\n" +
+ "zJil/iTM8sNSfbhOQ4HDA5Sn+WJXFRkz1fx+7O8bpikDBZAanEUDxO7gsn/VFezgIqJZsJGN\n" +
+ "4U2Y+C3TkRT7jxvYISFJtTr7KzQEJurFvjHUBjf+KcDc4J4CAQdDilAro4auJm9ji1k7+6dn\n" +
+ "rd5iX4Uu1GIs92wWbZ+jI7CwWDCG8GFwaPXa3+rfMgzWQLK5Z5papSZ3HTU2zEFNj4w51M5n\n" +
+ "4N9hmyZolUROZ/Md7gB5lI73EcAxVmbmpSCQ+tTarj3jIfzXU8gx3xrTx/IjhqYFX1jvzf1n\n" +
+ "Q6BNzyctkUAVpilUv8FFdCVl5qVhNHcOzzXGemxUNT/m5e/1P0dAk/dt3bgw1HfGvzvhoXG0\n" +
+ "19OMLCpf64P8uQbq53Dg6dlWXIQt8Bpg61x9z53kdD02AsK8LPy6H9O9HdQIgJX29o3BLwT5\n" +
+ "wMinuRUzgKPscuLOlHS9wCXbTJKa7mAK5gt4wf5Cpks6Ps7TYY2bq5AF0cUlHNnhiJ5XnbiA\n" +
+ "wB87rVdZLJaLHJRkw2P/Fd9xuEAHfmFqHkOHIF4g9dlPOV1nAzetM/B88QTWUta7W6uH9SrP\n" +
+ "wHkvN3D+Dri1KpAyGNauMJTXCl4iyF+9+oCD2IrXYo/imlGiNHvgoiBQeSnG//F5ZV4typ4u\n" +
+ "akQZu4NvOjI7fmkr4JW2w+hAo1zhNGCsEyl7jjU9x//xtfpKT2dZfg7JY6C2LlqyMbDXJFO0\n" +
+ "ru54525F7mHpJD1MG1a58G+bBhVGA1NxB1OSzmC9fdIpkFPsE01/bv0lcM22Shd6Y3jWW+U+\n" +
+ "4KupG6U7+RWwnNfQE8EwYAt3FLhHUz5SfdctalR8W2xG1HaUB911r7dX1/v9Hj617wYsgLwD\n" +
+ "rfQRJiQuMpleYjlsRGW9gonyH0k4WYHb4WbAB74QSkV4NYiqoYh5CRPzfpG3gCosNDw3pbil\n" +
+ "ZmA6MGB7x4EtviOMbyNbHy4SgLXpRxhOrBSFokvLseV9RsNW2xlXbS07zl1IFIo2GqZFvG7I\n" +
+ "RuvREl8D+83OMskSwKltdTIubJlLrFNPKbAXnXk4IIGRykhlkv+68zfP1hVqR2B7CTElHTvs\n" +
+ "VaLMtXKDPRvRae02HpiDCbzVKBMVlyttetXQSXg6d2YY9mT6O3ZlYri5aM47j1vwEnmgurSt\n" +
+ "hwoJF0mVCmbvNWR2JXLZ8IG8LP+xdkop7bBufL3Urt34iRucih0krQMp0txmIp3N9V8Bou5l\n" +
+ "Ce8Hc0J4uvcf5y3UHa28PydhK6XAJP8j4Lfkmkz0XrcXed4Z8psdsN+A78rJUHOsemcz1xmt\n" +
+ "r+qHdvDCW3SJ6vAS1NeaaKE7KepaWGFpIyA7uAegKvVKzSMigJZqF0DVhN6kVo675hBifJsz\n" +
+ "yZ+6douRnIqITYIrT0pF96O0D1totzUJ+zLTH+sOsrVusBDDNrad8ZX/YirSiS5vMDeyPKB5\n" +
+ "DJ6e0LgGhOyVigqNM/EBngFfk4OsKCHNi56KfQ3Egn0LAT7krK72KW2ml287CRJbnSYjLyIl\n" +
+ "PH6Alfa7wje4s48AVM2D2w7sAQl7PNr9fuOFcRnDIfjsWQUMAo/m8jsqKZYeBXy8RNXbeMdh\n" +
+ "KqieZIbWJhLJ85EGwcadWXNF60IeCa/ZXov0emYNMnN7uF1ZR5nIVUyDMV9MzG2RxcpTb1lO\n" +
+ "qaauedNmP0gI7l1OSCNz/Dt1KgzP38dg5YOi71RGrxYyz7Kva5NHiFhI3mWHJEdmRpnx142m\n" +
+ "Zy3MtpIPYoMWOxpyi9oEOPps1VvXxChVO1bePOh3CPdqzONsAXz4+P38R6MMEtiYQ3qOxv/F\n" +
+ "j+bE+UNAIyG2PAfKtaXOJ8rW8qLIMUP5aPL4/gkGDSRuvSBWpo4oWTfLwtI+FLkJSursuOha\n" +
+ "+96QakdwiSJ6p+yWaB/ex3AhULVsYWaBdV71daW9GHsa4tsPReoRcfHYHvXQy6LC6fppPiGV\n" +
+ "9iwhXbbfvuaQhn2Nb7B2j1ovG8wqtfyk+j+39asVFyNTaQiB0kA/KNu/NAi+ZNTtBaskvIjp\n" +
+ "4fFYn3pBV70OIiueCJbQTMzzCCqkPzQXtfcnvBLrDwwl4f8M59elOgPHCOBKOkEsgIf3SbNS\n" +
+ "2DreFkeMpcIed6vDDXIK1PIqmremOmSnJvoa4okRyu2SdXekQWknq2rpm20mySpeJd23/QXG\n" +
+ "gsNPPW8lVUYKDOY/YcjoxFzjRemhDZiivlN+4KBLkATO3x3sU/ZD1EOXSSCk2t8J6nzSCLPk\n" +
+ "JdLhaz/V7Lqt7ML5hmlYO30oF1wUS5U9Sx0vrWO62lvzj6FYiw75er57GfnF9n6RUl1VEwOk\n" +
+ "8NgYy+/XQkXLqExe50ueeKTICtEP0YwNekgKlrgKKwEtM2VGiyzSzZ0PL63yeNixOcVuh5zy\n" +
+ "WmsY1VgPzdZ2FwAzxtvBcYPEpkL0R5U1fmAhLjwAzd4jDG11Uo+bhpwTA+mf9KTlw6hwV83V\n" +
+ "ivNDJ+SXvLG+l2Bbu1dK+CLDB011U4lDV10EfvP+Op5keWlTY8nCozy3SLcm3LYkcnSB8aKg\n" +
+ "bRgOM2ZWO8gxxmYfub5OsOeTWoA8X1OEwOUgIA99KOu1p8PPr/tJuyuQd81KLUsdFUSlqmXs\n" +
+ "vHEpF03T825RTrmyFkusRXUCSgX3dvoxQ+Cgwkac2+Amrs3tz9FhVF3dZbgGuTXEIqb9pheB\n" +
+ "rlLGpmzJc4UXdsFZh2qSpFq5Of8aLgrXDYD2Z4S0oL2qvsjXF9rTTfqnWSTpbdKqj3AavDEO\n" +
+ "yFbhBRSNZhrI5/Fi96CWkx7JDk4boeX1REmRqQr11O+emU2eJq5em156zMBKaY5qeOX6kcyl\n" +
+ "BQrpnwq12Di+tVZ6IHamNEpobZYh7Om7l96FGsPgrCt8k7AAtsDRMbslBotcv+uzIGuiRC6N\n" +
+ "rmn6fLSbkp7M6dqDCXnQYzH1rIIIAhHH8t9kjUmv+QjdbTZV7UM/3mV/U+35i+dUi7uEoq4l\n" +
+ "pIORSDnGYj/mCvqa13pe+HKB3+dvr2G1n0Ouh8zg9weANoXwvRQ1/WQcrZLTS28woIssJgWM\n" +
+ "tRDeNQzpguB7B3GgoLf6N/4/3Nj+S9cPtnrgV3u1i7Cb3tMPOzpPmUjFtahJ09pt/CTnSKvC\n" +
+ "MZt9mu6B71hRdMb7mpwswVv50HwWtBVDIQ/nMwa1UX0iLmxZ0kRRYEzvsqyrcIPrxYzIwdgu\n" +
+ "eaoQXggEqcuEwT2k+rN5l8oBYoW6y4IHaFvuo63keiQRzGyDdtjxwvu7HaUyU0tJbcTBygcs\n" +
+ "TtBOMlVcxHGRrc2R5VD7lZTCWx86ROwI1j8WtpX94HuY+siOqLFonUiEurKss/4ehfbVSfcf\n" +
+ "TyHS4h+6lobZaFfoIkN5rW5iju5rzOWQxbtKfz0Fbl5bbs1qe4tJSHOG+Wsp16gP3W1qIB5L\n" +
+ "EgLc579ve9CFE57TBIR8zGsCRhDarWLRNk786yHSB11td0esF/9bMAA3RMMBe9UVSKIP/mdi\n" +
+ "L6C3XWzOzDWihUAs9VuDOWogl2+PZ0yMp21AjeRvo0afZYLYQwNng9fksmMC3qlD9Z0ssXX1\n" +
+ "109RZsDAo4jMOv4MZV95JTJqq5Ti2TvOf7FtyWubLkHxFTjoaoc5Vi3saXpM051if4pI1JDb\n" +
+ "WFmkxnOcxosVzcGvF36FGWuV3tx3BIPTY9p5Y3h43f8RLE2J0AmZVT9EBbWDEFYcHogjNGfT\n" +
+ "S7psFrm7FOBXwTmNQ70aUL/7sheXyftywrKl89In4E+Qfp7ARoq6hbrTUM4XP2q3Onyl6UVb\n" +
+ "qEZwvX8fzK3XACUcfyHXfunvn8NRGJk0EUgxf+GIXd4h76j8s0vyOhgTV9/m9uOs/SzPaoFg\n" +
+ "QAeiOgVWfaJTO3Ra7Lu931otDh1+e+Km/+kbx1cD55hNuHcS9wlU+ohwKD7d8jMXoLgQvXjz\n" +
+ "uKD/Zn3FMxyNgwhLJPYfD/tDqR6dRVWGQXbiibW7iieYD0IrHfsbUN2KQ/SraDEypcVSxXTh\n" +
+ "5OCJioK8C2p8fUuggfJTRhzUtVhnBwRA/eV44/b99Ifo8o52+n5g7eFo7KAnRAEAjRkHK+Gk\n" +
+ "Tm10mVPIl1Vsrz/j/NT+c6aRjq8RBIIgAEYUdOjDp3jyydz9k44SgJsSdnHnYlSeYwtsxaHI\n" +
+ "zf5Dm+cI324tx6Gdq0t7cxtrlEijUkegAVBSy+PF2kb4aeVc+GjosWjJ9r7yLzV2Lsz6j/Nl\n" +
+ "3vBXXdy9Ho4ZqOrlb7usA8ecbGxZ4wPdNysVRjiNJxDBvs2SV1BJ9HtQ3gUHek1KwrYRq4uF\n" +
+ "oxsmV2J5NV2e6mYCukTCvZyHpUA9ZIuhn/U4llnoOAaJXdgNou6FEUblyBe1QQ6FWmP0xcVq\n" +
+ "MnHKY0FpBYkGX8X6suyD6NdcCPU64wyqmmBX+hfmFEoEUjmSlpQ//au9voQPSUGBk1BHeyi1\n" +
+ "+oD7uy5xesBlcnenbmzAJVk1CWpdbvvII92ZAGLeQlK1JC/xiqvdkfQZ0ifkH56M838ZnPZJ\n" +
+ "rQCW9TB9Gv53QZ09x/P2b3VB+58X+UMxeYl2gU5dZIC1ZABOGihh0zLukCayCj3pgE5u2udu\n" +
+ "ZZjmNYvIx1khp02kWZxBl/R7mLn6sPJp8AJlHZvhg0eJFdnclPzveocADzfHPeTpFn+APBnz\n" +
+ "WPCGh5E04F5mujrGuKeRJaiRwYC8PEEIzrMSTCKthMOHts/xY3Ic26ULTMxPjI11fIZ11R8x\n" +
+ "54UYrrCZo8g7pWrxyBipkjbg85diLCQy5+rAxinVubo+gdSxnbpiVOMwRkY9lNt0/7Vy7c1Y\n" +
+ "yrTOG/2sU3DbopXfwf9JNkwP14Ba5pJXHy1yy3EsM9yR+KPc52dpz33m29BxGmOTTKIVVjov\n" +
+ "w1LFJkFkLuSX46/1bx4CTd/T8+EcGw+LRQYi8qw3xrXclhEJ171ZTp0XpE2ownDUcTwRiUjj\n" +
+ "x/E+pwFrwMnKTWt83ol+xWsU83N7w+DhoQ7jmi4CWK35J5JAh/mk5ofGNozJAmOdgk7xixbV\n" +
+ "uEkvOAGj9kwfFYtR7+N8ab69lAEFNbt/mBbd8ZfPudqi46DdMM94n26nFupuwRaNYEmZbYJf\n" +
+ "xZ1G0fCBgj9dLjw+ZlnC+nVnhkY7ZG9/WtHW1bIorRvJGnbk03MBzydKC150edeW8leqBp+v\n" +
+ "bp07c372rZHCC3G2J33xn36qlOJ3/zWf1wPbuJIrrF8tY9mcttpzzF2Qopb8oCVEECjaRlQU\n" +
+ "EkgAEb1xxIW0syvLQRuKSOKz0DU4kKUxoHbyN4PQ9lw3zYKQFTBh1CeuZqlYyuPuG0Pfj+Jh\n" +
+ "Lj6KVvRjdEgXG366qjEpa74cJ6rvqiTpbkBysvPzNfwA9SPNXUM3S+pAmOmaPwvcPsWVpevn\n" +
+ "/CYtksAPtKW7wO1wj/GmQ2kzDPZ5hKqVUkpvDPwZyLkWwLseiOX95g1/9Zz0UR0tRBnPzW0L\n" +
+ "pLttlajn3kv+zs99yQqjgrIVbuDj2kT222Q5FaiKYCaPGt6XlaHF7krMl6ojU4sLlYY5frME\n" +
+ "jKQBA79r75vHvDyRmgeBr3VstOI+su+pZCNEPlmYOzzWPOFJpZQEoz6wKhRlPMbxCufV79n3\n" +
+ "FmZFk+XIYaIXypz7TMaCiy2M1uzKD3ChuJc2SiKCLF4oGKMPG4iIcYWkKPuqxaBB4MHWSJw7\n" +
+ "FxU0SGBgQTQpuPZNNSygrHOcVThaIhsTIvPEbnYHG5FQvOHa5zuEEaep5Oj4PAy86vIj1SOp\n" +
+ "Y09gNrOt9PJgOvWIVCPYaAWUBONqx8B4SyqsccXpPVzrnTdN3qXlAhnBx6k9qwRCsttgASrx\n" +
+ "M+JSSiSakCBKpsHwuzceG8cG1NbWMYNQZbk8L3ojNVEqOG7awufWqOf1TNQv0A8a7cE6MLwy\n" +
+ "pCKrb+A/oawJq/pU4AUdEDvGKsnq771Ektk8uLXM9nxhM03vsT1Tv0CvIgtWC0DTnbT9DcpQ\n" +
+ "txMcv//WtOhZ2O2OXBhruf20KbdmbehBvYpFmLsfjVH5500MR38FaOfo7MTveaHfXIPK51TM\n" +
+ "OY1JLzLjZNtwrXDSLE2paTokynENryw60MRUnPRbqcImP3Ro5kFM/wQ4QmaHK/P8c9b+S5UY\n" +
+ "nrdDsFOCr65X+8/1DeX+jFHO8TGkZ5/+C3boM9sHEk59GTG8Ly8myYWDSEeNV1QgxAuFsRgi\n" +
+ "iz7aJ5QD2IfRIMGao++g8N7rYZg2GfprfWgBdOXV7A1CTCYmszdQDLxkLmd2uRUgrFOyJBeh\n" +
+ "1d4oAVem9rljEmhKhA9VfYR5GBxjKIauP8wUsE460dxh/Y5dx/UgTcoMM2EOozkZT07KemPY\n" +
+ "NIsLuVcIrpjx+4tIh9Jqxzj96IEf4R36sf0/mAi4Vr5i7ih7hNf9WqBQXJgmxn1jP9zyY/P5\n" +
+ "5Tj4eyAD+N7apNndwWvvWakk+RSSY1wZOs1/8qlNThl+Dx7xbQjXYGJ0l4Y1BrwmBsca/gMk\n" +
+ "hZ3KarNsb2ywJzL5ddbUr3CbSZuVVtzHQTeOEOTAkKgSYemFEwVADepFfP+N+CkQD9l/jPtb\n" +
+ "JtP3NxEne8OWrGZXDfH962jXVoVGo/n7LArJCk2eCBRLu2GeeI5U7t60+D3kvilKm3KzCNCG\n" +
+ "HoSmE3iOzRmCmzBam640WW4L2IEVrhdlYyIcLpHDev+FEIRF58KrTKR3+zsHncst8yHo2SJ+\n" +
+ "nPJor3ow4UxlPi9W5sciz0vqaBUw6GDI+4UssTWZXew5P9KnQXu27QCKFX174ol/Xj7MPK5G\n" +
+ "20QXGuHcUE6WeVbu/R046begqyWmfAIBSfsYJzh9lp/RKTwdbVd+eVI6Q96MXarLb16JdZom\n" +
+ "RCjaZKu96g6xl711JMHqP8ckYgghrqLvg67Fx7b0RmGCmX9d1UjxWWBkKRa1fcCyCNlzGdRD\n" +
+ "QjZ1/+SbAMDDnzuBjPE1r8RSfW1maD11JU506s6/N1U90oXe3jmgPovjyvPUo7Kfu7mdxc7c\n" +
+ "DAPdg2wTq79gcQ2dOKMHYUCBa9zqBAoZlXeDY8MCWBFB2oH1s4ZYVd1ZXOdo880T/QFNKmAt\n" +
+ "fkxFaXKwuVBbz832ntMngm3219LTs7dV5zQj7Ualn7XtHDUoptl14m/7K1Kvp47tW1TNdtNA\n" +
+ "5gcQsxY7enKaL1M/ymQPCB2Vtq88gAS1g81gudXWwSsgR/Ibd4chHXjX0AtLgSzQTO8njFcE\n" +
+ "tPqXcf6jxPH0hy55M1j+uTbCg0+eFHOsWvtak6DHmCXKJEVMtjDK8tu8Aqs3dszlC1vcc9wY\n" +
+ "7Tm6AJPb2NXD+Ly+/b3H2RsyUYU2GmgyKU9DTzm0Mso075CAg5Kijo0KmTbJtOdkqFdpd9rf\n" +
+ "PjA+c/Lt/VFyvyRkUppHyOmecwIqW+N/1gS3ZPWZv3CpO4kBL+I9VszGpe8OxiQBv8PVmWy2\n" +
+ "j5alpanRWLIxJUqxDrR++cwvY+zk06i/cj9PaA7ZS+IjXTgYCEJjJe5gLNikVYtXeh06biiZ\n" +
+ "NKV++wfi7BwbTk/zapIFl/aYPjricw+OioitAvtMlNy9TLfazmpMjepWGu6eyEmsJUu+PyIJ\n" +
+ "qLr4zp6nPL7NDkpC+d+NW+26UKJDFuFu/9G0zzzAFIqHCAtY4iVgHTQBeZjUXhyrkQMIlbhz\n" +
+ "/AmEAYHjNJMOcj/1fNp17bCJKsq2F9PVspsDAFWs5aM3xoiMdPZbacv/J1LnZzxqLTw4ayOq\n" +
+ "xISNpve4zdHKBaMXL711fnZ9Re5dvRxBtsikW76m/GYsZqtCXc4dAgwz5bJZTwCErKv2HEwA\n" +
+ "1czSfxIKtLZfgcn2OpUTu/pCXXwjUgrW76mMxx+Ew5lRzyQuZmh8FEWLVEfl6ZUfWF/uTp9Q\n" +
+ "2pdnrqZSfGa44NCqafNdCoBRA8+gBeAVRb4fDc3gdQMaJvgM/Q05BYk+x/7QSq33aPEteKH8\n" +
+ "WE10HHKeCF/Q72BfWs4fKAe9uLOdCSpxzzbW4ng+XcguAIsKW7BMGtH8mfr7Hx0VONL8o7Jr\n" +
+ "cE035LUC7EedjiZSXBXmIqoZ/WBd4IM3I9w0MCQqORg6eqsjcsYeioF0KEwyu8U64W88KqnV\n" +
+ "9UkUbf1pdO1vV31/r2FAfJL+zj+t2syzcDzaB931FtdFGb4mF9WBQMDAXZ/tRgyh7J/L/n/g\n" +
+ "zy6SMmsqB7XJelHjNXmi0o/Mz4GzneVzYomV5iwRctD6RP8dxhUDFgNdFZKepu5vke5Ly+ot\n" +
+ "mild9RHtQFL8tfoUVfAGdHTFByujfryb3agmM9Z1JccGa0qSXWUVOGLNWHDeBoKglSavkYGo\n" +
+ "60Ik/PVq5rn3Z9BraKsNEM1pa6IE6jyMJLmK3kxzs7F4AFpMV7fpzUMPhEPrQk+PTflWt+L8\n" +
+ "tttXzOOdeFeD0Vm8dqvmye8hhL5KwORhZ5Zj9vdKG9XZwxb3YL+o41AK1K2j5u2M/PzdpgQf\n" +
+ "oX0te214mUo+6MrYi5/2WG1Quzg4ZK3iyGDNKL8ezu380DyoYCXgzBkd3j4FxppxXVi/ARD7\n" +
+ "1mYABAPecrRIxDq9i8wf42ih+nm9M9n4CQ6LCTSLeH/uZPZeIGagFuIhHIeQe8DsQ2G9eH5p\n" +
+ "a7czcNHkWn755Uor3JcR3o+jQ9APDd43W57norYHXnRJomN+45PWIEhGyOMNozh3fZ3ySQ/C\n" +
+ "dDNiReFjxm1gWwSF1C58vZJl65nnup/r7ni6Ht2gia9yxA38EeVg5yELuthsxwTbbjk47VYv\n" +
+ "qRv3B5d2HcjfS0zg4mHjwSZLu+ia5GquBAOPKgotUkti0ianeA6SZ88owXsmqPV/fUwrwFkE\n" +
+ "qC+2TueF4hVHXXrArg+Y8HfFmNdHLFALhnCzpYKIvwAh+3ruQ6degZgl+LYW5gLPoPaZoy10\n" +
+ "c1oVkdvaIrk3WSp1wmg0X7MB5lKQwg/9pH9aZ5Fg66L/NmYHck5SKlnx/Snmc8bSMcBl1m/G\n" +
+ "QLzxOQaFVE7sTRw3hRiOos2mA/13y1pF0/yxT0sICyfy/8JtLXTFYSzIvr1YqdJWPXHRkC3d\n" +
+ "vaS3FJ7sS2zBxy/rqE5Ee7S+nkQH40BFgVBZ1Y1HC9h3PX1YyFgTT7DG884Na8mPbmkBtrw+\n" +
+ "TulraUhj9pk4Hl6sGlDyMYMJBXoNollqyVCzYAlrOWtVKroCYYo3OXV7Doa0cJBmfi6ZCDbB\n" +
+ "3g9DFd48Zn2jn93r7m3TZWlBLj+pGVieBddOhA/sV1RJcWWpojhlks2zjpPsnbg6PLo+w8Jl\n" +
+ "eJXii0rAir5oHxlGZjED7Q0vXfaaAv+eZsNHjzZrYty5sBI/5csCns/RaERoKRWnPjqAihFf\n" +
+ "G4R4n86fpR3qQXS2LCzUipcQ28qydzRGAkzJndFCLtyGhoaVkMaETK8kX/k2iuf471Xxyj0F\n" +
+ "CW8jabKPKeUCAtmGQhbwDFSrJvTmfa1QaUMRiIsNkU7wUnbdh1nEasT55GWVLZdbqWRmJzTj\n" +
+ "YB2jXv4/QN4Iie8EMRCaZyL3NLtFya/Vh2sUbqGsFw16YSZZK2E3fWfM94qcOPjHiJyPxoeT\n" +
+ "CaUdSGuSm+hTFLDoh+LZfwAJEeGMUDAZ/QgiwkunszCNI7FVEhyjcPEOjZ97ijODUhfO1r2T\n" +
+ "neGCYIoNVMzR4n0nGL8fALyxirV9F+dBZlIwCWPDZWwes+8A9buAJhoAZ7umJFYhCQiqXC+q\n" +
+ "ZY0glVYTnaS4fNQ8hTdi7SIN86WUAZiNm5LIlIUHk4ysOonExQ4VB6lCIHJrj+ucK4v4ZthK\n" +
+ "fP9w8kiqpm52fln2dqygroU73DxXsOcMCi90JqUaqAgKiXD82pYVJdGkuOFcsRVKyI3BaN4c\n" +
+ "eaJg61/PBbmGdgUUeubiuG82aGo6nxYGUa7GAD/VEKyyoyl3ba2PvhSXe+Fw5LdsKUs7GM+9\n" +
+ "YJ4gwNW9upKSDoA2ZXubC6XJ/fvAPlkDQj03sBM82GUB6Y2Y30ptrODa2RlQr4/aH1ny0Tt0\n" +
+ "fWv4rEivi8ZsKHwvAyThGW4TQi6Qa6j0d/eabNJgKHnRAPXPZPuYTeudcGg5tbaKn6OeZBoW\n" +
+ "7XzN36cOel46M+3ecBYcYkbGhtjqaMCWa/8iTA5KzXRgK0n29qZZC1sbFECkji4ObMQyjMTO\n" +
+ "sZZyFrJfLo9XUfhj2EoJZxTnF7rIznjSXhZsxN2KcOlqF4hvULDuql/2AiW7nRKa3LnlhYc9\n" +
+ "z5IJ7vyDU/0bMcu6T+bo3qdDNvMbBZ3EZQQzzQqqu0Egx/MEyyXo3UQqlhq2ueZ8VkOIpWjc\n" +
+ "x62QOVhkzn/rb0ASB98UDCMBoAHnu4Vt8HhNVj9gtYzIwsLhCWWZtXe1qslbKHG9RoANsfdk\n" +
+ "utZ/GMSuUXjoV9NyQFAomsKHf30bTAU4nYkxj3RJviAO9aQojVMRqUW/tjZ8k3hb0u1G6WWD\n" +
+ "Wa6IZAhj/WiQTnFkiiY/hSWiAKm0Er+prhJmlLuaGwMjtQIMOyNpjK80TxK1BAgJdGQnreVf\n" +
+ "8HnXz9dHAAiFGaloyC0XtjaadnXdKCW0Kq2DvyPEapfWgKoJnDc94dX80plKfwps/Dg09uoI\n" +
+ "h9ctSWicYy5q2gg/rmy6fBjw3rrv6P63Iu/Yr7d23G1lC8MukkUa8fTB89wZ3n0Eh5+SplvG\n" +
+ "YyVY7kPXxaLS9WPa7GdpEFFsdLoJ5aX0ipGibkTATFWdZEgSCGwCYUpMYJW5K+FvMtzztlCn\n" +
+ "e9ZafDobzb1UFcXTbMcQye2BlFFQl3+qq3AJQi7+OzP8tmprRws5z1WfnUr/8vvps5f1PxpW\n" +
+ "kWcQv1GNlq9ICkiX10mEOq1tVW+nVowf4tw+2y1f9VyuKnKYXWhwlfvuYWfqZG++nXZANhUO\n" +
+ "5CbHpk2IYVk9W1sMNOWq/yIoJPRcVCoNwjz2M0d/ugpAcr7RrPtdG2JWeMPaW2PVkt5UCUy7\n" +
+ "kLjx+EY+EJrSjJfTKNAwNP3zMCPohFuTL7P8zW/qWdoNdr1J6Jn5epvFXxg/4AFPM+LnchuU\n" +
+ "tDxIVNH9If++y1P7wIywzSZVjlCEk+ayGtADUqCvnzwAzlYFCoBEhwTgd+KlEZoyIQYeS8nY\n" +
+ "v6ZkgL0prWsIP5Ctg5cm8yZdXzfyWZ2VQf6qQD0amZ8HjMI1TjdWTtw+sJrTSn6Y/aA9vJeo\n" +
+ "ekNLOBvlYs9UkkxoB2/P+KlWpNswm8ykO4F+kETaPRIj+0Jquc/DFO8loLZJpomo9iyu2+BP\n" +
+ "Yi4cZjRnUNyo+aDK2DXM7wGm5cR+SYHkxPRRYmhrxDdJ8GA8Y3pw+KU+j5DeKgBInnfuZiHV\n" +
+ "sfxmjdhn3OFDW5NZ5Z9PE1M+qDxNkve22sJmiMGPmbBGP8L6icoMyikAJPrvNUll+7qpgt39\n" +
+ "eZE+P3vufj8yXa7STW781cPiWGb9b09nri2b81gwan++4n1UTWRUloMWj4m9TFFY/xw7/1lq\n" +
+ "hP09aPwIZa+sy5m4WP5wD4Hp3l8VcobFeWii2PoU7HCDRyM5J1BFAtPL9l4Mpx4CWTM5OYzB\n" +
+ "H0ihCM8sNWbQFCttc9w4Bh+vzWWpFJPDqikr50aWTEgXB7LwvTx6LiKZBV9cF/MDx/8kn9qN\n" +
+ "geqJobwdsQB8zjVzmLsSEatDHkwpn9owhFV8l+BxavMvBIMGx1lc5zOQ2mXV/n+FVDnEo9fk\n" +
+ "rurpjd663byXVgGtot3dWyr3tUjvARqNjyK7uRUT8O5mK3yDbmtE6+Gtwvwemm7nD0btP1c8\n" +
+ "pY7OaE6MXWioOnzhLH/5spqrbGGV/aP6MeQ+HNKtR4Jx5ujIp/0dVKIvRJ9g+jXSfC8o4j7N\n" +
+ "zw0/+uDrV3Lx0/6IuovFeTYLyKLyuyclVv5hnNZ7msql4Ld/+2tekKGR9Li2tLiVFJ3yfOvy\n" +
+ "YpxDMi//6FgB5tXubPRDLKCP93qurNaCpEICApW5m8NpWvbYcoSSdZKM3YNNaevmP57XVO4D\n" +
+ "IXn2H/7yvA1NdmtFkOFKkkXvhO0JnrNlEa6NMx58WUm+7owtUEv7XT22S1aOpZq4sPky2yxD\n" +
+ "gwXqB5ygbnKbNYQb+mkkLIjiTecgFM72gKmtLDz6huaNvnpxK4uPo0QEqdRUhwD55fcoYiya\n" +
+ "0RsQhNjUAQXC5056WzDuz5xRahSQ2PbrT04pI4hzrlvOdJssi8TtKiL5UFjD67pwIcbmNnps\n" +
+ "1RXo4g2O1nef5/WHe048ZaPdV/pvBdTiEp3bjKFTlD35dUwFcOmq5+W964BmljjQYu/6rGdG\n" +
+ "3Sby2g/B+RCtEz7NB4GA3/5ah7SoJ0cimcA2HRF71Pa5T0cIkyEORSCA9pXrXi3pDz0RrqRQ\n" +
+ "4MsFEiTnJvl7K8MVRfGhVpZSxyvfC1WY5dZ760HKv+fBJAKPZywaIT7wg3Ka58t38u5ZiKFc\n" +
+ "mGzN6M4mvgKTG8EMKgjCcFc9v1IdkWC9vijufVcxfW3rFkPNnakWL0td9qHKq3/mlpxVpBY5\n" +
+ "aDGpdCyzIAmshRa7zXt3LzVWSLmnCzW3aNWd/eLmjLfA05e09lE5ZRF4lOAU5bIC0EB3+iLS\n" +
+ "OSfPE4APylT+7cMlkp/CdBbAfio4xrJbkvSgqwESXWisFgZ9Zih1b/APM68woGmpf5aCY2Wy\n" +
+ "0MqzuOpHerXyh/O8nai1zTyDz0Nqe5Z60ITQR98tV2DHsQDazPSU4Jp1zAA4QuW67i2xps1p\n" +
+ "g0IlOREYGCry/mCh2SPX79USHOq3trmd7OVCaaWHSzzlCuVjm3FCplHq+11/sAw9c7Y9lriS\n" +
+ "zp5li+GbZ1ZVWt38XrVoUkGrexy5Im09C0zNNYMNMMehHkLGXhDBAmBwDWgw8xP+SP8vBa6j\n" +
+ "jCI9AB3AOr7kDL688ts8B+8oYeY9/2UiH7HA7Lb9Lpz06ifrz0/Ojt535D/WPqvJj3r3NgXT\n" +
+ "f0mrcEuuFUfcjnRyKtPQevdgzX5ZRHsvyijAFAt9yUt9AlcTiZOtJerz0RIsTgq8T2tsp0Mx\n" +
+ "vtHsZWqgzKfnW2SGiFhi2aIPZgAZA5FKq5zwT6sWJsaN3iyzqU+4reKwYrx6ZKNu4fT93y23\n" +
+ "Via+Z9s4dy3JFG8hrIY06WG+9XOyFqIoccDiwFfqHGf45mAjuuy9x8SQ5eMWe57tVSFUWxwo\n" +
+ "7zDT80Lh6wWc3cPomT5OWz379x2WXmO4MxXdrx9AKBT2tUXF8aCDbtx3IhG4QRtjQ0STbkjV\n" +
+ "ftV1iQfzx9invUZlUWJOBYYO+ZvnJ8bsS9+ZlNShwxZD3Eq5RfGAyEIF4W+PS2xpZuQxGySZ\n" +
+ "C9iaxZjBGjWJ1N8XwD0c+Vsuyavzgfv3ns7dKSiarIr4znXJaBhS4kJaq7buQ7zVf1iHySHj\n" +
+ "MxkhErY0oZ8DJTxKJDuOYPfW6GtinXZpGXE3KMy1FDXUSH3RC1DdnNtQBpsbVxEjzaYD8Gzj\n" +
+ "d/rWJzE1qtTK/OwlHwZyN/5XDN4Rul76dZbqC5En2jcVo3wlh0wiOQMk5yjycX0exzEJMlU7\n" +
+ "JTlcwkR6zY/Pjgd6l6dvATedIQbS5gxeu7f0ePCbN9coIAEJF+/LtRSeONypYb0MlKxEfena\n" +
+ "LR4XQ4kH3q0ed8jl9E9pXmGJKzEL9RuXiRZw455wx3J/f8ywNWrQ4JWdXKVklTLR1QrBRPSo\n" +
+ "K0qKkC4thqs3dyxgDdywKKq/Yz5pa1KPbp6RL6Pof348nmDbbj8QG59agAaMoRrZnqJmB4DK\n" +
+ "IS2iu+ES6KSmauUTlI+ZRV3HBj5rwu3QDrhQb8w6uC3TY33RcYlFP3MVaHQnlG76tMxkHQ59\n" +
+ "E8WL1dtVTzhOhETiZJZeAgzCqKc9L6aEtHvWZdqnUoWDV0O4UUDMjpNu2o8xYH9S7cFDbrWV\n" +
+ "coBYOkk8H0B4V1toNM8IMSSGs38G0hO0aK9LHyrGfEDO6HCF4qt8K1jcvbZmbUGUvB42a1Hu\n" +
+ "A02aNM7hRsnEOpRCp0l30VSlhdB3tgb6mI1LvNXe3pwSd61Hr+DIx8xDZ0cGA+b2DP7hnYp/\n" +
+ "Z57jk2qNwTYl3Yb/K+QTiv7AN08YDg5pcmkwfR/wuOrwqQp/remhQXUivUu13pMik8YYlwMc\n" +
+ "x0r4r8EmloRkiU8OuIv05EueMspLJItIEnXxchN6BuXdmB1G9C8NN9jl4T2xsmaE0f1vMPRI\n" +
+ "5OOHmPdwoRvGC6qWkY2rpY34haRyTAWrDhELca3kIgIVgsvIikbTkQvhY2+2mTrtlVSDcYhk\n" +
+ "ngvRPIT9Q0je0IK3+3XLog+uLQykqtOYKfsA3hfAAKgnghIQjwx9TO5ys1yR7AIGeYj0fOjI\n" +
+ "+hhwEgeUe3fttRe10FGXei10Z62TXiF1skEL7odnWMpkQ4vN2n4H7LdG+dFkVU1cgJXI16cP\n" +
+ "BKrrknaRXmURgVrJk0sItjxKGzU0OG/U9amKT39LXTc6x8hhAOwNeJLUksGCQpdjsV5XBnw9\n" +
+ "5+1ekWc+MPQK+SSgxWGaNfDPw1IxEWehrIAKjRqFhlsGLY/wbgM0Y9g4XugGMey/Ibbzdvuz\n" +
+ "M6HdffYtEHdzoAAh5CEo1g7jQvzyPHVdEwhxDxV/MmcWB+B3D2AHvgE3EDealVbtp0sOBcKj\n" +
+ "NGXbOPnaI2YzkKKC6Z2DeQBBOLzz3saaoSAi6yy4b+xxK3bcEi64nGZGJKuNi3MpCBL4v71W\n" +
+ "7eUFRwKYyaHhLY3FKwTOs2paQysjDc2NIlOBSrJLfo1wgmU9sQJx7BQdkUkPZn+p7GdfFUoN\n" +
+ "k4akjIewzRwKzPTmIPVhzb4HrmbFp3EtxPlKF7Afzt+6DC1FhzSHVqMyTOL37WBCa+Qthw6l\n" +
+ "lLRtXnwAErLnnaom2qgWl24HFkvaEKu/X98eQqcf/mGcjKgHII6zfdkdHvS+lMlbdo7ATzC4\n" +
+ "Dhx50456H4Q0/4CZ5VE91q1sGglKl7o3KdFiWu+WmgKfJo6/Q9BsHNluLxRPJMA2qEv7/e7o\n" +
+ "JeJ6HBYHHdwB+1DU9VnNXdk8d1SlAGyBjVzE3s28bHRe4pLmwCh3CIbwiNn0NCXavMTMnA7H\n" +
+ "RbrW6eHdJE0AwWs1EX+SPi4PzhFkT5k8iQxQqHbRiKAoqnD5rbhqxifapAf2SA0LNrlbvGY8\n" +
+ "22kE11mwbu3QXvbhD7Ji/1U6E+z/DpYFz9xeXGdcZAFEbo3XfuHh7LQ3FKENTKFQQhVnuX9+\n" +
+ "a301TXP6se2nBIIgALj+F1K0JvkeZE0ZxpXrM+5U3lhSBmPWT8xNBJ7c+EiJtGEhOyQVUZMR\n" +
+ "mOgMJ8sWfEPHQFpgFiRPtw3/Od4vK5IFpQUPqQWCU5wZrp9qrxlwcQAPu+VG2QFbudaIKXJk\n" +
+ "udzf8ltnEc2bjGFh2opSvUsQgh0kOSTnLLVAov9fIf3qKUVeKFcG2xpFIl1BlelOTmKAU7rH\n" +
+ "diRY9ujoLTvkIg/9o+rk83GmPHR3xz3i6RSrOGeiuLZ55PffPNc7aju38GYw0PV02E7Vex6X\n" +
+ "dtmimBHav6a2WvhFZhFzG4O0jr26UKDXYDVHKb1at4ymDgiQ34KAZxT0ZxJmeNAq/KZeXXfZ\n" +
+ "0D7hZ/xhS1+3CohTQM/fG8P6lWa/ohDJirS3tjFqbm4VSjqJaOZqMmTM6SgIeTvypH52i/ZM\n" +
+ "caYsH+/BcGn2W0nv1ZHcjmuMHkrQ5UfFH4AqR99LYahcAMFYE87unzVln/ljrM2hUCkzQjBd\n" +
+ "qeR5Kgfsstnc0O0dcGdmPTRHgJDoZdQzRFN2M6CbwpHl9OO4EselflWw6Z5QwCzBkC/3Hbmp\n" +
+ "wBLZBE45JFiiIqkrxT0t5BAxEYGGyv/JSTygvY6TrsvCoH4AFVIQTi3gsy2/TdcEFU8zwrQ2\n" +
+ "5Pui05SlMlfcccuoRTMMH3qqhuzbuQMz4JgLe7UdIvcQkPGIUdUmqliXOSd1VfSjhVIrqxJe\n" +
+ "4PxKcWNUdGbstdujHh+/KvH4AauRpn9pHw/P/verYdaFFtHpSpADHahd23SGdeWVuhvBGCV1\n" +
+ "/AUb6AoGFXU+m5TV8J+DLH//yvYfzu2ajmTWHpo85/CSnxhdhwF5MWQ2mdIq/x8TC8MwRTDv\n" +
+ "iXs6QCKTGlmSieaQnV1DS6y3np1rJvZodA2/zR6CMNvXoU/R+9aYVVA8jBI4eVeMghn6vp4e\n" +
+ "E+QAlJNU9ji1xLKMzPbWJ5tXryiB+AOF/hH1U31xfFEL/XzDTE+v4rCBpi7xgYLl6CYDIziN\n" +
+ "7AJuq9RdHhLimkxqT7dYH+rPE6BUgoS3wUi1KKy3IfRESuJ3UBPitkCaUvWeE3uZrK40vj81\n" +
+ "VDC3GnXWNXxSRAkx67hi5CBTuWlhFhIrVs9VzTiODlmlf1ln/AcCfwV/xg0QQ0NcuVj6s3a4\n" +
+ "qUm//jigFtxx+AFymf+1ABprCVxD05+eKvH010FgqX5+QPUre2ikKmh9/Cmi5/P6swC1AQgE\n" +
+ "ykXGHatZBjewwuegFIa4fMlqOwQn3VG/JzpsXiQxL5cDpWlT4e58RE8GI9bKJeL5c0ceIxDN\n" +
+ "qQnMgf5HCIUeEhPqskz8Q7nr5T5BRcxQ1oVaVkhbvCAYYJyGE2PgxZbwGcO1qgHVFahWMJnK\n" +
+ "EE2vIcig1OZ4zRdld+3zOdk6q4HExzr/YxllZjFasjr99sDXRnmVTbFQ4qdCwAKtEXfx6dx7\n" +
+ "MnhQ/B/UF3hwl8ODl8uqAu7IhWEYr5LlsOD2rd+T9WiBJW5dyLoBLbhvuVyJzw3dnajipT51\n" +
+ "bDNxbsFft2X1bje54joCjpGpcuIEGntZpU65X4OQiv/cdXI4nV9LaDFvyCsqJ2xQohXSIt2Z\n" +
+ "/pDxDT/ohuCFJDVGItjOcequa/CFwpC+/kH70Pg/84dAFPMug/WgAoIe+cgJ1q5NZSPIBu69\n" +
+ "1bRxZvvaG9cMa/Bs3KLzjWCDzH3zRDUWx+vD0M8gEPjxzF2hFVnwslVPIawHR45fRV3NdDAS\n" +
+ "DMHwVtj4xbFG94OHnBGtEnAH3LTa7dM5CcHZEamHWqnVbASuQkZuiU1xrZEHqtNlNZ+rkO6a\n" +
+ "0m6izOJtlf2Mqw02tIsd0gMD03UOtHC1uie+ZcIiO1bFw6kEoSh9BB3jxt2G2QHf7nJA8o5x\n" +
+ "tJO5Q43AwUIh1evygnVDYSCNtlQ8R2wdCQ6QfUVMhfMxqGajA+SXsCHXPI4YrXGQTawussIN\n" +
+ "E0g63Q/oBxmq+XwarM0+cILrEoq6VfMzz6t5i1DQv/jVmGBlhuKw7V7XbxZV7QKjXhsAhDXq\n" +
+ "sFYxwI/4/AEiPMv2s/p2BNa7WbkgqHrQC3QHrVzwXQglO0x3+iqqSoR2qL0H6TF4QazQiXig\n" +
+ "i3dBIBS8JhdkJFEXY1ylbfSF3xl4DsDHoxHl8KZGYVcH6sThi5aumQLzYxDcstjU26agaSwp\n" +
+ "Uy0HcsZfCK2HVJfBgGJakiEqmjayZKryijz41vqgqqPj1A818TbjUE+SlewGHnnzJY6xDStb\n" +
+ "x2i/Mmu5bvymyiFWaQKKPM5/fOwkUbSO4I+P7JwqFYOIgtuEdKbf2SM9nzatn4FRSCzK9O/E\n" +
+ "pQb5hQCwSONawekPvYfVWHj3WKnUjzuWaUGvCj+h7x1NOgvUvf3P/VrFyUSXQS0zcCiixAJc\n" +
+ "s2S4tbfafNuYSUsSG7DWcastrLHWq8mUkKW/4J/ENONFjzmuXt/iXJt8vSrhWzIx2dMwUYcj\n" +
+ "/9BhwSjTVn4NmMKagxHiOXxwyFer6GbLylVP9+fXXyCt/fODm1lRBPpAdL0ycfrs90GZ1C6q\n" +
+ "gGUvbGHhXlzUmTE8pI5Ao7+m1rm2t2NWWH7IgSK1XggHr9TqGToebgHHbT+peP+7rj50EU0N\n" +
+ "lvGzbpVdoDx8Aj0k4OKDcggHR8vaw8bkuSTNn0yrGN2OlhNrZjzvy1QtH0b1kcVvrVnzJkTs\n" +
+ "gERrq37zfYrZ3nOYegLR1dvuvnl4LScLBVmLzis12XUFoQZ72NMsS4cEVhREkaKkbYrb5kWk\n" +
+ "/nh0ATDW9lC3/yvo/tS8MWsE/MHt5Bhnfb0zH8mYeBIaotjE64S1xwXLr6C+BqO73PlfCeul\n" +
+ "7c7BKZlO8yiQxTPQ1RbWaXqiNT1o/ztvVSYtwFZGWfIdwG7pyG+ewF5aQj2iyxQBiszR0JOL\n" +
+ "KODKBVjKiFqyBjRZ6o9R9orB553QhKbuVC4+vBaGh+P3UwQxlvs3rYE8zInMafcEoSTCoRh3\n" +
+ "x2pFg+mieOPeCXQ2wTSSEd3aF0w7dCNMUv5JKKNPnGgn67sg+2e3s0HoHg6xHvNZ+7FfhDJi\n" +
+ "KydDxAPW8I4f0A6hiQayPN4BavHVIfg5JsAwMkTNbdUBvrTxVLtN1089bPPT3MSEEKf1hNjX\n" +
+ "gb8h7Rgd6zGOv4ovWQHyTncB4d0L4ycP0cBgqi7wh3qhc+FeC9+PCa7DtN6rmxC3knSVOXnY\n" +
+ "8rnDnyA3WN1WgwY/eg0geejJgZglwJU4kb6YpkC3jrZfxgnRETwxmW0ezsHV3jxfTSdntvjl\n" +
+ "EMnrkZTvRX1WWWbjNfGCX6H1qwO0IAWK8PJ6rt1ESOaFGOAQW0d2V2kZpVn/RyuzWtj9VDhf\n" +
+ "ZnpJfh6t46AtbX1eVQx+iE5LhEzxE9keI2vVHTm3m3TVincByj+M7iXz31WNqwPHe11wUgY7\n" +
+ "10q/l6ZcfuJJpv1k+GAbEqkOyMcc5O8TEuGdaVlntU2GFUw56oBYaXuaF5EZ8iu+YnBOXorP\n" +
+ "Byxc+xGM18X5E00NisCWi+Tp6NbK//ig/FHIQDne8qxgBsF2RBiDfBm7TH2i/g49K+FwTEtQ\n" +
+ "dx3Liv5WY/KqTfoK0utGmTt8/HOQmchPrRRv4UaREKFoV6Vq2lBnNsI/SjbJ5E1h4bLNIF5t\n" +
+ "PnxOH9SGzvm3t5VRkyVtWLHn/U92j4mGelwNs0+Su2R3qet6Tjn0NpZI6rkOMN6t/e2+Q/5s\n" +
+ "Ll54iPUt0U7JUiS8ltRQW9pOFLhWnJNImAkHF9CT6ka/QMFk3Q0Gt7RiJDXzHcY3AHmdJ9KU\n" +
+ "b6m8nth3jpLjfbtf0nWNV6MqrsRyPNXpx/Eh6Uu7S+FUAIS+uk9ks6vl6yxStTqFBofoZQqK\n" +
+ "qfTB5MJi+G9XA31vuuYg6V5kyjxuJ2LIYgDuO7tX6Six10eJvjMHqFTdXUekU8JYeucN6o4k\n" +
+ "D0MF0VzTHW3BRCQNJn5w9xAx8KfxB98OArnJjx8KvJ1SQFm4JqpB80bIfC1TIBaArBlN1g7k\n" +
+ "FPsb+JM7YMXrH6Y47u+1ThnmXxZwzsiPwRfD8NcNDGZGcwJvKQdGyd5IMS1db4r8PSMDjB83\n" +
+ "4v+9VOesOI68XrxFvYF49xozS7Uda0lGr3Pz7LFkZTeX+32BfYyMojy7+DrOyUFmUnaaxWpT\n" +
+ "wMp3V6Cj8pm8yGa8OW/ZidqBpMs9cOMy0+ObPvQz5x9p2Fb2yZ833xakHB2pLyNUqrsVzlvW\n" +
+ "CZo2AMGHFZ4Oz58YYWEao0QXWMtRkAEVawYcmkfVocqvuVvVWzh1Z19VujPjsD6pwRbnAGnH\n" +
+ "Gkha1w7GIRsIHvBC+zKJVnPO5VF8O9Vj7cgTgHK529o+w6OgjKrjubcPqopQgSwWAzVS42Xb\n" +
+ "FaFTvYzcdnB5te41pwy7sn3wDQq7fGXFvLfmFJQ3bWlXbc6IXwH6P0DAK8GKU/bp6dv7O+XB\n" +
+ "OBofFA6NRLCbUcBU61GsuNpVIltfLjI90CaGMGwRxGLgpfbTUxNzMBR4qn7E6wb66DR6iQ4Q\n" +
+ "4FyO5TaDHwkZmEgdr2yDWQJx7otQdEc1Gtho3rscsgP3n8wEfGzWCnWLvI4amlpF+lKL8x/I\n" +
+ "lgGUIQgbA/uHzuelF7zxhpXBVYtgiRGLCXkE25foYsTMHXvv51wyrJ+6agLd7ARNL6DVGP3l\n" +
+ "I15G8+ZTwq7ypHdab1IhTLyASjnBZZmPUjGVjC/lCDgc1smm3fFv9ORGpwpdrte9eL3X2Vkf\n" +
+ "D6K4yHuyoVdZN1Br5i1yV1jo884IT+mXgL2CvwONs/flu6cSI91qXgTtXB7m7PzQXARwS+XG\n" +
+ "UTcMaLlxq3Wy04/cg0hM4CiMSQbTcV1vnP1OetmvKXr/qaBhe5guawCfKlJu1vCPUng7Ff2h\n" +
+ "bKi97D/D1x1/ScA8+W5RdxuRLWAE8JFDMA7jHxOYrX21MTTra55pGa/V6i3fJ5NLNAR1aa+z\n" +
+ "RHDNss+/vTdiDYV+ZHkOST+rZE6SAC8KfMZfxrpIyBhaPMB2mZ+iOGi/H4vJS+q/X0COPyU5\n" +
+ "2oEMLyLUnlq+yu9kNskTJNRcn9UCCXEMzUut9/I9dN8JOXxPjF5uHZww0M7qC8DJaLa3tP2P\n" +
+ "QwRADNK1UlDFKmtkg0ZdjeYGpY5Um6sOT4zz7v6TCFLmTCXNiPlrPccvaySLANU3jbQnlJ1E\n" +
+ "ed63D1G+B7TO9IA8cJQZ3Px/H05Wv2ucAc3/rcpumpXRN1RfPKn+XNoglKcd/tM/oJwdCoNI\n" +
+ "iozf5SJmpBYBbOix/AJdb9BkD8shT2IQCevY/wjYJJfmLYA/kVyVDrzJwTYX+9EvgaiF5oNI\n" +
+ "1fBZ55iHr2tG6AdoumK8NpsxxFrOhB+uhl/BfO/YseuGi04rrlfZTn9+cA8vRB0VvFEu5It4\n" +
+ "YVVg4nVsGyoJTCalj/YJb0ZHQzb2Z9qA6l9wRx072k9kUT+iODt/TFmn+D1vVi/YV2ivX79K\n" +
+ "yTyyDprqv+iixouNXwbkqGNWSK6m8DSXfVeyK1vnDTMMUaHwDL3KhDGKIYqU2f1BIiLHNSQw\n" +
+ "XlTIxKYgMShdxCZKxXqPLebDBdkMUGKjIUV+oCryrqlaCMG8nRACZos9rmtTokXdkJA+PXve\n" +
+ "UlqiwJZyMSBjk9qdR+t/Sh0IxUXF0eAtrngJffVEgfQCXdtfUS8YqZeONpDQIYtfCfzjRuoD\n" +
+ "oN1t+FpGXp7M3t0E+CT9ImT1KQsnLAxsqeoJu2NGZVSFXRuba8l2c2tlWfq8o3dNiznWoMxC\n" +
+ "i0B+JLLBIhmzhz2pQOFWHg1FgrKhcqqlm4nnA9scFwP04Ly2uZmpvIBXyf126NMkXky24+mG\n" +
+ "D+BglZabg8au7Ndx0ROpQj6BDc8B7/MZWxDXrNtMYiYgqe1pzAZpK8CketC15t/x7l82BYUX\n" +
+ "hwpAn+Nd760mJjqhC+gzQahH09GqmjDLOe+v13KYUGmCnSEg4+FLXfiN1z9mY9St3DELfjC2\n" +
+ "m+cW3XupwZ8OQ8zErkjzW4zjsvQ4Xhz/6pmpEc3t7OJ1BMc6NhSHIYp98S9615OrfxEPPP6E\n" +
+ "QhR8d8nw0Yzi59bFFsEYRvI0ODqRfQeaM5jgqBooCNrV+KI3qvOmh2CgWg1ma+Verp8VvZNq\n" +
+ "GBnmjw3qQJC2PGGc5ioIVZNbbeZRPXzhrlbk88WaYIgUJ72gsk0Kba3diSqJJ1BuUVBJhakX\n" +
+ "Tx4qxv/seRggUgO3ell5E0e3a5xIEr/DycYI44i6LcYEn1eTCGtfuKHhcKv66nF+8iabaowN\n" +
+ "JIc8fhXO+vXK/tEBHC457Mskn5vSiAeZpWqQHQ8h2xpPTbmPnpYvgSxmmQZBpwv4R8s8PL3i\n" +
+ "XE8gtTyC4/fp8HN6WqG8Zq7wXnrdxyzA8Dw555oRnuJ+WvUXgk19rFm9VdAcXG6vwBhLYMcJ\n" +
+ "ZygnaYviqDmUldjCSZRhEWQNEeg0Xu4a4+lln7W9YeZkvQ3zj692pM5/bxfhRc2KvpfM/zNo\n" +
+ "qCP9ebJbn1vc9nFDDSfK6XAf5XH/7JoEZsXiLC7NN7R9x6RK3Rotupg1qMGtQn/FhJU7vscT\n" +
+ "JMBDfL4acoCpiII/hX54kN1nfQlPxEiVhco7FH3ZcuZ9FFpjy+uIyrsdH4QlyLXWsPq0Dajn\n" +
+ "CAi8om64U7GLayL+Lli72nHt8KWPxrCpDgVkYd1sNp4/QgBNfvsD8dOjAbCe4JzWz5Pr6k2Z\n" +
+ "OGnbXbQbptA/2r8ey/8AMHgUCU7VBZagsrYquYYskylXgtIl4QAoSieXjbsoTKRSjEs4KzUn\n" +
+ "v1C/dA0arWK4e2makIWFVrJH9OmLq7fF4nXsvKwjaz65k2rcHUCg7mQGHC07/9NyQnqE0UUx\n" +
+ "knlHKYvnRu7b5SjLBh6JqN0sbaDdh8vvmZS+zR1TlQ+Uq/ajfpWr1QPfqrgXooTI0KzVJHpw\n" +
+ "ske2e8072lsEW3sIP6WTdv4Q6vJJev7vAKmBOUMLWxtXK56/lH9H+mYlxNpi13NLpN0cNhk6\n" +
+ "1C8buigM8CNd1ePQyxbzAEbVqjP0bMDMI3PxuQBCF6MbDr2/wG6bed/qyYbRYOo4feW0Nsao\n" +
+ "itmFy7s9ZBPXynpAvDqKxSrhW3BNBQIA82KGohQKpRXhi4dr87LJTtu39bin6WLBXredeCH0\n" +
+ "5Jr5jJEdABo7Inkf+wR16svhzJzzpLAuEl8MOUDdZ6PDJS5B2Vnw1zuFbMedhbHz3EWAOkGL\n" +
+ "d35zyMy8TEudBq5lxplIZ0SjPEaJz6wuc2E1Mil2VFIYP5TAPWjFpgr4DB5LIi88aYz74/xd\n" +
+ "lX2VLkWJKuWcXnTaeff46PoXSTpQ+5AGir0fHD2FEhzT5AUx3FF0BKdvXqxeT1QBiYKDMR3p\n" +
+ "MPC0X+3efqz6wAeriiLpIOPauTBtHaaSkSjqOJtoVGkW48Anv7pyMsfwH1U8ayUDmE/6Rz7p\n" +
+ "jKiowjP2aXnGiqIjV18O6zLpW7QHFpvyylda6DrArFbBMIItElZvBmDLafqt/iOT4XKOA92U\n" +
+ "sG3KonD+ZSteuS38MPt4jxYNnnxyBdh5UIpvZ5UhVeHPTt7sjAROdyJnSvuhBUEv/OgDunQl\n" +
+ "+2gsdjn/sTvSCvg2uiXkBIxEm2rXNByEXAzt8eqlNqiNCNN3Z+3Itb8VNQFIWV22BGZWl/+3\n" +
+ "wN2uj/QfcDel5oi22wbhjNkxVfR0BmTefHIuK6yfxE0Gc/om86JLnjT1VaaXYjX8RCd/XRfo\n" +
+ "mkExlaP/JWqK4gpNWStrGHnhN1eqRQiCibAWk2ykzwe0q/QFWcYz9TNGEqbc7tZTg17vSVkX\n" +
+ "+O3FWnofEa4qV8rHBrAGL1mUjYZd8A9LUSQN4K7F+McPSE1vgzRM2146WExBEyx0n7YEtAPJ\n" +
+ "qSrpjQnz5H5TVUYgA7rs0CjQ7nHnGSzyxK+t3GUj4EMzljQO5zwsfBwQTPORvn9Skw46sUx+\n" +
+ "fLbGKt5Fo0RQiUoW6jHMmQ4d/76sBm2PiHfGLAHz5ldeeFvM9MFl1+aMjmZDjhxQkW2uNvd6\n" +
+ "iAPJJvmVf7szHloFgt8Rj2MlBMCnSUnv5cH2RTxRVrBKOuJ9sXHJWjyIABm1n4zYoI8veTzi\n" +
+ "vSjZz0zWaJpYhDC8XB3qaR3Oj29zZiUmCuVND84EVogig7sfjiRDVAmAfvEW21wTAdMwa7MH\n" +
+ "GbmgQs2dzFxfnLpsF8602HEb+41X4W4emzymxQ69YjCjpho27bNo7GM+Im/ye7afFb9dbkKB\n" +
+ "V7f1tn4fv1FkS4fyBqVx+v35rYqjOQFoA9jnFjkx/qwqG9z3MW8D5/zvlaQ7iw8sy9Vki0J3\n" +
+ "E8ge+GvtMaklCAmLsU1OSi5VM46R7h8KlJ9FEnd/ti3QA7DHxrko0gsZXna+fBVGs/wx9dLp\n" +
+ "ZKIrJy35Hi1Jz6ScpFeX3yGT3qo5WKfmLzTxDpVbZ7O06+uidndAsEO6LIEC4s+iTrylvC+4\n" +
+ "RhFt4ECZ0uqP+aOmM/l69K1RLGEAtwvZeo2/3XDyTkEmpa3g9PZtuSrN5QIQk9YKK/JdrskG\n" +
+ "oj7VqUmy3UbWam1xXaPzOF0nU8loT7ibsscCdAp9ePrn8wAJONMOPfIrcOxe+itqALWDl9OS\n" +
+ "tmR+nbLdV/pxDarCeEJphYgNxgLdKwOpN3BlB1EemKkOSwedqBGupAsszVw7uuc15hfOY5z9\n" +
+ "TV6OdnbG8Ne4JML7Iy48hcFG5i1yNk7up0xzvIrqPPOmZiBR5+0d7b5oByfHZBUy6+19ok6K\n" +
+ "0q4bddPWpNIyEErsddXcEoL1Iic2zkAPYB/IbqSyKv5aub1M/kqwvluw4FzZ0dDpJHewrO9/\n" +
+ "8uWwRhlmgHSCTqkJpUx8U2GrmX+Rn992cBFkuKoV+KceuBxwLsg+uG5c1Ml+kam5V3PrjHez\n" +
+ "TY/DoV/VnM8froXvBEaTw3NtdaYMz81+O5wzuYN1D2YnkIbZqESXEstnNna0vezcsOiEmg8Y\n" +
+ "Z/47oz3vU7+g1YXqOtWn6lnxzDTWe5W3FaCtAE4NmMgfXjn0wnIHFEEADRmwGO9+ftxvx8Uy\n" +
+ "wAMQMy753rCu5IebsKpy+Fe5UQAUSy9Xa3OptkgG14EayOCvq6rAyGa5AQdeMKX6PMT+g7co\n" +
+ "hYSVwIXUXzv32q2nV+FpfXPC2DkfgeIlWCWFaaBsSkG6G66JA/IYojkfDJDXYyQV4bjSp6A5\n" +
+ "hI5EzIcajmF45shoBa4wBJ5NrwJx1Mfu7uqfjZCUhP52gD9vIcC3975ReTQIgVfngDwNkcok\n" +
+ "xP7WWPUt/Q+2ZlYEANNgm/XMSgEN63FPvAs62ljNcLp0YCuXpsztDLXsrDKoXkM6LjGhSkXd\n" +
+ "shDR1TQ2GvWb76YWicgbNq5j5FdmWK1GbxxpdzRtkVBaqyHOFB7gAlGYFtF8CXlrhXKxySRZ\n" +
+ "jHWCPbDD0MHSZudWn3tvOoVTxDKG5PA3AqYKl8PoY0vJgNlGR6UrJQ0kDqWyeCODuXkfOVdF\n" +
+ "9ID79DTrNVDMtoQq270z6JSjRXA4VUAaZVzFdqFhsproIY7McL8J4luVfwc2lBhcNwyt0g0r\n" +
+ "M+3zfWELP0e6OxuSt5bsvuB9VtXngtmu4mEXse+oeyiHmuF9kTlsUASB2kne4AsnhqyIklVI\n" +
+ "eaofT3+JgoX4Kpy6vesU7jmUmdDQ5C5d3ccQTLJlNHiFmwQkitO7cpYN+lsLYO57kiWWMjWd\n" +
+ "tL5pg/tOycjsMcqZ+DOPQhcWa7c7WaIJilsqqQA9jKEeurQAr2sxN83BZ0ej1HDEbwA5cpu2\n" +
+ "M1gvCUIgTifKL+EdKzKTleSnMhWSgPgQHGBrkbBXoR5S7XrqCkpCXwpKhwXRCBplzSy8AHuo\n" +
+ "36LK63ofUKWSnrtqQHlLeLGs9k9lSxq5sWELRLhi0vuVe70YSY756VRvsA3V+Rh3h9zOU3mJ\n" +
+ "ob+0WCmzMzrCEkb50qF1mO6nE07gcy2nZ2fZXPlNJLBfPF48kGzNmUUuRG3dRwvGvNt6foMK\n" +
+ "+140jw8Q+/YnwOXahNM23BpkUhvrRaYhLjIOC9ak+uMdIu5ZyjT7CgerH2SDoSOD8CuGYOuI\n" +
+ "Z6vjGBKxEX92UDClwgiIK/2YfgAIpGEAQOWCSRWitU6Jhex/SFi7aVYJ92Biw4wBtcHaHuJo\n" +
+ "4TSSRe67z+LA5X823HG6ibh1nR+u1BIFCgoPKRLpt6w6LUArJZqbYSNyCc/rCynkf5Wz8ZRk\n" +
+ "kC3rDVWmeWAtvLU9k6i1KOUk4vFtaePeGxTNolybo98cOYlj+JFs5mWR+ro6/n4Ryr/IgK0n\n" +
+ "lMvvbIiQM4ckslusg7JOimp+Qvo3hKdbbLLu9ezLZbX6xgT1H5e6Zif3lg8zpbESDCoZ+ZkA\n" +
+ "2X0Q84ofDRRH7beKV+IkG1uIvai+DVlFB1aWvCbV1N2YFX2kdVVYvKuiSxlt66kehSlKsyQg\n" +
+ "U3VnWezaEUhriQrN6u3uqjgAHj0GPhfbtXLNkF7cqb77SreTM1Mkxl/Tx0NHAmPnOvs6DJYO\n" +
+ "goG5W1ywekIpkmDBzXMeFTnjCaXDyBQgpsUklUASySUeJxV39Y2iehjJRiShgyFO1MGF48u2\n" +
+ "ZYZAUN8c1J87DLgy6+pZg5m9eZ/Y5Q1uIP0vnKYA13PmCEvlOdcqb8bgimSixNpWIm58GAc8\n" +
+ "EOtQFCkrwhGq66lDiVBEEhJi7nllTV1WBiZpU//mCqPwV12MYjX45UJlAogpQH6D9rJWEfaC\n" +
+ "xjYyxSpF63jOxkkpcrD89UehYm3bq4eDOGUBW4bFj86iEX0b5Ic3dxMVtK6F/fWGWb823+fF\n" +
+ "mcVKVXV4d8kFAOboGPlC6nJTX9hP6n3CcdHBpU7D4+yamKSPMN0oorOveTkNwofDWwT/xXKC\n" +
+ "Qszrxv56awpebYOByT7CrVnyT1WdsOafrt2r9g7DPUqJwBMPjuuipBNAb5syK90bNWxRwsRz\n" +
+ "+gKSzzg0clu3UfSWof0Kdffclc5FPKPICAcfoVFonUwS2FzmiKpfOI88xVJMv6MjxtxERgiM\n" +
+ "DuBRK/ebHX775Fq/acD6EWAbqN6fysPaBLAoQ0D7RRweEFY8ULWnnVT43OJPO4cP/oYYBIIg\n" +
+ "ANKPCZsvO0TH1Rt1Q7BPtwuOKTt+RBdeXSSF6K3FaLTJH1zCtsVyeRQIjCLZKcssRJy1FGcQ\n" +
+ "OdAnbNIZ68EkC79ESzQ5w/nmXZ85BQBcy5Kez5M0w0f3T2QxBsS7+meWyArZpL1WVJOq+Sca\n" +
+ "6vT+M2Vz09xhBd03Trzyiob/YhmS9UCqlbcGNN6Q61yBT4y0FegjC3Sn5ky7hIP/528rWr4n\n" +
+ "QjjWo2GtcLLoTXRjleIL7VsZPRJ/c5oyWlkwBMX91T3Ta7uhKh85YqChm+6wq++Ov0V9tbxQ\n" +
+ "3JcVjH0lQy0U5dvLWiefkM+AsAJMkKyas+PVuRgIuBFvasILF5dnachcwF7Uunun09hq62nK\n" +
+ "zh3Coy0jSEfcHU92BHoSLisAt/A/ufIMvyqjdLMHnLX6vsWEUj+0XhlqSgAnFED2ngk4sM5q\n" +
+ "/TEH7Z7E/COP9nwJc8HHpIAz50YUgoar77TKZXFYhbc3Zw4Onvl2dYqWOkoTV6qjQ8qOR2Km\n" +
+ "34kCm4PqhHwgJvkMLp7LLX7W+YIg9cqd/rygIxEf6NoIWkp+9DJFfuCMF2qeT8jRnaSHs0To\n" +
+ "OdIrFlUi+V7SGos6AP6R44gkeuXNyon2LD3DnABmmqyjKM9JtqWgxtn/vLNBgOwBcqIp0l3q\n" +
+ "3Znk4QKEIIMCNszdCSZUJwBW5CZDQ4F3ai6X2lxN9g4kmX3n5yKtLkjrWZMfCIwjU51cQYBo\n" +
+ "15Ue0N1+E8WDXHYZ9ahvXWo+dBMmX3bf3SWl+U9xwiljAzZ9DOi/ABrdZsMkVu2N/nm2h7iE\n" +
+ "tf0YqOhwwdenGCNhRJg6MMzoXQLvB68hHO1gZj+Wo6SYLr5DKqdHT/2aXLPiLQtFtWwcwASB\n" +
+ "HGOVqMzMTLa/uqwy9Qr7kF6hddargZwGBozYNCAT1TS/eqGNBm18y7a691qoDg2vzSuHiEAN\n" +
+ "piz/kWW1skxWHFlmdtVT8zHkxg88/h0DAeMiupXvfhr3DygVOih7onC+0L1wgvAOHc/Mcuoq\n" +
+ "qr7rTdm2tdl9sYz9gGKcprVQJ94HW9Io+PJZi/cG1x483V+1K2mWcSDPUImRXNGx/VoSfaJR\n" +
+ "DlMVvNuhBYuVBvjrkx6XDndJPZSjzEDQliwV4rLb8VJJ70faOkI5WKcqesjYzebxqT5J2f48\n" +
+ "KLuZ9rjiW67e3fg8UW5RQyIjJwGwIglPKy+CEuBQaJCZxpzMZ3yzbttiDCkV0NlzuT9exdTr\n" +
+ "ExDsZg6kiZPivYQdQDOJBhhY3LpwZHr7FCjN/QhjC01W5fmbQNTanieL3IU0GjopnjvI748q\n" +
+ "cV2GFVpHWZECq2xGnPBTMfTRkmSQ8WlGcVJ/nMku+Ww0iN4UsWk/m/7ij3JK+KcgVtMHPJxG\n" +
+ "7VbxfnAzdHTSD1K3t1wI3NA5A0Imwd3erO1LPVuMw82PsjM0hUdR4Dtvd/GsCFLlCuUwePBU\n" +
+ "k2ZucLdRPWwdJT/Fy4rX+qbxhfRmvtFw12MzDcLS7sKYvGXIMd129Os6xv8h4Wk6Gag0TrnS\n" +
+ "74sdi8PWo4oK+pDTu8Cb3wZRrTEq9af9XQkFXIWCw8YHgCivEL71XUBAQGPhMF+sdifsgo6I\n" +
+ "ziwheYUtH9pXq2Jo6i4YvfedhJlPssmGento9OHGWTywi3ZHWbAc1h9A4kTivCh2zPO9ee5D\n" +
+ "VTvZuhIbhdA86G0lI+sQRCnLyLjZe7oeK0yOJDzMLUztNHFQhQ+kqQkZvt+bRqvcOYeWn9BI\n" +
+ "d3X+HdyutBWhhMSoknuEByDiK9zo30Wf1yOYt2xqb5p4fvSbFgkP3beD7jcznuEw6TNVApcc\n" +
+ "JpzyqITwXnZo+mJ38CCrPMYEr6RxNTA+XH4uav4BqrSoD5k1IrKqcXUOVoyUk1GOb/fcqUdl\n" +
+ "XJzCb3B0tCKM2qdBrtn7qUc/a7RhzNSwlousUe1OvMeZR3POIQecOjHVH7nD/ihlfhnTc5zl\n" +
+ "s03/ydPiCl5MIHNkaD8ZcIL9s+ejs+g1Mj8r15srXIqW4Q9HXYnwTyWmsTRtKKSKId3IHTmx\n" +
+ "tGycpLLhdZRuUao+lmzvwN6j4C2q3sgiISqnT3Qnti/8ZQxtaJ5yfu8tmGqX9kNlJA5JSzew\n" +
+ "CiEFf2LtG7ZPWoHrleY3zhLrMwbPWdKENohfZuCsZGmhqiqmO0FcOy2NosX3pUjiMrVet/RS\n" +
+ "x24k4Cec2xA6cThnuzBJ5TKxdclLIoNj9tNMsH2sUUEfIY0JcSLntHkdd2S6cb9NyWDCYi4W\n" +
+ "30+ibNY+RYug4Z3AjBMSUqdiKPLO+seP02kHiKm3IzVMQ1zg1abC3dMUgBfxOVOOqHcPaJ0K\n" +
+ "6/hQYhH9CxWggGF5R1yB5Rq6mHw5eD/nnUINjIc8D/dkO0j3hDpOLbtpeAW/O+3RUlAewO+K\n" +
+ "Hqy4B4WkvVTD4estRV8sl0R9hJpSMfXtlGXjkcTujVLcG5XVnzooNYr5QOHoiwS33Lk2aswV\n" +
+ "07ZFgzADntGdBV1oWlX4bEvH4Uhw0UQ3WfSu9Ejv5Lea+Ttp3ygktMGPcrAb1GMUlYBK0twr\n" +
+ "smvHaPAvW0YWN1yFsXEYC8Uhked7n/9IiYBQr1ddUVhjFPYgt9Wb7pbqj0ZXWacLsh8rybGX\n" +
+ "JhsIOKC1Q3ELNoSQU4XR6G3Iq5+sq0YF3R5doJVeqYK4ui4U4uvoqIyOIfAD+Fkd2B5ZedA9\n" +
+ "wR67vjxlsfXISLA5KGFnnFKuAO+k7XcxD5uScCPqz/7WLUl3qSZkL6FdRfJ5hDBh4OSmeqR3\n" +
+ "OkLz4x1PRUjcpcXYhSvnNmsjz88+xZE+uaUASTchKhj3GNvV8tRfXDkgeKOjFnrTCc9ti1vl\n" +
+ "hVfUhFBtVgXcZ6yTWhLTjkZxU6oK3Or1jNfGJ3+8OSGYfRIuSFT0xgi/IND8UYCg2wJVKjhb\n" +
+ "Ysah6CnUeQJuhWlAeHX0avGczd1wuVZVaxbHxtOiwi4IS/qYzTU8R0tFjT6sOOKkC8V1gHca\n" +
+ "AY8DS4uThZj8NZrKCQNRjxjLZvd4O9BqVM4zoVIc0/MumfKAzpj51QXtWsfeL6aUwLcjli4E\n" +
+ "cfk0h3FG0PXw6xmZQMZqRNbVDXydziMXg0tpwHBg9b3zTl5d10DGnMT0mkeVl5j+PhUO0Mmt\n" +
+ "sCXDiaZDVxFXwAIkSz6/5pdn2Iom+8GUe0qCctrEkL6T5hequlQsZAIw4VExd0FdW5zt1lnB\n" +
+ "cBFmUofzV36LG2BHqLXYj9FU+pUiMiOOlP3kPtvFwDmOIMBDHAsUJOIHUX4LHUjR/tAz5+Vn\n" +
+ "cPQVkqqUrps9sQ+syXLHrAPO7qZdRuRyLwjAxARhJozT1rOl39Qv2SnwK/OqP3UzTbA41U5q\n" +
+ "4zcveXZc+C/4zlufru36fLdMtzwnKnumewnUBdGF930V5aD1qsU4UAp/mDnnFZd3yN86ofWM\n" +
+ "fwcX80kZptrl2nxK5Zx3q/u5cPC0uFbbptHHYsPO+AGL5oPo+D6aJXbFh5BFT+od6+f3QFae\n" +
+ "icjnHtPnglfHlDMNCu3pjXDrCX1MpZKaNkfk2mL0rC5kXyOhsbSlZAjb74Xu76VZSIXQ7ad2\n" +
+ "P+c+67bVuG2/eTsiXwGjz/VxTfuQzdORdT6g8IFK9LxYsmeAO6dn9eoKGl4I1V6Dpwa5eLyo\n" +
+ "m6Y7Zi6h3Xe/1y7QQqsdtVRuc6HTSDnS912YqeAMCY1dBuRmRNlnBsVJvpJANAU6l937R52F\n" +
+ "yKZ82C3le/OAPYwJFy7KRpp0OyEwU+DLt7jE47Y5gA+pXJkBNBw2MJoGIKOv8CIXCEg0BPx6\n" +
+ "t7YVvs/H/qKkNLN+2Z1V+u8STlJEq+S2u5jGDBgsJ1JfrXu+difolZkLM32c8TgyplwlPtB0\n" +
+ "uQ6g56Z2Wn0hIcznfpHJLjsAAymRqa6ymEEG+RYGfG//pCyl2IBfx0tNJrdVDLdR0bkSzSEx\n" +
+ "Zmqs3mo05YW08fIaguvZI5TwShuj4VECmS19hvx++dRzg4KPB0nwbUcWXZ4cpXMCGrm37NbR\n" +
+ "wvHfaRByNmVF8e33H9lkMibyT0HMCc29twvMf3EMFSbdKvWlxt3P9hckos4nPKhzhxMKK+x1\n" +
+ "Sk8u9iV2IyNNaTKXhWn/6QdN8+yT8ALuuDyfNmvrnASD6xX1W/qA9i1KFf2ae8S0Y6YeJlgJ\n" +
+ "jq74MN02Z2DD+2EI05a/+fTPDtfnnQ7QZZKcANxXqmhjBBswQpi8fzvG+Zl+CESkJeMnNCaF\n" +
+ "9Pk0DclEeULVWNLZlsPBdfk3zvjCAVPm3L6MoxSKMb40cI5ICmTGpyfouof2YwxW/rvR1P8O\n" +
+ "2Ekkto5tesyxuFLjEkzmMbqDsVe+XBFzTXB+juygxPgkxtEmvw0EAzXp427BmLsPxBdKkA9s\n" +
+ "e5J6xGHkA+3hmOKTWizloRDR+fEQY18QkHp0oj8LYM9mlFhtVu2bbvd9AMJA0F9ypNCDsBOk\n" +
+ "Q9oBsecvfJ4W42uWXsE4jtUCthiJCljRVrNCqT8LS+wohHLKKhr7Ka70umS4PVzYHuvPE97t\n" +
+ "KmfhSJx+oO3yW9feaJQA668/Qc0lyJZ91flDEcKyKoHSWH7gFewPBwuSsU+Tk5wqI15PWf1X\n" +
+ "4lI1mOPaYg31zgckJSvh4YOME3+HTwNAun8gU8h73bMeAO8l6Fu7ijnyin+zBfNCjMgm+tWp\n" +
+ "zvecGCpqbIgbPEMYWqRbo0hvvO6BVQTNWeIMdZY3iZD4PHdHLI76Cuk3jbtvDf44k5m8e6mx\n" +
+ "w+MYOC1R7ep5STedsvmjdW5Fhs0W3oKl9OXk6DkRKFkhhEIZo7LN5KsjIzjkF1Lwj+nzJ3PH\n" +
+ "aPfPq3IAdI25DyKkeZFglrMGDNS8zlNkHzlYohNfBwi15aqwWnT+Us9KogYjpQFqYHt235aI\n" +
+ "JjqPPrQx12lh90DZSuBv00dRsT6nJ6lpAApSj1zfiOUscfc5SgJTV2/WtwmslYy4dQLQMoX3\n" +
+ "JaQwhMlp8ymkKSmTbsLGqJg0PraxzfnpNAsuH/rmr0vmsCVfePbf9ioKlvxJAzqtJ349dSOX\n" +
+ "RgFHcjSuMP/oGV5sL5Q9hKVQ+Lu+iavwR7FP8RVrK+hUzihdeLRueDbCN52UTCkdSrHkynog\n" +
+ "NMKgF/ISuQA3l28NSGdRnpCcKXH7YGbEkpoAd1JoxwtKkoPKsdZaezInM28lFvfY4EIgyeS6\n" +
+ "ec4z6bghHgQIj9CygiWsVeZWm2f8MWf/jHUcp9vZqI0t0hfHtCrsZAWbb68np+3lHhM8CiqA\n" +
+ "KDbW8M3BKeMCWNGMyadYADu+sX3MFjbgHnDncDCfNl1ZhWmtifpIPzjVkmWjwyEgYsSdC7yT\n" +
+ "L09tPrH72poWJRkoWjr0vWqHuqH2dYgUyUw3j63+7Cq6gYcG3ZUvdD8LTVfP6wj53m6GW4/f\n" +
+ "P3la/iS9jSU514dR7/ZsZvxmyxMg7ebjfiyh3oHUJQnJj8xG0gmiys2q7wL+xuKuSZSbOPsy\n" +
+ "z37Rve4xzlSR9ZltIjRx3oL0c+c2VQ2xs1+cIOhiU1udENvjseivKeFOGj+uUrJlCBXIGZep\n" +
+ "gMd6pTHNyLCuwkEnYKc3vxxWe2yeaZxQrBqfI690jq9uGRvmA9JQK9CnSAP9524300JhaGhF\n" +
+ "Mg4J8YmoSv9+gCsbsq90uAiLSrIkeIRpGmg3TAayntJ1lOXnSDZhZAJh3CTk7T8E3zJS+GG6\n" +
+ "mbAvcvo7WRL880W00ZOBtZBEhM7dkIxyqib9zn41SGyAWZAVy6g2G1aRnbz4G+edfwQ1H5jf\n" +
+ "iNGL6KTMioItA8ZpJQQ12aXTqylFi+5wT2N+pdUqBurQWoCnLhY2O1irbIfCUIwnDk5D3a7/\n" +
+ "ySgtiotJLjGkEL+dMcLGqZOo8G1yw0kbjo+iy0mM6MkmM03fTw5KNxjl26UpkjK1Il7vHhtZ\n" +
+ "SN7IqnMQ1gRIYjyIBkS8TRG1z4T/w4Lrh0fqAvy0etZj0Gv38XrlpSI03YrADGD0rI1Z+VZE\n" +
+ "VF0viG+iea7DHg4sP6AtKaHajZcUlinE2/pq9VD03enHxHBqcpo3v34VFlwBVgUfa1Bx5qky\n" +
+ "pOyqJ5XMBjjCcF3UT2GiQ2HigmyFbC8Wx0gAYy7BEmgBVhfqZAUeicrSlY/8hm300j1kkXS1\n" +
+ "QajIqPPWCM9BNqkN1fmsVnL7Npevg8h9zVKoczQH9lZmqnDzW/qckwu4McV8m75LRTNP+ADJ\n" +
+ "KDHREz2Y4VMjGjYoY/xJLwCOZyWd05yqQRiX1ijLjPQtA1BnWVvmtSY7l+V1V+jtjSMwQmNq\n" +
+ "AGirprCNyOBWfUv+lMvxAscCsdMwSWb48bFEQqGW+6onPYHi3QdtJ73U8yGYqCdX2z8Ri5OB\n" +
+ "ceLq8oe11qIIad9IOoklGwE0tvcxl8tZ+uDKV3t/sqCmrFV1/eYdTwaHn0tDVbdjwp78ZvXX\n" +
+ "7r2RgU5ePyP37f05wOgZTfD97KqO2l8oERl6SO9FgsPD7MZqA5MOL9CwLa9kFDe3PRW0OTlm\n" +
+ "5LbCWRNPR+X6qyd7zhWfOsjdyhRa9QYa82q4IOyeUtMHRiy4n9vRSzKVdGlCxbHxkoL8gVzC\n" +
+ "cjhKgnOn1xbMVD9TOPRS4ywu65DiEz3yH5ZUQdeUcxNAIfsyIJF4uLilBctx4QfRg2yk7mYZ\n" +
+ "HN/b5yrCzLIEGT50jbRqQVLdg38ZtnlG/BrvnsQesWqyfw8HQsZgY3Tr50TGxvqBIZpn+ywu\n" +
+ "UADKWcFnnxqggFmlEY/Cnove/yW/6AdZceiq6paNaW2eZY6PKSOgE6LaitOiHw1PCjUCjXVV\n" +
+ "3wts+LSjrFMf4x4QiruEXXu+V5VjX7jatDE+ko9Uz6IU0BkTHi8dublk7fgMq7UopTF/xsnR\n" +
+ "Vwv58q8+YnL+21cF+NQYj2QRPZ+s+xdIcGccrseOILXodpFbVPDGPqkKGIz8qb4STNtM5G4g\n" +
+ "qRTy/lh+oX/8tOll7q2EIYxkUBMUmeFA22S6lmCisiBzwtJT2P6571POxVvG75CvX+6YDUyt\n" +
+ "27K10jYStSrweUrNIO/KjrJ/yb9nSWOaLrni0y/42fa6L1YN9kg3VM4BCMhz9hO9N6gk1jiG\n" +
+ "aSrD6St1f91OLCoYuxjq8aKeo3uRS1mNXdpePIStKTd8ebEE9HMbYCgN9bdnTkmA7KhvQmKU\n" +
+ "5coukIyfFzgST1zngRoNU5HOJTlrb+eMgzdJqciTP64dEPtq5s39kVl18Ks8KsmUiR/eQnqZ\n" +
+ "7wESvxQEPH6JTojnzUrVctzuMWSiAU3o8EK6t7LjyZovKg1Ve1W6FKLerv6PZ+Jbmns1XjqU\n" +
+ "7pJ9pZNoCcjwWPXSx0M97cdEtrcxVMgxB05LEWIyJblPA3flZLpEOVwSAHuJyWXz4PRKJW1k\n" +
+ "56/uSuEAI0DHtfonYz9LQ5zTlmmYAneJTSGU6PZrzzuvGrHegSSZkPRtfVH8C4RmNqUK7iWT\n" +
+ "2MPaHzCAYGXa0AdX98pTh3uPx4LNuL1TrVTrofpypSbatuMVae4588PrAnCcW8yES1wG5Zvz\n" +
+ "tVCzdciw6bZ14dr6EDn9YBDcgqBeFsUpPS8zuQhQeU7/repqjRB8p4KhuZvSb8bMbACYcQhy\n" +
+ "cPvvinsGYHQJ9lrTgEhJetocgrJJxqe1OKnu10uWDg2h/sgaibl0jTmvaQ7Y9FwTZ5NUbNMG\n" +
+ "EeqvVnzsnLsgTAksIRzziIp4ZHFS3INDy+S6VvIDnca/mGTjwlkdgjC75kyihAsmdExopl9R\n" +
+ "W1awVUnXFQXaN7GQYwGApOwZV7VAUxIcfuy+TJjZYg1Fac9mI8RtsipJpfxZ7ZvKKBD7liQl\n" +
+ "kkWUxMdkkYkj0mtIG5Xpiswj/S0gl9wNxzvUNA1tZ/1zXuAmOtL1qGcJF6VBN8+sVS4vxoDb\n" +
+ "xPlWpgVJpZcnuX7qAuehsi70r/51aGCVfcEdVFapntsY8h5X0bbp4F9IcvlUaOqXrMc9IXoq\n" +
+ "nG0jrQ3rLGlYZo2i2gIMBdUCDZkLTlQ/0FGTF2nQi0htDC4cjo++c/y/PVrZ4UPcQdPJaOcE\n" +
+ "z/csRks02TriAc9dspI8dzF7/6qXUgUFPUEcRWv+hS5j2VL3zKlIhRQ14dDS8tHnAUrmoEoK\n" +
+ "YP7GrC/3L7YRaYbom5OrMhz/waZCIz/ZjSwxfd3LvrMMA8fuAKTu1t4qXGdZ7ocYMIQPLxMg\n" +
+ "pUe51B9xwnyiU7Ky0sPBX8s7KDItEO+YXJM32fwB7egddz5qzO09SskraOGloWab8nY0YX0k\n" +
+ "PDtmXUT5J+uzERFrfoZmpnta4qJoE1SKGyS/4L7+30mRrSaD2o+sUyWd+kP6pG23PiGUtqcf\n" +
+ "iQMSoGwehTfSC3cG5XyVbfkXVOukIA+jh5ysABs3KotOPikgVQJCYDS+JnTCkOZKrf0DwRfL\n" +
+ "/roKcv2/ON7W1o25W+QN2yIhNbN88Rjt/5twSB/SDFePFmItzinkibv5y+GZxr9HEaKZ/1jy\n" +
+ "myG1cI8gIUj/nihfEQ/WEjWSyJsO4smvu2Uf7ZN4zTSk/QRukyVrmoOq4dKzStiWssF980Ho\n" +
+ "LCbKzCJFcy13so3MqDtORxtDSp+960XWMOyVHZGUKRgWIKEX2AbJpaEvwCYKdeYAZzIuCwO9\n" +
+ "O1ixtfv1KvzMzCjF4Sk9mucFcVWeEeX7Uw5DDCZ01t8uXxiFkjh+bDal39a7NF+VNDiaAmrZ\n" +
+ "ezysTyjA9h7Su9uizTyeK/eZ+w8hDn1wUe9CgcCGz8PUJxOv14qcUNpZussP0hQVErIWPtH3\n" +
+ "mPRoClx/o+AGTVtLRCR7Fjy6n6Zq8SyGDKf/0xa1QufjHmoqumtnx7tNCsS748+Ys6PFwlVv\n" +
+ "OXcpimPVisL1kfrQHDdBodXnKNZW3rWNtFGYBP1VPVUxqJJ283WRyb/X9bVGx1gPaHzFQj7w\n" +
+ "z7BVhMpPmBt/vAXdIvDGsQGb/16O52bucATPWyOQMLUBVorR07v/5NolSzHYi19ehveceUHb\n" +
+ "omX5uAPXqSR6DvUFbG/n+rOTsYCTlqNIiF4Dm3iAjizMMh15MWvV0+PHyLLl8xcYze9Eb5eO\n" +
+ "5dMRERZCNejwJ6PfFKS46XkfmLCZbWbj30CU6Qb6reC4v6sucId63+TghD3CmbyQ8MZwjB7P\n" +
+ "e4gm1deP0Gw4EqvQvsi7Kq8WQii8OYLO0HSKyF8jzfb8JnIe57A0mge5Ru0KaC8blGuO0e5V\n" +
+ "Mm1SQDXuHJC1dErv4jd3+9Yj4TFgRYumplQR24jYCpC4OyFqcPEBJleAdOkrb775sa48rUqM\n" +
+ "F9dZjP+1MJNhEhgv7g/LLUUuKwwEa6o/Ksbvx6fNikWX+40EyS/wvKzpTZ1vsVxIHMmae2C/\n" +
+ "x3C5YVFN20PUc9VsXjQrFw2T7Q3rtqWgXRgFJrcoY7NDyvJY0UyCBqcWp28N6MKOhXztA6mG\n" +
+ "kS3gZXkpKE+q7yJCByjKLs6D1vTQgj241bswl8KxjlQLw+iC2VYAtZoC4O2BWyD4K9rL7H/I\n" +
+ "i6/ppxf6ofzNv5JHZ/7if19WP/n5XlX9XuMX0ZPU15nRXpapt1hOtT1ER1+bzifyOsywoKYE\n" +
+ "12IgKsb+/LK4k9KlKP+93S+yoFtSWKfunvA0Lyb3Js8h8OY8Kq1Izzw4UjO5npX/uYq18VDr\n" +
+ "LzU6q8d1IX7rYhgrWOmARkzzFbKQ7V9FGyxpbp23Fp4y/GY/F1wnZGRk9CUgdI7ZXIp+pR3R\n" +
+ "eVSECRmLpMEAgKwcoVY5SE7mZYMEh5W3T4GUKgZhIuZPL/8/OoX3HivC2q/A+BMxbJCBL/mx\n" +
+ "bsRTpRvPK8cJgf4QxgE5ylWDpamz52nuNNHxxl+Z7vm0MZ3I7Gj6Fc1pHd+ZhCCjKmIlAseq\n" +
+ "Lofz0yafH0NPd5F8T9onl9Of3ekLWjHSZqNKZVRgLqGPxcM4QMQSQ8vsme/bPABmhrMKZ1YG\n" +
+ "VBpYtJzVTnG55m3r8sFPmPNZ5tdLiLFj5WQvClnVE1Q/eU/1iOjVin9tIfDSf5O+/x6PYs4O\n" +
+ "wHayiyxiAurfPEqlzFcnPznn7R3r+L0mcwGu6YWNOduY3TwoF2NyHnaVzVQrmOQgCE0yD56X\n" +
+ "z2Ur7HwoNYi7Nzfz0CBV2gN3PLGdiYn7J6wyBa5zR5jDhMF0W+oza/+tZFPVjo73PRT4DYc3\n" +
+ "HEHjkHzDJETGdzOHPuQyTIyZGxx2BIyeReQn6oOIREtIub+Ct3KWd7CnW5wMbFaXEYHCagfj\n" +
+ "5/rl0uGBS4KsYrMEXvGCHlJcVDmelx80rBN+Vo6Yrkj8B1LOEgM1lR+9LOBXPrhf9+PRZ4Qp\n" +
+ "9HuoO7Z6x3R4JgERXrEmZUJVj/JVIaF0fjylTj88257mCRWxyO5YpfhBF2s0qLYdMvNKUAzM\n" +
+ "/NjP8oODt3TU0Xt30RTu06amfY6ZnWTX9uzaOffi2BrWrcib97frCqPdOEdPhRcqniIL5U1k\n" +
+ "YnVrSSDUQolLqs32MGdaRCkpGF1lN7YQCBRNXFf0f2KvzC4svuDfsqEnTL7R2Vcu+akAshXQ\n" +
+ "sSjEXmKd49Ky7sZZnEbmfnDaB/+4ZWAYk4gajJib5ewB2pHzp5muKInTyECgYMc2ReVD/tz/\n" +
+ "pd9NG1NzdStchS082PL8DwRJx6HvWjVo9sSd9DGIfVonx6txQ34QCTF9psQ2R0Q1LRtJLyZd\n" +
+ "Dgehsti4GCBZdAQ8dT+2sG4QxKTHwaHCp2mDNWI86eaoZ5q4m0UG+kJrKZM+bbZARbBS5Go6\n" +
+ "8cK2JiiRh465rPEh+CuWJQT12Whk89nbe5nvq+ILez8iYVj3IiBwy2FAVtidPIEgYfVI5TWD\n" +
+ "ElfmXJXR7r2bMCY7RfLR6u8JWLJEoHEGsB2DqUwWixPRaxMbvGMm6t3nwbnhqgeJFNpW+Ntc\n" +
+ "PpZF5XSFj0xTqr6M6alUd26vC7CuXc/MDe29raHZ2k95R8zEnfl/p0HiErPGGA0rA6HmY1L4\n" +
+ "m2yc4wmPwg0cW1m8T9U4bPQaXWQ19wOqrBFety/T+m+3Y/L8aGoHmQlNJpqzbw/DmorcmyjB\n" +
+ "B3EHg5pmn+AwxuQOjTolFP+mmW593LEdkuBpITZHa9mLl5Q2ts3ABIIB4IUmz0F0Z0EOYEjt\n" +
+ "lKryNdgwXjLJF5zLjcs+Rn7FuD9LWs9FRKS8hTYFxGwcnMJLbFgWobGIK8VwXUlhiuj4dlOH\n" +
+ "Llq6eerJUz96gUR5dY0pjci+uVhF9Pr0uJKeGCHJLluqJ8hvE6r0qyXJquWdMgFU480YKlAB\n" +
+ "5XXxVI2geOurRMSSoUXKOk/ZR3i41orN7/gZQPZXvZbNPSVNifbJnqhi0qy9nBsiEtV05tQ4\n" +
+ "kCBnnQmAlNgq//AnuN1H+UNjHxUvtU80yBZMsfbz0BZ6MWF/AlTXEwNnBTXpQI9hYus83AR9\n" +
+ "lht+11eNmwTEVj9VGQVk1S0OTCWe9Gv3mxrPyFGhOJ8vFtBDhpVjSZ5cFCPhGMCZxjrIbzf4\n" +
+ "xjz4fPdSnN3XpBRxuE0FW39coYHX4jNn2FhKtOljHUZjrFL91ZYYo2xdou7VgE7GfVvb7V70\n" +
+ "MiK0OsW8du1c8Iawqmb0H1cWo/GCA8TaFdjfXOWZjEfHpXJvGqW+zcYn2DN0UNYnuP4ITOd4\n" +
+ "A3OQiTaX1XV4M+vKOR1A0OzFty0IxMxcTEwSQM1JQ+zpE11DBMWf4JEo35uAmtvHXPjlyHd2\n" +
+ "YY0ohoV70z8CGMrBN6ws5zIE7n3q7klEWHds5PZMDlzoPZd2rwQIYAM4FwEheYIAAAAAAAAA\n" +
+ "AAAA";
diff --git a/comm/mailnews/db/gloda/test/unit/test_startup_offline.js b/comm/mailnews/db/gloda/test/unit/test_startup_offline.js
new file mode 100644
index 0000000000..d7ba435d76
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/test_startup_offline.js
@@ -0,0 +1,53 @@
+/* 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 {
+ assertExpectedMessagesIndexed,
+ glodaTestHelperInitialize,
+ waitForGlodaIndexer,
+} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm");
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+/*
+ * Test gloda starts up with indexing suppressed when offline at startup.
+ */
+
+var messageInjection;
+
+add_setup(async function () {
+ // We must do this before the first load otherwise gloda is started without
+ // picking up the necessary initialisation.
+ Services.io.manageOfflineStatus = false;
+ Services.io.offline = true;
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+ glodaTestHelperInitialize(messageInjection);
+});
+
+/**
+ * Make sure that if we have to reparse a local folder we do not hang or
+ * anything. (We had a regression where we would hang.)
+ */
+add_task(async function test_gloda_offline_startup() {
+ // Set up a folder for indexing and check the message doesn't get indexed.
+ let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [
+ { count: 1 },
+ ]);
+
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([]));
+
+ // Now go online...
+ Services.io.offline = false;
+
+ // ...and check we have done the indexing and indexed the message.
+ await waitForGlodaIndexer();
+ Assert.ok(...assertExpectedMessagesIndexed([msgSet]));
+});
diff --git a/comm/mailnews/db/gloda/test/unit/xpcshell.ini b/comm/mailnews/db/gloda/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..4efbf97583
--- /dev/null
+++ b/comm/mailnews/db/gloda/test/unit/xpcshell.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+head = head_gloda.js
+tail =
+support-files = base_*.js resources/*
+prefs =
+ gloda.loglevel=Debug
+
+[test_corrupt_database.js]
+[test_folder_logic.js]
+[test_fts3_tokenizer.js]
+[test_gloda_content_imap_offline.js]
+[test_gloda_content_local.js]
+[test_index_addressbook.js]
+[test_index_bad_messages.js]
+[test_index_compaction.js]
+[test_index_junk_imap_offline.js]
+[test_index_junk_imap_online.js]
+[test_index_junk_local.js]
+[test_index_messages_imap_offline.js]
+[test_index_messages_imap_online.js]
+[test_index_messages_imap_online_to_offline.js]
+[test_index_messages_local.js]
+[test_index_sweep_folder.js]
+[test_intl.js]
+[test_migration.js]
+[test_mime_attachments_size.js]
+[test_mime_emitter.js]
+[test_msg_search.js]
+[test_noun_mimetype.js]
+[test_nuke_migration.js]
+[test_nuke_migration_from_future.js]
+[test_query_core.js]
+[test_query_messages_imap_offline.js]
+[test_query_messages_imap_online.js]
+[test_query_messages_imap_online_to_offline.js]
+[test_query_messages_local.js]
+[test_smime_mimemsg_representation.js]
+[test_startup_offline.js]
diff --git a/comm/mailnews/db/mork/components.conf b/comm/mailnews/db/mork/components.conf
new file mode 100644
index 0000000000..edf2f0d382
--- /dev/null
+++ b/comm/mailnews/db/mork/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{36d90300-27f5-11d3-8d74-00805f8a6617}",
+ "contract_ids": ["@mozilla.org/db/mork;1"],
+ "type": "nsMorkFactoryService",
+ "headers": ["/comm/mailnews/db/mork/nsMorkFactory.h"],
+ },
+]
diff --git a/comm/mailnews/db/mork/mdb.h b/comm/mailnews/db/mork/mdb.h
new file mode 100644
index 0000000000..2f0f0d80e1
--- /dev/null
+++ b/comm/mailnews/db/mork/mdb.h
@@ -0,0 +1,2550 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Blake Ross (blake@blakeross.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MDB_
+#define _MDB_ 1
+
+#include "mozilla/Path.h"
+#include "nscore.h"
+#include "nsISupports.h"
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// { %%%%% begin scalar typedefs %%%%%
+typedef unsigned char mdb_u1; // make sure this is one byte
+typedef unsigned short mdb_u2; // make sure this is two bytes
+typedef short mdb_i2; // make sure this is two bytes
+typedef uint32_t mdb_u4; // make sure this is four bytes
+typedef int32_t mdb_i4; // make sure this is four bytes
+typedef PRWord mdb_ip; // make sure sizeof(mdb_ip) == sizeof(void*)
+
+typedef mdb_u1 mdb_bool; // unsigned byte with zero=false, nonzero=true
+
+/* canonical boolean constants provided only for code clarity: */
+#define mdbBool_kTrue ((mdb_bool)1) /* actually any nonzero means true */
+#define mdbBool_kFalse ((mdb_bool)0) /* only zero means false */
+
+typedef mdb_u4 mdb_id; // unsigned object identity in a scope
+typedef mdb_id mdb_rid; // unsigned row identity inside scope
+typedef mdb_id mdb_tid; // unsigned table identity inside scope
+typedef mdb_u4 mdb_token; // unsigned token for atomized string
+typedef mdb_token mdb_scope; // token used to id scope for rows
+typedef mdb_token mdb_kind; // token used to id kind for tables
+typedef mdb_token mdb_column; // token used to id columns for rows
+typedef mdb_token mdb_cscode; // token used to id charset names
+typedef mdb_u4 mdb_seed; // unsigned collection change counter
+typedef mdb_u4 mdb_count; // unsigned collection member count
+typedef mdb_u4 mdb_size; // unsigned physical media size
+typedef mdb_u4 mdb_fill; // unsigned logical content size
+typedef mdb_u4 mdb_more; // more available bytes for larger buffer
+
+typedef mdb_u2 mork_uses; // 2-byte strong uses count
+typedef mdb_u2 mork_refs; // 2-byte actual reference count
+
+#define mdbId_kNone ((mdb_id)-1) /* never a valid Mork object ID */
+
+typedef mdb_u4 mdb_percent; // 0..100, with values >100 same as 100
+
+typedef mdb_u1 mdb_priority; // 0..9, for a total of ten different values
+
+// sequence position is signed; negative is useful to mean "before first":
+typedef mdb_i4 mdb_pos; // signed zero-based ordinal collection position
+
+#define mdbPos_kBeforeFirst ((mdb_pos)-1) /* any negative is before zero */
+
+// order is also signed, so we can use three states for comparison order:
+typedef mdb_i4 mdb_order; // neg:lessthan, zero:equalto, pos:greaterthan
+
+typedef mdb_order (*mdbAny_Order)(const void* inA, const void* inB,
+ const void* inClosure);
+
+// } %%%%% end scalar typedefs %%%%%
+
+// { %%%%% begin C structs %%%%%
+
+#ifndef mdbScopeStringSet_typedef
+typedef struct mdbScopeStringSet mdbScopeStringSet;
+# define mdbScopeStringSet_typedef 1
+#endif
+
+/*| mdbScopeStringSet: a set of null-terminated C strings that enumerate some
+**| names of row scopes, so that row scopes intended for use by an application
+**| can be declared by an app when trying to open or create a database file.
+**| (We use strings and not tokens because we cannot know the tokens for any
+**| particular db without having first opened the db.) The goal is to inform
+**| a db runtime that scopes not appearing in this list can be given relatively
+**| short shrift in runtime representation, with the expectation that other
+**| scopes will not actually be used. However, a db should still be prepared
+**| to handle accessing row scopes not in this list, rather than raising errors.
+**| But it could be quite expensive to access a row scope not on the list.
+**| Note a zero count for the string set means no such string set is being
+**| specified, and that a db should handle all row scopes efficiently.
+**| (It does NOT mean an app plans to use no content whatsoever.)
+|*/
+#ifndef mdbScopeStringSet_struct
+# define mdbScopeStringSet_struct 1
+struct mdbScopeStringSet { // vector of scopes for use in db opening policy
+ // when mScopeStringSet_Count is zero, this means no scope constraints
+ mdb_count mScopeStringSet_Count; // number of strings in vector below
+ const char** mScopeStringSet_Strings; // null-ended ascii scope strings
+};
+#endif /*mdbScopeStringSet_struct*/
+
+#ifndef mdbOpenPolicy_typedef
+typedef struct mdbOpenPolicy mdbOpenPolicy;
+# define mdbOpenPolicy_typedef 1
+#endif
+
+#ifndef mdbOpenPolicy_struct
+# define mdbOpenPolicy_struct 1
+struct mdbOpenPolicy { // policies affecting db usage for ports and stores
+ mdbScopeStringSet mOpenPolicy_ScopePlan; // predeclare scope usage plan
+ mdb_bool mOpenPolicy_MaxLazy; // nonzero: do least work
+ mdb_bool mOpenPolicy_MinMemory; // nonzero: use least memory
+};
+#endif /*mdbOpenPolicy_struct*/
+
+#ifndef mdbTokenSet_typedef
+typedef struct mdbTokenSet mdbTokenSet;
+# define mdbTokenSet_typedef 1
+#endif
+
+#ifndef mdbTokenSet_struct
+# define mdbTokenSet_struct 1
+struct mdbTokenSet { // array for a set of tokens, and actual slots used
+ mdb_count mTokenSet_Count; // number of token slots in the array
+ mdb_fill mTokenSet_Fill; // the subset of count slots actually used
+ mdb_more mTokenSet_More; // more tokens available for bigger array
+ mdb_token* mTokenSet_Tokens; // array of count mdb_token instances
+};
+#endif /*mdbTokenSet_struct*/
+
+#ifndef mdbUsagePolicy_typedef
+typedef struct mdbUsagePolicy mdbUsagePolicy;
+# define mdbUsagePolicy_typedef 1
+#endif
+
+/*| mdbUsagePolicy: another version of mdbOpenPolicy which uses tokens instead
+**| of scope strings, because usage policies can be constructed for use with a
+**| db that is already open, while an open policy must be constructed before a
+**| db has yet been opened.
+|*/
+#ifndef mdbUsagePolicy_struct
+# define mdbUsagePolicy_struct 1
+struct mdbUsagePolicy { // policies affecting db usage for ports and stores
+ mdbTokenSet mUsagePolicy_ScopePlan; // current scope usage plan
+ mdb_bool mUsagePolicy_MaxLazy; // nonzero: do least work
+ mdb_bool mUsagePolicy_MinMemory; // nonzero: use least memory
+};
+#endif /*mdbUsagePolicy_struct*/
+
+#ifndef mdbOid_typedef
+typedef struct mdbOid mdbOid;
+# define mdbOid_typedef 1
+#endif
+
+#ifndef mdbOid_struct
+# define mdbOid_struct 1
+struct mdbOid { // identity of some row or table inside a database
+ mdb_scope mOid_Scope; // scope token for an id's namespace
+ mdb_id mOid_Id; // identity of object inside scope namespace
+};
+#endif /*mdbOid_struct*/
+
+#ifndef mdbRange_typedef
+typedef struct mdbRange mdbRange;
+# define mdbRange_typedef 1
+#endif
+
+#ifndef mdbRange_struct
+# define mdbRange_struct 1
+struct mdbRange { // range of row positions in a table
+ mdb_pos mRange_FirstPos; // position of first row
+ mdb_pos mRange_LastPos; // position of last row
+};
+#endif /*mdbRange_struct*/
+
+#ifndef mdbColumnSet_typedef
+typedef struct mdbColumnSet mdbColumnSet;
+# define mdbColumnSet_typedef 1
+#endif
+
+#ifndef mdbColumnSet_struct
+# define mdbColumnSet_struct 1
+struct mdbColumnSet { // array of column tokens (just the same as mdbTokenSet)
+ mdb_count mColumnSet_Count; // number of columns
+ mdb_column* mColumnSet_Columns; // count mdb_column instances
+};
+#endif /*mdbColumnSet_struct*/
+
+#ifndef mdbYarn_typedef
+typedef struct mdbYarn mdbYarn;
+# define mdbYarn_typedef 1
+#endif
+
+#ifdef MDB_BEGIN_C_LINKAGE_define
+# define MDB_BEGIN_C_LINKAGE_define 1
+# define MDB_BEGIN_C_LINKAGE extern "C" {
+# define MDB_END_C_LINKAGE }
+#endif /*MDB_BEGIN_C_LINKAGE_define*/
+
+/*| mdbYarn_mGrow: an abstract API for growing the size of a mdbYarn
+**| instance. With respect to a specific API that requires a caller
+**| to supply a string (mdbYarn) that a callee fills with content
+**| that might exceed the specified size, mdbYarn_mGrow is a caller-
+**| supplied means of letting a callee attempt to increase the string
+**| size to become large enough to receive all content available.
+**|
+**|| Grow(): a method for requesting that a yarn instance be made
+**| larger in size. Note that such requests need not be honored, and
+**| need not be honored in full if only partial size growth is desired.
+**| (Note that no nsIMdbEnv instance is passed as argument, although one
+**| might be needed in some circumstances. So if an nsIMdbEnv is needed,
+**| a reference to one might be held inside a mdbYarn member slot.)
+**|
+**|| self: a yarn instance to be grown. Presumably this yarn is
+**| the instance which holds the mYarn_Grow method pointer. Yarn
+**| instancesshould only be passed to grow methods which they were
+**| specifically designed to fit, as indicated by the mYarn_Grow slot.
+**|
+**|| inNewSize: the new desired value for slot mYarn_Size in self.
+**| If mYarn_Size is already this big, then nothing should be done.
+**| If inNewSize is larger than seems feasible or desirable to honor,
+**| then any size restriction policy can be used to grow to some size
+**| greater than mYarn_Size. (Grow() might even grow to a size
+**| greater than inNewSize in order to make the increase in size seem
+**| worthwhile, rather than growing in many smaller steps over time.)
+|*/
+typedef void (*mdbYarn_mGrow)(mdbYarn* self, mdb_size inNewSize);
+// mdbYarn_mGrow methods must be declared with C linkage in C++
+
+/*| mdbYarn: a variable length "string" of arbitrary binary bytes,
+**| whose length is mYarn_Fill, inside a buffer mYarn_Buf that has
+**| at most mYarn_Size byte of physical space.
+**|
+**|| mYarn_Buf: a pointer to space containing content. This slot
+**| might never be nil when mYarn_Size is nonzero, but checks for nil
+**| are recommended anyway.
+**| (Implementations of mdbYarn_mGrow methods should take care to
+**| ensure the existence of a replacement before dropping old Bufs.)
+**| Content in Buf can be anything in any format, but the mYarn_Form
+**| implies the actual format by some caller-to-callee convention.
+**| mYarn_Form==0 implies US-ASCII iso-8859-1 Latin1 string content.
+**|
+**|| mYarn_Size: the physical size of Buf in bytes. Note that if one
+**| intends to terminate a string with a null byte, that it must not
+**| be written at or after mYarn_Buf[mYarn_Size] because this is after
+**| the last byte in the physical buffer space. Size can be zero,
+**| which means the string has no content whatsoever; note that when
+**| Size is zero, this is a suitable reason for Buf==nil as well.
+**|
+**|| mYarn_Fill: the logical content in Buf in bytes, where Fill must
+**| never exceed mYarn_Size. Note that yarn strings might not have a
+**| terminating null byte (since they might not even be C strings), but
+**| when they do, such terminating nulls are considered part of content
+**| and therefore Fill will count such null bytes. So an "empty" C
+**| string will have Fill==1, because content includes one null byte.
+**| Fill does not mean "length" when applied to C strings for this
+**| reason. However, clients using yarns to hold C strings can infer
+**| that length is equal to Fill-1 (but should take care to handle the
+**| case where Fill==0). To be paranoid, one can always copy to a
+**| destination with size exceeding Fill, and place a redundant null
+**| byte in the Fill position when this simplifies matters.
+**|
+**|| mYarn_Form: a designation of content format within mYarn_Buf.
+**| The semantics of this slot are the least well defined, since the
+**| actual meaning is context dependent, to the extent that callers
+**| and callees must agree on format encoding conventions when such
+**| are not standardized in many computing contexts. However, in the
+**| context of a specific mdb database, mYarn_Form is a token for an
+**| atomized string in that database that typically names a preferred
+**| mime type charset designation. If and when mdbYarn is used for
+**| other purposes away from the mdb interface, folks can use another
+**| convention system for encoding content formats. However, in all
+**| contexts is it useful to maintain the convention that Form==0
+**| implies Buf contains US-ASCII iso-8859-1 Latin1 string content.
+**|
+**|| mYarn_Grow: either a mdbYarn_mGrow method, or else nil. When
+**| a mdbYarn_mGrow method is provided, this method can be used to
+**| request a yarn buf size increase. A caller who constructs the
+**| original mdbYarn instance decides whether a grow method is necessary
+**| or desirable, and uses only grow methods suitable for the buffering
+**| nature of a specific mdbYarn instance. (For example, Buf might be a
+**| statically allocated string space which switches to something heap-based
+**| when grown, and subsequent calls to grow the yarn must distinguish the
+**| original static string from heap allocated space, etc.) Note that the
+**| method stored in mYarn_Grow can change, and this might be a common way
+**| to track memory managent changes in policy for mYarn_Buf.
+|*/
+#ifndef mdbYarn_struct
+# define mdbYarn_struct 1
+struct mdbYarn { // buffer with caller space allocation semantics
+ void* mYarn_Buf; // space for holding any binary content
+ mdb_fill mYarn_Fill; // logical content in Buf in bytes
+ mdb_size mYarn_Size; // physical size of Buf in bytes
+ mdb_more mYarn_More; // more available bytes if Buf is bigger
+ mdb_cscode mYarn_Form; // charset format encoding
+ mdbYarn_mGrow mYarn_Grow; // optional method to grow mYarn_Buf
+
+ // Subclasses might add further slots after mYarn_Grow in order to
+ // maintain bookkeeping needs, such as state info about mYarn_Buf.
+};
+#endif /*mdbYarn_struct*/
+
+// } %%%%% end C structs %%%%%
+
+// { %%%%% begin class forward defines %%%%%
+class nsIMdbEnv;
+class nsIMdbObject;
+class nsIMdbErrorHook;
+class nsIMdbThumb;
+class nsIMdbFactory;
+class nsIMdbFile;
+class nsIMdbPort;
+class nsIMdbStore;
+class nsIMdbCursor;
+class nsIMdbPortTableCursor;
+class nsIMdbCollection;
+class nsIMdbTable;
+class nsIMdbTableRowCursor;
+class nsIMdbRow;
+class nsIMdbRowCellCursor;
+class nsIMdbBlob;
+class nsIMdbCell;
+class nsIMdbSorting;
+// } %%%%% end class forward defines %%%%%
+
+// { %%%%% begin C++ abstract class interfaces %%%%%
+
+/*| nsIMdbObject: base class for all message db class interfaces
+**|
+**|| factory: all nsIMdbObjects from the same code suite have the same factory
+**|
+**|| refcounting: both strong and weak references, to ensure strong refs are
+**| acyclic, while weak refs can cause cycles. CloseMdbObject() is
+**| called when (strong) use counts hit zero, but clients can call this close
+**| method early for some reason, if absolutely necessary even though it will
+**| thwart the other uses of the same object. Note that implementations must
+**| cope with close methods being called arbitrary numbers of times. The COM
+**| calls to AddRef() and release ref map directly to strong use ref calls,
+**| but the total ref count for COM objects is the sum of weak & strong refs.
+|*/
+
+#define NS_IMDBOBJECT_IID_STR "5533ea4b-14c3-4bef-ac60-22f9e9a49084"
+
+#define NS_IMDBOBJECT_IID \
+ { \
+ 0x5533ea4b, 0x14c3, 0x4bef, { \
+ 0xac, 0x60, 0x22, 0xf9, 0xe9, 0xa4, 0x90, 0x84 \
+ } \
+ }
+
+class nsIMdbObject : public nsISupports { // msg db base class
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBOBJECT_IID)
+ // { ===== begin nsIMdbObject methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD IsFrozenMdbObject(nsIMdbEnv* ev, mdb_bool* outIsReadonly) = 0;
+ // same as nsIMdbPort::GetIsPortReadonly() when this object is inside a port.
+ // } ----- end attribute methods -----
+
+ // { ----- begin factory methods -----
+ NS_IMETHOD GetMdbFactory(nsIMdbEnv* ev, nsIMdbFactory** acqFactory) = 0;
+ // } ----- end factory methods -----
+
+ // { ----- begin ref counting for well-behaved cyclic graphs -----
+ NS_IMETHOD GetWeakRefCount(nsIMdbEnv* ev, // weak refs
+ mdb_count* outCount) = 0;
+ NS_IMETHOD GetStrongRefCount(nsIMdbEnv* ev, // strong refs
+ mdb_count* outCount) = 0;
+
+ NS_IMETHOD AddWeakRef(nsIMdbEnv* ev) = 0;
+ NS_IMETHOD_(mork_uses) AddStrongRef(nsIMdbEnv* ev) = 0;
+
+ NS_IMETHOD CutWeakRef(nsIMdbEnv* ev) = 0;
+ NS_IMETHOD CutStrongRef(nsIMdbEnv* ev) = 0;
+
+ NS_IMETHOD CloseMdbObject(nsIMdbEnv* ev) = 0; // called at strong refs zero
+ NS_IMETHOD IsOpenMdbObject(nsIMdbEnv* ev, mdb_bool* outOpen) = 0;
+ // } ----- end ref counting -----
+
+ // } ===== end nsIMdbObject methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbObject, NS_IMDBOBJECT_IID)
+
+/*| nsIMdbErrorHook: a base class for clients of this API to subclass, in order
+**| to provide a callback installable in nsIMdbEnv for error notifications. If
+**| apps that subclass nsIMdbErrorHook wish to maintain a reference to the env
+**| that contains the hook, then this should be a weak ref to avoid cycles.
+**|
+**|| OnError: when nsIMdbEnv has an error condition that causes the total count
+**| of errors to increase, then nsIMdbEnv should call OnError() to report the
+**| error in some fashion when an instance of nsIMdbErrorHook is installed. The
+**| variety of string flavors is currently due to the uncertainty here in the
+**| nsIMdbBlob and nsIMdbCell interfaces. (Note that overloading by using the
+**| same method name is not necessary here, and potentially less clear.)
+|*/
+class nsIMdbErrorHook
+ : public nsISupports { // env callback handler to report errors
+ public:
+ // { ===== begin error methods =====
+ NS_IMETHOD OnErrorString(nsIMdbEnv* ev, const char* inAscii) = 0;
+ NS_IMETHOD OnErrorYarn(nsIMdbEnv* ev, const mdbYarn* inYarn) = 0;
+ // } ===== end error methods =====
+
+ // { ===== begin warning methods =====
+ NS_IMETHOD OnWarningString(nsIMdbEnv* ev, const char* inAscii) = 0;
+ NS_IMETHOD OnWarningYarn(nsIMdbEnv* ev, const mdbYarn* inYarn) = 0;
+ // } ===== end warning methods =====
+
+ // { ===== begin abort hint methods =====
+ NS_IMETHOD OnAbortHintString(nsIMdbEnv* ev, const char* inAscii) = 0;
+ NS_IMETHOD OnAbortHintYarn(nsIMdbEnv* ev, const mdbYarn* inYarn) = 0;
+ // } ===== end abort hint methods =====
+};
+
+/*| nsIMdbHeap: abstract memory allocation interface.
+**|
+**|| Alloc: return a block at least inSize bytes in size with alignment
+**| suitable for any native type (such as long integers). When no such
+**| block can be allocated, failure is indicated by a null address in
+**| addition to reporting an error in the environment.
+**|
+**|| Free: deallocate a block allocated or resized earlier by the same
+**| heap instance. If the inBlock parameter is nil, the heap should do
+**| nothing (and crashing is strongly discouraged).
+|*/
+class nsIMdbHeap { // caller-supplied memory management interface
+ public:
+ // { ===== begin nsIMdbHeap methods =====
+ NS_IMETHOD Alloc(nsIMdbEnv* ev, // allocate a piece of memory
+ mdb_size inSize, // requested byte size of new memory block
+ void** outBlock) =
+ 0; // memory block of inSize bytes, or nil
+
+ NS_IMETHOD Free(nsIMdbEnv* ev, // free block from Alloc or Resize()
+ void* ioBlock) = 0; // block to be destroyed/deallocated
+
+ virtual size_t GetUsedSize() = 0;
+
+ virtual ~nsIMdbHeap(){};
+ // } ===== end nsIMdbHeap methods =====
+};
+
+/*| nsIMdbCPlusHeap: Alloc() with global ::new(), Free() with global ::delete().
+**| Resize() is done by ::new() followed by ::delete().
+|*/
+class nsIMdbCPlusHeap { // caller-supplied memory management interface
+ public:
+ // { ===== begin nsIMdbHeap methods =====
+ NS_IMETHOD Alloc(nsIMdbEnv* ev, // allocate a piece of memory
+ mdb_size inSize, // requested size of new memory block
+ void** outBlock); // memory block of inSize bytes, or nil
+
+ NS_IMETHOD Free(nsIMdbEnv* ev, // free block allocated earlier by Alloc()
+ void* inBlock);
+
+ NS_IMETHOD HeapAddStrongRef(nsIMdbEnv* ev);
+ NS_IMETHOD HeapCutStrongRef(nsIMdbEnv* ev);
+ // } ===== end nsIMdbHeap methods =====
+};
+
+/*| nsIMdbThumb:
+|*/
+
+#define NS_IMDBTHUMB_IID_STR "6d3ad7c1-a809-4e74-8577-49fa9a4562fa"
+
+#define NS_IMDBTHUMB_IID \
+ { \
+ 0x6d3ad7c1, 0xa809, 0x4e74, { \
+ 0x85, 0x77, 0x49, 0xfa, 0x9a, 0x45, 0x62, 0xfa \
+ } \
+ }
+
+class nsIMdbThumb
+ : public nsISupports { // closure for repeating incremental method
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBTHUMB_IID)
+
+ // { ===== begin nsIMdbThumb methods =====
+ NS_IMETHOD GetProgress(
+ nsIMdbEnv* ev,
+ mdb_count* outTotal, // total somethings to do in operation
+ mdb_count* outCurrent, // subportion of total completed so far
+ mdb_bool* outDone, // is operation finished?
+ mdb_bool* outBroken // is operation irreparably dead and broken?
+ ) = 0;
+
+ NS_IMETHOD DoMore(
+ nsIMdbEnv* ev,
+ mdb_count* outTotal, // total somethings to do in operation
+ mdb_count* outCurrent, // subportion of total completed so far
+ mdb_bool* outDone, // is operation finished?
+ mdb_bool* outBroken // is operation irreparably dead and broken?
+ ) = 0;
+
+ NS_IMETHOD CancelAndBreakThumb( // cancel pending operation
+ nsIMdbEnv* ev) = 0;
+ // } ===== end nsIMdbThumb methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbThumb, NS_IMDBTHUMB_IID)
+
+/*| nsIMdbEnv: a context parameter used when calling most abstract db methods.
+**| The main purpose of such an object is to permit a database implementation
+**| to avoid the use of globals to share information between various parts of
+**| the implementation behind the abstract db interface. An environment acts
+**| like a session object for a given calling thread, and callers should use
+**| at least one different nsIMdbEnv instance for each thread calling the API.
+**| While the database implementation might not be threaded, it is highly
+**| desirable that the db be thread-safe if calling threads use distinct
+**| instances of nsIMdbEnv. Callers can stop at one nsIMdbEnv per thread, or
+they
+**| might decide to make on nsIMdbEnv instance for every nsIMdbPort opened, so
+that
+**| error information is segregated by database instance. Callers create
+**| instances of nsIMdbEnv by calling the MakeEnv() method in nsIMdbFactory.
+**|
+**|| tracing: an environment might support some kind of tracing, and this
+**| boolean attribute permits such activity to be enabled or disabled.
+**|
+**|| errors: when a call to the abstract db interface returns, a caller might
+**| check the number of outstanding errors to see whether the operation did
+**| actually succeed. Each nsIMdbEnv should have all its errors cleared by a
+**| call to ClearErrors() before making each call to the abstract db API,
+**| because outstanding errors might disable further database actions. (This
+**| is not done inside the db interface, because the db cannot in general know
+**| when a call originates from inside or outside -- only the app knows this.)
+**|
+**|| error hook: callers can install an instance of nsIMdbErrorHook to receive
+**| error notifications whenever the error count increases. The hook can
+**| be uninstalled by passing a null pointer.
+**|
+|*/
+
+#define NS_IMDBENV_IID_STR "a765e46b-efb6-41e6-b75b-c5d6bd710594"
+
+#define NS_IMDBENV_IID \
+ { \
+ 0xa765e46b, 0xefb6, 0x41e6, { \
+ 0xb7, 0x5b, 0xc5, 0xd6, 0xbd, 0x71, 0x05, 0x94 \
+ } \
+ }
+
+class nsIMdbEnv : public nsISupports { // db specific context parameter
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBENV_IID)
+ // { ===== begin nsIMdbEnv methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetErrorCount(mdb_count* outCount, mdb_bool* outShouldAbort) = 0;
+ NS_IMETHOD GetWarningCount(mdb_count* outCount, mdb_bool* outShouldAbort) = 0;
+
+ NS_IMETHOD GetEnvBeVerbose(mdb_bool* outBeVerbose) = 0;
+ NS_IMETHOD SetEnvBeVerbose(mdb_bool inBeVerbose) = 0;
+
+ NS_IMETHOD GetDoTrace(mdb_bool* outDoTrace) = 0;
+ NS_IMETHOD SetDoTrace(mdb_bool inDoTrace) = 0;
+
+ NS_IMETHOD GetAutoClear(mdb_bool* outAutoClear) = 0;
+ NS_IMETHOD SetAutoClear(mdb_bool inAutoClear) = 0;
+
+ NS_IMETHOD GetErrorHook(nsIMdbErrorHook** acqErrorHook) = 0;
+ NS_IMETHOD SetErrorHook(nsIMdbErrorHook* ioErrorHook) =
+ 0; // becomes referenced
+
+ NS_IMETHOD GetHeap(nsIMdbHeap** acqHeap) = 0;
+ NS_IMETHOD SetHeap(nsIMdbHeap* ioHeap) = 0; // becomes referenced
+ // } ----- end attribute methods -----
+
+ NS_IMETHOD ClearErrors() = 0; // clear errors beore re-entering db API
+ NS_IMETHOD ClearWarnings() = 0; // clear warnings
+ NS_IMETHOD ClearErrorsAndWarnings() = 0; // clear both errors & warnings
+ // } ===== end nsIMdbEnv methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbEnv, NS_IMDBENV_IID)
+
+/*| nsIMdbFactory: the main entry points to the abstract db interface. A DLL
+**| that supports this mdb interface need only have a single exported method
+**| that will return an instance of nsIMdbFactory, so that further methods in
+**| the suite can be accessed from objects returned by nsIMdbFactory methods.
+**|
+**|| mdbYarn: note all nsIMdbFactory subclasses must guarantee null
+**| termination of all strings written into mdbYarn instances, as long as
+**| mYarn_Size and mYarn_Buf are nonzero. Even truncated string values must
+**| be null terminated. This is more strict behavior than mdbYarn requires,
+**| but it is part of the nsIMdbFactory interface.
+**|
+**|| envs: an environment instance is required as per-thread context for
+**| most of the db method calls, so nsIMdbFactory creates such instances.
+**|
+**|| rows: callers must be able to create row instances that are independent
+**| of storage space that is part of the db content graph. Many interfaces
+**| for data exchange have strictly copy semantics, so that a row instance
+**| has no specific identity inside the db content model, and the text in
+**| cells are an independenty copy of unexposed content inside the db model.
+**| Callers are expected to maintain one or more row instances as a buffer
+**| for staging cell content copied into or out of a table inside the db.
+**| Callers are urged to use an instance of nsIMdbRow created by the
+nsIMdbFactory
+**| code suite, because reading and writing might be much more efficient than
+**| when using a hand-rolled nsIMdbRow subclass with no relation to the suite.
+**|
+**|| ports: a port is a readonly interface to a specific database file. Most
+**| of the methods to access a db file are suitable for a readonly interface,
+**| so a port is the basic minimum for accessing content. This makes it
+**| possible to read other external formats for import purposes, without
+**| needing the code or competence necessary to write every such format. So
+**| we can write generic import code just once, as long as every format can
+**| show a face based on nsIMdbPort. (However, same suite import can be faster.)
+**| Given a file name and the first 512 bytes of a file, a factory can say if
+**| a port can be opened by this factory. Presumably an app maintains chains
+**| of factories for different suites, and asks each in turn about opening a
+**| a prospective file for reading (as a port) or writing (as a store). I'm
+**| not ready to tackle issues of format fidelity and factory chain ordering.
+**|
+**|| stores: a store is a mutable interface to a specific database file, and
+**| includes the port interface plus any methods particular to writing, which
+**| are few in number. Presumably the set of files that can be opened as
+**| stores is a subset of the set of files that can be opened as ports. A
+**| new store can be created with CreateNewFileStore() by supplying a new
+**| file name which does not yet exist (callers are always responsible for
+**| destroying any existing files before calling this method).
+|*/
+
+#define NS_IMDBFACTORY_IID_STR "2b80395c-b91e-4990-b1a7-023e99ab14e9"
+
+#define NS_IMDBFACTORY_IID \
+ { \
+ 0xf04aa4ab, 0x1fe, 0x4115, { \
+ 0xa4, 0xa5, 0x68, 0x19, 0xdf, 0xf1, 0x10, 0x3d \
+ } \
+ }
+
+class nsIMdbFactory : public nsISupports { // suite entry points
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBFACTORY_IID)
+ // { ===== begin nsIMdbFactory methods =====
+
+ // { ----- begin file methods -----
+ NS_IMETHOD OpenOldFile(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath, mdb_bool inFrozen,
+ nsIMdbFile** acqFile) = 0;
+ // Choose some subclass of nsIMdbFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be open and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+
+ NS_IMETHOD CreateNewFile(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath,
+ nsIMdbFile** acqFile) = 0;
+ // Choose some subclass of nsIMdbFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be created and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+ // } ----- end file methods -----
+
+ // { ----- begin env methods -----
+ NS_IMETHOD MakeEnv(nsIMdbHeap* ioHeap,
+ nsIMdbEnv** acqEnv) = 0; // acquire new env
+ // ioHeap can be nil, causing a MakeHeap() style heap instance to be used
+ // } ----- end env methods -----
+
+ // { ----- begin heap methods -----
+ NS_IMETHOD MakeHeap(nsIMdbEnv* ev,
+ nsIMdbHeap** acqHeap) = 0; // acquire new heap
+ // } ----- end heap methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD MakeRow(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbRow** acqRow) = 0; // new row
+ // ioHeap can be nil, causing the heap associated with ev to be used
+ // } ----- end row methods -----
+
+ // { ----- begin port methods -----
+ NS_IMETHOD CanOpenFilePort(
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpen, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) = 0; // informal file format description
+
+ NS_IMETHOD OpenFilePort(
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for readonly import
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental port open
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then call nsIMdbFactory::ThumbToOpenPort() to get the port instance.
+
+ NS_IMETHOD
+ ThumbToOpenPort( // redeeming a completed thumb from OpenFilePort()
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFilePort() with done status
+ nsIMdbPort** acqPort) = 0; // acquire new port object
+ // } ----- end port methods -----
+
+ // { ----- begin store methods -----
+ NS_IMETHOD CanOpenFileStore(
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpenAsStore, // whether OpenFileStore() might succeed
+ mdb_bool* outCanOpenAsPort, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) = 0; // informal file format description
+
+ NS_IMETHOD OpenFileStore( // open an existing database
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for general db usage
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental store open
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then call nsIMdbFactory::ThumbToOpenStore() to get the store instance.
+
+ NS_IMETHOD
+ ThumbToOpenStore( // redeem completed thumb from OpenFileStore()
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFileStore() with done status
+ nsIMdbStore** acqStore) = 0; // acquire new db store object
+
+ NS_IMETHOD CreateNewFileStore( // create a new db with minimal content
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // name of file which should not yet exist
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbStore** acqStore) = 0; // acquire new db store object
+ // } ----- end store methods -----
+
+ // } ===== end nsIMdbFactory methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbFactory, NS_IMDBFACTORY_IID)
+
+extern "C" nsIMdbFactory* MakeMdbFactory();
+
+/*| nsIMdbFile: abstract file interface resembling the original morkFile
+**| abstract interface (which was in turn modeled on the file interface
+**| from public domain IronDoc). The design of this file interface is
+**| complicated by the fact that some DB's will not find this interface
+**| adequate for all runtime requirements (even though this file API is
+**| enough to implement text-based DB's like Mork). For this reason,
+**| more methods have been added to let a DB library force the file to
+**| become closed so the DB can reopen the file in some other manner.
+**| Folks are encouraged to suggest ways to tune this interface to suit
+**| DB's that cannot manage to pull their maneuvers even given this API.
+**|
+**|| Tell: get the current i/o position in file
+**|
+**|| Seek: change the current i/o position in file
+**|
+**|| Eof: return file's total length in bytes
+**|
+**|| Read: input inSize bytes into outBuf, returning actual transfer size
+**|
+**|| Get: read starting at specific file offset (e.g. Seek(); Read();)
+**|
+**|| Write: output inSize bytes from inBuf, returning actual transfer size
+**|
+**|| Put: write starting at specific file offset (e.g. Seek(); Write();)
+**|
+**|| Flush: if written bytes are buffered, push them to final destination
+**|
+**|| Path: get file path in some string representation. This is intended
+**| either to support the display of file name in a user presentation, or
+**| to support the closing and reopening of the file when the DB needs more
+**| exotic file access than is presented by the nsIMdbFile interface.
+**|
+**|| Steal: tell this file to close any associated i/o stream in the file
+**| system, because the file ioThief intends to reopen the file in order
+**| to provide the MDB implementation with more exotic file access than is
+**| offered by the nsIMdbFile alone. Presumably the thief knows enough
+**| from Path() in order to know which file to reopen. If Steal() is
+**| successful, this file should probably delegate all future calls to
+**| the nsIMdbFile interface down to the thief files, so that even after
+**| the file has been stolen, it can still be read, written, or forcibly
+**| closed (by a call to CloseMdbObject()).
+**|
+**|| Thief: acquire and return thief passed to an earlier call to Steal().
+|*/
+
+#define NS_IMDBFILE_IID_STR "f04aa4ab-1fe7-4115-a4a5-6819dff1103d"
+
+#define NS_IMDBFILE_IID \
+ { \
+ 0xf04aa4ab, 0x1fe, 0x4115, { \
+ 0xa4, 0xa5, 0x68, 0x19, 0xdf, 0xf1, 0x10, 0x3d \
+ } \
+ }
+
+class nsIMdbFile : public nsISupports { // minimal file interface
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBFILE_IID)
+ // { ===== begin nsIMdbFile methods =====
+
+ // { ----- begin pos methods -----
+ NS_IMETHOD Tell(nsIMdbEnv* ev, mdb_pos* outPos) const = 0;
+ NS_IMETHOD Seek(nsIMdbEnv* ev, mdb_pos inPos, mdb_pos* outPos) = 0;
+ NS_IMETHOD Eof(nsIMdbEnv* ev, mdb_pos* outPos) = 0;
+ // } ----- end pos methods -----
+
+ // { ----- begin read methods -----
+ NS_IMETHOD Read(nsIMdbEnv* ev, void* outBuf, mdb_size inSize,
+ mdb_size* outActualSize) = 0;
+ NS_IMETHOD Get(nsIMdbEnv* ev, void* outBuf, mdb_size inSize, mdb_pos inPos,
+ mdb_size* outActualSize) = 0;
+ // } ----- end read methods -----
+
+ // { ----- begin write methods -----
+ NS_IMETHOD Write(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ mdb_size* outActualSize) = 0;
+ NS_IMETHOD Put(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ mdb_pos inPos, mdb_size* outActualSize) = 0;
+ NS_IMETHOD Flush(nsIMdbEnv* ev) = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin path methods -----
+ NS_IMETHOD Path(nsIMdbEnv* ev, mdbYarn* outFilePath) = 0;
+ // } ----- end path methods -----
+
+ // { ----- begin replacement methods -----
+ NS_IMETHOD Steal(nsIMdbEnv* ev, nsIMdbFile* ioThief) = 0;
+ NS_IMETHOD Thief(nsIMdbEnv* ev, nsIMdbFile** acqThief) = 0;
+ // } ----- end replacement methods -----
+
+ // { ----- begin versioning methods -----
+ NS_IMETHOD BecomeTrunk(nsIMdbEnv* ev) = 0;
+ // If this file is a file version branch created by calling AcquireBud(),
+ // BecomeTrunk() causes this file's content to replace the original
+ // file's content, typically by assuming the original file's identity.
+ // This default implementation of BecomeTrunk() does nothing, and this
+ // is appropriate behavior for files which are not branches, and is
+ // also the right behavior for files returned from AcquireBud() which are
+ // in fact the original file that has been truncated down to zero length.
+
+ NS_IMETHOD AcquireBud(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbFile** acqBud) =
+ 0; // acquired file for new version of content
+ // AcquireBud() starts a new "branch" version of the file, empty of content,
+ // so that a new version of the file can be written. This new file
+ // can later be told to BecomeTrunk() the original file, so the branch
+ // created by budding the file will replace the original file. Some
+ // file subclasses might initially take the unsafe but expedient
+ // approach of simply truncating this file down to zero length, and
+ // then returning the same morkFile pointer as this, with an extra
+ // reference count increment. Note that the caller of AcquireBud() is
+ // expected to eventually call CutStrongRef() on the returned file
+ // in order to release the strong reference. High quality versions
+ // of morkFile subclasses will create entirely new files which later
+ // are renamed to become the old file, so that better transactional
+ // behavior is exhibited by the file, so crashes protect old files.
+ // Note that AcquireBud() is an illegal operation on readonly files.
+ // } ----- end versioning methods -----
+
+ // } ===== end nsIMdbFile methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbFile, NS_IMDBFILE_IID)
+
+/*| nsIMdbPort: a readonly interface to a specific database file. The mutable
+**| nsIMdbStore interface is a subclass that includes writing behavior, but
+**| most of the needed db methods appear in the readonly nsIMdbPort interface.
+**|
+**|| mdbYarn: note all nsIMdbPort and nsIMdbStore subclasses must guarantee null
+**| termination of all strings written into mdbYarn instances, as long as
+**| mYarn_Size and mYarn_Buf are nonzero. Even truncated string values must
+**| be null terminated. This is more strict behavior than mdbYarn requires,
+**| but it is part of the nsIMdbPort and nsIMdbStore interface.
+**|
+**|| attributes: methods are provided to distinguish a readonly port from a
+**| mutable store, and whether a mutable store actually has any dirty content.
+**|
+**|| filepath: the file path used to open the port from the nsIMdbFactory can be
+**| queried and discovered by GetPortFilePath(), which includes format info.
+**|
+**|| export: a port can write itself in other formats, with perhaps a typical
+**| emphasis on text interchange formats used by other systems. A port can be
+**| queried to determine its preferred export interchange format, and a port
+**| can be queried to see whether a specific export format is supported. And
+**| actually exporting a port requires a new destination file name and format.
+**|
+**|| tokens: a port supports queries about atomized strings to map tokens to
+**| strings or strings to token integers. (All atomized strings must be in
+**| US-ASCII iso-8859-1 Latin1 charset encoding.) When a port is actually a
+**| mutable store and a string has not yet been atomized, then StringToToken()
+**| will actually do so and modify the store. The QueryToken() method will not
+**| atomize a string if it has not already been atomized yet, even in stores.
+**|
+**|| tables: other than string tokens, all port content is presented through
+**| tables, which are ordered collections of rows. Tables are identified by
+**| row scope and table kind, which might or might not be unique in a port,
+**| depending on app convention. When tables are effectively unique, then
+**| queries for specific scope and kind pairs will find those tables. To see
+**| all tables that match specific row scope and table kind patterns, even in
+**| the presence of duplicates, every port supports a GetPortTableCursor()
+**| method that returns an iterator over all matching tables. Table kind is
+**| considered scoped inside row scope, so passing a zero for table kind will
+**| find all table kinds for some nonzero row scope. Passing a zero for row
+**| scope will iterate over all tables in the port, in some undefined order.
+**| (A new table can be added to a port using nsIMdbStore::NewTable(), even when
+**| the requested scope and kind combination is already used by other tables.)
+**|
+**|| memory: callers can request that a database use less memory footprint in
+**| several flavors, from an inconsequential idle flavor to a rather drastic
+**| panic flavor. Callers might perform an idle purge very frequently if desired
+**| with very little cost, since only normally scheduled memory management will
+**| be conducted, such as freeing resources for objects scheduled to be dropped.
+**| Callers should perform session memory purges infrequently because they might
+**| involve costly scanning of data structures to removed cached content, and
+**| session purges are recommended only when a caller experiences memory crunch.
+**| Callers should only rarely perform a panic purge, in response to dire memory
+**| straits, since this is likely to make db operations much more expensive
+**| than they would be otherwise. A panic purge asks a database to free as much
+**| memory as possible while staying effective and operational, because a caller
+**| thinks application failure might otherwise occur. (Apps might better close
+**| an open db, so panic purges only make sense when a db is urgently needed.)
+|*/
+class nsIMdbPort : public nsISupports {
+ public:
+ // { ===== begin nsIMdbPort methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetIsPortReadonly(nsIMdbEnv* ev, mdb_bool* outBool) = 0;
+ NS_IMETHOD GetIsStore(nsIMdbEnv* ev, mdb_bool* outBool) = 0;
+ NS_IMETHOD GetIsStoreAndDirty(nsIMdbEnv* ev, mdb_bool* outBool) = 0;
+
+ NS_IMETHOD GetUsagePolicy(nsIMdbEnv* ev, mdbUsagePolicy* ioUsagePolicy) = 0;
+
+ NS_IMETHOD SetUsagePolicy(nsIMdbEnv* ev,
+ const mdbUsagePolicy* inUsagePolicy) = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin memory policy methods -----
+ NS_IMETHOD IdleMemoryPurge( // do memory management already scheduled
+ nsIMdbEnv* ev, // context
+ mdb_size* outEstimatedBytesFreed) =
+ 0; // approximate bytes actually freed
+
+ NS_IMETHOD SessionMemoryPurge( // request specific footprint decrease
+ nsIMdbEnv* ev, // context
+ mdb_size inDesiredBytesFreed, // approximate number of bytes wanted
+ mdb_size* outEstimatedBytesFreed) =
+ 0; // approximate bytes actually freed
+
+ NS_IMETHOD PanicMemoryPurge( // desperately free all possible memory
+ nsIMdbEnv* ev, // context
+ mdb_size* outEstimatedBytesFreed) =
+ 0; // approximate bytes actually freed
+ // } ----- end memory policy methods -----
+
+ // { ----- begin filepath methods -----
+ NS_IMETHOD GetPortFilePath(
+ nsIMdbEnv* ev, // context
+ mdbYarn* outFilePath, // name of file holding port content
+ mdbYarn* outFormatVersion) = 0; // file format description
+
+ NS_IMETHOD GetPortFile(nsIMdbEnv* ev, // context
+ nsIMdbFile** acqFile) =
+ 0; // acquire file used by port or store
+ // } ----- end filepath methods -----
+
+ // { ----- begin export methods -----
+ NS_IMETHOD BestExportFormat( // determine preferred export format
+ nsIMdbEnv* ev, // context
+ mdbYarn* outFormatVersion) = 0; // file format description
+
+ // some tentative suggested import/export formats
+ // "ns:msg:db:port:format:ldif:ns4.0:passthrough" // necessary
+ // "ns:msg:db:port:format:ldif:ns4.5:utf8" // necessary
+ // "ns:msg:db:port:format:ldif:ns4.5:tabbed"
+ // "ns:msg:db:port:format:ldif:ns4.5:binary" // necessary
+ // "ns:msg:db:port:format:html:ns3.0:addressbook" // necessary
+ // "ns:msg:db:port:format:html:display:verbose"
+ // "ns:msg:db:port:format:html:display:concise"
+ // "ns:msg:db:port:format:mork:zany:verbose" // necessary
+ // "ns:msg:db:port:format:mork:zany:atomized" // necessary
+ // "ns:msg:db:port:format:rdf:xml"
+ // "ns:msg:db:port:format:xml:mork"
+ // "ns:msg:db:port:format:xml:display:verbose"
+ // "ns:msg:db:port:format:xml:display:concise"
+ // "ns:msg:db:port:format:xml:print:verbose" // recommended
+ // "ns:msg:db:port:format:xml:print:concise"
+
+ NS_IMETHOD
+ CanExportToFormat( // can export content in given specific format?
+ nsIMdbEnv* ev, // context
+ const char* inFormatVersion, // file format description
+ mdb_bool* outCanExport) = 0; // whether ExportSource() might succeed
+
+ NS_IMETHOD ExportToFormat( // export content in given specific format
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to receive exported content
+ nsIMdbFile* ioFile, // destination abstract file interface
+ const char* inFormatVersion, // file format description
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental export
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the export will be finished.
+
+ // } ----- end export methods -----
+
+ // { ----- begin token methods -----
+ NS_IMETHOD TokenToString( // return a string name for an integer token
+ nsIMdbEnv* ev, // context
+ mdb_token inToken, // token for inTokenName inside this port
+ mdbYarn* outTokenName) = 0; // the type of table to access
+
+ NS_IMETHOD StringToToken( // return an integer token for scope name
+ nsIMdbEnv* ev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) = 0; // token for inTokenName inside this port
+
+ // String token zero is never used and never supported. If the port
+ // is a mutable store, then StringToToken() to create a new
+ // association of inTokenName with a new integer token if possible.
+ // But a readonly port will return zero for an unknown scope name.
+
+ NS_IMETHOD QueryToken( // like StringToToken(), but without adding
+ nsIMdbEnv* ev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) = 0; // token for inTokenName inside this port
+
+ // QueryToken() will return a string token if one already exists,
+ // but unlike StringToToken(), will not assign a new token if not
+ // already in use.
+
+ // } ----- end token methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD HasRow( // contains a row with the specified oid?
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_bool* outHasRow) = 0; // whether GetRow() might succeed
+
+ NS_IMETHOD GetRowRefCount( // get number of tables that contain a row
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_count* outRefCount) = 0; // number of tables containing inRowKey
+
+ NS_IMETHOD GetRow( // access one row with specific oid
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ nsIMdbRow** acqRow) = 0; // acquire specific row (or null)
+
+ // NS_IMETHOD
+ // GetPortRowCursor( // get cursor for all rows in specific scope
+ // nsIMdbEnv* ev, // context
+ // mdb_scope inRowScope, // row scope for row ids
+ // nsIMdbPortRowCursor** acqCursor) = 0; // all such rows in the port
+
+ NS_IMETHOD FindRow(
+ nsIMdbEnv* ev, // search for row with matching cell
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_column inColumn, // the column to search (and maintain an index)
+ const mdbYarn* inTargetCellValue, // cell value for which to search
+ mdbOid* outRowOid, // out row oid on match (or {0,-1} for no match)
+ nsIMdbRow** acqRow) = 0; // acquire matching row (or nil for no match)
+ // can be null if you only want the oid
+ // FindRow() searches for one row that has a cell in column inColumn with
+ // a contained value with the same form (i.e. charset) and is byte-wise
+ // identical to the blob described by yarn inTargetCellValue. Both content
+ // and form of the yarn must be an exact match to find a matching row.
+ //
+ // (In other words, both a yarn's blob bytes and form are significant. The
+ // form is not expected to vary in columns used for identity anyway. This
+ // is intended to make the cost of FindRow() cheaper for MDB implementors,
+ // since any cell value atomization performed internally must necessarily
+ // make yarn form significant in order to avoid data loss in atomization.)
+ //
+ // FindRow() can lazily create an index on attribute inColumn for all rows
+ // with that attribute in row space scope inRowScope, so that subsequent
+ // calls to FindRow() will perform faster. Such an index might or might
+ // not be persistent (but this seems desirable if it is cheap to do so).
+ // Note that lazy index creation in readonly DBs is not very feasible.
+ //
+ // This FindRow() interface assumes that attribute inColumn is effectively
+ // an alternative means of unique identification for a row in a rowspace,
+ // so correct behavior is only guaranteed when no duplicates for this col
+ // appear in the given set of rows. (If more than one row has the same cell
+ // value in this column, no more than one will be found; and cutting one of
+ // two duplicate rows can cause the index to assume no other such row lives
+ // in the row space, so future calls return nil for negative search results
+ // even though some duplicate row might still live within the rowspace.)
+ //
+ // In other words, the FindRow() implementation is allowed to assume simple
+ // hash tables mapping unique column keys to associated row values will be
+ // sufficient, where any duplication is not recorded because only one copy
+ // of a given key need be remembered. Implementors are not required to sort
+ // all rows by the specified column.
+ // } ----- end row methods -----
+
+ // { ----- begin table methods -----
+ NS_IMETHOD HasTable( // supports a table with the specified oid?
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ mdb_bool* outHasTable) = 0; // whether GetTable() might succeed
+
+ NS_IMETHOD GetTable( // access one table with specific oid
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ nsIMdbTable** acqTable) = 0; // acquire specific table (or null)
+
+ NS_IMETHOD HasTableKind( // supports a table of the specified type?
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // rid scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outSupportsTable) = 0; // whether GetTableKind() might succeed
+
+ // row scopes to be supported include the following suggestions:
+ // "ns:msg:db:row:scope:address:cards:all"
+ // "ns:msg:db:row:scope:mail:messages:all"
+ // "ns:msg:db:row:scope:news:articles:all"
+
+ // table kinds to be supported include the following suggestions:
+ // "ns:msg:db:table:kind:address:cards:main"
+ // "ns:msg:db:table:kind:address:lists:all"
+ // "ns:msg:db:table:kind:address:list"
+ // "ns:msg:db:table:kind:news:threads:all"
+ // "ns:msg:db:table:kind:news:thread"
+ // "ns:msg:db:table:kind:mail:threads:all"
+ // "ns:msg:db:table:kind:mail:thread"
+
+ NS_IMETHOD GetTableKind( // access one (random) table of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outMustBeUnique, // whether port can hold only one of these
+ nsIMdbTable** acqTable) = 0; // acquire scoped collection of rows
+
+ NS_IMETHOD
+ GetPortTableCursor( // get cursor for all tables of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ nsIMdbPortTableCursor** acqCursor) = 0; // all such tables in the port
+ // } ----- end table methods -----
+
+ // { ----- begin commit methods -----
+
+ NS_IMETHOD ShouldCompress( // store wastes at least inPercentWaste?
+ nsIMdbEnv* ev, // context
+ mdb_percent inPercentWaste, // 0..100 percent file size waste threshold
+ mdb_percent* outActualWaste, // 0..100 percent of file actually wasted
+ mdb_bool* outShould) = 0; // true when about inPercentWaste% is wasted
+ // ShouldCompress() returns true if the store can determine that the file
+ // will shrink by an estimated percentage of inPercentWaste% (or more) if
+ // CompressCommit() is called, because that percentage of the file seems
+ // to be recoverable free space. The granularity is only in terms of
+ // percentage points, and any value over 100 is considered equal to 100.
+ //
+ // If a store only has an approximate idea how much space might be saved
+ // during a compress, then a best guess should be made. For example, the
+ // Mork implementation might keep track of how much file space began with
+ // text content before the first updating transaction, and then consider
+ // all content following the start of the first transaction as potentially
+ // wasted space if it is all updates and not just new content. (This is
+ // a safe assumption in the sense that behavior will stabilize on a low
+ // estimate of wastage after a commit removes all transaction updates.)
+ //
+ // Some db formats might attempt to keep a very accurate reckoning of free
+ // space size, so a very accurate determination can be made. But other db
+ // formats might have difficulty determining size of free space, and might
+ // require some lengthy calculation to answer. This is the reason for
+ // passing in the percentage threshold of interest, so that such lengthy
+ // computations can terminate early as soon as at least inPercentWaste is
+ // found, so that the entire file need not be groveled when unnecessary.
+ // However, we hope implementations will always favor fast but imprecise
+ // heuristic answers instead of extremely slow but very precise answers.
+ //
+ // If the outActualWaste parameter is non-nil, it will be used to return
+ // the actual estimated space wasted as a percentage of file size. (This
+ // parameter is provided so callers need not call repeatedly with altered
+ // inPercentWaste values to isolate the actual wastage figure.) Note the
+ // actual wastage figure returned can exactly equal inPercentWaste even
+ // when this grossly underestimates the real figure involved, if the db
+ // finds it very expensive to determine the extent of wastage after it is
+ // known to at least exceed inPercentWaste. Note we expect that whenever
+ // outShould returns true, that outActualWaste returns >= inPercentWaste.
+ //
+ // The effect of different inPercentWaste values is not very uniform over
+ // the permitted range. For example, 50 represents 50% wastage, or a file
+ // that is about double what it should be ideally. But 99 represents 99%
+ // wastage, or a file that is about ninety-nine times as big as it should
+ // be ideally. In the smaller direction, 25 represents 25% wastage, or
+ // a file that is only 33% larger than it should be ideally.
+ //
+ // Callers can determine what policy they want to use for considering when
+ // a file holds too much wasted space, and express this as a percentage
+ // of total file size to pass as in the inPercentWaste parameter. A zero
+ // likely returns always trivially true, and 100 always trivially false.
+ // The great majority of callers are expected to use values from 25 to 75,
+ // since most plausible thresholds for compressing might fall between the
+ // extremes of 133% of ideal size and 400% of ideal size. (Presumably the
+ // larger a file gets, the more important the percentage waste involved, so
+ // a sliding scale for compress thresholds might use smaller numbers for
+ // much bigger file sizes.)
+
+ // } ----- end commit methods -----
+
+ // } ===== end nsIMdbPort methods =====
+};
+
+/*| nsIMdbStore: a mutable interface to a specific database file.
+**|
+**|| tables: one can force a new table to exist in a store with NewTable()
+**| and nonzero values for both row scope and table kind. (If one wishes only
+**| one table of a certain kind, then one might look for it first using the
+**| GetTableKind() method). One can pass inMustBeUnique to force future
+**| users of this store to be unable to create other tables with the same pair
+**| of scope and kind attributes. When inMustBeUnique is true, and the table
+**| with the given scope and kind pair already exists, then the existing one
+**| is returned instead of making a new table. Similarly, if one passes false
+**| for inMustBeUnique, but the table kind has already been marked unique by a
+**| previous user of the store, then the existing unique table is returned.
+**|
+**|| import: all or some of another port's content can be imported by calling
+**| AddPortContent() with a row scope identifying the extent of content to
+**| be imported. A zero row scope will import everything. A nonzero row
+**| scope will only import tables with a matching row scope. Note that one
+**| must somehow find a way to negotiate possible conflicts between existing
+**| row content and imported row content, and this involves a specific kind of
+**| definition for row identity involving either row IDs or unique attributes,
+**| or some combination of these two. At the moment I am just going to wave
+**| my hands, and say the default behavior is to assign all new row identities
+**| to all imported content, which will result in no merging of content; this
+**| must change later because it is unacceptable in some contexts.
+**|
+**|| commits: to manage modifications in a mutable store, very few methods are
+**| really needed to indicate global policy choices that are independent of
+**| the actual modifications that happen in objects at the level of tables,
+**| rows, and cells, etc. The most important policy to specify is which sets
+**| of changes are considered associated in a manner such that they should be
+**| applied together atomically to a given store. We call each such group of
+**| changes a transaction. We handle three different grades of transaction,
+**| but they differ only in semantic significance to the application, and are
+**| not intended to nest. (If small transactions were nested inside large
+**| transactions, that would imply that a single large transaction must be
+**| atomic over all the contained small transactions; but actually we intend
+**| smalls transaction never be undone once committed due to, say, aborting a
+**| transaction of greater significance.) The small, large, and session level
+**| commits have equal granularity, and differ only in risk of loss from the
+**| perspective of an application. Small commits characterize changes that
+**| can be lost with relatively small risk, so small transactions can delay
+**| until later if they are expensive or impractical to commit. Large commits
+**| involve changes that would probably inconvenience users if lost, so the
+**| need to pay costs of writing is rather greater than with small commits.
+**| Session commits are last ditch attempts to save outstanding changes before
+**| stopping the use of a particular database, so there will be no later point
+**| in time to save changes that have been delayed due to possible high cost.
+**| If large commits are never delayed, then a session commit has about the
+**| same performance effect as another large commit; but if small and large
+**| commits are always delayed, then a session commit is likely to be rather
+**| expensive as a runtime cost compared to any earlier database usage.
+**|
+**|| aborts: the only way to abort changes to a store is by closing the store.
+**| So there is no specific method for causing any abort. Stores must discard
+**| all changes made that are uncommitted when a store is closed. This design
+**| choice makes the implementations of tables, rows, and cells much less
+**| complex because they need not maintain a record of undobable changes. When
+**| a store is closed, presumably this precipitates the closure of all tables,
+**| rows, and cells in the store as well. So an application can revert the
+**| state of a store in the user interface by quietly closing and reopening a
+**| store, because this will discard uncommitted changes and show old content.
+**| This implies an app that closes a store will need to send a "scramble"
+**| event notification to any views that depend on old discarded content.
+|*/
+
+#define NS_IMDBSTORE_IID_STR "74d6218d-44b0-43b5-9ebe-69a17dfb562c"
+#define NS_IMDBSTORE_IID \
+ { \
+ 0x74d6218d, 0x44b0, 0x43b5, { \
+ 0x9e, 0xbe, 0x69, 0xa1, 0x7d, 0xfb, 0x56, 0x2c \
+ } \
+ }
+
+class nsIMdbStore : public nsIMdbPort {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBSTORE_IID)
+
+ // { ===== begin nsIMdbStore methods =====
+
+ // { ----- begin table methods -----
+ NS_IMETHOD NewTable( // make one new table of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) = 0; // acquire scoped collection of rows
+
+ NS_IMETHOD NewTableWithOid( // make one new table of specific type
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // caller assigned oid
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) = 0; // acquire scoped collection of rows
+ // } ----- end table methods -----
+
+ // { ----- begin row scope methods -----
+ NS_IMETHOD RowScopeHasAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) =
+ 0; // nonzero if store db assigned specified
+
+ NS_IMETHOD SetCallerAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) =
+ 0; // nonzero if store db assigned specified
+
+ NS_IMETHOD SetStoreAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) =
+ 0; // nonzero if store db assigned specified
+ // } ----- end row scope methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD NewRowWithOid(nsIMdbEnv* ev, // new row w/ caller assigned oid
+ const mdbOid* inOid, // caller assigned oid
+ nsIMdbRow** acqRow) = 0; // create new row
+
+ NS_IMETHOD NewRow(nsIMdbEnv* ev, // new row with db assigned oid
+ mdb_scope inRowScope, // row scope for row ids
+ nsIMdbRow** acqRow) = 0; // create new row
+ // Note this row must be added to some table or cell child before the
+ // store is closed in order to make this row persist across sessions.
+
+ // } ----- end row methods -----
+
+ // { ----- begin import/export methods -----
+ NS_IMETHOD ImportContent( // import content from port
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // scope for rows (or zero for all?)
+ nsIMdbPort* ioPort, // the port with content to add to store
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental import
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the import will be finished.
+
+ NS_IMETHOD ImportFile( // import content from port
+ nsIMdbEnv* ev, // context
+ nsIMdbFile* ioFile, // the file with content to add to store
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental import
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the import will be finished.
+ // } ----- end import/export methods -----
+
+ // { ----- begin hinting methods -----
+ NS_IMETHOD
+ ShareAtomColumnsHint( // advise re shared column content atomizing
+ nsIMdbEnv* ev, // context
+ mdb_scope inScopeHint, // zero, or suggested shared namespace
+ const mdbColumnSet* inColumnSet) = 0; // cols desired tokenized together
+
+ NS_IMETHOD
+ AvoidAtomColumnsHint( // advise column with poor atomizing prospects
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet) =
+ 0; // cols with poor atomizing prospects
+ // } ----- end hinting methods -----
+
+ // { ----- begin commit methods -----
+ NS_IMETHOD LargeCommit( // save important changes if at all possible
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ NS_IMETHOD SessionCommit( // save all changes if large commits delayed
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ NS_IMETHOD
+ CompressCommit( // commit and make db physically smaller if possible
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) = 0; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ // } ----- end commit methods -----
+
+ // } ===== end nsIMdbStore methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbStore, NS_IMDBSTORE_IID)
+
+/*| nsIMdbCursor: base cursor class for iterating row cells and table rows
+**|
+**|| count: the number of elements in the collection (table or row)
+**|
+**|| seed: the change count in the underlying collection, which is synced
+**| with the collection when the iteration position is set, and henceforth
+**| acts to show whether the iter has lost collection synchronization, in
+**| case it matters to clients whether any change happens during iteration.
+**|
+**|| pos: the position of the current element in the collection. Negative
+**| means a position logically before the first element. A positive value
+**| equal to count (or larger) implies a position after the last element.
+**| To iterate over all elements, set the position to negative, so subsequent
+**| calls to any 'next' method will access the first collection element.
+**|
+**|| doFailOnSeedOutOfSync: whether a cursor should return an error if the
+**| cursor's snapshot of a table's seed becomes stale with respect the table's
+**| current seed value (which implies the iteration is less than total) in
+**| between to cursor calls that actually access collection content. By
+**| default, a cursor should assume this attribute is false until specified,
+**| so that iterations quietly try to re-sync when they lose coherence.
+|*/
+
+#define NS_IMDBCURSOR_IID_STR "a0c37337-6ebc-474c-90db-e65ea0b850aa"
+
+#define NS_IMDBCURSOR_IID \
+ { \
+ 0xa0c37337, 0x6ebc, 0x474c, { \
+ 0x90, 0xdb, 0xe6, 0x5e, 0xa0, 0xb8, 0x50, 0xaa \
+ } \
+ }
+
+class nsIMdbCursor : public nsISupports { // collection iterator
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBCURSOR_IID)
+ // { ===== begin nsIMdbCursor methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetCount(nsIMdbEnv* ev, mdb_count* outCount) = 0; // readonly
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev, mdb_seed* outSeed) = 0; // readonly
+
+ NS_IMETHOD SetPos(nsIMdbEnv* ev, mdb_pos inPos) = 0; // mutable
+ NS_IMETHOD GetPos(nsIMdbEnv* ev, mdb_pos* outPos) = 0;
+
+ NS_IMETHOD SetDoFailOnSeedOutOfSync(nsIMdbEnv* ev, mdb_bool inFail) = 0;
+ NS_IMETHOD GetDoFailOnSeedOutOfSync(nsIMdbEnv* ev, mdb_bool* outFail) = 0;
+ // } ----- end attribute methods -----
+
+ // } ===== end nsIMdbCursor methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbCursor, NS_IMDBCURSOR_IID)
+
+#define NS_IMDBPORTTABLECURSOR_IID_STR = "f181a41e-933d-49b3-af93-20d3634b8b78"
+
+#define NS_IMDBPORTTABLECURSOR_IID \
+ { \
+ 0xf181a41e, 0x933d, 0x49b3, { \
+ 0xaf, 0x93, 0x20, 0xd3, 0x63, 0x4b, 0x8b, 0x78 \
+ } \
+ }
+
+/*| nsIMdbPortTableCursor: cursor class for iterating port tables
+**|
+**|| port: the cursor is associated with a specific port, which can be
+**| set to a different port (which resets the position to -1 so the
+**| next table acquired is the first in the port.
+**|
+|*/
+class nsIMdbPortTableCursor : public nsISupports { // table collection iterator
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBPORTTABLECURSOR_IID)
+ // { ===== begin nsIMdbPortTableCursor methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetPort(nsIMdbEnv* ev, nsIMdbPort* ioPort) = 0; // sets pos to -1
+ NS_IMETHOD GetPort(nsIMdbEnv* ev, nsIMdbPort** acqPort) = 0;
+
+ NS_IMETHOD SetRowScope(nsIMdbEnv* ev, // sets pos to -1
+ mdb_scope inRowScope) = 0;
+ NS_IMETHOD GetRowScope(nsIMdbEnv* ev, mdb_scope* outRowScope) = 0;
+ // setting row scope to zero iterates over all row scopes in port
+
+ NS_IMETHOD SetTableKind(nsIMdbEnv* ev, // sets pos to -1
+ mdb_kind inTableKind) = 0;
+ NS_IMETHOD GetTableKind(nsIMdbEnv* ev, mdb_kind* outTableKind) = 0;
+ // setting table kind to zero iterates over all table kinds in row scope
+ // } ----- end attribute methods -----
+
+ // { ----- begin table iteration methods -----
+ NS_IMETHOD NextTable( // get table at next position in the db
+ nsIMdbEnv* ev, // context
+ nsIMdbTable** acqTable) = 0; // the next table in the iteration
+ // } ----- end table iteration methods -----
+
+ // } ===== end nsIMdbPortTableCursor methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbPortTableCursor, NS_IMDBPORTTABLECURSOR_IID)
+
+/*| nsIMdbCollection: an object that collects a set of other objects as members.
+**| The main purpose of this base class is to unify the perceived semantics
+**| of tables and rows where their collection behavior is similar. This helps
+**| isolate the mechanics of collection behavior from the other semantics that
+**| are more characteristic of rows and tables.
+**|
+**|| count: the number of objects in a collection is the member count. (Some
+**| collection interfaces call this attribute the 'size', but that can be a
+**| little ambiguous, and counting actual members is harder to confuse.)
+**|
+**|| seed: the seed of a collection is a counter for changes in membership in
+**| a specific collection. This seed should change when members are added to
+**| or removed from a collection, but not when a member changes internal state.
+**| The seed should also change whenever the internal collection of members has
+**| a complex state change that reorders member positions (say by sorting) that
+**| would affect the nature of an iteration over that collection of members.
+**| The purpose of a seed is to inform any outstanding collection cursors that
+**| they might be stale, without incurring the cost of broadcasting an event
+**| notification to such cursors, which would need more data structure support.
+**| Presumably a cursor in a particular mdb code suite has much more direct
+**| access to a collection seed member slot that this abstract COM interface,
+**| so this information is intended more for clients outside mdb that want to
+**| make inferences similar to those made by the collection cursors. The seed
+**| value as an integer magnitude is not very important, and callers should not
+**| assume meaningful information can be derived from an integer value beyond
+**| whether it is equal or different from a previous inspection. A seed uses
+**| integers of many bits in order to make the odds of wrapping and becoming
+**| equal to an earlier seed value have probability that is vanishingly small.
+**|
+**|| port: every collection is associated with a specific database instance.
+**|
+**|| cursor: a subclass of nsIMdbCursor suitable for this specific collection
+**| subclass. The ability to GetCursor() from the base nsIMdbCollection class
+**| is not really as useful as getting a more specifically typed cursor more
+**| directly from the base class without any casting involved. So including
+**| this method here is more for conceptual illustration.
+**|
+**|| oid: every collection has an identity that persists from session to
+**| session. Implementations are probably able to distinguish row IDs from
+**| table IDs, but we don't specify anything official in this regard. A
+**| collection has the same identity for the lifetime of the collection,
+**| unless identity is swapped with another collection by means of a call to
+**| BecomeContent(), which is considered a way to swap a new representation
+**| for an old well-known object. (Even so, only content appears to change,
+**| while the identity seems to stay the same.)
+**|
+**|| become: developers can effectively cause two objects to swap identities,
+**| in order to effect a complete swap between what persistent content is
+**| represented by two oids. The caller should consider this a content swap,
+**| and not identity wap, because identities will seem to stay the same while
+**| only content changes. However, implementations will likely do this
+**| internally by swapping identities. Callers must swap content only
+**| between objects of similar type, such as a row with another row, and a
+**| table with another table, because implementations need not support
+**| cross-object swapping because it might break object name spaces.
+**|
+**|| dropping: when a caller expects a row or table will no longer be used, the
+**| caller can tell the collection to 'drop activity', which means the runtime
+**| object can have its internal representation purged to save memory or any
+**| other resource that is being consumed by the collection's representation.
+**| This has no effect on the collection's persistent content or semantics,
+**| and is only considered a runtime effect. After a collection drops
+**| activity, the object should still be as usable as before (because it has
+**| NOT been closed), but further usage can be expensive to re-instate because
+**| it might involve reallocating space and/or re-reading disk space. But
+**| since this future usage is not expected, the caller does not expect to
+**| pay the extra expense. An implementation can choose to implement
+**| 'dropping activity' in different ways, or even not at all if this
+**| operation is not really feasible. Callers cannot ask objects whether they
+**| are 'dropped' or not, so this should be transparent. (Note that
+**| implementors might fear callers do not really know whether future
+**| usage will occur, and therefore might delay the act of dropping until
+**| the near future, until seeing whether the object is used again
+**| immediately elsewhere. Such use soon after the drop request might cause
+**| the drop to be cancelled.)
+|*/
+class nsIMdbCollection : public nsISupports { // sequence of objects
+ public:
+ // { ===== begin nsIMdbCollection methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev,
+ mdb_seed* outSeed) = 0; // member change count
+ NS_IMETHOD GetCount(nsIMdbEnv* ev,
+ mdb_count* outCount) = 0; // member count
+
+ NS_IMETHOD GetPort(nsIMdbEnv* ev,
+ nsIMdbPort** acqPort) = 0; // collection container
+ // } ----- end attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetCursor( // make a cursor starting iter at inMemberPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inMemberPos, // zero-based ordinal pos of member in collection
+ nsIMdbCursor** acqCursor) = 0; // acquire new cursor instance
+ // } ----- end cursor methods -----
+
+ // { ----- begin ID methods -----
+ NS_IMETHOD GetOid(nsIMdbEnv* ev,
+ mdbOid* outOid) = 0; // read object identity
+ NS_IMETHOD BecomeContent(nsIMdbEnv* ev,
+ const mdbOid* inOid) = 0; // exchange content
+ // } ----- end ID methods -----
+
+ // { ----- begin activity dropping methods -----
+ NS_IMETHOD DropActivity( // tell collection usage no longer expected
+ nsIMdbEnv* ev) = 0;
+ // } ----- end activity dropping methods -----
+
+ // } ===== end nsIMdbCollection methods =====
+};
+
+/*| nsIMdbTable: an ordered collection of rows
+**|
+**|| row scope: an integer token for an atomized string in this database
+**| that names a space for row IDs. This attribute of a table is intended
+**| as guidance metainformation that helps with searching a database for
+**| tables that operate on collections of rows of the specific type. By
+**| convention, a table with a specific row scope is expected to focus on
+**| containing rows that belong to that scope, however exceptions are easily
+**| allowed because all rows in a table are known by both row ID and scope.
+**| (A table with zero row scope is never allowed because this would make it
+**| ambiguous to use a zero row scope when iterating over tables in a port to
+**| indicate that all row scopes should be seen by a cursor.)
+**|
+**|| table kind: an integer token for an atomized string in this database
+**| that names a kind of table as a subset of the associated row scope. This
+**| attribute is intended as guidance metainformation to clarify the role of
+**| this table with respect to other tables in the same row scope, and this
+**| also helps search for such tables in a database. By convention, a table
+**| with a specific table kind has a consistent role for containing rows with
+**| respect to other collections of such rows in the same row scope. Also by
+**| convention, at least one table in a row scope has a table kind purporting
+**| to contain ALL the rows that belong in that row scope, so that at least
+**| one table exists that allows all rows in a scope to be iterated over.
+**| (A table with zero table kind is never allowed because this would make it
+**| ambiguous to use a zero table kind when iterating over tables in a port to
+**| indicate that all table kinds in a row scope should be seen by a cursor.)
+**|
+**|| port: every table is considered part of some port that contains the
+**| table, so that closing the containing port will cause the table to be
+**| indirectly closed as well. We make it easy to get the containing port for
+**| a table, because the port supports important semantic interfaces that will
+**| affect how content in table is presented; the most important port context
+**| that affects a table is specified by the set of token to string mappings
+**| that affect all tokens used throughout the database, and which drive the
+**| meanings of row scope, table kind, cell columns, etc.
+**|
+**|| cursor: a cursor that iterates over the rows in this table, where rows
+**| have zero-based index positions from zero to count-1. Making a cursor
+**| with negative position will next iterate over the first row in the table.
+**|
+**|| position: given any position from zero to count-1, a table will return
+**| the row ID and row scope for the row at that position. (One can use the
+**| GetRowAllCells() method to read that row, or else use a row cursor to both
+**| get the row at some position and read its content at the same time.) The
+**| position depends on whether a table is sorted, and upon the actual sort.
+**| Note that moving a row's position is only possible in unsorted tables.
+**|
+**|| row set: every table contains a collection of rows, where a member row is
+**| referenced by the table using the row ID and row scope for the row. No
+**| single table owns a given row instance, because rows are effectively ref-
+**| counted and destroyed only when the last table removes a reference to that
+**| particular row. (But a row can be emptied of all content no matter how
+**| many refs exist, and this might be the next best thing to destruction.)
+**| Once a row exists in a least one table (after NewRow() is called), then it
+**| can be added to any other table by calling AddRow(), or removed from any
+**| table by calling CutRow(), or queried as a member by calling HasRow(). A
+**| row can only be added to a table once, and further additions do nothing and
+**| complain not at all. Cutting a row from a table only does something when
+**| the row was actually a member, and otherwise does nothing silently.
+**|
+**|| row ref count: one can query the number of tables (and/or cells)
+**| containing a row as a member or a child.
+**|
+**|| row content: one can access or modify the cell content in a table's row
+**| by moving content to or from an instance of nsIMdbRow. Note that nsIMdbRow
+**| never represents the actual row inside a table, and this is the reason
+**| why nsIMdbRow instances do not have row IDs or row scopes. So an instance
+**| of nsIMdbRow always and only contains a snapshot of some or all content in
+**| past, present, or future persistent row inside a table. This means that
+**| reading and writing rows in tables has strictly copy semantics, and we
+**| currently do not plan any exceptions for specific performance reasons.
+**|
+**|| sorting: note all rows are assumed sorted by row ID as a secondary
+**| sort following the primary column sort, when table rows are sorted.
+**|
+**|| indexes:
+|*/
+
+#define NS_IMDBTABLE_IID_STR = "fe11bc98-d02b-4128-9fac-87042fdf9639"
+
+#define NS_IMDBTABLE_IID \
+ { \
+ 0xfe11bc98, 0xd02b, 0x4128, { \
+ 0x9f, 0xac, 0x87, 0x04, 0x2f, 0xdf, 0x96, 0x39 \
+ } \
+ }
+
+class nsIMdbTable : public nsIMdbCollection { // a collection of rows
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBTABLE_IID)
+ // { ===== begin nsIMdbTable methods =====
+
+ // { ----- begin meta attribute methods -----
+ NS_IMETHOD SetTablePriority(nsIMdbEnv* ev, mdb_priority inPrio) = 0;
+ NS_IMETHOD GetTablePriority(nsIMdbEnv* ev, mdb_priority* outPrio) = 0;
+
+ NS_IMETHOD GetTableBeVerbose(nsIMdbEnv* ev, mdb_bool* outBeVerbose) = 0;
+ NS_IMETHOD SetTableBeVerbose(nsIMdbEnv* ev, mdb_bool inBeVerbose) = 0;
+
+ NS_IMETHOD GetTableIsUnique(nsIMdbEnv* ev, mdb_bool* outIsUnique) = 0;
+
+ NS_IMETHOD GetTableKind(nsIMdbEnv* ev, mdb_kind* outTableKind) = 0;
+ NS_IMETHOD GetRowScope(nsIMdbEnv* ev, mdb_scope* outRowScope) = 0;
+
+ NS_IMETHOD GetMetaRow(
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ mdbOid* outOid, // output meta row oid, can be nil to suppress output
+ nsIMdbRow** acqRow) = 0; // acquire table's unique singleton meta row
+ // The purpose of a meta row is to support the persistent recording of
+ // meta info about a table as cells put into the distinguished meta row.
+ // Each table has exactly one meta row, which is not considered a member
+ // of the collection of rows inside the table. The only way to tell
+ // whether a row is a meta row is by the fact that it is returned by this
+ // GetMetaRow() method from some table. Otherwise nothing distinguishes
+ // a meta row from any other row. A meta row can be used anyplace that
+ // any other row can be used, and can even be put into other tables (or
+ // the same table) as a table member, if this is useful for some reason.
+ // The first attempt to access a table's meta row using GetMetaRow() will
+ // cause the meta row to be created if it did not already exist. When the
+ // meta row is created, it will have the row oid that was previously
+ // requested for this table's meta row; or if no oid was ever explicitly
+ // specified for this meta row, then a unique oid will be generated in
+ // the row scope named "m" (so obviously MDB clients should not
+ // manually allocate any row IDs from that special meta scope namespace).
+ // The meta row oid can be specified either when the table is created, or
+ // else the first time that GetMetaRow() is called, by passing a non-nil
+ // pointer to an oid for parameter inOptionalMetaRowOid. The meta row's
+ // actual oid is returned in outOid (if this is a non-nil pointer), and
+ // it will be different from inOptionalMetaRowOid when the meta row was
+ // already given a different oid earlier.
+ // } ----- end meta attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD
+ GetTableRowCursor( // make a cursor, starting iteration at inRowPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbTableRowCursor** acqCursor) = 0; // acquire new cursor instance
+ // } ----- end row position methods -----
+
+ // { ----- begin row position methods -----
+ NS_IMETHOD PosToOid( // get row member for a table position
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ mdbOid* outOid) = 0; // row oid at the specified position
+
+ NS_IMETHOD OidToPos( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_pos* outPos) = 0; // zero-based ordinal position of row in table
+
+ NS_IMETHOD PosToRow( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbRow** acqRow) = 0; // acquire row at table position inRowPos
+
+ NS_IMETHOD RowToPos( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_pos* outPos) = 0; // zero-based ordinal position of row in table
+ // } ----- end row position methods -----
+
+ // { ----- begin oid set methods -----
+ NS_IMETHOD AddOid( // make sure the row with inOid is a table member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid) = 0; // row to ensure membership in table
+
+ NS_IMETHOD HasOid( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_bool* outHasOid) = 0; // whether inOid is a member row
+
+ NS_IMETHOD CutOid( // make sure the row with inOid is not a member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid) = 0; // row to remove from table
+ // } ----- end oid set methods -----
+
+ // { ----- begin row set methods -----
+ NS_IMETHOD NewRow( // create a new row instance in table
+ nsIMdbEnv* ev, // context
+ mdbOid*
+ ioOid, // please use minus one (unbound) rowId for db-assigned IDs
+ nsIMdbRow** acqRow) = 0; // create new row
+
+ NS_IMETHOD AddRow( // make sure the row with inOid is a table member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow) = 0; // row to ensure membership in table
+
+ NS_IMETHOD HasRow( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_bool* outHasRow) = 0; // whether row is a table member
+
+ NS_IMETHOD CutRow( // make sure the row with inOid is not a member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow) = 0; // row to remove from table
+
+ NS_IMETHOD CutAllRows( // remove all rows from the table
+ nsIMdbEnv* ev) = 0; // context
+ // } ----- end row set methods -----
+
+ // { ----- begin hinting methods -----
+ NS_IMETHOD SearchColumnsHint( // advise re future expected search cols
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet) = 0; // columns likely to be searched
+
+ NS_IMETHOD SortColumnsHint( // advise re future expected sort columns
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet) = 0; // columns for likely sort requests
+
+ NS_IMETHOD StartBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* ev, // context
+ const void* inLabel) = 0; // intend unique address to match end call
+ // If batch starts nest by virtue of nesting calls in the stack, then
+ // the address of a local variable makes a good batch start label that
+ // can be used at batch end time, and such addresses remain unique.
+
+ NS_IMETHOD EndBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* ev, // context
+ const void* inLabel) = 0; // label matching start label
+ // Suppose a table is maintaining one or many sort orders for a table,
+ // so that every row added to the table must be inserted in each sort,
+ // and every row cut must be removed from each sort. If a db client
+ // intends to make many such changes before needing any information
+ // about the order or positions of rows inside a table, then a client
+ // might tell the table to start batch changes in order to disable
+ // sorting of rows for the interim. Presumably a table will then do
+ // a full sort of all rows at need when the batch changes end, or when
+ // a surprise request occurs for row position during batch changes.
+ // } ----- end hinting methods -----
+
+ // { ----- begin searching methods -----
+ NS_IMETHOD FindRowMatches( // search variable number of sorted cols
+ nsIMdbEnv* ev, // context
+ const mdbYarn*
+ inPrefix, // content to find as prefix in row's column cell
+ nsIMdbTableRowCursor** acqCursor) = 0; // set of matching rows
+
+ NS_IMETHOD GetSearchColumns( // query columns used by FindRowMatches()
+ nsIMdbEnv* ev, // context
+ mdb_count* outCount, // context
+ mdbColumnSet* outColSet) = 0; // caller supplied space to put columns
+ // GetSearchColumns() returns the columns actually searched when the
+ // FindRowMatches() method is called. No more than mColumnSet_Count
+ // slots of mColumnSet_Columns will be written, since mColumnSet_Count
+ // indicates how many slots are present in the column array. The
+ // actual number of search column used by the table is returned in
+ // the outCount parameter; if this number exceeds mColumnSet_Count,
+ // then a caller needs a bigger array to read the entire column set.
+ // The minimum of mColumnSet_Count and outCount is the number slots
+ // in mColumnSet_Columns that were actually written by this method.
+ //
+ // Callers are expected to change this set of columns by calls to
+ // nsIMdbTable::SearchColumnsHint() or SetSearchSorting(), or both.
+ // } ----- end searching methods -----
+
+ // { ----- begin sorting methods -----
+ // sorting: note all rows are assumed sorted by row ID as a secondary
+ // sort following the primary column sort, when table rows are sorted.
+
+ NS_IMETHOD
+ CanSortColumn( // query which column is currently used for sorting
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to query sorting potential
+ mdb_bool* outCanSort) = 0; // whether the column can be sorted
+
+ NS_IMETHOD GetSorting( // view same table in particular sorting
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // requested new column for sorting table
+ nsIMdbSorting** acqSorting) = 0; // acquire sorting for column
+
+ NS_IMETHOD SetSearchSorting( // use this sorting in FindRowMatches()
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // often same as nsIMdbSorting::GetSortColumn()
+ nsIMdbSorting* ioSorting) = 0; // requested sorting for some column
+ // SetSearchSorting() attempts to inform the table that ioSorting
+ // should be used during calls to FindRowMatches() for searching
+ // the column which is actually sorted by ioSorting. This method
+ // is most useful in conjunction with nsIMdbSorting::SetCompare(),
+ // because otherwise a caller would not be able to override the
+ // comparison ordering method used during searches. Note that some
+ // database implementations might be unable to use an arbitrarily
+ // specified sort order, either due to schema or runtime interface
+ // constraints, in which case ioSorting might not actually be used.
+ // Presumably ioSorting is an instance that was returned from some
+ // earlier call to nsIMdbTable::GetSorting(). A caller can also
+ // use nsIMdbTable::SearchColumnsHint() to specify desired change
+ // in which columns are sorted and searched by FindRowMatches().
+ //
+ // A caller can pass a nil pointer for ioSorting to request that
+ // column inColumn no longer be used at all by FindRowMatches().
+ // But when ioSorting is non-nil, then inColumn should match the
+ // column actually sorted by ioSorting; when these do not agree,
+ // implementations are instructed to give precedence to the column
+ // specified by ioSorting (so this means callers might just pass
+ // zero for inColumn when ioSorting is also provided, since then
+ // inColumn is both redundant and ignored).
+ // } ----- end sorting methods -----
+
+ // { ----- begin moving methods -----
+ // moving a row does nothing unless a table is currently unsorted
+
+ NS_IMETHOD MoveOid( // change position of row in unsorted table
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row inRowId
+ mdb_pos* outActualPos) = 0; // actual new position of row in table
+
+ NS_IMETHOD MoveRow( // change position of row in unsorted table
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row inRowId
+ mdb_pos* outActualPos) = 0; // actual new position of row in table
+ // } ----- end moving methods -----
+
+ // { ----- begin index methods -----
+ NS_IMETHOD AddIndex( // create a sorting index for column if possible
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to sort by index
+ nsIMdbThumb** acqThumb) =
+ 0; // acquire thumb for incremental index building
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the index addition will be finished.
+
+ NS_IMETHOD CutIndex( // stop supporting a specific column index
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column with index to be removed
+ nsIMdbThumb** acqThumb) =
+ 0; // acquire thumb for incremental index destroy
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the index removal will be finished.
+
+ NS_IMETHOD HasIndex( // query for current presence of a column index
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outHasIndex) = 0; // whether column has index for this column
+
+ NS_IMETHOD EnableIndexOnSort( // create an index for col on first sort
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) = 0; // the column to index if ever sorted
+
+ NS_IMETHOD QueryIndexOnSort( // check whether index on sort is enabled
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outIndexOnSort) =
+ 0; // whether column has index-on-sort enabled
+
+ NS_IMETHOD DisableIndexOnSort( // prevent future index creation on sort
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) = 0; // the column to index if ever sorted
+ // } ----- end index methods -----
+
+ // } ===== end nsIMdbTable methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbTable, NS_IMDBTABLE_IID)
+
+/*| nsIMdbSorting: a view of a table in some particular sort order. This
+**| row order closely resembles a readonly array of rows with the same row
+**| membership as the underlying table, but in a different order than the
+**| table's explicit row order. But the sorting's row membership changes
+**| whenever the table's membership changes (without any notification, so
+**| keep this in mind when modifying the table).
+**|
+**|| table: every sorting is associated with a particular table. You
+**| cannot change which table is used by a sorting (just ask some new
+**| table for a suitable sorting instance instead).
+**|
+**|| compare: the ordering method used by a sorting, wrapped up in a
+**| abstract plug-in interface. When this was never installed by an
+**| explicit call to SetNewCompare(), a compare object is still returned,
+**| and it might match the compare instance returned by the factory method
+**| nsIMdbFactory::MakeCompare(), which represents a default sort order
+**| (which we fervently hope is consistently ASCII byte ordering).
+**|
+**|| cursor: in case callers are more comfortable with a cursor style
+**| of accessing row members, each sorting will happily return a cursor
+**| instance with behavior very similar to a cursor returned from a call
+**| to nsIMdbTable::GetTableRowCursor(), but with different row order.
+**| A cursor should show exactly the same information as the pos methods.
+**|
+**|| pos: the PosToOid() and PosToRow() methods are just like the table
+**| methods of the same name, except they show rows in the sort order of
+**| the sorting, rather than that of the table. These methods are like
+**| readonly array position accessor's, or like a C++ operator[].
+|*/
+class nsIMdbSorting : public nsIMdbObject { // sorting of some table
+ public:
+ // { ===== begin nsIMdbSorting methods =====
+
+ // { ----- begin attribute methods -----
+ // sorting: note all rows are assumed sorted by row ID as a secondary
+ // sort following the primary column sort, when table rows are sorted.
+
+ NS_IMETHOD GetTable(nsIMdbEnv* ev, nsIMdbTable** acqTable) = 0;
+ NS_IMETHOD GetSortColumn( // query which col is currently sorted
+ nsIMdbEnv* ev, // context
+ mdb_column* outColumn) = 0; // col the table uses for sorting (or zero)
+
+ // } ----- end attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetSortingRowCursor( // make a cursor, starting at inRowPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbTableRowCursor** acqCursor) = 0; // acquire new cursor instance
+ // A cursor interface turning same info as PosToOid() or PosToRow().
+ // } ----- end row position methods -----
+
+ // { ----- begin row position methods -----
+ NS_IMETHOD PosToOid( // get row member for a table position
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ mdbOid* outOid) = 0; // row oid at the specified position
+
+ NS_IMETHOD PosToRow( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbRow** acqRow) = 0; // acquire row at table position inRowPos
+ // } ----- end row position methods -----
+
+ // } ===== end nsIMdbSorting methods =====
+};
+
+/*| nsIMdbTableRowCursor: cursor class for iterating table rows
+**|
+**|| table: the cursor is associated with a specific table, which can be
+**| set to a different table (which resets the position to -1 so the
+**| next row acquired is the first in the table.
+**|
+**|| NextRowId: the rows in the table can be iterated by identity alone,
+**| without actually reading the cells of any row with this method.
+**|
+**|| NextRowCells: read the next row in the table, but only read cells
+**| from the table which are already present in the row (so no new cells
+**| are added to the row, even if they are present in the table). All the
+**| cells will have content specified, even it is the empty string. No
+**| columns will be removed, even if missing from the row (because missing
+**| and empty are semantically equivalent).
+**|
+**|| NextRowAllCells: read the next row in the table, and access all the
+**| cells for this row in the table, adding any missing columns to the row
+**| as needed until all cells are represented. All the
+**| cells will have content specified, even it is the empty string. No
+**| columns will be removed, even if missing from the row (because missing
+**| and empty are semantically equivalent).
+**|
+|*/
+
+#define NS_IMDBTABLEROWCURSOR_IID_STR = "4f325dad-0385-4b62-a992-c914ab93587e"
+
+#define NS_IMDBTABLEROWCURSOR_IID \
+ { \
+ 0x4f325dad, 0x0385, 0x4b62, { \
+ 0xa9, 0x92, 0xc9, 0x14, 0xab, 0x93, 0x58, 0x7e \
+ } \
+ }
+
+class nsIMdbTableRowCursor : public nsISupports { // table row iterator
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBTABLEROWCURSOR_IID)
+
+ // { ===== begin nsIMdbTableRowCursor methods =====
+
+ // { ----- begin attribute methods -----
+ // NS_IMETHOD SetTable(nsIMdbEnv* ev, nsIMdbTable* ioTable) = 0; // sets pos
+ // to -1 Method SetTable() cut and made obsolete in keeping with new sorting
+ // methods.
+
+ NS_IMETHOD GetTable(nsIMdbEnv* ev, nsIMdbTable** acqTable) = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin duplicate row removal methods -----
+ NS_IMETHOD CanHaveDupRowMembers(nsIMdbEnv* ev, // cursor might hold dups?
+ mdb_bool* outCanHaveDups) = 0;
+
+ NS_IMETHOD MakeUniqueCursor( // clone cursor, removing duplicate rows
+ nsIMdbEnv* ev, // context
+ nsIMdbTableRowCursor** acqCursor) = 0; // acquire clone with no dups
+ // Note that MakeUniqueCursor() is never necessary for a cursor which was
+ // created by table method nsIMdbTable::GetTableRowCursor(), because a table
+ // never contains the same row as a member more than once. However, a cursor
+ // created by table method nsIMdbTable::FindRowMatches() might contain the
+ // same row more than once, because the same row can generate a hit by more
+ // than one column with a matching string prefix. Note this method can
+ // return the very same cursor instance with just an incremented refcount,
+ // when the original cursor could not contain any duplicate rows (calling
+ // CanHaveDupRowMembers() shows this case on a false return). Otherwise
+ // this method returns a different cursor instance. Callers should not use
+ // this MakeUniqueCursor() method lightly, because it tends to defeat the
+ // purpose of lazy programming techniques, since it can force creation of
+ // an explicit row collection in a new cursor's representation, in order to
+ // inspect the row membership and remove any duplicates; this can have big
+ // impact if a collection holds tens of thousands of rows or more, when
+ // the original cursor with dups simply referenced rows indirectly by row
+ // position ranges, without using an explicit row set representation.
+ // Callers are encouraged to use nsIMdbCursor::GetCount() to determine
+ // whether the row collection is very large (tens of thousands), and to
+ // delay calling MakeUniqueCursor() when possible, until a user interface
+ // element actually demands the creation of an explicit set representation.
+ // } ----- end duplicate row removal methods -----
+
+ // { ----- begin oid iteration methods -----
+ NS_IMETHOD NextRowOid( // get row id of next row in the table
+ nsIMdbEnv* ev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+ // } ----- end oid iteration methods -----
+
+ // { ----- begin row iteration methods -----
+ NS_IMETHOD NextRow( // get row cells from table for cells already in row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // acquire next row in table
+ mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+
+ NS_IMETHOD PrevRowOid( // get row id of previous row in the table
+ nsIMdbEnv* ev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+ // } ----- end oid iteration methods -----
+
+ // { ----- begin row iteration methods -----
+ NS_IMETHOD PrevRow( // get row cells from table for cells already in row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // acquire previous row in table
+ mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+
+ // } ----- end row iteration methods -----
+
+ // { ----- begin copy iteration methods -----
+ // NS_IMETHOD NextRowCopy( // put row cells into sink only when already in
+ // sink
+ // nsIMdbEnv* ev, // context
+ // nsIMdbRow* ioSinkRow, // sink for row cells read from next row
+ // mdbOid* outOid, // out row oid
+ // mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+ //
+ // NS_IMETHOD NextRowCopyAll( // put all row cells into sink, adding to sink
+ // nsIMdbEnv* ev, // context
+ // nsIMdbRow* ioSinkRow, // sink for row cells read from next row
+ // mdbOid* outOid, // out row oid
+ // mdb_pos* outRowPos) = 0; // zero-based position of the row in table
+ // } ----- end copy iteration methods -----
+
+ // } ===== end nsIMdbTableRowCursor methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbTableRowCursor, NS_IMDBTABLEROWCURSOR_IID)
+
+/*| nsIMdbRow: a collection of cells
+**|
+|*/
+
+#define NS_IMDBROW_IID_STR "271e8d6e-183a-40e3-9f18-36913b4c7853"
+
+#define NS_IMDBROW_IID \
+ { \
+ 0x271e8d6e, 0x183a, 0x40e3, { \
+ 0x9f, 0x18, 0x36, 0x91, 0x3b, 0x4c, 0x78, 0x53 \
+ } \
+ }
+
+class nsIMdbRow : public nsIMdbCollection { // cell tuple
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBROW_IID)
+ // { ===== begin nsIMdbRow methods =====
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetRowCellCursor( // make a cursor starting iteration at inCellPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inCellPos, // zero-based ordinal position of cell in row
+ nsIMdbRowCellCursor** acqCursor) = 0; // acquire new cursor instance
+ // } ----- end cursor methods -----
+
+ // { ----- begin column methods -----
+ NS_IMETHOD AddColumn( // make sure a particular column is inside row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to add
+ const mdbYarn* inYarn) = 0; // cell value to install
+
+ NS_IMETHOD CutColumn( // make sure a column is absent from the row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) = 0; // column to ensure absent from row
+
+ NS_IMETHOD CutAllColumns( // remove all columns from the row
+ nsIMdbEnv* ev) = 0; // context
+ // } ----- end column methods -----
+
+ // { ----- begin cell methods -----
+ NS_IMETHOD NewCell( // get cell for specified column, or add new one
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to add
+ nsIMdbCell** acqCell) = 0; // cell column and value
+
+ NS_IMETHOD AddCell( // copy a cell from another row to this row
+ nsIMdbEnv* ev, // context
+ const nsIMdbCell* inCell) = 0; // cell column and value
+
+ NS_IMETHOD GetCell( // find a cell in this row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to find
+ nsIMdbCell** acqCell) = 0; // cell for specified column, or null
+
+ NS_IMETHOD EmptyAllCells( // make all cells in row empty of content
+ nsIMdbEnv* ev) = 0; // context
+ // } ----- end cell methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD AddRow( // add all cells in another row to this one
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioSourceRow) = 0; // row to union with
+
+ NS_IMETHOD SetRow( // make exact duplicate of another row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioSourceRow) = 0; // row to duplicate
+ // } ----- end row methods -----
+
+ // { ----- begin blob methods -----
+ NS_IMETHOD SetCellYarn(nsIMdbEnv* ev, // synonym for AddColumn()
+ mdb_column inColumn, // column to write
+ const mdbYarn* inYarn) = 0; // reads from yarn slots
+ // make this text object contain content from the yarn's buffer
+
+ NS_IMETHOD GetCellYarn(nsIMdbEnv* ev,
+ mdb_column inColumn, // column to read
+ mdbYarn* outYarn) = 0; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+
+ NS_IMETHOD AliasCellYarn(nsIMdbEnv* ev,
+ mdb_column inColumn, // column to alias
+ mdbYarn* outYarn) = 0; // writes ALL yarn slots
+
+ NS_IMETHOD NextCellYarn(nsIMdbEnv* ev, // iterative version of GetCellYarn()
+ mdb_column* ioColumn, // next column to read
+ mdbYarn* outYarn) = 0; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+ //
+ // The ioColumn argument is an inout parameter which initially contains the
+ // last column accessed and returns the next column corresponding to the
+ // content read into the yarn. Callers should start with a zero column
+ // value to say 'no previous column', which causes the first column to be
+ // read. Then the value returned in ioColumn is perfect for the next call
+ // to NextCellYarn(), since it will then be the previous column accessed.
+ // Callers need only examine the column token returned to see which cell
+ // in the row is being read into the yarn. When no more columns remain,
+ // and the iteration has ended, ioColumn will return a zero token again.
+ // So iterating over cells starts and ends with a zero column token.
+
+ NS_IMETHOD SeekCellYarn( // resembles nsIMdbRowCellCursor::SeekCell()
+ nsIMdbEnv* ev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ mdbYarn* outYarn) = 0; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+ // Callers can pass nil for outYarn to indicate no interest in content, so
+ // only the outColumn value is returned. NOTE to subclasses: you must be
+ // able to ignore outYarn when the pointer is nil; please do not crash.
+
+ // } ----- end blob methods -----
+
+ // } ===== end nsIMdbRow methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbRow, NS_IMDBROW_IID)
+
+/*| nsIMdbRowCellCursor: cursor class for iterating row cells
+**|
+**|| row: the cursor is associated with a specific row, which can be
+**| set to a different row (which resets the position to -1 so the
+**| next cell acquired is the first in the row.
+**|
+**|| NextCell: get the next cell in the row and return its position and
+**| a new instance of a nsIMdbCell to represent this next cell.
+|*/
+
+#define NS_IMDBROWCELLCURSOR_IID_STR "b33371a7-5d63-4d10-85a8-e44dffe75c28"
+
+#define NS_IMDBROWCELLCURSOR_IID \
+ { \
+ 0x271e8d6e, 0x5d63, 0x4d10, { \
+ 0x85, 0xa8, 0xe4, 0x4d, 0xff, 0xe7, 0x5c, 0x28 \
+ } \
+ }
+
+class nsIMdbRowCellCursor : public nsISupports { // cell collection iterator
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBROWCELLCURSOR_IID)
+ // { ===== begin nsIMdbRowCellCursor methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetRow(nsIMdbEnv* ev, nsIMdbRow* ioRow) = 0; // sets pos to -1
+ NS_IMETHOD GetRow(nsIMdbEnv* ev, nsIMdbRow** acqRow) = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin cell seeking methods -----
+ NS_IMETHOD SeekCell(nsIMdbEnv* ev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ nsIMdbCell** acqCell) = 0; // the cell at inPos
+ // } ----- end cell seeking methods -----
+
+ // { ----- begin cell iteration methods -----
+ NS_IMETHOD NextCell( // get next cell in the row
+ nsIMdbEnv* ev, // context
+ nsIMdbCell** acqCell, // changes to the next cell in the iteration
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos) = 0; // position of cell in row sequence
+
+ NS_IMETHOD PickNextCell( // get next cell in row within filter set
+ nsIMdbEnv* ev, // context
+ nsIMdbCell* ioCell, // changes to the next cell in the iteration
+ const mdbColumnSet* inFilterSet, // col set of actual caller interest
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos) = 0; // position of cell in row sequence
+
+ // Note that inFilterSet should not have too many (many more than 10?)
+ // cols, since this might imply a potential excessive consumption of time
+ // over many cursor calls when looking for column and filter intersection.
+ // } ----- end cell iteration methods -----
+
+ // } ===== end nsIMdbRowCellCursor methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbRowCellCursor, NS_IMDBROWCELLCURSOR_IID)
+
+/*| nsIMdbBlob: a base class for objects composed mainly of byte sequence state.
+**| (This provides a base class for nsIMdbCell, so that cells themselves can
+**| be used to set state in another cell, without extracting a buffer.)
+|*/
+class nsIMdbBlob : public nsISupports { // a string with associated charset
+ public:
+ // { ===== begin nsIMdbBlob methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetBlob(nsIMdbEnv* ev,
+ nsIMdbBlob* ioBlob) = 0; // reads inBlob slots
+ // when inBlob is in the same suite, this might be fastest cell-to-cell
+
+ NS_IMETHOD ClearBlob( // make empty (so content has zero length)
+ nsIMdbEnv* ev) = 0;
+ // clearing a yarn is like SetYarn() with empty yarn instance content
+
+ NS_IMETHOD GetBlobFill(nsIMdbEnv* ev,
+ mdb_fill* outFill) = 0; // size of blob
+ // Same value that would be put into mYarn_Fill, if one called GetYarn()
+ // with a yarn instance that had mYarn_Buf==nil and mYarn_Size==0.
+
+ NS_IMETHOD SetYarn(nsIMdbEnv* ev,
+ const mdbYarn* inYarn) = 0; // reads from yarn slots
+ // make this text object contain content from the yarn's buffer
+
+ NS_IMETHOD GetYarn(nsIMdbEnv* ev,
+ mdbYarn* outYarn) = 0; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+
+ NS_IMETHOD AliasYarn(nsIMdbEnv* ev,
+ mdbYarn* outYarn) = 0; // writes ALL yarn slots
+ // AliasYarn() reveals sensitive internal text buffer state to the caller
+ // by setting mYarn_Buf to point into the guts of this text implementation.
+ //
+ // The caller must take great care to avoid writing on this space, and to
+ // avoid calling any method that would cause the state of this text object
+ // to change (say by directly or indirectly setting the text to hold more
+ // content that might grow the size of the buffer and free the old buffer).
+ // In particular, callers should scrupulously avoid making calls into the
+ // mdb interface to write any content while using the buffer pointer found
+ // in the returned yarn instance. Best safe usage involves copying content
+ // into some other kind of external content representation beyond mdb.
+ //
+ // (The original design of this method a week earlier included the concept
+ // of very fast and efficient cooperative locking via a pointer to some lock
+ // member slot. But let's ignore that complexity in the current design.)
+ //
+ // AliasYarn() is specifically intended as the first step in transferring
+ // content from nsIMdbBlob to a nsString representation, without forcing extra
+ // allocations and/or memory copies. (A standard nsIMdbBlob_AsString() utility
+ // will use AliasYarn() as the first step in setting a nsString instance.)
+ //
+ // This is an alternative to the GetYarn() method, which has copy semantics
+ // only; AliasYarn() relaxes a robust safety principle only for performance
+ // reasons, to accommodate the need for callers to transform text content to
+ // some other canonical representation that would necessitate an additional
+ // copy and transformation when such is incompatible with the mdbYarn format.
+ //
+ // The implementation of AliasYarn() should have extremely little overhead
+ // besides the virtual dispatch to the method implementation, and the code
+ // necessary to populate all the mdbYarn member slots with internal buffer
+ // address and metainformation that describes the buffer content. Note that
+ // mYarn_Grow must always be set to nil to indicate no resizing is allowed.
+
+ // } ----- end attribute methods -----
+
+ // } ===== end nsIMdbBlob methods =====
+};
+
+/*| nsIMdbCell: the text in a single column of a row. The base nsIMdbBlob
+**| class provides all the interface related to accessing cell text.
+**|
+**|| column: each cell in a row appears in a specific column, where this
+**| column is identified by the an integer mdb_scope value (generated by
+**| the StringToScopeToken() method in the containing nsIMdbPort instance).
+**| Because a row cannot have more than one cell with the same column,
+**| something must give if one calls SetColumn() with an existing column
+**| in the same row. When this happens, the other cell is replaced with
+**| this cell (and the old cell is closed if it has outstanding refs).
+**|
+**|| row: every cell instance is a part of some row, and every cell knows
+**| which row is the parent row. (Note this should be represented by a
+**| weak backpointer, so that outstanding cell references cannot keep a
+**| row open that should be closed. Otherwise we'd have ref graph cycles.)
+**|
+**|| text: a cell can either be text, or it can have a child row or table,
+**| but not both at once. If text is read from a cell with a child, the text
+**| content should be empty (for AliasYarn()) or a description of the type
+**| of child (perhaps "mdb:cell:child:row" or "mdb:cell:child:table").
+**|
+**|| child: a cell might reference another row or a table, rather than text.
+**| The interface for putting and getting children rows and tables was first
+**| defined in the nsIMdbTable interface, but then this was moved to this cell
+**| interface as more natural.
+|*/
+
+#define NS_IMDBCELL_IID \
+ { \
+ 0xa3b62f71, 0xa181, 0x4a91, { \
+ 0xb6, 0x6b, 0x27, 0x10, 0x9b, 0x88, 0x98, 0x35 \
+ } \
+ }
+
+#define NS_IMDBCELL_IID_STR = "a3b62f71-a181-4a91-b66b-27109b889835"
+
+class nsIMdbCell
+ : public nsIMdbBlob { // text attribute in row with column scope
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBTABLEROWCURSOR_IID)
+ // { ===== begin nsIMdbCell methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetColumn(nsIMdbEnv* ev, mdb_column inColumn) = 0;
+ NS_IMETHOD GetColumn(nsIMdbEnv* ev, mdb_column* outColumn) = 0;
+
+ NS_IMETHOD GetCellInfo( // all cell metainfo except actual content
+ nsIMdbEnv* ev,
+ mdb_column* outColumn, // the column in the containing row
+ mdb_fill* outBlobFill, // the size of text content in bytes
+ mdbOid* outChildOid, // oid of possible row or table child
+ mdb_bool* outIsRowChild) = 0; // nonzero if child, and a row child
+
+ // Checking all cell metainfo is a good way to avoid forcing a large cell
+ // in to memory when you don't actually want to use the content.
+
+ NS_IMETHOD GetRow(nsIMdbEnv* ev, // parent row for this cell
+ nsIMdbRow** acqRow) = 0;
+ NS_IMETHOD GetPort(nsIMdbEnv* ev, // port containing cell
+ nsIMdbPort** acqPort) = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin children methods -----
+ NS_IMETHOD HasAnyChild( // does cell have a child instead of text?
+ nsIMdbEnv* ev,
+ mdbOid* outOid, // out id of row or table (or unbound if no child)
+ mdb_bool* outIsRow) =
+ 0; // nonzero if child is a row (rather than a table)
+
+ NS_IMETHOD GetAnyChild( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // child row (or null)
+ nsIMdbTable** acqTable) = 0; // child table (or null)
+
+ NS_IMETHOD SetChildRow( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow) = 0; // inRow must be bound inside this same db port
+
+ NS_IMETHOD GetChildRow( // access row of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow) = 0; // acquire child row (or nil if no child)
+
+ NS_IMETHOD SetChildTable( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbTable* inTable) =
+ 0; // table must be bound inside this same db port
+
+ NS_IMETHOD GetChildTable( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbTable** acqTable) = 0; // acquire child table (or nil if no child)
+ // } ----- end children methods -----
+
+ // } ===== end nsIMdbCell methods =====
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbCell, NS_IMDBTABLEROWCURSOR_IID)
+
+// } %%%%% end C++ abstract class interfaces %%%%%
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MDB_ */
diff --git a/comm/mailnews/db/mork/mork.h b/comm/mailnews/db/mork/mork.h
new file mode 100644
index 0000000000..ec48e67046
--- /dev/null
+++ b/comm/mailnews/db/mork/mork.h
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORK_
+#define _MORK_ 1
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#include "nscore.h"
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// { %%%%% begin disable unused param warnings %%%%%
+#define MORK_USED_1(x) (void)(&x)
+#define MORK_USED_2(x, y) \
+ (void)(&x); \
+ (void)(&y);
+#define MORK_USED_3(x, y, z) \
+ (void)(&x); \
+ (void)(&y); \
+ (void)(&z);
+#define MORK_USED_4(w, x, y, z) \
+ (void)(&w); \
+ (void)(&x); \
+ (void)(&y); \
+ (void)(&z);
+
+// } %%%%% end disable unused param warnings %%%%%
+
+// { %%%%% begin macro for finding class member offset %%%%%
+
+/*| OffsetOf: the unsigned integer offset of a class or struct
+**| field from the beginning of that class or struct. This is
+**| the same as the similarly named public domain IronDoc macro,
+**| and is also the same as another macro appearing in stdlib.h.
+**| We want these offsets so we can correctly convert pointers
+**| to member slots back into pointers to enclosing objects, and
+**| have this exactly match what the compiler thinks is true.
+**|
+**|| Basically we are asking the compiler to determine the offset at
+**| compile time, and we use the definition of address artithmetic
+**| to do this. By casting integer zero to a pointer of type obj*,
+**| we can reference the address of a slot in such an object that
+**| is hypothetically physically placed at address zero, but without
+**| actually dereferencing a memory location. The absolute address
+**| of slot is the same as offset of that slot, when the object is
+**| placed at address zero.
+|*/
+#define mork_OffsetOf(obj, slot) ((unsigned int)&((obj*)0)->slot)
+
+// } %%%%% end macro for finding class member offset %%%%%
+
+// { %%%%% begin specific-size integer scalar typedefs %%%%%
+typedef unsigned char mork_u1; // make sure this is one byte
+typedef unsigned short mork_u2; // make sure this is two bytes
+typedef short mork_i2; // make sure this is two bytes
+typedef uint32_t mork_u4; // make sure this is four bytes
+typedef int32_t mork_i4; // make sure this is four bytes
+typedef PRWord mork_ip; // make sure sizeof(mork_ip) == sizeof(void*)
+
+typedef mork_u1 mork_ch; // small byte-sized character (never wide)
+typedef mork_u1 mork_flags; // one byte's worth of predicate bit flags
+
+typedef mork_u2 mork_base; // 2-byte magic class signature slot in object
+typedef mork_u2 mork_derived; // 2-byte magic class signature slot in object
+
+typedef mork_u4 mork_token; // unsigned token for atomized string
+typedef mork_token mork_scope; // token used to id scope for rows
+typedef mork_token mork_kind; // token used to id kind for tables
+typedef mork_token mork_cscode; // token used to id charset names
+typedef mork_token mork_aid; // token used to id atomize cell values
+
+typedef mork_token mork_column; // token used to id columns for rows
+typedef mork_column mork_delta; // mork_column plus mork_change
+
+typedef mork_token mork_color; // bead ID
+#define morkColor_kNone ((mork_color)0)
+
+typedef mork_u4 mork_magic; // unsigned magic signature
+
+typedef mork_u4 mork_seed; // unsigned collection change counter
+typedef mork_u4 mork_count; // unsigned collection member count
+typedef mork_count mork_num; // synonym for count
+typedef mork_u4 mork_size; // unsigned physical media size
+typedef mork_u4 mork_fill; // unsigned logical content size
+typedef mork_u4 mork_more; // more available bytes for larger buffer
+
+typedef mdb_u4 mork_percent; // 0..100, with values >100 same as 100
+
+typedef mork_i4 mork_pos; // negative means "before first" (at zero pos)
+typedef mork_i4 mork_line; // negative means "before first line in file"
+
+typedef mork_u1 mork_usage; // 1-byte magic usage signature slot in object
+typedef mork_u1 mork_access; // 1-byte magic access signature slot in object
+
+typedef mork_u1 mork_change; // add, cut, put, set, nil
+typedef mork_u1 mork_priority; // 0..9, for a total of ten different values
+
+typedef mork_u1 mork_able; // on, off, asleep (clone IronDoc's fe_able)
+typedef mork_u1 mork_load; // dirty or clean (clone IronDoc's fe_load)
+// } %%%%% end specific-size integer scalar typedefs %%%%%
+
+// 'test' is a public domain Mithril for key equality tests in probe maps
+typedef mork_i2 mork_test; /* neg=>kVoid, zero=>kHit, pos=>kMiss */
+
+#define morkTest_kVoid ((mork_test)-1) /* -1: nil key slot, no key order */
+#define morkTest_kHit ((mork_test)0) /* 0: keys are equal, a map hit */
+#define morkTest_kMiss ((mork_test)1) /* 1: keys not equal, a map miss */
+
+// { %%%%% begin constants for Mork scalar types %%%%%
+#define morkPriority_kHi ((mork_priority)0) /* best priority */
+#define morkPriority_kMin ((mork_priority)0) /* best priority is smallest */
+
+#define morkPriority_kLo ((mork_priority)9) /* worst priority */
+#define morkPriority_kMax ((mork_priority)9) /* worst priority is biggest */
+
+#define morkPriority_kCount 10 /* number of distinct priority values */
+
+#define morkAble_kEnabled ((mork_able)0x55) /* same as IronDoc constant */
+#define morkAble_kDisabled ((mork_able)0xAA) /* same as IronDoc constant */
+#define morkAble_kAsleep ((mork_able)0x5A) /* same as IronDoc constant */
+
+#define morkChange_kAdd 'a' /* add member */
+#define morkChange_kCut 'c' /* cut member */
+#define morkChange_kPut 'p' /* put member */
+#define morkChange_kSet 's' /* set all members */
+#define morkChange_kNil 0 /* no change in this member */
+#define morkChange_kDup 'd' /* duplicate changes have no effect */
+// kDup is intended to replace another change constant in an object as a
+// conclusion about change feasibility while staging intended alterations.
+
+#define morkLoad_kDirty ((mork_load)0xDD) /* same as IronDoc constant */
+#define morkLoad_kClean ((mork_load)0x22) /* same as IronDoc constant */
+
+#define morkAccess_kOpen 'o'
+#define morkAccess_kClosing 'c'
+#define morkAccess_kShut 's'
+#define morkAccess_kDead 'd'
+// } %%%%% end constants for Mork scalar types %%%%%
+
+// { %%%%% begin non-specific-size integer scalar typedefs %%%%%
+typedef int mork_char; // nominal type for ints used to hold input byte
+#define morkChar_IsWhite(c) \
+ ((c) == 0xA || (c) == 0x9 || (c) == 0xD || (c) == ' ')
+// } %%%%% end non-specific-size integer scalar typedefs %%%%%
+
+// { %%%%% begin mdb-driven scalar typedefs %%%%%
+// easier to define bool exactly the same as mdb:
+typedef mdb_bool mork_bool; // unsigned byte with zero=false, nonzero=true
+
+/* canonical boolean constants provided only for code clarity: */
+#define morkBool_kTrue ((mork_bool)1) /* actually any nonzero means true */
+#define morkBool_kFalse ((mork_bool)0) /* only zero means false */
+
+// mdb clients can assign these, so we cannot pick maximum size:
+typedef mdb_id mork_id; // unsigned object identity in a scope
+typedef mork_id mork_rid; // unsigned row identity inside scope
+typedef mork_id mork_tid; // unsigned table identity inside scope
+typedef mork_id mork_gid; // unsigned group identity without any scope
+
+// we only care about neg, zero, pos -- so we don't care about size:
+typedef mdb_order mork_order; // neg:lessthan, zero:equalto, pos:greaterthan
+// } %%%%% end mdb-driven scalar typedefs %%%%%
+
+#define morkId_kMinusOne ((mdb_id)-1)
+
+// { %%%%% begin class forward defines %%%%%
+// try to put these in alphabetical order for easier examination:
+class morkMid;
+class morkAtom;
+class morkAtomSpace;
+class morkBookAtom;
+class morkBuf;
+class morkBuilder;
+class morkCell;
+class morkCellObject;
+class morkCursor;
+class morkEnv;
+class morkFactory;
+class morkFile;
+class morkHandle;
+class morkHandleFace; // just an opaque cookie type
+class morkHandleFrame;
+class morkHashArrays;
+class morkMap;
+class morkNode;
+class morkObject;
+class morkOidAtom;
+class morkParser;
+class morkPool;
+class morkPlace;
+class morkPort;
+class morkPortTableCursor;
+class morkProbeMap;
+class morkRow;
+class morkRowCellCursor;
+class morkRowObject;
+class morkRowSpace;
+class morkSorting;
+class morkSortingRowCursor;
+class morkSpace;
+class morkSpan;
+class morkStore;
+class morkStream;
+class morkTable;
+class morkTableChange;
+class morkTableRowCursor;
+class morkThumb;
+class morkWriter;
+class morkZone;
+// } %%%%% end class forward defines %%%%%
+
+// include this config file last for platform & environment specific stuff:
+#ifndef _MORKCONFIG_
+# include "morkConfig.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORK_ */
diff --git a/comm/mailnews/db/mork/morkArray.cpp b/comm/mailnews/db/mork/morkArray.cpp
new file mode 100644
index 0000000000..fff5f8a626
--- /dev/null
+++ b/comm/mailnews/db/mork/morkArray.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nscore.h"
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKARRAY_
+# include "morkArray.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkArray::CloseMorkNode(
+ morkEnv* ev) // CloseTable() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseArray(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkArray::~morkArray() // assert CloseTable() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+ MORK_ASSERT(mArray_Slots == 0);
+}
+
+/*public non-poly*/
+morkArray::morkArray(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_size inSize, nsIMdbHeap* ioSlotHeap)
+ : morkNode(ev, inUsage, ioHeap),
+ mArray_Slots(0),
+ mArray_Heap(0),
+ mArray_Fill(0),
+ mArray_Size(0),
+ mArray_Seed(
+ (mork_u4)NS_PTR_TO_INT32(this)) // "random" integer assignment
+{
+ if (ev->Good()) {
+ if (ioSlotHeap) {
+ nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mArray_Heap);
+ if (ev->Good()) {
+ if (inSize < 3) inSize = 3;
+ mdb_size byteSize = inSize * sizeof(void*);
+ void** block = 0;
+ ioSlotHeap->Alloc(ev->AsMdbEnv(), byteSize, (void**)&block);
+ if (block && ev->Good()) {
+ mArray_Slots = block;
+ mArray_Size = inSize;
+ MORK_MEMSET(mArray_Slots, 0, byteSize);
+ if (ev->Good()) mNode_Derived = morkDerived_kArray;
+ }
+ }
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkArray::CloseArray(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ if (mArray_Heap && mArray_Slots)
+ mArray_Heap->Free(ev->AsMdbEnv(), mArray_Slots);
+
+ mArray_Slots = 0;
+ mArray_Size = 0;
+ mArray_Fill = 0;
+ ++mArray_Seed;
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mArray_Heap);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkArray::NonArrayTypeError(morkEnv* ev) {
+ ev->NewError("non morkArray");
+}
+
+/*static*/ void morkArray::IndexBeyondEndError(morkEnv* ev) {
+ ev->NewError("array index beyond end");
+}
+
+/*static*/ void morkArray::NilSlotsAddressError(morkEnv* ev) {
+ ev->NewError("nil mArray_Slots");
+}
+
+/*static*/ void morkArray::FillBeyondSizeError(morkEnv* ev) {
+ ev->NewError("mArray_Fill > mArray_Size");
+}
+
+mork_bool morkArray::Grow(morkEnv* ev, mork_size inNewSize)
+// Grow() returns true if capacity becomes >= inNewSize and ev->Good()
+{
+ if (ev->Good() && inNewSize > mArray_Size) // make array larger?
+ {
+ if (mArray_Fill <= mArray_Size) // fill and size fit the invariant?
+ {
+ if (mArray_Size <= 3)
+ inNewSize = mArray_Size + 3;
+ else
+ inNewSize = mArray_Size *
+ 2; // + 3; // try doubling size here - used to grow by 3
+
+ mdb_size newByteSize = inNewSize * sizeof(void*);
+ void** newBlock = 0;
+ mArray_Heap->Alloc(ev->AsMdbEnv(), newByteSize, (void**)&newBlock);
+ if (newBlock && ev->Good()) // okay new block?
+ {
+ void** oldSlots = mArray_Slots;
+ void** oldEnd = oldSlots + mArray_Fill;
+
+ void** newSlots = newBlock;
+ void** newEnd = newBlock + inNewSize;
+
+ while (oldSlots < oldEnd) *newSlots++ = *oldSlots++;
+
+ while (newSlots < newEnd) *newSlots++ = (void*)0;
+
+ oldSlots = mArray_Slots;
+ mArray_Size = inNewSize;
+ mArray_Slots = newBlock;
+ mArray_Heap->Free(ev->AsMdbEnv(), oldSlots);
+ }
+ } else
+ this->FillBeyondSizeError(ev);
+ }
+ ++mArray_Seed; // always modify seed, since caller intends to add slots
+ return (ev->Good() && mArray_Size >= inNewSize);
+}
+
+void* morkArray::SafeAt(morkEnv* ev, mork_pos inPos) {
+ if (mArray_Slots) {
+ if (inPos >= 0 && inPos < (mork_pos)mArray_Fill)
+ return mArray_Slots[inPos];
+ else
+ this->IndexBeyondEndError(ev);
+ } else
+ this->NilSlotsAddressError(ev);
+
+ return (void*)0;
+}
+
+void morkArray::SafeAtPut(morkEnv* ev, mork_pos inPos, void* ioSlot) {
+ if (mArray_Slots) {
+ if (inPos >= 0 && inPos < (mork_pos)mArray_Fill) {
+ mArray_Slots[inPos] = ioSlot;
+ ++mArray_Seed;
+ } else
+ this->IndexBeyondEndError(ev);
+ } else
+ this->NilSlotsAddressError(ev);
+}
+
+mork_pos morkArray::AppendSlot(morkEnv* ev, void* ioSlot) {
+ mork_pos outPos = -1;
+ if (mArray_Slots) {
+ mork_fill fill = mArray_Fill;
+ if (this->Grow(ev, fill + 1)) {
+ outPos = (mork_pos)fill;
+ mArray_Slots[fill] = ioSlot;
+ mArray_Fill = fill + 1;
+ // note Grow() increments mArray_Seed
+ }
+ } else
+ this->NilSlotsAddressError(ev);
+
+ return outPos;
+}
+
+void morkArray::AddSlot(morkEnv* ev, mork_pos inPos, void* ioSlot) {
+ if (mArray_Slots) {
+ mork_fill fill = mArray_Fill;
+ if (this->Grow(ev, fill + 1)) {
+ void** slot = mArray_Slots; // the slot vector
+ void** end = slot + fill; // one past the last used array slot
+ slot += inPos; // the slot to be added
+
+ while (--end >= slot) // another slot to move upward?
+ end[1] = *end;
+
+ *slot = ioSlot;
+ mArray_Fill = fill + 1;
+ // note Grow() increments mArray_Seed
+ }
+ } else
+ this->NilSlotsAddressError(ev);
+}
+
+void morkArray::CutSlot(morkEnv* ev, mork_pos inPos) {
+ MORK_USED_1(ev);
+ mork_fill fill = mArray_Fill;
+ if (inPos >= 0 &&
+ inPos < (mork_pos)fill) // cutting slot in used array portion?
+ {
+ void** slot = mArray_Slots; // the slot vector
+ void** end = slot + fill; // one past the last used array slot
+ slot += inPos; // the slot to be cut
+
+ while (++slot < end) // another slot to move downward?
+ slot[-1] = *slot;
+
+ slot[-1] = 0; // clear the last used slot which is now unused
+
+ // note inPos<fill implies fill>0, so fill-1 must be nonnegative:
+ mArray_Fill = fill - 1;
+ ++mArray_Seed;
+ }
+}
+
+void morkArray::CutAllSlots(morkEnv* ev) {
+ if (mArray_Slots) {
+ if (mArray_Fill <= mArray_Size) {
+ mdb_size oldByteSize = mArray_Fill * sizeof(void*);
+ MORK_MEMSET(mArray_Slots, 0, oldByteSize);
+ } else
+ this->FillBeyondSizeError(ev);
+ } else
+ this->NilSlotsAddressError(ev);
+
+ ++mArray_Seed;
+ mArray_Fill = 0;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkArray.h b/comm/mailnews/db/mork/morkArray.h
new file mode 100644
index 0000000000..daf9c96f35
--- /dev/null
+++ b/comm/mailnews/db/mork/morkArray.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKARRAY_
+#define _MORKARRAY_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kArray /*i*/ 0x4179 /* ascii 'Ay' */
+
+class morkArray : public morkNode { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ void** mArray_Slots; // array of pointers
+ nsIMdbHeap* mArray_Heap; // required heap for allocating mArray_Slots
+ mork_fill mArray_Fill; // logical count of used slots in mArray_Slots
+ mork_size mArray_Size; // physical count of mArray_Slots ( >= Fill)
+ mork_seed mArray_Seed; // change counter for syncing with iterators
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseArray()
+ virtual ~morkArray(); // assert that close executed earlier
+
+ public: // morkArray construction & destruction
+ morkArray(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_size inSize, nsIMdbHeap* ioSlotHeap);
+ void CloseArray(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkArray(const morkArray& other);
+ morkArray& operator=(const morkArray& other);
+
+ public: // dynamic type identification
+ mork_bool IsArray() const {
+ return IsNode() && mNode_Derived == morkDerived_kArray;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing & errors
+ static void NonArrayTypeError(morkEnv* ev);
+ static void IndexBeyondEndError(morkEnv* ev);
+ static void NilSlotsAddressError(morkEnv* ev);
+ static void FillBeyondSizeError(morkEnv* ev);
+
+ public: // other table row cursor methods
+ mork_fill Length() const { return mArray_Fill; }
+ mork_size Capacity() const { return mArray_Size; }
+
+ mork_bool Grow(morkEnv* ev, mork_size inNewSize);
+ // Grow() returns true if capacity becomes >= inNewSize and ev->Good()
+
+ void* At(mork_pos inPos) const { return mArray_Slots[inPos]; }
+ void AtPut(mork_pos inPos, void* ioSlot) { mArray_Slots[inPos] = ioSlot; }
+
+ void* SafeAt(morkEnv* ev, mork_pos inPos);
+ void SafeAtPut(morkEnv* ev, mork_pos inPos, void* ioSlot);
+
+ mork_pos AppendSlot(morkEnv* ev, void* ioSlot);
+ void AddSlot(morkEnv* ev, mork_pos inPos, void* ioSlot);
+ void CutSlot(morkEnv* ev, mork_pos inPos);
+ void CutAllSlots(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakArray(morkArray* me, morkEnv* ev, morkArray** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongArray(morkArray* me, morkEnv* ev, morkArray** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKTABLEROWCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkAtom.cpp b/comm/mailnews/db/mork/morkAtom.cpp
new file mode 100644
index 0000000000..ad3b1d53bf
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtom.cpp
@@ -0,0 +1,432 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/* static */
+mork_bool morkAtom::GetYarn(const morkAtom* atom, mdbYarn* outYarn) {
+ const void* source = 0;
+ mdb_fill fill = 0;
+ mdb_cscode form = 0;
+ outYarn->mYarn_More = 0;
+
+ if (atom) {
+ if (atom->IsWeeBook()) {
+ morkWeeBookAtom* weeBook = (morkWeeBookAtom*)atom;
+ source = weeBook->mWeeBookAtom_Body;
+ fill = weeBook->mAtom_Size;
+ } else if (atom->IsBigBook()) {
+ morkBigBookAtom* bigBook = (morkBigBookAtom*)atom;
+ source = bigBook->mBigBookAtom_Body;
+ fill = bigBook->mBigBookAtom_Size;
+ form = bigBook->mBigBookAtom_Form;
+ } else if (atom->IsWeeAnon()) {
+ morkWeeAnonAtom* weeAnon = (morkWeeAnonAtom*)atom;
+ source = weeAnon->mWeeAnonAtom_Body;
+ fill = weeAnon->mAtom_Size;
+ } else if (atom->IsBigAnon()) {
+ morkBigAnonAtom* bigAnon = (morkBigAnonAtom*)atom;
+ source = bigAnon->mBigAnonAtom_Body;
+ fill = bigAnon->mBigAnonAtom_Size;
+ form = bigAnon->mBigAnonAtom_Form;
+ }
+ }
+
+ if (source && fill) // have an atom with nonempty content?
+ {
+ // if we have too many bytes, and yarn seems growable:
+ if (fill > outYarn->mYarn_Size && outYarn->mYarn_Grow) // try grow?
+ (*outYarn->mYarn_Grow)(outYarn, (mdb_size)fill); // request bigger
+
+ mdb_size size = outYarn->mYarn_Size; // max dest size
+ if (fill > size) // too much atom content?
+ {
+ outYarn->mYarn_More = fill - size; // extra atom bytes omitted
+ fill = size; // copy no more bytes than size of yarn buffer
+ }
+ void* dest = outYarn->mYarn_Buf; // where bytes are going
+ if (!dest) // nil destination address buffer?
+ fill = 0; // we can't write any content at all
+
+ if (fill) // anything to copy?
+ MORK_MEMCPY(dest, source, fill); // copy fill bytes to yarn
+
+ outYarn->mYarn_Fill = fill; // tell yarn size of copied content
+ } else // no content to put into the yarn
+ {
+ outYarn->mYarn_Fill = 0; // tell yarn that atom has no bytes
+ }
+ outYarn->mYarn_Form = form; // always update the form slot
+
+ return (source != 0);
+}
+
+/* static */
+mork_bool morkAtom::AliasYarn(const morkAtom* atom, mdbYarn* outYarn) {
+ outYarn->mYarn_More = 0;
+ outYarn->mYarn_Form = 0;
+
+ if (atom) {
+ if (atom->IsWeeBook()) {
+ morkWeeBookAtom* weeBook = (morkWeeBookAtom*)atom;
+ outYarn->mYarn_Buf = weeBook->mWeeBookAtom_Body;
+ outYarn->mYarn_Fill = weeBook->mAtom_Size;
+ outYarn->mYarn_Size = weeBook->mAtom_Size;
+ } else if (atom->IsBigBook()) {
+ morkBigBookAtom* bigBook = (morkBigBookAtom*)atom;
+ outYarn->mYarn_Buf = bigBook->mBigBookAtom_Body;
+ outYarn->mYarn_Fill = bigBook->mBigBookAtom_Size;
+ outYarn->mYarn_Size = bigBook->mBigBookAtom_Size;
+ outYarn->mYarn_Form = bigBook->mBigBookAtom_Form;
+ } else if (atom->IsWeeAnon()) {
+ morkWeeAnonAtom* weeAnon = (morkWeeAnonAtom*)atom;
+ outYarn->mYarn_Buf = weeAnon->mWeeAnonAtom_Body;
+ outYarn->mYarn_Fill = weeAnon->mAtom_Size;
+ outYarn->mYarn_Size = weeAnon->mAtom_Size;
+ } else if (atom->IsBigAnon()) {
+ morkBigAnonAtom* bigAnon = (morkBigAnonAtom*)atom;
+ outYarn->mYarn_Buf = bigAnon->mBigAnonAtom_Body;
+ outYarn->mYarn_Fill = bigAnon->mBigAnonAtom_Size;
+ outYarn->mYarn_Size = bigAnon->mBigAnonAtom_Size;
+ outYarn->mYarn_Form = bigAnon->mBigAnonAtom_Form;
+ } else
+ atom = 0; // show desire to put empty content in yarn
+ }
+
+ if (!atom) // empty content for yarn?
+ {
+ outYarn->mYarn_Buf = 0;
+ outYarn->mYarn_Fill = 0;
+ outYarn->mYarn_Size = 0;
+ // outYarn->mYarn_Grow = 0; // please don't modify the Grow slot
+ }
+ return (atom != 0);
+}
+
+mork_aid morkAtom::GetBookAtomAid() const // zero or book atom's ID
+{
+ return (this->IsBook()) ? ((morkBookAtom*)this)->mBookAtom_Id : 0;
+}
+
+mork_scope morkAtom::GetBookAtomSpaceScope(
+ morkEnv* ev) const // zero or book's space's scope
+{
+ mork_scope outScope = 0;
+ if (this->IsBook()) {
+ const morkBookAtom* bookAtom = (const morkBookAtom*)this;
+ morkAtomSpace* space = bookAtom->mBookAtom_Space;
+ if (space->IsAtomSpace())
+ outScope = space->SpaceScope();
+ else
+ space->NonAtomSpaceTypeError(ev);
+ }
+
+ return outScope;
+}
+
+void morkAtom::MakeCellUseForever(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mAtom_CellUses = morkAtom_kForeverCellUses;
+}
+
+mork_u1 morkAtom::AddCellUse(morkEnv* ev) {
+ MORK_USED_1(ev);
+ if (mAtom_CellUses < morkAtom_kMaxCellUses) // not already maxed out?
+ ++mAtom_CellUses;
+
+ return mAtom_CellUses;
+}
+
+mork_u1 morkAtom::CutCellUse(morkEnv* ev) {
+ if (mAtom_CellUses) // any outstanding uses to cut?
+ {
+ if (mAtom_CellUses < morkAtom_kMaxCellUses) // not frozen at max?
+ --mAtom_CellUses;
+ } else
+ this->CellUsesUnderflowWarning(ev);
+
+ return mAtom_CellUses;
+}
+
+/*static*/ void morkAtom::CellUsesUnderflowWarning(morkEnv* ev) {
+ ev->NewWarning("mAtom_CellUses underflow");
+}
+
+/*static*/ void morkAtom::BadAtomKindError(morkEnv* ev) {
+ ev->NewError("bad mAtom_Kind");
+}
+
+/*static*/ void morkAtom::ZeroAidError(morkEnv* ev) {
+ ev->NewError("zero atom ID");
+}
+
+/*static*/ void morkAtom::AtomSizeOverflowError(morkEnv* ev) {
+ ev->NewError("atom mAtom_Size overflow");
+}
+
+void morkOidAtom::InitRowOidAtom(morkEnv* ev, const mdbOid& inOid) {
+ MORK_USED_1(ev);
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindRowOid;
+ mAtom_Change = morkChange_kNil;
+ mAtom_Size = 0;
+ mOidAtom_Oid = inOid; // bitwise copy
+}
+
+void morkOidAtom::InitTableOidAtom(morkEnv* ev, const mdbOid& inOid) {
+ MORK_USED_1(ev);
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindTableOid;
+ mAtom_Change = morkChange_kNil;
+ mAtom_Size = 0;
+ mOidAtom_Oid = inOid; // bitwise copy
+}
+
+void morkWeeAnonAtom::InitWeeAnonAtom(morkEnv* ev, const morkBuf& inBuf) {
+ mAtom_Kind = 0;
+ mAtom_Change = morkChange_kNil;
+ if (inBuf.mBuf_Fill <= morkAtom_kMaxByteSize) {
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindWeeAnon;
+ mork_size size = inBuf.mBuf_Fill;
+ mAtom_Size = (mork_u1)size;
+ if (size && inBuf.mBuf_Body)
+ MORK_MEMCPY(mWeeAnonAtom_Body, inBuf.mBuf_Body, size);
+
+ mWeeAnonAtom_Body[size] = 0;
+ } else
+ this->AtomSizeOverflowError(ev);
+}
+
+void morkBigAnonAtom::InitBigAnonAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm) {
+ MORK_USED_1(ev);
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindBigAnon;
+ mAtom_Change = morkChange_kNil;
+ mAtom_Size = 0;
+ mBigAnonAtom_Form = inForm;
+ mork_size size = inBuf.mBuf_Fill;
+ mBigAnonAtom_Size = size;
+ if (size && inBuf.mBuf_Body)
+ MORK_MEMCPY(mBigAnonAtom_Body, inBuf.mBuf_Body, size);
+
+ mBigAnonAtom_Body[size] = 0;
+}
+
+/*static*/ void morkBookAtom::NonBookAtomTypeError(morkEnv* ev) {
+ ev->NewError("non morkBookAtom");
+}
+
+mork_u4 morkBookAtom::HashFormAndBody(morkEnv* ev) const {
+ // This hash is obviously a variation of the dragon book string hash.
+ // (I won't bother to explain or rationalize this usage for you.)
+
+ mork_u4 outHash = 0; // hash value returned
+ unsigned char c; // next character
+ const mork_u1* body; // body of bytes to hash
+ mork_size size = 0; // the number of bytes to hash
+
+ if (this->IsWeeBook()) {
+ size = mAtom_Size;
+ body = ((const morkWeeBookAtom*)this)->mWeeBookAtom_Body;
+ } else if (this->IsBigBook()) {
+ size = ((const morkBigBookAtom*)this)->mBigBookAtom_Size;
+ body = ((const morkBigBookAtom*)this)->mBigBookAtom_Body;
+ } else if (this->IsFarBook()) {
+ size = ((const morkFarBookAtom*)this)->mFarBookAtom_Size;
+ body = ((const morkFarBookAtom*)this)->mFarBookAtom_Body;
+ } else {
+ this->NonBookAtomTypeError(ev);
+ return 0;
+ }
+
+ const mork_u1* end = body + size;
+ while (body < end) {
+ c = *body++;
+ outHash <<= 4;
+ outHash += c;
+ mork_u4 top = outHash & 0xF0000000L; // top four bits
+ if (top) // any of high four bits equal to one?
+ {
+ outHash ^= (top >> 24); // fold down high bits
+ outHash ^= top; // zero top four bits
+ }
+ }
+
+ return outHash;
+}
+
+mork_bool morkBookAtom::EqualFormAndBody(morkEnv* ev,
+ const morkBookAtom* inAtom) const {
+ mork_bool outEqual = morkBool_kFalse;
+
+ const mork_u1* body = 0; // body of inAtom bytes to compare
+ mork_size size; // the number of inAtom bytes to compare
+ mork_cscode form; // nominal charset for ioAtom
+
+ if (inAtom->IsWeeBook()) {
+ size = inAtom->mAtom_Size;
+ body = ((const morkWeeBookAtom*)inAtom)->mWeeBookAtom_Body;
+ form = 0;
+ } else if (inAtom->IsBigBook()) {
+ size = ((const morkBigBookAtom*)inAtom)->mBigBookAtom_Size;
+ body = ((const morkBigBookAtom*)inAtom)->mBigBookAtom_Body;
+ form = ((const morkBigBookAtom*)inAtom)->mBigBookAtom_Form;
+ } else if (inAtom->IsFarBook()) {
+ size = ((const morkFarBookAtom*)inAtom)->mFarBookAtom_Size;
+ body = ((const morkFarBookAtom*)inAtom)->mFarBookAtom_Body;
+ form = ((const morkFarBookAtom*)inAtom)->mFarBookAtom_Form;
+ } else {
+ inAtom->NonBookAtomTypeError(ev);
+ return morkBool_kFalse;
+ }
+
+ const mork_u1* thisBody = 0; // body of bytes in this to compare
+ mork_size thisSize; // the number of bytes in this to compare
+ mork_cscode thisForm; // nominal charset for this atom
+
+ if (this->IsWeeBook()) {
+ thisSize = mAtom_Size;
+ thisBody = ((const morkWeeBookAtom*)this)->mWeeBookAtom_Body;
+ thisForm = 0;
+ } else if (this->IsBigBook()) {
+ thisSize = ((const morkBigBookAtom*)this)->mBigBookAtom_Size;
+ thisBody = ((const morkBigBookAtom*)this)->mBigBookAtom_Body;
+ thisForm = ((const morkBigBookAtom*)this)->mBigBookAtom_Form;
+ } else if (this->IsFarBook()) {
+ thisSize = ((const morkFarBookAtom*)this)->mFarBookAtom_Size;
+ thisBody = ((const morkFarBookAtom*)this)->mFarBookAtom_Body;
+ thisForm = ((const morkFarBookAtom*)this)->mFarBookAtom_Form;
+ } else {
+ this->NonBookAtomTypeError(ev);
+ return morkBool_kFalse;
+ }
+
+ // if atoms are empty, form is irrelevant
+ if (body && thisBody && size == thisSize && (!size || form == thisForm))
+ outEqual = (MORK_MEMCMP(body, thisBody, size) == 0);
+
+ return outEqual;
+}
+
+void morkBookAtom::CutBookAtomFromSpace(morkEnv* ev) {
+ morkAtomSpace* space = mBookAtom_Space;
+ if (space) {
+ mBookAtom_Space = 0;
+ space->mAtomSpace_AtomBodies.CutAtom(ev, this);
+ space->mAtomSpace_AtomAids.CutAtom(ev, this);
+ } else
+ ev->NilPointerError();
+}
+
+morkWeeBookAtom::morkWeeBookAtom(mork_aid inAid) {
+ mAtom_Kind = morkAtom_kKindWeeBook;
+ mAtom_CellUses = 0;
+ mAtom_Change = morkChange_kNil;
+ mAtom_Size = 0;
+
+ mBookAtom_Space = 0;
+ mBookAtom_Id = inAid;
+
+ mWeeBookAtom_Body[0] = 0;
+}
+
+void morkWeeBookAtom::InitWeeBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ morkAtomSpace* ioSpace, mork_aid inAid) {
+ mAtom_Kind = 0;
+ mAtom_Change = morkChange_kNil;
+ if (ioSpace) {
+ if (inAid) {
+ if (inBuf.mBuf_Fill <= morkAtom_kMaxByteSize) {
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindWeeBook;
+ mBookAtom_Space = ioSpace;
+ mBookAtom_Id = inAid;
+ mork_size size = inBuf.mBuf_Fill;
+ mAtom_Size = (mork_u1)size;
+ if (size && inBuf.mBuf_Body)
+ MORK_MEMCPY(mWeeBookAtom_Body, inBuf.mBuf_Body, size);
+
+ mWeeBookAtom_Body[size] = 0;
+ } else
+ this->AtomSizeOverflowError(ev);
+ } else
+ this->ZeroAidError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+void morkBigBookAtom::InitBigBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm,
+ morkAtomSpace* ioSpace, mork_aid inAid) {
+ mAtom_Kind = 0;
+ mAtom_Change = morkChange_kNil;
+ if (ioSpace) {
+ if (inAid) {
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindBigBook;
+ mAtom_Size = 0;
+ mBookAtom_Space = ioSpace;
+ mBookAtom_Id = inAid;
+ mBigBookAtom_Form = inForm;
+ mork_size size = inBuf.mBuf_Fill;
+ mBigBookAtom_Size = size;
+ if (size && inBuf.mBuf_Body)
+ MORK_MEMCPY(mBigBookAtom_Body, inBuf.mBuf_Body, size);
+
+ mBigBookAtom_Body[size] = 0;
+ } else
+ this->ZeroAidError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+void morkFarBookAtom::InitFarBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm,
+ morkAtomSpace* ioSpace, mork_aid inAid) {
+ mAtom_Kind = 0;
+ mAtom_Change = morkChange_kNil;
+ if (ioSpace) {
+ if (inAid) {
+ mAtom_CellUses = 0;
+ mAtom_Kind = morkAtom_kKindFarBook;
+ mAtom_Size = 0;
+ mBookAtom_Space = ioSpace;
+ mBookAtom_Id = inAid;
+ mFarBookAtom_Form = inForm;
+ mFarBookAtom_Size = inBuf.mBuf_Fill;
+ mFarBookAtom_Body = (mork_u1*)inBuf.mBuf_Body;
+ } else
+ this->ZeroAidError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkAtom.h b/comm/mailnews/db/mork/morkAtom.h
new file mode 100644
index 0000000000..4313a2e8fa
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtom.h
@@ -0,0 +1,362 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKATOM_
+#define _MORKATOM_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkAtom_kMaxByteSize 255 /* max for 8-bit integer */
+#define morkAtom_kForeverCellUses 0x0FF /* max for 8-bit integer */
+#define morkAtom_kMaxCellUses 0x07F /* max for 7-bit integer */
+
+#define morkAtom_kKindWeeAnon 'a' /* means morkWeeAnonAtom subclass */
+#define morkAtom_kKindBigAnon 'A' /* means morkBigAnonAtom subclass */
+#define morkAtom_kKindWeeBook 'b' /* means morkWeeBookAtom subclass */
+#define morkAtom_kKindBigBook 'B' /* means morkBigBookAtom subclass */
+#define morkAtom_kKindFarBook 'f' /* means morkFarBookAtom subclass */
+#define morkAtom_kKindRowOid 'r' /* means morkOidAtom subclass */
+#define morkAtom_kKindTableOid 't' /* means morkOidAtom subclass */
+
+/*| Atom: .
+|*/
+class morkAtom { //
+
+ public:
+ mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ mork_change mAtom_Change; // how has this atom been changed?
+ mork_u1 mAtom_Size; // only for atoms smaller than 256 bytes
+
+ public:
+ morkAtom(mork_aid inAid, mork_u1 inKind);
+
+ mork_bool IsWeeAnon() const { return mAtom_Kind == morkAtom_kKindWeeAnon; }
+ mork_bool IsBigAnon() const { return mAtom_Kind == morkAtom_kKindBigAnon; }
+ mork_bool IsWeeBook() const { return mAtom_Kind == morkAtom_kKindWeeBook; }
+ mork_bool IsBigBook() const { return mAtom_Kind == morkAtom_kKindBigBook; }
+ mork_bool IsFarBook() const { return mAtom_Kind == morkAtom_kKindFarBook; }
+ mork_bool IsRowOid() const { return mAtom_Kind == morkAtom_kKindRowOid; }
+ mork_bool IsTableOid() const { return mAtom_Kind == morkAtom_kKindTableOid; }
+
+ mork_bool IsBook() const { return this->IsWeeBook() || this->IsBigBook(); }
+
+ public: // clean vs dirty
+ void SetAtomClean() { mAtom_Change = morkChange_kNil; }
+ void SetAtomDirty() { mAtom_Change = morkChange_kAdd; }
+
+ mork_bool IsAtomClean() const { return mAtom_Change == morkChange_kNil; }
+ mork_bool IsAtomDirty() const { return mAtom_Change == morkChange_kAdd; }
+
+ public: // atom space scope if IsBook() is true, or else zero:
+ mork_scope GetBookAtomSpaceScope(morkEnv* ev) const;
+ // zero or book's space's scope
+
+ mork_aid GetBookAtomAid() const;
+ // zero or book atom's ID
+
+ public: // empty construction does nothing
+ morkAtom() {}
+
+ public: // one-byte refcounting, freezing at maximum
+ void MakeCellUseForever(morkEnv* ev);
+ mork_u1 AddCellUse(morkEnv* ev);
+ mork_u1 CutCellUse(morkEnv* ev);
+
+ mork_bool IsCellUseForever() const {
+ return mAtom_CellUses == morkAtom_kForeverCellUses;
+ }
+
+ private: // warnings
+ static void CellUsesUnderflowWarning(morkEnv* ev);
+
+ public: // errors
+ static void BadAtomKindError(morkEnv* ev);
+ static void ZeroAidError(morkEnv* ev);
+ static void AtomSizeOverflowError(morkEnv* ev);
+
+ public: // yarns
+ static mork_bool AliasYarn(const morkAtom* atom, mdbYarn* outYarn);
+ static mork_bool GetYarn(const morkAtom* atom, mdbYarn* outYarn);
+
+ private: // copying is not allowed
+ morkAtom(const morkAtom& other);
+ morkAtom& operator=(const morkAtom& other);
+};
+
+/*| OidAtom: an atom that references a row or table by identity.
+|*/
+class morkOidAtom : public morkAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // NOT USED IN "BIG" format atoms
+
+ public:
+ mdbOid mOidAtom_Oid; // identity of referenced object
+
+ public: // empty construction does nothing
+ morkOidAtom() {}
+ void InitRowOidAtom(morkEnv* ev, const mdbOid& inOid);
+ void InitTableOidAtom(morkEnv* ev, const mdbOid& inOid);
+
+ private: // copying is not allowed
+ morkOidAtom(const morkOidAtom& other);
+ morkOidAtom& operator=(const morkOidAtom& other);
+};
+
+/*| WeeAnonAtom: an atom whose content immediately follows morkAtom slots
+**| in an inline fashion, so that morkWeeAnonAtom contains both leading
+**| atom slots and then the content bytes without further overhead. Note
+**| that charset encoding is not indicated, so zero is implied for Latin1.
+**| (Non-Latin1 content must be stored in a morkBigAnonAtom with a charset.)
+**|
+**|| An anon (anonymous) atom has no identity, with no associated bookkeeping
+**| for lookup needed for sharing like a book atom.
+**|
+**|| A wee anon atom is immediate but not shared with any other users of this
+**| atom, so no bookkeeping for sharing is needed. This means the atom has
+**| no ID, because the atom has no identity other than this immediate content,
+**| and no hash table is needed to look up this particular atom. This also
+**| applies to the larger format morkBigAnonAtom, which has more slots.
+|*/
+class morkWeeAnonAtom : public morkAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // only for atoms smaller than 256 bytes
+
+ public:
+ mork_u1 mWeeAnonAtom_Body[1]; // 1st byte of immediate content vector
+
+ public: // empty construction does nothing
+ morkWeeAnonAtom() {}
+ void InitWeeAnonAtom(morkEnv* ev, const morkBuf& inBuf);
+
+ // allow extra trailing byte for a null byte:
+ static mork_size SizeForFill(mork_fill inFill) {
+ return sizeof(morkWeeAnonAtom) + inFill;
+ }
+
+ private: // copying is not allowed
+ morkWeeAnonAtom(const morkWeeAnonAtom& other);
+ morkWeeAnonAtom& operator=(const morkWeeAnonAtom& other);
+};
+
+/*| BigAnonAtom: another immediate atom that cannot be encoded as the smaller
+**| morkWeeAnonAtom format because either the size is too great, and/or the
+**| charset is not the default zero for Latin1 and must be explicitly noted.
+**|
+**|| An anon (anonymous) atom has no identity, with no associated bookkeeping
+**| for lookup needed for sharing like a book atom.
+|*/
+class morkBigAnonAtom : public morkAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // NOT USED IN "BIG" format atoms
+
+ public:
+ mork_cscode mBigAnonAtom_Form; // charset format encoding
+ mork_size mBigAnonAtom_Size; // size of content vector
+ mork_u1 mBigAnonAtom_Body[1]; // 1st byte of immed content vector
+
+ public: // empty construction does nothing
+ morkBigAnonAtom() {}
+ void InitBigAnonAtom(morkEnv* ev, const morkBuf& inBuf, mork_cscode inForm);
+
+ // allow extra trailing byte for a null byte:
+ static mork_size SizeForFill(mork_fill inFill) {
+ return sizeof(morkBigAnonAtom) + inFill;
+ }
+
+ private: // copying is not allowed
+ morkBigAnonAtom(const morkBigAnonAtom& other);
+ morkBigAnonAtom& operator=(const morkBigAnonAtom& other);
+};
+
+#define morkBookAtom_kMaxBodySize 1024 /* if larger, cannot be shared */
+
+/*| BookAtom: the common subportion of wee book atoms and big book atoms that
+**| includes the atom ID and the pointer to the space referencing this atom
+**| through a hash table.
+|*/
+class morkBookAtom : public morkAtom { //
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // only for atoms smaller than 256 bytes
+
+ public:
+ morkAtomSpace*
+ mBookAtom_Space; // mBookAtom_Space->SpaceScope() is atom scope
+ mork_aid mBookAtom_Id; // identity token for this shared atom
+
+ public: // empty construction does nothing
+ morkBookAtom() {}
+
+ static void NonBookAtomTypeError(morkEnv* ev);
+
+ public: // Hash() and Equal() for atom ID maps are same for all subclasses:
+ mork_u4 HashAid() const { return mBookAtom_Id; }
+ mork_bool EqualAid(const morkBookAtom* inAtom) const {
+ return (mBookAtom_Id == inAtom->mBookAtom_Id);
+ }
+
+ public: // Hash() and Equal() for atom body maps know about subclasses:
+ // YOU CANNOT SUBCLASS morkBookAtom WITHOUT FIXING Hash and Equal METHODS:
+
+ mork_u4 HashFormAndBody(morkEnv* ev) const;
+ mork_bool EqualFormAndBody(morkEnv* ev, const morkBookAtom* inAtom) const;
+
+ public: // separation from containing space
+ void CutBookAtomFromSpace(morkEnv* ev);
+
+ private: // copying is not allowed
+ morkBookAtom(const morkBookAtom& other);
+ morkBookAtom& operator=(const morkBookAtom& other);
+};
+
+/*| FarBookAtom: this alternative format for book atoms was introduced
+**| in May 2000 in order to support finding atoms in hash tables without
+**| first copying the strings from original parsing buffers into a new
+**| atom format. This was consuming too much time. However, we can
+**| use morkFarBookAtom to stage a hash table query, as long as we then
+**| fix HashFormAndBody() and EqualFormAndBody() to use morkFarBookAtom
+**| correctly.
+**|
+**|| Note we do NOT intend that instances of morkFarBookAtom will ever
+**| be installed in hash tables, because this is not space efficient.
+**| We only expect to create temp instances for table lookups.
+|*/
+class morkFarBookAtom : public morkBookAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // NOT USED IN "BIG" format atoms
+
+ // morkAtomSpace* mBookAtom_Space; // mBookAtom_Space->SpaceScope() is scope
+ // mork_aid mBookAtom_Id; // identity token for this shared atom
+
+ public:
+ mork_cscode mFarBookAtom_Form; // charset format encoding
+ mork_size mFarBookAtom_Size; // size of content vector
+ mork_u1* mFarBookAtom_Body; // bytes are elsewhere, out of line
+
+ public: // empty construction does nothing
+ morkFarBookAtom() {}
+ void InitFarBookAtom(morkEnv* ev, const morkBuf& inBuf, mork_cscode inForm,
+ morkAtomSpace* ioSpace, mork_aid inAid);
+
+ private: // copying is not allowed
+ morkFarBookAtom(const morkFarBookAtom& other);
+ morkFarBookAtom& operator=(const morkFarBookAtom& other);
+};
+
+/*| WeeBookAtom: .
+|*/
+class morkWeeBookAtom : public morkBookAtom { //
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // only for atoms smaller than 256 bytes
+
+ // morkAtomSpace* mBookAtom_Space; // mBookAtom_Space->SpaceScope() is scope
+ // mork_aid mBookAtom_Id; // identity token for this shared atom
+
+ public:
+ mork_u1 mWeeBookAtom_Body[1]; // 1st byte of immed content vector
+
+ public: // empty construction does nothing
+ morkWeeBookAtom() {}
+ explicit morkWeeBookAtom(mork_aid inAid);
+
+ void InitWeeBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ morkAtomSpace* ioSpace, mork_aid inAid);
+
+ // allow extra trailing byte for a null byte:
+ static mork_size SizeForFill(mork_fill inFill) {
+ return sizeof(morkWeeBookAtom) + inFill;
+ }
+
+ private: // copying is not allowed
+ morkWeeBookAtom(const morkWeeBookAtom& other);
+ morkWeeBookAtom& operator=(const morkWeeBookAtom& other);
+};
+
+/*| BigBookAtom: .
+|*/
+class morkBigBookAtom : public morkBookAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // NOT USED IN "BIG" format atoms
+
+ // morkAtomSpace* mBookAtom_Space; // mBookAtom_Space->SpaceScope() is scope
+ // mork_aid mBookAtom_Id; // identity token for this shared atom
+
+ public:
+ mork_cscode mBigBookAtom_Form; // charset format encoding
+ mork_size mBigBookAtom_Size; // size of content vector
+ mork_u1 mBigBookAtom_Body[1]; // 1st byte of immed content vector
+
+ public: // empty construction does nothing
+ morkBigBookAtom() {}
+ void InitBigBookAtom(morkEnv* ev, const morkBuf& inBuf, mork_cscode inForm,
+ morkAtomSpace* ioSpace, mork_aid inAid);
+
+ // allow extra trailing byte for a null byte:
+ static mork_size SizeForFill(mork_fill inFill) {
+ return sizeof(morkBigBookAtom) + inFill;
+ }
+
+ private: // copying is not allowed
+ morkBigBookAtom(const morkBigBookAtom& other);
+ morkBigBookAtom& operator=(const morkBigBookAtom& other);
+};
+
+/*| MaxBookAtom: .
+|*/
+class morkMaxBookAtom : public morkBigBookAtom { //
+
+ // mork_u1 mAtom_Kind; // identifies a specific atom subclass
+ // mork_u1 mAtom_CellUses; // number of persistent uses in a cell
+ // mork_change mAtom_Change; // how has this atom been changed?
+ // mork_u1 mAtom_Size; // NOT USED IN "BIG" format atoms
+
+ // morkAtomSpace* mBookAtom_Space; // mBookAtom_Space->SpaceScope() is scope
+ // mork_aid mBookAtom_Id; // identity token for this shared atom
+
+ // mork_cscode mBigBookAtom_Form; // charset format encoding
+ // mork_size mBigBookAtom_Size; // size of content vector
+ // mork_u1 mBigBookAtom_Body[ 1 ]; // 1st byte of immed content vector
+
+ public:
+ mork_u1 mMaxBookAtom_Body[morkBookAtom_kMaxBodySize + 3]; // max bytes
+
+ public: // empty construction does nothing
+ morkMaxBookAtom() {}
+ void InitMaxBookAtom(morkEnv* ev, const morkBuf& inBuf, mork_cscode inForm,
+ morkAtomSpace* ioSpace, mork_aid inAid) {
+ this->InitBigBookAtom(ev, inBuf, inForm, ioSpace, inAid);
+ }
+
+ private: // copying is not allowed
+ morkMaxBookAtom(const morkMaxBookAtom& other);
+ morkMaxBookAtom& operator=(const morkMaxBookAtom& other);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKATOM_ */
diff --git a/comm/mailnews/db/mork/morkAtomMap.cpp b/comm/mailnews/db/mork/morkAtomMap.cpp
new file mode 100644
index 0000000000..3ae3422b5a
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtomMap.cpp
@@ -0,0 +1,378 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKATOMMAP_
+# include "morkAtomMap.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKINTMAP_
+# include "morkIntMap.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkAtomAidMap::CloseMorkNode(
+ morkEnv* ev) // CloseAtomAidMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseAtomAidMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkAtomAidMap::~morkAtomAidMap() // assert CloseAtomAidMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkAtomAidMap::morkAtomAidMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+#ifdef MORK_ENABLE_PROBE_MAPS
+ : morkProbeMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkBookAtom*), /*inValSize*/ 0,
+ ioSlotHeap, morkAtomAidMap_kStartSlotCount,
+ /*inZeroIsClearKey*/ morkBool_kTrue)
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ : morkMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkBookAtom*), /*inValSize*/ 0,
+ morkAtomAidMap_kStartSlotCount, ioSlotHeap,
+ /*inHoldChanges*/ morkBool_kFalse)
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+{
+ if (ev->Good()) mNode_Derived = morkDerived_kAtomAidMap;
+}
+
+/*public non-poly*/ void morkAtomAidMap::CloseAtomAidMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->CloseProbeMap(ev);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->CloseMap(ev);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+
+/*virtual*/ mork_test // hit(a,b) implies hash(a) == hash(b)
+morkAtomAidMap::MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const {
+ MORK_USED_1(ev);
+ const morkBookAtom* key = *(const morkBookAtom**)inMapKey;
+ if (key) {
+ mork_bool hit = key->EqualAid(*(const morkBookAtom**)inAppKey);
+ return (hit) ? morkTest_kHit : morkTest_kMiss;
+ } else
+ return morkTest_kVoid;
+}
+
+/*virtual*/ mork_u4 // hit(a,b) implies hash(a) == hash(b)
+morkAtomAidMap::MapHash(morkEnv* ev, const void* inAppKey) const {
+ const morkBookAtom* key = *(const morkBookAtom**)inAppKey;
+ if (key)
+ return key->HashAid();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+
+/*virtual*/ mork_u4 morkAtomAidMap::ProbeMapHashMapKey(
+ morkEnv* ev, const void* inMapKey) const {
+ const morkBookAtom* key = *(const morkBookAtom**)inMapKey;
+ if (key)
+ return key->HashAid();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+#else /*MORK_ENABLE_PROBE_MAPS*/
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool //
+morkAtomAidMap::Equal(morkEnv* ev, const void* inKeyA,
+ const void* inKeyB) const {
+ MORK_USED_1(ev);
+ return (*(const morkBookAtom**)inKeyA)
+ ->EqualAid(*(const morkBookAtom**)inKeyB);
+}
+
+/*virtual*/ mork_u4 //
+morkAtomAidMap::Hash(morkEnv* ev, const void* inKey) const {
+ MORK_USED_1(ev);
+ return (*(const morkBookAtom**)inKey)->HashAid();
+}
+// } ===== end morkMap poly interface =====
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+mork_bool morkAtomAidMap::AddAtom(morkEnv* ev, morkBookAtom* ioAtom) {
+ if (ev->Good()) {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->MapAtPut(ev, &ioAtom, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Put(ev, &ioAtom, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ }
+ return ev->Good();
+}
+
+morkBookAtom* morkAtomAidMap::CutAtom(morkEnv* ev, const morkBookAtom* inAtom) {
+ morkBookAtom* oldKey = 0;
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ MORK_USED_1(inAtom);
+ morkProbeMap::ProbeMapCutError(ev);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Cut(ev, &inAtom, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ return oldKey;
+}
+
+morkBookAtom* morkAtomAidMap::GetAtom(morkEnv* ev, const morkBookAtom* inAtom) {
+ morkBookAtom* key = 0; // old val in the map
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->MapAt(ev, &inAtom, &key, /*val*/ (void*)0);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Get(ev, &inAtom, &key, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ return key;
+}
+
+morkBookAtom* morkAtomAidMap::GetAid(morkEnv* ev, mork_aid inAid) {
+ morkWeeBookAtom weeAtom(inAid);
+ morkBookAtom* key = &weeAtom; // we need a pointer
+ morkBookAtom* oldKey = 0; // old key in the map
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->MapAt(ev, &key, &oldKey, /*val*/ (void*)0);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Get(ev, &key, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ return oldKey;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkAtomBodyMap::CloseMorkNode(
+ morkEnv* ev) // CloseAtomBodyMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseAtomBodyMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkAtomBodyMap::~morkAtomBodyMap() // assert CloseAtomBodyMap() executed
+ // earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkAtomBodyMap::morkAtomBodyMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+#ifdef MORK_ENABLE_PROBE_MAPS
+ : morkProbeMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkBookAtom*), /*inValSize*/ 0,
+ ioSlotHeap, morkAtomBodyMap_kStartSlotCount,
+ /*inZeroIsClearKey*/ morkBool_kTrue)
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ : morkMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkBookAtom*), /*inValSize*/ 0,
+ morkAtomBodyMap_kStartSlotCount, ioSlotHeap,
+ /*inHoldChanges*/ morkBool_kFalse)
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+{
+ if (ev->Good()) mNode_Derived = morkDerived_kAtomBodyMap;
+}
+
+/*public non-poly*/ void morkAtomBodyMap::CloseAtomBodyMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->CloseProbeMap(ev);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->CloseMap(ev);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+#ifdef MORK_ENABLE_PROBE_MAPS
+
+/*virtual*/ mork_test // hit(a,b) implies hash(a) == hash(b)
+morkAtomBodyMap::MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const {
+ const morkBookAtom* key = *(const morkBookAtom**)inMapKey;
+ if (key) {
+ return (key->EqualFormAndBody(ev, *(const morkBookAtom**)inAppKey))
+ ? morkTest_kHit
+ : morkTest_kMiss;
+ } else
+ return morkTest_kVoid;
+}
+
+/*virtual*/ mork_u4 // hit(a,b) implies hash(a) == hash(b)
+morkAtomBodyMap::MapHash(morkEnv* ev, const void* inAppKey) const {
+ const morkBookAtom* key = *(const morkBookAtom**)inAppKey;
+ if (key)
+ return key->HashFormAndBody(ev);
+ else
+ return 0;
+}
+
+/*virtual*/ mork_u4 morkAtomBodyMap::ProbeMapHashMapKey(
+ morkEnv* ev, const void* inMapKey) const {
+ const morkBookAtom* key = *(const morkBookAtom**)inMapKey;
+ if (key)
+ return key->HashFormAndBody(ev);
+ else
+ return 0;
+}
+#else /*MORK_ENABLE_PROBE_MAPS*/
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool //
+morkAtomBodyMap::Equal(morkEnv* ev, const void* inKeyA,
+ const void* inKeyB) const {
+ return (*(const morkBookAtom**)inKeyA)
+ ->EqualFormAndBody(ev, *(const morkBookAtom**)inKeyB);
+}
+
+/*virtual*/ mork_u4 //
+morkAtomBodyMap::Hash(morkEnv* ev, const void* inKey) const {
+ return (*(const morkBookAtom**)inKey)->HashFormAndBody(ev);
+}
+// } ===== end morkMap poly interface =====
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+mork_bool morkAtomBodyMap::AddAtom(morkEnv* ev, morkBookAtom* ioAtom) {
+ if (ev->Good()) {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->MapAtPut(ev, &ioAtom, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Put(ev, &ioAtom, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ }
+ return ev->Good();
+}
+
+morkBookAtom* morkAtomBodyMap::CutAtom(morkEnv* ev,
+ const morkBookAtom* inAtom) {
+ morkBookAtom* oldKey = 0;
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ MORK_USED_1(inAtom);
+ morkProbeMap::ProbeMapCutError(ev);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Cut(ev, &inAtom, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ return oldKey;
+}
+
+morkBookAtom* morkAtomBodyMap::GetAtom(morkEnv* ev,
+ const morkBookAtom* inAtom) {
+ morkBookAtom* key = 0; // old val in the map
+#ifdef MORK_ENABLE_PROBE_MAPS
+ this->MapAt(ev, &inAtom, &key, /*val*/ (void*)0);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ this->Get(ev, &inAtom, &key, /*val*/ (void*)0, (mork_change**)0);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ return key;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkAtomRowMap::~morkAtomRowMap() {}
+
+// I changed to sizeof(mork_ip) from sizeof(mork_aid) to fix a crash on
+// 64 bit machines. I am not sure it was the right way to fix the problem,
+// but it does stop the crash. Perhaps we should be using the
+// morkPointerMap instead?
+morkAtomRowMap::morkAtomRowMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ mork_column inIndexColumn)
+ : morkIntMap(ev, inUsage, sizeof(mork_ip), ioHeap, ioSlotHeap,
+ /*inHoldChanges*/ morkBool_kFalse),
+ mAtomRowMap_IndexColumn(inIndexColumn) {
+ if (ev->Good()) mNode_Derived = morkDerived_kAtomRowMap;
+}
+
+void morkAtomRowMap::AddRow(morkEnv* ev, morkRow* ioRow)
+// add ioRow only if it contains a cell in mAtomRowMap_IndexColumn.
+{
+ mork_aid aid = ioRow->GetCellAtomAid(ev, mAtomRowMap_IndexColumn);
+ if (aid) this->AddAid(ev, aid, ioRow);
+}
+
+void morkAtomRowMap::CutRow(morkEnv* ev, morkRow* ioRow)
+// cut ioRow only if it contains a cell in mAtomRowMap_IndexColumn.
+{
+ mork_aid aid = ioRow->GetCellAtomAid(ev, mAtomRowMap_IndexColumn);
+ if (aid) this->CutAid(ev, aid);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkAtomMap.h b/comm/mailnews/db/mork/morkAtomMap.h
new file mode 100644
index 0000000000..895fbedc74
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtomMap.h
@@ -0,0 +1,394 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKATOMMAP_
+#define _MORKATOMMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKPROBEMAP_
+# include "morkProbeMap.h"
+#endif
+
+#ifndef _MORKINTMAP_
+# include "morkIntMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kAtomAidMap /*i*/ 0x6141 /* ascii 'aA' */
+
+#define morkAtomAidMap_kStartSlotCount 23
+
+/*| morkAtomAidMap: keys of morkBookAtom organized by atom ID
+|*/
+#ifdef MORK_ENABLE_PROBE_MAPS
+class morkAtomAidMap : public morkProbeMap { // for mapping tokens to maps
+#else /*MORK_ENABLE_PROBE_MAPS*/
+class morkAtomAidMap : public morkMap { // for mapping tokens to maps
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseAtomAidMap() only if open
+ virtual ~morkAtomAidMap(); // assert that CloseAtomAidMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkAtomAidMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseAtomAidMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsAtomAidMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kAtomAidMap;
+ }
+ // } ===== end morkNode methods =====
+
+ public:
+#ifdef MORK_ENABLE_PROBE_MAPS
+ // { ===== begin morkProbeMap methods =====
+ virtual mork_test // hit(a,b) implies hash(a) == hash(b)
+ MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const override;
+
+ virtual mork_u4 // hit(a,b) implies hash(a) == hash(b)
+ MapHash(morkEnv* ev, const void* inAppKey) const override;
+
+ virtual mork_u4 ProbeMapHashMapKey(morkEnv* ev,
+ const void* inMapKey) const override;
+
+ // virtual mork_bool ProbeMapIsKeyNil(morkEnv* ev, void* ioMapKey);
+
+ // virtual void ProbeMapClearKey(morkEnv* ev, // put 'nil' into all keys
+ // inside map
+ // void* ioMapKey, mork_count inKeyCount); // array of keys inside map
+
+ // virtual void ProbeMapPushIn(morkEnv* ev, // move (key,val) into the map
+ // const void* inAppKey, const void* inAppVal, // (key,val) outside map
+ // void* outMapKey, void* outMapVal); // (key,val) inside map
+
+ // virtual void ProbeMapPullOut(morkEnv* ev, // move (key,val) out from the
+ // map
+ // const void* inMapKey, const void* inMapVal, // (key,val) inside map
+ // void* outAppKey, void* outAppVal) const; // (key,val) outside map
+ // } ===== end morkProbeMap methods =====
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // note: equal(a,b) implies hash(a) == hash(b)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const override;
+ // implemented using morkBookAtom::HashAid()
+
+ virtual mork_u4 // note: equal(a,b) implies hash(a) == hash(b)
+ Hash(morkEnv* ev, const void* inKey) const override;
+ // implemented using morkBookAtom::EqualAid()
+// } ===== end morkMap poly interface =====
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ public: // other map methods
+ mork_bool AddAtom(morkEnv* ev, morkBookAtom* ioAtom);
+ // AddAtom() returns ev->Good()
+
+ morkBookAtom* CutAtom(morkEnv* ev, const morkBookAtom* inAtom);
+ // CutAtom() returns the atom removed equal to inAtom, if there was one
+
+ morkBookAtom* GetAtom(morkEnv* ev, const morkBookAtom* inAtom);
+ // GetAtom() returns the atom equal to inAtom, or else nil
+
+ morkBookAtom* GetAid(morkEnv* ev, mork_aid inAid);
+ // GetAid() returns the atom equal to inAid, or else nil
+
+ // note the atoms are owned elsewhere, usually by morkAtomSpace
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakAtomAidMap(morkAtomAidMap* me, morkEnv* ev,
+ morkAtomAidMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongAtomAidMap(morkAtomAidMap* me, morkEnv* ev,
+ morkAtomAidMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+class morkAtomAidMapIter : public morkProbeMapIter { // typesafe wrapper class
+#else /*MORK_ENABLE_PROBE_MAPS*/
+class morkAtomAidMapIter : public morkMapIter { // typesafe wrapper class
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ public:
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkAtomAidMapIter(morkEnv* ev, morkAtomAidMap* ioMap)
+ : morkProbeMapIter(ev, ioMap) {}
+
+ morkAtomAidMapIter() : morkProbeMapIter() {}
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkAtomAidMapIter(morkEnv* ev, morkAtomAidMap* ioMap)
+ : morkMapIter(ev, ioMap) {}
+
+ morkAtomAidMapIter() : morkMapIter() {}
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ void InitAtomAidMapIter(morkEnv* ev, morkAtomAidMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->First(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* NextAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->Next(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* HereAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->Here(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* CutHereAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->CutHere(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kAtomBodyMap /*i*/ 0x6142 /* ascii 'aB' */
+
+#define morkAtomBodyMap_kStartSlotCount 23
+
+/*| morkAtomBodyMap: keys of morkBookAtom organized by body bytes
+|*/
+#ifdef MORK_ENABLE_PROBE_MAPS
+class morkAtomBodyMap : public morkProbeMap { // for mapping tokens to maps
+#else /*MORK_ENABLE_PROBE_MAPS*/
+class morkAtomBodyMap : public morkMap { // for mapping tokens to maps
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseAtomBodyMap() only if open
+ virtual ~morkAtomBodyMap(); // assert CloseAtomBodyMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkAtomBodyMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseAtomBodyMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsAtomBodyMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kAtomBodyMap;
+ }
+ // } ===== end morkNode methods =====
+
+ public:
+#ifdef MORK_ENABLE_PROBE_MAPS
+ // { ===== begin morkProbeMap methods =====
+ virtual mork_test // hit(a,b) implies hash(a) == hash(b)
+ MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const override;
+
+ virtual mork_u4 // hit(a,b) implies hash(a) == hash(b)
+ MapHash(morkEnv* ev, const void* inAppKey) const override;
+
+ virtual mork_u4 ProbeMapHashMapKey(morkEnv* ev,
+ const void* inMapKey) const override;
+
+ // virtual mork_bool ProbeMapIsKeyNil(morkEnv* ev, void* ioMapKey);
+
+ // virtual void ProbeMapClearKey(morkEnv* ev, // put 'nil' into all keys
+ // inside map
+ // void* ioMapKey, mork_count inKeyCount); // array of keys inside map
+
+ // virtual void ProbeMapPushIn(morkEnv* ev, // move (key,val) into the map
+ // const void* inAppKey, const void* inAppVal, // (key,val) outside map
+ // void* outMapKey, void* outMapVal); // (key,val) inside map
+
+ // virtual void ProbeMapPullOut(morkEnv* ev, // move (key,val) out from the
+ // map
+ // const void* inMapKey, const void* inMapVal, // (key,val) inside map
+ // void* outAppKey, void* outAppVal) const; // (key,val) outside map
+ // } ===== end morkProbeMap methods =====
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // note: equal(a,b) implies hash(a) == hash(b)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const override;
+ // implemented using morkBookAtom::EqualFormAndBody()
+
+ virtual mork_u4 // note: equal(a,b) implies hash(a) == hash(b)
+ Hash(morkEnv* ev, const void* inKey) const override;
+ // implemented using morkBookAtom::HashFormAndBody()
+// } ===== end morkMap poly interface =====
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ public: // other map methods
+ mork_bool AddAtom(morkEnv* ev, morkBookAtom* ioAtom);
+ // AddAtom() returns ev->Good()
+
+ morkBookAtom* CutAtom(morkEnv* ev, const morkBookAtom* inAtom);
+ // CutAtom() returns the atom removed equal to inAtom, if there was one
+
+ morkBookAtom* GetAtom(morkEnv* ev, const morkBookAtom* inAtom);
+ // GetAtom() returns the atom equal to inAtom, or else nil
+
+ // note the atoms are owned elsewhere, usually by morkAtomSpace
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakAtomBodyMap(morkAtomBodyMap* me, morkEnv* ev,
+ morkAtomBodyMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongAtomBodyMap(morkAtomBodyMap* me, morkEnv* ev,
+ morkAtomBodyMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+class morkAtomBodyMapIter : public morkProbeMapIter { // typesafe wrapper class
+#else /*MORK_ENABLE_PROBE_MAPS*/
+class morkAtomBodyMapIter : public morkMapIter { // typesafe wrapper class
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ public:
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkAtomBodyMapIter(morkEnv* ev, morkAtomBodyMap* ioMap)
+ : morkProbeMapIter(ev, ioMap) {}
+
+ morkAtomBodyMapIter() : morkProbeMapIter() {}
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkAtomBodyMapIter(morkEnv* ev, morkAtomBodyMap* ioMap)
+ : morkMapIter(ev, ioMap) {}
+
+ morkAtomBodyMapIter() : morkMapIter() {}
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ void InitAtomBodyMapIter(morkEnv* ev, morkAtomBodyMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->First(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* NextAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->Next(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* HereAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->Here(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* CutHereAtom(morkEnv* ev, morkBookAtom** outAtomPtr) {
+ return this->CutHere(ev, outAtomPtr, /*val*/ (void*)0);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kAtomRowMap /*i*/ 0x6152 /* ascii 'aR' */
+
+/*| morkAtomRowMap: maps morkAtom* -> morkRow*
+|*/
+class morkAtomRowMap : public morkIntMap { // for mapping atoms to rows
+
+ public:
+ mork_column mAtomRowMap_IndexColumn; // row column being indexed
+
+ public:
+ virtual ~morkAtomRowMap();
+ morkAtomRowMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_column inIndexColumn);
+
+ public: // adding and cutting from morkRow instance candidate
+ void AddRow(morkEnv* ev, morkRow* ioRow);
+ // add ioRow only if it contains a cell in mAtomRowMap_IndexColumn.
+
+ void CutRow(morkEnv* ev, morkRow* ioRow);
+ // cut ioRow only if it contains a cell in mAtomRowMap_IndexColumn.
+
+ public: // other map methods
+ mork_bool AddAid(morkEnv* ev, mork_aid inAid, morkRow* ioRow) {
+ return this->AddInt(ev, inAid, ioRow);
+ }
+ // the AddAid() boolean return equals ev->Good().
+
+ mork_bool CutAid(morkEnv* ev, mork_aid inAid) {
+ return this->CutInt(ev, inAid);
+ }
+ // The CutAid() boolean return indicates whether removal happened.
+
+ morkRow* GetAid(morkEnv* ev, mork_aid inAid) {
+ return (morkRow*)this->GetInt(ev, inAid);
+ }
+ // Note the returned space does NOT have an increase in refcount for this.
+
+ public: // dynamic type identification
+ mork_bool IsAtomRowMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kAtomRowMap;
+ }
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakAtomRowMap(morkAtomRowMap* me, morkEnv* ev,
+ morkAtomRowMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongAtomRowMap(morkAtomRowMap* me, morkEnv* ev,
+ morkAtomRowMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+class morkAtomRowMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkAtomRowMapIter(morkEnv* ev, morkAtomRowMap* ioMap)
+ : morkMapIter(ev, ioMap) {}
+
+ morkAtomRowMapIter() : morkMapIter() {}
+ void InitAtomRowMapIter(morkEnv* ev, morkAtomRowMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstAtomAndRow(morkEnv* ev, morkAtom** outAtom,
+ morkRow** outRow) {
+ return this->First(ev, outAtom, outRow);
+ }
+
+ mork_change* NextAtomAndRow(morkEnv* ev, morkAtom** outAtom,
+ morkRow** outRow) {
+ return this->Next(ev, outAtom, outRow);
+ }
+
+ mork_change* HereAtomAndRow(morkEnv* ev, morkAtom** outAtom,
+ morkRow** outRow) {
+ return this->Here(ev, outAtom, outRow);
+ }
+
+ mork_change* CutHereAtomAndRow(morkEnv* ev, morkAtom** outAtom,
+ morkRow** outRow) {
+ return this->CutHere(ev, outAtom, outRow);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKATOMMAP_ */
diff --git a/comm/mailnews/db/mork/morkAtomSpace.cpp b/comm/mailnews/db/mork/morkAtomSpace.cpp
new file mode 100644
index 0000000000..5ecdfe2b4c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtomSpace.cpp
@@ -0,0 +1,233 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkAtomSpace::CloseMorkNode(
+ morkEnv* ev) // CloseAtomSpace() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseAtomSpace(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkAtomSpace::~morkAtomSpace() // assert CloseAtomSpace() executed earlier
+{
+ MORK_ASSERT(mAtomSpace_HighUnderId == 0);
+ MORK_ASSERT(mAtomSpace_HighOverId == 0);
+ MORK_ASSERT(this->IsShutNode());
+ MORK_ASSERT(mAtomSpace_AtomAids.IsShutNode());
+ MORK_ASSERT(mAtomSpace_AtomBodies.IsShutNode());
+}
+
+/*public non-poly*/
+morkAtomSpace::morkAtomSpace(morkEnv* ev, const morkUsage& inUsage,
+ mork_scope inScope, morkStore* ioStore,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkSpace(ev, inUsage, inScope, ioStore, ioHeap, ioSlotHeap),
+ mAtomSpace_HighUnderId(morkAtomSpace_kMinUnderId),
+ mAtomSpace_HighOverId(morkAtomSpace_kMinOverId),
+ mAtomSpace_AtomAids(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioSlotHeap),
+ mAtomSpace_AtomBodies(ev, morkUsage::kMember, (nsIMdbHeap*)0,
+ ioSlotHeap) {
+ // the morkSpace base constructor handles any dirty propagation
+ if (ev->Good()) mNode_Derived = morkDerived_kAtomSpace;
+}
+
+/*public non-poly*/ void morkAtomSpace::CloseAtomSpace(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mAtomSpace_AtomBodies.CloseMorkNode(ev);
+ morkStore* store = mSpace_Store;
+ if (store) this->CutAllAtoms(ev, &store->mStore_Pool);
+
+ mAtomSpace_AtomAids.CloseMorkNode(ev);
+ this->CloseSpace(ev);
+ mAtomSpace_HighUnderId = 0;
+ mAtomSpace_HighOverId = 0;
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkAtomSpace::NonAtomSpaceTypeError(morkEnv* ev) {
+ ev->NewError("non morkAtomSpace");
+}
+
+mork_num morkAtomSpace::CutAllAtoms(morkEnv* ev, morkPool* ioPool) {
+#ifdef MORK_ENABLE_ZONE_ARENAS
+ MORK_USED_2(ev, ioPool);
+ return 0;
+#else /*MORK_ENABLE_ZONE_ARENAS*/
+ if (this->IsAtomSpaceClean()) this->MaybeDirtyStoreAndSpace();
+
+ mork_num outSlots = mAtomSpace_AtomAids.MapFill();
+ morkBookAtom* a = 0; // old key atom in the map
+
+ morkStore* store = mSpace_Store;
+ mork_change* c = 0;
+ morkAtomAidMapIter i(ev, &mAtomSpace_AtomAids);
+ for (c = i.FirstAtom(ev, &a); c; c = i.NextAtom(ev, &a)) {
+ if (a) ioPool->ZapAtom(ev, a, &store->mStore_Zone);
+
+# ifdef MORK_ENABLE_PROBE_MAPS
+ // do not cut anything from the map
+# else /*MORK_ENABLE_PROBE_MAPS*/
+ i.CutHereAtom(ev, /*key*/ (morkBookAtom**)0);
+# endif /*MORK_ENABLE_PROBE_MAPS*/
+ }
+
+ return outSlots;
+#endif /*MORK_ENABLE_ZONE_ARENAS*/
+}
+
+morkBookAtom* morkAtomSpace::MakeBookAtomCopyWithAid(
+ morkEnv* ev, const morkFarBookAtom& inAtom, mork_aid inAid)
+// Make copy of inAtom and put it in both maps, using specified ID.
+{
+ morkBookAtom* outAtom = 0;
+ morkStore* store = mSpace_Store;
+ if (ev->Good() && store) {
+ morkPool* pool = this->GetSpaceStorePool();
+ outAtom = pool->NewFarBookAtomCopy(ev, inAtom, &store->mStore_Zone);
+ if (outAtom) {
+ if (store->mStore_CanDirty) {
+ outAtom->SetAtomDirty();
+ if (this->IsAtomSpaceClean()) this->MaybeDirtyStoreAndSpace();
+ }
+
+ outAtom->mBookAtom_Id = inAid;
+ outAtom->mBookAtom_Space = this;
+ mAtomSpace_AtomAids.AddAtom(ev, outAtom);
+ mAtomSpace_AtomBodies.AddAtom(ev, outAtom);
+ if (this->SpaceScope() == morkAtomSpace_kColumnScope)
+ outAtom->MakeCellUseForever(ev);
+
+ if (mAtomSpace_HighUnderId <= inAid) mAtomSpace_HighUnderId = inAid + 1;
+ }
+ }
+ return outAtom;
+}
+
+morkBookAtom* morkAtomSpace::MakeBookAtomCopy(morkEnv* ev,
+ const morkFarBookAtom& inAtom)
+// make copy of inAtom and put it in both maps, using a new ID as needed.
+{
+ morkBookAtom* outAtom = 0;
+ morkStore* store = mSpace_Store;
+ if (ev->Good() && store) {
+ if (store->mStore_CanAutoAssignAtomIdentity) {
+ morkPool* pool = this->GetSpaceStorePool();
+ morkBookAtom* atom =
+ pool->NewFarBookAtomCopy(ev, inAtom, &mSpace_Store->mStore_Zone);
+ if (atom) {
+ mork_aid id = this->MakeNewAtomId(ev, atom);
+ if (id) {
+ if (store->mStore_CanDirty) {
+ atom->SetAtomDirty();
+ if (this->IsAtomSpaceClean()) this->MaybeDirtyStoreAndSpace();
+ }
+
+ outAtom = atom;
+ atom->mBookAtom_Space = this;
+ mAtomSpace_AtomAids.AddAtom(ev, atom);
+ mAtomSpace_AtomBodies.AddAtom(ev, atom);
+ if (this->SpaceScope() == morkAtomSpace_kColumnScope)
+ outAtom->MakeCellUseForever(ev);
+ } else
+ pool->ZapAtom(ev, atom, &mSpace_Store->mStore_Zone);
+ }
+ } else
+ mSpace_Store->CannotAutoAssignAtomIdentityError(ev);
+ }
+ return outAtom;
+}
+
+mork_aid morkAtomSpace::MakeNewAtomId(morkEnv* ev, morkBookAtom* ioAtom) {
+ mork_aid outAid = 0;
+ mork_tid id = mAtomSpace_HighUnderId;
+ mork_num count = 8; // try up to eight times
+
+ while (!outAid && count) // still trying to find an unused table ID?
+ {
+ --count;
+ ioAtom->mBookAtom_Id = id;
+ if (!mAtomSpace_AtomAids.GetAtom(ev, ioAtom))
+ outAid = id;
+ else {
+ MORK_ASSERT(morkBool_kFalse); // alert developer about ID problems
+ ++id;
+ }
+ }
+
+ mAtomSpace_HighUnderId = id + 1;
+ return outAid;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkAtomSpaceMap::~morkAtomSpaceMap() {}
+
+morkAtomSpaceMap::morkAtomSpaceMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkNodeMap(ev, inUsage, ioHeap, ioSlotHeap) {
+ if (ev->Good()) mNode_Derived = morkDerived_kAtomSpaceMap;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkAtomSpace.h b/comm/mailnews/db/mork/morkAtomSpace.h
new file mode 100644
index 0000000000..d057ad6cfd
--- /dev/null
+++ b/comm/mailnews/db/mork/morkAtomSpace.h
@@ -0,0 +1,227 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKATOMSPACE_
+#define _MORKATOMSPACE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKATOMMAP_
+# include "morkAtomMap.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| kMinUnderId: the smallest ID we auto-assign to the 'under' namespace
+**| reserved for tokens expected to occur very frequently, such as the names
+**| of columns. We reserve single byte ids in the ASCII range to correspond
+**| one-to-one to those tokens consisting single ASCII characters (so that
+**| this assignment is always known and constant). So we start at 0x80, and
+**| then reserve the upper half of two hex digit ids and all the three hex
+**| digit IDs for the 'under' namespace for common tokens.
+|*/
+#define morkAtomSpace_kMinUnderId 0x80 /* low 7 bits mean byte tokens */
+
+#define morkAtomSpace_kMaxSevenBitAid 0x7F /* low seven bit integer ID */
+
+/*| kMinOverId: the smallest ID we auto-assign to the 'over' namespace that
+**| might include very large numbers of tokens that are used infrequently,
+**| so that we care less whether the shortest hex representation is used.
+**| So we start all IDs for 'over' category tokens at a value range that
+**| needs at least four hex digits, so we can reserve three hex digits and
+**| shorter for more commonly occurring tokens in the 'under' category.
+|*/
+#define morkAtomSpace_kMinOverId 0x1000 /* using at least four hex bytes */
+
+#define morkDerived_kAtomSpace /*i*/ 0x6153 /* ascii 'aS' */
+
+#define morkAtomSpace_kColumnScope \
+ ((mork_scope)'c') /* column scope is forever */
+
+/*| morkAtomSpace:
+|*/
+class morkAtomSpace : public morkSpace { //
+
+ // public: // slots inherited from morkSpace (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkStore* mSpace_Store; // weak ref to containing store
+
+ // mork_bool mSpace_DoAutoIDs; // whether db should assign member IDs
+ // mork_bool mSpace_HaveDoneAutoIDs; // whether actually auto assigned IDs
+ // mork_u1 mSpace_Pad[ 2 ]; // pad to u4 alignment
+
+ public: // state is public because the entire Mork system is private
+ mork_aid mAtomSpace_HighUnderId; // high ID in 'under' range
+ mork_aid mAtomSpace_HighOverId; // high ID in 'over' range
+
+ morkAtomAidMap mAtomSpace_AtomAids; // all atoms in space by ID
+ morkAtomBodyMap mAtomSpace_AtomBodies; // all atoms in space by body
+
+ public: // more specific dirty methods for atom space:
+ void SetAtomSpaceDirty() { this->SetNodeDirty(); }
+ void SetAtomSpaceClean() { this->SetNodeClean(); }
+
+ mork_bool IsAtomSpaceClean() const { return this->IsNodeClean(); }
+ mork_bool IsAtomSpaceDirty() const { return this->IsNodeDirty(); }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseAtomSpace() only if open
+ virtual ~morkAtomSpace(); // assert that CloseAtomSpace() executed earlier
+
+ public: // morkMap construction & destruction
+ morkAtomSpace(morkEnv* ev, const morkUsage& inUsage, mork_scope inScope,
+ morkStore* ioStore, nsIMdbHeap* ioNodeHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseAtomSpace(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsAtomSpace() const {
+ return IsNode() && mNode_Derived == morkDerived_kAtomSpace;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ void NonAtomSpaceTypeError(morkEnv* ev);
+
+ public: // setup
+ mork_bool MarkAllAtomSpaceContentDirty(morkEnv* ev);
+ // MarkAllAtomSpaceContentDirty() visits every space object and marks
+ // them dirty, including every table, row, cell, and atom. The return
+ // equals ev->Good(), to show whether any error happened. This method is
+ // intended for use in the beginning of a "compress commit" which writes
+ // all store content, whether dirty or not. We dirty everything first so
+ // that later iterations over content can mark things clean as they are
+ // written, and organize the process of serialization so that objects are
+ // written only at need (because of being dirty).
+
+ public: // other space methods
+ // void ReserveColumnAidCount(mork_count inCount)
+ // {
+ // mAtomSpace_HighUnderId = morkAtomSpace_kMinUnderId + inCount;
+ // mAtomSpace_HighOverId = morkAtomSpace_kMinOverId + inCount;
+ // }
+
+ mork_num CutAllAtoms(morkEnv* ev, morkPool* ioPool);
+ // CutAllAtoms() puts all the atoms back in the pool.
+
+ morkBookAtom* MakeBookAtomCopyWithAid(morkEnv* ev,
+ const morkFarBookAtom& inAtom,
+ mork_aid inAid);
+ // Make copy of inAtom and put it in both maps, using specified ID.
+
+ morkBookAtom* MakeBookAtomCopy(morkEnv* ev, const morkFarBookAtom& inAtom);
+ // Make copy of inAtom and put it in both maps, using a new ID as needed.
+
+ mork_aid MakeNewAtomId(morkEnv* ev, morkBookAtom* ioAtom);
+ // generate an unused atom id.
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakAtomSpace(morkAtomSpace* me, morkEnv* ev,
+ morkAtomSpace** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongAtomSpace(morkAtomSpace* me, morkEnv* ev,
+ morkAtomSpace** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kAtomSpaceMap /*i*/ 0x615A /* ascii 'aZ' */
+
+/*| morkAtomSpaceMap: maps mork_scope -> morkAtomSpace
+|*/
+class morkAtomSpaceMap : public morkNodeMap { // for mapping tokens to tables
+
+ public:
+ virtual ~morkAtomSpaceMap();
+ morkAtomSpaceMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+
+ public: // other map methods
+ mork_bool AddAtomSpace(morkEnv* ev, morkAtomSpace* ioAtomSpace) {
+ return this->AddNode(ev, ioAtomSpace->SpaceScope(), ioAtomSpace);
+ }
+ // the AddAtomSpace() boolean return equals ev->Good().
+
+ mork_bool CutAtomSpace(morkEnv* ev, mork_scope inScope) {
+ return this->CutNode(ev, inScope);
+ }
+ // The CutAtomSpace() boolean return indicates whether removal happened.
+
+ morkAtomSpace* GetAtomSpace(morkEnv* ev, mork_scope inScope) {
+ return (morkAtomSpace*)this->GetNode(ev, inScope);
+ }
+ // Note the returned space does NOT have an increase in refcount for this.
+
+ mork_num CutAllAtomSpaces(morkEnv* ev) { return this->CutAllNodes(ev); }
+ // CutAllAtomSpaces() releases all the referenced table values.
+};
+
+class morkAtomSpaceMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkAtomSpaceMapIter(morkEnv* ev, morkAtomSpaceMap* ioMap)
+ : morkMapIter(ev, ioMap) {}
+
+ morkAtomSpaceMapIter() : morkMapIter() {}
+ void InitAtomSpaceMapIter(morkEnv* ev, morkAtomSpaceMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstAtomSpace(morkEnv* ev, mork_scope* outScope,
+ morkAtomSpace** outAtomSpace) {
+ return this->First(ev, outScope, outAtomSpace);
+ }
+
+ mork_change* NextAtomSpace(morkEnv* ev, mork_scope* outScope,
+ morkAtomSpace** outAtomSpace) {
+ return this->Next(ev, outScope, outAtomSpace);
+ }
+
+ mork_change* HereAtomSpace(morkEnv* ev, mork_scope* outScope,
+ morkAtomSpace** outAtomSpace) {
+ return this->Here(ev, outScope, outAtomSpace);
+ }
+
+ mork_change* CutHereAtomSpace(morkEnv* ev, mork_scope* outScope,
+ morkAtomSpace** outAtomSpace) {
+ return this->CutHere(ev, outScope, outAtomSpace);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKATOMSPACE_ */
diff --git a/comm/mailnews/db/mork/morkBead.cpp b/comm/mailnews/db/mork/morkBead.cpp
new file mode 100644
index 0000000000..839322621f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBead.cpp
@@ -0,0 +1,361 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKBEAD_
+# include "morkBead.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkBead::CloseMorkNode(
+ morkEnv* ev) // CloseBead() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseBead(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkBead::~morkBead() // assert CloseBead() executed earlier
+{
+ MORK_ASSERT(mBead_Color == 0 || mNode_Usage == morkUsage_kStack);
+}
+
+/*public non-poly*/
+morkBead::morkBead(mork_color inBeadColor)
+ : morkNode(morkUsage_kStack), mBead_Color(inBeadColor) {}
+
+/*public non-poly*/
+morkBead::morkBead(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor)
+ : morkNode(inUsage, ioHeap), mBead_Color(inBeadColor) {}
+
+/*public non-poly*/
+morkBead::morkBead(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor)
+ : morkNode(ev, inUsage, ioHeap), mBead_Color(inBeadColor) {
+ if (ev->Good()) {
+ if (ev->Good()) mNode_Derived = morkDerived_kBead;
+ }
+}
+
+/*public non-poly*/ void morkBead::CloseBead(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ if (!this->IsShutNode()) {
+ mBead_Color = 0;
+ this->MarkShut();
+ }
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkBeadMap::CloseMorkNode(
+ morkEnv* ev) // CloseBeadMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseBeadMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkBeadMap::~morkBeadMap() // assert CloseBeadMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkBeadMap::morkBeadMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkMap(ev, inUsage, ioHeap, sizeof(morkBead*), /*inValSize*/ 0,
+ /*slotCount*/ 11, ioSlotHeap, /*holdChanges*/ morkBool_kFalse) {
+ if (ev->Good()) mNode_Derived = morkDerived_kBeadMap;
+}
+
+/*public non-poly*/ void morkBeadMap::CloseBeadMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CutAllBeads(ev);
+ this->CloseMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+mork_bool morkBeadMap::AddBead(morkEnv* ev, morkBead* ioBead)
+// the AddBead() boolean return equals ev->Good().
+{
+ if (ioBead && ev->Good()) {
+ morkBead* oldBead = 0; // old key in the map
+
+ mork_bool put =
+ this->Put(ev, &ioBead, /*val*/ (void*)0,
+ /*key*/ &oldBead, /*val*/ (void*)0, (mork_change**)0);
+
+ if (put) // replaced an existing key?
+ {
+ if (oldBead != ioBead) // new bead was not already in table?
+ ioBead->AddStrongRef(ev); // now there's another ref
+
+ if (oldBead && oldBead != ioBead) // need to release old node?
+ oldBead->CutStrongRef(ev);
+ } else
+ ioBead->AddStrongRef(ev); // another ref if not already in table
+ } else if (!ioBead)
+ ev->NilPointerError();
+
+ return ev->Good();
+}
+
+mork_bool morkBeadMap::CutBead(morkEnv* ev, mork_color inColor) {
+ morkBead* oldBead = 0; // old key in the map
+ morkBead bead(inColor);
+ morkBead* key = &bead;
+
+ mork_bool outCutNode =
+ this->Cut(ev, &key,
+ /*key*/ &oldBead, /*val*/ (void*)0, (mork_change**)0);
+
+ if (oldBead) oldBead->CutStrongRef(ev);
+
+ bead.CloseBead(ev);
+ return outCutNode;
+}
+
+morkBead* morkBeadMap::GetBead(morkEnv* ev, mork_color inColor)
+// Note the returned bead does NOT have an increase in refcount for this.
+{
+ morkBead* oldBead = 0; // old key in the map
+ morkBead bead(inColor);
+ morkBead* key = &bead;
+
+ this->Get(ev, &key, /*key*/ &oldBead, /*val*/ (void*)0, (mork_change**)0);
+
+ bead.CloseBead(ev);
+ return oldBead;
+}
+
+mork_num morkBeadMap::CutAllBeads(morkEnv* ev)
+// CutAllBeads() releases all the referenced beads.
+{
+ mork_num outSlots = mMap_Slots;
+
+ morkBeadMapIter i(ev, this);
+ morkBead* b = i.FirstBead(ev);
+
+ while (b) {
+ b->CutStrongRef(ev);
+ i.CutHereBead(ev);
+ b = i.NextBead(ev);
+ }
+
+ return outSlots;
+}
+
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool morkBeadMap::Equal(morkEnv* ev, const void* inKeyA,
+ const void* inKeyB) const {
+ MORK_USED_1(ev);
+ return (*(const morkBead**)inKeyA)->BeadEqual(*(const morkBead**)inKeyB);
+}
+
+/*virtual*/ mork_u4 morkBeadMap::Hash(morkEnv* ev, const void* inKey) const {
+ MORK_USED_1(ev);
+ return (*(const morkBead**)inKey)->BeadHash();
+}
+// } ===== end morkMap poly interface =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkBead* morkBeadMapIter::FirstBead(morkEnv* ev) {
+ morkBead* bead = 0;
+ this->First(ev, &bead, /*val*/ (void*)0);
+ return bead;
+}
+
+morkBead* morkBeadMapIter::NextBead(morkEnv* ev) {
+ morkBead* bead = 0;
+ this->Next(ev, &bead, /*val*/ (void*)0);
+ return bead;
+}
+
+morkBead* morkBeadMapIter::HereBead(morkEnv* ev) {
+ morkBead* bead = 0;
+ this->Here(ev, &bead, /*val*/ (void*)0);
+ return bead;
+}
+
+void morkBeadMapIter::CutHereBead(morkEnv* ev) {
+ this->CutHere(ev, /*key*/ (void*)0, /*val*/ (void*)0);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkBeadProbeMap::CloseMorkNode(
+ morkEnv* ev) // CloseBeadProbeMap() if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseBeadProbeMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkBeadProbeMap::~morkBeadProbeMap() // assert CloseBeadProbeMap() earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkBeadProbeMap::morkBeadProbeMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkProbeMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkBead*), /*inValSize*/ 0, ioSlotHeap,
+ /*startSlotCount*/ 11,
+ /*inZeroIsClearKey*/ morkBool_kTrue) {
+ if (ev->Good()) mNode_Derived = morkDerived_kBeadProbeMap;
+}
+
+/*public non-poly*/ void morkBeadProbeMap::CloseBeadProbeMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CutAllBeads(ev);
+ this->CloseProbeMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*virtual*/ mork_test // hit(a,b) implies hash(a) == hash(b)
+morkBeadProbeMap::MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const {
+ MORK_USED_1(ev);
+ const morkBead* key = *(const morkBead**)inMapKey;
+ if (key) {
+ mork_bool hit = key->BeadEqual(*(const morkBead**)inAppKey);
+ return (hit) ? morkTest_kHit : morkTest_kMiss;
+ } else
+ return morkTest_kVoid;
+}
+
+/*virtual*/ mork_u4 // hit(a,b) implies hash(a) == hash(b)
+morkBeadProbeMap::MapHash(morkEnv* ev, const void* inAppKey) const {
+ const morkBead* key = *(const morkBead**)inAppKey;
+ if (key)
+ return key->BeadHash();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+
+/*virtual*/ mork_u4 morkBeadProbeMap::ProbeMapHashMapKey(
+ morkEnv* ev, const void* inMapKey) const {
+ const morkBead* key = *(const morkBead**)inMapKey;
+ if (key)
+ return key->BeadHash();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+
+mork_bool morkBeadProbeMap::AddBead(morkEnv* ev, morkBead* ioBead) {
+ if (ioBead && ev->Good()) {
+ morkBead* bead = 0; // old key in the map
+
+ mork_bool put = this->MapAtPut(ev, &ioBead, /*val*/ (void*)0,
+ /*key*/ &bead, /*val*/ (void*)0);
+
+ if (put) // replaced an existing key?
+ {
+ if (bead != ioBead) // new bead was not already in table?
+ ioBead->AddStrongRef(ev); // now there's another ref
+
+ if (bead && bead != ioBead) // need to release old node?
+ bead->CutStrongRef(ev);
+ } else
+ ioBead->AddStrongRef(ev); // now there's another ref
+ } else if (!ioBead)
+ ev->NilPointerError();
+
+ return ev->Good();
+}
+
+morkBead* morkBeadProbeMap::GetBead(morkEnv* ev, mork_color inColor) {
+ morkBead* oldBead = 0; // old key in the map
+ morkBead bead(inColor);
+ morkBead* key = &bead;
+
+ this->MapAt(ev, &key, &oldBead, /*val*/ (void*)0);
+
+ bead.CloseBead(ev);
+ return oldBead;
+}
+
+mork_num morkBeadProbeMap::CutAllBeads(morkEnv* ev)
+// CutAllBeads() releases all the referenced bead values.
+{
+ mork_num outSlots = sMap_Slots;
+
+ morkBeadProbeMapIter i(ev, this);
+ morkBead* b = i.FirstBead(ev);
+
+ while (b) {
+ b->CutStrongRef(ev);
+ b = i.NextBead(ev);
+ }
+ this->MapCutAll(ev);
+
+ return outSlots;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkBead.h b/comm/mailnews/db/mork/morkBead.h
new file mode 100644
index 0000000000..deccec0ba6
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBead.h
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKBEAD_
+#define _MORKBEAD_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKPROBEMAP_
+# include "morkProbeMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kBead /*i*/ 0x426F /* ascii 'Bo' */
+
+/*| morkBead: subclass of morkNode that adds knowledge of db suite factory
+**| and containing port to those objects that are exposed as instances of
+**| nsIMdbBead in the public interface.
+|*/
+class morkBead : public morkNode {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ mork_color mBead_Color; // ID for this bead
+
+ public: // Hash() and Equal() for bead maps are same for all subclasses:
+ mork_u4 BeadHash() const { return (mork_u4)mBead_Color; }
+ mork_bool BeadEqual(const morkBead* inBead) const {
+ return (mBead_Color == inBead->mBead_Color);
+ }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseBead() only if open
+ virtual ~morkBead(); // assert that CloseBead() executed earlier
+
+ public: // special case for stack construction for map usage:
+ explicit morkBead(mork_color inBeadColor); // stack-based bead instance
+
+ protected: // special case for morkObject:
+ morkBead(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor);
+
+ public: // morkEnv construction & destruction
+ morkBead(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor);
+ void CloseBead(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkBead(const morkBead& other);
+ morkBead& operator=(const morkBead& other);
+
+ public: // dynamic type identification
+ mork_bool IsBead() const {
+ return IsNode() && mNode_Derived == morkDerived_kBead;
+ }
+ // } ===== end morkNode methods =====
+
+ // void NewNilHandleError(morkEnv* ev); // mBead_Handle is nil
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakBead(morkBead* me, morkEnv* ev, morkBead** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongBead(morkBead* me, morkEnv* ev, morkBead** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kBeadMap /*i*/ 0x744D /* ascii 'bM' */
+
+/*| morkBeadMap: maps bead -> bead (key only using mBead_Color)
+|*/
+class morkBeadMap : public morkMap {
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseBeadMap() only if open
+ virtual ~morkBeadMap(); // assert that CloseBeadMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkBeadMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseBeadMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsBeadMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kBeadMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkMap poly interface =====
+ public:
+ virtual mork_bool // *((mork_u4*) inKeyA) == *((mork_u4*) inKeyB)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const override;
+
+ virtual mork_u4 // some integer function of *((mork_u4*) inKey)
+ Hash(morkEnv* ev, const void* inKey) const override;
+ // } ===== end morkMap poly interface =====
+
+ public: // other map methods
+ mork_bool AddBead(morkEnv* ev, morkBead* ioBead);
+ // the AddBead() boolean return equals ev->Good().
+
+ mork_bool CutBead(morkEnv* ev, mork_color inColor);
+ // The CutBead() boolean return indicates whether removal happened.
+
+ morkBead* GetBead(morkEnv* ev, mork_color inColor);
+ // Note the returned bead does NOT have an increase in refcount for this.
+
+ mork_num CutAllBeads(morkEnv* ev);
+ // CutAllBeads() releases all the referenced beads.
+};
+
+class morkBeadMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkBeadMapIter(morkEnv* ev, morkBeadMap* ioMap) : morkMapIter(ev, ioMap) {}
+
+ morkBeadMapIter() : morkMapIter() {}
+ void InitBeadMapIter(morkEnv* ev, morkBeadMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ morkBead* FirstBead(morkEnv* ev);
+ morkBead* NextBead(morkEnv* ev);
+ morkBead* HereBead(morkEnv* ev);
+ void CutHereBead(morkEnv* ev);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kBeadProbeMap /*i*/ 0x6D74 /* ascii 'mb' */
+
+/*| morkBeadProbeMap: maps bead -> bead (key only using mBead_Color)
+|*/
+class morkBeadProbeMap : public morkProbeMap {
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseBeadProbeMap() only if open
+ virtual ~morkBeadProbeMap(); // assert that CloseBeadProbeMap() executed
+ // earlier
+
+ public: // morkMap construction & destruction
+ morkBeadProbeMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseBeadProbeMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsBeadProbeMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kBeadProbeMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkProbeMap methods =====
+ public:
+ virtual mork_test // hit(a,b) implies hash(a) == hash(b)
+ MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const override;
+
+ virtual mork_u4 // hit(a,b) implies hash(a) == hash(b)
+ MapHash(morkEnv* ev, const void* inAppKey) const override;
+
+ virtual mork_u4 ProbeMapHashMapKey(morkEnv* ev,
+ const void* inMapKey) const override;
+
+ // virtual mork_bool ProbeMapIsKeyNil(morkEnv* ev, void* ioMapKey);
+
+ // virtual void ProbeMapClearKey(morkEnv* ev, // put 'nil' into all keys
+ // inside map
+ // void* ioMapKey, mork_count inKeyCount); // array of keys inside map
+
+ // virtual void ProbeMapPushIn(morkEnv* ev, // move (key,val) into the map
+ // const void* inAppKey, const void* inAppVal, // (key,val) outside map
+ // void* outMapKey, void* outMapVal); // (key,val) inside map
+
+ // virtual void ProbeMapPullOut(morkEnv* ev, // move (key,val) out from the
+ // map
+ // const void* inMapKey, const void* inMapVal, // (key,val) inside map
+ // void* outAppKey, void* outAppVal) const; // (key,val) outside map
+ // } ===== end morkProbeMap methods =====
+
+ public: // other map methods
+ mork_bool AddBead(morkEnv* ev, morkBead* ioBead);
+ // the AddBead() boolean return equals ev->Good().
+
+ morkBead* GetBead(morkEnv* ev, mork_color inColor);
+ // Note the returned bead does NOT have an increase in refcount for this.
+
+ mork_num CutAllBeads(morkEnv* ev);
+ // CutAllBeads() releases all the referenced bead values.
+};
+
+class morkBeadProbeMapIter
+ : public morkProbeMapIter { // typesafe wrapper class
+
+ public:
+ morkBeadProbeMapIter(morkEnv* ev, morkBeadProbeMap* ioMap)
+ : morkProbeMapIter(ev, ioMap) {}
+
+ morkBeadProbeMapIter() : morkProbeMapIter() {}
+ void InitBeadProbeMapIter(morkEnv* ev, morkBeadProbeMap* ioMap) {
+ this->InitProbeMapIter(ev, ioMap);
+ }
+
+ morkBead* FirstBead(morkEnv* ev) { return (morkBead*)this->IterFirstKey(ev); }
+
+ morkBead* NextBead(morkEnv* ev) { return (morkBead*)this->IterNextKey(ev); }
+
+ morkBead* HereBead(morkEnv* ev) { return (morkBead*)this->IterHereKey(ev); }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKBEAD_ */
diff --git a/comm/mailnews/db/mork/morkBlob.cpp b/comm/mailnews/db/mork/morkBlob.cpp
new file mode 100644
index 0000000000..d0fdf104ff
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBlob.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*static*/ void morkBuf::NilBufBodyError(morkEnv* ev) {
+ ev->NewError("nil mBuf_Body");
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*static*/ void morkBlob::BlobFillOverSizeError(morkEnv* ev) {
+ ev->NewError("mBuf_Fill > mBlob_Size");
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+mork_bool morkBlob::GrowBlob(morkEnv* ev, nsIMdbHeap* ioHeap,
+ mork_size inNewSize) {
+ if (ioHeap) {
+ if (!mBuf_Body) // no body? implies zero sized?
+ mBlob_Size = 0;
+
+ if (mBuf_Fill > mBlob_Size) // fill more than size?
+ {
+ ev->NewWarning("mBuf_Fill > mBlob_Size");
+ mBuf_Fill = mBlob_Size;
+ }
+
+ if (inNewSize > mBlob_Size) // need to allocate larger blob?
+ {
+ mork_u1* body = 0;
+ ioHeap->Alloc(ev->AsMdbEnv(), inNewSize, (void**)&body);
+ if (body && ev->Good()) {
+ void* oldBody = mBuf_Body;
+ if (mBlob_Size) // any old content to transfer?
+ MORK_MEMCPY(body, oldBody, mBlob_Size);
+
+ mBlob_Size = inNewSize; // install new size
+ mBuf_Body = body; // install new body
+
+ if (oldBody) // need to free old buffer body?
+ ioHeap->Free(ev->AsMdbEnv(), oldBody);
+ }
+ }
+ } else
+ ev->NilPointerError();
+
+ if (ev->Good() && mBlob_Size < inNewSize)
+ ev->NewError("mBlob_Size < inNewSize");
+
+ return ev->Good();
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkCoil::morkCoil(morkEnv* ev, nsIMdbHeap* ioHeap) {
+ mBuf_Body = 0;
+ mBuf_Fill = 0;
+ mBlob_Size = 0;
+ mText_Form = 0;
+ mCoil_Heap = ioHeap;
+ if (!ioHeap) ev->NilPointerError();
+}
+
+void morkCoil::CloseCoil(morkEnv* ev) {
+ void* body = mBuf_Body;
+ nsIMdbHeap* heap = mCoil_Heap;
+
+ mBuf_Body = 0;
+ mCoil_Heap = 0;
+
+ if (body && heap) {
+ heap->Free(ev->AsMdbEnv(), body);
+ }
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkBlob.h b/comm/mailnews/db/mork/morkBlob.h
new file mode 100644
index 0000000000..8ce923d232
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBlob.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKBLOB_
+#define _MORKBLOB_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| Buf: the minimum needed to describe location and content length.
+**| This is typically only enough to read from this buffer, since
+**| one cannot write effectively without knowing the size of a buf.
+|*/
+class morkBuf { // subset of nsIMdbYarn slots
+ public:
+ void* mBuf_Body; // space for holding any binary content
+ mork_fill mBuf_Fill; // logical content in Buf in bytes
+
+ public:
+ morkBuf() {}
+ morkBuf(const void* ioBuf, mork_fill inFill)
+ : mBuf_Body((void*)ioBuf), mBuf_Fill(inFill) {}
+
+ void ClearBufFill() { mBuf_Fill = 0; }
+
+ static void NilBufBodyError(morkEnv* ev);
+
+ private: // copying is not allowed
+ morkBuf(const morkBuf& other);
+ morkBuf& operator=(const morkBuf& other);
+};
+
+/*| Blob: a buffer with an associated size, to increase known buf info
+**| to include max capacity in addition to buf location and content.
+**| This form factor allows us to allocate a vector of such blobs,
+**| which can share the same managing heap stored elsewhere, and that
+**| is why we don't include a pointer to a heap in this blob class.
+|*/
+class morkBlob : public morkBuf { // greater subset of nsIMdbYarn slots
+
+ // void* mBuf_Body; // space for holding any binary content
+ // mdb_fill mBuf_Fill; // logical content in Buf in bytes
+ public:
+ mork_size mBlob_Size; // physical size of Buf in bytes
+
+ public:
+ morkBlob() {}
+ morkBlob(const void* ioBuf, mork_fill inFill, mork_size inSize)
+ : morkBuf(ioBuf, inFill), mBlob_Size(inSize) {}
+
+ static void BlobFillOverSizeError(morkEnv* ev);
+
+ public:
+ mork_bool GrowBlob(morkEnv* ev, nsIMdbHeap* ioHeap, mork_size inNewSize);
+
+ private: // copying is not allowed
+ morkBlob(const morkBlob& other);
+ morkBlob& operator=(const morkBlob& other);
+};
+
+/*| Text: a blob with an associated charset annotation, where the
+**| charset actually includes the general notion of typing, and not
+**| just a specification of character set alone; we want to permit
+**| arbitrary charset annotations for ad hoc binary types as well.
+**| (We avoid including a nsIMdbHeap pointer in morkText for the same
+**| reason morkBlob does: we want minimal size vectors of morkText.)
+|*/
+class morkText : public morkBlob { // greater subset of nsIMdbYarn slots
+
+ // void* mBuf_Body; // space for holding any binary content
+ // mdb_fill mBuf_Fill; // logical content in Buf in bytes
+ // mdb_size mBlob_Size; // physical size of Buf in bytes
+
+ public:
+ mork_cscode mText_Form; // charset format encoding
+
+ morkText() {}
+
+ private: // copying is not allowed
+ morkText(const morkText& other);
+ morkText& operator=(const morkText& other);
+};
+
+/*| Coil: a text with an associated nsIMdbHeap instance that provides
+**| all memory management for the space pointed to by mBuf_Body. (This
+**| was the hardest type to give a name in this small class hierarchy,
+**| because it's hard to characterize self-management of one's space.)
+**| A coil is a self-contained blob that knows how to grow itself as
+**| necessary to hold more content when necessary. Coil descends from
+**| morkText to include the mText_Form slot, even though this won't be
+**| needed always, because we are not as concerned about the overall
+**| size of this particular Coil object (if we were concerned about
+**| the size of an array of Coil instances, we would not bother with
+**| a separate heap pointer for each of them).
+**|
+**|| A coil makes a good medium in which to stream content as a sink,
+**| so we will have a subclass of morkSink called morkCoil that
+**| will stream bytes into this self-contained coil object. The name
+**| of this morkCoil class derives more from this intended usage than
+**| from anything else. The Mork code to parse db content will use
+**| coils with associated sinks to accumulate parsed strings.
+**|
+**|| Heap: this is the heap used for memory allocation. This instance
+**| is NOT refcounted, since this coil always assumes the heap is held
+**| through a reference elsewhere (for example, through the same object
+**| that contains or holds the coil itself. This lack of refcounting
+**| is consistent with the fact that morkCoil itself is not refcounted,
+**| and is not intended for use as a standalone object.
+|*/
+class morkCoil : public morkText { // self-managing text blob object
+
+ // void* mBuf_Body; // space for holding any binary content
+ // mdb_fill mBuf_Fill; // logical content in Buf in bytes
+ // mdb_size mBlob_Size; // physical size of Buf in bytes
+ // mdb_cscode mText_Form; // charset format encoding
+ public:
+ nsIMdbHeap* mCoil_Heap; // storage manager for mBuf_Body pointer
+
+ public:
+ morkCoil(morkEnv* ev, nsIMdbHeap* ioHeap);
+
+ void CloseCoil(morkEnv* ev);
+
+ mork_bool GrowCoil(morkEnv* ev, mork_size inNewSize) {
+ return this->GrowBlob(ev, mCoil_Heap, inNewSize);
+ }
+
+ private: // copying is not allowed
+ morkCoil(const morkCoil& other);
+ morkCoil& operator=(const morkCoil& other);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKBLOB_ */
diff --git a/comm/mailnews/db/mork/morkBuilder.cpp b/comm/mailnews/db/mork/morkBuilder.cpp
new file mode 100644
index 0000000000..4e96209929
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBuilder.cpp
@@ -0,0 +1,892 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKPARSER_
+# include "morkParser.h"
+#endif
+
+#ifndef _MORKBUILDER_
+# include "morkBuilder.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkBuilder::CloseMorkNode(
+ morkEnv* ev) // CloseBuilder() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseBuilder(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkBuilder::~morkBuilder() // assert CloseBuilder() executed earlier
+{
+ MORK_ASSERT(mBuilder_Store == 0);
+ MORK_ASSERT(mBuilder_Row == 0);
+ MORK_ASSERT(mBuilder_Table == 0);
+ MORK_ASSERT(mBuilder_Cell == 0);
+ MORK_ASSERT(mBuilder_RowSpace == 0);
+ MORK_ASSERT(mBuilder_AtomSpace == 0);
+}
+
+/*public non-poly*/
+morkBuilder::morkBuilder(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkStream* ioStream,
+ mdb_count inBytesPerParseSegment,
+ nsIMdbHeap* ioSlotHeap, morkStore* ioStore)
+
+ : morkParser(ev, inUsage, ioHeap, ioStream, inBytesPerParseSegment,
+ ioSlotHeap)
+
+ ,
+ mBuilder_Store(0)
+
+ ,
+ mBuilder_Table(0),
+ mBuilder_Row(0),
+ mBuilder_Cell(0)
+
+ ,
+ mBuilder_RowSpace(0),
+ mBuilder_AtomSpace(0)
+
+ ,
+ mBuilder_OidAtomSpace(0),
+ mBuilder_ScopeAtomSpace(0)
+
+ ,
+ mBuilder_PortForm(0),
+ mBuilder_PortRowScope((mork_scope)'r'),
+ mBuilder_PortAtomScope((mork_scope)'v')
+
+ ,
+ mBuilder_TableForm(0),
+ mBuilder_TableRowScope((mork_scope)'r'),
+ mBuilder_TableAtomScope((mork_scope)'v'),
+ mBuilder_TableKind(0)
+
+ ,
+ mBuilder_TablePriority(morkPriority_kLo),
+ mBuilder_TableIsUnique(morkBool_kFalse),
+ mBuilder_TableIsVerbose(morkBool_kFalse),
+ mBuilder_TablePadByte(0)
+
+ ,
+ mBuilder_RowForm(0),
+ mBuilder_RowRowScope((mork_scope)'r'),
+ mBuilder_RowAtomScope((mork_scope)'v')
+
+ ,
+ mBuilder_CellForm(0),
+ mBuilder_CellAtomScope((mork_scope)'v')
+
+ ,
+ mBuilder_DictForm(0),
+ mBuilder_DictAtomScope((mork_scope)'v')
+
+ ,
+ mBuilder_MetaTokenSlot(0)
+
+ ,
+ mBuilder_DoCutRow(morkBool_kFalse),
+ mBuilder_DoCutCell(morkBool_kFalse),
+ mBuilder_CellsVecFill(0) {
+ if (ev->Good()) {
+ if (ioStore) {
+ morkStore::SlotWeakStore(ioStore, ev, &mBuilder_Store);
+ if (ev->Good()) mNode_Derived = morkDerived_kBuilder;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkBuilder::CloseBuilder(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mBuilder_Row = 0;
+ mBuilder_Cell = 0;
+ mBuilder_MetaTokenSlot = 0;
+
+ morkTable::SlotStrongTable((morkTable*)0, ev, &mBuilder_Table);
+ morkStore::SlotWeakStore((morkStore*)0, ev, &mBuilder_Store);
+
+ morkRowSpace::SlotStrongRowSpace((morkRowSpace*)0, ev, &mBuilder_RowSpace);
+
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mBuilder_AtomSpace);
+
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mBuilder_OidAtomSpace);
+
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mBuilder_ScopeAtomSpace);
+ this->CloseParser(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkBuilder::NonBuilderTypeError(morkEnv* ev) {
+ ev->NewError("non morkBuilder");
+}
+
+/*static*/ void morkBuilder::NilBuilderCellError(morkEnv* ev) {
+ ev->NewError("nil mBuilder_Cell");
+}
+
+/*static*/ void morkBuilder::NilBuilderRowError(morkEnv* ev) {
+ ev->NewError("nil mBuilder_Row");
+}
+
+/*static*/ void morkBuilder::NilBuilderTableError(morkEnv* ev) {
+ ev->NewError("nil mBuilder_Table");
+}
+
+/*static*/ void morkBuilder::NonColumnSpaceScopeError(morkEnv* ev) {
+ ev->NewError("column space != 'c'");
+}
+
+void morkBuilder::LogGlitch(morkEnv* ev, const morkGlitch& inGlitch,
+ const char* inKind) {
+ MORK_USED_2(inGlitch, inKind);
+ ev->NewWarning("parsing glitch");
+}
+
+/*virtual*/ void morkBuilder::MidToYarn(
+ morkEnv* ev,
+ const morkMid& inMid, // typically an alias to concat with strings
+ mdbYarn* outYarn)
+// The parser might ask that some aliases be turned into yarns, so they
+// can be concatenated into longer blobs under some circumstances. This
+// is an alternative to using a long and complex callback for many parts
+// for a single cell value.
+{
+ mBuilder_Store->MidToYarn(ev, inMid, outYarn);
+}
+
+/*virtual*/ void morkBuilder::OnNewPort(morkEnv* ev, const morkPlace& inPlace)
+// mp:Start ::= OnNewPort mp:PortItem* OnPortEnd
+// mp:PortItem ::= mp:Content | mp:Group | OnPortGlitch
+// mp:Content ::= mp:PortRow | mp:Dict | mp:Table | mp:Row
+{
+ MORK_USED_2(ev, inPlace);
+ // mParser_InPort = morkBool_kTrue;
+ mBuilder_PortForm = 0;
+ mBuilder_PortRowScope = (mork_scope)'r';
+ mBuilder_PortAtomScope = (mork_scope)'v';
+}
+
+/*virtual*/ void morkBuilder::OnPortGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "port");
+}
+
+/*virtual*/ void morkBuilder::OnPortEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Start ::= OnNewPort mp:PortItem* OnPortEnd
+{
+ MORK_USED_2(ev, inSpan);
+ // ev->StubMethodOnlyError();
+ // nothing to do?
+ // mParser_InPort = morkBool_kFalse;
+}
+
+/*virtual*/ void morkBuilder::OnNewGroup(morkEnv* ev, const morkPlace& inPlace,
+ mork_gid inGid) {
+ MORK_USED_1(inPlace);
+ mParser_InGroup = morkBool_kTrue;
+ mork_pos startPos = inPlace.mPlace_Pos;
+
+ morkStore* store = mBuilder_Store;
+ if (store) {
+ if (inGid >= store->mStore_CommitGroupIdentity)
+ store->mStore_CommitGroupIdentity = inGid + 1;
+
+ if (!store->mStore_FirstCommitGroupPos)
+ store->mStore_FirstCommitGroupPos = startPos;
+ else if (!store->mStore_SecondCommitGroupPos)
+ store->mStore_SecondCommitGroupPos = startPos;
+ }
+}
+
+/*virtual*/ void morkBuilder::OnGroupGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "group");
+}
+
+/*virtual*/ void morkBuilder::OnGroupCommitEnd(morkEnv* ev,
+ const morkSpan& inSpan) {
+ MORK_USED_2(ev, inSpan);
+ // mParser_InGroup = morkBool_kFalse;
+ // ev->StubMethodOnlyError();
+}
+
+/*virtual*/ void morkBuilder::OnGroupAbortEnd(morkEnv* ev,
+ const morkSpan& inSpan) {
+ MORK_USED_1(inSpan);
+ // mParser_InGroup = morkBool_kFalse;
+ ev->StubMethodOnlyError();
+}
+
+/*virtual*/ void morkBuilder::OnNewPortRow(morkEnv* ev,
+ const morkPlace& inPlace,
+ const morkMid& inMid,
+ mork_change inChange) {
+ MORK_USED_3(inMid, inPlace, inChange);
+ // mParser_InPortRow = morkBool_kTrue;
+ ev->StubMethodOnlyError();
+}
+
+/*virtual*/ void morkBuilder::OnPortRowGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "port row");
+}
+
+/*virtual*/ void morkBuilder::OnPortRowEnd(morkEnv* ev,
+ const morkSpan& inSpan) {
+ MORK_USED_1(inSpan);
+ // mParser_InPortRow = morkBool_kFalse;
+ ev->StubMethodOnlyError();
+}
+
+/*virtual*/ void morkBuilder::OnNewTable(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid,
+ mork_bool inCutAllRows)
+// mp:Table ::= OnNewTable mp:TableItem* OnTableEnd
+// mp:TableItem ::= mp:Row | mp:MetaTable | OnTableGlitch
+// mp:MetaTable ::= OnNewMeta mp:MetaItem* mp:Row OnMetaEnd
+// mp:Meta ::= OnNewMeta mp:MetaItem* OnMetaEnd
+// mp:MetaItem ::= mp:Cell | OnMetaGlitch
+{
+ MORK_USED_1(inPlace);
+ // mParser_InTable = morkBool_kTrue;
+ mBuilder_TableForm = mBuilder_PortForm;
+ mBuilder_TableRowScope = mBuilder_PortRowScope;
+ mBuilder_TableAtomScope = mBuilder_PortAtomScope;
+ mBuilder_TableKind = morkStore_kNoneToken;
+
+ mBuilder_TablePriority = morkPriority_kLo;
+ mBuilder_TableIsUnique = morkBool_kFalse;
+ mBuilder_TableIsVerbose = morkBool_kFalse;
+
+ morkTable* table = mBuilder_Store->MidToTable(ev, inMid);
+ morkTable::SlotStrongTable(table, ev, &mBuilder_Table);
+ if (table) {
+ if (table->mTable_RowSpace)
+ mBuilder_TableRowScope = table->mTable_RowSpace->SpaceScope();
+
+ if (inCutAllRows) table->CutAllRows(ev);
+ }
+}
+
+/*virtual*/ void morkBuilder::OnTableGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "table");
+}
+
+/*virtual*/ void morkBuilder::OnTableEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Table ::= OnNewTable mp:TableItem* OnTableEnd
+{
+ MORK_USED_1(inSpan);
+ // mParser_InTable = morkBool_kFalse;
+ if (mBuilder_Table) {
+ mBuilder_Table->mTable_Priority = mBuilder_TablePriority;
+
+ if (mBuilder_TableIsUnique) mBuilder_Table->SetTableUnique();
+
+ if (mBuilder_TableIsVerbose) mBuilder_Table->SetTableVerbose();
+
+ morkTable::SlotStrongTable((morkTable*)0, ev, &mBuilder_Table);
+ } else
+ this->NilBuilderTableError(ev);
+
+ mBuilder_Row = 0;
+ mBuilder_Cell = 0;
+
+ mBuilder_TablePriority = morkPriority_kLo;
+ mBuilder_TableIsUnique = morkBool_kFalse;
+ mBuilder_TableIsVerbose = morkBool_kFalse;
+
+ if (mBuilder_TableKind == morkStore_kNoneToken)
+ ev->NewError("missing table kind");
+
+ mBuilder_CellAtomScope = mBuilder_RowAtomScope = mBuilder_TableAtomScope =
+ mBuilder_PortAtomScope;
+
+ mBuilder_DoCutCell = morkBool_kFalse;
+ mBuilder_DoCutRow = morkBool_kFalse;
+}
+
+/*virtual*/ void morkBuilder::OnNewMeta(morkEnv* ev, const morkPlace& inPlace)
+// mp:Meta ::= OnNewMeta mp:MetaItem* OnMetaEnd
+// mp:MetaItem ::= mp:Cell | OnMetaGlitch
+// mp:Cell ::= OnMinusCell? OnNewCell mp:CellItem? OnCellEnd
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_2(ev, inPlace);
+ // mParser_InMeta = morkBool_kTrue;
+}
+
+/*virtual*/ void morkBuilder::OnMetaGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "meta");
+}
+
+/*virtual*/ void morkBuilder::OnMetaEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Meta ::= OnNewMeta mp:MetaItem* OnMetaEnd
+{
+ MORK_USED_2(ev, inSpan);
+ // mParser_InMeta = morkBool_kFalse;
+}
+
+/*virtual*/ void morkBuilder::OnMinusRow(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mBuilder_DoCutRow = morkBool_kTrue;
+}
+
+/*virtual*/ void morkBuilder::OnNewRow(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid,
+ mork_bool inCutAllCols)
+// mp:Table ::= OnNewTable mp:TableItem* OnTableEnd
+// mp:TableItem ::= mp:Row | mp:MetaTable | OnTableGlitch
+// mp:MetaTable ::= OnNewMeta mp:MetaItem* mp:Row OnMetaEnd
+// mp:Row ::= OnMinusRow? OnNewRow mp:RowItem* OnRowEnd
+// mp:RowItem ::= mp:Cell | mp:Meta | OnRowGlitch
+// mp:Cell ::= OnMinusCell? OnNewCell mp:CellItem? OnCellEnd
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inPlace);
+ // mParser_InRow = morkBool_kTrue;
+
+ mBuilder_CellForm = mBuilder_RowForm = mBuilder_TableForm;
+ mBuilder_CellAtomScope = mBuilder_RowAtomScope = mBuilder_TableAtomScope;
+ mBuilder_RowRowScope = mBuilder_TableRowScope;
+ morkStore* store = mBuilder_Store;
+
+ if (!inMid.mMid_Buf && !inMid.mMid_Oid.mOid_Scope) {
+ morkMid mid(inMid);
+ mid.mMid_Oid.mOid_Scope = mBuilder_RowRowScope;
+ mBuilder_Row = store->MidToRow(ev, mid);
+ } else {
+ mBuilder_Row = store->MidToRow(ev, inMid);
+ }
+ morkRow* row = mBuilder_Row;
+ if (row && inCutAllCols) {
+ row->CutAllColumns(ev);
+ }
+
+ morkTable* table = mBuilder_Table;
+ if (table) {
+ if (row) {
+ if (mParser_InMeta) {
+ morkRow* metaRow = table->mTable_MetaRow;
+ if (!metaRow) {
+ table->mTable_MetaRow = row;
+ table->mTable_MetaRowOid = row->mRow_Oid;
+ row->AddRowGcUse(ev);
+ } else if (metaRow != row) // not identical?
+ ev->NewError("duplicate table meta row");
+ } else {
+ if (mBuilder_DoCutRow)
+ table->CutRow(ev, row);
+ else
+ table->AddRow(ev, row);
+ }
+ }
+ }
+ // else // it is now okay to have rows outside a table:
+ // this->NilBuilderTableError(ev);
+
+ mBuilder_DoCutRow = morkBool_kFalse;
+}
+
+/*virtual*/ void morkBuilder::OnRowPos(morkEnv* ev, mork_pos inRowPos) {
+ if (mBuilder_Row && mBuilder_Table && !mParser_InMeta) {
+ mork_pos hintFromPos = 0; // best hint when we don't know position
+ mBuilder_Table->MoveRow(ev, mBuilder_Row, hintFromPos, inRowPos);
+ }
+}
+
+/*virtual*/ void morkBuilder::OnRowGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "row");
+}
+
+void morkBuilder::FlushBuilderCells(morkEnv* ev) {
+ if (mBuilder_Row) {
+ morkPool* pool = mBuilder_Store->StorePool();
+ morkCell* cells = mBuilder_CellsVec;
+ mork_fill fill = mBuilder_CellsVecFill;
+ mBuilder_Row->TakeCells(ev, cells, fill, mBuilder_Store);
+
+ morkCell* end = cells + fill;
+ --cells; // prepare for preincrement
+ while (++cells < end) {
+ if (cells->mCell_Atom) cells->SetAtom(ev, (morkAtom*)0, pool);
+ }
+ mBuilder_CellsVecFill = 0;
+ } else
+ this->NilBuilderRowError(ev);
+}
+
+/*virtual*/ void morkBuilder::OnRowEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Row ::= OnMinusRow? OnNewRow mp:RowItem* OnRowEnd
+{
+ MORK_USED_1(inSpan);
+ // mParser_InRow = morkBool_kFalse;
+ if (mBuilder_Row) {
+ this->FlushBuilderCells(ev);
+ } else
+ this->NilBuilderRowError(ev);
+
+ mBuilder_Row = 0;
+ mBuilder_Cell = 0;
+
+ mBuilder_DoCutCell = morkBool_kFalse;
+ mBuilder_DoCutRow = morkBool_kFalse;
+}
+
+/*virtual*/ void morkBuilder::OnNewDict(morkEnv* ev, const morkPlace& inPlace)
+// mp:Dict ::= OnNewDict mp:DictItem* OnDictEnd
+// mp:DictItem ::= OnAlias | OnAliasGlitch | mp:Meta | OnDictGlitch
+{
+ MORK_USED_2(ev, inPlace);
+ // mParser_InDict = morkBool_kTrue;
+
+ mBuilder_CellForm = mBuilder_DictForm = mBuilder_PortForm;
+ mBuilder_CellAtomScope = mBuilder_DictAtomScope = mBuilder_PortAtomScope;
+}
+
+/*virtual*/ void morkBuilder::OnDictGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "dict");
+}
+
+/*virtual*/ void morkBuilder::OnDictEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Dict ::= OnNewDict mp:DictItem* OnDictEnd
+{
+ MORK_USED_2(ev, inSpan);
+ // mParser_InDict = morkBool_kFalse;
+
+ mBuilder_DictForm = 0;
+ mBuilder_DictAtomScope = 0;
+}
+
+/*virtual*/ void morkBuilder::OnAlias(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) {
+ MORK_USED_1(inSpan);
+ if (mParser_InDict) {
+ morkMid mid = inMid; // local copy for modification
+ mid.mMid_Oid.mOid_Scope = mBuilder_DictAtomScope;
+ mBuilder_Store->AddAlias(ev, mid, mBuilder_DictForm);
+ } else
+ ev->NewError("alias not in dict");
+}
+
+/*virtual*/ void morkBuilder::OnAliasGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "alias");
+}
+
+morkCell* morkBuilder::AddBuilderCell(morkEnv* ev, const morkMid& inMid,
+ mork_change inChange) {
+ morkCell* outCell = 0;
+ mork_column column = inMid.mMid_Oid.mOid_Id;
+
+ if (ev->Good()) {
+ if (mBuilder_CellsVecFill >= morkBuilder_kCellsVecSize)
+ this->FlushBuilderCells(ev);
+ if (ev->Good()) {
+ if (mBuilder_CellsVecFill < morkBuilder_kCellsVecSize) {
+ mork_fill indx = mBuilder_CellsVecFill++;
+ outCell = mBuilder_CellsVec + indx;
+ outCell->SetColumnAndChange(column, inChange);
+ outCell->mCell_Atom = 0;
+ } else
+ ev->NewError("out of builder cells");
+ }
+ }
+ return outCell;
+}
+
+/*virtual*/ void morkBuilder::OnMinusCell(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mBuilder_DoCutCell = morkBool_kTrue;
+}
+
+/*virtual*/ void morkBuilder::OnNewCell(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid* inMid,
+ const morkBuf* inBuf)
+// Exactly one of inMid and inBuf is nil, and the other is non-nil.
+// When hex ID syntax is used for a column, then inMid is not nil, and
+// when a naked string names a column, then inBuf is not nil.
+
+// mp:Cell ::= OnMinusCell? OnNewCell mp:CellItem? OnCellEnd
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inPlace);
+ // mParser_InCell = morkBool_kTrue;
+
+ mork_change cellChange =
+ (mBuilder_DoCutCell) ? morkChange_kCut : morkChange_kAdd;
+
+ mBuilder_DoCutCell = morkBool_kFalse;
+
+ mBuilder_CellAtomScope = mBuilder_RowAtomScope;
+
+ mBuilder_Cell = 0; // nil until determined for a row
+ morkStore* store = mBuilder_Store;
+ mork_scope scope = morkStore_kColumnSpaceScope;
+ morkMid tempMid; // space for local and modifiable cell mid
+ morkMid* cellMid = &tempMid; // default to local if inMid==0
+
+ if (inMid) // mid parameter is actually provided?
+ {
+ *cellMid = *inMid; // bitwise copy for modifiable local mid
+
+ if (!cellMid->mMid_Oid.mOid_Scope) {
+ if (cellMid->mMid_Buf) {
+ scope = store->BufToToken(ev, cellMid->mMid_Buf);
+ cellMid->mMid_Buf = 0; // don't do scope lookup again
+ ev->NewWarning("column mids need column scope");
+ }
+ cellMid->mMid_Oid.mOid_Scope = scope;
+ }
+ } else if (inBuf) // buf points to naked column string name?
+ {
+ cellMid->ClearMid();
+ cellMid->mMid_Oid.mOid_Id = store->BufToToken(ev, inBuf);
+ cellMid->mMid_Oid.mOid_Scope = scope; // kColumnSpaceScope
+ } else
+ ev->NilPointerError(); // either inMid or inBuf must be non-nil
+
+ mork_column column = cellMid->mMid_Oid.mOid_Id;
+
+ if (mBuilder_Row && ev->Good()) // this cell must be inside a row
+ {
+ // mBuilder_Cell = this->AddBuilderCell(ev, *cellMid, cellChange);
+
+ if (mBuilder_CellsVecFill >= morkBuilder_kCellsVecSize)
+ this->FlushBuilderCells(ev);
+ if (ev->Good()) {
+ if (mBuilder_CellsVecFill < morkBuilder_kCellsVecSize) {
+ mork_fill ix = mBuilder_CellsVecFill++;
+ morkCell* cell = mBuilder_CellsVec + ix;
+ cell->SetColumnAndChange(column, cellChange);
+
+ cell->mCell_Atom = 0;
+ mBuilder_Cell = cell;
+ } else
+ ev->NewError("out of builder cells");
+ }
+ }
+
+ else if (mParser_InMeta && ev->Good()) // cell is in metainfo structure?
+ {
+ if (scope == morkStore_kColumnSpaceScope) {
+ if (mParser_InTable) // metainfo for table?
+ {
+ if (column == morkStore_kKindColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_TableKind;
+ else if (column == morkStore_kStatusColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_TableStatus;
+ else if (column == morkStore_kRowScopeColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_TableRowScope;
+ else if (column == morkStore_kAtomScopeColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_TableAtomScope;
+ else if (column == morkStore_kFormColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_TableForm;
+ } else if (mParser_InDict) // metainfo for dict?
+ {
+ if (column == morkStore_kAtomScopeColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_DictAtomScope;
+ else if (column == morkStore_kFormColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_DictForm;
+ } else if (mParser_InRow) // metainfo for row?
+ {
+ if (column == morkStore_kAtomScopeColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_RowAtomScope;
+ else if (column == morkStore_kRowScopeColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_RowRowScope;
+ else if (column == morkStore_kFormColumn)
+ mBuilder_MetaTokenSlot = &mBuilder_RowForm;
+ }
+ } else
+ ev->NewWarning("expected column scope");
+ }
+}
+
+/*virtual*/ void morkBuilder::OnCellGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) {
+ this->LogGlitch(ev, inGlitch, "cell");
+}
+
+/*virtual*/ void morkBuilder::OnCellForm(morkEnv* ev,
+ mork_cscode inCharsetFormat) {
+ morkCell* cell = mBuilder_Cell;
+ if (cell) {
+ mBuilder_CellForm = inCharsetFormat;
+ } else
+ this->NilBuilderCellError(ev);
+}
+
+/*virtual*/ void morkBuilder::OnCellEnd(morkEnv* ev, const morkSpan& inSpan)
+// mp:Cell ::= OnMinusCell? OnNewCell mp:CellItem? OnCellEnd
+{
+ MORK_USED_2(ev, inSpan);
+ // mParser_InCell = morkBool_kFalse;
+
+ mBuilder_MetaTokenSlot = 0;
+ mBuilder_CellAtomScope = mBuilder_RowAtomScope;
+}
+
+/*virtual*/ void morkBuilder::OnValue(morkEnv* ev, const morkSpan& inSpan,
+ const morkBuf& inBuf)
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inSpan);
+ morkStore* store = mBuilder_Store;
+ morkCell* cell = mBuilder_Cell;
+ if (cell) {
+ mdbYarn yarn;
+ yarn.mYarn_Buf = inBuf.mBuf_Body;
+ yarn.mYarn_Fill = yarn.mYarn_Size = inBuf.mBuf_Fill;
+ yarn.mYarn_More = 0;
+ yarn.mYarn_Form = mBuilder_CellForm;
+ yarn.mYarn_Grow = 0;
+ morkAtom* atom = store->YarnToAtom(ev, &yarn, true /* create */);
+ cell->SetAtom(ev, atom, store->StorePool());
+ } else if (mParser_InMeta) {
+ mork_token* metaSlot = mBuilder_MetaTokenSlot;
+ if (metaSlot) {
+ if (metaSlot == &mBuilder_TableStatus) // table status?
+ {
+ if (mParser_InTable && mBuilder_Table) {
+ const char* body = (const char*)inBuf.mBuf_Body;
+ mork_fill bufFill = inBuf.mBuf_Fill;
+ if (body && bufFill) {
+ const char* bodyEnd = body + bufFill;
+ while (body < bodyEnd) {
+ int c = *body++;
+ switch (c) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ mBuilder_TablePriority = (mork_priority)(c - '0');
+ break;
+
+ case 'u':
+ case 'U':
+ mBuilder_TableIsUnique = morkBool_kTrue;
+ break;
+
+ case 'v':
+ case 'V':
+ mBuilder_TableIsVerbose = morkBool_kTrue;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ mork_token token = store->BufToToken(ev, &inBuf);
+ if (token) {
+ *metaSlot = token;
+ if (metaSlot == &mBuilder_TableKind) // table kind?
+ {
+ if (mParser_InTable && mBuilder_Table)
+ mBuilder_Table->mTable_Kind = token;
+ }
+ }
+ }
+ }
+ } else
+ this->NilBuilderCellError(ev);
+}
+
+/*virtual*/ void morkBuilder::OnValueMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid)
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inSpan);
+ morkStore* store = mBuilder_Store;
+ morkCell* cell = mBuilder_Cell;
+
+ morkMid valMid; // local mid for modifications
+ mdbOid* valOid = &valMid.mMid_Oid; // ref to oid inside mid
+ *valOid = inMid.mMid_Oid; // bitwise copy inMid's oid
+
+ if (inMid.mMid_Buf) {
+ if (!valOid->mOid_Scope) store->MidToOid(ev, inMid, valOid);
+ } else if (!valOid->mOid_Scope)
+ valOid->mOid_Scope = mBuilder_CellAtomScope;
+
+ if (cell) {
+ morkBookAtom* atom = store->MidToAtom(ev, valMid);
+ if (atom)
+ cell->SetAtom(ev, atom, store->StorePool());
+ else
+ ev->NewError("undefined cell value alias");
+ } else if (mParser_InMeta) {
+ mork_token* metaSlot = mBuilder_MetaTokenSlot;
+ if (metaSlot) {
+ mork_scope valScope = valOid->mOid_Scope;
+ if (!valScope || valScope == morkStore_kColumnSpaceScope) {
+ if (ev->Good() && valMid.HasSomeId()) {
+ *metaSlot = valOid->mOid_Id;
+ if (metaSlot == &mBuilder_TableKind) // table kind?
+ {
+ if (mParser_InTable && mBuilder_Table) {
+ mBuilder_Table->mTable_Kind = valOid->mOid_Id;
+ } else
+ ev->NewWarning("mBuilder_TableKind not in table");
+ } else if (metaSlot == &mBuilder_TableStatus) // table status?
+ {
+ if (mParser_InTable && mBuilder_Table) {
+ // $$ what here??
+ } else
+ ev->NewWarning("mBuilder_TableStatus not in table");
+ }
+ }
+ } else
+ this->NonColumnSpaceScopeError(ev);
+ }
+ } else
+ this->NilBuilderCellError(ev);
+}
+
+/*virtual*/ void morkBuilder::OnRowMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid)
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inSpan);
+ morkStore* store = mBuilder_Store;
+ morkCell* cell = mBuilder_Cell;
+ if (cell) {
+ mdbOid rowOid = inMid.mMid_Oid;
+ if (inMid.mMid_Buf) {
+ if (!rowOid.mOid_Scope) store->MidToOid(ev, inMid, &rowOid);
+ } else if (!rowOid.mOid_Scope)
+ rowOid.mOid_Scope = mBuilder_RowRowScope;
+
+ if (ev->Good()) {
+ morkPool* pool = store->StorePool();
+ morkAtom* atom = pool->NewRowOidAtom(ev, rowOid, &store->mStore_Zone);
+ if (atom) {
+ cell->SetAtom(ev, atom, pool);
+ morkRow* row = store->OidToRow(ev, &rowOid);
+ if (row) // found or created such a row?
+ row->AddRowGcUse(ev);
+ }
+ }
+ } else
+ this->NilBuilderCellError(ev);
+}
+
+/*virtual*/ void morkBuilder::OnTableMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid)
+// mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+// mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+{
+ MORK_USED_1(inSpan);
+ morkStore* store = mBuilder_Store;
+ morkCell* cell = mBuilder_Cell;
+ if (cell) {
+ mdbOid tableOid = inMid.mMid_Oid;
+ if (inMid.mMid_Buf) {
+ if (!tableOid.mOid_Scope) store->MidToOid(ev, inMid, &tableOid);
+ } else if (!tableOid.mOid_Scope)
+ tableOid.mOid_Scope = mBuilder_RowRowScope;
+
+ if (ev->Good()) {
+ morkPool* pool = store->StorePool();
+ morkAtom* atom = pool->NewTableOidAtom(ev, tableOid, &store->mStore_Zone);
+ if (atom) {
+ cell->SetAtom(ev, atom, pool);
+ morkTable* table = store->OidToTable(ev, &tableOid,
+ /*optionalMetaRowOid*/ (mdbOid*)0);
+ if (table) // found or created such a table?
+ table->AddTableGcUse(ev);
+ }
+ }
+ } else
+ this->NilBuilderCellError(ev);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkBuilder.h b/comm/mailnews/db/mork/morkBuilder.h
new file mode 100644
index 0000000000..2c8b2e573f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkBuilder.h
@@ -0,0 +1,303 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKBUILDER_
+#define _MORKBUILDER_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKPARSER_
+# include "morkParser.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| kCellsVecSize: length of cell vector buffer inside morkBuilder
+|*/
+#define morkBuilder_kCellsVecSize 64
+
+#define morkBuilder_kDefaultBytesPerParseSegment 512 /* plausible to big */
+
+#define morkDerived_kBuilder /*i*/ 0x4275 /* ascii 'Bu' */
+
+class morkBuilder /*d*/ : public morkParser {
+ // public: // slots inherited from morkParser (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // nsIMdbHeap* mParser_Heap; // refcounted heap used for allocation
+ // morkStream* mParser_Stream; // refcounted input stream
+
+ // mork_u4 mParser_Tag; // must equal morkParser_kTag
+ // mork_count mParser_MoreGranularity; // constructor
+ // inBytesPerParseSegment
+
+ // mork_u4 mParser_State; // state where parser should resume
+
+ // after finding ends of group transactions, we can re-seek the start:
+ // mork_pos mParser_GroupContentStartPos; // start of this group
+
+ // mdbOid mParser_TableOid; // table oid if inside a table
+ // mdbOid mParser_RowOid; // row oid if inside a row
+ // mork_gid mParser_GroupId; // group ID if inside a group
+
+ // mork_bool mParser_InPort; // called OnNewPort but not OnPortEnd?
+ // mork_bool mParser_InDict; // called OnNewDict but not OnDictEnd?
+ // mork_bool mParser_InCell; // called OnNewCell but not OnCellEnd?
+ // mork_bool mParser_InMeta; // called OnNewMeta but not OnMetaEnd?
+
+ // morkMid mParser_Mid; // current alias being parsed
+ // note that mParser_Mid.mMid_Buf points at mParser_ScopeCoil below:
+
+ // blob coils allocated in mParser_Heap
+ // morkCoil mParser_ScopeCoil; // place to accumulate ID scope blobs
+ // morkCoil mParser_ValueCoil; // place to accumulate value blobs
+ // morkCoil mParser_ColumnCoil; // place to accumulate column blobs
+ // morkCoil mParser_StringCoil; // place to accumulate string blobs
+
+ // morkSpool mParser_ScopeSpool; // writes to mParser_ScopeCoil
+ // morkSpool mParser_ValueSpool; // writes to mParser_ValueCoil
+ // morkSpool mParser_ColumnSpool; // writes to mParser_ColumnCoil
+ // morkSpool mParser_StringSpool; // writes to mParser_StringCoil
+
+ // yarns allocated in mParser_Heap
+ // morkYarn mParser_MidYarn; // place to receive from MidToYarn()
+
+ // span showing current ongoing file position status:
+ // morkSpan mParser_PortSpan; // span of current db port file
+
+ // various spans denoting nested subspaces inside the file's port span:
+ // morkSpan mParser_GroupSpan; // span of current transaction group
+ // morkSpan mParser_DictSpan;
+ // morkSpan mParser_AliasSpan;
+ // morkSpan mParser_MetaDictSpan;
+ // morkSpan mParser_TableSpan;
+ // morkSpan mParser_MetaTableSpan;
+ // morkSpan mParser_RowSpan;
+ // morkSpan mParser_MetaRowSpan;
+ // morkSpan mParser_CellSpan;
+ // morkSpan mParser_ColumnSpan;
+ // morkSpan mParser_SlotSpan;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected morkBuilder members
+ // weak refs that do not prevent closure of referenced nodes:
+ morkStore* mBuilder_Store; // weak ref to builder's store
+
+ // strong refs that do indeed prevent closure of referenced nodes:
+ morkTable* mBuilder_Table; // current table being built (or nil)
+ morkRow* mBuilder_Row; // current row being built (or nil)
+ morkCell* mBuilder_Cell; // current cell within CellsVec (or nil)
+
+ morkRowSpace* mBuilder_RowSpace; // space for mBuilder_CellRowScope
+ morkAtomSpace* mBuilder_AtomSpace; // space for mBuilder_CellAtomScope
+
+ morkAtomSpace* mBuilder_OidAtomSpace; // ground atom space for oids
+ morkAtomSpace* mBuilder_ScopeAtomSpace; // ground atom space for scopes
+
+ // scoped object ids for current objects under construction:
+ mdbOid mBuilder_TableOid; // full oid for current table
+ mdbOid mBuilder_RowOid; // full oid for current row
+
+ // tokens that become set as the result of meta cells in port rows:
+ mork_cscode mBuilder_PortForm; // default port charset format
+ mork_scope mBuilder_PortRowScope; // port row scope
+ mork_scope mBuilder_PortAtomScope; // port atom scope
+
+ // tokens that become set as the result of meta cells in meta tables:
+ mork_cscode mBuilder_TableForm; // default table charset format
+ mork_scope mBuilder_TableRowScope; // table row scope
+ mork_scope mBuilder_TableAtomScope; // table atom scope
+ mork_kind mBuilder_TableKind; // table kind
+
+ mork_token mBuilder_TableStatus; // dummy: priority/unique/verbose
+
+ mork_priority mBuilder_TablePriority; // table priority
+ mork_bool mBuilder_TableIsUnique; // table uniqueness
+ mork_bool mBuilder_TableIsVerbose; // table verboseness
+ mork_u1 mBuilder_TablePadByte; // for u4 alignment
+
+ // tokens that become set as the result of meta cells in meta rows:
+ mork_cscode mBuilder_RowForm; // default row charset format
+ mork_scope mBuilder_RowRowScope; // row scope per row metainfo
+ mork_scope mBuilder_RowAtomScope; // row atom scope
+
+ // meta tokens currently in force, driven by meta info slots above:
+ mork_cscode mBuilder_CellForm; // cell charset format
+ mork_scope mBuilder_CellAtomScope; // cell atom scope
+
+ mork_cscode mBuilder_DictForm; // dict charset format
+ mork_scope mBuilder_DictAtomScope; // dict atom scope
+
+ mork_token* mBuilder_MetaTokenSlot; // pointer to some slot above
+
+ // If any of these 'cut' bools are true, it means a minus was seen in the
+ // Mork source text to indicate removal of content from some container.
+ // (Note there is no corresponding 'add' bool, since add is the default.)
+ // CutRow implies the current row should be cut from the table.
+ // CutCell implies the current column should be cut from the row.
+ mork_bool mBuilder_DoCutRow; // row with kCut change
+ mork_bool mBuilder_DoCutCell; // cell with kCut change
+ mork_u1 mBuilder_row_pad; // pad to u4 alignment
+ mork_u1 mBuilder_cell_pad; // pad to u4 alignment
+
+ morkCell mBuilder_CellsVec[morkBuilder_kCellsVecSize + 1];
+ mork_fill mBuilder_CellsVecFill; // count used in CellsVec
+ // Note when mBuilder_CellsVecFill equals morkBuilder_kCellsVecSize, and
+ // another cell is added, this means all the cells in the vector above
+ // must be flushed to the current row being built to create more room.
+
+ protected: // protected inlines
+ mork_bool CellVectorIsFull() const {
+ return (mBuilder_CellsVecFill == morkBuilder_kCellsVecSize);
+ }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseBuilder() only if open
+ virtual ~morkBuilder(); // assert that CloseBuilder() executed earlier
+
+ public: // morkYarn construction & destruction
+ morkBuilder(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkStream* ioStream, // the readonly stream for input bytes
+ mdb_count inBytesPerParseSegment, // target for ParseMore()
+ nsIMdbHeap* ioSlotHeap, morkStore* ioStore);
+
+ void CloseBuilder(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkBuilder(const morkBuilder& other);
+ morkBuilder& operator=(const morkBuilder& other);
+
+ public: // dynamic type identification
+ mork_bool IsBuilder() const {
+ return IsNode() && mNode_Derived == morkDerived_kBuilder;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // errors
+ static void NonBuilderTypeError(morkEnv* ev);
+ static void NilBuilderCellError(morkEnv* ev);
+ static void NilBuilderRowError(morkEnv* ev);
+ static void NilBuilderTableError(morkEnv* ev);
+ static void NonColumnSpaceScopeError(morkEnv* ev);
+
+ void LogGlitch(morkEnv* ev, const morkGlitch& inGlitch, const char* inKind);
+
+ public: // other builder methods
+ morkCell* AddBuilderCell(morkEnv* ev, const morkMid& inMid,
+ mork_change inChange);
+
+ void FlushBuilderCells(morkEnv* ev);
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // in virtual morkParser methods, data flow subclass to parser
+ virtual void MidToYarn(
+ morkEnv* ev,
+ const morkMid& inMid, // typically an alias to concat with strings
+ mdbYarn* outYarn) override;
+ // The parser might ask that some aliases be turned into yarns, so they
+ // can be concatenated into longer blobs under some circumstances. This
+ // is an alternative to using a long and complex callback for many parts
+ // for a single cell value.
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // out virtual morkParser methods, data flow parser to subclass
+ virtual void OnNewPort(morkEnv* ev, const morkPlace& inPlace) override;
+ virtual void OnPortGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnPortEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnNewGroup(morkEnv* ev, const morkPlace& inPlace,
+ mork_gid inGid) override;
+ virtual void OnGroupGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnGroupCommitEnd(morkEnv* ev, const morkSpan& inSpan) override;
+ virtual void OnGroupAbortEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnNewPortRow(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid,
+ mork_change inChange) override;
+ virtual void OnPortRowGlitch(morkEnv* ev,
+ const morkGlitch& inGlitch) override;
+ virtual void OnPortRowEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnNewTable(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid,
+ mork_bool inCutAllRows) override;
+ virtual void OnTableGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnTableEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnNewMeta(morkEnv* ev, const morkPlace& inPlace) override;
+ virtual void OnMetaGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnMetaEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnMinusRow(morkEnv* ev) override;
+ virtual void OnNewRow(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid, mork_bool inCutAllCols) override;
+ virtual void OnRowPos(morkEnv* ev, mork_pos inRowPos) override;
+ virtual void OnRowGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnRowEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnNewDict(morkEnv* ev, const morkPlace& inPlace) override;
+ virtual void OnDictGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnDictEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnAlias(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) override;
+
+ virtual void OnAliasGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+
+ virtual void OnMinusCell(morkEnv* ev) override;
+ virtual void OnNewCell(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid* inMid, const morkBuf* inBuf) override;
+ // Exactly one of inMid and inBuf is nil, and the other is non-nil.
+ // When hex ID syntax is used for a column, then inMid is not nil, and
+ // when a naked string names a column, then inBuf is not nil.
+
+ virtual void OnCellGlitch(morkEnv* ev, const morkGlitch& inGlitch) override;
+ virtual void OnCellForm(morkEnv* ev, mork_cscode inCharsetFormat) override;
+ virtual void OnCellEnd(morkEnv* ev, const morkSpan& inSpan) override;
+
+ virtual void OnValue(morkEnv* ev, const morkSpan& inSpan,
+ const morkBuf& inBuf) override;
+
+ virtual void OnValueMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) override;
+
+ virtual void OnRowMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) override;
+
+ virtual void OnTableMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) override;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkBuilder methods
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakBuilder(morkBuilder* me, morkEnv* ev,
+ morkBuilder** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongBuilder(morkBuilder* me, morkEnv* ev,
+ morkBuilder** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKBUILDER_ */
diff --git a/comm/mailnews/db/mork/morkCell.cpp b/comm/mailnews/db/mork/morkCell.cpp
new file mode 100644
index 0000000000..5e0ca9128c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCell.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+void morkCell::SetYarn(morkEnv* ev, const mdbYarn* inYarn, morkStore* ioStore) {
+ morkAtom* atom = ioStore->YarnToAtom(ev, inYarn, true /* create */);
+ if (atom) this->SetAtom(ev, atom, ioStore->StorePool()); // refcounts atom
+}
+
+void morkCell::GetYarn(morkEnv* ev, mdbYarn* outYarn) const {
+ MORK_USED_1(ev);
+ morkAtom::GetYarn(mCell_Atom, outYarn);
+}
+
+void morkCell::AliasYarn(morkEnv* ev, mdbYarn* outYarn) const {
+ MORK_USED_1(ev);
+ morkAtom::AliasYarn(mCell_Atom, outYarn);
+}
+
+void morkCell::SetCellClean() {
+ mork_column col = this->GetColumn();
+ this->SetColumnAndChange(col, morkChange_kNil);
+}
+
+void morkCell::SetCellDirty() {
+ mork_column col = this->GetColumn();
+ this->SetColumnAndChange(col, morkChange_kAdd);
+}
+
+void morkCell::SetAtom(morkEnv* ev, morkAtom* ioAtom, morkPool* ioPool)
+// SetAtom() "acquires" the new ioAtom if non-nil, by calling AddCellUse()
+// to increase the refcount, and puts ioAtom into mCell_Atom. If the old
+// atom in mCell_Atom is non-nil, then it is "released" first by a call to
+// CutCellUse(), and if the use count then becomes zero, then the old atom
+// is deallocated by returning it to the pool ioPool. (And this is
+// why ioPool is a parameter to this method.) Note that ioAtom can be nil
+// to cause the cell to refer to nothing, and the old atom in mCell_Atom
+// can also be nil, and all the atom refcounting is handled correctly.
+//
+// Note that if ioAtom was just created, it typically has a zero use count
+// before calling SetAtom(). But use count is one higher after SetAtom().
+{
+ morkAtom* oldAtom = mCell_Atom;
+ if (oldAtom != ioAtom) // ioAtom is not already installed in this cell?
+ {
+ if (oldAtom) {
+ mCell_Atom = 0;
+ if (oldAtom->CutCellUse(ev) == 0) {
+ // this was zapping atoms still in use - comment out until davidmc
+ // can figure out a better fix.
+ // if ( ioPool )
+ // {
+ // if ( oldAtom->IsBook() )
+ // ((morkBookAtom*) oldAtom)->CutBookAtomFromSpace(ev);
+
+ // ioPool->ZapAtom(ev, oldAtom);
+ // }
+ // else
+ // ev->NilPointerError();
+ }
+ }
+ if (ioAtom) ioAtom->AddCellUse(ev);
+
+ mCell_Atom = ioAtom;
+ }
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkCell.h b/comm/mailnews/db/mork/morkCell.h
new file mode 100644
index 0000000000..5b5194ccc4
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCell.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKCELL_
+#define _MORKCELL_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDelta_kShift 8 /* 8 bit shift */
+#define morkDelta_kChangeMask 0x0FF /* low 8 bit mask */
+#define morkDelta_kColumnMask (~(mork_column)morkDelta_kChangeMask)
+#define morkDelta_Init(self, cl, ch) \
+ ((self) = ((cl) << morkDelta_kShift) | (ch))
+#define morkDelta_Change(self) ((mork_change)((self)&morkDelta_kChangeMask))
+#define morkDelta_Column(self) ((self) >> morkDelta_kShift)
+
+class morkCell { // minimal cell format
+
+ public:
+ mork_delta mCell_Delta; // encoding of both column and change
+ morkAtom* mCell_Atom; // content in this cell
+
+ public:
+ morkCell() : mCell_Delta(0), mCell_Atom(0) {}
+
+ morkCell(const morkCell& c)
+ : mCell_Delta(c.mCell_Delta), mCell_Atom(c.mCell_Atom) {}
+
+ // note if ioAtom is non-nil, caller needs to call ioAtom->AddCellUse():
+ morkCell(mork_column inCol, mork_change inChange, morkAtom* ioAtom) {
+ morkDelta_Init(mCell_Delta, inCol, inChange);
+ mCell_Atom = ioAtom;
+ }
+
+ // note if ioAtom is non-nil, caller needs to call ioAtom->AddCellUse():
+ void Init(mork_column inCol, mork_change inChange, morkAtom* ioAtom) {
+ morkDelta_Init(mCell_Delta, inCol, inChange);
+ mCell_Atom = ioAtom;
+ }
+
+ mork_column GetColumn() const { return morkDelta_Column(mCell_Delta); }
+ mork_change GetChange() const { return morkDelta_Change(mCell_Delta); }
+
+ mork_bool IsCellClean() const { return GetChange() == morkChange_kNil; }
+ mork_bool IsCellDirty() const { return GetChange() != morkChange_kNil; }
+
+ void SetCellClean(); // set change to kNil
+ void SetCellDirty(); // set change to kAdd
+
+ void SetCellColumnDirty(mork_column inCol) {
+ this->SetColumnAndChange(inCol, morkChange_kAdd);
+ }
+
+ void SetCellColumnClean(mork_column inCol) {
+ this->SetColumnAndChange(inCol, morkChange_kNil);
+ }
+
+ void SetColumnAndChange(mork_column inCol, mork_change inChange) {
+ morkDelta_Init(mCell_Delta, inCol, inChange);
+ }
+
+ morkAtom* GetAtom() { return mCell_Atom; }
+
+ void SetAtom(morkEnv* ev, morkAtom* ioAtom, morkPool* ioPool);
+ // SetAtom() "acquires" the new ioAtom if non-nil, by calling AddCellUse()
+ // to increase the refcount, and puts ioAtom into mCell_Atom. If the old
+ // atom in mCell_Atom is non-nil, then it is "released" first by a call to
+ // CutCellUse(), and if the use count then becomes zero, then the old atom
+ // is deallocated by returning it to the pool ioPool. (And this is
+ // why ioPool is a parameter to this method.) Note that ioAtom can be nil
+ // to cause the cell to refer to nothing, and the old atom in mCell_Atom
+ // can also be nil, and all the atom refcounting is handled correctly.
+ //
+ // Note that if ioAtom was just created, it typically has a zero use count
+ // before calling SetAtom(). But use count is one higher after SetAtom().
+
+ void SetYarn(morkEnv* ev, const mdbYarn* inYarn, morkStore* ioStore);
+
+ void AliasYarn(morkEnv* ev, mdbYarn* outYarn) const;
+ void GetYarn(morkEnv* ev, mdbYarn* outYarn) const;
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKCELL_ */
diff --git a/comm/mailnews/db/mork/morkCellObject.cpp b/comm/mailnews/db/mork/morkCellObject.cpp
new file mode 100644
index 0000000000..7ad8402348
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCellObject.cpp
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCELLOBJECT_
+# include "morkCellObject.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkCellObject::CloseMorkNode(
+ morkEnv* ev) // CloseCellObject() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseCellObject(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkCellObject::~morkCellObject() // assert CloseCellObject() executed earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(mCellObject_Row == 0);
+}
+
+/*public non-poly*/
+morkCellObject::morkCellObject(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkRow* ioRow,
+ morkCell* ioCell, mork_column inCol,
+ mork_pos inPos)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mCellObject_RowObject(0),
+ mCellObject_Row(0),
+ mCellObject_Cell(0),
+ mCellObject_Col(inCol),
+ mCellObject_RowSeed(0),
+ mCellObject_Pos((mork_u2)inPos) {
+ if (ev->Good()) {
+ if (ioRow && ioCell) {
+ if (ioRow->IsRow()) {
+ morkStore* store = ioRow->GetRowSpaceStore(ev);
+ if (store) {
+ morkRowObject* rowObj = ioRow->AcquireRowObject(ev, store);
+ if (rowObj) {
+ mCellObject_Row = ioRow;
+ mCellObject_Cell = ioCell;
+ mCellObject_RowSeed = ioRow->mRow_Seed;
+
+ // morkRowObject::SlotStrongRowObject(rowObj, ev,
+ // &mCellObject_RowObject);
+
+ mCellObject_RowObject = rowObj; // assume control of strong ref
+ }
+ if (ev->Good()) mNode_Derived = morkDerived_kCellObject;
+ }
+ } else
+ ioRow->NonRowTypeError(ev);
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkCellObject, morkObject, nsIMdbCell)
+
+/*public non-poly*/ void morkCellObject::CloseCellObject(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ NS_RELEASE(mCellObject_RowObject);
+ mCellObject_Row = 0;
+ mCellObject_Cell = 0;
+ mCellObject_RowSeed = 0;
+ this->CloseObject(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+mork_bool morkCellObject::ResyncWithRow(morkEnv* ev) {
+ morkRow* row = mCellObject_Row;
+ mork_pos pos = 0;
+ morkCell* cell = row->GetCell(ev, mCellObject_Col, &pos);
+ if (cell) {
+ mCellObject_Pos = (mork_u2)pos;
+ mCellObject_Cell = cell;
+ mCellObject_RowSeed = row->mRow_Seed;
+ } else {
+ mCellObject_Cell = 0;
+ this->MissingRowColumnError(ev);
+ }
+ return ev->Good();
+}
+
+morkAtom* morkCellObject::GetCellAtom(morkEnv* ev) const {
+ morkCell* cell = mCellObject_Cell;
+ if (cell)
+ return cell->GetAtom();
+ else
+ this->NilCellError(ev);
+
+ return (morkAtom*)0;
+}
+
+/*static*/ void morkCellObject::WrongRowObjectRowError(morkEnv* ev) {
+ ev->NewError("mCellObject_Row != mCellObject_RowObject->mRowObject_Row");
+}
+
+/*static*/ void morkCellObject::NilRowError(morkEnv* ev) {
+ ev->NewError("nil mCellObject_Row");
+}
+
+/*static*/ void morkCellObject::NilRowObjectError(morkEnv* ev) {
+ ev->NewError("nil mCellObject_RowObject");
+}
+
+/*static*/ void morkCellObject::NilCellError(morkEnv* ev) {
+ ev->NewError("nil mCellObject_Cell");
+}
+
+/*static*/ void morkCellObject::NonCellObjectTypeError(morkEnv* ev) {
+ ev->NewError("non morkCellObject");
+}
+
+/*static*/ void morkCellObject::MissingRowColumnError(morkEnv* ev) {
+ ev->NewError("mCellObject_Col not in mCellObject_Row");
+}
+
+nsIMdbCell* morkCellObject::AcquireCellHandle(morkEnv* ev) {
+ nsIMdbCell* outCell = this;
+ NS_ADDREF(outCell);
+ return outCell;
+}
+
+morkEnv* morkCellObject::CanUseCell(nsIMdbEnv* mev, mork_bool inMutable,
+ nsresult* outErr, morkCell** outCell) {
+ morkEnv* outEnv = 0;
+ morkCell* cell = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (IsCellObject()) {
+ if (IsMutable() || !inMutable) {
+ morkRowObject* rowObj = mCellObject_RowObject;
+ if (rowObj) {
+ morkRow* row = mCellObject_Row;
+ if (row) {
+ if (rowObj->mRowObject_Row == row) {
+ mork_u2 oldSeed = mCellObject_RowSeed;
+ if (row->mRow_Seed == oldSeed || ResyncWithRow(ev)) {
+ cell = mCellObject_Cell;
+ if (cell) {
+ outEnv = ev;
+ } else
+ NilCellError(ev);
+ }
+ } else
+ WrongRowObjectRowError(ev);
+ } else
+ NilRowError(ev);
+ } else
+ NilRowObjectError(ev);
+ } else
+ NonMutableNodeError(ev);
+ } else
+ NonCellObjectTypeError(ev);
+ }
+ *outErr = ev->AsErr();
+ MORK_ASSERT(outEnv);
+ *outCell = cell;
+
+ return outEnv;
+}
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP morkCellObject::SetBlob(nsIMdbEnv* /* mev */,
+ nsIMdbBlob* /* ioBlob */) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+} // reads inBlob slots
+
+// when inBlob is in the same suite, this might be fastest cell-to-cell
+
+NS_IMETHODIMP
+morkCellObject::ClearBlob( // make empty (so content has zero length)
+ nsIMdbEnv* /* mev */) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // remember row->MaybeDirtySpaceStoreAndRow();
+}
+// clearing a yarn is like SetYarn() with empty yarn instance content
+
+NS_IMETHODIMP morkCellObject::GetBlobFill(nsIMdbEnv* mev, mdb_fill* outFill)
+// Same value that would be put into mYarn_Fill, if one called GetYarn()
+// with a yarn instance that had mYarn_Buf==nil and mYarn_Size==0.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+} // size of blob
+
+NS_IMETHODIMP morkCellObject::SetYarn(nsIMdbEnv* mev, const mdbYarn* inYarn) {
+ nsresult outErr = NS_OK;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ morkRow* row = mCellObject_Row;
+ if (row) {
+ morkStore* store = row->GetRowSpaceStore(ev);
+ if (store) {
+ cell->SetYarn(ev, inYarn, store);
+ if (row->IsRowClean() && store->mStore_CanDirty)
+ row->MaybeDirtySpaceStoreAndRow();
+ }
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+} // reads from yarn slots
+// make this text object contain content from the yarn's buffer
+
+NS_IMETHODIMP morkCellObject::GetYarn(nsIMdbEnv* mev, mdbYarn* outYarn) {
+ nsresult outErr = NS_OK;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ morkAtom* atom = cell->GetAtom();
+ morkAtom::GetYarn(atom, outYarn);
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+} // writes some yarn slots
+// copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+
+NS_IMETHODIMP morkCellObject::AliasYarn(nsIMdbEnv* mev, mdbYarn* outYarn) {
+ nsresult outErr = NS_OK;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ morkAtom* atom = cell->GetAtom();
+ morkAtom::AliasYarn(atom, outYarn);
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+} // writes ALL yarn slots
+
+// } ----- end attribute methods -----
+
+// } ===== end nsIMdbBlob methods =====
+
+// { ===== begin nsIMdbCell methods =====
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP morkCellObject::SetColumn(nsIMdbEnv* mev, mdb_column inColumn) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // remember row->MaybeDirtySpaceStoreAndRow();
+}
+
+NS_IMETHODIMP morkCellObject::GetColumn(nsIMdbEnv* mev, mdb_column* outColumn) {
+ nsresult outErr = NS_OK;
+ mdb_column col = 0;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ col = mCellObject_Col;
+ outErr = ev->AsErr();
+ }
+ if (outColumn) *outColumn = col;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkCellObject::GetCellInfo( // all cell metainfo except actual content
+ nsIMdbEnv* mev,
+ mdb_column* outColumn, // the column in the containing row
+ mdb_fill* outBlobFill, // the size of text content in bytes
+ mdbOid* outChildOid, // oid of possible row or table child
+ mdb_bool* outIsRowChild) // nonzero if child, and a row child
+// Checking all cell metainfo is a good way to avoid forcing a large cell
+// in to memory when you don't actually want to use the content.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP morkCellObject::GetRow(
+ nsIMdbEnv* mev, // parent row for this cell
+ nsIMdbRow** acqRow) {
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ outRow = mCellObject_RowObject->AcquireRowHandle(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP morkCellObject::GetPort(nsIMdbEnv* mev, // port containing cell
+ nsIMdbPort** acqPort) {
+ nsresult outErr = NS_OK;
+ nsIMdbPort* outPort = 0;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ if (mCellObject_Row) {
+ morkStore* store = mCellObject_Row->GetRowSpaceStore(ev);
+ if (store) outPort = store->AcquireStoreHandle(ev);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqPort) *acqPort = outPort;
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin children methods -----
+NS_IMETHODIMP
+morkCellObject::HasAnyChild( // does cell have a child instead of text?
+ nsIMdbEnv* mev,
+ mdbOid* outOid, // out id of row or table (or unbound if no child)
+ mdb_bool* outIsRow) // nonzero if child is a row (rather than a table)
+{
+ nsresult outErr = NS_OK;
+ mdb_bool isRow = morkBool_kFalse;
+ outOid->mOid_Scope = 0;
+ outOid->mOid_Id = morkId_kMinusOne;
+ morkCell* cell = 0;
+ morkEnv* ev =
+ this->CanUseCell(mev, /*inMutable*/ morkBool_kTrue, &outErr, &cell);
+ if (ev) {
+ morkAtom* atom = GetCellAtom(ev);
+ if (atom) {
+ isRow = atom->IsRowOid();
+ if (isRow || atom->IsTableOid())
+ *outOid = ((morkOidAtom*)atom)->mOidAtom_Oid;
+ }
+
+ outErr = ev->AsErr();
+ }
+ if (outIsRow) *outIsRow = isRow;
+
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkCellObject::GetAnyChild( // access table of specific attribute
+ nsIMdbEnv* mev, // context
+ nsIMdbRow** acqRow, // child row (or null)
+ nsIMdbTable** acqTable) // child table (or null)
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCellObject::SetChildRow( // access table of specific attribute
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+} // inRow must be bound inside this same db port
+
+NS_IMETHODIMP morkCellObject::GetChildRow( // access row of specific attribute
+ nsIMdbEnv* mev, // context
+ nsIMdbRow** acqRow) // acquire child row (or nil if no child)
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCellObject::SetChildTable( // access table of specific attribute
+ nsIMdbEnv* mev, // context
+ nsIMdbTable* inTable) // table must be bound inside this same db port
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // remember row->MaybeDirtySpaceStoreAndRow();
+}
+
+NS_IMETHODIMP
+morkCellObject::GetChildTable( // access table of specific attribute
+ nsIMdbEnv* mev, // context
+ nsIMdbTable** acqTable) // acquire child tabdle (or nil if no chil)
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end children methods -----
+
+// } ===== end nsIMdbCell methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkCellObject.h b/comm/mailnews/db/mork/morkCellObject.h
new file mode 100644
index 0000000000..1ef8718ab2
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCellObject.h
@@ -0,0 +1,180 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKCELLOBJECT_
+#define _MORKCELLOBJECT_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kCellObject /*i*/ 0x634F /* ascii 'cO' */
+
+class morkCellObject : public morkObject,
+ public nsIMdbCell { // blob attribute in column scope
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+
+ morkRowObject* mCellObject_RowObject; // strong ref to row's object
+ morkRow* mCellObject_Row; // cell's row if still in row object
+ morkCell* mCellObject_Cell; // cell in row if rowseed matches
+ mork_column mCellObject_Col; // col of cell last living in pos
+ mork_u2 mCellObject_RowSeed; // copy of row's seed
+ mork_u2 mCellObject_Pos; // position of cell in row
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseCellObject() only if open
+
+ public: // morkCellObject construction & destruction
+ morkCellObject(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkRow* ioRow, morkCell* ioCell, mork_column inCol,
+ mork_pos inPos);
+ void CloseCellObject(morkEnv* ev); // called by CloseMorkNode();
+
+ NS_IMETHOD SetBlob(nsIMdbEnv* ev,
+ nsIMdbBlob* ioBlob) override; // reads inBlob slots
+ // when inBlob is in the same suite, this might be fastest cell-to-cell
+
+ NS_IMETHOD ClearBlob( // make empty (so content has zero length)
+ nsIMdbEnv* ev) override;
+ // clearing a yarn is like SetYarn() with empty yarn instance content
+
+ NS_IMETHOD GetBlobFill(nsIMdbEnv* ev,
+ mdb_fill* outFill) override; // size of blob
+ // Same value that would be put into mYarn_Fill, if one called GetYarn()
+ // with a yarn instance that had mYarn_Buf==nil and mYarn_Size==0.
+
+ NS_IMETHOD SetYarn(nsIMdbEnv* ev,
+ const mdbYarn* inYarn) override; // reads from yarn slots
+ // make this text object contain content from the yarn's buffer
+
+ NS_IMETHOD GetYarn(nsIMdbEnv* ev,
+ mdbYarn* outYarn) override; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+
+ NS_IMETHOD AliasYarn(nsIMdbEnv* ev,
+ mdbYarn* outYarn) override; // writes ALL yarn slots
+ NS_IMETHOD SetColumn(nsIMdbEnv* ev, mdb_column inColumn) override;
+ NS_IMETHOD GetColumn(nsIMdbEnv* ev, mdb_column* outColumn) override;
+
+ NS_IMETHOD GetCellInfo( // all cell metainfo except actual content
+ nsIMdbEnv* ev,
+ mdb_column* outColumn, // the column in the containing row
+ mdb_fill* outBlobFill, // the size of text content in bytes
+ mdbOid* outChildOid, // oid of possible row or table child
+ mdb_bool* outIsRowChild) override; // nonzero if child, and a row child
+
+ // Checking all cell metainfo is a good way to avoid forcing a large cell
+ // in to memory when you don't actually want to use the content.
+
+ NS_IMETHOD GetRow(nsIMdbEnv* ev, // parent row for this cell
+ nsIMdbRow** acqRow) override;
+ NS_IMETHOD GetPort(nsIMdbEnv* ev, // port containing cell
+ nsIMdbPort** acqPort) override;
+ // } ----- end attribute methods -----
+
+ // { ----- begin children methods -----
+ NS_IMETHOD HasAnyChild( // does cell have a child instead of text?
+ nsIMdbEnv* ev,
+ mdbOid* outOid, // out id of row or table (or unbound if no child)
+ mdb_bool* outIsRow)
+ override; // nonzero if child is a row (rather than a table)
+
+ NS_IMETHOD GetAnyChild( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // child row (or null)
+ nsIMdbTable** acqTable) override; // child table (or null)
+
+ NS_IMETHOD SetChildRow( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow)
+ override; // inRow must be bound inside this same db port
+
+ NS_IMETHOD GetChildRow( // access row of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow) override; // acquire child row (or nil if no child)
+
+ NS_IMETHOD SetChildTable( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbTable* inTable)
+ override; // table must be bound inside this same db port
+
+ NS_IMETHOD GetChildTable( // access table of specific attribute
+ nsIMdbEnv* ev, // context
+ nsIMdbTable** acqTable)
+ override; // acquire child table (or nil if no child)
+
+ // } ----- end children methods -----
+
+ // } ===== end nsIMdbCell methods =====
+ private: // copying is not allowed
+ virtual ~morkCellObject(); // assert that CloseCellObject() executed earlier
+ morkCellObject(const morkCellObject& other);
+ morkCellObject& operator=(const morkCellObject& other);
+
+ public: // dynamic type identification
+ mork_bool IsCellObject() const {
+ return IsNode() && mNode_Derived == morkDerived_kCellObject;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // other cell node methods
+ morkEnv* CanUseCell(nsIMdbEnv* mev, mork_bool inMutable, nsresult* outErr,
+ morkCell** outCell);
+
+ mork_bool ResyncWithRow(morkEnv* ev); // return ev->Good()
+ morkAtom* GetCellAtom(morkEnv* ev) const;
+
+ static void MissingRowColumnError(morkEnv* ev);
+ static void NilRowError(morkEnv* ev);
+ static void NilCellError(morkEnv* ev);
+ static void NilRowObjectError(morkEnv* ev);
+ static void WrongRowObjectRowError(morkEnv* ev);
+ static void NonCellObjectTypeError(morkEnv* ev);
+
+ nsIMdbCell* AcquireCellHandle(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakCellObject(morkCellObject* me, morkEnv* ev,
+ morkCellObject** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongCellObject(morkCellObject* me, morkEnv* ev,
+ morkCellObject** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKCELLOBJECT_ */
diff --git a/comm/mailnews/db/mork/morkCh.cpp b/comm/mailnews/db/mork/morkCh.cpp
new file mode 100644
index 0000000000..334d9c689c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCh.cpp
@@ -0,0 +1,344 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCH_
+# include "morkCh.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/* this byte char predicate source file derives from public domain Mithril */
+/* (that means much of this has a copyright dedicated to the public domain) */
+
+/*============================================================================*/
+/* morkCh_Type */
+
+const mork_flags morkCh_Type[] = /* derives from public domain Mithril table */
+ {
+ 0, /* 0x0 */
+ 0, /* 0x1 */
+ 0, /* 0x2 */
+ 0, /* 0x3 */
+ 0, /* 0x4 */
+ 0, /* 0x5 */
+ 0, /* 0x6 */
+ 0, /* 0x7 */
+ morkCh_kW, /* 0x8 backspace */
+ morkCh_kW, /* 0x9 tab */
+ morkCh_kW, /* 0xA linefeed */
+ 0, /* 0xB */
+ morkCh_kW, /* 0xC page */
+ morkCh_kW, /* 0xD return */
+ 0, /* 0xE */
+ 0, /* 0xF */
+ 0, /* 0x10 */
+ 0, /* 0x11 */
+ 0, /* 0x12 */
+ 0, /* 0x13 */
+ 0, /* 0x14 */
+ 0, /* 0x15 */
+ 0, /* 0x16 */
+ 0, /* 0x17 */
+ 0, /* 0x18 */
+ 0, /* 0x19 */
+ 0, /* 0x1A */
+ 0, /* 0x1B */
+ 0, /* 0x1C */
+ 0, /* 0x1D */
+ 0, /* 0x1E */
+ 0, /* 0x1F */
+
+ morkCh_kV | morkCh_kW, /* 0x20 space */
+ morkCh_kV | morkCh_kM, /* 0x21 ! */
+ morkCh_kV, /* 0x22 " */
+ morkCh_kV, /* 0x23 # */
+ 0, /* 0x24 $ cannot be kV because needs escape */
+ morkCh_kV, /* 0x25 % */
+ morkCh_kV, /* 0x26 & */
+ morkCh_kV, /* 0x27 ' */
+ morkCh_kV, /* 0x28 ( */
+ 0, /* 0x29 ) cannot be kV because needs escape */
+ morkCh_kV, /* 0x2A * */
+ morkCh_kV | morkCh_kM, /* 0x2B + */
+ morkCh_kV, /* 0x2C , */
+ morkCh_kV | morkCh_kM, /* 0x2D - */
+ morkCh_kV, /* 0x2E . */
+ morkCh_kV, /* 0x2F / */
+
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x30 0 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x31 1 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x32 2 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x33 3 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x34 4 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x35 5 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x36 6 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x37 7 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x38 8 */
+ morkCh_kV | morkCh_kD | morkCh_kX, /* 0x39 9 */
+ morkCh_kV | morkCh_kN | morkCh_kM, /* 0x3A : */
+ morkCh_kV, /* 0x3B ; */
+ morkCh_kV, /* 0x3C < */
+ morkCh_kV, /* 0x3D = */
+ morkCh_kV, /* 0x3E > */
+ morkCh_kV | morkCh_kM, /* 0x3F ? */
+
+ morkCh_kV, /* 0x40 @ */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x41 A */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x42 B */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x43 C */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x44 D */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x45 E */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU | morkCh_kX, /* 0x46 F */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x47 G */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x48 H */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x49 I */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4A J */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4B K */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4C L */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4D M */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4E N */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x4F O */
+
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x50 P */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x51 Q */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x52 R */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x53 S */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x54 T */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x55 U */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x56 V */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x57 W */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x58 X */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x59 Y */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kU, /* 0x5A Z */
+ morkCh_kV, /* 0x5B [ */
+ 0, /* 0x5C \ cannot be kV because needs escape */
+ morkCh_kV, /* 0x5D ] */
+ morkCh_kV, /* 0x5E ^ */
+ morkCh_kV | morkCh_kN | morkCh_kM, /* 0x5F _ */
+
+ morkCh_kV, /* 0x60 ` */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x61 a */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x62 b */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x63 c */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x64 d */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x65 e */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL | morkCh_kX, /* 0x66 f */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x67 g */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x68 h */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x69 i */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6A j */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6B k */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6C l */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6D m */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6E n */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x6F o */
+
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x70 p */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x71 q */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x72 r */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x73 s */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x74 t */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x75 u */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x76 v */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x77 w */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x78 x */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x79 y */
+ morkCh_kV | morkCh_kN | morkCh_kM | morkCh_kL, /* 0x7A z */
+ morkCh_kV, /* 0x7B { */
+ morkCh_kV, /* 0x7C | */
+ morkCh_kV, /* 0x7D } */
+ morkCh_kV, /* 0x7E ~ */
+ morkCh_kW, /* 0x7F rubout */
+
+ /* $"80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* $"F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF" */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkCh.h b/comm/mailnews/db/mork/morkCh.h
new file mode 100644
index 0000000000..a3fc155a4d
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCh.h
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORKCH_
+# define _MORKCH_ 1
+
+# ifndef _MORK_
+# include "mork.h"
+# endif
+
+/* this byte char predicate header file derives from public domain Mithril */
+/* (that means much of this has a copyright dedicated to the public domain) */
+
+/* Use all 8 pred bits; lose some pred bits only if we need to reuse them. */
+
+/* ch pred bits: W:white D:digit V:value U:upper L:lower N:name M:more */
+# define morkCh_kW (1 << 0)
+# define morkCh_kD (1 << 1)
+# define morkCh_kV (1 << 2)
+# define morkCh_kU (1 << 3)
+# define morkCh_kL (1 << 4)
+# define morkCh_kX (1 << 5)
+# define morkCh_kN (1 << 6)
+# define morkCh_kM (1 << 7)
+
+extern const mork_flags morkCh_Type[]; /* 256 byte predicate bits ch map */
+
+/* is a numeric decimal digit: (note memory access might be slower) */
+/* define morkCh_IsDigit(c) ( morkCh_Type[ (mork_ch)(c) ] & morkCh_kD ) */
+# define morkCh_IsDigit(c) (((mork_ch)c) >= '0' && ((mork_ch)c) <= '9')
+
+/* is a numeric octal digit: */
+# define morkCh_IsOctal(c) (((mork_ch)c) >= '0' && ((mork_ch)c) <= '7')
+
+/* is a numeric hexadecimal digit: */
+# define morkCh_IsHex(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kX)
+
+/* is value (can be printed in Mork value without needing hex or escape): */
+# define morkCh_IsValue(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kV)
+
+/* is white space : */
+# define morkCh_IsWhite(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kW)
+
+/* is name (can start a Mork name): */
+# define morkCh_IsName(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kN)
+
+/* is name (can continue a Mork name): */
+# define morkCh_IsMore(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kM)
+
+/* is alphabetic upper or lower case */
+# define morkCh_IsAlpha(c) \
+ (morkCh_Type[(mork_ch)(c)] & (morkCh_kL | morkCh_kU))
+
+/* is alphanumeric, including lower case, upper case, and digits */
+# define morkCh_IsAlphaNum(c) \
+ (morkCh_Type[(mork_ch)(c)] & (morkCh_kL | morkCh_kU | morkCh_kD))
+
+/* ````` repeated testing of predicate bits in single flag byte ````` */
+
+# define morkCh_GetFlags(c) (morkCh_Type[(mork_ch)(c)])
+
+# define morkFlags_IsDigit(f) ((f)&morkCh_kD)
+# define morkFlags_IsHex(f) ((f)&morkCh_kX)
+# define morkFlags_IsValue(f) ((f)&morkCh_kV)
+# define morkFlags_IsWhite(f) ((f)&morkCh_kW)
+# define morkFlags_IsName(f) ((f)&morkCh_kN)
+# define morkFlags_IsMore(f) ((f)&morkCh_kM)
+# define morkFlags_IsAlpha(f) ((f) & (morkCh_kL | morkCh_kU))
+# define morkFlags_IsAlphaNum(f) ((f) & (morkCh_kL | morkCh_kU | morkCh_kD))
+
+# define morkFlags_IsUpper(f) ((f)&morkCh_kU)
+# define morkFlags_IsLower(f) ((f)&morkCh_kL)
+
+/* ````` character case (e.g. for case insensitive operations) ````` */
+
+# define morkCh_IsAscii(c) (((mork_u1)c) <= 0x7F)
+# define morkCh_IsSevenBitChar(c) (((mork_u1)c) <= 0x7F)
+
+/* ````` character case (e.g. for case insensitive operations) ````` */
+
+# define morkCh_ToLower(c) ((c) - 'A' + 'a')
+# define morkCh_ToUpper(c) ((c) - 'a' + 'A')
+
+/* extern int morkCh_IsUpper (int c); */
+# define morkCh_IsUpper(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kU)
+
+/* extern int morkCh_IsLower (int c); */
+# define morkCh_IsLower(c) (morkCh_Type[(mork_ch)(c)] & morkCh_kL)
+
+#endif
+/* _MORKCH_ */
diff --git a/comm/mailnews/db/mork/morkConfig.cpp b/comm/mailnews/db/mork/morkConfig.cpp
new file mode 100644
index 0000000000..a02cecead9
--- /dev/null
+++ b/comm/mailnews/db/mork/morkConfig.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCONFIG_
+# include "morkConfig.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+void mork_assertion_signal(const char* inMessage) { NS_ERROR(inMessage); }
+
+#ifdef MORK_PROVIDE_STDLIB
+
+MORK_LIB_IMPL(mork_i4)
+mork_memcmp(const void* inOne, const void* inTwo, mork_size inSize) {
+ const mork_u1* t = (const mork_u1*)inTwo;
+ const mork_u1* s = (const mork_u1*)inOne;
+ const mork_u1* end = s + inSize;
+ mork_i4 delta;
+
+ while (s < end) {
+ delta = ((mork_i4)*s) - ((mork_i4)*t);
+ if (delta)
+ return delta;
+ else {
+ ++t;
+ ++s;
+ }
+ }
+ return 0;
+}
+
+MORK_LIB_IMPL(void)
+mork_memcpy(void* outDst, const void* inSrc, mork_size inSize) {
+ mork_u1* d = (mork_u1*)outDst;
+ mork_u1* end = d + inSize;
+ const mork_u1* s = ((const mork_u1*)inSrc);
+
+ while (inSize >= 8) {
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+
+ inSize -= 8;
+ }
+
+ while (d < end) *d++ = *s++;
+}
+
+MORK_LIB_IMPL(void)
+mork_memmove(void* outDst, const void* inSrc, mork_size inSize) {
+ mork_u1* d = (mork_u1*)outDst;
+ const mork_u1* s = (const mork_u1*)inSrc;
+ if (d != s && inSize) // copy is necessary?
+ {
+ const mork_u1* srcEnd = s + inSize; // one past last source byte
+
+ if (d > s && d < srcEnd) // overlap? need to copy backwards?
+ {
+ s = srcEnd; // start one past last source byte
+ d += inSize; // start one past last dest byte
+ mork_u1* dstBegin = d; // last byte to write is first in dest range
+ while (d - dstBegin >= 8) {
+ *--d = *--s;
+ *--d = *--s;
+ *--d = *--s;
+ *--d = *--s;
+
+ *--d = *--s;
+ *--d = *--s;
+ *--d = *--s;
+ *--d = *--s;
+ }
+ while (d > dstBegin) *--d = *--s;
+ } else // can copy forwards without any overlap
+ {
+ mork_u1* dstEnd = d + inSize;
+ while (dstEnd - d >= 8) {
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+ *d++ = *s++;
+ }
+ while (d < dstEnd) *d++ = *s++;
+ }
+ }
+}
+
+MORK_LIB_IMPL(void)
+mork_memset(void* outDst, int inByte, mork_size inSize) {
+ mork_u1* d = (mork_u1*)outDst;
+ mork_u1* end = d + inSize;
+ while (d < end) *d++ = (mork_u1)inByte;
+}
+
+MORK_LIB_IMPL(void)
+mork_strcpy(void* outDst, const void* inSrc) {
+ // back up one first to support preincrement
+ mork_u1* d = ((mork_u1*)outDst) - 1;
+ const mork_u1* s = ((const mork_u1*)inSrc) - 1;
+ while ((*++d = *++s) != 0)
+ ; /* empty */
+}
+
+MORK_LIB_IMPL(mork_i4)
+mork_strcmp(const void* inOne, const void* inTwo) {
+ const mork_u1* t = (const mork_u1*)inTwo;
+ const mork_u1* s = ((const mork_u1*)inOne);
+ mork_i4 a;
+ mork_i4 b;
+ mork_i4 delta;
+
+ do {
+ a = (mork_i4)*s++;
+ b = (mork_i4)*t++;
+ delta = a - b;
+ } while (!delta && a && b);
+
+ return delta;
+}
+
+MORK_LIB_IMPL(mork_i4)
+mork_strncmp(const void* inOne, const void* inTwo, mork_size inSize) {
+ const mork_u1* t = (const mork_u1*)inTwo;
+ const mork_u1* s = (const mork_u1*)inOne;
+ const mork_u1* end = s + inSize;
+ mork_i4 delta;
+ mork_i4 a;
+ mork_i4 b;
+
+ while (s < end) {
+ a = (mork_i4)*s++;
+ b = (mork_i4)*t++;
+ delta = a - b;
+ if (delta || !a || !b) return delta;
+ }
+ return 0;
+}
+
+MORK_LIB_IMPL(mork_size)
+mork_strlen(const void* inString) {
+ // back up one first to support preincrement
+ const mork_u1* s = ((const mork_u1*)inString) - 1;
+ while (*++s) /* preincrement is cheapest */
+ ; /* empty */
+
+ return s - ((const mork_u1*)inString); // distance from original address
+}
+
+#endif /*MORK_PROVIDE_STDLIB*/
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkConfig.h b/comm/mailnews/db/mork/morkConfig.h
new file mode 100644
index 0000000000..812641ee09
--- /dev/null
+++ b/comm/mailnews/db/mork/morkConfig.h
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKCONFIG_
+#define _MORKCONFIG_ 1
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// { %%%%% begin debug mode options in Mork %%%%%
+#define MORK_DEBUG 1
+// } %%%%% end debug mode options in Mork %%%%%
+
+#ifdef MORK_DEBUG
+# define MORK_MAX_CODE_COMPILE 1
+#endif
+
+// { %%%%% begin platform defs peculiar to Mork %%%%%
+
+#ifdef XP_MACOSX
+# define MORK_MAC 1
+#endif
+
+#ifdef XP_WIN
+# define MORK_WIN 1
+#endif
+
+#ifdef XP_UNIX
+# define MORK_UNIX 1
+#endif
+
+// } %%%%% end platform defs peculiar to Mork %%%%%
+
+#if defined(MORK_WIN) || defined(MORK_UNIX) || defined(MORK_MAC)
+# include <stdio.h>
+# include <ctype.h>
+# include <errno.h>
+# include <string.h>
+# ifdef HAVE_MEMORY_H
+# include <memory.h>
+# endif
+# ifdef HAVE_UNISTD_H
+# include <unistd.h> /* for SEEK_SET, SEEK_END */
+# endif
+
+# include "nsDebug.h"
+
+# define MORK_ISPRINT(c) isprint(c)
+
+# define MORK_FILETELL(file) ftell(file)
+# define MORK_FILESEEK(file, where, how) fseek(file, where, how)
+# define MORK_FILEREAD(outbuf, insize, file) fread(outbuf, 1, insize, file)
+# if defined(MORK_WIN)
+void mork_fileflush(FILE* file);
+# define MORK_FILEFLUSH(file) mork_fileflush(file)
+# else
+# define MORK_FILEFLUSH(file) fflush(file)
+# endif /*MORK_WIN*/
+
+# if defined(MORK_WIN)
+# define MORK_FILEOPEN(file, how) \
+ _wfopen(char16ptr_t(file), NS_ConvertASCIItoUTF16(how).get())
+# else
+# define MORK_FILEOPEN(file, how) fopen(file, how)
+# endif /*MORK_WIN*/
+# define MORK_FILECLOSE(file) fclose(file)
+#endif /*defined(MORK_WIN) || defined(MORK_UNIX) || defined(MORK_MAC)*/
+
+/* ===== separating switchable features ===== */
+
+#define MORK_ENABLE_ZONE_ARENAS 1 /* using morkZone for pooling */
+
+// #define MORK_ENABLE_PROBE_MAPS 1 /* use smaller hash tables */
+
+#define MORK_BEAD_OVER_NODE_MAPS 1 /* use bead not node maps */
+
+/* ===== pooling ===== */
+
+#if defined(HAVE_64BIT_BUILD)
+# define MORK_CONFIG_ALIGN_8 1 /* must have 8 byte alignment */
+#else
+# define MORK_CONFIG_PTR_SIZE_4 1 /* sizeof(void*) == 4 */
+#endif
+
+// #define MORK_DEBUG_HEAP_STATS 1 /* analyze per-block heap usage */
+
+/* ===== ===== ===== ===== line characters ===== ===== ===== ===== */
+#define mork_kCR 0x0D
+#define mork_kLF 0x0A
+#define mork_kVTAB '\013'
+#define mork_kFF '\014'
+#define mork_kTAB '\011'
+#define mork_kCRLF "\015\012" /* A CR LF equivalent string */
+
+#if defined(MORK_MAC)
+# define mork_kNewline "\015"
+# define mork_kNewlineSize 1
+#else
+# if defined(MORK_WIN)
+# define mork_kNewline "\015\012"
+# define mork_kNewlineSize 2
+# else
+# if defined(MORK_UNIX)
+# define mork_kNewline "\012"
+# define mork_kNewlineSize 1
+# endif /* MORK_UNIX */
+# endif /* MORK_WIN */
+#endif /* MORK_MAC */
+
+// { %%%%% begin assertion macro %%%%%
+extern void mork_assertion_signal(const char* inMessage);
+#define MORK_ASSERTION_SIGNAL(Y) mork_assertion_signal(Y)
+#define MORK_ASSERT(X) \
+ if (!(X)) MORK_ASSERTION_SIGNAL(#X)
+// } %%%%% end assertion macro %%%%%
+
+#define MORK_LIB(return) return /*API return declaration*/
+#define MORK_LIB_IMPL(return) return /*implementation return declaration*/
+
+// { %%%%% begin standard c utility methods %%%%%
+
+#if defined(MORK_WIN) || defined(MORK_UNIX) || defined(MORK_MAC)
+# define MORK_USE_C_STDLIB 1
+#endif /*MORK_WIN*/
+
+#ifdef MORK_USE_C_STDLIB
+# define MORK_MEMCMP(src1, src2, size) memcmp(src1, src2, size)
+# define MORK_MEMCPY(dest, src, size) memcpy(dest, src, size)
+# define MORK_MEMMOVE(dest, src, size) memmove(dest, src, size)
+# define MORK_MEMSET(dest, byte, size) memset(dest, byte, size)
+# if defined(MORK_WIN)
+# define MORK_STRCPY(dest, src) wcscpy(char16ptr_t(dest), char16ptr_t(src))
+# else
+# define MORK_STRCPY(dest, src) strcpy(dest, src)
+# endif /*MORK_WIN*/
+# define MORK_STRCMP(one, two) strcmp(one, two)
+# define MORK_STRNCMP(one, two, length) strncmp(one, two, length)
+# if defined(MORK_WIN)
+# define MORK_STRLEN(string) wcslen(char16ptr_t(string))
+# else
+# define MORK_STRLEN(string) strlen(string)
+# endif /*MORK_WIN*/
+#endif /*MORK_USE_C_STDLIB*/
+
+#ifdef MORK_PROVIDE_STDLIB
+MORK_LIB(mork_i4) mork_memcmp(const void* a, const void* b, mork_size inSize);
+MORK_LIB(void) mork_memcpy(void* dst, const void* src, mork_size inSize);
+MORK_LIB(void) mork_memmove(void* dst, const void* src, mork_size inSize);
+MORK_LIB(void) mork_memset(void* dst, int inByte, mork_size inSize);
+MORK_LIB(void) mork_strcpy(void* dst, const void* src);
+MORK_LIB(mork_i4) mork_strcmp(const void* a, const void* b);
+MORK_LIB(mork_i4) mork_strncmp(const void* a, const void* b, mork_size inSize);
+MORK_LIB(mork_size) mork_strlen(const void* inString);
+
+# define MORK_MEMCMP(src1, src2, size) mork_memcmp(src1, src2, size)
+# define MORK_MEMCPY(dest, src, size) mork_memcpy(dest, src, size)
+# define MORK_MEMMOVE(dest, src, size) mork_memmove(dest, src, size)
+# define MORK_MEMSET(dest, byte, size) mork_memset(dest, byte, size)
+# define MORK_STRCPY(dest, src) mork_strcpy(dest, src)
+# define MORK_STRCMP(one, two) mork_strcmp(one, two)
+# define MORK_STRNCMP(one, two, length) mork_strncmp(one, two, length)
+# define MORK_STRLEN(string) mork_strlen(string)
+#endif /*MORK_PROVIDE_STDLIB*/
+
+// } %%%%% end standard c utility methods %%%%%
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKCONFIG_ */
diff --git a/comm/mailnews/db/mork/morkCursor.cpp b/comm/mailnews/db/mork/morkCursor.cpp
new file mode 100644
index 0000000000..407aa9b3fb
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCursor.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkCursor::CloseMorkNode(
+ morkEnv* ev) // CloseCursor() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseCursor(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkCursor::~morkCursor() // assert CloseCursor() executed earlier
+{}
+
+/*public non-poly*/
+morkCursor::morkCursor(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mCursor_Seed(0),
+ mCursor_Pos(-1),
+ mCursor_DoFailOnSeedOutOfSync(morkBool_kFalse) {
+ if (ev->Good()) mNode_Derived = morkDerived_kCursor;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkCursor, morkObject, nsIMdbCursor)
+
+/*public non-poly*/ void morkCursor::CloseCursor(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mCursor_Seed = 0;
+ mCursor_Pos = -1;
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// { ----- begin ref counting for well-behaved cyclic graphs -----
+NS_IMETHODIMP
+morkCursor::GetWeakRefCount(nsIMdbEnv* mev, // weak refs
+ mdb_count* outCount) {
+ *outCount = WeakRefsOnly();
+ return NS_OK;
+}
+NS_IMETHODIMP
+morkCursor::GetStrongRefCount(nsIMdbEnv* mev, // strong refs
+ mdb_count* outCount) {
+ *outCount = StrongRefsOnly();
+ return NS_OK;
+}
+// ### TODO - clean up this cast, if required
+NS_IMETHODIMP
+morkCursor::AddWeakRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::AddWeakRef((morkEnv*)mev));
+}
+
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkCursor::AddStrongRef(morkEnv* mev) { return morkNode::AddStrongRef(mev); }
+#endif
+
+NS_IMETHODIMP_(mork_uses)
+morkCursor::AddStrongRef(nsIMdbEnv* mev) {
+ return morkNode::AddStrongRef((morkEnv*)mev);
+}
+
+NS_IMETHODIMP
+morkCursor::CutWeakRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::CutWeakRef((morkEnv*)mev));
+}
+
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkCursor::CutStrongRef(morkEnv* mev) { return morkNode::CutStrongRef(mev); }
+#endif
+
+NS_IMETHODIMP
+morkCursor::CutStrongRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_uses to nsresult
+ return static_cast<nsresult>(morkNode::CutStrongRef((morkEnv*)mev));
+}
+
+NS_IMETHODIMP
+morkCursor::CloseMdbObject(nsIMdbEnv* mev) {
+ return morkNode::CloseMdbObject((morkEnv*)mev);
+}
+
+NS_IMETHODIMP
+morkCursor::IsOpenMdbObject(nsIMdbEnv* mev, mdb_bool* outOpen) {
+ *outOpen = IsOpenNode();
+ return NS_OK;
+}
+NS_IMETHODIMP
+morkCursor::IsFrozenMdbObject(nsIMdbEnv* mev, mdb_bool* outIsReadonly) {
+ *outIsReadonly = IsFrozen();
+ return NS_OK;
+}
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+NS_IMETHODIMP
+morkCursor::GetCount(nsIMdbEnv* mev, mdb_count* outCount) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCursor::GetSeed(nsIMdbEnv* mev, mdb_seed* outSeed) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCursor::SetPos(nsIMdbEnv* mev, mdb_pos inPos) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCursor::GetPos(nsIMdbEnv* mev, mdb_pos* outPos) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCursor::SetDoFailOnSeedOutOfSync(nsIMdbEnv* mev, mdb_bool inFail) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkCursor::GetDoFailOnSeedOutOfSync(nsIMdbEnv* mev, mdb_bool* outFail) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkCursor.h b/comm/mailnews/db/mork/morkCursor.h
new file mode 100644
index 0000000000..11c8ec8839
--- /dev/null
+++ b/comm/mailnews/db/mork/morkCursor.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKCURSOR_
+#define _MORKCURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kCursor /*i*/ 0x4375 /* ascii 'Cu' */
+
+class morkCursor : public morkObject,
+ public nsIMdbCursor { // collection iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD IsFrozenMdbObject(nsIMdbEnv* ev, mdb_bool* outIsReadonly) override;
+ // same as nsIMdbPort::GetIsPortReadonly() when this object is inside a port.
+ // } ----- end attribute methods -----
+
+ // { ----- begin ref counting for well-behaved cyclic graphs -----
+ NS_IMETHOD GetWeakRefCount(nsIMdbEnv* ev, // weak refs
+ mdb_count* outCount) override;
+ NS_IMETHOD GetStrongRefCount(nsIMdbEnv* ev, // strong refs
+ mdb_count* outCount) override;
+
+ NS_IMETHOD AddWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of AddStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) AddStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD_(mork_uses) AddStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CutWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of CutStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) CutStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD CutStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CloseMdbObject(
+ nsIMdbEnv* ev) override; // called at strong refs zero
+ NS_IMETHOD IsOpenMdbObject(nsIMdbEnv* ev, mdb_bool* outOpen) override;
+ // } ----- end ref counting -----
+
+ // } ===== end nsIMdbObject methods =====
+
+ // { ===== begin nsIMdbCursor methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetCount(nsIMdbEnv* ev, mdb_count* outCount) override; // readonly
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev, mdb_seed* outSeed) override; // readonly
+
+ NS_IMETHOD SetPos(nsIMdbEnv* ev, mdb_pos inPos) override; // mutable
+ NS_IMETHOD GetPos(nsIMdbEnv* ev, mdb_pos* outPos) override;
+
+ NS_IMETHOD SetDoFailOnSeedOutOfSync(nsIMdbEnv* ev, mdb_bool inFail) override;
+ NS_IMETHOD GetDoFailOnSeedOutOfSync(nsIMdbEnv* ev,
+ mdb_bool* outFail) override;
+ // } ----- end attribute methods -----
+
+ // } ===== end nsIMdbCursor methods =====
+
+ // } ----- end attribute methods -----
+
+ mork_seed mCursor_Seed;
+ mork_pos mCursor_Pos;
+ mork_bool mCursor_DoFailOnSeedOutOfSync;
+ mork_u1 mCursor_Pad[3]; // explicitly pad to u4 alignment
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseCursor() only if open
+
+ public: // morkCursor construction & destruction
+ morkCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap);
+ void CloseCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ protected:
+ virtual ~morkCursor(); // assert that CloseCursor() executed earlier
+
+ private: // copying is not allowed
+ morkCursor(const morkCursor& other);
+ morkCursor& operator=(const morkCursor& other);
+
+ public: // dynamic type identification
+ mork_bool IsCursor() const {
+ return IsNode() && mNode_Derived == morkDerived_kCursor;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // other cursor methods
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakCursor(morkCursor* me, morkEnv* ev, morkCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongCursor(morkCursor* me, morkEnv* ev,
+ morkCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkDeque.cpp b/comm/mailnews/db/mork/morkDeque.cpp
new file mode 100644
index 0000000000..7490aef84b
--- /dev/null
+++ b/comm/mailnews/db/mork/morkDeque.cpp
@@ -0,0 +1,246 @@
+/*************************************************************************
+This software is part of a public domain IronDoc source code distribution,
+and is provided on an "AS IS" basis, with all risks borne by the consumers
+or users of the IronDoc software. There are no warranties, guarantees, or
+promises about quality of any kind; and no remedies for failure exist.
+
+Permission is hereby granted to use this IronDoc software for any purpose
+at all, without need for written agreements, without royalty or license
+fees, and without fees or obligations of any other kind. Anyone can use,
+copy, change and distribute this software for any purpose, and nothing is
+required, implicitly or otherwise, in exchange for this usage.
+
+You cannot apply your own copyright to this software, but otherwise you
+are encouraged to enjoy the use of this software in any way you see fit.
+However, it would be rude to remove names of developers from the code.
+(IronDoc is also known by the short name "Fe" and a longer name "Ferrum",
+which are used interchangeably with the name IronDoc in the sources.)
+*************************************************************************/
+/*
+ * File: morkDeque.cpp
+ * Contains: Ferrum deque (double ended queue (linked list))
+ *
+ * Copied directly from public domain IronDoc, with minor naming tweaks:
+ * Designed and written by David McCusker, but all this code is public domain.
+ * There are no warranties, no guarantees, no promises, and no remedies.
+ */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+/*=============================================================================
+ * morkNext: linked list node for very simple, singly-linked list
+ */
+
+morkNext::morkNext() : mNext_Link(0) {}
+
+/*static*/ void* morkNext::MakeNewNext(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) {
+ void* next = 0;
+ ioHeap.Alloc(ev->AsMdbEnv(), inSize, (void**)&next);
+ if (!next) ev->OutOfMemoryError();
+
+ return next;
+}
+
+/*static*/
+void morkNext::ZapOldNext(morkEnv* ev, nsIMdbHeap* ioHeap) {
+ if (ioHeap) {
+ ioHeap->Free(ev->AsMdbEnv(), this);
+ } else
+ ev->NilPointerError();
+}
+
+/*=============================================================================
+ * morkList: simple, singly-linked list
+ */
+
+morkList::morkList() : mList_Head(0), mList_Tail(0) {}
+
+void morkList::CutAndZapAllListMembers(morkEnv* ev, nsIMdbHeap* ioHeap)
+// make empty list, zapping every member by calling ZapOldNext()
+{
+ if (ioHeap) {
+ morkNext* next = 0;
+ while ((next = this->PopHead()) != 0) next->ZapOldNext(ev, ioHeap);
+
+ mList_Head = 0;
+ mList_Tail = 0;
+ } else
+ ev->NilPointerError();
+}
+
+void morkList::CutAllListMembers()
+// just make list empty, dropping members without zapping
+{
+ while (this->PopHead())
+ ; /* empty */
+
+ mList_Head = 0;
+ mList_Tail = 0;
+}
+
+morkNext* morkList::PopHead() // cut head of list
+{
+ morkNext* outHead = mList_Head;
+ if (outHead) // anything to cut from list?
+ {
+ morkNext* next = outHead->mNext_Link;
+ mList_Head = next;
+ if (!next) // cut the last member, so tail no longer exists?
+ mList_Tail = 0;
+
+ outHead->mNext_Link = 0; // nil outgoing node link; unnecessary, but tidy
+ }
+ return outHead;
+}
+
+void morkList::PushHead(morkNext* ioLink) // add to head of list
+{
+ morkNext* head = mList_Head; // old head of list
+ morkNext* tail = mList_Tail; // old tail of list
+
+ MORK_ASSERT((head && tail) || (!head && !tail));
+
+ ioLink->mNext_Link = head; // make old head follow the new link
+ if (!head) // list was previously empty?
+ mList_Tail = ioLink; // head is also tail for first member added
+
+ mList_Head = ioLink; // head of list is the new link
+}
+
+void morkList::PushTail(morkNext* ioLink) // add to tail of list
+{
+ morkNext* head = mList_Head; // old head of list
+ morkNext* tail = mList_Tail; // old tail of list
+
+ MORK_ASSERT((head && tail) || (!head && !tail));
+
+ ioLink->mNext_Link = 0;
+ if (tail) {
+ tail->mNext_Link = ioLink;
+ mList_Tail = ioLink;
+ } else // list was previously empty?
+ mList_Head = mList_Tail =
+ ioLink; // tail is also head for first member added
+}
+
+/*=============================================================================
+ * morkLink: linked list node embedded in objs to allow insertion in morkDeques
+ */
+
+morkLink::morkLink() : mLink_Next(0), mLink_Prev(0) {}
+
+/*static*/ void* morkLink::MakeNewLink(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) {
+ void* alink = 0;
+ ioHeap.Alloc(ev->AsMdbEnv(), inSize, (void**)&alink);
+ if (!alink) ev->OutOfMemoryError();
+
+ return alink;
+}
+
+/*static*/
+void morkLink::ZapOldLink(morkEnv* ev, nsIMdbHeap* ioHeap) {
+ if (ioHeap) {
+ ioHeap->Free(ev->AsMdbEnv(), this);
+ } else
+ ev->NilPointerError();
+}
+
+/*=============================================================================
+ * morkDeque: doubly linked list modeled after VAX queue instructions
+ */
+
+morkDeque::morkDeque() { mDeque_Head.SelfRefer(); }
+
+/*| RemoveFirst:
+|*/
+morkLink* morkDeque::RemoveFirst() /*i*/
+{
+ morkLink* alink = mDeque_Head.mLink_Next;
+ if (alink != &mDeque_Head) {
+ (mDeque_Head.mLink_Next = alink->mLink_Next)->mLink_Prev = &mDeque_Head;
+ return alink;
+ }
+ return (morkLink*)0;
+}
+
+/*| RemoveLast:
+|*/
+morkLink* morkDeque::RemoveLast() /*i*/
+{
+ morkLink* alink = mDeque_Head.mLink_Prev;
+ if (alink != &mDeque_Head) {
+ (mDeque_Head.mLink_Prev = alink->mLink_Prev)->mLink_Next = &mDeque_Head;
+ return alink;
+ }
+ return (morkLink*)0;
+}
+
+/*| At:
+|*/
+morkLink* morkDeque::At(mork_pos index) const /*i*/
+/* indexes are one based (and not zero based) */
+{
+ mork_num count = 0;
+ morkLink* alink;
+ for (alink = this->First(); alink; alink = this->After(alink)) {
+ if (++count == (mork_num)index) break;
+ }
+ return alink;
+}
+
+/*| IndexOf:
+|*/
+mork_pos morkDeque::IndexOf(const morkLink* member) const /*i*/
+/* indexes are one based (and not zero based) */
+/* zero means member is not in deque */
+{
+ mork_num count = 0;
+ const morkLink* alink;
+ for (alink = this->First(); alink; alink = this->After(alink)) {
+ ++count;
+ if (member == alink) return (mork_pos)count;
+ }
+ return 0;
+}
+
+/*| Length:
+|*/
+mork_num morkDeque::Length() const /*i*/
+{
+ mork_num count = 0;
+ morkLink* alink;
+ for (alink = this->First(); alink; alink = this->After(alink)) ++count;
+ return count;
+}
+
+/*| LengthCompare:
+|*/
+int morkDeque::LengthCompare(mork_num c) const /*i*/
+{
+ mork_num count = 0;
+ const morkLink* alink;
+ for (alink = this->First(); alink; alink = this->After(alink)) {
+ if (++count > c) return 1;
+ }
+ return (count == c) ? 0 : -1;
+}
diff --git a/comm/mailnews/db/mork/morkDeque.h b/comm/mailnews/db/mork/morkDeque.h
new file mode 100644
index 0000000000..54e5080254
--- /dev/null
+++ b/comm/mailnews/db/mork/morkDeque.h
@@ -0,0 +1,244 @@
+/*************************************************************************
+This software is part of a public domain IronDoc source code distribution,
+and is provided on an "AS IS" basis, with all risks borne by the consumers
+or users of the IronDoc software. There are no warranties, guarantees, or
+promises about quality of any kind; and no remedies for failure exist.
+
+Permission is hereby granted to use this IronDoc software for any purpose
+at all, without need for written agreements, without royalty or license
+fees, and without fees or obligations of any other kind. Anyone can use,
+copy, change and distribute this software for any purpose, and nothing is
+required, implicitly or otherwise, in exchange for this usage.
+
+You cannot apply your own copyright to this software, but otherwise you
+are encouraged to enjoy the use of this software in any way you see fit.
+However, it would be rude to remove names of developers from the code.
+(IronDoc is also known by the short name "Fe" and a longer name "Ferrum",
+which are used interchangeably with the name IronDoc in the sources.)
+*************************************************************************/
+/*
+ * File: morkDeque.h
+ * Contains: Ferrum deque (double ended queue (linked list))
+ *
+ * Copied directly from public domain IronDoc, with minor naming tweaks:
+ * Designed and written by David McCusker, but all this code is public domain.
+ * There are no warranties, no guarantees, no promises, and no remedies.
+ */
+
+#ifndef _MORKDEQUE_
+#define _MORKDEQUE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+/*=============================================================================
+ * morkNext: linked list node for very simple, singly-linked list
+ */
+
+class morkNext /*d*/ {
+ public:
+ morkNext* mNext_Link;
+
+ public:
+ explicit morkNext(int inZero) : mNext_Link(0) {}
+
+ explicit morkNext(morkNext* ioLink) : mNext_Link(ioLink) {}
+
+ morkNext(); // mNext_Link( 0 ), { }
+
+ public:
+ morkNext* GetNextLink() const { return mNext_Link; }
+
+ public: // link memory management methods
+ static void* MakeNewNext(size_t inSize, nsIMdbHeap& ioHeap, morkEnv* ev);
+ void ZapOldNext(morkEnv* ev, nsIMdbHeap* ioHeap);
+
+ public: // link memory management operators
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkNext::MakeNewNext(inSize, ioHeap, ev);
+ }
+
+ void operator delete(void* ioAddress) // DO NOT CALL THIS, hope to crash:
+ {
+ ((morkNext*)0)->ZapOldNext((morkEnv*)0, (nsIMdbHeap*)0);
+ } // boom
+};
+
+/*=============================================================================
+ * morkList: simple, singly-linked list
+ */
+
+/*| morkList: a list of singly-linked members (instances of morkNext), where
+**| the number of list members might be so numerous that we must about cost
+**| for two pointer link slots per member (as happens with morkLink).
+**|
+**|| morkList is intended to support lists of changes in morkTable, where we
+**| are worried about the space cost of representing such changes. (Later we
+**| can use an array instead, when we get even more worried, to avoid cost
+**| of link slots at all, per member).
+**|
+**|| Do NOT create cycles in links using this list class, since we do not
+**| deal with them very nicely.
+|*/
+class morkList /*d*/ {
+ public:
+ morkNext* mList_Head; // first link in the list
+ morkNext* mList_Tail; // last link in the list
+
+ public:
+ morkNext* GetListHead() const { return mList_Head; }
+ morkNext* GetListTail() const { return mList_Tail; }
+
+ mork_bool IsListEmpty() const { return (mList_Head == 0); }
+ mork_bool HasListMembers() const { return (mList_Head != 0); }
+
+ public:
+ morkList(); // : mList_Head( 0 ), mList_Tail( 0 ) { }
+
+ void CutAndZapAllListMembers(morkEnv* ev, nsIMdbHeap* ioHeap);
+ // make empty list, zapping every member by calling ZapOldNext()
+
+ void CutAllListMembers();
+ // just make list empty, dropping members without zapping
+
+ public:
+ morkNext* PopHead(); // cut head of list
+
+ // Note we don't support PopTail(), so use morkDeque if you need that.
+
+ void PushHead(morkNext* ioLink); // add to head of list
+ void PushTail(morkNext* ioLink); // add to tail of list
+};
+
+/*=============================================================================
+ * morkLink: linked list node embedded in objs to allow insertion in morkDeques
+ */
+
+class morkLink /*d*/ {
+ public:
+ morkLink* mLink_Next;
+ morkLink* mLink_Prev;
+
+ public:
+ explicit morkLink(int inZero) : mLink_Next(0), mLink_Prev(0) {}
+
+ morkLink(); // mLink_Next( 0 ), mLink_Prev( 0 ) { }
+
+ public:
+ morkLink* Next() const { return mLink_Next; }
+ morkLink* Prev() const { return mLink_Prev; }
+
+ void SelfRefer() { mLink_Next = mLink_Prev = this; }
+ void Clear() { mLink_Next = mLink_Prev = 0; }
+
+ void AddBefore(morkLink* old) {
+ ((old)->mLink_Prev->mLink_Next = (this))->mLink_Prev = (old)->mLink_Prev;
+ ((this)->mLink_Next = (old))->mLink_Prev = this;
+ }
+
+ void AddAfter(morkLink* old) {
+ ((old)->mLink_Next->mLink_Prev = (this))->mLink_Next = (old)->mLink_Next;
+ ((this)->mLink_Prev = (old))->mLink_Next = this;
+ }
+
+ void Remove() {
+ (mLink_Prev->mLink_Next = mLink_Next)->mLink_Prev = mLink_Prev;
+ }
+
+ public: // link memory management methods
+ static void* MakeNewLink(size_t inSize, nsIMdbHeap& ioHeap, morkEnv* ev);
+ void ZapOldLink(morkEnv* ev, nsIMdbHeap* ioHeap);
+
+ public: // link memory management operators
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkLink::MakeNewLink(inSize, ioHeap, ev);
+ }
+};
+
+/*=============================================================================
+ * morkDeque: doubly linked list modeled after VAX queue instructions
+ */
+
+class morkDeque /*d*/ {
+ public:
+ morkLink mDeque_Head;
+
+ public: // construction
+ morkDeque(); // { mDeque_Head.SelfRefer(); }
+
+ public: // methods
+ morkLink* RemoveFirst();
+
+ morkLink* RemoveLast();
+
+ morkLink* At(mork_pos index) const; /* one-based, not zero-based */
+
+ mork_pos IndexOf(const morkLink* inMember) const;
+ /* one-based index ; zero means member is not in deque */
+
+ mork_num Length() const;
+
+ /* the following method is more efficient for long lists: */
+ int LengthCompare(mork_num inCount) const;
+ /* -1: length < count, 0: length == count, 1: length > count */
+
+ public: // inlines
+ mork_bool IsEmpty() const {
+ return (mDeque_Head.mLink_Next == (morkLink*)&mDeque_Head);
+ }
+
+ morkLink* After(const morkLink* old) const {
+ return (((old)->mLink_Next != &mDeque_Head) ? (old)->mLink_Next
+ : (morkLink*)0);
+ }
+
+ morkLink* Before(const morkLink* old) const {
+ return (((old)->mLink_Prev != &mDeque_Head) ? (old)->mLink_Prev
+ : (morkLink*)0);
+ }
+
+ morkLink* First() const {
+ return ((mDeque_Head.mLink_Next != &mDeque_Head) ? mDeque_Head.mLink_Next
+ : (morkLink*)0);
+ }
+
+ morkLink* Last() const {
+ return ((mDeque_Head.mLink_Prev != &mDeque_Head) ? mDeque_Head.mLink_Prev
+ : (morkLink*)0);
+ }
+
+ /*
+ From IronDoc documentation for AddFirst:
+ +--------+ +--------+ +--------+ +--------+ +--------+
+ | h.next |-->| b.next | | h.next |-->| a.next |-->| b.next |
+ +--------+ +--------+ ==> +--------+ +--------+ +--------+
+ | h.prev |<--| b.prev | | h.prev |<--| a.prev |<--| b.prev |
+ +--------+ +--------+ +--------+ +--------+ +--------+
+ */
+
+ void AddFirst(morkLink* in) /*i*/
+ {
+ (mDeque_Head.mLink_Next->mLink_Prev = in)->mLink_Next =
+ mDeque_Head.mLink_Next;
+ (in->mLink_Prev = &mDeque_Head)->mLink_Next = in;
+ }
+ /*
+ From IronDoc documentation for AddLast:
+ +--------+ +--------+ +--------+ +--------+ +--------+
+ | y.next |-->| h.next | | y.next |-->| z.next |-->| h.next |
+ +--------+ +--------+ ==> +--------+ +--------+ +--------+
+ | y.prev |<--| h.prev | | y.prev |<--| z.prev |<--| h.prev |
+ +--------+ +--------+ +--------+ +--------+ +--------+
+ */
+
+ void AddLast(morkLink* in) {
+ (mDeque_Head.mLink_Prev->mLink_Next = in)->mLink_Prev =
+ mDeque_Head.mLink_Prev;
+ (in->mLink_Next = &mDeque_Head)->mLink_Prev = in;
+ }
+};
+
+#endif /* _MORKDEQUE_ */
diff --git a/comm/mailnews/db/mork/morkEnv.cpp b/comm/mailnews/db/mork/morkEnv.cpp
new file mode 100644
index 0000000000..c7c67f1e9e
--- /dev/null
+++ b/comm/mailnews/db/mork/morkEnv.cpp
@@ -0,0 +1,519 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKCH_
+# include "morkCh.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKFACTORY_
+# include "morkFactory.h"
+#endif
+
+#include "mozilla/Char16.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkEnv::CloseMorkNode(
+ morkEnv* ev) /*i*/ // CloseEnv() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseEnv(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkEnv::~morkEnv() /*i*/ // assert CloseEnv() executed earlier
+{
+ CloseMorkNode(mMorkEnv);
+ if (mEnv_Heap) {
+ mork_bool ownsHeap = mEnv_OwnsHeap;
+ nsIMdbHeap* saveHeap = mEnv_Heap;
+
+ if (ownsHeap) {
+#ifdef MORK_DEBUG_HEAP_STATS
+ printf("%d blocks remaining \n",
+ ((orkinHeap*)saveHeap)->HeapBlockCount());
+ mork_u4* array = (mork_u4*)this;
+ array -= 3;
+ // null out heap ptr in mem block so we won't crash trying to use it to
+ // delete the env.
+ *array = nullptr;
+#endif // MORK_DEBUG_HEAP_STATS
+ // whoops, this is our heap - hmm. Can't delete it, or not allocate env's
+ // from an orkinHeap.
+ delete saveHeap;
+ }
+ }
+ // MORK_ASSERT(mEnv_SelfAsMdbEnv==0);
+ MORK_ASSERT(mEnv_ErrorHook == 0);
+}
+
+/* choose morkBool_kTrue or morkBool_kFalse for kBeVerbose: */
+#define morkEnv_kBeVerbose morkBool_kFalse
+
+/*public non-poly*/
+morkEnv::morkEnv(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkFactory* ioFactory, nsIMdbHeap* ioSlotHeap)
+ : morkObject(inUsage, ioHeap, morkColor_kNone),
+ mEnv_Factory(ioFactory),
+ mEnv_Heap(ioSlotHeap)
+
+ ,
+ mEnv_SelfAsMdbEnv(0),
+ mEnv_ErrorHook(0),
+ mEnv_HandlePool(0)
+
+ ,
+ mEnv_ErrorCount(0),
+ mEnv_WarningCount(0)
+
+ ,
+ mEnv_ErrorCode(NS_OK)
+
+ ,
+ mEnv_DoTrace(morkBool_kFalse),
+ mEnv_AutoClear(morkAble_kDisabled),
+ mEnv_ShouldAbort(morkBool_kFalse),
+ mEnv_BeVerbose(morkEnv_kBeVerbose),
+ mEnv_OwnsHeap(morkBool_kFalse) {
+ MORK_ASSERT(ioSlotHeap && ioFactory);
+ if (ioSlotHeap) {
+ // mEnv_Heap is NOT refcounted:
+ // nsIMdbHeap_SlotStrongHeap(ioSlotHeap, this, &mEnv_Heap);
+
+ mEnv_HandlePool =
+ new morkPool(morkUsage::kGlobal, (nsIMdbHeap*)0, ioSlotHeap);
+
+ MORK_ASSERT(mEnv_HandlePool);
+ if (mEnv_HandlePool && this->Good()) {
+ mNode_Derived = morkDerived_kEnv;
+ mNode_Refs += morkEnv_kWeakRefCountEnvBonus;
+ }
+ }
+}
+
+/*public non-poly*/
+morkEnv::morkEnv(morkEnv* ev, /*i*/
+ const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbEnv* inSelfAsMdbEnv, morkFactory* ioFactory,
+ nsIMdbHeap* ioSlotHeap)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mEnv_Factory(ioFactory),
+ mEnv_Heap(ioSlotHeap)
+
+ ,
+ mEnv_SelfAsMdbEnv(inSelfAsMdbEnv),
+ mEnv_ErrorHook(0),
+ mEnv_HandlePool(0)
+
+ ,
+ mEnv_ErrorCount(0),
+ mEnv_WarningCount(0)
+
+ ,
+ mEnv_ErrorCode(NS_OK)
+
+ ,
+ mEnv_DoTrace(morkBool_kFalse),
+ mEnv_AutoClear(morkAble_kDisabled),
+ mEnv_ShouldAbort(morkBool_kFalse),
+ mEnv_BeVerbose(morkEnv_kBeVerbose),
+ mEnv_OwnsHeap(morkBool_kFalse) {
+ // $$$ do we need to refcount the inSelfAsMdbEnv nsIMdbEnv??
+
+ if (ioFactory && inSelfAsMdbEnv && ioSlotHeap) {
+ // mEnv_Heap is NOT refcounted:
+ // nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mEnv_Heap);
+
+ mEnv_HandlePool = new (*ioSlotHeap, ev)
+ morkPool(ev, morkUsage::kHeap, ioSlotHeap, ioSlotHeap);
+
+ MORK_ASSERT(mEnv_HandlePool);
+ if (mEnv_HandlePool && ev->Good()) {
+ mNode_Derived = morkDerived_kEnv;
+ mNode_Refs += morkEnv_kWeakRefCountEnvBonus;
+ }
+ } else
+ ev->NilPointerError();
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkEnv, morkObject, nsIMdbEnv)
+/*public non-poly*/ void morkEnv::CloseEnv(
+ morkEnv* ev) /*i*/ // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ // $$$ release mEnv_SelfAsMdbEnv??
+ // $$$ release mEnv_ErrorHook??
+
+ mEnv_SelfAsMdbEnv = 0;
+ mEnv_ErrorHook = 0;
+
+ morkPool* savePool = mEnv_HandlePool;
+ morkPool::SlotStrongPool((morkPool*)0, ev, &mEnv_HandlePool);
+ // free the pool
+ if (mEnv_SelfAsMdbEnv) {
+ if (savePool && mEnv_Heap) mEnv_Heap->Free(this->AsMdbEnv(), savePool);
+ } else {
+ if (savePool) {
+ if (savePool->IsOpenNode()) savePool->CloseMorkNode(ev);
+ delete savePool;
+ }
+ // how do we free this? might need to get rid of asserts.
+ }
+ // mEnv_Factory is NOT refcounted
+
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+mork_size morkEnv::OidAsHex(void* outBuf, const mdbOid& inOid)
+// sprintf(buf, "%lX:^%lX", (long) inOid.mOid_Id, (long) inOid.mOid_Scope);
+{
+ mork_u1* p = (mork_u1*)outBuf;
+ mork_size outSize = this->TokenAsHex(p, inOid.mOid_Id);
+ p += outSize;
+ *p++ = ':';
+
+ mork_scope scope = inOid.mOid_Scope;
+ if (scope < 0x80 && morkCh_IsName((mork_ch)scope)) {
+ *p++ = (mork_u1)scope;
+ *p = 0; // null termination
+ outSize += 2;
+ } else {
+ *p++ = '^';
+ mork_size scopeSize = this->TokenAsHex(p, scope);
+ outSize += scopeSize + 2;
+ }
+ return outSize;
+}
+
+mork_u1 morkEnv::HexToByte(mork_ch inFirstHex, mork_ch inSecondHex) {
+ mork_u1 hi = 0; // high four hex bits
+ mork_flags f = morkCh_GetFlags(inFirstHex);
+ if (morkFlags_IsDigit(f))
+ hi = (mork_u1)(inFirstHex - (mork_ch)'0');
+ else if (morkFlags_IsUpper(f))
+ hi = (mork_u1)((inFirstHex - (mork_ch)'A') + 10);
+ else if (morkFlags_IsLower(f))
+ hi = (mork_u1)((inFirstHex - (mork_ch)'a') + 10);
+
+ mork_u1 lo = 0; // low four hex bits
+ f = morkCh_GetFlags(inSecondHex);
+ if (morkFlags_IsDigit(f))
+ lo = (mork_u1)(inSecondHex - (mork_ch)'0');
+ else if (morkFlags_IsUpper(f))
+ lo = (mork_u1)((inSecondHex - (mork_ch)'A') + 10);
+ else if (morkFlags_IsLower(f))
+ lo = (mork_u1)((inSecondHex - (mork_ch)'a') + 10);
+
+ return (mork_u1)((hi << 4) | lo);
+}
+
+// TokenAsHex() is the same as sprintf(outBuf, "%lX", (long) inToken);
+// Writes up to 32 hex digits, plus a NUL-terminator. So outBuf must
+// be at least 33 bytes.
+// Return value is number of characters written, excluding the NUL.
+mork_size morkEnv::TokenAsHex(void* outBuf, mork_token inToken) {
+ static const char morkEnv_kHexDigits[] = "0123456789ABCDEF";
+ char* p = (char*)outBuf;
+ char* end = p + 32; // write no more than 32 digits for safety
+ if (inToken) {
+ // first write all the hex digits in backwards order:
+ while (p < end && inToken) // more digits to write?
+ {
+ *p++ = morkEnv_kHexDigits[inToken & 0x0F]; // low four bits
+ inToken >>= 4; // we fervently hope this does not sign extend
+ }
+ *p = 0; // end the string with a null byte
+ char* s = (char*)outBuf; // first byte in string
+ mork_size size = (mork_size)(p - s); // distance from start
+
+ // now reverse the string in place:
+ // note that p starts on the null byte, so we need predecrement:
+ while (--p > s) // need to swap another byte in the string?
+ {
+ char c = *p; // temp for swap
+ *p = *s;
+ *s++ = c; // move s forward here, and p backward in the test
+ }
+ return size;
+ } else // special case for zero integer
+ {
+ *p++ = '0'; // write a zero digit
+ *p = 0; // end with a null byte
+ return 1; // one digit in hex representation
+ }
+}
+
+void morkEnv::StringToYarn(const PathChar* inString, mdbYarn* outYarn) {
+ if (outYarn) {
+ mdb_fill fill =
+ (inString) ? (mdb_fill)MORK_STRLEN(inString) * sizeof(PathChar) : 0;
+
+ if (fill) // have nonempty content?
+ {
+ mdb_size size = outYarn->mYarn_Size; // max dest size
+ if (fill > size) // too much string content?
+ {
+ outYarn->mYarn_More = fill - size; // extra string bytes omitted
+ fill = size; // copy no more bytes than size of yarn buffer
+ }
+ void* dest = outYarn->mYarn_Buf; // where bytes are going
+ if (!dest) // nil destination address buffer?
+ fill = 0; // we can't write any content at all
+
+ if (fill) // anything to copy?
+ MORK_MEMCPY(dest, inString, fill); // copy fill bytes to yarn
+
+ outYarn->mYarn_Fill = fill; // tell yarn size of copied content
+ } else // no content to put into the yarn
+ {
+ outYarn->mYarn_Fill = 0; // tell yarn that string has no bytes
+ }
+ outYarn->mYarn_Form = 0; // always update the form slot
+ } else
+ this->NilPointerError();
+}
+
+morkEnv::PathChar* morkEnv::CopyString(nsIMdbHeap* ioHeap,
+ const PathChar* inString) {
+ PathChar* outString = nullptr;
+ if (ioHeap && inString) {
+ mork_size size = (MORK_STRLEN(inString) + 1) * sizeof(PathChar);
+ ioHeap->Alloc(this->AsMdbEnv(), size, (void**)&outString);
+ if (outString) MORK_STRCPY(outString, inString);
+ } else
+ this->NilPointerError();
+ return outString;
+}
+
+void morkEnv::FreeString(nsIMdbHeap* ioHeap, PathChar* ioString) {
+ if (ioHeap) {
+ if (ioString) ioHeap->Free(this->AsMdbEnv(), ioString);
+ } else
+ this->NilPointerError();
+}
+
+void morkEnv::NewError(const char* inString) {
+ MORK_ASSERT(morkBool_kFalse); // get developer's attention
+
+ ++mEnv_ErrorCount;
+ mEnv_ErrorCode = NS_ERROR_FAILURE;
+
+ if (mEnv_ErrorHook) mEnv_ErrorHook->OnErrorString(this->AsMdbEnv(), inString);
+}
+
+void morkEnv::NewWarning(const char* inString) {
+ MORK_ASSERT(morkBool_kFalse); // get developer's attention
+
+ ++mEnv_WarningCount;
+ if (mEnv_ErrorHook)
+ mEnv_ErrorHook->OnWarningString(this->AsMdbEnv(), inString);
+}
+
+void morkEnv::StubMethodOnlyError() { this->NewError("method is stub only"); }
+
+void morkEnv::OutOfMemoryError() { this->NewError("out of memory"); }
+
+void morkEnv::CantMakeWhenBadError() {
+ this->NewError("can't make an object when ev->Bad()");
+}
+
+static const char morkEnv_kNilPointer[] = "nil pointer";
+
+void morkEnv::NilPointerError() { this->NewError(morkEnv_kNilPointer); }
+
+void morkEnv::NilPointerWarning() { this->NewWarning(morkEnv_kNilPointer); }
+
+void morkEnv::NewNonEnvError() { this->NewError("non-env instance"); }
+
+void morkEnv::NilEnvSlotError() {
+ if (!mEnv_HandlePool || !mEnv_Factory) {
+ if (!mEnv_HandlePool) this->NewError("nil mEnv_HandlePool");
+ if (!mEnv_Factory) this->NewError("nil mEnv_Factory");
+ } else
+ this->NewError("unknown nil env slot");
+}
+
+void morkEnv::NonEnvTypeError(morkEnv* ev) { ev->NewError("non morkEnv"); }
+
+void morkEnv::ClearMorkErrorsAndWarnings() {
+ mEnv_ErrorCount = 0;
+ mEnv_WarningCount = 0;
+ mEnv_ErrorCode = NS_OK;
+ mEnv_ShouldAbort = morkBool_kFalse;
+}
+
+void morkEnv::AutoClearMorkErrorsAndWarnings() {
+ if (this->DoAutoClear()) {
+ mEnv_ErrorCount = 0;
+ mEnv_WarningCount = 0;
+ mEnv_ErrorCode = NS_OK;
+ mEnv_ShouldAbort = morkBool_kFalse;
+ }
+}
+
+/*static*/ morkEnv* morkEnv::FromMdbEnv(
+ nsIMdbEnv* ioEnv) // dynamic type checking
+{
+ morkEnv* outEnv = 0;
+ if (ioEnv) {
+ // Note this cast is expected to perform some address adjustment of the
+ // pointer, so oenv likely does not equal ioEnv. Do not cast to void*
+ // first to force an exactly equal pointer (we tried it and it's wrong).
+ morkEnv* ev = (morkEnv*)ioEnv;
+ if (ev && ev->IsEnv()) {
+ if (ev->DoAutoClear()) {
+ ev->mEnv_ErrorCount = 0;
+ ev->mEnv_WarningCount = 0;
+ ev->mEnv_ErrorCode = NS_OK;
+ }
+ outEnv = ev;
+ } else
+ MORK_ASSERT(outEnv);
+ } else
+ MORK_ASSERT(outEnv);
+ return outEnv;
+}
+
+NS_IMETHODIMP
+morkEnv::GetErrorCount(mdb_count* outCount, mdb_bool* outShouldAbort) {
+ if (outCount) *outCount = mEnv_ErrorCount;
+ if (outShouldAbort) *outShouldAbort = mEnv_ShouldAbort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetWarningCount(mdb_count* outCount, mdb_bool* outShouldAbort) {
+ if (outCount) *outCount = mEnv_WarningCount;
+ if (outShouldAbort) *outShouldAbort = mEnv_ShouldAbort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetEnvBeVerbose(mdb_bool* outBeVerbose) {
+ NS_ENSURE_ARG_POINTER(outBeVerbose);
+ *outBeVerbose = mEnv_BeVerbose;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::SetEnvBeVerbose(mdb_bool inBeVerbose) {
+ mEnv_BeVerbose = inBeVerbose;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetDoTrace(mdb_bool* outDoTrace) {
+ NS_ENSURE_ARG_POINTER(outDoTrace);
+ *outDoTrace = mEnv_DoTrace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::SetDoTrace(mdb_bool inDoTrace) {
+ mEnv_DoTrace = inDoTrace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetAutoClear(mdb_bool* outAutoClear) {
+ NS_ENSURE_ARG_POINTER(outAutoClear);
+ *outAutoClear = DoAutoClear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::SetAutoClear(mdb_bool inAutoClear) {
+ if (inAutoClear)
+ EnableAutoClear();
+ else
+ DisableAutoClear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetErrorHook(nsIMdbErrorHook** acqErrorHook) {
+ NS_ENSURE_ARG_POINTER(acqErrorHook);
+ *acqErrorHook = mEnv_ErrorHook;
+ NS_IF_ADDREF(mEnv_ErrorHook);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::SetErrorHook(nsIMdbErrorHook* ioErrorHook) // becomes referenced
+{
+ mEnv_ErrorHook = ioErrorHook;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::GetHeap(nsIMdbHeap** acqHeap) {
+ NS_ENSURE_ARG_POINTER(acqHeap);
+ nsIMdbHeap* outHeap = mEnv_Heap;
+
+ if (acqHeap) *acqHeap = outHeap;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::SetHeap(nsIMdbHeap* ioHeap) // becomes referenced
+{
+ nsIMdbHeap_SlotStrongHeap(ioHeap, this, &mEnv_Heap);
+ return NS_OK;
+}
+// } ----- end attribute methods -----
+
+NS_IMETHODIMP
+morkEnv::ClearErrors() // clear errors beore re-entering db API
+{
+ mEnv_ErrorCount = 0;
+ mEnv_ErrorCode = NS_OK;
+ mEnv_ShouldAbort = morkBool_kFalse;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::ClearWarnings() // clear warning
+{
+ mEnv_WarningCount = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkEnv::ClearErrorsAndWarnings() // clear both errors & warnings
+{
+ ClearMorkErrorsAndWarnings();
+ return NS_OK;
+}
+// } ===== end nsIMdbEnv methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkEnv.h b/comm/mailnews/db/mork/morkEnv.h
new file mode 100644
index 0000000000..e9b635051d
--- /dev/null
+++ b/comm/mailnews/db/mork/morkEnv.h
@@ -0,0 +1,221 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKENV_
+#define _MORKENV_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+// sean was here
+#include "mozilla/Path.h"
+#include "nsError.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kEnv /*i*/ 0x4576 /* ascii 'Ev' */
+
+// use NS error codes to make Mork easier to use with the rest of mozilla
+#define morkEnv_kNoError NS_SUCCEEDED /* no error has happened */
+#define morkEnv_kNonEnvTypeError \
+ NS_ERROR_FAILURE /* morkEnv::IsEnv() is false */
+
+#define morkEnv_kStubMethodOnlyError NS_ERROR_NO_INTERFACE
+#define morkEnv_kOutOfMemoryError NS_ERROR_OUT_OF_MEMORY
+#define morkEnv_kNilPointerError NS_ERROR_NULL_POINTER
+#define morkEnv_kNewNonEnvError NS_ERROR_FAILURE
+#define morkEnv_kNilEnvSlotError NS_ERROR_FAILURE
+
+#define morkEnv_kBadFactoryError NS_ERROR_FACTORY_NOT_LOADED
+#define morkEnv_kBadFactoryEnvError NS_ERROR_FACTORY_NOT_LOADED
+#define morkEnv_kBadEnvError NS_ERROR_FAILURE
+
+#define morkEnv_kNonHandleTypeError NS_ERROR_FAILURE
+#define morkEnv_kNonOpenNodeError NS_ERROR_FAILURE
+
+/* try NOT to leak all env instances */
+#define morkEnv_kWeakRefCountEnvBonus 0
+
+/*| morkEnv:
+|*/
+class morkEnv : public morkObject, public nsIMdbEnv {
+ using PathChar = mozilla::filesystem::Path::value_type;
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // state is public because the entire Mork system is private
+ morkFactory* mEnv_Factory; // NON-refcounted factory
+ nsIMdbHeap* mEnv_Heap; // NON-refcounted heap
+
+ nsIMdbEnv* mEnv_SelfAsMdbEnv;
+ nsIMdbErrorHook* mEnv_ErrorHook;
+
+ morkPool* mEnv_HandlePool; // pool for re-using handles
+
+ mork_u2 mEnv_ErrorCount;
+ mork_u2 mEnv_WarningCount;
+
+ nsresult mEnv_ErrorCode;
+
+ mork_bool mEnv_DoTrace;
+ mork_able mEnv_AutoClear;
+ mork_bool mEnv_ShouldAbort;
+ mork_bool mEnv_BeVerbose;
+ mork_bool mEnv_OwnsHeap;
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseEnv() only if open
+ virtual ~morkEnv(); // assert that CloseEnv() executed earlier
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetErrorCount(mdb_count* outCount,
+ mdb_bool* outShouldAbort) override;
+ NS_IMETHOD GetWarningCount(mdb_count* outCount,
+ mdb_bool* outShouldAbort) override;
+
+ NS_IMETHOD GetEnvBeVerbose(mdb_bool* outBeVerbose) override;
+ NS_IMETHOD SetEnvBeVerbose(mdb_bool inBeVerbose) override;
+
+ NS_IMETHOD GetDoTrace(mdb_bool* outDoTrace) override;
+ NS_IMETHOD SetDoTrace(mdb_bool inDoTrace) override;
+
+ NS_IMETHOD GetAutoClear(mdb_bool* outAutoClear) override;
+ NS_IMETHOD SetAutoClear(mdb_bool inAutoClear) override;
+
+ NS_IMETHOD GetErrorHook(nsIMdbErrorHook** acqErrorHook) override;
+ NS_IMETHOD SetErrorHook(
+ nsIMdbErrorHook* ioErrorHook) override; // becomes referenced
+
+ NS_IMETHOD GetHeap(nsIMdbHeap** acqHeap) override;
+ NS_IMETHOD SetHeap(nsIMdbHeap* ioHeap) override; // becomes referenced
+ // } ----- end attribute methods -----
+
+ NS_IMETHOD ClearErrors() override; // clear errors beore re-entering db API
+ NS_IMETHOD ClearWarnings() override; // clear warnings
+ NS_IMETHOD ClearErrorsAndWarnings() override; // clear both errors & warnings
+ // } ===== end nsIMdbEnv methods =====
+ public: // morkEnv construction & destruction
+ morkEnv(const morkUsage& inUsage, nsIMdbHeap* ioHeap, morkFactory* ioFactory,
+ nsIMdbHeap* ioSlotHeap);
+ morkEnv(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbEnv* inSelfAsMdbEnv, morkFactory* ioFactory,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseEnv(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkEnv(const morkEnv& other);
+ morkEnv& operator=(const morkEnv& other);
+
+ public: // dynamic type identification
+ mork_bool IsEnv() const {
+ return IsNode() && mNode_Derived == morkDerived_kEnv;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // utility env methods
+ mork_u1 HexToByte(mork_ch inFirstHex, mork_ch inSecondHex);
+
+ mork_size TokenAsHex(void* outBuf, mork_token inToken);
+ // TokenAsHex() is the same as sprintf(outBuf, "%lX", (long) inToken);
+
+ mork_size OidAsHex(void* outBuf, const mdbOid& inOid);
+ // sprintf(buf, "%lX:^%lX", (long) inOid.mOid_Id, (long) inOid.mOid_Scope);
+
+ PathChar* CopyString(nsIMdbHeap* ioHeap, const PathChar* inString);
+ void FreeString(nsIMdbHeap* ioHeap, PathChar* ioString);
+ void StringToYarn(const PathChar* inString, mdbYarn* outYarn);
+
+ public: // other env methods
+ morkHandleFace* NewHandle(mork_size inSize) {
+ return mEnv_HandlePool->NewHandle(this, inSize, (morkZone*)0);
+ }
+
+ void ZapHandle(morkHandleFace* ioHandle) {
+ mEnv_HandlePool->ZapHandle(this, ioHandle);
+ }
+
+ void EnableAutoClear() { mEnv_AutoClear = morkAble_kEnabled; }
+ void DisableAutoClear() { mEnv_AutoClear = morkAble_kDisabled; }
+
+ mork_bool DoAutoClear() const { return mEnv_AutoClear == morkAble_kEnabled; }
+
+ void NewError(const char* inString);
+ void NewWarning(const char* inString);
+
+ void ClearMorkErrorsAndWarnings(); // clear both errors & warnings
+ void AutoClearMorkErrorsAndWarnings(); // clear if auto is enabled
+
+ void StubMethodOnlyError();
+ void OutOfMemoryError();
+ void NilPointerError();
+ void NilPointerWarning();
+ void CantMakeWhenBadError();
+ void NewNonEnvError();
+ void NilEnvSlotError();
+
+ void NonEnvTypeError(morkEnv* ev);
+
+ // canonical env convenience methods to check for presence of errors:
+ mork_bool Good() const { return (mEnv_ErrorCount == 0); }
+ mork_bool Bad() const { return (mEnv_ErrorCount != 0); }
+
+ nsIMdbEnv* AsMdbEnv() { return (nsIMdbEnv*)this; }
+ static morkEnv* FromMdbEnv(nsIMdbEnv* ioEnv); // dynamic type checking
+
+ nsresult AsErr() const { return mEnv_ErrorCode; }
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakEnv(morkEnv* me, morkEnv* ev, morkEnv** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongEnv(morkEnv* me, morkEnv* ev, morkEnv** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+#undef MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING
+#ifdef MOZ_IS_DESTRUCTIBLE
+# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) \
+ static_assert( \
+ !MOZ_IS_DESTRUCTIBLE(X) || mozilla::IsSame<X, morkEnv>::value, \
+ "Reference-counted class " #X \
+ " should not have a public destructor. " \
+ "Try to make this class's destructor non-public. If that is really " \
+ "not possible, you can whitelist this class by providing a " \
+ "HasDangerousPublicDestructor specialization for it.");
+#else
+# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X)
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKENV_ */
diff --git a/comm/mailnews/db/mork/morkFactory.cpp b/comm/mailnews/db/mork/morkFactory.cpp
new file mode 100644
index 0000000000..09a76ba86a
--- /dev/null
+++ b/comm/mailnews/db/mork/morkFactory.cpp
@@ -0,0 +1,521 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKFACTORY_
+# include "morkFactory.h"
+#endif
+
+#ifndef _ORKINHEAP_
+# include "orkinHeap.h"
+#endif
+
+#ifndef _MORKFILE_
+# include "morkFile.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKTHUMB_
+# include "morkThumb.h"
+#endif
+
+#ifndef _MORKWRITER_
+# include "morkWriter.h"
+#endif
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkFactory::CloseMorkNode(
+ morkEnv* ev) /*i*/ // CloseFactory() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseFactory(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkFactory::~morkFactory() /*i*/ // assert CloseFactory() executed earlier
+{
+ CloseFactory(&mFactory_Env);
+ MORK_ASSERT(mFactory_Env.IsShutNode());
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkFactory::morkFactory() // uses orkinHeap
+ : morkObject(morkUsage::kGlobal, (nsIMdbHeap*)0, morkColor_kNone),
+ mFactory_Env(morkUsage::kMember, (nsIMdbHeap*)0, this, new orkinHeap()),
+ mFactory_Heap() {
+ if (mFactory_Env.Good()) {
+ mNode_Derived = morkDerived_kFactory;
+ mNode_Refs += morkFactory_kWeakRefCountBonus;
+ }
+}
+
+/*public non-poly*/
+morkFactory::morkFactory(nsIMdbHeap* ioHeap)
+ : morkObject(morkUsage::kHeap, ioHeap, morkColor_kNone),
+ mFactory_Env(morkUsage::kMember, (nsIMdbHeap*)0, this, ioHeap),
+ mFactory_Heap() {
+ if (mFactory_Env.Good()) {
+ mNode_Derived = morkDerived_kFactory;
+ mNode_Refs += morkFactory_kWeakRefCountBonus;
+ }
+}
+
+/*public non-poly*/
+morkFactory::morkFactory(morkEnv* ev, /*i*/
+ const morkUsage& inUsage, nsIMdbHeap* ioHeap)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mFactory_Env(morkUsage::kMember, (nsIMdbHeap*)0, this, ioHeap),
+ mFactory_Heap() {
+ if (ev->Good()) {
+ mNode_Derived = morkDerived_kFactory;
+ mNode_Refs += morkFactory_kWeakRefCountBonus;
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkFactory, morkObject, nsIMdbFactory)
+
+extern "C" nsIMdbFactory* MakeMdbFactory() {
+ return new morkFactory(new orkinHeap());
+}
+
+/*public non-poly*/ void morkFactory::CloseFactory(
+ morkEnv* ev) /*i*/ // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mFactory_Env.CloseMorkNode(ev);
+ this->CloseObject(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+morkEnv* morkFactory::GetInternalFactoryEnv(nsresult* outErr) {
+ morkEnv* outEnv = 0;
+ if (IsNode() && IsOpenNode() && IsFactory()) {
+ morkEnv* fenv = &mFactory_Env;
+ if (fenv && fenv->IsNode() && fenv->IsOpenNode() && fenv->IsEnv()) {
+ fenv->ClearMorkErrorsAndWarnings(); // drop any earlier errors
+ outEnv = fenv;
+ } else
+ *outErr = morkEnv_kBadFactoryEnvError;
+ } else
+ *outErr = morkEnv_kBadFactoryError;
+
+ return outEnv;
+}
+
+void morkFactory::NonFactoryTypeError(morkEnv* ev) {
+ ev->NewError("non morkFactory");
+}
+
+NS_IMETHODIMP
+morkFactory::OpenOldFile(nsIMdbEnv* mev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath, mork_bool inFrozen,
+ nsIMdbFile** acqFile)
+// Choose some subclass of nsIMdbFile to instantiate, in order to read
+// (and write if not frozen) the file known by inFilePath. The file
+// returned should be open and ready for use, and presumably positioned
+// at the first byte position of the file. The exact manner in which
+// files must be opened is considered a subclass specific detail, and
+// other portions or Mork source code don't want to know how it's done.
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ morkFile* file = nullptr;
+ if (ev) {
+ if (!ioHeap) ioHeap = &mFactory_Heap;
+
+ file = morkFile::OpenOldFile(ev, ioHeap, inFilePath, inFrozen);
+ NS_IF_ADDREF(file);
+
+ outErr = ev->AsErr();
+ }
+ if (acqFile) *acqFile = file;
+
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkFactory::CreateNewFile(nsIMdbEnv* mev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath, nsIMdbFile** acqFile)
+// Choose some subclass of nsIMdbFile to instantiate, in order to read
+// (and write if not frozen) the file known by inFilePath. The file
+// returned should be created and ready for use, and presumably positioned
+// at the first byte position of the file. The exact manner in which
+// files must be opened is considered a subclass specific detail, and
+// other portions or Mork source code don't want to know how it's done.
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ morkFile* file = nullptr;
+ if (ev) {
+ if (!ioHeap) ioHeap = &mFactory_Heap;
+
+ file = morkFile::CreateNewFile(ev, ioHeap, inFilePath);
+ if (file) NS_ADDREF(file);
+
+ outErr = ev->AsErr();
+ }
+ if (acqFile) *acqFile = file;
+
+ return outErr;
+}
+// } ----- end file methods -----
+
+// { ----- begin env methods -----
+NS_IMETHODIMP
+morkFactory::MakeEnv(nsIMdbHeap* ioHeap, nsIMdbEnv** acqEnv)
+// ioHeap can be nil, causing a MakeHeap() style heap instance to be used
+{
+ nsresult outErr = NS_OK;
+ nsIMdbEnv* outEnv = 0;
+ mork_bool ownsHeap = (ioHeap == 0);
+ if (!ioHeap) ioHeap = new orkinHeap();
+
+ if (acqEnv && ioHeap) {
+ morkEnv* fenv = this->GetInternalFactoryEnv(&outErr);
+ if (fenv) {
+ morkEnv* newEnv =
+ new (*ioHeap, fenv) morkEnv(morkUsage::kHeap, ioHeap, this, ioHeap);
+
+ if (newEnv) {
+ newEnv->mEnv_OwnsHeap = ownsHeap;
+ newEnv->mNode_Refs += morkEnv_kWeakRefCountEnvBonus;
+ NS_ADDREF(newEnv);
+ newEnv->mEnv_SelfAsMdbEnv = newEnv;
+ outEnv = newEnv;
+ } else
+ outErr = morkEnv_kOutOfMemoryError;
+ }
+
+ *acqEnv = outEnv;
+ } else
+ outErr = morkEnv_kNilPointerError;
+
+ return outErr;
+}
+// } ----- end env methods -----
+
+// { ----- begin heap methods -----
+NS_IMETHODIMP
+morkFactory::MakeHeap(nsIMdbEnv* mev, nsIMdbHeap** acqHeap) {
+ nsresult outErr = NS_OK;
+ nsIMdbHeap* outHeap = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ outHeap = new orkinHeap();
+ if (!outHeap) ev->OutOfMemoryError();
+ }
+ MORK_ASSERT(acqHeap);
+ if (acqHeap) *acqHeap = outHeap;
+ return outErr;
+}
+// } ----- end heap methods -----
+
+// { ----- begin row methods -----
+NS_IMETHODIMP
+morkFactory::MakeRow(nsIMdbEnv* mev, nsIMdbHeap* ioHeap, nsIMdbRow** acqRow) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// ioHeap can be nil, causing the heap associated with ev to be used
+// } ----- end row methods -----
+
+// { ----- begin port methods -----
+NS_IMETHODIMP
+morkFactory::CanOpenFilePort(
+ nsIMdbEnv* mev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpen, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) {
+ nsresult outErr = NS_OK;
+ if (outFormatVersion) {
+ outFormatVersion->mYarn_Fill = 0;
+ }
+ mdb_bool canOpenAsPort = morkBool_kFalse;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioFile && outCanOpen) {
+ canOpenAsPort = this->CanOpenMorkTextFile(ev, ioFile);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+
+ if (outCanOpen) *outCanOpen = canOpenAsPort;
+
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkFactory::OpenFilePort(
+ nsIMdbEnv* mev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for readonly import
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb) {
+ NS_ASSERTION(false, "this doesn't look implemented");
+ MORK_USED_1(ioHeap);
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioFile && inOpenPolicy && acqThumb) {
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then call nsIMdbFactory::ThumbToOpenPort() to get the port instance.
+
+NS_IMETHODIMP
+morkFactory::ThumbToOpenPort( // redeeming a completed thumb from
+ // OpenFilePort()
+ nsIMdbEnv* mev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFilePort() with done status
+ nsIMdbPort** acqPort) {
+ nsresult outErr = NS_OK;
+ nsIMdbPort* outPort = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioThumb && acqPort) {
+ morkThumb* thumb = (morkThumb*)ioThumb;
+ morkStore* store = thumb->ThumbToOpenStore(ev);
+ if (store) {
+ store->mStore_CanAutoAssignAtomIdentity = morkBool_kTrue;
+ store->mStore_CanDirty = morkBool_kTrue;
+ store->SetStoreAndAllSpacesCanDirty(ev, morkBool_kTrue);
+
+ NS_ADDREF(store);
+ outPort = store;
+ }
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqPort) *acqPort = outPort;
+ return outErr;
+}
+// } ----- end port methods -----
+
+mork_bool morkFactory::CanOpenMorkTextFile(morkEnv* ev,
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile) {
+ MORK_USED_1(ev);
+ mork_bool outBool = morkBool_kFalse;
+ mork_size headSize = strlen(morkWriter_kFileHeader);
+
+ char localBuf[256 + 4]; // for extra for sloppy safety
+ mdbYarn localYarn;
+ mdbYarn* y = &localYarn;
+ y->mYarn_Buf = localBuf; // space to hold content
+ y->mYarn_Fill = 0; // no logical content yet
+ y->mYarn_Size = 256; // physical capacity is 256 bytes
+ y->mYarn_More = 0;
+ y->mYarn_Form = 0;
+ y->mYarn_Grow = 0;
+
+ if (ioFile) {
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ mdb_size actualSize = 0;
+ ioFile->Get(menv, y->mYarn_Buf, y->mYarn_Size, /*pos*/ 0, &actualSize);
+ y->mYarn_Fill = actualSize;
+
+ if (y->mYarn_Buf && actualSize >= headSize && ev->Good()) {
+ mork_u1* buf = (mork_u1*)y->mYarn_Buf;
+ outBool = (MORK_MEMCMP(morkWriter_kFileHeader, buf, headSize) == 0);
+ }
+ } else
+ ev->NilPointerError();
+
+ return outBool;
+}
+
+// { ----- begin store methods -----
+NS_IMETHODIMP
+morkFactory::CanOpenFileStore(
+ nsIMdbEnv* mev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpenAsStore, // whether OpenFileStore() might succeed
+ mdb_bool* outCanOpenAsPort, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) {
+ mdb_bool canOpenAsStore = morkBool_kFalse;
+ mdb_bool canOpenAsPort = morkBool_kFalse;
+ if (outFormatVersion) {
+ outFormatVersion->mYarn_Fill = 0;
+ }
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioFile && outCanOpenAsStore) {
+ // right now always say true; later we should look for magic patterns
+ canOpenAsStore = this->CanOpenMorkTextFile(ev, ioFile);
+ canOpenAsPort = canOpenAsStore;
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (outCanOpenAsStore) *outCanOpenAsStore = canOpenAsStore;
+
+ if (outCanOpenAsPort) *outCanOpenAsPort = canOpenAsPort;
+
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkFactory::OpenFileStore( // open an existing database
+ nsIMdbEnv* mev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for general db usage
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb) {
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (!ioHeap) // need to use heap from env?
+ ioHeap = ev->mEnv_Heap;
+
+ if (ioFile && inOpenPolicy && acqThumb) {
+ morkStore* store = new (*ioHeap, ev)
+ morkStore(ev, morkUsage::kHeap, ioHeap, this, ioHeap);
+
+ if (store) {
+ mork_bool frozen = morkBool_kFalse; // open store mutable access
+ if (store->OpenStoreFile(ev, frozen, ioFile, inOpenPolicy)) {
+ morkThumb* thumb = morkThumb::Make_OpenFileStore(ev, ioHeap, store);
+ if (thumb) {
+ outThumb = thumb;
+ thumb->AddRef();
+ }
+ }
+ // store->CutStrongRef(mev); // always cut ref (handle has its
+ // own ref)
+ }
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then call nsIMdbFactory::ThumbToOpenStore() to get the store instance.
+
+NS_IMETHODIMP
+morkFactory::ThumbToOpenStore( // redeem completed thumb from OpenFileStore()
+ nsIMdbEnv* mev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFileStore() with done status
+ nsIMdbStore** acqStore) {
+ nsresult outErr = NS_OK;
+ nsIMdbStore* outStore = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioThumb && acqStore) {
+ morkThumb* thumb = (morkThumb*)ioThumb;
+ morkStore* store = thumb->ThumbToOpenStore(ev);
+ if (store) {
+ store->mStore_CanAutoAssignAtomIdentity = morkBool_kTrue;
+ store->mStore_CanDirty = morkBool_kTrue;
+ store->SetStoreAndAllSpacesCanDirty(ev, morkBool_kTrue);
+
+ outStore = store;
+ NS_ADDREF(store);
+ }
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqStore) *acqStore = outStore;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkFactory::CreateNewFileStore( // create a new db with minimal content
+ nsIMdbEnv* mev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // name of file which should not yet exist
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbStore** acqStore) {
+ nsresult outErr = NS_OK;
+ nsIMdbStore* outStore = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (!ioHeap) // need to use heap from env?
+ ioHeap = ev->mEnv_Heap;
+
+ if (ioFile && inOpenPolicy && acqStore && ioHeap) {
+ morkStore* store = new (*ioHeap, ev)
+ morkStore(ev, morkUsage::kHeap, ioHeap, this, ioHeap);
+
+ if (store) {
+ store->mStore_CanAutoAssignAtomIdentity = morkBool_kTrue;
+ store->mStore_CanDirty = morkBool_kTrue;
+ store->SetStoreAndAllSpacesCanDirty(ev, morkBool_kTrue);
+
+ if (store->CreateStoreFile(ev, ioFile, inOpenPolicy)) outStore = store;
+ NS_ADDREF(store);
+ }
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqStore) *acqStore = outStore;
+ return outErr;
+}
+// } ----- end store methods -----
+
+// } ===== end nsIMdbFactory methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkFactory.h b/comm/mailnews/db/mork/morkFactory.h
new file mode 100644
index 0000000000..c04d478edf
--- /dev/null
+++ b/comm/mailnews/db/mork/morkFactory.h
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKFACTORY_
+#define _MORKFACTORY_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _ORKINHEAP_
+# include "orkinHeap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class nsIMdbFactory;
+
+#define morkDerived_kFactory /*i*/ 0x4663 /* ascii 'Fc' */
+#define morkFactory_kWeakRefCountBonus 0 /* try NOT to leak all factories */
+
+/*| morkFactory:
+|*/
+class morkFactory : public morkObject, public nsIMdbFactory { // nsIMdbObject
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // state is public because the entire Mork system is private
+ morkEnv mFactory_Env; // private env instance used internally
+ orkinHeap mFactory_Heap;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ // { ===== begin morkNode interface =====
+ public: // morkFactory virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseFactory() only if open
+
+ // { ===== begin nsIMdbFactory methods =====
+
+ // { ----- begin file methods -----
+ NS_IMETHOD OpenOldFile(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath, mdb_bool inFrozen,
+ nsIMdbFile** acqFile) override;
+ // Choose some subclass of nsIMdbFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be open and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+
+ NS_IMETHOD CreateNewFile(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath,
+ nsIMdbFile** acqFile) override;
+ // Choose some subclass of nsIMdbFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be created and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+ // } ----- end file methods -----
+
+ // { ----- begin env methods -----
+ NS_IMETHOD MakeEnv(nsIMdbHeap* ioHeap,
+ nsIMdbEnv** acqEnv) override; // new env
+ // ioHeap can be nil, causing a MakeHeap() style heap instance to be used
+ // } ----- end env methods -----
+
+ // { ----- begin heap methods -----
+ NS_IMETHOD MakeHeap(nsIMdbEnv* ev,
+ nsIMdbHeap** acqHeap) override; // new heap
+ // } ----- end heap methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD MakeRow(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbRow** acqRow) override; // new row
+ // ioHeap can be nil, causing the heap associated with ev to be used
+ // } ----- end row methods -----
+
+ // { ----- begin port methods -----
+ NS_IMETHOD CanOpenFilePort(
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpen, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) override; // informal file format description
+
+ NS_IMETHOD OpenFilePort(
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for readonly import
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb)
+ override; // acquire thumb for incremental port open
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then call nsIMdbFactory::ThumbToOpenPort() to get the port instance.
+
+ NS_IMETHOD
+ ThumbToOpenPort( // redeeming a completed thumb from OpenFilePort()
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFilePort() with done status
+ nsIMdbPort** acqPort) override; // acquire new port object
+ // } ----- end port methods -----
+
+ // { ----- begin store methods -----
+ NS_IMETHOD CanOpenFileStore(
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to investigate
+ // const mdbYarn* inFirst512Bytes,
+ nsIMdbFile* ioFile, // db abstract file interface
+ mdb_bool* outCanOpenAsStore, // whether OpenFileStore() might succeed
+ mdb_bool* outCanOpenAsPort, // whether OpenFilePort() might succeed
+ mdbYarn* outFormatVersion) override; // informal file format description
+
+ NS_IMETHOD OpenFileStore( // open an existing database
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // the file to open for general db usage
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbThumb** acqThumb)
+ override; // acquire thumb for incremental store open
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then call nsIMdbFactory::ThumbToOpenStore() to get the store instance.
+
+ NS_IMETHOD
+ ThumbToOpenStore( // redeem completed thumb from OpenFileStore()
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb* ioThumb, // thumb from OpenFileStore() with done status
+ nsIMdbStore** acqStore) override; // acquire new db store object
+
+ NS_IMETHOD CreateNewFileStore( // create a new db with minimal content
+ nsIMdbEnv* ev, // context
+ nsIMdbHeap* ioHeap, // can be nil to cause ev's heap attribute to be used
+ // const char* inFilePath, // name of file which should not yet exist
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy, // runtime policies for using db
+ nsIMdbStore** acqStore) override; // acquire new db store object
+
+ // } ----- end store methods -----
+
+ // } ===== end nsIMdbFactory methods =====
+
+ public: // morkYarn construction & destruction
+ morkFactory(); // uses orkinHeap
+ explicit morkFactory(nsIMdbHeap* ioHeap); // caller supplied heap
+ morkFactory(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap);
+ void CloseFactory(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // morkNode memory management operators
+ void* operator new(size_t inSize) noexcept(true) {
+ return ::operator new(inSize);
+ }
+
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkNode::MakeNew(inSize, ioHeap, ev);
+ }
+
+ private: // copying is not allowed
+ morkFactory(const morkFactory& other);
+ morkFactory& operator=(const morkFactory& other);
+ virtual ~morkFactory(); // assert that CloseFactory() executed earlier
+
+ public: // dynamic type identification
+ mork_bool IsFactory() const {
+ return IsNode() && mNode_Derived == morkDerived_kFactory;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // other factory methods
+ void NonFactoryTypeError(morkEnv* ev);
+ morkEnv* GetInternalFactoryEnv(nsresult* outErr);
+ mork_bool CanOpenMorkTextFile(morkEnv* ev, nsIMdbFile* ioFile);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakFactory(morkFactory* me, morkEnv* ev,
+ morkFactory** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongFactory(morkFactory* me, morkEnv* ev,
+ morkFactory** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKFACTORY_ */
diff --git a/comm/mailnews/db/mork/morkFile.cpp b/comm/mailnews/db/mork/morkFile.cpp
new file mode 100644
index 0000000000..b7b7848cc2
--- /dev/null
+++ b/comm/mailnews/db/mork/morkFile.cpp
@@ -0,0 +1,738 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKFILE_
+# include "morkFile.h"
+#endif
+
+#ifdef MORK_WIN
+# include "io.h"
+# include <windows.h>
+#endif
+
+#include "mozilla/Unused.h"
+#include "nsString.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkFile::CloseMorkNode(
+ morkEnv* ev) // CloseFile() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseFile(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkFile::~morkFile() // assert CloseFile() executed earlier
+{
+ MORK_ASSERT(mFile_Frozen == 0);
+ MORK_ASSERT(mFile_DoTrace == 0);
+ MORK_ASSERT(mFile_IoOpen == 0);
+ MORK_ASSERT(mFile_Active == 0);
+}
+
+/*public non-poly*/
+morkFile::morkFile(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mFile_Frozen(0),
+ mFile_DoTrace(0),
+ mFile_IoOpen(0),
+ mFile_Active(0)
+
+ ,
+ mFile_SlotHeap(0),
+ mFile_Name(0),
+ mFile_Thief(0) {
+ if (ev->Good()) {
+ if (ioSlotHeap) {
+ nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mFile_SlotHeap);
+ if (ev->Good()) mNode_Derived = morkDerived_kFile;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkFile, morkObject, nsIMdbFile)
+/*public non-poly*/ void morkFile::CloseFile(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mFile_Frozen = 0;
+ mFile_DoTrace = 0;
+ mFile_IoOpen = 0;
+ mFile_Active = 0;
+
+ if (mFile_Name) this->SetFileName(ev, nullptr);
+
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mFile_SlotHeap);
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mFile_Thief);
+
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ morkFile* morkFile::OpenOldFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath,
+ mork_bool inFrozen)
+// Choose some subclass of morkFile to instantiate, in order to read
+// (and write if not frozen) the file known by inFilePath. The file
+// returned should be open and ready for use, and presumably positioned
+// at the first byte position of the file. The exact manner in which
+// files must be opened is considered a subclass specific detail, and
+// other portions or Mork source code don't want to know how it's done.
+{
+ return morkStdioFile::OpenOldStdioFile(ev, ioHeap, inFilePath, inFrozen);
+}
+
+/*static*/ morkFile* morkFile::CreateNewFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath)
+// Choose some subclass of morkFile to instantiate, in order to read
+// (and write if not frozen) the file known by inFilePath. The file
+// returned should be created and ready for use, and presumably positioned
+// at the first byte position of the file. The exact manner in which
+// files must be opened is considered a subclass specific detail, and
+// other portions or Mork source code don't want to know how it's done.
+{
+ return morkStdioFile::CreateNewStdioFile(ev, ioHeap, inFilePath);
+}
+
+void morkFile::NewMissingIoError(morkEnv* ev) const {
+ ev->NewError("file missing io");
+}
+
+/*static*/ void morkFile::NonFileTypeError(morkEnv* ev) {
+ ev->NewError("non morkFile");
+}
+
+/*static*/ void morkFile::NilSlotHeapError(morkEnv* ev) {
+ ev->NewError("nil mFile_SlotHeap");
+}
+
+/*static*/ void morkFile::NilFileNameError(morkEnv* ev) {
+ ev->NewError("nil mFile_Name");
+}
+
+void morkFile::SetThief(morkEnv* ev, nsIMdbFile* ioThief) {
+ nsIMdbFile_SlotStrongFile(ioThief, ev, &mFile_Thief);
+}
+
+void morkFile::SetFileName(morkEnv* ev,
+ const PathChar* inName) // inName can be nil
+{
+ nsIMdbHeap* heap = mFile_SlotHeap;
+ if (heap) {
+ PathChar* name = mFile_Name;
+ if (name) {
+ mFile_Name = 0;
+ ev->FreeString(heap, name);
+ }
+ if (ev->Good() && inName) mFile_Name = ev->CopyString(heap, inName);
+ } else
+ this->NilSlotHeapError(ev);
+}
+
+void morkFile::NewFileDownError(morkEnv* ev) const
+// call NewFileDownError() when either IsOpenAndActiveFile()
+// is false, or when IsOpenActiveAndMutableFile() is false.
+{
+ if (this->IsOpenNode()) {
+ if (this->FileActive()) {
+ if (this->FileFrozen()) {
+ ev->NewError("file frozen");
+ } else
+ ev->NewError("unknown file problem");
+ } else
+ ev->NewError("file not active");
+ } else
+ ev->NewError("file not open");
+}
+
+void morkFile::NewFileErrnoError(morkEnv* ev) const
+// call NewFileErrnoError() to convert std C errno into AB fault
+{
+ const char* errnoString = strerror(errno);
+ ev->NewError(errnoString); // maybe pass value of strerror() instead
+}
+
+// ````` ````` ````` ````` newlines ````` ````` ````` `````
+
+#if defined(MORK_MAC)
+static const char morkFile_kNewlines[] =
+ "\015\015\015\015\015\015\015\015\015\015\015\015\015\015\015\015";
+# define morkFile_kNewlinesCount 16
+#else
+# if defined(MORK_WIN)
+static const char morkFile_kNewlines[] =
+ "\015\012\015\012\015\012\015\012\015\012\015\012\015\012\015\012";
+# define morkFile_kNewlinesCount 8
+# else
+# ifdef MORK_UNIX
+static const char morkFile_kNewlines[] =
+ "\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012";
+# define morkFile_kNewlinesCount 16
+# endif /* MORK_UNIX */
+# endif /* MORK_WIN */
+#endif /* MORK_MAC */
+
+mork_size morkFile::WriteNewlines(morkEnv* ev, mork_count inNewlines)
+// WriteNewlines() returns the number of bytes written.
+{
+ mork_size outSize = 0;
+ while (inNewlines && ev->Good()) // more newlines to write?
+ {
+ mork_u4 quantum = inNewlines;
+ if (quantum > morkFile_kNewlinesCount) quantum = morkFile_kNewlinesCount;
+
+ mork_size quantumSize = quantum * mork_kNewlineSize;
+ mdb_size bytesWritten;
+ this->Write(ev->AsMdbEnv(), morkFile_kNewlines, quantumSize, &bytesWritten);
+ outSize += quantumSize;
+ inNewlines -= quantum;
+ }
+ return outSize;
+}
+
+NS_IMETHODIMP
+morkFile::Eof(nsIMdbEnv* mev, mdb_pos* outPos) {
+ nsresult outErr = NS_OK;
+ mdb_pos pos = -1;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ pos = Length(ev);
+ outErr = ev->AsErr();
+ if (outPos) *outPos = pos;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkFile::Get(nsIMdbEnv* mev, void* outBuf, mdb_size inSize, mdb_pos inPos,
+ mdb_size* outActualSize) {
+ nsresult rv = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mdb_pos outPos;
+ Seek(mev, inPos, &outPos);
+ if (ev->Good()) rv = Read(mev, outBuf, inSize, outActualSize);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+morkFile::Put(nsIMdbEnv* mev, const void* inBuf, mdb_size inSize, mdb_pos inPos,
+ mdb_size* outActualSize) {
+ nsresult outErr = NS_OK;
+ *outActualSize = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mdb_pos outPos;
+
+ Seek(mev, inPos, &outPos);
+ if (ev->Good()) Write(mev, inBuf, inSize, outActualSize);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+// { ----- begin path methods -----
+NS_IMETHODIMP
+morkFile::Path(nsIMdbEnv* mev, mdbYarn* outFilePath) {
+ nsresult outErr = NS_OK;
+ if (outFilePath) outFilePath->mYarn_Fill = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ ev->StringToYarn(GetFileNameString(), outFilePath);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+// } ----- end path methods -----
+
+// { ----- begin replacement methods -----
+
+NS_IMETHODIMP
+morkFile::Thief(nsIMdbEnv* mev, nsIMdbFile** acqThief) {
+ nsresult outErr = NS_OK;
+ nsIMdbFile* outThief = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ outThief = GetThief();
+ NS_IF_ADDREF(outThief);
+ outErr = ev->AsErr();
+ }
+ if (acqThief) *acqThief = outThief;
+ return outErr;
+}
+
+// } ----- end replacement methods -----
+
+// { ----- begin versioning methods -----
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkStdioFile::CloseMorkNode(
+ morkEnv* ev) // CloseStdioFile() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseStdioFile(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkStdioFile::~morkStdioFile() // assert CloseStdioFile() executed earlier
+{
+ if (mStdioFile_File) CloseStdioFile(mMorkEnv);
+ MORK_ASSERT(mStdioFile_File == 0);
+}
+
+/*public non-poly*/ void morkStdioFile::CloseStdioFile(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ if (mStdioFile_File && this->FileActive() && this->FileIoOpen()) {
+ this->CloseStdio(ev);
+ }
+
+ mStdioFile_File = 0;
+
+ this->CloseFile(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// compatible with the morkFile::MakeFile() entry point
+
+/*static*/ morkStdioFile* morkStdioFile::OpenOldStdioFile(
+ morkEnv* ev, nsIMdbHeap* ioHeap, const PathChar* inFilePath,
+ mork_bool inFrozen) {
+ morkStdioFile* outFile = 0;
+ if (ioHeap && inFilePath) {
+ const char* mode = (inFrozen) ? "rb" : "rb+";
+ outFile = new (*ioHeap, ev)
+ morkStdioFile(ev, morkUsage::kHeap, ioHeap, ioHeap, inFilePath, mode);
+
+ if (outFile) {
+ outFile->SetFileFrozen(inFrozen);
+ }
+ } else
+ ev->NilPointerError();
+
+ return outFile;
+}
+
+/*static*/ morkStdioFile* morkStdioFile::CreateNewStdioFile(
+ morkEnv* ev, nsIMdbHeap* ioHeap, const PathChar* inFilePath) {
+ morkStdioFile* outFile = 0;
+ if (ioHeap && inFilePath) {
+ const char* mode = "wb+";
+ outFile = new (*ioHeap, ev)
+ morkStdioFile(ev, morkUsage::kHeap, ioHeap, ioHeap, inFilePath, mode);
+ } else
+ ev->NilPointerError();
+
+ return outFile;
+}
+
+NS_IMETHODIMP
+morkStdioFile::BecomeTrunk(nsIMdbEnv* ev)
+// If this file is a file version branch created by calling AcquireBud(),
+// BecomeTrunk() causes this file's content to replace the original
+// file's content, typically by assuming the original file's identity.
+{
+ return Flush(ev);
+}
+
+NS_IMETHODIMP
+morkStdioFile::AcquireBud(nsIMdbEnv* mdbev, nsIMdbHeap* ioHeap,
+ nsIMdbFile** acquiredFile)
+// AcquireBud() starts a new "branch" version of the file, empty of content,
+// so that a new version of the file can be written. This new file
+// can later be told to BecomeTrunk() the original file, so the branch
+// created by budding the file will replace the original file. Some
+// file subclasses might initially take the unsafe but expedient
+// approach of simply truncating this file down to zero length, and
+// then returning the same morkFile pointer as this, with an extra
+// reference count increment. Note that the caller of AcquireBud() is
+// expected to eventually call CutStrongRef() on the returned file
+// in order to release the strong reference. High quality versions
+// of morkFile subclasses will create entirely new files which later
+// are renamed to become the old file, so that better transactional
+// behavior is exhibited by the file, so crashes protect old files.
+// Note that AcquireBud() is an illegal operation on readonly files.
+{
+ NS_ENSURE_ARG(acquiredFile);
+ MORK_USED_1(ioHeap);
+ nsresult rv = NS_OK;
+ morkFile* outFile = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+
+ if (this->IsOpenAndActiveFile()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ // #ifdef MORK_WIN
+ // truncate(file, /*eof*/ 0);
+ // #else /*MORK_WIN*/
+ PathChar* name = mFile_Name;
+ if (name) {
+ if (MORK_FILECLOSE(file) >= 0) {
+ this->SetFileActive(morkBool_kFalse);
+ this->SetFileIoOpen(morkBool_kFalse);
+ mStdioFile_File = 0;
+
+ file = MORK_FILEOPEN(
+ name, "wb+"); // open for write, discarding old content
+ if (file) {
+ mStdioFile_File = file;
+ this->SetFileActive(morkBool_kTrue);
+ this->SetFileIoOpen(morkBool_kTrue);
+ this->SetFileFrozen(morkBool_kFalse);
+ } else
+ this->new_stdio_file_fault(ev);
+ } else
+ this->new_stdio_file_fault(ev);
+ } else
+ this->NilFileNameError(ev);
+
+ // #endif /*MORK_WIN*/
+
+ if (ev->Good() && this->AddStrongRef(ev->AsMdbEnv())) {
+ outFile = this;
+ AddRef();
+ }
+ } else if (mFile_Thief) {
+ rv = mFile_Thief->AcquireBud(ev->AsMdbEnv(), ioHeap, acquiredFile);
+ } else
+ this->NewMissingIoError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ *acquiredFile = outFile;
+ return rv;
+}
+
+mork_pos morkStdioFile::Length(morkEnv* ev) const {
+ mork_pos outPos = 0;
+
+ if (this->IsOpenAndActiveFile()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ long start = MORK_FILETELL(file);
+ if (start >= 0) {
+ long fore = MORK_FILESEEK(file, 0, SEEK_END);
+ if (fore >= 0) {
+ long eof = MORK_FILETELL(file);
+ if (eof >= 0) {
+ long back = MORK_FILESEEK(file, start, SEEK_SET);
+ if (back >= 0)
+ outPos = eof;
+ else
+ this->new_stdio_file_fault(ev);
+ } else
+ this->new_stdio_file_fault(ev);
+ } else
+ this->new_stdio_file_fault(ev);
+ } else
+ this->new_stdio_file_fault(ev);
+ } else if (mFile_Thief)
+ mFile_Thief->Eof(ev->AsMdbEnv(), &outPos);
+ else
+ this->NewMissingIoError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ return outPos;
+}
+
+NS_IMETHODIMP
+morkStdioFile::Tell(nsIMdbEnv* ev, mork_pos* outPos) const {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG(outPos);
+ morkEnv* mev = morkEnv::FromMdbEnv(ev);
+ if (this->IsOpenAndActiveFile()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ long where = MORK_FILETELL(file);
+ if (where >= 0)
+ *outPos = where;
+ else
+ this->new_stdio_file_fault(mev);
+ } else if (mFile_Thief)
+ mFile_Thief->Tell(ev, outPos);
+ else
+ this->NewMissingIoError(mev);
+ } else
+ this->NewFileDownError(mev);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStdioFile::Read(nsIMdbEnv* ev, void* outBuf, mork_size inSize,
+ mork_num* outCount) {
+ nsresult rv = NS_OK;
+ morkEnv* mev = morkEnv::FromMdbEnv(ev);
+ if (this->IsOpenAndActiveFile()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ long count = (long)MORK_FILEREAD(outBuf, inSize, file);
+ if (count >= 0) {
+ *outCount = (mork_num)count;
+ } else
+ this->new_stdio_file_fault(mev);
+ } else if (mFile_Thief)
+ mFile_Thief->Read(ev, outBuf, inSize, outCount);
+ else
+ this->NewMissingIoError(mev);
+ } else
+ this->NewFileDownError(mev);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStdioFile::Seek(nsIMdbEnv* mdbev, mork_pos inPos, mork_pos* aOutPos) {
+ mork_pos outPos = 0;
+ nsresult rv = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+
+ if (this->IsOpenOrClosingNode() && this->FileActive()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ long where = MORK_FILESEEK(file, inPos, SEEK_SET);
+ if (where >= 0)
+ outPos = inPos;
+ else
+ this->new_stdio_file_fault(ev);
+ } else if (mFile_Thief)
+ mFile_Thief->Seek(mdbev, inPos, aOutPos);
+ else
+ this->NewMissingIoError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ *aOutPos = outPos;
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStdioFile::Write(nsIMdbEnv* mdbev, const void* inBuf, mork_size inSize,
+ mork_size* aOutSize) {
+ mork_num outCount = 0;
+ nsresult rv = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+ if (this->IsOpenActiveAndMutableFile()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ mozilla::Unused << fwrite(inBuf, 1, inSize, file);
+ if (!ferror(file))
+ outCount = inSize;
+ else
+ this->new_stdio_file_fault(ev);
+ } else if (mFile_Thief)
+ mFile_Thief->Write(mdbev, inBuf, inSize, &outCount);
+ else
+ this->NewMissingIoError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ *aOutSize = outCount;
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStdioFile::Flush(nsIMdbEnv* mdbev) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+ if (this->IsOpenOrClosingNode() && this->FileActive()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (file) {
+ MORK_FILEFLUSH(file);
+
+ } else if (mFile_Thief)
+ mFile_Thief->Flush(mdbev);
+ else
+ this->NewMissingIoError(ev);
+ } else
+ this->NewFileDownError(ev);
+ return NS_OK;
+}
+
+// ````` ````` ````` ````` ````` ````` ````` `````
+// protected: // protected non-poly morkStdioFile methods
+
+void morkStdioFile::new_stdio_file_fault(morkEnv* ev) const {
+ FILE* file = (FILE*)mStdioFile_File;
+
+ int copyErrno = errno; // facilitate seeing error in debugger
+
+ // bunch of stuff not ported here
+ if (!copyErrno && file) {
+ copyErrno = ferror(file);
+ errno = copyErrno;
+ }
+
+ this->NewFileErrnoError(ev);
+}
+
+// ````` ````` ````` ````` ````` ````` ````` `````
+// public: // public non-poly morkStdioFile methods
+
+/*public non-poly*/
+morkStdioFile::morkStdioFile(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkFile(ev, inUsage, ioHeap, ioSlotHeap), mStdioFile_File(0) {
+ if (ev->Good()) mNode_Derived = morkDerived_kStdioFile;
+}
+
+morkStdioFile::morkStdioFile(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ const PathChar* inName, const char* inMode)
+ // calls OpenStdio() after construction
+ : morkFile(ev, inUsage, ioHeap, ioSlotHeap), mStdioFile_File(0) {
+ if (ev->Good()) this->OpenStdio(ev, inName, inMode);
+}
+
+morkStdioFile::morkStdioFile(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ void* ioFile, const PathChar* inName,
+ mork_bool inFrozen)
+ // calls UseStdio() after construction
+ : morkFile(ev, inUsage, ioHeap, ioSlotHeap), mStdioFile_File(0) {
+ if (ev->Good()) this->UseStdio(ev, ioFile, inName, inFrozen);
+}
+
+void morkStdioFile::OpenStdio(morkEnv* ev, const PathChar* inName,
+ const char* inMode)
+// Open a new FILE with name inName, using mode flags from inMode.
+{
+ if (ev->Good()) {
+ if (!inMode) inMode = "";
+
+ mork_bool frozen = (*inMode == 'r'); // cursory attempt to note readonly
+
+ if (this->IsOpenNode()) {
+ if (!this->FileActive()) {
+ this->SetFileIoOpen(morkBool_kFalse);
+ if (inName && *inName) {
+ this->SetFileName(ev, inName);
+ if (ev->Good()) {
+ FILE* file = MORK_FILEOPEN(inName, inMode);
+ if (file) {
+ mStdioFile_File = file;
+ this->SetFileActive(morkBool_kTrue);
+ this->SetFileIoOpen(morkBool_kTrue);
+ this->SetFileFrozen(frozen);
+ } else
+ this->new_stdio_file_fault(ev);
+ }
+ } else
+ ev->NewError("no file name");
+ } else
+ ev->NewError("file already active");
+ } else
+ this->NewFileDownError(ev);
+ }
+}
+
+void morkStdioFile::UseStdio(morkEnv* ev, void* ioFile, const PathChar* inName,
+ mork_bool inFrozen)
+// Use an existing file, like stdin/stdout/stderr, which should not
+// have the io stream closed when the file is closed. The ioFile
+// parameter must actually be of type FILE (but we don't want to make
+// this header file include the stdio.h header file).
+{
+ if (ev->Good()) {
+ if (this->IsOpenNode()) {
+ if (!this->FileActive()) {
+ if (ioFile) {
+ this->SetFileIoOpen(morkBool_kFalse);
+ this->SetFileName(ev, inName);
+ if (ev->Good()) {
+ mStdioFile_File = ioFile;
+ this->SetFileActive(morkBool_kTrue);
+ this->SetFileFrozen(inFrozen);
+ }
+ } else
+ ev->NilPointerError();
+ } else
+ ev->NewError("file already active");
+ } else
+ this->NewFileDownError(ev);
+ }
+}
+
+void morkStdioFile::CloseStdio(morkEnv* ev)
+// Close the stream io if both and FileActive() and FileIoOpen(), but
+// this does not close this instances (like CloseStdioFile() does).
+// If stream io was made active by means of calling UseStdio(),
+// then this method does little beyond marking the stream inactive
+// because FileIoOpen() is false.
+{
+ if (mStdioFile_File && this->FileActive() && this->FileIoOpen()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (MORK_FILECLOSE(file) < 0) this->new_stdio_file_fault(ev);
+
+ mStdioFile_File = 0;
+ this->SetFileActive(morkBool_kFalse);
+ this->SetFileIoOpen(morkBool_kFalse);
+ }
+}
+
+NS_IMETHODIMP
+morkStdioFile::Steal(nsIMdbEnv* ev, nsIMdbFile* ioThief)
+// If this file is a file version branch created by calling AcquireBud(),
+// BecomeTrunk() causes this file's content to replace the original
+// file's content, typically by assuming the original file's identity.
+{
+ morkEnv* mev = morkEnv::FromMdbEnv(ev);
+ if (mStdioFile_File && FileActive() && FileIoOpen()) {
+ FILE* file = (FILE*)mStdioFile_File;
+ if (MORK_FILECLOSE(file) < 0) new_stdio_file_fault(mev);
+
+ mStdioFile_File = 0;
+ }
+ SetThief(mev, ioThief);
+ return NS_OK;
+}
+
+#if defined(MORK_WIN)
+
+void mork_fileflush(FILE* file) { fflush(file); }
+
+#endif /*MORK_WIN*/
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkFile.h b/comm/mailnews/db/mork/morkFile.h
new file mode 100644
index 0000000000..1a2933643f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkFile.h
@@ -0,0 +1,360 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORKFILE_
+#define _MORKFILE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#include "mozilla/Path.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*=============================================================================
+ * morkFile: abstract file interface
+ */
+
+#define morkDerived_kFile /*i*/ 0x4669 /* ascii 'Fi' */
+
+class morkFile /*d*/ : public morkObject,
+ public nsIMdbFile { /* ````` simple file API ````` */
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // public: // slots inherited from morkObject (meant to inform only)
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected morkFile members (similar to public domain IronDoc)
+ virtual ~morkFile(); // assert that CloseFile() executed earlier
+
+ mork_u1 mFile_Frozen; // 'F' => file allows only read access
+ mork_u1 mFile_DoTrace; // 'T' trace if ev->DoTrace()
+ mork_u1 mFile_IoOpen; // 'O' => io stream is open (& needs a close)
+ mork_u1 mFile_Active; // 'A' => file is active and usable
+
+ nsIMdbHeap* mFile_SlotHeap; // heap for Name and other allocated slots
+ PathChar* mFile_Name; // can be nil if SetFileName() is never called
+ // mFile_Name convention: managed with morkEnv::CopyString()/FreeString()
+
+ nsIMdbFile* mFile_Thief; // from a call to orkinFile::Steal()
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ NS_DECL_ISUPPORTS_INHERITED
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseFile() only if open
+
+ public: // morkFile construction & destruction
+ morkFile(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseFile(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkFile(const morkFile& other);
+ morkFile& operator=(const morkFile& other);
+
+ public: // dynamic type identification
+ mork_bool IsFile() const {
+ return IsNode() && mNode_Derived == morkDerived_kFile;
+ }
+ // } ===== end morkNode methods =====
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public static standard file creation entry point
+ static morkFile* OpenOldFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath, mork_bool inFrozen);
+ // Choose some subclass of morkFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be open and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+
+ static morkFile* CreateNewFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath);
+ // Choose some subclass of morkFile to instantiate, in order to read
+ // (and write if not frozen) the file known by inFilePath. The file
+ // returned should be created and ready for use, and presumably positioned
+ // at the first byte position of the file. The exact manner in which
+ // files must be opened is considered a subclass specific detail, and
+ // other portions or Mork source code don't want to know how it's done.
+
+ public: // non-poly morkFile methods
+ mork_bool FileFrozen() const { return mFile_Frozen == 'F'; }
+ mork_bool FileDoTrace() const { return mFile_DoTrace == 'T'; }
+ mork_bool FileIoOpen() const { return mFile_IoOpen == 'O'; }
+ mork_bool FileActive() const { return mFile_Active == 'A'; }
+
+ void SetFileFrozen(mork_bool b) { mFile_Frozen = (mork_u1)((b) ? 'F' : 0); }
+ void SetFileDoTrace(mork_bool b) { mFile_DoTrace = (mork_u1)((b) ? 'T' : 0); }
+ void SetFileIoOpen(mork_bool b) { mFile_IoOpen = (mork_u1)((b) ? 'O' : 0); }
+ void SetFileActive(mork_bool b) { mFile_Active = (mork_u1)((b) ? 'A' : 0); }
+
+ mork_bool IsOpenActiveAndMutableFile() const {
+ return (IsOpenNode() && FileActive() && !FileFrozen());
+ }
+ // call IsOpenActiveAndMutableFile() before writing a file
+
+ mork_bool IsOpenAndActiveFile() const {
+ return (this->IsOpenNode() && this->FileActive());
+ }
+ // call IsOpenAndActiveFile() before using a file
+
+ nsIMdbFile* GetThief() const { return mFile_Thief; }
+ void SetThief(morkEnv* ev, nsIMdbFile* ioThief); // ioThief can be nil
+
+ const PathChar* GetFileNameString() const { return mFile_Name; }
+ void SetFileName(morkEnv* ev, const PathChar* inName); // inName can be nil
+ static void NilSlotHeapError(morkEnv* ev);
+ static void NilFileNameError(morkEnv* ev);
+ static void NonFileTypeError(morkEnv* ev);
+
+ void NewMissingIoError(morkEnv* ev) const;
+
+ void NewFileDownError(morkEnv* ev) const;
+ // call NewFileDownError() when either IsOpenAndActiveFile()
+ // is false, or when IsOpenActiveAndMutableFile() is false.
+
+ void NewFileErrnoError(morkEnv* ev) const;
+ // call NewFileErrnoError() to convert std C errno into AB fault
+
+ mork_size WriteNewlines(morkEnv* ev, mork_count inNewlines);
+ // WriteNewlines() returns the number of bytes written.
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakFile(morkFile* me, morkEnv* ev, morkFile** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongFile(morkFile* me, morkEnv* ev, morkFile** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ public:
+ virtual mork_pos Length(morkEnv* ev) const = 0; // eof
+ // nsIMdbFile methods
+ NS_IMETHOD Tell(nsIMdbEnv* ev, mdb_pos* outPos) const override = 0;
+ NS_IMETHOD Seek(nsIMdbEnv* ev, mdb_pos inPos, mdb_pos* outPos) override = 0;
+ NS_IMETHOD Eof(nsIMdbEnv* ev, mdb_pos* outPos) override;
+ // } ----- end pos methods -----
+
+ // { ----- begin read methods -----
+ NS_IMETHOD Read(nsIMdbEnv* ev, void* outBuf, mdb_size inSize,
+ mdb_size* outActualSize) override = 0;
+ NS_IMETHOD Get(nsIMdbEnv* ev, void* outBuf, mdb_size inSize, mdb_pos inPos,
+ mdb_size* outActualSize) override;
+ // } ----- end read methods -----
+
+ // { ----- begin write methods -----
+ NS_IMETHOD Write(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ mdb_size* outActualSize) override = 0;
+ NS_IMETHOD Put(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ mdb_pos inPos, mdb_size* outActualSize) override;
+ NS_IMETHOD Flush(nsIMdbEnv* ev) override = 0;
+ // } ----- end attribute methods -----
+
+ // { ----- begin path methods -----
+ NS_IMETHOD Path(nsIMdbEnv* ev, mdbYarn* outFilePath) override;
+ // } ----- end path methods -----
+
+ // { ----- begin replacement methods -----
+ NS_IMETHOD Steal(nsIMdbEnv* ev, nsIMdbFile* ioThief) override = 0;
+ NS_IMETHOD Thief(nsIMdbEnv* ev, nsIMdbFile** acqThief) override;
+ // } ----- end replacement methods -----
+
+ // { ----- begin versioning methods -----
+ NS_IMETHOD BecomeTrunk(nsIMdbEnv* ev) override = 0;
+
+ NS_IMETHOD AcquireBud(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbFile** acqBud) override = 0;
+ // } ----- end versioning methods -----
+
+ // } ===== end nsIMdbFile methods =====
+};
+
+/*=============================================================================
+ * morkStdioFile: concrete file using standard C file io
+ */
+
+#define morkDerived_kStdioFile /*i*/ 0x7346 /* ascii 'sF' */
+
+class morkStdioFile /*d*/ : public morkFile { /* `` copied from IronDoc `` */
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected morkStdioFile members
+ void* mStdioFile_File;
+ // actually type FILE*, but using opaque void* type
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseStdioFile() only if open
+ virtual ~morkStdioFile(); // assert that CloseStdioFile() executed earlier
+
+ public: // morkStdioFile construction & destruction
+ morkStdioFile(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseStdioFile(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkStdioFile(const morkStdioFile& other);
+ morkStdioFile& operator=(const morkStdioFile& other);
+
+ public: // dynamic type identification
+ mork_bool IsStdioFile() const {
+ return IsNode() && mNode_Derived == morkDerived_kStdioFile;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonStdioFileTypeError(morkEnv* ev);
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // compatible with the morkFile::OpenOldFile() entry point
+ static morkStdioFile* OpenOldStdioFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath,
+ mork_bool inFrozen);
+
+ static morkStdioFile* CreateNewStdioFile(morkEnv* ev, nsIMdbHeap* ioHeap,
+ const PathChar* inFilePath);
+
+ virtual mork_pos Length(morkEnv* ev) const override; // eof
+
+ NS_IMETHOD Tell(nsIMdbEnv* ev, mdb_pos* outPos) const override;
+ NS_IMETHOD Seek(nsIMdbEnv* ev, mdb_pos inPos, mdb_pos* outPos) override;
+ // NS_IMETHOD Eof(nsIMdbEnv* ev, mdb_pos* outPos);
+ // } ----- end pos methods -----
+
+ // { ----- begin read methods -----
+ NS_IMETHOD Read(nsIMdbEnv* ev, void* outBuf, mdb_size inSize,
+ mdb_size* outActualSize) override;
+
+ // { ----- begin write methods -----
+ NS_IMETHOD Write(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ mdb_size* outActualSize) override;
+ // NS_IMETHOD Put(nsIMdbEnv* ev, const void* inBuf, mdb_size inSize,
+ // mdb_pos inPos, mdb_size* outActualSize);
+ NS_IMETHOD Flush(nsIMdbEnv* ev) override;
+ // } ----- end attribute methods -----
+
+ NS_IMETHOD Steal(nsIMdbEnv* ev, nsIMdbFile* ioThief) override;
+
+ // { ----- begin versioning methods -----
+ NS_IMETHOD BecomeTrunk(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD AcquireBud(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbFile** acqBud) override;
+ // } ----- end versioning methods -----
+
+ // } ===== end nsIMdbFile methods =====
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected non-poly morkStdioFile methods
+ void new_stdio_file_fault(morkEnv* ev) const;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkStdioFile methods
+ morkStdioFile(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, const PathChar* inName,
+ const char* inMode);
+ // calls OpenStdio() after construction
+
+ morkStdioFile(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, void* ioFile, const PathChar* inName,
+ mork_bool inFrozen);
+ // calls UseStdio() after construction
+
+ void OpenStdio(morkEnv* ev, const PathChar* inName, const char* inMode);
+ // Open a new FILE with name inName, using mode flags from inMode.
+
+ void UseStdio(morkEnv* ev, void* ioFile, const PathChar* inName,
+ mork_bool inFrozen);
+ // Use an existing file, like stdin/stdout/stderr, which should not
+ // have the io stream closed when the file is closed. The ioFile
+ // parameter must actually be of type FILE (but we don't want to make
+ // this header file include the stdio.h header file).
+
+ void CloseStdio(morkEnv* ev);
+ // Close the stream io if both and FileActive() and FileIoOpen(), but
+ // this does not close this instances (like CloseStdioFile() does).
+ // If stream io was made active by means of calling UseStdio(),
+ // then this method does little beyond marking the stream inactive
+ // because FileIoOpen() is false.
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakStdioFile(morkStdioFile* me, morkEnv* ev,
+ morkStdioFile** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongStdioFile(morkStdioFile* me, morkEnv* ev,
+ morkStdioFile** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKFILE_ */
diff --git a/comm/mailnews/db/mork/morkHandle.cpp b/comm/mailnews/db/mork/morkHandle.cpp
new file mode 100644
index 0000000000..e838f8b997
--- /dev/null
+++ b/comm/mailnews/db/mork/morkHandle.cpp
@@ -0,0 +1,357 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKFACTORY_
+# include "morkFactory.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKHANDLE_
+# include "morkHandle.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkHandle::CloseMorkNode(
+ morkEnv* ev) // CloseHandle() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseHandle(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkHandle::~morkHandle() // assert CloseHandle() executed earlier
+{
+ MORK_ASSERT(mHandle_Env == 0);
+ MORK_ASSERT(mHandle_Face == 0);
+ MORK_ASSERT(mHandle_Object == 0);
+ MORK_ASSERT(mHandle_Magic == 0);
+ MORK_ASSERT(mHandle_Tag == morkHandle_kTag); // should still have correct tag
+}
+
+/*public non-poly*/
+morkHandle::morkHandle(
+ morkEnv* ev, // note morkUsage is always morkUsage_kPool
+ morkHandleFace* ioFace, // must not be nil, cookie for this handle
+ morkObject* ioObject, // must not be nil, the object for this handle
+ mork_magic inMagic) // magic sig to denote specific subclass
+ : morkNode(ev, morkUsage::kPool, (nsIMdbHeap*)0L),
+ mHandle_Tag(0),
+ mHandle_Env(ev),
+ mHandle_Face(ioFace),
+ mHandle_Object(0),
+ mHandle_Magic(0) {
+ if (ioFace && ioObject) {
+ if (ev->Good()) {
+ mHandle_Tag = morkHandle_kTag;
+ morkObject::SlotStrongObject(ioObject, ev, &mHandle_Object);
+ morkHandle::SlotWeakHandle(this, ev, &ioObject->mObject_Handle);
+ if (ev->Good()) {
+ mHandle_Magic = inMagic;
+ mNode_Derived = morkDerived_kHandle;
+ }
+ } else
+ ev->CantMakeWhenBadError();
+ } else
+ ev->NilPointerError();
+}
+
+/*public non-poly*/ void morkHandle::CloseHandle(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkObject* obj = mHandle_Object;
+ mork_bool objDidRefSelf = (obj && obj->mObject_Handle == this);
+ if (objDidRefSelf) obj->mObject_Handle = 0; // drop the reference
+
+ morkObject::SlotStrongObject((morkObject*)0, ev, &mHandle_Object);
+ mHandle_Magic = 0;
+ // note mHandle_Tag MUST stay morkHandle_kTag for morkNode::ZapOld()
+ this->MarkShut();
+
+ if (objDidRefSelf)
+ this->CutWeakRef(ev); // do last, because it might self destroy
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+void morkHandle::NilFactoryError(morkEnv* ev) const {
+ ev->NewError("nil mHandle_Factory");
+}
+
+void morkHandle::NilHandleObjectError(morkEnv* ev) const {
+ ev->NewError("nil mHandle_Object");
+}
+
+void morkHandle::NonNodeObjectError(morkEnv* ev) const {
+ ev->NewError("non-node mHandle_Object");
+}
+
+void morkHandle::NonOpenObjectError(morkEnv* ev) const {
+ ev->NewError("non-open mHandle_Object");
+}
+
+void morkHandle::NewBadMagicHandleError(morkEnv* ev, mork_magic inMagic) const {
+ MORK_USED_1(inMagic);
+ ev->NewError("wrong mHandle_Magic");
+}
+
+void morkHandle::NewDownHandleError(morkEnv* ev) const {
+ if (this->IsHandle()) {
+ if (this->GoodHandleTag()) {
+ if (this->IsOpenNode())
+ ev->NewError("unknown down morkHandle error");
+ else
+ this->NonOpenNodeError(ev);
+ } else
+ ev->NewError("wrong morkHandle tag");
+ } else
+ ev->NewError("non morkHandle");
+}
+
+morkObject* morkHandle::GetGoodHandleObject(morkEnv* ev, mork_bool inMutable,
+ mork_magic inMagicType,
+ mork_bool inClosedOkay) const {
+ morkObject* outObject = 0;
+ if (this->IsHandle() && this->GoodHandleTag() &&
+ (inClosedOkay || this->IsOpenNode())) {
+ if (!inMagicType || mHandle_Magic == inMagicType) {
+ morkObject* obj = this->mHandle_Object;
+ if (obj) {
+ if (obj->IsNode()) {
+ if (inClosedOkay || obj->IsOpenNode()) {
+ if (this->IsMutable() || !inMutable)
+ outObject = obj;
+ else
+ this->NonMutableNodeError(ev);
+ } else
+ this->NonOpenObjectError(ev);
+ } else
+ this->NonNodeObjectError(ev);
+ } else if (!inClosedOkay)
+ this->NilHandleObjectError(ev);
+ } else
+ this->NewBadMagicHandleError(ev, inMagicType);
+ } else
+ this->NewDownHandleError(ev);
+
+ MORK_ASSERT(outObject || inClosedOkay);
+ return outObject;
+}
+
+morkEnv* morkHandle::CanUseHandle(nsIMdbEnv* mev, mork_bool inMutable,
+ mork_bool inClosedOkay,
+ nsresult* outErr) const {
+ morkEnv* outEnv = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkObject* obj = this->GetGoodHandleObject(ev, inMutable,
+ /*magic*/ 0, inClosedOkay);
+ if (obj) {
+ outEnv = ev;
+ }
+ *outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outEnv || inClosedOkay);
+ return outEnv;
+}
+
+// { ===== begin nsIMdbObject methods =====
+
+// { ----- begin attribute methods -----
+/*virtual*/ nsresult morkHandle::Handle_IsFrozenMdbObject(
+ nsIMdbEnv* mev, mdb_bool* outIsReadonly) {
+ nsresult outErr = NS_OK;
+ mdb_bool readOnly = mdbBool_kTrue;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ readOnly = mHandle_Object->IsFrozen();
+
+ outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outIsReadonly);
+ if (outIsReadonly) *outIsReadonly = readOnly;
+
+ return outErr;
+}
+// same as nsIMdbPort::GetIsPortReadonly() when this object is inside a port.
+// } ----- end attribute methods -----
+
+// { ----- begin factory methods -----
+/*virtual*/ nsresult morkHandle::Handle_GetMdbFactory(
+ nsIMdbEnv* mev, nsIMdbFactory** acqFactory) {
+ nsresult outErr = NS_OK;
+ nsIMdbFactory* handle = 0;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ morkFactory* factory = ev->mEnv_Factory;
+ if (factory) {
+ handle = factory;
+ NS_ADDREF(handle);
+ } else
+ this->NilFactoryError(ev);
+
+ outErr = ev->AsErr();
+ }
+
+ MORK_ASSERT(acqFactory);
+ if (acqFactory) *acqFactory = handle;
+
+ return outErr;
+}
+// } ----- end factory methods -----
+
+// { ----- begin ref counting for well-behaved cyclic graphs -----
+/*virtual*/ nsresult morkHandle::Handle_GetWeakRefCount(
+ nsIMdbEnv* mev, // weak refs
+ mdb_count* outCount) {
+ nsresult outErr = NS_OK;
+ mdb_count count = 0;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ count = this->WeakRefsOnly();
+
+ outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outCount);
+ if (outCount) *outCount = count;
+
+ return outErr;
+}
+/*virtual*/ nsresult morkHandle::Handle_GetStrongRefCount(
+ nsIMdbEnv* mev, // strong refs
+ mdb_count* outCount) {
+ nsresult outErr = NS_OK;
+ mdb_count count = 0;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ count = this->StrongRefsOnly();
+
+ outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outCount);
+ if (outCount) *outCount = count;
+
+ return outErr;
+}
+
+/*virtual*/ nsresult morkHandle::Handle_AddWeakRef(nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ this->AddWeakRef(ev);
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+}
+/*virtual*/ nsresult morkHandle::Handle_AddStrongRef(nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ this->AddStrongRef(ev);
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+}
+
+/*virtual*/ nsresult morkHandle::Handle_CutWeakRef(nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ this->CutWeakRef(ev);
+ outErr = ev->AsErr();
+ }
+
+ return outErr;
+}
+/*virtual*/ nsresult morkHandle::Handle_CutStrongRef(nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ this->CutStrongRef(ev);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+/*virtual*/ nsresult morkHandle::Handle_CloseMdbObject(nsIMdbEnv* mev)
+// called at strong refs zero
+{
+ // if only one ref, Handle_CutStrongRef will clean up better.
+ if (mNode_Uses == 1) return Handle_CutStrongRef(mev);
+
+ nsresult outErr = NS_OK;
+
+ if (this->IsNode() && this->IsOpenNode()) {
+ morkEnv* ev = CanUseHandle(mev, /*inMutable*/ morkBool_kFalse,
+ /*inClosedOkay*/ morkBool_kTrue, &outErr);
+ if (ev) {
+ morkObject* object = mHandle_Object;
+ if (object && object->IsNode() && object->IsOpenNode())
+ object->CloseMorkNode(ev);
+
+ this->CloseMorkNode(ev);
+ outErr = ev->AsErr();
+ }
+ }
+ return outErr;
+}
+
+/*virtual*/ nsresult morkHandle::Handle_IsOpenMdbObject(nsIMdbEnv* mev,
+ mdb_bool* outOpen) {
+ MORK_USED_1(mev);
+ nsresult outErr = NS_OK;
+
+ MORK_ASSERT(outOpen);
+ if (outOpen) *outOpen = this->IsOpenNode();
+
+ return outErr;
+}
+// } ----- end ref counting -----
+
+// } ===== end nsIMdbObject methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkHandle.h b/comm/mailnews/db/mork/morkHandle.h
new file mode 100644
index 0000000000..5089e629c8
--- /dev/null
+++ b/comm/mailnews/db/mork/morkHandle.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKHANDLE_
+#define _MORKHANDLE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class morkPool;
+class morkObject;
+class morkFactory;
+
+#define morkDerived_kHandle /*i*/ 0x486E /* ascii 'Hn' */
+#define morkHandle_kTag 0x68416E44 /* ascii 'hAnD' */
+
+/*| morkHandle:
+|*/
+class morkHandle : public morkNode {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ mork_u4 mHandle_Tag; // must equal morkHandle_kTag
+ morkEnv* mHandle_Env; // pool that allocated this handle
+ morkHandleFace* mHandle_Face; // cookie from pool containing this
+ morkObject* mHandle_Object; // object this handle wraps for MDB API
+ mork_magic mHandle_Magic; // magic sig different in each subclass
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseHandle() only if open
+ virtual ~morkHandle(); // assert that CloseHandle() executed earlier
+
+ public: // morkHandle construction & destruction
+ morkHandle(
+ morkEnv* ev, // note morkUsage is always morkUsage_kPool
+ morkHandleFace* ioFace, // must not be nil, cookie for this handle
+ morkObject* ioObject, // must not be nil, the object for this handle
+ mork_magic inMagic); // magic sig to denote specific subclass
+ void CloseHandle(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkHandle(const morkHandle& other);
+ morkHandle& operator=(const morkHandle& other);
+
+ protected: // special case empty construction for morkHandleFrame
+ friend class morkHandleFrame;
+ morkHandle() {}
+
+ public: // dynamic type identification
+ mork_bool IsHandle() const {
+ return IsNode() && mNode_Derived == morkDerived_kHandle;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // morkHandle memory management operators
+ void* operator new(size_t inSize, morkPool& ioPool, morkZone& ioZone,
+ morkEnv* ev) noexcept(true) {
+ return ioPool.NewHandle(ev, inSize, &ioZone);
+ }
+
+ void* operator new(size_t inSize, morkPool& ioPool,
+ morkEnv* ev) noexcept(true) {
+ return ioPool.NewHandle(ev, inSize, (morkZone*)0);
+ }
+
+ void* operator new(size_t inSize, morkHandleFace* ioFace) noexcept(true) {
+ MORK_USED_1(inSize);
+ return ioFace;
+ }
+
+ public: // other handle methods
+ mork_bool GoodHandleTag() const { return mHandle_Tag == morkHandle_kTag; }
+
+ void NewBadMagicHandleError(morkEnv* ev, mork_magic inMagic) const;
+ void NewDownHandleError(morkEnv* ev) const;
+ void NilFactoryError(morkEnv* ev) const;
+ void NilHandleObjectError(morkEnv* ev) const;
+ void NonNodeObjectError(morkEnv* ev) const;
+ void NonOpenObjectError(morkEnv* ev) const;
+
+ morkObject* GetGoodHandleObject(morkEnv* ev, mork_bool inMutable,
+ mork_magic inMagicType,
+ mork_bool inClosedOkay) const;
+
+ public: // interface supporting mdbObject methods
+ morkEnv* CanUseHandle(nsIMdbEnv* mev, mork_bool inMutable,
+ mork_bool inClosedOkay, nsresult* outErr) const;
+
+ // { ----- begin mdbObject style methods -----
+ nsresult Handle_IsFrozenMdbObject(nsIMdbEnv* ev, mdb_bool* outIsReadonly);
+
+ nsresult Handle_GetMdbFactory(nsIMdbEnv* ev, nsIMdbFactory** acqFactory);
+ nsresult Handle_GetWeakRefCount(nsIMdbEnv* ev, mdb_count* outCount);
+ nsresult Handle_GetStrongRefCount(nsIMdbEnv* ev, mdb_count* outCount);
+
+ nsresult Handle_AddWeakRef(nsIMdbEnv* ev);
+ nsresult Handle_AddStrongRef(nsIMdbEnv* ev);
+
+ nsresult Handle_CutWeakRef(nsIMdbEnv* ev);
+ nsresult Handle_CutStrongRef(nsIMdbEnv* ev);
+
+ nsresult Handle_CloseMdbObject(nsIMdbEnv* ev);
+ nsresult Handle_IsOpenMdbObject(nsIMdbEnv* ev, mdb_bool* outOpen);
+ // } ----- end mdbObject style methods -----
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakHandle(morkHandle* me, morkEnv* ev, morkHandle** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongHandle(morkHandle* me, morkEnv* ev,
+ morkHandle** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+#define morkHandleFrame_kPadSlotCount 4
+
+/*| morkHandleFrame: an object format used for allocating and maintaining
+**| instances of morkHandle, with leading slots used to maintain this in a
+**| linked list, and following slots to provide extra footprint that might
+**| be needed by any morkHandle subclasses that include very little extra
+**| space (by virtue of the fact that each morkHandle subclass is expected
+**| to multiply inherit from another base class that has only abstract methods
+**| for space overhead related only to some vtable representation).
+|*/
+class morkHandleFrame {
+ public:
+ morkLink mHandleFrame_Link; // list storage without trampling Handle
+ morkHandle mHandleFrame_Handle;
+ mork_ip mHandleFrame_Padding[morkHandleFrame_kPadSlotCount];
+
+ public:
+ morkHandle* AsHandle() { return &mHandleFrame_Handle; }
+
+ morkHandleFrame() {} // actually, morkHandleFrame never gets constructed
+
+ private: // copying is not allowed
+ morkHandleFrame(const morkHandleFrame& other);
+ morkHandleFrame& operator=(const morkHandleFrame& other);
+};
+
+#define morkHandleFrame_kHandleOffset \
+ mork_OffsetOf(morkHandleFrame, mHandleFrame_Handle)
+
+#define morkHandle_AsHandleFrame(h) \
+ ((h)->mHandle_Block, \
+ ((morkHandleFrame*)(((mork_u1*)(h)) - morkHandleFrame_kHandleOffset)))
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKHANDLE_ */
diff --git a/comm/mailnews/db/mork/morkIntMap.cpp b/comm/mailnews/db/mork/morkIntMap.cpp
new file mode 100644
index 0000000000..69caa867a1
--- /dev/null
+++ b/comm/mailnews/db/mork/morkIntMap.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKINTMAP_
+# include "morkIntMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkIntMap::CloseMorkNode(
+ morkEnv* ev) // CloseIntMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseIntMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkIntMap::~morkIntMap() // assert CloseIntMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkIntMap::morkIntMap(morkEnv* ev, const morkUsage& inUsage,
+ mork_size inValSize, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_bool inHoldChanges)
+ : morkMap(ev, inUsage, ioHeap, sizeof(mork_u4), inValSize,
+ morkIntMap_kStartSlotCount, ioSlotHeap, inHoldChanges) {
+ if (ev->Good()) mNode_Derived = morkDerived_kIntMap;
+}
+
+/*public non-poly*/ void morkIntMap::CloseIntMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CloseMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool // *((mork_u4*) inKeyA) == *((mork_u4*) inKeyB)
+morkIntMap::Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const {
+ MORK_USED_1(ev);
+ return *((const mork_u4*)inKeyA) == *((const mork_u4*)inKeyB);
+}
+
+/*virtual*/ mork_u4 // some integer function of *((mork_u4*) inKey)
+morkIntMap::Hash(morkEnv* ev, const void* inKey) const {
+ MORK_USED_1(ev);
+ return *((const mork_u4*)inKey);
+}
+// } ===== end morkMap poly interface =====
+
+mork_bool morkIntMap::AddInt(morkEnv* ev, mork_u4 inKey, void* ioAddress)
+// the AddInt() method return value equals ev->Good().
+{
+ if (ev->Good()) {
+ this->Put(ev, &inKey, &ioAddress,
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+ }
+
+ return ev->Good();
+}
+
+mork_bool morkIntMap::CutInt(morkEnv* ev, mork_u4 inKey) {
+ return this->Cut(ev, &inKey, /*key*/ (void*)0, /*val*/ (void*)0,
+ (mork_change**)0);
+}
+
+void* morkIntMap::GetInt(morkEnv* ev, mork_u4 inKey)
+// Note the returned val does NOT have an increase in refcount for this.
+{
+ void* val = 0; // old val in the map
+ this->Get(ev, &inKey, /*key*/ (void*)0, &val, (mork_change**)0);
+
+ return val;
+}
+
+mork_bool morkIntMap::HasInt(morkEnv* ev, mork_u4 inKey)
+// Note the returned val does NOT have an increase in refcount for this.
+{
+ return this->Get(ev, &inKey, /*key*/ (void*)0, /*val*/ (void*)0,
+ (mork_change**)0);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#ifdef MORK_POINTER_MAP_IMPL
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkPointerMap::CloseMorkNode(
+ morkEnv* ev) // ClosePointerMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->ClosePointerMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkPointerMap::~morkPointerMap() // assert ClosePointerMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkPointerMap::morkPointerMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkMap(ev, inUsage, ioHeap, sizeof(void*), sizeof(void*),
+ morkPointerMap_kStartSlotCount, ioSlotHeap,
+ /*inHoldChanges*/ morkBool_kFalse) {
+ if (ev->Good()) mNode_Derived = morkDerived_kPointerMap;
+}
+
+/*public non-poly*/ void morkPointerMap::ClosePointerMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CloseMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool // *((void**) inKeyA) == *((void**) inKeyB)
+morkPointerMap::Equal(morkEnv* ev, const void* inKeyA,
+ const void* inKeyB) const {
+ MORK_USED_1(ev);
+ return *((const void**)inKeyA) == *((const void**)inKeyB);
+}
+
+/*virtual*/ mork_u4 // some integer function of *((mork_u4*) inKey)
+morkPointerMap::Hash(morkEnv* ev, const void* inKey) const {
+ MORK_USED_1(ev);
+ return *((const mork_u4*)inKey);
+}
+// } ===== end morkMap poly interface =====
+
+mork_bool morkPointerMap::AddPointer(morkEnv* ev, void* inKey, void* ioAddress)
+// the AddPointer() method return value equals ev->Good().
+{
+ if (ev->Good()) {
+ this->Put(ev, &inKey, &ioAddress,
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+ }
+
+ return ev->Good();
+}
+
+mork_bool morkPointerMap::CutPointer(morkEnv* ev, void* inKey) {
+ return this->Cut(ev, &inKey, /*key*/ (void*)0, /*val*/ (void*)0,
+ (mork_change**)0);
+}
+
+void* morkPointerMap::GetPointer(morkEnv* ev, void* inKey)
+// Note the returned val does NOT have an increase in refcount for this.
+{
+ void* val = 0; // old val in the map
+ this->Get(ev, &inKey, /*key*/ (void*)0, &val, (mork_change**)0);
+
+ return val;
+}
+
+mork_bool morkPointerMap::HasPointer(morkEnv* ev, void* inKey)
+// Note the returned val does NOT have an increase in refcount for this.
+{
+ return this->Get(ev, &inKey, /*key*/ (void*)0, /*val*/ (void*)0,
+ (mork_change**)0);
+}
+#endif /*MORK_POINTER_MAP_IMPL*/
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkIntMap.h b/comm/mailnews/db/mork/morkIntMap.h
new file mode 100644
index 0000000000..97f9c4b977
--- /dev/null
+++ b/comm/mailnews/db/mork/morkIntMap.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKINTMAP_
+#define _MORKINTMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kIntMap /*i*/ 0x694D /* ascii 'iM' */
+
+#define morkIntMap_kStartSlotCount 256
+
+/*| morkIntMap: maps mork_token -> morkNode
+|*/
+class morkIntMap : public morkMap { // for mapping tokens to maps
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseIntMap() only if open
+ virtual ~morkIntMap(); // assert that CloseIntMap() executed earlier
+
+ public: // morkMap construction & destruction
+ // keySize for morkIntMap equals sizeof(mork_u4)
+ morkIntMap(morkEnv* ev, const morkUsage& inUsage, mork_size inValSize,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ mork_bool inHoldChanges);
+ void CloseIntMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsIntMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kIntMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // *((mork_u4*) inKeyA) == *((mork_u4*) inKeyB)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const override;
+
+ virtual mork_u4 // some integer function of *((mork_u4*) inKey)
+ Hash(morkEnv* ev, const void* inKey) const override;
+ // } ===== end morkMap poly interface =====
+
+ public: // other map methods
+ mork_bool AddInt(morkEnv* ev, mork_u4 inKey, void* ioAddress);
+ // the AddInt() boolean return equals ev->Good().
+
+ mork_bool CutInt(morkEnv* ev, mork_u4 inKey);
+ // The CutInt() boolean return indicates whether removal happened.
+
+ void* GetInt(morkEnv* ev, mork_u4 inKey);
+ // Note the returned node does NOT have an increase in refcount for this.
+
+ mork_bool HasInt(morkEnv* ev, mork_u4 inKey);
+ // Note the returned node does NOT have an increase in refcount for this.
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#ifdef MORK_POINTER_MAP_IMPL
+
+# define morkDerived_kPointerMap /*i*/ 0x704D /* ascii 'pM' */
+
+# define morkPointerMap_kStartSlotCount 256
+
+/*| morkPointerMap: maps void* -> void*
+**|
+**| This pointer map class is equivalent to morkIntMap when sizeof(mork_u4)
+**| equals sizeof(void*). However, when these two sizes are different,
+**| then we cannot use the same hash table structure very easily without
+**| being very careful about the size and usage assumptions of those
+**| clients using the smaller data type. So we just go ahead and use
+**| morkPointerMap for hash tables using pointer key types.
+|*/
+class morkPointerMap : public morkMap { // for mapping tokens to maps
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // ClosePointerMap() only if open
+ virtual ~morkPointerMap(); // assert that ClosePointerMap() executed earlier
+
+ public: // morkMap construction & destruction
+ // keySize for morkPointerMap equals sizeof(mork_u4)
+ morkPointerMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void ClosePointerMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsPointerMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kPointerMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // *((void**) inKeyA) == *((void**) inKeyB)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const;
+
+ virtual mork_u4 // some integer function of *((mork_u4*) inKey)
+ Hash(morkEnv* ev, const void* inKey) const;
+ // } ===== end morkMap poly interface =====
+
+ public: // other map methods
+ mork_bool AddPointer(morkEnv* ev, void* inKey, void* ioAddress);
+ // the AddPointer() boolean return equals ev->Good().
+
+ mork_bool CutPointer(morkEnv* ev, void* inKey);
+ // The CutPointer() boolean return indicates whether removal happened.
+
+ void* GetPointer(morkEnv* ev, void* inKey);
+ // Note the returned node does NOT have an increase in refcount for this.
+
+ mork_bool HasPointer(morkEnv* ev, void* inKey);
+ // Note the returned node does NOT have an increase in refcount for this.
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakIntMap(morkIntMap* me, morkEnv* ev, morkIntMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongIntMap(morkIntMap* me, morkEnv* ev,
+ morkIntMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+#endif /*MORK_POINTER_MAP_IMPL*/
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKINTMAP_ */
diff --git a/comm/mailnews/db/mork/morkMap.cpp b/comm/mailnews/db/mork/morkMap.cpp
new file mode 100644
index 0000000000..22e57b4df5
--- /dev/null
+++ b/comm/mailnews/db/mork/morkMap.cpp
@@ -0,0 +1,852 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This code is mostly a port to C++ from public domain IronDoc C sources.
+// Note many code comments here come verbatim from cut-and-pasted IronDoc.
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+class morkHashArrays {
+ public:
+ nsIMdbHeap* mHashArrays_Heap; // copy of mMap_Heap
+ mork_count mHashArrays_Slots; // copy of mMap_Slots
+
+ mork_u1* mHashArrays_Keys; // copy of mMap_Keys
+ mork_u1* mHashArrays_Vals; // copy of mMap_Vals
+ morkAssoc* mHashArrays_Assocs; // copy of mMap_Assocs
+ mork_change* mHashArrays_Changes; // copy of mMap_Changes
+ morkAssoc** mHashArrays_Buckets; // copy of mMap_Buckets
+ morkAssoc* mHashArrays_FreeList; // copy of mMap_FreeList
+
+ public:
+ void finalize(morkEnv* ev);
+};
+
+void morkHashArrays::finalize(morkEnv* ev) {
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ nsIMdbHeap* heap = mHashArrays_Heap;
+
+ if (heap) {
+ if (mHashArrays_Keys) heap->Free(menv, mHashArrays_Keys);
+ if (mHashArrays_Vals) heap->Free(menv, mHashArrays_Vals);
+ if (mHashArrays_Assocs) heap->Free(menv, mHashArrays_Assocs);
+ if (mHashArrays_Changes) heap->Free(menv, mHashArrays_Changes);
+ if (mHashArrays_Buckets) heap->Free(menv, mHashArrays_Buckets);
+ }
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkMap::CloseMorkNode(
+ morkEnv* ev) // CloseMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkMap::~morkMap() // assert CloseMap() executed earlier
+{
+ MORK_ASSERT(mMap_FreeList == 0);
+ MORK_ASSERT(mMap_Buckets == 0);
+ MORK_ASSERT(mMap_Keys == 0);
+ MORK_ASSERT(mMap_Vals == 0);
+ MORK_ASSERT(mMap_Changes == 0);
+ MORK_ASSERT(mMap_Assocs == 0);
+}
+
+/*public non-poly*/ void morkMap::CloseMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ nsIMdbHeap* heap = mMap_Heap;
+ if (heap) /* need to free the arrays? */
+ {
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+
+ if (mMap_Keys) heap->Free(menv, mMap_Keys);
+
+ if (mMap_Vals) heap->Free(menv, mMap_Vals);
+
+ if (mMap_Assocs) heap->Free(menv, mMap_Assocs);
+
+ if (mMap_Buckets) heap->Free(menv, mMap_Buckets);
+
+ if (mMap_Changes) heap->Free(menv, mMap_Changes);
+ }
+ mMap_Keys = 0;
+ mMap_Vals = 0;
+ mMap_Buckets = 0;
+ mMap_Assocs = 0;
+ mMap_Changes = 0;
+ mMap_FreeList = 0;
+ MORK_MEMSET(&mMap_Form, 0, sizeof(morkMapForm));
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+void morkMap::clear_map(morkEnv* ev, nsIMdbHeap* ioSlotHeap) {
+ mMap_Tag = 0;
+ mMap_Seed = 0;
+ mMap_Slots = 0;
+ mMap_Fill = 0;
+ mMap_Keys = 0;
+ mMap_Vals = 0;
+ mMap_Assocs = 0;
+ mMap_Changes = 0;
+ mMap_Buckets = 0;
+ mMap_FreeList = 0;
+ MORK_MEMSET(&mMap_Form, 0, sizeof(morkMapForm));
+
+ mMap_Heap = 0;
+ if (ioSlotHeap) {
+ nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mMap_Heap);
+ } else
+ ev->NilPointerError();
+}
+
+morkMap::morkMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_size inKeySize, mork_size inValSize, mork_size inSlots,
+ nsIMdbHeap* ioSlotHeap, mork_bool inHoldChanges)
+ : morkNode(ev, inUsage, ioHeap), mMap_Heap(0) {
+ if (ev->Good()) {
+ this->clear_map(ev, ioSlotHeap);
+ if (ev->Good()) {
+ mMap_Form.mMapForm_HoldChanges = inHoldChanges;
+
+ mMap_Form.mMapForm_KeySize = inKeySize;
+ mMap_Form.mMapForm_ValSize = inValSize;
+ mMap_Form.mMapForm_KeyIsIP = (inKeySize == sizeof(mork_ip));
+ mMap_Form.mMapForm_ValIsIP = (inValSize == sizeof(mork_ip));
+
+ this->InitMap(ev, inSlots);
+ if (ev->Good()) mNode_Derived = morkDerived_kMap;
+ }
+ }
+}
+
+void morkMap::NewIterOutOfSyncError(morkEnv* ev) {
+ ev->NewError("map iter out of sync");
+}
+
+void morkMap::NewBadMapError(morkEnv* ev) { ev->NewError("bad morkMap tag"); }
+
+void morkMap::NewSlotsUnderflowWarning(morkEnv* ev) {
+ ev->NewWarning("member count underflow");
+}
+
+void morkMap::InitMap(morkEnv* ev, mork_size inSlots) {
+ if (ev->Good()) {
+ morkHashArrays old;
+ // MORK_MEMCPY(&mMap_Form, &inForm, sizeof(morkMapForm));
+ if (inSlots < 3) /* requested capacity absurdly small? */
+ inSlots = 3; /* bump it up to a minimum practical level */
+ else if (inSlots > (128 * 1024)) /* requested slots absurdly big? */
+ inSlots = (128 * 1024); /* decrease it to a maximum practical level */
+
+ if (this->new_arrays(ev, &old, inSlots)) mMap_Tag = morkMap_kTag;
+
+ MORK_MEMSET(&old, 0, sizeof(morkHashArrays)); // do NOT finalize
+ }
+}
+
+morkAssoc** morkMap::find(morkEnv* ev, const void* inKey,
+ mork_u4 inHash) const {
+ mork_u1* keys = mMap_Keys;
+ mork_num keySize = this->FormKeySize();
+ // morkMap_mEqual equal = this->FormEqual();
+
+ morkAssoc** ref = mMap_Buckets + (inHash % mMap_Slots);
+ morkAssoc* assoc = *ref;
+ while (assoc) /* look at another assoc in the bucket? */
+ {
+ mork_pos i = assoc - mMap_Assocs; /* index of this assoc */
+ if (this->Equal(ev, keys + (i * keySize), inKey)) /* found? */
+ return ref;
+
+ ref = &assoc->mAssoc_Next; /* consider next assoc slot in bucket */
+ assoc = *ref; /* if this is null, then we are done */
+ }
+ return 0;
+}
+
+/*| get_assoc: read the key and/or value at index inPos
+|*/
+void morkMap::get_assoc(void* outKey, void* outVal, mork_pos inPos) const {
+ mork_num valSize = this->FormValSize();
+ if (valSize && outVal) /* map holds values? caller wants the value? */
+ {
+ const mork_u1* value = mMap_Vals + (valSize * inPos);
+ if (valSize == sizeof(mork_ip) && this->FormValIsIP()) /* ip case? */
+ *((mork_ip*)outVal) = *((const mork_ip*)value);
+ else
+ MORK_MEMCPY(outVal, value, valSize);
+ }
+ if (outKey) /* caller wants the key? */
+ {
+ mork_num keySize = this->FormKeySize();
+ const mork_u1* key = mMap_Keys + (keySize * inPos);
+ if (keySize == sizeof(mork_ip) && this->FormKeyIsIP()) /* ip case? */
+ *((mork_ip*)outKey) = *((const mork_ip*)key);
+ else
+ MORK_MEMCPY(outKey, key, keySize);
+ }
+}
+
+/*| put_assoc: write the key and/or value at index inPos
+|*/
+void morkMap::put_assoc(const void* inKey, const void* inVal,
+ mork_pos inPos) const {
+ mork_num valSize = this->FormValSize();
+ if (valSize && inVal) /* map holds values? caller sends a value? */
+ {
+ mork_u1* value = mMap_Vals + (valSize * inPos);
+ if (valSize == sizeof(mork_ip) && this->FormValIsIP()) /* ip case? */
+ *((mork_ip*)value) = *((const mork_ip*)inVal);
+ else
+ MORK_MEMCPY(value, inVal, valSize);
+ }
+ if (inKey) /* caller sends a key? */
+ {
+ mork_num keySize = this->FormKeySize();
+ mork_u1* key = mMap_Keys + (keySize * inPos);
+ if (keySize == sizeof(mork_ip) && this->FormKeyIsIP()) /* ip case? */
+ *((mork_ip*)key) = *((const mork_ip*)inKey);
+ else
+ MORK_MEMCPY(key, inKey, keySize);
+ }
+}
+
+void* morkMap::clear_alloc(morkEnv* ev, mork_size inSize) {
+ void* p = 0;
+ nsIMdbHeap* heap = mMap_Heap;
+ if (heap) {
+ if (NS_SUCCEEDED(heap->Alloc(ev->AsMdbEnv(), inSize, (void**)&p)) && p) {
+ MORK_MEMSET(p, 0, inSize);
+ return p;
+ }
+ } else
+ ev->NilPointerError();
+
+ return (void*)0;
+}
+
+void* morkMap::alloc(morkEnv* ev, mork_size inSize) {
+ void* p = 0;
+ nsIMdbHeap* heap = mMap_Heap;
+ if (heap) {
+ if (NS_SUCCEEDED(heap->Alloc(ev->AsMdbEnv(), inSize, (void**)&p)) && p)
+ return p;
+ } else
+ ev->NilPointerError();
+
+ return (void*)0;
+}
+
+/*| new_keys: allocate an array of inSlots new keys filled with zero.
+|*/
+mork_u1* morkMap::new_keys(morkEnv* ev, mork_num inSlots) {
+ mork_num size = inSlots * this->FormKeySize();
+ return (mork_u1*)this->clear_alloc(ev, size);
+}
+
+/*| new_values: allocate an array of inSlots new values filled with zero.
+**| When values are zero sized, we just return a null pointer.
+|*/
+mork_u1* morkMap::new_values(morkEnv* ev, mork_num inSlots) {
+ mork_u1* values = 0;
+ mork_num size = inSlots * this->FormValSize();
+ if (size) values = (mork_u1*)this->clear_alloc(ev, size);
+ return values;
+}
+
+mork_change* morkMap::new_changes(morkEnv* ev, mork_num inSlots) {
+ mork_change* changes = 0;
+ mork_num size = inSlots * sizeof(mork_change);
+ if (size && mMap_Form.mMapForm_HoldChanges)
+ changes = (mork_change*)this->clear_alloc(ev, size);
+ return changes;
+}
+
+/*| new_buckets: allocate an array of inSlots new buckets filled with zero.
+|*/
+morkAssoc** morkMap::new_buckets(morkEnv* ev, mork_num inSlots) {
+ mork_num size = inSlots * sizeof(morkAssoc*);
+ return (morkAssoc**)this->clear_alloc(ev, size);
+}
+
+/*| new_assocs: allocate an array of inSlots new assocs, with each assoc
+**| linked together in a list with the first array element at the list head
+**| and the last element at the list tail. (morkMap::grow() needs this.)
+|*/
+morkAssoc* morkMap::new_assocs(morkEnv* ev, mork_num inSlots) {
+ mork_num size = inSlots * sizeof(morkAssoc);
+ morkAssoc* assocs = (morkAssoc*)this->alloc(ev, size);
+ if (assocs) /* able to allocate the array? */
+ {
+ morkAssoc* a = assocs + (inSlots - 1); /* the last array element */
+ a->mAssoc_Next = 0; /* terminate tail element of the list with null */
+ while (--a >= assocs) /* another assoc to link into the list? */
+ a->mAssoc_Next = a + 1; /* each points to the following assoc */
+ }
+ return assocs;
+}
+
+mork_bool morkMap::new_arrays(morkEnv* ev, morkHashArrays* old,
+ mork_num inSlots) {
+ mork_bool outNew = morkBool_kFalse;
+
+ /* see if we can allocate all the new arrays before we go any further: */
+ morkAssoc** newBuckets = this->new_buckets(ev, inSlots);
+ morkAssoc* newAssocs = this->new_assocs(ev, inSlots);
+ mork_u1* newKeys = this->new_keys(ev, inSlots);
+ mork_u1* newValues = this->new_values(ev, inSlots);
+ mork_change* newChanges = this->new_changes(ev, inSlots);
+
+ /* it is okay for newChanges to be null when changes are not held: */
+ mork_bool okayChanges = (newChanges || !this->FormHoldChanges());
+
+ /* it is okay for newValues to be null when values are zero sized: */
+ mork_bool okayValues = (newValues || !this->FormValSize());
+
+ if (newBuckets && newAssocs && newKeys && okayChanges && okayValues) {
+ outNew = morkBool_kTrue; /* yes, we created all the arrays we need */
+
+ /* init the old hashArrays with slots from this hash table: */
+ old->mHashArrays_Heap = mMap_Heap;
+
+ old->mHashArrays_Slots = mMap_Slots;
+ old->mHashArrays_Keys = mMap_Keys;
+ old->mHashArrays_Vals = mMap_Vals;
+ old->mHashArrays_Assocs = mMap_Assocs;
+ old->mHashArrays_Buckets = mMap_Buckets;
+ old->mHashArrays_Changes = mMap_Changes;
+
+ /* now replace all our array slots with the new structures: */
+ ++mMap_Seed; /* note the map is now changed */
+ mMap_Keys = newKeys;
+ mMap_Vals = newValues;
+ mMap_Buckets = newBuckets;
+ mMap_Assocs = newAssocs;
+ mMap_FreeList = newAssocs; /* all are free to start with */
+ mMap_Changes = newChanges;
+ mMap_Slots = inSlots;
+ } else /* free the partial set of arrays that were actually allocated */
+ {
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ nsIMdbHeap* heap = mMap_Heap;
+ if (newBuckets) heap->Free(menv, newBuckets);
+ if (newAssocs) heap->Free(menv, newAssocs);
+ if (newKeys) heap->Free(menv, newKeys);
+ if (newValues) heap->Free(menv, newValues);
+ if (newChanges) heap->Free(menv, newChanges);
+
+ MORK_MEMSET(old, 0, sizeof(morkHashArrays));
+ }
+
+ return outNew;
+}
+
+/*| grow: make the map arrays bigger by 33%. The old map is completely
+**| full, or else we would not have called grow() to get more space. This
+**| means the free list is empty, and also means every old key and value is in
+**| use in the old arrays. So every key and value must be copied to the new
+**| arrays, and since they are contiguous in the old arrays, we can efficiently
+**| bitwise copy them in bulk from the old arrays to the new arrays, without
+**| paying any attention to the structure of the old arrays.
+**|
+**|| The content of the old bucket and assoc arrays need not be copied because
+**| this information is going to be completely rebuilt by rehashing all the
+**| keys into their new buckets, given the new larger map capacity. The new
+**| bucket array from new_arrays() is assumed to contain all zeroes, which is
+**| necessary to ensure the lists in each bucket stay null terminated as we
+**| build the new linked lists. (Note no old bucket ordering is preserved.)
+**|
+**|| If the old capacity is N, then in the new arrays the assocs with indexes
+**| from zero to N-1 are still allocated and must be rehashed into the map.
+**| The new free list contains all the following assocs, starting with the new
+**| assoc link at index N. (We call the links in the link array "assocs"
+**| because allocating a link is the same as allocating the key/value pair
+**| with the same index as the link.)
+**|
+**|| The new free list is initialized simply by pointing at the first new link
+**| in the assoc array after the size of the old assoc array. This assumes
+**| that FeHashTable_new_arrays() has already linked all the new assocs into a
+**| list with the first at the head of the list and the last at the tail of the
+**| list. So by making the free list point to the first of the new uncopied
+**| assocs, the list is already well-formed.
+|*/
+mork_bool morkMap::grow(morkEnv* ev) {
+ if (mMap_Heap) /* can we grow the map? */
+ {
+ mork_num newSlots = (mMap_Slots * 2); /* +100% */
+ morkHashArrays old; /* a place to temporarily hold all the old arrays */
+ if (this->new_arrays(ev, &old, newSlots)) /* have more? */
+ {
+ // morkMap_mHash hash = this->FormHash(); /* for terse loop use */
+
+ /* figure out the bulk volume sizes of old keys and values to move: */
+ mork_num oldSlots = old.mHashArrays_Slots; /* number of old assocs */
+ mork_num keyBulk = oldSlots * this->FormKeySize(); /* key volume */
+ mork_num valBulk = oldSlots * this->FormValSize(); /* values */
+
+ /* convenient variables for new arrays that need to be rehashed: */
+ morkAssoc** newBuckets = mMap_Buckets; /* new all zeroes */
+ morkAssoc* newAssocs = mMap_Assocs; /* hash into buckets */
+ morkAssoc* newFreeList = newAssocs + oldSlots; /* new room is free */
+ mork_u1* key = mMap_Keys; /* the first key to rehash */
+ --newAssocs; /* back up one before preincrement used in while loop */
+
+ /* move all old keys and values to the new arrays: */
+ MORK_MEMCPY(mMap_Keys, old.mHashArrays_Keys, keyBulk);
+ if (valBulk) /* are values nonzero sized? */
+ MORK_MEMCPY(mMap_Vals, old.mHashArrays_Vals, valBulk);
+
+ mMap_FreeList = newFreeList; /* remaining assocs are free */
+
+ while (++newAssocs < newFreeList) /* rehash another old assoc? */
+ {
+ morkAssoc** top = newBuckets + (this->Hash(ev, key) % newSlots);
+ key += this->FormKeySize(); /* the next key to rehash */
+ newAssocs->mAssoc_Next = *top; /* link to prev bucket top */
+ *top = newAssocs; /* push assoc on top of bucket */
+ }
+ ++mMap_Seed; /* note the map has changed */
+ old.finalize(ev); /* remember to free the old arrays */
+ }
+ } else
+ ev->OutOfMemoryError();
+
+ return ev->Good();
+}
+
+mork_bool morkMap::Put(morkEnv* ev, const void* inKey, const void* inVal,
+ void* outKey, void* outVal, mork_change** outChange) {
+ mork_bool outPut = morkBool_kFalse;
+
+ if (this->GoodMap()) /* looks good? */
+ {
+ mork_u4 hash = this->Hash(ev, inKey);
+ morkAssoc** ref = this->find(ev, inKey, hash);
+ if (ref) /* assoc was found? reuse an existing assoc slot? */
+ {
+ outPut = morkBool_kTrue; /* inKey was indeed already inside the map */
+ } else /* assoc not found -- need to allocate a new assoc slot */
+ {
+ morkAssoc* assoc = this->pop_free_assoc();
+ if (!assoc) /* no slots remaining in free list? must grow map? */
+ {
+ if (this->grow(ev)) /* successfully made map larger? */
+ assoc = this->pop_free_assoc();
+ }
+ if (assoc) /* allocated new assoc without error? */
+ {
+ ref = mMap_Buckets + (hash % mMap_Slots);
+ assoc->mAssoc_Next = *ref; /* link to prev bucket top */
+ *ref = assoc; /* push assoc on top of bucket */
+
+ ++mMap_Fill; /* one more member in the collection */
+ ++mMap_Seed; /* note the map has changed */
+ }
+ }
+ if (ref) /* did not have an error during possible growth? */
+ {
+ mork_pos i = (*ref) - mMap_Assocs; /* index of assoc */
+ if (outPut && (outKey || outVal)) /* copy old before cobbering? */
+ this->get_assoc(outKey, outVal, i);
+
+ this->put_assoc(inKey, inVal, i);
+ ++mMap_Seed; /* note the map has changed */
+
+ if (outChange) {
+ if (mMap_Changes)
+ *outChange = mMap_Changes + i;
+ else
+ *outChange = this->FormDummyChange();
+ }
+ }
+ } else
+ this->NewBadMapError(ev);
+
+ return outPut;
+}
+
+mork_num morkMap::CutAll(morkEnv* ev) {
+ mork_num outCutAll = 0;
+
+ if (this->GoodMap()) /* map looks good? */
+ {
+ mork_num slots = mMap_Slots;
+ morkAssoc* before = mMap_Assocs - 1; /* before first member */
+ morkAssoc* assoc = before + slots; /* the very last member */
+
+ ++mMap_Seed; /* note the map is changed */
+
+ /* make the assoc array a linked list headed by first & tailed by last: */
+ assoc->mAssoc_Next = 0; /* null terminate the new free list */
+ while (--assoc > before) /* another assoc to link into the list? */
+ assoc->mAssoc_Next = assoc + 1;
+ mMap_FreeList = mMap_Assocs; /* all are free */
+
+ outCutAll = mMap_Fill; /* we'll cut all of them of course */
+
+ mMap_Fill = 0; /* the map is completely empty */
+ } else
+ this->NewBadMapError(ev);
+
+ return outCutAll;
+}
+
+mork_bool morkMap::Cut(morkEnv* ev, const void* inKey, void* outKey,
+ void* outVal, mork_change** outChange) {
+ mork_bool outCut = morkBool_kFalse;
+
+ if (this->GoodMap()) /* looks good? */
+ {
+ mork_u4 hash = this->Hash(ev, inKey);
+ morkAssoc** ref = this->find(ev, inKey, hash);
+ if (ref) /* found an assoc for key? */
+ {
+ outCut = morkBool_kTrue;
+ morkAssoc* assoc = *ref;
+ mork_pos i = assoc - mMap_Assocs; /* index of assoc */
+ if (outKey || outVal) this->get_assoc(outKey, outVal, i);
+
+ *ref = assoc->mAssoc_Next; /* unlink the found assoc */
+ this->push_free_assoc(assoc); /* and put it in free list */
+
+ if (outChange) {
+ if (mMap_Changes)
+ *outChange = mMap_Changes + i;
+ else
+ *outChange = this->FormDummyChange();
+ }
+
+ ++mMap_Seed; /* note the map has changed */
+ if (mMap_Fill) /* the count shows nonzero members? */
+ --mMap_Fill; /* one less member in the collection */
+ else
+ this->NewSlotsUnderflowWarning(ev);
+ }
+ } else
+ this->NewBadMapError(ev);
+
+ return outCut;
+}
+
+mork_bool morkMap::Get(morkEnv* ev, const void* inKey, void* outKey,
+ void* outVal, mork_change** outChange) {
+ mork_bool outGet = morkBool_kFalse;
+
+ if (this->GoodMap()) /* looks good? */
+ {
+ mork_u4 hash = this->Hash(ev, inKey);
+ morkAssoc** ref = this->find(ev, inKey, hash);
+ if (ref) /* found an assoc for inKey? */
+ {
+ mork_pos i = (*ref) - mMap_Assocs; /* index of assoc */
+ outGet = morkBool_kTrue;
+ this->get_assoc(outKey, outVal, i);
+ if (outChange) {
+ if (mMap_Changes)
+ *outChange = mMap_Changes + i;
+ else
+ *outChange = this->FormDummyChange();
+ }
+ }
+ } else
+ this->NewBadMapError(ev);
+
+ return outGet;
+}
+
+morkMapIter::morkMapIter()
+ : mMapIter_Map(0),
+ mMapIter_Seed(0)
+
+ ,
+ mMapIter_Bucket(0),
+ mMapIter_AssocRef(0),
+ mMapIter_Assoc(0),
+ mMapIter_Next(0) {}
+
+void morkMapIter::InitMapIter(morkEnv* ev, morkMap* ioMap) {
+ mMapIter_Map = 0;
+ mMapIter_Seed = 0;
+
+ mMapIter_Bucket = 0;
+ mMapIter_AssocRef = 0;
+ mMapIter_Assoc = 0;
+ mMapIter_Next = 0;
+
+ if (ioMap) {
+ if (ioMap->GoodMap()) {
+ mMapIter_Map = ioMap;
+ mMapIter_Seed = ioMap->mMap_Seed;
+ } else
+ ioMap->NewBadMapError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+morkMapIter::morkMapIter(morkEnv* ev, morkMap* ioMap)
+ : mMapIter_Map(0),
+ mMapIter_Seed(0)
+
+ ,
+ mMapIter_Bucket(0),
+ mMapIter_AssocRef(0),
+ mMapIter_Assoc(0),
+ mMapIter_Next(0) {
+ if (ioMap) {
+ if (ioMap->GoodMap()) {
+ mMapIter_Map = ioMap;
+ mMapIter_Seed = ioMap->mMap_Seed;
+ } else
+ ioMap->NewBadMapError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+void morkMapIter::CloseMapIter(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mMapIter_Map = 0;
+ mMapIter_Seed = 0;
+
+ mMapIter_Bucket = 0;
+ mMapIter_AssocRef = 0;
+ mMapIter_Assoc = 0;
+ mMapIter_Next = 0;
+}
+
+mork_change* morkMapIter::First(morkEnv* ev, void* outKey, void* outVal) {
+ mork_change* outFirst = 0;
+
+ morkMap* map = mMapIter_Map;
+
+ if (map && map->GoodMap()) /* map looks good? */
+ {
+ morkAssoc** bucket = map->mMap_Buckets;
+ morkAssoc** end = bucket + map->mMap_Slots; /* one past last */
+
+ mMapIter_Seed = map->mMap_Seed; /* sync the seeds */
+
+ while (bucket < end) /* another bucket in which to look for assocs? */
+ {
+ morkAssoc* assoc = *bucket++;
+ if (assoc) /* found the first map assoc in use? */
+ {
+ mork_pos i = assoc - map->mMap_Assocs;
+ mork_change* c = map->mMap_Changes;
+ outFirst = (c) ? (c + i) : map->FormDummyChange();
+
+ mMapIter_Assoc = assoc; /* current assoc in iteration */
+ mMapIter_Next = assoc->mAssoc_Next; /* more in bucket */
+ mMapIter_Bucket = --bucket; /* bucket for this assoc */
+ mMapIter_AssocRef = bucket; /* slot referencing assoc */
+
+ map->get_assoc(outKey, outVal, i);
+
+ break; /* end while loop */
+ }
+ }
+ } else
+ map->NewBadMapError(ev);
+
+ return outFirst;
+}
+
+mork_change* morkMapIter::Next(morkEnv* ev, void* outKey, void* outVal) {
+ mork_change* outNext = 0;
+
+ morkMap* map = mMapIter_Map;
+
+ if (map && map->GoodMap()) /* map looks good? */
+ {
+ if (mMapIter_Seed == map->mMap_Seed) /* in sync? */
+ {
+ morkAssoc* here = mMapIter_Assoc; /* current assoc */
+ if (here) /* iteration is not yet concluded? */
+ {
+ morkAssoc* next = mMapIter_Next;
+ morkAssoc* assoc = next; /* default new mMapIter_Assoc */
+ if (next) /* there are more assocs in the same bucket after Here? */
+ {
+ morkAssoc** ref = mMapIter_AssocRef;
+
+ /* (*HereRef) equals Here, except when Here has been cut, after
+ ** which (*HereRef) always equals Next. So if (*HereRef) is not
+ ** equal to Next, then HereRef still needs to be updated to point
+ ** somewhere else other than Here. Otherwise it is fine.
+ */
+ if (*ref != next) /* Here was not cut? must update HereRef? */
+ mMapIter_AssocRef = &here->mAssoc_Next;
+
+ mMapIter_Next = next->mAssoc_Next;
+ } else /* look for the next assoc in the next nonempty bucket */
+ {
+ morkAssoc** bucket = map->mMap_Buckets;
+ morkAssoc** end = bucket + map->mMap_Slots; /* beyond */
+ mMapIter_Assoc = 0; /* default to no more assocs */
+ bucket = mMapIter_Bucket; /* last exhausted bucket */
+ mMapIter_Assoc = 0; /* default to iteration ended */
+
+ while (++bucket < end) /* another bucket to search for assocs? */
+ {
+ assoc = *bucket;
+ if (assoc) /* found another map assoc in use? */
+ {
+ mMapIter_Bucket = bucket;
+ mMapIter_AssocRef = bucket; /* ref to assoc */
+ mMapIter_Next = assoc->mAssoc_Next; /* more */
+
+ break; /* end while loop */
+ }
+ }
+ }
+ if (assoc) /* did we find another assoc in the iteration? */
+ {
+ mMapIter_Assoc = assoc; /* current assoc */
+ mork_pos i = assoc - map->mMap_Assocs;
+ mork_change* c = map->mMap_Changes;
+ outNext = (c) ? (c + i) : map->FormDummyChange();
+
+ map->get_assoc(outKey, outVal, i);
+ }
+ }
+ } else
+ map->NewIterOutOfSyncError(ev);
+ } else
+ map->NewBadMapError(ev);
+
+ return outNext;
+}
+
+mork_change* morkMapIter::Here(morkEnv* ev, void* outKey, void* outVal) {
+ mork_change* outHere = 0;
+
+ morkMap* map = mMapIter_Map;
+
+ if (map && map->GoodMap()) /* map looks good? */
+ {
+ if (mMapIter_Seed == map->mMap_Seed) /* in sync? */
+ {
+ morkAssoc* here = mMapIter_Assoc; /* current assoc */
+ if (here) /* iteration is not yet concluded? */
+ {
+ mork_pos i = here - map->mMap_Assocs;
+ mork_change* c = map->mMap_Changes;
+ outHere = (c) ? (c + i) : map->FormDummyChange();
+
+ map->get_assoc(outKey, outVal, i);
+ }
+ } else
+ map->NewIterOutOfSyncError(ev);
+ } else
+ map->NewBadMapError(ev);
+
+ return outHere;
+}
+
+mork_change* morkMapIter::CutHere(morkEnv* ev, void* outKey, void* outVal) {
+ mork_change* outCutHere = 0;
+ morkMap* map = mMapIter_Map;
+
+ if (map && map->GoodMap()) /* map looks good? */
+ {
+ if (mMapIter_Seed == map->mMap_Seed) /* in sync? */
+ {
+ morkAssoc* here = mMapIter_Assoc; /* current assoc */
+ if (here) /* iteration is not yet concluded? */
+ {
+ morkAssoc** ref = mMapIter_AssocRef;
+ if (*ref != mMapIter_Next) /* not already cut? */
+ {
+ mork_pos i = here - map->mMap_Assocs;
+ mork_change* c = map->mMap_Changes;
+ outCutHere = (c) ? (c + i) : map->FormDummyChange();
+ if (outKey || outVal) map->get_assoc(outKey, outVal, i);
+
+ map->push_free_assoc(here); /* add to free list */
+ *ref = mMapIter_Next; /* unlink here from bucket list */
+
+ /* note the map has changed, but we are still in sync: */
+ mMapIter_Seed = ++map->mMap_Seed; /* sync */
+
+ if (map->mMap_Fill) /* still has nonzero value? */
+ --map->mMap_Fill; /* one less member in the collection */
+ else
+ map->NewSlotsUnderflowWarning(ev);
+ }
+ }
+ } else
+ map->NewIterOutOfSyncError(ev);
+ } else
+ map->NewBadMapError(ev);
+
+ return outCutHere;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkMap.h b/comm/mailnews/db/mork/morkMap.h
new file mode 100644
index 0000000000..0275755c4f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkMap.h
@@ -0,0 +1,379 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORKMAP_
+#define _MORKMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/* (These hash methods closely resemble those in public domain IronDoc.) */
+
+/*| Equal: equal for hash table. Note equal(a,b) implies hash(a)==hash(b).
+|*/
+typedef mork_bool (*morkMap_mEqual)(const morkMap* self, morkEnv* ev,
+ const void* inKeyA, const void* inKeyB);
+
+/*| Hash: hash for hash table. Note equal(a,b) implies hash(a)==hash(b).
+|*/
+typedef mork_u4 (*morkMap_mHash)(const morkMap* self, morkEnv* ev,
+ const void* inKey);
+
+/*| IsNil: whether a key slot contains a "null" value denoting "no such key".
+|*/
+typedef mork_bool (*morkMap_mIsNil)(const morkMap* self, morkEnv* ev,
+ const void* inKey);
+
+/*| Note: notify regarding a refcounting change for a key or a value.
+|*/
+// typedef void (* morkMap_mNote)
+//(morkMap* self, morkEnv* ev, void* inKeyOrVal);
+
+/*| morkMapForm: slots need to initialize a new dict. (This is very similar
+**| to the config object for public domain IronDoc hash tables.)
+|*/
+class morkMapForm { // a struct of callback method pointers for morkMap
+ public:
+ // const void* mMapForm_NilKey; // externally defined 'nil' bit pattern
+
+ // void* mMapForm_NilBuf[ 8 ]; // potential place to put NilKey
+ // If keys are no larger than 8*sizeof(void*), NilKey can be put in NilBuf.
+ // Note this should be true for all Mork subclasses, and we plan usage so.
+
+ // These three methods must always be provided, so zero will cause errors:
+
+ // morkMap_mEqual mMapForm_Equal; // for comparing two keys for identity
+ // morkMap_mHash mMapForm_Hash; // deterministic key to hash method
+ // morkMap_mIsNil mMapForm_IsNil; // to query whether a key equals 'nil'
+
+ // If any of these method slots are nonzero, then morkMap will call the
+ // appropriate one to notify dict users when a key or value is added or cut.
+ // Presumably a caller wants to know this in order to perform refcounting or
+ // some other kind of memory management. These methods are definitely only
+ // called when references to keys or values are inserted or removed, and are
+ // never called when the actual number of references does not change (such
+ // as when added keys are already present or cut keys are alreading missing).
+ //
+ // morkMap_mNote mMapForm_AddKey; // if nonzero, notify about add key
+ // morkMap_mNote mMapForm_CutKey; // if nonzero, notify about cut key
+ // morkMap_mNote mMapForm_AddVal; // if nonzero, notify about add val
+ // morkMap_mNote mMapForm_CutVal; // if nonzero, notify about cut val
+ //
+ // These note methods have been removed because it seems difficult to
+ // guarantee suitable alignment of objects passed to notification methods.
+
+ // Note dict clients should pick key and val sizes that provide whatever
+ // alignment will be required for an array of such keys and values.
+ mork_size mMapForm_KeySize; // size of every key (cannot be zero)
+ mork_size mMapForm_ValSize; // size of every val (can indeed be zero)
+
+ mork_bool mMapForm_HoldChanges; // support changes array in the map
+ mork_change mMapForm_DummyChange; // change used for false HoldChanges
+ mork_bool mMapForm_KeyIsIP; // key is mork_ip sized
+ mork_bool mMapForm_ValIsIP; // key is mork_ip sized
+};
+
+/*| morkAssoc: a canonical association slot in a morkMap. A single assoc
+**| instance does nothing except point to the next assoc in the same bucket
+**| of a hash table. Each assoc has only two interesting attributes: 1) the
+**| address of the assoc, and 2) the next assoc in a bucket's list. The assoc
+**| address is interesting because in the context of an array of such assocs,
+**| one can determine the index of a particular assoc in the array by address
+**| arithmetic, subtracting the array address from the assoc address. And the
+**| index of each assoc is the same index as the associated key and val slots
+**| in the associated arrays
+**|
+**|| Think of an assoc instance as really also containing a key slot and a val
+**| slot, where each key is mMap_Form.mMapForm_KeySize bytes in size, and
+**| each val is mMap_Form.mMapForm_ValSize in size. But the key and val
+**| slots are stored in separate arrays with indexes that are parallel to the
+**| indexes in the array of morkAssoc instances. We have taken the variable
+**| sized slots out of the morkAssoc structure, and put them into parallel
+**| arrays associated with each morkAssoc by array index. And this leaves us
+**| with only the link field to the next assoc in each assoc instance.
+|*/
+class morkAssoc {
+ public:
+ morkAssoc* mAssoc_Next;
+};
+
+#define morkDerived_kMap /*i*/ 0x4D70 /* ascii 'Mp' */
+
+#define morkMap_kTag /*i*/ 0x6D4D6150 /* ascii 'mMaP' */
+
+/*| morkMap: a hash table based on the public domain IronDoc hash table
+**| (which is in turn rather like a similar OpenDoc hash table).
+|*/
+class morkMap : public morkNode {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ nsIMdbHeap* mMap_Heap; // strong ref to heap allocating all space
+ mork_u4 mMap_Tag; // must equal morkMap_kTag
+
+ // When a morkMap instance is constructed, the dict form slots must be
+ // provided in order to properly configure a dict with all runtime needs:
+
+ morkMapForm mMap_Form; // construction time parameterization
+
+ // Whenever the dict changes structure in a way that would affect any
+ // iteration of the dict associations, the seed increments to show this:
+
+ mork_seed mMap_Seed; // counter for member and structural changes
+
+ // The current total assoc capacity of the dict is mMap_Slots, where
+ // mMap_Fill of these slots are actually holding content, so mMap_Fill
+ // is the actual membership count, and mMap_Slots is how larger membership
+ // can become before the hash table must grow the buffers being used.
+
+ mork_count mMap_Slots; // count of slots in the hash table
+ mork_fill mMap_Fill; // number of used slots in the hash table
+
+ // Key and value slots are bound to corresponding mMap_Assocs array slots.
+ // Instead of having a single array like this: {key,val,next}[ mMap_Slots ]
+ // we have instead three parallel arrays with essentially the same meaning:
+ // {key}[ mMap_Slots ], {val}[ mMap_Slots ], {assocs}[ mMap_Slots ]
+
+ mork_u1* mMap_Keys; // mMap_Slots * mMapForm_KeySize buffer
+ mork_u1* mMap_Vals; // mMap_Slots * mMapForm_ValSize buffer
+
+ // An assoc is "used" when it appears in a bucket's linked list of assocs.
+ // Until an assoc is used, it appears in the FreeList linked list. Every
+ // assoc that becomes used goes into the bucket determined by hashing the
+ // key associated with a new assoc. The key associated with a new assoc
+ // goes in to the slot in mMap_Keys which occupies exactly the same array
+ // index as the array index of the used assoc in the mMap_Assocs array.
+
+ morkAssoc* mMap_Assocs; // mMap_Slots * sizeof(morkAssoc) buffer
+
+ // The changes array is only needed when the
+
+ mork_change* mMap_Changes; // mMap_Slots * sizeof(mork_change) buffer
+
+ // The Buckets array need not be the same length as the Assocs array, but we
+ // usually do it that way so the average bucket depth is no more than one.
+ // (We could pick a different policy, or make it parameterizable, but that's
+ // tuning we can do some other time.)
+
+ morkAssoc** mMap_Buckets; // mMap_Slots * sizeof(morkAssoc*) buffer
+
+ // The length of the mMap_FreeList should equal (mMap_Slots - mMap_Fill).
+ // We need a free list instead of a simpler representation because assocs
+ // can be cut and returned to availability in any kind of unknown pattern.
+ // (However, when assocs are first allocated, or when the dict is grown, we
+ // know all new assocs are contiguous and can chain together adjacently.)
+
+ morkAssoc* mMap_FreeList; // list of unused mMap_Assocs array slots
+
+ public: // getters (morkProbeMap compatibility)
+ mork_fill MapFill() const { return mMap_Fill; }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseMap() only if open
+ virtual ~morkMap(); // assert that CloseMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioNodeHeap,
+ mork_size inKeySize, mork_size inValSize, mork_size inSlots,
+ nsIMdbHeap* ioSlotHeap, mork_bool inHoldChanges);
+
+ void CloseMap(morkEnv* ev); // called by
+
+ public: // dynamic type identification
+ mork_bool IsMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kMap;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // poly map hash table methods
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // note: equal(a,b) implies hash(a) == hash(b)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const = 0;
+
+ virtual mork_u4 // note: equal(a,b) implies hash(a) == hash(b)
+ Hash(morkEnv* ev, const void* inKey) const = 0;
+ // } ===== end morkMap poly interface =====
+
+ public: // open utility methods
+ mork_bool GoodMapTag() const { return mMap_Tag == morkMap_kTag; }
+ mork_bool GoodMap() const { return (IsNode() && GoodMapTag()); }
+
+ void NewIterOutOfSyncError(morkEnv* ev);
+ void NewBadMapError(morkEnv* ev);
+ void NewSlotsUnderflowWarning(morkEnv* ev);
+ void InitMap(morkEnv* ev, mork_size inSlots);
+
+ protected: // internal utility methods
+ friend class morkMapIter;
+ void clear_map(morkEnv* ev, nsIMdbHeap* ioHeap);
+
+ void* alloc(morkEnv* ev, mork_size inSize);
+ void* clear_alloc(morkEnv* ev, mork_size inSize);
+
+ void push_free_assoc(morkAssoc* ioAssoc) {
+ ioAssoc->mAssoc_Next = mMap_FreeList;
+ mMap_FreeList = ioAssoc;
+ }
+
+ morkAssoc* pop_free_assoc() {
+ morkAssoc* assoc = mMap_FreeList;
+ if (assoc) mMap_FreeList = assoc->mAssoc_Next;
+ return assoc;
+ }
+
+ morkAssoc** find(morkEnv* ev, const void* inKey, mork_u4 inHash) const;
+
+ mork_u1* new_keys(morkEnv* ev, mork_num inSlots);
+ mork_u1* new_values(morkEnv* ev, mork_num inSlots);
+ mork_change* new_changes(morkEnv* ev, mork_num inSlots);
+ morkAssoc** new_buckets(morkEnv* ev, mork_num inSlots);
+ morkAssoc* new_assocs(morkEnv* ev, mork_num inSlots);
+ mork_bool new_arrays(morkEnv* ev, morkHashArrays* old, mork_num inSlots);
+
+ mork_bool grow(morkEnv* ev);
+
+ void get_assoc(void* outKey, void* outVal, mork_pos inPos) const;
+ void put_assoc(const void* inKey, const void* inVal, mork_pos inPos) const;
+
+ public: // inlines to form slots
+ // const void* FormNilKey() const { return mMap_Form.mMapForm_NilKey; }
+
+ // morkMap_mEqual FormEqual() const { return mMap_Form.mMapForm_Equal; }
+ // morkMap_mHash FormHash() const { return mMap_Form.mMapForm_Hash; }
+ // orkMap_mIsNil FormIsNil() const { return mMap_Form.mMapForm_IsNil; }
+
+ // morkMap_mNote FormAddKey() const { return mMap_Form.mMapForm_AddKey; }
+ // morkMap_mNote FormCutKey() const { return mMap_Form.mMapForm_CutKey; }
+ // morkMap_mNote FormAddVal() const { return mMap_Form.mMapForm_AddVal; }
+ // morkMap_mNote FormCutVal() const { return mMap_Form.mMapForm_CutVal; }
+
+ mork_size FormKeySize() const { return mMap_Form.mMapForm_KeySize; }
+ mork_size FormValSize() const { return mMap_Form.mMapForm_ValSize; }
+
+ mork_bool FormKeyIsIP() const { return mMap_Form.mMapForm_KeyIsIP; }
+ mork_bool FormValIsIP() const { return mMap_Form.mMapForm_ValIsIP; }
+
+ mork_bool FormHoldChanges() const { return mMap_Form.mMapForm_HoldChanges; }
+
+ mork_change* FormDummyChange() { return &mMap_Form.mMapForm_DummyChange; }
+
+ public: // other map methods
+ mork_bool Put(morkEnv* ev, const void* inKey, const void* inVal, void* outKey,
+ void* outVal, mork_change** outChange);
+
+ mork_bool Cut(morkEnv* ev, const void* inKey, void* outKey, void* outVal,
+ mork_change** outChange);
+
+ mork_bool Get(morkEnv* ev, const void* inKey, void* outKey, void* outVal,
+ mork_change** outChange);
+
+ mork_num CutAll(morkEnv* ev);
+
+ private: // copying is not allowed
+ morkMap(const morkMap& other);
+ morkMap& operator=(const morkMap& other);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakMap(morkMap* me, morkEnv* ev, morkMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongMap(morkMap* me, morkEnv* ev, morkMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+/*| morkMapIter: an iterator for morkMap and subclasses. This is not a node,
+**| and expected usage is as a member of some other node subclass, such as in
+**| a cursor subclass or a thumb subclass. Also, iters might be as temp stack
+**| objects when scanning the content of a map.
+|*/
+class morkMapIter { // iterator for hash table map
+
+ protected:
+ morkMap* mMapIter_Map; // map to iterate, NOT refcounted
+ mork_seed mMapIter_Seed; // cached copy of map's seed
+
+ morkAssoc** mMapIter_Bucket; // one bucket in mMap_Buckets array
+ morkAssoc** mMapIter_AssocRef; // usually *AtRef equals Here
+ morkAssoc* mMapIter_Assoc; // the current assoc in an iteration
+ morkAssoc* mMapIter_Next; // mMapIter_Assoc->mAssoc_Next */
+
+ public:
+ morkMapIter(morkEnv* ev, morkMap* ioMap);
+ void CloseMapIter(morkEnv* ev);
+
+ morkMapIter(); // everything set to zero -- need to call InitMapIter()
+
+ protected: // we want all subclasses to provide typesafe wrappers:
+ void InitMapIter(morkEnv* ev, morkMap* ioMap);
+
+ // The morkAssoc returned below is always either mork_change* or
+ // else nil (when there is no such assoc). We return a pointer to
+ // the change rather than a simple bool, because callers might
+ // want to access change info associated with an assoc.
+
+ mork_change* First(morkEnv* ev, void* outKey, void* outVal);
+ mork_change* Next(morkEnv* ev, void* outKey, void* outVal);
+ mork_change* Here(morkEnv* ev, void* outKey, void* outVal);
+
+ mork_change* CutHere(morkEnv* ev, void* outKey, void* outVal);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKMAP_ */
diff --git a/comm/mailnews/db/mork/morkNode.cpp b/comm/mailnews/db/mork/morkNode.cpp
new file mode 100644
index 0000000000..c12fc6a0fd
--- /dev/null
+++ b/comm/mailnews/db/mork/morkNode.cpp
@@ -0,0 +1,550 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKHANDLE_
+# include "morkHandle.h"
+#endif
+
+/*3456789_123456789_123456789_123456789_123456789_123456789_123456789_12345678*/
+
+/* ===== ===== ===== ===== morkUsage ===== ===== ===== ===== */
+
+static morkUsage morkUsage_gHeap; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kHeap = morkUsage_gHeap;
+
+static morkUsage morkUsage_gStack; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kStack = morkUsage_gStack;
+
+static morkUsage morkUsage_gMember; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kMember = morkUsage_gMember;
+
+static morkUsage morkUsage_gGlobal; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kGlobal = morkUsage_gGlobal;
+
+static morkUsage morkUsage_gPool; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kPool = morkUsage_gPool;
+
+static morkUsage morkUsage_gNone; // ensure EnsureReadyStaticUsage()
+const morkUsage& morkUsage::kNone = morkUsage_gNone;
+
+// This must be structured to allow for non-zero values in global variables
+// just before static init time. We can only safely check for whether a
+// global has the address of some other global. Please, do not initialize
+// either of the variables below to zero, because this could break when a zero
+// is assigned at static init time, but after EnsureReadyStaticUsage() runs.
+
+static mork_u4 morkUsage_g_static_init_target; // only address of this matters
+static mork_u4* morkUsage_g_static_init_done; // is address of target above?
+
+#define morkUsage_do_static_init() \
+ (morkUsage_g_static_init_done = &morkUsage_g_static_init_target)
+
+#define morkUsage_need_static_init() \
+ (morkUsage_g_static_init_done != &morkUsage_g_static_init_target)
+
+/*static*/
+void morkUsage::EnsureReadyStaticUsage() {
+ if (morkUsage_need_static_init()) {
+ morkUsage_do_static_init();
+
+ morkUsage_gHeap.InitUsage(morkUsage_kHeap);
+ morkUsage_gStack.InitUsage(morkUsage_kStack);
+ morkUsage_gMember.InitUsage(morkUsage_kMember);
+ morkUsage_gGlobal.InitUsage(morkUsage_kGlobal);
+ morkUsage_gPool.InitUsage(morkUsage_kPool);
+ morkUsage_gNone.InitUsage(morkUsage_kNone);
+ }
+}
+
+/*static*/
+const morkUsage& morkUsage::GetHeap() // kHeap safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gHeap;
+}
+
+/*static*/
+const morkUsage& morkUsage::GetStack() // kStack safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gStack;
+}
+
+/*static*/
+const morkUsage& morkUsage::GetMember() // kMember safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gMember;
+}
+
+/*static*/
+const morkUsage& morkUsage::GetGlobal() // kGlobal safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gGlobal;
+}
+
+/*static*/
+const morkUsage& morkUsage::GetPool() // kPool safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gPool;
+}
+
+/*static*/
+const morkUsage& morkUsage::GetNone() // kNone safe at static init time
+{
+ EnsureReadyStaticUsage();
+ return morkUsage_gNone;
+}
+
+morkUsage::morkUsage() {
+ if (morkUsage_need_static_init()) {
+ morkUsage::EnsureReadyStaticUsage();
+ }
+}
+
+morkUsage::morkUsage(mork_usage code) : mUsage_Code(code) {
+ if (morkUsage_need_static_init()) {
+ morkUsage::EnsureReadyStaticUsage();
+ }
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*static*/ void* morkNode::MakeNew(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) {
+ void* node = 0;
+ ioHeap.Alloc(ev->AsMdbEnv(), inSize, (void**)&node);
+ if (!node) ev->OutOfMemoryError();
+
+ return node;
+}
+
+/*public non-poly*/ void morkNode::ZapOld(morkEnv* ev, nsIMdbHeap* ioHeap) {
+ if (this->IsNode()) {
+ mork_usage usage = mNode_Usage; // mNode_Usage before ~morkNode
+ this->morkNode::~morkNode(); // first call polymorphic destructor
+ if (ioHeap) // was this node heap allocated?
+ ioHeap->Free(ev->AsMdbEnv(), this);
+ else if (usage == morkUsage_kPool) // mNode_Usage before ~morkNode
+ {
+ morkHandle* h = (morkHandle*)this;
+ if (h->IsHandle() && h->GoodHandleTag()) {
+ if (h->mHandle_Face) {
+ if (ev->mEnv_HandlePool)
+ ev->mEnv_HandlePool->ZapHandle(ev, h->mHandle_Face);
+ else if (h->mHandle_Env && h->mHandle_Env->mEnv_HandlePool)
+ h->mHandle_Env->mEnv_HandlePool->ZapHandle(ev, h->mHandle_Face);
+ } else
+ ev->NilPointerError();
+ }
+ }
+ } else
+ this->NonNodeError(ev);
+}
+
+/*public virtual*/ void morkNode::CloseMorkNode(
+ morkEnv* ev) // CloseNode() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseNode(ev);
+ this->MarkShut();
+ }
+}
+NS_IMETHODIMP
+morkNode::CloseMdbObject(nsIMdbEnv* mev) {
+ return morkNode::CloseMdbObject((morkEnv*)mev);
+}
+
+nsresult morkNode::CloseMdbObject(morkEnv* ev) {
+ // if only one ref, Handle_CutStrongRef will clean up better.
+ if (mNode_Uses == 1)
+ // XXX Casting mork_uses to nsresult
+ return static_cast<nsresult>(CutStrongRef(ev));
+
+ nsresult outErr = NS_OK;
+
+ if (IsNode() && IsOpenNode()) {
+ if (ev) {
+ CloseMorkNode(ev);
+ outErr = ev->AsErr();
+ }
+ }
+ return outErr;
+}
+
+/*public virtual*/
+morkNode::~morkNode() // assert that CloseNode() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode() ||
+ IsDeadNode()); // sometimes we call destructor explicitly w/o
+ // freeing object.
+ mNode_Access = morkAccess_kDead;
+ mNode_Usage = morkUsage_kNone;
+}
+
+/*public virtual*/
+// void CloseMorkNode(morkEnv* ev) = 0; // CloseNode() only if open
+// CloseMorkNode() is the polymorphic close method called when uses==0,
+// which must do NOTHING at all when IsOpenNode() is not true. Otherwise,
+// CloseMorkNode() should call a static close method specific to an object.
+// Each such static close method should either call inherited static close
+// methods, or else perform the consolidated effect of calling them, where
+// subclasses should closely track any changes in base classes with care.
+
+/*public non-poly*/
+morkNode::morkNode(mork_usage inCode)
+ : mNode_Heap(0),
+ mNode_Base(morkBase_kNode),
+ mNode_Derived(0) // until subclass sets appropriately
+ ,
+ mNode_Access(morkAccess_kOpen),
+ mNode_Usage(inCode),
+ mNode_Mutable(morkAble_kEnabled),
+ mNode_Load(morkLoad_kClean),
+ mNode_Uses(1),
+ mNode_Refs(1) {}
+
+/*public non-poly*/
+morkNode::morkNode(const morkUsage& inUsage, nsIMdbHeap* ioHeap)
+ : mNode_Heap(ioHeap),
+ mNode_Base(morkBase_kNode),
+ mNode_Derived(0) // until subclass sets appropriately
+ ,
+ mNode_Access(morkAccess_kOpen),
+ mNode_Usage(inUsage.Code()),
+ mNode_Mutable(morkAble_kEnabled),
+ mNode_Load(morkLoad_kClean),
+ mNode_Uses(1),
+ mNode_Refs(1) {
+ if (!ioHeap && mNode_Usage == morkUsage_kHeap) MORK_ASSERT(ioHeap);
+}
+
+/*public non-poly*/
+morkNode::morkNode(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap)
+ : mNode_Heap(ioHeap),
+ mNode_Base(morkBase_kNode),
+ mNode_Derived(0) // until subclass sets appropriately
+ ,
+ mNode_Access(morkAccess_kOpen),
+ mNode_Usage(inUsage.Code()),
+ mNode_Mutable(morkAble_kEnabled),
+ mNode_Load(morkLoad_kClean),
+ mNode_Uses(1),
+ mNode_Refs(1) {
+ if (!ioHeap && mNode_Usage == morkUsage_kHeap) {
+ this->NilHeapError(ev);
+ }
+}
+
+/*protected non-poly*/ void morkNode::RefsUnderUsesWarning(morkEnv* ev) const {
+ ev->NewError("mNode_Refs < mNode_Uses");
+}
+
+/*protected non-poly*/ void morkNode::NonNodeError(
+ morkEnv* ev) const // called when IsNode() is false
+{
+ ev->NewError("non-morkNode");
+}
+
+/*protected non-poly*/ void morkNode::NonOpenNodeError(
+ morkEnv* ev) const // when IsOpenNode() is false
+{
+ ev->NewError("non-open-morkNode");
+}
+
+/*protected non-poly*/ void morkNode::NonMutableNodeError(
+ morkEnv* ev) const // when IsMutable() is false
+{
+ ev->NewError("non-mutable-morkNode");
+}
+
+/*protected non-poly*/ void morkNode::NilHeapError(
+ morkEnv* ev) const // zero mNode_Heap w/ kHeap usage
+{
+ ev->NewError("nil mNode_Heap");
+}
+
+/*protected non-poly*/ void morkNode::RefsOverflowWarning(
+ morkEnv* ev) const // mNode_Refs overflow
+{
+ ev->NewWarning("mNode_Refs overflow");
+}
+
+/*protected non-poly*/ void morkNode::UsesOverflowWarning(
+ morkEnv* ev) const // mNode_Uses overflow
+{
+ ev->NewWarning("mNode_Uses overflow");
+}
+
+/*protected non-poly*/ void morkNode::RefsUnderflowWarning(
+ morkEnv* ev) const // mNode_Refs underflow
+{
+ ev->NewWarning("mNode_Refs underflow");
+}
+
+/*protected non-poly*/ void morkNode::UsesUnderflowWarning(
+ morkEnv* ev) const // mNode_Uses underflow
+{
+ ev->NewWarning("mNode_Uses underflow");
+}
+
+/*public non-poly*/ void morkNode::CloseNode(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode())
+ this->MarkShut();
+ else
+ this->NonNodeError(ev);
+}
+
+extern void // utility method very similar to morkNode::SlotStrongNode():
+nsIMdbFile_SlotStrongFile(nsIMdbFile* self, morkEnv* ev, nsIMdbFile** ioSlot)
+// If *ioSlot is non-nil, that file is released by CutStrongRef() and
+// then zeroed out. Then if self is non-nil, this is acquired by
+// calling AddStrongRef(), and if the return value shows success,
+// then self is put into slot *ioSlot. Note self can be nil, so we take
+// expression 'nsIMdbFile_SlotStrongFile(0, ev, &slot)'.
+{
+ nsIMdbFile* file = *ioSlot;
+ if (self != file) {
+ if (file) {
+ *ioSlot = 0;
+ NS_RELEASE(file);
+ }
+ if (self && ev->Good()) NS_ADDREF(*ioSlot = self);
+ }
+}
+
+void // utility method very similar to morkNode::SlotStrongNode():
+nsIMdbHeap_SlotStrongHeap(nsIMdbHeap* self, morkEnv* ev, nsIMdbHeap** ioSlot)
+// If *ioSlot is non-nil, that heap is released by CutStrongRef() and
+// then zeroed out. Then if self is non-nil, self is acquired by
+// calling AddStrongRef(), and if the return value shows success,
+// then self is put into slot *ioSlot. Note self can be nil, so we
+// permit expression 'nsIMdbHeap_SlotStrongHeap(0, ev, &slot)'.
+{
+ nsIMdbHeap* heap = *ioSlot;
+ if (self != heap) {
+ if (heap) *ioSlot = 0;
+
+ if (self && ev->Good()) *ioSlot = self;
+ }
+}
+
+/*public static*/ void morkNode::SlotStrongNode(morkNode* me, morkEnv* ev,
+ morkNode** ioSlot)
+// If *ioSlot is non-nil, that node is released by CutStrongRef() and
+// then zeroed out. Then if me is non-nil, this is acquired by
+// calling AddStrongRef(), and if positive is returned to show success,
+// then me is put into slot *ioSlot. Note me can be nil, so we take
+// expression 'morkNode::SlotStrongNode((morkNode*) 0, ev, &slot)'.
+{
+ morkNode* node = *ioSlot;
+ if (me != node) {
+ if (node) {
+ // what if this nulls out the ev and causes asserts?
+ // can we move this after the CutStrongRef()?
+ *ioSlot = 0;
+ node->CutStrongRef(ev);
+ }
+ if (me && me->AddStrongRef(ev)) *ioSlot = me;
+ }
+}
+
+/*public static*/ void morkNode::SlotWeakNode(morkNode* me, morkEnv* ev,
+ morkNode** ioSlot)
+// If *ioSlot is non-nil, that node is released by CutWeakRef() and
+// then zeroed out. Then if me is non-nil, this is acquired by
+// calling AddWeakRef(), and if positive is returned to show success,
+// then me is put into slot *ioSlot. Note me can be nil, so we
+// expression 'morkNode::SlotWeakNode((morkNode*) 0, ev, &slot)'.
+{
+ morkNode* node = *ioSlot;
+ if (me != node) {
+ if (node) {
+ *ioSlot = 0;
+ node->CutWeakRef(ev);
+ }
+ if (me && me->AddWeakRef(ev)) *ioSlot = me;
+ }
+}
+
+/*public non-poly*/ mork_uses morkNode::AddStrongRef(morkEnv* ev) {
+ mork_uses outUses = 0;
+ if (this->IsNode()) {
+ mork_uses uses = mNode_Uses;
+ mork_refs refs = mNode_Refs;
+ if (refs < uses) // need to fix broken refs/uses relation?
+ {
+ this->RefsUnderUsesWarning(ev);
+ mNode_Refs = mNode_Uses = refs = uses;
+ }
+ if (refs < morkNode_kMaxRefCount) // not too great?
+ {
+ mNode_Refs = ++refs;
+ mNode_Uses = ++uses;
+ } else
+ this->RefsOverflowWarning(ev);
+
+ outUses = uses;
+ } else
+ this->NonNodeError(ev);
+ return outUses;
+}
+
+/*private non-poly*/ mork_bool morkNode::cut_use_count(
+ morkEnv* ev) // just one part of CutStrongRef()
+{
+ mork_bool didCut = morkBool_kFalse;
+ if (this->IsNode()) {
+ mork_uses uses = mNode_Uses;
+ if (uses) // not yet zero?
+ mNode_Uses = --uses;
+ else
+ this->UsesUnderflowWarning(ev);
+
+ didCut = morkBool_kTrue;
+ if (!mNode_Uses) // last use gone? time to close node?
+ {
+ if (this->IsOpenNode()) {
+ if (!mNode_Refs) // no outstanding reference?
+ {
+ this->RefsUnderflowWarning(ev);
+ ++mNode_Refs; // prevent potential crash during close
+ }
+ this->CloseMorkNode(ev); // polymorphic self close
+ // (Note CutNode() is not polymorphic -- so don't call that.)
+ }
+ }
+ } else
+ this->NonNodeError(ev);
+ return didCut;
+}
+
+/*public non-poly*/ mork_uses morkNode::CutStrongRef(morkEnv* ev) {
+ mork_refs outRefs = 0;
+ if (this->IsNode()) {
+ if (this->cut_use_count(ev)) outRefs = this->CutWeakRef(ev);
+ } else
+ this->NonNodeError(ev);
+
+ return outRefs;
+}
+
+/*public non-poly*/ mork_refs morkNode::AddWeakRef(morkEnv* ev) {
+ mork_refs outRefs = 0;
+ if (this->IsNode()) {
+ mork_refs refs = mNode_Refs;
+ if (refs < morkNode_kMaxRefCount) // not too great?
+ mNode_Refs = ++refs;
+ else
+ this->RefsOverflowWarning(ev);
+
+ outRefs = refs;
+ } else
+ this->NonNodeError(ev);
+
+ return outRefs;
+}
+
+/*public non-poly*/ mork_refs morkNode::CutWeakRef(morkEnv* ev) {
+ mork_refs outRefs = 0;
+ if (this->IsNode()) {
+ mork_uses uses = mNode_Uses;
+ mork_refs refs = mNode_Refs;
+ if (refs) // not yet zero?
+ mNode_Refs = --refs;
+ else
+ this->RefsUnderflowWarning(ev);
+
+ if (refs < uses) // need to fix broken refs/uses relation?
+ {
+ this->RefsUnderUsesWarning(ev);
+ mNode_Refs = mNode_Uses = refs = uses;
+ }
+
+ outRefs = refs;
+ if (!refs) // last reference gone? time to destroy node?
+ this->ZapOld(ev, mNode_Heap); // self destroy, use this no longer
+ } else
+ this->NonNodeError(ev);
+
+ return outRefs;
+}
+
+static const char morkNode_kBroken[] = "broken";
+
+/*public non-poly*/ const char* morkNode::GetNodeAccessAsString()
+ const // e.g. "open", "shut", etc.
+{
+ const char* outString = morkNode_kBroken;
+ switch (mNode_Access) {
+ case morkAccess_kOpen:
+ outString = "open";
+ break;
+ case morkAccess_kClosing:
+ outString = "closing";
+ break;
+ case morkAccess_kShut:
+ outString = "shut";
+ break;
+ case morkAccess_kDead:
+ outString = "dead";
+ break;
+ }
+ return outString;
+}
+
+/*public non-poly*/ const char* morkNode::GetNodeUsageAsString()
+ const // e.g. "heap", "stack", etc.
+{
+ const char* outString = morkNode_kBroken;
+ switch (mNode_Usage) {
+ case morkUsage_kHeap:
+ outString = "heap";
+ break;
+ case morkUsage_kStack:
+ outString = "stack";
+ break;
+ case morkUsage_kMember:
+ outString = "member";
+ break;
+ case morkUsage_kGlobal:
+ outString = "global";
+ break;
+ case morkUsage_kPool:
+ outString = "pool";
+ break;
+ case morkUsage_kNone:
+ outString = "none";
+ break;
+ }
+ return outString;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkNode.h b/comm/mailnews/db/mork/morkNode.h
new file mode 100644
index 0000000000..6518487a30
--- /dev/null
+++ b/comm/mailnews/db/mork/morkNode.h
@@ -0,0 +1,290 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORKNODE_
+#define _MORKNODE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkUsage_kHeap 'h'
+#define morkUsage_kStack 's'
+#define morkUsage_kMember 'm'
+#define morkUsage_kGlobal 'g'
+#define morkUsage_kPool 'p'
+#define morkUsage_kNone 'n'
+
+class morkUsage {
+ public:
+ mork_usage mUsage_Code; // kHeap, kStack, kMember, or kGhost
+
+ public:
+ explicit morkUsage(mork_usage inCode);
+
+ morkUsage(); // does nothing except maybe call EnsureReadyStaticUsage()
+ void InitUsage(mork_usage inCode) { mUsage_Code = inCode; }
+
+ ~morkUsage() {}
+ mork_usage Code() const { return mUsage_Code; }
+
+ static void EnsureReadyStaticUsage();
+
+ public:
+ static const morkUsage& kHeap; // morkUsage_kHeap
+ static const morkUsage& kStack; // morkUsage_kStack
+ static const morkUsage& kMember; // morkUsage_kMember
+ static const morkUsage& kGlobal; // morkUsage_kGlobal
+ static const morkUsage& kPool; // morkUsage_kPool
+ static const morkUsage& kNone; // morkUsage_kNone
+
+ static const morkUsage& GetHeap(); // kHeap, safe at static init time
+ static const morkUsage& GetStack(); // kStack, safe at static init time
+ static const morkUsage& GetMember(); // kMember, safe at static init time
+ static const morkUsage& GetGlobal(); // kGlobal, safe at static init time
+ static const morkUsage& GetPool(); // kPool, safe at static init time
+ static const morkUsage& GetNone(); // kNone, safe at static init time
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkNode_kMaxRefCount 0x0FFFF /* count sticks if it hits this */
+
+#define morkBase_kNode /*i*/ 0x4E64 /* ascii 'Nd' */
+
+/*| morkNode: several groups of two-byte integers that track the basic
+**| status of an object that can be used to compose in-memory graphs.
+**| This is the base class for nsIMdbObject (which adds members that fit
+**| the needs of an nsIMdbObject subclass). The morkNode class is also used
+**| as the base class for other Mork db classes with no strong relation to
+**| the MDB class hierarchy.
+**|
+**|| Heap: the heap in which this node was allocated, when the usage equals
+**| morkUsage_kHeap to show dynamic allocation. Note this heap is NOT ref-
+**| counted, because that would be too great and complex a burden for all
+**| the nodes allocated in that heap. So heap users should take care to
+**| understand that nodes allocated in that heap are considered protected
+**| by some inclusive context in which all those nodes are allocated, and
+**| that context must maintain at least one strong refcount for the heap.
+**| Occasionally a node subclass will indeed wish to hold a refcounted
+**| reference to a heap, and possibly the same heap that is in mNode_Heap,
+**| but this is always done in a separate slot that explicitly refcounts,
+**| so we avoid confusing what is meant by the mNode_Heap slot.
+|*/
+class morkNode /*: public nsISupports */ { // base class for constructing Mork
+ // object graphs
+
+ public: // state is public because the entire Mork system is private
+ // NS_DECL_ISUPPORTS;
+ nsIMdbHeap* mNode_Heap; // NON-refcounted heap pointer
+
+ mork_base mNode_Base; // must equal morkBase_kNode
+ mork_derived mNode_Derived; // depends on specific node subclass
+
+ mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ mork_able mNode_Mutable; // can this node be modified?
+ mork_load mNode_Load; // is this node clean or dirty?
+
+ mork_uses mNode_Uses; // refcount for strong refs
+ mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ protected: // special case empty construction for morkHandleFrame
+ friend class morkHandleFrame;
+ morkNode() {}
+
+ public: // inlines for weird mNode_Mutable and mNode_Load constants
+ void SetFrozen() { mNode_Mutable = morkAble_kDisabled; }
+ void SetMutable() { mNode_Mutable = morkAble_kEnabled; }
+ void SetAsleep() { mNode_Mutable = morkAble_kAsleep; }
+
+ mork_bool IsFrozen() const { return mNode_Mutable == morkAble_kDisabled; }
+ mork_bool IsMutable() const { return mNode_Mutable == morkAble_kEnabled; }
+ mork_bool IsAsleep() const { return mNode_Mutable == morkAble_kAsleep; }
+
+ void SetNodeClean() { mNode_Load = morkLoad_kClean; }
+ void SetNodeDirty() { mNode_Load = morkLoad_kDirty; }
+
+ mork_bool IsNodeClean() const { return mNode_Load == morkLoad_kClean; }
+ mork_bool IsNodeDirty() const { return mNode_Load == morkLoad_kDirty; }
+
+ public: // morkNode memory management methods
+ static void* MakeNew(size_t inSize, nsIMdbHeap& ioHeap, morkEnv* ev);
+
+ void ZapOld(morkEnv* ev, nsIMdbHeap* ioHeap); // replaces operator delete()
+ // this->morkNode::~morkNode(); // first call polymorphic destructor
+ // if ( ioHeap ) // was this node heap allocated?
+ // ioHeap->Free(ev->AsMdbEnv(), this);
+
+ public: // morkNode memory management operators
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkNode::MakeNew(inSize, ioHeap, ev);
+ }
+
+ protected: // construction without an anv needed for first env constructed:
+ morkNode(const morkUsage& inUsage, nsIMdbHeap* ioHeap);
+
+ explicit morkNode(mork_usage inCode); // usage == inCode, heap == nil
+
+ // { ===== begin basic node interface =====
+ public: // morkNode virtual methods
+ // virtual FlushMorkNode(morkEnv* ev, morkStream* ioStream);
+ // virtual WriteMorkNode(morkEnv* ev, morkStream* ioStream);
+
+ virtual ~morkNode(); // assert that CloseNode() executed earlier
+ virtual void CloseMorkNode(morkEnv* ev); // CloseNode() only if open
+
+ // CloseMorkNode() is the polymorphic close method called when uses==0,
+ // which must do NOTHING at all when IsOpenNode() is not true. Otherwise,
+ // CloseMorkNode() should call a static close method specific to an object.
+ // Each such static close method should either call inherited static close
+ // methods, or else perform the consolidated effect of calling them, where
+ // subclasses should closely track any changes in base classes with care.
+
+ public: // morkNode construction
+ morkNode(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap);
+ void CloseNode(morkEnv* ev); // called by CloseMorkNode();
+ nsresult CloseMdbObject(morkEnv* ev);
+ NS_IMETHOD CloseMdbObject(nsIMdbEnv* ev);
+
+ private: // copying is not allowed
+ morkNode(const morkNode& other);
+ morkNode& operator=(const morkNode& other);
+
+ public: // dynamic type identification
+ mork_bool IsNode() const { return mNode_Base == morkBase_kNode; }
+ // } ===== end basic node methods =====
+
+ public: // public error & warning methods
+ void RefsUnderUsesWarning(
+ morkEnv* ev) const; // call if mNode_Refs < mNode_Uses
+ void NonNodeError(morkEnv* ev) const; // call when IsNode() is false
+ void NilHeapError(morkEnv* ev) const; // zero mNode_Heap when usage is kHeap
+ void NonOpenNodeError(morkEnv* ev) const; // call when IsOpenNode() is false
+
+ void NonMutableNodeError(morkEnv* ev) const; // when IsMutable() is false
+
+ void RefsOverflowWarning(morkEnv* ev) const; // call on mNode_Refs overflow
+ void UsesOverflowWarning(morkEnv* ev) const; // call on mNode_Uses overflow
+ void RefsUnderflowWarning(morkEnv* ev) const; // call on mNode_Refs underflow
+ void UsesUnderflowWarning(morkEnv* ev) const; // call on mNode_Uses underflow
+
+ private: // private refcounting methods
+ mork_bool cut_use_count(morkEnv* ev); // just one part of CutStrongRef()
+
+ public: // other morkNode methods
+ mork_bool GoodRefs() const { return mNode_Refs >= mNode_Uses; }
+ mork_bool BadRefs() const { return mNode_Refs < mNode_Uses; }
+
+ mork_uses StrongRefsOnly() const { return mNode_Uses; }
+ mork_refs WeakRefsOnly() const {
+ return (mork_refs)(mNode_Refs - mNode_Uses);
+ }
+
+ // (this refcounting derives from public domain IronDoc node refcounts)
+ virtual mork_uses AddStrongRef(morkEnv* ev);
+ virtual mork_uses CutStrongRef(morkEnv* ev);
+ mork_refs AddWeakRef(morkEnv* ev);
+ mork_refs CutWeakRef(morkEnv* ev);
+
+ const char* GetNodeAccessAsString() const; // e.g. "open", "shut", etc.
+ const char* GetNodeUsageAsString() const; // e.g. "heap", "stack", etc.
+
+ mork_usage NodeUsage() const { return mNode_Usage; }
+
+ mork_bool IsHeapNode() const { return mNode_Usage == morkUsage_kHeap; }
+
+ mork_bool IsOpenNode() const { return mNode_Access == morkAccess_kOpen; }
+
+ mork_bool IsShutNode() const { return mNode_Access == morkAccess_kShut; }
+
+ mork_bool IsDeadNode() const { return mNode_Access == morkAccess_kDead; }
+
+ mork_bool IsClosingNode() const {
+ return mNode_Access == morkAccess_kClosing;
+ }
+
+ mork_bool IsOpenOrClosingNode() const {
+ return IsOpenNode() || IsClosingNode();
+ }
+
+ mork_bool HasNodeAccess() const {
+ return (IsOpenNode() || IsShutNode() || IsClosingNode());
+ }
+
+ void MarkShut() { mNode_Access = morkAccess_kShut; }
+ void MarkClosing() { mNode_Access = morkAccess_kClosing; }
+ void MarkDead() { mNode_Access = morkAccess_kDead; }
+
+ public: // refcounting for typesafe subclass inline methods
+ static void SlotWeakNode(morkNode* me, morkEnv* ev, morkNode** ioSlot);
+ // If *ioSlot is non-nil, that node is released by CutWeakRef() and
+ // then zeroed out. Then if me is non-nil, this is acquired by
+ // calling AddWeakRef(), and if positive is returned to show success,
+ // then this is put into slot *ioSlot. Note me can be nil, so we
+ // permit expression '((morkNode*) 0L)->SlotWeakNode(ev, &slot)'.
+
+ static void SlotStrongNode(morkNode* me, morkEnv* ev, morkNode** ioSlot);
+ // If *ioSlot is non-nil, that node is released by CutStrongRef() and
+ // then zeroed out. Then if me is non-nil, this is acquired by
+ // calling AddStrongRef(), and if positive is returned to show success,
+ // then me is put into slot *ioSlot. Note me can be nil, so we take
+ // expression 'morkNode::SlotStrongNode((morkNode*) 0, ev, &slot)'.
+};
+
+extern void // utility method very similar to morkNode::SlotStrongNode():
+nsIMdbHeap_SlotStrongHeap(nsIMdbHeap* self, morkEnv* ev, nsIMdbHeap** ioSlot);
+// If *ioSlot is non-nil, that heap is released by CutStrongRef() and
+// then zeroed out. Then if self is non-nil, this is acquired by
+// calling AddStrongRef(), and if the return value shows success,
+// then self is put into slot *ioSlot. Note self can be nil, so we take
+// expression 'nsIMdbHeap_SlotStrongHeap(0, ev, &slot)'.
+
+extern void // utility method very similar to morkNode::SlotStrongNode():
+nsIMdbFile_SlotStrongFile(nsIMdbFile* self, morkEnv* ev, nsIMdbFile** ioSlot);
+// If *ioSlot is non-nil, that file is released by CutStrongRef() and
+// then zeroed out. Then if self is non-nil, this is acquired by
+// calling AddStrongRef(), and if the return value shows success,
+// then self is put into slot *ioSlot. Note self can be nil, so we take
+// expression 'nsIMdbFile_SlotStrongFile(0, ev, &slot)'.
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKNODE_ */
diff --git a/comm/mailnews/db/mork/morkNodeMap.cpp b/comm/mailnews/db/mork/morkNodeMap.cpp
new file mode 100644
index 0000000000..a01b688b16
--- /dev/null
+++ b/comm/mailnews/db/mork/morkNodeMap.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKINTMAP_
+# include "morkIntMap.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkNodeMap::CloseMorkNode(
+ morkEnv* ev) // CloseNodeMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseNodeMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkNodeMap::~morkNodeMap() // assert CloseNodeMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkNodeMap::morkNodeMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkIntMap(ev, inUsage, /*valsize*/ sizeof(morkNode*), ioHeap, ioSlotHeap,
+ /*inHoldChanges*/ morkBool_kTrue) {
+ if (ev->Good()) mNode_Derived = morkDerived_kNodeMap;
+}
+
+/*public non-poly*/ void morkNodeMap::CloseNodeMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CutAllNodes(ev);
+ this->CloseMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+mork_bool morkNodeMap::AddNode(morkEnv* ev, mork_token inToken,
+ morkNode* ioNode)
+// the AddNode() method return value equals ev->Good().
+{
+ if (ioNode && ev->Good()) {
+ morkNode* node = 0; // old val in the map
+
+ mork_bool put = this->Put(ev, &inToken, &ioNode,
+ /*key*/ (void*)0, &node, (mork_change**)0);
+
+ if (put) // replaced an existing value for key inToken?
+ {
+ if (node && node != ioNode) // need to release old node?
+ node->CutStrongRef(ev);
+ }
+
+ if (ev->Bad() || !ioNode->AddStrongRef(ev)) {
+ // problems adding node or increasing refcount?
+ this->Cut(ev, &inToken, // make sure not in map
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+ }
+ } else if (!ioNode)
+ ev->NilPointerError();
+
+ return ev->Good();
+}
+
+mork_bool morkNodeMap::CutNode(morkEnv* ev, mork_token inToken) {
+ morkNode* node = 0; // old val in the map
+ mork_bool outCutNode = this->Cut(ev, &inToken,
+ /*key*/ (void*)0, &node, (mork_change**)0);
+ if (node) node->CutStrongRef(ev);
+
+ return outCutNode;
+}
+
+morkNode* morkNodeMap::GetNode(morkEnv* ev, mork_token inToken)
+// Note the returned node does NOT have an increase in refcount for this.
+{
+ morkNode* node = 0; // old val in the map
+ this->Get(ev, &inToken, /*key*/ (void*)0, &node, (mork_change**)0);
+
+ return node;
+}
+
+mork_num morkNodeMap::CutAllNodes(morkEnv* ev)
+// CutAllNodes() releases all the reference node values.
+{
+ mork_num outSlots = mMap_Slots;
+ mork_token key = 0; // old key token in the map
+ morkNode* val = 0; // old val node in the map
+
+ mork_change* c = 0;
+ morkNodeMapIter i(ev, this);
+ for (c = i.FirstNode(ev, &key, &val); c; c = i.NextNode(ev, &key, &val)) {
+ if (val) val->CutStrongRef(ev);
+ i.CutHereNode(ev, /*key*/ (mork_token*)0, /*val*/ (morkNode**)0);
+ }
+
+ return outSlots;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkNodeMap.h b/comm/mailnews/db/mork/morkNodeMap.h
new file mode 100644
index 0000000000..c2edc7007e
--- /dev/null
+++ b/comm/mailnews/db/mork/morkNodeMap.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKNODEMAP_
+#define _MORKNODEMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKINTMAP_
+# include "morkIntMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kNodeMap /*i*/ 0x6E4D /* ascii 'nM' */
+
+#define morkNodeMap_kStartSlotCount 512
+
+/*| morkNodeMap: maps mork_token -> morkNode
+|*/
+class morkNodeMap : public morkIntMap { // for mapping tokens to nodes
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseNodeMap() only if open
+ virtual ~morkNodeMap(); // assert that CloseNodeMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkNodeMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseNodeMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsNodeMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kNodeMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkMap poly interface =====
+ // use the Equal() and Hash() for mork_u4 inherited from morkIntMap
+ // } ===== end morkMap poly interface =====
+
+ protected: // we want all subclasses to provide typesafe wrappers:
+ mork_bool AddNode(morkEnv* ev, mork_token inToken, morkNode* ioNode);
+ // the AddNode() boolean return equals ev->Good().
+
+ mork_bool CutNode(morkEnv* ev, mork_token inToken);
+ // The CutNode() boolean return indicates whether removal happened.
+
+ morkNode* GetNode(morkEnv* ev, mork_token inToken);
+ // Note the returned node does NOT have an increase in refcount for this.
+
+ mork_num CutAllNodes(morkEnv* ev);
+ // CutAllNodes() releases all the reference node values.
+};
+
+class morkNodeMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkNodeMapIter(morkEnv* ev, morkNodeMap* ioMap) : morkMapIter(ev, ioMap) {}
+
+ morkNodeMapIter() : morkMapIter() {}
+ void InitNodeMapIter(morkEnv* ev, morkNodeMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstNode(morkEnv* ev, mork_token* outToken,
+ morkNode** outNode) {
+ return this->First(ev, outToken, outNode);
+ }
+
+ mork_change* NextNode(morkEnv* ev, mork_token* outToken, morkNode** outNode) {
+ return this->Next(ev, outToken, outNode);
+ }
+
+ mork_change* HereNode(morkEnv* ev, mork_token* outToken, morkNode** outNode) {
+ return this->Here(ev, outToken, outNode);
+ }
+
+ mork_change* CutHereNode(morkEnv* ev, mork_token* outToken,
+ morkNode** outNode) {
+ return this->CutHere(ev, outToken, outNode);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKNODEMAP_ */
diff --git a/comm/mailnews/db/mork/morkObject.cpp b/comm/mailnews/db/mork/morkObject.cpp
new file mode 100644
index 0000000000..227fa81f08
--- /dev/null
+++ b/comm/mailnews/db/mork/morkObject.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKHANDLE_
+# include "morkHandle.h"
+#endif
+
+#include "nsCOMPtr.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+NS_IMPL_ISUPPORTS(morkObject, nsIMdbObject)
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkObject::CloseMorkNode(
+ morkEnv* ev) // CloseObject() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseObject(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkObject::~morkObject() // assert CloseObject() executed earlier
+{
+ if (!IsShutNode()) CloseMorkNode(this->mMorkEnv);
+ MORK_ASSERT(mObject_Handle == 0);
+}
+
+/*public non-poly*/
+morkObject::morkObject(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor)
+ : morkBead(inUsage, ioHeap, inBeadColor), mObject_Handle(0) {
+ mMorkEnv = nullptr;
+}
+
+/*public non-poly*/
+morkObject::morkObject(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, mork_color inBeadColor,
+ morkHandle* ioHandle)
+ : morkBead(ev, inUsage, ioHeap, inBeadColor), mObject_Handle(0) {
+ mMorkEnv = ev;
+ if (ev->Good()) {
+ if (ioHandle) morkHandle::SlotWeakHandle(ioHandle, ev, &mObject_Handle);
+
+ if (ev->Good()) mNode_Derived = morkDerived_kObject;
+ }
+}
+
+/*public non-poly*/ void morkObject::CloseObject(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ if (!this->IsShutNode()) {
+ if (mObject_Handle)
+ morkHandle::SlotWeakHandle((morkHandle*)0L, ev, &mObject_Handle);
+
+ mBead_Color = 0; // this->CloseBead(ev);
+ this->MarkShut();
+ }
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ----- begin factory methods -----
+NS_IMETHODIMP
+morkObject::GetMdbFactory(nsIMdbEnv* mev, nsIMdbFactory** acqFactory) {
+ nsresult rv;
+ nsCOMPtr<nsIMdbObject> obj = do_QueryInterface(mev);
+ if (obj)
+ rv = obj->GetMdbFactory(mev, acqFactory);
+ else
+ return NS_ERROR_NO_INTERFACE;
+
+ return rv;
+}
+// } ----- end factory methods -----
+
+// { ----- begin ref counting for well-behaved cyclic graphs -----
+NS_IMETHODIMP
+morkObject::GetWeakRefCount(nsIMdbEnv* mev, // weak refs
+ mdb_count* outCount) {
+ *outCount = WeakRefsOnly();
+ return NS_OK;
+}
+NS_IMETHODIMP
+morkObject::GetStrongRefCount(nsIMdbEnv* mev, // strong refs
+ mdb_count* outCount) {
+ *outCount = StrongRefsOnly();
+ return NS_OK;
+}
+// ### TODO - clean up this cast, if required
+NS_IMETHODIMP
+morkObject::AddWeakRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::AddWeakRef((morkEnv*)mev));
+}
+
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkObject::AddStrongRef(morkEnv* mev) { return morkNode::AddStrongRef(mev); }
+#endif
+
+NS_IMETHODIMP_(mork_uses)
+morkObject::AddStrongRef(nsIMdbEnv* mev) {
+ return morkNode::AddStrongRef((morkEnv*)mev);
+}
+
+NS_IMETHODIMP
+morkObject::CutWeakRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::CutWeakRef((morkEnv*)mev));
+}
+
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkObject::CutStrongRef(morkEnv* mev) { return morkNode::CutStrongRef(mev); }
+#endif
+
+NS_IMETHODIMP
+morkObject::CutStrongRef(nsIMdbEnv* mev) {
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::CutStrongRef((morkEnv*)mev));
+}
+
+NS_IMETHODIMP
+morkObject::CloseMdbObject(nsIMdbEnv* mev) {
+ return morkNode::CloseMdbObject((morkEnv*)mev);
+}
+
+NS_IMETHODIMP
+morkObject::IsOpenMdbObject(nsIMdbEnv* mev, mdb_bool* outOpen) {
+ *outOpen = IsOpenNode();
+ return NS_OK;
+}
+NS_IMETHODIMP
+morkObject::IsFrozenMdbObject(nsIMdbEnv* mev, mdb_bool* outIsReadonly) {
+ *outIsReadonly = IsFrozen();
+ return NS_OK;
+}
+
+// void morkObject::NewNilHandleError(morkEnv* ev) // mObject_Handle is nil
+//{
+// ev->NewError("nil mObject_Handle");
+//}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkObject.h b/comm/mailnews/db/mork/morkObject.h
new file mode 100644
index 0000000000..9548c779d1
--- /dev/null
+++ b/comm/mailnews/db/mork/morkObject.h
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKOBJECT_
+#define _MORKOBJECT_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKBEAD_
+# include "morkBead.h"
+#endif
+
+#ifndef _MORKCONFIG_
+# include "morkConfig.h"
+#endif
+
+#ifndef _ORKINHEAP_
+# include "orkinHeap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kObject /*i*/ 0x6F42 /* ascii 'oB' */
+
+/*| morkObject: subclass of morkNode that adds knowledge of db suite factory
+**| and containing port to those objects that are exposed as instances of
+**| nsIMdbObject in the public interface.
+|*/
+class morkObject : public morkBead, public nsIMdbObject {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+
+ public: // state is public because the entire Mork system is private
+ morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ morkEnv* mMorkEnv; // weak ref to environment this object created in.
+
+ // { ===== begin morkNode interface =====
+ public:
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseObject() only if open
+#ifdef MORK_DEBUG_HEAP_STATS
+ void operator delete(void* ioAddress, size_t size) {
+ mork_u4* array = (mork_u4*)ioAddress;
+ array -= 3;
+ orkinHeap* heap = (orkinHeap*)*array;
+ if (heap) heap->Free(nullptr, ioAddress);
+ }
+#endif
+
+ NS_DECL_ISUPPORTS
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD IsFrozenMdbObject(nsIMdbEnv* ev, mdb_bool* outIsReadonly) override;
+ // same as nsIMdbPort::GetIsPortReadonly() when this object is inside a port.
+ // } ----- end attribute methods -----
+
+ // { ----- begin factory methods -----
+ NS_IMETHOD GetMdbFactory(nsIMdbEnv* ev, nsIMdbFactory** acqFactory) override;
+ // } ----- end factory methods -----
+
+ // { ----- begin ref counting for well-behaved cyclic graphs -----
+ NS_IMETHOD GetWeakRefCount(nsIMdbEnv* ev, // weak refs
+ mdb_count* outCount) override;
+ NS_IMETHOD GetStrongRefCount(nsIMdbEnv* ev, // strong refs
+ mdb_count* outCount) override;
+
+ NS_IMETHOD AddWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of AddStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) AddStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD_(mork_uses) AddStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CutWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of CutStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) CutStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD CutStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CloseMdbObject(
+ nsIMdbEnv* ev) override; // called at strong refs zero
+ NS_IMETHOD IsOpenMdbObject(nsIMdbEnv* ev, mdb_bool* outOpen) override;
+ // } ----- end ref counting -----
+
+ protected: // special case construction of first env without preceding env
+ morkObject(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor);
+ virtual ~morkObject(); // assert that CloseObject() executed earlier
+
+ public: // morkEnv construction & destruction
+ morkObject(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ mork_color inBeadColor,
+ morkHandle* ioHandle); // ioHandle can be nil
+ void CloseObject(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkObject(const morkObject& other);
+ morkObject& operator=(const morkObject& other);
+
+ public: // dynamic type identification
+ mork_bool IsObject() const {
+ return IsNode() && mNode_Derived == morkDerived_kObject;
+ }
+ // } ===== end morkNode methods =====
+
+ // void NewNilHandleError(morkEnv* ev); // mObject_Handle is nil
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakObject(morkObject* me, morkEnv* ev, morkObject** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongObject(morkObject* me, morkEnv* ev,
+ morkObject** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKOBJECT_ */
diff --git a/comm/mailnews/db/mork/morkParser.cpp b/comm/mailnews/db/mork/morkParser.cpp
new file mode 100644
index 0000000000..8ca635014f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkParser.cpp
@@ -0,0 +1,1331 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKPARSER_
+# include "morkParser.h"
+#endif
+
+#ifndef _MORKSTREAM_
+# include "morkStream.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKSINK_
+# include "morkSink.h"
+#endif
+
+#ifndef _MORKCH_
+# include "morkCh.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkParser::CloseMorkNode(
+ morkEnv* ev) // CloseParser() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseParser(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkParser::~morkParser() // assert CloseParser() executed earlier
+{
+ MORK_ASSERT(mParser_Heap == 0);
+ MORK_ASSERT(mParser_Stream == 0);
+}
+
+/*public non-poly*/
+morkParser::morkParser(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkStream* ioStream,
+ mdb_count inBytesPerParseSegment, nsIMdbHeap* ioSlotHeap)
+ : morkNode(ev, inUsage, ioHeap),
+ mParser_Heap(0),
+ mParser_Stream(0),
+ mParser_MoreGranularity(inBytesPerParseSegment),
+ mParser_State(morkParser_kStartState)
+
+ ,
+ mParser_GroupContentStartPos(0)
+
+ ,
+ mParser_TableMid(),
+ mParser_RowMid(),
+ mParser_CellMid()
+
+ ,
+ mParser_InPort(morkBool_kFalse),
+ mParser_InDict(morkBool_kFalse),
+ mParser_InCell(morkBool_kFalse),
+ mParser_InMeta(morkBool_kFalse)
+
+ ,
+ mParser_InPortRow(morkBool_kFalse),
+ mParser_InRow(morkBool_kFalse),
+ mParser_InTable(morkBool_kFalse),
+ mParser_InGroup(morkBool_kFalse)
+
+ ,
+ mParser_AtomChange(morkChange_kNil),
+ mParser_CellChange(morkChange_kNil),
+ mParser_RowChange(morkChange_kNil),
+ mParser_TableChange(morkChange_kNil)
+
+ ,
+ mParser_Change(morkChange_kNil),
+ mParser_IsBroken(morkBool_kFalse),
+ mParser_IsDone(morkBool_kFalse),
+ mParser_DoMore(morkBool_kTrue)
+
+ ,
+ mParser_Mid()
+
+ ,
+ mParser_ScopeCoil(ev, ioSlotHeap),
+ mParser_ValueCoil(ev, ioSlotHeap),
+ mParser_ColumnCoil(ev, ioSlotHeap),
+ mParser_StringCoil(ev, ioSlotHeap)
+
+ ,
+ mParser_ScopeSpool(ev, &mParser_ScopeCoil),
+ mParser_ValueSpool(ev, &mParser_ValueCoil),
+ mParser_ColumnSpool(ev, &mParser_ColumnCoil),
+ mParser_StringSpool(ev, &mParser_StringCoil)
+
+ ,
+ mParser_MidYarn(ev, morkUsage(morkUsage_kMember), ioSlotHeap) {
+ if (inBytesPerParseSegment < morkParser_kMinGranularity)
+ inBytesPerParseSegment = morkParser_kMinGranularity;
+ else if (inBytesPerParseSegment > morkParser_kMaxGranularity)
+ inBytesPerParseSegment = morkParser_kMaxGranularity;
+
+ mParser_MoreGranularity = inBytesPerParseSegment;
+
+ if (ioSlotHeap && ioStream) {
+ nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mParser_Heap);
+ morkStream::SlotStrongStream(ioStream, ev, &mParser_Stream);
+
+ if (ev->Good()) {
+ mParser_Tag = morkParser_kTag;
+ mNode_Derived = morkDerived_kParser;
+ }
+ } else
+ ev->NilPointerError();
+}
+
+/*public non-poly*/ void morkParser::CloseParser(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ if (!this->IsShutNode()) {
+ mParser_ScopeCoil.CloseCoil(ev);
+ mParser_ValueCoil.CloseCoil(ev);
+ mParser_ColumnCoil.CloseCoil(ev);
+ mParser_StringCoil.CloseCoil(ev);
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mParser_Heap);
+ morkStream::SlotStrongStream((morkStream*)0, ev, &mParser_Stream);
+ this->MarkShut();
+ }
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*protected non-poly*/ void morkParser::NonGoodParserError(
+ morkEnv* ev) // when GoodParserTag() is false
+{
+ ev->NewError("non-morkNode");
+}
+
+/*protected non-poly*/ void morkParser::NonUsableParserError(morkEnv* ev) //
+{
+ if (this->IsNode()) {
+ if (this->IsOpenNode()) {
+ if (this->GoodParserTag()) {
+ // okay
+ } else
+ this->NonGoodParserError(ev);
+ } else
+ this->NonOpenNodeError(ev);
+ } else
+ this->NonNodeError(ev);
+}
+
+/*protected non-poly*/ void morkParser::StartParse(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mParser_InCell = morkBool_kFalse;
+ mParser_InMeta = morkBool_kFalse;
+ mParser_InDict = morkBool_kFalse;
+ mParser_InPortRow = morkBool_kFalse;
+
+ mParser_RowMid.ClearMid();
+ mParser_TableMid.ClearMid();
+ mParser_CellMid.ClearMid();
+
+ mParser_GroupId = 0;
+ mParser_InPort = morkBool_kTrue;
+
+ mParser_GroupSpan.ClearSpan();
+ mParser_DictSpan.ClearSpan();
+ mParser_AliasSpan.ClearSpan();
+ mParser_MetaSpan.ClearSpan();
+ mParser_TableSpan.ClearSpan();
+ mParser_RowSpan.ClearSpan();
+ mParser_CellSpan.ClearSpan();
+ mParser_ColumnSpan.ClearSpan();
+ mParser_SlotSpan.ClearSpan();
+
+ mParser_PortSpan.ClearSpan();
+}
+
+/*protected non-poly*/ void morkParser::StopParse(morkEnv* ev) {
+ if (mParser_InCell) {
+ mParser_InCell = morkBool_kFalse;
+ mParser_CellSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnCellEnd(ev, mParser_CellSpan);
+ }
+ if (mParser_InMeta) {
+ mParser_InMeta = morkBool_kFalse;
+ mParser_MetaSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnMetaEnd(ev, mParser_MetaSpan);
+ }
+ if (mParser_InDict) {
+ mParser_InDict = morkBool_kFalse;
+ mParser_DictSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnDictEnd(ev, mParser_DictSpan);
+ }
+ if (mParser_InPortRow) {
+ mParser_InPortRow = morkBool_kFalse;
+ mParser_RowSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnPortRowEnd(ev, mParser_RowSpan);
+ }
+ if (mParser_InRow) {
+ mParser_InRow = morkBool_kFalse;
+ mParser_RowMid.ClearMid();
+ mParser_RowSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnRowEnd(ev, mParser_RowSpan);
+ }
+ if (mParser_InTable) {
+ mParser_InTable = morkBool_kFalse;
+ mParser_TableMid.ClearMid();
+ mParser_TableSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnTableEnd(ev, mParser_TableSpan);
+ }
+ if (mParser_GroupId) {
+ mParser_GroupId = 0;
+ mParser_GroupSpan.SetEndWithEnd(mParser_PortSpan);
+ this->OnGroupAbortEnd(ev, mParser_GroupSpan);
+ }
+ if (mParser_InPort) {
+ mParser_InPort = morkBool_kFalse;
+ this->OnPortEnd(ev, mParser_PortSpan);
+ }
+}
+
+int morkParser::eat_comment(morkEnv* ev) // last char was '/'
+{
+ morkStream* s = mParser_Stream;
+ // Note morkStream::Getc() returns EOF when an error occurs, so
+ // we don't need to check for both c != EOF and ev->Good() below.
+
+ int c = s->Getc(ev);
+ if (c == '/') // C++ style comment?
+ {
+ while ((c = s->Getc(ev)) != EOF && c != 0xA && c != 0xD)
+ ; /* empty */
+
+ if (c == 0xA || c == 0xD) c = this->eat_line_break(ev, c);
+ } else if (c == '*') /* C style comment? */
+ {
+ int depth = 1; // count depth of comments until depth reaches zero
+
+ while (depth > 0 && c != EOF) // still looking for comment end(s)?
+ {
+ while ((c = s->Getc(ev)) != EOF && c != '/' && c != '*') {
+ if (c == 0xA || c == 0xD) // need to count a line break?
+ {
+ c = this->eat_line_break(ev, c);
+ if (c == '/' || c == '*') break; // end while loop
+ }
+ }
+
+ if (c == '*') // maybe end of a comment, if next char is '/'?
+ {
+ if ((c = s->Getc(ev)) == '/') // end of comment?
+ {
+ --depth; // depth of comments has decreased by one
+ if (!depth) // comments all done?
+ c = s->Getc(ev); // return the byte after end of comment
+ } else if (c != EOF) // need to put the char back?
+ s->Ungetc(c); // especially need to put back '*', 0xA, or 0xD
+ } else if (c == '/') // maybe nested comemnt, if next char is '*'?
+ {
+ if ((c = s->Getc(ev)) == '*') // nested comment?
+ ++depth; // depth of comments has increased by one
+ else if (c != EOF) // need to put the char back?
+ s->Ungetc(c); // especially need to put back '/', 0xA, or 0xD
+ }
+
+ if (ev->Bad()) c = EOF;
+ }
+ if (c == EOF && depth > 0) ev->NewWarning("EOF before end of comment");
+ } else
+ ev->NewWarning("expected / or *");
+
+ return c;
+}
+
+int morkParser::eat_line_break(morkEnv* ev, int inLast) {
+ morkStream* s = mParser_Stream;
+ int c = s->Getc(ev); // get next char after 0xA or 0xD
+ this->CountLineBreak();
+ if (c == 0xA || c == 0xD) // another line break character?
+ {
+ if (c != inLast) // not the same as the last one?
+ c = s->Getc(ev); // get next char after two-byte linebreak
+ }
+ return c;
+}
+
+int morkParser::eat_line_continue(morkEnv* ev) // last char was '\'
+{
+ morkStream* s = mParser_Stream;
+ int c = s->Getc(ev);
+ if (c == 0xA || c == 0xD) // linebreak follows \ as expected?
+ {
+ c = this->eat_line_break(ev, c);
+ } else
+ ev->NewWarning("expected linebreak");
+
+ return c;
+}
+
+int morkParser::NextChar(morkEnv* ev) // next non-white content
+{
+ morkStream* s = mParser_Stream;
+ int c = s->Getc(ev);
+ while (c > 0 && ev->Good()) {
+ if (c == '/')
+ c = this->eat_comment(ev);
+ else if (c == 0xA || c == 0xD)
+ c = this->eat_line_break(ev, c);
+ else if (c == '\\')
+ c = this->eat_line_continue(ev);
+ else if (morkCh_IsWhite(c))
+ c = s->Getc(ev);
+ else
+ break; // end while loop when return c is acceptable
+ }
+ if (ev->Bad()) {
+ mParser_State = morkParser_kBrokenState;
+ mParser_DoMore = morkBool_kFalse;
+ mParser_IsDone = morkBool_kTrue;
+ mParser_IsBroken = morkBool_kTrue;
+ c = EOF;
+ } else if (c == EOF) {
+ mParser_DoMore = morkBool_kFalse;
+ mParser_IsDone = morkBool_kTrue;
+ }
+ return c;
+}
+
+void morkParser::OnCellState(morkEnv* ev) { ev->StubMethodOnlyError(); }
+
+void morkParser::OnMetaState(morkEnv* ev) { ev->StubMethodOnlyError(); }
+
+void morkParser::OnRowState(morkEnv* ev) { ev->StubMethodOnlyError(); }
+
+void morkParser::OnTableState(morkEnv* ev) { ev->StubMethodOnlyError(); }
+
+void morkParser::OnDictState(morkEnv* ev) { ev->StubMethodOnlyError(); }
+
+morkBuf* morkParser::ReadName(morkEnv* ev, int c) {
+ morkBuf* outBuf = 0;
+
+ if (!morkCh_IsName(c)) ev->NewError("not a name char");
+
+ morkCoil* coil = &mParser_ColumnCoil;
+ coil->ClearBufFill();
+
+ morkSpool* spool = &mParser_ColumnSpool;
+ spool->Seek(ev, /*pos*/ 0);
+
+ if (ev->Good()) {
+ spool->Putc(ev, c);
+
+ morkStream* s = mParser_Stream;
+ while ((c = s->Getc(ev)) != EOF && morkCh_IsMore(c) && ev->Good())
+ spool->Putc(ev, c);
+
+ if (ev->Good()) {
+ if (c != EOF) {
+ s->Ungetc(c);
+ spool->FlushSink(ev); // update coil->mBuf_Fill
+ } else
+ this->UnexpectedEofError(ev);
+
+ if (ev->Good()) outBuf = coil;
+ }
+ }
+ return outBuf;
+}
+
+mork_bool morkParser::ReadMid(morkEnv* ev, morkMid* outMid) {
+ outMid->ClearMid();
+
+ morkStream* s = mParser_Stream;
+ int next;
+ outMid->mMid_Oid.mOid_Id = this->ReadHex(ev, &next);
+ int c = next;
+ if (c == ':') {
+ if ((c = s->Getc(ev)) != EOF && ev->Good()) {
+ if (c == '^') {
+ outMid->mMid_Oid.mOid_Scope = this->ReadHex(ev, &next);
+ if (ev->Good()) s->Ungetc(next);
+ } else if (morkCh_IsName(c)) {
+ outMid->mMid_Buf = this->ReadName(ev, c);
+ } else
+ ev->NewError("expected name or hex after ':' following ID");
+ }
+
+ if (c == EOF && ev->Good()) this->UnexpectedEofError(ev);
+ } else
+ s->Ungetc(c);
+
+ return ev->Good();
+}
+
+void morkParser::ReadCell(morkEnv* ev) {
+ mParser_CellMid.ClearMid();
+ // this->StartSpanOnLastByte(ev, &mParser_CellSpan);
+ morkMid* cellMid = 0; // if mid syntax is used for column
+ morkBuf* cellBuf = 0; // if naked string is used for column
+
+ morkStream* s = mParser_Stream;
+ int c;
+ if ((c = s->Getc(ev)) != EOF && ev->Good()) {
+ // this->StartSpanOnLastByte(ev, &mParser_ColumnSpan);
+ if (c == '^') {
+ cellMid = &mParser_CellMid;
+ this->ReadMid(ev, cellMid);
+ // if ( !mParser_CellMid.mMid_Oid.mOid_Scope )
+ // mParser_CellMid.mMid_Oid.mOid_Scope = (mork_scope) 'c';
+ } else {
+ if (mParser_InMeta && c == morkStore_kFormColumn) {
+ ReadCellForm(ev, c);
+ return;
+ } else
+ cellBuf = this->ReadName(ev, c);
+ }
+ if (ev->Good()) {
+ // this->EndSpanOnThisByte(ev, &mParser_ColumnSpan);
+
+ mParser_InCell = morkBool_kTrue;
+ this->OnNewCell(ev, *mParser_CellSpan.AsPlace(), cellMid,
+ cellBuf); // , mParser_CellChange
+
+ mParser_CellChange = morkChange_kNil;
+ if ((c = this->NextChar(ev)) != EOF && ev->Good()) {
+ // this->StartSpanOnLastByte(ev, &mParser_SlotSpan);
+ if (c == '=') {
+ morkBuf* buf = this->ReadValue(ev);
+ if (buf) {
+ // this->EndSpanOnThisByte(ev, &mParser_SlotSpan);
+ this->OnValue(ev, mParser_SlotSpan, *buf);
+ }
+ } else if (c == '^') {
+ if (this->ReadMid(ev, &mParser_Mid)) {
+ // this->EndSpanOnThisByte(ev, &mParser_SlotSpan);
+ if ((c = this->NextChar(ev)) != EOF && ev->Good()) {
+ if (c != ')') ev->NewError("expected ')' after cell ^ID value");
+ } else if (c == EOF)
+ this->UnexpectedEofError(ev);
+
+ if (ev->Good()) this->OnValueMid(ev, mParser_SlotSpan, mParser_Mid);
+ }
+ } else if (c == 'r' || c == 't' || c == '"' || c == '\'') {
+ ev->NewError("cell syntax not yet supported");
+ } else {
+ ev->NewError("unknown cell syntax");
+ }
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_CellSpan);
+ mParser_InCell = morkBool_kFalse;
+ this->OnCellEnd(ev, mParser_CellSpan);
+ }
+ }
+ mParser_CellChange = morkChange_kNil;
+
+ if (c == EOF && ev->Good()) this->UnexpectedEofError(ev);
+}
+
+void morkParser::ReadRowPos(morkEnv* ev) {
+ int c; // next character
+ mork_pos rowPos = this->ReadHex(ev, &c);
+
+ if (ev->Good() && c != EOF) // should put back byte after hex?
+ mParser_Stream->Ungetc(c);
+
+ this->OnRowPos(ev, rowPos);
+}
+
+void morkParser::ReadRow(morkEnv* ev, int c)
+// zm:Row ::= zm:S? '[' zm:S? zm:Id zm:RowItem* zm:S? ']'
+// zm:RowItem ::= zm:MetaRow | zm:Cell
+// zm:MetaRow ::= zm:S? '[' zm:S? zm:Cell* zm:S? ']' /* meta attributes */
+// zm:Cell ::= zm:S? '(' zm:Column zm:S? zm:Slot? ')'
+{
+ if (ev->Good()) {
+ // this->StartSpanOnLastByte(ev, &mParser_RowSpan);
+ if (mParser_Change) mParser_RowChange = mParser_Change;
+
+ mork_bool cutAllRowCols = morkBool_kFalse;
+
+ if (c == '[') {
+ if ((c = this->NextChar(ev)) == '-')
+ cutAllRowCols = morkBool_kTrue;
+ else if (ev->Good() && c != EOF)
+ mParser_Stream->Ungetc(c);
+
+ if (this->ReadMid(ev, &mParser_RowMid)) {
+ mParser_InRow = morkBool_kTrue;
+ this->OnNewRow(ev, *mParser_RowSpan.AsPlace(), mParser_RowMid,
+ cutAllRowCols);
+
+ mParser_Change = mParser_RowChange = morkChange_kNil;
+
+ while ((c = this->NextChar(ev)) != EOF && ev->Good() && c != ']') {
+ switch (c) {
+ case '(': // cell
+ this->ReadCell(ev);
+ break;
+
+ case '[': // meta
+ this->ReadMeta(ev, ']');
+ break;
+
+ // case '+': // plus
+ // mParser_CellChange = morkChange_kAdd;
+ // break;
+
+ case '-': // minus
+ // mParser_CellChange = morkChange_kCut;
+ this->OnMinusCell(ev);
+ break;
+
+ // case '!': // bang
+ // mParser_CellChange = morkChange_kSet;
+ // break;
+
+ default:
+ ev->NewWarning("unexpected byte in row");
+ break;
+ } // switch
+ } // while
+
+ if (ev->Good()) {
+ if ((c = this->NextChar(ev)) == '!')
+ this->ReadRowPos(ev);
+ else if (c != EOF && ev->Good())
+ mParser_Stream->Ungetc(c);
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_RowSpan);
+ mParser_InRow = morkBool_kFalse;
+ this->OnRowEnd(ev, mParser_RowSpan);
+
+ } // if ReadMid
+ } // if '['
+
+ else // c != '['
+ {
+ morkStream* s = mParser_Stream;
+ s->Ungetc(c);
+ if (this->ReadMid(ev, &mParser_RowMid)) {
+ mParser_InRow = morkBool_kTrue;
+ this->OnNewRow(ev, *mParser_RowSpan.AsPlace(), mParser_RowMid,
+ cutAllRowCols);
+
+ mParser_Change = mParser_RowChange = morkChange_kNil;
+
+ if (ev->Good()) {
+ if ((c = this->NextChar(ev)) == '!')
+ this->ReadRowPos(ev);
+ else if (c != EOF && ev->Good())
+ s->Ungetc(c);
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_RowSpan);
+ mParser_InRow = morkBool_kFalse;
+ this->OnRowEnd(ev, mParser_RowSpan);
+ }
+ }
+ }
+
+ if (ev->Bad())
+ mParser_State = morkParser_kBrokenState;
+ else if (c == EOF)
+ mParser_State = morkParser_kDoneState;
+}
+
+void morkParser::ReadTable(morkEnv* ev)
+// zm:Table ::= zm:S? '{' zm:S? zm:Id zm:TableItem* zm:S? '}'
+// zm:TableItem ::= zm:MetaTable | zm:RowRef | zm:Row
+// zm:MetaTable ::= zm:S? '{' zm:S? zm:Cell* zm:S? '}' /* meta attributes */
+{
+ // this->StartSpanOnLastByte(ev, &mParser_TableSpan);
+
+ if (mParser_Change) mParser_TableChange = mParser_Change;
+
+ mork_bool cutAllTableRows = morkBool_kFalse;
+
+ int c = this->NextChar(ev);
+ if (c == '-')
+ cutAllTableRows = morkBool_kTrue;
+ else if (ev->Good() && c != EOF)
+ mParser_Stream->Ungetc(c);
+
+ if (ev->Good() && this->ReadMid(ev, &mParser_TableMid)) {
+ mParser_InTable = morkBool_kTrue;
+ this->OnNewTable(ev, *mParser_TableSpan.AsPlace(), mParser_TableMid,
+ cutAllTableRows);
+
+ mParser_Change = mParser_TableChange = morkChange_kNil;
+
+ while ((c = this->NextChar(ev)) != EOF && ev->Good() && c != '}') {
+ if (morkCh_IsHex(c)) {
+ this->ReadRow(ev, c);
+ } else {
+ switch (c) {
+ case '[': // row
+ this->ReadRow(ev, '[');
+ break;
+
+ case '{': // meta
+ this->ReadMeta(ev, '}');
+ break;
+
+ // case '+': // plus
+ // mParser_RowChange = morkChange_kAdd;
+ // break;
+
+ case '-': // minus
+ // mParser_RowChange = morkChange_kCut;
+ this->OnMinusRow(ev);
+ break;
+
+ // case '!': // bang
+ // mParser_RowChange = morkChange_kSet;
+ // break;
+
+ default:
+ ev->NewWarning("unexpected byte in table");
+ break;
+ }
+ }
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_TableSpan);
+ mParser_InTable = morkBool_kFalse;
+ this->OnTableEnd(ev, mParser_TableSpan);
+
+ if (ev->Bad())
+ mParser_State = morkParser_kBrokenState;
+ else if (c == EOF)
+ mParser_State = morkParser_kDoneState;
+ }
+}
+
+mork_id morkParser::ReadHex(morkEnv* ev, int* outNextChar)
+// zm:Hex ::= [0-9a-fA-F] /* a single hex digit */
+// zm:Hex+ ::= zm:Hex | zm:Hex zm:Hex+
+{
+ mork_id hex = 0;
+
+ morkStream* s = mParser_Stream;
+ int c = this->NextChar(ev);
+
+ if (ev->Good()) {
+ if (c != EOF) {
+ if (morkCh_IsHex(c)) {
+ do {
+ if (morkCh_IsDigit(c)) // '0' through '9'?
+ c -= '0';
+ else if (morkCh_IsUpper(c)) // 'A' through 'F'?
+ c -= ('A' - 10); // c = (c - 'A') + 10;
+ else // 'a' through 'f'?
+ c -= ('a' - 10); // c = (c - 'a') + 10;
+
+ hex = (hex << 4) + c;
+ } while ((c = s->Getc(ev)) != EOF && ev->Good() && morkCh_IsHex(c));
+ } else
+ this->ExpectedHexDigitError(ev, c);
+ }
+ }
+ if (c == EOF) this->EofInsteadOfHexError(ev);
+
+ *outNextChar = c;
+ return hex;
+}
+
+/*static*/ void morkParser::EofInsteadOfHexError(morkEnv* ev) {
+ ev->NewWarning("eof instead of hex");
+}
+
+/*static*/ void morkParser::ExpectedHexDigitError(morkEnv* ev, int c) {
+ MORK_USED_1(c);
+ ev->NewWarning("expected hex digit");
+}
+
+/*static*/ void morkParser::ExpectedEqualError(morkEnv* ev) {
+ ev->NewWarning("expected '='");
+}
+
+/*static*/ void morkParser::UnexpectedEofError(morkEnv* ev) {
+ ev->NewWarning("unexpected eof");
+}
+
+morkBuf* morkParser::ReadValue(morkEnv* ev) {
+ morkBuf* outBuf = 0;
+
+ morkCoil* coil = &mParser_ValueCoil;
+ coil->ClearBufFill();
+
+ morkSpool* spool = &mParser_ValueSpool;
+ spool->Seek(ev, /*pos*/ 0);
+
+ if (ev->Good()) {
+ morkStream* s = mParser_Stream;
+ int c;
+ while ((c = s->Getc(ev)) != EOF && c != ')' && ev->Good()) {
+ if (c == '\\') // next char is escaped by '\'?
+ {
+ if ((c = s->Getc(ev)) == 0xA || c == 0xD) // linebreak after \?
+ {
+ c = this->eat_line_break(ev, c);
+ if (c == ')' || c == '\\' || c == '$') {
+ s->Ungetc(c); // just let while loop test read this again
+ continue; // goto next iteration of while loop
+ }
+ }
+ if (c == EOF || ev->Bad()) break; // end while loop
+ } else if (c == '$') // "$" escapes next two hex digits?
+ {
+ if ((c = s->Getc(ev)) != EOF && ev->Good()) {
+ mork_ch first = (mork_ch)c; // first hex digit
+ if ((c = s->Getc(ev)) != EOF && ev->Good()) {
+ mork_ch second = (mork_ch)c; // second hex digit
+ c = ev->HexToByte(first, second);
+ } else
+ break; // end while loop
+ } else
+ break; // end while loop
+ }
+ spool->Putc(ev, c);
+ }
+
+ if (ev->Good()) {
+ if (c != EOF)
+ spool->FlushSink(ev); // update coil->mBuf_Fill
+ else
+ this->UnexpectedEofError(ev);
+
+ if (ev->Good()) outBuf = coil;
+ }
+ }
+ return outBuf;
+}
+
+void morkParser::ReadDictForm(morkEnv* ev) {
+ int nextChar;
+ nextChar = this->NextChar(ev);
+ if (nextChar == '(') {
+ nextChar = this->NextChar(ev);
+ if (nextChar == morkStore_kFormColumn) {
+ int dictForm;
+
+ nextChar = this->NextChar(ev);
+ if (nextChar == '=') {
+ dictForm = this->NextChar(ev);
+ nextChar = this->NextChar(ev);
+ } else if (nextChar == '^') {
+ dictForm = this->ReadHex(ev, &nextChar);
+ } else {
+ ev->NewWarning("unexpected byte in dict form");
+ return;
+ }
+ mParser_ValueCoil.mText_Form = dictForm;
+ if (nextChar == ')') {
+ nextChar = this->NextChar(ev);
+ if (nextChar == '>') return;
+ }
+ }
+ }
+ ev->NewWarning("unexpected byte in dict form");
+}
+
+void morkParser::ReadCellForm(morkEnv* ev, int c) {
+ MORK_ASSERT(c == morkStore_kFormColumn);
+ int nextChar;
+ nextChar = this->NextChar(ev);
+ int cellForm;
+
+ if (nextChar == '=') {
+ cellForm = this->NextChar(ev);
+ nextChar = this->NextChar(ev);
+ } else if (nextChar == '^') {
+ cellForm = this->ReadHex(ev, &nextChar);
+ } else {
+ ev->NewWarning("unexpected byte in cell form");
+ return;
+ }
+ // ### not sure about this. Which form should we set?
+ // mBuilder_CellForm = mBuilder_RowForm = cellForm;
+ if (nextChar == ')') {
+ OnCellForm(ev, cellForm);
+ return;
+ }
+ ev->NewWarning("unexpected byte in cell form");
+}
+
+void morkParser::ReadAlias(morkEnv* ev)
+// zm:Alias ::= zm:S? '(' ('#')? zm:Hex+ zm:S? zm:Value ')'
+// zm:Value ::= '=' ([^)$\] | '\' zm:NonCRLF | zm:Continue | zm:Dollar)*
+{
+ // this->StartSpanOnLastByte(ev, &mParser_AliasSpan);
+
+ int nextChar;
+ mork_id hex = this->ReadHex(ev, &nextChar);
+ int c = nextChar;
+
+ mParser_Mid.ClearMid();
+ mParser_Mid.mMid_Oid.mOid_Id = hex;
+
+ if (morkCh_IsWhite(c) && ev->Good()) c = this->NextChar(ev);
+
+ if (ev->Good()) {
+ if (c == '<') {
+ ReadDictForm(ev);
+ if (ev->Good()) c = this->NextChar(ev);
+ }
+ if (c == '=') {
+ mParser_Mid.mMid_Buf = this->ReadValue(ev);
+ if (mParser_Mid.mMid_Buf) {
+ // this->EndSpanOnThisByte(ev, &mParser_AliasSpan);
+ this->OnAlias(ev, mParser_AliasSpan, mParser_Mid);
+ // need to reset this somewhere.
+ mParser_ValueCoil.mText_Form = 0;
+ }
+ } else
+ this->ExpectedEqualError(ev);
+ }
+}
+
+void morkParser::ReadMeta(morkEnv* ev, int inEndMeta)
+// zm:MetaDict ::= zm:S? '<' zm:S? zm:Cell* zm:S? '>' /* meta attributes */
+// zm:MetaTable ::= zm:S? '{' zm:S? zm:Cell* zm:S? '}' /* meta attributes */
+// zm:MetaRow ::= zm:S? '[' zm:S? zm:Cell* zm:S? ']' /* meta attributes */
+{
+ // this->StartSpanOnLastByte(ev, &mParser_MetaSpan);
+ mParser_InMeta = morkBool_kTrue;
+ this->OnNewMeta(ev, *mParser_MetaSpan.AsPlace());
+
+ mork_bool more = morkBool_kTrue; // until end meta
+ int c;
+ while (more && (c = this->NextChar(ev)) != EOF && ev->Good()) {
+ switch (c) {
+ case '(': // cell
+ this->ReadCell(ev);
+ break;
+
+ case '>': // maybe end meta?
+ if (inEndMeta == '>')
+ more = morkBool_kFalse; // stop reading meta
+ else
+ this->UnexpectedByteInMetaWarning(ev);
+ break;
+
+ case '}': // maybe end meta?
+ if (inEndMeta == '}')
+ more = morkBool_kFalse; // stop reading meta
+ else
+ this->UnexpectedByteInMetaWarning(ev);
+ break;
+
+ case ']': // maybe end meta?
+ if (inEndMeta == ']')
+ more = morkBool_kFalse; // stop reading meta
+ else
+ this->UnexpectedByteInMetaWarning(ev);
+ break;
+
+ case '[': // maybe table meta row?
+ if (mParser_InTable)
+ this->ReadRow(ev, '[');
+ else
+ this->UnexpectedByteInMetaWarning(ev);
+ break;
+
+ default:
+ if (mParser_InTable && morkCh_IsHex(c))
+ this->ReadRow(ev, c);
+ else
+ this->UnexpectedByteInMetaWarning(ev);
+ break;
+ }
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_MetaSpan);
+ mParser_InMeta = morkBool_kFalse;
+ this->OnMetaEnd(ev, mParser_MetaSpan);
+}
+
+/*static*/ void morkParser::UnexpectedByteInMetaWarning(morkEnv* ev) {
+ ev->NewWarning("unexpected byte in meta");
+}
+
+/*static*/ void morkParser::NonParserTypeError(morkEnv* ev) {
+ ev->NewError("non morkParser");
+}
+
+mork_bool morkParser::MatchPattern(morkEnv* ev, const char* inPattern) {
+ // if an error occurs, we want original inPattern in the debugger:
+ const char* pattern = inPattern; // mutable copy of pointer
+ morkStream* s = mParser_Stream;
+ int c;
+ while (*pattern && ev->Good()) {
+ char byte = *pattern++;
+ if ((c = s->Getc(ev)) != byte) {
+ ev->NewError("byte not in expected pattern");
+ }
+ }
+ return ev->Good();
+}
+
+mork_bool morkParser::FindGroupEnd(morkEnv* ev) {
+ mork_bool foundEnd = morkBool_kFalse;
+
+ // char gidBuf[ 64 ]; // to hold hex pattern we want
+ // (void) ev->TokenAsHex(gidBuf, mParser_GroupId);
+
+ morkStream* s = mParser_Stream;
+ int c;
+
+ while ((c = s->Getc(ev)) != EOF && ev->Good() && !foundEnd) {
+ if (c == '@') // maybe start of group ending?
+ {
+ // this->EndSpanOnThisByte(ev, &mParser_GroupSpan);
+ if ((c = s->Getc(ev)) == '$') // '$' follows '@' ?
+ {
+ if ((c = s->Getc(ev)) == '$') // '$' follows "@$" ?
+ {
+ if ((c = s->Getc(ev)) == '}') {
+ foundEnd = this->ReadEndGroupId(ev);
+ // this->EndSpanOnThisByte(ev, &mParser_GroupSpan);
+
+ } else
+ ev->NewError("expected '}' after @$$");
+ }
+ }
+ if (!foundEnd && c == '@') s->Ungetc(c);
+ }
+ }
+
+ return foundEnd && ev->Good();
+}
+
+void morkParser::ReadGroup(morkEnv* mev) {
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ int next = 0;
+ mParser_GroupId = this->ReadHex(mev, &next);
+ if (next == '{') {
+ morkStream* s = mParser_Stream;
+ int c;
+ if ((c = s->Getc(mev)) == '@') {
+ // we really need the following span inside morkBuilder::OnNewGroup():
+ this->StartSpanOnThisByte(mev, &mParser_GroupSpan);
+ mork_pos startPos = mParser_GroupSpan.mSpan_Start.mPlace_Pos;
+
+ // if ( !store->mStore_FirstCommitGroupPos )
+ // store->mStore_FirstCommitGroupPos = startPos;
+ // else if ( !store->mStore_SecondCommitGroupPos )
+ // store->mStore_SecondCommitGroupPos = startPos;
+
+ if (this->FindGroupEnd(mev)) {
+ mork_pos outPos;
+ s->Seek(ev, startPos, &outPos);
+ if (mev->Good()) {
+ this->OnNewGroup(mev, mParser_GroupSpan.mSpan_Start, mParser_GroupId);
+
+ this->ReadContent(mev, /*inInsideGroup*/ morkBool_kTrue);
+
+ this->OnGroupCommitEnd(mev, mParser_GroupSpan);
+ }
+ }
+ } else
+ mev->NewError("expected '@' after @$${id{");
+ } else
+ mev->NewError("expected '{' after @$$id");
+}
+
+mork_bool morkParser::ReadAt(morkEnv* ev, mork_bool inInsideGroup)
+/* groups must be ignored until properly terminated */
+// zm:Group ::= zm:GroupStart zm:Content zm:GroupEnd /* transaction */
+// zm:GroupStart ::= zm:S? '@$${' zm:Hex+ '{@' /* xaction id has own space */
+// zm:GroupEnd ::= zm:GroupCommit | zm:GroupAbort
+// zm:GroupCommit ::= zm:S? '@$$}' zm:Hex+ '}@' /* id matches start id */
+// zm:GroupAbort ::= zm:S? '@$$}~~}@' /* id matches start id */
+/* We must allow started transactions to be aborted in summary files. */
+/* Note '$$' will never occur unescaped in values we will see in Mork. */
+{
+ if (this->MatchPattern(ev, "$$")) {
+ morkStream* s = mParser_Stream;
+ int c;
+ if (((c = s->Getc(ev)) == '{' || c == '}') && ev->Good()) {
+ if (c == '{') // start of new group?
+ {
+ if (!inInsideGroup)
+ this->ReadGroup(ev);
+ else
+ ev->NewError("nested @$${ inside another group");
+ } else // c == '}' // end of old group?
+ {
+ if (inInsideGroup) {
+ this->ReadEndGroupId(ev);
+ mParser_GroupId = 0;
+ } else
+ ev->NewError("unmatched @$$} outside any group");
+ }
+ } else
+ ev->NewError("expected '{' or '}' after @$$");
+ }
+ return ev->Good();
+}
+
+mork_bool morkParser::ReadEndGroupId(morkEnv* ev) {
+ mork_bool outSawGroupId = morkBool_kFalse;
+ morkStream* s = mParser_Stream;
+ int c;
+ if ((c = s->Getc(ev)) != EOF && ev->Good()) {
+ if (c == '~') // transaction is aborted?
+ {
+ this->MatchPattern(ev, "~}@"); // finish rest of pattern
+ } else // push back byte and read expected trailing hex id
+ {
+ s->Ungetc(c);
+ int next = 0;
+ mork_gid endGroupId = this->ReadHex(ev, &next);
+ if (ev->Good()) {
+ if (endGroupId == mParser_GroupId) // matches start?
+ {
+ if (next == '}') // '}' after @$$}id ?
+ {
+ if ((c = s->Getc(ev)) == '@') // '@' after @$$}id} ?
+ {
+ // looks good, so return with no error
+ outSawGroupId = morkBool_kTrue;
+ mParser_InGroup = false;
+ } else
+ ev->NewError("expected '@' after @$$}id}");
+ } else
+ ev->NewError("expected '}' after @$$}id");
+ } else
+ ev->NewError("end group id mismatch");
+ }
+ }
+ }
+ return (outSawGroupId && ev->Good());
+}
+
+void morkParser::ReadDict(morkEnv* ev)
+// zm:Dict ::= zm:S? '<' zm:DictItem* zm:S? '>'
+// zm:DictItem ::= zm:MetaDict | zm:Alias
+// zm:MetaDict ::= zm:S? '<' zm:S? zm:Cell* zm:S? '>' /* meta attributes */
+// zm:Alias ::= zm:S? '(' ('#')? zm:Hex+ zm:S? zm:Value ')'
+{
+ mParser_Change = morkChange_kNil;
+ mParser_AtomChange = morkChange_kNil;
+
+ // this->StartSpanOnLastByte(ev, &mParser_DictSpan);
+ mParser_InDict = morkBool_kTrue;
+ this->OnNewDict(ev, *mParser_DictSpan.AsPlace());
+
+ int c;
+ while ((c = this->NextChar(ev)) != EOF && ev->Good() && c != '>') {
+ switch (c) {
+ case '(': // alias
+ this->ReadAlias(ev);
+ break;
+
+ case '<': // meta
+ this->ReadMeta(ev, '>');
+ break;
+
+ default:
+ ev->NewWarning("unexpected byte in dict");
+ break;
+ }
+ }
+
+ // this->EndSpanOnThisByte(ev, &mParser_DictSpan);
+ mParser_InDict = morkBool_kFalse;
+ this->OnDictEnd(ev, mParser_DictSpan);
+
+ if (ev->Bad())
+ mParser_State = morkParser_kBrokenState;
+ else if (c == EOF)
+ mParser_State = morkParser_kDoneState;
+}
+
+void morkParser::EndSpanOnThisByte(morkEnv* mev, morkSpan* ioSpan) {
+ mork_pos here;
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ nsresult rv = mParser_Stream->Tell(ev, &here);
+ if (NS_SUCCEEDED(rv) && mev->Good()) {
+ this->SetHerePos(here);
+ ioSpan->SetEndWithEnd(mParser_PortSpan);
+ }
+}
+
+void morkParser::EndSpanOnLastByte(morkEnv* mev, morkSpan* ioSpan) {
+ mork_pos here;
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ nsresult rv = mParser_Stream->Tell(ev, &here);
+ if (NS_SUCCEEDED(rv) && mev->Good()) {
+ if (here > 0)
+ --here;
+ else
+ here = 0;
+
+ this->SetHerePos(here);
+ ioSpan->SetEndWithEnd(mParser_PortSpan);
+ }
+}
+
+void morkParser::StartSpanOnLastByte(morkEnv* mev, morkSpan* ioSpan) {
+ mork_pos here;
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ nsresult rv = mParser_Stream->Tell(ev, &here);
+ if (NS_SUCCEEDED(rv) && mev->Good()) {
+ if (here > 0)
+ --here;
+ else
+ here = 0;
+
+ this->SetHerePos(here);
+ ioSpan->SetStartWithEnd(mParser_PortSpan);
+ ioSpan->SetEndWithEnd(mParser_PortSpan);
+ }
+}
+
+void morkParser::StartSpanOnThisByte(morkEnv* mev, morkSpan* ioSpan) {
+ mork_pos here;
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ nsresult rv = mParser_Stream->Tell(ev, &here);
+ if (NS_SUCCEEDED(rv) && mev->Good()) {
+ this->SetHerePos(here);
+ ioSpan->SetStartWithEnd(mParser_PortSpan);
+ ioSpan->SetEndWithEnd(mParser_PortSpan);
+ }
+}
+
+mork_bool morkParser::ReadContent(morkEnv* ev, mork_bool inInsideGroup) {
+ int c;
+ mork_bool keep_going = true;
+ while (keep_going && (c = this->NextChar(ev)) != EOF && ev->Good()) {
+ switch (c) {
+ case '[': // row
+ this->ReadRow(ev, '[');
+ keep_going = false;
+ break;
+
+ case '{': // table
+ this->ReadTable(ev);
+ keep_going = false;
+ break;
+
+ case '<': // dict
+ this->ReadDict(ev);
+ keep_going = false;
+ break;
+
+ case '@': // group
+ return this->ReadAt(ev, inInsideGroup);
+ // break;
+
+ // case '+': // plus
+ // mParser_Change = morkChange_kAdd;
+ // break;
+
+ // case '-': // minus
+ // mParser_Change = morkChange_kCut;
+ // break;
+
+ // case '!': // bang
+ // mParser_Change = morkChange_kSet;
+ // break;
+
+ default:
+ ev->NewWarning("unexpected byte in ReadContent()");
+ break;
+ }
+ }
+ if (ev->Bad())
+ mParser_State = morkParser_kBrokenState;
+ else if (c == EOF)
+ mParser_State = morkParser_kDoneState;
+
+ return (ev->Good() && c != EOF);
+}
+
+void morkParser::OnPortState(morkEnv* ev) {
+ mork_bool firstTime = !mParser_InPort;
+ mParser_InPort = morkBool_kTrue;
+ if (firstTime) this->OnNewPort(ev, *mParser_PortSpan.AsPlace());
+
+ mork_bool done = !this->ReadContent(ev, mParser_InGroup /*inInsideGroup*/);
+
+ if (done) {
+ mParser_InPort = morkBool_kFalse;
+ this->OnPortEnd(ev, mParser_PortSpan);
+ }
+
+ if (ev->Bad()) mParser_State = morkParser_kBrokenState;
+}
+
+void morkParser::OnStartState(morkEnv* mev) {
+ morkStream* s = mParser_Stream;
+ nsIMdbEnv* ev = mev->AsMdbEnv();
+ if (s && s->IsNode() && s->IsOpenNode()) {
+ mork_pos outPos;
+ nsresult rv = s->Seek(ev, 0, &outPos);
+ if (NS_SUCCEEDED(rv) && mev->Good()) {
+ this->StartParse(mev);
+ mParser_State = morkParser_kPortState;
+ }
+ } else
+ mev->NilPointerError();
+
+ if (mev->Bad()) mParser_State = morkParser_kBrokenState;
+}
+
+/*protected non-poly*/ void morkParser::ParseChunk(morkEnv* ev) {
+ mParser_Change = morkChange_kNil;
+ mParser_DoMore = morkBool_kTrue;
+
+ switch (mParser_State) {
+ case morkParser_kCellState: // 0
+ this->OnCellState(ev);
+ break;
+
+ case morkParser_kMetaState: // 1
+ this->OnMetaState(ev);
+ break;
+
+ case morkParser_kRowState: // 2
+ this->OnRowState(ev);
+ break;
+
+ case morkParser_kTableState: // 3
+ this->OnTableState(ev);
+ break;
+
+ case morkParser_kDictState: // 4
+ this->OnDictState(ev);
+ break;
+
+ case morkParser_kPortState: // 5
+ this->OnPortState(ev);
+ break;
+
+ case morkParser_kStartState: // 6
+ this->OnStartState(ev);
+ break;
+
+ case morkParser_kDoneState: // 7
+ mParser_DoMore = morkBool_kFalse;
+ mParser_IsDone = morkBool_kTrue;
+ this->StopParse(ev);
+ break;
+ case morkParser_kBrokenState: // 8
+ mParser_DoMore = morkBool_kFalse;
+ mParser_IsBroken = morkBool_kTrue;
+ this->StopParse(ev);
+ break;
+ default: // ?
+ MORK_ASSERT(morkBool_kFalse);
+ mParser_State = morkParser_kBrokenState;
+ break;
+ }
+}
+
+/*public non-poly*/ mdb_count
+morkParser::ParseMore( // return count of bytes consumed now
+ morkEnv* ev, // context
+ mork_pos* outPos, // current byte pos in the stream afterwards
+ mork_bool* outDone, // is parsing finished?
+ mork_bool* outBroken // is parsing irreparably dead and broken?
+) {
+ mdb_count outCount = 0;
+ if (this->IsNode() && this->GoodParserTag() && this->IsOpenNode()) {
+ mork_pos startPos = this->HerePos();
+
+ if (!mParser_IsDone && !mParser_IsBroken) this->ParseChunk(ev);
+
+ // HerePos is only updated for groups. I'd like it to be more accurate.
+
+ mork_pos here;
+ mParser_Stream->Tell(ev, &here);
+
+ if (outDone) *outDone = mParser_IsDone;
+ if (outBroken) *outBroken = mParser_IsBroken;
+ if (outPos) *outPos = here;
+
+ if (here > startPos) outCount = (mdb_count)(here - startPos);
+ } else {
+ this->NonUsableParserError(ev);
+ if (outDone) *outDone = morkBool_kTrue;
+ if (outBroken) *outBroken = morkBool_kTrue;
+ if (outPos) *outPos = 0;
+ }
+ return outCount;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkParser.h b/comm/mailnews/db/mork/morkParser.h
new file mode 100644
index 0000000000..61184ee995
--- /dev/null
+++ b/comm/mailnews/db/mork/morkParser.h
@@ -0,0 +1,547 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKPARSER_
+#define _MORKPARSER_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKSINK_
+# include "morkSink.h"
+#endif
+
+#ifndef _MORKYARN_
+# include "morkYarn.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*=============================================================================
+ * morkPlace: stream byte position and stream line count
+ */
+
+class morkPlace {
+ public:
+ mork_pos mPlace_Pos; // byte offset in an input stream
+ mork_line mPlace_Line; // line count in an input stream
+
+ void ClearPlace() {
+ mPlace_Pos = 0;
+ mPlace_Line = 0;
+ }
+
+ void SetPlace(mork_pos inPos, mork_line inLine) {
+ mPlace_Pos = inPos;
+ mPlace_Line = inLine;
+ }
+
+ morkPlace() {
+ mPlace_Pos = 0;
+ mPlace_Line = 0;
+ }
+
+ morkPlace(mork_pos inPos, mork_line inLine) {
+ mPlace_Pos = inPos;
+ mPlace_Line = inLine;
+ }
+
+ morkPlace(const morkPlace& inPlace)
+ : mPlace_Pos(inPlace.mPlace_Pos), mPlace_Line(inPlace.mPlace_Line) {}
+};
+
+/*=============================================================================
+ * morkGlitch: stream place and error comment describing a parsing error
+ */
+
+class morkGlitch {
+ public:
+ morkPlace mGlitch_Place; // place in stream where problem happened
+ const char* mGlitch_Comment; // null-terminated ASCII C string
+
+ morkGlitch() { mGlitch_Comment = 0; }
+
+ morkGlitch(const morkPlace& inPlace, const char* inComment)
+ : mGlitch_Place(inPlace), mGlitch_Comment(inComment) {}
+};
+
+/*=============================================================================
+ * morkMid: all possible ways needed to express an alias ID in Mork syntax
+ */
+
+/*| morkMid: an abstraction of all the variations we might need to support
+**| in order to present an ID through the parser interface most cheaply and
+**| with minimum transformation away from the original text format.
+**|
+**|| An ID can have one of four forms:
+**| 1) idHex (mMid_Oid.mOid_Id <- idHex)
+**| 2) idHex:^scopeHex (mMid_Oid.mOid_Id <- idHex, mOid_Scope <- scopeHex)
+**| 3) idHex:scopeName (mMid_Oid.mOid_Id <- idHex, mMid_Buf <- scopeName)
+**| 4) columnName (mMid_Buf <- columnName, for columns in cells only)
+**|
+**|| Typically, mMid_Oid.mOid_Id will hold a nonzero integer value for
+**| an ID, but we might have an optional scope specified by either an integer
+**| in hex format, or a string name. (Note that while the first ID can be
+**| scoped variably, any integer ID for a scope is assumed always located in
+**| the same scope, so the second ID need not be disambiguated.)
+**|
+**|| The only time mMid_Oid.mOid_Id is ever zero is when mMid_Buf alone
+**| is nonzero, to indicate an explicit string instead of an alias appeared.
+**| This case happens to make the representation of columns in cells somewhat
+**| easier to represent, since columns can just appear as a string name; and
+**| this unifies those interfaces with row and table APIs expecting IDs.
+**|
+**|| So when the parser passes an instance of morkMid to a subclass, the
+**| mMid_Oid.mOid_Id slot should usually be nonzero. And the other two
+**| slots, mMid_Oid.mOid_Scope and mMid_Buf, might both be zero, or at
+**| most one of them will be nonzero to indicate an explicit scope; the
+**| parser is responsible for ensuring at most one of these is nonzero.
+|*/
+class morkMid {
+ public:
+ mdbOid mMid_Oid; // mOid_Scope is zero when not specified
+ const morkBuf* mMid_Buf; // points to some specific buf subclass
+
+ morkMid() {
+ mMid_Oid.mOid_Scope = 0;
+ mMid_Oid.mOid_Id = morkId_kMinusOne;
+ mMid_Buf = 0;
+ }
+
+ void InitMidWithCoil(morkCoil* ioCoil) {
+ mMid_Oid.mOid_Scope = 0;
+ mMid_Oid.mOid_Id = morkId_kMinusOne;
+ mMid_Buf = ioCoil;
+ }
+
+ void ClearMid() {
+ mMid_Oid.mOid_Scope = 0;
+ mMid_Oid.mOid_Id = morkId_kMinusOne;
+ mMid_Buf = 0;
+ }
+
+ morkMid(const morkMid& other)
+ : mMid_Oid(other.mMid_Oid), mMid_Buf(other.mMid_Buf) {}
+
+ mork_bool HasNoId() const // ID is unspecified?
+ {
+ return (mMid_Oid.mOid_Id == morkId_kMinusOne);
+ }
+
+ mork_bool HasSomeId() const // ID is specified?
+ {
+ return (mMid_Oid.mOid_Id != morkId_kMinusOne);
+ }
+};
+
+/*=============================================================================
+ * morkSpan: start and end stream byte position and stream line count
+ */
+
+class morkSpan {
+ public:
+ morkPlace mSpan_Start;
+ morkPlace mSpan_End;
+
+ public: // methods
+ public: // inlines
+ morkSpan() {} // use inline empty constructor for each place
+
+ morkPlace* AsPlace() { return &mSpan_Start; }
+ const morkPlace* AsConstPlace() const { return &mSpan_Start; }
+
+ void SetSpan(mork_pos inFromPos, mork_line inFromLine, mork_pos inToPos,
+ mork_line inToLine) {
+ mSpan_Start.SetPlace(inFromPos, inFromLine);
+ mSpan_End.SetPlace(inToPos, inToLine);
+ }
+
+ // setting end, useful to terminate a span using current port span end:
+ void SetEndWithEnd(const morkSpan& inSpan) // end <- span.end
+ {
+ mSpan_End = inSpan.mSpan_End;
+ }
+
+ // setting start, useful to initiate a span using current port span end:
+ void SetStartWithEnd(const morkSpan& inSpan) // start <- span.end
+ {
+ mSpan_Start = inSpan.mSpan_End;
+ }
+
+ void ClearSpan() {
+ mSpan_Start.mPlace_Pos = 0;
+ mSpan_Start.mPlace_Line = 0;
+ mSpan_End.mPlace_Pos = 0;
+ mSpan_End.mPlace_Line = 0;
+ }
+
+ morkSpan(mork_pos inFromPos, mork_line inFromLine, mork_pos inToPos,
+ mork_line inToLine)
+ : mSpan_Start(inFromPos, inFromLine),
+ mSpan_End(inToPos, inToLine) { /* empty implementation */
+ }
+};
+
+/*=============================================================================
+ * morkParser: for parsing Mork text syntax
+ */
+
+/* parse at least half 0.5K at once */
+#define morkParser_kMinGranularity 512
+/* parse at most 64 K at once */
+#define morkParser_kMaxGranularity (64 * 1024)
+
+#define morkDerived_kParser /*i*/ 0x5073 /* ascii 'Ps' */
+#define morkParser_kTag /*i*/ 0x70417253 /* ascii 'pArS' */
+
+// These are states for the simple parsing virtual machine. Needless to say,
+// these must be distinct, and preferably in a contiguous integer range.
+// Don't change these constants without looking at switch statements in code.
+#define morkParser_kCellState 0 /* cell is tightest scope */
+#define morkParser_kMetaState 1 /* meta is tightest scope */
+#define morkParser_kRowState 2 /* row is tightest scope */
+#define morkParser_kTableState 3 /* table is tightest scope */
+#define morkParser_kDictState 4 /* dict is tightest scope */
+#define morkParser_kPortState 5 /* port is tightest scope */
+
+#define morkParser_kStartState 6 /* parsing has not yet begun */
+#define morkParser_kDoneState 7 /* parsing is complete */
+#define morkParser_kBrokenState 8 /* parsing is to broken to work */
+
+class morkParser /*d*/ : public morkNode {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected morkParser members
+ nsIMdbHeap* mParser_Heap; // refcounted heap used for allocation
+ morkStream* mParser_Stream; // refcounted input stream
+
+ mork_u4 mParser_Tag; // must equal morkParser_kTag
+ mork_count mParser_MoreGranularity; // constructor inBytesPerParseSegment
+
+ mork_u4 mParser_State; // state where parser should resume
+
+ // after finding ends of group transactions, we can re-seek the start:
+ mork_pos mParser_GroupContentStartPos; // start of this group
+
+ morkMid mParser_TableMid; // table mid if inside a table
+ morkMid mParser_RowMid; // row mid if inside a row
+ morkMid mParser_CellMid; // cell mid if inside a row
+ mork_gid mParser_GroupId; // group ID if inside a group
+
+ mork_bool mParser_InPort; // called OnNewPort but not OnPortEnd?
+ mork_bool mParser_InDict; // called OnNewDict but not OnDictEnd?
+ mork_bool mParser_InCell; // called OnNewCell but not OnCellEnd?
+ mork_bool mParser_InMeta; // called OnNewMeta but not OnMetaEnd?
+
+ mork_bool mParser_InPortRow; // called OnNewPortRow but not OnPortRowEnd?
+ mork_bool mParser_InRow; // called OnNewRow but not OnNewRowEnd?
+ mork_bool mParser_InTable; // called OnNewMeta but not OnMetaEnd?
+ mork_bool mParser_InGroup; // called OnNewGroup but not OnGroupEnd?
+
+ mork_change mParser_AtomChange; // driven by mParser_Change
+ mork_change mParser_CellChange; // driven by mParser_Change
+ mork_change mParser_RowChange; // driven by mParser_Change
+ mork_change mParser_TableChange; // driven by mParser_Change
+
+ mork_change mParser_Change; // driven by modifier in text
+ mork_bool mParser_IsBroken; // has the parse become broken?
+ mork_bool mParser_IsDone; // has the parse finished?
+ mork_bool mParser_DoMore; // mParser_MoreGranularity not exhausted?
+
+ morkMid mParser_Mid; // current alias being parsed
+ // note that mParser_Mid.mMid_Buf points at mParser_ScopeCoil below:
+
+ // blob coils allocated in mParser_Heap
+ morkCoil mParser_ScopeCoil; // place to accumulate ID scope blobs
+ morkCoil mParser_ValueCoil; // place to accumulate value blobs
+ morkCoil mParser_ColumnCoil; // place to accumulate column blobs
+ morkCoil mParser_StringCoil; // place to accumulate string blobs
+
+ morkSpool mParser_ScopeSpool; // writes to mParser_ScopeCoil
+ morkSpool mParser_ValueSpool; // writes to mParser_ValueCoil
+ morkSpool mParser_ColumnSpool; // writes to mParser_ColumnCoil
+ morkSpool mParser_StringSpool; // writes to mParser_StringCoil
+
+ // yarns allocated in mParser_Heap
+ morkYarn mParser_MidYarn; // place to receive from MidToYarn()
+
+ // span showing current ongoing file position status:
+ morkSpan mParser_PortSpan; // span of current db port file
+
+ // various spans denoting nested subspaces inside the file's port span:
+ morkSpan mParser_GroupSpan; // span of current transaction group
+ morkSpan mParser_DictSpan;
+ morkSpan mParser_AliasSpan;
+ morkSpan mParser_MetaSpan;
+ morkSpan mParser_TableSpan;
+ morkSpan mParser_RowSpan;
+ morkSpan mParser_CellSpan;
+ morkSpan mParser_ColumnSpan;
+ morkSpan mParser_SlotSpan;
+
+ private: // convenience inlines
+ mork_pos HerePos() const { return mParser_PortSpan.mSpan_End.mPlace_Pos; }
+
+ void SetHerePos(mork_pos inPos) {
+ mParser_PortSpan.mSpan_End.mPlace_Pos = inPos;
+ }
+
+ void CountLineBreak() { ++mParser_PortSpan.mSpan_End.mPlace_Line; }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseParser() only if open
+ virtual ~morkParser(); // assert that CloseParser() executed earlier
+
+ public: // morkYarn construction & destruction
+ morkParser(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkStream* ioStream, // the readonly stream for input bytes
+ mdb_count inBytesPerParseSegment, // target for ParseMore()
+ nsIMdbHeap* ioSlotHeap);
+
+ void CloseParser(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkParser(const morkParser& other);
+ morkParser& operator=(const morkParser& other);
+
+ public: // dynamic type identification
+ mork_bool IsParser() const {
+ return IsNode() && mNode_Derived == morkDerived_kParser;
+ }
+
+ // } ===== end morkNode methods =====
+
+ public: // errors and warnings
+ static void UnexpectedEofError(morkEnv* ev);
+ static void EofInsteadOfHexError(morkEnv* ev);
+ static void ExpectedEqualError(morkEnv* ev);
+ static void ExpectedHexDigitError(morkEnv* ev, int c);
+ static void NonParserTypeError(morkEnv* ev);
+ static void UnexpectedByteInMetaWarning(morkEnv* ev);
+
+ public: // other type methods
+ mork_bool GoodParserTag() const { return mParser_Tag == morkParser_kTag; }
+ void NonGoodParserError(morkEnv* ev);
+ void NonUsableParserError(morkEnv* ev);
+ // call when IsNode() or GoodParserTag() is false
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // in virtual morkParser methods, data flow subclass to parser
+ virtual void MidToYarn(
+ morkEnv* ev,
+ const morkMid& inMid, // typically an alias to concat with strings
+ mdbYarn* outYarn) = 0;
+ // The parser might ask that some aliases be turned into yarns, so they
+ // can be concatenated into longer blobs under some circumstances. This
+ // is an alternative to using a long and complex callback for many parts
+ // for a single cell value.
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // out virtual morkParser methods, data flow parser to subclass
+ // The virtual methods below will be called in a pattern corresponding
+ // to the following grammar isomorphic to the Mork grammar. There should
+ // be no exceptions, so subclasses can rely on seeing an appropriate "end"
+ // method whenever some "new" method has been seen earlier. In the event
+ // that some error occurs that causes content to be flushed, or sudden early
+ // termination of a larger containing entity, we will always call a more
+ // enclosed "end" method before we call an "end" method with greater scope.
+
+ // Note the "mp" prefix stands for "Mork Parser":
+
+ // mp:Start ::= OnNewPort mp:PortItem* OnPortEnd
+ // mp:PortItem ::= mp:Content | mp:Group | OnPortGlitch
+ // mp:Group ::= OnNewGroup mp:GroupItem* mp:GroupEnd
+ // mp:GroupItem ::= mp:Content | OnGroupGlitch
+ // mp:GroupEnd ::= OnGroupCommitEnd | OnGroupAbortEnd
+ // mp:Content ::= mp:PortRow | mp:Dict | mp:Table | mp:Row
+ // mp:PortRow ::= OnNewPortRow mp:RowItem* OnPortRowEnd
+ // mp:Dict ::= OnNewDict mp:DictItem* OnDictEnd
+ // mp:DictItem ::= OnAlias | OnAliasGlitch | mp:Meta | OnDictGlitch
+ // mp:Table ::= OnNewTable mp:TableItem* OnTableEnd
+ // mp:TableItem ::= mp:Row | mp:MetaTable | OnTableGlitch
+ // mp:MetaTable ::= OnNewMeta mp:MetaItem* mp:Row OnMetaEnd
+ // mp:Meta ::= OnNewMeta mp:MetaItem* OnMetaEnd
+ // mp:MetaItem ::= mp:Cell | OnMetaGlitch
+ // mp:Row ::= OnMinusRow? OnNewRow mp:RowItem* OnRowEnd
+ // mp:RowItem ::= mp:Cell | mp:Meta | OnRowGlitch
+ // mp:Cell ::= OnMinusCell? OnNewCell mp:CellItem? OnCellEnd
+ // mp:CellItem ::= mp:Slot | OnCellForm | OnCellGlitch
+ // mp:Slot ::= OnValue | OnValueMid | OnRowMid | OnTableMid
+
+ // Note that in interfaces below, mork_change parameters kAdd and kNil
+ // both mean about the same thing by default. Only kCut is interesting,
+ // because this usually means to remove members instead of adding them.
+
+ virtual void OnNewPort(morkEnv* ev, const morkPlace& inPlace) = 0;
+ virtual void OnPortGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnPortEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnNewGroup(morkEnv* ev, const morkPlace& inPlace,
+ mork_gid inGid) = 0;
+ virtual void OnGroupGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnGroupCommitEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+ virtual void OnGroupAbortEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnNewPortRow(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid, mork_change inChange) = 0;
+ virtual void OnPortRowGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnPortRowEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnNewTable(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid, mork_bool inCutAllRows) = 0;
+ virtual void OnTableGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnTableEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnNewMeta(morkEnv* ev, const morkPlace& inPlace) = 0;
+ virtual void OnMetaGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnMetaEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnMinusRow(morkEnv* ev) = 0;
+ virtual void OnNewRow(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid& inMid, mork_bool inCutAllCols) = 0;
+ virtual void OnRowPos(morkEnv* ev, mork_pos inRowPos) = 0;
+ virtual void OnRowGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnRowEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnNewDict(morkEnv* ev, const morkPlace& inPlace) = 0;
+ virtual void OnDictGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnDictEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnAlias(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) = 0;
+
+ virtual void OnAliasGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+
+ virtual void OnMinusCell(morkEnv* ev) = 0;
+ virtual void OnNewCell(morkEnv* ev, const morkPlace& inPlace,
+ const morkMid* inMid, const morkBuf* inBuf) = 0;
+ // Exactly one of inMid and inBuf is nil, and the other is non-nil.
+ // When hex ID syntax is used for a column, then inMid is not nil, and
+ // when a naked string names a column, then inBuf is not nil.
+
+ virtual void OnCellGlitch(morkEnv* ev, const morkGlitch& inGlitch) = 0;
+ virtual void OnCellForm(morkEnv* ev, mork_cscode inCharsetFormat) = 0;
+ virtual void OnCellEnd(morkEnv* ev, const morkSpan& inSpan) = 0;
+
+ virtual void OnValue(morkEnv* ev, const morkSpan& inSpan,
+ const morkBuf& inBuf) = 0;
+
+ virtual void OnValueMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) = 0;
+
+ virtual void OnRowMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) = 0;
+
+ virtual void OnTableMid(morkEnv* ev, const morkSpan& inSpan,
+ const morkMid& inMid) = 0;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected parser helper methods
+ void ParseChunk(morkEnv* ev); // find parse continuation and resume
+
+ void StartParse(morkEnv* ev); // prepare for parsing
+ void StopParse(morkEnv* ev); // terminate parsing & call needed methods
+
+ int NextChar(morkEnv* ev); // next non-white content
+
+ void OnCellState(morkEnv* ev);
+ void OnMetaState(morkEnv* ev);
+ void OnRowState(morkEnv* ev);
+ void OnTableState(morkEnv* ev);
+ void OnDictState(morkEnv* ev);
+ void OnPortState(morkEnv* ev);
+ void OnStartState(morkEnv* ev);
+
+ void ReadCell(morkEnv* ev);
+ void ReadRow(morkEnv* ev, int c);
+ void ReadRowPos(morkEnv* ev);
+ void ReadTable(morkEnv* ev);
+ void ReadTableMeta(morkEnv* ev);
+ void ReadDict(morkEnv* ev);
+ mork_bool ReadContent(morkEnv* ev, mork_bool inInsideGroup);
+ void ReadGroup(morkEnv* ev);
+ mork_bool ReadEndGroupId(morkEnv* ev);
+ mork_bool ReadAt(morkEnv* ev, mork_bool inInsideGroup);
+ mork_bool FindGroupEnd(morkEnv* ev);
+ void ReadMeta(morkEnv* ev, int inEndMeta);
+ void ReadAlias(morkEnv* ev);
+ mork_id ReadHex(morkEnv* ev, int* outNextChar);
+ morkBuf* ReadValue(morkEnv* ev);
+ morkBuf* ReadName(morkEnv* ev, int c);
+ mork_bool ReadMid(morkEnv* ev, morkMid* outMid);
+ void ReadDictForm(morkEnv* ev);
+ void ReadCellForm(morkEnv* ev, int c);
+
+ mork_bool MatchPattern(morkEnv* ev, const char* inPattern);
+
+ void EndSpanOnThisByte(morkEnv* ev, morkSpan* ioSpan);
+ void EndSpanOnLastByte(morkEnv* ev, morkSpan* ioSpan);
+ void StartSpanOnLastByte(morkEnv* ev, morkSpan* ioSpan);
+
+ void StartSpanOnThisByte(morkEnv* ev, morkSpan* ioSpan);
+
+ // void EndSpanOnThisByte(morkEnv* ev, morkSpan* ioSpan)
+ // { MORK_USED_2(ev,ioSpan); }
+
+ // void EndSpanOnLastByte(morkEnv* ev, morkSpan* ioSpan)
+ // { MORK_USED_2(ev,ioSpan); }
+
+ // void StartSpanOnLastByte(morkEnv* ev, morkSpan* ioSpan)
+ // { MORK_USED_2(ev,ioSpan); }
+
+ // void StartSpanOnThisByte(morkEnv* ev, morkSpan* ioSpan)
+ // { MORK_USED_2(ev,ioSpan); }
+
+ int eat_line_break(morkEnv* ev, int inLast);
+ int eat_line_continue(morkEnv* ev); // last char was '\\'
+ int eat_comment(morkEnv* ev); // last char was '/'
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkParser methods
+ mdb_count ParseMore( // return count of bytes consumed now
+ morkEnv* ev, // context
+ mork_pos* outPos, // current byte pos in the stream afterwards
+ mork_bool* outDone, // is parsing finished?
+ mork_bool* outBroken // is parsing irreparably dead and broken?
+ );
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakParser(morkParser* me, morkEnv* ev, morkParser** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongParser(morkParser* me, morkEnv* ev,
+ morkParser** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKPARSER_ */
diff --git a/comm/mailnews/db/mork/morkPool.cpp b/comm/mailnews/db/mork/morkPool.cpp
new file mode 100644
index 0000000000..eb7d543395
--- /dev/null
+++ b/comm/mailnews/db/mork/morkPool.cpp
@@ -0,0 +1,483 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKHANDLE_
+# include "morkHandle.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+#ifndef _MORKZONE_
+# include "morkZone.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkPool::CloseMorkNode(
+ morkEnv* ev) // ClosePool() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->ClosePool(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkPool::~morkPool() // assert ClosePool() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkPool::morkPool(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap)
+ : morkNode(inUsage, ioHeap),
+ mPool_Heap(ioSlotHeap),
+ mPool_UsedFramesCount(0),
+ mPool_FreeFramesCount(0) {
+ // mPool_Heap is NOT refcounted
+ MORK_ASSERT(ioSlotHeap);
+ if (ioSlotHeap) mNode_Derived = morkDerived_kPool;
+}
+
+/*public non-poly*/
+morkPool::morkPool(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap)
+ : morkNode(ev, inUsage, ioHeap),
+ mPool_Heap(ioSlotHeap),
+ mPool_UsedFramesCount(0),
+ mPool_FreeFramesCount(0) {
+ if (ioSlotHeap) {
+ // mPool_Heap is NOT refcounted:
+ // nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mPool_Heap);
+ if (ev->Good()) mNode_Derived = morkDerived_kPool;
+ } else
+ ev->NilPointerError();
+}
+
+/*public non-poly*/ void morkPool::ClosePool(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+#ifdef morkZone_CONFIG_ARENA
+#else /*morkZone_CONFIG_ARENA*/
+ // MORK_USED_1(ioZone);
+#endif /*morkZone_CONFIG_ARENA*/
+
+ nsIMdbHeap* heap = mPool_Heap;
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+ morkLink* aLink;
+ morkDeque* d = &mPool_FreeHandleFrames;
+ while ((aLink = d->RemoveFirst()) != 0) heap->Free(mev, aLink);
+
+ // if the pool's closed, get rid of the frames in use too.
+ d = &mPool_UsedHandleFrames;
+ while ((aLink = d->RemoveFirst()) != 0) heap->Free(mev, aLink);
+
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// alloc and free individual instances of handles (inside hand frames):
+morkHandleFace* morkPool::NewHandle(morkEnv* ev, mork_size inSize,
+ morkZone* ioZone) {
+ void* newBlock = 0;
+ if (inSize <= sizeof(morkHandleFrame)) {
+ morkLink* firstLink = mPool_FreeHandleFrames.RemoveFirst();
+ if (firstLink) {
+ newBlock = firstLink;
+ if (mPool_FreeFramesCount)
+ --mPool_FreeFramesCount;
+ else
+ ev->NewWarning("mPool_FreeFramesCount underflow");
+ } else
+ mPool_Heap->Alloc(ev->AsMdbEnv(), sizeof(morkHandleFrame),
+ (void**)&newBlock);
+ } else {
+ ev->NewWarning("inSize > sizeof(morkHandleFrame)");
+ mPool_Heap->Alloc(ev->AsMdbEnv(), inSize, (void**)&newBlock);
+ }
+#ifdef morkZone_CONFIG_ARENA
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+#endif /*morkZone_CONFIG_ARENA*/
+
+ return (morkHandleFace*)newBlock;
+}
+
+void morkPool::ZapHandle(morkEnv* ev, morkHandleFace* ioHandle) {
+ if (ioHandle) {
+ morkLink* handleLink = (morkLink*)ioHandle;
+ mPool_FreeHandleFrames.AddLast(handleLink);
+ ++mPool_FreeFramesCount;
+ // lets free all handles to track down leaks
+ // - uncomment out next 3 lines, comment out above 2
+ // nsIMdbHeap* heap = mPool_Heap;
+ // nsIMdbEnv* mev = ev->AsMdbEnv();
+ // heap->Free(mev, handleLink);
+ }
+}
+
+// alloc and free individual instances of rows:
+morkRow* morkPool::NewRow(morkEnv* ev,
+ morkZone* ioZone) // allocate a new row instance
+{
+ morkRow* newRow = 0;
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newRow = (morkRow*)ioZone->ZoneNewChip(ev, sizeof(morkRow));
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), sizeof(morkRow), (void**)&newRow);
+#endif /*morkZone_CONFIG_ARENA*/
+
+ if (newRow) MORK_MEMSET(newRow, 0, sizeof(morkRow));
+
+ return newRow;
+}
+
+void morkPool::ZapRow(morkEnv* ev, morkRow* ioRow,
+ morkZone* ioZone) // free old row instance
+{
+#ifdef morkZone_CONFIG_ARENA
+ if (!ioRow) ev->NilPointerWarning(); // a zone 'chip' cannot be freed
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ if (ioRow) mPool_Heap->Free(ev->AsMdbEnv(), ioRow);
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+// alloc and free entire vectors of cells (not just one cell at a time)
+morkCell* morkPool::NewCells(morkEnv* ev, mork_size inSize, morkZone* ioZone) {
+ morkCell* newCells = 0;
+
+ mork_size size = inSize * sizeof(morkCell);
+ if (size) {
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'run' knows its size, and can indeed be deallocated:
+ newCells = (morkCell*)ioZone->ZoneNewRun(ev, size);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), size, (void**)&newCells);
+#endif /*morkZone_CONFIG_ARENA*/
+ }
+
+ // note morkAtom depends on having nil stored in all new mCell_Atom slots:
+ if (newCells) MORK_MEMSET(newCells, 0, size);
+ return newCells;
+}
+
+void morkPool::ZapCells(morkEnv* ev, morkCell* ioVector, mork_size inSize,
+ morkZone* ioZone) {
+ MORK_USED_1(inSize);
+
+ if (ioVector) {
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'run' knows its size, and can indeed be deallocated:
+ ioZone->ZoneZapRun(ev, ioVector);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Free(ev->AsMdbEnv(), ioVector);
+#endif /*morkZone_CONFIG_ARENA*/
+ }
+}
+
+// resize (grow or trim) cell vectors inside a containing row instance
+mork_bool morkPool::AddRowCells(morkEnv* ev, morkRow* ioRow,
+ mork_size inNewSize, morkZone* ioZone) {
+ // note strong implementation similarity to morkArray::Grow()
+
+ MORK_USED_1(ioZone);
+#ifdef morkZone_CONFIG_ARENA
+#else /*morkZone_CONFIG_ARENA*/
+#endif /*morkZone_CONFIG_ARENA*/
+
+ mork_fill fill = ioRow->mRow_Length;
+ if (ev->Good() && fill < inNewSize) // need more cells?
+ {
+ morkCell* newCells = this->NewCells(ev, inNewSize, ioZone);
+ if (newCells) {
+ morkCell* c = newCells; // for iterating during copy
+ morkCell* oldCells = ioRow->mRow_Cells;
+ morkCell* end = oldCells + fill; // copy all the old cells
+ while (oldCells < end) {
+ *c++ = *oldCells++; // bitwise copy each old cell struct
+ }
+ oldCells = ioRow->mRow_Cells;
+ ioRow->mRow_Cells = newCells;
+ ioRow->mRow_Length = (mork_u2)inNewSize;
+ ++ioRow->mRow_Seed;
+
+ if (oldCells) this->ZapCells(ev, oldCells, fill, ioZone);
+ }
+ }
+ return (ev->Good() && ioRow->mRow_Length >= inNewSize);
+}
+
+mork_bool morkPool::CutRowCells(morkEnv* ev, morkRow* ioRow,
+ mork_size inNewSize, morkZone* ioZone) {
+ MORK_USED_1(ioZone);
+#ifdef morkZone_CONFIG_ARENA
+#else /*morkZone_CONFIG_ARENA*/
+#endif /*morkZone_CONFIG_ARENA*/
+
+ mork_fill fill = ioRow->mRow_Length;
+ if (ev->Good() && fill > inNewSize) // need fewer cells?
+ {
+ if (inNewSize) // want any row cells at all?
+ {
+ morkCell* newCells = this->NewCells(ev, inNewSize, ioZone);
+ if (newCells) {
+ morkCell* saveNewCells = newCells; // Keep newcell pos
+ morkCell* oldCells = ioRow->mRow_Cells;
+ morkCell* oldEnd = oldCells + fill; // one past all old cells
+ morkCell* newEnd = oldCells + inNewSize; // copy only kept old cells
+ while (oldCells < newEnd) {
+ *newCells++ = *oldCells++; // bitwise copy each old cell struct
+ }
+ while (oldCells < oldEnd) {
+ if (oldCells->mCell_Atom) // need to unref old cell atom?
+ oldCells->SetAtom(ev, (morkAtom*)0, this); // unref cell atom
+ ++oldCells;
+ }
+ oldCells = ioRow->mRow_Cells;
+ ioRow->mRow_Cells = saveNewCells;
+ ioRow->mRow_Length = (mork_u2)inNewSize;
+ ++ioRow->mRow_Seed;
+
+ if (oldCells) this->ZapCells(ev, oldCells, fill, ioZone);
+ }
+ } else // get rid of all row cells
+ {
+ morkCell* oldCells = ioRow->mRow_Cells;
+ ioRow->mRow_Cells = 0;
+ ioRow->mRow_Length = 0;
+ ++ioRow->mRow_Seed;
+
+ if (oldCells) this->ZapCells(ev, oldCells, fill, ioZone);
+ }
+ }
+ return (ev->Good() && ioRow->mRow_Length <= inNewSize);
+}
+
+// alloc & free individual instances of atoms (lots of atom subclasses):
+void morkPool::ZapAtom(morkEnv* ev, morkAtom* ioAtom,
+ morkZone* ioZone) // any subclass (by kind)
+{
+#ifdef morkZone_CONFIG_ARENA
+ if (!ioAtom) ev->NilPointerWarning(); // a zone 'chip' cannot be freed
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ if (ioAtom) mPool_Heap->Free(ev->AsMdbEnv(), ioAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+morkOidAtom* morkPool::NewRowOidAtom(morkEnv* ev, const mdbOid& inOid,
+ morkZone* ioZone) {
+ morkOidAtom* newAtom = 0;
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkOidAtom*)ioZone->ZoneNewChip(ev, sizeof(morkOidAtom));
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), sizeof(morkOidAtom), (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+
+ if (newAtom) newAtom->InitRowOidAtom(ev, inOid);
+ return newAtom;
+}
+
+morkOidAtom* morkPool::NewTableOidAtom(morkEnv* ev, const mdbOid& inOid,
+ morkZone* ioZone) {
+ morkOidAtom* newAtom = 0;
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkOidAtom*)ioZone->ZoneNewChip(ev, sizeof(morkOidAtom));
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), sizeof(morkOidAtom), (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+ if (newAtom) newAtom->InitTableOidAtom(ev, inOid);
+ return newAtom;
+}
+
+morkAtom* morkPool::NewAnonAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm, morkZone* ioZone)
+// if inForm is zero, and inBuf.mBuf_Fill is less than 256, then a 'wee'
+// anon atom will be created, and otherwise a 'big' anon atom.
+{
+ morkAtom* newAtom = 0;
+
+ mork_bool needBig = (inForm || inBuf.mBuf_Fill > 255);
+ mork_size size = (needBig) ? morkBigAnonAtom::SizeForFill(inBuf.mBuf_Fill)
+ : morkWeeAnonAtom::SizeForFill(inBuf.mBuf_Fill);
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkAtom*)ioZone->ZoneNewChip(ev, size);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), size, (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+ if (newAtom) {
+ if (needBig)
+ ((morkBigAnonAtom*)newAtom)->InitBigAnonAtom(ev, inBuf, inForm);
+ else
+ ((morkWeeAnonAtom*)newAtom)->InitWeeAnonAtom(ev, inBuf);
+ }
+ return newAtom;
+}
+
+morkBookAtom* morkPool::NewBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm, morkAtomSpace* ioSpace,
+ mork_aid inAid, morkZone* ioZone)
+// if inForm is zero, and inBuf.mBuf_Fill is less than 256, then a 'wee'
+// book atom will be created, and otherwise a 'big' book atom.
+{
+ morkBookAtom* newAtom = 0;
+
+ mork_bool needBig = (inForm || inBuf.mBuf_Fill > 255);
+ mork_size size = (needBig) ? morkBigBookAtom::SizeForFill(inBuf.mBuf_Fill)
+ : morkWeeBookAtom::SizeForFill(inBuf.mBuf_Fill);
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkBookAtom*)ioZone->ZoneNewChip(ev, size);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), size, (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+ if (newAtom) {
+ if (needBig)
+ ((morkBigBookAtom*)newAtom)
+ ->InitBigBookAtom(ev, inBuf, inForm, ioSpace, inAid);
+ else
+ ((morkWeeBookAtom*)newAtom)->InitWeeBookAtom(ev, inBuf, ioSpace, inAid);
+ }
+ return newAtom;
+}
+
+morkBookAtom* morkPool::NewBookAtomCopy(morkEnv* ev,
+ const morkBigBookAtom& inAtom,
+ morkZone* ioZone)
+// make the smallest kind of book atom that can hold content in inAtom.
+// The inAtom parameter is often expected to be a staged book atom in
+// the store, which was used to search an atom space for existing atoms.
+{
+ morkBookAtom* newAtom = 0;
+
+ mork_cscode form = inAtom.mBigBookAtom_Form;
+ mork_fill fill = inAtom.mBigBookAtom_Size;
+ mork_bool needBig = (form || fill > 255);
+ mork_size size = (needBig) ? morkBigBookAtom::SizeForFill(fill)
+ : morkWeeBookAtom::SizeForFill(fill);
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkBookAtom*)ioZone->ZoneNewChip(ev, size);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), size, (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+ if (newAtom) {
+ morkBuf buf(inAtom.mBigBookAtom_Body, fill);
+ if (needBig)
+ ((morkBigBookAtom*)newAtom)
+ ->InitBigBookAtom(ev, buf, form, inAtom.mBookAtom_Space,
+ inAtom.mBookAtom_Id);
+ else
+ ((morkWeeBookAtom*)newAtom)
+ ->InitWeeBookAtom(ev, buf, inAtom.mBookAtom_Space,
+ inAtom.mBookAtom_Id);
+ }
+ return newAtom;
+}
+
+morkBookAtom* morkPool::NewFarBookAtomCopy(morkEnv* ev,
+ const morkFarBookAtom& inAtom,
+ morkZone* ioZone)
+// make the smallest kind of book atom that can hold content in inAtom.
+// The inAtom parameter is often expected to be a staged book atom in
+// the store, which was used to search an atom space for existing atoms.
+{
+ morkBookAtom* newAtom = 0;
+
+ mork_cscode form = inAtom.mFarBookAtom_Form;
+ mork_fill fill = inAtom.mFarBookAtom_Size;
+ mork_bool needBig = (form || fill > 255);
+ mork_size size = (needBig) ? morkBigBookAtom::SizeForFill(fill)
+ : morkWeeBookAtom::SizeForFill(fill);
+
+#ifdef morkZone_CONFIG_ARENA
+ // a zone 'chip' remembers no size, and so cannot be deallocated:
+ newAtom = (morkBookAtom*)ioZone->ZoneNewChip(ev, size);
+#else /*morkZone_CONFIG_ARENA*/
+ MORK_USED_1(ioZone);
+ mPool_Heap->Alloc(ev->AsMdbEnv(), size, (void**)&newAtom);
+#endif /*morkZone_CONFIG_ARENA*/
+ if (newAtom) {
+ morkBuf buf(inAtom.mFarBookAtom_Body, fill);
+ if (needBig)
+ ((morkBigBookAtom*)newAtom)
+ ->InitBigBookAtom(ev, buf, form, inAtom.mBookAtom_Space,
+ inAtom.mBookAtom_Id);
+ else
+ ((morkWeeBookAtom*)newAtom)
+ ->InitWeeBookAtom(ev, buf, inAtom.mBookAtom_Space,
+ inAtom.mBookAtom_Id);
+ }
+ return newAtom;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkPool.h b/comm/mailnews/db/mork/morkPool.h
new file mode 100644
index 0000000000..dbad8593b7
--- /dev/null
+++ b/comm/mailnews/db/mork/morkPool.h
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKPOOL_
+#define _MORKPOOL_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class morkHandle;
+class morkHandleFrame;
+class morkHandleFace; // just an opaque cookie type
+class morkBigBookAtom;
+class morkFarBookAtom;
+
+#define morkDerived_kPool /*i*/ 0x706C /* ascii 'pl' */
+
+/*| morkPool: a place to manage pools of non-node objects that are memory
+**| managed out of large chunks of space, so that per-object management
+**| space overhead has no significant cost.
+|*/
+class morkPool : public morkNode {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ nsIMdbHeap* mPool_Heap; // NON-refcounted heap instance
+
+ morkDeque mPool_Blocks; // linked list of large blocks from heap
+
+ // These two lists contain instances of morkHandleFrame:
+ morkDeque mPool_UsedHandleFrames; // handle frames currently being used
+ morkDeque mPool_FreeHandleFrames; // handle frames currently in free list
+
+ mork_count mPool_UsedFramesCount; // length of mPool_UsedHandleFrames
+ mork_count mPool_FreeFramesCount; // length of mPool_UsedHandleFrames
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev); // ClosePool() only if open
+ virtual ~morkPool(); // assert that ClosePool() executed earlier
+
+ public: // morkPool construction & destruction
+ morkPool(const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ morkPool(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void ClosePool(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkPool(const morkPool& other);
+ morkPool& operator=(const morkPool& other);
+
+ public: // dynamic type identification
+ mork_bool IsPool() const {
+ return IsNode() && mNode_Derived == morkDerived_kPool;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ void NonPoolTypeError(morkEnv* ev);
+
+ public: // morkNode memory management operators
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkNode::MakeNew(inSize, ioHeap, ev);
+ }
+
+ void* operator new(size_t inSize) noexcept(true) {
+ return ::operator new(inSize);
+ }
+
+ public: // other pool methods
+ // alloc and free individual instances of handles (inside hand frames):
+ morkHandleFace* NewHandle(morkEnv* ev, mork_size inSize, morkZone* ioZone);
+ void ZapHandle(morkEnv* ev, morkHandleFace* ioHandle);
+
+ // alloc and free individual instances of rows:
+ morkRow* NewRow(morkEnv* ev, morkZone* ioZone); // alloc new row instance
+ void ZapRow(morkEnv* ev, morkRow* ioRow,
+ morkZone* ioZone); // free old row instance
+
+ // alloc and free entire vectors of cells (not just one cell at a time)
+ morkCell* NewCells(morkEnv* ev, mork_size inSize, morkZone* ioZone);
+ void ZapCells(morkEnv* ev, morkCell* ioVector, mork_size inSize,
+ morkZone* ioZone);
+
+ // resize (grow or trim) cell vectors inside a containing row instance
+ mork_bool AddRowCells(morkEnv* ev, morkRow* ioRow, mork_size inNewSize,
+ morkZone* ioZone);
+ mork_bool CutRowCells(morkEnv* ev, morkRow* ioRow, mork_size inNewSize,
+ morkZone* ioZone);
+
+ // alloc & free individual instances of atoms (lots of atom subclasses):
+ void ZapAtom(morkEnv* ev, morkAtom* ioAtom,
+ morkZone* ioZone); // any subclass (by kind)
+
+ morkOidAtom* NewRowOidAtom(morkEnv* ev, const mdbOid& inOid,
+ morkZone* ioZone);
+ morkOidAtom* NewTableOidAtom(morkEnv* ev, const mdbOid& inOid,
+ morkZone* ioZone);
+
+ morkAtom* NewAnonAtom(morkEnv* ev, const morkBuf& inBuf, mork_cscode inForm,
+ morkZone* ioZone);
+ // if inForm is zero, and inBuf.mBuf_Fill is less than 256, then a 'wee'
+ // anon atom will be created, and otherwise a 'big' anon atom.
+
+ morkBookAtom* NewBookAtom(morkEnv* ev, const morkBuf& inBuf,
+ mork_cscode inForm, morkAtomSpace* ioSpace,
+ mork_aid inAid, morkZone* ioZone);
+ // if inForm is zero, and inBuf.mBuf_Fill is less than 256, then a 'wee'
+ // book atom will be created, and otherwise a 'big' book atom.
+
+ morkBookAtom* NewBookAtomCopy(morkEnv* ev, const morkBigBookAtom& inAtom,
+ morkZone* ioZone);
+ // make the smallest kind of book atom that can hold content in inAtom.
+ // The inAtom parameter is often expected to be a staged book atom in
+ // the store, which was used to search an atom space for existing atoms.
+
+ morkBookAtom* NewFarBookAtomCopy(morkEnv* ev, const morkFarBookAtom& inAtom,
+ morkZone* ioZone);
+ // make the smallest kind of book atom that can hold content in inAtom.
+ // The inAtom parameter is often expected to be a staged book atom in
+ // the store, which was used to search an atom space for existing atoms.
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakPool(morkPool* me, morkEnv* ev, morkPool** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongPool(morkPool* me, morkEnv* ev, morkPool** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKPOOL_ */
diff --git a/comm/mailnews/db/mork/morkPortTableCursor.cpp b/comm/mailnews/db/mork/morkPortTableCursor.cpp
new file mode 100644
index 0000000000..8f4f62411d
--- /dev/null
+++ b/comm/mailnews/db/mork/morkPortTableCursor.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKPORTTABLECURSOR_
+# include "morkPortTableCursor.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkPortTableCursor::CloseMorkNode(
+ morkEnv* ev) // ClosePortTableCursor() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->ClosePortTableCursor(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkPortTableCursor::~morkPortTableCursor() // ClosePortTableCursor() executed
+ // earlier
+{
+ CloseMorkNode(mMorkEnv);
+}
+
+/*public non-poly*/
+morkPortTableCursor::morkPortTableCursor(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkStore* ioStore,
+ mdb_scope inRowScope,
+ mdb_kind inTableKind,
+ nsIMdbHeap* ioSlotHeap)
+ : morkCursor(ev, inUsage, ioHeap),
+ mPortTableCursor_Store(0),
+ mPortTableCursor_RowScope((mdb_scope)-1) // we want != inRowScope
+ ,
+ mPortTableCursor_TableKind((mdb_kind)-1) // we want != inTableKind
+ ,
+ mPortTableCursor_LastTable(0) // not refcounted
+ ,
+ mPortTableCursor_RowSpace(0) // strong ref to row space
+ ,
+ mPortTableCursor_TablesDidEnd(morkBool_kFalse),
+ mPortTableCursor_SpacesDidEnd(morkBool_kFalse) {
+ if (ev->Good()) {
+ if (ioStore && ioSlotHeap) {
+ mCursor_Pos = -1;
+ mCursor_Seed = 0; // let the iterator do its own seed handling
+ morkStore::SlotWeakStore(ioStore, ev, &mPortTableCursor_Store);
+
+ if (this->SetRowScope(ev, inRowScope))
+ this->SetTableKind(ev, inTableKind);
+
+ if (ev->Good()) mNode_Derived = morkDerived_kPortTableCursor;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkPortTableCursor, morkCursor,
+ nsIMdbPortTableCursor)
+
+morkEnv* morkPortTableCursor::CanUsePortTableCursor(nsIMdbEnv* mev,
+ mork_bool inMutable,
+ nsresult* outErr) const {
+ morkEnv* outEnv = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (IsPortTableCursor())
+ outEnv = ev;
+ else
+ NonPortTableCursorTypeError(ev);
+ *outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outEnv);
+ return outEnv;
+}
+
+/*public non-poly*/ void morkPortTableCursor::ClosePortTableCursor(
+ morkEnv* ev) {
+ if (this->IsNode()) {
+ mCursor_Pos = -1;
+ mCursor_Seed = 0;
+ mPortTableCursor_LastTable = 0;
+ morkStore::SlotWeakStore((morkStore*)0, ev, &mPortTableCursor_Store);
+ morkRowSpace::SlotStrongRowSpace((morkRowSpace*)0, ev,
+ &mPortTableCursor_RowSpace);
+ this->CloseCursor(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkPortTableCursor::NilCursorStoreError(morkEnv* ev) {
+ ev->NewError("nil mPortTableCursor_Store");
+}
+
+/*static*/ void morkPortTableCursor::NonPortTableCursorTypeError(morkEnv* ev) {
+ ev->NewError("non morkPortTableCursor");
+}
+
+mork_bool morkPortTableCursor::SetRowScope(morkEnv* ev, mork_scope inRowScope) {
+ mPortTableCursor_RowScope = inRowScope;
+ mPortTableCursor_LastTable = 0; // restart iteration of space
+
+ mPortTableCursor_TableIter.CloseMapIter(ev);
+ mPortTableCursor_TablesDidEnd = morkBool_kTrue;
+ mPortTableCursor_SpacesDidEnd = morkBool_kTrue;
+
+ morkStore* store = mPortTableCursor_Store;
+ if (store) {
+ morkRowSpace* space = mPortTableCursor_RowSpace;
+
+ if (inRowScope) // intend to cover a specific scope only?
+ {
+ space = store->LazyGetRowSpace(ev, inRowScope);
+ morkRowSpace::SlotStrongRowSpace(space, ev, &mPortTableCursor_RowSpace);
+
+ // We want mPortTableCursor_SpacesDidEnd == morkBool_kTrue
+ // to show this is the only space to be covered.
+ } else // prepare space map iter to cover all space scopes
+ {
+ morkRowSpaceMapIter* rsi = &mPortTableCursor_SpaceIter;
+ rsi->InitRowSpaceMapIter(ev, &store->mStore_RowSpaces);
+
+ space = 0;
+ (void)rsi->FirstRowSpace(ev, (mork_scope*)0, &space);
+ morkRowSpace::SlotStrongRowSpace(space, ev, &mPortTableCursor_RowSpace);
+
+ if (space) // found first space in store
+ mPortTableCursor_SpacesDidEnd = morkBool_kFalse;
+ }
+
+ this->init_space_tables_map(ev);
+ } else
+ this->NilCursorStoreError(ev);
+
+ return ev->Good();
+}
+
+void morkPortTableCursor::init_space_tables_map(morkEnv* ev) {
+ morkRowSpace* space = mPortTableCursor_RowSpace;
+ if (space && ev->Good()) {
+ morkTableMapIter* ti = &mPortTableCursor_TableIter;
+ ti->InitTableMapIter(ev, &space->mRowSpace_Tables);
+ if (ev->Good()) mPortTableCursor_TablesDidEnd = morkBool_kFalse;
+ }
+}
+
+mork_bool morkPortTableCursor::SetTableKind(morkEnv* ev,
+ mork_kind inTableKind) {
+ mPortTableCursor_TableKind = inTableKind;
+ mPortTableCursor_LastTable = 0; // restart iteration of space
+
+ mPortTableCursor_TablesDidEnd = morkBool_kTrue;
+
+ morkRowSpace* space = mPortTableCursor_RowSpace;
+ if (!space && mPortTableCursor_RowScope == 0) {
+ this->SetRowScope(ev, 0);
+ }
+ this->init_space_tables_map(ev);
+
+ return ev->Good();
+}
+
+morkRowSpace* morkPortTableCursor::NextSpace(morkEnv* ev) {
+ morkRowSpace* outSpace = 0;
+ mPortTableCursor_LastTable = 0;
+ mPortTableCursor_SpacesDidEnd = morkBool_kTrue;
+ mPortTableCursor_TablesDidEnd = morkBool_kTrue;
+
+ if (!mPortTableCursor_RowScope) // not just one scope?
+ {
+ morkStore* store = mPortTableCursor_Store;
+ if (store) {
+ morkRowSpaceMapIter* rsi = &mPortTableCursor_SpaceIter;
+
+ (void)rsi->NextRowSpace(ev, (mork_scope*)0, &outSpace);
+ morkRowSpace::SlotStrongRowSpace(outSpace, ev,
+ &mPortTableCursor_RowSpace);
+
+ if (outSpace) // found next space in store
+ {
+ mPortTableCursor_SpacesDidEnd = morkBool_kFalse;
+
+ this->init_space_tables_map(ev);
+
+ if (ev->Bad()) outSpace = 0;
+ }
+ } else
+ this->NilCursorStoreError(ev);
+ }
+
+ return outSpace;
+}
+
+morkTable* morkPortTableCursor::NextTable(morkEnv* ev) {
+ mork_kind kind = mPortTableCursor_TableKind;
+
+ do // until spaces end, or until we find a table in a space
+ {
+ morkRowSpace* space = mPortTableCursor_RowSpace;
+ if (mPortTableCursor_TablesDidEnd) // current space exhausted?
+ space = this->NextSpace(ev); // go on to the next space
+
+ if (space) // have a space remaining that might hold tables?
+ {
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ morkTableMapIter* ti = &mPortTableCursor_TableIter;
+ morkTable* table =
+ (mPortTableCursor_LastTable) ? ti->NextTable(ev) : ti->FirstTable(ev);
+
+ for (; table && ev->Good(); table = ti->NextTable(ev))
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ mork_tid* key = 0; // ignore keys in table map
+ morkTable* table = 0; // old value table in the map
+ morkTableMapIter* ti = &mPortTableCursor_TableIter;
+ mork_change* c = (mPortTableCursor_LastTable)
+ ? ti->NextTable(ev, key, &table)
+ : ti->FirstTable(ev, key, &table);
+
+ for (; c && ev->Good(); c = ti->NextTable(ev, key, &table))
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+ {
+ if (table && table->IsTable()) {
+ if (!kind || kind == table->mTable_Kind) {
+ mPortTableCursor_LastTable = table; // ti->NextTable() hence
+ return table;
+ }
+ } else
+ table->NonTableTypeWarning(ev);
+ }
+ mPortTableCursor_TablesDidEnd = morkBool_kTrue; // space is done
+ mPortTableCursor_LastTable = 0; // make sure next space starts fresh
+ }
+
+ } while (ev->Good() && !mPortTableCursor_SpacesDidEnd);
+
+ return (morkTable*)0;
+}
+
+// { ----- begin table iteration methods -----
+
+// { ===== begin nsIMdbPortTableCursor methods =====
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP
+morkPortTableCursor::SetPort(nsIMdbEnv* mev, nsIMdbPort* ioPort) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkPortTableCursor::GetPort(nsIMdbEnv* mev, nsIMdbPort** acqPort) {
+ nsresult outErr = NS_OK;
+ nsIMdbPort* outPort = 0;
+ morkEnv* ev =
+ this->CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ if (mPortTableCursor_Store)
+ outPort = mPortTableCursor_Store->AcquireStoreHandle(ev);
+ outErr = ev->AsErr();
+ }
+ if (acqPort) *acqPort = outPort;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkPortTableCursor::SetRowScope(nsIMdbEnv* mev, // sets pos to -1
+ mdb_scope inRowScope) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev =
+ this->CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ mCursor_Pos = -1;
+
+ SetRowScope(ev, inRowScope);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkPortTableCursor::GetRowScope(nsIMdbEnv* mev, mdb_scope* outRowScope) {
+ nsresult outErr = NS_OK;
+ mdb_scope rowScope = 0;
+ morkEnv* ev =
+ this->CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ rowScope = mPortTableCursor_RowScope;
+ outErr = ev->AsErr();
+ }
+ *outRowScope = rowScope;
+ return outErr;
+}
+// setting row scope to zero iterates over all row scopes in port
+
+NS_IMETHODIMP
+morkPortTableCursor::SetTableKind(nsIMdbEnv* mev, // sets pos to -1
+ mdb_kind inTableKind) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev =
+ this->CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ mCursor_Pos = -1;
+
+ SetTableKind(ev, inTableKind);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkPortTableCursor::GetTableKind(nsIMdbEnv* mev, mdb_kind* outTableKind)
+// setting table kind to zero iterates over all table kinds in row scope
+{
+ nsresult outErr = NS_OK;
+ mdb_kind tableKind = 0;
+ morkEnv* ev =
+ this->CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ tableKind = mPortTableCursor_TableKind;
+ outErr = ev->AsErr();
+ }
+ *outTableKind = tableKind;
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin table iteration methods -----
+NS_IMETHODIMP
+morkPortTableCursor::NextTable( // get table at next position in the db
+ nsIMdbEnv* mev, // context
+ nsIMdbTable** acqTable) {
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev =
+ CanUsePortTableCursor(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = NextTable(ev);
+ if (table && ev->Good()) outTable = table->AcquireTableHandle(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkPortTableCursor.h b/comm/mailnews/db/mork/morkPortTableCursor.h
new file mode 100644
index 0000000000..2a7bf3d0e9
--- /dev/null
+++ b/comm/mailnews/db/mork/morkPortTableCursor.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKPORTTABLECURSOR_
+#define _MORKPORTTABLECURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class orkinPortTableCursor;
+#define morkDerived_kPortTableCursor /*i*/ 0x7443 /* ascii 'tC' */
+
+class morkPortTableCursor : public morkCursor,
+ public nsIMdbPortTableCursor { // row iterator
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkFactory* mObject_Factory; // weak ref to suite factory
+
+ // mork_seed mCursor_Seed;
+ // mork_pos mCursor_Pos;
+ // mork_bool mCursor_DoFailOnSeedOutOfSync;
+ // mork_u1 mCursor_Pad[ 3 ]; // explicitly pad to u4 alignment
+
+ public: // state is public because the entire Mork system is private
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetPort(nsIMdbEnv* ev,
+ nsIMdbPort* ioPort) override; // sets pos to -1
+ NS_IMETHOD GetPort(nsIMdbEnv* ev, nsIMdbPort** acqPort) override;
+
+ NS_IMETHOD SetRowScope(nsIMdbEnv* ev, // sets pos to -1
+ mdb_scope inRowScope) override;
+ NS_IMETHOD GetRowScope(nsIMdbEnv* ev, mdb_scope* outRowScope) override;
+ // setting row scope to zero iterates over all row scopes in port
+
+ NS_IMETHOD SetTableKind(nsIMdbEnv* ev, // sets pos to -1
+ mdb_kind inTableKind) override;
+ NS_IMETHOD GetTableKind(nsIMdbEnv* ev, mdb_kind* outTableKind) override;
+ // setting table kind to zero iterates over all table kinds in row scope
+ // } ----- end attribute methods -----
+
+ // { ----- begin table iteration methods -----
+ NS_IMETHOD NextTable( // get table at next position in the db
+ nsIMdbEnv* ev, // context
+ nsIMdbTable** acqTable) override; // the next table in the iteration
+ // } ----- end table iteration methods -----
+ morkStore* mPortTableCursor_Store; // weak ref to store
+
+ mdb_scope mPortTableCursor_RowScope;
+ mdb_kind mPortTableCursor_TableKind;
+
+ // We only care if LastTable is non-nil, so it is not refcounted;
+ // so you must never access table state or methods using LastTable:
+
+ morkTable* mPortTableCursor_LastTable; // nil or last table (no refcount)
+ morkRowSpace* mPortTableCursor_RowSpace; // current space (strong ref)
+
+ morkRowSpaceMapIter mPortTableCursor_SpaceIter; // iter over spaces
+ morkTableMapIter mPortTableCursor_TableIter; // iter over tables
+
+ // these booleans indicate when the table or space iterator is exhausted:
+
+ mork_bool mPortTableCursor_TablesDidEnd; // no more tables?
+ mork_bool mPortTableCursor_SpacesDidEnd; // no more spaces?
+ mork_u1 mPortTableCursor_Pad[2]; // for u4 alignment
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // ClosePortTableCursor()
+
+ public: // morkPortTableCursor construction & destruction
+ morkPortTableCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkStore* ioStore, mdb_scope inRowScope,
+ mdb_kind inTableKind, nsIMdbHeap* ioSlotHeap);
+ void ClosePortTableCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkPortTableCursor(const morkPortTableCursor& other);
+ morkPortTableCursor& operator=(const morkPortTableCursor& other);
+
+ public: // dynamic type identification
+ mork_bool IsPortTableCursor() const {
+ return IsNode() && mNode_Derived == morkDerived_kPortTableCursor;
+ }
+ // } ===== end morkNode methods =====
+
+ protected: // utilities
+ virtual ~morkPortTableCursor(); // assert that close executed earlier
+
+ void init_space_tables_map(morkEnv* ev);
+
+ public: // other cursor methods
+ static void NilCursorStoreError(morkEnv* ev);
+ static void NonPortTableCursorTypeError(morkEnv* ev);
+
+ morkEnv* CanUsePortTableCursor(nsIMdbEnv* mev, mork_bool inMutable,
+ nsresult* outErr) const;
+
+ morkRowSpace* NextSpace(morkEnv* ev);
+ morkTable* NextTable(morkEnv* ev);
+
+ mork_bool SetRowScope(morkEnv* ev, mork_scope inRowScope);
+ mork_bool SetTableKind(morkEnv* ev, mork_kind inTableKind);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakPortTableCursor(morkPortTableCursor* me, morkEnv* ev,
+ morkPortTableCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongPortTableCursor(morkPortTableCursor* me, morkEnv* ev,
+ morkPortTableCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKPORTTABLECURSOR_ */
diff --git a/comm/mailnews/db/mork/morkProbeMap.cpp b/comm/mailnews/db/mork/morkProbeMap.cpp
new file mode 100644
index 0000000000..6c9c3ecb6c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkProbeMap.cpp
@@ -0,0 +1,1107 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This code is a port to NS Mork from public domain Mithril C++ sources.
+// Note many code comments here come verbatim from cut-and-pasted Mithril.
+// In many places, code is identical; Mithril versions stay public domain.
+// Changes in porting are mainly class type and scalar type name changes.
+
+#include "nscore.h"
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKPROBEMAP_
+# include "morkProbeMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+/*============================================================================*/
+/* morkMapScratch */
+
+void morkMapScratch::halt_map_scratch(morkEnv* ev) {
+ nsIMdbHeap* heap = sMapScratch_Heap;
+
+ if (heap) {
+ if (sMapScratch_Keys) heap->Free(ev->AsMdbEnv(), sMapScratch_Keys);
+ if (sMapScratch_Vals) heap->Free(ev->AsMdbEnv(), sMapScratch_Vals);
+ }
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*============================================================================*/
+/* morkProbeMap */
+
+void morkProbeMap::ProbeMapBadTagError(morkEnv* ev) const {
+ ev->NewError("bad sProbeMap_Tag");
+}
+
+void morkProbeMap::WrapWithNoVoidSlotError(morkEnv* ev) const {
+ ev->NewError("wrap without void morkProbeMap slot");
+}
+
+void morkProbeMap::GrowFailsMaxFillError(morkEnv* ev) const {
+ ev->NewError("grow fails morkEnv > sMap_Fill");
+}
+
+void morkProbeMap::MapKeyIsNotIPError(morkEnv* ev) const {
+ ev->NewError("not sMap_KeyIsIP");
+}
+
+void morkProbeMap::MapValIsNotIPError(morkEnv* ev) const {
+ ev->NewError("not sMap_ValIsIP");
+}
+
+void morkProbeMap::rehash_old_map(morkEnv* ev, morkMapScratch* ioScratch) {
+ mork_size keySize = sMap_KeySize; // size of every key bucket
+ mork_size valSize = sMap_ValSize; // size of every associated value
+
+ mork_count slots = sMap_Slots; // number of new buckets
+ mork_u1* keys = sMap_Keys; // destination for rehashed keys
+ mork_u1* vals = sMap_Vals; // destination for any copied values
+
+ mork_bool keyIsIP = (keys && keySize == sizeof(mork_ip) && sMap_KeyIsIP);
+ mork_bool valIsIP = (vals && valSize == sizeof(mork_ip) && sMap_ValIsIP);
+
+ mork_count oldSlots = ioScratch->sMapScratch_Slots; // sMap_Slots
+ mork_u1* oldKeys = ioScratch->sMapScratch_Keys; // sMap_Keys
+ mork_u1* oldVals = ioScratch->sMapScratch_Vals; // sMap_Vals
+ mork_u1* end = oldKeys + (keySize * oldSlots); // one byte past last key
+
+ mork_fill fill = 0; // let's count the actual fill for a double check
+
+ while (oldKeys < end) // another old key bucket to rehash if non-nil?
+ {
+ if (!this->ProbeMapIsKeyNil(ev, oldKeys)) // need to rehash?
+ {
+ ++fill; // this had better match sMap_Fill when we are all done
+ mork_u4 hash = this->ProbeMapHashMapKey(ev, oldKeys);
+
+ mork_pos i = hash % slots; // target hash bucket
+ mork_pos startPos = i; // remember start to detect
+
+ mork_u1* k = keys + (i * keySize);
+ while (!this->ProbeMapIsKeyNil(ev, k)) {
+ if (++i >=
+ (mork_pos)slots) // advanced past end? need to wrap around now?
+ i = 0; // wrap around to first slot in map's hash table
+
+ if (i == startPos) // no void slots were found anywhere in map?
+ {
+ this->WrapWithNoVoidSlotError(ev); // should never happen
+ return; // this is bad, and we can't go on with the rehash
+ }
+ k = keys + (i * keySize);
+ }
+ if (keyIsIP) // int special case?
+ *((mork_ip*)k) = *((const mork_ip*)oldKeys); // fast bitwise copy
+ else
+ MORK_MEMCPY(k, oldKeys, keySize); // slow bitwise copy
+
+ if (oldVals) // need to copy values as well?
+ {
+ mork_size valOffset = (i * valSize);
+ mork_u1* v = vals + valOffset;
+ mork_u1* ov = oldVals + valOffset;
+ if (valIsIP) // int special case?
+ *((mork_ip*)v) = *((const mork_ip*)ov); // fast bitwise copy
+ else
+ MORK_MEMCPY(v, ov, valSize); // slow bitwise copy
+ }
+ }
+ oldKeys += keySize; // advance to next key bucket in old map
+ }
+ if (fill != sMap_Fill) // is the recorded value of sMap_Fill wrong?
+ {
+ ev->NewWarning("fill != sMap_Fill");
+ sMap_Fill = fill;
+ }
+}
+
+mork_bool morkProbeMap::grow_probe_map(morkEnv* ev) {
+ if (sMap_Heap) // can we grow the map?
+ {
+ mork_num newSlots = ((sMap_Slots * 4) / 3) + 1; // +25%
+ morkMapScratch old; // a place to temporarily hold all the old arrays
+ if (this->new_slots(ev, &old, newSlots)) // have more?
+ {
+ ++sMap_Seed; // note the map has changed
+ this->rehash_old_map(ev, &old);
+
+ if (ev->Good()) {
+ mork_count slots = sMap_Slots;
+ mork_num emptyReserve = (slots / 7) + 1; // keep this many empty
+ mork_fill maxFill = slots - emptyReserve; // new max occupancy
+ if (maxFill > sMap_Fill) // new max is bigger than old occupancy?
+ sProbeMap_MaxFill = maxFill; // we can install new max for fill
+ else
+ this->GrowFailsMaxFillError(ev); // we have invariant failure
+ }
+
+ if (ev->Bad()) // rehash failed? need to revert map to last state?
+ this->revert_map(ev, &old); // swap the vectors back again
+
+ old.halt_map_scratch(ev); // remember to free the old arrays
+ }
+ } else
+ ev->OutOfMemoryError();
+
+ return ev->Good();
+}
+
+void morkProbeMap::revert_map(morkEnv* ev, morkMapScratch* ioScratch) {
+ mork_count tempSlots = ioScratch->sMapScratch_Slots; // sMap_Slots
+ mork_u1* tempKeys = ioScratch->sMapScratch_Keys; // sMap_Keys
+ mork_u1* tempVals = ioScratch->sMapScratch_Vals; // sMap_Vals
+
+ ioScratch->sMapScratch_Slots = sMap_Slots;
+ ioScratch->sMapScratch_Keys = sMap_Keys;
+ ioScratch->sMapScratch_Vals = sMap_Vals;
+
+ sMap_Slots = tempSlots;
+ sMap_Keys = tempKeys;
+ sMap_Vals = tempVals;
+}
+
+void morkProbeMap::put_probe_kv(morkEnv* ev, const void* inAppKey,
+ const void* inAppVal, mork_pos inPos) {
+ mork_u1* mapVal = 0;
+ mork_u1* mapKey = 0;
+
+ mork_num valSize = sMap_ValSize;
+ if (valSize && inAppVal) // map holds values? caller sends value?
+ {
+ mork_u1* val = sMap_Vals + (valSize * inPos);
+ if (valSize == sizeof(mork_ip) && sMap_ValIsIP) // int special case?
+ *((mork_ip*)val) = *((const mork_ip*)inAppVal);
+ else
+ mapVal = val; // show possible need to call ProbeMapPushIn()
+ }
+ if (inAppKey) // caller sends the key?
+ {
+ mork_num keySize = sMap_KeySize;
+ mork_u1* key = sMap_Keys + (keySize * inPos);
+ if (keySize == sizeof(mork_ip) && sMap_KeyIsIP) // int special case?
+ *((mork_ip*)key) = *((const mork_ip*)inAppKey);
+ else
+ mapKey = key; // show possible need to call ProbeMapPushIn()
+ } else
+ ev->NilPointerError();
+
+ if ((inAppVal && mapVal) || (inAppKey && mapKey))
+ this->ProbeMapPushIn(ev, inAppKey, inAppVal, mapKey, mapVal);
+
+ if (sMap_Fill > sProbeMap_MaxFill) this->grow_probe_map(ev);
+}
+
+void morkProbeMap::get_probe_kv(morkEnv* ev, void* outAppKey, void* outAppVal,
+ mork_pos inPos) const {
+ const mork_u1* mapVal = 0;
+ const mork_u1* mapKey = 0;
+
+ mork_num valSize = sMap_ValSize;
+ if (valSize && outAppVal) // map holds values? caller wants value?
+ {
+ const mork_u1* val = sMap_Vals + (valSize * inPos);
+ if (valSize == sizeof(mork_ip) && sMap_ValIsIP) // int special case?
+ *((mork_ip*)outAppVal) = *((const mork_ip*)val);
+ else
+ mapVal = val; // show possible need to call ProbeMapPullOut()
+ }
+ if (outAppKey) // caller wants the key?
+ {
+ mork_num keySize = sMap_KeySize;
+ const mork_u1* key = sMap_Keys + (keySize * inPos);
+ if (keySize == sizeof(mork_ip) && sMap_KeyIsIP) // int special case?
+ *((mork_ip*)outAppKey) = *((const mork_ip*)key);
+ else
+ mapKey = key; // show possible need to call ProbeMapPullOut()
+ }
+ if ((outAppVal && mapVal) || (outAppKey && mapKey))
+ this->ProbeMapPullOut(ev, mapKey, mapVal, outAppKey, outAppVal);
+}
+
+mork_test morkProbeMap::find_key_pos(morkEnv* ev, const void* inAppKey,
+ mork_u4 inHash, mork_pos* outPos) const {
+ mork_u1* k = sMap_Keys; // array of keys, each of size sMap_KeySize
+ mork_num size = sMap_KeySize; // number of bytes in each key
+ mork_count slots = sMap_Slots; // total number of key buckets
+ mork_pos i = inHash % slots; // target hash bucket
+ mork_pos startPos = i; // remember start to detect
+
+ mork_test outTest = this->MapTest(ev, k + (i * size), inAppKey);
+ while (outTest == morkTest_kMiss) {
+ if (++i >=
+ (mork_pos)slots) // advancing goes beyond end? need to wrap around now?
+ i = 0; // wrap around to first slot in map's hash table
+
+ if (i == startPos) // no void slots were found anywhere in map?
+ {
+ this->WrapWithNoVoidSlotError(ev); // should never happen
+ break; // end loop on kMiss; note caller expects either kVoid or kHit
+ }
+ outTest = this->MapTest(ev, k + (i * size), inAppKey);
+ }
+ *outPos = i;
+
+ return outTest;
+}
+
+void morkProbeMap::probe_map_lazy_init(morkEnv* ev) {
+ if (this->need_lazy_init() && sMap_Fill == 0) // pending lazy action?
+ {
+ // The constructor cannot successfully call virtual ProbeMapClearKey(),
+ // so we lazily do so now, when we add the first member to the map.
+
+ mork_u1* keys = sMap_Keys;
+ if (keys) // okay to call lazy virtual clear method on new map keys?
+ {
+ if (sProbeMap_ZeroIsClearKey) // zero is good enough to clear keys?
+ {
+ mork_num keyVolume = sMap_Slots * sMap_KeySize;
+ if (keyVolume) MORK_MEMSET(keys, 0, keyVolume);
+ } else
+ this->ProbeMapClearKey(ev, keys, sMap_Slots);
+ } else
+ this->MapNilKeysError(ev);
+ }
+ sProbeMap_LazyClearOnAdd = 0; // don't do this ever again
+}
+
+mork_bool morkProbeMap::MapAtPut(morkEnv* ev, const void* inAppKey,
+ const void* inAppVal, void* outAppKey,
+ void* outAppVal) {
+ mork_bool outPut = morkBool_kFalse;
+
+ if (this->GoodProbeMap()) /* looks good? */
+ {
+ if (this->need_lazy_init() && sMap_Fill == 0) // pending lazy action?
+ this->probe_map_lazy_init(ev);
+
+ if (ev->Good()) {
+ mork_pos slotPos = 0;
+ mork_u4 hash = this->MapHash(ev, inAppKey);
+ mork_test test = this->find_key_pos(ev, inAppKey, hash, &slotPos);
+ outPut = (test == morkTest_kHit);
+
+ if (outPut) // replacing an old assoc? no change in member count?
+ {
+ if (outAppKey || outAppVal) /* copy old before cobber? */
+ this->get_probe_kv(ev, outAppKey, outAppVal, slotPos);
+ } else // adding a new assoc increases membership by one
+ {
+ ++sMap_Fill; /* one more member in the collection */
+ }
+
+ if (test != morkTest_kMiss) /* found slot to hold new assoc? */
+ {
+ ++sMap_Seed; /* note the map has changed */
+ this->put_probe_kv(ev, inAppKey, inAppVal, slotPos);
+ }
+ }
+ } else
+ this->ProbeMapBadTagError(ev);
+
+ return outPut;
+}
+
+mork_bool morkProbeMap::MapAt(morkEnv* ev, const void* inAppKey,
+ void* outAppKey, void* outAppVal) {
+ if (this->GoodProbeMap()) /* looks good? */
+ {
+ if (this->need_lazy_init() && sMap_Fill == 0) // pending lazy action?
+ this->probe_map_lazy_init(ev);
+
+ mork_pos slotPos = 0;
+ mork_u4 hash = this->MapHash(ev, inAppKey);
+ mork_test test = this->find_key_pos(ev, inAppKey, hash, &slotPos);
+ if (test == morkTest_kHit) /* found an assoc pair for inAppKey? */
+ {
+ this->get_probe_kv(ev, outAppKey, outAppVal, slotPos);
+ return morkBool_kTrue;
+ }
+ } else
+ this->ProbeMapBadTagError(ev);
+
+ return morkBool_kFalse;
+}
+
+mork_num morkProbeMap::MapCutAll(morkEnv* ev) {
+ mork_num outCutAll = 0;
+
+ if (this->GoodProbeMap()) /* looks good? */
+ {
+ outCutAll = sMap_Fill; /* number of members cut, which is all of them */
+
+ if (sMap_Keys && !sProbeMap_ZeroIsClearKey)
+ this->ProbeMapClearKey(ev, sMap_Keys, sMap_Slots);
+
+ sMap_Fill = 0; /* map now has no members */
+ } else
+ this->ProbeMapBadTagError(ev);
+
+ return outCutAll;
+}
+
+// { ===== node interface =====
+
+/*virtual*/
+morkProbeMap::~morkProbeMap() // assert NodeStop() finished earlier
+{
+ MORK_ASSERT(sMap_Keys == 0);
+ MORK_ASSERT(sProbeMap_Tag == 0);
+}
+
+/*public virtual*/ void morkProbeMap::CloseMorkNode(
+ morkEnv* ev) // CloseMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseProbeMap(ev);
+ this->MarkShut();
+ }
+}
+
+void morkProbeMap::CloseProbeMap(morkEnv* ev) {
+ if (this->IsNode()) {
+ nsIMdbHeap* heap = sMap_Heap;
+ if (heap) // able to free map arrays?
+ {
+ void* block = sMap_Keys;
+ if (block) {
+ heap->Free(ev->AsMdbEnv(), block);
+ sMap_Keys = 0;
+ }
+
+ block = sMap_Vals;
+ if (block) {
+ heap->Free(ev->AsMdbEnv(), block);
+ sMap_Vals = 0;
+ }
+ }
+ sMap_Keys = 0;
+ sMap_Vals = 0;
+
+ this->CloseNode(ev);
+ sProbeMap_Tag = 0;
+ sProbeMap_MaxFill = 0;
+
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+void* morkProbeMap::clear_alloc(morkEnv* ev, mork_size inSize) {
+ void* p = 0;
+ nsIMdbHeap* heap = sMap_Heap;
+ if (heap) {
+ if (NS_SUCCEEDED(heap->Alloc(ev->AsMdbEnv(), inSize, (void**)&p)) && p) {
+ MORK_MEMSET(p, 0, inSize);
+ return p;
+ }
+ } else
+ ev->NilPointerError();
+
+ return (void*)0;
+}
+
+/*| map_new_keys: allocate an array of inSlots new keys filled with zero.
+**| (cf IronDoc's FeHashTable_new_keys())
+|*/
+mork_u1* morkProbeMap::map_new_keys(morkEnv* ev, mork_num inSlots) {
+ mork_num size = inSlots * sMap_KeySize;
+ return (mork_u1*)this->clear_alloc(ev, size);
+}
+
+/*| map_new_vals: allocate an array of inSlots new values filled with zero.
+**| When values are zero sized, we just return a null pointer.
+**|
+**| (cf IronDoc's FeHashTable_new_values())
+|*/
+mork_u1* morkProbeMap::map_new_vals(morkEnv* ev, mork_num inSlots) {
+ mork_u1* values = 0;
+ mork_num size = inSlots * sMap_ValSize;
+ if (size) values = (mork_u1*)this->clear_alloc(ev, size);
+ return values;
+}
+
+void morkProbeMap::MapSeedOutOfSyncError(morkEnv* ev) {
+ ev->NewError("sMap_Seed out of sync");
+}
+
+void morkProbeMap::MapFillUnderflowWarning(morkEnv* ev) {
+ ev->NewWarning("sMap_Fill underflow");
+}
+
+void morkProbeMap::MapNilKeysError(morkEnv* ev) {
+ ev->NewError("nil sMap_Keys");
+}
+
+void morkProbeMap::MapZeroKeySizeError(morkEnv* ev) {
+ ev->NewError("zero sMap_KeySize");
+}
+
+/*static*/
+void morkProbeMap::ProbeMapCutError(morkEnv* ev) {
+ ev->NewError("morkProbeMap cannot cut");
+}
+
+void morkProbeMap::init_probe_map(morkEnv* ev, mork_size inSlots) {
+ // Note we cannot successfully call virtual ProbeMapClearKey() when we
+ // call init_probe_map() inside the constructor; so we leave this problem
+ // to the caller. (The constructor will call ProbeMapClearKey() later
+ // after setting a suitable lazy flag to show this action is pending.)
+
+ if (ev->Good()) {
+ morkMapScratch old;
+
+ if (inSlots < 7) // capacity too small?
+ inSlots = 7; // increase to reasonable minimum
+ else if (inSlots > (128 * 1024)) // requested capacity too big?
+ inSlots = (128 * 1024); // decrease to reasonable maximum
+
+ if (this->new_slots(ev, &old, inSlots)) sProbeMap_Tag = morkProbeMap_kTag;
+
+ mork_count slots = sMap_Slots;
+ mork_num emptyReserve = (slots / 7) + 1; // keep this many empty
+ sProbeMap_MaxFill = slots - emptyReserve;
+
+ MORK_MEMSET(&old, 0, sizeof(morkMapScratch)); // don't bother halting
+ }
+}
+
+mork_bool morkProbeMap::new_slots(morkEnv* ev, morkMapScratch* old,
+ mork_num inSlots) {
+ mork_bool outNew = morkBool_kFalse;
+
+ // Note we cannot successfully call virtual ProbeMapClearKey() when we
+ // call new_slots() inside the constructor; so we leave this problem
+ // to the caller. (The constructor will call ProbeMapClearKey() later
+ // after setting a suitable lazy flag to show this action is pending.)
+
+ // allocate every new array before we continue:
+ mork_u1* newKeys = this->map_new_keys(ev, inSlots);
+ mork_u1* newVals = this->map_new_vals(ev, inSlots);
+
+ // okay for newVals to be null when values are zero sized?
+ mork_bool okayValues = (newVals || !sMap_ValSize);
+
+ if (newKeys && okayValues) {
+ outNew = morkBool_kTrue; // we created every array needed
+
+ // init mapScratch using slots from current map:
+ old->sMapScratch_Heap = sMap_Heap;
+
+ old->sMapScratch_Slots = sMap_Slots;
+ old->sMapScratch_Keys = sMap_Keys;
+ old->sMapScratch_Vals = sMap_Vals;
+
+ // replace all map array slots using the newly allocated members:
+ ++sMap_Seed; // the map has changed
+ sMap_Keys = newKeys;
+ sMap_Vals = newVals;
+ sMap_Slots = inSlots;
+ } else // free any allocations if only partially successful
+ {
+ nsIMdbHeap* heap = sMap_Heap;
+ if (newKeys) heap->Free(ev->AsMdbEnv(), newKeys);
+ if (newVals) heap->Free(ev->AsMdbEnv(), newVals);
+
+ MORK_MEMSET(old, 0, sizeof(morkMapScratch)); // zap scratch space
+ }
+
+ return outNew;
+}
+
+void morkProbeMap::clear_probe_map(morkEnv* ev, nsIMdbHeap* ioMapHeap) {
+ sProbeMap_Tag = 0;
+ sMap_Seed = 0;
+ sMap_Slots = 0;
+ sMap_Fill = 0;
+ sMap_Keys = 0;
+ sMap_Vals = 0;
+ sProbeMap_MaxFill = 0;
+
+ sMap_Heap = ioMapHeap;
+ if (!ioMapHeap) ev->NilPointerError();
+}
+
+morkProbeMap::morkProbeMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioNodeHeap, mork_size inKeySize,
+ mork_size inValSize, nsIMdbHeap* ioMapHeap,
+ mork_size inSlots, mork_bool inZeroIsClearKey)
+
+ : morkNode(ev, inUsage, ioNodeHeap),
+ sMap_Heap(ioMapHeap)
+
+ ,
+ sMap_Keys(0),
+ sMap_Vals(0)
+
+ ,
+ sMap_Seed(0) // change count of members or structure
+
+ ,
+ sMap_Slots(0) // count of slots in the hash table
+ ,
+ sMap_Fill(0) // number of used slots in the hash table
+
+ ,
+ sMap_KeySize(0) // size of each key (cannot be zero)
+ ,
+ sMap_ValSize(0) // size of each val (zero allowed)
+
+ ,
+ sMap_KeyIsIP(morkBool_kFalse) // sMap_KeySize == sizeof(mork_ip)
+ ,
+ sMap_ValIsIP(morkBool_kFalse) // sMap_ValSize == sizeof(mork_ip)
+
+ ,
+ sProbeMap_MaxFill(0),
+ sProbeMap_LazyClearOnAdd(0),
+ sProbeMap_ZeroIsClearKey(inZeroIsClearKey),
+ sProbeMap_Tag(0) {
+ // Note we cannot successfully call virtual ProbeMapClearKey() when we
+ // call init_probe_map() inside the constructor; so we leave this problem
+ // to the caller. (The constructor will call ProbeMapClearKey() later
+ // after setting a suitable lazy flag to show this action is pending.)
+
+ if (ev->Good()) {
+ this->clear_probe_map(ev, ioMapHeap);
+ if (ev->Good()) {
+ sMap_KeySize = inKeySize;
+ sMap_ValSize = inValSize;
+ sMap_KeyIsIP = (inKeySize == sizeof(mork_ip));
+ sMap_ValIsIP = (inValSize == sizeof(mork_ip));
+
+ this->init_probe_map(ev, inSlots);
+ if (ev->Good()) {
+ if (!inZeroIsClearKey) // must lazy clear later with virtual method?
+ sProbeMap_LazyClearOnAdd = morkProbeMap_kLazyClearOnAdd;
+
+ mNode_Derived = morkDerived_kProbeMap;
+ }
+ }
+ }
+}
+
+/*============================================================================*/
+
+/*virtual*/ mork_test // hit(a,b) implies hash(a) == hash(b)
+morkProbeMap::MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const
+// Note inMapKey is always a key already stored in the map, while inAppKey
+// is always a method argument parameter from a client method call.
+// This matters the most in morkProbeMap subclasses, which have the
+// responsibility of putting 'app' keys into slots for 'map' keys, and
+// the bit pattern representation might be different in such cases.
+// morkTest_kHit means that inMapKey equals inAppKey (and this had better
+// also imply that hash(inMapKey) == hash(inAppKey)).
+// morkTest_kMiss means that inMapKey does NOT equal inAppKey (but this
+// implies nothing at all about hash(inMapKey) and hash(inAppKey)).
+// morkTest_kVoid means that inMapKey is not a valid key bit pattern,
+// which means that key slot in the map is not being used. Note that
+// kVoid is only expected as a return value in morkProbeMap subclasses,
+// because morkProbeMap must ask whether a key slot is used or not.
+// morkChainMap however, always knows when a key slot is used, so only
+// key slots expected to have valid bit patterns will be presented to
+// the MapTest() methods for morkChainMap subclasses.
+//
+// NOTE: it is very important that subclasses correctly return the value
+// morkTest_kVoid whenever the slot for inMapKey contains a bit pattern
+// that means the slot is not being used, because this is the only way a
+// probe map can terminate an unsuccessful search for a key in the map.
+{
+ mork_size keySize = sMap_KeySize;
+ if (keySize == sizeof(mork_ip) && sMap_KeyIsIP) {
+ mork_ip mapKey = *((const mork_ip*)inMapKey);
+ if (mapKey == *((const mork_ip*)inAppKey))
+ return morkTest_kHit;
+ else {
+ return (mapKey) ? morkTest_kMiss : morkTest_kVoid;
+ }
+ } else {
+ mork_bool allSame = morkBool_kTrue;
+ mork_bool allZero = morkBool_kTrue;
+ const mork_u1* ak = (const mork_u1*)inAppKey;
+ const mork_u1* mk = (const mork_u1*)inMapKey;
+ const mork_u1* end = mk + keySize;
+ --mk; // prepare for preincrement:
+ while (++mk < end) {
+ mork_u1 byte = *mk;
+ if (byte) // any nonzero byte in map key means slot is not nil?
+ allZero = morkBool_kFalse;
+ if (byte != *ak++) // bytes differ in map and app keys?
+ allSame = morkBool_kFalse;
+ }
+ if (allSame)
+ return morkTest_kHit;
+ else
+ return (allZero) ? morkTest_kVoid : morkTest_kMiss;
+ }
+}
+
+/*virtual*/ mork_u4 // hit(a,b) implies hash(a) == hash(b)
+morkProbeMap::MapHash(morkEnv* ev, const void* inAppKey) const {
+ mork_size keySize = sMap_KeySize;
+ if (keySize == sizeof(mork_ip) && sMap_KeyIsIP) {
+ return *((const mork_ip*)inAppKey);
+ } else {
+ const mork_u1* key = (const mork_u1*)inAppKey;
+ const mork_u1* end = key + keySize;
+ --key; // prepare for preincrement:
+ while (++key < end) {
+ if (*key) // any nonzero byte in map key means slot is not nil?
+ return morkBool_kFalse;
+ }
+ return morkBool_kTrue;
+ }
+ return (mork_u4)NS_PTR_TO_INT32(inAppKey);
+}
+
+/*============================================================================*/
+
+/*virtual*/ mork_u4 morkProbeMap::ProbeMapHashMapKey(morkEnv* ev,
+ const void* inMapKey) const
+// ProbeMapHashMapKey() does logically the same thing as MapHash(), and
+// the default implementation actually calls virtual MapHash(). However,
+// Subclasses must override this method whenever the formats of keys in
+// the map differ from app keys outside the map, because MapHash() only
+// works on keys in 'app' format, while ProbeMapHashMapKey() only works
+// on keys in 'map' format. This method is called in order to rehash all
+// map keys when a map is grown, and this causes all old map members to
+// move into new slot locations.
+//
+// Note it is absolutely imperative that a hash for a key in 'map' format
+// be exactly the same the hash of the same key in 'app' format, or else
+// maps will seem corrupt later when keys in 'app' format cannot be found.
+{
+ return this->MapHash(ev, inMapKey);
+}
+
+/*virtual*/ mork_bool morkProbeMap::ProbeMapIsKeyNil(morkEnv* ev,
+ void* ioMapKey)
+// ProbeMapIsKeyNil() must say whether the representation of logical 'nil'
+// is currently found inside the key at ioMapKey, for a key found within
+// the map. The the map iterator uses this method to find map keys that
+// are actually being used for valid map associations; otherwise the
+// iterator cannot determine which map slots actually denote used keys.
+// The default method version returns true if all the bits equal zero.
+{
+ if (sMap_KeySize == sizeof(mork_ip) && sMap_KeyIsIP) {
+ return !*((const mork_ip*)ioMapKey);
+ } else {
+ const mork_u1* key = (const mork_u1*)ioMapKey;
+ const mork_u1* end = key + sMap_KeySize;
+ --key; // prepare for preincrement:
+ while (++key < end) {
+ if (*key) // any nonzero byte in map key means slot is not nil?
+ return morkBool_kFalse;
+ }
+ return morkBool_kTrue;
+ }
+}
+
+/*virtual*/ void morkProbeMap::ProbeMapClearKey(
+ morkEnv* ev, // put 'nil' into all keys inside map
+ void* ioMapKey, mork_count inKeyCount) // array of keys inside map
+// ProbeMapClearKey() must put some representation of logical 'nil' into
+// every key slot in the map, such that MapTest() will later recognize
+// that this bit pattern shows each key slot is not actually being used.
+//
+// This method is typically called whenever the map is either created or
+// grown into a larger size, where ioMapKey is a pointer to an array of
+// inKeyCount keys, where each key is this->MapKeySize() bytes in size.
+// Note that keys are assumed immediately adjacent with no padding, so
+// if any alignment requirements must be met, then subclasses should have
+// already accounted for this when specifying a key size in the map.
+//
+// Since this method will be called when a map is being grown in size,
+// nothing should be assumed about the state slots of the map, since the
+// ioMapKey array might not yet live in sMap_Keys, and the array length
+// inKeyCount might not yet live in sMap_Slots. However, the value kept
+// in sMap_KeySize never changes, so this->MapKeySize() is always correct.
+{
+ if (ioMapKey && inKeyCount) {
+ MORK_MEMSET(ioMapKey, 0, (inKeyCount * sMap_KeySize));
+ } else
+ ev->NilPointerWarning();
+}
+
+/*virtual*/ void morkProbeMap::ProbeMapPushIn(
+ morkEnv* ev, // move (key,val) into the map
+ const void* inAppKey, const void* inAppVal, // (key,val) outside map
+ void* outMapKey, void* outMapVal) // (key,val) inside map
+// This method actually puts keys and vals in the map in suitable format.
+//
+// ProbeMapPushIn() must copy a caller key and value in 'app' format
+// into the map slots provided, which are in 'map' format. When the
+// 'app' and 'map' formats are identical, then this is just a bitwise
+// copy of this->MapKeySize() key bytes and this->MapValSize() val bytes,
+// and this is exactly what the default implementation performs. However,
+// if 'app' and 'map' formats are different, and MapTest() depends on this
+// difference in format, then subclasses must override this method to do
+// whatever is necessary to store the input app key in output map format.
+//
+// Do NOT write more than this->MapKeySize() bytes of a map key, or more
+// than this->MapValSize() bytes of a map val, or corruption might ensue.
+//
+// The inAppKey and inAppVal parameters are the same ones passed into a
+// call to MapAtPut(), and the outMapKey and outMapVal parameters are ones
+// determined by how the map currently positions key inAppKey in the map.
+//
+// Note any key or val parameter can be a null pointer, in which case
+// this method must do nothing with those parameters. In particular, do
+// no key move at all when either inAppKey or outMapKey is nil, and do
+// no val move at all when either inAppVal or outMapVal is nil. Note that
+// outMapVal should always be nil when this->MapValSize() is nil.
+{}
+
+/*virtual*/ void morkProbeMap::ProbeMapPullOut(
+ morkEnv* ev, // move (key,val) out from the map
+ const void* inMapKey, const void* inMapVal, // (key,val) inside map
+ void* outAppKey, void* outAppVal) const // (key,val) outside map
+// This method actually gets keys and vals from the map in suitable format.
+//
+// ProbeMapPullOut() must copy a key and val in 'map' format into the
+// caller key and val slots provided, which are in 'app' format. When the
+// 'app' and 'map' formats are identical, then this is just a bitwise
+// copy of this->MapKeySize() key bytes and this->MapValSize() val bytes,
+// and this is exactly what the default implementation performs. However,
+// if 'app' and 'map' formats are different, and MapTest() depends on this
+// difference in format, then subclasses must override this method to do
+// whatever is necessary to store the input map key in output app format.
+//
+// The outAppKey and outAppVal parameters are the same ones passed into a
+// call to either MapAtPut() or MapAt(), while inMapKey and inMapVal are
+// determined by how the map currently positions the target key in the map.
+//
+// Note any key or val parameter can be a null pointer, in which case
+// this method must do nothing with those parameters. In particular, do
+// no key move at all when either inMapKey or outAppKey is nil, and do
+// no val move at all when either inMapVal or outAppVal is nil. Note that
+// inMapVal should always be nil when this->MapValSize() is nil.
+{}
+
+/*============================================================================*/
+/* morkProbeMapIter */
+
+morkProbeMapIter::morkProbeMapIter(morkEnv* ev, morkProbeMap* ioMap)
+ : sProbeMapIter_Map(0),
+ sProbeMapIter_Seed(0),
+ sProbeMapIter_HereIx(morkProbeMapIter_kBeforeIx) {
+ if (ioMap) {
+ if (ioMap->GoodProbeMap()) {
+ if (ioMap->need_lazy_init()) // pending lazy action?
+ ioMap->probe_map_lazy_init(ev);
+
+ sProbeMapIter_Map = ioMap;
+ sProbeMapIter_Seed = ioMap->sMap_Seed;
+ } else
+ ioMap->ProbeMapBadTagError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+void morkProbeMapIter::CloseMapIter(morkEnv* ev) {
+ MORK_USED_1(ev);
+ sProbeMapIter_Map = 0;
+ sProbeMapIter_Seed = 0;
+
+ sProbeMapIter_HereIx = morkProbeMapIter_kAfterIx;
+}
+
+morkProbeMapIter::morkProbeMapIter()
+// zero most slots; caller must call InitProbeMapIter()
+{
+ sProbeMapIter_Map = 0;
+ sProbeMapIter_Seed = 0;
+
+ sProbeMapIter_HereIx = morkProbeMapIter_kBeforeIx;
+}
+
+void morkProbeMapIter::InitProbeMapIter(morkEnv* ev, morkProbeMap* ioMap) {
+ sProbeMapIter_Map = 0;
+ sProbeMapIter_Seed = 0;
+
+ sProbeMapIter_HereIx = morkProbeMapIter_kBeforeIx;
+
+ if (ioMap) {
+ if (ioMap->GoodProbeMap()) {
+ if (ioMap->need_lazy_init()) // pending lazy action?
+ ioMap->probe_map_lazy_init(ev);
+
+ sProbeMapIter_Map = ioMap;
+ sProbeMapIter_Seed = ioMap->sMap_Seed;
+ } else
+ ioMap->ProbeMapBadTagError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+mork_bool morkProbeMapIter::IterFirst(morkEnv* ev, void* outAppKey,
+ void* outAppVal) {
+ sProbeMapIter_HereIx = morkProbeMapIter_kAfterIx; // default to done
+ morkProbeMap* map = sProbeMapIter_Map;
+
+ if (map && map->GoodProbeMap()) /* looks good? */
+ {
+ sProbeMapIter_Seed = map->sMap_Seed; /* sync the seeds */
+
+ mork_u1* k = map->sMap_Keys; // array of keys, each of size sMap_KeySize
+ mork_num size = map->sMap_KeySize; // number of bytes in each key
+ mork_count slots = map->sMap_Slots; // total number of key buckets
+ mork_pos here = 0; // first hash bucket
+
+ while (here < (mork_pos)slots) {
+ if (!map->ProbeMapIsKeyNil(ev, k + (here * size))) {
+ map->get_probe_kv(ev, outAppKey, outAppVal, here);
+
+ sProbeMapIter_HereIx = (mork_i4)here;
+ return morkBool_kTrue;
+ }
+ ++here; // next bucket
+ }
+ } else
+ map->ProbeMapBadTagError(ev);
+
+ return morkBool_kFalse;
+}
+
+mork_bool morkProbeMapIter::IterNext(morkEnv* ev, void* outAppKey,
+ void* outAppVal) {
+ morkProbeMap* map = sProbeMapIter_Map;
+
+ if (map && map->GoodProbeMap()) /* looks good? */
+ {
+ if (sProbeMapIter_Seed == map->sMap_Seed) /* in sync? */
+ {
+ if (sProbeMapIter_HereIx != morkProbeMapIter_kAfterIx) {
+ mork_pos here = (mork_pos)sProbeMapIter_HereIx;
+ if (sProbeMapIter_HereIx < 0)
+ here = 0;
+ else
+ ++here;
+
+ sProbeMapIter_HereIx = morkProbeMapIter_kAfterIx; // default to done
+
+ mork_u1* k = map->sMap_Keys; // key array, each of size sMap_KeySize
+ mork_num size = map->sMap_KeySize; // number of bytes in each key
+ mork_count slots = map->sMap_Slots; // total number of key buckets
+
+ while (here < (mork_pos)slots) {
+ if (!map->ProbeMapIsKeyNil(ev, k + (here * size))) {
+ map->get_probe_kv(ev, outAppKey, outAppVal, here);
+
+ sProbeMapIter_HereIx = (mork_i4)here;
+ return morkBool_kTrue;
+ }
+ ++here; // next bucket
+ }
+ }
+ } else
+ map->MapSeedOutOfSyncError(ev);
+ } else
+ map->ProbeMapBadTagError(ev);
+
+ return morkBool_kFalse;
+}
+
+mork_bool morkProbeMapIter::IterHere(morkEnv* ev, void* outAppKey,
+ void* outAppVal) {
+ morkProbeMap* map = sProbeMapIter_Map;
+
+ if (map && map->GoodProbeMap()) /* looks good? */
+ {
+ if (sProbeMapIter_Seed == map->sMap_Seed) /* in sync? */
+ {
+ mork_pos here = (mork_pos)sProbeMapIter_HereIx;
+ mork_count slots = map->sMap_Slots; // total number of key buckets
+ if (sProbeMapIter_HereIx >= 0 && (here < (mork_pos)slots)) {
+ mork_u1* k = map->sMap_Keys; // key array, each of size sMap_KeySize
+ mork_num size = map->sMap_KeySize; // number of bytes in each key
+
+ if (!map->ProbeMapIsKeyNil(ev, k + (here * size))) {
+ map->get_probe_kv(ev, outAppKey, outAppVal, here);
+ return morkBool_kTrue;
+ }
+ }
+ } else
+ map->MapSeedOutOfSyncError(ev);
+ } else
+ map->ProbeMapBadTagError(ev);
+
+ return morkBool_kFalse;
+}
+
+mork_change* morkProbeMapIter::First(morkEnv* ev, void* outKey, void* outVal) {
+ if (this->IterFirst(ev, outKey, outVal)) return &sProbeMapIter_Change;
+
+ return (mork_change*)0;
+}
+
+mork_change* morkProbeMapIter::Next(morkEnv* ev, void* outKey, void* outVal) {
+ if (this->IterNext(ev, outKey, outVal)) return &sProbeMapIter_Change;
+
+ return (mork_change*)0;
+}
+
+mork_change* morkProbeMapIter::Here(morkEnv* ev, void* outKey, void* outVal) {
+ if (this->IterHere(ev, outKey, outVal)) return &sProbeMapIter_Change;
+
+ return (mork_change*)0;
+}
+
+mork_change* morkProbeMapIter::CutHere(morkEnv* ev, void* outKey,
+ void* outVal) {
+ morkProbeMap::ProbeMapCutError(ev);
+
+ return (mork_change*)0;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// NOTE: the following methods ONLY work for sMap_ValIsIP pointer values.
+// (Note the implied assumption that zero is never a good value pattern.)
+
+void* morkProbeMapIter::IterFirstVal(morkEnv* ev, void* outKey)
+// equivalent to { void* v=0; this->IterFirst(ev, outKey, &v); return v; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_ValIsIP) {
+ void* v = 0;
+ this->IterFirst(ev, outKey, &v);
+ return v;
+ } else
+ map->MapValIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+void* morkProbeMapIter::IterNextVal(morkEnv* ev, void* outKey)
+// equivalent to { void* v=0; this->IterNext(ev, outKey, &v); return v; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_ValIsIP) {
+ void* v = 0;
+ this->IterNext(ev, outKey, &v);
+ return v;
+ } else
+ map->MapValIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+void* morkProbeMapIter::IterHereVal(morkEnv* ev, void* outKey)
+// equivalent to { void* v=0; this->IterHere(ev, outKey, &v); return v; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_ValIsIP) {
+ void* v = 0;
+ this->IterHere(ev, outKey, &v);
+ return v;
+ } else
+ map->MapValIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+// NOTE: the following methods ONLY work for sMap_KeyIsIP pointer values.
+// (Note the implied assumption that zero is never a good key pattern.)
+
+void* morkProbeMapIter::IterFirstKey(morkEnv* ev)
+// equivalent to { void* k=0; this->IterFirst(ev, &k, 0); return k; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_KeyIsIP) {
+ void* k = 0;
+ this->IterFirst(ev, &k, (void*)0);
+ return k;
+ } else
+ map->MapKeyIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+void* morkProbeMapIter::IterNextKey(morkEnv* ev)
+// equivalent to { void* k=0; this->IterNext(ev, &k, 0); return k; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_KeyIsIP) {
+ void* k = 0;
+ this->IterNext(ev, &k, (void*)0);
+ return k;
+ } else
+ map->MapKeyIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+void* morkProbeMapIter::IterHereKey(morkEnv* ev)
+// equivalent to { void* k=0; this->IterHere(ev, &k, 0); return k; }
+{
+ morkProbeMap* map = sProbeMapIter_Map;
+ if (map) {
+ if (map->sMap_KeyIsIP) {
+ void* k = 0;
+ this->IterHere(ev, &k, (void*)0);
+ return k;
+ } else
+ map->MapKeyIsNotIPError(ev);
+ }
+ return (void*)0;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkProbeMap.h b/comm/mailnews/db/mork/morkProbeMap.h
new file mode 100644
index 0000000000..01068a1c82
--- /dev/null
+++ b/comm/mailnews/db/mork/morkProbeMap.h
@@ -0,0 +1,423 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This code is a port to NS Mork from public domain Mithril C++ sources.
+// Note many code comments here come verbatim from cut-and-pasted Mithril.
+// In many places, code is identical; Mithril versions stay public domain.
+// Changes in porting are mainly class type and scalar type name changes.
+
+#ifndef _MORKPROBEMAP_
+#define _MORKPROBEMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class morkMapScratch { // utility class used by map subclasses
+ public:
+ nsIMdbHeap* sMapScratch_Heap; // cached sMap_Heap
+ mork_count sMapScratch_Slots; // cached sMap_Slots
+
+ mork_u1* sMapScratch_Keys; // cached sMap_Keys
+ mork_u1* sMapScratch_Vals; // cached sMap_Vals
+
+ public:
+ void halt_map_scratch(morkEnv* ev);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kProbeMap 0x7072 /* ascii 'pr' */
+#define morkProbeMap_kTag 0x70724D50 /* ascii 'prMP' */
+
+#define morkProbeMap_kLazyClearOnAdd ((mork_u1)'c')
+
+class morkProbeMap : public morkNode {
+ protected:
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ protected:
+ // { begin morkMap slots
+ nsIMdbHeap* sMap_Heap; // strong ref to heap allocating all space
+
+ mork_u1* sMap_Keys;
+ mork_u1* sMap_Vals;
+
+ mork_count sMap_Seed; // change count of members or structure
+
+ mork_count sMap_Slots; // count of slots in the hash table
+ mork_fill sMap_Fill; // number of used slots in the hash table
+
+ mork_size sMap_KeySize; // size of each key (cannot be zero)
+ mork_size sMap_ValSize; // size of each val (zero allowed)
+
+ mork_bool sMap_KeyIsIP; // sMap_KeySize == sizeof(mork_ip)
+ mork_bool sMap_ValIsIP; // sMap_ValSize == sizeof(mork_ip)
+ mork_u1 sMap_Pad[2]; // for u4 alignment
+ // } end morkMap slots
+
+ friend class morkProbeMapIter; // for access to protected slots
+
+ public: // getters
+ mork_count MapSeed() const { return sMap_Seed; }
+
+ mork_count MapSlots() const { return sMap_Slots; }
+ mork_fill MapFill() const { return sMap_Fill; }
+
+ mork_size MapKeySize() const { return sMap_KeySize; }
+ mork_size MapValSize() const { return sMap_ValSize; }
+
+ mork_bool MapKeyIsIP() const { return sMap_KeyIsIP; }
+ mork_bool MapValIsIP() const { return sMap_ValIsIP; }
+
+ protected: // slots
+ // { begin morkProbeMap slots
+
+ mork_fill sProbeMap_MaxFill; // max sMap_Fill before map must grow
+
+ mork_u1 sProbeMap_LazyClearOnAdd; // true if kLazyClearOnAdd
+ mork_bool sProbeMap_ZeroIsClearKey; // zero is adequate to clear keys
+ mork_u1 sProbeMap_Pad[2]; // for u4 alignment
+
+ mork_u4 sProbeMap_Tag;
+
+ // } end morkProbeMap slots
+
+ public: // lazy clear on add
+ mork_bool need_lazy_init() const {
+ return sProbeMap_LazyClearOnAdd == morkProbeMap_kLazyClearOnAdd;
+ }
+
+ public: // typing
+ mork_bool GoodProbeMap() const { return sProbeMap_Tag == morkProbeMap_kTag; }
+
+ protected: // utilities
+ void* clear_alloc(morkEnv* ev, mork_size inSize);
+
+ mork_u1* map_new_vals(morkEnv* ev, mork_num inSlots);
+ mork_u1* map_new_keys(morkEnv* ev, mork_num inSlots);
+
+ void clear_probe_map(morkEnv* ev, nsIMdbHeap* ioMapHeap);
+ void init_probe_map(morkEnv* ev, mork_size inSlots);
+ void probe_map_lazy_init(morkEnv* ev);
+
+ mork_bool new_slots(morkEnv* ev, morkMapScratch* old, mork_num inSlots);
+
+ mork_test find_key_pos(morkEnv* ev, const void* inAppKey, mork_u4 inHash,
+ mork_pos* outPos) const;
+
+ void put_probe_kv(morkEnv* ev, const void* inAppKey, const void* inAppVal,
+ mork_pos inPos);
+ void get_probe_kv(morkEnv* ev, void* outAppKey, void* outAppVal,
+ mork_pos inPos) const;
+
+ mork_bool grow_probe_map(morkEnv* ev);
+ void rehash_old_map(morkEnv* ev, morkMapScratch* ioScratch);
+ void revert_map(morkEnv* ev, morkMapScratch* ioScratch);
+
+ public: // errors
+ void ProbeMapBadTagError(morkEnv* ev) const;
+ void WrapWithNoVoidSlotError(morkEnv* ev) const;
+ void GrowFailsMaxFillError(morkEnv* ev) const;
+ void MapKeyIsNotIPError(morkEnv* ev) const;
+ void MapValIsNotIPError(morkEnv* ev) const;
+
+ void MapNilKeysError(morkEnv* ev);
+ void MapZeroKeySizeError(morkEnv* ev);
+
+ void MapSeedOutOfSyncError(morkEnv* ev);
+ void MapFillUnderflowWarning(morkEnv* ev);
+
+ static void ProbeMapCutError(morkEnv* ev);
+
+ // { ===== begin morkMap methods =====
+ public:
+ virtual mork_test // hit(a,b) implies hash(a) == hash(b)
+ MapTest(morkEnv* ev, const void* inMapKey, const void* inAppKey) const;
+ // Note inMapKey is always a key already stored in the map, while inAppKey
+ // is always a method argument parameter from a client method call.
+ // This matters the most in morkProbeMap subclasses, which have the
+ // responsibility of putting 'app' keys into slots for 'map' keys, and
+ // the bit pattern representation might be different in such cases.
+ // morkTest_kHit means that inMapKey equals inAppKey (and this had better
+ // also imply that hash(inMapKey) == hash(inAppKey)).
+ // morkTest_kMiss means that inMapKey does NOT equal inAppKey (but this
+ // implies nothing at all about hash(inMapKey) and hash(inAppKey)).
+ // morkTest_kVoid means that inMapKey is not a valid key bit pattern,
+ // which means that key slot in the map is not being used. Note that
+ // kVoid is only expected as a return value in morkProbeMap subclasses,
+ // because morkProbeMap must ask whether a key slot is used or not.
+ // morkChainMap however, always knows when a key slot is used, so only
+ // key slots expected to have valid bit patterns will be presented to
+ // the MapTest() methods for morkChainMap subclasses.
+ //
+ // NOTE: it is very important that subclasses correctly return the value
+ // morkTest_kVoid whenever the slot for inMapKey contains a bit pattern
+ // that means the slot is not being used, because this is the only way a
+ // probe map can terminate an unsuccessful search for a key in the map.
+
+ virtual mork_u4 // hit(a,b) implies hash(a) == hash(b)
+ MapHash(morkEnv* ev, const void* inAppKey) const;
+
+ virtual mork_bool MapAtPut(morkEnv* ev, const void* inAppKey,
+ const void* inAppVal, void* outAppKey,
+ void* outAppVal);
+
+ virtual mork_bool MapAt(morkEnv* ev, const void* inAppKey, void* outAppKey,
+ void* outAppVal);
+
+ virtual mork_num MapCutAll(morkEnv* ev);
+ // } ===== end morkMap methods =====
+
+ // { ===== begin morkProbeMap methods =====
+ public:
+ virtual mork_u4 ProbeMapHashMapKey(morkEnv* ev, const void* inMapKey) const;
+ // ProbeMapHashMapKey() does logically the same thing as MapHash(), and
+ // the default implementation actually calls virtual MapHash(). However,
+ // Subclasses must override this method whenever the formats of keys in
+ // the map differ from app keys outside the map, because MapHash() only
+ // works on keys in 'app' format, while ProbeMapHashMapKey() only works
+ // on keys in 'map' format. This method is called in order to rehash all
+ // map keys when a map is grown, and this causes all old map members to
+ // move into new slot locations.
+ //
+ // Note it is absolutely imperative that a hash for a key in 'map' format
+ // be exactly the same the hash of the same key in 'app' format, or else
+ // maps will seem corrupt later when keys in 'app' format cannot be found.
+
+ virtual mork_bool ProbeMapIsKeyNil(morkEnv* ev, void* ioMapKey);
+ // ProbeMapIsKeyNil() must say whether the representation of logical 'nil'
+ // is currently found inside the key at ioMapKey, for a key found within
+ // the map. The the map iterator uses this method to find map keys that
+ // are actually being used for valid map associations; otherwise the
+ // iterator cannot determine which map slots actually denote used keys.
+ // The default method version returns true if all the bits equal zero.
+
+ virtual void ProbeMapClearKey(
+ morkEnv* ev, // put 'nil' into all keys inside map
+ void* ioMapKey, mork_count inKeyCount); // array of keys inside map
+ // ProbeMapClearKey() must put some representation of logical 'nil' into
+ // every key slot in the map, such that MapTest() will later recognize
+ // that this bit pattern shows each key slot is not actually being used.
+ //
+ // This method is typically called whenever the map is either created or
+ // grown into a larger size, where ioMapKey is a pointer to an array of
+ // inKeyCount keys, where each key is this->MapKeySize() bytes in size.
+ // Note that keys are assumed immediately adjacent with no padding, so
+ // if any alignment requirements must be met, then subclasses should have
+ // already accounted for this when specifying a key size in the map.
+ //
+ // Since this method will be called when a map is being grown in size,
+ // nothing should be assumed about the state slots of the map, since the
+ // ioMapKey array might not yet live in sMap_Keys, and the array length
+ // inKeyCount might not yet live in sMap_Slots. However, the value kept
+ // in sMap_KeySize never changes, so this->MapKeySize() is always correct.
+
+ virtual void ProbeMapPushIn(morkEnv* ev, // move (key,val) into the map
+ const void* inAppKey,
+ const void* inAppVal, // (key,val) outside map
+ void* outMapKey,
+ void* outMapVal); // (key,val) inside map
+ // This method actually puts keys and vals in the map in suitable format.
+ //
+ // ProbeMapPushIn() must copy a caller key and value in 'app' format
+ // into the map slots provided, which are in 'map' format. When the
+ // 'app' and 'map' formats are identical, then this is just a bitwise
+ // copy of this->MapKeySize() key bytes and this->MapValSize() val bytes,
+ // and this is exactly what the default implementation performs. However,
+ // if 'app' and 'map' formats are different, and MapTest() depends on this
+ // difference in format, then subclasses must override this method to do
+ // whatever is necessary to store the input app key in output map format.
+ //
+ // Do NOT write more than this->MapKeySize() bytes of a map key, or more
+ // than this->MapValSize() bytes of a map val, or corruption might ensue.
+ //
+ // The inAppKey and inAppVal parameters are the same ones passed into a
+ // call to MapAtPut(), and the outMapKey and outMapVal parameters are ones
+ // determined by how the map currently positions key inAppKey in the map.
+ //
+ // Note any key or val parameter can be a null pointer, in which case
+ // this method must do nothing with those parameters. In particular, do
+ // no key move at all when either inAppKey or outMapKey is nil, and do
+ // no val move at all when either inAppVal or outMapVal is nil. Note that
+ // outMapVal should always be nil when this->MapValSize() is nil.
+
+ virtual void ProbeMapPullOut(morkEnv* ev, // move (key,val) out from the map
+ const void* inMapKey,
+ const void* inMapVal, // (key,val) inside map
+ void* outAppKey,
+ void* outAppVal) const; // (key,val) outside map
+ // This method actually gets keys and vals from the map in suitable format.
+ //
+ // ProbeMapPullOut() must copy a key and val in 'map' format into the
+ // caller key and val slots provided, which are in 'app' format. When the
+ // 'app' and 'map' formats are identical, then this is just a bitwise
+ // copy of this->MapKeySize() key bytes and this->MapValSize() val bytes,
+ // and this is exactly what the default implementation performs. However,
+ // if 'app' and 'map' formats are different, and MapTest() depends on this
+ // difference in format, then subclasses must override this method to do
+ // whatever is necessary to store the input map key in output app format.
+ //
+ // The outAppKey and outAppVal parameters are the same ones passed into a
+ // call to either MapAtPut() or MapAt(), while inMapKey and inMapVal are
+ // determined by how the map currently positions the target key in the map.
+ //
+ // Note any key or val parameter can be a null pointer, in which case
+ // this method must do nothing with those parameters. In particular, do
+ // no key move at all when either inMapKey or outAppKey is nil, and do
+ // no val move at all when either inMapVal or outAppVal is nil. Note that
+ // inMapVal should always be nil when this->MapValSize() is nil.
+
+ // } ===== end morkProbeMap methods =====
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseProbeMap() only if open
+ virtual ~morkProbeMap(); // assert that CloseProbeMap() executed earlier
+
+ public: // morkProbeMap construction & destruction
+ morkProbeMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioNodeHeap,
+ mork_size inKeySize, mork_size inValSize, nsIMdbHeap* ioMapHeap,
+ mork_size inSlots, mork_bool inZeroIsClearKey);
+
+ void CloseProbeMap(morkEnv* ev); // called by
+
+ public: // dynamic type identification
+ mork_bool IsProbeMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kProbeMap;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakMap(morkMap* me, morkEnv* ev, morkMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongMap(morkMap* me, morkEnv* ev, morkMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+/*============================================================================*/
+/* morkProbeMapIter */
+
+#define morkProbeMapIter_kBeforeIx ((mork_i4)-1) /* before first member */
+#define morkProbeMapIter_kAfterIx ((mork_i4)-2) /* after last member */
+
+class morkProbeMapIter {
+ protected:
+ morkProbeMap* sProbeMapIter_Map; // nonref
+ mork_num sProbeMapIter_Seed; // iter's cached copy of map's seed
+
+ mork_i4 sProbeMapIter_HereIx;
+
+ mork_change sProbeMapIter_Change; // morkMapIter API simulation dummy
+ mork_u1 sProbeMapIter_Pad[3]; // for u4 alignment
+
+ public:
+ morkProbeMapIter(morkEnv* ev, morkProbeMap* ioMap);
+ void CloseMapIter(morkEnv* ev);
+
+ morkProbeMapIter(); // zero most slots; caller must call InitProbeMapIter()
+
+ protected: // protected so subclasses must provide suitable typesafe inlines:
+ void InitProbeMapIter(morkEnv* ev, morkProbeMap* ioMap);
+
+ void InitMapIter(morkEnv* ev,
+ morkProbeMap* ioMap) // morkMapIter compatibility
+ {
+ this->InitProbeMapIter(ev, ioMap);
+ }
+
+ mork_bool IterFirst(morkEnv* ev, void* outKey, void* outVal);
+ mork_bool IterNext(morkEnv* ev, void* outKey, void* outVal);
+ mork_bool IterHere(morkEnv* ev, void* outKey, void* outVal);
+
+ // NOTE: the following methods ONLY work for sMap_ValIsIP pointer values.
+ // (Note the implied assumption that zero is never a good value pattern.)
+
+ void* IterFirstVal(morkEnv* ev, void* outKey);
+ // equivalent to { void* v=0; this->IterFirst(ev, outKey, &v); return v; }
+
+ void* IterNextVal(morkEnv* ev, void* outKey);
+ // equivalent to { void* v=0; this->IterNext(ev, outKey, &v); return v; }
+
+ void* IterHereVal(morkEnv* ev, void* outKey);
+ // equivalent to { void* v=0; this->IterHere(ev, outKey, &v); return v; }
+
+ // NOTE: the following methods ONLY work for sMap_KeyIsIP pointer values.
+ // (Note the implied assumption that zero is never a good key pattern.)
+
+ void* IterFirstKey(morkEnv* ev);
+ // equivalent to { void* k=0; this->IterFirst(ev, &k, 0); return k; }
+
+ void* IterNextKey(morkEnv* ev);
+ // equivalent to { void* k=0; this->IterNext(ev, &k, 0); return k; }
+
+ void* IterHereKey(morkEnv* ev);
+ // equivalent to { void* k=0; this->IterHere(ev, &k, 0); return k; }
+
+ public: // simulation of the morkMapIter API for morkMap compatibility:
+ mork_change* First(morkEnv* ev, void* outKey, void* outVal);
+ mork_change* Next(morkEnv* ev, void* outKey, void* outVal);
+ mork_change* Here(morkEnv* ev, void* outKey, void* outVal);
+
+ mork_change* CutHere(morkEnv* ev, void* outKey, void* outVal);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKPROBEMAP_ */
diff --git a/comm/mailnews/db/mork/morkQuickSort.cpp b/comm/mailnews/db/mork/morkQuickSort.cpp
new file mode 100644
index 0000000000..6fd211ee5c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkQuickSort.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/*-
+ * Copyright (c) 1992, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKQUICKSORT_
+# include "morkQuickSort.h"
+#endif
+
+#if !defined(DEBUG) && (defined(__cplusplus) || defined(__gcc))
+# ifndef INLINE
+# define INLINE inline
+# endif
+#else
+# define INLINE
+#endif
+
+static INLINE mork_u1* morkQS_med3(mork_u1*, mork_u1*, mork_u1*, mdbAny_Order,
+ void*);
+
+static INLINE void morkQS_swapfunc(mork_u1*, mork_u1*, int, int);
+
+/*
+ * Qsort routine from Bentley & McIlroy's "Engineering a Sort Function".
+ */
+#define morkQS_swapcode(TYPE, parmi, parmj, n) \
+ { \
+ long i = (n) / sizeof(TYPE); \
+ TYPE* pi = (TYPE*)(parmi); \
+ TYPE* pj = (TYPE*)(parmj); \
+ do { \
+ TYPE t = *pi; \
+ *pi++ = *pj; \
+ *pj++ = t; \
+ } while (--i > 0); \
+ }
+
+#define morkQS_SwapInit(a, es) \
+ swaptype = (a - (mork_u1*)0) % sizeof(long) || es % sizeof(long) ? 2 \
+ : es == sizeof(long) ? 0 \
+ : 1;
+
+static INLINE void morkQS_swapfunc(mork_u1* a, mork_u1* b, int n,
+ int swaptype) {
+ if (swaptype <= 1)
+ morkQS_swapcode(long, a, b, n) else morkQS_swapcode(mork_u1, a, b, n)
+}
+
+#define morkQS_swap(a, b) \
+ if (swaptype == 0) { \
+ long t = *(long*)(a); \
+ *(long*)(a) = *(long*)(b); \
+ *(long*)(b) = t; \
+ } else \
+ morkQS_swapfunc(a, b, (int)inSize, swaptype)
+
+#define morkQS_vecswap(a, b, n) \
+ if ((n) > 0) morkQS_swapfunc(a, b, (int)n, swaptype)
+
+static INLINE mork_u1* morkQS_med3(mork_u1* a, mork_u1* b, mork_u1* c,
+ mdbAny_Order cmp, void* closure) {
+ return (*cmp)(a, b, closure) < 0
+ ? ((*cmp)(b, c, closure) < 0 ? b
+ : ((*cmp)(a, c, closure) < 0 ? c : a))
+ : ((*cmp)(b, c, closure) > 0
+ ? b
+ : ((*cmp)(a, c, closure) < 0 ? a : c));
+}
+
+#define morkQS_MIN(x, y) ((x) < (y) ? (x) : (y))
+
+void morkQuickSort(mork_u1* ioVec, mork_u4 inCount, mork_u4 inSize,
+ mdbAny_Order inOrder, void* ioClosure) {
+ mork_u1 *pa, *pb, *pc, *pd, *pl, *pm, *pn;
+ int d, r, swaptype, swap_cnt;
+
+tailCall:
+ morkQS_SwapInit(ioVec, inSize);
+ swap_cnt = 0;
+ if (inCount < 7) {
+ for (pm = ioVec + inSize; pm < ioVec + inCount * inSize; pm += inSize)
+ for (pl = pm; pl > ioVec && (*inOrder)(pl - inSize, pl, ioClosure) > 0;
+ pl -= inSize)
+ morkQS_swap(pl, pl - inSize);
+ return;
+ }
+ pm = ioVec + (inCount / 2) * inSize;
+ if (inCount > 7) {
+ pl = ioVec;
+ pn = ioVec + (inCount - 1) * inSize;
+ if (inCount > 40) {
+ d = (inCount / 8) * inSize;
+ pl = morkQS_med3(pl, pl + d, pl + 2 * d, inOrder, ioClosure);
+ pm = morkQS_med3(pm - d, pm, pm + d, inOrder, ioClosure);
+ pn = morkQS_med3(pn - 2 * d, pn - d, pn, inOrder, ioClosure);
+ }
+ pm = morkQS_med3(pl, pm, pn, inOrder, ioClosure);
+ }
+ morkQS_swap(ioVec, pm);
+ pa = pb = ioVec + inSize;
+
+ pc = pd = ioVec + (inCount - 1) * inSize;
+ for (;;) {
+ while (pb <= pc && (r = (*inOrder)(pb, ioVec, ioClosure)) <= 0) {
+ if (r == 0) {
+ swap_cnt = 1;
+ morkQS_swap(pa, pb);
+ pa += inSize;
+ }
+ pb += inSize;
+ }
+ while (pb <= pc && (r = (*inOrder)(pc, ioVec, ioClosure)) >= 0) {
+ if (r == 0) {
+ swap_cnt = 1;
+ morkQS_swap(pc, pd);
+ pd -= inSize;
+ }
+ pc -= inSize;
+ }
+ if (pb > pc) break;
+ morkQS_swap(pb, pc);
+ swap_cnt = 1;
+ pb += inSize;
+ pc -= inSize;
+ }
+ if (swap_cnt == 0) { /* Switch to insertion sort */
+ for (pm = ioVec + inSize; pm < ioVec + inCount * inSize; pm += inSize)
+ for (pl = pm; pl > ioVec && (*inOrder)(pl - inSize, pl, ioClosure) > 0;
+ pl -= inSize)
+ morkQS_swap(pl, pl - inSize);
+ return;
+ }
+
+ pn = ioVec + inCount * inSize;
+ r = morkQS_MIN(pa - ioVec, pb - pa);
+ morkQS_vecswap(ioVec, pb - r, r);
+ r = morkQS_MIN(pd - pc, (int)(pn - pd - inSize));
+ morkQS_vecswap(pb, pn - r, r);
+ if ((r = pb - pa) > (int)inSize)
+ morkQuickSort(ioVec, r / inSize, inSize, inOrder, ioClosure);
+ if ((r = pd - pc) > (int)inSize) {
+ /* Iterate rather than recurse to save stack space */
+ ioVec = pn - r;
+ inCount = r / inSize;
+ goto tailCall;
+ }
+ /* morkQuickSort(pn - r, r / inSize, inSize, inOrder, ioClosure);*/
+}
diff --git a/comm/mailnews/db/mork/morkQuickSort.h b/comm/mailnews/db/mork/morkQuickSort.h
new file mode 100644
index 0000000000..98561d5758
--- /dev/null
+++ b/comm/mailnews/db/mork/morkQuickSort.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKQUICKSORT_
+#define _MORKQUICKSORT_ 1
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+extern void morkQuickSort(mork_u1* ioVec, mork_u4 inCount, mork_u4 inSize,
+ mdbAny_Order inOrder, void* ioClosure);
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKQUICKSORT_ */
diff --git a/comm/mailnews/db/mork/morkRow.cpp b/comm/mailnews/db/mork/morkRow.cpp
new file mode 100644
index 0000000000..2af5e9adcd
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRow.cpp
@@ -0,0 +1,769 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+#ifndef _MORKCELLOBJECT_
+# include "morkCellObject.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKROWCELLCURSOR_
+# include "morkRowCellCursor.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// notifications regarding row changes:
+
+void morkRow::NoteRowAddCol(morkEnv* ev, mork_column inColumn) {
+ if (!this->IsRowRewrite()) {
+ mork_delta newDelta;
+ morkDelta_Init(newDelta, inColumn, morkChange_kAdd);
+
+ if (newDelta != mRow_Delta) // not repeating existing data?
+ {
+ if (this->HasRowDelta()) // already have one change recorded?
+ this->SetRowRewrite(); // just plan to write all row cells
+ else
+ this->SetRowDelta(inColumn, morkChange_kAdd);
+ }
+ } else
+ this->ClearRowDelta();
+}
+
+void morkRow::NoteRowCutCol(morkEnv* ev, mork_column inColumn) {
+ if (!this->IsRowRewrite()) {
+ mork_delta newDelta;
+ morkDelta_Init(newDelta, inColumn, morkChange_kCut);
+
+ if (newDelta != mRow_Delta) // not repeating existing data?
+ {
+ if (this->HasRowDelta()) // already have one change recorded?
+ this->SetRowRewrite(); // just plan to write all row cells
+ else
+ this->SetRowDelta(inColumn, morkChange_kCut);
+ }
+ } else
+ this->ClearRowDelta();
+}
+
+void morkRow::NoteRowSetCol(morkEnv* ev, mork_column inColumn) {
+ if (!this->IsRowRewrite()) {
+ if (this->HasRowDelta()) // already have one change recorded?
+ this->SetRowRewrite(); // just plan to write all row cells
+ else
+ this->SetRowDelta(inColumn, morkChange_kSet);
+ } else
+ this->ClearRowDelta();
+}
+
+void morkRow::NoteRowSetAll(morkEnv* ev) {
+ this->SetRowRewrite(); // just plan to write all row cells
+ this->ClearRowDelta();
+}
+
+mork_u2 morkRow::AddRowGcUse(morkEnv* ev) {
+ if (this->IsRow()) {
+ if (mRow_GcUses < morkRow_kMaxGcUses) // not already maxed out?
+ ++mRow_GcUses;
+ } else
+ this->NonRowTypeError(ev);
+
+ return mRow_GcUses;
+}
+
+mork_u2 morkRow::CutRowGcUse(morkEnv* ev) {
+ if (this->IsRow()) {
+ if (mRow_GcUses) // any outstanding uses to cut?
+ {
+ if (mRow_GcUses < morkRow_kMaxGcUses) // not frozen at max?
+ --mRow_GcUses;
+ } else
+ this->GcUsesUnderflowWarning(ev);
+ } else
+ this->NonRowTypeError(ev);
+
+ return mRow_GcUses;
+}
+
+/*static*/ void morkRow::GcUsesUnderflowWarning(morkEnv* ev) {
+ ev->NewWarning("mRow_GcUses underflow");
+}
+
+/*static*/ void morkRow::NonRowTypeError(morkEnv* ev) {
+ ev->NewError("non morkRow");
+}
+
+/*static*/ void morkRow::NonRowTypeWarning(morkEnv* ev) {
+ ev->NewWarning("non morkRow");
+}
+
+/*static*/ void morkRow::LengthBeyondMaxError(morkEnv* ev) {
+ ev->NewError("mRow_Length over max");
+}
+
+/*static*/ void morkRow::ZeroColumnError(morkEnv* ev) {
+ ev->NewError(" zero mork_column");
+}
+
+/*static*/ void morkRow::NilCellsError(morkEnv* ev) {
+ ev->NewError("nil mRow_Cells");
+}
+
+void morkRow::InitRow(morkEnv* ev, const mdbOid* inOid, morkRowSpace* ioSpace,
+ mork_size inLength, morkPool* ioPool)
+// if inLength is nonzero, cells will be allocated from ioPool
+{
+ if (ioSpace && ioPool && inOid) {
+ if (inLength <= morkRow_kMaxLength) {
+ if (inOid->mOid_Id != morkRow_kMinusOneRid) {
+ mRow_Space = ioSpace;
+ mRow_Object = 0;
+ mRow_Cells = 0;
+ mRow_Oid = *inOid;
+
+ mRow_Length = (mork_u2)inLength;
+ mRow_Seed = (mork_u2)(mork_ip)this; // "random" assignment
+
+ mRow_GcUses = 0;
+ mRow_Pad = 0;
+ mRow_Flags = 0;
+ mRow_Tag = morkRow_kTag;
+
+ morkZone* zone = &ioSpace->mSpace_Store->mStore_Zone;
+
+ if (inLength) mRow_Cells = ioPool->NewCells(ev, inLength, zone);
+
+ if (this->MaybeDirtySpaceStoreAndRow()) // new row might dirty store
+ {
+ this->SetRowRewrite();
+ this->NoteRowSetAll(ev);
+ }
+ } else
+ ioSpace->MinusOneRidError(ev);
+ } else
+ this->LengthBeyondMaxError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+morkRowObject* morkRow::AcquireRowObject(morkEnv* ev, morkStore* ioStore) {
+ morkRowObject* ro = mRow_Object;
+ if (ro) // need new row object?
+ ro->AddRef();
+ else {
+ nsIMdbHeap* heap = ioStore->mPort_Heap;
+ ro = new (*heap, ev)
+ morkRowObject(ev, morkUsage::kHeap, heap, this, ioStore);
+ if (!ro) return (morkRowObject*)0;
+
+ morkRowObject::SlotWeakRowObject(ro, ev, &mRow_Object);
+ ro->AddRef();
+ }
+ return ro;
+}
+
+nsIMdbRow* morkRow::AcquireRowHandle(morkEnv* ev, morkStore* ioStore) {
+ return AcquireRowObject(ev, ioStore);
+}
+
+nsIMdbCell* morkRow::AcquireCellHandle(morkEnv* ev, morkCell* ioCell,
+ mdb_column inCol, mork_pos inPos) {
+ nsIMdbHeap* heap = ev->mEnv_Heap;
+ morkCellObject* cellObj = new (*heap, ev)
+ morkCellObject(ev, morkUsage::kHeap, heap, this, ioCell, inCol, inPos);
+ if (cellObj) {
+ nsIMdbCell* cellHandle = cellObj->AcquireCellHandle(ev);
+ // cellObj->CutStrongRef(ev->AsMdbEnv());
+ return cellHandle;
+ }
+ return (nsIMdbCell*)0;
+}
+
+mork_count morkRow::CountOverlap(morkEnv* ev, morkCell* ioVector,
+ mork_fill inFill)
+// Count cells in ioVector that change existing cells in this row when
+// ioVector is added to the row (as in TakeCells()). This is the set
+// of cells with the same columns in ioVector and mRow_Cells, which do
+// not have exactly the same value in mCell_Atom, and which do not both
+// have change status equal to morkChange_kCut (because cutting a cut
+// cell still yields a cell that has been cut). CountOverlap() also
+// modifies the change attribute of any cell in ioVector to kDup when
+// the change was previously kCut and the same column cell was found
+// in this row with change also equal to kCut; this tells callers later
+// they need not look for that cell in the row again on a second pass.
+{
+ mork_count outCount = 0;
+ mork_pos pos = 0; // needed by GetCell()
+ morkCell* cells = ioVector;
+ morkCell* end = cells + inFill;
+ --cells; // prepare for preincrement
+ while (++cells < end && ev->Good()) {
+ mork_column col = cells->GetColumn();
+
+ morkCell* old = this->GetCell(ev, col, &pos);
+ if (old) // same column?
+ {
+ mork_change newChg = cells->GetChange();
+ mork_change oldChg = old->GetChange();
+ if (newChg != morkChange_kCut || oldChg != newChg) // not cut+cut?
+ {
+ if (cells->mCell_Atom != old->mCell_Atom) // not same atom?
+ ++outCount; // cells will replace old significantly when added
+ } else
+ cells->SetColumnAndChange(col, morkChange_kDup); // note dup status
+ }
+ }
+ return outCount;
+}
+
+void morkRow::MergeCells(morkEnv* ev, morkCell* ioVector, mork_fill inVecLength,
+ mork_fill inOldRowFill, mork_fill inOverlap)
+// MergeCells() is the part of TakeCells() that does the insertion.
+// inOldRowFill is the old value of mRow_Length, and inOverlap is the
+// number of cells in the intersection that must be updated.
+{
+ morkCell* newCells = mRow_Cells + inOldRowFill; // 1st new cell in row
+ morkCell* newEnd = newCells + mRow_Length; // one past last cell
+
+ morkCell* srcCells = ioVector;
+ morkCell* srcEnd = srcCells + inVecLength;
+
+ --srcCells; // prepare for preincrement
+ while (++srcCells < srcEnd && ev->Good()) {
+ mork_change srcChg = srcCells->GetChange();
+ if (srcChg != morkChange_kDup) // anything to be done?
+ {
+ morkCell* dstCell = 0;
+ if (inOverlap) {
+ mork_pos pos = 0; // needed by GetCell()
+ dstCell = this->GetCell(ev, srcCells->GetColumn(), &pos);
+ }
+ if (dstCell) {
+ --inOverlap; // one fewer intersections to resolve
+ // swap the atoms in the cells to avoid ref counting here:
+ morkAtom* dstAtom = dstCell->mCell_Atom;
+ *dstCell = *srcCells; // bitwise copy, taking src atom
+ srcCells->mCell_Atom = dstAtom; // forget cell ref, if any
+ } else if (newCells < newEnd) // another new cell exists?
+ {
+ dstCell = newCells++; // alloc another new cell
+ // take atom from source cell, transferring ref to this row:
+ *dstCell = *srcCells; // bitwise copy, taking src atom
+ srcCells->mCell_Atom = 0; // forget cell ref, if any
+ } else // oops, we ran out...
+ ev->NewError("out of new cells");
+ }
+ }
+}
+
+void morkRow::TakeCells(morkEnv* ev, morkCell* ioVector, mork_fill inVecLength,
+ morkStore* ioStore) {
+ if (ioVector && inVecLength && ev->Good()) {
+ ++mRow_Seed; // intend to change structure of mRow_Cells
+ mork_size length = (mork_size)mRow_Length;
+
+ mork_count overlap = this->CountOverlap(ev, ioVector, inVecLength);
+
+ mork_size growth = inVecLength - overlap; // cells to add
+ mork_size newLength = length + growth;
+
+ if (growth && ev->Good()) // need to add any cells?
+ {
+ morkZone* zone = &ioStore->mStore_Zone;
+ morkPool* pool = ioStore->StorePool();
+ if (!pool->AddRowCells(ev, this, length + growth, zone))
+ ev->NewError("cannot take cells");
+ }
+ if (ev->Good()) {
+ if (mRow_Length >= newLength)
+ this->MergeCells(ev, ioVector, inVecLength, length, overlap);
+ else
+ ev->NewError("not enough new cells");
+ }
+ }
+}
+
+mork_bool morkRow::MaybeDirtySpaceStoreAndRow() {
+ morkRowSpace* rowSpace = mRow_Space;
+ if (rowSpace) {
+ morkStore* store = rowSpace->mSpace_Store;
+ if (store && store->mStore_CanDirty) {
+ store->SetStoreDirty();
+ rowSpace->mSpace_CanDirty = morkBool_kTrue;
+ }
+
+ if (rowSpace->mSpace_CanDirty) {
+ this->SetRowDirty();
+ rowSpace->SetRowSpaceDirty();
+ return morkBool_kTrue;
+ }
+ }
+ return morkBool_kFalse;
+}
+
+morkCell* morkRow::NewCell(morkEnv* ev, mdb_column inColumn, mork_pos* outPos,
+ morkStore* ioStore) {
+ ++mRow_Seed; // intend to change structure of mRow_Cells
+ mork_size length = (mork_size)mRow_Length;
+ *outPos = (mork_pos)length;
+ morkPool* pool = ioStore->StorePool();
+ morkZone* zone = &ioStore->mStore_Zone;
+
+ mork_bool canDirty = this->MaybeDirtySpaceStoreAndRow();
+
+ if (pool->AddRowCells(ev, this, length + 1, zone)) {
+ morkCell* cell = mRow_Cells + length;
+ // next line equivalent to inline morkCell::SetCellDirty():
+ if (canDirty)
+ cell->SetCellColumnDirty(inColumn);
+ else
+ cell->SetCellColumnClean(inColumn);
+
+ if (canDirty && !this->IsRowRewrite()) this->NoteRowAddCol(ev, inColumn);
+
+ return cell;
+ }
+
+ return (morkCell*)0;
+}
+
+void morkRow::SeekColumn(morkEnv* ev, mdb_pos inPos, mdb_column* outColumn,
+ mdbYarn* outYarn) {
+ morkCell* cells = mRow_Cells;
+ if (cells && inPos < mRow_Length && inPos >= 0) {
+ morkCell* c = cells + inPos;
+ if (outColumn) *outColumn = c->GetColumn();
+ if (outYarn) morkAtom::GetYarn(c->mCell_Atom, outYarn);
+ } else {
+ if (outColumn) *outColumn = 0;
+ if (outYarn) morkAtom::GetYarn((morkAtom*)0, outYarn);
+ }
+}
+
+void morkRow::NextColumn(morkEnv* ev, mdb_column* ioColumn, mdbYarn* outYarn) {
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ mork_column last = 0;
+ mork_column inCol = *ioColumn;
+ morkCell* end = cells + mRow_Length;
+ while (cells < end) {
+ if (inCol == last) // found column?
+ {
+ if (outYarn) morkAtom::GetYarn(cells->mCell_Atom, outYarn);
+ *ioColumn = cells->GetColumn();
+ return; // stop, we are done
+ } else {
+ last = cells->GetColumn();
+ ++cells;
+ }
+ }
+ }
+ *ioColumn = 0;
+ if (outYarn) morkAtom::GetYarn((morkAtom*)0, outYarn);
+}
+
+morkCell* morkRow::CellAt(morkEnv* ev, mork_pos inPos) const {
+ MORK_USED_1(ev);
+ morkCell* cells = mRow_Cells;
+ if (cells && inPos < mRow_Length && inPos >= 0) {
+ return cells + inPos;
+ }
+ return (morkCell*)0;
+}
+
+morkCell* morkRow::GetCell(morkEnv* ev, mdb_column inColumn,
+ mork_pos* outPos) const {
+ MORK_USED_1(ev);
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + mRow_Length;
+ while (cells < end) {
+ mork_column col = cells->GetColumn();
+ if (col == inColumn) // found the desired column?
+ {
+ *outPos = cells - mRow_Cells;
+ return cells;
+ } else
+ ++cells;
+ }
+ }
+ *outPos = -1;
+ return (morkCell*)0;
+}
+
+mork_aid morkRow::GetCellAtomAid(morkEnv* ev, mdb_column inColumn) const
+// GetCellAtomAid() finds the cell with column inColumn, and sees if the
+// atom has a token ID, and returns the atom's ID if there is one. Or
+// else zero is returned if there is no such column, or no atom, or if
+// the atom has no ID to return. This method is intended to support
+// efficient updating of column indexes for rows in a row space.
+{
+ if (this->IsRow()) {
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + mRow_Length;
+ while (cells < end) {
+ mork_column col = cells->GetColumn();
+ if (col == inColumn) // found desired column?
+ {
+ morkAtom* atom = cells->mCell_Atom;
+ if (atom && atom->IsBook())
+ return ((morkBookAtom*)atom)->mBookAtom_Id;
+ else
+ return 0;
+ } else
+ ++cells;
+ }
+ }
+ } else
+ this->NonRowTypeError(ev);
+
+ return 0;
+}
+
+void morkRow::EmptyAllCells(morkEnv* ev) {
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ morkStore* store = this->GetRowSpaceStore(ev);
+ if (store) {
+ if (this->MaybeDirtySpaceStoreAndRow()) {
+ this->SetRowRewrite();
+ this->NoteRowSetAll(ev);
+ }
+ morkPool* pool = store->StorePool();
+ morkCell* end = cells + mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end) {
+ if (cells->mCell_Atom) cells->SetAtom(ev, (morkAtom*)0, pool);
+ }
+ }
+ }
+}
+
+void morkRow::cut_all_index_entries(morkEnv* ev) {
+ morkRowSpace* rowSpace = mRow_Space;
+ if (rowSpace->mRowSpace_IndexCount) // any indexes?
+ {
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end) {
+ morkAtom* atom = cells->mCell_Atom;
+ if (atom) {
+ mork_aid atomAid = atom->GetBookAtomAid();
+ if (atomAid) {
+ mork_column col = cells->GetColumn();
+ morkAtomRowMap* map = rowSpace->FindMap(ev, col);
+ if (map) // cut row from index for this column?
+ map->CutAid(ev, atomAid);
+ }
+ }
+ }
+ }
+ }
+}
+
+void morkRow::CutAllColumns(morkEnv* ev) {
+ morkStore* store = this->GetRowSpaceStore(ev);
+ if (store) {
+ if (this->MaybeDirtySpaceStoreAndRow()) {
+ this->SetRowRewrite();
+ this->NoteRowSetAll(ev);
+ }
+ morkRowSpace* rowSpace = mRow_Space;
+ if (rowSpace->mRowSpace_IndexCount) // any indexes?
+ this->cut_all_index_entries(ev);
+
+ morkPool* pool = store->StorePool();
+ pool->CutRowCells(ev, this, /*newSize*/ 0, &store->mStore_Zone);
+ }
+}
+
+void morkRow::SetRow(morkEnv* ev, const morkRow* inSourceRow) {
+ // note inSourceRow might be in another DB, with a different store...
+ morkStore* store = this->GetRowSpaceStore(ev);
+ morkStore* srcStore = inSourceRow->GetRowSpaceStore(ev);
+ if (store && srcStore) {
+ if (this->MaybeDirtySpaceStoreAndRow()) {
+ this->SetRowRewrite();
+ this->NoteRowSetAll(ev);
+ }
+ morkRowSpace* rowSpace = mRow_Space;
+ mork_count indexes = rowSpace->mRowSpace_IndexCount; // any indexes?
+
+ mork_bool sameStore = (store == srcStore); // identical stores?
+ morkPool* pool = store->StorePool();
+ if (pool->CutRowCells(ev, this, /*newSize*/ 0, &store->mStore_Zone)) {
+ mork_fill fill = inSourceRow->mRow_Length;
+ if (pool->AddRowCells(ev, this, fill, &store->mStore_Zone)) {
+ morkCell* dst = mRow_Cells;
+ morkCell* dstEnd = dst + mRow_Length;
+
+ const morkCell* src = inSourceRow->mRow_Cells;
+ const morkCell* srcEnd = src + fill;
+ --dst;
+ --src; // prepare both for preincrement:
+
+ while (++dst < dstEnd && ++src < srcEnd && ev->Good()) {
+ morkAtom* atom = src->mCell_Atom;
+ mork_column dstCol = src->GetColumn();
+ // Note we modify the mCell_Atom slot directly instead of using
+ // morkCell::SetAtom(), because we know it starts equal to nil.
+
+ if (sameStore) // source and dest in same store?
+ {
+ // next line equivalent to inline morkCell::SetCellDirty():
+ dst->SetCellColumnDirty(dstCol);
+ dst->mCell_Atom = atom;
+ if (atom) // another ref to non-nil atom?
+ atom->AddCellUse(ev);
+ } else // need to dup items from src store in a dest store
+ {
+ dstCol = store->CopyToken(ev, dstCol, srcStore);
+ if (dstCol) {
+ // next line equivalent to inline morkCell::SetCellDirty():
+ dst->SetCellColumnDirty(dstCol);
+ atom = store->CopyAtom(ev, atom);
+ dst->mCell_Atom = atom;
+ if (atom) // another ref?
+ atom->AddCellUse(ev);
+ }
+ }
+ if (indexes && atom) {
+ mork_aid atomAid = atom->GetBookAtomAid();
+ if (atomAid) {
+ morkAtomRowMap* map = rowSpace->FindMap(ev, dstCol);
+ if (map) map->AddAid(ev, atomAid, this);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+void morkRow::AddRow(morkEnv* ev, const morkRow* inSourceRow) {
+ if (mRow_Length) // any existing cells we might need to keep?
+ {
+ ev->StubMethodOnlyError();
+ } else
+ this->SetRow(ev, inSourceRow); // just exactly duplicate inSourceRow
+}
+
+void morkRow::OnZeroRowGcUse(morkEnv* ev)
+// OnZeroRowGcUse() is called when CutRowGcUse() returns zero.
+{
+ MORK_USED_1(ev);
+ // ev->NewWarning("need to implement OnZeroRowGcUse");
+}
+
+void morkRow::DirtyAllRowContent(morkEnv* ev) {
+ MORK_USED_1(ev);
+
+ if (this->MaybeDirtySpaceStoreAndRow()) {
+ this->SetRowRewrite();
+ this->NoteRowSetAll(ev);
+ }
+ morkCell* cells = mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end) {
+ cells->SetCellDirty();
+ }
+ }
+}
+
+morkStore* morkRow::GetRowSpaceStore(morkEnv* ev) const {
+ morkRowSpace* rowSpace = mRow_Space;
+ if (rowSpace) {
+ morkStore* store = rowSpace->mSpace_Store;
+ if (store) {
+ if (store->IsStore()) {
+ return store;
+ } else
+ store->NonStoreTypeError(ev);
+ } else
+ ev->NilPointerError();
+ } else
+ ev->NilPointerError();
+
+ return (morkStore*)0;
+}
+
+void morkRow::CutColumn(morkEnv* ev, mdb_column inColumn) {
+ mork_pos pos = -1;
+ morkCell* cell = this->GetCell(ev, inColumn, &pos);
+ if (cell) {
+ morkStore* store = this->GetRowSpaceStore(ev);
+ if (store) {
+ if (this->MaybeDirtySpaceStoreAndRow() && !this->IsRowRewrite())
+ this->NoteRowCutCol(ev, inColumn);
+
+ morkRowSpace* rowSpace = mRow_Space;
+ morkAtomRowMap* map = (rowSpace->mRowSpace_IndexCount)
+ ? rowSpace->FindMap(ev, inColumn)
+ : (morkAtomRowMap*)0;
+ if (map) // this row attribute is indexed by row space?
+ {
+ morkAtom* oldAtom = cell->mCell_Atom;
+ if (oldAtom) // need to cut an entry from the index?
+ {
+ mork_aid oldAid = oldAtom->GetBookAtomAid();
+ if (oldAid) // cut old row attribute from row index in space?
+ map->CutAid(ev, oldAid);
+ }
+ }
+
+ morkPool* pool = store->StorePool();
+ cell->SetAtom(ev, (morkAtom*)0, pool);
+
+ mork_fill fill = mRow_Length; // should not be zero
+ MORK_ASSERT(fill);
+ if (fill) // index < fill for last cell exists?
+ {
+ mork_fill last = fill - 1; // index of last cell in row
+
+ if (pos < (mork_pos)last) // need to move cells following cut cell?
+ {
+ morkCell* lastCell = mRow_Cells + last;
+ mork_count after = last - pos; // cell count after cut cell
+ morkCell* next = cell + 1; // next cell after cut cell
+ MORK_MEMMOVE(cell, next, after * sizeof(morkCell));
+ lastCell->SetColumnAndChange(0, 0);
+ lastCell->mCell_Atom = 0;
+ }
+
+ if (ev->Good())
+ pool->CutRowCells(ev, this, fill - 1, &store->mStore_Zone);
+ }
+ }
+ }
+}
+
+morkAtom* morkRow::GetColumnAtom(morkEnv* ev, mdb_column inColumn) {
+ if (ev->Good()) {
+ mork_pos pos = -1;
+ morkCell* cell = this->GetCell(ev, inColumn, &pos);
+ if (cell) return cell->mCell_Atom;
+ }
+ return (morkAtom*)0;
+}
+
+void morkRow::AddColumn(morkEnv* ev, mdb_column inColumn, const mdbYarn* inYarn,
+ morkStore* ioStore) {
+ if (ev->Good()) {
+ mork_pos pos = -1;
+ morkCell* cell = this->GetCell(ev, inColumn, &pos);
+ morkCell* oldCell = cell; // need to know later whether new
+ if (!cell) // column does not yet exist?
+ cell = this->NewCell(ev, inColumn, &pos, ioStore);
+
+ if (cell) {
+ morkAtom* oldAtom = cell->mCell_Atom;
+
+ morkAtom* atom = ioStore->YarnToAtom(ev, inYarn, true /* create */);
+ if (atom && atom != oldAtom) {
+ morkRowSpace* rowSpace = mRow_Space;
+ morkAtomRowMap* map = (rowSpace->mRowSpace_IndexCount)
+ ? rowSpace->FindMap(ev, inColumn)
+ : (morkAtomRowMap*)0;
+
+ if (map) // inColumn is indexed by row space?
+ {
+ if (oldAtom && oldAtom != atom) // cut old cell from index?
+ {
+ mork_aid oldAid = oldAtom->GetBookAtomAid();
+ if (oldAid) // cut old row attribute from row index in space?
+ map->CutAid(ev, oldAid);
+ }
+ }
+
+ cell->SetAtom(ev, atom, ioStore->StorePool()); // refcounts atom
+
+ if (oldCell) // we changed a pre-existing cell in the row?
+ {
+ ++mRow_Seed;
+ if (this->MaybeDirtySpaceStoreAndRow() && !this->IsRowRewrite())
+ this->NoteRowAddCol(ev, inColumn);
+ }
+
+ if (map) // inColumn is indexed by row space?
+ {
+ mork_aid newAid = atom->GetBookAtomAid();
+ if (newAid) // add new row attribute to row index in space?
+ map->AddAid(ev, newAid, this);
+ }
+ }
+ }
+ }
+}
+
+morkRowCellCursor* morkRow::NewRowCellCursor(morkEnv* ev, mdb_pos inPos) {
+ morkRowCellCursor* outCursor = 0;
+ if (ev->Good()) {
+ morkStore* store = this->GetRowSpaceStore(ev);
+ if (store) {
+ morkRowObject* rowObj = this->AcquireRowObject(ev, store);
+ if (rowObj) {
+ nsIMdbHeap* heap = store->mPort_Heap;
+ morkRowCellCursor* cursor = new (*heap, ev)
+ morkRowCellCursor(ev, morkUsage::kHeap, heap, rowObj);
+
+ if (cursor) {
+ if (ev->Good()) {
+ cursor->mRowCellCursor_Col = inPos;
+ outCursor = cursor;
+ } else
+ cursor->CutStrongRef(ev->mEnv_SelfAsMdbEnv);
+ }
+ rowObj->Release(); // always cut ref (cursor has its own)
+ }
+ }
+ }
+ return outCursor;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkRow.h b/comm/mailnews/db/mork/morkRow.h
new file mode 100644
index 0000000000..e8a8c728ac
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRow.h
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKROW_
+#define _MORKROW_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class nsIMdbRow;
+class nsIMdbCell;
+#define morkDerived_kRow /*i*/ 0x5277 /* ascii 'Rw' */
+
+#define morkRow_kMaxGcUses 0x0FF /* max for 8-bit unsigned int */
+#define morkRow_kMaxLength 0x0FFFF /* max for 16-bit unsigned int */
+#define morkRow_kMinusOneRid ((mork_rid)-1)
+
+#define morkRow_kTag 'r' /* magic signature for mRow_Tag */
+
+#define morkRow_kNotedBit ((mork_u1)(1 << 0)) /* space has change notes */
+#define morkRow_kRewriteBit ((mork_u1)(1 << 1)) /* must rewrite all cells */
+#define morkRow_kDirtyBit ((mork_u1)(1 << 2)) /* row has been changed */
+
+class morkRow { // row of cells
+
+ public: // state is public because the entire Mork system is private
+ morkRowSpace* mRow_Space; // mRow_Space->SpaceScope() is the row scope
+ morkRowObject* mRow_Object; // refcount & other state for object sharing
+ morkCell* mRow_Cells;
+ mdbOid mRow_Oid;
+
+ mork_delta mRow_Delta; // space to note a single column change
+
+ mork_u2 mRow_Length; // physical count of cells in mRow_Cells
+ mork_u2 mRow_Seed; // count changes in mRow_Cells structure
+
+ mork_u1 mRow_GcUses; // persistent references from tables
+ mork_u1 mRow_Pad; // for u1 alignment
+ mork_u1 mRow_Flags; // one-byte flags slot
+ mork_u1 mRow_Tag; // one-byte tag (need u4 alignment pad)
+
+ public: // interpreting mRow_Delta
+ mork_bool HasRowDelta() const { return (mRow_Delta != 0); }
+
+ void ClearRowDelta() { mRow_Delta = 0; }
+
+ void SetRowDelta(mork_column inCol, mork_change inChange) {
+ morkDelta_Init(mRow_Delta, inCol, inChange);
+ }
+
+ mork_column GetDeltaColumn() const { return morkDelta_Column(mRow_Delta); }
+ mork_change GetDeltaChange() const { return morkDelta_Change(mRow_Delta); }
+
+ public: // noting row changes
+ void NoteRowSetAll(morkEnv* ev);
+ void NoteRowSetCol(morkEnv* ev, mork_column inCol);
+ void NoteRowAddCol(morkEnv* ev, mork_column inCol);
+ void NoteRowCutCol(morkEnv* ev, mork_column inCol);
+
+ public: // flags bit twiddling
+ void SetRowNoted() { mRow_Flags |= morkRow_kNotedBit; }
+ void SetRowRewrite() { mRow_Flags |= morkRow_kRewriteBit; }
+ void SetRowDirty() { mRow_Flags |= morkRow_kDirtyBit; }
+
+ void ClearRowNoted() { mRow_Flags &= (mork_u1)~morkRow_kNotedBit; }
+ void ClearRowRewrite() { mRow_Flags &= (mork_u1)~morkRow_kRewriteBit; }
+ void SetRowClean() {
+ mRow_Flags = 0;
+ mRow_Delta = 0;
+ }
+
+ mork_bool IsRowNoted() const { return (mRow_Flags & morkRow_kNotedBit) != 0; }
+
+ mork_bool IsRowRewrite() const {
+ return (mRow_Flags & morkRow_kRewriteBit) != 0;
+ }
+
+ mork_bool IsRowClean() const { return (mRow_Flags & morkRow_kDirtyBit) == 0; }
+
+ mork_bool IsRowDirty() const { return (mRow_Flags & morkRow_kDirtyBit) != 0; }
+
+ mork_bool IsRowUsed() const { return mRow_GcUses != 0; }
+
+ public: // other row methods
+ morkRow() {}
+ explicit morkRow(const mdbOid* inOid) : mRow_Oid(*inOid) {}
+ void InitRow(morkEnv* ev, const mdbOid* inOid, morkRowSpace* ioSpace,
+ mork_size inLength, morkPool* ioPool);
+ // if inLength is nonzero, cells will be allocated from ioPool
+
+ morkRowObject* AcquireRowObject(morkEnv* ev, morkStore* ioStore);
+ nsIMdbRow* AcquireRowHandle(morkEnv* ev, morkStore* ioStore);
+ nsIMdbCell* AcquireCellHandle(morkEnv* ev, morkCell* ioCell,
+ mdb_column inColumn, mork_pos inPos);
+
+ mork_u2 AddRowGcUse(morkEnv* ev);
+ mork_u2 CutRowGcUse(morkEnv* ev);
+
+ mork_bool MaybeDirtySpaceStoreAndRow();
+
+ public: // internal row methods
+ void cut_all_index_entries(morkEnv* ev);
+
+ // void cut_cell_from_space_index(morkEnv* ev, morkCell* ioCell);
+
+ mork_count CountOverlap(morkEnv* ev, morkCell* ioVector, mork_fill inFill);
+ // Count cells in ioVector that change existing cells in this row when
+ // ioVector is added to the row (as in TakeCells()). This is the set
+ // of cells with the same columns in ioVector and mRow_Cells, which do
+ // not have exactly the same value in mCell_Atom, and which do not both
+ // have change status equal to morkChange_kCut (because cutting a cut
+ // cell still yields a cell that has been cut). CountOverlap() also
+ // modifies the change attribute of any cell in ioVector to kDup when
+ // the change was previously kCut and the same column cell was found
+ // in this row with change also equal to kCut; this tells callers later
+ // they need not look for that cell in the row again on a second pass.
+
+ void MergeCells(morkEnv* ev, morkCell* ioVector, mork_fill inVecLength,
+ mork_fill inOldRowFill, mork_fill inOverlap);
+ // MergeCells() is the part of TakeCells() that does the insertion.
+ // inOldRowFill is the old value of mRow_Length, and inOverlap is the
+ // number of cells in the intersection that must be updated.
+
+ void TakeCells(morkEnv* ev, morkCell* ioVector, mork_fill inVecLength,
+ morkStore* ioStore);
+
+ morkCell* NewCell(morkEnv* ev, mdb_column inColumn, mork_pos* outPos,
+ morkStore* ioStore);
+ morkCell* GetCell(morkEnv* ev, mdb_column inColumn, mork_pos* outPos) const;
+ morkCell* CellAt(morkEnv* ev, mork_pos inPos) const;
+
+ mork_aid GetCellAtomAid(morkEnv* ev, mdb_column inColumn) const;
+ // GetCellAtomAid() finds the cell with column inColumn, and sees if the
+ // atom has a token ID, and returns the atom's ID if there is one. Or
+ // else zero is returned if there is no such column, or no atom, or if
+ // the atom has no ID to return. This method is intended to support
+ // efficient updating of column indexes for rows in a row space.
+
+ public: // external row methods
+ void DirtyAllRowContent(morkEnv* ev);
+
+ morkStore* GetRowSpaceStore(morkEnv* ev) const;
+
+ void AddColumn(morkEnv* ev, mdb_column inColumn, const mdbYarn* inYarn,
+ morkStore* ioStore);
+
+ morkAtom* GetColumnAtom(morkEnv* ev, mdb_column inColumn);
+
+ void NextColumn(morkEnv* ev, mdb_column* ioColumn, mdbYarn* outYarn);
+
+ void SeekColumn(morkEnv* ev, mdb_pos inPos, mdb_column* outColumn,
+ mdbYarn* outYarn);
+
+ void CutColumn(morkEnv* ev, mdb_column inColumn);
+
+ morkRowCellCursor* NewRowCellCursor(morkEnv* ev, mdb_pos inPos);
+
+ void EmptyAllCells(morkEnv* ev);
+ void AddRow(morkEnv* ev, const morkRow* inSourceRow);
+ void SetRow(morkEnv* ev, const morkRow* inSourceRow);
+ void CutAllColumns(morkEnv* ev);
+
+ void OnZeroRowGcUse(morkEnv* ev);
+ // OnZeroRowGcUse() is called when CutRowGcUse() returns zero.
+
+ public: // dynamic typing
+ mork_bool IsRow() const { return mRow_Tag == morkRow_kTag; }
+
+ public: // hash and equal
+ mork_u4 HashRow() const {
+ return (mRow_Oid.mOid_Scope << 16) ^ mRow_Oid.mOid_Id;
+ }
+
+ mork_bool EqualRow(const morkRow* ioRow) const {
+ return ((mRow_Oid.mOid_Scope == ioRow->mRow_Oid.mOid_Scope) &&
+ (mRow_Oid.mOid_Id == ioRow->mRow_Oid.mOid_Id));
+ }
+
+ mork_bool EqualOid(const mdbOid* ioOid) const {
+ return ((mRow_Oid.mOid_Scope == ioOid->mOid_Scope) &&
+ (mRow_Oid.mOid_Id == ioOid->mOid_Id));
+ }
+
+ public: // errors
+ static void ZeroColumnError(morkEnv* ev);
+ static void LengthBeyondMaxError(morkEnv* ev);
+ static void NilCellsError(morkEnv* ev);
+ static void NonRowTypeError(morkEnv* ev);
+ static void NonRowTypeWarning(morkEnv* ev);
+ static void GcUsesUnderflowWarning(morkEnv* ev);
+
+ private: // copying is not allowed
+ morkRow(const morkRow& other);
+ morkRow& operator=(const morkRow& other);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKROW_ */
diff --git a/comm/mailnews/db/mork/morkRowCellCursor.cpp b/comm/mailnews/db/mork/morkRowCellCursor.cpp
new file mode 100644
index 0000000000..edd6ebfd19
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowCellCursor.cpp
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKROWCELLCURSOR_
+# include "morkRowCellCursor.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkRowCellCursor::CloseMorkNode(
+ morkEnv* ev) // CloseRowCellCursor() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseRowCellCursor(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkRowCellCursor::~morkRowCellCursor() // CloseRowCellCursor() executed
+ // earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkRowCellCursor::morkRowCellCursor(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap,
+ morkRowObject* ioRowObject)
+ : morkCursor(ev, inUsage, ioHeap),
+ mRowCellCursor_RowObject(0),
+ mRowCellCursor_Col(0) {
+ if (ev->Good()) {
+ if (ioRowObject) {
+ morkRow* row = ioRowObject->mRowObject_Row;
+ if (row) {
+ if (row->IsRow()) {
+ mCursor_Pos = -1;
+ mCursor_Seed = row->mRow_Seed;
+
+ morkRowObject::SlotStrongRowObject(ioRowObject, ev,
+ &mRowCellCursor_RowObject);
+ if (ev->Good()) mNode_Derived = morkDerived_kRowCellCursor;
+ } else
+ row->NonRowTypeError(ev);
+ } else
+ ioRowObject->NilRowError(ev);
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkRowCellCursor, morkCursor, nsIMdbRowCellCursor)
+
+/*public non-poly*/ void morkRowCellCursor::CloseRowCellCursor(morkEnv* ev) {
+ if (this->IsNode()) {
+ mCursor_Pos = -1;
+ mCursor_Seed = 0;
+ morkRowObject::SlotStrongRowObject((morkRowObject*)0, ev,
+ &mRowCellCursor_RowObject);
+ this->CloseCursor(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkRowCellCursor::NilRowObjectError(morkEnv* ev) {
+ ev->NewError("nil mRowCellCursor_RowObject");
+}
+
+/*static*/ void morkRowCellCursor::NonRowCellCursorTypeError(morkEnv* ev) {
+ ev->NewError("non morkRowCellCursor");
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+// { ----- begin attribute methods -----
+NS_IMETHODIMP
+morkRowCellCursor::SetRow(nsIMdbEnv* mev, nsIMdbRow* ioRow) {
+ nsresult outErr = NS_OK;
+ morkRow* row = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ row = (morkRow*)ioRow;
+ morkStore* store = row->GetRowSpaceStore(ev);
+ if (store) {
+ morkRowObject* rowObj = row->AcquireRowObject(ev, store);
+ if (rowObj) {
+ morkRowObject::SlotStrongRowObject((morkRowObject*)0, ev,
+ &mRowCellCursor_RowObject);
+
+ mRowCellCursor_RowObject = rowObj; // take this strong ref
+ mCursor_Seed = row->mRow_Seed;
+
+ row->GetCell(ev, mRowCellCursor_Col, &mCursor_Pos);
+ }
+ }
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowCellCursor::GetRow(nsIMdbEnv* mev, nsIMdbRow** acqRow) {
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* rowObj = mRowCellCursor_RowObject;
+ if (rowObj) outRow = rowObj->AcquireRowHandle(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin cell seeking methods -----
+NS_IMETHODIMP
+morkRowCellCursor::SeekCell(
+ nsIMdbEnv* mev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ nsIMdbCell** acqCell) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end cell seeking methods -----
+
+// { ----- begin cell iteration methods -----
+NS_IMETHODIMP
+morkRowCellCursor::NextCell( // get next cell in the row
+ nsIMdbEnv* mev, // context
+ nsIMdbCell** acqCell, // changes to the next cell in the iteration
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ mdb_column col = 0;
+ mdb_pos pos = mRowCellCursor_Col;
+ if (pos < 0)
+ pos = 0;
+ else
+ ++pos;
+
+ morkCell* cell = mRowCellCursor_RowObject->mRowObject_Row->CellAt(ev, pos);
+ if (cell) {
+ col = cell->GetColumn();
+ *acqCell = mRowCellCursor_RowObject->mRowObject_Row->AcquireCellHandle(
+ ev, cell, col, pos);
+ } else {
+ *acqCell = nullptr;
+ pos = -1;
+ }
+ if (outPos) *outPos = pos;
+ if (outColumn) *outColumn = col;
+
+ mRowCellCursor_Col = pos;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkRowCellCursor::PickNextCell( // get next cell in row within filter set
+ nsIMdbEnv* mev, // context
+ nsIMdbCell* ioCell, // changes to the next cell in the iteration
+ const mdbColumnSet* inFilterSet, // col set of actual caller interest
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos)
+// Note that inFilterSet should not have too many (many more than 10?)
+// cols, since this might imply a potential excessive consumption of time
+// over many cursor calls when looking for column and filter intersection.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// } ----- end cell iteration methods -----
+
+// } ===== end nsIMdbRowCellCursor methods =====
diff --git a/comm/mailnews/db/mork/morkRowCellCursor.h b/comm/mailnews/db/mork/morkRowCellCursor.h
new file mode 100644
index 0000000000..91e032d2bc
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowCellCursor.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKROWCELLCURSOR_
+#define _MORKROWCELLCURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class orkinRowCellCursor;
+#define morkDerived_kRowCellCursor /*i*/ 0x6343 /* ascii 'cC' */
+
+class morkRowCellCursor : public morkCursor,
+ public nsIMdbRowCellCursor { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkFactory* mObject_Factory; // weak ref to suite factory
+
+ // mork_seed mCursor_Seed;
+ // mork_pos mCursor_Pos;
+ // mork_bool mCursor_DoFailOnSeedOutOfSync;
+ // mork_u1 mCursor_Pad[ 3 ]; // explicitly pad to u4 alignment
+
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+ morkRowObject* mRowCellCursor_RowObject; // strong ref to row
+ mork_column mRowCellCursor_Col; // col of cell last at mCursor_Pos
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseRowCellCursor()
+
+ public: // morkRowCellCursor construction & destruction
+ morkRowCellCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkRowObject* ioRowObject);
+ void CloseRowCellCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD SetRow(nsIMdbEnv* ev,
+ nsIMdbRow* ioRow) override; // sets pos to -1
+ NS_IMETHOD GetRow(nsIMdbEnv* ev, nsIMdbRow** acqRow) override;
+ // } ----- end attribute methods -----
+
+ // { ----- begin cell seeking methods -----
+ NS_IMETHOD SeekCell(nsIMdbEnv* ev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ nsIMdbCell** acqCell) override; // the cell at inPos
+ // } ----- end cell seeking methods -----
+
+ // { ----- begin cell iteration methods -----
+ NS_IMETHOD NextCell( // get next cell in the row
+ nsIMdbEnv* ev, // context
+ nsIMdbCell** acqCell, // changes to the next cell in the iteration
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos) override; // position of cell in row sequence
+
+ NS_IMETHOD PickNextCell( // get next cell in row within filter set
+ nsIMdbEnv* ev, // context
+ nsIMdbCell* ioCell, // changes to the next cell in the iteration
+ const mdbColumnSet* inFilterSet, // col set of actual caller interest
+ mdb_column* outColumn, // column for this particular cell
+ mdb_pos* outPos) override; // position of cell in row sequence
+
+ // Note that inFilterSet should not have too many (many more than 10?)
+ // cols, since this might imply a potential excessive consumption of time
+ // over many cursor calls when looking for column and filter intersection.
+ // } ----- end cell iteration methods -----
+
+ private: // copying is not allowed
+ morkRowCellCursor(const morkRowCellCursor& other);
+ morkRowCellCursor& operator=(const morkRowCellCursor& other);
+ virtual ~morkRowCellCursor(); // assert that close executed earlier
+
+ public: // dynamic type identification
+ mork_bool IsRowCellCursor() const {
+ return IsNode() && mNode_Derived == morkDerived_kRowCellCursor;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // errors
+ static void NilRowObjectError(morkEnv* ev);
+ static void NonRowCellCursorTypeError(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakRowCellCursor(morkRowCellCursor* me, morkEnv* ev,
+ morkRowCellCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongRowCellCursor(morkRowCellCursor* me, morkEnv* ev,
+ morkRowCellCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKROWCELLCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkRowMap.cpp b/comm/mailnews/db/mork/morkRowMap.cpp
new file mode 100644
index 0000000000..a1d415f8d2
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowMap.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkRowMap::CloseMorkNode(
+ morkEnv* ev) // CloseRowMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseRowMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkRowMap::~morkRowMap() // assert CloseRowMap() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkRowMap::morkRowMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ mork_size inSlots)
+ : morkMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkRow*), /*inValSize*/ 0, inSlots,
+ ioSlotHeap, /*inHoldChanges*/ morkBool_kFalse) {
+ if (ev->Good()) mNode_Derived = morkDerived_kRowMap;
+}
+
+/*public non-poly*/ void morkRowMap::CloseRowMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CloseMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ===== begin morkMap poly interface =====
+/*virtual*/ mork_bool //
+morkRowMap::Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const {
+ MORK_USED_1(ev);
+ return (*(const morkRow**)inKeyA)->EqualRow(*(const morkRow**)inKeyB);
+}
+
+/*virtual*/ mork_u4 //
+morkRowMap::Hash(morkEnv* ev, const void* inKey) const {
+ MORK_USED_1(ev);
+ return (*(const morkRow**)inKey)->HashRow();
+}
+// } ===== end morkMap poly interface =====
+
+mork_bool morkRowMap::AddRow(morkEnv* ev, morkRow* ioRow) {
+ if (ev->Good()) {
+ this->Put(ev, &ioRow, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0, (mork_change**)0);
+ }
+ return ev->Good();
+}
+
+morkRow* morkRowMap::CutOid(morkEnv* ev, const mdbOid* inOid) {
+ morkRow row(inOid);
+ morkRow* key = &row;
+ morkRow* oldKey = 0;
+ this->Cut(ev, &key, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+
+ return oldKey;
+}
+
+morkRow* morkRowMap::CutRow(morkEnv* ev, const morkRow* ioRow) {
+ morkRow* oldKey = 0;
+ this->Cut(ev, &ioRow, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+
+ return oldKey;
+}
+
+morkRow* morkRowMap::GetOid(morkEnv* ev, const mdbOid* inOid) {
+ morkRow row(inOid);
+ morkRow* key = &row;
+ morkRow* oldKey = 0;
+ this->Get(ev, &key, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+
+ return oldKey;
+}
+
+morkRow* morkRowMap::GetRow(morkEnv* ev, const morkRow* ioRow) {
+ morkRow* oldKey = 0;
+ this->Get(ev, &ioRow, &oldKey, /*val*/ (void*)0, (mork_change**)0);
+
+ return oldKey;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkRowProbeMap::CloseMorkNode(
+ morkEnv* ev) // CloseRowProbeMap() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseRowProbeMap(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkRowProbeMap::~morkRowProbeMap() // assert CloseRowProbeMap() executed
+ // earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkRowProbeMap::morkRowProbeMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap,
+ mork_size inSlots)
+ : morkProbeMap(ev, inUsage, ioHeap,
+ /*inKeySize*/ sizeof(morkRow*), /*inValSize*/ 0, ioSlotHeap,
+ inSlots,
+ /*inHoldChanges*/ morkBool_kTrue) {
+ if (ev->Good()) mNode_Derived = morkDerived_kRowProbeMap;
+}
+
+/*public non-poly*/ void morkRowProbeMap::CloseRowProbeMap(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ this->CloseProbeMap(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*virtual*/ mork_test // hit(a,b) implies hash(a) == hash(b)
+morkRowProbeMap::MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const {
+ MORK_USED_1(ev);
+ const morkRow* key = *(const morkRow**)inMapKey;
+ if (key) {
+ mork_bool hit = key->EqualRow(*(const morkRow**)inAppKey);
+ return (hit) ? morkTest_kHit : morkTest_kMiss;
+ } else
+ return morkTest_kVoid;
+}
+
+/*virtual*/ mork_u4 // hit(a,b) implies hash(a) == hash(b)
+morkRowProbeMap::MapHash(morkEnv* ev, const void* inAppKey) const {
+ const morkRow* key = *(const morkRow**)inAppKey;
+ if (key)
+ return key->HashRow();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+
+/*virtual*/ mork_u4 morkRowProbeMap::ProbeMapHashMapKey(
+ morkEnv* ev, const void* inMapKey) const {
+ const morkRow* key = *(const morkRow**)inMapKey;
+ if (key)
+ return key->HashRow();
+ else {
+ ev->NilPointerWarning();
+ return 0;
+ }
+}
+
+mork_bool morkRowProbeMap::AddRow(morkEnv* ev, morkRow* ioRow) {
+ if (ev->Good()) {
+ this->MapAtPut(ev, &ioRow, /*val*/ (void*)0,
+ /*key*/ (void*)0, /*val*/ (void*)0);
+ }
+ return ev->Good();
+}
+
+morkRow* morkRowProbeMap::CutOid(morkEnv* ev, const mdbOid* inOid) {
+ MORK_USED_1(inOid);
+ morkProbeMap::ProbeMapCutError(ev);
+
+ return 0;
+}
+
+morkRow* morkRowProbeMap::CutRow(morkEnv* ev, const morkRow* ioRow) {
+ MORK_USED_1(ioRow);
+ morkProbeMap::ProbeMapCutError(ev);
+
+ return 0;
+}
+
+morkRow* morkRowProbeMap::GetOid(morkEnv* ev, const mdbOid* inOid) {
+ morkRow row(inOid);
+ morkRow* key = &row;
+ morkRow* oldKey = 0;
+ this->MapAt(ev, &key, &oldKey, /*val*/ (void*)0);
+
+ return oldKey;
+}
+
+morkRow* morkRowProbeMap::GetRow(morkEnv* ev, const morkRow* ioRow) {
+ morkRow* oldKey = 0;
+ this->MapAt(ev, &ioRow, &oldKey, /*val*/ (void*)0);
+
+ return oldKey;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkRowMap.h b/comm/mailnews/db/mork/morkRowMap.h
new file mode 100644
index 0000000000..d58515d4db
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowMap.h
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKROWMAP_
+#define _MORKROWMAP_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKPROBEMAP_
+# include "morkProbeMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kRowMap /*i*/ 0x724D /* ascii 'rM' */
+
+/*| morkRowMap: maps a set of morkRow by contained Oid
+|*/
+class morkRowMap : public morkMap { // for mapping row IDs to rows
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseRowMap() only if open
+ virtual ~morkRowMap(); // assert that CloseRowMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkRowMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_size inSlots);
+ void CloseRowMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsRowMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kRowMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkMap poly interface =====
+ virtual mork_bool // note: equal(a,b) implies hash(a) == hash(b)
+ Equal(morkEnv* ev, const void* inKeyA, const void* inKeyB) const override;
+ // implemented using morkRow::EqualRow()
+
+ virtual mork_u4 // note: equal(a,b) implies hash(a) == hash(b)
+ Hash(morkEnv* ev, const void* inKey) const override;
+ // implemented using morkRow::HashRow()
+ // } ===== end morkMap poly interface =====
+
+ public: // other map methods
+ mork_bool AddRow(morkEnv* ev, morkRow* ioRow);
+ // AddRow() returns ev->Good()
+
+ morkRow* CutOid(morkEnv* ev, const mdbOid* inOid);
+ // CutRid() returns the row removed equal to inRid, if there was one
+
+ morkRow* CutRow(morkEnv* ev, const morkRow* ioRow);
+ // CutRow() returns the row removed equal to ioRow, if there was one
+
+ morkRow* GetOid(morkEnv* ev, const mdbOid* inOid);
+ // GetOid() returns the row equal to inRid, or else nil
+
+ morkRow* GetRow(morkEnv* ev, const morkRow* ioRow);
+ // GetRow() returns the row equal to ioRow, or else nil
+
+ // note the rows are owned elsewhere, usually by morkRowSpace
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakRowMap(morkRowMap* me, morkEnv* ev, morkRowMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongRowMap(morkRowMap* me, morkEnv* ev,
+ morkRowMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+class morkRowMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkRowMapIter(morkEnv* ev, morkRowMap* ioMap) : morkMapIter(ev, ioMap) {}
+
+ morkRowMapIter() : morkMapIter() {}
+ void InitRowMapIter(morkEnv* ev, morkRowMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->First(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* NextRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->Next(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* HereRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->Here(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* CutHereRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->CutHere(ev, outRowPtr, /*val*/ (void*)0);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kRowProbeMap /*i*/ 0x726D /* ascii 'rm' */
+
+/*| morkRowProbeMap: maps a set of morkRow by contained Oid
+|*/
+class morkRowProbeMap : public morkProbeMap { // for mapping row IDs to rows
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseRowProbeMap() only if open
+ virtual ~morkRowProbeMap(); // assert CloseRowProbeMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkRowProbeMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_size inSlots);
+ void CloseRowProbeMap(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsRowMap() const {
+ return IsNode() && mNode_Derived == morkDerived_kRowMap;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkProbeMap methods =====
+ virtual mork_test // hit(a,b) implies hash(a) == hash(b)
+ MapTest(morkEnv* ev, const void* inMapKey,
+ const void* inAppKey) const override;
+
+ virtual mork_u4 // hit(a,b) implies hash(a) == hash(b)
+ MapHash(morkEnv* ev, const void* inAppKey) const override;
+
+ virtual mork_u4 ProbeMapHashMapKey(morkEnv* ev,
+ const void* inMapKey) const override;
+
+ // virtual mork_bool ProbeMapIsKeyNil(morkEnv* ev, void* ioMapKey);
+
+ // virtual void ProbeMapClearKey(morkEnv* ev, // put 'nil' into all keys
+ // inside map
+ // void* ioMapKey, mork_count inKeyCount); // array of keys inside map
+
+ // virtual void ProbeMapPushIn(morkEnv* ev, // move (key,val) into the map
+ // const void* inAppKey, const void* inAppVal, // (key,val) outside map
+ // void* outMapKey, void* outMapVal); // (key,val) inside map
+
+ // virtual void ProbeMapPullOut(morkEnv* ev, // move (key,val) out from the
+ // map
+ // const void* inMapKey, const void* inMapVal, // (key,val) inside map
+ // void* outAppKey, void* outAppVal) const; // (key,val) outside map
+ // } ===== end morkProbeMap methods =====
+
+ public: // other map methods
+ mork_bool AddRow(morkEnv* ev, morkRow* ioRow);
+ // AddRow() returns ev->Good()
+
+ morkRow* CutOid(morkEnv* ev, const mdbOid* inOid);
+ // CutRid() returns the row removed equal to inRid, if there was one
+
+ morkRow* CutRow(morkEnv* ev, const morkRow* ioRow);
+ // CutRow() returns the row removed equal to ioRow, if there was one
+
+ morkRow* GetOid(morkEnv* ev, const mdbOid* inOid);
+ // GetOid() returns the row equal to inRid, or else nil
+
+ morkRow* GetRow(morkEnv* ev, const morkRow* ioRow);
+ // GetRow() returns the row equal to ioRow, or else nil
+
+ // note the rows are owned elsewhere, usually by morkRowSpace
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakRowProbeMap(morkRowProbeMap* me, morkEnv* ev,
+ morkRowProbeMap** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongRowProbeMap(morkRowProbeMap* me, morkEnv* ev,
+ morkRowProbeMap** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+class morkRowProbeMapIter : public morkProbeMapIter { // typesafe wrapper class
+
+ public:
+ morkRowProbeMapIter(morkEnv* ev, morkRowProbeMap* ioMap)
+ : morkProbeMapIter(ev, ioMap) {}
+
+ morkRowProbeMapIter() : morkProbeMapIter() {}
+ void InitRowMapIter(morkEnv* ev, morkRowProbeMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->First(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* NextRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->Next(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* HereRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->Here(ev, outRowPtr, /*val*/ (void*)0);
+ }
+
+ mork_change* CutHereRow(morkEnv* ev, morkRow** outRowPtr) {
+ return this->CutHere(ev, outRowPtr, /*val*/ (void*)0);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKROWMAP_ */
diff --git a/comm/mailnews/db/mork/morkRowObject.cpp b/comm/mailnews/db/mork/morkRowObject.cpp
new file mode 100644
index 0000000000..39844172b8
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowObject.cpp
@@ -0,0 +1,530 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKROWCELLCURSOR_
+# include "morkRowCellCursor.h"
+#endif
+
+#ifndef _MORKCELLOBJECT_
+# include "morkCellObject.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkRowObject::CloseMorkNode(
+ morkEnv* ev) // CloseRowObject() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseRowObject(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkRowObject::~morkRowObject() // assert CloseRowObject() executed earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkRowObject::morkRowObject(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkRow* ioRow,
+ morkStore* ioStore)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mRowObject_Row(0),
+ mRowObject_Store(0) {
+ if (ev->Good()) {
+ if (ioRow && ioStore) {
+ mRowObject_Row = ioRow;
+ mRowObject_Store =
+ ioStore; // morkRowObjects don't ref-cnt the owning store.
+
+ if (ev->Good()) mNode_Derived = morkDerived_kRowObject;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkRowObject, morkObject, nsIMdbRow)
+// { ===== begin nsIMdbCollection methods =====
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP
+morkRowObject::GetSeed(nsIMdbEnv* mev, mdb_seed* outSeed) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ *outSeed = (mdb_seed)mRowObject_Row->mRow_Seed;
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+NS_IMETHODIMP
+morkRowObject::GetCount(nsIMdbEnv* mev, mdb_count* outCount) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ *outCount = (mdb_count)mRowObject_Row->mRow_Length;
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::GetPort(nsIMdbEnv* mev, nsIMdbPort** acqPort) {
+ nsresult outErr = NS_OK;
+ nsIMdbPort* outPort = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowSpace* rowSpace = mRowObject_Row->mRow_Space;
+ if (rowSpace && rowSpace->mSpace_Store) {
+ morkStore* store = mRowObject_Row->GetRowSpaceStore(ev);
+ if (store) outPort = store->AcquireStoreHandle(ev);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqPort) *acqPort = outPort;
+
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin cursor methods -----
+NS_IMETHODIMP
+morkRowObject::GetCursor( // make a cursor starting iter at inMemberPos
+ nsIMdbEnv* mev, // context
+ mdb_pos inMemberPos, // zero-based ordinal pos of member in collection
+ nsIMdbCursor** acqCursor) {
+ return this->GetRowCellCursor(mev, inMemberPos,
+ (nsIMdbRowCellCursor**)acqCursor);
+}
+// } ----- end cursor methods -----
+
+// { ----- begin ID methods -----
+NS_IMETHODIMP
+morkRowObject::GetOid(nsIMdbEnv* mev, mdbOid* outOid) {
+ *outOid = mRowObject_Row->mRow_Oid;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ return (ev) ? ev->AsErr() : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+morkRowObject::BecomeContent(nsIMdbEnv* mev, const mdbOid* inOid) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // remember row->MaybeDirtySpaceStoreAndRow();
+}
+// } ----- end ID methods -----
+
+// { ----- begin activity dropping methods -----
+NS_IMETHODIMP
+morkRowObject::DropActivity( // tell collection usage no longer expected
+ nsIMdbEnv* mev) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end activity dropping methods -----
+
+// } ===== end nsIMdbCollection methods =====
+
+// { ===== begin nsIMdbRow methods =====
+
+// { ----- begin cursor methods -----
+NS_IMETHODIMP
+morkRowObject::GetRowCellCursor( // make a cursor starting iteration at
+ // inCellPos
+ nsIMdbEnv* mev, // context
+ mdb_pos inPos, // zero-based ordinal position of cell in row
+ nsIMdbRowCellCursor** acqCursor) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ nsIMdbRowCellCursor* outCursor = 0;
+ if (ev) {
+ morkRowCellCursor* cursor = mRowObject_Row->NewRowCellCursor(ev, inPos);
+ if (cursor) {
+ if (ev->Good()) {
+ cursor->mCursor_Seed = (mork_seed)inPos;
+ outCursor = cursor;
+ NS_ADDREF(cursor);
+ }
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqCursor) *acqCursor = outCursor;
+ return outErr;
+}
+// } ----- end cursor methods -----
+
+// { ----- begin column methods -----
+NS_IMETHODIMP
+morkRowObject::AddColumn( // make sure a particular column is inside row
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to add
+ const mdbYarn* inYarn) {
+ nsresult outErr = NS_ERROR_FAILURE;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store && mRowObject_Row)
+ mRowObject_Row->AddColumn(ev, inColumn, inYarn, mRowObject_Store);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::CutColumn( // make sure a column is absent from the row
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn) {
+ nsresult outErr = NS_ERROR_FAILURE;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mRowObject_Row->CutColumn(ev, inColumn);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::CutAllColumns( // remove all columns from the row
+ nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mRowObject_Row->CutAllColumns(ev);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end column methods -----
+
+// { ----- begin cell methods -----
+NS_IMETHODIMP
+morkRowObject::NewCell( // get cell for specified column, or add new one
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to add
+ nsIMdbCell** acqCell) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ GetCell(mev, inColumn, acqCell);
+ if (!*acqCell) {
+ if (mRowObject_Store) {
+ mdbYarn yarn; // to pass empty yarn into morkRowObject::AddColumn()
+ yarn.mYarn_Buf = 0;
+ yarn.mYarn_Fill = 0;
+ yarn.mYarn_Size = 0;
+ yarn.mYarn_More = 0;
+ yarn.mYarn_Form = 0;
+ yarn.mYarn_Grow = 0;
+ AddColumn(ev, inColumn, &yarn);
+ GetCell(mev, inColumn, acqCell);
+ }
+ }
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::AddCell( // copy a cell from another row to this row
+ nsIMdbEnv* mev, // context
+ const nsIMdbCell* inCell) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkCell* cell = 0;
+ morkCellObject* cellObj = (morkCellObject*)inCell;
+ if (cellObj->CanUseCell(mev, morkBool_kFalse, &outErr, &cell)) {
+ morkRow* cellRow = cellObj->mCellObject_Row;
+ if (cellRow) {
+ if (mRowObject_Row != cellRow) {
+ morkStore* store = mRowObject_Row->GetRowSpaceStore(ev);
+ morkStore* cellStore = cellRow->GetRowSpaceStore(ev);
+ if (store && cellStore) {
+ mork_column col = cell->GetColumn();
+ morkAtom* atom = cell->mCell_Atom;
+ mdbYarn yarn;
+ morkAtom::AliasYarn(atom, &yarn); // works even when atom is nil
+
+ if (store != cellStore) col = store->CopyToken(ev, col, cellStore);
+ if (ev->Good()) AddColumn(ev, col, &yarn);
+ } else
+ ev->NilPointerError();
+ }
+ } else
+ ev->NilPointerError();
+ }
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::GetCell( // find a cell in this row
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to find
+ nsIMdbCell** acqCell) {
+ nsresult outErr = NS_OK;
+ nsIMdbCell* outCell = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+
+ if (ev) {
+ if (inColumn) {
+ mork_pos pos = 0;
+ morkCell* cell = mRowObject_Row->GetCell(ev, inColumn, &pos);
+ if (cell) {
+ outCell = mRowObject_Row->AcquireCellHandle(ev, cell, inColumn, pos);
+ }
+ } else
+ mRowObject_Row->ZeroColumnError(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (acqCell) *acqCell = outCell;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::EmptyAllCells( // make all cells in row empty of content
+ nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ EmptyAllCells(ev);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end cell methods -----
+
+// { ----- begin row methods -----
+NS_IMETHODIMP
+morkRowObject::AddRow( // add all cells in another row to this one
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioSourceRow) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRow* unsafeSource = (morkRow*)ioSourceRow; // unsafe cast
+ // if ( unsafeSource->CanUseRow(mev, morkBool_kFalse, &outErr, &source) )
+ { mRowObject_Row->AddRow(ev, unsafeSource); }
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::SetRow( // make exact duplicate of another row
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioSourceRow) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* sourceObject = (morkRowObject*)ioSourceRow; // unsafe cast
+ morkRow* unsafeSource = sourceObject->mRowObject_Row;
+ // if ( unsafeSource->CanUseRow(mev, morkBool_kFalse, &outErr, &source) )
+ { mRowObject_Row->SetRow(ev, unsafeSource); }
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end row methods -----
+
+// { ----- begin blob methods -----
+NS_IMETHODIMP
+morkRowObject::SetCellYarn( // synonym for AddColumn()
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to add
+ const mdbYarn* inYarn) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store) AddColumn(ev, inColumn, inYarn);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+NS_IMETHODIMP
+morkRowObject::GetCellYarn(nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to read
+ mdbYarn* outYarn) // writes some yarn slots
+// copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store && mRowObject_Row) {
+ morkAtom* atom = mRowObject_Row->GetColumnAtom(ev, inColumn);
+ morkAtom::GetYarn(atom, outYarn);
+ }
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::AliasCellYarn(nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to alias
+ mdbYarn* outYarn) // writes ALL yarn slots
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store && mRowObject_Row) {
+ morkAtom* atom = mRowObject_Row->GetColumnAtom(ev, inColumn);
+ morkAtom::AliasYarn(atom, outYarn);
+ // note nil atom works and sets yarn correctly
+ }
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::NextCellYarn(
+ nsIMdbEnv* mev, // iterative version of GetCellYarn()
+ mdb_column* ioColumn, // next column to read
+ mdbYarn* outYarn) // writes some yarn slots
+// copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+//
+// The ioColumn argument is an inout parameter which initially contains the
+// last column accessed and returns the next column corresponding to the
+// content read into the yarn. Callers should start with a zero column
+// value to say 'no previous column', which causes the first column to be
+// read. Then the value returned in ioColumn is perfect for the next call
+// to NextCellYarn(), since it will then be the previous column accessed.
+// Callers need only examine the column token returned to see which cell
+// in the row is being read into the yarn. When no more columns remain,
+// and the iteration has ended, ioColumn will return a zero token again.
+// So iterating over cells starts and ends with a zero column token.
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store && mRowObject_Row)
+ mRowObject_Row->NextColumn(ev, ioColumn, outYarn);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkRowObject::SeekCellYarn( // resembles nsIMdbRowCellCursor::SeekCell()
+ nsIMdbEnv* mev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ mdbYarn* outYarn) // writes some yarn slots
+// copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+// Callers can pass nil for outYarn to indicate no interest in content, so
+// only the outColumn value is returned. NOTE to subclasses: you must be
+// able to ignore outYarn when the pointer is nil; please do not crash.
+
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mRowObject_Store && mRowObject_Row)
+ mRowObject_Row->SeekColumn(ev, inPos, outColumn, outYarn);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+// } ----- end blob methods -----
+
+// } ===== end nsIMdbRow methods =====
+
+/*public non-poly*/ void morkRowObject::CloseRowObject(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkRow* row = mRowObject_Row;
+ mRowObject_Row = 0;
+ this->CloseObject(ev);
+ this->MarkShut();
+
+ if (row) {
+ MORK_ASSERT(row->mRow_Object == this);
+ if (row->mRow_Object == this) {
+ row->mRow_Object = 0; // just nil this slot -- cut ref down below
+
+ mRowObject_Store = 0; // morkRowObjects don't ref-cnt the owning store.
+
+ this->CutWeakRef(
+ ev->AsMdbEnv()); // do last, because it might self destroy
+ }
+ }
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkRowObject::NonRowObjectTypeError(morkEnv* ev) {
+ ev->NewError("non morkRowObject");
+}
+
+/*static*/ void morkRowObject::NilRowError(morkEnv* ev) {
+ ev->NewError("nil mRowObject_Row");
+}
+
+/*static*/ void morkRowObject::NilStoreError(morkEnv* ev) {
+ ev->NewError("nil mRowObject_Store");
+}
+
+/*static*/ void morkRowObject::RowObjectRowNotSelfError(morkEnv* ev) {
+ ev->NewError("mRowObject_Row->mRow_Object != self");
+}
+
+nsIMdbRow* morkRowObject::AcquireRowHandle(morkEnv* ev) // mObject_Handle
+{
+ AddRef();
+ return this;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkRowObject.h b/comm/mailnews/db/mork/morkRowObject.h
new file mode 100644
index 0000000000..7af5642a3f
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowObject.h
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKROWOBJECT_
+#define _MORKROWOBJECT_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class nsIMdbRow;
+#define morkDerived_kRowObject /*i*/ 0x724F /* ascii 'rO' */
+
+class morkRowObject : public morkObject, public nsIMdbRow { //
+
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+
+ morkRow* mRowObject_Row; // non-refcounted alias to morkRow
+ morkStore* mRowObject_Store; // non-refcounted ptr to store containing row
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseRowObject() only if open
+
+ public: // morkRowObject construction & destruction
+ morkRowObject(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkRow* ioRow, morkStore* ioStore);
+ void CloseRowObject(morkEnv* ev); // called by CloseMorkNode();
+
+ // { ===== begin nsIMdbCollection methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev,
+ mdb_seed* outSeed) override; // member change count
+ NS_IMETHOD GetCount(nsIMdbEnv* ev,
+ mdb_count* outCount) override; // member count
+
+ NS_IMETHOD GetPort(nsIMdbEnv* ev,
+ nsIMdbPort** acqPort) override; // collection container
+ // } ----- end attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetCursor( // make a cursor starting iter at inMemberPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inMemberPos, // zero-based ordinal pos of member in collection
+ nsIMdbCursor** acqCursor) override; // acquire new cursor instance
+ // } ----- end cursor methods -----
+
+ // { ----- begin ID methods -----
+ NS_IMETHOD GetOid(nsIMdbEnv* ev,
+ mdbOid* outOid) override; // read object identity
+ NS_IMETHOD BecomeContent(nsIMdbEnv* ev,
+ const mdbOid* inOid) override; // exchange content
+ // } ----- end ID methods -----
+
+ // { ----- begin activity dropping methods -----
+ NS_IMETHOD DropActivity( // tell collection usage no longer expected
+ nsIMdbEnv* ev) override;
+ // } ----- end activity dropping methods -----
+
+ // } ===== end nsIMdbCollection methods =====
+ // { ===== begin nsIMdbRow methods =====
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetRowCellCursor( // make a cursor starting iteration at inRowPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbRowCellCursor** acqCursor) override; // acquire new cursor instance
+ // } ----- end cursor methods -----
+
+ // { ----- begin column methods -----
+ NS_IMETHOD AddColumn( // make sure a particular column is inside row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to add
+ const mdbYarn* inYarn) override; // cell value to install
+
+ NS_IMETHOD CutColumn( // make sure a column is absent from the row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) override; // column to ensure absent from row
+
+ NS_IMETHOD CutAllColumns( // remove all columns from the row
+ nsIMdbEnv* ev) override; // context
+ // } ----- end column methods -----
+
+ // { ----- begin cell methods -----
+ NS_IMETHOD NewCell( // get cell for specified column, or add new one
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to add
+ nsIMdbCell** acqCell) override; // cell column and value
+
+ NS_IMETHOD AddCell( // copy a cell from another row to this row
+ nsIMdbEnv* ev, // context
+ const nsIMdbCell* inCell) override; // cell column and value
+
+ NS_IMETHOD GetCell( // find a cell in this row
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to find
+ nsIMdbCell** acqCell) override; // cell for specified column, or null
+
+ NS_IMETHOD EmptyAllCells( // make all cells in row empty of content
+ nsIMdbEnv* ev) override; // context
+ // } ----- end cell methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD AddRow( // add all cells in another row to this one
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioSourceRow) override; // row to union with
+
+ NS_IMETHOD SetRow( // make exact duplicate of another row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioSourceRow) override; // row to duplicate
+ // } ----- end row methods -----
+
+ // { ----- begin blob methods -----
+ NS_IMETHOD SetCellYarn(
+ nsIMdbEnv* ev, // synonym for AddColumn()
+ mdb_column inColumn, // column to write
+ const mdbYarn* inYarn) override; // reads from yarn slots
+ // make this text object contain content from the yarn's buffer
+
+ NS_IMETHOD GetCellYarn(nsIMdbEnv* ev,
+ mdb_column inColumn, // column to read
+ mdbYarn* outYarn) override; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+
+ NS_IMETHOD AliasCellYarn(nsIMdbEnv* ev,
+ mdb_column inColumn, // column to alias
+ mdbYarn* outYarn) override; // writes ALL yarn slots
+
+ NS_IMETHOD NextCellYarn(nsIMdbEnv* ev, // iterative version of GetCellYarn()
+ mdb_column* ioColumn, // next column to read
+ mdbYarn* outYarn) override; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+ //
+ // The ioColumn argument is an inout parameter which initially contains the
+ // last column accessed and returns the next column corresponding to the
+ // content read into the yarn. Callers should start with a zero column
+ // value to say 'no previous column', which causes the first column to be
+ // read. Then the value returned in ioColumn is perfect for the next call
+ // to NextCellYarn(), since it will then be the previous column accessed.
+ // Callers need only examine the column token returned to see which cell
+ // in the row is being read into the yarn. When no more columns remain,
+ // and the iteration has ended, ioColumn will return a zero token again.
+ // So iterating over cells starts and ends with a zero column token.
+
+ NS_IMETHOD SeekCellYarn( // resembles nsIMdbRowCellCursor::SeekCell()
+ nsIMdbEnv* ev, // context
+ mdb_pos inPos, // position of cell in row sequence
+ mdb_column* outColumn, // column for this particular cell
+ mdbYarn* outYarn) override; // writes some yarn slots
+ // copy content into the yarn buffer, and update mYarn_Fill and mYarn_Form
+ // Callers can pass nil for outYarn to indicate no interest in content, so
+ // only the outColumn value is returned. NOTE to subclasses: you must be
+ // able to ignore outYarn when the pointer is nil; please do not crash.
+
+ // } ----- end blob methods -----
+
+ // } ===== end nsIMdbRow methods =====
+
+ private: // copying is not allowed
+ morkRowObject(const morkRowObject& other);
+ morkRowObject& operator=(const morkRowObject& other);
+ virtual ~morkRowObject(); // assert that CloseRowObject() executed earlier
+
+ public: // dynamic type identification
+ mork_bool IsRowObject() const {
+ return IsNode() && mNode_Derived == morkDerived_kRowObject;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonRowObjectTypeError(morkEnv* ev);
+ static void NilRowError(morkEnv* ev);
+ static void NilStoreError(morkEnv* ev);
+ static void RowObjectRowNotSelfError(morkEnv* ev);
+
+ public: // other row node methods
+ nsIMdbRow* AcquireRowHandle(morkEnv* ev); // mObject_Handle
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakRowObject(morkRowObject* me, morkEnv* ev,
+ morkRowObject** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongRowObject(morkRowObject* me, morkEnv* ev,
+ morkRowObject** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKROWOBJECT_ */
diff --git a/comm/mailnews/db/mork/morkRowSpace.cpp b/comm/mailnews/db/mork/morkRowSpace.cpp
new file mode 100644
index 0000000000..4ee9d5aead
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowSpace.cpp
@@ -0,0 +1,540 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKATOMMAP_
+# include "morkAtomMap.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkRowSpace::CloseMorkNode(
+ morkEnv* ev) // CloseRowSpace() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseRowSpace(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkRowSpace::~morkRowSpace() // assert CloseRowSpace() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkRowSpace::morkRowSpace(morkEnv* ev, const morkUsage& inUsage,
+ mork_scope inScope, morkStore* ioStore,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkSpace(ev, inUsage, inScope, ioStore, ioHeap, ioSlotHeap),
+ mRowSpace_SlotHeap(ioSlotHeap),
+ mRowSpace_Rows(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioSlotHeap,
+ morkRowSpace_kStartRowMapSlotCount),
+ mRowSpace_Tables(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioSlotHeap),
+ mRowSpace_NextTableId(1),
+ mRowSpace_NextRowId(1)
+
+ ,
+ mRowSpace_IndexCount(0) {
+ morkAtomRowMap** cache = mRowSpace_IndexCache;
+ morkAtomRowMap** cacheEnd = cache + morkRowSpace_kPrimeCacheSize;
+ while (cache < cacheEnd)
+ *cache++ = 0; // put nil into every slot of cache table
+
+ if (ev->Good()) {
+ if (ioSlotHeap) {
+ mNode_Derived = morkDerived_kRowSpace;
+
+ // the morkSpace base constructor handles any dirty propagation
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkRowSpace::CloseRowSpace(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkAtomRowMap** cache = mRowSpace_IndexCache;
+ morkAtomRowMap** cacheEnd = cache + morkRowSpace_kPrimeCacheSize;
+ --cache; // prepare for preincrement:
+ while (++cache < cacheEnd) {
+ if (*cache) morkAtomRowMap::SlotStrongAtomRowMap(0, ev, cache);
+ }
+
+ mRowSpace_Tables.CloseMorkNode(ev);
+
+ morkStore* store = mSpace_Store;
+ if (store) this->CutAllRows(ev, &store->mStore_Pool);
+
+ mRowSpace_Rows.CloseMorkNode(ev);
+ this->CloseSpace(ev);
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkRowSpace::NonRowSpaceTypeError(morkEnv* ev) {
+ ev->NewError("non morkRowSpace");
+}
+
+/*static*/ void morkRowSpace::ZeroKindError(morkEnv* ev) {
+ ev->NewError("zero table kind");
+}
+
+/*static*/ void morkRowSpace::ZeroScopeError(morkEnv* ev) {
+ ev->NewError("zero row scope");
+}
+
+/*static*/ void morkRowSpace::ZeroTidError(morkEnv* ev) {
+ ev->NewError("zero table ID");
+}
+
+/*static*/ void morkRowSpace::MinusOneRidError(morkEnv* ev) {
+ ev->NewError("row ID is -1");
+}
+
+///*static*/ void
+// morkRowSpace::ExpectAutoIdOnlyError(morkEnv* ev)
+//{
+// ev->NewError("zero row ID");
+//}
+
+///*static*/ void
+// morkRowSpace::ExpectAutoIdNeverError(morkEnv* ev)
+//{
+//}
+
+mork_num morkRowSpace::CutAllRows(morkEnv* ev, morkPool* ioPool) {
+ if (this->IsRowSpaceClean()) this->MaybeDirtyStoreAndSpace();
+
+#ifdef MORK_ENABLE_ZONE_ARENAS
+ MORK_USED_2(ev, ioPool);
+ return 0;
+#else /*MORK_ENABLE_ZONE_ARENAS*/
+ mork_num outSlots = mRowSpace_Rows.MapFill();
+ morkZone* zone = &mSpace_Store->mStore_Zone;
+ morkRow* r = 0; // old key row in the map
+ mork_change* c = 0;
+
+# ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMapIter i(ev, &mRowSpace_Rows);
+# else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMapIter i(ev, &mRowSpace_Rows);
+# endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ for (c = i.FirstRow(ev, &r); c && ev->Good(); c = i.NextRow(ev, &r)) {
+ if (r) {
+ if (r->IsRow()) {
+ if (r->mRow_Object) {
+ morkRowObject::SlotWeakRowObject((morkRowObject*)0, ev,
+ &r->mRow_Object);
+ }
+ ioPool->ZapRow(ev, r, zone);
+ } else
+ r->NonRowTypeWarning(ev);
+ } else
+ ev->NilPointerError();
+
+# ifdef MORK_ENABLE_PROBE_MAPS
+ // cut nothing from the map
+# else /*MORK_ENABLE_PROBE_MAPS*/
+ i.CutHereRow(ev, /*key*/ (morkRow**)0);
+# endif /*MORK_ENABLE_PROBE_MAPS*/
+ }
+
+ return outSlots;
+#endif /*MORK_ENABLE_ZONE_ARENAS*/
+}
+
+morkTable* morkRowSpace::FindTableByKind(morkEnv* ev, mork_kind inTableKind) {
+ if (inTableKind) {
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+
+ morkTableMapIter i(ev, &mRowSpace_Tables);
+ morkTable* table = i.FirstTable(ev);
+ for (; table && ev->Good(); table = i.NextTable(ev))
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ mork_tid* key = 0; // nil pointer to suppress key access
+ morkTable* table = 0; // old table in the map
+
+ mork_change* c = 0;
+ morkTableMapIter i(ev, &mRowSpace_Tables);
+ for (c = i.FirstTable(ev, key, &table); c && ev->Good();
+ c = i.NextTable(ev, key, &table))
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+ {
+ if (table->mTable_Kind == inTableKind) return table;
+ }
+ } else
+ this->ZeroKindError(ev);
+
+ return (morkTable*)0;
+}
+
+morkTable* morkRowSpace::NewTableWithTid(
+ morkEnv* ev, mork_tid inTid, mork_kind inTableKind,
+ const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
+{
+ morkTable* outTable = 0;
+ morkStore* store = mSpace_Store;
+
+ if (inTableKind && store) {
+ mdb_bool mustBeUnique = morkBool_kFalse;
+ nsIMdbHeap* heap = store->mPort_Heap;
+ morkTable* table = new (*heap, ev)
+ morkTable(ev, morkUsage::kHeap, heap, store, heap, this,
+ inOptionalMetaRowOid, inTid, inTableKind, mustBeUnique);
+ if (table) {
+ if (mRowSpace_Tables.AddTable(ev, table)) {
+ outTable = table;
+ if (mRowSpace_NextTableId <= inTid) mRowSpace_NextTableId = inTid + 1;
+ }
+
+ if (this->IsRowSpaceClean() && store->mStore_CanDirty)
+ this->MaybeDirtyStoreAndSpace(); // morkTable does already
+ }
+ } else if (store)
+ this->ZeroKindError(ev);
+ else
+ this->NilSpaceStoreError(ev);
+
+ return outTable;
+}
+
+morkTable* morkRowSpace::NewTable(
+ morkEnv* ev, mork_kind inTableKind, mdb_bool inMustBeUnique,
+ const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
+{
+ morkTable* outTable = 0;
+ morkStore* store = mSpace_Store;
+
+ if (inTableKind && store) {
+ if (inMustBeUnique) // need to look for existing table first?
+ outTable = this->FindTableByKind(ev, inTableKind);
+
+ if (!outTable && ev->Good()) {
+ mork_tid id = this->MakeNewTableId(ev);
+ if (id) {
+ nsIMdbHeap* heap = mSpace_Store->mPort_Heap;
+ morkTable* table = new (*heap, ev)
+ morkTable(ev, morkUsage::kHeap, heap, mSpace_Store, heap, this,
+ inOptionalMetaRowOid, id, inTableKind, inMustBeUnique);
+ if (table) {
+ if (mRowSpace_Tables.AddTable(ev, table))
+ outTable = table;
+ else
+ table->Release();
+
+ if (this->IsRowSpaceClean() && store->mStore_CanDirty)
+ this->MaybeDirtyStoreAndSpace(); // morkTable does already
+ }
+ }
+ }
+ } else if (store)
+ this->ZeroKindError(ev);
+ else
+ this->NilSpaceStoreError(ev);
+
+ return outTable;
+}
+
+mork_tid morkRowSpace::MakeNewTableId(morkEnv* ev) {
+ mork_tid outTid = 0;
+ mork_tid id = mRowSpace_NextTableId;
+ mork_num count = 9; // try up to eight times
+
+ while (!outTid && --count) // still trying to find an unused table ID?
+ {
+ if (!mRowSpace_Tables.GetTable(ev, id))
+ outTid = id;
+ else {
+ MORK_ASSERT(morkBool_kFalse); // alert developer about ID problems
+ ++id;
+ }
+ }
+
+ mRowSpace_NextTableId = id + 1;
+ return outTid;
+}
+
+#define MAX_AUTO_ID (morkRow_kMinusOneRid - 2)
+mork_rid morkRowSpace::MakeNewRowId(morkEnv* ev) {
+ mork_rid outRid = 0;
+ mork_rid id = mRowSpace_NextRowId;
+ mork_num count = 9; // try up to eight times
+ mdbOid oid;
+ oid.mOid_Scope = this->SpaceScope();
+
+ // still trying to find an unused row ID?
+ while (!outRid && --count && id <= MAX_AUTO_ID) {
+ oid.mOid_Id = id;
+ if (!mRowSpace_Rows.GetOid(ev, &oid))
+ outRid = id;
+ else {
+ MORK_ASSERT(morkBool_kFalse); // alert developer about ID problems
+ ++id;
+ }
+ }
+
+ if (id < MAX_AUTO_ID) mRowSpace_NextRowId = id + 1;
+ return outRid;
+}
+
+morkAtomRowMap* morkRowSpace::make_index(morkEnv* ev, mork_column inCol) {
+ morkAtomRowMap* outMap = 0;
+ nsIMdbHeap* heap = mRowSpace_SlotHeap;
+ if (heap) // have expected heap for allocations?
+ {
+ morkAtomRowMap* map =
+ new (*heap, ev) morkAtomRowMap(ev, morkUsage::kHeap, heap, heap, inCol);
+
+ if (map) // able to create new map index?
+ {
+ if (ev->Good()) // no errors during construction?
+ {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMapIter i(ev, &mRowSpace_Rows);
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMapIter i(ev, &mRowSpace_Rows);
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ mork_change* c = 0;
+ morkRow* row = 0;
+ mork_aid aidKey = 0;
+
+ for (c = i.FirstRow(ev, &row); c && ev->Good();
+ c = i.NextRow(ev, &row)) // another row in space?
+ {
+ aidKey = row->GetCellAtomAid(ev, inCol);
+ if (aidKey) // row has indexed attribute?
+ map->AddAid(ev, aidKey, row); // include in map
+ }
+ }
+ if (ev->Good()) // no errors constructing index?
+ outMap = map; // return from function
+ else
+ map->CutStrongRef(ev); // discard map on error
+ }
+ } else
+ ev->NilPointerError();
+
+ return outMap;
+}
+
+morkAtomRowMap* morkRowSpace::ForceMap(morkEnv* ev, mork_column inCol) {
+ morkAtomRowMap* outMap = this->FindMap(ev, inCol);
+
+ if (!outMap && ev->Good()) // no such existing index?
+ {
+ if (mRowSpace_IndexCount < morkRowSpace_kMaxIndexCount) {
+ morkAtomRowMap* map = this->make_index(ev, inCol);
+ if (map) // created a new index for col?
+ {
+ mork_count wrap = 0; // count times wrap-around occurs
+ morkAtomRowMap** slot = mRowSpace_IndexCache; // table
+ morkAtomRowMap** end = slot + morkRowSpace_kPrimeCacheSize;
+ slot += (inCol % morkRowSpace_kPrimeCacheSize); // hash
+ while (*slot) // empty slot not yet found?
+ {
+ if (++slot >= end) // wrap around?
+ {
+ slot = mRowSpace_IndexCache; // back to table start
+ if (++wrap > 1) // wrapped more than once?
+ {
+ ev->NewError("no free cache slots"); // disaster
+ break; // end while loop
+ }
+ }
+ }
+ if (ev->Good()) // everything went just fine?
+ {
+ ++mRowSpace_IndexCount; // note another new map
+ *slot = map; // install map in the hash table
+ outMap = map; // return the new map from function
+ } else
+ map->CutStrongRef(ev); // discard map on error
+ }
+ } else
+ ev->NewError("too many indexes"); // why so many indexes?
+ }
+ return outMap;
+}
+
+morkAtomRowMap* morkRowSpace::FindMap(morkEnv* ev, mork_column inCol) {
+ if (mRowSpace_IndexCount && ev->Good()) {
+ mork_count wrap = 0; // count times wrap-around occurs
+ morkAtomRowMap** slot = mRowSpace_IndexCache; // table
+ morkAtomRowMap** end = slot + morkRowSpace_kPrimeCacheSize;
+ slot += (inCol % morkRowSpace_kPrimeCacheSize); // hash
+ morkAtomRowMap* map = *slot;
+ while (map) // another used slot to examine?
+ {
+ if (inCol == map->mAtomRowMap_IndexColumn) // found col?
+ return map;
+ if (++slot >= end) // wrap around?
+ {
+ slot = mRowSpace_IndexCache;
+ if (++wrap > 1) // wrapped more than once?
+ return (morkAtomRowMap*)0; // stop searching
+ }
+ map = *slot;
+ }
+ }
+ return (morkAtomRowMap*)0;
+}
+
+morkRow* morkRowSpace::FindRow(morkEnv* ev, mork_column inCol,
+ const mdbYarn* inYarn) {
+ morkRow* outRow = 0;
+
+ // if yarn hasn't been atomized, there can't be a corresponding row,
+ // so pass in false to not create the row - should help history bloat
+ morkAtom* atom = mSpace_Store->YarnToAtom(ev, inYarn, false);
+ if (atom) // have or created an atom corresponding to input yarn?
+ {
+ mork_aid atomAid = atom->GetBookAtomAid();
+ if (atomAid) // atom has an identity for use in hash table?
+ {
+ morkAtomRowMap* map = this->ForceMap(ev, inCol);
+ if (map) // able to find or create index for col?
+ {
+ outRow = map->GetAid(ev, atomAid); // search for row
+ }
+ }
+ }
+
+ return outRow;
+}
+
+morkRow* morkRowSpace::NewRowWithOid(morkEnv* ev, const mdbOid* inOid) {
+ morkRow* outRow = mRowSpace_Rows.GetOid(ev, inOid);
+ MORK_ASSERT(outRow == 0);
+ if (!outRow && ev->Good()) {
+ morkStore* store = mSpace_Store;
+ if (store) {
+ morkPool* pool = this->GetSpaceStorePool();
+ morkRow* row = pool->NewRow(ev, &store->mStore_Zone);
+ if (row) {
+ row->InitRow(ev, inOid, this, /*length*/ 0, pool);
+
+ if (ev->Good() && mRowSpace_Rows.AddRow(ev, row)) {
+ outRow = row;
+ mork_rid rid = inOid->mOid_Id;
+ if (mRowSpace_NextRowId <= rid) mRowSpace_NextRowId = rid + 1;
+ } else
+ pool->ZapRow(ev, row, &store->mStore_Zone);
+
+ if (this->IsRowSpaceClean() && store->mStore_CanDirty)
+ this->MaybeDirtyStoreAndSpace(); // InitRow() does already
+ }
+ } else
+ this->NilSpaceStoreError(ev);
+ }
+ return outRow;
+}
+
+morkRow* morkRowSpace::NewRow(morkEnv* ev) {
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ mork_rid id = this->MakeNewRowId(ev);
+ if (id) {
+ morkStore* store = mSpace_Store;
+ if (store) {
+ mdbOid oid;
+ oid.mOid_Scope = this->SpaceScope();
+ oid.mOid_Id = id;
+ morkPool* pool = this->GetSpaceStorePool();
+ morkRow* row = pool->NewRow(ev, &store->mStore_Zone);
+ if (row) {
+ row->InitRow(ev, &oid, this, /*length*/ 0, pool);
+
+ if (ev->Good() && mRowSpace_Rows.AddRow(ev, row))
+ outRow = row;
+ else
+ pool->ZapRow(ev, row, &store->mStore_Zone);
+
+ if (this->IsRowSpaceClean() && store->mStore_CanDirty)
+ this->MaybeDirtyStoreAndSpace(); // InitRow() does already
+ }
+ } else
+ this->NilSpaceStoreError(ev);
+ }
+ }
+ return outRow;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkRowSpaceMap::~morkRowSpaceMap() {}
+
+morkRowSpaceMap::morkRowSpaceMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+ : morkNodeMap(ev, inUsage, ioHeap, ioSlotHeap) {
+ if (ev->Good()) mNode_Derived = morkDerived_kRowSpaceMap;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkRowSpace.h b/comm/mailnews/db/mork/morkRowSpace.h
new file mode 100644
index 0000000000..81386576a1
--- /dev/null
+++ b/comm/mailnews/db/mork/morkRowSpace.h
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKROWSPACE_
+#define _MORKROWSPACE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKARRAY_
+# include "morkArray.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kRowSpace /*i*/ 0x7253 /* ascii 'rS' */
+
+#define morkRowSpace_kStartRowMapSlotCount 11
+
+#define morkRowSpace_kMaxIndexCount 8 /* no more indexes than this */
+#define morkRowSpace_kPrimeCacheSize 17 /* should be prime number */
+
+class morkAtomRowMap;
+
+/*| morkRowSpace:
+|*/
+class morkRowSpace : public morkSpace { //
+
+ // public: // slots inherited from morkSpace (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkStore* mSpace_Store; // weak ref to containing store
+
+ // mork_bool mSpace_DoAutoIDs; // whether db should assign member IDs
+ // mork_bool mSpace_HaveDoneAutoIDs; // whether actually auto assigned IDs
+ // mork_u1 mSpace_Pad[ 2 ]; // pad to u4 alignment
+
+ public: // state is public because the entire Mork system is private
+ nsIMdbHeap* mRowSpace_SlotHeap;
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMap mRowSpace_Rows; // hash table of morkRow instances
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMap mRowSpace_Rows; // hash table of morkRow instances
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ morkTableMap mRowSpace_Tables; // all the tables in this row scope
+
+ mork_tid mRowSpace_NextTableId; // for auto-assigning table IDs
+ mork_rid mRowSpace_NextRowId; // for auto-assigning row IDs
+
+ mork_count mRowSpace_IndexCount; // if nonzero, row indexes exist
+
+ // every nonzero slot in IndexCache is a strong ref to a morkAtomRowMap:
+ morkAtomRowMap* mRowSpace_IndexCache[morkRowSpace_kPrimeCacheSize];
+
+ morkDeque mRowSpace_TablesByPriority[morkPriority_kCount];
+
+ public: // more specific dirty methods for row space:
+ void SetRowSpaceDirty() { this->SetNodeDirty(); }
+ void SetRowSpaceClean() { this->SetNodeClean(); }
+
+ mork_bool IsRowSpaceClean() const { return this->IsNodeClean(); }
+ mork_bool IsRowSpaceDirty() const { return this->IsNodeDirty(); }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseRowSpace() only if open
+ virtual ~morkRowSpace(); // assert that CloseRowSpace() executed earlier
+
+ public: // morkMap construction & destruction
+ morkRowSpace(morkEnv* ev, const morkUsage& inUsage, mork_scope inScope,
+ morkStore* ioStore, nsIMdbHeap* ioNodeHeap,
+ nsIMdbHeap* ioSlotHeap);
+ void CloseRowSpace(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsRowSpace() const {
+ return IsNode() && mNode_Derived == morkDerived_kRowSpace;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonRowSpaceTypeError(morkEnv* ev);
+ static void ZeroScopeError(morkEnv* ev);
+ static void ZeroKindError(morkEnv* ev);
+ static void ZeroTidError(morkEnv* ev);
+ static void MinusOneRidError(morkEnv* ev);
+
+ // static void ExpectAutoIdOnlyError(morkEnv* ev);
+ // static void ExpectAutoIdNeverError(morkEnv* ev);
+
+ public: // other space methods
+ mork_num CutAllRows(morkEnv* ev, morkPool* ioPool);
+ // CutAllRows() puts all rows and cells back into the pool.
+
+ morkTable* NewTable(morkEnv* ev, mork_kind inTableKind,
+ mdb_bool inMustBeUnique,
+ const mdbOid* inOptionalMetaRowOid);
+
+ morkTable* NewTableWithTid(morkEnv* ev, mork_tid inTid, mork_kind inTableKind,
+ const mdbOid* inOptionalMetaRowOid);
+
+ morkTable* FindTableByKind(morkEnv* ev, mork_kind inTableKind);
+ morkTable* FindTableByTid(morkEnv* ev, mork_tid inTid) {
+ return mRowSpace_Tables.GetTable(ev, inTid);
+ }
+
+ mork_tid MakeNewTableId(morkEnv* ev);
+ mork_rid MakeNewRowId(morkEnv* ev);
+
+ // morkRow* FindRowByRid(morkEnv* ev, mork_rid inRid)
+ // { return (morkRow*) mRowSpace_Rows.GetRow(ev, inRid); }
+
+ morkRow* NewRowWithOid(morkEnv* ev, const mdbOid* inOid);
+ morkRow* NewRow(morkEnv* ev);
+
+ morkRow* FindRow(morkEnv* ev, mork_column inColumn, const mdbYarn* inYarn);
+
+ morkAtomRowMap* ForceMap(morkEnv* ev, mork_column inColumn);
+ morkAtomRowMap* FindMap(morkEnv* ev, mork_column inColumn);
+
+ protected: // internal utilities
+ morkAtomRowMap* make_index(morkEnv* ev, mork_column inColumn);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakRowSpace(morkRowSpace* me, morkEnv* ev,
+ morkRowSpace** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongRowSpace(morkRowSpace* me, morkEnv* ev,
+ morkRowSpace** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kRowSpaceMap /*i*/ 0x725A /* ascii 'rZ' */
+
+/*| morkRowSpaceMap: maps mork_scope -> morkRowSpace
+|*/
+class morkRowSpaceMap : public morkNodeMap { // for mapping tokens to tables
+
+ public:
+ virtual ~morkRowSpaceMap();
+ morkRowSpaceMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+
+ public: // other map methods
+ mork_bool AddRowSpace(morkEnv* ev, morkRowSpace* ioRowSpace) {
+ return this->AddNode(ev, ioRowSpace->SpaceScope(), ioRowSpace);
+ }
+ // the AddRowSpace() boolean return equals ev->Good().
+
+ mork_bool CutRowSpace(morkEnv* ev, mork_scope inScope) {
+ return this->CutNode(ev, inScope);
+ }
+ // The CutRowSpace() boolean return indicates whether removal happened.
+
+ morkRowSpace* GetRowSpace(morkEnv* ev, mork_scope inScope) {
+ return (morkRowSpace*)this->GetNode(ev, inScope);
+ }
+ // Note the returned space does NOT have an increase in refcount for this.
+
+ mork_num CutAllRowSpaces(morkEnv* ev) { return this->CutAllNodes(ev); }
+ // CutAllRowSpaces() releases all the referenced table values.
+};
+
+class morkRowSpaceMapIter : public morkMapIter { // typesafe wrapper class
+
+ public:
+ morkRowSpaceMapIter(morkEnv* ev, morkRowSpaceMap* ioMap)
+ : morkMapIter(ev, ioMap) {}
+
+ morkRowSpaceMapIter() : morkMapIter() {}
+ void InitRowSpaceMapIter(morkEnv* ev, morkRowSpaceMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstRowSpace(morkEnv* ev, mork_scope* outScope,
+ morkRowSpace** outRowSpace) {
+ return this->First(ev, outScope, outRowSpace);
+ }
+
+ mork_change* NextRowSpace(morkEnv* ev, mork_scope* outScope,
+ morkRowSpace** outRowSpace) {
+ return this->Next(ev, outScope, outRowSpace);
+ }
+
+ mork_change* HereRowSpace(morkEnv* ev, mork_scope* outScope,
+ morkRowSpace** outRowSpace) {
+ return this->Here(ev, outScope, outRowSpace);
+ }
+
+ mork_change* CutHereRowSpace(morkEnv* ev, mork_scope* outScope,
+ morkRowSpace** outRowSpace) {
+ return this->CutHere(ev, outScope, outRowSpace);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKROWSPACE_ */
diff --git a/comm/mailnews/db/mork/morkSearchRowCursor.cpp b/comm/mailnews/db/mork/morkSearchRowCursor.cpp
new file mode 100644
index 0000000000..f7cc4c56d6
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSearchRowCursor.cpp
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKSEARCHROWCURSOR_
+# include "morkSearchRowCursor.h"
+#endif
+
+#ifndef _MORKUNIQROWCURSOR_
+# include "morkUniqRowCursor.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkSearchRowCursor::CloseMorkNode(
+ morkEnv* ev) // CloseSearchRowCursor() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseSearchRowCursor(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkSearchRowCursor::~morkSearchRowCursor() // CloseSearchRowCursor() executed
+ // earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkSearchRowCursor::morkSearchRowCursor(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkTable* ioTable,
+ mork_pos inRowPos)
+ : morkTableRowCursor(ev, inUsage, ioHeap, ioTable, inRowPos)
+// , mSortingRowCursor_Sorting( 0 )
+{
+ if (ev->Good()) {
+ if (ioTable) {
+ // morkSorting::SlotWeakSorting(ioSorting, ev,
+ // &mSortingRowCursor_Sorting);
+ if (ev->Good()) {
+ // mNode_Derived = morkDerived_kTableRowCursor;
+ // mNode_Derived must stay equal to kTableRowCursor
+ }
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkSearchRowCursor::CloseSearchRowCursor(
+ morkEnv* ev) {
+ if (this->IsNode()) {
+ // morkSorting::SlotWeakSorting((morkSorting*) 0, ev,
+ // &mSortingRowCursor_Sorting);
+ this->CloseTableRowCursor(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkSearchRowCursor::NonSearchRowCursorTypeError(morkEnv* ev) {
+ ev->NewError("non morkSearchRowCursor");
+}
+
+morkUniqRowCursor* morkSearchRowCursor::MakeUniqCursor(morkEnv* ev) {
+ morkUniqRowCursor* outCursor = 0;
+
+ return outCursor;
+}
+
+#if 0
+orkinTableRowCursor*
+morkSearchRowCursor::AcquireUniqueRowCursorHandle(morkEnv* ev)
+{
+ orkinTableRowCursor* outCursor = 0;
+
+ morkUniqRowCursor* uniqCursor = this->MakeUniqCursor(ev);
+ if ( uniqCursor )
+ {
+ outCursor = uniqCursor->AcquireTableRowCursorHandle(ev);
+ uniqCursor->CutStrongRef(ev);
+ }
+ return outCursor;
+}
+#endif
+mork_bool morkSearchRowCursor::CanHaveDupRowMembers(morkEnv* ev) {
+ return morkBool_kTrue; // true is correct
+}
+
+mork_count morkSearchRowCursor::GetMemberCount(morkEnv* ev) {
+ morkTable* table = mTableRowCursor_Table;
+ if (table)
+ return table->mTable_RowArray.mArray_Fill;
+ else
+ return 0;
+}
+
+morkRow* morkSearchRowCursor::NextRow(morkEnv* ev, mdbOid* outOid,
+ mdb_pos* outPos) {
+ morkRow* outRow = 0;
+ mork_pos pos = -1;
+
+ morkTable* table = mTableRowCursor_Table;
+ if (table) {
+ } else
+ ev->NilPointerError();
+
+ *outPos = pos;
+ return outRow;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkSearchRowCursor.h b/comm/mailnews/db/mork/morkSearchRowCursor.h
new file mode 100644
index 0000000000..c267f35bf6
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSearchRowCursor.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKSEARCHROWCURSOR_
+#define _MORKSEARCHROWCURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKTABLEROWCURSOR_
+# include "morkTableRowCursor.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class morkUniqRowCursor;
+class orkinTableRowCursor;
+// #define morkDerived_kSearchRowCursor /*i*/ 0x7352 /* ascii 'sR' */
+
+class morkSearchRowCursor : public morkTableRowCursor { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkFactory* mObject_Factory; // weak ref to suite factory
+
+ // mork_seed mCursor_Seed;
+ // mork_pos mCursor_Pos;
+ // mork_bool mCursor_DoFailOnSeedOutOfSync;
+ // mork_u1 mCursor_Pad[ 3 ]; // explicitly pad to u4 alignment
+
+ // morkTable* mTableRowCursor_Table; // weak ref to table
+
+ // { ===== begin morkNode interface =====
+ public:
+ virtual void CloseMorkNode(morkEnv* ev); // CloseSearchRowCursor()
+ virtual ~morkSearchRowCursor(); // assert that close executed earlier
+
+ public: // morkSearchRowCursor construction & destruction
+ morkSearchRowCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkTable* ioTable, mork_pos inRowPos);
+ void CloseSearchRowCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkSearchRowCursor(const morkSearchRowCursor& other);
+ morkSearchRowCursor& operator=(const morkSearchRowCursor& other);
+
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonSearchRowCursorTypeError(morkEnv* ev);
+
+ public: // uniquify
+ morkUniqRowCursor* MakeUniqCursor(morkEnv* ev);
+
+ public: // other search row cursor methods
+ virtual mork_bool CanHaveDupRowMembers(morkEnv* ev);
+ virtual mork_count GetMemberCount(morkEnv* ev);
+
+#if 0
+ virtual orkinTableRowCursor* AcquireUniqueRowCursorHandle(morkEnv* ev);
+#endif
+
+ // virtual mdb_pos NextRowOid(morkEnv* ev, mdbOid* outOid);
+ virtual morkRow* NextRow(morkEnv* ev, mdbOid* outOid, mdb_pos* outPos);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakSearchRowCursor(morkSearchRowCursor* me, morkEnv* ev,
+ morkSearchRowCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongSearchRowCursor(morkSearchRowCursor* me, morkEnv* ev,
+ morkSearchRowCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKSEARCHROWCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkSink.cpp b/comm/mailnews/db/mork/morkSink.cpp
new file mode 100644
index 0000000000..daf1bc1b9c
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSink.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKSINK_
+# include "morkSink.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*virtual*/ morkSink::~morkSink() {
+ mSink_At = 0;
+ mSink_End = 0;
+}
+
+/*virtual*/ void morkSpool::FlushSink(
+ morkEnv* ev) // sync mSpool_Coil->mBuf_Fill
+{
+ morkCoil* coil = mSpool_Coil;
+ if (coil) {
+ mork_u1* body = (mork_u1*)coil->mBuf_Body;
+ if (body) {
+ mork_u1* at = mSink_At;
+ mork_u1* end = mSink_End;
+ if (at >= body && at <= end) // expected cursor order?
+ {
+ mork_fill fill = (mork_fill)(at - body); // current content size
+ if (fill <= coil->mBlob_Size)
+ coil->mBuf_Fill = fill;
+ else {
+ coil->BlobFillOverSizeError(ev);
+ coil->mBuf_Fill = coil->mBlob_Size; // make it safe
+ }
+ } else
+ this->BadSpoolCursorOrderError(ev);
+ } else
+ coil->NilBufBodyError(ev);
+ } else
+ this->NilSpoolCoilError(ev);
+}
+
+/*virtual*/ void morkSpool::SpillPutc(morkEnv* ev,
+ int c) // grow coil and write byte
+{
+ morkCoil* coil = mSpool_Coil;
+ if (coil) {
+ mork_u1* body = (mork_u1*)coil->mBuf_Body;
+ if (body) {
+ mork_u1* at = mSink_At;
+ mork_u1* end = mSink_End;
+ if (at >= body && at <= end) // expected cursor order?
+ {
+ mork_size size = coil->mBlob_Size;
+ mork_fill fill = (mork_fill)(at - body); // current content size
+ if (fill <= size) // less content than medium size?
+ {
+ coil->mBuf_Fill = fill;
+ if (at >= end) // need to grow the coil?
+ {
+ if (size > 2048) // grow slower over 2K?
+ size += 512;
+ else {
+ mork_size growth = (size * 4) / 3; // grow by 33%
+ if (growth < 64) // grow faster under (64 * 3)?
+ growth = 64;
+ size += growth;
+ }
+ if (coil->GrowCoil(ev, size)) // made coil bigger?
+ {
+ body = (mork_u1*)coil->mBuf_Body;
+ if (body) // have a coil body?
+ {
+ mSink_At = at = body + fill;
+ mSink_End = end = body + coil->mBlob_Size;
+ } else
+ coil->NilBufBodyError(ev);
+ }
+ }
+ if (ev->Good()) // seem ready to write byte c?
+ {
+ if (at < end) // morkSink::Putc() would succeed?
+ {
+ *at++ = (mork_u1)c;
+ mSink_At = at;
+ coil->mBuf_Fill = fill + 1;
+ } else
+ this->BadSpoolCursorOrderError(ev);
+ }
+ } else // fill exceeds size
+ {
+ coil->BlobFillOverSizeError(ev);
+ coil->mBuf_Fill = coil->mBlob_Size; // make it safe
+ }
+ } else
+ this->BadSpoolCursorOrderError(ev);
+ } else
+ coil->NilBufBodyError(ev);
+ } else
+ this->NilSpoolCoilError(ev);
+}
+
+// ````` ````` ````` ````` ````` ````` ````` `````
+// public: // public non-poly morkSink methods
+
+/*virtual*/
+morkSpool::~morkSpool()
+// Zero all slots to show this sink is disabled, but destroy no memory.
+// Note it is typically unnecessary to flush this coil sink, since all
+// content is written directly to the coil without any buffering.
+{
+ mSink_At = 0;
+ mSink_End = 0;
+ mSpool_Coil = 0;
+}
+
+morkSpool::morkSpool(morkEnv* ev, morkCoil* ioCoil)
+ // After installing the coil, calls Seek(ev, 0) to prepare for writing.
+ : morkSink(), mSpool_Coil(0) {
+ mSink_At = 0; // set correctly later in Seek()
+ mSink_End = 0; // set correctly later in Seek()
+
+ if (ev->Good()) {
+ if (ioCoil) {
+ mSpool_Coil = ioCoil;
+ this->Seek(ev, /*pos*/ 0);
+ } else
+ ev->NilPointerError();
+ }
+}
+
+// ----- All boolean return values below are equal to ev->Good(): -----
+
+/*static*/ void morkSpool::BadSpoolCursorOrderError(morkEnv* ev) {
+ ev->NewError("bad morkSpool cursor order");
+}
+
+/*static*/ void morkSpool::NilSpoolCoilError(morkEnv* ev) {
+ ev->NewError("nil mSpool_Coil");
+}
+
+mork_bool morkSpool::Seek(morkEnv* ev, mork_pos inPos)
+// Changed the current write position in coil's buffer to inPos.
+// For example, to start writing the coil from scratch, use inPos==0.
+{
+ morkCoil* coil = mSpool_Coil;
+ if (coil) {
+ mork_size minSize = (mork_size)(inPos + 64);
+
+ if (coil->mBlob_Size < minSize) coil->GrowCoil(ev, minSize);
+
+ if (ev->Good()) {
+ coil->mBuf_Fill = (mork_fill)inPos;
+ mork_u1* body = (mork_u1*)coil->mBuf_Body;
+ if (body) {
+ mSink_At = body + inPos;
+ mSink_End = body + coil->mBlob_Size;
+ } else
+ coil->NilBufBodyError(ev);
+ }
+ } else
+ this->NilSpoolCoilError(ev);
+
+ return ev->Good();
+}
+
+mork_bool morkSpool::Write(morkEnv* ev, const void* inBuf, mork_size inSize)
+// write inSize bytes of inBuf to current position inside coil's buffer
+{
+ // This method is conceptually very similar to morkStream::Write(),
+ // and this code was written while looking at that method for clues.
+
+ morkCoil* coil = mSpool_Coil;
+ if (coil) {
+ mork_u1* body = (mork_u1*)coil->mBuf_Body;
+ if (body) {
+ if (inBuf && inSize) // anything to write?
+ {
+ mork_u1* at = mSink_At;
+ mork_u1* end = mSink_End;
+ if (at >= body && at <= end) // expected cursor order?
+ {
+ // note coil->mBuf_Fill can be stale after morkSink::Putc():
+ mork_pos fill = at - body; // current content size
+ mork_num space = (mork_num)(end - at); // space left in body
+ if (space < inSize) // not enough to hold write?
+ {
+ mork_size minGrowth = space + 16;
+ mork_size minSize = coil->mBlob_Size + minGrowth;
+ if (coil->GrowCoil(ev, minSize)) {
+ body = (mork_u1*)coil->mBuf_Body;
+ if (body) {
+ mSink_At = at = body + fill;
+ mSink_End = end = body + coil->mBlob_Size;
+ space = (mork_num)(end - at); // space left in body
+ } else
+ coil->NilBufBodyError(ev);
+ }
+ }
+ if (ev->Good()) {
+ if (space >= inSize) // enough room to hold write?
+ {
+ MORK_MEMCPY(at, inBuf, inSize); // into body
+ mSink_At = at + inSize; // advance past written bytes
+ coil->mBuf_Fill = fill + inSize; // "flush" to fix fill
+ } else
+ ev->NewError("insufficient morkSpool space");
+ }
+ } else
+ this->BadSpoolCursorOrderError(ev);
+ }
+ } else
+ coil->NilBufBodyError(ev);
+ } else
+ this->NilSpoolCoilError(ev);
+
+ return ev->Good();
+}
+
+mork_bool morkSpool::PutString(morkEnv* ev, const char* inString)
+// call Write() with inBuf=inString and inSize=strlen(inString),
+// unless inString is null, in which case we then do nothing at all.
+{
+ if (inString) {
+ mork_size size = strlen(inString);
+ this->Write(ev, inString, size);
+ }
+ return ev->Good();
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkSink.h b/comm/mailnews/db/mork/morkSink.h
new file mode 100644
index 0000000000..9803b5c6da
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSink.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKSINK_
+#define _MORKSINK_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| morkSink is intended to be a very cheap buffered i/o sink which
+**| writes to bufs and other strings a single byte at a time. The
+**| basic idea is that writing a single byte has a very cheap average
+**| cost, because a polymophic function call need only occur when the
+**| space between At and End is exhausted. The rest of the time a
+**| very cheap inline method will write a byte, and then bump a pointer.
+**|
+**|| At: the current position in some sequence of bytes at which to
+**| write the next byte put into the sink. Presumably At points into
+**| the private storage of some space which is not yet filled (except
+**| when At reaches End, and the overflow must then spill). Note both
+**| At and End are zeroed in the destructor to help show that a sink
+**| is no longer usable; this is safe because At==End causes the case
+**| where SpillPutc() is called to handled an exhausted buffer space.
+**|
+**|| End: an address one byte past the last byte which can be written
+**| without needing to make a buffer larger than previously. When At
+**| and End are equal, this means there is no space to write a byte,
+**| and that some underlying buffer space must be grown before another
+**| byte can be written. Note At must always be less than or equal to
+**| End, and otherwise an important invariant has failed severely.
+**|
+**|| Buf: this original class slot has been commented out in the new
+**| and more abstract version of this sink class, but the general idea
+**| behind this slot should be explained to help design subclasses.
+**| Each subclass should provide space into which At and End can point,
+**| where End is beyond the last writable byte, and At is less than or
+**| equal to this point inside the same buffer. With some kinds of
+**| medium, such as writing to an instance of morkBlob, it is feasible
+**| to point directly into the final resting place for all the content
+**| written to the medium. Other mediums such as files, which write
+**| only through function calls, will typically need a local buffer
+**| to efficiently accumulate many bytes between such function calls.
+**|
+**|| FlushSink: this flush method should move any buffered content to
+**| its final destination. For example, for buffered writes to a
+**| string medium, where string methods are function calls and not just
+**| inline macros, it is faster to accumulate many bytes in a small
+**| local buffer and then move these en masse later in a single call.
+**|
+**|| SpillPutc: when At is greater than or equal to End, this means an
+**| underlying buffer has become full, so the buffer must be flushed
+**| before a new byte can be written. The intention is that SpillPutc()
+**| will be equivalent to calling FlushSink() followed by another call
+**| to Putc(), where the flush is expected to make At less then End once
+**| again. Except that FlushSink() need not make the underlying buffer
+**| any larger, and SpillPutc() typically must make room for more bytes.
+**| Note subclasses might want to guard against the case that both At
+**| and End are null, which happens when a sink is destroyed, which sets
+**| both these pointers to null as an indication the sink is disabled.
+|*/
+class morkSink {
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public sink virtual methods
+ virtual void FlushSink(morkEnv* ev) = 0;
+ virtual void SpillPutc(morkEnv* ev, int c) = 0;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // member variables
+ mork_u1* mSink_At; // pointer into mSink_Buf
+ mork_u1* mSink_End; // one byte past last content byte
+
+ // define morkSink_kBufSize 256 /* small enough to go on stack */
+
+ // mork_u1 mSink_Buf[ morkSink_kBufSize + 4 ];
+ // want plus one for any needed end null byte; use plus 4 for alignment
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkSink methods
+ virtual ~morkSink(); // zero both At and End; virtual for subclasses
+ morkSink() {} // does nothing; subclasses must set At and End suitably
+
+ void Putc(morkEnv* ev, int c) {
+ if (mSink_At < mSink_End)
+ *mSink_At++ = (mork_u1)c;
+ else
+ this->SpillPutc(ev, c);
+ }
+};
+
+/*| morkSpool: an output sink that efficiently writes individual bytes
+**| or entire byte sequences to a coil instance, which grows as needed by
+**| using the heap instance in the coil to grow the internal buffer.
+**|
+**|| Note we do not "own" the coil referenced by mSpool_Coil, and
+**| the lifetime of the coil is expected to equal or exceed that of this
+**| sink by some external means. Typical usage might involve keeping an
+**| instance of morkCoil and an instance of morkSpool in the same
+**| owning parent object, which uses the spool with the associated coil.
+|*/
+class morkSpool : public morkSink { // for buffered i/o to a morkCoil
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public sink virtual methods
+ // when morkSink::Putc() moves mSink_At, mSpool_Coil->mBuf_Fill is wrong:
+
+ virtual void FlushSink(morkEnv* ev); // sync mSpool_Coil->mBuf_Fill
+ virtual void SpillPutc(morkEnv* ev, int c); // grow coil and write byte
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // member variables
+ morkCoil* mSpool_Coil; // destination medium for written bytes
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkSink methods
+ static void BadSpoolCursorOrderError(morkEnv* ev);
+ static void NilSpoolCoilError(morkEnv* ev);
+
+ virtual ~morkSpool();
+ // Zero all slots to show this sink is disabled, but destroy no memory.
+ // Note it is typically unnecessary to flush this coil sink, since all
+ // content is written directly to the coil without any buffering.
+
+ morkSpool(morkEnv* ev, morkCoil* ioCoil);
+ // After installing the coil, calls Seek(ev, 0) to prepare for writing.
+
+ // ----- All boolean return values below are equal to ev->Good(): -----
+
+ mork_bool Seek(morkEnv* ev, mork_pos inPos);
+ // Changed the current write position in coil's buffer to inPos.
+ // For example, to start writing the coil from scratch, use inPos==0.
+
+ mork_bool Write(morkEnv* ev, const void* inBuf, mork_size inSize);
+ // write inSize bytes of inBuf to current position inside coil's buffer
+
+ mork_bool PutBuf(morkEnv* ev, const morkBuf& inBuffer) {
+ return this->Write(ev, inBuffer.mBuf_Body, inBuffer.mBuf_Fill);
+ }
+
+ mork_bool PutString(morkEnv* ev, const char* inString);
+ // call Write() with inBuf=inString and inSize=strlen(inString),
+ // unless inString is null, in which case we then do nothing at all.
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKSINK_ */
diff --git a/comm/mailnews/db/mork/morkSpace.cpp b/comm/mailnews/db/mork/morkSpace.cpp
new file mode 100644
index 0000000000..d3b1980089
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSpace.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKSPACE_
+# include "morkSpace.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkSpace::CloseMorkNode(
+ morkEnv* ev) // CloseSpace() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseSpace(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkSpace::~morkSpace() // assert CloseSpace() executed earlier
+{
+ MORK_ASSERT(SpaceScope() == 0);
+ MORK_ASSERT(mSpace_Store == 0);
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+// morkSpace::morkSpace(morkEnv* ev, const morkUsage& inUsage,
+// nsIMdbHeap* ioNodeHeap, const morkMapForm& inForm,
+// nsIMdbHeap* ioSlotHeap)
+//: morkNode(ev, inUsage, ioNodeHeap)
+//, mSpace_Map(ev, morkUsage::kMember, (nsIMdbHeap*) 0, ioSlotHeap)
+//{
+// ev->StubMethodOnlyError();
+//}
+
+/*public non-poly*/
+morkSpace::morkSpace(morkEnv* ev, const morkUsage& inUsage, mork_scope inScope,
+ morkStore* ioStore, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap)
+ : morkBead(ev, inUsage, ioHeap, inScope),
+ mSpace_Store(0),
+ mSpace_DoAutoIDs(morkBool_kFalse),
+ mSpace_HaveDoneAutoIDs(morkBool_kFalse),
+ mSpace_CanDirty(morkBool_kFalse) // only when store can be dirtied
+{
+ if (ev->Good()) {
+ if (ioStore && ioSlotHeap) {
+ morkStore::SlotWeakStore(ioStore, ev, &mSpace_Store);
+
+ mSpace_CanDirty = ioStore->mStore_CanDirty;
+ if (mSpace_CanDirty) // this new space dirties the store?
+ this->MaybeDirtyStoreAndSpace();
+
+ if (ev->Good()) mNode_Derived = morkDerived_kSpace;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkSpace::CloseSpace(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkStore::SlotWeakStore((morkStore*)0, ev, &mSpace_Store);
+ mBead_Color = 0; // this->CloseBead();
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkSpace::NonAsciiSpaceScopeName(morkEnv* ev) {
+ ev->NewError("SpaceScope() > 0x7F");
+}
+
+/*static*/ void morkSpace::NilSpaceStoreError(morkEnv* ev) {
+ ev->NewError("nil mSpace_Store");
+}
+
+morkPool* morkSpace::GetSpaceStorePool() const {
+ return &mSpace_Store->mStore_Pool;
+}
+
+mork_bool morkSpace::MaybeDirtyStoreAndSpace() {
+ morkStore* store = mSpace_Store;
+ if (store && store->mStore_CanDirty) {
+ store->SetStoreDirty();
+ mSpace_CanDirty = morkBool_kTrue;
+ }
+
+ if (mSpace_CanDirty) {
+ this->SetSpaceDirty();
+ return morkBool_kTrue;
+ }
+
+ return morkBool_kFalse;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkSpace.h b/comm/mailnews/db/mork/morkSpace.h
new file mode 100644
index 0000000000..baf74ee677
--- /dev/null
+++ b/comm/mailnews/db/mork/morkSpace.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKSPACE_
+#define _MORKSPACE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKBEAD_
+# include "morkBead.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkSpace_kInitialSpaceSlots /*i*/ 1024 /* default */
+#define morkDerived_kSpace /*i*/ 0x5370 /* ascii 'Sp' */
+
+/*| morkSpace:
+|*/
+class morkSpace : public morkBead { //
+
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+
+ public: // bead color setter & getter replace obsolete member mTable_Id:
+ mork_tid SpaceScope() const { return mBead_Color; }
+ void SetSpaceScope(mork_scope inScope) { mBead_Color = inScope; }
+
+ public: // state is public because the entire Mork system is private
+ morkStore* mSpace_Store; // weak ref to containing store
+
+ mork_bool mSpace_DoAutoIDs; // whether db should assign member IDs
+ mork_bool mSpace_HaveDoneAutoIDs; // whether actually auto assigned IDs
+ mork_bool mSpace_CanDirty; // changes imply the store becomes dirty?
+ mork_u1 mSpace_Pad; // pad to u4 alignment
+
+ public: // more specific dirty methods for space:
+ void SetSpaceDirty() { this->SetNodeDirty(); }
+ void SetSpaceClean() { this->SetNodeClean(); }
+
+ mork_bool IsSpaceClean() const { return this->IsNodeClean(); }
+ mork_bool IsSpaceDirty() const { return this->IsNodeDirty(); }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev); // CloseSpace() only if open
+ virtual ~morkSpace(); // assert that CloseSpace() executed earlier
+
+ public: // morkMap construction & destruction
+ // morkSpace(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioNodeHeap,
+ // const morkMapForm& inForm, nsIMdbHeap* ioSlotHeap);
+
+ morkSpace(morkEnv* ev, const morkUsage& inUsage, mork_scope inScope,
+ morkStore* ioStore, nsIMdbHeap* ioNodeHeap, nsIMdbHeap* ioSlotHeap);
+ void CloseSpace(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsSpace() const {
+ return IsNode() && mNode_Derived == morkDerived_kSpace;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // other space methods
+ mork_bool MaybeDirtyStoreAndSpace();
+
+ static void NonAsciiSpaceScopeName(morkEnv* ev);
+ static void NilSpaceStoreError(morkEnv* ev);
+
+ morkPool* GetSpaceStorePool() const;
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakSpace(morkSpace* me, morkEnv* ev, morkSpace** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongSpace(morkSpace* me, morkEnv* ev, morkSpace** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKSPACE_ */
diff --git a/comm/mailnews/db/mork/morkStore.cpp b/comm/mailnews/db/mork/morkStore.cpp
new file mode 100644
index 0000000000..9356864c35
--- /dev/null
+++ b/comm/mailnews/db/mork/morkStore.cpp
@@ -0,0 +1,1981 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKFACTORY_
+# include "morkFactory.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKTHUMB_
+# include "morkThumb.h"
+#endif
+// #ifndef _MORKFILE_
+// #include "morkFile.h"
+// #endif
+
+#ifndef _MORKBUILDER_
+# include "morkBuilder.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKSTREAM_
+# include "morkStream.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKPORTTABLECURSOR_
+# include "morkPortTableCursor.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKPARSER_
+# include "morkParser.h"
+#endif
+
+#include "nsCOMPtr.h"
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkStore::CloseMorkNode(
+ morkEnv* ev) // ClosePort() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseStore(ev);
+ this->MarkShut();
+ }
+}
+
+/*public non-poly*/ void morkStore::ClosePort(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkFactory::SlotWeakFactory((morkFactory*)0, ev, &mPort_Factory);
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mPort_Heap);
+ this->CloseObject(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+/*public virtual*/
+morkStore::~morkStore() // assert CloseStore() executed earlier
+{
+ if (IsOpenNode()) CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(this->IsShutNode());
+ MORK_ASSERT(mStore_File == 0);
+ MORK_ASSERT(mStore_InStream == 0);
+ MORK_ASSERT(mStore_OutStream == 0);
+ MORK_ASSERT(mStore_Builder == 0);
+ MORK_ASSERT(mStore_OidAtomSpace == 0);
+ MORK_ASSERT(mStore_GroundAtomSpace == 0);
+ MORK_ASSERT(mStore_GroundColumnSpace == 0);
+ MORK_ASSERT(mStore_RowSpaces.IsShutNode());
+ MORK_ASSERT(mStore_AtomSpaces.IsShutNode());
+ MORK_ASSERT(mStore_Pool.IsShutNode());
+}
+
+/*public non-poly*/
+morkStore::morkStore(
+ morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioNodeHeap, // the heap (if any) for this node instance
+ morkFactory* inFactory, // the factory for this
+ nsIMdbHeap* ioPortHeap // the heap to hold all content in the port
+ )
+ : morkObject(ev, inUsage, ioNodeHeap, morkColor_kNone, (morkHandle*)0),
+ mPort_Env(ev),
+ mPort_Factory(0),
+ mPort_Heap(0),
+ mStore_OidAtomSpace(0),
+ mStore_GroundAtomSpace(0),
+ mStore_GroundColumnSpace(0)
+
+ ,
+ mStore_File(0),
+ mStore_InStream(0),
+ mStore_Builder(0),
+ mStore_OutStream(0)
+
+ ,
+ mStore_RowSpaces(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioPortHeap),
+ mStore_AtomSpaces(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioPortHeap),
+ mStore_Zone(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioPortHeap),
+ mStore_Pool(ev, morkUsage::kMember, (nsIMdbHeap*)0, ioPortHeap)
+
+ ,
+ mStore_CommitGroupIdentity(0)
+
+ ,
+ mStore_FirstCommitGroupPos(0),
+ mStore_SecondCommitGroupPos(0)
+
+ // disable auto-assignment of atom IDs until someone knows it is okay:
+ ,
+ mStore_CanAutoAssignAtomIdentity(morkBool_kFalse),
+ mStore_CanDirty(morkBool_kFalse) // not until the store is open
+ ,
+ mStore_CanWriteIncremental(morkBool_kTrue) // always with few exceptions
+{
+ if (ev->Good()) {
+ if (inFactory && ioPortHeap) {
+ morkFactory::SlotWeakFactory(inFactory, ev, &mPort_Factory);
+ nsIMdbHeap_SlotStrongHeap(ioPortHeap, ev, &mPort_Heap);
+ if (ev->Good()) mNode_Derived = morkDerived_kPort;
+ } else
+ ev->NilPointerError();
+ }
+ if (ev->Good()) {
+ mNode_Derived = morkDerived_kStore;
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkStore, morkObject, nsIMdbStore)
+
+/*public non-poly*/ void morkStore::CloseStore(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ nsIMdbFile* file = mStore_File;
+ file->AddRef();
+
+ morkFactory::SlotWeakFactory((morkFactory*)0, ev, &mPort_Factory);
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mPort_Heap);
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mStore_OidAtomSpace);
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mStore_GroundAtomSpace);
+ morkAtomSpace::SlotStrongAtomSpace((morkAtomSpace*)0, ev,
+ &mStore_GroundColumnSpace);
+ mStore_RowSpaces.CloseMorkNode(ev);
+ mStore_AtomSpaces.CloseMorkNode(ev);
+ morkBuilder::SlotStrongBuilder((morkBuilder*)0, ev, &mStore_Builder);
+
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mStore_File);
+
+ file->Release();
+
+ morkStream::SlotStrongStream((morkStream*)0, ev, &mStore_InStream);
+ morkStream::SlotStrongStream((morkStream*)0, ev, &mStore_OutStream);
+
+ mStore_Pool.CloseMorkNode(ev);
+ mStore_Zone.CloseMorkNode(ev);
+ this->ClosePort(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+mork_bool morkStore::DoPreferLargeOverCompressCommit(morkEnv* ev)
+// true when mStore_CanWriteIncremental && store has file large enough
+{
+ nsIMdbFile* file = mStore_File;
+ if (file && mStore_CanWriteIncremental) {
+ mdb_pos fileEof = 0;
+ file->Eof(ev->AsMdbEnv(), &fileEof);
+ if (ev->Good() && fileEof > 128) return morkBool_kTrue;
+ }
+ return morkBool_kFalse;
+}
+
+mork_percent morkStore::PercentOfStoreWasted(morkEnv* ev) {
+ mork_percent outPercent = 0;
+ nsIMdbFile* file = mStore_File;
+
+ if (file) {
+ mork_pos firstPos = mStore_FirstCommitGroupPos;
+ mork_pos secondPos = mStore_SecondCommitGroupPos;
+ if (firstPos || secondPos) {
+ if (firstPos < 512 && secondPos > firstPos)
+ firstPos = secondPos; // better approximation of first commit
+
+ mork_pos fileLength = 0;
+ file->Eof(ev->AsMdbEnv(), &fileLength); // end of file
+ if (ev->Good() && fileLength > firstPos) {
+ mork_size groupContent = fileLength - firstPos;
+ outPercent = (groupContent * 100) / fileLength;
+ }
+ }
+ } else
+ this->NilStoreFileError(ev);
+
+ return outPercent;
+}
+
+void morkStore::SetStoreAndAllSpacesCanDirty(morkEnv* ev,
+ mork_bool inCanDirty) {
+ mStore_CanDirty = inCanDirty;
+
+ mork_change* c = 0;
+ mork_scope* key = 0; // ignore keys in maps
+
+ if (ev->Good()) {
+ morkAtomSpaceMapIter asi(ev, &mStore_AtomSpaces);
+
+ morkAtomSpace* atomSpace = 0; // old val node in the map
+
+ for (c = asi.FirstAtomSpace(ev, key, &atomSpace); c && ev->Good();
+ c = asi.NextAtomSpace(ev, key, &atomSpace)) {
+ if (atomSpace) {
+ if (atomSpace->IsAtomSpace())
+ atomSpace->mSpace_CanDirty = inCanDirty;
+ else
+ atomSpace->NonAtomSpaceTypeError(ev);
+ } else
+ ev->NilPointerError();
+ }
+ }
+
+ if (ev->Good()) {
+ morkRowSpaceMapIter rsi(ev, &mStore_RowSpaces);
+ morkRowSpace* rowSpace = 0; // old val node in the map
+
+ for (c = rsi.FirstRowSpace(ev, key, &rowSpace); c && ev->Good();
+ c = rsi.NextRowSpace(ev, key, &rowSpace)) {
+ if (rowSpace) {
+ if (rowSpace->IsRowSpace())
+ rowSpace->mSpace_CanDirty = inCanDirty;
+ else
+ rowSpace->NonRowSpaceTypeError(ev);
+ }
+ }
+ }
+}
+
+void morkStore::RenumberAllCollectableContent(morkEnv* ev) {
+ MORK_USED_1(ev);
+ // do nothing currently
+}
+
+nsIMdbStore* morkStore::AcquireStoreHandle(morkEnv* ev) { return this; }
+
+morkFarBookAtom* morkStore::StageAliasAsFarBookAtom(morkEnv* ev,
+ const morkMid* inMid,
+ morkAtomSpace* ioSpace,
+ mork_cscode inForm) {
+ if (inMid && inMid->mMid_Buf) {
+ const morkBuf* buf = inMid->mMid_Buf;
+ mork_size length = buf->mBuf_Fill;
+ if (length <= morkBookAtom_kMaxBodySize) {
+ mork_aid dummyAid = 1;
+ // mStore_BookAtom.InitMaxBookAtom(ev, *buf,
+ // inForm, ioSpace, dummyAid);
+
+ mStore_FarBookAtom.InitFarBookAtom(ev, *buf, inForm, ioSpace, dummyAid);
+ return &mStore_FarBookAtom;
+ }
+ } else
+ ev->NilPointerError();
+
+ return (morkFarBookAtom*)0;
+}
+
+morkFarBookAtom* morkStore::StageYarnAsFarBookAtom(morkEnv* ev,
+ const mdbYarn* inYarn,
+ morkAtomSpace* ioSpace) {
+ if (inYarn && inYarn->mYarn_Buf) {
+ mork_size length = inYarn->mYarn_Fill;
+ if (length <= morkBookAtom_kMaxBodySize) {
+ morkBuf buf(inYarn->mYarn_Buf, length);
+ mork_aid dummyAid = 1;
+ // mStore_BookAtom.InitMaxBookAtom(ev, buf,
+ // inYarn->mYarn_Form, ioSpace, dummyAid);
+ mStore_FarBookAtom.InitFarBookAtom(ev, buf, inYarn->mYarn_Form, ioSpace,
+ dummyAid);
+ return &mStore_FarBookAtom;
+ }
+ } else
+ ev->NilPointerError();
+
+ return (morkFarBookAtom*)0;
+}
+
+morkFarBookAtom* morkStore::StageStringAsFarBookAtom(morkEnv* ev,
+ const char* inString,
+ mork_cscode inForm,
+ morkAtomSpace* ioSpace) {
+ if (inString) {
+ mork_size length = strlen(inString);
+ if (length <= morkBookAtom_kMaxBodySize) {
+ morkBuf buf(inString, length);
+ mork_aid dummyAid = 1;
+ // mStore_BookAtom.InitMaxBookAtom(ev, buf, inForm, ioSpace, dummyAid);
+ mStore_FarBookAtom.InitFarBookAtom(ev, buf, inForm, ioSpace, dummyAid);
+ return &mStore_FarBookAtom;
+ }
+ } else
+ ev->NilPointerError();
+
+ return (morkFarBookAtom*)0;
+}
+
+morkAtomSpace* morkStore::LazyGetOidAtomSpace(morkEnv* ev) {
+ MORK_USED_1(ev);
+ if (!mStore_OidAtomSpace) {
+ }
+ return mStore_OidAtomSpace;
+}
+
+morkAtomSpace* morkStore::LazyGetGroundAtomSpace(morkEnv* ev) {
+ if (!mStore_GroundAtomSpace) {
+ mork_scope atomScope = morkStore_kValueSpaceScope;
+ nsIMdbHeap* heap = mPort_Heap;
+ morkAtomSpace* space = new (*heap, ev)
+ morkAtomSpace(ev, morkUsage::kHeap, atomScope, this, heap, heap);
+
+ if (space) // successful space creation?
+ {
+ this->MaybeDirtyStore();
+
+ mStore_GroundAtomSpace = space; // transfer strong ref to this slot
+ mStore_AtomSpaces.AddAtomSpace(ev, space);
+ }
+ }
+ return mStore_GroundAtomSpace;
+}
+
+morkAtomSpace* morkStore::LazyGetGroundColumnSpace(morkEnv* ev) {
+ if (!mStore_GroundColumnSpace) {
+ mork_scope atomScope = morkStore_kGroundColumnSpace;
+ nsIMdbHeap* heap = mPort_Heap;
+ morkAtomSpace* space = new (*heap, ev)
+ morkAtomSpace(ev, morkUsage::kHeap, atomScope, this, heap, heap);
+
+ if (space) // successful space creation?
+ {
+ this->MaybeDirtyStore();
+
+ mStore_GroundColumnSpace = space; // transfer strong ref to this slot
+ mStore_AtomSpaces.AddAtomSpace(ev, space);
+ }
+ }
+ return mStore_GroundColumnSpace;
+}
+
+morkStream* morkStore::LazyGetInStream(morkEnv* ev) {
+ if (!mStore_InStream) {
+ nsIMdbFile* file = mStore_File;
+ if (file) {
+ morkStream* stream = new (*mPort_Heap, ev)
+ morkStream(ev, morkUsage::kHeap, mPort_Heap, file,
+ morkStore_kStreamBufSize, /*frozen*/ morkBool_kTrue);
+ if (stream) {
+ this->MaybeDirtyStore();
+ mStore_InStream = stream; // transfer strong ref to this slot
+ }
+ } else
+ this->NilStoreFileError(ev);
+ }
+ return mStore_InStream;
+}
+
+morkStream* morkStore::LazyGetOutStream(morkEnv* ev) {
+ if (!mStore_OutStream) {
+ nsIMdbFile* file = mStore_File;
+ if (file) {
+ morkStream* stream = new (*mPort_Heap, ev)
+ morkStream(ev, morkUsage::kHeap, mPort_Heap, file,
+ morkStore_kStreamBufSize, /*frozen*/ morkBool_kFalse);
+ if (stream) {
+ this->MaybeDirtyStore();
+ mStore_InStream = stream; // transfer strong ref to this slot
+ }
+ } else
+ this->NilStoreFileError(ev);
+ }
+ return mStore_OutStream;
+}
+
+void morkStore::ForgetBuilder(morkEnv* ev) {
+ if (mStore_Builder)
+ morkBuilder::SlotStrongBuilder((morkBuilder*)0, ev, &mStore_Builder);
+ if (mStore_InStream)
+ morkStream::SlotStrongStream((morkStream*)0, ev, &mStore_InStream);
+}
+
+morkBuilder* morkStore::LazyGetBuilder(morkEnv* ev) {
+ if (!mStore_Builder) {
+ morkStream* stream = this->LazyGetInStream(ev);
+ if (stream) {
+ nsIMdbHeap* heap = mPort_Heap;
+ morkBuilder* builder = new (*heap, ev)
+ morkBuilder(ev, morkUsage::kHeap, heap, stream,
+ morkBuilder_kDefaultBytesPerParseSegment, heap, this);
+ if (builder) {
+ mStore_Builder = builder; // transfer strong ref to this slot
+ }
+ }
+ }
+ return mStore_Builder;
+}
+
+morkRowSpace* morkStore::LazyGetRowSpace(morkEnv* ev, mdb_scope inRowScope) {
+ morkRowSpace* outSpace = mStore_RowSpaces.GetRowSpace(ev, inRowScope);
+ if (!outSpace && ev->Good()) // try to make new space?
+ {
+ nsIMdbHeap* heap = mPort_Heap;
+ outSpace = new (*heap, ev)
+ morkRowSpace(ev, morkUsage::kHeap, inRowScope, this, heap, heap);
+
+ if (outSpace) // successful space creation?
+ {
+ this->MaybeDirtyStore();
+
+ // note adding to node map creates its own strong ref...
+ if (mStore_RowSpaces.AddRowSpace(ev, outSpace))
+ outSpace->CutStrongRef(ev); // ...so we can drop our strong ref
+ }
+ }
+ return outSpace;
+}
+
+morkAtomSpace* morkStore::LazyGetAtomSpace(morkEnv* ev, mdb_scope inAtomScope) {
+ morkAtomSpace* outSpace = mStore_AtomSpaces.GetAtomSpace(ev, inAtomScope);
+ if (!outSpace && ev->Good()) // try to make new space?
+ {
+ if (inAtomScope == morkStore_kValueSpaceScope)
+ outSpace = this->LazyGetGroundAtomSpace(ev);
+
+ else if (inAtomScope == morkStore_kGroundColumnSpace)
+ outSpace = this->LazyGetGroundColumnSpace(ev);
+ else {
+ nsIMdbHeap* heap = mPort_Heap;
+ outSpace = new (*heap, ev)
+ morkAtomSpace(ev, morkUsage::kHeap, inAtomScope, this, heap, heap);
+
+ if (outSpace) // successful space creation?
+ {
+ this->MaybeDirtyStore();
+
+ // note adding to node map creates its own strong ref...
+ if (mStore_AtomSpaces.AddAtomSpace(ev, outSpace))
+ outSpace->CutStrongRef(ev); // ...so we can drop our strong ref
+ }
+ }
+ }
+ return outSpace;
+}
+
+/*static*/ void morkStore::NonStoreTypeError(morkEnv* ev) {
+ ev->NewError("non morkStore");
+}
+
+/*static*/ void morkStore::NilStoreFileError(morkEnv* ev) {
+ ev->NewError("nil mStore_File");
+}
+
+/*static*/ void morkStore::CannotAutoAssignAtomIdentityError(morkEnv* ev) {
+ ev->NewError("false mStore_CanAutoAssignAtomIdentity");
+}
+
+mork_bool morkStore::OpenStoreFile(
+ morkEnv* ev, mork_bool inFrozen,
+ // const char* inFilePath,
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy) {
+ MORK_USED_2(inOpenPolicy, inFrozen);
+ nsIMdbFile_SlotStrongFile(ioFile, ev, &mStore_File);
+
+ // if ( ev->Good() )
+ // {
+ // morkFile* file = morkFile::OpenOldFile(ev, mPort_Heap,
+ // inFilePath, inFrozen);
+ // if ( ioFile )
+ // {
+ // if ( ev->Good() )
+ // morkFile::SlotStrongFile(file, ev, &mStore_File);
+ // else
+ // file->CutStrongRef(ev);
+ //
+ // }
+ // }
+ return ev->Good();
+}
+
+mork_bool morkStore::CreateStoreFile(
+ morkEnv* ev,
+ // const char* inFilePath,
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy) {
+ MORK_USED_1(inOpenPolicy);
+ nsIMdbFile_SlotStrongFile(ioFile, ev, &mStore_File);
+
+ return ev->Good();
+}
+
+morkAtom* morkStore::CopyAtom(morkEnv* ev, const morkAtom* inAtom)
+// copy inAtom (from some other store) over to this store
+{
+ morkAtom* outAtom = 0;
+ if (inAtom) {
+ mdbYarn yarn;
+ if (morkAtom::AliasYarn(inAtom, &yarn))
+ outAtom = this->YarnToAtom(ev, &yarn, true /* create */);
+ }
+ return outAtom;
+}
+
+morkAtom* morkStore::YarnToAtom(morkEnv* ev, const mdbYarn* inYarn,
+ bool createIfMissing /* = true */) {
+ morkAtom* outAtom = 0;
+ if (ev->Good()) {
+ morkAtomSpace* groundSpace = this->LazyGetGroundAtomSpace(ev);
+ if (groundSpace) {
+ morkFarBookAtom* keyAtom =
+ this->StageYarnAsFarBookAtom(ev, inYarn, groundSpace);
+
+ if (keyAtom) {
+ morkAtomBodyMap* map = &groundSpace->mAtomSpace_AtomBodies;
+ outAtom = map->GetAtom(ev, keyAtom);
+ if (!outAtom && createIfMissing) {
+ this->MaybeDirtyStore();
+ outAtom = groundSpace->MakeBookAtomCopy(ev, *keyAtom);
+ }
+ } else if (ev->Good()) {
+ morkBuf b(inYarn->mYarn_Buf, inYarn->mYarn_Fill);
+ morkZone* z = &mStore_Zone;
+ outAtom = mStore_Pool.NewAnonAtom(ev, b, inYarn->mYarn_Form, z);
+ }
+ }
+ }
+ return outAtom;
+}
+
+mork_bool morkStore::MidToOid(morkEnv* ev, const morkMid& inMid,
+ mdbOid* outOid) {
+ *outOid = inMid.mMid_Oid;
+ const morkBuf* buf = inMid.mMid_Buf;
+ if (buf && !outOid->mOid_Scope) {
+ if (buf->mBuf_Fill <= morkBookAtom_kMaxBodySize) {
+ if (buf->mBuf_Fill == 1) {
+ mork_u1* name = (mork_u1*)buf->mBuf_Body;
+ if (name) {
+ outOid->mOid_Scope = (mork_scope)*name;
+ return ev->Good();
+ }
+ }
+ morkAtomSpace* groundSpace = this->LazyGetGroundColumnSpace(ev);
+ if (groundSpace) {
+ mork_cscode form = 0; // default
+ mork_aid aid = 1; // dummy
+ mStore_FarBookAtom.InitFarBookAtom(ev, *buf, form, groundSpace, aid);
+ morkFarBookAtom* keyAtom = &mStore_FarBookAtom;
+ morkAtomBodyMap* map = &groundSpace->mAtomSpace_AtomBodies;
+ morkBookAtom* bookAtom = map->GetAtom(ev, keyAtom);
+ if (bookAtom)
+ outOid->mOid_Scope = bookAtom->mBookAtom_Id;
+ else {
+ this->MaybeDirtyStore();
+ bookAtom = groundSpace->MakeBookAtomCopy(ev, *keyAtom);
+ if (bookAtom) {
+ outOid->mOid_Scope = bookAtom->mBookAtom_Id;
+ bookAtom->MakeCellUseForever(ev);
+ }
+ }
+ }
+ }
+ }
+ return ev->Good();
+}
+
+morkRow* morkStore::MidToRow(morkEnv* ev, const morkMid& inMid) {
+ mdbOid tempOid;
+ this->MidToOid(ev, inMid, &tempOid);
+ return this->OidToRow(ev, &tempOid);
+}
+
+morkTable* morkStore::MidToTable(morkEnv* ev, const morkMid& inMid) {
+ mdbOid tempOid;
+ this->MidToOid(ev, inMid, &tempOid);
+ return this->OidToTable(ev, &tempOid, /*metarow*/ (mdbOid*)0);
+}
+
+mork_bool morkStore::MidToYarn(morkEnv* ev, const morkMid& inMid,
+ mdbYarn* outYarn) {
+ mdbOid tempOid;
+ this->MidToOid(ev, inMid, &tempOid);
+ return this->OidToYarn(ev, tempOid, outYarn);
+}
+
+mork_bool morkStore::OidToYarn(morkEnv* ev, const mdbOid& inOid,
+ mdbYarn* outYarn) {
+ morkBookAtom* atom = 0;
+
+ morkAtomSpace* atomSpace =
+ mStore_AtomSpaces.GetAtomSpace(ev, inOid.mOid_Scope);
+ if (atomSpace) {
+ morkAtomAidMap* map = &atomSpace->mAtomSpace_AtomAids;
+ atom = map->GetAid(ev, (mork_aid)inOid.mOid_Id);
+ }
+ morkAtom::GetYarn(atom, outYarn);
+
+ return ev->Good();
+}
+
+morkBookAtom* morkStore::MidToAtom(morkEnv* ev, const morkMid& inMid) {
+ morkBookAtom* outAtom = 0;
+ mdbOid oid;
+ if (this->MidToOid(ev, inMid, &oid)) {
+ morkAtomSpace* atomSpace =
+ mStore_AtomSpaces.GetAtomSpace(ev, oid.mOid_Scope);
+ if (atomSpace) {
+ morkAtomAidMap* map = &atomSpace->mAtomSpace_AtomAids;
+ outAtom = map->GetAid(ev, (mork_aid)oid.mOid_Id);
+ }
+ }
+ return outAtom;
+}
+
+/*static*/ void morkStore::SmallTokenToOneByteYarn(morkEnv* ev,
+ mdb_token inToken,
+ mdbYarn* outYarn) {
+ MORK_USED_1(ev);
+ if (outYarn->mYarn_Buf && outYarn->mYarn_Size) // any space in yarn at all?
+ {
+ mork_u1* buf = (mork_u1*)outYarn->mYarn_Buf; // for byte arithmetic
+ buf[0] = (mork_u1)inToken; // write the single byte
+ outYarn->mYarn_Fill = 1;
+ outYarn->mYarn_More = 0;
+ } else // just record we could not write the single byte
+ {
+ outYarn->mYarn_More = 1;
+ outYarn->mYarn_Fill = 0;
+ }
+}
+
+void morkStore::TokenToString(morkEnv* ev, mdb_token inToken,
+ mdbYarn* outTokenName) {
+ if (inToken > morkAtomSpace_kMaxSevenBitAid) {
+ morkBookAtom* atom = 0;
+ morkAtomSpace* space = mStore_GroundColumnSpace;
+ if (space) atom = space->mAtomSpace_AtomAids.GetAid(ev, (mork_aid)inToken);
+
+ morkAtom::GetYarn(atom, outTokenName);
+ } else // token is an "immediate" single byte string representation?
+ this->SmallTokenToOneByteYarn(ev, inToken, outTokenName);
+}
+
+// void
+// morkStore::SyncTokenIdChange(morkEnv* ev, const morkBookAtom* inAtom,
+// const mdbOid* inOid)
+// {
+// mork_token mStore_MorkNoneToken; // token for "mork:none" // fill=9
+// mork_column mStore_CharsetToken; // token for "charset" // fill=7
+// mork_column mStore_AtomScopeToken; // token for "atomScope" // fill=9
+// mork_column mStore_RowScopeToken; // token for "rowScope" // fill=8
+// mork_column mStore_TableScopeToken; // token for "tableScope" // fill=10
+// mork_column mStore_ColumnScopeToken; // token for "columnScope" // fill=11
+// mork_kind mStore_TableKindToken; // token for "tableKind" // fill=9
+// ---------------------ruler-for-token-length-above---123456789012
+//
+// if ( inOid->mOid_Scope == morkStore_kColumnSpaceScope &&
+// inAtom->IsWeeBook() )
+// {
+// const mork_u1* body = ((const morkWeeBookAtom*)
+// inAtom)->mWeeBookAtom_Body; mork_size size = inAtom->mAtom_Size;
+//
+// if ( size >= 7 && size <= 11 )
+// {
+// if ( size == 9 )
+// {
+// if ( *body == 'm' )
+// {
+// if ( MORK_MEMCMP(body, "mork:none", 9) == 0 )
+// mStore_MorkNoneToken = inAtom->mBookAtom_Id;
+// }
+// else if ( *body == 'a' )
+// {
+// if ( MORK_MEMCMP(body, "atomScope", 9) == 0 )
+// mStore_AtomScopeToken = inAtom->mBookAtom_Id;
+// }
+// else if ( *body == 't' )
+// {
+// if ( MORK_MEMCMP(body, "tableKind", 9) == 0 )
+// mStore_TableKindToken = inAtom->mBookAtom_Id;
+// }
+// }
+// else if ( size == 7 && *body == 'c' )
+// {
+// if ( MORK_MEMCMP(body, "charset", 7) == 0 )
+// mStore_CharsetToken = inAtom->mBookAtom_Id;
+// }
+// else if ( size == 8 && *body == 'r' )
+// {
+// if ( MORK_MEMCMP(body, "rowScope", 8) == 0 )
+// mStore_RowScopeToken = inAtom->mBookAtom_Id;
+// }
+// else if ( size == 10 && *body == 't' )
+// {
+// if ( MORK_MEMCMP(body, "tableScope", 10) == 0 )
+// mStore_TableScopeToken = inAtom->mBookAtom_Id;
+// }
+// else if ( size == 11 && *body == 'c' )
+// {
+// if ( MORK_MEMCMP(body, "columnScope", 11) == 0 )
+// mStore_ColumnScopeToken = inAtom->mBookAtom_Id;
+// }
+// }
+// }
+// }
+
+morkAtom* morkStore::AddAlias(morkEnv* ev, const morkMid& inMid,
+ mork_cscode inForm) {
+ morkBookAtom* outAtom = 0;
+ if (ev->Good()) {
+ const mdbOid* oid = &inMid.mMid_Oid;
+ morkAtomSpace* atomSpace = this->LazyGetAtomSpace(ev, oid->mOid_Scope);
+ if (atomSpace) {
+ morkFarBookAtom* keyAtom =
+ this->StageAliasAsFarBookAtom(ev, &inMid, atomSpace, inForm);
+ if (keyAtom) {
+ morkAtomAidMap* map = &atomSpace->mAtomSpace_AtomAids;
+ outAtom = map->GetAid(ev, (mork_aid)oid->mOid_Id);
+ if (outAtom) {
+ if (!outAtom->EqualFormAndBody(ev, keyAtom))
+ ev->NewError("duplicate alias ID");
+ } else {
+ this->MaybeDirtyStore();
+ keyAtom->mBookAtom_Id = oid->mOid_Id;
+ outAtom = atomSpace->MakeBookAtomCopyWithAid(ev, *keyAtom,
+ (mork_aid)oid->mOid_Id);
+
+ // if ( outAtom && outAtom->IsWeeBook() )
+ // {
+ // if ( oid->mOid_Scope == morkStore_kColumnSpaceScope )
+ // {
+ // mork_size size = outAtom->mAtom_Size;
+ // if ( size >= 7 && size <= 11 )
+ // this->SyncTokenIdChange(ev, outAtom, oid);
+ // }
+ // }
+ }
+ }
+ }
+ }
+ return outAtom;
+}
+
+#define morkStore_kMaxCopyTokenSize 512 /* if larger, cannot be copied */
+
+mork_token morkStore::CopyToken(morkEnv* ev, mdb_token inToken,
+ morkStore* inStore)
+// copy inToken from inStore over to this store
+{
+ mork_token outToken = 0;
+ if (inStore == this) // same store?
+ outToken = inToken; // just return token unchanged
+ else {
+ char yarnBuf[morkStore_kMaxCopyTokenSize];
+ mdbYarn yarn;
+ yarn.mYarn_Buf = yarnBuf;
+ yarn.mYarn_Fill = 0;
+ yarn.mYarn_Size = morkStore_kMaxCopyTokenSize;
+ yarn.mYarn_More = 0;
+ yarn.mYarn_Form = 0;
+ yarn.mYarn_Grow = 0;
+
+ inStore->TokenToString(ev, inToken, &yarn);
+ if (ev->Good()) {
+ morkBuf buf(yarn.mYarn_Buf, yarn.mYarn_Fill);
+ outToken = this->BufToToken(ev, &buf);
+ }
+ }
+ return outToken;
+}
+
+mork_token morkStore::BufToToken(morkEnv* ev, const morkBuf* inBuf) {
+ mork_token outToken = 0;
+ if (ev->Good()) {
+ const mork_u1* s = (const mork_u1*)inBuf->mBuf_Body;
+ mork_bool nonAscii = (*s > 0x7F);
+ mork_size length = inBuf->mBuf_Fill;
+ if (nonAscii || length > 1) // more than one byte?
+ {
+ mork_cscode form = 0; // default charset
+ morkAtomSpace* space = this->LazyGetGroundColumnSpace(ev);
+ if (space) {
+ morkFarBookAtom* keyAtom = 0;
+ if (length <= morkBookAtom_kMaxBodySize) {
+ mork_aid aid = 1; // dummy
+ // mStore_BookAtom.InitMaxBookAtom(ev, *inBuf, form, space, aid);
+ mStore_FarBookAtom.InitFarBookAtom(ev, *inBuf, form, space, aid);
+ keyAtom = &mStore_FarBookAtom;
+ }
+ if (keyAtom) {
+ morkAtomBodyMap* map = &space->mAtomSpace_AtomBodies;
+ morkBookAtom* bookAtom = map->GetAtom(ev, keyAtom);
+ if (bookAtom)
+ outToken = bookAtom->mBookAtom_Id;
+ else {
+ this->MaybeDirtyStore();
+ bookAtom = space->MakeBookAtomCopy(ev, *keyAtom);
+ if (bookAtom) {
+ outToken = bookAtom->mBookAtom_Id;
+ bookAtom->MakeCellUseForever(ev);
+ }
+ }
+ }
+ }
+ } else // only a single byte in inTokenName string:
+ outToken = *s;
+ }
+
+ return outToken;
+}
+
+mork_token morkStore::StringToToken(morkEnv* ev, const char* inTokenName) {
+ mork_token outToken = 0;
+ if (ev->Good()) {
+ const mork_u1* s = (const mork_u1*)inTokenName;
+ mork_bool nonAscii = (*s > 0x7F);
+ if (nonAscii || (*s && s[1])) // more than one byte?
+ {
+ mork_cscode form = 0; // default charset
+ morkAtomSpace* groundSpace = this->LazyGetGroundColumnSpace(ev);
+ if (groundSpace) {
+ morkFarBookAtom* keyAtom =
+ this->StageStringAsFarBookAtom(ev, inTokenName, form, groundSpace);
+ if (keyAtom) {
+ morkAtomBodyMap* map = &groundSpace->mAtomSpace_AtomBodies;
+ morkBookAtom* bookAtom = map->GetAtom(ev, keyAtom);
+ if (bookAtom)
+ outToken = bookAtom->mBookAtom_Id;
+ else {
+ this->MaybeDirtyStore();
+ bookAtom = groundSpace->MakeBookAtomCopy(ev, *keyAtom);
+ if (bookAtom) {
+ outToken = bookAtom->mBookAtom_Id;
+ bookAtom->MakeCellUseForever(ev);
+ }
+ }
+ }
+ }
+ } else // only a single byte in inTokenName string:
+ outToken = *s;
+ }
+
+ return outToken;
+}
+
+mork_token morkStore::QueryToken(morkEnv* ev, const char* inTokenName) {
+ mork_token outToken = 0;
+ if (ev->Good()) {
+ const mork_u1* s = (const mork_u1*)inTokenName;
+ mork_bool nonAscii = (*s > 0x7F);
+ if (nonAscii || (*s && s[1])) // more than one byte?
+ {
+ mork_cscode form = 0; // default charset
+ morkAtomSpace* groundSpace = this->LazyGetGroundColumnSpace(ev);
+ if (groundSpace) {
+ morkFarBookAtom* keyAtom =
+ this->StageStringAsFarBookAtom(ev, inTokenName, form, groundSpace);
+ if (keyAtom) {
+ morkAtomBodyMap* map = &groundSpace->mAtomSpace_AtomBodies;
+ morkBookAtom* bookAtom = map->GetAtom(ev, keyAtom);
+ if (bookAtom) {
+ outToken = bookAtom->mBookAtom_Id;
+ bookAtom->MakeCellUseForever(ev);
+ }
+ }
+ }
+ } else // only a single byte in inTokenName string:
+ outToken = *s;
+ }
+
+ return outToken;
+}
+
+mork_bool morkStore::HasTableKind(morkEnv* ev, mdb_scope inRowScope,
+ mdb_kind inTableKind,
+ mdb_count* outTableCount) {
+ MORK_USED_2(inRowScope, inTableKind);
+ mork_bool outBool = morkBool_kFalse;
+ mdb_count tableCount = 0;
+
+ ev->StubMethodOnlyError();
+
+ if (outTableCount) *outTableCount = tableCount;
+ return outBool;
+}
+
+morkTable* morkStore::GetTableKind(morkEnv* ev, mdb_scope inRowScope,
+ mdb_kind inTableKind,
+ mdb_count* outTableCount,
+ mdb_bool* outMustBeUnique) {
+ morkTable* outTable = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inRowScope);
+ if (rowSpace) {
+ outTable = rowSpace->FindTableByKind(ev, inTableKind);
+ if (outTable) {
+ if (outTableCount) *outTableCount = outTable->GetRowCount();
+ if (outMustBeUnique) *outMustBeUnique = outTable->IsTableUnique();
+ }
+ }
+ }
+ return outTable;
+}
+
+morkRow* morkStore::FindRow(morkEnv* ev, mdb_scope inScope, mdb_column inColumn,
+ const mdbYarn* inYarn) {
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inScope);
+ if (rowSpace) {
+ outRow = rowSpace->FindRow(ev, inColumn, inYarn);
+ }
+ }
+ return outRow;
+}
+
+morkRow* morkStore::GetRow(morkEnv* ev, const mdbOid* inOid) {
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inOid->mOid_Scope);
+ if (rowSpace) {
+ outRow = rowSpace->mRowSpace_Rows.GetOid(ev, inOid);
+ }
+ }
+ return outRow;
+}
+
+morkTable* morkStore::GetTable(morkEnv* ev, const mdbOid* inOid) {
+ morkTable* outTable = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inOid->mOid_Scope);
+ if (rowSpace) {
+ outTable = rowSpace->FindTableByTid(ev, inOid->mOid_Id);
+ }
+ }
+ return outTable;
+}
+
+morkTable* morkStore::NewTable(
+ morkEnv* ev, mdb_scope inRowScope, mdb_kind inTableKind,
+ mdb_bool inMustBeUnique,
+ const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
+{
+ morkTable* outTable = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inRowScope);
+ if (rowSpace)
+ outTable = rowSpace->NewTable(ev, inTableKind, inMustBeUnique,
+ inOptionalMetaRowOid);
+ }
+ return outTable;
+}
+
+morkPortTableCursor* morkStore::GetPortTableCursor(morkEnv* ev,
+ mdb_scope inRowScope,
+ mdb_kind inTableKind) {
+ morkPortTableCursor* outCursor = 0;
+ if (ev->Good()) {
+ nsIMdbHeap* heap = mPort_Heap;
+ outCursor = new (*heap, ev) morkPortTableCursor(
+ ev, morkUsage::kHeap, heap, this, inRowScope, inTableKind, heap);
+ }
+ NS_IF_ADDREF(outCursor);
+ return outCursor;
+}
+
+morkRow* morkStore::NewRow(morkEnv* ev, mdb_scope inRowScope) {
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inRowScope);
+ if (rowSpace) outRow = rowSpace->NewRow(ev);
+ }
+ return outRow;
+}
+
+morkRow* morkStore::NewRowWithOid(morkEnv* ev, const mdbOid* inOid) {
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inOid->mOid_Scope);
+ if (rowSpace) outRow = rowSpace->NewRowWithOid(ev, inOid);
+ }
+ return outRow;
+}
+
+morkRow* morkStore::OidToRow(morkEnv* ev, const mdbOid* inOid)
+// OidToRow() finds old row with oid, or makes new one if not found.
+{
+ morkRow* outRow = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inOid->mOid_Scope);
+ if (rowSpace) {
+ outRow = rowSpace->mRowSpace_Rows.GetOid(ev, inOid);
+ if (!outRow && ev->Good()) outRow = rowSpace->NewRowWithOid(ev, inOid);
+ }
+ }
+ return outRow;
+}
+
+morkTable* morkStore::OidToTable(
+ morkEnv* ev, const mdbOid* inOid,
+ const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
+// OidToTable() finds old table with oid, or makes new one if not found.
+{
+ morkTable* outTable = 0;
+ if (ev->Good()) {
+ morkRowSpace* rowSpace = this->LazyGetRowSpace(ev, inOid->mOid_Scope);
+ if (rowSpace) {
+ outTable = rowSpace->mRowSpace_Tables.GetTable(ev, inOid->mOid_Id);
+ if (!outTable && ev->Good()) {
+ mork_kind tableKind = morkStore_kNoneToken;
+ outTable = rowSpace->NewTableWithTid(ev, inOid->mOid_Id, tableKind,
+ inOptionalMetaRowOid);
+ }
+ }
+ }
+ return outTable;
+}
+
+// { ===== begin nsIMdbObject methods =====
+
+// { ----- begin ref counting for well-behaved cyclic graphs -----
+NS_IMETHODIMP
+morkStore::GetWeakRefCount(nsIMdbEnv* mev, // weak refs
+ mdb_count* outCount) {
+ *outCount = WeakRefsOnly();
+ return NS_OK;
+}
+NS_IMETHODIMP
+morkStore::GetStrongRefCount(nsIMdbEnv* mev, // strong refs
+ mdb_count* outCount) {
+ *outCount = StrongRefsOnly();
+ return NS_OK;
+}
+// ### TODO - clean up this cast, if required
+NS_IMETHODIMP
+morkStore::AddWeakRef(nsIMdbEnv* mev) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::AddWeakRef(ev));
+}
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkStore::AddStrongRef(morkEnv* mev) { return AddRef(); }
+#endif
+NS_IMETHODIMP_(mork_uses)
+morkStore::AddStrongRef(nsIMdbEnv* mev) { return AddRef(); }
+NS_IMETHODIMP
+morkStore::CutWeakRef(nsIMdbEnv* mev) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ // XXX Casting mork_refs to nsresult
+ return static_cast<nsresult>(morkNode::CutWeakRef(ev));
+}
+#ifndef _MSC_VER
+NS_IMETHODIMP_(mork_uses)
+morkStore::CutStrongRef(morkEnv* mev) { return Release(); }
+#endif
+NS_IMETHODIMP
+morkStore::CutStrongRef(nsIMdbEnv* mev) {
+ // XXX Casting nsrefcnt to nsresult
+ return static_cast<nsresult>(Release());
+}
+
+NS_IMETHODIMP
+morkStore::CloseMdbObject(nsIMdbEnv* mev) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ CloseMorkNode(ev);
+ Release();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkStore::IsOpenMdbObject(nsIMdbEnv* mev, mdb_bool* outOpen) {
+ *outOpen = IsOpenNode();
+ return NS_OK;
+}
+// } ----- end ref counting -----
+
+// } ===== end nsIMdbObject methods =====
+
+// { ===== begin nsIMdbPort methods =====
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP
+morkStore::GetIsPortReadonly(nsIMdbEnv* mev, mdb_bool* outBool) {
+ nsresult outErr = NS_OK;
+ mdb_bool isReadOnly = morkBool_kFalse;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ ev->StubMethodOnlyError();
+ outErr = ev->AsErr();
+ }
+ if (outBool) *outBool = isReadOnly;
+ return outErr;
+}
+
+morkEnv* morkStore::CanUseStore(nsIMdbEnv* mev, mork_bool inMutable,
+ nsresult* outErr) const {
+ morkEnv* outEnv = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (IsStore())
+ outEnv = ev;
+ else
+ NonStoreTypeError(ev);
+ *outErr = ev->AsErr();
+ }
+ MORK_ASSERT(outEnv);
+ return outEnv;
+}
+
+NS_IMETHODIMP
+morkStore::GetIsStore(nsIMdbEnv* mev, mdb_bool* outBool) {
+ MORK_USED_1(mev);
+ if (outBool) *outBool = morkBool_kTrue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkStore::GetIsStoreAndDirty(nsIMdbEnv* mev, mdb_bool* outBool) {
+ nsresult outErr = NS_OK;
+ mdb_bool isStoreAndDirty = morkBool_kFalse;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ ev->StubMethodOnlyError();
+ outErr = ev->AsErr();
+ }
+ if (outBool) *outBool = isStoreAndDirty;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetUsagePolicy(nsIMdbEnv* mev, mdbUsagePolicy* ioUsagePolicy) {
+ MORK_USED_1(ioUsagePolicy);
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ ev->StubMethodOnlyError();
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::SetUsagePolicy(nsIMdbEnv* mev, const mdbUsagePolicy* inUsagePolicy) {
+ MORK_USED_1(inUsagePolicy);
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing?
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin memory policy methods -----
+NS_IMETHODIMP
+morkStore::IdleMemoryPurge( // do memory management already scheduled
+ nsIMdbEnv* mev, // context
+ mdb_size* outEstimatedBytesFreed) // approximate bytes actually freed
+{
+ nsresult outErr = NS_OK;
+ mdb_size estimatedBytesFreed = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing?
+ outErr = ev->AsErr();
+ }
+ if (outEstimatedBytesFreed) *outEstimatedBytesFreed = estimatedBytesFreed;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::SessionMemoryPurge( // request specific footprint decrease
+ nsIMdbEnv* mev, // context
+ mdb_size inDesiredBytesFreed, // approximate number of bytes wanted
+ mdb_size* outEstimatedBytesFreed) // approximate bytes actually freed
+{
+ MORK_USED_1(inDesiredBytesFreed);
+ nsresult outErr = NS_OK;
+ mdb_size estimate = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing?
+ outErr = ev->AsErr();
+ }
+ if (outEstimatedBytesFreed) *outEstimatedBytesFreed = estimate;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::PanicMemoryPurge( // desperately free all possible memory
+ nsIMdbEnv* mev, // context
+ mdb_size* outEstimatedBytesFreed) // approximate bytes actually freed
+{
+ nsresult outErr = NS_OK;
+ mdb_size estimate = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing?
+ outErr = ev->AsErr();
+ }
+ if (outEstimatedBytesFreed) *outEstimatedBytesFreed = estimate;
+ return outErr;
+}
+// } ----- end memory policy methods -----
+
+// { ----- begin filepath methods -----
+NS_IMETHODIMP
+morkStore::GetPortFilePath(
+ nsIMdbEnv* mev, // context
+ mdbYarn* outFilePath, // name of file holding port content
+ mdbYarn* outFormatVersion) // file format description
+{
+ nsresult outErr = NS_OK;
+ if (outFormatVersion) outFormatVersion->mYarn_Fill = 0;
+ if (outFilePath) outFilePath->mYarn_Fill = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ if (mStore_File)
+ mStore_File->Path(mev, outFilePath);
+ else
+ NilStoreFileError(ev);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetPortFile(
+ nsIMdbEnv* mev, // context
+ nsIMdbFile** acqFile) // acquire file used by port or store
+{
+ nsresult outErr = NS_OK;
+ if (acqFile) *acqFile = 0;
+
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ if (mStore_File) {
+ if (acqFile) {
+ mStore_File->AddRef();
+ if (ev->Good()) *acqFile = mStore_File;
+ }
+ } else
+ NilStoreFileError(ev);
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end filepath methods -----
+
+// { ----- begin export methods -----
+NS_IMETHODIMP
+morkStore::BestExportFormat( // determine preferred export format
+ nsIMdbEnv* mev, // context
+ mdbYarn* outFormatVersion) // file format description
+{
+ nsresult outErr = NS_OK;
+ if (outFormatVersion) outFormatVersion->mYarn_Fill = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ ev->StubMethodOnlyError();
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::CanExportToFormat( // can export content in given specific format?
+ nsIMdbEnv* mev, // context
+ const char* inFormatVersion, // file format description
+ mdb_bool* outCanExport) // whether ExportSource() might succeed
+{
+ MORK_USED_1(inFormatVersion);
+ mdb_bool canExport = morkBool_kFalse;
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ ev->StubMethodOnlyError();
+ outErr = ev->AsErr();
+ }
+ if (outCanExport) *outCanExport = canExport;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::ExportToFormat( // export content in given specific format
+ nsIMdbEnv* mev, // context
+ // const char* inFilePath, // the file to receive exported content
+ nsIMdbFile* ioFile, // destination abstract file interface
+ const char* inFormatVersion, // file format description
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental export
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the export will be finished.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ if (ioFile && inFormatVersion && acqThumb) {
+ ev->StubMethodOnlyError();
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+
+// } ----- end export methods -----
+
+// { ----- begin token methods -----
+NS_IMETHODIMP
+morkStore::TokenToString( // return a string name for an integer token
+ nsIMdbEnv* mev, // context
+ mdb_token inToken, // token for inTokenName inside this port
+ mdbYarn* outTokenName) // the type of table to access
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ TokenToString(ev, inToken, outTokenName);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::StringToToken( // return an integer token for scope name
+ nsIMdbEnv* mev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) // token for inTokenName inside this port
+// String token zero is never used and never supported. If the port
+// is a mutable store, then StringToToken() to create a new
+// association of inTokenName with a new integer token if possible.
+// But a readonly port will return zero for an unknown scope name.
+{
+ nsresult outErr = NS_OK;
+ mdb_token token = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ token = StringToToken(ev, inTokenName);
+ outErr = ev->AsErr();
+ }
+ if (outToken) *outToken = token;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::QueryToken( // like StringToToken(), but without adding
+ nsIMdbEnv* mev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) // token for inTokenName inside this port
+// QueryToken() will return a string token if one already exists,
+// but unlike StringToToken(), will not assign a new token if not
+// already in use.
+{
+ nsresult outErr = NS_OK;
+ mdb_token token = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ token = QueryToken(ev, inTokenName);
+ outErr = ev->AsErr();
+ }
+ if (outToken) *outToken = token;
+ return outErr;
+}
+
+// } ----- end token methods -----
+
+// { ----- begin row methods -----
+NS_IMETHODIMP
+morkStore::HasRow( // contains a row with the specified oid?
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_bool* outHasRow) // whether GetRow() might succeed
+{
+ nsresult outErr = NS_OK;
+ mdb_bool hasRow = morkBool_kFalse;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = GetRow(ev, inOid);
+ if (row) hasRow = morkBool_kTrue;
+
+ outErr = ev->AsErr();
+ }
+ if (outHasRow) *outHasRow = hasRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetRow( // access one row with specific oid
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ nsIMdbRow** acqRow) // acquire specific row (or null)
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = GetRow(ev, inOid);
+ if (row && ev->Good()) outRow = row->AcquireRowHandle(ev, this);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetRowRefCount( // get number of tables that contain a row
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_count* outRefCount) // number of tables containing inRowKey
+{
+ nsresult outErr = NS_OK;
+ mdb_count count = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = GetRow(ev, inOid);
+ if (row && ev->Good()) count = row->mRow_GcUses;
+
+ outErr = ev->AsErr();
+ }
+ if (outRefCount) *outRefCount = count;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::FindRow(
+ nsIMdbEnv* mev, // search for row with matching cell
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_column inColumn, // the column to search (and maintain an index)
+ const mdbYarn* inTargetCellValue, // cell value for which to search
+ mdbOid* outRowOid, // out row oid on match (or {0,-1} for no match)
+ nsIMdbRow** acqRow) // acquire matching row (or nil for no match)
+// FindRow() searches for one row that has a cell in column inColumn with
+// a contained value with the same form (i.e. charset) and is byte-wise
+// identical to the blob described by yarn inTargetCellValue. Both content
+// and form of the yarn must be an exact match to find a matching row.
+//
+// (In other words, both a yarn's blob bytes and form are significant. The
+// form is not expected to vary in columns used for identity anyway. This
+// is intended to make the cost of FindRow() cheaper for MDB implementors,
+// since any cell value atomization performed internally must necessarily
+// make yarn form significant in order to avoid data loss in atomization.)
+//
+// FindRow() can lazily create an index on attribute inColumn for all rows
+// with that attribute in row space scope inRowScope, so that subsequent
+// calls to FindRow() will perform faster. Such an index might or might
+// not be persistent (but this seems desirable if it is cheap to do so).
+// Note that lazy index creation in readonly DBs is not very feasible.
+//
+// This FindRow() interface assumes that attribute inColumn is effectively
+// an alternative means of unique identification for a row in a rowspace,
+// so correct behavior is only guaranteed when no duplicates for this col
+// appear in the given set of rows. (If more than one row has the same cell
+// value in this column, no more than one will be found; and cutting one of
+// two duplicate rows can cause the index to assume no other such row lives
+// in the row space, so future calls return nil for negative search results
+// even though some duplicate row might still live within the rowspace.)
+//
+// In other words, the FindRow() implementation is allowed to assume simple
+// hash tables mapping unique column keys to associated row values will be
+// sufficient, where any duplication is not recorded because only one copy
+// of a given key need be remembered. Implementors are not required to sort
+// all rows by the specified column.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ mdbOid rowOid;
+ rowOid.mOid_Scope = 0;
+ rowOid.mOid_Id = (mdb_id)-1;
+
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = FindRow(ev, inRowScope, inColumn, inTargetCellValue);
+ if (row && ev->Good()) {
+ rowOid = row->mRow_Oid;
+ if (acqRow) outRow = row->AcquireRowHandle(ev, this);
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ if (outRowOid) *outRowOid = rowOid;
+
+ return outErr;
+}
+
+// } ----- end row methods -----
+
+// { ----- begin table methods -----
+NS_IMETHODIMP
+morkStore::HasTable( // supports a table with the specified oid?
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ mdb_bool* outHasTable) // whether GetTable() might succeed
+{
+ nsresult outErr = NS_OK;
+ mork_bool hasTable = morkBool_kFalse;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = GetTable(ev, inOid);
+ if (table) hasTable = morkBool_kTrue;
+
+ outErr = ev->AsErr();
+ }
+ if (outHasTable) *outHasTable = hasTable;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetTable( // access one table with specific oid
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ nsIMdbTable** acqTable) // acquire specific table (or null)
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = GetTable(ev, inOid);
+ if (table && ev->Good()) outTable = table->AcquireTableHandle(ev);
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::HasTableKind( // supports a table of the specified type?
+ nsIMdbEnv* mev, // context
+ mdb_scope inRowScope, // rid scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outSupportsTable) // whether GetTableKind() might succeed
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ *outSupportsTable =
+ HasTableKind(ev, inRowScope, inTableKind, outTableCount);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetTableKind( // access one (random) table of specific type
+ nsIMdbEnv* mev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outMustBeUnique, // whether port can hold only one of these
+ nsIMdbTable** acqTable) // acquire scoped collection of rows
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = GetTableKind(ev, inRowScope, inTableKind, outTableCount,
+ outMustBeUnique);
+ if (table && ev->Good()) outTable = table->AcquireTableHandle(ev);
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::GetPortTableCursor( // get cursor for all tables of specific type
+ nsIMdbEnv* mev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ nsIMdbPortTableCursor** acqCursor) // all such tables in the port
+{
+ nsresult outErr = NS_OK;
+ nsIMdbPortTableCursor* outCursor = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkPortTableCursor* cursor =
+ GetPortTableCursor(ev, inRowScope, inTableKind);
+ if (cursor && ev->Good()) outCursor = cursor;
+
+ outErr = ev->AsErr();
+ }
+ if (acqCursor) *acqCursor = outCursor;
+ return outErr;
+}
+// } ----- end table methods -----
+
+// { ----- begin commit methods -----
+
+NS_IMETHODIMP
+morkStore::ShouldCompress( // store wastes at least inPercentWaste?
+ nsIMdbEnv* mev, // context
+ mdb_percent inPercentWaste, // 0..100 percent file size waste threshold
+ mdb_percent* outActualWaste, // 0..100 percent of file actually wasted
+ mdb_bool* outShould) // true when about inPercentWaste% is wasted
+{
+ mdb_percent actualWaste = 0;
+ mdb_bool shouldCompress = morkBool_kFalse;
+ nsresult outErr = NS_OK;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ actualWaste = PercentOfStoreWasted(ev);
+ if (inPercentWaste > 100) inPercentWaste = 100;
+ shouldCompress = (actualWaste >= inPercentWaste);
+ outErr = ev->AsErr();
+ }
+ if (outActualWaste) *outActualWaste = actualWaste;
+ if (outShould) *outShould = shouldCompress;
+ return outErr;
+}
+
+// } ===== end nsIMdbPort methods =====
+
+NS_IMETHODIMP
+morkStore::NewTable( // make one new table of specific type
+ nsIMdbEnv* mev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) // acquire scoped collection of rows
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev = this->CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = NewTable(ev, inRowScope, inTableKind, inMustBeUnique,
+ inOptionalMetaRowOid);
+ if (table && ev->Good()) outTable = table->AcquireTableHandle(ev);
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::NewTableWithOid( // make one new table of specific type
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // caller assigned oid
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) // acquire scoped collection of rows
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkTable* table = OidToTable(ev, inOid, inOptionalMetaRowOid);
+ if (table && ev->Good()) {
+ table->mTable_Kind = inTableKind;
+ if (inMustBeUnique) table->SetTableUnique();
+ outTable = table->AcquireTableHandle(ev);
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+// } ----- end table methods -----
+
+// { ----- begin row scope methods -----
+NS_IMETHODIMP
+morkStore::RowScopeHasAssignedIds(
+ nsIMdbEnv* mev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) // nonzero if store db assigned specified
+{
+ NS_ASSERTION(false, " not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkStore::SetCallerAssignedIds(
+ nsIMdbEnv* mev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) // nonzero if store db assigned specified
+{
+ NS_ASSERTION(false, " not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkStore::SetStoreAssignedIds(
+ nsIMdbEnv* mev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned) // nonzero if store db assigned specified
+{
+ NS_ASSERTION(false, " not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end row scope methods -----
+
+// { ----- begin row methods -----
+NS_IMETHODIMP
+morkStore::NewRowWithOid(nsIMdbEnv* mev, // new row w/ caller assigned oid
+ const mdbOid* inOid, // caller assigned oid
+ nsIMdbRow** acqRow) // create new row
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = NewRowWithOid(ev, inOid);
+ if (row && ev->Good()) outRow = row->AcquireRowHandle(ev, this);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::NewRow(nsIMdbEnv* mev, // new row with db assigned oid
+ mdb_scope inRowScope, // row scope for row ids
+ nsIMdbRow** acqRow) // create new row
+// Note this row must be added to some table or cell child before the
+// store is closed in order to make this row persist across sessions.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkRow* row = NewRow(ev, inRowScope);
+ if (row && ev->Good()) outRow = row->AcquireRowHandle(ev, this);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+// } ----- end row methods -----
+
+// { ----- begin import/export methods -----
+NS_IMETHODIMP
+morkStore::ImportContent( // import content from port
+ nsIMdbEnv* mev, // context
+ mdb_scope inRowScope, // scope for rows (or zero for all?)
+ nsIMdbPort* ioPort, // the port with content to add to store
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental import
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the import will be finished.
+{
+ NS_ASSERTION(false, " not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkStore::ImportFile( // import content from port
+ nsIMdbEnv* mev, // context
+ nsIMdbFile* ioFile, // the file with content to add to store
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental import
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the import will be finished.
+{
+ NS_ASSERTION(false, " not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end import/export methods -----
+
+// { ----- begin hinting methods -----
+NS_IMETHODIMP
+morkStore::ShareAtomColumnsHint( // advise re shared col content atomizing
+ nsIMdbEnv* mev, // context
+ mdb_scope inScopeHint, // zero, or suggested shared namespace
+ const mdbColumnSet* inColumnSet) // cols desired tokenized together
+{
+ MORK_USED_2(inColumnSet, inScopeHint);
+ nsresult outErr = NS_OK;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing for a hint method
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::AvoidAtomColumnsHint( // advise col w/ poor atomizing prospects
+ nsIMdbEnv* mev, // context
+ const mdbColumnSet* inColumnSet) // cols with poor atomizing prospects
+{
+ MORK_USED_1(inColumnSet);
+ nsresult outErr = NS_OK;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ // ev->StubMethodOnlyError(); // okay to do nothing for a hint method
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end hinting methods -----
+
+// { ----- begin commit methods -----
+NS_IMETHODIMP
+morkStore::LargeCommit( // save important changes if at all possible
+ nsIMdbEnv* mev, // context
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental commit
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the commit will be finished. Note the store is effectively write
+// locked until commit is finished or canceled through the thumb instance.
+// Until the commit is done, the store will report it has readonly status.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkThumb* thumb = 0;
+ // morkFile* file = store->mStore_File;
+ if (DoPreferLargeOverCompressCommit(ev)) {
+ thumb = morkThumb::Make_LargeCommit(ev, mPort_Heap, this);
+ } else {
+ mork_bool doCollect = morkBool_kFalse;
+ thumb = morkThumb::Make_CompressCommit(ev, mPort_Heap, this, doCollect);
+ }
+
+ if (thumb) {
+ outThumb = thumb;
+ thumb->AddRef();
+ }
+
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::SessionCommit( // save all changes if large commits delayed
+ nsIMdbEnv* mev, // context
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental commit
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the commit will be finished. Note the store is effectively write
+// locked until commit is finished or canceled through the thumb instance.
+// Until the commit is done, the store will report it has readonly status.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ morkThumb* thumb = 0;
+ if (DoPreferLargeOverCompressCommit(ev)) {
+ thumb = morkThumb::Make_LargeCommit(ev, mPort_Heap, this);
+ } else {
+ mork_bool doCollect = morkBool_kFalse;
+ thumb = morkThumb::Make_CompressCommit(ev, mPort_Heap, this, doCollect);
+ }
+
+ if (thumb) {
+ outThumb = thumb;
+ thumb->AddRef();
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkStore::CompressCommit( // commit and make db smaller if possible
+ nsIMdbEnv* mev, // context
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental commit
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the commit will be finished. Note the store is effectively write
+// locked until commit is finished or canceled through the thumb instance.
+// Until the commit is done, the store will report it has readonly status.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbThumb* outThumb = 0;
+ morkEnv* ev = CanUseStore(mev, /*inMutable*/ morkBool_kFalse, &outErr);
+ if (ev) {
+ mork_bool doCollect = morkBool_kFalse;
+ morkThumb* thumb =
+ morkThumb::Make_CompressCommit(ev, mPort_Heap, this, doCollect);
+ if (thumb) {
+ outThumb = thumb;
+ thumb->AddRef();
+ mStore_CanWriteIncremental = morkBool_kTrue;
+ }
+
+ outErr = ev->AsErr();
+ }
+ if (acqThumb) *acqThumb = outThumb;
+ return outErr;
+}
+
+// } ----- end commit methods -----
+
+// } ===== end nsIMdbStore methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkStore.h b/comm/mailnews/db/mork/morkStore.h
new file mode 100644
index 0000000000..c2a08cb7d2
--- /dev/null
+++ b/comm/mailnews/db/mork/morkStore.h
@@ -0,0 +1,770 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKSTORE_
+#define _MORKSTORE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+#ifndef _MORKPOOL_
+# include "morkPool.h"
+#endif
+
+#ifndef _MORKZONE_
+# include "morkZone.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kPort /*i*/ 0x7054 /* ascii 'pT' */
+
+#define morkDerived_kStore /*i*/ 0x7354 /* ascii 'sT' */
+
+/*| kGroundColumnSpace: we use the 'column space' as the default scope
+**| for grounding column name IDs, and this is also the default scope for
+**| all other explicitly tokenized strings.
+|*/
+#define morkStore_kGroundColumnSpace 'c' /* for mStore_GroundColumnSpace*/
+#define morkStore_kColumnSpaceScope ((mork_scope)'c') /*kGroundColumnSpace*/
+#define morkStore_kValueSpaceScope ((mork_scope)'v')
+#define morkStore_kStreamBufSize (8 * 1024) /* okay buffer size */
+
+#define morkStore_kReservedColumnCount 0x20 /* for well-known columns */
+
+#define morkStore_kNoneToken ((mork_token)'n')
+#define morkStore_kFormColumn ((mork_column)'f')
+#define morkStore_kAtomScopeColumn ((mork_column)'a')
+#define morkStore_kRowScopeColumn ((mork_column)'r')
+#define morkStore_kMetaScope ((mork_scope)'m')
+#define morkStore_kKindColumn ((mork_column)'k')
+#define morkStore_kStatusColumn ((mork_column)'s')
+
+/*| morkStore:
+|*/
+class morkStore : public morkObject, public nsIMdbStore {
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+
+ morkEnv* mPort_Env; // non-refcounted env which created port
+ morkFactory* mPort_Factory; // weak ref to suite factory
+ nsIMdbHeap* mPort_Heap; // heap in which this port allocs objects
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ void ClosePort(morkEnv* ev); // called by CloseMorkNode();
+
+ public: // dynamic type identification
+ mork_bool IsPort() const {
+ return IsNode() && mNode_Derived == morkDerived_kPort;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // other port methods
+ // { ----- begin attribute methods -----
+ // NS_IMETHOD IsFrozenMdbObject(nsIMdbEnv* ev, mdb_bool* outIsReadonly);
+ // same as nsIMdbPort::GetIsPortReadonly() when this object is inside a port.
+ // } ----- end attribute methods -----
+
+ // { ----- begin factory methods -----
+ // NS_IMETHOD GetMdbFactory(nsIMdbEnv* ev, nsIMdbFactory** acqFactory);
+ // } ----- end factory methods -----
+
+ // { ----- begin ref counting for well-behaved cyclic graphs -----
+ NS_IMETHOD GetWeakRefCount(nsIMdbEnv* ev, // weak refs
+ mdb_count* outCount) override;
+ NS_IMETHOD GetStrongRefCount(nsIMdbEnv* ev, // strong refs
+ mdb_count* outCount) override;
+
+ NS_IMETHOD AddWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of AddStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) AddStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD_(mork_uses) AddStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CutWeakRef(nsIMdbEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of CutStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ NS_IMETHOD_(mork_uses) CutStrongRef(morkEnv* ev) override;
+#endif
+ NS_IMETHOD CutStrongRef(nsIMdbEnv* ev) override;
+
+ NS_IMETHOD CloseMdbObject(
+ nsIMdbEnv* ev) override; // called at strong refs zero
+ NS_IMETHOD IsOpenMdbObject(nsIMdbEnv* ev, mdb_bool* outOpen) override;
+ // } ----- end ref counting -----
+
+ // } ===== end nsIMdbObject methods =====
+
+ // { ===== begin nsIMdbPort methods =====
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetIsPortReadonly(nsIMdbEnv* ev, mdb_bool* outBool) override;
+ NS_IMETHOD GetIsStore(nsIMdbEnv* ev, mdb_bool* outBool) override;
+ NS_IMETHOD GetIsStoreAndDirty(nsIMdbEnv* ev, mdb_bool* outBool) override;
+
+ NS_IMETHOD GetUsagePolicy(nsIMdbEnv* ev,
+ mdbUsagePolicy* ioUsagePolicy) override;
+
+ NS_IMETHOD SetUsagePolicy(nsIMdbEnv* ev,
+ const mdbUsagePolicy* inUsagePolicy) override;
+ // } ----- end attribute methods -----
+
+ // { ----- begin memory policy methods -----
+ NS_IMETHOD IdleMemoryPurge( // do memory management already scheduled
+ nsIMdbEnv* ev, // context
+ mdb_size* outEstimatedBytesFreed)
+ override; // approximate bytes actually freed
+
+ NS_IMETHOD SessionMemoryPurge( // request specific footprint decrease
+ nsIMdbEnv* ev, // context
+ mdb_size inDesiredBytesFreed, // approximate number of bytes wanted
+ mdb_size* outEstimatedBytesFreed)
+ override; // approximate bytes actually freed
+
+ NS_IMETHOD PanicMemoryPurge( // desperately free all possible memory
+ nsIMdbEnv* ev, // context
+ mdb_size* outEstimatedBytesFreed)
+ override; // approximate bytes actually freed
+ // } ----- end memory policy methods -----
+
+ // { ----- begin filepath methods -----
+ NS_IMETHOD GetPortFilePath(
+ nsIMdbEnv* ev, // context
+ mdbYarn* outFilePath, // name of file holding port content
+ mdbYarn* outFormatVersion) override; // file format description
+
+ NS_IMETHOD GetPortFile(
+ nsIMdbEnv* ev, // context
+ nsIMdbFile** acqFile) override; // acquire file used by port or store
+ // } ----- end filepath methods -----
+
+ // { ----- begin export methods -----
+ NS_IMETHOD BestExportFormat( // determine preferred export format
+ nsIMdbEnv* ev, // context
+ mdbYarn* outFormatVersion) override; // file format description
+
+ NS_IMETHOD
+ CanExportToFormat( // can export content in given specific format?
+ nsIMdbEnv* ev, // context
+ const char* inFormatVersion, // file format description
+ mdb_bool* outCanExport) override; // whether ExportSource() might succeed
+
+ NS_IMETHOD ExportToFormat( // export content in given specific format
+ nsIMdbEnv* ev, // context
+ // const char* inFilePath, // the file to receive exported content
+ nsIMdbFile* ioFile, // destination abstract file interface
+ const char* inFormatVersion, // file format description
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental export
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the export will be finished.
+
+ // } ----- end export methods -----
+
+ // { ----- begin token methods -----
+ NS_IMETHOD TokenToString( // return a string name for an integer token
+ nsIMdbEnv* ev, // context
+ mdb_token inToken, // token for inTokenName inside this port
+ mdbYarn* outTokenName) override; // the type of table to access
+
+ NS_IMETHOD StringToToken( // return an integer token for scope name
+ nsIMdbEnv* ev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) override; // token for inTokenName inside this port
+
+ // String token zero is never used and never supported. If the port
+ // is a mutable store, then StringToToken() to create a new
+ // association of inTokenName with a new integer token if possible.
+ // But a readonly port will return zero for an unknown scope name.
+
+ NS_IMETHOD QueryToken( // like StringToToken(), but without adding
+ nsIMdbEnv* ev, // context
+ const char* inTokenName, // Latin1 string to tokenize if possible
+ mdb_token* outToken) override; // token for inTokenName inside this port
+
+ // QueryToken() will return a string token if one already exists,
+ // but unlike StringToToken(), will not assign a new token if not
+ // already in use.
+
+ // } ----- end token methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD HasRow( // contains a row with the specified oid?
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_bool* outHasRow) override; // whether GetRow() might succeed
+
+ NS_IMETHOD GetRowRefCount( // get number of tables that contain a row
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ mdb_count* outRefCount) override; // number of tables containing inRowKey
+
+ NS_IMETHOD GetRow( // access one row with specific oid
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical row oid
+ nsIMdbRow** acqRow) override; // acquire specific row (or null)
+
+ NS_IMETHOD FindRow(
+ nsIMdbEnv* ev, // search for row with matching cell
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_column inColumn, // the column to search (and maintain an index)
+ const mdbYarn* inTargetCellValue, // cell value for which to search
+ mdbOid* outRowOid, // out row oid on match (or {0,-1} for no match)
+ nsIMdbRow** acqRow)
+ override; // acquire matching row (or nil for no match)
+ // can be null if you only want the oid
+ // FindRow() searches for one row that has a cell in column inColumn with
+ // a contained value with the same form (i.e. charset) and is byte-wise
+ // identical to the blob described by yarn inTargetCellValue. Both content
+ // and form of the yarn must be an exact match to find a matching row.
+ //
+ // (In other words, both a yarn's blob bytes and form are significant. The
+ // form is not expected to vary in columns used for identity anyway. This
+ // is intended to make the cost of FindRow() cheaper for MDB implementors,
+ // since any cell value atomization performed internally must necessarily
+ // make yarn form significant in order to avoid data loss in atomization.)
+ //
+ // FindRow() can lazily create an index on attribute inColumn for all rows
+ // with that attribute in row space scope inRowScope, so that subsequent
+ // calls to FindRow() will perform faster. Such an index might or might
+ // not be persistent (but this seems desirable if it is cheap to do so).
+ // Note that lazy index creation in readonly DBs is not very feasible.
+ //
+ // This FindRow() interface assumes that attribute inColumn is effectively
+ // an alternative means of unique identification for a row in a rowspace,
+ // so correct behavior is only guaranteed when no duplicates for this col
+ // appear in the given set of rows. (If more than one row has the same cell
+ // value in this column, no more than one will be found; and cutting one of
+ // two duplicate rows can cause the index to assume no other such row lives
+ // in the row space, so future calls return nil for negative search results
+ // even though some duplicate row might still live within the rowspace.)
+ //
+ // In other words, the FindRow() implementation is allowed to assume simple
+ // hash tables mapping unique column keys to associated row values will be
+ // sufficient, where any duplication is not recorded because only one copy
+ // of a given key need be remembered. Implementors are not required to sort
+ // all rows by the specified column.
+ // } ----- end row methods -----
+
+ // { ----- begin table methods -----
+ NS_IMETHOD HasTable( // supports a table with the specified oid?
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ mdb_bool* outHasTable) override; // whether GetTable() might succeed
+
+ NS_IMETHOD GetTable( // access one table with specific oid
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // hypothetical table oid
+ nsIMdbTable** acqTable) override; // acquire specific table (or null)
+
+ NS_IMETHOD HasTableKind( // supports a table of the specified type?
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // rid scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outSupportsTable)
+ override; // whether GetTableKind() might succeed
+
+ NS_IMETHOD GetTableKind( // access one (random) table of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_count* outTableCount, // current number of such tables
+ mdb_bool* outMustBeUnique, // whether port can hold only one of these
+ nsIMdbTable** acqTable) override; // acquire scoped collection of rows
+
+ NS_IMETHOD
+ GetPortTableCursor( // get cursor for all tables of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ nsIMdbPortTableCursor** acqCursor)
+ override; // all such tables in the port
+ // } ----- end table methods -----
+
+ // { ----- begin commit methods -----
+
+ NS_IMETHOD ShouldCompress( // store wastes at least inPercentWaste?
+ nsIMdbEnv* ev, // context
+ mdb_percent inPercentWaste, // 0..100 percent file size waste threshold
+ mdb_percent* outActualWaste, // 0..100 percent of file actually wasted
+ mdb_bool* outShould)
+ override; // true when about inPercentWaste% is wasted
+ // ShouldCompress() returns true if the store can determine that the file
+ // will shrink by an estimated percentage of inPercentWaste% (or more) if
+ // CompressCommit() is called, because that percentage of the file seems
+ // to be recoverable free space. The granularity is only in terms of
+ // percentage points, and any value over 100 is considered equal to 100.
+ //
+ // If a store only has an approximate idea how much space might be saved
+ // during a compress, then a best guess should be made. For example, the
+ // Mork implementation might keep track of how much file space began with
+ // text content before the first updating transaction, and then consider
+ // all content following the start of the first transaction as potentially
+ // wasted space if it is all updates and not just new content. (This is
+ // a safe assumption in the sense that behavior will stabilize on a low
+ // estimate of wastage after a commit removes all transaction updates.)
+ //
+ // Some db formats might attempt to keep a very accurate reckoning of free
+ // space size, so a very accurate determination can be made. But other db
+ // formats might have difficulty determining size of free space, and might
+ // require some lengthy calculation to answer. This is the reason for
+ // passing in the percentage threshold of interest, so that such lengthy
+ // computations can terminate early as soon as at least inPercentWaste is
+ // found, so that the entire file need not be groveled when unnecessary.
+ // However, we hope implementations will always favor fast but imprecise
+ // heuristic answers instead of extremely slow but very precise answers.
+ //
+ // If the outActualWaste parameter is non-nil, it will be used to return
+ // the actual estimated space wasted as a percentage of file size. (This
+ // parameter is provided so callers need not call repeatedly with altered
+ // inPercentWaste values to isolate the actual wastage figure.) Note the
+ // actual wastage figure returned can exactly equal inPercentWaste even
+ // when this grossly underestimates the real figure involved, if the db
+ // finds it very expensive to determine the extent of wastage after it is
+ // known to at least exceed inPercentWaste. Note we expect that whenever
+ // outShould returns true, that outActualWaste returns >= inPercentWaste.
+ //
+ // The effect of different inPercentWaste values is not very uniform over
+ // the permitted range. For example, 50 represents 50% wastage, or a file
+ // that is about double what it should be ideally. But 99 represents 99%
+ // wastage, or a file that is about ninety-nine times as big as it should
+ // be ideally. In the smaller direction, 25 represents 25% wastage, or
+ // a file that is only 33% larger than it should be ideally.
+ //
+ // Callers can determine what policy they want to use for considering when
+ // a file holds too much wasted space, and express this as a percentage
+ // of total file size to pass as in the inPercentWaste parameter. A zero
+ // likely returns always trivially true, and 100 always trivially false.
+ // The great majority of callers are expected to use values from 25 to 75,
+ // since most plausible thresholds for compressing might fall between the
+ // extremes of 133% of ideal size and 400% of ideal size. (Presumably the
+ // larger a file gets, the more important the percentage waste involved, so
+ // a sliding scale for compress thresholds might use smaller numbers for
+ // much bigger file sizes.)
+
+ // } ----- end commit methods -----
+
+ // } ===== end nsIMdbPort methods =====
+
+ // { ===== begin nsIMdbStore methods =====
+
+ // { ----- begin table methods -----
+ NS_IMETHOD NewTable( // make one new table of specific type
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) override; // acquire scoped collection of rows
+
+ NS_IMETHOD NewTableWithOid( // make one new table of specific type
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // caller assigned oid
+ mdb_kind inTableKind, // the type of table to access
+ mdb_bool inMustBeUnique, // whether store can hold only one of these
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ nsIMdbTable** acqTable) override; // acquire scoped collection of rows
+ // } ----- end table methods -----
+
+ // { ----- begin row scope methods -----
+ NS_IMETHOD RowScopeHasAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned)
+ override; // nonzero if store db assigned specified
+
+ NS_IMETHOD SetCallerAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned)
+ override; // nonzero if store db assigned specified
+
+ NS_IMETHOD SetStoreAssignedIds(
+ nsIMdbEnv* ev,
+ mdb_scope inRowScope, // row scope for row ids
+ mdb_bool* outCallerAssigned, // nonzero if caller assigned specified
+ mdb_bool* outStoreAssigned)
+ override; // nonzero if store db assigned specified
+ // } ----- end row scope methods -----
+
+ // { ----- begin row methods -----
+ NS_IMETHOD NewRowWithOid(nsIMdbEnv* ev, // new row w/ caller assigned oid
+ const mdbOid* inOid, // caller assigned oid
+ nsIMdbRow** acqRow) override; // create new row
+
+ NS_IMETHOD NewRow(nsIMdbEnv* ev, // new row with db assigned oid
+ mdb_scope inRowScope, // row scope for row ids
+ nsIMdbRow** acqRow) override; // create new row
+ // Note this row must be added to some table or cell child before the
+ // store is closed in order to make this row persist across sessions.
+
+ // } ----- end row methods -----
+
+ // { ----- begin import/export methods -----
+ NS_IMETHOD ImportContent( // import content from port
+ nsIMdbEnv* ev, // context
+ mdb_scope inRowScope, // scope for rows (or zero for all?)
+ nsIMdbPort* ioPort, // the port with content to add to store
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental import
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the import will be finished.
+
+ NS_IMETHOD ImportFile( // import content from port
+ nsIMdbEnv* ev, // context
+ nsIMdbFile* ioFile, // the file with content to add to store
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental import
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the import will be finished.
+ // } ----- end import/export methods -----
+
+ // { ----- begin hinting methods -----
+ NS_IMETHOD
+ ShareAtomColumnsHint( // advise re shared column content atomizing
+ nsIMdbEnv* ev, // context
+ mdb_scope inScopeHint, // zero, or suggested shared namespace
+ const mdbColumnSet* inColumnSet)
+ override; // cols desired tokenized together
+
+ NS_IMETHOD
+ AvoidAtomColumnsHint( // advise column with poor atomizing prospects
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet)
+ override; // cols with poor atomizing prospects
+ // } ----- end hinting methods -----
+
+ // { ----- begin commit methods -----
+ NS_IMETHOD LargeCommit( // save important changes if at all possible
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ NS_IMETHOD SessionCommit( // save all changes if large commits delayed
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ NS_IMETHOD
+ CompressCommit( // commit and make db physically smaller if possible
+ nsIMdbEnv* ev, // context
+ nsIMdbThumb** acqThumb) override; // acquire thumb for incremental commit
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the commit will be finished. Note the store is effectively write
+ // locked until commit is finished or canceled through the thumb instance.
+ // Until the commit is done, the store will report it has readonly status.
+
+ // } ----- end commit methods -----
+
+ // } ===== end nsIMdbStore methods =====
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakPort(morkPort* me, morkEnv* ev, morkPort** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongPort(morkPort* me, morkEnv* ev, morkPort** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+ // public: // slots inherited from morkPort (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkEnv* mPort_Env; // non-refcounted env which created port
+ // morkFactory* mPort_Factory; // weak ref to suite factory
+ // nsIMdbHeap* mPort_Heap; // heap in which this port allocs objects
+
+ public: // state is public because the entire Mork system is private
+ // mStore_OidAtomSpace might be unnecessary; I don't remember why I wanted it.
+ morkAtomSpace* mStore_OidAtomSpace; // ground atom space for oids
+ morkAtomSpace* mStore_GroundAtomSpace; // ground atom space for scopes
+ morkAtomSpace* mStore_GroundColumnSpace; // ground column space for scopes
+
+ nsIMdbFile* mStore_File; // the file containing Mork text
+ morkStream* mStore_InStream; // stream using file used by the builder
+ morkBuilder* mStore_Builder; // to parse Mork text and build structures
+
+ morkStream* mStore_OutStream; // stream using file used by the writer
+
+ morkRowSpaceMap mStore_RowSpaces; // maps mork_scope -> morkSpace
+ morkAtomSpaceMap mStore_AtomSpaces; // maps mork_scope -> morkSpace
+
+ morkZone mStore_Zone;
+
+ morkPool mStore_Pool;
+
+ // we alloc a max size book atom to reuse space for atom map key searches:
+ // morkMaxBookAtom mStore_BookAtom; // staging area for atom map searches
+
+ morkFarBookAtom mStore_FarBookAtom; // staging area for atom map searches
+
+ // GroupIdentity should be one more than largest seen in a parsed db file:
+ mork_gid mStore_CommitGroupIdentity; // transaction ID number
+
+ // group positions are used to help compute PercentOfStoreWasted():
+ mork_pos mStore_FirstCommitGroupPos; // start of first group
+ mork_pos mStore_SecondCommitGroupPos; // start of second group
+ // If the first commit group is very near the start of the file (say less
+ // than 512 bytes), then we might assume the file started nearly empty and
+ // that most of the first group is not wasted. In that case, the pos of
+ // the second commit group might make a better estimate of the start of
+ // transaction space that might represent wasted file space. That's why
+ // we support fields for both first and second commit group positions.
+ //
+ // We assume that a zero in either group pos means that the slot has not
+ // yet been given a valid value, since the file will always start with a
+ // tag, and a commit group cannot actually start at position zero.
+ //
+ // Either or both the first or second commit group positions might be
+ // supplied by either morkWriter (while committing) or morkBuilder (while
+ // parsing), since either reading or writing the file might encounter the
+ // first transaction groups which came into existence either in the past
+ // or in the very recent present.
+
+ mork_bool mStore_CanAutoAssignAtomIdentity;
+ mork_bool mStore_CanDirty; // changes imply the store becomes dirty?
+ mork_u1 mStore_CanWriteIncremental; // compress not required?
+ mork_u1 mStore_Pad; // for u4 alignment
+
+ // mStore_CanDirty should be FALSE when parsing a file while building the
+ // content going into the store, because such data structure modifications
+ // are actuallly in sync with the file. So content read from a file must
+ // be clean with respect to the file. After a file is finished parsing,
+ // the mStore_CanDirty slot should become TRUE, so that any additional
+ // changes at runtime cause structures to be marked dirty with respect to
+ // the file which must later be updated with changes during a commit.
+ //
+ // It might also make sense to set mStore_CanDirty to FALSE while a commit
+ // is in progress, lest some internal transformations make more content
+ // appear dirty when it should not. So anyone modifying content during a
+ // commit should think about the intended significance regarding dirty.
+
+ public: // more specific dirty methods for store:
+ void SetStoreDirty() { this->SetNodeDirty(); }
+ void SetStoreClean() { this->SetNodeClean(); }
+
+ mork_bool IsStoreClean() const { return this->IsNodeClean(); }
+ mork_bool IsStoreDirty() const { return this->IsNodeDirty(); }
+
+ public: // setting dirty based on CanDirty:
+ void MaybeDirtyStore() {
+ if (mStore_CanDirty) this->SetStoreDirty();
+ }
+
+ public: // space waste analysis
+ mork_percent PercentOfStoreWasted(morkEnv* ev);
+
+ public: // setting store and all subspaces canDirty:
+ void SetStoreAndAllSpacesCanDirty(morkEnv* ev, mork_bool inCanDirty);
+
+ public: // building an atom inside mStore_FarBookAtom from a char* string
+ morkFarBookAtom* StageAliasAsFarBookAtom(morkEnv* ev, const morkMid* inMid,
+ morkAtomSpace* ioSpace,
+ mork_cscode inForm);
+
+ morkFarBookAtom* StageYarnAsFarBookAtom(morkEnv* ev, const mdbYarn* inYarn,
+ morkAtomSpace* ioSpace);
+
+ morkFarBookAtom* StageStringAsFarBookAtom(morkEnv* ev, const char* inString,
+ mork_cscode inForm,
+ morkAtomSpace* ioSpace);
+
+ public: // determining whether incremental writing is a good use of time:
+ mork_bool DoPreferLargeOverCompressCommit(morkEnv* ev);
+ // true when mStore_CanWriteIncremental && store has file large enough
+
+ public: // lazy creation of members and nested row or atom spaces
+ morkAtomSpace* LazyGetOidAtomSpace(morkEnv* ev);
+ morkAtomSpace* LazyGetGroundAtomSpace(morkEnv* ev);
+ morkAtomSpace* LazyGetGroundColumnSpace(morkEnv* ev);
+
+ morkStream* LazyGetInStream(morkEnv* ev);
+ morkBuilder* LazyGetBuilder(morkEnv* ev);
+ void ForgetBuilder(morkEnv* ev);
+
+ morkStream* LazyGetOutStream(morkEnv* ev);
+
+ morkRowSpace* LazyGetRowSpace(morkEnv* ev, mdb_scope inRowScope);
+ morkAtomSpace* LazyGetAtomSpace(morkEnv* ev, mdb_scope inAtomScope);
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseStore() only if open
+
+ public: // morkStore construction & destruction
+ morkStore(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioNodeHeap, // the heap (if any) for this node instance
+ morkFactory* inFactory, // the factory for this
+ nsIMdbHeap* ioPortHeap // the heap to hold all content in the port
+ );
+ void CloseStore(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkStore(const morkStore& other);
+ morkStore& operator=(const morkStore& other);
+ virtual ~morkStore(); // assert that CloseStore() executed earlier
+
+ public: // dynamic type identification
+ morkEnv* CanUseStore(nsIMdbEnv* mev, mork_bool inMutable,
+ nsresult* outErr) const;
+ mork_bool IsStore() const {
+ return IsNode() && mNode_Derived == morkDerived_kStore;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonStoreTypeError(morkEnv* ev);
+ static void NilStoreFileError(morkEnv* ev);
+ static void CannotAutoAssignAtomIdentityError(morkEnv* ev);
+
+ public: // store utilities
+ morkAtom* YarnToAtom(morkEnv* ev, const mdbYarn* inYarn,
+ bool createIfMissing = true);
+ morkAtom* AddAlias(morkEnv* ev, const morkMid& inMid, mork_cscode inForm);
+
+ public: // other store methods
+ void RenumberAllCollectableContent(morkEnv* ev);
+
+ nsIMdbStore* AcquireStoreHandle(morkEnv* ev); // mObject_Handle
+
+ morkPool* StorePool() { return &mStore_Pool; }
+
+ mork_bool OpenStoreFile(morkEnv* ev, // return value equals ev->Good()
+ mork_bool inFrozen,
+ // const char* inFilePath,
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy);
+
+ mork_bool CreateStoreFile(morkEnv* ev, // return value equals ev->Good()
+ // const char* inFilePath,
+ nsIMdbFile* ioFile, // db abstract file interface
+ const mdbOpenPolicy* inOpenPolicy);
+
+ morkAtom* CopyAtom(morkEnv* ev, const morkAtom* inAtom);
+ // copy inAtom (from some other store) over to this store
+
+ mork_token CopyToken(morkEnv* ev, mdb_token inToken, morkStore* inStore);
+ // copy inToken from inStore over to this store
+
+ mork_token BufToToken(morkEnv* ev, const morkBuf* inBuf);
+ mork_token StringToToken(morkEnv* ev, const char* inTokenName);
+ mork_token QueryToken(morkEnv* ev, const char* inTokenName);
+ void TokenToString(morkEnv* ev, mdb_token inToken, mdbYarn* outTokenName);
+
+ mork_bool MidToOid(morkEnv* ev, const morkMid& inMid, mdbOid* outOid);
+ mork_bool OidToYarn(morkEnv* ev, const mdbOid& inOid, mdbYarn* outYarn);
+ mork_bool MidToYarn(morkEnv* ev, const morkMid& inMid, mdbYarn* outYarn);
+
+ morkBookAtom* MidToAtom(morkEnv* ev, const morkMid& inMid);
+ morkRow* MidToRow(morkEnv* ev, const morkMid& inMid);
+ morkTable* MidToTable(morkEnv* ev, const morkMid& inMid);
+
+ morkRow* OidToRow(morkEnv* ev, const mdbOid* inOid);
+ // OidToRow() finds old row with oid, or makes new one if not found.
+
+ morkTable* OidToTable(morkEnv* ev, const mdbOid* inOid,
+ const mdbOid* inOptionalMetaRowOid);
+ // OidToTable() finds old table with oid, or makes new one if not found.
+
+ static void SmallTokenToOneByteYarn(morkEnv* ev, mdb_token inToken,
+ mdbYarn* outYarn);
+
+ mork_bool HasTableKind(morkEnv* ev, mdb_scope inRowScope,
+ mdb_kind inTableKind, mdb_count* outTableCount);
+
+ morkTable* GetTableKind(morkEnv* ev, mdb_scope inRowScope,
+ mdb_kind inTableKind, mdb_count* outTableCount,
+ mdb_bool* outMustBeUnique);
+
+ morkRow* FindRow(morkEnv* ev, mdb_scope inScope, mdb_column inColumn,
+ const mdbYarn* inTargetCellValue);
+
+ morkRow* GetRow(morkEnv* ev, const mdbOid* inOid);
+ morkTable* GetTable(morkEnv* ev, const mdbOid* inOid);
+
+ morkTable* NewTable(morkEnv* ev, mdb_scope inRowScope, mdb_kind inTableKind,
+ mdb_bool inMustBeUnique,
+ const mdbOid* inOptionalMetaRowOid);
+
+ morkPortTableCursor* GetPortTableCursor(morkEnv* ev, mdb_scope inRowScope,
+ mdb_kind inTableKind);
+
+ morkRow* NewRowWithOid(morkEnv* ev, const mdbOid* inOid);
+ morkRow* NewRow(morkEnv* ev, mdb_scope inRowScope);
+
+ morkThumb* MakeCompressCommitThumb(morkEnv* ev, mork_bool inDoCollect);
+
+ public: // commit related methods
+ mork_bool MarkAllStoreContentDirty(morkEnv* ev);
+ // MarkAllStoreContentDirty() visits every object in the store and marks
+ // them dirty, including every table, row, cell, and atom. The return
+ // equals ev->Good(), to show whether any error happened. This method is
+ // intended for use in the beginning of a "compress commit" which writes
+ // all store content, whether dirty or not. We dirty everything first so
+ // that later iterations over content can mark things clean as they are
+ // written, and organize the process of serialization so that objects are
+ // written only at need (because of being dirty).
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakStore(morkStore* me, morkEnv* ev, morkStore** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongStore(morkStore* me, morkEnv* ev, morkStore** ioSlot) {
+ morkStore* store = *ioSlot;
+ if (me != store) {
+ if (store) {
+ // what if this nulls out the ev and causes asserts?
+ // can we move this after the CutStrongRef()?
+ *ioSlot = 0;
+ store->Release();
+ }
+ if (me && me->AddRef()) *ioSlot = me;
+ }
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKSTORE_ */
diff --git a/comm/mailnews/db/mork/morkStream.cpp b/comm/mailnews/db/mork/morkStream.cpp
new file mode 100644
index 0000000000..23fd7b91ae
--- /dev/null
+++ b/comm/mailnews/db/mork/morkStream.cpp
@@ -0,0 +1,790 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKFILE_
+# include "morkFile.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKSTREAM_
+# include "morkStream.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkStream::CloseMorkNode(
+ morkEnv* ev) // CloseStream() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseStream(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkStream::~morkStream() // assert CloseStream() executed earlier
+{
+ MORK_ASSERT(mStream_ContentFile == 0);
+ MORK_ASSERT(mStream_Buf == 0);
+}
+
+/*public non-poly*/
+morkStream::morkStream(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbFile* ioContentFile,
+ mork_size inBufSize, mork_bool inFrozen)
+ : morkFile(ev, inUsage, ioHeap, ioHeap),
+ mStream_At(0),
+ mStream_ReadEnd(0),
+ mStream_WriteEnd(0)
+
+ ,
+ mStream_ContentFile(0)
+
+ ,
+ mStream_Buf(0),
+ mStream_BufSize(inBufSize),
+ mStream_BufPos(0),
+ mStream_Dirty(morkBool_kFalse),
+ mStream_HitEof(morkBool_kFalse) {
+ if (ev->Good()) {
+ if (inBufSize < morkStream_kMinBufSize)
+ mStream_BufSize = inBufSize = morkStream_kMinBufSize;
+ else if (inBufSize > morkStream_kMaxBufSize)
+ mStream_BufSize = inBufSize = morkStream_kMaxBufSize;
+
+ if (ioContentFile && ioHeap) {
+ // if ( ioContentFile->FileFrozen() ) // forced to be readonly?
+ // inFrozen = morkBool_kTrue; // override the input value
+
+ nsIMdbFile_SlotStrongFile(ioContentFile, ev, &mStream_ContentFile);
+ if (ev->Good()) {
+ mork_u1* buf = 0;
+ ioHeap->Alloc(ev->AsMdbEnv(), inBufSize, (void**)&buf);
+ if (buf) {
+ mStream_At = mStream_Buf = buf;
+
+ if (!inFrozen) {
+ // physical buffer end never moves:
+ mStream_WriteEnd = buf + inBufSize;
+ } else
+ mStream_WriteEnd = 0; // no writing is allowed
+
+ if (inFrozen) {
+ // logical buffer end starts at Buf with no content:
+ mStream_ReadEnd = buf;
+ this->SetFileFrozen(inFrozen);
+ } else
+ mStream_ReadEnd = 0; // no reading is allowed
+
+ this->SetFileActive(morkBool_kTrue);
+ this->SetFileIoOpen(morkBool_kTrue);
+ }
+ if (ev->Good()) mNode_Derived = morkDerived_kStream;
+ }
+ } else
+ ev->NilPointerError();
+ }
+}
+
+/*public non-poly*/ void morkStream::CloseStream(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mStream_ContentFile);
+ nsIMdbHeap* heap = mFile_SlotHeap;
+ mork_u1* buf = mStream_Buf;
+ mStream_Buf = 0;
+
+ if (heap && buf) heap->Free(ev->AsMdbEnv(), buf);
+
+ this->CloseFile(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+#define morkStream_kSpacesPerIndent 1 /* one space per indent */
+#define morkStream_kMaxIndentDepth 70 /* max indent of 70 space bytes */
+static const char morkStream_kSpaces[] // next line to ease length perception
+ = " "
+ " ";
+// 123456789_123456789_123456789_123456789_123456789_123456789_123456789_
+// morkStream_kSpaces above must contain (at least) 70 spaces (ASCII 0x20)
+
+mork_size morkStream::PutIndent(morkEnv* ev, mork_count inDepth)
+// PutIndent() puts a linebreak, and then
+// "indents" by inDepth, and returns the line length after indentation.
+{
+ mork_size outLength = 0;
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+ if (ev->Good()) {
+ this->PutLineBreak(ev);
+ if (ev->Good()) {
+ outLength = inDepth;
+ mdb_size bytesWritten;
+ if (inDepth) this->Write(mev, morkStream_kSpaces, inDepth, &bytesWritten);
+ }
+ }
+ return outLength;
+}
+
+mork_size morkStream::PutByteThenIndent(morkEnv* ev, int inByte,
+ mork_count inDepth)
+// PutByteThenIndent() puts the byte, then a linebreak, and then
+// "indents" by inDepth, and returns the line length after indentation.
+{
+ mork_size outLength = 0;
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+
+ if (inDepth > morkStream_kMaxIndentDepth)
+ inDepth = morkStream_kMaxIndentDepth;
+
+ this->Putc(ev, inByte);
+ if (ev->Good()) {
+ this->PutLineBreak(ev);
+ if (ev->Good()) {
+ outLength = inDepth;
+ mdb_size bytesWritten;
+ if (inDepth) this->Write(mev, morkStream_kSpaces, inDepth, &bytesWritten);
+ }
+ }
+ return outLength;
+}
+
+mork_size morkStream::PutStringThenIndent(morkEnv* ev, const char* inString,
+ mork_count inDepth)
+// PutStringThenIndent() puts the string, then a linebreak, and then
+// "indents" by inDepth, and returns the line length after indentation.
+{
+ mork_size outLength = 0;
+ mdb_size bytesWritten;
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+
+ if (inDepth > morkStream_kMaxIndentDepth)
+ inDepth = morkStream_kMaxIndentDepth;
+
+ if (inString) {
+ mork_size length = strlen(inString);
+ if (length && ev->Good()) // any bytes to write?
+ this->Write(mev, inString, length, &bytesWritten);
+ }
+
+ if (ev->Good()) {
+ this->PutLineBreak(ev);
+ if (ev->Good()) {
+ outLength = inDepth;
+ if (inDepth) this->Write(mev, morkStream_kSpaces, inDepth, &bytesWritten);
+ }
+ }
+ return outLength;
+}
+
+mork_size morkStream::PutString(morkEnv* ev, const char* inString) {
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+ mork_size outSize = 0;
+ mdb_size bytesWritten;
+ if (inString) {
+ outSize = strlen(inString);
+ if (outSize && ev->Good()) // any bytes to write?
+ {
+ this->Write(mev, inString, outSize, &bytesWritten);
+ }
+ }
+ return outSize;
+}
+
+mork_size morkStream::PutStringThenNewline(morkEnv* ev, const char* inString)
+// PutStringThenNewline() returns total number of bytes written.
+{
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+ mork_size outSize = 0;
+ mdb_size bytesWritten;
+ if (inString) {
+ outSize = strlen(inString);
+ if (outSize && ev->Good()) // any bytes to write?
+ {
+ this->Write(mev, inString, outSize, &bytesWritten);
+ if (ev->Good()) outSize += this->PutLineBreak(ev);
+ }
+ }
+ return outSize;
+}
+
+mork_size morkStream::PutByteThenNewline(morkEnv* ev, int inByte)
+// PutByteThenNewline() returns total number of bytes written.
+{
+ mork_size outSize = 1; // one for the following byte
+ this->Putc(ev, inByte);
+ if (ev->Good()) outSize += this->PutLineBreak(ev);
+ return outSize;
+}
+
+mork_size morkStream::PutLineBreak(morkEnv* ev) {
+#if defined(MORK_MAC)
+
+ this->Putc(ev, mork_kCR);
+ return 1;
+
+#else
+# if defined(MORK_WIN)
+
+ this->Putc(ev, mork_kCR);
+ this->Putc(ev, mork_kLF);
+ return 2;
+
+# else
+# ifdef MORK_UNIX
+
+ this->Putc(ev, mork_kLF);
+ return 1;
+
+# endif /* MORK_UNIX */
+# endif /* MORK_WIN */
+#endif /* MORK_MAC */
+}
+// ````` ````` ````` ````` ````` ````` ````` `````
+// public: // virtual morkFile methods
+
+NS_IMETHODIMP
+morkStream::Steal(nsIMdbEnv* mev, nsIMdbFile* ioThief)
+// Steal: tell this file to close any associated i/o stream in the file
+// system, because the file ioThief intends to reopen the file in order
+// to provide the MDB implementation with more exotic file access than is
+// offered by the nsIMdbFile alone. Presumably the thief knows enough
+// from Path() in order to know which file to reopen. If Steal() is
+// successful, this file should probably delegate all future calls to
+// the nsIMdbFile interface down to the thief files, so that even after
+// the file has been stolen, it can still be read, written, or forcibly
+// closed (by a call to CloseMdbObject()).
+{
+ MORK_USED_1(ioThief);
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ ev->StubMethodOnlyError();
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkStream::BecomeTrunk(nsIMdbEnv* mev)
+// If this file is a file version branch created by calling AcquireBud(),
+// BecomeTrunk() causes this file's content to replace the original
+// file's content, typically by assuming the original file's identity.
+{
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ ev->StubMethodOnlyError();
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkStream::AcquireBud(nsIMdbEnv* mev, nsIMdbHeap* ioHeap, nsIMdbFile** acqBud)
+// AcquireBud() starts a new "branch" version of the file, empty of content,
+// so that a new version of the file can be written. This new file
+// can later be told to BecomeTrunk() the original file, so the branch
+// created by budding the file will replace the original file. Some
+// file subclasses might initially take the unsafe but expedient
+// approach of simply truncating this file down to zero length, and
+// then returning the same morkFile pointer as this, with an extra
+// reference count increment. Note that the caller of AcquireBud() is
+// expected to eventually call CutStrongRef() on the returned file
+// in order to release the strong reference. High quality versions
+// of morkFile subclasses will create entirely new files which later
+// are renamed to become the old file, so that better transactional
+// behavior is exhibited by the file, so crashes protect old files.
+// Note that AcquireBud() is an illegal operation on readonly files.
+{
+ MORK_USED_1(ioHeap);
+ morkFile* outFile = 0;
+ nsIMdbFile* file = mStream_ContentFile;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (this->IsOpenAndActiveFile() && file) {
+ // figure out how this interacts with buffering and mStream_WriteEnd:
+ ev->StubMethodOnlyError();
+ } else
+ this->NewFileDownError(ev);
+
+ *acqBud = outFile;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+mork_pos morkStream::Length(morkEnv* ev) const // eof
+{
+ mork_pos outPos = 0;
+
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenAndActiveFile() && file) {
+ mork_pos contentEof = 0;
+ file->Eof(ev->AsMdbEnv(), &contentEof);
+ if (ev->Good()) {
+ if (mStream_WriteEnd) // this stream supports writing?
+ {
+ // the local buffer might have buffered content past content eof
+ if (ev->Good()) // no error happened during Length() above?
+ {
+ mork_u1* at = mStream_At;
+ mork_u1* buf = mStream_Buf;
+ if (at >= buf) // expected cursor order?
+ {
+ mork_pos localContent = mStream_BufPos + (at - buf);
+ if (localContent > contentEof) // buffered past eof?
+ contentEof = localContent; // return new logical eof
+
+ outPos = contentEof;
+ } else
+ this->NewBadCursorOrderError(ev);
+ }
+ } else
+ outPos = contentEof; // frozen files get length from content file
+ }
+ } else
+ this->NewFileDownError(ev);
+
+ return outPos;
+}
+
+void morkStream::NewBadCursorSlotsError(morkEnv* ev) const {
+ ev->NewError("bad stream cursor slots");
+}
+
+void morkStream::NewNullStreamBufferError(morkEnv* ev) const {
+ ev->NewError("null stream buffer");
+}
+
+void morkStream::NewCantReadSinkError(morkEnv* ev) const {
+ ev->NewError("can't read stream sink");
+}
+
+void morkStream::NewCantWriteSourceError(morkEnv* ev) const {
+ ev->NewError("can't write stream source");
+}
+
+void morkStream::NewPosBeyondEofError(morkEnv* ev) const {
+ ev->NewError("stream pos beyond eof");
+}
+
+void morkStream::NewBadCursorOrderError(morkEnv* ev) const {
+ ev->NewError("bad stream cursor order");
+}
+
+NS_IMETHODIMP
+morkStream::Tell(nsIMdbEnv* mdbev, mork_pos* aOutPos) const {
+ nsresult rv = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+
+ NS_ENSURE_ARG_POINTER(aOutPos);
+
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenAndActiveFile() && file) {
+ mork_u1* buf = mStream_Buf;
+ mork_u1* at = mStream_At;
+
+ mork_u1* readEnd = mStream_ReadEnd; // nonzero only if readonly
+ mork_u1* writeEnd = mStream_WriteEnd; // nonzero only if writeonly
+
+ if (writeEnd) {
+ if (buf && at >= buf && at <= writeEnd) {
+ *aOutPos = mStream_BufPos + (at - buf);
+ } else
+ this->NewBadCursorOrderError(ev);
+ } else if (readEnd) {
+ if (buf && at >= buf && at <= readEnd) {
+ *aOutPos = mStream_BufPos + (at - buf);
+ } else
+ this->NewBadCursorOrderError(ev);
+ }
+ } else
+ this->NewFileDownError(ev);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStream::Read(nsIMdbEnv* mdbev, void* outBuf, mork_size inSize,
+ mork_size* aOutSize) {
+ NS_ENSURE_ARG_POINTER(aOutSize);
+ // First we satisfy the request from buffered bytes, if any. Then
+ // if additional bytes are needed, we satisfy these by direct reads
+ // from the content file without any local buffering (but we still need
+ // to adjust the buffer position to reflect the current i/o point).
+
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+ nsresult rv = NS_OK;
+
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenAndActiveFile() && file) {
+ mork_u1* end = mStream_ReadEnd; // byte after last buffered byte
+ if (end) // file is open for read access?
+ {
+ if (inSize) // caller wants any output?
+ {
+ mork_u1* sink = (mork_u1*)outBuf; // where we plan to write bytes
+ if (sink) // caller passed good buffer address?
+ {
+ mork_u1* at = mStream_At;
+ mork_u1* buf = mStream_Buf;
+ if (at >= buf && at <= end) // expected cursor order?
+ {
+ mork_num remaining = (mork_num)(end - at); // bytes left in buffer
+
+ mork_num quantum = inSize; // number of bytes to copy
+ if (quantum > remaining) // more than buffer content?
+ quantum = remaining; // restrict to buffered bytes
+
+ if (quantum) // any bytes left in the buffer?
+ {
+ MORK_MEMCPY(sink, at, quantum); // from buffer bytes
+
+ at += quantum; // advance past read bytes
+ mStream_At = at;
+ *aOutSize += quantum; // this much copied so far
+
+ sink += quantum; // in case we need to copy more
+ inSize -= quantum; // filled this much of request
+ mStream_HitEof = morkBool_kFalse;
+ }
+
+ if (inSize) // we still need to read more content?
+ {
+ // We need to read more bytes directly from the
+ // content file, without local buffering. We have
+ // exhausted the local buffer, so we need to show
+ // it is now empty, and adjust the current buf pos.
+
+ mork_num posDelta = (mork_num)(at - buf); // old buf content
+ mStream_BufPos += posDelta; // past now empty buf
+
+ mStream_At = mStream_ReadEnd = buf; // empty buffer
+
+ // file->Seek(ev, mStream_BufPos); // set file pos
+ // if ( ev->Good() ) // no seek error?
+ // {
+ // }
+
+ mork_num actual = 0;
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ file->Get(menv, sink, inSize, mStream_BufPos, &actual);
+ if (ev->Good()) // no read error?
+ {
+ if (actual) {
+ *aOutSize += actual;
+ mStream_BufPos += actual;
+ mStream_HitEof = morkBool_kFalse;
+ } else if (!*aOutSize)
+ mStream_HitEof = morkBool_kTrue;
+ }
+ }
+ } else
+ this->NewBadCursorOrderError(ev);
+ } else
+ this->NewNullStreamBufferError(ev);
+ }
+ } else
+ this->NewCantReadSinkError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ if (ev->Bad()) *aOutSize = 0;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStream::Seek(nsIMdbEnv* mdbev, mork_pos inPos, mork_pos* aOutPos) {
+ NS_ENSURE_ARG_POINTER(aOutPos);
+ morkEnv* ev = morkEnv::FromMdbEnv(mdbev);
+ *aOutPos = 0;
+ nsresult rv = NS_OK;
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenOrClosingNode() && this->FileActive() && file) {
+ mork_u1* at = mStream_At; // current position in buffer
+ mork_u1* buf = mStream_Buf; // beginning of buffer
+ mork_u1* readEnd = mStream_ReadEnd; // nonzero only if readonly
+ mork_u1* writeEnd = mStream_WriteEnd; // nonzero only if writeonly
+
+ if (writeEnd) // file is mutable/writeonly?
+ {
+ if (mStream_Dirty) // need to commit buffer changes?
+ this->Flush(mdbev);
+
+ if (ev->Good()) // no errors during flush or earlier?
+ {
+ if (at == buf) // expected post flush cursor value?
+ {
+ if (mStream_BufPos != inPos) // need to change pos?
+ {
+ mork_pos eof = 0;
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ file->Eof(menv, &eof);
+ if (ev->Good()) // no errors getting length?
+ {
+ if (inPos <= eof) // acceptable new position?
+ {
+ mStream_BufPos = inPos; // new stream position
+ *aOutPos = inPos;
+ } else
+ this->NewPosBeyondEofError(ev);
+ }
+ }
+ } else
+ this->NewBadCursorOrderError(ev);
+ }
+ } else if (readEnd) // file is frozen/readonly?
+ {
+ if (at >= buf && at <= readEnd) // expected cursor order?
+ {
+ mork_pos eof = 0;
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ file->Eof(menv, &eof);
+ if (ev->Good()) // no errors getting length?
+ {
+ if (inPos <= eof) // acceptable new position?
+ {
+ *aOutPos = inPos;
+ mStream_BufPos = inPos; // new stream position
+ mStream_At = mStream_ReadEnd = buf; // empty buffer
+ if (inPos == eof) // notice eof reached?
+ mStream_HitEof = morkBool_kTrue;
+ } else
+ this->NewPosBeyondEofError(ev);
+ }
+ } else
+ this->NewBadCursorOrderError(ev);
+ }
+
+ } else
+ this->NewFileDownError(ev);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+morkStream::Write(nsIMdbEnv* menv, const void* inBuf, mork_size inSize,
+ mork_size* aOutSize) {
+ mork_num outActual = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(menv);
+
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenActiveAndMutableFile() && file) {
+ mork_u1* end = mStream_WriteEnd; // byte after last buffered byte
+ if (end) // file is open for write access?
+ {
+ if (inSize) // caller provided any input?
+ {
+ const mork_u1* source = (const mork_u1*)inBuf; // from where
+ if (source) // caller passed good buffer address?
+ {
+ mork_u1* at = mStream_At;
+ mork_u1* buf = mStream_Buf;
+ if (at >= buf && at <= end) // expected cursor order?
+ {
+ mork_num space = (mork_num)(end - at); // space left in buffer
+
+ mork_num quantum = inSize; // number of bytes to write
+ if (quantum > space) // more than buffer size?
+ quantum = space; // restrict to avail space
+
+ if (quantum) // any space left in the buffer?
+ {
+ mStream_Dirty = morkBool_kTrue; // to ensure later flush
+ MORK_MEMCPY(at, source, quantum); // into buffer
+
+ mStream_At += quantum; // advance past written bytes
+ outActual += quantum; // this much written so far
+
+ source += quantum; // in case we need to write more
+ inSize -= quantum; // filled this much of request
+ }
+
+ if (inSize) // we still need to write more content?
+ {
+ // We need to write more bytes directly to the
+ // content file, without local buffering. We have
+ // exhausted the local buffer, so we need to flush
+ // it and empty it, and adjust the current buf pos.
+ // After flushing, if the rest of the write fits
+ // inside the buffer, we will put bytes into the
+ // buffer rather than write them to content file.
+
+ if (mStream_Dirty)
+ this->Flush(menv); // will update mStream_BufPos
+
+ at = mStream_At;
+ if (at < buf || at > end) // bad cursor?
+ this->NewBadCursorOrderError(ev);
+
+ if (ev->Good()) // no errors?
+ {
+ space = (mork_num)(end - at); // space left in buffer
+ if (space > inSize) // write to buffer?
+ {
+ mStream_Dirty = morkBool_kTrue; // ensure flush
+ MORK_MEMCPY(at, source, inSize); // copy
+
+ mStream_At += inSize; // past written bytes
+ outActual += inSize; // this much written
+ } else // directly to content file instead
+ {
+ // file->Seek(ev, mStream_BufPos); // set pos
+ // if ( ev->Good() ) // no seek error?
+ // {
+ // }
+
+ mork_num actual = 0;
+ file->Put(menv, source, inSize, mStream_BufPos, &actual);
+ if (ev->Good()) // no write error?
+ {
+ outActual += actual;
+ mStream_BufPos += actual;
+ }
+ }
+ }
+ }
+ } else
+ this->NewBadCursorOrderError(ev);
+ } else
+ this->NewNullStreamBufferError(ev);
+ }
+ } else
+ this->NewCantWriteSourceError(ev);
+ } else
+ this->NewFileDownError(ev);
+
+ if (ev->Bad()) outActual = 0;
+
+ *aOutSize = outActual;
+ return ev->AsErr();
+}
+
+NS_IMETHODIMP
+morkStream::Flush(nsIMdbEnv* ev) {
+ morkEnv* mev = morkEnv::FromMdbEnv(ev);
+ nsresult rv = NS_ERROR_FAILURE;
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenOrClosingNode() && this->FileActive() && file) {
+ if (mStream_Dirty) this->spill_buf(mev);
+
+ rv = file->Flush(ev);
+ } else
+ this->NewFileDownError(mev);
+ return rv;
+}
+
+// ````` ````` ````` ````` ````` ````` ````` `````
+// protected: // protected non-poly morkStream methods (for char io)
+
+int morkStream::fill_getc(morkEnv* ev) {
+ int c = EOF;
+
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenAndActiveFile() && file) {
+ mork_u1* buf = mStream_Buf;
+ mork_u1* end = mStream_ReadEnd; // beyond buf after earlier read
+ if (end > buf) // any earlier read bytes buffered?
+ {
+ mStream_BufPos += (end - buf); // advance past old read
+ }
+
+ if (ev->Good()) // no errors yet?
+ {
+ // file->Seek(ev, mStream_BufPos); // set file pos
+ // if ( ev->Good() ) // no seek error?
+ // {
+ // }
+
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ mork_num actual = 0;
+ file->Get(menv, buf, mStream_BufSize, mStream_BufPos, &actual);
+ if (ev->Good()) // no read errors?
+ {
+ if (actual > mStream_BufSize) // more than asked for??
+ actual = mStream_BufSize;
+
+ mStream_At = buf;
+ mStream_ReadEnd = buf + actual;
+ if (actual) // any bytes actually read?
+ {
+ c = *mStream_At++; // return first byte from buffer
+ mStream_HitEof = morkBool_kFalse;
+ } else
+ mStream_HitEof = morkBool_kTrue;
+ }
+ }
+ } else
+ this->NewFileDownError(ev);
+
+ return c;
+}
+
+void morkStream::spill_putc(morkEnv* ev, int c) {
+ this->spill_buf(ev);
+ if (ev->Good() && mStream_At < mStream_WriteEnd) this->Putc(ev, c);
+}
+
+void morkStream::spill_buf(morkEnv* ev) // spill/flush from buffer to file
+{
+ nsIMdbFile* file = mStream_ContentFile;
+ if (this->IsOpenOrClosingNode() && this->FileActive() && file) {
+ mork_u1* buf = mStream_Buf;
+ if (mStream_Dirty) {
+ mork_u1* at = mStream_At;
+ if (at >= buf && at <= mStream_WriteEnd) // order?
+ {
+ mork_num count = (mork_num)(at - buf); // bytes buffered
+ if (count) // anything to write to the string?
+ {
+ if (count > mStream_BufSize) // no more than max?
+ {
+ count = mStream_BufSize;
+ mStream_WriteEnd = buf + mStream_BufSize;
+ this->NewBadCursorSlotsError(ev);
+ }
+ if (ev->Good()) {
+ // file->Seek(ev, mStream_BufPos);
+ // if ( ev->Good() )
+ // {
+ // }
+ nsIMdbEnv* menv = ev->AsMdbEnv();
+ mork_num actual = 0;
+
+ file->Put(menv, buf, count, mStream_BufPos, &actual);
+ if (ev->Good()) {
+ mStream_BufPos += actual; // past bytes written
+ mStream_At = buf; // reset buffer cursor
+ mStream_Dirty = morkBool_kFalse;
+ }
+ }
+ }
+ } else
+ this->NewBadCursorOrderError(ev);
+ } else {
+#ifdef MORK_DEBUG
+ ev->NewWarning("stream:spill:not:dirty");
+#endif /*MORK_DEBUG*/
+ }
+ } else
+ this->NewFileDownError(ev);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkStream.h b/comm/mailnews/db/mork/morkStream.h
new file mode 100644
index 0000000000..be7a40c1dc
--- /dev/null
+++ b/comm/mailnews/db/mork/morkStream.h
@@ -0,0 +1,258 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _MORKSTREAM_
+#define _MORKSTREAM_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKFILE_
+# include "morkFile.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*=============================================================================
+ * morkStream: buffered file i/o
+ */
+
+/*| morkStream exists to define an morkFile subclass that provides buffered
+**| i/o for an underlying content file. Naturally this arrangement only makes
+**| sense when the underlying content file is itself not efficiently buffered
+**| (especially for character by character i/o).
+**|
+**|| morkStream is intended for either reading use or writing use, but not
+**| both simultaneously or interleaved. Pick one when the stream is created
+**| and don't change your mind. This restriction is intended to avoid obscure
+**| and complex bugs that might arise from interleaved reads and writes -- so
+**| just don't do it. A stream is either a sink or a source, but not both.
+**|
+**|| (When the underlying content file is intended to support both reading and
+**| writing, a developer might use two instances of morkStream where one is for
+**| reading and the other is for writing. In this case, a developer must take
+**| care to keep the two streams in sync because each will maintain a separate
+**| buffer representing a cache consistency problem for the other. A simple
+**| approach is to invalidate the buffer of one when one uses the other, with
+**| the assumption that closely mixed reading and writing is not expected, so
+**| that little cost is associated with changing read/write streaming modes.)
+**|
+**|| Exactly one of mStream_ReadEnd or mStream_WriteEnd must be a null pointer,
+**| and this will cause the right thing to occur when inlines use them, because
+**| mStream_At < mStream_WriteEnd (for example) will always be false and the
+**| else branch of the statement calls a function that raises an appropriate
+**| error to complain about either reading a sink or writing a source.
+**|
+**|| morkStream is a direct clone of ab_Stream from Communicator 4.5's
+**| address book code, which in turn was based on the stream class in the
+**| public domain Mithril programming language.
+|*/
+
+#define morkStream_kPrintBufSize /*i*/ 512 /* buffer size used by printf() */
+
+#define morkStream_kMinBufSize /*i*/ 512 /* buffer no fewer bytes */
+#define morkStream_kMaxBufSize /*i*/ (32 * 1024) /* buffer no more bytes */
+
+#define morkDerived_kStream /*i*/ 0x7A74 /* ascii 'zt' */
+
+class morkStream /*d*/ : public morkFile { /* from Mithril's AgStream class */
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected morkStream members
+ mork_u1* mStream_At; // pointer into mStream_Buf
+ mork_u1* mStream_ReadEnd; // null or one byte past last readable byte
+ mork_u1* mStream_WriteEnd; // null or mStream_Buf + mStream_BufSize
+
+ nsIMdbFile* mStream_ContentFile; // where content is read and written
+
+ mork_u1* mStream_Buf; // dynamically allocated memory to buffer io
+ mork_size mStream_BufSize; // requested buf size (fixed by min and max)
+ mork_pos mStream_BufPos; // logical position of byte at mStream_Buf
+ mork_bool mStream_Dirty; // does the buffer need to be flushed?
+ mork_bool mStream_HitEof; // has eof been reached? (only frozen streams)
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseStream() only if open
+ virtual ~morkStream(); // assert that CloseStream() executed earlier
+
+ public: // morkStream construction & destruction
+ morkStream(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbFile* ioContentFile, mork_size inBufSize,
+ mork_bool inFrozen);
+ void CloseStream(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkStream(const morkStream& other);
+ morkStream& operator=(const morkStream& other);
+
+ public: // dynamic type identification
+ mork_bool IsStream() const {
+ return IsNode() && mNode_Derived == morkDerived_kStream;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ void NonStreamTypeError(morkEnv* ev);
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // virtual morkFile methods
+ NS_IMETHOD Steal(nsIMdbEnv* ev, nsIMdbFile* ioThief) override;
+ // Steal: tell this file to close any associated i/o stream in the file
+ // system, because the file ioThief intends to reopen the file in order
+ // to provide the MDB implementation with more exotic file access than is
+ // offered by the nsIMdbFile alone. Presumably the thief knows enough
+ // from Path() in order to know which file to reopen. If Steal() is
+ // successful, this file should probably delegate all future calls to
+ // the nsIMdbFile interface down to the thief files, so that even after
+ // the file has been stolen, it can still be read, written, or forcibly
+ // closed (by a call to CloseMdbObject()).
+
+ NS_IMETHOD BecomeTrunk(nsIMdbEnv* ev) override;
+ // If this file is a file version branch created by calling AcquireBud(),
+ // BecomeTrunk() causes this file's content to replace the original
+ // file's content, typically by assuming the original file's identity.
+
+ NS_IMETHOD AcquireBud(nsIMdbEnv* ev, nsIMdbHeap* ioHeap,
+ nsIMdbFile** acqBud) override;
+ // AcquireBud() starts a new "branch" version of the file, empty of content,
+ // so that a new version of the file can be written. This new file
+ // can later be told to BecomeTrunk() the original file, so the branch
+ // created by budding the file will replace the original file. Some
+ // file subclasses might initially take the unsafe but expedient
+ // approach of simply truncating this file down to zero length, and
+ // then returning the same morkFile pointer as this, with an extra
+ // reference count increment. Note that the caller of AcquireBud() is
+ // expected to eventually call CutStrongRef() on the returned file
+ // in order to release the strong reference. High quality versions
+ // of morkFile subclasses will create entirely new files which later
+ // are renamed to become the old file, so that better transactional
+ // behavior is exhibited by the file, so crashes protect old files.
+ // Note that AcquireBud() is an illegal operation on readonly files.
+
+ virtual mork_pos Length(morkEnv* ev) const override; // eof
+ NS_IMETHOD Tell(nsIMdbEnv* ev, mork_pos* aOutPos) const override;
+ NS_IMETHOD Read(nsIMdbEnv* ev, void* outBuf, mork_size inSize,
+ mork_size* aOutCount) override;
+ NS_IMETHOD Seek(nsIMdbEnv* ev, mork_pos inPos, mork_pos* aOutPos) override;
+ NS_IMETHOD Write(nsIMdbEnv* ev, const void* inBuf, mork_size inSize,
+ mork_size* aOutCount) override;
+ NS_IMETHOD Flush(nsIMdbEnv* ev) override;
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ protected: // protected non-poly morkStream methods (for char io)
+ int fill_getc(morkEnv* ev);
+ void spill_putc(morkEnv* ev, int c);
+ void spill_buf(morkEnv* ev); // spill/flush from buffer to file
+
+ // ````` ````` ````` ````` ````` ````` ````` `````
+ public: // public non-poly morkStream methods
+ void NewBadCursorSlotsError(morkEnv* ev) const;
+ void NewBadCursorOrderError(morkEnv* ev) const;
+ void NewNullStreamBufferError(morkEnv* ev) const;
+ void NewCantReadSinkError(morkEnv* ev) const;
+ void NewCantWriteSourceError(morkEnv* ev) const;
+ void NewPosBeyondEofError(morkEnv* ev) const;
+
+ nsIMdbFile* GetStreamContentFile() const { return mStream_ContentFile; }
+ mork_size GetStreamBufferSize() const { return mStream_BufSize; }
+
+ mork_size PutIndent(morkEnv* ev, mork_count inDepth);
+ // PutIndent() puts a linebreak, and then
+ // "indents" by inDepth, and returns the line length after indentation.
+
+ mork_size PutByteThenIndent(morkEnv* ev, int inByte, mork_count inDepth);
+ // PutByteThenIndent() puts the byte, then a linebreak, and then
+ // "indents" by inDepth, and returns the line length after indentation.
+
+ mork_size PutStringThenIndent(morkEnv* ev, const char* inString,
+ mork_count inDepth);
+ // PutStringThenIndent() puts the string, then a linebreak, and then
+ // "indents" by inDepth, and returns the line length after indentation.
+
+ mork_size PutString(morkEnv* ev, const char* inString);
+ // PutString() returns the length of the string written.
+
+ mork_size PutStringThenNewline(morkEnv* ev, const char* inString);
+ // PutStringThenNewline() returns total number of bytes written.
+
+ mork_size PutByteThenNewline(morkEnv* ev, int inByte);
+ // PutByteThenNewline() returns total number of bytes written.
+
+ // ````` ````` stdio type methods ````` `````
+ void Ungetc(int c) /*i*/
+ {
+ if (mStream_At > mStream_Buf && c > 0) *--mStream_At = (mork_u1)c;
+ }
+
+ // Note Getc() returns EOF consistently after any fill_getc() error occurs.
+ int Getc(morkEnv* ev) /*i*/
+ {
+ return (mStream_At < mStream_ReadEnd) ? *mStream_At++ : fill_getc(ev);
+ }
+
+ void Putc(morkEnv* ev, int c) /*i*/
+ {
+ mStream_Dirty = morkBool_kTrue;
+ if (mStream_At < mStream_WriteEnd)
+ *mStream_At++ = (mork_u1)c;
+ else
+ spill_putc(ev, c);
+ }
+
+ mork_size PutLineBreak(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakStream(morkStream* me, morkEnv* ev, morkStream** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongStream(morkStream* me, morkEnv* ev,
+ morkStream** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKSTREAM_ */
diff --git a/comm/mailnews/db/mork/morkTable.cpp b/comm/mailnews/db/mork/morkTable.cpp
new file mode 100644
index 0000000000..ee27ff1328
--- /dev/null
+++ b/comm/mailnews/db/mork/morkTable.cpp
@@ -0,0 +1,1415 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKARRAY_
+# include "morkArray.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKTABLEROWCURSOR_
+# include "morkTableRowCursor.h"
+#endif
+
+#ifndef _MORKROWOBJECT_
+# include "morkRowObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkTable::CloseMorkNode(
+ morkEnv* ev) /*i*/ // CloseTable() only if open
+{
+ if (this->IsOpenNode()) {
+ morkObject::CloseMorkNode(ev); // give base class a chance.
+ this->MarkClosing();
+ this->CloseTable(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkTable::~morkTable() /*i*/ // assert CloseTable() executed earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(this->IsShutNode());
+ MORK_ASSERT(mTable_Store == 0);
+ MORK_ASSERT(mTable_RowSpace == 0);
+}
+
+/*public non-poly*/
+morkTable::morkTable(
+ morkEnv* ev, /*i*/
+ const morkUsage& inUsage, nsIMdbHeap* ioHeap, morkStore* ioStore,
+ nsIMdbHeap* ioSlotHeap, morkRowSpace* ioRowSpace,
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ mork_tid inTid, mork_kind inKind, mork_bool inMustBeUnique)
+ : morkObject(ev, inUsage, ioHeap, (mork_color)inTid, (morkHandle*)0),
+ mTable_Store(0),
+ mTable_RowSpace(0),
+ mTable_MetaRow(0)
+
+ ,
+ mTable_RowMap(0)
+ // , mTable_RowMap(ev, morkUsage::kMember, (nsIMdbHeap*) 0, ioSlotHeap,
+ // morkTable_kStartRowMapSlotCount)
+ ,
+ mTable_RowArray(ev, morkUsage::kMember, (nsIMdbHeap*)0,
+ morkTable_kStartRowArraySize, ioSlotHeap)
+
+ ,
+ mTable_ChangeList(),
+ mTable_ChangesCount(0),
+ mTable_ChangesMax(3) // any very small number greater than zero
+
+ ,
+ mTable_Kind(inKind)
+
+ ,
+ mTable_Flags(0),
+ mTable_Priority(morkPriority_kLo) // NOT high priority
+ ,
+ mTable_GcUses(0),
+ mTable_Pad(0) {
+ this->mLink_Next = 0;
+ this->mLink_Prev = 0;
+
+ if (ev->Good()) {
+ if (ioStore && ioSlotHeap && ioRowSpace) {
+ if (inKind) {
+ if (inMustBeUnique) this->SetTableUnique();
+ mTable_Store = ioStore;
+ mTable_RowSpace = ioRowSpace;
+ if (inOptionalMetaRowOid)
+ mTable_MetaRowOid = *inOptionalMetaRowOid;
+ else {
+ mTable_MetaRowOid.mOid_Scope = 0;
+ mTable_MetaRowOid.mOid_Id = morkRow_kMinusOneRid;
+ }
+ if (ev->Good()) {
+ if (this->MaybeDirtySpaceStoreAndTable())
+ this->SetTableRewrite(); // everything is dirty
+
+ mNode_Derived = morkDerived_kTable;
+ }
+ this->MaybeDirtySpaceStoreAndTable(); // new table might dirty store
+ } else
+ ioRowSpace->ZeroKindError(ev);
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkTable, morkObject, nsIMdbTable)
+
+/*public non-poly*/ void morkTable::CloseTable(
+ morkEnv* ev) /*i*/ // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkRowMap::SlotStrongRowMap((morkRowMap*)0, ev, &mTable_RowMap);
+ // mTable_RowMap.CloseMorkNode(ev);
+ mTable_RowArray.CloseMorkNode(ev);
+ mTable_Store = 0;
+ mTable_RowSpace = 0;
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ===== begin nsIMdbCollection methods =====
+
+// { ----- begin attribute methods -----
+NS_IMETHODIMP
+morkTable::GetSeed(nsIMdbEnv* mev,
+ mdb_seed* outSeed) // member change count
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ *outSeed = mTable_RowArray.mArray_Seed;
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::GetCount(nsIMdbEnv* mev,
+ mdb_count* outCount) // member count
+{
+ NS_ENSURE_ARG_POINTER(outCount);
+ *outCount = mTable_RowArray.mArray_Fill;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::GetPort(nsIMdbEnv* mev,
+ nsIMdbPort** acqPort) // collection container
+{
+ (void)morkEnv::FromMdbEnv(mev);
+ NS_ENSURE_ARG_POINTER(acqPort);
+ *acqPort = mTable_Store;
+ return NS_OK;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin cursor methods -----
+NS_IMETHODIMP
+morkTable::GetCursor( // make a cursor starting iter at inMemberPos
+ nsIMdbEnv* mev, // context
+ mdb_pos inMemberPos, // zero-based ordinal pos of member in collection
+ nsIMdbCursor** acqCursor) // acquire new cursor instance
+{
+ return this->GetTableRowCursor(mev, inMemberPos,
+ (nsIMdbTableRowCursor**)acqCursor);
+}
+// } ----- end cursor methods -----
+
+// { ----- begin ID methods -----
+NS_IMETHODIMP
+morkTable::GetOid(nsIMdbEnv* mev,
+ mdbOid* outOid) // read object identity
+{
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ GetTableOid(ev, outOid);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::BecomeContent(nsIMdbEnv* mev,
+ const mdbOid* inOid) // exchange content
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // remember table->MaybeDirtySpaceStoreAndTable();
+}
+
+// } ----- end ID methods -----
+
+// { ----- begin activity dropping methods -----
+NS_IMETHODIMP
+morkTable::DropActivity( // tell collection usage no longer expected
+ nsIMdbEnv* mev) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// } ----- end activity dropping methods -----
+
+// } ===== end nsIMdbCollection methods =====
+
+// { ===== begin nsIMdbTable methods =====
+
+// { ----- begin attribute methods -----
+
+NS_IMETHODIMP
+morkTable::SetTablePriority(nsIMdbEnv* mev, mdb_priority inPrio) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (inPrio > morkPriority_kMax) inPrio = morkPriority_kMax;
+
+ mTable_Priority = inPrio;
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::GetTablePriority(nsIMdbEnv* mev, mdb_priority* outPrio) {
+ nsresult outErr = NS_OK;
+ mork_priority prio = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ prio = mTable_Priority;
+ if (prio > morkPriority_kMax) {
+ prio = morkPriority_kMax;
+ mTable_Priority = prio;
+ }
+ outErr = ev->AsErr();
+ }
+ if (outPrio) *outPrio = prio;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::GetTableBeVerbose(nsIMdbEnv* mev, mdb_bool* outBeVerbose) {
+ NS_ENSURE_ARG_POINTER(outBeVerbose);
+ *outBeVerbose = IsTableVerbose();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::SetTableBeVerbose(nsIMdbEnv* mev, mdb_bool inBeVerbose) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (inBeVerbose)
+ SetTableVerbose();
+ else
+ ClearTableVerbose();
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::GetTableIsUnique(nsIMdbEnv* mev, mdb_bool* outIsUnique) {
+ NS_ENSURE_ARG_POINTER(outIsUnique);
+ *outIsUnique = IsTableUnique();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::GetTableKind(nsIMdbEnv* mev, mdb_kind* outTableKind) {
+ NS_ENSURE_ARG_POINTER(outTableKind);
+ *outTableKind = mTable_Kind;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::GetRowScope(nsIMdbEnv* mev, mdb_scope* outRowScope) {
+ nsresult outErr = NS_OK;
+ mdb_scope rowScope = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mTable_RowSpace)
+ rowScope = mTable_RowSpace->SpaceScope();
+ else
+ NilRowSpaceError(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (outRowScope) *outRowScope = rowScope;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::GetMetaRow(
+ nsIMdbEnv* mev,
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ mdbOid* outOid, // output meta row oid, can be nil to suppress output
+ nsIMdbRow** acqRow) // acquire table's unique singleton meta row
+// The purpose of a meta row is to support the persistent recording of
+// meta info about a table as cells put into the distinguished meta row.
+// Each table has exactly one meta row, which is not considered a member
+// of the collection of rows inside the table. The only way to tell
+// whether a row is a meta row is by the fact that it is returned by this
+// GetMetaRow() method from some table. Otherwise nothing distinguishes
+// a meta row from any other row. A meta row can be used anyplace that
+// any other row can be used, and can even be put into other tables (or
+// the same table) as a table member, if this is useful for some reason.
+// The first attempt to access a table's meta row using GetMetaRow() will
+// cause the meta row to be created if it did not already exist. When the
+// meta row is created, it will have the row oid that was previously
+// requested for this table's meta row; or if no oid was ever explicitly
+// specified for this meta row, then a unique oid will be generated in
+// the row scope named "metaScope" (so obviously MDB clients should not
+// manually allocate any row IDs from that special meta scope namespace).
+// The meta row oid can be specified either when the table is created, or
+// else the first time that GetMetaRow() is called, by passing a non-nil
+// pointer to an oid for parameter inOptionalMetaRowOid. The meta row's
+// actual oid is returned in outOid (if this is a non-nil pointer), and
+// it will be different from inOptionalMetaRowOid when the meta row was
+// already given a different oid earlier.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRow* row = GetMetaRow(ev, inOptionalMetaRowOid);
+ if (row && ev->Good()) {
+ if (outOid) *outOid = row->mRow_Oid;
+
+ outRow = row->AcquireRowHandle(ev, mTable_Store);
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+
+ if (ev->Bad() && outOid) {
+ outOid->mOid_Scope = 0;
+ outOid->mOid_Id = morkRow_kMinusOneRid;
+ }
+ return outErr;
+}
+
+// } ----- end attribute methods -----
+
+// { ----- begin cursor methods -----
+NS_IMETHODIMP
+morkTable::GetTableRowCursor( // make a cursor, starting iteration at inRowPos
+ nsIMdbEnv* mev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbTableRowCursor** acqCursor) // acquire new cursor instance
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTableRowCursor* outCursor = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkTableRowCursor* cursor = NewTableRowCursor(ev, inRowPos);
+ if (cursor) {
+ if (ev->Good()) {
+ // cursor->mCursor_Seed = (mork_seed) inRowPos;
+ outCursor = cursor;
+ outCursor->AddRef();
+ }
+ }
+
+ outErr = ev->AsErr();
+ }
+ if (acqCursor) *acqCursor = outCursor;
+ return outErr;
+}
+// } ----- end row position methods -----
+
+// { ----- begin row position methods -----
+NS_IMETHODIMP
+morkTable::PosToOid( // get row member for a table position
+ nsIMdbEnv* mev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ mdbOid* outOid) // row oid at the specified position
+{
+ nsresult outErr = NS_OK;
+ mdbOid roid;
+ roid.mOid_Scope = 0;
+ roid.mOid_Id = (mork_id)-1;
+
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRow* row = SafeRowAt(ev, inRowPos);
+ if (row) roid = row->mRow_Oid;
+
+ outErr = ev->AsErr();
+ }
+ if (outOid) *outOid = roid;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::OidToPos( // test for the table position of a row member
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_pos* outPos) // zero-based ordinal position of row in table
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mork_pos pos = ArrayHasOid(ev, inOid);
+ if (outPos) *outPos = pos;
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::PosToRow( // get row member for a table position
+ nsIMdbEnv* mev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbRow** acqRow) // acquire row at table position inRowPos
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRow* row = SafeRowAt(ev, inRowPos);
+ if (row && mTable_Store) outRow = row->AcquireRowHandle(ev, mTable_Store);
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::RowToPos( // test for the table position of a row member
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_pos* outPos) // zero-based ordinal position of row in table
+{
+ nsresult outErr = NS_OK;
+ mork_pos pos = -1;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* row = (morkRowObject*)ioRow;
+ pos = ArrayHasOid(ev, &row->mRowObject_Row->mRow_Oid);
+ outErr = ev->AsErr();
+ }
+ if (outPos) *outPos = pos;
+ return outErr;
+}
+
+// Note that HasRow() performs the inverse oid->pos mapping
+// } ----- end row position methods -----
+
+// { ----- begin oid set methods -----
+NS_IMETHODIMP
+morkTable::AddOid( // make sure the row with inOid is a table member
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid) // row to ensure membership in table
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::HasOid( // test for the table position of a row member
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_bool* outHasOid) // whether inOid is a member row
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (outHasOid) *outHasOid = MapHasOid(ev, inOid);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::CutOid( // make sure the row with inOid is not a member
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid) // row to remove from table
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (inOid && mTable_Store) {
+ morkRow* row = mTable_Store->GetRow(ev, inOid);
+ if (row) CutRow(ev, row);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end oid set methods -----
+
+// { ----- begin row set methods -----
+NS_IMETHODIMP
+morkTable::NewRow( // create a new row instance in table
+ nsIMdbEnv* mev, // context
+ mdbOid* ioOid, // please use zero (unbound) rowId for db-assigned IDs
+ nsIMdbRow** acqRow) // create new row
+{
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (ioOid && mTable_Store) {
+ morkRow* row = 0;
+ if (ioOid->mOid_Id == morkRow_kMinusOneRid)
+ row = mTable_Store->NewRow(ev, ioOid->mOid_Scope);
+ else
+ row = mTable_Store->NewRowWithOid(ev, ioOid);
+
+ if (row && AddRow(ev, row))
+ outRow = row->AcquireRowHandle(ev, mTable_Store);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::AddRow( // make sure the row with inOid is a table member
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow) // row to ensure membership in table
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* rowObj = (morkRowObject*)ioRow;
+ morkRow* row = rowObj->mRowObject_Row;
+ AddRow(ev, row);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::HasRow( // test for the table position of a row member
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_bool* outBool) // zero-based ordinal position of row in table
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* rowObj = (morkRowObject*)ioRow;
+ morkRow* row = rowObj->mRowObject_Row;
+ if (outBool) *outBool = MapHasOid(ev, &row->mRow_Oid);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::CutRow( // make sure the row with inOid is not a member
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow) // row to remove from table
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* rowObj = (morkRowObject*)ioRow;
+ morkRow* row = rowObj->mRowObject_Row;
+ CutRow(ev, row);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::CutAllRows( // remove all rows from the table
+ nsIMdbEnv* mev) // context
+{
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ CutAllRows(ev);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ----- end row set methods -----
+
+// { ----- begin searching methods -----
+NS_IMETHODIMP
+morkTable::FindRowMatches( // search variable number of sorted cols
+ nsIMdbEnv* mev, // context
+ const mdbYarn* inPrefix, // content to find as prefix in row's column cell
+ nsIMdbTableRowCursor** acqCursor) // set of matching rows
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::GetSearchColumns( // query columns used by FindRowMatches()
+ nsIMdbEnv* mev, // context
+ mdb_count* outCount, // context
+ mdbColumnSet* outColSet) // caller supplied space to put columns
+// GetSearchColumns() returns the columns actually searched when the
+// FindRowMatches() method is called. No more than mColumnSet_Count
+// slots of mColumnSet_Columns will be written, since mColumnSet_Count
+// indicates how many slots are present in the column array. The
+// actual number of search column used by the table is returned in
+// the outCount parameter; if this number exceeds mColumnSet_Count,
+// then a caller needs a bigger array to read the entire column set.
+// The minimum of mColumnSet_Count and outCount is the number slots
+// in mColumnSet_Columns that were actually written by this method.
+//
+// Callers are expected to change this set of columns by calls to
+// nsIMdbTable::SearchColumnsHint() or SetSearchSorting(), or both.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end searching methods -----
+
+// { ----- begin hinting methods -----
+NS_IMETHODIMP
+morkTable::SearchColumnsHint( // advise re future expected search cols
+ nsIMdbEnv* mev, // context
+ const mdbColumnSet* inColumnSet) // columns likely to be searched
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::SortColumnsHint( // advise re future expected sort columns
+ nsIMdbEnv* mev, // context
+ const mdbColumnSet* inColumnSet) // columns for likely sort requests
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::StartBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* mev, // context
+ const void* inLabel) // intend unique address to match end call
+// If batch starts nest by virtue of nesting calls in the stack, then
+// the address of a local variable makes a good batch start label that
+// can be used at batch end time, and such addresses remain unique.
+{
+ // we don't do anything here.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+morkTable::EndBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* mev, // context
+ const void* inLabel) // label matching start label
+// Suppose a table is maintaining one or many sort orders for a table,
+// so that every row added to the table must be inserted in each sort,
+// and every row cut must be removed from each sort. If a db client
+// intends to make many such changes before needing any information
+// about the order or positions of rows inside a table, then a client
+// might tell the table to start batch changes in order to disable
+// sorting of rows for the interim. Presumably a table will then do
+// a full sort of all rows at need when the batch changes end, or when
+// a surprise request occurs for row position during batch changes.
+{
+ // we don't do anything here.
+ return NS_OK;
+}
+// } ----- end hinting methods -----
+
+// { ----- begin sorting methods -----
+// sorting: note all rows are assumed sorted by row ID as a secondary
+// sort following the primary column sort, when table rows are sorted.
+
+NS_IMETHODIMP
+morkTable::CanSortColumn( // query which column is currently used for sorting
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // column to query sorting potential
+ mdb_bool* outCanSort) // whether the column can be sorted
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::GetSorting( // view same table in particular sorting
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // requested new column for sorting table
+ nsIMdbSorting** acqSorting) // acquire sorting for column
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::SetSearchSorting( // use this sorting in FindRowMatches()
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // often same as nsIMdbSorting::GetSortColumn()
+ nsIMdbSorting* ioSorting) // requested sorting for some column
+// SetSearchSorting() attempts to inform the table that ioSorting
+// should be used during calls to FindRowMatches() for searching
+// the column which is actually sorted by ioSorting. This method
+// is most useful in conjunction with nsIMdbSorting::SetCompare(),
+// because otherwise a caller would not be able to override the
+// comparison ordering method used during searches. Note that some
+// database implementations might be unable to use an arbitrarily
+// specified sort order, either due to schema or runtime interface
+// constraints, in which case ioSorting might not actually be used.
+// Presumably ioSorting is an instance that was returned from some
+// earlier call to nsIMdbTable::GetSorting(). A caller can also
+// use nsIMdbTable::SearchColumnsHint() to specify desired change
+// in which columns are sorted and searched by FindRowMatches().
+//
+// A caller can pass a nil pointer for ioSorting to request that
+// column inColumn no longer be used at all by FindRowMatches().
+// But when ioSorting is non-nil, then inColumn should match the
+// column actually sorted by ioSorting; when these do not agree,
+// implementations are instructed to give precedence to the column
+// specified by ioSorting (so this means callers might just pass
+// zero for inColumn when ioSorting is also provided, since then
+// inColumn is both redundant and ignored).
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// } ----- end sorting methods -----
+
+// { ----- begin moving methods -----
+// moving a row does nothing unless a table is currently unsorted
+
+NS_IMETHODIMP
+morkTable::MoveOid( // change position of row in unsorted table
+ nsIMdbEnv* mev, // context
+ const mdbOid* inOid, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row inOid
+ mdb_pos* outActualPos) // actual new position of row in table
+{
+ nsresult outErr = NS_OK;
+ mdb_pos actualPos = -1; // meaning it was never found in table
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (inOid && mTable_Store) {
+ morkRow* row = mTable_Store->GetRow(ev, inOid);
+ if (row) actualPos = MoveRow(ev, row, inHintFromPos, inToPos);
+ } else
+ ev->NilPointerError();
+
+ outErr = ev->AsErr();
+ }
+ if (outActualPos) *outActualPos = actualPos;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTable::MoveRow( // change position of row in unsorted table
+ nsIMdbEnv* mev, // context
+ nsIMdbRow* ioRow, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row ioRow
+ mdb_pos* outActualPos) // actual new position of row in table
+{
+ mdb_pos actualPos = -1; // meaning it was never found in table
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ morkRowObject* rowObj = (morkRowObject*)ioRow;
+ morkRow* row = rowObj->mRowObject_Row;
+ actualPos = MoveRow(ev, row, inHintFromPos, inToPos);
+ outErr = ev->AsErr();
+ }
+ if (outActualPos) *outActualPos = actualPos;
+ return outErr;
+}
+// } ----- end moving methods -----
+
+// { ----- begin index methods -----
+NS_IMETHODIMP
+morkTable::AddIndex( // create a sorting index for column if possible
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // the column to sort by index
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental index building
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the index addition will be finished.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::CutIndex( // stop supporting a specific column index
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // the column with index to be removed
+ nsIMdbThumb** acqThumb) // acquire thumb for incremental index destroy
+// Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+// then the index removal will be finished.
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::HasIndex( // query for current presence of a column index
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outHasIndex) // whether column has index for this column
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::EnableIndexOnSort( // create an index for col on first sort
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn) // the column to index if ever sorted
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::QueryIndexOnSort( // check whether index on sort is enabled
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outIndexOnSort) // whether column has index-on-sort enabled
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+morkTable::DisableIndexOnSort( // prevent future index creation on sort
+ nsIMdbEnv* mev, // context
+ mdb_column inColumn) // the column to index if ever sorted
+{
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+// } ----- end index methods -----
+
+// } ===== end nsIMdbTable methods =====
+
+// we override these so that we'll use the xpcom add and release ref.
+#ifndef _MSC_VER
+mork_refs morkTable::AddStrongRef(nsIMdbEnv* ev) { return (mork_refs)AddRef(); }
+#endif
+
+mork_refs morkTable::AddStrongRef(morkEnv* ev) { return (mork_refs)AddRef(); }
+
+#ifndef _MSC_VER
+nsresult morkTable::CutStrongRef(nsIMdbEnv* ev) { return (nsresult)Release(); }
+#endif
+
+mork_refs morkTable::CutStrongRef(morkEnv* ev) { return (mork_refs)Release(); }
+
+mork_u2 morkTable::AddTableGcUse(morkEnv* ev) {
+ MORK_USED_1(ev);
+ if (mTable_GcUses < morkTable_kMaxTableGcUses) // not already maxed out?
+ ++mTable_GcUses;
+
+ return mTable_GcUses;
+}
+
+mork_u2 morkTable::CutTableGcUse(morkEnv* ev) {
+ if (mTable_GcUses) // any outstanding uses to cut?
+ {
+ if (mTable_GcUses < morkTable_kMaxTableGcUses) // not frozen at max?
+ --mTable_GcUses;
+ } else
+ this->TableGcUsesUnderflowWarning(ev);
+
+ return mTable_GcUses;
+}
+
+// table dirty handling more complex than morkNode::SetNodeDirty() etc.
+
+void morkTable::SetTableClean(morkEnv* ev) {
+ if (mTable_ChangeList.HasListMembers()) {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ mTable_ChangeList.CutAndZapAllListMembers(ev, heap); // forget changes
+ }
+ mTable_ChangesCount = 0;
+
+ mTable_Flags = 0;
+ this->SetNodeClean();
+}
+
+// notifications regarding table changes:
+
+void morkTable::NoteTableMoveRow(morkEnv* ev, morkRow* ioRow, mork_pos inPos) {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ if (this->IsTableRewrite() || this->HasChangeOverflow())
+ this->NoteTableSetAll(ev);
+ else {
+ morkTableChange* tableChange =
+ new (*heap, ev) morkTableChange(ev, ioRow, inPos);
+ if (tableChange) {
+ if (ev->Good()) {
+ mTable_ChangeList.PushTail(tableChange);
+ ++mTable_ChangesCount;
+ } else {
+ tableChange->ZapOldNext(ev, heap);
+ this->SetTableRewrite(); // just plan to write all table rows
+ }
+ }
+ }
+}
+
+void morkTable::note_row_move(morkEnv* ev, morkRow* ioRow, mork_pos inNewPos) {
+ if (this->IsTableRewrite() || this->HasChangeOverflow())
+ this->NoteTableSetAll(ev);
+ else {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ morkTableChange* tableChange =
+ new (*heap, ev) morkTableChange(ev, ioRow, inNewPos);
+ if (tableChange) {
+ if (ev->Good()) {
+ mTable_ChangeList.PushTail(tableChange);
+ ++mTable_ChangesCount;
+ } else {
+ tableChange->ZapOldNext(ev, heap);
+ this->NoteTableSetAll(ev);
+ }
+ }
+ }
+}
+
+void morkTable::note_row_change(morkEnv* ev, mork_change inChange,
+ morkRow* ioRow) {
+ if (this->IsTableRewrite() || this->HasChangeOverflow())
+ this->NoteTableSetAll(ev);
+ else {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ morkTableChange* tableChange =
+ new (*heap, ev) morkTableChange(ev, inChange, ioRow);
+ if (tableChange) {
+ if (ev->Good()) {
+ mTable_ChangeList.PushTail(tableChange);
+ ++mTable_ChangesCount;
+ } else {
+ tableChange->ZapOldNext(ev, heap);
+ this->NoteTableSetAll(ev);
+ }
+ }
+ }
+}
+
+void morkTable::NoteTableSetAll(morkEnv* ev) {
+ if (mTable_ChangeList.HasListMembers()) {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ mTable_ChangeList.CutAndZapAllListMembers(ev, heap); // forget changes
+ }
+ mTable_ChangesCount = 0;
+ this->SetTableRewrite();
+}
+
+/*static*/ void morkTable::TableGcUsesUnderflowWarning(morkEnv* ev) {
+ ev->NewWarning("mTable_GcUses underflow");
+}
+
+/*static*/ void morkTable::NonTableTypeError(morkEnv* ev) {
+ ev->NewError("non morkTable");
+}
+
+/*static*/ void morkTable::NonTableTypeWarning(morkEnv* ev) {
+ ev->NewWarning("non morkTable");
+}
+
+/*static*/ void morkTable::NilRowSpaceError(morkEnv* ev) {
+ ev->NewError("nil mTable_RowSpace");
+}
+
+mork_bool morkTable::MaybeDirtySpaceStoreAndTable() {
+ morkRowSpace* rowSpace = mTable_RowSpace;
+ if (rowSpace) {
+ morkStore* store = rowSpace->mSpace_Store;
+ if (store && store->mStore_CanDirty) {
+ store->SetStoreDirty();
+ rowSpace->mSpace_CanDirty = morkBool_kTrue;
+ }
+
+ if (rowSpace->mSpace_CanDirty) // first time being dirtied?
+ {
+ if (this->IsTableClean()) {
+ mork_count rowCount = this->GetRowCount();
+ mork_count oneThird = rowCount / 4; // one third of rows
+ if (oneThird > 0x07FFF) // more than half max u2?
+ oneThird = 0x07FFF;
+
+ mTable_ChangesMax = (mork_u2)oneThird;
+ }
+ this->SetTableDirty();
+ rowSpace->SetRowSpaceDirty();
+
+ return morkBool_kTrue;
+ }
+ }
+ return morkBool_kFalse;
+}
+
+morkRow* morkTable::GetMetaRow(morkEnv* ev,
+ const mdbOid* inOptionalMetaRowOid) {
+ morkRow* outRow = mTable_MetaRow;
+ if (!outRow) {
+ morkStore* store = mTable_Store;
+ mdbOid* oid = &mTable_MetaRowOid;
+ if (inOptionalMetaRowOid && !oid->mOid_Scope) *oid = *inOptionalMetaRowOid;
+
+ if (oid->mOid_Scope) // oid already recorded in table?
+ outRow = store->OidToRow(ev, oid);
+ else {
+ outRow = store->NewRow(ev, morkStore_kMetaScope);
+ if (outRow) // need to record new oid in table?
+ *oid = outRow->mRow_Oid;
+ }
+ mTable_MetaRow = outRow;
+ if (outRow) // need to note another use of this row?
+ {
+ outRow->AddRowGcUse(ev);
+
+ this->SetTableNewMeta();
+ if (this->IsTableClean()) // catch dirty status of meta row?
+ this->MaybeDirtySpaceStoreAndTable();
+ }
+ }
+
+ return outRow;
+}
+
+void morkTable::GetTableOid(morkEnv* ev, mdbOid* outOid) {
+ morkRowSpace* space = mTable_RowSpace;
+ if (space) {
+ outOid->mOid_Scope = space->SpaceScope();
+ outOid->mOid_Id = this->TableId();
+ } else
+ this->NilRowSpaceError(ev);
+}
+
+nsIMdbTable* morkTable::AcquireTableHandle(morkEnv* ev) {
+ AddRef();
+ return this;
+}
+
+mork_pos morkTable::ArrayHasOid(morkEnv* ev, const mdbOid* inOid) {
+ MORK_USED_1(ev);
+ mork_count count = mTable_RowArray.mArray_Fill;
+ mork_pos pos = -1;
+ while (++pos < (mork_pos)count) {
+ morkRow* row = (morkRow*)mTable_RowArray.At(pos);
+ MORK_ASSERT(row);
+ if (row && row->EqualOid(inOid)) {
+ return pos;
+ }
+ }
+ return -1;
+}
+
+mork_bool morkTable::MapHasOid(morkEnv* ev, const mdbOid* inOid) {
+ if (mTable_RowMap)
+ return (mTable_RowMap->GetOid(ev, inOid) != 0);
+ else
+ return (ArrayHasOid(ev, inOid) >= 0);
+}
+
+void morkTable::build_row_map(morkEnv* ev) {
+ morkRowMap* map = mTable_RowMap;
+ if (!map) {
+ mork_count count = mTable_RowArray.mArray_Fill + 3;
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ map = new (*heap, ev) morkRowMap(ev, morkUsage::kHeap, heap, heap, count);
+ if (map) {
+ if (ev->Good()) {
+ mTable_RowMap = map; // put strong ref here
+ count = mTable_RowArray.mArray_Fill;
+ mork_pos pos = -1;
+ while (++pos < (mork_pos)count) {
+ morkRow* row = (morkRow*)mTable_RowArray.At(pos);
+ if (row && row->IsRow())
+ map->AddRow(ev, row);
+ else
+ row->NonRowTypeError(ev);
+ }
+ } else
+ map->CutStrongRef(ev);
+ }
+ }
+}
+
+morkRow* morkTable::find_member_row(morkEnv* ev, morkRow* ioRow) {
+ if (mTable_RowMap)
+ return mTable_RowMap->GetRow(ev, ioRow);
+ else {
+ mork_count count = mTable_RowArray.mArray_Fill;
+ mork_pos pos = -1;
+ while (++pos < (mork_pos)count) {
+ morkRow* row = (morkRow*)mTable_RowArray.At(pos);
+ if (row == ioRow) return row;
+ }
+ }
+ return (morkRow*)0;
+}
+
+mork_pos morkTable::MoveRow(
+ morkEnv* ev, morkRow* ioRow, // change row position
+ mork_pos inHintFromPos, // suggested hint regarding start position
+ mork_pos inToPos) // desired new position for row ioRow
+// MoveRow() returns the actual position of ioRow afterwards; this
+// position is -1 if and only if ioRow was not found as a member.
+{
+ mork_pos outPos = -1; // means ioRow was not a table member
+ mork_bool canDirty = (this->IsTableClean())
+ ? this->MaybeDirtySpaceStoreAndTable()
+ : morkBool_kTrue;
+
+ morkRow** rows = (morkRow**)mTable_RowArray.mArray_Slots;
+ mork_count count = mTable_RowArray.mArray_Fill;
+ if (count && rows && ev->Good()) // any members at all? no errors?
+ {
+ mork_pos lastPos = count - 1; // index of last row slot
+
+ if (inToPos > lastPos) // beyond last used array slot?
+ inToPos = lastPos; // put row into last available slot
+ else if (inToPos < 0) // before first usable slot?
+ inToPos = 0; // put row in very first slow
+
+ if (inHintFromPos > lastPos) // beyond last used array slot?
+ inHintFromPos = lastPos; // seek row in last available slot
+ else if (inHintFromPos < 0) // before first usable slot?
+ inHintFromPos = 0; // seek row in very first slow
+
+ morkRow** fromSlot = 0; // becomes nonzero of ioRow is ever found
+ morkRow** rowsEnd = rows + count; // one past last used array slot
+
+ if (inHintFromPos <= 0) // start of table? just scan for row?
+ {
+ morkRow** cursor = rows - 1; // before first array slot
+ while (++cursor < rowsEnd) {
+ if (*cursor == ioRow) {
+ fromSlot = cursor;
+ break; // end while loop
+ }
+ }
+ } else // search near the start position and work outwards
+ {
+ morkRow** lo = rows + inHintFromPos; // lowest search point
+ morkRow** hi = lo; // highest search point starts at lowest point
+
+ // Seek ioRow in spiral widening search below and above inHintFromPos.
+ // This is faster when inHintFromPos is at all accurate, but is slower
+ // than a straightforward scan when inHintFromPos is nearly random.
+
+ while (lo >= rows || hi < rowsEnd) // keep searching?
+ {
+ if (lo >= rows) // low direction search still feasible?
+ {
+ if (*lo == ioRow) // actually found the row?
+ {
+ fromSlot = lo;
+ break; // end while loop
+ }
+ --lo; // advance further lower
+ }
+ if (hi < rowsEnd) // high direction search still feasible?
+ {
+ if (*hi == ioRow) // actually found the row?
+ {
+ fromSlot = hi;
+ break; // end while loop
+ }
+ ++hi; // advance further higher
+ }
+ }
+ }
+
+ if (fromSlot) // ioRow was found as a table member?
+ {
+ outPos = fromSlot - rows; // actual position where row was found
+ if (outPos != inToPos) // actually need to move this row?
+ {
+ morkRow** toSlot = rows + inToPos; // slot where row must go
+
+ ++mTable_RowArray.mArray_Seed; // we modify the array now:
+
+ if (fromSlot < toSlot) // row is moving upwards?
+ {
+ morkRow** up = fromSlot; // leading pointer going upward
+ while (++up <= toSlot) // have not gone above destination?
+ {
+ *fromSlot = *up; // shift down one
+ fromSlot = up; // shift trailing pointer up
+ }
+ } else // ( fromSlot > toSlot ) // row is moving downwards
+ {
+ morkRow** down = fromSlot; // leading pointer going downward
+ while (--down >= toSlot) // have not gone below destination?
+ {
+ *fromSlot = *down; // shift up one
+ fromSlot = down; // shift trailing pointer
+ }
+ }
+ *toSlot = ioRow;
+ outPos = inToPos; // okay, we actually moved the row here
+
+ if (canDirty) this->note_row_move(ev, ioRow, inToPos);
+ }
+ }
+ }
+ return outPos;
+}
+
+mork_bool morkTable::AddRow(morkEnv* ev, morkRow* ioRow) {
+ morkRow* row = this->find_member_row(ev, ioRow);
+ if (!row && ev->Good()) {
+ mork_bool canDirty = (this->IsTableClean())
+ ? this->MaybeDirtySpaceStoreAndTable()
+ : morkBool_kTrue;
+
+ mork_pos pos = mTable_RowArray.AppendSlot(ev, ioRow);
+ if (ev->Good() && pos >= 0) {
+ ioRow->AddRowGcUse(ev);
+ if (mTable_RowMap) {
+ if (mTable_RowMap->AddRow(ev, ioRow)) {
+ // okay, anything else?
+ } else
+ mTable_RowArray.CutSlot(ev, pos);
+ } else if (mTable_RowArray.mArray_Fill >= morkTable_kMakeRowMapThreshold)
+ this->build_row_map(ev);
+
+ if (canDirty && ev->Good()) this->NoteTableAddRow(ev, ioRow);
+ }
+ }
+ return ev->Good();
+}
+
+mork_bool morkTable::CutRow(morkEnv* ev, morkRow* ioRow) {
+ morkRow* row = this->find_member_row(ev, ioRow);
+ if (row) {
+ mork_bool canDirty = (this->IsTableClean())
+ ? this->MaybeDirtySpaceStoreAndTable()
+ : morkBool_kTrue;
+
+ mork_count count = mTable_RowArray.mArray_Fill;
+ morkRow** rowSlots = (morkRow**)mTable_RowArray.mArray_Slots;
+ if (rowSlots) // array has vector as expected?
+ {
+ mork_pos pos = -1;
+ morkRow** end = rowSlots + count;
+ morkRow** slot = rowSlots - 1; // prepare for preincrement:
+ while (++slot < end) // another slot to check?
+ {
+ if (*slot == row) // found the slot containing row?
+ {
+ pos = slot - rowSlots; // record absolute position
+ break; // end while loop
+ }
+ }
+ if (pos >= 0) // need to cut if from the array?
+ mTable_RowArray.CutSlot(ev, pos);
+ else
+ ev->NewWarning("row not found in array");
+ } else
+ mTable_RowArray.NilSlotsAddressError(ev);
+
+ if (mTable_RowMap) mTable_RowMap->CutRow(ev, ioRow);
+
+ if (canDirty) this->NoteTableCutRow(ev, ioRow);
+
+ if (ioRow->CutRowGcUse(ev) == 0) ioRow->OnZeroRowGcUse(ev);
+ }
+ return ev->Good();
+}
+
+mork_bool morkTable::CutAllRows(morkEnv* ev) {
+ if (this->MaybeDirtySpaceStoreAndTable()) {
+ this->SetTableRewrite(); // everything is dirty
+ this->NoteTableSetAll(ev);
+ }
+
+ if (ev->Good()) {
+ mTable_RowArray.CutAllSlots(ev);
+ if (mTable_RowMap) {
+ morkRowMapIter i(ev, mTable_RowMap);
+ mork_change* c = 0;
+ morkRow* r = 0;
+
+ for (c = i.FirstRow(ev, &r); c; c = i.NextRow(ev, &r)) {
+ if (r) {
+ if (r->CutRowGcUse(ev) == 0) r->OnZeroRowGcUse(ev);
+
+ i.CutHereRow(ev, (morkRow**)0);
+ } else
+ ev->NewWarning("nil row in table map");
+ }
+ }
+ }
+ return ev->Good();
+}
+
+morkTableRowCursor* morkTable::NewTableRowCursor(morkEnv* ev,
+ mork_pos inRowPos) {
+ morkTableRowCursor* outCursor = 0;
+ if (ev->Good()) {
+ nsIMdbHeap* heap = mTable_Store->mPort_Heap;
+ morkTableRowCursor* cursor = new (*heap, ev)
+ morkTableRowCursor(ev, morkUsage::kHeap, heap, this, inRowPos);
+ if (cursor) {
+ if (ev->Good())
+ outCursor = cursor;
+ else
+ cursor->CutStrongRef((nsIMdbEnv*)ev);
+ }
+ }
+ return outCursor;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkTableChange::morkTableChange(morkEnv* ev, mork_change inChange,
+ morkRow* ioRow)
+ // use this constructor for inChange == morkChange_kAdd or morkChange_kCut
+ : morkNext(),
+ mTableChange_Row(ioRow),
+ mTableChange_Pos(morkTableChange_kNone) {
+ if (ioRow) {
+ if (ioRow->IsRow()) {
+ if (inChange == morkChange_kAdd)
+ mTableChange_Pos = morkTableChange_kAdd;
+ else if (inChange == morkChange_kCut)
+ mTableChange_Pos = morkTableChange_kCut;
+ else
+ this->UnknownChangeError(ev);
+ } else
+ ioRow->NonRowTypeError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+morkTableChange::morkTableChange(morkEnv* ev, morkRow* ioRow, mork_pos inPos)
+ // use this constructor when the row is moved
+ : morkNext(), mTableChange_Row(ioRow), mTableChange_Pos(inPos) {
+ if (ioRow) {
+ if (ioRow->IsRow()) {
+ if (inPos < 0) this->NegativeMovePosError(ev);
+ } else
+ ioRow->NonRowTypeError(ev);
+ } else
+ ev->NilPointerError();
+}
+
+void morkTableChange::UnknownChangeError(morkEnv* ev) const
+// morkChange_kAdd or morkChange_kCut
+{
+ ev->NewError("mTableChange_Pos neither kAdd nor kCut");
+}
+
+void morkTableChange::NegativeMovePosError(morkEnv* ev) const
+// move must be non-neg position
+{
+ ev->NewError("negative mTableChange_Pos for row move");
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+morkTableMap::~morkTableMap() {}
+
+morkTableMap::morkTableMap(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ : morkBeadMap(ev, inUsage, ioHeap, ioSlotHeap)
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ : morkNodeMap(ev, inUsage, ioHeap, ioSlotHeap)
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+{
+ if (ev->Good()) mNode_Derived = morkDerived_kTableMap;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkTable.h b/comm/mailnews/db/mork/morkTable.h
new file mode 100644
index 0000000000..c0ca5ddd84
--- /dev/null
+++ b/comm/mailnews/db/mork/morkTable.h
@@ -0,0 +1,742 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKTABLE_
+#define _MORKTABLE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+#ifndef _MORKARRAY_
+# include "morkArray.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKNODEMAP_
+# include "morkNodeMap.h"
+#endif
+
+#ifndef _MORKPROBEMAP_
+# include "morkProbeMap.h"
+#endif
+
+#ifndef _MORKBEAD_
+# include "morkBead.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class nsIMdbTable;
+#define morkDerived_kTable /*i*/ 0x5462 /* ascii 'Tb' */
+
+/*| kStartRowArraySize: starting physical size of array for mTable_RowArray.
+**| We want this number very small, so that a table containing exactly one
+**| row member will not pay too significantly in space overhead. But we want
+**| a number bigger than one, so there is some space for growth.
+|*/
+#define morkTable_kStartRowArraySize 3 /* modest starting size for array */
+
+/*| kMakeRowMapThreshold: this is the number of rows in a table which causes
+**| a hash table (mTable_RowMap) to be lazily created for faster member row
+**| identification, during such operations as cuts and adds. This number must
+**| be small enough that linear searches are not bad for member counts less
+**| than this; but this number must also be large enough that creating a hash
+**| table does not increase the per-row space overhead by a big percentage.
+**| For speed, numbers on the order of ten to twenty are all fine; for space,
+**| I believe a number as small as ten will have too much space overhead.
+|*/
+#define morkTable_kMakeRowMapThreshold 17 /* when to build mTable_RowMap */
+
+#define morkTable_kStartRowMapSlotCount 13
+#define morkTable_kMaxTableGcUses 0x0FF /* max for 8-bit unsigned int */
+
+#define morkTable_kUniqueBit ((mork_u1)(1 << 0))
+#define morkTable_kVerboseBit ((mork_u1)(1 << 1))
+#define morkTable_kNotedBit ((mork_u1)(1 << 2)) /* space has change notes */
+#define morkTable_kRewriteBit ((mork_u1)(1 << 3)) /* must rewrite all rows */
+#define morkTable_kNewMetaBit ((mork_u1)(1 << 4)) /* new table meta row */
+
+class morkTable : public morkObject, public morkLink, public nsIMdbTable {
+ // NOTE the morkLink base is for morkRowSpace::mRowSpace_TablesByPriority
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // bead color setter & getter replace obsolete member mTable_Id:
+ NS_DECL_ISUPPORTS_INHERITED
+ mork_tid TableId() const { return mBead_Color; }
+ void SetTableId(mork_tid inTid) { mBead_Color = inTid; }
+
+ // we override these so we use xpcom ref-counting semantics.
+#ifndef _MSC_VER
+ // The first declaration of AddStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ virtual mork_refs AddStrongRef(nsIMdbEnv* ev) override;
+#endif
+ virtual mork_refs AddStrongRef(morkEnv* ev) override;
+#ifndef _MSC_VER
+ // The first declaration of CutStrongRef is to suppress
+ // -Werror,-Woverloaded-virtual.
+ virtual nsresult CutStrongRef(nsIMdbEnv* ev) override;
+#endif
+ virtual mork_refs CutStrongRef(morkEnv* ev) override;
+
+ public:
+ // { ===== begin nsIMdbCollection methods =====
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev,
+ mdb_seed* outSeed) override; // member change count
+ NS_IMETHOD GetCount(nsIMdbEnv* ev,
+ mdb_count* outCount) override; // member count
+
+ NS_IMETHOD GetPort(nsIMdbEnv* ev,
+ nsIMdbPort** acqPort) override; // collection container
+ // } ----- end attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD GetCursor( // make a cursor starting iter at inMemberPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inMemberPos, // zero-based ordinal pos of member in collection
+ nsIMdbCursor** acqCursor) override; // acquire new cursor instance
+ // } ----- end cursor methods -----
+
+ // { ----- begin ID methods -----
+ NS_IMETHOD GetOid(nsIMdbEnv* ev,
+ mdbOid* outOid) override; // read object identity
+ NS_IMETHOD BecomeContent(nsIMdbEnv* ev,
+ const mdbOid* inOid) override; // exchange content
+ // } ----- end ID methods -----
+
+ // { ----- begin activity dropping methods -----
+ NS_IMETHOD DropActivity( // tell collection usage no longer expected
+ nsIMdbEnv* ev) override;
+ // } ----- end activity dropping methods -----
+
+ // } ===== end nsIMdbCollection methods =====
+ NS_IMETHOD SetTablePriority(nsIMdbEnv* ev, mdb_priority inPrio) override;
+ NS_IMETHOD GetTablePriority(nsIMdbEnv* ev, mdb_priority* outPrio) override;
+
+ NS_IMETHOD GetTableBeVerbose(nsIMdbEnv* ev, mdb_bool* outBeVerbose) override;
+ NS_IMETHOD SetTableBeVerbose(nsIMdbEnv* ev, mdb_bool inBeVerbose) override;
+
+ NS_IMETHOD GetTableIsUnique(nsIMdbEnv* ev, mdb_bool* outIsUnique) override;
+
+ NS_IMETHOD GetTableKind(nsIMdbEnv* ev, mdb_kind* outTableKind) override;
+ NS_IMETHOD GetRowScope(nsIMdbEnv* ev, mdb_scope* outRowScope) override;
+
+ NS_IMETHOD GetMetaRow(
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ mdbOid* outOid, // output meta row oid, can be nil to suppress output
+ nsIMdbRow** acqRow)
+ override; // acquire table's unique singleton meta row
+ // The purpose of a meta row is to support the persistent recording of
+ // meta info about a table as cells put into the distinguished meta row.
+ // Each table has exactly one meta row, which is not considered a member
+ // of the collection of rows inside the table. The only way to tell
+ // whether a row is a meta row is by the fact that it is returned by this
+ // GetMetaRow() method from some table. Otherwise nothing distinguishes
+ // a meta row from any other row. A meta row can be used anyplace that
+ // any other row can be used, and can even be put into other tables (or
+ // the same table) as a table member, if this is useful for some reason.
+ // The first attempt to access a table's meta row using GetMetaRow() will
+ // cause the meta row to be created if it did not already exist. When the
+ // meta row is created, it will have the row oid that was previously
+ // requested for this table's meta row; or if no oid was ever explicitly
+ // specified for this meta row, then a unique oid will be generated in
+ // the row scope named "m" (so obviously MDB clients should not
+ // manually allocate any row IDs from that special meta scope namespace).
+ // The meta row oid can be specified either when the table is created, or
+ // else the first time that GetMetaRow() is called, by passing a non-nil
+ // pointer to an oid for parameter inOptionalMetaRowOid. The meta row's
+ // actual oid is returned in outOid (if this is a non-nil pointer), and
+ // it will be different from inOptionalMetaRowOid when the meta row was
+ // already given a different oid earlier.
+ // } ----- end meta attribute methods -----
+
+ // { ----- begin cursor methods -----
+ NS_IMETHOD
+ GetTableRowCursor( // make a cursor, starting iteration at inRowPos
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbTableRowCursor** acqCursor)
+ override; // acquire new cursor instance
+ // } ----- end row position methods -----
+
+ // { ----- begin row position methods -----
+ NS_IMETHOD PosToOid( // get row member for a table position
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ mdbOid* outOid) override; // row oid at the specified position
+
+ NS_IMETHOD OidToPos( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_pos* outPos) override; // zero-based ordinal position of row in table
+
+ NS_IMETHOD PosToRow( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ mdb_pos inRowPos, // zero-based ordinal position of row in table
+ nsIMdbRow** acqRow) override; // acquire row at table position inRowPos
+
+ NS_IMETHOD RowToPos( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_pos* outPos) override; // zero-based ordinal position of row in table
+ // } ----- end row position methods -----
+
+ // { ----- begin oid set methods -----
+ NS_IMETHOD AddOid( // make sure the row with inOid is a table member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid) override; // row to ensure membership in table
+
+ NS_IMETHOD HasOid( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row to find in table
+ mdb_bool* outHasOid) override; // whether inOid is a member row
+
+ NS_IMETHOD CutOid( // make sure the row with inOid is not a member
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid) override; // row to remove from table
+ // } ----- end oid set methods -----
+
+ // { ----- begin row set methods -----
+ NS_IMETHOD NewRow( // create a new row instance in table
+ nsIMdbEnv* ev, // context
+ mdbOid*
+ ioOid, // please use minus one (unbound) rowId for db-assigned IDs
+ nsIMdbRow** acqRow) override; // create new row
+
+ NS_IMETHOD AddRow( // make sure the row with inOid is a table member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow) override; // row to ensure membership in table
+
+ NS_IMETHOD HasRow( // test for the table position of a row member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row to find in table
+ mdb_bool* outHasRow) override; // whether row is a table member
+
+ NS_IMETHOD CutRow( // make sure the row with inOid is not a member
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow) override; // row to remove from table
+
+ NS_IMETHOD CutAllRows( // remove all rows from the table
+ nsIMdbEnv* ev) override; // context
+ // } ----- end row set methods -----
+
+ // { ----- begin hinting methods -----
+ NS_IMETHOD SearchColumnsHint( // advise re future expected search cols
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet)
+ override; // columns likely to be searched
+
+ NS_IMETHOD SortColumnsHint( // advise re future expected sort columns
+ nsIMdbEnv* ev, // context
+ const mdbColumnSet* inColumnSet)
+ override; // columns for likely sort requests
+
+ NS_IMETHOD StartBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* ev, // context
+ const void* inLabel) override; // intend unique address to match end call
+ // If batch starts nest by virtue of nesting calls in the stack, then
+ // the address of a local variable makes a good batch start label that
+ // can be used at batch end time, and such addresses remain unique.
+
+ NS_IMETHOD EndBatchChangeHint( // advise before many adds and cuts
+ nsIMdbEnv* ev, // context
+ const void* inLabel) override; // label matching start label
+ // Suppose a table is maintaining one or many sort orders for a table,
+ // so that every row added to the table must be inserted in each sort,
+ // and every row cut must be removed from each sort. If a db client
+ // intends to make many such changes before needing any information
+ // about the order or positions of rows inside a table, then a client
+ // might tell the table to start batch changes in order to disable
+ // sorting of rows for the interim. Presumably a table will then do
+ // a full sort of all rows at need when the batch changes end, or when
+ // a surprise request occurs for row position during batch changes.
+ // } ----- end hinting methods -----
+
+ // { ----- begin searching methods -----
+ NS_IMETHOD FindRowMatches( // search variable number of sorted cols
+ nsIMdbEnv* ev, // context
+ const mdbYarn*
+ inPrefix, // content to find as prefix in row's column cell
+ nsIMdbTableRowCursor** acqCursor) override; // set of matching rows
+
+ NS_IMETHOD GetSearchColumns( // query columns used by FindRowMatches()
+ nsIMdbEnv* ev, // context
+ mdb_count* outCount, // context
+ mdbColumnSet* outColSet)
+ override; // caller supplied space to put columns
+ // GetSearchColumns() returns the columns actually searched when the
+ // FindRowMatches() method is called. No more than mColumnSet_Count
+ // slots of mColumnSet_Columns will be written, since mColumnSet_Count
+ // indicates how many slots are present in the column array. The
+ // actual number of search column used by the table is returned in
+ // the outCount parameter; if this number exceeds mColumnSet_Count,
+ // then a caller needs a bigger array to read the entire column set.
+ // The minimum of mColumnSet_Count and outCount is the number slots
+ // in mColumnSet_Columns that were actually written by this method.
+ //
+ // Callers are expected to change this set of columns by calls to
+ // nsIMdbTable::SearchColumnsHint() or SetSearchSorting(), or both.
+ // } ----- end searching methods -----
+
+ // { ----- begin sorting methods -----
+ // sorting: note all rows are assumed sorted by row ID as a secondary
+ // sort following the primary column sort, when table rows are sorted.
+
+ NS_IMETHOD
+ CanSortColumn( // query which column is currently used for sorting
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // column to query sorting potential
+ mdb_bool* outCanSort) override; // whether the column can be sorted
+
+ NS_IMETHOD GetSorting( // view same table in particular sorting
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // requested new column for sorting table
+ nsIMdbSorting** acqSorting) override; // acquire sorting for column
+
+ NS_IMETHOD SetSearchSorting( // use this sorting in FindRowMatches()
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // often same as nsIMdbSorting::GetSortColumn()
+ nsIMdbSorting* ioSorting) override; // requested sorting for some column
+ // SetSearchSorting() attempts to inform the table that ioSorting
+ // should be used during calls to FindRowMatches() for searching
+ // the column which is actually sorted by ioSorting. This method
+ // is most useful in conjunction with nsIMdbSorting::SetCompare(),
+ // because otherwise a caller would not be able to override the
+ // comparison ordering method used during searches. Note that some
+ // database implementations might be unable to use an arbitrarily
+ // specified sort order, either due to schema or runtime interface
+ // constraints, in which case ioSorting might not actually be used.
+ // Presumably ioSorting is an instance that was returned from some
+ // earlier call to nsIMdbTable::GetSorting(). A caller can also
+ // use nsIMdbTable::SearchColumnsHint() to specify desired change
+ // in which columns are sorted and searched by FindRowMatches().
+ //
+ // A caller can pass a nil pointer for ioSorting to request that
+ // column inColumn no longer be used at all by FindRowMatches().
+ // But when ioSorting is non-nil, then inColumn should match the
+ // column actually sorted by ioSorting; when these do not agree,
+ // implementations are instructed to give precedence to the column
+ // specified by ioSorting (so this means callers might just pass
+ // zero for inColumn when ioSorting is also provided, since then
+ // inColumn is both redundant and ignored).
+ // } ----- end sorting methods -----
+
+ // { ----- begin moving methods -----
+ // moving a row does nothing unless a table is currently unsorted
+
+ NS_IMETHOD MoveOid( // change position of row in unsorted table
+ nsIMdbEnv* ev, // context
+ const mdbOid* inOid, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row inRowId
+ mdb_pos* outActualPos) override; // actual new position of row in table
+
+ NS_IMETHOD MoveRow( // change position of row in unsorted table
+ nsIMdbEnv* ev, // context
+ nsIMdbRow* ioRow, // row oid to find in table
+ mdb_pos inHintFromPos, // suggested hint regarding start position
+ mdb_pos inToPos, // desired new position for row inRowId
+ mdb_pos* outActualPos) override; // actual new position of row in table
+ // } ----- end moving methods -----
+
+ // { ----- begin index methods -----
+ NS_IMETHOD AddIndex( // create a sorting index for column if possible
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to sort by index
+ nsIMdbThumb** acqThumb)
+ override; // acquire thumb for incremental index building
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the index addition will be finished.
+
+ NS_IMETHOD CutIndex( // stop supporting a specific column index
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column with index to be removed
+ nsIMdbThumb** acqThumb)
+ override; // acquire thumb for incremental index destroy
+ // Call nsIMdbThumb::DoMore() until done, or until the thumb is broken, and
+ // then the index removal will be finished.
+
+ NS_IMETHOD HasIndex( // query for current presence of a column index
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outHasIndex)
+ override; // whether column has index for this column
+
+ NS_IMETHOD EnableIndexOnSort( // create an index for col on first sort
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) override; // the column to index if ever sorted
+
+ NS_IMETHOD QueryIndexOnSort( // check whether index on sort is enabled
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn, // the column to investigate
+ mdb_bool* outIndexOnSort)
+ override; // whether column has index-on-sort enabled
+
+ NS_IMETHOD DisableIndexOnSort( // prevent future index creation on sort
+ nsIMdbEnv* ev, // context
+ mdb_column inColumn) override; // the column to index if ever sorted
+ // } ----- end index methods -----
+
+ morkStore* mTable_Store; // non-refcnted ptr to port
+
+ // mTable_RowSpace->SpaceScope() is row scope
+ morkRowSpace* mTable_RowSpace; // non-refcnted ptr to containing space
+
+ morkRow* mTable_MetaRow; // table's actual meta row
+ mdbOid mTable_MetaRowOid; // oid for meta row
+
+ morkRowMap* mTable_RowMap; // (strong ref) hash table of all members
+ morkArray mTable_RowArray; // array of morkRow pointers
+
+ morkList mTable_ChangeList; // list of table changes
+ mork_u2 mTable_ChangesCount; // length of changes list
+ mork_u2 mTable_ChangesMax; // max list length before rewrite
+
+ // mork_tid mTable_Id;
+ mork_kind mTable_Kind;
+
+ mork_u1 mTable_Flags; // bit flags
+ mork_priority mTable_Priority; // 0..9, any other value equals 9
+ mork_u1 mTable_GcUses; // persistent references from cells
+ mork_u1 mTable_Pad; // for u4 alignment
+
+ public: // flags bit twiddling
+ void SetTableUnique() { mTable_Flags |= morkTable_kUniqueBit; }
+ void SetTableVerbose() { mTable_Flags |= morkTable_kVerboseBit; }
+ void SetTableNoted() { mTable_Flags |= morkTable_kNotedBit; }
+ void SetTableRewrite() { mTable_Flags |= morkTable_kRewriteBit; }
+ void SetTableNewMeta() { mTable_Flags |= morkTable_kNewMetaBit; }
+
+ void ClearTableUnique() { mTable_Flags &= (mork_u1)~morkTable_kUniqueBit; }
+ void ClearTableVerbose() { mTable_Flags &= (mork_u1)~morkTable_kVerboseBit; }
+ void ClearTableNoted() { mTable_Flags &= (mork_u1)~morkTable_kNotedBit; }
+ void ClearTableRewrite() { mTable_Flags &= (mork_u1)~morkTable_kRewriteBit; }
+ void ClearTableNewMeta() { mTable_Flags &= (mork_u1)~morkTable_kNewMetaBit; }
+
+ mork_bool IsTableUnique() const {
+ return (mTable_Flags & morkTable_kUniqueBit) != 0;
+ }
+
+ mork_bool IsTableVerbose() const {
+ return (mTable_Flags & morkTable_kVerboseBit) != 0;
+ }
+
+ mork_bool IsTableNoted() const {
+ return (mTable_Flags & morkTable_kNotedBit) != 0;
+ }
+
+ mork_bool IsTableRewrite() const {
+ return (mTable_Flags & morkTable_kRewriteBit) != 0;
+ }
+
+ mork_bool IsTableNewMeta() const {
+ return (mTable_Flags & morkTable_kNewMetaBit) != 0;
+ }
+
+ public
+ : // table dirty handling more complex than morkNode::SetNodeDirty() etc.
+ void SetTableDirty() { this->SetNodeDirty(); }
+ void SetTableClean(morkEnv* ev);
+
+ mork_bool IsTableClean() const { return this->IsNodeClean(); }
+ mork_bool IsTableDirty() const { return this->IsNodeDirty(); }
+
+ public: // morkNode memory management operators
+ void* operator new(size_t inSize, nsIMdbHeap& ioHeap,
+ morkEnv* ev) noexcept(true) {
+ return morkNode::MakeNew(inSize, ioHeap, ev);
+ }
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseTable() if open
+
+ public: // morkTable construction & destruction
+ morkTable(
+ morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioNodeHeap,
+ morkStore* ioStore, nsIMdbHeap* ioSlotHeap, morkRowSpace* ioRowSpace,
+ const mdbOid* inOptionalMetaRowOid, // can be nil to avoid specifying
+ mork_tid inTableId, mork_kind inKind, mork_bool inMustBeUnique);
+ void CloseTable(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkTable(const morkTable& other);
+ morkTable& operator=(const morkTable& other);
+ virtual ~morkTable(); // assert that close executed earlier
+
+ public: // dynamic type identification
+ mork_bool IsTable() const {
+ return IsNode() && mNode_Derived == morkDerived_kTable;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // errors
+ static void NonTableTypeError(morkEnv* ev);
+ static void NonTableTypeWarning(morkEnv* ev);
+ static void NilRowSpaceError(morkEnv* ev);
+
+ public: // warnings
+ static void TableGcUsesUnderflowWarning(morkEnv* ev);
+
+ public: // noting table changes
+ mork_bool HasChangeOverflow() const {
+ return mTable_ChangesCount >= mTable_ChangesMax;
+ }
+
+ void NoteTableSetAll(morkEnv* ev);
+ void NoteTableMoveRow(morkEnv* ev, morkRow* ioRow, mork_pos inPos);
+
+ void note_row_change(morkEnv* ev, mork_change inChange, morkRow* ioRow);
+ void note_row_move(morkEnv* ev, morkRow* ioRow, mork_pos inNewPos);
+
+ void NoteTableAddRow(morkEnv* ev, morkRow* ioRow) {
+ this->note_row_change(ev, morkChange_kAdd, ioRow);
+ }
+
+ void NoteTableCutRow(morkEnv* ev, morkRow* ioRow) {
+ this->note_row_change(ev, morkChange_kCut, ioRow);
+ }
+
+ protected: // internal row map methods
+ morkRow* find_member_row(morkEnv* ev, morkRow* ioRow);
+ void build_row_map(morkEnv* ev);
+
+ public: // other table methods
+ mork_bool MaybeDirtySpaceStoreAndTable();
+
+ morkRow* GetMetaRow(morkEnv* ev, const mdbOid* inOptionalMetaRowOid);
+
+ mork_u2 AddTableGcUse(morkEnv* ev);
+ mork_u2 CutTableGcUse(morkEnv* ev);
+
+ // void DirtyAllTableContent(morkEnv* ev);
+
+ mork_seed TableSeed() const { return mTable_RowArray.mArray_Seed; }
+
+ morkRow* SafeRowAt(morkEnv* ev, mork_pos inPos) {
+ return (morkRow*)mTable_RowArray.SafeAt(ev, inPos);
+ }
+
+ nsIMdbTable* AcquireTableHandle(morkEnv* ev); // mObject_Handle
+
+ mork_count GetRowCount() const { return mTable_RowArray.mArray_Fill; }
+
+ mork_bool IsTableUsed() const {
+ return (mTable_GcUses != 0 || this->GetRowCount() != 0);
+ }
+
+ void GetTableOid(morkEnv* ev, mdbOid* outOid);
+ mork_pos ArrayHasOid(morkEnv* ev, const mdbOid* inOid);
+ mork_bool MapHasOid(morkEnv* ev, const mdbOid* inOid);
+ mork_bool AddRow(morkEnv* ev, morkRow* ioRow); // returns ev->Good()
+ mork_bool CutRow(morkEnv* ev, morkRow* ioRow); // returns ev->Good()
+ mork_bool CutAllRows(morkEnv* ev); // returns ev->Good()
+
+ mork_pos MoveRow(
+ morkEnv* ev, morkRow* ioRow, // change row position
+ mork_pos inHintFromPos, // suggested hint regarding start position
+ mork_pos inToPos); // desired new position for row ioRow
+ // MoveRow() returns the actual position of ioRow afterwards; this
+ // position is -1 if and only if ioRow was not found as a member.
+
+ morkTableRowCursor* NewTableRowCursor(morkEnv* ev, mork_pos inRowPos);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakTable(morkTable* me, morkEnv* ev, morkTable** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongTable(morkTable* me, morkEnv* ev, morkTable** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// use negative values for kCut and kAdd, to keep non-neg move pos distinct:
+#define morkTableChange_kCut ((mork_pos)-1) /* shows row was cut */
+#define morkTableChange_kAdd ((mork_pos)-2) /* shows row was added */
+#define morkTableChange_kNone ((mork_pos)-3) /* unknown change */
+
+class morkTableChange : public morkNext {
+ public: // state is public because the entire Mork system is private
+ morkRow* mTableChange_Row; // the row in the change
+
+ mork_pos mTableChange_Pos; // kAdd, kCut, or non-neg for row move
+
+ public:
+ morkTableChange(morkEnv* ev, mork_change inChange, morkRow* ioRow);
+ // use this constructor for inChange == morkChange_kAdd or morkChange_kCut
+
+ morkTableChange(morkEnv* ev, morkRow* ioRow, mork_pos inPos);
+ // use this constructor when the row is moved
+
+ public:
+ void UnknownChangeError(
+ morkEnv* ev) const; // morkChange_kAdd or morkChange_kCut
+ void NegativeMovePosError(
+ morkEnv* ev) const; // move must be non-neg position
+
+ public:
+ mork_bool IsAddRowTableChange() const {
+ return (mTableChange_Pos == morkTableChange_kAdd);
+ }
+
+ mork_bool IsCutRowTableChange() const {
+ return (mTableChange_Pos == morkTableChange_kCut);
+ }
+
+ mork_bool IsMoveRowTableChange() const { return (mTableChange_Pos >= 0); }
+
+ public:
+ mork_pos GetMovePos() const { return mTableChange_Pos; }
+ // GetMovePos() assumes that IsMoveRowTableChange() is true.
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kTableMap /*i*/ 0x744D /* ascii 'tM' */
+
+/*| morkTableMap: maps mork_token -> morkTable
+|*/
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+class morkTableMap : public morkBeadMap {
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+class morkTableMap : public morkNodeMap { // for mapping tokens to tables
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+
+ public:
+ virtual ~morkTableMap();
+ morkTableMap(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap);
+
+ public: // other map methods
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ mork_bool AddTable(morkEnv* ev, morkTable* ioTable) {
+ return this->AddBead(ev, ioTable);
+ }
+ // the AddTable() boolean return equals ev->Good().
+
+ mork_bool CutTable(morkEnv* ev, mork_tid inTid) {
+ return this->CutBead(ev, inTid);
+ }
+ // The CutTable() boolean return indicates whether removal happened.
+
+ morkTable* GetTable(morkEnv* ev, mork_tid inTid) {
+ return (morkTable*)this->GetBead(ev, inTid);
+ }
+ // Note the returned table does NOT have an increase in refcount for this.
+
+ mork_num CutAllTables(morkEnv* ev) { return this->CutAllBeads(ev); }
+ // CutAllTables() releases all the referenced table values.
+
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ mork_bool AddTable(morkEnv* ev, morkTable* ioTable) {
+ return this->AddNode(ev, ioTable->TableId(), ioTable);
+ }
+ // the AddTable() boolean return equals ev->Good().
+
+ mork_bool CutTable(morkEnv* ev, mork_tid inTid) {
+ return this->CutNode(ev, inTid);
+ }
+ // The CutTable() boolean return indicates whether removal happened.
+
+ morkTable* GetTable(morkEnv* ev, mork_tid inTid) {
+ return (morkTable*)this->GetNode(ev, inTid);
+ }
+ // Note the returned table does NOT have an increase in refcount for this.
+
+ mork_num CutAllTables(morkEnv* ev) { return this->CutAllNodes(ev); }
+ // CutAllTables() releases all the referenced table values.
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+};
+
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+class morkTableMapIter : public morkBeadMapIter {
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+class morkTableMapIter : public morkMapIter { // typesafe wrapper class
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+
+ public:
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ morkTableMapIter(morkEnv* ev, morkTableMap* ioMap)
+ : morkBeadMapIter(ev, ioMap) {}
+
+ morkTableMapIter() : morkBeadMapIter() {}
+ void InitTableMapIter(morkEnv* ev, morkTableMap* ioMap) {
+ this->InitBeadMapIter(ev, ioMap);
+ }
+
+ morkTable* FirstTable(morkEnv* ev) { return (morkTable*)this->FirstBead(ev); }
+
+ morkTable* NextTable(morkEnv* ev) { return (morkTable*)this->NextBead(ev); }
+
+ morkTable* HereTable(morkEnv* ev) { return (morkTable*)this->HereBead(ev); }
+
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ morkTableMapIter(morkEnv* ev, morkTableMap* ioMap) : morkMapIter(ev, ioMap) {}
+
+ morkTableMapIter() : morkMapIter() {}
+ void InitTableMapIter(morkEnv* ev, morkTableMap* ioMap) {
+ this->InitMapIter(ev, ioMap);
+ }
+
+ mork_change* FirstTable(morkEnv* ev, mork_tid* outTid, morkTable** outTable) {
+ return this->First(ev, outTid, outTable);
+ }
+
+ mork_change* NextTable(morkEnv* ev, mork_tid* outTid, morkTable** outTable) {
+ return this->Next(ev, outTid, outTable);
+ }
+
+ mork_change* HereTable(morkEnv* ev, mork_tid* outTid, morkTable** outTable) {
+ return this->Here(ev, outTid, outTable);
+ }
+
+ // cutting while iterating hash map might dirty the parent table:
+ mork_change* CutHereTable(morkEnv* ev, mork_tid* outTid,
+ morkTable** outTable) {
+ return this->CutHere(ev, outTid, outTable);
+ }
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKTABLE_ */
diff --git a/comm/mailnews/db/mork/morkTableRowCursor.cpp b/comm/mailnews/db/mork/morkTableRowCursor.cpp
new file mode 100644
index 0000000000..6644d2c2b3
--- /dev/null
+++ b/comm/mailnews/db/mork/morkTableRowCursor.cpp
@@ -0,0 +1,410 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKTABLEROWCURSOR_
+# include "morkTableRowCursor.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkTableRowCursor::CloseMorkNode(
+ morkEnv* ev) // CloseTableRowCursor() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseTableRowCursor(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkTableRowCursor::~morkTableRowCursor() // CloseTableRowCursor() executed
+ // earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(this->IsShutNode());
+}
+
+/*public non-poly*/
+morkTableRowCursor::morkTableRowCursor(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkTable* ioTable,
+ mork_pos inRowPos)
+ : morkCursor(ev, inUsage, ioHeap), mTableRowCursor_Table(0) {
+ if (ev->Good()) {
+ if (ioTable) {
+ mCursor_Pos = inRowPos;
+ mCursor_Seed = ioTable->TableSeed();
+ morkTable::SlotWeakTable(ioTable, ev, &mTableRowCursor_Table);
+ if (ev->Good()) mNode_Derived = morkDerived_kTableRowCursor;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkTableRowCursor, morkCursor,
+ nsIMdbTableRowCursor)
+/*public non-poly*/ void morkTableRowCursor::CloseTableRowCursor(morkEnv* ev) {
+ if (this->IsNode()) {
+ mCursor_Pos = -1;
+ mCursor_Seed = 0;
+ morkTable::SlotWeakTable((morkTable*)0, ev, &mTableRowCursor_Table);
+ this->CloseCursor(ev);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+// { ----- begin attribute methods -----
+/*virtual*/ nsresult morkTableRowCursor::GetCount(nsIMdbEnv* mev,
+ mdb_count* outCount) {
+ nsresult outErr = NS_OK;
+ mdb_count count = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ count = GetMemberCount(ev);
+ outErr = ev->AsErr();
+ }
+ if (outCount) *outCount = count;
+ return outErr;
+}
+
+/*virtual*/ nsresult morkTableRowCursor::GetSeed(nsIMdbEnv* mev,
+ mdb_seed* outSeed) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/*virtual*/ nsresult morkTableRowCursor::SetPos(nsIMdbEnv* mev, mdb_pos inPos) {
+ mCursor_Pos = inPos;
+ return NS_OK;
+}
+
+/*virtual*/ nsresult morkTableRowCursor::GetPos(nsIMdbEnv* mev,
+ mdb_pos* outPos) {
+ *outPos = mCursor_Pos;
+ return NS_OK;
+}
+
+/*virtual*/ nsresult morkTableRowCursor::SetDoFailOnSeedOutOfSync(
+ nsIMdbEnv* mev, mdb_bool inFail) {
+ mCursor_DoFailOnSeedOutOfSync = inFail;
+ return NS_OK;
+}
+
+/*virtual*/ nsresult morkTableRowCursor::GetDoFailOnSeedOutOfSync(
+ nsIMdbEnv* mev, mdb_bool* outFail) {
+ NS_ENSURE_ARG_POINTER(outFail);
+ *outFail = mCursor_DoFailOnSeedOutOfSync;
+ return NS_OK;
+}
+// } ----- end attribute methods -----
+
+// { ===== begin nsIMdbTableRowCursor methods =====
+
+// { ----- begin attribute methods -----
+
+NS_IMETHODIMP
+morkTableRowCursor::GetTable(nsIMdbEnv* mev, nsIMdbTable** acqTable) {
+ nsresult outErr = NS_OK;
+ nsIMdbTable* outTable = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (mTableRowCursor_Table)
+ outTable = mTableRowCursor_Table->AcquireTableHandle(ev);
+
+ outErr = ev->AsErr();
+ }
+ if (acqTable) *acqTable = outTable;
+ return outErr;
+}
+// } ----- end attribute methods -----
+
+// { ----- begin oid iteration methods -----
+NS_IMETHODIMP
+morkTableRowCursor::NextRowOid( // get row id of next row in the table
+ nsIMdbEnv* mev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) {
+ nsresult outErr = NS_OK;
+ mork_pos pos = -1;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (outOid) {
+ pos = NextRowOid(ev, outOid);
+ } else
+ ev->NilPointerError();
+ outErr = ev->AsErr();
+ }
+ if (outRowPos) *outRowPos = pos;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTableRowCursor::PrevRowOid( // get row id of previous row in the table
+ nsIMdbEnv* mev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) {
+ nsresult outErr = NS_OK;
+ mork_pos pos = -1;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ if (outOid) {
+ pos = PrevRowOid(ev, outOid);
+ } else
+ ev->NilPointerError();
+ outErr = ev->AsErr();
+ }
+ if (outRowPos) *outRowPos = pos;
+ return outErr;
+}
+// } ----- end oid iteration methods -----
+
+// { ----- begin row iteration methods -----
+NS_IMETHODIMP
+morkTableRowCursor::NextRow( // get row cells from table for cells already in
+ // row
+ nsIMdbEnv* mev, // context
+ nsIMdbRow** acqRow, // acquire next row in table
+ mdb_pos* outRowPos) {
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mdbOid oid; // place to put oid we intend to ignore
+ morkRow* row = NextRow(ev, &oid, outRowPos);
+ if (row) {
+ morkStore* store = row->GetRowSpaceStore(ev);
+ if (store) outRow = row->AcquireRowHandle(ev, store);
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTableRowCursor::PrevRow( // get row cells from table for cells already in
+ // row
+ nsIMdbEnv* mev, // context
+ nsIMdbRow** acqRow, // acquire previous row in table
+ mdb_pos* outRowPos) {
+ nsresult outErr = NS_OK;
+ nsIMdbRow* outRow = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mdbOid oid; // place to put oid we intend to ignore
+ morkRow* row = PrevRow(ev, &oid, outRowPos);
+ if (row) {
+ morkStore* store = row->GetRowSpaceStore(ev);
+ if (store) outRow = row->AcquireRowHandle(ev, store);
+ }
+ outErr = ev->AsErr();
+ }
+ if (acqRow) *acqRow = outRow;
+ return outErr;
+}
+
+// } ----- end row iteration methods -----
+
+// { ----- begin duplicate row removal methods -----
+NS_IMETHODIMP
+morkTableRowCursor::CanHaveDupRowMembers(
+ nsIMdbEnv* mev, // cursor might hold dups?
+ mdb_bool* outCanHaveDups) {
+ nsresult outErr = NS_OK;
+ mdb_bool canHaveDups = mdbBool_kFalse;
+
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ canHaveDups = CanHaveDupRowMembers(ev);
+ outErr = ev->AsErr();
+ }
+ if (outCanHaveDups) *outCanHaveDups = canHaveDups;
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkTableRowCursor::MakeUniqueCursor( // clone cursor, removing duplicate rows
+ nsIMdbEnv* mev, // context
+ nsIMdbTableRowCursor** acqCursor) // acquire clone with no dups
+// Note that MakeUniqueCursor() is never necessary for a cursor which was
+// created by table method nsIMdbTable::GetTableRowCursor(), because a table
+// never contains the same row as a member more than once. However, a cursor
+// created by table method nsIMdbTable::FindRowMatches() might contain the
+// same row more than once, because the same row can generate a hit by more
+// than one column with a matching string prefix. Note this method can
+// return the very same cursor instance with just an incremented refcount,
+// when the original cursor could not contain any duplicate rows (calling
+// CanHaveDupRowMembers() shows this case on a false return). Otherwise
+// this method returns a different cursor instance. Callers should not use
+// this MakeUniqueCursor() method lightly, because it tends to defeat the
+// purpose of lazy programming techniques, since it can force creation of
+// an explicit row collection in a new cursor's representation, in order to
+// inspect the row membership and remove any duplicates; this can have big
+// impact if a collection holds tens of thousands of rows or more, when
+// the original cursor with dups simply referenced rows indirectly by row
+// position ranges, without using an explicit row set representation.
+// Callers are encouraged to use nsIMdbCursor::GetCount() to determine
+// whether the row collection is very large (tens of thousands), and to
+// delay calling MakeUniqueCursor() when possible, until a user interface
+// element actually demands the creation of an explicit set representation.
+{
+ nsresult outErr = NS_OK;
+ nsIMdbTableRowCursor* outCursor = 0;
+
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ AddRef();
+ outCursor = this;
+
+ outErr = ev->AsErr();
+ }
+ if (acqCursor) *acqCursor = outCursor;
+ return outErr;
+}
+// } ----- end duplicate row removal methods -----
+
+// } ===== end nsIMdbTableRowCursor methods =====
+
+/*static*/ void morkTableRowCursor::NonTableRowCursorTypeError(morkEnv* ev) {
+ ev->NewError("non morkTableRowCursor");
+}
+
+mdb_pos morkTableRowCursor::NextRowOid(morkEnv* ev, mdbOid* outOid) {
+ mdb_pos outPos = -1;
+ (void)this->NextRow(ev, outOid, &outPos);
+ return outPos;
+}
+
+mdb_pos morkTableRowCursor::PrevRowOid(morkEnv* ev, mdbOid* outOid) {
+ mdb_pos outPos = -1;
+ (void)this->PrevRow(ev, outOid, &outPos);
+ return outPos;
+}
+
+mork_bool morkTableRowCursor::CanHaveDupRowMembers(morkEnv* ev) {
+ return morkBool_kFalse; // false default is correct
+}
+
+mork_count morkTableRowCursor::GetMemberCount(morkEnv* ev) {
+ morkTable* table = mTableRowCursor_Table;
+ if (table)
+ return table->mTable_RowArray.mArray_Fill;
+ else
+ return 0;
+}
+
+morkRow* morkTableRowCursor::PrevRow(morkEnv* ev, mdbOid* outOid,
+ mdb_pos* outPos) {
+ morkRow* outRow = 0;
+ mork_pos pos = -1;
+
+ morkTable* table = mTableRowCursor_Table;
+ if (table) {
+ if (table->IsOpenNode()) {
+ morkArray* array = &table->mTable_RowArray;
+ pos = mCursor_Pos - 1;
+
+ if (pos >= 0 && pos < (mork_pos)(array->mArray_Fill)) {
+ mCursor_Pos = pos; // update for next time
+ morkRow* row = (morkRow*)array->At(pos);
+ if (row) {
+ if (row->IsRow()) {
+ outRow = row;
+ *outOid = row->mRow_Oid;
+ } else
+ row->NonRowTypeError(ev);
+ } else
+ ev->NilPointerError();
+ } else {
+ outOid->mOid_Scope = 0;
+ outOid->mOid_Id = morkId_kMinusOne;
+ }
+ } else
+ table->NonOpenNodeError(ev);
+ } else
+ ev->NilPointerError();
+
+ *outPos = pos;
+ return outRow;
+}
+
+morkRow* morkTableRowCursor::NextRow(morkEnv* ev, mdbOid* outOid,
+ mdb_pos* outPos) {
+ morkRow* outRow = 0;
+ mork_pos pos = -1;
+
+ morkTable* table = mTableRowCursor_Table;
+ if (table) {
+ if (table->IsOpenNode()) {
+ morkArray* array = &table->mTable_RowArray;
+ pos = mCursor_Pos;
+ if (pos < 0)
+ pos = 0;
+ else
+ ++pos;
+
+ if (pos < (mork_pos)(array->mArray_Fill)) {
+ mCursor_Pos = pos; // update for next time
+ morkRow* row = (morkRow*)array->At(pos);
+ if (row) {
+ if (row->IsRow()) {
+ outRow = row;
+ *outOid = row->mRow_Oid;
+ } else
+ row->NonRowTypeError(ev);
+ } else
+ ev->NilPointerError();
+ } else {
+ outOid->mOid_Scope = 0;
+ outOid->mOid_Id = morkId_kMinusOne;
+ }
+ } else
+ table->NonOpenNodeError(ev);
+ } else
+ ev->NilPointerError();
+
+ *outPos = pos;
+ return outRow;
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkTableRowCursor.h b/comm/mailnews/db/mork/morkTableRowCursor.h
new file mode 100644
index 0000000000..9801eb174b
--- /dev/null
+++ b/comm/mailnews/db/mork/morkTableRowCursor.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKTABLEROWCURSOR_
+#define _MORKTABLEROWCURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class orkinTableRowCursor;
+#define morkDerived_kTableRowCursor /*i*/ 0x7243 /* ascii 'rC' */
+
+class morkTableRowCursor : public morkCursor,
+ public nsIMdbTableRowCursor { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkFactory* mObject_Factory; // weak ref to suite factory
+
+ // mork_seed mCursor_Seed;
+ // mork_pos mCursor_Pos;
+ // mork_bool mCursor_DoFailOnSeedOutOfSync;
+ // mork_u1 mCursor_Pad[ 3 ]; // explicitly pad to u4 alignment
+
+ public: // state is public because the entire Mork system is private
+ morkTable* mTableRowCursor_Table; // weak ref to table
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseTableRowCursor()
+
+ protected:
+ virtual ~morkTableRowCursor(); // assert that close executed earlier
+
+ public: // morkTableRowCursor construction & destruction
+ morkTableRowCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkTable* ioTable, mork_pos inRowPos);
+ void CloseTableRowCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkTableRowCursor(const morkTableRowCursor& other);
+ morkTableRowCursor& operator=(const morkTableRowCursor& other);
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // { ----- begin attribute methods -----
+ NS_IMETHOD GetCount(nsIMdbEnv* ev, mdb_count* outCount) override; // readonly
+ NS_IMETHOD GetSeed(nsIMdbEnv* ev, mdb_seed* outSeed) override; // readonly
+
+ NS_IMETHOD SetPos(nsIMdbEnv* ev, mdb_pos inPos) override; // mutable
+ NS_IMETHOD GetPos(nsIMdbEnv* ev, mdb_pos* outPos) override;
+
+ NS_IMETHOD SetDoFailOnSeedOutOfSync(nsIMdbEnv* ev, mdb_bool inFail) override;
+ NS_IMETHOD GetDoFailOnSeedOutOfSync(nsIMdbEnv* ev,
+ mdb_bool* outFail) override;
+
+ // } ----- end attribute methods -----
+ NS_IMETHOD GetTable(nsIMdbEnv* ev, nsIMdbTable** acqTable) override;
+ // } ----- end attribute methods -----
+
+ // { ----- begin duplicate row removal methods -----
+ NS_IMETHOD CanHaveDupRowMembers(nsIMdbEnv* ev, // cursor might hold dups?
+ mdb_bool* outCanHaveDups) override;
+
+ NS_IMETHOD MakeUniqueCursor( // clone cursor, removing duplicate rows
+ nsIMdbEnv* ev, // context
+ nsIMdbTableRowCursor** acqCursor) override; // acquire clone with no dups
+ // } ----- end duplicate row removal methods -----
+
+ // { ----- begin oid iteration methods -----
+ NS_IMETHOD NextRowOid( // get row id of next row in the table
+ nsIMdbEnv* ev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) override; // zero-based position of the row in table
+ NS_IMETHOD PrevRowOid( // get row id of previous row in the table
+ nsIMdbEnv* ev, // context
+ mdbOid* outOid, // out row oid
+ mdb_pos* outRowPos) override; // zero-based position of the row in table
+ // } ----- end oid iteration methods -----
+
+ // { ----- begin row iteration methods -----
+ NS_IMETHOD NextRow( // get row cells from table for cells already in row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // acquire next row in table
+ mdb_pos* outRowPos) override; // zero-based position of the row in table
+ NS_IMETHOD PrevRow( // get row cells from table for cells already in row
+ nsIMdbEnv* ev, // context
+ nsIMdbRow** acqRow, // acquire previous row in table
+ mdb_pos* outRowPos) override; // zero-based position of the row in table
+
+ // } ----- end row iteration methods -----
+
+ public: // dynamic type identification
+ mork_bool IsTableRowCursor() const {
+ return IsNode() && mNode_Derived == morkDerived_kTableRowCursor;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonTableRowCursorTypeError(morkEnv* ev);
+
+ public: // oid only iteration
+ mdb_pos NextRowOid(morkEnv* ev, mdbOid* outOid);
+ mdb_pos PrevRowOid(morkEnv* ev, mdbOid* outOid);
+
+ public: // other table row cursor methods
+ virtual mork_bool CanHaveDupRowMembers(morkEnv* ev);
+ virtual mork_count GetMemberCount(morkEnv* ev);
+
+ virtual morkRow* NextRow(morkEnv* ev, mdbOid* outOid, mdb_pos* outPos);
+ virtual morkRow* PrevRow(morkEnv* ev, mdbOid* outOid, mdb_pos* outPos);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakTableRowCursor(morkTableRowCursor* me, morkEnv* ev,
+ morkTableRowCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongTableRowCursor(morkTableRowCursor* me, morkEnv* ev,
+ morkTableRowCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKTABLEROWCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkThumb.cpp b/comm/mailnews/db/mork/morkThumb.cpp
new file mode 100644
index 0000000000..3076ca6f3e
--- /dev/null
+++ b/comm/mailnews/db/mork/morkThumb.cpp
@@ -0,0 +1,455 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKTHUMB_
+# include "morkThumb.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+// #ifndef _MORKFILE_
+// #include "morkFile.h"
+// #endif
+
+#ifndef _MORKWRITER_
+# include "morkWriter.h"
+#endif
+
+#ifndef _MORKPARSER_
+# include "morkParser.h"
+#endif
+
+#ifndef _MORKBUILDER_
+# include "morkBuilder.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkThumb::CloseMorkNode(
+ morkEnv* ev) // CloseThumb() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseThumb(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkThumb::~morkThumb() // assert CloseThumb() executed earlier
+{
+ CloseMorkNode(mMorkEnv);
+ MORK_ASSERT(mThumb_Magic == 0);
+ MORK_ASSERT(mThumb_Store == 0);
+ MORK_ASSERT(mThumb_File == 0);
+}
+
+/*public non-poly*/
+morkThumb::morkThumb(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_magic inMagic)
+ : morkObject(ev, inUsage, ioHeap, morkColor_kNone, (morkHandle*)0),
+ mThumb_Magic(0),
+ mThumb_Total(0),
+ mThumb_Current(0)
+
+ ,
+ mThumb_Done(morkBool_kFalse),
+ mThumb_Broken(morkBool_kFalse),
+ mThumb_Seed(0)
+
+ ,
+ mThumb_Store(0),
+ mThumb_File(0),
+ mThumb_Writer(0),
+ mThumb_Builder(0),
+ mThumb_SourcePort(0)
+
+ ,
+ mThumb_DoCollect(morkBool_kFalse) {
+ if (ev->Good()) {
+ if (ioSlotHeap) {
+ mThumb_Magic = inMagic;
+ mNode_Derived = morkDerived_kThumb;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(morkThumb, morkObject, nsIMdbThumb)
+
+/*public non-poly*/ void morkThumb::CloseThumb(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ mThumb_Magic = 0;
+ if (mThumb_Builder && mThumb_Store) mThumb_Store->ForgetBuilder(ev);
+ morkBuilder::SlotStrongBuilder((morkBuilder*)0, ev, &mThumb_Builder);
+
+ morkWriter::SlotStrongWriter((morkWriter*)0, ev, &mThumb_Writer);
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mThumb_File);
+ morkStore::SlotStrongStore((morkStore*)0, ev, &mThumb_Store);
+ morkStore::SlotStrongPort((morkPort*)0, ev, &mThumb_SourcePort);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+// { ===== begin nsIMdbThumb methods =====
+NS_IMETHODIMP
+morkThumb::GetProgress(nsIMdbEnv* mev, mdb_count* outTotal,
+ mdb_count* outCurrent, mdb_bool* outDone,
+ mdb_bool* outBroken) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ GetProgress(ev, outTotal, outCurrent, outDone, outBroken);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkThumb::DoMore(nsIMdbEnv* mev, mdb_count* outTotal, mdb_count* outCurrent,
+ mdb_bool* outDone, mdb_bool* outBroken) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ DoMore(ev, outTotal, outCurrent, outDone, outBroken);
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+
+NS_IMETHODIMP
+morkThumb::CancelAndBreakThumb(nsIMdbEnv* mev) {
+ nsresult outErr = NS_OK;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ mThumb_Done = morkBool_kTrue;
+ mThumb_Broken = morkBool_kTrue;
+ CloseMorkNode(ev); // should I close this here?
+ outErr = ev->AsErr();
+ }
+ return outErr;
+}
+// } ===== end nsIMdbThumb methods =====
+
+/*static*/ void morkThumb::NonThumbTypeError(morkEnv* ev) {
+ ev->NewError("non morkThumb");
+}
+
+/*static*/ void morkThumb::UnsupportedThumbMagicError(morkEnv* ev) {
+ ev->NewError("unsupported mThumb_Magic");
+}
+
+/*static*/ void morkThumb::NilThumbStoreError(morkEnv* ev) {
+ ev->NewError("nil mThumb_Store");
+}
+
+/*static*/ void morkThumb::NilThumbFileError(morkEnv* ev) {
+ ev->NewError("nil mThumb_File");
+}
+
+/*static*/ void morkThumb::NilThumbWriterError(morkEnv* ev) {
+ ev->NewError("nil mThumb_Writer");
+}
+
+/*static*/ void morkThumb::NilThumbBuilderError(morkEnv* ev) {
+ ev->NewError("nil mThumb_Builder");
+}
+
+/*static*/ void morkThumb::NilThumbSourcePortError(morkEnv* ev) {
+ ev->NewError("nil mThumb_SourcePort");
+}
+
+/*static*/ morkThumb* morkThumb::Make_OpenFileStore(morkEnv* ev,
+ nsIMdbHeap* ioHeap,
+ morkStore* ioStore) {
+ morkThumb* outThumb = 0;
+ if (ioHeap && ioStore) {
+ nsIMdbFile* file = ioStore->mStore_File;
+ if (file) {
+ mork_pos fileEof = 0;
+ file->Eof(ev->AsMdbEnv(), &fileEof);
+ if (ev->Good()) {
+ outThumb =
+ new (*ioHeap, ev) morkThumb(ev, morkUsage::kHeap, ioHeap, ioHeap,
+ morkThumb_kMagic_OpenFileStore);
+
+ if (outThumb) {
+ morkBuilder* builder = ioStore->LazyGetBuilder(ev);
+ if (builder) {
+ outThumb->mThumb_Total = (mork_count)fileEof;
+ morkStore::SlotStrongStore(ioStore, ev, &outThumb->mThumb_Store);
+ morkBuilder::SlotStrongBuilder(builder, ev,
+ &outThumb->mThumb_Builder);
+ }
+ }
+ }
+ } else
+ ioStore->NilStoreFileError(ev);
+ } else
+ ev->NilPointerError();
+
+ return outThumb;
+}
+
+/*static*/ morkThumb* morkThumb::Make_LargeCommit(morkEnv* ev,
+ nsIMdbHeap* ioHeap,
+ morkStore* ioStore) {
+ morkThumb* outThumb = 0;
+ if (ioHeap && ioStore) {
+ nsIMdbFile* file = ioStore->mStore_File;
+ if (file) {
+ outThumb = new (*ioHeap, ev) morkThumb(
+ ev, morkUsage::kHeap, ioHeap, ioHeap, morkThumb_kMagic_LargeCommit);
+
+ if (outThumb) {
+ morkWriter* writer = new (*ioHeap, ev)
+ morkWriter(ev, morkUsage::kHeap, ioHeap, ioStore, file, ioHeap);
+ if (writer) {
+ writer->mWriter_CommitGroupIdentity =
+ ++ioStore->mStore_CommitGroupIdentity;
+ writer->mWriter_NeedDirtyAll = morkBool_kFalse;
+ outThumb->mThumb_DoCollect = morkBool_kFalse;
+ morkStore::SlotStrongStore(ioStore, ev, &outThumb->mThumb_Store);
+
+ nsIMdbFile_SlotStrongFile(file, ev, &outThumb->mThumb_File);
+
+ outThumb->mThumb_Writer = writer; // pass writer ownership to thumb
+ }
+ }
+ } else
+ ioStore->NilStoreFileError(ev);
+ } else
+ ev->NilPointerError();
+
+ return outThumb;
+}
+
+/*static*/ morkThumb* morkThumb::Make_CompressCommit(morkEnv* ev,
+ nsIMdbHeap* ioHeap,
+ morkStore* ioStore,
+ mork_bool inDoCollect) {
+ morkThumb* outThumb = 0;
+ if (ioHeap && ioStore) {
+ nsIMdbFile* file = ioStore->mStore_File;
+ if (file) {
+ outThumb =
+ new (*ioHeap, ev) morkThumb(ev, morkUsage::kHeap, ioHeap, ioHeap,
+ morkThumb_kMagic_CompressCommit);
+
+ if (outThumb) {
+ morkWriter* writer = new (*ioHeap, ev)
+ morkWriter(ev, morkUsage::kHeap, ioHeap, ioStore, file, ioHeap);
+ if (writer) {
+ writer->mWriter_NeedDirtyAll = morkBool_kTrue;
+ outThumb->mThumb_DoCollect = inDoCollect;
+ morkStore::SlotStrongStore(ioStore, ev, &outThumb->mThumb_Store);
+ nsIMdbFile_SlotStrongFile(file, ev, &outThumb->mThumb_File);
+ outThumb->mThumb_Writer = writer; // pass writer ownership to thumb
+
+ // cope with fact that parsed transaction groups are going away:
+ ioStore->mStore_FirstCommitGroupPos = 0;
+ ioStore->mStore_SecondCommitGroupPos = 0;
+ }
+ }
+ } else
+ ioStore->NilStoreFileError(ev);
+ } else
+ ev->NilPointerError();
+
+ return outThumb;
+}
+
+// { ===== begin non-poly methods imitating nsIMdbThumb =====
+void morkThumb::GetProgress(morkEnv* ev, mdb_count* outTotal,
+ mdb_count* outCurrent, mdb_bool* outDone,
+ mdb_bool* outBroken) {
+ MORK_USED_1(ev);
+ if (outTotal) *outTotal = mThumb_Total;
+ if (outCurrent) *outCurrent = mThumb_Current;
+ if (outDone) *outDone = mThumb_Done;
+ if (outBroken) *outBroken = mThumb_Broken;
+}
+
+void morkThumb::DoMore(morkEnv* ev, mdb_count* outTotal, mdb_count* outCurrent,
+ mdb_bool* outDone, mdb_bool* outBroken) {
+ if (!mThumb_Done && !mThumb_Broken) {
+ switch (mThumb_Magic) {
+ case morkThumb_kMagic_OpenFilePort: // 1 /* factory method */
+ this->DoMore_OpenFilePort(ev);
+ break;
+
+ case morkThumb_kMagic_OpenFileStore: // 2 /* factory method */
+ this->DoMore_OpenFileStore(ev);
+ break;
+
+ case morkThumb_kMagic_ExportToFormat: // 3 /* port method */
+ this->DoMore_ExportToFormat(ev);
+ break;
+
+ case morkThumb_kMagic_ImportContent: // 4 /* store method */
+ this->DoMore_ImportContent(ev);
+ break;
+
+ case morkThumb_kMagic_LargeCommit: // 5 /* store method */
+ this->DoMore_LargeCommit(ev);
+ break;
+
+ case morkThumb_kMagic_SessionCommit: // 6 /* store method */
+ this->DoMore_SessionCommit(ev);
+ break;
+
+ case morkThumb_kMagic_CompressCommit: // 7 /* store method */
+ this->DoMore_CompressCommit(ev);
+ break;
+
+ case morkThumb_kMagic_SearchManyColumns: // 8 /* table method */
+ this->DoMore_SearchManyColumns(ev);
+ break;
+
+ case morkThumb_kMagic_NewSortColumn: // 9 /* table metho) */
+ this->DoMore_NewSortColumn(ev);
+ break;
+
+ case morkThumb_kMagic_NewSortColumnWithCompare: // 10 /* table method */
+ this->DoMore_NewSortColumnWithCompare(ev);
+ break;
+
+ case morkThumb_kMagic_CloneSortColumn: // 11 /* table method */
+ this->DoMore_CloneSortColumn(ev);
+ break;
+
+ case morkThumb_kMagic_AddIndex: // 12 /* table method */
+ this->DoMore_AddIndex(ev);
+ break;
+
+ case morkThumb_kMagic_CutIndex: // 13 /* table method */
+ this->DoMore_CutIndex(ev);
+ break;
+
+ default:
+ this->UnsupportedThumbMagicError(ev);
+ break;
+ }
+ }
+ if (outTotal) *outTotal = mThumb_Total;
+ if (outCurrent) *outCurrent = mThumb_Current;
+ if (outDone) *outDone = mThumb_Done;
+ if (outBroken) *outBroken = mThumb_Broken;
+}
+
+void morkThumb::CancelAndBreakThumb(morkEnv* ev) {
+ MORK_USED_1(ev);
+ mThumb_Broken = morkBool_kTrue;
+}
+
+// } ===== end non-poly methods imitating nsIMdbThumb =====
+
+morkStore* morkThumb::ThumbToOpenStore(morkEnv* ev)
+// for orkinFactory::ThumbToOpenStore() after OpenFileStore()
+{
+ MORK_USED_1(ev);
+ return mThumb_Store;
+}
+
+void morkThumb::DoMore_OpenFilePort(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_OpenFileStore(morkEnv* ev) {
+ morkBuilder* builder = mThumb_Builder;
+ if (builder) {
+ mork_pos pos = 0;
+ builder->ParseMore(ev, &pos, &mThumb_Done, &mThumb_Broken);
+ // mThumb_Total = builder->mBuilder_TotalCount;
+ // mThumb_Current = builder->mBuilder_DoneCount;
+ mThumb_Current = (mork_count)pos;
+ } else {
+ this->NilThumbBuilderError(ev);
+ mThumb_Broken = morkBool_kTrue;
+ mThumb_Done = morkBool_kTrue;
+ }
+}
+
+void morkThumb::DoMore_ExportToFormat(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_ImportContent(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_LargeCommit(morkEnv* ev) { this->DoMore_Commit(ev); }
+
+void morkThumb::DoMore_SessionCommit(morkEnv* ev) { this->DoMore_Commit(ev); }
+
+void morkThumb::DoMore_Commit(morkEnv* ev) {
+ morkWriter* writer = mThumb_Writer;
+ if (writer) {
+ writer->WriteMore(ev);
+ mThumb_Total = writer->mWriter_TotalCount;
+ mThumb_Current = writer->mWriter_DoneCount;
+ mThumb_Done = (ev->Bad() || writer->IsWritingDone());
+ mThumb_Broken = ev->Bad();
+ } else {
+ this->NilThumbWriterError(ev);
+ mThumb_Broken = morkBool_kTrue;
+ mThumb_Done = morkBool_kTrue;
+ }
+}
+
+void morkThumb::DoMore_CompressCommit(morkEnv* ev) { this->DoMore_Commit(ev); }
+
+void morkThumb::DoMore_SearchManyColumns(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_NewSortColumn(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_NewSortColumnWithCompare(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_CloneSortColumn(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_AddIndex(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+void morkThumb::DoMore_CutIndex(morkEnv* ev) {
+ this->UnsupportedThumbMagicError(ev);
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkThumb.h b/comm/mailnews/db/mork/morkThumb.h
new file mode 100644
index 0000000000..0e4f9f4592
--- /dev/null
+++ b/comm/mailnews/db/mork/morkThumb.h
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKTHUMB_
+#define _MORKTHUMB_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKOBJECT_
+# include "morkObject.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkThumb_kMagic_OpenFilePort 1 /* factory method */
+#define morkThumb_kMagic_OpenFileStore 2 /* factory method */
+#define morkThumb_kMagic_ExportToFormat 3 /* port method */
+#define morkThumb_kMagic_ImportContent 4 /* store method */
+#define morkThumb_kMagic_LargeCommit 5 /* store method */
+#define morkThumb_kMagic_SessionCommit 6 /* store method */
+#define morkThumb_kMagic_CompressCommit 7 /* store method */
+#define morkThumb_kMagic_SearchManyColumns 8 /* table method */
+#define morkThumb_kMagic_NewSortColumn 9 /* table metho) */
+#define morkThumb_kMagic_NewSortColumnWithCompare 10 /* table method */
+#define morkThumb_kMagic_CloneSortColumn 11 /* table method */
+#define morkThumb_kMagic_AddIndex 12 /* table method */
+#define morkThumb_kMagic_CutIndex 13 /* table method */
+
+#define morkDerived_kThumb /*i*/ 0x5468 /* ascii 'Th' */
+
+/*| morkThumb:
+|*/
+class morkThumb : public morkObject, public nsIMdbThumb {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // mork_color mBead_Color; // ID for this bead
+ // morkHandle* mObject_Handle; // weak ref to handle for this object
+
+ public: // state is public because the entire Mork system is private
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // { ===== begin nsIMdbThumb methods =====
+ NS_IMETHOD GetProgress(nsIMdbEnv* ev, mdb_count* outTotal,
+ mdb_count* outCurrent, mdb_bool* outDone,
+ mdb_bool* outBroken) override;
+
+ NS_IMETHOD DoMore(nsIMdbEnv* ev, mdb_count* outTotal, mdb_count* outCurrent,
+ mdb_bool* outDone, mdb_bool* outBroken) override;
+
+ NS_IMETHOD CancelAndBreakThumb(nsIMdbEnv* ev) override;
+ // } ===== end nsIMdbThumb methods =====
+
+ // might as well include all the return values here:
+
+ mork_magic mThumb_Magic; // magic sig different in each thumb type
+ mork_count mThumb_Total;
+ mork_count mThumb_Current;
+
+ mork_bool mThumb_Done;
+ mork_bool mThumb_Broken;
+ mork_u2 mThumb_Seed; // optional seed for u4 alignment padding
+
+ morkStore* mThumb_Store; // weak ref to created store
+ nsIMdbFile* mThumb_File; // strong ref to file (store, import, export)
+ morkWriter* mThumb_Writer; // strong ref to writer (for commit)
+ morkBuilder* mThumb_Builder; // strong ref to builder (for store open)
+ morkPort* mThumb_SourcePort; // strong ref to port for import
+
+ mork_bool mThumb_DoCollect; // influence whether a collect happens
+ mork_bool mThumb_Pad[3]; // padding for u4 alignment
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(
+ morkEnv* ev) override; // CloseThumb() only if open
+
+ public: // morkThumb construction & destruction
+ morkThumb(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ nsIMdbHeap* ioSlotHeap, mork_magic inMagic);
+ void CloseThumb(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkThumb(const morkThumb& other);
+ morkThumb& operator=(const morkThumb& other);
+ virtual ~morkThumb(); // assert that CloseThumb() executed earlier
+
+ public: // dynamic type identification
+ mork_bool IsThumb() const {
+ return IsNode() && mNode_Derived == morkDerived_kThumb;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonThumbTypeError(morkEnv* ev);
+ static void UnsupportedThumbMagicError(morkEnv* ev);
+
+ static void NilThumbStoreError(morkEnv* ev);
+ static void NilThumbFileError(morkEnv* ev);
+ static void NilThumbWriterError(morkEnv* ev);
+ static void NilThumbBuilderError(morkEnv* ev);
+ static void NilThumbSourcePortError(morkEnv* ev);
+
+ public: // 'do more' methods
+ void DoMore_OpenFilePort(morkEnv* ev);
+ void DoMore_OpenFileStore(morkEnv* ev);
+ void DoMore_ExportToFormat(morkEnv* ev);
+ void DoMore_ImportContent(morkEnv* ev);
+ void DoMore_LargeCommit(morkEnv* ev);
+ void DoMore_SessionCommit(morkEnv* ev);
+ void DoMore_CompressCommit(morkEnv* ev);
+ void DoMore_Commit(morkEnv* ev);
+ void DoMore_SearchManyColumns(morkEnv* ev);
+ void DoMore_NewSortColumn(morkEnv* ev);
+ void DoMore_NewSortColumnWithCompare(morkEnv* ev);
+ void DoMore_CloneSortColumn(morkEnv* ev);
+ void DoMore_AddIndex(morkEnv* ev);
+ void DoMore_CutIndex(morkEnv* ev);
+
+ public: // other thumb methods
+ morkStore* ThumbToOpenStore(morkEnv* ev);
+ // for orkinFactory::ThumbToOpenStore() after OpenFileStore()
+
+ public: // assorted thumb constructors
+ static morkThumb* Make_OpenFileStore(morkEnv* ev, nsIMdbHeap* ioHeap,
+ morkStore* ioStore);
+
+ static morkThumb* Make_CompressCommit(morkEnv* ev, nsIMdbHeap* ioHeap,
+ morkStore* ioStore,
+ mork_bool inDoCollect);
+
+ static morkThumb* Make_LargeCommit(morkEnv* ev, nsIMdbHeap* ioHeap,
+ morkStore* ioStore);
+
+ // { ===== begin non-poly methods imitating nsIMdbThumb =====
+ void GetProgress(morkEnv* ev, mdb_count* outTotal, mdb_count* outCurrent,
+ mdb_bool* outDone, mdb_bool* outBroken);
+
+ void DoMore(morkEnv* ev, mdb_count* outTotal, mdb_count* outCurrent,
+ mdb_bool* outDone, mdb_bool* outBroken);
+
+ void CancelAndBreakThumb(morkEnv* ev);
+ // } ===== end non-poly methods imitating nsIMdbThumb =====
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakThumb(morkThumb* me, morkEnv* ev, morkThumb** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongThumb(morkThumb* me, morkEnv* ev, morkThumb** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKTHUMB_ */
diff --git a/comm/mailnews/db/mork/morkUniqRowCursor.h b/comm/mailnews/db/mork/morkUniqRowCursor.h
new file mode 100644
index 0000000000..4099f19996
--- /dev/null
+++ b/comm/mailnews/db/mork/morkUniqRowCursor.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKUNIQROWCURSOR_
+#define _MORKUNIQROWCURSOR_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKCURSOR_
+# include "morkCursor.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+class orkinTableRowCursor;
+// #define morkDerived_kUniqRowCursor /*i*/ 0x7352 /* ascii 'sR' */
+
+class morkUniqRowCursor : public morkTableRowCursor { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ // morkFactory* mObject_Factory; // weak ref to suite factory
+
+ // mork_seed mCursor_Seed;
+ // mork_pos mCursor_Pos;
+ // mork_bool mCursor_DoFailOnSeedOutOfSync;
+ // mork_u1 mCursor_Pad[ 3 ]; // explicitly pad to u4 alignment
+
+ // morkTable* mTableRowCursor_Table; // weak ref to table
+
+ // { ===== begin morkNode interface =====
+ public:
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseUniqRowCursor()
+ virtual ~morkUniqRowCursor(); // assert that close executed earlier
+
+ public: // morkUniqRowCursor construction & destruction
+ morkUniqRowCursor(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkTable* ioTable, mork_pos inRowPos);
+ void CloseUniqRowCursor(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkUniqRowCursor(const morkUniqRowCursor& other);
+ morkUniqRowCursor& operator=(const morkUniqRowCursor& other);
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonUniqRowCursorTypeError(morkEnv* ev);
+
+ public: // other search row cursor methods
+ virtual mork_bool CanHaveDupRowMembers(morkEnv* ev);
+ virtual mork_count GetMemberCount(morkEnv* ev);
+
+ virtual orkinTableRowCursor* AcquireUniqueRowCursorHandle(morkEnv* ev);
+
+ // virtual mdb_pos NextRowOid(morkEnv* ev, mdbOid* outOid);
+ virtual morkRow* NextRow(morkEnv* ev, mdbOid* outOid, mdb_pos* outPos);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakUniqRowCursor(morkUniqRowCursor* me, morkEnv* ev,
+ morkUniqRowCursor** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongUniqRowCursor(morkUniqRowCursor* me, morkEnv* ev,
+ morkUniqRowCursor** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKUNIQROWCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkWriter.cpp b/comm/mailnews/db/mork/morkWriter.cpp
new file mode 100644
index 0000000000..dc1bb1a1ed
--- /dev/null
+++ b/comm/mailnews/db/mork/morkWriter.cpp
@@ -0,0 +1,1936 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKBLOB_
+# include "morkBlob.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKARRAY_
+# include "morkWriter.h"
+#endif
+
+// #ifndef _MORKFILE_
+// #include "morkFile.h"
+// #endif
+
+#ifndef _MORKSTREAM_
+# include "morkStream.h"
+#endif
+
+#ifndef _MORKSTORE_
+# include "morkStore.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKATOMMAP_
+# include "morkAtomMap.h"
+#endif
+
+#ifndef _MORKROW_
+# include "morkRow.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKCELL_
+# include "morkCell.h"
+#endif
+
+#ifndef _MORKATOM_
+# include "morkAtom.h"
+#endif
+
+#ifndef _MORKCH_
+# include "morkCh.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkWriter::CloseMorkNode(
+ morkEnv* ev) // CloseTable() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseWriter(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkWriter::~morkWriter() // assert CloseTable() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+ MORK_ASSERT(mWriter_Store == 0);
+}
+
+/*public non-poly*/
+morkWriter::morkWriter(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioHeap, morkStore* ioStore,
+ nsIMdbFile* ioFile, nsIMdbHeap* ioSlotHeap)
+ : morkNode(ev, inUsage, ioHeap),
+ mWriter_Store(0),
+ mWriter_File(0),
+ mWriter_Bud(0),
+ mWriter_Stream(0),
+ mWriter_SlotHeap(0)
+
+ ,
+ mWriter_CommitGroupIdentity(0) // see mStore_CommitGroupIdentity
+ ,
+ mWriter_GroupBufFill(0)
+
+ ,
+ mWriter_TotalCount(morkWriter_kCountNumberOfPhases),
+ mWriter_DoneCount(0)
+
+ ,
+ mWriter_LineSize(0),
+ mWriter_MaxIndent(morkWriter_kMaxIndent),
+ mWriter_MaxLine(morkWriter_kMaxLine)
+
+ ,
+ mWriter_TableForm(0),
+ mWriter_TableAtomScope('v'),
+ mWriter_TableRowScope(0),
+ mWriter_TableKind(0)
+
+ ,
+ mWriter_RowForm(0),
+ mWriter_RowAtomScope(0),
+ mWriter_RowScope(0)
+
+ ,
+ mWriter_DictForm(0),
+ mWriter_DictAtomScope('v')
+
+ ,
+ mWriter_NeedDirtyAll(morkBool_kFalse),
+ mWriter_Incremental(morkBool_kTrue) // opposite of mWriter_NeedDirtyAll
+ ,
+ mWriter_DidStartDict(morkBool_kFalse),
+ mWriter_DidEndDict(morkBool_kTrue)
+
+ ,
+ mWriter_SuppressDirtyRowNewline(morkBool_kFalse),
+ mWriter_DidStartGroup(morkBool_kFalse),
+ mWriter_DidEndGroup(morkBool_kTrue),
+ mWriter_Phase(morkWriter_kPhaseNothingDone)
+
+ ,
+ mWriter_BeVerbose(ev->mEnv_BeVerbose)
+
+ ,
+ mWriter_TableRowArrayPos(0)
+
+ // empty constructors for map iterators:
+ ,
+ mWriter_StoreAtomSpacesIter(),
+ mWriter_AtomSpaceAtomAidsIter()
+
+ ,
+ mWriter_StoreRowSpacesIter(),
+ mWriter_RowSpaceTablesIter(),
+ mWriter_RowSpaceRowsIter() {
+ mWriter_GroupBuf[0] = 0;
+
+ mWriter_SafeNameBuf[0] = 0;
+ mWriter_SafeNameBuf[morkWriter_kMaxColumnNameSize * 2] = 0;
+ mWriter_ColNameBuf[0] = 0;
+ mWriter_ColNameBuf[morkWriter_kMaxColumnNameSize] = 0;
+
+ mdbYarn* y = &mWriter_ColYarn;
+ y->mYarn_Buf = mWriter_ColNameBuf; // where to put col bytes
+ y->mYarn_Fill = 0; // set later by writer
+ y->mYarn_Size = morkWriter_kMaxColumnNameSize; // our buf size
+ y->mYarn_More = 0; // set later by writer
+ y->mYarn_Form = 0; // set later by writer
+ y->mYarn_Grow = 0; // do not allow buffer growth
+
+ y = &mWriter_SafeYarn;
+ y->mYarn_Buf = mWriter_SafeNameBuf; // where to put col bytes
+ y->mYarn_Fill = 0; // set later by writer
+ y->mYarn_Size = morkWriter_kMaxColumnNameSize * 2; // our buf size
+ y->mYarn_More = 0; // set later by writer
+ y->mYarn_Form = 0; // set later by writer
+ y->mYarn_Grow = 0; // do not allow buffer growth
+
+ if (ev->Good()) {
+ if (ioSlotHeap && ioFile && ioStore) {
+ morkStore::SlotWeakStore(ioStore, ev, &mWriter_Store);
+ nsIMdbFile_SlotStrongFile(ioFile, ev, &mWriter_File);
+ nsIMdbHeap_SlotStrongHeap(ioSlotHeap, ev, &mWriter_SlotHeap);
+ if (ev->Good()) {
+ mNode_Derived = morkDerived_kWriter;
+ }
+ } else
+ ev->NilPointerError();
+ }
+}
+
+void morkWriter::MakeWriterStream(morkEnv* ev) // give writer a suitable stream
+{
+ mWriter_Incremental = !mWriter_NeedDirtyAll; // opposites
+
+ if (!mWriter_Stream && ev->Good()) {
+ if (mWriter_File) {
+ morkStream* stream = 0;
+ mork_bool frozen = morkBool_kFalse; // need to modify
+ nsIMdbHeap* heap = mWriter_SlotHeap;
+
+ if (mWriter_Incremental) {
+ stream =
+ new (*heap, ev) morkStream(ev, morkUsage::kHeap, heap, mWriter_File,
+ morkWriter_kStreamBufSize, frozen);
+ } else // compress commit
+ {
+ nsIMdbFile* bud = 0;
+ mWriter_File->AcquireBud(ev->AsMdbEnv(), heap, &bud);
+ if (bud) {
+ if (ev->Good()) {
+ mWriter_Bud = bud;
+ stream =
+ new (*heap, ev) morkStream(ev, morkUsage::kHeap, heap, bud,
+ morkWriter_kStreamBufSize, frozen);
+ } else
+ bud->Release();
+ }
+ }
+
+ if (stream) {
+ if (ev->Good())
+ mWriter_Stream = stream;
+ else
+ stream->CutStrongRef(ev->AsMdbEnv());
+ }
+ } else
+ this->NilWriterFileError(ev);
+ }
+}
+
+/*public non-poly*/ void morkWriter::CloseWriter(
+ morkEnv* ev) // called by CloseMorkNode();
+{
+ if (this->IsNode()) {
+ morkStore::SlotWeakStore((morkStore*)0, ev, &mWriter_Store);
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mWriter_File);
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mWriter_Bud);
+ morkStream::SlotStrongStream((morkStream*)0, ev, &mWriter_Stream);
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mWriter_SlotHeap);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkWriter::NonWriterTypeError(morkEnv* ev) {
+ ev->NewError("non morkWriter");
+}
+
+/*static*/ void morkWriter::NilWriterStoreError(morkEnv* ev) {
+ ev->NewError("nil mWriter_Store");
+}
+
+/*static*/ void morkWriter::NilWriterBudError(morkEnv* ev) {
+ ev->NewError("nil mWriter_Bud");
+}
+
+/*static*/ void morkWriter::NilWriterFileError(morkEnv* ev) {
+ ev->NewError("nil mWriter_File");
+}
+
+/*static*/ void morkWriter::NilWriterStreamError(morkEnv* ev) {
+ ev->NewError("nil mWriter_Stream");
+}
+
+/*static*/ void morkWriter::UnsupportedPhaseError(morkEnv* ev) {
+ ev->NewError("unsupported mWriter_Phase");
+}
+
+mork_bool morkWriter::WriteMore(
+ morkEnv* ev) // call until IsWritingDone() is true
+{
+ if (this->IsOpenNode()) {
+ if (this->IsWriter()) {
+ if (!mWriter_Stream) this->MakeWriterStream(ev);
+
+ if (mWriter_Stream) {
+ if (ev->Bad()) {
+ ev->NewWarning("writing stops on error");
+ mWriter_Phase = morkWriter_kPhaseWritingDone;
+ }
+ switch (mWriter_Phase) {
+ case morkWriter_kPhaseNothingDone:
+ OnNothingDone(ev);
+ break;
+
+ case morkWriter_kPhaseDirtyAllDone:
+ OnDirtyAllDone(ev);
+ break;
+
+ case morkWriter_kPhasePutHeaderDone:
+ OnPutHeaderDone(ev);
+ break;
+
+ case morkWriter_kPhaseRenumberAllDone:
+ OnRenumberAllDone(ev);
+ break;
+
+ case morkWriter_kPhaseStoreAtomSpaces:
+ OnStoreAtomSpaces(ev);
+ break;
+
+ case morkWriter_kPhaseAtomSpaceAtomAids:
+ OnAtomSpaceAtomAids(ev);
+ break;
+
+ case morkWriter_kPhaseStoreRowSpacesTables:
+ OnStoreRowSpacesTables(ev);
+ break;
+
+ case morkWriter_kPhaseRowSpaceTables:
+ OnRowSpaceTables(ev);
+ break;
+
+ case morkWriter_kPhaseTableRowArray:
+ OnTableRowArray(ev);
+ break;
+
+ case morkWriter_kPhaseStoreRowSpacesRows:
+ OnStoreRowSpacesRows(ev);
+ break;
+
+ case morkWriter_kPhaseRowSpaceRows:
+ OnRowSpaceRows(ev);
+ break;
+
+ case morkWriter_kPhaseContentDone:
+ OnContentDone(ev);
+ break;
+
+ case morkWriter_kPhaseWritingDone:
+ OnWritingDone(ev);
+ break;
+
+ default:
+ this->UnsupportedPhaseError(ev);
+ }
+ } else
+ this->NilWriterStreamError(ev);
+ } else
+ this->NonWriterTypeError(ev);
+ } else
+ this->NonOpenNodeError(ev);
+
+ return ev->Good();
+}
+
+static const char morkWriter_kHexDigits[] = "0123456789ABCDEF";
+
+mork_size morkWriter::WriteYarn(morkEnv* ev, const mdbYarn* inYarn)
+// return number of atom bytes written on the current line (which
+// implies that escaped line breaks will make the size value smaller
+// than the entire yarn's size, since only part goes on a last line).
+{
+ mork_size outSize = 0;
+ mork_size lineSize = mWriter_LineSize;
+ morkStream* stream = mWriter_Stream;
+
+ const mork_u1* b = (const mork_u1*)inYarn->mYarn_Buf;
+ if (b) {
+ int c;
+ mork_fill fill = inYarn->mYarn_Fill;
+
+ const mork_u1* end = b + fill;
+ while (b < end && ev->Good()) {
+ if (lineSize + outSize >= mWriter_MaxLine) // continue line?
+ {
+ stream->PutByteThenNewline(ev, '\\');
+ mWriter_LineSize = lineSize = outSize = 0;
+ }
+
+ c = *b++; // next byte to print
+ if (morkCh_IsValue(c)) {
+ stream->Putc(ev, c);
+ ++outSize; // c
+ } else if (c == ')' || c == '$' || c == '\\') {
+ stream->Putc(ev, '\\');
+ stream->Putc(ev, c);
+ outSize += 2; // '\' c
+ } else {
+ outSize += 3; // '$' hex hex
+ stream->Putc(ev, '$');
+ stream->Putc(ev, morkWriter_kHexDigits[(c >> 4) & 0x0F]);
+ stream->Putc(ev, morkWriter_kHexDigits[c & 0x0F]);
+ }
+ }
+ }
+ mWriter_LineSize += outSize;
+
+ return outSize;
+}
+
+mork_size morkWriter::WriteAtom(morkEnv* ev, const morkAtom* inAtom)
+// return number of atom bytes written on the current line (which
+// implies that escaped line breaks will make the size value smaller
+// than the entire atom's size, since only part goes on a last line).
+{
+ mork_size outSize = 0;
+ mdbYarn yarn; // to ref content inside atom
+
+ if (morkAtom::AliasYarn(inAtom, &yarn)) {
+ if (mWriter_DidStartDict && yarn.mYarn_Form != mWriter_DictForm)
+ this->ChangeDictForm(ev, yarn.mYarn_Form);
+
+ outSize = this->WriteYarn(ev, &yarn);
+ // mWriter_LineSize += stream->Write(ev, inYarn->mYarn_Buf, outSize);
+ } else
+ inAtom->BadAtomKindError(ev);
+
+ return outSize;
+}
+
+void morkWriter::WriteAtomSpaceAsDict(morkEnv* ev, morkAtomSpace* ioSpace) {
+ morkStream* stream = mWriter_Stream;
+ nsIMdbEnv* mdbev = ev->AsMdbEnv();
+ mork_scope scope = ioSpace->SpaceScope();
+ if (scope < 0x80) {
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ stream->PutString(ev, "< <(a=");
+ stream->Putc(ev, (int)scope);
+ ++mWriter_LineSize;
+ stream->PutString(ev, ")> // (f=iso-8859-1)");
+ mWriter_LineSize = stream->PutIndent(ev, morkWriter_kDictAliasDepth);
+ } else
+ ioSpace->NonAsciiSpaceScopeName(ev);
+
+ if (ev->Good()) {
+ mdbYarn yarn; // to ref content inside atom
+ char buf[64]; // buffer for staging the dict alias hex ID
+ char* idBuf = buf + 1; // where the id always starts
+ buf[0] = '('; // we always start with open paren
+ morkBookAtom* atom = 0;
+ morkAtomAidMapIter* ai = &mWriter_AtomSpaceAtomAidsIter;
+ ai->InitAtomAidMapIter(ev, &ioSpace->mAtomSpace_AtomAids);
+ mork_change* c = 0;
+
+ for (c = ai->FirstAtom(ev, &atom); c && ev->Good();
+ c = ai->NextAtom(ev, &atom)) {
+ if (atom) {
+ if (atom->IsAtomDirty()) {
+ atom->SetAtomClean(); // neutralize change
+
+ morkAtom::AliasYarn(atom, &yarn);
+ mork_size size = ev->TokenAsHex(idBuf, atom->mBookAtom_Id);
+
+ if (yarn.mYarn_Form != mWriter_DictForm)
+ this->ChangeDictForm(ev, yarn.mYarn_Form);
+
+ mork_size pending =
+ yarn.mYarn_Fill + size + morkWriter_kYarnEscapeSlop + 4;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kDictAliasDepth);
+ mork_size bytesWritten;
+ stream->Write(mdbev, buf, size + 1, &bytesWritten); // + '('
+ mWriter_LineSize += bytesWritten;
+
+ pending -= (size + 1);
+ this->IndentOverMaxLine(ev, pending, morkWriter_kDictAliasValueDepth);
+ stream->Putc(ev, '='); // start alias
+ ++mWriter_LineSize;
+
+ this->WriteYarn(ev, &yarn);
+ stream->Putc(ev, ')'); // end alias
+ ++mWriter_LineSize;
+
+ ++mWriter_DoneCount;
+ }
+ } else
+ ev->NilPointerError();
+ }
+ ai->CloseMapIter(ev);
+ }
+
+ if (ev->Good()) {
+ ioSpace->SetAtomSpaceClean();
+ // this->IndentAsNeeded(ev, 0);
+ // stream->PutByteThenNewline(ev, '>'); // end dict
+
+ stream->Putc(ev, '>'); // end dict
+ ++mWriter_LineSize;
+ }
+}
+
+/*
+(I'm putting the text of this message in file morkWriter.cpp.)
+
+I'm making a change which should cause rows and tables to go away
+when a Mork db is compress committed, when the rows and tables
+are no longer needed. Because this is subtle, I'm describing it
+here in case misbehavior is ever observed. Otherwise you'll have
+almost no hope of fixing a related bug.
+
+This is done entirely in morkWriter.cpp: morkWriter::DirtyAll(),
+which currently marks all rows and tables dirty so they will be
+written in a later phase of the commit. My change is to merely
+selectively not mark certain rows and tables dirty, when they seem
+to be superfluous.
+
+A row is no longer needed when the mRow_GcUses slot hits zero, and
+this is used by the following inline morkRow method:
+
+ mork_bool IsRowUsed() const { return mRow_GcUses != 0; }
+
+Naturally disaster ensues if mRow_GcUses is ever smaller than right.
+
+Similarly, we should drop tables when mTable_GcUses hits zero, but
+only when a table contains no row members. We consider tables to
+self reference (and prevent collection) when they contain content.
+Again, disaster ensues if mTable_GcUses is ever smaller than right.
+
+ mork_count GetRowCount() const
+ { return mTable_RowArray.mArray_Fill; }
+
+ mork_bool IsTableUsed() const
+ { return (mTable_GcUses != 0 || this->GetRowCount() != 0); }
+
+Now let's question why the design involves filtering what gets set
+to dirty. Why not apply a filter in the later phase when we write
+content? Because I'm afraid of missing some subtle interaction in
+updating table and row relationships. It seems safer to write a row
+or table when it starts out dirty, before morkWriter::DirtyAll() is
+called. So this design calls for writing out rows and tables when
+they are still clearly used, and additionally, <i>when we have just
+been actively writing to them right before this commit</i>.
+
+Presumably if they are truly useless, they will no longer be dirtied
+in later sessions and will get collected during the next compress
+commit. So we wait to collect them until they become all dead, and
+not just mostly dead. (At which time you can feel free to go through
+their pockets looking for loose change.)
+*/
+
+mork_bool morkWriter::DirtyAll(morkEnv* ev)
+// DirtyAll() visits every store sub-object and marks
+// them dirty, including every table, row, cell, and atom. The return
+// equals ev->Good(), to show whether any error happened. This method is
+// intended for use in the beginning of a "compress commit" which writes
+// all store content, whether dirty or not. We dirty everything first so
+// that later iterations over content can mark things clean as they are
+// written, and organize the process of serialization so that objects are
+// written only at need (because of being dirty). Note the method can
+// stop early when any error happens, since this will abort any commit.
+{
+ morkStore* store = mWriter_Store;
+ if (store) {
+ store->SetStoreDirty();
+ mork_change* c = 0;
+
+ if (ev->Good()) {
+ morkAtomSpaceMapIter* asi = &mWriter_StoreAtomSpacesIter;
+ asi->InitAtomSpaceMapIter(ev, &store->mStore_AtomSpaces);
+
+ mork_scope* key = 0; // ignore keys in map
+ morkAtomSpace* space = 0; // old val node in the map
+
+ for (c = asi->FirstAtomSpace(ev, key, &space); c && ev->Good();
+ c = asi->NextAtomSpace(ev, key, &space)) {
+ if (space) {
+ if (space->IsAtomSpace()) {
+ space->SetAtomSpaceDirty();
+ morkBookAtom* atom = 0;
+ morkAtomAidMapIter* ai = &mWriter_AtomSpaceAtomAidsIter;
+ ai->InitAtomAidMapIter(ev, &space->mAtomSpace_AtomAids);
+
+ for (c = ai->FirstAtom(ev, &atom); c && ev->Good();
+ c = ai->NextAtom(ev, &atom)) {
+ if (atom) {
+ atom->SetAtomDirty();
+ ++mWriter_TotalCount;
+ } else
+ ev->NilPointerError();
+ }
+
+ ai->CloseMapIter(ev);
+ } else
+ space->NonAtomSpaceTypeError(ev);
+ } else
+ ev->NilPointerError();
+ }
+ }
+
+ if (ev->Good()) {
+ morkRowSpaceMapIter* rsi = &mWriter_StoreRowSpacesIter;
+ rsi->InitRowSpaceMapIter(ev, &store->mStore_RowSpaces);
+
+ mork_scope* key = 0; // ignore keys in map
+ morkRowSpace* space = 0; // old val node in the map
+
+ for (c = rsi->FirstRowSpace(ev, key, &space); c && ev->Good();
+ c = rsi->NextRowSpace(ev, key, &space)) {
+ if (space) {
+ if (space->IsRowSpace()) {
+ space->SetRowSpaceDirty();
+ if (ev->Good()) {
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMapIter* ri = &mWriter_RowSpaceRowsIter;
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMapIter* ri = &mWriter_RowSpaceRowsIter;
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ ri->InitRowMapIter(ev, &space->mRowSpace_Rows);
+
+ morkRow* row = 0; // old key row in the map
+
+ for (c = ri->FirstRow(ev, &row); c && ev->Good();
+ c = ri->NextRow(ev, &row)) {
+ if (row && row->IsRow()) // need to dirty row?
+ {
+ if (row->IsRowUsed() || row->IsRowDirty()) {
+ row->DirtyAllRowContent(ev);
+ ++mWriter_TotalCount;
+ }
+ } else
+ row->NonRowTypeWarning(ev);
+ }
+ ri->CloseMapIter(ev);
+ }
+
+ if (ev->Good()) {
+ morkTableMapIter* ti = &mWriter_RowSpaceTablesIter;
+ ti->InitTableMapIter(ev, &space->mRowSpace_Tables);
+
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ morkTable* table = ti->FirstTable(ev);
+
+ for (; table && ev->Good(); table = ti->NextTable(ev))
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ mork_tid* tableKey = 0; // ignore keys in table map
+ morkTable* table = 0; // old key row in the map
+
+ for (c = ti->FirstTable(ev, tableKey, &table); c && ev->Good();
+ c = ti->NextTable(ev, tableKey, &table))
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+ {
+ if (table && table->IsTable()) // need to dirty table?
+ {
+ if (table->IsTableUsed() || table->IsTableDirty()) {
+ // table->DirtyAllTableContent(ev);
+ // only necessary to mark table itself dirty:
+ table->SetTableDirty();
+ table->SetTableRewrite();
+ ++mWriter_TotalCount;
+ }
+ } else
+ table->NonTableTypeWarning(ev);
+ }
+ ti->CloseMapIter(ev);
+ }
+ } else
+ space->NonRowSpaceTypeError(ev);
+ } else
+ ev->NilPointerError();
+ }
+ }
+ } else
+ this->NilWriterStoreError(ev);
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnNothingDone(morkEnv* ev) {
+ mWriter_Incremental = !mWriter_NeedDirtyAll; // opposites
+
+ if (!mWriter_Store->IsStoreDirty() && !mWriter_NeedDirtyAll) {
+ mWriter_Phase = morkWriter_kPhaseWritingDone;
+ return morkBool_kTrue;
+ }
+
+ // morkStream* stream = mWriter_Stream;
+ if (mWriter_NeedDirtyAll) this->DirtyAll(ev);
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseDirtyAllDone;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::StartGroup(morkEnv* ev) {
+ nsIMdbEnv* mdbev = ev->AsMdbEnv();
+ morkStream* stream = mWriter_Stream;
+ mWriter_DidStartGroup = morkBool_kTrue;
+ mWriter_DidEndGroup = morkBool_kFalse;
+
+ char buf[4 + morkWriter_kGroupBufSize + 2]; // "@$${" + groupid + "{@"
+ char* p = buf;
+ *p++ = '@';
+ *p++ = '$';
+ *p++ = '$';
+ *p++ = '{';
+
+ mork_token groupID = mWriter_CommitGroupIdentity;
+ mork_fill idFill = ev->TokenAsHex(p, groupID);
+ mWriter_GroupBufFill = 0;
+ // ev->TokenAsHex(mWriter_GroupBuf, groupID);
+ if (idFill < morkWriter_kGroupBufSize) {
+ // TokenAsHex appends a '\0', but it's not included in idFill count.
+ MORK_MEMCPY(mWriter_GroupBuf, p, idFill + 1);
+ mWriter_GroupBufFill = idFill;
+ } else {
+ *mWriter_GroupBuf = '\0';
+ }
+
+ p += idFill;
+ *p++ = '{';
+ *p++ = '@';
+
+ stream->PutLineBreak(ev);
+
+ morkStore* store = mWriter_Store;
+ if (store) // might need to capture commit group position?
+ {
+ mork_pos groupPos;
+ stream->Tell(mdbev, &groupPos);
+ if (!store->mStore_FirstCommitGroupPos)
+ store->mStore_FirstCommitGroupPos = groupPos;
+ else if (!store->mStore_SecondCommitGroupPos)
+ store->mStore_SecondCommitGroupPos = groupPos;
+ }
+
+ mork_size bytesWritten;
+ stream->Write(mdbev, buf, 4 + idFill + 2,
+ &bytesWritten); // '@$${' + idFill + '{@'
+ stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::CommitGroup(morkEnv* ev) {
+ if (mWriter_DidStartGroup) {
+ nsIMdbEnv* mdbev = ev->AsMdbEnv();
+ mork_size bytesWritten;
+ morkStream* stream = mWriter_Stream;
+
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ stream->Putc(ev, '@');
+ stream->Putc(ev, '$');
+ stream->Putc(ev, '$');
+ stream->Putc(ev, '}');
+
+ mork_fill bufFill = mWriter_GroupBufFill;
+ if (bufFill) stream->Write(mdbev, mWriter_GroupBuf, bufFill, &bytesWritten);
+
+ stream->Putc(ev, '}');
+ stream->Putc(ev, '@');
+ stream->PutLineBreak(ev);
+
+ mWriter_LineSize = 0;
+ }
+
+ mWriter_DidStartGroup = morkBool_kFalse;
+ mWriter_DidEndGroup = morkBool_kTrue;
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::AbortGroup(morkEnv* ev) {
+ if (mWriter_DidStartGroup) {
+ morkStream* stream = mWriter_Stream;
+ stream->PutLineBreak(ev);
+ stream->PutStringThenNewline(ev, "@$$}~~}@");
+ mWriter_LineSize = 0;
+ }
+
+ mWriter_DidStartGroup = morkBool_kFalse;
+ mWriter_DidEndGroup = morkBool_kTrue;
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnDirtyAllDone(morkEnv* ev) {
+ if (ev->Good()) {
+ nsIMdbEnv* mdbev = ev->AsMdbEnv();
+ morkStream* stream = mWriter_Stream;
+ mork_pos resultPos;
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ stream->Seek(mdbev, 0, &resultPos); // beginning of stream
+ stream->PutStringThenNewline(ev, morkWriter_kFileHeader);
+ mWriter_LineSize = 0;
+ } else // else mWriter_Incremental
+ {
+ mork_pos eos = stream->Length(ev); // length is end of stream
+ if (ev->Good()) {
+ stream->Seek(mdbev, eos, &resultPos); // goto end of stream
+ if (eos < 128) // maybe need file header?
+ {
+ stream->PutStringThenNewline(ev, morkWriter_kFileHeader);
+ mWriter_LineSize = 0;
+ }
+ this->StartGroup(ev); // begin incremental transaction
+ }
+ }
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhasePutHeaderDone;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnPutHeaderDone(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnPutHeaderDone()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ morkStore* store = mWriter_Store;
+ if (store)
+ store->RenumberAllCollectableContent(ev);
+ else
+ this->NilWriterStoreError(ev);
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseRenumberAllDone;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnRenumberAllDone(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnRenumberAllDone()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreAtomSpaces;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnStoreAtomSpaces(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnStoreAtomSpaces()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good()) {
+ morkStore* store = mWriter_Store;
+ if (store) {
+ morkAtomSpace* space = store->LazyGetGroundColumnSpace(ev);
+ if (space && space->IsAtomSpaceDirty()) {
+ // stream->PutStringThenNewline(ev, "// ground column space dict:");
+
+ if (mWriter_LineSize) {
+ stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+ }
+ this->WriteAtomSpaceAsDict(ev, space);
+ space->SetAtomSpaceClean();
+ }
+ } else
+ this->NilWriterStoreError(ev);
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreRowSpacesTables;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnAtomSpaceAtomAids(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnAtomSpaceAtomAids()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreRowSpacesTables;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+void morkWriter::WriteAllStoreTables(morkEnv* ev) {
+ morkStore* store = mWriter_Store;
+ if (store && ev->Good()) {
+ morkRowSpaceMapIter* rsi = &mWriter_StoreRowSpacesIter;
+ rsi->InitRowSpaceMapIter(ev, &store->mStore_RowSpaces);
+
+ mork_scope* key = 0; // ignore keys in map
+ morkRowSpace* space = 0; // old val node in the map
+ mork_change* c = 0;
+
+ for (c = rsi->FirstRowSpace(ev, key, &space); c && ev->Good();
+ c = rsi->NextRowSpace(ev, key, &space)) {
+ if (space) {
+ if (space->IsRowSpace()) {
+ space->SetRowSpaceClean();
+ if (ev->Good()) {
+ morkTableMapIter* ti = &mWriter_RowSpaceTablesIter;
+ ti->InitTableMapIter(ev, &space->mRowSpace_Tables);
+
+#ifdef MORK_BEAD_OVER_NODE_MAPS
+ morkTable* table = ti->FirstTable(ev);
+
+ for (; table && ev->Good(); table = ti->NextTable(ev))
+#else /*MORK_BEAD_OVER_NODE_MAPS*/
+ mork_tid* key2 = 0; // ignore keys in table map
+ morkTable* table = 0; // old key row in the map
+
+ for (c = ti->FirstTable(ev, key2, &table); c && ev->Good();
+ c = ti->NextTable(ev, key2, &table))
+#endif /*MORK_BEAD_OVER_NODE_MAPS*/
+ {
+ if (table && table->IsTable()) {
+ if (table->IsTableDirty()) {
+ mWriter_BeVerbose =
+ (ev->mEnv_BeVerbose || table->IsTableVerbose());
+
+ if (this->PutTableDict(ev, table)) this->PutTable(ev, table);
+
+ table->SetTableClean(ev);
+ mWriter_BeVerbose = ev->mEnv_BeVerbose;
+ }
+ } else
+ table->NonTableTypeWarning(ev);
+ }
+ ti->CloseMapIter(ev);
+ }
+ if (ev->Good()) {
+ mWriter_TableRowScope = 0; // ensure no table context now
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMapIter* ri = &mWriter_RowSpaceRowsIter;
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMapIter* ri = &mWriter_RowSpaceRowsIter;
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+ ri->InitRowMapIter(ev, &space->mRowSpace_Rows);
+
+ morkRow* row = 0; // old row in the map
+
+ for (c = ri->FirstRow(ev, &row); c && ev->Good();
+ c = ri->NextRow(ev, &row)) {
+ if (row && row->IsRow()) {
+ // later we should also check that table use count is nonzero:
+ if (row->IsRowDirty()) // && row->IsRowUsed() ??
+ {
+ mWriter_BeVerbose = ev->mEnv_BeVerbose;
+ if (this->PutRowDict(ev, row)) {
+ if (ev->Good() && mWriter_DidStartDict) {
+ this->EndDict(ev);
+ if (mWriter_LineSize < 32 && ev->Good())
+ mWriter_SuppressDirtyRowNewline = morkBool_kTrue;
+ }
+
+ if (ev->Good()) this->PutRow(ev, row);
+ }
+ mWriter_BeVerbose = ev->mEnv_BeVerbose;
+ }
+ } else
+ row->NonRowTypeWarning(ev);
+ }
+ ri->CloseMapIter(ev);
+ }
+ } else
+ space->NonRowSpaceTypeError(ev);
+ } else
+ ev->NilPointerError();
+ }
+ }
+}
+
+mork_bool morkWriter::OnStoreRowSpacesTables(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnStoreRowSpacesTables()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ // later we'll break this up, but today we'll write all in one shot:
+ this->WriteAllStoreTables(ev);
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreRowSpacesRows;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnRowSpaceTables(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnRowSpaceTables()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreRowSpacesRows;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnTableRowArray(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnTableRowArray()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseStoreRowSpacesRows;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnStoreRowSpacesRows(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnStoreRowSpacesRows()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseContentDone;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnRowSpaceRows(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnRowSpaceRows()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_NeedDirtyAll) // compress commit
+ {
+ }
+
+ if (ev->Good())
+ mWriter_Phase = morkWriter_kPhaseContentDone;
+ else
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop on error
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnContentDone(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+
+ // if ( mWriter_NeedDirtyAll )
+ // stream->PutStringThenNewline(ev, "// OnContentDone()");
+ mWriter_LineSize = 0;
+
+ if (mWriter_Incremental) {
+ if (ev->Good())
+ this->CommitGroup(ev);
+ else
+ this->AbortGroup(ev);
+ } else if (mWriter_Store && ev->Good()) {
+ // after rewriting everything, there are no transaction groups:
+ mWriter_Store->mStore_FirstCommitGroupPos = 0;
+ mWriter_Store->mStore_SecondCommitGroupPos = 0;
+ }
+
+ stream->Flush(ev->AsMdbEnv());
+ nsIMdbFile* bud = mWriter_Bud;
+ if (bud) {
+ bud->Flush(ev->AsMdbEnv());
+ bud->BecomeTrunk(ev->AsMdbEnv());
+ nsIMdbFile_SlotStrongFile((nsIMdbFile*)0, ev, &mWriter_Bud);
+ } else if (!mWriter_Incremental) // should have a bud?
+ this->NilWriterBudError(ev);
+
+ mWriter_Phase = morkWriter_kPhaseWritingDone; // stop always
+ mWriter_DoneCount = mWriter_TotalCount;
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::OnWritingDone(morkEnv* ev) {
+ mWriter_DoneCount = mWriter_TotalCount;
+ ev->NewWarning("writing is done");
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutTableChange(morkEnv* ev,
+ const morkTableChange* inChange) {
+ nsIMdbEnv* mdbev = ev->AsMdbEnv();
+ if (inChange->IsAddRowTableChange()) {
+ this->PutRow(ev, inChange->mTableChange_Row); // row alone means add
+ } else if (inChange->IsCutRowTableChange()) {
+ mWriter_Stream->Putc(ev, '-'); // prefix '-' indicates cut row
+ ++mWriter_LineSize;
+ this->PutRow(ev, inChange->mTableChange_Row);
+ } else if (inChange->IsMoveRowTableChange()) {
+ this->PutRow(ev, inChange->mTableChange_Row);
+ char buf[64];
+ char* p = buf;
+ *p++ = '!'; // for moves, position is indicated by prefix '!'
+ mork_size posSize = ev->TokenAsHex(p, inChange->mTableChange_Pos);
+ p += posSize;
+ *p++ = ' ';
+ mork_size bytesWritten;
+ mWriter_Stream->Write(mdbev, buf, posSize + 2, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+ } else
+ inChange->UnknownChangeError(ev);
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutTable(morkEnv* ev, morkTable* ioTable) {
+ if (ev->Good()) this->StartTable(ev, ioTable);
+
+ if (ev->Good()) {
+ if (ioTable->IsTableRewrite() || mWriter_NeedDirtyAll) {
+ morkArray* array = &ioTable->mTable_RowArray; // vector of rows
+ mork_fill fill = array->mArray_Fill; // count of rows
+ morkRow** rows = (morkRow**)array->mArray_Slots;
+ if (rows && fill) {
+ morkRow** end = rows + fill;
+ while (rows < end && ev->Good()) {
+ morkRow* r = *rows++; // next row to consider
+ this->PutRow(ev, r);
+ }
+ }
+ } else // incremental write only table changes
+ {
+ morkList* list = &ioTable->mTable_ChangeList;
+ morkNext* next = list->GetListHead();
+ while (next && ev->Good()) {
+ this->PutTableChange(ev, (morkTableChange*)next);
+ next = next->GetNextLink();
+ }
+ }
+ }
+
+ if (ev->Good()) this->EndTable(ev);
+
+ ioTable->SetTableClean(ev); // note this also cleans change list
+ mWriter_TableRowScope = 0;
+
+ ++mWriter_DoneCount;
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutTableDict(morkEnv* ev, morkTable* ioTable) {
+ morkRowSpace* space = ioTable->mTable_RowSpace;
+ mWriter_TableRowScope = space->SpaceScope();
+ mWriter_TableForm = 0; // (f=iso-8859-1)
+ mWriter_TableAtomScope = 'v'; // (a=v)
+ mWriter_TableKind = ioTable->mTable_Kind;
+
+ mWriter_RowForm = mWriter_TableForm;
+ mWriter_RowAtomScope = mWriter_TableAtomScope;
+ mWriter_RowScope = mWriter_TableRowScope;
+
+ mWriter_DictForm = mWriter_TableForm;
+ mWriter_DictAtomScope = mWriter_TableAtomScope;
+
+ // if ( ev->Good() )
+ // this->StartDict(ev); // delay as long as possible
+
+ if (ev->Good()) {
+ morkRow* r = ioTable->mTable_MetaRow;
+ if (r) {
+ if (r->IsRow())
+ this->PutRowDict(ev, r);
+ else
+ r->NonRowTypeError(ev);
+ }
+ morkArray* array = &ioTable->mTable_RowArray; // vector of rows
+ mork_fill fill = array->mArray_Fill; // count of rows
+ morkRow** rows = (morkRow**)array->mArray_Slots;
+ if (rows && fill) {
+ morkRow** end = rows + fill;
+ while (rows < end && ev->Good()) {
+ r = *rows++; // next row to consider
+ if (r && r->IsRow())
+ this->PutRowDict(ev, r);
+ else
+ r->NonRowTypeError(ev);
+ }
+ }
+ // we may have a change for a row which is no longer in the
+ // table, but contains a cell with something not in the dictionary.
+ // So, loop through the rows in the change log, writing out any
+ // dirty dictionary elements.
+ morkList* list = &ioTable->mTable_ChangeList;
+ morkNext* next = list->GetListHead();
+ while (next && ev->Good()) {
+ r = ((morkTableChange*)next)->mTableChange_Row;
+ if (r && r->IsRow()) this->PutRowDict(ev, r);
+ next = next->GetNextLink();
+ }
+ }
+ if (ev->Good()) this->EndDict(ev);
+
+ return ev->Good();
+}
+
+void morkWriter::WriteTokenToTokenMetaCell(morkEnv* ev, mork_token inCol,
+ mork_token inValue) {
+ morkStream* stream = mWriter_Stream;
+ mork_bool isKindCol = (morkStore_kKindColumn == inCol);
+ mork_u1 valSep = (mork_u1)((isKindCol) ? '^' : '=');
+
+ char buf[128]; // buffer for staging the two hex IDs
+ char* p = buf;
+
+ mork_size bytesWritten;
+ if (inCol < 0x80) {
+ stream->Putc(ev, '(');
+ stream->Putc(ev, (char)inCol);
+ stream->Putc(ev, valSep);
+ } else {
+ *p++ = '('; // we always start with open paren
+
+ *p++ = '^'; // indicates col is hex ID
+ mork_size colSize = ev->TokenAsHex(p, inCol);
+ p += colSize;
+ *p++ = (char)valSep;
+ stream->Write(ev->AsMdbEnv(), buf, colSize + 3, &bytesWritten);
+
+ mWriter_LineSize += bytesWritten;
+ }
+
+ if (isKindCol) {
+ p = buf;
+ mork_size valSize = ev->TokenAsHex(p, inValue);
+ p += valSize;
+ *p++ = ':';
+ *p++ = 'c';
+ *p++ = ')';
+ stream->Write(ev->AsMdbEnv(), buf, valSize + 3, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+ } else {
+ this->IndentAsNeeded(ev, morkWriter_kTableMetaCellValueDepth);
+ mdbYarn* yarn = &mWriter_ColYarn;
+ // mork_u1* yarnBuf = (mork_u1*) yarn->mYarn_Buf;
+ mWriter_Store->TokenToString(ev, inValue, yarn);
+ this->WriteYarn(ev, yarn);
+ stream->Putc(ev, ')');
+ ++mWriter_LineSize;
+ }
+
+ // mork_fill fill = yarn->mYarn_Fill;
+ // yarnBuf[ fill ] = ')'; // append terminator
+ // mWriter_LineSize += stream->Write(ev, yarnBuf, fill + 1); // +1 for ')'
+}
+
+void morkWriter::WriteStringToTokenDictCell(morkEnv* ev, const char* inCol,
+ mork_token inValue)
+// Note inCol should begin with '(' and end with '=', with col in between.
+{
+ morkStream* stream = mWriter_Stream;
+ mWriter_LineSize += stream->PutString(ev, inCol);
+
+ this->IndentAsNeeded(ev, morkWriter_kDictMetaCellValueDepth);
+ mdbYarn* yarn = &mWriter_ColYarn;
+ // mork_u1* yarnBuf = (mork_u1*) yarn->mYarn_Buf;
+ mWriter_Store->TokenToString(ev, inValue, yarn);
+ this->WriteYarn(ev, yarn);
+ stream->Putc(ev, ')');
+ ++mWriter_LineSize;
+
+ // mork_fill fill = yarn->mYarn_Fill;
+ // yarnBuf[ fill ] = ')'; // append terminator
+ // mWriter_LineSize += stream->Write(ev, yarnBuf, fill + 1); // +1 for ')'
+}
+
+void morkWriter::ChangeDictAtomScope(morkEnv* ev, mork_scope inScope) {
+ if (inScope != mWriter_DictAtomScope) {
+ ev->NewWarning("unexpected atom scope change");
+
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+
+ char buf[128]; // buffer for staging the two hex IDs
+ char* p = buf;
+ *p++ = '<'; // we always start with open paren
+ *p++ = '('; // we always start with open paren
+ *p++ = (char)morkStore_kAtomScopeColumn;
+
+ mork_size scopeSize = 1; // default to one byte
+ if (inScope >= 0x80) {
+ *p++ = '^'; // indicates col is hex ID
+ scopeSize = ev->TokenAsHex(p, inScope);
+ p += scopeSize;
+ } else {
+ *p++ = '='; // indicates col is imm byte
+ *p++ = (char)(mork_u1)inScope;
+ }
+
+ *p++ = ')';
+ *p++ = '>';
+ *p = 0;
+
+ mork_size pending = scopeSize + 6;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kDictAliasDepth);
+ mork_size bytesWritten;
+
+ stream->Write(ev->AsMdbEnv(), buf, pending, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ mWriter_DictAtomScope = inScope;
+ }
+}
+
+void morkWriter::ChangeRowForm(morkEnv* ev, mork_cscode inNewForm) {
+ if (inNewForm != mWriter_RowForm) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+
+ char buf[128]; // buffer for staging the two hex IDs
+ char* p = buf;
+ *p++ = '['; // we always start with open bracket
+ *p++ = '('; // we always start with open paren
+ *p++ = (char)morkStore_kFormColumn;
+
+ mork_size formSize = 1; // default to one byte
+ if (!morkCh_IsValue(inNewForm)) {
+ *p++ = '^'; // indicates col is hex ID
+ formSize = ev->TokenAsHex(p, inNewForm);
+ p += formSize;
+ } else {
+ *p++ = '='; // indicates col is imm byte
+ *p++ = (char)(mork_u1)inNewForm;
+ }
+
+ *p++ = ')';
+ *p++ = ']';
+ *p = 0;
+
+ mork_size pending = formSize + 6;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kRowCellDepth);
+ mork_size bytesWritten;
+ stream->Write(ev->AsMdbEnv(), buf, pending, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ mWriter_RowForm = inNewForm;
+ }
+}
+
+void morkWriter::ChangeDictForm(morkEnv* ev, mork_cscode inNewForm) {
+ if (inNewForm != mWriter_DictForm) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+
+ char buf[128]; // buffer for staging the two hex IDs
+ char* p = buf;
+ *p++ = '<'; // we always start with open angle
+ *p++ = '('; // we always start with open paren
+ *p++ = (char)morkStore_kFormColumn;
+
+ mork_size formSize = 1; // default to one byte
+ if (!morkCh_IsValue(inNewForm)) {
+ *p++ = '^'; // indicates col is hex ID
+ formSize = ev->TokenAsHex(p, inNewForm);
+ p += formSize;
+ } else {
+ *p++ = '='; // indicates col is imm byte
+ *p++ = (char)(mork_u1)inNewForm;
+ }
+
+ *p++ = ')';
+ *p++ = '>';
+ *p = 0;
+
+ mork_size pending = formSize + 6;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kDictAliasDepth);
+
+ mork_size bytesWritten;
+ stream->Write(ev->AsMdbEnv(), buf, pending, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ mWriter_DictForm = inNewForm;
+ }
+}
+
+void morkWriter::StartDict(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_DidStartDict) {
+ stream->Putc(ev, '>'); // end dict
+ ++mWriter_LineSize;
+ }
+ mWriter_DidStartDict = morkBool_kTrue;
+ mWriter_DidEndDict = morkBool_kFalse;
+
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+
+ if (mWriter_TableRowScope) // blank line before table's dict?
+ stream->PutLineBreak(ev);
+
+ if (mWriter_DictForm || mWriter_DictAtomScope != 'v') {
+ stream->Putc(ev, '<');
+ stream->Putc(ev, ' ');
+ stream->Putc(ev, '<');
+ mWriter_LineSize = 3;
+ if (mWriter_DictForm)
+ this->WriteStringToTokenDictCell(ev, "(f=", mWriter_DictForm);
+ if (mWriter_DictAtomScope != 'v')
+ this->WriteStringToTokenDictCell(ev, "(a=", mWriter_DictAtomScope);
+
+ stream->Putc(ev, '>');
+ ++mWriter_LineSize;
+
+ mWriter_LineSize = stream->PutIndent(ev, morkWriter_kDictAliasDepth);
+ } else {
+ stream->Putc(ev, '<');
+ // stream->Putc(ev, ' ');
+ ++mWriter_LineSize;
+ }
+}
+
+void morkWriter::EndDict(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_DidStartDict) {
+ stream->Putc(ev, '>'); // end dict
+ ++mWriter_LineSize;
+ }
+ mWriter_DidStartDict = morkBool_kFalse;
+ mWriter_DidEndDict = morkBool_kTrue;
+}
+
+void morkWriter::StartTable(morkEnv* ev, morkTable* ioTable) {
+ mdbOid toid; // to receive table oid
+ ioTable->GetTableOid(ev, &toid);
+
+ if (ev->Good()) {
+ morkStream* stream = mWriter_Stream;
+ if (mWriter_LineSize) stream->PutLineBreak(ev);
+ mWriter_LineSize = 0;
+ // stream->PutLineBreak(ev);
+
+ char buf[64 + 16]; // buffer for staging hex
+ char* p = buf;
+ *p++ = '{'; // punct 1
+ mork_size punctSize =
+ (mWriter_BeVerbose) ? 10 : 3; // counting "{ {/*r=*/ "
+
+ if (ioTable->IsTableRewrite() && mWriter_Incremental) {
+ *p++ = '-';
+ ++punctSize; // counting '-' // punct ++
+ ++mWriter_LineSize;
+ }
+ mork_size oidSize = ev->OidAsHex(p, toid);
+ p += oidSize;
+ *p++ = ' '; // punct 2
+ *p++ = '{'; // punct 3
+ if (mWriter_BeVerbose) {
+ *p++ = '/'; // punct=4
+ *p++ = '*'; // punct=5
+ *p++ = 'r'; // punct=6
+ *p++ = '='; // punct=7
+
+ mork_token tableUses = (mork_token)ioTable->mTable_GcUses;
+ mork_size usesSize = ev->TokenAsHex(p, tableUses);
+ punctSize += usesSize;
+ p += usesSize;
+
+ *p++ = '*'; // punct=8
+ *p++ = '/'; // punct=9
+ *p++ = ' '; // punct=10
+ }
+ mork_size bytesWritten;
+
+ stream->Write(ev->AsMdbEnv(), buf, oidSize + punctSize, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ mork_kind tk = mWriter_TableKind;
+ if (tk) {
+ this->IndentAsNeeded(ev, morkWriter_kTableMetaCellDepth);
+ this->WriteTokenToTokenMetaCell(ev, morkStore_kKindColumn, tk);
+ }
+
+ stream->Putc(ev, '('); // start 's' col cell
+ stream->Putc(ev, 's'); // column
+ stream->Putc(ev, '='); // column
+ mWriter_LineSize += 3;
+
+ int prio = (int)ioTable->mTable_Priority;
+ if (prio > 9) // need to force down to max decimal digit?
+ prio = 9;
+ prio += '0'; // add base digit zero
+ stream->Putc(ev, prio); // priority: (s=0
+ ++mWriter_LineSize;
+
+ if (ioTable->IsTableUnique()) {
+ stream->Putc(ev, 'u'); // (s=0u
+ ++mWriter_LineSize;
+ }
+ if (ioTable->IsTableVerbose()) {
+ stream->Putc(ev, 'v'); // (s=0uv
+ ++mWriter_LineSize;
+ }
+
+ // stream->Putc(ev, ':'); // (s=0uv:
+ // stream->Putc(ev, 'c'); // (s=0uv:c
+ stream->Putc(ev, ')'); // end 's' col cell (s=0uv:c)
+ mWriter_LineSize += 1; // maybe 3 if we add ':' and 'c'
+
+ morkRow* r = ioTable->mTable_MetaRow;
+ if (r) {
+ if (r->IsRow()) {
+ mWriter_SuppressDirtyRowNewline = morkBool_kTrue;
+ this->PutRow(ev, r);
+ } else
+ r->NonRowTypeError(ev);
+ }
+
+ stream->Putc(ev, '}'); // end meta
+ ++mWriter_LineSize;
+
+ if (mWriter_LineSize < mWriter_MaxIndent) {
+ stream->Putc(ev, ' '); // nice white space
+ ++mWriter_LineSize;
+ }
+ }
+}
+
+void morkWriter::EndTable(morkEnv* ev) {
+ morkStream* stream = mWriter_Stream;
+ stream->Putc(ev, '}'); // end table
+ ++mWriter_LineSize;
+
+ mWriter_TableAtomScope = 'v'; // (a=v)
+}
+
+mork_bool morkWriter::PutRowDict(morkEnv* ev, morkRow* ioRow) {
+ mWriter_RowForm = mWriter_TableForm;
+
+ morkCell* cells = ioRow->mRow_Cells;
+ if (cells) {
+ morkStream* stream = mWriter_Stream;
+ mdbYarn yarn; // to ref content inside atom
+ char buf[64]; // buffer for staging the dict alias hex ID
+ char* idBuf = buf + 1; // where the id always starts
+ buf[0] = '('; // we always start with open paren
+
+ morkCell* end = cells + ioRow->mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end && ev->Good()) {
+ morkAtom* atom = cells->GetAtom();
+ if (atom && atom->IsAtomDirty()) {
+ if (atom->IsBook()) // is it possible to write atom ID?
+ {
+ if (!this->DidStartDict()) {
+ this->StartDict(ev);
+ if (ev->Bad()) break;
+ }
+ atom->SetAtomClean(); // neutralize change
+
+ this->IndentAsNeeded(ev, morkWriter_kDictAliasDepth);
+ morkBookAtom* ba = (morkBookAtom*)atom;
+ mork_size size = ev->TokenAsHex(idBuf, ba->mBookAtom_Id);
+ mork_size bytesWritten;
+ stream->Write(ev->AsMdbEnv(), buf, size + 1, &bytesWritten); // '('
+ mWriter_LineSize += bytesWritten;
+
+ if (morkAtom::AliasYarn(atom, &yarn)) {
+ mork_scope atomScope = atom->GetBookAtomSpaceScope(ev);
+ if (atomScope && atomScope != mWriter_DictAtomScope)
+ this->ChangeDictAtomScope(ev, atomScope);
+
+ if (mWriter_DidStartDict && yarn.mYarn_Form != mWriter_DictForm)
+ this->ChangeDictForm(ev, yarn.mYarn_Form);
+
+ mork_size pending =
+ yarn.mYarn_Fill + morkWriter_kYarnEscapeSlop + 1;
+ this->IndentOverMaxLine(ev, pending,
+ morkWriter_kDictAliasValueDepth);
+
+ stream->Putc(ev, '='); // start value
+ ++mWriter_LineSize;
+
+ this->WriteYarn(ev, &yarn);
+
+ stream->Putc(ev, ')'); // end value
+ ++mWriter_LineSize;
+ } else
+ atom->BadAtomKindError(ev);
+
+ ++mWriter_DoneCount;
+ }
+ }
+ }
+ }
+ return ev->Good();
+}
+
+mork_bool morkWriter::IsYarnAllValue(const mdbYarn* inYarn) {
+ mork_fill fill = inYarn->mYarn_Fill;
+ const mork_u1* buf = (const mork_u1*)inYarn->mYarn_Buf;
+ const mork_u1* end = buf + fill;
+ --buf; // prepare for preincrement
+ while (++buf < end) {
+ mork_ch c = *buf;
+ if (!morkCh_IsValue(c)) return morkBool_kFalse;
+ }
+ return morkBool_kTrue;
+}
+
+mork_bool morkWriter::PutVerboseCell(morkEnv* ev, morkCell* ioCell,
+ mork_bool inWithVal) {
+ morkStream* stream = mWriter_Stream;
+ morkStore* store = mWriter_Store;
+
+ mdbYarn* colYarn = &mWriter_ColYarn;
+
+ morkAtom* atom = (inWithVal) ? ioCell->GetAtom() : (morkAtom*)0;
+
+ mork_column col = ioCell->GetColumn();
+ store->TokenToString(ev, col, colYarn);
+
+ mdbYarn yarn; // to ref content inside atom
+ morkAtom::AliasYarn(atom, &yarn); // works even when atom==nil
+
+ if (yarn.mYarn_Form != mWriter_RowForm)
+ this->ChangeRowForm(ev, yarn.mYarn_Form);
+
+ mork_size pending =
+ yarn.mYarn_Fill + colYarn->mYarn_Fill + morkWriter_kYarnEscapeSlop + 3;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kRowCellDepth);
+
+ stream->Putc(ev, '('); // start cell
+ ++mWriter_LineSize;
+
+ this->WriteYarn(ev, colYarn); // column
+
+ pending = yarn.mYarn_Fill + morkWriter_kYarnEscapeSlop;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kRowCellValueDepth);
+ stream->Putc(ev, '=');
+ ++mWriter_LineSize;
+
+ this->WriteYarn(ev, &yarn); // value
+
+ stream->Putc(ev, ')'); // end cell
+ ++mWriter_LineSize;
+
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutVerboseRowCells(morkEnv* ev, morkRow* ioRow) {
+ morkCell* cells = ioRow->mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + ioRow->mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end && ev->Good()) {
+ // note we prefer to avoid writing cells here with no value:
+ if (cells->GetAtom()) // does cell have any value?
+ this->PutVerboseCell(ev, cells, /*inWithVal*/ morkBool_kTrue);
+ }
+ }
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutCell(morkEnv* ev, morkCell* ioCell,
+ mork_bool inWithVal) {
+ morkStream* stream = mWriter_Stream;
+ char buf[128]; // buffer for staging hex ids
+ char* idBuf = buf + 2; // where the id always starts
+ buf[0] = '('; // we always start with open paren
+ buf[1] = '^'; // column is always a hex ID
+
+ mork_size colSize = 0; // the size of col hex ID
+ mork_size bytesWritten;
+
+ morkAtom* atom = (inWithVal) ? ioCell->GetAtom() : (morkAtom*)0;
+
+ mork_column col = ioCell->GetColumn();
+ char* p = idBuf;
+ colSize = ev->TokenAsHex(p, col);
+ p += colSize;
+
+ mdbYarn yarn; // to ref content inside atom
+ morkAtom::AliasYarn(atom, &yarn); // works even when atom==nil
+
+ if (yarn.mYarn_Form != mWriter_RowForm)
+ this->ChangeRowForm(ev, yarn.mYarn_Form);
+
+ if (atom && atom->IsBook()) // is it possible to write atom ID?
+ {
+ this->IndentAsNeeded(ev, morkWriter_kRowCellDepth);
+ *p++ = '^';
+ morkBookAtom* ba = (morkBookAtom*)atom;
+
+ mork_size valSize = ev->TokenAsHex(p, ba->mBookAtom_Id);
+ mork_fill yarnFill = yarn.mYarn_Fill;
+ mork_bool putImmYarn = (yarnFill <= valSize);
+ if (putImmYarn) putImmYarn = this->IsYarnAllValue(&yarn);
+
+ if (putImmYarn) // value no bigger than id?
+ {
+ p[-1] = '='; // go back and clobber '^' with '=' instead
+ if (yarnFill) {
+ MORK_MEMCPY(p, yarn.mYarn_Buf, yarnFill);
+ p += yarnFill;
+ }
+ *p++ = ')';
+ mork_size distance = (mork_size)(p - buf);
+ stream->Write(ev->AsMdbEnv(), buf, distance, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+ } else {
+ p += valSize;
+ *p = ')';
+ stream->Write(ev->AsMdbEnv(), buf, colSize + valSize + 4, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+ }
+
+ if (atom->IsAtomDirty()) {
+ atom->SetAtomClean();
+ ++mWriter_DoneCount;
+ }
+ } else // must write an anonymous atom
+ {
+ mork_size pending =
+ yarn.mYarn_Fill + colSize + morkWriter_kYarnEscapeSlop + 2;
+ this->IndentOverMaxLine(ev, pending, morkWriter_kRowCellDepth);
+
+ mork_size bytesWritten;
+ stream->Write(ev->AsMdbEnv(), buf, colSize + 2, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ pending -= (colSize + 2);
+ this->IndentOverMaxLine(ev, pending, morkWriter_kRowCellDepth);
+ stream->Putc(ev, '=');
+ ++mWriter_LineSize;
+
+ this->WriteYarn(ev, &yarn);
+ stream->Putc(ev, ')'); // end cell
+ ++mWriter_LineSize;
+ }
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutRowCells(morkEnv* ev, morkRow* ioRow) {
+ morkCell* cells = ioRow->mRow_Cells;
+ if (cells) {
+ morkCell* end = cells + ioRow->mRow_Length;
+ --cells; // prepare for preincrement:
+ while (++cells < end && ev->Good()) {
+ // note we prefer to avoid writing cells here with no value:
+ if (cells->GetAtom()) // does cell have any value?
+ this->PutCell(ev, cells, /*inWithVal*/ morkBool_kTrue);
+ }
+ }
+ return ev->Good();
+}
+
+mork_bool morkWriter::PutRow(morkEnv* ev, morkRow* ioRow) {
+ if (ioRow && ioRow->IsRow()) {
+ mWriter_RowForm = mWriter_TableForm;
+
+ mork_size bytesWritten;
+ morkStream* stream = mWriter_Stream;
+ char buf[128 + 16]; // buffer for staging hex
+ char* p = buf;
+ mdbOid* roid = &ioRow->mRow_Oid;
+ mork_size ridSize = 0;
+
+ mork_scope tableScope = mWriter_TableRowScope;
+
+ if (ioRow->IsRowDirty()) {
+ if (mWriter_SuppressDirtyRowNewline || !mWriter_LineSize)
+ mWriter_SuppressDirtyRowNewline = morkBool_kFalse;
+ else {
+ if (tableScope) // in a table?
+ mWriter_LineSize = stream->PutIndent(ev, morkWriter_kRowDepth);
+ else
+ mWriter_LineSize = stream->PutIndent(ev, 0); // no indent
+ }
+
+ // mork_rid rid = roid->mOid_Id;
+ *p++ = '['; // start row punct=1
+ mork_size punctSize =
+ (mWriter_BeVerbose) ? 9 : 1; // counting "[ /*r=*/ "
+
+ mork_bool rowRewrite = ioRow->IsRowRewrite();
+
+ if (rowRewrite && mWriter_Incremental) {
+ *p++ = '-';
+ ++punctSize; // counting '-'
+ ++mWriter_LineSize;
+ }
+
+ if (tableScope && roid->mOid_Scope == tableScope)
+ ridSize = ev->TokenAsHex(p, roid->mOid_Id);
+ else
+ ridSize = ev->OidAsHex(p, *roid);
+
+ p += ridSize;
+
+ if (mWriter_BeVerbose) {
+ *p++ = ' '; // punct=2
+ *p++ = '/'; // punct=3
+ *p++ = '*'; // punct=4
+ *p++ = 'r'; // punct=5
+ *p++ = '='; // punct=6
+
+ mork_size usesSize = ev->TokenAsHex(p, (mork_token)ioRow->mRow_GcUses);
+ punctSize += usesSize;
+ p += usesSize;
+
+ *p++ = '*'; // punct=7
+ *p++ = '/'; // punct=8
+ *p++ = ' '; // punct=9
+ }
+ stream->Write(ev->AsMdbEnv(), buf, ridSize + punctSize, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+
+ // special case situation where row puts exactly one column:
+ if (!rowRewrite && mWriter_Incremental && ioRow->HasRowDelta()) {
+ mork_column col = ioRow->GetDeltaColumn();
+ morkCell dummy(col, morkChange_kNil, (morkAtom*)0);
+ morkCell* cell = 0;
+
+ mork_bool withVal = (ioRow->GetDeltaChange() != morkChange_kCut);
+
+ if (withVal) {
+ mork_pos cellPos = 0; // dummy pos
+ cell = ioRow->GetCell(ev, col, &cellPos);
+ }
+ if (!cell) cell = &dummy;
+
+ if (mWriter_BeVerbose)
+ this->PutVerboseCell(ev, cell, withVal);
+ else
+ this->PutCell(ev, cell, withVal);
+ } else // put entire row?
+ {
+ if (mWriter_BeVerbose)
+ this->PutVerboseRowCells(ev, ioRow); // write all, verbosely
+ else
+ this->PutRowCells(ev, ioRow); // write all, hex notation
+ }
+
+ stream->Putc(ev, ']'); // end row
+ ++mWriter_LineSize;
+ } else {
+ this->IndentAsNeeded(ev, morkWriter_kRowDepth);
+
+ if (tableScope && roid->mOid_Scope == tableScope)
+ ridSize = ev->TokenAsHex(p, roid->mOid_Id);
+ else
+ ridSize = ev->OidAsHex(p, *roid);
+
+ stream->Write(ev->AsMdbEnv(), buf, ridSize, &bytesWritten);
+ mWriter_LineSize += bytesWritten;
+ stream->Putc(ev, ' ');
+ ++mWriter_LineSize;
+ }
+
+ ++mWriter_DoneCount;
+
+ ioRow->SetRowClean(); // try to do this at the very last
+ } else
+ ioRow->NonRowTypeWarning(ev);
+
+ return ev->Good();
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkWriter.h b/comm/mailnews/db/mork/morkWriter.h
new file mode 100644
index 0000000000..7e716bec6a
--- /dev/null
+++ b/comm/mailnews/db/mork/morkWriter.h
@@ -0,0 +1,340 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKWRITER_
+#define _MORKWRITER_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKMAP_
+# include "morkMap.h"
+#endif
+
+#ifndef _MORKROWMAP_
+# include "morkRowMap.h"
+#endif
+
+#ifndef _MORKTABLE_
+# include "morkTable.h"
+#endif
+
+#ifndef _MORKATOMMAP_
+# include "morkAtomMap.h"
+#endif
+
+#ifndef _MORKATOMSPACE_
+# include "morkAtomSpace.h"
+#endif
+
+#ifndef _MORKROWSPACE_
+# include "morkRowSpace.h"
+#endif
+
+#ifndef _MORKSTREAM_
+# include "morkStream.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/* buffer size for stream */
+#define morkWriter_kStreamBufSize /*i*/ (16 * 1024)
+
+#define morkDerived_kWriter /*i*/ 0x5772 /* ascii 'Wr' */
+
+#define morkWriter_kPhaseNothingDone 0 /* nothing has yet been done */
+#define morkWriter_kPhaseDirtyAllDone 1 /* DirtyAll() is done */
+#define morkWriter_kPhasePutHeaderDone 2 /* PutHeader() is done */
+
+#define morkWriter_kPhaseRenumberAllDone 3 /* RenumberAll() is done */
+
+#define morkWriter_kPhaseStoreAtomSpaces 4 /*mWriter_StoreAtomSpacesIter*/
+#define morkWriter_kPhaseAtomSpaceAtomAids 5 /*mWriter_AtomSpaceAtomAidsIter*/
+
+#define morkWriter_kPhaseStoreRowSpacesTables 6 /*mWriter_StoreRowSpacesIter*/
+#define morkWriter_kPhaseRowSpaceTables 7 /*mWriter_RowSpaceTablesIter*/
+#define morkWriter_kPhaseTableRowArray 8 /*mWriter_TableRowArrayPos */
+
+#define morkWriter_kPhaseStoreRowSpacesRows 9 /*mWriter_StoreRowSpacesIter*/
+#define morkWriter_kPhaseRowSpaceRows 10 /*mWriter_RowSpaceRowsIter*/
+
+#define morkWriter_kPhaseContentDone 11 /* all content written */
+#define morkWriter_kPhaseWritingDone 12 /* everything has been done */
+
+#define morkWriter_kCountNumberOfPhases 13 /* part of mWrite_TotalCount */
+
+#define morkWriter_kMaxColumnNameSize 128 /* longest writable col name */
+
+#define morkWriter_kMaxIndent 66 /* default value for mWriter_MaxIndent */
+#define morkWriter_kMaxLine 78 /* default value for mWriter_MaxLine */
+
+#define morkWriter_kYarnEscapeSlop 4 /* guess average yarn escape overhead */
+
+#define morkWriter_kTableMetaCellDepth 4 /* */
+#define morkWriter_kTableMetaCellValueDepth 6 /* */
+
+#define morkWriter_kDictMetaCellDepth 4 /* */
+#define morkWriter_kDictMetaCellValueDepth 6 /* */
+
+#define morkWriter_kDictAliasDepth 2 /* */
+#define morkWriter_kDictAliasValueDepth 4 /* */
+
+#define morkWriter_kRowDepth 2 /* */
+#define morkWriter_kRowCellDepth 4 /* */
+#define morkWriter_kRowCellValueDepth 6 /* */
+
+#define morkWriter_kGroupBufSize 64 /* */
+
+// v=1.1 retired on 23-Mar-99 (for metainfo one char column names)
+// v=1.2 retired on 20-Apr-99 (for ":c" suffix on table kind hex refs)
+// v=1.3 retired on 20-Apr-99 (for 1CE:m instead of ill-formed 1CE:^6D)
+#define morkWriter_kFileHeader "// <!-- <mdb:mork:z v=\"1.4\"/> -->"
+
+class morkWriter : public morkNode { // row iterator
+
+ // public: // slots inherited from morkObject (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ morkStore* mWriter_Store; // weak ref to committing store
+ nsIMdbFile* mWriter_File; // strong ref to store's file
+ nsIMdbFile* mWriter_Bud; // strong ref to bud of mWriter_File
+ morkStream* mWriter_Stream; // strong ref to stream on bud file
+ nsIMdbHeap* mWriter_SlotHeap; // strong ref to slot heap
+
+ // GroupIdentity should be based on mStore_CommitGroupIdentity:
+ mork_gid mWriter_CommitGroupIdentity; // transaction ID number
+
+ // GroupBuf holds a hex version of mWriter_CommitGroupIdentity:
+ char mWriter_GroupBuf[morkWriter_kGroupBufSize];
+ mork_fill mWriter_GroupBufFill; // actual bytes in GroupBuf
+
+ mork_count mWriter_TotalCount; // count of all things to be written
+ mork_count mWriter_DoneCount; // count of things already written
+
+ mork_size mWriter_LineSize; // length of current line being written
+ mork_size mWriter_MaxIndent; // line size forcing a line break
+ mork_size mWriter_MaxLine; // line size forcing a value continuation
+
+ mork_cscode mWriter_TableForm; // current charset metainfo
+ mork_scope mWriter_TableAtomScope; // current atom scope
+ mork_scope mWriter_TableRowScope; // current row scope
+ mork_kind mWriter_TableKind; // current table kind
+
+ mork_cscode mWriter_RowForm; // current charset metainfo
+ mork_scope mWriter_RowAtomScope; // current atom scope
+ mork_scope mWriter_RowScope; // current row scope
+
+ mork_cscode mWriter_DictForm; // current charset metainfo
+ mork_scope mWriter_DictAtomScope; // current atom scope
+
+ mork_bool mWriter_NeedDirtyAll; // need to call DirtyAll()
+ mork_bool mWriter_Incremental; // opposite of mWriter_NeedDirtyAll
+ mork_bool mWriter_DidStartDict; // true when a dict has been started
+ mork_bool mWriter_DidEndDict; // true when a dict has been ended
+
+ mork_bool mWriter_SuppressDirtyRowNewline; // for table meta rows
+ mork_bool mWriter_DidStartGroup; // true when a group has been started
+ mork_bool mWriter_DidEndGroup; // true when a group has been ended
+ mork_u1 mWriter_Phase; // status of writing process
+
+ mork_bool mWriter_BeVerbose; // driven by env and table verbose settings:
+ // mWriter_BeVerbose equals ( ev->mEnv_BeVerbose || table->IsTableVerbose() )
+
+ mork_u1 mWriter_Pad[3]; // for u4 alignment
+
+ mork_pos mWriter_TableRowArrayPos; // index into mTable_RowArray
+
+ char mWriter_SafeNameBuf[(morkWriter_kMaxColumnNameSize * 2) + 4];
+ // Note: extra four bytes in ColNameBuf means we can always append to yarn
+
+ char mWriter_ColNameBuf[morkWriter_kMaxColumnNameSize + 4];
+ // Note: extra four bytes in ColNameBuf means we can always append to yarn
+
+ mdbYarn mWriter_ColYarn; // a yarn to describe space in ColNameBuf:
+ // mYarn_Buf == mWriter_ColNameBuf, mYarn_Size ==
+ // morkWriter_kMaxColumnNameSize
+
+ mdbYarn mWriter_SafeYarn; // a yarn to describe space in ColNameBuf:
+ // mYarn_Buf == mWriter_SafeNameBuf, mYarn_Size == (kMaxColumnNameSize * 2)
+
+ morkAtomSpaceMapIter mWriter_StoreAtomSpacesIter; // for mStore_AtomSpaces
+ morkAtomAidMapIter mWriter_AtomSpaceAtomAidsIter; // for AtomSpace_AtomAids
+
+ morkRowSpaceMapIter mWriter_StoreRowSpacesIter; // for mStore_RowSpaces
+ morkTableMapIter mWriter_RowSpaceTablesIter; // for mRowSpace_Tables
+
+#ifdef MORK_ENABLE_PROBE_MAPS
+ morkRowProbeMapIter mWriter_RowSpaceRowsIter; // for mRowSpace_Rows
+#else /*MORK_ENABLE_PROBE_MAPS*/
+ morkRowMapIter mWriter_RowSpaceRowsIter; // for mRowSpace_Rows
+#endif /*MORK_ENABLE_PROBE_MAPS*/
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseWriter()
+ virtual ~morkWriter(); // assert that close executed earlier
+
+ public: // morkWriter construction & destruction
+ morkWriter(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap,
+ morkStore* ioStore, nsIMdbFile* ioFile, nsIMdbHeap* ioSlotHeap);
+ void CloseWriter(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkWriter(const morkWriter& other);
+ morkWriter& operator=(const morkWriter& other);
+
+ public: // dynamic type identification
+ mork_bool IsWriter() const {
+ return IsNode() && mNode_Derived == morkDerived_kWriter;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing & errors
+ static void NonWriterTypeError(morkEnv* ev);
+ static void NilWriterStoreError(morkEnv* ev);
+ static void NilWriterBudError(morkEnv* ev);
+ static void NilWriterStreamError(morkEnv* ev);
+ static void NilWriterFileError(morkEnv* ev);
+ static void UnsupportedPhaseError(morkEnv* ev);
+
+ public: // utitlities
+ void ChangeRowForm(morkEnv* ev, mork_cscode inNewForm);
+ void ChangeDictForm(morkEnv* ev, mork_cscode inNewForm);
+ void ChangeDictAtomScope(morkEnv* ev, mork_scope inScope);
+
+ public: // inlines
+ mork_bool DidStartDict() const { return mWriter_DidStartDict; }
+ mork_bool DidEndDict() const { return mWriter_DidEndDict; }
+
+ void IndentAsNeeded(morkEnv* ev, mork_size inDepth) {
+ if (mWriter_LineSize > mWriter_MaxIndent)
+ mWriter_LineSize = mWriter_Stream->PutIndent(ev, inDepth);
+ }
+
+ void IndentOverMaxLine(morkEnv* ev, mork_size inPendingSize,
+ mork_size inDepth) {
+ if (mWriter_LineSize + inPendingSize > mWriter_MaxLine)
+ mWriter_LineSize = mWriter_Stream->PutIndent(ev, inDepth);
+ }
+
+ public: // delayed construction
+ void MakeWriterStream(morkEnv* ev); // give writer a suitable stream
+
+ public: // iterative/asynchronous writing
+ mork_bool WriteMore(morkEnv* ev); // call until IsWritingDone() is true
+
+ mork_bool IsWritingDone() const // don't call WriteMore() any longer?
+ {
+ return mWriter_Phase == morkWriter_kPhaseWritingDone;
+ }
+
+ public: // marking all content dirty
+ mork_bool DirtyAll(morkEnv* ev);
+ // DirtyAll() visits every store sub-object and marks
+ // them dirty, including every table, row, cell, and atom. The return
+ // equals ev->Good(), to show whether any error happened. This method is
+ // intended for use in the beginning of a "compress commit" which writes
+ // all store content, whether dirty or not. We dirty everything first so
+ // that later iterations over content can mark things clean as they are
+ // written, and organize the process of serialization so that objects are
+ // written only at need (because of being dirty). Note the method can
+ // stop early when any error happens, since this will abort any commit.
+
+ public: // group commit transactions
+ mork_bool StartGroup(morkEnv* ev);
+ mork_bool CommitGroup(morkEnv* ev);
+ mork_bool AbortGroup(morkEnv* ev);
+
+ public: // phase methods
+ mork_bool OnNothingDone(morkEnv* ev);
+ mork_bool OnDirtyAllDone(morkEnv* ev);
+ mork_bool OnPutHeaderDone(morkEnv* ev);
+
+ mork_bool OnRenumberAllDone(morkEnv* ev);
+
+ mork_bool OnStoreAtomSpaces(morkEnv* ev);
+ mork_bool OnAtomSpaceAtomAids(morkEnv* ev);
+
+ mork_bool OnStoreRowSpacesTables(morkEnv* ev);
+ mork_bool OnRowSpaceTables(morkEnv* ev);
+ mork_bool OnTableRowArray(morkEnv* ev);
+
+ mork_bool OnStoreRowSpacesRows(morkEnv* ev);
+ mork_bool OnRowSpaceRows(morkEnv* ev);
+
+ mork_bool OnContentDone(morkEnv* ev);
+ mork_bool OnWritingDone(morkEnv* ev);
+
+ public: // writing dict items first pass
+ mork_bool PutTableDict(morkEnv* ev, morkTable* ioTable);
+ mork_bool PutRowDict(morkEnv* ev, morkRow* ioRow);
+
+ public: // writing node content second pass
+ mork_bool PutTable(morkEnv* ev, morkTable* ioTable);
+ mork_bool PutRow(morkEnv* ev, morkRow* ioRow);
+ mork_bool PutRowCells(morkEnv* ev, morkRow* ioRow);
+ mork_bool PutVerboseRowCells(morkEnv* ev, morkRow* ioRow);
+
+ mork_bool PutCell(morkEnv* ev, morkCell* ioCell, mork_bool inWithVal);
+ mork_bool PutVerboseCell(morkEnv* ev, morkCell* ioCell, mork_bool inWithVal);
+
+ mork_bool PutTableChange(morkEnv* ev, const morkTableChange* inChange);
+
+ public: // other writer methods
+ mork_bool IsYarnAllValue(const mdbYarn* inYarn);
+
+ mork_size WriteYarn(morkEnv* ev, const mdbYarn* inYarn);
+ // return number of atom bytes written on the current line (which
+ // implies that escaped line breaks will make the size value smaller
+ // than the entire yarn's size, since only part goes on a last line).
+
+ mork_size WriteAtom(morkEnv* ev, const morkAtom* inAtom);
+ // return number of atom bytes written on the current line (which
+ // implies that escaped line breaks will make the size value smaller
+ // than the entire atom's size, since only part goes on a last line).
+
+ void WriteAllStoreTables(morkEnv* ev);
+ void WriteAtomSpaceAsDict(morkEnv* ev, morkAtomSpace* ioSpace);
+
+ void WriteTokenToTokenMetaCell(morkEnv* ev, mork_token inCol,
+ mork_token inValue);
+ void WriteStringToTokenDictCell(morkEnv* ev, const char* inCol,
+ mork_token inValue);
+ // Note inCol should begin with '(' and end with '=', with col in between.
+
+ void StartDict(morkEnv* ev);
+ void EndDict(morkEnv* ev);
+
+ void StartTable(morkEnv* ev, morkTable* ioTable);
+ void EndTable(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakWriter(morkWriter* me, morkEnv* ev, morkWriter** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongWriter(morkWriter* me, morkEnv* ev,
+ morkWriter** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKTABLEROWCURSOR_ */
diff --git a/comm/mailnews/db/mork/morkYarn.cpp b/comm/mailnews/db/mork/morkYarn.cpp
new file mode 100644
index 0000000000..013b36c754
--- /dev/null
+++ b/comm/mailnews/db/mork/morkYarn.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#ifndef _MORKYARN_
+# include "morkYarn.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// ````` ````` ````` ````` `````
+// { ===== begin morkNode interface =====
+
+/*public virtual*/ void morkYarn::CloseMorkNode(
+ morkEnv* ev) /*i*/ // CloseYarn() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseYarn(ev);
+ this->MarkShut();
+ }
+}
+
+/*public virtual*/
+morkYarn::~morkYarn() /*i*/ // assert CloseYarn() executed earlier
+{
+ MORK_ASSERT(mYarn_Body.mYarn_Buf == 0);
+}
+
+/*public non-poly*/
+morkYarn::morkYarn(morkEnv* ev, /*i*/
+ const morkUsage& inUsage, nsIMdbHeap* ioHeap)
+ : morkNode(ev, inUsage, ioHeap) {
+ if (ev->Good()) mNode_Derived = morkDerived_kYarn;
+}
+
+/*public non-poly*/ void morkYarn::CloseYarn(
+ morkEnv* ev) /*i*/ // called by CloseMorkNode();
+{
+ if (this->IsNode())
+ this->MarkShut();
+ else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+// ````` ````` ````` ````` `````
+
+/*static*/ void morkYarn::NonYarnTypeError(morkEnv* ev) {
+ ev->NewError("non morkYarn");
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/morkYarn.h b/comm/mailnews/db/mork/morkYarn.h
new file mode 100644
index 0000000000..e4cca8a843
--- /dev/null
+++ b/comm/mailnews/db/mork/morkYarn.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKYARN_
+#define _MORKYARN_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkDerived_kYarn /*i*/ 0x7952 /* ascii 'yR' */
+
+/*| morkYarn: a reference counted nsIMdbYarn C struct. This is for use in those
+**| few cases where single instances of reference counted buffers are needed
+**| in Mork, and we expect few enough instances that overhead is not a factor
+**| in decided whether to use such a thing.
+|*/
+class morkYarn : public morkNode { // refcounted yarn
+
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ mdbYarn mYarn_Body;
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseYarn() only if open
+ virtual ~morkYarn(); // assert that CloseYarn() executed earlier
+
+ public: // morkYarn construction & destruction
+ morkYarn(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioHeap);
+ void CloseYarn(morkEnv* ev); // called by CloseMorkNode();
+
+ private: // copying is not allowed
+ morkYarn(const morkYarn& other);
+ morkYarn& operator=(const morkYarn& other);
+
+ public: // dynamic type identification
+ mork_bool IsYarn() const {
+ return IsNode() && mNode_Derived == morkDerived_kYarn;
+ }
+ // } ===== end morkNode methods =====
+
+ public: // typing
+ static void NonYarnTypeError(morkEnv* ev);
+
+ public: // typesafe refcounting inlines calling inherited morkNode methods
+ static void SlotWeakYarn(morkYarn* me, morkEnv* ev, morkYarn** ioSlot) {
+ morkNode::SlotWeakNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+
+ static void SlotStrongYarn(morkYarn* me, morkEnv* ev, morkYarn** ioSlot) {
+ morkNode::SlotStrongNode((morkNode*)me, ev, (morkNode**)ioSlot);
+ }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKYARN_ */
diff --git a/comm/mailnews/db/mork/morkZone.cpp b/comm/mailnews/db/mork/morkZone.cpp
new file mode 100644
index 0000000000..6ee3032f48
--- /dev/null
+++ b/comm/mailnews/db/mork/morkZone.cpp
@@ -0,0 +1,487 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKZONE_
+# include "morkZone.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// { ===== begin morkNode interface =====
+// public: // morkNode virtual methods
+void morkZone::CloseMorkNode(morkEnv* ev) // CloseZone() only if open
+{
+ if (this->IsOpenNode()) {
+ this->MarkClosing();
+ this->CloseZone(ev);
+ this->MarkShut();
+ }
+}
+
+morkZone::~morkZone() // assert that CloseZone() executed earlier
+{
+ MORK_ASSERT(this->IsShutNode());
+}
+
+// public: // morkMap construction & destruction
+morkZone::morkZone(morkEnv* ev, const morkUsage& inUsage,
+ nsIMdbHeap* ioNodeHeap, nsIMdbHeap* ioZoneHeap)
+ : morkNode(ev, inUsage, ioNodeHeap),
+ mZone_Heap(0),
+ mZone_HeapVolume(0),
+ mZone_BlockVolume(0),
+ mZone_RunVolume(0),
+ mZone_ChipVolume(0)
+
+ ,
+ mZone_FreeOldRunVolume(0)
+
+ ,
+ mZone_HunkCount(0),
+ mZone_FreeOldRunCount(0)
+
+ ,
+ mZone_HunkList(0),
+ mZone_FreeOldRunList(0)
+
+ ,
+ mZone_At(0),
+ mZone_AtSize(0)
+
+// morkRun* mZone_FreeRuns[ morkZone_kBuckets + 1 ];
+{
+ morkRun** runs = mZone_FreeRuns;
+ morkRun** end = runs + (morkZone_kBuckets + 1); // one past last slot
+ --runs; // prepare for preincrement
+ while (++runs < end) // another slot in array?
+ *runs = 0; // clear all the slots
+
+ if (ev->Good()) {
+ if (ioZoneHeap) {
+ nsIMdbHeap_SlotStrongHeap(ioZoneHeap, ev, &mZone_Heap);
+ if (ev->Good()) mNode_Derived = morkDerived_kZone;
+ } else
+ ev->NilPointerError();
+ }
+}
+
+void morkZone::CloseZone(morkEnv* ev) // called by CloseMorkNode()
+{
+ if (this->IsNode()) {
+ nsIMdbHeap* heap = mZone_Heap;
+ if (heap) {
+ morkHunk* hunk = 0;
+ nsIMdbEnv* mev = ev->AsMdbEnv();
+
+ morkHunk* next = mZone_HunkList;
+ while ((hunk = next) != 0) {
+#ifdef morkHunk_USE_TAG_SLOT
+ if (!hunk->HunkGoodTag()) hunk->BadHunkTagWarning(ev);
+#endif /* morkHunk_USE_TAG_SLOT */
+
+ next = hunk->HunkNext();
+ heap->Free(mev, hunk);
+ }
+ }
+ nsIMdbHeap_SlotStrongHeap((nsIMdbHeap*)0, ev, &mZone_Heap);
+ this->MarkShut();
+ } else
+ this->NonNodeError(ev);
+}
+
+// } ===== end morkNode methods =====
+
+/*static*/ void morkZone::NonZoneTypeError(morkEnv* ev) {
+ ev->NewError("non morkZone");
+}
+
+/*static*/ void morkZone::NilZoneHeapError(morkEnv* ev) {
+ ev->NewError("nil mZone_Heap");
+}
+
+/*static*/ void morkHunk::BadHunkTagWarning(morkEnv* ev) {
+ ev->NewWarning("bad mHunk_Tag");
+}
+
+/*static*/ void morkRun::BadRunTagError(morkEnv* ev) {
+ ev->NewError("bad mRun_Tag");
+}
+
+/*static*/ void morkRun::RunSizeAlignError(morkEnv* ev) {
+ ev->NewError("bad RunSize() alignment");
+}
+
+// { ===== begin morkZone methods =====
+
+mork_size morkZone::zone_grow_at(morkEnv* ev, mork_size inNeededSize) {
+ mZone_At = 0; // remove any ref to current hunk
+ mZone_AtSize = 0; // zero available bytes in current hunk
+
+ mork_size runSize = 0; // actual size of a particular run
+
+ // try to find a run in old run list with at least inNeededSize bytes:
+ morkRun* run = mZone_FreeOldRunList; // cursor in list scan
+ morkRun* prev = 0; // the node before run in the list scan
+
+ while (run) // another run in list to check?
+ {
+ morkOldRun* oldRun = (morkOldRun*)run;
+ mork_size oldSize = oldRun->OldSize();
+ if (oldSize >= inNeededSize) // found one big enough?
+ {
+ runSize = oldSize;
+ break; // end while loop early
+ }
+ prev = run; // remember last position in singly linked list
+ run = run->RunNext(); // advance cursor to next node in list
+ }
+ if (runSize && run) // found a usable old run?
+ {
+ morkRun* next = run->RunNext();
+ if (prev) // another node in free list precedes run?
+ prev->RunSetNext(next); // unlink run
+ else
+ mZone_FreeOldRunList = next; // unlink run from head of list
+
+ morkOldRun* oldRun = (morkOldRun*)run;
+ oldRun->OldSetSize(runSize);
+ mZone_At = (mork_u1*)run;
+ mZone_AtSize = runSize;
+
+#ifdef morkZone_CONFIG_DEBUG
+# ifdef morkZone_CONFIG_ALIGN_8
+ mork_ip lowThree = ((mork_ip)mZone_At) & 7;
+ if (lowThree) // not 8 byte aligned?
+# else /*morkZone_CONFIG_ALIGN_8*/
+ mork_ip lowTwo = ((mork_ip)mZone_At) & 3;
+ if (lowTwo) // not 4 byte aligned?
+# endif /*morkZone_CONFIG_ALIGN_8*/
+ ev->NewWarning("mZone_At not aligned");
+#endif /*morkZone_CONFIG_DEBUG*/
+ } else // need to allocate a brand new run
+ {
+ inNeededSize += 7; // allow for possible alignment padding
+ mork_size newSize = (inNeededSize > morkZone_kNewHunkSize)
+ ? inNeededSize
+ : morkZone_kNewHunkSize;
+
+ morkHunk* hunk = this->zone_new_hunk(ev, newSize);
+ if (hunk) {
+ morkRun* hunkRun = hunk->HunkRun();
+ mork_u1* at = (mork_u1*)hunkRun->RunAsBlock();
+ mork_ip lowBits = ((mork_ip)at) & 7;
+ if (lowBits) // not 8 byte aligned?
+ {
+ mork_ip skip = (8 - lowBits); // skip the complement to align
+ at += skip;
+ newSize -= skip;
+ }
+ mZone_At = at;
+ mZone_AtSize = newSize;
+ }
+ }
+
+ return mZone_AtSize;
+}
+
+morkHunk* morkZone::zone_new_hunk(morkEnv* ev, mdb_size inSize) // alloc
+{
+ mdb_size hunkSize = inSize + sizeof(morkHunk);
+ void* outBlock = 0; // we are going straight to the heap:
+ mZone_Heap->Alloc(ev->AsMdbEnv(), hunkSize, &outBlock);
+ if (outBlock) {
+#ifdef morkZone_CONFIG_VOL_STATS
+ mZone_HeapVolume += hunkSize; // track all heap allocations
+#endif /* morkZone_CONFIG_VOL_STATS */
+
+ morkHunk* hunk = (morkHunk*)outBlock;
+#ifdef morkHunk_USE_TAG_SLOT
+ hunk->HunkInitTag();
+#endif /* morkHunk_USE_TAG_SLOT */
+
+ hunk->HunkSetNext(mZone_HunkList);
+ mZone_HunkList = hunk;
+ ++mZone_HunkCount;
+
+ morkRun* run = hunk->HunkRun();
+ run->RunSetSize(inSize);
+#ifdef morkRun_USE_TAG_SLOT
+ run->RunInitTag();
+#endif /* morkRun_USE_TAG_SLOT */
+
+ return hunk;
+ }
+ if (ev->Good()) // got this far without any error reported yet?
+ ev->OutOfMemoryError();
+ return (morkHunk*)0;
+}
+
+void* morkZone::zone_new_chip(morkEnv* ev, mdb_size inSize) // alloc
+{
+#ifdef morkZone_CONFIG_VOL_STATS
+ mZone_BlockVolume += inSize; // sum sizes of both chips and runs
+#endif /* morkZone_CONFIG_VOL_STATS */
+
+ mork_u1* at = mZone_At;
+ mork_size atSize = mZone_AtSize; // available bytes in current hunk
+ if (atSize >= inSize) // current hunk can satisfy request?
+ {
+ mZone_At = at + inSize;
+ mZone_AtSize = atSize - inSize;
+ return at;
+ } else if (atSize > morkZone_kMaxHunkWaste) // over max waste allowed?
+ {
+ morkHunk* hunk = this->zone_new_hunk(ev, inSize);
+ if (hunk) return hunk->HunkRun();
+
+ return (void*)0; // show allocation has failed
+ } else // get ourselves a new hunk for suballocation:
+ {
+ atSize = this->zone_grow_at(ev, inSize); // get a new hunk
+ }
+
+ if (atSize >= inSize) // current hunk can satisfy request?
+ {
+ at = mZone_At;
+ mZone_At = at + inSize;
+ mZone_AtSize = atSize - inSize;
+ return at;
+ }
+
+ if (ev->Good()) // got this far without any error reported yet?
+ ev->OutOfMemoryError();
+
+ return (void*)0; // show allocation has failed
+}
+
+void* morkZone::ZoneNewChip(morkEnv* ev, mdb_size inSize) // alloc
+{
+#ifdef morkZone_CONFIG_ARENA
+
+# ifdef morkZone_CONFIG_DEBUG
+ if (!this->IsZone())
+ this->NonZoneTypeError(ev);
+ else if (!mZone_Heap)
+ this->NilZoneHeapError(ev);
+# endif /*morkZone_CONFIG_DEBUG*/
+
+# ifdef morkZone_CONFIG_ALIGN_8
+ inSize += 7;
+ inSize &= ~((mork_ip)7); // force to multiple of 8 bytes
+# else /*morkZone_CONFIG_ALIGN_8*/
+ inSize += 3;
+ inSize &= ~((mork_ip)3); // force to multiple of 4 bytes
+# endif /*morkZone_CONFIG_ALIGN_8*/
+
+# ifdef morkZone_CONFIG_VOL_STATS
+ mZone_ChipVolume += inSize; // sum sizes of chips only
+# endif /* morkZone_CONFIG_VOL_STATS */
+
+ return this->zone_new_chip(ev, inSize);
+
+#else /*morkZone_CONFIG_ARENA*/
+ void* outBlock = 0;
+ mZone_Heap->Alloc(ev->AsMdbEnv(), inSize, &outBlock);
+ return outBlock;
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+// public: // ...but runs do indeed know how big they are
+void* morkZone::ZoneNewRun(morkEnv* ev, mdb_size inSize) // alloc
+{
+#ifdef morkZone_CONFIG_ARENA
+
+# ifdef morkZone_CONFIG_DEBUG
+ if (!this->IsZone())
+ this->NonZoneTypeError(ev);
+ else if (!mZone_Heap)
+ this->NilZoneHeapError(ev);
+# endif /*morkZone_CONFIG_DEBUG*/
+
+ inSize += morkZone_kRoundAdd;
+ inSize &= morkZone_kRoundMask;
+ if (inSize <= morkZone_kMaxCachedRun) {
+ morkRun** bucket = mZone_FreeRuns + (inSize >> morkZone_kRoundBits);
+ morkRun* hit = *bucket;
+ if (hit) // cache hit?
+ {
+ *bucket = hit->RunNext();
+ hit->RunSetSize(inSize);
+ return hit->RunAsBlock();
+ }
+ }
+ mdb_size blockSize = inSize + sizeof(morkRun); // plus run overhead
+# ifdef morkZone_CONFIG_VOL_STATS
+ mZone_RunVolume += blockSize; // sum sizes of runs only
+# endif /* morkZone_CONFIG_VOL_STATS */
+ morkRun* run = (morkRun*)this->zone_new_chip(ev, blockSize);
+ if (run) {
+ run->RunSetSize(inSize);
+# ifdef morkRun_USE_TAG_SLOT
+ run->RunInitTag();
+# endif /* morkRun_USE_TAG_SLOT */
+ return run->RunAsBlock();
+ }
+
+ if (ev->Good()) // got this far without any error reported yet?
+ ev->OutOfMemoryError();
+
+ return (void*)0; // indicate failed allocation
+
+#else /*morkZone_CONFIG_ARENA*/
+ void* outBlock = 0;
+ mZone_Heap->Alloc(ev->AsMdbEnv(), inSize, &outBlock);
+ return outBlock;
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+void morkZone::ZoneZapRun(morkEnv* ev, void* ioRunBlock) // free
+{
+#ifdef morkZone_CONFIG_ARENA
+
+ morkRun* run = morkRun::BlockAsRun(ioRunBlock);
+ mdb_size runSize = run->RunSize();
+# ifdef morkZone_CONFIG_VOL_STATS
+ mZone_BlockVolume -= runSize; // tracking sizes of both chips and runs
+# endif /* morkZone_CONFIG_VOL_STATS */
+
+# ifdef morkZone_CONFIG_DEBUG
+ if (!this->IsZone())
+ this->NonZoneTypeError(ev);
+ else if (!mZone_Heap)
+ this->NilZoneHeapError(ev);
+ else if (!ioRunBlock)
+ ev->NilPointerError();
+ else if (runSize & morkZone_kRoundAdd)
+ run->RunSizeAlignError(ev);
+# ifdef morkRun_USE_TAG_SLOT
+ else if (!run->RunGoodTag())
+ run->BadRunTagError(ev);
+# endif /* morkRun_USE_TAG_SLOT */
+# endif /*morkZone_CONFIG_DEBUG*/
+
+ if (runSize <= morkZone_kMaxCachedRun) // goes into free run list?
+ {
+ morkRun** bucket = mZone_FreeRuns + (runSize >> morkZone_kRoundBits);
+ run->RunSetNext(*bucket); // push onto free run list
+ *bucket = run;
+ } else // free old run list
+ {
+ run->RunSetNext(mZone_FreeOldRunList); // push onto free old run list
+ mZone_FreeOldRunList = run;
+ ++mZone_FreeOldRunCount;
+# ifdef morkZone_CONFIG_VOL_STATS
+ mZone_FreeOldRunVolume += runSize;
+# endif /* morkZone_CONFIG_VOL_STATS */
+
+ morkOldRun* oldRun = (morkOldRun*)run; // to access extra size slot
+ oldRun->OldSetSize(runSize); // so we know how big this is later
+ }
+
+#else /*morkZone_CONFIG_ARENA*/
+ mZone_Heap->Free(ev->AsMdbEnv(), ioRunBlock);
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+void* morkZone::ZoneGrowRun(morkEnv* ev, void* ioRunBlock, mdb_size inSize) {
+#ifdef morkZone_CONFIG_ARENA
+
+ morkRun* run = morkRun::BlockAsRun(ioRunBlock);
+ mdb_size runSize = run->RunSize();
+
+# ifdef morkZone_CONFIG_DEBUG
+ if (!this->IsZone())
+ this->NonZoneTypeError(ev);
+ else if (!mZone_Heap)
+ this->NilZoneHeapError(ev);
+# endif /*morkZone_CONFIG_DEBUG*/
+
+# ifdef morkZone_CONFIG_ALIGN_8
+ inSize += 7;
+ inSize &= ~((mork_ip)7); // force to multiple of 8 bytes
+# else /*morkZone_CONFIG_ALIGN_8*/
+ inSize += 3;
+ inSize &= ~((mork_ip)3); // force to multiple of 4 bytes
+# endif /*morkZone_CONFIG_ALIGN_8*/
+
+ if (inSize > runSize) {
+ void* newBuf = this->ZoneNewRun(ev, inSize);
+ if (newBuf) {
+ MORK_MEMCPY(newBuf, ioRunBlock, runSize);
+ this->ZoneZapRun(ev, ioRunBlock);
+
+ return newBuf;
+ }
+ } else
+ return ioRunBlock; // old size is big enough
+
+ if (ev->Good()) // got this far without any error reported yet?
+ ev->OutOfMemoryError();
+
+ return (void*)0; // indicate failed allocation
+
+#else /*morkZone_CONFIG_ARENA*/
+ void* outBlock = 0;
+ mZone_Heap->Free(ev->AsMdbEnv(), ioRunBlock);
+ return outBlock;
+#endif /*morkZone_CONFIG_ARENA*/
+}
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+// { ===== begin nsIMdbHeap methods =====
+/*virtual*/ nsresult morkZone::Alloc(
+ nsIMdbEnv* mev, // allocate a piece of memory
+ mdb_size inSize, // requested size of new memory block
+ void** outBlock) // memory block of inSize bytes, or nil
+{
+ nsresult outErr = NS_OK;
+ void* block = 0;
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ block = this->ZoneNewRun(ev, inSize);
+ outErr = ev->AsErr();
+ } else
+ outErr = morkEnv_kOutOfMemoryError;
+
+ if (outBlock) *outBlock = block;
+
+ return outErr;
+}
+
+/*virtual*/ nsresult morkZone::Free(
+ nsIMdbEnv* mev, // free block allocated earlier by Alloc()
+ void* inBlock) {
+ nsresult outErr = NS_OK;
+ if (inBlock) {
+ morkEnv* ev = morkEnv::FromMdbEnv(mev);
+ if (ev) {
+ this->ZoneZapRun(ev, inBlock);
+ outErr = ev->AsErr();
+ } else
+ // XXX 1 is not a valid nsresult
+ outErr = static_cast<nsresult>(1);
+ }
+
+ return outErr;
+}
+
+// } ===== end nsIMdbHeap methods =====
diff --git a/comm/mailnews/db/mork/morkZone.h b/comm/mailnews/db/mork/morkZone.h
new file mode 100644
index 0000000000..5399bb9827
--- /dev/null
+++ b/comm/mailnews/db/mork/morkZone.h
@@ -0,0 +1,313 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MORKZONE_
+#define _MORKZONE_ 1
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _MORKNODE_
+# include "morkNode.h"
+#endif
+
+#ifndef _MORKDEQUE_
+# include "morkDeque.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| CONFIG_DEBUG: do paranoid debug checks if defined.
+|*/
+#ifdef MORK_DEBUG
+# define morkZone_CONFIG_DEBUG 1 /* debug paranoid if defined */
+#endif /*MORK_DEBUG*/
+
+/*| CONFIG_STATS: keep volume and usage statistics.
+|*/
+#define morkZone_CONFIG_VOL_STATS 1 /* count space used by zone instance */
+
+/*| CONFIG_ARENA: if this is defined, then the morkZone class will alloc big
+**| blocks from the zone's heap, and suballocate from these. If undefined,
+**| then morkZone will just pass all calls through to the zone's heap.
+|*/
+#ifdef MORK_ENABLE_ZONE_ARENAS
+# define morkZone_CONFIG_ARENA 1 /* be arena, if defined; otherwise no-op */
+#endif /*MORK_ENABLE_ZONE_ARENAS*/
+
+/*| CONFIG_ALIGN_8: if this is defined, then the morkZone class will give
+**| blocks 8 byte alignment instead of only 4 byte alignment.
+|*/
+#ifdef MORK_CONFIG_ALIGN_8
+# define morkZone_CONFIG_ALIGN_8 1 /* ifdef: align to 8 bytes, otherwise 4 */
+#endif /*MORK_CONFIG_ALIGN_8*/
+
+/*| CONFIG_PTR_SIZE_4: if this is defined, then the morkZone class will
+**| assume sizeof(void*) == 4, so a tag slot for padding is needed.
+|*/
+#ifdef MORK_CONFIG_PTR_SIZE_4
+# define morkZone_CONFIG_PTR_SIZE_4 1 /* ifdef: sizeof(void*) == 4 */
+#endif /*MORK_CONFIG_PTR_SIZE_4*/
+
+/*| morkZone_USE_TAG_SLOT: if this is defined, then define slot mRun_Tag
+**| in order to achieve eight byte alignment after the mRun_Next slot.
+|*/
+#if defined(morkZone_CONFIG_ALIGN_8) && defined(morkZone_CONFIG_PTR_SIZE_4)
+# define morkRun_USE_TAG_SLOT 1 /* need mRun_Tag slot inside morkRun */
+# define morkHunk_USE_TAG_SLOT 1 /* need mHunk_Tag slot inside morkHunk */
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkRun_kTag ((mork_u4)0x6D52754E) /* ascii 'mRuN' */
+
+/*| morkRun: structure used by morkZone for sized blocks
+|*/
+class morkRun {
+ protected: // member variable slots
+#ifdef morkRun_USE_TAG_SLOT
+ mork_u4 mRun_Tag; // force 8 byte alignment after mRun_Next
+#endif /* morkRun_USE_TAG_SLOT */
+
+ morkRun* mRun_Next;
+
+ public: // pointer interpretation of mRun_Next (when inside a list):
+ morkRun* RunNext() const { return mRun_Next; }
+ void RunSetNext(morkRun* ioNext) { mRun_Next = ioNext; }
+
+ public: // size interpretation of mRun_Next (when not inside a list):
+ mork_size RunSize() const { return (mork_size)((mork_ip)mRun_Next); }
+ void RunSetSize(mork_size inSize) { mRun_Next = (morkRun*)((mork_ip)inSize); }
+
+ public: // maintenance and testing of optional tag magic signature slot:
+#ifdef morkRun_USE_TAG_SLOT
+ void RunInitTag() { mRun_Tag = morkRun_kTag; }
+ mork_bool RunGoodTag() { return (mRun_Tag == morkRun_kTag); }
+#endif /* morkRun_USE_TAG_SLOT */
+
+ public: // conversion back and forth to inline block following run instance:
+ void* RunAsBlock() { return (((mork_u1*)this) + sizeof(morkRun)); }
+
+ static morkRun* BlockAsRun(void* ioBlock) {
+ return (morkRun*)(((mork_u1*)ioBlock) - sizeof(morkRun));
+ }
+
+ public: // typing & errors
+ static void BadRunTagError(morkEnv* ev);
+ static void RunSizeAlignError(morkEnv* ev);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| morkOldRun: more space to record size when run is put into old free list
+|*/
+class morkOldRun : public morkRun {
+ protected: // need another size field when mRun_Next is used for linkage:
+ mdb_size mOldRun_Size;
+
+ public: // size getter/setter
+ mork_size OldSize() const { return mOldRun_Size; }
+ void OldSetSize(mork_size inSize) { mOldRun_Size = inSize; }
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define morkHunk_kTag ((mork_u4)0x68556E4B) /* ascii 'hUnK' */
+
+/*| morkHunk: structure used by morkZone for heap allocations.
+|*/
+class morkHunk {
+ protected: // member variable slots
+#ifdef morkHunk_USE_TAG_SLOT
+ mork_u4 mHunk_Tag; // force 8 byte alignment after mHunk_Next
+#endif /* morkHunk_USE_TAG_SLOT */
+
+ morkHunk* mHunk_Next;
+
+ morkRun mHunk_Run;
+
+ public: // setters
+ void HunkSetNext(morkHunk* ioNext) { mHunk_Next = ioNext; }
+
+ public: // getters
+ morkHunk* HunkNext() const { return mHunk_Next; }
+
+ morkRun* HunkRun() { return &mHunk_Run; }
+
+ public: // maintenance and testing of optional tag magic signature slot:
+#ifdef morkHunk_USE_TAG_SLOT
+ void HunkInitTag() { mHunk_Tag = morkHunk_kTag; }
+ mork_bool HunkGoodTag() { return (mHunk_Tag == morkHunk_kTag); }
+#endif /* morkHunk_USE_TAG_SLOT */
+
+ public: // typing & errors
+ static void BadHunkTagWarning(morkEnv* ev);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+/*| kNewHunkSize: the default size for a hunk, assuming we must allocate
+**| a new one whenever the free hunk list does not already have. Note this
+**| number should not be changed without also considering suitable changes
+**| in the related kMaxHunkWaste and kMinHunkSize constants.
+|*/
+#define morkZone_kNewHunkSize ((mork_size)(64 * 1024)) /* 64K per hunk */
+
+/*| kMaxFreeVolume: some number of bytes of free space in the free hunk list
+**| over which we no longer want to add more free hunks to the list, for fear
+**| of accumulating too much unused, fragmented free space. This should be a
+**| small multiple of kNewHunkSize, say about two to four times as great, to
+**| allow for no more free hunk space than fits in a handful of new hunks.
+**| This strategy will let us usefully accumulate "some" free space in the
+**| free hunk list, but without accumulating "too much" free space that way.
+|*/
+#define morkZone_kMaxFreeVolume (morkZone_kNewHunkSize * 3)
+
+/*| kMaxHunkWaste: if a current request is larger than this, and we cannot
+**| satisfy the request with the current hunk, then we just allocate the
+**| block from the heap without changing the current hunk. Basically this
+**| number represents the largest amount of memory we are willing to waste,
+**| since a block request barely less than this can cause the current hunk
+**| to be retired (with any unused space wasted) as well get a new hunk.
+|*/
+#define morkZone_kMaxHunkWaste ((mork_size)4096) /* 1/16 kNewHunkSize */
+
+/*| kRound*: the algorithm for rounding up allocation sizes for caching
+**| in free lists works like the following. We add kRoundAdd to any size
+**| requested, and then bitwise AND with kRoundMask, and this will give us
+**| the smallest multiple of kRoundSize that is at least as large as the
+**| requested size. Then if we rightshift this number by kRoundBits, we
+**| will have the index into the mZone_FreeRuns array which will hold any
+**| cache runs of that size. So 4 bits of shift gives us a granularity
+**| of 16 bytes, so that free lists will hold successive runs that are
+**| 16 bytes greater than the next smaller run size. If we have 256 free
+**| lists of nonzero sized runs altogether, then the largest run that can
+**| be cached is 4096, or 4K (since 4096 == 16 * 256). A larger run that
+**| gets freed will go in to the free hunk list (or back to the heap).
+|*/
+#define morkZone_kRoundBits 4 /* bits to round-up size for free lists */
+#define morkZone_kRoundSize (1 << morkZone_kRoundBits)
+#define morkZone_kRoundAdd ((1 << morkZone_kRoundBits) - 1)
+#define morkZone_kRoundMask (~((mork_ip)morkZone_kRoundAdd))
+
+#define morkZone_kBuckets 256 /* number of distinct free lists */
+
+/*| kMaxCachedRun: the largest run that will be stored inside a free
+**| list of old zapped runs. A run larger than this cannot be put in
+**| a free list, and must be allocated from the heap at need, and put
+**| into the free hunk list when discarded.
+|*/
+#define morkZone_kMaxCachedRun (morkZone_kBuckets * morkZone_kRoundSize)
+
+#define morkDerived_kZone /*i*/ 0x5A6E /* ascii 'Zn' */
+
+/*| morkZone: a pooled memory allocator like an NSPR arena. The term 'zone'
+**| is roughly synonymous with 'heap'. I avoid calling this class a "heap"
+**| to avoid any confusion with nsIMdbHeap, and I avoid calling this class
+**| an arean to avoid confusion with NSPR usage.
+|*/
+class morkZone : public morkNode, public nsIMdbHeap {
+ // public: // slots inherited from morkNode (meant to inform only)
+ // nsIMdbHeap* mNode_Heap;
+
+ // mork_base mNode_Base; // must equal morkBase_kNode
+ // mork_derived mNode_Derived; // depends on specific node subclass
+
+ // mork_access mNode_Access; // kOpen, kClosing, kShut, or kDead
+ // mork_usage mNode_Usage; // kHeap, kStack, kMember, kGlobal, kNone
+ // mork_able mNode_Mutable; // can this node be modified?
+ // mork_load mNode_Load; // is this node clean or dirty?
+
+ // mork_uses mNode_Uses; // refcount for strong refs
+ // mork_refs mNode_Refs; // refcount for strong refs + weak refs
+
+ public: // state is public because the entire Mork system is private
+ nsIMdbHeap* mZone_Heap; // strong ref to heap allocating all space
+
+ mork_size mZone_HeapVolume; // total bytes allocated from heap
+ mork_size mZone_BlockVolume; // total bytes in all zone blocks
+ mork_size mZone_RunVolume; // total bytes in all zone runs
+ mork_size mZone_ChipVolume; // total bytes in all zone chips
+
+ mork_size mZone_FreeOldRunVolume; // total bytes in all used hunks
+
+ mork_count mZone_HunkCount; // total number of used hunks
+ mork_count mZone_FreeOldRunCount; // total free old runs
+
+ morkHunk* mZone_HunkList; // linked list of all used hunks
+ morkRun* mZone_FreeOldRunList; // linked list of free old runs
+
+ // note mZone_At is a byte pointer for single byte address arithmetic:
+ mork_u1* mZone_At; // current position in most recent hunk
+ mork_size mZone_AtSize; // number of bytes remaining in this hunk
+
+ // kBuckets+1 so indexes zero through kBuckets are all okay to use:
+
+ morkRun* mZone_FreeRuns[morkZone_kBuckets + 1];
+ // Each piece of memory stored in list mZone_FreeRuns[ i ] has an
+ // allocation size equal to sizeof(morkRun) + (i * kRoundSize), so
+ // that callers can be given a piece of memory with (i * kRoundSize)
+ // bytes of writeable space while reserving the first sizeof(morkRun)
+ // bytes to keep track of size information for later re-use. Note
+ // that mZone_FreeRuns[ 0 ] is unused because no run will be zero
+ // bytes in size (and morkZone plans to complain about zero sizes).
+
+ protected: // zone utilities
+ mork_size zone_grow_at(morkEnv* ev, mork_size inNeededSize);
+
+ void* zone_new_chip(morkEnv* ev, mdb_size inSize); // alloc
+ morkHunk* zone_new_hunk(morkEnv* ev, mdb_size inRunSize); // alloc
+
+ // { ===== begin nsIMdbHeap methods =====
+ public:
+ NS_IMETHOD Alloc(
+ nsIMdbEnv* ev, // allocate a piece of memory
+ mdb_size inSize, // requested size of new memory block
+ void** outBlock) override; // memory block of inSize bytes, or nil
+
+ NS_IMETHOD Free(nsIMdbEnv* ev, // free block allocated earlier by Alloc()
+ void* inBlock) override;
+
+ virtual size_t GetUsedSize() override { return mZone_Heap->GetUsedSize(); }
+ // } ===== end nsIMdbHeap methods =====
+
+ // { ===== begin morkNode interface =====
+ public: // morkNode virtual methods
+ virtual void CloseMorkNode(morkEnv* ev) override; // CloseZone() only if open
+ virtual ~morkZone(); // assert that CloseMap() executed earlier
+
+ public: // morkMap construction & destruction
+ morkZone(morkEnv* ev, const morkUsage& inUsage, nsIMdbHeap* ioNodeHeap,
+ nsIMdbHeap* ioZoneHeap);
+
+ void CloseZone(morkEnv* ev); // called by CloseMorkNode()
+
+ public: // dynamic type identification
+ mork_bool IsZone() const {
+ return IsNode() && mNode_Derived == morkDerived_kZone;
+ }
+ // } ===== end morkNode methods =====
+
+ // { ===== begin morkZone methods =====
+ public: // chips do not know how big they are...
+ void* ZoneNewChip(morkEnv* ev, mdb_size inSize); // alloc
+
+ public: // ...but runs do indeed know how big they are
+ void* ZoneNewRun(morkEnv* ev, mdb_size inSize); // alloc
+ void ZoneZapRun(morkEnv* ev, void* ioRunBody); // free
+ void* ZoneGrowRun(morkEnv* ev, void* ioRunBody, mdb_size inSize); // realloc
+
+ // } ===== end morkZone methods =====
+
+ public: // typing & errors
+ static void NonZoneTypeError(morkEnv* ev);
+ static void NilZoneHeapError(morkEnv* ev);
+ static void BadZoneTagError(morkEnv* ev);
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _MORKZONE_ */
diff --git a/comm/mailnews/db/mork/moz.build b/comm/mailnews/db/mork/moz.build
new file mode 100644
index 0000000000..4d97c3e562
--- /dev/null
+++ b/comm/mailnews/db/mork/moz.build
@@ -0,0 +1,68 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "mdb.h",
+ "nsIMdbFactoryFactory.h",
+]
+
+SOURCES += [
+ "morkArray.cpp",
+ "morkAtom.cpp",
+ "morkAtomMap.cpp",
+ "morkAtomSpace.cpp",
+ "morkBead.cpp",
+ "morkBlob.cpp",
+ "morkBuilder.cpp",
+ "morkCell.cpp",
+ "morkCellObject.cpp",
+ "morkCh.cpp",
+ "morkConfig.cpp",
+ "morkCursor.cpp",
+ "morkDeque.cpp",
+ "morkEnv.cpp",
+ "morkFactory.cpp",
+ "morkFile.cpp",
+ "morkHandle.cpp",
+ "morkIntMap.cpp",
+ "morkMap.cpp",
+ "morkNode.cpp",
+ "morkNodeMap.cpp",
+ "morkObject.cpp",
+ "morkParser.cpp",
+ "morkPool.cpp",
+ "morkPortTableCursor.cpp",
+ "morkProbeMap.cpp",
+ "morkRow.cpp",
+ "morkRowCellCursor.cpp",
+ "morkRowMap.cpp",
+ "morkRowObject.cpp",
+ "morkRowSpace.cpp",
+ "morkSink.cpp",
+ "morkSpace.cpp",
+ "morkStore.cpp",
+ "morkStream.cpp",
+ "morkTable.cpp",
+ "morkTableRowCursor.cpp",
+ "morkThumb.cpp",
+ "morkWriter.cpp",
+ "morkYarn.cpp",
+ "morkZone.cpp",
+ "nsMorkFactory.cpp",
+ "orkinHeap.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += ["morkSearchRowCursor.cpp"]
+
+Library("mork")
+FINAL_LIBRARY = "mail"
+# clang-cl complains about this.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-overloaded-virtual"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/db/mork/nsIMdbFactoryFactory.h b/comm/mailnews/db/mork/nsIMdbFactoryFactory.h
new file mode 100644
index 0000000000..06a362078d
--- /dev/null
+++ b/comm/mailnews/db/mork/nsIMdbFactoryFactory.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsIMdbFactoryFactory_h__
+#define nsIMdbFactoryFactory_h__
+
+#include "nsISupports.h"
+#include "nsIFactory.h"
+#include "nsIComponentManager.h"
+
+class nsIMdbFactory;
+
+// 2794D0B7-E740-47a4-91C0-3E4FCB95B806
+#define NS_IMDBFACTORYFACTORY_IID \
+ { \
+ 0x2794d0b7, 0xe740, 0x47a4, { \
+ 0x91, 0xc0, 0x3e, 0x4f, 0xcb, 0x95, 0xb8, 0x6 \
+ } \
+ }
+
+// because Mork doesn't support XPCOM, we have to wrap the mdb factory interface
+// with an interface that gives you an mdb factory.
+class nsIMdbFactoryService : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMDBFACTORYFACTORY_IID)
+ NS_IMETHOD GetMdbFactory(nsIMdbFactory** aFactory) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMdbFactoryService, NS_IMDBFACTORYFACTORY_IID)
+
+#endif
diff --git a/comm/mailnews/db/mork/nsMorkFactory.cpp b/comm/mailnews/db/mork/nsMorkFactory.cpp
new file mode 100644
index 0000000000..f1354699df
--- /dev/null
+++ b/comm/mailnews/db/mork/nsMorkFactory.cpp
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMorkFactory.h"
+
+NS_IMPL_ISUPPORTS(nsMorkFactoryService, nsIMdbFactoryService)
+
+NS_IMETHODIMP nsMorkFactoryService::GetMdbFactory(nsIMdbFactory** aFactory) {
+ if (!mMdbFactory) mMdbFactory = MakeMdbFactory();
+ NS_IF_ADDREF(*aFactory = mMdbFactory);
+ return *aFactory ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
diff --git a/comm/mailnews/db/mork/nsMorkFactory.h b/comm/mailnews/db/mork/nsMorkFactory.h
new file mode 100644
index 0000000000..582ffe3a83
--- /dev/null
+++ b/comm/mailnews/db/mork/nsMorkFactory.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMorkFactory_h__
+#define nsMorkFactory_h__
+
+#include "mozilla/ModuleUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIMdbFactoryFactory.h"
+#include "mdb.h"
+
+class nsMorkFactoryService final : public nsIMdbFactoryService {
+ public:
+ nsMorkFactoryService(){};
+ // nsISupports methods
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD GetMdbFactory(nsIMdbFactory** aFactory) override;
+
+ protected:
+ ~nsMorkFactoryService() {}
+ nsCOMPtr<nsIMdbFactory> mMdbFactory;
+};
+
+#endif
diff --git a/comm/mailnews/db/mork/orkinHeap.cpp b/comm/mailnews/db/mork/orkinHeap.cpp
new file mode 100644
index 0000000000..0bd2545a7b
--- /dev/null
+++ b/comm/mailnews/db/mork/orkinHeap.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+#ifndef _ORKINHEAP_
+# include "orkinHeap.h"
+#endif
+
+#ifndef _MORKENV_
+# include "morkEnv.h"
+#endif
+
+#include "nsIMemoryReporter.h"
+
+#include <stdlib.h>
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+orkinHeap::orkinHeap() // does nothing
+ : mUsedSize(0) {}
+
+/*virtual*/
+orkinHeap::~orkinHeap() // does nothing
+{}
+
+MOZ_DEFINE_MALLOC_SIZE_OF_ON_ALLOC(MorkSizeOfOnAlloc)
+MOZ_DEFINE_MALLOC_SIZE_OF_ON_FREE(MorkSizeOfOnFree)
+
+// { ===== begin nsIMdbHeap methods =====
+/*virtual*/ nsresult orkinHeap::Alloc(
+ nsIMdbEnv* mev, // allocate a piece of memory
+ mdb_size inSize, // requested size of new memory block
+ void** outBlock) // memory block of inSize bytes, or nil
+{
+ MORK_USED_1(mev);
+ nsresult outErr = NS_OK;
+ void* block = malloc(inSize);
+ if (!block)
+ outErr = morkEnv_kOutOfMemoryError;
+ else
+ mUsedSize += MorkSizeOfOnAlloc(block);
+
+ MORK_ASSERT(outBlock);
+ if (outBlock) *outBlock = block;
+ return outErr;
+}
+
+/*virtual*/ nsresult orkinHeap::Free(
+ nsIMdbEnv* mev, // free block allocated earlier by Alloc()
+ void* inBlock) {
+ MORK_USED_1(mev);
+ MORK_ASSERT(inBlock);
+ if (inBlock) {
+ mUsedSize -= MorkSizeOfOnFree(inBlock);
+ free(inBlock);
+ }
+ return NS_OK;
+}
+
+size_t orkinHeap::GetUsedSize() { return mUsedSize; }
+// } ===== end nsIMdbHeap methods =====
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
diff --git a/comm/mailnews/db/mork/orkinHeap.h b/comm/mailnews/db/mork/orkinHeap.h
new file mode 100644
index 0000000000..f431d6fe82
--- /dev/null
+++ b/comm/mailnews/db/mork/orkinHeap.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _ORKINHEAP_
+#define _ORKINHEAP_ 1
+
+#ifndef _MDB_
+# include "mdb.h"
+#endif
+
+#ifndef _MORK_
+# include "mork.h"
+#endif
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#define orkinHeap_kTag 0x68456150 /* ascii 'hEaP' */
+
+/*| orkinHeap:
+|*/
+class orkinHeap : public nsIMdbHeap { //
+ protected:
+ size_t mUsedSize;
+
+ public:
+ orkinHeap(); // does nothing
+ virtual ~orkinHeap(); // does nothing
+
+ private: // copying is not allowed
+ orkinHeap(const orkinHeap& other);
+ orkinHeap& operator=(const orkinHeap& other);
+
+ public:
+ // { ===== begin nsIMdbHeap methods =====
+ NS_IMETHOD Alloc(nsIMdbEnv* ev, // allocate a piece of memory
+ mdb_size inSize, // requested size of new memory block
+ void** outBlock); // memory block of inSize bytes, or nil
+
+ NS_IMETHOD Free(nsIMdbEnv* ev, // free block allocated earlier by Alloc()
+ void* inBlock);
+
+ virtual size_t GetUsedSize();
+ // } ===== end nsIMdbHeap methods =====
+};
+
+// 456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+
+#endif /* _ORKINHEAP_ */
diff --git a/comm/mailnews/db/moz.build b/comm/mailnews/db/moz.build
new file mode 100644
index 0000000000..deeb047fbc
--- /dev/null
+++ b/comm/mailnews/db/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "msgdb",
+ "gloda",
+]
diff --git a/comm/mailnews/db/msgdb/.eslintrc.js b/comm/mailnews/db/msgdb/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/db/msgdb/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/db/msgdb/moz.build b/comm/mailnews/db/msgdb/moz.build
new file mode 100644
index 0000000000..a49689ab64
--- /dev/null
+++ b/comm/mailnews/db/msgdb/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/db/msgdb/public/moz.build b/comm/mailnews/db/msgdb/public/moz.build
new file mode 100644
index 0000000000..5bed71ce9c
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIDBChangeAnnouncer.idl",
+ "nsIDBChangeListener.idl",
+ "nsIDBFolderInfo.idl",
+ "nsIMsgDatabase.idl",
+ "nsIMsgOfflineImapOperation.idl",
+ "nsINewsDatabase.idl",
+]
+
+XPIDL_MODULE = "msgdb"
+
+EXPORTS += [
+ "nsDBFolderInfo.h",
+ "nsImapMailDatabase.h",
+ "nsMailDatabase.h",
+ "nsMsgDatabase.h",
+ "nsMsgHdr.h",
+ "nsMsgThread.h",
+ "nsNewsDatabase.h",
+]
diff --git a/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h b/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h
new file mode 100644
index 0000000000..5b1150e4e7
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/* This class encapsulates the global information about a folder stored in the
+ summary file.
+*/
+#ifndef _nsDBFolderInfo_H
+#define _nsDBFolderInfo_H
+
+#include "mozilla/MemoryReporting.h"
+#include "nsString.h"
+#include "MailNewsTypes.h"
+#include "mdb.h"
+#include "nsTArray.h"
+#include "nsIDBFolderInfo.h"
+#include <time.h>
+
+class nsMsgDatabase;
+
+// again, this could inherit from nsISupports, but I don't see the need as of
+// yet. I'm not sure it needs to be ref-counted (but I think it does).
+
+// I think these getters and setters really need to go through mdb and not rely
+// on the object caching the values. If this somehow turns out to be
+// prohibitively expensive, we can invent some sort of dirty mechanism, but I
+// think it turns out that these values will be cached by the MSG_FolderInfo's
+// anyway.
+class nsDBFolderInfo : public nsIDBFolderInfo {
+ public:
+ friend class nsMsgDatabase;
+
+ explicit nsDBFolderInfo(nsMsgDatabase* mdb);
+
+ NS_DECL_ISUPPORTS
+ // interface methods.
+ NS_DECL_NSIDBFOLDERINFO
+ // create the appropriate table and row in a new db.
+ nsresult AddToNewMDB();
+ // accessor methods.
+
+ bool TestFlag(int32_t flags);
+ int16_t GetIMAPHierarchySeparator();
+ void SetIMAPHierarchySeparator(int16_t hierarchyDelimiter);
+ void ChangeImapTotalPendingMessages(int32_t delta);
+ void ChangeImapUnreadPendingMessages(int32_t delta);
+
+ nsresult InitFromExistingDB();
+ // get and set arbitrary property, aka row cell value.
+ nsresult SetPropertyWithToken(mdb_token aProperty,
+ const nsAString& propertyStr);
+ nsresult SetUint32PropertyWithToken(mdb_token aProperty,
+ uint32_t propertyValue);
+ nsresult SetInt64PropertyWithToken(mdb_token aProperty,
+ int64_t propertyValue);
+ nsresult SetInt32PropertyWithToken(mdb_token aProperty,
+ int32_t propertyValue);
+ nsresult GetPropertyWithToken(mdb_token aProperty, nsAString& propertyValue);
+ nsresult GetUint32PropertyWithToken(mdb_token aProperty,
+ uint32_t& propertyValue,
+ uint32_t defaultValue = 0);
+ nsresult GetInt32PropertyWithToken(mdb_token aProperty,
+ int32_t& propertyValue,
+ int32_t defaultValue = 0);
+ nsresult GetInt64PropertyWithToken(mdb_token aProperty,
+ int64_t& propertyValue,
+ int64_t defaultValue = 0);
+
+ nsTArray<nsMsgKey> m_lateredKeys; // list of latered messages
+
+ virtual size_t SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return m_lateredKeys.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ virtual size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ protected:
+ virtual ~nsDBFolderInfo();
+
+ // initialize from appropriate table and row in existing db.
+ nsresult InitMDBInfo();
+ nsresult LoadMemberVariables();
+
+ nsresult AdjustHighWater(nsMsgKey highWater, bool force);
+
+ void
+ ReleaseExternalReferences(); // let go of any references to other objects.
+
+ int64_t m_folderSize;
+ int64_t m_expungedBytes; // sum of size of deleted messages in folder
+ uint32_t m_folderDate;
+ nsMsgKey m_highWaterMessageKey; // largest news article number or imap uid
+ // whose header we've seen
+
+ // m_numUnreadMessages and m_numMessages can never be negative. 0 means 'no
+ // msgs'.
+ int32_t m_numUnreadMessages;
+ int32_t m_numMessages; // includes expunged and ignored messages
+
+ int32_t m_flags; // folder specific flags. This holds things like re-use
+ // thread pane,
+ // configured for off-line use, use default retrieval, purge article/header
+ // options
+
+ uint16_t m_version; // for upgrading...
+ int16_t m_IMAPHierarchySeparator; // imap path separator
+
+ // mail only (for now)
+
+ // IMAP only
+ int32_t m_ImapUidValidity;
+ int32_t m_totalPendingMessages;
+ int32_t m_unreadPendingMessages;
+
+ // news only (for now)
+ nsMsgKey
+ m_expiredMark; // Highest invalid article number in group - for expiring
+ // the db folder info will have to know what db and row it belongs to, since
+ // it is really just a wrapper around the singleton folder info row in the
+ // mdb.
+ nsMsgDatabase* m_mdb;
+ nsIMdbTable* m_mdbTable; // singleton table in db
+ nsIMdbRow* m_mdbRow; // singleton row in table;
+
+ bool m_mdbTokensInitialized;
+
+ mdb_token m_rowScopeToken;
+ mdb_token m_tableKindToken;
+ // tokens for the pre-set columns - we cache these for speed, which may be
+ // silly
+ mdb_token m_mailboxNameColumnToken;
+ mdb_token m_numMessagesColumnToken;
+ mdb_token m_numUnreadMessagesColumnToken;
+ mdb_token m_flagsColumnToken;
+ mdb_token m_folderSizeColumnToken;
+ mdb_token m_expungedBytesColumnToken;
+ mdb_token m_folderDateColumnToken;
+ mdb_token m_highWaterMessageKeyColumnToken;
+
+ mdb_token m_imapUidValidityColumnToken;
+ mdb_token m_totalPendingMessagesColumnToken;
+ mdb_token m_unreadPendingMessagesColumnToken;
+ mdb_token m_expiredMarkColumnToken;
+ mdb_token m_versionColumnToken;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl b/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl
new file mode 100644
index 0000000000..afdb06f8ca
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIDBChangeListener;
+interface nsIMsgDBHdr;
+
+[scriptable, uuid(22baf00b-939d-42c3-ac51-21d99dfa1f05)]
+interface nsIDBChangeAnnouncer : nsISupports {
+ void addListener(in nsIDBChangeListener listener);
+ void removeListener(in nsIDBChangeListener listener);
+
+ void notifyHdrChangeAll(in nsIMsgDBHdr aHdrChanged,
+ in unsigned long aOldFlags,
+ in unsigned long aNewFlags,
+ in nsIDBChangeListener instigator);
+
+ void notifyHdrAddedAll(in nsIMsgDBHdr aHdrAdded,
+ in nsMsgKey parentKey,
+ in long flags,
+ in nsIDBChangeListener instigator);
+
+ void notifyHdrDeletedAll(in nsIMsgDBHdr aHdrDeleted,
+ in nsMsgKey parentKey,
+ in long flags,
+ in nsIDBChangeListener instigator);
+
+ void notifyParentChangedAll(in nsMsgKey keyReparented,
+ in nsMsgKey oldParent,
+ in nsMsgKey newParent,
+ in nsIDBChangeListener instigator);
+
+ void notifyReadChanged(in nsIDBChangeListener instigator);
+
+ void notifyJunkScoreChanged(in nsIDBChangeListener aInstigator);
+
+ void notifyAnnouncerGoingAway();
+};
diff --git a/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl b/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl
new file mode 100644
index 0000000000..30c50db49c
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIDBChangeAnnouncer;
+interface nsIMsgDBHdr;
+interface nsIMsgDatabase;
+
+/**
+ * These callbacks are provided to allow listeners to the message database
+ * to update their status when changes occur.
+ */
+[scriptable, uuid(21c56d34-71b9-42bb-9606-331a6a5f8210)]
+
+interface nsIDBChangeListener : nsISupports {
+ /**
+ * Callback when message flags are changed.
+ *
+ * @param aHdrChanged The changed header.
+ * @param aOldFlags Message flags prior to change.
+ * @param aNewFlags Message flags after change.
+ * @param aInstigator Object that initiated the change.
+ */
+ void onHdrFlagsChanged(in nsIMsgDBHdr aHdrChanged, in unsigned long aOldFlags,
+ in unsigned long aNewFlags, in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback when message is marked as deleted.
+ *
+ * @param aHdrChanged The message header that is going to be deleted.
+ * @param aParentKey Key of parent.
+ * @param aFlags Flags that message has before delete.
+ * @param aInstigator Object that initiated the change. Can be null.
+ */
+ void onHdrDeleted(in nsIMsgDBHdr aHdrChanged, in nsMsgKey aParentKey, in long aFlags,
+ in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback when message is added.
+ *
+ * @param aHdrChanged The message header that is added.
+ * @param aParentKey Parent key of message.
+ * @param aFlags Flags that new message will have.
+ * @param aInstigator Object that initiated the change. Can be null.
+ */
+ void onHdrAdded(in nsIMsgDBHdr aHdrChanged, in nsMsgKey aParentKey, in long aFlags,
+ in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback when message parent is changed. Parent is changed when message is deleted or moved.
+ *
+ * @param aKeyChanged The message key that parent key was changed.
+ * @param oldParent Old parent key.
+ * @param newParent New parent key.
+ * @param aInstigator Object that initiated the change. Can be null.
+ */
+ void onParentChanged(in nsMsgKey aKeyChanged, in nsMsgKey oldParent, in nsMsgKey newParent,
+ in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback when announcer is going away. This is good place to release strong pointers to announcer.
+ *
+ * @param instigator Object that initiated the change. Can be null.
+ */
+ void onAnnouncerGoingAway(in nsIDBChangeAnnouncer instigator);
+
+ /**
+ * Callback when read flag is changed.
+ *
+ * @param aInstigator Object that initiated the change. Can be null.
+ */
+ void onReadChanged(in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback used in case when "junkscore" property is changed.
+ *
+ * @param aInstigator Object that initiated the change. Can be null.
+ */
+ void onJunkScoreChanged(in nsIDBChangeListener aInstigator);
+
+ /**
+ * Callback used in the general case where any field may have changed.
+ * OnHdrPropertyChanged is called twice per change. On the first call, aPreChange
+ * is true, and aStatus is undefined. OnHdrPropertyChanged saves any required status in aStatus
+ * (such as a filter match). The calling function stores the value of aStatus, changes the
+ * header aHdrToChange, then calls OnHdrPropertyChanged again with aPreChange false. On this
+ * second call, the stored value of aStatus is provided, so that any changes may be noted.
+ *
+ * @param aHdrToChange the message header that is changing.
+ * @param aPreChange true on first call before change, false on second call after change
+ * @param aStatus storage location provided by calling routine for status
+ * @param aInstigator object that initiated the change
+ */
+ void onHdrPropertyChanged(in nsIMsgDBHdr aHdrToChange,
+ in AUTF8String property,
+ in boolean aPreChange,
+ inout uint32_t aStatus,
+ in nsIDBChangeListener aInstigator);
+
+ /**
+ * Generic notification for extensibility. Common events should be documented
+ * here so we have a hope of keeping the documentation up to date.
+ * Current events are:
+ * "DBOpened" - When a pending listener becomes real. This can happen when
+ * the existing db is force closed and a new one opened. Only
+ * registered pending listeners are notified.
+ *
+ * @param aDB the db for this event.
+ * @param aEvent type of event.
+ *
+ */
+ void onEvent(in nsIMsgDatabase aDB, in string aEvent);
+};
diff --git a/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl b/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl
new file mode 100644
index 0000000000..cb41041268
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(a72dab4b-b3bd-471e-9a38-1b242b385459)]
+interface nsIDBFolderInfo : nsISupports {
+ attribute long flags;
+
+ /**
+ * Or's aFlags into flags.
+ *
+ * @param - the flags(s) to set
+ *
+ * @return - the resulting flags.
+ */
+ long orFlags(in long aFlags);
+ /**
+ * And's aFlags with flags, set flags to the result
+ *
+ * @param the flags(s) to AND
+ *
+ * @return the resulting flags.
+ */
+ long andFlags(in long aFlags);
+
+ /**
+ * Allows us to keep track of the highwater mark
+ *
+ * @param aNewKey If larger than the current highwater
+ * mark, sets the highwater mark to aNewKey.
+ */
+ void onKeyAdded(in nsMsgKey aNewKey);
+
+ attribute nsMsgKey highWater;
+ attribute nsMsgKey expiredMark;
+ attribute long long folderSize;
+ attribute unsigned long folderDate;
+ void changeNumUnreadMessages(in long aDelta);
+ void changeNumMessages(in long aDelta);
+
+ // numUnreadMessages and numMessages will never return negative numbers. 0 means 'no msgs'.
+ attribute long numUnreadMessages;
+ attribute long numMessages;
+
+ attribute long long expungedBytes;
+ attribute long imapUidValidity;
+ attribute unsigned long version;
+ attribute long imapTotalPendingMessages;
+ attribute long imapUnreadPendingMessages;
+
+ attribute nsMsgViewTypeValue viewType;
+ attribute nsMsgViewFlagsTypeValue viewFlags;
+ attribute nsMsgViewSortTypeValue sortType;
+ attribute nsMsgViewSortOrderValue sortOrder;
+
+ void changeExpungedBytes(in long aDelta);
+
+ /**
+ * Gets a string property from the folder. Also used for URIs, hence the AUTF8String type.
+ *
+ * @param propertyName The name of the property for the value to retrieve.
+ */
+ AUTF8String getCharProperty(in string propertyName);
+
+ /**
+ * Sets a string property from the folder. Also used for URIs, hence the AUTF8String type.
+ *
+ * @param propertyName The name of the property for which to set a value
+ * @param propertyValue The new value of the property.
+ */
+ void setCharProperty(in string aPropertyName, in AUTF8String aPropertyValue);
+ void setUint32Property(in string propertyName, in unsigned long propertyValue);
+ void setInt64Property(in string propertyName, in long long propertyValue);
+ unsigned long getUint32Property(in string propertyName, in unsigned long defaultValue);
+ long long getInt64Property(in string propertyName, in long long defaultValue);
+ boolean getBooleanProperty(in string propertyName, in boolean defaultValue);
+ void setBooleanProperty(in string propertyName, in boolean aPropertyValue);
+ nsIDBFolderInfo GetTransferInfo();
+ void initFromTransferInfo(in nsIDBFolderInfo transferInfo);
+
+ attribute AString locale;
+ attribute AString mailboxName;
+
+
+ AString getProperty(in string propertyName);
+ void setProperty(in string propertyName, in AString propertyStr);
+
+ attribute string knownArtsSet;
+ attribute ACString folderName;
+};
diff --git a/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl b/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl
new file mode 100644
index 0000000000..e54c938c15
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl
@@ -0,0 +1,506 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * @defgroup msgdb Mailnews message database
+ * This module is the access point to locally-stored databases.
+ *
+ * These databases are stored in .msf files. Each file contains useful cached
+ * information, like the message id or references, as well as the cc header or
+ * tag information. This cached information is encapsulated in nsIMsgDBHdr.
+ *
+ * Also included is threading information, mostly encapsulated in nsIMsgThread.
+ * The final component is the database folder info, which contains information
+ * on the view and basic information also stored in the folder cache such as the
+ * name or most recent update.
+ *
+ * What this module does not do is access individual messages. Access is
+ * strictly controlled by the nsIMsgFolder objects and their backends.
+ * @{
+ */
+#include "nsISupports.idl"
+#include "nsIDBChangeAnnouncer.idl"
+
+interface nsIMsgDatabase;
+interface nsIDBChangeListener;
+interface nsIMsgDBHdr;
+interface nsIMsgEnumerator;
+interface nsIMsgThread;
+interface nsIMsgThreadEnumerator;
+interface nsIDBFolderInfo;
+interface nsIMsgOfflineImapOperation;
+interface nsIMsgFolder;
+interface nsIFile;
+interface nsIMsgSearchTerm;
+
+typedef unsigned long nsMsgRetainByPreference;
+
+
+[scriptable, uuid(fe8b7cec-eec8-4bcd-82ff-d8bb23cef3da)]
+
+interface nsIMsgRetentionSettings : nsISupports
+{
+ const unsigned long nsMsgRetainAll = 1;
+ const unsigned long nsMsgRetainByAge = 2;
+ const unsigned long nsMsgRetainByNumHeaders = 3;
+
+ attribute boolean useServerDefaults;
+ attribute nsMsgRetainByPreference retainByPreference;
+ attribute unsigned long daysToKeepHdrs;
+ attribute unsigned long numHeadersToKeep;
+
+ // this is for keeping offline bodies.
+ attribute boolean cleanupBodiesByDays;
+ attribute unsigned long daysToKeepBodies;
+
+ /**
+ * Should retention settings be applied to flagged/starred messages?
+ * If false, flagged messages are never automatically deleted.
+ */
+ attribute boolean applyToFlaggedMessages;
+};
+
+[scriptable, uuid(86a9da90-14f1-11d5-a5c0-0060b0fc04b7)]
+interface nsIMsgDownloadSettings : nsISupports
+{
+ attribute boolean useServerDefaults;
+ attribute boolean downloadByDate;
+ attribute boolean downloadUnreadOnly;
+ attribute unsigned long ageLimitOfMsgsToDownload;
+};
+
+typedef long nsMsgDBCommit;
+
+[scriptable, uuid(15431853-e448-45dc-8978-9958bf74d9b7)]
+interface nsMsgDBCommitType : nsISupports
+{
+ const long kLargeCommit = 1;
+ const long kSessionCommit = 2;
+ const long kCompressCommit = 3;
+};
+
+/**
+ * A service to open mail databases and manipulate listeners automatically.
+ *
+ * The contract ID for this component is
+ * <tt>\@mozilla.org/msgDatabase/msgDBService;1</tt>.
+ */
+[scriptable, uuid(4cbbf024-3760-402d-89f3-6ababafeb07d)]
+interface nsIMsgDBService : nsISupports
+{
+ /**
+ * Opens a database for a given folder.
+ *
+ * This method is preferred over nsIMsgDBService::openMailDBFromFile if the
+ * caller has an actual nsIMsgFolder around. If the database detects that it
+ * is unreadable or out of date (using nsIMsgDatabase::outOfDate) it will
+ * destroy itself and prepare to be rebuilt, unless aLeaveInvalidDB is true.
+ *
+ * If one gets a NS_MSG_ERROR_FOLDER_SUMMARY_MISSING message, then one
+ * should call nsIMsgDBService::createNewDB to create the new database.
+ *
+ * @param aFolder The folder whose database should be returned.
+ * @param aLeaveInvalidDB Whether or not the database should be deleted if it
+ * is invalid.
+ * @return A new nsIMsgDatabase object representing the folder
+ * database that was opened.
+ * @exception NS_ERROR_FILE_NOT_FOUND
+ * The file could not be created.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE
+ * The database is present (and was opened), but the
+ * summary file is out of date.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING
+ * The database is present, but the summary file is
+ * missing.
+ * @see nsIMsgDatabase::Open
+ * @see nsIMsgDBService::createNewDB
+ */
+ nsIMsgDatabase openFolderDB(in nsIMsgFolder aFolder,
+ in boolean aLeaveInvalidDB);
+
+ /**
+ * Creates a new database for the given folder.
+ *
+ * If the database already exists, it will return the database, emit a
+ * warning, but not fully initialize it. For this reason, it should only be
+ * used when it is known that the database does not exist, such as when
+ * nsIMsgDBService::openFolderDB throws an error.
+ *
+ * @see nsIMsgDBService::openFolderDB
+ */
+ nsIMsgDatabase createNewDB(in nsIMsgFolder aFolder);
+
+ /**
+ * Opens or creates a database for a given file.
+ *
+ * This method should only be used if the caller does not have a folder
+ * instance, because the resulting db and message headers retrieved from the
+ * database would not know their owning folder, which limits their usefulness.
+ * For this reason, one should use nsIMsgDBService::openFolderDB instead
+ * except under special circumstances.
+ *
+ * Unlike nsIMsgDBService::openFolderDB, there is no corresponding method to
+ * create a new database if opening the database failed. However, this method
+ * will never throw NS_MSG_ERROR_FOLDER_SUMMARY_MISSING, so no corresponding
+ * method is needed.
+ *
+ * @param aFile The file for which the database should be returned.
+ * @param aFolder Folder the db corresponds to (may be null)
+ * @param aCreate Whether or not the file should be created.
+ * @param aLeaveInvalidDB Whether or not the database should be deleted if it
+ * is invalid.
+ * @return A new nsIMsgDatabase object encapsulating the file
+ * passed in.
+ * @exception NS_ERROR_FILE_NOT_FOUND
+ * The file could not be created.
+ * @see nsIMsgDBService::openFolderDB
+ * @see nsIMsgDatabase::Open
+ */
+ nsIMsgDatabase openMailDBFromFile(in nsIFile aFile,
+ in nsIMsgFolder aFolder,
+ in boolean aCreate,
+ in boolean aLeaveInvalidDB);
+ /**
+ * Adds the given listener to the listener set for the folder.
+ *
+ * Since the message database will likely be opened and closed many times, by
+ * registering using this method, one will be guaranteed to see all subsequent
+ * modifications. This will also add the listener to the database if it is
+ * already opened.
+ *
+ * @param aFolder The folder to add a listener to.
+ * @param aListener The listener to add the folder to.
+ */
+ void registerPendingListener(in nsIMsgFolder aFolder,
+ in nsIDBChangeListener aListener);
+ /**
+ * Removes the listener from all folder listener sets.
+ *
+ * @param aListener The listener to remove.
+ * @exception NS_ERROR_FAILURE
+ * The listener is not registered.
+ */
+ void unregisterPendingListener(in nsIDBChangeListener aListener);
+
+ /**
+ * Get the db for a folder, if already open.
+ *
+ * @param aFolder The folder to get the cached (open) db for.
+ *
+ * @returns null if the db isn't open, otherwise the db.
+ */
+ nsIMsgDatabase cachedDBForFolder(in nsIMsgFolder aFolder);
+
+ /**
+ * Close the db for a folder, if already open.
+ *
+ * @param aFolder The folder to close the cached (open) db for.
+ */
+ void forceFolderDBClosed(in nsIMsgFolder aFolder);
+
+ /// an enumerator to iterate over the open dbs.
+ readonly attribute Array<nsIMsgDatabase> openDBs;
+};
+
+[scriptable, uuid(b64e66f8-4717-423a-be42-482658fb2199)]
+interface nsIMsgDatabase : nsIDBChangeAnnouncer {
+ void close(in boolean aForceCommit);
+
+ void commit(in nsMsgDBCommit commitType);
+ // Force closed is evil, and we should see if we can do without it.
+ // In 4.x, it was mainly used to remove corrupted databases.
+ void forceClosed();
+ void clearCachedHdrs();
+ void resetHdrCacheSize(in unsigned long size);
+
+ readonly attribute nsIDBFolderInfo dBFolderInfo;
+
+ /// Size of the database file in bytes.
+ readonly attribute long long databaseSize;
+
+ /// Folder this db was opened on.
+ readonly attribute nsIMsgFolder folder;
+
+ /**
+ * This is used when deciding which db's to close to free up memory
+ * and other resources in an LRU manner. It doesn't track every operation
+ * on every object from the db, but high level things like open, commit,
+ * and perhaps some of the list methods. Commit should be a proxy for all
+ * the mutation methods.
+ *
+ * I'm allowing clients to set the last use time as well, so that
+ * nsIMsgFolder.msgDatabase can set the last use time.
+ */
+ attribute PRTime lastUseTime;
+
+ // get a message header for the given key. Caller must release()!
+
+ nsIMsgDBHdr getMsgHdrForKey(in nsMsgKey key);
+ nsIMsgDBHdr getMsgHdrForMessageID(in string messageID);
+
+ /**
+ * Get a message header for a Gmail message with the given X-GM-MSGID.
+ * @param {string} aGmailMessageID - The ID of the message to find.
+ *
+ * @returns the message, or null if not found (without throwing an error).
+ */
+ nsIMsgDBHdr getMsgHdrForGMMsgID(in string aGmailMessageID);
+ //Returns whether or not this database contains the given key
+ boolean containsKey(in nsMsgKey key);
+
+/**
+ * Must call AddNewHdrToDB after creating. The idea is that you create
+ * a new header, fill in its properties, and then call AddNewHdrToDB.
+ * AddNewHdrToDB will send notifications to any listeners.
+ *
+ * @param aKey msgKey for the new header. If aKey is nsMsgKey_None,
+ * we will auto-assign a new key.
+ */
+ nsIMsgDBHdr createNewHdr(in nsMsgKey aKey);
+
+ void addNewHdrToDB(in nsIMsgDBHdr newHdr, in boolean notify);
+
+ nsIMsgDBHdr copyHdrFromExistingHdr(in nsMsgKey key, in nsIMsgDBHdr existingHdr, in boolean addHdrToDB);
+
+ /**
+ * Returns all message keys stored in the database.
+ * Keys are returned in the order as stored in the database.
+ * The caller should sort them if it needs to.
+ */
+ Array<nsMsgKey> listAllKeys();
+
+ nsIMsgEnumerator enumerateMessages();
+ nsIMsgEnumerator reverseEnumerateMessages();
+ nsIMsgThreadEnumerator enumerateThreads();
+
+ /**
+ * Get an enumerator of messages matching the passed-in search terms.
+ *
+ * @param searchTerms Array of search terms to evaluate.
+ * @param reverse Start at the end, defaults to false.
+ *
+ * @returns An enumerator to iterate over matching messages.
+ */
+ nsIMsgEnumerator getFilterEnumerator(in Array<nsIMsgSearchTerm> searchTerms,
+ [optional] in boolean reverse);
+
+ // count the total and unread msgs, and adjust global count if needed
+ void syncCounts();
+
+ nsIMsgThread getThreadContainingMsgHdr(in nsIMsgDBHdr msgHdr) ;
+
+ // helpers for user command functions like delete, mark read, etc.
+
+ void markHdrRead(in nsIMsgDBHdr msgHdr, in boolean bRead,
+ in nsIDBChangeListener instigator);
+
+ void markHdrReplied(in nsIMsgDBHdr msgHdr, in boolean bReplied,
+ in nsIDBChangeListener instigator);
+
+ void markHdrMarked(in nsIMsgDBHdr msgHdr, in boolean mark,
+ in nsIDBChangeListener instigator);
+ /**
+ * Remove the new status from a message.
+ *
+ * @param aMsgHdr The database reference header for the message
+ * @param aInstigator Reference to original calling object
+ */
+ void markHdrNotNew(in nsIMsgDBHdr aMsgHdr,
+ in nsIDBChangeListener aInstigator);
+
+ // MDN support
+ void markMDNNeeded(in nsMsgKey key, in boolean bNeeded,
+ in nsIDBChangeListener instigator);
+
+ void markMDNSent(in nsMsgKey key, in boolean bNeeded,
+ in nsIDBChangeListener instigator);
+ boolean isMDNSent(in nsMsgKey key);
+
+ void markRead(in nsMsgKey key, in boolean bRead,
+ in nsIDBChangeListener instigator);
+
+ void markReplied(in nsMsgKey key, in boolean bReplied,
+ in nsIDBChangeListener instigator);
+
+ void markForwarded(in nsMsgKey key, in boolean bForwarded,
+ in nsIDBChangeListener instigator);
+
+ void markRedirected(in nsMsgKey key, in boolean bRedirected,
+ in nsIDBChangeListener instigator);
+
+ void markHasAttachments(in nsMsgKey key, in boolean bHasAttachments,
+ in nsIDBChangeListener instigator);
+
+ Array<nsMsgKey> markThreadRead(in nsIMsgThread thread, in nsIDBChangeListener instigator);
+
+ /// Mark the specified thread ignored.
+ void markThreadIgnored(in nsIMsgThread thread, in nsMsgKey threadKey,
+ in boolean bIgnored,
+ in nsIDBChangeListener instigator);
+
+ /// Mark the specified thread watched.
+ void markThreadWatched(in nsIMsgThread thread, in nsMsgKey threadKey,
+ in boolean bWatched,
+ in nsIDBChangeListener instigator);
+
+ /// Mark the specified subthread ignored.
+ void markHeaderKilled(in nsIMsgDBHdr msg, in boolean bIgnored,
+ in nsIDBChangeListener instigator);
+
+ /// Is the message read.
+ boolean isRead(in nsMsgKey key);
+ /// Is the message part of an ignored thread.
+ boolean isIgnored(in nsMsgKey key);
+ /// Is the message part of a watched thread.
+ boolean isWatched(in nsMsgKey key);
+ /// Is the message flagged/starred.
+ boolean isMarked(in nsMsgKey key);
+ /// Does the message have attachments.
+ boolean hasAttachments(in nsMsgKey key);
+
+ Array<nsMsgKey> markAllRead();
+
+ void deleteMessages(in Array<nsMsgKey> nsMsgKeys,
+ in nsIDBChangeListener instigator);
+ void deleteMessage(in nsMsgKey key,
+ in nsIDBChangeListener instigator,
+ in boolean commit);
+ void deleteHeader(in nsIMsgDBHdr msgHdr, in nsIDBChangeListener instigator,
+ in boolean commit, in boolean notify);
+
+ /// Lower level routine that doesn't remove hdr from thread or adjust counts.
+ void removeHeaderMdbRow(in nsIMsgDBHdr msgHdr);
+
+ void undoDelete(in nsIMsgDBHdr msgHdr);
+
+ void markMarked(in nsMsgKey key, in boolean mark,
+ in nsIDBChangeListener instigator);
+ void markOffline(in nsMsgKey key, in boolean offline,
+ in nsIDBChangeListener instigator);
+ void setStringProperty(in nsMsgKey aKey, in string aProperty, in AUTF8String aValue);
+ /**
+ * Set the value of a string property in a message header
+ *
+ * @param msgHdr Header of the message whose property will be changed
+ * @param aProperty the property to change
+ * @param aValue new value for the property
+ */
+ void setStringPropertyByHdr(in nsIMsgDBHdr msgHdr, in string aProperty, in AUTF8String aValue);
+
+ /**
+ * Set the value of a uint32 property in a message header.
+ *
+ * @param aMsgHdr header of the message whose property will be changed
+ * @param aProperty the property to change
+ * @param aValue new value for the property
+ */
+ void setUint32PropertyByHdr(in nsIMsgDBHdr aMsgHdr,
+ in string aProperty, in unsigned long aValue);
+
+ void markImapDeleted(in nsMsgKey key, in boolean deleted,
+ in nsIDBChangeListener instigator);
+
+ readonly attribute nsMsgKey firstNew;
+
+ attribute nsIMsgRetentionSettings msgRetentionSettings;
+ // Purge unwanted message headers and/or bodies. If deleteViaFolder is
+ // true, we'll call nsIMsgFolder::DeleteMessages to delete the messages.
+ // Otherwise, we'll just delete them from the db.
+ void applyRetentionSettings(in nsIMsgRetentionSettings aMsgRetentionSettings,
+ in boolean aDeleteViaFolder);
+
+ attribute nsIMsgDownloadSettings msgDownloadSettings;
+
+ boolean hasNew();
+ void clearNewList(in boolean notify);
+ void addToNewList(in nsMsgKey key);
+
+ // Used mainly to force the timestamp of a local mail folder db to
+ // match the time stamp of the corresponding berkeley mail folder,
+ // but also useful to tell the summary to mark itself invalid
+ // Also, if a local folder is being reparsed, summary will be invalid
+ // until the reparsing is done.
+ attribute boolean summaryValid;
+
+ Array<nsMsgKey> listAllOfflineMsgs();
+
+ void setAttributeOnPendingHdr(in nsIMsgDBHdr pendingHdr, in string property,
+ in string propertyVal);
+
+ void setUint32AttributeOnPendingHdr(in nsIMsgDBHdr pendingHdr, in string property,
+ in unsigned long propertyVal);
+
+ /**
+ * Sets a pending 64 bit attribute, which tells the DB that when a message
+ * which looks like the pendingHdr (e.g., same message-id) is added to the
+ * db, set the passed in property and value on the new header. This is
+ * usually because we've copied an imap message to a different folder, and
+ * want to carry forward attributes from the original message to the copy,
+ * but don't have the message hdr for the copy yet so we can't set
+ * attributes directly.
+ *
+ * @param aPendingHdr usually the source of the copy.
+ * @param aProperty name of property to set.
+ * @param aPropertyVal 64 bit value of property to set.
+ */
+ void setUint64AttributeOnPendingHdr(in nsIMsgDBHdr aPendingHdr,
+ in string aProperty,
+ in unsigned long long aPropertyVal);
+
+ /**
+ * Given a message header with its message-id set, update any pending
+ * attributes on the header.
+ *
+ * @param aNewHdr a new header that may have pending attributes.
+ */
+ void updatePendingAttributes(in nsIMsgDBHdr aNewHdr);
+
+ readonly attribute nsMsgKey lowWaterArticleNum;
+ readonly attribute nsMsgKey highWaterArticleNum;
+ attribute nsMsgKey nextPseudoMsgKey; //for undo-redo of move pop->imap
+ readonly attribute nsMsgKey nextFakeOfflineMsgKey; // for saving "fake" offline msg hdrs
+ // for sorting
+ Array<octet> createCollationKey(in AString sourceString);
+ long compareCollationKeys(in Array<octet> key1, in Array<octet> key2);
+
+ // when creating a view, the default sort order and view flags
+ // use these for the default. (this allows news to override, so that
+ // news can be threaded by default)
+ readonly attribute nsMsgViewFlagsTypeValue defaultViewFlags;
+ readonly attribute nsMsgViewSortTypeValue defaultSortType;
+ readonly attribute nsMsgViewSortOrderValue defaultSortOrder;
+
+ // for msg hdr hash table allocation. controllable by caller to improve folder loading performance.
+ attribute unsigned long msgHdrCacheSize;
+
+ /**
+ * The list of messages currently in the NEW state.
+ */
+ Array<nsMsgKey> getNewList();
+
+ // These are used for caching search hits in a db, to speed up saved search folders.
+ nsIMsgEnumerator getCachedHits(in AUTF8String aSearchFolderUri);
+
+ /**
+ * Update search cache to ensure it contains aNewHits.
+ *
+ * @param aSearchFolderUri the target folder.
+ * @param aNewHits sorted list of new message keys.
+ * @returns list of keys of messages removed from cache.
+ */
+ Array<nsMsgKey> refreshCache(in AUTF8String aSearchFolderUri, in Array<nsMsgKey> aNewHits);
+ void updateHdrInCache(in AUTF8String aSearchFolderUri, in nsIMsgDBHdr aHdr, in boolean aAdd);
+ boolean hdrIsInCache(in AUTF8String aSearchFolderUri, in nsIMsgDBHdr aHdr);
+};
+
+[scriptable, uuid(7f98410c-41b7-4a55-8e0c-02107e7f4c0f)]
+interface nsIMsgOfflineOpsDatabase : nsIMsgDatabase {
+ // Has to be in nsMailDatabase, since local folders can be move destinations.
+
+ nsIMsgOfflineImapOperation getOfflineOpForKey(in nsMsgKey messageKey, in boolean create);
+ void removeOfflineOp(in nsIMsgOfflineImapOperation op);
+ Array<nsMsgKey> listAllOfflineOpIds();
+ Array<nsMsgKey> listAllOfflineDeletes();
+};
diff --git a/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl b/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl
new file mode 100644
index 0000000000..bc2aecfa28
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl
@@ -0,0 +1,50 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+#include "nsIImapUrl.idl" // for imapMessageFlagsType
+
+typedef long nsOfflineImapOperationType;
+
+[scriptable, uuid(b5229a55-22bb-444b-be92-13d719353828)]
+
+interface nsIMsgOfflineImapOperation : nsISupports
+{
+// type of stored imap operations
+ const long kFlagsChanged = 0x1;
+ const long kMsgMoved = 0x2;
+ const long kMsgCopy = 0x4;
+ const long kMoveResult = 0x8;
+ const long kAppendDraft = 0x10;
+ const long kAddedHeader = 0x20;
+ const long kDeletedMsg = 0x40;
+ const long kMsgMarkedDeleted = 0x80;
+ const long kAppendTemplate = 0x100;
+ const long kDeleteAllMsgs = 0x200;
+ const long kAddKeywords = 0x400;
+ const long kRemoveKeywords = 0x800;
+
+ attribute nsOfflineImapOperationType operation;
+ void clearOperation(in nsOfflineImapOperationType operation);
+ attribute nsMsgKey messageKey;
+
+ // for move/copy operations, the msg key of the source msg.
+ attribute nsMsgKey srcMessageKey;
+
+ attribute imapMessageFlagsType flagOperation;
+ attribute imapMessageFlagsType newFlags; // for kFlagsChanged
+ attribute AUTF8String destinationFolderURI; // for move or copy
+ attribute AUTF8String sourceFolderURI;
+ void addKeywordToAdd(in string aKeyword);
+ void addKeywordToRemove(in string aKeyword);
+ readonly attribute string keywordsToAdd;
+ readonly attribute string keywordsToRemove;
+ readonly attribute long numberOfCopies;
+ void addMessageCopyOperation(in AUTF8String destinationBox);
+ string getCopyDestination(in long copyIndex);
+ attribute unsigned long msgSize;
+ attribute boolean playingBack;
+};
diff --git a/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl b/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl
new file mode 100644
index 0000000000..151a42f019
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#include "nsMsgKeySet.h"
+%}
+
+[ptr] native nsMsgKeySetPtr(nsMsgKeySet);
+
+[scriptable, uuid(f700208a-1dd1-11b2-b947-e4e1e4fdf278)]
+
+interface nsINewsDatabase : nsISupports {
+ [noscript] attribute nsMsgKeySetPtr readSet;
+};
diff --git a/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h b/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h
new file mode 100644
index 0000000000..307ad2ed0a
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsImapMailDatabase_H_
+#define _nsImapMailDatabase_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMailDatabase.h"
+
+class nsImapMailDatabase : public nsMailDatabase {
+ public:
+ // OK, it's dumb that this should require a fileSpec, since there is no file
+ // for the folder. This is mainly because we're deriving from nsMailDatabase;
+ // Perhaps we shouldn't...
+ nsImapMailDatabase();
+ virtual ~nsImapMailDatabase();
+
+ NS_IMETHOD GetSummaryValid(bool* aResult) override;
+ NS_IMETHOD SetSummaryValid(bool valid = true) override;
+ virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) override;
+
+ NS_IMETHOD ForceClosed() override;
+ NS_IMETHOD AddNewHdrToDB(nsIMsgDBHdr* newHdr, bool notify) override;
+ NS_IMETHOD SetAttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr,
+ const char* property,
+ const char* propertyVal) override;
+ NS_IMETHOD SetUint32AttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr,
+ const char* property,
+ uint32_t propertyVal) override;
+ NS_IMETHOD SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr,
+ const char* aProperty,
+ uint64_t aPropertyVal) override;
+ NS_IMETHOD DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys,
+ nsIDBChangeListener* instigator) override;
+ NS_IMETHOD UpdatePendingAttributes(nsIMsgDBHdr* aNewHdr) override;
+
+ protected:
+ nsresult GetRowForPendingHdr(nsIMsgDBHdr* pendingHdr, nsIMdbRow** row);
+ nsresult GetAllPendingHdrsTable();
+ mdb_token m_pendingHdrsRowScopeToken;
+ mdb_token m_pendingHdrsTableKindToken;
+ nsCOMPtr<nsIMdbTable> m_mdbAllPendingHdrsTable;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsMailDatabase.h b/comm/mailnews/db/msgdb/public/nsMailDatabase.h
new file mode 100644
index 0000000000..4e29b26322
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsMailDatabase.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMailDatabase_H_
+#define _nsMailDatabase_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDatabase.h"
+#include "nsTArray.h"
+
+#include "nsIDBChangeListener.h"
+#include "nsIMsgOfflineImapOperation.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIFile.h"
+
+// This is the subclass of nsMsgDatabase that handles local mail messages.
+
+class nsMailDatabase : public nsMsgDatabase {
+ public:
+ nsMailDatabase();
+ virtual ~nsMailDatabase();
+ NS_IMETHOD ForceClosed() override;
+ NS_IMETHOD DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys,
+ nsIDBChangeListener* instigator) override;
+
+ nsresult Open(nsMsgDBService* aDBService, nsIFile* aSummaryFile, bool create,
+ bool upgrading) override;
+ virtual nsMailDatabase* GetMailDB() { return this; }
+
+ virtual uint32_t GetCurVersion() override { return kMsgDBVersion; }
+
+ NS_IMETHOD GetOfflineOpForKey(nsMsgKey opKey, bool create,
+ nsIMsgOfflineImapOperation** op) override;
+ NS_IMETHOD RemoveOfflineOp(nsIMsgOfflineImapOperation* op) override;
+
+ NS_IMETHOD SetSummaryValid(bool valid) override;
+ NS_IMETHOD GetSummaryValid(bool* valid) override;
+
+ NS_IMETHOD ListAllOfflineOpIds(nsTArray<nsMsgKey>& offlineOpIds) override;
+ NS_IMETHOD ListAllOfflineDeletes(nsTArray<nsMsgKey>& offlineDeletes) override;
+
+ friend class nsMsgOfflineOpEnumerator;
+
+ protected:
+ nsresult GetAllOfflineOpsTable(); // get this on demand
+
+ // get the time and date of the mailbox file
+ void GetMailboxModProperties(int64_t* aSize, uint32_t* aDate);
+
+ nsCOMPtr<nsIMdbTable> m_mdbAllOfflineOpsTable;
+ mdb_token m_offlineOpsRowScopeToken;
+ mdb_token m_offlineOpsTableKindToken;
+
+ virtual void SetReparse(bool reparse);
+
+ protected:
+ bool m_reparse;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsMsgDatabase.h b/comm/mailnews/db/msgdb/public/nsMsgDatabase.h
new file mode 100644
index 0000000000..f61b9a7b25
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsMsgDatabase.h
@@ -0,0 +1,447 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgDatabase_H_
+#define _nsMsgDatabase_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Path.h"
+#include "nsIFile.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgHdr.h"
+#include "nsString.h"
+#include "nsIDBChangeAnnouncer.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFolder.h"
+#include "nsDBFolderInfo.h"
+#include "mozilla/intl/Collator.h"
+#include "nsIMimeConverter.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "PLDHashTable.h"
+#include "nsTArray.h"
+#include "nsTObserverArray.h"
+
+using mozilla::intl::Collator;
+
+class nsMsgThread;
+class nsMsgDatabase;
+class nsIMsgOfflineOpsDatabase;
+class nsIMsgThread;
+class nsMsgDBEnumerator;
+class nsMsgDBThreadEnumerator;
+
+const int32_t kMsgDBVersion = 1;
+
+// Hopefully we're not opening up lots of databases at the same time, however
+// this will give us a buffer before we need to start reallocating the cache
+// array.
+const uint32_t kInitialMsgDBCacheSize = 20;
+
+class nsMsgDBService final : public nsIMsgDBService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDBSERVICE
+
+ nsMsgDBService();
+
+ void AddToCache(nsMsgDatabase* pMessageDB);
+ void DumpCache();
+ void EnsureCached(nsMsgDatabase* pMessageDB) {
+ if (!m_dbCache.Contains(pMessageDB)) m_dbCache.AppendElement(pMessageDB);
+ }
+ void RemoveFromCache(nsMsgDatabase* pMessageDB) {
+ m_dbCache.RemoveElement(pMessageDB);
+ }
+
+ protected:
+ ~nsMsgDBService();
+ void HookupPendingListeners(nsIMsgDatabase* db, nsIMsgFolder* folder);
+ void FinishDBOpen(nsIMsgFolder* aFolder, nsMsgDatabase* aMsgDB);
+ nsMsgDatabase* FindInCache(nsIFile* dbName);
+
+ nsCOMArray<nsIMsgFolder> m_foldersPendingListeners;
+ nsCOMArray<nsIDBChangeListener> m_pendingListeners;
+ AutoTArray<nsMsgDatabase*, kInitialMsgDBCacheSize> m_dbCache;
+};
+
+namespace mozilla {
+namespace mailnews {
+class MsgDBReporter;
+}
+} // namespace mozilla
+
+class nsMsgDatabase : public nsIMsgOfflineOpsDatabase {
+ public:
+ friend class nsMsgDBService;
+ friend class nsMsgPropertyEnumerator; // accesses m_mdbEnv and m_mdbStore
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDBCHANGEANNOUNCER
+ NS_DECL_NSIMSGDATABASE
+ NS_DECL_NSIMSGOFFLINEOPSDATABASE
+
+ /**
+ * Opens a database folder.
+ *
+ * @param aFolderName The name of the folder to create.
+ * @param aCreate Whether or not the file should be created.
+ * @param aLeaveInvalidDB Set to true if you do not want the database to be
+ * deleted if it is invalid.
+ * @exception NS_ERROR_FILE_NOT_FOUND
+ * The file could not be created.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE
+ * The database is present (and was opened), but the
+ * summary file is out of date.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING
+ * The database is present (and was opened), but the
+ * summary file is missing.
+ */
+ virtual nsresult Open(nsMsgDBService* aDBService, nsIFile* aFolderName,
+ bool aCreate, bool aLeaveInvalidDB);
+ virtual nsresult IsHeaderRead(nsIMsgDBHdr* hdr, bool* pRead);
+ virtual nsresult MarkHdrReadInDB(nsIMsgDBHdr* msgHdr, bool bRead,
+ nsIDBChangeListener* instigator);
+ nsresult OpenInternal(nsMsgDBService* aDBService, nsIFile* aFolderName,
+ bool aCreate, bool aLeaveInvalidDB, bool sync);
+ nsresult CheckForErrors(nsresult err, bool sync, nsMsgDBService* aDBService,
+ nsIFile* summaryFile);
+ virtual nsresult OpenMDB(nsIFile* dbfile, bool create, bool sync);
+ virtual nsresult CloseMDB(bool commit);
+ virtual nsresult CreateMsgHdr(nsIMdbRow* hdrRow, nsMsgKey key,
+ nsIMsgDBHdr** result);
+ virtual nsresult GetThreadForMsgKey(nsMsgKey msgKey, nsIMsgThread** result);
+ virtual nsresult EnumerateMessagesWithFlag(nsIMsgEnumerator** result,
+ uint32_t* pFlag);
+ nsresult GetSearchResultsTable(const nsACString& searchFolderUri,
+ bool createIfMissing, nsIMdbTable** table);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // nsMsgDatabase methods:
+ nsMsgDatabase();
+
+ nsresult GetMDBFactory(nsIMdbFactory** aMdbFactory);
+ nsIMdbEnv* GetEnv() { return m_mdbEnv; }
+ nsIMdbStore* GetStore() { return m_mdbStore; }
+ virtual uint32_t GetCurVersion();
+ nsresult GetCollationKeyGenerator();
+ nsIMimeConverter* GetMimeConverter();
+
+ nsresult GetTableCreateIfMissing(const char* scope, const char* kind,
+ nsIMdbTable** table, mdb_token& scopeToken,
+ mdb_token& kindToken);
+
+ // helper function to fill in nsStrings from hdr row cell contents.
+ nsresult RowCellColumnTonsString(nsIMdbRow* row, mdb_token columnToken,
+ nsAString& resultStr);
+ nsresult RowCellColumnToUInt32(nsIMdbRow* row, mdb_token columnToken,
+ uint32_t* uint32Result,
+ uint32_t defaultValue = 0);
+ nsresult RowCellColumnToUInt32(nsIMdbRow* row, mdb_token columnToken,
+ uint32_t& uint32Result,
+ uint32_t defaultValue = 0);
+ nsresult RowCellColumnToUInt64(nsIMdbRow* row, mdb_token columnToken,
+ uint64_t* uint64Result,
+ uint64_t defaultValue = 0);
+ nsresult RowCellColumnToMime2DecodedString(nsIMdbRow* row,
+ mdb_token columnToken,
+ nsAString& resultStr);
+ nsresult RowCellColumnToCollationKey(nsIMdbRow* row, mdb_token columnToken,
+ nsTArray<uint8_t>& result);
+ nsresult RowCellColumnToConstCharPtr(nsIMdbRow* row, mdb_token columnToken,
+ const char** ptr);
+ nsresult RowCellColumnToAddressCollationKey(nsIMdbRow* row,
+ mdb_token colToken,
+ nsTArray<uint8_t>& result);
+
+ nsresult GetEffectiveCharset(nsIMdbRow* row, nsACString& resultCharset);
+
+ // these methods take the property name as a string, not a token.
+ // they should be used when the properties aren't accessed a lot
+ nsresult GetProperty(nsIMdbRow* row, const char* propertyName, char** result);
+ nsresult SetProperty(nsIMdbRow* row, const char* propertyName,
+ const char* propertyVal);
+ nsresult GetPropertyAsNSString(nsIMdbRow* row, const char* propertyName,
+ nsAString& result);
+ nsresult SetPropertyFromNSString(nsIMdbRow* row, const char* propertyName,
+ const nsAString& propertyVal);
+ nsresult GetUint32Property(nsIMdbRow* row, const char* propertyName,
+ uint32_t* result, uint32_t defaultValue = 0);
+ nsresult GetUint64Property(nsIMdbRow* row, const char* propertyName,
+ uint64_t* result, uint64_t defaultValue = 0);
+ nsresult SetUint32Property(nsIMdbRow* row, const char* propertyName,
+ uint32_t propertyVal);
+ nsresult SetUint64Property(nsIMdbRow* row, const char* propertyName,
+ uint64_t propertyVal);
+ nsresult GetBooleanProperty(nsIMdbRow* row, const char* propertyName,
+ bool* result, bool defaultValue = false);
+ nsresult SetBooleanProperty(nsIMdbRow* row, const char* propertyName,
+ bool propertyVal);
+ // helper function for once we have the token.
+ nsresult SetNSStringPropertyWithToken(nsIMdbRow* row, mdb_token aProperty,
+ const nsAString& propertyStr);
+
+ // helper functions to put values in cells for the passed-in row
+ nsresult UInt32ToRowCellColumn(nsIMdbRow* row, mdb_token columnToken,
+ uint32_t value);
+ nsresult CharPtrToRowCellColumn(nsIMdbRow* row, mdb_token columnToken,
+ const char* charPtr);
+ nsresult RowCellColumnToCharPtr(nsIMdbRow* row, mdb_token columnToken,
+ char** result);
+ nsresult UInt64ToRowCellColumn(nsIMdbRow* row, mdb_token columnToken,
+ uint64_t value);
+
+ // helper functions to copy an nsString to a yarn, int32 to yarn, and vice
+ // versa.
+ static struct mdbYarn* nsStringToYarn(struct mdbYarn* yarn,
+ const nsAString& str);
+ static struct mdbYarn* UInt32ToYarn(struct mdbYarn* yarn, uint32_t i);
+ static struct mdbYarn* UInt64ToYarn(struct mdbYarn* yarn, uint64_t i);
+ static void YarnTonsString(struct mdbYarn* yarn, nsAString& str);
+ static void YarnTonsCString(struct mdbYarn* yarn, nsACString& str);
+ static void YarnToUInt32(struct mdbYarn* yarn, uint32_t* i);
+ static void YarnToUInt64(struct mdbYarn* yarn, uint64_t* i);
+
+#ifdef DEBUG
+ virtual nsresult DumpContents();
+#endif
+
+ friend class nsMsgHdr; // use this to get access to cached tokens for hdr
+ // fields
+ friend class nsMsgThread; // use this to get access to cached tokens for hdr
+ // fields
+
+ friend class nsMsgDBEnumerator;
+ friend class nsMsgDBThreadEnumerator;
+
+ protected:
+ virtual ~nsMsgDatabase();
+
+ // prefs stuff - in future, we might want to cache the prefs interface
+ nsresult GetBoolPref(const char* prefName, bool* result);
+ nsresult GetIntPref(const char* prefName, int32_t* result);
+ virtual void GetGlobalPrefs();
+ // retrieval methods
+ nsIMsgThread* GetThreadForReference(nsCString& msgID, nsIMsgDBHdr** pMsgHdr);
+ nsIMsgThread* GetThreadForSubject(nsCString& subject);
+ nsIMsgThread* GetThreadForMessageId(nsCString& msgId);
+ nsIMsgThread* GetThreadForThreadId(nsMsgKey threadId);
+ nsMsgHdr* GetMsgHdrForReference(nsCString& reference);
+ nsIMsgDBHdr* GetMsgHdrForSubject(nsCString& subject);
+ // threading interfaces
+ virtual nsresult CreateNewThread(nsMsgKey key, const char* subject,
+ nsMsgThread** newThread);
+ virtual bool ThreadBySubjectWithoutRe();
+ virtual bool UseStrictThreading();
+ virtual bool UseCorrectThreading();
+ virtual nsresult ThreadNewHdr(nsMsgHdr* hdr, bool& newThread);
+ virtual nsresult AddNewThread(nsMsgHdr* msgHdr);
+ virtual nsresult AddToThread(nsMsgHdr* newHdr, nsIMsgThread* thread,
+ nsIMsgDBHdr* pMsgHdr, bool threadInThread);
+
+ static PRTime gLastUseTime; // global last use time
+ PRTime m_lastUseTime; // last use time for this db
+ // inline to make instrumentation as cheap as possible
+ inline void RememberLastUseTime() { gLastUseTime = m_lastUseTime = PR_Now(); }
+
+ bool MatchDbName(nsIFile* dbName); // returns TRUE if they match
+
+ // Flag handling routines
+ virtual nsresult SetKeyFlag(nsMsgKey key, bool set, nsMsgMessageFlagType flag,
+ nsIDBChangeListener* instigator = nullptr);
+ virtual nsresult SetMsgHdrFlag(nsIMsgDBHdr* msgHdr, bool set,
+ nsMsgMessageFlagType flag,
+ nsIDBChangeListener* instigator);
+
+ virtual bool SetHdrFlag(nsIMsgDBHdr*, bool bSet, nsMsgMessageFlagType flag);
+ virtual bool SetHdrReadFlag(nsIMsgDBHdr*, bool pRead);
+ virtual uint32_t GetStatusFlags(nsIMsgDBHdr* msgHdr,
+ nsMsgMessageFlagType origFlags);
+ // helper function which doesn't involve thread object
+
+ virtual nsresult RemoveHeaderFromDB(nsMsgHdr* msgHdr);
+ virtual nsresult RemoveHeaderFromThread(nsMsgHdr* msgHdr);
+ virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr);
+
+ mozilla::UniquePtr<mozilla::intl::Collator> m_collationKeyGenerator = nullptr;
+ nsCOMPtr<nsIMimeConverter> m_mimeConverter;
+ nsCOMPtr<nsIMsgRetentionSettings> m_retentionSettings;
+ nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings;
+
+ nsresult FindMessagesOlderThan(uint32_t daysToKeepHdrs,
+ bool applyToFlaggedMessages,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete);
+ nsresult FindExcessMessages(uint32_t numHeadersToKeep,
+ bool applyToFlaggedMessages,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete);
+
+ // mdb bookkeeping stuff
+ virtual nsresult InitExistingDB();
+ virtual nsresult InitNewDB();
+ virtual nsresult InitMDBInfo();
+
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ RefPtr<nsDBFolderInfo> m_dbFolderInfo;
+ nsMsgKey m_nextPseudoMsgKey;
+ nsIMdbEnv* m_mdbEnv; // to be used in all the db calls.
+ nsIMdbStore* m_mdbStore;
+ nsIMdbTable* m_mdbAllMsgHeadersTable;
+ nsIMdbTable* m_mdbAllThreadsTable;
+
+ // Used for asynchronous db opens. If non-null, we're still opening
+ // the underlying mork database. If null, the db has been completely opened.
+ nsCOMPtr<nsIMdbThumb> m_thumb;
+ // used to remember the args to Open for async open.
+ bool m_create;
+ bool m_leaveInvalidDB;
+
+ nsCOMPtr<nsIFile> m_dbFile;
+ nsTArray<nsMsgKey> m_newSet; // new messages since last open.
+ bool m_mdbTokensInitialized;
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>> m_ChangeListeners;
+ mdb_token m_hdrRowScopeToken;
+ mdb_token m_threadRowScopeToken;
+ mdb_token m_hdrTableKindToken;
+ mdb_token m_threadTableKindToken;
+ mdb_token m_allThreadsTableKindToken;
+ mdb_token m_subjectColumnToken;
+ mdb_token m_senderColumnToken;
+ mdb_token m_messageIdColumnToken;
+ mdb_token m_referencesColumnToken;
+ mdb_token m_recipientsColumnToken;
+ mdb_token m_dateColumnToken;
+ mdb_token m_messageSizeColumnToken;
+ mdb_token m_flagsColumnToken;
+ mdb_token m_priorityColumnToken;
+ mdb_token m_labelColumnToken;
+ mdb_token m_numLinesColumnToken;
+ mdb_token m_ccListColumnToken;
+ mdb_token m_bccListColumnToken;
+ mdb_token m_threadFlagsColumnToken;
+ mdb_token m_threadIdColumnToken;
+ mdb_token m_threadChildrenColumnToken;
+ mdb_token m_threadUnreadChildrenColumnToken;
+ mdb_token m_messageThreadIdColumnToken;
+ mdb_token m_threadSubjectColumnToken;
+ mdb_token m_messageCharSetColumnToken;
+ mdb_token m_threadParentColumnToken;
+ mdb_token m_threadRootKeyColumnToken;
+ mdb_token m_threadNewestMsgDateColumnToken;
+ mdb_token m_offlineMsgOffsetColumnToken;
+ mdb_token m_offlineMessageSizeColumnToken;
+
+ // header caching stuff - MRU headers, keeps them around in memory
+ nsresult AddHdrToCache(nsIMsgDBHdr* hdr, nsMsgKey key);
+ nsresult ClearHdrCache(bool reInit);
+ nsresult RemoveHdrFromCache(nsIMsgDBHdr* hdr, nsMsgKey key);
+ // all headers currently instantiated, doesn't hold refs
+ // these get added when msg hdrs get constructed, and removed when they get
+ // destroyed.
+ nsresult GetHdrFromUseCache(nsMsgKey key, nsIMsgDBHdr** result);
+ nsresult AddHdrToUseCache(nsIMsgDBHdr* hdr, nsMsgKey key);
+ nsresult ClearUseHdrCache();
+ nsresult RemoveHdrFromUseCache(nsIMsgDBHdr* hdr, nsMsgKey key);
+
+ // not-reference holding array of threads we've handed out.
+ // If a db goes away, it will clean up the outstanding threads.
+ // We use an nsTArray because we don't expect to ever have very many
+ // of these, rarely more than 5.
+ nsTArray<nsMsgThread*> m_threads;
+ // Clear outstanding thread objects
+ void ClearThreads();
+ nsMsgThread* FindExistingThread(nsMsgKey threadId);
+
+ mdb_pos FindInsertIndexInSortedTable(nsIMdbTable* table, mdb_id idToInsert);
+
+ void ClearCachedObjects(bool dbGoingAway);
+ void InvalidateEnumerators();
+ // all instantiated headers, but doesn't hold refs.
+ PLDHashTable* m_headersInUse;
+ static PLDHashNumber HashKey(const void* aKey);
+ static bool MatchEntry(const PLDHashEntryHdr* aEntry, const void* aKey);
+ static void MoveEntry(PLDHashTable* aTable, const PLDHashEntryHdr* aFrom,
+ PLDHashEntryHdr* aTo);
+ static void ClearEntry(PLDHashTable* aTable, PLDHashEntryHdr* aEntry);
+ static PLDHashTableOps gMsgDBHashTableOps;
+ struct MsgHdrHashElement : public PLDHashEntryHdr {
+ nsMsgKey mKey;
+ nsIMsgDBHdr* mHdr;
+ };
+ PLDHashTable* m_cachedHeaders;
+ bool m_bCacheHeaders;
+ nsMsgKey m_cachedThreadId;
+ nsCOMPtr<nsIMsgThread> m_cachedThread;
+ nsCOMPtr<nsIMdbFactory> mMdbFactory;
+
+ // Message reference hash table
+ static PLDHashTableOps gRefHashTableOps;
+ struct RefHashElement : public PLDHashEntryHdr {
+ const char* mRef; // Hash entry key, must come first
+ nsMsgKey mThreadId;
+ uint32_t mCount;
+ };
+ PLDHashTable* m_msgReferences;
+ nsresult GetRefFromHash(nsCString& reference, nsMsgKey* threadId);
+ nsresult AddRefToHash(nsCString& reference, nsMsgKey threadId);
+ nsresult AddMsgRefsToHash(nsIMsgDBHdr* msgHdr);
+ nsresult RemoveRefFromHash(nsCString& reference);
+ nsresult RemoveMsgRefsFromHash(nsIMsgDBHdr* msgHdr);
+ nsresult InitRefHash();
+
+ // The enumerators add themselves to these lists.
+ // If a db goes away - via destruction or ForceClosed() - it needs to
+ // invalidate any outstanding enumerators.
+ nsTArray<nsMsgDBEnumerator*> m_msgEnumerators;
+ nsTArray<nsMsgDBThreadEnumerator*> m_threadEnumerators;
+
+ // Memory reporter details
+ public:
+ static size_t HeaderHashSizeOf(PLDHashEntryHdr* hdr,
+ mozilla::MallocSizeOf aMallocSizeOf,
+ void* arg);
+ virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ uint32_t m_cacheSize;
+ RefPtr<mozilla::mailnews::MsgDBReporter> mMemReporter;
+};
+
+class nsMsgRetentionSettings : public nsIMsgRetentionSettings {
+ public:
+ nsMsgRetentionSettings();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGRETENTIONSETTINGS
+ protected:
+ virtual ~nsMsgRetentionSettings();
+ nsMsgRetainByPreference m_retainByPreference;
+ uint32_t m_daysToKeepHdrs;
+ uint32_t m_numHeadersToKeep;
+ bool m_useServerDefaults;
+ bool m_cleanupBodiesByDays;
+ uint32_t m_daysToKeepBodies;
+ bool m_applyToFlaggedMessages;
+};
+
+class nsMsgDownloadSettings : public nsIMsgDownloadSettings {
+ public:
+ nsMsgDownloadSettings();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDOWNLOADSETTINGS
+ protected:
+ virtual ~nsMsgDownloadSettings();
+ bool m_useServerDefaults;
+ bool m_downloadUnreadOnly;
+ bool m_downloadByDate;
+ int32_t m_ageLimitOfMsgsToDownload;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsMsgHdr.h b/comm/mailnews/db/msgdb/public/nsMsgHdr.h
new file mode 100644
index 0000000000..94e5b1b8c8
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsMsgHdr.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsMsgHdr_H
+#define _nsMsgHdr_H
+
+#include "mozilla/MemoryReporting.h"
+#include "nsIMsgHdr.h"
+#include "nsString.h"
+#include "MailNewsTypes.h"
+#include "mdb.h"
+#include "nsTArray.h"
+
+class nsMsgDatabase;
+class nsIMsgThread;
+
+class nsMsgHdr : public nsIMsgDBHdr {
+ public:
+ NS_DECL_NSIMSGDBHDR
+ friend class nsMsgDatabase;
+ friend class nsImapMailDatabase;
+ friend class nsMsgPropertyEnumerator;
+ friend class nsMsgThread;
+
+ ////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////
+ // nsMsgHdr methods:
+ nsMsgHdr(nsMsgDatabase* db, nsIMdbRow* dbRow);
+
+ NS_DECL_ISUPPORTS
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOfFun) const {
+ return m_references.ShallowSizeOfExcludingThis(aMallocSizeOfFun);
+ }
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOfFun) const {
+ return aMallocSizeOfFun(this) + SizeOfExcludingThis(aMallocSizeOfFun);
+ }
+
+ protected:
+ nsIMdbRow* GetMDBRow() { return m_mdbRow; }
+ void ReleaseMDBRow() { NS_IF_RELEASE(m_mdbRow); }
+ nsMsgDatabase* GetMdb() { return m_mdb; }
+ void ClearCachedValues() { m_initedValues = 0; }
+
+ virtual nsresult GetRawFlags(uint32_t* result);
+
+ bool IsParentOf(nsIMsgDBHdr* possibleChild);
+ bool IsAncestorOf(nsIMsgDBHdr* possibleChild);
+
+ private:
+ virtual ~nsMsgHdr();
+
+ void Init();
+ virtual nsresult InitFlags();
+ virtual nsresult InitCachedValues();
+
+ bool IsAncestorKilled(uint32_t ancestorsToCheck);
+ void ReparentInThread(nsIMsgThread* thread);
+
+ nsresult SetStringColumn(const char* str, mdb_token token);
+ nsresult SetUInt32Column(uint32_t value, mdb_token token);
+ nsresult GetUInt32Column(mdb_token token, uint32_t* pvalue,
+ uint32_t defaultValue = 0);
+ nsresult SetUInt64Column(uint64_t value, mdb_token token);
+ nsresult GetUInt64Column(mdb_token token, uint64_t* pvalue,
+ uint64_t defaultValue = 0);
+
+ // reference and threading stuff.
+ nsresult ParseReferences(const char* references);
+ const char* GetNextReference(const char* startNextRef, nsCString& reference,
+ bool acceptNonDelimitedReferences);
+
+ nsMsgKey m_threadId;
+ nsMsgKey m_messageKey; // news: article number, local mail: key, imap: uid...
+ nsMsgKey m_threadParent; // message this is a reply to, in thread.
+ PRTime m_date;
+ uint32_t m_messageSize; // lines for news articles, bytes for mail messages
+ uint32_t m_flags;
+ // avoid parsing references every time we want one
+ nsTArray<nsCString> m_references;
+
+ // nsMsgHdrs will have to know what db and row they belong to, since they are
+ // really just a wrapper around the msg row in the mdb. This could cause
+ // problems, though I hope not.
+ nsMsgDatabase* m_mdb;
+ nsIMdbRow* m_mdbRow;
+ uint32_t m_initedValues;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsMsgThread.h b/comm/mailnews/db/msgdb/public/nsMsgThread.h
new file mode 100644
index 0000000000..37add58082
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsMsgThread.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgThread_H
+#define _nsMsgThread_H
+
+#include "nsIMsgThread.h"
+#include "nsString.h"
+#include "MailNewsTypes.h"
+#include "mdb.h"
+
+class nsIMdbTable;
+class nsIMsgDBHdr;
+class nsMsgDatabase;
+
+class nsMsgThread : public nsIMsgThread {
+ public:
+ nsMsgThread();
+ nsMsgThread(nsMsgDatabase* db, nsIMdbTable* table);
+
+ friend class nsMsgThreadEnumerator;
+ friend class nsMsgDatabase;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTHREAD
+
+ RefPtr<nsMsgDatabase> m_mdbDB;
+
+ protected:
+ virtual ~nsMsgThread();
+
+ void Init();
+ void Clear();
+ virtual nsresult InitCachedValues();
+ nsresult ChangeChildCount(int32_t delta);
+ nsresult ChangeUnreadChildCount(int32_t delta);
+ nsresult RemoveChild(nsMsgKey msgKey);
+ nsresult SetThreadRootKey(nsMsgKey threadRootKey);
+ nsresult GetChildHdrForKey(nsMsgKey desiredKey, nsIMsgDBHdr** result,
+ int32_t* resultIndex);
+ nsresult RerootThread(nsIMsgDBHdr* newParentOfOldRoot, nsIMsgDBHdr* oldRoot,
+ nsIDBChangeAnnouncer* announcer);
+ nsresult ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer);
+
+ nsresult ReparentNonReferenceChildrenOf(nsIMsgDBHdr* topLevelHdr,
+ nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer);
+ nsresult ReparentMsgsWithInvalidParent(uint32_t numChildren,
+ nsMsgKey threadParentKey);
+
+ nsMsgKey m_threadKey;
+ uint32_t m_numChildren;
+ uint32_t m_numUnreadChildren;
+ uint32_t m_flags;
+ nsCOMPtr<nsIMdbTable> m_mdbTable;
+ nsCOMPtr<nsIMdbRow> m_metaRow;
+ bool m_cachedValuesInitialized;
+ nsMsgKey m_threadRootKey;
+ uint32_t m_newestMsgDate;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/public/nsNewsDatabase.h b/comm/mailnews/db/msgdb/public/nsNewsDatabase.h
new file mode 100644
index 0000000000..4a804d0d69
--- /dev/null
+++ b/comm/mailnews/db/msgdb/public/nsNewsDatabase.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsNewsDatabase_H_
+#define _nsNewsDatabase_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDatabase.h"
+#include "nsINewsDatabase.h"
+#include "nsTArray.h"
+#include "nsIMsgHdr.h"
+
+// news group database
+
+class nsNewsDatabase : public nsMsgDatabase, public nsINewsDatabase {
+ public:
+ nsNewsDatabase();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSINEWSDATABASE
+
+ NS_IMETHOD Close(bool forceCommit) override;
+ NS_IMETHOD ForceClosed() override;
+ NS_IMETHOD Commit(nsMsgDBCommit commitType) override;
+ virtual uint32_t GetCurVersion() override;
+
+ // methods to get and set docsets for ids.
+ NS_IMETHOD IsRead(nsMsgKey key, bool* pRead) override;
+ virtual nsresult IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) override;
+
+ NS_IMETHOD GetHighWaterArticleNum(nsMsgKey* key) override;
+ NS_IMETHOD GetLowWaterArticleNum(nsMsgKey* key) override;
+ NS_IMETHOD MarkAllRead(nsTArray<nsMsgKey>& thoseMarked) override;
+
+ virtual nsresult ExpireUpTo(nsMsgKey expireKey);
+ virtual nsresult ExpireRange(nsMsgKey startRange, nsMsgKey endRange);
+
+ virtual bool SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) override;
+
+ virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) override;
+ nsresult SyncWithReadSet();
+
+ NS_IMETHOD GetDefaultViewFlags(
+ nsMsgViewFlagsTypeValue* aDefaultViewFlags) override;
+ NS_IMETHOD GetDefaultSortType(
+ nsMsgViewSortTypeValue* aDefaultSortType) override;
+ NS_IMETHOD GetDefaultSortOrder(
+ nsMsgViewSortOrderValue* aDefaultSortOrder) override;
+
+ protected:
+ virtual ~nsNewsDatabase();
+ // this is owned by the nsNewsFolder, which lives longer than the db.
+ nsMsgKeySet* m_readSet;
+};
+
+#endif
diff --git a/comm/mailnews/db/msgdb/src/components.conf b/comm/mailnews/db/msgdb/src/components.conf
new file mode 100644
index 0000000000..1d65e685f2
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/components.conf
@@ -0,0 +1,44 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{a86c86ae-e97f-11d2-a506-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-mailbox"],
+ "type": "nsMailDatabase",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsMailDatabase.h"],
+ },
+ {
+ "cid": "{36414aa0-e980-11d2-a506-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-news"],
+ "type": "nsNewsDatabase",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsNewsDatabase.h"],
+ },
+ {
+ "cid": "{9e4b07ee-e980-11d2-a506-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-imap"],
+ "type": "nsImapMailDatabase",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h"],
+ },
+ {
+ "cid": "{1bd976d6-df44-11d4-a5b6-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/msgDatabase/retentionSettings;1"],
+ "type": "nsMsgRetentionSettings",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"],
+ },
+ {
+ "cid": "{4e3dae5a-157a-11d5-a5c0-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/msgDatabase/downloadSettings;1"],
+ "type": "nsMsgDownloadSettings",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"],
+ },
+ {
+ "cid": "{03223c50-1e88-45e8-ba1a-7ce792dc3fc3}",
+ "contract_ids": ["@mozilla.org/msgDatabase/msgDBService;1"],
+ "type": "nsMsgDBService",
+ "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"],
+ "name": "DB",
+ "interfaces": ["nsIMsgDBService"],
+ },
+]
diff --git a/comm/mailnews/db/msgdb/src/moz.build b/comm/mailnews/db/msgdb/src/moz.build
new file mode 100644
index 0000000000..06c2b92475
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/moz.build
@@ -0,0 +1,22 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsDBFolderInfo.cpp",
+ "nsImapMailDatabase.cpp",
+ "nsMailDatabase.cpp",
+ "nsMsgDatabase.cpp",
+ "nsMsgDatabaseEnumerators.cpp",
+ "nsMsgHdr.cpp",
+ "nsMsgOfflineImapOperation.cpp",
+ "nsMsgThread.cpp",
+ "nsNewsDatabase.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp b/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp
new file mode 100644
index 0000000000..57118ffaf8
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp
@@ -0,0 +1,749 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsDBFolderInfo.h"
+#include "nsMsgDatabase.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIMsgDBView.h"
+#include "nsServiceManagerUtils.h"
+#include "nsImapCore.h"
+
+static const char* kDBFolderInfoScope = "ns:msg:db:row:scope:dbfolderinfo:all";
+static const char* kDBFolderInfoTableKind = "ns:msg:db:table:kind:dbfolderinfo";
+
+struct mdbOid gDBFolderInfoOID;
+
+static const char* kNumMessagesColumnName = "numMsgs";
+// have to leave this as numNewMsgs even though it's numUnread Msgs
+static const char* kNumUnreadMessagesColumnName = "numNewMsgs";
+static const char* kFlagsColumnName = "flags";
+static const char* kFolderSizeColumnName = "folderSize";
+static const char* kExpungedBytesColumnName = "expungedBytes";
+static const char* kFolderDateColumnName = "folderDate";
+static const char* kHighWaterMessageKeyColumnName = "highWaterKey";
+
+static const char* kImapUidValidityColumnName = "UIDValidity";
+static const char* kTotalPendingMessagesColumnName = "totPendingMsgs";
+static const char* kUnreadPendingMessagesColumnName = "unreadPendingMsgs";
+static const char* kMailboxNameColumnName = "mailboxName";
+static const char* kKnownArtsSetColumnName = "knownArts";
+static const char* kExpiredMarkColumnName = "expiredMark";
+static const char* kVersionColumnName = "version";
+static const char* kLocaleColumnName = "locale";
+
+NS_IMPL_ADDREF(nsDBFolderInfo)
+NS_IMPL_RELEASE(nsDBFolderInfo)
+
+NS_IMETHODIMP
+nsDBFolderInfo::QueryInterface(REFNSIID iid, void** result) {
+ if (!result) return NS_ERROR_NULL_POINTER;
+
+ *result = nullptr;
+ if (iid.Equals(NS_GET_IID(nsIDBFolderInfo)) ||
+ iid.Equals(NS_GET_IID(nsISupports))) {
+ *result = static_cast<nsIDBFolderInfo*>(this);
+ AddRef();
+ return NS_OK;
+ }
+ return NS_NOINTERFACE;
+}
+
+nsDBFolderInfo::nsDBFolderInfo(nsMsgDatabase* mdb)
+ : m_flags(0),
+ m_expiredMark(0),
+ m_tableKindToken(0),
+ m_expiredMarkColumnToken(0) {
+ m_mdbTable = NULL;
+ m_mdbRow = NULL;
+ m_version = 1; // for upgrading...
+ m_IMAPHierarchySeparator = 0; // imap path separator
+ // mail only (for now)
+ m_folderSize = 0;
+ m_folderDate = 0;
+ m_expungedBytes = 0; // sum of size of deleted messages in folder
+ m_highWaterMessageKey = 0;
+
+ m_numUnreadMessages = 0;
+ m_numMessages = 0;
+ // IMAP only
+ m_ImapUidValidity = kUidUnknown;
+ m_totalPendingMessages = 0;
+ m_unreadPendingMessages = 0;
+
+ m_mdbTokensInitialized = false;
+
+ m_mdb = mdb;
+ if (mdb) {
+ nsresult err;
+
+ err = m_mdb->GetStore()->StringToToken(mdb->GetEnv(), kDBFolderInfoScope,
+ &m_rowScopeToken);
+ if (NS_SUCCEEDED(err)) {
+ err = m_mdb->GetStore()->StringToToken(
+ mdb->GetEnv(), kDBFolderInfoTableKind, &m_tableKindToken);
+ if (NS_SUCCEEDED(err)) {
+ gDBFolderInfoOID.mOid_Scope = m_rowScopeToken;
+ gDBFolderInfoOID.mOid_Id = 1;
+ }
+ }
+ InitMDBInfo();
+ }
+}
+
+nsDBFolderInfo::~nsDBFolderInfo() {
+ // nsMsgDatabase strictly owns nsDBFolderInfo, so don't ref-count db.
+ ReleaseExternalReferences();
+}
+
+// Release any objects we're holding onto. This needs to be safe
+// to call multiple times.
+void nsDBFolderInfo::ReleaseExternalReferences() {
+ if (m_mdb) {
+ if (m_mdbTable) {
+ NS_RELEASE(m_mdbTable);
+ m_mdbTable = nullptr;
+ }
+ if (m_mdbRow) {
+ NS_RELEASE(m_mdbRow);
+ m_mdbRow = nullptr;
+ }
+ m_mdb = nullptr;
+ }
+}
+
+// this routine sets up a new db to know about the dbFolderInfo stuff...
+nsresult nsDBFolderInfo::AddToNewMDB() {
+ nsresult ret = NS_OK;
+ if (m_mdb && m_mdb->GetStore()) {
+ nsIMdbStore* store = m_mdb->GetStore();
+ // create the unique table for the dbFolderInfo.
+ nsresult err =
+ store->NewTable(m_mdb->GetEnv(), m_rowScopeToken, m_tableKindToken,
+ true, nullptr, &m_mdbTable);
+
+ // create the singleton row for the dbFolderInfo.
+ err = store->NewRowWithOid(m_mdb->GetEnv(), &gDBFolderInfoOID, &m_mdbRow);
+
+ // add the row to the singleton table.
+ if (m_mdbRow && NS_SUCCEEDED(err))
+ err = m_mdbTable->AddRow(m_mdb->GetEnv(), m_mdbRow);
+
+ ret = err; // what are we going to do about nsresult's?
+ }
+ return ret;
+}
+
+nsresult nsDBFolderInfo::InitFromExistingDB() {
+ nsresult ret = NS_OK;
+ if (m_mdb && m_mdb->GetStore()) {
+ nsIMdbStore* store = m_mdb->GetStore();
+ if (store) {
+ mdb_pos rowPos;
+ mdb_count outTableCount; // current number of such tables
+ mdb_bool mustBeUnique; // whether port can hold only one of these
+ mdb_bool hasOid;
+ ret = store->GetTableKind(m_mdb->GetEnv(), m_rowScopeToken,
+ m_tableKindToken, &outTableCount, &mustBeUnique,
+ &m_mdbTable);
+ // NS_ASSERTION(mustBeUnique && outTableCount == 1, "only one global db
+ // info allowed");
+
+ if (m_mdbTable) {
+ // find singleton row for global info.
+ ret = m_mdbTable->HasOid(m_mdb->GetEnv(), &gDBFolderInfoOID, &hasOid);
+ if (NS_SUCCEEDED(ret)) {
+ nsIMdbTableRowCursor* rowCursor;
+ rowPos = -1;
+ ret = m_mdbTable->GetTableRowCursor(m_mdb->GetEnv(), rowPos,
+ &rowCursor);
+ if (NS_SUCCEEDED(ret)) {
+ ret = rowCursor->NextRow(m_mdb->GetEnv(), &m_mdbRow, &rowPos);
+ NS_RELEASE(rowCursor);
+ if (!m_mdbRow) ret = NS_ERROR_FAILURE;
+ if (NS_SUCCEEDED(ret)) LoadMemberVariables();
+ }
+ }
+ } else
+ ret = NS_ERROR_FAILURE;
+ }
+ }
+ return ret;
+}
+
+nsresult nsDBFolderInfo::InitMDBInfo() {
+ nsresult ret = NS_OK;
+ if (!m_mdbTokensInitialized && m_mdb && m_mdb->GetStore()) {
+ nsIMdbStore* store = m_mdb->GetStore();
+ nsIMdbEnv* env = m_mdb->GetEnv();
+
+ store->StringToToken(env, kNumMessagesColumnName,
+ &m_numMessagesColumnToken);
+ store->StringToToken(env, kNumUnreadMessagesColumnName,
+ &m_numUnreadMessagesColumnToken);
+ store->StringToToken(env, kFlagsColumnName, &m_flagsColumnToken);
+ store->StringToToken(env, kFolderSizeColumnName, &m_folderSizeColumnToken);
+ store->StringToToken(env, kExpungedBytesColumnName,
+ &m_expungedBytesColumnToken);
+ store->StringToToken(env, kFolderDateColumnName, &m_folderDateColumnToken);
+
+ store->StringToToken(env, kHighWaterMessageKeyColumnName,
+ &m_highWaterMessageKeyColumnToken);
+ store->StringToToken(env, kMailboxNameColumnName,
+ &m_mailboxNameColumnToken);
+
+ store->StringToToken(env, kImapUidValidityColumnName,
+ &m_imapUidValidityColumnToken);
+ store->StringToToken(env, kTotalPendingMessagesColumnName,
+ &m_totalPendingMessagesColumnToken);
+ store->StringToToken(env, kUnreadPendingMessagesColumnName,
+ &m_unreadPendingMessagesColumnToken);
+ store->StringToToken(env, kExpiredMarkColumnName,
+ &m_expiredMarkColumnToken);
+ store->StringToToken(env, kVersionColumnName, &m_versionColumnToken);
+ m_mdbTokensInitialized = true;
+ }
+
+ return ret;
+}
+
+nsresult nsDBFolderInfo::LoadMemberVariables() {
+ // it's really not an error for these properties to not exist...
+ GetInt32PropertyWithToken(m_numMessagesColumnToken, m_numMessages);
+ GetInt32PropertyWithToken(m_numUnreadMessagesColumnToken,
+ m_numUnreadMessages);
+ GetInt32PropertyWithToken(m_flagsColumnToken, m_flags);
+ GetInt64PropertyWithToken(m_folderSizeColumnToken, m_folderSize);
+ GetUint32PropertyWithToken(m_folderDateColumnToken, m_folderDate);
+ GetInt32PropertyWithToken(m_imapUidValidityColumnToken, m_ImapUidValidity,
+ kUidUnknown);
+ GetUint32PropertyWithToken(m_expiredMarkColumnToken, m_expiredMark);
+ GetInt64PropertyWithToken(m_expungedBytesColumnToken, m_expungedBytes);
+ GetUint32PropertyWithToken(m_highWaterMessageKeyColumnToken,
+ m_highWaterMessageKey);
+ int32_t version;
+
+ GetInt32PropertyWithToken(m_versionColumnToken, version);
+ m_version = (uint16_t)version;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetVersion(uint32_t version) {
+ m_version = version;
+ return SetUint32PropertyWithToken(m_versionColumnToken, (uint32_t)m_version);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetVersion(uint32_t* version) {
+ *version = m_version;
+ return NS_OK;
+}
+
+nsresult nsDBFolderInfo::AdjustHighWater(nsMsgKey highWater, bool force) {
+ if (force || m_highWaterMessageKey < highWater) {
+ m_highWaterMessageKey = highWater;
+ SetUint32PropertyWithToken(m_highWaterMessageKeyColumnToken, highWater);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetHighWater(nsMsgKey highWater) {
+ return AdjustHighWater(highWater, true);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::OnKeyAdded(nsMsgKey aNewKey) {
+ return AdjustHighWater(aNewKey, false);
+}
+
+NS_IMETHODIMP
+nsDBFolderInfo::GetFolderSize(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+ *size = m_folderSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetFolderSize(int64_t size) {
+ m_folderSize = size;
+ return SetInt64Property(kFolderSizeColumnName, m_folderSize);
+}
+
+NS_IMETHODIMP
+nsDBFolderInfo::GetFolderDate(uint32_t* folderDate) {
+ NS_ENSURE_ARG_POINTER(folderDate);
+ *folderDate = m_folderDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetFolderDate(uint32_t folderDate) {
+ m_folderDate = folderDate;
+ return SetUint32PropertyWithToken(m_folderDateColumnToken, folderDate);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetHighWater(nsMsgKey* result) {
+ // Sanity check highwater - if it gets too big, other code
+ // can fail. Look through last 100 messages to recalculate
+ // the highwater mark.
+ *result = m_highWaterMessageKey;
+ if (m_highWaterMessageKey > 0xFFFFFF00 && m_mdb) {
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ nsresult rv = m_mdb->ReverseEnumerateMessages(getter_AddRefs(hdrs));
+ if (NS_FAILED(rv)) return rv;
+ bool hasMore = false;
+ nsCOMPtr<nsIMsgDBHdr> pHeader;
+ nsMsgKey recalculatedHighWater = 1;
+ int32_t i = 0;
+ while (i++ < 100 && NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ (void)hdrs->GetNext(getter_AddRefs(pHeader));
+ if (pHeader) {
+ nsMsgKey msgKey;
+ pHeader->GetMessageKey(&msgKey);
+ if (msgKey > recalculatedHighWater) recalculatedHighWater = msgKey;
+ }
+ }
+ NS_ASSERTION(m_highWaterMessageKey >= recalculatedHighWater,
+ "highwater incorrect");
+ m_highWaterMessageKey = recalculatedHighWater;
+ }
+ *result = m_highWaterMessageKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetExpiredMark(nsMsgKey expiredKey) {
+ m_expiredMark = expiredKey;
+ return SetUint32PropertyWithToken(m_expiredMarkColumnToken, expiredKey);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetExpiredMark(nsMsgKey* result) {
+ *result = m_expiredMark;
+ return NS_OK;
+}
+
+// The size of the argument depends on the maximum size of a single message
+NS_IMETHODIMP nsDBFolderInfo::ChangeExpungedBytes(int32_t delta) {
+ return SetExpungedBytes(m_expungedBytes + delta);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetMailboxName(const nsAString& newBoxName) {
+ return SetPropertyWithToken(m_mailboxNameColumnToken, newBoxName);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetMailboxName(nsAString& boxName) {
+ return GetPropertyWithToken(m_mailboxNameColumnToken, boxName);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::ChangeNumUnreadMessages(int32_t delta) {
+ m_numUnreadMessages += delta;
+ // m_numUnreadMessages can never be set to negative.
+ if (m_numUnreadMessages < 0) {
+#ifdef DEBUG_bienvenu1
+ NS_ASSERTION(false, "Hardcoded assertion");
+#endif
+ m_numUnreadMessages = 0;
+ }
+ return SetUint32PropertyWithToken(m_numUnreadMessagesColumnToken,
+ m_numUnreadMessages);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::ChangeNumMessages(int32_t delta) {
+ m_numMessages += delta;
+ // m_numMessages can never be set to negative.
+ if (m_numMessages < 0) {
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(false, "num messages can't be < 0");
+#endif
+ m_numMessages = 0;
+ }
+ return SetUint32PropertyWithToken(m_numMessagesColumnToken, m_numMessages);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetNumUnreadMessages(int32_t* result) {
+ *result = m_numUnreadMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetNumUnreadMessages(int32_t numUnreadMessages) {
+ m_numUnreadMessages = numUnreadMessages;
+ return SetUint32PropertyWithToken(m_numUnreadMessagesColumnToken,
+ m_numUnreadMessages);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetNumMessages(int32_t* result) {
+ *result = m_numMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetNumMessages(int32_t numMessages) {
+ m_numMessages = numMessages;
+ return SetUint32PropertyWithToken(m_numMessagesColumnToken, m_numMessages);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetExpungedBytes(int64_t* result) {
+ *result = m_expungedBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetExpungedBytes(int64_t expungedBytes) {
+ m_expungedBytes = expungedBytes;
+ return SetInt64PropertyWithToken(m_expungedBytesColumnToken, m_expungedBytes);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetFlags(int32_t* result) {
+ *result = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetFlags(int32_t flags) {
+ nsresult ret = NS_OK;
+
+ if (m_flags != flags) {
+ NS_ASSERTION((m_flags & nsMsgFolderFlags::Inbox) == 0 ||
+ (flags & nsMsgFolderFlags::Inbox) != 0,
+ "lost inbox flag");
+ m_flags = flags;
+ ret = SetInt32PropertyWithToken(m_flagsColumnToken, m_flags);
+ }
+ return ret;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::OrFlags(int32_t flags, int32_t* result) {
+ m_flags |= flags;
+ *result = m_flags;
+ return SetInt32PropertyWithToken(m_flagsColumnToken, m_flags);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::AndFlags(int32_t flags, int32_t* result) {
+ m_flags &= flags;
+ *result = m_flags;
+ return SetInt32PropertyWithToken(m_flagsColumnToken, m_flags);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetImapUidValidity(int32_t* result) {
+ *result = m_ImapUidValidity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetImapUidValidity(int32_t uidValidity) {
+ m_ImapUidValidity = uidValidity;
+ return SetUint32PropertyWithToken(m_imapUidValidityColumnToken,
+ m_ImapUidValidity);
+}
+
+bool nsDBFolderInfo::TestFlag(int32_t flags) { return (m_flags & flags) != 0; }
+
+NS_IMETHODIMP
+nsDBFolderInfo::GetLocale(nsAString& result) {
+ GetProperty(kLocaleColumnName, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetLocale(const nsAString& locale) {
+ return SetProperty(kLocaleColumnName, locale);
+}
+
+NS_IMETHODIMP
+nsDBFolderInfo::GetImapTotalPendingMessages(int32_t* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_totalPendingMessages;
+ return NS_OK;
+}
+
+void nsDBFolderInfo::ChangeImapTotalPendingMessages(int32_t delta) {
+ m_totalPendingMessages += delta;
+ SetInt32PropertyWithToken(m_totalPendingMessagesColumnToken,
+ m_totalPendingMessages);
+}
+
+NS_IMETHODIMP
+nsDBFolderInfo::GetImapUnreadPendingMessages(int32_t* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_unreadPendingMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetImapUnreadPendingMessages(
+ int32_t numUnreadPendingMessages) {
+ m_unreadPendingMessages = numUnreadPendingMessages;
+ return SetUint32PropertyWithToken(m_unreadPendingMessagesColumnToken,
+ m_unreadPendingMessages);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetImapTotalPendingMessages(
+ int32_t numTotalPendingMessages) {
+ m_totalPendingMessages = numTotalPendingMessages;
+ return SetUint32PropertyWithToken(m_totalPendingMessagesColumnToken,
+ m_totalPendingMessages);
+}
+
+void nsDBFolderInfo::ChangeImapUnreadPendingMessages(int32_t delta) {
+ m_unreadPendingMessages += delta;
+ SetInt32PropertyWithToken(m_unreadPendingMessagesColumnToken,
+ m_unreadPendingMessages);
+}
+
+/* attribute nsMsgViewTypeValue viewType; */
+NS_IMETHODIMP nsDBFolderInfo::GetViewType(nsMsgViewTypeValue* aViewType) {
+ uint32_t viewTypeValue;
+ nsresult rv = GetUint32Property("viewType", nsMsgViewType::eShowAllThreads,
+ &viewTypeValue);
+ *aViewType = viewTypeValue;
+ return rv;
+}
+NS_IMETHODIMP nsDBFolderInfo::SetViewType(nsMsgViewTypeValue aViewType) {
+ return SetUint32Property("viewType", aViewType);
+}
+
+/* attribute nsMsgViewFlagsTypeValue viewFlags; */
+NS_IMETHODIMP nsDBFolderInfo::GetViewFlags(
+ nsMsgViewFlagsTypeValue* aViewFlags) {
+ nsMsgViewFlagsTypeValue defaultViewFlags;
+ nsresult rv = m_mdb->GetDefaultViewFlags(&defaultViewFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t viewFlagsValue;
+ rv = GetUint32Property("viewFlags", defaultViewFlags, &viewFlagsValue);
+ *aViewFlags = viewFlagsValue;
+ return rv;
+}
+NS_IMETHODIMP nsDBFolderInfo::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) {
+ return SetUint32Property("viewFlags", aViewFlags);
+}
+
+/* attribute nsMsgViewSortTypeValue sortType; */
+NS_IMETHODIMP nsDBFolderInfo::GetSortType(nsMsgViewSortTypeValue* aSortType) {
+ nsMsgViewSortTypeValue defaultSortType;
+ nsresult rv = m_mdb->GetDefaultSortType(&defaultSortType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t sortTypeValue;
+ rv = GetUint32Property("sortType", defaultSortType, &sortTypeValue);
+ *aSortType = sortTypeValue;
+ return rv;
+}
+NS_IMETHODIMP nsDBFolderInfo::SetSortType(nsMsgViewSortTypeValue aSortType) {
+ return SetUint32Property("sortType", aSortType);
+}
+
+/* attribute nsMsgViewSortOrderValue sortOrder; */
+NS_IMETHODIMP nsDBFolderInfo::GetSortOrder(
+ nsMsgViewSortOrderValue* aSortOrder) {
+ nsMsgViewSortOrderValue defaultSortOrder;
+ nsresult rv = m_mdb->GetDefaultSortOrder(&defaultSortOrder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t sortOrderValue;
+ rv = GetUint32Property("sortOrder", defaultSortOrder, &sortOrderValue);
+ *aSortOrder = sortOrderValue;
+ return rv;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetSortOrder(nsMsgViewSortOrderValue aSortOrder) {
+ return SetUint32Property("sortOrder", aSortOrder);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetKnownArtsSet(const char* newsArtSet) {
+ return m_mdb->SetProperty(m_mdbRow, kKnownArtsSetColumnName, newsArtSet);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetKnownArtsSet(char** newsArtSet) {
+ return m_mdb->GetProperty(m_mdbRow, kKnownArtsSetColumnName, newsArtSet);
+}
+
+// get arbitrary property, aka row cell value.
+NS_IMETHODIMP nsDBFolderInfo::GetProperty(const char* propertyName,
+ nsAString& resultProperty) {
+ return m_mdb->GetPropertyAsNSString(m_mdbRow, propertyName, resultProperty);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetCharProperty(
+ const char* aPropertyName, const nsACString& aPropertyValue) {
+ return m_mdb->SetProperty(m_mdbRow, aPropertyName,
+ PromiseFlatCString(aPropertyValue).get());
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetCharProperty(const char* propertyName,
+ nsACString& resultProperty) {
+ nsCString result;
+ nsresult rv =
+ m_mdb->GetProperty(m_mdbRow, propertyName, getter_Copies(result));
+ if (NS_SUCCEEDED(rv)) resultProperty.Assign(result);
+ return rv;
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetUint32Property(const char* propertyName,
+ uint32_t propertyValue) {
+ return m_mdb->SetUint32Property(m_mdbRow, propertyName, propertyValue);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetInt64Property(const char* propertyName,
+ int64_t propertyValue) {
+ return m_mdb->SetUint64Property(m_mdbRow, propertyName,
+ (uint64_t)propertyValue);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetProperty(const char* propertyName,
+ const nsAString& propertyStr) {
+ return m_mdb->SetPropertyFromNSString(m_mdbRow, propertyName, propertyStr);
+}
+
+nsresult nsDBFolderInfo::SetPropertyWithToken(mdb_token aProperty,
+ const nsAString& propertyStr) {
+ return m_mdb->SetNSStringPropertyWithToken(m_mdbRow, aProperty, propertyStr);
+}
+
+nsresult nsDBFolderInfo::SetUint32PropertyWithToken(mdb_token aProperty,
+ uint32_t propertyValue) {
+ return m_mdb->UInt32ToRowCellColumn(m_mdbRow, aProperty, propertyValue);
+}
+
+nsresult nsDBFolderInfo::SetInt64PropertyWithToken(mdb_token aProperty,
+ int64_t propertyValue) {
+ return m_mdb->UInt64ToRowCellColumn(m_mdbRow, aProperty,
+ (uint64_t)propertyValue);
+}
+
+nsresult nsDBFolderInfo::SetInt32PropertyWithToken(mdb_token aProperty,
+ int32_t propertyValue) {
+ nsAutoString propertyStr;
+ propertyStr.AppendInt(propertyValue, 16);
+ return SetPropertyWithToken(aProperty, propertyStr);
+}
+
+nsresult nsDBFolderInfo::GetPropertyWithToken(mdb_token aProperty,
+ nsAString& resultProperty) {
+ return m_mdb->RowCellColumnTonsString(m_mdbRow, aProperty, resultProperty);
+}
+
+nsresult nsDBFolderInfo::GetUint32PropertyWithToken(mdb_token aProperty,
+ uint32_t& propertyValue,
+ uint32_t defaultValue) {
+ return m_mdb->RowCellColumnToUInt32(m_mdbRow, aProperty, propertyValue,
+ defaultValue);
+}
+
+nsresult nsDBFolderInfo::GetInt32PropertyWithToken(mdb_token aProperty,
+ int32_t& propertyValue,
+ int32_t defaultValue) {
+ return m_mdb->RowCellColumnToUInt32(m_mdbRow, aProperty,
+ (uint32_t&)propertyValue, defaultValue);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetUint32Property(const char* propertyName,
+ uint32_t defaultValue,
+ uint32_t* propertyValue) {
+ return m_mdb->GetUint32Property(m_mdbRow, propertyName, propertyValue,
+ defaultValue);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetInt64Property(const char* propertyName,
+ int64_t defaultValue,
+ int64_t* propertyValue) {
+ return m_mdb->GetUint64Property(m_mdbRow, propertyName,
+ (uint64_t*)&propertyValue, defaultValue);
+}
+
+nsresult nsDBFolderInfo::GetInt64PropertyWithToken(mdb_token aProperty,
+ int64_t& propertyValue,
+ int64_t defaultValue) {
+ return m_mdb->RowCellColumnToUInt64(m_mdbRow, aProperty,
+ (uint64_t*)&propertyValue, defaultValue);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetBooleanProperty(const char* propertyName,
+ bool defaultValue,
+ bool* propertyValue) {
+ uint32_t defaultUint32Value = (defaultValue) ? 1 : 0;
+ uint32_t returnValue;
+ nsresult rv = m_mdb->GetUint32Property(m_mdbRow, propertyName, &returnValue,
+ defaultUint32Value);
+ *propertyValue = (returnValue != 0);
+ return rv;
+}
+NS_IMETHODIMP nsDBFolderInfo::SetBooleanProperty(const char* propertyName,
+ bool propertyValue) {
+ return m_mdb->SetUint32Property(m_mdbRow, propertyName,
+ propertyValue ? 1 : 0);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::GetFolderName(nsACString& folderName) {
+ return GetCharProperty("folderName", folderName);
+}
+
+NS_IMETHODIMP nsDBFolderInfo::SetFolderName(const nsACString& folderName) {
+ return SetCharProperty("folderName", folderName);
+}
+
+class nsTransferDBFolderInfo : public nsDBFolderInfo {
+ public:
+ nsTransferDBFolderInfo();
+ virtual ~nsTransferDBFolderInfo();
+ // parallel arrays of properties and values
+ nsTArray<nsCString> m_properties;
+ nsTArray<nsCString> m_values;
+};
+
+nsTransferDBFolderInfo::nsTransferDBFolderInfo() : nsDBFolderInfo(nullptr) {}
+
+nsTransferDBFolderInfo::~nsTransferDBFolderInfo() {}
+
+/* void GetTransferInfo (out nsIDBFolderInfo transferInfo); */
+NS_IMETHODIMP nsDBFolderInfo::GetTransferInfo(nsIDBFolderInfo** transferInfo) {
+ NS_ENSURE_ARG_POINTER(transferInfo);
+ NS_ENSURE_STATE(m_mdbRow);
+
+ RefPtr<nsTransferDBFolderInfo> newInfo = new nsTransferDBFolderInfo;
+
+ mdb_count numCells;
+ mdbYarn cellYarn;
+ mdb_column cellColumn;
+ char columnName[100];
+ mdbYarn cellName = {columnName, 0, sizeof(columnName), 0, 0, nullptr};
+
+ m_mdbRow->GetCount(m_mdb->GetEnv(), &numCells);
+ // iterate over the cells in the dbfolderinfo remembering attribute names and
+ // values.
+ for (mdb_count cellIndex = 0; cellIndex < numCells; cellIndex++) {
+ nsresult err = m_mdbRow->SeekCellYarn(m_mdb->GetEnv(), cellIndex,
+ &cellColumn, nullptr);
+ if (NS_SUCCEEDED(err)) {
+ err = m_mdbRow->AliasCellYarn(m_mdb->GetEnv(), cellColumn, &cellYarn);
+ if (NS_SUCCEEDED(err)) {
+ m_mdb->GetStore()->TokenToString(m_mdb->GetEnv(), cellColumn,
+ &cellName);
+ newInfo->m_values.AppendElement(
+ Substring((const char*)cellYarn.mYarn_Buf,
+ (const char*)cellYarn.mYarn_Buf + cellYarn.mYarn_Fill));
+ newInfo->m_properties.AppendElement(
+ Substring((const char*)cellName.mYarn_Buf,
+ (const char*)cellName.mYarn_Buf + cellName.mYarn_Fill));
+ }
+ }
+ }
+
+ newInfo.forget(transferInfo);
+ return NS_OK;
+}
+
+/* void InitFromTransferInfo (in nsIDBFolderInfo transferInfo); */
+NS_IMETHODIMP nsDBFolderInfo::InitFromTransferInfo(
+ nsIDBFolderInfo* aTransferInfo) {
+ NS_ENSURE_ARG(aTransferInfo);
+
+ nsTransferDBFolderInfo* transferInfo =
+ static_cast<nsTransferDBFolderInfo*>(aTransferInfo);
+
+ for (uint32_t i = 0; i < transferInfo->m_values.Length(); i++)
+ SetCharProperty(transferInfo->m_properties[i].get(),
+ transferInfo->m_values[i]);
+
+ LoadMemberVariables();
+ return NS_OK;
+}
diff --git a/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp b/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp
new file mode 100644
index 0000000000..7103d0c475
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <sys/stat.h>
+
+#include "msgCore.h"
+#include "nsImapMailDatabase.h"
+#include "nsDBFolderInfo.h"
+#include "nsMsgMessageFlags.h"
+
+const char* kPendingHdrsScope =
+ "ns:msg:db:row:scope:pending:all"; // scope for all offine ops table
+const char* kPendingHdrsTableKind = "ns:msg:db:table:kind:pending";
+struct mdbOid gAllPendingHdrsTableOID;
+
+nsImapMailDatabase::nsImapMailDatabase()
+ : m_pendingHdrsRowScopeToken(0), m_pendingHdrsTableKindToken(0) {}
+
+nsImapMailDatabase::~nsImapMailDatabase() {}
+
+NS_IMETHODIMP nsImapMailDatabase::GetSummaryValid(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (m_dbFolderInfo) {
+ uint32_t version;
+ m_dbFolderInfo->GetVersion(&version);
+ *aResult = (GetCurVersion() == version);
+ } else
+ *aResult = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailDatabase::SetSummaryValid(bool valid) {
+ if (m_dbFolderInfo) {
+ m_dbFolderInfo->SetVersion(valid ? GetCurVersion() : 0);
+ Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return NS_OK;
+}
+
+// We override this to avoid our parent class (nsMailDatabase)'s
+// grabbing of the folder semaphore, and bailing on failure.
+NS_IMETHODIMP nsImapMailDatabase::DeleteMessages(
+ nsTArray<nsMsgKey> const& nsMsgKeys, nsIDBChangeListener* instigator) {
+ return nsMsgDatabase::DeleteMessages(nsMsgKeys, instigator);
+}
+
+nsresult nsImapMailDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) {
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::Offline && m_dbFolderInfo) {
+ uint32_t size = 0;
+ (void)msgHdr->GetOfflineMessageSize(&size);
+ return m_dbFolderInfo->ChangeExpungedBytes(size);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailDatabase::ForceClosed() {
+ m_mdbAllPendingHdrsTable = nullptr;
+ return nsMailDatabase::ForceClosed();
+}
+
+nsresult nsImapMailDatabase::GetAllPendingHdrsTable() {
+ nsresult rv = NS_OK;
+ if (!m_mdbAllPendingHdrsTable)
+ rv = GetTableCreateIfMissing(kPendingHdrsScope, kPendingHdrsTableKind,
+ getter_AddRefs(m_mdbAllPendingHdrsTable),
+ m_pendingHdrsRowScopeToken,
+ m_pendingHdrsTableKindToken);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailDatabase::AddNewHdrToDB(nsIMsgDBHdr* newHdr,
+ bool notify) {
+ nsresult rv = nsMsgDatabase::AddNewHdrToDB(newHdr, notify);
+ if (NS_SUCCEEDED(rv)) rv = UpdatePendingAttributes(newHdr);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailDatabase::UpdatePendingAttributes(
+ nsIMsgDBHdr* aNewHdr) {
+ nsresult rv = GetAllPendingHdrsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+ mdb_count numPendingHdrs = 0;
+ m_mdbAllPendingHdrsTable->GetCount(GetEnv(), &numPendingHdrs);
+ if (numPendingHdrs > 0) {
+ mdbYarn messageIdYarn;
+ nsCOMPtr<nsIMdbRow> pendingRow;
+ mdbOid outRowId;
+
+ nsCString messageId;
+ aNewHdr->GetMessageId(getter_Copies(messageId));
+ messageIdYarn.mYarn_Buf = (void*)messageId.get();
+ messageIdYarn.mYarn_Fill = messageId.Length();
+ messageIdYarn.mYarn_Form = 0;
+ messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill;
+
+ m_mdbStore->FindRow(GetEnv(), m_pendingHdrsRowScopeToken,
+ m_messageIdColumnToken, &messageIdYarn, &outRowId,
+ getter_AddRefs(pendingRow));
+ if (pendingRow) {
+ mdb_count numCells;
+ mdbYarn cellYarn;
+ mdb_column cellColumn;
+ uint32_t existingFlags;
+
+ pendingRow->GetCount(GetEnv(), &numCells);
+ aNewHdr->GetFlags(&existingFlags);
+ // iterate over the cells in the pending hdr setting properties on the
+ // aNewHdr. we skip cell 0, which is the messageId;
+ nsMsgHdr* msgHdr =
+ static_cast<nsMsgHdr*>(aNewHdr); // closed system, cast ok
+ nsIMdbRow* row = msgHdr->GetMDBRow();
+ for (mdb_count cellIndex = 1; cellIndex < numCells; cellIndex++) {
+ nsresult err =
+ pendingRow->SeekCellYarn(GetEnv(), cellIndex, &cellColumn, nullptr);
+ if (NS_SUCCEEDED(err)) {
+ err = pendingRow->AliasCellYarn(GetEnv(), cellColumn, &cellYarn);
+ if (NS_SUCCEEDED(err)) {
+ if (row) row->AddColumn(GetEnv(), cellColumn, &cellYarn);
+ }
+ }
+ }
+ // We might have changed some cached values, so force a refresh.
+ msgHdr->ClearCachedValues();
+ uint32_t resultFlags;
+ msgHdr->OrFlags(existingFlags, &resultFlags);
+ m_mdbAllPendingHdrsTable->CutRow(GetEnv(), pendingRow);
+ pendingRow->CutAllColumns(GetEnv());
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapMailDatabase::GetRowForPendingHdr(nsIMsgDBHdr* pendingHdr,
+ nsIMdbRow** row) {
+ nsresult rv = GetAllPendingHdrsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mdbYarn messageIdYarn;
+ nsCOMPtr<nsIMdbRow> pendingRow;
+ mdbOid outRowId;
+ nsCString messageId;
+ pendingHdr->GetMessageId(getter_Copies(messageId));
+ messageIdYarn.mYarn_Buf = (void*)messageId.get();
+ messageIdYarn.mYarn_Fill = messageId.Length();
+ messageIdYarn.mYarn_Form = 0;
+ messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill;
+
+ rv = m_mdbStore->FindRow(GetEnv(), m_pendingHdrsRowScopeToken,
+ m_messageIdColumnToken, &messageIdYarn, &outRowId,
+ getter_AddRefs(pendingRow));
+
+ if (!pendingRow)
+ rv = m_mdbStore->NewRow(GetEnv(), m_pendingHdrsRowScopeToken,
+ getter_AddRefs(pendingRow));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (pendingRow) {
+ // now we need to add cells to the row to remember the messageid, property
+ // and property value, and flags. Then, when hdrs are added to the db, we'll
+ // check if they have a matching message-id, and if so, set the property and
+ // flags
+ // XXX we already fetched messageId from the pending hdr, could it have
+ // changed by the time we get here?
+ nsCString messageId;
+ pendingHdr->GetMessageId(getter_Copies(messageId));
+ // we're just going to ignore messages without a message-id. They should be
+ // rare. If SPAM messages often didn't have message-id's, they'd be filtered
+ // on the server, most likely, and spammers would then start putting in
+ // message-id's.
+ if (!messageId.IsEmpty()) {
+ extern const char* kMessageIdColumnName;
+ m_mdbAllPendingHdrsTable->AddRow(GetEnv(), pendingRow);
+ // make sure this is the first cell so that when we ignore the first
+ // cell in nsImapMailDatabase::AddNewHdrToDB, we're ignoring the right one
+ (void)SetProperty(pendingRow, kMessageIdColumnName, messageId.get());
+ pendingRow.forget(row);
+ } else
+ return NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailDatabase::SetAttributeOnPendingHdr(
+ nsIMsgDBHdr* pendingHdr, const char* property, const char* propertyVal) {
+ NS_ENSURE_ARG_POINTER(pendingHdr);
+ nsCOMPtr<nsIMdbRow> pendingRow;
+ nsresult rv = GetRowForPendingHdr(pendingHdr, getter_AddRefs(pendingRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetProperty(pendingRow, property, propertyVal);
+}
+
+NS_IMETHODIMP
+nsImapMailDatabase::SetUint32AttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr,
+ const char* property,
+ uint32_t propertyVal) {
+ NS_ENSURE_ARG_POINTER(pendingHdr);
+ nsCOMPtr<nsIMdbRow> pendingRow;
+ nsresult rv = GetRowForPendingHdr(pendingHdr, getter_AddRefs(pendingRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetUint32Property(pendingRow, property, propertyVal);
+}
+
+NS_IMETHODIMP
+nsImapMailDatabase::SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr,
+ const char* aProperty,
+ uint64_t aPropertyVal) {
+ NS_ENSURE_ARG_POINTER(aPendingHdr);
+ nsCOMPtr<nsIMdbRow> pendingRow;
+ nsresult rv = GetRowForPendingHdr(aPendingHdr, getter_AddRefs(pendingRow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetUint64Property(pendingRow, aProperty, aPropertyVal);
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp b/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp
new file mode 100644
index 0000000000..c07294785a
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp
@@ -0,0 +1,380 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMailDatabase.h"
+#include "nsDBFolderInfo.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsNetUtil.h"
+#include "nsMsgOfflineImapOperation.h"
+#include "nsMsgFolderFlags.h"
+#include "mozilla/Logging.h"
+#include "prprf.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsSimpleEnumerator.h"
+
+using namespace mozilla;
+
+extern LazyLogModule IMAPOffline; // defined in nsMsgOfflineImapOperation.cpp
+
+// scope for all offine ops table
+const char* kOfflineOpsScope = "ns:msg:db:row:scope:ops:all";
+const char* kOfflineOpsTableKind = "ns:msg:db:table:kind:ops";
+struct mdbOid gAllOfflineOpsTableOID;
+
+nsMailDatabase::nsMailDatabase() : m_reparse(false) {
+ m_mdbAllOfflineOpsTable = nullptr;
+ m_offlineOpsRowScopeToken = 0;
+ m_offlineOpsTableKindToken = 0;
+}
+
+nsMailDatabase::~nsMailDatabase() {}
+
+// caller passes in upgrading==true if they want back a db even if the db is out
+// of date. If so, they'll extract out the interesting info from the db, close
+// it, delete it, and then try to open the db again, prior to reparsing.
+nsresult nsMailDatabase::Open(nsMsgDBService* aDBService, nsIFile* aSummaryFile,
+ bool aCreate, bool aUpgrading) {
+#ifdef DEBUG
+ nsString leafName;
+ aSummaryFile->GetLeafName(leafName);
+ if (!StringEndsWith(leafName, NS_LITERAL_STRING_FROM_CSTRING(SUMMARY_SUFFIX),
+ nsCaseInsensitiveStringComparator))
+ NS_ERROR("non summary file passed into open");
+#endif
+ return nsMsgDatabase::Open(aDBService, aSummaryFile, aCreate, aUpgrading);
+}
+
+NS_IMETHODIMP nsMailDatabase::ForceClosed() {
+ m_mdbAllOfflineOpsTable = nullptr;
+ return nsMsgDatabase::ForceClosed();
+}
+
+// get this on demand so that only db's that have offline ops will
+// create the table.
+nsresult nsMailDatabase::GetAllOfflineOpsTable() {
+ nsresult rv = NS_OK;
+ if (!m_mdbAllOfflineOpsTable)
+ rv = GetTableCreateIfMissing(kOfflineOpsScope, kOfflineOpsTableKind,
+ getter_AddRefs(m_mdbAllOfflineOpsTable),
+ m_offlineOpsRowScopeToken,
+ m_offlineOpsTableKindToken);
+ return rv;
+}
+
+NS_IMETHODIMP nsMailDatabase::DeleteMessages(
+ nsTArray<nsMsgKey> const& nsMsgKeys, nsIDBChangeListener* instigator) {
+ nsresult rv;
+ if (m_folder) {
+ bool isLocked;
+ m_folder->GetLocked(&isLocked);
+ if (isLocked) {
+ NS_ASSERTION(false, "Some other operation is in progress");
+ return NS_MSG_FOLDER_BUSY;
+ }
+ }
+
+ rv = nsMsgDatabase::DeleteMessages(nsMsgKeys, instigator);
+ SetSummaryValid(true);
+ return rv;
+}
+
+NS_IMETHODIMP nsMailDatabase::GetSummaryValid(bool* aResult) {
+ uint32_t version;
+ m_dbFolderInfo->GetVersion(&version);
+ if (GetCurVersion() != version) {
+ *aResult = false;
+ return NS_OK;
+ }
+ if (!m_folder) {
+ // If the folder is not set, we just return without checking the validity
+ // of the summary file. For now, this is an expected condition when the
+ // message database is being opened from a URL in
+ // nsMailboxUrl::GetMsgHdrForKey() which calls
+ // nsMsgDBService::OpenMailDBFromFile() without a folder.
+ // Returning an error here would lead to the deletion of the MSF in the
+ // caller nsMsgDatabase::CheckForErrors().
+ *aResult = true;
+ return NS_OK;
+ }
+
+ // If this is a virtual folder, there is no storage.
+ bool isVirtual = false;
+ m_folder->GetFlag(nsMsgFolderFlags::Virtual, &isVirtual);
+ if (isVirtual) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = m_folder->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->IsSummaryFileValid(m_folder, this, aResult);
+}
+
+NS_IMETHODIMP nsMailDatabase::SetSummaryValid(bool aValid) {
+ nsMsgDatabase::SetSummaryValid(aValid);
+
+ if (!m_folder) return NS_ERROR_NULL_POINTER;
+
+ // If this is a virtual folder, there is no storage.
+ bool flag;
+ m_folder->GetFlag(nsMsgFolderFlags::Virtual, &flag);
+ if (flag) return NS_OK;
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = m_folder->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->SetSummaryFileValid(m_folder, this, aValid);
+}
+
+NS_IMETHODIMP nsMailDatabase::RemoveOfflineOp(nsIMsgOfflineImapOperation* op) {
+ nsresult rv = GetAllOfflineOpsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!op || !m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER;
+ nsMsgOfflineImapOperation* offlineOp =
+ static_cast<nsMsgOfflineImapOperation*>(
+ op); // closed system, so this is ok
+ nsIMdbRow* row = offlineOp->GetMDBRow();
+ rv = m_mdbAllOfflineOpsTable->CutRow(GetEnv(), row);
+ row->CutAllColumns(GetEnv());
+ return rv;
+}
+
+NS_IMETHODIMP nsMailDatabase::GetOfflineOpForKey(
+ nsMsgKey msgKey, bool create, nsIMsgOfflineImapOperation** offlineOp) {
+ mdb_bool hasOid;
+ mdbOid rowObjectId;
+ nsresult err;
+
+ nsresult rv = GetAllOfflineOpsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!offlineOp || !m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER;
+
+ *offlineOp = NULL;
+
+ rowObjectId.mOid_Id = msgKey;
+ rowObjectId.mOid_Scope = m_offlineOpsRowScopeToken;
+ err = m_mdbAllOfflineOpsTable->HasOid(GetEnv(), &rowObjectId, &hasOid);
+ if (NS_SUCCEEDED(err) && m_mdbStore && (hasOid || create)) {
+ nsCOMPtr<nsIMdbRow> offlineOpRow;
+ err = m_mdbStore->GetRow(GetEnv(), &rowObjectId,
+ getter_AddRefs(offlineOpRow));
+
+ if (create) {
+ if (!offlineOpRow) {
+ err = m_mdbStore->NewRowWithOid(GetEnv(), &rowObjectId,
+ getter_AddRefs(offlineOpRow));
+ NS_ENSURE_SUCCESS(err, err);
+ }
+ if (offlineOpRow && !hasOid)
+ m_mdbAllOfflineOpsTable->AddRow(GetEnv(), offlineOpRow);
+ }
+
+ if (NS_SUCCEEDED(err) && offlineOpRow) {
+ NS_IF_ADDREF(*offlineOp =
+ new nsMsgOfflineImapOperation(this, offlineOpRow));
+ (*offlineOp)->SetMessageKey(msgKey);
+ }
+ if (!hasOid && m_dbFolderInfo) {
+ // set initial value for flags so we don't lose them.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ (*offlineOp)->SetNewFlags(flags);
+ }
+ int32_t newFlags;
+ m_dbFolderInfo->OrFlags(nsMsgFolderFlags::OfflineEvents, &newFlags);
+ }
+ }
+
+ return err;
+}
+
+NS_IMETHODIMP nsMailDatabase::ListAllOfflineOpIds(
+ nsTArray<nsMsgKey>& offlineOpIds) {
+ nsresult rv = GetAllOfflineOpsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsIMdbTableRowCursor* rowCursor;
+
+ if (m_mdbAllOfflineOpsTable) {
+ nsresult err =
+ m_mdbAllOfflineOpsTable->GetTableRowCursor(GetEnv(), -1, &rowCursor);
+ while (NS_SUCCEEDED(err) && rowCursor) {
+ mdbOid outOid;
+ mdb_pos outPos;
+
+ err = rowCursor->NextRowOid(GetEnv(), &outOid, &outPos);
+ // is this right? Mork is returning a 0 id, but that should valid.
+ if (outPos < 0 || outOid.mOid_Id == (mdb_id)-1) break;
+ if (NS_SUCCEEDED(err)) {
+ offlineOpIds.AppendElement(outOid.mOid_Id);
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> offlineOp;
+ GetOfflineOpForKey(outOid.mOid_Id, false, getter_AddRefs(offlineOp));
+ if (offlineOp) {
+ nsMsgOfflineImapOperation* logOp =
+ static_cast<nsMsgOfflineImapOperation*>(
+ static_cast<nsIMsgOfflineImapOperation*>(offlineOp.get()));
+ if (logOp) logOp->Log();
+ }
+ }
+ }
+ }
+ // TODO: would it cause a problem to replace this with "rv = err;" ?
+ rv = (NS_SUCCEEDED(err)) ? NS_OK : NS_ERROR_FAILURE;
+ rowCursor->Release();
+ }
+
+ offlineOpIds.Sort();
+ return rv;
+}
+
+NS_IMETHODIMP nsMailDatabase::ListAllOfflineDeletes(
+ nsTArray<nsMsgKey>& offlineDeletes) {
+ nsresult rv = GetAllOfflineOpsTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsIMdbTableRowCursor* rowCursor;
+ if (m_mdbAllOfflineOpsTable) {
+ nsresult err =
+ m_mdbAllOfflineOpsTable->GetTableRowCursor(GetEnv(), -1, &rowCursor);
+ while (NS_SUCCEEDED(err) && rowCursor) {
+ mdbOid outOid;
+ mdb_pos outPos;
+ nsIMdbRow* offlineOpRow;
+
+ err = rowCursor->NextRow(GetEnv(), &offlineOpRow, &outPos);
+ // is this right? Mork is returning a 0 id, but that should valid.
+ if (outPos < 0 || offlineOpRow == nullptr) break;
+ if (NS_SUCCEEDED(err)) {
+ offlineOpRow->GetOid(GetEnv(), &outOid);
+ RefPtr<nsIMsgOfflineImapOperation> offlineOp =
+ new nsMsgOfflineImapOperation(this, offlineOpRow);
+ imapMessageFlagsType newFlags;
+ nsOfflineImapOperationType opType;
+
+ offlineOp->GetOperation(&opType);
+ offlineOp->GetNewFlags(&newFlags);
+ if (opType & nsIMsgOfflineImapOperation::kMsgMoved ||
+ ((opType & nsIMsgOfflineImapOperation::kFlagsChanged) &&
+ (newFlags & nsIMsgOfflineImapOperation::kMsgMarkedDeleted)))
+ offlineDeletes.AppendElement(outOid.mOid_Id);
+
+ offlineOpRow->Release();
+ }
+ }
+ // TODO: would it cause a problem to replace this with "rv = err;" ?
+ rv = (NS_SUCCEEDED(err)) ? NS_OK : NS_ERROR_FAILURE;
+ rowCursor->Release();
+ }
+ return rv;
+}
+
+// This is used to remember that the db is out of sync with the mail folder
+// and needs to be regenerated.
+void nsMailDatabase::SetReparse(bool reparse) { m_reparse = reparse; }
+
+class nsMsgOfflineOpEnumerator : public nsSimpleEnumerator {
+ public:
+ const nsID& DefaultInterface() override {
+ return NS_GET_IID(nsIMsgOfflineImapOperation);
+ }
+
+ // nsISimpleEnumerator methods:
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ explicit nsMsgOfflineOpEnumerator(nsMailDatabase* db);
+
+ protected:
+ ~nsMsgOfflineOpEnumerator() override;
+ nsresult GetRowCursor();
+ nsresult PrefetchNext();
+ nsMailDatabase* mDB;
+ nsIMdbTableRowCursor* mRowCursor;
+ nsCOMPtr<nsIMsgOfflineImapOperation> mResultOp;
+ bool mDone;
+ bool mNextPrefetched;
+};
+
+nsMsgOfflineOpEnumerator::nsMsgOfflineOpEnumerator(nsMailDatabase* db)
+ : mDB(db), mRowCursor(nullptr), mDone(false) {
+ NS_ADDREF(mDB);
+ mNextPrefetched = false;
+}
+
+nsMsgOfflineOpEnumerator::~nsMsgOfflineOpEnumerator() {
+ NS_IF_RELEASE(mRowCursor);
+ NS_RELEASE(mDB);
+}
+
+nsresult nsMsgOfflineOpEnumerator::GetRowCursor() {
+ nsresult rv = NS_OK;
+ mDone = false;
+
+ if (!mDB || !mDB->m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER;
+
+ rv = mDB->m_mdbAllOfflineOpsTable->GetTableRowCursor(mDB->GetEnv(), -1,
+ &mRowCursor);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgOfflineOpEnumerator::GetNext(nsISupports** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ nsresult rv = NS_OK;
+ if (!mNextPrefetched) rv = PrefetchNext();
+ if (NS_SUCCEEDED(rv)) {
+ if (mResultOp) {
+ NS_ADDREF(*aItem = mResultOp);
+ mNextPrefetched = false;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgOfflineOpEnumerator::PrefetchNext() {
+ nsresult rv = NS_OK;
+ nsIMdbRow* offlineOpRow;
+ mdb_pos rowPos;
+
+ if (!mRowCursor) {
+ rv = GetRowCursor();
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ rv = mRowCursor->NextRow(mDB->GetEnv(), &offlineOpRow, &rowPos);
+ if (!offlineOpRow) {
+ mDone = true;
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mDone = true;
+ return rv;
+ }
+
+ nsIMsgOfflineImapOperation* op =
+ new nsMsgOfflineImapOperation(mDB, offlineOpRow);
+ mResultOp = op;
+ if (!op) return NS_ERROR_OUT_OF_MEMORY;
+
+ if (mResultOp) {
+ mNextPrefetched = true;
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgOfflineOpEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!mNextPrefetched) PrefetchNext();
+ *aResult = !mDone;
+ return NS_OK;
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp b/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp
new file mode 100644
index 0000000000..779ca400a9
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp
@@ -0,0 +1,4730 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// this file implements the nsMsgDatabase interface using the MDB Interface.
+
+#include "nscore.h"
+#include "msgCore.h"
+#include "nsIFile.h"
+#include "nsMailDatabase.h"
+#include "nsDBFolderInfo.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsMsgThread.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMdbFactoryFactory.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+#include "prprf.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgDBView.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "MailNewsTypes2.h"
+#include "nsMsgUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsPrintfCString.h"
+#include "nsMsgDatabaseEnumerators.h"
+#include "nsIMemoryReporter.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsMailDirServiceDefs.h"
+#include "mozilla/Components.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/intl/LocaleService.h"
+
+using namespace mozilla::mailnews;
+using namespace mozilla;
+
+#if defined(DEBUG_sspitzer_) || defined(DEBUG_seth_)
+# define DEBUG_MSGKEYSET 1
+#endif
+
+#define MSG_HASH_SIZE 512
+
+// This will be used on discovery, since we don't know total.
+const int32_t kMaxHdrsInCache = 512;
+
+// special keys
+static const nsMsgKey kAllMsgHdrsTableKey = 1;
+static const nsMsgKey kTableKeyForThreadOne = 0xfffffffe;
+static const nsMsgKey kAllThreadsTableKey = 0xfffffffd;
+static const nsMsgKey kFirstPseudoKey = 0xfffffff0;
+static const nsMsgKey kIdStartOfFake = 0xffffff80;
+static const nsMsgKey kForceReparseKey = 0xfffffff0;
+
+LazyLogModule DBLog("MsgDB");
+
+PRTime nsMsgDatabase::gLastUseTime;
+
+/**
+ * mozilla::intl APIs require sizeable buffers. This class abstracts over
+ * the nsTArray.
+ */
+class nsTArrayU8Buffer {
+ public:
+ using CharType = uint8_t;
+
+ // Do not allow copy or move. Move could be added in the future if needed.
+ nsTArrayU8Buffer(const nsTArrayU8Buffer&) = delete;
+ nsTArrayU8Buffer& operator=(const nsTArrayU8Buffer&) = delete;
+
+ explicit nsTArrayU8Buffer(nsTArray<CharType>& aArray) : mArray(aArray) {}
+
+ /**
+ * Ensures the buffer has enough space to accommodate |size| elements.
+ */
+ [[nodiscard]] bool reserve(size_t size) {
+ mArray.SetCapacity(size);
+ // nsTArray::SetCapacity returns void, return true to keep the API the same
+ // as the other Buffer implementations.
+ return true;
+ }
+
+ /**
+ * Returns the raw data inside the buffer.
+ */
+ CharType* data() { return mArray.Elements(); }
+
+ /**
+ * Returns the count of elements written into the buffer.
+ */
+ size_t length() const { return mArray.Length(); }
+
+ /**
+ * Returns the buffer's overall capacity.
+ */
+ size_t capacity() const { return mArray.Capacity(); }
+
+ /**
+ * Resizes the buffer to the given amount of written elements.
+ */
+ void written(size_t amount) {
+ MOZ_ASSERT(amount <= mArray.Capacity());
+ // This sets |mArray|'s internal size so that it matches how much was
+ // written. This is necessary because the write happens across FFI
+ // boundaries.
+ mArray.SetLengthAndRetainStorage(amount);
+ }
+
+ private:
+ nsTArray<CharType>& mArray;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgDBService, nsIMsgDBService)
+
+nsMsgDBService::nsMsgDBService() {}
+
+nsMsgDBService::~nsMsgDBService() {
+#ifdef DEBUG
+ // If you hit this warning, it means that some code is holding onto
+ // a db at shutdown.
+ NS_WARNING_ASSERTION(!m_dbCache.Length(), "some msg dbs left open");
+# ifndef MOZILLA_OFFICIAL
+ // Only print this on local builds since it causes crashes,
+ // see bug 1468691, bug 1377692 and bug 1342858.
+ for (uint32_t i = 0; i < m_dbCache.Length(); i++) {
+ nsMsgDatabase* pMessageDB = m_dbCache.ElementAt(i);
+ if (pMessageDB)
+ printf("db left open %s\n",
+ pMessageDB->m_dbFile->HumanReadablePath().get());
+ }
+# endif
+#endif
+}
+
+NS_IMETHODIMP nsMsgDBService::OpenFolderDB(nsIMsgFolder* aFolder,
+ bool aLeaveInvalidDB,
+ nsIMsgDatabase** _retval) {
+ NS_ENSURE_ARG(aFolder);
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ nsresult rv = aFolder->GetServer(getter_AddRefs(incomingServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> summaryFilePath;
+ rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgDatabase* cacheDB = FindInCache(summaryFilePath);
+ if (cacheDB) {
+ // this db could have ended up in the folder cache w/o an m_folder pointer
+ // via OpenMailDBFromFile. If so, take this chance to fix the folder.
+ if (!cacheDB->m_folder) cacheDB->m_folder = aFolder;
+ cacheDB->RememberLastUseTime();
+ *_retval = cacheDB; // FindInCache already addRefed.
+ // if m_thumb is set, someone is asynchronously opening the db. But our
+ // caller wants to synchronously open it, so just do it.
+ if (cacheDB->m_thumb)
+ return cacheDB->Open(this, summaryFilePath, false, aLeaveInvalidDB);
+ return NS_OK;
+ }
+
+ nsCString localDatabaseType;
+ incomingServer->GetLocalDatabaseType(localDatabaseType);
+ nsAutoCString dbContractID("@mozilla.org/nsMsgDatabase/msgDB-");
+ dbContractID.Append(localDatabaseType.get());
+ nsCOMPtr<nsIMsgDatabase> msgDB = do_CreateInstance(dbContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't try to create the database yet--let the createNewDB call do that.
+ nsMsgDatabase* msgDatabase = static_cast<nsMsgDatabase*>(msgDB.get());
+ msgDatabase->m_folder = aFolder;
+ rv = msgDatabase->Open(this, summaryFilePath, false, aLeaveInvalidDB);
+ if (NS_FAILED(rv) && rv != NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) return rv;
+
+ NS_ADDREF(*_retval = msgDB);
+
+ if (NS_FAILED(rv)) {
+#ifdef DEBUG
+ // Doing these checks for debug only as we don't want to report certain
+ // errors in debug mode, but in release mode we wouldn't report them either
+
+ // These errors are expected.
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ return rv;
+
+ // If it isn't one of the expected errors, throw a warning.
+ NS_ENSURE_SUCCESS(rv, rv);
+#endif
+ return rv;
+ }
+
+ FinishDBOpen(aFolder, msgDatabase);
+ return rv;
+}
+
+/**
+ * When a db is opened, we need to hook up any pending listeners for
+ * that db, and notify them.
+ */
+void nsMsgDBService::HookupPendingListeners(nsIMsgDatabase* db,
+ nsIMsgFolder* folder) {
+ for (int32_t listenerIndex = 0;
+ listenerIndex < m_foldersPendingListeners.Count(); listenerIndex++) {
+ // check if we have a pending listener on this db, and if so, add it.
+ if (m_foldersPendingListeners[listenerIndex] == folder) {
+ db->AddListener(m_pendingListeners.ObjectAt(listenerIndex));
+ m_pendingListeners.ObjectAt(listenerIndex)->OnEvent(db, "DBOpened");
+ }
+ }
+}
+
+void nsMsgDBService::FinishDBOpen(nsIMsgFolder* aFolder,
+ nsMsgDatabase* aMsgDB) {
+ uint32_t folderFlags;
+ aFolder->GetFlags(&folderFlags);
+
+ if (!(folderFlags & nsMsgFolderFlags::Virtual) &&
+ aMsgDB->m_mdbAllMsgHeadersTable) {
+ mdb_count numHdrsInTable = 0;
+ int32_t numMessages;
+ aMsgDB->m_mdbAllMsgHeadersTable->GetCount(aMsgDB->GetEnv(),
+ &numHdrsInTable);
+ aMsgDB->m_dbFolderInfo->GetNumMessages(&numMessages);
+ if (numMessages != (int32_t)numHdrsInTable) aMsgDB->SyncCounts();
+ }
+ HookupPendingListeners(aMsgDB, aFolder);
+ aMsgDB->RememberLastUseTime();
+}
+
+//----------------------------------------------------------------------
+// FindInCache - this addrefs the db it finds.
+//----------------------------------------------------------------------
+nsMsgDatabase* nsMsgDBService::FindInCache(nsIFile* dbName) {
+ for (uint32_t i = 0; i < m_dbCache.Length(); i++) {
+ nsMsgDatabase* pMessageDB = m_dbCache[i];
+ if (pMessageDB->MatchDbName(dbName)) {
+ if (pMessageDB->m_mdbStore) // don't return db without store
+ {
+ NS_ADDREF(pMessageDB);
+ return pMessageDB;
+ }
+ }
+ }
+ return nullptr;
+}
+
+// This method is called when the caller is trying to create a db without
+// having a corresponding nsIMsgFolder object. This happens in a few
+// situations, including imap folder discovery, compacting local folders,
+// and copying local folders.
+NS_IMETHODIMP nsMsgDBService::OpenMailDBFromFile(nsIFile* aFolderName,
+ nsIMsgFolder* aFolder,
+ bool aCreate,
+ bool aLeaveInvalidDB,
+ nsIMsgDatabase** pMessageDB) {
+ if (!aFolderName) return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIFile> dbPath;
+ nsresult rv = GetSummaryFileLocation(aFolderName, getter_AddRefs(dbPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *pMessageDB = FindInCache(dbPath);
+ if (*pMessageDB) return NS_OK;
+
+ RefPtr<nsMailDatabase> msgDB = new nsMailDatabase;
+ NS_ENSURE_TRUE(msgDB, NS_ERROR_OUT_OF_MEMORY);
+ rv = msgDB->Open(this, dbPath, aCreate, aLeaveInvalidDB);
+ if (rv == NS_ERROR_FILE_NOT_FOUND) return rv;
+ NS_IF_ADDREF(*pMessageDB = msgDB);
+ if (aCreate && msgDB && rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) rv = NS_OK;
+ if (NS_SUCCEEDED(rv)) msgDB->m_folder = aFolder;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBService::CreateNewDB(nsIMsgFolder* aFolder,
+ nsIMsgDatabase** _retval) {
+ NS_ENSURE_ARG(aFolder);
+
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ nsresult rv = aFolder->GetServer(getter_AddRefs(incomingServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> summaryFilePath;
+ rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString localDatabaseType;
+ incomingServer->GetLocalDatabaseType(localDatabaseType);
+ nsAutoCString dbContractID("@mozilla.org/nsMsgDatabase/msgDB-");
+ dbContractID.Append(localDatabaseType.get());
+
+ nsCOMPtr<nsIMsgDatabase> msgDB = do_CreateInstance(dbContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgDatabase* msgDatabase = static_cast<nsMsgDatabase*>(msgDB.get());
+
+ msgDatabase->m_folder = aFolder;
+ rv = msgDatabase->Open(this, summaryFilePath, true, true);
+
+ // We are trying to create a new database, but that implies that it did not
+ // already exist. Open returns NS_MSG_ERROR_FOLDER_SUMMARY_MISSING for the
+ // successful creation of a new database. But if it existed for some
+ // reason, then we would get rv = NS_OK instead. That is a "failure"
+ // from our perspective, so we want to return a failure since we are not
+ // returning a valid database object.
+ NS_ENSURE_TRUE(rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING,
+ NS_SUCCEEDED(rv) ? NS_ERROR_FILE_ALREADY_EXISTS : rv);
+
+ NS_ADDREF(*_retval = msgDB);
+
+ HookupPendingListeners(msgDB, aFolder);
+
+ msgDatabase->RememberLastUseTime();
+
+ return NS_OK;
+}
+
+/* void registerPendingListener (in nsIMsgFolder aFolder, in nsIDBChangeListener
+ * aListener); */
+NS_IMETHODIMP nsMsgDBService::RegisterPendingListener(
+ nsIMsgFolder* aFolder, nsIDBChangeListener* aListener) {
+ // need to make sure we don't hold onto these forever. Maybe a shutdown
+ // listener? if there is a db open on this folder already, we should register
+ // the listener.
+ m_foldersPendingListeners.AppendObject(aFolder);
+ m_pendingListeners.AppendObject(aListener);
+ nsCOMPtr<nsIMsgDatabase> openDB;
+ CachedDBForFolder(aFolder, getter_AddRefs(openDB));
+ if (openDB) openDB->AddListener(aListener);
+ return NS_OK;
+}
+
+/* void unregisterPendingListener (in nsIDBChangeListener aListener); */
+NS_IMETHODIMP nsMsgDBService::UnregisterPendingListener(
+ nsIDBChangeListener* aListener) {
+ int32_t listenerIndex = m_pendingListeners.IndexOfObject(aListener);
+ if (listenerIndex != -1) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ CachedDBForFolder(m_foldersPendingListeners[listenerIndex],
+ getter_AddRefs(msgDB));
+ if (msgDB) msgDB->RemoveListener(aListener);
+ m_foldersPendingListeners.RemoveObjectAt(listenerIndex);
+ m_pendingListeners.RemoveObjectAt(listenerIndex);
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDBService::CachedDBForFolder(nsIMsgFolder* aFolder,
+ nsIMsgDatabase** aRetDB) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aRetDB);
+
+ nsCOMPtr<nsIFile> summaryFilePath;
+ nsresult rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aRetDB = FindInCache(summaryFilePath);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBService::ForceFolderDBClosed(nsIMsgFolder* aFolder) {
+ nsCOMPtr<nsIMsgDatabase> mailDB;
+ nsresult rv = CachedDBForFolder(aFolder, getter_AddRefs(mailDB));
+ if (mailDB) {
+ mailDB->ForceClosed();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBService::GetOpenDBs(
+ nsTArray<RefPtr<nsIMsgDatabase>>& aOpenDBs) {
+ aOpenDBs.Clear();
+ aOpenDBs.SetCapacity(m_dbCache.Length());
+ for (auto db : m_dbCache) {
+ aOpenDBs.AppendElement(db);
+ }
+ return NS_OK;
+}
+
+static bool gGotGlobalPrefs = false;
+static bool gThreadWithoutRe = true;
+static bool gStrictThreading = false;
+static bool gCorrectThreading = false;
+
+void nsMsgDatabase::GetGlobalPrefs() {
+ if (!gGotGlobalPrefs) {
+ GetBoolPref("mail.thread_without_re", &gThreadWithoutRe);
+ GetBoolPref("mail.strict_threading", &gStrictThreading);
+ GetBoolPref("mail.correct_threading", &gCorrectThreading);
+ gGotGlobalPrefs = true;
+ }
+}
+
+nsresult nsMsgDatabase::AddHdrToCache(
+ nsIMsgDBHdr* hdr, nsMsgKey key) // do we want key? We could get it from hdr
+{
+ if (m_bCacheHeaders) {
+ if (!m_cachedHeaders)
+ m_cachedHeaders = new PLDHashTable(
+ &gMsgDBHashTableOps, sizeof(struct MsgHdrHashElement), m_cacheSize);
+ if (m_cachedHeaders) {
+ if (key == nsMsgKey_None) hdr->GetMessageKey(&key);
+ if (m_cachedHeaders->EntryCount() > m_cacheSize) ClearHdrCache(true);
+ PLDHashEntryHdr* entry =
+ m_cachedHeaders->Add((void*)(uintptr_t)key, mozilla::fallible);
+ if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory
+
+ MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry);
+ element->mHdr = hdr;
+ element->mKey = key;
+ NS_ADDREF(hdr); // make the cache hold onto the header
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetMsgHdrCacheSize(uint32_t aSize) {
+ m_cacheSize = aSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetMsgHdrCacheSize(uint32_t* aSize) {
+ NS_ENSURE_ARG_POINTER(aSize);
+ *aSize = m_cacheSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetLastUseTime(PRTime* aTime) {
+ NS_ENSURE_ARG_POINTER(aTime);
+ *aTime = m_lastUseTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetLastUseTime(PRTime aTime) {
+ gLastUseTime = m_lastUseTime = aTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetDatabaseSize(int64_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv;
+ bool exists;
+ NS_ENSURE_TRUE(m_dbFile, NS_ERROR_NULL_POINTER);
+ rv = m_dbFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv)) {
+ if (exists)
+ rv = m_dbFile->GetFileSize(_retval);
+ else
+ *_retval = 0;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ClearCachedHdrs() {
+ ClearCachedObjects(false);
+#ifdef DEBUG_bienvenu1
+ if (mRefCnt > 1) {
+ NS_ASSERTION(false, "");
+ printf("someone's holding onto db - refs = %ld\n", mRefCnt);
+ }
+#endif
+ return NS_OK;
+}
+
+// Invalidate any outstanding message enumerators using this db.
+void nsMsgDatabase::InvalidateEnumerators() {
+ RefPtr<nsMsgDatabase> kungFuDeathGrip(this);
+ // Work in reverse, as the enumerators remove themselves from the list.
+ {
+ auto n = m_msgEnumerators.Length();
+ for (auto i = n; i > 0; --i) {
+ m_msgEnumerators[i - 1]->Invalidate();
+ }
+ }
+ // And again for thread enumerators.
+ {
+ auto n = m_threadEnumerators.Length();
+ for (auto i = n; i > 0; --i) {
+ m_threadEnumerators[i - 1]->Invalidate();
+ }
+ }
+}
+
+nsMsgThread* nsMsgDatabase::FindExistingThread(nsMsgKey threadId) {
+ uint32_t numThreads = m_threads.Length();
+ for (uint32_t i = 0; i < numThreads; i++)
+ if (m_threads[i]->m_threadKey == threadId) return m_threads[i];
+
+ return nullptr;
+}
+
+void nsMsgDatabase::ClearThreads() {
+ // clear out existing threads
+ nsTArray<nsMsgThread*> copyThreads;
+ copyThreads.SwapElements(m_threads);
+
+ uint32_t numThreads = copyThreads.Length();
+ for (uint32_t i = 0; i < numThreads; i++) copyThreads[i]->Clear();
+}
+
+void nsMsgDatabase::ClearCachedObjects(bool dbGoingAway) {
+ ClearHdrCache(false);
+#ifdef DEBUG_DavidBienvenu
+ if (m_headersInUse && m_headersInUse->EntryCount() > 0) {
+ NS_ASSERTION(false, "leaking headers");
+ printf("leaking %d headers in %s\n", m_headersInUse->EntryCount(),
+ m_dbFile->HumanReadablePath().get());
+ }
+#endif
+ m_cachedThread = nullptr;
+ m_cachedThreadId = nsMsgKey_None;
+ // We should only clear the use hdr cache when the db is going away, or we
+ // could end up with multiple copies of the same logical msg hdr, which will
+ // lead to ref-counting problems.
+ if (dbGoingAway) {
+ ClearUseHdrCache();
+ ClearThreads();
+ }
+ m_thumb = nullptr;
+}
+
+nsresult nsMsgDatabase::ClearHdrCache(bool reInit) {
+ if (m_cachedHeaders) {
+ // save this away in case we renter this code.
+ PLDHashTable* saveCachedHeaders = m_cachedHeaders;
+ m_cachedHeaders = nullptr;
+ for (auto iter = saveCachedHeaders->Iter(); !iter.Done(); iter.Next()) {
+ auto element = static_cast<MsgHdrHashElement*>(iter.Get());
+ if (element) NS_IF_RELEASE(element->mHdr);
+ }
+
+ if (reInit) {
+ saveCachedHeaders->ClearAndPrepareForLength(m_cacheSize);
+ m_cachedHeaders = saveCachedHeaders;
+ } else {
+ delete saveCachedHeaders;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::RemoveHdrFromCache(nsIMsgDBHdr* hdr, nsMsgKey key) {
+ if (m_cachedHeaders) {
+ if (key == nsMsgKey_None) hdr->GetMessageKey(&key);
+
+ PLDHashEntryHdr* entry =
+ m_cachedHeaders->Search((const void*)(uintptr_t)key);
+ if (entry) {
+ m_cachedHeaders->Remove((void*)(uintptr_t)key);
+ NS_RELEASE(hdr); // get rid of extra ref the cache was holding.
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::GetHdrFromUseCache(nsMsgKey key, nsIMsgDBHdr** result) {
+ if (!result) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ *result = nullptr;
+
+ if (m_headersInUse) {
+ PLDHashEntryHdr* entry =
+ m_headersInUse->Search((const void*)(uintptr_t)key);
+ if (entry) {
+ MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry);
+ *result = element->mHdr;
+ }
+ if (*result) {
+ NS_ADDREF(*result);
+ rv = NS_OK;
+ }
+ }
+ return rv;
+}
+
+PLDHashTableOps nsMsgDatabase::gMsgDBHashTableOps = {
+ HashKey, MatchEntry, MoveEntry, ClearEntry, nullptr};
+
+// HashKey is supposed to maximize entropy in the low order bits, and the key
+// as is, should do that.
+PLDHashNumber nsMsgDatabase::HashKey(const void* aKey) {
+ return PLDHashNumber(NS_PTR_TO_INT32(aKey));
+}
+
+bool nsMsgDatabase::MatchEntry(const PLDHashEntryHdr* aEntry,
+ const void* aKey) {
+ const MsgHdrHashElement* hdr = static_cast<const MsgHdrHashElement*>(aEntry);
+ return aKey == (const void*)(uintptr_t)
+ hdr->mKey; // ### or get the key from the hdr...
+}
+
+void nsMsgDatabase::MoveEntry(PLDHashTable* aTable,
+ const PLDHashEntryHdr* aFrom,
+ PLDHashEntryHdr* aTo) {
+ new (KnownNotNull, aTo)
+ MsgHdrHashElement(std::move(*((MsgHdrHashElement*)aFrom)));
+}
+
+void nsMsgDatabase::ClearEntry(PLDHashTable* aTable, PLDHashEntryHdr* aEntry) {
+ MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(aEntry);
+ element->mHdr = nullptr; // eh? Need to release this or not?
+ element->mKey = nsMsgKey_None; // eh?
+}
+
+nsresult nsMsgDatabase::AddHdrToUseCache(nsIMsgDBHdr* hdr, nsMsgKey key) {
+ if (!m_headersInUse) {
+ mdb_count numHdrs = MSG_HASH_SIZE;
+ if (m_mdbAllMsgHeadersTable)
+ m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrs);
+ m_headersInUse =
+ new PLDHashTable(&gMsgDBHashTableOps, sizeof(struct MsgHdrHashElement),
+ std::max((mdb_count)MSG_HASH_SIZE, numHdrs));
+ }
+ if (m_headersInUse) {
+ if (key == nsMsgKey_None) hdr->GetMessageKey(&key);
+ PLDHashEntryHdr* entry =
+ m_headersInUse->Add((void*)(uintptr_t)key, mozilla::fallible);
+ if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory
+
+ MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry);
+ element->mHdr = hdr;
+ element->mKey = key;
+ // the hash table won't add ref, we'll do it ourselves
+ // stand for the addref that CreateMsgHdr normally does.
+ NS_ADDREF(hdr);
+ return NS_OK;
+ }
+
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult nsMsgDatabase::ClearUseHdrCache() {
+ if (m_headersInUse) {
+ // clear mdb row pointers of any headers still in use, because the
+ // underlying db is going away.
+ for (auto iter = m_headersInUse->Iter(); !iter.Done(); iter.Next()) {
+ auto element = static_cast<const MsgHdrHashElement*>(iter.Get());
+ if (element && element->mHdr) {
+ nsMsgHdr* msgHdr = static_cast<nsMsgHdr*>(
+ element->mHdr); // closed system, so this is ok
+ // clear out m_mdbRow member variable - the db is going away, which
+ // means that this member variable might very well point to a mork db
+ // that is gone.
+ NS_IF_RELEASE(msgHdr->m_mdbRow);
+ // NS_IF_RELEASE(msgHdr->m_mdb);
+ }
+ }
+ delete m_headersInUse;
+ m_headersInUse = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::RemoveHdrFromUseCache(nsIMsgDBHdr* hdr, nsMsgKey key) {
+ if (m_headersInUse) {
+ if (key == nsMsgKey_None) hdr->GetMessageKey(&key);
+
+ m_headersInUse->Remove((void*)(uintptr_t)key);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::CreateMsgHdr(nsIMdbRow* hdrRow, nsMsgKey key,
+ nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(hdrRow);
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv = GetHdrFromUseCache(key, result);
+ if (NS_SUCCEEDED(rv) && *result) {
+ hdrRow->Release();
+ return rv;
+ }
+
+ nsMsgHdr* msgHdr = new nsMsgHdr(this, hdrRow);
+ if (!msgHdr) return NS_ERROR_OUT_OF_MEMORY;
+ msgHdr->SetMessageKey(key);
+ // don't need to addref here; GetHdrFromUseCache addrefs.
+ *result = msgHdr;
+
+ AddHdrToCache(msgHdr, key);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::AddListener(nsIDBChangeListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ m_ChangeListeners.AppendElementUnlessExists(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::RemoveListener(nsIDBChangeListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ m_ChangeListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+// XXX should we return rv for listener->propertyfunc_?
+#define NOTIFY_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator iter( \
+ m_ChangeListeners); \
+ nsCOMPtr<nsIDBChangeListener> listener; \
+ while (iter.HasMore()) { \
+ listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+// change announcer methods - just broadcast to all listeners.
+NS_IMETHODIMP nsMsgDatabase::NotifyHdrChangeAll(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ // We will only notify the change if the header exists in the database.
+ // This allows database functions to be usable in both the case where the
+ // header is in the db, or the header is not so no notifications should be
+ // given.
+ nsMsgKey key;
+ bool inDb = false;
+ if (aHdrChanged) {
+ aHdrChanged->GetMessageKey(&key);
+ ContainsKey(key, &inDb);
+ }
+ if (inDb)
+ NOTIFY_LISTENERS(OnHdrFlagsChanged,
+ (aHdrChanged, aOldFlags, aNewFlags, aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyReadChanged(
+ nsIDBChangeListener* aInstigator) {
+ NOTIFY_LISTENERS(OnReadChanged, (aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyJunkScoreChanged(
+ nsIDBChangeListener* aInstigator) {
+ NOTIFY_LISTENERS(OnJunkScoreChanged, (aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyHdrDeletedAll(
+ nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ NOTIFY_LISTENERS(OnHdrDeleted,
+ (aHdrDeleted, aParentKey, aFlags, aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyHdrAddedAll(
+ nsIMsgDBHdr* aHdrAdded, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+#ifdef DEBUG_bienvenu1
+ printf("notifying add of %ld parent %ld\n", keyAdded, parentKey);
+#endif
+ NOTIFY_LISTENERS(OnHdrAdded, (aHdrAdded, aParentKey, aFlags, aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyParentChangedAll(
+ nsMsgKey aKeyReparented, nsMsgKey aOldParent, nsMsgKey aNewParent,
+ nsIDBChangeListener* aInstigator) {
+ NOTIFY_LISTENERS(OnParentChanged,
+ (aKeyReparented, aOldParent, aNewParent, aInstigator));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::NotifyAnnouncerGoingAway(void) {
+ NOTIFY_LISTENERS(OnAnnouncerGoingAway, (this));
+ return NS_OK;
+}
+
+bool nsMsgDatabase::MatchDbName(nsIFile* dbFile) // returns true if they match
+{
+ NS_ENSURE_TRUE(m_dbFile, false);
+ return dbFile->NativePath().Equals(m_dbFile->NativePath());
+}
+
+void nsMsgDBService::AddToCache(nsMsgDatabase* pMessageDB) {
+#ifdef DEBUG_David_Bienvenu
+ NS_ASSERTION(m_dbCache.Length() < 50, "50 or more open db's");
+#endif
+#ifdef DEBUG
+ if (pMessageDB->m_folder) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ CachedDBForFolder(pMessageDB->m_folder, getter_AddRefs(msgDB));
+ NS_ASSERTION(!msgDB, "shouldn't have db in cache");
+ }
+#endif
+ m_dbCache.AppendElement(pMessageDB);
+}
+
+/**
+ * Log the open db's, and how many headers are in memory.
+ */
+void nsMsgDBService::DumpCache() {
+ nsMsgDatabase* db = nullptr;
+ MOZ_LOG(DBLog, LogLevel::Info, ("%zu open DBs", m_dbCache.Length()));
+ for (uint32_t i = 0; i < m_dbCache.Length(); i++) {
+ db = m_dbCache.ElementAt(i);
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("%s - %" PRIu32 " hdrs in use",
+ db->m_dbFile->HumanReadablePath().get(),
+ db->m_headersInUse ? db->m_headersInUse->EntryCount() : 0));
+ }
+}
+
+// Memory Reporting implementations
+
+size_t nsMsgDatabase::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t totalSize = 0;
+ if (m_dbFolderInfo)
+ totalSize += m_dbFolderInfo->SizeOfExcludingThis(aMallocSizeOf);
+ if (m_mdbEnv) {
+ nsIMdbHeap* morkHeap = nullptr;
+ m_mdbEnv->GetHeap(&morkHeap);
+ if (morkHeap) totalSize += morkHeap->GetUsedSize();
+ }
+ totalSize += m_newSet.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ totalSize += m_ChangeListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ totalSize += m_threads.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ // We have two tables of header objects, but every header in m_cachedHeaders
+ // should be in m_headersInUse.
+ // double-counting...
+ size_t headerSize = 0;
+ if (m_headersInUse) {
+ headerSize = m_headersInUse->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ for (auto iter = m_headersInUse->Iter(); !iter.Done(); iter.Next()) {
+ auto entry = static_cast<MsgHdrHashElement*>(iter.Get());
+ // Sigh, this is dangerous, but so long as this is a closed system, this
+ // is safe.
+ headerSize += static_cast<nsMsgHdr*>(entry->mHdr)
+ ->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ totalSize += headerSize;
+ if (m_msgReferences)
+ totalSize += m_msgReferences->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ return totalSize;
+}
+
+namespace mozilla {
+namespace mailnews {
+
+MOZ_DEFINE_MALLOC_SIZE_OF(GetMallocSize)
+
+class MsgDBReporter final : public nsIMemoryReporter {
+ nsWeakPtr mDatabase;
+
+ public:
+ explicit MsgDBReporter(nsMsgDatabase* db)
+ : mDatabase(do_GetWeakReference(db)) {}
+
+ NS_DECL_ISUPPORTS
+ NS_IMETHOD GetName(nsACString& aName) {
+ aName.AssignLiteral("msg-database-objects");
+ return NS_OK;
+ }
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aCb, nsISupports* aClosure,
+ bool aAnonymize) override {
+ nsCString path;
+ GetPath(path, aAnonymize);
+ nsCOMPtr<nsIMsgDatabase> database = do_QueryReferent(mDatabase);
+ nsMsgDatabase* db =
+ database ? static_cast<nsMsgDatabase*>(database.get()) : nullptr;
+ return aCb->Callback(EmptyCString(), path, nsIMemoryReporter::KIND_HEAP,
+ nsIMemoryReporter::UNITS_BYTES,
+ db ? db->SizeOfIncludingThis(GetMallocSize) : 0,
+ "Memory used for the folder database."_ns, aClosure);
+ }
+
+ void GetPath(nsACString& memoryPath, bool aAnonymize) {
+ memoryPath.AssignLiteral("explicit/maildb/database(");
+ nsCOMPtr<nsIMsgDatabase> database = do_QueryReferent(mDatabase);
+ nsCOMPtr<nsIMsgFolder> folder;
+ if (database) database->GetFolder(getter_AddRefs(folder));
+ if (folder) {
+ if (aAnonymize)
+ memoryPath.AppendLiteral("<anonymized>");
+ else {
+ nsAutoCString folderURL;
+ folder->GetFolderURL(folderURL);
+ folderURL.ReplaceChar('/', '\\');
+ memoryPath += folderURL;
+ }
+ } else {
+ memoryPath.AppendLiteral("UNKNOWN-FOLDER");
+ }
+ memoryPath.Append(')');
+ }
+
+ private:
+ ~MsgDBReporter() {}
+};
+
+NS_IMPL_ISUPPORTS(MsgDBReporter, nsIMemoryReporter)
+} // namespace mailnews
+} // namespace mozilla
+
+nsMsgDatabase::nsMsgDatabase()
+ : m_dbFolderInfo(nullptr),
+ m_nextPseudoMsgKey(kFirstPseudoKey),
+ m_mdbEnv(nullptr),
+ m_mdbStore(nullptr),
+ m_mdbAllMsgHeadersTable(nullptr),
+ m_mdbAllThreadsTable(nullptr),
+ m_create(false),
+ m_leaveInvalidDB(false),
+ m_mdbTokensInitialized(false),
+ m_hdrRowScopeToken(0),
+ m_hdrTableKindToken(0),
+ m_threadTableKindToken(0),
+ m_subjectColumnToken(0),
+ m_senderColumnToken(0),
+ m_messageIdColumnToken(0),
+ m_referencesColumnToken(0),
+ m_recipientsColumnToken(0),
+ m_dateColumnToken(0),
+ m_messageSizeColumnToken(0),
+ m_flagsColumnToken(0),
+ m_priorityColumnToken(0),
+ m_labelColumnToken(0),
+ m_numLinesColumnToken(0),
+ m_ccListColumnToken(0),
+ m_bccListColumnToken(0),
+ m_threadFlagsColumnToken(0),
+ m_threadIdColumnToken(0),
+ m_threadChildrenColumnToken(0),
+ m_threadUnreadChildrenColumnToken(0),
+ m_messageThreadIdColumnToken(0),
+ m_threadSubjectColumnToken(0),
+ m_messageCharSetColumnToken(0),
+ m_threadParentColumnToken(0),
+ m_threadRootKeyColumnToken(0),
+ m_threadNewestMsgDateColumnToken(0),
+ m_offlineMsgOffsetColumnToken(0),
+ m_offlineMessageSizeColumnToken(0),
+ m_headersInUse(nullptr),
+ m_cachedHeaders(nullptr),
+ m_bCacheHeaders(true),
+ m_cachedThreadId(nsMsgKey_None),
+ m_msgReferences(nullptr),
+ m_cacheSize(kMaxHdrsInCache) {
+ mMemReporter = new mozilla::mailnews::MsgDBReporter(this);
+ mozilla::RegisterWeakMemoryReporter(mMemReporter);
+}
+
+nsMsgDatabase::~nsMsgDatabase() {
+ mozilla::UnregisterWeakMemoryReporter(mMemReporter);
+ mMemReporter = nullptr;
+ // Close(FALSE); // better have already been closed.
+ ClearCachedObjects(true);
+ InvalidateEnumerators();
+ delete m_cachedHeaders;
+ delete m_headersInUse;
+
+ if (m_msgReferences) {
+ delete m_msgReferences;
+ m_msgReferences = nullptr;
+ }
+
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("closing database %s", m_dbFile->HumanReadablePath().get()));
+
+ nsCOMPtr<nsIMsgDBService> serv(
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1"));
+ if (serv) static_cast<nsMsgDBService*>(serv.get())->RemoveFromCache(this);
+
+ // if the db folder info refers to the mdb db, we must clear it because
+ // the reference will be a dangling one soon.
+ if (m_dbFolderInfo) m_dbFolderInfo->ReleaseExternalReferences();
+ m_dbFolderInfo = nullptr;
+
+ if (m_mdbAllMsgHeadersTable) m_mdbAllMsgHeadersTable->Release();
+
+ if (m_mdbAllThreadsTable) m_mdbAllThreadsTable->Release();
+
+ if (m_mdbStore) m_mdbStore->Release();
+
+ if (m_mdbEnv) {
+ m_mdbEnv->Release(); //??? is this right?
+ m_mdbEnv = nullptr;
+ }
+ m_ChangeListeners.Clear();
+}
+
+NS_IMPL_ISUPPORTS(nsMsgDatabase, nsIMsgDatabase, nsIMsgOfflineOpsDatabase,
+ nsIDBChangeAnnouncer)
+
+nsresult nsMsgDatabase::GetMDBFactory(nsIMdbFactory** aMdbFactory) {
+ if (!mMdbFactory) {
+ nsresult rv;
+ nsCOMPtr<nsIMdbFactoryService> mdbFactoryService =
+ do_GetService("@mozilla.org/db/mork;1", &rv);
+ if (NS_SUCCEEDED(rv) && mdbFactoryService) {
+ rv = mdbFactoryService->GetMdbFactory(getter_AddRefs(mMdbFactory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mMdbFactory) return NS_ERROR_FAILURE;
+ }
+ }
+ NS_ADDREF(*aMdbFactory = mMdbFactory);
+ return NS_OK;
+}
+
+// aLeaveInvalidDB: true if caller wants back a db even out of date.
+// If so, they'll extract out the interesting info from the db, close it,
+// delete it, and then try to open the db again, prior to reparsing.
+nsresult nsMsgDatabase::Open(nsMsgDBService* aDBService, nsIFile* aFolderName,
+ bool aCreate, bool aLeaveInvalidDB) {
+ return nsMsgDatabase::OpenInternal(aDBService, aFolderName, aCreate,
+ aLeaveInvalidDB,
+ true /* open synchronously */);
+}
+
+nsresult nsMsgDatabase::OpenInternal(nsMsgDBService* aDBService,
+ nsIFile* summaryFile, bool aCreate,
+ bool aLeaveInvalidDB, bool sync) {
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("nsMsgDatabase::Open(%s, %s, %p, %s)",
+ summaryFile->HumanReadablePath().get(), aCreate ? "TRUE" : "FALSE",
+ this, aLeaveInvalidDB ? "TRUE" : "FALSE"));
+
+ nsresult rv = OpenMDB(summaryFile, aCreate, sync);
+ if (NS_FAILED(rv))
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("error opening db %" PRIx32, static_cast<uint32_t>(rv)));
+
+ if (MOZ_LOG_TEST(DBLog, LogLevel::Debug)) aDBService->DumpCache();
+
+ if (rv == NS_ERROR_FILE_NOT_FOUND) return rv;
+
+ m_create = aCreate;
+ m_leaveInvalidDB = aLeaveInvalidDB;
+ if (!sync && NS_SUCCEEDED(rv)) {
+ aDBService->AddToCache(this);
+ // remember open options for when the parsing is complete.
+ return rv;
+ }
+ return CheckForErrors(rv, true, aDBService, summaryFile);
+}
+
+nsresult nsMsgDatabase::CheckForErrors(nsresult err, bool sync,
+ nsMsgDBService* aDBService,
+ nsIFile* summaryFile) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ bool summaryFileExists;
+ bool newFile = false;
+ bool deleteInvalidDB = false;
+
+ bool exists;
+ int64_t fileSize = 0;
+ summaryFile->Exists(&exists);
+ if (exists) summaryFile->GetFileSize(&fileSize);
+ // if the old summary doesn't exist, we're creating a new one.
+ if ((!exists || !fileSize) && m_create) newFile = true;
+
+ summaryFileExists = exists && fileSize > 0;
+
+ if (NS_SUCCEEDED(err)) {
+ if (!m_dbFolderInfo) {
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ } else {
+ if (!newFile && summaryFileExists) {
+ bool valid = false;
+ nsresult rv = GetSummaryValid(&valid);
+ if (NS_FAILED(rv) || !valid)
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ // compare current version of db versus filed out version info.
+ uint32_t version;
+ m_dbFolderInfo->GetVersion(&version);
+ if (GetCurVersion() != version)
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+
+ // Check if we should force a reparse because, for example, we have
+ // reached the key limit.
+ bool forceReparse;
+ m_dbFolderInfo->GetBooleanProperty("forceReparse", false, &forceReparse);
+ if (forceReparse) {
+ NS_WARNING("Forcing a reparse presumably because key limit reached");
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ }
+ if (NS_FAILED(err) && !m_leaveInvalidDB) deleteInvalidDB = true;
+ } else if (err != NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) {
+ // No point declaring it out-of-date and trying to delete it
+ // if it's missing.
+ // We get here with NS_ERROR_FAILURE when Mork can't open the
+ // file due to too many open files. In this case there is no
+ // point to blow away the MSF file.
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ if (!m_leaveInvalidDB) deleteInvalidDB = true;
+ }
+
+ if (deleteInvalidDB) {
+ // this will make the db folder info release its ref to the mail db...
+ m_dbFolderInfo = nullptr;
+ ForceClosed();
+ if (err == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ summaryFile->Remove(false);
+ }
+ if (NS_FAILED(err) || newFile) {
+ // if we couldn't open file, or we have a blank one, and we're supposed
+ // to upgrade, upgrade it.
+ if (newFile && !m_leaveInvalidDB) // caller is upgrading, and we have empty
+ // summary file,
+ { // leave db around and open so caller can upgrade it.
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_MISSING;
+ } else if (NS_FAILED(err) &&
+ err != NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ Close(false);
+ summaryFile->Remove(false); // blow away the db if it's corrupt.
+ }
+ }
+ if (sync && (NS_SUCCEEDED(err) || err == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING))
+ aDBService->AddToCache(this);
+ return (summaryFileExists) ? err : NS_MSG_ERROR_FOLDER_SUMMARY_MISSING;
+}
+
+/**
+ * Open the MDB database synchronously or async based on sync argument.
+ * If successful, this routine will set up the m_mdbStore and m_mdbEnv of
+ * the database object so other database calls can work.
+ */
+nsresult nsMsgDatabase::OpenMDB(nsIFile* dbFile, bool create, bool sync) {
+ nsCOMPtr<nsIMdbFactory> mdbFactory;
+ nsresult ret = GetMDBFactory(getter_AddRefs(mdbFactory));
+ NS_ENSURE_SUCCESS(ret, ret);
+
+ ret = mdbFactory->MakeEnv(NULL, &m_mdbEnv);
+ if (NS_SUCCEEDED(ret)) {
+ nsIMdbHeap* dbHeap = nullptr;
+
+ if (m_mdbEnv) m_mdbEnv->SetAutoClear(true);
+ PathString dbName = dbFile->NativePath();
+ ret = dbFile->Clone(getter_AddRefs(m_dbFile));
+ NS_ENSURE_SUCCESS(ret, ret);
+ bool exists = false;
+ ret = dbFile->Exists(&exists);
+ if (!exists) {
+ ret = NS_MSG_ERROR_FOLDER_SUMMARY_MISSING;
+ }
+ // If m_thumb is set, we're asynchronously opening the db already.
+ else if (!m_thumb) {
+ mdbOpenPolicy inOpenPolicy;
+ mdb_bool canOpen;
+ mdbYarn outFormatVersion;
+
+ nsIMdbFile* oldFile = nullptr;
+ ret = mdbFactory->OpenOldFile(
+ m_mdbEnv, dbHeap, dbName.get(),
+ mdbBool_kFalse, // not readonly, we want modifiable
+ &oldFile);
+ if (oldFile) {
+ if (NS_SUCCEEDED(ret)) {
+ ret = mdbFactory->CanOpenFilePort(m_mdbEnv,
+ oldFile, // the file to investigate
+ &canOpen, &outFormatVersion);
+ if (NS_SUCCEEDED(ret) && canOpen) {
+ inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0;
+ inOpenPolicy.mOpenPolicy_MinMemory = 0;
+ inOpenPolicy.mOpenPolicy_MaxLazy = 0;
+
+ ret = mdbFactory->OpenFileStore(m_mdbEnv, dbHeap, oldFile,
+ &inOpenPolicy,
+ getter_AddRefs(m_thumb));
+ } else
+ ret = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ NS_RELEASE(oldFile); // always release our file ref, store has own
+ }
+ }
+ if (NS_SUCCEEDED(ret) && m_thumb && sync) {
+ mdb_count outTotal; // total somethings to do in operation
+ mdb_count outCurrent; // subportion of total completed so far
+ mdb_bool outDone = false; // is operation finished?
+ mdb_bool outBroken; // is operation irreparably dead and broken?
+ do {
+ ret = m_thumb->DoMore(m_mdbEnv, &outTotal, &outCurrent, &outDone,
+ &outBroken);
+ if (NS_FAILED(ret)) { // mork isn't really doing NS errors yet.
+ outDone = true;
+ break;
+ }
+ } while (NS_SUCCEEDED(ret) && !outBroken && !outDone);
+ // m_mdbEnv->ClearErrors(); // ### temporary...
+ // only 0 is a non-error return.
+ if (NS_SUCCEEDED(ret) && outDone) {
+ ret = mdbFactory->ThumbToOpenStore(m_mdbEnv, m_thumb, &m_mdbStore);
+ if (NS_SUCCEEDED(ret))
+ ret = (m_mdbStore) ? InitExistingDB() : NS_ERROR_FAILURE;
+ }
+#ifdef DEBUG_bienvenu1
+ DumpContents();
+#endif
+ m_thumb = nullptr;
+ } else if (create) // ### need error code saying why open file store failed
+ {
+ nsIMdbFile* newFile = 0;
+ ret = mdbFactory->CreateNewFile(m_mdbEnv, dbHeap, dbName.get(), &newFile);
+ if (NS_FAILED(ret)) ret = NS_ERROR_FILE_NOT_FOUND;
+ if (newFile) {
+ if (NS_SUCCEEDED(ret)) {
+ mdbOpenPolicy inOpenPolicy;
+
+ inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0;
+ inOpenPolicy.mOpenPolicy_MinMemory = 0;
+ inOpenPolicy.mOpenPolicy_MaxLazy = 0;
+
+ ret = mdbFactory->CreateNewFileStore(m_mdbEnv, dbHeap, newFile,
+ &inOpenPolicy, &m_mdbStore);
+ if (NS_SUCCEEDED(ret))
+ ret = (m_mdbStore) ? InitNewDB() : NS_ERROR_FAILURE;
+ }
+ NS_RELEASE(newFile); // always release our file ref, store has own
+ }
+ }
+ }
+
+ return ret;
+}
+
+nsresult nsMsgDatabase::CloseMDB(bool commit) {
+ if (commit) Commit(nsMsgDBCommitType::kSessionCommit);
+ return (NS_OK);
+}
+
+// force the database to close - this'll flush out anybody holding onto
+// a database without having a listener!
+// This is evil in the com world, but there are times we need to delete the
+// file.
+NS_IMETHODIMP nsMsgDatabase::ForceClosed() {
+ nsresult err = NS_OK;
+
+ // make sure someone has a reference so object won't get deleted out from
+ // under us.
+ NS_ADDREF_THIS();
+ NotifyAnnouncerGoingAway();
+ // make sure dbFolderInfo isn't holding onto mork stuff because mork db is
+ // going away
+ if (m_dbFolderInfo) m_dbFolderInfo->ReleaseExternalReferences();
+ m_dbFolderInfo = nullptr;
+
+ err = CloseMDB(true); // Backup DB will try to recover info, so commit
+ ClearCachedObjects(true);
+ InvalidateEnumerators();
+ if (m_mdbAllMsgHeadersTable) {
+ m_mdbAllMsgHeadersTable->Release();
+ m_mdbAllMsgHeadersTable = nullptr;
+ }
+ if (m_mdbAllThreadsTable) {
+ m_mdbAllThreadsTable->Release();
+ m_mdbAllThreadsTable = nullptr;
+ }
+ if (m_mdbStore) {
+ m_mdbStore->Release();
+ m_mdbStore = nullptr;
+ }
+
+ // There'd better not be any listeners, because we're going away.
+ NS_ASSERTION(m_ChangeListeners.IsEmpty(),
+ "shouldn't have any listeners left");
+ m_ChangeListeners.Clear();
+
+ NS_RELEASE_THIS();
+ return err;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetDBFolderInfo(nsIDBFolderInfo** result) {
+ if (!m_dbFolderInfo) {
+ NS_ERROR("db must be corrupt");
+ return NS_ERROR_NULL_POINTER;
+ }
+ NS_ADDREF(*result = m_dbFolderInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::Commit(nsMsgDBCommit commitType) {
+ nsresult err = NS_OK;
+ nsCOMPtr<nsIMdbThumb> commitThumb;
+
+ RememberLastUseTime();
+ if (commitType == nsMsgDBCommitType::kLargeCommit ||
+ commitType == nsMsgDBCommitType::kSessionCommit) {
+ mdb_percent outActualWaste = 0;
+ mdb_bool outShould;
+ if (m_mdbStore) {
+ err =
+ m_mdbStore->ShouldCompress(GetEnv(), 30, &outActualWaste, &outShould);
+ if (NS_SUCCEEDED(err) && outShould)
+ commitType = nsMsgDBCommitType::kCompressCommit;
+ }
+ }
+ // commitType = nsMsgDBCommitType::kCompressCommit; // ### until incremental
+ // writing works.
+
+ if (m_mdbStore) {
+ switch (commitType) {
+ case nsMsgDBCommitType::kLargeCommit:
+ err = m_mdbStore->LargeCommit(GetEnv(), getter_AddRefs(commitThumb));
+ break;
+ case nsMsgDBCommitType::kSessionCommit:
+ err = m_mdbStore->SessionCommit(GetEnv(), getter_AddRefs(commitThumb));
+ break;
+ case nsMsgDBCommitType::kCompressCommit:
+ err = m_mdbStore->CompressCommit(GetEnv(), getter_AddRefs(commitThumb));
+ break;
+ }
+ }
+ if (commitThumb) {
+ mdb_count outTotal = 0; // total somethings to do in operation
+ mdb_count outCurrent = 0; // subportion of total completed so far
+ mdb_bool outDone = false; // is operation finished?
+ mdb_bool outBroken = false; // is operation irreparably dead and broken?
+ while (!outDone && !outBroken && NS_SUCCEEDED(err)) {
+ err = commitThumb->DoMore(GetEnv(), &outTotal, &outCurrent, &outDone,
+ &outBroken);
+ }
+ }
+ // ### do something with error, but clear it now because mork errors out on
+ // commits.
+ if (GetEnv()) GetEnv()->ClearErrors();
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+
+ rv = accountManager->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(rv) && folderCache) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ nsCString persistentPath;
+ NS_ENSURE_TRUE(m_dbFile, NS_ERROR_NULL_POINTER);
+ rv = m_dbFile->GetPersistentDescriptor(persistentPath);
+ NS_ENSURE_SUCCESS(rv, err);
+ rv = folderCache->GetCacheElement(persistentPath, false,
+ getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(rv) && cacheElement && m_dbFolderInfo) {
+ int32_t totalMessages, unreadMessages, pendingMessages,
+ pendingUnreadMessages;
+
+ m_dbFolderInfo->GetNumMessages(&totalMessages);
+ m_dbFolderInfo->GetNumUnreadMessages(&unreadMessages);
+ m_dbFolderInfo->GetImapUnreadPendingMessages(&pendingUnreadMessages);
+ m_dbFolderInfo->GetImapTotalPendingMessages(&pendingMessages);
+ cacheElement->SetCachedInt32("totalMsgs", totalMessages);
+ cacheElement->SetCachedInt32("totalUnreadMsgs", unreadMessages);
+ cacheElement->SetCachedInt32("pendingMsgs", pendingMessages);
+ cacheElement->SetCachedInt32("pendingUnreadMsgs",
+ pendingUnreadMessages);
+ }
+ }
+ }
+
+ return err;
+}
+
+NS_IMETHODIMP nsMsgDatabase::Close(bool forceCommit /* = TRUE */) {
+ InvalidateEnumerators();
+ return CloseMDB(forceCommit);
+}
+
+const char* kMsgHdrsScope =
+ "ns:msg:db:row:scope:msgs:all"; // scope for all headers table
+const char* kMsgHdrsTableKind = "ns:msg:db:table:kind:msgs";
+const char* kThreadTableKind = "ns:msg:db:table:kind:thread";
+const char* kThreadHdrsScope =
+ "ns:msg:db:row:scope:threads:all"; // scope for all threads table
+const char* kAllThreadsTableKind =
+ "ns:msg:db:table:kind:allthreads"; // kind for table of all threads
+const char* kSubjectColumnName = "subject";
+const char* kSenderColumnName = "sender";
+const char* kMessageIdColumnName = "message-id";
+const char* kReferencesColumnName = "references";
+const char* kRecipientsColumnName = "recipients";
+const char* kDateColumnName = "date";
+const char* kMessageSizeColumnName = "size";
+const char* kFlagsColumnName = "flags";
+const char* kPriorityColumnName = "priority";
+const char* kLabelColumnName = "label";
+const char* kNumLinesColumnName = "numLines";
+const char* kCCListColumnName = "ccList";
+const char* kBCCListColumnName = "bccList";
+const char* kMessageThreadIdColumnName = "msgThreadId";
+const char* kThreadFlagsColumnName = "threadFlags";
+const char* kThreadIdColumnName = "threadId";
+const char* kThreadChildrenColumnName = "children";
+const char* kThreadUnreadChildrenColumnName = "unreadChildren";
+const char* kThreadSubjectColumnName = "threadSubject";
+const char* kMessageCharSetColumnName = "msgCharSet";
+const char* kThreadParentColumnName = "threadParent";
+const char* kThreadRootColumnName = "threadRoot";
+const char* kThreadNewestMsgDateColumnName = "threadNewestMsgDate";
+const char* kOfflineMsgOffsetColumnName = "msgOffset";
+const char* kOfflineMsgSizeColumnName = "offlineMsgSize";
+struct mdbOid gAllMsgHdrsTableOID;
+struct mdbOid gAllThreadsTableOID;
+const char* kFixedBadRefThreadingProp = "fixedBadRefThreading";
+
+// set up empty tables, dbFolderInfo, etc.
+nsresult nsMsgDatabase::InitNewDB() {
+ nsresult err = NS_OK;
+
+ err = InitMDBInfo();
+ if (NS_SUCCEEDED(err)) {
+ nsDBFolderInfo* dbFolderInfo = new nsDBFolderInfo(this);
+ if (dbFolderInfo) {
+ err = dbFolderInfo->AddToNewMDB();
+ dbFolderInfo->SetVersion(GetCurVersion());
+ dbFolderInfo->SetBooleanProperty("forceReparse", false);
+ dbFolderInfo->SetBooleanProperty(kFixedBadRefThreadingProp, true);
+ nsIMdbStore* store = GetStore();
+ // create the unique table for the dbFolderInfo.
+ struct mdbOid allMsgHdrsTableOID;
+ struct mdbOid allThreadsTableOID;
+ if (!store) return NS_ERROR_NULL_POINTER;
+
+ allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken;
+ allMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey;
+ allThreadsTableOID.mOid_Scope = m_threadRowScopeToken;
+ allThreadsTableOID.mOid_Id = kAllThreadsTableKey;
+
+ // TODO: check this error value?
+ (void)store->NewTableWithOid(GetEnv(), &allMsgHdrsTableOID,
+ m_hdrTableKindToken, false, nullptr,
+ &m_mdbAllMsgHeadersTable);
+
+ // error here is not fatal.
+ (void)store->NewTableWithOid(GetEnv(), &allThreadsTableOID,
+ m_allThreadsTableKindToken, false, nullptr,
+ &m_mdbAllThreadsTable);
+
+ m_dbFolderInfo = dbFolderInfo;
+
+ } else
+ err = NS_ERROR_OUT_OF_MEMORY;
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::GetTableCreateIfMissing(const char* scope,
+ const char* kind,
+ nsIMdbTable** table,
+ mdb_token& scopeToken,
+ mdb_token& kindToken) {
+ struct mdbOid tableOID;
+
+ if (!m_mdbStore) return NS_ERROR_FAILURE;
+ (void)m_mdbStore->StringToToken(GetEnv(), scope, &scopeToken);
+ (void)m_mdbStore->StringToToken(GetEnv(), kind, &kindToken);
+ tableOID.mOid_Scope = scopeToken;
+ tableOID.mOid_Id = 1;
+
+ nsresult rv = m_mdbStore->GetTable(GetEnv(), &tableOID, table);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // create new all all offline ops table, if it doesn't exist.
+ if (NS_SUCCEEDED(rv) && !*table) {
+ rv = m_mdbStore->NewTable(GetEnv(), scopeToken, kindToken, false, nullptr,
+ table);
+ if (NS_FAILED(rv) || !*table) rv = NS_ERROR_FAILURE;
+ }
+ NS_ASSERTION(NS_SUCCEEDED(rv), "couldn't create offline ops table");
+ return rv;
+}
+
+nsresult nsMsgDatabase::InitExistingDB() {
+ nsresult err = NS_OK;
+
+ err = InitMDBInfo();
+ if (NS_SUCCEEDED(err)) {
+ err = GetStore()->GetTable(GetEnv(), &gAllMsgHdrsTableOID,
+ &m_mdbAllMsgHeadersTable);
+ if (NS_SUCCEEDED(err)) {
+ m_dbFolderInfo = new nsDBFolderInfo(this);
+ if (m_dbFolderInfo) {
+ err = m_dbFolderInfo->InitFromExistingDB();
+ }
+ } else
+ err = NS_ERROR_FAILURE;
+
+ NS_ASSERTION(NS_SUCCEEDED(err), "failed initing existing db");
+ NS_ENSURE_SUCCESS(err, err);
+ // create new all msg hdrs table, if it doesn't exist.
+ if (NS_SUCCEEDED(err) && !m_mdbAllMsgHeadersTable) {
+ struct mdbOid allMsgHdrsTableOID;
+ allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken;
+ allMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey;
+
+ nsresult mdberr = GetStore()->NewTableWithOid(
+ GetEnv(), &allMsgHdrsTableOID, m_hdrTableKindToken, false, nullptr,
+ &m_mdbAllMsgHeadersTable);
+ if (NS_FAILED(mdberr) || !m_mdbAllMsgHeadersTable) err = NS_ERROR_FAILURE;
+ }
+ struct mdbOid allThreadsTableOID;
+ allThreadsTableOID.mOid_Scope = m_threadRowScopeToken;
+ allThreadsTableOID.mOid_Id = kAllThreadsTableKey;
+ err = GetStore()->GetTable(GetEnv(), &gAllThreadsTableOID,
+ &m_mdbAllThreadsTable);
+ if (!m_mdbAllThreadsTable) {
+ nsresult mdberr = GetStore()->NewTableWithOid(
+ GetEnv(), &allThreadsTableOID, m_allThreadsTableKindToken, false,
+ nullptr, &m_mdbAllThreadsTable);
+ if (NS_FAILED(mdberr) || !m_mdbAllThreadsTable) err = NS_ERROR_FAILURE;
+ }
+ }
+ if (NS_SUCCEEDED(err) && m_dbFolderInfo) {
+ bool fixedBadRefThreading;
+ m_dbFolderInfo->GetBooleanProperty(kFixedBadRefThreadingProp, false,
+ &fixedBadRefThreading);
+ if (!fixedBadRefThreading) {
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ err = EnumerateMessages(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(err) && enumerator) {
+ bool hasMore;
+
+ while (NS_SUCCEEDED(err = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ err = enumerator->GetNext(getter_AddRefs(msgHdr));
+ if (msgHdr && NS_SUCCEEDED(err)) {
+ nsCString messageId;
+ nsAutoCString firstReference;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ msgHdr->GetStringReference(0, firstReference);
+ if (messageId.Equals(firstReference)) {
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ break;
+ }
+ }
+ }
+ }
+
+ m_dbFolderInfo->SetBooleanProperty(kFixedBadRefThreadingProp, true);
+ }
+ }
+ return err;
+}
+
+// initialize the various tokens and tables in our db's env
+nsresult nsMsgDatabase::InitMDBInfo() {
+ nsresult err = NS_OK;
+
+ if (!m_mdbTokensInitialized && GetStore()) {
+ m_mdbTokensInitialized = true;
+ err =
+ GetStore()->StringToToken(GetEnv(), kMsgHdrsScope, &m_hdrRowScopeToken);
+ if (NS_SUCCEEDED(err)) {
+ GetStore()->StringToToken(GetEnv(), kSubjectColumnName,
+ &m_subjectColumnToken);
+ GetStore()->StringToToken(GetEnv(), kSenderColumnName,
+ &m_senderColumnToken);
+ GetStore()->StringToToken(GetEnv(), kMessageIdColumnName,
+ &m_messageIdColumnToken);
+ // if we just store references as a string, we won't get any savings from
+ // the fact there's a lot of duplication. So we may want to break them up
+ // into multiple columns, r1, r2, etc.
+ GetStore()->StringToToken(GetEnv(), kReferencesColumnName,
+ &m_referencesColumnToken);
+ // similarly, recipients could be tokenized properties
+ GetStore()->StringToToken(GetEnv(), kRecipientsColumnName,
+ &m_recipientsColumnToken);
+ GetStore()->StringToToken(GetEnv(), kDateColumnName, &m_dateColumnToken);
+ GetStore()->StringToToken(GetEnv(), kMessageSizeColumnName,
+ &m_messageSizeColumnToken);
+ GetStore()->StringToToken(GetEnv(), kFlagsColumnName,
+ &m_flagsColumnToken);
+ GetStore()->StringToToken(GetEnv(), kPriorityColumnName,
+ &m_priorityColumnToken);
+ GetStore()->StringToToken(GetEnv(), kLabelColumnName,
+ &m_labelColumnToken);
+ GetStore()->StringToToken(GetEnv(), kNumLinesColumnName,
+ &m_numLinesColumnToken);
+ GetStore()->StringToToken(GetEnv(), kCCListColumnName,
+ &m_ccListColumnToken);
+ GetStore()->StringToToken(GetEnv(), kBCCListColumnName,
+ &m_bccListColumnToken);
+ GetStore()->StringToToken(GetEnv(), kMessageThreadIdColumnName,
+ &m_messageThreadIdColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadIdColumnName,
+ &m_threadIdColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadFlagsColumnName,
+ &m_threadFlagsColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadNewestMsgDateColumnName,
+ &m_threadNewestMsgDateColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadChildrenColumnName,
+ &m_threadChildrenColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadUnreadChildrenColumnName,
+ &m_threadUnreadChildrenColumnToken);
+ GetStore()->StringToToken(GetEnv(), kThreadSubjectColumnName,
+ &m_threadSubjectColumnToken);
+ GetStore()->StringToToken(GetEnv(), kMessageCharSetColumnName,
+ &m_messageCharSetColumnToken);
+ err = GetStore()->StringToToken(GetEnv(), kMsgHdrsTableKind,
+ &m_hdrTableKindToken);
+ if (NS_SUCCEEDED(err))
+ err = GetStore()->StringToToken(GetEnv(), kThreadTableKind,
+ &m_threadTableKindToken);
+ err = GetStore()->StringToToken(GetEnv(), kAllThreadsTableKind,
+ &m_allThreadsTableKindToken);
+ err = GetStore()->StringToToken(GetEnv(), kThreadHdrsScope,
+ &m_threadRowScopeToken);
+ err = GetStore()->StringToToken(GetEnv(), kThreadParentColumnName,
+ &m_threadParentColumnToken);
+ err = GetStore()->StringToToken(GetEnv(), kThreadRootColumnName,
+ &m_threadRootKeyColumnToken);
+ err = GetStore()->StringToToken(GetEnv(), kOfflineMsgOffsetColumnName,
+ &m_offlineMsgOffsetColumnToken);
+ err = GetStore()->StringToToken(GetEnv(), kOfflineMsgSizeColumnName,
+ &m_offlineMessageSizeColumnToken);
+
+ if (NS_SUCCEEDED(err)) {
+ // The table of all message hdrs will have table id 1.
+ gAllMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken;
+ gAllMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey;
+ gAllThreadsTableOID.mOid_Scope = m_threadRowScopeToken;
+ gAllThreadsTableOID.mOid_Id = kAllThreadsTableKey;
+ }
+ }
+ }
+ return err;
+}
+
+// Returns if the db contains this key
+NS_IMETHODIMP nsMsgDatabase::ContainsKey(nsMsgKey key, bool* containsKey) {
+ nsresult err = NS_OK;
+ mdb_bool hasOid;
+ mdbOid rowObjectId;
+
+ if (!containsKey || !m_mdbAllMsgHeadersTable) return NS_ERROR_NULL_POINTER;
+ *containsKey = false;
+
+ rowObjectId.mOid_Id = key;
+ rowObjectId.mOid_Scope = m_hdrRowScopeToken;
+ err = m_mdbAllMsgHeadersTable->HasOid(GetEnv(), &rowObjectId, &hasOid);
+ if (NS_SUCCEEDED(err)) *containsKey = hasOid;
+
+ return err;
+}
+
+// get a message header for the given key. Caller must release()!
+NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForKey(nsMsgKey key,
+ nsIMsgDBHdr** pmsgHdr) {
+ *pmsgHdr = nullptr;
+ NS_ENSURE_ARG_POINTER(pmsgHdr);
+ NS_ENSURE_STATE(m_folder);
+ NS_ENSURE_STATE(m_mdbAllMsgHeadersTable);
+ NS_ENSURE_STATE(m_mdbStore);
+
+ // Because this may be called a lot, and we don't want gettimeofday() to show
+ // up in trace logs, we just remember the most recent time any db was used,
+ // which should be close enough for our purposes.
+ m_lastUseTime = gLastUseTime;
+
+ nsresult rv = GetHdrFromUseCache(key, pmsgHdr);
+ if (NS_SUCCEEDED(rv) && *pmsgHdr) return rv;
+
+ mdbOid rowObjectId;
+ rowObjectId.mOid_Id = key;
+ rowObjectId.mOid_Scope = m_hdrRowScopeToken;
+ mdb_bool hasOid;
+ rv = m_mdbAllMsgHeadersTable->HasOid(GetEnv(), &rowObjectId, &hasOid);
+ if (NS_SUCCEEDED(rv) /* && hasOid */) {
+ nsIMdbRow* hdrRow;
+ rv = m_mdbStore->GetRow(GetEnv(), &rowObjectId, &hdrRow);
+ if (NS_SUCCEEDED(rv)) {
+ if (!hdrRow) {
+ rv = NS_ERROR_NULL_POINTER;
+ } else {
+ rv = CreateMsgHdr(hdrRow, key, pmsgHdr);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::DeleteMessage(nsMsgKey key,
+ nsIDBChangeListener* instigator,
+ bool commit) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ return DeleteHeader(msgHdr, instigator, commit, true);
+}
+
+NS_IMETHODIMP nsMsgDatabase::DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys,
+ nsIDBChangeListener* instigator) {
+ nsresult err = NS_OK;
+
+ uint32_t kindex;
+ for (kindex = 0; kindex < nsMsgKeys.Length(); kindex++) {
+ nsMsgKey key = nsMsgKeys[kindex];
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ bool hasKey;
+
+ if (NS_SUCCEEDED(ContainsKey(key, &hasKey)) && hasKey) {
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) {
+ err = NS_MSG_MESSAGE_NOT_FOUND;
+ break;
+ }
+ err = DeleteHeader(msgHdr, instigator, kindex % 300 == 0, true);
+ if (NS_FAILED(err)) break;
+ }
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) {
+ uint32_t size = 0;
+ (void)msgHdr->GetMessageSize(&size);
+ return m_dbFolderInfo->ChangeExpungedBytes(size);
+}
+
+NS_IMETHODIMP nsMsgDatabase::DeleteHeader(nsIMsgDBHdr* msg,
+ nsIDBChangeListener* instigator,
+ bool commit, bool notify) {
+ if (!msg) return NS_ERROR_NULL_POINTER;
+
+ nsMsgHdr* msgHdr =
+ static_cast<nsMsgHdr*>(msg); // closed system, so this is ok
+ nsMsgKey key;
+ (void)msg->GetMessageKey(&key);
+ // only need to do this for mail - will this speed up news expiration?
+ SetHdrFlag(msg, true, nsMsgMessageFlags::Expunged); // tell mailbox (mail)
+
+ bool hdrWasNew = m_newSet.BinaryIndexOf(key) != m_newSet.NoIndex;
+ m_newSet.RemoveElement(key);
+
+ if (m_dbFolderInfo) {
+ bool isRead;
+ m_dbFolderInfo->ChangeNumMessages(-1);
+ IsRead(key, &isRead);
+ if (!isRead) m_dbFolderInfo->ChangeNumUnreadMessages(-1);
+ AdjustExpungedBytesOnDelete(msg);
+ }
+
+ uint32_t flags;
+ nsMsgKey threadParent;
+
+ // Save off flags and threadparent since they will no longer exist after we
+ // remove the header from the db.
+ if (notify) {
+ (void)msg->GetFlags(&flags);
+ msg->GetThreadParent(&threadParent);
+ }
+
+ RemoveHeaderFromThread(msgHdr);
+ if (notify) {
+ // If deleted hdr was new, restore the new flag on flags
+ // so saved searches will know to reduce their new msg count.
+ if (hdrWasNew) flags |= nsMsgMessageFlags::New;
+ NotifyHdrDeletedAll(msg, threadParent, flags,
+ instigator); // tell listeners
+ }
+ // if (!onlyRemoveFromThread) // to speed up expiration, try this. But
+ // really need to do this in RemoveHeaderFromDB
+ nsresult ret = RemoveHeaderFromDB(msgHdr);
+
+ if (commit)
+ Commit(nsMsgDBCommitType::kLargeCommit); // ### dmb is this a good time to
+ // commit?
+ return ret;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::UndoDelete(nsIMsgDBHdr* aMsgHdr) {
+ if (aMsgHdr) {
+ // Force deleted flag, so SetHdrFlag won't bail out because deleted flag
+ // isn't set.
+ uint32_t result;
+ aMsgHdr->OrFlags(nsMsgMessageFlags::Expunged, &result);
+ SetHdrFlag(aMsgHdr, false,
+ nsMsgMessageFlags::Expunged); // Clear deleted flag in db.
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::RemoveHeaderFromThread(nsMsgHdr* msgHdr) {
+ if (!msgHdr) return NS_ERROR_NULL_POINTER;
+ nsresult ret = NS_OK;
+ nsCOMPtr<nsIMsgThread> thread;
+ ret = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(ret) && thread) {
+ ret = thread->RemoveChildHdr(msgHdr, this);
+ }
+ return ret;
+}
+
+NS_IMETHODIMP nsMsgDatabase::RemoveHeaderMdbRow(nsIMsgDBHdr* msg) {
+ NS_ENSURE_ARG_POINTER(msg);
+ nsMsgHdr* msgHdr =
+ static_cast<nsMsgHdr*>(msg); // closed system, so this is ok
+ return RemoveHeaderFromDB(msgHdr);
+}
+
+// This is a lower level routine which doesn't send notifications or
+// update folder info. One use is when a rule fires moving a header
+// from one db to another, to remove it from the first db.
+
+nsresult nsMsgDatabase::RemoveHeaderFromDB(nsMsgHdr* msgHdr) {
+ if (!msgHdr) return NS_ERROR_NULL_POINTER;
+ nsresult ret = NS_OK;
+
+ RemoveHdrFromCache(msgHdr, nsMsgKey_None);
+ if (UseCorrectThreading()) RemoveMsgRefsFromHash(msgHdr);
+ nsIMdbRow* row = msgHdr->GetMDBRow();
+ if (row) {
+ ret = m_mdbAllMsgHeadersTable->CutRow(GetEnv(), row);
+ row->CutAllColumns(GetEnv());
+ }
+ msgHdr->ClearCachedValues();
+ return ret;
+}
+
+nsresult nsMsgDatabase::IsRead(nsMsgKey key, bool* pRead) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ return IsHeaderRead(msgHdr, pRead);
+}
+
+uint32_t nsMsgDatabase::GetStatusFlags(nsIMsgDBHdr* msgHdr,
+ nsMsgMessageFlagType origFlags) {
+ uint32_t statusFlags = origFlags;
+ bool isRead = true;
+
+ nsMsgKey key;
+ (void)msgHdr->GetMessageKey(&key);
+ if ((!m_newSet.IsEmpty() && m_newSet[m_newSet.Length() - 1] == key) ||
+ (m_newSet.BinaryIndexOf(key) != m_newSet.NoIndex))
+ statusFlags |= nsMsgMessageFlags::New;
+ if (NS_SUCCEEDED(IsHeaderRead(msgHdr, &isRead)) && isRead)
+ statusFlags |= nsMsgMessageFlags::Read;
+ return statusFlags;
+}
+
+nsresult nsMsgDatabase::IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) {
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsMsgHdr* hdr = static_cast<nsMsgHdr*>(msgHdr); // closed system, cast ok
+ // can't call GetFlags, because it will be recursive.
+ uint32_t flags;
+ hdr->GetRawFlags(&flags);
+ *pRead = !!(flags & nsMsgMessageFlags::Read);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::IsMarked(nsMsgKey key, bool* pMarked) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t flags;
+ (void)msgHdr->GetFlags(&flags);
+ *pMarked = !!(flags & nsMsgMessageFlags::Marked);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::IsIgnored(nsMsgKey key, bool* pIgnored) {
+ NS_ENSURE_ARG_POINTER(pIgnored);
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+
+ nsresult rv = GetThreadForMsgKey(key, getter_AddRefs(threadHdr));
+ // This should be very surprising, but we leave that up to the caller
+ // to determine for now.
+ if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ *pIgnored = !!(threadFlags & nsMsgMessageFlags::Ignored);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::IsWatched(nsMsgKey key, bool* pWatched) {
+ NS_ENSURE_ARG_POINTER(pWatched);
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+
+ nsresult rv = GetThreadForMsgKey(key, getter_AddRefs(threadHdr));
+ // This should be very surprising, but we leave that up to the caller
+ // to determine for now.
+ if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ *pWatched = !!(threadFlags & nsMsgMessageFlags::Watched);
+ return rv;
+}
+
+nsresult nsMsgDatabase::HasAttachments(nsMsgKey key, bool* pHasThem) {
+ NS_ENSURE_ARG_POINTER(pHasThem);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t flags;
+ (void)msgHdr->GetFlags(&flags);
+ *pHasThem = !!(flags & nsMsgMessageFlags::Attachment);
+ return NS_OK;
+}
+
+bool nsMsgDatabase::SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) {
+ return SetHdrFlag(msgHdr, bRead, nsMsgMessageFlags::Read);
+}
+
+nsresult nsMsgDatabase::MarkHdrReadInDB(nsIMsgDBHdr* msgHdr, bool bRead,
+ nsIDBChangeListener* instigator) {
+ nsresult rv;
+ nsMsgKey key;
+ uint32_t oldFlags;
+ bool hdrInDB;
+ (void)msgHdr->GetMessageKey(&key);
+ msgHdr->GetFlags(&oldFlags);
+
+ m_newSet.RemoveElement(key);
+ (void)ContainsKey(key, &hdrInDB);
+ if (hdrInDB && m_dbFolderInfo) {
+ if (bRead)
+ m_dbFolderInfo->ChangeNumUnreadMessages(-1);
+ else
+ m_dbFolderInfo->ChangeNumUnreadMessages(1);
+ }
+
+ SetHdrReadFlag(msgHdr, bRead); // this will cause a commit, at least for
+ // local mail, so do it after we change
+ // the folder counts above, so they will get committed too.
+ uint32_t flags;
+ rv = msgHdr->GetFlags(&flags);
+ flags &= ~nsMsgMessageFlags::New;
+ msgHdr->SetFlags(flags);
+ if (NS_FAILED(rv)) return rv;
+
+ if (oldFlags == flags) return NS_OK;
+
+ return NotifyHdrChangeAll(msgHdr, oldFlags, flags, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkRead(nsMsgKey key, bool bRead,
+ nsIDBChangeListener* instigator) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ return MarkHdrRead(msgHdr, bRead, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkReplied(
+ nsMsgKey key, bool bReplied, nsIDBChangeListener* instigator /* = NULL */) {
+ return SetKeyFlag(key, bReplied, nsMsgMessageFlags::Replied, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkForwarded(
+ nsMsgKey key, bool bForwarded,
+ nsIDBChangeListener* instigator /* = NULL */) {
+ return SetKeyFlag(key, bForwarded, nsMsgMessageFlags::Forwarded, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkRedirected(
+ nsMsgKey key, bool bRedirected,
+ nsIDBChangeListener* instigator /* = NULL */) {
+ return SetKeyFlag(key, bRedirected, nsMsgMessageFlags::Redirected,
+ instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkHasAttachments(
+ nsMsgKey key, bool bHasAttachments, nsIDBChangeListener* instigator) {
+ return SetKeyFlag(key, bHasAttachments, nsMsgMessageFlags::Attachment,
+ instigator);
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::MarkThreadRead(nsIMsgThread* thread,
+ nsIDBChangeListener* instigator,
+ nsTArray<nsMsgKey>& aThoseMarkedRead) {
+ NS_ENSURE_ARG_POINTER(thread);
+ aThoseMarkedRead.ClearAndRetainStorage();
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ aThoseMarkedRead.SetCapacity(numChildren);
+ for (uint32_t curChildIndex = 0; curChildIndex < numChildren;
+ curChildIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+
+ rv = thread->GetChildHdrAt(curChildIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ bool isRead = true;
+ IsHeaderRead(child, &isRead);
+ if (!isRead) {
+ nsMsgKey key;
+ if (NS_SUCCEEDED(child->GetMessageKey(&key)))
+ aThoseMarkedRead.AppendElement(key);
+ MarkHdrRead(child, true, instigator);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::MarkThreadIgnored(nsIMsgThread* thread, nsMsgKey threadKey,
+ bool bIgnored,
+ nsIDBChangeListener* instigator) {
+ NS_ENSURE_ARG(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t oldThreadFlags =
+ threadFlags; // not quite right, since we probably want msg hdr flags.
+ if (bIgnored) {
+ threadFlags |= nsMsgMessageFlags::Ignored;
+ threadFlags &= ~nsMsgMessageFlags::Watched; // ignore is implicit un-watch
+ } else
+ threadFlags &= ~nsMsgMessageFlags::Ignored;
+ thread->SetFlags(threadFlags);
+
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ GetMsgHdrForKey(threadKey, getter_AddRefs(msg));
+ NS_ENSURE_TRUE(msg, NS_MSG_MESSAGE_NOT_FOUND);
+
+ // We'll add the message flags to the thread flags when notifying, since
+ // notifications are supposed to be about messages, not threads.
+ uint32_t msgFlags;
+ msg->GetFlags(&msgFlags);
+
+ return NotifyHdrChangeAll(msg, oldThreadFlags | msgFlags,
+ threadFlags | msgFlags, instigator);
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::MarkHeaderKilled(nsIMsgDBHdr* msg, bool bIgnored,
+ nsIDBChangeListener* instigator) {
+ uint32_t msgFlags;
+ msg->GetFlags(&msgFlags);
+ uint32_t oldFlags = msgFlags;
+ if (bIgnored)
+ msgFlags |= nsMsgMessageFlags::Ignored;
+ else
+ msgFlags &= ~nsMsgMessageFlags::Ignored;
+ msg->SetFlags(msgFlags);
+
+ return NotifyHdrChangeAll(msg, oldFlags, msgFlags, instigator);
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::MarkThreadWatched(nsIMsgThread* thread, nsMsgKey threadKey,
+ bool bWatched,
+ nsIDBChangeListener* instigator) {
+ NS_ENSURE_ARG(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t oldThreadFlags =
+ threadFlags; // not quite right, since we probably want msg hdr flags.
+ if (bWatched) {
+ threadFlags |= nsMsgMessageFlags::Watched;
+ threadFlags &= ~nsMsgMessageFlags::Ignored; // watch is implicit un-ignore
+ } else
+ threadFlags &= ~nsMsgMessageFlags::Watched;
+ thread->SetFlags(threadFlags);
+
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ GetMsgHdrForKey(threadKey, getter_AddRefs(msg));
+ if (!msg) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ // We'll add the message flags to the thread flags when notifying, since
+ // notifications are supposed to be about messages, not threads.
+ uint32_t msgFlags;
+ msg->GetFlags(&msgFlags);
+
+ return NotifyHdrChangeAll(msg, oldThreadFlags | msgFlags,
+ threadFlags | msgFlags, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkMarked(nsMsgKey key, bool mark,
+ nsIDBChangeListener* instigator) {
+ return SetKeyFlag(key, mark, nsMsgMessageFlags::Marked, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkOffline(nsMsgKey key, bool offline,
+ nsIDBChangeListener* instigator) {
+ return SetKeyFlag(key, offline, nsMsgMessageFlags::Offline, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetStringProperty(nsMsgKey aKey,
+ const char* aProperty,
+ const nsACString& aValue) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(aKey, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+ return SetStringPropertyByHdr(msgHdr, aProperty, aValue);
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetStringPropertyByHdr(nsIMsgDBHdr* msgHdr,
+ const char* aProperty,
+ const nsACString& aValue) {
+ // don't do notifications if message not yet added to database.
+ // Ignore errors (consequences of failure are minor).
+ bool notify = true;
+ nsMsgKey key = nsMsgKey_None;
+ msgHdr->GetMessageKey(&key);
+ ContainsKey(key, &notify);
+
+ nsCString oldValue;
+ nsresult rv = msgHdr->GetStringProperty(aProperty, oldValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if no change to this string property, bail out
+ if (oldValue.Equals(aValue)) return NS_OK;
+
+ // Precall OnHdrPropertyChanged to store prechange status
+ nsTArray<uint32_t> statusArray(m_ChangeListeners.Length());
+ nsCOMPtr<nsIDBChangeListener> listener;
+ if (notify) {
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners(
+ m_ChangeListeners);
+ while (listeners.HasMore()) {
+ listener = listeners.GetNext();
+ // initialize |status| because some implementations of
+ // OnHdrPropertyChanged does not set the value.
+ uint32_t status = 0;
+ (void)listener->OnHdrPropertyChanged(msgHdr, nsCString(aProperty), true,
+ &status, nullptr);
+ // ignore errors, but append element to keep arrays in sync
+ statusArray.AppendElement(status);
+ }
+ }
+
+ rv = msgHdr->SetStringProperty(aProperty, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Postcall OnHdrPropertyChanged to process the change
+ if (notify) {
+ // if this is the junk score property notify, as long as we're not going
+ // from no value to non junk
+ if (!strcmp(aProperty, "junkscore") &&
+ !(oldValue.IsEmpty() && aValue.Equals("0")))
+ NotifyJunkScoreChanged(nullptr);
+
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners(
+ m_ChangeListeners);
+ for (uint32_t i = 0; listeners.HasMore() && i < statusArray.Length(); i++) {
+ listener = listeners.GetNext();
+ uint32_t status = statusArray[i];
+ (void)listener->OnHdrPropertyChanged(msgHdr, nsCString(aProperty), false,
+ &status, nullptr);
+ // ignore errors
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::SetUint32PropertyByHdr(nsIMsgDBHdr* aMsgHdr,
+ const char* aProperty, uint32_t aValue) {
+ // If no change to this property, bail out.
+ uint32_t oldValue;
+ nsresult rv = aMsgHdr->GetUint32Property(aProperty, &oldValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (oldValue == aValue) return NS_OK;
+
+ // Don't do notifications if message not yet added to database.
+ bool notify = true;
+ nsMsgKey key = nsMsgKey_None;
+ aMsgHdr->GetMessageKey(&key);
+ ContainsKey(key, &notify);
+
+ // Precall OnHdrPropertyChanged to store prechange status.
+ nsTArray<uint32_t> statusArray(m_ChangeListeners.Length());
+ nsCOMPtr<nsIDBChangeListener> listener;
+ if (notify) {
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners(
+ m_ChangeListeners);
+ while (listeners.HasMore()) {
+ listener = listeners.GetNext();
+ // initialize |status| because some implementations of
+ // OnHdrPropertyChanged does not set the value.
+ uint32_t status = 0;
+ (void)listener->OnHdrPropertyChanged(aMsgHdr, nsCString(aProperty), true,
+ &status, nullptr);
+ // Ignore errors, but append element to keep arrays in sync.
+ statusArray.AppendElement(status);
+ }
+ }
+
+ rv = aMsgHdr->SetUint32Property(aProperty, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Postcall OnHdrPropertyChanged to process the change.
+ if (notify) {
+ nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners(
+ m_ChangeListeners);
+ for (uint32_t i = 0; listeners.HasMore(); i++) {
+ listener = listeners.GetNext();
+ uint32_t status = statusArray[i];
+ (void)listener->OnHdrPropertyChanged(aMsgHdr, nsCString(aProperty), false,
+ &status, nullptr);
+ // Ignore errors.
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkImapDeleted(nsMsgKey key, bool deleted,
+ nsIDBChangeListener* instigator) {
+ return SetKeyFlag(key, deleted, nsMsgMessageFlags::IMAPDeleted, instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkMDNNeeded(
+ nsMsgKey key, bool bNeeded, nsIDBChangeListener* instigator /* = NULL */) {
+ return SetKeyFlag(key, bNeeded, nsMsgMessageFlags::MDNReportNeeded,
+ instigator);
+}
+
+nsresult nsMsgDatabase::MarkMDNSent(
+ nsMsgKey key, bool bSent, nsIDBChangeListener* instigator /* = NULL */) {
+ return SetKeyFlag(key, bSent, nsMsgMessageFlags::MDNReportSent, instigator);
+}
+
+nsresult nsMsgDatabase::IsMDNSent(nsMsgKey key, bool* pSent) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t flags;
+ (void)msgHdr->GetFlags(&flags);
+ *pSent = !!(flags & nsMsgMessageFlags::MDNReportSent);
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::SetKeyFlag(nsMsgKey key, bool set,
+ nsMsgMessageFlagType flag,
+ nsIDBChangeListener* instigator) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ return SetMsgHdrFlag(msgHdr, set, flag, instigator);
+}
+
+nsresult nsMsgDatabase::SetMsgHdrFlag(nsIMsgDBHdr* msgHdr, bool set,
+ nsMsgMessageFlagType flag,
+ nsIDBChangeListener* instigator) {
+ uint32_t oldFlags;
+ (void)msgHdr->GetFlags(&oldFlags);
+
+ if (!SetHdrFlag(msgHdr, set, flag)) return NS_OK;
+
+ uint32_t flags;
+ (void)msgHdr->GetFlags(&flags);
+
+ return NotifyHdrChangeAll(msgHdr, oldFlags, flags, instigator);
+}
+
+// Helper routine - lowest level of flag setting - returns true if flags change,
+// false otherwise.
+bool nsMsgDatabase::SetHdrFlag(nsIMsgDBHdr* msgHdr, bool bSet,
+ nsMsgMessageFlagType flag) {
+ uint32_t statusFlags;
+ (void)msgHdr->GetFlags(&statusFlags);
+ uint32_t currentStatusFlags = GetStatusFlags(msgHdr, statusFlags);
+ bool flagAlreadySet = (currentStatusFlags & flag) != 0;
+
+ if ((flagAlreadySet && !bSet) || (!flagAlreadySet && bSet)) {
+ uint32_t resultFlags;
+ if (bSet)
+ msgHdr->OrFlags(flag, &resultFlags);
+ else
+ msgHdr->AndFlags(~flag, &resultFlags);
+ return true;
+ }
+ return false;
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkHdrRead(nsIMsgDBHdr* msgHdr, bool bRead,
+ nsIDBChangeListener* instigator) {
+ bool isReadInDB = true;
+ nsresult rv = nsMsgDatabase::IsHeaderRead(msgHdr, &isReadInDB);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isRead = true;
+ rv = IsHeaderRead(msgHdr, &isRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if the flag is already correct in the db, don't change it.
+ // Check msg flags as well as IsHeaderRead in case it's a newsgroup
+ // and the msghdr flags are out of sync with the newsrc settings.
+ // (we could override this method for news db's, but it's a trivial fix here.
+ if (bRead != isRead || isRead != isReadInDB) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+
+ bool inDB = false;
+ (void)ContainsKey(msgKey, &inDB);
+
+ if (inDB) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ rv = GetThreadForMsgKey(msgKey, getter_AddRefs(threadHdr));
+ if (threadHdr) threadHdr->MarkChildRead(bRead);
+ }
+
+#ifndef MOZ_SUITE
+ if (bRead) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TB_MAILS_READ, 1);
+ }
+#endif
+
+ return MarkHdrReadInDB(msgHdr, bRead, instigator);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkHdrReplied(nsIMsgDBHdr* msgHdr, bool bReplied,
+ nsIDBChangeListener* instigator) {
+ return SetMsgHdrFlag(msgHdr, bReplied, nsMsgMessageFlags::Replied,
+ instigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkHdrMarked(nsIMsgDBHdr* msgHdr, bool mark,
+ nsIDBChangeListener* instigator) {
+ return SetMsgHdrFlag(msgHdr, mark, nsMsgMessageFlags::Marked, instigator);
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::MarkHdrNotNew(nsIMsgDBHdr* aMsgHdr,
+ nsIDBChangeListener* aInstigator) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ nsMsgKey msgKey;
+ aMsgHdr->GetMessageKey(&msgKey);
+ m_newSet.RemoveElement(msgKey);
+ return SetMsgHdrFlag(aMsgHdr, false, nsMsgMessageFlags::New, aInstigator);
+}
+
+NS_IMETHODIMP nsMsgDatabase::MarkAllRead(nsTArray<nsMsgKey>& aThoseMarked) {
+ aThoseMarked.ClearAndRetainStorage();
+
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ nsresult rv = EnumerateMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ rv = hdrs->GetNext(getter_AddRefs(msg));
+ if (NS_FAILED(rv)) break;
+
+ bool isRead;
+ IsHeaderRead(msg, &isRead);
+
+ if (!isRead) {
+ nsMsgKey key;
+ (void)msg->GetMessageKey(&key);
+ aThoseMarked.AppendElement(key);
+ rv = MarkHdrRead(msg, true, nullptr); // ### dmb - blow off error?
+ }
+ }
+
+ // force num new to 0.
+ int32_t numUnreadMessages;
+
+ rv = m_dbFolderInfo->GetNumUnreadMessages(&numUnreadMessages);
+ if (NS_SUCCEEDED(rv))
+ m_dbFolderInfo->ChangeNumUnreadMessages(-numUnreadMessages);
+ // caller will Commit the db, so no need to do it here.
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::AddToNewList(nsMsgKey key) {
+ // we add new keys in increasing order...
+ if (m_newSet.IsEmpty() || (m_newSet[m_newSet.Length() - 1] < key))
+ m_newSet.AppendElement(key);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ClearNewList(bool notify /* = FALSE */) {
+ if (notify && !m_newSet.IsEmpty()) // need to update view
+ {
+ nsTArray<nsMsgKey> saveNewSet;
+ // clear m_newSet so that the code that's listening to the key change
+ // doesn't think we have new messages and send notifications all over
+ // that we have new messages.
+ saveNewSet.SwapElements(m_newSet);
+ for (uint32_t elementIndex = saveNewSet.Length() - 1;; elementIndex--) {
+ nsMsgKey lastNewKey = saveNewSet.ElementAt(elementIndex);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(lastNewKey, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t flags;
+ (void)msgHdr->GetFlags(&flags);
+
+ if ((flags | nsMsgMessageFlags::New) != flags) {
+ msgHdr->AndFlags(~nsMsgMessageFlags::New, &flags);
+ NotifyHdrChangeAll(msgHdr, flags | nsMsgMessageFlags::New, flags,
+ nullptr);
+ }
+ }
+ if (elementIndex == 0) break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::HasNew(bool* _retval) {
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = (m_newSet.Length() > 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetFirstNew(nsMsgKey* result) {
+ bool hasnew;
+ nsresult rv = HasNew(&hasnew);
+ if (NS_FAILED(rv)) return rv;
+ *result = (hasnew) ? m_newSet.ElementAt(0) : nsMsgKey_None;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::EnumerateMessages(nsIMsgEnumerator** result) {
+ RememberLastUseTime();
+ NS_ENSURE_ARG_POINTER(result);
+ NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable,
+ nullptr, nullptr));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::ReverseEnumerateMessages(nsIMsgEnumerator** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable,
+ nullptr, nullptr, false));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::GetFilterEnumerator(
+ const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms, bool aReverse,
+ nsIMsgEnumerator** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ RefPtr<nsMsgFilteredDBEnumerator> e =
+ new nsMsgFilteredDBEnumerator(this, m_mdbAllMsgHeadersTable, aReverse);
+
+ NS_ENSURE_TRUE(e, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv = e->InitSearchSession(searchTerms, m_folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::SyncCounts() {
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ nsresult rv = EnumerateMessages(getter_AddRefs(hdrs));
+ if (NS_FAILED(rv)) return rv;
+ bool hasMore = false;
+
+ mdb_count numHdrsInTable = 0;
+ int32_t numUnread = 0;
+ int32_t numHdrs = 0;
+
+ if (m_mdbAllMsgHeadersTable)
+ m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrsInTable);
+ else
+ return NS_ERROR_NULL_POINTER;
+
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = hdrs->GetNext(getter_AddRefs(header));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken");
+ if (NS_FAILED(rv)) break;
+
+ bool isRead;
+ IsHeaderRead(header, &isRead);
+ if (!isRead) numUnread++;
+ numHdrs++;
+ }
+
+ int32_t oldTotal, oldUnread;
+ (void)m_dbFolderInfo->GetNumUnreadMessages(&oldUnread);
+ (void)m_dbFolderInfo->GetNumMessages(&oldTotal);
+ if (oldUnread != numUnread)
+ m_dbFolderInfo->ChangeNumUnreadMessages(numUnread - oldUnread);
+ if (oldTotal != numHdrs)
+ m_dbFolderInfo->ChangeNumMessages(numHdrs - oldTotal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ListAllKeys(nsTArray<nsMsgKey>& keys) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
+ RememberLastUseTime();
+ keys.Clear();
+
+ if (m_mdbAllMsgHeadersTable) {
+ uint32_t numMsgs = 0;
+ m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numMsgs);
+ keys.SetCapacity(numMsgs);
+ rv = m_mdbAllMsgHeadersTable->GetTableRowCursor(GetEnv(), -1,
+ getter_AddRefs(rowCursor));
+ while (NS_SUCCEEDED(rv) && rowCursor) {
+ mdbOid outOid;
+ mdb_pos outPos;
+
+ rv = rowCursor->NextRowOid(GetEnv(), &outOid, &outPos);
+ // is this right? Mork is returning a 0 id, but that should valid.
+ if (outPos < 0 || outOid.mOid_Id == (mdb_id)-1) break;
+ if (NS_SUCCEEDED(rv)) keys.AppendElement(outOid.mOid_Id);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::EnumerateThreads(nsIMsgThreadEnumerator** result) {
+ RememberLastUseTime();
+ NS_ADDREF(*result = new nsMsgDBThreadEnumerator(this, nullptr));
+ return NS_OK;
+}
+
+// only return headers with a particular flag set
+static nsresult nsMsgFlagSetFilter(nsIMsgDBHdr* msg, void* closure) {
+ uint32_t msgFlags, desiredFlags;
+ desiredFlags = *(uint32_t*)closure;
+ msg->GetFlags(&msgFlags);
+ return (msgFlags & desiredFlags) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDatabase::EnumerateMessagesWithFlag(nsIMsgEnumerator** result,
+ uint32_t* pFlag) {
+ RememberLastUseTime();
+ NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable,
+ nsMsgFlagSetFilter, pFlag));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::CreateNewHdr(nsMsgKey key, nsIMsgDBHdr** pnewHdr) {
+ nsresult err = NS_OK;
+ nsIMdbRow* hdrRow = nullptr;
+ struct mdbOid allMsgHdrsTableOID;
+
+ if (!pnewHdr || !m_mdbAllMsgHeadersTable || !m_mdbStore)
+ return NS_ERROR_NULL_POINTER;
+
+ if (key != nsMsgKey_None) {
+ allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken;
+ allMsgHdrsTableOID.mOid_Id = key; // presumes 0 is valid key value
+
+ err = m_mdbStore->GetRow(GetEnv(), &allMsgHdrsTableOID, &hdrRow);
+ if (!hdrRow)
+ err = m_mdbStore->NewRowWithOid(GetEnv(), &allMsgHdrsTableOID, &hdrRow);
+ } else {
+ // Mork will assign an ID to the new row, generally the next available ID.
+ err = m_mdbStore->NewRow(GetEnv(), m_hdrRowScopeToken, &hdrRow);
+ if (hdrRow) {
+ struct mdbOid oid;
+ hdrRow->GetOid(GetEnv(), &oid);
+ key = oid.mOid_Id;
+ } else {
+ // We failed to create a new row. That can happen if we run out of keys,
+ // which will force a reparse.
+ nsTArray<nsMsgKey> keys;
+ if (NS_SUCCEEDED(ListAllKeys(keys))) {
+ for (nsMsgKey key : keys) {
+ if (key >= kForceReparseKey) {
+ // Force a reparse.
+ if (m_dbFolderInfo)
+ m_dbFolderInfo->SetBooleanProperty("forceReparse", true);
+ break;
+ }
+ }
+ }
+ err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ }
+ if (NS_FAILED(err)) return err;
+ err = CreateMsgHdr(hdrRow, key, pnewHdr);
+ return err;
+}
+
+NS_IMETHODIMP nsMsgDatabase::AddNewHdrToDB(nsIMsgDBHdr* newHdr, bool notify) {
+ NS_ENSURE_ARG_POINTER(newHdr);
+ nsMsgHdr* hdr = static_cast<nsMsgHdr*>(newHdr); // closed system, cast ok
+ bool newThread;
+ bool hasKey = false;
+ nsMsgKey msgKey = nsMsgKey_None;
+ (void)hdr->GetMessageKey(&msgKey);
+ (void)ContainsKey(msgKey, &hasKey);
+ if (hasKey) {
+ NS_ERROR("adding hdr that already exists");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult err = ThreadNewHdr(hdr, newThread);
+ // we thread header before we add it to the all headers table
+ // so that subject and reference threading will work (otherwise,
+ // when we try to find the first header with the same subject or
+ // reference, we get the new header!)
+ if (NS_SUCCEEDED(err)) {
+ nsMsgKey key;
+ uint32_t flags;
+
+ newHdr->GetMessageKey(&key);
+ hdr->GetRawFlags(&flags);
+ // use raw flags instead of GetFlags, because GetFlags will
+ // pay attention to what's in m_newSet, and this new hdr isn't
+ // in m_newSet yet.
+ if (flags & nsMsgMessageFlags::New) {
+ uint32_t newFlags;
+ newHdr->AndFlags(~nsMsgMessageFlags::New,
+ &newFlags); // make sure not filed out
+ AddToNewList(key);
+ }
+ if (m_dbFolderInfo) {
+ m_dbFolderInfo->ChangeNumMessages(1);
+ bool isRead = true;
+ IsHeaderRead(newHdr, &isRead);
+ if (!isRead) m_dbFolderInfo->ChangeNumUnreadMessages(1);
+ m_dbFolderInfo->OnKeyAdded(key);
+ }
+
+ err = m_mdbAllMsgHeadersTable->AddRow(GetEnv(), hdr->GetMDBRow());
+ if (notify) {
+ nsMsgKey threadParent;
+
+ newHdr->GetThreadParent(&threadParent);
+ NotifyHdrAddedAll(newHdr, threadParent, flags, NULL);
+ }
+
+ if (UseCorrectThreading()) err = AddMsgRefsToHash(newHdr);
+ }
+ NS_ASSERTION(NS_SUCCEEDED(err), "error creating thread");
+ return err;
+}
+
+NS_IMETHODIMP nsMsgDatabase::CopyHdrFromExistingHdr(nsMsgKey key,
+ nsIMsgDBHdr* existingHdr,
+ bool addHdrToDB,
+ nsIMsgDBHdr** newHdr) {
+ nsresult err = NS_OK;
+
+ if (existingHdr) {
+ nsMsgHdr* sourceMsgHdr =
+ static_cast<nsMsgHdr*>(existingHdr); // closed system, cast ok
+ nsMsgHdr* destMsgHdr = nullptr;
+ CreateNewHdr(key, (nsIMsgDBHdr**)&destMsgHdr);
+ nsIMdbRow* sourceRow = sourceMsgHdr->GetMDBRow();
+ if (!destMsgHdr || !sourceRow) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsIMdbRow* destRow = destMsgHdr->GetMDBRow();
+ if (!destRow) return NS_ERROR_UNEXPECTED;
+
+ err = destRow->SetRow(GetEnv(), sourceRow);
+ if (NS_SUCCEEDED(err)) {
+ // we may have gotten the header from a cache - calling SetRow
+ // basically invalidates any cached values, so invalidate them.
+ destMsgHdr->ClearCachedValues();
+ if (addHdrToDB) err = AddNewHdrToDB(destMsgHdr, true);
+ if (NS_SUCCEEDED(err) && newHdr) *newHdr = destMsgHdr;
+ }
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::RowCellColumnTonsString(nsIMdbRow* hdrRow,
+ mdb_token columnToken,
+ nsAString& resultStr) {
+ NS_ENSURE_ARG_POINTER(hdrRow);
+
+ struct mdbYarn yarn;
+ nsresult rv = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ YarnTonsString(&yarn, resultStr);
+ return NS_OK;
+}
+
+// as long as the row still exists, and isn't changed, the returned const char
+// ** will be valid. But be very careful using this data - the caller should
+// never return it in turn to another caller.
+nsresult nsMsgDatabase::RowCellColumnToConstCharPtr(nsIMdbRow* hdrRow,
+ mdb_token columnToken,
+ const char** ptr) {
+ NS_ENSURE_ARG_POINTER(hdrRow);
+
+ struct mdbYarn yarn;
+ nsresult rv = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *ptr = (const char*)yarn.mYarn_Buf;
+ return NS_OK;
+}
+
+nsIMimeConverter* nsMsgDatabase::GetMimeConverter() {
+ if (!m_mimeConverter) {
+ // apply mime decode
+ m_mimeConverter = do_GetService("@mozilla.org/messenger/mimeconverter;1");
+ }
+ return m_mimeConverter;
+}
+
+nsresult nsMsgDatabase::GetEffectiveCharset(nsIMdbRow* row,
+ nsACString& resultCharset) {
+ resultCharset.Truncate();
+ nsresult rv = RowCellColumnToCharPtr(row, m_messageCharSetColumnToken,
+ getter_Copies(resultCharset));
+ if (NS_FAILED(rv) || resultCharset.IsEmpty() ||
+ resultCharset.EqualsLiteral("us-ascii")) {
+ resultCharset.AssignLiteral("UTF-8");
+ nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(m_folder));
+ if (newsfolder) newsfolder->GetCharset(resultCharset);
+ }
+ return rv;
+}
+
+nsresult nsMsgDatabase::RowCellColumnToMime2DecodedString(
+ nsIMdbRow* row, mdb_token columnToken, nsAString& resultStr) {
+ nsresult err = NS_OK;
+ const char* nakedString = nullptr;
+ err = RowCellColumnToConstCharPtr(row, columnToken, &nakedString);
+ if (NS_SUCCEEDED(err) && nakedString && strlen(nakedString)) {
+ GetMimeConverter();
+ if (m_mimeConverter) {
+ nsAutoString decodedStr;
+ nsCString charSet;
+ GetEffectiveCharset(row, charSet);
+
+ err = m_mimeConverter->DecodeMimeHeader(nakedString, charSet.get(), false,
+ true, resultStr);
+ }
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::RowCellColumnToAddressCollationKey(
+ nsIMdbRow* row, mdb_token colToken, nsTArray<uint8_t>& result) {
+ nsString sender;
+ nsresult rv = RowCellColumnToMime2DecodedString(row, colToken, sender);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString name;
+ ExtractName(DecodedHeader(sender), name);
+ return CreateCollationKey(name, result);
+}
+
+nsresult nsMsgDatabase::GetCollationKeyGenerator() {
+ if (!m_collationKeyGenerator) {
+ auto result = mozilla::intl::LocaleService::TryCreateComponent<Collator>();
+ if (result.isErr()) {
+ NS_WARNING("Could not create mozilla::intl::Collation.");
+ return NS_ERROR_FAILURE;
+ }
+
+ m_collationKeyGenerator = result.unwrap();
+
+ // Sort in a case-insensitive way, where "base" letters are considered
+ // equal, e.g: a = á, a = A, a ≠ b.
+ Collator::Options options{};
+ options.sensitivity = Collator::Sensitivity::Base;
+ auto optResult = m_collationKeyGenerator->SetOptions(options);
+
+ if (optResult.isErr()) {
+ NS_WARNING("Could not configure the mozilla::intl::Collation.");
+ m_collationKeyGenerator = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::RowCellColumnToCollationKey(nsIMdbRow* row,
+ mdb_token columnToken,
+ nsTArray<uint8_t>& result) {
+ const char* nakedString = nullptr;
+ nsresult err;
+
+ err = RowCellColumnToConstCharPtr(row, columnToken, &nakedString);
+ if (!nakedString) nakedString = "";
+ if (NS_SUCCEEDED(err)) {
+ GetMimeConverter();
+ if (m_mimeConverter) {
+ nsCString decodedStr;
+ nsCString charSet;
+ GetEffectiveCharset(row, charSet);
+
+ err = m_mimeConverter->DecodeMimeHeaderToUTF8(
+ nsDependentCString(nakedString), charSet.get(), false, true,
+ decodedStr);
+ if (NS_SUCCEEDED(err))
+ err = CreateCollationKey(NS_ConvertUTF8toUTF16(decodedStr), result);
+ }
+ }
+ return err;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::CompareCollationKeys(const nsTArray<uint8_t>& key1,
+ const nsTArray<uint8_t>& key2,
+ int32_t* result) {
+ nsresult rv = GetCollationKeyGenerator();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!m_collationKeyGenerator) return NS_ERROR_FAILURE;
+
+ *result = m_collationKeyGenerator->CompareSortKeys(key1, key2);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::CreateCollationKey(const nsAString& sourceString,
+ nsTArray<uint8_t>& key) {
+ nsresult err = GetCollationKeyGenerator();
+ NS_ENSURE_SUCCESS(err, err);
+ if (!m_collationKeyGenerator) return NS_ERROR_FAILURE;
+
+ nsTArrayU8Buffer buffer(key);
+
+ auto result = m_collationKeyGenerator->GetSortKey(sourceString, buffer);
+ NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::RowCellColumnToUInt32(nsIMdbRow* hdrRow,
+ mdb_token columnToken,
+ uint32_t& uint32Result,
+ uint32_t defaultValue) {
+ return RowCellColumnToUInt32(hdrRow, columnToken, &uint32Result,
+ defaultValue);
+}
+
+nsresult nsMsgDatabase::RowCellColumnToUInt32(nsIMdbRow* hdrRow,
+ mdb_token columnToken,
+ uint32_t* uint32Result,
+ uint32_t defaultValue) {
+ nsresult err = NS_OK;
+
+ if (uint32Result) *uint32Result = defaultValue;
+ if (hdrRow) // ### probably should be an error if hdrRow is NULL...
+ {
+ struct mdbYarn yarn;
+ err = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn);
+ if (NS_SUCCEEDED(err)) YarnToUInt32(&yarn, uint32Result);
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::UInt32ToRowCellColumn(nsIMdbRow* row,
+ mdb_token columnToken,
+ uint32_t value) {
+ struct mdbYarn yarn;
+ char yarnBuf[100];
+
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ yarn.mYarn_Buf = (void*)yarnBuf;
+ yarn.mYarn_Size = sizeof(yarnBuf);
+ yarn.mYarn_Fill = yarn.mYarn_Size;
+ yarn.mYarn_Form = 0;
+ yarn.mYarn_Grow = NULL;
+ return row->AddColumn(GetEnv(), columnToken, UInt32ToYarn(&yarn, value));
+}
+
+nsresult nsMsgDatabase::UInt64ToRowCellColumn(nsIMdbRow* row,
+ mdb_token columnToken,
+ uint64_t value) {
+ NS_ENSURE_ARG_POINTER(row);
+ struct mdbYarn yarn;
+ char yarnBuf[17]; // max string is 16 bytes, + 1 for null.
+
+ yarn.mYarn_Buf = (void*)yarnBuf;
+ yarn.mYarn_Size = sizeof(yarnBuf);
+ yarn.mYarn_Form = 0;
+ yarn.mYarn_Grow = NULL;
+ PR_snprintf((char*)yarn.mYarn_Buf, yarn.mYarn_Size, "%llx", value);
+ yarn.mYarn_Fill = PL_strlen((const char*)yarn.mYarn_Buf);
+ return row->AddColumn(GetEnv(), columnToken, &yarn);
+}
+
+nsresult nsMsgDatabase::RowCellColumnToUInt64(nsIMdbRow* hdrRow,
+ mdb_token columnToken,
+ uint64_t* uint64Result,
+ uint64_t defaultValue) {
+ nsresult err = NS_OK;
+
+ if (uint64Result) *uint64Result = defaultValue;
+ if (hdrRow) // ### probably should be an error if hdrRow is NULL...
+ {
+ struct mdbYarn yarn;
+ err = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn);
+ if (NS_SUCCEEDED(err)) YarnToUInt64(&yarn, uint64Result);
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::CharPtrToRowCellColumn(nsIMdbRow* row,
+ mdb_token columnToken,
+ const char* charPtr) {
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ struct mdbYarn yarn;
+ yarn.mYarn_Buf = (void*)charPtr;
+ yarn.mYarn_Size = PL_strlen((const char*)yarn.mYarn_Buf) + 1;
+ yarn.mYarn_Fill = yarn.mYarn_Size - 1;
+ yarn.mYarn_Form =
+ 0; // what to do with this? we're storing csid in the msg hdr...
+
+ return row->AddColumn(GetEnv(), columnToken, &yarn);
+}
+
+// caller must free result
+nsresult nsMsgDatabase::RowCellColumnToCharPtr(nsIMdbRow* row,
+ mdb_token columnToken,
+ char** result) {
+ nsresult err = NS_ERROR_NULL_POINTER;
+
+ if (row && result) {
+ struct mdbYarn yarn;
+ err = row->AliasCellYarn(GetEnv(), columnToken, &yarn);
+ if (NS_SUCCEEDED(err)) {
+ *result = (char*)moz_xmalloc(yarn.mYarn_Fill + 1);
+ if (*result) {
+ if (yarn.mYarn_Fill > 0)
+ memcpy(*result, yarn.mYarn_Buf, yarn.mYarn_Fill);
+ (*result)[yarn.mYarn_Fill] = '\0';
+ } else
+ err = NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ return err;
+}
+
+/* static */ struct mdbYarn* nsMsgDatabase::nsStringToYarn(
+ struct mdbYarn* yarn, const nsAString& str) {
+ yarn->mYarn_Buf = ToNewCString(NS_ConvertUTF16toUTF8(str));
+ yarn->mYarn_Size = strlen((const char*)yarn->mYarn_Buf) + 1;
+ yarn->mYarn_Fill = yarn->mYarn_Size - 1;
+ yarn->mYarn_Form =
+ 0; // what to do with this? we're storing csid in the msg hdr...
+ return yarn;
+}
+
+/* static */ struct mdbYarn* nsMsgDatabase::UInt32ToYarn(struct mdbYarn* yarn,
+ uint32_t i) {
+ PR_snprintf((char*)yarn->mYarn_Buf, yarn->mYarn_Size, "%lx", i);
+ yarn->mYarn_Fill = PL_strlen((const char*)yarn->mYarn_Buf);
+ yarn->mYarn_Form =
+ 0; // what to do with this? Should be parsed out of the mime2 header?
+ return yarn;
+}
+
+/* static */ struct mdbYarn* nsMsgDatabase::UInt64ToYarn(struct mdbYarn* yarn,
+ uint64_t i) {
+ PR_snprintf((char*)yarn->mYarn_Buf, yarn->mYarn_Size, "%llx", i);
+ yarn->mYarn_Fill = PL_strlen((const char*)yarn->mYarn_Buf);
+ yarn->mYarn_Form = 0;
+ return yarn;
+}
+
+/* static */ void nsMsgDatabase::YarnTonsString(struct mdbYarn* yarn,
+ nsAString& str) {
+ const char* buf = (const char*)yarn->mYarn_Buf;
+ if (buf)
+ CopyUTF8toUTF16(Substring(buf, buf + yarn->mYarn_Fill), str);
+ else
+ str.Truncate();
+}
+
+/* static */ void nsMsgDatabase::YarnTonsCString(struct mdbYarn* yarn,
+ nsACString& str) {
+ const char* buf = (const char*)yarn->mYarn_Buf;
+ if (buf)
+ str.Assign(buf, yarn->mYarn_Fill);
+ else
+ str.Truncate();
+}
+
+// WARNING - if yarn is empty, *pResult will not be changed!!!!
+// this is so we can leave default values as they were.
+/* static */ void nsMsgDatabase::YarnToUInt32(struct mdbYarn* yarn,
+ uint32_t* pResult) {
+ uint8_t numChars = std::min<mdb_fill>(8, yarn->mYarn_Fill);
+
+ if (numChars == 0) return;
+
+ *pResult = MsgUnhex((char*)yarn->mYarn_Buf, numChars);
+}
+
+// WARNING - if yarn is empty, *pResult will not be changed!!!!
+// this is so we can leave default values as they were.
+/* static */ void nsMsgDatabase::YarnToUInt64(struct mdbYarn* yarn,
+ uint64_t* pResult) {
+ uint8_t numChars = std::min<mdb_fill>(16, yarn->mYarn_Fill);
+
+ if (numChars == 0) return;
+
+ *pResult = MsgUnhex((char*)yarn->mYarn_Buf, numChars);
+}
+
+nsresult nsMsgDatabase::GetProperty(nsIMdbRow* row, const char* propertyName,
+ char** result) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ if (m_mdbStore)
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ else
+ err = NS_ERROR_NULL_POINTER;
+ if (NS_SUCCEEDED(err))
+ err = RowCellColumnToCharPtr(row, property_token, result);
+
+ return err;
+}
+
+nsresult nsMsgDatabase::SetProperty(nsIMdbRow* row, const char* propertyName,
+ const char* propertyVal) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err))
+ CharPtrToRowCellColumn(row, property_token, propertyVal);
+ return err;
+}
+
+nsresult nsMsgDatabase::GetPropertyAsNSString(nsIMdbRow* row,
+ const char* propertyName,
+ nsAString& result) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err))
+ err = RowCellColumnTonsString(row, property_token, result);
+
+ return err;
+}
+
+nsresult nsMsgDatabase::SetPropertyFromNSString(nsIMdbRow* row,
+ const char* propertyName,
+ const nsAString& propertyVal) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err))
+ return SetNSStringPropertyWithToken(row, property_token, propertyVal);
+
+ return err;
+}
+
+nsresult nsMsgDatabase::GetUint32Property(nsIMdbRow* row,
+ const char* propertyName,
+ uint32_t* result,
+ uint32_t defaultValue) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err))
+ err = RowCellColumnToUInt32(row, property_token, result, defaultValue);
+
+ return err;
+}
+
+nsresult nsMsgDatabase::GetUint64Property(nsIMdbRow* row,
+ const char* propertyName,
+ uint64_t* result,
+ uint64_t defaultValue) {
+ nsresult err = NS_OK;
+ mdb_token property_token;
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err))
+ err = RowCellColumnToUInt64(row, property_token, result, defaultValue);
+
+ return err;
+}
+
+nsresult nsMsgDatabase::SetUint32Property(nsIMdbRow* row,
+ const char* propertyName,
+ uint32_t propertyVal) {
+ struct mdbYarn yarn;
+ char int32StrBuf[20];
+ yarn.mYarn_Buf = int32StrBuf;
+ yarn.mYarn_Size = sizeof(int32StrBuf);
+ yarn.mYarn_Fill = sizeof(int32StrBuf);
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ mdb_token property_token;
+
+ nsresult err =
+ m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err)) {
+ UInt32ToYarn(&yarn, propertyVal);
+ err = row->AddColumn(GetEnv(), property_token, &yarn);
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::SetUint64Property(nsIMdbRow* row,
+ const char* propertyName,
+ uint64_t propertyVal) {
+ struct mdbYarn yarn;
+ char int64StrBuf[100];
+ yarn.mYarn_Buf = int64StrBuf;
+ yarn.mYarn_Size = sizeof(int64StrBuf);
+ yarn.mYarn_Fill = sizeof(int64StrBuf);
+
+ NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us.
+ if (!row) return NS_ERROR_NULL_POINTER;
+
+ mdb_token property_token;
+
+ nsresult err =
+ m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token);
+ if (NS_SUCCEEDED(err)) {
+ UInt64ToYarn(&yarn, propertyVal);
+ err = row->AddColumn(GetEnv(), property_token, &yarn);
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::GetBooleanProperty(nsIMdbRow* row,
+ const char* propertyName,
+ bool* result,
+ bool defaultValue /* = false */) {
+ uint32_t res;
+ nsresult rv =
+ GetUint32Property(row, propertyName, &res, (uint32_t)defaultValue);
+ *result = !!res;
+ return rv;
+}
+
+nsresult nsMsgDatabase::SetBooleanProperty(nsIMdbRow* row,
+ const char* propertyName,
+ bool propertyVal) {
+ return SetUint32Property(row, propertyName, (uint32_t)propertyVal);
+}
+
+nsresult nsMsgDatabase::SetNSStringPropertyWithToken(
+ nsIMdbRow* row, mdb_token aProperty, const nsAString& propertyStr) {
+ NS_ENSURE_ARG(row);
+ struct mdbYarn yarn;
+
+ yarn.mYarn_Grow = NULL;
+ nsresult err =
+ row->AddColumn(GetEnv(), aProperty, nsStringToYarn(&yarn, propertyStr));
+ free((char*)yarn.mYarn_Buf); // won't need this when we have nsCString
+ return err;
+}
+
+uint32_t nsMsgDatabase::GetCurVersion() { return kMsgDBVersion; }
+
+NS_IMETHODIMP nsMsgDatabase::SetSummaryValid(bool valid /* = true */) {
+ // If the file was invalid when opened (for example in folder compact), then
+ // it may
+ // not have been added to the cache. Add it now if missing.
+ if (valid) {
+ nsCOMPtr<nsIMsgDBService> serv(mozilla::components::DB::Service());
+ static_cast<nsMsgDBService*>(serv.get())->EnsureCached(this);
+ }
+ // setting the version to 0 ought to make it pretty invalid.
+ if (m_dbFolderInfo) m_dbFolderInfo->SetVersion(valid ? GetCurVersion() : 0);
+
+ // for default db (and news), there's no nothing to set to make it it valid
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetSummaryValid(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ return NS_OK;
+}
+
+// protected routines
+
+// should we thread messages with common subjects that don't start with Re:
+// together? I imagine we might have separate preferences for mail and news, so
+// this is a virtual method.
+bool nsMsgDatabase::ThreadBySubjectWithoutRe() {
+ GetGlobalPrefs();
+ return gThreadWithoutRe;
+}
+
+bool nsMsgDatabase::UseStrictThreading() {
+ GetGlobalPrefs();
+ return gStrictThreading;
+}
+
+// Should we make sure messages are always threaded correctly (see bug 181446)
+bool nsMsgDatabase::UseCorrectThreading() {
+ GetGlobalPrefs();
+ return gCorrectThreading;
+}
+
+// adapted from removed PL_DHashFreeStringKey
+static void msg_DHashFreeStringKey(PLDHashTable* aTable,
+ PLDHashEntryHdr* aEntry) {
+ const PLDHashEntryStub* stub = (const PLDHashEntryStub*)aEntry;
+ free((void*)stub->key);
+ PLDHashTable::ClearEntryStub(aTable, aEntry);
+}
+
+PLDHashTableOps nsMsgDatabase::gRefHashTableOps = {
+ PLDHashTable::HashStringKey, PLDHashTable::MatchStringKey,
+ PLDHashTable::MoveEntryStub, msg_DHashFreeStringKey, nullptr};
+
+nsresult nsMsgDatabase::GetRefFromHash(nsCString& reference,
+ nsMsgKey* threadId) {
+ // Initialize the reference hash
+ if (!m_msgReferences) {
+ nsresult rv = InitRefHash();
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Find reference from the hash
+ PLDHashEntryHdr* entry =
+ m_msgReferences->Search((const void*)reference.get());
+ if (entry) {
+ RefHashElement* element = static_cast<RefHashElement*>(entry);
+ *threadId = element->mThreadId;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDatabase::AddRefToHash(nsCString& reference, nsMsgKey threadId) {
+ if (m_msgReferences) {
+ PLDHashEntryHdr* entry =
+ m_msgReferences->Add((void*)reference.get(), mozilla::fallible);
+ if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory
+
+ RefHashElement* element = static_cast<RefHashElement*>(entry);
+ if (!element->mRef) {
+ element->mRef =
+ ToNewCString(reference); // Will be freed in msg_DHashFreeStringKey()
+ element->mThreadId = threadId;
+ element->mCount = 1;
+ } else
+ element->mCount++;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::AddMsgRefsToHash(nsIMsgDBHdr* msgHdr) {
+ uint16_t numReferences = 0;
+ nsMsgKey threadId;
+ nsresult rv = NS_OK;
+
+ msgHdr->GetThreadId(&threadId);
+ msgHdr->GetNumReferences(&numReferences);
+
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = AddRefToHash(reference, threadId);
+ if (NS_FAILED(rv)) break;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDatabase::RemoveRefFromHash(nsCString& reference) {
+ if (m_msgReferences) {
+ PLDHashEntryHdr* entry =
+ m_msgReferences->Search((const void*)reference.get());
+ if (entry) {
+ RefHashElement* element = static_cast<RefHashElement*>(entry);
+ if (--element->mCount == 0)
+ m_msgReferences->Remove((void*)reference.get());
+ }
+ }
+ return NS_OK;
+}
+
+// Filter only messages with one or more references
+nsresult nsMsgDatabase::RemoveMsgRefsFromHash(nsIMsgDBHdr* msgHdr) {
+ uint16_t numReferences = 0;
+ nsresult rv = NS_OK;
+
+ msgHdr->GetNumReferences(&numReferences);
+
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = RemoveRefFromHash(reference);
+ if (NS_FAILED(rv)) break;
+ }
+
+ return rv;
+}
+
+static nsresult nsReferencesOnlyFilter(nsIMsgDBHdr* msg, void* closure) {
+ uint16_t numReferences = 0;
+ msg->GetNumReferences(&numReferences);
+ return (numReferences) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDatabase::InitRefHash() {
+ // Delete an existing table just in case
+ if (m_msgReferences) delete m_msgReferences;
+
+ // Create new table
+ m_msgReferences = new PLDHashTable(
+ &gRefHashTableOps, sizeof(struct RefHashElement), MSG_HASH_SIZE);
+ if (!m_msgReferences) return NS_ERROR_OUT_OF_MEMORY;
+
+ // Create enumerator to go through all messages with references
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ enumerator = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable,
+ nsReferencesOnlyFilter, nullptr);
+ if (enumerator == nullptr) return NS_ERROR_OUT_OF_MEMORY;
+
+ // Populate table with references of existing messages
+ bool hasMore;
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = enumerator->GetNext(getter_AddRefs(msgHdr));
+ if (msgHdr && NS_SUCCEEDED(rv)) rv = AddMsgRefsToHash(msgHdr);
+ if (NS_FAILED(rv)) break;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDatabase::CreateNewThread(nsMsgKey threadId, const char* subject,
+ nsMsgThread** pnewThread) {
+ nsresult err = NS_OK;
+ nsCOMPtr<nsIMdbTable> threadTable;
+ struct mdbOid threadTableOID;
+ struct mdbOid allThreadsTableOID;
+
+ if (!pnewThread || !m_mdbStore) return NS_ERROR_NULL_POINTER;
+
+ threadTableOID.mOid_Scope = m_hdrRowScopeToken;
+ threadTableOID.mOid_Id = threadId;
+
+ // Under some circumstances, mork seems to reuse an old table when we create
+ // one. Prevent problems from that by finding any old table first, and
+ // deleting its rows.
+ nsresult res = GetStore()->GetTable(GetEnv(), &threadTableOID,
+ getter_AddRefs(threadTable));
+ if (NS_SUCCEEDED(res) && threadTable) threadTable->CutAllRows(GetEnv());
+
+ err = GetStore()->NewTableWithOid(GetEnv(), &threadTableOID,
+ m_threadTableKindToken, false, nullptr,
+ getter_AddRefs(threadTable));
+ if (NS_FAILED(err)) return err;
+
+ allThreadsTableOID.mOid_Scope = m_threadRowScopeToken;
+ allThreadsTableOID.mOid_Id = threadId;
+
+ // add a row for this thread in the table of all threads that we'll use
+ // to do our mapping between subject strings and threads.
+ nsCOMPtr<nsIMdbRow> threadRow;
+
+ err = m_mdbStore->GetRow(GetEnv(), &allThreadsTableOID,
+ getter_AddRefs(threadRow));
+ if (!threadRow) {
+ err = m_mdbStore->NewRowWithOid(GetEnv(), &allThreadsTableOID,
+ getter_AddRefs(threadRow));
+ if (NS_SUCCEEDED(err) && threadRow) {
+ if (m_mdbAllThreadsTable)
+ m_mdbAllThreadsTable->AddRow(GetEnv(), threadRow);
+ err = CharPtrToRowCellColumn(threadRow, m_threadSubjectColumnToken,
+ subject);
+ }
+ } else {
+#ifdef DEBUG_David_Bienvenu
+ NS_WARNING("odd that thread row already exists");
+#endif
+ threadRow->CutAllColumns(GetEnv());
+ nsCOMPtr<nsIMdbRow> metaRow;
+ threadTable->GetMetaRow(GetEnv(), nullptr, nullptr,
+ getter_AddRefs(metaRow));
+ if (metaRow) metaRow->CutAllColumns(GetEnv());
+
+ CharPtrToRowCellColumn(threadRow, m_threadSubjectColumnToken, subject);
+ }
+
+ *pnewThread = new nsMsgThread(this, threadTable);
+ if (*pnewThread) {
+ (*pnewThread)->SetThreadKey(threadId);
+ m_cachedThread = *pnewThread;
+ m_cachedThreadId = threadId;
+ }
+ return err;
+}
+
+nsIMsgThread* nsMsgDatabase::GetThreadForReference(nsCString& msgID,
+ nsIMsgDBHdr** pMsgHdr) {
+ nsMsgKey threadId;
+ nsIMsgDBHdr* msgHdr = nullptr;
+ GetMsgHdrForMessageID(msgID.get(), &msgHdr);
+ nsIMsgThread* thread = NULL;
+
+ if (msgHdr != NULL) {
+ if (NS_SUCCEEDED(msgHdr->GetThreadId(&threadId))) {
+ // find thread header for header whose message id we matched.
+ thread = GetThreadForThreadId(threadId);
+ }
+ if (pMsgHdr)
+ *pMsgHdr = msgHdr;
+ else
+ msgHdr->Release();
+ }
+ // Referenced message not found, check if there are messages that reference
+ // same message
+ else if (UseCorrectThreading()) {
+ if (NS_SUCCEEDED(GetRefFromHash(msgID, &threadId)))
+ thread = GetThreadForThreadId(threadId);
+ }
+
+ return thread;
+}
+
+nsIMsgThread* nsMsgDatabase::GetThreadForSubject(nsCString& subject) {
+ nsIMsgThread* thread = nullptr;
+
+ mdbYarn subjectYarn;
+
+ subjectYarn.mYarn_Buf = (void*)subject.get();
+ subjectYarn.mYarn_Fill = PL_strlen(subject.get());
+ subjectYarn.mYarn_Form = 0;
+ subjectYarn.mYarn_Size = subjectYarn.mYarn_Fill;
+
+ nsCOMPtr<nsIMdbRow> threadRow;
+ mdbOid outRowId;
+ if (m_mdbStore) {
+ nsresult result = m_mdbStore->FindRow(
+ GetEnv(), m_threadRowScopeToken, m_threadSubjectColumnToken,
+ &subjectYarn, &outRowId, getter_AddRefs(threadRow));
+ if (NS_SUCCEEDED(result) && threadRow) {
+ // Get key from row
+ mdbOid outOid;
+ nsMsgKey key = nsMsgKey_None;
+ if (NS_SUCCEEDED(threadRow->GetOid(GetEnv(), &outOid)))
+ key = outOid.mOid_Id;
+ // find thread header for header whose message id we matched.
+ // It is fine if key was not found,
+ // GetThreadForThreadId(nsMsgKey_None) returns nullptr.
+ thread = GetThreadForThreadId(key);
+ }
+#ifdef DEBUG_bienvenu1
+ else {
+ nsresult rv;
+ RefPtr<nsMsgThread> pThread;
+
+ nsCOMPtr<nsIMdbPortTableCursor> tableCursor;
+ m_mdbStore->GetPortTableCursor(GetEnv(), m_hdrRowScopeToken,
+ m_threadTableKindToken,
+ getter_AddRefs(tableCursor));
+
+ nsCOMPtr<nsIMdbTable> table;
+
+ while (true) {
+ rv = tableCursor->NextTable(GetEnv(), getter_AddRefs(table));
+ if (!table) break;
+ if (NS_FAILED(rv)) break;
+
+ pThread = new nsMsgThread(this, table);
+ if (pThread) {
+ nsCString curSubject;
+ pThread->GetSubject(curSubject);
+ if (subject.Equals(curSubject)) {
+ NS_ERROR("thread with subject exists, but FindRow didn't find it");
+ break;
+ }
+ } else
+ break;
+ }
+ }
+#endif
+ }
+ return thread;
+}
+
+// Returns thread that contains a message that references the passed message ID
+nsIMsgThread* nsMsgDatabase::GetThreadForMessageId(nsCString& msgId) {
+ nsIMsgThread* thread = NULL;
+ nsMsgKey threadId;
+
+ if (NS_SUCCEEDED(GetRefFromHash(msgId, &threadId)))
+ thread = GetThreadForThreadId(threadId);
+
+ return thread;
+}
+
+nsresult nsMsgDatabase::ThreadNewHdr(nsMsgHdr* newHdr, bool& newThread) {
+ nsresult result = NS_ERROR_UNEXPECTED;
+ nsCOMPtr<nsIMsgThread> thread;
+ nsCOMPtr<nsIMsgDBHdr> replyToHdr;
+ nsMsgKey threadId = nsMsgKey_None, newHdrKey;
+
+ if (!newHdr) return NS_ERROR_NULL_POINTER;
+
+ newHdr->SetThreadParent(
+ nsMsgKey_None); // if we're undoing, could have a thread parent
+ uint16_t numReferences = 0;
+ uint32_t newHdrFlags = 0;
+
+ // use raw flags instead of GetFlags, because GetFlags will
+ // pay attention to what's in m_newSet, and this new hdr isn't
+ // in m_newSet yet.
+ newHdr->GetRawFlags(&newHdrFlags);
+ newHdr->GetNumReferences(&numReferences);
+ newHdr->GetMessageKey(&newHdrKey);
+
+ // try reference threading first
+ for (int32_t i = numReferences - 1; i >= 0; i--) {
+ nsAutoCString reference;
+
+ newHdr->GetStringReference(i, reference);
+ // first reference we have hdr for is best top-level hdr.
+ // but we have to handle case of promoting new header to top-level
+ // in case the top-level header comes after a reply.
+
+ if (reference.IsEmpty()) break;
+
+ thread = dont_AddRef(
+ GetThreadForReference(reference, getter_AddRefs(replyToHdr)));
+ if (thread) {
+ if (replyToHdr) {
+ nsMsgKey replyToKey;
+ replyToHdr->GetMessageKey(&replyToKey);
+ // message claims to be a reply to itself - ignore that since it leads
+ // to corrupt threading.
+ if (replyToKey == newHdrKey) {
+ // bad references - throw them all away.
+ newHdr->SetMessageId("");
+ thread = nullptr;
+ break;
+ }
+ }
+ thread->GetThreadKey(&threadId);
+ newHdr->SetThreadId(threadId);
+ result = AddToThread(newHdr, thread, replyToHdr, true);
+ break;
+ }
+ }
+ // if user hasn't said "only thread by ref headers", thread by subject
+ if (!thread && !UseStrictThreading()) {
+ // try subject threading if we couldn't find a reference and the subject
+ // starts with Re:
+ nsCString subject;
+ newHdr->GetSubject(subject);
+ if (ThreadBySubjectWithoutRe() ||
+ (newHdrFlags & nsMsgMessageFlags::HasRe)) {
+ nsAutoCString cSubject(subject);
+ thread = dont_AddRef(GetThreadForSubject(cSubject));
+ if (thread) {
+ thread->GetThreadKey(&threadId);
+ newHdr->SetThreadId(threadId);
+ // TRACE("threading based on subject %s\n", (const char *)
+ // msgHdr->m_subject);
+ // if we move this and do subject threading after, ref threading,
+ // don't thread within children, since we know it won't work. But for
+ // now, pass TRUE.
+ result = AddToThread(newHdr, thread, nullptr, true);
+ }
+ }
+ }
+
+ // Check if this is a new parent to an existing message (that has a reference
+ // to this message)
+ if (!thread && UseCorrectThreading()) {
+ nsCString msgId;
+ newHdr->GetMessageId(getter_Copies(msgId));
+
+ thread = dont_AddRef(GetThreadForMessageId(msgId));
+ if (thread) {
+ thread->GetThreadKey(&threadId);
+ newHdr->SetThreadId(threadId);
+ result = AddToThread(newHdr, thread, nullptr, true);
+ }
+ }
+
+ if (!thread) {
+ // Not a parent or child, make it a new thread for now
+ result = AddNewThread(newHdr);
+ newThread = true;
+ } else {
+ newThread = false;
+ }
+ return result;
+}
+
+nsresult nsMsgDatabase::AddToThread(nsMsgHdr* newHdr, nsIMsgThread* thread,
+ nsIMsgDBHdr* inReplyTo,
+ bool threadInThread) {
+ // don't worry about real threading yet.
+ return thread->AddChild(newHdr, inReplyTo, threadInThread, this);
+}
+
+nsMsgHdr* nsMsgDatabase::GetMsgHdrForReference(nsCString& reference) {
+ NS_ASSERTION(false, "not implemented yet.");
+ return nullptr;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForMessageID(const char* aMsgID,
+ nsIMsgDBHdr** aHdr) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ NS_ENSURE_ARG_POINTER(aMsgID);
+ nsIMsgDBHdr* msgHdr = nullptr;
+ nsresult rv = NS_OK;
+ mdbYarn messageIdYarn;
+
+ messageIdYarn.mYarn_Buf = (void*)aMsgID;
+ messageIdYarn.mYarn_Fill = PL_strlen(aMsgID);
+ messageIdYarn.mYarn_Form = 0;
+ messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill;
+
+ nsIMdbRow* hdrRow;
+ mdbOid outRowId;
+ nsresult result;
+ if (m_mdbStore)
+ result = m_mdbStore->FindRow(GetEnv(), m_hdrRowScopeToken,
+ m_messageIdColumnToken, &messageIdYarn,
+ &outRowId, &hdrRow);
+ else
+ return NS_ERROR_FAILURE;
+ if (NS_SUCCEEDED(result) && hdrRow) {
+ // Get key from row
+ mdbOid outOid;
+ nsMsgKey key = nsMsgKey_None;
+ rv = hdrRow->GetOid(GetEnv(), &outOid);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ key = outOid.mOid_Id;
+
+ rv = CreateMsgHdr(hdrRow, key, &msgHdr);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ }
+ *aHdr = msgHdr; // already addreffed above.
+ return NS_OK; // it's not an error not to find a msg hdr.
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForGMMsgID(const char* aGMMsgId,
+ nsIMsgDBHdr** aHdr) {
+ NS_ENSURE_ARG_POINTER(aGMMsgId);
+ NS_ENSURE_ARG_POINTER(aHdr);
+ nsIMsgDBHdr* msgHdr = nullptr;
+ nsresult rv = NS_OK;
+ mdbYarn gMailMessageIdYarn;
+ gMailMessageIdYarn.mYarn_Buf = (void*)aGMMsgId;
+ gMailMessageIdYarn.mYarn_Fill = strlen(aGMMsgId);
+ gMailMessageIdYarn.mYarn_Form = 0;
+ gMailMessageIdYarn.mYarn_Size = gMailMessageIdYarn.mYarn_Fill;
+
+ nsIMdbRow* hdrRow;
+ mdbOid outRowId;
+ nsresult result;
+ mdb_token property_token;
+ NS_ENSURE_TRUE(m_mdbStore, NS_ERROR_NULL_POINTER);
+ result = m_mdbStore->StringToToken(GetEnv(), "X-GM-MSGID", &property_token);
+ NS_ENSURE_SUCCESS(result, result);
+ result = m_mdbStore->FindRow(GetEnv(), m_hdrRowScopeToken, property_token,
+ &gMailMessageIdYarn, &outRowId, &hdrRow);
+ if (NS_SUCCEEDED(result) && hdrRow) {
+ // Get key from row
+ mdbOid outOid;
+ rv = hdrRow->GetOid(GetEnv(), &outOid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey key = outOid.mOid_Id;
+ rv = CreateMsgHdr(hdrRow, key, &msgHdr);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ }
+ *aHdr = msgHdr;
+ return NS_OK; // it's not an error not to find a msg hdr.
+}
+
+nsIMsgDBHdr* nsMsgDatabase::GetMsgHdrForSubject(nsCString& subject) {
+ nsIMsgDBHdr* msgHdr = nullptr;
+ nsresult rv = NS_OK;
+ mdbYarn subjectYarn;
+
+ subjectYarn.mYarn_Buf = (void*)subject.get();
+ subjectYarn.mYarn_Fill = PL_strlen(subject.get());
+ subjectYarn.mYarn_Form = 0;
+ subjectYarn.mYarn_Size = subjectYarn.mYarn_Fill;
+
+ nsIMdbRow* hdrRow;
+ mdbOid outRowId;
+ nsresult result =
+ GetStore()->FindRow(GetEnv(), m_hdrRowScopeToken, m_subjectColumnToken,
+ &subjectYarn, &outRowId, &hdrRow);
+ if (NS_SUCCEEDED(result) && hdrRow) {
+ // Get key from row
+ mdbOid outOid;
+ nsMsgKey key = nsMsgKey_None;
+ rv = hdrRow->GetOid(GetEnv(), &outOid);
+ if (NS_WARN_IF(NS_FAILED(rv))) return nullptr;
+ key = outOid.mOid_Id;
+
+ rv = CreateMsgHdr(hdrRow, key, &msgHdr);
+ if (NS_WARN_IF(NS_FAILED(rv))) return nullptr;
+ }
+ return msgHdr;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** result) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(result);
+
+ *result = nullptr;
+ nsMsgKey threadId = nsMsgKey_None;
+ (void)msgHdr->GetThreadId(&threadId);
+ if (threadId != nsMsgKey_None) *result = GetThreadForThreadId(threadId);
+
+ // if we can't find the thread, try using the msg key as the thread id,
+ // because the msg hdr might not have the thread id set correctly
+ // Or maybe the message was deleted?
+ if (!*result) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ *result = GetThreadForThreadId(msgKey);
+ }
+ // failure is normal when message was deleted
+ return (*result) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDatabase::GetThreadForMsgKey(nsMsgKey msgKey,
+ nsIMsgThread** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ GetMsgHdrForKey(msgKey, getter_AddRefs(msg));
+ if (!msg) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ return GetThreadContainingMsgHdr(msg, aResult);
+}
+
+// caller needs to unrefer.
+nsIMsgThread* nsMsgDatabase::GetThreadForThreadId(nsMsgKey threadId) {
+ nsIMsgThread* retThread = (threadId == m_cachedThreadId && m_cachedThread)
+ ? m_cachedThread.get()
+ : FindExistingThread(threadId);
+ if (retThread) {
+ NS_ADDREF(retThread);
+ return retThread;
+ }
+ if (m_mdbStore) {
+ mdbOid tableId;
+ tableId.mOid_Id = threadId;
+ tableId.mOid_Scope = m_hdrRowScopeToken;
+
+ nsCOMPtr<nsIMdbTable> threadTable;
+ nsresult res =
+ m_mdbStore->GetTable(GetEnv(), &tableId, getter_AddRefs(threadTable));
+
+ if (NS_SUCCEEDED(res) && threadTable) {
+ retThread = new nsMsgThread(this, threadTable);
+ if (retThread) {
+ NS_ADDREF(retThread);
+ m_cachedThread = retThread;
+ m_cachedThreadId = threadId;
+ }
+ }
+ }
+ return retThread;
+}
+
+// make the passed in header a thread header
+nsresult nsMsgDatabase::AddNewThread(nsMsgHdr* msgHdr) {
+ if (!msgHdr) return NS_ERROR_NULL_POINTER;
+
+ nsMsgThread* threadHdr = nullptr;
+
+ nsCString subject;
+ nsMsgKey threadKey;
+ msgHdr->GetMessageKey(&threadKey);
+ // can't have a thread with key 1 since that's the table id of the all msg hdr
+ // table, so give it kTableKeyForThreadOne (0xfffffffe).
+ if (threadKey == kAllMsgHdrsTableKey) threadKey = kTableKeyForThreadOne;
+
+ nsresult err = msgHdr->GetSubject(subject);
+
+ err = CreateNewThread(threadKey, subject.get(), &threadHdr);
+ msgHdr->SetThreadId(threadKey);
+ if (threadHdr) {
+ NS_ADDREF(threadHdr);
+ // err = msgHdr->GetSubject(subject);
+ // threadHdr->SetThreadKey(msgHdr->m_messageKey);
+ // threadHdr->SetSubject(subject.get());
+ // need to add the thread table to the db.
+ AddToThread(msgHdr, threadHdr, nullptr, false);
+ NS_RELEASE(threadHdr);
+ }
+ return err;
+}
+
+nsresult nsMsgDatabase::GetBoolPref(const char* prefName, bool* result) {
+ bool prefValue = false;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (pPrefBranch) {
+ rv = pPrefBranch->GetBoolPref(prefName, &prefValue);
+ *result = prefValue;
+ }
+ return rv;
+}
+
+nsresult nsMsgDatabase::GetIntPref(const char* prefName, int32_t* result) {
+ int32_t prefValue = 0;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (pPrefBranch) {
+ rv = pPrefBranch->GetIntPref(prefName, &prefValue);
+ *result = prefValue;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetAttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr,
+ const char* property,
+ const char* propertyVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetUint32AttributeOnPendingHdr(
+ nsIMsgDBHdr* pendingHdr, const char* property, uint32_t propertyVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr,
+ const char* aProperty,
+ uint64_t aPropertyVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::UpdatePendingAttributes(nsIMsgDBHdr* aNewHdr) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgDatabase::GetOfflineOpForKey(
+ nsMsgKey msgKey, bool create, nsIMsgOfflineImapOperation** offlineOp) {
+ NS_ASSERTION(false, "overridden by nsMailDatabase");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDatabase::RemoveOfflineOp(nsIMsgOfflineImapOperation* op) {
+ NS_ASSERTION(false, "overridden by nsMailDatabase");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ListAllOfflineMsgs(nsTArray<nsMsgKey>& keys) {
+ keys.Clear();
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ uint32_t flag = nsMsgMessageFlags::Offline;
+ // if we change this routine to return an enumerator that generates the keys
+ // one by one, we'll need to somehow make a copy of flag for the enumerator
+ // to own, since the enumerator will persist past the life of flag on the
+ // stack.
+ nsresult rv = EnumerateMessagesWithFlag(getter_AddRefs(enumerator), &flag);
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMoreElements;
+ while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreElements)) &&
+ hasMoreElements) {
+ // clear out db hdr, because it won't be valid when we get rid of the .msf
+ // file
+ nsCOMPtr<nsIMsgDBHdr> dbMessage;
+ rv = enumerator->GetNext(getter_AddRefs(dbMessage));
+ if (NS_SUCCEEDED(rv) && dbMessage) {
+ nsMsgKey msgKey;
+ dbMessage->GetMessageKey(&msgKey);
+ keys.AppendElement(msgKey);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ListAllOfflineOpIds(
+ nsTArray<nsMsgKey>& offlineOpIds) {
+ NS_ASSERTION(false, "overridden by nsMailDatabase");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ListAllOfflineDeletes(
+ nsTArray<nsMsgKey>& offlineDeletes) {
+ // technically, notimplemented, but no one's putting offline ops in anyway.
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgDatabase::GetHighWaterArticleNum(nsMsgKey* key) {
+ if (!m_dbFolderInfo) return NS_ERROR_NULL_POINTER;
+ return m_dbFolderInfo->GetHighWater(key);
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetLowWaterArticleNum(nsMsgKey* key) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* attribute nsMsgKey NextPseudoMsgKey */
+
+NS_IMETHODIMP nsMsgDatabase::GetNextPseudoMsgKey(nsMsgKey* nextPseudoMsgKey) {
+ NS_ENSURE_ARG_POINTER(nextPseudoMsgKey);
+ *nextPseudoMsgKey = m_nextPseudoMsgKey--;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetNextPseudoMsgKey(nsMsgKey nextPseudoMsgKey) {
+ m_nextPseudoMsgKey = nextPseudoMsgKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetNextFakeOfflineMsgKey(
+ nsMsgKey* nextFakeOfflineMsgKey) {
+ NS_ENSURE_ARG_POINTER(nextFakeOfflineMsgKey);
+ // iterate over hdrs looking for first non-existent fake offline msg key
+ nsMsgKey fakeMsgKey = kIdStartOfFake;
+
+ bool containsKey;
+ do {
+ ContainsKey(fakeMsgKey, &containsKey);
+ if (!containsKey) break;
+ fakeMsgKey--;
+ } while (containsKey);
+
+ *nextFakeOfflineMsgKey = fakeMsgKey;
+ return NS_OK;
+}
+
+#ifdef DEBUG
+nsresult nsMsgDatabase::DumpContents() {
+ nsTArray<nsMsgKey> keys;
+ nsresult rv = ListAllKeys(keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsMsgKey key : keys) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsCString author;
+ nsCString subject;
+
+ msgHdr->GetMessageKey(&key);
+ msgHdr->GetAuthor(getter_Copies(author));
+ msgHdr->GetSubject(subject);
+ printf("hdr key = %u, author = %s subject = %s\n", key, author.get(),
+ subject.get());
+ }
+ }
+
+ nsCOMPtr<nsIMsgThreadEnumerator> threads;
+ rv = EnumerateThreads(getter_AddRefs(threads));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = threads->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = threads->GetNext(getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey key;
+ thread->GetThreadKey(&key);
+ printf("thread key = %u\n", key);
+ // DumpThread(key);
+ }
+ return NS_OK;
+}
+#endif /* DEBUG */
+
+NS_IMETHODIMP nsMsgDatabase::SetMsgRetentionSettings(
+ nsIMsgRetentionSettings* retentionSettings) {
+ m_retentionSettings = retentionSettings;
+ if (retentionSettings && m_dbFolderInfo) {
+ nsresult rv;
+
+ nsMsgRetainByPreference retainByPreference;
+ uint32_t daysToKeepHdrs;
+ uint32_t numHeadersToKeep;
+ uint32_t daysToKeepBodies;
+ bool cleanupBodiesByDays;
+ bool useServerDefaults;
+ bool applyToFlaggedMessages;
+
+ rv = retentionSettings->GetRetainByPreference(&retainByPreference);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = retentionSettings->GetDaysToKeepHdrs(&daysToKeepHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = retentionSettings->GetNumHeadersToKeep(&numHeadersToKeep);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = retentionSettings->GetDaysToKeepBodies(&daysToKeepBodies);
+ NS_ENSURE_SUCCESS(rv, rv);
+ (void)retentionSettings->GetCleanupBodiesByDays(&cleanupBodiesByDays);
+ (void)retentionSettings->GetUseServerDefaults(&useServerDefaults);
+ rv = retentionSettings->GetApplyToFlaggedMessages(&applyToFlaggedMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // need to write this to the db. We'll just use the dbfolderinfo to write
+ // properties.
+ m_dbFolderInfo->SetUint32Property("retainBy", retainByPreference);
+ m_dbFolderInfo->SetUint32Property("daysToKeepHdrs", daysToKeepHdrs);
+ m_dbFolderInfo->SetUint32Property("numHdrsToKeep", numHeadersToKeep);
+ m_dbFolderInfo->SetUint32Property("daysToKeepBodies", daysToKeepBodies);
+ m_dbFolderInfo->SetBooleanProperty("cleanupBodies", cleanupBodiesByDays);
+ m_dbFolderInfo->SetBooleanProperty("useServerDefaults", useServerDefaults);
+ m_dbFolderInfo->SetBooleanProperty("applyToFlaggedMessages",
+ applyToFlaggedMessages);
+ }
+ Commit(nsMsgDBCommitType::kLargeCommit);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetMsgRetentionSettings(
+ nsIMsgRetentionSettings** retentionSettings) {
+ NS_ENSURE_ARG_POINTER(retentionSettings);
+ if (!m_retentionSettings) {
+ // create a new one, and initialize it from the db.
+ m_retentionSettings = new nsMsgRetentionSettings;
+ if (m_retentionSettings && m_dbFolderInfo) {
+ nsMsgRetainByPreference retainByPreference;
+ uint32_t daysToKeepHdrs = 0;
+ uint32_t numHeadersToKeep = 0;
+ bool useServerDefaults;
+ uint32_t daysToKeepBodies = 0;
+ bool cleanupBodiesByDays = false;
+ bool applyToFlaggedMessages;
+
+ m_dbFolderInfo->GetUint32Property("retainBy",
+ nsIMsgRetentionSettings::nsMsgRetainAll,
+ &retainByPreference);
+ m_dbFolderInfo->GetUint32Property("daysToKeepHdrs", 0, &daysToKeepHdrs);
+ m_dbFolderInfo->GetUint32Property("numHdrsToKeep", 0, &numHeadersToKeep);
+ m_dbFolderInfo->GetUint32Property("daysToKeepBodies", 0,
+ &daysToKeepBodies);
+ m_dbFolderInfo->GetBooleanProperty("useServerDefaults", true,
+ &useServerDefaults);
+ m_dbFolderInfo->GetBooleanProperty("cleanupBodies", false,
+ &cleanupBodiesByDays);
+ m_dbFolderInfo->GetBooleanProperty("applyToFlaggedMessages", false,
+ &applyToFlaggedMessages);
+ m_retentionSettings->SetRetainByPreference(retainByPreference);
+ m_retentionSettings->SetDaysToKeepHdrs(daysToKeepHdrs);
+ m_retentionSettings->SetNumHeadersToKeep(numHeadersToKeep);
+ m_retentionSettings->SetDaysToKeepBodies(daysToKeepBodies);
+ m_retentionSettings->SetUseServerDefaults(useServerDefaults);
+ m_retentionSettings->SetCleanupBodiesByDays(cleanupBodiesByDays);
+ m_retentionSettings->SetApplyToFlaggedMessages(applyToFlaggedMessages);
+ }
+ }
+ NS_IF_ADDREF(*retentionSettings = m_retentionSettings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::SetMsgDownloadSettings(
+ nsIMsgDownloadSettings* downloadSettings) {
+ m_downloadSettings = downloadSettings;
+ if (downloadSettings && m_dbFolderInfo) {
+ nsresult rv;
+
+ bool useServerDefaults;
+ bool downloadByDate;
+ uint32_t ageLimitOfMsgsToDownload;
+ bool downloadUnreadOnly;
+
+ rv = downloadSettings->GetUseServerDefaults(&useServerDefaults);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = downloadSettings->GetDownloadByDate(&downloadByDate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = downloadSettings->GetAgeLimitOfMsgsToDownload(
+ &ageLimitOfMsgsToDownload);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // need to write this to the db. We'll just use the dbfolderinfo to write
+ // properties.
+ m_dbFolderInfo->SetBooleanProperty("useServerDefaults", useServerDefaults);
+ m_dbFolderInfo->SetBooleanProperty("downloadByDate", downloadByDate);
+ m_dbFolderInfo->SetBooleanProperty("downloadUnreadOnly",
+ downloadUnreadOnly);
+ m_dbFolderInfo->SetUint32Property("ageLimit", ageLimitOfMsgsToDownload);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetMsgDownloadSettings(
+ nsIMsgDownloadSettings** downloadSettings) {
+ NS_ENSURE_ARG_POINTER(downloadSettings);
+ if (!m_downloadSettings) {
+ // create a new one, and initialize it from the db.
+ m_downloadSettings = new nsMsgDownloadSettings;
+ if (m_downloadSettings && m_dbFolderInfo) {
+ bool useServerDefaults;
+ bool downloadByDate;
+ uint32_t ageLimitOfMsgsToDownload;
+ bool downloadUnreadOnly;
+
+ m_dbFolderInfo->GetBooleanProperty("useServerDefaults", true,
+ &useServerDefaults);
+ m_dbFolderInfo->GetBooleanProperty("downloadByDate", false,
+ &downloadByDate);
+ m_dbFolderInfo->GetBooleanProperty("downloadUnreadOnly", false,
+ &downloadUnreadOnly);
+ m_dbFolderInfo->GetUint32Property("ageLimit", 0,
+ &ageLimitOfMsgsToDownload);
+
+ m_downloadSettings->SetUseServerDefaults(useServerDefaults);
+ m_downloadSettings->SetDownloadByDate(downloadByDate);
+ m_downloadSettings->SetDownloadUnreadOnly(downloadUnreadOnly);
+ m_downloadSettings->SetAgeLimitOfMsgsToDownload(ageLimitOfMsgsToDownload);
+ }
+ }
+ NS_IF_ADDREF(*downloadSettings = m_downloadSettings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ApplyRetentionSettings(
+ nsIMsgRetentionSettings* aMsgRetentionSettings, bool aDeleteViaFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgRetentionSettings);
+ nsresult rv = NS_OK;
+
+ if (!m_folder) return NS_ERROR_NULL_POINTER;
+
+ bool isDraftsTemplatesOutbox;
+ uint32_t dtoFlags = nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates |
+ nsMsgFolderFlags::Queue;
+ (void)m_folder->IsSpecialFolder(dtoFlags, true, &isDraftsTemplatesOutbox);
+ // Never apply retention settings to Drafts/Templates/Outbox.
+ if (isDraftsTemplatesOutbox) return NS_OK;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsToDelete;
+ nsMsgRetainByPreference retainByPreference;
+ aMsgRetentionSettings->GetRetainByPreference(&retainByPreference);
+
+ bool applyToFlaggedMessages = false;
+ aMsgRetentionSettings->GetApplyToFlaggedMessages(&applyToFlaggedMessages);
+
+ uint32_t daysToKeepHdrs = 0;
+ uint32_t numHeadersToKeep = 0;
+ switch (retainByPreference) {
+ case nsIMsgRetentionSettings::nsMsgRetainAll:
+ break;
+ case nsIMsgRetentionSettings::nsMsgRetainByAge:
+ aMsgRetentionSettings->GetDaysToKeepHdrs(&daysToKeepHdrs);
+ rv = FindMessagesOlderThan(daysToKeepHdrs, applyToFlaggedMessages,
+ msgHdrsToDelete);
+ break;
+ case nsIMsgRetentionSettings::nsMsgRetainByNumHeaders:
+ aMsgRetentionSettings->GetNumHeadersToKeep(&numHeadersToKeep);
+ rv = FindExcessMessages(numHeadersToKeep, applyToFlaggedMessages,
+ msgHdrsToDelete);
+ break;
+ }
+ if (m_folder) {
+ // update the time we attempted to purge this folder
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
+ &exploded);
+ m_folder->SetStringProperty("LastPurgeTime", nsDependentCString(dateBuf));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (msgHdrsToDelete.IsEmpty()) {
+ return NS_OK; // No action required.
+ }
+
+ if (aDeleteViaFolder) {
+ // The folder delete will also delete headers from the DB.
+ rv = m_folder->DeleteMessages(msgHdrsToDelete, nullptr, true, false,
+ nullptr, false);
+ } else {
+ // We're just deleting headers in the DB.
+ uint32_t kindex = 0;
+ for (nsIMsgDBHdr* hdr : msgHdrsToDelete) {
+ // Commit after every 300.
+ rv = DeleteHeader(hdr, nullptr, kindex % 300, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ // compress commit if we deleted more than 10
+ if (msgHdrsToDelete.Length() > 10) {
+ Commit(nsMsgDBCommitType::kCompressCommit);
+ } else {
+ Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDatabase::FindMessagesOlderThan(
+ uint32_t daysToKeepHdrs, bool applyToFlaggedMessages,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete) {
+ nsresult rv = NS_OK;
+ hdrsToDelete.Clear();
+
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ rv = EnumerateMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cutOffDay is the PRTime cut-off point. Any msg with a date less than
+ // that will get purged.
+ PRTime cutOffDay = PR_Now() - daysToKeepHdrs * PR_USEC_PER_DAY;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ rv = hdrs->GetNext(getter_AddRefs(msg));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!applyToFlaggedMessages) {
+ uint32_t flags;
+ (void)msg->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Marked) {
+ continue;
+ }
+ }
+
+ PRTime date;
+ msg->GetDate(&date);
+ if (date < cutOffDay) {
+ hdrsToDelete.AppendElement(msg);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::FindExcessMessages(
+ uint32_t numHeadersToKeep, bool applyToFlaggedMessages,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete) {
+ nsresult rv = NS_OK;
+ hdrsToDelete.Clear();
+
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ rv = EnumerateMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mdb_count numHdrs = 0;
+ if (m_mdbAllMsgHeadersTable)
+ m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrs);
+ else
+ return NS_ERROR_NULL_POINTER;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ rv = hdrs->GetNext(getter_AddRefs(msg));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!applyToFlaggedMessages) {
+ uint32_t flags;
+ (void)msg->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Marked) {
+ continue;
+ }
+ }
+
+ // this isn't quite right - we want to prefer unread messages (keep all of
+ // those we can)
+ if (numHdrs > numHeadersToKeep) {
+ numHdrs--;
+ hdrsToDelete.AppendElement(msg);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgRetentionSettings, nsIMsgRetentionSettings)
+
+// Initialise the member variables to reasonable defaults.
+nsMsgRetentionSettings::nsMsgRetentionSettings()
+ : m_retainByPreference(1),
+ m_daysToKeepHdrs(0),
+ m_numHeadersToKeep(0),
+ m_useServerDefaults(true),
+ m_cleanupBodiesByDays(false),
+ m_daysToKeepBodies(0),
+ m_applyToFlaggedMessages(false) {}
+
+nsMsgRetentionSettings::~nsMsgRetentionSettings() {}
+
+/* attribute unsigned long retainByPreference */
+
+NS_IMETHODIMP nsMsgRetentionSettings::GetRetainByPreference(
+ nsMsgRetainByPreference* retainByPreference) {
+ NS_ENSURE_ARG_POINTER(retainByPreference);
+ *retainByPreference = m_retainByPreference;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgRetentionSettings::SetRetainByPreference(
+ nsMsgRetainByPreference retainByPreference) {
+ m_retainByPreference = retainByPreference;
+ return NS_OK;
+}
+
+/* attribute long daysToKeepHdrs; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetDaysToKeepHdrs(
+ uint32_t* aDaysToKeepHdrs) {
+ NS_ENSURE_ARG_POINTER(aDaysToKeepHdrs);
+ *aDaysToKeepHdrs = m_daysToKeepHdrs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgRetentionSettings::SetDaysToKeepHdrs(
+ uint32_t aDaysToKeepHdrs) {
+ m_daysToKeepHdrs = aDaysToKeepHdrs;
+ return NS_OK;
+}
+
+/* attribute long numHeadersToKeep; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetNumHeadersToKeep(
+ uint32_t* aNumHeadersToKeep) {
+ NS_ENSURE_ARG_POINTER(aNumHeadersToKeep);
+ *aNumHeadersToKeep = m_numHeadersToKeep;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgRetentionSettings::SetNumHeadersToKeep(
+ uint32_t aNumHeadersToKeep) {
+ m_numHeadersToKeep = aNumHeadersToKeep;
+ return NS_OK;
+}
+/* attribute boolean useServerDefaults; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetUseServerDefaults(
+ bool* aUseServerDefaults) {
+ NS_ENSURE_ARG_POINTER(aUseServerDefaults);
+ *aUseServerDefaults = m_useServerDefaults;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgRetentionSettings::SetUseServerDefaults(
+ bool aUseServerDefaults) {
+ m_useServerDefaults = aUseServerDefaults;
+ return NS_OK;
+}
+
+/* attribute boolean cleanupBodiesByDays; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetCleanupBodiesByDays(
+ bool* aCleanupBodiesByDays) {
+ NS_ENSURE_ARG_POINTER(aCleanupBodiesByDays);
+ *aCleanupBodiesByDays = m_cleanupBodiesByDays;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgRetentionSettings::SetCleanupBodiesByDays(
+ bool aCleanupBodiesByDays) {
+ m_cleanupBodiesByDays = aCleanupBodiesByDays;
+ return NS_OK;
+}
+
+/* attribute long daysToKeepBodies; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetDaysToKeepBodies(
+ uint32_t* aDaysToKeepBodies) {
+ NS_ENSURE_ARG_POINTER(aDaysToKeepBodies);
+ *aDaysToKeepBodies = m_daysToKeepBodies;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgRetentionSettings::SetDaysToKeepBodies(
+ uint32_t aDaysToKeepBodies) {
+ m_daysToKeepBodies = aDaysToKeepBodies;
+ return NS_OK;
+}
+
+/* attribute boolean applyToFlaggedMessages; */
+NS_IMETHODIMP nsMsgRetentionSettings::GetApplyToFlaggedMessages(
+ bool* aApplyToFlaggedMessages) {
+ NS_ENSURE_ARG_POINTER(aApplyToFlaggedMessages);
+ *aApplyToFlaggedMessages = m_applyToFlaggedMessages;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgRetentionSettings::SetApplyToFlaggedMessages(
+ bool aApplyToFlaggedMessages) {
+ m_applyToFlaggedMessages = aApplyToFlaggedMessages;
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgDownloadSettings, nsIMsgDownloadSettings)
+
+nsMsgDownloadSettings::nsMsgDownloadSettings() {
+ m_useServerDefaults = false;
+ m_downloadUnreadOnly = false;
+ m_downloadByDate = false;
+ m_ageLimitOfMsgsToDownload = 0;
+}
+
+nsMsgDownloadSettings::~nsMsgDownloadSettings() {}
+
+/* attribute boolean useServerDefaults; */
+NS_IMETHODIMP nsMsgDownloadSettings::GetUseServerDefaults(
+ bool* aUseServerDefaults) {
+ NS_ENSURE_ARG_POINTER(aUseServerDefaults);
+ *aUseServerDefaults = m_useServerDefaults;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgDownloadSettings::SetUseServerDefaults(
+ bool aUseServerDefaults) {
+ m_useServerDefaults = aUseServerDefaults;
+ return NS_OK;
+}
+
+/* attribute boolean downloadUnreadOnly; */
+NS_IMETHODIMP nsMsgDownloadSettings::GetDownloadUnreadOnly(
+ bool* aDownloadUnreadOnly) {
+ NS_ENSURE_ARG_POINTER(aDownloadUnreadOnly);
+ *aDownloadUnreadOnly = m_downloadUnreadOnly;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgDownloadSettings::SetDownloadUnreadOnly(
+ bool aDownloadUnreadOnly) {
+ m_downloadUnreadOnly = aDownloadUnreadOnly;
+ return NS_OK;
+}
+
+/* attribute boolean downloadByDate; */
+NS_IMETHODIMP nsMsgDownloadSettings::GetDownloadByDate(bool* aDownloadByDate) {
+ NS_ENSURE_ARG_POINTER(aDownloadByDate);
+ *aDownloadByDate = m_downloadByDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDownloadSettings::SetDownloadByDate(bool aDownloadByDate) {
+ m_downloadByDate = aDownloadByDate;
+ return NS_OK;
+}
+
+/* attribute long ageLimitOfMsgsToDownload; */
+NS_IMETHODIMP nsMsgDownloadSettings::GetAgeLimitOfMsgsToDownload(
+ uint32_t* ageLimitOfMsgsToDownload) {
+ NS_ENSURE_ARG_POINTER(ageLimitOfMsgsToDownload);
+ *ageLimitOfMsgsToDownload = m_ageLimitOfMsgsToDownload;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgDownloadSettings::SetAgeLimitOfMsgsToDownload(
+ uint32_t ageLimitOfMsgsToDownload) {
+ m_ageLimitOfMsgsToDownload = ageLimitOfMsgsToDownload;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetDefaultViewFlags(
+ nsMsgViewFlagsTypeValue* aDefaultViewFlags) {
+ NS_ENSURE_ARG_POINTER(aDefaultViewFlags);
+ GetIntPref("mailnews.default_view_flags", aDefaultViewFlags);
+ if (*aDefaultViewFlags < nsMsgViewFlagsType::kNone ||
+ *aDefaultViewFlags >
+ (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kShowIgnored | nsMsgViewFlagsType::kUnreadOnly |
+ nsMsgViewFlagsType::kExpandAll | nsMsgViewFlagsType::kGroupBySort))
+ *aDefaultViewFlags = nsMsgViewFlagsType::kNone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetDefaultSortType(
+ nsMsgViewSortTypeValue* aDefaultSortType) {
+ NS_ENSURE_ARG_POINTER(aDefaultSortType);
+ GetIntPref("mailnews.default_sort_type", aDefaultSortType);
+ if (*aDefaultSortType < nsMsgViewSortType::byDate ||
+ *aDefaultSortType > nsMsgViewSortType::byAccount)
+ *aDefaultSortType = nsMsgViewSortType::byDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::GetDefaultSortOrder(
+ nsMsgViewSortOrderValue* aDefaultSortOrder) {
+ NS_ENSURE_ARG_POINTER(aDefaultSortOrder);
+ GetIntPref("mailnews.default_sort_order", aDefaultSortOrder);
+ if (*aDefaultSortOrder != nsMsgViewSortOrder::descending)
+ *aDefaultSortOrder = nsMsgViewSortOrder::ascending;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::ResetHdrCacheSize(uint32_t aSize) {
+ if (m_cacheSize > aSize) {
+ m_cacheSize = aSize;
+ ClearHdrCache(false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::GetNewList(nsTArray<nsMsgKey>& aNewKeys) {
+ aNewKeys = m_newSet.Clone();
+ return NS_OK;
+}
+
+nsresult nsMsgDatabase::GetSearchResultsTable(const nsACString& searchFolderUri,
+ bool createIfMissing,
+ nsIMdbTable** table) {
+ mdb_kind kindToken;
+ mdb_count numTables;
+ mdb_bool mustBeUnique;
+ NS_ENSURE_TRUE(m_mdbStore, NS_ERROR_NULL_POINTER);
+
+ nsresult err = m_mdbStore->StringToToken(
+ GetEnv(), PromiseFlatCString(searchFolderUri).get(), &kindToken);
+ err = m_mdbStore->GetTableKind(GetEnv(), m_hdrRowScopeToken, kindToken,
+ &numTables, &mustBeUnique, table);
+ if ((!*table || NS_FAILED(err)) && createIfMissing)
+ err = m_mdbStore->NewTable(GetEnv(), m_hdrRowScopeToken, kindToken, true,
+ nullptr, table);
+
+ return *table ? err : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgDatabase::GetCachedHits(const nsACString& aSearchFolderUri,
+ nsIMsgEnumerator** aEnumerator) {
+ nsCOMPtr<nsIMdbTable> table;
+ (void)GetSearchResultsTable(aSearchFolderUri, false, getter_AddRefs(table));
+ if (!table) return NS_ERROR_FAILURE; // expected result for no cached hits
+ NS_ADDREF(*aEnumerator =
+ new nsMsgDBEnumerator(this, table, nullptr, nullptr));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDatabase::RefreshCache(const nsACString& aSearchFolderUri,
+ nsTArray<nsMsgKey> const& aNewHits,
+ nsTArray<nsMsgKey>& aStaleHits) {
+ nsCOMPtr<nsIMdbTable> table;
+ nsresult err =
+ GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(err, err);
+ // update the table so that it just contains aNewHits.
+ // And, keep track of the headers in the original table but not in aNewHits,
+ // so we can put those in aStaleHits. both aNewHits and the db table are
+ // sorted by uid/key. So, start at the beginning of the table and the aNewHits
+ // array.
+ uint32_t newHitIndex = 0;
+ uint32_t tableRowIndex = 0;
+
+ uint32_t rowCount;
+ table->GetCount(GetEnv(), &rowCount);
+ aStaleHits.Clear();
+
+#ifdef DEBUG
+ for (uint64_t i = 1; i < aNewHits.Length(); i++) {
+ NS_ASSERTION(aNewHits[i - 1] < aNewHits[i],
+ "cached hits for storage not sorted correctly");
+ }
+#endif
+
+ while (newHitIndex < aNewHits.Length() || tableRowIndex < rowCount) {
+ mdbOid oid;
+ nsMsgKey tableRowKey = nsMsgKey_None;
+ if (tableRowIndex < rowCount) {
+ nsresult ret = table->PosToOid(GetEnv(), tableRowIndex, &oid);
+ if (NS_FAILED(ret)) {
+ tableRowIndex++;
+ continue;
+ }
+ tableRowKey =
+ oid.mOid_Id; // ### TODO need the real key for the 0th key problem.
+ }
+
+ if (newHitIndex < aNewHits.Length() &&
+ aNewHits[newHitIndex] == tableRowKey) {
+ newHitIndex++;
+ tableRowIndex++;
+ continue;
+ } else if (tableRowIndex >= rowCount ||
+ (newHitIndex < aNewHits.Length() &&
+ aNewHits[newHitIndex] < tableRowKey)) {
+ nsCOMPtr<nsIMdbRow> hdrRow;
+ mdbOid rowObjectId;
+
+ rowObjectId.mOid_Id = aNewHits[newHitIndex];
+ rowObjectId.mOid_Scope = m_hdrRowScopeToken;
+ err = m_mdbStore->GetRow(GetEnv(), &rowObjectId, getter_AddRefs(hdrRow));
+ if (hdrRow) {
+ table->AddRow(GetEnv(), hdrRow);
+ mdb_pos newPos;
+ table->MoveRow(GetEnv(), hdrRow, rowCount, tableRowIndex, &newPos);
+ rowCount++;
+ tableRowIndex++;
+ }
+ newHitIndex++;
+ continue;
+ } else if (newHitIndex >= aNewHits.Length() ||
+ aNewHits[newHitIndex] > tableRowKey) {
+ aStaleHits.AppendElement(tableRowKey);
+ table->CutOid(GetEnv(), &oid);
+ rowCount--;
+ continue; // don't increment tableRowIndex since we removed that row.
+ }
+ }
+
+#ifdef DEBUG_David_Bienvenu
+ printf("after refreshing cache\n");
+ // iterate over table and assert that it's in id order
+ table->GetCount(GetEnv(), &rowCount);
+ mdbOid oid;
+ tableRowIndex = 0;
+ mdb_id prevId = 0;
+ while (tableRowIndex < rowCount) {
+ nsresult ret = table->PosToOid(m_mdbEnv, tableRowIndex++, &oid);
+ if (tableRowIndex > 1 && oid.mOid_Id <= prevId) {
+ NS_ASSERTION(
+ false, "inserting row into cached hits table, not sorted correctly");
+ printf("key %lx is before or equal %lx\n", prevId, oid.mOid_Id);
+ }
+ prevId = oid.mOid_Id;
+ }
+
+#endif
+ Commit(nsMsgDBCommitType::kLargeCommit);
+ return NS_OK;
+}
+
+// search sorted table
+mdb_pos nsMsgDatabase::FindInsertIndexInSortedTable(nsIMdbTable* table,
+ mdb_id idToInsert) {
+ mdb_pos searchPos = 0;
+ uint32_t rowCount;
+ table->GetCount(GetEnv(), &rowCount);
+ mdb_pos hi = rowCount;
+ mdb_pos lo = 0;
+
+ while (hi > lo) {
+ mdbOid outOid;
+ searchPos = (lo + hi - 1) / 2;
+ table->PosToOid(GetEnv(), searchPos, &outOid);
+ if (outOid.mOid_Id == idToInsert) {
+ NS_ASSERTION(false, "id shouldn't be in table");
+ return hi;
+ }
+ if (outOid.mOid_Id > idToInsert)
+ hi = searchPos;
+ else // if (outOid.mOid_Id < idToInsert)
+ lo = searchPos + 1;
+ }
+ return hi;
+}
+NS_IMETHODIMP
+nsMsgDatabase::UpdateHdrInCache(const nsACString& aSearchFolderUri,
+ nsIMsgDBHdr* aHdr, bool aAdd) {
+ nsCOMPtr<nsIMdbTable> table;
+ nsresult err =
+ GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(err, err);
+ nsMsgKey key;
+ err = aHdr->GetMessageKey(&key);
+ nsMsgHdr* msgHdr =
+ static_cast<nsMsgHdr*>(aHdr); // closed system, so this is ok
+ nsIMdbRow* hdrRow = msgHdr->GetMDBRow();
+ if (NS_SUCCEEDED(err) && m_mdbStore && hdrRow) {
+ if (!aAdd) {
+ table->CutRow(m_mdbEnv, hdrRow);
+ } else {
+ mdbOid rowId;
+ hdrRow->GetOid(m_mdbEnv, &rowId);
+ mdb_pos insertPos = FindInsertIndexInSortedTable(table, rowId.mOid_Id);
+ uint32_t rowCount;
+ table->GetCount(m_mdbEnv, &rowCount);
+ table->AddRow(m_mdbEnv, hdrRow);
+ mdb_pos newPos;
+ table->MoveRow(m_mdbEnv, hdrRow, rowCount, insertPos, &newPos);
+ }
+ }
+
+ // if (aAdd)
+ // if we need to add this hdr, we need to insert it in key order.
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsMsgDatabase::HdrIsInCache(const nsACString& aSearchFolderUri,
+ nsIMsgDBHdr* aHdr, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsCOMPtr<nsIMdbTable> table;
+ nsresult err =
+ GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table));
+ NS_ENSURE_SUCCESS(err, err);
+ nsMsgKey key;
+ aHdr->GetMessageKey(&key);
+ mdbOid rowObjectId;
+ rowObjectId.mOid_Id = key;
+ rowObjectId.mOid_Scope = m_hdrRowScopeToken;
+ mdb_bool hasOid;
+ err = table->HasOid(GetEnv(), &rowObjectId, &hasOid);
+ *aResult = hasOid;
+ return err;
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp
new file mode 100644
index 0000000000..a64ef46f57
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp
@@ -0,0 +1,317 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgDatabaseEnumerators.h"
+#include "nsMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgThread.h"
+
+/*
+ * nsMsgDBEnumerator implementation
+ */
+
+nsMsgDBEnumerator::nsMsgDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table,
+ nsMsgDBEnumeratorFilter filter,
+ void* closure, bool iterateForwards)
+ : mDB(db),
+ mDone(false),
+ mIterateForwards(iterateForwards),
+ mFilter(filter),
+ mClosure(closure) {
+ mTable = table;
+ mRowPos = 0;
+ mDB->m_msgEnumerators.AppendElement(this);
+}
+
+nsMsgDBEnumerator::~nsMsgDBEnumerator() { Invalidate(); }
+
+void nsMsgDBEnumerator::Invalidate() {
+ // Order is important here. If the database is destroyed first, releasing
+ // the cursor will crash (due, I think, to a disconnect between XPCOM and
+ // Mork internal memory management).
+ mRowCursor = nullptr;
+ mTable = nullptr;
+ mResultHdr = nullptr;
+ mDone = true;
+ if (mDB) {
+ mDB->m_msgEnumerators.RemoveElement(this);
+ mDB = nullptr;
+ }
+}
+
+nsresult nsMsgDBEnumerator::GetRowCursor() {
+ mDone = false;
+
+ if (!mDB || !mTable) return NS_ERROR_NULL_POINTER;
+
+ if (mIterateForwards) {
+ mRowPos = -1;
+ } else {
+ mdb_count numRows;
+ mTable->GetCount(mDB->GetEnv(), &numRows);
+ mRowPos = numRows; // startPos is 0 relative.
+ }
+ return mTable->GetTableRowCursor(mDB->GetEnv(), mRowPos,
+ getter_AddRefs(mRowCursor));
+}
+
+NS_IMETHODIMP nsMsgDBEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ if (!aItem) return NS_ERROR_NULL_POINTER;
+ *aItem = nullptr;
+
+ // If we've already got one ready, return it.
+ if (mResultHdr) {
+ mResultHdr.forget(aItem);
+ return NS_OK;
+ }
+
+ // Bail out if enumerator has been invalidated.
+ if (!mDB) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = InternalGetNext(aItem);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return *aItem ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDBEnumerator::InternalGetNext(nsIMsgDBHdr** nextHdr) {
+ nsresult rv;
+
+ *nextHdr = nullptr;
+
+ if (!mRowCursor) {
+ rv = GetRowCursor();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ while (true) {
+ nsIMdbRow* hdrRow;
+ if (mIterateForwards) {
+ rv = mRowCursor->NextRow(mDB->GetEnv(), &hdrRow, &mRowPos);
+ } else {
+ rv = mRowCursor->PrevRow(mDB->GetEnv(), &hdrRow, &mRowPos);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hdrRow) {
+ // No more rows, so we're done.
+ *nextHdr = nullptr;
+ return NS_OK;
+ }
+
+ // Get key from row
+ mdbOid outOid;
+ nsMsgKey key = nsMsgKey_None;
+ rv = hdrRow->GetOid(mDB->GetEnv(), &outOid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ key = outOid.mOid_Id;
+
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDB->CreateMsgHdr(hdrRow, key, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ignore expunged messages.
+ uint32_t flags;
+ hdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Expunged) {
+ continue;
+ }
+
+ // Ignore anything which doesn't pass the filter func (if there is one).
+ if (mFilter && NS_FAILED(mFilter(hdr, mClosure))) {
+ continue;
+ }
+
+ // If we get this far, we've found it.
+ hdr.forget(nextHdr);
+ return NS_OK;
+ }
+}
+
+NS_IMETHODIMP nsMsgDBEnumerator::HasMoreElements(bool* aResult) {
+ if (!aResult) return NS_ERROR_NULL_POINTER;
+
+ if (!mResultHdr) {
+ // Bail out if enumerator has been invalidated.
+ if (!mDB) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = InternalGetNext(getter_AddRefs(mResultHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mResultHdr) {
+ mDone = true;
+ }
+ }
+
+ *aResult = !mDone;
+ return NS_OK;
+}
+
+/*
+ * nsMsgFilteredDBEnumerator implementation
+ */
+
+nsMsgFilteredDBEnumerator::nsMsgFilteredDBEnumerator(nsMsgDatabase* db,
+ nsIMdbTable* table,
+ bool reverse)
+ : nsMsgDBEnumerator(db, table, nullptr, nullptr, !reverse) {}
+
+nsMsgFilteredDBEnumerator::~nsMsgFilteredDBEnumerator() {}
+
+/**
+ * Create the search session for the enumerator,
+ * add the scope term for "folder" to the search session, and add the search
+ * terms in the array to the search session.
+ */
+nsresult nsMsgFilteredDBEnumerator::InitSearchSession(
+ const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms,
+ nsIMsgFolder* folder) {
+ nsresult rv;
+ m_searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, folder);
+ for (auto searchTerm : searchTerms) {
+ m_searchSession->AppendTerm(searchTerm);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgFilteredDBEnumerator::InternalGetNext(nsIMsgDBHdr** nextHdr) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ while (true) {
+ nsresult rv = nsMsgDBEnumerator::InternalGetNext(getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hdr) {
+ break; // No more.
+ }
+ bool matches;
+ rv = m_searchSession->MatchHdr(hdr, mDB, &matches);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (matches) {
+ break; // Found one!
+ }
+ }
+ hdr.forget(nextHdr);
+ return NS_OK;
+}
+
+/*
+ * nsMsgDBThreadEnumerator implementation
+ */
+
+nsMsgDBThreadEnumerator::nsMsgDBThreadEnumerator(
+ nsMsgDatabase* db, nsMsgDBThreadEnumeratorFilter filter)
+ : mDB(db),
+ mTableCursor(nullptr),
+ mResultThread(nullptr),
+ mDone(false),
+ mFilter(filter) {
+ mDB->m_threadEnumerators.AppendElement(this);
+ mNextPrefetched = false;
+}
+
+nsMsgDBThreadEnumerator::~nsMsgDBThreadEnumerator() { Invalidate(); }
+
+void nsMsgDBThreadEnumerator::Invalidate() {
+ // Order is important here. If the database is destroyed first, releasing
+ // the cursor will crash (due, I think, to a disconnect between XPCOM and
+ // Mork internal memory management).
+ mTableCursor = nullptr;
+ mResultThread = nullptr;
+ mDone = true;
+ if (mDB) {
+ mDB->m_threadEnumerators.RemoveElement(this);
+ mDB = nullptr;
+ }
+}
+
+nsresult nsMsgDBThreadEnumerator::GetTableCursor(void) {
+ nsresult rv = NS_OK;
+
+ // DB might have disappeared.
+ if (!mDB || !mDB->m_mdbStore) return NS_ERROR_NULL_POINTER;
+ if (NS_FAILED(rv)) return rv;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBThreadEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!mNextPrefetched) {
+ PrefetchNext();
+ }
+ *aResult = !mDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBThreadEnumerator::GetNext(nsIMsgThread** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ *aItem = nullptr;
+ nsresult rv = NS_OK;
+ if (!mNextPrefetched) rv = PrefetchNext();
+ if (NS_SUCCEEDED(rv)) {
+ if (mResultThread) {
+ NS_ADDREF(*aItem = mResultThread);
+ mNextPrefetched = false;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBThreadEnumerator::PrefetchNext() {
+ nsresult rv;
+
+ // DB might have disappeared.
+ if (!mDB || !mDB->m_mdbStore) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!mTableCursor) {
+ rv = mDB->m_mdbStore->GetPortTableCursor(
+ mDB->GetEnv(), mDB->m_hdrRowScopeToken, mDB->m_threadTableKindToken,
+ getter_AddRefs(mTableCursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMdbTable> table;
+ while (true) {
+ mResultThread = nullptr;
+ rv = mTableCursor->NextTable(mDB->GetEnv(), getter_AddRefs(table));
+ if (!table) {
+ mDone = true;
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mDone = true;
+ return rv;
+ }
+
+ mdbOid tableId;
+ table->GetOid(mDB->GetEnv(), &tableId);
+
+ mResultThread = mDB->FindExistingThread(tableId.mOid_Id);
+ if (!mResultThread) mResultThread = new nsMsgThread(mDB, table);
+
+ if (mResultThread) {
+ uint32_t numChildren = 0;
+ mResultThread->GetNumChildren(&numChildren);
+ // we've got empty thread; don't tell caller about it.
+ if (numChildren == 0) continue;
+ }
+ if (mFilter && NS_FAILED(mFilter(mResultThread)))
+ continue;
+ else
+ break;
+ }
+ if (mResultThread) {
+ mNextPrefetched = true;
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h
new file mode 100644
index 0000000000..f68b084611
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgDatabaseEnumerators_H_
+#define _nsMsgDatabaseEnumerators_H_
+
+/*
+ * This file provides some enumerator classes, private to nsMsgDatabase.
+ * The outside world would only ever see these as nsIMsgEnumerator or
+ * nsIMsgThreadEnumerator.
+ *
+ * These enumerators automatically register themselves with the nsMsgDatabase
+ * during construction/destruction. This lets the database track all
+ * outstanding enumerators, so they can be invalidated if the database is
+ * destroyed or ForceClosed().
+ * Due to this lifecycle coupling, we try to avoid using refcounted pointers
+ * here, as we don't want outstanding enumerators to lock an otherwise unused
+ * database in existence.
+ */
+
+#include "nsMsgEnumerator.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "mdb.h"
+#include "nsIDBChangeListener.h"
+
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgSearchSession.h"
+
+class nsMsgDatabase;
+class nsIMdbTable;
+class nsIMdbTableRowCursor;
+class nsIMsgFolder;
+
+/**
+ * Enumerates over messages, forwards or backward, with an optional filter fn.
+ */
+class nsMsgDBEnumerator : public nsBaseMsgEnumerator {
+ public:
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ // Function type for filtering which messages are enumerated.
+ typedef nsresult (*nsMsgDBEnumeratorFilter)(nsIMsgDBHdr* hdr, void* closure);
+
+ nsMsgDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table,
+ nsMsgDBEnumeratorFilter filter, void* closure,
+ bool iterateForwards = true);
+ // Called by db when no longer valid (db is being destroyed or ForcedClosed).
+ void Invalidate();
+
+ protected:
+ // internals
+ nsresult GetRowCursor();
+
+ // Returns next message or nullptr if none more.
+ virtual nsresult InternalGetNext(nsIMsgDBHdr** nextHdr);
+
+ // Our source DB. Not refcounted, because we don't want to lock the DB
+ // in existence. The enumerator is registered with the DB, and the DB will
+ // call Invalidate() if it is destroyed or ForceClosed().
+ nsMsgDatabase* mDB;
+ nsCOMPtr<nsIMdbTableRowCursor> mRowCursor;
+ mdb_pos mRowPos;
+ nsCOMPtr<nsIMsgDBHdr> mResultHdr;
+ bool mDone;
+ bool mIterateForwards;
+ nsMsgDBEnumeratorFilter mFilter;
+ nsIMdbTable* mTable;
+ void* mClosure;
+
+ virtual ~nsMsgDBEnumerator() override;
+};
+
+/**
+ * Enumerate over messages which match the given search terms.
+ */
+class nsMsgFilteredDBEnumerator : public nsMsgDBEnumerator {
+ public:
+ nsMsgFilteredDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table,
+ bool reverse);
+ virtual ~nsMsgFilteredDBEnumerator() override;
+ nsresult InitSearchSession(
+ const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms,
+ nsIMsgFolder* folder);
+
+ protected:
+ virtual nsresult InternalGetNext(nsIMsgDBHdr** nextHdr) override;
+
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+};
+
+/**
+ * Helper class for fetching message threads from a database.
+ * This enumerator automatically registers itself with the nsMsgDatabase.
+ * If the DB is destroyed or ForceClosed() it will call the enumerators
+ * Invalidate() method.
+ */
+class nsMsgDBThreadEnumerator : public nsBaseMsgThreadEnumerator {
+ public:
+ // nsIMsgThreadEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgThread** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ // Function type for filtering threads that appear in the enumeration.
+ typedef nsresult (*nsMsgDBThreadEnumeratorFilter)(nsIMsgThread* thread);
+
+ nsMsgDBThreadEnumerator(nsMsgDatabase* db,
+ nsMsgDBThreadEnumeratorFilter filter);
+
+ // Called by DB when being destroyed or ForcedClosed.
+ void Invalidate();
+
+ protected:
+ virtual ~nsMsgDBThreadEnumerator();
+ nsresult GetTableCursor(void);
+ nsresult PrefetchNext();
+
+ // Our source DB. Not refcounted, because we don't want to lock the DB
+ // in existence. The enumerator is registered with the DB, and the DB will
+ // call Invalidate() if it is destroyed or ForceClosed().
+ nsMsgDatabase* mDB;
+ nsCOMPtr<nsIMdbPortTableCursor> mTableCursor;
+ RefPtr<nsIMsgThread> mResultThread;
+ bool mDone;
+ bool mNextPrefetched;
+ nsMsgDBThreadEnumeratorFilter mFilter;
+};
+
+#endif // _nsMsgDatabaseEnumerators_H_
diff --git a/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp b/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp
new file mode 100644
index 0000000000..c3d36d9b9b
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp
@@ -0,0 +1,936 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsMsgHdr.h"
+#include "nsMsgDatabase.h"
+#include "nsMsgUtils.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgThread.h"
+#include "mozilla/Attributes.h"
+#include "nsStringEnumerator.h"
+#ifdef DEBUG
+# include "nsPrintfCString.h"
+#endif
+using namespace mozilla::mailnews;
+
+NS_IMPL_ISUPPORTS(nsMsgHdr, nsIMsgDBHdr)
+
+#define FLAGS_INITED 0x1
+#define CACHED_VALUES_INITED 0x2
+#define REFERENCES_INITED 0x4
+#define THREAD_PARENT_INITED 0x8
+
+nsMsgHdr::nsMsgHdr(nsMsgDatabase* db, nsIMdbRow* dbRow) {
+ m_mdb = db;
+ Init();
+ m_mdbRow = dbRow;
+ if (m_mdb) {
+ NS_ADDREF(m_mdb); // Released in DTOR.
+ mdbOid outOid;
+ if (dbRow && NS_SUCCEEDED(dbRow->GetOid(m_mdb->GetEnv(), &outOid))) {
+ m_messageKey = outOid.mOid_Id;
+ m_mdb->AddHdrToUseCache((nsIMsgDBHdr*)this, m_messageKey);
+ }
+ }
+}
+
+void nsMsgHdr::Init() {
+ m_initedValues = 0;
+ m_messageKey = nsMsgKey_None;
+ m_messageSize = 0;
+ m_date = 0;
+ m_flags = 0;
+ m_mdbRow = NULL;
+ m_threadId = nsMsgKey_None;
+ m_threadParent = nsMsgKey_None;
+}
+
+nsresult nsMsgHdr::InitCachedValues() {
+ nsresult err = NS_OK;
+
+ if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
+
+ if (!(m_initedValues & CACHED_VALUES_INITED)) {
+ uint32_t uint32Value;
+ mdbOid outOid;
+ if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid)))
+ m_messageKey = outOid.mOid_Id;
+
+ err = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &m_messageSize);
+
+ err = GetUInt32Column(m_mdb->m_dateColumnToken, &uint32Value);
+ Seconds2PRTime(uint32Value, &m_date);
+
+ err = GetUInt32Column(m_mdb->m_messageThreadIdColumnToken, &m_threadId);
+
+ if (NS_SUCCEEDED(err)) m_initedValues |= CACHED_VALUES_INITED;
+ }
+ return err;
+}
+
+nsresult nsMsgHdr::InitFlags() {
+ nsresult err = NS_OK;
+
+ if (!m_mdb) return NS_ERROR_NULL_POINTER;
+
+ if (!(m_initedValues & FLAGS_INITED)) {
+ err = GetUInt32Column(m_mdb->m_flagsColumnToken, &m_flags);
+ m_flags &= ~nsMsgMessageFlags::New; // don't get new flag from MDB
+
+ if (NS_SUCCEEDED(err)) m_initedValues |= FLAGS_INITED;
+ }
+
+ return err;
+}
+
+nsMsgHdr::~nsMsgHdr() {
+ if (m_mdbRow) {
+ if (m_mdb) {
+ NS_RELEASE(m_mdbRow);
+ m_mdb->RemoveHdrFromUseCache((nsIMsgDBHdr*)this, m_messageKey);
+ }
+ }
+ NS_IF_RELEASE(m_mdb);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMessageKey(nsMsgKey* result) {
+ if (m_messageKey == nsMsgKey_None && m_mdbRow != NULL) {
+ mdbOid outOid;
+ if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid)))
+ m_messageKey = outOid.mOid_Id;
+ }
+ *result = m_messageKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetThreadId(nsMsgKey* result) {
+ if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues();
+
+ if (result) {
+ *result = m_threadId;
+ return NS_OK;
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetThreadId(nsMsgKey inKey) {
+ m_threadId = inKey;
+ return SetUInt32Column(m_threadId, m_mdb->m_messageThreadIdColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetMessageKey(nsMsgKey value) {
+ m_messageKey = value;
+ return NS_OK;
+}
+
+nsresult nsMsgHdr::GetRawFlags(uint32_t* result) {
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ *result = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetFlags(uint32_t* result) {
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ if (m_mdb)
+ *result = m_mdb->GetStatusFlags(this, m_flags);
+ else
+ *result = m_flags;
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(!(*result & (nsMsgMessageFlags::Elided)),
+ "shouldn't be set in db");
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetFlags(uint32_t flags) {
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(!(flags & (nsMsgMessageFlags::Elided)),
+ "shouldn't set this flag on db");
+#endif
+ m_initedValues |= FLAGS_INITED;
+ m_flags = flags;
+ // don't write out nsMsgMessageFlags::New to MDB.
+ return SetUInt32Column(m_flags & ~nsMsgMessageFlags::New,
+ m_mdb->m_flagsColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::OrFlags(uint32_t flags, uint32_t* result) {
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ if ((m_flags & flags) != flags) SetFlags(m_flags | flags);
+ *result = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::AndFlags(uint32_t flags, uint32_t* result) {
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ if ((m_flags & flags) != m_flags) SetFlags(m_flags & flags);
+ *result = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::MarkHasAttachments(bool bHasAttachments) {
+ nsresult rv = NS_OK;
+
+ if (m_mdb) {
+ nsMsgKey key;
+ rv = GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv))
+ rv = m_mdb->MarkHasAttachments(key, bHasAttachments, nullptr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgHdr::MarkRead(bool bRead) {
+ nsresult rv = NS_OK;
+
+ if (m_mdb) {
+ nsMsgKey key;
+ rv = GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkRead(key, bRead, nullptr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgHdr::MarkFlagged(bool bFlagged) {
+ nsresult rv = NS_OK;
+
+ if (m_mdb) {
+ nsMsgKey key;
+ rv = GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkMarked(key, bFlagged, nullptr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetStringProperty(const char* propertyName,
+ const nsACString& propertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
+ return m_mdb->SetProperty(m_mdbRow, propertyName,
+ PromiseFlatCString(propertyValue).get());
+}
+
+NS_IMETHODIMP nsMsgHdr::GetStringProperty(const char* propertyName,
+ nsACString& aPropertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
+ return m_mdb->GetProperty(m_mdbRow, propertyName,
+ getter_Copies(aPropertyValue));
+}
+
+NS_IMETHODIMP nsMsgHdr::GetUint32Property(const char* propertyName,
+ uint32_t* pResult) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
+ return m_mdb->GetUint32Property(m_mdbRow, propertyName, pResult);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetUint32Property(const char* propertyName,
+ uint32_t value) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
+ return m_mdb->SetUint32Property(m_mdbRow, propertyName, value);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetNumReferences(uint16_t* result) {
+ if (!(m_initedValues & REFERENCES_INITED)) {
+ const char* references;
+ if (NS_SUCCEEDED(m_mdb->RowCellColumnToConstCharPtr(
+ GetMDBRow(), m_mdb->m_referencesColumnToken, &references)))
+ ParseReferences(references);
+ m_initedValues |= REFERENCES_INITED;
+ }
+
+ if (result) *result = m_references.Length();
+ // there is no real failure here; if there are no references, there are no
+ // references.
+ return NS_OK;
+}
+
+nsresult nsMsgHdr::ParseReferences(const char* references) {
+ const char* startNextRef = references;
+ nsAutoCString resultReference;
+ nsCString messageId;
+ GetMessageId(getter_Copies(messageId));
+
+ while (startNextRef && *startNextRef) {
+ startNextRef = GetNextReference(startNextRef, resultReference,
+ startNextRef == references);
+ // Don't add self-references.
+ if (!resultReference.IsEmpty() && !resultReference.Equals(messageId))
+ m_references.AppendElement(resultReference);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetStringReference(int32_t refNum,
+ nsACString& resultReference) {
+ nsresult err = NS_OK;
+
+ if (!(m_initedValues & REFERENCES_INITED))
+ GetNumReferences(nullptr); // it can handle the null
+
+ if ((uint32_t)refNum < m_references.Length())
+ resultReference = m_references.ElementAt(refNum);
+ else
+ err = NS_ERROR_ILLEGAL_VALUE;
+ return err;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetDate(PRTime* result) {
+ if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues();
+
+ *result = m_date;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetDateInSeconds(uint32_t* aResult) {
+ return GetUInt32Column(m_mdb->m_dateColumnToken, aResult);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetMessageId(const char* messageId) {
+ if (messageId && *messageId == '<') {
+ nsAutoCString tempMessageID(messageId + 1);
+ if (tempMessageID.CharAt(tempMessageID.Length() - 1) == '>')
+ tempMessageID.SetLength(tempMessageID.Length() - 1);
+ return SetStringColumn(tempMessageID.get(), m_mdb->m_messageIdColumnToken);
+ }
+ return SetStringColumn(messageId, m_mdb->m_messageIdColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetSubject(const nsACString& subject) {
+ return SetStringColumn(PromiseFlatCString(subject).get(),
+ m_mdb->m_subjectColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetAuthor(const char* author) {
+ return SetStringColumn(author, m_mdb->m_senderColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetReferences(const nsACString& references) {
+ m_references.Clear();
+ ParseReferences(PromiseFlatCString(references).get());
+
+ m_initedValues |= REFERENCES_INITED;
+
+ return SetStringColumn(PromiseFlatCString(references).get(),
+ m_mdb->m_referencesColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetRecipients(const char* recipients) {
+ // need to put in rfc822 address parsing code here (or make caller do it...)
+ return SetStringColumn(recipients, m_mdb->m_recipientsColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetCcList(const char* ccList) {
+ return SetStringColumn(ccList, m_mdb->m_ccListColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetBccList(const char* bccList) {
+ return SetStringColumn(bccList, m_mdb->m_bccListColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetMessageSize(uint32_t messageSize) {
+ SetUInt32Column(messageSize, m_mdb->m_messageSizeColumnToken);
+ m_messageSize = messageSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetOfflineMessageSize(uint32_t* result) {
+ uint32_t size;
+ nsresult res = GetUInt32Column(m_mdb->m_offlineMessageSizeColumnToken, &size);
+
+ *result = size;
+ return res;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetOfflineMessageSize(uint32_t messageSize) {
+ return SetUInt32Column(messageSize, m_mdb->m_offlineMessageSizeColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetLineCount(uint32_t lineCount) {
+ SetUInt32Column(lineCount, m_mdb->m_numLinesColumnToken);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetDate(PRTime date) {
+ m_date = date;
+ uint32_t seconds;
+ PRTime2Seconds(date, &seconds);
+ return SetUInt32Column((uint32_t)seconds, m_mdb->m_dateColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetPriority(nsMsgPriorityValue priority) {
+ SetUInt32Column((uint32_t)priority, m_mdb->m_priorityColumnToken);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetPriority(nsMsgPriorityValue* result) {
+ if (!result) return NS_ERROR_NULL_POINTER;
+
+ uint32_t priority = 0;
+ nsresult rv = GetUInt32Column(m_mdb->m_priorityColumnToken, &priority);
+ if (NS_FAILED(rv)) return rv;
+
+ *result = (nsMsgPriorityValue)priority;
+ return NS_OK;
+}
+
+// I'd like to not store the account key, if the msg is in
+// the same account as it was received in, to save disk space and memory.
+// This might be problematic when a message gets moved...
+// And I'm not sure if we should short circuit it here,
+// or at a higher level where it might be more efficient.
+NS_IMETHODIMP nsMsgHdr::SetAccountKey(const char* aAccountKey) {
+ return SetStringProperty("account", nsDependentCString(aAccountKey));
+}
+
+NS_IMETHODIMP nsMsgHdr::GetAccountKey(char** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCString key;
+ nsresult rv = GetStringProperty("account", key);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aResult = ToNewCString(key);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMessageOffset(uint64_t* result) {
+ NS_ENSURE_ARG(result);
+
+ (void)GetUInt64Column(m_mdb->m_offlineMsgOffsetColumnToken, result,
+ (unsigned)-1);
+ if (*result == (unsigned)-1) {
+ // It's unset. Unfortunately there's not much we can do here. There's
+ // a lot of code which relies on being able to read .messageOffset,
+ // even if it doesn't require it to return anything sensible.
+ // (For example - in js unit tests - Assert.equals() stringifies the
+ // attributes of it's expected/actual values to produce an error
+ // message even if the assert passes).
+#ifdef DEBUG
+ nsCString tok;
+ GetStringProperty("storeToken", tok);
+ nsPrintfCString err("Missing .messageOffset (key=%u, storeToken='%s')",
+ m_messageKey, tok.get());
+ NS_WARNING(err.get());
+#endif
+ // Return something obviously broken.
+ *result = 12345678;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::SetMessageOffset(uint64_t offset) {
+ SetUInt64Column(offset, m_mdb->m_offlineMsgOffsetColumnToken);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMessageSize(uint32_t* result) {
+ uint32_t size;
+ nsresult res = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &size);
+
+ *result = size;
+ return res;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetLineCount(uint32_t* result) {
+ uint32_t linecount;
+ nsresult res = GetUInt32Column(m_mdb->m_numLinesColumnToken, &linecount);
+ *result = linecount;
+ return res;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetAuthor(char** resultAuthor) {
+ return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_senderColumnToken,
+ resultAuthor);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetSubject(nsACString& resultSubject) {
+ return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_subjectColumnToken,
+ getter_Copies(resultSubject));
+}
+
+NS_IMETHODIMP nsMsgHdr::GetRecipients(char** resultRecipients) {
+ return m_mdb->RowCellColumnToCharPtr(
+ GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetCcList(char** resultCCList) {
+ return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_ccListColumnToken,
+ resultCCList);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetBccList(char** resultBCCList) {
+ return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_bccListColumnToken,
+ resultBCCList);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMessageId(char** resultMessageId) {
+ return m_mdb->RowCellColumnToCharPtr(
+ GetMDBRow(), m_mdb->m_messageIdColumnToken, resultMessageId);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMime2DecodedAuthor(nsAString& resultAuthor) {
+ return m_mdb->RowCellColumnToMime2DecodedString(
+ GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMime2DecodedSubject(nsAString& resultSubject) {
+ return m_mdb->RowCellColumnToMime2DecodedString(
+ GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetMime2DecodedRecipients(nsAString& resultRecipients) {
+ return m_mdb->RowCellColumnToMime2DecodedString(
+ GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetAuthorCollationKey(nsTArray<uint8_t>& resultAuthor) {
+ return m_mdb->RowCellColumnToAddressCollationKey(
+ GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetSubjectCollationKey(
+ nsTArray<uint8_t>& resultSubject) {
+ return m_mdb->RowCellColumnToCollationKey(
+ GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetRecipientsCollationKey(
+ nsTArray<uint8_t>& resultRecipients) {
+ return m_mdb->RowCellColumnToCollationKey(
+ GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetCharset(char** aCharset) {
+ return m_mdb->RowCellColumnToCharPtr(
+ GetMDBRow(), m_mdb->m_messageCharSetColumnToken, aCharset);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetCharset(const char* aCharset) {
+ return SetStringColumn(aCharset, m_mdb->m_messageCharSetColumnToken);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetEffectiveCharset(nsACString& resultCharset) {
+ return m_mdb->GetEffectiveCharset(m_mdbRow, resultCharset);
+}
+
+NS_IMETHODIMP nsMsgHdr::SetThreadParent(nsMsgKey inKey) {
+ m_threadParent = inKey;
+ if (inKey == m_messageKey) NS_ASSERTION(false, "can't be your own parent");
+ SetUInt32Column(m_threadParent, m_mdb->m_threadParentColumnToken);
+ m_initedValues |= THREAD_PARENT_INITED;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetThreadParent(nsMsgKey* result) {
+ nsresult res;
+ if (!(m_initedValues & THREAD_PARENT_INITED)) {
+ res = GetUInt32Column(m_mdb->m_threadParentColumnToken, &m_threadParent,
+ nsMsgKey_None);
+ if (NS_SUCCEEDED(res)) m_initedValues |= THREAD_PARENT_INITED;
+ }
+ *result = m_threadParent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetFolder(nsIMsgFolder** result) {
+ NS_ENSURE_ARG(result);
+
+ if (m_mdb && m_mdb->m_folder) {
+ NS_ADDREF(*result = m_mdb->m_folder);
+ } else
+ *result = nullptr;
+ return NS_OK;
+}
+
+nsresult nsMsgHdr::SetStringColumn(const char* str, mdb_token token) {
+ NS_ENSURE_ARG_POINTER(str);
+ return m_mdb->CharPtrToRowCellColumn(m_mdbRow, token, str);
+}
+
+nsresult nsMsgHdr::SetUInt32Column(uint32_t value, mdb_token token) {
+ return m_mdb->UInt32ToRowCellColumn(m_mdbRow, token, value);
+}
+
+nsresult nsMsgHdr::GetUInt32Column(mdb_token token, uint32_t* pvalue,
+ uint32_t defaultValue) {
+ return m_mdb->RowCellColumnToUInt32(GetMDBRow(), token, pvalue, defaultValue);
+}
+
+nsresult nsMsgHdr::SetUInt64Column(uint64_t value, mdb_token token) {
+ return m_mdb->UInt64ToRowCellColumn(m_mdbRow, token, value);
+}
+
+nsresult nsMsgHdr::GetUInt64Column(mdb_token token, uint64_t* pvalue,
+ uint64_t defaultValue) {
+ return m_mdb->RowCellColumnToUInt64(GetMDBRow(), token, pvalue, defaultValue);
+}
+
+/**
+ * Roughly speaking, get the next message-id (starts with a '<' ends with a
+ * '>'). Except, we also try to handle the case where your reference is of
+ * a prehistoric vintage that just stuck any old random junk in there. Our
+ * old logic would (unintentionally?) just trim the whitespace off the front
+ * and hand you everything after that. We change things at all because that
+ * same behaviour does not make sense if we have already seen a proper message
+ * id. We keep the old behaviour at all because it would seem to have
+ * benefits. (See jwz's non-zero stats: http://www.jwz.org/doc/threading.html)
+ * So, to re-state, if there is a valid message-id in there at all, we only
+ * return valid message-id's (sans bracketing '<' and '>'). If there isn't,
+ * our result (via "references") is a left-trimmed copy of the string. If
+ * there is nothing in there, our result is an empty string.) We do require
+ * that you pass allowNonDelimitedReferences what it demands, though.
+ * For example: "<valid@stuff> this stuff is invalid" would net you
+ * "valid@stuff" and "this stuff is invalid" as results. We now only would
+ * provide "valid-stuff" and an empty string (which you should ignore) as
+ * results. However "this stuff is invalid" would return itself, allowing
+ * anything relying on that behaviour to keep working.
+ *
+ * Note: We accept anything inside the '<' and '>'; technically, we should want
+ * at least a '@' in there (per rfc 2822). But since we're going out of our
+ * way to support weird things...
+ *
+ * @param startNextRef The position to start at; this should either be the start
+ * of your references string or our return value from a previous call.
+ * @param reference You pass a nsCString by reference, we put the reference we
+ * find in it, if we find one. It may be empty! Beware!
+ * @param allowNonDelimitedReferences Should we support the
+ * pre-reasonable-standards form of In-Reply-To where it could be any
+ * arbitrary string and our behaviour was just to take off leading
+ * whitespace. It only makes sense to pass true for your first call to this
+ * function, as if you are around to make a second call, it means we found
+ * a properly formatted message-id and so we should only look for more
+ * properly formatted message-ids.
+ * NOTE: this option will also strip off a single leading '<' if there is
+ * one. Some examples:
+ * " foo" => "foo"
+ * " <bar" => "bar"
+ * "<<<foo" => "<<foo"
+ * "<foo@bar>" => "foo@bar" (completed message-id)
+ * @returns The next starting position of this routine, which may be pointing at
+ * a nul '\0' character to indicate termination.
+ */
+const char* nsMsgHdr::GetNextReference(const char* startNextRef,
+ nsCString& reference,
+ bool acceptNonDelimitedReferences) {
+ const char* ptr = startNextRef;
+ const char* whitespaceEndedAt = nullptr;
+ const char* firstMessageIdChar = nullptr;
+
+ // make the reference result string empty by default; we will set it to
+ // something valid if the time comes.
+ reference.Truncate();
+
+ // walk until we find a '<', but keep track of the first point we found that
+ // was not whitespace (as defined by previous versions of this code.)
+ for (bool foundLessThan = false; !foundLessThan; ptr++) {
+ switch (*ptr) {
+ case '\0':
+ // if we are at the end of the string, we found some non-whitespace, and
+ // the caller requested that we accept non-delimited whitespace,
+ // give them that as their reference. (otherwise, leave it empty)
+ if (acceptNonDelimitedReferences && whitespaceEndedAt)
+ reference = whitespaceEndedAt;
+ return ptr;
+ case ' ':
+ case '\r':
+ case '\n':
+ case '\t':
+ // do nothing, make default case mean you didn't get whitespace
+ break;
+ case '<':
+ firstMessageIdChar = ptr + 1; // skip over the '<'
+ foundLessThan = true; // (flag to stop)
+ // Ensure whitespaceEndedAt skips the leading '<' and is set to
+ // a non-NULL value, just in case the message-id is not valid (no '>')
+ // and the old-school support is desired.
+ if (!whitespaceEndedAt) whitespaceEndedAt = ptr + 1;
+ break;
+ default:
+ if (!whitespaceEndedAt) whitespaceEndedAt = ptr;
+ break;
+ }
+ }
+
+ // keep going until we hit a '>' or hit the end of the string
+ for (; *ptr; ptr++) {
+ if (*ptr == '>') {
+ // it's valid, update reference, making sure to stop before the '>'
+ reference.Assign(firstMessageIdChar, ptr - firstMessageIdChar);
+ // and return a start point just after the '>'
+ return ++ptr;
+ }
+ }
+
+ // we did not have a fully-formed, valid message-id, so consider falling back
+ if (acceptNonDelimitedReferences && whitespaceEndedAt)
+ reference = whitespaceEndedAt;
+ return ptr;
+}
+
+bool nsMsgHdr::IsParentOf(nsIMsgDBHdr* possibleChild) {
+ uint16_t referenceToCheck = 0;
+ possibleChild->GetNumReferences(&referenceToCheck);
+ nsAutoCString reference;
+
+ nsCString messageId;
+ GetMessageId(getter_Copies(messageId));
+
+ while (referenceToCheck > 0) {
+ possibleChild->GetStringReference(referenceToCheck - 1, reference);
+
+ if (reference.Equals(messageId)) return true;
+ // if reference didn't match, check if this ref is for a non-existent
+ // header. If it is, continue looking at ancestors.
+ nsCOMPtr<nsIMsgDBHdr> refHdr;
+ if (!m_mdb) break;
+ (void)m_mdb->GetMsgHdrForMessageID(reference.get(), getter_AddRefs(refHdr));
+ if (refHdr) break;
+ referenceToCheck--;
+ }
+ return false;
+}
+
+bool nsMsgHdr::IsAncestorOf(nsIMsgDBHdr* possibleChild) {
+ const char* references;
+ nsMsgHdr* curHdr =
+ static_cast<nsMsgHdr*>(possibleChild); // closed system, cast ok
+ m_mdb->RowCellColumnToConstCharPtr(
+ curHdr->GetMDBRow(), m_mdb->m_referencesColumnToken, &references);
+ if (!references) return false;
+
+ nsCString messageId;
+ // should put < > around message id to make strstr strictly match
+ GetMessageId(getter_Copies(messageId));
+ return (strstr(references, messageId.get()) != nullptr);
+}
+
+NS_IMETHODIMP nsMsgHdr::GetIsRead(bool* isRead) {
+ NS_ENSURE_ARG_POINTER(isRead);
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ *isRead = !!(m_flags & nsMsgMessageFlags::Read);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetIsFlagged(bool* isFlagged) {
+ NS_ENSURE_ARG_POINTER(isFlagged);
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ *isFlagged = !!(m_flags & nsMsgMessageFlags::Marked);
+ return NS_OK;
+}
+
+void nsMsgHdr::ReparentInThread(nsIMsgThread* thread) {
+ NS_WARNING("Borked message header, attempting to fix!");
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ // bail out early for the singleton thread case.
+ if (numChildren == 1) {
+ SetThreadParent(nsMsgKey_None);
+ return;
+ } else {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ // loop through thread, looking for our proper parent.
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ thread->GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ // closed system, cast ok
+ nsMsgHdr* curMsgHdr = static_cast<nsMsgHdr*>(curHdr.get());
+ if (curHdr && curMsgHdr->IsParentOf(this)) {
+ nsMsgKey curHdrKey;
+ curHdr->GetMessageKey(&curHdrKey);
+ SetThreadParent(curHdrKey);
+ return;
+ }
+ }
+ // we didn't find it. So either the root header is our parent,
+ // or we're the root.
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ thread->GetRootHdr(getter_AddRefs(rootHdr));
+ NS_ASSERTION(rootHdr, "thread has no root hdr - shouldn't happen");
+ if (rootHdr) {
+ nsMsgKey rootKey;
+ rootHdr->GetMessageKey(&rootKey);
+ // if we're the root, our thread parent is -1.
+ SetThreadParent(rootKey == m_messageKey ? nsMsgKey_None : rootKey);
+ }
+ }
+}
+
+bool nsMsgHdr::IsAncestorKilled(uint32_t ancestorsToCheck) {
+ if (!(m_initedValues & FLAGS_INITED)) InitFlags();
+ bool isKilled = m_flags & nsMsgMessageFlags::Ignored;
+
+ if (!isKilled) {
+ nsMsgKey threadParent;
+ GetThreadParent(&threadParent);
+
+ if (threadParent == m_messageKey) {
+ // isKilled is false by virtue of the enclosing if statement
+ NS_ERROR("Thread is parent of itself, please fix!");
+ nsCOMPtr<nsIMsgThread> thread;
+ (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
+ if (!thread) return false;
+ ReparentInThread(thread);
+ // Something's wrong, but the problem happened some time ago, so erroring
+ // out now is probably not a good idea. Ergo, we'll pretend to be OK, show
+ // the user the thread (err on the side of caution), and let the assertion
+ // alert debuggers to a problem.
+ return false;
+ }
+ if (threadParent != nsMsgKey_None) {
+ nsCOMPtr<nsIMsgDBHdr> parentHdr;
+ (void)m_mdb->GetMsgHdrForKey(threadParent, getter_AddRefs(parentHdr));
+
+ if (parentHdr) {
+ // More proofing against crashers. This crasher was derived from the
+ // fact that something got borked, leaving is in hand with a circular
+ // reference to borked headers inducing these loops. The defining
+ // characteristic of these headers is that they don't actually seat
+ // themselves in the thread properly.
+ nsCOMPtr<nsIMsgThread> thread;
+ (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
+ if (thread) {
+ nsCOMPtr<nsIMsgDBHdr> claimant;
+ (void)thread->GetChild(threadParent, getter_AddRefs(claimant));
+ if (!claimant) {
+ // attempt to reparent, and say the thread isn't killed,
+ // erring on the side of safety.
+ ReparentInThread(thread);
+ return false;
+ }
+ }
+
+ if (!ancestorsToCheck) {
+ // We think we have a parent, but we have no more ancestors to check
+ NS_ASSERTION(false, "cycle in parent relationship, please fix!");
+ return false;
+ }
+ // closed system, cast ok
+ nsMsgHdr* parent = static_cast<nsMsgHdr*>(parentHdr.get());
+ return parent->IsAncestorKilled(ancestorsToCheck - 1);
+ }
+ }
+ }
+ return isKilled;
+}
+
+NS_IMETHODIMP nsMsgHdr::GetIsKilled(bool* isKilled) {
+ NS_ENSURE_ARG_POINTER(isKilled);
+ *isKilled = false;
+ nsCOMPtr<nsIMsgThread> thread;
+ (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
+ // if we can't find the thread, let's at least check one level; maybe
+ // the header hasn't been added to a thread yet.
+ uint32_t numChildren = 1;
+ if (thread) thread->GetNumChildren(&numChildren);
+ if (!numChildren) return NS_ERROR_FAILURE;
+ // We can't have as many ancestors as there are messages in the thread,
+ // so tell IsAncestorKilled to only check numChildren - 1 ancestors.
+ *isKilled = IsAncestorKilled(numChildren - 1);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#include "nsIStringEnumerator.h"
+#define NULL_MORK_COLUMN 0
+class nsMsgPropertyEnumerator : public nsStringEnumeratorBase {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUTF8STRINGENUMERATOR
+
+ using nsStringEnumeratorBase::GetNext;
+
+ explicit nsMsgPropertyEnumerator(nsMsgHdr* aHdr);
+ void PrefetchNext();
+
+ protected:
+ virtual ~nsMsgPropertyEnumerator();
+ nsCOMPtr<nsIMdbRowCellCursor> mRowCellCursor;
+ nsCOMPtr<nsIMdbEnv> m_mdbEnv;
+ nsCOMPtr<nsIMdbStore> m_mdbStore;
+ // Hold a reference to the hdr so it will hold an xpcom reference to the
+ // underlying mdb row. The row cell cursor will crash if the underlying
+ // row goes away.
+ RefPtr<nsMsgHdr> m_hdr;
+ bool mNextPrefetched;
+ mdb_column mNextColumn;
+};
+
+nsMsgPropertyEnumerator::nsMsgPropertyEnumerator(nsMsgHdr* aHdr)
+ : mNextPrefetched(false), mNextColumn(NULL_MORK_COLUMN) {
+ RefPtr<nsMsgDatabase> mdb;
+ nsCOMPtr<nsIMdbRow> mdbRow;
+
+ if (aHdr && (mdbRow = aHdr->GetMDBRow()) && (m_hdr = aHdr) &&
+ (mdb = aHdr->GetMdb()) && (m_mdbEnv = mdb->m_mdbEnv) &&
+ (m_mdbStore = mdb->m_mdbStore)) {
+ mdbRow->GetRowCellCursor(m_mdbEnv, -1, getter_AddRefs(mRowCellCursor));
+ }
+}
+
+nsMsgPropertyEnumerator::~nsMsgPropertyEnumerator() {
+ // Need to clear this before the nsMsgHdr and its corresponding
+ // nsIMdbRow potentially go away.
+ mRowCellCursor = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgPropertyEnumerator, nsIUTF8StringEnumerator,
+ nsIStringEnumerator)
+
+NS_IMETHODIMP nsMsgPropertyEnumerator::GetNext(nsACString& aItem) {
+ PrefetchNext();
+ if (mNextColumn == NULL_MORK_COLUMN)
+ return NS_ERROR_FAILURE; // call HasMore first
+ if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NOT_INITIALIZED;
+ mNextPrefetched = false;
+ char columnName[100];
+ struct mdbYarn colYarn = {columnName, 0, sizeof(columnName), 0, 0, nullptr};
+ // Get the column of the cell
+ nsresult rv = m_mdbStore->TokenToString(m_mdbEnv, mNextColumn, &colYarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aItem.Assign(static_cast<char*>(colYarn.mYarn_Buf), colYarn.mYarn_Fill);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPropertyEnumerator::HasMore(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ PrefetchNext();
+ *aResult = (mNextColumn != NULL_MORK_COLUMN);
+ return NS_OK;
+}
+
+void nsMsgPropertyEnumerator::PrefetchNext(void) {
+ if (!mNextPrefetched && m_mdbEnv && mRowCellCursor) {
+ mNextPrefetched = true;
+ nsCOMPtr<nsIMdbCell> cell;
+ mRowCellCursor->NextCell(m_mdbEnv, getter_AddRefs(cell), &mNextColumn,
+ nullptr);
+ if (mNextColumn == NULL_MORK_COLUMN) {
+ // free up references
+ m_mdbStore = nullptr;
+ m_mdbEnv = nullptr;
+ mRowCellCursor = nullptr;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgHdr::GetProperties(nsTArray<nsCString>& headers) {
+ nsCOMPtr<nsIUTF8StringEnumerator> propertyEnumerator =
+ new nsMsgPropertyEnumerator(this);
+ bool hasMore;
+ while (NS_SUCCEEDED(propertyEnumerator->HasMore(&hasMore)) && hasMore) {
+ nsAutoCString property;
+ propertyEnumerator->GetNext(property);
+ headers.AppendElement(property);
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp
new file mode 100644
index 0000000000..96410654d8
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp
@@ -0,0 +1,385 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgOfflineImapOperation.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+
+LazyLogModule IMAPOffline("IMAPOffline");
+
+/* Implementation file */
+NS_IMPL_ISUPPORTS(nsMsgOfflineImapOperation, nsIMsgOfflineImapOperation)
+
+// property names for offine imap operation fields.
+#define PROP_OPERATION "op"
+#define PROP_OPERATION_FLAGS "opFlags"
+#define PROP_NEW_FLAGS "newFlags"
+#define PROP_MESSAGE_KEY "msgKey"
+#define PROP_SRC_MESSAGE_KEY "srcMsgKey"
+#define PROP_SRC_FOLDER_URI "srcFolderURI"
+#define PROP_MOVE_DEST_FOLDER_URI "moveDest"
+#define PROP_NUM_COPY_DESTS "numCopyDests"
+#define PROP_COPY_DESTS \
+ "copyDests" // how to delimit these? Or should we do the "dest1","dest2" etc
+ // trick? But then we'd need to shuffle them around since we
+ // delete off the front first.
+#define PROP_KEYWORD_ADD "addedKeywords"
+#define PROP_KEYWORD_REMOVE "removedKeywords"
+#define PROP_MSG_SIZE "msgSize"
+#define PROP_PLAYINGBACK "inPlayback"
+
+nsMsgOfflineImapOperation::nsMsgOfflineImapOperation(nsMsgDatabase* db,
+ nsIMdbRow* row) {
+ NS_ASSERTION(db, "can't have null db");
+ NS_ASSERTION(row, "can't have null row");
+ m_operation = 0;
+ m_operationFlags = 0;
+ m_messageKey = nsMsgKey_None;
+ m_sourceMessageKey = nsMsgKey_None;
+ m_mdb = db;
+ NS_ADDREF(m_mdb);
+ m_mdbRow = row;
+ m_newFlags = 0;
+ m_mdb->GetUint32Property(m_mdbRow, PROP_OPERATION, (uint32_t*)&m_operation,
+ 0);
+ m_mdb->GetUint32Property(m_mdbRow, PROP_MESSAGE_KEY, &m_messageKey, 0);
+ m_mdb->GetUint32Property(m_mdbRow, PROP_OPERATION_FLAGS, &m_operationFlags,
+ 0);
+ m_mdb->GetUint32Property(m_mdbRow, PROP_NEW_FLAGS, (uint32_t*)&m_newFlags, 0);
+}
+
+nsMsgOfflineImapOperation::~nsMsgOfflineImapOperation() {
+ // clear the row first, in case we're holding the last reference
+ // to the db.
+ m_mdbRow = nullptr;
+ NS_IF_RELEASE(m_mdb);
+}
+
+/* attribute nsOfflineImapOperationType operation; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetOperation(
+ nsOfflineImapOperationType* aOperation) {
+ NS_ENSURE_ARG(aOperation);
+ *aOperation = m_operation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetOperation(
+ nsOfflineImapOperationType aOperation) {
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info))
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x setOperation was %x add %x", m_messageKey, m_operation,
+ aOperation));
+
+ m_operation |= aOperation;
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION, m_operation);
+}
+
+/* void clearOperation (in nsOfflineImapOperationType operation); */
+NS_IMETHODIMP nsMsgOfflineImapOperation::ClearOperation(
+ nsOfflineImapOperationType aOperation) {
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info))
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x clearOperation was %x clear %x", m_messageKey,
+ m_operation, aOperation));
+ m_operation &= ~aOperation;
+ switch (aOperation) {
+ case kMsgMoved:
+ case kAppendTemplate:
+ case kAppendDraft:
+ m_moveDestination.Truncate();
+ break;
+ case kMsgCopy:
+ m_copyDestinations.RemoveElementAt(0);
+ break;
+ }
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION, m_operation);
+}
+
+/* attribute nsMsgKey messageKey; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetMessageKey(nsMsgKey* aMessageKey) {
+ NS_ENSURE_ARG(aMessageKey);
+ *aMessageKey = m_messageKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetMessageKey(nsMsgKey aMessageKey) {
+ m_messageKey = aMessageKey;
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_MESSAGE_KEY, m_messageKey);
+}
+
+/* attribute nsMsgKey srcMessageKey; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetSrcMessageKey(
+ nsMsgKey* aMessageKey) {
+ NS_ENSURE_ARG(aMessageKey);
+ return m_mdb->GetUint32Property(m_mdbRow, PROP_SRC_MESSAGE_KEY, aMessageKey,
+ nsMsgKey_None);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetSrcMessageKey(
+ nsMsgKey aMessageKey) {
+ m_messageKey = aMessageKey;
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_SRC_MESSAGE_KEY, m_messageKey);
+}
+
+/* attribute imapMessageFlagsType flagOperation; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetFlagOperation(
+ imapMessageFlagsType* aFlagOperation) {
+ NS_ENSURE_ARG(aFlagOperation);
+ *aFlagOperation = m_operationFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetFlagOperation(
+ imapMessageFlagsType aFlagOperation) {
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info))
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x setFlagOperation was %x add %x", m_messageKey,
+ m_operationFlags, aFlagOperation));
+ SetOperation(kFlagsChanged);
+ nsresult rv = SetNewFlags(aFlagOperation);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_operationFlags |= aFlagOperation;
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION_FLAGS,
+ m_operationFlags);
+}
+
+/* attribute imapMessageFlagsType flagOperation; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetNewFlags(
+ imapMessageFlagsType* aNewFlags) {
+ NS_ENSURE_ARG(aNewFlags);
+ uint32_t flags;
+ nsresult rv = m_mdb->GetUint32Property(m_mdbRow, PROP_NEW_FLAGS, &flags, 0);
+ *aNewFlags = m_newFlags = (imapMessageFlagsType)flags;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetNewFlags(
+ imapMessageFlagsType aNewFlags) {
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info) && m_newFlags != aNewFlags)
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x SetNewFlags was %x to %x", m_messageKey, m_newFlags,
+ aNewFlags));
+ m_newFlags = aNewFlags;
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_NEW_FLAGS, m_newFlags);
+}
+
+/* attribute string destinationFolderURI; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetDestinationFolderURI(
+ nsACString& aDestinationFolderURI) {
+ (void)m_mdb->GetProperty(m_mdbRow, PROP_MOVE_DEST_FOLDER_URI,
+ getter_Copies(m_moveDestination));
+ aDestinationFolderURI = m_moveDestination;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetDestinationFolderURI(
+ const nsACString& aDestinationFolderURI) {
+ if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info))
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x SetDestinationFolderURI to %s", m_messageKey,
+ PromiseFlatCString(aDestinationFolderURI).get()));
+ m_moveDestination = aDestinationFolderURI;
+ return m_mdb->SetProperty(m_mdbRow, PROP_MOVE_DEST_FOLDER_URI,
+ PromiseFlatCString(aDestinationFolderURI).get());
+}
+
+/* attribute string sourceFolderURI; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetSourceFolderURI(
+ nsACString& aSourceFolderURI) {
+ nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_SRC_FOLDER_URI,
+ getter_Copies(m_sourceFolder));
+ aSourceFolderURI = m_sourceFolder;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetSourceFolderURI(
+ const nsACString& aSourceFolderURI) {
+ m_sourceFolder = aSourceFolderURI;
+ SetOperation(kMoveResult);
+
+ return m_mdb->SetProperty(m_mdbRow, PROP_SRC_FOLDER_URI,
+ PromiseFlatCString(aSourceFolderURI).get());
+}
+
+/* attribute string keyword; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetKeywordsToAdd(char** aKeywords) {
+ NS_ENSURE_ARG(aKeywords);
+ nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_KEYWORD_ADD,
+ getter_Copies(m_keywordsToAdd));
+ *aKeywords = ToNewCString(m_keywordsToAdd);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::AddKeywordToAdd(const char* aKeyword) {
+ SetOperation(kAddKeywords);
+ return AddKeyword(aKeyword, m_keywordsToAdd, PROP_KEYWORD_ADD,
+ m_keywordsToRemove, PROP_KEYWORD_REMOVE);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetKeywordsToRemove(char** aKeywords) {
+ NS_ENSURE_ARG(aKeywords);
+ nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_KEYWORD_REMOVE,
+ getter_Copies(m_keywordsToRemove));
+ *aKeywords = ToNewCString(m_keywordsToRemove);
+ return rv;
+}
+
+nsresult nsMsgOfflineImapOperation::AddKeyword(const char* aKeyword,
+ nsCString& addList,
+ const char* addProp,
+ nsCString& removeList,
+ const char* removeProp) {
+ int32_t startOffset, keywordLength;
+ if (!MsgFindKeyword(nsDependentCString(aKeyword), addList, &startOffset,
+ &keywordLength)) {
+ if (!addList.IsEmpty()) addList.Append(' ');
+ addList.Append(aKeyword);
+ }
+ // if the keyword we're removing was in the list of keywords to add,
+ // cut it from that list.
+ if (MsgFindKeyword(nsDependentCString(aKeyword), removeList, &startOffset,
+ &keywordLength)) {
+ removeList.Cut(startOffset, keywordLength);
+ m_mdb->SetProperty(m_mdbRow, removeProp, removeList.get());
+ }
+ return m_mdb->SetProperty(m_mdbRow, addProp, addList.get());
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::AddKeywordToRemove(
+ const char* aKeyword) {
+ SetOperation(kRemoveKeywords);
+ return AddKeyword(aKeyword, m_keywordsToRemove, PROP_KEYWORD_REMOVE,
+ m_keywordsToAdd, PROP_KEYWORD_ADD);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::AddMessageCopyOperation(
+ const nsACString& destinationBox) {
+ SetOperation(kMsgCopy);
+ nsresult rv = GetCopiesFromDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_copyDestinations.AppendElement(destinationBox);
+ return SetCopiesToDB();
+}
+
+// we write out the folders as one string, separated by 0x1.
+#define FOLDER_SEP_CHAR '\001'
+
+nsresult nsMsgOfflineImapOperation::GetCopiesFromDB() {
+ nsCString copyDests;
+ m_copyDestinations.Clear();
+ nsresult rv =
+ m_mdb->GetProperty(m_mdbRow, PROP_COPY_DESTS, getter_Copies(copyDests));
+ // use 0x1 as the delimiter between folder names since it's not a legal
+ // character
+ if (NS_SUCCEEDED(rv) && !copyDests.IsEmpty()) {
+ int32_t curCopyDestStart = 0;
+ int32_t nextCopyDestPos = 0;
+
+ while (nextCopyDestPos != -1) {
+ nsCString curDest;
+ nextCopyDestPos = copyDests.FindChar(FOLDER_SEP_CHAR, curCopyDestStart);
+ if (nextCopyDestPos > 0)
+ curDest = Substring(copyDests, curCopyDestStart,
+ nextCopyDestPos - curCopyDestStart);
+ else
+ curDest = Substring(copyDests, curCopyDestStart,
+ copyDests.Length() - curCopyDestStart);
+ curCopyDestStart = nextCopyDestPos + 1;
+ m_copyDestinations.AppendElement(curDest);
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgOfflineImapOperation::SetCopiesToDB() {
+ nsAutoCString copyDests;
+
+ // use 0x1 as the delimiter between folders
+ for (uint32_t i = 0; i < m_copyDestinations.Length(); i++) {
+ if (i > 0) copyDests.Append(FOLDER_SEP_CHAR);
+ copyDests.Append(m_copyDestinations.ElementAt(i));
+ }
+ return m_mdb->SetProperty(m_mdbRow, PROP_COPY_DESTS, copyDests.get());
+}
+
+/* attribute long numberOfCopies; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetNumberOfCopies(
+ int32_t* aNumberOfCopies) {
+ NS_ENSURE_ARG(aNumberOfCopies);
+ nsresult rv = GetCopiesFromDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aNumberOfCopies = m_copyDestinations.Length();
+ return NS_OK;
+}
+
+/* string getCopyDestination (in long copyIndex); */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetCopyDestination(int32_t copyIndex,
+ char** retval) {
+ NS_ENSURE_ARG(retval);
+ nsresult rv = GetCopiesFromDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (copyIndex >= (int32_t)m_copyDestinations.Length())
+ return NS_ERROR_ILLEGAL_VALUE;
+ *retval = ToNewCString(m_copyDestinations.ElementAt(copyIndex));
+ return (*retval) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+/* attribute unsigned log msgSize; */
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetMsgSize(uint32_t* aMsgSize) {
+ NS_ENSURE_ARG(aMsgSize);
+ return m_mdb->GetUint32Property(m_mdbRow, PROP_MSG_SIZE, aMsgSize, 0);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetMsgSize(uint32_t aMsgSize) {
+ return m_mdb->SetUint32Property(m_mdbRow, PROP_MSG_SIZE, aMsgSize);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::SetPlayingBack(bool aPlayingBack) {
+ return m_mdb->SetBooleanProperty(m_mdbRow, PROP_PLAYINGBACK, aPlayingBack);
+}
+
+NS_IMETHODIMP nsMsgOfflineImapOperation::GetPlayingBack(bool* aPlayingBack) {
+ NS_ENSURE_ARG(aPlayingBack);
+ return m_mdb->GetBooleanProperty(m_mdbRow, PROP_PLAYINGBACK, aPlayingBack);
+}
+
+void nsMsgOfflineImapOperation::Log() {
+ if (!MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) return;
+ // const long kMoveResult = 0x8;
+ // const long kAppendDraft = 0x10;
+ // const long kAddedHeader = 0x20;
+ // const long kDeletedMsg = 0x40;
+ // const long kMsgMarkedDeleted = 0x80;
+ // const long kAppendTemplate = 0x100;
+ // const long kDeleteAllMsgs = 0x200;
+ if (m_operation & nsIMsgOfflineImapOperation::kFlagsChanged)
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x changeFlag:%x", m_messageKey, m_newFlags));
+ if (m_operation & nsIMsgOfflineImapOperation::kMsgMoved) {
+ nsCString moveDestFolder;
+ GetDestinationFolderURI(moveDestFolder);
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x moveTo:%s", m_messageKey, moveDestFolder.get()));
+ }
+ if (m_operation & nsIMsgOfflineImapOperation::kMsgCopy) {
+ nsCString copyDests;
+ m_mdb->GetProperty(m_mdbRow, PROP_COPY_DESTS, getter_Copies(copyDests));
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x moveTo:%s", m_messageKey, copyDests.get()));
+ }
+ if (m_operation & nsIMsgOfflineImapOperation::kAppendDraft)
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x append draft", m_messageKey));
+ if (m_operation & nsIMsgOfflineImapOperation::kAddKeywords)
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x add keyword:%s", m_messageKey, m_keywordsToAdd.get()));
+ if (m_operation & nsIMsgOfflineImapOperation::kRemoveKeywords)
+ MOZ_LOG(IMAPOffline, LogLevel::Info,
+ ("msg id %x remove keyword:%s", m_messageKey,
+ m_keywordsToRemove.get()));
+}
diff --git a/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h
new file mode 100644
index 0000000000..ba6ad3f079
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#ifndef _nsMsgOfflineImapOperation_H_
+
+# include "nsIMsgOfflineImapOperation.h"
+# include "mdb.h"
+# include "nsMsgDatabase.h"
+# include "prlog.h"
+
+class nsMsgOfflineImapOperation : public nsIMsgOfflineImapOperation {
+ public:
+ /** Instance Methods **/
+ nsMsgOfflineImapOperation(nsMsgDatabase* db, nsIMdbRow* row);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGOFFLINEIMAPOPERATION
+
+ nsIMdbRow* GetMDBRow() { return m_mdbRow; }
+ nsresult GetCopiesFromDB();
+ nsresult SetCopiesToDB();
+ void Log();
+
+ protected:
+ virtual ~nsMsgOfflineImapOperation();
+ nsresult AddKeyword(const char* aKeyword, nsCString& addList,
+ const char* addProp, nsCString& removeList,
+ const char* removeProp);
+
+ nsOfflineImapOperationType m_operation;
+ nsMsgKey m_messageKey;
+ nsMsgKey m_sourceMessageKey;
+ uint32_t m_operationFlags; // what to do on sync
+ imapMessageFlagsType m_newFlags; // used for kFlagsChanged
+
+ // these are URI's, and are escaped. Thus, we can use a delimter like ' '
+ // because the real spaces should be escaped.
+ nsCString m_sourceFolder;
+ nsCString m_moveDestination;
+ nsTArray<nsCString> m_copyDestinations;
+
+ nsCString m_keywordsToAdd;
+ nsCString m_keywordsToRemove;
+
+ // nsMsgOfflineImapOperation will have to know what db and row they belong to,
+ // since they are really just a wrapper around the offline operation row in
+ // the mdb. though I hope not.
+ nsMsgDatabase* m_mdb;
+ nsCOMPtr<nsIMdbRow> m_mdbRow;
+};
+
+#endif /* _nsMsgOfflineImapOperation_H_ */
diff --git a/comm/mailnews/db/msgdb/src/nsMsgThread.cpp b/comm/mailnews/db/msgdb/src/nsMsgThread.cpp
new file mode 100644
index 0000000000..15398cf321
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsMsgThread.cpp
@@ -0,0 +1,1050 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgDatabase.h"
+#include "nsCOMPtr.h"
+#include "nsMsgThread.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgEnumerator.h"
+#include "MailNewsTypes2.h"
+#include "mozilla/DebugOnly.h"
+
+NS_IMPL_ISUPPORTS(nsMsgThread, nsIMsgThread)
+
+nsMsgThread::nsMsgThread() { Init(); }
+
+nsMsgThread::nsMsgThread(nsMsgDatabase* db, nsIMdbTable* table) {
+ Init();
+ m_mdbTable = table;
+ m_mdbDB = db;
+ if (db)
+ db->m_threads.AppendElement(this);
+ else
+ NS_ERROR("no db for thread");
+#ifdef DEBUG_David_Bienvenu
+ if (m_mdbDB->m_threads.Length() > 5)
+ printf("more than five outstanding threads\n");
+#endif
+ if (table && db) {
+ table->GetMetaRow(db->GetEnv(), nullptr, nullptr,
+ getter_AddRefs(m_metaRow));
+ InitCachedValues();
+ }
+}
+
+void nsMsgThread::Init() {
+ m_threadKey = nsMsgKey_None;
+ m_threadRootKey = nsMsgKey_None;
+ m_numChildren = 0;
+ m_numUnreadChildren = 0;
+ m_flags = 0;
+ m_newestMsgDate = 0;
+ m_cachedValuesInitialized = false;
+}
+
+nsMsgThread::~nsMsgThread() {
+ if (m_mdbDB) {
+ mozilla::DebugOnly<bool> found = m_mdbDB->m_threads.RemoveElement(this);
+ NS_ASSERTION(found, "removing thread not in threads array");
+ } else // This can happen if db is forced closed
+ NS_WARNING("null db in thread");
+ Clear();
+}
+
+void nsMsgThread::Clear() {
+ m_mdbTable = nullptr;
+ m_metaRow = nullptr;
+ m_mdbDB = nullptr;
+}
+
+nsresult nsMsgThread::InitCachedValues() {
+ nsresult err = NS_OK;
+
+ NS_ENSURE_TRUE(m_mdbDB && m_metaRow, NS_ERROR_INVALID_POINTER);
+
+ if (!m_cachedValuesInitialized) {
+ err = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadFlagsColumnToken, &m_flags);
+ err = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadChildrenColumnToken, &m_numChildren);
+ err = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadIdColumnToken, &m_threadKey, nsMsgKey_None);
+ err = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken,
+ &m_numUnreadChildren);
+ err = m_mdbDB->RowCellColumnToUInt32(m_metaRow,
+ m_mdbDB->m_threadRootKeyColumnToken,
+ &m_threadRootKey, nsMsgKey_None);
+ err = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadNewestMsgDateColumnToken, &m_newestMsgDate,
+ 0);
+ // fix num children if it's wrong. this doesn't work - some DB's have a
+ // bogus thread table that is full of bogus headers - don't know why.
+ uint32_t rowCount = 0;
+ m_mdbTable->GetCount(m_mdbDB->GetEnv(), &rowCount);
+ // NS_ASSERTION(m_numChildren <= rowCount, "num children wrong -
+ // fixing");
+ if (m_numChildren > rowCount)
+ ChangeChildCount((int32_t)rowCount - (int32_t)m_numChildren);
+ if ((int32_t)m_numUnreadChildren < 0)
+ ChangeUnreadChildCount(-(int32_t)m_numUnreadChildren);
+ if (NS_SUCCEEDED(err)) m_cachedValuesInitialized = true;
+ }
+ return err;
+}
+
+NS_IMETHODIMP nsMsgThread::SetThreadKey(nsMsgKey threadKey) {
+ NS_ASSERTION(m_threadKey == nsMsgKey_None || m_threadKey == threadKey,
+ "shouldn't be changing thread key");
+ m_threadKey = threadKey;
+ // by definition, the initial thread key is also the thread root key.
+ SetThreadRootKey(threadKey);
+ // gotta set column in meta row here.
+ return m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadIdColumnToken, threadKey);
+}
+
+NS_IMETHODIMP nsMsgThread::GetThreadKey(nsMsgKey* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ nsresult res = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadIdColumnToken, &m_threadKey);
+ *result = m_threadKey;
+ return res;
+}
+
+NS_IMETHODIMP nsMsgThread::GetFlags(uint32_t* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ nsresult res = m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadFlagsColumnToken, &m_flags);
+ *result = m_flags;
+ return res;
+}
+
+NS_IMETHODIMP nsMsgThread::SetFlags(uint32_t flags) {
+ m_flags = flags;
+ return m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadFlagsColumnToken, m_flags);
+}
+
+NS_IMETHODIMP nsMsgThread::SetSubject(const nsACString& aSubject) {
+ return m_mdbDB->CharPtrToRowCellColumn(m_metaRow,
+ m_mdbDB->m_threadSubjectColumnToken,
+ PromiseFlatCString(aSubject).get());
+}
+
+NS_IMETHODIMP nsMsgThread::GetSubject(nsACString& aSubject) {
+ nsCString subjectStr;
+ nsresult rv = m_mdbDB->RowCellColumnToCharPtr(
+ m_metaRow, m_mdbDB->m_threadSubjectColumnToken,
+ getter_Copies(subjectStr));
+
+ aSubject.Assign(subjectStr);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::GetNumChildren(uint32_t* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_numChildren;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThread::GetNumUnreadChildren(uint32_t* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_numUnreadChildren;
+ return NS_OK;
+}
+
+nsresult nsMsgThread::RerootThread(nsIMsgDBHdr* newParentOfOldRoot,
+ nsIMsgDBHdr* oldRoot,
+ nsIDBChangeAnnouncer* announcer) {
+ nsresult rv = NS_OK;
+ mdb_pos outPos;
+ nsMsgKey newHdrAncestor;
+ nsCOMPtr<nsIMsgDBHdr> ancestorHdr = newParentOfOldRoot;
+ nsMsgKey newRoot;
+
+ ancestorHdr->GetMessageKey(&newRoot);
+ // loop trying to find the oldest ancestor of this msg
+ // that is a parent of the root. The oldest ancestor will
+ // become the root of the thread.
+ do {
+ ancestorHdr->GetThreadParent(&newHdrAncestor);
+ if (newHdrAncestor != nsMsgKey_None && newHdrAncestor != m_threadRootKey &&
+ newHdrAncestor != newRoot) {
+ newRoot = newHdrAncestor;
+ rv = m_mdbDB->GetMsgHdrForKey(newRoot, getter_AddRefs(ancestorHdr));
+ }
+ } while (NS_SUCCEEDED(rv) && ancestorHdr && newHdrAncestor != nsMsgKey_None &&
+ newHdrAncestor != m_threadRootKey && newHdrAncestor != newRoot);
+ SetThreadRootKey(newRoot);
+ ReparentNonReferenceChildrenOf(oldRoot, newRoot, announcer);
+ if (ancestorHdr) {
+ nsIMsgDBHdr* msgHdr = ancestorHdr;
+ nsMsgHdr* rootMsgHdr =
+ static_cast<nsMsgHdr*>(msgHdr); // closed system, cast ok
+ nsIMdbRow* newRootHdrRow = rootMsgHdr->GetMDBRow();
+ // move the root hdr to pos 0.
+ m_mdbTable->MoveRow(m_mdbDB->GetEnv(), newRootHdrRow, -1, 0, &outPos);
+ ancestorHdr->SetThreadParent(nsMsgKey_None);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::AddChild(nsIMsgDBHdr* child, nsIMsgDBHdr* inReplyTo,
+ bool threadInThread,
+ nsIDBChangeAnnouncer* announcer) {
+ nsresult rv = NS_OK;
+ nsMsgHdr* hdr = static_cast<nsMsgHdr*>(child); // closed system, cast ok
+ uint32_t newHdrFlags = 0;
+ uint32_t msgDate;
+ nsMsgKey newHdrKey = 0;
+ bool parentKeyNeedsSetting = true;
+
+ nsIMdbRow* hdrRow = hdr->GetMDBRow();
+ NS_ENSURE_STATE(hdrRow);
+ hdr->GetRawFlags(&newHdrFlags);
+ hdr->GetMessageKey(&newHdrKey);
+ hdr->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate);
+
+ if (newHdrFlags & nsMsgMessageFlags::Watched)
+ SetFlags(m_flags | nsMsgMessageFlags::Watched);
+
+ child->AndFlags(~(nsMsgMessageFlags::Watched), &newHdrFlags);
+
+ // These are threading flags that the child may have set before being added
+ // to the database.
+ uint32_t protoThreadFlags;
+ child->GetUint32Property("ProtoThreadFlags", &protoThreadFlags);
+ SetFlags(m_flags | protoThreadFlags);
+ // Clear the flag so that it doesn't fudge anywhere else
+ child->SetUint32Property("ProtoThreadFlags", 0);
+
+ uint32_t numChildren = 0;
+ // get the num children before we add the new header.
+ GetNumChildren(&numChildren);
+
+ // if this is an empty thread, set the root key to this header's key
+ if (numChildren == 0) SetThreadRootKey(newHdrKey);
+
+ if (m_mdbTable) {
+ m_mdbTable->AddRow(m_mdbDB->GetEnv(), hdrRow);
+ ChangeChildCount(1);
+ if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1);
+ }
+ if (inReplyTo) {
+ nsMsgKey parentKey;
+ inReplyTo->GetMessageKey(&parentKey);
+ child->SetThreadParent(parentKey);
+ parentKeyNeedsSetting = false;
+ }
+
+ // check if this header is a parent of one of the messages in this thread
+ bool hdrMoved = false;
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ uint32_t moveIndex = 0;
+
+ PRTime newHdrDate;
+ child->GetDate(&newHdrDate);
+
+ // This is an ugly but simple fix for a difficult problem. Basically, when we
+ // add a message to a thread, we have to run through the thread to see if the
+ // new message is a parent of an existing message in the thread, and adjust
+ // things accordingly. If you thread by subject, and you have a large folder
+ // with messages w/ all the same subject, this code can take a really long
+ // time. So the pragmatic thing is to say that for threads with more than 1000
+ // messages, it's simply not worth dealing with the case where the parent
+ // comes in after the child. Threads with more than 1000 messages are pretty
+ // unwieldy anyway. See Bug 90452
+
+ if (numChildren < 1000) {
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsMsgKey msgKey = nsMsgKey_None;
+
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ if (hdr->IsParentOf(curHdr)) {
+ nsMsgKey oldThreadParent;
+ mdb_pos outPos;
+ // move this hdr before the current header.
+ if (!hdrMoved) {
+ m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, childIndex,
+ &outPos);
+ hdrMoved = true;
+ curHdr->GetThreadParent(&oldThreadParent);
+ curHdr->GetMessageKey(&msgKey);
+ nsCOMPtr<nsIMsgDBHdr> curParent;
+ m_mdbDB->GetMsgHdrForKey(oldThreadParent,
+ getter_AddRefs(curParent));
+ if (curParent && hdr->IsAncestorOf(curParent)) {
+ nsMsgKey curParentKey;
+ curParent->GetMessageKey(&curParentKey);
+ if (curParentKey == m_threadRootKey) {
+ m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, 0, &outPos);
+ RerootThread(child, curParent, announcer);
+ parentKeyNeedsSetting = false;
+ }
+ } else if (msgKey == m_threadRootKey) {
+ RerootThread(child, curHdr, announcer);
+ parentKeyNeedsSetting = false;
+ }
+ }
+ curHdr->SetThreadParent(newHdrKey);
+ // TODO: what should be msgKey if hdrMoved was true above?
+ if (msgKey == newHdrKey) parentKeyNeedsSetting = false;
+
+ // OK, this is a reparenting - need to send notification
+ if (announcer)
+ announcer->NotifyParentChangedAll(msgKey, oldThreadParent,
+ newHdrKey, nullptr);
+#ifdef DEBUG_bienvenu1
+ if (newHdrKey != m_threadKey) printf("adding second level child\n");
+#endif
+ }
+ // Calculate a position for this child in date order
+ else if (!hdrMoved && childIndex > 0 && moveIndex == 0) {
+ PRTime curHdrDate;
+
+ curHdr->GetDate(&curHdrDate);
+ if (newHdrDate < curHdrDate) moveIndex = childIndex;
+ }
+ }
+ }
+ }
+ // If this header is not a reply to a header in the thread, and isn't a parent
+ // check to see if it starts with Re: - if not, and the first header does
+ // start with re, should we make this header the top level header? If it's
+ // date is less (or it's ID?), then yes.
+ if (numChildren > 0 && !(newHdrFlags & nsMsgMessageFlags::HasRe) &&
+ !inReplyTo) {
+ PRTime topLevelHdrDate;
+
+ nsCOMPtr<nsIMsgDBHdr> topLevelHdr;
+ rv = GetRootHdr(getter_AddRefs(topLevelHdr));
+ if (NS_SUCCEEDED(rv) && topLevelHdr) {
+ topLevelHdr->GetDate(&topLevelHdrDate);
+ if (newHdrDate < topLevelHdrDate) {
+ RerootThread(child, topLevelHdr, announcer);
+ mdb_pos outPos;
+ m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, 0, &outPos);
+ hdrMoved = true;
+ topLevelHdr->SetThreadParent(newHdrKey);
+ parentKeyNeedsSetting = false;
+ // ### need to get ancestor of new hdr here too.
+ SetThreadRootKey(newHdrKey);
+ child->SetThreadParent(nsMsgKey_None);
+ // argh, here we'd need to adjust all the headers that listed
+ // the demoted header as their thread parent, but only because
+ // of subject threading. Adjust them to point to the new parent,
+ // that is.
+ ReparentNonReferenceChildrenOf(topLevelHdr, newHdrKey, announcer);
+ }
+ }
+ }
+ // OK, check to see if we added this header, and didn't parent it.
+
+ if (numChildren > 0 && parentKeyNeedsSetting)
+ child->SetThreadParent(m_threadRootKey);
+
+ // Move child to keep thread sorted in ascending date order
+ if (!hdrMoved && moveIndex > 0) {
+ mdb_pos outPos;
+ m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, moveIndex, &outPos);
+ }
+
+ // do this after we've put the new hdr in the thread
+ bool isKilled;
+ child->GetIsKilled(&isKilled);
+ if ((m_flags & nsMsgMessageFlags::Ignored || isKilled) && m_mdbDB)
+ m_mdbDB->MarkHdrRead(child, true, nullptr);
+#ifdef DEBUG_David_Bienvenu
+ nsMsgKey msgHdrThreadKey;
+ child->GetThreadId(&msgHdrThreadKey);
+ NS_ASSERTION(msgHdrThreadKey == m_threadKey,
+ "adding msg to thread it doesn't belong to");
+#endif
+ return rv;
+}
+
+nsresult nsMsgThread::ReparentNonReferenceChildrenOf(
+ nsIMsgDBHdr* oldTopLevelHdr, nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ uint32_t numChildren = 0;
+
+ GetNumChildren(&numChildren);
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsMsgKey oldTopLevelHdrKey;
+
+ oldTopLevelHdr->GetMessageKey(&oldTopLevelHdrKey);
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey oldThreadParent, curHdrKey;
+ nsMsgHdr* oldTopLevelMsgHdr =
+ static_cast<nsMsgHdr*>(oldTopLevelHdr); // closed system, cast ok
+ curHdr->GetThreadParent(&oldThreadParent);
+ curHdr->GetMessageKey(&curHdrKey);
+ if (oldThreadParent == oldTopLevelHdrKey && curHdrKey != newParentKey &&
+ !oldTopLevelMsgHdr->IsParentOf(curHdr)) {
+ curHdr->GetThreadParent(&oldThreadParent);
+ curHdr->SetThreadParent(newParentKey);
+ // OK, this is a reparenting - need to send notification
+ if (announcer)
+ announcer->NotifyParentChangedAll(curHdrKey, oldThreadParent,
+ newParentKey, nullptr);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThread::GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsresult rv;
+
+ if (aIndex >= m_numChildren) {
+ *aResult = nsMsgKey_None;
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ mdbOid oid;
+ rv = m_mdbTable->PosToOid(m_mdbDB->GetEnv(), aIndex, &oid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = oid.mOid_Id;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThread::GetChildHdrAt(uint32_t aIndex,
+ nsIMsgDBHdr** result) {
+ // mork doesn't seem to handle this correctly, so deal with going off
+ // the end here.
+ if (aIndex >= m_numChildren) return NS_MSG_MESSAGE_NOT_FOUND;
+ mdbOid oid;
+ nsresult rv = m_mdbTable->PosToOid(m_mdbDB->GetEnv(), aIndex, &oid);
+ NS_ENSURE_SUCCESS(rv, NS_MSG_MESSAGE_NOT_FOUND);
+ nsIMdbRow* hdrRow = nullptr;
+ rv = m_mdbTable->PosToRow(m_mdbDB->GetEnv(), aIndex, &hdrRow);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hdrRow, NS_ERROR_FAILURE);
+ // CreateMsgHdr takes ownership of the hdrRow reference.
+ rv = m_mdbDB->CreateMsgHdr(hdrRow, oid.mOid_Id, result);
+ return (NS_SUCCEEDED(rv)) ? NS_OK : NS_MSG_MESSAGE_NOT_FOUND;
+}
+
+NS_IMETHODIMP nsMsgThread::GetChild(nsMsgKey msgKey, nsIMsgDBHdr** result) {
+ nsresult rv;
+
+ mdb_bool hasOid;
+ mdbOid rowObjectId;
+
+ NS_ENSURE_ARG_POINTER(result);
+ NS_ENSURE_TRUE(m_mdbTable, NS_ERROR_INVALID_POINTER);
+
+ *result = NULL;
+ rowObjectId.mOid_Id = msgKey;
+ rowObjectId.mOid_Scope = m_mdbDB->m_hdrRowScopeToken;
+ rv = m_mdbTable->HasOid(m_mdbDB->GetEnv(), &rowObjectId, &hasOid);
+
+ if (NS_SUCCEEDED(rv) && hasOid && m_mdbDB && m_mdbDB->m_mdbStore) {
+ nsIMdbRow* hdrRow = nullptr;
+ rv = m_mdbDB->m_mdbStore->GetRow(m_mdbDB->GetEnv(), &rowObjectId, &hdrRow);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hdrRow, NS_ERROR_FAILURE);
+ rv = m_mdbDB->CreateMsgHdr(hdrRow, msgKey, result);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::RemoveChildAt(uint32_t aIndex) { return NS_OK; }
+
+nsresult nsMsgThread::RemoveChild(nsMsgKey msgKey) {
+ nsresult rv;
+
+ mdbOid rowObjectId;
+ rowObjectId.mOid_Id = msgKey;
+ rowObjectId.mOid_Scope = m_mdbDB->m_hdrRowScopeToken;
+ rv = m_mdbTable->CutOid(m_mdbDB->GetEnv(), &rowObjectId);
+ // if this thread is empty, remove it from the all threads table.
+ if (m_numChildren == 0 && m_mdbDB->m_mdbAllThreadsTable) {
+ mdbOid rowID;
+ rowID.mOid_Id = m_threadKey;
+ rowID.mOid_Scope = m_mdbDB->m_threadRowScopeToken;
+
+ m_mdbDB->m_mdbAllThreadsTable->CutOid(m_mdbDB->GetEnv(), &rowID);
+ }
+#if 0 // this seems to cause problems
+ if (m_numChildren == 0 && m_metaRow && m_mdbDB)
+ m_metaRow->CutAllColumns(m_mdbDB->GetEnv());
+#endif
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::RemoveChildHdr(nsIMsgDBHdr* child,
+ nsIDBChangeAnnouncer* announcer) {
+ uint32_t flags;
+ nsMsgKey key;
+ nsMsgKey threadParent;
+
+ NS_ENSURE_ARG_POINTER(child);
+
+ child->GetFlags(&flags);
+ child->GetMessageKey(&key);
+
+ child->GetThreadParent(&threadParent);
+ ReparentChildrenOf(key, threadParent, announcer);
+
+ // if this was the newest msg, clear the newest msg date so we'll recalc.
+ uint32_t date;
+ child->GetDateInSeconds(&date);
+ if (date == m_newestMsgDate) SetNewestMsgDate(0);
+
+ if (!(flags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1);
+ ChangeChildCount(-1);
+ return RemoveChild(key);
+}
+
+nsresult nsMsgThread::ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer) {
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ if (numChildren > 0) {
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey threadParent;
+
+ curHdr->GetThreadParent(&threadParent);
+ if (threadParent == oldParent) {
+ nsMsgKey curKey;
+
+ curHdr->SetThreadParent(newParent);
+ curHdr->GetMessageKey(&curKey);
+ if (announcer)
+ announcer->NotifyParentChangedAll(curKey, oldParent, newParent,
+ nullptr);
+ // if the old parent was the root of the thread, then only the first
+ // child gets promoted to root, and other children become children of
+ // the new root.
+ if (newParent == nsMsgKey_None) {
+ SetThreadRootKey(curKey);
+ newParent = curKey;
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::MarkChildRead(bool bRead) {
+ ChangeUnreadChildCount(bRead ? -1 : 1);
+ return NS_OK;
+}
+
+/**
+ * Helper class for enumerating through the messages in a thread.
+ */
+class nsMsgThreadEnumerator : public nsBaseMsgEnumerator {
+ public:
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ // nsMsgThreadEnumerator methods:
+ typedef nsresult (*nsMsgThreadEnumeratorFilter)(nsIMsgDBHdr* hdr,
+ void* closure);
+
+ nsMsgThreadEnumerator(nsMsgThread* thread, nsMsgKey startKey,
+ nsMsgThreadEnumeratorFilter filter, void* closure);
+ int32_t MsgKeyFirstChildIndex(nsMsgKey inMsgKey);
+
+ protected:
+ ~nsMsgThreadEnumerator() override = default;
+ nsresult Prefetch();
+
+ nsIMdbTableRowCursor* mRowCursor;
+ nsCOMPtr<nsIMsgDBHdr> mResultHdr;
+ RefPtr<nsMsgThread> mThread;
+ nsMsgKey mThreadParentKey;
+ nsMsgKey mFirstMsgKey;
+ int32_t mChildIndex;
+ bool mDone;
+ bool mNeedToPrefetch;
+ nsMsgThreadEnumeratorFilter mFilter;
+ void* mClosure;
+ bool mFoundChildren;
+};
+
+nsMsgThreadEnumerator::nsMsgThreadEnumerator(nsMsgThread* thread,
+ nsMsgKey startKey,
+ nsMsgThreadEnumeratorFilter filter,
+ void* closure)
+ : mRowCursor(nullptr),
+ mDone(false),
+ mFilter(filter),
+ mClosure(closure),
+ mFoundChildren(false) {
+ mThreadParentKey = startKey;
+ mChildIndex = 0;
+ mThread = thread;
+ mNeedToPrefetch = true;
+ mFirstMsgKey = nsMsgKey_None;
+
+ nsresult rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+
+ if (NS_SUCCEEDED(rv) && mResultHdr) mResultHdr->GetMessageKey(&mFirstMsgKey);
+
+ uint32_t numChildren = 0;
+ mThread->GetNumChildren(&numChildren);
+
+ if (mThreadParentKey != nsMsgKey_None) {
+ nsMsgKey msgKey = nsMsgKey_None;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ mResultHdr->GetMessageKey(&msgKey);
+
+ if (msgKey == startKey) {
+ mChildIndex = MsgKeyFirstChildIndex(msgKey);
+ mDone = (mChildIndex < 0);
+ break;
+ }
+
+ if (mDone) break;
+ } else
+ NS_ASSERTION(false, "couldn't get child from thread");
+ }
+ }
+
+#ifdef DEBUG_bienvenu1
+ nsCOMPtr<nsIMsgDBHdr> child;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey threadParent;
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ child->GetMessageKey(&msgKey);
+ child->GetThreadParent(&threadParent);
+
+ printf("index = %ld key = %ld parent = %lx\n", childIndex, msgKey,
+ threadParent);
+ }
+ }
+#endif
+}
+
+int32_t nsMsgThreadEnumerator::MsgKeyFirstChildIndex(nsMsgKey inMsgKey) {
+ // if (msgKey != mThreadParentKey)
+ // mDone = true;
+ // look through rest of thread looking for a child of this message.
+ // If the inMsgKey is the first message in the thread, then all children
+ // without parents are considered to be children of inMsgKey.
+ // Otherwise, only true children qualify.
+
+ int32_t firstChildIndex = -1;
+ uint32_t numChildren = 0;
+ mThread->GetNumChildren(&numChildren);
+
+ // if this is the first message in the thread, just check if there's more than
+ // one message in the thread.
+ // if (inMsgKey == mThread->m_threadRootKey)
+ // return (numChildren > 1) ? 1 : -1;
+
+ for (uint32_t curChildIndex = 0; curChildIndex < numChildren;
+ curChildIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ nsresult rv = mThread->GetChildHdrAt(curChildIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey parentKey;
+
+ curHdr->GetThreadParent(&parentKey);
+ if (parentKey == inMsgKey) {
+ firstChildIndex = curChildIndex;
+ break;
+ }
+ }
+ }
+#ifdef DEBUG_bienvenu1
+ printf("first child index of %ld = %ld\n", inMsgKey, firstChildIndex);
+#endif
+ return firstChildIndex;
+}
+
+NS_IMETHODIMP nsMsgThreadEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+ nsresult rv;
+
+ if (mNeedToPrefetch) {
+ rv = Prefetch();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mResultHdr) {
+ NS_ADDREF(*aItem = mResultHdr);
+ mNeedToPrefetch = true;
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgThreadEnumerator::Prefetch() {
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ mResultHdr = nullptr;
+ if (mThreadParentKey == nsMsgKey_None) {
+ rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && mResultHdr,
+ "better be able to get root hdr");
+ mChildIndex = 0; // since root can be anywhere, set mChildIndex to 0.
+ } else if (!mDone) {
+ uint32_t numChildren = 0;
+ mThread->GetNumChildren(&numChildren);
+
+ while (mChildIndex < (int32_t)numChildren) {
+ rv = mThread->GetChildHdrAt(mChildIndex++, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ nsMsgKey parentKey;
+ nsMsgKey curKey;
+
+ if (mFilter && NS_FAILED(mFilter(mResultHdr, mClosure))) {
+ mResultHdr = nullptr;
+ continue;
+ }
+
+ mResultHdr->GetThreadParent(&parentKey);
+ mResultHdr->GetMessageKey(&curKey);
+ // if the parent is the same as the msg we're enumerating over,
+ // or the parentKey isn't set, and we're iterating over the top
+ // level message in the thread, then leave mResultHdr set to cur msg.
+ if (parentKey == mThreadParentKey ||
+ (parentKey == nsMsgKey_None && mThreadParentKey == mFirstMsgKey &&
+ curKey != mThreadParentKey))
+ break;
+ mResultHdr = nullptr;
+ } else
+ NS_ASSERTION(false, "better be able to get child");
+ }
+ if (!mResultHdr && mThreadParentKey == mFirstMsgKey && !mFoundChildren &&
+ numChildren > 1)
+ mThread->ReparentMsgsWithInvalidParent(numChildren, mThreadParentKey);
+ }
+ if (!mResultHdr) {
+ mDone = true;
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mDone = true;
+ return rv;
+ } else
+ mNeedToPrefetch = false;
+ mFoundChildren = true;
+
+#ifdef DEBUG_bienvenu1
+ nsMsgKey debugMsgKey;
+ mResultHdr->GetMessageKey(&debugMsgKey);
+ printf("next for %ld = %ld\n", mThreadParentKey, debugMsgKey);
+#endif
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThreadEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (mNeedToPrefetch) Prefetch();
+ *aResult = !mDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThread::EnumerateMessages(nsMsgKey parentKey,
+ nsIMsgEnumerator** result) {
+ NS_ADDREF(*result =
+ new nsMsgThreadEnumerator(this, parentKey, nullptr, nullptr));
+ return NS_OK;
+}
+
+nsresult nsMsgThread::ReparentMsgsWithInvalidParent(uint32_t numChildren,
+ nsMsgKey threadParentKey) {
+ nsresult rv = NS_OK;
+ // run through looking for messages that don't have a correct parent,
+ // i.e., a parent that's in the thread!
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(rv) && curChild) {
+ nsMsgKey parentKey;
+ nsCOMPtr<nsIMsgDBHdr> parent;
+
+ curChild->GetThreadParent(&parentKey);
+
+ if (parentKey != nsMsgKey_None) {
+ GetChild(parentKey, getter_AddRefs(parent));
+ if (!parent)
+ curChild->SetThreadParent(threadParentKey);
+ else {
+ nsMsgKey childKey;
+ curChild->GetMessageKey(&childKey);
+ // can't be your own parent; set parent to thread parent,
+ // or make ourselves the root if we are the root.
+ if (childKey == parentKey)
+ curChild->SetThreadParent(
+ m_threadRootKey == childKey ? nsMsgKey_None : m_threadRootKey);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::GetRootHdr(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ *result = nullptr;
+ int32_t resultIndex = -1;
+ nsresult rv = NS_OK;
+
+ if (m_threadRootKey != nsMsgKey_None) {
+ rv = GetChildHdrForKey(m_threadRootKey, result, &resultIndex);
+ if (NS_SUCCEEDED(rv) && *result) {
+ // check that we're really the root key.
+ nsMsgKey parentKey;
+ (*result)->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) return rv;
+ // XXX Hack: since GetChildHdrForKey() addref'ed result, we need to
+ // release any unwanted result before continuing.
+ NS_RELEASE(*result);
+ }
+#ifdef DEBUG_David_Bienvenu
+ printf("need to reset thread root key\n");
+#endif
+ nsMsgKey threadParentKey = nsMsgKey_None;
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(rv) && curChild) {
+ nsMsgKey parentKey;
+
+ curChild->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) {
+ curChild->GetMessageKey(&threadParentKey);
+ if (*result) {
+ NS_WARNING("two top level msgs, not good");
+ continue;
+ }
+ SetThreadRootKey(threadParentKey);
+ curChild.forget(result);
+ ReparentMsgsWithInvalidParent(numChildren, threadParentKey);
+ }
+ }
+ }
+ }
+ if (!*result) {
+ // if we can't get the thread root key, we'll just get the first hdr.
+ // there's a bug where sometimes we weren't resetting the thread root key
+ // when removing the thread root key.
+ rv = GetChildHdrAt(0, result);
+ }
+ if (!*result) return rv;
+ // Check that the thread id of the message is this thread.
+ nsMsgKey threadId = nsMsgKey_None;
+ (void)(*result)->GetThreadId(&threadId);
+ if (threadId != m_threadKey) (*result)->SetThreadId(m_threadKey);
+ return rv;
+}
+
+nsresult nsMsgThread::ChangeChildCount(int32_t delta) {
+ nsresult rv;
+
+ uint32_t childCount = 0;
+ m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadChildrenColumnToken, childCount);
+
+ NS_WARNING_ASSERTION(childCount != 0 || delta > 0,
+ "child count gone negative");
+ childCount += delta;
+
+ NS_WARNING_ASSERTION((int32_t)childCount >= 0,
+ "child count gone to 0 or below");
+ if ((int32_t)childCount < 0) // force child count to >= 0
+ childCount = 0;
+
+ rv = m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadChildrenColumnToken, childCount);
+ m_numChildren = childCount;
+ return rv;
+}
+
+nsresult nsMsgThread::ChangeUnreadChildCount(int32_t delta) {
+ nsresult rv;
+
+ uint32_t childCount = 0;
+ m_mdbDB->RowCellColumnToUInt32(
+ m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken, childCount);
+ childCount += delta;
+ if ((int32_t)childCount < 0) {
+#ifdef DEBUG_bienvenu1
+ NS_ASSERTION(false, "negative unread child count");
+#endif
+ childCount = 0;
+ }
+ rv = m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken, childCount);
+ m_numUnreadChildren = childCount;
+ return rv;
+}
+
+nsresult nsMsgThread::SetThreadRootKey(nsMsgKey threadRootKey) {
+ m_threadRootKey = threadRootKey;
+ return m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadRootKeyColumnToken, threadRootKey);
+}
+
+nsresult nsMsgThread::GetChildHdrForKey(nsMsgKey desiredKey,
+ nsIMsgDBHdr** result,
+ int32_t* resultIndex) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+ uint32_t childIndex;
+ for (childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = GetChildHdrAt(childIndex, result);
+ if (NS_SUCCEEDED(rv) && *result) {
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ (*result)->GetMessageKey(&msgKey);
+
+ if (msgKey == desiredKey) {
+ nsMsgKey threadKey;
+ (*result)->GetThreadId(&threadKey);
+ if (threadKey != m_threadKey) // this msg isn't in this thread
+ {
+ NS_WARNING("msg in wrong thread - this shouldn't happen");
+ uint32_t msgSize;
+ (*result)->GetMessageSize(&msgSize);
+ if (msgSize == 0) // this is a phantom message - let's get rid of it.
+ {
+ RemoveChild(msgKey);
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ // otherwise, let's try to figure out which thread
+ // this message really belongs to.
+ nsCOMPtr<nsIMsgThread> threadKeyThread =
+ dont_AddRef(m_mdbDB->GetThreadForThreadId(threadKey));
+ if (threadKeyThread) {
+ nsCOMPtr<nsIMsgDBHdr> otherThreadHdr;
+ threadKeyThread->GetChild(msgKey, getter_AddRefs(otherThreadHdr));
+ if (otherThreadHdr) {
+ // Message is in one thread but has a different thread id.
+ // Remove it from the thread and then rethread it.
+ RemoveChild(msgKey);
+ threadKeyThread->RemoveChildHdr(otherThreadHdr, nullptr);
+ bool newThread;
+ nsMsgHdr* msgHdr = static_cast<nsMsgHdr*>(otherThreadHdr.get());
+ m_mdbDB->ThreadNewHdr(msgHdr, newThread);
+ } else {
+ (*result)->SetThreadId(m_threadKey);
+ }
+ }
+ }
+ }
+ break;
+ }
+ // XXX Hack: since GetChildHdrAt() addref'ed result, we need to
+ // release any unwanted result before continuing in the loop.
+ NS_RELEASE(*result);
+ }
+ }
+ if (resultIndex) *resultIndex = (int32_t)childIndex;
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgThread::GetFirstUnreadChild(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ uint8_t minLevel = 0xff;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ nsCOMPtr<nsIMsgDBHdr> retHdr;
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ bool isRead;
+ rv = m_mdbDB->IsRead(msgKey, &isRead);
+ if (NS_SUCCEEDED(rv) && !isRead) {
+ // this is the root, so it's the best we're going to do.
+ if (msgKey == m_threadRootKey) {
+ retHdr = child;
+ break;
+ }
+ uint8_t level = 0;
+ nsMsgKey parentId;
+ child->GetThreadParent(&parentId);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ // count number of ancestors - that's our level
+ while (parentId != nsMsgKey_None) {
+ rv = m_mdbDB->GetMsgHdrForKey(parentId, getter_AddRefs(parent));
+ if (parent) {
+ parent->GetThreadParent(&parentId);
+ level++;
+ }
+ }
+ if (level < minLevel) {
+ minLevel = level;
+ retHdr = child;
+ }
+ }
+ }
+ }
+
+ retHdr.forget(result);
+ return (*result) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgThread::GetNewestMsgDate(uint32_t* aResult) {
+ // if this hasn't been set, figure it out by enumerating the msgs in the
+ // thread.
+ if (!m_newestMsgDate) {
+ nsresult rv;
+ uint32_t numChildren;
+ GetNumChildren(&numChildren);
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t msgDate;
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate;
+ }
+ }
+ }
+ *aResult = m_newestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThread::SetNewestMsgDate(uint32_t aNewestMsgDate) {
+ m_newestMsgDate = aNewestMsgDate;
+ return m_mdbDB->UInt32ToRowCellColumn(
+ m_metaRow, m_mdbDB->m_threadNewestMsgDateColumnToken, aNewestMsgDate);
+}
diff --git a/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp b/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp
new file mode 100644
index 0000000000..5a5ba19d5e
--- /dev/null
+++ b/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIMsgDBView.h"
+#include "nsIMsgThread.h"
+#include "nsNewsDatabase.h"
+#include "nsMsgKeySet.h"
+#include "nsMsgMessageFlags.h"
+#include "nsCOMPtr.h"
+#include "prlog.h"
+
+#if defined(DEBUG_sspitzer_) || defined(DEBUG_seth_)
+# define DEBUG_NEWS_DATABASE 1
+#endif
+
+nsNewsDatabase::nsNewsDatabase() { m_readSet = nullptr; }
+
+nsNewsDatabase::~nsNewsDatabase() {}
+
+NS_IMPL_ADDREF_INHERITED(nsNewsDatabase, nsMsgDatabase)
+NS_IMPL_RELEASE_INHERITED(nsNewsDatabase, nsMsgDatabase)
+
+NS_IMETHODIMP nsNewsDatabase::QueryInterface(REFNSIID aIID,
+ void** aInstancePtr) {
+ if (!aInstancePtr) return NS_ERROR_NULL_POINTER;
+ *aInstancePtr = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsINewsDatabase))) {
+ *aInstancePtr = static_cast<nsINewsDatabase*>(this);
+ }
+
+ if (*aInstancePtr) {
+ AddRef();
+ return NS_OK;
+ }
+
+ return nsMsgDatabase::QueryInterface(aIID, aInstancePtr);
+}
+
+nsresult nsNewsDatabase::Close(bool forceCommit) {
+ return nsMsgDatabase::Close(forceCommit);
+}
+
+nsresult nsNewsDatabase::ForceClosed() { return nsMsgDatabase::ForceClosed(); }
+
+nsresult nsNewsDatabase::Commit(nsMsgDBCommit commitType) {
+ if (m_dbFolderInfo && m_readSet) {
+ // let's write out our idea of the read set so we can compare it with that
+ // of the .rc file next time we start up.
+ nsCString readSet;
+ m_readSet->Output(getter_Copies(readSet));
+ m_dbFolderInfo->SetCharProperty("readSet", readSet);
+ }
+ return nsMsgDatabase::Commit(commitType);
+}
+
+uint32_t nsNewsDatabase::GetCurVersion() { return kMsgDBVersion; }
+
+NS_IMETHODIMP nsNewsDatabase::IsRead(nsMsgKey key, bool* pRead) {
+ NS_ASSERTION(pRead, "null out param in IsRead");
+ if (!pRead) return NS_ERROR_NULL_POINTER;
+
+ if (!m_readSet) return NS_ERROR_FAILURE;
+
+ *pRead = m_readSet->IsMember(key);
+ return NS_OK;
+}
+
+nsresult nsNewsDatabase::IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) {
+ nsresult rv;
+ nsMsgKey messageKey;
+
+ if (!msgHdr || !pRead) return NS_ERROR_NULL_POINTER;
+
+ rv = msgHdr->GetMessageKey(&messageKey);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = IsRead(messageKey, pRead);
+ return rv;
+}
+
+// return highest article number we've seen.
+NS_IMETHODIMP nsNewsDatabase::GetHighWaterArticleNum(nsMsgKey* key) {
+ NS_ASSERTION(m_dbFolderInfo, "null db folder info");
+ if (!m_dbFolderInfo) return NS_ERROR_FAILURE;
+ return m_dbFolderInfo->GetHighWater(key);
+}
+
+// return the key of the first article number we know about.
+// Since the iterator iterates in id order, we can just grab the
+// messagekey of the first header it returns.
+// ### dmb
+// This will not deal with the situation where we get holes in
+// the headers we know about. Need to figure out how and when
+// to solve that. This could happen if a transfer is interrupted.
+// Do we need to keep track of known arts permanently?
+NS_IMETHODIMP nsNewsDatabase::GetLowWaterArticleNum(nsMsgKey* key) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ rv = EnumerateMessages(getter_AddRefs(hdrs));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgDBHdr> first;
+ rv = hdrs->GetNext(getter_AddRefs(first));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken");
+ if (NS_FAILED(rv)) return rv;
+
+ return first->GetMessageKey(key);
+}
+
+nsresult nsNewsDatabase::ExpireUpTo(nsMsgKey expireKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+nsresult nsNewsDatabase::ExpireRange(nsMsgKey startRange, nsMsgKey endRange) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsNewsDatabase::GetReadSet(nsMsgKeySet** pSet) {
+ if (!pSet) return NS_ERROR_NULL_POINTER;
+ *pSet = m_readSet;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNewsDatabase::SetReadSet(nsMsgKeySet* pSet) {
+ m_readSet = pSet;
+
+ if (m_readSet) {
+ // compare this read set with the one in the db folder info.
+ // If not equivalent, sync with this one.
+ nsCString dbReadSet;
+ if (m_dbFolderInfo) m_dbFolderInfo->GetCharProperty("readSet", dbReadSet);
+ nsCString newsrcReadSet;
+ m_readSet->Output(getter_Copies(newsrcReadSet));
+ if (!dbReadSet.Equals(newsrcReadSet)) SyncWithReadSet();
+ }
+ return NS_OK;
+}
+
+bool nsNewsDatabase::SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) {
+ nsresult rv;
+ bool isRead;
+ rv = IsHeaderRead(msgHdr, &isRead);
+
+ if (isRead == bRead) {
+ // give the base class a chance to update m_flags.
+ nsMsgDatabase::SetHdrReadFlag(msgHdr, bRead);
+ return false;
+ } else {
+ nsMsgKey messageKey;
+
+ // give the base class a chance to update m_flags.
+ nsMsgDatabase::SetHdrReadFlag(msgHdr, bRead);
+ rv = msgHdr->GetMessageKey(&messageKey);
+ if (NS_FAILED(rv)) return false;
+
+ NS_ASSERTION(m_readSet, "m_readSet is null");
+ if (!m_readSet) return false;
+
+ if (!bRead) {
+#ifdef DEBUG_NEWS_DATABASE
+ printf("remove %d from the set\n", messageKey);
+#endif
+
+ m_readSet->Remove(messageKey);
+
+ rv = NotifyReadChanged(nullptr);
+ if (NS_FAILED(rv)) return false;
+ } else {
+#ifdef DEBUG_NEWS_DATABASE
+ printf("add %d to the set\n", messageKey);
+#endif
+
+ if (m_readSet->Add(messageKey) < 0) return false;
+
+ rv = NotifyReadChanged(nullptr);
+ if (NS_FAILED(rv)) return false;
+ }
+ }
+ return true;
+}
+
+NS_IMETHODIMP nsNewsDatabase::MarkAllRead(nsTArray<nsMsgKey>& aThoseMarked) {
+ nsMsgKey lowWater = nsMsgKey_None, highWater;
+ nsCString knownArts;
+ if (m_dbFolderInfo) {
+ m_dbFolderInfo->GetKnownArtsSet(getter_Copies(knownArts));
+ RefPtr<nsMsgKeySet> knownKeys = nsMsgKeySet::Create(knownArts.get());
+ if (knownKeys) lowWater = knownKeys->GetFirstMember();
+ }
+ if (lowWater == nsMsgKey_None) GetLowWaterArticleNum(&lowWater);
+ GetHighWaterArticleNum(&highWater);
+ if (lowWater > 2) m_readSet->AddRange(1, lowWater - 1);
+ nsresult err = nsMsgDatabase::MarkAllRead(aThoseMarked);
+ if (NS_SUCCEEDED(err) && 1 <= highWater)
+ m_readSet->AddRange(1, highWater); // mark everything read in newsrc.
+
+ return err;
+}
+
+nsresult nsNewsDatabase::SyncWithReadSet() {
+ // The code below attempts to update the underlying nsMsgDatabase's idea
+ // of read/unread flags to match the read set in the .newsrc file. It should
+ // only be called when they don't match, e.g., we crashed after committing the
+ // db but before writing out the .newsrc
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ nsresult rv = EnumerateMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false, readInNewsrc, isReadInDB, changed = false;
+ int32_t numMessages = 0, numUnreadMessages = 0;
+
+ // Scan all messages in DB
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = hdrs->GetNext(getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsMsgDatabase::IsHeaderRead(header, &isReadInDB);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey messageKey;
+ header->GetMessageKey(&messageKey);
+ IsRead(messageKey, &readInNewsrc);
+
+ numMessages++;
+ if (!readInNewsrc) numUnreadMessages++;
+
+ // If DB and readSet disagree on Read/Unread, fix DB
+ if (readInNewsrc != isReadInDB) {
+ MarkHdrRead(header, readInNewsrc, nullptr);
+ changed = true;
+ }
+ }
+
+ // Update FolderInfo Counters
+ if (m_dbFolderInfo) {
+ do {
+ int32_t oldMessages, oldUnreadMessages;
+ rv = m_dbFolderInfo->GetNumMessages(&oldMessages);
+ if (NS_FAILED(rv)) break;
+ if (oldMessages != numMessages) {
+ changed = true;
+ m_dbFolderInfo->ChangeNumMessages(numMessages - oldMessages);
+ }
+ rv = m_dbFolderInfo->GetNumUnreadMessages(&oldUnreadMessages);
+ if (NS_FAILED(rv)) break;
+ if (oldUnreadMessages != numUnreadMessages) {
+ changed = true;
+ m_dbFolderInfo->ChangeNumUnreadMessages(numUnreadMessages -
+ oldUnreadMessages);
+ }
+ } while (false);
+ }
+
+ if (changed) Commit(nsMsgDBCommitType::kLargeCommit);
+
+ return rv;
+}
+
+nsresult nsNewsDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) {
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::Offline && m_dbFolderInfo) {
+ uint32_t size = 0;
+ (void)msgHdr->GetOfflineMessageSize(&size);
+ return m_dbFolderInfo->ChangeExpungedBytes(size);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNewsDatabase::GetDefaultViewFlags(
+ nsMsgViewFlagsTypeValue* aDefaultViewFlags) {
+ NS_ENSURE_ARG_POINTER(aDefaultViewFlags);
+ GetIntPref("mailnews.default_news_view_flags", aDefaultViewFlags);
+ if (*aDefaultViewFlags < nsMsgViewFlagsType::kNone ||
+ *aDefaultViewFlags >
+ (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kShowIgnored | nsMsgViewFlagsType::kUnreadOnly |
+ nsMsgViewFlagsType::kExpandAll | nsMsgViewFlagsType::kGroupBySort))
+ *aDefaultViewFlags = nsMsgViewFlagsType::kThreadedDisplay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNewsDatabase::GetDefaultSortType(nsMsgViewSortTypeValue* aDefaultSortType) {
+ NS_ENSURE_ARG_POINTER(aDefaultSortType);
+ GetIntPref("mailnews.default_news_sort_type", aDefaultSortType);
+ if (*aDefaultSortType < nsMsgViewSortType::byDate ||
+ *aDefaultSortType > nsMsgViewSortType::byAccount)
+ *aDefaultSortType = nsMsgViewSortType::byThread;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNewsDatabase::GetDefaultSortOrder(
+ nsMsgViewSortOrderValue* aDefaultSortOrder) {
+ NS_ENSURE_ARG_POINTER(aDefaultSortOrder);
+ GetIntPref("mailnews.default_news_sort_order", aDefaultSortOrder);
+ if (*aDefaultSortOrder != nsMsgViewSortOrder::descending)
+ *aDefaultSortOrder = nsMsgViewSortOrder::ascending;
+ return NS_OK;
+}
diff --git a/comm/mailnews/db/msgdb/test/moz.build b/comm/mailnews/db/msgdb/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/db/msgdb/test/unit/head_maildb.js b/comm/mailnews/db/msgdb/test/unit/head_maildb.js
new file mode 100644
index 0000000000..5b52dbb304
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/head_maildb.js
@@ -0,0 +1,21 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+registerCleanupFunction(function () {
+ load("../../../../../mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js b/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js
new file mode 100644
index 0000000000..dfb5aa5285
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js
@@ -0,0 +1,56 @@
+/*
+ * Test nsMsgDatabase's cleanup of nsMsgDBEnumerators
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var anyOldMessage = do_get_file("../../../../data/bugmail1");
+
+/**
+ * Test closing a db with an outstanding enumerator.
+ */
+function test_enumerator_cleanup() {
+ let db = localAccountUtils.inboxFolder.msgDatabase;
+ let enumerator = db.enumerateMessages();
+ Cc["@mozilla.org/msgDatabase/msgDBService;1"]
+ .getService(Ci.nsIMsgDBService)
+ .forceFolderDBClosed(localAccountUtils.inboxFolder);
+ localAccountUtils.inboxFolder.msgDatabase = null;
+ db = null;
+ gc();
+ [...enumerator];
+ do_test_finished();
+}
+
+/*
+ * This infrastructure down here exists just to get
+ * test_references_header_parsing its message header.
+ */
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ anyOldMessage,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ messageHeaderGetterListener,
+ null
+ );
+ return true;
+}
+
+var messageHeaderGetterListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ GetMessageId(aMessageId) {},
+ SetMessageKey(aKey) {},
+ OnStopCopy(aStatus) {
+ do_timeout(0, test_enumerator_cleanup);
+ },
+};
diff --git a/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js b/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js
new file mode 100644
index 0000000000..914b5afd29
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js
@@ -0,0 +1,100 @@
+/* import-globals-from ../../../../test/resources/MessageGenerator.jsm */
+load("../../../../resources/MessageGenerator.jsm");
+
+var gMessages = [];
+
+const kSetCount = 13;
+const kNumExpectedMatches = 10;
+
+function setupGlobals() {
+ localAccountUtils.loadLocalMailAccount();
+ // Create a message generator
+ let messageGenerator = new MessageGenerator();
+ let localInbox = localAccountUtils.inboxFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+
+ for (let i = 0; i < kSetCount; i++) {
+ let message = messageGenerator.makeMessage();
+ gMessages.push(message);
+ localInbox.addMessage(message.toMboxString());
+ }
+}
+
+function run_test() {
+ setupGlobals();
+ do_test_pending();
+ let inboxDB = localAccountUtils.inboxFolder.msgDatabase;
+
+ // give messages 1,3,5 gloda-ids. These won't end up in our search hits.
+ let msgHdr1 = inboxDB.getMsgHdrForMessageID(gMessages[0].messageId);
+ msgHdr1.setUint32Property("gloda-id", 11111);
+ let msgHdr3 = inboxDB.getMsgHdrForMessageID(gMessages[2].messageId);
+ msgHdr3.setUint32Property("gloda-id", 33333);
+ let msgHdr5 = inboxDB.getMsgHdrForMessageID(gMessages[4].messageId);
+ msgHdr5.setUint32Property("gloda-id", 5555);
+ // set up a search term array that will give us the array of messages
+ // that gloda should index, as defined by this function:
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerms = [];
+
+ searchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ localAccountUtils.inboxFolder
+ );
+ let searchTerm = searchSession.createTerm();
+
+ // Create the following search term:
+ // (folderFlag & Mail && folderFlag != ImapBox) &&
+ // msg property.gloda-id isEmpty
+
+ searchTerm.beginsGrouping = true;
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.FolderFlag;
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ let value = searchTerm.value;
+ value.status = Ci.nsMsgFolderFlags.Mail;
+ value.attrib = Ci.nsMsgSearchAttrib.FolderFlag;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.FolderFlag;
+ searchTerm.op = Ci.nsMsgSearchOp.Isnt;
+ value = searchTerm.value;
+ value.status = Ci.nsMsgFolderFlags.ImapBox;
+ value.attrib = Ci.nsMsgSearchAttrib.FolderFlag;
+ searchTerm.value = value;
+ searchTerm.endsGrouping = true;
+ searchTerms.push(searchTerm);
+
+ searchTerm = searchSession.createTerm();
+ searchTerm.booleanAnd = true;
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.HdrProperty;
+ searchTerm.hdrProperty = "gloda-id";
+ searchTerm.op = Ci.nsMsgSearchOp.IsEmpty;
+ value = searchTerm.value;
+ value.str = "gloda-id";
+ value.attrib = Ci.nsMsgSearchAttrib.HdrProperty;
+ searchTerm.value = value;
+ searchTerms.push(searchTerm);
+
+ let msgEnumerator = inboxDB.getFilterEnumerator(searchTerms);
+ let matchingHdrs = [...msgEnumerator];
+ Assert.equal(kNumExpectedMatches, matchingHdrs.length);
+ Assert.equal(matchingHdrs[0].messageId, gMessages[1].messageId);
+ Assert.equal(matchingHdrs[1].messageId, gMessages[3].messageId);
+
+ // try it backwards, with roller skates:
+ msgEnumerator = inboxDB.getFilterEnumerator(searchTerms, true);
+ matchingHdrs = [...msgEnumerator];
+ Assert.equal(kNumExpectedMatches, matchingHdrs.length);
+ Assert.equal(matchingHdrs[0].messageId, gMessages[12].messageId);
+ Assert.equal(matchingHdrs[1].messageId, gMessages[11].messageId);
+ Assert.equal(matchingHdrs[9].messageId, gMessages[1].messageId);
+
+ do_test_finished();
+}
diff --git a/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js b/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js
new file mode 100644
index 0000000000..c0bc034bad
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test telemetry related to mails read.
+ */
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+/**
+ * Check that we're counting mails read.
+ */
+add_task(async function test_mails_read() {
+ Services.telemetry.clearScalars();
+
+ localAccountUtils.loadLocalMailAccount();
+
+ const NUM_MAILS = 5;
+ let headers =
+ "from: alice@t1.example.com\r\n" +
+ "to: bob@t2.example.net\r\n" +
+ "return-path: alice@t1.example.com\r\n" +
+ "Disposition-Notification-To: alice@t1.example.com\r\n";
+ for (let i = 0; i < NUM_MAILS; i++) {
+ localAccountUtils.inboxFolder.addMessage(
+ "From \r\n" + headers + "\r\nhello\r\n"
+ );
+ }
+ localAccountUtils.inboxFolder.markAllMessagesRead(null);
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars["tb.mails.read"],
+ NUM_MAILS,
+ "Count of mails read must be correct."
+ );
+});
diff --git a/comm/mailnews/db/msgdb/test/unit/test_maildb.js b/comm/mailnews/db/msgdb/test/unit/test_maildb.js
new file mode 100644
index 0000000000..9b6bca9303
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_maildb.js
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for msg database functions.
+ */
+
+/* import-globals-from ../../../../test/resources/MessageGenerator.jsm */
+load("../../../../resources/MessageGenerator.jsm");
+
+var dbService;
+var gTestFolder;
+var gCurTestNum = 0;
+var kNumTestMessages = 10;
+
+var gTestArray = [
+ function test_db_open() {
+ dbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+ // Get the root folder
+ let root = localAccountUtils.incomingServer.rootFolder;
+ root.createSubfolder("dbTest", null);
+ gTestFolder = root.getChildNamed("dbTest");
+ let db = dbService.openFolderDB(gTestFolder, true);
+ Assert.notEqual(db, null);
+ db.dBFolderInfo.highWater = 10;
+ db.close(true);
+ db = dbService.openFolderDB(gTestFolder, true);
+ Assert.notEqual(db, null);
+ Assert.equal(db.dBFolderInfo.highWater, 10);
+ db.dBFolderInfo.onKeyAdded(15);
+ Assert.equal(db.dBFolderInfo.highWater, 15);
+ db.close(true);
+ db.forceClosed();
+ db = null;
+ doTest(++gCurTestNum);
+ },
+];
+
+function doTest(test) {
+ if (test <= gTestArray.length) {
+ dump("Doing test " + test + "\n");
+ gCurTestNum = test;
+
+ var testFn = gTestArray[test - 1];
+ // Set a limit of 10 seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gCurTestNum == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " + testFn.name
+ );
+ }
+ });
+ try {
+ testFn();
+ } catch (ex) {
+ do_throw(ex);
+ }
+ } else {
+ do_test_finished(); // for the one in run_test()
+ }
+}
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+ doTest(1);
+}
diff --git a/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js b/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js
new file mode 100644
index 0000000000..57fb2605bd
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+// tests properties in nsIMsgDBHdr;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gHdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ // Get a message into the local filestore.
+ // Function continue_test() continues the testing after the copy.
+ var bugmail1 = do_get_file("../../../../data/bugmail1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continue_test();
+ },
+};
+
+function continue_test() {
+ // test some of the default properties
+ let properties = gHdr.properties;
+ Assert.ok(properties.includes("flags"));
+ Assert.ok(properties.includes("size"));
+ // this will be added in the next section, but does not exist yet
+ Assert.ok(!properties.includes("iamnew"));
+
+ // add a new property, and make sure that it appears
+ gHdr.setStringProperty("iamnew", "somevalue");
+
+ properties = [];
+ for (let property of gHdr.properties) {
+ // dump("\nProperty 2 is " + property);
+ properties.push(property);
+ }
+ Assert.ok(properties.includes("flags"));
+ Assert.ok(properties.includes("size"));
+ Assert.ok(properties.includes("iamnew"));
+ Assert.ok(!properties.includes("idonotexist"));
+
+ gHdr = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js b/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js
new file mode 100644
index 0000000000..fdfa76dd6d
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js
@@ -0,0 +1,124 @@
+/*
+ * Test nsMsgHdr's In-Reply-To/References parsing logic.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var anyOldMessage = do_get_file("../../../../data/bugmail1");
+
+var refsAndResults = [
+ // an empty string is not a reference.
+ ["", []],
+ // super valid things
+ ["<abc@def>", ["abc@def"]],
+ [
+ "<up@down> <left@right> <ying@yang>",
+ ["up@down", "left@right", "ying@yang"],
+ ],
+ // whitespace type things
+ [" ", []],
+ [" <left@space>", ["left@space"]],
+ ["<space@right> ", ["space@right"]],
+ [" <space@space> ", ["space@space"]],
+ ["\t<tab@tab>\t", ["tab@tab"]],
+ ["<a@b>\n\t<tab@newline.n>", ["a@b", "tab@newline.n"]],
+ ["<a@b>\r\t<tab@newline.r>", ["a@b", "tab@newline.r"]],
+ ["<a@b>\n\t<tab@newline.nr>", ["a@b", "tab@newline.nr"]],
+ [
+ "<a@1>\n<a@2> <a@3>\t <a@4>\n <a@5>\r\t<a@6>\r\n <a@7>\r\n\t ",
+ ["a@1", "a@2", "a@3", "a@4", "a@5", "a@6", "a@7"],
+ ],
+ // be backwards compatible with old-school things that make some sense
+ ["i am a stupid message-id", ["i am a stupid message-id"]],
+ [" those were spaces!", ["those were spaces!"]],
+ // be backwards compatible with things that make no sense
+ [" seriously\n who does this?", ["seriously\n who does this?"]],
+ // handle things we used to be stupid about
+ ["<z@1a> was an awesome message!", ["z@1a"]],
+ [" <z@1b> was an awesomer message!", ["z@1b"]],
+ ["I can't get enough of <z@2a>", ["z@2a"]],
+ [" nor of I can enough get <z@2b> ", ["z@2b"]],
+ ["let's talk about <z@3a> shall we", ["z@3a"]],
+ ["and then let us speak of <z@3b> and its\n many points", ["z@3b"]],
+ // be backwards compatible with things that just seem malicious
+ [" 4 < 5", ["4 < 5"]],
+ [" 6 > 3", ["6 > 3"]],
+ [" look ma!\n newlines!", ["look ma!\n newlines!"]],
+];
+
+/**
+ * Parse the references in refsAndResults and ensure their references match
+ * the corresponding results.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - A message header that you don't mind if we
+ * mess with.
+ */
+function test_references_header_parsing(aMsgHdr) {
+ var iCase, iResult, refString, results;
+ for (iCase = 0; iCase < refsAndResults.length; iCase++) {
+ refString = refsAndResults[iCase][0];
+ results = refsAndResults[iCase][1];
+
+ dump("Setting references to: '" + refString + "'\n");
+ aMsgHdr.setReferences(refString);
+ if (aMsgHdr.numReferences != results.length) {
+ dump("Length mismatch! Was expecting:\n");
+ for (iResult = 0; iResult < results.length; iResult++) {
+ dump("'" + results[iResult] + "'\n");
+ }
+
+ dump("Got:\n");
+
+ for (iResult = 0; iResult < aMsgHdr.numReferences; iResult++) {
+ dump("'" + aMsgHdr.getStringReference(iResult) + "'\n");
+ }
+
+ Assert.equal(aMsgHdr.numReferences, results.length);
+ }
+
+ for (iResult = 0; iResult < results.length; iResult++) {
+ Assert.equal(aMsgHdr.getStringReference(iResult), results[iResult]);
+ }
+ }
+
+ do_test_finished();
+}
+
+/*
+ * This infrastructure down here exists just to get
+ * test_references_header_parsing its message header.
+ */
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ anyOldMessage,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ messageHeaderGetterListener,
+ null
+ );
+ return true;
+}
+
+var messageHeaderGetterListener = {
+ msgKey: null,
+
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ GetMessageId(aMessageId) {},
+ SetMessageKey(aKey) {
+ this.msgKey = aKey;
+ },
+ OnStopCopy(aStatus) {
+ test_references_header_parsing(
+ localAccountUtils.inboxFolder.GetMessageHeader(this.msgKey)
+ );
+ },
+};
diff --git a/comm/mailnews/db/msgdb/test/unit/xpcshell.ini b/comm/mailnews/db/msgdb/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..d32c984408
--- /dev/null
+++ b/comm/mailnews/db/msgdb/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head = head_maildb.js
+tail =
+
+[test_enumerator_cleanup.js]
+[test_filter_enumerator.js]
+[test_maildb.js]
+[test_mailTelemetry.js]
+[test_propertyEnumerator.js]
+[test_references_parsing.js]
diff --git a/comm/mailnews/export/content/exportDialog.js b/comm/mailnews/export/content/exportDialog.js
new file mode 100644
index 0000000000..4887526865
--- /dev/null
+++ b/comm/mailnews/export/content/exportDialog.js
@@ -0,0 +1,154 @@
+/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+var nsFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+);
+
+// No need to backup those paths, they are not used when importing.
+const IGNORE_PATHS = [
+ "cache2",
+ "chrome_debugger_profile",
+ "crashes",
+ "datareporting",
+ "extensions",
+ "extension-store",
+ "logs",
+ "lock",
+ "minidumps",
+ "parent.lock",
+ "shader-cache",
+ "saved-telemetry-pings",
+ "security_state",
+ "storage",
+ "xulstore",
+];
+
+var zipW;
+
+var logger = console.createInstance({
+ prefix: "mail.export",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.export.loglevel",
+});
+
+document.addEventListener("dialogaccept", async event => {
+ if (zipW) {
+ // This will close the dialog.
+ return;
+ }
+
+ // Do not close the dialog, but open a FilePicker to set the output location.
+ event.preventDefault();
+
+ let [filePickerTitle, brandName] = await document.l10n.formatValues([
+ "export-dialog-file-picker",
+ "export-dialog-brand-name",
+ ]);
+ let filePicker = Components.Constructor(
+ "@mozilla.org/filepicker;1",
+ "nsIFilePicker"
+ )();
+ filePicker.init(window, filePickerTitle, Ci.nsIFilePicker.modeSave);
+ filePicker.defaultString = `${brandName}_profile_backup.zip`;
+ filePicker.defaultExtension = "zip";
+ filePicker.appendFilter("", "*.zip");
+ filePicker.open(rv => {
+ if (
+ [Ci.nsIFilePicker.returnOK, Ci.nsIFilePicker.returnReplace].includes(
+ rv
+ ) &&
+ filePicker.file
+ ) {
+ exportCurrentProfile(filePicker.file);
+ } else {
+ window.close();
+ }
+ });
+});
+
+/**
+ * Export the current profile to the specified target zip file.
+ *
+ * @param {nsIFile} targetFile - A target zip file to write to.
+ */
+async function exportCurrentProfile(targetFile) {
+ let [progressExporting, progressExported, buttonLabelFinish] =
+ await document.l10n.formatValues([
+ "export-dialog-exporting",
+ "export-dialog-exported",
+ "export-dialog-button-finish",
+ ]);
+ document.getElementById("progressBar").hidden = false;
+ let progressStatus = document.getElementById("progressStatus");
+ progressStatus.value = progressExporting;
+ let buttonAccept = document.querySelector("dialog").getButton("accept");
+ buttonAccept.disabled = true;
+ document.querySelector("dialog").getButton("cancel").hidden = true;
+
+ zipW = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter")();
+ // MODE_WRONLY (0x02) and MODE_CREATE (0x08)
+ zipW.open(targetFile, 0x02 | 0x08);
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let rootPath = profileDir.parent.path;
+ let zipEntryMap = new Map();
+ await collectFilesToZip(zipEntryMap, rootPath, profileDir);
+
+ let progressElement = document.getElementById("progress");
+ progressElement.max = zipEntryMap.size;
+ let i = 0;
+ for (let [path, file] of zipEntryMap) {
+ logger.debug("Adding entry file:", path);
+ zipW.addEntryFile(
+ path,
+ 0, // no compression, bigger file but much faster
+ file,
+ false
+ );
+ if (++i % 10 === 0) {
+ progressElement.value = i;
+ await new Promise(resolve => setTimeout(resolve));
+ }
+ }
+ progressElement.value = progressElement.max;
+ zipW.close();
+
+ progressStatus.value = progressExported;
+ buttonAccept.disabled = false;
+ buttonAccept.label = buttonLabelFinish;
+}
+
+/**
+ * Recursively collect files to be zipped, save the entries into zipEntryMap.
+ *
+ * @param {Map<string, nsIFile>} zipEntryMap - Collection of files to be zipped.
+ * @param {string} rootPath - The rootPath to zip from.
+ * @param {nsIFile} folder - The folder to search for files to zip.
+ */
+async function collectFilesToZip(zipEntryMap, rootPath, folder) {
+ let entries = await IOUtils.getChildren(folder.path);
+ let separator = Services.appinfo.OS == "WINNT" ? "\\" : "/";
+ for (let entry of entries) {
+ let file = nsFile(entry);
+ if (file.isDirectory()) {
+ await collectFilesToZip(zipEntryMap, rootPath, file);
+ } else {
+ // We don't want to include the rootPath part in the zip file.
+ let path = entry.slice(rootPath.length + 1);
+ // path now looks like this: profile-default/lock.
+ let parts = path.split(separator);
+ if (IGNORE_PATHS.includes(parts[1])) {
+ continue;
+ }
+ // Path separator inside a zip file is always "/".
+ zipEntryMap.set(parts.join("/"), file);
+ }
+ }
+}
diff --git a/comm/mailnews/export/content/exportDialog.xhtml b/comm/mailnews/export/content/exportDialog.xhtml
new file mode 100644
index 0000000000..9cc28fb064
--- /dev/null
+++ b/comm/mailnews/export/content/exportDialog.xhtml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTf-8"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ lightweightthemes="true"
+>
+ <head>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/exportDialog.ftl" />
+ <title data-l10n-id="export-dialog-title"></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/exportDialog.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept,cancel"
+ style="min-height: 12em; width: 500px"
+ data-l10n-id="export-dialog"
+ data-l10n-attrs="buttonlabelaccept"
+ >
+ <description data-l10n-id="export-dialog-description1"></description>
+ <description data-l10n-id="export-dialog-desc2"></description>
+
+ <separator />
+ <vbox id="progressBar" hidden="true">
+ <html:progress id="progress" value="0" max="100" style="flex: 1" />
+ <label id="progressStatus" crop="center" />
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/export/modules/ProfileExporter.jsm b/comm/mailnews/export/modules/ProfileExporter.jsm
new file mode 100644
index 0000000000..6348471fbd
--- /dev/null
+++ b/comm/mailnews/export/modules/ProfileExporter.jsm
@@ -0,0 +1,114 @@
+/* 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 = ["ProfileExporter"];
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+// No need to backup those paths, they are not used when importing.
+const IGNORE_PATHS = [
+ "cache2",
+ "chrome_debugger_profile",
+ "crashes",
+ "datareporting",
+ "extensions",
+ "extension-store",
+ "logs",
+ "lock",
+ "minidumps",
+ "parent.lock",
+ "shader-cache",
+ "saved-telemetry-pings",
+ "security_state",
+ "storage",
+ "xulstore",
+];
+
+/**
+ * A module to export the current profile to a zip file.
+ */
+class ProfileExporter {
+ _logger = console.createInstance({
+ prefix: "mail.export",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.export.loglevel",
+ });
+
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ /**
+ * Export the current profile to the specified target zip file.
+ *
+ * @param {nsIFile} targetFile - A target zip file to write to.
+ */
+ async startExport(targetFile) {
+ let zipW = Components.Constructor(
+ "@mozilla.org/zipwriter;1",
+ "nsIZipWriter"
+ )();
+ // MODE_WRONLY (0x02) and MODE_CREATE (0x08)
+ zipW.open(targetFile, 0x02 | 0x08);
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let rootPathCount = PathUtils.split(profileDir.parent.path).length;
+ let zipEntryMap = new Map();
+ await this._collectFilesToZip(zipEntryMap, rootPathCount, profileDir);
+
+ let totalEntries = zipEntryMap.size;
+ let i = 0;
+ for (let [path, file] of zipEntryMap) {
+ try {
+ zipW.addEntryFile(
+ path,
+ 0, // no compression, bigger file but much faster
+ file,
+ false
+ );
+ } catch (e) {
+ this._logger.error(`Failed to add ${path}`, e);
+ }
+ if (++i % 10 === 0) {
+ this.onProgress(i, totalEntries);
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(totalEntries, totalEntries);
+ zipW.close();
+ }
+
+ /**
+ * Recursively collect files to be zipped, save the entries into zipEntryMap.
+ *
+ * @param {Map<string, nsIFile>} zipEntryMap - Collection of files to be zipped.
+ * @param {number} rootPathCount - The count of rootPath parts.
+ * @param {nsIFile} folder - The folder to search for files to zip.
+ */
+ async _collectFilesToZip(zipEntryMap, rootPathCount, folder) {
+ for (let file of folder.directoryEntries) {
+ if (!file.exists()) {
+ continue;
+ }
+ if (file.isDirectory()) {
+ await this._collectFilesToZip(zipEntryMap, rootPathCount, file);
+ } else {
+ // We don't want to include the rootPath part in the zip file.
+ let parts = PathUtils.split(file.path).slice(rootPathCount);
+ // Parts look like this: ["profile-default", "lock"].
+ if (IGNORE_PATHS.includes(parts[1])) {
+ continue;
+ }
+ // Path separator inside a zip file is always "/".
+ zipEntryMap.set(parts.join("/"), file);
+ }
+ }
+ }
+}
diff --git a/comm/mailnews/export/modules/moz.build b/comm/mailnews/export/modules/moz.build
new file mode 100644
index 0000000000..a6d704b287
--- /dev/null
+++ b/comm/mailnews/export/modules/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "ProfileExporter.jsm",
+]
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/components.conf b/comm/mailnews/extensions/bayesian-spam-filter/components.conf
new file mode 100644
index 0000000000..98fe2d6aeb
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/components.conf
@@ -0,0 +1,15 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{f1070bfa-d539-11d6-90ca-00039310a47a}",
+ "contract_ids": ["@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter"],
+ "type": "nsBayesianFilter",
+ "init_method": "Init",
+ "headers": [
+ "/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.h"
+ ],
+ },
+]
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/moz.build b/comm/mailnews/extensions/bayesian-spam-filter/moz.build
new file mode 100644
index 0000000000..329fdcafa4
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/moz.build
@@ -0,0 +1,16 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsBayesianFilter.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp b/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp
new file mode 100644
index 0000000000..8a4cca905b
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp
@@ -0,0 +1,2548 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsBayesianFilter.h"
+#include "nsIInputStream.h"
+#include "nsIStreamListener.h"
+#include "nsNetUtil.h"
+#include "nsQuickSort.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h" // for GetMessageServiceFromURI
+#include "prnetdb.h"
+#include "nsIMsgWindow.h"
+#include "mozilla/Logging.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsUnicharUtils.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIMIMEHeaderParam.h"
+#include "nsNetCID.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIStringEnumerator.h"
+#include "nsIObserverService.h"
+#include "nsIChannel.h"
+#include "nsIMailChannel.h"
+#include "nsDependentSubstring.h"
+#include "nsMemory.h"
+#include "nsUnicodeProperties.h"
+
+#include "mozilla/ArenaAllocatorExtensions.h" // for ArenaStrdup
+
+using namespace mozilla;
+using mozilla::intl::Script;
+using mozilla::intl::UnicodeProperties;
+
+// needed to mark attachment flag on the db hdr
+#include "nsIMsgHdr.h"
+
+// needed to strip html out of the body
+#include "nsLayoutCID.h"
+#include "nsIParserUtils.h"
+#include "nsIDocumentEncoder.h"
+
+#include "nsIncompleteGamma.h"
+#include <math.h>
+#include <prmem.h>
+#include "nsIMsgTraitService.h"
+#include "mozilla/Services.h"
+#include "mozilla/Attributes.h"
+#include <cstdlib> // for std::abs(int/long)
+#include <cmath> // for std::abs(float/double)
+
+static mozilla::LazyLogModule BayesianFilterLogModule("BayesianFilter");
+
+#define kDefaultJunkThreshold .99 // we override this value via a pref
+static const char* kBayesianFilterTokenDelimiters = " \t\n\r\f.";
+static unsigned int kMinLengthForToken =
+ 3; // lower bound on the number of characters in a word before we treat it
+ // as a token
+static unsigned int kMaxLengthForToken =
+ 12; // upper bound on the number of characters in a word to be declared as
+ // a token
+
+#define FORGED_RECEIVED_HEADER_HINT "may be forged"_ns
+
+#ifndef M_LN2
+# define M_LN2 0.69314718055994530942
+#endif
+
+#ifndef M_E
+# define M_E 2.7182818284590452354
+#endif
+
+// provide base implementation of hash lookup of a string
+struct BaseToken : public PLDHashEntryHdr {
+ const char* mWord;
+};
+
+// token for a particular message
+// mCount, mAnalysisLink are initialized to zero by the hash code
+struct Token : public BaseToken {
+ uint32_t mCount;
+ uint32_t mAnalysisLink; // index in mAnalysisStore of the AnalysisPerToken
+ // object for the first trait for this token
+ // Helper to support Tokenizer::copyTokens()
+ void clone(const Token& other) {
+ mWord = other.mWord;
+ mCount = other.mCount;
+ mAnalysisLink = other.mAnalysisLink;
+ }
+};
+
+// token stored in a training file for a group of messages
+// mTraitLink is initialized to 0 by the hash code
+struct CorpusToken : public BaseToken {
+ uint32_t mTraitLink; // index in mTraitStore of the TraitPerToken
+ // object for the first trait for this token
+};
+
+// set the value of a TraitPerToken object
+TraitPerToken::TraitPerToken(uint32_t aTraitId, uint32_t aCount)
+ : mId(aTraitId), mCount(aCount), mNextLink(0) {}
+
+// shorthand representations of trait ids for junk and good
+static const uint32_t kJunkTrait = nsIJunkMailPlugin::JUNK_TRAIT;
+static const uint32_t kGoodTrait = nsIJunkMailPlugin::GOOD_TRAIT;
+
+// set the value of an AnalysisPerToken object
+AnalysisPerToken::AnalysisPerToken(uint32_t aTraitIndex, double aDistance,
+ double aProbability)
+ : mTraitIndex(aTraitIndex),
+ mDistance(aDistance),
+ mProbability(aProbability),
+ mNextLink(0) {}
+
+// the initial size of the AnalysisPerToken linked list storage
+const uint32_t kAnalysisStoreCapacity = 2048;
+
+// the initial size of the TraitPerToken linked list storage
+const uint32_t kTraitStoreCapacity = 16384;
+
+// Size of Auto arrays representing per trait information
+const uint32_t kTraitAutoCapacity = 10;
+
+TokenEnumeration::TokenEnumeration(PLDHashTable* table)
+ : mIterator(table->Iter()) {}
+
+inline bool TokenEnumeration::hasMoreTokens() { return !mIterator.Done(); }
+
+inline BaseToken* TokenEnumeration::nextToken() {
+ auto token = static_cast<BaseToken*>(mIterator.Get());
+ mIterator.Next();
+ return token;
+}
+
+// member variables
+static const PLDHashTableOps gTokenTableOps = {
+ PLDHashTable::HashStringKey, PLDHashTable::MatchStringKey,
+ PLDHashTable::MoveEntryStub, PLDHashTable::ClearEntryStub, nullptr};
+
+TokenHash::TokenHash(uint32_t aEntrySize)
+ : mTokenTable(&gTokenTableOps, aEntrySize, 128) {
+ mEntrySize = aEntrySize;
+}
+
+TokenHash::~TokenHash() {}
+
+nsresult TokenHash::clearTokens() {
+ // we re-use the tokenizer when classifying multiple messages,
+ // so this gets called after every message classification.
+ mTokenTable.ClearAndPrepareForLength(128);
+ mWordPool.Clear();
+ return NS_OK;
+}
+
+char* TokenHash::copyWord(const char* word, uint32_t len) {
+ return ArenaStrdup(Substring(word, len), mWordPool);
+}
+
+inline BaseToken* TokenHash::get(const char* word) {
+ PLDHashEntryHdr* entry = mTokenTable.Search(word);
+ if (entry) return static_cast<BaseToken*>(entry);
+ return NULL;
+}
+
+BaseToken* TokenHash::add(const char* word) {
+ if (!word || !*word) {
+ NS_ERROR("Trying to add a null word");
+ return nullptr;
+ }
+
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug, ("add word: %s", word));
+
+ PLDHashEntryHdr* entry = mTokenTable.Add(word, mozilla::fallible);
+ BaseToken* token = static_cast<BaseToken*>(entry);
+ if (token) {
+ if (token->mWord == NULL) {
+ uint32_t len = strlen(word);
+ NS_ASSERTION(len != 0, "adding zero length word to tokenizer");
+ if (!len)
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("adding zero length word to tokenizer"));
+ token->mWord = copyWord(word, len);
+ NS_ASSERTION(token->mWord, "copyWord failed");
+ if (!token->mWord) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("copyWord failed: %s (%d)", word, len));
+ mTokenTable.RawRemove(entry);
+ return NULL;
+ }
+ }
+ }
+ return token;
+}
+
+inline uint32_t TokenHash::countTokens() { return mTokenTable.EntryCount(); }
+
+inline TokenEnumeration TokenHash::getTokens() {
+ return TokenEnumeration(&mTokenTable);
+}
+
+Tokenizer::Tokenizer()
+ : TokenHash(sizeof(Token)),
+ mBodyDelimiters(kBayesianFilterTokenDelimiters),
+ mHeaderDelimiters(kBayesianFilterTokenDelimiters),
+ mCustomHeaderTokenization(false),
+ mMaxLengthForToken(kMaxLengthForToken),
+ mIframeToDiv(false) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefs->GetBranch("mailnews.bayesian_spam_filter.",
+ getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS_VOID(rv); // no branch defined, just use defaults
+
+ /*
+ * RSS feeds store their summary as alternate content of an iframe. But due
+ * to bug 365953, this is not seen by the serializer. As a workaround, allow
+ * the tokenizer to replace the iframe with div for tokenization.
+ */
+ rv = prefBranch->GetBoolPref("iframe_to_div", &mIframeToDiv);
+ if (NS_FAILED(rv)) mIframeToDiv = false;
+
+ /*
+ * the list of delimiters used to tokenize the message and body
+ * defaults to the value in kBayesianFilterTokenDelimiters, but may be
+ * set with the following preferences for the body and header
+ * separately.
+ *
+ * \t, \n, \v, \f, \r, and \\ will be escaped to their normal
+ * C-library values, all other two-letter combinations beginning with \
+ * will be ignored.
+ */
+
+ prefBranch->GetCharPref("body_delimiters", mBodyDelimiters);
+ if (!mBodyDelimiters.IsEmpty())
+ UnescapeCString(mBodyDelimiters);
+ else // prefBranch empties the result when it fails :(
+ mBodyDelimiters.Assign(kBayesianFilterTokenDelimiters);
+
+ prefBranch->GetCharPref("header_delimiters", mHeaderDelimiters);
+ if (!mHeaderDelimiters.IsEmpty())
+ UnescapeCString(mHeaderDelimiters);
+ else
+ mHeaderDelimiters.Assign(kBayesianFilterTokenDelimiters);
+
+ /*
+ * Extensions may wish to enable or disable tokenization of certain headers.
+ * Define any headers to enable/disable in a string preference like this:
+ * "mailnews.bayesian_spam_filter.tokenizeheader.headername"
+ *
+ * where "headername" is the header to tokenize. For example, to tokenize the
+ * header "x-spam-status" use the preference:
+ *
+ * "mailnews.bayesian_spam_filter.tokenizeheader.x-spam-status"
+ *
+ * The value of the string preference will be interpreted in one of
+ * four ways, depending on the value:
+ *
+ * If "false" then do not tokenize that header
+ * If "full" then add the entire header value as a token,
+ * without breaking up into subtokens using delimiters
+ * If "standard" then tokenize the header using as delimiters the current
+ * value of the generic header delimiters
+ * Any other string is interpreted as a list of delimiters to use to parse
+ * the header. \t, \n, \v, \f, \r, and \\ will be escaped to their normal
+ * C-library values, all other two-letter combinations beginning with \
+ * will be ignored.
+ *
+ * Header names in the preference should be all lower case
+ *
+ * Extensions may also set the maximum length of a token (default is
+ * kMaxLengthForToken) by setting the int preference:
+ * "mailnews.bayesian_spam_filter.maxlengthfortoken"
+ */
+
+ nsTArray<nsCString> headers;
+
+ // get customized maximum token length
+ int32_t maxLengthForToken;
+ rv = prefBranch->GetIntPref("maxlengthfortoken", &maxLengthForToken);
+ mMaxLengthForToken =
+ NS_SUCCEEDED(rv) ? uint32_t(maxLengthForToken) : kMaxLengthForToken;
+
+ rv = prefs->GetBranch("mailnews.bayesian_spam_filter.tokenizeheader.",
+ getter_AddRefs(prefBranch));
+ if (NS_SUCCEEDED(rv)) rv = prefBranch->GetChildList("", headers);
+
+ if (NS_SUCCEEDED(rv)) {
+ mCustomHeaderTokenization = true;
+ for (auto& header : headers) {
+ nsCString value;
+ prefBranch->GetCharPref(header.get(), value);
+ if (value.EqualsLiteral("false")) {
+ mDisabledHeaders.AppendElement(header);
+ continue;
+ }
+ mEnabledHeaders.AppendElement(header);
+ if (value.EqualsLiteral("standard"))
+ value.SetIsVoid(true); // Void means use default delimiter
+ else if (value.EqualsLiteral("full"))
+ value.Truncate(); // Empty means add full header
+ else
+ UnescapeCString(value);
+ mEnabledHeadersDelimiters.AppendElement(value);
+ }
+ }
+}
+
+Tokenizer::~Tokenizer() {}
+
+inline Token* Tokenizer::get(const char* word) {
+ return static_cast<Token*>(TokenHash::get(word));
+}
+
+Token* Tokenizer::add(const char* word, uint32_t count) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("add word: %s (count=%d)", word, count));
+
+ Token* token = static_cast<Token*>(TokenHash::add(word));
+ if (token) {
+ token->mCount += count; // hash code initializes this to zero
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("adding word to tokenizer: %s (count=%d) (mCount=%d)", word, count,
+ token->mCount));
+ }
+ return token;
+}
+
+static bool isDecimalNumber(const char* word) {
+ const char* p = word;
+ if (*p == '-') ++p;
+ char c;
+ while ((c = *p++)) {
+ if (!isdigit((unsigned char)c)) return false;
+ }
+ return true;
+}
+
+static bool isASCII(const char* word) {
+ const unsigned char* p = (const unsigned char*)word;
+ unsigned char c;
+ while ((c = *p++)) {
+ if (c > 127) return false;
+ }
+ return true;
+}
+
+inline bool isUpperCase(char c) { return ('A' <= c) && (c <= 'Z'); }
+
+static char* toLowerCase(char* str) {
+ char c, *p = str;
+ while ((c = *p++)) {
+ if (isUpperCase(c)) p[-1] = c + ('a' - 'A');
+ }
+ return str;
+}
+
+void Tokenizer::addTokenForHeader(const char* aTokenPrefix, nsACString& aValue,
+ bool aTokenizeValue,
+ const char* aDelimiters) {
+ if (aValue.Length()) {
+ ToLowerCase(aValue);
+ if (!aTokenizeValue) {
+ nsCString tmpStr;
+ tmpStr.Assign(aTokenPrefix);
+ tmpStr.Append(':');
+ tmpStr.Append(aValue);
+
+ add(tmpStr.get());
+ } else {
+ char* word;
+ nsCString str(aValue);
+ char* next = str.BeginWriting();
+ const char* delimiters =
+ !aDelimiters ? mHeaderDelimiters.get() : aDelimiters;
+ while ((word = NS_strtok(delimiters, &next)) != NULL) {
+ if (strlen(word) < kMinLengthForToken) continue;
+ if (isDecimalNumber(word)) continue;
+ if (isASCII(word)) {
+ nsCString tmpStr;
+ tmpStr.Assign(aTokenPrefix);
+ tmpStr.Append(':');
+ tmpStr.Append(word);
+ add(tmpStr.get());
+ }
+ }
+ }
+ }
+}
+
+void Tokenizer::tokenizeAttachments(
+ nsTArray<RefPtr<nsIPropertyBag2>>& attachments) {
+ for (auto attachment : attachments) {
+ nsCString contentType;
+ ToLowerCase(contentType);
+ attachment->GetPropertyAsAUTF8String(u"contentType"_ns, contentType);
+ addTokenForHeader("attachment/content-type", contentType);
+
+ nsCString displayName;
+ attachment->GetPropertyAsAUTF8String(u"displayName"_ns, displayName);
+ ToLowerCase(displayName);
+ addTokenForHeader("attachment/filename", displayName);
+ }
+}
+
+void Tokenizer::tokenizeHeaders(nsTArray<nsCString>& aHeaderNames,
+ nsTArray<nsCString>& aHeaderValues) {
+ nsCString headerValue;
+ nsAutoCString
+ headerName; // we'll be normalizing all header names to lower case
+
+ for (uint32_t i = 0; i < aHeaderNames.Length(); i++) {
+ headerName = aHeaderNames[i];
+ ToLowerCase(headerName);
+ headerValue = aHeaderValues[i];
+
+ bool headerProcessed = false;
+ if (mCustomHeaderTokenization) {
+ // Process any exceptions set from preferences
+ for (uint32_t i = 0; i < mEnabledHeaders.Length(); i++)
+ if (headerName.Equals(mEnabledHeaders[i])) {
+ if (mEnabledHeadersDelimiters[i].IsVoid())
+ // tokenize with standard delimiters for all headers
+ addTokenForHeader(headerName.get(), headerValue, true);
+ else if (mEnabledHeadersDelimiters[i].IsEmpty())
+ // do not break the header into tokens
+ addTokenForHeader(headerName.get(), headerValue);
+ else
+ // use the delimiter in mEnabledHeadersDelimiters
+ addTokenForHeader(headerName.get(), headerValue, true,
+ mEnabledHeadersDelimiters[i].get());
+ headerProcessed = true;
+ break; // we found the header, no need to look for more custom values
+ }
+
+ for (uint32_t i = 0; i < mDisabledHeaders.Length(); i++) {
+ if (headerName.Equals(mDisabledHeaders[i])) {
+ headerProcessed = true;
+ break;
+ }
+ }
+
+ if (headerProcessed) continue;
+ }
+
+ switch (headerName.First()) {
+ case 'c':
+ if (headerName.EqualsLiteral("content-type")) {
+ nsresult rv;
+ nsCOMPtr<nsIMIMEHeaderParam> mimehdrpar =
+ do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) break;
+
+ // extract the charset parameter
+ nsCString parameterValue;
+ mimehdrpar->GetParameterInternal(headerValue, "charset", nullptr,
+ nullptr,
+ getter_Copies(parameterValue));
+ addTokenForHeader("charset", parameterValue);
+
+ // create a token containing just the content type
+ mimehdrpar->GetParameterInternal(headerValue, "type", nullptr,
+ nullptr,
+ getter_Copies(parameterValue));
+ if (!parameterValue.Length())
+ mimehdrpar->GetParameterInternal(
+ headerValue, nullptr /* use first unnamed param */, nullptr,
+ nullptr, getter_Copies(parameterValue));
+ addTokenForHeader("content-type/type", parameterValue);
+
+ // XXX: should we add a token for the entire content-type header as
+ // well or just these parts we have extracted?
+ }
+ break;
+ case 'r':
+ if (headerName.EqualsLiteral("received")) {
+ // look for the string "may be forged" in the received headers.
+ // sendmail sometimes adds this hint This does not compile on linux
+ // yet. Need to figure out why. Commenting out for now if
+ // (FindInReadable(FORGED_RECEIVED_HEADER_HINT, headerValue))
+ // addTokenForHeader(headerName.get(), FORGED_RECEIVED_HEADER_HINT);
+ }
+
+ // leave out reply-to
+ break;
+ case 's':
+ if (headerName.EqualsLiteral("subject")) {
+ // we want to tokenize the subject
+ addTokenForHeader(headerName.get(), headerValue, true);
+ }
+
+ // important: leave out sender field. Too strong of an indicator
+ break;
+ case 'x': // (2) X-Mailer / user-agent works best if it is untokenized,
+ // just fold the case and any leading/trailing white space
+ // all headers beginning with x-mozilla are being changed by us, so
+ // ignore
+ if (StringBeginsWith(headerName, "x-mozilla"_ns)) break;
+ // fall through
+ [[fallthrough]];
+ case 'u':
+ addTokenForHeader(headerName.get(), headerValue);
+ break;
+ default:
+ addTokenForHeader(headerName.get(), headerValue);
+ break;
+ } // end switch
+ }
+}
+
+void Tokenizer::tokenize_ascii_word(char* aWord) {
+ // always deal with normalized lower case strings
+ toLowerCase(aWord);
+ uint32_t wordLength = strlen(aWord);
+
+ // if the wordLength is within our accepted token limit, then add it
+ if (wordLength >= kMinLengthForToken && wordLength <= mMaxLengthForToken)
+ add(aWord);
+ else if (wordLength > mMaxLengthForToken) {
+ // don't skip over the word if it looks like an email address,
+ // there is value in adding tokens for addresses
+ nsDependentCString word(aWord,
+ wordLength); // CHEAP, no allocation occurs here...
+
+ // XXX: i think the 40 byte check is just for perf reasons...if the email
+ // address is longer than that then forget about it.
+ const char* atSign = strchr(aWord, '@');
+ if (wordLength < 40 && strchr(aWord, '.') && atSign &&
+ !strchr(atSign + 1, '@')) {
+ uint32_t numBytesToSep = atSign - aWord;
+ if (numBytesToSep <
+ wordLength - 1) // if the @ sign is the last character, it must not
+ // be an email address
+ {
+ // split the john@foo.com into john and foo.com, treat them as separate
+ // tokens
+ nsCString emailNameToken;
+ emailNameToken.AssignLiteral("email name:");
+ emailNameToken.Append(Substring(word, 0, numBytesToSep++));
+ add(emailNameToken.get());
+ nsCString emailAddrToken;
+ emailAddrToken.AssignLiteral("email addr:");
+ emailAddrToken.Append(
+ Substring(word, numBytesToSep, wordLength - numBytesToSep));
+ add(emailAddrToken.get());
+ return;
+ }
+ }
+
+ // there is value in generating a token indicating the number
+ // of characters we are skipping. We'll round to the nearest 10
+ nsCString skipToken;
+ skipToken.AssignLiteral("skip:");
+ skipToken.Append(word[0]);
+ skipToken.Append(' ');
+ skipToken.AppendInt((wordLength / 10) * 10);
+ add(skipToken.get());
+ }
+}
+
+// Copied from mozilla/intl/lwbrk/WordBreaker.cpp
+
+#define ASCII_IS_ALPHA(c) \
+ ((('a' <= (c)) && ((c) <= 'z')) || (('A' <= (c)) && ((c) <= 'Z')))
+#define ASCII_IS_DIGIT(c) (('0' <= (c)) && ((c) <= '9'))
+#define ASCII_IS_SPACE(c) \
+ ((' ' == (c)) || ('\t' == (c)) || ('\r' == (c)) || ('\n' == (c)))
+#define IS_ALPHABETICAL_SCRIPT(c) ((c) < 0x2E80)
+
+// we change the beginning of IS_HAN from 0x4e00 to 0x3400 to relfect
+// Unicode 3.0
+#define IS_HAN(c) \
+ ((0x3400 <= (c)) && ((c) <= 0x9fff)) || ((0xf900 <= (c)) && ((c) <= 0xfaff))
+#define IS_KATAKANA(c) ((0x30A0 <= (c)) && ((c) <= 0x30FF))
+#define IS_HIRAGANA(c) ((0x3040 <= (c)) && ((c) <= 0x309F))
+#define IS_HALFWIDTHKATAKANA(c) ((0xFF60 <= (c)) && ((c) <= 0xFF9F))
+
+// Return true if aChar belongs to a SEAsian script that is written without
+// word spaces, so we need to use the "complex breaker" to find possible word
+// boundaries. (https://en.wikipedia.org/wiki/Scriptio_continua)
+// (How well this works depends on the level of platform support for finding
+// possible line breaks - or possible word boundaries - in the particular
+// script. Thai, at least, works pretty well on the major desktop OSes. If
+// the script is not supported by the platform, we just won't find any useful
+// boundaries.)
+static bool IsScriptioContinua(char16_t aChar) {
+ Script sc = UnicodeProperties::GetScriptCode(aChar);
+ return sc == Script::THAI || sc == Script::MYANMAR || sc == Script::KHMER ||
+ sc == Script::JAVANESE || sc == Script::BALINESE ||
+ sc == Script::SUNDANESE || sc == Script::LAO;
+}
+
+// one subtract and one conditional jump should be faster than two conditional
+// jump on most recent system.
+#define IN_RANGE(x, low, high) ((uint16_t)((x) - (low)) <= (high) - (low))
+
+#define IS_JA_HIRAGANA(x) IN_RANGE(x, 0x3040, 0x309F)
+// swapping the range using xor operation to reduce conditional jump.
+#define IS_JA_KATAKANA(x) \
+ (IN_RANGE(x ^ 0x0004, 0x30A0, 0x30FE) || (IN_RANGE(x, 0xFF66, 0xFF9F)))
+#define IS_JA_KANJI(x) \
+ (IN_RANGE(x, 0x2E80, 0x2FDF) || IN_RANGE(x, 0x4E00, 0x9FAF))
+#define IS_JA_KUTEN(x) (((x) == 0x3001) || ((x) == 0xFF64) || ((x) == 0xFF0E))
+#define IS_JA_TOUTEN(x) (((x) == 0x3002) || ((x) == 0xFF61) || ((x) == 0xFF0C))
+#define IS_JA_SPACE(x) ((x) == 0x3000)
+#define IS_JA_FWLATAIN(x) IN_RANGE(x, 0xFF01, 0xFF5E)
+#define IS_JA_FWNUMERAL(x) IN_RANGE(x, 0xFF10, 0xFF19)
+
+#define IS_JAPANESE_SPECIFIC(x) \
+ (IN_RANGE(x, 0x3040, 0x30FF) || IN_RANGE(x, 0xFF01, 0xFF9F))
+
+enum char_class {
+ others = 0,
+ space,
+ hiragana,
+ katakana,
+ kanji,
+ kuten,
+ touten,
+ kigou,
+ fwlatain,
+ ascii
+};
+
+static char_class getCharClass(char16_t c) {
+ char_class charClass = others;
+
+ if (IS_JA_HIRAGANA(c))
+ charClass = hiragana;
+ else if (IS_JA_KATAKANA(c))
+ charClass = katakana;
+ else if (IS_JA_KANJI(c))
+ charClass = kanji;
+ else if (IS_JA_KUTEN(c))
+ charClass = kuten;
+ else if (IS_JA_TOUTEN(c))
+ charClass = touten;
+ else if (IS_JA_FWLATAIN(c))
+ charClass = fwlatain;
+
+ return charClass;
+}
+
+static bool isJapanese(const char* word) {
+ nsString text = NS_ConvertUTF8toUTF16(word);
+ const char16_t* p = (const char16_t*)text.get();
+ char16_t c;
+
+ // it is japanese chunk if it contains any hiragana or katakana.
+ while ((c = *p++))
+ if (IS_JAPANESE_SPECIFIC(c)) return true;
+
+ return false;
+}
+
+static bool isFWNumeral(const char16_t* p1, const char16_t* p2) {
+ for (; p1 < p2; p1++)
+ if (!IS_JA_FWNUMERAL(*p1)) return false;
+
+ return true;
+}
+
+// The japanese tokenizer was added as part of Bug #277354
+void Tokenizer::tokenize_japanese_word(char* chunk) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("entering tokenize_japanese_word(%s)", chunk));
+
+ nsString srcStr = NS_ConvertUTF8toUTF16(chunk);
+ const char16_t* p1 = srcStr.get();
+ const char16_t* p2 = p1;
+ if (!*p2) return;
+
+ char_class cc = getCharClass(*p2);
+ while (*(++p2)) {
+ if (cc == getCharClass(*p2)) continue;
+
+ nsCString token = NS_ConvertUTF16toUTF8(p1, p2 - p1);
+ if ((!isDecimalNumber(token.get())) && (!isFWNumeral(p1, p2))) {
+ nsCString tmpStr;
+ tmpStr.AppendLiteral("JA:");
+ tmpStr.Append(token);
+ add(tmpStr.get());
+ }
+
+ cc = getCharClass(*p2);
+ p1 = p2;
+ }
+}
+
+nsresult Tokenizer::stripHTML(const nsAString& inString, nsAString& outString) {
+ uint32_t flags = nsIDocumentEncoder::OutputLFLineBreak |
+ nsIDocumentEncoder::OutputNoScriptContent |
+ nsIDocumentEncoder::OutputNoFramesContent |
+ nsIDocumentEncoder::OutputBodyOnly;
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(inString, flags, 80, outString);
+}
+
+// Copied from WorfdBreker.cpp due to changes in bug 1728708.
+enum WordBreakClass : uint8_t {
+ kWbClassSpace = 0,
+ kWbClassAlphaLetter,
+ kWbClassPunct,
+ kWbClassHanLetter,
+ kWbClassKatakanaLetter,
+ kWbClassHiraganaLetter,
+ kWbClassHWKatakanaLetter,
+ kWbClassScriptioContinua
+};
+
+WordBreakClass GetWordBreakClass(char16_t c) {
+ // begin of the hack
+
+ if (IS_ALPHABETICAL_SCRIPT(c)) {
+ if (IS_ASCII(c)) {
+ if (ASCII_IS_SPACE(c)) {
+ return WordBreakClass::kWbClassSpace;
+ }
+ if (ASCII_IS_ALPHA(c) || ASCII_IS_DIGIT(c) || (c == '_')) {
+ return WordBreakClass::kWbClassAlphaLetter;
+ }
+ return WordBreakClass::kWbClassPunct;
+ }
+ if (c == 0x00A0 /*NBSP*/) {
+ return WordBreakClass::kWbClassSpace;
+ }
+ if (mozilla::unicode::GetGenCategory(c) == nsUGenCategory::kPunctuation) {
+ return WordBreakClass::kWbClassPunct;
+ }
+ if (IsScriptioContinua(c)) {
+ return WordBreakClass::kWbClassScriptioContinua;
+ }
+ return WordBreakClass::kWbClassAlphaLetter;
+ }
+ if (IS_HAN(c)) {
+ return WordBreakClass::kWbClassHanLetter;
+ }
+ if (IS_KATAKANA(c)) {
+ return kWbClassKatakanaLetter;
+ }
+ if (IS_HIRAGANA(c)) {
+ return WordBreakClass::kWbClassHiraganaLetter;
+ }
+ if (IS_HALFWIDTHKATAKANA(c)) {
+ return WordBreakClass::kWbClassHWKatakanaLetter;
+ }
+ if (mozilla::unicode::GetGenCategory(c) == nsUGenCategory::kPunctuation) {
+ return WordBreakClass::kWbClassPunct;
+ }
+ if (IsScriptioContinua(c)) {
+ return WordBreakClass::kWbClassScriptioContinua;
+ }
+ return WordBreakClass::kWbClassAlphaLetter;
+}
+
+// Copied from nsSemanticUnitScanner.cpp which was removed in bug 1368418.
+nsresult Tokenizer::ScannerNext(const char16_t* text, int32_t length,
+ int32_t pos, bool isLastBuffer, int32_t* begin,
+ int32_t* end, bool* _retval) {
+ // if we reach the end, just return
+ if (pos >= length) {
+ *begin = pos;
+ *end = pos;
+ *_retval = false;
+ return NS_OK;
+ }
+
+ WordBreakClass char_class = GetWordBreakClass(text[pos]);
+
+ // If we are in Chinese mode, return one Han letter at a time.
+ // We should not do this if we are in Japanese or Korean mode.
+ if (WordBreakClass::kWbClassHanLetter == char_class) {
+ *begin = pos;
+ *end = pos + 1;
+ *_retval = true;
+ return NS_OK;
+ }
+
+ int32_t next;
+ // Find the next "word".
+ next =
+ mozilla::intl::WordBreaker::Next(text, (uint32_t)length, (uint32_t)pos);
+
+ // If we don't have enough text to make decision, return.
+ if (next == NS_WORDBREAKER_NEED_MORE_TEXT) {
+ *begin = pos;
+ *end = isLastBuffer ? length : pos;
+ *_retval = isLastBuffer;
+ return NS_OK;
+ }
+
+ // If what we got is space or punct, look at the next break.
+ if (char_class == WordBreakClass::kWbClassSpace ||
+ char_class == WordBreakClass::kWbClassPunct) {
+ // If the next "word" is not letters,
+ // call itself recursively with the new pos.
+ return ScannerNext(text, length, next, isLastBuffer, begin, end, _retval);
+ }
+
+ // For the rest, return.
+ *begin = pos;
+ *end = next;
+ *_retval = true;
+ return NS_OK;
+}
+
+void Tokenizer::tokenize(const char* aText) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug, ("tokenize: %s", aText));
+
+ // strip out HTML tags before we begin processing
+ // uggh but first we have to blow up our string into UCS2
+ // since that's what the document encoder wants. UTF8/UCS2, I wish we all
+ // spoke the same language here..
+ nsString text = NS_ConvertUTF8toUTF16(aText);
+ nsString strippedUCS2;
+
+ // RSS feeds store their summary information as an iframe. But due to
+ // bug 365953, we can't see those in the plaintext serializer. As a
+ // workaround, allow an option to replace iframe with div in the message
+ // text. We disable by default, since most people won't be applying bayes
+ // to RSS
+
+ if (mIframeToDiv) {
+ text.ReplaceSubstring(u"<iframe"_ns, u"<div"_ns);
+ text.ReplaceSubstring(u"/iframe>"_ns, u"/div>"_ns);
+ }
+
+ stripHTML(text, strippedUCS2);
+
+ // convert 0x3000(full width space) into 0x0020
+ char16_t* substr_start = strippedUCS2.BeginWriting();
+ char16_t* substr_end = strippedUCS2.EndWriting();
+ while (substr_start != substr_end) {
+ if (*substr_start == 0x3000) *substr_start = 0x0020;
+ ++substr_start;
+ }
+
+ nsCString strippedStr = NS_ConvertUTF16toUTF8(strippedUCS2);
+ char* strippedText = strippedStr.BeginWriting();
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("tokenize stripped html: %s", strippedText));
+
+ char* word;
+ char* next = strippedText;
+ while ((word = NS_strtok(mBodyDelimiters.get(), &next)) != NULL) {
+ if (!*word) continue;
+ if (isDecimalNumber(word)) continue;
+ if (isASCII(word))
+ tokenize_ascii_word(word);
+ else if (isJapanese(word))
+ tokenize_japanese_word(word);
+ else {
+ nsresult rv;
+ // Convert this word from UTF-8 into UCS2.
+ NS_ConvertUTF8toUTF16 uword(word);
+ ToLowerCase(uword);
+ const char16_t* utext = uword.get();
+ int32_t len = uword.Length(), pos = 0, begin, end;
+ bool gotUnit;
+ while (pos < len) {
+ rv = ScannerNext(utext, len, pos, true, &begin, &end, &gotUnit);
+ if (NS_SUCCEEDED(rv) && gotUnit) {
+ NS_ConvertUTF16toUTF8 utfUnit(utext + begin, end - begin);
+ add(utfUnit.get());
+ // Advance to end of current unit.
+ pos = end;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+}
+
+// helper function to un-escape \n, \t, etc from a CString
+void Tokenizer::UnescapeCString(nsCString& aCString) {
+ nsAutoCString result;
+
+ const char* readEnd = aCString.EndReading();
+ result.SetLength(aCString.Length());
+ char* writeStart = result.BeginWriting();
+ char* writeIter = writeStart;
+
+ bool inEscape = false;
+ for (const char* readIter = aCString.BeginReading(); readIter != readEnd;
+ readIter++) {
+ if (!inEscape) {
+ if (*readIter == '\\')
+ inEscape = true;
+ else
+ *(writeIter++) = *readIter;
+ } else {
+ inEscape = false;
+ switch (*readIter) {
+ case '\\':
+ *(writeIter++) = '\\';
+ break;
+ case 't':
+ *(writeIter++) = '\t';
+ break;
+ case 'n':
+ *(writeIter++) = '\n';
+ break;
+ case 'v':
+ *(writeIter++) = '\v';
+ break;
+ case 'f':
+ *(writeIter++) = '\f';
+ break;
+ case 'r':
+ *(writeIter++) = '\r';
+ break;
+ default:
+ // all other escapes are ignored
+ break;
+ }
+ }
+ }
+ result.Truncate(writeIter - writeStart);
+ aCString.Assign(result);
+}
+
+Token* Tokenizer::copyTokens() {
+ uint32_t count = countTokens();
+ if (count > 0) {
+ Token* tokens = new Token[count];
+ if (tokens) {
+ Token* tp = tokens;
+ TokenEnumeration e(&mTokenTable);
+ while (e.hasMoreTokens()) {
+ Token* src = static_cast<Token*>(e.nextToken());
+ tp->clone(*src);
+ ++tp;
+ }
+ }
+ return tokens;
+ }
+ return NULL;
+}
+
+class TokenAnalyzer {
+ public:
+ virtual ~TokenAnalyzer() {}
+
+ virtual void analyzeTokens(Tokenizer& tokenizer) = 0;
+ void setTokenListener(nsIStreamListener* aTokenListener) {
+ mTokenListener = aTokenListener;
+ }
+
+ void setSource(const nsACString& sourceURI) { mTokenSource = sourceURI; }
+
+ nsCOMPtr<nsIStreamListener> mTokenListener;
+ nsCString mTokenSource;
+};
+
+/**
+ * This class downloads the raw content of an email message, buffering until
+ * complete segments are seen, that is until a linefeed is seen, although
+ * any of the valid token separators would do. This could be a further
+ * refinement.
+ */
+class TokenStreamListener : public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ explicit TokenStreamListener(TokenAnalyzer* analyzer);
+
+ protected:
+ virtual ~TokenStreamListener();
+ TokenAnalyzer* mAnalyzer;
+ char* mBuffer;
+ uint32_t mBufferSize;
+ uint32_t mLeftOverCount;
+ Tokenizer mTokenizer;
+ bool mSetAttachmentFlag;
+};
+
+const uint32_t kBufferSize = 16384;
+
+TokenStreamListener::TokenStreamListener(TokenAnalyzer* analyzer)
+ : mAnalyzer(analyzer),
+ mBuffer(NULL),
+ mBufferSize(kBufferSize),
+ mLeftOverCount(0),
+ mSetAttachmentFlag(false) {}
+
+TokenStreamListener::~TokenStreamListener() {
+ delete[] mBuffer;
+ delete mAnalyzer;
+}
+
+NS_IMPL_ISUPPORTS(TokenStreamListener, nsIRequestObserver, nsIStreamListener)
+
+/* void onStartRequest (in nsIRequest aRequest); */
+NS_IMETHODIMP TokenStreamListener::OnStartRequest(nsIRequest* aRequest) {
+ mLeftOverCount = 0;
+ if (!mBuffer) {
+ mBuffer = new char[mBufferSize];
+ NS_ENSURE_TRUE(mBuffer, NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ return NS_OK;
+}
+
+/* void onDataAvailable (in nsIRequest aRequest, in nsIInputStream aInputStream,
+ * in unsigned long long aOffset, in unsigned long aCount); */
+NS_IMETHODIMP TokenStreamListener::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset,
+ uint32_t aCount) {
+ nsresult rv = NS_OK;
+
+ while (aCount > 0) {
+ uint32_t readCount, totalCount = (aCount + mLeftOverCount);
+ if (totalCount >= mBufferSize) {
+ readCount = mBufferSize - mLeftOverCount - 1;
+ } else {
+ readCount = aCount;
+ }
+
+ // mBuffer is supposed to be allocated in onStartRequest. But something
+ // is causing that to not happen, so as a last-ditch attempt we'll
+ // do it here.
+ if (!mBuffer) {
+ mBuffer = new char[mBufferSize];
+ NS_ENSURE_TRUE(mBuffer, NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ char* buffer = mBuffer;
+ rv = aInputStream->Read(buffer + mLeftOverCount, readCount, &readCount);
+ if (NS_FAILED(rv)) break;
+
+ if (readCount == 0) {
+ rv = NS_ERROR_UNEXPECTED;
+ NS_WARNING("failed to tokenize");
+ break;
+ }
+
+ aCount -= readCount;
+
+ /* consume the tokens up to the last legal token delimiter in the buffer. */
+ totalCount = (readCount + mLeftOverCount);
+ buffer[totalCount] = '\0';
+ char* lastDelimiter = NULL;
+ char* scan = buffer + totalCount;
+ while (scan > buffer) {
+ if (strchr(mTokenizer.mBodyDelimiters.get(), *--scan)) {
+ lastDelimiter = scan;
+ break;
+ }
+ }
+
+ if (lastDelimiter) {
+ *lastDelimiter = '\0';
+ mTokenizer.tokenize(buffer);
+
+ uint32_t consumedCount = 1 + (lastDelimiter - buffer);
+ mLeftOverCount = totalCount - consumedCount;
+ if (mLeftOverCount)
+ memmove(buffer, buffer + consumedCount, mLeftOverCount);
+ } else {
+ /* didn't find a delimiter, keep the whole buffer around. */
+ mLeftOverCount = totalCount;
+ if (totalCount >= (mBufferSize / 2)) {
+ uint32_t newBufferSize = mBufferSize * 2;
+ char* newBuffer = new char[newBufferSize];
+ NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
+ memcpy(newBuffer, mBuffer, mLeftOverCount);
+ delete[] mBuffer;
+ mBuffer = newBuffer;
+ mBufferSize = newBufferSize;
+ }
+ }
+ }
+
+ return rv;
+}
+
+/* void onStopRequest (in nsIRequest aRequest, in nsresult aStatusCode); */
+NS_IMETHODIMP TokenStreamListener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(aRequest);
+ if (mailChannel) {
+ nsTArray<nsCString> headerNames;
+ nsTArray<nsCString> headerValues;
+ mailChannel->GetHeaderNames(headerNames);
+ mailChannel->GetHeaderValues(headerValues);
+ mTokenizer.tokenizeHeaders(headerNames, headerValues);
+
+ nsTArray<RefPtr<nsIPropertyBag2>> attachments;
+ mailChannel->GetAttachments(attachments);
+ mTokenizer.tokenizeAttachments(attachments);
+ }
+
+ if (mLeftOverCount) {
+ /* assume final buffer is complete. */
+ mBuffer[mLeftOverCount] = '\0';
+ mTokenizer.tokenize(mBuffer);
+ }
+
+ /* finally, analyze the tokenized message. */
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("analyze the tokenized message"));
+ if (mAnalyzer) mAnalyzer->analyzeTokens(mTokenizer);
+
+ return NS_OK;
+}
+
+/* Implementation file */
+
+NS_IMPL_ISUPPORTS(nsBayesianFilter, nsIMsgFilterPlugin, nsIJunkMailPlugin,
+ nsIMsgCorpus, nsISupportsWeakReference, nsIObserver)
+
+nsBayesianFilter::nsBayesianFilter() : mTrainingDataDirty(false) {
+ int32_t junkThreshold = 0;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (pPrefBranch)
+ pPrefBranch->GetIntPref("mail.adaptivefilters.junk_threshold",
+ &junkThreshold);
+
+ mJunkProbabilityThreshold = (static_cast<double>(junkThreshold)) / 100.0;
+ if (mJunkProbabilityThreshold == 0 || mJunkProbabilityThreshold >= 1)
+ mJunkProbabilityThreshold = kDefaultJunkThreshold;
+
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Warning,
+ ("junk probability threshold: %f", mJunkProbabilityThreshold));
+
+ mCorpus.readTrainingData();
+
+ // get parameters for training data flushing, from the prefs
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed accessing preferences service");
+ rv = prefs->GetBranch(nullptr, getter_AddRefs(prefBranch));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed getting preferences branch");
+
+ rv = prefBranch->GetIntPref(
+ "mailnews.bayesian_spam_filter.flush.minimum_interval",
+ &mMinFlushInterval);
+ // it is not a good idea to allow a minimum interval of under 1 second
+ if (NS_FAILED(rv) || (mMinFlushInterval <= 1000))
+ mMinFlushInterval = DEFAULT_MIN_INTERVAL_BETWEEN_WRITES;
+
+ rv = prefBranch->GetIntPref("mailnews.bayesian_spam_filter.junk_maxtokens",
+ &mMaximumTokenCount);
+ if (NS_FAILED(rv))
+ mMaximumTokenCount = 0; // which means do not limit token counts
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Warning,
+ ("maximum junk tokens: %d", mMaximumTokenCount));
+
+ // give a default capacity to the memory structure used to store
+ // per-message/per-trait token data
+ mAnalysisStore.SetCapacity(kAnalysisStoreCapacity);
+
+ // dummy 0th element. Index 0 means "end of list" so we need to
+ // start from 1
+ AnalysisPerToken analysisPT(0, 0.0, 0.0);
+ mAnalysisStore.AppendElement(analysisPT);
+ mNextAnalysisIndex = 1;
+}
+
+nsresult nsBayesianFilter::Init() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService)
+ observerService->AddObserver(this, "profile-before-change", true);
+ return NS_OK;
+}
+
+void nsBayesianFilter::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ // we will flush the training data to disk after enough time has passed
+ // since the first time a message has been classified after the last flush
+
+ nsBayesianFilter* filter = static_cast<nsBayesianFilter*>(aClosure);
+ filter->mCorpus.writeTrainingData(filter->mMaximumTokenCount);
+ filter->mTrainingDataDirty = false;
+}
+
+nsBayesianFilter::~nsBayesianFilter() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ // call shutdown when we are going away in case we need
+ // to flush the training set to disk
+ Shutdown();
+}
+
+// this object is used for one call to classifyMessage or classifyMessages().
+// So if we're classifying multiple messages, this object will be used for each
+// message. It's going to hold a reference to itself, basically, to stay in
+// memory.
+class MessageClassifier : public TokenAnalyzer {
+ public:
+ // full classifier with arbitrary traits
+ MessageClassifier(nsBayesianFilter* aFilter,
+ nsIJunkMailClassificationListener* aJunkListener,
+ nsIMsgTraitClassificationListener* aTraitListener,
+ nsIMsgTraitDetailListener* aDetailListener,
+ const nsTArray<uint32_t>& aProTraits,
+ const nsTArray<uint32_t>& aAntiTraits,
+ nsIMsgWindow* aMsgWindow,
+ const nsTArray<nsCString>& aMessageURIs)
+ : mFilter(aFilter),
+ mJunkMailPlugin(aFilter),
+ mJunkListener(aJunkListener),
+ mTraitListener(aTraitListener),
+ mDetailListener(aDetailListener),
+ mProTraits(aProTraits.Clone()),
+ mAntiTraits(aAntiTraits.Clone()),
+ mMsgWindow(aMsgWindow),
+ mMessageURIs(aMessageURIs.Clone()),
+ mCurMessageToClassify(0) {
+ MOZ_ASSERT(aProTraits.Length() == aAntiTraits.Length());
+ }
+
+ // junk-only classifier
+ MessageClassifier(nsBayesianFilter* aFilter,
+ nsIJunkMailClassificationListener* aJunkListener,
+ nsIMsgWindow* aMsgWindow,
+ const nsTArray<nsCString>& aMessageURIs)
+ : mFilter(aFilter),
+ mJunkMailPlugin(aFilter),
+ mJunkListener(aJunkListener),
+ mTraitListener(nullptr),
+ mDetailListener(nullptr),
+ mMsgWindow(aMsgWindow),
+ mMessageURIs(aMessageURIs.Clone()),
+ mCurMessageToClassify(0) {
+ mProTraits.AppendElement(kJunkTrait);
+ mAntiTraits.AppendElement(kGoodTrait);
+ }
+
+ virtual ~MessageClassifier() {}
+ virtual void analyzeTokens(Tokenizer& tokenizer) {
+ mFilter->classifyMessage(tokenizer, mTokenSource, mProTraits, mAntiTraits,
+ mJunkListener, mTraitListener, mDetailListener);
+ tokenizer.clearTokens();
+ classifyNextMessage();
+ }
+
+ virtual void classifyNextMessage() {
+ if (++mCurMessageToClassify < mMessageURIs.Length()) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Warning,
+ ("classifyNextMessage(%s)",
+ mMessageURIs[mCurMessageToClassify].get()));
+ mFilter->tokenizeMessage(mMessageURIs[mCurMessageToClassify], mMsgWindow,
+ this);
+ } else {
+ // call all listeners with null parameters to signify end of batch
+ if (mJunkListener)
+ mJunkListener->OnMessageClassified(EmptyCString(),
+ nsIJunkMailPlugin::UNCLASSIFIED, 0);
+ if (mTraitListener) {
+ nsTArray<uint32_t> nullTraits;
+ nsTArray<uint32_t> nullPercents;
+ mTraitListener->OnMessageTraitsClassified(EmptyCString(), nullTraits,
+ nullPercents);
+ }
+ mTokenListener =
+ nullptr; // this breaks the circular ref that keeps this object alive
+ // so we will be destroyed as a result.
+ }
+ }
+
+ private:
+ nsBayesianFilter* mFilter;
+ nsCOMPtr<nsIJunkMailPlugin> mJunkMailPlugin;
+ nsCOMPtr<nsIJunkMailClassificationListener> mJunkListener;
+ nsCOMPtr<nsIMsgTraitClassificationListener> mTraitListener;
+ nsCOMPtr<nsIMsgTraitDetailListener> mDetailListener;
+ nsTArray<uint32_t> mProTraits;
+ nsTArray<uint32_t> mAntiTraits;
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ nsTArray<nsCString> mMessageURIs;
+ uint32_t mCurMessageToClassify; // 0-based index
+};
+
+nsresult nsBayesianFilter::tokenizeMessage(const nsACString& aMessageURI,
+ nsIMsgWindow* aMsgWindow,
+ TokenAnalyzer* aAnalyzer) {
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsresult rv =
+ GetMessageServiceFromURI(aMessageURI, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aAnalyzer->setSource(aMessageURI);
+ nsCOMPtr<nsIURI> dummyNull;
+ return msgService->StreamMessage(
+ aMessageURI, aAnalyzer->mTokenListener, aMsgWindow, nullptr,
+ true /* convert data */, "filter"_ns, false, getter_AddRefs(dummyNull));
+}
+
+// a TraitAnalysis is the per-token representation of the statistical
+// calculations, basically created to group information that is then
+// sorted by mDistance
+struct TraitAnalysis {
+ uint32_t mTokenIndex;
+ double mDistance;
+ double mProbability;
+};
+
+// comparator required to sort an nsTArray
+class compareTraitAnalysis {
+ public:
+ bool Equals(const TraitAnalysis& a, const TraitAnalysis& b) const {
+ return a.mDistance == b.mDistance;
+ }
+ bool LessThan(const TraitAnalysis& a, const TraitAnalysis& b) const {
+ return a.mDistance < b.mDistance;
+ }
+};
+
+inline double dmax(double x, double y) { return (x > y ? x : y); }
+inline double dmin(double x, double y) { return (x < y ? x : y); }
+
+// Chi square functions are implemented by an incomplete gamma function.
+// Note that chi2P's callers multiply the arguments by 2 but chi2P
+// divides them by 2 again. Inlining chi2P gives the compiler a
+// chance to notice this.
+
+// Both chi2P and nsIncompleteGammaP set *error negative on domain
+// errors and nsIncompleteGammaP sets it posivive on internal errors.
+// This may be useful but the chi2P callers treat any error as fatal.
+
+// Note that converting unsigned ints to floating point can be slow on
+// some platforms (like Intel) so use signed quantities for the numeric
+// routines.
+static inline double chi2P(double chi2, double nu, int32_t* error) {
+ // domain checks; set error and return a dummy value
+ if (chi2 < 0.0 || nu <= 0.0) {
+ *error = -1;
+ return 0.0;
+ }
+ // reversing the arguments is intentional
+ return nsIncompleteGammaP(nu / 2.0, chi2 / 2.0, error);
+}
+
+void nsBayesianFilter::classifyMessage(
+ Tokenizer& tokenizer, const nsACString& messageURI,
+ nsTArray<uint32_t>& aProTraits, nsTArray<uint32_t>& aAntiTraits,
+ nsIJunkMailClassificationListener* listener,
+ nsIMsgTraitClassificationListener* aTraitListener,
+ nsIMsgTraitDetailListener* aDetailListener) {
+ if (aProTraits.Length() != aAntiTraits.Length()) {
+ NS_ERROR("Each Pro trait needs a matching Anti trait");
+ return;
+ }
+ Token* tokens = tokenizer.copyTokens();
+ uint32_t tokenCount;
+ if (!tokens) {
+ // This can happen with problems with UTF conversion
+ NS_ERROR("Trying to classify a null or invalid message");
+ tokenCount = 0;
+ // don't return so that we still call the listeners
+ } else {
+ tokenCount = tokenizer.countTokens();
+ }
+
+ /* this part is similar to the Graham algorithm with some adjustments. */
+ uint32_t traitCount = aProTraits.Length();
+
+ // pro message counts per trait index
+ AutoTArray<uint32_t, kTraitAutoCapacity> numProMessages;
+ // anti message counts per trait index
+ AutoTArray<uint32_t, kTraitAutoCapacity> numAntiMessages;
+ // array of pro aliases per trait index
+ AutoTArray<nsTArray<uint32_t>, kTraitAutoCapacity> proAliasArrays;
+ // array of anti aliases per trait index
+ AutoTArray<nsTArray<uint32_t>, kTraitAutoCapacity> antiAliasArrays;
+ // construct the outgoing listener arrays
+ AutoTArray<uint32_t, kTraitAutoCapacity> traits;
+ AutoTArray<uint32_t, kTraitAutoCapacity> percents;
+ if (traitCount > kTraitAutoCapacity) {
+ traits.SetCapacity(traitCount);
+ percents.SetCapacity(traitCount);
+ numProMessages.SetCapacity(traitCount);
+ numAntiMessages.SetCapacity(traitCount);
+ proAliasArrays.SetCapacity(traitCount);
+ antiAliasArrays.SetCapacity(traitCount);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgTraitService> traitService(
+ do_GetService("@mozilla.org/msg-trait-service;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Failed to get trait service");
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("Failed to get trait service"));
+ }
+
+ // get aliases and message counts for the pro and anti traits
+ for (uint32_t traitIndex = 0; traitIndex < traitCount; traitIndex++) {
+ nsresult rv;
+
+ // pro trait
+ nsTArray<uint32_t> proAliases;
+ uint32_t proTrait = aProTraits[traitIndex];
+ if (traitService) {
+ rv = traitService->GetAliases(proTrait, proAliases);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("trait service failed to get aliases");
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("trait service failed to get aliases"));
+ }
+ }
+ proAliasArrays.AppendElement(proAliases.Clone());
+ uint32_t proMessageCount = mCorpus.getMessageCount(proTrait);
+ for (uint32_t aliasIndex = 0; aliasIndex < proAliases.Length();
+ aliasIndex++)
+ proMessageCount += mCorpus.getMessageCount(proAliases[aliasIndex]);
+ numProMessages.AppendElement(proMessageCount);
+
+ // anti trait
+ nsTArray<uint32_t> antiAliases;
+ uint32_t antiTrait = aAntiTraits[traitIndex];
+ if (traitService) {
+ rv = traitService->GetAliases(antiTrait, antiAliases);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("trait service failed to get aliases");
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("trait service failed to get aliases"));
+ }
+ }
+ antiAliasArrays.AppendElement(antiAliases.Clone());
+ uint32_t antiMessageCount = mCorpus.getMessageCount(antiTrait);
+ for (uint32_t aliasIndex = 0; aliasIndex < antiAliases.Length();
+ aliasIndex++)
+ antiMessageCount += mCorpus.getMessageCount(antiAliases[aliasIndex]);
+ numAntiMessages.AppendElement(antiMessageCount);
+ }
+
+ for (uint32_t i = 0; i < tokenCount; ++i) {
+ Token& token = tokens[i];
+ CorpusToken* t = mCorpus.get(token.mWord);
+ if (!t) continue;
+ for (uint32_t traitIndex = 0; traitIndex < traitCount; traitIndex++) {
+ uint32_t iProCount = mCorpus.getTraitCount(t, aProTraits[traitIndex]);
+ // add in any counts for aliases to proTrait
+ for (uint32_t aliasIndex = 0;
+ aliasIndex < proAliasArrays[traitIndex].Length(); aliasIndex++)
+ iProCount +=
+ mCorpus.getTraitCount(t, proAliasArrays[traitIndex][aliasIndex]);
+ double proCount = static_cast<double>(iProCount);
+
+ uint32_t iAntiCount = mCorpus.getTraitCount(t, aAntiTraits[traitIndex]);
+ // add in any counts for aliases to antiTrait
+ for (uint32_t aliasIndex = 0;
+ aliasIndex < antiAliasArrays[traitIndex].Length(); aliasIndex++)
+ iAntiCount +=
+ mCorpus.getTraitCount(t, antiAliasArrays[traitIndex][aliasIndex]);
+ double antiCount = static_cast<double>(iAntiCount);
+
+ double prob, denom;
+ // Prevent a divide by zero error by setting defaults for prob
+
+ // If there are no matching tokens at all, ignore.
+ if (antiCount == 0.0 && proCount == 0.0) continue;
+ // if only anti match, set probability to 0%
+ if (proCount == 0.0) prob = 0.0;
+ // if only pro match, set probability to 100%
+ else if (antiCount == 0.0)
+ prob = 1.0;
+ // not really needed, but just to be sure check the denom as well
+ else if ((denom = proCount * numAntiMessages[traitIndex] +
+ antiCount * numProMessages[traitIndex]) == 0.0)
+ continue;
+ else
+ prob = (proCount * numAntiMessages[traitIndex]) / denom;
+
+ double n = proCount + antiCount;
+ prob = (0.225 + n * prob) / (.45 + n);
+ double distance = std::abs(prob - 0.5);
+ if (distance >= .1) {
+ mozilla::DebugOnly<nsresult> rv =
+ setAnalysis(token, traitIndex, distance, prob);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Problem in setAnalysis");
+ }
+ }
+ }
+
+ for (uint32_t traitIndex = 0; traitIndex < traitCount; traitIndex++) {
+ AutoTArray<TraitAnalysis, 1024> traitAnalyses;
+ // copy valid tokens into an array to sort
+ for (uint32_t tokenIndex = 0; tokenIndex < tokenCount; tokenIndex++) {
+ uint32_t storeIndex = getAnalysisIndex(tokens[tokenIndex], traitIndex);
+ if (storeIndex) {
+ TraitAnalysis ta = {tokenIndex, mAnalysisStore[storeIndex].mDistance,
+ mAnalysisStore[storeIndex].mProbability};
+ traitAnalyses.AppendElement(ta);
+ }
+ }
+
+ // sort the array by the distances
+ traitAnalyses.Sort(compareTraitAnalysis());
+ uint32_t count = traitAnalyses.Length();
+ uint32_t first, last = count;
+ const uint32_t kMaxTokens = 150;
+ first = (count > kMaxTokens) ? count - kMaxTokens : 0;
+
+ // Setup the arrays to save details if needed
+ nsTArray<double> sArray;
+ nsTArray<double> hArray;
+ uint32_t usedTokenCount = (count > kMaxTokens) ? kMaxTokens : count;
+ if (aDetailListener) {
+ sArray.SetCapacity(usedTokenCount);
+ hArray.SetCapacity(usedTokenCount);
+ }
+
+ double H = 1.0, S = 1.0;
+ int32_t Hexp = 0, Sexp = 0;
+ uint32_t goodclues = 0;
+ int e;
+
+ // index from end to analyze most significant first
+ for (uint32_t ip1 = last; ip1 != first; --ip1) {
+ TraitAnalysis& ta = traitAnalyses[ip1 - 1];
+ if (ta.mDistance > 0.0) {
+ goodclues++;
+ double value = ta.mProbability;
+ S *= (1.0 - value);
+ H *= value;
+ if (S < 1e-200) {
+ S = frexp(S, &e);
+ Sexp += e;
+ }
+ if (H < 1e-200) {
+ H = frexp(H, &e);
+ Hexp += e;
+ }
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Warning,
+ ("token probability (%s) is %f", tokens[ta.mTokenIndex].mWord,
+ ta.mProbability));
+ }
+ if (aDetailListener) {
+ sArray.AppendElement(log(S) + Sexp * M_LN2);
+ hArray.AppendElement(log(H) + Hexp * M_LN2);
+ }
+ }
+
+ S = log(S) + Sexp * M_LN2;
+ H = log(H) + Hexp * M_LN2;
+
+ double prob;
+ if (goodclues > 0) {
+ int32_t chi_error;
+ S = chi2P(-2.0 * S, 2.0 * goodclues, &chi_error);
+ if (!chi_error) H = chi2P(-2.0 * H, 2.0 * goodclues, &chi_error);
+ // if any error toss the entire calculation
+ if (!chi_error)
+ prob = (S - H + 1.0) / 2.0;
+ else
+ prob = 0.5;
+ } else
+ prob = 0.5;
+
+ if (aDetailListener) {
+ // Prepare output arrays
+ nsTArray<uint32_t> tokenPercents(usedTokenCount);
+ nsTArray<uint32_t> runningPercents(usedTokenCount);
+ nsTArray<nsString> tokenStrings(usedTokenCount);
+
+ double clueCount = 1.0;
+ for (uint32_t tokenIndex = 0; tokenIndex < usedTokenCount; tokenIndex++) {
+ TraitAnalysis& ta = traitAnalyses[last - 1 - tokenIndex];
+ int32_t chi_error;
+ S = chi2P(-2.0 * sArray[tokenIndex], 2.0 * clueCount, &chi_error);
+ if (!chi_error)
+ H = chi2P(-2.0 * hArray[tokenIndex], 2.0 * clueCount, &chi_error);
+ clueCount += 1.0;
+ double runningProb;
+ if (!chi_error)
+ runningProb = (S - H + 1.0) / 2.0;
+ else
+ runningProb = 0.5;
+ runningPercents.AppendElement(
+ static_cast<uint32_t>(runningProb * 100. + .5));
+ tokenPercents.AppendElement(
+ static_cast<uint32_t>(ta.mProbability * 100. + .5));
+ tokenStrings.AppendElement(
+ NS_ConvertUTF8toUTF16(tokens[ta.mTokenIndex].mWord));
+ }
+
+ aDetailListener->OnMessageTraitDetails(messageURI, aProTraits[traitIndex],
+ tokenStrings, tokenPercents,
+ runningPercents);
+ }
+
+ uint32_t proPercent = static_cast<uint32_t>(prob * 100. + .5);
+
+ // directly classify junk to maintain backwards compatibility
+ if (aProTraits[traitIndex] == kJunkTrait) {
+ bool isJunk = (prob >= mJunkProbabilityThreshold);
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Info,
+ ("%s is junk probability = (%f) HAM SCORE:%f SPAM SCORE:%f",
+ PromiseFlatCString(messageURI).get(), prob, H, S));
+
+ // the algorithm in "A Plan For Spam" assumes that you have a large good
+ // corpus and a large junk corpus.
+ // that won't be the case with users who first use the junk mail trait
+ // so, we do certain things to encourage them to train.
+ //
+ // if there are no good tokens, assume the message is junk
+ // this will "encourage" the user to train
+ // and if there are no bad tokens, assume the message is not junk
+ // this will also "encourage" the user to train
+ // see bug #194238
+
+ if (listener && !mCorpus.getMessageCount(kGoodTrait))
+ isJunk = true;
+ else if (listener && !mCorpus.getMessageCount(kJunkTrait))
+ isJunk = false;
+
+ if (listener)
+ listener->OnMessageClassified(
+ messageURI,
+ isJunk ? nsMsgJunkStatus(nsIJunkMailPlugin::JUNK)
+ : nsMsgJunkStatus(nsIJunkMailPlugin::GOOD),
+ proPercent);
+ }
+
+ if (aTraitListener) {
+ traits.AppendElement(aProTraits[traitIndex]);
+ percents.AppendElement(proPercent);
+ }
+ }
+
+ if (aTraitListener)
+ aTraitListener->OnMessageTraitsClassified(messageURI, traits, percents);
+
+ delete[] tokens;
+ // reuse mAnalysisStore without clearing memory
+ mNextAnalysisIndex = 1;
+ // but shrink it back to the default size
+ if (mAnalysisStore.Length() > kAnalysisStoreCapacity)
+ mAnalysisStore.RemoveElementsAt(
+ kAnalysisStoreCapacity,
+ mAnalysisStore.Length() - kAnalysisStoreCapacity);
+ mAnalysisStore.Compact();
+}
+
+void nsBayesianFilter::classifyMessage(
+ Tokenizer& tokens, const nsACString& messageURI,
+ nsIJunkMailClassificationListener* aJunkListener) {
+ AutoTArray<uint32_t, 1> proTraits;
+ AutoTArray<uint32_t, 1> antiTraits;
+ proTraits.AppendElement(kJunkTrait);
+ antiTraits.AppendElement(kGoodTrait);
+ classifyMessage(tokens, messageURI, proTraits, antiTraits, aJunkListener,
+ nullptr, nullptr);
+}
+
+NS_IMETHODIMP
+nsBayesianFilter::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "profile-before-change")) Shutdown();
+ return NS_OK;
+}
+
+/* void shutdown (); */
+NS_IMETHODIMP nsBayesianFilter::Shutdown() {
+ if (mTrainingDataDirty) mCorpus.writeTrainingData(mMaximumTokenCount);
+ mTrainingDataDirty = false;
+
+ return NS_OK;
+}
+
+/* readonly attribute boolean shouldDownloadAllHeaders; */
+NS_IMETHODIMP nsBayesianFilter::GetShouldDownloadAllHeaders(
+ bool* aShouldDownloadAllHeaders) {
+ // bayesian filters work on the whole msg body currently.
+ *aShouldDownloadAllHeaders = false;
+ return NS_OK;
+}
+
+/* void classifyMessage (in string aMsgURL, in nsIJunkMailClassificationListener
+ * aListener); */
+NS_IMETHODIMP nsBayesianFilter::ClassifyMessage(
+ const nsACString& aMessageURL, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aListener) {
+ AutoTArray<nsCString, 1> urls = {PromiseFlatCString(aMessageURL)};
+ MessageClassifier* analyzer =
+ new MessageClassifier(this, aListener, aMsgWindow, urls);
+ NS_ENSURE_TRUE(analyzer, NS_ERROR_OUT_OF_MEMORY);
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+ NS_ENSURE_TRUE(tokenListener, NS_ERROR_OUT_OF_MEMORY);
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMessageURL, aMsgWindow, analyzer);
+}
+
+/* void classifyMessages(in Array<ACString> aMsgURIs,
+ * in nsIMsgWindow aMsgWindow,
+ * in nsIJunkMailClassificationListener aListener); */
+NS_IMETHODIMP nsBayesianFilter::ClassifyMessages(
+ const nsTArray<nsCString>& aMsgURLs, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aListener) {
+ TokenAnalyzer* analyzer =
+ new MessageClassifier(this, aListener, aMsgWindow, aMsgURLs);
+ NS_ENSURE_TRUE(analyzer, NS_ERROR_OUT_OF_MEMORY);
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+ NS_ENSURE_TRUE(tokenListener, NS_ERROR_OUT_OF_MEMORY);
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMsgURLs[0], aMsgWindow, analyzer);
+}
+
+nsresult nsBayesianFilter::setAnalysis(Token& token, uint32_t aTraitIndex,
+ double aDistance, double aProbability) {
+ uint32_t nextLink = token.mAnalysisLink;
+ uint32_t lastLink = 0;
+ uint32_t linkCount = 0, maxLinks = 100;
+
+ // try to find an existing element. Limit the search to maxLinks
+ // as a precaution
+ for (linkCount = 0; nextLink && linkCount < maxLinks; linkCount++) {
+ AnalysisPerToken& rAnalysis = mAnalysisStore[nextLink];
+ if (rAnalysis.mTraitIndex == aTraitIndex) {
+ rAnalysis.mDistance = aDistance;
+ rAnalysis.mProbability = aProbability;
+ return NS_OK;
+ }
+ lastLink = nextLink;
+ nextLink = rAnalysis.mNextLink;
+ }
+ if (linkCount >= maxLinks) return NS_ERROR_FAILURE;
+
+ // trait does not exist, so add it
+
+ AnalysisPerToken analysis(aTraitIndex, aDistance, aProbability);
+ if (mAnalysisStore.Length() == mNextAnalysisIndex)
+ mAnalysisStore.InsertElementAt(mNextAnalysisIndex, analysis);
+ else if (mAnalysisStore.Length() > mNextAnalysisIndex)
+ mAnalysisStore.ReplaceElementsAt(mNextAnalysisIndex, 1, analysis);
+ else // we can only insert at the end of the array
+ return NS_ERROR_FAILURE;
+
+ if (lastLink)
+ // the token had at least one link, so update the last link to point to
+ // the new item
+ mAnalysisStore[lastLink].mNextLink = mNextAnalysisIndex;
+ else
+ // need to update the token's first link
+ token.mAnalysisLink = mNextAnalysisIndex;
+ mNextAnalysisIndex++;
+ return NS_OK;
+}
+
+uint32_t nsBayesianFilter::getAnalysisIndex(Token& token,
+ uint32_t aTraitIndex) {
+ uint32_t nextLink;
+ uint32_t linkCount = 0, maxLinks = 100;
+ for (nextLink = token.mAnalysisLink; nextLink && linkCount < maxLinks;
+ linkCount++) {
+ AnalysisPerToken& rAnalysis = mAnalysisStore[nextLink];
+ if (rAnalysis.mTraitIndex == aTraitIndex) return nextLink;
+ nextLink = rAnalysis.mNextLink;
+ }
+ NS_ASSERTION(linkCount < maxLinks, "corrupt analysis store");
+
+ // Trait not found, indicate by zero
+ return 0;
+}
+
+NS_IMETHODIMP nsBayesianFilter::ClassifyTraitsInMessage(
+ const nsACString& aMsgURI, const nsTArray<uint32_t>& aProTraits,
+ const nsTArray<uint32_t>& aAntiTraits,
+ nsIMsgTraitClassificationListener* aTraitListener, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aJunkListener) {
+ AutoTArray<nsCString, 1> uris = {PromiseFlatCString(aMsgURI)};
+ return ClassifyTraitsInMessages(uris, aProTraits, aAntiTraits, aTraitListener,
+ aMsgWindow, aJunkListener);
+}
+
+NS_IMETHODIMP nsBayesianFilter::ClassifyTraitsInMessages(
+ const nsTArray<nsCString>& aMsgURIs, const nsTArray<uint32_t>& aProTraits,
+ const nsTArray<uint32_t>& aAntiTraits,
+ nsIMsgTraitClassificationListener* aTraitListener, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aJunkListener) {
+ MOZ_ASSERT(aProTraits.Length() == aAntiTraits.Length());
+ MessageClassifier* analyzer =
+ new MessageClassifier(this, aJunkListener, aTraitListener, nullptr,
+ aProTraits, aAntiTraits, aMsgWindow, aMsgURIs);
+
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMsgURIs[0], aMsgWindow, analyzer);
+}
+
+class MessageObserver : public TokenAnalyzer {
+ public:
+ MessageObserver(nsBayesianFilter* filter,
+ const nsTArray<uint32_t>& aOldClassifications,
+ const nsTArray<uint32_t>& aNewClassifications,
+ nsIJunkMailClassificationListener* aJunkListener,
+ nsIMsgTraitClassificationListener* aTraitListener)
+ : mFilter(filter),
+ mJunkMailPlugin(filter),
+ mJunkListener(aJunkListener),
+ mTraitListener(aTraitListener),
+ mOldClassifications(aOldClassifications.Clone()),
+ mNewClassifications(aNewClassifications.Clone()) {}
+
+ virtual void analyzeTokens(Tokenizer& tokenizer) {
+ mFilter->observeMessage(tokenizer, mTokenSource, mOldClassifications,
+ mNewClassifications, mJunkListener, mTraitListener);
+ // release reference to listener, which will allow us to go away as well.
+ mTokenListener = nullptr;
+ }
+
+ private:
+ nsBayesianFilter* mFilter;
+ nsCOMPtr<nsIJunkMailPlugin> mJunkMailPlugin;
+ nsCOMPtr<nsIJunkMailClassificationListener> mJunkListener;
+ nsCOMPtr<nsIMsgTraitClassificationListener> mTraitListener;
+ nsTArray<uint32_t> mOldClassifications;
+ nsTArray<uint32_t> mNewClassifications;
+};
+
+NS_IMETHODIMP nsBayesianFilter::SetMsgTraitClassification(
+ const nsACString& aMsgURI, const nsTArray<uint32_t>& aOldTraits,
+ const nsTArray<uint32_t>& aNewTraits,
+ nsIMsgTraitClassificationListener* aTraitListener, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aJunkListener) {
+ MessageObserver* analyzer = new MessageObserver(
+ this, aOldTraits, aNewTraits, aJunkListener, aTraitListener);
+ NS_ENSURE_TRUE(analyzer, NS_ERROR_OUT_OF_MEMORY);
+
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+ NS_ENSURE_TRUE(tokenListener, NS_ERROR_OUT_OF_MEMORY);
+
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMsgURI, aMsgWindow, analyzer);
+}
+
+// set new message classifications for a message
+void nsBayesianFilter::observeMessage(
+ Tokenizer& tokenizer, const nsACString& messageURL,
+ nsTArray<uint32_t>& oldClassifications,
+ nsTArray<uint32_t>& newClassifications,
+ nsIJunkMailClassificationListener* aJunkListener,
+ nsIMsgTraitClassificationListener* aTraitListener) {
+ bool trainingDataWasDirty = mTrainingDataDirty;
+
+ // Uhoh...if the user is re-training then the message may already be
+ // classified and we are classifying it again with the same classification.
+ // the old code would have removed the tokens for this message then added them
+ // back. But this really hurts the message occurrence count for tokens if you
+ // just removed training.dat and are re-training. See Bug #237095 for more
+ // details. What can we do here? Well we can skip the token removal step if
+ // the classifications are the same and assume the user is just re-training.
+ // But this then allows users to re-classify the same message on the same
+ // training set over and over again leading to data skew. But that's all I can
+ // think to do right now to address this.....
+ uint32_t oldLength = oldClassifications.Length();
+ for (uint32_t index = 0; index < oldLength; index++) {
+ uint32_t trait = oldClassifications.ElementAt(index);
+ // skip removing if trait is also in the new set
+ if (newClassifications.Contains(trait)) continue;
+ // remove the tokens from the token set it is currently in
+ uint32_t messageCount;
+ messageCount = mCorpus.getMessageCount(trait);
+ if (messageCount > 0) {
+ mCorpus.setMessageCount(trait, messageCount - 1);
+ mCorpus.forgetTokens(tokenizer, trait, 1);
+ mTrainingDataDirty = true;
+ }
+ }
+
+ nsMsgJunkStatus newClassification = nsIJunkMailPlugin::UNCLASSIFIED;
+ uint32_t junkPercent =
+ 0; // 0 here is no possibility of meeting the classification
+ uint32_t newLength = newClassifications.Length();
+ for (uint32_t index = 0; index < newLength; index++) {
+ uint32_t trait = newClassifications.ElementAt(index);
+ mCorpus.setMessageCount(trait, mCorpus.getMessageCount(trait) + 1);
+ mCorpus.rememberTokens(tokenizer, trait, 1);
+ mTrainingDataDirty = true;
+
+ if (aJunkListener) {
+ if (trait == kJunkTrait) {
+ junkPercent = nsIJunkMailPlugin::IS_SPAM_SCORE;
+ newClassification = nsIJunkMailPlugin::JUNK;
+ } else if (trait == kGoodTrait) {
+ junkPercent = nsIJunkMailPlugin::IS_HAM_SCORE;
+ newClassification = nsIJunkMailPlugin::GOOD;
+ }
+ }
+ }
+
+ if (aJunkListener)
+ aJunkListener->OnMessageClassified(messageURL, newClassification,
+ junkPercent);
+
+ if (aTraitListener) {
+ // construct the outgoing listener arrays
+ AutoTArray<uint32_t, kTraitAutoCapacity> traits;
+ AutoTArray<uint32_t, kTraitAutoCapacity> percents;
+ uint32_t newLength = newClassifications.Length();
+ if (newLength > kTraitAutoCapacity) {
+ traits.SetCapacity(newLength);
+ percents.SetCapacity(newLength);
+ }
+ traits.AppendElements(newClassifications);
+ for (uint32_t index = 0; index < newLength; index++)
+ percents.AppendElement(100); // This is 100 percent, or certainty
+ aTraitListener->OnMessageTraitsClassified(messageURL, traits, percents);
+ }
+
+ if (mTrainingDataDirty && !trainingDataWasDirty) {
+ // if training data became dirty just now, schedule flush
+ // mMinFlushInterval msec from now
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("starting training data flush timer %i msec", mMinFlushInterval));
+
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTimer), nsBayesianFilter::TimerCallback, (void*)this,
+ mMinFlushInterval, nsITimer::TYPE_ONE_SHOT,
+ "nsBayesianFilter::TimerCallback", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start nsBayesianFilter timer");
+ }
+ }
+}
+
+NS_IMETHODIMP nsBayesianFilter::GetUserHasClassified(bool* aResult) {
+ *aResult = ((mCorpus.getMessageCount(kGoodTrait) +
+ mCorpus.getMessageCount(kJunkTrait)) &&
+ mCorpus.countTokens());
+ return NS_OK;
+}
+
+// Set message classification (only allows junk and good)
+NS_IMETHODIMP nsBayesianFilter::SetMessageClassification(
+ const nsACString& aMsgURL, nsMsgJunkStatus aOldClassification,
+ nsMsgJunkStatus aNewClassification, nsIMsgWindow* aMsgWindow,
+ nsIJunkMailClassificationListener* aListener) {
+ AutoTArray<uint32_t, 1> oldClassifications;
+ AutoTArray<uint32_t, 1> newClassifications;
+
+ // convert between classifications and trait
+ if (aOldClassification == nsIJunkMailPlugin::JUNK)
+ oldClassifications.AppendElement(kJunkTrait);
+ else if (aOldClassification == nsIJunkMailPlugin::GOOD)
+ oldClassifications.AppendElement(kGoodTrait);
+ if (aNewClassification == nsIJunkMailPlugin::JUNK)
+ newClassifications.AppendElement(kJunkTrait);
+ else if (aNewClassification == nsIJunkMailPlugin::GOOD)
+ newClassifications.AppendElement(kGoodTrait);
+
+ MessageObserver* analyzer = new MessageObserver(
+ this, oldClassifications, newClassifications, aListener, nullptr);
+ NS_ENSURE_TRUE(analyzer, NS_ERROR_OUT_OF_MEMORY);
+
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+ NS_ENSURE_TRUE(tokenListener, NS_ERROR_OUT_OF_MEMORY);
+
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMsgURL, aMsgWindow, analyzer);
+}
+
+NS_IMETHODIMP nsBayesianFilter::ResetTrainingData() {
+ return mCorpus.resetTrainingData();
+}
+
+NS_IMETHODIMP nsBayesianFilter::DetailMessage(
+ const nsACString& aMsgURI, uint32_t aProTrait, uint32_t aAntiTrait,
+ nsIMsgTraitDetailListener* aDetailListener, nsIMsgWindow* aMsgWindow) {
+ AutoTArray<uint32_t, 1> proTraits = {aProTrait};
+ AutoTArray<uint32_t, 1> antiTraits = {aAntiTrait};
+ AutoTArray<nsCString, 1> uris = {PromiseFlatCString(aMsgURI)};
+
+ MessageClassifier* analyzer =
+ new MessageClassifier(this, nullptr, nullptr, aDetailListener, proTraits,
+ antiTraits, aMsgWindow, uris);
+ NS_ENSURE_TRUE(analyzer, NS_ERROR_OUT_OF_MEMORY);
+
+ TokenStreamListener* tokenListener = new TokenStreamListener(analyzer);
+ NS_ENSURE_TRUE(tokenListener, NS_ERROR_OUT_OF_MEMORY);
+
+ analyzer->setTokenListener(tokenListener);
+ return tokenizeMessage(aMsgURI, aMsgWindow, analyzer);
+}
+
+// nsIMsgCorpus implementation
+
+NS_IMETHODIMP nsBayesianFilter::CorpusCounts(uint32_t aTrait,
+ uint32_t* aMessageCount,
+ uint32_t* aTokenCount) {
+ NS_ENSURE_ARG_POINTER(aTokenCount);
+ *aTokenCount = mCorpus.countTokens();
+ if (aTrait && aMessageCount) *aMessageCount = mCorpus.getMessageCount(aTrait);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBayesianFilter::ClearTrait(uint32_t aTrait) {
+ return mCorpus.ClearTrait(aTrait);
+}
+
+NS_IMETHODIMP
+nsBayesianFilter::UpdateData(nsIFile* aFile, bool aIsAdd,
+ const nsTArray<uint32_t>& aFromTraits,
+ const nsTArray<uint32_t>& aToTraits) {
+ MOZ_ASSERT(aFromTraits.Length() == aToTraits.Length());
+ return mCorpus.UpdateData(aFile, aIsAdd, aFromTraits, aToTraits);
+}
+
+NS_IMETHODIMP
+nsBayesianFilter::GetTokenCount(const nsACString& aWord, uint32_t aTrait,
+ uint32_t* aCount) {
+ NS_ENSURE_ARG_POINTER(aCount);
+ CorpusToken* t = mCorpus.get(PromiseFlatCString(aWord).get());
+ uint32_t count = mCorpus.getTraitCount(t, aTrait);
+ *aCount = count;
+ return NS_OK;
+}
+
+/* Corpus Store */
+
+/*
+ Format of the training file for version 1:
+ [0xFEEDFACE]
+ [number good messages][number bad messages]
+ [number good tokens]
+ [count][length of word]word
+ ...
+ [number bad tokens]
+ [count][length of word]word
+ ...
+
+ Format of the trait file for version 1:
+ [0xFCA93601] (the 01 is the version)
+ for each trait to write
+ [id of trait to write] (0 means end of list)
+ [number of messages per trait]
+ for each token with non-zero count
+ [count]
+ [length of word]word
+*/
+
+CorpusStore::CorpusStore()
+ : TokenHash(sizeof(CorpusToken)),
+ mNextTraitIndex(1) // skip 0 since index=0 will mean end of linked list
+{
+ getTrainingFile(getter_AddRefs(mTrainingFile));
+ mTraitStore.SetCapacity(kTraitStoreCapacity);
+ TraitPerToken traitPT(0, 0);
+ mTraitStore.AppendElement(traitPT); // dummy 0th element
+}
+
+CorpusStore::~CorpusStore() {}
+
+inline int writeUInt32(FILE* stream, uint32_t value) {
+ value = PR_htonl(value);
+ return fwrite(&value, sizeof(uint32_t), 1, stream);
+}
+
+inline int readUInt32(FILE* stream, uint32_t* value) {
+ int n = fread(value, sizeof(uint32_t), 1, stream);
+ if (n == 1) {
+ *value = PR_ntohl(*value);
+ }
+ return n;
+}
+
+void CorpusStore::forgetTokens(Tokenizer& aTokenizer, uint32_t aTraitId,
+ uint32_t aCount) {
+ // if we are forgetting the tokens for a message, should only
+ // subtract 1 from the occurrence count for that token in the training set
+ // because we assume we only bumped the training set count once per messages
+ // containing the token.
+ TokenEnumeration tokens = aTokenizer.getTokens();
+ while (tokens.hasMoreTokens()) {
+ CorpusToken* token = static_cast<CorpusToken*>(tokens.nextToken());
+ remove(token->mWord, aTraitId, aCount);
+ }
+}
+
+void CorpusStore::rememberTokens(Tokenizer& aTokenizer, uint32_t aTraitId,
+ uint32_t aCount) {
+ TokenEnumeration tokens = aTokenizer.getTokens();
+ while (tokens.hasMoreTokens()) {
+ CorpusToken* token = static_cast<CorpusToken*>(tokens.nextToken());
+ if (!token) {
+ NS_ERROR("null token");
+ continue;
+ }
+ add(token->mWord, aTraitId, aCount);
+ }
+}
+
+bool CorpusStore::writeTokens(FILE* stream, bool shrink, uint32_t aTraitId) {
+ uint32_t tokenCount = countTokens();
+ uint32_t newTokenCount = 0;
+
+ // calculate the tokens for this trait to write
+
+ TokenEnumeration tokens = getTokens();
+ for (uint32_t i = 0; i < tokenCount; ++i) {
+ CorpusToken* token = static_cast<CorpusToken*>(tokens.nextToken());
+ uint32_t count = getTraitCount(token, aTraitId);
+ // Shrinking the token database is accomplished by dividing all token counts
+ // by 2. If shrinking, we'll ignore counts < 2, otherwise only ignore counts
+ // of < 1
+ if ((shrink && count > 1) || (!shrink && count)) newTokenCount++;
+ }
+
+ if (writeUInt32(stream, newTokenCount) != 1) return false;
+
+ if (newTokenCount > 0) {
+ TokenEnumeration tokens = getTokens();
+ for (uint32_t i = 0; i < tokenCount; ++i) {
+ CorpusToken* token = static_cast<CorpusToken*>(tokens.nextToken());
+ uint32_t wordCount = getTraitCount(token, aTraitId);
+ if (shrink) wordCount /= 2;
+ if (!wordCount) continue; // Don't output zero count words
+ if (writeUInt32(stream, wordCount) != 1) return false;
+ uint32_t tokenLength = strlen(token->mWord);
+ if (writeUInt32(stream, tokenLength) != 1) return false;
+ if (fwrite(token->mWord, tokenLength, 1, stream) != 1) return false;
+ }
+ }
+ return true;
+}
+
+bool CorpusStore::readTokens(FILE* stream, int64_t fileSize, uint32_t aTraitId,
+ bool aIsAdd) {
+ uint32_t tokenCount;
+ if (readUInt32(stream, &tokenCount) != 1) return false;
+
+ int64_t fpos = ftell(stream);
+ if (fpos < 0) return false;
+
+ uint32_t bufferSize = 4096;
+ char* buffer = new char[bufferSize];
+ if (!buffer) return false;
+
+ for (uint32_t i = 0; i < tokenCount; ++i) {
+ uint32_t count;
+ if (readUInt32(stream, &count) != 1) break;
+ uint32_t size;
+ if (readUInt32(stream, &size) != 1) break;
+ fpos += 8;
+ if (fpos + size > fileSize) {
+ delete[] buffer;
+ return false;
+ }
+ if (size >= bufferSize) {
+ delete[] buffer;
+ while (size >= bufferSize) {
+ bufferSize *= 2;
+ if (bufferSize == 0) return false;
+ }
+ buffer = new char[bufferSize];
+ if (!buffer) return false;
+ }
+ if (fread(buffer, size, 1, stream) != 1) break;
+ fpos += size;
+ buffer[size] = '\0';
+ if (aIsAdd)
+ add(buffer, aTraitId, count);
+ else
+ remove(buffer, aTraitId, count);
+ }
+
+ delete[] buffer;
+
+ return true;
+}
+
+nsresult CorpusStore::getTrainingFile(nsIFile** aTrainingFile) {
+ // should we cache the profile manager's directory?
+ nsCOMPtr<nsIFile> profileDir;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = profileDir->Append(u"training.dat"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return profileDir->QueryInterface(NS_GET_IID(nsIFile), (void**)aTrainingFile);
+}
+
+nsresult CorpusStore::getTraitFile(nsIFile** aTraitFile) {
+ // should we cache the profile manager's directory?
+ nsCOMPtr<nsIFile> profileDir;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = profileDir->Append(u"traits.dat"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return profileDir->QueryInterface(NS_GET_IID(nsIFile), (void**)aTraitFile);
+}
+
+static const char kMagicCookie[] = {'\xFE', '\xED', '\xFA', '\xCE'};
+
+// random string used to identify trait file and version (last byte is version)
+static const char kTraitCookie[] = {'\xFC', '\xA9', '\x36', '\x01'};
+
+void CorpusStore::writeTrainingData(uint32_t aMaximumTokenCount) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("writeTrainingData() entered"));
+ if (!mTrainingFile) return;
+
+ /*
+ * For backwards compatibility, write the good and junk tokens to
+ * training.dat; additional traits are added to a different file
+ */
+
+ // open the file, and write out training data
+ FILE* stream;
+ nsresult rv = mTrainingFile->OpenANSIFileDesc("wb", &stream);
+ if (NS_FAILED(rv)) return;
+
+ // If the number of tokens exceeds our limit, set the shrink flag
+ bool shrink = false;
+ if ((aMaximumTokenCount > 0) && // if 0, do not limit tokens
+ (countTokens() > aMaximumTokenCount)) {
+ shrink = true;
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Warning,
+ ("shrinking token data file"));
+ }
+
+ // We implement shrink by dividing counts by two
+ uint32_t shrinkFactor = shrink ? 2 : 1;
+
+ if (!((fwrite(kMagicCookie, sizeof(kMagicCookie), 1, stream) == 1) &&
+ (writeUInt32(stream, getMessageCount(kGoodTrait) / shrinkFactor)) &&
+ (writeUInt32(stream, getMessageCount(kJunkTrait) / shrinkFactor)) &&
+ writeTokens(stream, shrink, kGoodTrait) &&
+ writeTokens(stream, shrink, kJunkTrait))) {
+ NS_WARNING("failed to write training data.");
+ fclose(stream);
+ // delete the training data file, since it is potentially corrupt.
+ mTrainingFile->Remove(false);
+ } else {
+ fclose(stream);
+ }
+
+ /*
+ * Write the remaining data to a second file traits.dat
+ */
+
+ if (!mTraitFile) {
+ getTraitFile(getter_AddRefs(mTraitFile));
+ if (!mTraitFile) return;
+ }
+
+ // open the file, and write out training data
+ rv = mTraitFile->OpenANSIFileDesc("wb", &stream);
+ if (NS_FAILED(rv)) return;
+
+ uint32_t numberOfTraits = mMessageCounts.Length();
+ bool error;
+ while (1) // break on error or done
+ {
+ if ((error = (fwrite(kTraitCookie, sizeof(kTraitCookie), 1, stream) != 1)))
+ break;
+
+ for (uint32_t index = 0; index < numberOfTraits; index++) {
+ uint32_t trait = mMessageCountsId[index];
+ if (trait == 1 || trait == 2)
+ continue; // junk traits are stored in training.dat
+ if ((error = (writeUInt32(stream, trait) != 1))) break;
+ if ((error = (writeUInt32(stream, mMessageCounts[index] / shrinkFactor) !=
+ 1)))
+ break;
+ if ((error = !writeTokens(stream, shrink, trait))) break;
+ }
+ break;
+ }
+ // we add a 0 at the end to represent end of trait list
+ error = writeUInt32(stream, 0) != 1;
+
+ fclose(stream);
+ if (error) {
+ NS_WARNING("failed to write trait data.");
+ // delete the trait data file, since it is probably corrupt.
+ mTraitFile->Remove(false);
+ }
+
+ if (shrink) {
+ // We'll clear the tokens, and read them back in from the file.
+ // Yes this is slower than in place, but this is a rare event.
+
+ if (countTokens()) {
+ clearTokens();
+ for (uint32_t index = 0; index < numberOfTraits; index++)
+ mMessageCounts[index] = 0;
+ }
+
+ readTrainingData();
+ }
+}
+
+void CorpusStore::readTrainingData() {
+ /*
+ * To maintain backwards compatibility, good and junk traits
+ * are stored in a file "training.dat"
+ */
+ if (!mTrainingFile) return;
+
+ bool exists;
+ nsresult rv = mTrainingFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return;
+
+ FILE* stream;
+ rv = mTrainingFile->OpenANSIFileDesc("rb", &stream);
+ if (NS_FAILED(rv)) return;
+
+ int64_t fileSize;
+ rv = mTrainingFile->GetFileSize(&fileSize);
+ if (NS_FAILED(rv)) return;
+
+ // FIXME: should make sure that the tokenizers are empty.
+ char cookie[4];
+ uint32_t goodMessageCount = 0, junkMessageCount = 0;
+ if (!((fread(cookie, sizeof(cookie), 1, stream) == 1) &&
+ (memcmp(cookie, kMagicCookie, sizeof(cookie)) == 0) &&
+ (readUInt32(stream, &goodMessageCount) == 1) &&
+ (readUInt32(stream, &junkMessageCount) == 1) &&
+ readTokens(stream, fileSize, kGoodTrait, true) &&
+ readTokens(stream, fileSize, kJunkTrait, true))) {
+ NS_WARNING("failed to read training data.");
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("failed to read training data."));
+ }
+ setMessageCount(kGoodTrait, goodMessageCount);
+ setMessageCount(kJunkTrait, junkMessageCount);
+
+ fclose(stream);
+
+ /*
+ * Additional traits are stored in traits.dat
+ */
+
+ if (!mTraitFile) {
+ getTraitFile(getter_AddRefs(mTraitFile));
+ if (!mTraitFile) return;
+ }
+
+ rv = mTraitFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return;
+
+ nsTArray<uint32_t> empty;
+ rv = UpdateData(mTraitFile, true, empty, empty);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to read training data.");
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Error,
+ ("failed to read training data."));
+ }
+ return;
+}
+
+nsresult CorpusStore::resetTrainingData() {
+ // clear out our in memory training tokens...
+ if (countTokens()) clearTokens();
+
+ uint32_t length = mMessageCounts.Length();
+ for (uint32_t index = 0; index < length; index++) mMessageCounts[index] = 0;
+
+ if (mTrainingFile) mTrainingFile->Remove(false);
+ if (mTraitFile) mTraitFile->Remove(false);
+ return NS_OK;
+}
+
+inline CorpusToken* CorpusStore::get(const char* word) {
+ return static_cast<CorpusToken*>(TokenHash::get(word));
+}
+
+nsresult CorpusStore::updateTrait(CorpusToken* token, uint32_t aTraitId,
+ int32_t aCountChange) {
+ NS_ENSURE_ARG_POINTER(token);
+ uint32_t nextLink = token->mTraitLink;
+ uint32_t lastLink = 0;
+
+ uint32_t linkCount, maxLinks = 100; // sanity check
+ for (linkCount = 0; nextLink && linkCount < maxLinks; linkCount++) {
+ TraitPerToken& traitPT = mTraitStore[nextLink];
+ if (traitPT.mId == aTraitId) {
+ // be careful with signed versus unsigned issues here
+ if (static_cast<int32_t>(traitPT.mCount) + aCountChange > 0)
+ traitPT.mCount += aCountChange;
+ else
+ traitPT.mCount = 0;
+ // we could delete zero count traits here, but let's not. It's rare
+ // anyway.
+ return NS_OK;
+ }
+ lastLink = nextLink;
+ nextLink = traitPT.mNextLink;
+ }
+ if (linkCount >= maxLinks) return NS_ERROR_FAILURE;
+
+ // trait does not exist, so add it
+
+ if (aCountChange > 0) // don't set a negative count
+ {
+ TraitPerToken traitPT(aTraitId, aCountChange);
+ if (mTraitStore.Length() == mNextTraitIndex)
+ mTraitStore.InsertElementAt(mNextTraitIndex, traitPT);
+ else if (mTraitStore.Length() > mNextTraitIndex)
+ mTraitStore.ReplaceElementsAt(mNextTraitIndex, 1, traitPT);
+ else
+ return NS_ERROR_FAILURE;
+ if (lastLink)
+ // the token had a parent, so update it
+ mTraitStore[lastLink].mNextLink = mNextTraitIndex;
+ else
+ // need to update the token's root link
+ token->mTraitLink = mNextTraitIndex;
+ mNextTraitIndex++;
+ }
+ return NS_OK;
+}
+
+uint32_t CorpusStore::getTraitCount(CorpusToken* token, uint32_t aTraitId) {
+ uint32_t nextLink;
+ if (!token || !(nextLink = token->mTraitLink)) return 0;
+
+ uint32_t linkCount, maxLinks = 100; // sanity check
+ for (linkCount = 0; nextLink && linkCount < maxLinks; linkCount++) {
+ TraitPerToken& traitPT = mTraitStore[nextLink];
+ if (traitPT.mId == aTraitId) return traitPT.mCount;
+ nextLink = traitPT.mNextLink;
+ }
+ NS_ASSERTION(linkCount < maxLinks, "Corrupt trait count store");
+
+ // trait not found (or error), so count is zero
+ return 0;
+}
+
+CorpusToken* CorpusStore::add(const char* word, uint32_t aTraitId,
+ uint32_t aCount) {
+ CorpusToken* token = static_cast<CorpusToken*>(TokenHash::add(word));
+ if (token) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("adding word to corpus store: %s (Trait=%d) (deltaCount=%d)", word,
+ aTraitId, aCount));
+ updateTrait(token, aTraitId, aCount);
+ }
+ return token;
+}
+
+void CorpusStore::remove(const char* word, uint32_t aTraitId, uint32_t aCount) {
+ MOZ_LOG(BayesianFilterLogModule, LogLevel::Debug,
+ ("remove word: %s (TraitId=%d) (Count=%d)", word, aTraitId, aCount));
+ CorpusToken* token = get(word);
+ if (token) updateTrait(token, aTraitId, -static_cast<int32_t>(aCount));
+}
+
+uint32_t CorpusStore::getMessageCount(uint32_t aTraitId) {
+ size_t index = mMessageCountsId.IndexOf(aTraitId);
+ if (index == mMessageCountsId.NoIndex) return 0;
+ return mMessageCounts.ElementAt(index);
+}
+
+void CorpusStore::setMessageCount(uint32_t aTraitId, uint32_t aCount) {
+ size_t index = mMessageCountsId.IndexOf(aTraitId);
+ if (index == mMessageCountsId.NoIndex) {
+ mMessageCounts.AppendElement(aCount);
+ mMessageCountsId.AppendElement(aTraitId);
+ } else {
+ mMessageCounts[index] = aCount;
+ }
+}
+
+nsresult CorpusStore::UpdateData(nsIFile* aFile, bool aIsAdd,
+ const nsTArray<uint32_t>& aFromTraits,
+ const nsTArray<uint32_t>& aToTraits) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ MOZ_ASSERT(aFromTraits.Length() == aToTraits.Length());
+
+ int64_t fileSize;
+ nsresult rv = aFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ FILE* stream;
+ rv = aFile->OpenANSIFileDesc("rb", &stream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool error;
+ do // break on error or done
+ {
+ char cookie[4];
+ if ((error = (fread(cookie, sizeof(cookie), 1, stream) != 1))) break;
+
+ if ((error = memcmp(cookie, kTraitCookie, sizeof(cookie)))) break;
+
+ uint32_t fileTrait;
+ while (!(error = (readUInt32(stream, &fileTrait) != 1)) && fileTrait) {
+ uint32_t count;
+ if ((error = (readUInt32(stream, &count) != 1))) break;
+
+ uint32_t localTrait = fileTrait;
+ // remap the trait
+ for (uint32_t i = 0; i < aFromTraits.Length(); i++) {
+ if (aFromTraits[i] == fileTrait) localTrait = aToTraits[i];
+ }
+
+ uint32_t messageCount = getMessageCount(localTrait);
+ if (aIsAdd)
+ messageCount += count;
+ else if (count > messageCount)
+ messageCount = 0;
+ else
+ messageCount -= count;
+ setMessageCount(localTrait, messageCount);
+
+ if ((error = !readTokens(stream, fileSize, localTrait, aIsAdd))) break;
+ }
+ break;
+ } while (0);
+
+ fclose(stream);
+
+ if (error) return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+nsresult CorpusStore::ClearTrait(uint32_t aTrait) {
+ // clear message counts
+ setMessageCount(aTrait, 0);
+
+ TokenEnumeration tokens = getTokens();
+ while (tokens.hasMoreTokens()) {
+ CorpusToken* token = static_cast<CorpusToken*>(tokens.nextToken());
+ int32_t wordCount = static_cast<int32_t>(getTraitCount(token, aTrait));
+ updateTrait(token, aTrait, -wordCount);
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.h b/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.h
new file mode 100644
index 0000000000..70d0a1a02b
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.h
@@ -0,0 +1,397 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsBayesianFilter_h__
+#define nsBayesianFilter_h__
+
+#include <stdio.h>
+#include "nsCOMPtr.h"
+#include "nsIMsgFilterPlugin.h"
+#include "PLDHashTable.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "nsIObserver.h"
+#include "nsHashPropertyBag.h"
+#include "mozilla/intl/WordBreaker.h"
+
+#include "mozilla/ArenaAllocator.h"
+
+#define DEFAULT_MIN_INTERVAL_BETWEEN_WRITES 15 * 60 * 1000
+
+struct Token;
+class TokenEnumeration;
+class TokenAnalyzer;
+class nsIMsgWindow;
+class nsIUTF8StringEnumerator;
+struct BaseToken;
+struct CorpusToken;
+
+/**
+ * Helper class to enumerate Token objects in a PLDHashTable
+ * safely and without copying (see bugzilla #174859). The
+ * enumeration is safe to use until an Add()
+ * or Remove() is performed on the table.
+ */
+class TokenEnumeration {
+ public:
+ explicit TokenEnumeration(PLDHashTable* table);
+ bool hasMoreTokens();
+ BaseToken* nextToken();
+
+ private:
+ PLDHashTable::Iterator mIterator;
+};
+
+// A trait is some aspect of a message, like being junk or tagged as
+// Personal, that the statistical classifier should track. The Trait
+// structure is a per-token representation of information pertaining to
+// a message trait.
+//
+// Traits per token are maintained as a linked list.
+//
+struct TraitPerToken {
+ uint32_t mId; // identifying number for a trait
+ uint32_t mCount; // count of messages with this token and trait
+ uint32_t mNextLink; // index in mTraitStore for the next trait, or 0
+ // for none
+ TraitPerToken(uint32_t aId, uint32_t aCount); // inititializer
+};
+
+// An Analysis is the statistical results for a particular message, a
+// particular token, and for a particular pair of trait/antitrait, that
+// is then used in subsequent analysis to score the message.
+//
+// Analyses per token are maintained as a linked list.
+//
+struct AnalysisPerToken {
+ uint32_t mTraitIndex; // index representing a protrait/antitrait pair.
+ // So if we are analyzing 3 different traits, then
+ // the first trait is 0, the second 1, etc.
+ double mDistance; // absolute value of mProbability - 0.5
+ double mProbability; // relative indicator of match of trait to token
+ uint32_t mNextLink; // index in mAnalysisStore for the Analysis object
+ // for the next trait index, or 0 for none.
+ // initializer
+ AnalysisPerToken(uint32_t aTraitIndex, double aDistance, double aProbability);
+};
+
+class TokenHash {
+ public:
+ virtual ~TokenHash();
+ /**
+ * Clears out the previous message tokens.
+ */
+ nsresult clearTokens();
+ uint32_t countTokens();
+ TokenEnumeration getTokens();
+ BaseToken* add(const char* word);
+
+ protected:
+ explicit TokenHash(uint32_t entrySize);
+ mozilla::ArenaAllocator<16384, 2> mWordPool;
+ uint32_t mEntrySize;
+ PLDHashTable mTokenTable;
+ char* copyWord(const char* word, uint32_t len);
+ BaseToken* get(const char* word);
+};
+
+class Tokenizer : public TokenHash {
+ public:
+ Tokenizer();
+ ~Tokenizer();
+
+ Token* get(const char* word);
+
+ // The training set keeps an occurrence count on each word. This count
+ // is supposed to count the # of messages it occurs in.
+ // When add/remove is called while tokenizing a message and NOT the training
+ // set,
+ //
+ Token* add(const char* word, uint32_t count = 1);
+
+ Token* copyTokens();
+
+ void tokenize(const char* text);
+
+ /**
+ * Creates specific tokens based on the mime headers for the message being
+ * tokenized
+ */
+ void tokenizeHeaders(nsTArray<nsCString>& aHeaderNames,
+ nsTArray<nsCString>& aHeaderValues);
+
+ void tokenizeAttachments(nsTArray<RefPtr<nsIPropertyBag2>>& attachments);
+
+ nsCString mBodyDelimiters; // delimiters for body tokenization
+ nsCString mHeaderDelimiters; // delimiters for header tokenization
+
+ // arrays of extra headers to tokenize / to not tokenize
+ nsTArray<nsCString> mEnabledHeaders;
+ nsTArray<nsCString> mDisabledHeaders;
+ // Delimiters used in tokenizing a particular header.
+ // Parallel array to mEnabledHeaders
+ nsTArray<nsCString> mEnabledHeadersDelimiters;
+ bool mCustomHeaderTokenization; // Are there any preference-set tokenization
+ // customizations?
+ uint32_t mMaxLengthForToken; // maximum length of a token
+ // should we convert iframe to div during tokenization?
+ bool mIframeToDiv;
+
+ private:
+ void tokenize_ascii_word(char* word);
+ void tokenize_japanese_word(char* chunk);
+ inline void addTokenForHeader(const char* aTokenPrefix, nsACString& aValue,
+ bool aTokenizeValue = false,
+ const char* aDelimiters = nullptr);
+ nsresult stripHTML(const nsAString& inString, nsAString& outString);
+ // helper function to escape \n, \t, etc from a CString
+ void UnescapeCString(nsCString& aCString);
+ nsresult ScannerNext(const char16_t* text, int32_t length, int32_t pos,
+ bool isLastBuffer, int32_t* begin, int32_t* end,
+ bool* _retval);
+};
+
+/**
+ * Implements storage of a collection of message tokens and counts for
+ * a corpus of classified messages
+ */
+
+class CorpusStore : public TokenHash {
+ public:
+ CorpusStore();
+ ~CorpusStore();
+
+ /**
+ * retrieve the token structure for a particular string
+ *
+ * @param word the character representation of the token
+ *
+ * @return token structure containing counts, null if not found
+ */
+ CorpusToken* get(const char* word);
+
+ /**
+ * add tokens to the storage, or increment counts if already exists.
+ *
+ * @param aTokenizer tokenizer for the list of tokens to remember
+ * @param aTraitId id for the trait whose counts will be remembered
+ * @param aCount number of new messages represented by the token list
+ */
+ void rememberTokens(Tokenizer& aTokenizer, uint32_t aTraitId,
+ uint32_t aCount);
+
+ /**
+ * decrement counts for tokens in the storage, removing if all counts
+ * are zero
+ *
+ * @param aTokenizer tokenizer for the list of tokens to forget
+ * @param aTraitId id for the trait whose counts will be removed
+ * @param aCount number of messages represented by the token list
+ */
+ void forgetTokens(Tokenizer& aTokenizer, uint32_t aTraitId, uint32_t aCount);
+
+ /**
+ * write the corpus information to file storage
+ *
+ * @param aMaximumTokenCount prune tokens if number of tokens exceeds
+ * this value. == 0 for no pruning
+ */
+ void writeTrainingData(uint32_t aMaximumTokenCount);
+
+ /**
+ * read the corpus information from file storage
+ */
+ void readTrainingData();
+
+ /**
+ * delete the local corpus storage file and data
+ */
+ nsresult resetTrainingData();
+
+ /**
+ * get the count of messages whose tokens are stored that are associated
+ * with a trait
+ *
+ * @param aTraitId identifier for the trait
+ * @return number of messages for that trait
+ */
+ uint32_t getMessageCount(uint32_t aTraitId);
+
+ /**
+ * set the count of messages whose tokens are stored that are associated
+ * with a trait
+ *
+ * @param aTraitId identifier for the trait
+ * @param aCount number of messages for that trait
+ */
+ void setMessageCount(uint32_t aTraitId, uint32_t aCount);
+
+ /**
+ * get the count of messages associated with a particular token and trait
+ *
+ * @param token the token string and associated counts
+ * @param aTraitId identifier for the trait
+ */
+ uint32_t getTraitCount(CorpusToken* token, uint32_t aTraitId);
+
+ /**
+ * Add (or remove) data from a particular file to the corpus data.
+ *
+ * @param aFile the file with the data, in the format:
+ *
+ * Format of the trait file for version 1:
+ * [0xFCA93601] (the 01 is the version)
+ * for each trait to write:
+ * [id of trait to write] (0 means end of list)
+ * [number of messages per trait]
+ * for each token with non-zero count
+ * [count]
+ * [length of word]word
+ *
+ * @param aIsAdd should the data be added, or removed? true if adding,
+ * else removing.
+ *
+ * @param aFromTraits array of trait ids used in aFile. If aFile contains
+ * trait ids that are not in this array, they are not
+ * remapped, but assumed to be local trait ids.
+ *
+ * @param aToTraits array of trait ids, corresponding to elements of
+ * aFromTraits, that represent the local trait ids to be
+ * used in storing data from aFile into the local corpus.
+ *
+ */
+ nsresult UpdateData(nsIFile* aFile, bool aIsAdd,
+ const nsTArray<uint32_t>& aFromTraits,
+ const nsTArray<uint32_t>& aToTraits);
+
+ /**
+ * remove all counts (message and tokens) for a trait id
+ *
+ * @param aTrait trait id for the trait to remove
+ */
+ nsresult ClearTrait(uint32_t aTrait);
+
+ protected:
+ /**
+ * return the local corpus storage file for junk traits
+ */
+ nsresult getTrainingFile(nsIFile** aFile);
+
+ /**
+ * return the local corpus storage file for non-junk traits
+ */
+ nsresult getTraitFile(nsIFile** aFile);
+
+ /**
+ * read token strings from the data file
+ *
+ * @param stream file stream with token data
+ * @param fileSize file size
+ * @param aTraitId id for the trait whose counts will be read
+ * @param aIsAdd true to add the counts, false to remove them
+ *
+ * @return true if successful, false if error
+ */
+ bool readTokens(FILE* stream, int64_t fileSize, uint32_t aTraitId,
+ bool aIsAdd);
+
+ /**
+ * write token strings to the data file
+ */
+ bool writeTokens(FILE* stream, bool shrink, uint32_t aTraitId);
+
+ /**
+ * remove counts for a token string
+ */
+ void remove(const char* word, uint32_t aTraitId, uint32_t aCount);
+
+ /**
+ * add counts for a token string, adding the token string if new
+ */
+ CorpusToken* add(const char* word, uint32_t aTraitId, uint32_t aCount);
+
+ /**
+ * change counts in a trait in the traits array, adding the trait if needed
+ */
+ nsresult updateTrait(CorpusToken* token, uint32_t aTraitId,
+ int32_t aCountChange);
+ nsCOMPtr<nsIFile> mTrainingFile; // file used to store junk training data
+ nsCOMPtr<nsIFile> mTraitFile; // file used to store non-junk
+ // training data
+ nsTArray<TraitPerToken> mTraitStore; // memory for linked-list of counts
+ uint32_t mNextTraitIndex; // index in mTraitStore to first empty
+ // TraitPerToken
+ nsTArray<uint32_t> mMessageCounts; // count of messages per trait
+ // represented in the store
+ nsTArray<uint32_t> mMessageCountsId; // Parallel array to mMessageCounts,
+ // with the corresponding trait ID
+};
+
+class nsBayesianFilter : public nsIJunkMailPlugin,
+ nsIMsgCorpus,
+ nsIObserver,
+ nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFILTERPLUGIN
+ NS_DECL_NSIJUNKMAILPLUGIN
+ NS_DECL_NSIMSGCORPUS
+ NS_DECL_NSIOBSERVER
+
+ nsBayesianFilter();
+
+ nsresult Init();
+
+ nsresult tokenizeMessage(const nsACString& messageURI,
+ nsIMsgWindow* aMsgWindow, TokenAnalyzer* analyzer);
+ void classifyMessage(Tokenizer& tokens, const nsACString& messageURI,
+ nsIJunkMailClassificationListener* listener);
+
+ void classifyMessage(Tokenizer& tokenizer, const nsACString& messageURI,
+ nsTArray<uint32_t>& aProTraits,
+ nsTArray<uint32_t>& aAntiTraits,
+ nsIJunkMailClassificationListener* listener,
+ nsIMsgTraitClassificationListener* aTraitListener,
+ nsIMsgTraitDetailListener* aDetailListener);
+
+ void observeMessage(Tokenizer& tokens, const nsACString& messageURI,
+ nsTArray<uint32_t>& oldClassifications,
+ nsTArray<uint32_t>& newClassifications,
+ nsIJunkMailClassificationListener* listener,
+ nsIMsgTraitClassificationListener* aTraitListener);
+
+ protected:
+ virtual ~nsBayesianFilter();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+
+ CorpusStore mCorpus;
+ double mJunkProbabilityThreshold;
+ int32_t mMaximumTokenCount;
+ bool mTrainingDataDirty;
+ int32_t mMinFlushInterval; // in milliseconds, must be positive
+ // and not too close to 0
+ nsCOMPtr<nsITimer> mTimer;
+
+ // index in mAnalysisStore for first empty AnalysisPerToken
+ uint32_t mNextAnalysisIndex;
+ // memory for linked list of AnalysisPerToken objects
+ nsTArray<AnalysisPerToken> mAnalysisStore;
+ /**
+ * Determine the location in mAnalysisStore where the AnalysisPerToken
+ * object for a particular token and trait is stored
+ */
+ uint32_t getAnalysisIndex(Token& token, uint32_t aTraitIndex);
+ /**
+ * Set the value of the AnalysisPerToken object for a particular
+ * token and trait
+ */
+ nsresult setAnalysis(Token& token, uint32_t aTraitIndex, double aDistance,
+ double aProbability);
+};
+
+#endif // _nsBayesianFilter_h__
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/nsIncompleteGamma.h b/comm/mailnews/extensions/bayesian-spam-filter/nsIncompleteGamma.h
new file mode 100644
index 0000000000..9b96459c7c
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/nsIncompleteGamma.h
@@ -0,0 +1,239 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsIncompleteGamma_h__
+#define nsIncompleteGamma_h__
+
+/* An implementation of the incomplete gamma functions for real
+ arguments. P is defined as
+
+ x
+ /
+ 1 [ a - 1 - t
+ P(a, x) = -------- I t e dt
+ Gamma(a) ]
+ /
+ 0
+
+ and
+
+ infinity
+ /
+ 1 [ a - 1 - t
+ Q(a, x) = -------- I t e dt
+ Gamma(a) ]
+ /
+ x
+
+ so that P(a,x) + Q(a,x) = 1.
+
+ Both a series expansion and a continued fraction exist. This
+ implementation uses the more efficient method based on the arguments.
+
+ Either case involves calculating a multiplicative term:
+ e^(-x)*x^a/Gamma(a).
+ Here we calculate the log of this term. Most math libraries have a
+ "lgamma" function but it is not re-entrant. Some libraries have a
+ "lgamma_r" which is re-entrant. Use it if possible. I have included a
+ simple replacement but it is certainly not as accurate.
+
+ Relative errors are almost always < 1e-10 and usually < 1e-14. Very
+ small and very large arguments cause trouble.
+
+ The region where a < 0.5 and x < 0.5 has poor error properties and is
+ not too stable. Get a better routine if you need results in this
+ region.
+
+ The error argument will be set negative if there is a domain error or
+ positive for an internal calculation error, currently lack of
+ convergence. A value is always returned, though.
+
+ */
+
+#include <math.h>
+#include <float.h>
+
+// the main routine
+static double nsIncompleteGammaP(double a, double x, int* error);
+
+// nsLnGamma(z): either a wrapper around lgamma_r or the internal function.
+// C_m = B[2*m]/(2*m*(2*m-1)) where B is a Bernoulli number
+static const double C_1 = 1.0 / 12.0;
+static const double C_2 = -1.0 / 360.0;
+static const double C_3 = 1.0 / 1260.0;
+static const double C_4 = -1.0 / 1680.0;
+static const double C_5 = 1.0 / 1188.0;
+static const double C_6 = -691.0 / 360360.0;
+static const double C_7 = 1.0 / 156.0;
+static const double C_8 = -3617.0 / 122400.0;
+static const double C_9 = 43867.0 / 244188.0;
+static const double C_10 = -174611.0 / 125400.0;
+static const double C_11 = 77683.0 / 5796.0;
+
+// truncated asymptotic series in 1/z
+static inline double lngamma_asymp(double z) {
+ double w, w2, sum;
+ w = 1.0 / z;
+ w2 = w * w;
+ sum =
+ w *
+ (w2 * (w2 * (w2 * (w2 * (w2 * (w2 * (w2 * (w2 * (w2 * (C_11 * w2 + C_10) +
+ C_9) +
+ C_8) +
+ C_7) +
+ C_6) +
+ C_5) +
+ C_4) +
+ C_3) +
+ C_2) +
+ C_1);
+
+ return sum;
+}
+
+struct fact_table_s {
+ double fact;
+ double lnfact;
+};
+
+// for speed and accuracy
+static const struct fact_table_s FactTable[] = {
+ {1.000000000000000, 0.0000000000000000000000e+00},
+ {1.000000000000000, 0.0000000000000000000000e+00},
+ {2.000000000000000, 6.9314718055994530942869e-01},
+ {6.000000000000000, 1.7917594692280550007892e+00},
+ {24.00000000000000, 3.1780538303479456197550e+00},
+ {120.0000000000000, 4.7874917427820459941458e+00},
+ {720.0000000000000, 6.5792512120101009952602e+00},
+ {5040.000000000000, 8.5251613610654142999881e+00},
+ {40320.00000000000, 1.0604602902745250228925e+01},
+ {362880.0000000000, 1.2801827480081469610995e+01},
+ {3628800.000000000, 1.5104412573075515295248e+01},
+ {39916800.00000000, 1.7502307845873885839769e+01},
+ {479001600.0000000, 1.9987214495661886149228e+01},
+ {6227020800.000000, 2.2552163853123422886104e+01},
+ {87178291200.00000, 2.5191221182738681499610e+01},
+ {1307674368000.000, 2.7899271383840891566988e+01},
+ {20922789888000.00, 3.0671860106080672803835e+01},
+ {355687428096000.0, 3.3505073450136888885825e+01},
+ {6402373705728000., 3.6395445208033053576674e+01}};
+#define FactTableLength (int)(sizeof(FactTable) / sizeof(FactTable[0]))
+
+// for speed
+static const double ln_2pi_2 = 0.918938533204672741803; // log(2*PI)/2
+
+/* A simple lgamma function, not very robust.
+
+ Valid for z_in > 0 ONLY.
+
+ For z_in > 8 precision is quite good, relative errors < 1e-14 and
+ usually better. For z_in < 8 relative errors increase but are usually
+ < 1e-10. In two small regions, 1 +/- .001 and 2 +/- .001 errors
+ increase quickly.
+*/
+static double nsLnGamma(double z_in, int* gsign) {
+ double scale, z, sum, result;
+ *gsign = 1;
+
+ int zi = (int)z_in;
+ if (z_in == (double)zi) {
+ if (0 < zi && zi <= FactTableLength)
+ return FactTable[zi - 1].lnfact; // gamma(z) = (z-1)!
+ }
+
+ for (scale = 1.0, z = z_in; z < 8.0; ++z) scale *= z;
+
+ sum = lngamma_asymp(z);
+ result = (z - 0.5) * log(z) - z + ln_2pi_2 - log(scale);
+ result += sum;
+ return result;
+}
+
+// log( e^(-x)*x^a/Gamma(a) )
+static inline double lnPQfactor(double a, double x) {
+ int gsign; // ignored because a > 0
+ return a * log(x) - x - nsLnGamma(a, &gsign);
+}
+
+static double Pseries(double a, double x, int* error) {
+ double sum, term;
+ const double eps = 2.0 * DBL_EPSILON;
+ const int imax = 5000;
+ int i;
+
+ sum = term = 1.0 / a;
+ for (i = 1; i < imax; ++i) {
+ term *= x / (a + i);
+ sum += term;
+ if (fabs(term) < eps * fabs(sum)) break;
+ }
+
+ if (i >= imax) *error = 1;
+
+ return sum;
+}
+
+static double Qcontfrac(double a, double x, int* error) {
+ double result, D, C, e, f, term;
+ const double eps = 2.0 * DBL_EPSILON;
+ const double small = DBL_EPSILON * DBL_EPSILON * DBL_EPSILON * DBL_EPSILON;
+ const int imax = 5000;
+ int i;
+
+ // modified Lentz method
+ f = x - a + 1.0;
+ if (fabs(f) < small) f = small;
+ C = f + 1.0 / small;
+ D = 1.0 / f;
+ result = D;
+ for (i = 1; i < imax; ++i) {
+ e = i * (a - i);
+ f += 2.0;
+ D = f + e * D;
+ if (fabs(D) < small) D = small;
+ D = 1.0 / D;
+ C = f + e / C;
+ if (fabs(C) < small) C = small;
+ term = C * D;
+ result *= term;
+ if (fabs(term - 1.0) < eps) break;
+ }
+
+ if (i >= imax) *error = 1;
+ return result;
+}
+
+static double nsIncompleteGammaP(double a, double x, int* error) {
+ double result, dom, ldom;
+ // domain errors. the return values are meaningless but have
+ // to return something.
+ *error = -1;
+ if (a <= 0.0) return 1.0;
+ if (x < 0.0) return 0.0;
+ *error = 0;
+ if (x == 0.0) return 0.0;
+
+ ldom = lnPQfactor(a, x);
+ dom = exp(ldom);
+ // might need to adjust the crossover point
+ if (a <= 0.5) {
+ if (x < a + 1.0)
+ result = dom * Pseries(a, x, error);
+ else
+ result = 1.0 - dom * Qcontfrac(a, x, error);
+ } else {
+ if (x < a)
+ result = dom * Pseries(a, x, error);
+ else
+ result = 1.0 - dom * Qcontfrac(a, x, error);
+ }
+
+ // not clear if this can ever happen
+ if (result > 1.0) result = 1.0;
+ if (result < 0.0) result = 0.0;
+ return result;
+}
+
+#endif
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/head_bayes.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/head_bayes.js
new file mode 100644
index 0000000000..b502dcc2e5
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/head_bayes.js
@@ -0,0 +1,28 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+function getSpec(aFileName) {
+ var file = do_get_file("resources/" + aFileName);
+ var uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIURL);
+ uri = uri.mutate().setQuery("type=application/x-message-display").finalize();
+ return uri.spec;
+}
+
+registerCleanupFunction(function () {
+ load("../../../../resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases.dat b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases.dat
new file mode 100644
index 0000000000..31162459e4
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases.dat
Binary files differ
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases1.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases1.eml
new file mode 100644
index 0000000000..4720467fe6
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases1.eml
@@ -0,0 +1,6 @@
+From - Sat Jan 26 08:43:42 2008
+Subject: test1
+Content-Type: text/plain; charset=iso-8859-1
+
+important
+
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases2.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases2.eml
new file mode 100644
index 0000000000..9a251486a9
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases2.eml
@@ -0,0 +1,6 @@
+From - Sat Jan 26 08:43:42 2008
+Subject: test2
+Content-Type: text/plain; charset=iso-8859-1
+
+work
+
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases3.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases3.eml
new file mode 100644
index 0000000000..de31992ac5
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/aliases3.eml
@@ -0,0 +1,6 @@
+From - Sat Jan 26 08:43:42 2008
+Subject: test3
+Content-Type: text/plain; charset=iso-8859-1
+
+very important work
+
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham1.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham1.eml
new file mode 100644
index 0000000000..6a63f587b8
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham1.eml
@@ -0,0 +1,7 @@
+Date: Tue, 30 Apr 2008 00:12:17 -0700
+From: Mom <mother@example.com>
+To: Careful Reader <reader@example.org>
+Subject: eat your vegetables
+MIME-Version: 1.0
+
+vegetables are very important for your health and wealth.
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham2.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham2.eml
new file mode 100644
index 0000000000..cd6691b921
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/ham2.eml
@@ -0,0 +1,8 @@
+Date: Tue, 27 Apr 2006 00:13:23 -0700
+From: Evil Despot <boss@example.com>
+To: Careful Reader <reader@example.org>
+Subject: finish your report
+MIME-Version: 1.0
+
+If you want to keep your sorry job and health, finish that
+important report before the close of business today.
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/msgCorpus.dat b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/msgCorpus.dat
new file mode 100644
index 0000000000..f273a4f10c
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/msgCorpus.dat
Binary files differ
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam1.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam1.eml
new file mode 100644
index 0000000000..ea629213cc
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam1.eml
@@ -0,0 +1,7 @@
+Date: Tue, 29 Apr 2008 00:10:07 -0700
+From: Spam King <spammer@example.com>
+To: Careful Reader <reader@example.org>
+Subject: viagra is your nigerian xxx dream
+MIME-Version: 1.0
+
+click here to make lots of money and wealth
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam2.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam2.eml
new file mode 100644
index 0000000000..817d328cf2
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam2.eml
@@ -0,0 +1,8 @@
+Date: Mon, 27 Apr 2008 01:02:03 -0700
+From: Stock Pusher <broker@example.net>
+To: Careful Reader <reader@example.org>
+Subject: ABCD Corporation will soar tomorrow!
+MIME-Version: 1.0
+
+Make lots of money! Put all of your money into ACBD Corporation
+Stock!
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam3.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam3.eml
new file mode 100644
index 0000000000..0a524e604b
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam3.eml
@@ -0,0 +1,7 @@
+Date: Wed, 30 Apr 2008 01:11:17 -0700
+From: Spam King <spammer@example.com>
+To: Careful Reader <reader@example.org>
+Subject: we have your nigerian xxx dream
+MIME-Version: 1.0
+
+Not making lots of money and wealth? Call me!
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam4.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam4.eml
new file mode 100644
index 0000000000..775d3b41fa
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/spam4.eml
@@ -0,0 +1,8 @@
+Date: Tue, 28 Apr 2008 01:02:04 -0700
+From: Stock Pusher <broker@example.net>
+To: Careful Reader <reader@example.org>
+Subject: ABCD Corporation will really soar this time!
+MIME-Version: 1.0
+
+Make lots of money! Put all of your money into ABCD Corporation
+Stock! (We really mean it this time!)
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/tokenTest.eml b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/tokenTest.eml
new file mode 100644
index 0000000000..d6e7e0ae3d
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/tokenTest.eml
@@ -0,0 +1,14 @@
+Date: Tue, 30 Apr 2008 00:12:17 -0700
+From: Mom <mother@example.com>
+To: Careful Reader <reader@example.org>
+Subject: eat your vegetables to live long
+Received: from c-1-2-3-4.hsd1.wa.example.net ([1.2.3.4] helo=theComputer)
+ by host301.example.com with esmtpa (Exim 4.69)
+ (envelope-from <someone@example.com>)
+ id 1LeEgH-0003GN-Rr
+ for reader@example.org; Mon, 02 Mar 2009 13:24:06 -0700
+MIME-Version: 1.0
+Message-Id: 14159
+Sender: Bugzilla Test Setup <noreply@example.org>
+
+This is a sentence. Important URL is http://www.example.org Check it out!
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/trainingfile.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/trainingfile.js
new file mode 100644
index 0000000000..b6d37e879b
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/resources/trainingfile.js
@@ -0,0 +1,108 @@
+/* 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/. */
+
+// service class to manipulate the junk training.dat file
+// code is adapted from Mnehy Thunderbird Extension
+
+/* exported TrainingData */
+function TrainingData() {
+ // local constants
+
+ const CC = Components.Constructor;
+
+ // public methods
+
+ this.read = read;
+
+ // public variables
+
+ this.mGoodTokens = 0;
+ this.mJunkTokens = 0;
+ this.mGoodMessages = 0;
+ this.mJunkMessages = 0;
+ this.mGoodCounts = {};
+ this.mJunkCounts = {};
+
+ // helper functions
+
+ function getJunkStatFile() {
+ var sBaseDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ var CFileByFile = new CC(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithFile"
+ );
+ var oFile = new CFileByFile(sBaseDir);
+ oFile.append("training.dat");
+ return oFile;
+ }
+
+ function getBinStream(oFile) {
+ if (oFile && oFile.exists()) {
+ var oUri = Services.io.newFileURI(oFile);
+ // open stream (channel)
+ let channel = Services.io.newChannelFromURI(
+ oUri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ var oStream = channel.open();
+ // buffer it
+ var oBufStream = Cc[
+ "@mozilla.org/network/buffered-input-stream;1"
+ ].createInstance(Ci.nsIBufferedInputStream);
+ oBufStream.init(oStream, oFile.fileSize);
+ // read as binary
+ var oBinStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ oBinStream.setInputStream(oBufStream);
+ // return it
+ return oBinStream;
+ }
+ return null;
+ }
+
+ // method specifications
+
+ function read() {
+ var file = getJunkStatFile();
+
+ // does the file exist?
+ Assert.ok(file.exists());
+
+ var fileStream = getBinStream(file);
+
+ // check magic number
+ var iMagicNumber = fileStream.read32();
+ Assert.equal(iMagicNumber, 0xfeedface);
+
+ // get ham'n'spam numbers
+ this.mGoodMessages = fileStream.read32();
+ this.mJunkMessages = fileStream.read32();
+
+ // Read good tokens
+ this.mGoodTokens = fileStream.read32();
+ var iRefCount, iTokenLen, sToken;
+ for (let i = 0; i < this.mGoodTokens; ++i) {
+ iRefCount = fileStream.read32();
+ iTokenLen = fileStream.read32();
+ sToken = fileStream.readBytes(iTokenLen);
+ this.mGoodCounts[sToken] = iRefCount;
+ }
+
+ // we have no further good tokens, so read junk tokens
+ this.mJunkTokens = fileStream.read32();
+ for (let i = 0; i < this.mJunkTokens; i++) {
+ // read token data
+ iRefCount = fileStream.read32();
+ iTokenLen = fileStream.read32();
+ sToken = fileStream.readBytes(iTokenLen);
+ this.mJunkCounts[sToken] = iRefCount;
+ }
+ }
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_bug228675.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_bug228675.js
new file mode 100644
index 0000000000..40180006d7
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_bug228675.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+// tests reduction in size of training.dat
+
+// main setup
+
+/* import-globals-from resources/trainingfile.js */
+load("resources/trainingfile.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// before shrink, the trained messages have 76 tokens. Force shrink.
+Services.prefs.setIntPref("mailnews.bayesian_spam_filter.junk_maxtokens", 75);
+
+// local constants
+var kUnclassified = MailServices.junk.UNCLASSIFIED;
+var kJunk = MailServices.junk.JUNK;
+var kGood = MailServices.junk.GOOD;
+
+var emails = [
+ "ham1.eml",
+ "ham2.eml",
+ "spam1.eml",
+ "spam2.eml",
+ "spam3.eml",
+ "spam4.eml",
+];
+var classifications = [kGood, kGood, kJunk, kJunk, kJunk, kJunk];
+var trainingData;
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ MailServices.junk.resetTrainingData();
+
+ do_test_pending();
+
+ var email = emails.shift();
+ var classification = classifications.shift();
+ // additional calls to setMessageClassifiaction are done in the callback
+ MailServices.junk.setMessageClassification(
+ getSpec(email),
+ kUnclassified,
+ classification,
+ null,
+ doTestingListener
+ );
+}
+
+var doTestingListener = {
+ onMessageClassified(aMsgURI, aClassification, aJunkPercent) {
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+ var email = emails.shift();
+ var classification = classifications.shift();
+ if (email) {
+ MailServices.junk.setMessageClassification(
+ getSpec(email),
+ kUnclassified,
+ classification,
+ null,
+ doTestingListener
+ );
+ return;
+ }
+
+ // all done classifying, time to test
+ MailServices.junk.shutdown(); // just flushes training.dat
+ trainingData = new TrainingData();
+ trainingData.read();
+
+ /*
+ // List training.dat information for debug
+ dump("training.data results: goodMessages=" + trainingData.mGoodMessages
+ + " junkMessages = " + trainingData.mJunkMessages
+ + " goodTokens = " + trainingData.mGoodTokens
+ + " junkTokens = " + trainingData.mJunkTokens
+ + "\n");
+ print("Good counts");
+ for (var token in trainingData.mGoodCounts)
+ dump("count: " + trainingData.mGoodCounts[token] + " token: " + token + "\n");
+ print("Junk Counts");
+ for (var token in trainingData.mJunkCounts)
+ dump("count: " + trainingData.mJunkCounts[token] + " token: " + token + "\n");
+ */
+
+ /* Selected pre-shrink counts after training
+ training.data results: goodMessages=2 junkMessages = 4 tokens = 78
+ Good counts
+ count: 1 token: subject:report
+ count: 2 token: important
+ count: 2 token: to:careful reader <reader@example.org>
+
+ Junk Counts
+ count: 3 token: make
+ count: 4 token: money
+ count: 4 token: to:careful reader <reader@example.org>
+ count: 2 token: money!
+ */
+
+ // Shrinking divides all counts by two. In comments, I show the
+ // calculation for each test, (pre-shrink count)/2.
+
+ Assert.equal(trainingData.mGoodMessages, 1); // 2/2
+ Assert.equal(trainingData.mJunkMessages, 2); // 4/2
+ checkToken("money", 0, 2); // (0/2, 4/2)
+ checkToken("subject:report", 0, 0); // (1/2, 0/2)
+ checkToken("to:careful reader <reader@example.org>", 1, 2); // (2/2, 4/2)
+ checkToken("make", 0, 1); // (0/2, 3/2)
+ checkToken("important", 1, 0); // (2/2, 0/2)
+
+ do_test_finished();
+ },
+};
+
+// helper functions
+
+function checkToken(aToken, aGoodCount, aJunkCount) {
+ print(" checking " + aToken);
+ var goodCount = trainingData.mGoodCounts[aToken];
+ var junkCount = trainingData.mJunkCounts[aToken];
+ if (!goodCount) {
+ goodCount = 0;
+ }
+ if (!junkCount) {
+ junkCount = 0;
+ }
+ Assert.equal(goodCount, aGoodCount);
+ Assert.equal(junkCount, aJunkCount);
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_customTokenization.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_customTokenization.js
new file mode 100644
index 0000000000..222a9557d8
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_customTokenization.js
@@ -0,0 +1,197 @@
+/* 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/. */
+
+// Tests use of custom tokenization, originally introduced in bug 476389
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// command functions for test data
+var kTrain = 0; // train a file
+var kTest = 1; // test headers returned from detail
+var kSetup = 2; // run a setup function
+
+// trait ids
+var kProArray = [3];
+var kAntiArray = [4];
+
+var gTest; // currently active test
+
+// The tests array defines the tests to attempt.
+
+var tests = [
+ // test a few tokens using defaults
+ {
+ command: kTrain,
+ fileName: "tokenTest.eml",
+ },
+ {
+ command: kTest,
+ fileName: "tokenTest.eml",
+ tokens: ["important", "subject:eat", "message-id:14159", "http://www"],
+ nottokens: ["idonotexist", "subject:to"],
+ },
+
+ // enable received, disable message-id
+ // switch tokenization of body to catch full urls (no "." delimiter)
+ // enable sender, keeping full value
+ {
+ command: kSetup,
+ operation() {
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.tokenizeheader.received",
+ "standard"
+ );
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.tokenizeheader.message-id",
+ "false"
+ );
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.body_delimiters",
+ " \t\r\n\v"
+ );
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.tokenizeheader.sender",
+ "full"
+ );
+ },
+ },
+ {
+ command: kTrain,
+ fileName: "tokenTest.eml",
+ },
+ {
+ command: kTest,
+ fileName: "tokenTest.eml",
+ tokens: [
+ "important",
+ "subject:eat",
+ "received:reader@example",
+ "skip:h 20",
+ "sender:bugzilla test setup <noreply@example.org>",
+ "received:<someone@example",
+ ],
+ nottokens: ["message-id:14159", "http://www"],
+ },
+
+ // increase the length of the maximum token to catch full URLs in the body
+ // add <>;, remove . from standard header delimiters to better capture emails
+ // use custom delimiters on sender, without "." or "<>"
+ {
+ command: kSetup,
+ operation() {
+ Services.prefs.setIntPref(
+ "mailnews.bayesian_spam_filter.maxlengthfortoken",
+ 50
+ );
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.header_delimiters",
+ " ;<>\t\r\n\v"
+ );
+ Services.prefs.setCharPref(
+ "mailnews.bayesian_spam_filter.tokenizeheader.sender",
+ " \t\r\n\v"
+ );
+ },
+ },
+ {
+ command: kTrain,
+ fileName: "tokenTest.eml",
+ },
+ {
+ command: kTest,
+ fileName: "tokenTest.eml",
+ tokens: [
+ "received:someone@example.com",
+ "http://www.example.org",
+ "received:reader@example.org",
+ "sender:<noreply@example.org>",
+ ],
+ nottokens: ["skip:h 20", "received:<someone@example"],
+ },
+];
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+
+ startCommand();
+}
+
+var listener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ startCommand();
+ },
+
+ onMessageTraitDetails(
+ aMsgURI,
+ aProTrait,
+ aTokenString,
+ aTokenPercents,
+ aRunningPercents
+ ) {
+ print("Details for " + aMsgURI);
+ for (let i = 0; i < aTokenString.length; i++) {
+ print("Token " + aTokenString[i]);
+ }
+
+ // we should have these tokens
+ for (let value of gTest.tokens) {
+ print("We should have '" + value + "'? ");
+ Assert.ok(aTokenString.includes(value));
+ }
+
+ // should not have these tokens
+ for (let value of gTest.nottokens) {
+ print("We should not have '" + value + "'? ");
+ Assert.ok(!aTokenString.includes(value));
+ }
+ startCommand();
+ },
+};
+
+// start the next test command
+function startCommand() {
+ if (!tests.length) {
+ // Do we have more commands?
+ // no, all done
+ do_test_finished();
+ return;
+ }
+
+ gTest = tests.shift();
+ // print("StartCommand command = " + gTest.command + ", remaining tests " + tests.length);
+ switch (gTest.command) {
+ case kTrain:
+ // train message
+
+ MailServices.junk.setMsgTraitClassification(
+ getSpec(gTest.fileName), // aMsgURI
+ [], // aOldTraits
+ kProArray, // aNewTraits
+ listener
+ ); // [optional] in nsIMsgTraitClassificationListener aTraitListener
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ break;
+
+ case kTest:
+ // test headers from detail message
+ MailServices.junk.detailMessage(
+ getSpec(gTest.fileName), // in string aMsgURI
+ kProArray[0], // proTrait
+ kAntiArray[0], // antiTrait
+ listener
+ ); // in nsIMsgTraitDetailListener aDetailListener
+ break;
+
+ case kSetup:
+ gTest.operation();
+ startCommand();
+ break;
+ }
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_junkAsTraits.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_junkAsTraits.js
new file mode 100644
index 0000000000..a1800b93e7
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_junkAsTraits.js
@@ -0,0 +1,574 @@
+/* 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/. */
+
+// tests calls to the bayesian filter plugin to train, classify, and forget
+// messages using both the older junk-oriented calls, as well as the newer
+// trait-oriented calls. Only a single trait is tested. The main intent of
+// these tests is to demonstrate that both the old junk-oriented calls and the
+// new trait-oriented calls give the same results on junk processing.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// local constants
+var kUnclassified = MailServices.junk.UNCLASSIFIED;
+var kJunk = MailServices.junk.JUNK;
+var kGood = MailServices.junk.GOOD;
+var kJunkTrait = MailServices.junk.JUNK_TRAIT;
+var kGoodTrait = MailServices.junk.GOOD_TRAIT;
+var kIsHamScore = MailServices.junk.IS_HAM_SCORE;
+var kIsSpamScore = MailServices.junk.IS_SPAM_SCORE;
+
+// command functions for test data
+var kTrainJ = 0; // train using junk method
+var kTrainT = 1; // train using trait method
+var kClassJ = 2; // classify using junk method
+var kClassT = 3; // classify using trait method
+var kForgetJ = 4; // forget training using junk method
+var kForgetT = 5; // forget training using trait method
+var kCounts = 6; // test token and message counts
+
+var gProArray = [],
+ gAntiArray = []; // traits arrays, pro is junk, anti is good
+var gTest; // currently active test
+
+// The tests array defines the tests to attempt. Format of
+// an element "test" of this array (except for kCounts):
+//
+// test.command: function to perform, see definitions above
+// test.fileName: file containing message to test
+// test.junkPercent: sets the classification (for Class or Forget commands)
+// tests the classification (for Class commands)
+// As a special case for the no-training tests, if
+// junkPercent is negative, test its absolute value
+// for percents, but reverse the junk/good classification
+// test.traitListener: should we use the trait listener call?
+// test.junkListener: should we use the junk listener call?
+
+var tests = [
+ // test the trait-based calls. We mix trait listeners, junk listeners,
+ // and both
+
+ {
+ // with no training, percents is 50 - but classifies as junk
+ command: kClassT,
+ fileName: "ham1.eml",
+ junkPercent: -50, // negative means classifies as junk
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // train 1 ham message
+ command: kTrainT,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // with ham but no spam training, percents are 0 and classifies as ham
+ command: kClassT,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // train 1 spam message
+ command: kTrainT,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: false,
+ },
+ {
+ // the trained messages will classify at 0 and 100
+ command: kClassT,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: false,
+ },
+ {
+ // ham2, spam2, spam4 give partial percents, but still ham
+ command: kClassT,
+ fileName: "ham2.eml",
+ junkPercent: 8,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "spam2.eml",
+ junkPercent: 81,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "spam4.eml",
+ junkPercent: 81,
+ traitListener: true,
+ junkListener: false,
+ },
+ {
+ // spam3 evaluates to spam
+ command: kClassT,
+ fileName: "spam3.eml",
+ junkPercent: 98,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ // train ham2, then test percents of 0 (clearly good)
+ command: kTrainT,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ // forget ham2, percents should return to partial value
+ command: kForgetT,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "ham2.eml",
+ junkPercent: 8,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ // train, classify, forget, reclassify spam4
+ command: kTrainT,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ command: kClassT,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ command: kCounts,
+ tokenCount: 66, // count of tokens in the corpus
+ junkCount: 2, // count of junk messages in the corpus
+ goodCount: 1, // count of good messages in the corpus
+ },
+ {
+ command: kForgetT,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: false,
+ },
+ {
+ command: kClassT,
+ fileName: "spam4.eml",
+ junkPercent: 81,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ // forget ham1 and spam1 to empty training
+ command: kForgetT,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: true,
+ junkListener: true,
+ },
+ {
+ command: kForgetT,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: true,
+ junkListener: true,
+ },
+ // repeat the whole sequence using the junk calls
+ {
+ // train 1 ham and 1 spam message
+ command: kTrainJ,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kTrainJ,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // the trained messages will classify at 0 and 100
+ command: kClassJ,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // ham2, spam2, spam4 give partial percents, but still ham
+ command: kClassJ,
+ fileName: "ham2.eml",
+ junkPercent: 8,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "spam2.eml",
+ junkPercent: 81,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "spam4.eml",
+ junkPercent: 81,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // spam3 evaluates to spam
+ command: kClassJ,
+ fileName: "spam3.eml",
+ junkPercent: 98,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // train ham2, then test percents of 0 (clearly good)
+ command: kTrainJ,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // forget ham2, percents should return to partial value
+ command: kForgetJ,
+ fileName: "ham2.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "ham2.eml",
+ junkPercent: 8,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // train, classify, forget, reclassify spam4
+ command: kTrainJ,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kForgetJ,
+ fileName: "spam4.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kClassJ,
+ fileName: "spam4.eml",
+ junkPercent: 81,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ // forget ham1 and spam1 to be empty
+ command: kForgetJ,
+ fileName: "ham1.eml",
+ junkPercent: 0,
+ traitListener: false,
+ junkListener: true,
+ },
+ {
+ command: kForgetJ,
+ fileName: "spam1.eml",
+ junkPercent: 100,
+ traitListener: false,
+ junkListener: true,
+ },
+];
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+
+ // setup pro/anti arrays as junk/good
+ gProArray.push(kJunkTrait);
+ gAntiArray.push(kGoodTrait);
+
+ startCommand();
+}
+
+var junkListener = {
+ // nsIJunkMailClassificationListener implementation
+ onMessageClassified(aMsgURI, aClassification, aJunkPercent) {
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+ // print("Message URI is " + aMsgURI);
+ // print("Junk percent is " + aJunkPercent);
+ // print("Classification is " + aClassification);
+ var command = gTest.command;
+ var junkPercent = gTest.junkPercent;
+ // file returned correctly
+ Assert.equal(getSpec(gTest.fileName), aMsgURI);
+
+ // checks of aClassification
+
+ // forget returns unclassified
+ if (command == kForgetJ || command == kForgetT) {
+ Assert.equal(aClassification, kUnclassified);
+ } else {
+ // classification or train should return an actual classification
+ // check junk classification set by default cutoff of 90
+ var isGood = Math.abs(junkPercent) < 90;
+ if (junkPercent < 0) {
+ isGood = !isGood;
+ }
+ Assert.equal(aClassification, isGood ? kGood : kJunk);
+ }
+
+ // checks of aJunkPercent
+
+ if (command == kClassJ || command == kClassT) {
+ // classify returns the actual junk percents
+ Assert.equal(Math.abs(junkPercent), aJunkPercent);
+ } else if (command == kTrainJ || command == kTrainT) {
+ // train returns the ham and spam limits
+ Assert.equal(aJunkPercent, junkPercent < 90 ? kIsHamScore : kIsSpamScore);
+ } else {
+ // Forget always returns 0.
+ Assert.equal(aJunkPercent, 0);
+ }
+
+ // if the current test includes a trait listener, it will
+ // run next, so we defer to it for starting the next command
+ if (gTest.traitListener) {
+ return;
+ }
+ startCommand();
+ },
+};
+
+var traitListener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+ // print("(Trait Listener)Message URI is " + aMsgURI);
+ // print("(Trait Listener)Junk percent is " + aPercents);
+ var command = gTest.command;
+ var junkPercent = gTest.junkPercent;
+ // print("command, junkPercent is " + command + " , " + junkPercent);
+
+ Assert.equal(getSpec(gTest.fileName), aMsgURI);
+
+ // checks of aPercents
+
+ if (command == kForgetJ || command == kForgetT) {
+ // "forgets" with null newClassifications does not return a percent
+ Assert.equal(aPercents.length, 0);
+ } else {
+ var percent = aPercents[0];
+ // print("Percent is " + percent);
+ if (command == kClassJ || command == kClassT) {
+ // Classify returns actual percents
+ Assert.equal(percent, junkPercent);
+ } else {
+ // Train simply returns 100.
+ Assert.equal(percent, 100);
+ }
+ }
+
+ // checks of aTraits
+
+ if (command == kForgetJ || command == kForgetT) {
+ // "forgets" with null newClassifications does not return a
+ // classification
+ Assert.equal(aTraits.length, 0);
+ } else if (command == kClassJ || command == kClassT) {
+ // classification just returns the tested "Pro" trait (junk)
+ let trait = aTraits[0];
+ Assert.equal(trait, kJunkTrait);
+ } else {
+ // training returns the actual trait trained
+ let trait = aTraits[0];
+ Assert.equal(trait, junkPercent < 90 ? kGoodTrait : kJunkTrait);
+ }
+
+ // All done, start the next test
+ startCommand();
+ },
+};
+
+// start the next test command
+function startCommand() {
+ if (!tests.length) {
+ // Do we have more commands?
+ // no, all done
+ do_test_finished();
+ return;
+ }
+
+ gTest = tests.shift();
+ print(
+ "StartCommand command = " +
+ gTest.command +
+ ", remaining tests " +
+ tests.length
+ );
+ var command = gTest.command;
+ var junkPercent = gTest.junkPercent;
+ var fileName = gTest.fileName;
+ var tListener = gTest.traitListener;
+ var jListener = gTest.junkListener;
+ switch (command) {
+ case kTrainJ:
+ // train message using junk call
+ MailServices.junk.setMessageClassification(
+ getSpec(fileName), // in string aMsgURI
+ null, // in nsMsgJunkStatus aOldUserClassification
+ junkPercent == kIsHamScore ? kGood : kJunk, // in nsMsgJunkStatus aNewClassification
+ null, // in nsIMsgWindow aMsgWindow
+ junkListener
+ ); // in nsIJunkMailClassificationListener aListener);
+ break;
+
+ case kTrainT:
+ // train message using trait call
+ MailServices.junk.setMsgTraitClassification(
+ getSpec(fileName), // aMsgURI
+ [], // aOldTraits
+ junkPercent == kIsSpamScore ? gProArray : gAntiArray, // aNewTraits
+ tListener ? traitListener : null, // aTraitListener
+ null, // aMsgWindow
+ jListener ? junkListener : null
+ );
+ break;
+
+ case kClassJ:
+ // classify message using junk call
+ MailServices.junk.classifyMessage(
+ getSpec(fileName), // in string aMsgURI
+ null, // in nsIMsgWindow aMsgWindow
+ junkListener
+ ); // in nsIJunkMailClassificationListener aListener
+ break;
+
+ case kClassT:
+ // classify message using trait call
+ MailServices.junk.classifyTraitsInMessage(
+ getSpec(fileName), // in string aMsgURI
+ gProArray, // in array aProTraits,
+ gAntiArray, // in array aAntiTraits
+ tListener ? traitListener : null, // in nsIMsgTraitClassificationListener aTraitListener
+ null, // in nsIMsgWindow aMsgWindow
+ jListener ? junkListener : null
+ ); // in nsIJunkMailClassificationListener aJunkListener
+ break;
+
+ case kForgetJ:
+ // forget message using junk call
+ MailServices.junk.setMessageClassification(
+ getSpec(fileName), // in string aMsgURI
+ junkPercent == kIsHamScore ? kGood : kJunk, // in nsMsgJunkStatus aOldUserClassification
+ null, // in nsMsgJunkStatus aNewClassification,
+ null, // in nsIMsgWindow aMsgWindow,
+ junkListener
+ ); // in nsIJunkMailClassificationListener aListener
+ break;
+
+ case kForgetT:
+ // forget message using trait call
+ MailServices.junk.setMsgTraitClassification(
+ getSpec(fileName), // in string aMsgURI
+ junkPercent == kIsSpamScore ? gProArray : gAntiArray, // in array aOldTraits
+ [], // in array aNewTraits
+ tListener ? traitListener : null, // in nsIMsgTraitClassificationListener aTraitListener
+ null, // in nsIMsgWindow aMsgWindow
+ jListener ? junkListener : null
+ ); // in nsIJunkMailClassificationListener aJunkListener
+ break;
+
+ case kCounts:
+ // test counts
+ let msgCount = {};
+ let nsIMsgCorpus = MailServices.junk.QueryInterface(Ci.nsIMsgCorpus);
+ let tokenCount = nsIMsgCorpus.corpusCounts(null, {});
+ nsIMsgCorpus.corpusCounts(kJunkTrait, msgCount);
+ let junkCount = msgCount.value;
+ nsIMsgCorpus.corpusCounts(kGoodTrait, msgCount);
+ let goodCount = msgCount.value;
+ print(
+ "tokenCount, junkCount, goodCount is " + tokenCount,
+ junkCount,
+ goodCount
+ );
+ Assert.equal(tokenCount, gTest.tokenCount);
+ Assert.equal(junkCount, gTest.junkCount);
+ Assert.equal(goodCount, gTest.goodCount);
+ do_timeout(0, startCommand);
+ break;
+ }
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_msgCorpus.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_msgCorpus.js
new file mode 100644
index 0000000000..0c39215fcb
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_msgCorpus.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+// Tests corpus management functions using nsIMsgCorpus
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var msgCorpus = MailServices.junk.QueryInterface(Ci.nsIMsgCorpus);
+
+// tokens found in the test corpus file. trait 1001 was trained with
+// 2 messages, and trait 1003 with 1.
+
+var tokenData = [
+ // [traitid, count, token]
+ [1001, 0, "iDoNotExist"],
+ [1001, 1, "linecount"],
+ [1001, 2, "envelope-to:kenttest@caspia.com"],
+ [1003, 0, "iAlsoDoNotExist"],
+ [1003, 0, "isjunk"], // in 1001 but not 1003
+ [1003, 1, "linecount"],
+ [1003, 1, "subject:test"],
+ [1003, 1, "envelope-to:kenttest@caspia.com"],
+];
+
+// list of tests
+
+var gTests = [
+ // train two different combinations of messages
+ function checkLoadOnce() {
+ let fileName = "msgCorpus.dat";
+ let file = do_get_file("resources/" + fileName);
+ msgCorpus.updateData(file, true);
+
+ // check message counts
+ let messageCount = {};
+ msgCorpus.corpusCounts(1001, messageCount);
+ Assert.equal(2, messageCount.value);
+ msgCorpus.corpusCounts(1003, messageCount);
+ Assert.equal(1, messageCount.value);
+
+ for (let i = 0; i < tokenData.length; i++) {
+ let id = tokenData[i][0];
+ let count = tokenData[i][1];
+ let word = tokenData[i][2];
+ Assert.equal(count, msgCorpus.getTokenCount(word, id));
+ }
+ },
+ function checkLoadTwice() {
+ let fileName = "msgCorpus.dat";
+ let file = do_get_file("resources/" + fileName);
+ msgCorpus.updateData(file, true);
+
+ // check message counts
+ let messageCount = {};
+ msgCorpus.corpusCounts(1001, messageCount);
+ Assert.equal(4, messageCount.value);
+ msgCorpus.corpusCounts(1003, messageCount);
+ Assert.equal(2, messageCount.value);
+
+ for (let i = 0; i < tokenData.length; i++) {
+ let id = tokenData[i][0];
+ let count = 2 * tokenData[i][1];
+ let word = tokenData[i][2];
+ Assert.equal(count, msgCorpus.getTokenCount(word, id));
+ }
+ },
+ // remap the ids in the file to different local ids
+ function loadWithRemap() {
+ let fileName = "msgCorpus.dat";
+ let file = do_get_file("resources/" + fileName);
+ msgCorpus.updateData(file, true, [1001, 1003], [1, 3]);
+
+ for (let i = 0; i < tokenData.length; i++) {
+ let id = tokenData[i][0] - 1000;
+ let count = tokenData[i][1];
+ let word = tokenData[i][2];
+ Assert.equal(count, msgCorpus.getTokenCount(word, id));
+ }
+ },
+ // test removing data
+ function checkRemove() {
+ let fileName = "msgCorpus.dat";
+ let file = do_get_file("resources/" + fileName);
+ msgCorpus.updateData(file, false);
+
+ // check message counts
+ let messageCount = {};
+ msgCorpus.corpusCounts(1001, messageCount);
+ Assert.equal(2, messageCount.value);
+ msgCorpus.corpusCounts(1003, messageCount);
+ Assert.equal(1, messageCount.value);
+
+ for (let i = 0; i < tokenData.length; i++) {
+ let id = tokenData[i][0];
+ let count = tokenData[i][1];
+ let word = tokenData[i][2];
+ Assert.equal(count, msgCorpus.getTokenCount(word, id));
+ }
+ },
+ // test clearing a trait
+ function checkClear() {
+ let messageCountObject = {};
+ /*
+ msgCorpus.corpusCounts(1001, messageCountObject);
+ let v1001 = messageCountObject.value;
+ msgCorpus.corpusCounts(1003, messageCountObject);
+ let v1003 = messageCountObject.value;
+ dump("pre-clear value " + v1001 + " " + v1003 + "\n");
+ /**/
+ msgCorpus.clearTrait(1001);
+ // check that the message count is zero
+ msgCorpus.corpusCounts(1001, messageCountObject);
+ Assert.equal(0, messageCountObject.value);
+ // but the other trait should still have counts
+ msgCorpus.corpusCounts(1003, messageCountObject);
+ Assert.equal(1, messageCountObject.value);
+ // check that token count was cleared
+ for (let i = 0; i < tokenData.length; i++) {
+ let id = tokenData[i][0];
+ let count = tokenData[i][1];
+ let word = tokenData[i][2];
+ Assert.equal(id == 1001 ? 0 : count, msgCorpus.getTokenCount(word, id));
+ }
+ },
+];
+
+// main test
+function run_test() {
+ do_test_pending();
+ while (true) {
+ if (!gTests.length) {
+ // Do we have more commands?
+ // no, all done
+ do_test_finished();
+ return;
+ }
+
+ let test = gTests.shift();
+ test();
+ }
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traitAliases.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traitAliases.js
new file mode 100644
index 0000000000..41a9f22a9b
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traitAliases.js
@@ -0,0 +1,172 @@
+/* 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/. */
+
+// Tests bayes trait analysis with aliases. Adapted from test_traits.js
+
+/*
+ * These tests rely on data stored in a file, with the same format as traits.dat,
+ * that was trained in the following manner. There are two training messages,
+ * included here as files aliases1.eml and aliases2.eml Aliases.dat was trained on
+ * each of these messages, for different trait indices, as follows, with
+ * columns showing the training count for each trait index:
+ *
+ * file count(1001) count(1005) count(1007) count(1009)
+ *
+ * aliases1.eml 1 0 2 0
+ * aliases2.eml 0 1 0 1
+ *
+ * There is also a third email file, aliases3.eml, which combines tokens
+ * from aliases1.eml and aliases2.eml
+ *
+ * The goal here is to demonstrate that traits 1001 and 1007, and traits
+ * 1005 and 1009, can be combined using aliases. We classify messages with
+ * trait 1001 as the PRO trait, and 1005 as the ANTI trait.
+ *
+ * With these characteristics, I've run a trait analysis without aliases, and
+ * determined that the following is the correct percentage results from the
+ * analysis for each message. "Train11" means that the training was 1 pro count
+ * from aliases1.eml, and 1 anti count from alias2.eml. "Train32" is 3 pro counts,
+ * and 2 anti counts.
+ *
+ * percentage
+ * file Train11 Train32
+ *
+ * alias1.eml 92 98
+ * alias2.eml 8 3
+ * alias3.eml 50 53
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var traitService = Cc["@mozilla.org/msg-trait-service;1"].getService(
+ Ci.nsIMsgTraitService
+);
+var kProTrait = 1001;
+var kAntiTrait = 1005;
+var kProAlias = 1007;
+var kAntiAlias = 1009;
+
+var gTest; // currently active test
+
+// The tests array defines the tests to attempt. Format of
+// an element "test" of this array:
+//
+// test.fileName: file containing message to test
+// test.proAliases: array of aliases for the pro trait
+// test.antiAliases: array of aliases for the anti trait
+// test.percent: expected results from the classifier
+
+var tests = [
+ {
+ fileName: "aliases1.eml",
+ proAliases: [],
+ antiAliases: [],
+ percent: 92,
+ },
+ {
+ fileName: "aliases2.eml",
+ proAliases: [],
+ antiAliases: [],
+ percent: 8,
+ },
+ {
+ fileName: "aliases3.eml",
+ proAliases: [],
+ antiAliases: [],
+ percent: 50,
+ },
+ {
+ fileName: "aliases1.eml",
+ proAliases: [kProAlias],
+ antiAliases: [kAntiAlias],
+ percent: 98,
+ },
+ {
+ fileName: "aliases2.eml",
+ proAliases: [kProAlias],
+ antiAliases: [kAntiAlias],
+ percent: 3,
+ },
+ {
+ fileName: "aliases3.eml",
+ proAliases: [kProAlias],
+ antiAliases: [kAntiAlias],
+ percent: 53,
+ },
+];
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // load in the aliases trait testing file
+ MailServices.junk
+ .QueryInterface(Ci.nsIMsgCorpus)
+ .updateData(do_get_file("resources/aliases.dat"), true);
+ do_test_pending();
+
+ startCommand();
+}
+
+var listener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ // print("Message URI is " + aMsgURI);
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+
+ Assert.equal(aPercents[0], gTest.percent);
+ // All done, start the next test
+ startCommand();
+ },
+};
+
+// start the next test command
+function startCommand() {
+ if (!tests.length) {
+ // Do we have more commands?
+ // no, all done
+ do_test_finished();
+ return;
+ }
+
+ gTest = tests.shift();
+
+ // classify message
+ var antiArray = [kAntiTrait];
+ var proArray = [kProTrait];
+
+ // remove any existing aliases
+ let proAliases = traitService.getAliases(kProTrait);
+ let antiAliases = traitService.getAliases(kAntiTrait);
+ let proAlias;
+ let antiAlias;
+ while ((proAlias = proAliases.pop())) {
+ traitService.removeAlias(kProTrait, proAlias);
+ }
+ while ((antiAlias = antiAliases.pop())) {
+ traitService.removeAlias(kAntiTrait, antiAlias);
+ }
+
+ // add new aliases
+ while ((proAlias = gTest.proAliases.pop())) {
+ traitService.addAlias(kProTrait, proAlias);
+ }
+ while ((antiAlias = gTest.antiAliases.pop())) {
+ traitService.addAlias(kAntiTrait, antiAlias);
+ }
+
+ MailServices.junk.classifyTraitsInMessage(
+ getSpec(gTest.fileName), // in string aMsgURI
+ proArray, // in array aProTraits,
+ antiArray, // in array aAntiTraits
+ listener
+ ); // in nsIMsgTraitClassificationListener aTraitListener
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traits.js b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traits.js
new file mode 100644
index 0000000000..b005db72cc
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/test_traits.js
@@ -0,0 +1,287 @@
+/* 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/. */
+
+// Tests bayes trait analysis
+
+// I make this an instance so that I know I can reset and get
+// a completely new component. Should be getService in production code.
+var nsIJunkMailPlugin = Cc[
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter"
+].createInstance(Ci.nsIJunkMailPlugin);
+
+// command functions for test data
+var kTrain = 0; // train a file as a trait
+var kClass = 1; // classify files with traits
+var kReset = 2; // reload plugin, reading in data from disk
+var kDetail = 3; // test details
+
+var gTest; // currently active test
+
+// The tests array defines the tests to attempt. Format of
+// an element "test" of this array:
+//
+// test.command: function to perform, see definitions above
+// test.fileName: file(s) containing message(s) to test
+// test.traitIds: Array of traits to train (kTrain) or pro trait (kClass)
+// test.traitAntiIds: Array of anti traits to classify
+// test.percents: array of arrays (1 per message, 1 per trait) of
+// expected results from the classifier
+
+var tests = [
+ // train two different combinations of messages
+ {
+ command: kTrain,
+ fileName: "ham1.eml",
+ traitIds: [3, 6],
+ },
+ {
+ command: kTrain,
+ fileName: "spam1.eml",
+ traitIds: [4],
+ },
+ {
+ command: kTrain,
+ fileName: "spam4.eml",
+ traitIds: [5],
+ },
+ // test the message classifications using both singular and plural classifier
+ {
+ command: kClass,
+ fileName: "ham1.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ // ham1 is trained "anti" for first test, "pro" for second
+ percents: [[0, 100]],
+ },
+ {
+ command: kClass,
+ fileName: "ham2.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ // these are partial percents for an untrained message. ham2 is similar to ham1
+ percents: [[8, 95]],
+ },
+ {
+ command: kDetail,
+ fileName: "spam2.eml",
+ traitIds: [4],
+ traitAntiIds: [3],
+ percents: {
+ lots: 84,
+ money: 84,
+ make: 84,
+ your: 16,
+ },
+ runnings: [84, 92, 95, 81],
+ },
+ {
+ command: kClass,
+ fileName: "spam1.eml,spam2.eml,spam3.eml,spam4.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ // spam1 trained as "pro" for first pro/anti pair
+ // spam4 trained as "anti" for second pro/anti pair
+ // others are partials
+ percents: [
+ [100, 50],
+ [81, 0],
+ [98, 50],
+ [81, 0],
+ ],
+ },
+ // reset the plugin, read in data, and retest the classification
+ // this tests the trait file writing
+ {
+ command: kReset,
+ },
+ {
+ command: kClass,
+ fileName: "ham1.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ percents: [[0, 100]],
+ },
+ {
+ command: kClass,
+ fileName: "ham2.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ percents: [[8, 95]],
+ },
+ {
+ command: kClass,
+ fileName: "spam1.eml,spam2.eml,spam3.eml,spam4.eml",
+ traitIds: [4, 6],
+ traitAntiIds: [3, 5],
+ percents: [
+ [100, 50],
+ [81, 0],
+ [98, 50],
+ [81, 0],
+ ],
+ },
+];
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+
+ startCommand();
+}
+
+var listener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ // print("Message URI is " + aMsgURI);
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+
+ switch (gTest.command) {
+ case kClass:
+ Assert.equal(gTest.files[gTest.currentIndex], aMsgURI);
+ var currentPercents = gTest.percents[gTest.currentIndex];
+ for (let i = 0; i < currentPercents.length; i++) {
+ // print("expecting score " + currentPercents[i] +
+ // " got score " + aPercents[i]);
+ Assert.equal(currentPercents[i], aPercents[i]);
+ }
+ gTest.currentIndex++;
+ break;
+
+ case kTrain: // We tested this some in test_junkAsTraits.js, so let's not bother
+ default:
+ break;
+ }
+ if (!--gTest.callbacks) {
+ // All done, start the next test
+ startCommand();
+ }
+ },
+ onMessageTraitDetails(
+ aMsgURI,
+ aProTrait,
+ aTokenString,
+ aTokenPercents,
+ aRunningPercents
+ ) {
+ print("Details for " + aMsgURI);
+ for (let i = 0; i < aTokenString.length; i++) {
+ print(
+ "Percent " +
+ aTokenPercents[i] +
+ " Running " +
+ aRunningPercents[i] +
+ " Token " +
+ aTokenString[i]
+ );
+ Assert.ok(aTokenString[i] in gTest.percents);
+
+ Assert.equal(gTest.percents[aTokenString[i]], aTokenPercents[i]);
+ Assert.equal(gTest.runnings[i], aRunningPercents[i]);
+ delete gTest.percents[aTokenString[i]];
+ }
+ Assert.equal(Object.keys(gTest.percents).length, 0);
+ if (gTest.command == kClass) {
+ gTest.currentIndex++;
+ }
+ startCommand();
+ },
+};
+
+// start the next test command
+function startCommand() {
+ if (!tests.length) {
+ // Do we have more commands?
+ // no, all done
+ do_test_finished();
+ return;
+ }
+
+ gTest = tests.shift();
+ print(
+ "StartCommand command = " +
+ gTest.command +
+ ", remaining tests " +
+ tests.length
+ );
+ switch (gTest.command) {
+ case kTrain: {
+ // train message
+ let proArray = [];
+ for (let i = 0; i < gTest.traitIds.length; i++) {
+ proArray.push(gTest.traitIds[i]);
+ }
+ gTest.callbacks = 1;
+
+ nsIJunkMailPlugin.setMsgTraitClassification(
+ getSpec(gTest.fileName), // aMsgURI
+ [], // aOldTraits
+ proArray, // aNewTraits
+ listener
+ ); // [optional] in nsIMsgTraitClassificationListener aTraitListener
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ break;
+ }
+ case kClass: {
+ // classify message
+ var antiArray = [];
+ let proArray = [];
+ for (let i = 0; i < gTest.traitIds.length; i++) {
+ antiArray.push(gTest.traitAntiIds[i]);
+ proArray.push(gTest.traitIds[i]);
+ }
+ gTest.files = gTest.fileName.split(",");
+ gTest.callbacks = gTest.files.length;
+ gTest.currentIndex = 0;
+ for (let i = 0; i < gTest.files.length; i++) {
+ gTest.files[i] = getSpec(gTest.files[i]);
+ }
+ if (gTest.files.length == 1) {
+ // use the singular classifier
+ nsIJunkMailPlugin.classifyTraitsInMessage(
+ getSpec(gTest.fileName), // in string aMsgURI
+ proArray, // in array aProTraits,
+ antiArray, // in array aAntiTraits
+ listener
+ ); // in nsIMsgTraitClassificationListener aTraitListener
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ } else {
+ // use the plural classifier
+ nsIJunkMailPlugin.classifyTraitsInMessages(
+ gTest.files, // in Array<ACString> aMsgURIs,
+ proArray, // in array aProTraits,
+ antiArray, // in array aAntiTraits
+ listener
+ ); // in nsIMsgTraitClassificationListener aTraitListener
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ }
+ break;
+ }
+ case kDetail:
+ // detail message
+ nsIJunkMailPlugin.detailMessage(
+ getSpec(gTest.fileName), // in string aMsgURI
+ gTest.traitIds[0], // proTrait
+ gTest.traitAntiIds[0], // antiTrait
+ listener
+ ); // in nsIMsgTraitDetailListener aDetailListener
+ break;
+ case kReset:
+ // reload a new nsIJunkMailPlugin, reading file in the process
+ nsIJunkMailPlugin.shutdown(); // writes files
+ nsIJunkMailPlugin = null;
+ nsIJunkMailPlugin = Cc[
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter"
+ ].createInstance(Ci.nsIJunkMailPlugin);
+ // does not do a callback, so we must restart next command
+ startCommand();
+ break;
+ }
+}
diff --git a/comm/mailnews/extensions/bayesian-spam-filter/test/unit/xpcshell.ini b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..86776834ba
--- /dev/null
+++ b/comm/mailnews/extensions/bayesian-spam-filter/test/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = head_bayes.js
+tail =
+support-files = resources/*
+
+[test_bug228675.js]
+[test_customTokenization.js]
+[test_junkAsTraits.js]
+[test_msgCorpus.js]
+[test_traitAliases.js]
+[test_traits.js]
diff --git a/comm/mailnews/extensions/fts3/Normalize.c b/comm/mailnews/extensions/fts3/Normalize.c
new file mode 100644
index 0000000000..984a03bd08
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/Normalize.c
@@ -0,0 +1,2694 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/* THIS FILE IS GENERATED BY generate_table.py. DON'T EDIT THIS */
+
+static const unsigned short gNormalizeTable0040[] = {
+ /* U+0040 */
+ 0x0040, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
+};
+
+static const unsigned short gNormalizeTable0080[] = {
+ /* U+0080 */
+ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
+ 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
+ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
+ 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
+ 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
+ 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x0020, 0x00ae, 0x0020,
+ 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
+ 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
+};
+
+static const unsigned short gNormalizeTable00c0[] = {
+ /* U+00c0 */
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00d7,
+ 0x00f8, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0073,
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
+ 0x00f8, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079,
+};
+
+static const unsigned short gNormalizeTable0100[] = {
+ /* U+0100 */
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0063, 0x0063,
+ 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0064, 0x0064,
+ 0x0111, 0x0111, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0067, 0x0067, 0x0067, 0x0067,
+ 0x0067, 0x0067, 0x0067, 0x0067, 0x0068, 0x0068, 0x0127, 0x0127,
+ 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x0069, 0x0131, 0x0069, 0x0069, 0x006a, 0x006a, 0x006b, 0x006b,
+ 0x0138, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c,
+};
+
+static const unsigned short gNormalizeTable0140[] = {
+ /* U+0140 */
+ 0x006c, 0x0142, 0x0142, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e,
+ 0x006e, 0x02bc, 0x014b, 0x014b, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x0153, 0x0153, 0x0072, 0x0072, 0x0072, 0x0072,
+ 0x0072, 0x0072, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073,
+ 0x0073, 0x0073, 0x0074, 0x0074, 0x0074, 0x0074, 0x0167, 0x0167,
+ 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0075, 0x0075, 0x0075, 0x0075, 0x0077, 0x0077, 0x0079, 0x0079,
+ 0x0079, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x0073,
+};
+
+static const unsigned short gNormalizeTable0180[] = {
+ /* U+0180 */
+ 0x0180, 0x0253, 0x0183, 0x0183, 0x0185, 0x0185, 0x0254, 0x0188,
+ 0x0188, 0x0256, 0x0257, 0x018c, 0x018c, 0x018d, 0x01dd, 0x0259,
+ 0x025b, 0x0192, 0x0192, 0x0260, 0x0263, 0x0195, 0x0269, 0x0268,
+ 0x0199, 0x0199, 0x019a, 0x019b, 0x026f, 0x0272, 0x019e, 0x0275,
+ 0x006f, 0x006f, 0x01a3, 0x01a3, 0x01a5, 0x01a5, 0x0280, 0x01a8,
+ 0x01a8, 0x0283, 0x01aa, 0x01ab, 0x01ad, 0x01ad, 0x0288, 0x0075,
+ 0x0075, 0x028a, 0x028b, 0x01b4, 0x01b4, 0x01b6, 0x01b6, 0x0292,
+ 0x01b9, 0x01b9, 0x01ba, 0x01bb, 0x01bd, 0x01bd, 0x01be, 0x01bf,
+};
+
+static const unsigned short gNormalizeTable01c0[] = {
+ /* U+01c0 */
+ 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0064, 0x0064, 0x0064, 0x006c,
+ 0x006c, 0x006c, 0x006e, 0x006e, 0x006e, 0x0061, 0x0061, 0x0069,
+ 0x0069, 0x006f, 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x01dd, 0x0061, 0x0061,
+ 0x0061, 0x0061, 0x00e6, 0x00e6, 0x01e5, 0x01e5, 0x0067, 0x0067,
+ 0x006b, 0x006b, 0x006f, 0x006f, 0x006f, 0x006f, 0x0292, 0x0292,
+ 0x006a, 0x0064, 0x0064, 0x0064, 0x0067, 0x0067, 0x0195, 0x01bf,
+ 0x006e, 0x006e, 0x0061, 0x0061, 0x00e6, 0x00e6, 0x00f8, 0x00f8,
+};
+
+static const unsigned short gNormalizeTable0200[] = {
+ /* U+0200 */
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0065, 0x0065, 0x0065, 0x0065,
+ 0x0069, 0x0069, 0x0069, 0x0069, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x0072, 0x0072, 0x0072, 0x0072, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0073, 0x0073, 0x0074, 0x0074, 0x021d, 0x021d, 0x0068, 0x0068,
+ 0x019e, 0x0221, 0x0223, 0x0223, 0x0225, 0x0225, 0x0061, 0x0061,
+ 0x0065, 0x0065, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x0079, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
+ 0x0238, 0x0239, 0x2c65, 0x023c, 0x023c, 0x019a, 0x2c66, 0x023f,
+};
+
+static const unsigned short gNormalizeTable0240[] = {
+ /* U+0240 */
+ 0x0240, 0x0242, 0x0242, 0x0180, 0x0289, 0x028c, 0x0247, 0x0247,
+ 0x0249, 0x0249, 0x024b, 0x024b, 0x024d, 0x024d, 0x024f, 0x024f,
+ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
+ 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
+ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
+ 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
+ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
+ 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
+};
+
+static const unsigned short gNormalizeTable0280[] = {
+ /* U+0280 */
+ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
+ 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
+ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
+ 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
+ 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
+ 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
+ 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
+ 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
+};
+
+static const unsigned short gNormalizeTable02c0[] = {
+ /* U+02c0 */
+ 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
+ 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
+ 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
+ 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
+ 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
+ 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
+ 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
+};
+
+static const unsigned short gNormalizeTable0340[] = {
+ /* U+0340 */
+ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x03b9, 0x0346, 0x0347,
+ 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x0020,
+ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
+ 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
+ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
+ 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
+ 0x0371, 0x0371, 0x0373, 0x0373, 0x02b9, 0x0375, 0x0377, 0x0377,
+ 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
+};
+
+static const unsigned short gNormalizeTable0380[] = {
+ /* U+0380 */
+ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x0020, 0x03b1, 0x00b7,
+ 0x03b5, 0x03b7, 0x03b9, 0x038b, 0x03bf, 0x038d, 0x03c5, 0x03c9,
+ 0x03b9, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
+ 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
+ 0x03c0, 0x03c1, 0x03a2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
+ 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
+ 0x03c5, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
+ 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
+};
+
+static const unsigned short gNormalizeTable03c0[] = {
+ /* U+03c0 */
+ 0x03c0, 0x03c1, 0x03c3, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
+ 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03d7,
+ 0x03b2, 0x03b8, 0x03c5, 0x03c5, 0x03c5, 0x03c6, 0x03c0, 0x03d7,
+ 0x03d9, 0x03d9, 0x03db, 0x03db, 0x03dd, 0x03dd, 0x03df, 0x03df,
+ 0x03e1, 0x03e1, 0x03e3, 0x03e3, 0x03e5, 0x03e5, 0x03e7, 0x03e7,
+ 0x03e9, 0x03e9, 0x03eb, 0x03eb, 0x03ed, 0x03ed, 0x03ef, 0x03ef,
+ 0x03ba, 0x03c1, 0x03c3, 0x03f3, 0x03b8, 0x03b5, 0x03f6, 0x03f8,
+ 0x03f8, 0x03c3, 0x03fb, 0x03fb, 0x03fc, 0x037b, 0x037c, 0x037d,
+};
+
+static const unsigned short gNormalizeTable0400[] = {
+ /* U+0400 */
+ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
+ 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
+ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
+ 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
+ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
+ 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+};
+
+static const unsigned short gNormalizeTable0440[] = {
+ /* U+0440 */
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
+ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
+ 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
+ 0x0461, 0x0461, 0x0463, 0x0463, 0x0465, 0x0465, 0x0467, 0x0467,
+ 0x0469, 0x0469, 0x046b, 0x046b, 0x046d, 0x046d, 0x046f, 0x046f,
+ 0x0471, 0x0471, 0x0473, 0x0473, 0x0475, 0x0475, 0x0475, 0x0475,
+ 0x0479, 0x0479, 0x047b, 0x047b, 0x047d, 0x047d, 0x047f, 0x047f,
+};
+
+static const unsigned short gNormalizeTable0480[] = {
+ /* U+0480 */
+ 0x0481, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
+ 0x0488, 0x0489, 0x048b, 0x048b, 0x048d, 0x048d, 0x048f, 0x048f,
+ 0x0491, 0x0491, 0x0493, 0x0493, 0x0495, 0x0495, 0x0497, 0x0497,
+ 0x0499, 0x0499, 0x049b, 0x049b, 0x049d, 0x049d, 0x049f, 0x049f,
+ 0x04a1, 0x04a1, 0x04a3, 0x04a3, 0x04a5, 0x04a5, 0x04a7, 0x04a7,
+ 0x04a9, 0x04a9, 0x04ab, 0x04ab, 0x04ad, 0x04ad, 0x04af, 0x04af,
+ 0x04b1, 0x04b1, 0x04b3, 0x04b3, 0x04b5, 0x04b5, 0x04b7, 0x04b7,
+ 0x04b9, 0x04b9, 0x04bb, 0x04bb, 0x04bd, 0x04bd, 0x04bf, 0x04bf,
+};
+
+static const unsigned short gNormalizeTable04c0[] = {
+ /* U+04c0 */
+ 0x04cf, 0x0436, 0x0436, 0x04c4, 0x04c4, 0x04c6, 0x04c6, 0x04c8,
+ 0x04c8, 0x04ca, 0x04ca, 0x04cc, 0x04cc, 0x04ce, 0x04ce, 0x04cf,
+ 0x0430, 0x0430, 0x0430, 0x0430, 0x04d5, 0x04d5, 0x0435, 0x0435,
+ 0x04d9, 0x04d9, 0x04d9, 0x04d9, 0x0436, 0x0436, 0x0437, 0x0437,
+ 0x04e1, 0x04e1, 0x0438, 0x0438, 0x0438, 0x0438, 0x043e, 0x043e,
+ 0x04e9, 0x04e9, 0x04e9, 0x04e9, 0x044d, 0x044d, 0x0443, 0x0443,
+ 0x0443, 0x0443, 0x0443, 0x0443, 0x0447, 0x0447, 0x04f7, 0x04f7,
+ 0x044b, 0x044b, 0x04fb, 0x04fb, 0x04fd, 0x04fd, 0x04ff, 0x04ff,
+};
+
+static const unsigned short gNormalizeTable0500[] = {
+ /* U+0500 */
+ 0x0501, 0x0501, 0x0503, 0x0503, 0x0505, 0x0505, 0x0507, 0x0507,
+ 0x0509, 0x0509, 0x050b, 0x050b, 0x050d, 0x050d, 0x050f, 0x050f,
+ 0x0511, 0x0511, 0x0513, 0x0513, 0x0515, 0x0515, 0x0517, 0x0517,
+ 0x0519, 0x0519, 0x051b, 0x051b, 0x051d, 0x051d, 0x051f, 0x051f,
+ 0x0521, 0x0521, 0x0523, 0x0523, 0x0525, 0x0525, 0x0526, 0x0527,
+ 0x0528, 0x0529, 0x052a, 0x052b, 0x052c, 0x052d, 0x052e, 0x052f,
+ 0x0530, 0x0561, 0x0562, 0x0563, 0x0564, 0x0565, 0x0566, 0x0567,
+ 0x0568, 0x0569, 0x056a, 0x056b, 0x056c, 0x056d, 0x056e, 0x056f,
+};
+
+static const unsigned short gNormalizeTable0540[] = {
+ /* U+0540 */
+ 0x0570, 0x0571, 0x0572, 0x0573, 0x0574, 0x0575, 0x0576, 0x0577,
+ 0x0578, 0x0579, 0x057a, 0x057b, 0x057c, 0x057d, 0x057e, 0x057f,
+ 0x0580, 0x0581, 0x0582, 0x0583, 0x0584, 0x0585, 0x0586, 0x0557,
+ 0x0558, 0x0559, 0x055a, 0x055b, 0x055c, 0x055d, 0x055e, 0x055f,
+ 0x0560, 0x0561, 0x0562, 0x0563, 0x0564, 0x0565, 0x0566, 0x0567,
+ 0x0568, 0x0569, 0x056a, 0x056b, 0x056c, 0x056d, 0x056e, 0x056f,
+ 0x0570, 0x0571, 0x0572, 0x0573, 0x0574, 0x0575, 0x0576, 0x0577,
+ 0x0578, 0x0579, 0x057a, 0x057b, 0x057c, 0x057d, 0x057e, 0x057f,
+};
+
+static const unsigned short gNormalizeTable0580[] = {
+ /* U+0580 */
+ 0x0580, 0x0581, 0x0582, 0x0583, 0x0584, 0x0585, 0x0586, 0x0565,
+ 0x0588, 0x0589, 0x058a, 0x058b, 0x058c, 0x058d, 0x058e, 0x058f,
+ 0x0590, 0x0591, 0x0592, 0x0593, 0x0594, 0x0595, 0x0596, 0x0597,
+ 0x0598, 0x0599, 0x059a, 0x059b, 0x059c, 0x059d, 0x059e, 0x059f,
+ 0x05a0, 0x05a1, 0x05a2, 0x05a3, 0x05a4, 0x05a5, 0x05a6, 0x05a7,
+ 0x05a8, 0x05a9, 0x05aa, 0x05ab, 0x05ac, 0x05ad, 0x05ae, 0x05af,
+ 0x05b0, 0x05b1, 0x05b2, 0x05b3, 0x05b4, 0x05b5, 0x05b6, 0x05b7,
+ 0x05b8, 0x05b9, 0x05ba, 0x05bb, 0x05bc, 0x05bd, 0x05be, 0x05bf,
+};
+
+static const unsigned short gNormalizeTable0600[] = {
+ /* U+0600 */
+ 0x0600, 0x0601, 0x0602, 0x0603, 0x0604, 0x0605, 0x0606, 0x0607,
+ 0x0608, 0x0609, 0x060a, 0x060b, 0x060c, 0x060d, 0x060e, 0x060f,
+ 0x0610, 0x0611, 0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617,
+ 0x0618, 0x0619, 0x061a, 0x061b, 0x061c, 0x061d, 0x061e, 0x061f,
+ 0x0620, 0x0621, 0x0627, 0x0627, 0x0648, 0x0627, 0x064a, 0x0627,
+ 0x0628, 0x0629, 0x062a, 0x062b, 0x062c, 0x062d, 0x062e, 0x062f,
+ 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637,
+ 0x0638, 0x0639, 0x063a, 0x063b, 0x063c, 0x063d, 0x063e, 0x063f,
+};
+
+static const unsigned short gNormalizeTable0640[] = {
+ /* U+0640 */
+ 0x0640, 0x0641, 0x0642, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647,
+ 0x0648, 0x0649, 0x064a, 0x064b, 0x064c, 0x064d, 0x064e, 0x064f,
+ 0x0650, 0x0651, 0x0652, 0x0653, 0x0654, 0x0655, 0x0656, 0x0657,
+ 0x0658, 0x0659, 0x065a, 0x065b, 0x065c, 0x065d, 0x065e, 0x065f,
+ 0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667,
+ 0x0668, 0x0669, 0x066a, 0x066b, 0x066c, 0x066d, 0x066e, 0x066f,
+ 0x0670, 0x0671, 0x0672, 0x0673, 0x0674, 0x0627, 0x0648, 0x06c7,
+ 0x064a, 0x0679, 0x067a, 0x067b, 0x067c, 0x067d, 0x067e, 0x067f,
+};
+
+static const unsigned short gNormalizeTable06c0[] = {
+ /* U+06c0 */
+ 0x06d5, 0x06c1, 0x06c1, 0x06c3, 0x06c4, 0x06c5, 0x06c6, 0x06c7,
+ 0x06c8, 0x06c9, 0x06ca, 0x06cb, 0x06cc, 0x06cd, 0x06ce, 0x06cf,
+ 0x06d0, 0x06d1, 0x06d2, 0x06d2, 0x06d4, 0x06d5, 0x06d6, 0x06d7,
+ 0x06d8, 0x06d9, 0x06da, 0x06db, 0x06dc, 0x06dd, 0x06de, 0x06df,
+ 0x06e0, 0x06e1, 0x06e2, 0x06e3, 0x06e4, 0x06e5, 0x06e6, 0x06e7,
+ 0x06e8, 0x06e9, 0x06ea, 0x06eb, 0x06ec, 0x06ed, 0x06ee, 0x06ef,
+ 0x06f0, 0x06f1, 0x06f2, 0x06f3, 0x06f4, 0x06f5, 0x06f6, 0x06f7,
+ 0x06f8, 0x06f9, 0x06fa, 0x06fb, 0x06fc, 0x06fd, 0x06fe, 0x06ff,
+};
+
+static const unsigned short gNormalizeTable0900[] = {
+ /* U+0900 */
+ 0x0900, 0x0901, 0x0902, 0x0903, 0x0904, 0x0905, 0x0906, 0x0907,
+ 0x0908, 0x0909, 0x090a, 0x090b, 0x090c, 0x090d, 0x090e, 0x090f,
+ 0x0910, 0x0911, 0x0912, 0x0913, 0x0914, 0x0915, 0x0916, 0x0917,
+ 0x0918, 0x0919, 0x091a, 0x091b, 0x091c, 0x091d, 0x091e, 0x091f,
+ 0x0920, 0x0921, 0x0922, 0x0923, 0x0924, 0x0925, 0x0926, 0x0927,
+ 0x0928, 0x0928, 0x092a, 0x092b, 0x092c, 0x092d, 0x092e, 0x092f,
+ 0x0930, 0x0930, 0x0932, 0x0933, 0x0933, 0x0935, 0x0936, 0x0937,
+ 0x0938, 0x0939, 0x093a, 0x093b, 0x093c, 0x093d, 0x093e, 0x093f,
+};
+
+static const unsigned short gNormalizeTable0940[] = {
+ /* U+0940 */
+ 0x0940, 0x0941, 0x0942, 0x0943, 0x0944, 0x0945, 0x0946, 0x0947,
+ 0x0948, 0x0949, 0x094a, 0x094b, 0x094c, 0x094d, 0x094e, 0x094f,
+ 0x0950, 0x0951, 0x0952, 0x0953, 0x0954, 0x0955, 0x0956, 0x0957,
+ 0x0915, 0x0916, 0x0917, 0x091c, 0x0921, 0x0922, 0x092b, 0x092f,
+ 0x0960, 0x0961, 0x0962, 0x0963, 0x0964, 0x0965, 0x0966, 0x0967,
+ 0x0968, 0x0969, 0x096a, 0x096b, 0x096c, 0x096d, 0x096e, 0x096f,
+ 0x0970, 0x0971, 0x0972, 0x0973, 0x0974, 0x0975, 0x0976, 0x0977,
+ 0x0978, 0x0979, 0x097a, 0x097b, 0x097c, 0x097d, 0x097e, 0x097f,
+};
+
+static const unsigned short gNormalizeTable09c0[] = {
+ /* U+09c0 */
+ 0x09c0, 0x09c1, 0x09c2, 0x09c3, 0x09c4, 0x09c5, 0x09c6, 0x09c7,
+ 0x09c8, 0x09c9, 0x09ca, 0x09c7, 0x09c7, 0x09cd, 0x09ce, 0x09cf,
+ 0x09d0, 0x09d1, 0x09d2, 0x09d3, 0x09d4, 0x09d5, 0x09d6, 0x09d7,
+ 0x09d8, 0x09d9, 0x09da, 0x09db, 0x09a1, 0x09a2, 0x09de, 0x09af,
+ 0x09e0, 0x09e1, 0x09e2, 0x09e3, 0x09e4, 0x09e5, 0x09e6, 0x09e7,
+ 0x09e8, 0x09e9, 0x09ea, 0x09eb, 0x09ec, 0x09ed, 0x09ee, 0x09ef,
+ 0x09f0, 0x09f1, 0x09f2, 0x09f3, 0x09f4, 0x09f5, 0x09f6, 0x09f7,
+ 0x09f8, 0x09f9, 0x09fa, 0x09fb, 0x09fc, 0x09fd, 0x09fe, 0x09ff,
+};
+
+static const unsigned short gNormalizeTable0a00[] = {
+ /* U+0a00 */
+ 0x0a00, 0x0a01, 0x0a02, 0x0a03, 0x0a04, 0x0a05, 0x0a06, 0x0a07,
+ 0x0a08, 0x0a09, 0x0a0a, 0x0a0b, 0x0a0c, 0x0a0d, 0x0a0e, 0x0a0f,
+ 0x0a10, 0x0a11, 0x0a12, 0x0a13, 0x0a14, 0x0a15, 0x0a16, 0x0a17,
+ 0x0a18, 0x0a19, 0x0a1a, 0x0a1b, 0x0a1c, 0x0a1d, 0x0a1e, 0x0a1f,
+ 0x0a20, 0x0a21, 0x0a22, 0x0a23, 0x0a24, 0x0a25, 0x0a26, 0x0a27,
+ 0x0a28, 0x0a29, 0x0a2a, 0x0a2b, 0x0a2c, 0x0a2d, 0x0a2e, 0x0a2f,
+ 0x0a30, 0x0a31, 0x0a32, 0x0a32, 0x0a34, 0x0a35, 0x0a38, 0x0a37,
+ 0x0a38, 0x0a39, 0x0a3a, 0x0a3b, 0x0a3c, 0x0a3d, 0x0a3e, 0x0a3f,
+};
+
+static const unsigned short gNormalizeTable0a40[] = {
+ /* U+0a40 */
+ 0x0a40, 0x0a41, 0x0a42, 0x0a43, 0x0a44, 0x0a45, 0x0a46, 0x0a47,
+ 0x0a48, 0x0a49, 0x0a4a, 0x0a4b, 0x0a4c, 0x0a4d, 0x0a4e, 0x0a4f,
+ 0x0a50, 0x0a51, 0x0a52, 0x0a53, 0x0a54, 0x0a55, 0x0a56, 0x0a57,
+ 0x0a58, 0x0a16, 0x0a17, 0x0a1c, 0x0a5c, 0x0a5d, 0x0a2b, 0x0a5f,
+ 0x0a60, 0x0a61, 0x0a62, 0x0a63, 0x0a64, 0x0a65, 0x0a66, 0x0a67,
+ 0x0a68, 0x0a69, 0x0a6a, 0x0a6b, 0x0a6c, 0x0a6d, 0x0a6e, 0x0a6f,
+ 0x0a70, 0x0a71, 0x0a72, 0x0a73, 0x0a74, 0x0a75, 0x0a76, 0x0a77,
+ 0x0a78, 0x0a79, 0x0a7a, 0x0a7b, 0x0a7c, 0x0a7d, 0x0a7e, 0x0a7f,
+};
+
+static const unsigned short gNormalizeTable0b40[] = {
+ /* U+0b40 */
+ 0x0b40, 0x0b41, 0x0b42, 0x0b43, 0x0b44, 0x0b45, 0x0b46, 0x0b47,
+ 0x0b47, 0x0b49, 0x0b4a, 0x0b47, 0x0b47, 0x0b4d, 0x0b4e, 0x0b4f,
+ 0x0b50, 0x0b51, 0x0b52, 0x0b53, 0x0b54, 0x0b55, 0x0b56, 0x0b57,
+ 0x0b58, 0x0b59, 0x0b5a, 0x0b5b, 0x0b21, 0x0b22, 0x0b5e, 0x0b5f,
+ 0x0b60, 0x0b61, 0x0b62, 0x0b63, 0x0b64, 0x0b65, 0x0b66, 0x0b67,
+ 0x0b68, 0x0b69, 0x0b6a, 0x0b6b, 0x0b6c, 0x0b6d, 0x0b6e, 0x0b6f,
+ 0x0b70, 0x0b71, 0x0b72, 0x0b73, 0x0b74, 0x0b75, 0x0b76, 0x0b77,
+ 0x0b78, 0x0b79, 0x0b7a, 0x0b7b, 0x0b7c, 0x0b7d, 0x0b7e, 0x0b7f,
+};
+
+static const unsigned short gNormalizeTable0b80[] = {
+ /* U+0b80 */
+ 0x0b80, 0x0b81, 0x0b82, 0x0b83, 0x0b84, 0x0b85, 0x0b86, 0x0b87,
+ 0x0b88, 0x0b89, 0x0b8a, 0x0b8b, 0x0b8c, 0x0b8d, 0x0b8e, 0x0b8f,
+ 0x0b90, 0x0b91, 0x0b92, 0x0b93, 0x0b92, 0x0b95, 0x0b96, 0x0b97,
+ 0x0b98, 0x0b99, 0x0b9a, 0x0b9b, 0x0b9c, 0x0b9d, 0x0b9e, 0x0b9f,
+ 0x0ba0, 0x0ba1, 0x0ba2, 0x0ba3, 0x0ba4, 0x0ba5, 0x0ba6, 0x0ba7,
+ 0x0ba8, 0x0ba9, 0x0baa, 0x0bab, 0x0bac, 0x0bad, 0x0bae, 0x0baf,
+ 0x0bb0, 0x0bb1, 0x0bb2, 0x0bb3, 0x0bb4, 0x0bb5, 0x0bb6, 0x0bb7,
+ 0x0bb8, 0x0bb9, 0x0bba, 0x0bbb, 0x0bbc, 0x0bbd, 0x0bbe, 0x0bbf,
+};
+
+static const unsigned short gNormalizeTable0bc0[] = {
+ /* U+0bc0 */
+ 0x0bc0, 0x0bc1, 0x0bc2, 0x0bc3, 0x0bc4, 0x0bc5, 0x0bc6, 0x0bc7,
+ 0x0bc8, 0x0bc9, 0x0bc6, 0x0bc7, 0x0bc6, 0x0bcd, 0x0bce, 0x0bcf,
+ 0x0bd0, 0x0bd1, 0x0bd2, 0x0bd3, 0x0bd4, 0x0bd5, 0x0bd6, 0x0bd7,
+ 0x0bd8, 0x0bd9, 0x0bda, 0x0bdb, 0x0bdc, 0x0bdd, 0x0bde, 0x0bdf,
+ 0x0be0, 0x0be1, 0x0be2, 0x0be3, 0x0be4, 0x0be5, 0x0be6, 0x0be7,
+ 0x0be8, 0x0be9, 0x0bea, 0x0beb, 0x0bec, 0x0bed, 0x0bee, 0x0bef,
+ 0x0bf0, 0x0bf1, 0x0bf2, 0x0bf3, 0x0bf4, 0x0bf5, 0x0bf6, 0x0bf7,
+ 0x0bf8, 0x0bf9, 0x0bfa, 0x0bfb, 0x0bfc, 0x0bfd, 0x0bfe, 0x0bff,
+};
+
+static const unsigned short gNormalizeTable0c40[] = {
+ /* U+0c40 */
+ 0x0c40, 0x0c41, 0x0c42, 0x0c43, 0x0c44, 0x0c45, 0x0c46, 0x0c47,
+ 0x0c46, 0x0c49, 0x0c4a, 0x0c4b, 0x0c4c, 0x0c4d, 0x0c4e, 0x0c4f,
+ 0x0c50, 0x0c51, 0x0c52, 0x0c53, 0x0c54, 0x0c55, 0x0c56, 0x0c57,
+ 0x0c58, 0x0c59, 0x0c5a, 0x0c5b, 0x0c5c, 0x0c5d, 0x0c5e, 0x0c5f,
+ 0x0c60, 0x0c61, 0x0c62, 0x0c63, 0x0c64, 0x0c65, 0x0c66, 0x0c67,
+ 0x0c68, 0x0c69, 0x0c6a, 0x0c6b, 0x0c6c, 0x0c6d, 0x0c6e, 0x0c6f,
+ 0x0c70, 0x0c71, 0x0c72, 0x0c73, 0x0c74, 0x0c75, 0x0c76, 0x0c77,
+ 0x0c78, 0x0c79, 0x0c7a, 0x0c7b, 0x0c7c, 0x0c7d, 0x0c7e, 0x0c7f,
+};
+
+static const unsigned short gNormalizeTable0cc0[] = {
+ /* U+0cc0 */
+ 0x0cbf, 0x0cc1, 0x0cc2, 0x0cc3, 0x0cc4, 0x0cc5, 0x0cc6, 0x0cc6,
+ 0x0cc6, 0x0cc9, 0x0cc6, 0x0cc6, 0x0ccc, 0x0ccd, 0x0cce, 0x0ccf,
+ 0x0cd0, 0x0cd1, 0x0cd2, 0x0cd3, 0x0cd4, 0x0cd5, 0x0cd6, 0x0cd7,
+ 0x0cd8, 0x0cd9, 0x0cda, 0x0cdb, 0x0cdc, 0x0cdd, 0x0cde, 0x0cdf,
+ 0x0ce0, 0x0ce1, 0x0ce2, 0x0ce3, 0x0ce4, 0x0ce5, 0x0ce6, 0x0ce7,
+ 0x0ce8, 0x0ce9, 0x0cea, 0x0ceb, 0x0cec, 0x0ced, 0x0cee, 0x0cef,
+ 0x0cf0, 0x0cf1, 0x0cf2, 0x0cf3, 0x0cf4, 0x0cf5, 0x0cf6, 0x0cf7,
+ 0x0cf8, 0x0cf9, 0x0cfa, 0x0cfb, 0x0cfc, 0x0cfd, 0x0cfe, 0x0cff,
+};
+
+static const unsigned short gNormalizeTable0d40[] = {
+ /* U+0d40 */
+ 0x0d40, 0x0d41, 0x0d42, 0x0d43, 0x0d44, 0x0d45, 0x0d46, 0x0d47,
+ 0x0d48, 0x0d49, 0x0d46, 0x0d47, 0x0d46, 0x0d4d, 0x0d4e, 0x0d4f,
+ 0x0d50, 0x0d51, 0x0d52, 0x0d53, 0x0d54, 0x0d55, 0x0d56, 0x0d57,
+ 0x0d58, 0x0d59, 0x0d5a, 0x0d5b, 0x0d5c, 0x0d5d, 0x0d5e, 0x0d5f,
+ 0x0d60, 0x0d61, 0x0d62, 0x0d63, 0x0d64, 0x0d65, 0x0d66, 0x0d67,
+ 0x0d68, 0x0d69, 0x0d6a, 0x0d6b, 0x0d6c, 0x0d6d, 0x0d6e, 0x0d6f,
+ 0x0d70, 0x0d71, 0x0d72, 0x0d73, 0x0d74, 0x0d75, 0x0d76, 0x0d77,
+ 0x0d78, 0x0d79, 0x0d7a, 0x0d7b, 0x0d7c, 0x0d7d, 0x0d7e, 0x0d7f,
+};
+
+static const unsigned short gNormalizeTable0dc0[] = {
+ /* U+0dc0 */
+ 0x0dc0, 0x0dc1, 0x0dc2, 0x0dc3, 0x0dc4, 0x0dc5, 0x0dc6, 0x0dc7,
+ 0x0dc8, 0x0dc9, 0x0dca, 0x0dcb, 0x0dcc, 0x0dcd, 0x0dce, 0x0dcf,
+ 0x0dd0, 0x0dd1, 0x0dd2, 0x0dd3, 0x0dd4, 0x0dd5, 0x0dd6, 0x0dd7,
+ 0x0dd8, 0x0dd9, 0x0dd9, 0x0ddb, 0x0dd9, 0x0dd9, 0x0dd9, 0x0ddf,
+ 0x0de0, 0x0de1, 0x0de2, 0x0de3, 0x0de4, 0x0de5, 0x0de6, 0x0de7,
+ 0x0de8, 0x0de9, 0x0dea, 0x0deb, 0x0dec, 0x0ded, 0x0dee, 0x0def,
+ 0x0df0, 0x0df1, 0x0df2, 0x0df3, 0x0df4, 0x0df5, 0x0df6, 0x0df7,
+ 0x0df8, 0x0df9, 0x0dfa, 0x0dfb, 0x0dfc, 0x0dfd, 0x0dfe, 0x0dff,
+};
+
+static const unsigned short gNormalizeTable0e00[] = {
+ /* U+0e00 */
+ 0x0e00, 0x0e01, 0x0e02, 0x0e03, 0x0e04, 0x0e05, 0x0e06, 0x0e07,
+ 0x0e08, 0x0e09, 0x0e0a, 0x0e0b, 0x0e0c, 0x0e0d, 0x0e0e, 0x0e0f,
+ 0x0e10, 0x0e11, 0x0e12, 0x0e13, 0x0e14, 0x0e15, 0x0e16, 0x0e17,
+ 0x0e18, 0x0e19, 0x0e1a, 0x0e1b, 0x0e1c, 0x0e1d, 0x0e1e, 0x0e1f,
+ 0x0e20, 0x0e21, 0x0e22, 0x0e23, 0x0e24, 0x0e25, 0x0e26, 0x0e27,
+ 0x0e28, 0x0e29, 0x0e2a, 0x0e2b, 0x0e2c, 0x0e2d, 0x0e2e, 0x0e2f,
+ 0x0e30, 0x0e31, 0x0e32, 0x0e4d, 0x0e34, 0x0e35, 0x0e36, 0x0e37,
+ 0x0e38, 0x0e39, 0x0e3a, 0x0e3b, 0x0e3c, 0x0e3d, 0x0e3e, 0x0e3f,
+};
+
+static const unsigned short gNormalizeTable0e80[] = {
+ /* U+0e80 */
+ 0x0e80, 0x0e81, 0x0e82, 0x0e83, 0x0e84, 0x0e85, 0x0e86, 0x0e87,
+ 0x0e88, 0x0e89, 0x0e8a, 0x0e8b, 0x0e8c, 0x0e8d, 0x0e8e, 0x0e8f,
+ 0x0e90, 0x0e91, 0x0e92, 0x0e93, 0x0e94, 0x0e95, 0x0e96, 0x0e97,
+ 0x0e98, 0x0e99, 0x0e9a, 0x0e9b, 0x0e9c, 0x0e9d, 0x0e9e, 0x0e9f,
+ 0x0ea0, 0x0ea1, 0x0ea2, 0x0ea3, 0x0ea4, 0x0ea5, 0x0ea6, 0x0ea7,
+ 0x0ea8, 0x0ea9, 0x0eaa, 0x0eab, 0x0eac, 0x0ead, 0x0eae, 0x0eaf,
+ 0x0eb0, 0x0eb1, 0x0eb2, 0x0ecd, 0x0eb4, 0x0eb5, 0x0eb6, 0x0eb7,
+ 0x0eb8, 0x0eb9, 0x0eba, 0x0ebb, 0x0ebc, 0x0ebd, 0x0ebe, 0x0ebf,
+};
+
+static const unsigned short gNormalizeTable0ec0[] = {
+ /* U+0ec0 */
+ 0x0ec0, 0x0ec1, 0x0ec2, 0x0ec3, 0x0ec4, 0x0ec5, 0x0ec6, 0x0ec7,
+ 0x0ec8, 0x0ec9, 0x0eca, 0x0ecb, 0x0ecc, 0x0ecd, 0x0ece, 0x0ecf,
+ 0x0ed0, 0x0ed1, 0x0ed2, 0x0ed3, 0x0ed4, 0x0ed5, 0x0ed6, 0x0ed7,
+ 0x0ed8, 0x0ed9, 0x0eda, 0x0edb, 0x0eab, 0x0eab, 0x0ede, 0x0edf,
+ 0x0ee0, 0x0ee1, 0x0ee2, 0x0ee3, 0x0ee4, 0x0ee5, 0x0ee6, 0x0ee7,
+ 0x0ee8, 0x0ee9, 0x0eea, 0x0eeb, 0x0eec, 0x0eed, 0x0eee, 0x0eef,
+ 0x0ef0, 0x0ef1, 0x0ef2, 0x0ef3, 0x0ef4, 0x0ef5, 0x0ef6, 0x0ef7,
+ 0x0ef8, 0x0ef9, 0x0efa, 0x0efb, 0x0efc, 0x0efd, 0x0efe, 0x0eff,
+};
+
+static const unsigned short gNormalizeTable0f00[] = {
+ /* U+0f00 */
+ 0x0f00, 0x0f01, 0x0f02, 0x0f03, 0x0f04, 0x0f05, 0x0f06, 0x0f07,
+ 0x0f08, 0x0f09, 0x0f0a, 0x0f0b, 0x0f0b, 0x0f0d, 0x0f0e, 0x0f0f,
+ 0x0f10, 0x0f11, 0x0f12, 0x0f13, 0x0f14, 0x0f15, 0x0f16, 0x0f17,
+ 0x0f18, 0x0f19, 0x0f1a, 0x0f1b, 0x0f1c, 0x0f1d, 0x0f1e, 0x0f1f,
+ 0x0f20, 0x0f21, 0x0f22, 0x0f23, 0x0f24, 0x0f25, 0x0f26, 0x0f27,
+ 0x0f28, 0x0f29, 0x0f2a, 0x0f2b, 0x0f2c, 0x0f2d, 0x0f2e, 0x0f2f,
+ 0x0f30, 0x0f31, 0x0f32, 0x0f33, 0x0f34, 0x0f35, 0x0f36, 0x0f37,
+ 0x0f38, 0x0f39, 0x0f3a, 0x0f3b, 0x0f3c, 0x0f3d, 0x0f3e, 0x0f3f,
+};
+
+static const unsigned short gNormalizeTable0f40[] = {
+ /* U+0f40 */
+ 0x0f40, 0x0f41, 0x0f42, 0x0f42, 0x0f44, 0x0f45, 0x0f46, 0x0f47,
+ 0x0f48, 0x0f49, 0x0f4a, 0x0f4b, 0x0f4c, 0x0f4c, 0x0f4e, 0x0f4f,
+ 0x0f50, 0x0f51, 0x0f51, 0x0f53, 0x0f54, 0x0f55, 0x0f56, 0x0f56,
+ 0x0f58, 0x0f59, 0x0f5a, 0x0f5b, 0x0f5b, 0x0f5d, 0x0f5e, 0x0f5f,
+ 0x0f60, 0x0f61, 0x0f62, 0x0f63, 0x0f64, 0x0f65, 0x0f66, 0x0f67,
+ 0x0f68, 0x0f40, 0x0f6a, 0x0f6b, 0x0f6c, 0x0f6d, 0x0f6e, 0x0f6f,
+ 0x0f70, 0x0f71, 0x0f72, 0x0f71, 0x0f74, 0x0f71, 0x0fb2, 0x0fb2,
+ 0x0fb3, 0x0fb3, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f7e, 0x0f7f,
+};
+
+static const unsigned short gNormalizeTable0f80[] = {
+ /* U+0f80 */
+ 0x0f80, 0x0f71, 0x0f82, 0x0f83, 0x0f84, 0x0f85, 0x0f86, 0x0f87,
+ 0x0f88, 0x0f89, 0x0f8a, 0x0f8b, 0x0f8c, 0x0f8d, 0x0f8e, 0x0f8f,
+ 0x0f90, 0x0f91, 0x0f92, 0x0f92, 0x0f94, 0x0f95, 0x0f96, 0x0f97,
+ 0x0f98, 0x0f99, 0x0f9a, 0x0f9b, 0x0f9c, 0x0f9c, 0x0f9e, 0x0f9f,
+ 0x0fa0, 0x0fa1, 0x0fa1, 0x0fa3, 0x0fa4, 0x0fa5, 0x0fa6, 0x0fa6,
+ 0x0fa8, 0x0fa9, 0x0faa, 0x0fab, 0x0fab, 0x0fad, 0x0fae, 0x0faf,
+ 0x0fb0, 0x0fb1, 0x0fb2, 0x0fb3, 0x0fb4, 0x0fb5, 0x0fb6, 0x0fb7,
+ 0x0fb8, 0x0f90, 0x0fba, 0x0fbb, 0x0fbc, 0x0fbd, 0x0fbe, 0x0fbf,
+};
+
+static const unsigned short gNormalizeTable1000[] = {
+ /* U+1000 */
+ 0x1000, 0x1001, 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007,
+ 0x1008, 0x1009, 0x100a, 0x100b, 0x100c, 0x100d, 0x100e, 0x100f,
+ 0x1010, 0x1011, 0x1012, 0x1013, 0x1014, 0x1015, 0x1016, 0x1017,
+ 0x1018, 0x1019, 0x101a, 0x101b, 0x101c, 0x101d, 0x101e, 0x101f,
+ 0x1020, 0x1021, 0x1022, 0x1023, 0x1024, 0x1025, 0x1025, 0x1027,
+ 0x1028, 0x1029, 0x102a, 0x102b, 0x102c, 0x102d, 0x102e, 0x102f,
+ 0x1030, 0x1031, 0x1032, 0x1033, 0x1034, 0x1035, 0x1036, 0x1037,
+ 0x1038, 0x1039, 0x103a, 0x103b, 0x103c, 0x103d, 0x103e, 0x103f,
+};
+
+static const unsigned short gNormalizeTable1080[] = {
+ /* U+1080 */
+ 0x1080, 0x1081, 0x1082, 0x1083, 0x1084, 0x1085, 0x1086, 0x1087,
+ 0x1088, 0x1089, 0x108a, 0x108b, 0x108c, 0x108d, 0x108e, 0x108f,
+ 0x1090, 0x1091, 0x1092, 0x1093, 0x1094, 0x1095, 0x1096, 0x1097,
+ 0x1098, 0x1099, 0x109a, 0x109b, 0x109c, 0x109d, 0x109e, 0x109f,
+ 0x2d00, 0x2d01, 0x2d02, 0x2d03, 0x2d04, 0x2d05, 0x2d06, 0x2d07,
+ 0x2d08, 0x2d09, 0x2d0a, 0x2d0b, 0x2d0c, 0x2d0d, 0x2d0e, 0x2d0f,
+ 0x2d10, 0x2d11, 0x2d12, 0x2d13, 0x2d14, 0x2d15, 0x2d16, 0x2d17,
+ 0x2d18, 0x2d19, 0x2d1a, 0x2d1b, 0x2d1c, 0x2d1d, 0x2d1e, 0x2d1f,
+};
+
+static const unsigned short gNormalizeTable10c0[] = {
+ /* U+10c0 */
+ 0x2d20, 0x2d21, 0x2d22, 0x2d23, 0x2d24, 0x2d25, 0x10c6, 0x10c7,
+ 0x10c8, 0x10c9, 0x10ca, 0x10cb, 0x10cc, 0x10cd, 0x10ce, 0x10cf,
+ 0x10d0, 0x10d1, 0x10d2, 0x10d3, 0x10d4, 0x10d5, 0x10d6, 0x10d7,
+ 0x10d8, 0x10d9, 0x10da, 0x10db, 0x10dc, 0x10dd, 0x10de, 0x10df,
+ 0x10e0, 0x10e1, 0x10e2, 0x10e3, 0x10e4, 0x10e5, 0x10e6, 0x10e7,
+ 0x10e8, 0x10e9, 0x10ea, 0x10eb, 0x10ec, 0x10ed, 0x10ee, 0x10ef,
+ 0x10f0, 0x10f1, 0x10f2, 0x10f3, 0x10f4, 0x10f5, 0x10f6, 0x10f7,
+ 0x10f8, 0x10f9, 0x10fa, 0x10fb, 0x10dc, 0x10fd, 0x10fe, 0x10ff,
+};
+
+static const unsigned short gNormalizeTable1140[] = {
+ /* U+1140 */
+ 0x1140, 0x1141, 0x1142, 0x1143, 0x1144, 0x1145, 0x1146, 0x1147,
+ 0x1148, 0x1149, 0x114a, 0x114b, 0x114c, 0x114d, 0x114e, 0x114f,
+ 0x1150, 0x1151, 0x1152, 0x1153, 0x1154, 0x1155, 0x1156, 0x1157,
+ 0x1158, 0x1159, 0x115a, 0x115b, 0x115c, 0x115d, 0x115e, 0x0020,
+ 0x0020, 0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167,
+ 0x1168, 0x1169, 0x116a, 0x116b, 0x116c, 0x116d, 0x116e, 0x116f,
+ 0x1170, 0x1171, 0x1172, 0x1173, 0x1174, 0x1175, 0x1176, 0x1177,
+ 0x1178, 0x1179, 0x117a, 0x117b, 0x117c, 0x117d, 0x117e, 0x117f,
+};
+
+static const unsigned short gNormalizeTable1780[] = {
+ /* U+1780 */
+ 0x1780, 0x1781, 0x1782, 0x1783, 0x1784, 0x1785, 0x1786, 0x1787,
+ 0x1788, 0x1789, 0x178a, 0x178b, 0x178c, 0x178d, 0x178e, 0x178f,
+ 0x1790, 0x1791, 0x1792, 0x1793, 0x1794, 0x1795, 0x1796, 0x1797,
+ 0x1798, 0x1799, 0x179a, 0x179b, 0x179c, 0x179d, 0x179e, 0x179f,
+ 0x17a0, 0x17a1, 0x17a2, 0x17a3, 0x17a4, 0x17a5, 0x17a6, 0x17a7,
+ 0x17a8, 0x17a9, 0x17aa, 0x17ab, 0x17ac, 0x17ad, 0x17ae, 0x17af,
+ 0x17b0, 0x17b1, 0x17b2, 0x17b3, 0x0020, 0x0020, 0x17b6, 0x17b7,
+ 0x17b8, 0x17b9, 0x17ba, 0x17bb, 0x17bc, 0x17bd, 0x17be, 0x17bf,
+};
+
+static const unsigned short gNormalizeTable1800[] = {
+ /* U+1800 */
+ 0x1800, 0x1801, 0x1802, 0x1803, 0x1804, 0x1805, 0x1806, 0x1807,
+ 0x1808, 0x1809, 0x180a, 0x0020, 0x0020, 0x0020, 0x180e, 0x180f,
+ 0x1810, 0x1811, 0x1812, 0x1813, 0x1814, 0x1815, 0x1816, 0x1817,
+ 0x1818, 0x1819, 0x181a, 0x181b, 0x181c, 0x181d, 0x181e, 0x181f,
+ 0x1820, 0x1821, 0x1822, 0x1823, 0x1824, 0x1825, 0x1826, 0x1827,
+ 0x1828, 0x1829, 0x182a, 0x182b, 0x182c, 0x182d, 0x182e, 0x182f,
+ 0x1830, 0x1831, 0x1832, 0x1833, 0x1834, 0x1835, 0x1836, 0x1837,
+ 0x1838, 0x1839, 0x183a, 0x183b, 0x183c, 0x183d, 0x183e, 0x183f,
+};
+
+static const unsigned short gNormalizeTable1b00[] = {
+ /* U+1b00 */
+ 0x1b00, 0x1b01, 0x1b02, 0x1b03, 0x1b04, 0x1b05, 0x1b05, 0x1b07,
+ 0x1b07, 0x1b09, 0x1b09, 0x1b0b, 0x1b0b, 0x1b0d, 0x1b0d, 0x1b0f,
+ 0x1b10, 0x1b11, 0x1b11, 0x1b13, 0x1b14, 0x1b15, 0x1b16, 0x1b17,
+ 0x1b18, 0x1b19, 0x1b1a, 0x1b1b, 0x1b1c, 0x1b1d, 0x1b1e, 0x1b1f,
+ 0x1b20, 0x1b21, 0x1b22, 0x1b23, 0x1b24, 0x1b25, 0x1b26, 0x1b27,
+ 0x1b28, 0x1b29, 0x1b2a, 0x1b2b, 0x1b2c, 0x1b2d, 0x1b2e, 0x1b2f,
+ 0x1b30, 0x1b31, 0x1b32, 0x1b33, 0x1b34, 0x1b35, 0x1b36, 0x1b37,
+ 0x1b38, 0x1b39, 0x1b3a, 0x1b3a, 0x1b3c, 0x1b3c, 0x1b3e, 0x1b3f,
+};
+
+static const unsigned short gNormalizeTable1b40[] = {
+ /* U+1b40 */
+ 0x1b3e, 0x1b3f, 0x1b42, 0x1b42, 0x1b44, 0x1b45, 0x1b46, 0x1b47,
+ 0x1b48, 0x1b49, 0x1b4a, 0x1b4b, 0x1b4c, 0x1b4d, 0x1b4e, 0x1b4f,
+ 0x1b50, 0x1b51, 0x1b52, 0x1b53, 0x1b54, 0x1b55, 0x1b56, 0x1b57,
+ 0x1b58, 0x1b59, 0x1b5a, 0x1b5b, 0x1b5c, 0x1b5d, 0x1b5e, 0x1b5f,
+ 0x1b60, 0x1b61, 0x1b62, 0x1b63, 0x1b64, 0x1b65, 0x1b66, 0x1b67,
+ 0x1b68, 0x1b69, 0x1b6a, 0x1b6b, 0x1b6c, 0x1b6d, 0x1b6e, 0x1b6f,
+ 0x1b70, 0x1b71, 0x1b72, 0x1b73, 0x1b74, 0x1b75, 0x1b76, 0x1b77,
+ 0x1b78, 0x1b79, 0x1b7a, 0x1b7b, 0x1b7c, 0x1b7d, 0x1b7e, 0x1b7f,
+};
+
+static const unsigned short gNormalizeTable1d00[] = {
+ /* U+1d00 */
+ 0x1d00, 0x1d01, 0x1d02, 0x1d03, 0x1d04, 0x1d05, 0x1d06, 0x1d07,
+ 0x1d08, 0x1d09, 0x1d0a, 0x1d0b, 0x1d0c, 0x1d0d, 0x1d0e, 0x1d0f,
+ 0x1d10, 0x1d11, 0x1d12, 0x1d13, 0x1d14, 0x1d15, 0x1d16, 0x1d17,
+ 0x1d18, 0x1d19, 0x1d1a, 0x1d1b, 0x1d1c, 0x1d1d, 0x1d1e, 0x1d1f,
+ 0x1d20, 0x1d21, 0x1d22, 0x1d23, 0x1d24, 0x1d25, 0x1d26, 0x1d27,
+ 0x1d28, 0x1d29, 0x1d2a, 0x1d2b, 0x0061, 0x00e6, 0x0062, 0x1d2f,
+ 0x0064, 0x0065, 0x01dd, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b,
+ 0x006c, 0x006d, 0x006e, 0x1d3b, 0x006f, 0x0223, 0x0070, 0x0072,
+};
+
+static const unsigned short gNormalizeTable1d40[] = {
+ /* U+1d40 */
+ 0x0074, 0x0075, 0x0077, 0x0061, 0x0250, 0x0251, 0x1d02, 0x0062,
+ 0x0064, 0x0065, 0x0259, 0x025b, 0x025c, 0x0067, 0x1d4e, 0x006b,
+ 0x006d, 0x014b, 0x006f, 0x0254, 0x1d16, 0x1d17, 0x0070, 0x0074,
+ 0x0075, 0x1d1d, 0x026f, 0x0076, 0x1d25, 0x03b2, 0x03b3, 0x03b4,
+ 0x03c6, 0x03c7, 0x0069, 0x0072, 0x0075, 0x0076, 0x03b2, 0x03b3,
+ 0x03c1, 0x03c6, 0x03c7, 0x1d6b, 0x1d6c, 0x1d6d, 0x1d6e, 0x1d6f,
+ 0x1d70, 0x1d71, 0x1d72, 0x1d73, 0x1d74, 0x1d75, 0x1d76, 0x1d77,
+ 0x043d, 0x1d79, 0x1d7a, 0x1d7b, 0x1d7c, 0x1d7d, 0x1d7e, 0x1d7f,
+};
+
+static const unsigned short gNormalizeTable1d80[] = {
+ /* U+1d80 */
+ 0x1d80, 0x1d81, 0x1d82, 0x1d83, 0x1d84, 0x1d85, 0x1d86, 0x1d87,
+ 0x1d88, 0x1d89, 0x1d8a, 0x1d8b, 0x1d8c, 0x1d8d, 0x1d8e, 0x1d8f,
+ 0x1d90, 0x1d91, 0x1d92, 0x1d93, 0x1d94, 0x1d95, 0x1d96, 0x1d97,
+ 0x1d98, 0x1d99, 0x1d9a, 0x0252, 0x0063, 0x0255, 0x00f0, 0x025c,
+ 0x0066, 0x025f, 0x0261, 0x0265, 0x0268, 0x0269, 0x026a, 0x1d7b,
+ 0x029d, 0x026d, 0x1d85, 0x029f, 0x0271, 0x0270, 0x0272, 0x0273,
+ 0x0274, 0x0275, 0x0278, 0x0282, 0x0283, 0x01ab, 0x0289, 0x028a,
+ 0x1d1c, 0x028b, 0x028c, 0x007a, 0x0290, 0x0291, 0x0292, 0x03b8,
+};
+
+static const unsigned short gNormalizeTable1e00[] = {
+ /* U+1e00 */
+ 0x0061, 0x0061, 0x0062, 0x0062, 0x0062, 0x0062, 0x0062, 0x0062,
+ 0x0063, 0x0063, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064,
+ 0x0064, 0x0064, 0x0064, 0x0064, 0x0065, 0x0065, 0x0065, 0x0065,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0066, 0x0066,
+ 0x0067, 0x0067, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068,
+ 0x0068, 0x0068, 0x0068, 0x0068, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x006b, 0x006b, 0x006b, 0x006b, 0x006b, 0x006b, 0x006c, 0x006c,
+ 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006d, 0x006d,
+};
+
+static const unsigned short gNormalizeTable1e40[] = {
+ /* U+1e40 */
+ 0x006d, 0x006d, 0x006d, 0x006d, 0x006e, 0x006e, 0x006e, 0x006e,
+ 0x006e, 0x006e, 0x006e, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x006f, 0x006f, 0x0070, 0x0070, 0x0070, 0x0070,
+ 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072,
+ 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073,
+ 0x0073, 0x0073, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074,
+ 0x0074, 0x0074, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0075, 0x0075, 0x0075, 0x0075, 0x0076, 0x0076, 0x0076, 0x0076,
+};
+
+static const unsigned short gNormalizeTable1e80[] = {
+ /* U+1e80 */
+ 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077,
+ 0x0077, 0x0077, 0x0078, 0x0078, 0x0078, 0x0078, 0x0079, 0x0079,
+ 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x0068, 0x0074,
+ 0x0077, 0x0079, 0x0061, 0x0073, 0x1e9c, 0x1e9d, 0x0073, 0x1e9f,
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061,
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061,
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065,
+};
+
+static const unsigned short gNormalizeTable1ec0[] = {
+ /* U+1ec0 */
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065,
+ 0x0069, 0x0069, 0x0069, 0x0069, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f,
+ 0x006f, 0x006f, 0x006f, 0x006f, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075,
+ 0x0075, 0x0075, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079,
+ 0x0079, 0x0079, 0x1efb, 0x1efb, 0x1efd, 0x1efd, 0x1eff, 0x1eff,
+};
+
+static const unsigned short gNormalizeTable1f00[] = {
+ /* U+1f00 */
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1,
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1,
+ 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x1f16, 0x1f17,
+ 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x1f1e, 0x1f1f,
+ 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7,
+ 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7,
+ 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9,
+ 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9,
+};
+
+static const unsigned short gNormalizeTable1f40[] = {
+ /* U+1f40 */
+ 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x1f46, 0x1f47,
+ 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x1f4e, 0x1f4f,
+ 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5,
+ 0x1f58, 0x03c5, 0x1f5a, 0x03c5, 0x1f5c, 0x03c5, 0x1f5e, 0x03c5,
+ 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9,
+ 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9,
+ 0x03b1, 0x03b1, 0x03b5, 0x03b5, 0x03b7, 0x03b7, 0x03b9, 0x03b9,
+ 0x03bf, 0x03bf, 0x03c5, 0x03c5, 0x03c9, 0x03c9, 0x1f7e, 0x1f7f,
+};
+
+static const unsigned short gNormalizeTable1f80[] = {
+ /* U+1f80 */
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1,
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1,
+ 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7,
+ 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7,
+ 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9,
+ 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9,
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x1fb5, 0x03b1, 0x03b1,
+ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x0020, 0x03b9, 0x0020,
+};
+
+static const unsigned short gNormalizeTable1fc0[] = {
+ /* U+1fc0 */
+ 0x0020, 0x0020, 0x03b7, 0x03b7, 0x03b7, 0x1fc5, 0x03b7, 0x03b7,
+ 0x03b5, 0x03b5, 0x03b7, 0x03b7, 0x03b7, 0x0020, 0x0020, 0x0020,
+ 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x1fd4, 0x1fd5, 0x03b9, 0x03b9,
+ 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x1fdc, 0x0020, 0x0020, 0x0020,
+ 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c1, 0x03c1, 0x03c5, 0x03c5,
+ 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c1, 0x0020, 0x0020, 0x0060,
+ 0x1ff0, 0x1ff1, 0x03c9, 0x03c9, 0x03c9, 0x1ff5, 0x03c9, 0x03c9,
+ 0x03bf, 0x03bf, 0x03c9, 0x03c9, 0x03c9, 0x0020, 0x0020, 0x1fff,
+};
+
+static const unsigned short gNormalizeTable2000[] = {
+ /* U+2000 */
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x2010, 0x2010, 0x2012, 0x2013, 0x2014, 0x2015, 0x2016, 0x0020,
+ 0x2018, 0x2019, 0x201a, 0x201b, 0x201c, 0x201d, 0x201e, 0x201f,
+ 0x2020, 0x2021, 0x2022, 0x2023, 0x002e, 0x002e, 0x002e, 0x2027,
+ 0x2028, 0x2029, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x2030, 0x2031, 0x2032, 0x2032, 0x2032, 0x2035, 0x2035, 0x2035,
+ 0x2038, 0x2039, 0x203a, 0x203b, 0x0021, 0x203d, 0x0020, 0x203f,
+};
+
+static const unsigned short gNormalizeTable2040[] = {
+ /* U+2040 */
+ 0x2040, 0x2041, 0x2042, 0x2043, 0x2044, 0x2045, 0x2046, 0x003f,
+ 0x003f, 0x0021, 0x204a, 0x204b, 0x204c, 0x204d, 0x204e, 0x204f,
+ 0x2050, 0x2051, 0x2052, 0x2053, 0x2054, 0x2055, 0x2056, 0x2032,
+ 0x2058, 0x2059, 0x205a, 0x205b, 0x205c, 0x205d, 0x205e, 0x0020,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x0030, 0x0069, 0x2072, 0x2073, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x002b, 0x2212, 0x003d, 0x0028, 0x0029, 0x006e,
+};
+
+static const unsigned short gNormalizeTable2080[] = {
+ /* U+2080 */
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x002b, 0x2212, 0x003d, 0x0028, 0x0029, 0x208f,
+ 0x0061, 0x0065, 0x006f, 0x0078, 0x0259, 0x2095, 0x2096, 0x2097,
+ 0x2098, 0x2099, 0x209a, 0x209b, 0x209c, 0x209d, 0x209e, 0x209f,
+ 0x20a0, 0x20a1, 0x20a2, 0x20a3, 0x20a4, 0x20a5, 0x20a6, 0x20a7,
+ 0x0072, 0x20a9, 0x20aa, 0x20ab, 0x20ac, 0x20ad, 0x20ae, 0x20af,
+ 0x20b0, 0x20b1, 0x20b2, 0x20b3, 0x20b4, 0x20b5, 0x20b6, 0x20b7,
+ 0x20b8, 0x20b9, 0x20ba, 0x20bb, 0x20bc, 0x20bd, 0x20be, 0x20bf,
+};
+
+static const unsigned short gNormalizeTable2100[] = {
+ /* U+2100 */
+ 0x0061, 0x0061, 0x0063, 0x00b0, 0x2104, 0x0063, 0x0063, 0x025b,
+ 0x2108, 0x00b0, 0x0067, 0x0068, 0x0068, 0x0068, 0x0068, 0x0127,
+ 0x0069, 0x0069, 0x006c, 0x006c, 0x2114, 0x006e, 0x006e, 0x2117,
+ 0x2118, 0x0070, 0x0071, 0x0072, 0x0072, 0x0072, 0x211e, 0x211f,
+ 0x0073, 0x0074, 0x0074, 0x2123, 0x007a, 0x2125, 0x03c9, 0x2127,
+ 0x007a, 0x2129, 0x006b, 0x0061, 0x0062, 0x0063, 0x212e, 0x0065,
+ 0x0065, 0x0066, 0x214e, 0x006d, 0x006f, 0x05d0, 0x05d1, 0x05d2,
+ 0x05d3, 0x0069, 0x213a, 0x0066, 0x03c0, 0x03b3, 0x03b3, 0x03c0,
+};
+
+static const unsigned short gNormalizeTable2140[] = {
+ /* U+2140 */
+ 0x2211, 0x2141, 0x2142, 0x2143, 0x2144, 0x0064, 0x0064, 0x0065,
+ 0x0069, 0x006a, 0x214a, 0x214b, 0x214c, 0x214d, 0x214e, 0x214f,
+ 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0031, 0x0032, 0x0033,
+ 0x0034, 0x0031, 0x0035, 0x0031, 0x0033, 0x0035, 0x0037, 0x0031,
+ 0x0069, 0x0069, 0x0069, 0x0069, 0x0076, 0x0076, 0x0076, 0x0076,
+ 0x0069, 0x0078, 0x0078, 0x0078, 0x006c, 0x0063, 0x0064, 0x006d,
+ 0x0069, 0x0069, 0x0069, 0x0069, 0x0076, 0x0076, 0x0076, 0x0076,
+ 0x0069, 0x0078, 0x0078, 0x0078, 0x006c, 0x0063, 0x0064, 0x006d,
+};
+
+static const unsigned short gNormalizeTable2180[] = {
+ /* U+2180 */
+ 0x2180, 0x2181, 0x2182, 0x2184, 0x2184, 0x2185, 0x2186, 0x2187,
+ 0x2188, 0x0030, 0x218a, 0x218b, 0x218c, 0x218d, 0x218e, 0x218f,
+ 0x2190, 0x2191, 0x2192, 0x2193, 0x2194, 0x2195, 0x2196, 0x2197,
+ 0x2198, 0x2199, 0x2190, 0x2192, 0x219c, 0x219d, 0x219e, 0x219f,
+ 0x21a0, 0x21a1, 0x21a2, 0x21a3, 0x21a4, 0x21a5, 0x21a6, 0x21a7,
+ 0x21a8, 0x21a9, 0x21aa, 0x21ab, 0x21ac, 0x21ad, 0x2194, 0x21af,
+ 0x21b0, 0x21b1, 0x21b2, 0x21b3, 0x21b4, 0x21b5, 0x21b6, 0x21b7,
+ 0x21b8, 0x21b9, 0x21ba, 0x21bb, 0x21bc, 0x21bd, 0x21be, 0x21bf,
+};
+
+static const unsigned short gNormalizeTable21c0[] = {
+ /* U+21c0 */
+ 0x21c0, 0x21c1, 0x21c2, 0x21c3, 0x21c4, 0x21c5, 0x21c6, 0x21c7,
+ 0x21c8, 0x21c9, 0x21ca, 0x21cb, 0x21cc, 0x21d0, 0x21d4, 0x21d2,
+ 0x21d0, 0x21d1, 0x21d2, 0x21d3, 0x21d4, 0x21d5, 0x21d6, 0x21d7,
+ 0x21d8, 0x21d9, 0x21da, 0x21db, 0x21dc, 0x21dd, 0x21de, 0x21df,
+ 0x21e0, 0x21e1, 0x21e2, 0x21e3, 0x21e4, 0x21e5, 0x21e6, 0x21e7,
+ 0x21e8, 0x21e9, 0x21ea, 0x21eb, 0x21ec, 0x21ed, 0x21ee, 0x21ef,
+ 0x21f0, 0x21f1, 0x21f2, 0x21f3, 0x21f4, 0x21f5, 0x21f6, 0x21f7,
+ 0x21f8, 0x21f9, 0x21fa, 0x21fb, 0x21fc, 0x21fd, 0x21fe, 0x21ff,
+};
+
+static const unsigned short gNormalizeTable2200[] = {
+ /* U+2200 */
+ 0x2200, 0x2201, 0x2202, 0x2203, 0x2203, 0x2205, 0x2206, 0x2207,
+ 0x2208, 0x2208, 0x220a, 0x220b, 0x220b, 0x220d, 0x220e, 0x220f,
+ 0x2210, 0x2211, 0x2212, 0x2213, 0x2214, 0x2215, 0x2216, 0x2217,
+ 0x2218, 0x2219, 0x221a, 0x221b, 0x221c, 0x221d, 0x221e, 0x221f,
+ 0x2220, 0x2221, 0x2222, 0x2223, 0x2223, 0x2225, 0x2225, 0x2227,
+ 0x2228, 0x2229, 0x222a, 0x222b, 0x222b, 0x222b, 0x222e, 0x222e,
+ 0x222e, 0x2231, 0x2232, 0x2233, 0x2234, 0x2235, 0x2236, 0x2237,
+ 0x2238, 0x2239, 0x223a, 0x223b, 0x223c, 0x223d, 0x223e, 0x223f,
+};
+
+static const unsigned short gNormalizeTable2240[] = {
+ /* U+2240 */
+ 0x2240, 0x223c, 0x2242, 0x2243, 0x2243, 0x2245, 0x2246, 0x2245,
+ 0x2248, 0x2248, 0x224a, 0x224b, 0x224c, 0x224d, 0x224e, 0x224f,
+ 0x2250, 0x2251, 0x2252, 0x2253, 0x2254, 0x2255, 0x2256, 0x2257,
+ 0x2258, 0x2259, 0x225a, 0x225b, 0x225c, 0x225d, 0x225e, 0x225f,
+ 0x003d, 0x2261, 0x2261, 0x2263, 0x2264, 0x2265, 0x2266, 0x2267,
+ 0x2268, 0x2269, 0x226a, 0x226b, 0x226c, 0x224d, 0x003c, 0x003e,
+ 0x2264, 0x2265, 0x2272, 0x2273, 0x2272, 0x2273, 0x2276, 0x2277,
+ 0x2276, 0x2277, 0x227a, 0x227b, 0x227c, 0x227d, 0x227e, 0x227f,
+};
+
+static const unsigned short gNormalizeTable2280[] = {
+ /* U+2280 */
+ 0x227a, 0x227b, 0x2282, 0x2283, 0x2282, 0x2283, 0x2286, 0x2287,
+ 0x2286, 0x2287, 0x228a, 0x228b, 0x228c, 0x228d, 0x228e, 0x228f,
+ 0x2290, 0x2291, 0x2292, 0x2293, 0x2294, 0x2295, 0x2296, 0x2297,
+ 0x2298, 0x2299, 0x229a, 0x229b, 0x229c, 0x229d, 0x229e, 0x229f,
+ 0x22a0, 0x22a1, 0x22a2, 0x22a3, 0x22a4, 0x22a5, 0x22a6, 0x22a7,
+ 0x22a8, 0x22a9, 0x22aa, 0x22ab, 0x22a2, 0x22a8, 0x22a9, 0x22ab,
+ 0x22b0, 0x22b1, 0x22b2, 0x22b3, 0x22b4, 0x22b5, 0x22b6, 0x22b7,
+ 0x22b8, 0x22b9, 0x22ba, 0x22bb, 0x22bc, 0x22bd, 0x22be, 0x22bf,
+};
+
+static const unsigned short gNormalizeTable22c0[] = {
+ /* U+22c0 */
+ 0x22c0, 0x22c1, 0x22c2, 0x22c3, 0x22c4, 0x22c5, 0x22c6, 0x22c7,
+ 0x22c8, 0x22c9, 0x22ca, 0x22cb, 0x22cc, 0x22cd, 0x22ce, 0x22cf,
+ 0x22d0, 0x22d1, 0x22d2, 0x22d3, 0x22d4, 0x22d5, 0x22d6, 0x22d7,
+ 0x22d8, 0x22d9, 0x22da, 0x22db, 0x22dc, 0x22dd, 0x22de, 0x22df,
+ 0x227c, 0x227d, 0x2291, 0x2292, 0x22e4, 0x22e5, 0x22e6, 0x22e7,
+ 0x22e8, 0x22e9, 0x22b2, 0x22b3, 0x22b4, 0x22b5, 0x22ee, 0x22ef,
+ 0x22f0, 0x22f1, 0x22f2, 0x22f3, 0x22f4, 0x22f5, 0x22f6, 0x22f7,
+ 0x22f8, 0x22f9, 0x22fa, 0x22fb, 0x22fc, 0x22fd, 0x22fe, 0x22ff,
+};
+
+static const unsigned short gNormalizeTable2300[] = {
+ /* U+2300 */
+ 0x2300, 0x2301, 0x2302, 0x2303, 0x2304, 0x2305, 0x2306, 0x2307,
+ 0x2308, 0x2309, 0x230a, 0x230b, 0x230c, 0x230d, 0x230e, 0x230f,
+ 0x2310, 0x2311, 0x2312, 0x2313, 0x2314, 0x2315, 0x2316, 0x2317,
+ 0x2318, 0x2319, 0x231a, 0x231b, 0x231c, 0x231d, 0x231e, 0x231f,
+ 0x2320, 0x2321, 0x2322, 0x2323, 0x2324, 0x2325, 0x2326, 0x2327,
+ 0x2328, 0x3008, 0x3009, 0x232b, 0x232c, 0x232d, 0x232e, 0x232f,
+ 0x2330, 0x2331, 0x2332, 0x2333, 0x2334, 0x2335, 0x2336, 0x2337,
+ 0x2338, 0x2339, 0x233a, 0x233b, 0x233c, 0x233d, 0x233e, 0x233f,
+};
+
+static const unsigned short gNormalizeTable2440[] = {
+ /* U+2440 */
+ 0x2440, 0x2441, 0x2442, 0x2443, 0x2444, 0x2445, 0x2446, 0x2447,
+ 0x2448, 0x2449, 0x244a, 0x244b, 0x244c, 0x244d, 0x244e, 0x244f,
+ 0x2450, 0x2451, 0x2452, 0x2453, 0x2454, 0x2455, 0x2456, 0x2457,
+ 0x2458, 0x2459, 0x245a, 0x245b, 0x245c, 0x245d, 0x245e, 0x245f,
+ 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
+ 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031,
+ 0x0031, 0x0031, 0x0031, 0x0032, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+};
+
+static const unsigned short gNormalizeTable2480[] = {
+ /* U+2480 */
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
+ 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031,
+ 0x0031, 0x0031, 0x0031, 0x0032, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0061, 0x0062,
+ 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a,
+};
+
+static const unsigned short gNormalizeTable24c0[] = {
+ /* U+24c0 */
+ 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072,
+ 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a,
+ 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068,
+ 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070,
+ 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
+ 0x0079, 0x007a, 0x0030, 0x24eb, 0x24ec, 0x24ed, 0x24ee, 0x24ef,
+ 0x24f0, 0x24f1, 0x24f2, 0x24f3, 0x24f4, 0x24f5, 0x24f6, 0x24f7,
+ 0x24f8, 0x24f9, 0x24fa, 0x24fb, 0x24fc, 0x24fd, 0x24fe, 0x24ff,
+};
+
+static const unsigned short gNormalizeTable2a00[] = {
+ /* U+2a00 */
+ 0x2a00, 0x2a01, 0x2a02, 0x2a03, 0x2a04, 0x2a05, 0x2a06, 0x2a07,
+ 0x2a08, 0x2a09, 0x2a0a, 0x2a0b, 0x222b, 0x2a0d, 0x2a0e, 0x2a0f,
+ 0x2a10, 0x2a11, 0x2a12, 0x2a13, 0x2a14, 0x2a15, 0x2a16, 0x2a17,
+ 0x2a18, 0x2a19, 0x2a1a, 0x2a1b, 0x2a1c, 0x2a1d, 0x2a1e, 0x2a1f,
+ 0x2a20, 0x2a21, 0x2a22, 0x2a23, 0x2a24, 0x2a25, 0x2a26, 0x2a27,
+ 0x2a28, 0x2a29, 0x2a2a, 0x2a2b, 0x2a2c, 0x2a2d, 0x2a2e, 0x2a2f,
+ 0x2a30, 0x2a31, 0x2a32, 0x2a33, 0x2a34, 0x2a35, 0x2a36, 0x2a37,
+ 0x2a38, 0x2a39, 0x2a3a, 0x2a3b, 0x2a3c, 0x2a3d, 0x2a3e, 0x2a3f,
+};
+
+static const unsigned short gNormalizeTable2a40[] = {
+ /* U+2a40 */
+ 0x2a40, 0x2a41, 0x2a42, 0x2a43, 0x2a44, 0x2a45, 0x2a46, 0x2a47,
+ 0x2a48, 0x2a49, 0x2a4a, 0x2a4b, 0x2a4c, 0x2a4d, 0x2a4e, 0x2a4f,
+ 0x2a50, 0x2a51, 0x2a52, 0x2a53, 0x2a54, 0x2a55, 0x2a56, 0x2a57,
+ 0x2a58, 0x2a59, 0x2a5a, 0x2a5b, 0x2a5c, 0x2a5d, 0x2a5e, 0x2a5f,
+ 0x2a60, 0x2a61, 0x2a62, 0x2a63, 0x2a64, 0x2a65, 0x2a66, 0x2a67,
+ 0x2a68, 0x2a69, 0x2a6a, 0x2a6b, 0x2a6c, 0x2a6d, 0x2a6e, 0x2a6f,
+ 0x2a70, 0x2a71, 0x2a72, 0x2a73, 0x003a, 0x003d, 0x003d, 0x2a77,
+ 0x2a78, 0x2a79, 0x2a7a, 0x2a7b, 0x2a7c, 0x2a7d, 0x2a7e, 0x2a7f,
+};
+
+static const unsigned short gNormalizeTable2ac0[] = {
+ /* U+2ac0 */
+ 0x2ac0, 0x2ac1, 0x2ac2, 0x2ac3, 0x2ac4, 0x2ac5, 0x2ac6, 0x2ac7,
+ 0x2ac8, 0x2ac9, 0x2aca, 0x2acb, 0x2acc, 0x2acd, 0x2ace, 0x2acf,
+ 0x2ad0, 0x2ad1, 0x2ad2, 0x2ad3, 0x2ad4, 0x2ad5, 0x2ad6, 0x2ad7,
+ 0x2ad8, 0x2ad9, 0x2ada, 0x2adb, 0x2add, 0x2add, 0x2ade, 0x2adf,
+ 0x2ae0, 0x2ae1, 0x2ae2, 0x2ae3, 0x2ae4, 0x2ae5, 0x2ae6, 0x2ae7,
+ 0x2ae8, 0x2ae9, 0x2aea, 0x2aeb, 0x2aec, 0x2aed, 0x2aee, 0x2aef,
+ 0x2af0, 0x2af1, 0x2af2, 0x2af3, 0x2af4, 0x2af5, 0x2af6, 0x2af7,
+ 0x2af8, 0x2af9, 0x2afa, 0x2afb, 0x2afc, 0x2afd, 0x2afe, 0x2aff,
+};
+
+static const unsigned short gNormalizeTable2c00[] = {
+ /* U+2c00 */
+ 0x2c30, 0x2c31, 0x2c32, 0x2c33, 0x2c34, 0x2c35, 0x2c36, 0x2c37,
+ 0x2c38, 0x2c39, 0x2c3a, 0x2c3b, 0x2c3c, 0x2c3d, 0x2c3e, 0x2c3f,
+ 0x2c40, 0x2c41, 0x2c42, 0x2c43, 0x2c44, 0x2c45, 0x2c46, 0x2c47,
+ 0x2c48, 0x2c49, 0x2c4a, 0x2c4b, 0x2c4c, 0x2c4d, 0x2c4e, 0x2c4f,
+ 0x2c50, 0x2c51, 0x2c52, 0x2c53, 0x2c54, 0x2c55, 0x2c56, 0x2c57,
+ 0x2c58, 0x2c59, 0x2c5a, 0x2c5b, 0x2c5c, 0x2c5d, 0x2c5e, 0x2c2f,
+ 0x2c30, 0x2c31, 0x2c32, 0x2c33, 0x2c34, 0x2c35, 0x2c36, 0x2c37,
+ 0x2c38, 0x2c39, 0x2c3a, 0x2c3b, 0x2c3c, 0x2c3d, 0x2c3e, 0x2c3f,
+};
+
+static const unsigned short gNormalizeTable2c40[] = {
+ /* U+2c40 */
+ 0x2c40, 0x2c41, 0x2c42, 0x2c43, 0x2c44, 0x2c45, 0x2c46, 0x2c47,
+ 0x2c48, 0x2c49, 0x2c4a, 0x2c4b, 0x2c4c, 0x2c4d, 0x2c4e, 0x2c4f,
+ 0x2c50, 0x2c51, 0x2c52, 0x2c53, 0x2c54, 0x2c55, 0x2c56, 0x2c57,
+ 0x2c58, 0x2c59, 0x2c5a, 0x2c5b, 0x2c5c, 0x2c5d, 0x2c5e, 0x2c5f,
+ 0x2c61, 0x2c61, 0x026b, 0x1d7d, 0x027d, 0x2c65, 0x2c66, 0x2c68,
+ 0x2c68, 0x2c6a, 0x2c6a, 0x2c6c, 0x2c6c, 0x0251, 0x0271, 0x0250,
+ 0x0252, 0x2c71, 0x2c73, 0x2c73, 0x2c74, 0x2c76, 0x2c76, 0x2c77,
+ 0x2c78, 0x2c79, 0x2c7a, 0x2c7b, 0x006a, 0x0076, 0x023f, 0x0240,
+};
+
+static const unsigned short gNormalizeTable2c80[] = {
+ /* U+2c80 */
+ 0x2c81, 0x2c81, 0x2c83, 0x2c83, 0x2c85, 0x2c85, 0x2c87, 0x2c87,
+ 0x2c89, 0x2c89, 0x2c8b, 0x2c8b, 0x2c8d, 0x2c8d, 0x2c8f, 0x2c8f,
+ 0x2c91, 0x2c91, 0x2c93, 0x2c93, 0x2c95, 0x2c95, 0x2c97, 0x2c97,
+ 0x2c99, 0x2c99, 0x2c9b, 0x2c9b, 0x2c9d, 0x2c9d, 0x2c9f, 0x2c9f,
+ 0x2ca1, 0x2ca1, 0x2ca3, 0x2ca3, 0x2ca5, 0x2ca5, 0x2ca7, 0x2ca7,
+ 0x2ca9, 0x2ca9, 0x2cab, 0x2cab, 0x2cad, 0x2cad, 0x2caf, 0x2caf,
+ 0x2cb1, 0x2cb1, 0x2cb3, 0x2cb3, 0x2cb5, 0x2cb5, 0x2cb7, 0x2cb7,
+ 0x2cb9, 0x2cb9, 0x2cbb, 0x2cbb, 0x2cbd, 0x2cbd, 0x2cbf, 0x2cbf,
+};
+
+static const unsigned short gNormalizeTable2cc0[] = {
+ /* U+2cc0 */
+ 0x2cc1, 0x2cc1, 0x2cc3, 0x2cc3, 0x2cc5, 0x2cc5, 0x2cc7, 0x2cc7,
+ 0x2cc9, 0x2cc9, 0x2ccb, 0x2ccb, 0x2ccd, 0x2ccd, 0x2ccf, 0x2ccf,
+ 0x2cd1, 0x2cd1, 0x2cd3, 0x2cd3, 0x2cd5, 0x2cd5, 0x2cd7, 0x2cd7,
+ 0x2cd9, 0x2cd9, 0x2cdb, 0x2cdb, 0x2cdd, 0x2cdd, 0x2cdf, 0x2cdf,
+ 0x2ce1, 0x2ce1, 0x2ce3, 0x2ce3, 0x2ce4, 0x2ce5, 0x2ce6, 0x2ce7,
+ 0x2ce8, 0x2ce9, 0x2cea, 0x2cec, 0x2cec, 0x2cee, 0x2cee, 0x2cef,
+ 0x2cf0, 0x2cf1, 0x2cf2, 0x2cf3, 0x2cf4, 0x2cf5, 0x2cf6, 0x2cf7,
+ 0x2cf8, 0x2cf9, 0x2cfa, 0x2cfb, 0x2cfc, 0x2cfd, 0x2cfe, 0x2cff,
+};
+
+static const unsigned short gNormalizeTable2d40[] = {
+ /* U+2d40 */
+ 0x2d40, 0x2d41, 0x2d42, 0x2d43, 0x2d44, 0x2d45, 0x2d46, 0x2d47,
+ 0x2d48, 0x2d49, 0x2d4a, 0x2d4b, 0x2d4c, 0x2d4d, 0x2d4e, 0x2d4f,
+ 0x2d50, 0x2d51, 0x2d52, 0x2d53, 0x2d54, 0x2d55, 0x2d56, 0x2d57,
+ 0x2d58, 0x2d59, 0x2d5a, 0x2d5b, 0x2d5c, 0x2d5d, 0x2d5e, 0x2d5f,
+ 0x2d60, 0x2d61, 0x2d62, 0x2d63, 0x2d64, 0x2d65, 0x2d66, 0x2d67,
+ 0x2d68, 0x2d69, 0x2d6a, 0x2d6b, 0x2d6c, 0x2d6d, 0x2d6e, 0x2d61,
+ 0x2d70, 0x2d71, 0x2d72, 0x2d73, 0x2d74, 0x2d75, 0x2d76, 0x2d77,
+ 0x2d78, 0x2d79, 0x2d7a, 0x2d7b, 0x2d7c, 0x2d7d, 0x2d7e, 0x2d7f,
+};
+
+static const unsigned short gNormalizeTable2e80[] = {
+ /* U+2e80 */
+ 0x2e80, 0x2e81, 0x2e82, 0x2e83, 0x2e84, 0x2e85, 0x2e86, 0x2e87,
+ 0x2e88, 0x2e89, 0x2e8a, 0x2e8b, 0x2e8c, 0x2e8d, 0x2e8e, 0x2e8f,
+ 0x2e90, 0x2e91, 0x2e92, 0x2e93, 0x2e94, 0x2e95, 0x2e96, 0x2e97,
+ 0x2e98, 0x2e99, 0x2e9a, 0x2e9b, 0x2e9c, 0x2e9d, 0x2e9e, 0x6bcd,
+ 0x2ea0, 0x2ea1, 0x2ea2, 0x2ea3, 0x2ea4, 0x2ea5, 0x2ea6, 0x2ea7,
+ 0x2ea8, 0x2ea9, 0x2eaa, 0x2eab, 0x2eac, 0x2ead, 0x2eae, 0x2eaf,
+ 0x2eb0, 0x2eb1, 0x2eb2, 0x2eb3, 0x2eb4, 0x2eb5, 0x2eb6, 0x2eb7,
+ 0x2eb8, 0x2eb9, 0x2eba, 0x2ebb, 0x2ebc, 0x2ebd, 0x2ebe, 0x2ebf,
+};
+
+static const unsigned short gNormalizeTable2ec0[] = {
+ /* U+2ec0 */
+ 0x2ec0, 0x2ec1, 0x2ec2, 0x2ec3, 0x2ec4, 0x2ec5, 0x2ec6, 0x2ec7,
+ 0x2ec8, 0x2ec9, 0x2eca, 0x2ecb, 0x2ecc, 0x2ecd, 0x2ece, 0x2ecf,
+ 0x2ed0, 0x2ed1, 0x2ed2, 0x2ed3, 0x2ed4, 0x2ed5, 0x2ed6, 0x2ed7,
+ 0x2ed8, 0x2ed9, 0x2eda, 0x2edb, 0x2edc, 0x2edd, 0x2ede, 0x2edf,
+ 0x2ee0, 0x2ee1, 0x2ee2, 0x2ee3, 0x2ee4, 0x2ee5, 0x2ee6, 0x2ee7,
+ 0x2ee8, 0x2ee9, 0x2eea, 0x2eeb, 0x2eec, 0x2eed, 0x2eee, 0x2eef,
+ 0x2ef0, 0x2ef1, 0x2ef2, 0x9f9f, 0x2ef4, 0x2ef5, 0x2ef6, 0x2ef7,
+ 0x2ef8, 0x2ef9, 0x2efa, 0x2efb, 0x2efc, 0x2efd, 0x2efe, 0x2eff,
+};
+
+static const unsigned short gNormalizeTable2f00[] = {
+ /* U+2f00 */
+ 0x4e00, 0x4e28, 0x4e36, 0x4e3f, 0x4e59, 0x4e85, 0x4e8c, 0x4ea0,
+ 0x4eba, 0x513f, 0x5165, 0x516b, 0x5182, 0x5196, 0x51ab, 0x51e0,
+ 0x51f5, 0x5200, 0x529b, 0x52f9, 0x5315, 0x531a, 0x5338, 0x5341,
+ 0x535c, 0x5369, 0x5382, 0x53b6, 0x53c8, 0x53e3, 0x56d7, 0x571f,
+ 0x58eb, 0x5902, 0x590a, 0x5915, 0x5927, 0x5973, 0x5b50, 0x5b80,
+ 0x5bf8, 0x5c0f, 0x5c22, 0x5c38, 0x5c6e, 0x5c71, 0x5ddb, 0x5de5,
+ 0x5df1, 0x5dfe, 0x5e72, 0x5e7a, 0x5e7f, 0x5ef4, 0x5efe, 0x5f0b,
+ 0x5f13, 0x5f50, 0x5f61, 0x5f73, 0x5fc3, 0x6208, 0x6236, 0x624b,
+};
+
+static const unsigned short gNormalizeTable2f40[] = {
+ /* U+2f40 */
+ 0x652f, 0x6534, 0x6587, 0x6597, 0x65a4, 0x65b9, 0x65e0, 0x65e5,
+ 0x66f0, 0x6708, 0x6728, 0x6b20, 0x6b62, 0x6b79, 0x6bb3, 0x6bcb,
+ 0x6bd4, 0x6bdb, 0x6c0f, 0x6c14, 0x6c34, 0x706b, 0x722a, 0x7236,
+ 0x723b, 0x723f, 0x7247, 0x7259, 0x725b, 0x72ac, 0x7384, 0x7389,
+ 0x74dc, 0x74e6, 0x7518, 0x751f, 0x7528, 0x7530, 0x758b, 0x7592,
+ 0x7676, 0x767d, 0x76ae, 0x76bf, 0x76ee, 0x77db, 0x77e2, 0x77f3,
+ 0x793a, 0x79b8, 0x79be, 0x7a74, 0x7acb, 0x7af9, 0x7c73, 0x7cf8,
+ 0x7f36, 0x7f51, 0x7f8a, 0x7fbd, 0x8001, 0x800c, 0x8012, 0x8033,
+};
+
+static const unsigned short gNormalizeTable2f80[] = {
+ /* U+2f80 */
+ 0x807f, 0x8089, 0x81e3, 0x81ea, 0x81f3, 0x81fc, 0x820c, 0x821b,
+ 0x821f, 0x826e, 0x8272, 0x8278, 0x864d, 0x866b, 0x8840, 0x884c,
+ 0x8863, 0x897e, 0x898b, 0x89d2, 0x8a00, 0x8c37, 0x8c46, 0x8c55,
+ 0x8c78, 0x8c9d, 0x8d64, 0x8d70, 0x8db3, 0x8eab, 0x8eca, 0x8f9b,
+ 0x8fb0, 0x8fb5, 0x9091, 0x9149, 0x91c6, 0x91cc, 0x91d1, 0x9577,
+ 0x9580, 0x961c, 0x96b6, 0x96b9, 0x96e8, 0x9751, 0x975e, 0x9762,
+ 0x9769, 0x97cb, 0x97ed, 0x97f3, 0x9801, 0x98a8, 0x98db, 0x98df,
+ 0x9996, 0x9999, 0x99ac, 0x9aa8, 0x9ad8, 0x9adf, 0x9b25, 0x9b2f,
+};
+
+static const unsigned short gNormalizeTable2fc0[] = {
+ /* U+2fc0 */
+ 0x9b32, 0x9b3c, 0x9b5a, 0x9ce5, 0x9e75, 0x9e7f, 0x9ea5, 0x9ebb,
+ 0x9ec3, 0x9ecd, 0x9ed1, 0x9ef9, 0x9efd, 0x9f0e, 0x9f13, 0x9f20,
+ 0x9f3b, 0x9f4a, 0x9f52, 0x9f8d, 0x9f9c, 0x9fa0, 0x2fd6, 0x2fd7,
+ 0x2fd8, 0x2fd9, 0x2fda, 0x2fdb, 0x2fdc, 0x2fdd, 0x2fde, 0x2fdf,
+ 0x2fe0, 0x2fe1, 0x2fe2, 0x2fe3, 0x2fe4, 0x2fe5, 0x2fe6, 0x2fe7,
+ 0x2fe8, 0x2fe9, 0x2fea, 0x2feb, 0x2fec, 0x2fed, 0x2fee, 0x2fef,
+ 0x2ff0, 0x2ff1, 0x2ff2, 0x2ff3, 0x2ff4, 0x2ff5, 0x2ff6, 0x2ff7,
+ 0x2ff8, 0x2ff9, 0x2ffa, 0x2ffb, 0x2ffc, 0x2ffd, 0x2ffe, 0x2fff,
+};
+
+static const unsigned short gNormalizeTable3000[] = {
+ /* U+3000 */
+ 0x0020, 0x3001, 0x3002, 0x3003, 0x3004, 0x3005, 0x3006, 0x3007,
+ 0x3008, 0x3009, 0x300a, 0x300b, 0x300c, 0x300d, 0x300e, 0x300f,
+ 0x3010, 0x3011, 0x3012, 0x3013, 0x3014, 0x3015, 0x3016, 0x3017,
+ 0x3018, 0x3019, 0x301a, 0x301b, 0x301c, 0x301d, 0x301e, 0x301f,
+ 0x3020, 0x3021, 0x3022, 0x3023, 0x3024, 0x3025, 0x3026, 0x3027,
+ 0x3028, 0x3029, 0x302a, 0x302b, 0x302c, 0x302d, 0x302e, 0x302f,
+ 0x3030, 0x3031, 0x3032, 0x3033, 0x3034, 0x3035, 0x3012, 0x3037,
+ 0x5341, 0x5344, 0x5345, 0x303b, 0x303c, 0x303d, 0x303e, 0x303f,
+};
+
+static const unsigned short gNormalizeTable3040[] = {
+ /* U+3040 */
+ 0x3040, 0x3041, 0x3042, 0x3043, 0x3044, 0x3045, 0x3046, 0x3047,
+ 0x3048, 0x3049, 0x304a, 0x304b, 0x304b, 0x304d, 0x304d, 0x304f,
+ 0x304f, 0x3051, 0x3051, 0x3053, 0x3053, 0x3055, 0x3055, 0x3057,
+ 0x3057, 0x3059, 0x3059, 0x305b, 0x305b, 0x305d, 0x305d, 0x305f,
+ 0x305f, 0x3061, 0x3061, 0x3063, 0x3064, 0x3064, 0x3066, 0x3066,
+ 0x3068, 0x3068, 0x306a, 0x306b, 0x306c, 0x306d, 0x306e, 0x306f,
+ 0x306f, 0x306f, 0x3072, 0x3072, 0x3072, 0x3075, 0x3075, 0x3075,
+ 0x3078, 0x3078, 0x3078, 0x307b, 0x307b, 0x307b, 0x307e, 0x307f,
+};
+
+static const unsigned short gNormalizeTable3080[] = {
+ /* U+3080 */
+ 0x3080, 0x3081, 0x3082, 0x3083, 0x3084, 0x3085, 0x3086, 0x3087,
+ 0x3088, 0x3089, 0x308a, 0x308b, 0x308c, 0x308d, 0x308e, 0x308f,
+ 0x3090, 0x3091, 0x3092, 0x3093, 0x3046, 0x3095, 0x3096, 0x3097,
+ 0x3098, 0x3099, 0x309a, 0x0020, 0x0020, 0x309d, 0x309d, 0x3088,
+ 0x30a0, 0x30a1, 0x30a2, 0x30a3, 0x30a4, 0x30a5, 0x30a6, 0x30a7,
+ 0x30a8, 0x30a9, 0x30aa, 0x30ab, 0x30ab, 0x30ad, 0x30ad, 0x30af,
+ 0x30af, 0x30b1, 0x30b1, 0x30b3, 0x30b3, 0x30b5, 0x30b5, 0x30b7,
+ 0x30b7, 0x30b9, 0x30b9, 0x30bb, 0x30bb, 0x30bd, 0x30bd, 0x30bf,
+};
+
+static const unsigned short gNormalizeTable30c0[] = {
+ /* U+30c0 */
+ 0x30bf, 0x30c1, 0x30c1, 0x30c3, 0x30c4, 0x30c4, 0x30c6, 0x30c6,
+ 0x30c8, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30cf,
+ 0x30cf, 0x30cf, 0x30d2, 0x30d2, 0x30d2, 0x30d5, 0x30d5, 0x30d5,
+ 0x30d8, 0x30d8, 0x30d8, 0x30db, 0x30db, 0x30db, 0x30de, 0x30df,
+ 0x30e0, 0x30e1, 0x30e2, 0x30e3, 0x30e4, 0x30e5, 0x30e6, 0x30e7,
+ 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ee, 0x30ef,
+ 0x30f0, 0x30f1, 0x30f2, 0x30f3, 0x30a6, 0x30f5, 0x30f6, 0x30ef,
+ 0x30f0, 0x30f1, 0x30f2, 0x30fb, 0x30fc, 0x30fd, 0x30fd, 0x30b3,
+};
+
+static const unsigned short gNormalizeTable3100[] = {
+ /* U+3100 */
+ 0x3100, 0x3101, 0x3102, 0x3103, 0x3104, 0x3105, 0x3106, 0x3107,
+ 0x3108, 0x3109, 0x310a, 0x310b, 0x310c, 0x310d, 0x310e, 0x310f,
+ 0x3110, 0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116, 0x3117,
+ 0x3118, 0x3119, 0x311a, 0x311b, 0x311c, 0x311d, 0x311e, 0x311f,
+ 0x3120, 0x3121, 0x3122, 0x3123, 0x3124, 0x3125, 0x3126, 0x3127,
+ 0x3128, 0x3129, 0x312a, 0x312b, 0x312c, 0x312d, 0x312e, 0x312f,
+ 0x3130, 0x1100, 0x1101, 0x11aa, 0x1102, 0x11ac, 0x11ad, 0x1103,
+ 0x1104, 0x1105, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, 0x11b5,
+};
+
+static const unsigned short gNormalizeTable3140[] = {
+ /* U+3140 */
+ 0x111a, 0x1106, 0x1107, 0x1108, 0x1121, 0x1109, 0x110a, 0x110b,
+ 0x110c, 0x110d, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x1161,
+ 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167, 0x1168, 0x1169,
+ 0x116a, 0x116b, 0x116c, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171,
+ 0x1172, 0x1173, 0x1174, 0x1175, 0x0020, 0x1114, 0x1115, 0x11c7,
+ 0x11c8, 0x11cc, 0x11ce, 0x11d3, 0x11d7, 0x11d9, 0x111c, 0x11dd,
+ 0x11df, 0x111d, 0x111e, 0x1120, 0x1122, 0x1123, 0x1127, 0x1129,
+ 0x112b, 0x112c, 0x112d, 0x112e, 0x112f, 0x1132, 0x1136, 0x1140,
+};
+
+static const unsigned short gNormalizeTable3180[] = {
+ /* U+3180 */
+ 0x1147, 0x114c, 0x11f1, 0x11f2, 0x1157, 0x1158, 0x1159, 0x1184,
+ 0x1185, 0x1188, 0x1191, 0x1192, 0x1194, 0x119e, 0x11a1, 0x318f,
+ 0x3190, 0x3191, 0x4e00, 0x4e8c, 0x4e09, 0x56db, 0x4e0a, 0x4e2d,
+ 0x4e0b, 0x7532, 0x4e59, 0x4e19, 0x4e01, 0x5929, 0x5730, 0x4eba,
+ 0x31a0, 0x31a1, 0x31a2, 0x31a3, 0x31a4, 0x31a5, 0x31a6, 0x31a7,
+ 0x31a8, 0x31a9, 0x31aa, 0x31ab, 0x31ac, 0x31ad, 0x31ae, 0x31af,
+ 0x31b0, 0x31b1, 0x31b2, 0x31b3, 0x31b4, 0x31b5, 0x31b6, 0x31b7,
+ 0x31b8, 0x31b9, 0x31ba, 0x31bb, 0x31bc, 0x31bd, 0x31be, 0x31bf,
+};
+
+static const unsigned short gNormalizeTable3200[] = {
+ /* U+3200 */
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x321f,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028,
+};
+
+static const unsigned short gNormalizeTable3240[] = {
+ /* U+3240 */
+ 0x0028, 0x0028, 0x0028, 0x0028, 0x554f, 0x5e7c, 0x6587, 0x7b8f,
+ 0x3248, 0x3249, 0x324a, 0x324b, 0x324c, 0x324d, 0x324e, 0x324f,
+ 0x0070, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032,
+ 0x0032, 0x0032, 0x0033, 0x0033, 0x0033, 0x0033, 0x0033, 0x0033,
+ 0x1100, 0x1102, 0x1103, 0x1105, 0x1106, 0x1107, 0x1109, 0x110b,
+ 0x110c, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x1100, 0x1102,
+ 0x1103, 0x1105, 0x1106, 0x1107, 0x1109, 0x110b, 0x110c, 0x110e,
+ 0x110f, 0x1110, 0x1111, 0x1112, 0x110e, 0x110c, 0x110b, 0x327f,
+};
+
+static const unsigned short gNormalizeTable3280[] = {
+ /* U+3280 */
+ 0x4e00, 0x4e8c, 0x4e09, 0x56db, 0x4e94, 0x516d, 0x4e03, 0x516b,
+ 0x4e5d, 0x5341, 0x6708, 0x706b, 0x6c34, 0x6728, 0x91d1, 0x571f,
+ 0x65e5, 0x682a, 0x6709, 0x793e, 0x540d, 0x7279, 0x8ca1, 0x795d,
+ 0x52b4, 0x79d8, 0x7537, 0x5973, 0x9069, 0x512a, 0x5370, 0x6ce8,
+ 0x9805, 0x4f11, 0x5199, 0x6b63, 0x4e0a, 0x4e2d, 0x4e0b, 0x5de6,
+ 0x53f3, 0x533b, 0x5b97, 0x5b66, 0x76e3, 0x4f01, 0x8cc7, 0x5354,
+ 0x591c, 0x0033, 0x0033, 0x0033, 0x0033, 0x0034, 0x0034, 0x0034,
+ 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0035,
+};
+
+static const unsigned short gNormalizeTable32c0[] = {
+ /* U+32c0 */
+ 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
+ 0x0039, 0x0031, 0x0031, 0x0031, 0x0068, 0x0065, 0x0065, 0x006c,
+ 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, 0x30ad, 0x30af,
+ 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, 0x30bf,
+ 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd,
+ 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de, 0x30df,
+ 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9, 0x30ea,
+ 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f0, 0x30f1, 0x30f2, 0x32ff,
+};
+
+static const unsigned short gNormalizeTable3300[] = {
+ /* U+3300 */
+ 0x30a2, 0x30a2, 0x30a2, 0x30a2, 0x30a4, 0x30a4, 0x30a6, 0x30a8,
+ 0x30a8, 0x30aa, 0x30aa, 0x30ab, 0x30ab, 0x30ab, 0x30ab, 0x30ab,
+ 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad,
+ 0x30af, 0x30af, 0x30af, 0x30af, 0x30b1, 0x30b3, 0x30b3, 0x30b5,
+ 0x30b5, 0x30b7, 0x30bb, 0x30bb, 0x30bf, 0x30c6, 0x30c8, 0x30c8,
+ 0x30ca, 0x30ce, 0x30cf, 0x30cf, 0x30cf, 0x30cf, 0x30d2, 0x30d2,
+ 0x30d2, 0x30d2, 0x30d5, 0x30d5, 0x30d5, 0x30d5, 0x30d8, 0x30d8,
+ 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30db, 0x30db, 0x30db,
+};
+
+static const unsigned short gNormalizeTable3340[] = {
+ /* U+3340 */
+ 0x30db, 0x30db, 0x30db, 0x30de, 0x30de, 0x30de, 0x30de, 0x30de,
+ 0x30df, 0x30df, 0x30df, 0x30e1, 0x30e1, 0x30e1, 0x30e4, 0x30e4,
+ 0x30e6, 0x30ea, 0x30ea, 0x30eb, 0x30eb, 0x30ec, 0x30ec, 0x30ef,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031,
+ 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0032, 0x0032, 0x0032,
+ 0x0032, 0x0068, 0x0064, 0x0061, 0x0062, 0x006f, 0x0070, 0x0064,
+ 0x0064, 0x0064, 0x0069, 0x5e73, 0x662d, 0x5927, 0x660e, 0x682a,
+};
+
+static const unsigned short gNormalizeTable3380[] = {
+ /* U+3380 */
+ 0x0070, 0x006e, 0x03bc, 0x006d, 0x006b, 0x006b, 0x006d, 0x0067,
+ 0x0063, 0x006b, 0x0070, 0x006e, 0x03bc, 0x03bc, 0x006d, 0x006b,
+ 0x0068, 0x006b, 0x006d, 0x0067, 0x0074, 0x03bc, 0x006d, 0x0064,
+ 0x006b, 0x0066, 0x006e, 0x03bc, 0x006d, 0x0063, 0x006b, 0x006d,
+ 0x0063, 0x006d, 0x006b, 0x006d, 0x0063, 0x006d, 0x006b, 0x006d,
+ 0x006d, 0x0070, 0x006b, 0x006d, 0x0067, 0x0072, 0x0072, 0x0072,
+ 0x0070, 0x006e, 0x03bc, 0x006d, 0x0070, 0x006e, 0x03bc, 0x006d,
+ 0x006b, 0x006d, 0x0070, 0x006e, 0x03bc, 0x006d, 0x006b, 0x006d,
+};
+
+static const unsigned short gNormalizeTable33c0[] = {
+ /* U+33c0 */
+ 0x006b, 0x006d, 0x0061, 0x0062, 0x0063, 0x0063, 0x0063, 0x0063,
+ 0x0064, 0x0067, 0x0068, 0x0068, 0x0069, 0x006b, 0x006b, 0x006b,
+ 0x006c, 0x006c, 0x006c, 0x006c, 0x006d, 0x006d, 0x006d, 0x0070,
+ 0x0070, 0x0070, 0x0070, 0x0073, 0x0073, 0x0077, 0x0076, 0x0061,
+ 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
+ 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031,
+ 0x0031, 0x0031, 0x0031, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032,
+ 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0033, 0x0033, 0x0067,
+};
+
+static const unsigned short gNormalizeTablea640[] = {
+ /* U+a640 */
+ 0xa641, 0xa641, 0xa643, 0xa643, 0xa645, 0xa645, 0xa647, 0xa647,
+ 0xa649, 0xa649, 0xa64b, 0xa64b, 0xa64d, 0xa64d, 0xa64f, 0xa64f,
+ 0xa651, 0xa651, 0xa653, 0xa653, 0xa655, 0xa655, 0xa657, 0xa657,
+ 0xa659, 0xa659, 0xa65b, 0xa65b, 0xa65d, 0xa65d, 0xa65f, 0xa65f,
+ 0xa660, 0xa661, 0xa663, 0xa663, 0xa665, 0xa665, 0xa667, 0xa667,
+ 0xa669, 0xa669, 0xa66b, 0xa66b, 0xa66d, 0xa66d, 0xa66e, 0xa66f,
+ 0xa670, 0xa671, 0xa672, 0xa673, 0xa674, 0xa675, 0xa676, 0xa677,
+ 0xa678, 0xa679, 0xa67a, 0xa67b, 0xa67c, 0xa67d, 0xa67e, 0xa67f,
+};
+
+static const unsigned short gNormalizeTablea680[] = {
+ /* U+a680 */
+ 0xa681, 0xa681, 0xa683, 0xa683, 0xa685, 0xa685, 0xa687, 0xa687,
+ 0xa689, 0xa689, 0xa68b, 0xa68b, 0xa68d, 0xa68d, 0xa68f, 0xa68f,
+ 0xa691, 0xa691, 0xa693, 0xa693, 0xa695, 0xa695, 0xa697, 0xa697,
+ 0xa698, 0xa699, 0xa69a, 0xa69b, 0xa69c, 0xa69d, 0xa69e, 0xa69f,
+ 0xa6a0, 0xa6a1, 0xa6a2, 0xa6a3, 0xa6a4, 0xa6a5, 0xa6a6, 0xa6a7,
+ 0xa6a8, 0xa6a9, 0xa6aa, 0xa6ab, 0xa6ac, 0xa6ad, 0xa6ae, 0xa6af,
+ 0xa6b0, 0xa6b1, 0xa6b2, 0xa6b3, 0xa6b4, 0xa6b5, 0xa6b6, 0xa6b7,
+ 0xa6b8, 0xa6b9, 0xa6ba, 0xa6bb, 0xa6bc, 0xa6bd, 0xa6be, 0xa6bf,
+};
+
+static const unsigned short gNormalizeTablea700[] = {
+ /* U+a700 */
+ 0xa700, 0xa701, 0xa702, 0xa703, 0xa704, 0xa705, 0xa706, 0xa707,
+ 0xa708, 0xa709, 0xa70a, 0xa70b, 0xa70c, 0xa70d, 0xa70e, 0xa70f,
+ 0xa710, 0xa711, 0xa712, 0xa713, 0xa714, 0xa715, 0xa716, 0xa717,
+ 0xa718, 0xa719, 0xa71a, 0xa71b, 0xa71c, 0xa71d, 0xa71e, 0xa71f,
+ 0xa720, 0xa721, 0xa723, 0xa723, 0xa725, 0xa725, 0xa727, 0xa727,
+ 0xa729, 0xa729, 0xa72b, 0xa72b, 0xa72d, 0xa72d, 0xa72f, 0xa72f,
+ 0xa730, 0xa731, 0xa733, 0xa733, 0xa735, 0xa735, 0xa737, 0xa737,
+ 0xa739, 0xa739, 0xa73b, 0xa73b, 0xa73d, 0xa73d, 0xa73f, 0xa73f,
+};
+
+static const unsigned short gNormalizeTablea740[] = {
+ /* U+a740 */
+ 0xa741, 0xa741, 0xa743, 0xa743, 0xa745, 0xa745, 0xa747, 0xa747,
+ 0xa749, 0xa749, 0xa74b, 0xa74b, 0xa74d, 0xa74d, 0xa74f, 0xa74f,
+ 0xa751, 0xa751, 0xa753, 0xa753, 0xa755, 0xa755, 0xa757, 0xa757,
+ 0xa759, 0xa759, 0xa75b, 0xa75b, 0xa75d, 0xa75d, 0xa75f, 0xa75f,
+ 0xa761, 0xa761, 0xa763, 0xa763, 0xa765, 0xa765, 0xa767, 0xa767,
+ 0xa769, 0xa769, 0xa76b, 0xa76b, 0xa76d, 0xa76d, 0xa76f, 0xa76f,
+ 0xa76f, 0xa771, 0xa772, 0xa773, 0xa774, 0xa775, 0xa776, 0xa777,
+ 0xa778, 0xa77a, 0xa77a, 0xa77c, 0xa77c, 0x1d79, 0xa77f, 0xa77f,
+};
+
+static const unsigned short gNormalizeTablea780[] = {
+ /* U+a780 */
+ 0xa781, 0xa781, 0xa783, 0xa783, 0xa785, 0xa785, 0xa787, 0xa787,
+ 0xa788, 0xa789, 0xa78a, 0xa78c, 0xa78c, 0xa78d, 0xa78e, 0xa78f,
+ 0xa790, 0xa791, 0xa792, 0xa793, 0xa794, 0xa795, 0xa796, 0xa797,
+ 0xa798, 0xa799, 0xa79a, 0xa79b, 0xa79c, 0xa79d, 0xa79e, 0xa79f,
+ 0xa7a0, 0xa7a1, 0xa7a2, 0xa7a3, 0xa7a4, 0xa7a5, 0xa7a6, 0xa7a7,
+ 0xa7a8, 0xa7a9, 0xa7aa, 0xa7ab, 0xa7ac, 0xa7ad, 0xa7ae, 0xa7af,
+ 0xa7b0, 0xa7b1, 0xa7b2, 0xa7b3, 0xa7b4, 0xa7b5, 0xa7b6, 0xa7b7,
+ 0xa7b8, 0xa7b9, 0xa7ba, 0xa7bb, 0xa7bc, 0xa7bd, 0xa7be, 0xa7bf,
+};
+
+static const unsigned short gNormalizeTablef900[] = {
+ /* U+f900 */
+ 0x8c48, 0x66f4, 0x8eca, 0x8cc8, 0x6ed1, 0x4e32, 0x53e5, 0x9f9c,
+ 0x9f9c, 0x5951, 0x91d1, 0x5587, 0x5948, 0x61f6, 0x7669, 0x7f85,
+ 0x863f, 0x87ba, 0x88f8, 0x908f, 0x6a02, 0x6d1b, 0x70d9, 0x73de,
+ 0x843d, 0x916a, 0x99f1, 0x4e82, 0x5375, 0x6b04, 0x721b, 0x862d,
+ 0x9e1e, 0x5d50, 0x6feb, 0x85cd, 0x8964, 0x62c9, 0x81d8, 0x881f,
+ 0x5eca, 0x6717, 0x6d6a, 0x72fc, 0x90ce, 0x4f86, 0x51b7, 0x52de,
+ 0x64c4, 0x6ad3, 0x7210, 0x76e7, 0x8001, 0x8606, 0x865c, 0x8def,
+ 0x9732, 0x9b6f, 0x9dfa, 0x788c, 0x797f, 0x7da0, 0x83c9, 0x9304,
+};
+
+static const unsigned short gNormalizeTablef940[] = {
+ /* U+f940 */
+ 0x9e7f, 0x8ad6, 0x58df, 0x5f04, 0x7c60, 0x807e, 0x7262, 0x78ca,
+ 0x8cc2, 0x96f7, 0x58d8, 0x5c62, 0x6a13, 0x6dda, 0x6f0f, 0x7d2f,
+ 0x7e37, 0x964b, 0x52d2, 0x808b, 0x51dc, 0x51cc, 0x7a1c, 0x7dbe,
+ 0x83f1, 0x9675, 0x8b80, 0x62cf, 0x6a02, 0x8afe, 0x4e39, 0x5be7,
+ 0x6012, 0x7387, 0x7570, 0x5317, 0x78fb, 0x4fbf, 0x5fa9, 0x4e0d,
+ 0x6ccc, 0x6578, 0x7d22, 0x53c3, 0x585e, 0x7701, 0x8449, 0x8aaa,
+ 0x6bba, 0x8fb0, 0x6c88, 0x62fe, 0x82e5, 0x63a0, 0x7565, 0x4eae,
+ 0x5169, 0x51c9, 0x6881, 0x7ce7, 0x826f, 0x8ad2, 0x91cf, 0x52f5,
+};
+
+static const unsigned short gNormalizeTablef980[] = {
+ /* U+f980 */
+ 0x5442, 0x5973, 0x5eec, 0x65c5, 0x6ffe, 0x792a, 0x95ad, 0x9a6a,
+ 0x9e97, 0x9ece, 0x529b, 0x66c6, 0x6b77, 0x8f62, 0x5e74, 0x6190,
+ 0x6200, 0x649a, 0x6f23, 0x7149, 0x7489, 0x79ca, 0x7df4, 0x806f,
+ 0x8f26, 0x84ee, 0x9023, 0x934a, 0x5217, 0x52a3, 0x54bd, 0x70c8,
+ 0x88c2, 0x8aaa, 0x5ec9, 0x5ff5, 0x637b, 0x6bae, 0x7c3e, 0x7375,
+ 0x4ee4, 0x56f9, 0x5be7, 0x5dba, 0x601c, 0x73b2, 0x7469, 0x7f9a,
+ 0x8046, 0x9234, 0x96f6, 0x9748, 0x9818, 0x4f8b, 0x79ae, 0x91b4,
+ 0x96b8, 0x60e1, 0x4e86, 0x50da, 0x5bee, 0x5c3f, 0x6599, 0x6a02,
+};
+
+static const unsigned short gNormalizeTablef9c0[] = {
+ /* U+f9c0 */
+ 0x71ce, 0x7642, 0x84fc, 0x907c, 0x9f8d, 0x6688, 0x962e, 0x5289,
+ 0x677b, 0x67f3, 0x6d41, 0x6e9c, 0x7409, 0x7559, 0x786b, 0x7d10,
+ 0x985e, 0x516d, 0x622e, 0x9678, 0x502b, 0x5d19, 0x6dea, 0x8f2a,
+ 0x5f8b, 0x6144, 0x6817, 0x7387, 0x9686, 0x5229, 0x540f, 0x5c65,
+ 0x6613, 0x674e, 0x68a8, 0x6ce5, 0x7406, 0x75e2, 0x7f79, 0x88cf,
+ 0x88e1, 0x91cc, 0x96e2, 0x533f, 0x6eba, 0x541d, 0x71d0, 0x7498,
+ 0x85fa, 0x96a3, 0x9c57, 0x9e9f, 0x6797, 0x6dcb, 0x81e8, 0x7acb,
+ 0x7b20, 0x7c92, 0x72c0, 0x7099, 0x8b58, 0x4ec0, 0x8336, 0x523a,
+};
+
+static const unsigned short gNormalizeTablefa00[] = {
+ /* U+fa00 */
+ 0x5207, 0x5ea6, 0x62d3, 0x7cd6, 0x5b85, 0x6d1e, 0x66b4, 0x8f3b,
+ 0x884c, 0x964d, 0x898b, 0x5ed3, 0x5140, 0x55c0, 0xfa0e, 0xfa0f,
+ 0x585a, 0xfa11, 0x6674, 0xfa13, 0xfa14, 0x51de, 0x732a, 0x76ca,
+ 0x793c, 0x795e, 0x7965, 0x798f, 0x9756, 0x7cbe, 0x7fbd, 0xfa1f,
+ 0x8612, 0xfa21, 0x8af8, 0xfa23, 0xfa24, 0x9038, 0x90fd, 0xfa27,
+ 0xfa28, 0xfa29, 0x98ef, 0x98fc, 0x9928, 0x9db4, 0xfa2e, 0xfa2f,
+ 0x4fae, 0x50e7, 0x514d, 0x52c9, 0x52e4, 0x5351, 0x559d, 0x5606,
+ 0x5668, 0x5840, 0x58a8, 0x5c64, 0x5c6e, 0x6094, 0x6168, 0x618e,
+};
+
+static const unsigned short gNormalizeTablefa40[] = {
+ /* U+fa40 */
+ 0x61f2, 0x654f, 0x65e2, 0x6691, 0x6885, 0x6d77, 0x6e1a, 0x6f22,
+ 0x716e, 0x722b, 0x7422, 0x7891, 0x793e, 0x7949, 0x7948, 0x7950,
+ 0x7956, 0x795d, 0x798d, 0x798e, 0x7a40, 0x7a81, 0x7bc0, 0x7df4,
+ 0x7e09, 0x7e41, 0x7f72, 0x8005, 0x81ed, 0x8279, 0x8279, 0x8457,
+ 0x8910, 0x8996, 0x8b01, 0x8b39, 0x8cd3, 0x8d08, 0x8fb6, 0x9038,
+ 0x96e3, 0x97ff, 0x983b, 0x6075, 0xfa6c, 0x8218, 0xfa6e, 0xfa6f,
+ 0x4e26, 0x51b5, 0x5168, 0x4f80, 0x5145, 0x5180, 0x52c7, 0x52fa,
+ 0x559d, 0x5555, 0x5599, 0x55e2, 0x585a, 0x58b3, 0x5944, 0x5954,
+};
+
+static const unsigned short gNormalizeTablefa80[] = {
+ /* U+fa80 */
+ 0x5a62, 0x5b28, 0x5ed2, 0x5ed9, 0x5f69, 0x5fad, 0x60d8, 0x614e,
+ 0x6108, 0x618e, 0x6160, 0x61f2, 0x6234, 0x63c4, 0x641c, 0x6452,
+ 0x6556, 0x6674, 0x6717, 0x671b, 0x6756, 0x6b79, 0x6bba, 0x6d41,
+ 0x6edb, 0x6ecb, 0x6f22, 0x701e, 0x716e, 0x77a7, 0x7235, 0x72af,
+ 0x732a, 0x7471, 0x7506, 0x753b, 0x761d, 0x761f, 0x76ca, 0x76db,
+ 0x76f4, 0x774a, 0x7740, 0x78cc, 0x7ab1, 0x7bc0, 0x7c7b, 0x7d5b,
+ 0x7df4, 0x7f3e, 0x8005, 0x8352, 0x83ef, 0x8779, 0x8941, 0x8986,
+ 0x8996, 0x8abf, 0x8af8, 0x8acb, 0x8b01, 0x8afe, 0x8aed, 0x8b39,
+};
+
+static const unsigned short gNormalizeTablefac0[] = {
+ /* U+fac0 */
+ 0x8b8a, 0x8d08, 0x8f38, 0x9072, 0x9199, 0x9276, 0x967c, 0x96e3,
+ 0x9756, 0x97db, 0x97ff, 0x980b, 0x983b, 0x9b12, 0x9f9c, 0xfacf,
+ 0xfad0, 0xfad1, 0x3b9d, 0x4018, 0x4039, 0xfad5, 0xfad6, 0xfad7,
+ 0x9f43, 0x9f8e, 0xfada, 0xfadb, 0xfadc, 0xfadd, 0xfade, 0xfadf,
+ 0xfae0, 0xfae1, 0xfae2, 0xfae3, 0xfae4, 0xfae5, 0xfae6, 0xfae7,
+ 0xfae8, 0xfae9, 0xfaea, 0xfaeb, 0xfaec, 0xfaed, 0xfaee, 0xfaef,
+ 0xfaf0, 0xfaf1, 0xfaf2, 0xfaf3, 0xfaf4, 0xfaf5, 0xfaf6, 0xfaf7,
+ 0xfaf8, 0xfaf9, 0xfafa, 0xfafb, 0xfafc, 0xfafd, 0xfafe, 0xfaff,
+};
+
+static const unsigned short gNormalizeTablefb00[] = {
+ /* U+fb00 */
+ 0x0066, 0x0066, 0x0066, 0x0066, 0x0066, 0x0073, 0x0073, 0xfb07,
+ 0xfb08, 0xfb09, 0xfb0a, 0xfb0b, 0xfb0c, 0xfb0d, 0xfb0e, 0xfb0f,
+ 0xfb10, 0xfb11, 0xfb12, 0x0574, 0x0574, 0x0574, 0x057e, 0x0574,
+ 0xfb18, 0xfb19, 0xfb1a, 0xfb1b, 0xfb1c, 0x05d9, 0xfb1e, 0x05f2,
+ 0x05e2, 0x05d0, 0x05d3, 0x05d4, 0x05db, 0x05dc, 0x05dd, 0x05e8,
+ 0x05ea, 0x002b, 0x05e9, 0x05e9, 0x05e9, 0x05e9, 0x05d0, 0x05d0,
+ 0x05d0, 0x05d1, 0x05d2, 0x05d3, 0x05d4, 0x05d5, 0x05d6, 0xfb37,
+ 0x05d8, 0x05d9, 0x05da, 0x05db, 0x05dc, 0xfb3d, 0x05de, 0xfb3f,
+};
+
+static const unsigned short gNormalizeTablefb40[] = {
+ /* U+fb40 */
+ 0x05e0, 0x05e1, 0xfb42, 0x05e3, 0x05e4, 0xfb45, 0x05e6, 0x05e7,
+ 0x05e8, 0x05e9, 0x05ea, 0x05d5, 0x05d1, 0x05db, 0x05e4, 0x05d0,
+ 0x0671, 0x0671, 0x067b, 0x067b, 0x067b, 0x067b, 0x067e, 0x067e,
+ 0x067e, 0x067e, 0x0680, 0x0680, 0x0680, 0x0680, 0x067a, 0x067a,
+ 0x067a, 0x067a, 0x067f, 0x067f, 0x067f, 0x067f, 0x0679, 0x0679,
+ 0x0679, 0x0679, 0x06a4, 0x06a4, 0x06a4, 0x06a4, 0x06a6, 0x06a6,
+ 0x06a6, 0x06a6, 0x0684, 0x0684, 0x0684, 0x0684, 0x0683, 0x0683,
+ 0x0683, 0x0683, 0x0686, 0x0686, 0x0686, 0x0686, 0x0687, 0x0687,
+};
+
+static const unsigned short gNormalizeTablefb80[] = {
+ /* U+fb80 */
+ 0x0687, 0x0687, 0x068d, 0x068d, 0x068c, 0x068c, 0x068e, 0x068e,
+ 0x0688, 0x0688, 0x0698, 0x0698, 0x0691, 0x0691, 0x06a9, 0x06a9,
+ 0x06a9, 0x06a9, 0x06af, 0x06af, 0x06af, 0x06af, 0x06b3, 0x06b3,
+ 0x06b3, 0x06b3, 0x06b1, 0x06b1, 0x06b1, 0x06b1, 0x06ba, 0x06ba,
+ 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06d5, 0x06d5, 0x06c1, 0x06c1,
+ 0x06c1, 0x06c1, 0x06be, 0x06be, 0x06be, 0x06be, 0x06d2, 0x06d2,
+ 0x06d2, 0x06d2, 0xfbb2, 0xfbb3, 0xfbb4, 0xfbb5, 0xfbb6, 0xfbb7,
+ 0xfbb8, 0xfbb9, 0xfbba, 0xfbbb, 0xfbbc, 0xfbbd, 0xfbbe, 0xfbbf,
+};
+
+static const unsigned short gNormalizeTablefbc0[] = {
+ /* U+fbc0 */
+ 0xfbc0, 0xfbc1, 0xfbc2, 0xfbc3, 0xfbc4, 0xfbc5, 0xfbc6, 0xfbc7,
+ 0xfbc8, 0xfbc9, 0xfbca, 0xfbcb, 0xfbcc, 0xfbcd, 0xfbce, 0xfbcf,
+ 0xfbd0, 0xfbd1, 0xfbd2, 0x06ad, 0x06ad, 0x06ad, 0x06ad, 0x06c7,
+ 0x06c7, 0x06c6, 0x06c6, 0x06c8, 0x06c8, 0x06c7, 0x06cb, 0x06cb,
+ 0x06c5, 0x06c5, 0x06c9, 0x06c9, 0x06d0, 0x06d0, 0x06d0, 0x06d0,
+ 0x0649, 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x064a, 0x064a, 0x064a, 0x06cc, 0x06cc, 0x06cc, 0x06cc,
+};
+
+static const unsigned short gNormalizeTablefc00[] = {
+ /* U+fc00 */
+ 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x0628,
+ 0x0628, 0x0628, 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a,
+ 0x062a, 0x062b, 0x062b, 0x062b, 0x062b, 0x062c, 0x062c, 0x062d,
+ 0x062d, 0x062e, 0x062e, 0x062e, 0x0633, 0x0633, 0x0633, 0x0633,
+ 0x0635, 0x0635, 0x0636, 0x0636, 0x0636, 0x0636, 0x0637, 0x0637,
+ 0x0638, 0x0639, 0x0639, 0x063a, 0x063a, 0x0641, 0x0641, 0x0641,
+ 0x0641, 0x0641, 0x0641, 0x0642, 0x0642, 0x0642, 0x0642, 0x0643,
+ 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644,
+};
+
+static const unsigned short gNormalizeTablefc40[] = {
+ /* U+fc40 */
+ 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0645,
+ 0x0645, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646,
+ 0x0646, 0x0647, 0x0647, 0x0647, 0x0647, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x064a, 0x064a, 0x0630, 0x0631, 0x0649, 0x0020, 0x0020,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x064a, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x064a, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628,
+ 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062b,
+ 0x062b, 0x062b, 0x062b, 0x062b, 0x0641, 0x0641, 0x0642, 0x0642,
+};
+
+static const unsigned short gNormalizeTablefc80[] = {
+ /* U+fc80 */
+ 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, 0x0644, 0x0644,
+ 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646,
+ 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x0628, 0x0628,
+ 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062c,
+ 0x062c, 0x062d, 0x062d, 0x062e, 0x062e, 0x0633, 0x0633, 0x0633,
+ 0x0633, 0x0635, 0x0635, 0x0635, 0x0636, 0x0636, 0x0636, 0x0636,
+ 0x0637, 0x0638, 0x0639, 0x0639, 0x063a, 0x063a, 0x0641, 0x0641,
+};
+
+static const unsigned short gNormalizeTablefcc0[] = {
+ /* U+fcc0 */
+ 0x0641, 0x0641, 0x0642, 0x0642, 0x0643, 0x0643, 0x0643, 0x0643,
+ 0x0643, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645,
+ 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0647,
+ 0x0647, 0x0647, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a,
+ 0x064a, 0x0628, 0x0628, 0x062a, 0x062a, 0x062b, 0x062b, 0x0633,
+ 0x0633, 0x0634, 0x0634, 0x0643, 0x0643, 0x0644, 0x0646, 0x0646,
+ 0x064a, 0x064a, 0x0640, 0x0640, 0x0640, 0x0637, 0x0637, 0x0639,
+ 0x0639, 0x063a, 0x063a, 0x0633, 0x0633, 0x0634, 0x0634, 0x062d,
+};
+
+static const unsigned short gNormalizeTablefd00[] = {
+ /* U+fd00 */
+ 0x062d, 0x062c, 0x062c, 0x062e, 0x062e, 0x0635, 0x0635, 0x0636,
+ 0x0636, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0633, 0x0635,
+ 0x0636, 0x0637, 0x0637, 0x0639, 0x0639, 0x063a, 0x063a, 0x0633,
+ 0x0633, 0x0634, 0x0634, 0x062d, 0x062d, 0x062c, 0x062c, 0x062e,
+ 0x062e, 0x0635, 0x0635, 0x0636, 0x0636, 0x0634, 0x0634, 0x0634,
+ 0x0634, 0x0634, 0x0633, 0x0635, 0x0636, 0x0634, 0x0634, 0x0634,
+ 0x0634, 0x0633, 0x0634, 0x0637, 0x0633, 0x0633, 0x0633, 0x0634,
+ 0x0634, 0x0634, 0x0637, 0x0638, 0x0627, 0x0627, 0xfd3e, 0xfd3f,
+};
+
+static const unsigned short gNormalizeTablefd40[] = {
+ /* U+fd40 */
+ 0xfd40, 0xfd41, 0xfd42, 0xfd43, 0xfd44, 0xfd45, 0xfd46, 0xfd47,
+ 0xfd48, 0xfd49, 0xfd4a, 0xfd4b, 0xfd4c, 0xfd4d, 0xfd4e, 0xfd4f,
+ 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a,
+ 0x062c, 0x062c, 0x062d, 0x062d, 0x0633, 0x0633, 0x0633, 0x0633,
+ 0x0633, 0x0633, 0x0633, 0x0633, 0x0635, 0x0635, 0x0635, 0x0634,
+ 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0636, 0x0636,
+ 0x0636, 0x0637, 0x0637, 0x0637, 0x0637, 0x0639, 0x0639, 0x0639,
+ 0x0639, 0x063a, 0x063a, 0x063a, 0x0641, 0x0641, 0x0642, 0x0642,
+};
+
+static const unsigned short gNormalizeTablefd80[] = {
+ /* U+fd80 */
+ 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644,
+ 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645,
+ 0xfd90, 0xfd91, 0x0645, 0x0647, 0x0647, 0x0646, 0x0646, 0x0646,
+ 0x0646, 0x0646, 0x0646, 0x0646, 0x064a, 0x064a, 0x0628, 0x062a,
+ 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062c, 0x062c, 0x062c,
+ 0x0633, 0x0635, 0x0634, 0x0636, 0x0644, 0x0644, 0x064a, 0x064a,
+ 0x064a, 0x0645, 0x0642, 0x0646, 0x0642, 0x0644, 0x0639, 0x0643,
+ 0x0646, 0x0645, 0x0644, 0x0643, 0x0644, 0x0646, 0x062c, 0x062d,
+};
+
+static const unsigned short gNormalizeTablefdc0[] = {
+ /* U+fdc0 */
+ 0x0645, 0x0641, 0x0628, 0x0643, 0x0639, 0x0635, 0x0633, 0x0646,
+ 0xfdc8, 0xfdc9, 0xfdca, 0xfdcb, 0xfdcc, 0xfdcd, 0xfdce, 0xfdcf,
+ 0xfdd0, 0xfdd1, 0xfdd2, 0xfdd3, 0xfdd4, 0xfdd5, 0xfdd6, 0xfdd7,
+ 0xfdd8, 0xfdd9, 0xfdda, 0xfddb, 0xfddc, 0xfddd, 0xfdde, 0xfddf,
+ 0xfde0, 0xfde1, 0xfde2, 0xfde3, 0xfde4, 0xfde5, 0xfde6, 0xfde7,
+ 0xfde8, 0xfde9, 0xfdea, 0xfdeb, 0xfdec, 0xfded, 0xfdee, 0xfdef,
+ 0x0635, 0x0642, 0x0627, 0x0627, 0x0645, 0x0635, 0x0631, 0x0639,
+ 0x0648, 0x0635, 0x0635, 0x062c, 0x0631, 0xfdfd, 0xfdfe, 0xfdff,
+};
+
+static const unsigned short gNormalizeTablefe00[] = {
+ /* U+fe00 */
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x002c, 0x3001, 0x3002, 0x003a, 0x003b, 0x0021, 0x003f, 0x3016,
+ 0x3017, 0x002e, 0xfe1a, 0xfe1b, 0xfe1c, 0xfe1d, 0xfe1e, 0xfe1f,
+ 0xfe20, 0xfe21, 0xfe22, 0xfe23, 0xfe24, 0xfe25, 0xfe26, 0xfe27,
+ 0xfe28, 0xfe29, 0xfe2a, 0xfe2b, 0xfe2c, 0xfe2d, 0xfe2e, 0xfe2f,
+ 0x002e, 0x2014, 0x2013, 0x005f, 0x005f, 0x0028, 0x0029, 0x007b,
+ 0x007d, 0x3014, 0x3015, 0x3010, 0x3011, 0x300a, 0x300b, 0x3008,
+};
+
+static const unsigned short gNormalizeTablefe40[] = {
+ /* U+fe40 */
+ 0x3009, 0x300c, 0x300d, 0x300e, 0x300f, 0xfe45, 0xfe46, 0x005b,
+ 0x005d, 0x0020, 0x0020, 0x0020, 0x0020, 0x005f, 0x005f, 0x005f,
+ 0x002c, 0x3001, 0x002e, 0xfe53, 0x003b, 0x003a, 0x003f, 0x0021,
+ 0x2014, 0x0028, 0x0029, 0x007b, 0x007d, 0x3014, 0x3015, 0x0023,
+ 0x0026, 0x002a, 0x002b, 0x002d, 0x003c, 0x003e, 0x003d, 0xfe67,
+ 0x005c, 0x0024, 0x0025, 0x0040, 0xfe6c, 0xfe6d, 0xfe6e, 0xfe6f,
+ 0x0020, 0x0640, 0x0020, 0xfe73, 0x0020, 0xfe75, 0x0020, 0x0640,
+ 0x0020, 0x0640, 0x0020, 0x0640, 0x0020, 0x0640, 0x0020, 0x0640,
+};
+
+static const unsigned short gNormalizeTablefe80[] = {
+ /* U+fe80 */
+ 0x0621, 0x0627, 0x0627, 0x0627, 0x0627, 0x0648, 0x0648, 0x0627,
+ 0x0627, 0x064a, 0x064a, 0x064a, 0x064a, 0x0627, 0x0627, 0x0628,
+ 0x0628, 0x0628, 0x0628, 0x0629, 0x0629, 0x062a, 0x062a, 0x062a,
+ 0x062a, 0x062b, 0x062b, 0x062b, 0x062b, 0x062c, 0x062c, 0x062c,
+ 0x062c, 0x062d, 0x062d, 0x062d, 0x062d, 0x062e, 0x062e, 0x062e,
+ 0x062e, 0x062f, 0x062f, 0x0630, 0x0630, 0x0631, 0x0631, 0x0632,
+ 0x0632, 0x0633, 0x0633, 0x0633, 0x0633, 0x0634, 0x0634, 0x0634,
+ 0x0634, 0x0635, 0x0635, 0x0635, 0x0635, 0x0636, 0x0636, 0x0636,
+};
+
+static const unsigned short gNormalizeTablefec0[] = {
+ /* U+fec0 */
+ 0x0636, 0x0637, 0x0637, 0x0637, 0x0637, 0x0638, 0x0638, 0x0638,
+ 0x0638, 0x0639, 0x0639, 0x0639, 0x0639, 0x063a, 0x063a, 0x063a,
+ 0x063a, 0x0641, 0x0641, 0x0641, 0x0641, 0x0642, 0x0642, 0x0642,
+ 0x0642, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, 0x0644, 0x0644,
+ 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646,
+ 0x0646, 0x0647, 0x0647, 0x0647, 0x0647, 0x0648, 0x0648, 0x0649,
+ 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x0644, 0x0644, 0x0644,
+ 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0xfefd, 0xfefe, 0x0020,
+};
+
+static const unsigned short gNormalizeTableff00[] = {
+ /* U+ff00 */
+ 0xff00, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
+ 0x0040, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
+};
+
+static const unsigned short gNormalizeTableff40[] = {
+ /* U+ff40 */
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x2985,
+ 0x2986, 0x3002, 0x300c, 0x300d, 0x3001, 0x30fb, 0x30f2, 0x30a1,
+ 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30e3, 0x30e5, 0x30e7, 0x30c3,
+ 0x30fc, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, 0x30ad,
+ 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd,
+};
+
+static const unsigned short gNormalizeTableff80[] = {
+ /* U+ff80 */
+ 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc,
+ 0x30cd, 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de,
+ 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9,
+ 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f3, 0x3099, 0x309a,
+ 0x0020, 0x1100, 0x1101, 0x11aa, 0x1102, 0x11ac, 0x11ad, 0x1103,
+ 0x1104, 0x1105, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, 0x11b5,
+ 0x111a, 0x1106, 0x1107, 0x1108, 0x1121, 0x1109, 0x110a, 0x110b,
+ 0x110c, 0x110d, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0xffbf,
+};
+
+static const unsigned short gNormalizeTableffc0[] = {
+ /* U+ffc0 */
+ 0xffc0, 0xffc1, 0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166,
+ 0xffc8, 0xffc9, 0x1167, 0x1168, 0x1169, 0x116a, 0x116b, 0x116c,
+ 0xffd0, 0xffd1, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171, 0x1172,
+ 0xffd8, 0xffd9, 0x1173, 0x1174, 0x1175, 0xffdd, 0xffde, 0xffdf,
+ 0x00a2, 0x00a3, 0x00ac, 0x0020, 0x00a6, 0x00a5, 0x20a9, 0xffe7,
+ 0x2502, 0x2190, 0x2191, 0x2192, 0x2193, 0x25a0, 0x25cb, 0xffef,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x0020, 0xfff9, 0xfffa, 0xfffb, 0xfffc, 0xfffd, 0xfffe, 0xffff,
+};
+
+static const unsigned short* gNormalizeTable[] = {
+ 0,
+ gNormalizeTable0040,
+ gNormalizeTable0080,
+ gNormalizeTable00c0,
+ gNormalizeTable0100,
+ gNormalizeTable0140,
+ gNormalizeTable0180,
+ gNormalizeTable01c0,
+ gNormalizeTable0200,
+ gNormalizeTable0240,
+ gNormalizeTable0280,
+ gNormalizeTable02c0,
+ 0,
+ gNormalizeTable0340,
+ gNormalizeTable0380,
+ gNormalizeTable03c0,
+ gNormalizeTable0400,
+ gNormalizeTable0440,
+ gNormalizeTable0480,
+ gNormalizeTable04c0,
+ gNormalizeTable0500,
+ gNormalizeTable0540,
+ gNormalizeTable0580,
+ 0,
+ gNormalizeTable0600,
+ gNormalizeTable0640,
+ 0,
+ gNormalizeTable06c0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable0900,
+ gNormalizeTable0940,
+ 0,
+ gNormalizeTable09c0,
+ gNormalizeTable0a00,
+ gNormalizeTable0a40,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable0b40,
+ gNormalizeTable0b80,
+ gNormalizeTable0bc0,
+ 0,
+ gNormalizeTable0c40,
+ 0,
+ gNormalizeTable0cc0,
+ 0,
+ gNormalizeTable0d40,
+ 0,
+ gNormalizeTable0dc0,
+ gNormalizeTable0e00,
+ 0,
+ gNormalizeTable0e80,
+ gNormalizeTable0ec0,
+ gNormalizeTable0f00,
+ gNormalizeTable0f40,
+ gNormalizeTable0f80,
+ 0,
+ gNormalizeTable1000,
+ 0,
+ gNormalizeTable1080,
+ gNormalizeTable10c0,
+ 0,
+ gNormalizeTable1140,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable1780,
+ 0,
+ gNormalizeTable1800,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable1b00,
+ gNormalizeTable1b40,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable1d00,
+ gNormalizeTable1d40,
+ gNormalizeTable1d80,
+ 0,
+ gNormalizeTable1e00,
+ gNormalizeTable1e40,
+ gNormalizeTable1e80,
+ gNormalizeTable1ec0,
+ gNormalizeTable1f00,
+ gNormalizeTable1f40,
+ gNormalizeTable1f80,
+ gNormalizeTable1fc0,
+ gNormalizeTable2000,
+ gNormalizeTable2040,
+ gNormalizeTable2080,
+ 0,
+ gNormalizeTable2100,
+ gNormalizeTable2140,
+ gNormalizeTable2180,
+ gNormalizeTable21c0,
+ gNormalizeTable2200,
+ gNormalizeTable2240,
+ gNormalizeTable2280,
+ gNormalizeTable22c0,
+ gNormalizeTable2300,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable2440,
+ gNormalizeTable2480,
+ gNormalizeTable24c0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable2a00,
+ gNormalizeTable2a40,
+ 0,
+ gNormalizeTable2ac0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable2c00,
+ gNormalizeTable2c40,
+ gNormalizeTable2c80,
+ gNormalizeTable2cc0,
+ 0,
+ gNormalizeTable2d40,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTable2e80,
+ gNormalizeTable2ec0,
+ gNormalizeTable2f00,
+ gNormalizeTable2f40,
+ gNormalizeTable2f80,
+ gNormalizeTable2fc0,
+ gNormalizeTable3000,
+ gNormalizeTable3040,
+ gNormalizeTable3080,
+ gNormalizeTable30c0,
+ gNormalizeTable3100,
+ gNormalizeTable3140,
+ gNormalizeTable3180,
+ 0,
+ gNormalizeTable3200,
+ gNormalizeTable3240,
+ gNormalizeTable3280,
+ gNormalizeTable32c0,
+ gNormalizeTable3300,
+ gNormalizeTable3340,
+ gNormalizeTable3380,
+ gNormalizeTable33c0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTablea640,
+ gNormalizeTablea680,
+ 0,
+ gNormalizeTablea700,
+ gNormalizeTablea740,
+ gNormalizeTablea780,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ gNormalizeTablef900,
+ gNormalizeTablef940,
+ gNormalizeTablef980,
+ gNormalizeTablef9c0,
+ gNormalizeTablefa00,
+ gNormalizeTablefa40,
+ gNormalizeTablefa80,
+ gNormalizeTablefac0,
+ gNormalizeTablefb00,
+ gNormalizeTablefb40,
+ gNormalizeTablefb80,
+ gNormalizeTablefbc0,
+ gNormalizeTablefc00,
+ gNormalizeTablefc40,
+ gNormalizeTablefc80,
+ gNormalizeTablefcc0,
+ gNormalizeTablefd00,
+ gNormalizeTablefd40,
+ gNormalizeTablefd80,
+ gNormalizeTablefdc0,
+ gNormalizeTablefe00,
+ gNormalizeTablefe40,
+ gNormalizeTablefe80,
+ gNormalizeTablefec0,
+ gNormalizeTableff00,
+ gNormalizeTableff40,
+ gNormalizeTableff80,
+ gNormalizeTableffc0,
+};
+
+unsigned int normalize_character(const unsigned int c) {
+ if (c >= 0x10000 || !gNormalizeTable[c >> 6]) return c;
+ return gNormalizeTable[c >> 6][c & 0x3f];
+}
diff --git a/comm/mailnews/extensions/fts3/README.mozilla b/comm/mailnews/extensions/fts3/README.mozilla
new file mode 100644
index 0000000000..0bfe7deb35
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/README.mozilla
@@ -0,0 +1,3 @@
+fts3_porter.c code is from SQLite3.
+
+This customized tokenizer "mozporter" by Mozilla supports CJK indexing using bi-gram. So you have to use bi-gram search string if you wanto to search CJK character.
diff --git a/comm/mailnews/extensions/fts3/components.conf b/comm/mailnews/extensions/fts3/components.conf
new file mode 100644
index 0000000000..9a6fc2fb3c
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{a67d724d-0015-4e2e-8cad-b84775330924}",
+ "contract_ids": ["@mozilla.org/messenger/fts3tokenizer;1"],
+ "type": "nsFts3Tokenizer",
+ "headers": ["/comm/mailnews/extensions/fts3/nsFts3Tokenizer.h"],
+ },
+]
diff --git a/comm/mailnews/extensions/fts3/data/README b/comm/mailnews/extensions/fts3/data/README
new file mode 100644
index 0000000000..a6617918b2
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/data/README
@@ -0,0 +1,5 @@
+The data files in this directory come from the ICU project:
+http://bugs.icu-project.org/trac/browser/icu/trunk/source/data/unidata/norm2
+
+They are intended to be consumed by the ICU project's gennorm2 script. We have
+our own script that processes them.
diff --git a/comm/mailnews/extensions/fts3/data/generate_table.py b/comm/mailnews/extensions/fts3/data/generate_table.py
new file mode 100644
index 0000000000..80e0db996c
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/data/generate_table.py
@@ -0,0 +1,269 @@
+#!/usr/bin/python
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Mozilla Thunderbird.
+#
+# The Initial Developer of the Original Code is Mozilla Japan.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Makoto Kato <m_kato@ga2.so-net.ne.jp>
+# Andrew Sutherland <asutherland@asutherland.org>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import re
+
+
+def print_table(f, t):
+ i = f
+ while i <= t:
+ c = array[i]
+ print("0x%04x," % c, end=" ")
+ i = i + 1
+ if not i % 8:
+ print("\n\t", end=" ")
+
+
+print(
+ """/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Japan.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Makoto Kato <m_kato@ga2.so-net.ne.jp>
+ * Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* THIS FILE IS GENERATED BY generate_table.py. DON'T EDIT THIS */
+"""
+)
+
+p = re.compile("([0-9A-F]{4,5})(?:\.\.([0-9A-F]{4,5}))?[=\>]([0-9A-F]{4,5})?")
+G_FROM = 1
+G_TO = 2
+G_FIRSTVAL = 3
+
+# Array whose value at index i is the unicode value unicode character i should
+# map to.
+array = []
+# Contents of gNormalizeTable. We insert zero entries for sub-pages where we
+# have no mappings. We insert references to the tables where we do have
+# such tables.
+globalTable = ["0"]
+# The (exclusive) upper bound of the conversion table, unicode character-wise.
+# This is 0x10000 because our generated table is only 16-bit. This also limits
+# the values we can map to; we perform an identity mapping for target values
+# that >= maxmapping.
+maxmapping = 0x10000
+sizePerTable = 64
+
+# Map characters that the mapping tells us to obliterate to the NUKE_CHAR
+# (such lines look like "FFF0..FFF8>")
+# We do this because if we didn't do this, we would emit these characters as
+# part of a token, which we definitely don't want.
+NUKE_CHAR = 0x20
+
+# --- load case folding table
+# entries in the file look like:
+# 0041>0061
+# 02D8>0020 0306
+# 2000..200A>0020
+#
+# The 0041 (uppercase A) tells us it lowercases to 0061 (lowercase a).
+# The 02D8 is a "spacing clone[s] of diacritic" breve which gets decomposed into
+# a space character and a breve. This entry/type of entry also shows up in
+# 'nfkc.txt'.
+# The 2000..200A covers a range of space characters and maps them down to the
+# 'normal' space character.
+
+file = open("nfkc_cf.txt")
+
+m = None
+line = "\n"
+i = 0x0
+low = high = val = 0
+while i < maxmapping and line:
+ if not m:
+ line = file.readline()
+ m = p.match(line)
+ if not m:
+ continue
+ low = int(m.group(G_FROM), 16)
+ # if G_TO is present, use it, otherwise fallback to low
+ high = m.group(G_TO) and int(m.group(G_TO), 16) or low
+ # if G_FIRSTVAL is present use it, otherwise use NUKE_CHAR
+ val = m.group(G_FIRSTVAL) and int(m.group(G_FIRSTVAL), 16) or NUKE_CHAR
+ continue
+
+ if low <= i <= high:
+ if val >= maxmapping:
+ array.append(i)
+ else:
+ array.append(val)
+ if i == high:
+ m = None
+ else:
+ array.append(i)
+ i = i + 1
+file.close()
+
+# --- load normalization / decomposition table
+# It is important that this file gets processed second because the other table
+# will tell us about mappings from uppercase U with diaeresis to lowercase u
+# with diaeresis. We obviously don't want that clobbering our value. (Although
+# this would work out if we propagated backwards rather than forwards...)
+#
+# - entries in this file that we care about look like:
+# 00A0>0020
+# 0100=0041 0304
+#
+# They are found in the "Canonical and compatibility decomposition mappings"
+# section.
+#
+# The 00A0 is mapping NBSP to the normal space character.
+# The 0100 (a capital A with a bar over top of) is equivalent to 0041 (capital
+# A) plus a 0304 (combining overline). We do not care about the combining
+# marks which is why our regular expression does not capture it.
+#
+#
+# - entries that we do not care about look like:
+# 0300..0314:230
+#
+# These map marks to their canonical combining class which appears to be a way
+# of specifying the precedence / order in which marks should be combined. The
+# key thing is we don't care about them.
+file = open("nfkc.txt")
+line = file.readline()
+m = p.match(line)
+while line:
+ if not m:
+ line = file.readline()
+ m = p.match(line)
+ continue
+
+ low = int(m.group(G_FROM), 16)
+ # if G_TO is present, use it, otherwise fallback to low
+ high = m.group(G_TO) and int(m.group(G_TO), 16) or low
+ # if G_FIRSTVAL is present use it, otherwise fall back to NUKE_CHAR
+ val = m.group(G_FIRSTVAL) and int(m.group(G_FIRSTVAL), 16) or NUKE_CHAR
+ for i in range(low, high + 1):
+ if i < maxmapping and val < maxmapping:
+ array[i] = val
+ m = None
+file.close()
+
+# --- generate a normalized table to support case and accent folding
+
+i = 0
+needTerm = False
+while i < maxmapping:
+ if not i % sizePerTable:
+ # table is empty?
+ j = i
+ while j < i + sizePerTable:
+ if array[j] != j:
+ break
+ j += 1
+
+ if j == i + sizePerTable:
+ if i:
+ globalTable.append("0")
+ i += sizePerTable
+ continue
+
+ if needTerm:
+ print("};\n")
+ globalTable.append("gNormalizeTable%04x" % i)
+ print("static const unsigned short gNormalizeTable%04x[] = {\n\t" % i, end=" ")
+ print("/* U+%04x */\n\t" % i, end=" ")
+ needTerm = True
+ # Decomposition does not case-fold, so we want to compensate by
+ # performing a lookup here. Because decomposition chains can be
+ # example: 01d5, a capital U with a diaeresis and a bar. yes, really.
+ # 01d5 -> 00dc -> 0055 (U) -> 0075 (u)
+ c = array[i]
+ while c != array[c]:
+ c = array[c]
+ if 0x41 <= c <= 0x5A:
+ raise Exception("got an uppercase character somehow: %x => %x" % (i, c))
+ print("0x%04x," % c, end=" ")
+ i = i + 1
+ if not i % 8:
+ print("\n\t", end=" ")
+
+print("};\n\nstatic const unsigned short* gNormalizeTable[] = {", end=" ")
+i = 0
+while i < (maxmapping / sizePerTable):
+ if not i % 4:
+ print("\n\t", end=" ")
+ print(globalTable[i] + ",", end=" ")
+ i += 1
+
+print(
+ """
+};
+
+unsigned int normalize_character(const unsigned int c)
+{
+ if (c >= """
+ + ("0x%x" % (maxmapping,))
+ + """ || !gNormalizeTable[c >> 6])
+ return c;
+ return gNormalizeTable[c >> 6][c & 0x3f];
+}
+"""
+)
diff --git a/comm/mailnews/extensions/fts3/data/nfkc.txt b/comm/mailnews/extensions/fts3/data/nfkc.txt
new file mode 100644
index 0000000000..08aaf353f9
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/data/nfkc.txt
@@ -0,0 +1,5786 @@
+# Copyright (C) 1999-2010, International Business Machines
+# Corporation and others. All Rights Reserved.
+#
+# file name: nfkc.txt
+#
+# machine-generated on: 2009-11-30
+#
+
+# Canonical_Combining_Class (ccc) values
+0300..0314:230
+0315:232
+0316..0319:220
+031A:232
+031B:216
+031C..0320:220
+0321..0322:202
+0323..0326:220
+0327..0328:202
+0329..0333:220
+0334..0338:1
+0339..033C:220
+033D..0344:230
+0345:240
+0346:230
+0347..0349:220
+034A..034C:230
+034D..034E:220
+0350..0352:230
+0353..0356:220
+0357:230
+0358:232
+0359..035A:220
+035B:230
+035C:233
+035D..035E:234
+035F:233
+0360..0361:234
+0362:233
+0363..036F:230
+0483..0487:230
+0591:220
+0592..0595:230
+0596:220
+0597..0599:230
+059A:222
+059B:220
+059C..05A1:230
+05A2..05A7:220
+05A8..05A9:230
+05AA:220
+05AB..05AC:230
+05AD:222
+05AE:228
+05AF:230
+05B0:10
+05B1:11
+05B2:12
+05B3:13
+05B4:14
+05B5:15
+05B6:16
+05B7:17
+05B8:18
+05B9..05BA:19
+05BB:20
+05BC:21
+05BD:22
+05BF:23
+05C1:24
+05C2:25
+05C4:230
+05C5:220
+05C7:18
+0610..0617:230
+0618:30
+0619:31
+061A:32
+064B:27
+064C:28
+064D:29
+064E:30
+064F:31
+0650:32
+0651:33
+0652:34
+0653..0654:230
+0655..0656:220
+0657..065B:230
+065C:220
+065D..065E:230
+0670:35
+06D6..06DC:230
+06DF..06E2:230
+06E3:220
+06E4:230
+06E7..06E8:230
+06EA:220
+06EB..06EC:230
+06ED:220
+0711:36
+0730:230
+0731:220
+0732..0733:230
+0734:220
+0735..0736:230
+0737..0739:220
+073A:230
+073B..073C:220
+073D:230
+073E:220
+073F..0741:230
+0742:220
+0743:230
+0744:220
+0745:230
+0746:220
+0747:230
+0748:220
+0749..074A:230
+07EB..07F1:230
+07F2:220
+07F3:230
+0816..0819:230
+081B..0823:230
+0825..0827:230
+0829..082D:230
+093C:7
+094D:9
+0951:230
+0952:220
+0953..0954:230
+09BC:7
+09CD:9
+0A3C:7
+0A4D:9
+0ABC:7
+0ACD:9
+0B3C:7
+0B4D:9
+0BCD:9
+0C4D:9
+0C55:84
+0C56:91
+0CBC:7
+0CCD:9
+0D4D:9
+0DCA:9
+0E38..0E39:103
+0E3A:9
+0E48..0E4B:107
+0EB8..0EB9:118
+0EC8..0ECB:122
+0F18..0F19:220
+0F35:220
+0F37:220
+0F39:216
+0F71:129
+0F72:130
+0F74:132
+0F7A..0F7D:130
+0F80:130
+0F82..0F83:230
+0F84:9
+0F86..0F87:230
+0FC6:220
+1037:7
+1039..103A:9
+108D:220
+135F:230
+1714:9
+1734:9
+17D2:9
+17DD:230
+18A9:228
+1939:222
+193A:230
+193B:220
+1A17:230
+1A18:220
+1A60:9
+1A75..1A7C:230
+1A7F:220
+1B34:7
+1B44:9
+1B6B:230
+1B6C:220
+1B6D..1B73:230
+1BAA:9
+1C37:7
+1CD0..1CD2:230
+1CD4:1
+1CD5..1CD9:220
+1CDA..1CDB:230
+1CDC..1CDF:220
+1CE0:230
+1CE2..1CE8:1
+1CED:220
+1DC0..1DC1:230
+1DC2:220
+1DC3..1DC9:230
+1DCA:220
+1DCB..1DCC:230
+1DCD:234
+1DCE:214
+1DCF:220
+1DD0:202
+1DD1..1DE6:230
+1DFD:220
+1DFE:230
+1DFF:220
+20D0..20D1:230
+20D2..20D3:1
+20D4..20D7:230
+20D8..20DA:1
+20DB..20DC:230
+20E1:230
+20E5..20E6:1
+20E7:230
+20E8:220
+20E9:230
+20EA..20EB:1
+20EC..20EF:220
+20F0:230
+2CEF..2CF1:230
+2DE0..2DFF:230
+302A:218
+302B:228
+302C:232
+302D:222
+302E..302F:224
+3099..309A:8
+A66F:230
+A67C..A67D:230
+A6F0..A6F1:230
+A806:9
+A8C4:9
+A8E0..A8F1:230
+A92B..A92D:220
+A953:9
+A9B3:7
+A9C0:9
+AAB0:230
+AAB2..AAB3:230
+AAB4:220
+AAB7..AAB8:230
+AABE..AABF:230
+AAC1:230
+ABED:9
+FB1E:26
+FE20..FE26:230
+101FD:220
+10A0D:220
+10A0F:230
+10A38:230
+10A39:1
+10A3A:220
+10A3F:9
+110B9:9
+110BA:7
+1D165..1D166:216
+1D167..1D169:1
+1D16D:226
+1D16E..1D172:216
+1D17B..1D182:220
+1D185..1D189:230
+1D18A..1D18B:220
+1D1AA..1D1AD:230
+1D242..1D244:230
+
+# Canonical and compatibility decomposition mappings
+00A0>0020
+00A8>0020 0308
+00AA>0061
+00AF>0020 0304
+00B2>0032
+00B3>0033
+00B4>0020 0301
+00B5>03BC
+00B8>0020 0327
+00B9>0031
+00BA>006F
+00BC>0031 2044 0034
+00BD>0031 2044 0032
+00BE>0033 2044 0034
+00C0=0041 0300
+00C1=0041 0301
+00C2=0041 0302
+00C3=0041 0303
+00C4=0041 0308
+00C5=0041 030A
+00C7=0043 0327
+00C8=0045 0300
+00C9=0045 0301
+00CA=0045 0302
+00CB=0045 0308
+00CC=0049 0300
+00CD=0049 0301
+00CE=0049 0302
+00CF=0049 0308
+00D1=004E 0303
+00D2=004F 0300
+00D3=004F 0301
+00D4=004F 0302
+00D5=004F 0303
+00D6=004F 0308
+00D9=0055 0300
+00DA=0055 0301
+00DB=0055 0302
+00DC=0055 0308
+00DD=0059 0301
+00E0=0061 0300
+00E1=0061 0301
+00E2=0061 0302
+00E3=0061 0303
+00E4=0061 0308
+00E5=0061 030A
+00E7=0063 0327
+00E8=0065 0300
+00E9=0065 0301
+00EA=0065 0302
+00EB=0065 0308
+00EC=0069 0300
+00ED=0069 0301
+00EE=0069 0302
+00EF=0069 0308
+00F1=006E 0303
+00F2=006F 0300
+00F3=006F 0301
+00F4=006F 0302
+00F5=006F 0303
+00F6=006F 0308
+00F9=0075 0300
+00FA=0075 0301
+00FB=0075 0302
+00FC=0075 0308
+00FD=0079 0301
+00FF=0079 0308
+0100=0041 0304
+0101=0061 0304
+0102=0041 0306
+0103=0061 0306
+0104=0041 0328
+0105=0061 0328
+0106=0043 0301
+0107=0063 0301
+0108=0043 0302
+0109=0063 0302
+010A=0043 0307
+010B=0063 0307
+010C=0043 030C
+010D=0063 030C
+010E=0044 030C
+010F=0064 030C
+0112=0045 0304
+0113=0065 0304
+0114=0045 0306
+0115=0065 0306
+0116=0045 0307
+0117=0065 0307
+0118=0045 0328
+0119=0065 0328
+011A=0045 030C
+011B=0065 030C
+011C=0047 0302
+011D=0067 0302
+011E=0047 0306
+011F=0067 0306
+0120=0047 0307
+0121=0067 0307
+0122=0047 0327
+0123=0067 0327
+0124=0048 0302
+0125=0068 0302
+0128=0049 0303
+0129=0069 0303
+012A=0049 0304
+012B=0069 0304
+012C=0049 0306
+012D=0069 0306
+012E=0049 0328
+012F=0069 0328
+0130=0049 0307
+0132>0049 004A
+0133>0069 006A
+0134=004A 0302
+0135=006A 0302
+0136=004B 0327
+0137=006B 0327
+0139=004C 0301
+013A=006C 0301
+013B=004C 0327
+013C=006C 0327
+013D=004C 030C
+013E=006C 030C
+013F>004C 00B7
+0140>006C 00B7
+0143=004E 0301
+0144=006E 0301
+0145=004E 0327
+0146=006E 0327
+0147=004E 030C
+0148=006E 030C
+0149>02BC 006E
+014C=004F 0304
+014D=006F 0304
+014E=004F 0306
+014F=006F 0306
+0150=004F 030B
+0151=006F 030B
+0154=0052 0301
+0155=0072 0301
+0156=0052 0327
+0157=0072 0327
+0158=0052 030C
+0159=0072 030C
+015A=0053 0301
+015B=0073 0301
+015C=0053 0302
+015D=0073 0302
+015E=0053 0327
+015F=0073 0327
+0160=0053 030C
+0161=0073 030C
+0162=0054 0327
+0163=0074 0327
+0164=0054 030C
+0165=0074 030C
+0168=0055 0303
+0169=0075 0303
+016A=0055 0304
+016B=0075 0304
+016C=0055 0306
+016D=0075 0306
+016E=0055 030A
+016F=0075 030A
+0170=0055 030B
+0171=0075 030B
+0172=0055 0328
+0173=0075 0328
+0174=0057 0302
+0175=0077 0302
+0176=0059 0302
+0177=0079 0302
+0178=0059 0308
+0179=005A 0301
+017A=007A 0301
+017B=005A 0307
+017C=007A 0307
+017D=005A 030C
+017E=007A 030C
+017F>0073
+01A0=004F 031B
+01A1=006F 031B
+01AF=0055 031B
+01B0=0075 031B
+01C4>0044 017D
+01C5>0044 017E
+01C6>0064 017E
+01C7>004C 004A
+01C8>004C 006A
+01C9>006C 006A
+01CA>004E 004A
+01CB>004E 006A
+01CC>006E 006A
+01CD=0041 030C
+01CE=0061 030C
+01CF=0049 030C
+01D0=0069 030C
+01D1=004F 030C
+01D2=006F 030C
+01D3=0055 030C
+01D4=0075 030C
+01D5=00DC 0304
+01D6=00FC 0304
+01D7=00DC 0301
+01D8=00FC 0301
+01D9=00DC 030C
+01DA=00FC 030C
+01DB=00DC 0300
+01DC=00FC 0300
+01DE=00C4 0304
+01DF=00E4 0304
+01E0=0226 0304
+01E1=0227 0304
+01E2=00C6 0304
+01E3=00E6 0304
+01E6=0047 030C
+01E7=0067 030C
+01E8=004B 030C
+01E9=006B 030C
+01EA=004F 0328
+01EB=006F 0328
+01EC=01EA 0304
+01ED=01EB 0304
+01EE=01B7 030C
+01EF=0292 030C
+01F0=006A 030C
+01F1>0044 005A
+01F2>0044 007A
+01F3>0064 007A
+01F4=0047 0301
+01F5=0067 0301
+01F8=004E 0300
+01F9=006E 0300
+01FA=00C5 0301
+01FB=00E5 0301
+01FC=00C6 0301
+01FD=00E6 0301
+01FE=00D8 0301
+01FF=00F8 0301
+0200=0041 030F
+0201=0061 030F
+0202=0041 0311
+0203=0061 0311
+0204=0045 030F
+0205=0065 030F
+0206=0045 0311
+0207=0065 0311
+0208=0049 030F
+0209=0069 030F
+020A=0049 0311
+020B=0069 0311
+020C=004F 030F
+020D=006F 030F
+020E=004F 0311
+020F=006F 0311
+0210=0052 030F
+0211=0072 030F
+0212=0052 0311
+0213=0072 0311
+0214=0055 030F
+0215=0075 030F
+0216=0055 0311
+0217=0075 0311
+0218=0053 0326
+0219=0073 0326
+021A=0054 0326
+021B=0074 0326
+021E=0048 030C
+021F=0068 030C
+0226=0041 0307
+0227=0061 0307
+0228=0045 0327
+0229=0065 0327
+022A=00D6 0304
+022B=00F6 0304
+022C=00D5 0304
+022D=00F5 0304
+022E=004F 0307
+022F=006F 0307
+0230=022E 0304
+0231=022F 0304
+0232=0059 0304
+0233=0079 0304
+02B0>0068
+02B1>0266
+02B2>006A
+02B3>0072
+02B4>0279
+02B5>027B
+02B6>0281
+02B7>0077
+02B8>0079
+02D8>0020 0306
+02D9>0020 0307
+02DA>0020 030A
+02DB>0020 0328
+02DC>0020 0303
+02DD>0020 030B
+02E0>0263
+02E1>006C
+02E2>0073
+02E3>0078
+02E4>0295
+0340>0300
+0341>0301
+0343>0313
+0344>0308 0301
+0374>02B9
+037A>0020 0345
+037E>003B
+0384>0020 0301
+0385>00A8 0301
+0386=0391 0301
+0387>00B7
+0388=0395 0301
+0389=0397 0301
+038A=0399 0301
+038C=039F 0301
+038E=03A5 0301
+038F=03A9 0301
+0390=03CA 0301
+03AA=0399 0308
+03AB=03A5 0308
+03AC=03B1 0301
+03AD=03B5 0301
+03AE=03B7 0301
+03AF=03B9 0301
+03B0=03CB 0301
+03CA=03B9 0308
+03CB=03C5 0308
+03CC=03BF 0301
+03CD=03C5 0301
+03CE=03C9 0301
+03D0>03B2
+03D1>03B8
+03D2>03A5
+03D3>03D2 0301
+03D4>03D2 0308
+03D5>03C6
+03D6>03C0
+03F0>03BA
+03F1>03C1
+03F2>03C2
+03F4>0398
+03F5>03B5
+03F9>03A3
+0400=0415 0300
+0401=0415 0308
+0403=0413 0301
+0407=0406 0308
+040C=041A 0301
+040D=0418 0300
+040E=0423 0306
+0419=0418 0306
+0439=0438 0306
+0450=0435 0300
+0451=0435 0308
+0453=0433 0301
+0457=0456 0308
+045C=043A 0301
+045D=0438 0300
+045E=0443 0306
+0476=0474 030F
+0477=0475 030F
+04C1=0416 0306
+04C2=0436 0306
+04D0=0410 0306
+04D1=0430 0306
+04D2=0410 0308
+04D3=0430 0308
+04D6=0415 0306
+04D7=0435 0306
+04DA=04D8 0308
+04DB=04D9 0308
+04DC=0416 0308
+04DD=0436 0308
+04DE=0417 0308
+04DF=0437 0308
+04E2=0418 0304
+04E3=0438 0304
+04E4=0418 0308
+04E5=0438 0308
+04E6=041E 0308
+04E7=043E 0308
+04EA=04E8 0308
+04EB=04E9 0308
+04EC=042D 0308
+04ED=044D 0308
+04EE=0423 0304
+04EF=0443 0304
+04F0=0423 0308
+04F1=0443 0308
+04F2=0423 030B
+04F3=0443 030B
+04F4=0427 0308
+04F5=0447 0308
+04F8=042B 0308
+04F9=044B 0308
+0587>0565 0582
+0622=0627 0653
+0623=0627 0654
+0624=0648 0654
+0625=0627 0655
+0626=064A 0654
+0675>0627 0674
+0676>0648 0674
+0677>06C7 0674
+0678>064A 0674
+06C0=06D5 0654
+06C2=06C1 0654
+06D3=06D2 0654
+0929=0928 093C
+0931=0930 093C
+0934=0933 093C
+0958>0915 093C
+0959>0916 093C
+095A>0917 093C
+095B>091C 093C
+095C>0921 093C
+095D>0922 093C
+095E>092B 093C
+095F>092F 093C
+09CB=09C7 09BE
+09CC=09C7 09D7
+09DC>09A1 09BC
+09DD>09A2 09BC
+09DF>09AF 09BC
+0A33>0A32 0A3C
+0A36>0A38 0A3C
+0A59>0A16 0A3C
+0A5A>0A17 0A3C
+0A5B>0A1C 0A3C
+0A5E>0A2B 0A3C
+0B48=0B47 0B56
+0B4B=0B47 0B3E
+0B4C=0B47 0B57
+0B5C>0B21 0B3C
+0B5D>0B22 0B3C
+0B94=0B92 0BD7
+0BCA=0BC6 0BBE
+0BCB=0BC7 0BBE
+0BCC=0BC6 0BD7
+0C48=0C46 0C56
+0CC0=0CBF 0CD5
+0CC7=0CC6 0CD5
+0CC8=0CC6 0CD6
+0CCA=0CC6 0CC2
+0CCB=0CCA 0CD5
+0D4A=0D46 0D3E
+0D4B=0D47 0D3E
+0D4C=0D46 0D57
+0DDA=0DD9 0DCA
+0DDC=0DD9 0DCF
+0DDD=0DDC 0DCA
+0DDE=0DD9 0DDF
+0E33>0E4D 0E32
+0EB3>0ECD 0EB2
+0EDC>0EAB 0E99
+0EDD>0EAB 0EA1
+0F0C>0F0B
+0F43>0F42 0FB7
+0F4D>0F4C 0FB7
+0F52>0F51 0FB7
+0F57>0F56 0FB7
+0F5C>0F5B 0FB7
+0F69>0F40 0FB5
+0F73>0F71 0F72
+0F75>0F71 0F74
+0F76>0FB2 0F80
+0F77>0FB2 0F81
+0F78>0FB3 0F80
+0F79>0FB3 0F81
+0F81>0F71 0F80
+0F93>0F92 0FB7
+0F9D>0F9C 0FB7
+0FA2>0FA1 0FB7
+0FA7>0FA6 0FB7
+0FAC>0FAB 0FB7
+0FB9>0F90 0FB5
+1026=1025 102E
+10FC>10DC
+1B06=1B05 1B35
+1B08=1B07 1B35
+1B0A=1B09 1B35
+1B0C=1B0B 1B35
+1B0E=1B0D 1B35
+1B12=1B11 1B35
+1B3B=1B3A 1B35
+1B3D=1B3C 1B35
+1B40=1B3E 1B35
+1B41=1B3F 1B35
+1B43=1B42 1B35
+1D2C>0041
+1D2D>00C6
+1D2E>0042
+1D30>0044
+1D31>0045
+1D32>018E
+1D33>0047
+1D34>0048
+1D35>0049
+1D36>004A
+1D37>004B
+1D38>004C
+1D39>004D
+1D3A>004E
+1D3C>004F
+1D3D>0222
+1D3E>0050
+1D3F>0052
+1D40>0054
+1D41>0055
+1D42>0057
+1D43>0061
+1D44>0250
+1D45>0251
+1D46>1D02
+1D47>0062
+1D48>0064
+1D49>0065
+1D4A>0259
+1D4B>025B
+1D4C>025C
+1D4D>0067
+1D4F>006B
+1D50>006D
+1D51>014B
+1D52>006F
+1D53>0254
+1D54>1D16
+1D55>1D17
+1D56>0070
+1D57>0074
+1D58>0075
+1D59>1D1D
+1D5A>026F
+1D5B>0076
+1D5C>1D25
+1D5D>03B2
+1D5E>03B3
+1D5F>03B4
+1D60>03C6
+1D61>03C7
+1D62>0069
+1D63>0072
+1D64>0075
+1D65>0076
+1D66>03B2
+1D67>03B3
+1D68>03C1
+1D69>03C6
+1D6A>03C7
+1D78>043D
+1D9B>0252
+1D9C>0063
+1D9D>0255
+1D9E>00F0
+1D9F>025C
+1DA0>0066
+1DA1>025F
+1DA2>0261
+1DA3>0265
+1DA4>0268
+1DA5>0269
+1DA6>026A
+1DA7>1D7B
+1DA8>029D
+1DA9>026D
+1DAA>1D85
+1DAB>029F
+1DAC>0271
+1DAD>0270
+1DAE>0272
+1DAF>0273
+1DB0>0274
+1DB1>0275
+1DB2>0278
+1DB3>0282
+1DB4>0283
+1DB5>01AB
+1DB6>0289
+1DB7>028A
+1DB8>1D1C
+1DB9>028B
+1DBA>028C
+1DBB>007A
+1DBC>0290
+1DBD>0291
+1DBE>0292
+1DBF>03B8
+1E00=0041 0325
+1E01=0061 0325
+1E02=0042 0307
+1E03=0062 0307
+1E04=0042 0323
+1E05=0062 0323
+1E06=0042 0331
+1E07=0062 0331
+1E08=00C7 0301
+1E09=00E7 0301
+1E0A=0044 0307
+1E0B=0064 0307
+1E0C=0044 0323
+1E0D=0064 0323
+1E0E=0044 0331
+1E0F=0064 0331
+1E10=0044 0327
+1E11=0064 0327
+1E12=0044 032D
+1E13=0064 032D
+1E14=0112 0300
+1E15=0113 0300
+1E16=0112 0301
+1E17=0113 0301
+1E18=0045 032D
+1E19=0065 032D
+1E1A=0045 0330
+1E1B=0065 0330
+1E1C=0228 0306
+1E1D=0229 0306
+1E1E=0046 0307
+1E1F=0066 0307
+1E20=0047 0304
+1E21=0067 0304
+1E22=0048 0307
+1E23=0068 0307
+1E24=0048 0323
+1E25=0068 0323
+1E26=0048 0308
+1E27=0068 0308
+1E28=0048 0327
+1E29=0068 0327
+1E2A=0048 032E
+1E2B=0068 032E
+1E2C=0049 0330
+1E2D=0069 0330
+1E2E=00CF 0301
+1E2F=00EF 0301
+1E30=004B 0301
+1E31=006B 0301
+1E32=004B 0323
+1E33=006B 0323
+1E34=004B 0331
+1E35=006B 0331
+1E36=004C 0323
+1E37=006C 0323
+1E38=1E36 0304
+1E39=1E37 0304
+1E3A=004C 0331
+1E3B=006C 0331
+1E3C=004C 032D
+1E3D=006C 032D
+1E3E=004D 0301
+1E3F=006D 0301
+1E40=004D 0307
+1E41=006D 0307
+1E42=004D 0323
+1E43=006D 0323
+1E44=004E 0307
+1E45=006E 0307
+1E46=004E 0323
+1E47=006E 0323
+1E48=004E 0331
+1E49=006E 0331
+1E4A=004E 032D
+1E4B=006E 032D
+1E4C=00D5 0301
+1E4D=00F5 0301
+1E4E=00D5 0308
+1E4F=00F5 0308
+1E50=014C 0300
+1E51=014D 0300
+1E52=014C 0301
+1E53=014D 0301
+1E54=0050 0301
+1E55=0070 0301
+1E56=0050 0307
+1E57=0070 0307
+1E58=0052 0307
+1E59=0072 0307
+1E5A=0052 0323
+1E5B=0072 0323
+1E5C=1E5A 0304
+1E5D=1E5B 0304
+1E5E=0052 0331
+1E5F=0072 0331
+1E60=0053 0307
+1E61=0073 0307
+1E62=0053 0323
+1E63=0073 0323
+1E64=015A 0307
+1E65=015B 0307
+1E66=0160 0307
+1E67=0161 0307
+1E68=1E62 0307
+1E69=1E63 0307
+1E6A=0054 0307
+1E6B=0074 0307
+1E6C=0054 0323
+1E6D=0074 0323
+1E6E=0054 0331
+1E6F=0074 0331
+1E70=0054 032D
+1E71=0074 032D
+1E72=0055 0324
+1E73=0075 0324
+1E74=0055 0330
+1E75=0075 0330
+1E76=0055 032D
+1E77=0075 032D
+1E78=0168 0301
+1E79=0169 0301
+1E7A=016A 0308
+1E7B=016B 0308
+1E7C=0056 0303
+1E7D=0076 0303
+1E7E=0056 0323
+1E7F=0076 0323
+1E80=0057 0300
+1E81=0077 0300
+1E82=0057 0301
+1E83=0077 0301
+1E84=0057 0308
+1E85=0077 0308
+1E86=0057 0307
+1E87=0077 0307
+1E88=0057 0323
+1E89=0077 0323
+1E8A=0058 0307
+1E8B=0078 0307
+1E8C=0058 0308
+1E8D=0078 0308
+1E8E=0059 0307
+1E8F=0079 0307
+1E90=005A 0302
+1E91=007A 0302
+1E92=005A 0323
+1E93=007A 0323
+1E94=005A 0331
+1E95=007A 0331
+1E96=0068 0331
+1E97=0074 0308
+1E98=0077 030A
+1E99=0079 030A
+1E9A>0061 02BE
+1E9B>017F 0307
+1EA0=0041 0323
+1EA1=0061 0323
+1EA2=0041 0309
+1EA3=0061 0309
+1EA4=00C2 0301
+1EA5=00E2 0301
+1EA6=00C2 0300
+1EA7=00E2 0300
+1EA8=00C2 0309
+1EA9=00E2 0309
+1EAA=00C2 0303
+1EAB=00E2 0303
+1EAC=1EA0 0302
+1EAD=1EA1 0302
+1EAE=0102 0301
+1EAF=0103 0301
+1EB0=0102 0300
+1EB1=0103 0300
+1EB2=0102 0309
+1EB3=0103 0309
+1EB4=0102 0303
+1EB5=0103 0303
+1EB6=1EA0 0306
+1EB7=1EA1 0306
+1EB8=0045 0323
+1EB9=0065 0323
+1EBA=0045 0309
+1EBB=0065 0309
+1EBC=0045 0303
+1EBD=0065 0303
+1EBE=00CA 0301
+1EBF=00EA 0301
+1EC0=00CA 0300
+1EC1=00EA 0300
+1EC2=00CA 0309
+1EC3=00EA 0309
+1EC4=00CA 0303
+1EC5=00EA 0303
+1EC6=1EB8 0302
+1EC7=1EB9 0302
+1EC8=0049 0309
+1EC9=0069 0309
+1ECA=0049 0323
+1ECB=0069 0323
+1ECC=004F 0323
+1ECD=006F 0323
+1ECE=004F 0309
+1ECF=006F 0309
+1ED0=00D4 0301
+1ED1=00F4 0301
+1ED2=00D4 0300
+1ED3=00F4 0300
+1ED4=00D4 0309
+1ED5=00F4 0309
+1ED6=00D4 0303
+1ED7=00F4 0303
+1ED8=1ECC 0302
+1ED9=1ECD 0302
+1EDA=01A0 0301
+1EDB=01A1 0301
+1EDC=01A0 0300
+1EDD=01A1 0300
+1EDE=01A0 0309
+1EDF=01A1 0309
+1EE0=01A0 0303
+1EE1=01A1 0303
+1EE2=01A0 0323
+1EE3=01A1 0323
+1EE4=0055 0323
+1EE5=0075 0323
+1EE6=0055 0309
+1EE7=0075 0309
+1EE8=01AF 0301
+1EE9=01B0 0301
+1EEA=01AF 0300
+1EEB=01B0 0300
+1EEC=01AF 0309
+1EED=01B0 0309
+1EEE=01AF 0303
+1EEF=01B0 0303
+1EF0=01AF 0323
+1EF1=01B0 0323
+1EF2=0059 0300
+1EF3=0079 0300
+1EF4=0059 0323
+1EF5=0079 0323
+1EF6=0059 0309
+1EF7=0079 0309
+1EF8=0059 0303
+1EF9=0079 0303
+1F00=03B1 0313
+1F01=03B1 0314
+1F02=1F00 0300
+1F03=1F01 0300
+1F04=1F00 0301
+1F05=1F01 0301
+1F06=1F00 0342
+1F07=1F01 0342
+1F08=0391 0313
+1F09=0391 0314
+1F0A=1F08 0300
+1F0B=1F09 0300
+1F0C=1F08 0301
+1F0D=1F09 0301
+1F0E=1F08 0342
+1F0F=1F09 0342
+1F10=03B5 0313
+1F11=03B5 0314
+1F12=1F10 0300
+1F13=1F11 0300
+1F14=1F10 0301
+1F15=1F11 0301
+1F18=0395 0313
+1F19=0395 0314
+1F1A=1F18 0300
+1F1B=1F19 0300
+1F1C=1F18 0301
+1F1D=1F19 0301
+1F20=03B7 0313
+1F21=03B7 0314
+1F22=1F20 0300
+1F23=1F21 0300
+1F24=1F20 0301
+1F25=1F21 0301
+1F26=1F20 0342
+1F27=1F21 0342
+1F28=0397 0313
+1F29=0397 0314
+1F2A=1F28 0300
+1F2B=1F29 0300
+1F2C=1F28 0301
+1F2D=1F29 0301
+1F2E=1F28 0342
+1F2F=1F29 0342
+1F30=03B9 0313
+1F31=03B9 0314
+1F32=1F30 0300
+1F33=1F31 0300
+1F34=1F30 0301
+1F35=1F31 0301
+1F36=1F30 0342
+1F37=1F31 0342
+1F38=0399 0313
+1F39=0399 0314
+1F3A=1F38 0300
+1F3B=1F39 0300
+1F3C=1F38 0301
+1F3D=1F39 0301
+1F3E=1F38 0342
+1F3F=1F39 0342
+1F40=03BF 0313
+1F41=03BF 0314
+1F42=1F40 0300
+1F43=1F41 0300
+1F44=1F40 0301
+1F45=1F41 0301
+1F48=039F 0313
+1F49=039F 0314
+1F4A=1F48 0300
+1F4B=1F49 0300
+1F4C=1F48 0301
+1F4D=1F49 0301
+1F50=03C5 0313
+1F51=03C5 0314
+1F52=1F50 0300
+1F53=1F51 0300
+1F54=1F50 0301
+1F55=1F51 0301
+1F56=1F50 0342
+1F57=1F51 0342
+1F59=03A5 0314
+1F5B=1F59 0300
+1F5D=1F59 0301
+1F5F=1F59 0342
+1F60=03C9 0313
+1F61=03C9 0314
+1F62=1F60 0300
+1F63=1F61 0300
+1F64=1F60 0301
+1F65=1F61 0301
+1F66=1F60 0342
+1F67=1F61 0342
+1F68=03A9 0313
+1F69=03A9 0314
+1F6A=1F68 0300
+1F6B=1F69 0300
+1F6C=1F68 0301
+1F6D=1F69 0301
+1F6E=1F68 0342
+1F6F=1F69 0342
+1F70=03B1 0300
+1F71>03AC
+1F72=03B5 0300
+1F73>03AD
+1F74=03B7 0300
+1F75>03AE
+1F76=03B9 0300
+1F77>03AF
+1F78=03BF 0300
+1F79>03CC
+1F7A=03C5 0300
+1F7B>03CD
+1F7C=03C9 0300
+1F7D>03CE
+1F80=1F00 0345
+1F81=1F01 0345
+1F82=1F02 0345
+1F83=1F03 0345
+1F84=1F04 0345
+1F85=1F05 0345
+1F86=1F06 0345
+1F87=1F07 0345
+1F88=1F08 0345
+1F89=1F09 0345
+1F8A=1F0A 0345
+1F8B=1F0B 0345
+1F8C=1F0C 0345
+1F8D=1F0D 0345
+1F8E=1F0E 0345
+1F8F=1F0F 0345
+1F90=1F20 0345
+1F91=1F21 0345
+1F92=1F22 0345
+1F93=1F23 0345
+1F94=1F24 0345
+1F95=1F25 0345
+1F96=1F26 0345
+1F97=1F27 0345
+1F98=1F28 0345
+1F99=1F29 0345
+1F9A=1F2A 0345
+1F9B=1F2B 0345
+1F9C=1F2C 0345
+1F9D=1F2D 0345
+1F9E=1F2E 0345
+1F9F=1F2F 0345
+1FA0=1F60 0345
+1FA1=1F61 0345
+1FA2=1F62 0345
+1FA3=1F63 0345
+1FA4=1F64 0345
+1FA5=1F65 0345
+1FA6=1F66 0345
+1FA7=1F67 0345
+1FA8=1F68 0345
+1FA9=1F69 0345
+1FAA=1F6A 0345
+1FAB=1F6B 0345
+1FAC=1F6C 0345
+1FAD=1F6D 0345
+1FAE=1F6E 0345
+1FAF=1F6F 0345
+1FB0=03B1 0306
+1FB1=03B1 0304
+1FB2=1F70 0345
+1FB3=03B1 0345
+1FB4=03AC 0345
+1FB6=03B1 0342
+1FB7=1FB6 0345
+1FB8=0391 0306
+1FB9=0391 0304
+1FBA=0391 0300
+1FBB>0386
+1FBC=0391 0345
+1FBD>0020 0313
+1FBE>03B9
+1FBF>0020 0313
+1FC0>0020 0342
+1FC1>00A8 0342
+1FC2=1F74 0345
+1FC3=03B7 0345
+1FC4=03AE 0345
+1FC6=03B7 0342
+1FC7=1FC6 0345
+1FC8=0395 0300
+1FC9>0388
+1FCA=0397 0300
+1FCB>0389
+1FCC=0397 0345
+1FCD>1FBF 0300
+1FCE>1FBF 0301
+1FCF>1FBF 0342
+1FD0=03B9 0306
+1FD1=03B9 0304
+1FD2=03CA 0300
+1FD3>0390
+1FD6=03B9 0342
+1FD7=03CA 0342
+1FD8=0399 0306
+1FD9=0399 0304
+1FDA=0399 0300
+1FDB>038A
+1FDD>1FFE 0300
+1FDE>1FFE 0301
+1FDF>1FFE 0342
+1FE0=03C5 0306
+1FE1=03C5 0304
+1FE2=03CB 0300
+1FE3>03B0
+1FE4=03C1 0313
+1FE5=03C1 0314
+1FE6=03C5 0342
+1FE7=03CB 0342
+1FE8=03A5 0306
+1FE9=03A5 0304
+1FEA=03A5 0300
+1FEB>038E
+1FEC=03A1 0314
+1FED>00A8 0300
+1FEE>0385
+1FEF>0060
+1FF2=1F7C 0345
+1FF3=03C9 0345
+1FF4=03CE 0345
+1FF6=03C9 0342
+1FF7=1FF6 0345
+1FF8=039F 0300
+1FF9>038C
+1FFA=03A9 0300
+1FFB>038F
+1FFC=03A9 0345
+1FFD>00B4
+1FFE>0020 0314
+2000>2002
+2001>2003
+2002>0020
+2003>0020
+2004>0020
+2005>0020
+2006>0020
+2007>0020
+2008>0020
+2009>0020
+200A>0020
+2011>2010
+2017>0020 0333
+2024>002E
+2025>002E 002E
+2026>002E 002E 002E
+202F>0020
+2033>2032 2032
+2034>2032 2032 2032
+2036>2035 2035
+2037>2035 2035 2035
+203C>0021 0021
+203E>0020 0305
+2047>003F 003F
+2048>003F 0021
+2049>0021 003F
+2057>2032 2032 2032 2032
+205F>0020
+2070>0030
+2071>0069
+2074>0034
+2075>0035
+2076>0036
+2077>0037
+2078>0038
+2079>0039
+207A>002B
+207B>2212
+207C>003D
+207D>0028
+207E>0029
+207F>006E
+2080>0030
+2081>0031
+2082>0032
+2083>0033
+2084>0034
+2085>0035
+2086>0036
+2087>0037
+2088>0038
+2089>0039
+208A>002B
+208B>2212
+208C>003D
+208D>0028
+208E>0029
+2090>0061
+2091>0065
+2092>006F
+2093>0078
+2094>0259
+20A8>0052 0073
+2100>0061 002F 0063
+2101>0061 002F 0073
+2102>0043
+2103>00B0 0043
+2105>0063 002F 006F
+2106>0063 002F 0075
+2107>0190
+2109>00B0 0046
+210A>0067
+210B>0048
+210C>0048
+210D>0048
+210E>0068
+210F>0127
+2110>0049
+2111>0049
+2112>004C
+2113>006C
+2115>004E
+2116>004E 006F
+2119>0050
+211A>0051
+211B>0052
+211C>0052
+211D>0052
+2120>0053 004D
+2121>0054 0045 004C
+2122>0054 004D
+2124>005A
+2126>03A9
+2128>005A
+212A>004B
+212B>00C5
+212C>0042
+212D>0043
+212F>0065
+2130>0045
+2131>0046
+2133>004D
+2134>006F
+2135>05D0
+2136>05D1
+2137>05D2
+2138>05D3
+2139>0069
+213B>0046 0041 0058
+213C>03C0
+213D>03B3
+213E>0393
+213F>03A0
+2140>2211
+2145>0044
+2146>0064
+2147>0065
+2148>0069
+2149>006A
+2150>0031 2044 0037
+2151>0031 2044 0039
+2152>0031 2044 0031 0030
+2153>0031 2044 0033
+2154>0032 2044 0033
+2155>0031 2044 0035
+2156>0032 2044 0035
+2157>0033 2044 0035
+2158>0034 2044 0035
+2159>0031 2044 0036
+215A>0035 2044 0036
+215B>0031 2044 0038
+215C>0033 2044 0038
+215D>0035 2044 0038
+215E>0037 2044 0038
+215F>0031 2044
+2160>0049
+2161>0049 0049
+2162>0049 0049 0049
+2163>0049 0056
+2164>0056
+2165>0056 0049
+2166>0056 0049 0049
+2167>0056 0049 0049 0049
+2168>0049 0058
+2169>0058
+216A>0058 0049
+216B>0058 0049 0049
+216C>004C
+216D>0043
+216E>0044
+216F>004D
+2170>0069
+2171>0069 0069
+2172>0069 0069 0069
+2173>0069 0076
+2174>0076
+2175>0076 0069
+2176>0076 0069 0069
+2177>0076 0069 0069 0069
+2178>0069 0078
+2179>0078
+217A>0078 0069
+217B>0078 0069 0069
+217C>006C
+217D>0063
+217E>0064
+217F>006D
+2189>0030 2044 0033
+219A=2190 0338
+219B=2192 0338
+21AE=2194 0338
+21CD=21D0 0338
+21CE=21D4 0338
+21CF=21D2 0338
+2204=2203 0338
+2209=2208 0338
+220C=220B 0338
+2224=2223 0338
+2226=2225 0338
+222C>222B 222B
+222D>222B 222B 222B
+222F>222E 222E
+2230>222E 222E 222E
+2241=223C 0338
+2244=2243 0338
+2247=2245 0338
+2249=2248 0338
+2260=003D 0338
+2262=2261 0338
+226D=224D 0338
+226E=003C 0338
+226F=003E 0338
+2270=2264 0338
+2271=2265 0338
+2274=2272 0338
+2275=2273 0338
+2278=2276 0338
+2279=2277 0338
+2280=227A 0338
+2281=227B 0338
+2284=2282 0338
+2285=2283 0338
+2288=2286 0338
+2289=2287 0338
+22AC=22A2 0338
+22AD=22A8 0338
+22AE=22A9 0338
+22AF=22AB 0338
+22E0=227C 0338
+22E1=227D 0338
+22E2=2291 0338
+22E3=2292 0338
+22EA=22B2 0338
+22EB=22B3 0338
+22EC=22B4 0338
+22ED=22B5 0338
+2329>3008
+232A>3009
+2460>0031
+2461>0032
+2462>0033
+2463>0034
+2464>0035
+2465>0036
+2466>0037
+2467>0038
+2468>0039
+2469>0031 0030
+246A>0031 0031
+246B>0031 0032
+246C>0031 0033
+246D>0031 0034
+246E>0031 0035
+246F>0031 0036
+2470>0031 0037
+2471>0031 0038
+2472>0031 0039
+2473>0032 0030
+2474>0028 0031 0029
+2475>0028 0032 0029
+2476>0028 0033 0029
+2477>0028 0034 0029
+2478>0028 0035 0029
+2479>0028 0036 0029
+247A>0028 0037 0029
+247B>0028 0038 0029
+247C>0028 0039 0029
+247D>0028 0031 0030 0029
+247E>0028 0031 0031 0029
+247F>0028 0031 0032 0029
+2480>0028 0031 0033 0029
+2481>0028 0031 0034 0029
+2482>0028 0031 0035 0029
+2483>0028 0031 0036 0029
+2484>0028 0031 0037 0029
+2485>0028 0031 0038 0029
+2486>0028 0031 0039 0029
+2487>0028 0032 0030 0029
+2488>0031 002E
+2489>0032 002E
+248A>0033 002E
+248B>0034 002E
+248C>0035 002E
+248D>0036 002E
+248E>0037 002E
+248F>0038 002E
+2490>0039 002E
+2491>0031 0030 002E
+2492>0031 0031 002E
+2493>0031 0032 002E
+2494>0031 0033 002E
+2495>0031 0034 002E
+2496>0031 0035 002E
+2497>0031 0036 002E
+2498>0031 0037 002E
+2499>0031 0038 002E
+249A>0031 0039 002E
+249B>0032 0030 002E
+249C>0028 0061 0029
+249D>0028 0062 0029
+249E>0028 0063 0029
+249F>0028 0064 0029
+24A0>0028 0065 0029
+24A1>0028 0066 0029
+24A2>0028 0067 0029
+24A3>0028 0068 0029
+24A4>0028 0069 0029
+24A5>0028 006A 0029
+24A6>0028 006B 0029
+24A7>0028 006C 0029
+24A8>0028 006D 0029
+24A9>0028 006E 0029
+24AA>0028 006F 0029
+24AB>0028 0070 0029
+24AC>0028 0071 0029
+24AD>0028 0072 0029
+24AE>0028 0073 0029
+24AF>0028 0074 0029
+24B0>0028 0075 0029
+24B1>0028 0076 0029
+24B2>0028 0077 0029
+24B3>0028 0078 0029
+24B4>0028 0079 0029
+24B5>0028 007A 0029
+24B6>0041
+24B7>0042
+24B8>0043
+24B9>0044
+24BA>0045
+24BB>0046
+24BC>0047
+24BD>0048
+24BE>0049
+24BF>004A
+24C0>004B
+24C1>004C
+24C2>004D
+24C3>004E
+24C4>004F
+24C5>0050
+24C6>0051
+24C7>0052
+24C8>0053
+24C9>0054
+24CA>0055
+24CB>0056
+24CC>0057
+24CD>0058
+24CE>0059
+24CF>005A
+24D0>0061
+24D1>0062
+24D2>0063
+24D3>0064
+24D4>0065
+24D5>0066
+24D6>0067
+24D7>0068
+24D8>0069
+24D9>006A
+24DA>006B
+24DB>006C
+24DC>006D
+24DD>006E
+24DE>006F
+24DF>0070
+24E0>0071
+24E1>0072
+24E2>0073
+24E3>0074
+24E4>0075
+24E5>0076
+24E6>0077
+24E7>0078
+24E8>0079
+24E9>007A
+24EA>0030
+2A0C>222B 222B 222B 222B
+2A74>003A 003A 003D
+2A75>003D 003D
+2A76>003D 003D 003D
+2ADC>2ADD 0338
+2C7C>006A
+2C7D>0056
+2D6F>2D61
+2E9F>6BCD
+2EF3>9F9F
+2F00>4E00
+2F01>4E28
+2F02>4E36
+2F03>4E3F
+2F04>4E59
+2F05>4E85
+2F06>4E8C
+2F07>4EA0
+2F08>4EBA
+2F09>513F
+2F0A>5165
+2F0B>516B
+2F0C>5182
+2F0D>5196
+2F0E>51AB
+2F0F>51E0
+2F10>51F5
+2F11>5200
+2F12>529B
+2F13>52F9
+2F14>5315
+2F15>531A
+2F16>5338
+2F17>5341
+2F18>535C
+2F19>5369
+2F1A>5382
+2F1B>53B6
+2F1C>53C8
+2F1D>53E3
+2F1E>56D7
+2F1F>571F
+2F20>58EB
+2F21>5902
+2F22>590A
+2F23>5915
+2F24>5927
+2F25>5973
+2F26>5B50
+2F27>5B80
+2F28>5BF8
+2F29>5C0F
+2F2A>5C22
+2F2B>5C38
+2F2C>5C6E
+2F2D>5C71
+2F2E>5DDB
+2F2F>5DE5
+2F30>5DF1
+2F31>5DFE
+2F32>5E72
+2F33>5E7A
+2F34>5E7F
+2F35>5EF4
+2F36>5EFE
+2F37>5F0B
+2F38>5F13
+2F39>5F50
+2F3A>5F61
+2F3B>5F73
+2F3C>5FC3
+2F3D>6208
+2F3E>6236
+2F3F>624B
+2F40>652F
+2F41>6534
+2F42>6587
+2F43>6597
+2F44>65A4
+2F45>65B9
+2F46>65E0
+2F47>65E5
+2F48>66F0
+2F49>6708
+2F4A>6728
+2F4B>6B20
+2F4C>6B62
+2F4D>6B79
+2F4E>6BB3
+2F4F>6BCB
+2F50>6BD4
+2F51>6BDB
+2F52>6C0F
+2F53>6C14
+2F54>6C34
+2F55>706B
+2F56>722A
+2F57>7236
+2F58>723B
+2F59>723F
+2F5A>7247
+2F5B>7259
+2F5C>725B
+2F5D>72AC
+2F5E>7384
+2F5F>7389
+2F60>74DC
+2F61>74E6
+2F62>7518
+2F63>751F
+2F64>7528
+2F65>7530
+2F66>758B
+2F67>7592
+2F68>7676
+2F69>767D
+2F6A>76AE
+2F6B>76BF
+2F6C>76EE
+2F6D>77DB
+2F6E>77E2
+2F6F>77F3
+2F70>793A
+2F71>79B8
+2F72>79BE
+2F73>7A74
+2F74>7ACB
+2F75>7AF9
+2F76>7C73
+2F77>7CF8
+2F78>7F36
+2F79>7F51
+2F7A>7F8A
+2F7B>7FBD
+2F7C>8001
+2F7D>800C
+2F7E>8012
+2F7F>8033
+2F80>807F
+2F81>8089
+2F82>81E3
+2F83>81EA
+2F84>81F3
+2F85>81FC
+2F86>820C
+2F87>821B
+2F88>821F
+2F89>826E
+2F8A>8272
+2F8B>8278
+2F8C>864D
+2F8D>866B
+2F8E>8840
+2F8F>884C
+2F90>8863
+2F91>897E
+2F92>898B
+2F93>89D2
+2F94>8A00
+2F95>8C37
+2F96>8C46
+2F97>8C55
+2F98>8C78
+2F99>8C9D
+2F9A>8D64
+2F9B>8D70
+2F9C>8DB3
+2F9D>8EAB
+2F9E>8ECA
+2F9F>8F9B
+2FA0>8FB0
+2FA1>8FB5
+2FA2>9091
+2FA3>9149
+2FA4>91C6
+2FA5>91CC
+2FA6>91D1
+2FA7>9577
+2FA8>9580
+2FA9>961C
+2FAA>96B6
+2FAB>96B9
+2FAC>96E8
+2FAD>9751
+2FAE>975E
+2FAF>9762
+2FB0>9769
+2FB1>97CB
+2FB2>97ED
+2FB3>97F3
+2FB4>9801
+2FB5>98A8
+2FB6>98DB
+2FB7>98DF
+2FB8>9996
+2FB9>9999
+2FBA>99AC
+2FBB>9AA8
+2FBC>9AD8
+2FBD>9ADF
+2FBE>9B25
+2FBF>9B2F
+2FC0>9B32
+2FC1>9B3C
+2FC2>9B5A
+2FC3>9CE5
+2FC4>9E75
+2FC5>9E7F
+2FC6>9EA5
+2FC7>9EBB
+2FC8>9EC3
+2FC9>9ECD
+2FCA>9ED1
+2FCB>9EF9
+2FCC>9EFD
+2FCD>9F0E
+2FCE>9F13
+2FCF>9F20
+2FD0>9F3B
+2FD1>9F4A
+2FD2>9F52
+2FD3>9F8D
+2FD4>9F9C
+2FD5>9FA0
+3000>0020
+3036>3012
+3038>5341
+3039>5344
+303A>5345
+304C=304B 3099
+304E=304D 3099
+3050=304F 3099
+3052=3051 3099
+3054=3053 3099
+3056=3055 3099
+3058=3057 3099
+305A=3059 3099
+305C=305B 3099
+305E=305D 3099
+3060=305F 3099
+3062=3061 3099
+3065=3064 3099
+3067=3066 3099
+3069=3068 3099
+3070=306F 3099
+3071=306F 309A
+3073=3072 3099
+3074=3072 309A
+3076=3075 3099
+3077=3075 309A
+3079=3078 3099
+307A=3078 309A
+307C=307B 3099
+307D=307B 309A
+3094=3046 3099
+309B>0020 3099
+309C>0020 309A
+309E=309D 3099
+309F>3088 308A
+30AC=30AB 3099
+30AE=30AD 3099
+30B0=30AF 3099
+30B2=30B1 3099
+30B4=30B3 3099
+30B6=30B5 3099
+30B8=30B7 3099
+30BA=30B9 3099
+30BC=30BB 3099
+30BE=30BD 3099
+30C0=30BF 3099
+30C2=30C1 3099
+30C5=30C4 3099
+30C7=30C6 3099
+30C9=30C8 3099
+30D0=30CF 3099
+30D1=30CF 309A
+30D3=30D2 3099
+30D4=30D2 309A
+30D6=30D5 3099
+30D7=30D5 309A
+30D9=30D8 3099
+30DA=30D8 309A
+30DC=30DB 3099
+30DD=30DB 309A
+30F4=30A6 3099
+30F7=30EF 3099
+30F8=30F0 3099
+30F9=30F1 3099
+30FA=30F2 3099
+30FE=30FD 3099
+30FF>30B3 30C8
+3131>1100
+3132>1101
+3133>11AA
+3134>1102
+3135>11AC
+3136>11AD
+3137>1103
+3138>1104
+3139>1105
+313A>11B0
+313B>11B1
+313C>11B2
+313D>11B3
+313E>11B4
+313F>11B5
+3140>111A
+3141>1106
+3142>1107
+3143>1108
+3144>1121
+3145>1109
+3146>110A
+3147>110B
+3148>110C
+3149>110D
+314A>110E
+314B>110F
+314C>1110
+314D>1111
+314E>1112
+314F>1161
+3150>1162
+3151>1163
+3152>1164
+3153>1165
+3154>1166
+3155>1167
+3156>1168
+3157>1169
+3158>116A
+3159>116B
+315A>116C
+315B>116D
+315C>116E
+315D>116F
+315E>1170
+315F>1171
+3160>1172
+3161>1173
+3162>1174
+3163>1175
+3164>1160
+3165>1114
+3166>1115
+3167>11C7
+3168>11C8
+3169>11CC
+316A>11CE
+316B>11D3
+316C>11D7
+316D>11D9
+316E>111C
+316F>11DD
+3170>11DF
+3171>111D
+3172>111E
+3173>1120
+3174>1122
+3175>1123
+3176>1127
+3177>1129
+3178>112B
+3179>112C
+317A>112D
+317B>112E
+317C>112F
+317D>1132
+317E>1136
+317F>1140
+3180>1147
+3181>114C
+3182>11F1
+3183>11F2
+3184>1157
+3185>1158
+3186>1159
+3187>1184
+3188>1185
+3189>1188
+318A>1191
+318B>1192
+318C>1194
+318D>119E
+318E>11A1
+3192>4E00
+3193>4E8C
+3194>4E09
+3195>56DB
+3196>4E0A
+3197>4E2D
+3198>4E0B
+3199>7532
+319A>4E59
+319B>4E19
+319C>4E01
+319D>5929
+319E>5730
+319F>4EBA
+3200>0028 1100 0029
+3201>0028 1102 0029
+3202>0028 1103 0029
+3203>0028 1105 0029
+3204>0028 1106 0029
+3205>0028 1107 0029
+3206>0028 1109 0029
+3207>0028 110B 0029
+3208>0028 110C 0029
+3209>0028 110E 0029
+320A>0028 110F 0029
+320B>0028 1110 0029
+320C>0028 1111 0029
+320D>0028 1112 0029
+320E>0028 1100 1161 0029
+320F>0028 1102 1161 0029
+3210>0028 1103 1161 0029
+3211>0028 1105 1161 0029
+3212>0028 1106 1161 0029
+3213>0028 1107 1161 0029
+3214>0028 1109 1161 0029
+3215>0028 110B 1161 0029
+3216>0028 110C 1161 0029
+3217>0028 110E 1161 0029
+3218>0028 110F 1161 0029
+3219>0028 1110 1161 0029
+321A>0028 1111 1161 0029
+321B>0028 1112 1161 0029
+321C>0028 110C 116E 0029
+321D>0028 110B 1169 110C 1165 11AB 0029
+321E>0028 110B 1169 1112 116E 0029
+3220>0028 4E00 0029
+3221>0028 4E8C 0029
+3222>0028 4E09 0029
+3223>0028 56DB 0029
+3224>0028 4E94 0029
+3225>0028 516D 0029
+3226>0028 4E03 0029
+3227>0028 516B 0029
+3228>0028 4E5D 0029
+3229>0028 5341 0029
+322A>0028 6708 0029
+322B>0028 706B 0029
+322C>0028 6C34 0029
+322D>0028 6728 0029
+322E>0028 91D1 0029
+322F>0028 571F 0029
+3230>0028 65E5 0029
+3231>0028 682A 0029
+3232>0028 6709 0029
+3233>0028 793E 0029
+3234>0028 540D 0029
+3235>0028 7279 0029
+3236>0028 8CA1 0029
+3237>0028 795D 0029
+3238>0028 52B4 0029
+3239>0028 4EE3 0029
+323A>0028 547C 0029
+323B>0028 5B66 0029
+323C>0028 76E3 0029
+323D>0028 4F01 0029
+323E>0028 8CC7 0029
+323F>0028 5354 0029
+3240>0028 796D 0029
+3241>0028 4F11 0029
+3242>0028 81EA 0029
+3243>0028 81F3 0029
+3244>554F
+3245>5E7C
+3246>6587
+3247>7B8F
+3250>0050 0054 0045
+3251>0032 0031
+3252>0032 0032
+3253>0032 0033
+3254>0032 0034
+3255>0032 0035
+3256>0032 0036
+3257>0032 0037
+3258>0032 0038
+3259>0032 0039
+325A>0033 0030
+325B>0033 0031
+325C>0033 0032
+325D>0033 0033
+325E>0033 0034
+325F>0033 0035
+3260>1100
+3261>1102
+3262>1103
+3263>1105
+3264>1106
+3265>1107
+3266>1109
+3267>110B
+3268>110C
+3269>110E
+326A>110F
+326B>1110
+326C>1111
+326D>1112
+326E>1100 1161
+326F>1102 1161
+3270>1103 1161
+3271>1105 1161
+3272>1106 1161
+3273>1107 1161
+3274>1109 1161
+3275>110B 1161
+3276>110C 1161
+3277>110E 1161
+3278>110F 1161
+3279>1110 1161
+327A>1111 1161
+327B>1112 1161
+327C>110E 1161 11B7 1100 1169
+327D>110C 116E 110B 1174
+327E>110B 116E
+3280>4E00
+3281>4E8C
+3282>4E09
+3283>56DB
+3284>4E94
+3285>516D
+3286>4E03
+3287>516B
+3288>4E5D
+3289>5341
+328A>6708
+328B>706B
+328C>6C34
+328D>6728
+328E>91D1
+328F>571F
+3290>65E5
+3291>682A
+3292>6709
+3293>793E
+3294>540D
+3295>7279
+3296>8CA1
+3297>795D
+3298>52B4
+3299>79D8
+329A>7537
+329B>5973
+329C>9069
+329D>512A
+329E>5370
+329F>6CE8
+32A0>9805
+32A1>4F11
+32A2>5199
+32A3>6B63
+32A4>4E0A
+32A5>4E2D
+32A6>4E0B
+32A7>5DE6
+32A8>53F3
+32A9>533B
+32AA>5B97
+32AB>5B66
+32AC>76E3
+32AD>4F01
+32AE>8CC7
+32AF>5354
+32B0>591C
+32B1>0033 0036
+32B2>0033 0037
+32B3>0033 0038
+32B4>0033 0039
+32B5>0034 0030
+32B6>0034 0031
+32B7>0034 0032
+32B8>0034 0033
+32B9>0034 0034
+32BA>0034 0035
+32BB>0034 0036
+32BC>0034 0037
+32BD>0034 0038
+32BE>0034 0039
+32BF>0035 0030
+32C0>0031 6708
+32C1>0032 6708
+32C2>0033 6708
+32C3>0034 6708
+32C4>0035 6708
+32C5>0036 6708
+32C6>0037 6708
+32C7>0038 6708
+32C8>0039 6708
+32C9>0031 0030 6708
+32CA>0031 0031 6708
+32CB>0031 0032 6708
+32CC>0048 0067
+32CD>0065 0072 0067
+32CE>0065 0056
+32CF>004C 0054 0044
+32D0>30A2
+32D1>30A4
+32D2>30A6
+32D3>30A8
+32D4>30AA
+32D5>30AB
+32D6>30AD
+32D7>30AF
+32D8>30B1
+32D9>30B3
+32DA>30B5
+32DB>30B7
+32DC>30B9
+32DD>30BB
+32DE>30BD
+32DF>30BF
+32E0>30C1
+32E1>30C4
+32E2>30C6
+32E3>30C8
+32E4>30CA
+32E5>30CB
+32E6>30CC
+32E7>30CD
+32E8>30CE
+32E9>30CF
+32EA>30D2
+32EB>30D5
+32EC>30D8
+32ED>30DB
+32EE>30DE
+32EF>30DF
+32F0>30E0
+32F1>30E1
+32F2>30E2
+32F3>30E4
+32F4>30E6
+32F5>30E8
+32F6>30E9
+32F7>30EA
+32F8>30EB
+32F9>30EC
+32FA>30ED
+32FB>30EF
+32FC>30F0
+32FD>30F1
+32FE>30F2
+3300>30A2 30D1 30FC 30C8
+3301>30A2 30EB 30D5 30A1
+3302>30A2 30F3 30DA 30A2
+3303>30A2 30FC 30EB
+3304>30A4 30CB 30F3 30B0
+3305>30A4 30F3 30C1
+3306>30A6 30A9 30F3
+3307>30A8 30B9 30AF 30FC 30C9
+3308>30A8 30FC 30AB 30FC
+3309>30AA 30F3 30B9
+330A>30AA 30FC 30E0
+330B>30AB 30A4 30EA
+330C>30AB 30E9 30C3 30C8
+330D>30AB 30ED 30EA 30FC
+330E>30AC 30ED 30F3
+330F>30AC 30F3 30DE
+3310>30AE 30AC
+3311>30AE 30CB 30FC
+3312>30AD 30E5 30EA 30FC
+3313>30AE 30EB 30C0 30FC
+3314>30AD 30ED
+3315>30AD 30ED 30B0 30E9 30E0
+3316>30AD 30ED 30E1 30FC 30C8 30EB
+3317>30AD 30ED 30EF 30C3 30C8
+3318>30B0 30E9 30E0
+3319>30B0 30E9 30E0 30C8 30F3
+331A>30AF 30EB 30BC 30A4 30ED
+331B>30AF 30ED 30FC 30CD
+331C>30B1 30FC 30B9
+331D>30B3 30EB 30CA
+331E>30B3 30FC 30DD
+331F>30B5 30A4 30AF 30EB
+3320>30B5 30F3 30C1 30FC 30E0
+3321>30B7 30EA 30F3 30B0
+3322>30BB 30F3 30C1
+3323>30BB 30F3 30C8
+3324>30C0 30FC 30B9
+3325>30C7 30B7
+3326>30C9 30EB
+3327>30C8 30F3
+3328>30CA 30CE
+3329>30CE 30C3 30C8
+332A>30CF 30A4 30C4
+332B>30D1 30FC 30BB 30F3 30C8
+332C>30D1 30FC 30C4
+332D>30D0 30FC 30EC 30EB
+332E>30D4 30A2 30B9 30C8 30EB
+332F>30D4 30AF 30EB
+3330>30D4 30B3
+3331>30D3 30EB
+3332>30D5 30A1 30E9 30C3 30C9
+3333>30D5 30A3 30FC 30C8
+3334>30D6 30C3 30B7 30A7 30EB
+3335>30D5 30E9 30F3
+3336>30D8 30AF 30BF 30FC 30EB
+3337>30DA 30BD
+3338>30DA 30CB 30D2
+3339>30D8 30EB 30C4
+333A>30DA 30F3 30B9
+333B>30DA 30FC 30B8
+333C>30D9 30FC 30BF
+333D>30DD 30A4 30F3 30C8
+333E>30DC 30EB 30C8
+333F>30DB 30F3
+3340>30DD 30F3 30C9
+3341>30DB 30FC 30EB
+3342>30DB 30FC 30F3
+3343>30DE 30A4 30AF 30ED
+3344>30DE 30A4 30EB
+3345>30DE 30C3 30CF
+3346>30DE 30EB 30AF
+3347>30DE 30F3 30B7 30E7 30F3
+3348>30DF 30AF 30ED 30F3
+3349>30DF 30EA
+334A>30DF 30EA 30D0 30FC 30EB
+334B>30E1 30AC
+334C>30E1 30AC 30C8 30F3
+334D>30E1 30FC 30C8 30EB
+334E>30E4 30FC 30C9
+334F>30E4 30FC 30EB
+3350>30E6 30A2 30F3
+3351>30EA 30C3 30C8 30EB
+3352>30EA 30E9
+3353>30EB 30D4 30FC
+3354>30EB 30FC 30D6 30EB
+3355>30EC 30E0
+3356>30EC 30F3 30C8 30B2 30F3
+3357>30EF 30C3 30C8
+3358>0030 70B9
+3359>0031 70B9
+335A>0032 70B9
+335B>0033 70B9
+335C>0034 70B9
+335D>0035 70B9
+335E>0036 70B9
+335F>0037 70B9
+3360>0038 70B9
+3361>0039 70B9
+3362>0031 0030 70B9
+3363>0031 0031 70B9
+3364>0031 0032 70B9
+3365>0031 0033 70B9
+3366>0031 0034 70B9
+3367>0031 0035 70B9
+3368>0031 0036 70B9
+3369>0031 0037 70B9
+336A>0031 0038 70B9
+336B>0031 0039 70B9
+336C>0032 0030 70B9
+336D>0032 0031 70B9
+336E>0032 0032 70B9
+336F>0032 0033 70B9
+3370>0032 0034 70B9
+3371>0068 0050 0061
+3372>0064 0061
+3373>0041 0055
+3374>0062 0061 0072
+3375>006F 0056
+3376>0070 0063
+3377>0064 006D
+3378>0064 006D 00B2
+3379>0064 006D 00B3
+337A>0049 0055
+337B>5E73 6210
+337C>662D 548C
+337D>5927 6B63
+337E>660E 6CBB
+337F>682A 5F0F 4F1A 793E
+3380>0070 0041
+3381>006E 0041
+3382>03BC 0041
+3383>006D 0041
+3384>006B 0041
+3385>004B 0042
+3386>004D 0042
+3387>0047 0042
+3388>0063 0061 006C
+3389>006B 0063 0061 006C
+338A>0070 0046
+338B>006E 0046
+338C>03BC 0046
+338D>03BC 0067
+338E>006D 0067
+338F>006B 0067
+3390>0048 007A
+3391>006B 0048 007A
+3392>004D 0048 007A
+3393>0047 0048 007A
+3394>0054 0048 007A
+3395>03BC 2113
+3396>006D 2113
+3397>0064 2113
+3398>006B 2113
+3399>0066 006D
+339A>006E 006D
+339B>03BC 006D
+339C>006D 006D
+339D>0063 006D
+339E>006B 006D
+339F>006D 006D 00B2
+33A0>0063 006D 00B2
+33A1>006D 00B2
+33A2>006B 006D 00B2
+33A3>006D 006D 00B3
+33A4>0063 006D 00B3
+33A5>006D 00B3
+33A6>006B 006D 00B3
+33A7>006D 2215 0073
+33A8>006D 2215 0073 00B2
+33A9>0050 0061
+33AA>006B 0050 0061
+33AB>004D 0050 0061
+33AC>0047 0050 0061
+33AD>0072 0061 0064
+33AE>0072 0061 0064 2215 0073
+33AF>0072 0061 0064 2215 0073 00B2
+33B0>0070 0073
+33B1>006E 0073
+33B2>03BC 0073
+33B3>006D 0073
+33B4>0070 0056
+33B5>006E 0056
+33B6>03BC 0056
+33B7>006D 0056
+33B8>006B 0056
+33B9>004D 0056
+33BA>0070 0057
+33BB>006E 0057
+33BC>03BC 0057
+33BD>006D 0057
+33BE>006B 0057
+33BF>004D 0057
+33C0>006B 03A9
+33C1>004D 03A9
+33C2>0061 002E 006D 002E
+33C3>0042 0071
+33C4>0063 0063
+33C5>0063 0064
+33C6>0043 2215 006B 0067
+33C7>0043 006F 002E
+33C8>0064 0042
+33C9>0047 0079
+33CA>0068 0061
+33CB>0048 0050
+33CC>0069 006E
+33CD>004B 004B
+33CE>004B 004D
+33CF>006B 0074
+33D0>006C 006D
+33D1>006C 006E
+33D2>006C 006F 0067
+33D3>006C 0078
+33D4>006D 0062
+33D5>006D 0069 006C
+33D6>006D 006F 006C
+33D7>0050 0048
+33D8>0070 002E 006D 002E
+33D9>0050 0050 004D
+33DA>0050 0052
+33DB>0073 0072
+33DC>0053 0076
+33DD>0057 0062
+33DE>0056 2215 006D
+33DF>0041 2215 006D
+33E0>0031 65E5
+33E1>0032 65E5
+33E2>0033 65E5
+33E3>0034 65E5
+33E4>0035 65E5
+33E5>0036 65E5
+33E6>0037 65E5
+33E7>0038 65E5
+33E8>0039 65E5
+33E9>0031 0030 65E5
+33EA>0031 0031 65E5
+33EB>0031 0032 65E5
+33EC>0031 0033 65E5
+33ED>0031 0034 65E5
+33EE>0031 0035 65E5
+33EF>0031 0036 65E5
+33F0>0031 0037 65E5
+33F1>0031 0038 65E5
+33F2>0031 0039 65E5
+33F3>0032 0030 65E5
+33F4>0032 0031 65E5
+33F5>0032 0032 65E5
+33F6>0032 0033 65E5
+33F7>0032 0034 65E5
+33F8>0032 0035 65E5
+33F9>0032 0036 65E5
+33FA>0032 0037 65E5
+33FB>0032 0038 65E5
+33FC>0032 0039 65E5
+33FD>0033 0030 65E5
+33FE>0033 0031 65E5
+33FF>0067 0061 006C
+A770>A76F
+F900>8C48
+F901>66F4
+F902>8ECA
+F903>8CC8
+F904>6ED1
+F905>4E32
+F906>53E5
+F907>9F9C
+F908>9F9C
+F909>5951
+F90A>91D1
+F90B>5587
+F90C>5948
+F90D>61F6
+F90E>7669
+F90F>7F85
+F910>863F
+F911>87BA
+F912>88F8
+F913>908F
+F914>6A02
+F915>6D1B
+F916>70D9
+F917>73DE
+F918>843D
+F919>916A
+F91A>99F1
+F91B>4E82
+F91C>5375
+F91D>6B04
+F91E>721B
+F91F>862D
+F920>9E1E
+F921>5D50
+F922>6FEB
+F923>85CD
+F924>8964
+F925>62C9
+F926>81D8
+F927>881F
+F928>5ECA
+F929>6717
+F92A>6D6A
+F92B>72FC
+F92C>90CE
+F92D>4F86
+F92E>51B7
+F92F>52DE
+F930>64C4
+F931>6AD3
+F932>7210
+F933>76E7
+F934>8001
+F935>8606
+F936>865C
+F937>8DEF
+F938>9732
+F939>9B6F
+F93A>9DFA
+F93B>788C
+F93C>797F
+F93D>7DA0
+F93E>83C9
+F93F>9304
+F940>9E7F
+F941>8AD6
+F942>58DF
+F943>5F04
+F944>7C60
+F945>807E
+F946>7262
+F947>78CA
+F948>8CC2
+F949>96F7
+F94A>58D8
+F94B>5C62
+F94C>6A13
+F94D>6DDA
+F94E>6F0F
+F94F>7D2F
+F950>7E37
+F951>964B
+F952>52D2
+F953>808B
+F954>51DC
+F955>51CC
+F956>7A1C
+F957>7DBE
+F958>83F1
+F959>9675
+F95A>8B80
+F95B>62CF
+F95C>6A02
+F95D>8AFE
+F95E>4E39
+F95F>5BE7
+F960>6012
+F961>7387
+F962>7570
+F963>5317
+F964>78FB
+F965>4FBF
+F966>5FA9
+F967>4E0D
+F968>6CCC
+F969>6578
+F96A>7D22
+F96B>53C3
+F96C>585E
+F96D>7701
+F96E>8449
+F96F>8AAA
+F970>6BBA
+F971>8FB0
+F972>6C88
+F973>62FE
+F974>82E5
+F975>63A0
+F976>7565
+F977>4EAE
+F978>5169
+F979>51C9
+F97A>6881
+F97B>7CE7
+F97C>826F
+F97D>8AD2
+F97E>91CF
+F97F>52F5
+F980>5442
+F981>5973
+F982>5EEC
+F983>65C5
+F984>6FFE
+F985>792A
+F986>95AD
+F987>9A6A
+F988>9E97
+F989>9ECE
+F98A>529B
+F98B>66C6
+F98C>6B77
+F98D>8F62
+F98E>5E74
+F98F>6190
+F990>6200
+F991>649A
+F992>6F23
+F993>7149
+F994>7489
+F995>79CA
+F996>7DF4
+F997>806F
+F998>8F26
+F999>84EE
+F99A>9023
+F99B>934A
+F99C>5217
+F99D>52A3
+F99E>54BD
+F99F>70C8
+F9A0>88C2
+F9A1>8AAA
+F9A2>5EC9
+F9A3>5FF5
+F9A4>637B
+F9A5>6BAE
+F9A6>7C3E
+F9A7>7375
+F9A8>4EE4
+F9A9>56F9
+F9AA>5BE7
+F9AB>5DBA
+F9AC>601C
+F9AD>73B2
+F9AE>7469
+F9AF>7F9A
+F9B0>8046
+F9B1>9234
+F9B2>96F6
+F9B3>9748
+F9B4>9818
+F9B5>4F8B
+F9B6>79AE
+F9B7>91B4
+F9B8>96B8
+F9B9>60E1
+F9BA>4E86
+F9BB>50DA
+F9BC>5BEE
+F9BD>5C3F
+F9BE>6599
+F9BF>6A02
+F9C0>71CE
+F9C1>7642
+F9C2>84FC
+F9C3>907C
+F9C4>9F8D
+F9C5>6688
+F9C6>962E
+F9C7>5289
+F9C8>677B
+F9C9>67F3
+F9CA>6D41
+F9CB>6E9C
+F9CC>7409
+F9CD>7559
+F9CE>786B
+F9CF>7D10
+F9D0>985E
+F9D1>516D
+F9D2>622E
+F9D3>9678
+F9D4>502B
+F9D5>5D19
+F9D6>6DEA
+F9D7>8F2A
+F9D8>5F8B
+F9D9>6144
+F9DA>6817
+F9DB>7387
+F9DC>9686
+F9DD>5229
+F9DE>540F
+F9DF>5C65
+F9E0>6613
+F9E1>674E
+F9E2>68A8
+F9E3>6CE5
+F9E4>7406
+F9E5>75E2
+F9E6>7F79
+F9E7>88CF
+F9E8>88E1
+F9E9>91CC
+F9EA>96E2
+F9EB>533F
+F9EC>6EBA
+F9ED>541D
+F9EE>71D0
+F9EF>7498
+F9F0>85FA
+F9F1>96A3
+F9F2>9C57
+F9F3>9E9F
+F9F4>6797
+F9F5>6DCB
+F9F6>81E8
+F9F7>7ACB
+F9F8>7B20
+F9F9>7C92
+F9FA>72C0
+F9FB>7099
+F9FC>8B58
+F9FD>4EC0
+F9FE>8336
+F9FF>523A
+FA00>5207
+FA01>5EA6
+FA02>62D3
+FA03>7CD6
+FA04>5B85
+FA05>6D1E
+FA06>66B4
+FA07>8F3B
+FA08>884C
+FA09>964D
+FA0A>898B
+FA0B>5ED3
+FA0C>5140
+FA0D>55C0
+FA10>585A
+FA12>6674
+FA15>51DE
+FA16>732A
+FA17>76CA
+FA18>793C
+FA19>795E
+FA1A>7965
+FA1B>798F
+FA1C>9756
+FA1D>7CBE
+FA1E>7FBD
+FA20>8612
+FA22>8AF8
+FA25>9038
+FA26>90FD
+FA2A>98EF
+FA2B>98FC
+FA2C>9928
+FA2D>9DB4
+FA30>4FAE
+FA31>50E7
+FA32>514D
+FA33>52C9
+FA34>52E4
+FA35>5351
+FA36>559D
+FA37>5606
+FA38>5668
+FA39>5840
+FA3A>58A8
+FA3B>5C64
+FA3C>5C6E
+FA3D>6094
+FA3E>6168
+FA3F>618E
+FA40>61F2
+FA41>654F
+FA42>65E2
+FA43>6691
+FA44>6885
+FA45>6D77
+FA46>6E1A
+FA47>6F22
+FA48>716E
+FA49>722B
+FA4A>7422
+FA4B>7891
+FA4C>793E
+FA4D>7949
+FA4E>7948
+FA4F>7950
+FA50>7956
+FA51>795D
+FA52>798D
+FA53>798E
+FA54>7A40
+FA55>7A81
+FA56>7BC0
+FA57>7DF4
+FA58>7E09
+FA59>7E41
+FA5A>7F72
+FA5B>8005
+FA5C>81ED
+FA5D>8279
+FA5E>8279
+FA5F>8457
+FA60>8910
+FA61>8996
+FA62>8B01
+FA63>8B39
+FA64>8CD3
+FA65>8D08
+FA66>8FB6
+FA67>9038
+FA68>96E3
+FA69>97FF
+FA6A>983B
+FA6B>6075
+FA6C>242EE
+FA6D>8218
+FA70>4E26
+FA71>51B5
+FA72>5168
+FA73>4F80
+FA74>5145
+FA75>5180
+FA76>52C7
+FA77>52FA
+FA78>559D
+FA79>5555
+FA7A>5599
+FA7B>55E2
+FA7C>585A
+FA7D>58B3
+FA7E>5944
+FA7F>5954
+FA80>5A62
+FA81>5B28
+FA82>5ED2
+FA83>5ED9
+FA84>5F69
+FA85>5FAD
+FA86>60D8
+FA87>614E
+FA88>6108
+FA89>618E
+FA8A>6160
+FA8B>61F2
+FA8C>6234
+FA8D>63C4
+FA8E>641C
+FA8F>6452
+FA90>6556
+FA91>6674
+FA92>6717
+FA93>671B
+FA94>6756
+FA95>6B79
+FA96>6BBA
+FA97>6D41
+FA98>6EDB
+FA99>6ECB
+FA9A>6F22
+FA9B>701E
+FA9C>716E
+FA9D>77A7
+FA9E>7235
+FA9F>72AF
+FAA0>732A
+FAA1>7471
+FAA2>7506
+FAA3>753B
+FAA4>761D
+FAA5>761F
+FAA6>76CA
+FAA7>76DB
+FAA8>76F4
+FAA9>774A
+FAAA>7740
+FAAB>78CC
+FAAC>7AB1
+FAAD>7BC0
+FAAE>7C7B
+FAAF>7D5B
+FAB0>7DF4
+FAB1>7F3E
+FAB2>8005
+FAB3>8352
+FAB4>83EF
+FAB5>8779
+FAB6>8941
+FAB7>8986
+FAB8>8996
+FAB9>8ABF
+FABA>8AF8
+FABB>8ACB
+FABC>8B01
+FABD>8AFE
+FABE>8AED
+FABF>8B39
+FAC0>8B8A
+FAC1>8D08
+FAC2>8F38
+FAC3>9072
+FAC4>9199
+FAC5>9276
+FAC6>967C
+FAC7>96E3
+FAC8>9756
+FAC9>97DB
+FACA>97FF
+FACB>980B
+FACC>983B
+FACD>9B12
+FACE>9F9C
+FACF>2284A
+FAD0>22844
+FAD1>233D5
+FAD2>3B9D
+FAD3>4018
+FAD4>4039
+FAD5>25249
+FAD6>25CD0
+FAD7>27ED3
+FAD8>9F43
+FAD9>9F8E
+FB00>0066 0066
+FB01>0066 0069
+FB02>0066 006C
+FB03>0066 0066 0069
+FB04>0066 0066 006C
+FB05>017F 0074
+FB06>0073 0074
+FB13>0574 0576
+FB14>0574 0565
+FB15>0574 056B
+FB16>057E 0576
+FB17>0574 056D
+FB1D>05D9 05B4
+FB1F>05F2 05B7
+FB20>05E2
+FB21>05D0
+FB22>05D3
+FB23>05D4
+FB24>05DB
+FB25>05DC
+FB26>05DD
+FB27>05E8
+FB28>05EA
+FB29>002B
+FB2A>05E9 05C1
+FB2B>05E9 05C2
+FB2C>FB49 05C1
+FB2D>FB49 05C2
+FB2E>05D0 05B7
+FB2F>05D0 05B8
+FB30>05D0 05BC
+FB31>05D1 05BC
+FB32>05D2 05BC
+FB33>05D3 05BC
+FB34>05D4 05BC
+FB35>05D5 05BC
+FB36>05D6 05BC
+FB38>05D8 05BC
+FB39>05D9 05BC
+FB3A>05DA 05BC
+FB3B>05DB 05BC
+FB3C>05DC 05BC
+FB3E>05DE 05BC
+FB40>05E0 05BC
+FB41>05E1 05BC
+FB43>05E3 05BC
+FB44>05E4 05BC
+FB46>05E6 05BC
+FB47>05E7 05BC
+FB48>05E8 05BC
+FB49>05E9 05BC
+FB4A>05EA 05BC
+FB4B>05D5 05B9
+FB4C>05D1 05BF
+FB4D>05DB 05BF
+FB4E>05E4 05BF
+FB4F>05D0 05DC
+FB50>0671
+FB51>0671
+FB52>067B
+FB53>067B
+FB54>067B
+FB55>067B
+FB56>067E
+FB57>067E
+FB58>067E
+FB59>067E
+FB5A>0680
+FB5B>0680
+FB5C>0680
+FB5D>0680
+FB5E>067A
+FB5F>067A
+FB60>067A
+FB61>067A
+FB62>067F
+FB63>067F
+FB64>067F
+FB65>067F
+FB66>0679
+FB67>0679
+FB68>0679
+FB69>0679
+FB6A>06A4
+FB6B>06A4
+FB6C>06A4
+FB6D>06A4
+FB6E>06A6
+FB6F>06A6
+FB70>06A6
+FB71>06A6
+FB72>0684
+FB73>0684
+FB74>0684
+FB75>0684
+FB76>0683
+FB77>0683
+FB78>0683
+FB79>0683
+FB7A>0686
+FB7B>0686
+FB7C>0686
+FB7D>0686
+FB7E>0687
+FB7F>0687
+FB80>0687
+FB81>0687
+FB82>068D
+FB83>068D
+FB84>068C
+FB85>068C
+FB86>068E
+FB87>068E
+FB88>0688
+FB89>0688
+FB8A>0698
+FB8B>0698
+FB8C>0691
+FB8D>0691
+FB8E>06A9
+FB8F>06A9
+FB90>06A9
+FB91>06A9
+FB92>06AF
+FB93>06AF
+FB94>06AF
+FB95>06AF
+FB96>06B3
+FB97>06B3
+FB98>06B3
+FB99>06B3
+FB9A>06B1
+FB9B>06B1
+FB9C>06B1
+FB9D>06B1
+FB9E>06BA
+FB9F>06BA
+FBA0>06BB
+FBA1>06BB
+FBA2>06BB
+FBA3>06BB
+FBA4>06C0
+FBA5>06C0
+FBA6>06C1
+FBA7>06C1
+FBA8>06C1
+FBA9>06C1
+FBAA>06BE
+FBAB>06BE
+FBAC>06BE
+FBAD>06BE
+FBAE>06D2
+FBAF>06D2
+FBB0>06D3
+FBB1>06D3
+FBD3>06AD
+FBD4>06AD
+FBD5>06AD
+FBD6>06AD
+FBD7>06C7
+FBD8>06C7
+FBD9>06C6
+FBDA>06C6
+FBDB>06C8
+FBDC>06C8
+FBDD>0677
+FBDE>06CB
+FBDF>06CB
+FBE0>06C5
+FBE1>06C5
+FBE2>06C9
+FBE3>06C9
+FBE4>06D0
+FBE5>06D0
+FBE6>06D0
+FBE7>06D0
+FBE8>0649
+FBE9>0649
+FBEA>0626 0627
+FBEB>0626 0627
+FBEC>0626 06D5
+FBED>0626 06D5
+FBEE>0626 0648
+FBEF>0626 0648
+FBF0>0626 06C7
+FBF1>0626 06C7
+FBF2>0626 06C6
+FBF3>0626 06C6
+FBF4>0626 06C8
+FBF5>0626 06C8
+FBF6>0626 06D0
+FBF7>0626 06D0
+FBF8>0626 06D0
+FBF9>0626 0649
+FBFA>0626 0649
+FBFB>0626 0649
+FBFC>06CC
+FBFD>06CC
+FBFE>06CC
+FBFF>06CC
+FC00>0626 062C
+FC01>0626 062D
+FC02>0626 0645
+FC03>0626 0649
+FC04>0626 064A
+FC05>0628 062C
+FC06>0628 062D
+FC07>0628 062E
+FC08>0628 0645
+FC09>0628 0649
+FC0A>0628 064A
+FC0B>062A 062C
+FC0C>062A 062D
+FC0D>062A 062E
+FC0E>062A 0645
+FC0F>062A 0649
+FC10>062A 064A
+FC11>062B 062C
+FC12>062B 0645
+FC13>062B 0649
+FC14>062B 064A
+FC15>062C 062D
+FC16>062C 0645
+FC17>062D 062C
+FC18>062D 0645
+FC19>062E 062C
+FC1A>062E 062D
+FC1B>062E 0645
+FC1C>0633 062C
+FC1D>0633 062D
+FC1E>0633 062E
+FC1F>0633 0645
+FC20>0635 062D
+FC21>0635 0645
+FC22>0636 062C
+FC23>0636 062D
+FC24>0636 062E
+FC25>0636 0645
+FC26>0637 062D
+FC27>0637 0645
+FC28>0638 0645
+FC29>0639 062C
+FC2A>0639 0645
+FC2B>063A 062C
+FC2C>063A 0645
+FC2D>0641 062C
+FC2E>0641 062D
+FC2F>0641 062E
+FC30>0641 0645
+FC31>0641 0649
+FC32>0641 064A
+FC33>0642 062D
+FC34>0642 0645
+FC35>0642 0649
+FC36>0642 064A
+FC37>0643 0627
+FC38>0643 062C
+FC39>0643 062D
+FC3A>0643 062E
+FC3B>0643 0644
+FC3C>0643 0645
+FC3D>0643 0649
+FC3E>0643 064A
+FC3F>0644 062C
+FC40>0644 062D
+FC41>0644 062E
+FC42>0644 0645
+FC43>0644 0649
+FC44>0644 064A
+FC45>0645 062C
+FC46>0645 062D
+FC47>0645 062E
+FC48>0645 0645
+FC49>0645 0649
+FC4A>0645 064A
+FC4B>0646 062C
+FC4C>0646 062D
+FC4D>0646 062E
+FC4E>0646 0645
+FC4F>0646 0649
+FC50>0646 064A
+FC51>0647 062C
+FC52>0647 0645
+FC53>0647 0649
+FC54>0647 064A
+FC55>064A 062C
+FC56>064A 062D
+FC57>064A 062E
+FC58>064A 0645
+FC59>064A 0649
+FC5A>064A 064A
+FC5B>0630 0670
+FC5C>0631 0670
+FC5D>0649 0670
+FC5E>0020 064C 0651
+FC5F>0020 064D 0651
+FC60>0020 064E 0651
+FC61>0020 064F 0651
+FC62>0020 0650 0651
+FC63>0020 0651 0670
+FC64>0626 0631
+FC65>0626 0632
+FC66>0626 0645
+FC67>0626 0646
+FC68>0626 0649
+FC69>0626 064A
+FC6A>0628 0631
+FC6B>0628 0632
+FC6C>0628 0645
+FC6D>0628 0646
+FC6E>0628 0649
+FC6F>0628 064A
+FC70>062A 0631
+FC71>062A 0632
+FC72>062A 0645
+FC73>062A 0646
+FC74>062A 0649
+FC75>062A 064A
+FC76>062B 0631
+FC77>062B 0632
+FC78>062B 0645
+FC79>062B 0646
+FC7A>062B 0649
+FC7B>062B 064A
+FC7C>0641 0649
+FC7D>0641 064A
+FC7E>0642 0649
+FC7F>0642 064A
+FC80>0643 0627
+FC81>0643 0644
+FC82>0643 0645
+FC83>0643 0649
+FC84>0643 064A
+FC85>0644 0645
+FC86>0644 0649
+FC87>0644 064A
+FC88>0645 0627
+FC89>0645 0645
+FC8A>0646 0631
+FC8B>0646 0632
+FC8C>0646 0645
+FC8D>0646 0646
+FC8E>0646 0649
+FC8F>0646 064A
+FC90>0649 0670
+FC91>064A 0631
+FC92>064A 0632
+FC93>064A 0645
+FC94>064A 0646
+FC95>064A 0649
+FC96>064A 064A
+FC97>0626 062C
+FC98>0626 062D
+FC99>0626 062E
+FC9A>0626 0645
+FC9B>0626 0647
+FC9C>0628 062C
+FC9D>0628 062D
+FC9E>0628 062E
+FC9F>0628 0645
+FCA0>0628 0647
+FCA1>062A 062C
+FCA2>062A 062D
+FCA3>062A 062E
+FCA4>062A 0645
+FCA5>062A 0647
+FCA6>062B 0645
+FCA7>062C 062D
+FCA8>062C 0645
+FCA9>062D 062C
+FCAA>062D 0645
+FCAB>062E 062C
+FCAC>062E 0645
+FCAD>0633 062C
+FCAE>0633 062D
+FCAF>0633 062E
+FCB0>0633 0645
+FCB1>0635 062D
+FCB2>0635 062E
+FCB3>0635 0645
+FCB4>0636 062C
+FCB5>0636 062D
+FCB6>0636 062E
+FCB7>0636 0645
+FCB8>0637 062D
+FCB9>0638 0645
+FCBA>0639 062C
+FCBB>0639 0645
+FCBC>063A 062C
+FCBD>063A 0645
+FCBE>0641 062C
+FCBF>0641 062D
+FCC0>0641 062E
+FCC1>0641 0645
+FCC2>0642 062D
+FCC3>0642 0645
+FCC4>0643 062C
+FCC5>0643 062D
+FCC6>0643 062E
+FCC7>0643 0644
+FCC8>0643 0645
+FCC9>0644 062C
+FCCA>0644 062D
+FCCB>0644 062E
+FCCC>0644 0645
+FCCD>0644 0647
+FCCE>0645 062C
+FCCF>0645 062D
+FCD0>0645 062E
+FCD1>0645 0645
+FCD2>0646 062C
+FCD3>0646 062D
+FCD4>0646 062E
+FCD5>0646 0645
+FCD6>0646 0647
+FCD7>0647 062C
+FCD8>0647 0645
+FCD9>0647 0670
+FCDA>064A 062C
+FCDB>064A 062D
+FCDC>064A 062E
+FCDD>064A 0645
+FCDE>064A 0647
+FCDF>0626 0645
+FCE0>0626 0647
+FCE1>0628 0645
+FCE2>0628 0647
+FCE3>062A 0645
+FCE4>062A 0647
+FCE5>062B 0645
+FCE6>062B 0647
+FCE7>0633 0645
+FCE8>0633 0647
+FCE9>0634 0645
+FCEA>0634 0647
+FCEB>0643 0644
+FCEC>0643 0645
+FCED>0644 0645
+FCEE>0646 0645
+FCEF>0646 0647
+FCF0>064A 0645
+FCF1>064A 0647
+FCF2>0640 064E 0651
+FCF3>0640 064F 0651
+FCF4>0640 0650 0651
+FCF5>0637 0649
+FCF6>0637 064A
+FCF7>0639 0649
+FCF8>0639 064A
+FCF9>063A 0649
+FCFA>063A 064A
+FCFB>0633 0649
+FCFC>0633 064A
+FCFD>0634 0649
+FCFE>0634 064A
+FCFF>062D 0649
+FD00>062D 064A
+FD01>062C 0649
+FD02>062C 064A
+FD03>062E 0649
+FD04>062E 064A
+FD05>0635 0649
+FD06>0635 064A
+FD07>0636 0649
+FD08>0636 064A
+FD09>0634 062C
+FD0A>0634 062D
+FD0B>0634 062E
+FD0C>0634 0645
+FD0D>0634 0631
+FD0E>0633 0631
+FD0F>0635 0631
+FD10>0636 0631
+FD11>0637 0649
+FD12>0637 064A
+FD13>0639 0649
+FD14>0639 064A
+FD15>063A 0649
+FD16>063A 064A
+FD17>0633 0649
+FD18>0633 064A
+FD19>0634 0649
+FD1A>0634 064A
+FD1B>062D 0649
+FD1C>062D 064A
+FD1D>062C 0649
+FD1E>062C 064A
+FD1F>062E 0649
+FD20>062E 064A
+FD21>0635 0649
+FD22>0635 064A
+FD23>0636 0649
+FD24>0636 064A
+FD25>0634 062C
+FD26>0634 062D
+FD27>0634 062E
+FD28>0634 0645
+FD29>0634 0631
+FD2A>0633 0631
+FD2B>0635 0631
+FD2C>0636 0631
+FD2D>0634 062C
+FD2E>0634 062D
+FD2F>0634 062E
+FD30>0634 0645
+FD31>0633 0647
+FD32>0634 0647
+FD33>0637 0645
+FD34>0633 062C
+FD35>0633 062D
+FD36>0633 062E
+FD37>0634 062C
+FD38>0634 062D
+FD39>0634 062E
+FD3A>0637 0645
+FD3B>0638 0645
+FD3C>0627 064B
+FD3D>0627 064B
+FD50>062A 062C 0645
+FD51>062A 062D 062C
+FD52>062A 062D 062C
+FD53>062A 062D 0645
+FD54>062A 062E 0645
+FD55>062A 0645 062C
+FD56>062A 0645 062D
+FD57>062A 0645 062E
+FD58>062C 0645 062D
+FD59>062C 0645 062D
+FD5A>062D 0645 064A
+FD5B>062D 0645 0649
+FD5C>0633 062D 062C
+FD5D>0633 062C 062D
+FD5E>0633 062C 0649
+FD5F>0633 0645 062D
+FD60>0633 0645 062D
+FD61>0633 0645 062C
+FD62>0633 0645 0645
+FD63>0633 0645 0645
+FD64>0635 062D 062D
+FD65>0635 062D 062D
+FD66>0635 0645 0645
+FD67>0634 062D 0645
+FD68>0634 062D 0645
+FD69>0634 062C 064A
+FD6A>0634 0645 062E
+FD6B>0634 0645 062E
+FD6C>0634 0645 0645
+FD6D>0634 0645 0645
+FD6E>0636 062D 0649
+FD6F>0636 062E 0645
+FD70>0636 062E 0645
+FD71>0637 0645 062D
+FD72>0637 0645 062D
+FD73>0637 0645 0645
+FD74>0637 0645 064A
+FD75>0639 062C 0645
+FD76>0639 0645 0645
+FD77>0639 0645 0645
+FD78>0639 0645 0649
+FD79>063A 0645 0645
+FD7A>063A 0645 064A
+FD7B>063A 0645 0649
+FD7C>0641 062E 0645
+FD7D>0641 062E 0645
+FD7E>0642 0645 062D
+FD7F>0642 0645 0645
+FD80>0644 062D 0645
+FD81>0644 062D 064A
+FD82>0644 062D 0649
+FD83>0644 062C 062C
+FD84>0644 062C 062C
+FD85>0644 062E 0645
+FD86>0644 062E 0645
+FD87>0644 0645 062D
+FD88>0644 0645 062D
+FD89>0645 062D 062C
+FD8A>0645 062D 0645
+FD8B>0645 062D 064A
+FD8C>0645 062C 062D
+FD8D>0645 062C 0645
+FD8E>0645 062E 062C
+FD8F>0645 062E 0645
+FD92>0645 062C 062E
+FD93>0647 0645 062C
+FD94>0647 0645 0645
+FD95>0646 062D 0645
+FD96>0646 062D 0649
+FD97>0646 062C 0645
+FD98>0646 062C 0645
+FD99>0646 062C 0649
+FD9A>0646 0645 064A
+FD9B>0646 0645 0649
+FD9C>064A 0645 0645
+FD9D>064A 0645 0645
+FD9E>0628 062E 064A
+FD9F>062A 062C 064A
+FDA0>062A 062C 0649
+FDA1>062A 062E 064A
+FDA2>062A 062E 0649
+FDA3>062A 0645 064A
+FDA4>062A 0645 0649
+FDA5>062C 0645 064A
+FDA6>062C 062D 0649
+FDA7>062C 0645 0649
+FDA8>0633 062E 0649
+FDA9>0635 062D 064A
+FDAA>0634 062D 064A
+FDAB>0636 062D 064A
+FDAC>0644 062C 064A
+FDAD>0644 0645 064A
+FDAE>064A 062D 064A
+FDAF>064A 062C 064A
+FDB0>064A 0645 064A
+FDB1>0645 0645 064A
+FDB2>0642 0645 064A
+FDB3>0646 062D 064A
+FDB4>0642 0645 062D
+FDB5>0644 062D 0645
+FDB6>0639 0645 064A
+FDB7>0643 0645 064A
+FDB8>0646 062C 062D
+FDB9>0645 062E 064A
+FDBA>0644 062C 0645
+FDBB>0643 0645 0645
+FDBC>0644 062C 0645
+FDBD>0646 062C 062D
+FDBE>062C 062D 064A
+FDBF>062D 062C 064A
+FDC0>0645 062C 064A
+FDC1>0641 0645 064A
+FDC2>0628 062D 064A
+FDC3>0643 0645 0645
+FDC4>0639 062C 0645
+FDC5>0635 0645 0645
+FDC6>0633 062E 064A
+FDC7>0646 062C 064A
+FDF0>0635 0644 06D2
+FDF1>0642 0644 06D2
+FDF2>0627 0644 0644 0647
+FDF3>0627 0643 0628 0631
+FDF4>0645 062D 0645 062F
+FDF5>0635 0644 0639 0645
+FDF6>0631 0633 0648 0644
+FDF7>0639 0644 064A 0647
+FDF8>0648 0633 0644 0645
+FDF9>0635 0644 0649
+FDFA>0635 0644 0649 0020 0627 0644 0644 0647 0020 0639 0644 064A 0647 0020 0648 0633 0644 0645
+FDFB>062C 0644 0020 062C 0644 0627 0644 0647
+FDFC>0631 06CC 0627 0644
+FE10>002C
+FE11>3001
+FE12>3002
+FE13>003A
+FE14>003B
+FE15>0021
+FE16>003F
+FE17>3016
+FE18>3017
+FE19>2026
+FE30>2025
+FE31>2014
+FE32>2013
+FE33>005F
+FE34>005F
+FE35>0028
+FE36>0029
+FE37>007B
+FE38>007D
+FE39>3014
+FE3A>3015
+FE3B>3010
+FE3C>3011
+FE3D>300A
+FE3E>300B
+FE3F>3008
+FE40>3009
+FE41>300C
+FE42>300D
+FE43>300E
+FE44>300F
+FE47>005B
+FE48>005D
+FE49>203E
+FE4A>203E
+FE4B>203E
+FE4C>203E
+FE4D>005F
+FE4E>005F
+FE4F>005F
+FE50>002C
+FE51>3001
+FE52>002E
+FE54>003B
+FE55>003A
+FE56>003F
+FE57>0021
+FE58>2014
+FE59>0028
+FE5A>0029
+FE5B>007B
+FE5C>007D
+FE5D>3014
+FE5E>3015
+FE5F>0023
+FE60>0026
+FE61>002A
+FE62>002B
+FE63>002D
+FE64>003C
+FE65>003E
+FE66>003D
+FE68>005C
+FE69>0024
+FE6A>0025
+FE6B>0040
+FE70>0020 064B
+FE71>0640 064B
+FE72>0020 064C
+FE74>0020 064D
+FE76>0020 064E
+FE77>0640 064E
+FE78>0020 064F
+FE79>0640 064F
+FE7A>0020 0650
+FE7B>0640 0650
+FE7C>0020 0651
+FE7D>0640 0651
+FE7E>0020 0652
+FE7F>0640 0652
+FE80>0621
+FE81>0622
+FE82>0622
+FE83>0623
+FE84>0623
+FE85>0624
+FE86>0624
+FE87>0625
+FE88>0625
+FE89>0626
+FE8A>0626
+FE8B>0626
+FE8C>0626
+FE8D>0627
+FE8E>0627
+FE8F>0628
+FE90>0628
+FE91>0628
+FE92>0628
+FE93>0629
+FE94>0629
+FE95>062A
+FE96>062A
+FE97>062A
+FE98>062A
+FE99>062B
+FE9A>062B
+FE9B>062B
+FE9C>062B
+FE9D>062C
+FE9E>062C
+FE9F>062C
+FEA0>062C
+FEA1>062D
+FEA2>062D
+FEA3>062D
+FEA4>062D
+FEA5>062E
+FEA6>062E
+FEA7>062E
+FEA8>062E
+FEA9>062F
+FEAA>062F
+FEAB>0630
+FEAC>0630
+FEAD>0631
+FEAE>0631
+FEAF>0632
+FEB0>0632
+FEB1>0633
+FEB2>0633
+FEB3>0633
+FEB4>0633
+FEB5>0634
+FEB6>0634
+FEB7>0634
+FEB8>0634
+FEB9>0635
+FEBA>0635
+FEBB>0635
+FEBC>0635
+FEBD>0636
+FEBE>0636
+FEBF>0636
+FEC0>0636
+FEC1>0637
+FEC2>0637
+FEC3>0637
+FEC4>0637
+FEC5>0638
+FEC6>0638
+FEC7>0638
+FEC8>0638
+FEC9>0639
+FECA>0639
+FECB>0639
+FECC>0639
+FECD>063A
+FECE>063A
+FECF>063A
+FED0>063A
+FED1>0641
+FED2>0641
+FED3>0641
+FED4>0641
+FED5>0642
+FED6>0642
+FED7>0642
+FED8>0642
+FED9>0643
+FEDA>0643
+FEDB>0643
+FEDC>0643
+FEDD>0644
+FEDE>0644
+FEDF>0644
+FEE0>0644
+FEE1>0645
+FEE2>0645
+FEE3>0645
+FEE4>0645
+FEE5>0646
+FEE6>0646
+FEE7>0646
+FEE8>0646
+FEE9>0647
+FEEA>0647
+FEEB>0647
+FEEC>0647
+FEED>0648
+FEEE>0648
+FEEF>0649
+FEF0>0649
+FEF1>064A
+FEF2>064A
+FEF3>064A
+FEF4>064A
+FEF5>0644 0622
+FEF6>0644 0622
+FEF7>0644 0623
+FEF8>0644 0623
+FEF9>0644 0625
+FEFA>0644 0625
+FEFB>0644 0627
+FEFC>0644 0627
+FF01>0021
+FF02>0022
+FF03>0023
+FF04>0024
+FF05>0025
+FF06>0026
+FF07>0027
+FF08>0028
+FF09>0029
+FF0A>002A
+FF0B>002B
+FF0C>002C
+FF0D>002D
+FF0E>002E
+FF0F>002F
+FF10>0030
+FF11>0031
+FF12>0032
+FF13>0033
+FF14>0034
+FF15>0035
+FF16>0036
+FF17>0037
+FF18>0038
+FF19>0039
+FF1A>003A
+FF1B>003B
+FF1C>003C
+FF1D>003D
+FF1E>003E
+FF1F>003F
+FF20>0040
+FF21>0041
+FF22>0042
+FF23>0043
+FF24>0044
+FF25>0045
+FF26>0046
+FF27>0047
+FF28>0048
+FF29>0049
+FF2A>004A
+FF2B>004B
+FF2C>004C
+FF2D>004D
+FF2E>004E
+FF2F>004F
+FF30>0050
+FF31>0051
+FF32>0052
+FF33>0053
+FF34>0054
+FF35>0055
+FF36>0056
+FF37>0057
+FF38>0058
+FF39>0059
+FF3A>005A
+FF3B>005B
+FF3C>005C
+FF3D>005D
+FF3E>005E
+FF3F>005F
+FF40>0060
+FF41>0061
+FF42>0062
+FF43>0063
+FF44>0064
+FF45>0065
+FF46>0066
+FF47>0067
+FF48>0068
+FF49>0069
+FF4A>006A
+FF4B>006B
+FF4C>006C
+FF4D>006D
+FF4E>006E
+FF4F>006F
+FF50>0070
+FF51>0071
+FF52>0072
+FF53>0073
+FF54>0074
+FF55>0075
+FF56>0076
+FF57>0077
+FF58>0078
+FF59>0079
+FF5A>007A
+FF5B>007B
+FF5C>007C
+FF5D>007D
+FF5E>007E
+FF5F>2985
+FF60>2986
+FF61>3002
+FF62>300C
+FF63>300D
+FF64>3001
+FF65>30FB
+FF66>30F2
+FF67>30A1
+FF68>30A3
+FF69>30A5
+FF6A>30A7
+FF6B>30A9
+FF6C>30E3
+FF6D>30E5
+FF6E>30E7
+FF6F>30C3
+FF70>30FC
+FF71>30A2
+FF72>30A4
+FF73>30A6
+FF74>30A8
+FF75>30AA
+FF76>30AB
+FF77>30AD
+FF78>30AF
+FF79>30B1
+FF7A>30B3
+FF7B>30B5
+FF7C>30B7
+FF7D>30B9
+FF7E>30BB
+FF7F>30BD
+FF80>30BF
+FF81>30C1
+FF82>30C4
+FF83>30C6
+FF84>30C8
+FF85>30CA
+FF86>30CB
+FF87>30CC
+FF88>30CD
+FF89>30CE
+FF8A>30CF
+FF8B>30D2
+FF8C>30D5
+FF8D>30D8
+FF8E>30DB
+FF8F>30DE
+FF90>30DF
+FF91>30E0
+FF92>30E1
+FF93>30E2
+FF94>30E4
+FF95>30E6
+FF96>30E8
+FF97>30E9
+FF98>30EA
+FF99>30EB
+FF9A>30EC
+FF9B>30ED
+FF9C>30EF
+FF9D>30F3
+FF9E>3099
+FF9F>309A
+FFA0>3164
+FFA1>3131
+FFA2>3132
+FFA3>3133
+FFA4>3134
+FFA5>3135
+FFA6>3136
+FFA7>3137
+FFA8>3138
+FFA9>3139
+FFAA>313A
+FFAB>313B
+FFAC>313C
+FFAD>313D
+FFAE>313E
+FFAF>313F
+FFB0>3140
+FFB1>3141
+FFB2>3142
+FFB3>3143
+FFB4>3144
+FFB5>3145
+FFB6>3146
+FFB7>3147
+FFB8>3148
+FFB9>3149
+FFBA>314A
+FFBB>314B
+FFBC>314C
+FFBD>314D
+FFBE>314E
+FFC2>314F
+FFC3>3150
+FFC4>3151
+FFC5>3152
+FFC6>3153
+FFC7>3154
+FFCA>3155
+FFCB>3156
+FFCC>3157
+FFCD>3158
+FFCE>3159
+FFCF>315A
+FFD2>315B
+FFD3>315C
+FFD4>315D
+FFD5>315E
+FFD6>315F
+FFD7>3160
+FFDA>3161
+FFDB>3162
+FFDC>3163
+FFE0>00A2
+FFE1>00A3
+FFE2>00AC
+FFE3>00AF
+FFE4>00A6
+FFE5>00A5
+FFE6>20A9
+FFE8>2502
+FFE9>2190
+FFEA>2191
+FFEB>2192
+FFEC>2193
+FFED>25A0
+FFEE>25CB
+1109A=11099 110BA
+1109C=1109B 110BA
+110AB=110A5 110BA
+1D15E>1D157 1D165
+1D15F>1D158 1D165
+1D160>1D15F 1D16E
+1D161>1D15F 1D16F
+1D162>1D15F 1D170
+1D163>1D15F 1D171
+1D164>1D15F 1D172
+1D1BB>1D1B9 1D165
+1D1BC>1D1BA 1D165
+1D1BD>1D1BB 1D16E
+1D1BE>1D1BC 1D16E
+1D1BF>1D1BB 1D16F
+1D1C0>1D1BC 1D16F
+1D400>0041
+1D401>0042
+1D402>0043
+1D403>0044
+1D404>0045
+1D405>0046
+1D406>0047
+1D407>0048
+1D408>0049
+1D409>004A
+1D40A>004B
+1D40B>004C
+1D40C>004D
+1D40D>004E
+1D40E>004F
+1D40F>0050
+1D410>0051
+1D411>0052
+1D412>0053
+1D413>0054
+1D414>0055
+1D415>0056
+1D416>0057
+1D417>0058
+1D418>0059
+1D419>005A
+1D41A>0061
+1D41B>0062
+1D41C>0063
+1D41D>0064
+1D41E>0065
+1D41F>0066
+1D420>0067
+1D421>0068
+1D422>0069
+1D423>006A
+1D424>006B
+1D425>006C
+1D426>006D
+1D427>006E
+1D428>006F
+1D429>0070
+1D42A>0071
+1D42B>0072
+1D42C>0073
+1D42D>0074
+1D42E>0075
+1D42F>0076
+1D430>0077
+1D431>0078
+1D432>0079
+1D433>007A
+1D434>0041
+1D435>0042
+1D436>0043
+1D437>0044
+1D438>0045
+1D439>0046
+1D43A>0047
+1D43B>0048
+1D43C>0049
+1D43D>004A
+1D43E>004B
+1D43F>004C
+1D440>004D
+1D441>004E
+1D442>004F
+1D443>0050
+1D444>0051
+1D445>0052
+1D446>0053
+1D447>0054
+1D448>0055
+1D449>0056
+1D44A>0057
+1D44B>0058
+1D44C>0059
+1D44D>005A
+1D44E>0061
+1D44F>0062
+1D450>0063
+1D451>0064
+1D452>0065
+1D453>0066
+1D454>0067
+1D456>0069
+1D457>006A
+1D458>006B
+1D459>006C
+1D45A>006D
+1D45B>006E
+1D45C>006F
+1D45D>0070
+1D45E>0071
+1D45F>0072
+1D460>0073
+1D461>0074
+1D462>0075
+1D463>0076
+1D464>0077
+1D465>0078
+1D466>0079
+1D467>007A
+1D468>0041
+1D469>0042
+1D46A>0043
+1D46B>0044
+1D46C>0045
+1D46D>0046
+1D46E>0047
+1D46F>0048
+1D470>0049
+1D471>004A
+1D472>004B
+1D473>004C
+1D474>004D
+1D475>004E
+1D476>004F
+1D477>0050
+1D478>0051
+1D479>0052
+1D47A>0053
+1D47B>0054
+1D47C>0055
+1D47D>0056
+1D47E>0057
+1D47F>0058
+1D480>0059
+1D481>005A
+1D482>0061
+1D483>0062
+1D484>0063
+1D485>0064
+1D486>0065
+1D487>0066
+1D488>0067
+1D489>0068
+1D48A>0069
+1D48B>006A
+1D48C>006B
+1D48D>006C
+1D48E>006D
+1D48F>006E
+1D490>006F
+1D491>0070
+1D492>0071
+1D493>0072
+1D494>0073
+1D495>0074
+1D496>0075
+1D497>0076
+1D498>0077
+1D499>0078
+1D49A>0079
+1D49B>007A
+1D49C>0041
+1D49E>0043
+1D49F>0044
+1D4A2>0047
+1D4A5>004A
+1D4A6>004B
+1D4A9>004E
+1D4AA>004F
+1D4AB>0050
+1D4AC>0051
+1D4AE>0053
+1D4AF>0054
+1D4B0>0055
+1D4B1>0056
+1D4B2>0057
+1D4B3>0058
+1D4B4>0059
+1D4B5>005A
+1D4B6>0061
+1D4B7>0062
+1D4B8>0063
+1D4B9>0064
+1D4BB>0066
+1D4BD>0068
+1D4BE>0069
+1D4BF>006A
+1D4C0>006B
+1D4C1>006C
+1D4C2>006D
+1D4C3>006E
+1D4C5>0070
+1D4C6>0071
+1D4C7>0072
+1D4C8>0073
+1D4C9>0074
+1D4CA>0075
+1D4CB>0076
+1D4CC>0077
+1D4CD>0078
+1D4CE>0079
+1D4CF>007A
+1D4D0>0041
+1D4D1>0042
+1D4D2>0043
+1D4D3>0044
+1D4D4>0045
+1D4D5>0046
+1D4D6>0047
+1D4D7>0048
+1D4D8>0049
+1D4D9>004A
+1D4DA>004B
+1D4DB>004C
+1D4DC>004D
+1D4DD>004E
+1D4DE>004F
+1D4DF>0050
+1D4E0>0051
+1D4E1>0052
+1D4E2>0053
+1D4E3>0054
+1D4E4>0055
+1D4E5>0056
+1D4E6>0057
+1D4E7>0058
+1D4E8>0059
+1D4E9>005A
+1D4EA>0061
+1D4EB>0062
+1D4EC>0063
+1D4ED>0064
+1D4EE>0065
+1D4EF>0066
+1D4F0>0067
+1D4F1>0068
+1D4F2>0069
+1D4F3>006A
+1D4F4>006B
+1D4F5>006C
+1D4F6>006D
+1D4F7>006E
+1D4F8>006F
+1D4F9>0070
+1D4FA>0071
+1D4FB>0072
+1D4FC>0073
+1D4FD>0074
+1D4FE>0075
+1D4FF>0076
+1D500>0077
+1D501>0078
+1D502>0079
+1D503>007A
+1D504>0041
+1D505>0042
+1D507>0044
+1D508>0045
+1D509>0046
+1D50A>0047
+1D50D>004A
+1D50E>004B
+1D50F>004C
+1D510>004D
+1D511>004E
+1D512>004F
+1D513>0050
+1D514>0051
+1D516>0053
+1D517>0054
+1D518>0055
+1D519>0056
+1D51A>0057
+1D51B>0058
+1D51C>0059
+1D51E>0061
+1D51F>0062
+1D520>0063
+1D521>0064
+1D522>0065
+1D523>0066
+1D524>0067
+1D525>0068
+1D526>0069
+1D527>006A
+1D528>006B
+1D529>006C
+1D52A>006D
+1D52B>006E
+1D52C>006F
+1D52D>0070
+1D52E>0071
+1D52F>0072
+1D530>0073
+1D531>0074
+1D532>0075
+1D533>0076
+1D534>0077
+1D535>0078
+1D536>0079
+1D537>007A
+1D538>0041
+1D539>0042
+1D53B>0044
+1D53C>0045
+1D53D>0046
+1D53E>0047
+1D540>0049
+1D541>004A
+1D542>004B
+1D543>004C
+1D544>004D
+1D546>004F
+1D54A>0053
+1D54B>0054
+1D54C>0055
+1D54D>0056
+1D54E>0057
+1D54F>0058
+1D550>0059
+1D552>0061
+1D553>0062
+1D554>0063
+1D555>0064
+1D556>0065
+1D557>0066
+1D558>0067
+1D559>0068
+1D55A>0069
+1D55B>006A
+1D55C>006B
+1D55D>006C
+1D55E>006D
+1D55F>006E
+1D560>006F
+1D561>0070
+1D562>0071
+1D563>0072
+1D564>0073
+1D565>0074
+1D566>0075
+1D567>0076
+1D568>0077
+1D569>0078
+1D56A>0079
+1D56B>007A
+1D56C>0041
+1D56D>0042
+1D56E>0043
+1D56F>0044
+1D570>0045
+1D571>0046
+1D572>0047
+1D573>0048
+1D574>0049
+1D575>004A
+1D576>004B
+1D577>004C
+1D578>004D
+1D579>004E
+1D57A>004F
+1D57B>0050
+1D57C>0051
+1D57D>0052
+1D57E>0053
+1D57F>0054
+1D580>0055
+1D581>0056
+1D582>0057
+1D583>0058
+1D584>0059
+1D585>005A
+1D586>0061
+1D587>0062
+1D588>0063
+1D589>0064
+1D58A>0065
+1D58B>0066
+1D58C>0067
+1D58D>0068
+1D58E>0069
+1D58F>006A
+1D590>006B
+1D591>006C
+1D592>006D
+1D593>006E
+1D594>006F
+1D595>0070
+1D596>0071
+1D597>0072
+1D598>0073
+1D599>0074
+1D59A>0075
+1D59B>0076
+1D59C>0077
+1D59D>0078
+1D59E>0079
+1D59F>007A
+1D5A0>0041
+1D5A1>0042
+1D5A2>0043
+1D5A3>0044
+1D5A4>0045
+1D5A5>0046
+1D5A6>0047
+1D5A7>0048
+1D5A8>0049
+1D5A9>004A
+1D5AA>004B
+1D5AB>004C
+1D5AC>004D
+1D5AD>004E
+1D5AE>004F
+1D5AF>0050
+1D5B0>0051
+1D5B1>0052
+1D5B2>0053
+1D5B3>0054
+1D5B4>0055
+1D5B5>0056
+1D5B6>0057
+1D5B7>0058
+1D5B8>0059
+1D5B9>005A
+1D5BA>0061
+1D5BB>0062
+1D5BC>0063
+1D5BD>0064
+1D5BE>0065
+1D5BF>0066
+1D5C0>0067
+1D5C1>0068
+1D5C2>0069
+1D5C3>006A
+1D5C4>006B
+1D5C5>006C
+1D5C6>006D
+1D5C7>006E
+1D5C8>006F
+1D5C9>0070
+1D5CA>0071
+1D5CB>0072
+1D5CC>0073
+1D5CD>0074
+1D5CE>0075
+1D5CF>0076
+1D5D0>0077
+1D5D1>0078
+1D5D2>0079
+1D5D3>007A
+1D5D4>0041
+1D5D5>0042
+1D5D6>0043
+1D5D7>0044
+1D5D8>0045
+1D5D9>0046
+1D5DA>0047
+1D5DB>0048
+1D5DC>0049
+1D5DD>004A
+1D5DE>004B
+1D5DF>004C
+1D5E0>004D
+1D5E1>004E
+1D5E2>004F
+1D5E3>0050
+1D5E4>0051
+1D5E5>0052
+1D5E6>0053
+1D5E7>0054
+1D5E8>0055
+1D5E9>0056
+1D5EA>0057
+1D5EB>0058
+1D5EC>0059
+1D5ED>005A
+1D5EE>0061
+1D5EF>0062
+1D5F0>0063
+1D5F1>0064
+1D5F2>0065
+1D5F3>0066
+1D5F4>0067
+1D5F5>0068
+1D5F6>0069
+1D5F7>006A
+1D5F8>006B
+1D5F9>006C
+1D5FA>006D
+1D5FB>006E
+1D5FC>006F
+1D5FD>0070
+1D5FE>0071
+1D5FF>0072
+1D600>0073
+1D601>0074
+1D602>0075
+1D603>0076
+1D604>0077
+1D605>0078
+1D606>0079
+1D607>007A
+1D608>0041
+1D609>0042
+1D60A>0043
+1D60B>0044
+1D60C>0045
+1D60D>0046
+1D60E>0047
+1D60F>0048
+1D610>0049
+1D611>004A
+1D612>004B
+1D613>004C
+1D614>004D
+1D615>004E
+1D616>004F
+1D617>0050
+1D618>0051
+1D619>0052
+1D61A>0053
+1D61B>0054
+1D61C>0055
+1D61D>0056
+1D61E>0057
+1D61F>0058
+1D620>0059
+1D621>005A
+1D622>0061
+1D623>0062
+1D624>0063
+1D625>0064
+1D626>0065
+1D627>0066
+1D628>0067
+1D629>0068
+1D62A>0069
+1D62B>006A
+1D62C>006B
+1D62D>006C
+1D62E>006D
+1D62F>006E
+1D630>006F
+1D631>0070
+1D632>0071
+1D633>0072
+1D634>0073
+1D635>0074
+1D636>0075
+1D637>0076
+1D638>0077
+1D639>0078
+1D63A>0079
+1D63B>007A
+1D63C>0041
+1D63D>0042
+1D63E>0043
+1D63F>0044
+1D640>0045
+1D641>0046
+1D642>0047
+1D643>0048
+1D644>0049
+1D645>004A
+1D646>004B
+1D647>004C
+1D648>004D
+1D649>004E
+1D64A>004F
+1D64B>0050
+1D64C>0051
+1D64D>0052
+1D64E>0053
+1D64F>0054
+1D650>0055
+1D651>0056
+1D652>0057
+1D653>0058
+1D654>0059
+1D655>005A
+1D656>0061
+1D657>0062
+1D658>0063
+1D659>0064
+1D65A>0065
+1D65B>0066
+1D65C>0067
+1D65D>0068
+1D65E>0069
+1D65F>006A
+1D660>006B
+1D661>006C
+1D662>006D
+1D663>006E
+1D664>006F
+1D665>0070
+1D666>0071
+1D667>0072
+1D668>0073
+1D669>0074
+1D66A>0075
+1D66B>0076
+1D66C>0077
+1D66D>0078
+1D66E>0079
+1D66F>007A
+1D670>0041
+1D671>0042
+1D672>0043
+1D673>0044
+1D674>0045
+1D675>0046
+1D676>0047
+1D677>0048
+1D678>0049
+1D679>004A
+1D67A>004B
+1D67B>004C
+1D67C>004D
+1D67D>004E
+1D67E>004F
+1D67F>0050
+1D680>0051
+1D681>0052
+1D682>0053
+1D683>0054
+1D684>0055
+1D685>0056
+1D686>0057
+1D687>0058
+1D688>0059
+1D689>005A
+1D68A>0061
+1D68B>0062
+1D68C>0063
+1D68D>0064
+1D68E>0065
+1D68F>0066
+1D690>0067
+1D691>0068
+1D692>0069
+1D693>006A
+1D694>006B
+1D695>006C
+1D696>006D
+1D697>006E
+1D698>006F
+1D699>0070
+1D69A>0071
+1D69B>0072
+1D69C>0073
+1D69D>0074
+1D69E>0075
+1D69F>0076
+1D6A0>0077
+1D6A1>0078
+1D6A2>0079
+1D6A3>007A
+1D6A4>0131
+1D6A5>0237
+1D6A8>0391
+1D6A9>0392
+1D6AA>0393
+1D6AB>0394
+1D6AC>0395
+1D6AD>0396
+1D6AE>0397
+1D6AF>0398
+1D6B0>0399
+1D6B1>039A
+1D6B2>039B
+1D6B3>039C
+1D6B4>039D
+1D6B5>039E
+1D6B6>039F
+1D6B7>03A0
+1D6B8>03A1
+1D6B9>03F4
+1D6BA>03A3
+1D6BB>03A4
+1D6BC>03A5
+1D6BD>03A6
+1D6BE>03A7
+1D6BF>03A8
+1D6C0>03A9
+1D6C1>2207
+1D6C2>03B1
+1D6C3>03B2
+1D6C4>03B3
+1D6C5>03B4
+1D6C6>03B5
+1D6C7>03B6
+1D6C8>03B7
+1D6C9>03B8
+1D6CA>03B9
+1D6CB>03BA
+1D6CC>03BB
+1D6CD>03BC
+1D6CE>03BD
+1D6CF>03BE
+1D6D0>03BF
+1D6D1>03C0
+1D6D2>03C1
+1D6D3>03C2
+1D6D4>03C3
+1D6D5>03C4
+1D6D6>03C5
+1D6D7>03C6
+1D6D8>03C7
+1D6D9>03C8
+1D6DA>03C9
+1D6DB>2202
+1D6DC>03F5
+1D6DD>03D1
+1D6DE>03F0
+1D6DF>03D5
+1D6E0>03F1
+1D6E1>03D6
+1D6E2>0391
+1D6E3>0392
+1D6E4>0393
+1D6E5>0394
+1D6E6>0395
+1D6E7>0396
+1D6E8>0397
+1D6E9>0398
+1D6EA>0399
+1D6EB>039A
+1D6EC>039B
+1D6ED>039C
+1D6EE>039D
+1D6EF>039E
+1D6F0>039F
+1D6F1>03A0
+1D6F2>03A1
+1D6F3>03F4
+1D6F4>03A3
+1D6F5>03A4
+1D6F6>03A5
+1D6F7>03A6
+1D6F8>03A7
+1D6F9>03A8
+1D6FA>03A9
+1D6FB>2207
+1D6FC>03B1
+1D6FD>03B2
+1D6FE>03B3
+1D6FF>03B4
+1D700>03B5
+1D701>03B6
+1D702>03B7
+1D703>03B8
+1D704>03B9
+1D705>03BA
+1D706>03BB
+1D707>03BC
+1D708>03BD
+1D709>03BE
+1D70A>03BF
+1D70B>03C0
+1D70C>03C1
+1D70D>03C2
+1D70E>03C3
+1D70F>03C4
+1D710>03C5
+1D711>03C6
+1D712>03C7
+1D713>03C8
+1D714>03C9
+1D715>2202
+1D716>03F5
+1D717>03D1
+1D718>03F0
+1D719>03D5
+1D71A>03F1
+1D71B>03D6
+1D71C>0391
+1D71D>0392
+1D71E>0393
+1D71F>0394
+1D720>0395
+1D721>0396
+1D722>0397
+1D723>0398
+1D724>0399
+1D725>039A
+1D726>039B
+1D727>039C
+1D728>039D
+1D729>039E
+1D72A>039F
+1D72B>03A0
+1D72C>03A1
+1D72D>03F4
+1D72E>03A3
+1D72F>03A4
+1D730>03A5
+1D731>03A6
+1D732>03A7
+1D733>03A8
+1D734>03A9
+1D735>2207
+1D736>03B1
+1D737>03B2
+1D738>03B3
+1D739>03B4
+1D73A>03B5
+1D73B>03B6
+1D73C>03B7
+1D73D>03B8
+1D73E>03B9
+1D73F>03BA
+1D740>03BB
+1D741>03BC
+1D742>03BD
+1D743>03BE
+1D744>03BF
+1D745>03C0
+1D746>03C1
+1D747>03C2
+1D748>03C3
+1D749>03C4
+1D74A>03C5
+1D74B>03C6
+1D74C>03C7
+1D74D>03C8
+1D74E>03C9
+1D74F>2202
+1D750>03F5
+1D751>03D1
+1D752>03F0
+1D753>03D5
+1D754>03F1
+1D755>03D6
+1D756>0391
+1D757>0392
+1D758>0393
+1D759>0394
+1D75A>0395
+1D75B>0396
+1D75C>0397
+1D75D>0398
+1D75E>0399
+1D75F>039A
+1D760>039B
+1D761>039C
+1D762>039D
+1D763>039E
+1D764>039F
+1D765>03A0
+1D766>03A1
+1D767>03F4
+1D768>03A3
+1D769>03A4
+1D76A>03A5
+1D76B>03A6
+1D76C>03A7
+1D76D>03A8
+1D76E>03A9
+1D76F>2207
+1D770>03B1
+1D771>03B2
+1D772>03B3
+1D773>03B4
+1D774>03B5
+1D775>03B6
+1D776>03B7
+1D777>03B8
+1D778>03B9
+1D779>03BA
+1D77A>03BB
+1D77B>03BC
+1D77C>03BD
+1D77D>03BE
+1D77E>03BF
+1D77F>03C0
+1D780>03C1
+1D781>03C2
+1D782>03C3
+1D783>03C4
+1D784>03C5
+1D785>03C6
+1D786>03C7
+1D787>03C8
+1D788>03C9
+1D789>2202
+1D78A>03F5
+1D78B>03D1
+1D78C>03F0
+1D78D>03D5
+1D78E>03F1
+1D78F>03D6
+1D790>0391
+1D791>0392
+1D792>0393
+1D793>0394
+1D794>0395
+1D795>0396
+1D796>0397
+1D797>0398
+1D798>0399
+1D799>039A
+1D79A>039B
+1D79B>039C
+1D79C>039D
+1D79D>039E
+1D79E>039F
+1D79F>03A0
+1D7A0>03A1
+1D7A1>03F4
+1D7A2>03A3
+1D7A3>03A4
+1D7A4>03A5
+1D7A5>03A6
+1D7A6>03A7
+1D7A7>03A8
+1D7A8>03A9
+1D7A9>2207
+1D7AA>03B1
+1D7AB>03B2
+1D7AC>03B3
+1D7AD>03B4
+1D7AE>03B5
+1D7AF>03B6
+1D7B0>03B7
+1D7B1>03B8
+1D7B2>03B9
+1D7B3>03BA
+1D7B4>03BB
+1D7B5>03BC
+1D7B6>03BD
+1D7B7>03BE
+1D7B8>03BF
+1D7B9>03C0
+1D7BA>03C1
+1D7BB>03C2
+1D7BC>03C3
+1D7BD>03C4
+1D7BE>03C5
+1D7BF>03C6
+1D7C0>03C7
+1D7C1>03C8
+1D7C2>03C9
+1D7C3>2202
+1D7C4>03F5
+1D7C5>03D1
+1D7C6>03F0
+1D7C7>03D5
+1D7C8>03F1
+1D7C9>03D6
+1D7CA>03DC
+1D7CB>03DD
+1D7CE>0030
+1D7CF>0031
+1D7D0>0032
+1D7D1>0033
+1D7D2>0034
+1D7D3>0035
+1D7D4>0036
+1D7D5>0037
+1D7D6>0038
+1D7D7>0039
+1D7D8>0030
+1D7D9>0031
+1D7DA>0032
+1D7DB>0033
+1D7DC>0034
+1D7DD>0035
+1D7DE>0036
+1D7DF>0037
+1D7E0>0038
+1D7E1>0039
+1D7E2>0030
+1D7E3>0031
+1D7E4>0032
+1D7E5>0033
+1D7E6>0034
+1D7E7>0035
+1D7E8>0036
+1D7E9>0037
+1D7EA>0038
+1D7EB>0039
+1D7EC>0030
+1D7ED>0031
+1D7EE>0032
+1D7EF>0033
+1D7F0>0034
+1D7F1>0035
+1D7F2>0036
+1D7F3>0037
+1D7F4>0038
+1D7F5>0039
+1D7F6>0030
+1D7F7>0031
+1D7F8>0032
+1D7F9>0033
+1D7FA>0034
+1D7FB>0035
+1D7FC>0036
+1D7FD>0037
+1D7FE>0038
+1D7FF>0039
+1F100>0030 002E
+1F101>0030 002C
+1F102>0031 002C
+1F103>0032 002C
+1F104>0033 002C
+1F105>0034 002C
+1F106>0035 002C
+1F107>0036 002C
+1F108>0037 002C
+1F109>0038 002C
+1F10A>0039 002C
+1F110>0028 0041 0029
+1F111>0028 0042 0029
+1F112>0028 0043 0029
+1F113>0028 0044 0029
+1F114>0028 0045 0029
+1F115>0028 0046 0029
+1F116>0028 0047 0029
+1F117>0028 0048 0029
+1F118>0028 0049 0029
+1F119>0028 004A 0029
+1F11A>0028 004B 0029
+1F11B>0028 004C 0029
+1F11C>0028 004D 0029
+1F11D>0028 004E 0029
+1F11E>0028 004F 0029
+1F11F>0028 0050 0029
+1F120>0028 0051 0029
+1F121>0028 0052 0029
+1F122>0028 0053 0029
+1F123>0028 0054 0029
+1F124>0028 0055 0029
+1F125>0028 0056 0029
+1F126>0028 0057 0029
+1F127>0028 0058 0029
+1F128>0028 0059 0029
+1F129>0028 005A 0029
+1F12A>3014 0053 3015
+1F12B>0043
+1F12C>0052
+1F12D>0043 0044
+1F12E>0057 005A
+1F131>0042
+1F13D>004E
+1F13F>0050
+1F142>0053
+1F146>0057
+1F14A>0048 0056
+1F14B>004D 0056
+1F14C>0053 0044
+1F14D>0053 0053
+1F14E>0050 0050 0056
+1F190>0044 004A
+1F200>307B 304B
+1F210>624B
+1F211>5B57
+1F212>53CC
+1F213>30C7
+1F214>4E8C
+1F215>591A
+1F216>89E3
+1F217>5929
+1F218>4EA4
+1F219>6620
+1F21A>7121
+1F21B>6599
+1F21C>524D
+1F21D>5F8C
+1F21E>518D
+1F21F>65B0
+1F220>521D
+1F221>7D42
+1F222>751F
+1F223>8CA9
+1F224>58F0
+1F225>5439
+1F226>6F14
+1F227>6295
+1F228>6355
+1F229>4E00
+1F22A>4E09
+1F22B>904A
+1F22C>5DE6
+1F22D>4E2D
+1F22E>53F3
+1F22F>6307
+1F230>8D70
+1F231>6253
+1F240>3014 672C 3015
+1F241>3014 4E09 3015
+1F242>3014 4E8C 3015
+1F243>3014 5B89 3015
+1F244>3014 70B9 3015
+1F245>3014 6253 3015
+1F246>3014 76D7 3015
+1F247>3014 52DD 3015
+1F248>3014 6557 3015
+2F800>4E3D
+2F801>4E38
+2F802>4E41
+2F803>20122
+2F804>4F60
+2F805>4FAE
+2F806>4FBB
+2F807>5002
+2F808>507A
+2F809>5099
+2F80A>50E7
+2F80B>50CF
+2F80C>349E
+2F80D>2063A
+2F80E>514D
+2F80F>5154
+2F810>5164
+2F811>5177
+2F812>2051C
+2F813>34B9
+2F814>5167
+2F815>518D
+2F816>2054B
+2F817>5197
+2F818>51A4
+2F819>4ECC
+2F81A>51AC
+2F81B>51B5
+2F81C>291DF
+2F81D>51F5
+2F81E>5203
+2F81F>34DF
+2F820>523B
+2F821>5246
+2F822>5272
+2F823>5277
+2F824>3515
+2F825>52C7
+2F826>52C9
+2F827>52E4
+2F828>52FA
+2F829>5305
+2F82A>5306
+2F82B>5317
+2F82C>5349
+2F82D>5351
+2F82E>535A
+2F82F>5373
+2F830>537D
+2F831>537F
+2F832>537F
+2F833>537F
+2F834>20A2C
+2F835>7070
+2F836>53CA
+2F837>53DF
+2F838>20B63
+2F839>53EB
+2F83A>53F1
+2F83B>5406
+2F83C>549E
+2F83D>5438
+2F83E>5448
+2F83F>5468
+2F840>54A2
+2F841>54F6
+2F842>5510
+2F843>5553
+2F844>5563
+2F845>5584
+2F846>5584
+2F847>5599
+2F848>55AB
+2F849>55B3
+2F84A>55C2
+2F84B>5716
+2F84C>5606
+2F84D>5717
+2F84E>5651
+2F84F>5674
+2F850>5207
+2F851>58EE
+2F852>57CE
+2F853>57F4
+2F854>580D
+2F855>578B
+2F856>5832
+2F857>5831
+2F858>58AC
+2F859>214E4
+2F85A>58F2
+2F85B>58F7
+2F85C>5906
+2F85D>591A
+2F85E>5922
+2F85F>5962
+2F860>216A8
+2F861>216EA
+2F862>59EC
+2F863>5A1B
+2F864>5A27
+2F865>59D8
+2F866>5A66
+2F867>36EE
+2F868>36FC
+2F869>5B08
+2F86A>5B3E
+2F86B>5B3E
+2F86C>219C8
+2F86D>5BC3
+2F86E>5BD8
+2F86F>5BE7
+2F870>5BF3
+2F871>21B18
+2F872>5BFF
+2F873>5C06
+2F874>5F53
+2F875>5C22
+2F876>3781
+2F877>5C60
+2F878>5C6E
+2F879>5CC0
+2F87A>5C8D
+2F87B>21DE4
+2F87C>5D43
+2F87D>21DE6
+2F87E>5D6E
+2F87F>5D6B
+2F880>5D7C
+2F881>5DE1
+2F882>5DE2
+2F883>382F
+2F884>5DFD
+2F885>5E28
+2F886>5E3D
+2F887>5E69
+2F888>3862
+2F889>22183
+2F88A>387C
+2F88B>5EB0
+2F88C>5EB3
+2F88D>5EB6
+2F88E>5ECA
+2F88F>2A392
+2F890>5EFE
+2F891>22331
+2F892>22331
+2F893>8201
+2F894>5F22
+2F895>5F22
+2F896>38C7
+2F897>232B8
+2F898>261DA
+2F899>5F62
+2F89A>5F6B
+2F89B>38E3
+2F89C>5F9A
+2F89D>5FCD
+2F89E>5FD7
+2F89F>5FF9
+2F8A0>6081
+2F8A1>393A
+2F8A2>391C
+2F8A3>6094
+2F8A4>226D4
+2F8A5>60C7
+2F8A6>6148
+2F8A7>614C
+2F8A8>614E
+2F8A9>614C
+2F8AA>617A
+2F8AB>618E
+2F8AC>61B2
+2F8AD>61A4
+2F8AE>61AF
+2F8AF>61DE
+2F8B0>61F2
+2F8B1>61F6
+2F8B2>6210
+2F8B3>621B
+2F8B4>625D
+2F8B5>62B1
+2F8B6>62D4
+2F8B7>6350
+2F8B8>22B0C
+2F8B9>633D
+2F8BA>62FC
+2F8BB>6368
+2F8BC>6383
+2F8BD>63E4
+2F8BE>22BF1
+2F8BF>6422
+2F8C0>63C5
+2F8C1>63A9
+2F8C2>3A2E
+2F8C3>6469
+2F8C4>647E
+2F8C5>649D
+2F8C6>6477
+2F8C7>3A6C
+2F8C8>654F
+2F8C9>656C
+2F8CA>2300A
+2F8CB>65E3
+2F8CC>66F8
+2F8CD>6649
+2F8CE>3B19
+2F8CF>6691
+2F8D0>3B08
+2F8D1>3AE4
+2F8D2>5192
+2F8D3>5195
+2F8D4>6700
+2F8D5>669C
+2F8D6>80AD
+2F8D7>43D9
+2F8D8>6717
+2F8D9>671B
+2F8DA>6721
+2F8DB>675E
+2F8DC>6753
+2F8DD>233C3
+2F8DE>3B49
+2F8DF>67FA
+2F8E0>6785
+2F8E1>6852
+2F8E2>6885
+2F8E3>2346D
+2F8E4>688E
+2F8E5>681F
+2F8E6>6914
+2F8E7>3B9D
+2F8E8>6942
+2F8E9>69A3
+2F8EA>69EA
+2F8EB>6AA8
+2F8EC>236A3
+2F8ED>6ADB
+2F8EE>3C18
+2F8EF>6B21
+2F8F0>238A7
+2F8F1>6B54
+2F8F2>3C4E
+2F8F3>6B72
+2F8F4>6B9F
+2F8F5>6BBA
+2F8F6>6BBB
+2F8F7>23A8D
+2F8F8>21D0B
+2F8F9>23AFA
+2F8FA>6C4E
+2F8FB>23CBC
+2F8FC>6CBF
+2F8FD>6CCD
+2F8FE>6C67
+2F8FF>6D16
+2F900>6D3E
+2F901>6D77
+2F902>6D41
+2F903>6D69
+2F904>6D78
+2F905>6D85
+2F906>23D1E
+2F907>6D34
+2F908>6E2F
+2F909>6E6E
+2F90A>3D33
+2F90B>6ECB
+2F90C>6EC7
+2F90D>23ED1
+2F90E>6DF9
+2F90F>6F6E
+2F910>23F5E
+2F911>23F8E
+2F912>6FC6
+2F913>7039
+2F914>701E
+2F915>701B
+2F916>3D96
+2F917>704A
+2F918>707D
+2F919>7077
+2F91A>70AD
+2F91B>20525
+2F91C>7145
+2F91D>24263
+2F91E>719C
+2F91F>243AB
+2F920>7228
+2F921>7235
+2F922>7250
+2F923>24608
+2F924>7280
+2F925>7295
+2F926>24735
+2F927>24814
+2F928>737A
+2F929>738B
+2F92A>3EAC
+2F92B>73A5
+2F92C>3EB8
+2F92D>3EB8
+2F92E>7447
+2F92F>745C
+2F930>7471
+2F931>7485
+2F932>74CA
+2F933>3F1B
+2F934>7524
+2F935>24C36
+2F936>753E
+2F937>24C92
+2F938>7570
+2F939>2219F
+2F93A>7610
+2F93B>24FA1
+2F93C>24FB8
+2F93D>25044
+2F93E>3FFC
+2F93F>4008
+2F940>76F4
+2F941>250F3
+2F942>250F2
+2F943>25119
+2F944>25133
+2F945>771E
+2F946>771F
+2F947>771F
+2F948>774A
+2F949>4039
+2F94A>778B
+2F94B>4046
+2F94C>4096
+2F94D>2541D
+2F94E>784E
+2F94F>788C
+2F950>78CC
+2F951>40E3
+2F952>25626
+2F953>7956
+2F954>2569A
+2F955>256C5
+2F956>798F
+2F957>79EB
+2F958>412F
+2F959>7A40
+2F95A>7A4A
+2F95B>7A4F
+2F95C>2597C
+2F95D>25AA7
+2F95E>25AA7
+2F95F>7AEE
+2F960>4202
+2F961>25BAB
+2F962>7BC6
+2F963>7BC9
+2F964>4227
+2F965>25C80
+2F966>7CD2
+2F967>42A0
+2F968>7CE8
+2F969>7CE3
+2F96A>7D00
+2F96B>25F86
+2F96C>7D63
+2F96D>4301
+2F96E>7DC7
+2F96F>7E02
+2F970>7E45
+2F971>4334
+2F972>26228
+2F973>26247
+2F974>4359
+2F975>262D9
+2F976>7F7A
+2F977>2633E
+2F978>7F95
+2F979>7FFA
+2F97A>8005
+2F97B>264DA
+2F97C>26523
+2F97D>8060
+2F97E>265A8
+2F97F>8070
+2F980>2335F
+2F981>43D5
+2F982>80B2
+2F983>8103
+2F984>440B
+2F985>813E
+2F986>5AB5
+2F987>267A7
+2F988>267B5
+2F989>23393
+2F98A>2339C
+2F98B>8201
+2F98C>8204
+2F98D>8F9E
+2F98E>446B
+2F98F>8291
+2F990>828B
+2F991>829D
+2F992>52B3
+2F993>82B1
+2F994>82B3
+2F995>82BD
+2F996>82E6
+2F997>26B3C
+2F998>82E5
+2F999>831D
+2F99A>8363
+2F99B>83AD
+2F99C>8323
+2F99D>83BD
+2F99E>83E7
+2F99F>8457
+2F9A0>8353
+2F9A1>83CA
+2F9A2>83CC
+2F9A3>83DC
+2F9A4>26C36
+2F9A5>26D6B
+2F9A6>26CD5
+2F9A7>452B
+2F9A8>84F1
+2F9A9>84F3
+2F9AA>8516
+2F9AB>273CA
+2F9AC>8564
+2F9AD>26F2C
+2F9AE>455D
+2F9AF>4561
+2F9B0>26FB1
+2F9B1>270D2
+2F9B2>456B
+2F9B3>8650
+2F9B4>865C
+2F9B5>8667
+2F9B6>8669
+2F9B7>86A9
+2F9B8>8688
+2F9B9>870E
+2F9BA>86E2
+2F9BB>8779
+2F9BC>8728
+2F9BD>876B
+2F9BE>8786
+2F9BF>45D7
+2F9C0>87E1
+2F9C1>8801
+2F9C2>45F9
+2F9C3>8860
+2F9C4>8863
+2F9C5>27667
+2F9C6>88D7
+2F9C7>88DE
+2F9C8>4635
+2F9C9>88FA
+2F9CA>34BB
+2F9CB>278AE
+2F9CC>27966
+2F9CD>46BE
+2F9CE>46C7
+2F9CF>8AA0
+2F9D0>8AED
+2F9D1>8B8A
+2F9D2>8C55
+2F9D3>27CA8
+2F9D4>8CAB
+2F9D5>8CC1
+2F9D6>8D1B
+2F9D7>8D77
+2F9D8>27F2F
+2F9D9>20804
+2F9DA>8DCB
+2F9DB>8DBC
+2F9DC>8DF0
+2F9DD>208DE
+2F9DE>8ED4
+2F9DF>8F38
+2F9E0>285D2
+2F9E1>285ED
+2F9E2>9094
+2F9E3>90F1
+2F9E4>9111
+2F9E5>2872E
+2F9E6>911B
+2F9E7>9238
+2F9E8>92D7
+2F9E9>92D8
+2F9EA>927C
+2F9EB>93F9
+2F9EC>9415
+2F9ED>28BFA
+2F9EE>958B
+2F9EF>4995
+2F9F0>95B7
+2F9F1>28D77
+2F9F2>49E6
+2F9F3>96C3
+2F9F4>5DB2
+2F9F5>9723
+2F9F6>29145
+2F9F7>2921A
+2F9F8>4A6E
+2F9F9>4A76
+2F9FA>97E0
+2F9FB>2940A
+2F9FC>4AB2
+2F9FD>29496
+2F9FE>980B
+2F9FF>980B
+2FA00>9829
+2FA01>295B6
+2FA02>98E2
+2FA03>4B33
+2FA04>9929
+2FA05>99A7
+2FA06>99C2
+2FA07>99FE
+2FA08>4BCE
+2FA09>29B30
+2FA0A>9B12
+2FA0B>9C40
+2FA0C>9CFD
+2FA0D>4CCE
+2FA0E>4CED
+2FA0F>9D67
+2FA10>2A0CE
+2FA11>4CF8
+2FA12>2A105
+2FA13>2A20E
+2FA14>2A291
+2FA15>9EBB
+2FA16>4D56
+2FA17>9EF9
+2FA18>9EFE
+2FA19>9F05
+2FA1A>9F0F
+2FA1B>9F16
+2FA1C>9F3B
+2FA1D>2A600
diff --git a/comm/mailnews/extensions/fts3/data/nfkc_cf.txt b/comm/mailnews/extensions/fts3/data/nfkc_cf.txt
new file mode 100644
index 0000000000..becabbbf34
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/data/nfkc_cf.txt
@@ -0,0 +1,5376 @@
+# Extracted from:
+# DerivedNormalizationProps-5.2.0.txt
+# Date: 2009-08-26, 18:18:50 GMT [MD]
+#
+# Unicode Character Database
+# Copyright (c) 1991-2009 Unicode, Inc.
+# For terms of use, see http://www.unicode.org/terms_of_use.html
+# For documentation, see http://www.unicode.org/reports/tr44/
+
+# ================================================
+# This file has been reformatted into syntax for the
+# gennorm2 Normalizer2 data generator tool.
+# Only the NFKC_CF mappings are retained and reformatted.
+# Reformatting via regular expression: s/ *; NFKC_CF; */>/
+# Use this file as the second gennorm2 input file after nfkc.txt.
+# ================================================
+
+# Derived Property: NFKC_Casefold (NFKC_CF)
+# This property removes certain variations from characters: case, compatibility, and default-ignorables.
+# It is used for loose matching and certain types of identifiers.
+# It is constructed by applying NFKC, CaseFolding, and removal of Default_Ignorable_Code_Points.
+# The process of applying these transformations is repeated until a stable result is produced.
+# WARNING: Application to STRINGS must apply NFC after mapping each character, because characters may interact.
+# For more information, see [http://www.unicode.org/reports/tr44/]
+# Omitted code points are unchanged by this mapping.
+# @missing: 0000..10FFFF><code point>
+
+# All code points not explicitly listed for NFKC_Casefold
+# have the value <codepoint>.
+
+0041>0061
+0042>0062
+0043>0063
+0044>0064
+0045>0065
+0046>0066
+0047>0067
+0048>0068
+0049>0069
+004A>006A
+004B>006B
+004C>006C
+004D>006D
+004E>006E
+004F>006F
+0050>0070
+0051>0071
+0052>0072
+0053>0073
+0054>0074
+0055>0075
+0056>0076
+0057>0077
+0058>0078
+0059>0079
+005A>007A
+00A0>0020
+00A8>0020 0308
+00AA>0061
+00AD>
+00AF>0020 0304
+00B2>0032
+00B3>0033
+00B4>0020 0301
+00B5>03BC
+00B8>0020 0327
+00B9>0031
+00BA>006F
+00BC>0031 2044 0034
+00BD>0031 2044 0032
+00BE>0033 2044 0034
+00C0>00E0
+00C1>00E1
+00C2>00E2
+00C3>00E3
+00C4>00E4
+00C5>00E5
+00C6>00E6
+00C7>00E7
+00C8>00E8
+00C9>00E9
+00CA>00EA
+00CB>00EB
+00CC>00EC
+00CD>00ED
+00CE>00EE
+00CF>00EF
+00D0>00F0
+00D1>00F1
+00D2>00F2
+00D3>00F3
+00D4>00F4
+00D5>00F5
+00D6>00F6
+00D8>00F8
+00D9>00F9
+00DA>00FA
+00DB>00FB
+00DC>00FC
+00DD>00FD
+00DE>00FE
+00DF>0073 0073
+0100>0101
+0102>0103
+0104>0105
+0106>0107
+0108>0109
+010A>010B
+010C>010D
+010E>010F
+0110>0111
+0112>0113
+0114>0115
+0116>0117
+0118>0119
+011A>011B
+011C>011D
+011E>011F
+0120>0121
+0122>0123
+0124>0125
+0126>0127
+0128>0129
+012A>012B
+012C>012D
+012E>012F
+0130>0069 0307
+0132..0133>0069 006A
+0134>0135
+0136>0137
+0139>013A
+013B>013C
+013D>013E
+013F..0140>006C 00B7
+0141>0142
+0143>0144
+0145>0146
+0147>0148
+0149>02BC 006E
+014A>014B
+014C>014D
+014E>014F
+0150>0151
+0152>0153
+0154>0155
+0156>0157
+0158>0159
+015A>015B
+015C>015D
+015E>015F
+0160>0161
+0162>0163
+0164>0165
+0166>0167
+0168>0169
+016A>016B
+016C>016D
+016E>016F
+0170>0171
+0172>0173
+0174>0175
+0176>0177
+0178>00FF
+0179>017A
+017B>017C
+017D>017E
+017F>0073
+0181>0253
+0182>0183
+0184>0185
+0186>0254
+0187>0188
+0189>0256
+018A>0257
+018B>018C
+018E>01DD
+018F>0259
+0190>025B
+0191>0192
+0193>0260
+0194>0263
+0196>0269
+0197>0268
+0198>0199
+019C>026F
+019D>0272
+019F>0275
+01A0>01A1
+01A2>01A3
+01A4>01A5
+01A6>0280
+01A7>01A8
+01A9>0283
+01AC>01AD
+01AE>0288
+01AF>01B0
+01B1>028A
+01B2>028B
+01B3>01B4
+01B5>01B6
+01B7>0292
+01B8>01B9
+01BC>01BD
+01C4..01C6>0064 017E
+01C7..01C9>006C 006A
+01CA..01CC>006E 006A
+01CD>01CE
+01CF>01D0
+01D1>01D2
+01D3>01D4
+01D5>01D6
+01D7>01D8
+01D9>01DA
+01DB>01DC
+01DE>01DF
+01E0>01E1
+01E2>01E3
+01E4>01E5
+01E6>01E7
+01E8>01E9
+01EA>01EB
+01EC>01ED
+01EE>01EF
+01F1..01F3>0064 007A
+01F4>01F5
+01F6>0195
+01F7>01BF
+01F8>01F9
+01FA>01FB
+01FC>01FD
+01FE>01FF
+0200>0201
+0202>0203
+0204>0205
+0206>0207
+0208>0209
+020A>020B
+020C>020D
+020E>020F
+0210>0211
+0212>0213
+0214>0215
+0216>0217
+0218>0219
+021A>021B
+021C>021D
+021E>021F
+0220>019E
+0222>0223
+0224>0225
+0226>0227
+0228>0229
+022A>022B
+022C>022D
+022E>022F
+0230>0231
+0232>0233
+023A>2C65
+023B>023C
+023D>019A
+023E>2C66
+0241>0242
+0243>0180
+0244>0289
+0245>028C
+0246>0247
+0248>0249
+024A>024B
+024C>024D
+024E>024F
+02B0>0068
+02B1>0266
+02B2>006A
+02B3>0072
+02B4>0279
+02B5>027B
+02B6>0281
+02B7>0077
+02B8>0079
+02D8>0020 0306
+02D9>0020 0307
+02DA>0020 030A
+02DB>0020 0328
+02DC>0020 0303
+02DD>0020 030B
+02E0>0263
+02E1>006C
+02E2>0073
+02E3>0078
+02E4>0295
+0340>0300
+0341>0301
+0343>0313
+0344>0308 0301
+0345>03B9
+034F>
+0370>0371
+0372>0373
+0374>02B9
+0376>0377
+037A>0020 03B9
+037E>003B
+0384>0020 0301
+0385>0020 0308 0301
+0386>03AC
+0387>00B7
+0388>03AD
+0389>03AE
+038A>03AF
+038C>03CC
+038E>03CD
+038F>03CE
+0391>03B1
+0392>03B2
+0393>03B3
+0394>03B4
+0395>03B5
+0396>03B6
+0397>03B7
+0398>03B8
+0399>03B9
+039A>03BA
+039B>03BB
+039C>03BC
+039D>03BD
+039E>03BE
+039F>03BF
+03A0>03C0
+03A1>03C1
+03A3>03C3
+03A4>03C4
+03A5>03C5
+03A6>03C6
+03A7>03C7
+03A8>03C8
+03A9>03C9
+03AA>03CA
+03AB>03CB
+03C2>03C3
+03CF>03D7
+03D0>03B2
+03D1>03B8
+03D2>03C5
+03D3>03CD
+03D4>03CB
+03D5>03C6
+03D6>03C0
+03D8>03D9
+03DA>03DB
+03DC>03DD
+03DE>03DF
+03E0>03E1
+03E2>03E3
+03E4>03E5
+03E6>03E7
+03E8>03E9
+03EA>03EB
+03EC>03ED
+03EE>03EF
+03F0>03BA
+03F1>03C1
+03F2>03C3
+03F4>03B8
+03F5>03B5
+03F7>03F8
+03F9>03C3
+03FA>03FB
+03FD>037B
+03FE>037C
+03FF>037D
+0400>0450
+0401>0451
+0402>0452
+0403>0453
+0404>0454
+0405>0455
+0406>0456
+0407>0457
+0408>0458
+0409>0459
+040A>045A
+040B>045B
+040C>045C
+040D>045D
+040E>045E
+040F>045F
+0410>0430
+0411>0431
+0412>0432
+0413>0433
+0414>0434
+0415>0435
+0416>0436
+0417>0437
+0418>0438
+0419>0439
+041A>043A
+041B>043B
+041C>043C
+041D>043D
+041E>043E
+041F>043F
+0420>0440
+0421>0441
+0422>0442
+0423>0443
+0424>0444
+0425>0445
+0426>0446
+0427>0447
+0428>0448
+0429>0449
+042A>044A
+042B>044B
+042C>044C
+042D>044D
+042E>044E
+042F>044F
+0460>0461
+0462>0463
+0464>0465
+0466>0467
+0468>0469
+046A>046B
+046C>046D
+046E>046F
+0470>0471
+0472>0473
+0474>0475
+0476>0477
+0478>0479
+047A>047B
+047C>047D
+047E>047F
+0480>0481
+048A>048B
+048C>048D
+048E>048F
+0490>0491
+0492>0493
+0494>0495
+0496>0497
+0498>0499
+049A>049B
+049C>049D
+049E>049F
+04A0>04A1
+04A2>04A3
+04A4>04A5
+04A6>04A7
+04A8>04A9
+04AA>04AB
+04AC>04AD
+04AE>04AF
+04B0>04B1
+04B2>04B3
+04B4>04B5
+04B6>04B7
+04B8>04B9
+04BA>04BB
+04BC>04BD
+04BE>04BF
+04C0>04CF
+04C1>04C2
+04C3>04C4
+04C5>04C6
+04C7>04C8
+04C9>04CA
+04CB>04CC
+04CD>04CE
+04D0>04D1
+04D2>04D3
+04D4>04D5
+04D6>04D7
+04D8>04D9
+04DA>04DB
+04DC>04DD
+04DE>04DF
+04E0>04E1
+04E2>04E3
+04E4>04E5
+04E6>04E7
+04E8>04E9
+04EA>04EB
+04EC>04ED
+04EE>04EF
+04F0>04F1
+04F2>04F3
+04F4>04F5
+04F6>04F7
+04F8>04F9
+04FA>04FB
+04FC>04FD
+04FE>04FF
+0500>0501
+0502>0503
+0504>0505
+0506>0507
+0508>0509
+050A>050B
+050C>050D
+050E>050F
+0510>0511
+0512>0513
+0514>0515
+0516>0517
+0518>0519
+051A>051B
+051C>051D
+051E>051F
+0520>0521
+0522>0523
+0524>0525
+0531>0561
+0532>0562
+0533>0563
+0534>0564
+0535>0565
+0536>0566
+0537>0567
+0538>0568
+0539>0569
+053A>056A
+053B>056B
+053C>056C
+053D>056D
+053E>056E
+053F>056F
+0540>0570
+0541>0571
+0542>0572
+0543>0573
+0544>0574
+0545>0575
+0546>0576
+0547>0577
+0548>0578
+0549>0579
+054A>057A
+054B>057B
+054C>057C
+054D>057D
+054E>057E
+054F>057F
+0550>0580
+0551>0581
+0552>0582
+0553>0583
+0554>0584
+0555>0585
+0556>0586
+0587>0565 0582
+0675>0627 0674
+0676>0648 0674
+0677>06C7 0674
+0678>064A 0674
+0958>0915 093C
+0959>0916 093C
+095A>0917 093C
+095B>091C 093C
+095C>0921 093C
+095D>0922 093C
+095E>092B 093C
+095F>092F 093C
+09DC>09A1 09BC
+09DD>09A2 09BC
+09DF>09AF 09BC
+0A33>0A32 0A3C
+0A36>0A38 0A3C
+0A59>0A16 0A3C
+0A5A>0A17 0A3C
+0A5B>0A1C 0A3C
+0A5E>0A2B 0A3C
+0B5C>0B21 0B3C
+0B5D>0B22 0B3C
+0E33>0E4D 0E32
+0EB3>0ECD 0EB2
+0EDC>0EAB 0E99
+0EDD>0EAB 0EA1
+0F0C>0F0B
+0F43>0F42 0FB7
+0F4D>0F4C 0FB7
+0F52>0F51 0FB7
+0F57>0F56 0FB7
+0F5C>0F5B 0FB7
+0F69>0F40 0FB5
+0F73>0F71 0F72
+0F75>0F71 0F74
+0F76>0FB2 0F80
+0F77>0FB2 0F71 0F80
+0F78>0FB3 0F80
+0F79>0FB3 0F71 0F80
+0F81>0F71 0F80
+0F93>0F92 0FB7
+0F9D>0F9C 0FB7
+0FA2>0FA1 0FB7
+0FA7>0FA6 0FB7
+0FAC>0FAB 0FB7
+0FB9>0F90 0FB5
+10A0>2D00
+10A1>2D01
+10A2>2D02
+10A3>2D03
+10A4>2D04
+10A5>2D05
+10A6>2D06
+10A7>2D07
+10A8>2D08
+10A9>2D09
+10AA>2D0A
+10AB>2D0B
+10AC>2D0C
+10AD>2D0D
+10AE>2D0E
+10AF>2D0F
+10B0>2D10
+10B1>2D11
+10B2>2D12
+10B3>2D13
+10B4>2D14
+10B5>2D15
+10B6>2D16
+10B7>2D17
+10B8>2D18
+10B9>2D19
+10BA>2D1A
+10BB>2D1B
+10BC>2D1C
+10BD>2D1D
+10BE>2D1E
+10BF>2D1F
+10C0>2D20
+10C1>2D21
+10C2>2D22
+10C3>2D23
+10C4>2D24
+10C5>2D25
+10FC>10DC
+115F..1160>
+17B4..17B5>
+180B..180D>
+1D2C>0061
+1D2D>00E6
+1D2E>0062
+1D30>0064
+1D31>0065
+1D32>01DD
+1D33>0067
+1D34>0068
+1D35>0069
+1D36>006A
+1D37>006B
+1D38>006C
+1D39>006D
+1D3A>006E
+1D3C>006F
+1D3D>0223
+1D3E>0070
+1D3F>0072
+1D40>0074
+1D41>0075
+1D42>0077
+1D43>0061
+1D44>0250
+1D45>0251
+1D46>1D02
+1D47>0062
+1D48>0064
+1D49>0065
+1D4A>0259
+1D4B>025B
+1D4C>025C
+1D4D>0067
+1D4F>006B
+1D50>006D
+1D51>014B
+1D52>006F
+1D53>0254
+1D54>1D16
+1D55>1D17
+1D56>0070
+1D57>0074
+1D58>0075
+1D59>1D1D
+1D5A>026F
+1D5B>0076
+1D5C>1D25
+1D5D>03B2
+1D5E>03B3
+1D5F>03B4
+1D60>03C6
+1D61>03C7
+1D62>0069
+1D63>0072
+1D64>0075
+1D65>0076
+1D66>03B2
+1D67>03B3
+1D68>03C1
+1D69>03C6
+1D6A>03C7
+1D78>043D
+1D9B>0252
+1D9C>0063
+1D9D>0255
+1D9E>00F0
+1D9F>025C
+1DA0>0066
+1DA1>025F
+1DA2>0261
+1DA3>0265
+1DA4>0268
+1DA5>0269
+1DA6>026A
+1DA7>1D7B
+1DA8>029D
+1DA9>026D
+1DAA>1D85
+1DAB>029F
+1DAC>0271
+1DAD>0270
+1DAE>0272
+1DAF>0273
+1DB0>0274
+1DB1>0275
+1DB2>0278
+1DB3>0282
+1DB4>0283
+1DB5>01AB
+1DB6>0289
+1DB7>028A
+1DB8>1D1C
+1DB9>028B
+1DBA>028C
+1DBB>007A
+1DBC>0290
+1DBD>0291
+1DBE>0292
+1DBF>03B8
+1E00>1E01
+1E02>1E03
+1E04>1E05
+1E06>1E07
+1E08>1E09
+1E0A>1E0B
+1E0C>1E0D
+1E0E>1E0F
+1E10>1E11
+1E12>1E13
+1E14>1E15
+1E16>1E17
+1E18>1E19
+1E1A>1E1B
+1E1C>1E1D
+1E1E>1E1F
+1E20>1E21
+1E22>1E23
+1E24>1E25
+1E26>1E27
+1E28>1E29
+1E2A>1E2B
+1E2C>1E2D
+1E2E>1E2F
+1E30>1E31
+1E32>1E33
+1E34>1E35
+1E36>1E37
+1E38>1E39
+1E3A>1E3B
+1E3C>1E3D
+1E3E>1E3F
+1E40>1E41
+1E42>1E43
+1E44>1E45
+1E46>1E47
+1E48>1E49
+1E4A>1E4B
+1E4C>1E4D
+1E4E>1E4F
+1E50>1E51
+1E52>1E53
+1E54>1E55
+1E56>1E57
+1E58>1E59
+1E5A>1E5B
+1E5C>1E5D
+1E5E>1E5F
+1E60>1E61
+1E62>1E63
+1E64>1E65
+1E66>1E67
+1E68>1E69
+1E6A>1E6B
+1E6C>1E6D
+1E6E>1E6F
+1E70>1E71
+1E72>1E73
+1E74>1E75
+1E76>1E77
+1E78>1E79
+1E7A>1E7B
+1E7C>1E7D
+1E7E>1E7F
+1E80>1E81
+1E82>1E83
+1E84>1E85
+1E86>1E87
+1E88>1E89
+1E8A>1E8B
+1E8C>1E8D
+1E8E>1E8F
+1E90>1E91
+1E92>1E93
+1E94>1E95
+1E9A>0061 02BE
+1E9B>1E61
+1E9E>0073 0073
+1EA0>1EA1
+1EA2>1EA3
+1EA4>1EA5
+1EA6>1EA7
+1EA8>1EA9
+1EAA>1EAB
+1EAC>1EAD
+1EAE>1EAF
+1EB0>1EB1
+1EB2>1EB3
+1EB4>1EB5
+1EB6>1EB7
+1EB8>1EB9
+1EBA>1EBB
+1EBC>1EBD
+1EBE>1EBF
+1EC0>1EC1
+1EC2>1EC3
+1EC4>1EC5
+1EC6>1EC7
+1EC8>1EC9
+1ECA>1ECB
+1ECC>1ECD
+1ECE>1ECF
+1ED0>1ED1
+1ED2>1ED3
+1ED4>1ED5
+1ED6>1ED7
+1ED8>1ED9
+1EDA>1EDB
+1EDC>1EDD
+1EDE>1EDF
+1EE0>1EE1
+1EE2>1EE3
+1EE4>1EE5
+1EE6>1EE7
+1EE8>1EE9
+1EEA>1EEB
+1EEC>1EED
+1EEE>1EEF
+1EF0>1EF1
+1EF2>1EF3
+1EF4>1EF5
+1EF6>1EF7
+1EF8>1EF9
+1EFA>1EFB
+1EFC>1EFD
+1EFE>1EFF
+1F08>1F00
+1F09>1F01
+1F0A>1F02
+1F0B>1F03
+1F0C>1F04
+1F0D>1F05
+1F0E>1F06
+1F0F>1F07
+1F18>1F10
+1F19>1F11
+1F1A>1F12
+1F1B>1F13
+1F1C>1F14
+1F1D>1F15
+1F28>1F20
+1F29>1F21
+1F2A>1F22
+1F2B>1F23
+1F2C>1F24
+1F2D>1F25
+1F2E>1F26
+1F2F>1F27
+1F38>1F30
+1F39>1F31
+1F3A>1F32
+1F3B>1F33
+1F3C>1F34
+1F3D>1F35
+1F3E>1F36
+1F3F>1F37
+1F48>1F40
+1F49>1F41
+1F4A>1F42
+1F4B>1F43
+1F4C>1F44
+1F4D>1F45
+1F59>1F51
+1F5B>1F53
+1F5D>1F55
+1F5F>1F57
+1F68>1F60
+1F69>1F61
+1F6A>1F62
+1F6B>1F63
+1F6C>1F64
+1F6D>1F65
+1F6E>1F66
+1F6F>1F67
+1F71>03AC
+1F73>03AD
+1F75>03AE
+1F77>03AF
+1F79>03CC
+1F7B>03CD
+1F7D>03CE
+1F80>1F00 03B9
+1F81>1F01 03B9
+1F82>1F02 03B9
+1F83>1F03 03B9
+1F84>1F04 03B9
+1F85>1F05 03B9
+1F86>1F06 03B9
+1F87>1F07 03B9
+1F88>1F00 03B9
+1F89>1F01 03B9
+1F8A>1F02 03B9
+1F8B>1F03 03B9
+1F8C>1F04 03B9
+1F8D>1F05 03B9
+1F8E>1F06 03B9
+1F8F>1F07 03B9
+1F90>1F20 03B9
+1F91>1F21 03B9
+1F92>1F22 03B9
+1F93>1F23 03B9
+1F94>1F24 03B9
+1F95>1F25 03B9
+1F96>1F26 03B9
+1F97>1F27 03B9
+1F98>1F20 03B9
+1F99>1F21 03B9
+1F9A>1F22 03B9
+1F9B>1F23 03B9
+1F9C>1F24 03B9
+1F9D>1F25 03B9
+1F9E>1F26 03B9
+1F9F>1F27 03B9
+1FA0>1F60 03B9
+1FA1>1F61 03B9
+1FA2>1F62 03B9
+1FA3>1F63 03B9
+1FA4>1F64 03B9
+1FA5>1F65 03B9
+1FA6>1F66 03B9
+1FA7>1F67 03B9
+1FA8>1F60 03B9
+1FA9>1F61 03B9
+1FAA>1F62 03B9
+1FAB>1F63 03B9
+1FAC>1F64 03B9
+1FAD>1F65 03B9
+1FAE>1F66 03B9
+1FAF>1F67 03B9
+1FB2>1F70 03B9
+1FB3>03B1 03B9
+1FB4>03AC 03B9
+1FB7>1FB6 03B9
+1FB8>1FB0
+1FB9>1FB1
+1FBA>1F70
+1FBB>03AC
+1FBC>03B1 03B9
+1FBD>0020 0313
+1FBE>03B9
+1FBF>0020 0313
+1FC0>0020 0342
+1FC1>0020 0308 0342
+1FC2>1F74 03B9
+1FC3>03B7 03B9
+1FC4>03AE 03B9
+1FC7>1FC6 03B9
+1FC8>1F72
+1FC9>03AD
+1FCA>1F74
+1FCB>03AE
+1FCC>03B7 03B9
+1FCD>0020 0313 0300
+1FCE>0020 0313 0301
+1FCF>0020 0313 0342
+1FD3>0390
+1FD8>1FD0
+1FD9>1FD1
+1FDA>1F76
+1FDB>03AF
+1FDD>0020 0314 0300
+1FDE>0020 0314 0301
+1FDF>0020 0314 0342
+1FE3>03B0
+1FE8>1FE0
+1FE9>1FE1
+1FEA>1F7A
+1FEB>03CD
+1FEC>1FE5
+1FED>0020 0308 0300
+1FEE>0020 0308 0301
+1FEF>0060
+1FF2>1F7C 03B9
+1FF3>03C9 03B9
+1FF4>03CE 03B9
+1FF7>1FF6 03B9
+1FF8>1F78
+1FF9>03CC
+1FFA>1F7C
+1FFB>03CE
+1FFC>03C9 03B9
+1FFD>0020 0301
+1FFE>0020 0314
+2000..200A>0020
+200B..200F>
+2011>2010
+2017>0020 0333
+2024>002E
+2025>002E 002E
+2026>002E 002E 002E
+202A..202E>
+202F>0020
+2033>2032 2032
+2034>2032 2032 2032
+2036>2035 2035
+2037>2035 2035 2035
+203C>0021 0021
+203E>0020 0305
+2047>003F 003F
+2048>003F 0021
+2049>0021 003F
+2057>2032 2032 2032 2032
+205F>0020
+2060..2064>
+2065..2069>
+206A..206F>
+2070>0030
+2071>0069
+2074>0034
+2075>0035
+2076>0036
+2077>0037
+2078>0038
+2079>0039
+207A>002B
+207B>2212
+207C>003D
+207D>0028
+207E>0029
+207F>006E
+2080>0030
+2081>0031
+2082>0032
+2083>0033
+2084>0034
+2085>0035
+2086>0036
+2087>0037
+2088>0038
+2089>0039
+208A>002B
+208B>2212
+208C>003D
+208D>0028
+208E>0029
+2090>0061
+2091>0065
+2092>006F
+2093>0078
+2094>0259
+20A8>0072 0073
+2100>0061 002F 0063
+2101>0061 002F 0073
+2102>0063
+2103>00B0 0063
+2105>0063 002F 006F
+2106>0063 002F 0075
+2107>025B
+2109>00B0 0066
+210A>0067
+210B..210E>0068
+210F>0127
+2110..2111>0069
+2112..2113>006C
+2115>006E
+2116>006E 006F
+2119>0070
+211A>0071
+211B..211D>0072
+2120>0073 006D
+2121>0074 0065 006C
+2122>0074 006D
+2124>007A
+2126>03C9
+2128>007A
+212A>006B
+212B>00E5
+212C>0062
+212D>0063
+212F..2130>0065
+2131>0066
+2132>214E
+2133>006D
+2134>006F
+2135>05D0
+2136>05D1
+2137>05D2
+2138>05D3
+2139>0069
+213B>0066 0061 0078
+213C>03C0
+213D..213E>03B3
+213F>03C0
+2140>2211
+2145..2146>0064
+2147>0065
+2148>0069
+2149>006A
+2150>0031 2044 0037
+2151>0031 2044 0039
+2152>0031 2044 0031 0030
+2153>0031 2044 0033
+2154>0032 2044 0033
+2155>0031 2044 0035
+2156>0032 2044 0035
+2157>0033 2044 0035
+2158>0034 2044 0035
+2159>0031 2044 0036
+215A>0035 2044 0036
+215B>0031 2044 0038
+215C>0033 2044 0038
+215D>0035 2044 0038
+215E>0037 2044 0038
+215F>0031 2044
+2160>0069
+2161>0069 0069
+2162>0069 0069 0069
+2163>0069 0076
+2164>0076
+2165>0076 0069
+2166>0076 0069 0069
+2167>0076 0069 0069 0069
+2168>0069 0078
+2169>0078
+216A>0078 0069
+216B>0078 0069 0069
+216C>006C
+216D>0063
+216E>0064
+216F>006D
+2170>0069
+2171>0069 0069
+2172>0069 0069 0069
+2173>0069 0076
+2174>0076
+2175>0076 0069
+2176>0076 0069 0069
+2177>0076 0069 0069 0069
+2178>0069 0078
+2179>0078
+217A>0078 0069
+217B>0078 0069 0069
+217C>006C
+217D>0063
+217E>0064
+217F>006D
+2183>2184
+2189>0030 2044 0033
+222C>222B 222B
+222D>222B 222B 222B
+222F>222E 222E
+2230>222E 222E 222E
+2329>3008
+232A>3009
+2460>0031
+2461>0032
+2462>0033
+2463>0034
+2464>0035
+2465>0036
+2466>0037
+2467>0038
+2468>0039
+2469>0031 0030
+246A>0031 0031
+246B>0031 0032
+246C>0031 0033
+246D>0031 0034
+246E>0031 0035
+246F>0031 0036
+2470>0031 0037
+2471>0031 0038
+2472>0031 0039
+2473>0032 0030
+2474>0028 0031 0029
+2475>0028 0032 0029
+2476>0028 0033 0029
+2477>0028 0034 0029
+2478>0028 0035 0029
+2479>0028 0036 0029
+247A>0028 0037 0029
+247B>0028 0038 0029
+247C>0028 0039 0029
+247D>0028 0031 0030 0029
+247E>0028 0031 0031 0029
+247F>0028 0031 0032 0029
+2480>0028 0031 0033 0029
+2481>0028 0031 0034 0029
+2482>0028 0031 0035 0029
+2483>0028 0031 0036 0029
+2484>0028 0031 0037 0029
+2485>0028 0031 0038 0029
+2486>0028 0031 0039 0029
+2487>0028 0032 0030 0029
+2488>0031 002E
+2489>0032 002E
+248A>0033 002E
+248B>0034 002E
+248C>0035 002E
+248D>0036 002E
+248E>0037 002E
+248F>0038 002E
+2490>0039 002E
+2491>0031 0030 002E
+2492>0031 0031 002E
+2493>0031 0032 002E
+2494>0031 0033 002E
+2495>0031 0034 002E
+2496>0031 0035 002E
+2497>0031 0036 002E
+2498>0031 0037 002E
+2499>0031 0038 002E
+249A>0031 0039 002E
+249B>0032 0030 002E
+249C>0028 0061 0029
+249D>0028 0062 0029
+249E>0028 0063 0029
+249F>0028 0064 0029
+24A0>0028 0065 0029
+24A1>0028 0066 0029
+24A2>0028 0067 0029
+24A3>0028 0068 0029
+24A4>0028 0069 0029
+24A5>0028 006A 0029
+24A6>0028 006B 0029
+24A7>0028 006C 0029
+24A8>0028 006D 0029
+24A9>0028 006E 0029
+24AA>0028 006F 0029
+24AB>0028 0070 0029
+24AC>0028 0071 0029
+24AD>0028 0072 0029
+24AE>0028 0073 0029
+24AF>0028 0074 0029
+24B0>0028 0075 0029
+24B1>0028 0076 0029
+24B2>0028 0077 0029
+24B3>0028 0078 0029
+24B4>0028 0079 0029
+24B5>0028 007A 0029
+24B6>0061
+24B7>0062
+24B8>0063
+24B9>0064
+24BA>0065
+24BB>0066
+24BC>0067
+24BD>0068
+24BE>0069
+24BF>006A
+24C0>006B
+24C1>006C
+24C2>006D
+24C3>006E
+24C4>006F
+24C5>0070
+24C6>0071
+24C7>0072
+24C8>0073
+24C9>0074
+24CA>0075
+24CB>0076
+24CC>0077
+24CD>0078
+24CE>0079
+24CF>007A
+24D0>0061
+24D1>0062
+24D2>0063
+24D3>0064
+24D4>0065
+24D5>0066
+24D6>0067
+24D7>0068
+24D8>0069
+24D9>006A
+24DA>006B
+24DB>006C
+24DC>006D
+24DD>006E
+24DE>006F
+24DF>0070
+24E0>0071
+24E1>0072
+24E2>0073
+24E3>0074
+24E4>0075
+24E5>0076
+24E6>0077
+24E7>0078
+24E8>0079
+24E9>007A
+24EA>0030
+2A0C>222B 222B 222B 222B
+2A74>003A 003A 003D
+2A75>003D 003D
+2A76>003D 003D 003D
+2ADC>2ADD 0338
+2C00>2C30
+2C01>2C31
+2C02>2C32
+2C03>2C33
+2C04>2C34
+2C05>2C35
+2C06>2C36
+2C07>2C37
+2C08>2C38
+2C09>2C39
+2C0A>2C3A
+2C0B>2C3B
+2C0C>2C3C
+2C0D>2C3D
+2C0E>2C3E
+2C0F>2C3F
+2C10>2C40
+2C11>2C41
+2C12>2C42
+2C13>2C43
+2C14>2C44
+2C15>2C45
+2C16>2C46
+2C17>2C47
+2C18>2C48
+2C19>2C49
+2C1A>2C4A
+2C1B>2C4B
+2C1C>2C4C
+2C1D>2C4D
+2C1E>2C4E
+2C1F>2C4F
+2C20>2C50
+2C21>2C51
+2C22>2C52
+2C23>2C53
+2C24>2C54
+2C25>2C55
+2C26>2C56
+2C27>2C57
+2C28>2C58
+2C29>2C59
+2C2A>2C5A
+2C2B>2C5B
+2C2C>2C5C
+2C2D>2C5D
+2C2E>2C5E
+2C60>2C61
+2C62>026B
+2C63>1D7D
+2C64>027D
+2C67>2C68
+2C69>2C6A
+2C6B>2C6C
+2C6D>0251
+2C6E>0271
+2C6F>0250
+2C70>0252
+2C72>2C73
+2C75>2C76
+2C7C>006A
+2C7D>0076
+2C7E>023F
+2C7F>0240
+2C80>2C81
+2C82>2C83
+2C84>2C85
+2C86>2C87
+2C88>2C89
+2C8A>2C8B
+2C8C>2C8D
+2C8E>2C8F
+2C90>2C91
+2C92>2C93
+2C94>2C95
+2C96>2C97
+2C98>2C99
+2C9A>2C9B
+2C9C>2C9D
+2C9E>2C9F
+2CA0>2CA1
+2CA2>2CA3
+2CA4>2CA5
+2CA6>2CA7
+2CA8>2CA9
+2CAA>2CAB
+2CAC>2CAD
+2CAE>2CAF
+2CB0>2CB1
+2CB2>2CB3
+2CB4>2CB5
+2CB6>2CB7
+2CB8>2CB9
+2CBA>2CBB
+2CBC>2CBD
+2CBE>2CBF
+2CC0>2CC1
+2CC2>2CC3
+2CC4>2CC5
+2CC6>2CC7
+2CC8>2CC9
+2CCA>2CCB
+2CCC>2CCD
+2CCE>2CCF
+2CD0>2CD1
+2CD2>2CD3
+2CD4>2CD5
+2CD6>2CD7
+2CD8>2CD9
+2CDA>2CDB
+2CDC>2CDD
+2CDE>2CDF
+2CE0>2CE1
+2CE2>2CE3
+2CEB>2CEC
+2CED>2CEE
+2D6F>2D61
+2E9F>6BCD
+2EF3>9F9F
+2F00>4E00
+2F01>4E28
+2F02>4E36
+2F03>4E3F
+2F04>4E59
+2F05>4E85
+2F06>4E8C
+2F07>4EA0
+2F08>4EBA
+2F09>513F
+2F0A>5165
+2F0B>516B
+2F0C>5182
+2F0D>5196
+2F0E>51AB
+2F0F>51E0
+2F10>51F5
+2F11>5200
+2F12>529B
+2F13>52F9
+2F14>5315
+2F15>531A
+2F16>5338
+2F17>5341
+2F18>535C
+2F19>5369
+2F1A>5382
+2F1B>53B6
+2F1C>53C8
+2F1D>53E3
+2F1E>56D7
+2F1F>571F
+2F20>58EB
+2F21>5902
+2F22>590A
+2F23>5915
+2F24>5927
+2F25>5973
+2F26>5B50
+2F27>5B80
+2F28>5BF8
+2F29>5C0F
+2F2A>5C22
+2F2B>5C38
+2F2C>5C6E
+2F2D>5C71
+2F2E>5DDB
+2F2F>5DE5
+2F30>5DF1
+2F31>5DFE
+2F32>5E72
+2F33>5E7A
+2F34>5E7F
+2F35>5EF4
+2F36>5EFE
+2F37>5F0B
+2F38>5F13
+2F39>5F50
+2F3A>5F61
+2F3B>5F73
+2F3C>5FC3
+2F3D>6208
+2F3E>6236
+2F3F>624B
+2F40>652F
+2F41>6534
+2F42>6587
+2F43>6597
+2F44>65A4
+2F45>65B9
+2F46>65E0
+2F47>65E5
+2F48>66F0
+2F49>6708
+2F4A>6728
+2F4B>6B20
+2F4C>6B62
+2F4D>6B79
+2F4E>6BB3
+2F4F>6BCB
+2F50>6BD4
+2F51>6BDB
+2F52>6C0F
+2F53>6C14
+2F54>6C34
+2F55>706B
+2F56>722A
+2F57>7236
+2F58>723B
+2F59>723F
+2F5A>7247
+2F5B>7259
+2F5C>725B
+2F5D>72AC
+2F5E>7384
+2F5F>7389
+2F60>74DC
+2F61>74E6
+2F62>7518
+2F63>751F
+2F64>7528
+2F65>7530
+2F66>758B
+2F67>7592
+2F68>7676
+2F69>767D
+2F6A>76AE
+2F6B>76BF
+2F6C>76EE
+2F6D>77DB
+2F6E>77E2
+2F6F>77F3
+2F70>793A
+2F71>79B8
+2F72>79BE
+2F73>7A74
+2F74>7ACB
+2F75>7AF9
+2F76>7C73
+2F77>7CF8
+2F78>7F36
+2F79>7F51
+2F7A>7F8A
+2F7B>7FBD
+2F7C>8001
+2F7D>800C
+2F7E>8012
+2F7F>8033
+2F80>807F
+2F81>8089
+2F82>81E3
+2F83>81EA
+2F84>81F3
+2F85>81FC
+2F86>820C
+2F87>821B
+2F88>821F
+2F89>826E
+2F8A>8272
+2F8B>8278
+2F8C>864D
+2F8D>866B
+2F8E>8840
+2F8F>884C
+2F90>8863
+2F91>897E
+2F92>898B
+2F93>89D2
+2F94>8A00
+2F95>8C37
+2F96>8C46
+2F97>8C55
+2F98>8C78
+2F99>8C9D
+2F9A>8D64
+2F9B>8D70
+2F9C>8DB3
+2F9D>8EAB
+2F9E>8ECA
+2F9F>8F9B
+2FA0>8FB0
+2FA1>8FB5
+2FA2>9091
+2FA3>9149
+2FA4>91C6
+2FA5>91CC
+2FA6>91D1
+2FA7>9577
+2FA8>9580
+2FA9>961C
+2FAA>96B6
+2FAB>96B9
+2FAC>96E8
+2FAD>9751
+2FAE>975E
+2FAF>9762
+2FB0>9769
+2FB1>97CB
+2FB2>97ED
+2FB3>97F3
+2FB4>9801
+2FB5>98A8
+2FB6>98DB
+2FB7>98DF
+2FB8>9996
+2FB9>9999
+2FBA>99AC
+2FBB>9AA8
+2FBC>9AD8
+2FBD>9ADF
+2FBE>9B25
+2FBF>9B2F
+2FC0>9B32
+2FC1>9B3C
+2FC2>9B5A
+2FC3>9CE5
+2FC4>9E75
+2FC5>9E7F
+2FC6>9EA5
+2FC7>9EBB
+2FC8>9EC3
+2FC9>9ECD
+2FCA>9ED1
+2FCB>9EF9
+2FCC>9EFD
+2FCD>9F0E
+2FCE>9F13
+2FCF>9F20
+2FD0>9F3B
+2FD1>9F4A
+2FD2>9F52
+2FD3>9F8D
+2FD4>9F9C
+2FD5>9FA0
+3000>0020
+3036>3012
+3038>5341
+3039>5344
+303A>5345
+309B>0020 3099
+309C>0020 309A
+309F>3088 308A
+30FF>30B3 30C8
+3131>1100
+3132>1101
+3133>11AA
+3134>1102
+3135>11AC
+3136>11AD
+3137>1103
+3138>1104
+3139>1105
+313A>11B0
+313B>11B1
+313C>11B2
+313D>11B3
+313E>11B4
+313F>11B5
+3140>111A
+3141>1106
+3142>1107
+3143>1108
+3144>1121
+3145>1109
+3146>110A
+3147>110B
+3148>110C
+3149>110D
+314A>110E
+314B>110F
+314C>1110
+314D>1111
+314E>1112
+314F>1161
+3150>1162
+3151>1163
+3152>1164
+3153>1165
+3154>1166
+3155>1167
+3156>1168
+3157>1169
+3158>116A
+3159>116B
+315A>116C
+315B>116D
+315C>116E
+315D>116F
+315E>1170
+315F>1171
+3160>1172
+3161>1173
+3162>1174
+3163>1175
+3164>
+3165>1114
+3166>1115
+3167>11C7
+3168>11C8
+3169>11CC
+316A>11CE
+316B>11D3
+316C>11D7
+316D>11D9
+316E>111C
+316F>11DD
+3170>11DF
+3171>111D
+3172>111E
+3173>1120
+3174>1122
+3175>1123
+3176>1127
+3177>1129
+3178>112B
+3179>112C
+317A>112D
+317B>112E
+317C>112F
+317D>1132
+317E>1136
+317F>1140
+3180>1147
+3181>114C
+3182>11F1
+3183>11F2
+3184>1157
+3185>1158
+3186>1159
+3187>1184
+3188>1185
+3189>1188
+318A>1191
+318B>1192
+318C>1194
+318D>119E
+318E>11A1
+3192>4E00
+3193>4E8C
+3194>4E09
+3195>56DB
+3196>4E0A
+3197>4E2D
+3198>4E0B
+3199>7532
+319A>4E59
+319B>4E19
+319C>4E01
+319D>5929
+319E>5730
+319F>4EBA
+3200>0028 1100 0029
+3201>0028 1102 0029
+3202>0028 1103 0029
+3203>0028 1105 0029
+3204>0028 1106 0029
+3205>0028 1107 0029
+3206>0028 1109 0029
+3207>0028 110B 0029
+3208>0028 110C 0029
+3209>0028 110E 0029
+320A>0028 110F 0029
+320B>0028 1110 0029
+320C>0028 1111 0029
+320D>0028 1112 0029
+320E>0028 AC00 0029
+320F>0028 B098 0029
+3210>0028 B2E4 0029
+3211>0028 B77C 0029
+3212>0028 B9C8 0029
+3213>0028 BC14 0029
+3214>0028 C0AC 0029
+3215>0028 C544 0029
+3216>0028 C790 0029
+3217>0028 CC28 0029
+3218>0028 CE74 0029
+3219>0028 D0C0 0029
+321A>0028 D30C 0029
+321B>0028 D558 0029
+321C>0028 C8FC 0029
+321D>0028 C624 C804 0029
+321E>0028 C624 D6C4 0029
+3220>0028 4E00 0029
+3221>0028 4E8C 0029
+3222>0028 4E09 0029
+3223>0028 56DB 0029
+3224>0028 4E94 0029
+3225>0028 516D 0029
+3226>0028 4E03 0029
+3227>0028 516B 0029
+3228>0028 4E5D 0029
+3229>0028 5341 0029
+322A>0028 6708 0029
+322B>0028 706B 0029
+322C>0028 6C34 0029
+322D>0028 6728 0029
+322E>0028 91D1 0029
+322F>0028 571F 0029
+3230>0028 65E5 0029
+3231>0028 682A 0029
+3232>0028 6709 0029
+3233>0028 793E 0029
+3234>0028 540D 0029
+3235>0028 7279 0029
+3236>0028 8CA1 0029
+3237>0028 795D 0029
+3238>0028 52B4 0029
+3239>0028 4EE3 0029
+323A>0028 547C 0029
+323B>0028 5B66 0029
+323C>0028 76E3 0029
+323D>0028 4F01 0029
+323E>0028 8CC7 0029
+323F>0028 5354 0029
+3240>0028 796D 0029
+3241>0028 4F11 0029
+3242>0028 81EA 0029
+3243>0028 81F3 0029
+3244>554F
+3245>5E7C
+3246>6587
+3247>7B8F
+3250>0070 0074 0065
+3251>0032 0031
+3252>0032 0032
+3253>0032 0033
+3254>0032 0034
+3255>0032 0035
+3256>0032 0036
+3257>0032 0037
+3258>0032 0038
+3259>0032 0039
+325A>0033 0030
+325B>0033 0031
+325C>0033 0032
+325D>0033 0033
+325E>0033 0034
+325F>0033 0035
+3260>1100
+3261>1102
+3262>1103
+3263>1105
+3264>1106
+3265>1107
+3266>1109
+3267>110B
+3268>110C
+3269>110E
+326A>110F
+326B>1110
+326C>1111
+326D>1112
+326E>AC00
+326F>B098
+3270>B2E4
+3271>B77C
+3272>B9C8
+3273>BC14
+3274>C0AC
+3275>C544
+3276>C790
+3277>CC28
+3278>CE74
+3279>D0C0
+327A>D30C
+327B>D558
+327C>CC38 ACE0
+327D>C8FC C758
+327E>C6B0
+3280>4E00
+3281>4E8C
+3282>4E09
+3283>56DB
+3284>4E94
+3285>516D
+3286>4E03
+3287>516B
+3288>4E5D
+3289>5341
+328A>6708
+328B>706B
+328C>6C34
+328D>6728
+328E>91D1
+328F>571F
+3290>65E5
+3291>682A
+3292>6709
+3293>793E
+3294>540D
+3295>7279
+3296>8CA1
+3297>795D
+3298>52B4
+3299>79D8
+329A>7537
+329B>5973
+329C>9069
+329D>512A
+329E>5370
+329F>6CE8
+32A0>9805
+32A1>4F11
+32A2>5199
+32A3>6B63
+32A4>4E0A
+32A5>4E2D
+32A6>4E0B
+32A7>5DE6
+32A8>53F3
+32A9>533B
+32AA>5B97
+32AB>5B66
+32AC>76E3
+32AD>4F01
+32AE>8CC7
+32AF>5354
+32B0>591C
+32B1>0033 0036
+32B2>0033 0037
+32B3>0033 0038
+32B4>0033 0039
+32B5>0034 0030
+32B6>0034 0031
+32B7>0034 0032
+32B8>0034 0033
+32B9>0034 0034
+32BA>0034 0035
+32BB>0034 0036
+32BC>0034 0037
+32BD>0034 0038
+32BE>0034 0039
+32BF>0035 0030
+32C0>0031 6708
+32C1>0032 6708
+32C2>0033 6708
+32C3>0034 6708
+32C4>0035 6708
+32C5>0036 6708
+32C6>0037 6708
+32C7>0038 6708
+32C8>0039 6708
+32C9>0031 0030 6708
+32CA>0031 0031 6708
+32CB>0031 0032 6708
+32CC>0068 0067
+32CD>0065 0072 0067
+32CE>0065 0076
+32CF>006C 0074 0064
+32D0>30A2
+32D1>30A4
+32D2>30A6
+32D3>30A8
+32D4>30AA
+32D5>30AB
+32D6>30AD
+32D7>30AF
+32D8>30B1
+32D9>30B3
+32DA>30B5
+32DB>30B7
+32DC>30B9
+32DD>30BB
+32DE>30BD
+32DF>30BF
+32E0>30C1
+32E1>30C4
+32E2>30C6
+32E3>30C8
+32E4>30CA
+32E5>30CB
+32E6>30CC
+32E7>30CD
+32E8>30CE
+32E9>30CF
+32EA>30D2
+32EB>30D5
+32EC>30D8
+32ED>30DB
+32EE>30DE
+32EF>30DF
+32F0>30E0
+32F1>30E1
+32F2>30E2
+32F3>30E4
+32F4>30E6
+32F5>30E8
+32F6>30E9
+32F7>30EA
+32F8>30EB
+32F9>30EC
+32FA>30ED
+32FB>30EF
+32FC>30F0
+32FD>30F1
+32FE>30F2
+3300>30A2 30D1 30FC 30C8
+3301>30A2 30EB 30D5 30A1
+3302>30A2 30F3 30DA 30A2
+3303>30A2 30FC 30EB
+3304>30A4 30CB 30F3 30B0
+3305>30A4 30F3 30C1
+3306>30A6 30A9 30F3
+3307>30A8 30B9 30AF 30FC 30C9
+3308>30A8 30FC 30AB 30FC
+3309>30AA 30F3 30B9
+330A>30AA 30FC 30E0
+330B>30AB 30A4 30EA
+330C>30AB 30E9 30C3 30C8
+330D>30AB 30ED 30EA 30FC
+330E>30AC 30ED 30F3
+330F>30AC 30F3 30DE
+3310>30AE 30AC
+3311>30AE 30CB 30FC
+3312>30AD 30E5 30EA 30FC
+3313>30AE 30EB 30C0 30FC
+3314>30AD 30ED
+3315>30AD 30ED 30B0 30E9 30E0
+3316>30AD 30ED 30E1 30FC 30C8 30EB
+3317>30AD 30ED 30EF 30C3 30C8
+3318>30B0 30E9 30E0
+3319>30B0 30E9 30E0 30C8 30F3
+331A>30AF 30EB 30BC 30A4 30ED
+331B>30AF 30ED 30FC 30CD
+331C>30B1 30FC 30B9
+331D>30B3 30EB 30CA
+331E>30B3 30FC 30DD
+331F>30B5 30A4 30AF 30EB
+3320>30B5 30F3 30C1 30FC 30E0
+3321>30B7 30EA 30F3 30B0
+3322>30BB 30F3 30C1
+3323>30BB 30F3 30C8
+3324>30C0 30FC 30B9
+3325>30C7 30B7
+3326>30C9 30EB
+3327>30C8 30F3
+3328>30CA 30CE
+3329>30CE 30C3 30C8
+332A>30CF 30A4 30C4
+332B>30D1 30FC 30BB 30F3 30C8
+332C>30D1 30FC 30C4
+332D>30D0 30FC 30EC 30EB
+332E>30D4 30A2 30B9 30C8 30EB
+332F>30D4 30AF 30EB
+3330>30D4 30B3
+3331>30D3 30EB
+3332>30D5 30A1 30E9 30C3 30C9
+3333>30D5 30A3 30FC 30C8
+3334>30D6 30C3 30B7 30A7 30EB
+3335>30D5 30E9 30F3
+3336>30D8 30AF 30BF 30FC 30EB
+3337>30DA 30BD
+3338>30DA 30CB 30D2
+3339>30D8 30EB 30C4
+333A>30DA 30F3 30B9
+333B>30DA 30FC 30B8
+333C>30D9 30FC 30BF
+333D>30DD 30A4 30F3 30C8
+333E>30DC 30EB 30C8
+333F>30DB 30F3
+3340>30DD 30F3 30C9
+3341>30DB 30FC 30EB
+3342>30DB 30FC 30F3
+3343>30DE 30A4 30AF 30ED
+3344>30DE 30A4 30EB
+3345>30DE 30C3 30CF
+3346>30DE 30EB 30AF
+3347>30DE 30F3 30B7 30E7 30F3
+3348>30DF 30AF 30ED 30F3
+3349>30DF 30EA
+334A>30DF 30EA 30D0 30FC 30EB
+334B>30E1 30AC
+334C>30E1 30AC 30C8 30F3
+334D>30E1 30FC 30C8 30EB
+334E>30E4 30FC 30C9
+334F>30E4 30FC 30EB
+3350>30E6 30A2 30F3
+3351>30EA 30C3 30C8 30EB
+3352>30EA 30E9
+3353>30EB 30D4 30FC
+3354>30EB 30FC 30D6 30EB
+3355>30EC 30E0
+3356>30EC 30F3 30C8 30B2 30F3
+3357>30EF 30C3 30C8
+3358>0030 70B9
+3359>0031 70B9
+335A>0032 70B9
+335B>0033 70B9
+335C>0034 70B9
+335D>0035 70B9
+335E>0036 70B9
+335F>0037 70B9
+3360>0038 70B9
+3361>0039 70B9
+3362>0031 0030 70B9
+3363>0031 0031 70B9
+3364>0031 0032 70B9
+3365>0031 0033 70B9
+3366>0031 0034 70B9
+3367>0031 0035 70B9
+3368>0031 0036 70B9
+3369>0031 0037 70B9
+336A>0031 0038 70B9
+336B>0031 0039 70B9
+336C>0032 0030 70B9
+336D>0032 0031 70B9
+336E>0032 0032 70B9
+336F>0032 0033 70B9
+3370>0032 0034 70B9
+3371>0068 0070 0061
+3372>0064 0061
+3373>0061 0075
+3374>0062 0061 0072
+3375>006F 0076
+3376>0070 0063
+3377>0064 006D
+3378>0064 006D 0032
+3379>0064 006D 0033
+337A>0069 0075
+337B>5E73 6210
+337C>662D 548C
+337D>5927 6B63
+337E>660E 6CBB
+337F>682A 5F0F 4F1A 793E
+3380>0070 0061
+3381>006E 0061
+3382>03BC 0061
+3383>006D 0061
+3384>006B 0061
+3385>006B 0062
+3386>006D 0062
+3387>0067 0062
+3388>0063 0061 006C
+3389>006B 0063 0061 006C
+338A>0070 0066
+338B>006E 0066
+338C>03BC 0066
+338D>03BC 0067
+338E>006D 0067
+338F>006B 0067
+3390>0068 007A
+3391>006B 0068 007A
+3392>006D 0068 007A
+3393>0067 0068 007A
+3394>0074 0068 007A
+3395>03BC 006C
+3396>006D 006C
+3397>0064 006C
+3398>006B 006C
+3399>0066 006D
+339A>006E 006D
+339B>03BC 006D
+339C>006D 006D
+339D>0063 006D
+339E>006B 006D
+339F>006D 006D 0032
+33A0>0063 006D 0032
+33A1>006D 0032
+33A2>006B 006D 0032
+33A3>006D 006D 0033
+33A4>0063 006D 0033
+33A5>006D 0033
+33A6>006B 006D 0033
+33A7>006D 2215 0073
+33A8>006D 2215 0073 0032
+33A9>0070 0061
+33AA>006B 0070 0061
+33AB>006D 0070 0061
+33AC>0067 0070 0061
+33AD>0072 0061 0064
+33AE>0072 0061 0064 2215 0073
+33AF>0072 0061 0064 2215 0073 0032
+33B0>0070 0073
+33B1>006E 0073
+33B2>03BC 0073
+33B3>006D 0073
+33B4>0070 0076
+33B5>006E 0076
+33B6>03BC 0076
+33B7>006D 0076
+33B8>006B 0076
+33B9>006D 0076
+33BA>0070 0077
+33BB>006E 0077
+33BC>03BC 0077
+33BD>006D 0077
+33BE>006B 0077
+33BF>006D 0077
+33C0>006B 03C9
+33C1>006D 03C9
+33C2>0061 002E 006D 002E
+33C3>0062 0071
+33C4>0063 0063
+33C5>0063 0064
+33C6>0063 2215 006B 0067
+33C7>0063 006F 002E
+33C8>0064 0062
+33C9>0067 0079
+33CA>0068 0061
+33CB>0068 0070
+33CC>0069 006E
+33CD>006B 006B
+33CE>006B 006D
+33CF>006B 0074
+33D0>006C 006D
+33D1>006C 006E
+33D2>006C 006F 0067
+33D3>006C 0078
+33D4>006D 0062
+33D5>006D 0069 006C
+33D6>006D 006F 006C
+33D7>0070 0068
+33D8>0070 002E 006D 002E
+33D9>0070 0070 006D
+33DA>0070 0072
+33DB>0073 0072
+33DC>0073 0076
+33DD>0077 0062
+33DE>0076 2215 006D
+33DF>0061 2215 006D
+33E0>0031 65E5
+33E1>0032 65E5
+33E2>0033 65E5
+33E3>0034 65E5
+33E4>0035 65E5
+33E5>0036 65E5
+33E6>0037 65E5
+33E7>0038 65E5
+33E8>0039 65E5
+33E9>0031 0030 65E5
+33EA>0031 0031 65E5
+33EB>0031 0032 65E5
+33EC>0031 0033 65E5
+33ED>0031 0034 65E5
+33EE>0031 0035 65E5
+33EF>0031 0036 65E5
+33F0>0031 0037 65E5
+33F1>0031 0038 65E5
+33F2>0031 0039 65E5
+33F3>0032 0030 65E5
+33F4>0032 0031 65E5
+33F5>0032 0032 65E5
+33F6>0032 0033 65E5
+33F7>0032 0034 65E5
+33F8>0032 0035 65E5
+33F9>0032 0036 65E5
+33FA>0032 0037 65E5
+33FB>0032 0038 65E5
+33FC>0032 0039 65E5
+33FD>0033 0030 65E5
+33FE>0033 0031 65E5
+33FF>0067 0061 006C
+A640>A641
+A642>A643
+A644>A645
+A646>A647
+A648>A649
+A64A>A64B
+A64C>A64D
+A64E>A64F
+A650>A651
+A652>A653
+A654>A655
+A656>A657
+A658>A659
+A65A>A65B
+A65C>A65D
+A65E>A65F
+A662>A663
+A664>A665
+A666>A667
+A668>A669
+A66A>A66B
+A66C>A66D
+A680>A681
+A682>A683
+A684>A685
+A686>A687
+A688>A689
+A68A>A68B
+A68C>A68D
+A68E>A68F
+A690>A691
+A692>A693
+A694>A695
+A696>A697
+A722>A723
+A724>A725
+A726>A727
+A728>A729
+A72A>A72B
+A72C>A72D
+A72E>A72F
+A732>A733
+A734>A735
+A736>A737
+A738>A739
+A73A>A73B
+A73C>A73D
+A73E>A73F
+A740>A741
+A742>A743
+A744>A745
+A746>A747
+A748>A749
+A74A>A74B
+A74C>A74D
+A74E>A74F
+A750>A751
+A752>A753
+A754>A755
+A756>A757
+A758>A759
+A75A>A75B
+A75C>A75D
+A75E>A75F
+A760>A761
+A762>A763
+A764>A765
+A766>A767
+A768>A769
+A76A>A76B
+A76C>A76D
+A76E>A76F
+A770>A76F
+A779>A77A
+A77B>A77C
+A77D>1D79
+A77E>A77F
+A780>A781
+A782>A783
+A784>A785
+A786>A787
+A78B>A78C
+F900>8C48
+F901>66F4
+F902>8ECA
+F903>8CC8
+F904>6ED1
+F905>4E32
+F906>53E5
+F907..F908>9F9C
+F909>5951
+F90A>91D1
+F90B>5587
+F90C>5948
+F90D>61F6
+F90E>7669
+F90F>7F85
+F910>863F
+F911>87BA
+F912>88F8
+F913>908F
+F914>6A02
+F915>6D1B
+F916>70D9
+F917>73DE
+F918>843D
+F919>916A
+F91A>99F1
+F91B>4E82
+F91C>5375
+F91D>6B04
+F91E>721B
+F91F>862D
+F920>9E1E
+F921>5D50
+F922>6FEB
+F923>85CD
+F924>8964
+F925>62C9
+F926>81D8
+F927>881F
+F928>5ECA
+F929>6717
+F92A>6D6A
+F92B>72FC
+F92C>90CE
+F92D>4F86
+F92E>51B7
+F92F>52DE
+F930>64C4
+F931>6AD3
+F932>7210
+F933>76E7
+F934>8001
+F935>8606
+F936>865C
+F937>8DEF
+F938>9732
+F939>9B6F
+F93A>9DFA
+F93B>788C
+F93C>797F
+F93D>7DA0
+F93E>83C9
+F93F>9304
+F940>9E7F
+F941>8AD6
+F942>58DF
+F943>5F04
+F944>7C60
+F945>807E
+F946>7262
+F947>78CA
+F948>8CC2
+F949>96F7
+F94A>58D8
+F94B>5C62
+F94C>6A13
+F94D>6DDA
+F94E>6F0F
+F94F>7D2F
+F950>7E37
+F951>964B
+F952>52D2
+F953>808B
+F954>51DC
+F955>51CC
+F956>7A1C
+F957>7DBE
+F958>83F1
+F959>9675
+F95A>8B80
+F95B>62CF
+F95C>6A02
+F95D>8AFE
+F95E>4E39
+F95F>5BE7
+F960>6012
+F961>7387
+F962>7570
+F963>5317
+F964>78FB
+F965>4FBF
+F966>5FA9
+F967>4E0D
+F968>6CCC
+F969>6578
+F96A>7D22
+F96B>53C3
+F96C>585E
+F96D>7701
+F96E>8449
+F96F>8AAA
+F970>6BBA
+F971>8FB0
+F972>6C88
+F973>62FE
+F974>82E5
+F975>63A0
+F976>7565
+F977>4EAE
+F978>5169
+F979>51C9
+F97A>6881
+F97B>7CE7
+F97C>826F
+F97D>8AD2
+F97E>91CF
+F97F>52F5
+F980>5442
+F981>5973
+F982>5EEC
+F983>65C5
+F984>6FFE
+F985>792A
+F986>95AD
+F987>9A6A
+F988>9E97
+F989>9ECE
+F98A>529B
+F98B>66C6
+F98C>6B77
+F98D>8F62
+F98E>5E74
+F98F>6190
+F990>6200
+F991>649A
+F992>6F23
+F993>7149
+F994>7489
+F995>79CA
+F996>7DF4
+F997>806F
+F998>8F26
+F999>84EE
+F99A>9023
+F99B>934A
+F99C>5217
+F99D>52A3
+F99E>54BD
+F99F>70C8
+F9A0>88C2
+F9A1>8AAA
+F9A2>5EC9
+F9A3>5FF5
+F9A4>637B
+F9A5>6BAE
+F9A6>7C3E
+F9A7>7375
+F9A8>4EE4
+F9A9>56F9
+F9AA>5BE7
+F9AB>5DBA
+F9AC>601C
+F9AD>73B2
+F9AE>7469
+F9AF>7F9A
+F9B0>8046
+F9B1>9234
+F9B2>96F6
+F9B3>9748
+F9B4>9818
+F9B5>4F8B
+F9B6>79AE
+F9B7>91B4
+F9B8>96B8
+F9B9>60E1
+F9BA>4E86
+F9BB>50DA
+F9BC>5BEE
+F9BD>5C3F
+F9BE>6599
+F9BF>6A02
+F9C0>71CE
+F9C1>7642
+F9C2>84FC
+F9C3>907C
+F9C4>9F8D
+F9C5>6688
+F9C6>962E
+F9C7>5289
+F9C8>677B
+F9C9>67F3
+F9CA>6D41
+F9CB>6E9C
+F9CC>7409
+F9CD>7559
+F9CE>786B
+F9CF>7D10
+F9D0>985E
+F9D1>516D
+F9D2>622E
+F9D3>9678
+F9D4>502B
+F9D5>5D19
+F9D6>6DEA
+F9D7>8F2A
+F9D8>5F8B
+F9D9>6144
+F9DA>6817
+F9DB>7387
+F9DC>9686
+F9DD>5229
+F9DE>540F
+F9DF>5C65
+F9E0>6613
+F9E1>674E
+F9E2>68A8
+F9E3>6CE5
+F9E4>7406
+F9E5>75E2
+F9E6>7F79
+F9E7>88CF
+F9E8>88E1
+F9E9>91CC
+F9EA>96E2
+F9EB>533F
+F9EC>6EBA
+F9ED>541D
+F9EE>71D0
+F9EF>7498
+F9F0>85FA
+F9F1>96A3
+F9F2>9C57
+F9F3>9E9F
+F9F4>6797
+F9F5>6DCB
+F9F6>81E8
+F9F7>7ACB
+F9F8>7B20
+F9F9>7C92
+F9FA>72C0
+F9FB>7099
+F9FC>8B58
+F9FD>4EC0
+F9FE>8336
+F9FF>523A
+FA00>5207
+FA01>5EA6
+FA02>62D3
+FA03>7CD6
+FA04>5B85
+FA05>6D1E
+FA06>66B4
+FA07>8F3B
+FA08>884C
+FA09>964D
+FA0A>898B
+FA0B>5ED3
+FA0C>5140
+FA0D>55C0
+FA10>585A
+FA12>6674
+FA15>51DE
+FA16>732A
+FA17>76CA
+FA18>793C
+FA19>795E
+FA1A>7965
+FA1B>798F
+FA1C>9756
+FA1D>7CBE
+FA1E>7FBD
+FA20>8612
+FA22>8AF8
+FA25>9038
+FA26>90FD
+FA2A>98EF
+FA2B>98FC
+FA2C>9928
+FA2D>9DB4
+FA30>4FAE
+FA31>50E7
+FA32>514D
+FA33>52C9
+FA34>52E4
+FA35>5351
+FA36>559D
+FA37>5606
+FA38>5668
+FA39>5840
+FA3A>58A8
+FA3B>5C64
+FA3C>5C6E
+FA3D>6094
+FA3E>6168
+FA3F>618E
+FA40>61F2
+FA41>654F
+FA42>65E2
+FA43>6691
+FA44>6885
+FA45>6D77
+FA46>6E1A
+FA47>6F22
+FA48>716E
+FA49>722B
+FA4A>7422
+FA4B>7891
+FA4C>793E
+FA4D>7949
+FA4E>7948
+FA4F>7950
+FA50>7956
+FA51>795D
+FA52>798D
+FA53>798E
+FA54>7A40
+FA55>7A81
+FA56>7BC0
+FA57>7DF4
+FA58>7E09
+FA59>7E41
+FA5A>7F72
+FA5B>8005
+FA5C>81ED
+FA5D..FA5E>8279
+FA5F>8457
+FA60>8910
+FA61>8996
+FA62>8B01
+FA63>8B39
+FA64>8CD3
+FA65>8D08
+FA66>8FB6
+FA67>9038
+FA68>96E3
+FA69>97FF
+FA6A>983B
+FA6B>6075
+FA6C>242EE
+FA6D>8218
+FA70>4E26
+FA71>51B5
+FA72>5168
+FA73>4F80
+FA74>5145
+FA75>5180
+FA76>52C7
+FA77>52FA
+FA78>559D
+FA79>5555
+FA7A>5599
+FA7B>55E2
+FA7C>585A
+FA7D>58B3
+FA7E>5944
+FA7F>5954
+FA80>5A62
+FA81>5B28
+FA82>5ED2
+FA83>5ED9
+FA84>5F69
+FA85>5FAD
+FA86>60D8
+FA87>614E
+FA88>6108
+FA89>618E
+FA8A>6160
+FA8B>61F2
+FA8C>6234
+FA8D>63C4
+FA8E>641C
+FA8F>6452
+FA90>6556
+FA91>6674
+FA92>6717
+FA93>671B
+FA94>6756
+FA95>6B79
+FA96>6BBA
+FA97>6D41
+FA98>6EDB
+FA99>6ECB
+FA9A>6F22
+FA9B>701E
+FA9C>716E
+FA9D>77A7
+FA9E>7235
+FA9F>72AF
+FAA0>732A
+FAA1>7471
+FAA2>7506
+FAA3>753B
+FAA4>761D
+FAA5>761F
+FAA6>76CA
+FAA7>76DB
+FAA8>76F4
+FAA9>774A
+FAAA>7740
+FAAB>78CC
+FAAC>7AB1
+FAAD>7BC0
+FAAE>7C7B
+FAAF>7D5B
+FAB0>7DF4
+FAB1>7F3E
+FAB2>8005
+FAB3>8352
+FAB4>83EF
+FAB5>8779
+FAB6>8941
+FAB7>8986
+FAB8>8996
+FAB9>8ABF
+FABA>8AF8
+FABB>8ACB
+FABC>8B01
+FABD>8AFE
+FABE>8AED
+FABF>8B39
+FAC0>8B8A
+FAC1>8D08
+FAC2>8F38
+FAC3>9072
+FAC4>9199
+FAC5>9276
+FAC6>967C
+FAC7>96E3
+FAC8>9756
+FAC9>97DB
+FACA>97FF
+FACB>980B
+FACC>983B
+FACD>9B12
+FACE>9F9C
+FACF>2284A
+FAD0>22844
+FAD1>233D5
+FAD2>3B9D
+FAD3>4018
+FAD4>4039
+FAD5>25249
+FAD6>25CD0
+FAD7>27ED3
+FAD8>9F43
+FAD9>9F8E
+FB00>0066 0066
+FB01>0066 0069
+FB02>0066 006C
+FB03>0066 0066 0069
+FB04>0066 0066 006C
+FB05..FB06>0073 0074
+FB13>0574 0576
+FB14>0574 0565
+FB15>0574 056B
+FB16>057E 0576
+FB17>0574 056D
+FB1D>05D9 05B4
+FB1F>05F2 05B7
+FB20>05E2
+FB21>05D0
+FB22>05D3
+FB23>05D4
+FB24>05DB
+FB25>05DC
+FB26>05DD
+FB27>05E8
+FB28>05EA
+FB29>002B
+FB2A>05E9 05C1
+FB2B>05E9 05C2
+FB2C>05E9 05BC 05C1
+FB2D>05E9 05BC 05C2
+FB2E>05D0 05B7
+FB2F>05D0 05B8
+FB30>05D0 05BC
+FB31>05D1 05BC
+FB32>05D2 05BC
+FB33>05D3 05BC
+FB34>05D4 05BC
+FB35>05D5 05BC
+FB36>05D6 05BC
+FB38>05D8 05BC
+FB39>05D9 05BC
+FB3A>05DA 05BC
+FB3B>05DB 05BC
+FB3C>05DC 05BC
+FB3E>05DE 05BC
+FB40>05E0 05BC
+FB41>05E1 05BC
+FB43>05E3 05BC
+FB44>05E4 05BC
+FB46>05E6 05BC
+FB47>05E7 05BC
+FB48>05E8 05BC
+FB49>05E9 05BC
+FB4A>05EA 05BC
+FB4B>05D5 05B9
+FB4C>05D1 05BF
+FB4D>05DB 05BF
+FB4E>05E4 05BF
+FB4F>05D0 05DC
+FB50..FB51>0671
+FB52..FB55>067B
+FB56..FB59>067E
+FB5A..FB5D>0680
+FB5E..FB61>067A
+FB62..FB65>067F
+FB66..FB69>0679
+FB6A..FB6D>06A4
+FB6E..FB71>06A6
+FB72..FB75>0684
+FB76..FB79>0683
+FB7A..FB7D>0686
+FB7E..FB81>0687
+FB82..FB83>068D
+FB84..FB85>068C
+FB86..FB87>068E
+FB88..FB89>0688
+FB8A..FB8B>0698
+FB8C..FB8D>0691
+FB8E..FB91>06A9
+FB92..FB95>06AF
+FB96..FB99>06B3
+FB9A..FB9D>06B1
+FB9E..FB9F>06BA
+FBA0..FBA3>06BB
+FBA4..FBA5>06C0
+FBA6..FBA9>06C1
+FBAA..FBAD>06BE
+FBAE..FBAF>06D2
+FBB0..FBB1>06D3
+FBD3..FBD6>06AD
+FBD7..FBD8>06C7
+FBD9..FBDA>06C6
+FBDB..FBDC>06C8
+FBDD>06C7 0674
+FBDE..FBDF>06CB
+FBE0..FBE1>06C5
+FBE2..FBE3>06C9
+FBE4..FBE7>06D0
+FBE8..FBE9>0649
+FBEA..FBEB>0626 0627
+FBEC..FBED>0626 06D5
+FBEE..FBEF>0626 0648
+FBF0..FBF1>0626 06C7
+FBF2..FBF3>0626 06C6
+FBF4..FBF5>0626 06C8
+FBF6..FBF8>0626 06D0
+FBF9..FBFB>0626 0649
+FBFC..FBFF>06CC
+FC00>0626 062C
+FC01>0626 062D
+FC02>0626 0645
+FC03>0626 0649
+FC04>0626 064A
+FC05>0628 062C
+FC06>0628 062D
+FC07>0628 062E
+FC08>0628 0645
+FC09>0628 0649
+FC0A>0628 064A
+FC0B>062A 062C
+FC0C>062A 062D
+FC0D>062A 062E
+FC0E>062A 0645
+FC0F>062A 0649
+FC10>062A 064A
+FC11>062B 062C
+FC12>062B 0645
+FC13>062B 0649
+FC14>062B 064A
+FC15>062C 062D
+FC16>062C 0645
+FC17>062D 062C
+FC18>062D 0645
+FC19>062E 062C
+FC1A>062E 062D
+FC1B>062E 0645
+FC1C>0633 062C
+FC1D>0633 062D
+FC1E>0633 062E
+FC1F>0633 0645
+FC20>0635 062D
+FC21>0635 0645
+FC22>0636 062C
+FC23>0636 062D
+FC24>0636 062E
+FC25>0636 0645
+FC26>0637 062D
+FC27>0637 0645
+FC28>0638 0645
+FC29>0639 062C
+FC2A>0639 0645
+FC2B>063A 062C
+FC2C>063A 0645
+FC2D>0641 062C
+FC2E>0641 062D
+FC2F>0641 062E
+FC30>0641 0645
+FC31>0641 0649
+FC32>0641 064A
+FC33>0642 062D
+FC34>0642 0645
+FC35>0642 0649
+FC36>0642 064A
+FC37>0643 0627
+FC38>0643 062C
+FC39>0643 062D
+FC3A>0643 062E
+FC3B>0643 0644
+FC3C>0643 0645
+FC3D>0643 0649
+FC3E>0643 064A
+FC3F>0644 062C
+FC40>0644 062D
+FC41>0644 062E
+FC42>0644 0645
+FC43>0644 0649
+FC44>0644 064A
+FC45>0645 062C
+FC46>0645 062D
+FC47>0645 062E
+FC48>0645 0645
+FC49>0645 0649
+FC4A>0645 064A
+FC4B>0646 062C
+FC4C>0646 062D
+FC4D>0646 062E
+FC4E>0646 0645
+FC4F>0646 0649
+FC50>0646 064A
+FC51>0647 062C
+FC52>0647 0645
+FC53>0647 0649
+FC54>0647 064A
+FC55>064A 062C
+FC56>064A 062D
+FC57>064A 062E
+FC58>064A 0645
+FC59>064A 0649
+FC5A>064A 064A
+FC5B>0630 0670
+FC5C>0631 0670
+FC5D>0649 0670
+FC5E>0020 064C 0651
+FC5F>0020 064D 0651
+FC60>0020 064E 0651
+FC61>0020 064F 0651
+FC62>0020 0650 0651
+FC63>0020 0651 0670
+FC64>0626 0631
+FC65>0626 0632
+FC66>0626 0645
+FC67>0626 0646
+FC68>0626 0649
+FC69>0626 064A
+FC6A>0628 0631
+FC6B>0628 0632
+FC6C>0628 0645
+FC6D>0628 0646
+FC6E>0628 0649
+FC6F>0628 064A
+FC70>062A 0631
+FC71>062A 0632
+FC72>062A 0645
+FC73>062A 0646
+FC74>062A 0649
+FC75>062A 064A
+FC76>062B 0631
+FC77>062B 0632
+FC78>062B 0645
+FC79>062B 0646
+FC7A>062B 0649
+FC7B>062B 064A
+FC7C>0641 0649
+FC7D>0641 064A
+FC7E>0642 0649
+FC7F>0642 064A
+FC80>0643 0627
+FC81>0643 0644
+FC82>0643 0645
+FC83>0643 0649
+FC84>0643 064A
+FC85>0644 0645
+FC86>0644 0649
+FC87>0644 064A
+FC88>0645 0627
+FC89>0645 0645
+FC8A>0646 0631
+FC8B>0646 0632
+FC8C>0646 0645
+FC8D>0646 0646
+FC8E>0646 0649
+FC8F>0646 064A
+FC90>0649 0670
+FC91>064A 0631
+FC92>064A 0632
+FC93>064A 0645
+FC94>064A 0646
+FC95>064A 0649
+FC96>064A 064A
+FC97>0626 062C
+FC98>0626 062D
+FC99>0626 062E
+FC9A>0626 0645
+FC9B>0626 0647
+FC9C>0628 062C
+FC9D>0628 062D
+FC9E>0628 062E
+FC9F>0628 0645
+FCA0>0628 0647
+FCA1>062A 062C
+FCA2>062A 062D
+FCA3>062A 062E
+FCA4>062A 0645
+FCA5>062A 0647
+FCA6>062B 0645
+FCA7>062C 062D
+FCA8>062C 0645
+FCA9>062D 062C
+FCAA>062D 0645
+FCAB>062E 062C
+FCAC>062E 0645
+FCAD>0633 062C
+FCAE>0633 062D
+FCAF>0633 062E
+FCB0>0633 0645
+FCB1>0635 062D
+FCB2>0635 062E
+FCB3>0635 0645
+FCB4>0636 062C
+FCB5>0636 062D
+FCB6>0636 062E
+FCB7>0636 0645
+FCB8>0637 062D
+FCB9>0638 0645
+FCBA>0639 062C
+FCBB>0639 0645
+FCBC>063A 062C
+FCBD>063A 0645
+FCBE>0641 062C
+FCBF>0641 062D
+FCC0>0641 062E
+FCC1>0641 0645
+FCC2>0642 062D
+FCC3>0642 0645
+FCC4>0643 062C
+FCC5>0643 062D
+FCC6>0643 062E
+FCC7>0643 0644
+FCC8>0643 0645
+FCC9>0644 062C
+FCCA>0644 062D
+FCCB>0644 062E
+FCCC>0644 0645
+FCCD>0644 0647
+FCCE>0645 062C
+FCCF>0645 062D
+FCD0>0645 062E
+FCD1>0645 0645
+FCD2>0646 062C
+FCD3>0646 062D
+FCD4>0646 062E
+FCD5>0646 0645
+FCD6>0646 0647
+FCD7>0647 062C
+FCD8>0647 0645
+FCD9>0647 0670
+FCDA>064A 062C
+FCDB>064A 062D
+FCDC>064A 062E
+FCDD>064A 0645
+FCDE>064A 0647
+FCDF>0626 0645
+FCE0>0626 0647
+FCE1>0628 0645
+FCE2>0628 0647
+FCE3>062A 0645
+FCE4>062A 0647
+FCE5>062B 0645
+FCE6>062B 0647
+FCE7>0633 0645
+FCE8>0633 0647
+FCE9>0634 0645
+FCEA>0634 0647
+FCEB>0643 0644
+FCEC>0643 0645
+FCED>0644 0645
+FCEE>0646 0645
+FCEF>0646 0647
+FCF0>064A 0645
+FCF1>064A 0647
+FCF2>0640 064E 0651
+FCF3>0640 064F 0651
+FCF4>0640 0650 0651
+FCF5>0637 0649
+FCF6>0637 064A
+FCF7>0639 0649
+FCF8>0639 064A
+FCF9>063A 0649
+FCFA>063A 064A
+FCFB>0633 0649
+FCFC>0633 064A
+FCFD>0634 0649
+FCFE>0634 064A
+FCFF>062D 0649
+FD00>062D 064A
+FD01>062C 0649
+FD02>062C 064A
+FD03>062E 0649
+FD04>062E 064A
+FD05>0635 0649
+FD06>0635 064A
+FD07>0636 0649
+FD08>0636 064A
+FD09>0634 062C
+FD0A>0634 062D
+FD0B>0634 062E
+FD0C>0634 0645
+FD0D>0634 0631
+FD0E>0633 0631
+FD0F>0635 0631
+FD10>0636 0631
+FD11>0637 0649
+FD12>0637 064A
+FD13>0639 0649
+FD14>0639 064A
+FD15>063A 0649
+FD16>063A 064A
+FD17>0633 0649
+FD18>0633 064A
+FD19>0634 0649
+FD1A>0634 064A
+FD1B>062D 0649
+FD1C>062D 064A
+FD1D>062C 0649
+FD1E>062C 064A
+FD1F>062E 0649
+FD20>062E 064A
+FD21>0635 0649
+FD22>0635 064A
+FD23>0636 0649
+FD24>0636 064A
+FD25>0634 062C
+FD26>0634 062D
+FD27>0634 062E
+FD28>0634 0645
+FD29>0634 0631
+FD2A>0633 0631
+FD2B>0635 0631
+FD2C>0636 0631
+FD2D>0634 062C
+FD2E>0634 062D
+FD2F>0634 062E
+FD30>0634 0645
+FD31>0633 0647
+FD32>0634 0647
+FD33>0637 0645
+FD34>0633 062C
+FD35>0633 062D
+FD36>0633 062E
+FD37>0634 062C
+FD38>0634 062D
+FD39>0634 062E
+FD3A>0637 0645
+FD3B>0638 0645
+FD3C..FD3D>0627 064B
+FD50>062A 062C 0645
+FD51..FD52>062A 062D 062C
+FD53>062A 062D 0645
+FD54>062A 062E 0645
+FD55>062A 0645 062C
+FD56>062A 0645 062D
+FD57>062A 0645 062E
+FD58..FD59>062C 0645 062D
+FD5A>062D 0645 064A
+FD5B>062D 0645 0649
+FD5C>0633 062D 062C
+FD5D>0633 062C 062D
+FD5E>0633 062C 0649
+FD5F..FD60>0633 0645 062D
+FD61>0633 0645 062C
+FD62..FD63>0633 0645 0645
+FD64..FD65>0635 062D 062D
+FD66>0635 0645 0645
+FD67..FD68>0634 062D 0645
+FD69>0634 062C 064A
+FD6A..FD6B>0634 0645 062E
+FD6C..FD6D>0634 0645 0645
+FD6E>0636 062D 0649
+FD6F..FD70>0636 062E 0645
+FD71..FD72>0637 0645 062D
+FD73>0637 0645 0645
+FD74>0637 0645 064A
+FD75>0639 062C 0645
+FD76..FD77>0639 0645 0645
+FD78>0639 0645 0649
+FD79>063A 0645 0645
+FD7A>063A 0645 064A
+FD7B>063A 0645 0649
+FD7C..FD7D>0641 062E 0645
+FD7E>0642 0645 062D
+FD7F>0642 0645 0645
+FD80>0644 062D 0645
+FD81>0644 062D 064A
+FD82>0644 062D 0649
+FD83..FD84>0644 062C 062C
+FD85..FD86>0644 062E 0645
+FD87..FD88>0644 0645 062D
+FD89>0645 062D 062C
+FD8A>0645 062D 0645
+FD8B>0645 062D 064A
+FD8C>0645 062C 062D
+FD8D>0645 062C 0645
+FD8E>0645 062E 062C
+FD8F>0645 062E 0645
+FD92>0645 062C 062E
+FD93>0647 0645 062C
+FD94>0647 0645 0645
+FD95>0646 062D 0645
+FD96>0646 062D 0649
+FD97..FD98>0646 062C 0645
+FD99>0646 062C 0649
+FD9A>0646 0645 064A
+FD9B>0646 0645 0649
+FD9C..FD9D>064A 0645 0645
+FD9E>0628 062E 064A
+FD9F>062A 062C 064A
+FDA0>062A 062C 0649
+FDA1>062A 062E 064A
+FDA2>062A 062E 0649
+FDA3>062A 0645 064A
+FDA4>062A 0645 0649
+FDA5>062C 0645 064A
+FDA6>062C 062D 0649
+FDA7>062C 0645 0649
+FDA8>0633 062E 0649
+FDA9>0635 062D 064A
+FDAA>0634 062D 064A
+FDAB>0636 062D 064A
+FDAC>0644 062C 064A
+FDAD>0644 0645 064A
+FDAE>064A 062D 064A
+FDAF>064A 062C 064A
+FDB0>064A 0645 064A
+FDB1>0645 0645 064A
+FDB2>0642 0645 064A
+FDB3>0646 062D 064A
+FDB4>0642 0645 062D
+FDB5>0644 062D 0645
+FDB6>0639 0645 064A
+FDB7>0643 0645 064A
+FDB8>0646 062C 062D
+FDB9>0645 062E 064A
+FDBA>0644 062C 0645
+FDBB>0643 0645 0645
+FDBC>0644 062C 0645
+FDBD>0646 062C 062D
+FDBE>062C 062D 064A
+FDBF>062D 062C 064A
+FDC0>0645 062C 064A
+FDC1>0641 0645 064A
+FDC2>0628 062D 064A
+FDC3>0643 0645 0645
+FDC4>0639 062C 0645
+FDC5>0635 0645 0645
+FDC6>0633 062E 064A
+FDC7>0646 062C 064A
+FDF0>0635 0644 06D2
+FDF1>0642 0644 06D2
+FDF2>0627 0644 0644 0647
+FDF3>0627 0643 0628 0631
+FDF4>0645 062D 0645 062F
+FDF5>0635 0644 0639 0645
+FDF6>0631 0633 0648 0644
+FDF7>0639 0644 064A 0647
+FDF8>0648 0633 0644 0645
+FDF9>0635 0644 0649
+FDFA>0635 0644 0649 0020 0627 0644 0644 0647 0020 0639 0644 064A 0647 0020 0648 0633 0644 0645
+FDFB>062C 0644 0020 062C 0644 0627 0644 0647
+FDFC>0631 06CC 0627 0644
+FE00..FE0F>
+FE10>002C
+FE11>3001
+FE12>3002
+FE13>003A
+FE14>003B
+FE15>0021
+FE16>003F
+FE17>3016
+FE18>3017
+FE19>002E 002E 002E
+FE30>002E 002E
+FE31>2014
+FE32>2013
+FE33..FE34>005F
+FE35>0028
+FE36>0029
+FE37>007B
+FE38>007D
+FE39>3014
+FE3A>3015
+FE3B>3010
+FE3C>3011
+FE3D>300A
+FE3E>300B
+FE3F>3008
+FE40>3009
+FE41>300C
+FE42>300D
+FE43>300E
+FE44>300F
+FE47>005B
+FE48>005D
+FE49..FE4C>0020 0305
+FE4D..FE4F>005F
+FE50>002C
+FE51>3001
+FE52>002E
+FE54>003B
+FE55>003A
+FE56>003F
+FE57>0021
+FE58>2014
+FE59>0028
+FE5A>0029
+FE5B>007B
+FE5C>007D
+FE5D>3014
+FE5E>3015
+FE5F>0023
+FE60>0026
+FE61>002A
+FE62>002B
+FE63>002D
+FE64>003C
+FE65>003E
+FE66>003D
+FE68>005C
+FE69>0024
+FE6A>0025
+FE6B>0040
+FE70>0020 064B
+FE71>0640 064B
+FE72>0020 064C
+FE74>0020 064D
+FE76>0020 064E
+FE77>0640 064E
+FE78>0020 064F
+FE79>0640 064F
+FE7A>0020 0650
+FE7B>0640 0650
+FE7C>0020 0651
+FE7D>0640 0651
+FE7E>0020 0652
+FE7F>0640 0652
+FE80>0621
+FE81..FE82>0622
+FE83..FE84>0623
+FE85..FE86>0624
+FE87..FE88>0625
+FE89..FE8C>0626
+FE8D..FE8E>0627
+FE8F..FE92>0628
+FE93..FE94>0629
+FE95..FE98>062A
+FE99..FE9C>062B
+FE9D..FEA0>062C
+FEA1..FEA4>062D
+FEA5..FEA8>062E
+FEA9..FEAA>062F
+FEAB..FEAC>0630
+FEAD..FEAE>0631
+FEAF..FEB0>0632
+FEB1..FEB4>0633
+FEB5..FEB8>0634
+FEB9..FEBC>0635
+FEBD..FEC0>0636
+FEC1..FEC4>0637
+FEC5..FEC8>0638
+FEC9..FECC>0639
+FECD..FED0>063A
+FED1..FED4>0641
+FED5..FED8>0642
+FED9..FEDC>0643
+FEDD..FEE0>0644
+FEE1..FEE4>0645
+FEE5..FEE8>0646
+FEE9..FEEC>0647
+FEED..FEEE>0648
+FEEF..FEF0>0649
+FEF1..FEF4>064A
+FEF5..FEF6>0644 0622
+FEF7..FEF8>0644 0623
+FEF9..FEFA>0644 0625
+FEFB..FEFC>0644 0627
+FEFF>
+FF01>0021
+FF02>0022
+FF03>0023
+FF04>0024
+FF05>0025
+FF06>0026
+FF07>0027
+FF08>0028
+FF09>0029
+FF0A>002A
+FF0B>002B
+FF0C>002C
+FF0D>002D
+FF0E>002E
+FF0F>002F
+FF10>0030
+FF11>0031
+FF12>0032
+FF13>0033
+FF14>0034
+FF15>0035
+FF16>0036
+FF17>0037
+FF18>0038
+FF19>0039
+FF1A>003A
+FF1B>003B
+FF1C>003C
+FF1D>003D
+FF1E>003E
+FF1F>003F
+FF20>0040
+FF21>0061
+FF22>0062
+FF23>0063
+FF24>0064
+FF25>0065
+FF26>0066
+FF27>0067
+FF28>0068
+FF29>0069
+FF2A>006A
+FF2B>006B
+FF2C>006C
+FF2D>006D
+FF2E>006E
+FF2F>006F
+FF30>0070
+FF31>0071
+FF32>0072
+FF33>0073
+FF34>0074
+FF35>0075
+FF36>0076
+FF37>0077
+FF38>0078
+FF39>0079
+FF3A>007A
+FF3B>005B
+FF3C>005C
+FF3D>005D
+FF3E>005E
+FF3F>005F
+FF40>0060
+FF41>0061
+FF42>0062
+FF43>0063
+FF44>0064
+FF45>0065
+FF46>0066
+FF47>0067
+FF48>0068
+FF49>0069
+FF4A>006A
+FF4B>006B
+FF4C>006C
+FF4D>006D
+FF4E>006E
+FF4F>006F
+FF50>0070
+FF51>0071
+FF52>0072
+FF53>0073
+FF54>0074
+FF55>0075
+FF56>0076
+FF57>0077
+FF58>0078
+FF59>0079
+FF5A>007A
+FF5B>007B
+FF5C>007C
+FF5D>007D
+FF5E>007E
+FF5F>2985
+FF60>2986
+FF61>3002
+FF62>300C
+FF63>300D
+FF64>3001
+FF65>30FB
+FF66>30F2
+FF67>30A1
+FF68>30A3
+FF69>30A5
+FF6A>30A7
+FF6B>30A9
+FF6C>30E3
+FF6D>30E5
+FF6E>30E7
+FF6F>30C3
+FF70>30FC
+FF71>30A2
+FF72>30A4
+FF73>30A6
+FF74>30A8
+FF75>30AA
+FF76>30AB
+FF77>30AD
+FF78>30AF
+FF79>30B1
+FF7A>30B3
+FF7B>30B5
+FF7C>30B7
+FF7D>30B9
+FF7E>30BB
+FF7F>30BD
+FF80>30BF
+FF81>30C1
+FF82>30C4
+FF83>30C6
+FF84>30C8
+FF85>30CA
+FF86>30CB
+FF87>30CC
+FF88>30CD
+FF89>30CE
+FF8A>30CF
+FF8B>30D2
+FF8C>30D5
+FF8D>30D8
+FF8E>30DB
+FF8F>30DE
+FF90>30DF
+FF91>30E0
+FF92>30E1
+FF93>30E2
+FF94>30E4
+FF95>30E6
+FF96>30E8
+FF97>30E9
+FF98>30EA
+FF99>30EB
+FF9A>30EC
+FF9B>30ED
+FF9C>30EF
+FF9D>30F3
+FF9E>3099
+FF9F>309A
+FFA0>
+FFA1>1100
+FFA2>1101
+FFA3>11AA
+FFA4>1102
+FFA5>11AC
+FFA6>11AD
+FFA7>1103
+FFA8>1104
+FFA9>1105
+FFAA>11B0
+FFAB>11B1
+FFAC>11B2
+FFAD>11B3
+FFAE>11B4
+FFAF>11B5
+FFB0>111A
+FFB1>1106
+FFB2>1107
+FFB3>1108
+FFB4>1121
+FFB5>1109
+FFB6>110A
+FFB7>110B
+FFB8>110C
+FFB9>110D
+FFBA>110E
+FFBB>110F
+FFBC>1110
+FFBD>1111
+FFBE>1112
+FFC2>1161
+FFC3>1162
+FFC4>1163
+FFC5>1164
+FFC6>1165
+FFC7>1166
+FFCA>1167
+FFCB>1168
+FFCC>1169
+FFCD>116A
+FFCE>116B
+FFCF>116C
+FFD2>116D
+FFD3>116E
+FFD4>116F
+FFD5>1170
+FFD6>1171
+FFD7>1172
+FFDA>1173
+FFDB>1174
+FFDC>1175
+FFE0>00A2
+FFE1>00A3
+FFE2>00AC
+FFE3>0020 0304
+FFE4>00A6
+FFE5>00A5
+FFE6>20A9
+FFE8>2502
+FFE9>2190
+FFEA>2191
+FFEB>2192
+FFEC>2193
+FFED>25A0
+FFEE>25CB
+FFF0..FFF8>
+10400>10428
+10401>10429
+10402>1042A
+10403>1042B
+10404>1042C
+10405>1042D
+10406>1042E
+10407>1042F
+10408>10430
+10409>10431
+1040A>10432
+1040B>10433
+1040C>10434
+1040D>10435
+1040E>10436
+1040F>10437
+10410>10438
+10411>10439
+10412>1043A
+10413>1043B
+10414>1043C
+10415>1043D
+10416>1043E
+10417>1043F
+10418>10440
+10419>10441
+1041A>10442
+1041B>10443
+1041C>10444
+1041D>10445
+1041E>10446
+1041F>10447
+10420>10448
+10421>10449
+10422>1044A
+10423>1044B
+10424>1044C
+10425>1044D
+10426>1044E
+10427>1044F
+1D15E>1D157 1D165
+1D15F>1D158 1D165
+1D160>1D158 1D165 1D16E
+1D161>1D158 1D165 1D16F
+1D162>1D158 1D165 1D170
+1D163>1D158 1D165 1D171
+1D164>1D158 1D165 1D172
+1D173..1D17A>
+1D1BB>1D1B9 1D165
+1D1BC>1D1BA 1D165
+1D1BD>1D1B9 1D165 1D16E
+1D1BE>1D1BA 1D165 1D16E
+1D1BF>1D1B9 1D165 1D16F
+1D1C0>1D1BA 1D165 1D16F
+1D400>0061
+1D401>0062
+1D402>0063
+1D403>0064
+1D404>0065
+1D405>0066
+1D406>0067
+1D407>0068
+1D408>0069
+1D409>006A
+1D40A>006B
+1D40B>006C
+1D40C>006D
+1D40D>006E
+1D40E>006F
+1D40F>0070
+1D410>0071
+1D411>0072
+1D412>0073
+1D413>0074
+1D414>0075
+1D415>0076
+1D416>0077
+1D417>0078
+1D418>0079
+1D419>007A
+1D41A>0061
+1D41B>0062
+1D41C>0063
+1D41D>0064
+1D41E>0065
+1D41F>0066
+1D420>0067
+1D421>0068
+1D422>0069
+1D423>006A
+1D424>006B
+1D425>006C
+1D426>006D
+1D427>006E
+1D428>006F
+1D429>0070
+1D42A>0071
+1D42B>0072
+1D42C>0073
+1D42D>0074
+1D42E>0075
+1D42F>0076
+1D430>0077
+1D431>0078
+1D432>0079
+1D433>007A
+1D434>0061
+1D435>0062
+1D436>0063
+1D437>0064
+1D438>0065
+1D439>0066
+1D43A>0067
+1D43B>0068
+1D43C>0069
+1D43D>006A
+1D43E>006B
+1D43F>006C
+1D440>006D
+1D441>006E
+1D442>006F
+1D443>0070
+1D444>0071
+1D445>0072
+1D446>0073
+1D447>0074
+1D448>0075
+1D449>0076
+1D44A>0077
+1D44B>0078
+1D44C>0079
+1D44D>007A
+1D44E>0061
+1D44F>0062
+1D450>0063
+1D451>0064
+1D452>0065
+1D453>0066
+1D454>0067
+1D456>0069
+1D457>006A
+1D458>006B
+1D459>006C
+1D45A>006D
+1D45B>006E
+1D45C>006F
+1D45D>0070
+1D45E>0071
+1D45F>0072
+1D460>0073
+1D461>0074
+1D462>0075
+1D463>0076
+1D464>0077
+1D465>0078
+1D466>0079
+1D467>007A
+1D468>0061
+1D469>0062
+1D46A>0063
+1D46B>0064
+1D46C>0065
+1D46D>0066
+1D46E>0067
+1D46F>0068
+1D470>0069
+1D471>006A
+1D472>006B
+1D473>006C
+1D474>006D
+1D475>006E
+1D476>006F
+1D477>0070
+1D478>0071
+1D479>0072
+1D47A>0073
+1D47B>0074
+1D47C>0075
+1D47D>0076
+1D47E>0077
+1D47F>0078
+1D480>0079
+1D481>007A
+1D482>0061
+1D483>0062
+1D484>0063
+1D485>0064
+1D486>0065
+1D487>0066
+1D488>0067
+1D489>0068
+1D48A>0069
+1D48B>006A
+1D48C>006B
+1D48D>006C
+1D48E>006D
+1D48F>006E
+1D490>006F
+1D491>0070
+1D492>0071
+1D493>0072
+1D494>0073
+1D495>0074
+1D496>0075
+1D497>0076
+1D498>0077
+1D499>0078
+1D49A>0079
+1D49B>007A
+1D49C>0061
+1D49E>0063
+1D49F>0064
+1D4A2>0067
+1D4A5>006A
+1D4A6>006B
+1D4A9>006E
+1D4AA>006F
+1D4AB>0070
+1D4AC>0071
+1D4AE>0073
+1D4AF>0074
+1D4B0>0075
+1D4B1>0076
+1D4B2>0077
+1D4B3>0078
+1D4B4>0079
+1D4B5>007A
+1D4B6>0061
+1D4B7>0062
+1D4B8>0063
+1D4B9>0064
+1D4BB>0066
+1D4BD>0068
+1D4BE>0069
+1D4BF>006A
+1D4C0>006B
+1D4C1>006C
+1D4C2>006D
+1D4C3>006E
+1D4C5>0070
+1D4C6>0071
+1D4C7>0072
+1D4C8>0073
+1D4C9>0074
+1D4CA>0075
+1D4CB>0076
+1D4CC>0077
+1D4CD>0078
+1D4CE>0079
+1D4CF>007A
+1D4D0>0061
+1D4D1>0062
+1D4D2>0063
+1D4D3>0064
+1D4D4>0065
+1D4D5>0066
+1D4D6>0067
+1D4D7>0068
+1D4D8>0069
+1D4D9>006A
+1D4DA>006B
+1D4DB>006C
+1D4DC>006D
+1D4DD>006E
+1D4DE>006F
+1D4DF>0070
+1D4E0>0071
+1D4E1>0072
+1D4E2>0073
+1D4E3>0074
+1D4E4>0075
+1D4E5>0076
+1D4E6>0077
+1D4E7>0078
+1D4E8>0079
+1D4E9>007A
+1D4EA>0061
+1D4EB>0062
+1D4EC>0063
+1D4ED>0064
+1D4EE>0065
+1D4EF>0066
+1D4F0>0067
+1D4F1>0068
+1D4F2>0069
+1D4F3>006A
+1D4F4>006B
+1D4F5>006C
+1D4F6>006D
+1D4F7>006E
+1D4F8>006F
+1D4F9>0070
+1D4FA>0071
+1D4FB>0072
+1D4FC>0073
+1D4FD>0074
+1D4FE>0075
+1D4FF>0076
+1D500>0077
+1D501>0078
+1D502>0079
+1D503>007A
+1D504>0061
+1D505>0062
+1D507>0064
+1D508>0065
+1D509>0066
+1D50A>0067
+1D50D>006A
+1D50E>006B
+1D50F>006C
+1D510>006D
+1D511>006E
+1D512>006F
+1D513>0070
+1D514>0071
+1D516>0073
+1D517>0074
+1D518>0075
+1D519>0076
+1D51A>0077
+1D51B>0078
+1D51C>0079
+1D51E>0061
+1D51F>0062
+1D520>0063
+1D521>0064
+1D522>0065
+1D523>0066
+1D524>0067
+1D525>0068
+1D526>0069
+1D527>006A
+1D528>006B
+1D529>006C
+1D52A>006D
+1D52B>006E
+1D52C>006F
+1D52D>0070
+1D52E>0071
+1D52F>0072
+1D530>0073
+1D531>0074
+1D532>0075
+1D533>0076
+1D534>0077
+1D535>0078
+1D536>0079
+1D537>007A
+1D538>0061
+1D539>0062
+1D53B>0064
+1D53C>0065
+1D53D>0066
+1D53E>0067
+1D540>0069
+1D541>006A
+1D542>006B
+1D543>006C
+1D544>006D
+1D546>006F
+1D54A>0073
+1D54B>0074
+1D54C>0075
+1D54D>0076
+1D54E>0077
+1D54F>0078
+1D550>0079
+1D552>0061
+1D553>0062
+1D554>0063
+1D555>0064
+1D556>0065
+1D557>0066
+1D558>0067
+1D559>0068
+1D55A>0069
+1D55B>006A
+1D55C>006B
+1D55D>006C
+1D55E>006D
+1D55F>006E
+1D560>006F
+1D561>0070
+1D562>0071
+1D563>0072
+1D564>0073
+1D565>0074
+1D566>0075
+1D567>0076
+1D568>0077
+1D569>0078
+1D56A>0079
+1D56B>007A
+1D56C>0061
+1D56D>0062
+1D56E>0063
+1D56F>0064
+1D570>0065
+1D571>0066
+1D572>0067
+1D573>0068
+1D574>0069
+1D575>006A
+1D576>006B
+1D577>006C
+1D578>006D
+1D579>006E
+1D57A>006F
+1D57B>0070
+1D57C>0071
+1D57D>0072
+1D57E>0073
+1D57F>0074
+1D580>0075
+1D581>0076
+1D582>0077
+1D583>0078
+1D584>0079
+1D585>007A
+1D586>0061
+1D587>0062
+1D588>0063
+1D589>0064
+1D58A>0065
+1D58B>0066
+1D58C>0067
+1D58D>0068
+1D58E>0069
+1D58F>006A
+1D590>006B
+1D591>006C
+1D592>006D
+1D593>006E
+1D594>006F
+1D595>0070
+1D596>0071
+1D597>0072
+1D598>0073
+1D599>0074
+1D59A>0075
+1D59B>0076
+1D59C>0077
+1D59D>0078
+1D59E>0079
+1D59F>007A
+1D5A0>0061
+1D5A1>0062
+1D5A2>0063
+1D5A3>0064
+1D5A4>0065
+1D5A5>0066
+1D5A6>0067
+1D5A7>0068
+1D5A8>0069
+1D5A9>006A
+1D5AA>006B
+1D5AB>006C
+1D5AC>006D
+1D5AD>006E
+1D5AE>006F
+1D5AF>0070
+1D5B0>0071
+1D5B1>0072
+1D5B2>0073
+1D5B3>0074
+1D5B4>0075
+1D5B5>0076
+1D5B6>0077
+1D5B7>0078
+1D5B8>0079
+1D5B9>007A
+1D5BA>0061
+1D5BB>0062
+1D5BC>0063
+1D5BD>0064
+1D5BE>0065
+1D5BF>0066
+1D5C0>0067
+1D5C1>0068
+1D5C2>0069
+1D5C3>006A
+1D5C4>006B
+1D5C5>006C
+1D5C6>006D
+1D5C7>006E
+1D5C8>006F
+1D5C9>0070
+1D5CA>0071
+1D5CB>0072
+1D5CC>0073
+1D5CD>0074
+1D5CE>0075
+1D5CF>0076
+1D5D0>0077
+1D5D1>0078
+1D5D2>0079
+1D5D3>007A
+1D5D4>0061
+1D5D5>0062
+1D5D6>0063
+1D5D7>0064
+1D5D8>0065
+1D5D9>0066
+1D5DA>0067
+1D5DB>0068
+1D5DC>0069
+1D5DD>006A
+1D5DE>006B
+1D5DF>006C
+1D5E0>006D
+1D5E1>006E
+1D5E2>006F
+1D5E3>0070
+1D5E4>0071
+1D5E5>0072
+1D5E6>0073
+1D5E7>0074
+1D5E8>0075
+1D5E9>0076
+1D5EA>0077
+1D5EB>0078
+1D5EC>0079
+1D5ED>007A
+1D5EE>0061
+1D5EF>0062
+1D5F0>0063
+1D5F1>0064
+1D5F2>0065
+1D5F3>0066
+1D5F4>0067
+1D5F5>0068
+1D5F6>0069
+1D5F7>006A
+1D5F8>006B
+1D5F9>006C
+1D5FA>006D
+1D5FB>006E
+1D5FC>006F
+1D5FD>0070
+1D5FE>0071
+1D5FF>0072
+1D600>0073
+1D601>0074
+1D602>0075
+1D603>0076
+1D604>0077
+1D605>0078
+1D606>0079
+1D607>007A
+1D608>0061
+1D609>0062
+1D60A>0063
+1D60B>0064
+1D60C>0065
+1D60D>0066
+1D60E>0067
+1D60F>0068
+1D610>0069
+1D611>006A
+1D612>006B
+1D613>006C
+1D614>006D
+1D615>006E
+1D616>006F
+1D617>0070
+1D618>0071
+1D619>0072
+1D61A>0073
+1D61B>0074
+1D61C>0075
+1D61D>0076
+1D61E>0077
+1D61F>0078
+1D620>0079
+1D621>007A
+1D622>0061
+1D623>0062
+1D624>0063
+1D625>0064
+1D626>0065
+1D627>0066
+1D628>0067
+1D629>0068
+1D62A>0069
+1D62B>006A
+1D62C>006B
+1D62D>006C
+1D62E>006D
+1D62F>006E
+1D630>006F
+1D631>0070
+1D632>0071
+1D633>0072
+1D634>0073
+1D635>0074
+1D636>0075
+1D637>0076
+1D638>0077
+1D639>0078
+1D63A>0079
+1D63B>007A
+1D63C>0061
+1D63D>0062
+1D63E>0063
+1D63F>0064
+1D640>0065
+1D641>0066
+1D642>0067
+1D643>0068
+1D644>0069
+1D645>006A
+1D646>006B
+1D647>006C
+1D648>006D
+1D649>006E
+1D64A>006F
+1D64B>0070
+1D64C>0071
+1D64D>0072
+1D64E>0073
+1D64F>0074
+1D650>0075
+1D651>0076
+1D652>0077
+1D653>0078
+1D654>0079
+1D655>007A
+1D656>0061
+1D657>0062
+1D658>0063
+1D659>0064
+1D65A>0065
+1D65B>0066
+1D65C>0067
+1D65D>0068
+1D65E>0069
+1D65F>006A
+1D660>006B
+1D661>006C
+1D662>006D
+1D663>006E
+1D664>006F
+1D665>0070
+1D666>0071
+1D667>0072
+1D668>0073
+1D669>0074
+1D66A>0075
+1D66B>0076
+1D66C>0077
+1D66D>0078
+1D66E>0079
+1D66F>007A
+1D670>0061
+1D671>0062
+1D672>0063
+1D673>0064
+1D674>0065
+1D675>0066
+1D676>0067
+1D677>0068
+1D678>0069
+1D679>006A
+1D67A>006B
+1D67B>006C
+1D67C>006D
+1D67D>006E
+1D67E>006F
+1D67F>0070
+1D680>0071
+1D681>0072
+1D682>0073
+1D683>0074
+1D684>0075
+1D685>0076
+1D686>0077
+1D687>0078
+1D688>0079
+1D689>007A
+1D68A>0061
+1D68B>0062
+1D68C>0063
+1D68D>0064
+1D68E>0065
+1D68F>0066
+1D690>0067
+1D691>0068
+1D692>0069
+1D693>006A
+1D694>006B
+1D695>006C
+1D696>006D
+1D697>006E
+1D698>006F
+1D699>0070
+1D69A>0071
+1D69B>0072
+1D69C>0073
+1D69D>0074
+1D69E>0075
+1D69F>0076
+1D6A0>0077
+1D6A1>0078
+1D6A2>0079
+1D6A3>007A
+1D6A4>0131
+1D6A5>0237
+1D6A8>03B1
+1D6A9>03B2
+1D6AA>03B3
+1D6AB>03B4
+1D6AC>03B5
+1D6AD>03B6
+1D6AE>03B7
+1D6AF>03B8
+1D6B0>03B9
+1D6B1>03BA
+1D6B2>03BB
+1D6B3>03BC
+1D6B4>03BD
+1D6B5>03BE
+1D6B6>03BF
+1D6B7>03C0
+1D6B8>03C1
+1D6B9>03B8
+1D6BA>03C3
+1D6BB>03C4
+1D6BC>03C5
+1D6BD>03C6
+1D6BE>03C7
+1D6BF>03C8
+1D6C0>03C9
+1D6C1>2207
+1D6C2>03B1
+1D6C3>03B2
+1D6C4>03B3
+1D6C5>03B4
+1D6C6>03B5
+1D6C7>03B6
+1D6C8>03B7
+1D6C9>03B8
+1D6CA>03B9
+1D6CB>03BA
+1D6CC>03BB
+1D6CD>03BC
+1D6CE>03BD
+1D6CF>03BE
+1D6D0>03BF
+1D6D1>03C0
+1D6D2>03C1
+1D6D3..1D6D4>03C3
+1D6D5>03C4
+1D6D6>03C5
+1D6D7>03C6
+1D6D8>03C7
+1D6D9>03C8
+1D6DA>03C9
+1D6DB>2202
+1D6DC>03B5
+1D6DD>03B8
+1D6DE>03BA
+1D6DF>03C6
+1D6E0>03C1
+1D6E1>03C0
+1D6E2>03B1
+1D6E3>03B2
+1D6E4>03B3
+1D6E5>03B4
+1D6E6>03B5
+1D6E7>03B6
+1D6E8>03B7
+1D6E9>03B8
+1D6EA>03B9
+1D6EB>03BA
+1D6EC>03BB
+1D6ED>03BC
+1D6EE>03BD
+1D6EF>03BE
+1D6F0>03BF
+1D6F1>03C0
+1D6F2>03C1
+1D6F3>03B8
+1D6F4>03C3
+1D6F5>03C4
+1D6F6>03C5
+1D6F7>03C6
+1D6F8>03C7
+1D6F9>03C8
+1D6FA>03C9
+1D6FB>2207
+1D6FC>03B1
+1D6FD>03B2
+1D6FE>03B3
+1D6FF>03B4
+1D700>03B5
+1D701>03B6
+1D702>03B7
+1D703>03B8
+1D704>03B9
+1D705>03BA
+1D706>03BB
+1D707>03BC
+1D708>03BD
+1D709>03BE
+1D70A>03BF
+1D70B>03C0
+1D70C>03C1
+1D70D..1D70E>03C3
+1D70F>03C4
+1D710>03C5
+1D711>03C6
+1D712>03C7
+1D713>03C8
+1D714>03C9
+1D715>2202
+1D716>03B5
+1D717>03B8
+1D718>03BA
+1D719>03C6
+1D71A>03C1
+1D71B>03C0
+1D71C>03B1
+1D71D>03B2
+1D71E>03B3
+1D71F>03B4
+1D720>03B5
+1D721>03B6
+1D722>03B7
+1D723>03B8
+1D724>03B9
+1D725>03BA
+1D726>03BB
+1D727>03BC
+1D728>03BD
+1D729>03BE
+1D72A>03BF
+1D72B>03C0
+1D72C>03C1
+1D72D>03B8
+1D72E>03C3
+1D72F>03C4
+1D730>03C5
+1D731>03C6
+1D732>03C7
+1D733>03C8
+1D734>03C9
+1D735>2207
+1D736>03B1
+1D737>03B2
+1D738>03B3
+1D739>03B4
+1D73A>03B5
+1D73B>03B6
+1D73C>03B7
+1D73D>03B8
+1D73E>03B9
+1D73F>03BA
+1D740>03BB
+1D741>03BC
+1D742>03BD
+1D743>03BE
+1D744>03BF
+1D745>03C0
+1D746>03C1
+1D747..1D748>03C3
+1D749>03C4
+1D74A>03C5
+1D74B>03C6
+1D74C>03C7
+1D74D>03C8
+1D74E>03C9
+1D74F>2202
+1D750>03B5
+1D751>03B8
+1D752>03BA
+1D753>03C6
+1D754>03C1
+1D755>03C0
+1D756>03B1
+1D757>03B2
+1D758>03B3
+1D759>03B4
+1D75A>03B5
+1D75B>03B6
+1D75C>03B7
+1D75D>03B8
+1D75E>03B9
+1D75F>03BA
+1D760>03BB
+1D761>03BC
+1D762>03BD
+1D763>03BE
+1D764>03BF
+1D765>03C0
+1D766>03C1
+1D767>03B8
+1D768>03C3
+1D769>03C4
+1D76A>03C5
+1D76B>03C6
+1D76C>03C7
+1D76D>03C8
+1D76E>03C9
+1D76F>2207
+1D770>03B1
+1D771>03B2
+1D772>03B3
+1D773>03B4
+1D774>03B5
+1D775>03B6
+1D776>03B7
+1D777>03B8
+1D778>03B9
+1D779>03BA
+1D77A>03BB
+1D77B>03BC
+1D77C>03BD
+1D77D>03BE
+1D77E>03BF
+1D77F>03C0
+1D780>03C1
+1D781..1D782>03C3
+1D783>03C4
+1D784>03C5
+1D785>03C6
+1D786>03C7
+1D787>03C8
+1D788>03C9
+1D789>2202
+1D78A>03B5
+1D78B>03B8
+1D78C>03BA
+1D78D>03C6
+1D78E>03C1
+1D78F>03C0
+1D790>03B1
+1D791>03B2
+1D792>03B3
+1D793>03B4
+1D794>03B5
+1D795>03B6
+1D796>03B7
+1D797>03B8
+1D798>03B9
+1D799>03BA
+1D79A>03BB
+1D79B>03BC
+1D79C>03BD
+1D79D>03BE
+1D79E>03BF
+1D79F>03C0
+1D7A0>03C1
+1D7A1>03B8
+1D7A2>03C3
+1D7A3>03C4
+1D7A4>03C5
+1D7A5>03C6
+1D7A6>03C7
+1D7A7>03C8
+1D7A8>03C9
+1D7A9>2207
+1D7AA>03B1
+1D7AB>03B2
+1D7AC>03B3
+1D7AD>03B4
+1D7AE>03B5
+1D7AF>03B6
+1D7B0>03B7
+1D7B1>03B8
+1D7B2>03B9
+1D7B3>03BA
+1D7B4>03BB
+1D7B5>03BC
+1D7B6>03BD
+1D7B7>03BE
+1D7B8>03BF
+1D7B9>03C0
+1D7BA>03C1
+1D7BB..1D7BC>03C3
+1D7BD>03C4
+1D7BE>03C5
+1D7BF>03C6
+1D7C0>03C7
+1D7C1>03C8
+1D7C2>03C9
+1D7C3>2202
+1D7C4>03B5
+1D7C5>03B8
+1D7C6>03BA
+1D7C7>03C6
+1D7C8>03C1
+1D7C9>03C0
+1D7CA..1D7CB>03DD
+1D7CE>0030
+1D7CF>0031
+1D7D0>0032
+1D7D1>0033
+1D7D2>0034
+1D7D3>0035
+1D7D4>0036
+1D7D5>0037
+1D7D6>0038
+1D7D7>0039
+1D7D8>0030
+1D7D9>0031
+1D7DA>0032
+1D7DB>0033
+1D7DC>0034
+1D7DD>0035
+1D7DE>0036
+1D7DF>0037
+1D7E0>0038
+1D7E1>0039
+1D7E2>0030
+1D7E3>0031
+1D7E4>0032
+1D7E5>0033
+1D7E6>0034
+1D7E7>0035
+1D7E8>0036
+1D7E9>0037
+1D7EA>0038
+1D7EB>0039
+1D7EC>0030
+1D7ED>0031
+1D7EE>0032
+1D7EF>0033
+1D7F0>0034
+1D7F1>0035
+1D7F2>0036
+1D7F3>0037
+1D7F4>0038
+1D7F5>0039
+1D7F6>0030
+1D7F7>0031
+1D7F8>0032
+1D7F9>0033
+1D7FA>0034
+1D7FB>0035
+1D7FC>0036
+1D7FD>0037
+1D7FE>0038
+1D7FF>0039
+1F100>0030 002E
+1F101>0030 002C
+1F102>0031 002C
+1F103>0032 002C
+1F104>0033 002C
+1F105>0034 002C
+1F106>0035 002C
+1F107>0036 002C
+1F108>0037 002C
+1F109>0038 002C
+1F10A>0039 002C
+1F110>0028 0061 0029
+1F111>0028 0062 0029
+1F112>0028 0063 0029
+1F113>0028 0064 0029
+1F114>0028 0065 0029
+1F115>0028 0066 0029
+1F116>0028 0067 0029
+1F117>0028 0068 0029
+1F118>0028 0069 0029
+1F119>0028 006A 0029
+1F11A>0028 006B 0029
+1F11B>0028 006C 0029
+1F11C>0028 006D 0029
+1F11D>0028 006E 0029
+1F11E>0028 006F 0029
+1F11F>0028 0070 0029
+1F120>0028 0071 0029
+1F121>0028 0072 0029
+1F122>0028 0073 0029
+1F123>0028 0074 0029
+1F124>0028 0075 0029
+1F125>0028 0076 0029
+1F126>0028 0077 0029
+1F127>0028 0078 0029
+1F128>0028 0079 0029
+1F129>0028 007A 0029
+1F12A>3014 0073 3015
+1F12B>0063
+1F12C>0072
+1F12D>0063 0064
+1F12E>0077 007A
+1F131>0062
+1F13D>006E
+1F13F>0070
+1F142>0073
+1F146>0077
+1F14A>0068 0076
+1F14B>006D 0076
+1F14C>0073 0064
+1F14D>0073 0073
+1F14E>0070 0070 0076
+1F190>0064 006A
+1F200>307B 304B
+1F210>624B
+1F211>5B57
+1F212>53CC
+1F213>30C7
+1F214>4E8C
+1F215>591A
+1F216>89E3
+1F217>5929
+1F218>4EA4
+1F219>6620
+1F21A>7121
+1F21B>6599
+1F21C>524D
+1F21D>5F8C
+1F21E>518D
+1F21F>65B0
+1F220>521D
+1F221>7D42
+1F222>751F
+1F223>8CA9
+1F224>58F0
+1F225>5439
+1F226>6F14
+1F227>6295
+1F228>6355
+1F229>4E00
+1F22A>4E09
+1F22B>904A
+1F22C>5DE6
+1F22D>4E2D
+1F22E>53F3
+1F22F>6307
+1F230>8D70
+1F231>6253
+1F240>3014 672C 3015
+1F241>3014 4E09 3015
+1F242>3014 4E8C 3015
+1F243>3014 5B89 3015
+1F244>3014 70B9 3015
+1F245>3014 6253 3015
+1F246>3014 76D7 3015
+1F247>3014 52DD 3015
+1F248>3014 6557 3015
+2F800>4E3D
+2F801>4E38
+2F802>4E41
+2F803>20122
+2F804>4F60
+2F805>4FAE
+2F806>4FBB
+2F807>5002
+2F808>507A
+2F809>5099
+2F80A>50E7
+2F80B>50CF
+2F80C>349E
+2F80D>2063A
+2F80E>514D
+2F80F>5154
+2F810>5164
+2F811>5177
+2F812>2051C
+2F813>34B9
+2F814>5167
+2F815>518D
+2F816>2054B
+2F817>5197
+2F818>51A4
+2F819>4ECC
+2F81A>51AC
+2F81B>51B5
+2F81C>291DF
+2F81D>51F5
+2F81E>5203
+2F81F>34DF
+2F820>523B
+2F821>5246
+2F822>5272
+2F823>5277
+2F824>3515
+2F825>52C7
+2F826>52C9
+2F827>52E4
+2F828>52FA
+2F829>5305
+2F82A>5306
+2F82B>5317
+2F82C>5349
+2F82D>5351
+2F82E>535A
+2F82F>5373
+2F830>537D
+2F831..2F833>537F
+2F834>20A2C
+2F835>7070
+2F836>53CA
+2F837>53DF
+2F838>20B63
+2F839>53EB
+2F83A>53F1
+2F83B>5406
+2F83C>549E
+2F83D>5438
+2F83E>5448
+2F83F>5468
+2F840>54A2
+2F841>54F6
+2F842>5510
+2F843>5553
+2F844>5563
+2F845..2F846>5584
+2F847>5599
+2F848>55AB
+2F849>55B3
+2F84A>55C2
+2F84B>5716
+2F84C>5606
+2F84D>5717
+2F84E>5651
+2F84F>5674
+2F850>5207
+2F851>58EE
+2F852>57CE
+2F853>57F4
+2F854>580D
+2F855>578B
+2F856>5832
+2F857>5831
+2F858>58AC
+2F859>214E4
+2F85A>58F2
+2F85B>58F7
+2F85C>5906
+2F85D>591A
+2F85E>5922
+2F85F>5962
+2F860>216A8
+2F861>216EA
+2F862>59EC
+2F863>5A1B
+2F864>5A27
+2F865>59D8
+2F866>5A66
+2F867>36EE
+2F868>36FC
+2F869>5B08
+2F86A..2F86B>5B3E
+2F86C>219C8
+2F86D>5BC3
+2F86E>5BD8
+2F86F>5BE7
+2F870>5BF3
+2F871>21B18
+2F872>5BFF
+2F873>5C06
+2F874>5F53
+2F875>5C22
+2F876>3781
+2F877>5C60
+2F878>5C6E
+2F879>5CC0
+2F87A>5C8D
+2F87B>21DE4
+2F87C>5D43
+2F87D>21DE6
+2F87E>5D6E
+2F87F>5D6B
+2F880>5D7C
+2F881>5DE1
+2F882>5DE2
+2F883>382F
+2F884>5DFD
+2F885>5E28
+2F886>5E3D
+2F887>5E69
+2F888>3862
+2F889>22183
+2F88A>387C
+2F88B>5EB0
+2F88C>5EB3
+2F88D>5EB6
+2F88E>5ECA
+2F88F>2A392
+2F890>5EFE
+2F891..2F892>22331
+2F893>8201
+2F894..2F895>5F22
+2F896>38C7
+2F897>232B8
+2F898>261DA
+2F899>5F62
+2F89A>5F6B
+2F89B>38E3
+2F89C>5F9A
+2F89D>5FCD
+2F89E>5FD7
+2F89F>5FF9
+2F8A0>6081
+2F8A1>393A
+2F8A2>391C
+2F8A3>6094
+2F8A4>226D4
+2F8A5>60C7
+2F8A6>6148
+2F8A7>614C
+2F8A8>614E
+2F8A9>614C
+2F8AA>617A
+2F8AB>618E
+2F8AC>61B2
+2F8AD>61A4
+2F8AE>61AF
+2F8AF>61DE
+2F8B0>61F2
+2F8B1>61F6
+2F8B2>6210
+2F8B3>621B
+2F8B4>625D
+2F8B5>62B1
+2F8B6>62D4
+2F8B7>6350
+2F8B8>22B0C
+2F8B9>633D
+2F8BA>62FC
+2F8BB>6368
+2F8BC>6383
+2F8BD>63E4
+2F8BE>22BF1
+2F8BF>6422
+2F8C0>63C5
+2F8C1>63A9
+2F8C2>3A2E
+2F8C3>6469
+2F8C4>647E
+2F8C5>649D
+2F8C6>6477
+2F8C7>3A6C
+2F8C8>654F
+2F8C9>656C
+2F8CA>2300A
+2F8CB>65E3
+2F8CC>66F8
+2F8CD>6649
+2F8CE>3B19
+2F8CF>6691
+2F8D0>3B08
+2F8D1>3AE4
+2F8D2>5192
+2F8D3>5195
+2F8D4>6700
+2F8D5>669C
+2F8D6>80AD
+2F8D7>43D9
+2F8D8>6717
+2F8D9>671B
+2F8DA>6721
+2F8DB>675E
+2F8DC>6753
+2F8DD>233C3
+2F8DE>3B49
+2F8DF>67FA
+2F8E0>6785
+2F8E1>6852
+2F8E2>6885
+2F8E3>2346D
+2F8E4>688E
+2F8E5>681F
+2F8E6>6914
+2F8E7>3B9D
+2F8E8>6942
+2F8E9>69A3
+2F8EA>69EA
+2F8EB>6AA8
+2F8EC>236A3
+2F8ED>6ADB
+2F8EE>3C18
+2F8EF>6B21
+2F8F0>238A7
+2F8F1>6B54
+2F8F2>3C4E
+2F8F3>6B72
+2F8F4>6B9F
+2F8F5>6BBA
+2F8F6>6BBB
+2F8F7>23A8D
+2F8F8>21D0B
+2F8F9>23AFA
+2F8FA>6C4E
+2F8FB>23CBC
+2F8FC>6CBF
+2F8FD>6CCD
+2F8FE>6C67
+2F8FF>6D16
+2F900>6D3E
+2F901>6D77
+2F902>6D41
+2F903>6D69
+2F904>6D78
+2F905>6D85
+2F906>23D1E
+2F907>6D34
+2F908>6E2F
+2F909>6E6E
+2F90A>3D33
+2F90B>6ECB
+2F90C>6EC7
+2F90D>23ED1
+2F90E>6DF9
+2F90F>6F6E
+2F910>23F5E
+2F911>23F8E
+2F912>6FC6
+2F913>7039
+2F914>701E
+2F915>701B
+2F916>3D96
+2F917>704A
+2F918>707D
+2F919>7077
+2F91A>70AD
+2F91B>20525
+2F91C>7145
+2F91D>24263
+2F91E>719C
+2F91F>243AB
+2F920>7228
+2F921>7235
+2F922>7250
+2F923>24608
+2F924>7280
+2F925>7295
+2F926>24735
+2F927>24814
+2F928>737A
+2F929>738B
+2F92A>3EAC
+2F92B>73A5
+2F92C..2F92D>3EB8
+2F92E>7447
+2F92F>745C
+2F930>7471
+2F931>7485
+2F932>74CA
+2F933>3F1B
+2F934>7524
+2F935>24C36
+2F936>753E
+2F937>24C92
+2F938>7570
+2F939>2219F
+2F93A>7610
+2F93B>24FA1
+2F93C>24FB8
+2F93D>25044
+2F93E>3FFC
+2F93F>4008
+2F940>76F4
+2F941>250F3
+2F942>250F2
+2F943>25119
+2F944>25133
+2F945>771E
+2F946..2F947>771F
+2F948>774A
+2F949>4039
+2F94A>778B
+2F94B>4046
+2F94C>4096
+2F94D>2541D
+2F94E>784E
+2F94F>788C
+2F950>78CC
+2F951>40E3
+2F952>25626
+2F953>7956
+2F954>2569A
+2F955>256C5
+2F956>798F
+2F957>79EB
+2F958>412F
+2F959>7A40
+2F95A>7A4A
+2F95B>7A4F
+2F95C>2597C
+2F95D..2F95E>25AA7
+2F95F>7AEE
+2F960>4202
+2F961>25BAB
+2F962>7BC6
+2F963>7BC9
+2F964>4227
+2F965>25C80
+2F966>7CD2
+2F967>42A0
+2F968>7CE8
+2F969>7CE3
+2F96A>7D00
+2F96B>25F86
+2F96C>7D63
+2F96D>4301
+2F96E>7DC7
+2F96F>7E02
+2F970>7E45
+2F971>4334
+2F972>26228
+2F973>26247
+2F974>4359
+2F975>262D9
+2F976>7F7A
+2F977>2633E
+2F978>7F95
+2F979>7FFA
+2F97A>8005
+2F97B>264DA
+2F97C>26523
+2F97D>8060
+2F97E>265A8
+2F97F>8070
+2F980>2335F
+2F981>43D5
+2F982>80B2
+2F983>8103
+2F984>440B
+2F985>813E
+2F986>5AB5
+2F987>267A7
+2F988>267B5
+2F989>23393
+2F98A>2339C
+2F98B>8201
+2F98C>8204
+2F98D>8F9E
+2F98E>446B
+2F98F>8291
+2F990>828B
+2F991>829D
+2F992>52B3
+2F993>82B1
+2F994>82B3
+2F995>82BD
+2F996>82E6
+2F997>26B3C
+2F998>82E5
+2F999>831D
+2F99A>8363
+2F99B>83AD
+2F99C>8323
+2F99D>83BD
+2F99E>83E7
+2F99F>8457
+2F9A0>8353
+2F9A1>83CA
+2F9A2>83CC
+2F9A3>83DC
+2F9A4>26C36
+2F9A5>26D6B
+2F9A6>26CD5
+2F9A7>452B
+2F9A8>84F1
+2F9A9>84F3
+2F9AA>8516
+2F9AB>273CA
+2F9AC>8564
+2F9AD>26F2C
+2F9AE>455D
+2F9AF>4561
+2F9B0>26FB1
+2F9B1>270D2
+2F9B2>456B
+2F9B3>8650
+2F9B4>865C
+2F9B5>8667
+2F9B6>8669
+2F9B7>86A9
+2F9B8>8688
+2F9B9>870E
+2F9BA>86E2
+2F9BB>8779
+2F9BC>8728
+2F9BD>876B
+2F9BE>8786
+2F9BF>45D7
+2F9C0>87E1
+2F9C1>8801
+2F9C2>45F9
+2F9C3>8860
+2F9C4>8863
+2F9C5>27667
+2F9C6>88D7
+2F9C7>88DE
+2F9C8>4635
+2F9C9>88FA
+2F9CA>34BB
+2F9CB>278AE
+2F9CC>27966
+2F9CD>46BE
+2F9CE>46C7
+2F9CF>8AA0
+2F9D0>8AED
+2F9D1>8B8A
+2F9D2>8C55
+2F9D3>27CA8
+2F9D4>8CAB
+2F9D5>8CC1
+2F9D6>8D1B
+2F9D7>8D77
+2F9D8>27F2F
+2F9D9>20804
+2F9DA>8DCB
+2F9DB>8DBC
+2F9DC>8DF0
+2F9DD>208DE
+2F9DE>8ED4
+2F9DF>8F38
+2F9E0>285D2
+2F9E1>285ED
+2F9E2>9094
+2F9E3>90F1
+2F9E4>9111
+2F9E5>2872E
+2F9E6>911B
+2F9E7>9238
+2F9E8>92D7
+2F9E9>92D8
+2F9EA>927C
+2F9EB>93F9
+2F9EC>9415
+2F9ED>28BFA
+2F9EE>958B
+2F9EF>4995
+2F9F0>95B7
+2F9F1>28D77
+2F9F2>49E6
+2F9F3>96C3
+2F9F4>5DB2
+2F9F5>9723
+2F9F6>29145
+2F9F7>2921A
+2F9F8>4A6E
+2F9F9>4A76
+2F9FA>97E0
+2F9FB>2940A
+2F9FC>4AB2
+2F9FD>29496
+2F9FE..2F9FF>980B
+2FA00>9829
+2FA01>295B6
+2FA02>98E2
+2FA03>4B33
+2FA04>9929
+2FA05>99A7
+2FA06>99C2
+2FA07>99FE
+2FA08>4BCE
+2FA09>29B30
+2FA0A>9B12
+2FA0B>9C40
+2FA0C>9CFD
+2FA0D>4CCE
+2FA0E>4CED
+2FA0F>9D67
+2FA10>2A0CE
+2FA11>4CF8
+2FA12>2A105
+2FA13>2A20E
+2FA14>2A291
+2FA15>9EBB
+2FA16>4D56
+2FA17>9EF9
+2FA18>9EFE
+2FA19>9F05
+2FA1A>9F0F
+2FA1B>9F16
+2FA1C>9F3B
+2FA1D>2A600
+E0000>
+E0001>
+E0002..E001F>
+E0020..E007F>
+E0080..E00FF>
+E0100..E01EF>
+E01F0..E0FFF>
+
+# Total code points: 9740
diff --git a/comm/mailnews/extensions/fts3/fts3_porter.c b/comm/mailnews/extensions/fts3/fts3_porter.c
new file mode 100644
index 0000000000..e0e5d5ddaf
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/fts3_porter.c
@@ -0,0 +1,1140 @@
+/*
+** 2006 September 30
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** Implementation of the full-text-search tokenizer that implements
+** a Porter stemmer.
+**
+*/
+
+/*
+ * This file is based on the SQLite FTS3 Porter Stemmer implementation.
+ *
+ * This is an attempt to provide some level of full-text search to users of
+ * Thunderbird who use languages that are not space/punctuation delimited.
+ * This is accomplished by performing bi-gram indexing of characters fall
+ * into the unicode space occupied by character sets used in such languages.
+ *
+ * Bi-gram indexing means that given the string "12345" we would index the
+ * pairs "12", "23", "34", and "45" (with position information). We do this
+ * because we are not sure where the word/semantic boundaries are in that
+ * string. Then, when a user searches for "234" the FTS3 engine tokenizes the
+ * search query into "23" and "34". Using special phrase-logic FTS3 requires
+ * the matches to have the tokens "23" and "34" adjacent to each other and in
+ * that order. In theory if the user searched for "2345" we we could just
+ * search for "23 NEAR/2 34". Unfortunately, NEAR does not imply ordering,
+ * so even though that would be more efficient, we would lose correctness
+ * and cannot do it.
+ *
+ * The efficiency and usability of bi-gram search assumes that the character
+ * space is large enough and actually observed bi-grams sufficiently
+ * distributed throughout the potential space so that the search bi-grams
+ * generated when the user issues a query find a 'reasonable' number of
+ * documents for each bi-gram match.
+ *
+ * Mozilla contributors:
+ * Makoto Kato <m_kato@ga2.so-net.ne.jp>
+ * Andrew Sutherland <asutherland@asutherland.org>
+ */
+
+/*
+** The code in this file is only compiled if:
+**
+** * The FTS3 module is being built as an extension
+** (in which case SQLITE_CORE is not defined), or
+**
+** * The FTS3 module is being built into the core of
+** SQLite (in which case SQLITE_ENABLE_FTS3 is defined).
+*/
+#if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3)
+
+# include <assert.h>
+# include <stdlib.h>
+# include <stdio.h>
+# include <string.h>
+# include <ctype.h>
+
+# include "fts3_tokenizer.h"
+
+/* need some defined to compile without sqlite3 code */
+
+# define sqlite3_malloc malloc
+# define sqlite3_free free
+# define sqlite3_realloc realloc
+
+static const unsigned char sqlite3Utf8Trans1[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+ 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
+ 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00,
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00,
+};
+
+typedef unsigned char u8;
+
+/**
+ * SQLite helper macro from sqlite3.c (really utf.c) to encode a unicode
+ * character into utf8.
+ *
+ * @param zOut A pointer to the current write position that is updated by
+ * the routine. At entry it should point to one-past the last valid
+ * encoded byte. The same holds true at exit.
+ * @param c The character to encode; this should be an unsigned int.
+ */
+# define WRITE_UTF8(zOut, c) \
+ { \
+ if (c < 0x0080) { \
+ *zOut++ = (u8)(c & 0xff); \
+ } else if (c < 0x0800) { \
+ *zOut++ = 0xC0 + (u8)((c >> 6) & 0x1F); \
+ *zOut++ = 0x80 + (u8)(c & 0x3F); \
+ } else if (c < 0x10000) { \
+ *zOut++ = 0xE0 + (u8)((c >> 12) & 0x0F); \
+ *zOut++ = 0x80 + (u8)((c >> 6) & 0x3F); \
+ *zOut++ = 0x80 + (u8)(c & 0x3F); \
+ } else { \
+ *zOut++ = 0xf0 + (u8)((c >> 18) & 0x07); \
+ *zOut++ = 0x80 + (u8)((c >> 12) & 0x3F); \
+ *zOut++ = 0x80 + (u8)((c >> 6) & 0x3F); \
+ *zOut++ = 0x80 + (u8)(c & 0x3F); \
+ } \
+ }
+
+/**
+ * Fudge factor to avoid buffer overwrites when WRITE_UTF8 is involved.
+ *
+ * Our normalization table includes entries that may result in a larger
+ * utf-8 encoding. Namely, 023a maps to 2c65. This is a growth from 2 bytes
+ * as utf-8 encoded to 3 bytes. This is currently the only transition possible
+ * because 1-byte encodings are known to stay 1-byte and our normalization
+ * table is 16-bit and so can't generate a 4-byte encoded output.
+ *
+ * For simplicity, we just multiple by 2 which covers the current case and
+ * potential growth for 2-byte to 4-byte growth. We can afford to do this
+ * because we're not talking about a lot of memory here as a rule.
+ */
+# define MAX_UTF8_GROWTH_FACTOR 2
+
+/**
+ * Helper from sqlite3.c to read a single UTF8 character.
+ *
+ * The clever bit with multi-byte reading is that you keep going until you find
+ * a byte whose top bits are not '10'. A single-byte UTF8 character will have
+ * '00' or '01', and a multi-byte UTF8 character must start with '11'.
+ *
+ * In the event of illegal UTF-8 this macro may read an arbitrary number of
+ * characters but will never read past zTerm. The resulting character value
+ * of illegal UTF-8 can be anything, although efforts are made to return the
+ * illegal character (0xfffd) for UTF-16 surrogates.
+ *
+ * @param zIn A pointer to the current position that is updated by the routine,
+ * pointing at the start of the next character when the routine returns.
+ * @param zTerm A pointer one past the end of the buffer.
+ * @param c The 'unsigned int' to hold the resulting character value. Do not
+ * use a short or a char.
+ */
+# define READ_UTF8(zIn, zTerm, c) \
+ { \
+ c = *(zIn++); \
+ if (c >= 0xc0) { \
+ c = sqlite3Utf8Trans1[c - 0xc0]; \
+ while (zIn != zTerm && (*zIn & 0xc0) == 0x80) { \
+ c = (c << 6) + (0x3f & *(zIn++)); \
+ } \
+ if (c < 0x80 || (c & 0xFFFFF800) == 0xD800 || \
+ (c & 0xFFFFFFFE) == 0xFFFE) { \
+ c = 0xFFFD; \
+ } \
+ } \
+ }
+
+/* end of compatible block to complie codes */
+
+/*
+** Class derived from sqlite3_tokenizer
+*/
+typedef struct porter_tokenizer {
+ sqlite3_tokenizer base; /* Base class */
+} porter_tokenizer;
+
+/*
+** Class derived from sqlit3_tokenizer_cursor
+*/
+typedef struct porter_tokenizer_cursor {
+ sqlite3_tokenizer_cursor base;
+ const char* zInput; /* input we are tokenizing */
+ int nInput; /* size of the input */
+ int iOffset; /* current position in zInput */
+ int iToken; /* index of next token to be returned */
+ unsigned char* zToken; /* storage for current token */
+ int nAllocated; /* space allocated to zToken buffer */
+ /**
+ * Store the offset of the second character in the bi-gram pair that we just
+ * emitted so that we can consider it being the first character in a bi-gram
+ * pair.
+ * The value 0 indicates that there is no previous such character. This is
+ * an acceptable sentinel value because the 0th offset can never be the
+ * offset of the second in a bi-gram pair.
+ *
+ * For example, let us say we are tokenizing a string of 4 CJK characters
+ * represented by the byte-string "11223344" where each repeated digit
+ * indicates 2-bytes of storage used to encode the character in UTF-8.
+ * (It actually takes 3, btw.) Then on the passes to emit each token,
+ * the iOffset and iPrevGigramOffset values at entry will be:
+ *
+ * 1122: iOffset = 0, iPrevBigramOffset = 0
+ * 2233: iOffset = 4, iPrevBigramOffset = 2
+ * 3344: iOffset = 6, iPrevBigramOffset = 4
+ * (nothing will be emitted): iOffset = 8, iPrevBigramOffset = 6
+ */
+ int iPrevBigramOffset; /* previous result was bi-gram */
+} porter_tokenizer_cursor;
+
+/* Forward declaration */
+static const sqlite3_tokenizer_module porterTokenizerModule;
+
+/* from normalize.c */
+extern unsigned int normalize_character(const unsigned int c);
+
+/*
+** Create a new tokenizer instance.
+*/
+static int porterCreate(int argc, const char* const* argv,
+ sqlite3_tokenizer** ppTokenizer) {
+ porter_tokenizer* t;
+ t = (porter_tokenizer*)sqlite3_malloc(sizeof(*t));
+ if (t == NULL) return SQLITE_NOMEM;
+ memset(t, 0, sizeof(*t));
+ *ppTokenizer = &t->base;
+ return SQLITE_OK;
+}
+
+/*
+** Destroy a tokenizer
+*/
+static int porterDestroy(sqlite3_tokenizer* pTokenizer) {
+ sqlite3_free(pTokenizer);
+ return SQLITE_OK;
+}
+
+/*
+** Prepare to begin tokenizing a particular string. The input
+** string to be tokenized is zInput[0..nInput-1]. A cursor
+** used to incrementally tokenize this string is returned in
+** *ppCursor.
+*/
+static int porterOpen(
+ sqlite3_tokenizer* pTokenizer, /* The tokenizer */
+ const char* zInput, int nInput, /* String to be tokenized */
+ sqlite3_tokenizer_cursor** ppCursor /* OUT: Tokenization cursor */
+) {
+ porter_tokenizer_cursor* c;
+
+ c = (porter_tokenizer_cursor*)sqlite3_malloc(sizeof(*c));
+ if (c == NULL) return SQLITE_NOMEM;
+
+ c->zInput = zInput;
+ if (zInput == 0) {
+ c->nInput = 0;
+ } else if (nInput < 0) {
+ c->nInput = (int)strlen(zInput);
+ } else {
+ c->nInput = nInput;
+ }
+ c->iOffset = 0; /* start tokenizing at the beginning */
+ c->iToken = 0;
+ c->zToken = NULL; /* no space allocated, yet. */
+ c->nAllocated = 0;
+ c->iPrevBigramOffset = 0;
+
+ *ppCursor = &c->base;
+ return SQLITE_OK;
+}
+
+/*
+** Close a tokenization cursor previously opened by a call to
+** porterOpen() above.
+*/
+static int porterClose(sqlite3_tokenizer_cursor* pCursor) {
+ porter_tokenizer_cursor* c = (porter_tokenizer_cursor*)pCursor;
+ sqlite3_free(c->zToken);
+ sqlite3_free(c);
+ return SQLITE_OK;
+}
+/*
+** Vowel or consonant
+*/
+static const char cType[] = {0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1,
+ 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 2, 1};
+
+/*
+** isConsonant() and isVowel() determine if their first character in
+** the string they point to is a consonant or a vowel, according
+** to Porter ruls.
+**
+** A consonate is any letter other than 'a', 'e', 'i', 'o', or 'u'.
+** 'Y' is a consonant unless it follows another consonant,
+** in which case it is a vowel.
+**
+** In these routine, the letters are in reverse order. So the 'y' rule
+** is that 'y' is a consonant unless it is followed by another
+** consonant.
+*/
+static int isVowel(const char*);
+static int isConsonant(const char* z) {
+ int j;
+ char x = *z;
+ if (x == 0) return 0;
+ assert(x >= 'a' && x <= 'z');
+ j = cType[x - 'a'];
+ if (j < 2) return j;
+ return z[1] == 0 || isVowel(z + 1);
+}
+static int isVowel(const char* z) {
+ int j;
+ char x = *z;
+ if (x == 0) return 0;
+ assert(x >= 'a' && x <= 'z');
+ j = cType[x - 'a'];
+ if (j < 2) return 1 - j;
+ return isConsonant(z + 1);
+}
+
+/*
+** Let any sequence of one or more vowels be represented by V and let
+** C be sequence of one or more consonants. Then every word can be
+** represented as:
+**
+** [C] (VC){m} [V]
+**
+** In prose: A word is an optional consonant followed by zero or
+** vowel-consonant pairs followed by an optional vowel. "m" is the
+** number of vowel consonant pairs. This routine computes the value
+** of m for the first i bytes of a word.
+**
+** Return true if the m-value for z is 1 or more. In other words,
+** return true if z contains at least one vowel that is followed
+** by a consonant.
+**
+** In this routine z[] is in reverse order. So we are really looking
+** for an instance of of a consonant followed by a vowel.
+*/
+static int m_gt_0(const char* z) {
+ while (isVowel(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isConsonant(z)) {
+ z++;
+ }
+ return *z != 0;
+}
+
+/* Like mgt0 above except we are looking for a value of m which is
+** exactly 1
+*/
+static int m_eq_1(const char* z) {
+ while (isVowel(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isConsonant(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isVowel(z)) {
+ z++;
+ }
+ if (*z == 0) return 1;
+ while (isConsonant(z)) {
+ z++;
+ }
+ return *z == 0;
+}
+
+/* Like mgt0 above except we are looking for a value of m>1 instead
+** or m>0
+*/
+static int m_gt_1(const char* z) {
+ while (isVowel(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isConsonant(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isVowel(z)) {
+ z++;
+ }
+ if (*z == 0) return 0;
+ while (isConsonant(z)) {
+ z++;
+ }
+ return *z != 0;
+}
+
+/*
+** Return TRUE if there is a vowel anywhere within z[0..n-1]
+*/
+static int hasVowel(const char* z) {
+ while (isConsonant(z)) {
+ z++;
+ }
+ return *z != 0;
+}
+
+/*
+** Return TRUE if the word ends in a double consonant.
+**
+** The text is reversed here. So we are really looking at
+** the first two characters of z[].
+*/
+static int doubleConsonant(const char* z) {
+ return isConsonant(z) && z[0] == z[1] && isConsonant(z + 1);
+}
+
+/*
+** Return TRUE if the word ends with three letters which
+** are consonant-vowel-consonent and where the final consonant
+** is not 'w', 'x', or 'y'.
+**
+** The word is reversed here. So we are really checking the
+** first three letters and the first one cannot be in [wxy].
+*/
+static int star_oh(const char* z) {
+ return z[0] != 0 && isConsonant(z) && z[0] != 'w' && z[0] != 'x' &&
+ z[0] != 'y' && z[1] != 0 && isVowel(z + 1) && z[2] != 0 &&
+ isConsonant(z + 2);
+}
+
+/*
+** If the word ends with zFrom and xCond() is true for the stem
+** of the word that precedes the zFrom ending, then change the
+** ending to zTo.
+**
+** The input word *pz and zFrom are both in reverse order. zTo
+** is in normal order.
+**
+** Return TRUE if zFrom matches. Return FALSE if zFrom does not
+** match. Not that TRUE is returned even if xCond() fails and
+** no substitution occurs.
+*/
+static int stem(
+ char** pz, /* The word being stemmed (Reversed) */
+ const char* zFrom, /* If the ending matches this... (Reversed) */
+ const char* zTo, /* ... change the ending to this (not reversed) */
+ int (*xCond)(const char*) /* Condition that must be true */
+) {
+ char* z = *pz;
+ while (*zFrom && *zFrom == *z) {
+ z++;
+ zFrom++;
+ }
+ if (*zFrom != 0) return 0;
+ if (xCond && !xCond(z)) return 1;
+ while (*zTo) {
+ *(--z) = *(zTo++);
+ }
+ *pz = z;
+ return 1;
+}
+
+/**
+ * Voiced sound mark is only on Japanese. It is like accent. It combines with
+ * previous character. Example, "サ" (Katakana) with "゛" (voiced sound mark)
+ * is "ザ". Although full-width character mapping has combined character like
+ * "ザ", there is no combined character on half-width Katanaka character
+ * mapping.
+ */
+static int isVoicedSoundMark(const unsigned int c) {
+ if (c == 0xff9e || c == 0xff9f || c == 0x3099 || c == 0x309a) return 1;
+ return 0;
+}
+
+/**
+ * How many unicode characters to take from the front and back of a term in
+ * |copy_stemmer|.
+ */
+# define COPY_STEMMER_COPY_HALF_LEN 10
+
+/**
+ * Normalizing but non-stemming term copying.
+ *
+ * The original function would take 10 bytes from the front and 10 bytes from
+ * the back if there were no digits in the string and it was more than 20
+ * bytes long. If there were digits involved that would decrease to 3 bytes
+ * from the front and 3 from the back. This would potentially corrupt utf-8
+ * encoded characters, which is fine from the perspective of the FTS3 logic.
+ *
+ * In our revised form we now operate on a unicode character basis rather than
+ * a byte basis. Additionally we use the same length limit even if there are
+ * digits involved because it's not clear digit token-space reduction is saving
+ * us from anything and could be hurting. Specifically, if no one is ever
+ * going to search on things with digits, then we should just remove them.
+ * Right now, the space reduction is going to increase false positives when
+ * people do search on them and increase the number of collisions sufficiently
+ * to make it really expensive. The caveat is there will be some increase in
+ * index size which could be meaningful if people are receiving lots of emails
+ * full of distinct numbers.
+ *
+ * In order to do the copy-from-the-front and copy-from-the-back trick, once
+ * we reach N characters in, we set zFrontEnd to the current value of zOut
+ * (which represents the termination of the first part of the result string)
+ * and set zBackStart to the value of zOutStart. We then advanced zBackStart
+ * along a character at a time as we write more characters. Once we have
+ * traversed the entire string, if zBackStart > zFrontEnd, then we know
+ * the string should be shrunk using the characters in the two ranges.
+ *
+ * (It would be faster to scan from the back with specialized logic but that
+ * particular logic seems easy to screw up and we don't have unit tests in here
+ * to the extent required.)
+ *
+ * @param zIn Input string to normalize and potentially shrink.
+ * @param nBytesIn The number of bytes in zIn, distinct from the number of
+ * unicode characters encoded in zIn.
+ * @param zOut The string to write our output into. This must have at least
+ * nBytesIn * MAX_UTF8_GROWTH_FACTOR in order to compensate for
+ * normalization that results in a larger utf-8 encoding.
+ * @param pnBytesOut Integer to write the number of bytes in zOut into.
+ */
+static void copy_stemmer(const unsigned char* zIn, const int nBytesIn,
+ unsigned char* zOut, int* pnBytesOut) {
+ const unsigned char* zInTerm = zIn + nBytesIn;
+ unsigned char* zOutStart = zOut;
+ unsigned int c;
+ unsigned int charCount = 0;
+ unsigned char *zFrontEnd = NULL, *zBackStart = NULL;
+ unsigned int trashC;
+
+ /* copy normalized character */
+ while (zIn < zInTerm) {
+ READ_UTF8(zIn, zInTerm, c);
+ c = normalize_character(c);
+
+ /* ignore voiced/semi-voiced sound mark */
+ if (!isVoicedSoundMark(c)) {
+ /* advance one non-voiced sound mark character. */
+ if (zBackStart) READ_UTF8(zBackStart, zOut, trashC);
+
+ WRITE_UTF8(zOut, c);
+ charCount++;
+ if (charCount == COPY_STEMMER_COPY_HALF_LEN) {
+ zFrontEnd = zOut;
+ zBackStart = zOutStart;
+ }
+ }
+ }
+
+ /* if we need to shrink the string, transplant the back bytes */
+ if (zBackStart > zFrontEnd) { /* this handles when both are null too */
+ size_t backBytes = zOut - zBackStart;
+ memmove(zFrontEnd, zBackStart, backBytes);
+ zOut = zFrontEnd + backBytes;
+ }
+ *zOut = 0;
+ *pnBytesOut = zOut - zOutStart;
+}
+
+/*
+** Stem the input word zIn[0..nIn-1]. Store the output in zOut.
+** zOut is at least big enough to hold nIn bytes. Write the actual
+** size of the output word (exclusive of the '\0' terminator) into *pnOut.
+**
+** Any upper-case characters in the US-ASCII character set ([A-Z])
+** are converted to lower case. Upper-case UTF characters are
+** unchanged.
+**
+** Words that are longer than about 20 bytes are stemmed by retaining
+** a few bytes from the beginning and the end of the word. If the
+** word contains digits, 3 bytes are taken from the beginning and
+** 3 bytes from the end. For long words without digits, 10 bytes
+** are taken from each end. US-ASCII case folding still applies.
+**
+** If the input word contains not digits but does characters not
+** in [a-zA-Z] then no stemming is attempted and this routine just
+** copies the input into the input into the output with US-ASCII
+** case folding.
+**
+** Stemming never increases the length of the word. So there is
+** no chance of overflowing the zOut buffer.
+*/
+static void porter_stemmer(const unsigned char* zIn, unsigned int nIn,
+ unsigned char* zOut, int* pnOut) {
+ unsigned int i, j, c;
+ char zReverse[28];
+ char *z, *z2;
+ const unsigned char* zTerm = zIn + nIn;
+ const unsigned char* zTmp = zIn;
+
+ if (nIn < 3 || nIn >= sizeof(zReverse) - 7) {
+ /* The word is too big or too small for the porter stemmer.
+ ** Fallback to the copy stemmer */
+ copy_stemmer(zIn, nIn, zOut, pnOut);
+ return;
+ }
+ for (j = sizeof(zReverse) - 6; zTmp < zTerm; j--) {
+ READ_UTF8(zTmp, zTerm, c);
+ c = normalize_character(c);
+ if (c >= 'a' && c <= 'z') {
+ zReverse[j] = c;
+ } else {
+ /* The use of a character not in [a-zA-Z] means that we fallback
+ ** to the copy stemmer */
+ copy_stemmer(zIn, nIn, zOut, pnOut);
+ return;
+ }
+ }
+ memset(&zReverse[sizeof(zReverse) - 5], 0, 5);
+ z = &zReverse[j + 1];
+
+ /* Step 1a */
+ if (z[0] == 's') {
+ if (!stem(&z, "sess", "ss", 0) && !stem(&z, "sei", "i", 0) &&
+ !stem(&z, "ss", "ss", 0)) {
+ z++;
+ }
+ }
+
+ /* Step 1b */
+ z2 = z;
+ if (stem(&z, "dee", "ee", m_gt_0)) {
+ /* Do nothing. The work was all in the test */
+ } else if ((stem(&z, "gni", "", hasVowel) || stem(&z, "de", "", hasVowel)) &&
+ z != z2) {
+ if (stem(&z, "ta", "ate", 0) || stem(&z, "lb", "ble", 0) ||
+ stem(&z, "zi", "ize", 0)) {
+ /* Do nothing. The work was all in the test */
+ } else if (doubleConsonant(z) && (*z != 'l' && *z != 's' && *z != 'z')) {
+ z++;
+ } else if (m_eq_1(z) && star_oh(z)) {
+ *(--z) = 'e';
+ }
+ }
+
+ /* Step 1c */
+ if (z[0] == 'y' && hasVowel(z + 1)) {
+ z[0] = 'i';
+ }
+
+ /* Step 2 */
+ switch (z[1]) {
+ case 'a':
+ (void)(stem(&z, "lanoita", "ate", m_gt_0) ||
+ stem(&z, "lanoit", "tion", m_gt_0));
+ break;
+ case 'c':
+ (void)(stem(&z, "icne", "ence", m_gt_0) ||
+ stem(&z, "icna", "ance", m_gt_0));
+ break;
+ case 'e':
+ (void)(stem(&z, "rezi", "ize", m_gt_0));
+ break;
+ case 'g':
+ (void)(stem(&z, "igol", "log", m_gt_0));
+ break;
+ case 'l':
+ (void)(stem(&z, "ilb", "ble", m_gt_0) || stem(&z, "illa", "al", m_gt_0) ||
+ stem(&z, "iltne", "ent", m_gt_0) || stem(&z, "ile", "e", m_gt_0) ||
+ stem(&z, "ilsuo", "ous", m_gt_0));
+ break;
+ case 'o':
+ (void)(stem(&z, "noitazi", "ize", m_gt_0) ||
+ stem(&z, "noita", "ate", m_gt_0) ||
+ stem(&z, "rota", "ate", m_gt_0));
+ break;
+ case 's':
+ (void)(stem(&z, "msila", "al", m_gt_0) ||
+ stem(&z, "ssenevi", "ive", m_gt_0) ||
+ stem(&z, "ssenluf", "ful", m_gt_0) ||
+ stem(&z, "ssensuo", "ous", m_gt_0));
+ break;
+ case 't':
+ (void)(stem(&z, "itila", "al", m_gt_0) ||
+ stem(&z, "itivi", "ive", m_gt_0) ||
+ stem(&z, "itilib", "ble", m_gt_0));
+ break;
+ }
+
+ /* Step 3 */
+ switch (z[0]) {
+ case 'e':
+ (void)(stem(&z, "etaci", "ic", m_gt_0) || stem(&z, "evita", "", m_gt_0) ||
+ stem(&z, "ezila", "al", m_gt_0));
+ break;
+ case 'i':
+ (void)(stem(&z, "itici", "ic", m_gt_0));
+ break;
+ case 'l':
+ (void)(stem(&z, "laci", "ic", m_gt_0) || stem(&z, "luf", "", m_gt_0));
+ break;
+ case 's':
+ (void)(stem(&z, "ssen", "", m_gt_0));
+ break;
+ }
+
+ /* Step 4 */
+ switch (z[1]) {
+ case 'a':
+ if (z[0] == 'l' && m_gt_1(z + 2)) {
+ z += 2;
+ }
+ break;
+ case 'c':
+ if (z[0] == 'e' && z[2] == 'n' && (z[3] == 'a' || z[3] == 'e') &&
+ m_gt_1(z + 4)) {
+ z += 4;
+ }
+ break;
+ case 'e':
+ if (z[0] == 'r' && m_gt_1(z + 2)) {
+ z += 2;
+ }
+ break;
+ case 'i':
+ if (z[0] == 'c' && m_gt_1(z + 2)) {
+ z += 2;
+ }
+ break;
+ case 'l':
+ if (z[0] == 'e' && z[2] == 'b' && (z[3] == 'a' || z[3] == 'i') &&
+ m_gt_1(z + 4)) {
+ z += 4;
+ }
+ break;
+ case 'n':
+ if (z[0] == 't') {
+ if (z[2] == 'a') {
+ if (m_gt_1(z + 3)) {
+ z += 3;
+ }
+ } else if (z[2] == 'e') {
+ (void)(stem(&z, "tneme", "", m_gt_1) ||
+ stem(&z, "tnem", "", m_gt_1) || stem(&z, "tne", "", m_gt_1));
+ }
+ }
+ break;
+ case 'o':
+ if (z[0] == 'u') {
+ if (m_gt_1(z + 2)) {
+ z += 2;
+ }
+ } else if (z[3] == 's' || z[3] == 't') {
+ (void)(stem(&z, "noi", "", m_gt_1));
+ }
+ break;
+ case 's':
+ if (z[0] == 'm' && z[2] == 'i' && m_gt_1(z + 3)) {
+ z += 3;
+ }
+ break;
+ case 't':
+ (void)(stem(&z, "eta", "", m_gt_1) || stem(&z, "iti", "", m_gt_1));
+ break;
+ case 'u':
+ if (z[0] == 's' && z[2] == 'o' && m_gt_1(z + 3)) {
+ z += 3;
+ }
+ break;
+ case 'v':
+ case 'z':
+ if (z[0] == 'e' && z[2] == 'i' && m_gt_1(z + 3)) {
+ z += 3;
+ }
+ break;
+ }
+
+ /* Step 5a */
+ if (z[0] == 'e') {
+ if (m_gt_1(z + 1)) {
+ z++;
+ } else if (m_eq_1(z + 1) && !star_oh(z + 1)) {
+ z++;
+ }
+ }
+
+ /* Step 5b */
+ if (m_gt_1(z) && z[0] == 'l' && z[1] == 'l') {
+ z++;
+ }
+
+ /* z[] is now the stemmed word in reverse order. Flip it back
+ ** around into forward order and return.
+ */
+ *pnOut = i = strlen(z);
+ zOut[i] = 0;
+ while (*z) {
+ zOut[--i] = *(z++);
+ }
+}
+
+/**
+ * Indicate whether characters in the 0x30 - 0x7f region can be part of a token.
+ * Letters and numbers can; punctuation (and 'del') can't.
+ */
+static const char porterIdChar[] = {
+ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, /* 3x */
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 4x */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, /* 5x */
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 6x */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, /* 7x */
+};
+
+/**
+ * Test whether a character is a (non-ascii) space character or not. isDelim
+ * uses the existing porter stemmer logic for anything in the ASCII (< 0x80)
+ * space which covers 0x20.
+ *
+ * 0x2000-0x206F is the general punctuation table. 0x2000 - 0x200b are spaces.
+ * The spaces 0x2000 - 0x200a are all defined as roughly equivalent to a
+ * standard 0x20 space. 0x200b is a "zero width space" (ZWSP) and not like an
+ * 0x20 space. 0x202f is a narrow no-break space and roughly equivalent to an
+ * 0x20 space. 0x205f is a "medium mathematical space" and defined as roughly
+ * equivalent to an 0x20 space.
+ */
+# define IS_UNI_SPACE(x) \
+ (((x) >= 0x2000 && (x) <= 0x200a) || (x) == 0x202f || (x) == 0x205f)
+/**
+ * What we are checking for:
+ * - 0x3001: Ideographic comma (-> 0x2c ',')
+ * - 0x3002: Ideographic full stop (-> 0x2e '.')
+ * - 0xff0c: fullwidth comma (~ wide 0x2c ',')
+ * - 0xff0e: fullwidth full stop (~ wide 0x2e '.')
+ * - 0xff61: halfwidth ideographic full stop (~ narrow 0x3002)
+ * - 0xff64: halfwidth ideographic comma (~ narrow 0x3001)
+ *
+ * It is possible we should be treating other things as delimiters!
+ */
+# define IS_JA_DELIM(x) \
+ (((x) == 0x3001) || ((x) == 0xFF64) || ((x) == 0xFF0E) || \
+ ((x) == 0x3002) || ((x) == 0xFF61) || ((x) == 0xFF0C))
+
+/**
+ * The previous character was a delimiter (which includes the start of the
+ * string).
+ */
+# define BIGRAM_RESET 0
+/**
+ * The previous character was a CJK character and we have only seen one of them.
+ * If we had seen more than one in a row it would be the BIGRAM_USE state.
+ */
+# define BIGRAM_UNKNOWN 1
+/**
+ * We have seen two or more CJK characters in a row.
+ */
+# define BIGRAM_USE 2
+/**
+ * The previous character was ASCII or something in the unicode general scripts
+ * area that we do not believe is a delimiter. We call it 'alpha' as in
+ * alphabetic/alphanumeric and something that should be tokenized based on
+ * delimiters rather than on a bi-gram basis.
+ */
+# define BIGRAM_ALPHA 3
+
+static int isDelim(
+ const unsigned char* zCur, /* IN: current pointer of token */
+ const unsigned char* zTerm, /* IN: one character beyond end of token */
+ int* len, /* OUT: analyzed bytes in this token */
+ int* state /* IN/OUT: analyze state */
+) {
+ const unsigned char* zIn = zCur;
+ unsigned int c;
+ int delim;
+
+ /* get the unicode character to analyze */
+ READ_UTF8(zIn, zTerm, c);
+ c = normalize_character(c);
+ *len = zIn - zCur;
+
+ /* ASCII character range has rule */
+ if (c < 0x80) {
+ // This is original porter stemmer isDelim logic.
+ // 0x0 - 0x1f are all control characters, 0x20 is space, 0x21-0x2f are
+ // punctuation.
+ delim = (c < 0x30 || !porterIdChar[c - 0x30]);
+ // cases: "&a", "&."
+ if (*state == BIGRAM_USE || *state == BIGRAM_UNKNOWN) {
+ /* previous maybe CJK and current is ascii */
+ *state = BIGRAM_ALPHA; /*ascii*/
+ delim = 1; /* must break */
+ } else if (delim == 1) {
+ // cases: "a.", ".."
+ /* this is delimiter character */
+ *state = BIGRAM_RESET; /*reset*/
+ } else {
+ // cases: "aa", ".a"
+ *state = BIGRAM_ALPHA; /*ascii*/
+ }
+ return delim;
+ }
+
+ // (at this point we must be a non-ASCII character)
+
+ /* voiced/semi-voiced sound mark is ignore */
+ if (isVoicedSoundMark(c) && *state != BIGRAM_ALPHA) {
+ /* ignore this because it is combined with previous char */
+ return 0;
+ }
+
+ /* this isn't CJK range, so return as no delim */
+ // Anything less than 0x2000 (except to U+0E00-U+0EFF and U+1780-U+17FF)
+ // is the general scripts area and should not be bi-gram indexed.
+ // 0xa000 - 0a4cf is the Yi area. It is apparently a phonetic language whose
+ // usage does not appear to have simple delimiter rules, so we're leaving it
+ // as bigram processed. This is a guess, if you know better, let us know.
+ // (We previously bailed on this range too.)
+ // Addition, U+0E00-U+0E7F is Thai, U+0E80-U+0EFF is Laos,
+ // and U+1780-U+17FF is Khmer. It is no easy way to break each word.
+ // So these should use bi-gram too.
+ // cases: "aa", ".a", "&a"
+ if (c < 0xe00 || (c >= 0xf00 && c < 0x1780) || (c >= 0x1800 && c < 0x2000)) {
+ *state = BIGRAM_ALPHA; /* not really ASCII but same idea; tokenize it */
+ return 0;
+ }
+
+ // (at this point we must be a bi-grammable char or delimiter)
+
+ /* this is space character or delim character */
+ // cases: "a.", "..", "&."
+ if (IS_UNI_SPACE(c) || IS_JA_DELIM(c)) {
+ *state = BIGRAM_RESET; /* reset */
+ return 1; /* it actually is a delimiter; report as such */
+ }
+
+ // (at this point we must be a bi-grammable char)
+
+ // cases: "a&"
+ if (*state == BIGRAM_ALPHA) {
+ /* Previous is ascii and current maybe CJK */
+ *state = BIGRAM_UNKNOWN; /* mark as unknown */
+ return 1; /* break to emit the ASCII token*/
+ }
+
+ /* We have no rule for CJK!. use bi-gram */
+ // cases: "&&"
+ if (*state == BIGRAM_UNKNOWN || *state == BIGRAM_USE) {
+ /* previous state is unknown. mark as bi-gram */
+ *state = BIGRAM_USE;
+ return 1; /* break to emit the digram */
+ }
+
+ // cases: ".&" (*state == BIGRAM_RESET)
+ *state = BIGRAM_UNKNOWN; /* mark as unknown */
+ return 0; /* no need to break; nothing to emit */
+}
+
+/**
+ * Generate a new token. There are basically three types of token we can
+ * generate:
+ * - A porter stemmed token. This is a word entirely comprised of ASCII
+ * characters. We run the porter stemmer algorithm against the word.
+ * Because we have no way to know what is and is not an English word
+ * (the only language for which the porter stemmer was designed), this
+ * could theoretically map multiple words that are not variations of the
+ * same word down to the same root, resulting in potentially unexpected
+ * result inclusions in the search results. We accept this result because
+ * there's not a lot we can do about it and false positives are much
+ * better than false negatives.
+ * - A copied token; case/accent-folded but not stemmed. We call the porter
+ * stemmer for all non-CJK cases and it diverts to the copy stemmer if it
+ * sees any non-ASCII characters (after folding) or if the string is too
+ * long. The copy stemmer will shrink the string if it is deemed too long.
+ * - A bi-gram token; two CJK-ish characters. For query reasons we generate a
+ * series of overlapping bi-grams. (We can't require the user to start their
+ * search based on the arbitrary context of the indexed documents.)
+ *
+ * It may be useful to think of this function as operating at the points between
+ * characters. While we are considering the 'current' character (the one after
+ * the 'point'), we are also interested in the 'previous' character (the one
+ * preceding the point).
+ * At any 'point', there are a number of possible situations which I will
+ * illustrate with pairs of characters. 'a' means alphanumeric ASCII or a
+ * non-ASCII character that is not bi-grammable or a delimiter, '.'
+ * means a delimiter (space or punctuation), '&' means a bi-grammable
+ * character.
+ * - aa: We are in the midst of a token. State remains BIGRAM_ALPHA.
+ * - a.: We will generate a porter stemmed or copied token. State was
+ * BIGRAM_ALPHA, gets set to BIGRAM_RESET.
+ * - a&: We will generate a porter stemmed or copied token; we will set our
+ * state to BIGRAM_UNKNOWN to indicate we have seen one bigram character
+ * but that it is not yet time to emit a bigram.
+ * - .a: We are starting a token. State was BIGRAM_RESET, gets set to
+ * BIGRAM_ALPHA.
+ * - ..: We skip/eat the delimiters. State stays BIGRAM_RESET.
+ * - .&: State set to BIGRAM_UNKNOWN to indicate we have seen one bigram char.
+ * - &a: If the state was BIGRAM_USE, we generate a bi-gram token. If the state
+ * was BIGRAM_UNKNOWN we had only seen one CJK character and so don't do
+ * anything. State is set to BIGRAM_ALPHA.
+ * - &.: Same as the "&a" case, but state is set to BIGRAM_RESET.
+ * - &&: We will generate a bi-gram token. State was either BIGRAM_UNKNOWN or
+ * BIGRAM_USE, gets set to BIGRAM_USE.
+ */
+static int porterNext(
+ sqlite3_tokenizer_cursor* pCursor, /* Cursor returned by porterOpen */
+ const char** pzToken, /* OUT: *pzToken is the token text */
+ int* pnBytes, /* OUT: Number of bytes in token */
+ int* piStartOffset, /* OUT: Starting offset of token */
+ int* piEndOffset, /* OUT: Ending offset of token */
+ int* piPosition /* OUT: Position integer of token */
+) {
+ porter_tokenizer_cursor* c = (porter_tokenizer_cursor*)pCursor;
+ const unsigned char* z = (unsigned char*)c->zInput;
+ int len = 0;
+ int state;
+
+ while (c->iOffset < c->nInput) {
+ int iStartOffset, numChars;
+
+ /*
+ * This loop basically has two modes of operation:
+ * - general processing (iPrevBigramOffset == 0 here)
+ * - CJK processing (iPrevBigramOffset != 0 here)
+ *
+ * In an general processing pass we skip over all the delimiters, leaving us
+ * at a character that promises to produce a token. This could be a CJK
+ * token (state == BIGRAM_USE) or an ALPHA token (state == BIGRAM_ALPHA).
+ * If it was a CJK token, we transition into CJK state for the next loop.
+ * If it was an alpha token, our current offset is pointing at a delimiter
+ * (which could be a CJK character), so it is good that our next pass
+ * through the function and loop will skip over any delimiters. If the
+ * delimiter we hit was a CJK character, the next time through we will
+ * not treat it as a delimiter though; the entry state for that scan is
+ * BIGRAM_RESET so the transition is not treated as a delimiter!
+ *
+ * The CJK pass always starts with the second character in a bi-gram emitted
+ * as a token in the previous step. No delimiter skipping is required
+ * because we know that first character might produce a token for us. It
+ * only 'might' produce a token because the previous pass performed no
+ * lookahead and cannot be sure it is followed by another CJK character.
+ * This is why
+ */
+
+ // If we have a previous bigram offset
+ if (c->iPrevBigramOffset == 0) {
+ /* Scan past delimiter characters */
+ state = BIGRAM_RESET; /* reset */
+ while (c->iOffset < c->nInput &&
+ isDelim(z + c->iOffset, z + c->nInput, &len, &state)) {
+ c->iOffset += len;
+ }
+
+ } else {
+ /* for bigram indexing, use previous offset */
+ c->iOffset = c->iPrevBigramOffset;
+ }
+
+ /* Count non-delimiter characters. */
+ iStartOffset = c->iOffset;
+ numChars = 0;
+
+ // Start from a reset state. This means the first character we see
+ // (which will not be a delimiter) determines which of ALPHA or CJK modes
+ // we are operating in. (It won't be a delimiter because in a 'general'
+ // pass as defined above, we will have eaten all the delimiters, and in
+ // a CJK pass we are guaranteed that the first character is CJK.)
+ state = BIGRAM_RESET; /* state is reset */
+ // Advance until it is time to emit a token.
+ // For ALPHA characters, this means advancing until we encounter a delimiter
+ // or a CJK character. iOffset will be pointing at the delimiter or CJK
+ // character, aka one beyond the last ALPHA character.
+ // For CJK characters this means advancing until we encounter an ALPHA
+ // character, a delimiter, or we have seen two consecutive CJK
+ // characters. iOffset points at the ALPHA/delimiter in the first 2 cases
+ // and the second of two CJK characters in the last case.
+ // Because of the way this loop is structured, iOffset is only updated
+ // when we don't terminate. However, if we terminate, len still contains
+ // the number of bytes in the character found at iOffset. (This is useful
+ // in the CJK case.)
+ while (c->iOffset < c->nInput &&
+ !isDelim(z + c->iOffset, z + c->nInput, &len, &state)) {
+ c->iOffset += len;
+ numChars++;
+ }
+
+ if (state == BIGRAM_USE) {
+ /* Split word by bigram */
+ // Right now iOffset is pointing at the second character in a pair.
+ // Save this offset so next-time through we start with that as the
+ // first character.
+ c->iPrevBigramOffset = c->iOffset;
+ // And now advance so that iOffset is pointing at the character after
+ // the second character in the bi-gram pair. Also count the char.
+ c->iOffset += len;
+ numChars++;
+ } else {
+ /* Reset bigram offset */
+ c->iPrevBigramOffset = 0;
+ }
+
+ /* We emit a token if:
+ * - there are two ideograms together,
+ * - there are three chars or more,
+ * - we think this is a query and wildcard magic is desired.
+ * We think is a wildcard query when we have a single character, it starts
+ * at the start of the buffer, it's CJK, our current offset is one shy of
+ * nInput and the character at iOffset is '*'. Because the state gets
+ * clobbered by the incidence of '*' our requirement for CJK is that the
+ * implied character length is at least 3 given that it takes at least 3
+ * bytes to encode to 0x2000.
+ */
+ // It is possible we have no token to emit here if iPrevBigramOffset was not
+ // 0 on entry and there was no second CJK character. iPrevBigramOffset
+ // will now be 0 if that is the case (and c->iOffset == iStartOffset).
+ if ( // allow two-character words only if in bigram
+ (numChars == 2 && state == BIGRAM_USE) ||
+ // otherwise, drop two-letter words (considered stop-words)
+ (numChars >= 3) ||
+ // wildcard case:
+ (numChars == 1 && iStartOffset == 0 && (c->iOffset >= 3) &&
+ (c->iOffset == c->nInput - 1) && (z[c->iOffset] == '*'))) {
+ /* figure out the number of bytes to copy/stem */
+ int n = c->iOffset - iStartOffset;
+ /* make sure there is enough buffer space */
+ if (n * MAX_UTF8_GROWTH_FACTOR > c->nAllocated) {
+ c->nAllocated = n * MAX_UTF8_GROWTH_FACTOR + 20;
+ c->zToken = sqlite3_realloc(c->zToken, c->nAllocated);
+ if (c->zToken == NULL) return SQLITE_NOMEM;
+ }
+
+ if (state == BIGRAM_USE) {
+ /* This is by bigram. So it is unnecessary to convert word */
+ copy_stemmer(&z[iStartOffset], n, c->zToken, pnBytes);
+ } else {
+ porter_stemmer(&z[iStartOffset], n, c->zToken, pnBytes);
+ }
+ *pzToken = (const char*)c->zToken;
+ *piStartOffset = iStartOffset;
+ *piEndOffset = c->iOffset;
+ *piPosition = c->iToken++;
+ return SQLITE_OK;
+ }
+ }
+ return SQLITE_DONE;
+}
+
+/*
+** The set of routines that implement the porter-stemmer tokenizer
+*/
+static const sqlite3_tokenizer_module porterTokenizerModule = {
+ 0, porterCreate, porterDestroy, porterOpen, porterClose, porterNext,
+};
+
+/*
+** Allocate a new porter tokenizer. Return a pointer to the new
+** tokenizer in *ppModule
+*/
+void sqlite3Fts3PorterTokenizerModule(
+ sqlite3_tokenizer_module const** ppModule) {
+ *ppModule = &porterTokenizerModule;
+}
+
+#endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3) */
diff --git a/comm/mailnews/extensions/fts3/fts3_tokenizer.h b/comm/mailnews/extensions/fts3/fts3_tokenizer.h
new file mode 100644
index 0000000000..41973483a3
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/fts3_tokenizer.h
@@ -0,0 +1,146 @@
+/*
+** 2006 July 10
+**
+** The author disclaims copyright to this source code.
+**
+*************************************************************************
+** Defines the interface to tokenizers used by fulltext-search. There
+** are three basic components:
+**
+** sqlite3_tokenizer_module is a singleton defining the tokenizer
+** interface functions. This is essentially the class structure for
+** tokenizers.
+**
+** sqlite3_tokenizer is used to define a particular tokenizer, perhaps
+** including customization information defined at creation time.
+**
+** sqlite3_tokenizer_cursor is generated by a tokenizer to generate
+** tokens from a particular input.
+*/
+#ifndef _FTS3_TOKENIZER_H_
+#define _FTS3_TOKENIZER_H_
+
+/* TODO(shess) Only used for SQLITE_OK and SQLITE_DONE at this time.
+** If tokenizers are to be allowed to call sqlite3_*() functions, then
+** we will need a way to register the API consistently.
+*/
+#include "sqlite3.h"
+
+/*
+** Structures used by the tokenizer interface. When a new tokenizer
+** implementation is registered, the caller provides a pointer to
+** an sqlite3_tokenizer_module containing pointers to the callback
+** functions that make up an implementation.
+**
+** When an fts3 table is created, it passes any arguments passed to
+** the tokenizer clause of the CREATE VIRTUAL TABLE statement to the
+** sqlite3_tokenizer_module.xCreate() function of the requested tokenizer
+** implementation. The xCreate() function in turn returns an
+** sqlite3_tokenizer structure representing the specific tokenizer to
+** be used for the fts3 table (customized by the tokenizer clause arguments).
+**
+** To tokenize an input buffer, the sqlite3_tokenizer_module.xOpen()
+** method is called. It returns an sqlite3_tokenizer_cursor object
+** that may be used to tokenize a specific input buffer based on
+** the tokenization rules supplied by a specific sqlite3_tokenizer
+** object.
+*/
+typedef struct sqlite3_tokenizer_module sqlite3_tokenizer_module;
+typedef struct sqlite3_tokenizer sqlite3_tokenizer;
+typedef struct sqlite3_tokenizer_cursor sqlite3_tokenizer_cursor;
+
+struct sqlite3_tokenizer_module {
+ /*
+ ** Structure version. Should always be set to 0.
+ */
+ int iVersion;
+
+ /*
+ ** Create a new tokenizer. The values in the argv[] array are the
+ ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL
+ ** TABLE statement that created the fts3 table. For example, if
+ ** the following SQL is executed:
+ **
+ ** CREATE .. USING fts3( ... , tokenizer <tokenizer-name> arg1 arg2)
+ **
+ ** then argc is set to 2, and the argv[] array contains pointers
+ ** to the strings "arg1" and "arg2".
+ **
+ ** This method should return either SQLITE_OK (0), or an SQLite error
+ ** code. If SQLITE_OK is returned, then *ppTokenizer should be set
+ ** to point at the newly created tokenizer structure. The generic
+ ** sqlite3_tokenizer.pModule variable should not be initialised by
+ ** this callback. The caller will do so.
+ */
+ int (*xCreate)(int argc, /* Size of argv array */
+ const char* const* argv, /* Tokenizer argument strings */
+ sqlite3_tokenizer** ppTokenizer /* OUT: Created tokenizer */
+ );
+
+ /*
+ ** Destroy an existing tokenizer. The fts3 module calls this method
+ ** exactly once for each successful call to xCreate().
+ */
+ int (*xDestroy)(sqlite3_tokenizer* pTokenizer);
+
+ /*
+ ** Create a tokenizer cursor to tokenize an input buffer. The caller
+ ** is responsible for ensuring that the input buffer remains valid
+ ** until the cursor is closed (using the xClose() method).
+ */
+ int (*xOpen)(
+ sqlite3_tokenizer* pTokenizer, /* Tokenizer object */
+ const char* pInput, int nBytes, /* Input buffer */
+ sqlite3_tokenizer_cursor** ppCursor /* OUT: Created tokenizer cursor */
+ );
+
+ /*
+ ** Destroy an existing tokenizer cursor. The fts3 module calls this
+ ** method exactly once for each successful call to xOpen().
+ */
+ int (*xClose)(sqlite3_tokenizer_cursor* pCursor);
+
+ /*
+ ** Retrieve the next token from the tokenizer cursor pCursor. This
+ ** method should either return SQLITE_OK and set the values of the
+ ** "OUT" variables identified below, or SQLITE_DONE to indicate that
+ ** the end of the buffer has been reached, or an SQLite error code.
+ **
+ ** *ppToken should be set to point at a buffer containing the
+ ** normalized version of the token (i.e. after any case-folding and/or
+ ** stemming has been performed). *pnBytes should be set to the length
+ ** of this buffer in bytes. The input text that generated the token is
+ ** identified by the byte offsets returned in *piStartOffset and
+ ** *piEndOffset. *piStartOffset should be set to the index of the first
+ ** byte of the token in the input buffer. *piEndOffset should be set
+ ** to the index of the first byte just past the end of the token in
+ ** the input buffer.
+ **
+ ** The buffer *ppToken is set to point at is managed by the tokenizer
+ ** implementation. It is only required to be valid until the next call
+ ** to xNext() or xClose().
+ */
+ /* TODO(shess) current implementation requires pInput to be
+ ** nul-terminated. This should either be fixed, or pInput/nBytes
+ ** should be converted to zInput.
+ */
+ int (*xNext)(
+ sqlite3_tokenizer_cursor* pCursor, /* Tokenizer cursor */
+ const char** ppToken, int* pnBytes, /* OUT: Normalized text for token */
+ int* piStartOffset, /* OUT: Byte offset of token in input buffer */
+ int* piEndOffset, /* OUT: Byte offset of end of token in input buffer */
+ int* piPosition /* OUT: Number of tokens returned before this one */
+ );
+};
+
+struct sqlite3_tokenizer {
+ const sqlite3_tokenizer_module* pModule; /* The module for this tokenizer */
+ /* Tokenizer implementations will typically add additional fields */
+};
+
+struct sqlite3_tokenizer_cursor {
+ sqlite3_tokenizer* pTokenizer; /* Tokenizer for this cursor. */
+ /* Tokenizer implementations will typically add additional fields */
+};
+
+#endif /* _FTS3_TOKENIZER_H_ */
diff --git a/comm/mailnews/extensions/fts3/moz.build b/comm/mailnews/extensions/fts3/moz.build
new file mode 100644
index 0000000000..a2d7a8de83
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIFts3Tokenizer.idl",
+]
+
+XPIDL_MODULE = "fts3tok"
+
+SOURCES += [
+ "fts3_porter.c",
+ "Normalize.c",
+ "nsFts3Tokenizer.cpp",
+ "nsGlodaRankerFunction.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+CXXFLAGS += CONFIG["SQLITE_CFLAGS"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/extensions/fts3/nsFts3Tokenizer.cpp b/comm/mailnews/extensions/fts3/nsFts3Tokenizer.cpp
new file mode 100644
index 0000000000..9c85543147
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/nsFts3Tokenizer.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsFts3Tokenizer.h"
+
+#include "nsGlodaRankerFunction.h"
+
+#include "nsIFts3Tokenizer.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "nsString.h"
+
+extern "C" void sqlite3Fts3PorterTokenizerModule(
+ sqlite3_tokenizer_module const** ppModule);
+
+extern "C" void glodaRankFunc(sqlite3_context* pCtx, int nVal,
+ sqlite3_value** apVal);
+
+NS_IMPL_ISUPPORTS(nsFts3Tokenizer, nsIFts3Tokenizer)
+
+nsFts3Tokenizer::nsFts3Tokenizer() {}
+
+nsFts3Tokenizer::~nsFts3Tokenizer() {}
+
+NS_IMETHODIMP
+nsFts3Tokenizer::RegisterTokenizer(mozIStorageConnection* connection) {
+ nsresult rv;
+ nsCOMPtr<mozIStorageStatement> selectStatement;
+
+ // -- register the tokenizer
+ rv = connection->CreateStatement("SELECT fts3_tokenizer(?1, ?2)"_ns,
+ getter_AddRefs(selectStatement));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const sqlite3_tokenizer_module* module = nullptr;
+ sqlite3Fts3PorterTokenizerModule(&module);
+ if (!module) return NS_ERROR_FAILURE;
+
+ rv = selectStatement->BindUTF8StringByIndex(0, "mozporter"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = selectStatement->BindBlobByIndex(1, (uint8_t*)&module, sizeof(module));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ rv = selectStatement->ExecuteStep(&hasMore);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // -- register the ranking function
+ nsCOMPtr<mozIStorageFunction> func = new nsGlodaRankerFunction();
+ NS_ENSURE_TRUE(func, NS_ERROR_OUT_OF_MEMORY);
+ rv = connection->CreateFunction("glodaRank"_ns,
+ -1, // variable argument support
+ func);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
diff --git a/comm/mailnews/extensions/fts3/nsFts3Tokenizer.h b/comm/mailnews/extensions/fts3/nsFts3Tokenizer.h
new file mode 100644
index 0000000000..8e6bb7dab4
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/nsFts3Tokenizer.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsFts3Tokenizer_h__
+#define nsFts3Tokenizer_h__
+
+#include "nsCOMPtr.h"
+#include "nsIFts3Tokenizer.h"
+#include "fts3_tokenizer.h"
+
+extern const sqlite3_tokenizer_module* getWindowsTokenizer();
+
+class nsFts3Tokenizer final : public nsIFts3Tokenizer {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFTS3TOKENIZER
+
+ nsFts3Tokenizer();
+
+ private:
+ ~nsFts3Tokenizer();
+};
+
+#endif
diff --git a/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.cpp b/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.cpp
new file mode 100644
index 0000000000..9ba125466f
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.cpp
@@ -0,0 +1,106 @@
+/* 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/. */
+
+#include "nsGlodaRankerFunction.h"
+#include "mozIStorageValueArray.h"
+
+#include "sqlite3.h"
+
+#include "nsCOMPtr.h"
+#include "nsVariant.h"
+#include "nsComponentManagerUtils.h"
+
+#ifndef SQLITE_VERSION_NUMBER
+# error "We need SQLITE_VERSION_NUMBER defined!"
+#endif
+
+NS_IMPL_ISUPPORTS(nsGlodaRankerFunction, mozIStorageFunction)
+
+nsGlodaRankerFunction::nsGlodaRankerFunction() {}
+
+nsGlodaRankerFunction::~nsGlodaRankerFunction() {}
+
+static uint32_t COLUMN_SATURATION[] = {10, 1, 1, 1, 1};
+
+/**
+ * Our ranking function basically just multiplies the weight of the column
+ * against the number of (saturating) matches.
+ *
+ * The original code is a SQLite example ranking function, although somewhat
+ * rather modified at this point. All SQLite code is public domain, so we are
+ * subsuming it to MPL1.1/LGPL2/GPL2.
+ */
+NS_IMETHODIMP
+nsGlodaRankerFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // all argument names are maintained from the original SQLite code.
+ uint32_t nVal;
+ nsresult rv = aArguments->GetNumEntries(&nVal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Check that the number of arguments passed to this function is correct.
+ * If not, return an error. Set aArgsData to point to the array
+ * of unsigned integer values returned by FTS3 function. Set nPhrase
+ * to contain the number of reportable phrases in the users full-text
+ * query, and nCol to the number of columns in the table.
+ */
+ if (nVal < 1) return NS_ERROR_INVALID_ARG;
+
+ uint32_t lenArgsData;
+ uint32_t* aArgsData = (uint32_t*)aArguments->AsSharedBlob(0, &lenArgsData);
+
+ uint32_t nPhrase = aArgsData[0];
+ uint32_t nCol = aArgsData[1];
+ if (nVal != (1 + nCol)) return NS_ERROR_INVALID_ARG;
+
+ double score = 0.0;
+
+ // SQLite 3.6.22 has a different matchinfo layout than SQLite 3.6.23+
+#if SQLITE_VERSION_NUMBER <= 3006022
+ /* Iterate through each phrase in the users query. */
+ for (uint32_t iPhrase = 0; iPhrase < nPhrase; iPhrase++) {
+ // in SQ
+ for (uint32_t iCol = 0; iCol < nCol; iCol++) {
+ uint32_t nHitCount = aArgsData[2 + (iPhrase + 1) * nCol + iCol];
+ double weight = aArguments->AsDouble(iCol + 1);
+ if (nHitCount > 0) {
+ score += (nHitCount > COLUMN_SATURATION[iCol])
+ ? (COLUMN_SATURATION[iCol] * weight)
+ : (nHitCount * weight);
+ }
+ }
+ }
+#else
+ /* Iterate through each phrase in the users query. */
+ for (uint32_t iPhrase = 0; iPhrase < nPhrase; iPhrase++) {
+ /* Now iterate through each column in the users query. For each column,
+ ** increment the relevancy score by:
+ **
+ ** (<hit count> / <global hit count>) * <column weight>
+ **
+ ** aPhraseinfo[] points to the start of the data for phrase iPhrase. So
+ ** the hit count and global hit counts for each column are found in
+ ** aPhraseinfo[iCol*3] and aPhraseinfo[iCol*3+1], respectively.
+ */
+ uint32_t* aPhraseinfo = &aArgsData[2 + iPhrase * nCol * 3];
+ for (uint32_t iCol = 0; iCol < nCol; iCol++) {
+ uint32_t nHitCount = aPhraseinfo[3 * iCol];
+ double weight = aArguments->AsDouble(iCol + 1);
+ if (nHitCount > 0) {
+ score += (nHitCount > COLUMN_SATURATION[iCol])
+ ? (COLUMN_SATURATION[iCol] * weight)
+ : (nHitCount * weight);
+ }
+ }
+ }
+#endif
+
+ nsCOMPtr<nsIWritableVariant> result = new nsVariant();
+
+ rv = result->SetAsDouble(score);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ result.forget(_result);
+ return NS_OK;
+}
diff --git a/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.h b/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.h
new file mode 100644
index 0000000000..0a19073a90
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/nsGlodaRankerFunction.h
@@ -0,0 +1,25 @@
+/* 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/. */
+
+#ifndef _nsGlodaRankerFunction_h_
+#define _nsGlodaRankerFunction_h_
+
+#include "mozIStorageFunction.h"
+
+/**
+ * Basically a port of the example FTS3 ranking function to mozStorage's
+ * view of the universe. This might get fancier at some point.
+ */
+class nsGlodaRankerFunction final : public mozIStorageFunction {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ nsGlodaRankerFunction();
+
+ private:
+ ~nsGlodaRankerFunction();
+};
+
+#endif // _nsGlodaRankerFunction_h_
diff --git a/comm/mailnews/extensions/fts3/nsIFts3Tokenizer.idl b/comm/mailnews/extensions/fts3/nsIFts3Tokenizer.idl
new file mode 100644
index 0000000000..c2bb7d435a
--- /dev/null
+++ b/comm/mailnews/extensions/fts3/nsIFts3Tokenizer.idl
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface mozIStorageConnection;
+
+[scriptable, uuid(136c88ea-7003-4fe8-8835-333fd18e598c)]
+interface nsIFts3Tokenizer : nsISupports {
+ // register FTS3 tokenizer module for "mozporter" tokenizer
+ // mozporter is based by porter tokenizer with bi-gram tokenizer for CJK
+ void registerTokenizer(in mozIStorageConnection connection);
+};
diff --git a/comm/mailnews/extensions/mailviews/components.conf b/comm/mailnews/extensions/mailviews/components.conf
new file mode 100644
index 0000000000..757d1eca93
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{a0258267-44fd-4886-a858-8192615178ec}",
+ "contract_ids": ["@mozilla.org/messenger/mailviewlist;1"],
+ "type": "nsMsgMailViewList",
+ "headers": ["/comm/mailnews/extensions/mailviews/nsMsgMailViewList.h"],
+ },
+]
diff --git a/comm/mailnews/extensions/mailviews/mailViews.dat b/comm/mailnews/extensions/mailviews/mailViews.dat
new file mode 100644
index 0000000000..3c5af3831c
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/mailViews.dat
@@ -0,0 +1,22 @@
+version="9"
+logging="no"
+name="People I Know"
+enabled="yes"
+type="1"
+condition="AND (from,is in ab,jsaddrbook://abook.sqlite)"
+name="Recent Mail"
+enabled="yes"
+type="1"
+condition="AND (age in days,is less than,1)"
+name="Last 5 Days"
+enabled="yes"
+type="1"
+condition="AND (age in days,is less than,5)"
+name="Not Junk"
+enabled="yes"
+type="1"
+condition="AND (junk status,isn't,2)"
+name="Has Attachments"
+enabled="yes"
+type="1"
+condition="AND (has attachment status,is,true)"
diff --git a/comm/mailnews/extensions/mailviews/moz.build b/comm/mailnews/extensions/mailviews/moz.build
new file mode 100644
index 0000000000..de2df9ec3a
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsMsgMailViewList.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+XPIDL_SOURCES += [
+ "nsIMsgMailView.idl",
+ "nsIMsgMailViewList.idl",
+]
+
+XPIDL_MODULE = "mailview"
+
+FINAL_TARGET_FILES.defaults.messenger += [
+ "mailViews.dat",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/extensions/mailviews/nsIMsgMailView.idl b/comm/mailnews/extensions/mailviews/nsIMsgMailView.idl
new file mode 100644
index 0000000000..730cdcbc39
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/nsIMsgMailView.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIMsgSearchTerm;
+
+[scriptable, uuid(28AC84DF-CBE5-430d-A5C0-4FA63B5424DF)]
+interface nsIMsgMailView : nsISupports {
+ attribute wstring mailViewName;
+ readonly attribute wstring prettyName; // localized pretty name
+
+ // the array of search terms
+ attribute Array<nsIMsgSearchTerm> searchTerms;
+
+ // these two helper methods are required to allow searchTermsOverlay.js to
+ // manipulate a mail view without knowing it is dealing with a mail view. nsIMsgFilter
+ // and nsIMsgSearchSession have the same two methods....we should probably make an interface around them.
+ void appendTerm(in nsIMsgSearchTerm term);
+ nsIMsgSearchTerm createTerm();
+};
diff --git a/comm/mailnews/extensions/mailviews/nsIMsgMailViewList.idl b/comm/mailnews/extensions/mailviews/nsIMsgMailViewList.idl
new file mode 100644
index 0000000000..cc8b2f6d4b
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/nsIMsgMailViewList.idl
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgMailView.idl"
+
+///////////////////////////////////////////////////////////////////////////////
+// A mail view list is a list of mail views a particular implementor provides
+///////////////////////////////////////////////////////////////////////////////
+
+typedef long nsMsgMailViewListFileAttribValue;
+
+[scriptable, uuid(6DD798D7-9528-49e6-9447-3AAF14D2D36F)]
+interface nsIMsgMailViewList : nsISupports {
+
+ readonly attribute unsigned long mailViewCount;
+
+ nsIMsgMailView getMailViewAt(in unsigned long mailViewIndex);
+
+ void addMailView(in nsIMsgMailView mailView);
+ void removeMailView(in nsIMsgMailView mailView);
+
+ nsIMsgMailView createMailView();
+
+ void save();
+};
diff --git a/comm/mailnews/extensions/mailviews/nsMsgMailViewList.cpp b/comm/mailnews/extensions/mailviews/nsMsgMailViewList.cpp
new file mode 100644
index 0000000000..753061a540
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/nsMsgMailViewList.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgMailViewList.h"
+#include "nsArray.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/Components.h"
+#include "nsIMsgFilter.h"
+
+#define kDefaultViewPeopleIKnow "People I Know"
+#define kDefaultViewRecent "Recent Mail"
+#define kDefaultViewFiveDays "Last 5 Days"
+#define kDefaultViewNotJunk "Not Junk"
+#define kDefaultViewHasAttachments "Has Attachments"
+
+nsMsgMailView::nsMsgMailView() {}
+
+NS_IMPL_ADDREF(nsMsgMailView)
+NS_IMPL_RELEASE(nsMsgMailView)
+NS_IMPL_QUERY_INTERFACE(nsMsgMailView, nsIMsgMailView)
+
+nsMsgMailView::~nsMsgMailView() {}
+
+NS_IMETHODIMP nsMsgMailView::GetMailViewName(char16_t** aMailViewName) {
+ NS_ENSURE_ARG_POINTER(aMailViewName);
+
+ *aMailViewName = ToNewUnicode(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailView::SetMailViewName(const char16_t* aMailViewName) {
+ mName = aMailViewName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailView::GetPrettyName(char16_t** aMailViewName) {
+ NS_ENSURE_ARG_POINTER(aMailViewName);
+
+ nsresult rv = NS_OK;
+ if (!mBundle) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ bundleService->CreateBundle(
+ "chrome://messenger/locale/mailviews.properties",
+ getter_AddRefs(mBundle));
+ }
+
+ NS_ENSURE_TRUE(mBundle, NS_ERROR_FAILURE);
+
+ // see if mName has an associated pretty name inside our string bundle and if
+ // so, use that as the pretty name otherwise just return mName
+ nsAutoString mailViewName;
+ if (mName.EqualsLiteral(kDefaultViewPeopleIKnow)) {
+ rv = mBundle->GetStringFromName("mailViewPeopleIKnow", mailViewName);
+ *aMailViewName = ToNewUnicode(mailViewName);
+ } else if (mName.EqualsLiteral(kDefaultViewRecent)) {
+ rv = mBundle->GetStringFromName("mailViewRecentMail", mailViewName);
+ *aMailViewName = ToNewUnicode(mailViewName);
+ } else if (mName.EqualsLiteral(kDefaultViewFiveDays)) {
+ rv = mBundle->GetStringFromName("mailViewLastFiveDays", mailViewName);
+ *aMailViewName = ToNewUnicode(mailViewName);
+ } else if (mName.EqualsLiteral(kDefaultViewNotJunk)) {
+ rv = mBundle->GetStringFromName("mailViewNotJunk", mailViewName);
+ *aMailViewName = ToNewUnicode(mailViewName);
+ } else if (mName.EqualsLiteral(kDefaultViewHasAttachments)) {
+ rv = mBundle->GetStringFromName("mailViewHasAttachments", mailViewName);
+ *aMailViewName = ToNewUnicode(mailViewName);
+ } else {
+ *aMailViewName = ToNewUnicode(mName);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailView::GetSearchTerms(
+ nsTArray<RefPtr<nsIMsgSearchTerm>>& terms) {
+ terms = mViewSearchTerms.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailView::SetSearchTerms(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& terms) {
+ mViewSearchTerms = terms.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailView::AppendTerm(nsIMsgSearchTerm* aTerm) {
+ NS_ENSURE_TRUE(aTerm, NS_ERROR_NULL_POINTER);
+
+ mViewSearchTerms.AppendElement(aTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailView::CreateTerm(nsIMsgSearchTerm** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsCOMPtr<nsIMsgSearchTerm> searchTerm =
+ do_CreateInstance("@mozilla.org/messenger/searchTerm;1");
+ searchTerm.forget(aResult);
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// nsMsgMailViewList implementation
+/////////////////////////////////////////////////////////////////////////////
+nsMsgMailViewList::nsMsgMailViewList() { LoadMailViews(); }
+
+NS_IMPL_ADDREF(nsMsgMailViewList)
+NS_IMPL_RELEASE(nsMsgMailViewList)
+NS_IMPL_QUERY_INTERFACE(nsMsgMailViewList, nsIMsgMailViewList)
+
+nsMsgMailViewList::~nsMsgMailViewList() {}
+
+NS_IMETHODIMP nsMsgMailViewList::GetMailViewCount(uint32_t* aCount) {
+ NS_ENSURE_ARG_POINTER(aCount);
+
+ *aCount = m_mailViews.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailViewList::GetMailViewAt(uint32_t aMailViewIndex,
+ nsIMsgMailView** aMailView) {
+ NS_ENSURE_ARG_POINTER(aMailView);
+
+ uint32_t mailViewCount = m_mailViews.Length();
+
+ NS_ENSURE_ARG(mailViewCount > aMailViewIndex);
+
+ NS_IF_ADDREF(*aMailView = m_mailViews[aMailViewIndex]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailViewList::AddMailView(nsIMsgMailView* aMailView) {
+ NS_ENSURE_ARG_POINTER(aMailView);
+
+ m_mailViews.AppendElement(aMailView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailViewList::RemoveMailView(nsIMsgMailView* aMailView) {
+ NS_ENSURE_ARG_POINTER(aMailView);
+
+ m_mailViews.RemoveElement(aMailView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailViewList::CreateMailView(nsIMsgMailView** aMailView) {
+ NS_ENSURE_ARG_POINTER(aMailView);
+ NS_ADDREF(*aMailView = new nsMsgMailView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailViewList::Save() {
+ // brute force...remove all the old filters in our filter list, then we'll
+ // re-add our current list
+ nsCOMPtr<nsIMsgFilter> msgFilter;
+ uint32_t numFilters = 0;
+ if (mFilterList) mFilterList->GetFilterCount(&numFilters);
+ while (numFilters) {
+ mFilterList->RemoveFilterAt(numFilters - 1);
+ numFilters--;
+ }
+
+ // now convert our mail view list into a filter list and save it
+ ConvertMailViewListToFilterList();
+
+ // now save the filters to our file
+ return mFilterList ? mFilterList->SaveToDefaultFile() : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgMailViewList::ConvertMailViewListToFilterList() {
+ uint32_t mailViewCount = m_mailViews.Length();
+ nsCOMPtr<nsIMsgMailView> mailView;
+ nsCOMPtr<nsIMsgFilter> newMailFilter;
+ nsString mailViewName;
+ for (uint32_t index = 0; index < mailViewCount; index++) {
+ GetMailViewAt(index, getter_AddRefs(mailView));
+ if (!mailView) continue;
+ mailView->GetMailViewName(getter_Copies(mailViewName));
+ mFilterList->CreateFilter(mailViewName, getter_AddRefs(newMailFilter));
+ if (!newMailFilter) continue;
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ mailView->GetSearchTerms(searchTerms);
+ newMailFilter->SetSearchTerms(searchTerms);
+ mFilterList->InsertFilterAt(index, newMailFilter);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgMailViewList::LoadMailViews() {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->AppendNative("mailViews.dat"_ns);
+
+ // if the file doesn't exist, we should try to get it from the defaults
+ // directory and copy it over
+ bool exists = false;
+ file->Exists(&exists);
+ if (!exists) {
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> defaultMessagesFile;
+ nsCOMPtr<nsIFile> profileDir;
+ rv = mailSession->GetDataFilesDir("messenger",
+ getter_AddRefs(defaultMessagesFile));
+ rv = defaultMessagesFile->AppendNative("mailViews.dat"_ns);
+
+ // get the profile directory
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+
+ // now copy the file over to the profile directory
+ defaultMessagesFile->CopyToNative(profileDir, EmptyCString());
+ }
+ // this is kind of a hack but I think it will be an effective hack. The filter
+ // service already knows how to take a nsIFile and parse the contents into
+ // filters which are very similar to mail views. Instead of re-writing all of
+ // that dirty parsing code, let's just re-use it then convert the results into
+ // a data structure we wish to give to our consumers.
+
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ nsCOMPtr<nsIMsgFilterList> mfilterList;
+
+ rv = filterService->OpenFilterList(file, nullptr, nullptr,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return ConvertFilterListToMailViews();
+}
+/**
+ * Converts the filter list into our mail view objects,
+ * stripping out just the info we need.
+ */
+nsresult nsMsgMailViewList::ConvertFilterListToMailViews() {
+ nsresult rv = NS_OK;
+ m_mailViews.Clear();
+
+ // iterate over each filter in the list
+ uint32_t numFilters = 0;
+ mFilterList->GetFilterCount(&numFilters);
+ for (uint32_t index = 0; index < numFilters; index++) {
+ nsCOMPtr<nsIMsgFilter> msgFilter;
+ rv = mFilterList->GetFilterAt(index, getter_AddRefs(msgFilter));
+ if (NS_FAILED(rv) || !msgFilter) continue;
+
+ // create a new nsIMsgMailView for this item
+ nsCOMPtr<nsIMsgMailView> newMailView;
+ rv = CreateMailView(getter_AddRefs(newMailView));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString filterName;
+ msgFilter->GetFilterName(filterName);
+ newMailView->SetMailViewName(filterName.get());
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> filterSearchTerms;
+ rv = msgFilter->GetSearchTerms(filterSearchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = newMailView->SetSearchTerms(filterSearchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now append this new mail view to our global list view
+ m_mailViews.AppendElement(newMailView);
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/extensions/mailviews/nsMsgMailViewList.h b/comm/mailnews/extensions/mailviews/nsMsgMailViewList.h
new file mode 100644
index 0000000000..19e283681c
--- /dev/null
+++ b/comm/mailnews/extensions/mailviews/nsMsgMailViewList.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgMailViewList_H_
+#define _nsMsgMailViewList_H_
+
+#include "nscore.h"
+#include "nsIMsgMailViewList.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIStringBundle.h"
+#include "nsString.h"
+#include "nsIMsgFilterList.h"
+
+// a mail View is just a name and an array of search terms
+class nsMsgMailView : public nsIMsgMailView {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGMAILVIEW
+
+ nsMsgMailView();
+
+ protected:
+ virtual ~nsMsgMailView();
+ nsString mName;
+ nsCOMPtr<nsIStringBundle> mBundle;
+ nsTArray<RefPtr<nsIMsgSearchTerm>> mViewSearchTerms;
+};
+
+class nsMsgMailViewList : public nsIMsgMailViewList {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGMAILVIEWLIST
+
+ nsMsgMailViewList();
+
+ protected:
+ virtual ~nsMsgMailViewList();
+ nsresult
+ LoadMailViews(); // reads in user defined mail views from our default file
+ nsresult ConvertFilterListToMailViews();
+ nsresult ConvertMailViewListToFilterList();
+
+ nsCOMArray<nsIMsgMailView> m_mailViews;
+ nsCOMPtr<nsIMsgFilterList>
+ mFilterList; // our internal filter list representation
+};
+
+#endif
diff --git a/comm/mailnews/extensions/mdn/MDNService.jsm b/comm/mailnews/extensions/mdn/MDNService.jsm
new file mode 100644
index 0000000000..47f5257b6a
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/MDNService.jsm
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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 EXPORTED_SYMBOLS = ["MDNService"];
+
+function MDNService() {}
+
+MDNService.prototype = {
+ name: "mdn",
+ chromePackageName: "messenger",
+ showPanel(server) {
+ // don't show the panel for news, rss, im or local accounts
+ return (
+ server.type != "nntp" &&
+ server.type != "rss" &&
+ server.type != "im" &&
+ server.type != "none"
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgAccountManagerExtension"]),
+};
diff --git a/comm/mailnews/extensions/mdn/am-mdn.js b/comm/mailnews/extensions/mdn/am-mdn.js
new file mode 100644
index 0000000000..7725339fbd
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/am-mdn.js
@@ -0,0 +1,161 @@
+/* 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/. */
+
+/* import-globals-from ../../base/prefs/content/amUtils.js */
+
+var useCustomPrefs;
+var requestReceipt;
+var leaveInInbox;
+var moveToSent;
+var receiptSend;
+var neverReturn;
+var returnSome;
+var notInToCcPref;
+var notInToCcLabel;
+var outsideDomainPref;
+var outsideDomainLabel;
+var otherCasesPref;
+var otherCasesLabel;
+var receiptArriveLabel;
+var receiptRequestLabel;
+var gIdentity;
+var gIncomingServer;
+var gMdnPrefBranch;
+
+function onInit() {
+ useCustomPrefs = document.getElementById("identity.use_custom_prefs");
+ requestReceipt = document.getElementById(
+ "identity.request_return_receipt_on"
+ );
+ leaveInInbox = document.getElementById("leave_in_inbox");
+ moveToSent = document.getElementById("move_to_sent");
+ receiptSend = document.getElementById("server.mdn_report_enabled");
+ neverReturn = document.getElementById("never_return");
+ returnSome = document.getElementById("return_some");
+ notInToCcPref = document.getElementById("server.mdn_not_in_to_cc");
+ notInToCcLabel = document.getElementById("notInToCcLabel");
+ outsideDomainPref = document.getElementById("server.mdn_outside_domain");
+ outsideDomainLabel = document.getElementById("outsideDomainLabel");
+ otherCasesPref = document.getElementById("server.mdn_other");
+ otherCasesLabel = document.getElementById("otherCasesLabel");
+ receiptArriveLabel = document.getElementById("receiptArriveLabel");
+ receiptRequestLabel = document.getElementById("receiptRequestLabel");
+
+ EnableDisableCustomSettings();
+
+ return true;
+}
+
+function onSave() {}
+
+function EnableDisableCustomSettings() {
+ if (useCustomPrefs && useCustomPrefs.getAttribute("value") == "false") {
+ requestReceipt.setAttribute("disabled", "true");
+ leaveInInbox.setAttribute("disabled", "true");
+ moveToSent.setAttribute("disabled", "true");
+ neverReturn.setAttribute("disabled", "true");
+ returnSome.setAttribute("disabled", "true");
+ receiptArriveLabel.setAttribute("disabled", "true");
+ receiptRequestLabel.setAttribute("disabled", "true");
+ } else {
+ requestReceipt.removeAttribute("disabled");
+ leaveInInbox.removeAttribute("disabled");
+ moveToSent.removeAttribute("disabled");
+ neverReturn.removeAttribute("disabled");
+ returnSome.removeAttribute("disabled");
+ receiptArriveLabel.removeAttribute("disabled");
+ receiptRequestLabel.removeAttribute("disabled");
+ }
+ EnableDisableAllowedReceipts();
+ // Lock id based prefs
+ onLockPreference("mail.identity", gIdentity.key);
+ // Lock server based prefs
+ onLockPreference("mail.server", gIncomingServer.key);
+ return true;
+}
+
+function EnableDisableAllowedReceipts() {
+ if (receiptSend) {
+ if (
+ !neverReturn.getAttribute("disabled") &&
+ receiptSend.getAttribute("value") != "false"
+ ) {
+ notInToCcPref.removeAttribute("disabled");
+ notInToCcLabel.removeAttribute("disabled");
+ outsideDomainPref.removeAttribute("disabled");
+ outsideDomainLabel.removeAttribute("disabled");
+ otherCasesPref.removeAttribute("disabled");
+ otherCasesLabel.removeAttribute("disabled");
+ } else {
+ notInToCcPref.setAttribute("disabled", "true");
+ notInToCcLabel.setAttribute("disabled", "true");
+ outsideDomainPref.setAttribute("disabled", "true");
+ outsideDomainLabel.setAttribute("disabled", "true");
+ otherCasesPref.setAttribute("disabled", "true");
+ otherCasesLabel.setAttribute("disabled", "true");
+ }
+ }
+ return true;
+}
+
+function onPreInit(account, accountValues) {
+ gIdentity = account.defaultIdentity;
+ gIncomingServer = account.incomingServer;
+}
+
+// Disables xul elements that have associated preferences locked.
+function onLockPreference(initPrefString, keyString) {
+ var allPrefElements = [
+ {
+ prefstring: "request_return_receipt_on",
+ id: "identity.request_return_receipt_on",
+ },
+ { prefstring: "select_custom_prefs", id: "identity.select_custom_prefs" },
+ { prefstring: "select_global_prefs", id: "identity.select_global_prefs" },
+ {
+ prefstring: "incorporate_return_receipt",
+ id: "server.incorporate_return_receipt",
+ },
+ { prefstring: "never_return", id: "never_return" },
+ { prefstring: "return_some", id: "return_some" },
+ { prefstring: "mdn_not_in_to_cc", id: "server.mdn_not_in_to_cc" },
+ { prefstring: "mdn_outside_domain", id: "server.mdn_outside_domain" },
+ { prefstring: "mdn_other", id: "server.mdn_other" },
+ ];
+
+ var finalPrefString = initPrefString + "." + keyString + ".";
+ gMdnPrefBranch = Services.prefs.getBranch(finalPrefString);
+
+ disableIfLocked(allPrefElements);
+}
+
+function disableIfLocked(prefstrArray) {
+ for (let i = 0; i < prefstrArray.length; i++) {
+ var id = prefstrArray[i].id;
+ var element = document.getElementById(id);
+ if (gMdnPrefBranch.prefIsLocked(prefstrArray[i].prefstring)) {
+ if (id == "server.incorporate_return_receipt") {
+ document
+ .getElementById("leave_in_inbox")
+ .setAttribute("disabled", "true");
+ document
+ .getElementById("move_to_sent")
+ .setAttribute("disabled", "true");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+ }
+ }
+}
+
+/**
+ * Opens Preferences (Options) dialog on the pane and tab where
+ * the global receipts settings can be found.
+ */
+function showGlobalReceipts() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/preferences/receipts.xhtml",
+ { features: "resizable=no" }
+ );
+}
diff --git a/comm/mailnews/extensions/mdn/am-mdn.xhtml b/comm/mailnews/extensions/mdn/am-mdn.xhtml
new file mode 100644
index 0000000000..3cdf85d9e5
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/am-mdn.xhtml
@@ -0,0 +1,231 @@
+<?xml version="1.0"?>
+<!--
+ 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-mdn.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&pane.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/AccountManager.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-mdn.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-mdn.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_smime"
+ src="chrome://messenger/locale/am-mdn.properties"
+ />
+
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&pane.title;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <hbox id="prefChoices" align="center" flex="1">
+ <radiogroup
+ id="identity.use_custom_prefs"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="bool"
+ prefstring="mail.identity.%identitykey%.use_custom_prefs"
+ oncommand="EnableDisableCustomSettings();"
+ flex="1"
+ >
+ <radio
+ id="identity.select_global_prefs"
+ value="false"
+ label="&useGlobalPrefs.label;"
+ accesskey="&useGlobalPrefs.accesskey;"
+ />
+ <hbox flex="1">
+ <spacer flex="1" />
+ <button
+ id="globalReceiptsLink"
+ label="&globalReceipts.label;"
+ accesskey="&globalReceipts.accesskey;"
+ oncommand="showGlobalReceipts();"
+ />
+ </hbox>
+ <radio
+ id="identity.select_custom_prefs"
+ value="true"
+ label="&useCustomPrefs.label;"
+ accesskey="&useCustomPrefs.accesskey;"
+ />
+ </radiogroup>
+ </hbox>
+
+ <vbox id="returnReceiptSettings" class="indent" align="start">
+ <checkbox
+ id="identity.request_return_receipt_on"
+ label="&requestReceipt.label;"
+ accesskey="&requestReceipt.accesskey;"
+ wsm_persist="true"
+ genericattr="true"
+ iscontrolcontainer="true"
+ preftype="bool"
+ prefstring="mail.identity.%identitykey%.request_return_receipt_on"
+ />
+
+ <separator />
+
+ <vbox id="receiptArrive">
+ <label
+ id="receiptArriveLabel"
+ control="server.incorporate_return_receipt"
+ >&receiptArrive.label;</label
+ >
+ <radiogroup
+ id="server.incorporate_return_receipt"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.incorporate_return_receipt"
+ class="indent"
+ >
+ <radio
+ id="leave_in_inbox"
+ value="0"
+ label="&leaveIt.label;"
+ accesskey="&leaveIt.accesskey;"
+ />
+ <radio
+ id="move_to_sent"
+ value="1"
+ label="&moveToSent.label;"
+ accesskey="&moveToSent.accesskey;"
+ />
+ </radiogroup>
+ </vbox>
+
+ <separator />
+
+ <vbox id="receiptRequest">
+ <label
+ id="receiptRequestLabel"
+ control="server.mdn_report_enabled"
+ >&requestMDN.label;</label
+ >
+ <radiogroup
+ id="server.mdn_report_enabled"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="bool"
+ prefstring="mail.server.%serverkey%.mdn_report_enabled"
+ oncommand="EnableDisableAllowedReceipts();"
+ class="indent"
+ >
+ <radio
+ id="never_return"
+ value="false"
+ label="&never.label;"
+ accesskey="&never.accesskey;"
+ />
+ <radio
+ id="return_some"
+ value="true"
+ label="&returnSome.label;"
+ accesskey="&returnSome.accesskey;"
+ />
+
+ <hbox id="receiptSendIf" class="indent">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="notInToCcLabel"
+ value="&notInToCc.label;"
+ accesskey="&notInToCc.accesskey;"
+ control="server.mdn_not_in_to_cc"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="outsideDomainLabel"
+ value="&outsideDomain.label;"
+ accesskey="&outsideDomain.accesskey;"
+ control="server.mdn_outside_domain"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="otherCasesLabel"
+ value="&otherCases.label;"
+ accesskey="&otherCases.accesskey;"
+ control="server.mdn_other"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <menulist
+ id="server.mdn_not_in_to_cc"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.mdn_not_in_to_cc"
+ >
+ <menupopup>
+ <menuitem value="0" label="&neverSend.label;" />
+ <menuitem value="1" label="&alwaysSend.label;" />
+ <menuitem value="2" label="&askMe.label;" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="server.mdn_outside_domain"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.mdn_outside_domain"
+ >
+ <menupopup>
+ <menuitem value="0" label="&neverSend.label;" />
+ <menuitem value="1" label="&alwaysSend.label;" />
+ <menuitem value="2" label="&askMe.label;" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="server.mdn_other"
+ wsm_persist="true"
+ genericattr="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.mdn_other"
+ >
+ <menupopup>
+ <menuitem value="0" label="&neverSend.label;" />
+ <menuitem value="1" label="&alwaysSend.label;" />
+ <menuitem value="2" label="&askMe.label;" />
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ </vbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/mdn/components.conf b/comm/mailnews/extensions/mdn/components.conf
new file mode 100644
index 0000000000..84f53febbf
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/components.conf
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{e007d92e-1dd1-11b2-a61e-dc962c9b8571}",
+ "contract_ids": ["@mozilla.org/accountmanager/extension;1?name=mdn"],
+ "jsm": "resource:///modules/MDNService.jsm",
+ "constructor": "MDNService",
+ "categories": {
+ "mailnews-accountmanager-extensions": "mdn-account-manager-extension"
+ },
+ },
+ {
+ "cid": "{ec917b13-8f73-4d4d-9146-d7f7aafe9076}",
+ "contract_ids": ["@mozilla.org/messenger-mdn/generator;1"],
+ "type": "nsMsgMdnGenerator",
+ "headers": ["/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.h"],
+ },
+]
diff --git a/comm/mailnews/extensions/mdn/jar.mn b/comm/mailnews/extensions/mdn/jar.mn
new file mode 100644
index 0000000000..cb487cd969
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/am-mdn.xhtml (am-mdn.xhtml)
+ content/messenger/am-mdn.js (am-mdn.js)
diff --git a/comm/mailnews/extensions/mdn/mdn.js b/comm/mailnews/extensions/mdn/mdn.js
new file mode 100644
index 0000000000..ae82382f39
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/mdn.js
@@ -0,0 +1,25 @@
+#filter dumbComments emptyLines substitution
+
+// 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/.
+
+//
+// default prefs for mdn
+//
+
+// false: Use global true: Use custom
+pref("mail.identity.default.use_custom_prefs", false);
+pref("mail.identity.default.request_return_receipt_on", false);
+// 0: Inbox/filter 1: Sent folder
+pref("mail.server.default.incorporate_return_receipt", 0);
+// false: Never return receipts true: Return some receipts
+pref("mail.server.default.mdn_report_enabled", true);
+// 0: Never 1: Always 2: Ask me 3: Denial
+pref("mail.server.default.mdn_not_in_to_cc", 2);
+pref("mail.server.default.mdn_outside_domain", 2);
+pref("mail.server.default.mdn_other", 2);
+// return receipt header type - 0: MDN-DNT 1: RRT 2: Both
+pref("mail.identity.default.request_receipt_header_type", 0);
+
+pref("mail.server.default.mdn_report_enabled", true);
diff --git a/comm/mailnews/extensions/mdn/moz.build b/comm/mailnews/extensions/mdn/moz.build
new file mode 100644
index 0000000000..2d7b8bce5d
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/moz.build
@@ -0,0 +1,26 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+JS_PREFERENCE_PP_FILES += [
+ "mdn.js",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
+
+SOURCES += [
+ "nsMsgMdnGenerator.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "MDNService.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
diff --git a/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.cpp b/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.cpp
new file mode 100644
index 0000000000..76fc6d5214
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.cpp
@@ -0,0 +1,1040 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgMdnGenerator.h"
+#include "nsImapCore.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMimeTypes.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "prsystem.h"
+#include "nsMsgI18N.h"
+#include "nsMailHeaders.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsIHttpProtocolHandler.h"
+#include "nsISmtpService.h" // for actually sending the message...
+#include "nsComposeStrings.h"
+#include "nsISmtpServer.h"
+#include "nsIPrompt.h"
+#include "nsIMsgCompUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIStringBundle.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsMsgUtils.h"
+#include "nsNetUtil.h"
+#include "nsIMsgDatabase.h"
+#include "mozilla/Components.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/Unused.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+using namespace mozilla::mailnews;
+
+#define MDN_NOT_IN_TO_CC ((int)0x0001)
+#define MDN_OUTSIDE_DOMAIN ((int)0x0002)
+
+#define HEADER_RETURN_PATH "Return-Path"
+#define HEADER_DISPOSITION_NOTIFICATION_TO "Disposition-Notification-To"
+#define HEADER_APPARENTLY_TO "Apparently-To"
+#define HEADER_ORIGINAL_RECIPIENT "Original-Recipient"
+#define HEADER_REPORTING_UA "Reporting-UA"
+#define HEADER_MDN_GATEWAY "MDN-Gateway"
+#define HEADER_FINAL_RECIPIENT "Final-Recipient"
+#define HEADER_DISPOSITION "Disposition"
+#define HEADER_ORIGINAL_MESSAGE_ID "Original-Message-ID"
+#define HEADER_FAILURE "Failure"
+#define HEADER_ERROR "Error"
+#define HEADER_WARNING "Warning"
+#define HEADER_RETURN_RECEIPT_TO "Return-Receipt-To"
+#define HEADER_X_ACCEPT_LANGUAGE "X-Accept-Language"
+
+#define PUSH_N_FREE_STRING(p) \
+ do { \
+ if (p) { \
+ rv = WriteString(p); \
+ PR_smprintf_free(p); \
+ p = 0; \
+ if (NS_FAILED(rv)) return rv; \
+ } else { \
+ return NS_ERROR_OUT_OF_MEMORY; \
+ } \
+ } while (0)
+
+// String bundle for mdn. Class static.
+#define MDN_STRINGBUNDLE_URL "chrome://messenger/locale/msgmdn.properties"
+
+#if defined(DEBUG_jefft)
+# define DEBUG_MDN(s) printf("%s\n", s)
+#else
+# define DEBUG_MDN(s)
+#endif
+
+// machine parsible string; should not be localized
+char DispositionTypes[7][16] = {
+ "displayed", "dispatched", "processed", "deleted", "denied", "failed", ""};
+
+NS_IMPL_ISUPPORTS(nsMsgMdnGenerator, nsIMsgMdnGenerator, nsIUrlListener)
+
+nsMsgMdnGenerator::nsMsgMdnGenerator()
+ : m_disposeType(eDisplayed),
+ m_key(nsMsgKey_None),
+ m_notInToCcOp(eNeverSendOp),
+ m_outsideDomainOp(eNeverSendOp),
+ m_otherOp(eNeverSendOp),
+ m_reallySendMdn(false),
+ m_autoSend(false),
+ m_autoAction(false),
+ m_mdnEnabled(false) {}
+
+nsMsgMdnGenerator::~nsMsgMdnGenerator() {}
+
+nsresult nsMsgMdnGenerator::FormatStringFromName(const char* aName,
+ const nsString& aString,
+ nsAString& aResultString) {
+ DEBUG_MDN("nsMsgMdnGenerator::FormatStringFromName");
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv =
+ bundleService->CreateBundle(MDN_STRINGBUNDLE_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 1> formatStrings = {aString};
+ rv = bundle->FormatStringFromName(aName, formatStrings, aResultString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::GetStringFromName(const char* aName,
+ nsAString& aResultString) {
+ DEBUG_MDN("nsMsgMdnGenerator::GetStringFromName");
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv =
+ bundleService->CreateBundle(MDN_STRINGBUNDLE_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = bundle->GetStringFromName(aName, aResultString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::StoreMDNSentFlag(nsIMsgFolder* folder,
+ nsMsgKey key) {
+ DEBUG_MDN("nsMsgMdnGenerator::StoreMDNSentFlag");
+
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDB->MarkMDNSent(key, true, nullptr);
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ // Store the $MDNSent flag if the folder is an Imap Mail Folder
+ if (imapFolder)
+ return imapFolder->StoreImapFlags(kImapMsgMDNSentFlag, true, {key},
+ nullptr);
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::ClearMDNNeededFlag(nsIMsgFolder* folder,
+ nsMsgKey key) {
+ DEBUG_MDN("nsMsgMdnGenerator::ClearMDNNeededFlag");
+
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgDB->MarkMDNNeeded(key, false, nullptr);
+}
+
+bool nsMsgMdnGenerator::ProcessSendMode() {
+ DEBUG_MDN("nsMsgMdnGenerator::ProcessSendMode");
+ int32_t miscState = 0;
+
+ if (m_identity) {
+ m_identity->GetEmail(m_email);
+ if (m_email.IsEmpty()) return m_reallySendMdn;
+
+ const char* accountDomain = strchr(m_email.get(), '@');
+ if (!accountDomain) return m_reallySendMdn;
+
+ if (MailAddrMatch(m_email.get(),
+ m_dntRrt.get())) // return address is self, don't send
+ return false;
+
+ // *** fix me see Bug 132504 for more information
+ // *** what if the message has been filtered to different account
+ if (!PL_strcasestr(m_dntRrt.get(), accountDomain))
+ miscState |= MDN_OUTSIDE_DOMAIN;
+ if (NotInToOrCc()) miscState |= MDN_NOT_IN_TO_CC;
+ m_reallySendMdn = true;
+ // *********
+ // How are we gona deal with the auto forwarding issues? Some server
+ // didn't bother to add addition header or modify existing header to
+ // the message when forwarding. They simply copy the exact same
+ // message to another user's mailbox. Some change To: to
+ // Apparently-To:
+ // Unfortunately, there is nothing we can do. It's out of our control.
+ // *********
+ // starting from lowest denominator to highest
+ if (!miscState) { // under normal situation: recipent is in to and cc list,
+ // and the sender is from the same domain
+ switch (m_otherOp) {
+ default:
+ case eNeverSendOp:
+ m_reallySendMdn = false;
+ break;
+ case eAutoSendOp:
+ m_autoSend = true;
+ break;
+ case eAskMeOp:
+ m_autoSend = false;
+ break;
+ case eDeniedOp:
+ m_autoSend = true;
+ m_disposeType = eDenied;
+ break;
+ }
+ } else if (miscState == (MDN_OUTSIDE_DOMAIN | MDN_NOT_IN_TO_CC)) {
+ if (m_outsideDomainOp != m_notInToCcOp) {
+ m_autoSend = false; // ambiguous; always ask user
+ } else {
+ switch (m_outsideDomainOp) {
+ default:
+ case eNeverSendOp:
+ m_reallySendMdn = false;
+ break;
+ case eAutoSendOp:
+ m_autoSend = true;
+ break;
+ case eAskMeOp:
+ m_autoSend = false;
+ break;
+ }
+ }
+ } else if (miscState & MDN_OUTSIDE_DOMAIN) {
+ switch (m_outsideDomainOp) {
+ default:
+ case eNeverSendOp:
+ m_reallySendMdn = false;
+ break;
+ case eAutoSendOp:
+ m_autoSend = true;
+ break;
+ case eAskMeOp:
+ m_autoSend = false;
+ break;
+ }
+ } else if (miscState & MDN_NOT_IN_TO_CC) {
+ switch (m_notInToCcOp) {
+ default:
+ case eNeverSendOp:
+ m_reallySendMdn = false;
+ break;
+ case eAutoSendOp:
+ m_autoSend = true;
+ break;
+ case eAskMeOp:
+ m_autoSend = false;
+ break;
+ }
+ }
+ }
+ return m_reallySendMdn;
+}
+
+bool nsMsgMdnGenerator::MailAddrMatch(const char* addr1, const char* addr2) {
+ // Comparing two email addresses returns true if matched; local/account
+ // part comparison is case sensitive; domain part comparison is case
+ // insensitive
+ DEBUG_MDN("nsMsgMdnGenerator::MailAddrMatch");
+ bool isMatched = true;
+ const char *atSign1 = nullptr, *atSign2 = nullptr;
+ const char *lt = nullptr, *local1 = nullptr, *local2 = nullptr;
+ const char *end1 = nullptr, *end2 = nullptr;
+
+ if (!addr1 || !addr2) return false;
+
+ lt = strchr(addr1, '<');
+ local1 = !lt ? addr1 : lt + 1;
+ lt = strchr(addr2, '<');
+ local2 = !lt ? addr2 : lt + 1;
+ end1 = strchr(local1, '>');
+ if (!end1) end1 = addr1 + strlen(addr1);
+ end2 = strchr(local2, '>');
+ if (!end2) end2 = addr2 + strlen(addr2);
+ atSign1 = strchr(local1, '@');
+ atSign2 = strchr(local2, '@');
+ if (!atSign1 || !atSign2 // ill formed addr spec
+ || (atSign1 - local1) != (atSign2 - local2))
+ isMatched = false;
+ else if (strncmp(local1, local2, (atSign1 - local1))) // case sensitive
+ // compare for local part
+ isMatched = false;
+ else if ((end1 - atSign1) != (end2 - atSign2) ||
+ PL_strncasecmp(atSign1, atSign2, (end1 - atSign1))) // case
+ // insensitive compare for domain part
+ isMatched = false;
+ return isMatched;
+}
+
+bool nsMsgMdnGenerator::NotInToOrCc() {
+ DEBUG_MDN("nsMsgMdnGenerator::NotInToOrCc");
+ nsCString reply_to;
+ nsCString to;
+ nsCString cc;
+
+ m_identity->GetReplyTo(reply_to);
+ m_headers->ExtractHeader(HEADER_TO, true, to);
+ m_headers->ExtractHeader(HEADER_CC, true, cc);
+
+ // start with a simple check
+ if ((!to.IsEmpty() && PL_strcasestr(to.get(), m_email.get())) ||
+ (!cc.IsEmpty() && PL_strcasestr(cc.get(), m_email.get()))) {
+ return false;
+ }
+
+ if ((!reply_to.IsEmpty() && !to.IsEmpty() &&
+ PL_strcasestr(to.get(), reply_to.get())) ||
+ (!reply_to.IsEmpty() && !cc.IsEmpty() &&
+ PL_strcasestr(cc.get(), reply_to.get()))) {
+ return false;
+ }
+ return true;
+}
+
+bool nsMsgMdnGenerator::ValidateReturnPath() {
+ DEBUG_MDN("nsMsgMdnGenerator::ValidateReturnPath");
+ // ValidateReturnPath applies to Automatic Send Mode only. If we were not
+ // in auto send mode we simply by passing the check
+ if (!m_autoSend) return m_reallySendMdn;
+
+ nsCString returnPath;
+ m_headers->ExtractHeader(HEADER_RETURN_PATH, false, returnPath);
+ if (returnPath.IsEmpty()) {
+ m_autoSend = false;
+ return m_reallySendMdn;
+ }
+ m_autoSend = MailAddrMatch(returnPath.get(), m_dntRrt.get());
+ return m_reallySendMdn;
+}
+
+nsresult nsMsgMdnGenerator::CreateMdnMsg() {
+ DEBUG_MDN("nsMsgMdnGenerator::CreateMdnMsg");
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> tmpFile;
+ rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "mdnmsg",
+ getter_AddRefs(m_file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), m_file,
+ PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE,
+ 0664);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "creating mdn: failed to output stream");
+ if (NS_FAILED(rv)) return NS_OK;
+
+ rv = CreateFirstPart();
+ if (NS_SUCCEEDED(rv)) {
+ rv = CreateSecondPart();
+ if (NS_SUCCEEDED(rv)) rv = CreateThirdPart();
+ }
+
+ if (m_outputStream) {
+ m_outputStream->Flush();
+ m_outputStream->Close();
+ }
+ if (NS_FAILED(rv))
+ m_file->Remove(false);
+ else
+ rv = SendMdnMsg();
+
+ return NS_OK;
+}
+
+nsresult nsMsgMdnGenerator::CreateFirstPart() {
+ DEBUG_MDN("nsMsgMdnGenerator::CreateFirstPart");
+ char *convbuf = nullptr, *tmpBuffer = nullptr;
+ char* parm = nullptr;
+ nsString firstPart1;
+ nsString firstPart2;
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompUtils> compUtils;
+
+ if (m_mimeSeparator.IsEmpty()) {
+ compUtils = do_GetService("@mozilla.org/messengercompose/computils;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = compUtils->MimeMakeSeparator("mdn", getter_Copies(m_mimeSeparator));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (m_mimeSeparator.IsEmpty()) return NS_ERROR_OUT_OF_MEMORY;
+
+ tmpBuffer = (char*)PR_CALLOC(256);
+
+ if (!tmpBuffer) return NS_ERROR_OUT_OF_MEMORY;
+
+ PRExplodedTime now;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);
+
+ int gmtoffset =
+ (now.tm_params.tp_gmt_offset + now.tm_params.tp_dst_offset) / 60;
+ /* Use PR_FormatTimeUSEnglish() to format the date in US English format,
+ then figure out what our local GMT offset is, and append it (since
+ PR_FormatTimeUSEnglish() can't do that.) Generate four digit years as
+ per RFC 1123 (superseding RFC 822.)
+ */
+ PR_FormatTimeUSEnglish(tmpBuffer, 100, "Date: %a, %d %b %Y %H:%M:%S ", &now);
+
+ PR_snprintf(tmpBuffer + strlen(tmpBuffer), 100, "%c%02d%02d" CRLF,
+ (gmtoffset >= 0 ? '+' : '-'),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) / 60),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) % 60));
+
+ rv = WriteString(tmpBuffer);
+ PR_Free(tmpBuffer);
+ if (NS_FAILED(rv)) return rv;
+
+ bool conformToStandard = false;
+ if (compUtils) compUtils->GetMsgMimeConformToStandard(&conformToStandard);
+
+ nsString fullName;
+ m_identity->GetFullName(fullName);
+
+ nsCString fullAddress;
+ // convert fullName to UTF8 before passing it to MakeMimeAddress
+ MakeMimeAddress(NS_ConvertUTF16toUTF8(fullName), m_email, fullAddress);
+
+ convbuf = nsMsgI18NEncodeMimePartIIStr(fullAddress.get(), true, "UTF-8", 0,
+ conformToStandard);
+
+ parm = PR_smprintf("From: %s" CRLF, convbuf ? convbuf : m_email.get());
+
+ rv = FormatStringFromName("MsgMdnMsgSentTo", NS_ConvertASCIItoUTF16(m_email),
+ firstPart1);
+ if (NS_FAILED(rv)) return rv;
+
+ PUSH_N_FREE_STRING(parm);
+
+ PR_Free(convbuf);
+
+ if (compUtils) {
+ nsCString msgId;
+ rv = compUtils->MsgGenerateMessageId(m_identity, ""_ns, msgId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ tmpBuffer = PR_smprintf("Message-ID: %s" CRLF, msgId.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+
+ nsString receipt_string;
+ switch (m_disposeType) {
+ case nsIMsgMdnGenerator::eDisplayed:
+ rv = GetStringFromName("MdnDisplayedReceipt", receipt_string);
+ break;
+ case nsIMsgMdnGenerator::eDispatched:
+ rv = GetStringFromName("MdnDispatchedReceipt", receipt_string);
+ break;
+ case nsIMsgMdnGenerator::eProcessed:
+ rv = GetStringFromName("MdnProcessedReceipt", receipt_string);
+ break;
+ case nsIMsgMdnGenerator::eDeleted:
+ rv = GetStringFromName("MdnDeletedReceipt", receipt_string);
+ break;
+ case nsIMsgMdnGenerator::eDenied:
+ rv = GetStringFromName("MdnDeniedReceipt", receipt_string);
+ break;
+ case nsIMsgMdnGenerator::eFailed:
+ rv = GetStringFromName("MdnFailedReceipt", receipt_string);
+ break;
+ default:
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ if (NS_FAILED(rv)) return rv;
+
+ receipt_string.AppendLiteral(" - ");
+
+ char* encodedReceiptString =
+ nsMsgI18NEncodeMimePartIIStr(NS_ConvertUTF16toUTF8(receipt_string).get(),
+ false, "UTF-8", 0, conformToStandard);
+
+ nsCString subject;
+ m_headers->ExtractHeader(HEADER_SUBJECT, false, subject);
+ convbuf = nsMsgI18NEncodeMimePartIIStr(
+ subject.Length() ? subject.get() : "[no subject]", false, "UTF-8", 0,
+ conformToStandard);
+ tmpBuffer = PR_smprintf(
+ "Subject: %s%s" CRLF, encodedReceiptString,
+ (convbuf ? convbuf
+ : (subject.Length() ? subject.get() : "[no subject]")));
+
+ PUSH_N_FREE_STRING(tmpBuffer);
+ PR_Free(convbuf);
+ PR_Free(encodedReceiptString);
+
+ convbuf = nsMsgI18NEncodeMimePartIIStr(m_dntRrt.get(), true, "UTF-8", 0,
+ conformToStandard);
+ tmpBuffer = PR_smprintf("To: %s" CRLF, convbuf ? convbuf : m_dntRrt.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ PR_Free(convbuf);
+
+ // *** This is not in the spec. I am adding this so we could do
+ // threading
+ m_headers->ExtractHeader(HEADER_MESSAGE_ID, false, m_messageId);
+
+ if (!m_messageId.IsEmpty()) {
+ if (*m_messageId.get() == '<')
+ tmpBuffer = PR_smprintf("References: %s" CRLF, m_messageId.get());
+ else
+ tmpBuffer = PR_smprintf("References: <%s>" CRLF, m_messageId.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+ tmpBuffer = PR_smprintf("%s" CRLF, "MIME-Version: 1.0");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf(
+ "Content-Type: multipart/report; \
+report-type=disposition-notification;\r\n\tboundary=\"%s\"" CRLF CRLF,
+ m_mimeSeparator.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("--%s" CRLF, m_mimeSeparator.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("Content-Type: text/plain; charset=UTF-8" CRLF);
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer =
+ PR_smprintf("Content-Transfer-Encoding: %s" CRLF CRLF, ENCODING_8BIT);
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ if (!firstPart1.IsEmpty()) {
+ tmpBuffer =
+ PR_smprintf("%s" CRLF CRLF, NS_ConvertUTF16toUTF8(firstPart1).get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+
+ switch (m_disposeType) {
+ case nsIMsgMdnGenerator::eDisplayed:
+ rv = GetStringFromName("MsgMdnDisplayed", firstPart2);
+ break;
+ case nsIMsgMdnGenerator::eDispatched:
+ rv = GetStringFromName("MsgMdnDispatched", firstPart2);
+ break;
+ case nsIMsgMdnGenerator::eProcessed:
+ rv = GetStringFromName("MsgMdnProcessed", firstPart2);
+ break;
+ case nsIMsgMdnGenerator::eDeleted:
+ rv = GetStringFromName("MsgMdnDeleted", firstPart2);
+ break;
+ case nsIMsgMdnGenerator::eDenied:
+ rv = GetStringFromName("MsgMdnDenied", firstPart2);
+ break;
+ case nsIMsgMdnGenerator::eFailed:
+ rv = GetStringFromName("MsgMdnFailed", firstPart2);
+ break;
+ default:
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ if (NS_FAILED(rv)) return rv;
+
+ if (!firstPart2.IsEmpty()) {
+ tmpBuffer =
+ PR_smprintf("%s" CRLF CRLF, NS_ConvertUTF16toUTF8(firstPart2).get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::CreateSecondPart() {
+ DEBUG_MDN("nsMsgMdnGenerator::CreateSecondPart");
+ char* tmpBuffer = nullptr;
+ char* convbuf = nullptr;
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompUtils> compUtils;
+ bool conformToStandard = false;
+
+ tmpBuffer = PR_smprintf("--%s" CRLF, m_mimeSeparator.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("%s" CRLF,
+ "Content-Type: message/disposition-notification; "
+ "name=\042MDNPart2.txt\042");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("%s" CRLF, "Content-Disposition: inline");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer =
+ PR_smprintf("Content-Transfer-Encoding: %s" CRLF CRLF, ENCODING_7BIT);
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ nsCOMPtr<nsIHttpProtocolHandler> pHTTPHandler =
+ do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http", &rv);
+ if (NS_SUCCEEDED(rv) && pHTTPHandler) {
+ bool sendUserAgent = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ prefBranch->GetBoolPref("mailnews.headers.sendUserAgent", &sendUserAgent);
+ }
+
+ if (sendUserAgent) {
+ bool useMinimalUserAgent = false;
+ if (prefBranch) {
+ prefBranch->GetBoolPref("mailnews.headers.useMinimalUserAgent",
+ &useMinimalUserAgent);
+ }
+ if (useMinimalUserAgent) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (bundleService) {
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties",
+ getter_AddRefs(brandBundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsString brandName;
+ brandBundle->GetStringFromName("brandFullName", brandName);
+ if (!brandName.IsEmpty()) {
+ NS_ConvertUTF16toUTF8 ua8(brandName);
+ tmpBuffer = PR_smprintf("Reporting-UA: %s" CRLF, ua8.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+ }
+ }
+ } else {
+ nsAutoCString userAgentString;
+ // Ignore error since we're testing the return value.
+ mozilla::Unused << pHTTPHandler->GetUserAgent(userAgentString);
+
+ if (!userAgentString.IsEmpty()) {
+ // Prepend the product name with the dns name according to RFC 3798.
+ char hostName[256];
+ PR_GetSystemInfo(PR_SI_HOSTNAME_UNTRUNCATED, hostName,
+ sizeof hostName);
+ if ((hostName[0] != '\0') && (strchr(hostName, '.') != NULL)) {
+ userAgentString.InsertLiteral("; ", 0);
+ userAgentString.Insert(nsDependentCString(hostName), 0);
+ }
+
+ tmpBuffer =
+ PR_smprintf("Reporting-UA: %s" CRLF, userAgentString.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+ }
+ }
+ }
+
+ nsCString originalRecipient;
+ m_headers->ExtractHeader(HEADER_ORIGINAL_RECIPIENT, false, originalRecipient);
+
+ if (!originalRecipient.IsEmpty()) {
+ tmpBuffer =
+ PR_smprintf("Original-Recipient: %s" CRLF, originalRecipient.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+ }
+
+ compUtils = do_GetService("@mozilla.org/messengercompose/computils;1", &rv);
+ if (compUtils) compUtils->GetMsgMimeConformToStandard(&conformToStandard);
+
+ convbuf = nsMsgI18NEncodeMimePartIIStr(m_email.get(), true, "UTF-8", 0,
+ conformToStandard);
+ tmpBuffer = PR_smprintf("Final-Recipient: rfc822;%s" CRLF,
+ convbuf ? convbuf : m_email.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ PR_Free(convbuf);
+
+ if (*m_messageId.get() == '<')
+ tmpBuffer = PR_smprintf("Original-Message-ID: %s" CRLF, m_messageId.get());
+ else
+ tmpBuffer =
+ PR_smprintf("Original-Message-ID: <%s>" CRLF, m_messageId.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer =
+ PR_smprintf("Disposition: %s/%s; %s" CRLF CRLF,
+ (m_autoAction ? "automatic-action" : "manual-action"),
+ (m_autoSend ? "MDN-sent-automatically" : "MDN-sent-manually"),
+ DispositionTypes[(int)m_disposeType]);
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::CreateThirdPart() {
+ DEBUG_MDN("nsMsgMdnGenerator::CreateThirdPart");
+ char* tmpBuffer = nullptr;
+ nsresult rv = NS_OK;
+
+ tmpBuffer = PR_smprintf("--%s" CRLF, m_mimeSeparator.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf(
+ "%s" CRLF,
+ "Content-Type: text/rfc822-headers; name=\042MDNPart3.txt\042");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("%s" CRLF, "Content-Transfer-Encoding: 7bit");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ tmpBuffer = PR_smprintf("%s" CRLF CRLF, "Content-Disposition: inline");
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ rv = OutputAllHeaders();
+
+ if (NS_FAILED(rv)) return rv;
+
+ rv = WriteString(CRLF);
+ if (NS_FAILED(rv)) return rv;
+
+ tmpBuffer = PR_smprintf("--%s--" CRLF, m_mimeSeparator.get());
+ PUSH_N_FREE_STRING(tmpBuffer);
+
+ return rv;
+}
+
+nsresult nsMsgMdnGenerator::OutputAllHeaders() {
+ DEBUG_MDN("nsMsgMdnGenerator::OutputAllHeaders");
+ nsCString all_headers;
+ int32_t all_headers_size = 0;
+ nsresult rv = NS_OK;
+
+ rv = m_headers->GetAllHeaders(all_headers);
+ if (NS_FAILED(rv)) return rv;
+ all_headers_size = all_headers.Length();
+ char *buf = (char*)all_headers.get(),
+ *buf_end = (char*)all_headers.get() + all_headers_size;
+ char *start = buf, *end = buf;
+
+ while (buf < buf_end) {
+ switch (*buf) {
+ case 0:
+ if (*(buf + 1) == '\n') {
+ // *buf = '\r';
+ end = buf;
+ } else if (*(buf + 1) == 0) {
+ // the case of message id
+ *buf = '>';
+ }
+ break;
+ case '\r':
+ end = buf;
+ *buf = 0;
+ break;
+ case '\n':
+ if (buf > start && *(buf - 1) == 0) {
+ start = buf + 1;
+ end = start;
+ } else {
+ end = buf;
+ }
+ *buf = 0;
+ break;
+ default:
+ break;
+ }
+ buf++;
+
+ if (end > start && *end == 0) {
+ // strip out private X-Mozilla-Status header & X-Mozilla-Draft-Info &&
+ // envelope header
+ if (!PL_strncasecmp(start, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN) ||
+ !PL_strncasecmp(start, X_MOZILLA_DRAFT_INFO,
+ X_MOZILLA_DRAFT_INFO_LEN) ||
+ !PL_strncasecmp(start, "From ", 5)) {
+ while (end < buf_end && (*end == '\n' || *end == '\r' || *end == 0))
+ end++;
+ start = end;
+ } else {
+ NS_ASSERTION(*end == 0, "content of end should be null");
+ rv = WriteString(start);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = WriteString(CRLF);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (end < buf_end && (*end == '\n' || *end == '\r' || *end == 0))
+ end++;
+ start = end;
+ }
+ buf = start;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgMdnGenerator::SendMdnMsg() {
+ DEBUG_MDN("nsMsgMdnGenerator::SendMdnMsg");
+ nsresult rv;
+ nsCOMPtr<nsISmtpService> smtpService =
+ do_GetService("@mozilla.org/messengercompose/smtp;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> aUri;
+ nsCOMPtr<nsIRequest> aRequest;
+ nsCString identEmail;
+ m_identity->GetEmail(identEmail);
+ smtpService->SendMailMessage(m_file, m_dntRrt.get(), m_identity,
+ identEmail.get(), EmptyString(), this, nullptr,
+ nullptr, false, ""_ns, getter_AddRefs(aUri),
+ getter_AddRefs(aRequest));
+
+ return NS_OK;
+}
+
+nsresult nsMsgMdnGenerator::WriteString(const char* str) {
+ NS_ENSURE_ARG(str);
+ uint32_t len = strlen(str);
+ uint32_t wLen = 0;
+
+ return m_outputStream->Write(str, len, &wLen);
+}
+
+nsresult nsMsgMdnGenerator::InitAndProcess(bool* needToAskUser) {
+ DEBUG_MDN("nsMsgMdnGenerator::InitAndProcess");
+ nsresult rv = m_folder->GetServer(getter_AddRefs(m_server));
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (accountManager && m_server) {
+ if (!m_identity) {
+ // check if this is a message delivered to the global inbox,
+ // in which case we find the originating account's identity.
+ nsCString accountKey;
+ m_headers->ExtractHeader(HEADER_X_MOZILLA_ACCOUNT_KEY, false, accountKey);
+ nsCOMPtr<nsIMsgAccount> account;
+ if (!accountKey.IsEmpty())
+ accountManager->GetAccount(accountKey, getter_AddRefs(account));
+ if (account) account->GetIncomingServer(getter_AddRefs(m_server));
+
+ if (m_server) {
+ // Find the correct identity based on the "To:" and "Cc:" header
+ nsCString mailTo;
+ nsCString mailCC;
+ m_headers->ExtractHeader(HEADER_TO, true, mailTo);
+ m_headers->ExtractHeader(HEADER_CC, true, mailCC);
+ nsTArray<RefPtr<nsIMsgIdentity>> servIdentities;
+ accountManager->GetIdentitiesForServer(m_server, servIdentities);
+
+ // First check in the "To:" header
+ for (auto ident : servIdentities) {
+ nsCString identEmail;
+ ident->GetEmail(identEmail);
+ if (!mailTo.IsEmpty() && !identEmail.IsEmpty() &&
+ FindInReadable(identEmail, mailTo,
+ nsCaseInsensitiveCStringComparator)) {
+ m_identity = ident;
+ break;
+ }
+ }
+ // If no match, check the "Cc:" header
+ if (!m_identity) {
+ for (auto ident : servIdentities) {
+ nsCString identEmail;
+ ident->GetEmail(identEmail);
+ if (!mailCC.IsEmpty() && !identEmail.IsEmpty() &&
+ FindInReadable(identEmail, mailCC,
+ nsCaseInsensitiveCStringComparator)) {
+ m_identity = ident;
+ break;
+ }
+ }
+ }
+
+ // If still no match, use the first identity
+ if (!m_identity) {
+ rv = accountManager->GetFirstIdentityForServer(
+ m_server, getter_AddRefs(m_identity));
+ }
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_identity) {
+ bool useCustomPrefs = false;
+ m_identity->GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ if (useCustomPrefs) {
+ bool bVal = false;
+ m_server->GetBoolValue("mdn_report_enabled", &bVal);
+ m_mdnEnabled = bVal;
+ m_server->GetIntValue("mdn_not_in_to_cc", &m_notInToCcOp);
+ m_server->GetIntValue("mdn_outside_domain", &m_outsideDomainOp);
+ m_server->GetIntValue("mdn_other", &m_otherOp);
+ } else {
+ bool bVal = false;
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ if (prefBranch) {
+ prefBranch->GetBoolPref("mail.mdn.report.enabled", &bVal);
+ m_mdnEnabled = bVal;
+ prefBranch->GetIntPref("mail.mdn.report.not_in_to_cc",
+ &m_notInToCcOp);
+ prefBranch->GetIntPref("mail.mdn.report.outside_domain",
+ &m_outsideDomainOp);
+ prefBranch->GetIntPref("mail.mdn.report.other", &m_otherOp);
+ }
+ }
+ }
+ }
+
+ if (m_mdnEnabled) {
+ m_headers->ExtractHeader(HEADER_DISPOSITION_NOTIFICATION_TO, false,
+ m_dntRrt);
+ if (m_dntRrt.IsEmpty())
+ m_headers->ExtractHeader(HEADER_RETURN_RECEIPT_TO, false, m_dntRrt);
+ if (!m_dntRrt.IsEmpty() && ProcessSendMode() && ValidateReturnPath()) {
+ if (!m_autoSend) {
+ *needToAskUser = true;
+ rv = NS_OK;
+ } else {
+ *needToAskUser = false;
+ rv = UserAgreed();
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMdnGenerator::Process(EDisposeType type,
+ nsIMsgWindow* aWindow,
+ nsIMsgFolder* folder, nsMsgKey key,
+ nsIMimeHeaders* headers,
+ bool autoAction, bool* _retval) {
+ DEBUG_MDN("nsMsgMdnGenerator::Process");
+ NS_ENSURE_ARG_POINTER(folder);
+ NS_ENSURE_ARG_POINTER(headers);
+ NS_ENSURE_ARG_POINTER(aWindow);
+ NS_ENSURE_TRUE(key != nsMsgKey_None, NS_ERROR_INVALID_ARG);
+ m_disposeType = type;
+ m_autoAction = autoAction;
+ m_window = aWindow;
+ m_folder = folder;
+ m_headers = headers;
+ m_key = key;
+
+ mozilla::DebugOnly<nsresult> rv = InitAndProcess(_retval);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "InitAndProcess failed");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMdnGenerator::UserAgreed() {
+ DEBUG_MDN("nsMsgMdnGenerator::UserAgreed");
+ (void)NoteMDNRequestHandled();
+ return CreateMdnMsg();
+}
+
+NS_IMETHODIMP nsMsgMdnGenerator::UserDeclined() {
+ DEBUG_MDN("nsMsgMdnGenerator::UserDeclined");
+ return NoteMDNRequestHandled();
+}
+
+/**
+ * Set/clear flags appropriately so we won't ask user again about MDN
+ * request for this message.
+ */
+nsresult nsMsgMdnGenerator::NoteMDNRequestHandled() {
+ nsresult rv = StoreMDNSentFlag(m_folder, m_key);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "StoreMDNSentFlag failed");
+ rv = ClearMDNNeededFlag(m_folder, m_key);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "ClearMDNNeededFlag failed");
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMdnGenerator::OnStartRunningUrl(nsIURI* url) {
+ DEBUG_MDN("nsMsgMdnGenerator::OnStartRunningUrl");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMdnGenerator::OnStopRunningUrl(nsIURI* url,
+ nsresult aExitCode) {
+ nsresult rv;
+
+ DEBUG_MDN("nsMsgMdnGenerator::OnStopRunningUrl");
+ if (m_file) m_file->Remove(false);
+
+ if (NS_SUCCEEDED(aExitCode)) return NS_OK;
+
+ const char* exitString;
+
+ switch (aExitCode) {
+ case NS_ERROR_UNKNOWN_HOST:
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ exitString = "smtpSendFailedUnknownServer";
+ break;
+ case NS_ERROR_CONNECTION_REFUSED:
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ exitString = "smtpSendRequestRefused";
+ break;
+ case NS_ERROR_NET_INTERRUPT:
+ case NS_ERROR_ABORT: // we have no proper string for error code
+ // NS_ERROR_ABORT in compose bundle
+ exitString = "smtpSendInterrupted";
+ break;
+ case NS_ERROR_NET_TIMEOUT:
+ case NS_ERROR_NET_RESET:
+ exitString = "smtpSendTimeout";
+ break;
+ default:
+ exitString = errorStringNameForErrorCode(aExitCode);
+ break;
+ }
+
+ nsCOMPtr<nsISmtpService> smtpService(
+ do_GetService("@mozilla.org/messengercompose/smtp;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the smtp hostname and format the string.
+ nsCString smtpHostName;
+ nsCOMPtr<nsISmtpServer> smtpServer;
+ rv = smtpService->GetServerByIdentity(m_identity, getter_AddRefs(smtpServer));
+ if (NS_SUCCEEDED(rv)) smtpServer->GetHostname(smtpHostName);
+
+ AutoTArray<nsString, 1> params;
+ CopyASCIItoUTF16(smtpHostName, *params.AppendElement());
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString failed_msg, dialogTitle;
+
+ bundle->FormatStringFromName(exitString, params, failed_msg);
+ bundle->GetStringFromName("sendMessageErrorTitle", dialogTitle);
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ m_window->GetDomWindow(getter_AddRefs(domWindow));
+
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dlgService->Alert(domWindow, dialogTitle.get(), failed_msg.get());
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.h b/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.h
new file mode 100644
index 0000000000..74eea8bcad
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/nsMsgMdnGenerator.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgMdnGenerator_H_
+#define _nsMsgMdnGenerator_H_
+
+#include "nsIMsgMdnGenerator.h"
+#include "nsCOMPtr.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIOutputStream.h"
+#include "nsIFile.h"
+#include "nsIMsgIdentity.h"
+#include "nsIMsgWindow.h"
+#include "nsIMimeHeaders.h"
+#include "nsString.h"
+#include "MailNewsTypes2.h"
+
+#define eNeverSendOp ((int32_t)0)
+#define eAutoSendOp ((int32_t)1)
+#define eAskMeOp ((int32_t)2)
+#define eDeniedOp ((int32_t)3)
+
+class nsMsgMdnGenerator : public nsIMsgMdnGenerator, public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGMDNGENERATOR
+ NS_DECL_NSIURLLISTENER
+
+ nsMsgMdnGenerator();
+
+ private:
+ virtual ~nsMsgMdnGenerator();
+
+ // Sanity Check methods
+ bool ProcessSendMode(); // must called prior ValidateReturnPath
+ bool ValidateReturnPath();
+ bool NotInToOrCc();
+ bool MailAddrMatch(const char* addr1, const char* addr2);
+
+ nsresult StoreMDNSentFlag(nsIMsgFolder* folder, nsMsgKey key);
+ nsresult ClearMDNNeededFlag(nsIMsgFolder* folder, nsMsgKey key);
+ nsresult NoteMDNRequestHandled();
+
+ nsresult CreateMdnMsg();
+ nsresult CreateFirstPart();
+ nsresult CreateSecondPart();
+ nsresult CreateThirdPart();
+ nsresult SendMdnMsg();
+
+ // string bundle helper methods
+ nsresult GetStringFromName(const char* aName, nsAString& aResultString);
+ nsresult FormatStringFromName(const char* aName, const nsString& aString,
+ nsAString& aResultString);
+
+ // other helper methods
+ nsresult InitAndProcess(bool* needToAskUser);
+ nsresult OutputAllHeaders();
+ nsresult WriteString(const char* str);
+
+ private:
+ EDisposeType m_disposeType;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ nsCOMPtr<nsIFile> m_file;
+ nsCOMPtr<nsIMsgIdentity> m_identity;
+ nsMsgKey m_key;
+ nsCString m_email;
+ nsCString m_mimeSeparator;
+ nsCString m_messageId;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgIncomingServer> m_server;
+ nsCOMPtr<nsIMimeHeaders> m_headers;
+ nsCString m_dntRrt;
+ int32_t m_notInToCcOp;
+ int32_t m_outsideDomainOp;
+ int32_t m_otherOp;
+ bool m_reallySendMdn;
+ bool m_autoSend;
+ bool m_autoAction;
+ bool m_mdnEnabled;
+};
+
+#endif // _nsMsgMdnGenerator_H_
diff --git a/comm/mailnews/extensions/mdn/test/unit/head_mdn.js b/comm/mailnews/extensions/mdn/test/unit/head_mdn.js
new file mode 100644
index 0000000000..0735341e30
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/test/unit/head_mdn.js
@@ -0,0 +1,18 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+registerCleanupFunction(function () {
+ load("../../../../../mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/extensions/mdn/test/unit/test_askuser.js b/comm/mailnews/extensions/mdn/test/unit/test_askuser.js
new file mode 100644
index 0000000000..b30ab49b0f
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/test/unit/test_askuser.js
@@ -0,0 +1,67 @@
+localAccountUtils.loadLocalMailAccount();
+
+var localAccount = MailServices.accounts.FindAccountForServer(
+ localAccountUtils.incomingServer
+);
+var identity = MailServices.accounts.createIdentity();
+identity.email = "bob@t2.example.net";
+localAccount.addIdentity(identity);
+localAccount.defaultIdentity = identity;
+
+function run_test() {
+ var headers =
+ "from: alice@t1.example.com\r\n" +
+ "to: bob@t2.example.net\r\n" +
+ "return-path: alice@t1.example.com\r\n" +
+ "Disposition-Notification-To: alice@t1.example.com\r\n";
+
+ let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ mimeHdr.initialize(headers);
+ let receivedHeader = mimeHdr.extractHeader("To", false);
+ dump(receivedHeader + "\n");
+
+ localAccountUtils.inboxFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ localAccountUtils.inboxFolder.addMessage(
+ "From \r\n" + headers + "\r\nhello\r\n"
+ );
+ // Need to setup some prefs
+ Services.prefs.setBoolPref("mail.mdn.report.enabled", true);
+ Services.prefs.setIntPref("mail.mdn.report.not_in_to_cc", 2);
+ Services.prefs.setIntPref("mail.mdn.report.other", 2);
+ Services.prefs.setIntPref("mail.mdn.report.outside_domain", 2);
+
+ var msgFolder = localAccountUtils.inboxFolder;
+
+ var msgWindow = {};
+
+ var msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ // Everything looks good so far, let's generate the MDN response.
+ var mdnGenerator = Cc[
+ "@mozilla.org/messenger-mdn/generator;1"
+ ].createInstance(Ci.nsIMsgMdnGenerator);
+
+ Services.prefs.setIntPref("mail.mdn.report.outside_domain", 1);
+ var askUser = mdnGenerator.process(
+ Ci.nsIMsgMdnGenerator.eDisplayed,
+ msgWindow,
+ msgFolder,
+ msgHdr.messageKey,
+ mimeHdr,
+ false
+ );
+ Assert.ok(!askUser);
+
+ Services.prefs.setIntPref("mail.mdn.report.outside_domain", 2);
+ askUser = mdnGenerator.process(
+ Ci.nsIMsgMdnGenerator.eDisplayed,
+ msgWindow,
+ msgFolder,
+ msgHdr.messageKey,
+ mimeHdr,
+ false
+ );
+ Assert.ok(askUser);
+}
diff --git a/comm/mailnews/extensions/mdn/test/unit/test_mdnFlags.js b/comm/mailnews/extensions/mdn/test/unit/test_mdnFlags.js
new file mode 100644
index 0000000000..4d6094e89d
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/test/unit/test_mdnFlags.js
@@ -0,0 +1,60 @@
+/**
+ * This tests that setting mdn flags works correctly, so that we don't
+ * reprompt when the user re-selects a message.
+ */
+
+localAccountUtils.loadLocalMailAccount();
+
+var localAccount = MailServices.accounts.FindAccountForServer(
+ localAccountUtils.incomingServer
+);
+var identity = MailServices.accounts.createIdentity();
+identity.email = "bob@t2.example.net";
+localAccount.addIdentity(identity);
+localAccount.defaultIdentity = identity;
+
+function run_test() {
+ var headers =
+ "from: alice@t1.example.com\r\n" +
+ "to: bob@t2.example.net\r\n" +
+ "return-path: alice@t1.example.com\r\n" +
+ "Disposition-Notification-To: alice@t1.example.com\r\n";
+
+ let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ mimeHdr.initialize(headers);
+ mimeHdr.extractHeader("To", false);
+
+ localAccountUtils.inboxFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ localAccountUtils.inboxFolder.addMessage(
+ "From \r\n" + headers + "\r\nhello\r\n"
+ );
+ // Need to setup some prefs
+ Services.prefs.setBoolPref("mail.mdn.report.enabled", true);
+ Services.prefs.setIntPref("mail.mdn.report.not_in_to_cc", 2);
+ Services.prefs.setIntPref("mail.mdn.report.other", 2);
+ Services.prefs.setIntPref("mail.mdn.report.outside_domain", 2);
+
+ var msgFolder = localAccountUtils.inboxFolder;
+
+ var msgWindow = {};
+
+ var msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ // Everything looks good so far, let's generate the MDN response.
+ var mdnGenerator = Cc[
+ "@mozilla.org/messenger-mdn/generator;1"
+ ].createInstance(Ci.nsIMsgMdnGenerator);
+ mdnGenerator.process(
+ Ci.nsIMsgMdnGenerator.eDisplayed,
+ msgWindow,
+ msgFolder,
+ msgHdr.messageKey,
+ mimeHdr,
+ false
+ );
+ mdnGenerator.userDeclined();
+ Assert.notEqual(msgHdr.flags & Ci.nsMsgMessageFlags.MDNReportSent, 0);
+ Assert.equal(msgHdr.flags & Ci.nsMsgMessageFlags.MDNReportNeeded, 0);
+}
diff --git a/comm/mailnews/extensions/mdn/test/unit/xpcshell.ini b/comm/mailnews/extensions/mdn/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..243145b676
--- /dev/null
+++ b/comm/mailnews/extensions/mdn/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head_mdn.js
+tail =
+
+[test_askuser.js]
+[test_mdnFlags.js]
diff --git a/comm/mailnews/extensions/moz.build b/comm/mailnews/extensions/moz.build
new file mode 100644
index 0000000000..2f56ec9d2e
--- /dev/null
+++ b/comm/mailnews/extensions/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+# These extensions are not optional.
+DIRS += [
+ "mdn",
+ "mailviews",
+ "bayesian-spam-filter",
+ "offline-startup",
+ "newsblog",
+ "fts3",
+ "smime",
+]
diff --git a/comm/mailnews/extensions/newsblog/.eslintrc.js b/comm/mailnews/extensions/newsblog/.eslintrc.js
new file mode 100644
index 0000000000..8254a84aaa
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/.eslintrc.js
@@ -0,0 +1,18 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ // Enforce valid JSDoc comments.
+ "valid-jsdoc": [
+ "error",
+ {
+ prefer: { return: "returns" },
+ preferType: {
+ map: "Map",
+ set: "Set",
+ date: "Date",
+ },
+ },
+ ],
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/Feed.jsm b/comm/mailnews/extensions/newsblog/Feed.jsm
new file mode 100644
index 0000000000..8b899308d5
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/Feed.jsm
@@ -0,0 +1,700 @@
+/* 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 = ["Feed"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedParser",
+ "resource:///modules/FeedParser.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedUtils",
+ "resource:///modules/FeedUtils.jsm"
+);
+
+// Cache for all of the feeds currently being downloaded, indexed by URL,
+// so the load event listener can access the Feed objects after it finishes
+// downloading the feed.
+var FeedCache = {
+ mFeeds: {},
+
+ putFeed(aFeed) {
+ this.mFeeds[this.normalizeHost(aFeed.url)] = aFeed;
+ },
+
+ getFeed(aUrl) {
+ let index = this.normalizeHost(aUrl);
+ if (index in this.mFeeds) {
+ return this.mFeeds[index];
+ }
+
+ return null;
+ },
+
+ removeFeed(aUrl) {
+ let index = this.normalizeHost(aUrl);
+ if (index in this.mFeeds) {
+ delete this.mFeeds[index];
+ }
+ },
+
+ normalizeHost(aUrl) {
+ try {
+ let normalizedUrl = Services.io.newURI(aUrl);
+ let newHost = normalizedUrl.host.toLowerCase();
+ normalizedUrl = normalizedUrl.mutate().setHost(newHost).finalize();
+ return normalizedUrl.spec;
+ } catch (ex) {
+ return aUrl;
+ }
+ },
+};
+
+/**
+ * A Feed object. If aFolder is the account root folder, a new subfolder
+ * for the feed url is created otherwise the url will be subscribed to the
+ * existing aFolder, upon successful download() completion.
+ *
+ * @class
+ * @param {string} aFeedUrl - feed url.
+ * @param {nsIMsgFolder} aFolder - folder containing or to contain the feed
+ * subscription.
+ */
+function Feed(aFeedUrl, aFolder) {
+ this.url = aFeedUrl;
+ this.server = aFolder.server;
+ if (!aFolder.isServer) {
+ this.mFolder = aFolder;
+ }
+}
+
+Feed.prototype = {
+ url: null,
+ description: null,
+ author: null,
+ request: null,
+ server: null,
+ downloadCallback: null,
+ resource: null,
+ itemsToStore: [],
+ itemsStored: 0,
+ fileSize: 0,
+ mFolder: null,
+ mInvalidFeed: false,
+ mFeedType: null,
+ mLastModified: null,
+
+ get folder() {
+ return this.mFolder;
+ },
+
+ set folder(aFolder) {
+ this.mFolder = aFolder;
+ },
+
+ get name() {
+ // Used for the feed's title in Subscribe dialog and opml export.
+ let name = this.title || this.description || this.url;
+ /* eslint-disable-next-line no-control-regex */
+ return name.replace(/[\n\r\t]+/g, " ").replace(/[\x00-\x1F]+/g, "");
+ },
+
+ get folderName() {
+ if (this.mFolderName) {
+ return this.mFolderName;
+ }
+
+ // Get a unique sanitized name. Use title or description as a base;
+ // these are mandatory by spec. Length of 80 is plenty.
+ let folderName = (this.title || this.description || "").substr(0, 80);
+ let defaultName =
+ lazy.FeedUtils.strings.GetStringFromName("ImportFeedsNew");
+ return (this.mFolderName = lazy.FeedUtils.getSanitizedFolderName(
+ this.server.rootMsgFolder,
+ folderName,
+ defaultName,
+ true
+ ));
+ },
+
+ download(aParseItems, aCallback) {
+ // May be null.
+ this.downloadCallback = aCallback;
+
+ // Whether or not to parse items when downloading and parsing the feed.
+ // Defaults to true, but setting to false is useful for obtaining
+ // just the title of the feed when the user subscribes to it.
+ this.parseItems = aParseItems == null || aParseItems;
+
+ // Before we do anything, make sure the url is an http url. This is just
+ // a sanity check so we don't try opening mailto urls, imap urls, etc. that
+ // the user may have tried to subscribe to as an rss feed.
+ if (!lazy.FeedUtils.isValidScheme(this.url)) {
+ // Simulate an invalid feed error.
+ lazy.FeedUtils.log.info(
+ "Feed.download: invalid protocol for - " + this.url
+ );
+ this.onParseError(this);
+ return;
+ }
+
+ // Before we try to download the feed, make sure we aren't already
+ // processing the feed by looking up the url in our feed cache.
+ if (FeedCache.getFeed(this.url)) {
+ if (this.downloadCallback) {
+ this.downloadCallback.downloaded(
+ this,
+ lazy.FeedUtils.kNewsBlogFeedIsBusy
+ );
+ }
+
+ // Return, the feed is already in use.
+ return;
+ }
+
+ if (Services.io.offline) {
+ // If offline and don't want to go online, just add the feed subscription;
+ // it can be verified later (the folder name will be the url if not adding
+ // to an existing folder). Only for subscribe actions; passive biff and
+ // active get new messages are handled prior to getting here.
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!win.MailOfflineMgr.getNewMail()) {
+ this.storeNextItem();
+ return;
+ }
+ }
+
+ this.request = new XMLHttpRequest();
+ // Must set onProgress before calling open.
+ this.request.onprogress = this.onProgress;
+ this.request.open("GET", this.url, true);
+ this.request.channel.loadFlags |=
+ Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+
+ // Some servers, if sent If-Modified-Since, will send 304 if subsequently
+ // not sent If-Modified-Since, as in the case of an unsubscribe and new
+ // subscribe. Send start of century date to force a download; some servers
+ // will 304 on older dates (such as epoch 1970).
+ let lastModified = this.lastModified || "Sat, 01 Jan 2000 00:00:00 GMT";
+ this.request.setRequestHeader("If-Modified-Since", lastModified);
+
+ // Only order what you're going to eat...
+ this.request.responseType = "document";
+ this.request.overrideMimeType("text/xml");
+ this.request.setRequestHeader("Accept", lazy.FeedUtils.REQUEST_ACCEPT);
+ this.request.timeout = lazy.FeedUtils.REQUEST_TIMEOUT;
+ this.request.onload = this.onDownloaded;
+ this.request.onreadystatechange = this.onReadyStateChange;
+ this.request.onerror = this.onDownloadError;
+ this.request.ontimeout = this.onDownloadError;
+ FeedCache.putFeed(this);
+ this.request.send(null);
+ },
+
+ onReadyStateChange(aEvent) {
+ // Once a server responds with data, reset the timeout to allow potentially
+ // large files to complete the download.
+ let request = aEvent.target;
+ if (request.timeout && request.readyState == request.LOADING) {
+ request.timeout = 0;
+ }
+ },
+
+ onDownloaded(aEvent) {
+ let request = aEvent.target;
+ let isHttp = request.channel.originalURI.scheme.startsWith("http");
+ let url = request.channel.originalURI.spec;
+ if (isHttp && (request.status < 200 || request.status >= 300)) {
+ Feed.prototype.onDownloadError(aEvent);
+ return;
+ }
+
+ lazy.FeedUtils.log.debug(
+ "Feed.onDownloaded: got a download, fileSize:url - " +
+ aEvent.loaded +
+ " : " +
+ url
+ );
+ let feed = FeedCache.getFeed(url);
+ if (!feed) {
+ throw new Error(
+ "Feed.onDownloaded: error - couldn't retrieve feed from cache"
+ );
+ }
+
+ // If the server sends a Last-Modified header, store the property on the
+ // feed so we can use it when making future requests, to avoid downloading
+ // and parsing feeds that have not changed. Don't update if merely checking
+ // the url, as for subscribe move/copy, as a subsequent refresh may get a 304.
+ // Save the response and persist it only upon successful completion of the
+ // refresh cycle (i.e. not if the request is cancelled).
+ let lastModifiedHeader = request.getResponseHeader("Last-Modified");
+ feed.mLastModified =
+ lastModifiedHeader && feed.parseItems ? lastModifiedHeader : null;
+
+ feed.fileSize = aEvent.loaded;
+
+ // The download callback is called asynchronously when parse() is done.
+ feed.parse();
+ },
+
+ onProgress(aEvent) {
+ let request = aEvent.target;
+ let url = request.channel.originalURI.spec;
+ let feed = FeedCache.getFeed(url);
+
+ if (feed.downloadCallback) {
+ feed.downloadCallback.onProgress(
+ feed,
+ aEvent.loaded,
+ aEvent.total,
+ aEvent.lengthComputable
+ );
+ }
+ },
+
+ onDownloadError(aEvent) {
+ let request = aEvent.target;
+ let url = request.channel.originalURI.spec;
+ let feed = FeedCache.getFeed(url);
+ if (feed.downloadCallback) {
+ // Generic network or 'not found' error initially.
+ let error = lazy.FeedUtils.kNewsBlogRequestFailure;
+ // Certain errors should disable the feed.
+ let disable = false;
+
+ if (request.status == 304) {
+ // If the http status code is 304, the feed has not been modified
+ // since we last downloaded it and does not need to be parsed.
+ error = lazy.FeedUtils.kNewsBlogNoNewItems;
+ } else {
+ let [errType, errName] =
+ lazy.FeedUtils.createTCPErrorFromFailedXHR(request);
+ lazy.FeedUtils.log.info(
+ "Feed.onDownloaded: request errType:errName:statusCode - " +
+ errType +
+ ":" +
+ errName +
+ ":" +
+ request.status
+ );
+ if (errType == "SecurityCertificate") {
+ // This is the code for nsINSSErrorsService.ERROR_CLASS_BAD_CERT
+ // overridable security certificate errors.
+ error = lazy.FeedUtils.kNewsBlogBadCertError;
+ }
+
+ if (request.status == 401 || request.status == 403) {
+ // Unauthorized or Forbidden.
+ error = lazy.FeedUtils.kNewsBlogNoAuthError;
+ }
+
+ if (
+ request.status != 0 ||
+ error == lazy.FeedUtils.kNewsBlogBadCertError ||
+ errName == "DomainNotFoundError"
+ ) {
+ disable = true;
+ }
+ }
+
+ feed.downloadCallback.downloaded(feed, error, disable);
+ }
+
+ FeedCache.removeFeed(url);
+ },
+
+ onParseError(aFeed) {
+ if (!aFeed) {
+ return;
+ }
+
+ aFeed.mInvalidFeed = true;
+ if (aFeed.downloadCallback) {
+ aFeed.downloadCallback.downloaded(
+ aFeed,
+ lazy.FeedUtils.kNewsBlogInvalidFeed,
+ true
+ );
+ }
+
+ FeedCache.removeFeed(aFeed.url);
+ },
+
+ onUrlChange(aFeed, aOldUrl) {
+ if (!aFeed) {
+ return;
+ }
+
+ // Simulate a cancel after a url update; next cycle will check the new url.
+ aFeed.mInvalidFeed = true;
+ if (aFeed.downloadCallback) {
+ aFeed.downloadCallback.downloaded(aFeed, lazy.FeedUtils.kNewsBlogCancel);
+ }
+
+ FeedCache.removeFeed(aOldUrl);
+ },
+
+ // nsIUrlListener methods for getDatabaseWithReparse().
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ if (Components.isSuccessCode(aExitCode)) {
+ lazy.FeedUtils.log.debug(
+ "Feed.OnStopRunningUrl: rebuilt msgDatabase for " +
+ this.folder.name +
+ " - " +
+ this.folder.filePath.path
+ );
+ } else {
+ lazy.FeedUtils.log.error(
+ "Feed.OnStopRunningUrl: rebuild msgDatabase failed, " +
+ "error " +
+ aExitCode +
+ ", for " +
+ this.folder.name +
+ " - " +
+ this.folder.filePath.path
+ );
+ }
+ // Continue.
+ this.storeNextItem();
+ },
+
+ get title() {
+ return lazy.FeedUtils.getSubscriptionAttr(
+ this.url,
+ this.server,
+ "title",
+ ""
+ );
+ },
+
+ set title(aNewTitle) {
+ if (!aNewTitle) {
+ return;
+ }
+ lazy.FeedUtils.setSubscriptionAttr(
+ this.url,
+ this.server,
+ "title",
+ aNewTitle
+ );
+ },
+
+ get lastModified() {
+ return lazy.FeedUtils.getSubscriptionAttr(
+ this.url,
+ this.server,
+ "lastModified",
+ ""
+ );
+ },
+
+ set lastModified(aLastModified) {
+ lazy.FeedUtils.setSubscriptionAttr(
+ this.url,
+ this.server,
+ "lastModified",
+ aLastModified
+ );
+ },
+
+ get quickMode() {
+ let defaultValue = this.server.getBoolValue("quickMode");
+ return lazy.FeedUtils.getSubscriptionAttr(
+ this.url,
+ this.server,
+ "quickMode",
+ defaultValue
+ );
+ },
+
+ set quickMode(aNewQuickMode) {
+ lazy.FeedUtils.setSubscriptionAttr(
+ this.url,
+ this.server,
+ "quickMode",
+ aNewQuickMode
+ );
+ },
+
+ get options() {
+ let options = lazy.FeedUtils.getSubscriptionAttr(
+ this.url,
+ this.server,
+ "options",
+ null
+ );
+ if (options && options.version == lazy.FeedUtils._optionsDefault.version) {
+ return options;
+ }
+
+ let newOptions = lazy.FeedUtils.newOptions(options);
+ this.options = newOptions;
+ return newOptions;
+ },
+
+ set options(aOptions) {
+ let newOptions = aOptions ? aOptions : lazy.FeedUtils.optionsTemplate;
+ lazy.FeedUtils.setSubscriptionAttr(
+ this.url,
+ this.server,
+ "options",
+ newOptions
+ );
+ },
+
+ get link() {
+ return lazy.FeedUtils.getSubscriptionAttr(
+ this.url,
+ this.server,
+ "link",
+ ""
+ );
+ },
+
+ set link(aNewLink) {
+ if (!aNewLink) {
+ return;
+ }
+ lazy.FeedUtils.setSubscriptionAttr(this.url, this.server, "link", aNewLink);
+ },
+
+ parse() {
+ // Create a feed parser which will parse the feed.
+ let parser = new lazy.FeedParser();
+ this.itemsToStore = parser.parseFeed(this, this.request.responseXML);
+ parser = null;
+
+ if (this.mInvalidFeed) {
+ this.request = null;
+ this.mInvalidFeed = false;
+ return;
+ }
+
+ this.itemsToStoreIndex = 0;
+ this.itemsStored = 0;
+
+ // At this point, if we have items to potentially store and an existing
+ // folder, ensure the folder's msgDatabase is openable for new message
+ // processing. If not, reparse with an async nsIUrlListener |this| to
+ // continue once the reparse is complete.
+ if (
+ this.itemsToStore.length > 0 &&
+ this.folder &&
+ !lazy.FeedUtils.isMsgDatabaseOpenable(this.folder, true, this)
+ ) {
+ return;
+ }
+
+ // We have an msgDatabase; storeNextItem() will iterate through the parsed
+ // items, storing each one.
+ this.storeNextItem();
+ },
+
+ /**
+ * Clear the 'valid' field of all feeditems associated with this feed.
+ *
+ * @returns {void}
+ */
+ invalidateItems() {
+ let ds = lazy.FeedUtils.getItemsDS(this.server);
+ for (let id in ds.data) {
+ let item = ds.data[id];
+ if (item.feedURLs.includes(this.url)) {
+ item.valid = false;
+ lazy.FeedUtils.log.trace("Feed.invalidateItems: item - " + id);
+ }
+ }
+ ds.saveSoon();
+ },
+
+ /**
+ * Discards invalid items (in the feed item store) associated with the
+ * feed. There's a delay - invalid items are kept around for a set time
+ * before being purged.
+ *
+ * @param {Boolean} aDeleteFeed - is the feed being deleted (bypasses
+ * the delay time).
+ * @returns {void}
+ */
+ removeInvalidItems(aDeleteFeed) {
+ let ds = lazy.FeedUtils.getItemsDS(this.server);
+ lazy.FeedUtils.log.debug("Feed.removeInvalidItems: for url - " + this.url);
+
+ let currentTime = new Date().getTime();
+ for (let id in ds.data) {
+ let item = ds.data[id];
+ // skip valid items and ones not part of this feed.
+ if (!item.feedURLs.includes(this.url) || item.valid) {
+ continue;
+ }
+ let lastSeenTime = item.lastSeenTime || 0;
+
+ if (
+ currentTime - lastSeenTime < lazy.FeedUtils.INVALID_ITEM_PURGE_DELAY &&
+ !aDeleteFeed
+ ) {
+ // Don't immediately purge items in active feeds; do so for deleted feeds.
+ continue;
+ }
+
+ lazy.FeedUtils.log.trace("Feed.removeInvalidItems: item - " + id);
+ // Detach the item from this feed (it could be shared by multiple feeds).
+ item.feedURLs = item.feedURLs.filter(url => url != this.url);
+ if (item.feedURLs.length > 0) {
+ lazy.FeedUtils.log.debug(
+ "Feed.removeInvalidItems: " +
+ id +
+ " is from more than one feed; only the reference to" +
+ " this feed removed"
+ );
+ } else {
+ delete ds.data[id];
+ }
+ }
+ ds.saveSoon();
+ },
+
+ createFolder() {
+ if (this.folder) {
+ return;
+ }
+
+ try {
+ this.folder = this.server.rootMsgFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder(this.folderName);
+ } catch (ex) {
+ // An error creating.
+ lazy.FeedUtils.log.info(
+ "Feed.createFolder: error creating folder - '" +
+ this.folderName +
+ "' in parent folder " +
+ this.server.rootMsgFolder.filePath.path +
+ " -- " +
+ ex
+ );
+ // But its remnants are still there, clean up.
+ let xfolder = this.server.rootMsgFolder.getChildNamed(this.folderName);
+ this.server.rootMsgFolder.propagateDelete(xfolder, true);
+ }
+ },
+
+ // Gets the next item from itemsToStore and forces that item to be stored
+ // to the folder. If more items are left to be stored, fires a timer for
+ // the next one, otherwise triggers a download done notification to the UI.
+ storeNextItem() {
+ if (lazy.FeedUtils.CANCEL_REQUESTED) {
+ lazy.FeedUtils.CANCEL_REQUESTED = false;
+ this.cleanupParsingState(this, lazy.FeedUtils.kNewsBlogCancel);
+ return;
+ }
+
+ if (this.itemsToStore.length == 0) {
+ let code = lazy.FeedUtils.kNewsBlogSuccess;
+ this.createFolder();
+ if (!this.folder) {
+ code = lazy.FeedUtils.kNewsBlogFileError;
+ }
+
+ this.cleanupParsingState(this, code);
+ return;
+ }
+
+ let item = this.itemsToStore[this.itemsToStoreIndex];
+
+ if (item.store()) {
+ this.itemsStored++;
+ }
+
+ if (!this.folder) {
+ this.cleanupParsingState(this, lazy.FeedUtils.kNewsBlogFileError);
+ return;
+ }
+
+ this.itemsToStoreIndex++;
+
+ // If the listener is tracking progress for each item, report it here.
+ if (
+ item.feed.downloadCallback &&
+ item.feed.downloadCallback.onFeedItemStored
+ ) {
+ item.feed.downloadCallback.onFeedItemStored(
+ item.feed,
+ this.itemsToStoreIndex,
+ this.itemsToStore.length
+ );
+ }
+
+ // Eventually we'll report individual progress here.
+
+ if (this.itemsToStoreIndex < this.itemsToStore.length) {
+ if (!this.storeItemsTimer) {
+ this.storeItemsTimer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+ }
+
+ this.storeItemsTimer.initWithCallback(
+ this,
+ 50,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ } else {
+ // We have just finished downloading one or more feed items into the
+ // destination folder; if the folder is still listed as having new
+ // messages in it, then we should set the biff state on the folder so the
+ // right RDF UI changes happen in the folder pane to indicate new mail.
+ if (item.feed.folder.hasNewMessages) {
+ item.feed.folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ // Run the bayesian spam filter, if enabled.
+ item.feed.folder.callFilterPlugins(null);
+ }
+
+ this.cleanupParsingState(this, lazy.FeedUtils.kNewsBlogSuccess);
+ }
+ },
+
+ cleanupParsingState(aFeed, aCode) {
+ // Now that we are done parsing the feed, remove the feed from the cache.
+ FeedCache.removeFeed(aFeed.url);
+
+ if (aFeed.parseItems) {
+ // Do this only if we're in parse/store mode.
+ aFeed.removeInvalidItems(false);
+
+ if (aCode == lazy.FeedUtils.kNewsBlogSuccess && aFeed.mLastModified) {
+ aFeed.lastModified = aFeed.mLastModified;
+ }
+
+ // Flush any feed item changes to disk.
+ let ds = lazy.FeedUtils.getItemsDS(aFeed.server);
+ ds.saveSoon();
+ lazy.FeedUtils.log.debug(
+ "Feed.cleanupParsingState: items stored - " + this.itemsStored
+ );
+ }
+
+ // Force the xml http request to go away. This helps reduce some nasty
+ // assertions on shut down.
+ this.request = null;
+ this.itemsToStore = [];
+ this.itemsToStoreIndex = 0;
+ this.storeItemsTimer = null;
+
+ if (aFeed.downloadCallback) {
+ aFeed.downloadCallback.downloaded(aFeed, aCode);
+ }
+ },
+
+ // nsITimerCallback
+ notify(aTimer) {
+ this.storeNextItem();
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/FeedItem.jsm b/comm/mailnews/extensions/newsblog/FeedItem.jsm
new file mode 100644
index 0000000000..40a0424a0d
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/FeedItem.jsm
@@ -0,0 +1,490 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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 = ["FeedItem", "FeedEnclosure"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedUtils",
+ "resource:///modules/FeedUtils.jsm"
+);
+
+function FeedItem() {
+ this.mDate = lazy.FeedUtils.getValidRFC5322Date();
+ this.mParserUtils = Cc["@mozilla.org/parserutils;1"].getService(
+ Ci.nsIParserUtils
+ );
+}
+
+FeedItem.prototype = {
+ // Only for IETF Atom.
+ xmlContentBase: null,
+ id: null,
+ feed: null,
+ description: null,
+ content: null,
+ enclosures: [],
+ title: null,
+ // Author must be angle bracket enclosed to function as an addr-spec, in the
+ // absence of an addr-spec portion of an RFC5322 email address, as other
+ // functionality (gloda search) depends on this.
+ author: "<anonymous>",
+ inReplyTo: "",
+ keywords: [],
+ mURL: null,
+ characterSet: "UTF-8",
+
+ ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes
+ ENCLOSURE_HEADER_BOUNDARY_PREFIX: "------------", // 12 dashes
+ MESSAGE_TEMPLATE:
+ "\n" +
+ "<!DOCTYPE html>\n" +
+ "<html>\n" +
+ " <head>\n" +
+ " <title>%TITLE%</title>\n" +
+ ' <base href="%BASE%">\n' +
+ " </head>\n" +
+ ' <body id="msgFeedSummaryBody" selected="false">\n' +
+ " %CONTENT%\n" +
+ " </body>\n" +
+ "</html>\n",
+
+ get url() {
+ return this.mURL;
+ },
+
+ set url(aVal) {
+ try {
+ this.mURL = Services.io.newURI(aVal).spec;
+ } catch (ex) {
+ // The url as published or constructed can be a non url. It's used as a
+ // feeditem identifier in feeditems.rdf, as a messageId, and as an href
+ // and for the content-base header. Save as is; ensure not null.
+ this.mURL = aVal ? aVal : "";
+ }
+ },
+
+ get date() {
+ return this.mDate;
+ },
+
+ set date(aVal) {
+ this.mDate = aVal;
+ },
+
+ get identity() {
+ return this.feed.name + ": " + this.title + " (" + this.id + ")";
+ },
+
+ normalizeMessageID(messageID) {
+ // Escape occurrences of message ID meta characters <, >, and @.
+ messageID.replace(/</g, "%3C");
+ messageID.replace(/>/g, "%3E");
+ messageID.replace(/@/g, "%40");
+ messageID = "<" + messageID.trim() + "@localhost.localdomain>";
+
+ lazy.FeedUtils.log.trace(
+ "FeedItem.normalizeMessageID: messageID - " + messageID
+ );
+ return messageID;
+ },
+
+ get contentBase() {
+ if (this.xmlContentBase) {
+ return this.xmlContentBase;
+ }
+
+ return this.mURL;
+ },
+
+ /**
+ * Writes the item to the folder as a message and updates the feeditems db.
+ *
+ * @returns {void}
+ */
+ store() {
+ // this.title and this.content contain HTML.
+ // this.mUrl and this.contentBase contain plain text.
+
+ let stored = false;
+ let ds = lazy.FeedUtils.getItemsDS(this.feed.server);
+ let resource = this.findStoredResource();
+ if (!this.feed.folder) {
+ return stored;
+ }
+
+ if (resource == null) {
+ resource = {
+ feedURLs: [this.feed.url],
+ lastSeenTime: 0,
+ valid: false,
+ stored: false,
+ };
+ ds.data[this.id] = resource;
+ if (!this.content) {
+ lazy.FeedUtils.log.trace(
+ "FeedItem.store: " +
+ this.identity +
+ " no content; storing description or title"
+ );
+ this.content = this.description || this.title;
+ }
+
+ let content = this.MESSAGE_TEMPLATE;
+ content = content.replace(/%TITLE%/, this.title);
+ content = content.replace(/%BASE%/, this.htmlEscape(this.contentBase));
+ content = content.replace(/%CONTENT%/, this.content);
+ this.content = content;
+ this.writeToFolder();
+ this.markStored(resource);
+ stored = true;
+ }
+
+ this.markValid(resource);
+ ds.saveSoon();
+ return stored;
+ },
+
+ findStoredResource() {
+ // Checks to see if the item has already been stored in its feed's
+ // message folder.
+ lazy.FeedUtils.log.trace(
+ "FeedItem.findStoredResource: checking if stored - " + this.identity
+ );
+
+ let server = this.feed.server;
+ let folder = this.feed.folder;
+
+ if (!folder) {
+ lazy.FeedUtils.log.debug(
+ "FeedItem.findStoredResource: folder '" +
+ this.feed.folderName +
+ "' doesn't exist; creating as child of " +
+ server.rootMsgFolder.prettyName +
+ "\n"
+ );
+ this.feed.createFolder();
+ return null;
+ }
+
+ let ds = lazy.FeedUtils.getItemsDS(server);
+ let item = ds.data[this.id];
+ if (!item || !item.stored) {
+ lazy.FeedUtils.log.trace("FeedItem.findStoredResource: not stored");
+ return null;
+ }
+
+ lazy.FeedUtils.log.trace("FeedItem.findStoredResource: already stored");
+ return item;
+ },
+
+ markValid(resource) {
+ resource.lastSeenTime = new Date().getTime();
+ // Items can be in multiple feeds.
+ if (!resource.feedURLs.includes(this.feed.url)) {
+ resource.feedURLs.push(this.feed.url);
+ }
+ resource.valid = true;
+ },
+
+ markStored(resource) {
+ // Items can be in multiple feeds.
+ if (!resource.feedURLs.includes(this.feed.url)) {
+ resource.feedURLs.push(this.feed.url);
+ }
+ resource.stored = true;
+ },
+
+ writeToFolder() {
+ lazy.FeedUtils.log.trace(
+ "FeedItem.writeToFolder: " +
+ this.identity +
+ " writing to message folder " +
+ this.feed.name
+ );
+ // The subject may contain HTML entities. Convert these to their unencoded
+ // state. i.e. &amp; becomes '&'.
+ let title = this.title;
+ title = this.mParserUtils.convertToPlainText(
+ title,
+ Ci.nsIDocumentEncoder.OutputSelectionOnly |
+ Ci.nsIDocumentEncoder.OutputAbsoluteLinks,
+ 0
+ );
+
+ // Compress white space in the subject to make it look better. Trim
+ // leading/trailing spaces to prevent mbox header folding issue at just
+ // the right subject length.
+ this.title = title.replace(/[\t\r\n]+/g, " ").trim();
+
+ // If the date looks like it's in W3C-DTF format, convert it into
+ // an IETF standard date. Otherwise assume it's in IETF format.
+ if (this.mDate.search(/^\d\d\d\d/) != -1) {
+ this.mDate = new Date(this.mDate).toUTCString();
+ }
+
+ // If there is an inreplyto value, create the headers.
+ let inreplytoHdrsStr = this.inReplyTo
+ ? "References: " +
+ this.inReplyTo +
+ "\n" +
+ "In-Reply-To: " +
+ this.inReplyTo +
+ "\n"
+ : "";
+
+ // Support multiple authors in From.
+ let fromStr = this.createHeaderStrFromArray("From: ", this.author);
+
+ // If there are keywords (categories), create the headers.
+ let keywordsStr = this.createHeaderStrFromArray(
+ "Keywords: ",
+ this.keywords
+ );
+
+ // Escape occurrences of "From " at the beginning of lines of
+ // content per the mbox standard, since "From " denotes a new
+ // message, and add a line break so we know the last line has one.
+ this.content = this.content.replace(/([\r\n]+)(>*From )/g, "$1>$2");
+ this.content += "\n";
+
+ let source =
+ "From - " +
+ this.mDate +
+ "\n" +
+ "X-Mozilla-Status: 0000\n" +
+ "X-Mozilla-Status2: 00000000\n" +
+ "X-Mozilla-Keys: " +
+ " ".repeat(80) +
+ "\n" +
+ "Received: by localhost; " +
+ lazy.FeedUtils.getValidRFC5322Date() +
+ "\n" +
+ "Date: " +
+ this.mDate +
+ "\n" +
+ "Message-Id: " +
+ this.normalizeMessageID(this.id) +
+ "\n" +
+ fromStr +
+ "MIME-Version: 1.0\n" +
+ "Subject: " +
+ this.title +
+ "\n" +
+ inreplytoHdrsStr +
+ keywordsStr +
+ "Content-Transfer-Encoding: 8bit\n" +
+ "Content-Base: " +
+ this.mURL +
+ "\n";
+
+ if (this.enclosures.length) {
+ let boundaryID = source.length;
+ source +=
+ 'Content-Type: multipart/mixed; boundary="' +
+ this.ENCLOSURE_HEADER_BOUNDARY_PREFIX +
+ boundaryID +
+ '"\n\n' +
+ "This is a multi-part message in MIME format.\n" +
+ this.ENCLOSURE_BOUNDARY_PREFIX +
+ boundaryID +
+ "\n" +
+ "Content-Type: text/html; charset=" +
+ this.characterSet +
+ "\n" +
+ "Content-Transfer-Encoding: 8bit\n" +
+ this.content;
+
+ this.enclosures.forEach(function (enclosure) {
+ source += enclosure.convertToAttachment(boundaryID);
+ });
+
+ source += this.ENCLOSURE_BOUNDARY_PREFIX + boundaryID + "--\n\n\n";
+ } else {
+ source +=
+ "Content-Type: text/html; charset=" +
+ this.characterSet +
+ "\n" +
+ this.content;
+ }
+
+ lazy.FeedUtils.log.trace(
+ "FeedItem.writeToFolder: " +
+ this.identity +
+ " is " +
+ source.length +
+ " characters long"
+ );
+
+ // Get the folder and database storing the feed's messages and headers.
+ let folder = this.feed.folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
+ msgFolder.gettingNewMessages = true;
+ // Source is a unicode js string, as UTF-16, and we want to save a
+ // char * cpp |string| as UTF-8 bytes. The source xml doc encoding is utf8.
+ source = unescape(encodeURIComponent(source));
+ let msgDBHdr = folder.addMessage(source);
+ msgDBHdr.orFlags(Ci.nsMsgMessageFlags.FeedMsg);
+ msgFolder.gettingNewMessages = false;
+ this.tagItem(msgDBHdr, this.keywords);
+ },
+
+ /**
+ * Create a header string from an array. Intended for comma separated headers
+ * like From or Keywords. In the case of a longer than RFC5322 recommended
+ * line length, create multiple folded lines (easier to parse than multiple
+ * headers).
+ *
+ * @param {string} headerName - Name of the header.
+ * @param {string[]} headerItemsArray - An Array of strings to concatenate.
+ *
+ * @returns {String} - The header string.
+ */
+ createHeaderStrFromArray(headerName, headerItemsArray) {
+ let headerStr = "";
+ if (!headerItemsArray || headerItemsArray.length == 0) {
+ return headerStr;
+ }
+
+ const HEADER = headerName;
+ const LINELENGTH = 78;
+ const MAXLINELENGTH = 990;
+ let items = [].concat(headerItemsArray);
+ let lines = [];
+ headerStr = HEADER;
+ while (items.length) {
+ let item = items.shift();
+ if (
+ headerStr.length + item.length > LINELENGTH &&
+ headerStr.length > HEADER.length
+ ) {
+ lines.push(headerStr);
+ headerStr = " ".repeat(HEADER.length);
+ }
+
+ headerStr +=
+ headerStr.length + item.length > MAXLINELENGTH
+ ? item.substr(0, MAXLINELENGTH - headerStr.length) + "…, "
+ : item + ", ";
+ }
+
+ headerStr = headerStr.replace(/,\s$/, "\n");
+ lines.push(headerStr);
+ headerStr = lines.join("\n");
+
+ return headerStr;
+ },
+
+ /**
+ * Autotag messages.
+ *
+ * @param {nsIMsgDBHdr} aMsgDBHdr - message to tag
+ * @param {Array} aKeywords - keywords (tags)
+ * @returns {void}
+ */
+ tagItem(aMsgDBHdr, aKeywords) {
+ let category = this.feed.options.category;
+ if (!aKeywords.length || !category.enabled) {
+ return;
+ }
+
+ let prefix = category.prefixEnabled ? category.prefix : "";
+ let rtl = Services.prefs.getIntPref("bidi.direction") == 2;
+
+ let keys = [];
+ for (let keyword of aKeywords) {
+ keyword = rtl ? keyword + prefix : prefix + keyword;
+ let keyForTag = MailServices.tags.getKeyForTag(keyword);
+ if (!keyForTag) {
+ // Add the tag if it doesn't exist.
+ MailServices.tags.addTag(keyword, "", lazy.FeedUtils.AUTOTAG);
+ keyForTag = MailServices.tags.getKeyForTag(keyword);
+ }
+
+ // Add the tag key to the keys array.
+ keys.push(keyForTag);
+ }
+
+ if (keys.length) {
+ // Add the keys to the message.
+ aMsgDBHdr.folder.addKeywordsToMessages([aMsgDBHdr], keys.join(" "));
+ }
+ },
+
+ htmlEscape(s) {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ s = s.replace(/'/g, "&#39;");
+ s = s.replace(/"/g, "&quot;");
+ return s;
+ },
+};
+
+// A feed enclosure is to RSS what an attachment is for e-mail. We make
+// enclosures look like attachments in the UI.
+function FeedEnclosure(aURL, aContentType, aLength, aTitle) {
+ this.mURL = aURL;
+ // Store a reasonable mimetype if content-type is not present.
+ this.mContentType = aContentType || "application/unknown";
+ this.mLength = aLength;
+ this.mTitle = aTitle;
+
+ // Generate a fileName from the URL.
+ if (this.mURL) {
+ try {
+ let uri = Services.io.newURI(this.mURL).QueryInterface(Ci.nsIURL);
+ this.mFileName = uri.fileName;
+ // Determine mimetype from extension if content-type is not present.
+ if (!aContentType) {
+ let contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromExtension(uri.fileExtension);
+ this.mContentType = contentType;
+ }
+ } catch (ex) {
+ this.mFileName = this.mURL;
+ }
+ }
+}
+
+FeedEnclosure.prototype = {
+ mURL: "",
+ mContentType: "",
+ mLength: 0,
+ mFileName: "",
+ mTitle: "",
+ ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes
+
+ // Returns a string that looks like an e-mail attachment which represents
+ // the enclosure.
+ convertToAttachment(aBoundaryID) {
+ return (
+ "\n" +
+ this.ENCLOSURE_BOUNDARY_PREFIX +
+ aBoundaryID +
+ "\n" +
+ "Content-Type: " +
+ this.mContentType +
+ '; name="' +
+ (this.mTitle || this.mFileName) +
+ (this.mLength ? '"; size=' + this.mLength : '"') +
+ "\n" +
+ "X-Mozilla-External-Attachment-URL: " +
+ this.mURL +
+ "\n" +
+ 'Content-Disposition: attachment; filename="' +
+ this.mFileName +
+ '"\n\n' +
+ lazy.FeedUtils.strings.GetStringFromName("externalAttachmentMsg") +
+ "\n"
+ );
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/FeedParser.jsm b/comm/mailnews/extensions/newsblog/FeedParser.jsm
new file mode 100644
index 0000000000..863d5789fe
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/FeedParser.jsm
@@ -0,0 +1,1496 @@
+/* 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 = ["FeedParser"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedItem",
+ "resource:///modules/FeedItem.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedEnclosure",
+ "resource:///modules/FeedItem.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FeedUtils",
+ "resource:///modules/FeedUtils.jsm"
+);
+
+/**
+ * The feed parser. Depends on FeedItem.js, Feed.js.
+ *
+ * @class
+ */
+function FeedParser() {
+ this.parsedItems = [];
+ this.mSerializer = new XMLSerializer();
+}
+
+FeedParser.prototype = {
+ /**
+ * parseFeed() returns an array of parsed items ready for processing. It is
+ * currently a synchronous operation. If there is an error parsing the feed,
+ * parseFeed returns an empty feed in addition to calling aFeed.onParseError.
+ *
+ * @param {Feed} aFeed - The Feed object.
+ * @param {XMLDocument} aDOM - The document to parse.
+ * @returns {Array} - array of items, or empty array for error returns or
+ * nothing to do condition.
+ */
+ parseFeed(aFeed, aDOM) {
+ if (!XMLDocument.isInstance(aDOM)) {
+ // No xml doc.
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ let doc = aDOM.documentElement;
+ if (doc.namespaceURI == lazy.FeedUtils.MOZ_PARSERERROR_NS) {
+ // Gecko caught a basic parsing error.
+ let errStr =
+ doc.firstChild.textContent + "\n" + doc.firstElementChild.textContent;
+ lazy.FeedUtils.log.info("FeedParser.parseFeed: - " + errStr);
+ aFeed.onParseError(aFeed);
+ return [];
+ } else if (aDOM.querySelector("redirect")) {
+ // Check for RSS2.0 redirect document.
+ let channel = aDOM.querySelector("redirect");
+ if (this.isPermanentRedirect(aFeed, channel, null)) {
+ return [];
+ }
+
+ aFeed.onParseError(aFeed);
+ return [];
+ } else if (
+ doc.namespaceURI == lazy.FeedUtils.RDF_SYNTAX_NS &&
+ doc.getElementsByTagNameNS(lazy.FeedUtils.RSS_NS, "channel")[0]
+ ) {
+ aFeed.mFeedType = "RSS_1.xRDF";
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +
+ " : " +
+ aFeed.url
+ );
+
+ return this.parseAsRSS1(aFeed, aDOM);
+ } else if (doc.namespaceURI == lazy.FeedUtils.ATOM_03_NS) {
+ aFeed.mFeedType = "ATOM_0.3";
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +
+ " : " +
+ aFeed.url
+ );
+ return this.parseAsAtom(aFeed, aDOM);
+ } else if (doc.namespaceURI == lazy.FeedUtils.ATOM_IETF_NS) {
+ aFeed.mFeedType = "ATOM_IETF";
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +
+ " : " +
+ aFeed.url
+ );
+ return this.parseAsAtomIETF(aFeed, aDOM);
+ } else if (
+ doc.getElementsByTagNameNS(lazy.FeedUtils.RSS_090_NS, "channel")[0]
+ ) {
+ aFeed.mFeedType = "RSS_0.90";
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +
+ " : " +
+ aFeed.url
+ );
+ return this.parseAsRSS2(aFeed, aDOM);
+ }
+
+ // Parse as RSS 0.9x. In theory even RSS 1.0 feeds could be parsed by
+ // the 0.9x parser if the RSS namespace were the default.
+ let rssVer = doc.localName == "rss" ? doc.getAttribute("version") : null;
+ if (rssVer) {
+ aFeed.mFeedType = "RSS_" + rssVer;
+ } else {
+ aFeed.mFeedType = "RSS_0.9x?";
+ }
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseFeed: type:url - " + aFeed.mFeedType + " : " + aFeed.url
+ );
+ return this.parseAsRSS2(aFeed, aDOM);
+ },
+
+ parseAsRSS2(aFeed, aDOM) {
+ // Get the first channel (assuming there is only one per RSS File).
+ let channel = aDOM.querySelector("channel");
+ if (!channel) {
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ // Usually the empty string, unless this is RSS .90.
+ let nsURI = channel.namespaceURI || "";
+
+ if (this.isPermanentRedirect(aFeed, null, channel)) {
+ return [];
+ }
+
+ let tags = this.childrenByTagNameNS(channel, nsURI, "title");
+ aFeed.title = aFeed.title || this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, nsURI, "description");
+ aFeed.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, nsURI, "link");
+ aFeed.link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+
+ if (!(aFeed.title || aFeed.description)) {
+ lazy.FeedUtils.log.error(
+ "FeedParser.parseAsRSS2: missing mandatory element " +
+ "<title> and <description>"
+ );
+ // The RSS2 spec requires a <link> as well, but we can do without it
+ // so ignore the case of (valid) link missing.
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ if (!aFeed.parseItems) {
+ return [];
+ }
+
+ this.findSyUpdateTags(aFeed, channel);
+
+ aFeed.invalidateItems();
+ // XXX use getElementsByTagNameNS for now; childrenByTagNameNS would be
+ // better, but RSS .90 is still with us.
+ let itemNodes = aDOM.getElementsByTagNameNS(nsURI, "item");
+ itemNodes = itemNodes ? itemNodes : [];
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseAsRSS2: items to parse - " + itemNodes.length
+ );
+
+ for (let itemNode of itemNodes) {
+ if (!itemNode.childElementCount) {
+ continue;
+ }
+
+ let item = new lazy.FeedItem();
+ item.feed = aFeed;
+ item.enclosures = [];
+ item.keywords = [];
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.FEEDBURNER_NS,
+ "origLink"
+ );
+ let link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (!link) {
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "link");
+ link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ }
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "guid");
+ let guidNode = tags ? tags[0] : null;
+
+ let guid;
+ let isPermaLink = false;
+ if (guidNode) {
+ guid = this.getNodeValue(guidNode);
+ // isPermaLink is true if the value is "true" or if the attribute is
+ // not present; all other values, including "false" and "False" and
+ // for that matter "TRuE" and "meatcake" are false.
+ if (
+ !guidNode.hasAttribute("isPermaLink") ||
+ guidNode.getAttribute("isPermaLink") == "true"
+ ) {
+ isPermaLink = true;
+ }
+ // If attribute isPermaLink is missing, it is good to check the validity
+ // of <guid> value as an URL to avoid linking to non-URL strings.
+ if (!guidNode.hasAttribute("isPermaLink")) {
+ try {
+ Services.io.newURI(guid);
+ if (Services.io.extractScheme(guid) == "tag") {
+ isPermaLink = false;
+ }
+ } catch (ex) {
+ isPermaLink = false;
+ }
+ }
+
+ item.id = guid;
+ }
+
+ let guidLink = this.validLink(guid);
+ if (isPermaLink && guidLink) {
+ item.url = guidLink;
+ } else if (link) {
+ item.url = link;
+ } else {
+ item.url = null;
+ }
+
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "description");
+ item.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "title");
+ item.title = this.getNodeValue(tags ? tags[0] : null);
+ if (!(item.title || item.description)) {
+ lazy.FeedUtils.log.info(
+ "FeedParser.parseAsRSS2: <item> missing mandatory " +
+ "element, either <title> or <description>; skipping"
+ );
+ continue;
+ }
+
+ if (!item.id) {
+ // At this point, if there is no guid, uniqueness cannot be guaranteed
+ // by any of link or date (optional) or title (optional unless there
+ // is no description). Use a big chunk of description; minimize dupes
+ // with url and title if present.
+ item.id =
+ (item.url || item.feed.url) +
+ "#" +
+ item.title +
+ "#" +
+ (this.stripTags(
+ item.description ? item.description.substr(0, 150) : null
+ ) || item.title);
+ item.id = item.id.replace(/[\n\r\t\s]+/g, " ");
+ }
+
+ // Escape html entities in <title>, which are unescaped as textContent
+ // values. If the title is used as content, it will remain escaped; if
+ // it is used as the title, it will be unescaped upon store. Bug 1240603.
+ // The <description> tag must follow escaping examples found in
+ // http://www.rssboard.org/rss-encoding-examples, i.e. single escape angle
+ // brackets for tags, which are removed if used as title, and double
+ // escape entities for presentation in title.
+ // Better: always use <title>. Best: use Atom.
+ if (!item.title) {
+ item.title = this.stripTags(item.description).substr(0, 150);
+ } else {
+ item.title = item.htmlEscape(item.title);
+ }
+
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "author");
+ if (!tags) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.DC_NS,
+ "creator"
+ );
+ }
+ let author = this.getNodeValue(tags ? tags[0] : null) || aFeed.title;
+ author = this.cleanAuthorName(author);
+ item.author = author ? ["<" + author + ">"] : item.author;
+
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "pubDate");
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(itemNode, lazy.FeedUtils.DC_NS, "date");
+ }
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ // If the date is invalid, users will see the beginning of the epoch
+ // unless we reset it here, so they'll see the current time instead.
+ // This is typical aggregator behavior.
+ if (item.date) {
+ item.date = item.date.trim();
+ if (!lazy.FeedUtils.isValidRFC822Date(item.date)) {
+ // XXX Use this on the other formats as well.
+ item.date = this.dateRescue(item.date);
+ }
+ }
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.RSS_CONTENT_NS,
+ "encoded"
+ );
+ item.content = this.getNodeValueFormatted(tags ? tags[0] : null);
+
+ // Handle <enclosures> and <media:content>, which may be in a
+ // <media:group> (if present).
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "enclosure");
+ let encUrls = [];
+ if (tags) {
+ for (let tag of tags) {
+ let url = this.validLink(tag.getAttribute("url"));
+ if (url && !encUrls.includes(url)) {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let length = this.removeUnprintableASCII(
+ tag.getAttribute("length")
+ );
+ item.enclosures.push(new lazy.FeedEnclosure(url, type, length));
+ encUrls.push(url);
+ }
+ }
+ }
+
+ tags = itemNode.getElementsByTagNameNS(lazy.FeedUtils.MRSS_NS, "content");
+ if (tags) {
+ for (let tag of tags) {
+ let url = this.validLink(tag.getAttribute("url"));
+ if (url && !encUrls.includes(url)) {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let fileSize = this.removeUnprintableASCII(
+ tag.getAttribute("fileSize")
+ );
+ item.enclosures.push(new lazy.FeedEnclosure(url, type, fileSize));
+ }
+ }
+ }
+
+ // The <origEnclosureLink> tag has no specification, especially regarding
+ // whether more than one tag is allowed and, if so, how tags would
+ // relate to previously declared (and well specified) enclosure urls.
+ // The common usage is to include 1 origEnclosureLink, in addition to
+ // the specified enclosure tags for 1 enclosure. Thus, we will replace the
+ // first enclosure's, if found, url with the first <origEnclosureLink>
+ // url only or else add the <origEnclosureLink> url.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.FEEDBURNER_NS,
+ "origEnclosureLink"
+ );
+ let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (origEncUrl) {
+ if (item.enclosures.length) {
+ item.enclosures[0].mURL = origEncUrl;
+ } else {
+ item.enclosures.push(new lazy.FeedEnclosure(origEncUrl));
+ }
+ }
+
+ // Support <category> and autotagging.
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "category");
+ if (tags) {
+ for (let tag of tags) {
+ let term = this.getNodeValue(tag);
+ term = term ? this.xmlUnescape(term.replace(/,/g, ";")) : null;
+ if (term && !item.keywords.includes(term)) {
+ item.keywords.push(term);
+ }
+ }
+ }
+
+ this.parsedItems.push(item);
+ }
+
+ return this.parsedItems;
+ },
+
+ /**
+ * Extracts feed details and (optionally) items from an RSS1
+ * feed which has already been XML-parsed as an XMLDocument.
+ * The feed items are extracted only if feed.parseItems is set.
+ *
+ * Technically RSS1 is supposed to be treated as RDFXML, but in practice
+ * no feed parser anywhere ever does this, and feeds in the wild are
+ * pretty shakey on their RDF encoding too. So we just treat it as raw
+ * XML and pick out the bits we want.
+ *
+ * @param {Feed} feed - The Feed object.
+ * @param {XMLDocument} doc - The document to parse.
+ * @returns {Array} - array of FeedItems or empty array for error returns or
+ * nothing to do condition (ie unset feed.parseItems).
+ */
+ parseAsRSS1(feed, doc) {
+ let channel = doc.querySelector("channel");
+ if (!channel) {
+ feed.onParseError(feed);
+ return [];
+ }
+
+ if (this.isPermanentRedirect(feed, null, channel)) {
+ return [];
+ }
+
+ let titleNode = this.childByTagNameNS(
+ channel,
+ lazy.FeedUtils.RSS_NS,
+ "title"
+ );
+ // If user entered a title manually, retain it.
+ feed.title = feed.title || this.getNodeValue(titleNode) || feed.url;
+
+ let descNode = this.childByTagNameNS(
+ channel,
+ lazy.FeedUtils.RSS_NS,
+ "description"
+ );
+ feed.description = this.getNodeValueFormatted(descNode) || "";
+
+ let linkNode = this.childByTagNameNS(
+ channel,
+ lazy.FeedUtils.RSS_NS,
+ "link"
+ );
+ feed.link = this.validLink(this.getNodeValue(linkNode)) || feed.url;
+
+ if (!(feed.title || feed.description) || !feed.link) {
+ lazy.FeedUtils.log.error(
+ "FeedParser.parseAsRSS1: missing mandatory element " +
+ "<title> and <description>, or <link>"
+ );
+ feed.onParseError(feed);
+ return [];
+ }
+
+ // If we're only interested in the overall feed description, we're done.
+ if (!feed.parseItems) {
+ return [];
+ }
+
+ this.findSyUpdateTags(feed, channel);
+
+ feed.invalidateItems();
+
+ // Now process all the individual items in the feed.
+ let itemNodes = doc.getElementsByTagNameNS(lazy.FeedUtils.RSS_NS, "item");
+ itemNodes = itemNodes ? itemNodes : [];
+
+ for (let itemNode of itemNodes) {
+ let item = new lazy.FeedItem();
+ item.feed = feed;
+
+ // Prefer the value of the link tag to the item URI since the URI could be
+ // a relative URN.
+ let itemURI = itemNode.getAttribute("about") || "";
+ itemURI = this.removeUnprintableASCII(itemURI.trim());
+ let linkNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.RSS_NS,
+ "link"
+ );
+ item.id = this.getNodeValue(linkNode) || itemURI;
+ item.url = this.validLink(item.id);
+
+ let descNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.RSS_NS,
+ "description"
+ );
+ item.description = this.getNodeValueFormatted(descNode);
+
+ let titleNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.RSS_NS,
+ "title"
+ );
+ let subjectNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.DC_NS,
+ "subject"
+ );
+
+ item.title =
+ this.getNodeValue(titleNode) || this.getNodeValue(subjectNode);
+ if (!item.title && item.description) {
+ item.title = this.stripTags(item.description).substr(0, 150);
+ }
+ if (!item.url || !item.title) {
+ lazy.FeedUtils.log.info(
+ "FeedParser.parseAsRSS1: <item> missing mandatory " +
+ "element <item rdf:about> and <link>, or <title> and " +
+ "no <description>; skipping"
+ );
+ continue;
+ }
+
+ // TODO XXX: ignores multiple authors.
+ let authorNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.DC_NS,
+ "creator"
+ );
+ let channelCreatorNode = this.childByTagNameNS(
+ channel,
+ lazy.FeedUtils.DC_NS,
+ "creator"
+ );
+ let author =
+ this.getNodeValue(authorNode) ||
+ this.getNodeValue(channelCreatorNode) ||
+ feed.title;
+ author = this.cleanAuthorName(author);
+ item.author = author ? ["<" + author + ">"] : item.author;
+
+ let dateNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.DC_NS,
+ "date"
+ );
+ item.date = this.getNodeValue(dateNode) || item.date;
+
+ let contentNode = this.childByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.RSS_CONTENT_NS,
+ "encoded"
+ );
+ item.content = this.getNodeValueFormatted(contentNode);
+
+ this.parsedItems.push(item);
+ }
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseAsRSS1: items parsed - " + this.parsedItems.length
+ );
+
+ return this.parsedItems;
+ },
+
+ // TODO: deprecate ATOM_03_NS.
+ parseAsAtom(aFeed, aDOM) {
+ // Get the first channel (assuming there is only one per Atom File).
+ let channel = aDOM.querySelector("feed");
+ if (!channel) {
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ if (this.isPermanentRedirect(aFeed, null, channel)) {
+ return [];
+ }
+
+ let tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_03_NS,
+ "title"
+ );
+ aFeed.title =
+ aFeed.title || this.stripTags(this.getNodeValue(tags ? tags[0] : null));
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_03_NS,
+ "tagline"
+ );
+ aFeed.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, lazy.FeedUtils.ATOM_03_NS, "link");
+ aFeed.link = this.validLink(this.findAtomLink("alternate", tags));
+
+ if (!aFeed.title) {
+ lazy.FeedUtils.log.error(
+ "FeedParser.parseAsAtom: missing mandatory element <title>"
+ );
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ if (!aFeed.parseItems) {
+ return [];
+ }
+
+ this.findSyUpdateTags(aFeed, channel);
+
+ aFeed.invalidateItems();
+ let items = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_03_NS,
+ "entry"
+ );
+ items = items ? items : [];
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseAsAtom: items to parse - " + items.length
+ );
+
+ for (let itemNode of items) {
+ if (!itemNode.childElementCount) {
+ continue;
+ }
+
+ let item = new lazy.FeedItem();
+ item.feed = aFeed;
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "link"
+ );
+ item.url = this.validLink(this.findAtomLink("alternate", tags));
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "id"
+ );
+ item.id = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "summary"
+ );
+ item.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "title"
+ );
+ item.title =
+ this.getNodeValue(tags ? tags[0] : null) ||
+ (item.description ? item.description.substr(0, 150) : null);
+ if (!item.title || !item.id) {
+ // We're lenient about other mandatory tags, but insist on these.
+ lazy.FeedUtils.log.info(
+ "FeedParser.parseAsAtom: <entry> missing mandatory " +
+ "element <id>, or <title> and no <summary>; skipping"
+ );
+ continue;
+ }
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "author"
+ );
+ if (!tags) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "contributor"
+ );
+ }
+ if (!tags) {
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_03_NS,
+ "author"
+ );
+ }
+
+ let authorEl = tags ? tags[0] : null;
+
+ let author = "";
+ if (authorEl) {
+ tags = this.childrenByTagNameNS(
+ authorEl,
+ lazy.FeedUtils.ATOM_03_NS,
+ "name"
+ );
+ let name = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ authorEl,
+ lazy.FeedUtils.ATOM_03_NS,
+ "email"
+ );
+ let email = this.getNodeValue(tags ? tags[0] : null);
+ if (name) {
+ author = name + (email ? " <" + email + ">" : "");
+ } else if (email) {
+ author = email;
+ }
+ }
+
+ item.author = author || item.author || aFeed.title;
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "modified"
+ );
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "issued"
+ );
+ }
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_03_NS,
+ "created"
+ );
+ }
+
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ // XXX We should get the xml:base attribute from the content tag as well
+ // and use it as the base HREF of the message.
+ // XXX Atom feeds can have multiple content elements; we should differentiate
+ // between them and pick the best one.
+ // Some Atom feeds wrap the content in a CTYPE declaration; others use
+ // a namespace to identify the tags as HTML; and a few are buggy and put
+ // HTML tags in without declaring their namespace so they look like Atom.
+ // We deal with the first two but not the third.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_03_NS,
+ "content"
+ );
+ let contentNode = tags ? tags[0] : null;
+
+ let content;
+ if (contentNode) {
+ content = "";
+ for (let node of contentNode.childNodes) {
+ if (node.nodeType == node.CDATA_SECTION_NODE) {
+ content += node.data;
+ } else {
+ content += this.mSerializer.serializeToString(node);
+ }
+ }
+
+ if (contentNode.getAttribute("mode") == "escaped") {
+ content = content.replace(/&lt;/g, "<");
+ content = content.replace(/&gt;/g, ">");
+ content = content.replace(/&amp;/g, "&");
+ }
+
+ if (content == "") {
+ content = null;
+ }
+ }
+
+ item.content = content;
+ this.parsedItems.push(item);
+ }
+
+ return this.parsedItems;
+ },
+
+ parseAsAtomIETF(aFeed, aDOM) {
+ // Get the first channel (assuming there is only one per Atom File).
+ let channel = this.childrenByTagNameNS(
+ aDOM,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "feed"
+ )[0];
+ if (!channel) {
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ if (this.isPermanentRedirect(aFeed, null, channel)) {
+ return [];
+ }
+
+ let contentBase = channel.getAttribute("xml:base");
+
+ let tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "title"
+ );
+ aFeed.title =
+ aFeed.title ||
+ this.stripTags(this.serializeTextConstruct(tags ? tags[0] : null));
+
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "subtitle"
+ );
+ aFeed.description = this.serializeTextConstruct(tags ? tags[0] : null);
+
+ // Per spec, aFeed.link and contentBase may both end up null here.
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "link"
+ );
+ aFeed.link =
+ this.findAtomLink("self", tags, contentBase) ||
+ this.findAtomLink("alternate", tags, contentBase);
+ aFeed.link = this.validLink(aFeed.link);
+ if (!contentBase) {
+ contentBase = aFeed.link;
+ }
+
+ if (!aFeed.title) {
+ lazy.FeedUtils.log.error(
+ "FeedParser.parseAsAtomIETF: missing mandatory element <title>"
+ );
+ aFeed.onParseError(aFeed);
+ return [];
+ }
+
+ if (!aFeed.parseItems) {
+ return [];
+ }
+
+ this.findSyUpdateTags(aFeed, channel);
+
+ aFeed.invalidateItems();
+ let items = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "entry"
+ );
+ items = items ? items : [];
+ lazy.FeedUtils.log.debug(
+ "FeedParser.parseAsAtomIETF: items to parse - " + items.length
+ );
+
+ for (let itemNode of items) {
+ if (!itemNode.childElementCount) {
+ continue;
+ }
+
+ let item = new lazy.FeedItem();
+ item.feed = aFeed;
+ item.enclosures = [];
+ item.keywords = [];
+
+ contentBase = itemNode.getAttribute("xml:base") || contentBase;
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "source"
+ );
+ let source = tags ? tags[0] : null;
+
+ // Per spec, item.link and contentBase may both end up null here.
+ // If <content> is also not present, then <link rel="alternate"> is MUST
+ // but we're lenient.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.FEEDBURNER_NS,
+ "origLink"
+ );
+ item.url = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (!item.url) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "link"
+ );
+ item.url =
+ this.validLink(this.findAtomLink("alternate", tags, contentBase)) ||
+ aFeed.link;
+ }
+ if (!contentBase) {
+ contentBase = item.url;
+ }
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "id"
+ );
+ item.id = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "summary"
+ );
+ item.description = this.serializeTextConstruct(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "title"
+ );
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(
+ source,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "title"
+ );
+ }
+ item.title = this.stripTags(
+ this.serializeTextConstruct(tags ? tags[0] : null) ||
+ (item.description ? item.description.substr(0, 150) : null)
+ );
+ if (!item.title || !item.id) {
+ // We're lenient about other mandatory tags, but insist on these.
+ lazy.FeedUtils.log.info(
+ "FeedParser.parseAsAtomIETF: <entry> missing mandatory " +
+ "element <id>, or <title> and no <summary>; skipping"
+ );
+ continue;
+ }
+
+ // Support multiple authors.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "author"
+ );
+ if (!tags) {
+ tags = this.childrenByTagNameNS(
+ source,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "author"
+ );
+ }
+ if (!tags) {
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "author"
+ );
+ }
+
+ let authorTags = tags || [];
+ let authors = [];
+ for (let authorTag of authorTags) {
+ let author = "";
+ tags = this.childrenByTagNameNS(
+ authorTag,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "name"
+ );
+ let name = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(
+ authorTag,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "email"
+ );
+ let email = this.getNodeValue(tags ? tags[0] : null);
+ if (name) {
+ name = this.cleanAuthorName(name);
+ if (email) {
+ if (!email.match(/^<.*>$/)) {
+ email = " <" + email + ">";
+ }
+ author = name + email;
+ } else {
+ author = "<" + name + ">";
+ }
+ } else if (email) {
+ author = email;
+ }
+
+ if (author) {
+ authors.push(author);
+ }
+ }
+
+ if (authors.length == 0) {
+ tags = this.childrenByTagNameNS(
+ channel,
+ lazy.FeedUtils.DC_NS,
+ "publisher"
+ );
+ let author = this.getNodeValue(tags ? tags[0] : null) || aFeed.title;
+ author = this.cleanAuthorName(author);
+ item.author = author ? ["<" + author + ">"] : item.author;
+ } else {
+ item.author = authors;
+ }
+ lazy.FeedUtils.log.trace(
+ "FeedParser.parseAsAtomIETF: author(s) - " + item.author
+ );
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "updated"
+ );
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "published"
+ );
+ }
+ if (!tags || !this.getNodeValue(tags[0])) {
+ tags = this.childrenByTagNameNS(
+ source,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "published"
+ );
+ }
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "content"
+ );
+ item.content = this.serializeTextConstruct(tags ? tags[0] : null);
+
+ // Ensure relative links can be resolved and Content-Base set to an
+ // absolute url for the entry. But it's not mandatory that a url is found
+ // for Content-Base, per spec.
+ if (item.content) {
+ item.xmlContentBase =
+ (tags && tags[0].getAttribute("xml:base")) || contentBase;
+ } else if (item.description) {
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "summary"
+ );
+ item.xmlContentBase =
+ (tags && tags[0].getAttribute("xml:base")) || contentBase;
+ } else {
+ item.xmlContentBase = contentBase;
+ }
+
+ item.xmlContentBase = this.validLink(item.xmlContentBase);
+
+ // Handle <link rel="enclosure"> (if present).
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "link"
+ );
+ let encUrls = [];
+ if (tags) {
+ for (let tag of tags) {
+ let url =
+ tag.getAttribute("rel") == "enclosure"
+ ? (tag.getAttribute("href") || "").trim()
+ : null;
+ url = this.validLink(url);
+ if (url && !encUrls.includes(url)) {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let length = this.removeUnprintableASCII(
+ tag.getAttribute("length")
+ );
+ let title = this.removeUnprintableASCII(tag.getAttribute("title"));
+ item.enclosures.push(
+ new lazy.FeedEnclosure(url, type, length, title)
+ );
+ encUrls.push(url);
+ }
+ }
+ }
+
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.FEEDBURNER_NS,
+ "origEnclosureLink"
+ );
+ let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (origEncUrl) {
+ if (item.enclosures.length) {
+ item.enclosures[0].mURL = origEncUrl;
+ } else {
+ item.enclosures.push(new lazy.FeedEnclosure(origEncUrl));
+ }
+ }
+
+ // Handle atom threading extension, RFC4685. There may be 1 or more tags,
+ // and each must contain a ref attribute with 1 Message-Id equivalent
+ // value. This is the only attr of interest in the spec for presentation.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_THREAD_NS,
+ "in-reply-to"
+ );
+ if (tags) {
+ for (let tag of tags) {
+ let ref = this.removeUnprintableASCII(tag.getAttribute("ref"));
+ if (ref) {
+ item.inReplyTo += item.normalizeMessageID(ref) + " ";
+ }
+ }
+ item.inReplyTo = item.inReplyTo.trimRight();
+ }
+
+ // Support <category> and autotagging.
+ tags = this.childrenByTagNameNS(
+ itemNode,
+ lazy.FeedUtils.ATOM_IETF_NS,
+ "category"
+ );
+ if (tags) {
+ for (let tag of tags) {
+ let term = this.removeUnprintableASCII(tag.getAttribute("term"));
+ term = term ? this.xmlUnescape(term.replace(/,/g, ";")).trim() : null;
+ if (term && !item.keywords.includes(term)) {
+ item.keywords.push(term);
+ }
+ }
+ }
+
+ this.parsedItems.push(item);
+ }
+
+ return this.parsedItems;
+ },
+
+ isPermanentRedirect(aFeed, aRedirDocChannel, aFeedChannel) {
+ // If subscribing to a new feed, do not check redirect tags.
+ if (!aFeed.downloadCallback || aFeed.downloadCallback.mSubscribeMode) {
+ return false;
+ }
+
+ let tags, tagName, newUrl;
+ let oldUrl = aFeed.url;
+
+ // Check for RSS2.0 redirect document <newLocation> tag.
+ if (aRedirDocChannel) {
+ tagName = "newLocation";
+ tags = this.childrenByTagNameNS(aRedirDocChannel, "", tagName);
+ newUrl = this.getNodeValue(tags ? tags[0] : null);
+ }
+
+ // Check for <itunes:new-feed-url> tag.
+ if (aFeedChannel) {
+ tagName = "new-feed-url";
+ tags = this.childrenByTagNameNS(
+ aFeedChannel,
+ lazy.FeedUtils.ITUNES_NS,
+ tagName
+ );
+ newUrl = this.getNodeValue(tags ? tags[0] : null);
+ tagName = "itunes:" + tagName;
+ }
+
+ if (
+ newUrl &&
+ newUrl != oldUrl &&
+ lazy.FeedUtils.isValidScheme(newUrl) &&
+ lazy.FeedUtils.changeUrlForFeed(aFeed, newUrl)
+ ) {
+ lazy.FeedUtils.log.info(
+ "FeedParser.isPermanentRedirect: found <" +
+ tagName +
+ "> tag; updated feed url from: " +
+ oldUrl +
+ " to: " +
+ newUrl +
+ " in folder: " +
+ lazy.FeedUtils.getFolderPrettyPath(aFeed.folder)
+ );
+ aFeed.onUrlChange(aFeed, oldUrl);
+ return true;
+ }
+
+ return false;
+ },
+
+ serializeTextConstruct(textElement) {
+ let content = "";
+ if (textElement) {
+ let textType = textElement.getAttribute("type");
+
+ // Atom spec says consider it "text" if not present.
+ if (!textType) {
+ textType = "text";
+ }
+
+ // There could be some strange content type we don't handle.
+ if (textType != "text" && textType != "html" && textType != "xhtml") {
+ return null;
+ }
+
+ for (let node of textElement.childNodes) {
+ if (node.nodeType == node.CDATA_SECTION_NODE) {
+ content += this.xmlEscape(node.data);
+ } else {
+ content += this.mSerializer.serializeToString(node);
+ }
+ }
+
+ if (textType == "html") {
+ content = this.xmlUnescape(content);
+ }
+
+ content = content.trim();
+ }
+
+ // Other parts of the code depend on this being null if there's no content.
+ return content ? content : null;
+ },
+
+ /**
+ * Return a cleaned up author name value.
+ *
+ * @param {string} authorString - A string.
+ * @returns {String} - A clean string value.
+ */
+ cleanAuthorName(authorString) {
+ if (!authorString) {
+ return "";
+ }
+ lazy.FeedUtils.log.trace(
+ "FeedParser.cleanAuthor: author1 - " + authorString
+ );
+ let author = authorString
+ .replace(/[\n\r\t]+/g, " ")
+ .replace(/"/g, '\\"')
+ .trim();
+ // If the name contains special chars, quote it.
+ if (author.match(/[<>@,"]/)) {
+ author = '"' + author + '"';
+ }
+ lazy.FeedUtils.log.trace("FeedParser.cleanAuthor: author2 - " + author);
+
+ return author;
+ },
+
+ /**
+ * Return a cleaned up node value. This is intended for values that are not
+ * multiline and not formatted. A sequence of tab or newline is converted to
+ * a space and unprintable ascii is removed.
+ *
+ * @param {Node} node - A DOM node.
+ * @returns {String} - A clean string value or null.
+ */
+ getNodeValue(node) {
+ let nodeValue = this.getNodeValueRaw(node);
+ if (!nodeValue) {
+ return null;
+ }
+
+ nodeValue = nodeValue.replace(/[\n\r\t]+/g, " ");
+ return this.removeUnprintableASCII(nodeValue);
+ },
+
+ /**
+ * Return a cleaned up formatted node value, meaning CR/LF/TAB are retained
+ * while all other unprintable ascii is removed. This is intended for values
+ * that are multiline and formatted, such as content or description tags.
+ *
+ * @param {Node} node - A DOM node.
+ * @returns {String} - A clean string value or null.
+ */
+ getNodeValueFormatted(node) {
+ let nodeValue = this.getNodeValueRaw(node);
+ if (!nodeValue) {
+ return null;
+ }
+
+ return this.removeUnprintableASCIIexCRLFTAB(nodeValue);
+ },
+
+ /**
+ * Return a raw node value, as received. This should be sanitized as
+ * appropriate.
+ *
+ * @param {Node} node - A DOM node.
+ * @returns {String} - A string value or null.
+ */
+ getNodeValueRaw(node) {
+ if (node && node.textContent) {
+ return node.textContent.trim();
+ }
+
+ if (node && node.firstChild) {
+ let ret = "";
+ for (let child = node.firstChild; child; child = child.nextSibling) {
+ let value = this.getNodeValueRaw(child);
+ if (value) {
+ ret += value;
+ }
+ }
+
+ if (ret) {
+ return ret.trim();
+ }
+ }
+
+ return null;
+ },
+
+ // Finds elements that are direct children of the first arg.
+ childrenByTagNameNS(aElement, aNamespace, aTagName) {
+ if (!aElement) {
+ return null;
+ }
+
+ let matches = aElement.getElementsByTagNameNS(aNamespace, aTagName);
+ let matchingChildren = [];
+ for (let match of matches) {
+ if (match.parentNode == aElement) {
+ matchingChildren.push(match);
+ }
+ }
+
+ return matchingChildren.length ? matchingChildren : null;
+ },
+
+ /**
+ * Returns first matching descendent of element, or null.
+ *
+ * @param {Element} element - DOM element to search.
+ * @param {string} namespace - Namespace of the search tag.
+ * @param {String} tagName - Tag to search for.
+ * @returns {Element|null} - Matching element, or null.
+ */
+ childByTagNameNS(element, namespace, tagName) {
+ if (!element) {
+ return null;
+ }
+ // Handily, item() returns null for out-of-bounds access.
+ return element.getElementsByTagNameNS(namespace, tagName).item(0);
+ },
+
+ /**
+ * Ensure <link> type tags start with http[s]://, ftp:// or magnet:
+ * for values stored in mail headers (content-base and remote enclosures),
+ * particularly to prevent data: uris, javascript, and other spoofing.
+ *
+ * @param {string} link - An intended http url string.
+ * @returns {String} - A clean string starting with http, ftp or magnet,
+ * else null.
+ */
+ validLink(link) {
+ if (/^((https?|ftp):\/\/|magnet:)/.test(link)) {
+ return this.removeUnprintableASCII(link.trim());
+ }
+
+ return null;
+ },
+
+ /**
+ * Return an absolute link for <entry> relative links. If xml:base is
+ * present in a <feed> attribute or child <link> element attribute, use it;
+ * otherwise the Feed.link will be the relevant <feed> child <link> value
+ * and will be the |baseURI| for <entry> child <link>s if there is no further
+ * xml:base, which may be an attribute of any element.
+ *
+ * @param {string} linkRel - the <link> rel attribute value to find.
+ * @param {NodeList} linkElements - the nodelist of <links> to search in.
+ * @param {string} baseURI - the url to use when resolving relative
+ * links to absolute values.
+ * @returns {String} or null - absolute url for a <link>, or null if the
+ * rel type is not found.
+ */
+ findAtomLink(linkRel, linkElements, baseURI) {
+ if (!linkElements) {
+ return null;
+ }
+
+ // XXX Need to check for MIME type and hreflang.
+ for (let alink of linkElements) {
+ if (
+ alink &&
+ // If there's a link rel.
+ ((alink.getAttribute("rel") && alink.getAttribute("rel") == linkRel) ||
+ // If there isn't, assume 'alternate'.
+ (!alink.getAttribute("rel") && linkRel == "alternate")) &&
+ alink.getAttribute("href")
+ ) {
+ // Atom links are interpreted relative to xml:base.
+ let href = alink.getAttribute("href");
+ baseURI = alink.getAttribute("xml:base") || baseURI || href;
+ try {
+ return Services.io.newURI(baseURI).resolve(href);
+ } catch (ex) {}
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find RSS Syndication extension tags.
+ * http://web.resource.org/rss/1.0/modules/syndication/
+ *
+ * @param {Feed} aFeed - the feed object.
+ * @param {Node | String} aChannel - dom node for the <channel>.
+ * @returns {void}
+ */
+ findSyUpdateTags(aFeed, aChannel) {
+ let tag, updatePeriod, updateFrequency, updateBase;
+ tag = this.childrenByTagNameNS(
+ aChannel,
+ lazy.FeedUtils.RSS_SY_NS,
+ "updatePeriod"
+ );
+ updatePeriod = this.getNodeValue(tag ? tag[0] : null) || "";
+ tag = this.childrenByTagNameNS(
+ aChannel,
+ lazy.FeedUtils.RSS_SY_NS,
+ "updateFrequency"
+ );
+ updateFrequency = this.getNodeValue(tag ? tag[0] : null) || "";
+ tag = this.childrenByTagNameNS(
+ aChannel,
+ lazy.FeedUtils.RSS_SY_NS,
+ "updateBase"
+ );
+ updateBase = this.getNodeValue(tag ? tag[0] : null) || "";
+ lazy.FeedUtils.log.debug(
+ "FeedParser.findSyUpdateTags: updatePeriod:updateFrequency - " +
+ updatePeriod +
+ ":" +
+ updateFrequency
+ );
+
+ if (updatePeriod) {
+ if (lazy.FeedUtils.RSS_SY_UNITS.includes(updatePeriod.toLowerCase())) {
+ updatePeriod = updatePeriod.toLowerCase();
+ } else {
+ updatePeriod = "daily";
+ }
+ }
+
+ updateFrequency = isNaN(updateFrequency) ? 1 : updateFrequency;
+
+ let options = aFeed.options;
+ if (
+ options.updates.updatePeriod == updatePeriod &&
+ options.updates.updateFrequency == updateFrequency &&
+ options.updates.updateBase == updateBase
+ ) {
+ return;
+ }
+
+ options.updates.updatePeriod = updatePeriod;
+ options.updates.updateFrequency = updateFrequency;
+ options.updates.updateBase = updateBase;
+ aFeed.options = options;
+ },
+
+ /**
+ * Remove unprintable ascii, particularly CR/LF, for non formatted tag values.
+ *
+ * @param {string} s - String to clean.
+ * @returns {String} - Cleaned string.
+ */
+ removeUnprintableASCII(s) {
+ /* eslint-disable-next-line no-control-regex */
+ return s ? s.replace(/[\x00-\x1F\x7F]+/g, "") : "";
+ },
+
+ /**
+ * Remove unprintable ascii, except CR/LF/TAB, for formatted tag values.
+ *
+ * @param {string} s - String to clean.
+ * @returns {String} - Cleaned string.
+ */
+ removeUnprintableASCIIexCRLFTAB(s) {
+ /* eslint-disable-next-line no-control-regex */
+ return s ? s.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]+/g, "") : "";
+ },
+
+ stripTags(someHTML) {
+ return someHTML ? someHTML.replace(/<[^>]+>/g, "") : someHTML;
+ },
+
+ xmlUnescape(s) {
+ s = s.replace(/&lt;/g, "<");
+ s = s.replace(/&gt;/g, ">");
+ s = s.replace(/&amp;/g, "&");
+ return s;
+ },
+
+ xmlEscape(s) {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ return s;
+ },
+
+ dateRescue(dateString) {
+ // Deal with various kinds of invalid dates.
+ if (!isNaN(parseInt(dateString))) {
+ // It's an integer, so maybe it's a timestamp.
+ let d = new Date(parseInt(dateString) * 1000);
+ let now = new Date();
+ let yeardiff = now.getFullYear() - d.getFullYear();
+ lazy.FeedUtils.log.trace(
+ "FeedParser.dateRescue: Rescue Timestamp date - " +
+ d.toString() +
+ " ,year diff - " +
+ yeardiff
+ );
+ if (yeardiff >= 0 && yeardiff < 3) {
+ // It's quite likely the correct date.
+ return d.toString();
+ }
+ }
+
+ // Could be an ISO8601/W3C date. If not, get the current time.
+ return lazy.FeedUtils.getValidRFC5322Date(dateString);
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/FeedUtils.jsm b/comm/mailnews/extensions/newsblog/FeedUtils.jsm
new file mode 100644
index 0000000000..b881c358d8
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/FeedUtils.jsm
@@ -0,0 +1,2136 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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 = ["FeedUtils"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ Feed: "resource:///modules/Feed.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+var FeedUtils = {
+ MOZ_PARSERERROR_NS: "http://www.mozilla.org/newlayout/xml/parsererror.xml",
+
+ RDF_SYNTAX_NS: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ RDF_SYNTAX_TYPE: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
+ RSS_090_NS: "http://my.netscape.com/rdf/simple/0.9/",
+
+ RSS_NS: "http://purl.org/rss/1.0/",
+
+ RSS_CONTENT_NS: "http://purl.org/rss/1.0/modules/content/",
+
+ RSS_SY_NS: "http://purl.org/rss/1.0/modules/syndication/",
+ RSS_SY_UNITS: ["hourly", "daily", "weekly", "monthly", "yearly"],
+ kBiffUnitsMinutes: "min",
+ kBiffUnitsDays: "d",
+
+ DC_NS: "http://purl.org/dc/elements/1.1/",
+
+ MRSS_NS: "http://search.yahoo.com/mrss/",
+ FEEDBURNER_NS: "http://rssnamespace.org/feedburner/ext/1.0",
+ ITUNES_NS: "http://www.itunes.com/dtds/podcast-1.0.dtd",
+
+ FZ_NS: "urn:forumzilla:",
+ FZ_ITEM_NS: "urn:feeditem:",
+
+ // Atom constants
+ ATOM_03_NS: "http://purl.org/atom/ns#",
+ ATOM_IETF_NS: "http://www.w3.org/2005/Atom",
+ ATOM_THREAD_NS: "http://purl.org/syndication/thread/1.0",
+
+ // Accept content mimetype preferences for feeds.
+ REQUEST_ACCEPT:
+ "application/atom+xml," +
+ "application/rss+xml;q=0.9," +
+ "application/rdf+xml;q=0.8," +
+ "application/xml;q=0.7,text/xml;q=0.7," +
+ "*/*;q=0.1",
+ // Timeout for nonresponse to request, 30 seconds.
+ REQUEST_TIMEOUT: 30 * 1000,
+
+ MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
+
+ // Maximum number of concurrent in progress feeds, across all accounts.
+ kMaxConcurrentFeeds: 25,
+ get MAX_CONCURRENT_FEEDS() {
+ let pref = "rss.max_concurrent_feeds";
+ if (Services.prefs.prefHasUserValue(pref)) {
+ return Services.prefs.getIntPref(pref);
+ }
+
+ Services.prefs.setIntPref(pref, FeedUtils.kMaxConcurrentFeeds);
+ return FeedUtils.kMaxConcurrentFeeds;
+ },
+
+ // The amount of time, specified in milliseconds, to leave an item in the
+ // feeditems cache after the item has disappeared from the publisher's
+ // file. The default delay is one day.
+ kInvalidItemPurgeDelayDays: 1,
+ get INVALID_ITEM_PURGE_DELAY() {
+ let pref = "rss.invalid_item_purge_delay_days";
+ if (Services.prefs.prefHasUserValue(pref)) {
+ return Services.prefs.getIntPref(pref) * this.MILLISECONDS_PER_DAY;
+ }
+
+ Services.prefs.setIntPref(pref, FeedUtils.kInvalidItemPurgeDelayDays);
+ return FeedUtils.kInvalidItemPurgeDelayDays * this.MILLISECONDS_PER_DAY;
+ },
+
+ // Polling interval to check individual feed update interval preference.
+ kBiffPollMinutes: 1,
+ kNewsBlogSuccess: 0,
+ // Usually means there was an error trying to parse the feed.
+ kNewsBlogInvalidFeed: 1,
+ // Generic networking failure when trying to download the feed.
+ kNewsBlogRequestFailure: 2,
+ kNewsBlogFeedIsBusy: 3,
+ // For 304 Not Modified; There are no new articles for this feed.
+ kNewsBlogNoNewItems: 4,
+ kNewsBlogCancel: 5,
+ kNewsBlogFileError: 6,
+ // Invalid certificate, for overridable user exception errors.
+ kNewsBlogBadCertError: 7,
+ // For 401 Unauthorized or 403 Forbidden.
+ kNewsBlogNoAuthError: 8,
+
+ CANCEL_REQUESTED: false,
+ AUTOTAG: "~AUTOTAG",
+
+ FEED_ACCOUNT_TYPES: ["rss"],
+
+ /**
+ * Get all rss account servers rootFolders.
+ *
+ * @returns {nsIMsgIncomingServer}[] - Array of servers (empty array if none).
+ */
+ getAllRssServerRootFolders() {
+ let rssRootFolders = [];
+ for (let server of MailServices.accounts.allServers) {
+ if (server && server.type == "rss") {
+ rssRootFolders.push(server.rootFolder);
+ }
+ }
+
+ // By default, Tb sorts by hostname, ie Feeds, Feeds-1, and not by alpha
+ // prettyName. Do the same as a stock install to match folderpane order.
+ rssRootFolders.sort(function (a, b) {
+ return a.hostname > b.hostname;
+ });
+
+ return rssRootFolders;
+ },
+
+ /**
+ * Create rss account.
+ *
+ * @param {String} aName - Optional account name to override default.
+ * @returns {nsIMsgAccount} - The creaged account.
+ */
+ createRssAccount(aName) {
+ let userName = "nobody";
+ let hostName = "Feeds";
+ let hostNamePref = hostName;
+ let server;
+ let serverType = "rss";
+ let defaultName = FeedUtils.strings.GetStringFromName("feeds-accountname");
+ let i = 2;
+ while (MailServices.accounts.findServer(userName, hostName, serverType)) {
+ // If "Feeds" exists, try "Feeds-2", then "Feeds-3", etc.
+ hostName = hostNamePref + "-" + i++;
+ }
+
+ server = MailServices.accounts.createIncomingServer(
+ userName,
+ hostName,
+ serverType
+ );
+ server.biffMinutes = FeedUtils.kBiffPollMinutes;
+ server.prettyName = aName ? aName : defaultName;
+ server.valid = true;
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+ // Initialize the feed_options now.
+ this.getOptionsAcct(server);
+
+ // Ensure the Trash folder db (.msf) is created otherwise folder/message
+ // deletes will throw until restart creates it.
+ server.msgStore.discoverSubFolders(server.rootMsgFolder, false);
+
+ // Save new accounts in case of a crash.
+ try {
+ MailServices.accounts.saveAccountInfo();
+ } catch (ex) {
+ this.log.error(
+ "FeedUtils.createRssAccount: error on saveAccountInfo - " + ex
+ );
+ }
+
+ this.log.debug(
+ "FeedUtils.createRssAccount: " +
+ account.incomingServer.rootFolder.prettyName
+ );
+
+ return account;
+ },
+
+ /**
+ * Helper routine that checks our subscriptions list array and returns
+ * true if the url is already in our list. This is used to prevent the
+ * user from subscribing to the same feed multiple times for the same server.
+ *
+ * @param {string} aUrl - The url.
+ * @param {nsIMsgIncomingServer} aServer - Account server.
+ * @returns {Boolean} - true if exists else false.
+ */
+ feedAlreadyExists(aUrl, aServer) {
+ let ds = this.getSubscriptionsDS(aServer);
+ let sub = ds.data.find(x => x.url == aUrl);
+ if (sub === undefined) {
+ return false;
+ }
+ let folder = sub.destFolder;
+ this.log.info(
+ "FeedUtils.feedAlreadyExists: feed url " +
+ aUrl +
+ " subscribed in folder url " +
+ decodeURI(folder)
+ );
+
+ return true;
+ },
+
+ /**
+ * Download a feed url on biff or get new messages.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ * @param {nsIUrlListener} aUrlListener - Feed url.
+ * @param {Boolean} aIsBiff - true if biff, false if manual get.
+ * @param {nsIDOMWindow} aMsgWindow - The window.
+ *
+ * @returns {Promise<void>} When all feeds downloading have been set off.
+ */
+ async downloadFeed(aFolder, aUrlListener, aIsBiff, aMsgWindow) {
+ FeedUtils.log.debug(
+ "downloadFeed: account isBiff:isOffline - " +
+ aIsBiff +
+ " : " +
+ Services.io.offline
+ );
+ // User set.
+ if (Services.io.offline) {
+ return;
+ }
+
+ // No network connection. Unfortunately, this is only set if the event is
+ // received by Tb from the OS (ie it must already be running) and doesn't
+ // necessarily mean connectivity to the internet, only the nearest network
+ // point. But it's something.
+ if (!Services.io.connectivity) {
+ FeedUtils.log.warn("downloadFeed: network connection unavailable");
+ return;
+ }
+
+ // We don't yet support the ability to check for new articles while we are
+ // in the middle of subscribing to a feed. For now, abort the check for
+ // new feeds.
+ if (FeedUtils.progressNotifier.mSubscribeMode) {
+ FeedUtils.log.warn(
+ "downloadFeed: Aborting RSS New Mail Check. " +
+ "Feed subscription in progress\n"
+ );
+ return;
+ }
+
+ let forceDownload = !aIsBiff;
+ let inStartup = false;
+ if (aFolder.isServer) {
+ // The lastUpdateTime is |null| only at session startup/initialization.
+ // Note: feed processing does not impact startup, as the biff poll
+ // will go off in about kBiffPollMinutes (1) and process each feed
+ // according to its own lastUpdatTime/update frequency.
+ if (FeedUtils.getStatus(aFolder, aFolder.URI).lastUpdateTime === null) {
+ inStartup = true;
+ }
+
+ FeedUtils.setStatus(aFolder, aFolder.URI, "lastUpdateTime", Date.now());
+ }
+
+ let allFolders = aFolder.descendants;
+ if (!aFolder.isServer) {
+ // Add the base folder; it does not get returned by .descendants. Do not
+ // add the account folder as it doesn't have the feedUrl property or even
+ // a msgDatabase necessarily.
+ allFolders.unshift(aFolder);
+ }
+
+ let folder;
+ async function* feeder() {
+ for (let i = 0; i < allFolders.length; i++) {
+ folder = allFolders[i];
+ FeedUtils.log.debug(
+ "downloadFeed: START x/# folderName:folderPath - " +
+ (i + 1) +
+ "/" +
+ allFolders.length +
+ " " +
+ folder.name +
+ " : " +
+ folder.filePath.path
+ );
+
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(folder);
+ // Continue if there are no feedUrls for the folder in the feeds
+ // database. All folders in Trash are skipped.
+ if (!feedUrlArray) {
+ continue;
+ }
+
+ FeedUtils.log.debug(
+ "downloadFeed: CONTINUE foldername:urlArray - " +
+ folder.name +
+ " : " +
+ feedUrlArray
+ );
+
+ // We need to kick off a download for each feed.
+ let now = Date.now();
+ for (let url of feedUrlArray) {
+ // Check whether this feed should be updated; if forceDownload is true
+ // skip the per feed check.
+ let status = FeedUtils.getStatus(folder, url);
+ if (!forceDownload) {
+ // Also skip if user paused, or error paused (but not inStartup;
+ // go check the feed again), or update interval hasn't expired.
+ if (
+ status.enabled === false ||
+ (status.enabled === null && !inStartup) ||
+ now - status.lastUpdateTime < status.updateMinutes * 60000
+ ) {
+ FeedUtils.log.debug(
+ "downloadFeed: SKIP feed, " +
+ "aIsBiff:enabled:minsSinceLastUpdate::url - " +
+ aIsBiff +
+ " : " +
+ status.enabled +
+ " : " +
+ Math.round((now - status.lastUpdateTime) / 60) / 1000 +
+ " :: " +
+ url
+ );
+ continue;
+ }
+ }
+ // Update feed icons only once every 24h, tops.
+ if (
+ forceDownload ||
+ now - status.lastUpdateTime >= 24 * 60 * 60 * 1000
+ ) {
+ await FeedUtils.getFavicon(folder, url);
+ }
+
+ // Create a feed object.
+ let feed = new lazy.Feed(url, folder);
+
+ // init can be called multiple times. Checks if it should actually
+ // init itself.
+ FeedUtils.progressNotifier.init(aMsgWindow, false);
+
+ // Bump our pending feed download count. From now on, all feeds will
+ // be resolved and finish with progressNotifier.downloaded(). Any
+ // early returns must call downloaded() so mNumPendingFeedDownloads
+ // is decremented and notification/status feedback is reset.
+ FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
+
+ // If the current active count exceeds the max desired, exit from
+ // the current poll cycle. Only throttle for a background biff; for
+ // a user manual get messages, do them all.
+ if (
+ aIsBiff &&
+ FeedUtils.progressNotifier.mNumPendingFeedDownloads >
+ FeedUtils.MAX_CONCURRENT_FEEDS
+ ) {
+ FeedUtils.log.debug(
+ "downloadFeed: RETURN active feeds count is greater " +
+ "than the max - " +
+ FeedUtils.MAX_CONCURRENT_FEEDS
+ );
+ FeedUtils.progressNotifier.downloaded(
+ feed,
+ FeedUtils.kNewsBlogFeedIsBusy
+ );
+ return;
+ }
+
+ // Set status info and download.
+ FeedUtils.log.debug("downloadFeed: DOWNLOAD feed url - " + url);
+ FeedUtils.setStatus(
+ folder,
+ url,
+ "code",
+ FeedUtils.kNewsBlogFeedIsBusy
+ );
+ feed.download(true, FeedUtils.progressNotifier);
+
+ Services.tm.mainThread.dispatch(function () {
+ try {
+ let done = getFeed.next().done;
+ if (done) {
+ // Finished with all feeds in base aFolder and its subfolders.
+ FeedUtils.log.debug(
+ "downloadFeed: Finished with folder - " + aFolder.name
+ );
+ folder = null;
+ allFolders = null;
+ }
+ } catch (ex) {
+ FeedUtils.log.error("downloadFeed: error - " + ex);
+ FeedUtils.progressNotifier.downloaded(
+ feed,
+ FeedUtils.kNewsBlogFeedIsBusy
+ );
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ yield undefined;
+ }
+ }
+ }
+
+ let getFeed = await feeder();
+ try {
+ let done = getFeed.next().done;
+ if (done) {
+ // Nothing to do.
+ FeedUtils.log.debug(
+ "downloadFeed: Nothing to do in folder - " + aFolder.name
+ );
+ folder = null;
+ allFolders = null;
+ }
+ } catch (ex) {
+ FeedUtils.log.error("downloadFeed: error - " + ex);
+ FeedUtils.progressNotifier.downloaded(
+ { folder: aFolder, url: "" },
+ FeedUtils.kNewsBlogFeedIsBusy
+ );
+ }
+ },
+
+ /**
+ * Subscribe a new feed url.
+ *
+ * @param {String} aUrl - Feed url.
+ * @param {nsIMsgFolder} aFolder - Folder.
+ *
+ * @returns {void}
+ */
+ subscribeToFeed(aUrl, aFolder) {
+ // We don't support the ability to subscribe to several feeds at once yet.
+ // For now, abort the subscription if we are already in the middle of
+ // subscribing to a feed via drag and drop.
+ if (FeedUtils.progressNotifier.mNumPendingFeedDownloads > 0) {
+ FeedUtils.log.warn(
+ "subscribeToFeed: Aborting RSS subscription. " +
+ "Feed downloads already in progress\n"
+ );
+ return;
+ }
+
+ // If aFolder is null, then use the root folder for the first RSS account.
+ if (!aFolder) {
+ aFolder = FeedUtils.getAllRssServerRootFolders()[0];
+ }
+
+ // If the user has no Feeds account yet, create one.
+ if (!aFolder) {
+ aFolder = FeedUtils.createRssAccount().incomingServer.rootFolder;
+ }
+
+ // If aUrl is a feed url, then it is either of the form
+ // feed://example.org/feed.xml or feed:https://example.org/feed.xml.
+ // Replace feed:// with http:// per the spec, then strip off feed:
+ // for the second case.
+ aUrl = aUrl.replace(/^feed:\x2f\x2f/i, "http://");
+ aUrl = aUrl.replace(/^feed:/i, "");
+
+ let msgWindow = Services.wm.getMostRecentWindow("mail:3pane").msgWindow;
+
+ // Make sure we aren't already subscribed to this feed before we attempt
+ // to subscribe to it.
+ if (FeedUtils.feedAlreadyExists(aUrl, aFolder.server)) {
+ msgWindow.statusFeedback.showStatusString(
+ FeedUtils.strings.GetStringFromName("subscribe-feedAlreadySubscribed")
+ );
+ return;
+ }
+
+ let feed = new lazy.Feed(aUrl, aFolder);
+ // Default setting for new feeds per account settings.
+ feed.quickMode = feed.server.getBoolValue("quickMode");
+ feed.options = FeedUtils.getOptionsAcct(feed.server);
+
+ FeedUtils.progressNotifier.init(msgWindow, true);
+ FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
+ feed.download(true, FeedUtils.progressNotifier);
+ },
+
+ /**
+ * Enable or disable updates for all subscriptions in a folder, or all
+ * subscriptions in an account if the folder is the account folder.
+ * A folder's subfolders' feeds are not included.
+ *
+ * @param {nsIMsgFolder} aFolder - Folder or account folder (server).
+ * @param {boolean} aPause - To pause or not to pause.
+ * @param {Boolean} aBiffNow - If aPause is false, and aBiffNow is true
+ * do the biff immediately.
+ * @returns {void}
+ */
+ pauseFeedFolderUpdates(aFolder, aPause, aBiffNow) {
+ if (aFolder.isServer) {
+ let serverFolder = aFolder.server.rootFolder;
+ // Remove server from biff first. If enabling biff, this will make the
+ // latest biffMinutes take effect now rather than waiting for the timer
+ // to expire.
+ aFolder.server.doBiff = false;
+ if (!aPause) {
+ aFolder.server.doBiff = true;
+ }
+
+ FeedUtils.setStatus(serverFolder, serverFolder.URI, "enabled", !aPause);
+ if (!aPause && aBiffNow) {
+ aFolder.server.performBiff(null);
+ }
+
+ return;
+ }
+
+ let feedUrls = FeedUtils.getFeedUrlsInFolder(aFolder);
+ if (!feedUrls) {
+ return;
+ }
+
+ for (let feedUrl of feedUrls) {
+ let feed = new lazy.Feed(feedUrl, aFolder);
+ let options = feed.options;
+ options.updates.enabled = !aPause;
+ feed.options = options;
+ FeedUtils.setStatus(aFolder, feedUrl, "enabled", !aPause);
+ FeedUtils.log.debug(
+ "pauseFeedFolderUpdates: enabled:url " + !aPause + ": " + feedUrl
+ );
+ }
+
+ let win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ if (win) {
+ let curItem = win.FeedSubscriptions.mView.currentItem;
+ win.FeedSubscriptions.refreshSubscriptionView();
+ if (curItem.container) {
+ win.FeedSubscriptions.selectFolder(curItem.folder);
+ } else {
+ let feed = new lazy.Feed(curItem.url, curItem.parentFolder);
+ win.FeedSubscriptions.selectFeed(feed);
+ }
+ }
+ },
+
+ /**
+ * Add a feed record to the feeds database and update the folder's feedUrl
+ * property.
+ *
+ * @param {Feed} aFeed - Our feed object.
+ *
+ * @returns {void}
+ */
+ addFeed(aFeed) {
+ // Find or create subscription entry.
+ let ds = this.getSubscriptionsDS(aFeed.server);
+ let sub = ds.data.find(x => x.url == aFeed.url);
+ if (sub === undefined) {
+ sub = {};
+ ds.data.push(sub);
+ }
+ sub.url = aFeed.url;
+ sub.destFolder = aFeed.folder.URI;
+ if (aFeed.title) {
+ sub.title = aFeed.title;
+ }
+ ds.saveSoon();
+
+ // Update folderpane.
+ Services.obs.notifyObservers(aFeed.folder, "folder-properties-changed");
+ },
+
+ /**
+ * Delete a feed record from the feeds database and update the folder's
+ * feedUrl property.
+ *
+ * @param {Feed} aFeed - Our feed object.
+ *
+ * @returns {void}
+ */
+ deleteFeed(aFeed) {
+ // Remove items associated with this feed from the items db.
+ aFeed.invalidateItems();
+ aFeed.removeInvalidItems(true);
+
+ // Remove the entry in the subscriptions db.
+ let ds = this.getSubscriptionsDS(aFeed.server);
+ ds.data = ds.data.filter(x => x.url != aFeed.url);
+ ds.saveSoon();
+
+ // Update folderpane.
+ Services.obs.notifyObservers(aFeed.folder, "folder-properties-changed");
+ },
+
+ /**
+ * Change an existing feed's url.
+ *
+ * @param {Feed} aFeed - The feed object.
+ * @param {string} aNewUrl - New url.
+ *
+ * @returns {Boolean} - true if successful, else false.
+ */
+ changeUrlForFeed(aFeed, aNewUrl) {
+ if (!aFeed || !aFeed.folder || !aNewUrl) {
+ return false;
+ }
+
+ if (this.feedAlreadyExists(aNewUrl, aFeed.server)) {
+ this.log.info(
+ "FeedUtils.changeUrlForFeed: new feed url " +
+ aNewUrl +
+ " already subscribed in account " +
+ aFeed.server.prettyName
+ );
+ return false;
+ }
+
+ let title = aFeed.title;
+ let link = aFeed.link;
+ let quickMode = aFeed.quickMode;
+ let options = aFeed.options;
+
+ this.deleteFeed(aFeed);
+ aFeed.url = aNewUrl;
+ aFeed.title = title;
+ aFeed.link = link;
+ aFeed.quickMode = quickMode;
+ aFeed.options = options;
+ this.addFeed(aFeed);
+
+ let win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ if (win) {
+ win.FeedSubscriptions.refreshSubscriptionView(aFeed.folder, aNewUrl);
+ }
+
+ return true;
+ },
+
+ /**
+ * Determine if a message is a feed message. Prior to Tb15, a message had to
+ * be in an rss acount type folder. In Tb15 and later, a flag is set on the
+ * message itself upon initial store; the message can be moved to any folder.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The message.
+ *
+ * @returns {Boolean} - true if message is a feed, false if not.
+ */
+ isFeedMessage(aMsgHdr) {
+ return Boolean(
+ aMsgHdr instanceof Ci.nsIMsgDBHdr &&
+ (aMsgHdr.flags & Ci.nsMsgMessageFlags.FeedMsg ||
+ this.isFeedFolder(aMsgHdr.folder))
+ );
+ },
+
+ /**
+ * Determine if a folder is a feed acount folder. Trash or a folder in Trash
+ * should be checked with FeedUtils.isInTrash() if required.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ *
+ * @returns {Boolean} - true if folder's server.type is in FEED_ACCOUNT_TYPES,
+ * false if not.
+ */
+ isFeedFolder(aFolder) {
+ return Boolean(
+ aFolder instanceof Ci.nsIMsgFolder &&
+ this.FEED_ACCOUNT_TYPES.includes(aFolder.server.type)
+ );
+ },
+
+ /**
+ * Get the list of feed urls for a folder.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ *
+ * @returns {String}[] - Array of urls, or null if none.
+ */
+ getFeedUrlsInFolder(aFolder) {
+ if (
+ !aFolder ||
+ aFolder.isServer ||
+ aFolder.server.type != "rss" ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Virtual) ||
+ !aFolder.filePath.exists()
+ ) {
+ // There are never any feedUrls in the account/non-feed/trash/virtual
+ // folders or in a ghost folder (nonexistent on disk yet found in
+ // aFolder.subFolders).
+ return null;
+ }
+
+ let feedUrlArray = [];
+
+ // Get the list from the feeds database.
+ try {
+ let ds = this.getSubscriptionsDS(aFolder.server);
+ for (const sub of ds.data) {
+ if (sub.destFolder == aFolder.URI) {
+ feedUrlArray.push(sub.url);
+ }
+ }
+ } catch (ex) {
+ this.log.error("getFeedUrlsInFolder: feeds db error - " + ex);
+ this.log.error(
+ "getFeedUrlsInFolder: feeds db error for account - " +
+ aFolder.server.serverURI +
+ " : " +
+ aFolder.server.prettyName
+ );
+ }
+
+ return feedUrlArray.length ? feedUrlArray : null;
+ },
+
+ /**
+ * Check if the folder's msgDatabase is openable, reparse if desired.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ * @param {boolean} aReparse - Reparse if true.
+ * @param {nsIUrlListener} aUrlListener - Object implementing nsIUrlListener.
+ *
+ * @returns {Boolean} - true if msgDb is available, else false
+ */
+ isMsgDatabaseOpenable(aFolder, aReparse, aUrlListener) {
+ let msgDb;
+ try {
+ msgDb = Cc["@mozilla.org/msgDatabase/msgDBService;1"]
+ .getService(Ci.nsIMsgDBService)
+ .openFolderDB(aFolder, true);
+ } catch (ex) {}
+
+ if (msgDb) {
+ return true;
+ }
+
+ if (!aReparse) {
+ return false;
+ }
+
+ // Force a reparse.
+ FeedUtils.log.debug(
+ "checkMsgDb: rebuild msgDatabase for " +
+ aFolder.name +
+ " - " +
+ aFolder.filePath.path
+ );
+ try {
+ // Ignore error returns.
+ aFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .getDatabaseWithReparse(aUrlListener, null);
+ } catch (ex) {}
+
+ return false;
+ },
+
+ /**
+ * Return properties for nsITreeView getCellProperties, for a tree row item in
+ * folderpane or subscribe dialog tree.
+ *
+ * @param {nsIMsgFolder} aFolder - Folder or a feed url's parent folder.
+ * @param {string} aFeedUrl - Feed url for a feed row, null for folder.
+ *
+ * @returns {string} - Space separated properties.
+ */
+ getFolderProperties(aFolder, aFeedUrl) {
+ let folder = aFolder;
+ let feedUrls = aFeedUrl ? [aFeedUrl] : this.getFeedUrlsInFolder(aFolder);
+ if (!feedUrls && !folder.isServer) {
+ return "";
+ }
+
+ let serverEnabled = this.getStatus(
+ folder.server.rootFolder,
+ folder.server.rootFolder.URI
+ ).enabled;
+ if (folder.isServer) {
+ return !serverEnabled ? " isPaused" : "";
+ }
+
+ let properties = aFeedUrl ? " isFeed-true" : " isFeedFolder-true";
+ let hasError,
+ isBusy,
+ numPaused = 0;
+ for (let feedUrl of feedUrls) {
+ let feedStatus = this.getStatus(folder, feedUrl);
+ if (
+ feedStatus.code == FeedUtils.kNewsBlogInvalidFeed ||
+ feedStatus.code == FeedUtils.kNewsBlogRequestFailure ||
+ feedStatus.code == FeedUtils.kNewsBlogBadCertError ||
+ feedStatus.code == FeedUtils.kNewsBlogNoAuthError
+ ) {
+ hasError = true;
+ }
+ if (feedStatus.code == FeedUtils.kNewsBlogFeedIsBusy) {
+ isBusy = true;
+ }
+ if (!feedStatus.enabled) {
+ numPaused++;
+ }
+ }
+
+ properties += hasError ? " hasError" : "";
+ properties += isBusy ? " isBusy" : "";
+ properties += numPaused == feedUrls.length ? " isPaused" : "";
+
+ return properties;
+ },
+
+ /**
+ * Get a cached feed or folder status.
+ *
+ * @param {nsIMsgFolder} aFolder - Folder.
+ * @param {string} aUrl - Url key (feed url or folder URI).
+ *
+ * @returns {String} aValue - The value.
+ */
+ getStatus(aFolder, aUrl) {
+ if (!aFolder || !aUrl) {
+ return null;
+ }
+
+ let serverKey = aFolder.server.serverURI;
+ if (!this[serverKey]) {
+ this[serverKey] = {};
+ }
+
+ if (!this[serverKey][aUrl]) {
+ // Seed the status object.
+ this[serverKey][aUrl] = {};
+ this[serverKey][aUrl].status = this.statusTemplate;
+ if (FeedUtils.isValidScheme(aUrl)) {
+ // Seed persisted status properties for feed urls.
+ let feed = new lazy.Feed(aUrl, aFolder);
+ this[serverKey][aUrl].status.enabled = feed.options.updates.enabled;
+ this[serverKey][aUrl].status.updateMinutes =
+ feed.options.updates.updateMinutes;
+ this[serverKey][aUrl].status.lastUpdateTime =
+ feed.options.updates.lastUpdateTime;
+ feed = null;
+ } else {
+ // Seed persisted status properties for servers.
+ let optionsAcct = FeedUtils.getOptionsAcct(aFolder.server);
+ this[serverKey][aUrl].status.enabled = optionsAcct.doBiff;
+ }
+ FeedUtils.log.debug("getStatus: seed url - " + aUrl);
+ }
+
+ return this[serverKey][aUrl].status;
+ },
+
+ /**
+ * Update a feed or folder status and refresh folderpane.
+ *
+ * @param {nsIMsgFolder} aFolder - Folder.
+ * @param {string} aUrl - Url key (feed url or folder URI).
+ * @param {string} aProperty - Url status property.
+ * @param {string} aValue - Value.
+ *
+ * @returns {void}
+ */
+ setStatus(aFolder, aUrl, aProperty, aValue) {
+ if (!aFolder || !aUrl || !aProperty) {
+ return;
+ }
+
+ if (
+ !this[aFolder.server.serverURI] ||
+ !this[aFolder.server.serverURI][aUrl]
+ ) {
+ // Not yet seeded, so do it.
+ this.getStatus(aFolder, aUrl);
+ }
+
+ this[aFolder.server.serverURI][aUrl].status[aProperty] = aValue;
+
+ Services.obs.notifyObservers(aFolder, "folder-properties-changed");
+
+ let win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ if (win) {
+ win.FeedSubscriptions.mView.tree.invalidate();
+ }
+ },
+
+ /**
+ * Get the favicon for a feed folder subscription url (first one) or a feed
+ * message url. The favicon service caches it in memory if places history is
+ * not enabled.
+ *
+ * @param {?nsIMsgFolder} folder - The feed folder or null if url set.
+ * @param {?string} feedURL - A url (feed, message, other) or null if folder set.
+ *
+ * @returns {Promise<string>} - The favicon url or empty string.
+ */
+ async getFavicon(folder, feedURL) {
+ if (
+ !Services.prefs.getBoolPref("browser.chrome.site_icons") ||
+ !Services.prefs.getBoolPref("browser.chrome.favicons") ||
+ !Services.prefs.getBoolPref("places.history.enabled")
+ ) {
+ return "";
+ }
+
+ let url = feedURL;
+ if (!url) {
+ // Get the proposed iconUrl from the folder's first subscribed feed's
+ // <link>.
+ url = this.getFeedUrlsInFolder(folder)[0];
+ if (!url) {
+ return "";
+ }
+ feedURL = url;
+ }
+
+ if (folder) {
+ let feed = new lazy.Feed(url, folder);
+ url = feed.link && feed.link.startsWith("http") ? feed.link : url;
+ }
+
+ /**
+ * Convert a Blob to data URL.
+ * @param {Blob} blob - Blob to convert.
+ * @returns {string} data URL.
+ */
+ let blobToBase64 = blob => {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.result.endsWith("base64,")) {
+ reject(new Error(`Invalid blob encountered.`));
+ return;
+ }
+ resolve(reader.result);
+ };
+ reader.readAsDataURL(blob);
+ });
+ };
+
+ /**
+ * Try getting favicon from url.
+ * @param {string} url - The favicon url.
+ * @returns {Blob} - Existing favicon.
+ */
+ let fetchFavicon = async url => {
+ let response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`No favicon for url ${url}`);
+ }
+ if (!/^image\//i.test(response.headers.get("Content-Type"))) {
+ throw new Error(`Non-image favicon for ${url}`);
+ }
+ return response.blob();
+ };
+
+ /**
+ * Try getting favicon from the a html page.
+ * @param {string} page - The page url to check.
+ * @returns {Blob} - Found favicon.
+ */
+ let discoverFaviconURL = async page => {
+ let response = await fetch(page);
+ if (!response.ok) {
+ throw new Error(`No favicon for page ${page}`);
+ }
+ if (!/^text\/html/i.test(response.headers.get("Content-Type"))) {
+ throw new Error(`No page to get favicon from for ${page}`);
+ }
+ let doc = new DOMParser().parseFromString(
+ await response.text(),
+ "text/html"
+ );
+ let iconLink = doc.querySelector(
+ `link[href][rel~='icon']:is([sizes~='any'],[sizes~='16x16' i],[sizes~='32x32' i],:not([sizes])`
+ );
+ if (!iconLink) {
+ throw new Error(`No iconLink discovered for page=${page}`);
+ }
+ if (/^https?:/.test(iconLink.href)) {
+ return iconLink.href;
+ }
+ if (iconLink.href.at(0) != "/") {
+ iconLink.href = "/" + iconLink.href;
+ }
+ return new URL(page).origin + iconLink.href;
+ };
+
+ let uri = Services.io.newURI(url);
+ let iconURL = await fetchFavicon(uri.prePath + "/favicon.ico")
+ .then(blobToBase64)
+ .catch(e => {
+ return discoverFaviconURL(url)
+ .catch(() => discoverFaviconURL(uri.prePath))
+ .then(fetchFavicon)
+ .then(blobToBase64)
+ .catch(() => "");
+ });
+
+ if (!iconURL) {
+ return "";
+ }
+
+ // setAndFetchFaviconForPage needs the url to be in the database first.
+ await lazy.PlacesUtils.history.insert({
+ url: feedURL,
+ visits: [
+ {
+ date: new Date(),
+ },
+ ],
+ });
+ return new Promise(resolve => {
+ // All good. Now store iconURL for future usage.
+ lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ Services.io.newURI(feedURL),
+ Services.io.newURI(iconURL),
+ true,
+ lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ faviconURI => {
+ resolve(faviconURI.spec);
+ },
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+ },
+
+ /**
+ * Update the feeds database for rename and move/copy folder name changes.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder, new if rename or target of
+ * move/copy folder (new parent).
+ * @param {nsIMsgFolder} aOrigFolder - Original folder.
+ * @param {String} aAction - "move" or "copy" or "rename".
+ *
+ * @returns {void}
+ */
+ updateSubscriptionsDS(aFolder, aOrigFolder, aAction) {
+ this.log.debug(
+ "FeedUtils.updateSubscriptionsDS: " +
+ "\nfolder changed - " +
+ aAction +
+ "\nnew folder - " +
+ aFolder.filePath.path +
+ "\norig folder - " +
+ aOrigFolder.filePath.path
+ );
+
+ this.log.debug(
+ `updateSubscriptions(${aFolder.name}, ${aOrigFolder.name}, ${aAction})`
+ );
+
+ if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aOrigFolder)) {
+ // Target not a feed account folder; nothing to do, or move/rename in
+ // trash; no subscriptions already.
+ return;
+ }
+
+ let newFolder = aFolder;
+ let newParentURI = aFolder.URI;
+ let origParentURI = aOrigFolder.URI;
+ if (aAction == "move" || aAction == "copy") {
+ // Get the new folder. Don't process the entire parent (new dest folder)!
+ newFolder = aFolder.getChildNamed(aOrigFolder.name);
+ origParentURI = aOrigFolder.parent
+ ? aOrigFolder.parent.URI
+ : aOrigFolder.rootFolder.URI;
+ }
+
+ this.updateFolderChangeInFeedsDS(newFolder, aOrigFolder, null, null);
+
+ // There may be subfolders, but we only get a single notification; iterate
+ // over all descendent folders of the folder whose location has changed.
+ for (let newSubFolder of newFolder.descendants) {
+ FeedUtils.updateFolderChangeInFeedsDS(
+ newSubFolder,
+ aOrigFolder,
+ newParentURI,
+ origParentURI
+ );
+ }
+ },
+
+ /**
+ * Update the feeds database with the new folder's or subfolder's location
+ * for rename and move/copy name changes. The feeds subscriptions db is
+ * also synced on cross account folder copies. Note that if a copied folder's
+ * url exists in the new account, its active subscription will be switched to
+ * the folder being copied, to enforce the one unique url per account design.
+ *
+ * @param {nsIMsgFolder} aFolder - New folder.
+ * @param {nsIMsgFolder} aOrigFolder - Original folder.
+ * @param {string} aNewAncestorURI - For subfolders, ancestor new folder.
+ * @param {String} aOrigAncestorURI - For subfolders, ancestor original folder.
+ *
+ * @returns {void}
+ */
+ updateFolderChangeInFeedsDS(
+ aFolder,
+ aOrigFolder,
+ aNewAncestorURI,
+ aOrigAncestorURI
+ ) {
+ this.log.debug(
+ "updateFolderChangeInFeedsDS: " +
+ "\naFolder - " +
+ aFolder.URI +
+ "\naOrigFolder - " +
+ aOrigFolder.URI +
+ "\naOrigAncestor - " +
+ aOrigAncestorURI +
+ "\naNewAncestor - " +
+ aNewAncestorURI
+ );
+
+ // Get the original folder's URI.
+ let folderURI = aFolder.URI;
+ let origURI =
+ aNewAncestorURI && aOrigAncestorURI
+ ? folderURI.replace(aNewAncestorURI, aOrigAncestorURI)
+ : aOrigFolder.URI;
+ this.log.debug("updateFolderChangeInFeedsDS: urls origURI - " + origURI);
+
+ // Get affected feed subscriptions - all the ones in the original folder.
+ let origDS = this.getSubscriptionsDS(aOrigFolder.server);
+ let affectedSubs = origDS.data.filter(sub => sub.destFolder == origURI);
+ if (affectedSubs.length == 0) {
+ this.log.debug("updateFolderChangeInFeedsDS: no feedUrls in this folder");
+ return;
+ }
+
+ if (this.isInTrash(aFolder)) {
+ // Moving to trash. Unsubscribe.
+ affectedSubs.forEach(function (sub) {
+ let feed = new lazy.Feed(sub.url, aFolder);
+ FeedUtils.deleteFeed(feed);
+ });
+ // note: deleteFeed() calls saveSoon(), so we don't need to.
+ } else if (aFolder.server == aOrigFolder.server) {
+ // Staying in same account - just update destFolder as required
+ for (let sub of affectedSubs) {
+ sub.destFolder = folderURI;
+ }
+ origDS.saveSoon();
+ } else {
+ // Moving between accounts.
+ let destDS = this.getSubscriptionsDS(aFolder.server);
+ for (let sub of affectedSubs) {
+ // Move to the new subscription db (replacing any existing entry).
+ origDS.data = origDS.data.filter(x => x.url != sub.url);
+ destDS.data = destDS.data.filter(x => x.url != sub.url);
+ sub.destFolder = folderURI;
+ destDS.data.push(sub);
+ }
+
+ origDS.saveSoon();
+ destDS.saveSoon();
+ }
+ },
+
+ /**
+ * When subscribing to feeds by dnd on, or adding a url to, the account
+ * folder (only), or creating folder structure via opml import, a subfolder is
+ * autocreated and thus the derived/given name must be sanitized to prevent
+ * filesystem errors. Hashing invalid chars based on OS rather than filesystem
+ * is not strictly correct.
+ *
+ * @param {nsIMsgFolder} aParentFolder - Parent folder.
+ * @param {String} aProposedName - Proposed name.
+ * @param {String} aDefaultName - Default name if proposed sanitizes to
+ * blank, caller ensures sane value.
+ * @param {Boolean} aUnique - If true, return a unique indexed name.
+ *
+ * @returns {String} - Sanitized unique name.
+ */
+ getSanitizedFolderName(aParentFolder, aProposedName, aDefaultName, aUnique) {
+ // Clean up the name for the strictest fs (fat) and to ensure portability.
+ // 1) Replace line breaks and tabs '\n\r\t' with a space.
+ // 2) Remove nonprintable ascii.
+ // 3) Remove invalid win chars '* | \ / : < > ? "'.
+ // 4) Remove all '.' as starting/ending with one is trouble on osx/win.
+ // 5) No leading/trailing spaces.
+ /* eslint-disable no-control-regex */
+ let folderName = aProposedName
+ .replace(/[\n\r\t]+/g, " ")
+ .replace(/[\x00-\x1F]+/g, "")
+ .replace(/[*|\\\/:<>?"]+/g, "")
+ .replace(/[\.]+/g, "")
+ .trim();
+ /* eslint-enable no-control-regex */
+
+ // Prefix with __ if name is:
+ // 1) a reserved win filename.
+ // 2) an undeletable/unrenameable special folder name (bug 259184).
+ if (
+ folderName
+ .toUpperCase()
+ .match(/^COM\d$|^LPT\d$|^CON$|PRN$|^AUX$|^NUL$|^CLOCK\$/) ||
+ folderName
+ .toUpperCase()
+ .match(/^INBOX$|^OUTBOX$|^UNSENT MESSAGES$|^TRASH$/)
+ ) {
+ folderName = "__" + folderName;
+ }
+
+ // Use a default if no name is found.
+ if (!folderName) {
+ folderName = aDefaultName;
+ }
+
+ if (!aUnique) {
+ return folderName;
+ }
+
+ // Now ensure the folder name is not a dupe; if so append index.
+ let folderNameBase = folderName;
+ let i = 2;
+ while (aParentFolder.containsChildNamed(folderName)) {
+ folderName = folderNameBase + "-" + i++;
+ }
+
+ return folderName;
+ },
+
+ /**
+ * This object contains feed/account status info.
+ */
+ _statusDefault: {
+ // Derived from persisted value.
+ enabled: null,
+ // Last update result code, a kNewsBlog* value.
+ code: 0,
+ updateMinutes: null,
+ // JS Date; startup state is null indicating no update since startup.
+ lastUpdateTime: null,
+ },
+
+ get statusTemplate() {
+ // Copy the object.
+ return JSON.parse(JSON.stringify(this._statusDefault));
+ },
+
+ /**
+ * This object will contain all persisted feed specific properties.
+ */
+ _optionsDefault: {
+ version: 2,
+ updates: {
+ enabled: true,
+ // User set.
+ updateMinutes: 100,
+ // User set: "min"=minutes, "d"=days
+ updateUnits: "min",
+ // JS Date.
+ lastUpdateTime: null,
+ // The last time a new message was stored. JS Date.
+ lastDownloadTime: null,
+ // Publisher recommended from the feed.
+ updatePeriod: null,
+ updateFrequency: 1,
+ updateBase: null,
+ },
+ // Autotag and <category> handling options.
+ category: {
+ enabled: false,
+ prefixEnabled: false,
+ prefix: null,
+ },
+ },
+
+ get optionsTemplate() {
+ // Copy the object.
+ return JSON.parse(JSON.stringify(this._optionsDefault));
+ },
+
+ getOptionsAcct(aServer) {
+ let optionsAcct;
+ let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
+ let check_new_mail = "mail.server." + aServer.key + ".check_new_mail";
+ let check_time = "mail.server." + aServer.key + ".check_time";
+
+ // Biff enabled or not. Make sure pref exists.
+ if (!Services.prefs.prefHasUserValue(check_new_mail)) {
+ Services.prefs.setBoolPref(check_new_mail, true);
+ }
+
+ // System polling interval. Make sure pref exists.
+ if (!Services.prefs.prefHasUserValue(check_time)) {
+ Services.prefs.setIntPref(check_time, FeedUtils.kBiffPollMinutes);
+ }
+
+ try {
+ optionsAcct = JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
+ // Add the server specific biff enabled state.
+ optionsAcct.doBiff = Services.prefs.getBoolPref(check_new_mail);
+ } catch (ex) {}
+
+ if (optionsAcct && optionsAcct.version == this._optionsDefault.version) {
+ return optionsAcct;
+ }
+
+ // Init account updates options if new or upgrading to version in
+ // |_optionsDefault.version|.
+ if (!optionsAcct || optionsAcct.version < this._optionsDefault.version) {
+ this.initAcct(aServer);
+ }
+
+ let newOptions = this.newOptions(optionsAcct);
+ this.setOptionsAcct(aServer, newOptions);
+ newOptions.doBiff = Services.prefs.getBoolPref(check_new_mail);
+ return newOptions;
+ },
+
+ setOptionsAcct(aServer, aOptions) {
+ let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
+ aOptions = aOptions || this.optionsTemplate;
+ Services.prefs.setCharPref(optionsAcctPref, JSON.stringify(aOptions));
+ },
+
+ initAcct(aServer) {
+ let serverPrefStr = "mail.server." + aServer.key;
+ // System polling interval. Effective after current interval expires on
+ // change; no user facing ui.
+ Services.prefs.setIntPref(
+ serverPrefStr + ".check_time",
+ FeedUtils.kBiffPollMinutes
+ );
+
+ // If this pref is false, polling on biff is disabled and account updates
+ // are paused; ui in account server settings and folderpane context menu
+ // (Pause All Updates). Checking Enable updates or unchecking Pause takes
+ // effect immediately. Here on startup, we just ensure the polling interval
+ // above is reset immediately.
+ let doBiff = Services.prefs.getBoolPref(serverPrefStr + ".check_new_mail");
+ FeedUtils.log.debug(
+ "initAcct: " + aServer.prettyName + " doBiff - " + doBiff
+ );
+ this.pauseFeedFolderUpdates(aServer.rootFolder, !doBiff, false);
+ },
+
+ newOptions(aCurrentOptions) {
+ if (!aCurrentOptions) {
+ return this.optionsTemplate;
+ }
+
+ // Options version has changed; meld current template with existing
+ // aCurrentOptions settings, removing items gone from the template while
+ // preserving user settings for retained template items.
+ let newOptions = this.optionsTemplate;
+ this.Mixins.meld(aCurrentOptions, false, true).into(newOptions);
+ newOptions.version = this.optionsTemplate.version;
+ return newOptions;
+ },
+
+ /**
+ * A generalized recursive melder of two objects. Getters/setters not included.
+ */
+ Mixins: {
+ meld(source, keep, replace) {
+ function meldin(source, target, keep, replace) {
+ for (let attribute in source) {
+ // Recurse for objects.
+ if (
+ typeof source[attribute] == "object" &&
+ typeof target[attribute] == "object"
+ ) {
+ meldin(source[attribute], target[attribute], keep, replace);
+ } else {
+ // Use attribute values from source for the target, unless
+ // replace is false.
+ if (attribute in target && !replace) {
+ continue;
+ }
+ // Don't copy attribute from source to target if it is not in the
+ // target, unless keep is true.
+ if (!(attribute in target) && !keep) {
+ continue;
+ }
+
+ target[attribute] = source[attribute];
+ }
+ }
+ }
+ return {
+ source,
+ into(target) {
+ meldin(this.source, target, keep, replace);
+ },
+ };
+ },
+ },
+
+ /**
+ * Returns a reference to the feed subscriptions store for the given server
+ * (the feeds.json data).
+ *
+ * @param {nsIMsgIncomingServer} aServer - server to fetch item data for.
+ * @returns {JSONFile} - a JSONFile holding the array of feed subscriptions
+ * in its data field.
+ */
+ getSubscriptionsDS(aServer) {
+ if (this[aServer.serverURI] && this[aServer.serverURI].FeedsDS) {
+ return this[aServer.serverURI].FeedsDS;
+ }
+
+ let rssServer = aServer.QueryInterface(Ci.nsIRssIncomingServer);
+ let feedsFile = rssServer.subscriptionsPath; // Path to feeds.json
+ let exists = feedsFile.exists();
+ let ds = new lazy.JSONFile({
+ path: feedsFile.path,
+ backupTo: feedsFile.path + ".backup",
+ });
+ ds.ensureDataReady();
+ if (!this[aServer.serverURI]) {
+ this[aServer.serverURI] = {};
+ }
+ this[aServer.serverURI].FeedsDS = ds;
+ if (!exists) {
+ // No feeds.json, so we need to initialise.
+ ds.data = [];
+ }
+ return ds;
+ },
+
+ /**
+ * Fetch an attribute for a subscribed feed.
+ *
+ * @param {string} feedURL - URL of the feed.
+ * @param {nsIMsgIncomingServer} server - Server holding the subscription.
+ * @param {String} attrName - Name of attribute to fetch.
+ * @param {undefined} defaultValue - Value to return if not found.
+ *
+ * @returns {undefined} - the fetched value (defaultValue if the
+ * subscription or attribute doesn't exist).
+ */
+ getSubscriptionAttr(feedURL, server, attrName, defaultValue) {
+ let ds = this.getSubscriptionsDS(server);
+ let sub = ds.data.find(feed => feed.url == feedURL);
+ if (sub === undefined || sub[attrName] === undefined) {
+ return defaultValue;
+ }
+ return sub[attrName];
+ },
+
+ /**
+ * Set an attribute for a feed in the subscriptions store.
+ * NOTE: If the feed is not already in the store, it will be
+ * added.
+ *
+ * @param {string} feedURL - URL of the feed.
+ * @param {nsIMsgIncomingServer} server - server holding subscription.
+ * @param {String} attrName - Name of attribute to fetch.
+ * @param {undefined} value - Value to store.
+ *
+ * @returns {void}
+ */
+ setSubscriptionAttr(feedURL, server, attrName, value) {
+ let ds = this.getSubscriptionsDS(server);
+ let sub = ds.data.find(feed => feed.url == feedURL);
+ if (sub === undefined) {
+ // Add a new entry.
+ sub = { url: feedURL };
+ ds.data.push(sub);
+ }
+ sub[attrName] = value;
+ ds.saveSoon();
+ },
+
+ /**
+ * Returns a reference to the feeditems store for the given server
+ * (the feeditems.json data).
+ *
+ * @param {nsIMsgIncomingServer} aServer - server to fetch item data for.
+ * @returns {JSONFile} - JSONFile with data field containing a collection
+ * of feeditems indexed by item url.
+ */
+ getItemsDS(aServer) {
+ if (this[aServer.serverURI] && this[aServer.serverURI].FeedItemsDS) {
+ return this[aServer.serverURI].FeedItemsDS;
+ }
+
+ let rssServer = aServer.QueryInterface(Ci.nsIRssIncomingServer);
+ let itemsFile = rssServer.feedItemsPath; // Path to feeditems.json
+ let exists = itemsFile.exists();
+ let ds = new lazy.JSONFile({
+ path: itemsFile.path,
+ backupTo: itemsFile.path + ".backup",
+ });
+ ds.ensureDataReady();
+ if (!this[aServer.serverURI]) {
+ this[aServer.serverURI] = {};
+ }
+ this[aServer.serverURI].FeedItemsDS = ds;
+ if (!exists) {
+ // No feeditems.json, need to initialise our data.
+ ds.data = {};
+ }
+ return ds;
+ },
+
+ /**
+ * Dragging something from somewhere. It may be a nice x-moz-url or from a
+ * browser or app that provides a less nice dataTransfer object in the event.
+ * Extract the url and if it passes the scheme test, try to subscribe.
+ *
+ * @param {nsISupports} aDataTransfer - The dnd event's dataTransfer.
+ *
+ * @returns {nsIURI} or null - A uri if valid, null if none.
+ */
+ getFeedUriFromDataTransfer(aDataTransfer) {
+ let dt = aDataTransfer;
+ let types = ["text/x-moz-url-data", "text/x-moz-url"];
+ let validUri = false;
+ let uri;
+
+ if (dt.getData(types[0])) {
+ // The url is the data.
+ uri = Services.io.newURI(dt.mozGetDataAt(types[0], 0));
+ validUri = this.isValidScheme(uri);
+ this.log.trace(
+ "getFeedUriFromDataTransfer: dropEffect:type:value - " +
+ dt.dropEffect +
+ " : " +
+ types[0] +
+ " : " +
+ uri.spec
+ );
+ } else if (dt.getData(types[1])) {
+ // The url is the first part of the data, the second part is random.
+ uri = Services.io.newURI(dt.mozGetDataAt(types[1], 0).split("\n")[0]);
+ validUri = this.isValidScheme(uri);
+ this.log.trace(
+ "getFeedUriFromDataTransfer: dropEffect:type:value - " +
+ dt.dropEffect +
+ " : " +
+ types[0] +
+ " : " +
+ uri.spec
+ );
+ } else {
+ // Go through the types and see if there's a url; get the first one.
+ for (let i = 0; i < dt.types.length; i++) {
+ let spec = dt.mozGetDataAt(dt.types[i], 0);
+ this.log.trace(
+ "getFeedUriFromDataTransfer: dropEffect:index:type:value - " +
+ dt.dropEffect +
+ " : " +
+ i +
+ " : " +
+ dt.types[i] +
+ " : " +
+ spec
+ );
+ try {
+ uri = Services.io.newURI(spec);
+ validUri = this.isValidScheme(uri);
+ } catch (ex) {}
+
+ if (validUri) {
+ break;
+ }
+ }
+ }
+
+ return validUri ? uri : null;
+ },
+
+ /**
+ * Returns security/certificate/network error details for an XMLHTTPRequest.
+ *
+ * @param {XMLHTTPRequest} xhr - The xhr request.
+ *
+ * @returns {Array}[{String} errType or null, {String} errName or null]
+ * - Array with 2 error codes, (nulls if not determined).
+ */
+ createTCPErrorFromFailedXHR(xhr) {
+ let status = xhr.channel.QueryInterface(Ci.nsIRequest).status;
+
+ let errType = null;
+ let errName = null;
+ if ((status & 0xff0000) === 0x5a0000) {
+ // Security module.
+ const nsINSSErrorsService = Ci.nsINSSErrorsService;
+ let nssErrorsService =
+ Cc["@mozilla.org/nss_errors_service;1"].getService(nsINSSErrorsService);
+ let errorClass;
+
+ // getErrorClass()) will throw a generic NS_ERROR_FAILURE if the error
+ // code is somehow not in the set of covered errors.
+ try {
+ errorClass = nssErrorsService.getErrorClass(status);
+ } catch (ex) {
+ // Catch security protocol exception.
+ errorClass = "SecurityProtocol";
+ }
+
+ if (errorClass == nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ errType = "SecurityCertificate";
+ } else {
+ errType = "SecurityProtocol";
+ }
+
+ // NSS_SEC errors (happen below the base value because of negative vals).
+ if (
+ (status & 0xffff) <
+ Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)
+ ) {
+ // The bases are actually negative, so in our positive numeric space,
+ // we need to subtract the base off our value.
+ let nssErr =
+ Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
+
+ switch (nssErr) {
+ case 11: // SEC_ERROR_EXPIRED_CERTIFICATE, sec(11)
+ errName = "SecurityExpiredCertificateError";
+ break;
+ case 12: // SEC_ERROR_REVOKED_CERTIFICATE, sec(12)
+ errName = "SecurityRevokedCertificateError";
+ break;
+
+ // Per bsmith, we will be unable to tell these errors apart very soon,
+ // so it makes sense to just folder them all together already.
+ case 13: // SEC_ERROR_UNKNOWN_ISSUER, sec(13)
+ case 20: // SEC_ERROR_UNTRUSTED_ISSUER, sec(20)
+ case 21: // SEC_ERROR_UNTRUSTED_CERT, sec(21)
+ case 36: // SEC_ERROR_CA_CERT_INVALID, sec(36)
+ errName = "SecurityUntrustedCertificateIssuerError";
+ break;
+ case 90: // SEC_ERROR_INADEQUATE_KEY_USAGE, sec(90)
+ errName = "SecurityInadequateKeyUsageError";
+ break;
+ case 176: // SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, sec(176)
+ errName = "SecurityCertificateSignatureAlgorithmDisabledError";
+ break;
+ default:
+ errName = "SecurityError";
+ break;
+ }
+ } else {
+ // Calculating the difference.
+ let sslErr =
+ Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
+
+ switch (sslErr) {
+ case 3: // SSL_ERROR_NO_CERTIFICATE, ssl(3)
+ errName = "SecurityNoCertificateError";
+ break;
+ case 4: // SSL_ERROR_BAD_CERTIFICATE, ssl(4)
+ errName = "SecurityBadCertificateError";
+ break;
+ case 8: // SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE, ssl(8)
+ errName = "SecurityUnsupportedCertificateTypeError";
+ break;
+ case 9: // SSL_ERROR_UNSUPPORTED_VERSION, ssl(9)
+ errName = "SecurityUnsupportedTLSVersionError";
+ break;
+ case 12: // SSL_ERROR_BAD_CERT_DOMAIN, ssl(12)
+ errName = "SecurityCertificateDomainMismatchError";
+ break;
+ default:
+ errName = "SecurityError";
+ break;
+ }
+ }
+ } else {
+ errType = "Network";
+ switch (status) {
+ // Connect to host:port failed.
+ case 0x804b000c: // NS_ERROR_CONNECTION_REFUSED, network(13)
+ errName = "ConnectionRefusedError";
+ break;
+ // network timeout error.
+ case 0x804b000e: // NS_ERROR_NET_TIMEOUT, network(14)
+ errName = "NetworkTimeoutError";
+ break;
+ // Hostname lookup failed.
+ case 0x804b001e: // NS_ERROR_UNKNOWN_HOST, network(30)
+ errName = "DomainNotFoundError";
+ break;
+ case 0x804b0047: // NS_ERROR_NET_INTERRUPT, network(71)
+ errName = "NetworkInterruptError";
+ break;
+ default:
+ errName = "NetworkError";
+ break;
+ }
+ }
+
+ return [errType, errName];
+ },
+
+ /**
+ * Returns if a uri/url is valid to subscribe.
+ *
+ * @param {nsIURI} aUri or {String} aUrl - The Uri/Url.
+ *
+ * @returns {boolean} - true if a valid scheme, false if not.
+ */
+ _validSchemes: ["http", "https", "file"],
+ isValidScheme(aUri) {
+ if (!(aUri instanceof Ci.nsIURI)) {
+ try {
+ aUri = Services.io.newURI(aUri);
+ } catch (ex) {
+ return false;
+ }
+ }
+
+ return this._validSchemes.includes(aUri.scheme);
+ },
+
+ /**
+ * Is a folder Trash or in Trash.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ *
+ * @returns {Boolean} - true if folder is Trash else false.
+ */
+ isInTrash(aFolder) {
+ let trashFolder = aFolder.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Trash
+ );
+ if (
+ trashFolder &&
+ (trashFolder == aFolder || trashFolder.isAncestorOf(aFolder))
+ ) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Return a folder path string constructed from individual folder UTF8 names
+ * stored as properties (not possible hashes used to construct disk foldername).
+ *
+ * @param {nsIMsgFolder} aFolder - The folder.
+ *
+ * @returns {String} prettyName or null - Name or null if not a disk folder.
+ */
+ getFolderPrettyPath(aFolder) {
+ let msgFolder = lazy.MailUtils.getExistingFolder(aFolder.URI);
+ if (!msgFolder) {
+ // Not a real folder uri.
+ return null;
+ }
+
+ if (msgFolder.URI == msgFolder.server.serverURI) {
+ return msgFolder.server.prettyName;
+ }
+
+ // Server part first.
+ let pathParts = [msgFolder.server.prettyName];
+ let rawPathParts = msgFolder.URI.split(msgFolder.server.serverURI + "/");
+ let folderURI = msgFolder.server.serverURI;
+ rawPathParts = rawPathParts[1].split("/");
+ for (let i = 0; i < rawPathParts.length - 1; i++) {
+ // Two or more folders deep parts here.
+ folderURI += "/" + rawPathParts[i];
+ msgFolder = lazy.MailUtils.getExistingFolder(folderURI);
+ pathParts.push(msgFolder.name);
+ }
+
+ // Leaf folder last.
+ pathParts.push(aFolder.name);
+ return pathParts.join("/");
+ },
+
+ /**
+ * Date validator for feeds.
+ *
+ * @param {string} aDate - Date string.
+ *
+ * @returns {Boolean} - true if passes regex test, false if not.
+ */
+ isValidRFC822Date(aDate) {
+ const FZ_RFC822_RE =
+ "^(((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)), *)?\\d\\d?" +
+ " +((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec))" +
+ " +\\d\\d(\\d\\d)? +\\d\\d:\\d\\d(:\\d\\d)? +(([+-]?\\d\\d\\d\\d)|(UT)|(GMT)" +
+ "|(EST)|(EDT)|(CST)|(CDT)|(MST)|(MDT)|(PST)|(PDT)|\\w)$";
+ let regex = new RegExp(FZ_RFC822_RE);
+ return regex.test(aDate);
+ },
+
+ /**
+ * Create rfc5322 date.
+ *
+ * @param {string} aDateString - Optional date string; if null or invalid
+ * date, get the current datetime.
+ *
+ * @returns {String} - An rfc5322 date string.
+ */
+ getValidRFC5322Date(aDateString) {
+ let d = new Date(aDateString || new Date().getTime());
+ d = isNaN(d.getTime()) ? new Date() : d;
+ return lazy.jsmime.headeremitter
+ .emitStructuredHeader("Date", d, {})
+ .substring(6)
+ .trim();
+ },
+
+ /**
+ * Progress glue code. Acts as a go between the RSS back end and the mail
+ * window front end determined by the aMsgWindow parameter passed into
+ * nsINewsBlogFeedDownloader.
+ */
+ progressNotifier: {
+ mSubscribeMode: false,
+ mMsgWindow: null,
+ mStatusFeedback: null,
+ mFeeds: {},
+ // Keeps track of the total number of feeds we have been asked to download.
+ // This number may not reflect the # of entries in our mFeeds array because
+ // not all feeds may have reported in for the first time.
+ mNumPendingFeedDownloads: 0,
+
+ /**
+ * @param {?nsIMsgWindow} aMsgWindow - Associated aMsgWindow if any.
+ * @param {boolean} aSubscribeMode - Whether we're in subscribe mode.
+ * @returns {void}
+ */
+ init(aMsgWindow, aSubscribeMode) {
+ if (this.mNumPendingFeedDownloads == 0) {
+ // If we aren't already in the middle of downloading feed items.
+ this.mStatusFeedback = aMsgWindow ? aMsgWindow.statusFeedback : null;
+ this.mSubscribeMode = aSubscribeMode;
+ this.mMsgWindow = aMsgWindow;
+
+ if (this.mStatusFeedback) {
+ this.mStatusFeedback.startMeteors();
+ this.mStatusFeedback.showStatusString(
+ FeedUtils.strings.GetStringFromName(
+ aSubscribeMode
+ ? "subscribe-validating-feed"
+ : "newsblog-getNewMsgsCheck"
+ )
+ );
+ }
+ }
+ },
+
+ /**
+ * Called on final success or error resolution of a feed download and
+ * parsing. If aDisable is true, the error shouldn't be retried continually
+ * and the url should be verified by the user. A bad html response code or
+ * cert error will cause the url to be disabled, while general network
+ * connectivity errors applying to all urls will not.
+ *
+ * @param {Feed} feed - The Feed object, or a synthetic object that must
+ * contain members {nsIMsgFolder folder, String url}.
+ * @param {Integer} aErrorCode - The resolution code, a kNewsBlog* value.
+ * @param {Boolean} aDisable - If true, disable/pause the feed.
+ *
+ * @returns {void}
+ */
+ downloaded(feed, aErrorCode, aDisable) {
+ let folderName = feed.folder
+ ? feed.folder.name
+ : feed.server.rootFolder.prettyName;
+ FeedUtils.log.debug(
+ "downloaded: " +
+ (this.mSubscribeMode ? "Subscribe " : "Update ") +
+ "errorCode:folderName:feedUrl - " +
+ aErrorCode +
+ " : " +
+ folderName +
+ " : " +
+ feed.url
+ );
+ if (this.mSubscribeMode) {
+ if (aErrorCode == FeedUtils.kNewsBlogSuccess) {
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+
+ // Nice touch: notify so the window ca select the folder that now
+ // contains the newly subscribed feed.
+ // This is particularly nice if we just finished subscribing
+ // to a feed URL that the operating system gave us.
+ Services.obs.notifyObservers(feed.folder, "folder-subscribed");
+
+ // Check for an existing feed subscriptions window and update it.
+ let subscriptionsWindow = Services.wm.getMostRecentWindow(
+ "Mail:News-BlogSubscriptions"
+ );
+ if (subscriptionsWindow) {
+ subscriptionsWindow.FeedSubscriptions.FolderListener.folderAdded(
+ feed.folder
+ );
+ }
+ } else if (feed && feed.url && feed.server) {
+ // Non success. Remove intermediate traces from the feeds database.
+ FeedUtils.deleteFeed(feed);
+ }
+ }
+
+ if (aErrorCode != FeedUtils.kNewsBlogFeedIsBusy) {
+ if (
+ aErrorCode == FeedUtils.kNewsBlogSuccess ||
+ aErrorCode == FeedUtils.kNewsBlogNoNewItems
+ ) {
+ // Update lastUpdateTime only if successful normal processing.
+ let options = feed.options;
+ let now = Date.now();
+ options.updates.lastUpdateTime = now;
+ if (feed.itemsStored) {
+ options.updates.lastDownloadTime = now;
+ }
+
+ // If a previously disabled due to error feed is successful, set
+ // enabled state on, as that was the desired user setting.
+ if (options.updates.enabled == null) {
+ options.updates.enabled = true;
+ FeedUtils.setStatus(feed.folder, feed.url, "enabled", true);
+ }
+
+ feed.options = options;
+ FeedUtils.setStatus(feed.folder, feed.url, "lastUpdateTime", now);
+ } else if (aDisable) {
+ if (
+ Services.prefs.getBoolPref("rss.disable_feeds_on_update_failure")
+ ) {
+ // Do not keep retrying feeds with error states. Set persisted state
+ // to |null| to indicate error disable (and not user disable), but
+ // only if the feed is user enabled.
+ let options = feed.options;
+ if (options.updates.enabled) {
+ options.updates.enabled = null;
+ }
+
+ feed.options = options;
+ FeedUtils.setStatus(feed.folder, feed.url, "enabled", false);
+ FeedUtils.log.warn(
+ "downloaded: updates disabled due to error, " +
+ "check the url - " +
+ feed.url
+ );
+ } else {
+ FeedUtils.log.warn(
+ "downloaded: update failed, check the url - " + feed.url
+ );
+ }
+ }
+
+ if (!this.mSubscribeMode) {
+ FeedUtils.setStatus(feed.folder, feed.url, "code", aErrorCode);
+
+ if (
+ feed.folder &&
+ !FeedUtils.getFolderProperties(feed.folder).includes("isBusy")
+ ) {
+ // Free msgDatabase after new mail biff is set; if busy let the next
+ // result do the freeing. Otherwise new messages won't be indicated.
+ // This feed may belong to a folder with multiple other feeds, some
+ // of which may not yet be finished, so free only if the folder is
+ // no longer busy.
+ feed.folder.msgDatabase = null;
+ FeedUtils.log.debug(
+ "downloaded: msgDatabase freed - " + feed.folder.name
+ );
+ }
+ }
+ }
+
+ let message = "";
+ switch (aErrorCode) {
+ case FeedUtils.kNewsBlogSuccess:
+ case FeedUtils.kNewsBlogFeedIsBusy:
+ message = "";
+ break;
+ case FeedUtils.kNewsBlogNoNewItems:
+ message =
+ feed.url +
+ ". " +
+ FeedUtils.strings.GetStringFromName(
+ "newsblog-noNewArticlesForFeed"
+ );
+ break;
+ case FeedUtils.kNewsBlogInvalidFeed:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-feedNotValid",
+ [feed.url]
+ );
+ break;
+ case FeedUtils.kNewsBlogRequestFailure:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-networkError",
+ [feed.url]
+ );
+ break;
+ case FeedUtils.kNewsBlogFileError:
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile"
+ );
+ break;
+ case FeedUtils.kNewsBlogBadCertError:
+ let host = Services.io.newURI(feed.url).host;
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-badCertError",
+ [host]
+ );
+ break;
+ case FeedUtils.kNewsBlogNoAuthError:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-noAuthError",
+ [feed.url]
+ );
+ break;
+ }
+
+ if (message) {
+ let location =
+ FeedUtils.getFolderPrettyPath(feed.folder || feed.server.rootFolder) +
+ " -> ";
+ FeedUtils.log.info(
+ "downloaded: " +
+ (this.mSubscribeMode ? "Subscribe: " : "Update: ") +
+ location +
+ message
+ );
+ }
+
+ if (this.mStatusFeedback) {
+ this.mStatusFeedback.showStatusString(message);
+ this.mStatusFeedback.stopMeteors();
+ }
+
+ this.mNumPendingFeedDownloads--;
+
+ if (this.mNumPendingFeedDownloads == 0) {
+ this.mFeeds = {};
+ this.mSubscribeMode = false;
+ FeedUtils.log.debug("downloaded: all pending downloads finished");
+
+ // Should we do this on a timer so the text sticks around for a little
+ // while? It doesn't look like we do it on a timer for newsgroups so
+ // we'll follow that model. Don't clear the status text if we just
+ // dumped an error to the status bar!
+ if (aErrorCode == FeedUtils.kNewsBlogSuccess && this.mStatusFeedback) {
+ this.mStatusFeedback.showStatusString("");
+ }
+ }
+
+ feed = null;
+ },
+
+ /**
+ * This gets called after the RSS parser finishes storing a feed item to
+ * disk. aCurrentFeedItems is an integer corresponding to how many feed
+ * items have been downloaded so far. aMaxFeedItems is an integer
+ * corresponding to the total number of feed items to download.
+ *
+ * @param {Feed} feed - The Feed object.
+ * @param {Integer} aCurrentFeedItems - Number downloaded so far.
+ * @param {Integer} aMaxFeedItems - Total number to download.
+ *
+ * @returns {void}
+ */
+ onFeedItemStored(feed, aCurrentFeedItems, aMaxFeedItems) {
+ // We currently don't do anything here. Eventually we may add status
+ // text about the number of new feed articles received.
+
+ if (this.mSubscribeMode && this.mStatusFeedback) {
+ // If we are subscribing to a feed, show feed download progress.
+ this.mStatusFeedback.showStatusString(
+ FeedUtils.strings.formatStringFromName("subscribe-gettingFeedItems", [
+ aCurrentFeedItems,
+ aMaxFeedItems,
+ ])
+ );
+ this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
+ }
+ },
+
+ onProgress(feed, aProgress, aProgressMax, aLengthComputable) {
+ if (feed.url in this.mFeeds) {
+ // Have we already seen this feed?
+ this.mFeeds[feed.url].currentProgress = aProgress;
+ } else {
+ this.mFeeds[feed.url] = {
+ currentProgress: aProgress,
+ maxProgress: aProgressMax,
+ };
+ }
+
+ this.updateProgressBar();
+ },
+
+ updateProgressBar() {
+ let currentProgress = 0;
+ let maxProgress = 0;
+ for (let index in this.mFeeds) {
+ currentProgress += this.mFeeds[index].currentProgress;
+ maxProgress += this.mFeeds[index].maxProgress;
+ }
+
+ // If we start seeing weird "jumping" behavior where the progress bar
+ // goes below a threshold then above it again, then we can factor a
+ // fudge factor here based on the number of feeds that have not reported
+ // yet and the avg progress we've already received for existing feeds.
+ // Fortunately the progressmeter is on a timer and only updates every so
+ // often. For the most part all of our request have initial progress
+ // before the UI actually picks up a progress value.
+ if (this.mStatusFeedback) {
+ let progress = (currentProgress * 100) / maxProgress;
+ this.mStatusFeedback.showProgress(progress);
+ }
+ },
+ },
+};
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "log", function () {
+ return console.createInstance({
+ prefix: "feeds",
+ maxLogLevelPref: "feeds.loglevel",
+ });
+});
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "strings", function () {
+ return Services.strings.createBundle(
+ "chrome://messenger-newsblog/locale/newsblog.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "stringsPrefs", function () {
+ return Services.strings.createBundle(
+ "chrome://messenger/locale/prefs.properties"
+ );
+});
diff --git a/comm/mailnews/extensions/newsblog/NewsBlog.jsm b/comm/mailnews/extensions/newsblog/NewsBlog.jsm
new file mode 100644
index 0000000000..b1a1a22536
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/NewsBlog.jsm
@@ -0,0 +1,28 @@
+/* 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 EXPORTED_SYMBOLS = ["FeedDownloader", "FeedAcctMgrExtension"];
+
+var { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+
+function FeedDownloader() {}
+FeedDownloader.prototype = {
+ downloadFeed(aFolder, aUrlListener, aIsBiff, aMsgWindow) {
+ FeedUtils.downloadFeed(aFolder, aUrlListener, aIsBiff, aMsgWindow);
+ },
+ updateSubscriptionsDS(aFolder, aOrigFolder, aAction) {
+ FeedUtils.updateSubscriptionsDS(aFolder, aOrigFolder, aAction);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsINewsBlogFeedDownloader"]),
+};
+
+function FeedAcctMgrExtension() {}
+FeedAcctMgrExtension.prototype = {
+ name: "newsblog",
+ chromePackageName: "messenger-newsblog",
+ showPanel: server => false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgAccountManagerExtension"]),
+};
diff --git a/comm/mailnews/extensions/newsblog/am-newsblog.js b/comm/mailnews/extensions/newsblog/am-newsblog.js
new file mode 100644
index 0000000000..eacf2a3ae5
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/am-newsblog.js
@@ -0,0 +1,128 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+/* import-globals-from ../../base/prefs/content/am-prefs.js */
+
+var { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+
+var gAccount,
+ gUpdateEnabled,
+ gUpdateValue,
+ gBiffUnits,
+ gAutotagEnable,
+ gAutotagUsePrefix,
+ gAutotagPrefix;
+
+/**
+ * Initialize am-newsblog account settings page when it gets shown.
+ * Update an account's main settings title etc.
+ *
+ * @returns {void}
+ */
+function onInit() {
+ setAccountTitle();
+
+ let optionsAcct = FeedUtils.getOptionsAcct(gAccount.incomingServer);
+ document.getElementById("doBiff").checked = optionsAcct.doBiff;
+
+ gUpdateEnabled = document.getElementById("updateEnabled");
+ gUpdateValue = document.getElementById("updateValue");
+ gBiffUnits = document.getElementById("biffUnits");
+ gAutotagEnable = document.getElementById("autotagEnable");
+ gAutotagUsePrefix = document.getElementById("autotagUsePrefix");
+ gAutotagPrefix = document.getElementById("autotagPrefix");
+
+ gUpdateEnabled.checked = optionsAcct.updates.enabled;
+ gBiffUnits.value = optionsAcct.updates.updateUnits;
+ let minutes =
+ optionsAcct.updates.updateUnits == FeedUtils.kBiffUnitsMinutes
+ ? optionsAcct.updates.updateMinutes
+ : optionsAcct.updates.updateMinutes / (24 * 60);
+ gUpdateValue.value = Number(minutes);
+ onCheckItem("updateValue", ["updateEnabled"]);
+ onCheckItem("biffMinutes", ["updateEnabled"]);
+ onCheckItem("biffDays", ["updateEnabled"]);
+
+ gAutotagEnable.checked = optionsAcct.category.enabled;
+ gAutotagUsePrefix.disabled = !gAutotagEnable.checked;
+ gAutotagUsePrefix.checked = optionsAcct.category.prefixEnabled;
+ gAutotagPrefix.disabled =
+ gAutotagUsePrefix.disabled || !gAutotagUsePrefix.checked;
+ gAutotagPrefix.value = optionsAcct.category.prefix;
+}
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+}
+
+/**
+ * Handle the blur event of the #server.prettyName pref input.
+ * Update account name in account manager tree and account settings' main title.
+ *
+ * @param {Event} event - Blur event from the pretty name input.
+ * @returns {void}
+ */
+function serverPrettyNameOnBlur(event) {
+ parent.setAccountLabel(gAccount.key, event.target.value);
+ setAccountTitle();
+}
+
+/**
+ * Update an account's main settings title with the account name if applicable.
+ *
+ * @returns {void}
+ */
+function setAccountTitle() {
+ let accountName = document.getElementById("server.prettyName");
+ let title = document.querySelector("#am-newsblog-title .dialogheader-title");
+ let titleValue = title.getAttribute("defaultTitle");
+ if (accountName.value) {
+ titleValue += " - " + accountName.value;
+ }
+
+ title.setAttribute("value", titleValue);
+ document.title = titleValue;
+}
+
+function setPrefs(aNode) {
+ let optionsAcct = FeedUtils.getOptionsAcct(gAccount.incomingServer);
+ switch (aNode.id) {
+ case "doBiff":
+ FeedUtils.pauseFeedFolderUpdates(
+ gAccount.incomingServer.rootFolder,
+ !aNode.checked,
+ true
+ );
+ break;
+ case "updateEnabled":
+ case "updateValue":
+ case "biffUnits":
+ optionsAcct.updates.enabled = gUpdateEnabled.checked;
+ onCheckItem("updateValue", ["updateEnabled"]);
+ onCheckItem("biffMinutes", ["updateEnabled"]);
+ onCheckItem("biffDays", ["updateEnabled"]);
+ let minutes =
+ gBiffUnits.value == FeedUtils.kBiffUnitsMinutes
+ ? gUpdateValue.value
+ : gUpdateValue.value * 24 * 60;
+ optionsAcct.updates.updateMinutes = Number(minutes);
+ optionsAcct.updates.updateUnits = gBiffUnits.value;
+ break;
+ case "autotagEnable":
+ optionsAcct.category.enabled = aNode.checked;
+ gAutotagUsePrefix.disabled = !aNode.checked;
+ gAutotagPrefix.disabled = !aNode.checked || !gAutotagUsePrefix.checked;
+ break;
+ case "autotagUsePrefix":
+ optionsAcct.category.prefixEnabled = aNode.checked;
+ gAutotagPrefix.disabled = aNode.disabled || !aNode.checked;
+ break;
+ case "autotagPrefix":
+ optionsAcct.category.prefix = aNode.value;
+ break;
+ }
+
+ FeedUtils.setOptionsAcct(gAccount.incomingServer, optionsAcct);
+}
diff --git a/comm/mailnews/extensions/newsblog/am-newsblog.xhtml b/comm/mailnews/extensions/newsblog/am-newsblog.xhtml
new file mode 100644
index 0000000000..e6ba0f6a5d
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/am-newsblog.xhtml
@@ -0,0 +1,233 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd">
+%newsblogDTD;
+<!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd" >
+%feedDTD;
+<!ENTITY % accountNoIdentDTD SYSTEM "chrome://messenger/locale/am-serverwithnoidentities.dtd" >
+%accountNoIdentDTD;
+<!ENTITY % accountServerTopDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">
+%accountServerTopDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&accountTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/AccountManager.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-newsblog/content/am-newsblog.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-newsblog/content/newsblogOverlay.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-newsblog.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <hbox id="am-newsblog-title" class="dialogheader">
+ <label class="dialogheader-title" defaultTitle="&accountTitle.label;" />
+ </hbox>
+
+ <description class="secDesc">&accountSettingsDesc.label;</description>
+
+ <hbox class="input-container">
+ <label
+ id="server.prettyNameLabel"
+ value="&accountName.label;"
+ accesskey="&accountName.accesskey;"
+ control="server.prettyName"
+ />
+ <html:input
+ id="server.prettyName"
+ type="text"
+ wsm_persist="true"
+ class="input-inline"
+ aria-labelledby="server.prettyNameLabel"
+ onblur="serverPrettyNameOnBlur(event);"
+ prefstring="mail.server.%serverkey%.name"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&serverSettings.label;</html:legend>
+ <checkbox
+ id="doBiff"
+ label="&biffAll.label;"
+ accesskey="&biffAll.accesskey;"
+ oncommand="setPrefs(this)"
+ />
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&newFeedSettings.label;</html:legend>
+
+ <hbox align="center">
+ <checkbox
+ id="updateEnabled"
+ label="&biffStart.label;"
+ accesskey="&biffStart.accesskey;"
+ oncommand="setPrefs(this)"
+ />
+ <html:input
+ id="updateValue"
+ type="number"
+ class="size3"
+ min="1"
+ aria-labelledby="updateEnabled updateValue biffMinutes biffDays"
+ onchange="setPrefs(this)"
+ />
+ <radiogroup
+ id="biffUnits"
+ orient="horizontal"
+ oncommand="setPrefs(this)"
+ >
+ <radio
+ id="biffMinutes"
+ value="min"
+ label="&biffMinutes.label;"
+ accesskey="&biffMinutes.accesskey;"
+ />
+ <radio
+ id="biffDays"
+ value="d"
+ label="&biffDays.label;"
+ accesskey="&biffDays.accesskey;"
+ />
+ </radiogroup>
+ </hbox>
+
+ <checkbox
+ id="server.quickMode"
+ wsm_persist="true"
+ genericattr="true"
+ label="&quickMode.label;"
+ accesskey="&quickMode.accesskey;"
+ preftype="bool"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.quickMode"
+ />
+
+ <checkbox
+ id="autotagEnable"
+ accesskey="&autotagEnable.accesskey;"
+ label="&autotagEnable.label;"
+ oncommand="setPrefs(this)"
+ />
+ <hbox class="input-container">
+ <checkbox
+ id="autotagUsePrefix"
+ class="indent"
+ accesskey="&autotagUsePrefix.accesskey;"
+ label="&autotagUsePrefix.label;"
+ oncommand="setPrefs(this)"
+ />
+ <html:input
+ id="autotagPrefix"
+ type="text"
+ class="input-inline"
+ aria-labelledby="autotagUsePrefix"
+ placeholder="&autoTagPrefix.placeholder;"
+ onchange="setPrefs(this)"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <hbox pack="end">
+ <button
+ label="&manageSubscriptions.label;"
+ accesskey="&manageSubscriptions.accesskey;"
+ oncommand="openSubscriptionsDialog(gAccount.incomingServer.rootFolder);"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&messageStorage.label;</html:legend>
+
+ <checkbox
+ id="server.emptyTrashOnExit"
+ wsm_persist="true"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"
+ />
+
+ <separator class="thin" />
+
+ <vbox>
+ <hbox align="center">
+ <label
+ id="server.localPathLabel"
+ value="&localPath1.label;"
+ control="server.localPath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="server.localPath"
+ type="text"
+ readonly="readonly"
+ class="uri-element input-inline"
+ aria-labelledby="server.localPathLabel"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ />
+ </hbox>
+ <button
+ id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders();"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/newsblog/components.conf b/comm/mailnews/extensions/newsblog/components.conf
new file mode 100644
index 0000000000..0e2c4ce03a
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/components.conf
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{5c124537-adca-4456-b2b5-641ab687d1f6}",
+ "contract_ids": ["@mozilla.org/newsblog-feed-downloader;1"],
+ "jsm": "resource:///modules/NewsBlog.jsm",
+ "constructor": "FeedDownloader",
+ },
+ {
+ "cid": "{e109c05f-d304-4ca5-8c44-6de1bfaf1f74}",
+ "contract_ids": ["@mozilla.org/accountmanager/extension;1?name=newsblog"],
+ "jsm": "resource:///modules/NewsBlog.jsm",
+ "constructor": "FeedAcctMgrExtension",
+ "categories": {"mailnews-accountmanager-extensions": "newsblog"},
+ },
+]
diff --git a/comm/mailnews/extensions/newsblog/feed-subscriptions.js b/comm/mailnews/extensions/newsblog/feed-subscriptions.js
new file mode 100644
index 0000000000..43ce61b11b
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/feed-subscriptions.js
@@ -0,0 +1,3120 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+/**
+ * @file
+ * GUI-side code for managing folder subscriptions.
+ */
+
+var { Feed } = ChromeUtils.import("resource:///modules/Feed.jsm");
+var { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+var FeedSubscriptions = {
+ get mMainWin() {
+ return Services.wm.getMostRecentWindow("mail:3pane");
+ },
+
+ get mTree() {
+ return document.getElementById("rssSubscriptionsList");
+ },
+
+ mFeedContainers: [],
+ mRSSServer: null,
+ mActionMode: null,
+ kSubscribeMode: 1,
+ kUpdateMode: 2,
+ kMoveMode: 3,
+ kCopyMode: 4,
+ kImportingOPML: 5,
+ kVerifyUrlMode: 6,
+
+ get FOLDER_ACTIONS() {
+ return (
+ Ci.nsIMsgFolderNotificationService.folderAdded |
+ Ci.nsIMsgFolderNotificationService.folderDeleted |
+ Ci.nsIMsgFolderNotificationService.folderRenamed |
+ Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted
+ );
+ },
+
+ onLoad() {
+ // Extract the folder argument.
+ let folder;
+ if (window.arguments && window.arguments[0].folder) {
+ folder = window.arguments[0].folder;
+ }
+
+ // Ensure dialog is fully loaded before selecting, to get visible row.
+ setTimeout(() => {
+ FeedSubscriptions.refreshSubscriptionView(folder);
+ }, 100);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-loading");
+ this.updateStatusItem("statusText", message);
+
+ FeedUtils.CANCEL_REQUESTED = false;
+
+ if (this.mMainWin) {
+ MailServices.mfn.addListener(this.FolderListener, this.FOLDER_ACTIONS);
+ }
+ },
+
+ onDialogAccept() {
+ let dismissDialog = true;
+
+ // If we are in the middle of subscribing to a feed, inform the user that
+ // dismissing the dialog right now will abort the feed subscription.
+ if (this.mActionMode == this.kSubscribeMode) {
+ let pTitle = FeedUtils.strings.GetStringFromName(
+ "subscribe-cancelSubscriptionTitle"
+ );
+ let pMessage = FeedUtils.strings.GetStringFromName(
+ "subscribe-cancelSubscription"
+ );
+ dismissDialog = !Services.prompt.confirmEx(
+ window,
+ pTitle,
+ pMessage,
+ Ci.nsIPromptService.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ }
+
+ if (dismissDialog) {
+ FeedUtils.CANCEL_REQUESTED = this.mActionMode == this.kSubscribeMode;
+ if (this.mMainWin) {
+ MailServices.mfn.removeListener(
+ this.FolderListener,
+ this.FOLDER_ACTIONS
+ );
+ }
+ }
+
+ return dismissDialog;
+ },
+
+ refreshSubscriptionView(aSelectFolder, aSelectFeedUrl) {
+ let item = this.mView.currentItem;
+ this.loadSubscriptions();
+ this.mTree.view = this.mView;
+
+ if (aSelectFolder && !aSelectFeedUrl) {
+ this.selectFolder(aSelectFolder);
+ } else if (item) {
+ // If no folder to select, try to select the pre rebuild selection, in
+ // an existing window. For folderpane changes in a feed account.
+ let rootFolder = item.container
+ ? item.folder.rootFolder
+ : item.parentFolder.rootFolder;
+ if (item.container) {
+ if (!this.selectFolder(item.folder, { open: item.open })) {
+ // The item no longer exists, an ancestor folder was deleted or
+ // renamed/moved.
+ this.selectFolder(rootFolder);
+ }
+ } else {
+ let url =
+ item.parentFolder == aSelectFolder ? aSelectFeedUrl : item.url;
+ this.selectFeed({ folder: rootFolder, url }, null);
+ }
+ }
+
+ this.mView.tree.ensureRowIsVisible(this.mView.selection.currentIndex);
+ this.clearStatusInfo();
+ },
+
+ mView: {
+ kRowIndexUndefined: -1,
+
+ get currentItem() {
+ // Get the current selection, if any.
+ let seln = this.selection;
+ let currentSelectionIndex = seln ? seln.currentIndex : null;
+ let item;
+ if (currentSelectionIndex != null) {
+ item = this.getItemAtIndex(currentSelectionIndex);
+ }
+
+ return item;
+ },
+
+ /* nsITreeView */
+ /* eslint-disable no-multi-spaces */
+ tree: null,
+
+ mRowCount: 0,
+ get rowCount() {
+ return this.mRowCount;
+ },
+
+ _selection: null,
+ get selection() {
+ return this._selection;
+ },
+ set selection(val) {
+ this._selection = val;
+ },
+
+ setTree(aTree) {
+ this.tree = aTree;
+ },
+ isSeparator(aRow) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isEditable(aRow, aColumn) {
+ return false;
+ },
+
+ getProgressMode(aRow, aCol) {},
+ cycleHeader(aCol) {},
+ cycleCell(aRow, aCol) {},
+ selectionChanged() {},
+ getRowProperties(aRow) {
+ return "";
+ },
+ getColumnProperties(aCol) {
+ return "";
+ },
+ getCellValue(aRow, aColumn) {},
+ setCellValue(aRow, aColumn, aValue) {},
+ setCellText(aRow, aColumn, aValue) {},
+ /* eslint-enable no-multi-spaces */
+
+ getCellProperties(aRow, aColumn) {
+ let item = this.getItemAtIndex(aRow);
+ if (!item) {
+ return "";
+ }
+
+ if (AppConstants.MOZ_APP_NAME != "thunderbird") {
+ if (!item.folder) {
+ return "serverType-rss";
+ } else if (item.folder.isServer) {
+ return "serverType-rss isServer-true";
+ }
+
+ return "livemark";
+ }
+
+ let folder = item.folder;
+ let properties = "folderNameCol";
+ let mainWin = FeedSubscriptions.mMainWin;
+ if (!mainWin) {
+ let hasFeeds = FeedUtils.getFeedUrlsInFolder(folder);
+ if (!folder) {
+ properties += " isFeed-true";
+ } else if (hasFeeds) {
+ properties += " isFeedFolder-true";
+ } else if (folder.isServer) {
+ properties += " serverType-rss isServer-true";
+ }
+ } else {
+ let url = folder ? null : item.url;
+ folder = folder || item.parentFolder;
+ properties = mainWin.FolderUtils.getFolderProperties(folder, item.open);
+ properties += mainWin.FeedUtils.getFolderProperties(folder, url);
+ if (
+ this.selection.currentIndex == aRow &&
+ url &&
+ item.options.updates.enabled &&
+ properties.includes("isPaused")
+ ) {
+ item.options.updates.enabled = false;
+ FeedSubscriptions.updateFeedData(item);
+ }
+ }
+
+ item.properties = properties;
+ return properties;
+ },
+
+ isContainer(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ return item ? item.container : false;
+ },
+
+ isContainerOpen(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ return item ? item.open : false;
+ },
+
+ isContainerEmpty(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ if (!item) {
+ return false;
+ }
+
+ return item.children.length == 0;
+ },
+
+ getItemAtIndex(aRow) {
+ if (aRow < 0 || aRow >= FeedSubscriptions.mFeedContainers.length) {
+ return null;
+ }
+
+ return FeedSubscriptions.mFeedContainers[aRow];
+ },
+
+ getItemInViewIndex(aFolder) {
+ if (!aFolder || !(aFolder instanceof Ci.nsIMsgFolder)) {
+ return null;
+ }
+
+ for (let index = 0; index < this.rowCount; index++) {
+ // Find the visible folder in the view.
+ let item = this.getItemAtIndex(index);
+ if (item && item.container && item.url == aFolder.URI) {
+ return index;
+ }
+ }
+
+ return null;
+ },
+
+ removeItemAtIndex(aRow, aNoSelect) {
+ let itemToRemove = this.getItemAtIndex(aRow);
+ if (!itemToRemove) {
+ return;
+ }
+
+ if (itemToRemove.container && itemToRemove.open) {
+ // Close it, if open container.
+ this.toggleOpenState(aRow);
+ }
+
+ let parentIndex = this.getParentIndex(aRow);
+ let hasNextSibling = this.hasNextSibling(aRow, aRow);
+ if (parentIndex != this.kRowIndexUndefined) {
+ let parent = this.getItemAtIndex(parentIndex);
+ if (parent) {
+ for (let index = 0; index < parent.children.length; index++) {
+ if (parent.children[index] == itemToRemove) {
+ parent.children.splice(index, 1);
+ break;
+ }
+ }
+ }
+ }
+
+ // Now remove it from our view.
+ FeedSubscriptions.mFeedContainers.splice(aRow, 1);
+
+ // Now invalidate the correct tree rows.
+ this.mRowCount--;
+ this.tree.rowCountChanged(aRow, -1);
+
+ // Now update the selection position, unless noSelect (selection is
+ // done later or not at all). If the item is the last child, select the
+ // parent. Otherwise select the next sibling.
+ if (!aNoSelect) {
+ if (aRow <= FeedSubscriptions.mFeedContainers.length) {
+ this.selection.select(hasNextSibling ? aRow : aRow - 1);
+ } else {
+ this.selection.clearSelection();
+ }
+ }
+
+ // Now refocus the tree.
+ FeedSubscriptions.mTree.focus();
+ },
+
+ getCellText(aRow, aColumn) {
+ let item = this.getItemAtIndex(aRow);
+ return item && aColumn.id == "folderNameCol" ? item.name : "";
+ },
+
+ getImageSrc(aRow, aCol) {
+ let item = this.getItemAtIndex(aRow);
+ if ((item.folder && item.folder.isServer) || item.open) {
+ return "";
+ }
+
+ if (
+ !item.open &&
+ (item.properties.includes("hasError") ||
+ item.properties.includes("isBusy"))
+ ) {
+ return "";
+ }
+
+ if (item.favicon != null) {
+ return item.favicon;
+ }
+
+ let callback = iconUrl => {
+ item.favicon = iconUrl;
+ if (item.folder) {
+ for (let child of item.children) {
+ if (!child.container) {
+ child.favicon = iconUrl;
+ break;
+ }
+ }
+ }
+
+ this.selection.tree.invalidateRow(aRow);
+ };
+
+ // A closed non server folder.
+ if (item.folder) {
+ for (let child of item.children) {
+ if (!child.container) {
+ if (child.favicon != null) {
+ return child.favicon;
+ }
+
+ setTimeout(async () => {
+ let iconUrl = await FeedUtils.getFavicon(
+ child.parentFolder,
+ child.url
+ );
+ if (iconUrl) {
+ callback(iconUrl);
+ }
+ }, 0);
+ break;
+ }
+ }
+ } else {
+ // A feed.
+ setTimeout(async () => {
+ let iconUrl = await FeedUtils.getFavicon(item.parentFolder, item.url);
+ if (iconUrl) {
+ callback(iconUrl);
+ }
+ }, 0);
+ }
+
+ // Store empty string to return default while favicons are retrieved.
+ return (item.favicon = "");
+ },
+
+ canDrop(aRow, aOrientation) {
+ let dropResult = this.extractDragData(aRow);
+ return (
+ aOrientation == Ci.nsITreeView.DROP_ON &&
+ dropResult.canDrop &&
+ (dropResult.dropUrl ||
+ dropResult.dropOnIndex != this.kRowIndexUndefined)
+ );
+ },
+
+ drop(aRow, aOrientation) {
+ let win = FeedSubscriptions;
+ let results = this.extractDragData(aRow);
+ if (!results.canDrop) {
+ return;
+ }
+
+ // Preselect the drop folder.
+ this.selection.select(aRow);
+
+ if (results.dropUrl) {
+ // Don't freeze the app that initiated the drop just because we are
+ // in a loop waiting for the user to dimisss the add feed dialog.
+ setTimeout(() => {
+ win.addFeed(results.dropUrl, null, true, null, win.kSubscribeMode);
+ }, 0);
+
+ let folderItem = this.getItemAtIndex(aRow);
+ FeedUtils.log.debug(
+ "drop: folder, url - " +
+ folderItem.folder.name +
+ ", " +
+ results.dropUrl
+ );
+ } else if (results.dropOnIndex != this.kRowIndexUndefined) {
+ win.moveCopyFeed(results.dropOnIndex, aRow, results.dropEffect);
+ }
+ },
+
+ // Helper function for drag and drop.
+ extractDragData(aRow) {
+ let dt = this._currentDataTransfer;
+ let dragDataResults = {
+ canDrop: false,
+ dropUrl: null,
+ dropOnIndex: this.kRowIndexUndefined,
+ dropEffect: dt.dropEffect,
+ };
+
+ if (dt.getData("text/x-moz-feed-index")) {
+ // Dragging a feed in the tree.
+ if (this.selection) {
+ dragDataResults.dropOnIndex = this.selection.currentIndex;
+
+ let curItem = this.getItemAtIndex(this.selection.currentIndex);
+ let newItem = this.getItemAtIndex(aRow);
+ let curServer =
+ curItem && curItem.parentFolder
+ ? curItem.parentFolder.server
+ : null;
+ let newServer =
+ newItem && newItem.folder ? newItem.folder.server : null;
+
+ // No copying within the same account and no moving to the account
+ // folder in the same account.
+ if (
+ !(
+ curServer == newServer &&
+ (dragDataResults.dropEffect == "copy" ||
+ newItem.folder == curItem.parentFolder ||
+ newItem.folder.isServer)
+ )
+ ) {
+ dragDataResults.canDrop = true;
+ }
+ }
+ } else {
+ // Try to get a feed url.
+ let validUri = FeedUtils.getFeedUriFromDataTransfer(dt);
+
+ if (validUri) {
+ dragDataResults.canDrop = true;
+ dragDataResults.dropUrl = validUri.spec;
+ }
+ }
+
+ return dragDataResults;
+ },
+
+ getParentIndex(aRow) {
+ let item = this.getItemAtIndex(aRow);
+
+ if (item) {
+ for (let index = aRow; index >= 0; index--) {
+ if (FeedSubscriptions.mFeedContainers[index].level < item.level) {
+ return index;
+ }
+ }
+ }
+
+ return this.kRowIndexUndefined;
+ },
+
+ isIndexChildOfParentIndex(aRow, aChildRow) {
+ // For visible tree rows, not if items are children of closed folders.
+ let item = this.getItemAtIndex(aRow);
+ if (!item || aChildRow <= aRow) {
+ return false;
+ }
+
+ let targetLevel = this.getItemAtIndex(aRow).level;
+ let rows = FeedSubscriptions.mFeedContainers;
+
+ for (let i = aRow + 1; i < rows.length; i++) {
+ if (this.getItemAtIndex(i).level <= targetLevel) {
+ break;
+ }
+ if (aChildRow == i) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ hasNextSibling(aRow, aAfterIndex) {
+ let targetLevel = this.getItemAtIndex(aRow).level;
+ let rows = FeedSubscriptions.mFeedContainers;
+ for (let i = aAfterIndex + 1; i < rows.length; i++) {
+ if (this.getItemAtIndex(i).level == targetLevel) {
+ return true;
+ }
+ if (this.getItemAtIndex(i).level < targetLevel) {
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ hasPreviousSibling(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ if (item && aRow) {
+ return this.getItemAtIndex(aRow - 1).level == item.level;
+ }
+
+ return false;
+ },
+
+ getLevel(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ if (!item) {
+ return 0;
+ }
+
+ return item.level;
+ },
+
+ toggleOpenState(aRow) {
+ let item = this.getItemAtIndex(aRow);
+ if (!item) {
+ return;
+ }
+
+ // Save off the current selection item.
+ let seln = this.selection;
+ let currentSelectionIndex = seln.currentIndex;
+
+ let rowsChanged = this.toggle(aRow);
+
+ // Now restore selection, ensuring selection is maintained on toggles.
+ if (currentSelectionIndex > aRow) {
+ seln.currentIndex = currentSelectionIndex + rowsChanged;
+ } else {
+ seln.select(currentSelectionIndex);
+ }
+
+ seln.selectEventsSuppressed = false;
+ },
+
+ toggle(aRow) {
+ // Collapse the row, or build sub rows based on open states in the map.
+ let item = this.getItemAtIndex(aRow);
+ if (!item) {
+ return null;
+ }
+
+ let rows = FeedSubscriptions.mFeedContainers;
+ let rowCount = 0;
+ let multiplier;
+
+ function addDescendants(aItem) {
+ for (let i = 0; i < aItem.children.length; i++) {
+ rowCount++;
+ let child = aItem.children[i];
+ rows.splice(aRow + rowCount, 0, child);
+ if (child.open) {
+ addDescendants(child);
+ }
+ }
+ }
+
+ if (item.open) {
+ // Close the container. Add up all subfolders and their descendants
+ // who may be open.
+ multiplier = -1;
+ let nextRow = aRow + 1;
+ let nextItem = rows[nextRow];
+ while (nextItem && nextItem.level > item.level) {
+ rowCount++;
+ nextItem = rows[++nextRow];
+ }
+
+ rows.splice(aRow + 1, rowCount);
+ } else {
+ // Open the container. Restore the open state of all subfolder and
+ // their descendants.
+ multiplier = 1;
+ addDescendants(item);
+ }
+
+ let delta = multiplier * rowCount;
+ this.mRowCount += delta;
+
+ item.open = !item.open;
+ // Suppress the select event caused by rowCountChanged.
+ this.selection.selectEventsSuppressed = true;
+ // Add or remove the children from our view.
+ this.tree.rowCountChanged(aRow, delta);
+ return delta;
+ },
+ },
+
+ makeFolderObject(aFolder, aCurrentLevel) {
+ let defaultQuickMode = aFolder.server.getBoolValue("quickMode");
+ let optionsAcct = aFolder.isServer
+ ? FeedUtils.getOptionsAcct(aFolder.server)
+ : null;
+ let open =
+ !aFolder.isServer &&
+ aFolder.server == this.mRSSServer &&
+ this.mActionMode == this.kImportingOPML;
+ let folderObject = {
+ children: [],
+ folder: aFolder,
+ name: aFolder.prettyName,
+ level: aCurrentLevel,
+ url: aFolder.URI,
+ quickMode: defaultQuickMode,
+ options: optionsAcct,
+ open,
+ container: true,
+ favicon: null,
+ };
+
+ // If a feed has any sub folders, add them to the list of children.
+ for (let folder of aFolder.subFolders) {
+ if (
+ folder instanceof Ci.nsIMsgFolder &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Trash) &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ folderObject.children.push(
+ this.makeFolderObject(folder, aCurrentLevel + 1)
+ );
+ }
+ }
+
+ let feeds = this.getFeedsInFolder(aFolder);
+ for (let feed of feeds) {
+ // Now add any feed urls for the folder.
+ folderObject.children.push(
+ this.makeFeedObject(feed, aFolder, aCurrentLevel + 1)
+ );
+ }
+
+ // Finally, set the folder's quickMode based on the its first feed's
+ // quickMode, since that is how the view determines summary mode, and now
+ // quickMode is updated to be the same for all feeds in a folder.
+ if (feeds && feeds[0]) {
+ folderObject.quickMode = feeds[0].quickMode;
+ }
+
+ folderObject.children = this.folderItemSorter(folderObject.children);
+
+ return folderObject;
+ },
+
+ folderItemSorter(aArray) {
+ return aArray
+ .sort(function (a, b) {
+ return a.name.toLowerCase() > b.name.toLowerCase();
+ })
+ .sort(function (a, b) {
+ return a.container < b.container;
+ });
+ },
+
+ getFeedsInFolder(aFolder) {
+ let feeds = [];
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(aFolder);
+ if (!feedUrlArray) {
+ // No feedUrls in this folder.
+ return feeds;
+ }
+
+ for (let url of feedUrlArray) {
+ let feed = new Feed(url, aFolder);
+ feeds.push(feed);
+ }
+
+ return feeds;
+ },
+
+ makeFeedObject(aFeed, aFolder, aLevel) {
+ // Look inside the data source for the feed properties.
+ let feed = {
+ children: [],
+ parentFolder: aFolder,
+ name: aFeed.title || aFeed.description || aFeed.url,
+ url: aFeed.url,
+ quickMode: aFeed.quickMode,
+ options: aFeed.options || FeedUtils.optionsTemplate,
+ level: aLevel,
+ open: false,
+ container: false,
+ favicon: null,
+ };
+ return feed;
+ },
+
+ loadSubscriptions() {
+ // Put together an array of folders. Each feed account level folder is
+ // included as the root.
+ let numFolders = 0;
+ let feedContainers = [];
+ // Get all the feed account folders.
+ let feedRootFolders = FeedUtils.getAllRssServerRootFolders();
+
+ feedRootFolders.forEach(function (rootFolder) {
+ feedContainers.push(this.makeFolderObject(rootFolder, 0));
+ numFolders++;
+ }, this);
+
+ this.mFeedContainers = feedContainers;
+ this.mView.mRowCount = numFolders;
+
+ FeedSubscriptions.mTree.focus();
+ },
+
+ /**
+ * Find the folder in the tree. The search may be limited to subfolders of
+ * a known folder, or expanded to include the entire tree. This function is
+ * also used to insert/remove folders without rebuilding the tree view cache
+ * (to avoid position/toggle state loss).
+ *
+ * @param {nsIMsgFolder} aFolder - The folder to find.
+ * @param {object} aParms - The params object, containing:
+ *
+ * {Integer} parentIndex - index of folder to start the search; if
+ * null (default), the index of the folder's
+ * rootFolder will be used.
+ * {boolean} select - if true (default) the folder's ancestors
+ * will be opened and the folder selected.
+ * {boolean} open - if true (default) the folder is opened.
+ * {boolean} remove - delete the item from tree row cache if true,
+ * false (default) otherwise.
+ * {nsIMsgFolder} newFolder - if not null (default) the new folder,
+ * for add or rename.
+ *
+ * @returns {Boolean} found - true if found, false if not.
+ */
+ selectFolder(aFolder, aParms) {
+ let folderURI = aFolder.URI;
+ let parentIndex =
+ aParms && "parentIndex" in aParms ? aParms.parentIndex : null;
+ let selectIt = aParms && "select" in aParms ? aParms.select : true;
+ let openIt = aParms && "open" in aParms ? aParms.open : true;
+ let removeIt = aParms && "remove" in aParms ? aParms.remove : false;
+ let newFolder = aParms && "newFolder" in aParms ? aParms.newFolder : null;
+ let startIndex, startItem;
+ let found = false;
+
+ let firstVisRow, curFirstVisRow, curLastVisRow;
+ if (this.mView.tree) {
+ firstVisRow = this.mView.tree.getFirstVisibleRow();
+ }
+
+ if (parentIndex != null) {
+ // Use the parentIndex if given.
+ startIndex = parentIndex;
+ if (aFolder.isServer) {
+ // Fake item for account root folder.
+ startItem = {
+ name: "AccountRoot",
+ children: [this.mView.getItemAtIndex(startIndex)],
+ container: true,
+ open: false,
+ url: null,
+ level: -1,
+ };
+ } else {
+ startItem = this.mView.getItemAtIndex(startIndex);
+ }
+ } else {
+ // Get the folder's root parent index.
+ let index = 0;
+ for (index; index < this.mView.rowCount; index++) {
+ let item = this.mView.getItemAtIndex(index);
+ if (item.url == aFolder.server.rootFolder.URI) {
+ break;
+ }
+ }
+
+ startIndex = index;
+ if (aFolder.isServer) {
+ // Fake item for account root folder.
+ startItem = {
+ name: "AccountRoot",
+ children: [this.mView.getItemAtIndex(startIndex)],
+ container: true,
+ open: false,
+ url: null,
+ level: -1,
+ };
+ } else {
+ startItem = this.mView.getItemAtIndex(startIndex);
+ }
+ }
+
+ function containsFolder(aItem) {
+ // Search for the folder. If it's found, set the open state on all
+ // ancestor folders. A toggle() rebuilds the view rows to match the map.
+ if (aItem.url == folderURI) {
+ return (found = true);
+ }
+
+ for (let i = 0; i < aItem.children.length; i++) {
+ if (aItem.children[i].container && containsFolder(aItem.children[i])) {
+ if (removeIt && aItem.children[i].url == folderURI) {
+ // Get all occurrences in the tree cache arrays.
+ FeedUtils.log.debug(
+ "selectFolder: delete in cache, " +
+ "parent:children:item:index - " +
+ aItem.name +
+ ":" +
+ aItem.children.length +
+ ":" +
+ aItem.children[i].name +
+ ":" +
+ i
+ );
+ aItem.children.splice(i, 1);
+ FeedUtils.log.debug(
+ "selectFolder: deleted in cache, " +
+ "parent:children - " +
+ aItem.name +
+ ":" +
+ aItem.children.length
+ );
+ removeIt = false;
+ return true;
+ }
+ if (newFolder) {
+ let newItem = FeedSubscriptions.makeFolderObject(
+ newFolder,
+ aItem.level + 1
+ );
+ newItem.open = aItem.children[i].open;
+ if (newFolder.isServer) {
+ FeedSubscriptions.mFeedContainers[startIndex] = newItem;
+ } else {
+ aItem.children[i] = newItem;
+ aItem.children = FeedSubscriptions.folderItemSorter(
+ aItem.children
+ );
+ }
+ FeedUtils.log.trace(
+ "selectFolder: parentName:newFolderName:newFolderItem - " +
+ aItem.name +
+ ":" +
+ newItem.name +
+ ":" +
+ newItem.toSource()
+ );
+ newFolder = null;
+ return true;
+ }
+ if (!found) {
+ // For the folder to find.
+ found = true;
+ aItem.children[i].open = openIt;
+ } else if (selectIt || openIt) {
+ // For ancestor folders.
+ aItem.children[i].open = true;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (startItem) {
+ // Find a folder with a specific parent.
+ containsFolder(startItem);
+ if (!found) {
+ return false;
+ }
+
+ if (!selectIt) {
+ return true;
+ }
+
+ if (startItem.open) {
+ this.mView.toggle(startIndex);
+ }
+
+ this.mView.toggleOpenState(startIndex);
+ }
+
+ for (let index = 0; index < this.mView.rowCount && selectIt; index++) {
+ // The desired folder is now in the view.
+ let item = this.mView.getItemAtIndex(index);
+ if (!item.container) {
+ continue;
+ }
+
+ if (item.url == folderURI) {
+ if (
+ item.children.length &&
+ ((!item.open && openIt) || (item.open && !openIt))
+ ) {
+ this.mView.toggleOpenState(index);
+ }
+
+ this.mView.selection.select(index);
+ found = true;
+ break;
+ }
+ }
+
+ // Ensure tree position does not jump unnecessarily.
+ curFirstVisRow = this.mView.tree.getFirstVisibleRow();
+ curLastVisRow = this.mView.tree.getLastVisibleRow();
+ if (
+ firstVisRow >= 0 &&
+ this.mView.rowCount - curLastVisRow > firstVisRow - curFirstVisRow
+ ) {
+ this.mView.tree.scrollToRow(firstVisRow);
+ } else {
+ this.mView.tree.ensureRowIsVisible(this.mView.rowCount - 1);
+ }
+
+ FeedUtils.log.debug(
+ "selectFolder: curIndex:firstVisRow:" +
+ "curFirstVisRow:curLastVisRow:rowCount - " +
+ this.mView.selection.currentIndex +
+ ":" +
+ firstVisRow +
+ ":" +
+ curFirstVisRow +
+ ":" +
+ curLastVisRow +
+ ":" +
+ this.mView.rowCount
+ );
+ return found;
+ },
+
+ /**
+ * Find the feed in the tree. The search first gets the feed's folder,
+ * then selects the child feed.
+ *
+ * @param {Feed} aFeed - The feed to find.
+ * @param {Integer} aParentIndex - Index to start the folder search.
+ *
+ * @returns {Boolean} found - true if found, false if not.
+ */
+ selectFeed(aFeed, aParentIndex) {
+ let folder = aFeed.folder;
+ let server = aFeed.server || aFeed.folder.server;
+ let found = false;
+
+ if (aFeed.folder.isServer) {
+ // If passed the root folder, the caller wants to get the feed's folder
+ // from the db (for cases of an ancestor folder rename/move).
+ let destFolder = FeedUtils.getSubscriptionAttr(
+ aFeed.url,
+ server,
+ "destFolder"
+ );
+ folder = server.rootFolder.getChildWithURI(destFolder, true, false);
+ }
+
+ if (this.selectFolder(folder, { parentIndex: aParentIndex })) {
+ let seln = this.mView.selection;
+ let item = this.mView.currentItem;
+ if (item) {
+ for (let i = seln.currentIndex + 1; i < this.mView.rowCount; i++) {
+ if (this.mView.getItemAtIndex(i).url == aFeed.url) {
+ this.mView.selection.select(i);
+ this.mView.tree.ensureRowIsVisible(i);
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+
+ return found;
+ },
+
+ updateFeedData(aItem) {
+ if (!aItem) {
+ return;
+ }
+
+ let nameValue = document.getElementById("nameValue");
+ let locationValue = document.getElementById("locationValue");
+ let locationValidate = document.getElementById("locationValidate");
+ let isServer = aItem.folder && aItem.folder.isServer;
+ let isFolder = aItem.folder && !aItem.folder.isServer;
+ let isFeed = !aItem.container;
+ let server, displayFolder;
+
+ if (isFeed) {
+ // A feed item. Set the feed location and title info.
+ nameValue.value = aItem.name;
+ locationValue.value = aItem.url;
+ locationValidate.removeAttribute("collapsed");
+
+ // Root the location picker to the news & blogs server.
+ server = aItem.parentFolder.server;
+ displayFolder = aItem.parentFolder;
+ } else {
+ // A folder/container item.
+ nameValue.value = "";
+ nameValue.disabled = true;
+ locationValue.value = "";
+ locationValidate.setAttribute("collapsed", true);
+
+ server = aItem.folder.server;
+ displayFolder = aItem.folder;
+ }
+
+ // Common to both folder and feed items.
+ nameValue.disabled = aItem.container;
+ this.setFolderPicker(displayFolder, isFeed);
+
+ // Set quick mode value.
+ document.getElementById("quickMode").checked = aItem.quickMode;
+
+ if (isServer) {
+ aItem.options = FeedUtils.getOptionsAcct(server);
+ }
+
+ // Update items.
+ let updateEnabled = document.getElementById("updateEnabled");
+ let updateValue = document.getElementById("updateValue");
+ let biffUnits = document.getElementById("biffUnits");
+ let recommendedUnits = document.getElementById("recommendedUnits");
+ let recommendedUnitsVal = document.getElementById("recommendedUnitsVal");
+ let updates = aItem.options
+ ? aItem.options.updates
+ : FeedUtils._optionsDefault.updates;
+
+ updateEnabled.checked = updates.enabled;
+ updateValue.disabled = !updateEnabled.checked || isFolder;
+ biffUnits.disabled = !updateEnabled.checked || isFolder;
+ biffUnits.value = updates.updateUnits;
+ let minutes =
+ updates.updateUnits == FeedUtils.kBiffUnitsMinutes
+ ? updates.updateMinutes
+ : updates.updateMinutes / (24 * 60);
+ updateValue.value = Number(minutes);
+ if (isFeed) {
+ recommendedUnitsVal.value = this.getUpdateMinutesRec(updates);
+ } else {
+ recommendedUnitsVal.value = "";
+ }
+
+ let hideRec = recommendedUnitsVal.value == "";
+ recommendedUnits.hidden = hideRec;
+ recommendedUnitsVal.hidden = hideRec;
+
+ // Autotag items.
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+ let category = aItem.options ? aItem.options.category : null;
+
+ autotagEnable.checked = category && category.enabled;
+ autotagUsePrefix.checked = category && category.prefixEnabled;
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled =
+ autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+ autotagPrefix.value = category && category.prefix ? category.prefix : "";
+ },
+
+ setFolderPicker(aFolder, aIsFeed) {
+ let folderPrettyPath = FeedUtils.getFolderPrettyPath(aFolder);
+ if (!folderPrettyPath) {
+ return;
+ }
+
+ let selectFolder = document.getElementById("selectFolder");
+ let selectFolderPopup = document.getElementById("selectFolderPopup");
+ let selectFolderValue = document.getElementById("selectFolderValue");
+
+ selectFolder.setAttribute("hidden", !aIsFeed);
+ selectFolder._folder = aFolder;
+ selectFolderValue.toggleAttribute("hidden", aIsFeed);
+ selectFolderValue.setAttribute("showfilepath", false);
+
+ if (aIsFeed) {
+ selectFolderPopup._ensureInitialized();
+ selectFolderPopup.selectFolder(aFolder);
+ selectFolder.setAttribute("label", folderPrettyPath);
+ selectFolder.setAttribute("uri", aFolder.URI);
+ } else {
+ selectFolderValue.value = folderPrettyPath;
+ selectFolderValue.setAttribute("prettypath", folderPrettyPath);
+ selectFolderValue.setAttribute("filepath", aFolder.filePath.path);
+ }
+ },
+
+ onClickSelectFolderValue(aEvent) {
+ let target = aEvent.target;
+ if (
+ ("button" in aEvent &&
+ (aEvent.button != 0 ||
+ aEvent.target.localName != "div" ||
+ target.selectionStart != target.selectionEnd)) ||
+ (aEvent.keyCode && aEvent.keyCode != aEvent.DOM_VK_RETURN)
+ ) {
+ return;
+ }
+
+ // Toggle between showing prettyPath and absolute filePath.
+ if (target.getAttribute("showfilepath") == "true") {
+ target.setAttribute("showfilepath", false);
+ target.value = target.getAttribute("prettypath");
+ } else {
+ target.setAttribute("showfilepath", true);
+ target.value = target.getAttribute("filepath");
+ }
+ },
+
+ /**
+ * The user changed the folder for storing the feed.
+ *
+ * @param {Event} aEvent - Event.
+ * @returns {void}
+ */
+ setNewFolder(aEvent) {
+ aEvent.stopPropagation();
+ this.setFolderPicker(aEvent.target._folder, true);
+
+ let seln = this.mView.selection;
+ if (seln.count != 1) {
+ return;
+ }
+
+ let item = this.mView.getItemAtIndex(seln.currentIndex);
+ if (!item || item.container || !item.parentFolder) {
+ return;
+ }
+
+ let selectFolder = document.getElementById("selectFolder");
+ let editFolderURI = selectFolder.getAttribute("uri");
+ if (item.parentFolder.URI == editFolderURI) {
+ return;
+ }
+
+ let feed = new Feed(item.url, item.parentFolder);
+
+ // Make sure the new folderpicked folder is visible.
+ this.selectFolder(selectFolder._folder);
+ // Now go back to the feed item.
+ this.selectFeed(feed, null);
+ // We need to find the index of the new parent folder.
+ let newParentIndex = this.mView.kRowIndexUndefined;
+ for (let index = 0; index < this.mView.rowCount; index++) {
+ let item = this.mView.getItemAtIndex(index);
+ if (item && item.container && item.url == editFolderURI) {
+ newParentIndex = index;
+ break;
+ }
+ }
+
+ if (newParentIndex != this.mView.kRowIndexUndefined) {
+ this.moveCopyFeed(seln.currentIndex, newParentIndex, "move");
+ }
+ },
+
+ setSummary(aChecked) {
+ let item = this.mView.currentItem;
+ if (!item || !item.folder) {
+ // Not a folder.
+ return;
+ }
+
+ if (item.folder.isServer) {
+ if (document.getElementById("locationValue").value) {
+ // Intent is to add a feed/folder to the account, so return.
+ return;
+ }
+
+ // An account folder. If it changes, all non feed containing subfolders
+ // need to be updated with the new default.
+ item.folder.server.setBoolValue("quickMode", aChecked);
+ this.FolderListener.folderAdded(item.folder);
+ } else if (!FeedUtils.getFeedUrlsInFolder(item.folder)) {
+ // Not a folder with feeds.
+ return;
+ } else {
+ let feedsInFolder = this.getFeedsInFolder(item.folder);
+ // Update the feeds database, for each feed in the folder.
+ feedsInFolder.forEach(function (feed) {
+ feed.quickMode = aChecked;
+ });
+ // Update the folder's feeds properties in the tree map.
+ item.children.forEach(function (feed) {
+ feed.quickMode = aChecked;
+ });
+ }
+
+ // Update the folder in the tree map.
+ item.quickMode = aChecked;
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ },
+
+ setPrefs(aNode) {
+ let item = this.mView.currentItem;
+ if (!item) {
+ return;
+ }
+
+ let isServer = item.folder && item.folder.isServer;
+ let isFolder = item.folder && !item.folder.isServer;
+ let updateEnabled = document.getElementById("updateEnabled");
+ let updateValue = document.getElementById("updateValue");
+ let biffUnits = document.getElementById("biffUnits");
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+ if (
+ isFolder ||
+ (isServer && document.getElementById("locationValue").value)
+ ) {
+ // Intend to subscribe a feed to a folder, a value must be in the url
+ // field. Update states for addFeed() and return.
+ updateValue.disabled = !updateEnabled.checked;
+ biffUnits.disabled = !updateEnabled.checked;
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled =
+ autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+ return;
+ }
+
+ switch (aNode.id) {
+ case "nameValue":
+ // Check to see if the title value changed, no blank title allowed.
+ if (!aNode.value) {
+ aNode.value = item.name;
+ return;
+ }
+
+ item.name = aNode.value;
+ let seln = this.mView.selection;
+ seln.tree.invalidateRow(seln.currentIndex);
+ break;
+ case "locationValue":
+ let updateFeedButton = document.getElementById("updateFeed");
+ // Change label based on whether feed url has beed edited.
+ updateFeedButton.label =
+ aNode.value == item.url
+ ? updateFeedButton.getAttribute("verifylabel")
+ : updateFeedButton.getAttribute("updatelabel");
+ updateFeedButton.setAttribute(
+ "accesskey",
+ aNode.value == item.url
+ ? updateFeedButton.getAttribute("verifyaccesskey")
+ : updateFeedButton.getAttribute("updateaccesskey")
+ );
+ // Disable the Update button if no feed url value is entered.
+ updateFeedButton.disabled = !aNode.value;
+ return;
+ case "updateEnabled":
+ case "updateValue":
+ case "biffUnits":
+ item.options.updates.enabled = updateEnabled.checked;
+ let minutes =
+ biffUnits.value == FeedUtils.kBiffUnitsMinutes
+ ? updateValue.value
+ : updateValue.value * 24 * 60;
+ item.options.updates.updateMinutes = Number(minutes);
+ item.options.updates.updateUnits = biffUnits.value;
+ break;
+ case "autotagEnable":
+ item.options.category.enabled = aNode.checked;
+ break;
+ case "autotagUsePrefix":
+ item.options.category.prefixEnabled = aNode.checked;
+ item.options.category.prefix = autotagPrefix.value;
+ break;
+ case "autotagPrefix":
+ item.options.category.prefix = aNode.value;
+ break;
+ }
+
+ if (isServer) {
+ FeedUtils.setOptionsAcct(item.folder.server, item.options);
+ } else {
+ let feed = new Feed(item.url, item.parentFolder);
+ feed.title = item.name;
+ feed.options = item.options;
+
+ if (aNode.id == "updateEnabled") {
+ FeedUtils.setStatus(
+ item.parentFolder,
+ item.url,
+ "enabled",
+ aNode.checked
+ );
+ this.mView.selection.tree.invalidateRow(
+ this.mView.selection.currentIndex
+ );
+ }
+ if (aNode.id == "updateValue") {
+ FeedUtils.setStatus(
+ item.parentFolder,
+ item.url,
+ "updateMinutes",
+ item.options.updates.updateMinutes
+ );
+ }
+ }
+
+ this.updateFeedData(item);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ },
+
+ getUpdateMinutesRec(aUpdates) {
+ // Assume the parser has stored correct/valid values for the spec. If the
+ // feed doesn't use any of these tags, updatePeriod will be null.
+ if (aUpdates.updatePeriod == null) {
+ return "";
+ }
+
+ let biffUnits = document.getElementById("biffUnits").value;
+ let units = biffUnits == FeedUtils.kBiffUnitsDays ? 1 : 24 * 60;
+ let frequency = aUpdates.updateFrequency;
+ let val;
+ switch (aUpdates.updatePeriod) {
+ case "hourly":
+ val =
+ biffUnits == FeedUtils.kBiffUnitsDays
+ ? 1 / frequency / 24
+ : 60 / frequency;
+ break;
+ case "daily":
+ val = units / frequency;
+ break;
+ case "weekly":
+ val = (7 * units) / frequency;
+ break;
+ case "monthly":
+ val = (30 * units) / frequency;
+ break;
+ case "yearly":
+ val = (365 * units) / frequency;
+ break;
+ }
+
+ return val ? Math.round(val * 1000) / 1000 : "";
+ },
+
+ onKeyPress(aEvent) {
+ if (
+ aEvent.keyCode == aEvent.DOM_VK_DELETE &&
+ aEvent.target.id == "rssSubscriptionsList"
+ ) {
+ this.removeFeed(true);
+ }
+
+ this.clearStatusInfo();
+ },
+
+ onSelect() {
+ let item = this.mView.currentItem;
+ this.updateFeedData(item);
+ this.setFocus();
+ this.updateButtons(item);
+ },
+
+ updateButtons(aSelectedItem) {
+ let item = aSelectedItem;
+ let isServer = item && item.folder && item.folder.isServer;
+ let isFeed = item && !item.container;
+ document.getElementById("addFeed").hidden = !item || isFeed;
+ document.getElementById("updateFeed").hidden = !isFeed;
+ document.getElementById("removeFeed").hidden = !isFeed;
+ document.getElementById("importOPML").hidden = !isServer;
+ document.getElementById("exportOPML").hidden = !isServer;
+
+ document.getElementById("importOPML").disabled = document.getElementById(
+ "exportOPML"
+ ).disabled = this.mActionMode == this.kImportingOPML;
+ },
+
+ onMouseDown(aEvent) {
+ if (
+ aEvent.button != 0 ||
+ aEvent.target.id == "validationText" ||
+ aEvent.target.id == "addCertException"
+ ) {
+ return;
+ }
+
+ this.clearStatusInfo();
+ },
+
+ onFocusChange() {
+ setTimeout(() => {
+ this.setFocus();
+ }, 0);
+ },
+
+ setFocus() {
+ let item = this.mView.currentItem;
+ if (!item || this.mActionMode == this.kImportingOPML) {
+ return;
+ }
+
+ let locationValue = document.getElementById("locationValue");
+ let updateEnabled = document.getElementById("updateEnabled");
+
+ let quickMode = document.getElementById("quickMode");
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+
+ let addFeedButton = document.getElementById("addFeed");
+ let updateFeedButton = document.getElementById("updateFeed");
+
+ let isServer = item.folder && item.folder.isServer;
+ let isFolder = item.folder && !item.folder.isServer;
+
+ // Enabled by default.
+ updateEnabled.disabled =
+ quickMode.disabled =
+ autotagEnable.disabled =
+ false;
+
+ updateEnabled.parentNode
+ .querySelectorAll("input,radio,label")
+ .forEach(item => {
+ item.disabled = !updateEnabled.checked;
+ });
+
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled =
+ autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+
+ let focusedElement = window.document.commandDispatcher.focusedElement;
+
+ if (isServer) {
+ addFeedButton.disabled =
+ addFeedButton != focusedElement &&
+ locationValue != document.activeElement &&
+ !locationValue.value;
+ } else if (isFolder) {
+ let disable =
+ locationValue != document.activeElement && !locationValue.value;
+ // Summary is enabled for a folder with feeds or if adding a feed.
+ quickMode.disabled =
+ disable && !FeedUtils.getFeedUrlsInFolder(item.folder);
+ // All other options disabled unless intent is to add a feed.
+ updateEnabled.disabled = disable;
+ updateEnabled.parentNode
+ .querySelectorAll("input,radio,label")
+ .forEach(item => {
+ item.disabled = disable;
+ });
+
+ autotagEnable.disabled = disable;
+
+ addFeedButton.disabled =
+ addFeedButton != focusedElement &&
+ locationValue != document.activeElement &&
+ !locationValue.value;
+ } else {
+ // Summary is disabled; applied per folder to apply to all feeds in it.
+ quickMode.disabled = true;
+ // Ensure the current feed url is restored if the user did not update.
+ if (
+ locationValue.value != item.url &&
+ locationValue != document.activeElement &&
+ focusedElement != updateFeedButton &&
+ focusedElement.id != "addCertException"
+ ) {
+ locationValue.value = item.url;
+ }
+ this.setPrefs(locationValue);
+ // Set button state.
+ updateFeedButton.disabled = !locationValue.value;
+ }
+ },
+
+ removeFeed(aPrompt) {
+ let seln = this.mView.selection;
+ if (seln.count != 1) {
+ return;
+ }
+
+ let itemToRemove = this.mView.getItemAtIndex(seln.currentIndex);
+
+ if (!itemToRemove || itemToRemove.container) {
+ return;
+ }
+
+ if (aPrompt) {
+ // Confirm unsubscribe prompt.
+ let pTitle = FeedUtils.strings.GetStringFromName(
+ "subscribe-confirmFeedDeletionTitle"
+ );
+ let pMessage = FeedUtils.strings.formatStringFromName(
+ "subscribe-confirmFeedDeletion",
+ [itemToRemove.name]
+ );
+ if (
+ Services.prompt.confirmEx(
+ window,
+ pTitle,
+ pMessage,
+ Ci.nsIPromptService.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ {}
+ )
+ ) {
+ return;
+ }
+ }
+
+ let feed = new Feed(itemToRemove.url, itemToRemove.parentFolder);
+ FeedUtils.deleteFeed(feed);
+
+ // Now that we have removed the feed from the datasource, it is time to
+ // update our view layer. Update parent folder's quickMode if necessary
+ // and remove the child from its parent folder object.
+ let parentIndex = this.mView.getParentIndex(seln.currentIndex);
+ let parentItem = this.mView.getItemAtIndex(parentIndex);
+ this.updateFolderQuickModeInView(itemToRemove, parentItem, true);
+ this.mView.removeItemAtIndex(seln.currentIndex, false);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedRemoved");
+ this.updateStatusItem("statusText", message);
+ },
+
+ /**
+ * This addFeed is used by 1) Add button, 1) Update button, 3) Drop of a
+ * feed url on a folder (which can be an add or move). If Update, the new
+ * url is added and the old removed; thus aParse is false and no new messages
+ * are downloaded, the feed is only validated and stored in the db. If dnd,
+ * the drop folder is selected and the url is prefilled, so proceed just as
+ * though the url were entered manually. This allows a user to see the dnd
+ * url better in case of errors.
+ *
+ * @param {String} aFeedLocation - the feed url; get the url from the
+ * input field if null.
+ * @param {nsIMsgFolder} aFolder - folder to subscribe, current selected
+ * folder if null.
+ * @param {Boolean} aParse - if true (default) parse and download
+ * the feed's articles.
+ * @param {Object} aParams - additional params.
+ * @param {Integer} aMode - action mode (default is kSubscribeMode)
+ * of the add.
+ *
+ * @returns {Boolean} success - true if edit checks passed and an
+ * async download has been initiated.
+ */
+ addFeed(aFeedLocation, aFolder, aParse, aParams, aMode) {
+ let message;
+ let parse = aParse == null ? true : aParse;
+ let mode = aMode == null ? this.kSubscribeMode : aMode;
+ let locationValue = document.getElementById("locationValue");
+ let quickMode =
+ aParams && "quickMode" in aParams
+ ? aParams.quickMode
+ : document.getElementById("quickMode").checked;
+ let name =
+ aParams && "name" in aParams
+ ? aParams.name
+ : document.getElementById("nameValue").value;
+ let options = aParams && "options" in aParams ? aParams.options : null;
+
+ if (aFeedLocation) {
+ locationValue.value = aFeedLocation;
+ }
+ let feedLocation = locationValue.value.trim();
+
+ if (!feedLocation) {
+ locationValue.focus();
+ message = locationValue.getAttribute("placeholder");
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ if (!FeedUtils.isValidScheme(feedLocation)) {
+ locationValue.focus();
+ message = FeedUtils.strings.GetStringFromName("subscribe-feedNotValid");
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ let addFolder;
+ if (aFolder) {
+ // For Update or if passed a folder.
+ if (aFolder instanceof Ci.nsIMsgFolder) {
+ addFolder = aFolder;
+ }
+ } else {
+ // A folder must be selected for Add and Drop.
+ let index = this.mView.selection.currentIndex;
+ let item = this.mView.getItemAtIndex(index);
+ if (item && item.container) {
+ addFolder = item.folder;
+ }
+ }
+
+ // Shouldn't happen. Or else not passed an nsIMsgFolder.
+ if (!addFolder) {
+ return false;
+ }
+
+ // Before we go any further, make sure the user is not already subscribed
+ // to this feed.
+ if (FeedUtils.feedAlreadyExists(feedLocation, addFolder.server)) {
+ locationValue.focus();
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedAlreadySubscribed"
+ );
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ if (!options) {
+ // Not passed a param, get values from the ui.
+ options = FeedUtils.optionsTemplate;
+ options.updates.enabled =
+ document.getElementById("updateEnabled").checked;
+ let biffUnits = document.getElementById("biffUnits").value;
+ let units = document.getElementById("updateValue").value;
+ let minutes =
+ biffUnits == FeedUtils.kBiffUnitsMinutes ? units : units * 24 * 60;
+ options.updates.updateUnits = biffUnits;
+ options.updates.updateMinutes = Number(minutes);
+ options.category.enabled =
+ document.getElementById("autotagEnable").checked;
+ options.category.prefixEnabled =
+ document.getElementById("autotagUsePrefix").checked;
+ options.category.prefix = document.getElementById("autotagPrefix").value;
+ }
+
+ let feedProperties = {
+ feedName: name,
+ feedLocation,
+ feedFolder: addFolder,
+ quickMode,
+ options,
+ };
+
+ let feed = this.storeFeed(feedProperties);
+ if (!feed) {
+ return false;
+ }
+
+ // Now validate and start downloading the feed.
+ message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed");
+ this.updateStatusItem("statusText", message);
+ this.updateStatusItem("progressMeter", "?");
+ document.getElementById("addFeed").disabled = true;
+ this.mActionMode = mode;
+ feed.download(parse, this.mFeedDownloadCallback);
+ return true;
+ },
+
+ // Helper routine used by addFeed and importOPMLFile.
+ storeFeed(feedProperties) {
+ let feed = new Feed(feedProperties.feedLocation, feedProperties.feedFolder);
+ feed.title = feedProperties.feedName;
+ feed.quickMode = feedProperties.quickMode;
+ feed.options = feedProperties.options;
+ return feed;
+ },
+
+ /**
+ * When a feed item is selected, the Update button is used to verify the
+ * existing feed url, or to verify and update the feed url if the field
+ * has been edited. This is the only use of the Update button.
+ *
+ * @returns {void}
+ */
+ updateFeed() {
+ let seln = this.mView.selection;
+ if (seln.count != 1) {
+ return;
+ }
+
+ let item = this.mView.getItemAtIndex(seln.currentIndex);
+ if (!item || item.container || !item.parentFolder) {
+ return;
+ }
+
+ let feed = new Feed(item.url, item.parentFolder);
+
+ // Disable the button.
+ document.getElementById("updateFeed").disabled = true;
+
+ let feedLocation = document.getElementById("locationValue").value.trim();
+ if (feed.url != feedLocation) {
+ // Updating a url. We need to add the new url and delete the old, to
+ // ensure everything is cleaned up correctly.
+ this.addFeed(null, item.parentFolder, false, null, this.kUpdateMode);
+ return;
+ }
+
+ // Now we want to verify if the stored feed url still works. If it
+ // doesn't, show the error.
+ let message = FeedUtils.strings.GetStringFromName(
+ "subscribe-validating-feed"
+ );
+ this.mActionMode = this.kVerifyUrlMode;
+ this.updateStatusItem("statusText", message);
+ this.updateStatusItem("progressMeter", "?");
+ feed.download(false, this.mFeedDownloadCallback);
+ },
+
+ /**
+ * Moves or copies a feed to another folder or account.
+ *
+ * @param {Integer} aOldFeedIndex - Index in tree of target feed item.
+ * @param {Integer} aNewParentIndex - Index in tree of target parent folder item.
+ * @param {String} aMoveCopy - Either "move" or "copy".
+ *
+ * @returns {void}
+ */
+ moveCopyFeed(aOldFeedIndex, aNewParentIndex, aMoveCopy) {
+ let moveFeed = aMoveCopy == "move";
+ let currentItem = this.mView.getItemAtIndex(aOldFeedIndex);
+ if (
+ !currentItem ||
+ this.mView.getParentIndex(aOldFeedIndex) == aNewParentIndex
+ ) {
+ // If the new parent is the same as the current parent, then do nothing.
+ return;
+ }
+
+ let currentParentIndex = this.mView.getParentIndex(aOldFeedIndex);
+ let currentParentItem = this.mView.getItemAtIndex(currentParentIndex);
+ let currentFolder = currentParentItem.folder;
+
+ let newParentItem = this.mView.getItemAtIndex(aNewParentIndex);
+ let newFolder = newParentItem.folder;
+
+ let accountMoveCopy = false;
+ if (currentFolder.rootFolder.URI == newFolder.rootFolder.URI) {
+ // Moving within the same account/feeds db.
+ if (newFolder.isServer || !moveFeed) {
+ // No moving to account folder if already in the account; can only move,
+ // not copy, to folder in the same account.
+ return;
+ }
+
+ // Update the destFolder for this feed's subscription.
+ FeedUtils.setSubscriptionAttr(
+ currentItem.url,
+ currentItem.parentFolder.server,
+ "destFolder",
+ newFolder.URI
+ );
+
+ // Update folderpane favicons.
+ Services.obs.notifyObservers(currentFolder, "folder-properties-changed");
+ Services.obs.notifyObservers(newFolder, "folder-properties-changed");
+ } else {
+ // Moving/copying to a new account. If dropping on the account folder,
+ // a new subfolder is created if necessary.
+ accountMoveCopy = true;
+ let mode = moveFeed ? this.kMoveMode : this.kCopyMode;
+ let params = {
+ quickMode: currentItem.quickMode,
+ name: currentItem.name,
+ options: currentItem.options,
+ };
+ // Subscribe to the new folder first. If it already exists in the
+ // account or on error, return.
+ if (!this.addFeed(currentItem.url, newFolder, false, params, mode)) {
+ return;
+ }
+ // Unsubscribe the feed from the old folder, if add to the new folder
+ // is successful, and doing a move.
+ if (moveFeed) {
+ let feed = new Feed(currentItem.url, currentItem.parentFolder);
+ FeedUtils.deleteFeed(feed);
+ }
+ }
+
+ // Update local favicons.
+ currentParentItem.favicon = newParentItem.favicon = null;
+
+ // Finally, update our view layer. Update old parent folder's quickMode
+ // and remove the old row, if move. Otherwise no change to the view.
+ if (moveFeed) {
+ this.updateFolderQuickModeInView(currentItem, currentParentItem, true);
+ this.mView.removeItemAtIndex(aOldFeedIndex, true);
+ if (aNewParentIndex > aOldFeedIndex) {
+ aNewParentIndex--;
+ }
+ }
+
+ if (accountMoveCopy) {
+ // If a cross account move/copy, download callback will update the view
+ // with the new location. Preselect folder/mode for callback.
+ this.selectFolder(newFolder, { parentIndex: aNewParentIndex });
+ return;
+ }
+
+ // Add the new row location to the view.
+ currentItem.level = newParentItem.level + 1;
+ currentItem.parentFolder = newFolder;
+ this.updateFolderQuickModeInView(currentItem, newParentItem, false);
+ newParentItem.children.push(currentItem);
+
+ if (newParentItem.open) {
+ // Close the container, selecting the feed will rebuild the view rows.
+ this.mView.toggle(aNewParentIndex);
+ }
+
+ this.selectFeed(
+ { folder: newParentItem.folder, url: currentItem.url },
+ aNewParentIndex
+ );
+
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedMoved");
+ this.updateStatusItem("statusText", message);
+ },
+
+ updateFolderQuickModeInView(aFeedItem, aParentItem, aRemove) {
+ let feedItem = aFeedItem;
+ let parentItem = aParentItem;
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(feedItem.parentFolder);
+ let feedsInFolder = feedUrlArray ? feedUrlArray.length : 0;
+
+ if (aRemove && feedsInFolder < 1) {
+ // Removed only feed in folder; set quickMode to server default.
+ parentItem.quickMode = parentItem.folder.server.getBoolValue("quickMode");
+ }
+
+ if (!aRemove) {
+ // Just added a feed to a folder. If there are already feeds in the
+ // folder, the feed must reflect the parent's quickMode. If it is the
+ // only feed, update the parent folder to the feed's quickMode.
+ if (feedsInFolder > 1) {
+ let feed = new Feed(feedItem.url, feedItem.parentFolder);
+ feed.quickMode = parentItem.quickMode;
+ feedItem.quickMode = parentItem.quickMode;
+ } else {
+ parentItem.quickMode = feedItem.quickMode;
+ }
+ }
+ },
+
+ onDragStart(aEvent) {
+ // Get the selected feed article (if there is one).
+ let seln = this.mView.selection;
+ if (seln.count != 1) {
+ return;
+ }
+
+ // Only initiate a drag if the item is a feed (ignore folders/containers).
+ let item = this.mView.getItemAtIndex(seln.currentIndex);
+ if (!item || item.container) {
+ return;
+ }
+
+ aEvent.dataTransfer.setData("text/x-moz-feed-index", seln.currentIndex);
+ aEvent.dataTransfer.effectAllowed = "copyMove";
+ },
+
+ onDragOver(aEvent) {
+ this.mView._currentDataTransfer = aEvent.dataTransfer;
+ },
+
+ mFeedDownloadCallback: {
+ mSubscribeMode: true,
+ async downloaded(feed, aErrorCode) {
+ // Offline check is done in the context of 3pane, return to the subscribe
+ // window once the modal prompt is dispatched.
+ window.focus();
+ // Feed is null if our attempt to parse the feed failed.
+ let message = "";
+ let win = FeedSubscriptions;
+ if (
+ aErrorCode == FeedUtils.kNewsBlogSuccess ||
+ aErrorCode == FeedUtils.kNewsBlogNoNewItems
+ ) {
+ win.updateStatusItem("progressMeter", 100);
+
+ if (win.mActionMode == win.kVerifyUrlMode) {
+ // Just checking for errors, if none bye. The (non error) code
+ // kNewsBlogNoNewItems can only happen in verify mode.
+ win.mActionMode = null;
+ win.clearStatusInfo();
+ if (Services.io.offline) {
+ return;
+ }
+
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedVerified"
+ );
+ win.updateStatusItem("statusText", message);
+ return;
+ }
+
+ // Update lastUpdateTime if successful.
+ let options = feed.options;
+ options.updates.lastUpdateTime = Date.now();
+ feed.options = options;
+
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+
+ // Set isBusy status, and clear it after getting favicon. This makes
+ // sure the folder icon is redrawn to reflect what we got.
+ FeedUtils.setStatus(
+ feed.folder,
+ feed.url,
+ "code",
+ FeedUtils.kNewsBlogFeedIsBusy
+ );
+ await FeedUtils.getFavicon(feed.folder, feed.url);
+ FeedUtils.setStatus(feed.folder, feed.url, "code", aErrorCode);
+
+ // Now add the feed to our view. If adding, the current selection will
+ // be a folder; if updating it will be a feed. No need to rebuild the
+ // entire view, that is too jarring.
+ let curIndex = win.mView.selection.currentIndex;
+ let curItem = win.mView.getItemAtIndex(curIndex);
+ if (curItem) {
+ let parentIndex, parentItem, newItem, level;
+ if (curItem.container) {
+ // Open the container, if it exists.
+ let folderExists = win.selectFolder(feed.folder, {
+ parentIndex: curIndex,
+ });
+ if (!folderExists) {
+ // This means a new folder was created.
+ parentIndex = curIndex;
+ parentItem = curItem;
+ level = curItem.level + 1;
+ newItem = win.makeFolderObject(feed.folder, level);
+ } else {
+ // If a folder happens to exist which matches one that would
+ // have been created, the feed system reuses it. Get the
+ // current item again if reusing a previously unselected folder.
+ curIndex = win.mView.selection.currentIndex;
+ curItem = win.mView.getItemAtIndex(curIndex);
+ parentIndex = curIndex;
+ parentItem = curItem;
+ level = curItem.level + 1;
+ newItem = win.makeFeedObject(feed, feed.folder, level);
+ }
+ } else {
+ // Adding a feed.
+ parentIndex = win.mView.getParentIndex(curIndex);
+ parentItem = win.mView.getItemAtIndex(parentIndex);
+ level = curItem.level;
+ newItem = win.makeFeedObject(feed, feed.folder, level);
+ }
+
+ if (!newItem.container) {
+ win.updateFolderQuickModeInView(newItem, parentItem, false);
+ }
+
+ parentItem.children.push(newItem);
+ parentItem.children = win.folderItemSorter(parentItem.children);
+ parentItem.favicon = null;
+
+ if (win.mActionMode == win.kSubscribeMode) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedAdded"
+ );
+ }
+ if (win.mActionMode == win.kUpdateMode) {
+ win.removeFeed(false);
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedUpdated"
+ );
+ }
+ if (win.mActionMode == win.kMoveMode) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedMoved"
+ );
+ }
+ if (win.mActionMode == win.kCopyMode) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedCopied"
+ );
+ }
+
+ win.selectFeed(feed, parentIndex);
+ }
+ } else {
+ // Non success. Remove intermediate traces from the feeds database.
+ // But only if we're not in verify mode.
+ if (
+ win.mActionMode != win.kVerifyUrlMode &&
+ feed &&
+ feed.url &&
+ feed.server
+ ) {
+ FeedUtils.deleteFeed(feed);
+ }
+
+ if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedNotValid"
+ );
+ }
+ if (aErrorCode == FeedUtils.kNewsBlogRequestFailure) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-networkError"
+ );
+ }
+ if (aErrorCode == FeedUtils.kNewsBlogFileError) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile"
+ );
+ }
+ if (aErrorCode == FeedUtils.kNewsBlogBadCertError) {
+ let host = Services.io.newURI(feed.url).host;
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-badCertError",
+ [host]
+ );
+ }
+ if (aErrorCode == FeedUtils.kNewsBlogNoAuthError) {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-noAuthError"
+ );
+ }
+
+ // Focus the url if verify/update failed.
+ if (
+ win.mActionMode == win.kUpdateMode ||
+ win.mActionMode == win.kVerifyUrlMode
+ ) {
+ document.getElementById("locationValue").focus();
+ }
+ }
+
+ win.mActionMode = null;
+ win.clearStatusInfo();
+ let code = feed.url.startsWith("http") ? aErrorCode : null;
+ win.updateStatusItem("statusText", message, code);
+ },
+
+ // This gets called after the RSS parser finishes storing a feed item to
+ // disk. aCurrentFeedItems is an integer corresponding to how many feed
+ // items have been downloaded so far. aMaxFeedItems is an integer
+ // corresponding to the total number of feed items to download.
+ onFeedItemStored(feed, aCurrentFeedItems, aMaxFeedItems) {
+ window.focus();
+ let message = FeedUtils.strings.formatStringFromName(
+ "subscribe-gettingFeedItems",
+ [aCurrentFeedItems, aMaxFeedItems]
+ );
+ FeedSubscriptions.updateStatusItem("statusText", message);
+ this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
+ },
+
+ onProgress(feed, aProgress, aProgressMax, aLengthComputable) {
+ FeedSubscriptions.updateStatusItem(
+ "progressMeter",
+ (aProgress * 100) / (aProgressMax || 100)
+ );
+ },
+ },
+
+ // Status routines.
+ updateStatusItem(aID, aValue, aErrorCode) {
+ let el = document.getElementById(aID);
+ if (el.getAttribute("collapsed")) {
+ el.removeAttribute("collapsed");
+ }
+ if (el.hidden) {
+ el.hidden = false;
+ }
+
+ if (aID == "progressMeter") {
+ if (aValue == "?") {
+ el.removeAttribute("value");
+ } else {
+ el.value = aValue;
+ }
+ } else if (aID == "statusText") {
+ el.textContent = aValue;
+ }
+
+ el = document.getElementById("validationText");
+ if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed) {
+ el.removeAttribute("collapsed");
+ } else {
+ el.setAttribute("collapsed", true);
+ }
+
+ el = document.getElementById("addCertException");
+ if (aErrorCode == FeedUtils.kNewsBlogBadCertError) {
+ el.removeAttribute("collapsed");
+ } else {
+ el.setAttribute("collapsed", true);
+ }
+ },
+
+ clearStatusInfo() {
+ document.getElementById("statusText").textContent = "";
+ document.getElementById("progressMeter").hidden = true;
+ document.getElementById("validationText").collapsed = true;
+ document.getElementById("addCertException").collapsed = true;
+ },
+
+ checkValidation(aEvent) {
+ if (aEvent.button != 0) {
+ return;
+ }
+
+ let validationQuery = "http://validator.w3.org/feed/check.cgi?url=";
+
+ if (this.mMainWin) {
+ let tabmail = this.mMainWin.document.getElementById("tabmail");
+ if (tabmail) {
+ let feedLocation = document.getElementById("locationValue").value;
+ let url = validationQuery + encodeURIComponent(feedLocation);
+
+ this.mMainWin.focus();
+ this.mMainWin.openContentTab(url);
+ FeedUtils.log.debug("checkValidation: query url - " + url);
+ }
+ }
+ aEvent.stopPropagation();
+ },
+
+ addCertExceptionDialog() {
+ let locationValue = document.getElementById("locationValue");
+ let feedURL = locationValue.value.trim();
+ let params = {
+ exceptionAdded: false,
+ location: feedURL,
+ prefetchCert: true,
+ };
+ window.openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "",
+ "chrome,centerscreen,modal",
+ params
+ );
+ if (params.exceptionAdded) {
+ this.clearStatusInfo();
+ }
+
+ locationValue.focus();
+ },
+
+ // Listener for folder pane changes.
+ FolderListener: {
+ get feedWindow() {
+ let subscriptionsWindow = Services.wm.getMostRecentWindow(
+ "Mail:News-BlogSubscriptions"
+ );
+ return subscriptionsWindow ? subscriptionsWindow.FeedSubscriptions : null;
+ },
+
+ get currentSelectedIndex() {
+ return this.feedWindow
+ ? this.feedWindow.mView.selection.currentIndex
+ : -1;
+ },
+
+ get currentSelectedItem() {
+ return this.feedWindow ? this.feedWindow.mView.currentItem : null;
+ },
+
+ folderAdded(aFolder) {
+ if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder)) {
+ return;
+ }
+
+ let parentFolder = aFolder.isServer ? aFolder : aFolder.parent;
+ FeedUtils.log.debug(
+ "folderAdded: folder:parent - " +
+ aFolder.name +
+ ":" +
+ (parentFolder ? parentFolder.filePath.path : "(null)")
+ );
+
+ if (!parentFolder || !this.feedWindow) {
+ return;
+ }
+
+ let feedWindow = this.feedWindow;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.tree.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(parentFolder);
+ let open = indexInView != null;
+
+ if (aFolder.isServer) {
+ if (indexInView != null) {
+ // Existing account root folder in the view.
+ open = feedWindow.mView.getItemAtIndex(indexInView).open;
+ } else {
+ // Add the account root folder to the view.
+ feedWindow.mFeedContainers.push(
+ feedWindow.makeFolderObject(parentFolder, 0)
+ );
+ feedWindow.mView.mRowCount++;
+ feedWindow.mTree.view = feedWindow.mView;
+ feedWindow.mView.tree.scrollToRow(firstVisRow);
+ return;
+ }
+ }
+
+ // Rebuild the added folder's parent item in the tree row cache.
+ feedWindow.selectFolder(parentFolder, {
+ select: false,
+ open,
+ newFolder: parentFolder,
+ });
+
+ if (indexInView == null || !curSelItem) {
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+ }
+
+ let parentIndex = feedWindow.mView.getParentIndex(indexInView);
+ if (parentIndex == feedWindow.mView.kRowIndexUndefined) {
+ // Root folder is its own parent.
+ parentIndex = indexInView;
+ }
+
+ if (open) {
+ // Close an open parent (or root) folder.
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ }
+
+ feedWindow.mView.tree.scrollToRow(firstVisRow);
+
+ if (curSelItem.container) {
+ feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
+ } else {
+ feedWindow.selectFeed(
+ { folder: curSelItem.parentFolder, url: curSelItem.url },
+ parentIndex
+ );
+ }
+ },
+
+ folderDeleted(aFolder) {
+ if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder)) {
+ return;
+ }
+
+ FeedUtils.log.debug("folderDeleted: folder - " + aFolder.name);
+ if (!this.feedWindow) {
+ return;
+ }
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let indexInView = feedWindow.mView.getItemInViewIndex(aFolder);
+ let open = indexInView != null;
+
+ // Delete the folder from the tree row cache.
+ feedWindow.selectFolder(aFolder, {
+ select: false,
+ open: false,
+ remove: true,
+ });
+
+ if (!open || curSelIndex < 0) {
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+ }
+
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+
+ feedWindow.mView.removeItemAtIndex(indexInView, !select);
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ if (aNewFolder.server.type != "rss" || FeedUtils.isInTrash(aNewFolder)) {
+ return;
+ }
+
+ FeedUtils.log.debug(
+ "folderRenamed: old:new - " + aOrigFolder.name + ":" + aNewFolder.name
+ );
+ if (!this.feedWindow) {
+ return;
+ }
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.tree.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(aOrigFolder);
+ let open = indexInView != null;
+
+ // Rebuild the renamed folder's item in the tree row cache.
+ feedWindow.selectFolder(aOrigFolder, {
+ select: false,
+ open,
+ newFolder: aNewFolder,
+ });
+
+ if (!open || !curSelItem) {
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+ }
+
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+
+ let parentIndex = feedWindow.mView.getParentIndex(indexInView);
+ if (parentIndex == feedWindow.mView.kRowIndexUndefined) {
+ // Root folder is its own parent.
+ parentIndex = indexInView;
+ }
+
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ feedWindow.mView.tree.scrollToRow(firstVisRow);
+
+ if (curSelItem.container) {
+ if (curSelItem.folder == aOrigFolder) {
+ feedWindow.selectFolder(aNewFolder, { open: curSelItem.open });
+ } else if (select) {
+ feedWindow.mView.selection.select(indexInView);
+ } else {
+ feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
+ }
+ } else {
+ feedWindow.selectFeed(
+ { folder: curSelItem.parentFolder.rootFolder, url: curSelItem.url },
+ parentIndex
+ );
+ }
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ if (aDestFolder.server.type != "rss") {
+ return;
+ }
+
+ FeedUtils.log.debug(
+ "folderMoveCopyCompleted: move:src:dest - " +
+ aMove +
+ ":" +
+ aSrcFolder.name +
+ ":" +
+ aDestFolder.name
+ );
+ if (!this.feedWindow) {
+ return;
+ }
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.tree.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(aSrcFolder);
+ let destIndexInView = feedWindow.mView.getItemInViewIndex(aDestFolder);
+ let open = indexInView != null || destIndexInView != null;
+ let parentIndex = feedWindow.mView.getItemInViewIndex(
+ aDestFolder.parent || aDestFolder
+ );
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+
+ if (aMove) {
+ this.folderDeleted(aSrcFolder);
+ if (aDestFolder.getFlag(Ci.nsMsgFolderFlags.Trash)) {
+ return;
+ }
+ }
+
+ setTimeout(() => {
+ // State on disk needs to settle before a folder object can be rebuilt.
+ feedWindow.selectFolder(aDestFolder, {
+ select: false,
+ open: open || select,
+ newFolder: aDestFolder,
+ });
+
+ if (!open || !curSelItem) {
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+ }
+
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ feedWindow.mView.tree.scrollToRow(firstVisRow);
+
+ if (curSelItem.container) {
+ if (curSelItem.folder == aSrcFolder || select) {
+ feedWindow.selectFolder(aDestFolder, { open: true });
+ } else {
+ feedWindow.selectFolder(curSelItem.folder, {
+ open: curSelItem.open,
+ });
+ }
+ } else {
+ feedWindow.selectFeed(
+ { folder: curSelItem.parentFolder.rootFolder, url: curSelItem.url },
+ null
+ );
+ }
+ }, 50);
+ },
+ },
+
+ /* *************************************************************** */
+ /* OPML Functions */
+ /* *************************************************************** */
+
+ get brandShortName() {
+ let brandBundle = document.getElementById("bundle_brand");
+ return brandBundle ? brandBundle.getString("brandShortName") : "";
+ },
+
+ /**
+ * Export feeds as opml file Save As filepicker function.
+ *
+ * @param {Boolean} aList - If true, exporting as list; if false (default)
+ * exporting feeds in folder structure - used for title.
+ * @returns {Promise} nsIFile or null.
+ */
+ opmlPickSaveAsFile(aList) {
+ let accountName = this.mRSSServer.rootFolder.prettyName;
+ let fileName = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportDefaultFileName",
+ [this.brandShortName, accountName]
+ );
+ let title = aList
+ ? FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportTitleList",
+ [accountName]
+ )
+ : FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportTitleStruct",
+ [accountName]
+ );
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.defaultString = fileName;
+ fp.defaultExtension = "opml";
+ if (
+ this.opmlLastSaveAsDir &&
+ this.opmlLastSaveAsDir instanceof Ci.nsIFile
+ ) {
+ fp.displayDirectory = this.opmlLastSaveAsDir;
+ }
+
+ let opmlFilterText = FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLExportOPMLFilesFilterText"
+ );
+ fp.appendFilter(opmlFilterText, "*.opml");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+ fp.init(window, title, Ci.nsIFilePicker.modeSave);
+
+ return new Promise(resolve => {
+ fp.open(rv => {
+ if (
+ (rv != Ci.nsIFilePicker.returnOK &&
+ rv != Ci.nsIFilePicker.returnReplace) ||
+ !fp.file
+ ) {
+ resolve(null);
+ return;
+ }
+
+ this.opmlLastSaveAsDir = fp.file.parent;
+ resolve(fp.file);
+ });
+ });
+ },
+
+ /**
+ * Import feeds opml file Open filepicker function.
+ *
+ * @returns {Promise} [{nsIFile} file, {String} fileUrl] or null.
+ */
+ opmlPickOpenFile() {
+ let title = FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLImportTitle"
+ );
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.defaultString = "";
+ if (this.opmlLastOpenDir && this.opmlLastOpenDir instanceof Ci.nsIFile) {
+ fp.displayDirectory = this.opmlLastOpenDir;
+ }
+
+ let opmlFilterText = FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLExportOPMLFilesFilterText"
+ );
+ fp.appendFilter(opmlFilterText, "*.opml");
+ fp.appendFilters(Ci.nsIFilePicker.filterXML);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.init(window, title, Ci.nsIFilePicker.modeOpen);
+
+ return new Promise(resolve => {
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ resolve(null);
+ return;
+ }
+
+ this.opmlLastOpenDir = fp.file.parent;
+ resolve([fp.file, fp.fileURL.spec]);
+ });
+ });
+ },
+
+ async exportOPML(aEvent) {
+ // Account folder must be selected.
+ let item = this.mView.currentItem;
+ if (!item || !item.folder || !item.folder.isServer) {
+ return;
+ }
+
+ this.mRSSServer = item.folder.server;
+ let rootFolder = this.mRSSServer.rootFolder;
+ let exportAsList = aEvent.ctrlKey;
+ let SPACES2 = " ";
+ let SPACES4 = " ";
+
+ if (this.mRSSServer.rootFolder.hasSubFolders) {
+ let opmlDoc = document.implementation.createDocument("", "opml", null);
+ let opmlRoot = opmlDoc.documentElement;
+ opmlRoot.setAttribute("version", "1.0");
+ opmlRoot.setAttribute("xmlns:fz", "urn:forumzilla:");
+
+ this.generatePPSpace(opmlRoot, SPACES2);
+
+ // Make the <head> element.
+ let head = opmlDoc.createElement("head");
+ this.generatePPSpace(head, SPACES4);
+ let titleText = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportFileDialogTitle",
+ [this.brandShortName, rootFolder.prettyName]
+ );
+ let title = opmlDoc.createElement("title");
+ title.appendChild(opmlDoc.createTextNode(titleText));
+ head.appendChild(title);
+ this.generatePPSpace(head, SPACES4);
+ let dt = opmlDoc.createElement("dateCreated");
+ dt.appendChild(opmlDoc.createTextNode(new Date().toUTCString()));
+ head.appendChild(dt);
+ this.generatePPSpace(head, SPACES2);
+ opmlRoot.appendChild(head);
+
+ this.generatePPSpace(opmlRoot, SPACES2);
+
+ // Add <outline>s to the <body>.
+ let body = opmlDoc.createElement("body");
+ if (exportAsList) {
+ this.generateOutlineList(rootFolder, body, SPACES4.length + 2);
+ } else {
+ this.generateOutlineStruct(rootFolder, body, SPACES4.length);
+ }
+
+ this.generatePPSpace(body, SPACES2);
+
+ if (!body.childElementCount) {
+ // No folders/feeds.
+ return;
+ }
+
+ opmlRoot.appendChild(body);
+ this.generatePPSpace(opmlRoot, "");
+
+ // Get file to save from filepicker.
+ let saveAsFile = await this.opmlPickSaveAsFile(exportAsList);
+ if (!saveAsFile) {
+ return;
+ }
+
+ let fos = FileUtils.openSafeFileOutputStream(saveAsFile);
+ let serializer = new XMLSerializer();
+ serializer.serializeToStream(opmlDoc, fos, "utf-8");
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ let statusReport = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportDone",
+ [saveAsFile.path]
+ );
+ this.updateStatusItem("statusText", statusReport);
+ FeedUtils.log.info("exportOPML: " + statusReport);
+ }
+ },
+
+ generatePPSpace(aNode, indentString) {
+ aNode.appendChild(aNode.ownerDocument.createTextNode("\n"));
+ aNode.appendChild(aNode.ownerDocument.createTextNode(indentString));
+ },
+
+ generateOutlineList(baseFolder, parent, indentLevel) {
+ // Pretty printing.
+ let indentString = " ".repeat(indentLevel - 2);
+
+ let feedOutline;
+ for (let folder of baseFolder.subFolders) {
+ FeedUtils.log.debug(
+ "generateOutlineList: folder - " + folder.filePath.path
+ );
+ if (
+ !(folder instanceof Ci.nsIMsgFolder) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ continue;
+ }
+
+ FeedUtils.log.debug(
+ "generateOutlineList: CONTINUE folderName - " + folder.name
+ );
+
+ if (folder.hasSubFolders) {
+ FeedUtils.log.debug(
+ "generateOutlineList: has subfolders - " + folder.name
+ );
+ // Recurse.
+ this.generateOutlineList(folder, parent, indentLevel);
+ }
+
+ // Add outline elements with xmlUrls.
+ let feeds = this.getFeedsInFolder(folder);
+ for (let feed of feeds) {
+ FeedUtils.log.debug(
+ "generateOutlineList: folder has FEED url - " +
+ folder.name +
+ " : " +
+ feed.url
+ );
+ feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
+ this.generatePPSpace(parent, indentString);
+ parent.appendChild(feedOutline);
+ }
+ }
+ },
+
+ generateOutlineStruct(baseFolder, parent, indentLevel) {
+ // Pretty printing.
+ function indentString(len) {
+ return " ".repeat(len - 2);
+ }
+
+ let folderOutline, feedOutline;
+ for (let folder of baseFolder.subFolders) {
+ FeedUtils.log.debug(
+ "generateOutlineStruct: folder - " + folder.filePath.path
+ );
+ if (
+ !(folder instanceof Ci.nsIMsgFolder) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ continue;
+ }
+
+ FeedUtils.log.debug(
+ "generateOutlineStruct: CONTINUE folderName - " + folder.name
+ );
+
+ // Make a folder outline element.
+ folderOutline = parent.ownerDocument.createElement("outline");
+ folderOutline.setAttribute("title", folder.prettyName);
+ this.generatePPSpace(parent, indentString(indentLevel + 2));
+
+ if (folder.hasSubFolders) {
+ FeedUtils.log.debug(
+ "generateOutlineStruct: has subfolders - " + folder.name
+ );
+ // Recurse.
+ this.generateOutlineStruct(folder, folderOutline, indentLevel + 2);
+ }
+
+ let feeds = this.getFeedsInFolder(folder);
+ for (let feed of feeds) {
+ // Add feed outline elements with xmlUrls.
+ FeedUtils.log.debug(
+ "generateOutlineStruct: folder has FEED url - " +
+ folder.name +
+ " : " +
+ feed.url
+ );
+ feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
+ this.generatePPSpace(folderOutline, indentString(indentLevel + 4));
+ folderOutline.appendChild(feedOutline);
+ }
+
+ parent.appendChild(folderOutline);
+ }
+ },
+
+ exportOPMLOutline(aFeed, aDoc) {
+ let outRv = aDoc.createElement("outline");
+ outRv.setAttribute("type", "rss");
+ outRv.setAttribute("title", aFeed.title);
+ outRv.setAttribute("text", aFeed.title);
+ outRv.setAttribute("version", "RSS");
+ outRv.setAttribute("fz:quickMode", aFeed.quickMode);
+ outRv.setAttribute("fz:options", JSON.stringify(aFeed.options));
+ outRv.setAttribute("xmlUrl", aFeed.url);
+ outRv.setAttribute("htmlUrl", aFeed.link);
+ return outRv;
+ },
+
+ async importOPML() {
+ // Account folder must be selected in subscribe dialog.
+ let item = this.mView ? this.mView.currentItem : null;
+ if (!item || !item.folder || !item.folder.isServer) {
+ return;
+ }
+
+ let server = item.folder.server;
+ // Get file and file url to open from filepicker.
+ let [openFile, openFileUrl] = await this.opmlPickOpenFile();
+
+ this.mActionMode = this.kImportingOPML;
+ this.updateButtons(null);
+ this.selectFolder(item.folder, { select: false, open: true });
+ let statusReport = FeedUtils.strings.GetStringFromName("subscribe-loading");
+ this.updateStatusItem("statusText", statusReport);
+ // If there were a getElementsByAttribute in html, we could go determined...
+ this.updateStatusItem("progressMeter", "?");
+
+ if (
+ !(await this.importOPMLFile(
+ openFile,
+ openFileUrl,
+ server,
+ this.importOPMLFinished
+ ))
+ ) {
+ this.mActionMode = null;
+ this.updateButtons(item);
+ this.clearStatusInfo();
+ }
+ },
+
+ /**
+ * Import opml file into a feed account. Used by the Subscribe dialog and
+ * the Import wizard.
+ *
+ * @param {nsIFile} aFile - The opml file.
+ * @param {string} aFileUrl - The opml file url.
+ * @param {nsIMsgIncomingServer} aServer - The account server.
+ * @param {Function} aCallback - Callback function.
+ *
+ * @returns {Boolean} - false if error.
+ */
+ async importOPMLFile(aFile, aFileUrl, aServer, aCallback) {
+ if (aServer && aServer instanceof Ci.nsIMsgIncomingServer) {
+ this.mRSSServer = aServer;
+ }
+
+ if (!aFile || !aFileUrl || !this.mRSSServer) {
+ return false;
+ }
+
+ let opmlDom, statusReport;
+ FeedUtils.log.debug(
+ "importOPMLFile: fileName:fileUrl - " + aFile.leafName + ":" + aFileUrl
+ );
+ let request = new Request(aFileUrl);
+ await fetch(request)
+ .then(function (response) {
+ if (!response.ok) {
+ // If the OPML file is not readable/accessible.
+ statusReport = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile"
+ );
+ return null;
+ }
+
+ return response.text();
+ })
+ .then(function (responseText) {
+ if (responseText != null) {
+ opmlDom = new DOMParser().parseFromString(
+ responseText,
+ "application/xml"
+ );
+ if (
+ !XMLDocument.isInstance(opmlDom) ||
+ opmlDom.documentElement.namespaceURI ==
+ FeedUtils.MOZ_PARSERERROR_NS ||
+ opmlDom.documentElement.tagName != "opml" ||
+ !(
+ opmlDom.querySelector("body") &&
+ opmlDom.querySelector("body").childElementCount
+ )
+ ) {
+ // If the OPML file is invalid or empty.
+ statusReport = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLImportInvalidFile",
+ [aFile.leafName]
+ );
+ }
+ }
+ })
+ .catch(function (error) {
+ statusReport = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile"
+ );
+ FeedUtils.log.error("importOPMLFile: error - " + error.message);
+ });
+
+ if (statusReport) {
+ FeedUtils.log.error("importOPMLFile: status - " + statusReport);
+ Services.prompt.alert(window, null, statusReport);
+ return false;
+ }
+
+ let body = opmlDom.querySelector("body");
+ this.importOPMLOutlines(body, this.mRSSServer, aCallback);
+ return true;
+ },
+
+ importOPMLOutlines(aBody, aRSSServer, aCallback) {
+ let win = this;
+ let rssServer = aRSSServer;
+ let callback = aCallback;
+ let outline, feedFolder;
+ let badTag = false;
+ let firstFeedInFolderQuickMode = null;
+ let lastFolder;
+ let feedsAdded = 0;
+ let rssOutlines = 0;
+
+ function processor(aParentNode, aParentFolder) {
+ FeedUtils.log.trace(
+ "importOPMLOutlines: PROCESSOR tag:name:children - " +
+ aParentNode.tagName +
+ ":" +
+ aParentNode.getAttribute("text") +
+ ":" +
+ aParentNode.childElementCount
+ );
+ while (true) {
+ if (aParentNode.tagName == "body" && !aParentNode.childElementCount) {
+ // Finished.
+ let statusReport = win.importOPMLStatus(feedsAdded, rssOutlines);
+ callback(statusReport, lastFolder, win);
+ return;
+ }
+
+ outline = aParentNode.firstElementChild;
+ if (outline.tagName != "outline") {
+ FeedUtils.log.info(
+ "importOPMLOutlines: skipping, node is not an " +
+ "<outline> - <" +
+ outline.tagName +
+ ">"
+ );
+ badTag = true;
+ break;
+ }
+
+ let outlineName =
+ outline.getAttribute("text") ||
+ outline.getAttribute("title") ||
+ outline.getAttribute("xmlUrl");
+ let feedUrl, folder;
+
+ if (outline.getAttribute("type") == "rss") {
+ // A feed outline.
+ feedUrl =
+ outline.getAttribute("xmlUrl") || outline.getAttribute("url");
+ if (!feedUrl) {
+ FeedUtils.log.info(
+ "importOPMLOutlines: skipping, type=rss <outline> " +
+ "has no url - " +
+ outlineName
+ );
+ break;
+ }
+
+ rssOutlines++;
+ feedFolder = aParentFolder;
+
+ if (FeedUtils.feedAlreadyExists(feedUrl, rssServer)) {
+ FeedUtils.log.info(
+ "importOPMLOutlines: feed already subscribed in account " +
+ rssServer.prettyName +
+ ", url - " +
+ feedUrl
+ );
+ break;
+ }
+
+ if (
+ aParentNode.tagName == "outline" &&
+ aParentNode.getAttribute("type") != "rss"
+ ) {
+ // Parent is a folder, already created.
+ folder = feedFolder;
+ } else {
+ // Parent is not a folder outline, likely the <body> in a flat list.
+ // Create feed's folder with feed's name and account rootFolder as
+ // parent of feed's folder.
+ // NOTE: Assume a type=rss outline must be a leaf and is not a
+ // direct parent of another type=rss outline; such a structure
+ // may lead to unintended nesting and inaccurate counts.
+ folder = rssServer.rootFolder;
+ }
+
+ // Create the feed.
+ let quickMode = outline.hasAttribute("fz:quickMode")
+ ? outline.getAttribute("fz:quickMode") == "true"
+ : rssServer.getBoolValue("quickMode");
+ let options = outline.getAttribute("fz:options");
+ options = options ? JSON.parse(options) : null;
+
+ if (firstFeedInFolderQuickMode === null) {
+ // The summary/web page pref applies to all feeds in a folder,
+ // though it is a property of an individual feed. This can be
+ // set (and is obvious) in the subscribe dialog; ensure import
+ // doesn't leave mismatches if mismatched in the opml file.
+ firstFeedInFolderQuickMode = quickMode;
+ } else {
+ quickMode = firstFeedInFolderQuickMode;
+ }
+
+ let feedProperties = {
+ feedName: outlineName,
+ feedLocation: feedUrl,
+ feedFolder: folder,
+ quickMode,
+ options,
+ };
+
+ FeedUtils.log.info(
+ "importOPMLOutlines: importing feed: name, url - " +
+ outlineName +
+ ", " +
+ feedUrl
+ );
+
+ let feed = win.storeFeed(feedProperties);
+ if (outline.hasAttribute("htmlUrl")) {
+ feed.link = outline.getAttribute("htmlUrl");
+ }
+
+ feed.createFolder();
+ if (!feed.folder) {
+ // Non success. Remove intermediate traces from the feeds database.
+ if (feed && feed.url && feed.server) {
+ FeedUtils.deleteFeed(feed);
+ }
+
+ FeedUtils.log.info(
+ "importOPMLOutlines: skipping, error creating folder - '" +
+ feed.folderName +
+ "' from outlineName - '" +
+ outlineName +
+ "' in parent folder " +
+ aParentFolder.filePath.path
+ );
+ badTag = true;
+ break;
+ }
+
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+ // Feed correctly added.
+ feedsAdded++;
+ lastFolder = feed.folder;
+ } else {
+ // A folder outline. If a folder exists in the account structure at
+ // the same level as in the opml structure, feeds are placed into the
+ // existing folder.
+ let folderName = outlineName;
+ try {
+ feedFolder = aParentFolder.getChildNamed(folderName);
+ } catch (ex) {
+ // Folder not found, create it.
+ FeedUtils.log.info(
+ "importOPMLOutlines: creating folder - '" +
+ folderName +
+ "' from outlineName - '" +
+ outlineName +
+ "' in parent folder " +
+ aParentFolder.filePath.path
+ );
+ firstFeedInFolderQuickMode = null;
+ try {
+ feedFolder = aParentFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder(folderName);
+ } catch (ex) {
+ // An error creating. Skip it.
+ FeedUtils.log.info(
+ "importOPMLOutlines: skipping, error creating folder - '" +
+ folderName +
+ "' from outlineName - '" +
+ outlineName +
+ "' in parent folder " +
+ aParentFolder.filePath.path
+ );
+ let xfolder = aParentFolder.getChildNamed(folderName);
+ aParentFolder.propagateDelete(xfolder, true);
+ badTag = true;
+ break;
+ }
+ }
+ }
+
+ break;
+ }
+
+ if (!outline.childElementCount || badTag) {
+ // Remove leaf nodes that are processed or bad tags from the opml dom,
+ // and go back to reparse. This method lets us use setTimeout to
+ // prevent UI hang, in situations of both deep and shallow trees.
+ // A yield/generator.next() method is fine for shallow trees, but not
+ // the true recursion required for deeper trees; both the shallow loop
+ // and the recurse should give it up.
+ outline.remove();
+ badTag = false;
+ outline = aBody;
+ feedFolder = rssServer.rootFolder;
+ }
+
+ setTimeout(() => {
+ processor(outline, feedFolder);
+ }, 0);
+ }
+
+ processor(aBody, rssServer.rootFolder);
+ },
+
+ importOPMLStatus(aFeedsAdded, aRssOutlines, aFolderOutlines) {
+ let statusReport;
+ if (aRssOutlines > aFeedsAdded) {
+ statusReport = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLImportStatus",
+ [
+ PluralForm.get(
+ aFeedsAdded,
+ FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLImportUniqueFeeds"
+ )
+ ).replace("#1", aFeedsAdded),
+ PluralForm.get(
+ aRssOutlines,
+ FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLImportFoundFeeds"
+ )
+ ).replace("#1", aRssOutlines),
+ ],
+ 2
+ );
+ } else {
+ statusReport = PluralForm.get(
+ aFeedsAdded,
+ FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFeedCount")
+ ).replace("#1", aFeedsAdded);
+ }
+
+ return statusReport;
+ },
+
+ importOPMLFinished(aStatusReport, aLastFolder, aWin) {
+ if (aLastFolder) {
+ aWin.selectFolder(aLastFolder, { select: false, newFolder: aLastFolder });
+ aWin.selectFolder(aLastFolder.parent);
+ }
+ aWin.mActionMode = null;
+ aWin.updateButtons(aWin.mView.currentItem);
+ aWin.clearStatusInfo();
+ aWin.updateStatusItem("statusText", aStatusReport);
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/feed-subscriptions.xhtml b/comm/mailnews/extensions/newsblog/feed-subscriptions.xhtml
new file mode 100644
index 0000000000..d2f8cba2bb
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/feed-subscriptions.xhtml
@@ -0,0 +1,373 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd">
+%feedDTD;
+<!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd">
+%newsblogDTD; ]>
+
+<html
+ id="feedSubscriptions"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ windowtype="Mail:News-BlogSubscriptions"
+ persist="width height screenX screenY sizemode"
+ lightweightthemes="true"
+>
+ <head>
+ <title>&feedSubscriptions.label;</title>
+ <link rel="localization" href="security/certificates/certManager.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/specialTabs.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-newsblog/content/feed-subscriptions.js"
+ ></script>
+ <script>
+ window.addEventListener("load", event => {
+ FeedSubscriptions.onLoad();
+ });
+ window.addEventListener("keypress", event => {
+ FeedSubscriptions.onKeyPress(event);
+ });
+ window.addEventListener("mousedown", event => {
+ FeedSubscriptions.onMouseDown(event);
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ id="subscriptionsDialog"
+ buttons="accept"
+ buttonlabelaccept="&button.close.label;"
+ >
+ <keyset id="extensionsKeys">
+ <key
+ id="key_close"
+ key="&cmd.close.commandKey;"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ <key id="key_close2" keycode="VK_ESCAPE" oncommand="window.close();" />
+ </keyset>
+
+ <stringbundle
+ id="bundle_newsblog"
+ src="chrome://messenger-newsblog/locale/newsblog.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+
+ <vbox flex="1" id="contentPane">
+ <hbox pack="end">
+ <label
+ is="text-link"
+ id="learnMore"
+ crop="end"
+ value="&learnMore.label;"
+ href="https://support.mozilla.org/kb/how-subscribe-news-feeds-and-blogs"
+ />
+ </hbox>
+
+ <tree
+ id="rssSubscriptionsList"
+ treelines="true"
+ flex="1"
+ hidecolumnpicker="true"
+ onselect="FeedSubscriptions.onSelect();"
+ seltype="single"
+ >
+ <treecols>
+ <treecol id="folderNameCol" primary="true" hideheader="true" />
+ </treecols>
+ <treechildren
+ id="subscriptionChildren"
+ ondragstart="FeedSubscriptions.onDragStart(event);"
+ ondragover="FeedSubscriptions.onDragOver(event);"
+ />
+ </tree>
+
+ <hbox id="rssFeedInfoBox">
+ <vbox flex="1">
+ <hbox flex="1">
+ <vbox pack="end">
+ <hbox flex="1" align="center">
+ <label
+ id="nameLabel"
+ accesskey="&feedTitle.accesskey;"
+ control="nameValue"
+ value="&feedTitle.label;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="locationLabel"
+ accesskey="&feedLocation.accesskey;"
+ control="locationValue"
+ value="&feedLocation.label;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="feedFolderLabel"
+ value="&feedFolder.label;"
+ accesskey="&feedFolder.accesskey;"
+ control="selectFolder"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <html:input
+ id="nameValue"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ onchange="FeedSubscriptions.setPrefs(this);"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="locationValue"
+ type="url"
+ class="uri-element input-inline"
+ aria-labelledby="locationLabel"
+ placeholder="&feedLocation2.placeholder;"
+ onchange="FeedSubscriptions.setPrefs(this);"
+ onfocus="FeedSubscriptions.onFocusChange();"
+ onblur="FeedSubscriptions.onFocusChange();"
+ />
+ <hbox align="center">
+ <label
+ is="text-link"
+ id="locationValidate"
+ collapsed="true"
+ crop="end"
+ value="&locationValidate.label;"
+ onclick="FeedSubscriptions.checkValidation(event);"
+ />
+ </hbox>
+ </hbox>
+ <hbox class="input-container">
+ <menulist
+ id="selectFolder"
+ flex="1"
+ class="folderMenuItem"
+ hidden="true"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="selectFolderPopup"
+ class="menulist-menupopup"
+ mode="feeds"
+ showFileHereLabel="true"
+ showAccountsFileHere="true"
+ oncommand="FeedSubscriptions.setNewFolder(event);"
+ />
+ </menulist>
+ <html:input
+ id="selectFolderValue"
+ class="input-inline"
+ readonly="readonly"
+ aria-labelledby="feedFolderLabel"
+ onkeypress="FeedSubscriptions.onClickSelectFolderValue(event);"
+ onclick="FeedSubscriptions.onClickSelectFolderValue(event);"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ id="updateEnabled"
+ label="&biffStart.label;"
+ accesskey="&biffStart.accesskey;"
+ oncommand="FeedSubscriptions.setPrefs(this);"
+ />
+ <html:input
+ id="updateValue"
+ type="number"
+ class="size3 input-inline"
+ min="1"
+ aria-labelledby="updateEnabled updateValue biffMinutes biffDays recommendedUnits recommendedUnitsVal"
+ oninput="FeedSubscriptions.setPrefs(this);"
+ onchange="FeedSubscriptions.setPrefs(this);"
+ />
+ <radiogroup
+ id="biffUnits"
+ orient="horizontal"
+ oncommand="FeedSubscriptions.setPrefs(this);"
+ >
+ <radio
+ id="biffMinutes"
+ value="min"
+ label="&biffMinutes.label;"
+ accesskey="&biffMinutes.accesskey;"
+ />
+ <radio
+ id="biffDays"
+ value="d"
+ label="&biffDays.label;"
+ accesskey="&biffDays.accesskey;"
+ />
+ </radiogroup>
+ <hbox id="recommendedBox">
+ <label
+ id="recommendedUnits"
+ value="&recommendedUnits.label;"
+ hidden="true"
+ control="updateMinutes"
+ />
+ <label
+ id="recommendedUnitsVal"
+ value=""
+ hidden="true"
+ control="updateMinutes"
+ />
+ </hbox>
+ </hbox>
+ <checkbox
+ id="quickMode"
+ accesskey="&quickMode.accesskey;"
+ label="&quickMode.label;"
+ oncommand="FeedSubscriptions.setSummary(this.checked);"
+ />
+ <checkbox
+ id="autotagEnable"
+ accesskey="&autotagEnable.accesskey;"
+ label="&autotagEnable.label;"
+ oncommand="FeedSubscriptions.setPrefs(this);"
+ />
+ <hbox class="input-container">
+ <checkbox
+ id="autotagUsePrefix"
+ class="indent"
+ accesskey="&autotagUsePrefix.accesskey;"
+ label="&autotagUsePrefix.label;"
+ oncommand="FeedSubscriptions.setPrefs(this);"
+ />
+ <html:input
+ id="autotagPrefix"
+ type="text"
+ class="input-inline"
+ placeholder="&autoTagPrefix.placeholder;"
+ onchange="FeedSubscriptions.setPrefs(this);"
+ />
+ </hbox>
+ <separator class="thin" />
+ </vbox>
+ </hbox>
+
+ <hbox id="statusContainerBox" align="center">
+ <vbox flex="1">
+ <description id="statusText" />
+ </vbox>
+ <spacer flex="1" />
+ <label
+ id="validationText"
+ collapsed="true"
+ class="text-link"
+ crop="end"
+ value="&validateText.label;"
+ onclick="FeedSubscriptions.checkValidation(event);"
+ />
+ <button
+ id="addCertException"
+ collapsed="true"
+ data-l10n-id="certmgr-add-exception"
+ oncommand="FeedSubscriptions.addCertExceptionDialog();"
+ />
+ <html:progress
+ id="progressMeter"
+ hidden="hidden"
+ value="0"
+ max="100"
+ />
+ </hbox>
+
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <button
+ id="addFeed"
+ hidden="true"
+ disabled="true"
+ label="&button.addFeed.label;"
+ accesskey="&button.addFeed.accesskey;"
+ oncommand="FeedSubscriptions.addFeed();"
+ />
+
+ <button
+ id="updateFeed"
+ hidden="true"
+ disabled="true"
+ label="&button.verifyFeed.label;"
+ accesskey="&button.verifyFeed.accesskey;"
+ verifylabel="&button.verifyFeed.label;"
+ verifyaccesskey="&button.verifyFeed.accesskey;"
+ updatelabel="&button.updateFeed.label;"
+ updateaccesskey="&button.updateFeed.accesskey;"
+ oncommand="FeedSubscriptions.updateFeed();"
+ />
+
+ <button
+ id="removeFeed"
+ hidden="true"
+ label="&button.removeFeed.label;"
+ accesskey="&button.removeFeed.accesskey;"
+ oncommand="FeedSubscriptions.removeFeed(true);"
+ />
+
+ <spacer flex="1" />
+
+ <button
+ id="importOPML"
+ hidden="true"
+ label="&button.importOPML.label;"
+ accesskey="&button.importOPML.accesskey;"
+ oncommand="FeedSubscriptions.importOPML();"
+ />
+
+ <button
+ id="exportOPML"
+ hidden="true"
+ label="&button.exportOPML.label;"
+ accesskey="&button.exportOPML.accesskey;"
+ tooltiptext="&button.exportOPML.tooltip;"
+ oncommand="FeedSubscriptions.exportOPML(event);"
+ />
+ </hbox>
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/newsblog/feedAccountWizard.js b/comm/mailnews/extensions/newsblog/feedAccountWizard.js
new file mode 100644
index 0000000000..686caff3a3
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/feedAccountWizard.js
@@ -0,0 +1,56 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+
+window.addEventListener("DOMContentLoaded", () => {
+ FeedAccountWizard.onLoad();
+});
+
+/** Feed account standalone wizard functions. */
+var FeedAccountWizard = {
+ accountName: "",
+
+ onLoad() {
+ document
+ .querySelector("wizard")
+ .addEventListener("wizardfinish", this.onFinish.bind(this));
+ let accountSetupPage = document.getElementById("accountsetuppage");
+ accountSetupPage.addEventListener(
+ "pageshow",
+ this.accountSetupPageValidate.bind(this)
+ );
+ accountSetupPage.addEventListener(
+ "pagehide",
+ this.accountSetupPageValidate.bind(this)
+ );
+ let donePage = document.getElementById("done");
+ donePage.addEventListener("pageshow", this.donePageInit.bind(this));
+ },
+
+ accountSetupPageValidate() {
+ this.accountName = document.getElementById("prettyName").value.trim();
+ document.querySelector("wizard").canAdvance = this.accountName;
+ },
+
+ donePageInit() {
+ document.getElementById("account.name.text").value = this.accountName;
+ },
+
+ onFinish() {
+ let account = FeedUtils.createRssAccount(this.accountName);
+ let openerWindow = window.opener.top;
+ // The following block is the same as in AccountWizard.js.
+ if ("selectServer" in openerWindow) {
+ // Opened from Account Settings.
+ openerWindow.selectServer(account.incomingServer);
+ }
+
+ // Post a message to the main window on successful account setup.
+ openerWindow.postMessage("account-created", "*");
+
+ window.close();
+ },
+};
diff --git a/comm/mailnews/extensions/newsblog/feedAccountWizard.xhtml b/comm/mailnews/extensions/newsblog/feedAccountWizard.xhtml
new file mode 100644
index 0000000000..ef1be03dd7
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/feedAccountWizard.xhtml
@@ -0,0 +1,95 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountWizard.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % accountDTD SYSTEM "chrome://messenger/locale/AccountWizard.dtd">
+%accountDTD;
+<!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd" >
+%newsblogDTD;
+<!ENTITY % imDTD SYSTEM "chrome://messenger/locale/imAccountWizard.dtd" >
+%imDTD; ]>
+
+<html
+ id="FeedAccountWizard"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&feedWindowTitle.label;</title>
+ <link rel="localization" href="toolkit/global/wizard.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-newsblog/content/feedAccountWizard.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <wizard>
+ <!-- Account setup page : User gets a choice to enter a name for the account -->
+ <!-- Defaults : Feed account name -> default string -->
+ <wizardpage
+ id="accountsetuppage"
+ pageid="accountsetuppage"
+ label="&accnameTitle.label;"
+ >
+ <vbox flex="1">
+ <description>&accnameDesc.label;</description>
+ <separator class="thin" />
+ <hbox align="center" class="input-container">
+ <label
+ id="prettyNameLabel"
+ class="label"
+ value="&accnameLabel.label;"
+ accesskey="&accnameLabel.accesskey;"
+ control="prettyName"
+ />
+ <html:input
+ id="prettyName"
+ type="text"
+ class="input-inline"
+ value="&feeds.accountName;"
+ aria-labelledby="prettyNameLabel"
+ oninput="FeedAccountWizard.accountSetupPageValidate();"
+ />
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Done page : Summarizes information collected to create a feed account -->
+ <wizardpage id="done" pageid="done" label="&accountSummaryTitle.label;">
+ <vbox flex="1">
+ <description>&accountSummaryInfo.label;</description>
+ <separator class="thin" />
+ <hbox id="account.name" align="center">
+ <label
+ id="account.name.label"
+ class="label"
+ value="&accnameLabel.label;"
+ />
+ <label id="account.name.text" class="label" />
+ </hbox>
+ <separator />
+ <spacer flex="1" />
+ </vbox>
+ </wizardpage>
+ </wizard>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/newsblog/jar.mn b/comm/mailnews/extensions/newsblog/jar.mn
new file mode 100644
index 0000000000..46d1a700cd
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/jar.mn
@@ -0,0 +1,13 @@
+# 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/.
+
+newsblog.jar:
+% content messenger-newsblog %content/messenger-newsblog/
+ content/messenger-newsblog/newsblogOverlay.js (newsblogOverlay.js)
+ content/messenger-newsblog/feed-subscriptions.js (feed-subscriptions.js)
+ content/messenger-newsblog/feed-subscriptions.xhtml (feed-subscriptions.xhtml)
+ content/messenger-newsblog/am-newsblog.js (am-newsblog.js)
+ content/messenger-newsblog/am-newsblog.xhtml (am-newsblog.xhtml)
+ content/messenger-newsblog/feedAccountWizard.js (feedAccountWizard.js)
+ content/messenger-newsblog/feedAccountWizard.xhtml (feedAccountWizard.xhtml)
diff --git a/comm/mailnews/extensions/newsblog/moz.build b/comm/mailnews/extensions/newsblog/moz.build
new file mode 100644
index 0000000000..f4a9d0dd9f
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/moz.build
@@ -0,0 +1,25 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "Feed.jsm",
+ "FeedItem.jsm",
+ "FeedParser.jsm",
+ "FeedUtils.jsm",
+ "NewsBlog.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+]
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/unit/xpcshell.ini",
+]
diff --git a/comm/mailnews/extensions/newsblog/newsblogOverlay.js b/comm/mailnews/extensions/newsblog/newsblogOverlay.js
new file mode 100644
index 0000000000..9145ad0dab
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/newsblogOverlay.js
@@ -0,0 +1,416 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+/* globals ReloadMessage, getMessagePaneBrowser, openContentTab,
+ GetNumSelectedMessages, gMessageNotificationBar */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+var { MailE10SUtils } = ChromeUtils.import(
+ "resource:///modules/MailE10SUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ MsgHdrToMimeMessage: "resource:///modules/gloda/MimeMessage.jsm",
+});
+
+// This global is for SeaMonkey compatibility.
+var gShowFeedSummary;
+
+var FeedMessageHandler = {
+ gShowSummary: true,
+ gToggle: false,
+ kSelectOverrideWebPage: 0,
+ kSelectOverrideSummary: 1,
+ kSelectFeedDefault: 2,
+ kOpenWebPage: 0,
+ kOpenSummary: 1,
+ kOpenToggleInMessagePane: 2,
+ kOpenLoadInBrowser: 3,
+
+ FeedAccountTypes: ["rss"],
+
+ /**
+ * How to load message on threadpane select.
+ */
+ get onSelectPref() {
+ return Services.prefs.getIntPref("rss.show.summary");
+ },
+
+ set onSelectPref(val) {
+ Services.prefs.setIntPref("rss.show.summary", val);
+ ReloadMessage();
+ },
+
+ /**
+ * Load web page on threadpane select.
+ */
+ get loadWebPageOnSelectPref() {
+ return Services.prefs.getIntPref("rss.message.loadWebPageOnSelect");
+ },
+
+ /**
+ * How to load message on open (enter/dbl click in threadpane, contextmenu).
+ */
+ get onOpenPref() {
+ return Services.prefs.getIntPref("rss.show.content-base");
+ },
+
+ set onOpenPref(val) {
+ Services.prefs.setIntPref("rss.show.content-base", val);
+ },
+
+ /**
+ * Determine whether to show a feed message summary or load a web page in the
+ * message pane.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The message.
+ * @param {boolean} aToggle - true if in toggle mode, false otherwise.
+ *
+ * @returns {Boolean} - true if summary is to be displayed, false if web page.
+ */
+ shouldShowSummary(aMsgHdr, aToggle) {
+ // Not a feed message, always show summary (the message).
+ if (!FeedUtils.isFeedMessage(aMsgHdr)) {
+ return true;
+ }
+
+ // Notified of a summary reload when toggling, reset toggle and return.
+ if (!aToggle && this.gToggle) {
+ return !(this.gToggle = false);
+ }
+
+ let showSummary = true;
+ this.gToggle = aToggle;
+
+ // Thunderbird 2 rss messages with 'Show article summary' not selected,
+ // ie message body constructed to show web page in an iframe, can't show
+ // a summary - notify user.
+ let browser = getMessagePaneBrowser();
+ let contentDoc = browser ? browser.contentDocument : null;
+ let rssIframe = contentDoc
+ ? contentDoc.getElementById("_mailrssiframe")
+ : null;
+ if (rssIframe) {
+ if (this.gToggle || this.onSelectPref == this.kSelectOverrideSummary) {
+ this.gToggle = false;
+ }
+
+ return false;
+ }
+
+ if (aToggle) {
+ // Toggle mode, flip value.
+ return (gShowFeedSummary = this.gShowSummary = !this.gShowSummary);
+ }
+
+ let wintype = document.documentElement.getAttribute("windowtype");
+ let tabMail = document.getElementById("tabmail");
+ let messageTab = tabMail && tabMail.currentTabInfo.mode.type == "message";
+ let messageWindow = wintype == "mail:messageWindow";
+
+ switch (this.onSelectPref) {
+ case this.kSelectOverrideWebPage:
+ showSummary = false;
+ break;
+ case this.kSelectOverrideSummary:
+ showSummary = true;
+ break;
+ case this.kSelectFeedDefault:
+ // Get quickmode per feed folder pref from feed subscriptions. If the feed
+ // message is not in a feed account folder (hence the folder is not in
+ // the feeds database), err on the side of showing the summary.
+ // For the former, toggle or global override is necessary; for the
+ // latter, a show summary checkbox toggle in Subscribe dialog will set
+ // one on the path to bliss.
+ let folder = aMsgHdr.folder;
+ showSummary = true;
+ const ds = FeedUtils.getSubscriptionsDS(folder.server);
+ for (let sub of ds.data) {
+ if (sub.destFolder == folder.URI) {
+ showSummary = sub.quickMode;
+ break;
+ }
+ }
+ break;
+ }
+
+ gShowFeedSummary = this.gShowSummary = showSummary;
+
+ if (messageWindow || messageTab) {
+ // Message opened in either standalone window or tab, due to either
+ // message open pref (we are here only if the pref is 0 or 1) or
+ // contextmenu open.
+ switch (this.onOpenPref) {
+ case this.kOpenToggleInMessagePane:
+ // Opened by contextmenu, use the value derived above.
+ // XXX: allow a toggle via crtl?
+ break;
+ case this.kOpenWebPage:
+ showSummary = false;
+ break;
+ case this.kOpenSummary:
+ showSummary = true;
+ break;
+ }
+ }
+
+ // Auto load web page in browser on select, per pref; shouldShowSummary() is
+ // always called first to 1)test if feed, 2)get summary pref, so do it here.
+ if (this.loadWebPageOnSelectPref) {
+ setTimeout(FeedMessageHandler.loadWebPage, 20, aMsgHdr, {
+ browser: true,
+ });
+ }
+
+ return showSummary;
+ },
+
+ /**
+ * Load a web page for feed messages. Use MsgHdrToMimeMessage() to get
+ * the content-base url from the message headers. We cannot rely on
+ * currentHeaderData; it has not yet been streamed at our entry point in
+ * displayMessageChanged(), and in the case of a collapsed message pane it
+ * is not streamed.
+ *
+ * @param {nsIMsgDBHdr} aMessageHdr - The message.
+ * @param {Object} aWhere - name value=true pair, where name is in:
+ * 'messagepane', 'browser', 'tab', 'window'.
+ * @returns {void}
+ */
+ loadWebPage(aMessageHdr, aWhere) {
+ MsgHdrToMimeMessage(aMessageHdr, null, function (aMsgHdr, aMimeMsg) {
+ if (
+ aMimeMsg &&
+ aMimeMsg.headers["content-base"] &&
+ aMimeMsg.headers["content-base"][0]
+ ) {
+ let url = aMimeMsg.headers["content-base"],
+ uri;
+ try {
+ // The message and headers are stored as a string of UTF-8 bytes
+ // and we need to convert that cpp |string| to js UTF-16 explicitly
+ // for idn and non-ascii urls with this api.
+ url = decodeURIComponent(escape(url));
+ uri = Services.io.newURI(url);
+ } catch (ex) {
+ FeedUtils.log.info(
+ "FeedMessageHandler.loadWebPage: " +
+ "invalid Content-Base header url - " +
+ url
+ );
+ return;
+ }
+ if (aWhere.browser) {
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(uri);
+ } else if (aWhere.messagepane) {
+ let browser = getMessagePaneBrowser();
+ // Load about:blank in the browser before (potentially) switching
+ // to a remote process. This prevents sandbox flags being carried
+ // over to the web document.
+ MailE10SUtils.loadAboutBlank(browser);
+ MailE10SUtils.loadURI(browser, url);
+ } else if (aWhere.tab) {
+ openContentTab(url, "tab", null);
+ } else if (aWhere.window) {
+ openContentTab(url, "window", null);
+ }
+ } else {
+ FeedUtils.log.info(
+ "FeedMessageHandler.loadWebPage: could not get " +
+ "Content-Base header url for this message"
+ );
+ }
+ });
+ },
+
+ /**
+ * Display summary or load web page for feed messages. Caller should already
+ * know if the message is a feed message.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The message.
+ * @param {Boolean} aShowSummary - true if summary is to be displayed,
+ * false if web page.
+ * @returns {void}
+ */
+ setContent(aMsgHdr, aShowSummary) {
+ if (aShowSummary) {
+ // Only here if toggling to summary in 3pane.
+ if (this.gToggle && window.gDBView && GetNumSelectedMessages() == 1) {
+ ReloadMessage();
+ }
+ } else {
+ let browser = getMessagePaneBrowser();
+ if (browser && browser.contentDocument && browser.contentDocument.body) {
+ browser.contentDocument.body.hidden = true;
+ }
+ // If in a non rss folder, hide possible remote content bar on a web
+ // page load, as it doesn't apply.
+ gMessageNotificationBar.clearMsgNotifications();
+
+ this.loadWebPage(aMsgHdr, { messagepane: true });
+ this.gToggle = false;
+ }
+ },
+};
+
+function openSubscriptionsDialog(aFolder) {
+ // Check for an existing feed subscriptions window and focus it.
+ let subscriptionsWindow = Services.wm.getMostRecentWindow(
+ "Mail:News-BlogSubscriptions"
+ );
+
+ if (subscriptionsWindow) {
+ if (aFolder) {
+ subscriptionsWindow.FeedSubscriptions.selectFolder(aFolder);
+ subscriptionsWindow.FeedSubscriptions.mView.tree.ensureRowIsVisible(
+ subscriptionsWindow.FeedSubscriptions.mView.selection.currentIndex
+ );
+ }
+
+ subscriptionsWindow.focus();
+ } else {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger-newsblog/content/feed-subscriptions.xhtml",
+ "",
+ "centerscreen,chrome,dialog=no,resizable",
+ { folder: aFolder }
+ );
+ }
+}
+
+// Special case attempts to reply/forward/edit as new RSS articles. For
+// messages stored prior to Tb15, we are here only if the message's folder's
+// account server is rss and feed messages moved to other types will have their
+// summaries loaded, as viewing web pages only happened in an rss account.
+// The user may choose whether to load a summary or web page link by ensuring
+// the current feed message is being viewed as either a summary or web page.
+function openComposeWindowForRSSArticle(
+ aMsgComposeWindow,
+ aMsgHdr,
+ aMessageUri,
+ aType,
+ aFormat,
+ aIdentity,
+ aMsgWindow
+) {
+ // Ensure right content is handled for web pages in window/tab.
+ let tabmail = document.getElementById("tabmail");
+ let is3pane =
+ tabmail && tabmail.selectedTab && tabmail.selectedTab.mode
+ ? tabmail.selectedTab.mode.type == "folder"
+ : false;
+ let showingwebpage =
+ "FeedMessageHandler" in window &&
+ !is3pane &&
+ FeedMessageHandler.onOpenPref == FeedMessageHandler.kOpenWebPage;
+
+ if (gShowFeedSummary && !showingwebpage) {
+ // The user is viewing the summary.
+ MailServices.compose.OpenComposeWindow(
+ aMsgComposeWindow,
+ aMsgHdr,
+ aMessageUri,
+ aType,
+ aFormat,
+ aIdentity,
+ null,
+ aMsgWindow
+ );
+ } else {
+ // Set up the compose message and get the feed message's web page link.
+ let msgHdr = aMsgHdr;
+ let type = aType;
+ let msgComposeType = Ci.nsIMsgCompType;
+ let subject = msgHdr.mime2DecodedSubject;
+ let fwdPrefix = Services.prefs.getCharPref("mail.forward_subject_prefix");
+ fwdPrefix = fwdPrefix ? fwdPrefix + ": " : "";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+
+ let composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ if (
+ type == msgComposeType.Reply ||
+ type == msgComposeType.ReplyAll ||
+ type == msgComposeType.ReplyToSender ||
+ type == msgComposeType.ReplyToGroup ||
+ type == msgComposeType.ReplyToSenderAndGroup ||
+ type == msgComposeType.ReplyToList
+ ) {
+ subject = "Re: " + subject;
+ } else if (
+ type == msgComposeType.ForwardInline ||
+ type == msgComposeType.ForwardAsAttachment
+ ) {
+ subject = fwdPrefix + subject;
+ }
+
+ params.composeFields = composeFields;
+ params.composeFields.subject = subject;
+ params.composeFields.body = "";
+ params.bodyIsLink = false;
+ params.identity = aIdentity;
+
+ try {
+ // The feed's web page url is stored in the Content-Base header.
+ MsgHdrToMimeMessage(
+ msgHdr,
+ null,
+ function (aMsgHdr, aMimeMsg) {
+ if (
+ aMimeMsg &&
+ aMimeMsg.headers["content-base"] &&
+ aMimeMsg.headers["content-base"][0]
+ ) {
+ let url = decodeURIComponent(
+ escape(aMimeMsg.headers["content-base"])
+ );
+ params.composeFields.body = url;
+ params.bodyIsLink = true;
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ } else {
+ // No content-base url, use the summary.
+ MailServices.compose.OpenComposeWindow(
+ aMsgComposeWindow,
+ aMsgHdr,
+ aMessageUri,
+ aType,
+ aFormat,
+ aIdentity,
+ null,
+ aMsgWindow
+ );
+ }
+ },
+ false,
+ { saneBodySize: true }
+ );
+ } catch (ex) {
+ // Error getting header, use the summary.
+ MailServices.compose.OpenComposeWindow(
+ aMsgComposeWindow,
+ aMsgHdr,
+ aMessageUri,
+ aType,
+ aFormat,
+ aIdentity,
+ null,
+ aMsgWindow
+ );
+ }
+ }
+}
diff --git a/comm/mailnews/extensions/newsblog/test/browser/browser.ini b/comm/mailnews/extensions/newsblog/test/browser/browser.ini
new file mode 100644
index 0000000000..1abed389c7
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/browser/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+prefs =
+ datareporting.policy.dataSubmissionPolicyBypassNotification=true
+ mail.account.account1.server=server1
+ mail.accountmanager.accounts=account1
+ mail.provider.suppress_dialog_on_startup=true
+ mail.server.server1.hostname=Feeds
+ mail.server.server1.name=Feeds
+ mail.server.server1.type=rss
+ mail.server.server1.userName=nobody
+ mail.spellcheck.inline=false
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.database.global.indexer.enabled=false
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+support-files = data/**
+
+[browser_feedDisplay.js]
diff --git a/comm/mailnews/extensions/newsblog/test/browser/browser_feedDisplay.js b/comm/mailnews/extensions/newsblog/test/browser/browser_feedDisplay.js
new file mode 100644
index 0000000000..98fa755c46
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/browser/browser_feedDisplay.js
@@ -0,0 +1,228 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+add_task(async () => {
+ function folderTreeClick(row, event = {}) {
+ EventUtils.synthesizeMouseAtCenter(
+ folderTree.rows[row].querySelector(".name"),
+ event,
+ about3Pane
+ );
+ }
+ function threadTreeClick(row, event = {}) {
+ EventUtils.synthesizeMouseAtCenter(
+ threadTree.getRowAtIndex(row),
+ event,
+ about3Pane
+ );
+ }
+
+ /** @implements {nsIExternalProtocolService} */
+ let mockExternalProtocolService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+ _loadedURLs: [],
+ loadURI(uri, windowContext) {
+ this._loadedURLs.push(uri.spec);
+ },
+ isExposedProtocol(scheme) {
+ return true;
+ },
+ urlLoaded(url) {
+ return this._loadedURLs.includes(url);
+ },
+ };
+
+ let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+ );
+
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = about3Pane;
+ });
+
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ let { folderTree, threadTree, messageBrowser } = about3Pane;
+ let menu = about3Pane.document.getElementById("folderPaneContext");
+ let menuItem = about3Pane.document.getElementById(
+ "folderPaneContext-subscribe"
+ );
+ // Not `currentAboutMessage` as that's null right now.
+ let aboutMessage = messageBrowser.contentWindow;
+ let messagePane = aboutMessage.getMessagePaneBrowser();
+
+ let account = MailServices.accounts.getAccount("account1");
+ let rootFolder = account.incomingServer.rootFolder;
+ about3Pane.displayFolder(rootFolder.URI);
+ let index = about3Pane.folderTree.selectedIndex;
+ Assert.equal(index, 0);
+
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ folderTreeClick(index, { type: "contextmenu" });
+ await shownPromise;
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog(
+ null,
+ "chrome://messenger-newsblog/content/feed-subscriptions.xhtml",
+ {
+ async callback(dialogWindow) {
+ let dialogDocument = dialogWindow.document;
+
+ let list = dialogDocument.getElementById("rssSubscriptionsList");
+ let locationInput = dialogDocument.getElementById("locationValue");
+ let addFeedButton = dialogDocument.getElementById("addFeed");
+
+ await BrowserTestUtils.waitForEvent(list, "select");
+
+ EventUtils.synthesizeMouseAtCenter(locationInput, {}, dialogWindow);
+ await TestUtils.waitForCondition(() => !addFeedButton.disabled);
+ EventUtils.sendString(
+ "https://example.org/browser/comm/mailnews/extensions/newsblog/test/browser/data/rss.xml",
+ dialogWindow
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, dialogWindow);
+
+ // There's no good way to know if we're ready to continue.
+ await new Promise(r => dialogWindow.setTimeout(r, 250));
+
+ let hiddenPromise = BrowserTestUtils.waitForAttribute(
+ "hidden",
+ addFeedButton,
+ "true"
+ );
+ EventUtils.synthesizeMouseAtCenter(addFeedButton, {}, dialogWindow);
+ await hiddenPromise;
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.querySelector("dialog").getButton("accept"),
+ {},
+ dialogWindow
+ );
+ },
+ }
+ );
+ menu.activateItem(menuItem);
+ await Promise.all([hiddenPromise, dialogPromise]);
+
+ let folder = rootFolder.subFolders.find(f => f.name == "Test Feed");
+ Assert.ok(folder);
+
+ about3Pane.displayFolder(folder.URI);
+ index = folderTree.selectedIndex;
+ Assert.equal(about3Pane.threadTree.view.rowCount, 1);
+
+ // Description mode.
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(messagePane);
+ threadTreeClick(0);
+ await loadedPromise;
+
+ Assert.notEqual(messagePane.currentURI.spec, "about:blank");
+ await SpecialPowers.spawn(messagePane, [], () => {
+ let doc = content.document;
+
+ let p = doc.querySelector("p");
+ Assert.equal(p.textContent, "This is the description.");
+
+ let style = content.getComputedStyle(doc.body);
+ Assert.equal(style.backgroundColor, "rgba(0, 0, 0, 0)");
+
+ let noscript = doc.querySelector("noscript");
+ style = content.getComputedStyle(noscript);
+ Assert.equal(style.display, "inline");
+ });
+
+ Assert.ok(
+ aboutMessage.document.getElementById("expandedtoRow").hidden,
+ "The To field is not visible"
+ );
+ Assert.equal(
+ aboutMessage.document.getElementById("dateLabel").textContent,
+ aboutMessage.document.getElementById("dateLabelSubject").textContent,
+ "The regular date label and the subject date have the same value"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ aboutMessage.document.getElementById("dateLabel"),
+ "The regular date label is not visible"
+ )
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(
+ aboutMessage.document.getElementById("dateLabelSubject")
+ ),
+ "The date label on the subject line is visible"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("a", {}, messagePane);
+ Assert.deepEqual(mockExternalProtocolService._loadedURLs, [
+ "https://example.org/link/from/description",
+ ]);
+ mockExternalProtocolService._loadedURLs.length = 0;
+
+ // Web mode.
+
+ loadedPromise = BrowserTestUtils.browserLoaded(
+ messagePane,
+ false,
+ "https://example.org/browser/comm/mailnews/extensions/newsblog/test/browser/data/article.html"
+ );
+ window.FeedMessageHandler.onSelectPref = 0;
+ await loadedPromise;
+
+ await SpecialPowers.spawn(messagePane, [], () => {
+ let doc = content.document;
+
+ let p = doc.querySelector("p");
+ Assert.equal(p.textContent, "This is the article.");
+
+ let style = content.getComputedStyle(doc.body);
+ Assert.equal(style.backgroundColor, "rgb(0, 128, 0)");
+
+ let noscript = doc.querySelector("noscript");
+ style = content.getComputedStyle(noscript);
+ Assert.equal(style.display, "none");
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter("a", {}, messagePane);
+ Assert.deepEqual(mockExternalProtocolService._loadedURLs, [
+ "https://example.org/link/from/article",
+ ]);
+ mockExternalProtocolService._loadedURLs.length = 0;
+
+ // Clean up.
+
+ shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ about3Pane.folderTree.selectedRow,
+ { type: "contextmenu" },
+ about3Pane
+ );
+ await shownPromise;
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ menuItem = about3Pane.document.getElementById("folderPaneContext-remove");
+ menu.activateItem(menuItem);
+ await Promise.all([hiddenPromise, promptPromise]);
+
+ window.FeedMessageHandler.onSelectPref = 1;
+
+ folderTree.selectedIndex = 0;
+});
diff --git a/comm/mailnews/extensions/newsblog/test/browser/data/article.html b/comm/mailnews/extensions/newsblog/test/browser/data/article.html
new file mode 100644
index 0000000000..4c6b780c41
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/browser/data/article.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title></title>
+ </head>
+ <body>
+ <p>This is the article.</p>
+ <p><a href="https://example.org/link/from/article">Here's a link.</a></p>
+ <script>
+ document.body.style.backgroundColor = "green";
+ </script>
+ <noscript>
+ <p>This noscript should not display.</p>
+ </noscript>
+ </body>
+</html>
diff --git a/comm/mailnews/extensions/newsblog/test/browser/data/rss.xml b/comm/mailnews/extensions/newsblog/test/browser/data/rss.xml
new file mode 100644
index 0000000000..ec6aebccc1
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/browser/data/rss.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+ <channel>
+ <title>Test Feed</title>
+ <link>https://example.org/</link>
+ <description></description>
+ <lastBuildDate>Thu, 21 Jan 2021 17:57:54 +0000</lastBuildDate>
+ <language>en-US</language>
+
+ <item>
+ <title>Test Article</title>
+ <link>https://example.org/browser/comm/mailnews/extensions/newsblog/test/browser/data/article.html</link>
+ <pubDate>Wed, 20 Jan 2021 17:00:39 +0000</pubDate>
+
+ <description><![CDATA[
+ <p>This is the description.</p>
+ <p><a href="https://example.org/link/from/description">Here's a link.</a></p>
+ <script>
+ document.body.style.backgroundColor = "red";
+ </script>
+ <noscript>
+ <p>This noscript should display.</p>
+ </noscript>
+ ]]></description>
+ </item>
+ </channel>
+</rss>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/head_feeds.js b/comm/mailnews/extensions/newsblog/test/unit/head_feeds.js
new file mode 100644
index 0000000000..004e1acf68
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/head_feeds.js
@@ -0,0 +1,35 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+let { FeedParser } = ChromeUtils.import("resource:///modules/FeedParser.jsm");
+let { Feed } = ChromeUtils.import("resource:///modules/Feed.jsm");
+let { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// Set up local web server to serve up test files.
+// We run it on a random port so that other tests can run concurrently
+// even if they also run a web server.
+let httpServer = new HttpServer();
+httpServer.registerDirectory("/", do_get_file("resources"));
+httpServer.start(-1);
+const SERVER_PORT = httpServer.identity.primaryPort;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../../";
+
+registerCleanupFunction(async () => {
+ await httpServer.stop();
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/README.md b/comm/mailnews/extensions/newsblog/test/unit/resources/README.md
new file mode 100644
index 0000000000..df655769b0
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/README.md
@@ -0,0 +1,24 @@
+Data files for unit testing the feeds code.
+
+- `rss_7_1.rdf`
+ Simple RSS1.0 feed example, from:
+ https://www.w3.org/2000/10/rdf-tests/RSS_1.0/rss_7_1.rdf
+
+- `rss_7_1_BORKED.rdf`
+ Sabotaged version of `rss_7_1.rdf` with a bad
+ <items> list, pointing to all sorts of URLs not
+ represented as <item>s in the feed (see Bug 476641).
+
+- `rss2_example.xml`
+ RSS2.0 example from wikipedia, but with
+ Japanese text in the title, with leading/trailing
+ whitespace.
+
+- `rss2_guid.xml`
+ RSS2.0 feed where two items have the same link but different guid.
+ (they should both appear in the feed).
+
+- `feeds-*/`
+ Test cases for migrating legacy .rdf config files to
+ the new json ones. The .rdf files are the old data,
+ and the .json files are the expected results.
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.json
@@ -0,0 +1 @@
+{}
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.rdf
new file mode 100644
index 0000000000..81a5a62ec5
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeditems.rdf
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.json
@@ -0,0 +1 @@
+[]
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.rdf
new file mode 100644
index 0000000000..f7e6b400e2
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-bad/feeds.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NS1="http://purl.org/rss/1.0/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="urn:forumzilla:root">
+ <fz:feeds RDF:resource="rdf:#$cvA6q"/>
+ </RDF:Description>
+ <fz:feed RDF:about="https://example.com/feed/"
+ fz:quickMode="false"
+ dc:title="A feed with no dc:identifier, and thus no url. Should be ditched.">
+ </fz:feed>
+ <RDF:Seq RDF:about="rdf:#$cvA6q">
+ <RDF:li RDF:resource="https://example.com/feed/"/>
+ </RDF:Seq>
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.json
@@ -0,0 +1 @@
+{}
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.rdf
new file mode 100644
index 0000000000..81a5a62ec5
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeditems.rdf
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.json
@@ -0,0 +1 @@
+[]
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.rdf
new file mode 100644
index 0000000000..a5f9c997e8
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-empty/feeds.rdf
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NS1="http://purl.org/rss/1.0/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="urn:forumzilla:root">
+ <fz:feeds RDF:resource="rdf:#$cvA6q"/>
+ </RDF:Description>
+ <RDF:Seq RDF:about="rdf:#$cvA6q">
+ </RDF:Seq>
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.json
@@ -0,0 +1 @@
+{}
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.rdf
new file mode 100644
index 0000000000..81a5a62ec5
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeditems.rdf
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.json
new file mode 100644
index 0000000000..e20f7a2f00
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.json
@@ -0,0 +1,23 @@
+[
+ {
+ "title": "Government Digital Service",
+ "url": "https://gds.blog.gov.uk/feed/",
+ "quickMode": false,
+ "options": {
+ "version": 2,
+ "updates": {
+ "enabled": true,
+ "updateMinutes": 100,
+ "updateUnits": "min",
+ "lastUpdateTime": 1568784489107,
+ "lastDownloadTime": null,
+ "updatePeriod": "",
+ "updateFrequency": "",
+ "updateBase": ""
+ },
+ "category": { "enabled": false, "prefixEnabled": false, "prefix": "" }
+ },
+ "destFolder": "mailbox://nobody@Feeds/Government%20Digital%20Service",
+ "link": "https://gds.blog.gov.uk/feed/"
+ }
+]
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.rdf
new file mode 100644
index 0000000000..3b6b6b2df8
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-missing-timestamp/feeds.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NS1="http://purl.org/rss/1.0/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="urn:forumzilla:root">
+ <fz:feeds RDF:resource="rdf:#$cvA6q"/>
+ </RDF:Description>
+ <fz:feed RDF:about="https://gds.blog.gov.uk/feed/"
+ fz:quickMode="false"
+ dc:title="Government Digital Service"
+ NS1:link="https://gds.blog.gov.uk/feed/"
+ fz:options="{&quot;version&quot;:2,&quot;updates&quot;:{&quot;enabled&quot;:true,&quot;updateMinutes&quot;:100,&quot;updateUnits&quot;:&quot;min&quot;,&quot;lastUpdateTime&quot;:1568784489107,&quot;lastDownloadTime&quot;:null,&quot;updatePeriod&quot;:&quot;&quot;,&quot;updateFrequency&quot;:&quot;&quot;,&quot;updateBase&quot;:&quot;&quot;},&quot;category&quot;:{&quot;enabled&quot;:false,&quot;prefixEnabled&quot;:false,&quot;prefix&quot;:&quot;&quot;}}"
+ dc:identifier="https://gds.blog.gov.uk/feed/">
+ <fz:destFolder RDF:resource="mailbox://nobody@Feeds/Government%20Digital%20Service"/>
+ </fz:feed>
+ <RDF:Seq RDF:about="rdf:#$cvA6q">
+ <RDF:li RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Seq>
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.json
new file mode 100644
index 0000000000..a16c68f0c0
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.json
@@ -0,0 +1,122 @@
+{
+ "https://gds.blog.gov.uk/?p=32978": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488792,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32944": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488971,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=33011": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488610,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=33020": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488551,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16464": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520041,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32951": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488909,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32963": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488851,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16431": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520152,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16477": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784519983,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32939": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784489030,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16453": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520096,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16418": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520209,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16507": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784519869,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16490": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784519926,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16378": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520323,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32927": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784489089,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=33001": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488670,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16393": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784520265,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://civilservice.blog.gov.uk/?p=16514": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784519812,
+ "feedURLs": ["https://civilservice.blog.gov.uk/feed/"]
+ },
+ "https://gds.blog.gov.uk/?p=32988": {
+ "stored": true,
+ "valid": true,
+ "lastSeenTime": 1568784488731,
+ "feedURLs": ["https://gds.blog.gov.uk/feed/"]
+ }
+}
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.rdf
new file mode 100644
index 0000000000..f0941b5f6b
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeditems.rdf
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32978"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488792"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32944"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488971"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=33011"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488610"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=33020"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488551"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16464"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520041"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32951"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488909"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32963"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488851"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16431"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520152"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16477"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784519983"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32939"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784489030"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16453"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520096"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16418"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520209"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16507"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784519869"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16490"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784519926"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16378"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520323"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32927"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784489089"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=33001"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488670"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16393"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784520265"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fcivilservice.blog.gov.uk%2f%3fp=16514"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784519812"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:feeditem:https:%2f%2fgds.blog.gov.uk%2f%3fp=32988"
+ fz:stored="true"
+ fz:last-seen-timestamp="1568784488731"
+ fz:valid="true">
+ <fz:feed RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.json b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.json
new file mode 100644
index 0000000000..2ab7d4f780
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.json
@@ -0,0 +1,46 @@
+[
+ {
+ "title": "Government Digital Service",
+ "lastModified": "Wed, 11 Sep 2019 15:47:49 GMT",
+ "url": "https://gds.blog.gov.uk/feed/",
+ "quickMode": false,
+ "options": {
+ "version": 2,
+ "updates": {
+ "enabled": true,
+ "updateMinutes": 100,
+ "updateUnits": "min",
+ "lastUpdateTime": 1568784489107,
+ "lastDownloadTime": null,
+ "updatePeriod": "",
+ "updateFrequency": "",
+ "updateBase": ""
+ },
+ "category": { "enabled": false, "prefixEnabled": false, "prefix": "" }
+ },
+ "destFolder": "mailbox://nobody@Feeds/Government%20Digital%20Service",
+ "link": "https://gds.blog.gov.uk/feed/"
+ },
+ {
+ "title": "Civil Service",
+ "lastModified": "Tue, 17 Sep 2019 16:21:00 GMT",
+ "url": "https://civilservice.blog.gov.uk/feed/",
+ "quickMode": false,
+ "options": {
+ "version": 2,
+ "updates": {
+ "enabled": true,
+ "updateMinutes": 100,
+ "updateUnits": "min",
+ "lastUpdateTime": 1568784520338,
+ "lastDownloadTime": null,
+ "updatePeriod": "",
+ "updateFrequency": "",
+ "updateBase": ""
+ },
+ "category": { "enabled": false, "prefixEnabled": false, "prefix": "" }
+ },
+ "destFolder": "mailbox://nobody@Feeds/Civil%20Service",
+ "link": "https://civilservice.blog.gov.uk/feed/"
+ }
+]
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.rdf
new file mode 100644
index 0000000000..5c2fb72c74
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/feeds-simple/feeds.rdf
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NS1="http://purl.org/rss/1.0/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:fz="urn:forumzilla:"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="urn:forumzilla:root">
+ <fz:feeds RDF:resource="rdf:#$cvA6q"/>
+ </RDF:Description>
+ <fz:feed RDF:about="https://gds.blog.gov.uk/feed/"
+ fz:quickMode="false"
+ dc:title="Government Digital Service"
+ NS1:link="https://gds.blog.gov.uk/feed/"
+ dc:lastModified="Wed, 11 Sep 2019 15:47:49 GMT"
+ fz:options="{&quot;version&quot;:2,&quot;updates&quot;:{&quot;enabled&quot;:true,&quot;updateMinutes&quot;:100,&quot;updateUnits&quot;:&quot;min&quot;,&quot;lastUpdateTime&quot;:1568784489107,&quot;lastDownloadTime&quot;:null,&quot;updatePeriod&quot;:&quot;&quot;,&quot;updateFrequency&quot;:&quot;&quot;,&quot;updateBase&quot;:&quot;&quot;},&quot;category&quot;:{&quot;enabled&quot;:false,&quot;prefixEnabled&quot;:false,&quot;prefix&quot;:&quot;&quot;}}"
+ dc:identifier="https://gds.blog.gov.uk/feed/">
+ <fz:destFolder RDF:resource="mailbox://nobody@Feeds/Government%20Digital%20Service"/>
+ </fz:feed>
+ <fz:feed RDF:about="https://civilservice.blog.gov.uk/feed/"
+ fz:quickMode="false"
+ dc:title="Civil Service"
+ NS1:link="https://civilservice.blog.gov.uk/feed/"
+ dc:lastModified="Tue, 17 Sep 2019 16:21:00 GMT"
+ fz:options="{&quot;version&quot;:2,&quot;updates&quot;:{&quot;enabled&quot;:true,&quot;updateMinutes&quot;:100,&quot;updateUnits&quot;:&quot;min&quot;,&quot;lastUpdateTime&quot;:1568784520338,&quot;lastDownloadTime&quot;:null,&quot;updatePeriod&quot;:&quot;&quot;,&quot;updateFrequency&quot;:&quot;&quot;,&quot;updateBase&quot;:&quot;&quot;},&quot;category&quot;:{&quot;enabled&quot;:false,&quot;prefixEnabled&quot;:false,&quot;prefix&quot;:&quot;&quot;}}"
+ dc:identifier="https://civilservice.blog.gov.uk/feed/">
+ <fz:destFolder RDF:resource="mailbox://nobody@Feeds/Civil%20Service"/>
+ </fz:feed>
+ <RDF:Seq RDF:about="rdf:#$cvA6q">
+ <RDF:li RDF:resource="https://gds.blog.gov.uk/feed/"/>
+ <RDF:li RDF:resource="https://civilservice.blog.gov.uk/feed/"/>
+ </RDF:Seq>
+</RDF:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_example.xml b/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_example.xml
new file mode 100644
index 0000000000..0fe6268ec1
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_example.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0">
+<channel>
+ <title>
+
+ 本当ã«ç°¡å˜ãªã‚·ãƒ³ã‚¸ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã®ä¾‹
+
+ </title>
+ <description>This is an example of an RSS feed</description>
+ <link>http://www.example.com/main.html</link>
+ <lastBuildDate>Mon, 06 Sep 2010 00:01:00 +0000 </lastBuildDate>
+ <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
+ <ttl>1800</ttl>
+
+ <item>
+ <title>Example entry</title>
+ <description>Here is some text containing an interesting description.</description>
+ <link>http://www.example.com/blog/post/1</link>
+ <guid isPermaLink="false">7bd204c6-1655-4c27-aeee-53f933c5395f</guid>
+ <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
+ </item>
+
+</channel>
+</rss>
+
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_guid.xml b/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_guid.xml
new file mode 100644
index 0000000000..9f1efea9cc
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/rss2_guid.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0">
+<channel>
+ <title>GUID test</title>
+ <description>This RSS feed has multiple items with the same link, but with different guids. They should be treated as separate items (see Bug 1656090)</description>
+ <link>http://www.example.com/main.html</link>
+ <lastBuildDate>Mon, 06 Sep 2020 00:01:00 +0000 </lastBuildDate>
+ <pubDate>Sun, 06 Sep 2019 16:20:00 +0000</pubDate>
+ <ttl>1800</ttl>
+
+ <item>
+ <title>Entry One</title>
+ <description>Blah blah blah.</description>
+ <link>http://www.example.com/blog/post/1</link>
+ <guid isPermaLink="false">0524a046-df56-11ea-8bc9-47d63411283f</guid>
+ <pubDate>Sun, 06 Sep 2019 16:20:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Entry One Again(with same link but different guid!)</title>
+ <description>Blah blah blah.</description>
+ <link>http://www.example.com/blog/post/1</link>
+ <guid isPermaLink="false">0524a35c-df56-11ea-8bca-43f820f3bd93</guid>
+ <pubDate>Sun, 06 Sep 2019 16:20:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Entry Two</title>
+ <description>Blah blah blah.</description>
+ <link>http://www.example.com/blog/post/2</link>
+ <guid isPermaLink="false">0524a442-df56-11ea-8bcb-035a71f5f71a</guid>
+ <pubDate>Sun, 06 Sep 2019 16:20:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Entry Three</title>
+ <description>Blah blah blah.</description>
+ <link>http://www.example.com/blog/post/3</link>
+ <guid isPermaLink="false">0524a53c-df56-11ea-8bcc-8f0977d58351</guid>
+ <pubDate>Sun, 06 Sep 2019 16:20:00 +0000</pubDate>
+ </item>
+
+</channel>
+</rss>
+
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1.rdf
new file mode 100644
index 0000000000..179965e7de
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1.rdf
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+
+<!-- RDF Site Summary (RSS) 1.0
+ http://groups.yahoo.com/group/rss-dev/files/specification.html
+ Section 7
+ -->
+
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/">
+
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>XML.com</title>
+ <link>http://xml.com/pub</link>
+ <description>
+ XML.com features a rich mix of information and services
+ for the XML community.
+ </description>
+
+ <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" />
+
+ <items>
+ <rdf:Seq>
+ <rdf:li resource="http://xml.com/pub/2000/08/09/xslt/xslt.html" />
+ <rdf:li resource="http://xml.com/pub/2000/08/09/rdfdb/index.html" />
+ </rdf:Seq>
+ </items>
+
+ <textinput rdf:resource="http://search.xml.com" />
+
+ </channel>
+
+ <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
+ <title>XML.com</title>
+ <link>http://www.xml.com</link>
+ <url>http://xml.com/universal/images/xml_tiny.gif</url>
+ </image>
+
+ <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html">
+ <title>Processing Inclusions with XSLT</title>
+ <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>
+ <description>
+ Processing document inclusions with general XML tools can be
+ problematic. This article proposes a way of preserving inclusion
+ information through SAX-based processing.
+ </description>
+ </item>
+
+ <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html">
+ <title>Putting RDF to Work</title>
+ <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>
+ <description>
+ Tool and API support for the Resource Description Framework
+ is slowly coming of age. Edd Dumbill takes a look at RDFDB,
+ one of the most exciting new RDF toolkits.
+ </description>
+ </item>
+
+ <textinput rdf:about="http://search.xml.com">
+ <title>Search XML.com</title>
+ <description>Search XML.com's XML collection</description>
+ <name>s</name>
+ <link>http://search.xml.com</link>
+ </textinput>
+
+</rdf:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1_BORKED.rdf b/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1_BORKED.rdf
new file mode 100644
index 0000000000..e2c6e0b109
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/resources/rss_7_1_BORKED.rdf
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+
+<!-- RDF Site Summary (RSS) 1.0
+ http://groups.yahoo.com/group/rss-dev/files/specification.html
+ Section 7
+ -->
+
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/">
+
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>XML.com</title>
+ <link>http://xml.com/pub</link>
+ <description>
+ XML.com features a rich mix of information and services
+ for the XML community.
+ </description>
+
+ <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" />
+
+ <items>
+ <rdf:Seq>
+ <rdf:li resource="http://OBVIOUSLY_WRONG_URL.com/blah/blah.html" />
+ <rdf:li resource="http://ANOTHER_OBVIOUSLY_WRONG_URL.com/foo/bar/wibble.html" />
+ </rdf:Seq>
+ </items>
+
+ <textinput rdf:resource="http://search.xml.com" />
+
+ </channel>
+
+ <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
+ <title>XML.com</title>
+ <link>http://www.xml.com</link>
+ <url>http://xml.com/universal/images/xml_tiny.gif</url>
+ </image>
+
+ <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html">
+ <title>Processing Inclusions with XSLT</title>
+ <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>
+ <description>
+ Processing document inclusions with general XML tools can be
+ problematic. This article proposes a way of preserving inclusion
+ information through SAX-based processing.
+ </description>
+ </item>
+
+ <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html">
+ <title>Putting RDF to Work</title>
+ <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>
+ <description>
+ Tool and API support for the Resource Description Framework
+ is slowly coming of age. Edd Dumbill takes a look at RDFDB,
+ one of the most exciting new RDF toolkits.
+ </description>
+ </item>
+
+ <textinput rdf:about="http://search.xml.com">
+ <title>Search XML.com</title>
+ <description>Search XML.com's XML collection</description>
+ <name>s</name>
+ <link>http://search.xml.com</link>
+ </textinput>
+
+</rdf:RDF>
diff --git a/comm/mailnews/extensions/newsblog/test/unit/test_feedparser.js b/comm/mailnews/extensions/newsblog/test/unit/test_feedparser.js
new file mode 100644
index 0000000000..6577280356
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/test_feedparser.js
@@ -0,0 +1,146 @@
+// see https://www.w3.org/2000/10/rdf-tests/ for test files (including rss1)
+// - test examples for all feed types
+// - test items in test_download()
+// - test rss1 feed with itunes `new-feed-url` redirect
+// - test rss1 feed with RSS syndication extension tags (updatePeriod et al)
+// - test multiple/missing authors (with fallback to feed title)
+// - test missing dates
+// - test content formatting
+
+// Some RSS1 feeds in the wild:
+// https://www.livejournal.com/stats/latest-rss.bml
+// https://journals.sagepub.com/action/showFeed?ui=0&mi=ehikzz&ai=2b4&jc=acrc&type=etoc&feed=rss
+// https://www.revolutionspodcast.com/index.rdf
+// https://www.tandfonline.com/feed/rss/uasa20
+// http://export.arxiv.org/rss/astro-ph
+// - uses html formatting in <dc:creator>
+
+// Helper to compare feeditems.
+function assertItemsEqual(got, expected) {
+ Assert.equal(got.length, expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ // Only check fields in expected. Means testdata can exclude "description" and other bulky fields.
+ for (let k of Object.keys(expected[i])) {
+ Assert.equal(got[i][k], expected[i][k]);
+ }
+ }
+}
+
+// Test the rss1 feed parser
+add_task(async function test_rss1() {
+ // Boilerplate.
+ let account = FeedUtils.createRssAccount("test_rss1");
+ let rootFolder = account.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ let folder = rootFolder.createLocalSubfolder("folderofeeds");
+
+ // These two files yield the same feed, but the second one has a sabotaged
+ // <items> to simulate badly-encoded feeds seen in the wild.
+ for (let testFile of [
+ "resources/rss_7_1.rdf",
+ "resources/rss_7_1_BORKED.rdf",
+ ]) {
+ dump(`checking ${testFile}\n`);
+ // Would be nicer to use the test http server to fetch the file, but that
+ // would involve XMLHTTPRequest. This is more concise.
+ let doc = await do_parse_document(testFile, "application/xml");
+ let feed = new Feed(
+ "https://www.w3.org/2000/10/rdf-tests/RSS_1.0/rss_7_1.rdf",
+ folder
+ );
+ feed.parseItems = true; // We want items too, not just the feed details.
+ feed.onParseError = function (f) {
+ throw new Error("PARSE ERROR");
+ };
+ let parser = new FeedParser();
+ let items = parser.parseAsRSS1(feed, doc);
+
+ // Check some channel details.
+ Assert.equal(feed.title, "XML.com");
+ Assert.equal(feed.link, "http://xml.com/pub");
+
+ // Check the items (the titles and links at least!).
+ assertItemsEqual(items, [
+ {
+ url: "http://xml.com/pub/2000/08/09/xslt/xslt.html",
+ title: "Processing Inclusions with XSLT",
+ },
+ {
+ url: "http://xml.com/pub/2000/08/09/rdfdb/index.html",
+ title: "Putting RDF to Work",
+ },
+ ]);
+ }
+});
+
+// Test feed downloading.
+// Mainly checking that it doesn't crash and that the right feed parser is used.
+add_task(async function test_download() {
+ // Boilerplate
+ let account = FeedUtils.createRssAccount("test_feed_download");
+ let rootFolder = account.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+
+ // load & parse example rss feed
+ // Feed object rejects anything other than http and https, so we're
+ // running a local http server for testing (see head_feeds.js for it).
+ let feedTests = [
+ {
+ url: "http://localhost:" + SERVER_PORT + "/rss_7_1.rdf",
+ feedType: "RSS_1.xRDF",
+ title: "XML.com",
+ expectedItems: 2,
+ },
+ {
+ // Has Japanese title with leading/trailing whitespace.
+ url: "http://localhost:" + SERVER_PORT + "/rss2_example.xml",
+ feedType: "RSS_2.0",
+ title: "本当ã«ç°¡å˜ãªã‚·ãƒ³ã‚¸ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã®ä¾‹",
+ expectedItems: 1,
+ },
+ {
+ // Has two items with same link but different guid (Bug 1656090).
+ url: "http://localhost:" + SERVER_PORT + "/rss2_guid.xml",
+ feedType: "RSS_2.0",
+ title: "GUID test",
+ expectedItems: 4,
+ },
+ // TODO: examples for the other feed types!
+ ];
+
+ let n = 1;
+ for (let test of feedTests) {
+ let folder = rootFolder.createLocalSubfolder("feed" + n);
+ n++;
+ let feed = new Feed(test.url, folder);
+
+ let dl = new Promise(function (resolve, reject) {
+ let cb = {
+ downloaded(f, error, disable) {
+ if (error != FeedUtils.kNewsBlogSuccess) {
+ reject(
+ new Error(`download failed (url=${feed.url} error=${error})`)
+ );
+ return;
+ }
+ // Feed has downloaded - make sure the right type was detected.
+ Assert.equal(feed.mFeedType, test.feedType, "feed type matching");
+ Assert.equal(feed.title, test.title, "title matching");
+ // Make sure we're got the expected number of messages in the folder.
+ let cnt = [...folder.messages].length;
+ Assert.equal(cnt, test.expectedItems, "itemcount matching");
+
+ resolve();
+ },
+ onProgress(f, loaded, total, lengthComputable) {},
+ };
+
+ feed.download(true, cb);
+ });
+
+ // Wait for this feed to complete downloading.
+ await dl;
+ }
+});
diff --git a/comm/mailnews/extensions/newsblog/test/unit/test_rdfmigration.js b/comm/mailnews/extensions/newsblog/test/unit/test_rdfmigration.js
new file mode 100644
index 0000000000..a2bc2cf702
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/test_rdfmigration.js
@@ -0,0 +1,61 @@
+/* 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 { MailMigrator } = ChromeUtils.import(
+ "resource:///modules/MailMigrator.jsm"
+);
+
+/**
+ * Tests migration of old .rdf feed config files to the new .json files.
+ *
+ * @param {String} testDataDir - A directory containing legacy feeds.rdf and
+ * feeditems.rdf files, along with coressponding
+ * .json files containing the expected results
+ * of the migration.
+ * @returns {void}
+ */
+async function migrationTest(testDataDir) {
+ // Set up an RSS account/server.
+ let account = FeedUtils.createRssAccount("rss_migration_test");
+ let rootFolder = account.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ // Note, we don't create any folders to hold downloaded feed items,
+ // that's OK here, because we're only migrating the config files, not
+ // downloading feeds. The migration doesn't check destFolder existence.
+ let rootDir = rootFolder.filePath.path;
+
+ // Install legacy feeds.rdf/feeditems.rdf
+ for (let f of ["feeds.rdf", "feeditems.rdf"]) {
+ await IOUtils.copy(
+ PathUtils.join(testDataDir, f),
+ PathUtils.join(rootDir, f)
+ );
+ }
+
+ // Perform the migration
+ await MailMigrator._migrateRSSServer(account.incomingServer);
+
+ // Check actual results against expectations.
+ for (let f of ["feeds.json", "feeditems.json"]) {
+ let got = await IOUtils.readJSON(PathUtils.join(rootDir, f));
+ let expected = await IOUtils.readJSON(PathUtils.join(testDataDir, f));
+ Assert.deepEqual(got, expected, `match ${testDataDir}/${f}`);
+ }
+
+ // Delete the account and all it's files.
+ MailServices.accounts.removeAccount(account, true);
+}
+
+add_task(async function test_rdfmigration() {
+ let testDataDirs = [
+ "feeds-simple",
+ "feeds-empty",
+ "feeds-missing-timestamp",
+ "feeds-bad",
+ ];
+ for (let d of testDataDirs) {
+ await migrationTest(do_get_file("resources/" + d).path);
+ }
+});
diff --git a/comm/mailnews/extensions/newsblog/test/unit/xpcshell.ini b/comm/mailnews/extensions/newsblog/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..3b7fc2869c
--- /dev/null
+++ b/comm/mailnews/extensions/newsblog/test/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head_feeds.js
+tail =
+support-files = resources/*
+
+[test_feedparser.js]
+[test_rdfmigration.js]
+
diff --git a/comm/mailnews/extensions/offline-startup/OfflineStartup.jsm b/comm/mailnews/extensions/offline-startup/OfflineStartup.jsm
new file mode 100644
index 0000000000..a710b7ef32
--- /dev/null
+++ b/comm/mailnews/extensions/offline-startup/OfflineStartup.jsm
@@ -0,0 +1,126 @@
+/* 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 EXPORTED_SYMBOLS = ["OfflineStartup"];
+
+var kDebug = false;
+var kOfflineStartupPref = "offline.startup_state";
+var kRememberLastState = 0;
+var kAskForOnlineState = 1;
+var kAlwaysOnline = 2;
+var kAlwaysOffline = 3;
+var kAutomatic = 4;
+var gStartingUp = true;
+var gOfflineStartupMode; // 0 = remember last state, 1 = ask me, 2 == online, 3 == offline, 4 = automatic
+var gDebugLog;
+
+// Debug helper
+
+if (!kDebug) {
+ gDebugLog = function (m) {};
+} else {
+ gDebugLog = function (m) {
+ dump("\t *** nsOfflineStartup: " + m + "\n");
+ };
+}
+
+// nsOfflineStartup : nsIObserver
+//
+// Check if the user has set the pref to be prompted for
+// online/offline startup mode. If so, prompt the user. Also,
+// check if the user wants to remember their offline state
+// the next time they start up.
+// If the user shutdown offline, and is now starting up in online
+// mode, we will set the boolean pref "mailnews.playback_offline" to true.
+
+function OfflineStartup() {}
+OfflineStartup.prototype = {
+ onProfileStartup() {
+ gDebugLog("onProfileStartup");
+
+ if (gStartingUp) {
+ gStartingUp = false;
+ // if checked, the "work offline" checkbox overrides
+ if (Services.io.offline && !Services.io.manageOfflineStatus) {
+ gDebugLog("already offline!");
+ return;
+ }
+ }
+
+ var manageOfflineStatus = Services.prefs.getBoolPref("offline.autoDetect");
+ gOfflineStartupMode = Services.prefs.getIntPref(kOfflineStartupPref);
+ let wasOffline = !Services.prefs.getBoolPref("network.online");
+
+ if (gOfflineStartupMode == kAutomatic) {
+ // Offline state should be managed automatically
+ // so do nothing specific at startup.
+ } else if (gOfflineStartupMode == kAlwaysOffline) {
+ Services.io.manageOfflineStatus = false;
+ Services.io.offline = true;
+ } else if (gOfflineStartupMode == kAlwaysOnline) {
+ Services.io.manageOfflineStatus = manageOfflineStatus;
+ if (wasOffline) {
+ Services.prefs.setBoolPref("mailnews.playback_offline", true);
+ }
+ // If we're managing the offline status, don't force online here... it may
+ // be the network really is offline.
+ if (!manageOfflineStatus) {
+ Services.io.offline = false;
+ }
+ } else if (gOfflineStartupMode == kRememberLastState) {
+ Services.io.manageOfflineStatus = manageOfflineStatus && !wasOffline;
+ // If we are meant to be online, and managing the offline status
+ // then don't force it - it may be the network really is offline.
+ if (!manageOfflineStatus || wasOffline) {
+ Services.io.offline = wasOffline;
+ }
+ } else if (gOfflineStartupMode == kAskForOnlineState) {
+ var bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/offlineStartup.properties"
+ );
+ var title = bundle.GetStringFromName("title");
+ var desc = bundle.GetStringFromName("desc");
+ var button0Text = bundle.GetStringFromName("workOnline");
+ var button1Text = bundle.GetStringFromName("workOffline");
+ var checkVal = { value: 0 };
+
+ // Set Offline to true by default to prevent new mail checking at startup before
+ // the user answers the following question.
+ Services.io.manageOfflineStatus = false;
+ Services.io.offline = true;
+ var result = Services.prompt.confirmEx(
+ null,
+ title,
+ desc,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING,
+ button0Text,
+ button1Text,
+ null,
+ null,
+ checkVal
+ );
+ gDebugLog("result = " + result + "\n");
+ Services.io.manageOfflineStatus = manageOfflineStatus && result != 1;
+ Services.io.offline = result == 1;
+ if (result != 1 && wasOffline) {
+ Services.prefs.setBoolPref("mailnews.playback_offline", true);
+ }
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ gDebugLog("observe: " + aTopic);
+
+ if (aTopic == "profile-change-net-teardown") {
+ gDebugLog("remembering offline state");
+ Services.prefs.setBoolPref("network.online", !Services.io.offline);
+ } else if (aTopic == "profile-after-change") {
+ Services.obs.addObserver(this, "profile-change-net-teardown");
+ this.onProfileStartup();
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
diff --git a/comm/mailnews/extensions/offline-startup/components.conf b/comm/mailnews/extensions/offline-startup/components.conf
new file mode 100644
index 0000000000..8ca8a2cf2c
--- /dev/null
+++ b/comm/mailnews/extensions/offline-startup/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{3028a3c8-2165-42a4-b878-398da5d32736}",
+ "contract_ids": ["@mozilla.org/offline-startup;1"],
+ "jsm": "resource:///modules/OfflineStartup.jsm",
+ "constructor": "OfflineStartup",
+ "categories": {"profile-after-change": "Offline-startup"},
+ },
+]
diff --git a/comm/mailnews/extensions/offline-startup/moz.build b/comm/mailnews/extensions/offline-startup/moz.build
new file mode 100644
index 0000000000..e3b8854866
--- /dev/null
+++ b/comm/mailnews/extensions/offline-startup/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "OfflineStartup.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/extensions/smime/certFetchingStatus.js b/comm/mailnews/extensions/smime/certFetchingStatus.js
new file mode 100644
index 0000000000..ea7cd63226
--- /dev/null
+++ b/comm/mailnews/extensions/smime/certFetchingStatus.js
@@ -0,0 +1,255 @@
+/* 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/. */
+
+let USER_CERT_ATTRIBUTE = "usercertificate;binary";
+
+let gEmailAddresses;
+let gDirectoryPref;
+let gLdapServerURL;
+let gLdapConnection;
+let gCertDB;
+let gLdapOperation;
+let gLogin;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogcancel", stopFetching);
+
+/**
+ * Expects the following arguments:
+ * - pref name of LDAP directory to fetch from
+ * - array with email addresses
+ *
+ * Display modal dialog with message and stop button.
+ * In onload, kick off binding to LDAP.
+ * When bound, kick off the searches.
+ * On finding certificates, import into permanent cert database.
+ * When all searches are finished, close the dialog.
+ */
+function onLoad() {
+ gDirectoryPref = window.arguments[0];
+ gEmailAddresses = window.arguments[1];
+
+ if (!gEmailAddresses.length) {
+ window.close();
+ return;
+ }
+
+ setTimeout(search);
+}
+
+function search() {
+ // Get the login to authenticate as, if there is one. No big deal if we don't
+ // have one.
+ gLogin = Services.prefs.getStringPref(gDirectoryPref + ".auth.dn", undefined);
+
+ try {
+ let url = Services.prefs.getCharPref(gDirectoryPref + ".uri");
+
+ gLdapServerURL = Services.io.newURI(url).QueryInterface(Ci.nsILDAPURL);
+
+ gLdapConnection = Cc["@mozilla.org/network/ldap-connection;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsILDAPConnection);
+
+ gLdapConnection.init(
+ gLdapServerURL,
+ gLogin,
+ new BindListener(),
+ null,
+ Ci.nsILDAPConnection.VERSION3
+ );
+ } catch (ex) {
+ console.error(ex);
+ window.close();
+ }
+}
+
+function stopFetching() {
+ if (gLdapOperation) {
+ try {
+ gLdapOperation.abandon();
+ } catch (e) {}
+ }
+}
+
+function importCert(ber_value) {
+ if (!gCertDB) {
+ gCertDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ }
+
+ // ber_value has type nsILDAPBERValue
+ let cert_bytes = ber_value.get();
+ if (cert_bytes) {
+ gCertDB.importEmailCertificate(cert_bytes, cert_bytes.length, null);
+ }
+}
+
+function getLDAPOperation() {
+ gLdapOperation = Cc["@mozilla.org/network/ldap-operation;1"].createInstance(
+ Ci.nsILDAPOperation
+ );
+
+ gLdapOperation.init(gLdapConnection, new LDAPMessageListener(), null);
+}
+
+async function getPassword() {
+ // we only need a password if we are using credentials
+ if (!gLogin) {
+ return null;
+ }
+ let authPrompter = Services.ww.getNewAuthPrompter(window);
+ let strBundle = document.getElementById("bundle_ldap");
+ let password = { value: "" };
+
+ // nsLDAPAutocompleteSession uses asciiHost instead of host for the prompt
+ // text, I think we should be consistent.
+ if (
+ await authPrompter.asyncPromptPassword(
+ strBundle.getString("authPromptTitle"),
+ strBundle.getFormattedString("authPromptText", [
+ gLdapServerURL.asciiHost,
+ ]),
+ gLdapServerURL.spec,
+ authPrompter.SAVE_PASSWORD_PERMANENTLY,
+ password
+ )
+ ) {
+ return password.value;
+ }
+ return null;
+}
+
+/**
+ * Checks if the LDAP connection can be bound.
+ * @implements {nsILDAPMessageListener}
+ */
+class BindListener {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPMessageListener"]);
+
+ async onLDAPInit(conn, status) {
+ // Kick off bind.
+ getLDAPOperation();
+ gLdapOperation.simpleBind(await getPassword());
+ }
+
+ onLDAPMessage(message) {}
+
+ onLDAPError(status, secInfo, location) {
+ if (secInfo) {
+ console.warn(`LDAP bind connection security error for ${location}`);
+ } else {
+ console.warn(`LDAP bind error: ${status}`);
+ }
+ window.close();
+ }
+}
+
+/**
+ * LDAPMessageListener.
+ * @implements {nsILDAPMessageListener}
+ */
+class LDAPMessageListener {
+ QueryInterface = ChromeUtils.generateQI(["nsILDAPMessageListener"]);
+
+ onLDAPInit(conn, status) {}
+
+ onLDAPMessage(message) {
+ if (Ci.nsILDAPMessage.RES_SEARCH_RESULT == message.type) {
+ window.close();
+ return;
+ }
+
+ if (Ci.nsILDAPMessage.RES_BIND == message.type) {
+ if (Ci.nsILDAPErrors.SUCCESS != message.errorCode) {
+ window.close();
+ return;
+ }
+ // Kick off search.
+ let prefix1 = "";
+ let suffix1 = "";
+
+ let urlFilter = gLdapServerURL.filter;
+ if (
+ urlFilter != null &&
+ urlFilter.length > 0 &&
+ urlFilter != "(objectclass=*)"
+ ) {
+ if (urlFilter.startsWith("(")) {
+ prefix1 = "(&" + urlFilter;
+ } else {
+ prefix1 = "(&(" + urlFilter + ")";
+ }
+ suffix1 = ")";
+ }
+
+ let prefix2 = "";
+ let suffix2 = "";
+
+ if (gEmailAddresses.length > 1) {
+ prefix2 = "(|";
+ suffix2 = ")";
+ }
+
+ let mailFilter = "";
+
+ for (let email of gEmailAddresses) {
+ mailFilter += "(mail=" + email + ")";
+ }
+
+ let filter = prefix1 + prefix2 + mailFilter + suffix2 + suffix1;
+
+ // Max search results =>
+ // Double number of email addresses, because each person might have
+ // multiple certificates listed. We expect at most two certificates,
+ // one for signing, one for encrypting.
+ // Maybe that number should be larger, to allow for deployments,
+ // where even more certs can be stored per user???
+
+ let maxEntriesWanted = gEmailAddresses.length * 2;
+
+ getLDAPOperation();
+ gLdapOperation.searchExt(
+ gLdapServerURL.dn,
+ gLdapServerURL.scope,
+ filter,
+ USER_CERT_ATTRIBUTE,
+ 0,
+ maxEntriesWanted
+ );
+ return;
+ }
+
+ if (Ci.nsILDAPMessage.RES_SEARCH_ENTRY == message.type) {
+ let outBinValues = null;
+ try {
+ // This call may throw if the result message is empty or doesn't
+ // contain this attribute.
+ // It's an allowed condition that the attribute is missing on
+ // the server, so we silently ignore a failure to obtain it.
+ outBinValues = message.getBinaryValues(USER_CERT_ATTRIBUTE);
+ } catch (ex) {}
+ if (outBinValues) {
+ for (let i = 0; i < outBinValues.length; ++i) {
+ importCert(outBinValues[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param {nsresult} status
+ * @param {?nsITransportSecurityInfo} secInfo
+ * @param {?string} location
+ */
+ onLDAPError(status, secInfo, location) {
+ if (secInfo) {
+ console.warn(`LDAP connection security error for ${location}`);
+ } else {
+ console.warn(`LDAP error: ${status}`);
+ }
+ window.close();
+ }
+}
diff --git a/comm/mailnews/extensions/smime/certFetchingStatus.xhtml b/comm/mailnews/extensions/smime/certFetchingStatus.xhtml
new file mode 100644
index 0000000000..b82042edda
--- /dev/null
+++ b/comm/mailnews/extensions/smime/certFetchingStatus.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger-smime/locale/certFetchingStatus.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&title.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger-smime/content/certFetchingStatus.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ id="certFetchingStatus"
+ buttons="cancel"
+ buttonlabelcancel="&stop.label;"
+ >
+ <stringbundle
+ id="bundle_ldap"
+ src="chrome://mozldap/locale/ldap.properties"
+ />
+
+ <description>&info.message;</description>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/smime/certpicker.js b/comm/mailnews/extensions/smime/certpicker.js
new file mode 100644
index 0000000000..31ed9d3ed2
--- /dev/null
+++ b/comm/mailnews/extensions/smime/certpicker.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+"use strict";
+
+var dialogParams;
+var itemCount = 0;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogaccept", doOK);
+document.addEventListener("dialogcancel", doCancel);
+
+function onLoad() {
+ dialogParams = window.arguments[0].QueryInterface(Ci.nsIDialogParamBlock);
+
+ var selectElement = document.getElementById("nicknames");
+ itemCount = dialogParams.GetInt(0);
+
+ var selIndex = dialogParams.GetInt(1);
+ if (selIndex < 0) {
+ selIndex = 0;
+ }
+
+ for (let i = 0; i < itemCount; i++) {
+ let menuItemNode = document.createXULElement("menuitem");
+ let nick = dialogParams.GetString(i);
+ menuItemNode.setAttribute("value", i);
+ menuItemNode.setAttribute("label", nick); // This is displayed.
+ selectElement.menupopup.appendChild(menuItemNode);
+
+ if (selIndex == i) {
+ selectElement.selectedItem = menuItemNode;
+ }
+ }
+
+ dialogParams.SetInt(0, 0); // Set cancel return value.
+ setDetails();
+}
+
+function setDetails() {
+ let selItem = document.getElementById("nicknames").value;
+ if (selItem.length == 0) {
+ return;
+ }
+
+ let index = parseInt(selItem);
+ let details = dialogParams.GetString(index + itemCount);
+ document.getElementById("details").value = details;
+}
+
+function onCertSelected() {
+ setDetails();
+}
+
+function doOK() {
+ // Signal that the user accepted.
+ dialogParams.SetInt(0, 1);
+
+ // Signal the index of the selected cert in the list of cert nicknames
+ // provided.
+ let index = parseInt(document.getElementById("nicknames").value);
+ dialogParams.SetInt(1, index);
+}
+
+function doCancel() {
+ dialogParams.SetInt(0, 0); // Signal that the user cancelled.
+}
diff --git a/comm/mailnews/extensions/smime/certpicker.xhtml b/comm/mailnews/extensions/smime/certpicker.xhtml
new file mode 100644
index 0000000000..c0ff513cb6
--- /dev/null
+++ b/comm/mailnews/extensions/smime/certpicker.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % amE2EDTD SYSTEM "chrome://messenger/locale/am-smime.dtd">
+%amE2EDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&certPicker.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/certpicker.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="certPicker" buttons="accept,cancel">
+ <hbox align="center">
+ <label id="pickerInfo" value="&certPicker.info;" />
+ <!-- The items in this menulist must never be sorted,
+ but remain in the order filled by the application
+ -->
+ <menulist id="nicknames" oncommand="onCertSelected();">
+ <menupopup />
+ </menulist>
+ </hbox>
+ <separator class="thin" />
+ <label value="&certPicker.detailsLabel;" />
+ <html:textarea
+ id="details"
+ readonly="readonly"
+ style="height: 12em"
+ ></html:textarea>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/smime/components.conf b/comm/mailnews/extensions/smime/components.conf
new file mode 100644
index 0000000000..a5287d608a
--- /dev/null
+++ b/comm/mailnews/extensions/smime/components.conf
@@ -0,0 +1,63 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{54976882-7421-4286-8ecc-46373f15d7b5}",
+ "contract_ids": ["@mozilla.org/messengercompose/composesecure;1"],
+ "type": "nsMsgComposeSecure",
+ "headers": ["/comm/mailnews/extensions/smime/nsMsgComposeSecure.h"],
+ },
+ {
+ "cid": "{a0134d58-018f-4d40-a099-fa079e5024a6}",
+ "contract_ids": ["@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"],
+ "type": "nsEncryptedSMIMEURIsService",
+ "headers": ["/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.h"],
+ },
+ {
+ "cid": "{5fb907e0-1dd2-11b2-a7c0-f14c416a62a1}",
+ "contract_ids": ["@mozilla.org/nsCMSSecureMessage;1"],
+ "type": "nsCMSSecureMessage",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCMSSecureMessage.h"],
+ },
+ {
+ "cid": "{9dcef3a4-a3bc-11d5-ba47-00108303b117}",
+ "contract_ids": ["@mozilla.org/nsCMSDecoder;1"],
+ "type": "nsCMSDecoder",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCMS.h"],
+ },
+ {
+ "cid": "{fb62c8ed-b875-488a-be35-ab9764bcad25}",
+ "contract_ids": ["@mozilla.org/nsCMSDecoderJS;1"],
+ "type": "nsCMSDecoderJS",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCMS.h"],
+ },
+ {
+ "cid": "{a15789aa-8903-462b-81e9-4aa2cff4d5cb}",
+ "contract_ids": ["@mozilla.org/nsCMSEncoder;1"],
+ "type": "nsCMSEncoder",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCMS.h"],
+ },
+ {
+ "cid": "{a4557478-ae16-11d5-ba4b-00108303b117}",
+ "contract_ids": ["@mozilla.org/nsCMSMessage;1"],
+ "type": "nsCMSMessage",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCMS.h"],
+ },
+ {
+ "cid": "{735959a1-af01-447e-b02d-56e968fa52b4}",
+ "contract_ids": [
+ "@mozilla.org/nsCertPickDialogs;1",
+ "@mozilla.org/user_cert_picker;1",
+ ],
+ "type": "nsCertPicker",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/extensions/smime/nsCertPicker.h"],
+ },
+]
diff --git a/comm/mailnews/extensions/smime/moz.build b/comm/mailnews/extensions/smime/moz.build
new file mode 100644
index 0000000000..f33678ecf2
--- /dev/null
+++ b/comm/mailnews/extensions/smime/moz.build
@@ -0,0 +1,37 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsICertPickDialogs.idl",
+ "nsICMSDecoder.idl",
+ "nsICMSDecoderJS.idl",
+ "nsICMSEncoder.idl",
+ "nsICMSMessage.idl",
+ "nsICMSMessageErrors.idl",
+ "nsICMSSecureMessage.idl",
+ "nsIEncryptedSMIMEURIsSrvc.idl",
+ "nsIMsgSMIMEHeaderSink.idl",
+ "nsIUserCertPicker.idl",
+]
+
+XPIDL_MODULE = "msgsmime"
+
+SOURCES += [
+ "nsCertPicker.cpp",
+ "nsCMS.cpp",
+ "nsCMSSecureMessage.cpp",
+ "nsEncryptedSMIMEURIsService.cpp",
+ "nsMsgComposeSecure.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+LOCAL_INCLUDES += [
+ "/security/certverifier",
+ "/security/manager/pki",
+ "/security/manager/ssl",
+]
+
+XPCOM_MANIFESTS += ["components.conf"]
diff --git a/comm/mailnews/extensions/smime/msgCompSecurityInfo.js b/comm/mailnews/extensions/smime/msgCompSecurityInfo.js
new file mode 100644
index 0000000000..eb760a9c58
--- /dev/null
+++ b/comm/mailnews/extensions/smime/msgCompSecurityInfo.js
@@ -0,0 +1,122 @@
+/* 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 gListBox;
+var gViewButton;
+var gBundle;
+
+var gCerts = [];
+
+window.addEventListener("DOMContentLoaded", onLoad);
+window.addEventListener("resize", resizeColumns);
+
+function onLoad() {
+ let params = window.arguments[0];
+ if (!params) {
+ return;
+ }
+
+ gListBox = document.getElementById("infolist");
+ gViewButton = document.getElementById("viewCertButton");
+ gBundle = document.getElementById("bundle_smime_comp_info");
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+
+ let missing = [];
+ for (let i = 0; i < params.recipients.length; i++) {
+ let email = params.recipients[i];
+ let dbKey = params.compFields.composeSecure.getCertDBKeyForEmail(email);
+
+ if (dbKey) {
+ gCerts.push(certdb.findCertByDBKey(dbKey));
+ } else {
+ gCerts.push(null);
+ }
+
+ if (!gCerts[i]) {
+ missing.push(params.recipients[i]);
+ }
+ }
+
+ for (let i = 0; i < params.recipients.length; ++i) {
+ let email = document.createXULElement("label");
+ email.setAttribute("value", params.recipients[i]);
+ email.setAttribute("crop", "end");
+ email.setAttribute("style", "width: var(--recipientWidth)");
+
+ let listitem = document.createXULElement("richlistitem");
+ listitem.appendChild(email);
+
+ let cert = gCerts[i];
+ let statusItem = document.createXULElement("label");
+ statusItem.setAttribute(
+ "value",
+ gBundle.getString(cert ? "StatusValid" : "StatusNotFound")
+ );
+ statusItem.setAttribute("style", "width: var(--statusWidth)");
+ listitem.appendChild(statusItem);
+
+ gListBox.appendChild(listitem);
+ }
+ resizeColumns();
+}
+
+function resizeColumns() {
+ let list = document.getElementById("infolist");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty(
+ "--recipientWidth",
+ cols[0].getBoundingClientRect().width + "px"
+ );
+ list.style.setProperty(
+ "--statusWidth",
+ cols[1].getBoundingClientRect().width + "px"
+ );
+ list.style.setProperty(
+ "--issuedWidth",
+ cols[2].getBoundingClientRect().width + "px"
+ );
+ list.style.setProperty(
+ "--expireWidth",
+ cols[3].getBoundingClientRect().width - 5 + "px"
+ );
+}
+
+// --- borrowed from pippki.js ---
+const PRErrorCodeSuccess = 0;
+
+const certificateUsageEmailSigner = 0x0010;
+const certificateUsageEmailRecipient = 0x0020;
+
+// A map from the name of a certificate usage to the value of the usage.
+const certificateUsages = {
+ certificateUsageEmailRecipient,
+};
+
+function onSelectionChange(event) {
+ gViewButton.disabled = !(
+ gListBox.selectedItems.length == 1 && certForRow(gListBox.selectedIndex)
+ );
+}
+
+function viewCertHelper(parent, cert) {
+ let url = `about:certificate?cert=${encodeURIComponent(
+ cert.getBase64DERString()
+ )}`;
+ let mail3PaneWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ mail3PaneWindow.switchToTabHavingURI(url, true, {});
+ parent.close();
+}
+
+function certForRow(aRowIndex) {
+ return gCerts[aRowIndex];
+}
+
+function viewSelectedCert() {
+ if (!gViewButton.disabled) {
+ viewCertHelper(window, certForRow(gListBox.selectedIndex));
+ }
+}
diff --git a/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml b/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml
new file mode 100644
index 0000000000..982d76818e
--- /dev/null
+++ b/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/smime/msgCompSecurityInfo.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger-smime/locale/msgCompSecurityInfo.dtd">
+<html
+ id="msgCompSecurityInfo"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&title.label;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-smime/content/msgCompSecurityInfo.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept">
+ <stringbundle
+ id="bundle_smime_comp_info"
+ src="chrome://messenger-smime/locale/msgCompSecurityInfo.properties"
+ />
+
+ <separator class="thin" />
+ <label value="&status.certificates;" control="infolist" />
+
+ <richlistbox
+ id="infolist"
+ class="theme-listbox"
+ flex="1"
+ onselect="onSelectionChange(event);"
+ >
+ <treecols>
+ <treecol id="recipientHeader" label="&tree.recipient;" />
+ <treecol id="statusHeader" label="&tree.status;" />
+ <treecol id="issuedDateHeader" label="&tree.issuedDate;" />
+ <treecol id="expiresDateHeader" label="&tree.expiresDate;" />
+ </treecols>
+ </richlistbox>
+ <hbox pack="start">
+ <button
+ id="viewCertButton"
+ disabled="true"
+ label="&view.label;"
+ accesskey="&view.accesskey;"
+ oncommand="viewSelectedCert();"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/extensions/smime/msgReadSMIMEOverlay.js b/comm/mailnews/extensions/smime/msgReadSMIMEOverlay.js
new file mode 100644
index 0000000000..bd7672bbe8
--- /dev/null
+++ b/comm/mailnews/extensions/smime/msgReadSMIMEOverlay.js
@@ -0,0 +1,251 @@
+/* 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/. */
+
+/* import-globals-from ../../../mail/base/content/aboutMessage.js */
+
+var gEncryptionStatus = -1;
+var gSignatureStatus = -1;
+var gSignerCert = null;
+var gEncryptionCert = null;
+
+function showImapSignatureUnknown() {
+ let readSmimeBundle = Services.strings.createBundle(
+ "chrome://messenger-smime/locale/msgReadSMIMEOverlay.properties"
+ );
+ let brandBundle = document.getElementById("bundle_brand");
+ if (!readSmimeBundle || !brandBundle) {
+ return;
+ }
+
+ if (
+ Services.prompt.confirm(
+ window,
+ brandBundle.getString("brandShortName"),
+ readSmimeBundle.GetStringFromName("ImapOnDemand")
+ )
+ ) {
+ gDBView.reloadMessageWithAllParts();
+ }
+}
+
+/**
+ * Populate the message security popup panel with S/MIME data.
+ */
+function loadSmimeMessageSecurityInfo() {
+ let sBundle = Services.strings.createBundle(
+ "chrome://messenger-smime/locale/msgSecurityInfo.properties"
+ );
+
+ let sigInfoLabel = null;
+ let sigInfoHeader = null;
+ let sigInfo = null;
+ let sigInfo_clueless = false;
+ let sigClass = null;
+
+ switch (gSignatureStatus) {
+ case -1:
+ case Ci.nsICMSMessageErrors.VERIFY_NOT_SIGNED:
+ sigInfoLabel = "SINoneLabel";
+ sigInfo = "SINone";
+ sigClass = "none";
+ break;
+
+ case Ci.nsICMSMessageErrors.SUCCESS:
+ sigInfoLabel = "SIValidLabel";
+ sigInfo = "SIValid";
+ sigClass = "ok";
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_BAD_SIGNATURE:
+ case Ci.nsICMSMessageErrors.VERIFY_DIGEST_MISMATCH:
+ sigInfoLabel = "SIInvalidLabel";
+ sigInfoHeader = "SIInvalidHeader";
+ sigInfo = "SIContentAltered";
+ sigClass = "mismatch";
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_UNKNOWN_ALGO:
+ case Ci.nsICMSMessageErrors.VERIFY_UNSUPPORTED_ALGO:
+ sigInfoLabel = "SIInvalidLabel";
+ sigInfoHeader = "SIInvalidHeader";
+ sigInfo = "SIInvalidCipher";
+ sigClass = "unknown";
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_HEADER_MISMATCH:
+ sigInfoLabel = "SIPartiallyValidLabel";
+ sigInfoHeader = "SIPartiallyValidHeader";
+ sigInfo = "SIHeaderMismatch";
+ sigClass = "mismatch";
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_CERT_WITHOUT_ADDRESS:
+ sigInfoLabel = "SIPartiallyValidLabel";
+ sigInfoHeader = "SIPartiallyValidHeader";
+ sigInfo = "SICertWithoutAddress";
+ sigClass = "unknown";
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_UNTRUSTED:
+ sigInfoLabel = "SIInvalidLabel";
+ sigInfoHeader = "SIInvalidHeader";
+ sigInfo = "SIUntrustedCA";
+ sigClass = "notok";
+ // XXX Need to extend to communicate better errors
+ // might also be:
+ // SIExpired SIRevoked SINotYetValid SIUnknownCA SIExpiredCA SIRevokedCA SINotYetValidCA
+ break;
+
+ case Ci.nsICMSMessageErrors.VERIFY_NOT_YET_ATTEMPTED:
+ case Ci.nsICMSMessageErrors.GENERAL_ERROR:
+ case Ci.nsICMSMessageErrors.VERIFY_NO_CONTENT_INFO:
+ case Ci.nsICMSMessageErrors.VERIFY_BAD_DIGEST:
+ case Ci.nsICMSMessageErrors.VERIFY_NOCERT:
+ case Ci.nsICMSMessageErrors.VERIFY_ERROR_UNVERIFIED:
+ case Ci.nsICMSMessageErrors.VERIFY_ERROR_PROCESSING:
+ case Ci.nsICMSMessageErrors.VERIFY_MALFORMED_SIGNATURE:
+ case Ci.nsICMSMessageErrors.VERIFY_TIME_MISMATCH:
+ sigInfoLabel = "SIInvalidLabel";
+ sigInfoHeader = "SIInvalidHeader";
+ sigInfo_clueless = true;
+ sigClass = "unverified";
+ break;
+ default:
+ console.error("Unexpected gSignatureStatus: " + gSignatureStatus);
+ }
+
+ document.getElementById("techLabel").textContent = "- S/MIME";
+
+ let signatureLabel = document.getElementById("signatureLabel");
+ signatureLabel.textContent = sBundle.GetStringFromName(sigInfoLabel);
+
+ // Remove the second class to properly update the signature icon.
+ signatureLabel.classList.remove(signatureLabel.classList.item(1));
+ signatureLabel.classList.add(sigClass);
+
+ if (sigInfoHeader) {
+ let label = document.getElementById("signatureHeader");
+ label.collapsed = false;
+ label.textContent = sBundle.GetStringFromName(sigInfoHeader);
+ }
+
+ let str;
+ if (sigInfo) {
+ str = sBundle.GetStringFromName(sigInfo);
+ } else if (sigInfo_clueless) {
+ str =
+ sBundle.GetStringFromName("SIClueless") + " (" + gSignatureStatus + ")";
+ }
+ document.getElementById("signatureExplanation").textContent = str;
+
+ let encInfoLabel = null;
+ let encInfoHeader = null;
+ let encInfo = null;
+ let encInfo_clueless = false;
+ let encClass = null;
+
+ switch (gEncryptionStatus) {
+ case -1:
+ encInfoLabel = "EINoneLabel2";
+ encInfo = "EINone";
+ encClass = "none";
+ break;
+
+ case Ci.nsICMSMessageErrors.SUCCESS:
+ encInfoLabel = "EIValidLabel";
+ encInfo = "EIValid";
+ encClass = "ok";
+ break;
+
+ case Ci.nsICMSMessageErrors.ENCRYPT_INCOMPLETE:
+ encInfoLabel = "EIInvalidLabel";
+ encInfo = "EIContentAltered";
+ encClass = "notok";
+ break;
+
+ case Ci.nsICMSMessageErrors.GENERAL_ERROR:
+ encInfoLabel = "EIInvalidLabel";
+ encInfoHeader = "EIInvalidHeader";
+ encInfo_clueless = 1;
+ encClass = "notok";
+ break;
+ default:
+ console.error("Unexpected gEncryptionStatus: " + gEncryptionStatus);
+ }
+
+ let encryptionLabel = document.getElementById("encryptionLabel");
+ encryptionLabel.textContent = sBundle.GetStringFromName(encInfoLabel);
+
+ // Remove the second class to properly update the encryption icon.
+ encryptionLabel.classList.remove(encryptionLabel.classList.item(1));
+ encryptionLabel.classList.add(encClass);
+
+ if (encInfoHeader) {
+ let label = document.getElementById("encryptionHeader");
+ label.collapsed = false;
+ label.textContent = sBundle.GetStringFromName(encInfoHeader);
+ }
+
+ if (encInfo) {
+ str = sBundle.GetStringFromName(encInfo);
+ } else if (encInfo_clueless) {
+ str = sBundle.GetStringFromName("EIClueless");
+ }
+ document.getElementById("encryptionExplanation").textContent = str;
+
+ if (gSignerCert) {
+ document.getElementById("signatureCert").collapsed = false;
+ if (gSignerCert.subjectName) {
+ document.getElementById("signedBy").textContent = gSignerCert.commonName;
+ }
+ if (gSignerCert.emailAddress) {
+ document.getElementById("signerEmail").textContent =
+ gSignerCert.emailAddress;
+ }
+ if (gSignerCert.issuerName) {
+ document.getElementById("sigCertIssuedBy").textContent =
+ gSignerCert.issuerCommonName;
+ }
+ }
+
+ if (gEncryptionCert) {
+ document.getElementById("encryptionCert").collapsed = false;
+ if (gEncryptionCert.subjectName) {
+ document.getElementById("encryptedFor").textContent =
+ gEncryptionCert.commonName;
+ }
+ if (gEncryptionCert.emailAddress) {
+ document.getElementById("recipientEmail").textContent =
+ gEncryptionCert.emailAddress;
+ }
+ if (gEncryptionCert.issuerName) {
+ document.getElementById("encCertIssuedBy").textContent =
+ gEncryptionCert.issuerCommonName;
+ }
+ }
+}
+
+function viewSignatureCert() {
+ if (!gSignerCert) {
+ return;
+ }
+
+ let url = `about:certificate?cert=${encodeURIComponent(
+ gSignerCert.getBase64DERString()
+ )}`;
+ let mail3PaneWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ mail3PaneWindow.switchToTabHavingURI(url, true, {});
+}
+
+function viewEncryptionCert() {
+ if (!gEncryptionCert) {
+ return;
+ }
+
+ let url = `about:certificate?cert=${encodeURIComponent(
+ gEncryptionCert.getBase64DERString()
+ )}`;
+ let mail3PaneWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ mail3PaneWindow.switchToTabHavingURI(url, true, {});
+}
diff --git a/comm/mailnews/extensions/smime/nsCMS.cpp b/comm/mailnews/extensions/smime/nsCMS.cpp
new file mode 100644
index 0000000000..be743b9663
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCMS.cpp
@@ -0,0 +1,1187 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCMS.h"
+
+#include "CertVerifier.h"
+#include "CryptoTask.h"
+#include "ScopedNSSTypes.h"
+#include "cms.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "nsDependentSubstring.h"
+#include "nsICryptoHash.h"
+#include "nsISupports.h"
+#include "nsIX509CertDB.h"
+#include "nsNSSCertificate.h"
+#include "nsNSSComponent.h"
+#include "nsNSSHelper.h"
+#include "nsServiceManagerUtils.h"
+#include "mozpkix/Result.h"
+#include "mozpkix/pkixtypes.h"
+#include "sechash.h"
+#include "secerr.h"
+#include "smime.h"
+#include "mozilla/StaticMutex.h"
+#include "nsIPrefBranch.h"
+
+using namespace mozilla;
+using namespace mozilla::psm;
+using namespace mozilla::pkix;
+
+static mozilla::LazyLogModule gCMSLog("CMS");
+
+NS_IMPL_ISUPPORTS(nsCMSMessage, nsICMSMessage)
+
+nsCMSMessage::nsCMSMessage() { m_cmsMsg = nullptr; }
+nsCMSMessage::nsCMSMessage(NSSCMSMessage* aCMSMsg) { m_cmsMsg = aCMSMsg; }
+
+nsCMSMessage::~nsCMSMessage() {
+ if (m_cmsMsg) {
+ NSS_CMSMessage_Destroy(m_cmsMsg);
+ }
+}
+
+nsresult nsCMSMessage::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> nssInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsCMSMessage::VerifySignature(int32_t verifyFlags) {
+ return CommonVerifySignature(verifyFlags, {}, 0);
+}
+
+NSSCMSSignerInfo* nsCMSMessage::GetTopLevelSignerInfo() {
+ if (!m_cmsMsg) return nullptr;
+
+ if (!NSS_CMSMessage_IsSigned(m_cmsMsg)) return nullptr;
+
+ NSSCMSContentInfo* cinfo = NSS_CMSMessage_ContentLevel(m_cmsMsg, 0);
+ if (!cinfo) return nullptr;
+
+ NSSCMSSignedData* sigd =
+ (NSSCMSSignedData*)NSS_CMSContentInfo_GetContent(cinfo);
+ if (!sigd) return nullptr;
+
+ PR_ASSERT(NSS_CMSSignedData_SignerInfoCount(sigd) > 0);
+ return NSS_CMSSignedData_GetSignerInfo(sigd, 0);
+}
+
+NS_IMETHODIMP nsCMSMessage::GetSignerEmailAddress(char** aEmail) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::GetSignerEmailAddress"));
+ NS_ENSURE_ARG(aEmail);
+
+ NSSCMSSignerInfo* si = GetTopLevelSignerInfo();
+ if (!si) return NS_ERROR_FAILURE;
+
+ *aEmail = NSS_CMSSignerInfo_GetSignerEmailAddress(si);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSMessage::GetSignerCommonName(char** aName) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::GetSignerCommonName"));
+ NS_ENSURE_ARG(aName);
+
+ NSSCMSSignerInfo* si = GetTopLevelSignerInfo();
+ if (!si) return NS_ERROR_FAILURE;
+
+ *aName = NSS_CMSSignerInfo_GetSignerCommonName(si);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSMessage::ContentIsEncrypted(bool* isEncrypted) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::ContentIsEncrypted"));
+ NS_ENSURE_ARG(isEncrypted);
+
+ if (!m_cmsMsg) return NS_ERROR_FAILURE;
+
+ *isEncrypted = NSS_CMSMessage_IsEncrypted(m_cmsMsg);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSMessage::ContentIsSigned(bool* isSigned) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::ContentIsSigned"));
+ NS_ENSURE_ARG(isSigned);
+
+ if (!m_cmsMsg) return NS_ERROR_FAILURE;
+
+ *isSigned = NSS_CMSMessage_IsSigned(m_cmsMsg);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSMessage::GetSignerCert(nsIX509Cert** scert) {
+ NSSCMSSignerInfo* si = GetTopLevelSignerInfo();
+ if (!si) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIX509Cert> cert;
+ if (si->cert) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::GetSignerCert got signer cert"));
+
+ nsCOMPtr<nsIX509CertDB> certdb = do_GetService(NS_X509CERTDB_CONTRACTID);
+ nsTArray<uint8_t> certBytes;
+ certBytes.AppendElements(si->cert->derCert.data, si->cert->derCert.len);
+ nsresult rv = certdb->ConstructX509(certBytes, getter_AddRefs(cert));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::GetSignerCert no signer cert, do we have a cert "
+ "list? %s",
+ (si->certList ? "yes" : "no")));
+
+ *scert = nullptr;
+ }
+
+ cert.forget(scert);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSMessage::GetEncryptionCert(nsIX509Cert**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsCMSMessage::GetSigningTime(PRTime* aTime) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::GetSigningTime"));
+ NS_ENSURE_ARG(aTime);
+
+ NSSCMSSignerInfo* si = GetTopLevelSignerInfo();
+ if (!si) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SECStatus getSigningTimeResult = NSS_CMSSignerInfo_GetSigningTime(si, aTime);
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::GetSigningTime result: %s",
+ (getSigningTimeResult ? "ok" : "fail")));
+
+ return getSigningTimeResult == SECSuccess ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsCMSMessage::VerifyDetachedSignature(int32_t verifyFlags,
+ const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType) {
+ if (aDigestData.IsEmpty()) return NS_ERROR_FAILURE;
+
+ return CommonVerifySignature(verifyFlags, aDigestData, aDigestType);
+}
+
+// This is an exact copy of NSS_CMSArray_Count from NSS' cmsarray.c,
+// temporarily necessary, see below for for justification.
+static int myNSS_CMSArray_Count(void** array) {
+ int n = 0;
+
+ if (array == NULL) return 0;
+
+ while (*array++ != NULL) n++;
+
+ return n;
+}
+
+// This is an exact copy of NSS_CMSArray_Add from NSS' cmsarray.c,
+// temporarily necessary, see below for for justification.
+static SECStatus myNSS_CMSArray_Add(PLArenaPool* poolp, void*** array,
+ void* obj) {
+ void** p;
+ int n;
+ void** dest;
+
+ PORT_Assert(array != NULL);
+ if (array == NULL) return SECFailure;
+
+ if (*array == NULL) {
+ dest = (void**)PORT_ArenaAlloc(poolp, 2 * sizeof(void*));
+ n = 0;
+ } else {
+ n = 0;
+ p = *array;
+ while (*p++) n++;
+ dest = (void**)PORT_ArenaGrow(poolp, *array, (n + 1) * sizeof(void*),
+ (n + 2) * sizeof(void*));
+ }
+
+ if (dest == NULL) return SECFailure;
+
+ dest[n] = obj;
+ dest[n + 1] = NULL;
+ *array = dest;
+ return SECSuccess;
+}
+
+// This is an exact copy of NSS_CMSArray_Add from NSS' cmsarray.c,
+// temporarily necessary, see below for for justification.
+static SECStatus myNSS_CMSSignedData_AddTempCertificate(NSSCMSSignedData* sigd,
+ CERTCertificate* cert) {
+ CERTCertificate* c;
+ SECStatus rv;
+
+ if (!sigd || !cert) {
+ PORT_SetError(SEC_ERROR_INVALID_ARGS);
+ return SECFailure;
+ }
+
+ c = CERT_DupCertificate(cert);
+ rv = myNSS_CMSArray_Add(sigd->cmsg->poolp, (void***)&(sigd->tempCerts),
+ (void*)c);
+ return rv;
+}
+
+typedef SECStatus (*extraVerificationOnCertFn)(CERTCertificate* cert,
+ SECCertUsage certusage);
+
+static SECStatus myExtraVerificationOnCert(CERTCertificate* cert,
+ SECCertUsage certusage) {
+ RefPtr<SharedCertVerifier> certVerifier;
+ certVerifier = GetDefaultCertVerifier();
+ if (!certVerifier) {
+ return SECFailure;
+ }
+
+ SECCertificateUsage usageForPkix;
+
+ switch (certusage) {
+ case certUsageEmailSigner:
+ usageForPkix = certificateUsageEmailSigner;
+ break;
+ case certUsageEmailRecipient:
+ usageForPkix = certificateUsageEmailRecipient;
+ break;
+ default:
+ return SECFailure;
+ }
+
+ nsTArray<uint8_t> certBytes(cert->derCert.data, cert->derCert.len);
+ nsTArray<nsTArray<uint8_t>> builtChain;
+ // This code is used when verifying incoming certificates, including
+ // a signature certificate. Performing OCSP is necessary.
+ // Allowing OCSP in blocking mode should be fine, because all our
+ // callers run this code on a separate thread, using
+ // SMimeVerificationTask/CryptoTask.
+ mozilla::pkix::Result result = certVerifier->VerifyCert(
+ certBytes, usageForPkix, Now(), nullptr /*XXX pinarg*/,
+ nullptr /*hostname*/, builtChain);
+ if (result != mozilla::pkix::Success) {
+ return SECFailure;
+ }
+
+ return SECSuccess;
+}
+
+// This is a temporary copy of NSS_CMSSignedData_ImportCerts, which
+// performs additional verifications prior to import.
+// The copy is almost identical to the original.
+//
+// The ONLY DIFFERENCE is the addition of parameter extraVerifyFn,
+// and the call to it - plus a non-null check.
+//
+// NSS should add this or a similar API in the future,
+// and then these temporary functions should be removed, including
+// the ones above. Request is tracked in bugzilla 1738592.
+static SECStatus myNSS_CMSSignedData_ImportCerts(
+ NSSCMSSignedData* sigd, CERTCertDBHandle* certdb, SECCertUsage certusage,
+ PRBool keepcerts, extraVerificationOnCertFn extraVerifyFn) {
+ int certcount;
+ CERTCertificate** certArray = NULL;
+ CERTCertList* certList = NULL;
+ CERTCertListNode* node;
+ SECStatus rv;
+ SECItem** rawArray;
+ int i;
+ PRTime now;
+
+ if (!sigd) {
+ PORT_SetError(SEC_ERROR_INVALID_ARGS);
+ return SECFailure;
+ }
+
+ certcount = myNSS_CMSArray_Count((void**)sigd->rawCerts);
+
+ /* get the certs in the temp DB */
+ rv = CERT_ImportCerts(certdb, certusage, certcount, sigd->rawCerts,
+ &certArray, PR_FALSE, PR_FALSE, NULL);
+ if (rv != SECSuccess) {
+ goto loser;
+ }
+
+ /* save the certs so they don't get destroyed */
+ for (i = 0; i < certcount; i++) {
+ CERTCertificate* cert = certArray[i];
+ if (cert) myNSS_CMSSignedData_AddTempCertificate(sigd, cert);
+ }
+
+ if (!keepcerts) {
+ goto done;
+ }
+
+ /* build a CertList for filtering */
+ certList = CERT_NewCertList();
+ if (certList == NULL) {
+ rv = SECFailure;
+ goto loser;
+ }
+ for (i = 0; i < certcount; i++) {
+ CERTCertificate* cert = certArray[i];
+ if (cert) cert = CERT_DupCertificate(cert);
+ if (cert) CERT_AddCertToListTail(certList, cert);
+ }
+
+ /* filter out the certs we don't want */
+ rv = CERT_FilterCertListByUsage(certList, certusage, PR_FALSE);
+ if (rv != SECSuccess) {
+ goto loser;
+ }
+
+ /* go down the remaining list of certs and verify that they have
+ * valid chains, then import them.
+ */
+ now = PR_Now();
+ for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList);
+ node = CERT_LIST_NEXT(node)) {
+ CERTCertificateList* certChain;
+
+ if (!node->cert) {
+ continue;
+ }
+
+ if (extraVerifyFn) {
+ if ((*extraVerifyFn)(node->cert, certusage) != SECSuccess) {
+ continue;
+ }
+ }
+
+ if (CERT_VerifyCert(certdb, node->cert, PR_TRUE, certusage, now, NULL,
+ NULL) != SECSuccess) {
+ continue;
+ }
+
+ certChain = CERT_CertChainFromCert(node->cert, certusage, PR_FALSE);
+ if (!certChain) {
+ continue;
+ }
+
+ /*
+ * CertChain returns an array of SECItems, import expects an array of
+ * SECItem pointers. Create the SECItem Pointers from the array of
+ * SECItems.
+ */
+ rawArray = (SECItem**)PORT_Alloc(certChain->len * sizeof(SECItem*));
+ if (!rawArray) {
+ CERT_DestroyCertificateList(certChain);
+ continue;
+ }
+ for (i = 0; i < certChain->len; i++) {
+ rawArray[i] = &certChain->certs[i];
+ }
+ (void)CERT_ImportCerts(certdb, certusage, certChain->len, rawArray, NULL,
+ keepcerts, PR_FALSE, NULL);
+ PORT_Free(rawArray);
+ CERT_DestroyCertificateList(certChain);
+ }
+
+ rv = SECSuccess;
+
+ /* XXX CRL handling */
+
+done:
+ if (sigd->signerInfos != NULL) {
+ /* fill in all signerinfo's certs */
+ for (i = 0; sigd->signerInfos[i] != NULL; i++)
+ (void)NSS_CMSSignerInfo_GetSigningCertificate(sigd->signerInfos[i],
+ certdb);
+ }
+
+loser:
+ /* now free everything */
+ if (certArray) {
+ CERT_DestroyCertArray(certArray, certcount);
+ }
+ if (certList) {
+ CERT_DestroyCertList(certList);
+ }
+
+ return rv;
+}
+
+nsresult nsCMSMessage::CommonVerifySignature(
+ int32_t verifyFlags, const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature, content level count %d",
+ NSS_CMSMessage_ContentLevelCount(m_cmsMsg)));
+ NSSCMSContentInfo* cinfo = nullptr;
+ NSSCMSSignedData* sigd = nullptr;
+ NSSCMSSignerInfo* si;
+ int32_t nsigners;
+ nsresult rv = NS_ERROR_FAILURE;
+ SECOidTag sigAlgTag;
+
+ if (!NSS_CMSMessage_IsSigned(m_cmsMsg)) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - not signed"));
+ return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
+ }
+
+ cinfo = NSS_CMSMessage_ContentLevel(m_cmsMsg, 0);
+ if (cinfo) {
+ switch (NSS_CMSContentInfo_GetContentTypeTag(cinfo)) {
+ case SEC_OID_PKCS7_SIGNED_DATA:
+ sigd = reinterpret_cast<NSSCMSSignedData*>(
+ NSS_CMSContentInfo_GetContent(cinfo));
+ break;
+
+ case SEC_OID_PKCS7_ENVELOPED_DATA:
+ case SEC_OID_PKCS7_ENCRYPTED_DATA:
+ case SEC_OID_PKCS7_DIGESTED_DATA:
+ default: {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - unexpected "
+ "ContentTypeTag"));
+ rv = NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
+ goto loser;
+ }
+ }
+ }
+
+ if (!sigd) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - no content info"));
+ rv = NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
+ goto loser;
+ }
+
+ if (!aDigestData.IsEmpty()) {
+ SECOidTag oidTag;
+ SECItem digest;
+ // NSS_CMSSignedData_SetDigestValue() takes a copy and won't mutate our
+ // data, so we're OK to cast away the const here.
+ digest.data = const_cast<uint8_t*>(aDigestData.Elements());
+ digest.len = aDigestData.Length();
+
+ if (NSS_CMSSignedData_HasDigests(sigd)) {
+ SECAlgorithmID** existingAlgs = NSS_CMSSignedData_GetDigestAlgs(sigd);
+ if (existingAlgs) {
+ while (*existingAlgs) {
+ SECAlgorithmID* alg = *existingAlgs;
+ SECOidTag algOIDTag = SECOID_FindOIDTag(&alg->algorithm);
+ NSS_CMSSignedData_SetDigestValue(sigd, algOIDTag, NULL);
+ ++existingAlgs;
+ }
+ }
+ }
+
+ oidTag =
+ HASH_GetHashOidTagByHashType(static_cast<HASH_HashType>(aDigestType));
+ if (oidTag == SEC_OID_UNKNOWN) {
+ rv = NS_ERROR_CMS_VERIFY_BAD_DIGEST;
+ goto loser;
+ }
+
+ if (NSS_CMSSignedData_SetDigestValue(sigd, oidTag, &digest)) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - bad digest"));
+ rv = NS_ERROR_CMS_VERIFY_BAD_DIGEST;
+ goto loser;
+ }
+ }
+
+ // Import certs. Note that import failure is not a signature verification
+ // failure. //
+ if (myNSS_CMSSignedData_ImportCerts(
+ sigd, CERT_GetDefaultCertDB(), certUsageEmailRecipient, true,
+ myExtraVerificationOnCert) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - can not import certs"));
+ }
+
+ nsigners = NSS_CMSSignedData_SignerInfoCount(sigd);
+ PR_ASSERT(nsigners > 0);
+ NS_ENSURE_TRUE(nsigners > 0, NS_ERROR_UNEXPECTED);
+ si = NSS_CMSSignedData_GetSignerInfo(sigd, 0);
+
+ NS_ENSURE_TRUE(si, NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(si->cert, NS_ERROR_UNEXPECTED);
+
+ // See bug 324474. We want to make sure the signing cert is
+ // still valid at the current time.
+
+ if (myExtraVerificationOnCert(si->cert, certUsageEmailSigner) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - signing cert not trusted "
+ "now"));
+ rv = NS_ERROR_CMS_VERIFY_UNTRUSTED;
+ goto loser;
+ }
+
+ sigAlgTag = NSS_CMSSignerInfo_GetDigestAlgTag(si);
+ switch (sigAlgTag) {
+ case SEC_OID_SHA256:
+ case SEC_OID_SHA384:
+ case SEC_OID_SHA512:
+ break;
+
+ case SEC_OID_SHA1:
+ if (verifyFlags & nsICMSVerifyFlags::VERIFY_ALLOW_WEAK_SHA1) {
+ break;
+ }
+ // else fall through to failure
+#if defined(__clang__)
+ [[clang::fallthrough]];
+#endif
+
+ default:
+ MOZ_LOG(
+ gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - unsupported digest algo"));
+ rv = NS_ERROR_CMS_VERIFY_UNSUPPORTED_ALGO;
+ goto loser;
+ };
+
+ // We verify the first signer info, only //
+ // XXX: NSS_CMSSignedData_VerifySignerInfo calls CERT_VerifyCert, which
+ // requires NSS's certificate verification configuration to be done in
+ // order to work well (e.g. honoring OCSP preferences and proxy settings
+ // for OCSP requests), but Gecko stopped doing that configuration. Something
+ // similar to what was done for Gecko bug 1028643 needs to be done here too.
+ if (NSS_CMSSignedData_VerifySignerInfo(sigd, 0, CERT_GetDefaultCertDB(),
+ certUsageEmailSigner) != SECSuccess) {
+ MOZ_LOG(
+ gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - unable to verify signature"));
+
+ if (NSSCMSVS_SigningCertNotFound == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - signing cert not found"));
+ rv = NS_ERROR_CMS_VERIFY_NOCERT;
+ } else if (NSSCMSVS_SigningCertNotTrusted == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - signing cert not trusted "
+ "at signing time"));
+ rv = NS_ERROR_CMS_VERIFY_UNTRUSTED;
+ } else if (NSSCMSVS_Unverified == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - can not verify"));
+ rv = NS_ERROR_CMS_VERIFY_ERROR_UNVERIFIED;
+ } else if (NSSCMSVS_ProcessingError == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - processing error"));
+ rv = NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
+ } else if (NSSCMSVS_BadSignature == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - bad signature"));
+ rv = NS_ERROR_CMS_VERIFY_BAD_SIGNATURE;
+ } else if (NSSCMSVS_DigestMismatch == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - digest mismatch"));
+ rv = NS_ERROR_CMS_VERIFY_DIGEST_MISMATCH;
+ } else if (NSSCMSVS_SignatureAlgorithmUnknown == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - algo unknown"));
+ rv = NS_ERROR_CMS_VERIFY_UNKNOWN_ALGO;
+ } else if (NSSCMSVS_SignatureAlgorithmUnsupported ==
+ si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - algo not supported"));
+ rv = NS_ERROR_CMS_VERIFY_UNSUPPORTED_ALGO;
+ } else if (NSSCMSVS_MalformedSignature == si->verificationStatus) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - malformed signature"));
+ rv = NS_ERROR_CMS_VERIFY_MALFORMED_SIGNATURE;
+ }
+
+ goto loser;
+ }
+
+ // Save the profile. Note that save import failure is not a signature
+ // verification failure. //
+ if (NSS_SMIMESignerInfo_SaveSMIMEProfile(si) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CommonVerifySignature - unable to save smime "
+ "profile"));
+ }
+
+ rv = NS_OK;
+loser:
+ return rv;
+}
+
+NS_IMETHODIMP nsCMSMessage::AsyncVerifySignature(
+ int32_t verifyFlags, nsISMimeVerificationListener* aListener) {
+ return CommonAsyncVerifySignature(verifyFlags, aListener, {}, 0);
+}
+
+NS_IMETHODIMP nsCMSMessage::AsyncVerifyDetachedSignature(
+ int32_t verifyFlags, nsISMimeVerificationListener* aListener,
+ const nsTArray<uint8_t>& aDigestData, int16_t aDigestType) {
+ if (aDigestData.IsEmpty()) return NS_ERROR_FAILURE;
+
+ return CommonAsyncVerifySignature(verifyFlags, aListener, aDigestData,
+ aDigestType);
+}
+
+class SMimeVerificationTask final : public CryptoTask {
+ public:
+ SMimeVerificationTask(nsICMSMessage* aMessage, int32_t verifyFlags,
+ nsISMimeVerificationListener* aListener,
+ const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType)
+ : mMessage(aMessage),
+ mListener(aListener),
+ mDigestData(aDigestData.Clone()),
+ mDigestType(aDigestType),
+ mVerifyFlags(verifyFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ private:
+ virtual nsresult CalculateResult() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Because the S/MIME code and related certificate processing isn't
+ // sufficiently threadsafe (see bug 1529003), we want this code to
+ // never run in parallel (see bug 1386601).
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ nsresult rv;
+ if (mDigestData.IsEmpty()) {
+ rv = mMessage->VerifySignature(mVerifyFlags);
+ } else {
+ rv = mMessage->VerifyDetachedSignature(mVerifyFlags, mDigestData,
+ mDigestType);
+ }
+
+ return rv;
+ }
+ virtual void CallCallback(nsresult rv) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mListener->Notify(mMessage, rv);
+ }
+
+ nsCOMPtr<nsICMSMessage> mMessage;
+ nsCOMPtr<nsISMimeVerificationListener> mListener;
+ nsTArray<uint8_t> mDigestData;
+ int16_t mDigestType;
+ int32_t mVerifyFlags;
+
+ static mozilla::StaticMutex sMutex;
+};
+
+mozilla::StaticMutex SMimeVerificationTask::sMutex;
+
+nsresult nsCMSMessage::CommonAsyncVerifySignature(
+ int32_t verifyFlags, nsISMimeVerificationListener* aListener,
+ const nsTArray<uint8_t>& aDigestData, int16_t aDigestType) {
+ RefPtr<CryptoTask> task = new SMimeVerificationTask(
+ this, verifyFlags, aListener, aDigestData, aDigestType);
+ return task->Dispatch();
+}
+
+class nsZeroTerminatedCertArray {
+ public:
+ nsZeroTerminatedCertArray() : mCerts(nullptr), mPoolp(nullptr), mSize(0) {}
+
+ ~nsZeroTerminatedCertArray() {
+ if (mCerts) {
+ for (uint32_t i = 0; i < mSize; i++) {
+ if (mCerts[i]) {
+ CERT_DestroyCertificate(mCerts[i]);
+ }
+ }
+ }
+
+ if (mPoolp) PORT_FreeArena(mPoolp, false);
+ }
+
+ bool allocate(uint32_t count) {
+ // only allow allocation once
+ if (mPoolp) return false;
+
+ mSize = count;
+
+ if (!mSize) return false;
+
+ mPoolp = PORT_NewArena(1024);
+ if (!mPoolp) return false;
+
+ mCerts = (CERTCertificate**)PORT_ArenaZAlloc(
+ mPoolp, (count + 1) * sizeof(CERTCertificate*));
+
+ if (!mCerts) return false;
+
+ // null array, including zero termination
+ for (uint32_t i = 0; i < count + 1; i++) {
+ mCerts[i] = nullptr;
+ }
+
+ return true;
+ }
+
+ void set(uint32_t i, CERTCertificate* c) {
+ if (i >= mSize) return;
+
+ if (mCerts[i]) {
+ CERT_DestroyCertificate(mCerts[i]);
+ }
+
+ mCerts[i] = CERT_DupCertificate(c);
+ }
+
+ CERTCertificate* get(uint32_t i) {
+ if (i >= mSize) return nullptr;
+
+ return CERT_DupCertificate(mCerts[i]);
+ }
+
+ CERTCertificate** getRawArray() { return mCerts; }
+
+ private:
+ CERTCertificate** mCerts;
+ PLArenaPool* mPoolp;
+ uint32_t mSize;
+};
+
+NS_IMETHODIMP nsCMSMessage::CreateEncrypted(
+ const nsTArray<RefPtr<nsIX509Cert>>& aRecipientCerts) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::CreateEncrypted"));
+ NSSCMSContentInfo* cinfo;
+ NSSCMSEnvelopedData* envd;
+ NSSCMSRecipientInfo* recipientInfo;
+ nsZeroTerminatedCertArray recipientCerts;
+ SECOidTag bulkAlgTag;
+ int keySize;
+ uint32_t i;
+ nsresult rv = NS_ERROR_FAILURE;
+
+ // Check the recipient certificates //
+ uint32_t recipientCertCount = aRecipientCerts.Length();
+ PR_ASSERT(recipientCertCount > 0);
+
+ if (!recipientCerts.allocate(recipientCertCount)) {
+ goto loser;
+ }
+
+ for (i = 0; i < recipientCertCount; i++) {
+ nsIX509Cert* x509cert = aRecipientCerts[i];
+
+ if (!x509cert) return NS_ERROR_FAILURE;
+
+ UniqueCERTCertificate c(x509cert->GetCert());
+ recipientCerts.set(i, c.get());
+ }
+
+ // Find a bulk key algorithm //
+ if (NSS_SMIMEUtil_FindBulkAlgForRecipients(
+ recipientCerts.getRawArray(), &bulkAlgTag, &keySize) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't find bulk alg for "
+ "recipients"));
+ rv = NS_ERROR_CMS_ENCRYPT_NO_BULK_ALG;
+ goto loser;
+ }
+
+ m_cmsMsg = NSS_CMSMessage_Create(nullptr);
+ if (!m_cmsMsg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't create new cms message"));
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto loser;
+ }
+
+ if ((envd = NSS_CMSEnvelopedData_Create(m_cmsMsg, bulkAlgTag, keySize)) ==
+ nullptr) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't create enveloped data"));
+ goto loser;
+ }
+
+ cinfo = NSS_CMSMessage_GetContentInfo(m_cmsMsg);
+ if (NSS_CMSContentInfo_SetContent_EnvelopedData(m_cmsMsg, cinfo, envd) !=
+ SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't create content enveloped "
+ "data"));
+ goto loser;
+ }
+
+ cinfo = NSS_CMSEnvelopedData_GetContentInfo(envd);
+ if (NSS_CMSContentInfo_SetContent_Data(m_cmsMsg, cinfo, nullptr, false) !=
+ SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't set content data"));
+ goto loser;
+ }
+
+ // Create and attach recipient information //
+ for (i = 0; i < recipientCertCount; i++) {
+ UniqueCERTCertificate rc(recipientCerts.get(i));
+ if ((recipientInfo = NSS_CMSRecipientInfo_Create(m_cmsMsg, rc.get())) ==
+ nullptr) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't create recipient info"));
+ goto loser;
+ }
+ if (NSS_CMSEnvelopedData_AddRecipient(envd, recipientInfo) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateEncrypted - can't add recipient info"));
+ goto loser;
+ }
+ }
+
+ return NS_OK;
+loser:
+ if (m_cmsMsg) {
+ NSS_CMSMessage_Destroy(m_cmsMsg);
+ m_cmsMsg = nullptr;
+ }
+
+ return rv;
+}
+
+bool nsCMSMessage::IsAllowedHash(const int16_t aCryptoHashInt) {
+ switch (aCryptoHashInt) {
+ case nsICryptoHash::SHA1:
+ case nsICryptoHash::SHA256:
+ case nsICryptoHash::SHA384:
+ case nsICryptoHash::SHA512:
+ return true;
+ default:
+ return false;
+ }
+}
+
+NS_IMETHODIMP
+nsCMSMessage::CreateSigned(nsIX509Cert* aSigningCert, nsIX509Cert* aEncryptCert,
+ const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType) {
+ NS_ENSURE_ARG(aSigningCert);
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSMessage::CreateSigned"));
+ NSSCMSContentInfo* cinfo;
+ NSSCMSSignedData* sigd;
+ NSSCMSSignerInfo* signerinfo;
+ UniqueCERTCertificate scert(aSigningCert->GetCert());
+ UniqueCERTCertificate ecert;
+ nsresult rv = NS_ERROR_FAILURE;
+
+ if (!scert) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aEncryptCert) {
+ ecert = UniqueCERTCertificate(aEncryptCert->GetCert());
+ }
+
+ if (!IsAllowedHash(aDigestType)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ SECOidTag digestType =
+ HASH_GetHashOidTagByHashType(static_cast<HASH_HashType>(aDigestType));
+ if (digestType == SEC_OID_UNKNOWN) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ /*
+ * create the message object
+ */
+ m_cmsMsg =
+ NSS_CMSMessage_Create(nullptr); /* create a message on its own pool */
+ if (!m_cmsMsg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't create new message"));
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto loser;
+ }
+
+ /*
+ * build chain of objects: message->signedData->data
+ */
+ if ((sigd = NSS_CMSSignedData_Create(m_cmsMsg)) == nullptr) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't create signed data"));
+ goto loser;
+ }
+ cinfo = NSS_CMSMessage_GetContentInfo(m_cmsMsg);
+ if (NSS_CMSContentInfo_SetContent_SignedData(m_cmsMsg, cinfo, sigd) !=
+ SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't set content signed data"));
+ goto loser;
+ }
+
+ cinfo = NSS_CMSSignedData_GetContentInfo(sigd);
+
+ /* we're always passing data in and detaching optionally */
+ if (NSS_CMSContentInfo_SetContent_Data(m_cmsMsg, cinfo, nullptr, true) !=
+ SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't set content data"));
+ goto loser;
+ }
+
+ /*
+ * create & attach signer information
+ */
+ signerinfo = NSS_CMSSignerInfo_Create(m_cmsMsg, scert.get(), digestType);
+ if (!signerinfo) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't create signer info"));
+ goto loser;
+ }
+
+ /* we want the cert chain included for this one */
+ if (NSS_CMSSignerInfo_IncludeCerts(signerinfo, NSSCMSCM_CertChain,
+ certUsageEmailSigner) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't include signer cert chain"));
+ goto loser;
+ }
+
+ if (NSS_CMSSignerInfo_AddSigningTime(signerinfo, PR_Now()) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add signing time"));
+ goto loser;
+ }
+
+ if (NSS_CMSSignerInfo_AddSMIMECaps(signerinfo) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add smime caps"));
+ goto loser;
+ }
+
+ if (ecert) {
+ if (NSS_CMSSignerInfo_AddSMIMEEncKeyPrefs(
+ signerinfo, ecert.get(), CERT_GetDefaultCertDB()) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add smime enc key prefs"));
+ goto loser;
+ }
+
+ if (NSS_CMSSignerInfo_AddMSSMIMEEncKeyPrefs(
+ signerinfo, ecert.get(), CERT_GetDefaultCertDB()) != SECSuccess) {
+ MOZ_LOG(
+ gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add MS smime enc key prefs"));
+ goto loser;
+ }
+
+ // If signing and encryption cert are identical, don't add it twice.
+ bool addEncryptionCert =
+ (ecert && (!scert || !CERT_CompareCerts(ecert.get(), scert.get())));
+
+ if (addEncryptionCert &&
+ NSS_CMSSignedData_AddCertificate(sigd, ecert.get()) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add own encryption "
+ "certificate"));
+ goto loser;
+ }
+ }
+
+ if (NSS_CMSSignedData_AddSignerInfo(sigd, signerinfo) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't add signer info"));
+ goto loser;
+ }
+
+ // Finally, add the pre-computed digest if passed in
+ if (!aDigestData.IsEmpty()) {
+ SECItem digest;
+
+ // NSS_CMSSignedData_SetDigestValue() takes a copy and won't mutate our
+ // data, so we're OK to cast away the const here.
+ digest.data = const_cast<uint8_t*>(aDigestData.Elements());
+ digest.len = aDigestData.Length();
+ if (NSS_CMSSignedData_SetDigestValue(sigd, digestType, &digest) !=
+ SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSMessage::CreateSigned - can't set digest value"));
+ goto loser;
+ }
+ }
+
+ return NS_OK;
+loser:
+ if (m_cmsMsg) {
+ NSS_CMSMessage_Destroy(m_cmsMsg);
+ m_cmsMsg = nullptr;
+ }
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsCMSDecoder, nsICMSDecoder)
+NS_IMPL_ISUPPORTS(nsCMSDecoderJS, nsICMSDecoderJS)
+
+nsCMSDecoder::nsCMSDecoder() : m_dcx(nullptr) {}
+nsCMSDecoderJS::nsCMSDecoderJS() : m_dcx(nullptr) {}
+
+nsCMSDecoder::~nsCMSDecoder() {
+ if (m_dcx) {
+ NSS_CMSDecoder_Cancel(m_dcx);
+ m_dcx = nullptr;
+ }
+}
+
+nsCMSDecoderJS::~nsCMSDecoderJS() {
+ if (m_dcx) {
+ NSS_CMSDecoder_Cancel(m_dcx);
+ m_dcx = nullptr;
+ }
+}
+
+nsresult nsCMSDecoder::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> nssInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+nsresult nsCMSDecoderJS::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> nssInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+/* void start (in NSSCMSContentCallback cb, in voidPtr arg); */
+NS_IMETHODIMP nsCMSDecoder::Start(NSSCMSContentCallback cb, void* arg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSDecoder::Start"));
+ m_ctx = new PipUIContext();
+
+ m_dcx = NSS_CMSDecoder_Start(0, cb, arg, 0, m_ctx, 0, 0);
+ if (!m_dcx) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSDecoder::Start - can't start decoder"));
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+/* void update (in string bug, in long len); */
+NS_IMETHODIMP nsCMSDecoder::Update(const char* buf, int32_t len) {
+ NSS_CMSDecoder_Update(m_dcx, (char*)buf, len);
+ return NS_OK;
+}
+
+/* void finish (); */
+NS_IMETHODIMP nsCMSDecoder::Finish(nsICMSMessage** aCMSMsg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSDecoder::Finish"));
+ NSSCMSMessage* cmsMsg;
+ cmsMsg = NSS_CMSDecoder_Finish(m_dcx);
+ m_dcx = nullptr;
+ if (cmsMsg) {
+ nsCMSMessage* obj = new nsCMSMessage(cmsMsg);
+ // The NSS object cmsMsg still carries a reference to the context
+ // we gave it on construction.
+ // Make sure the context will live long enough.
+ obj->referenceContext(m_ctx);
+ NS_ADDREF(*aCMSMsg = obj);
+ }
+ return NS_OK;
+}
+
+void nsCMSDecoderJS::content_callback(void* arg, const char* input,
+ unsigned long length) {
+ nsCMSDecoderJS* self = reinterpret_cast<nsCMSDecoderJS*>(arg);
+ self->mDecryptedData.AppendElements(input, length);
+}
+
+NS_IMETHODIMP nsCMSDecoderJS::Decrypt(const nsTArray<uint8_t>& aInput,
+ nsTArray<uint8_t>& _retval) {
+ if (aInput.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ m_ctx = new PipUIContext();
+
+ m_dcx = NSS_CMSDecoder_Start(0, nsCMSDecoderJS::content_callback, this, 0,
+ m_ctx, 0, 0);
+ if (!m_dcx) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSDecoderJS::Start - can't start decoder"));
+ return NS_ERROR_FAILURE;
+ }
+
+ NSS_CMSDecoder_Update(m_dcx, (char*)aInput.Elements(), aInput.Length());
+
+ NSSCMSMessage* cmsMsg;
+ cmsMsg = NSS_CMSDecoder_Finish(m_dcx);
+ m_dcx = nullptr;
+ if (cmsMsg) {
+ nsCMSMessage* obj = new nsCMSMessage(cmsMsg);
+ // The NSS object cmsMsg still carries a reference to the context
+ // we gave it on construction.
+ // Make sure the context will live long enough.
+ obj->referenceContext(m_ctx);
+ mCMSMessage = obj;
+ }
+
+ _retval = mDecryptedData.Clone();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsCMSEncoder, nsICMSEncoder)
+
+nsCMSEncoder::nsCMSEncoder() : m_ecx(nullptr) {}
+
+nsCMSEncoder::~nsCMSEncoder() {
+ if (m_ecx) NSS_CMSEncoder_Cancel(m_ecx);
+}
+
+nsresult nsCMSEncoder::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> nssInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+/* void start (); */
+NS_IMETHODIMP nsCMSEncoder::Start(nsICMSMessage* aMsg, NSSCMSContentCallback cb,
+ void* arg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSEncoder::Start"));
+ nsCMSMessage* cmsMsg = static_cast<nsCMSMessage*>(aMsg);
+ m_ctx = new PipUIContext();
+
+ m_ecx = NSS_CMSEncoder_Start(cmsMsg->getCMS(), cb, arg, 0, 0, 0, m_ctx, 0, 0,
+ 0, 0);
+ if (!m_ecx) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSEncoder::Start - can't start encoder"));
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+/* void update (in string aBuf, in long aLen); */
+NS_IMETHODIMP nsCMSEncoder::Update(const char* aBuf, int32_t aLen) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSEncoder::Update"));
+ if (!m_ecx || NSS_CMSEncoder_Update(m_ecx, aBuf, aLen) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSEncoder::Update - can't update encoder"));
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+/* void finish (); */
+NS_IMETHODIMP nsCMSEncoder::Finish() {
+ nsresult rv = NS_OK;
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSEncoder::Finish"));
+ if (!m_ecx || NSS_CMSEncoder_Finish(m_ecx) != SECSuccess) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug,
+ ("nsCMSEncoder::Finish - can't finish encoder"));
+ rv = NS_ERROR_FAILURE;
+ }
+ m_ecx = nullptr;
+ return rv;
+}
+
+/* void encode (in nsICMSMessage aMsg); */
+NS_IMETHODIMP nsCMSEncoder::Encode(nsICMSMessage* aMsg) {
+ MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSEncoder::Encode"));
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/extensions/smime/nsCMS.h b/comm/mailnews/extensions/smime/nsCMS.h
new file mode 100644
index 0000000000..14ed2cd07f
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCMS.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __NS_CMS_H__
+#define __NS_CMS_H__
+
+#include "nsISupports.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsICMSMessage.h"
+#include "nsICMSEncoder.h"
+#include "nsICMSDecoder.h"
+#include "nsICMSDecoderJS.h"
+#include "sechash.h"
+#include "cms.h"
+
+#define NS_CMSMESSAGE_CID \
+ { \
+ 0xa4557478, 0xae16, 0x11d5, { \
+ 0xba, 0x4b, 0x00, 0x10, 0x83, 0x03, 0xb1, 0x17 \
+ } \
+ }
+
+class nsCMSMessage : public nsICMSMessage {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICMSMESSAGE
+
+ nsCMSMessage();
+ explicit nsCMSMessage(NSSCMSMessage* aCMSMsg);
+ nsresult Init();
+
+ void referenceContext(nsIInterfaceRequestor* aContext) { m_ctx = aContext; }
+ NSSCMSMessage* getCMS() { return m_cmsMsg; }
+
+ private:
+ virtual ~nsCMSMessage();
+ nsCOMPtr<nsIInterfaceRequestor> m_ctx;
+ NSSCMSMessage* m_cmsMsg;
+ NSSCMSSignerInfo* GetTopLevelSignerInfo();
+ nsresult CommonVerifySignature(int32_t verifyFlags,
+ const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType);
+
+ nsresult CommonAsyncVerifySignature(int32_t verifyFlags,
+ nsISMimeVerificationListener* aListener,
+ const nsTArray<uint8_t>& aDigestData,
+ int16_t aDigestType);
+ bool IsAllowedHash(const int16_t aCryptoHashInt);
+};
+
+// ===============================================
+// nsCMSDecoder - implementation of nsICMSDecoder
+// ===============================================
+
+#define NS_CMSDECODER_CID \
+ { \
+ 0x9dcef3a4, 0xa3bc, 0x11d5, { \
+ 0xba, 0x47, 0x00, 0x10, 0x83, 0x03, 0xb1, 0x17 \
+ } \
+ }
+
+class nsCMSDecoder : public nsICMSDecoder {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICMSDECODER
+
+ nsCMSDecoder();
+ nsresult Init();
+
+ private:
+ virtual ~nsCMSDecoder();
+ nsCOMPtr<nsIInterfaceRequestor> m_ctx;
+ NSSCMSDecoderContext* m_dcx;
+};
+
+class nsCMSDecoderJS : public nsICMSDecoderJS {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICMSDECODERJS
+
+ nsCMSDecoderJS();
+ nsresult Init();
+
+ private:
+ virtual ~nsCMSDecoderJS();
+ nsCOMPtr<nsIInterfaceRequestor> m_ctx;
+ NSSCMSDecoderContext* m_dcx;
+
+ nsTArray<uint8_t> mDecryptedData;
+ nsCOMPtr<nsICMSMessage> mCMSMessage;
+
+ static void content_callback(void* arg, const char* input,
+ unsigned long length);
+};
+
+// ===============================================
+// nsCMSEncoder - implementation of nsICMSEncoder
+// ===============================================
+
+#define NS_CMSENCODER_CID \
+ { \
+ 0xa15789aa, 0x8903, 0x462b, { \
+ 0x81, 0xe9, 0x4a, 0xa2, 0xcf, 0xf4, 0xd5, 0xcb \
+ } \
+ }
+class nsCMSEncoder : public nsICMSEncoder {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICMSENCODER
+
+ nsCMSEncoder();
+ nsresult Init();
+
+ private:
+ virtual ~nsCMSEncoder();
+ nsCOMPtr<nsIInterfaceRequestor> m_ctx;
+ NSSCMSEncoderContext* m_ecx;
+};
+
+#endif
diff --git a/comm/mailnews/extensions/smime/nsCMSSecureMessage.cpp b/comm/mailnews/extensions/smime/nsCMSSecureMessage.cpp
new file mode 100644
index 0000000000..c2c220a2f4
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCMSSecureMessage.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCMSSecureMessage.h"
+
+#include <string.h>
+
+#include "ScopedNSSTypes.h"
+#include "SharedCertVerifier.h"
+#include "cms.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsDependentSubstring.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsServiceManagerUtils.h"
+#include "nsISupports.h"
+#include "nsIX509Cert.h"
+#include "nsIX509CertDB.h"
+#include "nsNSSComponent.h"
+#include "nsNSSHelper.h"
+#include "plbase64.h"
+
+using namespace mozilla;
+using namespace mozilla::psm;
+
+// Standard ISupports implementation
+// NOTE: Should these be the thread-safe versions?
+
+/*****
+ * nsCMSSecureMessage
+ *****/
+
+// Standard ISupports implementation
+NS_IMPL_ISUPPORTS(nsCMSSecureMessage, nsICMSSecureMessage)
+
+// nsCMSSecureMessage constructor
+nsCMSSecureMessage::nsCMSSecureMessage() {
+ // initialize superclass
+}
+
+// nsCMSMessage destructor
+nsCMSSecureMessage::~nsCMSSecureMessage() {}
+
+nsresult nsCMSSecureMessage::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> nssInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+nsresult nsCMSSecureMessage::CheckUsageOk(nsIX509Cert* aCert,
+ SECCertificateUsage aUsage,
+ bool* aCanBeUsed) {
+ NS_ENSURE_ARG_POINTER(aCert);
+ *aCanBeUsed = false;
+
+ nsresult rv;
+ nsCOMPtr<nsIX509CertDB> certdb =
+ do_GetService("@mozilla.org/security/x509certdb;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<uint8_t> certBytes;
+ rv = aCert->GetRawDER(certBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
+ NS_ENSURE_TRUE(certVerifier, NS_ERROR_UNEXPECTED);
+
+ nsTArray<nsTArray<uint8_t>> unusedBuiltChain;
+ // It's fine to skip OCSP, because this is called only from code
+ // for selecting the user's own configured cert.
+ if (certVerifier->VerifyCert(certBytes, aUsage, mozilla::pkix::Now(), nullptr,
+ nullptr, unusedBuiltChain,
+ CertVerifier::FLAG_LOCAL_ONLY) ==
+ mozilla::pkix::Success) {
+ *aCanBeUsed = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCMSSecureMessage::CanBeUsedForEmailEncryption(
+ nsIX509Cert* aCert, bool* aCanBeUsed) {
+ return CheckUsageOk(aCert, certificateUsageEmailRecipient, aCanBeUsed);
+}
+
+NS_IMETHODIMP nsCMSSecureMessage::CanBeUsedForEmailSigning(nsIX509Cert* aCert,
+ bool* aCanBeUsed) {
+ return CheckUsageOk(aCert, certificateUsageEmailSigner, aCanBeUsed);
+}
diff --git a/comm/mailnews/extensions/smime/nsCMSSecureMessage.h b/comm/mailnews/extensions/smime/nsCMSSecureMessage.h
new file mode 100644
index 0000000000..186dba525f
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCMSSecureMessage.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsCMSSecureMessage_h
+#define nsCMSSecureMessage_h
+
+#include "nsICMSSecureMessage.h"
+#include "cert.h"
+
+// ===============================================
+// nsCMSManager - implementation of nsICMSManager
+// ===============================================
+
+#define NS_CMSSECUREMESSAGE_CID \
+ { \
+ 0x5fb907e0, 0x1dd2, 0x11b2, { \
+ 0xa7, 0xc0, 0xf1, 0x4c, 0x41, 0x6a, 0x62, 0xa1 \
+ } \
+ }
+
+class nsCMSSecureMessage : public nsICMSSecureMessage {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICMSSECUREMESSAGE
+
+ nsCMSSecureMessage();
+ nsresult Init();
+
+ private:
+ virtual ~nsCMSSecureMessage();
+ nsresult encode(const unsigned char* data, int32_t dataLen, char** _retval);
+ nsresult decode(const char* data, unsigned char** result, int32_t* _retval);
+ nsresult CheckUsageOk(nsIX509Cert* cert, SECCertificateUsage usage,
+ bool* _retval);
+};
+
+#endif // nsCMSSecureMessage_h
diff --git a/comm/mailnews/extensions/smime/nsCertPicker.cpp b/comm/mailnews/extensions/smime/nsCertPicker.cpp
new file mode 100644
index 0000000000..7224762eef
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCertPicker.cpp
@@ -0,0 +1,410 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCertPicker.h"
+
+#include "MainThreadUtils.h"
+#include "ScopedNSSTypes.h"
+#include "cert.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsICertPickDialogs.h"
+#include "nsIDialogParamBlock.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIX509CertValidity.h"
+#include "nsMemory.h"
+#include "nsMsgComposeSecure.h"
+#include "nsNSSCertificate.h"
+#include "nsNSSComponent.h"
+#include "nsNSSDialogHelper.h"
+#include "nsNSSHelper.h"
+#include "nsNSSCertHelper.h"
+#include "nsReadableUtils.h"
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsString.h"
+#include "mozpkix/pkixtypes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Unused.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+using namespace mozilla;
+
+// Copied from security/manager/ssl/nsCertTree.cpp
+static void PRTimeToLocalDateString(PRTime time, nsAString& result) {
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(time, PR_LocalTimeParameters, &explodedTime);
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ style.time = mozilla::Nothing();
+ mozilla::Unused << intl::AppDateTimeFormat::Format(style, &explodedTime,
+ result);
+}
+
+MOZ_TYPE_SPECIFIC_UNIQUE_PTR_TEMPLATE(UniqueCERTCertNicknames,
+ CERTCertNicknames, CERT_FreeNicknames)
+
+CERTCertNicknames* getNSSCertNicknamesFromCertList(
+ const UniqueCERTCertList& certList) {
+ nsAutoString expiredString, notYetValidString;
+ nsAutoString expiredStringLeadingSpace, notYetValidStringLeadingSpace;
+
+ GetPIPNSSBundleString("NicknameExpired", expiredString);
+ GetPIPNSSBundleString("NicknameNotYetValid", notYetValidString);
+
+ expiredStringLeadingSpace.Append(' ');
+ expiredStringLeadingSpace.Append(expiredString);
+
+ notYetValidStringLeadingSpace.Append(' ');
+ notYetValidStringLeadingSpace.Append(notYetValidString);
+
+ NS_ConvertUTF16toUTF8 aUtf8ExpiredString(expiredStringLeadingSpace);
+ NS_ConvertUTF16toUTF8 aUtf8NotYetValidString(notYetValidStringLeadingSpace);
+
+ return CERT_NicknameStringsFromCertList(
+ certList.get(), const_cast<char*>(aUtf8ExpiredString.get()),
+ const_cast<char*>(aUtf8NotYetValidString.get()));
+}
+
+nsresult FormatUIStrings(nsIX509Cert* cert, const nsAutoString& nickname,
+ nsAutoString& nickWithSerial, nsAutoString& details) {
+ if (!NS_IsMainThread()) {
+ NS_ERROR("nsNSSCertificate::FormatUIStrings called off the main thread");
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ RefPtr<nsMsgComposeSecure> mcs = new nsMsgComposeSecure;
+ if (!mcs) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString info;
+ nsAutoString temp1;
+
+ nickWithSerial.Append(nickname);
+
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoIssuedFor", info))) {
+ details.Append(info);
+ details.Append(char16_t(' '));
+ if (NS_SUCCEEDED(cert->GetSubjectName(temp1)) && !temp1.IsEmpty()) {
+ details.Append(temp1);
+ }
+ details.Append(char16_t('\n'));
+ }
+
+ if (NS_SUCCEEDED(cert->GetSerialNumber(temp1)) && !temp1.IsEmpty()) {
+ details.AppendLiteral(" ");
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertDumpSerialNo", info))) {
+ details.Append(info);
+ details.AppendLiteral(": ");
+ }
+ details.Append(temp1);
+
+ nickWithSerial.AppendLiteral(" [");
+ nickWithSerial.Append(temp1);
+ nickWithSerial.Append(char16_t(']'));
+
+ details.Append(char16_t('\n'));
+ }
+
+ nsCOMPtr<nsIX509CertValidity> validity;
+ nsresult rv = cert->GetValidity(getter_AddRefs(validity));
+ if (NS_SUCCEEDED(rv) && validity) {
+ details.AppendLiteral(" ");
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoValid", info))) {
+ details.Append(info);
+ }
+
+ PRTime notBefore;
+ rv = validity->GetNotBefore(&notBefore);
+ if (NS_SUCCEEDED(rv)) {
+ details.Append(char16_t(' '));
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoFrom", info))) {
+ details.Append(info);
+ details.Append(char16_t(' '));
+ }
+ PRTimeToLocalDateString(notBefore, temp1);
+ details.Append(temp1);
+ }
+
+ PRTime notAfter;
+ rv = validity->GetNotAfter(&notAfter);
+ if (NS_SUCCEEDED(rv)) {
+ details.Append(char16_t(' '));
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoTo", info))) {
+ details.Append(info);
+ details.Append(char16_t(' '));
+ }
+ PRTimeToLocalDateString(notAfter, temp1);
+ details.Append(temp1);
+ }
+ details.Append(char16_t('\n'));
+ }
+
+ UniqueCERTCertificate nssCert(cert->GetCert());
+ if (!nssCert) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString firstEmail;
+ const char* aWalkAddr;
+ for (aWalkAddr = CERT_GetFirstEmailAddress(nssCert.get()); aWalkAddr;
+ aWalkAddr = CERT_GetNextEmailAddress(nssCert.get(), aWalkAddr)) {
+ NS_ConvertUTF8toUTF16 email(aWalkAddr);
+ if (email.IsEmpty()) continue;
+
+ if (firstEmail.IsEmpty()) {
+ // If the first email address from the subject DN is also present
+ // in the subjectAltName extension, GetEmailAddresses() will return
+ // it twice (as received from NSS). Remember the first address so that
+ // we can filter out duplicates later on.
+ firstEmail = email;
+
+ details.AppendLiteral(" ");
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoEmail", info))) {
+ details.Append(info);
+ details.AppendLiteral(": ");
+ }
+ details.Append(email);
+ } else {
+ // Append current address if it's different from the first one.
+ if (!firstEmail.Equals(email)) {
+ details.AppendLiteral(", ");
+ details.Append(email);
+ }
+ }
+ }
+
+ if (!firstEmail.IsEmpty()) {
+ // We got at least one email address, so we want a newline
+ details.Append(char16_t('\n'));
+ }
+
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoIssuedBy", info))) {
+ details.Append(info);
+ details.Append(char16_t(' '));
+
+ if (NS_SUCCEEDED(cert->GetIssuerName(temp1)) && !temp1.IsEmpty()) {
+ details.Append(temp1);
+ }
+
+ details.Append(char16_t('\n'));
+ }
+
+ if (NS_SUCCEEDED(mcs->GetSMIMEBundleString(u"CertInfoStoredIn", info))) {
+ details.Append(info);
+ details.Append(char16_t(' '));
+
+ if (NS_SUCCEEDED(cert->GetTokenName(temp1)) && !temp1.IsEmpty()) {
+ details.Append(temp1);
+ }
+ }
+
+ // the above produces the following output:
+ //
+ // Issued to: $subjectName
+ // Serial number: $serialNumber
+ // Valid from: $starting_date to $expiration_date
+ // Certificate Key usage: $usages
+ // Email: $address(es)
+ // Issued by: $issuerName
+ // Stored in: $token
+
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsCertPicker, nsICertPickDialogs, nsIUserCertPicker)
+
+nsCertPicker::nsCertPicker() {}
+
+nsCertPicker::~nsCertPicker() {}
+
+nsresult nsCertPicker::Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> psm = do_GetService("@mozilla.org/psm;1", &rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsCertPicker::PickCertificate(nsIInterfaceRequestor* ctx,
+ const nsTArray<nsString>& certNickList,
+ const nsTArray<nsString>& certDetailsList,
+ int32_t* selectedIndex, bool* canceled) {
+ nsresult rv;
+ uint32_t i;
+ MOZ_ASSERT(certNickList.Length() == certDetailsList.Length());
+ const uint32_t count = certNickList.Length();
+
+ *canceled = false;
+
+ nsCOMPtr<nsIDialogParamBlock> block =
+ do_CreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID);
+ if (!block) return NS_ERROR_FAILURE;
+
+ block->SetNumberStrings(1 + count * 2);
+
+ for (i = 0; i < count; i++) {
+ rv = block->SetString(i, ToNewUnicode(certNickList[i]));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ for (i = 0; i < count; i++) {
+ rv = block->SetString(i + count, ToNewUnicode(certDetailsList[i]));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = block->SetInt(0, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = block->SetInt(1, *selectedIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsNSSDialogHelper::openDialog(
+ nullptr, "chrome://messenger/content/certpicker.xhtml", block);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t status;
+
+ rv = block->GetInt(0, &status);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *canceled = (status == 0) ? true : false;
+ if (!*canceled) {
+ rv = block->GetInt(1, selectedIndex);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsCertPicker::PickByUsage(nsIInterfaceRequestor* ctx,
+ const char16_t* selectedNickname,
+ int32_t certUsage, bool allowInvalid,
+ bool allowDuplicateNicknames,
+ const nsAString& emailAddress,
+ bool* canceled, nsIX509Cert** _retval) {
+ int32_t selectedIndex = -1;
+ bool selectionFound = false;
+ CERTCertListNode* node = nullptr;
+ nsresult rv = NS_OK;
+
+ {
+ // Iterate over all certs. This assures that user is logged in to all
+ // hardware tokens.
+ nsCOMPtr<nsIInterfaceRequestor> ctx = new PipUIContext();
+ UniqueCERTCertList allcerts(PK11_ListCerts(PK11CertListUnique, ctx));
+ }
+
+ /* find all user certs that are valid for the specified usage */
+ /* note that we are allowing expired certs in this list */
+ UniqueCERTCertList certList(CERT_FindUserCertsByUsage(
+ CERT_GetDefaultCertDB(), (SECCertUsage)certUsage,
+ !allowDuplicateNicknames, !allowInvalid, ctx));
+ if (!certList) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ /* if a (non-empty) emailAddress argument is supplied to PickByUsage, */
+ /* remove non-matching certificates from the candidate list */
+
+ if (!emailAddress.IsEmpty()) {
+ node = CERT_LIST_HEAD(certList);
+ while (!CERT_LIST_END(node, certList)) {
+ /* if the cert has at least one e-mail address, check if suitable */
+ if (CERT_GetFirstEmailAddress(node->cert)) {
+ RefPtr<nsNSSCertificate> tempCert(new nsNSSCertificate(node->cert));
+ bool match = false;
+ rv = tempCert->ContainsEmailAddress(emailAddress, &match);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!match) {
+ /* doesn't contain the specified address, so remove from the list */
+ CERTCertListNode* freenode = node;
+ node = CERT_LIST_NEXT(node);
+ CERT_RemoveCertListNode(freenode);
+ continue;
+ }
+ }
+ node = CERT_LIST_NEXT(node);
+ }
+ }
+
+ UniqueCERTCertNicknames nicknames(getNSSCertNicknamesFromCertList(certList));
+ if (!nicknames) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsTArray<nsString> certNicknameList(nicknames->numnicknames);
+ nsTArray<nsString> certDetailsList(nicknames->numnicknames);
+
+ int32_t CertsToUse;
+
+ for (CertsToUse = 0, node = CERT_LIST_HEAD(certList.get());
+ !CERT_LIST_END(node, certList.get()) &&
+ CertsToUse < nicknames->numnicknames;
+ node = CERT_LIST_NEXT(node)) {
+ RefPtr<nsNSSCertificate> tempCert(new nsNSSCertificate(node->cert));
+
+ if (tempCert) {
+ nsAutoString i_nickname(
+ NS_ConvertUTF8toUTF16(nicknames->nicknames[CertsToUse]));
+ nsAutoString nickWithSerial;
+ nsAutoString details;
+
+ if (!selectionFound) {
+ /* for the case when selectedNickname refers to a bare nickname */
+ if (i_nickname == nsDependentString(selectedNickname)) {
+ selectedIndex = CertsToUse;
+ selectionFound = true;
+ }
+ }
+
+ if (NS_SUCCEEDED(
+ FormatUIStrings(tempCert, i_nickname, nickWithSerial, details))) {
+ certNicknameList.AppendElement(nickWithSerial);
+ certDetailsList.AppendElement(details);
+ if (!selectionFound) {
+ /* for the case when selectedNickname refers to nickname + serial */
+ if (nickWithSerial == nsDependentString(selectedNickname)) {
+ selectedIndex = CertsToUse;
+ selectionFound = true;
+ }
+ }
+ } else {
+ // Placeholder, to keep the indexes valid.
+ certNicknameList.AppendElement(u""_ns);
+ certDetailsList.AppendElement(u""_ns);
+ }
+
+ ++CertsToUse;
+ }
+ }
+
+ if (certNicknameList.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsICertPickDialogs> dialogs;
+ rv = getNSSDialogs(getter_AddRefs(dialogs), NS_GET_IID(nsICertPickDialogs),
+ NS_CERTPICKDIALOGS_CONTRACTID);
+
+ if (NS_SUCCEEDED(rv)) {
+ // Show the cert picker dialog and get the index of the selected cert.
+ rv = dialogs->PickCertificate(ctx, certNicknameList, certDetailsList,
+ &selectedIndex, canceled);
+ }
+
+ if (NS_SUCCEEDED(rv) && !*canceled) {
+ int32_t i;
+ for (i = 0, node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList);
+ ++i, node = CERT_LIST_NEXT(node)) {
+ if (i == selectedIndex) {
+ RefPtr<nsNSSCertificate> cert = new nsNSSCertificate(node->cert);
+ cert.forget(_retval);
+ break;
+ }
+ }
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/extensions/smime/nsCertPicker.h b/comm/mailnews/extensions/smime/nsCertPicker.h
new file mode 100644
index 0000000000..04b5270eda
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsCertPicker.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsCertPicker_h
+#define nsCertPicker_h
+
+#include "nsICertPickDialogs.h"
+#include "nsIUserCertPicker.h"
+
+#define NS_CERT_PICKER_CID \
+ { \
+ 0x735959a1, 0xaf01, 0x447e, { \
+ 0xb0, 0x2d, 0x56, 0xe9, 0x68, 0xfa, 0x52, 0xb4 \
+ } \
+ }
+
+class nsCertPicker : public nsICertPickDialogs, public nsIUserCertPicker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICERTPICKDIALOGS
+ NS_DECL_NSIUSERCERTPICKER
+
+ nsCertPicker();
+ nsresult Init();
+
+ protected:
+ virtual ~nsCertPicker();
+};
+
+#endif // nsCertPicker_h
diff --git a/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.cpp b/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.cpp
new file mode 100644
index 0000000000..ecd428f29b
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.cpp
@@ -0,0 +1,32 @@
+/* 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/. */
+
+#include "nsEncryptedSMIMEURIsService.h"
+
+NS_IMPL_ISUPPORTS(nsEncryptedSMIMEURIsService, nsIEncryptedSMIMEURIsService)
+
+nsEncryptedSMIMEURIsService::nsEncryptedSMIMEURIsService() {}
+
+nsEncryptedSMIMEURIsService::~nsEncryptedSMIMEURIsService() {}
+
+NS_IMETHODIMP nsEncryptedSMIMEURIsService::RememberEncrypted(
+ const nsACString& uri) {
+ // Assuming duplicates are allowed.
+ mEncryptedURIs.AppendElement(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsEncryptedSMIMEURIsService::ForgetEncrypted(
+ const nsACString& uri) {
+ // Assuming, this will only remove one copy of the string, if the array
+ // contains multiple copies of the same string.
+ mEncryptedURIs.RemoveElement(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsEncryptedSMIMEURIsService::IsEncrypted(const nsACString& uri,
+ bool* _retval) {
+ *_retval = mEncryptedURIs.Contains(uri);
+ return NS_OK;
+}
diff --git a/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.h b/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.h
new file mode 100644
index 0000000000..dfce321f52
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsEncryptedSMIMEURIsService.h
@@ -0,0 +1,24 @@
+/* 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/. */
+
+#ifndef _nsEncryptedSMIMEURIsService_H_
+#define _nsEncryptedSMIMEURIsService_H_
+
+#include "nsIEncryptedSMIMEURIsSrvc.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+class nsEncryptedSMIMEURIsService : public nsIEncryptedSMIMEURIsService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIENCRYPTEDSMIMEURISSERVICE
+
+ nsEncryptedSMIMEURIsService();
+
+ protected:
+ virtual ~nsEncryptedSMIMEURIsService();
+ nsTArray<nsCString> mEncryptedURIs;
+};
+
+#endif
diff --git a/comm/mailnews/extensions/smime/nsICMSDecoder.idl b/comm/mailnews/extensions/smime/nsICMSDecoder.idl
new file mode 100644
index 0000000000..5764707c59
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSDecoder.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{ C++
+typedef void (*NSSCMSContentCallback)(void *arg, const char *buf, unsigned long len);
+
+#define NS_CMSDECODER_CONTRACTID "@mozilla.org/nsCMSDecoder;1"
+%}
+
+native NSSCMSContentCallback(NSSCMSContentCallback);
+
+interface nsICMSMessage;
+
+/**
+ * nsICMSDecoder
+ * Streaming interface to decode an CMS message, the input data may be
+ * passed in chunks. Cannot be called from JS.
+ */
+[uuid(c7c7033b-f341-4065-aadd-7eef55ce0dda)]
+interface nsICMSDecoder : nsISupports
+{
+ void start(in NSSCMSContentCallback cb, in voidPtr arg);
+ void update(in string aBuf, in long aLen);
+ void finish(out nsICMSMessage msg);
+};
diff --git a/comm/mailnews/extensions/smime/nsICMSDecoderJS.idl b/comm/mailnews/extensions/smime/nsICMSDecoderJS.idl
new file mode 100644
index 0000000000..83ff66e12d
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSDecoderJS.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsICMSMessage;
+
+/**
+ * nsICMSDecoderJS
+ * Interface to decode a CMS message, can be called from JavaScript.
+ */
+[scriptable,uuid(7e0a6708-17f4-4573-8115-1ca14f1442e0)]
+interface nsICMSDecoderJS : nsISupports
+{
+ /**
+ * Return the result of decoding/decrypting the given CMS message.
+ *
+ * @param aInput The bytes of a CMS message.
+ * @return The decoded data
+ */
+ Array<uint8_t> decrypt(in Array<uint8_t> input);
+};
diff --git a/comm/mailnews/extensions/smime/nsICMSEncoder.idl b/comm/mailnews/extensions/smime/nsICMSEncoder.idl
new file mode 100644
index 0000000000..23fe086acb
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSEncoder.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{ C++
+typedef void (*NSSCMSContentCallback)(void *arg, const char *buf, unsigned long len);
+
+#define NS_CMSENCODER_CONTRACTID "@mozilla.org/nsCMSEncoder;1"
+%}
+
+native NSSCMSContentCallback(NSSCMSContentCallback);
+
+interface nsICMSMessage;
+
+/**
+ * nsICMSEncoder
+ * Interface to Encode an CMS message
+ */
+[uuid(17dc4fb4-e379-4e56-a4a4-57cdcc74816f)]
+interface nsICMSEncoder : nsISupports
+{
+ void start(in nsICMSMessage aMsg, in NSSCMSContentCallback cb, in voidPtr arg);
+ void update(in string aBuf, in long aLen);
+ void finish();
+ void encode(in nsICMSMessage aMsg);
+};
diff --git a/comm/mailnews/extensions/smime/nsICMSMessage.idl b/comm/mailnews/extensions/smime/nsICMSMessage.idl
new file mode 100644
index 0000000000..ed863e5418
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSMessage.idl
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsISMimeVerificationListener;
+
+%{ C++
+#define NS_CMSMESSAGE_CONTRACTID "@mozilla.org/nsCMSMessage;1"
+%}
+
+/*
+ * At the time the ptr type is eliminated in both interfaces, both should be
+ * made scriptable.
+ */
+[ptr] native UnsignedCharPtr(unsigned char);
+
+interface nsIX509Cert;
+
+[uuid(cd76ec81-02f0-41a3-8852-c0acce0bab53)]
+interface nsICMSVerifyFlags : nsISupports
+{
+ const long NONE = 0;
+ const long VERIFY_ALLOW_WEAK_SHA1 = 1 << 0;
+};
+
+/**
+ * nsICMSMessage
+ * Interface to a CMS Message
+ */
+[uuid(c6d51c22-73e9-4dad-86b9-bde584e33c63)]
+interface nsICMSMessage : nsISupports
+{
+ void contentIsSigned(out boolean aSigned);
+ void contentIsEncrypted(out boolean aEncrypted);
+ void getSignerCommonName(out string aName);
+ void getSignerEmailAddress(out string aEmail);
+ void getSignerCert(out nsIX509Cert scert);
+ void getEncryptionCert(out nsIX509Cert ecert);
+ void getSigningTime(out PRTime aTime);
+
+ /**
+ * @param verifyFlags - Optional flags from nsICMSVerifyFlags.
+ */
+ void verifySignature(in long verifyFlags);
+
+ /**
+ * @param verifyFlags - Optional flags from nsICMSVerifyFlags.
+ */
+ void verifyDetachedSignature(in long verifyFlags,
+ in Array<octet> aDigestData,
+ in int16_t aDigestType);
+ void CreateEncrypted(in Array<nsIX509Cert> aRecipientCerts);
+
+ /* The parameter aDigestType must be one of the values in nsICryptoHash */
+ void CreateSigned(in nsIX509Cert scert, in nsIX509Cert ecert,
+ in Array<octet> aDigestData, in int16_t aDigestType);
+
+ /**
+ * Async version of nsICMSMessage::VerifySignature.
+ * Code will be executed on a background thread and
+ * availability of results will be notified using a
+ * call to nsISMimeVerificationListener.
+ */
+ void asyncVerifySignature(in long verifyFlags,
+ in nsISMimeVerificationListener listener);
+
+ /**
+ * Async version of nsICMSMessage::VerifyDetachedSignature.
+ * Code will be executed on a background thread and
+ * availability of results will be notified using a
+ * call to nsISMimeVerificationListener.
+ *
+ * Set aDigestType to one of the values from nsICryptoHash.
+ */
+ void asyncVerifyDetachedSignature(in long verifyFlags,
+ in nsISMimeVerificationListener listener,
+ in Array<octet> aDigestData,
+ in int16_t aDigestType);
+};
+
+[uuid(5226d698-0773-4f25-b94c-7944b3fc01d3)]
+interface nsISMimeVerificationListener : nsISupports {
+
+ /**
+ * Notify that results are ready, that have been requested
+ * using nsICMSMessage::asyncVerify[Detached]Signature()
+ *
+ * verificationResultCode matches synchronous result code from
+ * nsICMSMessage::verify[Detached]Signature
+ */
+ void notify(in nsICMSMessage verifiedMessage,
+ in nsresult verificationResultCode);
+};
diff --git a/comm/mailnews/extensions/smime/nsICMSMessageErrors.idl b/comm/mailnews/extensions/smime/nsICMSMessageErrors.idl
new file mode 100644
index 0000000000..7378185f50
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSMessageErrors.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsICMSMessageErrors
+ * Scriptable error constants for nsICMSMessage
+ */
+[scriptable,uuid(267f1a5b-88f7-413b-bc49-487e745282f1)]
+interface nsICMSMessageErrors : nsISupports
+{
+ const long SUCCESS = 0;
+ const long GENERAL_ERROR = 1;
+ const long VERIFY_NOT_SIGNED = 1024;
+ const long VERIFY_NO_CONTENT_INFO = 1025;
+ const long VERIFY_BAD_DIGEST = 1026;
+ const long VERIFY_NOCERT = 1028;
+ const long VERIFY_UNTRUSTED = 1029;
+ const long VERIFY_ERROR_UNVERIFIED = 1031;
+ const long VERIFY_ERROR_PROCESSING = 1032;
+ const long VERIFY_BAD_SIGNATURE = 1033;
+ const long VERIFY_DIGEST_MISMATCH = 1034;
+ const long VERIFY_UNKNOWN_ALGO = 1035;
+ const long VERIFY_UNSUPPORTED_ALGO = 1036;
+ const long VERIFY_MALFORMED_SIGNATURE = 1037;
+ const long VERIFY_HEADER_MISMATCH = 1038;
+ const long VERIFY_NOT_YET_ATTEMPTED = 1039;
+ const long VERIFY_CERT_WITHOUT_ADDRESS = 1040;
+ const long VERIFY_TIME_MISMATCH = 1041;
+
+ const long ENCRYPT_NO_BULK_ALG = 1056;
+ const long ENCRYPT_INCOMPLETE = 1057;
+};
diff --git a/comm/mailnews/extensions/smime/nsICMSSecureMessage.idl b/comm/mailnews/extensions/smime/nsICMSSecureMessage.idl
new file mode 100644
index 0000000000..d41fc04743
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICMSSecureMessage.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIX509Cert;
+
+/**
+ * nsICMSManager (service)
+ * Interface to access users certificate store
+ */
+[scriptable, uuid(17103436-0111-4819-a751-0fc4aa6e3d79)]
+interface nsICMSSecureMessage : nsISupports
+{
+ /**
+ * Return true if the certificate can be used for encrypting emails.
+ */
+ bool canBeUsedForEmailEncryption(in nsIX509Cert cert);
+
+ /**
+ * Return true if the certificate can be used for signing emails.
+ */
+ bool canBeUsedForEmailSigning(in nsIX509Cert cert);
+};
+
+%{C++
+#define NS_CMSSECUREMESSAGE_CONTRACTID "@mozilla.org/nsCMSSecureMessage;1"
+%}
diff --git a/comm/mailnews/extensions/smime/nsICertPickDialogs.idl b/comm/mailnews/extensions/smime/nsICertPickDialogs.idl
new file mode 100644
index 0000000000..534d47fa77
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsICertPickDialogs.idl
@@ -0,0 +1,27 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIInterfaceRequestor;
+
+/**
+ * nsICertPickDialogs provides a generic UI for choosing a certificate.
+ */
+[scriptable, uuid(51d59b08-1dd2-11b2-ad4a-a51b92f8a184)]
+interface nsICertPickDialogs : nsISupports
+{
+ /**
+ * General purpose certificate prompter.
+ */
+ void pickCertificate(in nsIInterfaceRequestor ctx,
+ in Array<AString> certNickList,
+ in Array<AString> certDetailsList,
+ inout long selectedIndex,
+ out boolean canceled);
+};
+
+%{C++
+#define NS_CERTPICKDIALOGS_CONTRACTID "@mozilla.org/nsCertPickDialogs;1"
+%}
diff --git a/comm/mailnews/extensions/smime/nsIEncryptedSMIMEURIsSrvc.idl b/comm/mailnews/extensions/smime/nsIEncryptedSMIMEURIsSrvc.idl
new file mode 100644
index 0000000000..53e305c977
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsIEncryptedSMIMEURIsSrvc.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* This is a private interface used exclusively by SMIME.
+ It provides functionality to the JS UI code,
+ that is only accessible from C++.
+*/
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(f86e55c9-530b-483f-91a7-10fb5b852488)]
+interface nsIEncryptedSMIMEURIsService : nsISupports
+{
+ /// Remember that this URI is encrypted.
+ void rememberEncrypted(in AUTF8String uri);
+
+ /// Forget that this URI is encrypted.
+ void forgetEncrypted(in AUTF8String uri);
+
+ /// Check if this URI is encrypted.
+ boolean isEncrypted(in AUTF8String uri);
+};
diff --git a/comm/mailnews/extensions/smime/nsIMsgSMIMEHeaderSink.idl b/comm/mailnews/extensions/smime/nsIMsgSMIMEHeaderSink.idl
new file mode 100644
index 0000000000..5a0a9b838b
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsIMsgSMIMEHeaderSink.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+
+/* This is a private interface used exclusively by SMIME. NO ONE outside of extensions/smime
+ or the hard coded smime decryption files in mime/src should have any knowledge nor should
+ be referring to this interface.
+*/
+
+#include "nsISupports.idl"
+
+interface nsIX509Cert;
+
+[scriptable, uuid(25380FA1-E70C-4e82-B0BC-F31C2F41C470)]
+interface nsIMsgSMIMEHeaderSink : nsISupports
+{
+ void signedStatus(in long aNestingLevel,
+ in long aSignatureStatus,
+ in nsIX509Cert aSignerCert,
+ in AUTF8String aMsgNeckoURL,
+ in ACString originMimePartNumber);
+ void encryptionStatus(in long aNestingLevel,
+ in long aEncryptionStatus,
+ in nsIX509Cert aReceipientCert,
+ in AUTF8String aMsgNeckoURL,
+ in ACString originMimePartNumber);
+ void ignoreStatusFrom(in ACString originMimePartNumber);
+};
diff --git a/comm/mailnews/extensions/smime/nsIUserCertPicker.idl b/comm/mailnews/extensions/smime/nsIUserCertPicker.idl
new file mode 100644
index 0000000000..666941c297
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsIUserCertPicker.idl
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIX509Cert;
+interface nsIInterfaceRequestor;
+
+[scriptable, uuid(92396323-23f2-49e0-bf98-a25a725231ab)]
+interface nsIUserCertPicker : nsISupports {
+ nsIX509Cert pickByUsage(in nsIInterfaceRequestor ctx,
+ in wstring selectedNickname,
+ in long certUsage, // as defined by NSS enum SECCertUsage
+ in boolean allowInvalid,
+ in boolean allowDuplicateNicknames,
+ in AString emailAddress, // optional - if non-empty,
+ // skip certificates which
+ // have at least one e-mail
+ // address but do not
+ // include this specific one
+ out boolean canceled);
+};
+
+%{C++
+#define NS_CERT_PICKER_CONTRACTID "@mozilla.org/user_cert_picker;1"
+%}
diff --git a/comm/mailnews/extensions/smime/nsMsgComposeSecure.cpp b/comm/mailnews/extensions/smime/nsMsgComposeSecure.cpp
new file mode 100644
index 0000000000..4ecf73783d
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsMsgComposeSecure.cpp
@@ -0,0 +1,1277 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsMsgComposeSecure.h"
+
+#include "ScopedNSSTypes.h"
+#include "cert.h"
+#include "keyhi.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Components.h"
+#include "mozilla/mailnews/MimeEncoder.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "msgCore.h"
+#include "nsComponentManagerUtils.h"
+#include "nsICryptoHash.h"
+#include "nsIMimeConverter.h"
+#include "nsIMsgCompFields.h"
+#include "nsIMsgIdentity.h"
+#include "nsIX509CertDB.h"
+#include "nsMemory.h"
+#include "nsMimeTypes.h"
+#include "nsNSSComponent.h"
+#include "nsServiceManagerUtils.h"
+#include "nspr.h"
+#include "mozpkix/Result.h"
+#include "nsNSSCertificate.h"
+#include "nsNSSHelper.h"
+#include "CryptoTask.h"
+
+using namespace mozilla::mailnews;
+using namespace mozilla;
+using namespace mozilla::psm;
+
+#define MK_MIME_ERROR_WRITING_FILE -1
+
+#define SMIME_STRBUNDLE_URL "chrome://messenger/locale/am-smime.properties"
+
+// It doesn't make sense to encode the message because the message will be
+// displayed only if the MUA doesn't support MIME.
+// We need to consider what to do in case the server doesn't support 8BITMIME.
+// In short, we can't use non-ASCII characters here.
+static const char crypto_multipart_blurb[] =
+ "This is a cryptographically signed message in MIME format.";
+
+static void mime_crypto_write_base64(void* closure, const char* buf,
+ unsigned long size);
+static nsresult mime_encoder_output_fn(const char* buf, int32_t size,
+ void* closure);
+static nsresult mime_nested_encoder_output_fn(const char* buf, int32_t size,
+ void* closure);
+static nsresult make_multipart_signed_header_string(bool outer_p,
+ char** header_return,
+ char** boundary_return,
+ int16_t hash_type);
+static char* mime_make_separator(const char* prefix);
+
+static void GenerateGlobalRandomBytes(unsigned char* buf, int32_t len) {
+ static bool firstTime = true;
+
+ if (firstTime) {
+ // Seed the random-number generator with current time so that
+ // the numbers will be different every time we run.
+ srand((unsigned)PR_Now());
+ firstTime = false;
+ }
+
+ for (int32_t i = 0; i < len; i++) buf[i] = rand() % 10;
+}
+
+char* mime_make_separator(const char* prefix) {
+ unsigned char rand_buf[13];
+ GenerateGlobalRandomBytes(rand_buf, 12);
+
+ return PR_smprintf(
+ "------------%s"
+ "%02X%02X%02X%02X"
+ "%02X%02X%02X%02X"
+ "%02X%02X%02X%02X",
+ prefix, rand_buf[0], rand_buf[1], rand_buf[2], rand_buf[3], rand_buf[4],
+ rand_buf[5], rand_buf[6], rand_buf[7], rand_buf[8], rand_buf[9],
+ rand_buf[10], rand_buf[11]);
+}
+
+// end of copied code which needs fixed....
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Implementation of nsMsgComposeSecure
+/////////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMPL_ISUPPORTS(nsMsgComposeSecure, nsIMsgComposeSecure)
+
+nsMsgComposeSecure::nsMsgComposeSecure()
+ : mSignMessage(false),
+ mAlwaysEncryptMessage(false),
+ mCryptoState(mime_crypto_none),
+ mHashType(0),
+ mMultipartSignedBoundary(nullptr),
+ mIsDraft(false),
+ mBuffer(nullptr),
+ mBufferedBytes(0),
+ mErrorAlreadyReported(false) {}
+
+nsMsgComposeSecure::~nsMsgComposeSecure() {
+ /* destructor code */
+ if (mEncryptionContext) {
+ if (mBufferedBytes) {
+ mEncryptionContext->Update(mBuffer, mBufferedBytes);
+ mBufferedBytes = 0;
+ }
+ mEncryptionContext->Finish();
+ mEncryptionContext = nullptr;
+ }
+
+ delete[] mBuffer;
+ mBuffer = nullptr;
+
+ PR_FREEIF(mMultipartSignedBoundary);
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::SetSignMessage(bool value) {
+ mSignMessage = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::GetSignMessage(bool* _retval) {
+ *_retval = mSignMessage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::SetRequireEncryptMessage(bool value) {
+ mAlwaysEncryptMessage = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::GetRequireEncryptMessage(bool* _retval) {
+ *_retval = mAlwaysEncryptMessage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::RequiresCryptoEncapsulation(
+ nsIMsgIdentity* aIdentity, nsIMsgCompFields* aCompFields,
+ bool* aRequiresEncryptionWork) {
+ NS_ENSURE_ARG_POINTER(aRequiresEncryptionWork);
+
+ *aRequiresEncryptionWork = false;
+
+ bool alwaysEncryptMessages = false;
+ bool signMessage = false;
+ nsresult rv = ExtractEncryptionState(aIdentity, aCompFields, &signMessage,
+ &alwaysEncryptMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (alwaysEncryptMessages || signMessage) *aRequiresEncryptionWork = true;
+
+ return NS_OK;
+}
+
+nsresult nsMsgComposeSecure::GetSMIMEBundleString(const char16_t* name,
+ nsString& outString) {
+ outString.Truncate();
+
+ NS_ENSURE_ARG_POINTER(name);
+
+ NS_ENSURE_TRUE(InitializeSMIMEBundle(), NS_ERROR_FAILURE);
+
+ return mSMIMEBundle->GetStringFromName(NS_ConvertUTF16toUTF8(name).get(),
+ outString);
+}
+
+nsresult nsMsgComposeSecure::SMIMEBundleFormatStringFromName(
+ const char* name, nsTArray<nsString>& params, nsAString& outString) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ if (!InitializeSMIMEBundle()) return NS_ERROR_FAILURE;
+
+ return mSMIMEBundle->FormatStringFromName(name, params, outString);
+}
+
+bool nsMsgComposeSecure::InitializeSMIMEBundle() {
+ if (mSMIMEBundle) return true;
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ nsresult rv = bundleService->CreateBundle(SMIME_STRBUNDLE_URL,
+ getter_AddRefs(mSMIMEBundle));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return true;
+}
+
+void nsMsgComposeSecure::SetError(nsIMsgSendReport* sendReport,
+ const char16_t* bundle_string) {
+ if (!sendReport || !bundle_string) return;
+
+ if (mErrorAlreadyReported) return;
+
+ mErrorAlreadyReported = true;
+
+ nsString errorString;
+ nsresult res = GetSMIMEBundleString(bundle_string, errorString);
+ if (NS_SUCCEEDED(res) && !errorString.IsEmpty()) {
+ sendReport->SetMessage(nsIMsgSendReport::process_Current, errorString.get(),
+ true);
+ }
+}
+
+void nsMsgComposeSecure::SetErrorWithParam(nsIMsgSendReport* sendReport,
+ const char* bundle_string,
+ const char* param) {
+ if (!sendReport || !bundle_string || !param) return;
+
+ if (mErrorAlreadyReported) return;
+
+ mErrorAlreadyReported = true;
+
+ nsString errorString;
+ nsresult res;
+ AutoTArray<nsString, 1> params;
+ CopyASCIItoUTF16(MakeStringSpan(param), *params.AppendElement());
+ res = SMIMEBundleFormatStringFromName(bundle_string, params, errorString);
+
+ if (NS_SUCCEEDED(res) && !errorString.IsEmpty()) {
+ sendReport->SetMessage(nsIMsgSendReport::process_Current, errorString.get(),
+ true);
+ }
+}
+
+nsresult nsMsgComposeSecure::ExtractEncryptionState(
+ nsIMsgIdentity* aIdentity, nsIMsgCompFields* aComposeFields,
+ bool* aSignMessage, bool* aEncrypt) {
+ if (!aComposeFields && !aIdentity)
+ return NS_ERROR_FAILURE; // kick out...invalid args....
+
+ NS_ENSURE_ARG_POINTER(aSignMessage);
+ NS_ENSURE_ARG_POINTER(aEncrypt);
+
+ this->GetSignMessage(aSignMessage);
+ this->GetRequireEncryptMessage(aEncrypt);
+
+ return NS_OK;
+}
+
+// Select a hash algorithm to sign message
+// based on subject public key type and size.
+static nsresult GetSigningHashFunction(nsIX509Cert* aSigningCert,
+ int16_t* hashType) {
+ // Get the signing certificate
+ CERTCertificate* scert = nullptr;
+ if (aSigningCert) {
+ scert = aSigningCert->GetCert();
+ }
+ if (!scert) {
+ return NS_ERROR_FAILURE;
+ }
+
+ UniqueSECKEYPublicKey scertPublicKey(CERT_ExtractPublicKey(scert));
+ if (!scertPublicKey) {
+ return mozilla::MapSECStatus(SECFailure);
+ }
+ KeyType subjectPublicKeyType = SECKEY_GetPublicKeyType(scertPublicKey.get());
+
+ // Get the length of the signature in bits.
+ unsigned siglen = SECKEY_SignatureLen(scertPublicKey.get()) * 8;
+ if (!siglen) {
+ return mozilla::MapSECStatus(SECFailure);
+ }
+
+ // Select a hash function for signature generation whose security strength
+ // meets or exceeds the security strength of the public key, using NIST
+ // Special Publication 800-57, Recommendation for Key Management - Part 1:
+ // General (Revision 3), where Table 2 specifies the security strength of
+ // the public key and Table 3 lists acceptable hash functions. (The security
+ // strength of the hash (for digital signatures) is half the length of the
+ // output.)
+ // [SP 800-57 is available at http://csrc.nist.gov/publications/PubsSPs.html.]
+ if (subjectPublicKeyType == rsaKey) {
+ // For RSA, siglen is the same as the length of the modulus.
+
+ // SHA-1 provides equivalent security strength for up to 1024 bits
+ // SHA-256 provides equivalent security strength for up to 3072 bits
+
+ if (siglen > 3072) {
+ *hashType = nsICryptoHash::SHA512;
+ } else if (siglen > 1024) {
+ *hashType = nsICryptoHash::SHA256;
+ } else {
+ *hashType = nsICryptoHash::SHA1;
+ }
+ } else if (subjectPublicKeyType == dsaKey) {
+ // For DSA, siglen is twice the length of the q parameter of the key.
+ // The security strength of the key is half the length (in bits) of
+ // the q parameter of the key.
+
+ // NSS only supports SHA-1, SHA-224, and SHA-256 for DSA signatures.
+ // The S/MIME code does not support SHA-224.
+
+ if (siglen >= 512) { // 512-bit signature = 256-bit q parameter
+ *hashType = nsICryptoHash::SHA256;
+ } else {
+ *hashType = nsICryptoHash::SHA1;
+ }
+ } else if (subjectPublicKeyType == ecKey) {
+ // For ECDSA, siglen is twice the length of the field size. The security
+ // strength of the key is half the length (in bits) of the field size.
+
+ if (siglen >= 1024) { // 1024-bit signature = 512-bit field size
+ *hashType = nsICryptoHash::SHA512;
+ } else if (siglen >= 768) { // 768-bit signature = 384-bit field size
+ *hashType = nsICryptoHash::SHA384;
+ } else if (siglen >= 512) { // 512-bit signature = 256-bit field size
+ *hashType = nsICryptoHash::SHA256;
+ } else {
+ *hashType = nsICryptoHash::SHA1;
+ }
+ } else {
+ // Unknown key type
+ *hashType = nsICryptoHash::SHA256;
+ NS_WARNING("GetSigningHashFunction: Subject public key type unknown.");
+ }
+ return NS_OK;
+}
+
+/* void beginCryptoEncapsulation (in nsOutputFileStream aStream, in boolean
+ * aEncrypt, in boolean aSign, in string aRecipeints, in boolean aIsDraft); */
+NS_IMETHODIMP nsMsgComposeSecure::BeginCryptoEncapsulation(
+ nsIOutputStream* aStream, const char* aRecipients,
+ nsIMsgCompFields* aCompFields, nsIMsgIdentity* aIdentity,
+ nsIMsgSendReport* sendReport, bool aIsDraft) {
+ mErrorAlreadyReported = false;
+ nsresult rv = NS_OK;
+
+ // CryptoEncapsulation should be synchronous, therefore it must
+ // avoid cert verification or looking up certs, which often involves
+ // async OCSP. The message composer should already have looked up
+ // and verified certificates whenever the user modified the recipient
+ // list, and should have used CacheValidCertForEmail to make those
+ // certificates known to us.
+ // (That code may use the AsyncFindCertByEmailAddr API which allows
+ // lookup and validation to be performed on a background thread,
+ // which is required when using OCSP.)
+
+ bool encryptMessages = false;
+ bool signMessage = false;
+ ExtractEncryptionState(aIdentity, aCompFields, &signMessage,
+ &encryptMessages);
+
+ if (!signMessage && !encryptMessages) return NS_ERROR_FAILURE;
+
+ mStream = aStream;
+ mIsDraft = aIsDraft;
+
+ if (encryptMessages && signMessage)
+ mCryptoState = mime_crypto_signed_encrypted;
+ else if (encryptMessages)
+ mCryptoState = mime_crypto_encrypted;
+ else if (signMessage)
+ mCryptoState = mime_crypto_clear_signed;
+ else
+ PR_ASSERT(0);
+
+ aIdentity->GetUnicharAttribute("signing_cert_name", mSigningCertName);
+ aIdentity->GetCharAttribute("signing_cert_dbkey", mSigningCertDBKey);
+ aIdentity->GetUnicharAttribute("encryption_cert_name", mEncryptionCertName);
+ aIdentity->GetCharAttribute("encryption_cert_dbkey", mEncryptionCertDBKey);
+
+ rv = MimeCryptoHackCerts(aRecipients, sendReport, encryptMessages,
+ signMessage, aIdentity);
+ if (NS_FAILED(rv)) {
+ goto FAIL;
+ }
+
+ if (signMessage && mSelfSigningCert) {
+ rv = GetSigningHashFunction(mSelfSigningCert, &mHashType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ switch (mCryptoState) {
+ case mime_crypto_clear_signed:
+ rv = MimeInitMultipartSigned(true, sendReport);
+ break;
+ case mime_crypto_opaque_signed:
+ PR_ASSERT(0); /* #### no api for this yet */
+ rv = NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ case mime_crypto_signed_encrypted:
+ rv = MimeInitEncryption(true, sendReport);
+ break;
+ case mime_crypto_encrypted:
+ rv = MimeInitEncryption(false, sendReport);
+ break;
+ case mime_crypto_none:
+ /* This can happen if mime_crypto_hack_certs() decided to turn off
+ encryption (by asking the user.) */
+ // XXX 1 is not a valid nsresult
+ rv = static_cast<nsresult>(1);
+ break;
+ default:
+ PR_ASSERT(0);
+ break;
+ }
+
+FAIL:
+ return rv;
+}
+
+/* void finishCryptoEncapsulation (in boolean aAbort); */
+NS_IMETHODIMP nsMsgComposeSecure::FinishCryptoEncapsulation(
+ bool aAbort, nsIMsgSendReport* sendReport) {
+ nsresult rv = NS_OK;
+
+ if (!aAbort) {
+ switch (mCryptoState) {
+ case mime_crypto_clear_signed:
+ rv = MimeFinishMultipartSigned(true, sendReport);
+ break;
+ case mime_crypto_opaque_signed:
+ PR_ASSERT(0); /* #### no api for this yet */
+ rv = NS_ERROR_FAILURE;
+ break;
+ case mime_crypto_signed_encrypted:
+ rv = MimeFinishEncryption(true, sendReport);
+ break;
+ case mime_crypto_encrypted:
+ rv = MimeFinishEncryption(false, sendReport);
+ break;
+ default:
+ PR_ASSERT(0);
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgComposeSecure::MimeInitMultipartSigned(
+ bool aOuter, nsIMsgSendReport* sendReport) {
+ /* First, construct and write out the multipart/signed MIME header data.
+ */
+ nsresult rv = NS_OK;
+ char* header = 0;
+ uint32_t L;
+
+ rv = make_multipart_signed_header_string(
+ aOuter, &header, &mMultipartSignedBoundary, mHashType);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ L = strlen(header);
+
+ if (aOuter) {
+ /* If this is the outer block, write it to the file. */
+ uint32_t n;
+ rv = mStream->Write(header, L, &n);
+ if (NS_FAILED(rv) || n < L) {
+ // XXX This is -1, not an nsresult
+ rv = static_cast<nsresult>(MK_MIME_ERROR_WRITING_FILE);
+ }
+ } else {
+ /* If this is an inner block, feed it through the crypto stream. */
+ rv = MimeCryptoWriteBlock(header, L);
+ }
+
+ PR_Free(header);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Now initialize the crypto library, so that we can compute a hash
+ on the object which we are signing.
+ */
+
+ PR_SetError(0, 0);
+ mDataHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDataHash->Init(mHashType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_SetError(0, 0);
+ return rv;
+}
+
+nsresult nsMsgComposeSecure::MimeInitEncryption(bool aSign,
+ nsIMsgSendReport* sendReport) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> sMIMEBundle;
+ nsString mime_smime_enc_content_desc;
+
+ bundleSvc->CreateBundle(SMIME_STRBUNDLE_URL, getter_AddRefs(sMIMEBundle));
+
+ if (!sMIMEBundle) return NS_ERROR_FAILURE;
+
+ sMIMEBundle->GetStringFromName("mime_smimeEncryptedContentDesc",
+ mime_smime_enc_content_desc);
+ NS_ConvertUTF16toUTF8 enc_content_desc_utf8(mime_smime_enc_content_desc);
+
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString encodedContentDescription;
+ mimeConverter->EncodeMimePartIIStr_UTF8(
+ enc_content_desc_utf8, false, sizeof("Content-Description: "),
+ nsIMimeConverter::MIME_ENCODED_WORD_SIZE, encodedContentDescription);
+
+ /* First, construct and write out the opaque-crypto-blob MIME header data.
+ */
+
+ char* s = PR_smprintf("Content-Type: " APPLICATION_PKCS7_MIME
+ "; name=\"smime.p7m\"; smime-type=enveloped-data" CRLF
+ "Content-Transfer-Encoding: " ENCODING_BASE64 CRLF
+ "Content-Disposition: attachment"
+ "; filename=\"smime.p7m\"" CRLF
+ "Content-Description: %s" CRLF CRLF,
+ encodedContentDescription.get());
+
+ uint32_t L;
+ if (!s) return NS_ERROR_OUT_OF_MEMORY;
+ L = strlen(s);
+ uint32_t n;
+ rv = mStream->Write(s, L, &n);
+ if (NS_FAILED(rv) || n < L) {
+ return NS_ERROR_FAILURE;
+ }
+ PR_Free(s);
+ s = 0;
+
+ /* Now initialize the crypto library, so that we can filter the object
+ to be encrypted through it.
+ */
+
+ if (!mIsDraft) {
+ PR_ASSERT(!mCerts.IsEmpty());
+ if (mCerts.IsEmpty()) return NS_ERROR_FAILURE;
+ }
+
+ // If a previous call to MimeInitEncryption (this function) failed,
+ // the mEncryptionContext already exists and references our
+ // mCryptoEncoder. Destroy mEncryptionContext to release the
+ // reference prior to resetting mCryptoEncoder.
+ if (mEncryptionContext) {
+ mEncryptionContext->Finish();
+ mEncryptionContext = nullptr;
+ }
+
+ // Initialize the base64 encoder
+ mCryptoEncoder.reset(
+ MimeEncoder::GetBase64Encoder(mime_encoder_output_fn, this));
+
+ /* Initialize the encrypter (and add the sender's cert.) */
+ PR_ASSERT(mSelfEncryptionCert);
+ PR_SetError(0, 0);
+ mEncryptionCinfo = do_CreateInstance(NS_CMSMESSAGE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+ rv = mEncryptionCinfo->CreateEncrypted(mCerts);
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorEncryptMail");
+ goto FAIL;
+ }
+
+ mEncryptionContext = do_CreateInstance(NS_CMSENCODER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mBuffer) {
+ mBuffer = new char[eBufferSize];
+ if (!mBuffer) return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mBufferedBytes = 0;
+
+ rv = mEncryptionContext->Start(mEncryptionCinfo, mime_crypto_write_base64,
+ mCryptoEncoder.get());
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorEncryptMail");
+ goto FAIL;
+ }
+
+ /* If we're signing, tack a multipart/signed header onto the front of
+ the data to be encrypted, and initialize the sign-hashing code too.
+ */
+ if (aSign) {
+ rv = MimeInitMultipartSigned(false, sendReport);
+ if (NS_FAILED(rv)) goto FAIL;
+ }
+
+FAIL:
+ return rv;
+}
+
+nsresult nsMsgComposeSecure::MimeFinishMultipartSigned(
+ bool aOuter, nsIMsgSendReport* sendReport) {
+ nsresult rv;
+ nsCOMPtr<nsICMSMessage> cinfo =
+ do_CreateInstance(NS_CMSMESSAGE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsICMSEncoder> encoder =
+ do_CreateInstance(NS_CMSENCODER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char* header = nullptr;
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> sMIMEBundle;
+ nsString mime_smime_sig_content_desc;
+
+ bundleSvc->CreateBundle(SMIME_STRBUNDLE_URL, getter_AddRefs(sMIMEBundle));
+
+ if (!sMIMEBundle) return NS_ERROR_FAILURE;
+
+ sMIMEBundle->GetStringFromName("mime_smimeSignatureContentDesc",
+ mime_smime_sig_content_desc);
+
+ NS_ConvertUTF16toUTF8 sig_content_desc_utf8(mime_smime_sig_content_desc);
+
+ /* Compute the hash...
+ */
+
+ NS_ENSURE_STATE(mDataHash);
+ nsAutoCString hashString;
+ rv = mDataHash->Finish(false, hashString);
+ mDataHash = nullptr;
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (PR_GetError() < 0) return NS_ERROR_FAILURE;
+
+ /* Write out the headers for the signature.
+ */
+ uint32_t L;
+ header = PR_smprintf(
+ CRLF "--%s" CRLF "Content-Type: " APPLICATION_PKCS7_SIGNATURE
+ "; name=\"smime.p7s\"" CRLF
+ "Content-Transfer-Encoding: " ENCODING_BASE64 CRLF
+ "Content-Disposition: attachment; "
+ "filename=\"smime.p7s\"" CRLF "Content-Description: %s" CRLF CRLF,
+ mMultipartSignedBoundary, sig_content_desc_utf8.get());
+
+ if (!header) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ L = strlen(header);
+ if (aOuter) {
+ /* If this is the outer block, write it to the file. */
+ uint32_t n;
+ rv = mStream->Write(header, L, &n);
+ if (NS_FAILED(rv) || n < L) {
+ // XXX This is -1, not an nsresult
+ rv = static_cast<nsresult>(MK_MIME_ERROR_WRITING_FILE);
+ }
+ } else {
+ /* If this is an inner block, feed it through the crypto stream. */
+ rv = MimeCryptoWriteBlock(header, L);
+ }
+
+ PR_Free(header);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Create the signature...
+ */
+
+ NS_ASSERTION(mHashType, "Hash function for signature has not been set.");
+
+ PR_ASSERT(mSelfSigningCert);
+ PR_SetError(0, 0);
+
+ nsTArray<uint8_t> digest;
+ digest.AppendElements(hashString.get(), hashString.Length());
+
+ rv = cinfo->CreateSigned(mSelfSigningCert, mSelfEncryptionCert, digest,
+ mHashType);
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorCanNotSignMail");
+ return rv;
+ }
+
+ // Initialize the base64 encoder for the signature data.
+ MOZ_ASSERT(!mSigEncoder, "Shouldn't already have a mSigEncoder");
+ mSigEncoder.reset(MimeEncoder::GetBase64Encoder(
+ (aOuter ? mime_encoder_output_fn : mime_nested_encoder_output_fn), this));
+
+ /* Write out the signature.
+ */
+ PR_SetError(0, 0);
+ rv = encoder->Start(cinfo, mime_crypto_write_base64, mSigEncoder.get());
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorCanNotSignMail");
+ return rv;
+ }
+
+ // We're not passing in any data, so no update needed.
+ rv = encoder->Finish();
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorCanNotSignMail");
+ return rv;
+ }
+
+ // Shut down the sig's base64 encoder.
+ rv = mSigEncoder->Flush();
+ mSigEncoder.reset();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Now write out the terminating boundary.
+ */
+ {
+ uint32_t L;
+ char* header = PR_smprintf(CRLF "--%s--" CRLF, mMultipartSignedBoundary);
+ PR_Free(mMultipartSignedBoundary);
+ mMultipartSignedBoundary = 0;
+
+ if (!header) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ L = strlen(header);
+ if (aOuter) {
+ /* If this is the outer block, write it to the file. */
+ uint32_t n;
+ rv = mStream->Write(header, L, &n);
+ if (NS_FAILED(rv) || n < L)
+ // XXX This is -1, not an nsresult
+ rv = static_cast<nsresult>(MK_MIME_ERROR_WRITING_FILE);
+ } else {
+ /* If this is an inner block, feed it through the crypto stream. */
+ rv = MimeCryptoWriteBlock(header, L);
+ }
+ }
+
+ return rv;
+}
+
+/* Helper function for mime_finish_crypto_encapsulation() to close off
+ an opaque crypto object (for encrypted or signed-and-encrypted messages.)
+ */
+nsresult nsMsgComposeSecure::MimeFinishEncryption(
+ bool aSign, nsIMsgSendReport* sendReport) {
+ nsresult rv;
+
+ /* If this object is both encrypted and signed, close off the
+ signature first (since it's inside.) */
+ if (aSign) {
+ rv = MimeFinishMultipartSigned(false, sendReport);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ /* Close off the opaque encrypted blob.
+ */
+ PR_ASSERT(mEncryptionContext);
+
+ if (mBufferedBytes) {
+ rv = mEncryptionContext->Update(mBuffer, mBufferedBytes);
+ mBufferedBytes = 0;
+ if (NS_FAILED(rv)) {
+ PR_ASSERT(PR_GetError() < 0);
+ return rv;
+ }
+ }
+
+ rv = mEncryptionContext->Finish();
+ mEncryptionContext = nullptr;
+
+ if (NS_FAILED(rv)) {
+ SetError(sendReport, u"ErrorEncryptMail");
+ return rv;
+ }
+
+ NS_ENSURE_TRUE(mEncryptionCinfo, NS_ERROR_UNEXPECTED);
+
+ mEncryptionCinfo = nullptr;
+
+ // Shut down the base64 encoder.
+ mCryptoEncoder->Flush();
+ mCryptoEncoder.reset();
+
+ uint32_t n;
+ rv = mStream->Write(CRLF, 2, &n);
+ if (NS_FAILED(rv) || n < 2) rv = NS_ERROR_FAILURE;
+
+ return rv;
+}
+
+/* Used to figure out what certs should be used when encrypting this message.
+ */
+nsresult nsMsgComposeSecure::MimeCryptoHackCerts(const char* aRecipients,
+ nsIMsgSendReport* sendReport,
+ bool aEncrypt, bool aSign,
+ nsIMsgIdentity* aIdentity) {
+ nsCOMPtr<nsIX509CertDB> certdb = do_GetService(NS_X509CERTDB_CONTRACTID);
+ nsresult res = NS_OK;
+
+ PR_ASSERT(aEncrypt || aSign);
+
+ /*
+ Signing and encryption certs use the following (per-identity) preferences:
+ - "signing_cert_name"/"encryption_cert_name": a string specifying the
+ nickname of the certificate
+ - "signing_cert_dbkey"/"encryption_cert_dbkey": a Base64 encoded blob
+ specifying an nsIX509Cert dbKey (represents serial number
+ and issuer DN, which is considered to be unique for X.509 certificates)
+ */
+
+ RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
+ NS_ENSURE_TRUE(certVerifier, NS_ERROR_UNEXPECTED);
+
+ // Calling CERT_GetCertNicknames has the desired side effect of
+ // traversing all tokens, and bringing up prompts to unlock them.
+ nsCOMPtr<nsIInterfaceRequestor> ctx = new PipUIContext();
+ CERTCertNicknames* result_unused = CERT_GetCertNicknames(
+ CERT_GetDefaultCertDB(), SEC_CERT_NICKNAMES_USER, ctx);
+ CERT_FreeNicknames(result_unused);
+
+ nsTArray<nsTArray<uint8_t>> builtChain;
+ if (!mEncryptionCertDBKey.IsEmpty()) {
+ res = certdb->FindCertByDBKey(mEncryptionCertDBKey,
+ getter_AddRefs(mSelfEncryptionCert));
+
+ if (NS_SUCCEEDED(res) && mSelfEncryptionCert) {
+ nsTArray<uint8_t> certBytes;
+ res = mSelfEncryptionCert->GetRawDER(certBytes);
+ NS_ENSURE_SUCCESS(res, res);
+
+ if (certVerifier->VerifyCert(
+ certBytes, certificateUsageEmailRecipient, mozilla::pkix::Now(),
+ nullptr, nullptr, builtChain,
+ // Only local checks can run on the main thread.
+ // Skipping OCSP for the user's own cert seems accaptable.
+ CertVerifier::FLAG_LOCAL_ONLY) != mozilla::pkix::Success) {
+ // not suitable for encryption, so unset cert and clear pref
+ mSelfEncryptionCert = nullptr;
+ mEncryptionCertDBKey.Truncate();
+ aIdentity->SetCharAttribute("encryption_cert_dbkey",
+ mEncryptionCertDBKey);
+ }
+ }
+ }
+
+ // same procedure for the signing cert
+ if (!mSigningCertDBKey.IsEmpty()) {
+ res = certdb->FindCertByDBKey(mSigningCertDBKey,
+ getter_AddRefs(mSelfSigningCert));
+ if (NS_SUCCEEDED(res) && mSelfSigningCert) {
+ nsTArray<uint8_t> certBytes;
+ res = mSelfSigningCert->GetRawDER(certBytes);
+ NS_ENSURE_SUCCESS(res, res);
+
+ if (certVerifier->VerifyCert(
+ certBytes, certificateUsageEmailSigner, mozilla::pkix::Now(),
+ nullptr, nullptr, builtChain,
+ // Only local checks can run on the main thread.
+ // Skipping OCSP for the user's own cert seems accaptable.
+ CertVerifier::FLAG_LOCAL_ONLY) != mozilla::pkix::Success) {
+ // not suitable for signing, so unset cert and clear pref
+ mSelfSigningCert = nullptr;
+ mSigningCertDBKey.Truncate();
+ aIdentity->SetCharAttribute("signing_cert_dbkey", mSigningCertDBKey);
+ }
+ }
+ }
+
+ // must have both the signing and encryption certs to sign
+ if (!mSelfSigningCert && aSign) {
+ SetError(sendReport, u"NoSenderSigningCert");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSelfEncryptionCert && aEncrypt) {
+ SetError(sendReport, u"NoSenderEncryptionCert");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aEncrypt && mSelfEncryptionCert) {
+ // Make sure self's configured cert is prepared for being used
+ // as an email recipient cert.
+ UniqueCERTCertificate nsscert(mSelfEncryptionCert->GetCert());
+ if (!nsscert) {
+ return NS_ERROR_FAILURE;
+ }
+ // XXX: This does not respect the nsNSSShutDownObject protocol.
+ if (CERT_SaveSMimeProfile(nsscert.get(), nullptr, nullptr) != SECSuccess) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ /* If the message is to be encrypted, then get the recipient certs */
+ if (aEncrypt) {
+ nsTArray<nsCString> mailboxes;
+ ExtractEmails(EncodedHeader(nsDependentCString(aRecipients)),
+ UTF16ArrayAdapter<>(mailboxes));
+ uint32_t count = mailboxes.Length();
+
+ bool already_added_self_cert = false;
+
+ for (uint32_t i = 0; i < count; i++) {
+ nsCString mailbox_lowercase;
+ ToLowerCase(mailboxes[i], mailbox_lowercase);
+ nsCOMPtr<nsIX509Cert> cert;
+
+ nsCString dbKey;
+ res = GetCertDBKeyForEmail(mailbox_lowercase, dbKey);
+ if (NS_SUCCEEDED(res)) {
+ res = certdb->FindCertByDBKey(dbKey, getter_AddRefs(cert));
+ }
+
+ if (NS_FAILED(res) || !cert) {
+ // Failure to find a valid encryption cert is fatal.
+ // Here I assume that mailbox is ascii rather than utf8.
+ SetErrorWithParam(sendReport, "MissingRecipientEncryptionCert",
+ mailboxes[i].get());
+ return res;
+ }
+
+ /* #### see if recipient requests `signedData'.
+ if (...) no_clearsigning_p = true;
+ (This is the only reason we even bother looking up the certs
+ of the recipients if we're sending a signed-but-not-encrypted
+ message.)
+ */
+
+ if (cert.get() == mSelfEncryptionCert.get()) {
+ already_added_self_cert = true;
+ }
+
+ mCerts.AppendElement(cert);
+ }
+
+ if (!already_added_self_cert) {
+ mCerts.AppendElement(mSelfEncryptionCert);
+ }
+ }
+ return res;
+}
+
+NS_IMETHODIMP nsMsgComposeSecure::MimeCryptoWriteBlock(const char* buf,
+ int32_t size) {
+ int status = 0;
+ nsresult rv;
+
+ /* If this is a From line, mangle it before signing it. You just know
+ that something somewhere is going to mangle it later, and that's
+ going to cause the signature check to fail.
+
+ (This assumes that, in the cases where From-mangling must happen,
+ this function is called a line at a time. That happens to be the
+ case.)
+ */
+ if (size >= 5 && buf[0] == 'F' && !strncmp(buf, "From ", 5)) {
+ char mangle[] = ">";
+ nsresult res = MimeCryptoWriteBlock(mangle, 1);
+ if (NS_FAILED(res)) return res;
+ // This value will actually be cast back to an nsresult before use, so this
+ // cast is reasonable under the circumstances.
+ status = static_cast<int>(res);
+ }
+
+ /* If we're signing, or signing-and-encrypting, feed this data into
+ the computation of the hash. */
+ if (mDataHash) {
+ PR_SetError(0, 0);
+ mDataHash->Update((const uint8_t*)buf, size);
+ status = PR_GetError();
+ if (status < 0) goto FAIL;
+ }
+
+ PR_SetError(0, 0);
+ if (mEncryptionContext) {
+ /* If we're encrypting, or signing-and-encrypting, write this data
+ by filtering it through the crypto library. */
+
+ /* We want to create equally sized encryption strings */
+ const char* inputBytesIterator = buf;
+ uint32_t inputBytesLeft = size;
+
+ while (inputBytesLeft) {
+ const uint32_t spaceLeftInBuffer = eBufferSize - mBufferedBytes;
+ const uint32_t bytesToAppend =
+ std::min(inputBytesLeft, spaceLeftInBuffer);
+
+ memcpy(mBuffer + mBufferedBytes, inputBytesIterator, bytesToAppend);
+ mBufferedBytes += bytesToAppend;
+
+ inputBytesIterator += bytesToAppend;
+ inputBytesLeft -= bytesToAppend;
+
+ if (eBufferSize == mBufferedBytes) {
+ rv = mEncryptionContext->Update(mBuffer, mBufferedBytes);
+ mBufferedBytes = 0;
+ if (NS_FAILED(rv)) {
+ status = PR_GetError();
+ PR_ASSERT(status < 0);
+ if (status >= 0) status = -1;
+ goto FAIL;
+ }
+ }
+ }
+ } else {
+ /* If we're not encrypting (presumably just signing) then write this
+ data directly to the file. */
+
+ uint32_t n;
+ rv = mStream->Write(buf, size, &n);
+ if (NS_FAILED(rv) || n < (uint32_t)size) {
+ // XXX MK_MIME_ERROR_WRITING_FILE is -1, which is not a valid nsresult
+ return static_cast<nsresult>(MK_MIME_ERROR_WRITING_FILE);
+ }
+ }
+FAIL:
+ // XXX status sometimes has invalid nsresults like -1 or PR_GetError()
+ // assigned to it
+ return static_cast<nsresult>(status);
+}
+
+/* Returns a string consisting of a Content-Type header, and a boundary
+ string, suitable for moving from the header block, down into the body
+ of a multipart object. The boundary itself is also returned (so that
+ the caller knows what to write to close it off.)
+ */
+static nsresult make_multipart_signed_header_string(bool outer_p,
+ char** header_return,
+ char** boundary_return,
+ int16_t hash_type) {
+ const char* hashStr;
+ *header_return = 0;
+ *boundary_return = mime_make_separator("ms");
+
+ if (!*boundary_return) return NS_ERROR_OUT_OF_MEMORY;
+
+ switch (hash_type) {
+ case nsICryptoHash::SHA1:
+ hashStr = PARAM_MICALG_SHA1;
+ break;
+ case nsICryptoHash::SHA256:
+ hashStr = PARAM_MICALG_SHA256;
+ break;
+ case nsICryptoHash::SHA384:
+ hashStr = PARAM_MICALG_SHA384;
+ break;
+ case nsICryptoHash::SHA512:
+ hashStr = PARAM_MICALG_SHA512;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *header_return = PR_smprintf("Content-Type: " MULTIPART_SIGNED
+ "; "
+ "protocol=\"" APPLICATION_PKCS7_SIGNATURE
+ "\"; "
+ "micalg=%s; "
+ "boundary=\"%s\"" CRLF CRLF
+ "%s%s"
+ "--%s" CRLF,
+ hashStr, *boundary_return,
+ (outer_p ? crypto_multipart_blurb : ""),
+ (outer_p ? CRLF CRLF : ""), *boundary_return);
+
+ if (!*header_return) {
+ PR_Free(*boundary_return);
+ *boundary_return = 0;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+/* Used as the output function of a SEC_PKCS7EncoderContext -- we feed
+ plaintext into the crypto engine, and it calls this function with encrypted
+ data; then this function writes a base64-encoded representation of that
+ data to the file (by filtering it through the given MimeEncoder object.)
+
+ Also used as the output function of SEC_PKCS7Encode() -- but in that case,
+ it's used to write the encoded representation of the signature. The only
+ difference is which MimeEncoder object is used.
+ */
+static void mime_crypto_write_base64(void* closure, const char* buf,
+ unsigned long size) {
+ MimeEncoder* encoder = (MimeEncoder*)closure;
+ nsresult rv = encoder->Write(buf, size);
+ PR_SetError(NS_FAILED(rv) ? static_cast<uint32_t>(rv) : 0, 0);
+}
+
+/* Used as the output function of MimeEncoder -- when we have generated
+ the signature for a multipart/signed object, this is used to write the
+ base64-encoded representation of the signature to the file.
+ */
+// TODO: size should probably be converted to uint32_t
+nsresult mime_encoder_output_fn(const char* buf, int32_t size, void* closure) {
+ nsMsgComposeSecure* state = (nsMsgComposeSecure*)closure;
+ nsCOMPtr<nsIOutputStream> stream;
+ state->GetOutputStream(getter_AddRefs(stream));
+ uint32_t n;
+ nsresult rv = stream->Write((char*)buf, size, &n);
+ if (NS_FAILED(rv) || n < (uint32_t)size)
+ return NS_ERROR_FAILURE;
+ else
+ return NS_OK;
+}
+
+/* Like mime_encoder_output_fn, except this is used for the case where we
+ are both signing and encrypting -- the base64-encoded output of the
+ signature should be fed into the crypto engine, rather than being written
+ directly to the file.
+ */
+static nsresult mime_nested_encoder_output_fn(const char* buf, int32_t size,
+ void* closure) {
+ nsMsgComposeSecure* state = (nsMsgComposeSecure*)closure;
+
+ // Copy to new null-terminated string so JS glue doesn't crash when
+ // MimeCryptoWriteBlock() is implemented in JS.
+ nsCString bufWithNull;
+ bufWithNull.Assign(buf, size);
+ return state->MimeCryptoWriteBlock(bufWithNull.get(), size);
+}
+
+class FindSMimeCertTask final : public CryptoTask {
+ public:
+ FindSMimeCertTask(const nsACString& email,
+ nsIDoneFindCertForEmailCallback* listener)
+ : mEmail(email), mListener(listener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+ ~FindSMimeCertTask();
+
+ private:
+ virtual nsresult CalculateResult() override;
+ virtual void CallCallback(nsresult rv) override;
+
+ const nsCString mEmail;
+ nsCOMPtr<nsIX509Cert> mCert;
+ nsCOMPtr<nsIDoneFindCertForEmailCallback> mListener;
+
+ static mozilla::StaticMutex sMutex;
+};
+
+mozilla::StaticMutex FindSMimeCertTask::sMutex;
+
+void FindSMimeCertTask::CallCallback(nsresult rv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIX509Cert> cert;
+ nsCOMPtr<nsIDoneFindCertForEmailCallback> listener;
+ {
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ if (!mListener) {
+ return;
+ }
+ // We won't need these objects after leaving this function, so let's
+ // destroy them early. Also has the benefit that we're already
+ // on the main thread. By destroying the listener here, we avoid
+ // dispatching in the destructor.
+ mCert.swap(cert);
+ mListener.swap(listener);
+ }
+ listener->FindCertDone(mEmail, cert);
+}
+
+/*
+called by:
+ GetValidCertInfo
+ GetRecipientCertsInfo
+ GetNoCertAddresses
+*/
+nsresult FindSMimeCertTask::CalculateResult() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsresult rv = BlockUntilLoadableCertsLoaded();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
+ NS_ENSURE_TRUE(certVerifier, NS_ERROR_UNEXPECTED);
+
+ const nsCString& flatEmailAddress = PromiseFlatCString(mEmail);
+ UniqueCERTCertList certlist(
+ PK11_FindCertsFromEmailAddress(flatEmailAddress.get(), nullptr));
+ if (!certlist) return NS_ERROR_FAILURE;
+
+ // certlist now contains certificates with the right email address,
+ // but they might not have the correct usage or might even be invalid
+
+ if (CERT_LIST_END(CERT_LIST_HEAD(certlist), certlist))
+ return NS_ERROR_FAILURE; // no certs found
+
+ CERTCertListNode* node;
+ // search for a valid certificate
+ for (node = CERT_LIST_HEAD(certlist); !CERT_LIST_END(node, certlist);
+ node = CERT_LIST_NEXT(node)) {
+ nsTArray<uint8_t> certBytes(node->cert->derCert.data,
+ node->cert->derCert.len);
+ nsTArray<nsTArray<uint8_t>> unusedCertChain;
+
+ mozilla::pkix::Result result = certVerifier->VerifyCert(
+ certBytes, certificateUsageEmailRecipient, mozilla::pkix::Now(),
+ nullptr /*XXX pinarg*/, nullptr /*hostname*/, unusedCertChain);
+ if (result == mozilla::pkix::Success) {
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ mCert = new nsNSSCertificate(node->cert);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+/*
+ * We need to ensure that the callback is destroyed on the main thread.
+ */
+class ProxyListenerDestructor final : public mozilla::Runnable {
+ public:
+ explicit ProxyListenerDestructor(
+ nsCOMPtr<nsIDoneFindCertForEmailCallback>&& aListener)
+ : mozilla::Runnable("ProxyListenerDestructor"),
+ mListener(std::move(aListener)) {}
+
+ NS_IMETHODIMP
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Release the object referenced by mListener.
+ mListener = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIDoneFindCertForEmailCallback> mListener;
+};
+
+FindSMimeCertTask::~FindSMimeCertTask() {
+ // Unless we already cleaned up inside CallCallback, we must release
+ // the listener on the main thread.
+ if (mListener && !NS_IsMainThread()) {
+ RefPtr<ProxyListenerDestructor> runnable =
+ new ProxyListenerDestructor(std::move(mListener));
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable));
+ }
+}
+
+NS_IMETHODIMP
+nsMsgComposeSecure::CacheValidCertForEmail(const nsACString& email,
+ const nsACString& certDBKey) {
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ mValidCertForEmailAddr.InsertOrUpdate(email, certDBKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeSecure::HaveValidCertForEmail(const nsACString& email,
+ bool* _retval) {
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ *_retval = mValidCertForEmailAddr.Get(email, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeSecure::GetCertDBKeyForEmail(const nsACString& email,
+ nsACString& _retval) {
+ mozilla::StaticMutexAutoLock lock(sMutex);
+ nsCString dbKey;
+ bool found = mValidCertForEmailAddr.Get(email, &dbKey);
+ if (found) {
+ _retval = dbKey;
+ } else {
+ _retval.Truncate();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgComposeSecure::AsyncFindCertByEmailAddr(
+ const nsACString& email, nsIDoneFindCertForEmailCallback* callback) {
+ RefPtr<CryptoTask> task = new FindSMimeCertTask(email, callback);
+ return task->Dispatch();
+}
+
+mozilla::StaticMutex nsMsgComposeSecure::sMutex;
diff --git a/comm/mailnews/extensions/smime/nsMsgComposeSecure.h b/comm/mailnews/extensions/smime/nsMsgComposeSecure.h
new file mode 100644
index 0000000000..14f7b54157
--- /dev/null
+++ b/comm/mailnews/extensions/smime/nsMsgComposeSecure.h
@@ -0,0 +1,103 @@
+/* -*- Mode: idl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+#ifndef _nsMsgComposeSecure_H_
+#define _nsMsgComposeSecure_H_
+
+#include "nsIMsgComposeSecure.h"
+#include "nsCOMPtr.h"
+#include "nsICMSEncoder.h"
+#include "nsIX509Cert.h"
+#include "nsIStringBundle.h"
+#include "nsICryptoHash.h"
+#include "nsICMSMessage.h"
+#include "nsString.h"
+#include "nsTHashMap.h"
+#include "nsIOutputStream.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/StaticMutex.h"
+
+class nsIMsgCompFields;
+namespace mozilla {
+namespace mailnews {
+class MimeEncoder;
+}
+} // namespace mozilla
+
+typedef enum {
+ mime_crypto_none, /* normal unencapsulated MIME message */
+ mime_crypto_clear_signed, /* multipart/signed encapsulation */
+ mime_crypto_opaque_signed, /* application/x-pkcs7-mime (signedData) */
+ mime_crypto_encrypted, /* application/x-pkcs7-mime */
+ mime_crypto_signed_encrypted /* application/x-pkcs7-mime */
+} mimeDeliveryCryptoState;
+
+class nsMsgComposeSecure : public nsIMsgComposeSecure {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOMPOSESECURE
+
+ nsMsgComposeSecure();
+
+ void GetOutputStream(nsIOutputStream** stream) {
+ NS_IF_ADDREF(*stream = mStream);
+ }
+ nsresult GetSMIMEBundleString(const char16_t* name, nsString& outString);
+
+ private:
+ virtual ~nsMsgComposeSecure();
+ typedef mozilla::mailnews::MimeEncoder MimeEncoder;
+ nsresult MimeInitMultipartSigned(bool aOuter, nsIMsgSendReport* sendReport);
+ nsresult MimeInitEncryption(bool aSign, nsIMsgSendReport* sendReport);
+ nsresult MimeFinishMultipartSigned(bool aOuter, nsIMsgSendReport* sendReport);
+ nsresult MimeFinishEncryption(bool aSign, nsIMsgSendReport* sendReport);
+ nsresult MimeCryptoHackCerts(const char* aRecipients,
+ nsIMsgSendReport* sendReport, bool aEncrypt,
+ bool aSign, nsIMsgIdentity* aIdentity);
+ bool InitializeSMIMEBundle();
+ nsresult SMIMEBundleFormatStringFromName(const char* name,
+ nsTArray<nsString>& params,
+ nsAString& outString);
+ nsresult ExtractEncryptionState(nsIMsgIdentity* aIdentity,
+ nsIMsgCompFields* aComposeFields,
+ bool* aSignMessage, bool* aEncrypt);
+
+ bool mSignMessage;
+ bool mAlwaysEncryptMessage;
+ mimeDeliveryCryptoState mCryptoState;
+ nsCOMPtr<nsIOutputStream> mStream;
+ int16_t mHashType;
+ nsCOMPtr<nsICryptoHash> mDataHash;
+ mozilla::UniquePtr<MimeEncoder> mSigEncoder;
+ char* mMultipartSignedBoundary;
+ nsString mSigningCertName;
+ nsAutoCString mSigningCertDBKey;
+ nsCOMPtr<nsIX509Cert> mSelfSigningCert;
+ nsString mEncryptionCertName;
+ nsAutoCString mEncryptionCertDBKey;
+ nsCOMPtr<nsIX509Cert> mSelfEncryptionCert;
+ nsTArray<RefPtr<nsIX509Cert>> mCerts;
+ nsCOMPtr<nsICMSMessage> mEncryptionCinfo;
+ nsCOMPtr<nsICMSEncoder> mEncryptionContext;
+ nsCOMPtr<nsIStringBundle> mSMIMEBundle;
+
+ // Maps email address to nsIX509Cert.dbKey of a verified certificate.
+ nsTHashMap<nsCStringHashKey, nsCString> mValidCertForEmailAddr;
+ static mozilla::StaticMutex sMutex;
+
+ mozilla::UniquePtr<MimeEncoder> mCryptoEncoder;
+ bool mIsDraft;
+
+ enum { eBufferSize = 8192 };
+ char* mBuffer;
+ uint32_t mBufferedBytes;
+
+ bool mErrorAlreadyReported;
+ void SetError(nsIMsgSendReport* sendReport, const char16_t* bundle_string);
+ void SetErrorWithParam(nsIMsgSendReport* sendReport,
+ const char* bundle_string, const char* param);
+};
+
+#endif
diff --git a/comm/mailnews/imap/public/moz.build b/comm/mailnews/imap/public/moz.build
new file mode 100644
index 0000000000..d6e40a3e80
--- /dev/null
+++ b/comm/mailnews/imap/public/moz.build
@@ -0,0 +1,29 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIAutoSyncFolderStrategy.idl",
+ "nsIAutoSyncManager.idl",
+ "nsIAutoSyncMsgStrategy.idl",
+ "nsIAutoSyncState.idl",
+ "nsIImapFlagAndUidState.idl",
+ "nsIImapHeaderXferInfo.idl",
+ "nsIImapIncomingServer.idl",
+ "nsIImapMailFolderSink.idl",
+ "nsIImapMessageSink.idl",
+ "nsIImapMockChannel.idl",
+ "nsIImapOfflineSync.idl",
+ "nsIImapProtocol.idl",
+ "nsIImapProtocolSink.idl",
+ "nsIImapServerSink.idl",
+ "nsIImapService.idl",
+ "nsIImapUrl.idl",
+ "nsIMailboxSpec.idl",
+ "nsIMsgImapMailFolder.idl",
+]
+
+XPIDL_MODULE = "msgimap"
+
+EXPORTS += []
diff --git a/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl b/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl
new file mode 100644
index 0000000000..23ada6d468
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIAutoSyncMsgStrategy.idl"
+
+interface nsIMsgFolder;
+
+[scriptable, uuid(d3bf91cc-37bb-4752-9994-1a8473e46a90)]
+interface nsIAutoSyncFolderStrategy : nsISupports {
+
+ /**
+ * Returns a relative-priority for the second folder by comparing with the first one.
+ */
+ nsAutoSyncStrategyDecisionType sort(in nsIMsgFolder aFolder1, in nsIMsgFolder aFolder2);
+
+ /**
+ * Tests whether the given folder should be excluded or not.
+ */
+ boolean isExcluded(in nsIMsgFolder aFolder);
+
+};
diff --git a/comm/mailnews/imap/public/nsIAutoSyncManager.idl b/comm/mailnews/imap/public/nsIAutoSyncManager.idl
new file mode 100644
index 0000000000..531b9e702b
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIAutoSyncManager.idl
@@ -0,0 +1,192 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAutoSyncMsgStrategy;
+interface nsIAutoSyncFolderStrategy;
+interface nsIMsgDBHdr;
+interface nsIAutoSyncState;
+interface nsIAutoSyncMgrListener;
+interface nsIMsgFolder;
+
+[scriptable, uuid(41ec36a7-1a53-4ca3-b698-dca6452a8761)]
+interface nsIAutoSyncMgrListener : nsISupports {
+
+ /**
+ * Queue types
+ */
+ const long PriorityQueue = 1;
+ const long UpdateQueue = 2;
+ const long DiscoveryQueue = 3;
+
+ /**
+ * It is called on the listener when a new folder is added into
+ * the queue
+ *
+ * @param aQType type of the queue
+ * @param aFolder folder that is added into the queue
+ */
+ void onFolderAddedIntoQ(in long aQType, in nsIMsgFolder aFolder);
+
+ /**
+ * It is called on the listener when a folder is removed from
+ * the queue
+ *
+ * @param aQType type of the queue
+ * @param aFolder folder that is removed from the queue
+ */
+ void onFolderRemovedFromQ(in long aQType, in nsIMsgFolder aFolder);
+
+ /**
+ * It is called on the listener when a message download is successfully started
+ *
+ * @param aFolder folder in which the download is started
+ * @param aNumberOfMessages number of the messages that will be downloaded
+ * @param aTotalPending total number of messages waiting to be downloaded
+ */
+ void onDownloadStarted(in nsIMsgFolder aFolder, in unsigned long aNumberOfMessages,
+ in unsigned long aTotalPending);
+ /**
+ * It is called on the listener when a message download on the given folder
+ * is completed
+ */
+ void onDownloadCompleted(in nsIMsgFolder aFolder);
+
+ /**
+ * It is called on the listener when an error occurs during the message download
+ */
+ void onDownloadError(in nsIMsgFolder aFolder);
+
+ /*
+ * Auto-Sync manager is running or waiting for idle
+ */
+ void onStateChanged(in boolean aRunning);
+
+ /**
+ * It is called on the listener after the auto-sync manager starts to process
+ * existing headers of the given folder to find missing message bodies
+ * (mostly for debugging purposes)
+ */
+ void onDiscoveryQProcessed(in nsIMsgFolder aFolder, in unsigned long aNumberOfHdrsProcessed,
+ in unsigned long aLeftToProcess);
+
+ /**
+ * It is called on the listener after the auto-sync manager updates the given folder
+ * (mostly for debugging purposes)
+ */
+ void onAutoSyncInitiated(in nsIMsgFolder aFolder);
+};
+
+
+[scriptable, uuid(7fe0b48e-f5d8-4747-beb7-888c9cced3a5)]
+interface nsIAutoSyncManager : nsISupports {
+
+ /**
+ * Download models
+ */
+ const long dmParallel = 0;
+ const long dmChained = 1;
+
+ /**
+ * Suggested minimum grouping size in bytes for message downloads.
+ * Setting this attribute to 0 resets its value to the
+ * hardcoded default.
+ */
+ attribute unsigned long groupSize;
+
+ /**
+ * Active strategy function to prioritize
+ * messages in the download queue
+ */
+ attribute nsIAutoSyncMsgStrategy msgStrategy;
+
+ /**
+ * Active strategy function to prioritize
+ * folders in the download queue
+ */
+ attribute nsIAutoSyncFolderStrategy folderStrategy;
+
+ /**
+ * Adds a listener to notify about auto-sync events
+ */
+ void addListener(in nsIAutoSyncMgrListener aListener);
+
+ /**
+ * Removes the listener from notification list
+ */
+ void removeListener(in nsIAutoSyncMgrListener aListener);
+
+ /**
+ * Tests the given message to make sure that whether
+ * it fits the download criteria or not
+ */
+ boolean doesMsgFitDownloadCriteria(in nsIMsgDBHdr aMsgHdr);
+
+ /**
+ * Called by the nsAutoSyncState object when the download
+ * queue is changed. Given interface is already addref'd.
+ */
+ void onDownloadQChanged(in nsIAutoSyncState aAutoSyncStateObj);
+
+ /**
+ * Called by the nsAutoSyncState object when the download
+ * is started. Given interface is already addref'd.
+ */
+ void onDownloadStarted(in nsIAutoSyncState aAutoSyncStateObj, in nsresult aStartCode);
+
+ /**
+ * Called by the nsAutoSyncState object when the download
+ * completed. Given interface is already addref'd.
+ */
+ void onDownloadCompleted(in nsIAutoSyncState aAutoSyncStateObj, in nsresult aExitCode);
+
+ /**
+ * Number of elements in the discovery queue.
+ * @see nsAutoSyncManager.h for details
+ */
+ readonly attribute unsigned long discoveryQLength;
+
+ /**
+ * Number of elements in the update queue.
+ * @see nsAutoSyncManager.h for details
+ */
+ readonly attribute unsigned long updateQLength;
+
+ /**
+ * Number of elements in the download queue (a.k.a priority queue).
+ * @see nsAutoSyncManager.h for details
+ */
+ readonly attribute unsigned long downloadQLength;
+
+ /**
+ * Active download model; Chained (serial), or Parallel
+ */
+ attribute long downloadModel;
+
+ /**
+ * The imap folder corresponding to aAutoSyncState has had a message
+ * added to it. Autosync may want to add this folder to the update q.
+ *
+ * @param aAutoSyncState state obj for folder needing updating
+ */
+ void onFolderHasPendingMsgs(in nsIAutoSyncState aAutoSyncState);
+
+ /// Pause autosync (e.g., we're downloading for offline).
+ void pause();
+
+ /// Resume normal autosync activities (e.g., we've come back online).
+ void resume();
+};
+
+%{C++
+#define NS_AUTOSYNCMANAGER_CID \
+{ /* C358C568-47B2-42b2-8146-3C0F8D1FAD6E */ \
+ 0xc358c568, 0x47b2, 0x42b2, \
+ { 0x81, 0x46, 0x3c, 0xf, 0x8d, 0x1f, 0xad, 0x6e }}
+#define NS_AUTOSYNCMANAGER_CLASSNAME \
+ "Auto-Sync Manager"
+#define NS_AUTOSYNCMANAGER_CONTRACTID \
+ "@mozilla.org/imap/autosyncmgr;1"
+%}
diff --git a/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl b/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl
new file mode 100644
index 0000000000..773c62426d
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl
@@ -0,0 +1,35 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+
+typedef long nsAutoSyncStrategyDecisionType;
+
+[scriptable,uuid(0365bec5-3753-43c2-b13e-441747815f37)]
+interface nsAutoSyncStrategyDecisions : nsISupports
+{
+ /// same priority
+ const nsAutoSyncStrategyDecisionType Same = 0x00000001;
+ /// higher priority
+ const nsAutoSyncStrategyDecisionType Higher = 0x00000002;
+ /// lower priority
+ const nsAutoSyncStrategyDecisionType Lower = 0x00000004;
+};
+
+[scriptable, uuid(9cb4baff-3112-4cf8-8463-f81b0aa78f93)]
+interface nsIAutoSyncMsgStrategy : nsISupports {
+
+ /**
+ * Returns a relative-priority for the second message by comparing with the first message.
+ */
+ nsAutoSyncStrategyDecisionType sort(in nsIMsgFolder aFolder, in nsIMsgDBHdr aMsgHdr1, in nsIMsgDBHdr aMsgHdr2);
+
+ /**
+ * Tests whether the given message should be excluded or not.
+ */
+ boolean isExcluded(in nsIMsgFolder aFolder, in nsIMsgDBHdr aMsgHdr);
+};
diff --git a/comm/mailnews/imap/public/nsIAutoSyncState.idl b/comm/mailnews/imap/public/nsIAutoSyncState.idl
new file mode 100644
index 0000000000..7d2c155435
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIAutoSyncState.idl
@@ -0,0 +1,157 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+
+[scriptable, uuid(7512f927-b8f0-48c4-b101-03e859e61281)]
+interface nsIAutoSyncState : nsISupports {
+
+ /**
+ * Auto-Sync states.
+ *
+ * ***WARNING***: If you change these, be sure to update stateStrings in
+ * nsAutoSyncState.cpp. If you do not, out-of-bounds memory accesses may
+ * happen.
+ */
+
+ /**
+ * Initial state. Returned to after new message bodies downloaded or if
+ * it is determined that no body fetches are actually needed. The next state
+ * is typically stStatusIssued but a transition directly to stUpdateNeeded
+ * may also occur.
+ */
+ const long stCompletedIdle = 0;
+
+ /**
+ * STATUS issued (URL "folderstatus"). Will check to see if new messages
+ * are present. If new messages are detected, the next state is
+ * stUpdateIssued; otherwise, a return to stCompleteIdle occurs. The previous
+ * state is always stCompleteIdle.
+ */
+ const long stStatusIssued = 1;
+
+ /**
+ * Occurs when imap APPEND and sometimes COPY or MOVE imap commands add
+ * messages that may not be detected in autosync stStatusIssued state. These
+ * are the "pending" messages referred to in the autosync code.
+ * stUpdateIssued state (URL "select") will occur next.
+ * The previous state is always stCompleteIdle, skipping stStatusIssued.
+ */
+ const long stUpdateNeeded = 2;
+
+ /**
+ * Update issued (URL "select"). Will figure out if there are any bodies to
+ * download after new headers are fetched and, if so, move to state
+ * stReadyToDownload. Otherwise, returns to stCompleteIdle.
+ */
+ const long stUpdateIssued = 3;
+
+ /**
+ * Preparing to download the next group of message bodies then move to
+ * stDownloadInProgress
+ */
+ const long stReadyToDownload = 4;
+
+ /**
+ * Fetch bodies issued (URL "fetch"). Group of message bodies download in
+ * progress. If more are needed, next state is stReadyToDownload; otherwise,
+ * when all are received, the next state is stCompleteIdle.
+ */
+ const long stDownloadInProgress = 5;
+
+ /**
+ * Puts the download queue offset to its previous position.
+ */
+ void rollback();
+
+ /**
+ * Clears the download queue. Resets the offsets.
+ */
+ void resetDownloadQ();
+
+ /**
+ * Rollbacks the offset to the previous position and
+ * changes the state to ready-to-download.
+ */
+ void tryCurrentGroupAgain(in unsigned long aRetryCount);
+
+ /**
+ * Resets the retry counter.
+ */
+ void resetRetryCounter();
+
+ /**
+ * Tests whether the given folder has the same imap server.
+ */
+ boolean isSibling(in nsIAutoSyncState aAnotherStateObj);
+
+ /**
+ * Update the folder to find new message headers to download
+ */
+ void updateFolder();
+
+ /**
+ * Downloads the bodies of the given messages from the server.
+ */
+ void downloadMessagesForOffline(in Array<nsIMsgDBHdr> aMessageList);
+
+ /**
+ * Returns an array containing the nsIMsgDBHdrs of the messages that will
+ * be downloaded next.
+ *
+ * @param aSuggestedGroupSizeLimit suggested size per group in bytes
+ * @param aActualGroupSize total size of the messages in bytes in the group
+ */
+ Array<nsIMsgDBHdr> getNextGroupOfMessages(in unsigned long aSuggestedGroupSizeLimit,
+ out unsigned long aActualGroupSize);
+
+ /**
+ * Iterates through the existing headers of the folder to find
+ * the messages not downloaded yet.
+ *
+ * @param aNumberOfHeadersToProcess number of headers to be processed
+ * at this pass
+ *
+ * @return the number of headers left to process
+ */
+ unsigned long processExistingHeaders(in unsigned long aNumberOfHeadersToProcess);
+
+ /**
+ * Tests whether the download queue is empty.
+ */
+ boolean isDownloadQEmpty();
+
+ /**
+ * Last time the existing headers are completely processed.
+ */
+ [noscript]readonly attribute PRTime lastSyncTime;
+
+ /**
+ * Last time the owner folder is updated.
+ */
+ [noscript]attribute PRTime lastUpdateTime;
+
+ /**
+ * Download operation state.
+ */
+ attribute long state;
+
+ /**
+ * Number of messages waiting to be downloaded.
+ */
+ readonly attribute long pendingMessageCount;
+
+ /**
+ * Total number of messages in the download queue.
+ */
+ readonly attribute long totalMessageCount;
+
+ /**
+ * The folder this auto-sync object is related to.
+ */
+ readonly attribute nsIMsgFolder ownerFolder;
+};
diff --git a/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl b/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl
new file mode 100644
index 0000000000..a65d1a81c2
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(360848be-f694-40db-80ef-793a2c43ddcb)]
+interface nsIImapFlagAndUidState : nsISupports
+{
+ readonly attribute long numberOfMessages;
+ readonly attribute long numberOfRecentMessages;
+
+ /**
+ * If a full update, the total number of deleted messages
+ * in the folder; if a partial update, the number of deleted
+ * messages in the partial update
+ **/
+ readonly attribute long numberOfDeletedMessages;
+
+ /**
+ * If this is true, instead of fetching 1:* (FLAGS), and putting all
+ * UIDs and flags in the array, we only fetched the uids and flags
+ * that changed since the last time we were selected on this folder.
+ * This means we have a sparse array, and should not assume missing
+ * UIDs have been deleted.
+ **/
+ readonly attribute boolean partialUIDFetch;
+
+ /**
+ * Set of flags the server supports storing per message. See nsImapCore.h
+ * for the set of flags.
+ */
+ readonly attribute unsigned short supportedUserFlags;
+
+ /**
+ * Check if a UID is in the state.
+ * @param uid - The message UID.
+ * @returns True if UID is in the state.
+ */
+ boolean hasMessage(in unsigned long uid);
+
+ /**
+ * OR's the passed in flags with the previous flags because we want to
+ * accumulate the FLAGS and PERMANENTFLAGS response.
+ *
+ * @param aFlags - flags to OR with current flags.
+ */
+ void orSupportedUserFlags(in unsigned short aFlags);
+
+ unsigned long getUidOfMessage(in long zeroBasedIndex);
+ unsigned short getMessageFlags(in long zeroBasedIndex);
+ void setMessageFlags(in long zeroBasedIndex, in unsigned short flags);
+ void expungeByIndex(in unsigned long zeroBasedIndex);
+ void addUidFlagPair(in unsigned long uid, in unsigned short flags, in unsigned long zeroBasedIndex);
+ void addUidCustomFlagPair(in unsigned long uid, in string customFlag);
+ /**
+ * Get the message flags by the message UID.
+ * @param uid - The message UID.
+ * @returns The message flags.
+ */
+ unsigned short getMessageFlagsByUid(in unsigned long uid);
+ string getCustomFlags(in unsigned long uid); // returns space-separated keywords
+ void reset();
+ void clearCustomFlags(in unsigned long uid);
+ /**
+ * Adds custom attributes to a hash table for the purpose of storing them
+ * them.
+ * @param aUid UID of the associated msg
+ * @param aCustomAttributeName Name of the custom attribute value
+ * @param aCustomAttributeValue Value of the attribute,
+ */
+ void setCustomAttribute(in unsigned long aUid,
+ in ACString aCustomAttributeName,
+ in ACString aCustomAttributeValue);
+
+ /**
+ * Gets the custom attributes from the hash table where they were stored earlier
+ * them.
+ * @param aUid UID of the associated msg
+ * @param aCustomAttributeName Name of the custom attribute value
+ * @param aCustomAttributeValue Value of the attribute,
+ */
+ ACString getCustomAttribute(in unsigned long aUid,
+ in ACString aCustomAttributeName);
+};
diff --git a/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl b/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl
new file mode 100644
index 0000000000..74718e056e
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(38f8f784-b092-11d6-ba4b-00108335942a)]
+interface nsIImapHeaderInfo : nsISupports {
+ attribute nsMsgKey msgUid;
+ attribute long msgSize;
+ readonly attribute ACString msgHdrs;
+ void cacheLine(in string line, in unsigned long uid);
+ void resetCache();
+};
+
+[scriptable, uuid(f0842eda-af29-4ecd-82e1-fba91bd65d66)]
+interface nsIImapHeaderXferInfo : nsISupports {
+ readonly attribute long numHeaders;
+ nsIImapHeaderInfo getHeader(in long hdrIndex);
+};
diff --git a/comm/mailnews/imap/public/nsIImapHostSessionList.h b/comm/mailnews/imap/public/nsIImapHostSessionList.h
new file mode 100644
index 0000000000..456bbdf7dc
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapHostSessionList.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsIImapHostSessionList_H_
+#define _nsIImapHostSessionList_H_
+
+#include "nsISupports.h"
+#include "nsImapCore.h"
+#include "../src/nsImapNamespace.h"
+
+class nsIImapIncomingServer;
+
+// f4d89e3e-77da-492c-962b-7835f0742c22
+#define NS_IIMAPHOSTSESSIONLIST_IID \
+ { \
+ 0xf4d89e3e, 0x77da, 0x492c, { \
+ 0x96, 0x2b, 0x78, 0x35, 0xf0, 0x74, 0x2c, 0x22 \
+ } \
+ }
+
+// this is an interface to a linked list of host info's
+class nsIImapHostSessionList : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IIMAPHOSTSESSIONLIST_IID)
+
+ // Host List
+ NS_IMETHOD AddHostToList(const char* serverKey,
+ nsIImapIncomingServer* server) = 0;
+ NS_IMETHOD ResetAll() = 0;
+
+ // Capabilities
+ NS_IMETHOD GetHostHasAdminURL(const char* serverKey, bool& result) = 0;
+ NS_IMETHOD SetHostHasAdminURL(const char* serverKey, bool hasAdminUrl) = 0;
+ // Subscription
+ NS_IMETHOD GetHostIsUsingSubscription(const char* serverKey,
+ bool& result) = 0;
+ NS_IMETHOD SetHostIsUsingSubscription(const char* serverKey,
+ bool usingSubscription) = 0;
+
+ // Passwords
+ NS_IMETHOD GetPasswordForHost(const char* serverKey, nsString& result) = 0;
+ NS_IMETHOD SetPasswordForHost(const char* serverKey,
+ const nsAString& password) = 0;
+ NS_IMETHOD GetPasswordVerifiedOnline(const char* serverKey, bool& result) = 0;
+ NS_IMETHOD SetPasswordVerifiedOnline(const char* serverKey) = 0;
+
+ // OnlineDir
+ NS_IMETHOD GetOnlineDirForHost(const char* serverKey, nsString& result) = 0;
+ NS_IMETHOD SetOnlineDirForHost(const char* serverKey,
+ const char* onlineDir) = 0;
+
+ // Delete is move to trash folder
+ NS_IMETHOD GetDeleteIsMoveToTrashForHost(const char* serverKey,
+ bool& result) = 0;
+ NS_IMETHOD SetDeleteIsMoveToTrashForHost(const char* serverKey,
+ bool isMoveToTrash) = 0;
+ NS_IMETHOD GetShowDeletedMessagesForHost(const char* serverKey,
+ bool& result) = 0;
+
+ NS_IMETHOD SetShowDeletedMessagesForHost(const char* serverKey,
+ bool showDeletedMessages) = 0;
+
+ // Get namespaces
+ NS_IMETHOD GetGotNamespacesForHost(const char* serverKey, bool& result) = 0;
+ NS_IMETHOD SetGotNamespacesForHost(const char* serverKey,
+ bool gotNamespaces) = 0;
+
+ // Folders
+ NS_IMETHOD SetHaveWeEverDiscoveredFoldersForHost(const char* serverKey,
+ bool discovered) = 0;
+ NS_IMETHOD GetHaveWeEverDiscoveredFoldersForHost(const char* serverKey,
+ bool& result) = 0;
+ NS_IMETHOD SetDiscoveryForHostInProgress(const char* serverKey,
+ bool inProgress) = 0;
+ NS_IMETHOD GetDiscoveryForHostInProgress(const char* serverKey,
+ bool& result) = 0;
+
+ // Trash Folder
+ NS_IMETHOD SetOnlineTrashFolderExistsForHost(const char* serverKey,
+ bool exists) = 0;
+ NS_IMETHOD GetOnlineTrashFolderExistsForHost(const char* serverKey,
+ bool& result) = 0;
+
+ // INBOX
+ NS_IMETHOD GetOnlineInboxPathForHost(const char* serverKey,
+ nsString& result) = 0;
+ NS_IMETHOD GetShouldAlwaysListInboxForHost(const char* serverKey,
+ bool& result) = 0;
+ NS_IMETHOD SetShouldAlwaysListInboxForHost(const char* serverKey,
+ bool shouldList) = 0;
+
+ // Namespaces
+ NS_IMETHOD GetNamespaceForMailboxForHost(const char* serverKey,
+ const char* mailbox_name,
+ nsImapNamespace*& result) = 0;
+ NS_IMETHOD SetNamespaceFromPrefForHost(const char* serverKey,
+ const char* namespacePref,
+ EIMAPNamespaceType type) = 0;
+ NS_IMETHOD AddNewNamespaceForHost(const char* serverKey,
+ nsImapNamespace* ns) = 0;
+ NS_IMETHOD ClearServerAdvertisedNamespacesForHost(const char* serverKey) = 0;
+ NS_IMETHOD ClearPrefsNamespacesForHost(const char* serverKey) = 0;
+ NS_IMETHOD GetDefaultNamespaceOfTypeForHost(const char* serverKey,
+ EIMAPNamespaceType type,
+ nsImapNamespace*& result) = 0;
+ NS_IMETHOD SetNamespacesOverridableForHost(const char* serverKey,
+ bool overridable) = 0;
+ NS_IMETHOD GetNamespacesOverridableForHost(const char* serverKey,
+ bool& result) = 0;
+ NS_IMETHOD GetNumberOfNamespacesForHost(const char* serverKey,
+ uint32_t& result) = 0;
+ NS_IMETHOD GetNamespaceNumberForHost(const char* serverKey, int32_t n,
+ nsImapNamespace*& result) = 0;
+ // ### dmb hoo boy, how are we going to do this?
+ NS_IMETHOD CommitNamespacesForHost(nsIImapIncomingServer* server) = 0;
+ NS_IMETHOD FlushUncommittedNamespacesForHost(const char* serverKey,
+ bool& result) = 0;
+
+ // Hierarchy Delimiters
+ NS_IMETHOD SetNamespaceHierarchyDelimiterFromMailboxForHost(
+ const char* serverKey, const char* boxName, char delimiter) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIImapHostSessionList,
+ NS_IIMAPHOSTSESSIONLIST_IID)
+
+#endif
diff --git a/comm/mailnews/imap/public/nsIImapIncomingServer.idl b/comm/mailnews/imap/public/nsIImapIncomingServer.idl
new file mode 100644
index 0000000000..25ff1c59c0
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapIncomingServer.idl
@@ -0,0 +1,114 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIImapUrl;
+interface nsIImapProtocol;
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+
+typedef long nsMsgImapDeleteModel;
+
+[scriptable, uuid(bbfc33de-fe89-11d3-a564-0060b0fc04b7)]
+interface nsMsgImapDeleteModels : nsISupports
+{
+ const long IMAPDelete = 0; /* delete with a big red x */
+ const long MoveToTrash = 1; /* delete moves message to the trash */
+ const long DeleteNoTrash = 2; /* delete is shift delete - don't create or use trash */
+};
+
+[scriptable, uuid(ea6a0765-07b8-40df-924c-9004ed707251)]
+interface nsIImapIncomingServer : nsISupports {
+
+ attribute long maximumConnectionsNumber;
+ attribute boolean forceSelect;
+ attribute long timeOutLimits;
+ attribute AUTF8String adminUrl;
+ attribute ACString serverDirectory;
+ /// RFC 2971 ID response stored as a pref
+ attribute AUTF8String serverIDPref;
+ attribute boolean cleanupInboxOnExit;
+ attribute nsMsgImapDeleteModel deleteModel;
+ attribute boolean dualUseFolders;
+ attribute ACString personalNamespace;
+ attribute ACString publicNamespace;
+ attribute ACString otherUsersNamespace;
+ attribute boolean offlineDownload;
+ attribute boolean overrideNamespaces;
+ attribute boolean usingSubscription;
+ attribute AUTF8String manageMailAccountUrl;
+ attribute boolean fetchByChunks;
+ attribute boolean sendID;
+ attribute boolean isAOLServer;
+ attribute boolean useIdle;
+ attribute boolean checkAllFoldersForNew;
+
+ /// Is this a GMail Server?
+ attribute boolean isGMailServer;
+
+ /**
+ * See IMAP RFC 4551
+ **/
+ attribute boolean useCondStore;
+
+ /**
+ * See IMAP RFC 4978
+ */
+ attribute boolean useCompressDeflate;
+
+ /**
+ * This contains a folder path, for example INBOX/Trash. Note that the
+ * account manager sets this attribute to the path of the trash folder the
+ * user has chosen.
+ */
+ attribute AString trashFolderName;
+
+ attribute boolean downloadBodiesOnGetNewMail;
+ attribute boolean autoSyncOfflineStores;
+
+ /// Max age of messages we will autosync to, or keep in offline store.
+ attribute long autoSyncMaxAgeDays;
+
+ /**
+ * See IMAP RFC 6855
+ */
+ attribute boolean allowUTF8Accept;
+
+ void GetImapConnectionAndLoadUrl(in nsIImapUrl aImapUrl,
+ in nsISupports aConsumer);
+
+ void RemoveConnection(in nsIImapProtocol aImapConnection);
+ void ResetNamespaceReferences();
+ void pseudoInterruptMsgLoad(in nsIMsgFolder aImapFolder, in nsIMsgWindow aMsgWindow, out boolean interrupted);
+ void ResetConnection(in AUTF8String folderName);
+ void CloseConnectionForFolder(in nsIMsgFolder aMsgFolder);
+ void reDiscoverAllFolders();
+ nsIURI subscribeToFolder(in AString name, in boolean subscribe);
+ void GetNewMessagesForNonInboxFolders(in nsIMsgFolder aRootFolder,
+ in nsIMsgWindow aWindow,
+ in boolean forceAllFolders,
+ in boolean performingBiff);
+ unsigned long long getCapability();
+
+ /**
+ * Get the password from the nsIMsgIncomingServer. May prompt the user
+ * if there's no password in the password manager or cached in the
+ * server object.
+ * @param aWindow msgWindow to associate the password prompt with
+ * @return Password string.
+ * @exception NS_ERROR_FAILURE The password could not be obtained.
+ * @note NS_MSG_PASSWORD_PROMPT_CANCELLED is a success code that is returned
+ * if the prompt was presented to the user but the user cancelled the
+ * prompt.
+ */
+ AString PromptPassword(in nsIMsgWindow aWindow);
+ attribute boolean doingLsub;
+
+ ACString getUriWithNamespacePrefixIfNecessary(in long namespaceType, in AUTF8String originalUri);
+ attribute boolean shuttingDown;
+ attribute boolean utf8AcceptEnabled;
+};
diff --git a/comm/mailnews/imap/public/nsIImapMailFolderSink.idl b/comm/mailnews/imap/public/nsIImapMailFolderSink.idl
new file mode 100644
index 0000000000..6868682640
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapMailFolderSink.idl
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+#include "nsIImapProtocol.idl"
+#include "nsIMailboxSpec.idl"
+
+interface nsIMsgMailNewsUrl;
+interface nsIImapMockChannel;
+interface nsIImapHeaderXferInfo;
+
+typedef long ImapOnlineCopyState;
+
+[scriptable, uuid(5f7484b0-68b4-11d3-a53e-0060b0fc04b7)]
+interface ImapOnlineCopyStateType : nsISupports
+{
+ const long kInProgress = 0;
+ const long kSuccessfulCopy = 1;
+ const long kSuccessfulMove = 2;
+ const long kSuccessfulDelete = 3;
+ const long kFailedDelete = 4;
+ const long kReadyForAppendData = 5;
+ const long kFailedAppend = 6;
+ const long kInterruptedState = 7;
+ const long kFailedCopy = 8;
+ const long kFailedMove = 9;
+};
+
+/**
+ * nsIImapMailFolderSink provides a way for the IMAP system to communicate
+ * with the local folder representation.
+ *
+ * The IMAP system could poke folders directly, but going through this
+ * interface has a couple of benefits:
+ *
+ * 1. It better defines the public coupling between the two systems.
+ * 2. It's easier to wrap with a proxy class so the IMAP system can safely
+ * call the methods across thread boundaries (see ImapMailFolderSinkProxy).
+ */
+[scriptable, uuid(525e1278-a39d-46d6-9dbc-b48c7e1d4faa)]
+interface nsIImapMailFolderSink : nsISupports {
+ attribute boolean folderNeedsACLListed;
+ attribute boolean folderNeedsSubscribing;
+ attribute boolean folderNeedsAdded;
+ attribute unsigned long aclFlags;
+ attribute long uidValidity;
+ /**
+ * Whether we have asked the server for this folder's quota information.
+ * If the server supports quotas, this occurs when the folder is opened.
+ */
+ attribute boolean folderQuotaCommandIssued;
+
+ /**
+ * Set FolderQuotaData information
+ * @param aAction Invalidate, store or validate the quota data.
+ * Remaining params are relevant only for store.
+ * @param aFolderQuotaRoot The IMAP quotaroot and resource names for this
+ * folder separated by a slash as obtained from the
+ * GETQUOTAROOT IMAP command response.
+ * @param aFolderQuotaUsage Amount of resourse in use, in KB for STORAGE
+ * resource.
+ * @param aFolderQuotaLimit Maximum usage allowed for this resource.
+ **/
+ void setFolderQuotaData(
+ in unsigned long aAction, in ACString aFolderQuotaRoot,
+ in unsigned long long aFolderQuotaUsed, in unsigned long long aFolderQuotaLimit);
+
+ /// Should we download all the rfc822 headers of messages, instead of subset.
+ readonly attribute boolean shouldDownloadAllHeaders;
+ readonly attribute char onlineDelimiter;
+ void OnNewIdleMessages();
+ // Tell mail master about the newly selected mailbox
+ void UpdateImapMailboxInfo(in nsIImapProtocol aProtocol,
+ in nsIMailboxSpec aSpec);
+ void UpdateImapMailboxStatus(in nsIImapProtocol aProtocol,
+ in nsIMailboxSpec aSpec);
+ /**
+ * Used when downloading headers in chunks.
+ * @param aSpec Mailbox spec of folder we're downloading headers for.
+ * @returns true if more to download, false otherwise.
+ * @returns total count of headers to download (across all chunks)
+ * @returns an array of msg keys to download, array size is this chunk's size
+ */
+ void getMsgHdrsToDownload(out boolean aMore, out long aTotalCount,
+ out Array<nsMsgKey> aKeys);
+ void parseMsgHdrs(in nsIImapProtocol aProtocol, in nsIImapHeaderXferInfo aHdrXferInfo);
+ void AbortHeaderParseStream(in nsIImapProtocol aProtocol) ;
+
+ void OnlineCopyCompleted(in nsIImapProtocol aProtocol, in ImapOnlineCopyState aCopyState);
+ void StartMessage(in nsIMsgMailNewsUrl aUrl);
+ void EndMessage(in nsIMsgMailNewsUrl aUrl, in nsMsgKey uidOfMessage);
+
+ void NotifySearchHit(in nsIMsgMailNewsUrl aUrl, in string hitLine);
+
+ void copyNextStreamMessage(in boolean copySucceeded, in nsISupports copyState);
+ void closeMockChannel(in nsIImapMockChannel aChannel);
+ void setUrlState(in nsIImapProtocol aProtocol, in nsIMsgMailNewsUrl aUrl,
+ in boolean isRunning, in boolean aSuspend,
+ in nsresult status);
+ void releaseUrlCacheEntry(in nsIMsgMailNewsUrl aUrl);
+
+ void headerFetchCompleted(in nsIImapProtocol aProtocol);
+ void setBiffStateAndUpdate(in long biffState);
+ void progressStatusString(in nsIImapProtocol aProtocol, in string aMsgId, in wstring extraInfo);
+ void percentProgress(in nsIImapProtocol aProtocol,
+ in ACString aFmtStringName, in AString aMailboxName,
+ in long long aCurrentProgress, in long long aMaxProgressProgressInfo);
+
+ void clearFolderRights();
+ void setCopyResponseUid(in string msgIdString,
+ in nsIImapUrl aUrl);
+ void setAppendMsgUid(in nsMsgKey newKey,
+ in nsIImapUrl aUrl);
+ ACString getMessageId(in nsIImapUrl aUrl);
+};
diff --git a/comm/mailnews/imap/public/nsIImapMessageSink.idl b/comm/mailnews/imap/public/nsIImapMessageSink.idl
new file mode 100644
index 0000000000..858522856c
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapMessageSink.idl
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+#include "nsIImapUrl.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(6ffb6a92-e43a-405f-92ea-92cf81a5e17b)]
+
+/**
+ * nsIImapMessageSink provides a way for the IMAP system to exchange
+ * message-related information with the local folder representation.
+ *
+ * The IMAP system could poke folders directly, but going through this
+ * interface has a couple of benefits:
+ *
+ * 1. It better defines the public coupling between the two systems.
+ * 2. It's easier to wrap with a proxy class so the IMAP system can safely
+ * call the methods across thread boundaries (see ImapMessageSinkProxy).
+ */
+interface nsIImapMessageSink : nsISupports {
+ // set up message download output stream
+ void setupMsgWriteStream(in nsIFile aFile, in boolean aAppendDummyEnvelope);
+
+ /**
+ * Used by the imap protocol code to notify the core backend code about
+ * downloaded imap messages.
+ *
+ * @param aAdoptedMsgLine a string with a lot of message lines,
+ * separated by native line terminators.
+ * @param aUidOfMsg IMAP UID of the fetched message.
+ * @param aImapUrl IMAP Url used to fetch the message.
+ */
+ void parseAdoptedMsgLine(in string aAdoptedMsgLine, in nsMsgKey aUidOfMsg,
+ in nsIImapUrl aImapUrl);
+
+ /**
+ * Notify the backend that the imap protocol is done downloading a message
+ *
+ * @param aUidOfMsg IMAP UID of the fetched message.
+ * @param aMarkMsgRead Set the SEEN flag on the message.
+ * @param aImapUrl IMAP Url used to fetch the message.
+ * @param aUpdatedMessageSize if this parameter is not -1, the stored size of the message
+ * should be set to this value to reflect the actual size of
+ * the downloaded message.
+ */
+ void normalEndMsgWriteStream(in nsMsgKey aUidOfMessage,
+ in boolean aMarkMsgRead, in nsIImapUrl aImapUrl,
+ in long aUpdatedMessageSize);
+
+ void abortMsgWriteStream();
+
+ void beginMessageUpload();
+
+ /**
+ * Notify the message sink that one or more flags have changed
+ * For Condstore servers, also update the highestMod Sequence
+ * @param aFlags - The new flags for the message
+ * @param aKeywords keywords for the message
+ * @param aMessageKey - The UID of the message that changed
+ * @param aHighestModSeq - The highest mod seq the parser has seen
+ * for this folder
+ **/
+ void notifyMessageFlags(in unsigned long aFlags, in ACString aKeywords,
+ in nsMsgKey aMessageKey,
+ in unsigned long long aHighestModSeq);
+
+ void notifyMessageDeleted(in string aOnlineFolderName,in boolean aDeleteAllMsgs,in string aMsgIdString);
+
+ void getMessageSizeFromDB(in string aId, out unsigned long aSize);
+
+ /**
+ * For a message stored in a file, get the message metadata needed to copy
+ * that message to an imap folder
+ *
+ * @param aRunningUrl message URL
+ * @param aDate message date
+ * @param aKeywords message custom keywords (if supported by the server),
+ * including messages tags and junk status
+ *
+ * @return message flags
+ */
+ unsigned long getCurMoveCopyMessageInfo(in nsIImapUrl aRunningUrl,
+ out PRTime aDate, out ACString aKeywords);
+};
diff --git a/comm/mailnews/imap/public/nsIImapMockChannel.idl b/comm/mailnews/imap/public/nsIImapMockChannel.idl
new file mode 100644
index 0000000000..cb624f314f
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapMockChannel.idl
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+
+/*
+ Because imap protocol connections (which are channels) run through a cache,
+ it isn't always the case that when you want to run a url, you actually get
+ a connection back right away. Often times, the url goes into a queue until
+ a connection becomes available.
+
+ Unfortunately, if we want to be a truly pluggable protocol with necko, necko
+ requires the ability to get a channel back right away when it wants to run
+ a url. It doesn't let you wait until an imap connection becomes available.
+
+ So I've created the notion of a "mock channel". This mock channel is what
+ gets returned to necko (or other callers) when they ask the imap service
+ for a new channel for a url. The mock channel has "mock" implementations
+ of nsIChannel. Eventually, when we actually assign the url to a real
+ channel, we set the real channel on the mock channel. From that point forward,
+ the mock channel forwards channel calls directly to the real channel.
+
+ In short, this class is how I'm solving the problem where necko wants
+ a channel back as soon as they ask for when with the fact that it
+ may be a while until the url is loaded into a connection.
+ */
+
+#include "nsISupports.idl"
+#include "nsIChannel.idl"
+
+interface nsIStreamListener;
+interface nsIProgressEventSink;
+interface nsIURI;
+interface nsIImapProtocol;
+
+[scriptable, uuid(e0178cd5-d37b-4bde-9ab8-752083536225)]
+interface nsIImapMockChannel : nsIChannel
+{
+ attribute nsIProgressEventSink progressEventSink;
+ void GetChannelListener(out nsIStreamListener aChannelListener);
+ void Close();
+ void setImapProtocol(in nsIImapProtocol aProtocol);
+ [noscript] void setSecurityInfo(in nsITransportSecurityInfo securityInfo);
+
+ void setURI(in nsIURI uri);
+ void readFromImapConnection();
+ attribute boolean writingToCache;
+};
diff --git a/comm/mailnews/imap/public/nsIImapOfflineSync.idl b/comm/mailnews/imap/public/nsIImapOfflineSync.idl
new file mode 100644
index 0000000000..c7f55cfa9c
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapOfflineSync.idl
@@ -0,0 +1,19 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIUrlListener;
+
+[scriptable, uuid(ffb683c5-f2c5-490a-b569-2f6b0de0a241)]
+interface nsIImapOfflineSync : nsISupports {
+ void init(in nsIMsgWindow window,
+ in nsIUrlListener listener,
+ in nsIMsgFolder folder,
+ in bool isPseudoOffline);
+
+ void processNextOperation();
+};
diff --git a/comm/mailnews/imap/public/nsIImapProtocol.idl b/comm/mailnews/imap/public/nsIImapProtocol.idl
new file mode 100644
index 0000000000..7f7ea50720
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapProtocol.idl
@@ -0,0 +1,90 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl" // for nsMsgKey
+
+interface nsIURI;
+interface nsIImapUrl;
+interface nsIImapIncomingServer;
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIImapFlagAndUidState;
+
+[ptr] native nsIImapHostSessionList(nsIImapHostSessionList);
+%{C++
+class nsIImapHostSessionList;
+%}
+
+[scriptable, uuid(6ef189e5-8711-4845-9625-d1c74c22c4b5)]
+interface nsIImapProtocol : nsISupports {
+ /**
+ * Set up this connection to run a URL.
+ * Called by nsImapIncomingServer to process a queued URL when it spots a
+ * free connection.
+ * Because nsImapProtocol is really a connection and doesn't follow the
+ * usual nsIChannel lifecycle, this function is provided to allow reuse.
+ * Over and over again.
+ */
+ void LoadImapUrl(in nsIURI aUrl, in nsISupports aConsumer);
+
+ /**
+ * IsBusy returns true if the connection is currently processing a URL
+ * and false otherwise.
+ */
+ void IsBusy(out boolean aIsConnectionBusy,
+ out boolean isInboxConnection);
+
+ /**
+ * Protocol instance examines the URL, looking at the host name,
+ * user name and folder the action would be on in order to figure out
+ * if it can process this URL. I decided to push the semantics about
+ * whether a connection can handle a URL down into the connection level
+ * instead of in the connection cache.
+ */
+ void CanHandleUrl(in nsIImapUrl aImapUrl, out boolean aCanRunUrl,
+ out boolean hasToWait);
+
+ /**
+ * Initialize a protocol object.
+ * @param aHostSessionList host session list service
+ * @param aServer imap server the protocol object will be talking to
+ */
+ void Initialize(in nsIImapHostSessionList aHostSessionList, in nsIImapIncomingServer aServer);
+
+ void NotifyBodysToDownload(in Array<nsMsgKey> keys);
+
+ // methods to get data from the imap parser flag state.
+ void GetFlagsForUID(in unsigned long uid, out boolean foundIt, out unsigned short flags, out string customFlags);
+ void GetSupportedUserFlags(out unsigned short flags);
+
+ void GetRunningImapURL(out nsIImapUrl aImapUrl);
+
+ void GetRunningUrl(out nsIURI aUrl);
+
+ readonly attribute nsIImapFlagAndUidState flagAndUidState;
+ /**
+ * Tell thread to die - only call from the UI thread
+ *
+ * @param aIsSafeToClose false if we're dropping a timed out connection.
+ */
+ void tellThreadToDie(in boolean aIsSafeToClose);
+
+ // Get last active time stamp
+ void GetLastActiveTimeStamp(out PRTime aTimeStamp);
+
+ void pseudoInterruptMsgLoad(in nsIMsgFolder imapFolder, in nsIMsgWindow aMsgWindow, out boolean interrupted);
+
+ /**
+ * Produce a pseudo-interrupt to trigger an abort of an imap mssage fetch.
+ *
+ * @param aInterrupt true to initiate a pseudo-interrupt; otherwise set false.
+ */
+ void pseudoInterrupt(in boolean aInterrupt);
+
+ void GetSelectedMailboxName(out string folderName);
+ // Reset folder connection to authenticated state
+ void ResetToAuthenticatedState();
+};
diff --git a/comm/mailnews/imap/public/nsIImapProtocolSink.idl b/comm/mailnews/imap/public/nsIImapProtocolSink.idl
new file mode 100644
index 0000000000..b3cda13d6e
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapProtocolSink.idl
@@ -0,0 +1,33 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgMailNewsUrl;
+
+/**
+ * Helper interface that contains operations MUST be proxied
+ * over UI thread.
+ */
+[scriptable, uuid(1217cd9d-7678-4026-b323-0d4b45816af0)]
+interface nsIImapProtocolSink : nsISupports {
+
+ /**
+ * Does general cleanup for the imap protocol object.
+ */
+ void closeStreams();
+ /**
+ * Get the msg window associated with a url
+ *
+ * @param aUrl url whose msgWindow we want.
+ * @returns msgWindow associated with url.
+ */
+ nsIMsgWindow getUrlWindow(in nsIMsgMailNewsUrl aUrl);
+
+ /**
+ * Setup main thread proxies.
+ */
+ void setupMainThreadProxies();
+};
diff --git a/comm/mailnews/imap/public/nsIImapServerSink.idl b/comm/mailnews/imap/public/nsIImapServerSink.idl
new file mode 100644
index 0000000000..736487877e
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapServerSink.idl
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgMailNewsUrl;
+interface nsIImapProtocol;
+interface nsIImapUrl;
+interface nsIImapMockChannel;
+
+/**
+ * nsIImapServerSink is designed to be used as a proxy to the application's UI
+ * thread from the running IMAP threads.
+ */
+[scriptable, uuid(2160c641-e4fa-4bbc-ab8b-d9ba45069027)]
+interface nsIImapServerSink : nsISupports {
+ /**
+ * Check if the given folder path is a possible IMAP mailbox.
+ * @param folderPath folder path to check
+ * @param hierarchyDelimiter IMAP hierarchy delimiter in canonical format,
+ * i.e., hierarchy delimiter has been replaced
+ * with '/'
+ * @param boxFlags IMAP folder flags (for subscription, namespaces etc.)
+ * @return true if it's a new mailbox
+ */
+ boolean possibleImapMailbox(in AUTF8String folderPath,
+ in char hierarchyDelimiter, in long boxFlags);
+ boolean folderNeedsACLInitialized(in AUTF8String folderPath);
+ void addFolderRights(in AUTF8String folderPath, in ACString userName, in ACString rights);
+ void refreshFolderRights(in AUTF8String folderPath);
+ void discoveryDone();
+ void onlineFolderDelete(in AUTF8String folderName);
+ void onlineFolderCreateFailed(in AUTF8String aFolderName);
+ void onlineFolderRename(in nsIMsgWindow msgWindow, in AUTF8String oldName, in AUTF8String newName);
+ boolean folderIsNoSelect(in AUTF8String folderName);
+ void setFolderAdminURL(in AUTF8String folderName, in AUTF8String adminUrl);
+ boolean folderVerifiedOnline(in AUTF8String folderName);
+
+ void setCapability(in unsigned long long capability);
+ /// RFC 2971 ID server response
+ void setServerID(in ACString aServerID);
+ boolean loadNextQueuedUrl(in nsIImapProtocol protocol);
+
+ /**
+ * Prepare to retry the given URL.
+ * @param imapUrl the url we're going to retry
+ * @return channel to associate with the url. We return this because access
+ * to the channel should only happen on the ui thread.
+ */
+ nsIImapMockChannel prepareToRetryUrl(in nsIImapUrl imapUrl);
+
+ /**
+ * Suspend the url. This puts it at the end of the queue. If the queue is
+ * empty, the url will get resumed immediately. Currently, the plan is
+ * do this when we have to download a lot of headers in chunks, though we
+ * could find other uses for it.
+ * @param imapUrl url to suspend
+ */
+ void suspendUrl(in nsIImapUrl aImapUrl);
+
+ /**
+ * Retry the given URL.
+ * @param imapUrl url to retry
+ * @param channel the channel to associate with the url
+ */
+ void retryUrl(in nsIImapUrl imapUrl, in nsIImapMockChannel channel);
+
+ /**
+ * If previous URL failed, this gives server chance to abort URLs with same
+ * mock channel.
+ */
+ void abortQueuedUrls();
+ AString getImapStringByName(in string msgName);
+ /**
+ * Alerts the user that the login to the IMAP server failed. Asks whether the
+ * connection should: retry, cancel, or request a new password.
+ *
+ * @param aMsgWindow The message window associated with this action (cannot
+ * be null).
+ * @return The button pressed. 0 for retry, 1 for cancel,
+ * 2 for enter a new password.
+ */
+ int32_t promptLoginFailed(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Alerts the user with the given string (FE = 'Front End').
+ *
+ * @param aAlertString The string to alert the user with.
+ * @param aUrl The running url.
+ */
+ void fEAlert(in AString aAlertString, in nsIMsgMailNewsUrl aUrl);
+
+ /**
+ * Alerts the user with a localized string. It will attempt to fill in
+ * the hostname into the string if necessary.
+ *
+ * @param aMsgName The id of the string to present to the user..
+ * @param aUrl The running url.
+ */
+ void fEAlertWithName(in string aMsgName, in nsIMsgMailNewsUrl aUrl);
+ /**
+ * Takes a response from the server and prepends it with IMAP_SERVER_SAID
+ *
+ * @param aServerString The string to alert the user with.
+ * @param url The running url.
+ */
+ void fEAlertFromServer(in ACString aServerString, in nsIMsgMailNewsUrl aUrl);
+
+ void commitNamespaces();
+
+ /**
+ * Returns a password via the out param, if we were able to prompt for one,
+ * or had one stored.
+ * If there is already a password prompt up, we return false, but we
+ * ask the async prompt service to notify us when we can put up a prompt.
+ * When that notification is received, we prompt the user and set the
+ * password on the protocol object, and signal a monitor that the imap
+ * thread should be waiting on.
+ *
+ * rv is NS_MSG_PASSWORD_PROMPT_CANCELLED if the user cancels the
+ * password prompt. That's not an exception, however.
+ *
+ * @param aProtocol imap protocol object requesting the password.
+ * @param aNewPasswordRequested Forces password prompt immediately
+ * @param aPassword returns the password, unless we had to prompt or use the,
+ * login manager and there was already a prompt up.
+ */
+ void asyncGetPassword(in nsIImapProtocol aProtocol,
+ in boolean aNewPasswordRequested,
+ out AString aPassword);
+
+ /**
+ * Returns a password via the out param if password is stored in login mgr.
+ * If no password is stored, this function returns NS_ERROR_NOT_AVAILABLE.
+ * This never triggers a password prompt.
+ *
+ * @param aPassword returns the stored password or empty string if not stored.
+ */
+ void syncGetPassword(out AString aPassword);
+
+ attribute boolean userAuthenticated;
+ void setMailServerUrls(in AUTF8String manageMailAccount, in AUTF8String manageLists, in AUTF8String manageFilters);
+
+ /** Used by the imap thread when upgrading from the socketType
+ * trySTARTTLS.
+ * @param aSucceeded whether STARTTLS succeeded. If it did, the server
+ * will set the socket type to alwaysSTARTTLS, otherwise plain.
+ */
+ void UpdateTrySTARTTLSPref(in boolean aSucceeded);
+
+ readonly attribute AUTF8String arbitraryHeaders;
+ void forgetPassword();
+
+ readonly attribute boolean showAttachmentsInline;
+ string cramMD5Hash(in string decodedChallenge, in string key);
+ /// String to send to the imap server as the login user name.
+ readonly attribute ACString loginUsername;
+ /// String to send to the imap server as the user name.
+ readonly attribute ACString originalUsername;
+ /// Internal pref key, unique over all servers
+ readonly attribute ACString serverKey;
+ /// password for server login
+ readonly attribute AString serverPassword;
+ /// remove a connection to the server
+ void removeServerConnection(in nsIImapProtocol aProtocol);
+ /// is the imap server shutting down?
+ readonly attribute boolean serverShuttingDown;
+ /// reset the connection for a particular folder
+ void resetServerConnection(in AUTF8String aFolderName);
+ /// tell the server if listing using lsub command
+ void setServerDoingLsub(in boolean aDoingLsub);
+ /// set whether UTF8=ACCEPT enabled or not
+ void setServerUtf8AcceptEnabled(in boolean aEnabled);
+};
diff --git a/comm/mailnews/imap/public/nsIImapService.idl b/comm/mailnews/imap/public/nsIImapService.idl
new file mode 100644
index 0000000000..04b2667467
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapService.idl
@@ -0,0 +1,253 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+////////////////////////////////////////////////////////////////////////////////////////
+// The IMAP Service is an interfaced designed to make building and running imap urls
+// easier. Clients typically go to the imap service and ask it do things such as:
+// get new mail, etc....
+//
+// Oh and in case you couldn't tell by the name, the imap service is a service! and you
+// should go through the service manager to obtain an instance of it.
+////////////////////////////////////////////////////////////////////////////////////////
+
+#include "nsISupports.idl"
+#include "nsIImapUrl.idl"
+
+interface nsIImapMessageSink;
+interface nsIUrlListener;
+interface nsIURI;
+interface nsIFile;
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIMsgMailNewsUrl;
+interface nsIImapIncomingServer;
+interface nsICacheStorage;
+
+/**
+ * Most of the nsIImapService methods are friendly front ends for composing and
+ * issuing "imap://" protocol operations. Usually a nsImapUrl will be returned.
+ * This url object is stateful and tracks the issued request.
+ */
+[scriptable, uuid(aba44b3d-7a0f-4987-8794-96d2de66d966)]
+interface nsIImapService : nsISupports
+{
+ nsIURI selectFolder(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Select the folder on the imap server without doing a sync of flags or
+ * headers. This is used for offline playback, where we don't want to
+ * download hdrs we don't have, because they may have been offline deleted.
+ *
+ * @param aImapMailFolder the folder to select
+ * @param aUrlListener url listener, can be null
+ * @param aMsgWindow msg window url is running in, can be null
+ *
+ * @returns the url created to run the lite select in.
+ */
+ nsIURI liteSelectFolder(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ void addImapFetchToUrl(in nsIMsgMailNewsUrl aURL,
+ in nsIMsgFolder aImapMailFolder,
+ in ACString aMessageIdentifierList,
+ in ACString aAdditionalHeader);
+
+ void fetchMessage(in nsIImapUrl aUrl,
+ in nsImapState aImapAction,
+ in nsIMsgFolder aImapMailFolder,
+ in nsIImapMessageSink aImapMessageSink,
+ in nsIMsgWindow aMsgWindow,
+ in nsISupports aConsumer,
+ in ACString aMessageIdentifierList,
+ in boolean convertDataToText,
+ out nsIURI aOutURL);
+
+ void noop(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL);
+
+ void getHeaders(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL,
+ in ACString aMessageIdentifierList,
+ in boolean aMessageIdsAreUID);
+
+ nsIURI getBodyStart(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in ACString aMessageIdentifierList,
+ in long numBytes);
+
+ /**
+ * Issue an EXPUNGE on the target folder.
+ *
+ * @param aImapMailFolder the folder to expunge
+ * @param aUrlListener url listener, can be null
+ * @param aMsgWindow msg window url is running in, can be null
+ */
+ void expunge(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Issue a STATUS on the target folder.
+ *
+ * @param aImapMailFolder the folder to expunge
+ * @param aUrlListener url listener, can be null
+ *
+ * @returns the url created to run the status.
+ */
+ nsIURI updateFolderStatus(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener);
+
+ /**
+ * Verify that we can login.
+ *
+ * @param aImapMailFolder - any old imap folder - we just need it to
+ * set url sinks.
+ * @param aMsgWindow - nsIMsgWindow to use for notification callbacks.
+ * @return - the url that we run.
+ */
+ nsIURI verifyLogon(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ void biff(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL,
+ in unsigned long aUidHighWater);
+
+ void deleteMessages(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL,
+ in ACString aMessageIdentifierList,
+ in boolean aMessageIdsAreUID);
+
+ void deleteAllMessages(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener);
+
+ void addMessageFlags(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in ACString aMessageIdentifierList,
+ in imapMessageFlagsType aFlags,
+ in boolean aMessageIdsAreUID);
+
+ void subtractMessageFlags(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in ACString aMessageIdentifierList,
+ in imapMessageFlagsType aFlags,
+ in boolean aMessageIdsAreUID);
+
+ void setMessageFlags(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL,
+ in ACString aMessageIdentifierList,
+ in imapMessageFlagsType aFlags,
+ in boolean aMessageIdsAreUID);
+
+ void discoverAllFolders(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ void discoverAllAndSubscribedFolders(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ void discoverChildren(in nsIMsgFolder aImapMailFolder,
+ in nsIUrlListener aUrlListener,
+ in ACString folderPath);
+
+ void onlineMessageCopy(in nsIMsgFolder aSrcFolder,
+ in ACString aMessageIds,
+ in nsIMsgFolder aDstFolder,
+ in boolean aIdsAreUids,
+ in boolean aIsMove,
+ in nsIUrlListener aUrlListener,
+ out nsIURI aURL,
+ in nsISupports aCopyState,
+ in nsIMsgWindow aWindow);
+
+
+ void appendMessageFromFile(in nsIFile aFile,
+ in nsIMsgFolder aDstFolder,
+ in ACString aMessageId,
+ in boolean idsAreUids,
+ in boolean aInSelectedState,
+ in nsIUrlListener aUrlListener,
+ in nsISupports aCopyState,
+ in nsIMsgWindow aMsgWindow);
+
+ void downloadMessagesForOffline(in ACString aMessageIds, in nsIMsgFolder aSrcFolder,
+ in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+
+ void moveFolder(in nsIMsgFolder aSrcFolder,
+ in nsIMsgFolder aDstFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow msgWindow);
+
+ void renameLeaf(in nsIMsgFolder aSrcFolder,
+ in AString aLeafName,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow msgWindow);
+
+ void deleteFolder(in nsIMsgFolder aFolder,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ nsIURI createFolder(in nsIMsgFolder aParentFolder,
+ in AString aLeafName,
+ in nsIUrlListener aUrlListener);
+
+ void listFolder(in nsIMsgFolder aMailFolder,
+ in nsIUrlListener aUrlListener);
+
+ nsIURI subscribeFolder(in nsIMsgFolder aMailFolder,
+ in AString mailboxName,
+ in nsIUrlListener aUrlListener);
+
+ nsIURI unsubscribeFolder(in nsIMsgFolder aMailFolder,
+ in AString mailboxName,
+ in nsIUrlListener aUrlListener);
+
+ // this method will first check if the folder exists but is
+ // not subscribed to, in which case it will subscribe to the folder.
+ // otherwise, it will try to create the folder. It will try to do this
+ // with one url.
+ void ensureFolderExists(in nsIMsgFolder aParentFolder,
+ in AString aLeafName,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+
+
+ nsIURI getFolderAdminUrl(in nsIMsgFolder aMailFolder,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+
+ nsIURI issueCommandOnMsgs(in nsIMsgFolder aMailFolder,
+ in nsIMsgWindow aMsgWindow,
+ in ACString aCommand,
+ in ACString aMessageIdentifierList);
+
+ nsIURI fetchCustomMsgAttribute(in nsIMsgFolder aMailFolder,
+ in nsIMsgWindow aMsgWindow,
+ in ACString aAttribute,
+ in ACString aMessageIdentifierList);
+
+ nsIURI storeCustomKeywords(in nsIMsgFolder aMailFolder,
+ in nsIMsgWindow aMsgWindow,
+ in ACString flagsToAdd,
+ in ACString flagsToSubtract,
+ in ACString aMessageIdentifierList);
+
+ void getListOfFoldersOnServer(in nsIImapIncomingServer aServer, in nsIMsgWindow aMsgWindow);
+ void getListOfFoldersWithPath(in nsIImapIncomingServer aServer, in nsIMsgWindow aMsgWindow, in ACString folderPath);
+
+ nsISupports playbackAllOfflineOperations(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+ void downloadAllOffineImapFolders(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+
+ readonly attribute nsICacheStorage cacheStorage;
+};
diff --git a/comm/mailnews/imap/public/nsIImapUrl.idl b/comm/mailnews/imap/public/nsIImapUrl.idl
new file mode 100644
index 0000000000..75f85b93a5
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIImapUrl.idl
@@ -0,0 +1,210 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIImapMailFolderSink;
+interface nsIImapMessageSink;
+interface nsIImapServerSink;
+interface nsIImapMockChannel;
+interface nsIFile;
+
+typedef long nsImapAction;
+typedef long nsImapState;
+
+typedef unsigned short imapMessageFlagsType;
+
+typedef long nsImapContentModifiedType;
+
+[scriptable, uuid(2e91901e-ff6c-11d3-b9fa-00108335942a)]
+interface nsImapContentModifiedTypes : nsISupports
+{
+ const long IMAP_CONTENT_NOT_MODIFIED = 0;
+ const long IMAP_CONTENT_MODIFIED_VIEW_INLINE = 1;
+ const long IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS = 2;
+ const long IMAP_CONTENT_FORCE_CONTENT_NOT_MODIFIED = 3;
+} ;
+
+[scriptable, uuid(fe2a8f9e-2886-4146-9896-27fff660c69f)]
+interface nsIImapUrl : nsISupports
+{
+ ///////////////////////////////////////////////////////////////////////////////
+ // Getters and Setters for the imap specific event sinks to bind to the url
+ ///////////////////////////////////////////////////////////////////////////////
+ attribute nsIImapMailFolderSink imapMailFolderSink;
+ attribute nsIImapMessageSink imapMessageSink;
+ attribute nsIImapServerSink imapServerSink;
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Getters and Setters for the imap url state
+ ///////////////////////////////////////////////////////////////////////////////
+ attribute nsImapAction imapAction;
+ readonly attribute nsImapState requiredImapState;
+ readonly attribute string imapPartToFetch;
+ readonly attribute ACString customAttributeToFetch;
+ attribute ACString customAttributeResult;
+ readonly attribute ACString command;
+ attribute ACString customCommandResult;
+ readonly attribute ACString customAddFlags;
+ readonly attribute ACString customSubtractFlags;
+ void allocateCanonicalPath(in string aServerPath, in char aOnlineDelimiter, out string aAllocatedPath);
+ void allocateServerPath(in string aCanonicalPath, in char aOnlineDelimiter, out string aAllocatedPath);
+ string createServerSourceFolderPathString();
+ string createCanonicalSourceFolderPathString();
+ string createServerDestinationFolderPathString();
+
+ string addOnlineDirectoryIfNecessary(in string onlineMailboxName);
+ void createSearchCriteriaString(out string aResult);
+ readonly attribute ACString listOfMessageIds;
+
+ boolean messageIdsAreUids();
+ readonly attribute imapMessageFlagsType msgFlags; // kAddMsgFlags or kSubtractMsgFlags only
+
+ readonly attribute long numBytesToFetch;
+ attribute char onlineSubDirSeparator;
+ attribute boolean mimePartSelectorDetected;
+ attribute boolean msgLoadingFromCache; // true if this msg load is coming from a cache, so we can know to mark it read
+ attribute boolean externalLinkUrl; // true if we ran this url because the user clicked on a link.
+ attribute boolean validUrl; // false if we couldn't parse url for whatever reason.
+ /**
+ * copyState is used by some IMAP copy operations. The exact type stashed
+ * here depends on the operation being performed. For online move/copy,
+ * it'll be an nsImapMailCopyState (private to nsImapMailFolder). For
+ * other operations it might be (say), an nsIStreamListener.
+ */
+ attribute nsISupports copyState;
+ attribute nsIFile msgFile;
+ attribute nsIImapMockChannel mockChannel;
+ /**
+ * Set to true if we should store the msg(s) for offline use if we can,
+ * e.g., we're fetching a message and the folder is configured for offline
+ * use and we're not doing mime parts on demand.
+ */
+ attribute boolean storeResultsOffline;
+ /**
+ * If we fallback from fetching by parts to fetching the whole message,
+ * because all the parts were inline, this tells us we should store
+ * the message offline.
+ */
+ attribute boolean storeOfflineOnFallback;
+
+ /**
+ * This attribute defaults to false, but if we only want to use the offline
+ * cache (disk, memory, or offline store) to fetch the message, then we set
+ * this to true. Currently, nsIMsgMessageService.streamMessage does this.
+ */
+ attribute boolean localFetchOnly;
+
+ /// Server disconnected first time so we're retrying.
+ attribute boolean rerunningUrl;
+
+ /**
+ * Do we have more headers to download? This is set when we decide to
+ * download newest headers first, followed by older headers in a subsequent
+ * run of the url, which allows other urls to run against the folder in the
+ * meantime.
+ */
+ attribute boolean moreHeadersToDownload;
+
+ /**
+ * @{
+ * This is used to tell the runner of the url more about the status of
+ * the command, beyond whether it was successful or not. For example,
+ * subtracting flags from a UID that doesn't exist isn't an error
+ * (the server returns OK), but the backend code may want to know about it.
+ */
+ attribute long extraStatus;
+
+ /**
+ * Current possible extra status values
+ */
+ const long ImapStatusNone = 0;
+ const long ImapStatusFlagChangeFailed = 1;
+ const long ImapStatusFlagsNotSettable = 2;
+ /** @} */
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Enumerated types specific to imap urls...
+ ///////////////////////////////////////////////////////////////////////////////
+
+ // the following are nsImapState enums.
+ // we have a basic set of imap url actions. These actions are nsImapActions.
+ // Certain actions require us to be in the authenticated state and others require us to
+ // be in the selected state. nsImapState is used to store the state the url needs to
+ // be in. You'll later see us refer to the imap url state in the imap protocol when we
+ // are processing the current url. Don't confuse nsImapState with the generic url state
+ // used to keep track of whether the url is running or not...
+ const long nsImapAuthenticatedState = 0;
+ const long nsImapSelectedState = 1;
+
+ const long nsImapActionSendText = 0; // a state used for testing purposes to send raw url text straight to the server....
+ // nsImapAuthenticatedStateUrl urls
+ // since the following url actions require us to be in the authenticated
+ // state, the high bit is left blank....
+ const long nsImapTest = 0x00000001;
+ const long nsImapCreateFolder = 0x00000005;
+ const long nsImapDeleteFolder = 0x00000006;
+ const long nsImapRenameFolder = 0x00000007;
+ const long nsImapMoveFolderHierarchy = 0x00000008;
+ const long nsImapLsubFolders = 0x00000009;
+ const long nsImapGetMailAccountUrl = 0x0000000A;
+ const long nsImapDiscoverChildrenUrl = 0x0000000B;
+ const long nsImapDiscoverAllBoxesUrl = 0x0000000D;
+ const long nsImapDiscoverAllAndSubscribedBoxesUrl = 0x0000000E;
+ const long nsImapAppendMsgFromFile = 0x0000000F;
+ const long nsImapSubscribe = 0x00000010;
+ const long nsImapUnsubscribe = 0x00000011;
+ const long nsImapRefreshACL = 0x00000012;
+ const long nsImapRefreshAllACLs = 0x00000013;
+ const long nsImapListFolder = 0x00000014;
+ const long nsImapUpgradeToSubscription = 0x00000015;
+ const long nsImapFolderStatus = 0x00000016;
+ const long nsImapRefreshFolderUrls = 0x00000017;
+ const long nsImapEnsureExistsFolder = 0x00000018;
+ const long nsImapOfflineToOnlineCopy = 0x00000019;
+ const long nsImapOfflineToOnlineMove = 0x0000001A;
+ const long nsImapVerifylogon = 0x0000001B;
+ // it's okay to add more imap actions that require us to
+ // be in the authenticated state here without renumbering
+ // the imap selected state url actions. just make sure you don't
+ // set the high bit...
+
+ // nsImapSelectedState urls. Note, the high bit is always set for
+ // imap actions which require us to be in the selected state
+ const long nsImapSelectFolder = 0x10000002;
+ const long nsImapLiteSelectFolder = 0x10000003;
+ const long nsImapExpungeFolder = 0x10000004;
+ const long nsImapMsgFetch = 0x10000018;
+ const long nsImapMsgHeader = 0x10000019;
+ const long nsImapSearch = 0x1000001A;
+ const long nsImapDeleteMsg = 0x1000001B;
+ const long nsImapDeleteAllMsgs = 0x1000001C;
+ const long nsImapAddMsgFlags = 0x1000001D;
+ const long nsImapSubtractMsgFlags = 0x1000001E;
+ const long nsImapSetMsgFlags = 0x1000001F;
+ const long nsImapOnlineCopy = 0x10000020;
+ const long nsImapOnlineMove = 0x10000021;
+ const long nsImapOnlineToOfflineCopy = 0x10000022;
+ const long nsImapOnlineToOfflineMove = 0x10000023;
+ const long nsImapMsgPreview = 0x10000024;
+ const long nsImapBiff = 0x10000026;
+ const long nsImapSelectNoopFolder = 0x10000027;
+ const long nsImapAppendDraftFromFile = 0x10000028;
+ const long nsImapUidExpunge = 0x10000029;
+ const long nsImapSaveMessageToDisk = 0x10000030;
+ const long nsImapOpenMimePart = 0x10000031;
+ const long nsImapMsgDownloadForOffline = 0x10000032;
+ const long nsImapDeleteFolderAndMsgs = 0x10000033;
+ const long nsImapUserDefinedMsgCommand = 0x10000034;
+ const long nsImapUserDefinedFetchAttribute = 0x10000035;
+ const long nsImapMsgFetchPeek = 0x10000036;
+ const long nsImapMsgStoreCustomKeywords = 0x10000037;
+
+ /// Constant for the default IMAP port number
+ const int32_t DEFAULT_IMAP_PORT = 143;
+
+ /// Constant for the default IMAP over ssl port number
+ const int32_t DEFAULT_IMAPS_PORT = 993;
+};
diff --git a/comm/mailnews/imap/public/nsIMailboxSpec.idl b/comm/mailnews/imap/public/nsIMailboxSpec.idl
new file mode 100644
index 0000000000..729b867075
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIMailboxSpec.idl
@@ -0,0 +1,47 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIImapFlagAndUidState;
+
+[ptr] native nsImapNamespace(nsImapNamespace);
+%{C++
+class nsImapNamespace;
+%}
+
+[scriptable, uuid(a9fbbc80-5291-4ed8-a7f7-c2fcad231269)]
+interface nsIMailboxSpec : nsISupports
+{
+ attribute long folder_UIDVALIDITY;
+ /**
+ * The highest modification sequence number the parser has seen
+ * for this mailbox. See IMAP RFC 4551
+ **/
+ attribute unsigned long long highestModSeq;
+ attribute long numMessages;
+ attribute long numUnseenMessages;
+ attribute long numRecentMessages;
+
+ /// If server supports UIDNEXT, we store the result here.
+ attribute long nextUID;
+
+ attribute unsigned long box_flags;
+ attribute unsigned long supportedUserFlags;
+
+ attribute ACString allocatedPathName;
+ attribute AString unicharPathName;
+ attribute char hierarchyDelimiter;
+ attribute ACString hostName;
+
+ attribute nsIImapFlagAndUidState flagState;
+
+ attribute boolean folderSelected;
+ attribute boolean discoveredFromLsub;
+
+ attribute boolean onlineVerified;
+
+ [noscript] attribute nsImapNamespace namespaceForFolder;
+};
diff --git a/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl b/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl
new file mode 100644
index 0000000000..070276508a
--- /dev/null
+++ b/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl
@@ -0,0 +1,253 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIUTF8StringEnumerator;
+interface nsIUrlListener;
+interface nsIURI;
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIImapIncomingServer;
+interface nsIMsgParseMailMsgState;
+interface nsIAutoSyncState;
+
+/**
+ * nsIMsgQuota defines the quota for a resource within a quota root.
+ * @see RFC 2087
+ */
+[scriptable, uuid(9db60f97-45c1-45c2-8ab1-395690228f3f)]
+interface nsIMsgQuota : nsISupports {
+ /**
+ * If quota root name not empty, the concatenation of the quota root name
+ * and the resource name separated by a slash , e.g.,
+ * "User Quota / MESSAGE" or with empty quota root name, just resource
+ * name, e.g., "STORAGE".
+ */
+ attribute AUTF8String name;
+
+ /**
+ * Amount of resource in use for this quota root. E.g., number of messages
+ * or storage in KB (1024 octets).
+ */
+
+ attribute unsigned long long usage;
+ /**
+ * Maximum amount of usage permitted by the server for this quota root
+ * resource.
+ */
+ attribute unsigned long long limit;
+};
+
+/**
+ * nsIMsgImapFolderProps is a simple interface which allows the IMAP folder to
+ * update some values that the folder props js code will use to update the
+ * sharing and quota tabs in the folder properties.
+ */
+[scriptable, uuid(09D99F2C-3E23-4f8c-A536-5C277BAA9585)]
+interface nsIMsgImapFolderProps : nsISupports {
+
+ void setFolderType(in AString folderType);
+ void setFolderTypeDescription(in AString folderTypeDescription);
+ void setFolderPermissions(in AString permissions);
+ void serverDoesntSupportACL();
+
+ /**
+ * Toggles the display of quota information in the Quota tab of the folder properties.
+ * If on, the quota root, usage, and percentage used are displayed.
+ * If off, a status message is displayed. The status message can be set with setQuotaStatus().
+ * @param showData If true, display the quota root, usage information and usage percentage bar.
+ * If false, display the status message.
+ */
+ void showQuotaData(in boolean showData);
+
+ /**
+ * Sets the status string displayed in the Quota tab of the folder properties if quota
+ * information is not visible.
+ */
+ void setQuotaStatus(in AString folderQuotaStatus);
+
+ /**
+ * Sets the quota data displayed in the folder properties Quota tab. An
+ * array of quota items is passed in.
+ */
+ void setQuotaData(in Array<nsIMsgQuota> quotaArray);
+};
+
+[scriptable, uuid(fea0f455-7adf-4683-bf2f-c95c3fff03df)]
+interface nsIMsgImapMailFolder : nsISupports {
+ /**
+ * Remove the local version of this folder (used to clean up local folders
+ * which don't correspond to ones on the server).
+ */
+ void removeLocalSelf();
+ void createClientSubfolderInfo(in AUTF8String folderName, in char hierarchyDelimiter,
+ in long flags, in boolean suppressNotification);
+ void list();
+ void renameLocal(in AUTF8String newname, in nsIMsgFolder parent);
+ void prepareToRename();
+ void performExpand(in nsIMsgWindow aMsgWindow);
+ void recursiveCloseActiveConnections(in nsIImapIncomingServer aImapServer);
+ void renameClient(in nsIMsgWindow msgWindow, in nsIMsgFolder msgFolder, in AUTF8String oldName, in AUTF8String newName);
+
+ // these are used for offline synchronization
+ void storeImapFlags(in long aFlags, in boolean aAddFlags,
+ in Array<nsMsgKey> aKeysToFlag, in nsIUrlListener aUrlListener);
+ nsIURI setImapFlags(in string uids, in long flags);
+ void replayOfflineMoveCopy(in Array<nsMsgKey> keys, in boolean isMove,
+ in nsIMsgFolder aDstFolder, in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aWindow, in boolean srcFolderOffline);
+ nsIURI playbackOfflineFolderCreate(in AString folderName, in nsIMsgWindow aWindow);
+ /**
+ * This is called by the offline sync code to tell the imap folder to
+ * remember info about the header with this key (messageId and key) because
+ * it's an offline move result header, and we need to generate an
+ * nsIMsgFolderListener.msgKeyChanged notification when we download the
+ * real header from the imap server.
+ *
+ * @param aMsgKey msg key of move result pseudo hdr.
+ */
+ void addMoveResultPseudoKey(in nsMsgKey aMsgKey);
+ /**
+ * Select this folder on the imap server without doing a sync of flags or
+ * headers. This is used for offline playback, where we don't want to
+ * download hdrs we don't have, because they may have been offline deleted.
+ *
+ * @param aUrlListener url listener, can be null
+ * @param aWindow msg window url is running in, can be null
+ */
+ void liteSelect(in nsIUrlListener aUrlListener, in nsIMsgWindow aWindow);
+
+ void fillInFolderProps(in nsIMsgImapFolderProps aFolderProps);
+ void resetNamespaceReferences();
+ void folderPrivileges(in nsIMsgWindow aWindow);
+ nsIMsgImapMailFolder findOnlineSubFolder(in AUTF8String onlineName);
+ void addFolderRights(in ACString userName, in ACString rights);
+ void refreshFolderRights();
+
+ /**
+ * Mark/unmark the header as pending removal from the offline store. If mark,
+ * this also increases the expungedBytes count on the folder so we know
+ * there's more local disk space to be reclaimed.
+ *
+ * @param aHdr msg hdr to mark pending removal from offline store.
+ * @param aMark whether to set or clear the pending removal status.
+ *
+ */
+ void markPendingRemoval(in nsIMsgDBHdr aHdr, in boolean aMark);
+
+ /**
+ * Issue an expunge of this folder to the imap server.
+ *
+ * @param aUrlListener url listener, can be null
+ * @param aWindow msg window url is running in, can be null
+ *
+ * @returns status of attempt to run url.
+ */
+ void expunge(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+
+ void updateStatus(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+ void updateFolderWithListener(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+ // this is used to issue an arbitrary imap command on the passed in msgs.
+ // It assumes the command needs to be run in the selected state.
+ nsIURI issueCommandOnMsgs(in ACString command, in string uids, in nsIMsgWindow aWindow);
+ nsIURI fetchCustomMsgAttribute(in ACString msgAttribute, in string uids, in nsIMsgWindow aWindow);
+ nsIURI storeCustomKeywords(in nsIMsgWindow aMsgWindow,
+ in ACString aFlagsToAdd,
+ in ACString aFlagsToSubtract,
+ in Array<nsMsgKey> aKeysToStore);
+
+ void notifyIfNewMail();
+
+ void initiateAutoSync(in nsIUrlListener aUrlListener);
+
+ attribute boolean verifiedAsOnlineFolder;
+ attribute boolean explicitlyVerify;
+ attribute char hierarchyDelimiter;
+ attribute long boxFlags;
+ /**
+ * onlineName is the IMAP name of the mailbox that this folder represents.
+ * It's a path with components separated by hierarchyDelimiter.
+ * For example, "INBOX/bar/wibble", "INBOX.bar.wibble", etc...
+ */
+ attribute AUTF8String onlineName;
+ attribute boolean isNamespace;
+ readonly attribute boolean canOpenFolder;
+ attribute AUTF8String adminUrl;
+ readonly attribute boolean hasAdminUrl;
+ attribute boolean performingBiff;
+ readonly attribute nsIMsgParseMailMsgState hdrParser;
+ readonly attribute nsIImapIncomingServer imapIncomingServer;
+ readonly attribute nsIAutoSyncState autoSyncStateObj;
+ readonly attribute boolean shouldUseUtf8FolderName;
+ /**
+ * @{
+ * These are used to access the response to the STATUS or SELECT command.
+ * The counts include deleted messages, or headers we haven't downloaded yet.
+ */
+ readonly attribute long serverTotal;
+ readonly attribute long serverUnseen;
+ readonly attribute long serverRecent;
+ readonly attribute long serverNextUID;
+ /** @} */
+
+ /**
+ * Return an array of quota items of type nsIMsgQuota defined above.
+ * A not-empty array indicates that the server has provided one or more
+ * sets of quota information on this folder. The array will be empty
+ * - if the server does not supports quotas,
+ * - if there are no resource quotas on this folder, or
+ * - if the folder has yet to be opened (selected) by the user.
+ */
+ Array<nsIMsgQuota> getQuota();
+
+ /**
+ * List all (human) users apart from the current user who have access to
+ * this folder.
+ *
+ * You can find out which rights they have with getRightsForUser().
+ */
+ nsIUTF8StringEnumerator getOtherUsersWithAccess();
+
+ /**
+ * Which access rights a certain user has for this folder.
+ *
+ * @return list of flags
+ * e.g. "lrswipcd" for write access and "lrs" for read only access.
+ *
+ * See RFC 2086 (e.g. Cyrus) and RFC 4314 (e.g. dovecot)
+ *
+ * l = locate = visible in folder list
+ * r = read = list mails, get/read mail contents
+ * s = set seen flag = mark read. Does not affect other users.
+ * d (or t) = delete mails
+ * w = write = change (other) flags of existing mails
+ * i = insert = add mails to this folder
+ * p = post = send mail directly to the submission address for folder
+ * c (or k) = create subfolders
+ * (e = expunge = compress)
+ * (x = delete folder)
+ * a = admin = change permissions
+ */
+ ACString getPermissionsForUser(in ACString username);
+
+ /**
+ * Change the number of "pending" messages in a folder,
+ * messages we know about, but don't have the headers for yet
+ *
+ * @param aDelta amount to change total by.
+ */
+ void changePendingTotal(in long aDelta);
+
+ /**
+ * Change the number of "pending" unread messages in a folder,
+ * unread messages we know about, but don't have the headers for yet
+ *
+ * @param aDelta amount to change the unread count by.
+ */
+ void changePendingUnread(in long aDelta);
+};
diff --git a/comm/mailnews/imap/src/ImapChannel.jsm b/comm/mailnews/imap/src/ImapChannel.jsm
new file mode 100644
index 0000000000..0e5c333d9a
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapChannel.jsm
@@ -0,0 +1,318 @@
+/* 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 = ["ImapChannel"];
+
+const { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+const { MailChannel } = ChromeUtils.importESModule(
+ "resource:///modules/MailChannel.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * A channel to interact with IMAP server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ * @implements {nsICacheEntryOpenCallback}
+ */
+class ImapChannel extends MailChannel {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMailChannel",
+ "nsIChannel",
+ "nsIRequest",
+ "nsIWritablePropertyBag",
+ "nsICacheEntryOpenCallback",
+ ]);
+
+ _logger = ImapUtils.logger;
+ _status = Cr.NS_OK;
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} loadInfo - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ super();
+ this._server = MailServices.accounts
+ .findServerByURI(uri)
+ .QueryInterface(Ci.nsIImapIncomingServer);
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.loadInfo = loadInfo;
+ this.contentLength = 0;
+
+ this.uri = uri;
+
+ uri = uri.QueryInterface(Ci.nsIMsgMessageUrl);
+ try {
+ this.contentLength = uri.messageHeader.messageSize;
+ } catch (e) {
+ // Got passed an IMAP folder URL.
+ this._isFolderURL = this._server && !/#(\d+)$/.test(uri.spec);
+ }
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {string}
+ */
+ get name() {
+ return this.URI?.spec;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {boolean}
+ */
+ isPending() {
+ return !!this._pending;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {nsresult}
+ */
+ get status() {
+ return this._status;
+ }
+
+ /**
+ * @see nsICacheEntryOpenCallback
+ */
+ onCacheEntryAvailable(entry, isNew, status) {
+ if (!Components.isSuccessCode(status)) {
+ // If memory cache doesn't work, read from the server.
+ this._readFromServer();
+ return;
+ }
+
+ if (isNew) {
+ if (Services.io.offline) {
+ this._status = Cr.NS_ERROR_OFFLINE;
+ return;
+ }
+ // It's a new entry, needs to read from the server.
+ let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ let outStream = entry.openOutputStream(0, -1);
+ // When the tee stream receives data from the server, it writes to both
+ // the original listener and outStream (memory cache).
+ tee.init(this._listener, outStream, null);
+ this._listener = tee;
+ this._cacheEntry = entry;
+ this._readFromServer();
+ return;
+ }
+
+ // It's an old entry, read from the memory cache.
+ this._readFromCacheStream(entry.openInputStream(0));
+ }
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ /**
+ * Get readonly URI.
+ * @see nsIChannel
+ */
+ get URI() {
+ return this.uri;
+ }
+
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ "ImapChannel.open() not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug(`asyncOpen ${this.URI.spec}`);
+ if (this._isFolderURL) {
+ let handler = Cc[
+ "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder"
+ ].createInstance(Ci.nsIContentHandler);
+ handler.handleContent("x-application-imapfolder", null, this);
+ return;
+ }
+
+ let url = new URL(this.URI.spec);
+ this._listener = listener;
+ if (url.searchParams.get("part")) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ this._listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ listener,
+ this
+ );
+ }
+
+ let msgIds = this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface(
+ Ci.nsIMsgMailNewsUrl
+ ).listOfMessageIds;
+ this._msgKey = parseInt(msgIds);
+ this.contentLength = 0;
+ try {
+ if (this.readFromLocalCache()) {
+ this._logger.debug("Read from local cache");
+ return;
+ }
+ } catch (e) {
+ this._logger.warn(e);
+ }
+
+ try {
+ let uri = this.URI;
+ if (this.URI.spec.includes("?")) {
+ uri = uri.mutate().setQuery("").finalize();
+ }
+ // Check if a memory cache is available for the current URI.
+ MailServices.imap.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ this.URI.QueryInterface(Ci.nsIImapUrl).storeResultsOffline
+ ? // Don't write to the memory cache if storing offline.
+ Ci.nsICacheStorage.OPEN_READONLY
+ : Ci.nsICacheStorage.OPEN_NORMALLY,
+ this
+ );
+ } catch (e) {
+ this._logger.warn(e);
+ this._readFromServer();
+ }
+ if (this._status == Cr.NS_ERROR_OFFLINE) {
+ throw new Components.Exception(
+ "The requested action could not be completed in the offline state",
+ Cr.NS_ERROR_OFFLINE
+ );
+ }
+ }
+
+ /**
+ * Try to read the message from the offline storage.
+ *
+ * @returns {boolean} True if successfully read from the offline storage.
+ */
+ readFromLocalCache() {
+ if (
+ !this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface(
+ Ci.nsIMsgMailNewsUrl
+ ).msgIsInLocalCache &&
+ !this.URI.folder.hasMsgOffline(this._msgKey, null, 10)
+ ) {
+ return false;
+ }
+
+ let hdr = this.URI.folder.GetMessageHeader(this._msgKey);
+ let stream = this.URI.folder.getLocalMsgStream(hdr);
+ this._readFromCacheStream(stream);
+ return true;
+ }
+
+ /**
+ * Read the message from the a stream.
+ *
+ * @param {nsIInputStream} cacheStream - The input stream to read.
+ */
+ _readFromCacheStream(stream) {
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this._contentType = "";
+ pump.init(stream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: () => {
+ this._listener.onStartRequest(this);
+ this.URI.SetUrlState(true, Cr.NS_OK);
+ this._pending = true;
+ },
+ onStopRequest: (request, status) => {
+ this._listener.onStopRequest(this, status);
+ this.URI.SetUrlState(false, status);
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ this._pending = false;
+ },
+ onDataAvailable: (request, stream, offset, count) => {
+ this.contentLength += count;
+ this._listener.onDataAvailable(this, stream, offset, count);
+ try {
+ if (!stream.available()) {
+ stream.close();
+ }
+ } catch (e) {}
+ },
+ });
+ }
+
+ /**
+ * Retrieve the message from the server.
+ */
+ _readFromServer() {
+ this._logger.debug("Read from server");
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+
+ this._server.wrappedJSObject.withClient(this.URI.folder, client => {
+ client.startRunningUrl(null, null, this.URI);
+ client.channel = this;
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onReady = () => {
+ client.fetchMessage(this.URI.folder, this._msgKey);
+ };
+
+ client.onData = data => {
+ this.contentLength += data.length;
+ outputStream.write(data, data.length);
+ this._listener.onDataAvailable(this, inputStream, 0, data.length);
+ };
+
+ client.onDone = status => {
+ try {
+ this.loadGroup?.removeRequest(this, null, status);
+ } catch (e) {}
+ this._listener.onStopRequest(this, status);
+ };
+ this._pending = false;
+ });
+ }
+
+ /** @see nsIWritablePropertyBag */
+ getProperty(key) {
+ return this[key];
+ }
+
+ setProperty(key, value) {
+ this[key] = value;
+ }
+
+ deleteProperty(key) {
+ delete this[key];
+ }
+}
diff --git a/comm/mailnews/imap/src/ImapClient.jsm b/comm/mailnews/imap/src/ImapClient.jsm
new file mode 100644
index 0000000000..319a03b958
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapClient.jsm
@@ -0,0 +1,1895 @@
+/* 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 = ["ImapClient"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { clearTimeout, setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+var { ImapAuthenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+);
+var { ImapResponse } = ChromeUtils.import(
+ "resource:///modules/ImapResponse.jsm"
+);
+var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+
+// There can be multiple ImapClient running concurrently, assign each logger a
+// unique prefix.
+let loggerInstanceId = 0;
+
+const PR_UINT32_MAX = 0xffffffff;
+
+/**
+ * A class to interact with IMAP server.
+ */
+class ImapClient {
+ _logger = console.createInstance({
+ prefix: `mailnews.imap.${loggerInstanceId++}`,
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.imap.loglevel",
+ });
+
+ /**
+ * @param {nsIImapIncomingServer} server - The associated server instance.
+ */
+ constructor(server) {
+ this._server = server.QueryInterface(Ci.nsIMsgIncomingServer);
+ this._serverSink = this._server.QueryInterface(Ci.nsIImapServerSink);
+ this._authenticator = new ImapAuthenticator(server);
+
+ // Auth methods detected from the CAPABILITY response.
+ this._supportedAuthMethods = [];
+ // Subset of _supportedAuthMethods that are allowed by user preference.
+ this._possibleAuthMethods = [];
+ // Auth methods set by user preference.
+ this._preferredAuthMethods =
+ {
+ [Ci.nsMsgAuthMethod.passwordCleartext]: ["PLAIN", "LOGIN"],
+ [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"],
+ [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"],
+ [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"],
+ [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"],
+ [Ci.nsMsgAuthMethod.External]: ["EXTERNAL"],
+ }[server.authMethod] || [];
+ // The next auth method to try if the current failed.
+ this._nextAuthMethod = null;
+
+ this._tag = Math.floor(100 * Math.random());
+ this._charsetManager = Cc[
+ "@mozilla.org/charset-converter-manager;1"
+ ].getService(Ci.nsICharsetConverterManager);
+
+ this._messageUids = [];
+ this._messages = new Map();
+
+ this._loadPrefs();
+ }
+
+ /**
+ * @type {boolean} - Whether the socket is open.
+ */
+ get isOnline() {
+ return this._socket?.readyState == "open";
+ }
+
+ /**
+ * Load imap related preferences, many behaviors depend on these pref values.
+ */
+ _loadPrefs() {
+ this._prefs = {
+ tcpTimeout: Services.prefs.getIntPref("mailnews.tcptimeout"),
+ };
+ }
+
+ /**
+ * Reset some internal states to be safely reused.
+ */
+ _reset() {
+ this.onData = () => {};
+ this.onDone = () => {};
+
+ this._actionAfterDiscoverAllFolders = null;
+ this.channel = null;
+ this._urlListener = null;
+ this._msgWindow = null;
+ this._authenticating = false;
+ this.verifyLogon = false;
+ this._idling = false;
+ if (this._idleTimer) {
+ clearTimeout(this._idleTimer);
+ this._idleTimer = null;
+ }
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ if (this.isOnline) {
+ // Reuse the connection.
+ this.onReady();
+ this._setSocketTimeout(this._prefs.tcpTimeout);
+ } else {
+ let hostname = this._server.hostName.toLowerCase();
+ this._logger.debug(`Connecting to ${hostname}:${this._server.port}`);
+ this._greeted = false;
+ this._capabilities = null;
+ this._secureTransport = this._server.socketType == Ci.nsMsgSocketType.SSL;
+ this._socket = new TCPSocket(hostname, this._server.port, {
+ binaryType: "arraybuffer",
+ useSecureTransport: this._secureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+ }
+ }
+
+ /**
+ * Set socket timeout in seconds.
+ *
+ * @param {number} timeout - The timeout in seconds.
+ */
+ _setSocketTimeout(timeout) {
+ this._socket.transport?.setTimeout(
+ Ci.nsISocketTransport.TIMEOUT_READ_WRITE,
+ timeout
+ );
+ }
+
+ /**
+ * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when
+ * the current request is finished.
+ *
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIMsgMailNewsUrl} [runningUri] - The url to run, if provided.
+ * @returns {nsIMsgMailNewsUrl}
+ */
+ startRunningUrl(urlListener, msgWindow, runningUri) {
+ this._urlListener = urlListener;
+ this._msgWindow = msgWindow;
+ this.runningUri = runningUri;
+ if (!this.runningUri) {
+ this.runningUri = Services.io.newURI(
+ `imap://${this._server.hostName}:${this._server.port}`
+ );
+ }
+ this._urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+ this.runningUri
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .SetUrlState(true, Cr.NS_OK);
+ return this.runningUri;
+ }
+
+ /**
+ * Discover all folders if the current server hasn't already discovered.
+ */
+ _discoverAllFoldersIfNecessary = () => {
+ if (this._server.hasDiscoveredFolders) {
+ this.onReady();
+ return;
+ }
+ this._actionAfterDiscoverAllFolders = this.onReady;
+ this.discoverAllFolders(this._server.rootFolder);
+ };
+
+ /**
+ * Discover all folders.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ */
+ discoverAllFolders(folder) {
+ this._logger.debug("discoverAllFolders", folder.URI);
+
+ let handleListResponse = res => {
+ this._hasTrash = res.mailboxes.some(
+ mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_TRASH
+ );
+ if (!this._hasTrash) {
+ let trashFolderName = this._server.trashFolderName.toLowerCase();
+ let trashMailbox = res.mailboxes.find(
+ mailbox => mailbox.name.toLowerCase() == trashFolderName
+ );
+ if (trashMailbox) {
+ this._hasTrash = true;
+ trashMailbox.flags |= ImapUtils.FLAG_IMAP_TRASH;
+ }
+ }
+ for (let mailbox of res.mailboxes) {
+ this._serverSink.possibleImapMailbox(
+ mailbox.name.replaceAll(mailbox.delimiter, "/"),
+ mailbox.delimiter,
+ mailbox.flags
+ );
+ }
+ };
+
+ if (this._capabilities.includes("LIST-EXTENDED")) {
+ this._nextAction = res => {
+ handleListResponse(res);
+ this._actionFinishFolderDiscovery();
+ };
+ let command = 'LIST (SUBSCRIBED) "" "*"';
+ if (this._capabilities.includes("SPECIAL-USE")) {
+ command += " RETURN (SPECIAL-USE)"; // rfc6154
+ }
+ this._sendTagged(command);
+ return;
+ }
+
+ this._nextAction = res => {
+ this._nextAction = res2 => {
+ // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST,
+ // use the LIST responses.
+ for (let mailbox of res2.mailboxes) {
+ let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name);
+ if (
+ mailboxFromList?.flags &&
+ mailboxFromList?.flags != mailbox.flags
+ ) {
+ mailbox.flags = mailboxFromList.flags;
+ }
+ }
+ handleListResponse(res2);
+ this._actionFinishFolderDiscovery();
+ };
+ this._sendTagged('LSUB "" "*"');
+ };
+ let command = 'LIST "" "*"';
+ if (this._capabilities.includes("SPECIAL-USE")) {
+ command += " RETURN (SPECIAL-USE)"; // rfc6154
+ }
+ this._sendTagged(command);
+ }
+
+ /**
+ * Discover all folders for the subscribe dialog.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ */
+ discoverAllAndSubscribedFolders(folder) {
+ this._logger.debug("discoverAllAndSubscribedFolders", folder.URI);
+ let handleListResponse = res => {
+ for (let mailbox of res.mailboxes) {
+ this._serverSink.possibleImapMailbox(
+ mailbox.name.replaceAll(mailbox.delimiter, "/"),
+ mailbox.delimiter,
+ mailbox.flags
+ );
+ }
+ };
+
+ this._nextAction = res => {
+ handleListResponse(res);
+ this._server.doingLsub = false;
+ this._nextAction = res2 => {
+ // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST,
+ // use the LIST responses.
+ for (let mailbox of res2.mailboxes) {
+ let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name);
+ if (
+ mailboxFromList?.flags &&
+ mailboxFromList?.flags != mailbox.flags
+ ) {
+ mailbox.flags = mailboxFromList.flags;
+ }
+ }
+ handleListResponse(res2);
+ this._actionDone();
+ };
+ this._sendTagged('LIST "" "*"');
+ };
+ this._sendTagged('LSUB "" "*"');
+ this._server.doingLsub = true;
+ }
+
+ /**
+ * Select a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to select.
+ */
+ selectFolder(folder) {
+ this._logger.debug("selectFolder", folder.URI);
+ if (this.folder == folder) {
+ this._actionNoop();
+ return;
+ }
+ this._actionAfterSelectFolder = this._actionUidFetch;
+ this._nextAction = this._actionSelectResponse(folder);
+ this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`);
+ }
+
+ /**
+ * Rename a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to rename.
+ * @param {string} newName - The new folder name.
+ */
+ renameFolder(folder, newName) {
+ this._logger.debug("renameFolder", folder.URI, newName);
+ let delimiter =
+ folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/";
+ let names = this._getAncestorFolderNames(folder);
+ let oldName = this._getServerFolderName(folder);
+ newName = this._encodeMailboxName([...names, newName].join(delimiter));
+
+ this._nextAction = this._actionRenameResponse(oldName, newName);
+ this._sendTagged(`RENAME "${oldName}" "${newName}"`);
+ }
+
+ /**
+ * Move a source folder to be a child of another folder.
+ *
+ * @param {nsIMsgFolder} srcFolder - The source folder to move.
+ * @param {nsIMsgFolder} dstFolder - The target parent folder.
+ */
+ moveFolder(srcFolder, dstFolder) {
+ this._logger.debug("moveFolder", srcFolder.URI, dstFolder.URI);
+ let oldName = this._getServerFolderName(srcFolder);
+ let newName = this._getServerSubFolderName(dstFolder, srcFolder.name);
+ this._nextAction = this._actionRenameResponse(oldName, newName, true);
+ this._sendTagged(`RENAME "${oldName}" "${newName}"`);
+ }
+
+ /**
+ * Send LIST command for a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to list.
+ */
+ listFolder(folder) {
+ this._logger.debug("listFolder", folder.URI);
+ this._actionList(this._getServerFolderName(folder), () => {
+ this._actionDone();
+ });
+ }
+
+ /**
+ * Send DELETE command for a folder and all subfolders.
+ *
+ * @param {nsIMsgFolder} folder - The folder to delete.
+ */
+ deleteFolder(folder) {
+ this._logger.debug("deleteFolder", folder.URI);
+ this._nextAction = res => {
+ // Leaves have longer names than parent mailbox, sort them by the name
+ // length, so that leaf mailbox will be deleted first.
+ let mailboxes = res.mailboxes.sort(
+ (x, y) => y.name.length - x.name.length
+ );
+ let selfName = this._getServerFolderName(folder);
+ let selfIncluded = false;
+ this._nextAction = () => {
+ let mailbox = mailboxes.shift();
+ if (mailbox) {
+ this._sendTagged(`DELETE "${mailbox.name}"`);
+ if (!selfIncluded && selfName == mailbox.name) {
+ selfIncluded = true;
+ }
+ } else if (!selfIncluded) {
+ this._nextAction = () => this._actionDone();
+ this._sendTagged(`DELETE "${this._getServerFolderName(folder)}"`);
+ } else {
+ this._actionDone();
+ }
+ };
+ this._nextAction();
+ };
+ this._sendTagged(`LIST "" "${this._getServerFolderName(folder)}"`);
+ }
+
+ /**
+ * Ensure a folder exists on the server. Create one if not already exists.
+ *
+ * @param {nsIMsgFolder} parent - The parent folder to check.
+ * @param {string} folderName - The folder name.
+ */
+ ensureFolderExists(parent, folderName) {
+ this._logger.debug("ensureFolderExists", parent.URI, folderName);
+ let mailboxName = this._getServerSubFolderName(parent, folderName);
+ this._nextAction = res => {
+ if (res.mailboxes.length) {
+ // Already exists.
+ this._actionDone();
+ return;
+ }
+ // Create one and subscribe to it.
+ this._actionCreateAndSubscribe(mailboxName, res => {
+ this._actionList(mailboxName, () => this._actionDone());
+ });
+ };
+ this._sendTagged(`LIST "" "${mailboxName}"`);
+ }
+
+ /**
+ * Create a folder on the server.
+ *
+ * @param {nsIMsgFolder} parent - The parent folder to check.
+ * @param {string} folderName - The folder name.
+ */
+ createFolder(parent, folderName) {
+ this._logger.debug("createFolder", parent.URI, folderName);
+ let mailboxName = this._getServerSubFolderName(parent, folderName);
+ this._actionCreateAndSubscribe(mailboxName, res => {
+ this._actionList(mailboxName, () => this._actionDone());
+ });
+ }
+
+ /**
+ * Subscribe a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to subscribe.
+ * @param {string} folderName - The folder name.
+ */
+ subscribeFolder(folder, folderName) {
+ this._logger.debug("subscribeFolder", folder.URI, folderName);
+ this._nextAction = () => this._server.performExpand();
+ this._sendTagged(`SUBSCRIBE "${folderName}"`);
+ }
+
+ /**
+ * Unsubscribe a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to unsubscribe.
+ * @param {string} folderName - The folder name.
+ */
+ unsubscribeFolder(folder, folderName) {
+ this._logger.debug("unsubscribeFolder", folder.URI, folderName);
+ this._nextAction = () => this._server.performExpand();
+ this._sendTagged(`UNSUBSCRIBE "${folderName}"`);
+ }
+
+ /**
+ * Fetch the attribute of messages.
+ *
+ * @param {nsIMsgFolder} folder - The folder to check.
+ * @param {string} uids - The message uids.
+ * @param {string} attribute - The message attribute to fetch
+ */
+ fetchMsgAttribute(folder, uids, attribute) {
+ this._logger.debug("fetchMsgAttribute", folder.URI, uids, attribute);
+ this._nextAction = res => {
+ if (res.done) {
+ let resultAttributes = res.messages
+ .map(m => m.customAttributes[attribute])
+ .flat();
+ this.runningUri.QueryInterface(Ci.nsIImapUrl).customAttributeResult =
+ resultAttributes.length > 1
+ ? `(${resultAttributes.join(" ")})`
+ : resultAttributes[0];
+ this._actionDone();
+ }
+ };
+ this._sendTagged(`UID FETCH ${uids} (${attribute})`);
+ }
+
+ /**
+ * Delete all the messages in a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to delete messages.
+ */
+ deleteAllMessages(folder) {
+ this._logger.debug("deleteAllMessages", folder.URI);
+ this._actionInFolder(folder, () => {
+ if (!this._messages.size) {
+ this._actionDone();
+ return;
+ }
+
+ this._nextAction = () => this.expunge(folder);
+ this._sendTagged("UID STORE 1:* +FLAGS.SILENT (\\Deleted)");
+ });
+ }
+
+ /**
+ * Search in a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to delete messages.
+ * @param {string} searchCommand - The SEARCH command together with the search
+ * criteria.
+ */
+ search(folder, searchCommand) {
+ this._logger.debug("search", folder.URI);
+ this._actionInFolder(folder, () => {
+ this._nextAction = res => {
+ this.onData(res.search);
+ this._actionDone();
+ };
+ this._sendTagged(`UID ${searchCommand}`);
+ });
+ }
+
+ /**
+ * Get the names of all ancestor folders. For example,
+ * folder a/b/c will return ['a', 'b'].
+ *
+ * @param {nsIMsgFolder} folder - The input folder.
+ * @returns {string[]}
+ */
+ _getAncestorFolderNames(folder) {
+ let matches = /imap:\/\/[^/]+\/(.+)/.exec(folder.URI);
+ return matches[1].split("/").slice(0, -1);
+ }
+
+ /**
+ * When UTF8 is enabled, use the name directly. Otherwise, encode to mUTF-7.
+ *
+ * @param {string} name - The mailbox name.
+ */
+ _encodeMailboxName(name) {
+ return this._utf8Enabled ? name : this._charsetManager.unicodeToMutf7(name);
+ }
+
+ /**
+ * Get the server name of a msg folder.
+ *
+ * @param {nsIMsgFolder} folder - The input folder.
+ * @returns {string}
+ */
+ _getServerFolderName(folder) {
+ if (folder.isServer) {
+ return "";
+ }
+
+ if (folder.onlineName) {
+ return folder.onlineName.replaceAll('"', '\\"');
+ }
+ let delimiter =
+ folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/";
+ let names = this._getAncestorFolderNames(folder);
+ return this._encodeMailboxName(
+ [...names, folder.name].join(delimiter)
+ ).replaceAll('"', '\\"');
+ }
+
+ /**
+ * Get the server name of a sub folder. The sub folder may or may not exist on
+ * the server.
+ *
+ * @param {nsIMsgFolder} parent - The parent folder.
+ * @param {string} folderName - The sub folder name.
+ * @returns {string}
+ */
+ _getServerSubFolderName(parent, folderName) {
+ folderName = this._encodeMailboxName(folderName);
+ let mailboxName = this._getServerFolderName(parent);
+ if (mailboxName) {
+ let delimiter = parent.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).hierarchyDelimiter;
+ // @see nsImapCore.h.
+ const ONLINE_HIERARCHY_SEPARATOR_UNKNOWN = "^";
+ if (!delimiter || delimiter == ONLINE_HIERARCHY_SEPARATOR_UNKNOWN) {
+ delimiter = "/";
+ }
+ return mailboxName + delimiter + folderName;
+ }
+ return folderName;
+ }
+
+ /**
+ * Fetch the full content of a message by UID.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ * @param {number} uid - The message uid.
+ * @param {number} [size] - The body size to fetch.
+ */
+ fetchMessage(folder, uid, size) {
+ this._logger.debug(`fetchMessage folder=${folder.name} uid=${uid}`);
+ if (folder.hasMsgOffline(uid, null, 10)) {
+ this.onDone = () => {};
+ this.channel?.readFromLocalCache();
+ this._actionDone();
+ return;
+ }
+ this._actionInFolder(folder, () => {
+ this._nextAction = this._actionUidFetchBodyResponse;
+ let command;
+ if (size) {
+ command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (Content-Type Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.${size}>)`;
+ } else {
+ command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[])`;
+ }
+ this._sendTagged(command);
+ });
+ }
+
+ /**
+ * Add, remove or replace flags of specified messages.
+ *
+ * @param {string} action - "+" means add, "-" means remove, "" means replace.
+ * @param {nsIMsgFolder} folder - The target folder.
+ * @param {string} messageIds - Message UIDs, e.g. "23,30:33".
+ * @param {number} flags - The internal flags number to update.
+ */
+ updateMessageFlags(action, folder, messageIds, flags) {
+ this._actionInFolder(folder, () => {
+ this._nextAction = () => this._actionDone();
+ // _supportedFlags is available after _actionSelectResponse.
+ let flagsStr = ImapUtils.flagsToString(flags, this._supportedFlags);
+ this._sendTagged(`UID STORE ${messageIds} ${action}FLAGS (${flagsStr})`);
+ });
+ }
+
+ /**
+ * Send EXPUNGE command to a folder.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ */
+ expunge(folder) {
+ this._actionInFolder(folder, () => {
+ this._nextAction = () => this._actionDone();
+ this._sendTagged("EXPUNGE");
+ });
+ }
+
+ /**
+ * Move or copy messages from a folder to another folder.
+ *
+ * @param {nsIMsgFolder} folder - The source folder.
+ * @param {nsIMsgFolder} folder - The target folder.
+ * @param {string} messageIds - The message identifiers.
+ * @param {boolean} idsAreUids - If true messageIds are UIDs, otherwise,
+ * messageIds are sequences.
+ * @param {boolean} isMove - If true, use MOVE command when supported.
+ */
+ copy(folder, dstFolder, messageIds, idsAreUids, isMove) {
+ let command = idsAreUids ? "UID " : "";
+ command +=
+ isMove && this._capabilities.includes("MOVE")
+ ? "MOVE " // rfc6851
+ : "COPY ";
+ command += messageIds + ` "${this._getServerFolderName(dstFolder)}"`;
+ this._actionInFolder(folder, () => {
+ this._nextAction = this._actionNoopResponse;
+ this._sendTagged(command);
+ });
+ }
+
+ /**
+ * Upload a message file to a folder.
+ *
+ * @param {nsIFile} file - The message file to upload.
+ * @param {nsIMsgFolder} dstFolder - The target folder.
+ * @param {nsImapMailCopyState} copyState - A state used by nsImapMailFolder.
+ * @param {boolean} isDraft - Is the uploaded file a draft.
+ */
+ async uploadMessageFromFile(file, dstFolder, copyState, isDraft) {
+ this._logger.debug("uploadMessageFromFile", file.path, dstFolder.URI);
+ let mailbox = this._getServerFolderName(dstFolder);
+ let content = MailStringUtils.uint8ArrayToByteString(
+ await IOUtils.read(file.path)
+ );
+ this._nextAction = res => {
+ if (res.tag != "+") {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._nextAction = res => {
+ this._folderSink = dstFolder.QueryInterface(Ci.nsIImapMailFolderSink);
+ if (
+ // See rfc4315.
+ this._capabilities.includes("UIDPLUS") &&
+ res.attributes.appenduid
+ ) {
+ // The response is like `<tag> OK [APPENDUID <uidvalidity> <uid>]`.
+ this._folderSink.setAppendMsgUid(
+ res.attributes.appenduid[1],
+ this.runningUri
+ );
+ }
+ this._actionDone();
+ if (res.exists) {
+ // FIXME: _actionNoopResponse should be enough here, but it breaks
+ // test_imapAttachmentSaves.js.
+ this.folder = null;
+ }
+ try {
+ this._folderSink.copyNextStreamMessage(true, copyState);
+ } catch (e) {
+ this._logger.warn("copyNextStreamMessage failed", e);
+ }
+ };
+ this._send(content + (this._utf8Enabled ? ")" : ""));
+ };
+ let outKeywords = {};
+ let flags = dstFolder
+ .QueryInterface(Ci.nsIImapMessageSink)
+ .getCurMoveCopyMessageInfo(this.runningUri, {}, outKeywords);
+ let flagString = ImapUtils.flagsToString(flags, this._supportedFlags);
+ if (isDraft && !/\b\Draft\b/.test(flagString)) {
+ flagString += " \\Draft";
+ }
+ if (outKeywords.value) {
+ flagString += " " + outKeywords.value;
+ }
+ let open = this._utf8Enabled ? "UTF8 (~{" : "{";
+ let command = `APPEND "${mailbox}" (${flagString.trim()}) ${open}${
+ content.length
+ }}`;
+ this._sendTagged(command);
+ }
+
+ /**
+ * Check the status of a folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to check.
+ */
+ updateFolderStatus(folder) {
+ this._logger.debug("updateFolderStatus", folder.URI);
+ if (this._folder == folder) {
+ // According to rfc3501, "the STATUS command SHOULD NOT be used on the
+ // currently selected mailbox", so use NOOP instead.
+ this._actionNoop();
+ return;
+ }
+
+ this._nextAction = res => {
+ if (res.status == "OK") {
+ folder
+ .QueryInterface(Ci.nsIImapMailFolderSink)
+ .UpdateImapMailboxStatus(this, {
+ QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]),
+ nextUID: res.attributes.uidnext,
+ numMessages: res.attributes.messages.length,
+ numUnseenMessages: res.attributes.unseen,
+ });
+ folder.msgDatabase = null;
+ }
+ this._actionDone();
+ };
+ this._sendTagged(
+ `STATUS "${this._getServerFolderName(folder)}" (UIDNEXT MESSAGES UNSEEN)`
+ );
+ }
+
+ /**
+ * Update message flags.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ * @param {string} flagsToAdd - The flags to add.
+ * @param {string} flagsToSubtract - The flags to subtract.
+ * @param {string} uids - The message uids.
+ */
+ storeCustomKeywords(folder, flagsToAdd, flagsToSubtract, uids) {
+ this._logger.debug(
+ "storeCustomKeywords",
+ folder.URI,
+ flagsToAdd,
+ flagsToSubtract,
+ uids
+ );
+ let subtractFlags = () => {
+ if (flagsToSubtract) {
+ this._nextAction = () => {
+ this._actionDone();
+ };
+ this._sendTagged(`UID STORE ${uids} -FLAGS (${flagsToSubtract})`);
+ } else {
+ this._actionDone();
+ }
+ };
+ this._actionInFolder(folder, () => {
+ if (flagsToAdd) {
+ this._nextAction = () => {
+ subtractFlags();
+ };
+ this._sendTagged(`UID STORE ${uids} +FLAGS (${flagsToAdd})`);
+ } else {
+ subtractFlags();
+ }
+ });
+ }
+
+ /**
+ * Get message headers by the specified uids.
+ *
+ * @param {nsIMsgFolder} folder - The folder of the messages.
+ * @param {string[]} uids - The message uids.
+ */
+ getHeaders(folder, uids) {
+ this._logger.debug("getHeaders", folder.URI, uids);
+ this._actionInFolder(folder, () => {
+ this._nextAction = this._actionUidFetchHeaderResponse;
+ let extraItems = "";
+ if (this._server.isGMailServer) {
+ extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS ";
+ }
+ this._sendTagged(
+ `UID FETCH ${uids} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])`
+ );
+ });
+ }
+
+ /**
+ * Send IDLE command to the server.
+ */
+ idle() {
+ if (!this.folder) {
+ this._actionDone();
+ return;
+ }
+ this._nextAction = res => {
+ if (res.tag == "*") {
+ this.folder.performingBiff = true;
+ this._actionNoopResponse(res);
+ }
+ };
+ this._sendTagged("IDLE");
+ this._setSocketTimeout(PR_UINT32_MAX);
+ this._idling = true;
+ this._idleTimer = setTimeout(() => {
+ this.endIdle(() => {
+ this._actionNoop();
+ });
+ // Per rfc2177, should terminate the IDLE and re-issue it at least every
+ // 29 minutes. But in practice many servers timeout before that. A noop
+ // every 5min is better than timeout.
+ }, 5 * 60 * 1000);
+ this._logger.debug(`Idling in ${this.folder.URI}`);
+ }
+
+ /**
+ * Send DONE to end the IDLE command.
+ *
+ * @param {Function} nextAction - Callback function after IDLE is ended.
+ */
+ endIdle(nextAction) {
+ this._nextAction = res => {
+ if (res.status == "OK") {
+ nextAction();
+ }
+ };
+ this._send("DONE");
+ this._idling = false;
+ this.busy = true;
+ clearTimeout(this._idleTimer);
+ this._idleTimer = null;
+ }
+
+ /**
+ * Send LOGOUT and close the socket.
+ */
+ logout() {
+ this._sendTagged("LOGOUT");
+ this._socket.close();
+ }
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this._nextAction = res => {
+ this._greeted = true;
+ this._actionCapabilityResponse(res);
+ };
+
+ this._setSocketTimeout(this._prefs.tcpTimeout);
+ };
+
+ /**
+ * The data event handler.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = async event => {
+ // Without this, some tests are blocked waiting for response from Maild.jsm.
+ // Don't know the real cause, but possibly because ImapClient and Maild runs
+ // on the same process. We also have this in Pop3Client.
+ await new Promise(resolve => setTimeout(resolve));
+
+ let stringPayload = this._utf8Enabled
+ ? new TextDecoder().decode(event.data)
+ : MailStringUtils.uint8ArrayToByteString(new Uint8Array(event.data));
+ this._logger.debug(`S: ${stringPayload}`);
+ if (!this._response || this._idling || this._response.done) {
+ this._response = new ImapResponse();
+ this._response.onMessage = this._onMessage;
+ }
+ this._response.parse(stringPayload);
+ if (
+ !this._authenticating &&
+ this._response.done &&
+ this._response.status &&
+ this._response.tag != "+" &&
+ !["OK", "+"].includes(this._response.status)
+ ) {
+ this._actionDone(ImapUtils.NS_MSG_ERROR_IMAP_COMMAND_FAILED);
+ return;
+ }
+ if (!this._greeted || this._idling || this._response.done) {
+ this._nextAction?.(this._response);
+ }
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = async event => {
+ this._logger.error(`${event.name}: a ${event.message} error occurred`);
+ if (event.errorCode == Cr.NS_ERROR_NET_TIMEOUT) {
+ this._actionError("imapNetTimeoutError");
+ this._actionDone(event.errorCode);
+ return;
+ }
+
+ let secInfo =
+ await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo();
+ if (secInfo) {
+ this._logger.error(`SecurityError info: ${secInfo.errorCodeString}`);
+ if (secInfo.failedCertChain.length) {
+ let chain = secInfo.failedCertChain.map(c => {
+ return c.commonName + "; serial# " + c.serialNumber;
+ });
+ this._logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`);
+ }
+ this.runningUri.failedSecInfo = secInfo;
+ this._server.closeCachedConnections();
+ } else {
+ this.logout();
+ }
+
+ this._actionDone(event.errorCode);
+ };
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed.");
+ this.folder = null;
+ };
+
+ /**
+ * Send a command to the server.
+ *
+ * @param {string} str - The command string to send.
+ * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str.
+ */
+ _send(str, suppressLogging) {
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this._logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ // Do not suppress for non-release builds, so that debugging auth problems
+ // is easier.
+ this._logger.debug(`C: ${str}`);
+ }
+
+ if (!this.isOnline) {
+ if (!str.includes("LOGOUT")) {
+ this._logger.warn(
+ `Failed to send because socket state is ${this._socket?.readyState}`
+ );
+ }
+ return;
+ }
+
+ let encode = this._utf8Enabled
+ ? x => new TextEncoder().encode(x)
+ : MailStringUtils.byteStringToUint8Array;
+ this._socket.send(encode(str + "\r\n").buffer);
+ }
+
+ /**
+ * Same as _send, but prepend a tag to the command.
+ */
+ _sendTagged(str, suppressLogging) {
+ if (this._idling) {
+ let nextAction = this._nextAction;
+ this.endIdle(() => {
+ this._nextAction = nextAction;
+ this._sendTagged(str, suppressLogging);
+ });
+ } else {
+ this._send(`${this._getNextTag()} ${str}`, suppressLogging);
+ }
+ }
+
+ /**
+ * Get the next command tag.
+ *
+ * @returns {number}
+ */
+ _getNextTag() {
+ this._tag = (this._tag + 1) % 100;
+ return this._tag;
+ }
+
+ /**
+ * Send CAPABILITY command to the server.
+ */
+ _actionCapability() {
+ this._nextAction = this._actionCapabilityResponse;
+ this._sendTagged("CAPABILITY");
+ }
+
+ /**
+ * Handle the capability response.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionCapabilityResponse = res => {
+ if (res.capabilities) {
+ this._capabilities = res.capabilities;
+ this._server.wrappedJSObject.capabilities = res.capabilities;
+ if (this._capabilities.includes("X-GM-EXT-1")) {
+ this._server.isGMailServer = true;
+ }
+
+ this._supportedAuthMethods = res.authMethods;
+ this._actionChooseFirstAuthMethod();
+ } else {
+ this._actionCapability();
+ }
+ };
+
+ /**
+ * Decide the first auth method to try.
+ */
+ _actionChooseFirstAuthMethod = () => {
+ if (
+ [
+ Ci.nsMsgSocketType.trySTARTTLS,
+ Ci.nsMsgSocketType.alwaysSTARTTLS,
+ ].includes(this._server.socketType) &&
+ !this._secureTransport
+ ) {
+ if (this._capabilities.includes("STARTTLS")) {
+ // Init STARTTLS negotiation if required by user pref and supported.
+ this._nextAction = this._actionStarttlsResponse;
+ this._sendTagged("STARTTLS");
+ } else {
+ // Abort if not supported.
+ this._logger.error("Server doesn't support STARTTLS. Aborting.");
+ this._actionError("imapServerDisconnected");
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+ return;
+ }
+
+ this._possibleAuthMethods = this._preferredAuthMethods.filter(x =>
+ this._supportedAuthMethods.includes(x)
+ );
+ if (
+ !this._possibleAuthMethods.length &&
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext &&
+ !this._capabilities.includes("LOGINDISABLED")
+ ) {
+ this._possibleAuthMethods = ["OLDLOGIN"];
+ }
+ this._logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`);
+ this._nextAuthMethod = this._possibleAuthMethods[0];
+ if (this._capabilities.includes("CLIENTID") && this._server.clientid) {
+ this._nextAction = res => {
+ if (res.status == "OK") {
+ this._actionAuth();
+ } else {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+ };
+ this._sendTagged(`CLIENTID UUID ${this._server.clientid}`);
+ } else {
+ this._actionAuth();
+ }
+ };
+
+ /**
+ * Handle the STARTTLS response.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionStarttlsResponse(res) {
+ if (!res.status == "OK") {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._socket.upgradeToSecure();
+ this._secureTransport = true;
+ this._actionCapability();
+ }
+
+ /**
+ * Init authentication depending on server capabilities and user prefs.
+ */
+ _actionAuth = async () => {
+ if (!this._nextAuthMethod) {
+ this._socket.close();
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+
+ this._authenticating = true;
+
+ this._currentAuthMethod = this._nextAuthMethod;
+ this._nextAuthMethod =
+ this._possibleAuthMethods[
+ this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1
+ ];
+
+ switch (this._currentAuthMethod) {
+ case "OLDLOGIN":
+ this._nextAction = this._actionAuthResponse;
+ let password = await this._getPassword();
+ this._sendTagged(
+ `LOGIN ${this._authenticator.username} ${password}`,
+ true
+ );
+ break;
+ case "PLAIN":
+ this._nextAction = this._actionAuthPlain;
+ this._sendTagged("AUTHENTICATE PLAIN");
+ break;
+ case "LOGIN":
+ this._nextAction = this._actionAuthLoginUser;
+ this._sendTagged("AUTHENTICATE LOGIN");
+ break;
+ case "CRAM-MD5":
+ this._nextAction = this._actionAuthCramMd5;
+ this._sendTagged("AUTHENTICATE CRAM-MD5");
+ break;
+ case "GSSAPI": {
+ this._nextAction = this._actionAuthGssapi;
+ this._authenticator.initGssapiAuth("imap");
+ let token;
+ try {
+ token = this._authenticator.getNextGssapiToken("");
+ } catch (e) {
+ this._logger.error(e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._sendTagged(`AUTHENTICATE GSSAPI ${token}`, true);
+ break;
+ }
+ case "NTLM": {
+ this._nextAction = this._actionAuthNtlm;
+ this._authenticator.initNtlmAuth("imap");
+ let token;
+ try {
+ token = this._authenticator.getNextNtlmToken("");
+ } catch (e) {
+ this._logger.error(e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._sendTagged(`AUTHENTICATE NTLM ${token}`, true);
+ break;
+ }
+ case "XOAUTH2":
+ this._nextAction = this._actionAuthResponse;
+ let token = await this._authenticator.getOAuthToken();
+ this._sendTagged(`AUTHENTICATE XOAUTH2 ${token}`, true);
+ break;
+ case "EXTERNAL":
+ this._nextAction = this._actionAuthResponse;
+ this._sendTagged(
+ `AUTHENTICATE EXTERNAL ${this._authenticator.username}`
+ );
+ break;
+ default:
+ this._actionDone();
+ }
+ };
+
+ /**
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionAuthResponse = res => {
+ this._authenticating = false;
+
+ if (this.verifyLogon) {
+ this._actionDone(res.status == "OK" ? Cr.NS_OK : Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ if (res.status == "OK") {
+ this._serverSink.userAuthenticated = true;
+ if (res.capabilities) {
+ this._capabilities = res.capabilities;
+ this._server.wrappedJSObject.capabilities = res.capabilities;
+ this._actionId();
+ } else {
+ this._nextAction = res => {
+ this._capabilities = res.capabilities;
+ this._server.wrappedJSObject.capabilities = res.capabilities;
+ this._actionId();
+ };
+ this._sendTagged("CAPABILITY");
+ }
+ return;
+ }
+ if (
+ ["OLDLOGIN", "PLAIN", "LOGIN", "CRAM-MD5"].includes(
+ this._currentAuthMethod
+ )
+ ) {
+ // Ask user what to do.
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this._socket.close();
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ if (action == 2) {
+ // 'New password' button pressed.
+ this._authenticator.forgetPassword();
+ }
+
+ // Retry.
+ this._nextAuthMethod = this._possibleAuthMethods[0];
+ this._actionAuth();
+ return;
+ }
+ this._logger.error("Authentication failed.");
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ };
+
+ /**
+ * Returns the saved/cached server password, or show a password dialog. If the
+ * user cancels the dialog, stop the process.
+ *
+ * @returns {string} The server password.
+ */
+ async _getPassword() {
+ try {
+ let password = await this._authenticator.getPassword();
+ return password;
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ this._actionDone(e.result);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * The second step of PLAIN auth. Send the auth token to the server.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionAuthPlain = async res => {
+ this._nextAction = this._actionAuthResponse;
+ this._send(await this._authenticator.getPlainToken(), true);
+ };
+
+ /**
+ * The second step of LOGIN auth. Send the username to the server.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionAuthLoginUser = res => {
+ this._nextAction = this._actionAuthLoginPass;
+ this._send(btoa(this._authenticator.username), true);
+ };
+
+ /**
+ * The third step of LOGIN auth. Send the password to the server.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionAuthLoginPass = async res => {
+ this._nextAction = this._actionAuthResponse;
+ let password = MailStringUtils.stringToByteString(
+ await this._getPassword()
+ );
+ this._send(btoa(password), true);
+ };
+
+ /**
+ * The second step of CRAM-MD5 auth, send a HMAC-MD5 signature to the server.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionAuthCramMd5 = async res => {
+ this._nextAction = this._actionAuthResponse;
+ let password = await this._getPassword();
+ this._send(
+ this._authenticator.getCramMd5Token(password, res.statusText),
+ true
+ );
+ };
+
+ /**
+ * The second and next step of GSSAPI auth.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionAuthGssapi = res => {
+ if (res.tag != "+") {
+ this._actionAuthResponse(res);
+ return;
+ }
+
+ // Server returns a challenge, we send a new token. Can happen multiple times.
+ let token;
+ try {
+ token = this._authenticator.getNextGssapiToken(res.statusText);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionAuthResponse(res);
+ return;
+ }
+ this._send(token, true);
+ };
+
+ /**
+ * The second and next step of NTLM auth.
+ *
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionAuthNtlm = res => {
+ if (res.tag != "+") {
+ this._actionAuthResponse(res);
+ return;
+ }
+
+ // Server returns a challenge, we send a new token. Can happen multiple times.
+ let token;
+ try {
+ token = this._authenticator.getNextNtlmToken(res.statusText);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionAuthResponse(res);
+ return;
+ }
+ this._send(token, true);
+ };
+
+ /**
+ * Send ID command to the server.
+ *
+ * @param {Function} [actionAfter] - A callback after processing ID command.
+ */
+ _actionId = (actionAfter = this._actionEnableUtf8) => {
+ if (this._capabilities.includes("ID") && Services.appinfo.name) {
+ this._nextAction = res => {
+ this._server.serverIDPref = res.id;
+ actionAfter();
+ };
+ this._sendTagged(
+ `ID ("name" "${Services.appinfo.name}" "version" "${Services.appinfo.version}")`
+ );
+ } else {
+ actionAfter();
+ }
+ };
+
+ /**
+ * Enable UTF8 if supported by the server.
+ *
+ * @param {Function} [actionAfter] - A callback after processing ENABLE UTF8.
+ */
+ _actionEnableUtf8 = (actionAfter = this._discoverAllFoldersIfNecessary) => {
+ if (
+ this._server.allowUTF8Accept &&
+ (this._capabilities.includes("UTF8=ACCEPT") ||
+ this._capabilities.includes("UTF8=ONLY"))
+ ) {
+ this._nextAction = res => {
+ this._utf8Enabled = res.status == "OK";
+ this._server.utf8AcceptEnabled = this._utf8Enabled;
+ actionAfter();
+ };
+ this._sendTagged("ENABLE UTF8=ACCEPT");
+ } else {
+ this._utf8Enabled = false;
+ actionAfter();
+ }
+ };
+
+ /**
+ * Execute an action with a folder selected.
+ *
+ * @param {nsIMsgFolder} folder - The folder to select.
+ * @param {Function} actionInFolder - The action to execute.
+ */
+ _actionInFolder(folder, actionInFolder) {
+ if (this.folder == folder) {
+ // If already in the folder, execute the action now.
+ actionInFolder();
+ } else {
+ // Send the SELECT command and queue the action.
+ this._actionAfterSelectFolder = actionInFolder;
+ this._nextAction = this._actionSelectResponse(folder);
+ this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`);
+ }
+ }
+
+ /**
+ * Send LSUB or LIST command depending on the server capabilities.
+ *
+ * @param {string} [mailbox="*"] - The mailbox to list, default to list all.
+ */
+ _actionListOrLsub(mailbox = "*") {
+ this._nextAction = this._actionListResponse();
+ let command = this._capabilities.includes("LIST-EXTENDED")
+ ? "LIST (SUBSCRIBED)" // rfc5258
+ : "LSUB";
+ command += ` "" "${mailbox}"`;
+ if (this._capabilities.includes("SPECIAL-USE")) {
+ command += " RETURN (SPECIAL-USE)"; // rfc6154
+ }
+ this._sendTagged(command);
+ this._listInboxSent = false;
+ }
+
+ /**
+ * Handle LIST response.
+ *
+ * @param {Function} actionAfterResponse - A callback after handling the response.
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionListResponse =
+ (actionAfterResponse = this._actionFinishFolderDiscovery) =>
+ res => {
+ if (!this._hasInbox) {
+ this._hasInbox = res.mailboxes.some(
+ mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_INBOX
+ );
+ }
+ for (let mailbox of res.mailboxes) {
+ this._serverSink.possibleImapMailbox(
+ mailbox.name.replaceAll(mailbox.delimiter, "/"),
+ mailbox.delimiter,
+ mailbox.flags
+ );
+ }
+
+ actionAfterResponse(res);
+ };
+
+ /**
+ * Send LIST command.
+ *
+ * @param {string} folderName - The name of the folder to list.
+ * @param {Function} actionAfterResponse - A callback after handling the response.
+ */
+ _actionList(folderName, actionAfterResponse) {
+ this._nextAction = this._actionListResponse(actionAfterResponse);
+ this._sendTagged(`LIST "" "${folderName}"`);
+ }
+
+ /**
+ * Finish folder discovery after checking Inbox and Trash folders.
+ */
+ _actionFinishFolderDiscovery = () => {
+ if (!this._hasInbox && !this._listInboxSent) {
+ this._actionList("INBOX");
+ this._listInboxSent = true;
+ return;
+ }
+ if (!this._hasTrash && !this._listTrashSent) {
+ this._actionCreateTrashFolderIfNeeded();
+ return;
+ }
+ this._serverSink.discoveryDone();
+ this._actionAfterDiscoverAllFolders
+ ? this._actionAfterDiscoverAllFolders()
+ : this._actionDone();
+ };
+
+ /**
+ * If Trash folder is not found on server, create one and subscribe to it.
+ */
+ _actionCreateTrashFolderIfNeeded() {
+ let trashFolderName = this._server.trashFolderName;
+ this._actionList(trashFolderName, res => {
+ this._hasTrash = res.mailboxes.length > 0;
+ if (this._hasTrash) {
+ // Trash folder exists.
+ this._actionFinishFolderDiscovery();
+ } else {
+ // Trash folder doesn't exist, create one and subscribe to it.
+ this._nextAction = res => {
+ this._actionList(trashFolderName, () => {
+ // After subscribing, finish folder discovery.
+ this._nextAction = this._actionFinishFolderDiscovery;
+ this._sendTagged(`SUBSCRIBE "${trashFolderName}"`);
+ });
+ };
+ this._sendTagged(`CREATE "${trashFolderName}"`);
+ }
+ });
+ this._listTrashSent = true;
+ }
+
+ /**
+ * Create and subscribe to a folder.
+ *
+ * @param {string} folderName - The folder name.
+ * @param {Function} callbackAfterSubscribe - The action after the subscribe
+ * command.
+ */
+ _actionCreateAndSubscribe(folderName, callbackAfterSubscribe) {
+ this._nextAction = res => {
+ this._nextAction = callbackAfterSubscribe;
+ this._sendTagged(`SUBSCRIBE "${folderName}"`);
+ };
+ this._sendTagged(`CREATE "${folderName}"`);
+ }
+
+ /**
+ * Handle SELECT response.
+ */
+ _actionSelectResponse = folder => res => {
+ if (folder) {
+ this.folder = folder;
+ }
+ this._supportedFlags = res.permanentflags || res.flags;
+ this._folderState = res;
+ if (this._capabilities.includes("QUOTA")) {
+ this._actionGetQuotaData();
+ } else {
+ this._actionAfterSelectFolder();
+ }
+ };
+
+ /**
+ * Send GETQUOTAROOT command and handle the response.
+ */
+ _actionGetQuotaData() {
+ this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink);
+ this._nextAction = res => {
+ const INVALIDATE_QUOTA = 0;
+ const STORE_QUOTA = 1;
+ const VALIDATE_QUOTA = 2;
+ for (let root of res.quotaRoots || []) {
+ this._folderSink.setFolderQuotaData(INVALIDATE_QUOTA, root, 0, 0);
+ }
+ for (let [mailbox, resource, usage, limit] of res.quotas || []) {
+ this._folderSink.setFolderQuotaData(
+ STORE_QUOTA,
+ mailbox ? `${mailbox} / ${resource}` : resource,
+ usage,
+ limit
+ );
+ }
+ this._folderSink.setFolderQuotaData(VALIDATE_QUOTA, "", 0, 0);
+ this._actionAfterSelectFolder();
+ };
+ this._sendTagged(
+ `GETQUOTAROOT "${this._getServerFolderName(this.folder)}"`
+ );
+ this._folderSink.folderQuotaCommandIssued = true;
+ }
+
+ /**
+ * Handle RENAME response. Three steps are involved.
+ *
+ * @param {string} oldName - The old folder name.
+ * @param {string} newName - The new folder name.
+ * @param {boolean} [isMove] - Is it response to MOVE command.
+ * @param {ImapResponse} res - The server response.
+ */
+ _actionRenameResponse = (oldName, newName, isMove) => res => {
+ // Step 3: Rename the local folder and send LIST command to re-sync folders.
+ let actionAfterUnsubscribe = () => {
+ this._serverSink.onlineFolderRename(this._msgWindow, oldName, newName);
+ if (isMove) {
+ this._actionDone();
+ } else {
+ this._actionListOrLsub(newName);
+ }
+ };
+ // Step 2: unsubscribe to the oldName.
+ this._nextAction = () => {
+ this._nextAction = actionAfterUnsubscribe;
+ this._sendTagged(`UNSUBSCRIBE "${oldName}"`);
+ };
+ // Step 1: subscribe to the newName.
+ this._sendTagged(`SUBSCRIBE "${newName}"`);
+ };
+
+ /**
+ * Send UID FETCH request to the server.
+ */
+ _actionUidFetch() {
+ if (this.runningUri.imapAction == Ci.nsIImapUrl.nsImapLiteSelectFolder) {
+ this._nextAction = () => this._actionDone();
+ } else {
+ this._nextAction = this._actionUidFetchResponse;
+ }
+ this._sendTagged("UID FETCH 1:* (FLAGS)");
+ }
+
+ /**
+ * Handle UID FETCH response.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionUidFetchResponse(res) {
+ let outFolderInfo = {};
+ this.folder.getDBFolderInfoAndDB(outFolderInfo);
+ let highestUid = outFolderInfo.value.getUint32Property(
+ "highestRecordedUID",
+ 0
+ );
+ this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink);
+ this._folderSink.UpdateImapMailboxInfo(this, this._getMailboxSpec());
+ let latestUid = this._messageUids.at(-1);
+ if (latestUid > highestUid) {
+ let extraItems = "";
+ if (this._server.isGMailServer) {
+ extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS ";
+ }
+ this._nextAction = this._actionUidFetchHeaderResponse;
+ this._sendTagged(
+ `UID FETCH ${
+ highestUid + 1
+ }:${latestUid} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])`
+ );
+ } else {
+ this._folderSink.headerFetchCompleted(this);
+ if (this._bodysToDownload.length) {
+ let uids = this._bodysToDownload.join(",");
+ this._nextAction = this._actionUidFetchBodyResponse;
+ this._sendTagged(
+ `UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])`
+ );
+ return;
+ }
+ this._actionDone();
+ }
+ }
+
+ /**
+ * Make an nsIMailboxSpec instance to interact with nsIImapMailFolderSink.
+ *
+ * @returns {nsIMailboxSpec}
+ */
+ _getMailboxSpec() {
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]),
+ folder_UIDVALIDITY: this._folderState.uidvalidity,
+ box_flags: this._folderState.flags,
+ supportedUserFlags: this._folderState.supportedUserFlags,
+ nextUID: this._folderState.attributes.uidnext,
+ numMessages: this._messages.size,
+ numUnseenMessages: this._folderState.attributes.unseen,
+ flagState: this.flagAndUidState,
+ };
+ }
+
+ /**
+ * Handle UID FETCH BODY.PEEK[HEADER] response.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionUidFetchHeaderResponse(res) {
+ this.folder
+ .QueryInterface(Ci.nsIImapMailFolderSink)
+ .headerFetchCompleted(this);
+ if (this._bodysToDownload.length) {
+ // nsImapMailFolder decides to fetch the full body by calling
+ // NotifyBodysToDownload.
+ let uids = this._bodysToDownload.join(",");
+ this._nextAction = this._actionUidFetchBodyResponse;
+ this._sendTagged(`UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])`);
+ return;
+ }
+ this._actionDone();
+ }
+
+ /**
+ * Handle UID FETCH BODY response.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionUidFetchBodyResponse(res) {
+ this._actionDone();
+ }
+
+ /**
+ * Handle a single message data response.
+ *
+ * @param {MessageData} msg - Message data parsed in ImapResponse.
+ */
+ _onMessage = msg => {
+ this._msgSink = this.folder.QueryInterface(Ci.nsIImapMessageSink);
+ this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink);
+
+ // Handle message flags.
+ if ((msg.uid || msg.sequence) && msg.flags != undefined) {
+ let uid = msg.uid;
+ if (uid && msg.sequence) {
+ this._messageUids[msg.sequence] = uid;
+ this._messages.set(uid, msg);
+ } else if (msg.sequence) {
+ uid = this._messageUids[msg.sequence];
+ }
+ if (uid) {
+ this.folder
+ .QueryInterface(Ci.nsIImapMessageSink)
+ .notifyMessageFlags(
+ msg.flags,
+ msg.keywords,
+ uid,
+ this._folderState.highestmodseq
+ );
+ }
+ }
+
+ if (msg.body) {
+ if (!msg.body.endsWith("\r\n")) {
+ msg.body += "\r\n";
+ }
+ if (msg.bodySection.length == 1 && msg.bodySection[0] == "HEADER") {
+ // Handle message headers.
+ this._messageUids[msg.sequence] = msg.uid;
+ this._messages.set(msg.uid, msg);
+ this._folderSink.StartMessage(this.runningUri);
+ let hdrXferInfo = {
+ numHeaders: 1,
+ getHeader() {
+ return {
+ msgUid: msg.uid,
+ msgSize: msg.size,
+ get msgHdrs() {
+ let sepIndex = msg.body.indexOf("\r\n\r\n");
+ return sepIndex == -1
+ ? msg.body + "\r\n"
+ : msg.body.slice(0, sepIndex + 2);
+ },
+ };
+ },
+ };
+ this._folderSink.parseMsgHdrs(this, hdrXferInfo);
+ } else {
+ // Handle message body.
+ let shouldStoreMsgOffline = false;
+ try {
+ shouldStoreMsgOffline = this.folder.shouldStoreMsgOffline(msg.uid);
+ } catch (e) {}
+ if (
+ (shouldStoreMsgOffline ||
+ this.runningUri.QueryInterface(Ci.nsIImapUrl)
+ .storeResultsOffline) &&
+ msg.body
+ ) {
+ this._folderSink.StartMessage(this.runningUri);
+ this._msgSink.parseAdoptedMsgLine(msg.body, msg.uid, this.runningUri);
+ this._msgSink.normalEndMsgWriteStream(
+ msg.uid,
+ true,
+ this.runningUri,
+ msg.body.length
+ );
+ this._folderSink.EndMessage(this.runningUri, msg.uid);
+ }
+
+ this.onData?.(msg.body);
+ // Release some memory.
+ msg.body = "";
+ }
+ }
+ };
+
+ /**
+ * Send NOOP command.
+ */
+ _actionNoop() {
+ this._nextAction = this._actionNoopResponse;
+ this._sendTagged("NOOP");
+ }
+
+ /**
+ * Handle NOOP response.
+ *
+ * @param {ImapResponse} res - Response received from the server.
+ */
+ _actionNoopResponse(res) {
+ if (
+ (res.exists && res.exists != this._folderState.exists) ||
+ res.expunged.length
+ ) {
+ // Handle messages number changes, re-sync the folder.
+ this._folderState.exists = res.exists;
+ this._actionAfterSelectFolder = this._actionUidFetch;
+ this._nextAction = this._actionSelectResponse();
+ if (res.expunged.length) {
+ this._messageUids = [];
+ this._messages.clear();
+ }
+ let folder = this.folder;
+ this.folder = null;
+ this.selectFolder(folder);
+ } else if (res.messages.length || res.exists) {
+ let outFolderInfo = {};
+ this.folder.getDBFolderInfoAndDB(outFolderInfo);
+ let highestUid = outFolderInfo.value.getUint32Property(
+ "highestRecordedUID",
+ 0
+ );
+ this._nextAction = this._actionUidFetchResponse;
+ this._sendTagged(`UID FETCH ${highestUid + 1}:* (FLAGS)`);
+ } else {
+ if (res.exists == 0) {
+ this._messageUids = [];
+ this._messages.clear();
+ this.folder
+ .QueryInterface(Ci.nsIImapMailFolderSink)
+ .UpdateImapMailboxInfo(this, this._getMailboxSpec());
+ }
+ if (!this._idling) {
+ this._actionDone();
+ }
+ }
+ }
+
+ /**
+ * Show an error prompt.
+ *
+ * @param {string} errorName - An error name corresponds to an entry of
+ * imapMsgs.properties.
+ */
+ _actionError(errorName) {
+ if (!this._msgWindow) {
+ return;
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/imapMsgs.properties"
+ );
+ let errorMsg = bundle.formatStringFromName(errorName, [
+ this._server.hostName,
+ ]);
+ Services.prompt.alert(this._msgWindow.domWindow, null, errorMsg);
+ }
+
+ /**
+ * Finish a request and do necessary cleanup.
+ */
+ _actionDone = (status = Cr.NS_OK) => {
+ this._logger.debug(`Done with status=${status}`);
+ this._nextAction = null;
+ this._urlListener?.OnStopRunningUrl(this.runningUri, status);
+ this.runningUri.SetUrlState(false, status);
+ this.onDone?.(status);
+ this._reset();
+ // Tell ImapIncomingServer this client can be reused now.
+ this.onFree?.();
+ };
+
+ /** @see nsIImapProtocol */
+ NotifyBodysToDownload(keys) {
+ this._logger.debug("NotifyBodysToDownload", keys);
+ this._bodysToDownload = keys;
+ }
+
+ GetRunningUrl() {
+ this._logger.debug("GetRunningUrl");
+ }
+
+ get flagAndUidState() {
+ // The server sequence is 1 based, nsIImapFlagAndUidState sequence is 0 based.
+ let getUidOfMessage = index => this._messageUids[index + 1];
+ let getMessageFlagsByUid = uid => this._messages.get(uid)?.flags;
+
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsIImapFlagAndUidState"]),
+ numberOfMessages: this._messages.size,
+ getUidOfMessage,
+ getMessageFlags: index => getMessageFlagsByUid(getUidOfMessage(index)),
+ hasMessage: uid => this._messages.has(uid),
+ getMessageFlagsByUid,
+ getCustomFlags: uid => this._messages.get(uid)?.keywords,
+ getCustomAttribute: (uid, name) => {
+ let value = this._messages.get(uid)?.customAttributes[name];
+ return Array.isArray(value) ? value.join(" ") : value;
+ },
+ };
+ }
+}
diff --git a/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs b/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs
new file mode 100644
index 0000000000..da33c08829
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+/**
+ * A service for handling content type x-application-imapfolder;
+ * that is, opening IMAP folder URLs.
+ *
+ * Set mailnews.imap.jsmodule to true to use this module.
+ *
+ * @implements {nsIContentHandler}
+ */
+export class ImapFolderContentHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIContentHandler"]);
+
+ /**
+ * @param contentType - The content type of request.
+ * @param windowContest - Window context, used to get things like the current
+ * nsIDOMWindow for this request.
+ * @param request - A request whose content type is already known.
+ * @see {nsIContentHandler}
+ */
+ handleContent(contentType, windowContext, request) {
+ if (contentType != "x-application-imapfolder") {
+ throw Components.Exception(
+ `Won't handle ${contentType}`,
+ Cr.NS_ERROR_WONT_HANDLE_CONTENT
+ );
+ }
+ request = request.QueryInterface(Ci.nsIChannel);
+
+ let imapFolderURL = Services.io.unescapeString(
+ request.URI.spec,
+ Ci.nsINetUtil.ESCAPE_URL_PATH
+ );
+
+ if (Services.wm.getMostRecentWindow("mail:3pane")) {
+ // Clicked IMAP folder URL in the window.
+ let folder = MailServices.folderLookup.getFolderForURL(imapFolderURL);
+ if (folder) {
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ } else {
+ folder =
+ MailServices.folderLookup.getOrCreateFolderForURL(imapFolderURL);
+ // TODO: ask and maybe subscribe, like
+ // https://searchfox.org/comm-central/rev/1dd06be9d6c1178a34e6c28db03161e07e97d98c/mailnews/imap/src/nsImapService.cpp#2471-2534
+ dump(`Maybe subscribe to folder ${folder.URI}\n`);
+ }
+ } else {
+ // Got IMAP folder URL from command line (most likely).
+ Cc["@mozilla.org/messenger/windowservice;1"]
+ .getService(Ci.nsIMessengerWindowService)
+ .openMessengerWindowWithUri("mail:3pane", imapFolderURL, -1);
+ }
+ }
+}
+
+ImapFolderContentHandler.prototype.classID = Components.ID(
+ "{d927a82f-2d15-4972-ab88-6d84601aae68}"
+);
diff --git a/comm/mailnews/imap/src/ImapIncomingServer.jsm b/comm/mailnews/imap/src/ImapIncomingServer.jsm
new file mode 100644
index 0000000000..c9cf86bfdc
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapIncomingServer.jsm
@@ -0,0 +1,783 @@
+/* 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 = ["ImapIncomingServer"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { MsgIncomingServer } = ChromeUtils.import(
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ ImapClient: "resource:///modules/ImapClient.jsm",
+ ImapCapFlags: "resource:///modules/ImapUtils.jsm",
+ ImapUtils: "resource:///modules/ImapUtils.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+/**
+ * @implements {nsIImapServerSink}
+ * @implements {nsIImapIncomingServer}
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsIUrlListener}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsISubscribableServer}
+ */
+class ImapIncomingServer extends MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIImapServerSink",
+ "nsIImapIncomingServer",
+ "nsIMsgIncomingServer",
+ "nsIUrlListener",
+ "nsISupportsWeakReference",
+ "nsISubscribableServer",
+ ]);
+
+ _logger = lazy.ImapUtils.logger;
+
+ constructor() {
+ super();
+
+ this._userAuthenticated = false;
+
+ // nsIMsgIncomingServer attributes.
+ this.localStoreType = "imap";
+ this.localDatabaseType = "imap";
+ this.canBeDefaultServer = true;
+ this.canSearchMessages = true;
+
+ // nsISubscribableServer attributes.
+ this.supportsSubscribeSearch = false;
+
+ // nsIImapIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Bool", "allowUTF8Accept", "allow_utf8_accept"],
+ ["Bool", "autoSyncOfflineStores", "autosync_offline_stores"],
+ ["Bool", "checkAllFoldersForNew", "check_all_folders_for_new"],
+ ["Bool", "cleanupInboxOnExit", "cleanup_inbox_on_exit"],
+ ["Bool", "downloadBodiesOnGetNewMail", "download_bodies_on_get_new_mail"],
+ ["Bool", "dualUseFolders", "dual_use_folders"],
+ ["Bool", "fetchByChunks", "fetch_by_chunks"],
+ ["Bool", "forceSelect", "force_select_imap"],
+ ["Bool", "isGMailServer", "is_gmail"],
+ ["Bool", "offlineDownload", "offline_download"],
+ ["Bool", "sendID", "send_client_info"],
+ ["Bool", "useCompressDeflate", "use_compress_deflate"],
+ ["Bool", "useCondStore", "use_condstore"],
+ ["Bool", "useIdle", "use_idle"],
+ ["Char", "adminUrl", "admin_url"],
+ ["Char", "otherUsersNamespace", "namespace.other_users"],
+ ["Char", "personalNamespace", "namespace.personal"],
+ ["Char", "publicNamespace", "namespace.public"],
+ ["Char", "serverIDPref", "serverIDResponse"],
+ ["Int", "autoSyncMaxAgeDays", "autosync_max_age_days"],
+ ["Int", "timeOutLimits", "timeout"],
+ ]);
+ }
+
+ /**
+ * Most of nsISubscribableServer interfaces are delegated to
+ * this._subscribable.
+ */
+ get _subscribable() {
+ if (!this._subscribableServer) {
+ this._subscribableServer = Cc[
+ "@mozilla.org/messenger/subscribableserver;1"
+ ].createInstance(Ci.nsISubscribableServer);
+ this._subscribableServer.setIncomingServer(this);
+ }
+ return this._subscribableServer;
+ }
+
+ /** @see nsISubscribableServer */
+ get folderView() {
+ return this._subscribable.folderView;
+ }
+
+ get subscribeListener() {
+ return this._subscribable.subscribeListener;
+ }
+
+ set subscribeListener(value) {
+ this._subscribable.subscribeListener = value;
+ }
+
+ set delimiter(value) {
+ this._subscribable.delimiter = value;
+ }
+
+ subscribeCleanup() {
+ this._subscribableServer = null;
+ }
+
+ startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._loadingInSubscribeDialog = true;
+ this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew);
+ this.delimiter = "/";
+ this.setShowFullName(false);
+ MailServices.imap.getListOfFoldersOnServer(this, msgWindow);
+ }
+
+ stopPopulating(msgWindow) {
+ this._loadingInSubscribeDialog = false;
+ this._subscribable.stopPopulating(msgWindow);
+ }
+
+ addTo(name, addAsSubscribed, subscribable, changeIfExists) {
+ this._subscribable.addTo(
+ name,
+ addAsSubscribed,
+ subscribable,
+ changeIfExists
+ );
+ }
+
+ subscribe(name) {
+ this.subscribeToFolder(name, true);
+ }
+
+ unsubscribe(name) {
+ this.subscribeToFolder(name, false);
+ }
+
+ commitSubscribeChanges() {
+ this.performExpand();
+ }
+
+ setAsSubscribed(path) {
+ this._subscribable.setAsSubscribed(path);
+ }
+
+ updateSubscribed() {}
+
+ setState(path, state) {
+ return this._subscribable.setState(path, state);
+ }
+
+ setShowFullName(showFullName) {
+ this._subscribable.setShowFullName(showFullName);
+ }
+
+ hasChildren(path) {
+ return this._subscribable.hasChildren(path);
+ }
+
+ isSubscribed(path) {
+ return this._subscribable.isSubscribed(path);
+ }
+
+ isSubscribable(path) {
+ return this._subscribable.isSubscribable(path);
+ }
+
+ getLeafName(path) {
+ return this._subscribable.getLeafName(path);
+ }
+
+ getFirstChildURI(path) {
+ return this._subscribable.getFirstChildURI(path);
+ }
+
+ getChildURIs(path) {
+ return this._subscribable.getChildURIs(path);
+ }
+
+ /** @see nsIUrlListener */
+ OnStartRunningUrl() {}
+
+ OnStopRunningUrl(url, exitCode) {
+ switch (url.QueryInterface(Ci.nsIImapUrl).imapAction) {
+ case Ci.nsIImapUrl.nsImapDiscoverAllAndSubscribedBoxesUrl:
+ this.stopPopulating();
+ break;
+ }
+ }
+
+ /** @see nsIMsgIncomingServer */
+ get serverRequiresPasswordForBiff() {
+ return !this._userAuthenticated;
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ const OFFLINE_SUPPORT_LEVEL_REGULAR = 10;
+ let level = this.getIntValue("offline_support_level");
+ return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? level
+ : OFFLINE_SUPPORT_LEVEL_REGULAR;
+ }
+
+ get constructedPrettyName() {
+ let identity = MailServices.accounts.getFirstIdentityForServer(this);
+ let email;
+ if (identity) {
+ email = identity.email;
+ } else {
+ email = `${this.username}`;
+ if (this.hostName) {
+ email += `@${this.hostName}`;
+ }
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/imapMsgs.properties"
+ );
+ return bundle.formatStringFromName("imapDefaultAccountName", [email]);
+ }
+
+ performBiff(msgWindow) {
+ this.performExpand(msgWindow);
+ }
+
+ performExpand(msgWindow) {
+ this._setFolderToUnverified();
+ this.hasDiscoveredFolders = false;
+ MailServices.imap.discoverAllFolders(this.rootFolder, this, msgWindow);
+ }
+
+ /**
+ * Recursively set a folder and its subFolders to unverified state.
+ *
+ * @param {nsIMsgFolder} folder - The folder to operate on.
+ */
+ _setFolderToUnverified(folder) {
+ if (!folder) {
+ this._setFolderToUnverified(this.rootFolder);
+ return;
+ }
+
+ folder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).verifiedAsOnlineFolder = false;
+ for (let child of folder.subFolders) {
+ this._setFolderToUnverified(child);
+ }
+ }
+
+ closeCachedConnections() {
+ // Close all connections.
+ for (let client of this._connections) {
+ client.logout();
+ }
+ // Cancel all waitings in queue.
+ for (let resolve of this._connectionWaitingQueue) {
+ resolve(false);
+ }
+ this._connections = [];
+ }
+
+ verifyLogon(urlListener, msgWindow) {
+ return MailServices.imap.verifyLogon(
+ this.rootFolder,
+ urlListener,
+ msgWindow
+ );
+ }
+
+ subscribeToFolder(name, subscribe) {
+ let folder = this.rootMsgFolder.findSubFolder(name);
+ if (subscribe) {
+ return MailServices.imap.subscribeFolder(folder, name, null);
+ }
+ return MailServices.imap.unsubscribeFolder(folder, name, null);
+ }
+
+ /** @see nsIImapServerSink */
+
+ /** @type {boolean} - User has authenticated with the server. */
+ get userAuthenticated() {
+ return this._userAuthenticated;
+ }
+
+ set userAuthenticated(value) {
+ this._userAuthenticated = value;
+ if (value) {
+ MailServices.accounts.userNeedsToAuthenticate = false;
+ }
+ }
+
+ possibleImapMailbox(folderPath, delimiter, boxFlags) {
+ let explicitlyVerify = false;
+
+ if (folderPath.endsWith("/")) {
+ folderPath = folderPath.slice(0, -1);
+ if (!folderPath) {
+ throw Components.Exception(
+ "Empty folder path",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+ explicitlyVerify = !(boxFlags & lazy.ImapUtils.FLAG_NAMESPACE);
+ }
+
+ if (this.hasDiscoveredFolders && this._loadingInSubscribeDialog) {
+ // Populate the subscribe dialog.
+ let noSelect = boxFlags & lazy.ImapUtils.FLAG_NO_SELECT;
+ this.addTo(
+ folderPath,
+ this.doingLsub && !noSelect,
+ !noSelect,
+ this.doingLsub
+ );
+ return false;
+ }
+
+ let slashIndex = folderPath.indexOf("/");
+ let token = folderPath;
+ let rest = "";
+ if (slashIndex > 0) {
+ token = folderPath.slice(0, slashIndex);
+ rest = folderPath.slice(slashIndex);
+ }
+
+ folderPath = (/^inbox/i.test(token) ? "INBOX" : token) + rest;
+
+ let uri = this.rootFolder.URI;
+ let parentName = folderPath;
+ let parentUri = uri;
+ let hasParent = false;
+ let lastSlashIndex = folderPath.lastIndexOf("/");
+ if (lastSlashIndex > 0) {
+ parentName = parentName.slice(0, lastSlashIndex);
+ hasParent = true;
+ parentUri += "/" + parentName;
+ }
+
+ if (/^inbox/i.test(folderPath) && delimiter == "|") {
+ delimiter = "/";
+ this.rootFolder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).hierarchyDelimiter = delimiter;
+ }
+
+ uri += "/" + folderPath;
+ let child = this.rootFolder.getChildWithURI(
+ uri,
+ true,
+ /^inbox/i.test(folderPath)
+ );
+
+ let isNewFolder = !child;
+ if (isNewFolder) {
+ if (hasParent) {
+ let parent = this.rootFolder.getChildWithURI(
+ parentUri,
+ true,
+ /^inbox/i.test(parentName)
+ );
+
+ if (!parent) {
+ this.possibleImapMailbox(
+ parentName,
+ delimiter,
+ lazy.ImapUtils.FLAG_NO_SELECT |
+ (boxFlags &
+ (lazy.ImapUtils.FLAG_PUBLIC_MAILBOX |
+ lazy.ImapUtils.FLAG_OTHER_USERS_MAILBOX |
+ lazy.ImapUtils.FLAG_PERSONAL_MAILBOX))
+ );
+ }
+ }
+ this.rootFolder
+ .QueryInterface(Ci.nsIMsgImapMailFolder)
+ .createClientSubfolderInfo(folderPath, delimiter, boxFlags, false);
+ child = this.rootFolder.getChildWithURI(
+ uri,
+ true,
+ /^inbox/i.test(folderPath)
+ );
+ }
+ if (child) {
+ let imapFolder = child.QueryInterface(Ci.nsIMsgImapMailFolder);
+ imapFolder.verifiedAsOnlineFolder = true;
+ imapFolder.hierarchyDelimiter = delimiter;
+ if (boxFlags & lazy.ImapUtils.FLAG_IMAP_TRASH) {
+ if (this.deleteModel == Ci.nsMsgImapDeleteModels.MoveToTrash) {
+ child.setFlag(Ci.nsMsgFolderFlags.Trash);
+ }
+ }
+ imapFolder.boxFlags = boxFlags;
+ imapFolder.explicitlyVerify = explicitlyVerify;
+ let onlineName = imapFolder.onlineName;
+ folderPath = folderPath.replaceAll("/", delimiter);
+ if (delimiter != "/") {
+ folderPath = decodeURIComponent(folderPath);
+ }
+
+ if (boxFlags & lazy.ImapUtils.FLAG_IMAP_INBOX) {
+ // GMail gives us a localized name for the inbox but doesn't let
+ // us select that localized name.
+ imapFolder.onlineName = "INBOX";
+ } else if (!onlineName || onlineName != folderPath) {
+ imapFolder.onlineName = folderPath;
+ }
+
+ child.prettyName = imapFolder.name;
+ if (isNewFolder) {
+ // Close the db so we don't hold open all the .msf files for new folders.
+ child.msgDatabase = null;
+ }
+ }
+
+ return isNewFolder;
+ }
+
+ discoveryDone() {
+ this.hasDiscoveredFolders = true;
+ // No need to verify the root.
+ this.rootFolder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).verifiedAsOnlineFolder = true;
+ let unverified = this._getUnverifiedFolders(this.rootFolder);
+ this._logger.debug(
+ `discoveryDone, unverified folders count=${unverified.length}.`
+ );
+ for (let folder of unverified) {
+ if (folder.flags & Ci.nsMsgFolderFlags.Virtual) {
+ // Do not remove virtual folders.
+ continue;
+ }
+ let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (
+ !this.usingSubscription ||
+ imapFolder.explicitlyVerify ||
+ (folder.hasSubFolders && this._noDescendentsAreVerified(folder))
+ ) {
+ imapFolder.explicitlyVerify = false;
+ imapFolder.list();
+ } else if (folder.parent) {
+ imapFolder.removeLocalSelf();
+ this._logger.debug(`Removed unverified folder name=${folder.name}`);
+ }
+ }
+ }
+
+ /**
+ * Find local folders that do not exist on the server.
+ *
+ * @param {nsIMsgFolder} parentFolder - The folder to check.
+ * @returns {nsIMsgFolder[]}
+ */
+ _getUnverifiedFolders(parentFolder) {
+ let folders = [];
+ let imapFolder = parentFolder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (!imapFolder.verifiedAsOnlineFolder || imapFolder.explicitlyVerify) {
+ folders.push(imapFolder);
+ }
+ for (let folder of parentFolder.subFolders) {
+ folders.push(...this._getUnverifiedFolders(folder));
+ }
+ return folders;
+ }
+
+ /**
+ * Returns true if all sub folders are unverified.
+ *
+ * @param {nsIMsgFolder} parentFolder - The folder to check.
+ * @returns {nsIMsgFolder[]}
+ */
+ _noDescendentsAreVerified(parentFolder) {
+ for (let folder of parentFolder.subFolders) {
+ let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (
+ imapFolder.verifiedAsOnlineFolder ||
+ !this._noDescendentsAreVerified(folder)
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ onlineFolderRename(msgWindow, oldName, newName) {
+ let folder = this._getFolder(oldName).QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ );
+ let index = newName.lastIndexOf("/");
+ let parent =
+ index > 0 ? this._getFolder(newName.slice(0, index)) : this.rootFolder;
+ folder.renameLocal(newName, parent);
+ if (parent instanceof Ci.nsIMsgImapMailFolder) {
+ try {
+ parent.renameClient(msgWindow, folder, oldName, newName);
+ } catch (e) {
+ this._logger.error("renameClient failed", e);
+ }
+ }
+
+ this._getFolder(newName).NotifyFolderEvent("RenameCompleted");
+ }
+
+ /**
+ * Given a canonical folder name, returns the corresponding msg folder.
+ *
+ * @param {string} name - The canonical folder name, e.g. a/b/c.
+ * @returns {nsIMsgFolder} The corresponding msg folder.
+ */
+ _getFolder(name) {
+ return lazy.MailUtils.getOrCreateFolder(this.rootFolder.URI + "/" + name);
+ }
+
+ abortQueuedUrls() {}
+
+ setCapability(capabilityFlags) {
+ this._capabilityFlags = capabilityFlags;
+ if (capabilityFlags & lazy.ImapCapFlags.Gmail) {
+ this.isGMailServer = true;
+ }
+ }
+
+ /** @see nsIImapIncomingServer */
+ getCapability() {
+ return this._capabilityFlags;
+ }
+
+ get deleteModel() {
+ return this.getIntValue("delete_model");
+ }
+
+ set deleteModel(value) {
+ this.setIntValue("delete_model", value);
+ let trashFolder = this._getFolder(this.trashFolderName);
+ if (trashFolder) {
+ if (value == Ci.nsMsgImapDeleteModels.MoveToTrash) {
+ trashFolder.setFlag(Ci.nsMsgFolderFlags.Trash);
+ } else {
+ trashFolder.clearFlag(Ci.nsMsgFolderFlags.Trash);
+ }
+ }
+ }
+
+ get usingSubscription() {
+ return this.getBoolValue("using_subscription");
+ }
+
+ get trashFolderName() {
+ return this.getUnicharValue("trash_folder_name") || "Trash";
+ }
+
+ get maximumConnectionsNumber() {
+ let maxConnections = this.getIntValue("max_cached_connections", 0);
+ if (maxConnections > 0) {
+ return maxConnections;
+ }
+ // The default is 5 connections, if the pref value is 0, we use the default.
+ // If it's negative, treat it as 1.
+ maxConnections = maxConnections == 0 ? 5 : 1;
+ this.maximumConnectionsNumber = maxConnections;
+ return maxConnections;
+ }
+
+ set maximumConnectionsNumber(value) {
+ this.setIntValue("max_cached_connections", value);
+ }
+
+ GetNewMessagesForNonInboxFolders(
+ folder,
+ msgWindow,
+ forceAllFolders,
+ performingBiff
+ ) {
+ let flags = folder.flags;
+
+ if (
+ folder.QueryInterface(Ci.nsIMsgImapMailFolder).canOpenFolder &&
+ ((forceAllFolders &&
+ !(
+ flags &
+ (Ci.nsMsgFolderFlags.Inbox |
+ Ci.nsMsgFolderFlags.Trash |
+ Ci.nsMsgFolderFlags.Junk |
+ Ci.nsMsgFolderFlags.Virtual)
+ )) ||
+ flags & Ci.nsMsgFolderFlags.CheckNew)
+ ) {
+ folder.gettingNewMessages = true;
+ if (performingBiff) {
+ folder.performingBiff = true;
+ }
+ }
+
+ if (
+ Services.prefs.getBoolPref("mail.imap.use_status_for_biff", false) &&
+ !MailServices.mailSession.IsFolderOpenInWindow(folder)
+ ) {
+ folder.updateStatus(this, msgWindow);
+ } else {
+ folder.updateFolder(msgWindow);
+ }
+
+ for (let subFolder of folder.subFolders) {
+ this.GetNewMessagesForNonInboxFolders(
+ subFolder,
+ msgWindow,
+ forceAllFolders,
+ performingBiff
+ );
+ }
+ }
+
+ CloseConnectionForFolder(folder) {
+ for (let client of this._connections) {
+ if (client.folder == folder) {
+ client.logout();
+ }
+ }
+ }
+
+ _capabilities = [];
+
+ set capabilities(value) {
+ this._capabilities = value;
+ this.setCapability(lazy.ImapCapFlags.stringsToFlags(value));
+ }
+
+ // @type {ImapClient[]} - An array of connections.
+ _connections = [];
+ // @type {Function[]} - An array of Promise.resolve functions.
+ _connectionWaitingQueue = [];
+
+ /**
+ * Wait for a free connection.
+ *
+ * @param {nsIMsgFolder} folder - The folder to operate on.
+ * @returns {ImapClient}
+ */
+ async _waitForNextClient(folder) {
+ // Wait until a connection is available. canGetNext is false when
+ // closeCachedConnections is called.
+ let canGetNext = await new Promise(resolve =>
+ this._connectionWaitingQueue.push(resolve)
+ );
+ if (canGetNext) {
+ return this._getNextClient(folder);
+ }
+ return null;
+ }
+
+ /**
+ * Check if INBOX folder is selected in a connection.
+ *
+ * @param {ImapClient} client - The client to check.
+ * @returns {boolean}
+ */
+ _isInboxConnection(client) {
+ return client.folder?.onlineName.toUpperCase() == "INBOX";
+ }
+
+ /**
+ * Get a free connection that can be used.
+ *
+ * @param {nsIMsgFolder} folder - The folder to operate on.
+ * @returns {ImapClient}
+ */
+ async _getNextClient(folder) {
+ let client;
+
+ for (client of this._connections) {
+ if (folder && client.folder == folder) {
+ if (client.busy) {
+ // Prevent operating on the same folder in two connections.
+ return this._waitForNextClient(folder);
+ }
+ // If we're idling in the target folder, reuse it.
+ client.busy = true;
+ return client;
+ }
+ }
+
+ // Create a new client if the pool is not full.
+ if (this._connections.length < this.maximumConnectionsNumber) {
+ client = new lazy.ImapClient(this);
+ this._connections.push(client);
+ client.busy = true;
+ return client;
+ }
+
+ let freeConnections = this._connections.filter(c => !c.busy);
+
+ // Wait if no free connection.
+ if (!freeConnections.length) {
+ return this._waitForNextClient(folder);
+ }
+
+ // Reuse any free connection if only have one connection or IDLE not used.
+ if (
+ this.maximumConnectionsNumber <= 1 ||
+ !this.useIdle ||
+ !this._capabilities.includes("IDLE")
+ ) {
+ freeConnections[0].busy = true;
+ return freeConnections[0];
+ }
+
+ // Reuse non-inbox free connection.
+ client = freeConnections.find(c => !this._isInboxConnection(c));
+ if (client) {
+ client.busy = true;
+ return client;
+ }
+ return this._waitForNextClient(folder);
+ }
+
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {nsIMsgFolder} folder - The folder to operate on.
+ * @param {Function} handler - A callback function to take a ImapClient
+ * instance, and do some actions.
+ */
+ async withClient(folder, handler) {
+ let client = await this._getNextClient(folder);
+ if (!client) {
+ return;
+ }
+ let startIdle = async () => {
+ if (!this.useIdle || !this._capabilities.includes("IDLE")) {
+ return;
+ }
+
+ // IDLE is configed and supported, use IDLE to receive server pushes.
+ let hasInboxConnection = this._connections.some(c =>
+ this._isInboxConnection(c)
+ );
+ let alreadyIdling =
+ client.folder &&
+ this._connections.find(
+ c => c != client && !c.busy && c.folder == client.folder
+ );
+ if (!hasInboxConnection) {
+ client.selectFolder(
+ this.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox)
+ );
+ } else if (client.folder && !alreadyIdling) {
+ client.idle();
+ } else if (alreadyIdling) {
+ client.folder = null;
+ }
+ };
+ client.onFree = () => {
+ client.busy = false;
+ let resolve = this._connectionWaitingQueue.shift();
+ if (resolve) {
+ // Resolve the first waiting in queue.
+ resolve(true);
+ } else if (client.isOnline) {
+ startIdle();
+ }
+ };
+ handler(client);
+ client.connect();
+ }
+}
+
+ImapIncomingServer.prototype.classID = Components.ID(
+ "{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}"
+);
diff --git a/comm/mailnews/imap/src/ImapMessageService.jsm b/comm/mailnews/imap/src/ImapMessageService.jsm
new file mode 100644
index 0000000000..e88e0f8090
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapMessageService.jsm
@@ -0,0 +1,292 @@
+/* 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 = ["ImapMessageService", "ImapMessageMessageService"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+});
+
+/**
+ * @implements {nsIMsgMessageService}
+ */
+class BaseMessageService {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgMessageService"]);
+
+ _logger = ImapUtils.logger;
+
+ copyMessage(messageUri, copyListener, moveMessage, urlListener, msgWindow) {
+ this._logger.debug("copyMessage", messageUri, moveMessage);
+ let { serverURI, folder, folderName, key } =
+ this._decomposeMessageUri(messageUri);
+ let imapUrl = Services.io
+ .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`)
+ .QueryInterface(Ci.nsIImapUrl);
+
+ if (urlListener) {
+ imapUrl
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .RegisterListener(urlListener);
+ }
+
+ return MailServices.imap.fetchMessage(
+ imapUrl,
+ moveMessage
+ ? Ci.nsIImapUrl.nsImapOnlineToOfflineMove
+ : Ci.nsIImapUrl.nsImapOnlineToOfflineCopy,
+ folder,
+ folder.QueryInterface(Ci.nsIImapMessageSink),
+ msgWindow,
+ copyListener,
+ key,
+ false,
+ {}
+ );
+ }
+
+ loadMessage(
+ messageUri,
+ displayConsumer,
+ msgWindow,
+ urlListener,
+ autodetectCharset
+ ) {
+ this._logger.debug("loadMessage", messageUri);
+ let { serverURI, folder, folderName, key } =
+ this._decomposeMessageUri(messageUri);
+ let imapUrl = Services.io
+ .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`)
+ .QueryInterface(Ci.nsIImapUrl);
+
+ let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ if (urlListener) {
+ mailnewsUrl.RegisterListener(urlListener);
+ }
+
+ return MailServices.imap.fetchMessage(
+ imapUrl,
+ Ci.nsIImapUrl.nsImapMsgFetch,
+ folder,
+ folder.QueryInterface(Ci.nsIImapMessageSink),
+ msgWindow,
+ displayConsumer,
+ key,
+ false,
+ {}
+ );
+ }
+
+ SaveMessageToDisk(
+ messageUri,
+ file,
+ addDummyEnvelope,
+ urlListener,
+ outUrl,
+ canonicalLineEnding,
+ msgWindow
+ ) {
+ this._logger.debug("SaveMessageToDisk", messageUri);
+ let { serverURI, folder, folderName, key } =
+ this._decomposeMessageUri(messageUri);
+ let imapUrl = Services.io
+ .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`)
+ .QueryInterface(Ci.nsIImapUrl);
+
+ let msgUrl = imapUrl.QueryInterface(Ci.nsIMsgMessageUrl);
+ msgUrl.messageFile = file;
+ msgUrl.AddDummyEnvelope = addDummyEnvelope;
+ msgUrl.canonicalLineEnding = canonicalLineEnding;
+ let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ mailnewsUrl.RegisterListener(urlListener);
+ mailnewsUrl.msgIsInLocalCache = folder.hasMsgOffline(key, null, 10);
+
+ return MailServices.imap.fetchMessage(
+ imapUrl,
+ Ci.nsIImapUrl.nsImapSaveMessageToDisk,
+ folder,
+ folder.QueryInterface(Ci.nsIImapMessageSink),
+ msgWindow,
+ mailnewsUrl.getSaveAsListener(addDummyEnvelope, file),
+ key,
+ false,
+ {}
+ );
+ }
+
+ getUrlForUri(messageUri, msgWindow) {
+ if (messageUri.includes("&type=application/x-message-display")) {
+ return Services.io.newURI(messageUri);
+ }
+
+ let { serverURI, folder, folderName, key } =
+ this._decomposeMessageUri(messageUri);
+ let delimiter =
+ folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/";
+ let imapUrl = Services.io
+ .newURI(
+ `${serverURI}:${folder.server.port}/fetch>UID>${delimiter}${folderName}>${key}`
+ )
+ .QueryInterface(Ci.nsIImapUrl);
+
+ return imapUrl;
+ }
+
+ streamMessage(
+ messageUri,
+ consumer,
+ msgWindow,
+ urlListener,
+ convertData,
+ additionalHeader,
+ localOnly
+ ) {
+ this._logger.debug("streamMessage", messageUri);
+ let { serverURI, folder, folderName, key } =
+ this._decomposeMessageUri(messageUri);
+ let url = `${serverURI}/fetch>UID>/${folderName}>${key}`;
+ if (additionalHeader) {
+ url += `?header=${additionalHeader}`;
+ }
+ let imapUrl = Services.io.newURI(url).QueryInterface(Ci.nsIImapUrl);
+ imapUrl.localFetchOnly = localOnly;
+
+ let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ mailnewsUrl.folder = folder;
+ mailnewsUrl.msgWindow = msgWindow;
+ mailnewsUrl.msgIsInLocalCache = folder.hasMsgOffline(key);
+ if (urlListener) {
+ mailnewsUrl.RegisterListener(urlListener);
+ }
+
+ return MailServices.imap.fetchMessage(
+ imapUrl,
+ Ci.nsIImapUrl.nsImapMsgFetchPeek,
+ folder,
+ folder.QueryInterface(Ci.nsIImapMessageSink),
+ msgWindow,
+ consumer,
+ key,
+ convertData,
+ {}
+ );
+ }
+
+ streamHeaders(messageUri, consumer, urlListener, localOnly) {
+ this._logger.debug("streamHeaders", messageUri);
+ let { folder, key } = this._decomposeMessageUri(messageUri);
+
+ let hasMsgOffline = folder.hasMsgOffline(key);
+ if (!hasMsgOffline) {
+ return;
+ }
+
+ let localMsgStream = folder.getLocalMsgStream(folder.GetMessageHeader(key));
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sstream.init(localMsgStream);
+ let headers = "";
+ let str = "";
+ do {
+ str = sstream.read(4096);
+ let index = str.indexOf("\r\n\r\n");
+ if (index != -1) {
+ headers += str.slice(0, index) + "\r\n";
+ break;
+ } else {
+ headers += str;
+ }
+ } while (str.length);
+
+ let headersStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ headersStream.setData(headers, headers.length);
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ pump.init(headersStream, 0, 0, true);
+ pump.asyncRead(consumer);
+ }
+
+ /**
+ * Go from message uri to go nsIMsgDBHdr.
+ *
+ * @param {string} uri - A message uri to get the nsIMsgDBHdr for.
+ * @returns {?nsIMsgDBHdr} Hdr for the uri, or or null if failed.
+ */
+ messageURIToMsgHdr(uri) {
+ try {
+ let { folder, key } = this._decomposeMessageUri(uri);
+ return folder.GetMessageHeader(key);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ Search(searchSession, msgWindow, folder, searchUri) {
+ let server = folder.server.QueryInterface(Ci.nsIMsgIncomingServer);
+ server.wrappedJSObject.withClient(folder, client => {
+ client.startRunningUrl(
+ searchSession.QueryInterface(Ci.nsIUrlListener),
+ msgWindow
+ );
+ client.onReady = () => {
+ client.search(folder, searchUri);
+ };
+ client.onData = uids => {
+ for (let uid of uids) {
+ let msgHdr = folder.msgDatabase.getMsgHdrForKey(uid);
+ searchSession.runningAdapter.AddResultElement(msgHdr);
+ }
+ };
+ });
+ }
+
+ /**
+ * Parse a message uri to hostname, folder and message key.
+ *
+ * @param {string} uri - The imap-message:// url to parse.
+ * @returns {serverURI: string, folder: nsIMsgFolder, folderName: string, key: string}
+ */
+ _decomposeMessageUri(messageUri) {
+ let matches = /imap-message:\/\/([^:/]+)\/(.+)#(\d+)/.exec(messageUri);
+ if (!matches) {
+ throw new Error(`Unexpected IMAP URL: ${messageUri}`);
+ }
+ let [, host, folderName, key] = matches;
+ let folder = lazy.MailUtils.getOrCreateFolder(
+ `imap://${host}/${folderName}`
+ );
+ return { serverURI: folder.server.serverURI, folder, folderName, key };
+ }
+}
+
+/**
+ * A message service for imap://.
+ */
+class ImapMessageService extends BaseMessageService {}
+
+ImapMessageService.prototype.classID = Components.ID(
+ "{d63af753-c2f3-4f1d-b650-9d12229de8ad}"
+);
+
+/**
+ * A message service for imap-message://.
+ */
+class ImapMessageMessageService extends BaseMessageService {}
+
+ImapMessageMessageService.prototype.classID = Components.ID(
+ "{2532ae4f-a852-4c96-be45-1308ba23d62e}"
+);
diff --git a/comm/mailnews/imap/src/ImapModuleLoader.jsm b/comm/mailnews/imap/src/ImapModuleLoader.jsm
new file mode 100644
index 0000000000..427fe2a3f2
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapModuleLoader.jsm
@@ -0,0 +1,131 @@
+/* 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 = ["ImapModuleLoader"];
+
+/**
+ * Determine whether to use nsImapService.cpp or ImapService.jsm. When
+ * `mailnews.imap.jsmodule` is `true`, use ImapService.jsm.
+ */
+function ImapModuleLoader() {
+ try {
+ this.loadModule();
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+var imapJSModules = [
+ // moduleName, interfaceId, contractId
+ [
+ "ImapIncomingServer",
+ "{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}",
+ "@mozilla.org/messenger/server;1?type=imap",
+ ],
+ [
+ "ImapService",
+ "{2ea8fbe6-029b-4bff-ae05-b794cf955afb}",
+ "@mozilla.org/messenger/imapservice;1",
+ ],
+ [
+ "ImapMessageService",
+ "{d63af753-c2f3-4f1d-b650-9d12229de8ad}",
+ "@mozilla.org/messenger/messageservice;1?type=imap",
+ "ImapMessageService",
+ ],
+ [
+ "ImapFolderContentHandler",
+ "{d927a82f-2d15-4972-ab88-6d84601aae68}",
+ "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder",
+ ],
+ [
+ "ImapMessageMessageService",
+ "{2532ae4f-a852-4c96-be45-1308ba23d62e}",
+ "@mozilla.org/messenger/messageservice;1?type=imap-message",
+ "ImapMessageService",
+ ],
+ [
+ "ImapProtocolHandler",
+ "{ebb06c58-6ccd-4bde-9087-40663e0388ae}",
+ "@mozilla.org/network/protocol;1?name=imap",
+ ],
+ [
+ "ImapProtocolInfo",
+ "{1d9473bc-423a-4632-ad5d-802154e80f6f}",
+ "@mozilla.org/messenger/protocol/info;1?type=imap",
+ ],
+];
+
+ImapModuleLoader.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe() {
+ // Nothing to do here, just need the entry so this is instantiated.
+ },
+
+ loadModule() {
+ if (Services.prefs.getBoolPref("mailnews.imap.jsmodule", false)) {
+ let registrar = Components.manager.QueryInterface(
+ Ci.nsIComponentRegistrar
+ );
+
+ for (let [
+ moduleName,
+ interfaceId,
+ contractId,
+ fileName,
+ ] of imapJSModules) {
+ // Register a module.
+ let classId = Components.ID(interfaceId);
+ registrar.registerFactory(
+ classId,
+ "",
+ contractId,
+ lazyFactoryFor(fileName || moduleName, moduleName)
+ );
+ }
+
+ dump("[ImapModuleLoader] Using ImapService.jsm\n");
+
+ const { ImapProtocolHandler } = ChromeUtils.import(
+ `resource:///modules/ImapProtocolHandler.jsm`
+ );
+ let protocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS |
+ Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC;
+
+ Services.io.registerProtocolHandler(
+ "imap",
+ new ImapProtocolHandler(),
+ protocolFlags,
+ Ci.nsIImapUrl.DEFAULT_IMAP_PORT
+ );
+ } else {
+ dump("[ImapModuleLoader] Using nsImapService.cpp\n");
+ // Ensure the imap protocol is actually registered.
+ Cc["@mozilla.org/network/protocol;1?name=imap"].getService(
+ Ci.nsIImapService
+ );
+ }
+ },
+};
+
+function lazyFactoryFor(fileName, constructorName) {
+ let factory = {
+ get scope() {
+ delete this.scope;
+ this.scope = ChromeUtils.import(`resource:///modules/${fileName}.jsm`);
+ return this.scope;
+ },
+ createInstance(interfaceID) {
+ let componentConstructor = this.scope[constructorName];
+ return new componentConstructor().QueryInterface(interfaceID);
+ },
+ };
+ return factory;
+}
diff --git a/comm/mailnews/imap/src/ImapProtocolHandler.jsm b/comm/mailnews/imap/src/ImapProtocolHandler.jsm
new file mode 100644
index 0000000000..949dca9632
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapProtocolHandler.jsm
@@ -0,0 +1,40 @@
+/* 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 EXPORTED_SYMBOLS = ["ImapProtocolHandler"];
+
+var { ImapChannel } = ChromeUtils.import("resource:///modules/ImapChannel.jsm");
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class ImapProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "imap";
+
+ newChannel(uri, loadInfo) {
+ let channel = new ImapChannel(uri, loadInfo);
+ let spec = uri.spec;
+ if (
+ spec.includes("part=") &&
+ !spec.includes("type=message/rfc822") &&
+ !spec.includes("type=application/x-message-display") &&
+ !spec.includes("type=application/pdf")
+ ) {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ } else {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE;
+ }
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return true;
+ }
+}
+
+ImapProtocolHandler.prototype.classID = Components.ID(
+ "{ebb06c58-6ccd-4bde-9087-40663e0388ae}"
+);
diff --git a/comm/mailnews/imap/src/ImapProtocolInfo.jsm b/comm/mailnews/imap/src/ImapProtocolInfo.jsm
new file mode 100644
index 0000000000..9d12175791
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapProtocolInfo.jsm
@@ -0,0 +1,44 @@
+/* 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 = ["ImapProtocolInfo"];
+
+var { MsgProtocolInfo } = ChromeUtils.importESModule(
+ "resource:///modules/MsgProtocolInfo.sys.mjs"
+);
+
+/**
+ * @implements {nsIMsgProtocolInfo}
+ */
+class ImapProtocolInfo extends MsgProtocolInfo {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]);
+
+ serverIID = Components.ID("{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}");
+
+ requiresUsername = true;
+ preflightPrettyNameWithEmailAddress = true;
+ canDelete = true;
+ canLoginAtStartUp = true;
+ canDuplicate = true;
+ canGetMessages = true;
+ canGetIncomingMessages = true;
+ defaultDoBiff = true;
+ showComposeMsgLink = true;
+ foldersCreatedAsync = true;
+
+ getDefaultServerPort(isSecure) {
+ return isSecure
+ ? Ci.nsIImapUrl.DEFAULT_IMAPS_PORT
+ : Ci.nsIImapUrl.DEFAULT_IMAP_PORT;
+ }
+
+ // @see MsgProtocolInfo.sys.mjs
+ RELATIVE_PREF = "mail.root.imap-rel";
+ ABSOLUTE_PREF = "mail.root.imap";
+ DIR_SERVICE_PROP = "IMapMD";
+}
+
+ImapProtocolInfo.prototype.classID = Components.ID(
+ "{1d9473bc-423a-4632-ad5d-802154e80f6f}"
+);
diff --git a/comm/mailnews/imap/src/ImapResponse.jsm b/comm/mailnews/imap/src/ImapResponse.jsm
new file mode 100644
index 0000000000..08d1c25916
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapResponse.jsm
@@ -0,0 +1,479 @@
+/* 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 = ["ImapResponse"];
+
+var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+
+/**
+ * A structure to represent a server response.
+ */
+class ImapResponse {
+ constructor() {
+ // @type {MailboxData[]} The mailbox-data in this response.
+ this.mailboxes = [];
+ // @type {MessageData[]} The message-data in this response.
+ this.messages = [];
+ // A holder for attributes.
+ this.attributes = {};
+ // Expunged message sequences.
+ this.expunged = [];
+
+ // The remaining string to parse.
+ this._response = "";
+
+ this.onMessage = () => {};
+ }
+
+ /**
+ * A server response can span multiple chunks, this function parses one chunk.
+ *
+ * @param {string} str - A chunk of server response.
+ */
+ parse(str) {
+ this._response += str;
+ if (this._pendingMessage) {
+ // We have an unfinished message in the last chunk.
+ let remaining =
+ this._pendingMessage.bodySize - this._pendingMessage.body.length;
+ if (remaining + ")\r\n".length <= this._response.length) {
+ // Consume the message together with the ending ")\r\n".
+ this._pendingMessage.body += this._response.slice(0, remaining);
+ this.onMessage(this._pendingMessage);
+ this._pendingMessage = null;
+ this._advance(remaining + ")\r\n".length);
+ } else {
+ this.done = false;
+ return;
+ }
+ }
+ this._parse();
+ }
+
+ /**
+ * Drop n characters from _response.
+ *
+ * @param {number} n - The number of characters to drop.
+ */
+ _advance(n) {
+ this._response = this._response.slice(n);
+ }
+
+ /**
+ * Parse the response line by line. Because a single response can contain
+ * multiple types of data, update the corresponding properties after parsing
+ * a line, e.g. this.capabilities, this.flags, this.messages.
+ */
+ _parse() {
+ if (!this._response && this.tag != "*") {
+ // Nothing more to parse.
+ this.done = true;
+ return;
+ }
+ let index = this._response.indexOf("\r\n");
+ if (index == -1) {
+ // Expect more string in the next chunk.
+ this.done = false;
+ return;
+ }
+
+ let line = this._response.slice(0, index);
+ this._advance(index + 2); // Consume the line and "\r\n".
+ let tokens = this._parseLine(line);
+ this.tag = tokens[0];
+ this.status = tokens[1];
+ if (this.tag == "+") {
+ this.statusText = tokens.slice(1).join(" ");
+ if (!this._response) {
+ this.done = true;
+ return;
+ }
+ }
+
+ let parsed;
+
+ if (this.tag == "*") {
+ parsed = true;
+ switch (tokens[1].toUpperCase()) {
+ case "CAPABILITY":
+ // * CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN
+ let { capabilities, authMethods } = new CapabilityData(
+ tokens.slice(2)
+ );
+ this.capabilities = capabilities;
+ this.authMethods = authMethods;
+ break;
+ case "FLAGS":
+ // * FLAGS (\Seen \Draft $Forwarded)
+ this.flags = ImapUtils.stringsToFlags(tokens[2]);
+ if (tokens[2].includes("\\*")) {
+ this.supportedUserFlags =
+ ImapUtils.FLAG_LABEL |
+ ImapUtils.FLAG_MDN_SENT |
+ ImapUtils.FLAG_SUPPORT_FORWARDED_FLAG |
+ ImapUtils.FLAG_SUPPORT_USER_FLAG;
+ }
+ break;
+ case "ID":
+ // * ID ("name" "imap" "vendor" "Example, Inc.")
+ this.id = line.slice("* ID ".length);
+ break;
+ case "LIST":
+ case "LSUB":
+ // * LIST (\Subscribed \NoInferiors \UnMarked \Sent) "/" Sent
+ this.mailboxes.push(new MailboxData(tokens));
+ break;
+ case "QUOTAROOT":
+ // * QUOTAROOT Sent INBOX
+ this.quotaRoots = tokens.slice(3);
+ break;
+ case "QUOTA":
+ // S: * QUOTA INBOX (STORAGE 95295 97656832)
+ if (!this.quotas) {
+ this.quotas = [];
+ }
+ this.quotas.push([tokens[2], ...tokens[3]]);
+ break;
+ case "SEARCH":
+ // * SEARCH 1 4 9
+ this.search = tokens.slice(2).map(x => Number(x));
+ break;
+ case "STATUS":
+ // * STATUS \"folder 2\" (UIDNEXT 2 MESSAGES 1 UNSEEN 1)
+ this.attributes = new StatusData(tokens).attributes;
+ break;
+ default:
+ if (Number.isInteger(+tokens[1])) {
+ this._parseNumbered(tokens);
+ } else {
+ parsed = false;
+ }
+ break;
+ }
+ }
+ if (!parsed && Array.isArray(tokens[2])) {
+ let type = tokens[2][0].toUpperCase();
+ let data = tokens[2].slice(1);
+ switch (type) {
+ case "CAPABILITY":
+ // 32 OK [CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN]
+ let { capabilities, authMethods } = new CapabilityData(data);
+ this.capabilities = capabilities;
+ this.authMethods = authMethods;
+ break;
+ case "PERMANENTFLAGS":
+ // * OK [PERMANENTFLAGS (\\Seen \\Draft $Forwarded \\*)]
+ this.permanentflags = ImapUtils.stringsToFlags(tokens[2][1]);
+ if (tokens[2][1].includes("\\*")) {
+ this.supportedUserFlags =
+ ImapUtils.FLAG_LABEL |
+ ImapUtils.FLAG_MDN_SENT |
+ ImapUtils.FLAG_SUPPORT_FORWARDED_FLAG |
+ ImapUtils.FLAG_SUPPORT_USER_FLAG;
+ }
+ break;
+ default:
+ let field = type.toLowerCase();
+ if (tokens[2].length == 1) {
+ // A boolean attribute, e.g. 12 OK [READ-WRITE]
+ this[field] = true;
+ } else if (tokens[2].length == 2) {
+ // An attribute/value pair, e.g. 12 OK [UIDNEXT 600]
+ this[field] = tokens[2][1];
+ } else {
+ // Hold other attributes.
+ this.attributes[field] = data;
+ }
+ }
+ }
+ this._parse();
+ }
+
+ /**
+ * Handle the tokens of a line in the form of "* NUM TYPE".
+ *
+ * @params {Array<string|string[]>} tokens - The tokens of the line.
+ */
+ _parseNumbered(tokens) {
+ let intValue = +tokens[1];
+ let type = tokens[2].toUpperCase();
+ switch (type) {
+ case "FETCH":
+ // * 1 FETCH (UID 5 FLAGS (\SEEN) BODY[HEADER.FIELDS (FROM TO)] {12}
+ let message = new MessageData(intValue, tokens[3]);
+ this.messages.push(message);
+ if (message.bodySize) {
+ if (message.bodySize + ")\r\n".length <= this._response.length) {
+ // Consume the message together with the ending ")\r\n".
+ message.body = this._response.slice(0, message.bodySize);
+ this.onMessage(message);
+ } else {
+ message.body = this._response;
+ this._pendingMessage = message;
+ this.done = false;
+ }
+ this._advance(message.bodySize + ")\r\n".length);
+ } else {
+ this.onMessage(message);
+ }
+ break;
+ case "EXISTS":
+ // * 6 EXISTS
+ this.exists = intValue;
+ break;
+ case "EXPUNGE":
+ // * 2 EXPUNGE
+ this.expunged.push(intValue);
+ break;
+ case "RECENT":
+ // Deprecated in rfc9051.
+ break;
+ default:
+ throw Components.Exception(
+ `Unrecognized response: ${tokens.join(" ")}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ }
+
+ /**
+ * Break a line into flat tokens array. For example,
+ * "(UID 24 FLAGS (NonJunk))" will be tokenized to
+ * ["(", "UID", "24", "FLAGS", "(", "NonJunk", ")", ")"].
+ *
+ * @param {string} line - A single line of string.
+ * @returns {string[]}
+ */
+ _tokenize(line) {
+ const SEPARATORS = /[()\[\]" ]/;
+ let tokens = [];
+ while (line) {
+ // Find the first separator.
+ let index = line.search(SEPARATORS);
+ if (index == -1) {
+ tokens.push(line);
+ break;
+ }
+ let sep = line[index];
+ let token = line.slice(0, index);
+ if (token) {
+ tokens.push(token);
+ }
+ if (sep == '"') {
+ // Parse the whole string as a token.
+ line = line.slice(index + 1);
+ let str = sep;
+ while (true) {
+ index = line.indexOf('"');
+ if (line[index - 1] == "\\") {
+ // Not the ending quote.
+ str += line.slice(0, index + 1);
+ line = line.slice(index + 1);
+ continue;
+ } else {
+ // The ending quote.
+ str += line.slice(0, index + 1);
+ tokens.push(str);
+ line = line.slice(index + 1);
+ break;
+ }
+ }
+ continue;
+ } else if (sep != " ") {
+ tokens.push(sep);
+ }
+ line = line.slice(index + 1);
+ }
+ return tokens;
+ }
+
+ /**
+ * Parse a line into nested tokens array. For example,
+ * "(UID 24 FLAGS (NonJunk))" will be parsed to
+ * ["UID", "24", "FLAGS", ["NonJunk"]].
+ *
+ * @param {string} line - A single line of string.
+ * @returns {Array<string|string[]>}
+ */
+ _parseLine(line) {
+ let tokens = [];
+ let arrayDepth = 0;
+
+ for (let token of this._tokenize(line)) {
+ let depth = arrayDepth;
+ let arr = tokens;
+ while (depth-- > 0) {
+ arr = arr.at(-1);
+ }
+ switch (token) {
+ case "(":
+ case "[":
+ arr.push([]);
+ arrayDepth++;
+ break;
+ case ")":
+ case "]":
+ arrayDepth--;
+ break;
+ default:
+ arr.push(token);
+ }
+ }
+
+ return tokens;
+ }
+}
+
+/**
+ * A structure to represent capability-data.
+ */
+class CapabilityData {
+ /**
+ * @param {string[]} tokens - An array like: ["IMAP4rev1", "IDLE", "STARTTLS",
+ * "AUTH=LOGIN", "AUTH=PLAIN"].
+ */
+ constructor(tokens) {
+ this.capabilities = [];
+ this.authMethods = [];
+ for (let cap of tokens) {
+ cap = cap.toUpperCase();
+ if (cap.startsWith("AUTH=")) {
+ this.authMethods.push(cap.slice(5));
+ } else {
+ this.capabilities.push(cap);
+ }
+ }
+ }
+}
+
+/**
+ * A structure to represent message-data.
+ */
+class MessageData {
+ /**
+ * @param {number} sequence - The sequence number of this message.
+ * @param {string[]} tokens - An array like: ["UID", "24", "FLAGS", ["\Seen"]].
+ */
+ constructor(sequence, tokens) {
+ this.sequence = sequence;
+ this.customAttributes = {};
+ for (let i = 0; i < tokens.length; i += 2) {
+ let name = tokens[i].toUpperCase();
+ switch (name) {
+ case "UID":
+ this.uid = +tokens[i + 1];
+ break;
+ case "FLAGS":
+ this.flags = ImapUtils.stringsToFlags(tokens[i + 1]);
+ this.keywords = tokens[i + 1]
+ .filter(x => !x.startsWith("\\"))
+ .join(" ");
+ break;
+ case "BODY": {
+ // bodySection is the part between [ and ].
+ this.bodySection = tokens[i + 1];
+ i++;
+ // {123} means the following 123 bytes are the body.
+ let matches = tokens[i + 1].match(/{(\d+)}/);
+ if (matches) {
+ this.bodySize = +matches[1];
+ this.body = "";
+ }
+ break;
+ }
+ case "RFC822.SIZE": {
+ this.size = +tokens[i + 1];
+ break;
+ }
+ default:
+ this.customAttributes[tokens[i]] = tokens[i + 1];
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * A structure to represent mailbox-data.
+ */
+class MailboxData {
+ constructor(tokens) {
+ let [, , attributes, delimiter, name] = tokens;
+ this.flags = this._stringsToFlags(attributes);
+ this.delimiter = unwrapString(delimiter);
+ this.name = unwrapString(name);
+ }
+
+ /**
+ * Convert an array of flag string to an internal flag number.
+ *
+ * @param {string[]} arr - An array of flag string.
+ * @returns {number} An internal flag number.
+ */
+ _stringsToFlags(arr) {
+ let stringToFlag = {
+ "\\MARKED": ImapUtils.FLAG_MARKED,
+ "\\UNMARKED": ImapUtils.FLAG_UNMARKED,
+ "\\NOINFERIORS":
+ // RFC 5258 \NoInferiors implies \HasNoChildren
+ ImapUtils.FLAG_NO_INFERIORS | ImapUtils.FLAG_HAS_NO_CHILDREN,
+ "\\NOSELECT": ImapUtils.FLAG_NO_SELECT,
+ "\\TRASH": ImapUtils.FLAG_IMAP_TRASH | ImapUtils.FLAG_IMAP_XLIST_TRASH,
+ "\\SENT": ImapUtils.FLAG_IMAP_SENT,
+ "\\DRAFTS": ImapUtils.FLAG_IMAP_DRAFTS,
+ "\\SPAM": ImapUtils.FLAG_IMAP_SPAM,
+ "\\JUNK": ImapUtils.FLAG_IMAP_SPAM,
+ "\\ARCHIVE": ImapUtils.FLAG_IMAP_ARCHIVE,
+ "\\ALL": ImapUtils.FLAG_IMAP_ALL_MAIL,
+ "\\ALLMAIL": ImapUtils.FLAG_IMAP_ALL_MAIL,
+ "\\INBOX": ImapUtils.FLAG_IMAP_INBOX,
+ "\\NONEXISTENT":
+ // RFC 5258 \NonExistent implies \NoSelect
+ ImapUtils.FLAG_NON_EXISTENT | ImapUtils.FLAG_NO_SELECT,
+ "\\SUBSCRIBED": ImapUtils.FLAG_SUBSCRIBED,
+ "\\REMOTE": ImapUtils.FLAG_REMOTE,
+ "\\HASCHILDREN": ImapUtils.FLAG_HAS_CHILDREN,
+ "\\HASNOCHILDREN": ImapUtils.FLAG_HAS_NO_CHILDREN,
+ };
+ let flags = 0;
+ for (let str of arr) {
+ flags |= stringToFlag[str.toUpperCase()] || 0;
+ }
+ return flags;
+ }
+}
+
+/**
+ * A structure to represent STATUS data.
+ * STATUS \"folder 2\" (UIDNEXT 2 MESSAGES 1 UNSEEN 1)
+ */
+class StatusData {
+ /**
+ * @params {Array<string|string[]>} tokens - The tokens of the line.
+ */
+ constructor(tokens) {
+ this.attributes = {};
+
+ // The first two tokens are ["*", "STATUS"], the last token is the attribute
+ // list, the middle part is the mailbox name.
+ this.attributes.mailbox = unwrapString(tokens[2]);
+
+ let attributes = tokens.at(-1);
+ for (let i = 0; i < attributes.length; i += 2) {
+ let type = attributes[i].toLowerCase();
+ this.attributes[type] = attributes[i + 1];
+ }
+ }
+}
+
+/**
+ * Following rfc3501 section-5.1 and section-9, this function does two things:
+ * 1. Remove the wrapping DQUOTE.
+ * 2. Unesacpe QUOTED-CHAR.
+ *
+ * @params {string} name - E.g. `"a \"b\" c"` will become `a "b" c`.
+ */
+function unwrapString(name) {
+ return name.replace(/(^"|"$)/g, "").replaceAll('\\"', '"');
+}
diff --git a/comm/mailnews/imap/src/ImapService.jsm b/comm/mailnews/imap/src/ImapService.jsm
new file mode 100644
index 0000000000..abc4036300
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapService.jsm
@@ -0,0 +1,518 @@
+/* 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 = ["ImapService"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ ImapChannel: "resource:///modules/ImapChannel.jsm",
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+});
+
+/**
+ * Set mailnews.imap.jsmodule to true to use this module.
+ *
+ * @implements {nsIImapService}
+ */
+class ImapService {
+ QueryInterface = ChromeUtils.generateQI(["nsIImapService"]);
+
+ constructor() {
+ // Initialize nsIAutoSyncManager.
+ Cc["@mozilla.org/imap/autosyncmgr;1"].getService(Ci.nsIAutoSyncManager);
+ }
+
+ get cacheStorage() {
+ if (!this._cacheStorage) {
+ this._cacheStorage = Services.cache2.memoryCacheStorage(
+ Services.loadContextInfo.custom(false, {})
+ );
+ }
+ return this._cacheStorage;
+ }
+
+ selectFolder(folder, urlListener, msgWindow) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(
+ urlListener || folder.QueryInterface(Ci.nsIUrlListener),
+ msgWindow,
+ runningUrl
+ );
+ runningUrl.updatingFolder = true;
+ client.onReady = () => {
+ client.selectFolder(folder);
+ };
+ });
+ }
+
+ liteSelectFolder(folder, urlListener, msgWindow) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(
+ urlListener || folder.QueryInterface(Ci.nsIUrlListener),
+ msgWindow,
+ runningUrl
+ );
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapLiteSelectFolder;
+ client.onReady = () => {
+ client.selectFolder(folder);
+ };
+ });
+ }
+
+ discoverAllFolders(folder, urlListener, msgWindow) {
+ let server = folder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).imapIncomingServer;
+ if (server.wrappedJSObject.hasDiscoveredFolders) {
+ return;
+ }
+ server.wrappedJSObject.hasDiscoveredFolders = true;
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener, msgWindow);
+ client.onReady = () => {
+ client.discoverAllFolders(folder);
+ };
+ });
+ }
+
+ discoverAllAndSubscribedFolders(folder, urlListener, msgWindow) {
+ this._withClient(folder, client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapDiscoverAllAndSubscribedBoxesUrl;
+ client.onReady = () => {
+ client.discoverAllAndSubscribedFolders(folder);
+ };
+ });
+ }
+
+ getListOfFoldersOnServer(server, msgWindow) {
+ this.discoverAllAndSubscribedFolders(
+ server.rootMsgFolder,
+ server.QueryInterface(Ci.nsIUrlListener),
+ msgWindow
+ );
+ }
+
+ subscribeFolder(folder, name, urlListener) {
+ return this._withClient(folder, client => {
+ client.startRunningUrl(urlListener);
+ client.onReady = () => {
+ client.subscribeFolder(folder, name);
+ };
+ });
+ }
+
+ unsubscribeFolder(folder, name, urlListener) {
+ return this._withClient(folder, client => {
+ client.startRunningUrl(urlListener);
+ client.onReady = () => {
+ client.unsubscribeFolder(folder, name);
+ };
+ });
+ }
+
+ addMessageFlags(folder, urlListener, messageIds, flags, messageIdsAreUID) {
+ this._updateMessageFlags("+", folder, urlListener, messageIds, flags);
+ }
+
+ subtractMessageFlags(
+ folder,
+ urlListener,
+ messageIds,
+ flags,
+ messageIdsAreUID
+ ) {
+ this._updateMessageFlags("-", folder, urlListener, messageIds, flags);
+ }
+
+ setMessageFlags(
+ folder,
+ urlListener,
+ outURL,
+ messageIds,
+ flags,
+ messageIdsAreUID
+ ) {
+ outURL.value = this._updateMessageFlags(
+ "",
+ folder,
+ urlListener,
+ messageIds,
+ flags
+ );
+ }
+
+ _updateMessageFlags(action, folder, urlListener, messageIds, flags) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(urlListener, null, runningUrl);
+ client.onReady = () => {
+ client.updateMessageFlags(action, folder, messageIds, flags);
+ };
+ });
+ }
+
+ renameLeaf(folder, newName, urlListener, msgWindow) {
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener, msgWindow);
+ client.onReady = () => {
+ client.renameFolder(folder, newName);
+ };
+ });
+ }
+
+ fetchMessage(
+ imapUrl,
+ imapAction,
+ folder,
+ msgSink,
+ msgWindow,
+ displayConsumer,
+ msgIds,
+ convertDataToText
+ ) {
+ imapUrl.imapAction = imapAction;
+ imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = msgWindow;
+ if (displayConsumer instanceof Ci.nsIDocShell) {
+ imapUrl
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .loadURI(
+ displayConsumer.QueryInterface(Ci.nsIDocShell),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+ );
+ } else {
+ let streamListener = displayConsumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.ImapChannel(imapUrl, {
+ QueryInterface: ChromeUtils.generateQI(["nsILoadInfo"]),
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ internalContentPolicy: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+ let listener = streamListener;
+ if (convertDataToText) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ streamListener,
+ channel
+ );
+ }
+ channel.asyncOpen(listener);
+ }
+ }
+
+ fetchCustomMsgAttribute(folder, msgWindow, attribute, uids) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(null, msgWindow, runningUrl);
+ client.onReady = () => {
+ client.fetchMsgAttribute(folder, uids, attribute);
+ };
+ });
+ }
+
+ expunge(folder, urlListener, msgWindow) {
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener, msgWindow);
+ client.onReady = () => {
+ client.expunge(folder);
+ };
+ });
+ }
+
+ onlineMessageCopy(
+ folder,
+ messageIds,
+ dstFolder,
+ idsAreUids,
+ isMove,
+ urlListener,
+ outURL,
+ copyState,
+ msgWindow
+ ) {
+ this._withClient(folder, client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = isMove
+ ? Ci.nsIImapUrl.nsImapOnlineMove
+ : Ci.nsIImapUrl.nsImapOnlineCopy;
+ client.onReady = () => {
+ client.copy(folder, dstFolder, messageIds, idsAreUids, isMove);
+ };
+ });
+ }
+
+ appendMessageFromFile(
+ file,
+ dstFolder,
+ messageId,
+ idsAreUids,
+ inSelectedState,
+ urlListener,
+ copyState,
+ msgWindow
+ ) {
+ let server = dstFolder.server;
+ let imapUrl = Services.io
+ .newURI(
+ `imap://${server.hostName}:${server.port}/fetch>UID>/${dstFolder.name}>${messageId}`
+ )
+ .QueryInterface(Ci.nsIImapUrl);
+ imapUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapAppendMsgFromFile;
+ imapUrl.copyState = copyState;
+ if (Services.io.offline) {
+ this._offlineAppendMessageFile(file, imapUrl, dstFolder, urlListener);
+ return;
+ }
+ this._withClient(dstFolder, client => {
+ client.startRunningUrl(urlListener, msgWindow, imapUrl);
+ client.onReady = () => {
+ client.uploadMessageFromFile(
+ file,
+ dstFolder,
+ copyState,
+ inSelectedState
+ );
+ };
+ });
+ }
+
+ /**
+ * Append a message file to a folder locally.
+ *
+ * @param {nsIFile} file - The message file to append.
+ * @param {nsIURI} url - The imap url to run.
+ * @param {nsIMsgFolder} dstFolder - The target message folder.
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ */
+ async _offlineAppendMessageFile(file, url, dstFolder, urlListener) {
+ if (dstFolder.locked) {
+ const NS_MSG_FOLDER_BUSY = 2153054218;
+ throw Components.Exception(
+ "Destination folder locked",
+ NS_MSG_FOLDER_BUSY
+ );
+ }
+
+ let db = dstFolder.msgDatabase.QueryInterface(Ci.nsIMsgOfflineOpsDatabase);
+ let fakeKey = db.nextFakeOfflineMsgKey;
+ let op = db.getOfflineOpForKey(fakeKey, true);
+ op.operation = Ci.nsIMsgOfflineImapOperation.kAppendDraft;
+ op.destinationFolderURI = dstFolder.URI;
+ // Release op eagerly, to make test_offlineDraftDataloss happy in debug build.
+ op = null;
+ Cu.forceGC();
+
+ let server = dstFolder.server;
+ let newMsgHdr = db.createNewHdr(fakeKey);
+ let outputStream = dstFolder.getOfflineStoreOutputStream(newMsgHdr);
+ let content = lazy.MailStringUtils.uint8ArrayToByteString(
+ await IOUtils.read(file.path)
+ );
+
+ let msgParser = Cc[
+ "@mozilla.org/messenger/messagestateparser;1"
+ ].createInstance(Ci.nsIMsgParseMailMsgState);
+ msgParser.SetMailDB(db);
+ msgParser.state = Ci.nsIMsgParseMailMsgState.ParseHeadersState;
+ msgParser.newMsgHdr = newMsgHdr;
+ msgParser.setNewKey(fakeKey);
+
+ for (let line of content.split("\r\n")) {
+ line += "\r\n";
+ msgParser.ParseAFolderLine(line, line.length);
+ outputStream.write(line, line.length);
+ }
+ msgParser.FinishHeader();
+
+ newMsgHdr.orFlags(Ci.nsMsgMessageFlags.Offline | Ci.nsMsgMessageFlags.Read);
+ newMsgHdr.offlineMessageSize = content.length;
+ db.addNewHdrToDB(newMsgHdr, true);
+ dstFolder.setFlag(Ci.nsMsgFolderFlags.OfflineEvents);
+ if (server.msgStore) {
+ server.msgStore.finishNewMessage(outputStream, newMsgHdr);
+ }
+
+ urlListener.OnStopRunningUrl(url, Cr.NS_OK);
+ outputStream.close();
+ db.close(true);
+ }
+
+ ensureFolderExists(parent, folderName, msgWindow, urlListener) {
+ this._withClient(parent, client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapEnsureExistsFolder;
+ client.onReady = () => {
+ client.ensureFolderExists(parent, folderName);
+ };
+ });
+ }
+
+ updateFolderStatus(folder, urlListener) {
+ this._withClient(folder, client => {
+ let runningUrl = client.startRunningUrl(urlListener);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapFolderStatus;
+ client.onReady = () => {
+ client.updateFolderStatus(folder);
+ };
+ });
+ }
+
+ createFolder(parent, folderName, urlListener) {
+ return this._withClient(parent, (client, runningUrl) => {
+ client.startRunningUrl(urlListener, null, runningUrl);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapCreateFolder;
+ client.onReady = () => {
+ client.createFolder(parent, folderName);
+ };
+ });
+ }
+
+ moveFolder(srcFolder, dstFolder, urlListener, msgWindow) {
+ this._withClient(srcFolder, client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapMoveFolderHierarchy;
+ client.onReady = () => {
+ client.moveFolder(srcFolder, dstFolder);
+ };
+ });
+ }
+
+ listFolder(folder, urlListener) {
+ this._withClient(folder, client => {
+ let runningUrl = client.startRunningUrl(urlListener);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapListFolder;
+ client.onReady = () => {
+ client.listFolder(folder);
+ };
+ });
+ }
+
+ deleteFolder(folder, urlListener, msgWindow) {
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener, msgWindow);
+ client.onReady = () => {
+ client.deleteFolder(folder);
+ };
+ });
+ }
+
+ storeCustomKeywords(folder, msgWindow, flagsToAdd, flagsToSubtract, uids) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(null, msgWindow, runningUrl);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapMsgStoreCustomKeywords;
+ client.onReady = () => {
+ client.storeCustomKeywords(folder, flagsToAdd, flagsToSubtract, uids);
+ };
+ });
+ }
+
+ downloadMessagesForOffline(messageIds, folder, urlListener, msgWindow) {
+ let server = folder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ ).imapIncomingServer;
+ let imapUrl = Services.io
+ .newURI(
+ `imap://${server.hostName}:${server.port}/fetch>UID>/${folder.name}>${messageIds}`
+ )
+ .QueryInterface(Ci.nsIImapUrl);
+ imapUrl.storeResultsOffline = true;
+ if (urlListener) {
+ imapUrl
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .RegisterListener(urlListener);
+ }
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener, msgWindow, imapUrl);
+ client.onReady = () => {
+ client.fetchMessage(folder, messageIds);
+ };
+ });
+ }
+
+ playbackAllOfflineOperations(msgWindow, urlListener) {
+ let offlineSync = Cc["@mozilla.org/imap/offlinesync;1"].createInstance(
+ Ci.nsIImapOfflineSync
+ );
+ offlineSync.init(msgWindow, urlListener, null, false);
+ offlineSync.processNextOperation();
+ }
+
+ getHeaders(folder, urlListener, outURL, messageIds, messageIdsAreUID) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.startRunningUrl(urlListener, null, runningUrl);
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapMsgFetch;
+ client.onReady = () => {
+ client.getHeaders(folder, messageIds);
+ };
+ });
+ }
+
+ getBodyStart(folder, urlListener, messageIds, numBytes) {
+ return this._withClient(folder, (client, runningUrl) => {
+ runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction =
+ Ci.nsIImapUrl.nsImapMsgPreview;
+ client.startRunningUrl(urlListener, null, runningUrl);
+ client.onReady = () => {
+ client.fetchMessage(folder, messageIds, numBytes);
+ };
+ });
+ }
+
+ deleteAllMessages(folder, urlListener) {
+ this._withClient(folder, client => {
+ client.startRunningUrl(urlListener);
+ client.onReady = () => {
+ client.deleteAllMessages(folder);
+ };
+ });
+ }
+
+ verifyLogon(folder, urlListener, msgWindow) {
+ return this._withClient(folder, (client, runningUrl) => {
+ client.verifyLogon = true;
+ client.startRunningUrl(urlListener, msgWindow, runningUrl);
+ client.onReady = () => {};
+ });
+ }
+
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {nsIMsgFolder} folder - The associated folder.
+ * @param {Function} handler - A callback function to take a ImapClient
+ * instance, and do some actions.
+ */
+ _withClient(folder, handler) {
+ let server = folder.server.QueryInterface(Ci.nsIMsgIncomingServer);
+ let runningUrl = Services.io
+ .newURI(`imap://${server.hostName}:${server.port}`)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ server.wrappedJSObject.withClient(folder, client =>
+ handler(client, runningUrl)
+ );
+ return runningUrl;
+ }
+}
+
+ImapService.prototype.classID = Components.ID(
+ "{2ea8fbe6-029b-4bff-ae05-b794cf955afb}"
+);
diff --git a/comm/mailnews/imap/src/ImapUtils.jsm b/comm/mailnews/imap/src/ImapUtils.jsm
new file mode 100644
index 0000000000..468d0c98a5
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapUtils.jsm
@@ -0,0 +1,197 @@
+/* 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 = ["ImapCapFlags", "ImapUtils"];
+
+/**
+ * The purpose here is not to convert all capabilities to flag number, but to
+ * interact with nsImapMailFolder through nsIImapIncomingServer.getCapability
+ * interface.
+ *
+ * @see nsImapCore.h
+ */
+var ImapCapFlags = {
+ Undefined: 0x00000000,
+ Defined: 0x00000001,
+ AuthLogin: 0x00000002, // AUTH LOGIN
+ XSender: 0x00000008,
+ IMAP4: 0x00000010, // RFC1734
+ IMAP4rev1: 0x00000020, // RFC2060
+ NoHierarchyRename: 0x00000080, // no hierarchy rename
+ ACL: 0x00000100, // ACL extension
+ Namespace: 0x00000200, // IMAP4 Namespace Extension
+ ID: 0x00000400, // client user agent id extension
+ XServerInfo: 0x00000800, // XSERVERINFO extension for admin urls
+ AuthPlain: 0x00001000, // new form of auth plain base64 login
+ Uidplus: 0x00002000, // RFC 2359 UIDPLUS extension
+ LiteralPlus: 0x00004000, // RFC 2088 LITERAL+ extension
+ AOLImap: 0x00008000, // aol imap extensions
+ Language: 0x00010000, // language extensions
+ CRAM: 0x00020000, // CRAM auth extension
+ Quota: 0x00040000, // RFC 2087 quota extension
+ Idle: 0x00080000, // RFC 2177 idle extension
+ AuthNTLM: 0x00100000, // AUTH NTLM extension
+ AuthMSN: 0x00200000, // AUTH MSN extension
+ StartTLS: 0x00400000, // STARTTLS support
+ AuthNone: 0x00800000, // needs no login
+ AuthGssApi: 0x01000000, // GSSAPI AUTH
+ CondStore: 0x02000000, // RFC 3551 CondStore extension
+ Enable: 0x04000000, // RFC 5161 ENABLE extension
+ XList: 0x08000000, // XLIST extension
+ CompressDeflate: 0x10000000, // RFC 4978 COMPRESS extension
+ AuthExternal: 0x20000000, // RFC 2222 SASL AUTH EXTERNAL
+ Move: 0x40000000, // Proposed MOVE RFC
+ HighestModSeq: 0x80000000, // Subset of RFC 3551
+ ListExtended: 0x100000000, // RFC 5258
+ SpecialUse: 0x200000000, // RFC 6154: Sent, Draft etc. folders
+ Gmail: 0x400000000, // X-GM-EXT-1 capability extension for gmail
+ XOAuth2: 0x800000000, // AUTH XOAUTH2 extension
+ ClientID: 0x1000000000, // ClientID capability
+ UTF8Accept: 0x2000000000, // RFC 6855: UTF8:ACCEPT
+
+ /**
+ * Convert an array of capability string to an internal flag number, for example,
+ * ["QUOTA", "X-GM-EXT-1"] will become 0x400040000.
+ *
+ * @param {string[]} arr - An array of flag string.
+ * @returns {number} An internal flag number.
+ */
+ stringsToFlags(arr) {
+ let flags = 0;
+ for (let str of arr) {
+ switch (str) {
+ case "QUOTA":
+ flags |= this.Quota;
+ break;
+ case "X-GM-EXT-1":
+ flags |= this.Gmail;
+ break;
+ default:
+ break;
+ }
+ }
+ return flags;
+ },
+};
+
+/**
+ * Collection of helper functions for IMAP.
+ */
+var ImapUtils = {
+ NS_MSG_ERROR_IMAP_COMMAND_FAILED: 0x80550021,
+
+ /** @see nsImapCore.h */
+ FLAG_NONE: 0x0000,
+ /** mailbox flags */
+ FLAG_MARKED: 0x01,
+ FLAG_UNMARKED: 0x02,
+ FLAG_NO_INFERIORS: 0x04,
+ FLAG_NO_SELECT: 0x08,
+ FLAG_IMAP_TRASH: 0x10,
+ FLAG_JUST_EXPUNGED: 0x20,
+ FLAG_PERSONAL_MAILBOX: 0x40,
+ FLAG_PUBLIC_MAILBOX: 0x80,
+ FLAG_OTHER_USERS_MAILBOX: 0x100,
+ FLAG_NAMESPACE: 0x200,
+ FLAG_NEWLY_CREATED_FOLDER: 0x400,
+ FLAG_IMAP_DRAFTS: 0x800,
+ FLAG_IMAP_SPAM: 0x1000,
+ FLAG_IMAP_SENT: 0x2000,
+ FLAG_IMAP_INBOX: 0x4000,
+ FLAG_IMAP_ALL_MAIL: 0x8000,
+ FLAG_IMAP_XLIST_TRASH: 0x10000,
+ FLAG_NON_EXISTENT: 0x20000,
+ FLAG_SUBSCRIBED: 0x40000,
+ FLAG_REMOTE: 0x80000,
+ FLAG_HAS_CHILDREN: 0x100000,
+ FLAG_HAS_NO_CHILDREN: 0x200000,
+ FLAG_IMAP_ARCHIVE: 0x400000,
+
+ /** message flags */
+ FLAG_SEEN: 0x0001,
+ FLAG_ANSWERED: 0x0002,
+ FLAG_FLAGGED: 0x0004,
+ FLAG_DELETED: 0x0008,
+ FLAG_DRAFT: 0x0010,
+ FLAG_FORWARDED: 0x0040,
+ FLAG_MDN_SENT: 0x0080,
+ FLAG_CUSTOM_KEYWORD: 0x0100,
+ FLAG_LABEL: 0x0e00,
+ FLAG_SUPPORT_FORWARDED_FLAG: 0x4000,
+ FLAG_SUPPORT_USER_FLAG: 0x8000,
+
+ logger: console.createInstance({
+ prefix: "mailnews.imap",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.imap.loglevel",
+ }),
+
+ /**
+ * Convert internal flag number to flag string, for example,
+ * 0x3 will become "\\Seen \\Answered".
+ *
+ * @param {number} flags - Internal flag number.
+ * @param {number} supportedFlags - Server supported flags.
+ * @returns {string} Flags string that can be sent to the server.
+ */
+ flagsToString(flags, supportedFlags) {
+ let arr = [];
+ let strFlags = [
+ ["\\Seen", this.FLAG_SEEN],
+ ["\\Answered", this.FLAG_ANSWERED],
+ ["\\Flagged", this.FLAG_FLAGGED],
+ ["\\Deleted", this.FLAG_DELETED],
+ ["\\Draft", this.FLAG_DRAFT],
+ ["$Forwarded", this.FLAG_FORWARDED],
+ ["$MDNSent", this.FLAG_MDN_SENT],
+ ];
+ for (let [str, flag] of strFlags) {
+ if (flags & flag && supportedFlags & flag) {
+ arr.push(str);
+ }
+ }
+ return arr.join(" ");
+ },
+
+ /**
+ * Convert a flag string to an internal flag number, for example,
+ * "\\Seen" will become 0x1.
+ *
+ * @param {string} str - A single flag string.
+ * @returns {number} An internal flag number.
+ */
+ stringToFlag(str) {
+ return (
+ {
+ "\\SEEN": this.FLAG_SEEN,
+ "\\ANSWERED": this.FLAG_ANSWERED,
+ "\\FLAGGED": this.FLAG_FLAGGED,
+ "\\DELETED": this.FLAG_DELETED,
+ "\\DRAFT": this.FLAG_DRAFT,
+ "\\*":
+ this.FLAG_LABEL |
+ this.FLAG_MDN_SENT |
+ this.FLAG_FORWARDED |
+ this.FLAG_SUPPORT_USER_FLAG,
+ $MDNSENT: this.FLAG_MDN_SENT,
+ $FORWARDED: this.FLAG_FORWARDED,
+ }[str.toUpperCase()] || this.FLAG_NONE
+ );
+ },
+
+ /**
+ * Convert an array of flag string to an internal flag number, for example,
+ * ["\\Seen", "\\Answered"] will become 0x3.
+ *
+ * @param {string[]} arr - An array of flag string.
+ * @returns {number} An internal flag number.
+ */
+ stringsToFlags(arr) {
+ let flags = 0;
+ for (let str of arr) {
+ flags |= this.stringToFlag(str);
+ }
+ return flags;
+ },
+};
diff --git a/comm/mailnews/imap/src/components.conf b/comm/mailnews/imap/src/components.conf
new file mode 100644
index 0000000000..c3c0f692cf
--- /dev/null
+++ b/comm/mailnews/imap/src/components.conf
@@ -0,0 +1,76 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{26b5aa4d-742d-4112-b834-d6c4b1276878}",
+ "contract_ids": ["@mozilla.org/messenger/imap-module-loader;1"],
+ "jsm": "resource:///modules/ImapModuleLoader.jsm",
+ "constructor": "ImapModuleLoader",
+ "categories": {"profile-after-change": "ImapModuleLoader"},
+ },
+ {
+ "cid": "{21a89611-dc0d-11d2-806c-006008128c4e}",
+ "type": "nsImapUrl",
+ "headers": ["/comm/mailnews/imap/src/nsImapUrl.h"],
+ },
+ {
+ "cid": "{8c0c40d1-e173-11d2-806e-006008128c4e}",
+ "type": "nsImapProtocol",
+ "headers": ["/comm/mailnews/imap/src/nsImapProtocol.h"],
+ },
+ {
+ "cid": "{4eca51df-6734-11d3-989a-001083010e9b}",
+ "type": "nsImapMockChannel",
+ "headers": ["/comm/mailnews/imap/src/nsImapProtocol.h"],
+ },
+ {
+ "cid": "{479ce8fc-e725-11d2-a505-0060b0fc04b7}",
+ "type": "nsImapHostSessionList",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/imap/src/nsImapHostSessionList.h"],
+ },
+ {
+ "cid": "{8d3675e0-ed46-11d2-8077-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=imap"],
+ "type": "nsImapIncomingServer",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/imap/src/nsImapIncomingServer.h"],
+ },
+ {
+ "cid": "{fa32d000-f6a0-11d2-af8d-001083002da8}",
+ "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=imap"],
+ "type": "nsImapMailFolder",
+ "headers": ["/comm/mailnews/imap/src/nsImapMailFolder.h"],
+ },
+ {
+ "cid": "{c5852b22-ebe2-11d2-95ad-000064657374}",
+ "contract_ids": [
+ "@mozilla.org/messenger/messageservice;1?type=imap-message",
+ "@mozilla.org/messenger/messageservice;1?type=imap",
+ "@mozilla.org/messenger/imapservice;1",
+ "@mozilla.org/network/protocol;1?name=imap",
+ "@mozilla.org/messenger/protocol/info;1?type=imap",
+ "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder",
+ ],
+ "type": "nsImapService",
+ "headers": ["/comm/mailnews/imap/src/nsImapService.h"],
+ "name": "Imap",
+ "interfaces": ["nsIImapService"],
+ },
+ {
+ "cid": "{c358c568-47b2-42b2-8146-3c0f8d1fad6e}",
+ "contract_ids": ["@mozilla.org/imap/autosyncmgr;1"],
+ "type": "nsAutoSyncManager",
+ "headers": ["/comm/mailnews/imap/src/nsAutoSyncManager.h"],
+ },
+ {
+ "cid": "{64fa0a31-a494-4f4b-ac4d-b29910d6ccd6}",
+ "contract_ids": ["@mozilla.org/imap/offlinesync;1"],
+ "type": "nsImapOfflineSync",
+ "headers": ["/comm/mailnews/imap/src/nsImapOfflineSync.h"],
+ },
+]
diff --git a/comm/mailnews/imap/src/moz.build b/comm/mailnews/imap/src/moz.build
new file mode 100644
index 0000000000..0d1f2150de
--- /dev/null
+++ b/comm/mailnews/imap/src/moz.build
@@ -0,0 +1,56 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "nsImapCore.h",
+]
+
+SOURCES += [
+ "nsAutoSyncManager.cpp",
+ "nsAutoSyncState.cpp",
+ "nsImapFlagAndUidState.cpp",
+ "nsImapGenericParser.cpp",
+ "nsImapHostSessionList.cpp",
+ "nsImapIncomingServer.cpp",
+ "nsImapMailFolder.cpp",
+ "nsImapNamespace.cpp",
+ "nsImapOfflineSync.cpp",
+ "nsImapProtocol.cpp",
+ "nsImapSearchResults.cpp",
+ "nsImapServerResponseParser.cpp",
+ "nsImapService.cpp",
+ "nsImapStringBundle.cpp",
+ "nsImapUndoTxn.cpp",
+ "nsImapUrl.cpp",
+ "nsImapUtils.cpp",
+ "nsSyncRunnableHelpers.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+LOCAL_INCLUDES += [
+ # for nsImapProtocol.cpp
+ "!/ipc/ipdl/_ipdlheaders",
+ "/ipc/chromium/src",
+ "/netwerk/base",
+]
+
+EXTRA_JS_MODULES += [
+ "ImapChannel.jsm",
+ "ImapClient.jsm",
+ "ImapFolderContentHandler.sys.mjs",
+ "ImapIncomingServer.jsm",
+ "ImapMessageService.jsm",
+ "ImapModuleLoader.jsm",
+ "ImapProtocolHandler.jsm",
+ "ImapProtocolInfo.jsm",
+ "ImapResponse.jsm",
+ "ImapService.jsm",
+ "ImapUtils.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/imap/src/nsAutoSyncManager.cpp b/comm/mailnews/imap/src/nsAutoSyncManager.cpp
new file mode 100644
index 0000000000..e0b9611dc0
--- /dev/null
+++ b/comm/mailnews/imap/src/nsAutoSyncManager.cpp
@@ -0,0 +1,1372 @@
+/* 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/. */
+
+#include "nsAutoSyncManager.h"
+#include "nsAutoSyncState.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIObserverService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIMsgMailSession.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+#include "nsIIOService.h"
+#include "nsITimer.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsDefaultAutoSyncMsgStrategy, nsIAutoSyncMsgStrategy)
+
+const char* kAppIdleNotification = "mail:appIdle";
+const char* kStartupDoneNotification = "mail-startup-done";
+LazyLogModule gAutoSyncLog("IMAPAutoSync");
+
+// recommended size of each group of messages per download
+static const uint32_t kDefaultGroupSize = 50U * 1024U /* 50K */;
+
+nsDefaultAutoSyncMsgStrategy::nsDefaultAutoSyncMsgStrategy() {}
+
+nsDefaultAutoSyncMsgStrategy::~nsDefaultAutoSyncMsgStrategy() {}
+
+NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::Sort(
+ nsIMsgFolder* aFolder, nsIMsgDBHdr* aMsgHdr1, nsIMsgDBHdr* aMsgHdr2,
+ nsAutoSyncStrategyDecisionType* aDecision) {
+ NS_ENSURE_ARG_POINTER(aDecision);
+
+ uint32_t msgSize1 = 0, msgSize2 = 0;
+ PRTime msgDate1 = 0, msgDate2 = 0;
+
+ if (!aMsgHdr1 || !aMsgHdr2) {
+ *aDecision = nsAutoSyncStrategyDecisions::Same;
+ return NS_OK;
+ }
+
+ aMsgHdr1->GetMessageSize(&msgSize1);
+ aMsgHdr1->GetDate(&msgDate1);
+
+ aMsgHdr2->GetMessageSize(&msgSize2);
+ aMsgHdr2->GetDate(&msgDate2);
+
+ // Special case: if message size is larger than a
+ // certain size, then place it to the bottom of the q
+ if (msgSize2 > kFirstPassMessageSize && msgSize1 > kFirstPassMessageSize)
+ *aDecision = msgSize2 > msgSize1 ? nsAutoSyncStrategyDecisions::Lower
+ : nsAutoSyncStrategyDecisions::Higher;
+ else if (msgSize2 > kFirstPassMessageSize)
+ *aDecision = nsAutoSyncStrategyDecisions::Lower;
+ else if (msgSize1 > kFirstPassMessageSize)
+ *aDecision = nsAutoSyncStrategyDecisions::Higher;
+ else {
+ // Most recent and smallest first
+ if (msgDate1 < msgDate2)
+ *aDecision = nsAutoSyncStrategyDecisions::Higher;
+ else if (msgDate1 > msgDate2)
+ *aDecision = nsAutoSyncStrategyDecisions::Lower;
+ else {
+ if (msgSize1 > msgSize2)
+ *aDecision = nsAutoSyncStrategyDecisions::Higher;
+ else if (msgSize1 < msgSize2)
+ *aDecision = nsAutoSyncStrategyDecisions::Lower;
+ else
+ *aDecision = nsAutoSyncStrategyDecisions::Same;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::IsExcluded(nsIMsgFolder* aFolder,
+ nsIMsgDBHdr* aMsgHdr,
+ bool* aDecision) {
+ NS_ENSURE_ARG_POINTER(aDecision);
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aFolder);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ nsresult rv = aFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapIncomingServer> imapServer(do_QueryInterface(server, &rv));
+ int32_t offlineMsgAgeLimit = -1;
+ imapServer->GetAutoSyncMaxAgeDays(&offlineMsgAgeLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+ PRTime msgDate;
+ aMsgHdr->GetDate(&msgDate);
+ *aDecision = offlineMsgAgeLimit > 0 &&
+ msgDate < MsgConvertAgeInDaysToCutoffDate(offlineMsgAgeLimit);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsDefaultAutoSyncFolderStrategy, nsIAutoSyncFolderStrategy)
+
+nsDefaultAutoSyncFolderStrategy::nsDefaultAutoSyncFolderStrategy() {}
+
+nsDefaultAutoSyncFolderStrategy::~nsDefaultAutoSyncFolderStrategy() {}
+
+NS_IMETHODIMP nsDefaultAutoSyncFolderStrategy::Sort(
+ nsIMsgFolder* aFolderA, nsIMsgFolder* aFolderB,
+ nsAutoSyncStrategyDecisionType* aDecision) {
+ NS_ENSURE_ARG_POINTER(aDecision);
+
+ if (!aFolderA || !aFolderB) {
+ *aDecision = nsAutoSyncStrategyDecisions::Same;
+ return NS_OK;
+ }
+
+ bool isInbox1, isInbox2, isDrafts1, isDrafts2, isTrash1, isTrash2;
+ aFolderA->GetFlag(nsMsgFolderFlags::Inbox, &isInbox1);
+ aFolderB->GetFlag(nsMsgFolderFlags::Inbox, &isInbox2);
+ //
+ aFolderA->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts1);
+ aFolderB->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts2);
+ //
+ aFolderA->GetFlag(nsMsgFolderFlags::Trash, &isTrash1);
+ aFolderB->GetFlag(nsMsgFolderFlags::Trash, &isTrash2);
+
+ // Follow this order;
+ // INBOX > DRAFTS > SUBFOLDERS > TRASH
+
+ // test whether the folder is opened by the user.
+ // we give high priority to the folders explicitly opened by
+ // the user.
+ nsresult rv;
+ bool folderAOpen = false;
+ bool folderBOpen = false;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) {
+ session->IsFolderOpenInWindow(aFolderA, &folderAOpen);
+ session->IsFolderOpenInWindow(aFolderB, &folderBOpen);
+ }
+
+ if (folderAOpen == folderBOpen) {
+ // if both of them or none of them are opened by the user
+ // make your decision based on the folder type
+ if (isInbox2 || (isDrafts2 && !isInbox1) || isTrash1)
+ *aDecision = nsAutoSyncStrategyDecisions::Higher;
+ else if (isInbox1 || (isDrafts1 && !isDrafts2) || isTrash2)
+ *aDecision = nsAutoSyncStrategyDecisions::Lower;
+ else
+ *aDecision = nsAutoSyncStrategyDecisions::Same;
+ } else {
+ // otherwise give higher priority to opened one
+ *aDecision = folderBOpen ? nsAutoSyncStrategyDecisions::Higher
+ : nsAutoSyncStrategyDecisions::Lower;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDefaultAutoSyncFolderStrategy::IsExcluded(nsIMsgFolder* aFolder,
+ bool* aDecision) {
+ NS_ENSURE_ARG_POINTER(aDecision);
+ NS_ENSURE_ARG_POINTER(aFolder);
+ uint32_t folderFlags;
+ aFolder->GetFlags(&folderFlags);
+ // exclude saved search
+ *aDecision = (folderFlags & nsMsgFolderFlags::Virtual);
+ if (!*aDecision) {
+ // Exclude orphans
+ nsCOMPtr<nsIMsgFolder> parent;
+ aFolder->GetParent(getter_AddRefs(parent));
+ if (!parent) *aDecision = true;
+ }
+ return NS_OK;
+}
+
+#define NOTIFY_LISTENERS_STATIC(obj_, propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIAutoSyncMgrListener>>::ForwardIterator iter( \
+ obj_->mListeners); \
+ nsCOMPtr<nsIAutoSyncMgrListener> listener; \
+ while (iter.HasMore()) { \
+ listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+#define NOTIFY_LISTENERS(propertyfunc_, params_) \
+ NOTIFY_LISTENERS_STATIC(this, propertyfunc_, params_)
+
+nsAutoSyncManager::nsAutoSyncManager() {
+ mGroupSize = kDefaultGroupSize;
+
+ mIdleState = notIdle;
+ mStartupDone = false;
+ mDownloadModel = dmChained;
+ mUpdateInProgress = false;
+ mPaused = false;
+
+ nsresult rv;
+ mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1", &rv);
+ if (mIdleService) mIdleService->AddIdleObserver(this, kIdleTimeInSec);
+
+ // Observe xpcom-shutdown event and app-idle changes
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ observerService->AddObserver(this, kAppIdleNotification, false);
+ observerService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false);
+ observerService->AddObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, false);
+ observerService->AddObserver(this, kStartupDoneNotification, false);
+}
+
+nsAutoSyncManager::~nsAutoSyncManager() {}
+
+void nsAutoSyncManager::InitTimer() {
+ if (!mTimer) {
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTimer), TimerCallback, (void*)this, kTimerIntervalInMs,
+ nsITimer::TYPE_REPEATING_SLACK, "nsAutoSyncManager::TimerCallback",
+ nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start nsAutoSyncManager timer");
+ }
+ }
+}
+
+void nsAutoSyncManager::StopTimer() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void nsAutoSyncManager::StartTimerIfNeeded() {
+ if ((mUpdateQ.Count() > 0 || mDiscoveryQ.Count() > 0) && !mTimer) InitTimer();
+}
+
+void nsAutoSyncManager::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ if (!aClosure) return;
+
+ nsAutoSyncManager* autoSyncMgr = static_cast<nsAutoSyncManager*>(aClosure);
+ if (autoSyncMgr->GetIdleState() == notIdle ||
+ (autoSyncMgr->mDiscoveryQ.Count() <= 0 &&
+ autoSyncMgr->mUpdateQ.Count() <= 0)) {
+ // Idle will create a new timer automatically if discovery Q or update Q is
+ // not empty
+ autoSyncMgr->StopTimer();
+ }
+
+ // process a folder in the discovery queue
+ if (autoSyncMgr->mDiscoveryQ.Count() > 0) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mDiscoveryQ[0]);
+ if (autoSyncStateObj) {
+ uint32_t leftToProcess;
+ nsresult rv = autoSyncStateObj->ProcessExistingHeaders(
+ kNumberOfHeadersToProcess, &leftToProcess);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder)
+ NOTIFY_LISTENERS_STATIC(
+ autoSyncMgr, OnDiscoveryQProcessed,
+ (folder, kNumberOfHeadersToProcess, leftToProcess));
+
+ if (NS_SUCCEEDED(rv) && 0 == leftToProcess) {
+ autoSyncMgr->mDiscoveryQ.RemoveObjectAt(0);
+ if (folder)
+ NOTIFY_LISTENERS_STATIC(
+ autoSyncMgr, OnFolderRemovedFromQ,
+ (nsIAutoSyncMgrListener::DiscoveryQueue, folder));
+ }
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: processed discovery q for folder=%s, "
+ "msgs left to process in folder=%d",
+ __func__, folderName.get(), leftToProcess));
+ }
+ }
+ }
+
+ if (autoSyncMgr->mUpdateQ.Count() > 0) {
+ if (!autoSyncMgr->mUpdateInProgress) // Avoids possible overlap of updates
+ {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mUpdateQ[0]);
+ if (autoSyncStateObj) {
+ int32_t state;
+ nsresult rv = autoSyncStateObj->GetState(&state);
+ if (NS_SUCCEEDED(rv) && (state == nsAutoSyncState::stCompletedIdle ||
+ state == nsAutoSyncState::stUpdateNeeded)) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(folder, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ rv = imapFolder->InitiateAutoSync(autoSyncMgr);
+ if (NS_SUCCEEDED(rv)) {
+ autoSyncMgr->mUpdateInProgress = true;
+ NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnAutoSyncInitiated,
+ (folder));
+ }
+ }
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: process update q for folder=%s", __func__,
+ folderName.get()));
+ }
+ }
+ }
+ }
+ // if initiation is not successful for some reason, or
+ // if there is an on going download for this folder,
+ // remove it from q and continue with the next one
+ if (!autoSyncMgr->mUpdateInProgress) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ autoSyncMgr->mUpdateQ[0]->GetOwnerFolder(getter_AddRefs(folder));
+
+ autoSyncMgr->mUpdateQ.RemoveObjectAt(0);
+
+ if (folder)
+ NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnFolderRemovedFromQ,
+ (nsIAutoSyncMgrListener::UpdateQueue, folder));
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Error)) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Error,
+ ("%s: update q init failed for folder=%s", __func__,
+ folderName.get()));
+ }
+ }
+
+ } // endif
+}
+
+/**
+ * Populates aChainedQ with the auto-sync state objects that are not owned by
+ * the same imap server.
+ * Assumes that aChainedQ initially empty.
+ */
+void nsAutoSyncManager::ChainFoldersInQ(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsCOMArray<nsIAutoSyncState>& aChainedQ) {
+ if (aQueue.Count() > 0) aChainedQ.AppendObject(aQueue[0]);
+
+ int32_t pqElemCount = aQueue.Count();
+ for (int32_t pqidx = 1; pqidx < pqElemCount; pqidx++) {
+ bool chained = false;
+ int32_t needToBeReplacedWith = -1;
+ int32_t elemCount = aChainedQ.Count();
+ for (int32_t idx = 0; idx < elemCount; idx++) {
+ bool isSibling;
+ nsresult rv = aChainedQ[idx]->IsSibling(aQueue[pqidx], &isSibling);
+
+ if (NS_SUCCEEDED(rv) && isSibling) {
+ // this prevent us to overwrite a lower priority sibling in
+ // download-in-progress state with a higher priority one.
+ // we have to wait until its download is completed before
+ // switching to new one.
+ int32_t state;
+ aQueue[pqidx]->GetState(&state);
+ if (aQueue[pqidx] != aChainedQ[idx] &&
+ state == nsAutoSyncState::stDownloadInProgress)
+ needToBeReplacedWith = idx;
+ else
+ chained = true;
+
+ break;
+ }
+ } // endfor
+
+ if (needToBeReplacedWith > -1)
+ aChainedQ.ReplaceObjectAt(aQueue[pqidx], needToBeReplacedWith);
+ else if (!chained)
+ aChainedQ.AppendObject(aQueue[pqidx]);
+
+ } // endfor
+}
+
+/**
+ * Searches the given queue for another folder owned by the same imap server.
+ */
+nsIAutoSyncState* nsAutoSyncManager::SearchQForSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t aStartIdx, int32_t* aIndex) {
+ if (aIndex) *aIndex = -1;
+
+ if (aAutoSyncStateObj) {
+ bool isSibling;
+ int32_t elemCount = aQueue.Count();
+ for (int32_t idx = aStartIdx; idx < elemCount; idx++) {
+ nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling);
+
+ if (NS_SUCCEEDED(rv) && isSibling && aAutoSyncStateObj != aQueue[idx]) {
+ if (aIndex) *aIndex = idx;
+
+ return aQueue[idx];
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Searches for the next folder owned by the same imap server in the given
+ * queue, starting from the index of the given folder.
+ */
+nsIAutoSyncState* nsAutoSyncManager::GetNextSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) {
+ if (aIndex) *aIndex = -1;
+
+ if (aAutoSyncStateObj) {
+ bool located = false;
+ bool isSibling;
+ int32_t elemCount = aQueue.Count();
+ for (int32_t idx = 0; idx < elemCount; idx++) {
+ if (!located) {
+ located = (aAutoSyncStateObj == aQueue[idx]);
+ continue;
+ }
+
+ nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling);
+ if (NS_SUCCEEDED(rv) && isSibling) {
+ if (aIndex) *aIndex = idx;
+
+ return aQueue[idx];
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Checks whether there is another folder in the given q that is owned
+ * by the same imap server or not.
+ *
+ * @param aQueue the queue that will be searched for a sibling
+ * @param aAutoSyncStateObj the auto-sync state object that we are looking
+ * a sibling for
+ * @param aState the state of the sibling. -1 means "any state"
+ * @param aIndex [out] the index of the found sibling, if it is provided by the
+ * caller (not null)
+ * @return true if found, false otherwise
+ */
+bool nsAutoSyncManager::DoesQContainAnySiblingOf(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, const int32_t aState,
+ int32_t* aIndex) {
+ if (aState == -1)
+ return (nullptr != SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex));
+
+ int32_t offset = 0;
+ nsIAutoSyncState* autoSyncState;
+ while ((autoSyncState =
+ SearchQForSibling(aQueue, aAutoSyncStateObj, offset, &offset))) {
+ int32_t state;
+ nsresult rv = autoSyncState->GetState(&state);
+ if (NS_SUCCEEDED(rv) && aState == state) break;
+
+ offset++;
+ }
+ if (aIndex) *aIndex = offset;
+
+ return (nullptr != autoSyncState);
+}
+
+/**
+ * Searches the given queue for the highest priority folder owned by the
+ * same imap server.
+ */
+nsIAutoSyncState* nsAutoSyncManager::GetHighestPrioSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) {
+ return SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex);
+}
+
+// to chain update folder actions
+NS_IMETHODIMP nsAutoSyncManager::OnStartRunningUrl(nsIURI* aUrl) {
+ return NS_OK;
+}
+
+/**
+ * This is called when an update folder URL finishes. It is also called by
+ * nsAutoSyncState::OnStopRunningUrl when a folder status URL finishes.
+ */
+NS_IMETHODIMP nsAutoSyncManager::OnStopRunningUrl(nsIURI* aUrl,
+ nsresult aExitCode) {
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString uri;
+ if (aUrl) uri = aUrl->GetSpecOrDefault();
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("nsAutoSyncManager::%s, count=%d, url=%s", __func__,
+ mUpdateQ.Count(), uri.get()));
+ }
+ mUpdateInProgress = false; // Set false to allow next folder to update
+ if (mUpdateQ.Count() > 0) mUpdateQ.RemoveObjectAt(0);
+
+ return aExitCode;
+}
+
+/**
+ * This occurs on system sleep, hibernate or when TB is set offline or shutdown.
+ */
+NS_IMETHODIMP nsAutoSyncManager::Pause() {
+ StopTimer();
+ mPaused = true;
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("autosync paused"));
+ return NS_OK;
+}
+
+/**
+ * This occurs on wakeup from sleep or hibernate and when TB is returned online.
+ */
+NS_IMETHODIMP nsAutoSyncManager::Resume() {
+ mPaused = false;
+ StartTimerIfNeeded();
+ // If mUpdateInProgress was true on resume it needs to be reset back to false
+ // to avoid inhibiting autosync until a restart. OnStopRunningUrl(), where it
+ // is normally reset, may not occur depending on timing and autosync will
+ // never be initiated in TimerCallback() for any folder.
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("autosync resumed, mUpdateInProgress=%d(bool)", mUpdateInProgress));
+ mUpdateInProgress = false; // May already be false, that's OK
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::Observe(nsISupports*, const char* aTopic,
+ const char16_t* aSomeData) {
+ if (!PL_strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ observerService->RemoveObserver(this, kAppIdleNotification);
+ observerService->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
+ observerService->RemoveObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC);
+ observerService->RemoveObserver(this, kStartupDoneNotification);
+ }
+
+ // cancel and release the timer
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ // unsubscribe from idle service
+ if (mIdleService) mIdleService->RemoveIdleObserver(this, kIdleTimeInSec);
+
+ return NS_OK;
+ }
+
+ if (!PL_strcmp(aTopic, kStartupDoneNotification)) {
+ mStartupDone = true;
+ } else if (!PL_strcmp(aTopic, kAppIdleNotification)) {
+ if (nsDependentString(aSomeData).EqualsLiteral("idle")) {
+ IdleState prevIdleState = GetIdleState();
+
+ // we were already idle (either system or app), so
+ // just remember that we're app idle and return.
+ SetIdleState(appIdle);
+ if (prevIdleState != notIdle) return NS_OK;
+
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in app idle", __func__));
+ return StartIdleProcessing();
+ }
+
+ // we're back from appIdle - if already notIdle, just return;
+ if (GetIdleState() == notIdle) return NS_OK;
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of app idle", __func__));
+
+ SetIdleState(notIdle);
+ NOTIFY_LISTENERS(OnStateChanged, (false));
+ return NS_OK;
+ } else if (!PL_strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) {
+ if (nsDependentString(aSomeData).EqualsLiteral(NS_IOSERVICE_ONLINE))
+ Resume();
+ } else if (!PL_strcmp(aTopic, NS_IOSERVICE_GOING_OFFLINE_TOPIC)) {
+ Pause();
+ }
+ // we're back from system idle
+ else if (!PL_strcmp(aTopic, "back")) {
+ // if we're app idle when we get back from system idle, we ignore
+ // it, since we'll keep doing our idle stuff.
+ if (GetIdleState() != appIdle) {
+ SetIdleState(notIdle);
+ NOTIFY_LISTENERS(OnStateChanged, (false));
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of idle", __func__));
+ }
+ return NS_OK;
+ } else // we've gone system idle
+ {
+ // Check if we were already idle. We may have gotten
+ // multiple system idle notificatons. In that case,
+ // just remember that we're systemIdle and return;
+ if (GetIdleState() != notIdle) return NS_OK;
+
+ // we might want to remember if we were app idle, because
+ // coming back from system idle while app idle shouldn't stop
+ // app indexing. But I think it's OK for now just leave ourselves
+ // in appIdle state.
+ if (GetIdleState() != appIdle) SetIdleState(systemIdle);
+ if (WeAreOffline()) return NS_OK;
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in sys idle", __func__));
+ return StartIdleProcessing();
+ }
+ return NS_OK;
+}
+
+nsresult nsAutoSyncManager::StartIdleProcessing() {
+ if (mPaused) return NS_OK;
+
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__));
+ StartTimerIfNeeded();
+
+ // Ignore idle events sent during the startup
+ if (!mStartupDone) return NS_OK;
+
+ // notify listeners that auto-sync is running
+ NOTIFY_LISTENERS(OnStateChanged, (true));
+
+ nsCOMArray<nsIAutoSyncState> chainedQ;
+ nsCOMArray<nsIAutoSyncState>* queue = &mPriorityQ;
+ if (mDownloadModel == dmChained) {
+ ChainFoldersInQ(mPriorityQ, chainedQ);
+ queue = &chainedQ;
+ }
+
+ // to store the folders that should be removed from the priority
+ // queue at the end of the iteration.
+ nsCOMArray<nsIAutoSyncState> foldersToBeRemoved;
+
+ // process folders in the priority queue
+ int32_t elemCount = queue->Count();
+ for (int32_t idx = 0; idx < elemCount; idx++) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj((*queue)[idx]);
+ if (!autoSyncStateObj) continue;
+
+ int32_t state;
+ autoSyncStateObj->GetState(&state);
+
+ // TODO: Test cached-connection availability in parallel mode
+ // and do not exceed (cached-connection count - 1)
+
+ if (state != nsAutoSyncState::stReadyToDownload) continue;
+
+ nsresult rv = DownloadMessagesForOffline(autoSyncStateObj);
+ if (NS_FAILED(rv)) {
+ // special case: this folder does not have any message to download
+ // (see bug 457342), remove it explicitly from the queue when iteration
+ // is over.
+ // Note that in normal execution flow, folders are removed from priority
+ // queue only in OnDownloadCompleted when all messages are downloaded
+ // successfully. This is the only place we change this flow.
+ if (NS_ERROR_NOT_AVAILABLE == rv)
+ foldersToBeRemoved.AppendObject(autoSyncStateObj);
+
+ HandleDownloadErrorFor(autoSyncStateObj, rv);
+ } // endif
+ } // endfor
+
+ // remove folders with no pending messages from the priority queue
+ elemCount = foldersToBeRemoved.Count();
+ for (int32_t idx = 0; idx < elemCount; idx++) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(foldersToBeRemoved[idx]);
+ if (!autoSyncStateObj) continue;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder));
+
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder=%s has no pending msgs, "
+ "remove from priority q",
+ __func__, folderName.get()));
+ }
+ autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle);
+
+ if (mPriorityQ.RemoveObject(autoSyncStateObj))
+ NOTIFY_LISTENERS(OnFolderRemovedFromQ,
+ (nsIAutoSyncMgrListener::PriorityQueue, folder));
+ }
+
+ return AutoUpdateFolders();
+}
+
+/**
+ * Updates offline imap folders that are not synchronized recently. This is
+ * called whenever we're idle.
+ */
+nsresult nsAutoSyncManager::AutoUpdateFolders() {
+ nsresult rv;
+
+ // iterate through each imap account and update offline folders automatically
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__));
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgAccount>> accounts;
+ rv = accountManager->GetAccounts(accounts);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto account : accounts) {
+ if (!account) continue;
+
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = account->GetIncomingServer(getter_AddRefs(incomingServer));
+ if (!incomingServer) continue;
+
+ nsCString type;
+ rv = incomingServer->GetType(type);
+
+ if (!type.EqualsLiteral("imap")) continue;
+
+ // If we haven't logged onto this server yet during this session or if the
+ // password has been removed from cache (see
+ // nsImapIncomingServer::ForgetSessionPassword) then skip autosync for
+ // this account.
+ bool notLoggedIn;
+ incomingServer->GetServerRequiresPasswordForBiff(&notLoggedIn);
+ if (notLoggedIn) {
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString serverName;
+ incomingServer->GetHostName(serverName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: server |%s| don't autosync; not yet logged in", __func__,
+ serverName.get()));
+ }
+ continue;
+ }
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ rv = incomingServer->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ if (NS_FAILED(rv)) continue;
+
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = rootFolder->GetDescendants(allDescendants);
+
+ // Get the update time in minutes for each folder of this account/server.
+ // It will be the user configured biff time for server even if user has
+ // disabled "Check for new messages every X minutes" for the account.
+ // Update time will default to 10 minutes if an invalid value is set or
+ // if there are errors obtaining it.
+ // Specifically, the value used here is mail.server.serverX.check_time
+ // or the default mail.server.default.check_time.
+ int32_t updateMinutes = -1;
+ rv = incomingServer->GetBiffMinutes(&updateMinutes);
+ if (NS_FAILED(rv) || updateMinutes < 1)
+ updateMinutes = kDefaultUpdateInterval;
+ PRTime span = updateMinutes * (PR_USEC_PER_SEC * 60UL);
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString serverName;
+ incomingServer->GetHostName(serverName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: Update time set to |%d| minutes for "
+ "folders in account |%s|",
+ __func__, updateMinutes, serverName.get()));
+ }
+
+ for (auto folder : allDescendants) {
+ uint32_t folderFlags;
+ rv = folder->GetFlags(&folderFlags);
+ // Skip this folder if not offline or is a saved search or is no select.
+ if (NS_FAILED(rv) || !(folderFlags & nsMsgFolderFlags::Offline) ||
+ folderFlags &
+ (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect))
+ continue;
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(folder, &rv);
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) {
+ bool autoSyncOfflineStores = false;
+ rv = imapServer->GetAutoSyncOfflineStores(&autoSyncOfflineStores);
+
+ // skip if AutoSyncOfflineStores pref is not set for this folder
+ if (NS_FAILED(rv) || !autoSyncOfflineStores) continue;
+ }
+
+ nsCOMPtr<nsIAutoSyncState> autoSyncState;
+ rv = imapFolder->GetAutoSyncStateObj(getter_AddRefs(autoSyncState));
+ NS_ASSERTION(
+ autoSyncState,
+ "*** nsAutoSyncState shouldn't be NULL, check owner folder");
+
+ // shouldn't happen but let's be defensive here
+ if (!autoSyncState) continue;
+
+ int32_t state;
+ rv = autoSyncState->GetState(&state);
+ nsCString folderName;
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ folder->GetURI(folderName);
+ MOZ_LOG(
+ gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder=%s, state=%d", __func__, folderName.get(), state));
+ }
+ if (state == nsAutoSyncState::stCompletedIdle ||
+ state == nsAutoSyncState::stUpdateNeeded ||
+ state == nsAutoSyncState::stUpdateIssued) {
+ // Ensure that we wait for at least the "span" time set above before
+ // queuing an update of the same folder.
+ PRTime lastUpdateTime;
+ rv = autoSyncState->GetLastUpdateTime(&lastUpdateTime);
+ if (NS_SUCCEEDED(rv) && ((lastUpdateTime + span) < PR_Now())) {
+ int32_t idx = mUpdateQ.IndexOf(autoSyncState);
+ if (state == nsAutoSyncState::stUpdateIssued) {
+ // Handle the case where an update is triggered but nothing is
+ // found to download. This can happen after messages are copied
+ // or moved between offline folders of the same server or if imap
+ // "folderstatus" URL triggers an update but no new messages
+ // are detected.
+ bool downloadQEmpty;
+ autoSyncState->IsDownloadQEmpty(&downloadQEmpty);
+ if (downloadQEmpty) {
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: nothing to download for folder %s, "
+ "set state to stCompletedIdle, updateQ idx=%d",
+ __func__, folderName.get(), idx));
+ autoSyncState->SetState(nsAutoSyncState::stCompletedIdle);
+
+ // This should already be done by
+ // nsAutoSyncManager::OnStopRunningUrl() but set update state to
+ // completed and remove folder state object from update queue in
+ // case OnStopRunningUrl never occurred.
+ mUpdateInProgress = false;
+ if (idx > -1) {
+ mUpdateQ.RemoveObjectAt(idx);
+ idx = -1; // re-q below
+ }
+ } else {
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: downloadQ not empty. Why? updateQ idx=%d",
+ __func__, idx));
+ if (idx > -1) {
+ // Download q not empty and folder still on update q, maybe it
+ // just needs more time so leave update q as it is to update
+ // on next "span" interval. (Never seen this happen.)
+ idx = 0;
+ }
+ }
+ }
+ // Now q or re-q the update for this folder unless it's still q'd.
+ if (idx < 0) {
+ mUpdateQ.AppendObject(autoSyncState);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder=%s added to update q", __func__,
+ folderName.get()));
+ if (folder)
+ NOTIFY_LISTENERS(OnFolderAddedIntoQ,
+ (nsIAutoSyncMgrListener::UpdateQueue, folder));
+ }
+ }
+ }
+
+ // Check if time to add folder to discovery q on kAutoSyncFreq (1 hour)
+ // time base.
+ PRTime lastSyncTime;
+ rv = autoSyncState->GetLastSyncTime(&lastSyncTime);
+ if (NS_SUCCEEDED(rv) && ((lastSyncTime + kAutoSyncFreq) < PR_Now())) {
+ // add this folder into discovery queue to process existing headers
+ // and discover messages not downloaded yet
+ if (mDiscoveryQ.IndexOf(autoSyncState) == -1) {
+ mDiscoveryQ.AppendObject(autoSyncState);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder=%s added to discovery q", __func__,
+ folderName.get()));
+ if (folder)
+ NOTIFY_LISTENERS(
+ OnFolderAddedIntoQ,
+ (nsIAutoSyncMgrListener::DiscoveryQueue, folder));
+ }
+ }
+ } // endfor
+ } // endif
+ } // endfor
+
+ // lazily create the timer if there is something to process in the queue
+ // when timer is done, it will self destruct
+ StartTimerIfNeeded();
+
+ return rv;
+}
+
+/**
+ * Places the given folder into the priority queue based on active
+ * strategy function.
+ */
+void nsAutoSyncManager::ScheduleFolderForOfflineDownload(
+ nsIAutoSyncState* aAutoSyncStateObj) {
+ if (aAutoSyncStateObj && (mPriorityQ.IndexOf(aAutoSyncStateObj) == -1)) {
+ nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy;
+ GetFolderStrategy(getter_AddRefs(folStrategy));
+
+ if (mPriorityQ.Count() <= 0) {
+ // make sure that we don't insert a folder excluded by the given strategy
+ nsCOMPtr<nsIMsgFolder> folder;
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder) {
+ bool excluded = false;
+ if (folStrategy) folStrategy->IsExcluded(folder, &excluded);
+
+ if (!excluded) {
+ mPriorityQ.AppendObject(
+ aAutoSyncStateObj); // insert into the first spot
+ NOTIFY_LISTENERS(OnFolderAddedIntoQ,
+ (nsIAutoSyncMgrListener::PriorityQueue, folder));
+ }
+ }
+ } else {
+ // find the right spot for the given folder
+ uint32_t qidx = mPriorityQ.Count();
+ while (qidx > 0) {
+ --qidx;
+
+ nsCOMPtr<nsIMsgFolder> folderA, folderB;
+ mPriorityQ[qidx]->GetOwnerFolder(getter_AddRefs(folderA));
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folderB));
+
+ bool excluded = false;
+ if (folderB && folStrategy) folStrategy->IsExcluded(folderB, &excluded);
+
+ if (excluded) break;
+
+ nsAutoSyncStrategyDecisionType decision =
+ nsAutoSyncStrategyDecisions::Same;
+ if (folderA && folderB && folStrategy)
+ folStrategy->Sort(folderA, folderB, &decision);
+
+ if (decision == nsAutoSyncStrategyDecisions::Higher && 0 == qidx)
+ mPriorityQ.InsertObjectAt(aAutoSyncStateObj, 0);
+ else if (decision == nsAutoSyncStrategyDecisions::Higher)
+ continue;
+ else if (decision == nsAutoSyncStrategyDecisions::Lower)
+ mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx + 1);
+ else // decision == nsAutoSyncStrategyDecisions::Same
+ mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx);
+
+ NOTIFY_LISTENERS(OnFolderAddedIntoQ,
+ (nsIAutoSyncMgrListener::PriorityQueue, folderB));
+ break;
+ } // end while
+ }
+ } // endif
+}
+
+/**
+ * Zero aSizeLimit means no limit
+ */
+nsresult nsAutoSyncManager::DownloadMessagesForOffline(
+ nsIAutoSyncState* aAutoSyncStateObj, uint32_t aSizeLimit) {
+ if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG;
+
+ int32_t count;
+ nsresult rv = aAutoSyncStateObj->GetPendingMessageCount(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // special case: no more message to download for this folder:
+ // see HandleDownloadErrorFor for recovery policy
+ if (!count) return NS_ERROR_NOT_AVAILABLE;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messagesToDownload;
+ uint32_t totalSize = 0;
+ rv = aAutoSyncStateObj->GetNextGroupOfMessages(mGroupSize, &totalSize,
+ messagesToDownload);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // there are pending messages but the cumulative size is zero:
+ // treat as special case.
+ // Note that although it shouldn't happen, we know that sometimes
+ // imap servers manifest messages as zero length. By returning
+ // NS_ERROR_NOT_AVAILABLE we cause this folder to be removed from
+ // the priority queue temporarily (until the next idle or next update)
+ // in an effort to prevent it blocking other folders of the same account
+ // being synced.
+ if (!totalSize) return NS_ERROR_NOT_AVAILABLE;
+
+ // ensure that we don't exceed the given size limit for this particular group
+ if (aSizeLimit && aSizeLimit < totalSize) return NS_ERROR_FAILURE;
+
+ if (!messagesToDownload.IsEmpty()) {
+ rv = aAutoSyncStateObj->DownloadMessagesForOffline(messagesToDownload);
+
+ int32_t totalCount;
+ (void)aAutoSyncStateObj->GetTotalMessageCount(&totalCount);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder)
+ NOTIFY_LISTENERS(OnDownloadStarted,
+ (folder, messagesToDownload.Length(), totalCount));
+ }
+
+ return rv;
+}
+
+// clang-format off
+/**
+ * Assuming that the download operation on the given folder has been failed at
+ * least once, execute these steps:
+ * - put the auto-sync state into ready-to-download mode
+ * - rollback the message offset so we can try the same group again (unless the
+ * retry count is reached to the given limit)
+ * - if parallel model is active, wait to be resumed by the next idle
+ * - if chained model is active, search the priority queue to find a sibling to
+ * continue with.
+ */
+// clang-format on
+nsresult nsAutoSyncManager::HandleDownloadErrorFor(
+ nsIAutoSyncState* aAutoSyncStateObj, const nsresult error) {
+ if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG;
+
+ // ensure that an error occurred
+ if (NS_SUCCEEDED(error)) return NS_OK;
+
+ // NS_ERROR_NOT_AVAILABLE is a special case/error happens when the queued
+ // folder doesn't have any message to download (see bug 457342). In such case
+ // we shouldn't retry the current message group, nor notify listeners. Simply
+ // continuing with the next sibling in the priority queue would suffice.
+
+ if (NS_ERROR_NOT_AVAILABLE != error) {
+ // force the auto-sync state to try downloading the same group at least
+ // kGroupRetryCount times before it moves to the next one
+ aAutoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder) NOTIFY_LISTENERS(OnDownloadError, (folder));
+ }
+
+ // if parallel model, don't do anything else
+
+ if (mDownloadModel == dmChained) {
+ // switch to the next folder in the chain and continue downloading
+ nsIAutoSyncState* autoSyncStateObj = aAutoSyncStateObj;
+ nsIAutoSyncState* nextAutoSyncStateObj = nullptr;
+ while (
+ (nextAutoSyncStateObj = GetNextSibling(mPriorityQ, autoSyncStateObj))) {
+ autoSyncStateObj = nextAutoSyncStateObj;
+ nsresult rv = DownloadMessagesForOffline(autoSyncStateObj);
+ if (NS_SUCCEEDED(rv)) break;
+ if (rv == NS_ERROR_NOT_AVAILABLE)
+ // next folder in the chain also doesn't have any message to download
+ // switch to next one if any
+ continue;
+ autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::GetGroupSize(uint32_t* aGroupSize) {
+ NS_ENSURE_ARG_POINTER(aGroupSize);
+ *aGroupSize = mGroupSize;
+ return NS_OK;
+}
+NS_IMETHODIMP nsAutoSyncManager::SetGroupSize(uint32_t aGroupSize) {
+ mGroupSize = aGroupSize ? aGroupSize : kDefaultGroupSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::GetMsgStrategy(
+ nsIAutoSyncMsgStrategy** aMsgStrategy) {
+ NS_ENSURE_ARG_POINTER(aMsgStrategy);
+
+ // lazily create if it is not done already
+ if (!mMsgStrategyImpl) {
+ mMsgStrategyImpl = new nsDefaultAutoSyncMsgStrategy;
+ if (!mMsgStrategyImpl) return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ NS_IF_ADDREF(*aMsgStrategy = mMsgStrategyImpl);
+ return NS_OK;
+}
+NS_IMETHODIMP nsAutoSyncManager::SetMsgStrategy(
+ nsIAutoSyncMsgStrategy* aMsgStrategy) {
+ mMsgStrategyImpl = aMsgStrategy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::GetFolderStrategy(
+ nsIAutoSyncFolderStrategy** aFolderStrategy) {
+ NS_ENSURE_ARG_POINTER(aFolderStrategy);
+
+ // lazily create if it is not done already
+ if (!mFolderStrategyImpl) {
+ mFolderStrategyImpl = new nsDefaultAutoSyncFolderStrategy;
+ if (!mFolderStrategyImpl) return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ NS_IF_ADDREF(*aFolderStrategy = mFolderStrategyImpl);
+ return NS_OK;
+}
+NS_IMETHODIMP nsAutoSyncManager::SetFolderStrategy(
+ nsIAutoSyncFolderStrategy* aFolderStrategy) {
+ mFolderStrategyImpl = aFolderStrategy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoSyncManager::DoesMsgFitDownloadCriteria(nsIMsgDBHdr* aMsgHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ uint32_t msgFlags = 0;
+ aMsgHdr->GetFlags(&msgFlags);
+
+ // check whether this message is marked imap deleted or not
+ *aResult = !(msgFlags & nsMsgMessageFlags::IMAPDeleted);
+ if (!(*aResult)) return NS_OK;
+
+ bool shouldStoreMsgOffline = true;
+ nsCOMPtr<nsIMsgFolder> folder;
+ aMsgHdr->GetFolder(getter_AddRefs(folder));
+ if (folder) {
+ nsMsgKey msgKey;
+ nsresult rv = aMsgHdr->GetMessageKey(&msgKey);
+ // a cheap way to get the size limit for this folder and make
+ // sure that we don't have this message offline already
+ if (NS_SUCCEEDED(rv))
+ folder->ShouldStoreMsgOffline(msgKey, &shouldStoreMsgOffline);
+ }
+
+ *aResult &= shouldStoreMsgOffline;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::OnDownloadQChanged(
+ nsIAutoSyncState* aAutoSyncStateObj) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
+ if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;
+
+ if (mPaused) return NS_OK;
+ // We want to start downloading immediately unless the folder is excluded.
+ bool excluded = false;
+ nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy;
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ GetFolderStrategy(getter_AddRefs(folStrategy));
+ autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+
+ if (folder && folStrategy) folStrategy->IsExcluded(folder, &excluded);
+
+ nsresult rv = NS_OK;
+
+ if (!excluded) {
+ // Add this folder into the priority queue.
+ autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);
+ ScheduleFolderForOfflineDownload(autoSyncStateObj);
+
+ // If we operate in parallel mode or if there is no sibling downloading
+ // messages at the moment, we can download the first group of the messages
+ // for this folder
+ if (mDownloadModel == dmParallel ||
+ !DoesQContainAnySiblingOf(mPriorityQ, autoSyncStateObj,
+ nsAutoSyncState::stDownloadInProgress)) {
+ // this will download the first group of messages immediately;
+ // to ensure that we don't end up downloading a large single message in
+ // not-idle time, we enforce a limit. If there is no message fits into
+ // this limit we postpone the download until the next idle.
+ if (GetIdleState() == notIdle)
+ rv = DownloadMessagesForOffline(autoSyncStateObj, kFirstGroupSizeLimit);
+ else
+ rv = DownloadMessagesForOffline(autoSyncStateObj);
+
+ if (NS_FAILED(rv))
+ autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAutoSyncManager::OnDownloadStarted(nsIAutoSyncState* aAutoSyncStateObj,
+ nsresult aStartCode) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
+ if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;
+
+ // resume downloads during next idle time
+ if (NS_FAILED(aStartCode))
+ autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);
+
+ return aStartCode;
+}
+
+NS_IMETHODIMP
+nsAutoSyncManager::OnDownloadCompleted(nsIAutoSyncState* aAutoSyncStateObj,
+ nsresult aExitCode) {
+ nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
+ if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = aExitCode;
+
+ if (NS_FAILED(aExitCode)) {
+ // retry the same group kGroupRetryCount times
+ // try again if TB still idle, otherwise wait for the next idle time
+ autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
+ if (GetIdleState() != notIdle) {
+ rv = DownloadMessagesForOffline(autoSyncStateObj);
+ if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(autoSyncStateObj, rv);
+ }
+ return rv;
+ }
+
+ // download is successful, reset the retry counter of the folder
+ autoSyncStateObj->ResetRetryCounter();
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder));
+
+ int32_t count;
+ rv = autoSyncStateObj->GetPendingMessageCount(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIAutoSyncState* nextFolderToDownload = nullptr;
+ if (count > 0) {
+ autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);
+
+ // in parallel model, we continue downloading the same folder as long as it
+ // has more pending messages
+ nextFolderToDownload = autoSyncStateObj;
+
+ // in chained model, ensure that we are always downloading the highest
+ // priority folder first
+ if (mDownloadModel == dmChained) {
+ // switch to higher priority folder and continue to download,
+ // if any added recently
+ int32_t myIndex = mPriorityQ.IndexOf(autoSyncStateObj);
+
+ int32_t siblingIndex;
+ nsIAutoSyncState* sibling =
+ GetHighestPrioSibling(mPriorityQ, autoSyncStateObj, &siblingIndex);
+
+ // lesser index = higher priority
+ if (sibling && myIndex > -1 && siblingIndex < myIndex)
+ nextFolderToDownload = sibling;
+ }
+ } else {
+ autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+
+ if (NS_SUCCEEDED(rv) && mPriorityQ.RemoveObject(autoSyncStateObj))
+ NOTIFY_LISTENERS(OnFolderRemovedFromQ,
+ (nsIAutoSyncMgrListener::PriorityQueue, folder));
+
+ // find the next folder owned by the same server in the queue and continue
+ // downloading
+ if (mDownloadModel == dmChained)
+ nextFolderToDownload =
+ GetHighestPrioSibling(mPriorityQ, autoSyncStateObj);
+
+ } // endif
+
+ // continue downloading if TB is still in idle state
+ if (nextFolderToDownload && GetIdleState() != notIdle) {
+ rv = DownloadMessagesForOffline(nextFolderToDownload);
+ if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(nextFolderToDownload, rv);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::GetDownloadModel(int32_t* aDownloadModel) {
+ NS_ENSURE_ARG_POINTER(aDownloadModel);
+ *aDownloadModel = mDownloadModel;
+ return NS_OK;
+}
+NS_IMETHODIMP nsAutoSyncManager::SetDownloadModel(int32_t aDownloadModel) {
+ mDownloadModel = aDownloadModel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::AddListener(
+ nsIAutoSyncMgrListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ mListeners.AppendElementUnlessExists(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncManager::RemoveListener(
+ nsIAutoSyncMgrListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ mListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long discoveryQLength; */
+NS_IMETHODIMP nsAutoSyncManager::GetDiscoveryQLength(
+ uint32_t* aDiscoveryQLength) {
+ NS_ENSURE_ARG_POINTER(aDiscoveryQLength);
+ *aDiscoveryQLength = mDiscoveryQ.Count();
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long uploadQLength; */
+NS_IMETHODIMP nsAutoSyncManager::GetUpdateQLength(uint32_t* aUpdateQLength) {
+ NS_ENSURE_ARG_POINTER(aUpdateQLength);
+ *aUpdateQLength = mUpdateQ.Count();
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long downloadQLength; */
+NS_IMETHODIMP nsAutoSyncManager::GetDownloadQLength(
+ uint32_t* aDownloadQLength) {
+ NS_ENSURE_ARG_POINTER(aDownloadQLength);
+ *aDownloadQLength = mPriorityQ.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoSyncManager::OnFolderHasPendingMsgs(nsIAutoSyncState* aAutoSyncStateObj) {
+ NS_ENSURE_ARG_POINTER(aAutoSyncStateObj);
+ if (mUpdateQ.IndexOf(aAutoSyncStateObj) == -1) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
+ // If this folder isn't the trash, add it to the update q.
+ if (folder) {
+ bool isTrash;
+ folder->GetFlag(nsMsgFolderFlags::Trash, &isTrash);
+ if (!isTrash) {
+ bool isSentOrArchive;
+ folder->IsSpecialFolder(
+ nsMsgFolderFlags::SentMail | nsMsgFolderFlags::Archive, true,
+ &isSentOrArchive);
+ // Sent or archive folders go to the q front, the rest to the end.
+ if (isSentOrArchive)
+ mUpdateQ.InsertObjectAt(aAutoSyncStateObj, 0);
+ else
+ mUpdateQ.AppendObject(aAutoSyncStateObj);
+ aAutoSyncStateObj->SetState(nsAutoSyncState::stUpdateNeeded);
+ NOTIFY_LISTENERS(OnFolderAddedIntoQ,
+ (nsIAutoSyncMgrListener::UpdateQueue, folder));
+ }
+ }
+ }
+ return NS_OK;
+}
+
+void nsAutoSyncManager::SetIdleState(IdleState st) { mIdleState = st; }
+
+nsAutoSyncManager::IdleState nsAutoSyncManager::GetIdleState() const {
+ return mIdleState;
+}
+
+NS_IMPL_ISUPPORTS(nsAutoSyncManager, nsIObserver, nsIUrlListener,
+ nsIAutoSyncManager)
diff --git a/comm/mailnews/imap/src/nsAutoSyncManager.h b/comm/mailnews/imap/src/nsAutoSyncManager.h
new file mode 100644
index 0000000000..fb77b66a77
--- /dev/null
+++ b/comm/mailnews/imap/src/nsAutoSyncManager.h
@@ -0,0 +1,265 @@
+/* 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/. */
+
+#ifndef nsAutoSyncManager_h__
+# define nsAutoSyncManager_h__
+
+# include "nsString.h"
+# include "nsCOMArray.h"
+# include "nsIObserver.h"
+# include "nsIUrlListener.h"
+# include "nsITimer.h"
+# include "nsTObserverArray.h"
+# include "nsIAutoSyncManager.h"
+# include "nsIAutoSyncMsgStrategy.h"
+# include "nsIAutoSyncFolderStrategy.h"
+# include "nsIUserIdleService.h"
+# include "prtime.h"
+
+// clang-format off
+/* Auto-Sync
+ *
+ * Background:
+ * it works only with offline imap folders. "autosync_offline_stores" pref
+ * enables/disables auto-sync mechanism. Note that setting "autosync_offline_stores"
+ * to false, or setting folder to not-offline doesn't stop synchronization
+ * process for already queued folders.
+ *
+ * Auto-Sync policy:
+ * o It kicks in during system idle time, and tries to download as much messages
+ * as possible based on given folder and message prioritization strategies/rules.
+ * Default folder prioritization strategy dictates to sort the folders based on the
+ * following order: INBOX > DRAFTS > SUBFOLDERS > TRASH.
+ * Similarly, default message prioritization strategy dictates to download the most
+ * recent and smallest message first. Also, by sorting the messages by size in the
+ * queue, it tries to maximize the number of messages downloaded.
+ * o It downloads the messages in groups. Default groups size is defined by |kDefaultGroupSize|.
+ * o It downloads the messages larger than the group size one-by-one.
+ * o If new messages arrive when not idle, it downloads the messages that do fit into
+ * |kFirstGroupSizeLimit| size limit immediately, without waiting for idle time, unless there is
+ * a sibling (a folder owned by the same imap server) in stDownloadInProgress state in the q
+ * o If new messages arrive when idle, it downloads all the messages without any restriction.
+ * o If new messages arrive into a folder while auto-sync is downloading other messages of the
+ * same folder, it simply puts the new messages into the folder's download queue, and
+ * re-prioritize the messages. That behavior makes sure that the high priority
+ * (defined by the message strategy) get downloaded first always.
+ * o If new messages arrive into a folder while auto-sync is downloading messages of a lower
+ * priority folder, auto-sync switches the folders in the queue and starts downloading the
+ * messages of the higher priority folder next time it downloads a message group.
+ * o Currently there is no way to stop/pause/cancel a message download. The smallest
+ * granularity is the message group size.
+ * o Auto-Sync manager periodically (kAutoSyncFreq) checks folder for existing messages
+ * w/o bodies. It persists the last time the folder is checked in the local database of the
+ * folder. We call this process 'Discovery'. This process is asynchronous and processes
+ * |kNumberOfHeadersToProcess| number of headers at each cycle. Since it works on local data,
+ * it doesn't consume lots of system resources, it does its job fast.
+ * o Discovery is necessary especially when the user makes a transition from not-offline
+ * to offline mode.
+ * o Update frequency is defined by nsMsgIncomingServer::BiffMinutes.
+ *
+ * Error Handling:
+ * o if the user moves/deletes/filters all messages of a folder already queued, auto-sync
+ * deals with that situation by skipping the folder in question, and continuing with the
+ * next in chain.
+ * o If the message size is zero, auto-sync ignores the message.
+ * o If the download of the message group fails for some reason, auto-sync tries to
+ * download the same group |kGroupRetryCount| times. If it still fails, continues with the
+ * next group of messages.
+ *
+ * Download Model:
+ * Parallel model should be used with the imap servers that do not have any "max number of sessions
+ * per IP" limit, and when the bandwidth is significantly large.
+ *
+ * How it really works:
+ * The AutoSyncManager gets an idle notification. First it processes any
+ * folders in the discovery queue (which means it schedules message download
+ * for any messages it previously determined it should download). Then it sets
+ * a timer, and in the timer callback, it processes the update q, by calling
+ * InitiateAutoSync on the first folder in the update q.
+ *
+ * See additional info near the bottom of this file.
+ */
+// clang-format on
+
+/**
+ * Default strategy implementation to prioritize messages in the download queue.
+ */
+class nsDefaultAutoSyncMsgStrategy final : public nsIAutoSyncMsgStrategy {
+ static const uint32_t kFirstPassMessageSize = 60U * 1024U; // 60K
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOSYNCMSGSTRATEGY
+
+ nsDefaultAutoSyncMsgStrategy();
+
+ private:
+ ~nsDefaultAutoSyncMsgStrategy();
+};
+
+/**
+ * Default strategy implementation to prioritize folders in the download queue.
+ */
+class nsDefaultAutoSyncFolderStrategy final : public nsIAutoSyncFolderStrategy {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOSYNCFOLDERSTRATEGY
+
+ nsDefaultAutoSyncFolderStrategy();
+
+ private:
+ ~nsDefaultAutoSyncFolderStrategy();
+};
+
+// see the end of the page for auto-sync internals
+
+/**
+ * Manages background message download operations for offline imap folders.
+ */
+class nsAutoSyncManager final : public nsIObserver,
+ public nsIUrlListener,
+ public nsIAutoSyncManager {
+ static const PRTime kAutoSyncFreq = 60UL * (PR_USEC_PER_SEC * 60UL); // 1hr
+ static const uint32_t kDefaultUpdateInterval = 10UL; // 10min
+ static const int32_t kTimerIntervalInMs = 400;
+ static const uint32_t kNumberOfHeadersToProcess = 250U;
+ // enforced size of the first group that will be downloaded before idle time
+ static const uint32_t kFirstGroupSizeLimit = 60U * 1024U /* 60K */;
+ static const int32_t kIdleTimeInSec = 10;
+ static const uint32_t kGroupRetryCount = 3;
+
+ enum IdleState { systemIdle, appIdle, notIdle };
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIAUTOSYNCMANAGER
+
+ nsAutoSyncManager();
+
+ private:
+ ~nsAutoSyncManager();
+
+ void SetIdleState(IdleState st);
+ IdleState GetIdleState() const;
+ nsresult StartIdleProcessing();
+ nsresult AutoUpdateFolders();
+ void ScheduleFolderForOfflineDownload(nsIAutoSyncState* aAutoSyncStateObj);
+ nsresult DownloadMessagesForOffline(nsIAutoSyncState* aAutoSyncStateObj,
+ uint32_t aSizeLimit = 0);
+ nsresult HandleDownloadErrorFor(nsIAutoSyncState* aAutoSyncStateObj,
+ const nsresult error);
+
+ // Helper methods for priority Q operations
+ static void ChainFoldersInQ(const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsCOMArray<nsIAutoSyncState>& aChainedQ);
+ static nsIAutoSyncState* SearchQForSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t aStartIdx,
+ int32_t* aIndex = nullptr);
+ static bool DoesQContainAnySiblingOf(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, const int32_t aState,
+ int32_t* aIndex = nullptr);
+ static nsIAutoSyncState* GetNextSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex = nullptr);
+ static nsIAutoSyncState* GetHighestPrioSibling(
+ const nsCOMArray<nsIAutoSyncState>& aQueue,
+ nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex = nullptr);
+
+ /// timer to process existing keys and updates
+ void InitTimer();
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+ void StopTimer();
+ void StartTimerIfNeeded();
+
+ protected:
+ nsCOMPtr<nsIAutoSyncMsgStrategy> mMsgStrategyImpl;
+ nsCOMPtr<nsIAutoSyncFolderStrategy> mFolderStrategyImpl;
+ // contains the folders that will be downloaded on background
+ nsCOMArray<nsIAutoSyncState> mPriorityQ;
+ // contains the folders that will be examined for existing headers and
+ // adds the headers we don't have offline into the autosyncState
+ // object's download queue.
+ nsCOMArray<nsIAutoSyncState> mDiscoveryQ;
+ // contains the folders that will be checked for new messages with STATUS,
+ // and if there are any, we'll call UpdateFolder on them.
+ nsCOMArray<nsIAutoSyncState> mUpdateQ;
+ // this is set true when autosync is initiated for a single folder. Its
+ // purpose is ensure that update for a folder finishes before the next one
+ // starts.
+ bool mUpdateInProgress;
+
+ // This is set if auto sync has been completely paused.
+ bool mPaused;
+ // This is set if we've finished startup and should start
+ // paying attention to idle notifications.
+ bool mStartupDone;
+
+ private:
+ uint32_t mGroupSize;
+ IdleState mIdleState;
+ int32_t mDownloadModel;
+ nsCOMPtr<nsIUserIdleService> mIdleService;
+ nsCOMPtr<nsITimer> mTimer;
+ nsTObserverArray<nsCOMPtr<nsIAutoSyncMgrListener> > mListeners;
+};
+
+#endif
+
+/*
+How queues inter-relate:
+
+nsAutoSyncState has an internal priority queue to store messages waiting to be
+downloaded. nsAutoSyncMsgStrategy object determines the order in this queue,
+nsAutoSyncManager uses this queue to manage downloads. Two events cause a
+change in this queue:
+
+1) nsImapMailFolder::HeaderFetchCompleted: is triggered when TB notices that
+there are pending messages on the server -- via IDLE command from the server,
+via explicit select from the user, or via automatic Update during idle time. If
+it turns out that there are pending messages on the server, it adds them into
+nsAutoSyncState's download queue.
+
+2) nsAutoSyncState::ProcessExistingHeaders: is triggered for every imap folder
+every hour or so (see kAutoSyncFreq). nsAutoSyncManager uses an internal queue
+called Discovery queue to keep track of this task. The purpose of
+ProcessExistingHeaders() method is to check existing headers of a given folder
+in batches and discover the messages without bodies, in asynchronous fashion.
+This process is sequential, one and only one folder at any given time, very
+similar to indexing. Again, if it turns out that the folder in hand has messages
+w/o bodies, ProcessExistingHeaders adds them into nsAutoSyncState's download
+queue.
+
+Any change in nsAutoSyncState's download queue, notifies nsAutoSyncManager and
+nsAutoSyncManager puts the requesting nsAutoSyncState into its internal
+priority queue (called mPriorityQ) -- if the folder is not already there.
+nsAutoSyncFolderStrategy object determines the order in this queue. This queue
+is processed in two modes: chained and parallel.
+
+i) Chained: One folder per imap server any given time. Folders owned by
+different imap servers are simultaneous.
+
+ii) Parallel: All folders at the same time, using all cached-connections -
+a.k.a 'Folders gone wild' mode.
+
+Note: The "Chained" mode is currently in use: mDownloadModel = dmChained;
+
+The order the folders are added into the mPriorityQ doesn't matter since every
+time a batch completed for an imap server, nsAutoSyncManager adjusts the order.
+So, lets say that updating a sub-folder starts downloading message immediately,
+when an higher priority folder is added into the queue, nsAutoSyncManager
+switches to this higher priority folder instead of processing the next group of
+messages of the lower priority one. Setting group size too high might delay
+this switch at worst.
+
+And finally, Update queue helps nsAutoSyncManager to keep track of folders
+waiting to be updated. With the latest change, we update one and only one
+folder at any given time. Default frequency of updating is 10 min
+(kDefaultUpdateInterval). We add folders into the update queue during idle time,
+if they are not in mPriorityQ already.
+
+*/
diff --git a/comm/mailnews/imap/src/nsAutoSyncState.cpp b/comm/mailnews/imap/src/nsAutoSyncState.cpp
new file mode 100644
index 0000000000..db3216419a
--- /dev/null
+++ b/comm/mailnews/imap/src/nsAutoSyncState.cpp
@@ -0,0 +1,782 @@
+/* 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/. */
+
+#include "nsAutoSyncState.h"
+#include "nsImapMailFolder.h"
+#include "nsIImapService.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgMailSession.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIAutoSyncManager.h"
+#include "nsIAutoSyncMsgStrategy.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+
+extern LazyLogModule gAutoSyncLog; // defined in nsAutoSyncManager.cpp
+
+MsgStrategyComparatorAdaptor::MsgStrategyComparatorAdaptor(
+ nsIAutoSyncMsgStrategy* aStrategy, nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDatabase)
+ : mStrategy(aStrategy), mFolder(aFolder), mDatabase(aDatabase) {}
+
+/** @return True if the elements are equals; false otherwise. */
+bool MsgStrategyComparatorAdaptor::Equals(const nsMsgKey& a,
+ const nsMsgKey& b) const {
+ nsCOMPtr<nsIMsgDBHdr> hdrA;
+ nsCOMPtr<nsIMsgDBHdr> hdrB;
+
+ mDatabase->GetMsgHdrForKey(a, getter_AddRefs(hdrA));
+ mDatabase->GetMsgHdrForKey(b, getter_AddRefs(hdrB));
+
+ if (hdrA && hdrB) {
+ nsresult rv = NS_OK;
+ nsAutoSyncStrategyDecisionType decision = nsAutoSyncStrategyDecisions::Same;
+
+ if (mStrategy) rv = mStrategy->Sort(mFolder, hdrA, hdrB, &decision);
+
+ if (NS_SUCCEEDED(rv))
+ return (decision == nsAutoSyncStrategyDecisions::Same);
+ }
+
+ return false;
+}
+
+/** @return True if (a < b); false otherwise. */
+bool MsgStrategyComparatorAdaptor::LessThan(const nsMsgKey& a,
+ const nsMsgKey& b) const {
+ nsCOMPtr<nsIMsgDBHdr> hdrA;
+ nsCOMPtr<nsIMsgDBHdr> hdrB;
+
+ mDatabase->GetMsgHdrForKey(a, getter_AddRefs(hdrA));
+ mDatabase->GetMsgHdrForKey(b, getter_AddRefs(hdrB));
+
+ if (hdrA && hdrB) {
+ nsresult rv = NS_OK;
+ nsAutoSyncStrategyDecisionType decision = nsAutoSyncStrategyDecisions::Same;
+
+ if (mStrategy) rv = mStrategy->Sort(mFolder, hdrA, hdrB, &decision);
+
+ if (NS_SUCCEEDED(rv))
+ return (decision == nsAutoSyncStrategyDecisions::Lower);
+ }
+
+ return false;
+}
+
+nsAutoSyncState::nsAutoSyncState(nsImapMailFolder* aOwnerFolder,
+ PRTime aLastSyncTime)
+ : mSyncState(stCompletedIdle),
+ mOffset(0U),
+ mLastOffset(0U),
+ mLastServerTotal(0),
+ mLastServerRecent(0),
+ mLastServerUnseen(0),
+ mLastNextUID(0),
+ mLastSyncTime(aLastSyncTime),
+ mLastUpdateTime(0UL),
+ mProcessPointer(0U),
+ mIsDownloadQChanged(false),
+ mRetryCounter(0U) {
+ mOwnerFolder =
+ do_GetWeakReference(static_cast<nsIMsgImapMailFolder*>(aOwnerFolder));
+ mHaveAStatusResponse = false;
+}
+
+nsAutoSyncState::~nsAutoSyncState() {}
+
+// TODO:XXXemre should be implemented when we start
+// doing space management
+nsresult nsAutoSyncState::ManageStorageSpace() { return NS_OK; }
+
+nsresult nsAutoSyncState::PlaceIntoDownloadQ(
+ const nsTArray<nsMsgKey>& aMsgKeyList) {
+ nsresult rv = NS_OK;
+ if (!aMsgKeyList.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDatabase> database;
+ rv = folder->GetMsgDatabase(getter_AddRefs(database));
+ if (!database) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy;
+ autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy));
+
+ // increase the array size
+ mDownloadQ.SetCapacity(mDownloadQ.Length() + aMsgKeyList.Length());
+
+ // remove excluded messages
+ int32_t elemCount = aMsgKeyList.Length();
+ for (int32_t idx = 0; idx < elemCount; idx++) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ bool containsKey;
+ database->ContainsKey(aMsgKeyList[idx], &containsKey);
+ if (!containsKey) continue;
+ rv = database->GetMsgHdrForKey(aMsgKeyList[idx], getter_AddRefs(hdr));
+ if (!hdr)
+ continue; // can't get message header, continue with the next one
+
+ bool doesFit = true;
+ rv = autoSyncMgr->DoesMsgFitDownloadCriteria(hdr, &doesFit);
+ if (NS_SUCCEEDED(rv) && !mDownloadSet.Contains(aMsgKeyList[idx]) &&
+ doesFit) {
+ bool excluded = false;
+ if (msgStrategy) {
+ rv = msgStrategy->IsExcluded(folder, hdr, &excluded);
+
+ if (NS_SUCCEEDED(rv) && !excluded) {
+ mIsDownloadQChanged = true;
+ mDownloadSet.PutEntry(aMsgKeyList[idx]);
+ mDownloadQ.AppendElement(aMsgKeyList[idx]);
+ }
+ }
+ }
+ } // endfor
+
+ if (mIsDownloadQChanged) {
+ LogOwnerFolderName("Download Q is created for ");
+ LogQWithSize(mDownloadQ, 0);
+ rv = autoSyncMgr->OnDownloadQChanged(this);
+ }
+ }
+ return rv;
+}
+
+nsresult nsAutoSyncState::SortQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDatabase> database;
+ rv = folder->GetMsgDatabase(getter_AddRefs(database));
+ if (!database) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy;
+ rv = autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MsgStrategyComparatorAdaptor strategyComp(msgStrategy, folder, database);
+ aQueue.Sort(strategyComp);
+
+ return rv;
+}
+
+// This method is a hack to prioritize newly inserted messages,
+// without changing the size of the queue. It is required since
+// we cannot sort ranges in nsTArray.
+nsresult nsAutoSyncState::SortSubQueueBasedOnStrategy(
+ nsTArray<nsMsgKey>& aQueue, uint32_t aStartingOffset) {
+ NS_ASSERTION(aStartingOffset < aQueue.Length(),
+ "*** Starting offset is out of range");
+
+ // Copy already downloaded messages into a temporary queue,
+ // we want to exclude them from the sort.
+ nsTArray<nsMsgKey> tmpQ;
+ tmpQ.AppendElements(aQueue.Elements(), aStartingOffset);
+
+ // Remove already downloaded messages and sort the resulting queue
+ aQueue.RemoveElementsAt(0, aStartingOffset);
+
+ nsresult rv = SortQueueBasedOnStrategy(aQueue);
+
+ // copy excluded messages back
+ aQueue.InsertElementsAt(0, tmpQ);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetNextGroupOfMessages(
+ uint32_t aSuggestedGroupSizeLimit, uint32_t* aActualGroupSize,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages) {
+ NS_ENSURE_ARG_POINTER(aActualGroupSize);
+
+ aMessages.Clear();
+ *aActualGroupSize = 0;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDatabase> database;
+ folder->GetMsgDatabase(getter_AddRefs(database));
+
+ if (database) {
+ if (!mDownloadQ.IsEmpty()) {
+ // sort the download queue if new items are added since the last time
+ if (mIsDownloadQChanged) {
+ // we want to sort only pending messages. mOffset is
+ // the position of the first pending message in the download queue
+ rv = (mOffset > 0) ? SortSubQueueBasedOnStrategy(mDownloadQ, mOffset)
+ : SortQueueBasedOnStrategy(mDownloadQ);
+
+ if (NS_SUCCEEDED(rv)) mIsDownloadQChanged = false;
+ }
+
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t msgCount = mDownloadQ.Length();
+ uint32_t idx = mOffset;
+
+ nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy;
+ autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy));
+
+ for (; idx < msgCount; idx++) {
+ bool containsKey = false;
+ database->ContainsKey(mDownloadQ[idx], &containsKey);
+ if (!containsKey) {
+ mDownloadSet.RemoveEntry(mDownloadQ[idx]);
+ mDownloadQ.RemoveElementAt(idx--);
+ msgCount--;
+ continue;
+ }
+ nsCOMPtr<nsIMsgDBHdr> qhdr;
+ database->GetMsgHdrForKey(mDownloadQ[idx], getter_AddRefs(qhdr));
+ if (!qhdr) continue; // maybe deleted, skip it!
+
+ // ensure that we don't have this message body offline already,
+ // possible if the user explicitly selects this message prior
+ // to auto-sync kicks in
+ bool hasMessageOffline;
+ folder->HasMsgOffline(mDownloadQ[idx], &hasMessageOffline);
+ if (hasMessageOffline) continue;
+
+ // this check point allows msg strategy function
+ // to do last minute decisions based on the current
+ // state of TB such as the size of the message store etc.
+ if (msgStrategy) {
+ bool excluded = false;
+ if (NS_SUCCEEDED(msgStrategy->IsExcluded(folder, qhdr, &excluded)) &&
+ excluded)
+ continue;
+ }
+
+ uint32_t msgSize;
+ qhdr->GetMessageSize(&msgSize);
+ // ignore 0 byte messages; the imap parser asserts when we try
+ // to download them, and there's no point anyway.
+ if (!msgSize) continue;
+
+ if (!*aActualGroupSize && msgSize >= aSuggestedGroupSizeLimit) {
+ *aActualGroupSize = msgSize;
+ aMessages.AppendElement(qhdr);
+ idx++;
+ break;
+ }
+ if ((*aActualGroupSize) + msgSize > aSuggestedGroupSizeLimit) break;
+
+ aMessages.AppendElement(qhdr);
+ *aActualGroupSize += msgSize;
+ } // endfor
+
+ mLastOffset = mOffset;
+ mOffset = idx;
+ }
+
+ LogOwnerFolderName("Next group of messages to be downloaded.");
+ LogQWithSize(aMessages, 0);
+ } // endif
+
+ return NS_OK;
+}
+
+/**
+ * Called by nsAutoSyncManager::TimerCallback to process message headers for a
+ * folder in the discovery queue. The queue is created on the kAutoSyncFreq
+ * time base (1 hour). Headers lacking offline store are placed in download
+ * queue.
+ */
+NS_IMETHODIMP nsAutoSyncState::ProcessExistingHeaders(
+ uint32_t aNumOfHdrsToProcess, uint32_t* aLeftToProcess) {
+ NS_ENSURE_ARG_POINTER(aLeftToProcess);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDatabase> database;
+ rv = folder->GetMsgDatabase(getter_AddRefs(database));
+ if (!database) return NS_ERROR_FAILURE;
+
+ // create a queue to process existing headers for the first time
+ if (mExistingHeadersQ.IsEmpty()) {
+ nsTArray<nsMsgKey> keys;
+ rv = database->ListAllKeys(keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ keys.Sort();
+ mExistingHeadersQ.AppendElements(keys);
+ mProcessPointer = 0;
+ }
+
+ // process the existing headers and find the messages not downloaded yet
+ uint32_t lastIdx = mProcessPointer;
+ nsTArray<nsMsgKey> msgKeys;
+ uint32_t keyCount = mExistingHeadersQ.Length();
+ for (; mProcessPointer < (lastIdx + aNumOfHdrsToProcess) &&
+ mProcessPointer < keyCount;
+ mProcessPointer++) {
+ bool hasMessageOffline;
+ folder->HasMsgOffline(mExistingHeadersQ[mProcessPointer],
+ &hasMessageOffline);
+ if (!hasMessageOffline)
+ msgKeys.AppendElement(mExistingHeadersQ[mProcessPointer]);
+ }
+ if (!msgKeys.IsEmpty()) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(
+ gAutoSyncLog, LogLevel::Debug,
+ ("%s: %zu messages will be added into the download q of folder %s\n",
+ __func__, msgKeys.Length(), folderName.get()));
+
+ rv = PlaceIntoDownloadQ(msgKeys);
+ if (NS_FAILED(rv)) mProcessPointer = lastIdx;
+ }
+
+ *aLeftToProcess = keyCount - mProcessPointer;
+
+ // cleanup if we are done processing
+ if (0 == *aLeftToProcess) {
+ mLastSyncTime = PR_Now();
+ mExistingHeadersQ.Clear();
+ mProcessPointer = 0;
+ folder->SetMsgDatabase(nullptr);
+ }
+
+ return rv;
+}
+
+void nsAutoSyncState::OnNewHeaderFetchCompleted(
+ const nsTArray<nsMsgKey>& aMsgKeyList) {
+ SetLastUpdateTime(PR_Now());
+ if (!aMsgKeyList.IsEmpty()) PlaceIntoDownloadQ(aMsgKeyList);
+ MOZ_LOG(
+ gAutoSyncLog, LogLevel::Debug,
+ ("%s: %zu msg keys put into download q", __func__, aMsgKeyList.Length()));
+}
+
+NS_IMETHODIMP nsAutoSyncState::UpdateFolder() {
+ nsresult rv;
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> autoSyncMgrListener =
+ do_QueryInterface(autoSyncMgr, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryReferent(mOwnerFolder, &rv);
+ SetState(nsAutoSyncState::stUpdateIssued);
+ return imapFolder->UpdateFolderWithListener(nullptr, autoSyncMgrListener);
+}
+
+NS_IMETHODIMP nsAutoSyncState::OnStartRunningUrl(nsIURI* aUrl) {
+ nsresult rv = NS_OK;
+
+ // if there is a problem to start the download, set rv with the
+ // corresponding error code. In that case, AutoSyncManager is going to
+ // set the autosync state to nsAutoSyncState::stReadyToDownload
+ // to resume downloading another time
+
+ // TODO: is there a way to make sure that download started without
+ // problem through nsIURI interface?
+
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return autoSyncMgr->OnDownloadStarted(this, rv);
+}
+
+/**
+ * This is called when a folder status URL finishes. It is also called when
+ * needed message downloads (imap fetch) for a folder completes.
+ */
+NS_IMETHODIMP nsAutoSyncState::OnStopRunningUrl(nsIURI* aUrl,
+ nsresult aExitCode) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> autoSyncMgrListener =
+ do_QueryInterface(autoSyncMgr, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mSyncState == stStatusIssued) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t serverTotal, serverUnseen, serverRecent, serverNextUID;
+ imapFolder->GetServerTotal(&serverTotal);
+ imapFolder->GetServerUnseen(&serverUnseen);
+ imapFolder->GetServerRecent(&serverRecent);
+ imapFolder->GetServerNextUID(&serverNextUID);
+ // Note: UNSEEN often shows a change when nothing else changes. This is
+ // because UNSEEN produced by SELECT is not the number of unseen messages.
+ // So ignore change to UNSEEN to avoid spurious folder updates. Commented
+ // out below.
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: serverUnseen=%d lastServerUnseen=%d", __func__, serverUnseen,
+ mLastServerUnseen));
+ if (serverNextUID != mLastNextUID || serverTotal != mLastServerTotal ||
+ serverRecent != mLastServerRecent //||
+ /*(serverUnseen != mLastServerUnseen)*/) {
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ ownerFolder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder %s status changed serverNextUID=%d lastNextUID=%d",
+ __func__, folderName.get(), serverNextUID, mLastNextUID));
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: serverTotal = %d lastServerTotal = %d serverRecent = %d "
+ "lastServerRecent = %d\n",
+ __func__, serverTotal, mLastServerTotal, serverRecent,
+ mLastServerRecent));
+ }
+ SetServerCounts(serverTotal, serverRecent, serverUnseen, serverNextUID);
+ SetState(nsAutoSyncState::stUpdateIssued);
+ rv = imapFolder->UpdateFolderWithListener(nullptr, autoSyncMgrListener);
+ } else // folderstatus detected no change
+ {
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ ownerFolder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder %s status or noop issued, no change", __func__,
+ folderName.get()));
+ }
+ // Status detected no change. This may be due to an previously deleted and
+ // now empty database so change compares above could be invalid. If so,
+ // force an update which will re-populate the database (.msf) and download
+ // all the message to mbox/maildir store. This check is only done on the
+ // first imap STATUS response after start-up and if the server response
+ // reports that the folder is not empty.
+ if (!mHaveAStatusResponse && serverTotal != 0) {
+ nsCOMPtr<nsIMsgDatabase> database;
+ ownerFolder->GetMsgDatabase(getter_AddRefs(database));
+ bool hasHeader = false;
+ if (database) {
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ database->EnumerateMessages(getter_AddRefs(hdrs));
+ if (hdrs) hdrs->HasMoreElements(&hasHeader);
+ }
+ if (!hasHeader) {
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ ownerFolder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: folder %s has empty DB, force an update", __func__,
+ folderName.get()));
+ }
+ SetServerCounts(serverTotal, serverRecent, serverUnseen,
+ serverNextUID);
+ SetState(nsAutoSyncState::stUpdateIssued);
+ rv = imapFolder->UpdateFolderWithListener(nullptr,
+ autoSyncMgrListener);
+ }
+ }
+ if (mSyncState == stStatusIssued) {
+ // Didn't force an update above so transition back to stCompletedIdle
+ ownerFolder->SetMsgDatabase(nullptr);
+ // nothing more to do.
+ SetState(nsAutoSyncState::stCompletedIdle);
+ // autoSyncMgr needs this notification, so manufacture it.
+ rv = autoSyncMgrListener->OnStopRunningUrl(aUrl, NS_OK);
+ }
+ } // end no change detected
+ mHaveAStatusResponse = true;
+ } else // URL not folderstatus but FETCH of message body
+ {
+ // XXXemre how we recover from this error?
+ rv = ownerFolder->ReleaseSemaphore(ownerFolder);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "*** Cannot release folder semaphore");
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) rv = mailUrl->UnRegisterListener(this);
+
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ ownerFolder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: URL for FETCH of msg body/bodies complete, folder %s",
+ __func__, folderName.get()));
+ }
+ rv = autoSyncMgr->OnDownloadCompleted(this, aExitCode);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetState(int32_t* aState) {
+ NS_ENSURE_ARG_POINTER(aState);
+ *aState = mSyncState;
+ return NS_OK;
+}
+
+// clang-format off
+const char* stateStrings[] = {
+ "stCompletedIdle:0", // Initial state
+ "stStatusIssued:1", // Imap STATUS or NOOP to occur to detect new msgs
+ "stUpdateNeeded:2", // Imap SELECT to occur due to "pending" msgs
+ "stUpdateIssued:3", // Imap SELECT to occur then fetch new headers
+ "stReadyToDownload:4", // Ready to download a group of new messages
+ "stDownloadInProgress:5" // Download, go to 4 if more msgs then 0 when all done
+};
+// clang-format on
+
+NS_IMETHODIMP nsAutoSyncState::SetState(int32_t aState) {
+ mSyncState = aState;
+ if (aState == stCompletedIdle) {
+ ResetDownloadQ();
+ // tell folder to let go of its cached msg db pointer
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) {
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool folderOpen;
+ uint32_t folderFlags;
+ ownerFolder->GetFlags(&folderFlags);
+ session->IsFolderOpenInWindow(ownerFolder, &folderOpen);
+ if (!folderOpen && !(folderFlags & nsMsgFolderFlags::Inbox))
+ ownerFolder->SetMsgDatabase(nullptr);
+ }
+ }
+ nsCString logStr("Sync State set to |");
+ logStr.Append(stateStrings[aState]);
+ logStr.AppendLiteral("| for ");
+ LogOwnerFolderName(logStr.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::TryCurrentGroupAgain(uint32_t aRetryCount) {
+ SetState(stReadyToDownload);
+
+ nsresult rv;
+ if (++mRetryCounter > aRetryCount) {
+ ResetRetryCounter();
+ rv = NS_ERROR_FAILURE;
+ } else
+ rv = Rollback();
+
+ return rv;
+}
+
+NS_IMETHODIMP nsAutoSyncState::ResetRetryCounter() {
+ mRetryCounter = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetPendingMessageCount(int32_t* aMsgCount) {
+ NS_ENSURE_ARG_POINTER(aMsgCount);
+ *aMsgCount = mDownloadQ.Length() - mOffset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetTotalMessageCount(int32_t* aMsgCount) {
+ NS_ENSURE_ARG_POINTER(aMsgCount);
+ *aMsgCount = mDownloadQ.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetOwnerFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ownerFolder.forget(aFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::Rollback() {
+ mOffset = mLastOffset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::ResetDownloadQ() {
+ mOffset = mLastOffset = 0;
+ mDownloadSet.Clear();
+ mDownloadQ.Clear();
+ mDownloadQ.Compact();
+
+ return NS_OK;
+}
+
+/**
+ * Tests whether the given folder is owned by the same imap server
+ * or not.
+ */
+NS_IMETHODIMP nsAutoSyncState::IsSibling(nsIAutoSyncState* aAnotherStateObj,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folderA, folderB;
+
+ rv = GetOwnerFolder(getter_AddRefs(folderA));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aAnotherStateObj->GetOwnerFolder(getter_AddRefs(folderB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> serverA, serverB;
+ rv = folderA->GetServer(getter_AddRefs(serverA));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderB->GetServer(getter_AddRefs(serverB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isSibling;
+ rv = serverA->Equals(serverB, &isSibling);
+
+ if (NS_SUCCEEDED(rv)) *aResult = isSibling;
+
+ return rv;
+}
+
+/**
+ * Test whether the download queue is empty.
+ */
+NS_IMETHODIMP nsAutoSyncState::IsDownloadQEmpty(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mDownloadQ.IsEmpty();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> msgKeys;
+
+ rv = nsImapMailFolder::BuildIdsAndKeyArray(messages, messageIds, msgKeys);
+ if (NS_FAILED(rv) || messageIds.IsEmpty()) return rv;
+
+ // acquire semaphore for offline store. If it fails, we won't download
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->AcquireSemaphore(folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
+ nsCString folderName;
+ folder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("%s: downloading UIDs %s for folder %s", __func__,
+ messageIds.get(), folderName.get()));
+ }
+ // start downloading
+ rv = imapService->DownloadMessagesForOffline(messageIds, folder, this,
+ nullptr);
+ if (NS_SUCCEEDED(rv)) SetState(stDownloadInProgress);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetLastSyncTime(PRTime* aLastSyncTime) {
+ NS_ENSURE_ARG_POINTER(aLastSyncTime);
+ *aLastSyncTime = mLastSyncTime;
+ return NS_OK;
+}
+
+void nsAutoSyncState::SetLastSyncTimeInSec(int32_t aLastSyncTime) {
+ mLastSyncTime = ((PRTime)aLastSyncTime * PR_USEC_PER_SEC);
+}
+
+NS_IMETHODIMP nsAutoSyncState::GetLastUpdateTime(PRTime* aLastUpdateTime) {
+ NS_ENSURE_ARG_POINTER(aLastUpdateTime);
+ *aLastUpdateTime = mLastUpdateTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoSyncState::SetLastUpdateTime(PRTime aLastUpdateTime) {
+ mLastUpdateTime = aLastUpdateTime;
+ return NS_OK;
+}
+
+void nsAutoSyncState::SetServerCounts(int32_t total, int32_t recent,
+ int32_t unseen, int32_t nextUID) {
+ mLastServerTotal = total;
+ mLastServerRecent = recent;
+ mLastServerUnseen = unseen;
+ mLastNextUID = nextUID;
+}
+
+NS_IMPL_ISUPPORTS(nsAutoSyncState, nsIAutoSyncState, nsIUrlListener)
+
+void nsAutoSyncState::LogQWithSize(nsTArray<nsMsgKey>& q, uint32_t toOffset) {
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder);
+ if (ownerFolder) {
+ nsCOMPtr<nsIMsgDatabase> database;
+ ownerFolder->GetMsgDatabase(getter_AddRefs(database));
+
+ uint32_t x = q.Length();
+ while (x > toOffset && database) {
+ x--;
+ nsCOMPtr<nsIMsgDBHdr> h;
+ database->GetMsgHdrForKey(q[x], getter_AddRefs(h));
+ uint32_t s;
+ if (h) {
+ h->GetMessageSize(&s);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("Elem #%d, size: %u bytes\n", x + 1, s));
+ } else
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("unable to get header for key %ul", q[x]));
+ }
+ }
+}
+
+void nsAutoSyncState::LogQWithSize(nsTArray<RefPtr<nsIMsgDBHdr>> const& q,
+ uint32_t toOffset) {
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder);
+ if (ownerFolder) {
+ nsCOMPtr<nsIMsgDatabase> database;
+ ownerFolder->GetMsgDatabase(getter_AddRefs(database));
+
+ uint32_t x = q.Length();
+ while (x > toOffset && database) {
+ x--;
+ if (q[x]) {
+ uint32_t s;
+ q[x]->GetMessageSize(&s);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("Elem #%d, size: %u bytes\n", x + 1, s));
+ } else
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("null header in q at index %ul", x));
+ }
+ }
+}
+
+void nsAutoSyncState::LogOwnerFolderName(const char* s) {
+ nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder);
+ if (ownerFolder) {
+ nsCString folderName;
+ ownerFolder->GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
+ ("*** %s Folder: %s ***\n", s, folderName.get()));
+ }
+}
diff --git a/comm/mailnews/imap/src/nsAutoSyncState.h b/comm/mailnews/imap/src/nsAutoSyncState.h
new file mode 100644
index 0000000000..d04ad016c2
--- /dev/null
+++ b/comm/mailnews/imap/src/nsAutoSyncState.h
@@ -0,0 +1,106 @@
+/* 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/. */
+
+#ifndef nsAutoSyncState_h__
+#define nsAutoSyncState_h__
+
+#include "MailNewsTypes.h"
+#include "nsIAutoSyncState.h"
+#include "nsIUrlListener.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsTArray.h"
+#include "prlog.h"
+
+class nsImapMailFolder;
+class nsIAutoSyncMsgStrategy;
+class nsIMsgDatabase;
+
+/**
+ * An adaptor class to make msg strategy nsTArray.Sort()
+ * compatible.
+ */
+class MsgStrategyComparatorAdaptor {
+ public:
+ MsgStrategyComparatorAdaptor(nsIAutoSyncMsgStrategy* aStrategy,
+ nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDatabase);
+
+ /** @return True if the elements are equals; false otherwise. */
+ bool Equals(const nsMsgKey& a, const nsMsgKey& b) const;
+
+ /** @return True if (a < b); false otherwise. */
+ bool LessThan(const nsMsgKey& a, const nsMsgKey& b) const;
+
+ private:
+ MsgStrategyComparatorAdaptor();
+
+ private:
+ nsIAutoSyncMsgStrategy* mStrategy;
+ nsIMsgFolder* mFolder;
+ nsIMsgDatabase* mDatabase;
+};
+
+/**
+ * Facilitates auto-sync capabilities for imap folders.
+ */
+class nsAutoSyncState final : public nsIAutoSyncState, public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOSYNCSTATE
+ NS_DECL_NSIURLLISTENER
+
+ explicit nsAutoSyncState(nsImapMailFolder* aOwnerFolder,
+ PRTime aLastSyncTime = 0UL);
+
+ /// Called by owner folder when new headers are fetched from the server
+ void OnNewHeaderFetchCompleted(const nsTArray<nsMsgKey>& aMsgKeyList);
+
+ /// Sets the last sync time in lower precision (seconds)
+ void SetLastSyncTimeInSec(int32_t aLastSyncTime);
+
+ /// Manages storage space for auto-sync operations
+ nsresult ManageStorageSpace();
+
+ void SetServerCounts(int32_t total, int32_t recent, int32_t unseen,
+ int32_t nextUID);
+
+ private:
+ ~nsAutoSyncState();
+
+ nsresult PlaceIntoDownloadQ(const nsTArray<nsMsgKey>& aMsgKeyList);
+ nsresult SortQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue);
+ nsresult SortSubQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue,
+ uint32_t aStartingOffset);
+
+ void LogOwnerFolderName(const char* s);
+ void LogQWithSize(nsTArray<nsMsgKey>& q, uint32_t toOffset = 0);
+ void LogQWithSize(nsTArray<RefPtr<nsIMsgDBHdr>> const& q,
+ uint32_t toOffset = 0);
+
+ private:
+ int32_t mSyncState;
+ nsWeakPtr mOwnerFolder;
+ uint32_t mOffset;
+ uint32_t mLastOffset;
+
+ // used to tell if the Server counts have changed.
+ int32_t mLastServerTotal;
+ int32_t mLastServerRecent;
+ int32_t mLastServerUnseen;
+ int32_t mLastNextUID;
+
+ PRTime mLastSyncTime;
+ PRTime mLastUpdateTime;
+ uint32_t mProcessPointer;
+ bool mIsDownloadQChanged;
+ uint32_t mRetryCounter;
+ nsTHashtable<nsUint32HashKey> mDownloadSet;
+ nsTArray<nsMsgKey> mDownloadQ;
+ nsTArray<nsMsgKey> mExistingHeadersQ;
+ bool mHaveAStatusResponse;
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapBodyShell.cpp b/comm/mailnews/imap/src/nsImapBodyShell.cpp
new file mode 100644
index 0000000000..fecd4230b7
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapBodyShell.cpp
@@ -0,0 +1,1060 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsImapBodyShell.h"
+#include "nsImapProtocol.h"
+#include "nsMimeTypes.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Logging.h"
+
+// need to talk to Rich about this...
+#define IMAP_EXTERNAL_CONTENT_HEADER "X-Mozilla-IMAP-Part"
+
+using namespace mozilla;
+extern LazyLogModule IMAPCache; // defined in nsImapProtocol.cpp
+
+// imapbody.cpp
+// Implementation of the nsImapBodyShell and associated classes
+// These are used to parse IMAP BODYSTRUCTURE responses, and intelligently (?)
+// figure out what parts we need to display inline.
+
+/*
+ Create a nsImapBodyShell from a full BODYSTRUCUTRE response from the
+ parser.
+
+ The body shell represents a single, top-level object, the message. The
+ message body might be treated as either a container or a leaf (just like any
+ arbitrary part).
+
+ Steps for creating a part:
+ 1. Pull out the paren grouping for the part
+ 2. Create a generic part object with that buffer
+ 3. The factory will return either a leaf or container, depending on what
+ it really is.
+ 4. It is responsible for parsing its children, if there are any
+*/
+
+///////////// nsImapBodyShell ////////////////////////////////////
+
+NS_IMPL_ISUPPORTS0(nsImapBodyShell)
+
+nsImapBodyShell::nsImapBodyShell(nsIMAPBodypartMessage* message, uint32_t UID,
+ uint32_t UIDValidity, const char* folderName,
+ bool showAttachmentsInline)
+ : m_message(message),
+ m_isValid(false),
+ m_folderName(folderName),
+ m_generatingPart(nullptr),
+ m_isBeingGenerated(false),
+ m_cached(false),
+ m_generatingWholeMessage(false),
+ m_contentModified(IMAP_ContentModifiedType::IMAP_CONTENT_NOT_MODIFIED) {
+ m_UID = "";
+ m_UID.AppendInt(UID);
+ m_UID_validity = m_UID;
+ m_UID_validity.AppendInt(UIDValidity);
+
+ m_contentModified = showAttachmentsInline
+ ? IMAP_CONTENT_MODIFIED_VIEW_INLINE
+ : IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS;
+
+ SetIsValid(m_message != nullptr);
+}
+
+nsImapBodyShell::~nsImapBodyShell() {
+ delete m_message;
+ m_prefetchQueue.Clear();
+}
+
+void nsImapBodyShell::SetIsValid(bool valid) { m_isValid = valid; }
+
+// Fills in buffer (and adopts storage) for header object
+void nsImapBodyShell::AdoptMessageHeaders(char* headers, const char* partNum) {
+ if (!GetIsValid()) return;
+
+ if (!partNum) partNum = "0";
+
+ // we are going to say that a message header object only has
+ // part data, and no header data.
+ nsIMAPBodypart* foundPart = m_message->FindPartWithNumber(partNum);
+ if (foundPart) {
+ nsIMAPBodypartMessage* messageObj = foundPart->GetnsIMAPBodypartMessage();
+ if (messageObj) {
+ messageObj->AdoptMessageHeaders(headers);
+ if (!messageObj->GetIsValid()) SetIsValid(false);
+ } else {
+ // We were filling in message headers for a given part number.
+ // We looked up that part number, found an object, but it
+ // wasn't of type message/rfc822.
+ // Something's wrong.
+ NS_ASSERTION(false, "object not of type message rfc822");
+ }
+ } else
+ SetIsValid(false);
+}
+
+// Fills in buffer (and adopts storage) for MIME headers in appropriate object.
+// If object can't be found, sets isValid to false.
+void nsImapBodyShell::AdoptMimeHeader(const char* partNum, char* mimeHeader) {
+ if (!GetIsValid()) return;
+
+ NS_ASSERTION(partNum, "null partnum in body shell");
+
+ nsIMAPBodypart* foundPart = m_message->FindPartWithNumber(partNum);
+
+ if (foundPart) {
+ foundPart->AdoptHeaderDataBuffer(mimeHeader);
+ if (!foundPart->GetIsValid()) SetIsValid(false);
+ } else {
+ SetIsValid(false);
+ }
+}
+
+void nsImapBodyShell::AddPrefetchToQueue(nsIMAPeFetchFields fields,
+ const char* partNumber) {
+ nsIMAPMessagePartID newPart(fields, partNumber);
+ m_prefetchQueue.AppendElement(newPart);
+}
+
+// Requires that the shell is valid when called
+// Performs a preflight check on all message parts to see if they are all
+// inline. Returns true if all parts are inline, false otherwise.
+bool nsImapBodyShell::PreflightCheckAllInline() {
+ bool rv = m_message->PreflightCheckAllInline(this);
+ // if (rv)
+ // MOZ_LOG(IMAP, out, ("BODYSHELL: All parts inline. Reverting to whole
+ // message download."));
+ return rv;
+}
+
+// When partNum is NULL, Generates a whole message and intelligently
+// leaves out parts that are not inline.
+
+// When partNum is not NULL, Generates a MIME part that hasn't been downloaded
+// yet Ok, here's how we're going to do this. Essentially, this will be the
+// mirror image of the "normal" generation. All parts will be left out except a
+// single part which is explicitly specified. All relevant headers will be
+// included. Libmime will extract only the part of interest, so we don't have to
+// worry about the other parts. This also has the advantage that it looks like
+// it will be more workable for nested parts. For instance, if a user clicks on
+// a link to a forwarded message, then that forwarded message may be generated
+// along with any images that the forwarded message contains, for instance.
+
+int32_t nsImapBodyShell::Generate(nsImapProtocol* conn, char* partNum) {
+ // Hold the connection in existence for the duration.
+ RefPtr<nsImapProtocol> kungFuDeathGrip(conn);
+
+ m_isBeingGenerated = true;
+ m_generatingPart = partNum;
+ int32_t contentLength = 0;
+
+ if (!GetIsValid() || PreflightCheckAllInline()) {
+ // We don't have a valid shell, or all parts are going to be inline anyway.
+ // Fall back to fetching the whole message.
+#ifdef DEBUG_chrisf
+ NS_ASSERTION(GetIsValid());
+#endif
+ m_generatingWholeMessage = true;
+ uint32_t messageSize = conn->GetMessageSize(GetUID());
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("Generate(): Set IMAP_CONTENT_NOT MODIFIED"));
+ // So that when we cache it, we know we have the whole message.
+ conn->SetContentModified(IMAP_CONTENT_NOT_MODIFIED);
+ if (!conn->DeathSignalReceived())
+ conn->FallbackToFetchWholeMsg(GetUID(), messageSize);
+ contentLength = (int32_t)messageSize; // ugh
+ } else {
+ // We have a valid shell.
+ bool streamCreated = false;
+ m_generatingWholeMessage = false;
+
+ ////// PASS 1 : PREFETCH ///////
+ // First, prefetch any additional headers/data that we need
+ if (!conn->GetPseudoInterrupted())
+ m_message->Generate(
+ this, conn, false,
+ true); // This queues up everything we need to prefetch
+ // Now, run a single pipelined prefetch (neato!)
+ conn->PipelinedFetchMessageParts(GetUID(), m_prefetchQueue);
+ m_prefetchQueue.Clear();
+
+ ////// PASS 2 : COMPUTE STREAM SIZE ///////
+ // Next, figure out the size from the parts that we're going to fill in,
+ // plus all of the MIME headers, plus the message header itself
+ if (!conn->GetPseudoInterrupted())
+ contentLength = m_message->Generate(this, conn, false, false);
+
+ // Setup the stream
+ if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived()) {
+ nsresult rv = conn->BeginMessageDownLoad(contentLength, MESSAGE_RFC822);
+ if (NS_FAILED(rv)) {
+ m_generatingPart = nullptr;
+ conn->AbortMessageDownLoad();
+ return 0;
+ }
+ streamCreated = true;
+ }
+
+ ////// PASS 3 : GENERATE ///////
+ // Generate the message
+ if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived())
+ m_message->Generate(this, conn, true, false);
+
+ // Close the stream here - normal. If pseudointerrupted, the connection
+ // will abort the download stream
+ if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived())
+ conn->NormalMessageEndDownload();
+ else if (streamCreated)
+ conn->AbortMessageDownLoad();
+
+ m_generatingPart = NULL;
+ }
+
+ m_isBeingGenerated = false;
+ return contentLength;
+}
+
+///////////// nsIMAPBodypart ////////////////////////////////////
+
+nsIMAPBodypart::nsIMAPBodypart(char* partNumber, nsIMAPBodypart* parentPart) {
+ SetIsValid(true);
+ m_parentPart = parentPart;
+ m_partNumberString = partNumber; // storage adopted
+ m_partData = NULL;
+ m_headerData = NULL;
+ m_boundaryData = NULL; // initialize from parsed BODYSTRUCTURE
+ m_contentLength = 0;
+ m_partLength = 0;
+
+ m_contentType = NULL;
+ m_bodyType = NULL;
+ m_bodySubType = NULL;
+ m_bodyID = NULL;
+ m_bodyDescription = NULL;
+ m_bodyEncoding = NULL;
+}
+
+nsIMAPBodypart::~nsIMAPBodypart() {
+ PR_FREEIF(m_partNumberString);
+ PR_FREEIF(m_contentType);
+ PR_FREEIF(m_bodyType);
+ PR_FREEIF(m_bodySubType);
+ PR_FREEIF(m_bodyID);
+ PR_FREEIF(m_bodyDescription);
+ PR_FREEIF(m_bodyEncoding);
+ PR_FREEIF(m_partData);
+ PR_FREEIF(m_headerData);
+ PR_FREEIF(m_boundaryData);
+}
+
+void nsIMAPBodypart::SetIsValid(bool valid) {
+ m_isValid = valid;
+ if (!m_isValid) {
+ // MOZ_LOG(IMAP, out, ("BODYSHELL: Part is invalid. Part Number: %s
+ // Content-Type: %s", m_partNumberString, m_contentType));
+ }
+}
+
+// Adopts storage for part data buffer. If NULL, sets isValid to false.
+void nsIMAPBodypart::AdoptPartDataBuffer(char* buf) {
+ m_partData = buf;
+ if (!m_partData) {
+ SetIsValid(false);
+ }
+}
+
+// Adopts storage for header data buffer. If NULL, sets isValid to false.
+void nsIMAPBodypart::AdoptHeaderDataBuffer(char* buf) {
+ m_headerData = buf;
+ if (!m_headerData) {
+ SetIsValid(false);
+ }
+}
+
+// Finds the part with given part number
+// Returns a nsIMAPBodystructure of the matched part if it is this
+// or one of its children. Returns NULL otherwise.
+nsIMAPBodypart* nsIMAPBodypart::FindPartWithNumber(const char* partNum) {
+ // either brute force, or do it the smart way - look at the number.
+ // (the parts should be ordered, and hopefully indexed by their number)
+
+ if (m_partNumberString && !PL_strcasecmp(partNum, m_partNumberString))
+ return this;
+
+ // if (!m_partNumberString && !PL_strcasecmp(partNum, "1"))
+ // return this;
+
+ return NULL;
+}
+
+void nsIMAPBodypart::QueuePrefetchMIMEHeader(nsImapBodyShell* aShell) {
+ aShell->AddPrefetchToQueue(kMIMEHeader, m_partNumberString);
+}
+
+int32_t nsIMAPBodypart::GenerateMIMEHeader(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ if (prefetch && !m_headerData) {
+ QueuePrefetchMIMEHeader(aShell);
+ return 0;
+ }
+ if (m_headerData) {
+ int32_t mimeHeaderLength = 0;
+
+ if (!ShouldFetchInline(aShell)) {
+ // if this part isn't inline, add the X-Mozilla-IMAP-Part header
+ char* xPartHeader = PR_smprintf("%s: %s", IMAP_EXTERNAL_CONTENT_HEADER,
+ m_partNumberString);
+ if (xPartHeader) {
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-XHeader", m_partNumberString);
+ conn->HandleMessageDownLoadLine(xPartHeader, false);
+ }
+ mimeHeaderLength += PL_strlen(xPartHeader);
+ PR_Free(xPartHeader);
+ }
+ }
+
+ mimeHeaderLength += PL_strlen(m_headerData);
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-MIMEHeader", m_partNumberString);
+ conn->HandleMessageDownLoadLine(m_headerData,
+ false); // all one line? Can we do that?
+ }
+
+ return mimeHeaderLength;
+ }
+
+ SetIsValid(false); // prefetch didn't adopt a MIME header
+ return 0;
+}
+
+int32_t nsIMAPBodypart::GeneratePart(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ if (prefetch) return 0; // don't need to prefetch anything
+
+ if (m_partData) // we have prefetched the part data
+ {
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-Part-Prefetched", m_partNumberString);
+ conn->HandleMessageDownLoadLine(m_partData, false);
+ }
+ return PL_strlen(m_partData);
+ }
+
+ // we are fetching and streaming this part's body as we go
+ if (stream && !conn->DeathSignalReceived()) {
+ char* generatingPart = aShell->GetGeneratingPart();
+ bool fetchingSpecificPart =
+ (generatingPart && !PL_strcmp(generatingPart, m_partNumberString));
+
+ conn->Log("SHELL", "GENERATE-Part-Inline", m_partNumberString);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("GeneratePart(): Call FetchTryChunking() part length=%" PRIi32
+ ", part number=%s",
+ m_partLength, m_partNumberString));
+ conn->FetchTryChunking(aShell->GetUID(), kMIMEPart, true,
+ m_partNumberString, m_partLength,
+ !fetchingSpecificPart);
+ }
+ return m_partLength; // the part length has been filled in from the
+ // BODYSTRUCTURE response
+}
+
+int32_t nsIMAPBodypart::GenerateBoundary(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch, bool lastBoundary) {
+ if (prefetch) return 0; // don't need to prefetch anything
+
+ if (m_boundaryData) {
+ if (!lastBoundary) {
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-Boundary", m_partNumberString);
+ conn->HandleMessageDownLoadLine(m_boundaryData, false);
+ }
+ return PL_strlen(m_boundaryData);
+ }
+
+ // the last boundary
+ char* lastBoundaryData = PR_smprintf("%s--", m_boundaryData);
+ if (lastBoundaryData) {
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-Boundary-Last", m_partNumberString);
+ conn->HandleMessageDownLoadLine(lastBoundaryData, false);
+ }
+ int32_t rv = PL_strlen(lastBoundaryData);
+ PR_Free(lastBoundaryData);
+ return rv;
+ }
+ // HandleMemoryFailure();
+ return 0;
+ }
+ return 0;
+}
+
+int32_t nsIMAPBodypart::GenerateEmptyFilling(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ if (prefetch) return 0; // don't need to prefetch anything
+
+ const nsString& emptyString = conn->GetEmptyMimePartString();
+ if (!emptyString.IsEmpty()) {
+ if (stream) {
+ conn->Log("SHELL", "GENERATE-Filling", m_partNumberString);
+ conn->HandleMessageDownLoadLine(NS_ConvertUTF16toUTF8(emptyString).get(),
+ false);
+ }
+ return emptyString.Length();
+ }
+ return 0;
+}
+
+// Returns true if the prefs say that this content type should
+// explicitly be kept in when filling in the shell
+bool nsIMAPBodypart::ShouldExplicitlyFetchInline() { return false; }
+
+// Returns true if the prefs say that this content type should
+// explicitly be left out when filling in the shell
+bool nsIMAPBodypart::ShouldExplicitlyNotFetchInline() { return false; }
+
+///////////// nsIMAPBodypartLeaf /////////////////////////////
+
+nsIMAPBodypartLeaf::nsIMAPBodypartLeaf(char* partNum,
+ nsIMAPBodypart* parentPart,
+ char* bodyType, char* bodySubType,
+ char* bodyID, char* bodyDescription,
+ char* bodyEncoding, int32_t partLength,
+ bool preferPlainText)
+ : nsIMAPBodypart(partNum, parentPart), mPreferPlainText(preferPlainText) {
+ m_bodyType = bodyType;
+ m_bodySubType = bodySubType;
+ m_bodyID = bodyID;
+ m_bodyDescription = bodyDescription;
+ m_bodyEncoding = bodyEncoding;
+ m_partLength = partLength;
+ if (m_bodyType && m_bodySubType) {
+ m_contentType = PR_smprintf("%s/%s", m_bodyType, m_bodySubType);
+ }
+ SetIsValid(true);
+}
+
+nsIMAPBodypartType nsIMAPBodypartLeaf::GetType() { return IMAP_BODY_LEAF; }
+
+int32_t nsIMAPBodypartLeaf::Generate(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ int32_t len = 0;
+
+ if (GetIsValid()) {
+ if (stream && !prefetch)
+ conn->Log("SHELL", "GENERATE-Leaf", m_partNumberString);
+
+ // Stream out the MIME part boundary
+ // GenerateBoundary();
+ NS_ASSERTION(m_parentPart, "part has no parent");
+ // nsIMAPBodypartMessage *parentMessage = m_parentPart ?
+ // m_parentPart->GetnsIMAPBodypartMessage() : NULL;
+
+ // Stream out the MIME header of this part, if this isn't the only body part
+ // of a message
+ // if (parentMessage ? !parentMessage->GetIsTopLevelMessage() : true)
+ if ((m_parentPart->GetType() != IMAP_BODY_MESSAGE_RFC822) &&
+ !conn->GetPseudoInterrupted())
+ len += GenerateMIMEHeader(aShell, conn, stream, prefetch);
+
+ if (!conn->GetPseudoInterrupted()) {
+ if (ShouldFetchInline(aShell)) {
+ // Fetch and stream the content of this part
+ len += GeneratePart(aShell, conn, stream, prefetch);
+ } else {
+ // fill in the filling within the empty part
+ len += GenerateEmptyFilling(aShell, conn, stream, prefetch);
+ }
+ }
+ }
+ m_contentLength = len;
+ return m_contentLength;
+}
+
+// returns true if this part should be fetched inline for generation.
+bool nsIMAPBodypartLeaf::ShouldFetchInline(nsImapBodyShell* aShell) {
+ char* generatingPart = aShell->GetGeneratingPart();
+ if (generatingPart) {
+ // If we are generating a specific part
+ if (!PL_strcmp(generatingPart, m_partNumberString)) {
+ // This is the part we're generating
+ return true;
+ }
+
+ // If this is the only body part of a message, and that
+ // message is the part being generated, then this leaf should
+ // be inline as well.
+ if ((m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822) &&
+ (!PL_strcmp(m_parentPart->GetPartNumberString(), generatingPart)))
+ return true;
+
+ // The parent of this part is a multipart
+ if (m_parentPart->GetType() == IMAP_BODY_MULTIPART) {
+ // This is the first text part of a forwarded message
+ // with a multipart body, and that message is being generated,
+ // then generate this part.
+ nsIMAPBodypart* grandParent = m_parentPart->GetParentPart();
+ // grandParent must exist, since multiparts need parents
+ NS_ASSERTION(grandParent, "grandparent doesn't exist for multi-part alt");
+ if (grandParent && (grandParent->GetType() == IMAP_BODY_MESSAGE_RFC822) &&
+ (!PL_strcmp(grandParent->GetPartNumberString(), generatingPart)) &&
+ (m_partNumberString[PL_strlen(m_partNumberString) - 1] == '1') &&
+ !PL_strcasecmp(m_bodyType, "text"))
+ return true; // we're downloading it inline
+
+ // This is a child of a multipart/appledouble attachment,
+ // and that multipart/appledouble attachment is being generated
+ if (!PL_strcasecmp(m_parentPart->GetBodySubType(), "appledouble") &&
+ !PL_strcmp(m_parentPart->GetPartNumberString(), generatingPart))
+ return true; // we're downloading it inline
+ }
+
+ // Leave out all other leaves if this isn't the one
+ // we're generating.
+ // Maybe change later to check parents, etc.
+ return false;
+ }
+
+ // We are generating the whole message, possibly (hopefully)
+ // leaving out non-inline parts
+ if (ShouldExplicitlyFetchInline()) return true;
+ if (ShouldExplicitlyNotFetchInline()) return false;
+
+ // If the parent is a message (this is the only body part of that
+ // message), and that message should be inline, then its body
+ // should inherit the inline characteristics of that message
+ if (m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822)
+ return m_parentPart->ShouldFetchInline(aShell);
+
+ // View Attachments As Links is on.
+ if (!(aShell->GetContentModified() == IMAP_CONTENT_MODIFIED_VIEW_INLINE)) {
+ // The last text part is still displayed inline,
+ // even if View Attachments As Links is on.
+ nsIMAPBodypart* grandParentPart = m_parentPart->GetParentPart();
+ if ((mPreferPlainText ||
+ !PL_strcasecmp(m_parentPart->GetBodySubType(), "mixed")) &&
+ !PL_strcmp(m_partNumberString, "1") &&
+ !PL_strcasecmp(m_bodyType, "text"))
+ return true; // we're downloading it inline
+
+ if ((!PL_strcasecmp(m_parentPart->GetBodySubType(), "alternative") ||
+ (grandParentPart &&
+ !PL_strcasecmp(grandParentPart->GetBodySubType(), "alternative"))) &&
+ !PL_strcasecmp(m_bodyType, "text") &&
+ ((!PL_strcasecmp(m_bodySubType, "plain") && mPreferPlainText) ||
+ (!PL_strcasecmp(m_bodySubType, "html") && !mPreferPlainText)))
+ return true;
+
+ // This is the first text part of a top-level multipart.
+ // For instance, a message with multipart body, where the first
+ // part is multipart, and this is the first leaf of that first part.
+ if (m_parentPart->GetType() == IMAP_BODY_MULTIPART &&
+ (PL_strlen(m_partNumberString) >= 2) &&
+ !PL_strcmp(m_partNumberString + PL_strlen(m_partNumberString) - 2,
+ ".1") && // this is the first text type on this level
+ (!PL_strcmp(m_parentPart->GetPartNumberString(), "1") ||
+ !PL_strcmp(m_parentPart->GetPartNumberString(), "2")) &&
+ !PL_strcasecmp(m_bodyType, "text"))
+ return true;
+ // This is the first text part of a top-level multipart of the
+ // toplevelmessage This 'assumes' the text body is first leaf. This is not
+ // required for valid email. The only other way is to get
+ // content-disposition = attachment and exclude those text parts.
+ if (m_parentPart->GetType() == IMAP_BODY_MULTIPART &&
+ !PL_strcasecmp(m_bodyType, "text") &&
+ !PL_strcmp(m_parentPart->GetPartNumberString(), "0") &&
+ !PL_strcmp(m_partNumberString, "1"))
+ return true;
+
+ // we may have future problems needing tests here
+
+ return false; // we can leave it on the server
+ }
+#ifdef XP_MACOSX
+ // If it is either applesingle, or a resource fork for appledouble
+ if (!PL_strcasecmp(m_contentType, "application/applefile")) {
+ // if it is appledouble
+ if (m_parentPart->GetType() == IMAP_BODY_MULTIPART &&
+ !PL_strcasecmp(m_parentPart->GetBodySubType(), "appledouble")) {
+ // This is the resource fork of a multipart/appledouble.
+ // We inherit the inline attributes of the parent,
+ // which was derived from its OTHER child. (The data fork.)
+ return m_parentPart->ShouldFetchInline(aShell);
+ }
+ // it is applesingle
+ return false; // we can leave it on the server
+ }
+#endif // XP_MACOSX
+
+ // Fetch type APPLICATION now if the subtype is a signature or if it's an
+ // octet-stream. Otherwise, fetch on demand.
+ if (!PL_strcasecmp(m_bodyType, "APPLICATION") &&
+ PL_strncasecmp(m_bodySubType, "x-pkcs7", 7) &&
+ PL_strcasecmp(m_bodySubType, "octet-stream"))
+ return false; // we can leave it on the server
+ if (!PL_strcasecmp(m_bodyType, "AUDIO")) return false;
+ // Here's where we can add some more intelligence -- let's leave out
+ // any other parts that we know we can't display inline.
+ return true; // we're downloading it inline
+}
+
+bool nsIMAPBodypartMultipart::IsLastTextPart(const char* partNumberString) {
+ // iterate backwards over the parent's part list and if the part is
+ // text, compare it to the part number string
+ for (int i = m_partList.Length() - 1; i >= 0; i--) {
+ nsIMAPBodypart* part = m_partList[i];
+ if (!PL_strcasecmp(part->GetBodyType(), "text"))
+ return !PL_strcasecmp(part->GetPartNumberString(), partNumberString);
+ }
+ return false;
+}
+
+bool nsIMAPBodypartLeaf::PreflightCheckAllInline(nsImapBodyShell* aShell) {
+ // only need to check this part, since it has no children.
+ return ShouldFetchInline(aShell);
+}
+
+///////////// nsIMAPBodypartMessage ////////////////////////
+
+nsIMAPBodypartMessage::nsIMAPBodypartMessage(
+ char* partNum, nsIMAPBodypart* parentPart, bool topLevelMessage,
+ char* bodyType, char* bodySubType, char* bodyID, char* bodyDescription,
+ char* bodyEncoding, int32_t partLength, bool preferPlainText)
+ : nsIMAPBodypartLeaf(partNum, parentPart, bodyType, bodySubType, bodyID,
+ bodyDescription, bodyEncoding, partLength,
+ preferPlainText) {
+ m_topLevelMessage = topLevelMessage;
+ if (m_topLevelMessage) {
+ m_partNumberString = PR_smprintf("0");
+ if (!m_partNumberString) {
+ SetIsValid(false);
+ return;
+ }
+ }
+ m_body = NULL;
+ m_headers = new nsIMAPMessageHeaders(
+ m_partNumberString, this); // We always have a Headers object
+ if (!m_headers || !m_headers->GetIsValid()) {
+ SetIsValid(false);
+ return;
+ }
+ SetIsValid(true);
+}
+
+void nsIMAPBodypartMessage::SetBody(nsIMAPBodypart* body) {
+ if (m_body) delete m_body;
+ m_body = body;
+}
+
+nsIMAPBodypartType nsIMAPBodypartMessage::GetType() {
+ return IMAP_BODY_MESSAGE_RFC822;
+}
+
+nsIMAPBodypartMessage::~nsIMAPBodypartMessage() {
+ delete m_headers;
+ delete m_body;
+}
+
+int32_t nsIMAPBodypartMessage::Generate(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ if (!GetIsValid()) return 0;
+
+ m_contentLength = 0;
+
+ if (stream && !prefetch)
+ conn->Log("SHELL", "GENERATE-MessageRFC822", m_partNumberString);
+
+ if (!m_topLevelMessage &&
+ !conn->GetPseudoInterrupted()) // not the top-level message - we need
+ // the MIME header as well as the
+ // message header
+ {
+ // but we don't need the MIME headers of a message/rfc822 part if this
+ // content type is in (part of) the main msg header. In other words, we
+ // still need these MIME headers if this message/rfc822 body part is
+ // enclosed in the msg body (most likely as a body part of a multipart/mixed
+ // msg).
+ // Don't fetch (bug 128888) Do fetch (bug 168097)
+ // ---------------------------------- -----------------------------------
+ // message/rfc822 (parent part) message/rfc822
+ // message/rfc822 <<<--- multipart/mixed (parent part)
+ // multipart/mixed message/rfc822 <<<---
+ // text/html (body text) multipart/mixed
+ // text/plain (attachment) text/html (body text)
+ // application/msword (attachment) text/plain (attachment)
+ // application/msword (attachment)
+ // "<<<---" points to the part we're examining here.
+ if (PL_strcasecmp(m_bodyType, "message") ||
+ PL_strcasecmp(m_bodySubType, "rfc822") ||
+ PL_strcasecmp(m_parentPart->GetBodyType(), "message") ||
+ PL_strcasecmp(m_parentPart->GetBodySubType(), "rfc822"))
+ m_contentLength += GenerateMIMEHeader(aShell, conn, stream, prefetch);
+ }
+
+ if (!conn->GetPseudoInterrupted())
+ m_contentLength += m_headers->Generate(aShell, conn, stream, prefetch);
+ if (!conn->GetPseudoInterrupted())
+ m_contentLength += m_body->Generate(aShell, conn, stream, prefetch);
+
+ return m_contentLength;
+}
+
+bool nsIMAPBodypartMessage::ShouldFetchInline(nsImapBodyShell* aShell) {
+ if (m_topLevelMessage) // the main message should always be defined as
+ // "inline"
+ return true;
+
+ char* generatingPart = aShell->GetGeneratingPart();
+ if (generatingPart) {
+ // If we are generating a specific part
+ // Always generate containers (just don't fill them in)
+ // because it is low cost (everything is cached)
+ // and it gives the message its full MIME structure,
+ // to avoid any potential mishap.
+ return true;
+ }
+
+ // Generating whole message
+ if (ShouldExplicitlyFetchInline()) return true;
+ if (ShouldExplicitlyNotFetchInline()) return false;
+
+ // Message types are inline, by default.
+ return true;
+}
+
+bool nsIMAPBodypartMessage::PreflightCheckAllInline(nsImapBodyShell* aShell) {
+ if (!ShouldFetchInline(aShell)) return false;
+
+ return m_body->PreflightCheckAllInline(aShell);
+}
+
+// Fills in buffer (and adopts storage) for header object
+void nsIMAPBodypartMessage::AdoptMessageHeaders(char* headers) {
+ if (!GetIsValid()) return;
+
+ // we are going to say that the message headers only have
+ // part data, and no header data.
+ m_headers->AdoptPartDataBuffer(headers);
+ if (!m_headers->GetIsValid()) SetIsValid(false);
+}
+
+// Finds the part with given part number
+// Returns a nsIMAPBodystructure of the matched part if it is this
+// or one of its children. Returns NULL otherwise.
+nsIMAPBodypart* nsIMAPBodypartMessage::FindPartWithNumber(const char* partNum) {
+ // either brute force, or do it the smart way - look at the number.
+ // (the parts should be ordered, and hopefully indexed by their number)
+
+ if (!PL_strcasecmp(partNum, m_partNumberString)) return this;
+
+ return m_body->FindPartWithNumber(partNum);
+}
+
+///////////// nsIMAPBodypartMultipart ////////////////////////
+
+nsIMAPBodypartMultipart::nsIMAPBodypartMultipart(char* partNum,
+ nsIMAPBodypart* parentPart)
+ : nsIMAPBodypart(partNum, parentPart) {
+ if (!m_parentPart || (m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822)) {
+ // the multipart (this) will inherit the part number of its parent
+ PR_FREEIF(m_partNumberString);
+ if (!m_parentPart) {
+ m_partNumberString = PR_smprintf("0");
+ } else
+ m_partNumberString = NS_xstrdup(m_parentPart->GetPartNumberString());
+ }
+ m_bodyType = NS_xstrdup("multipart");
+ if (m_parentPart && m_bodyType)
+ SetIsValid(true);
+ else
+ SetIsValid(false);
+}
+
+nsIMAPBodypartType nsIMAPBodypartMultipart::GetType() {
+ return IMAP_BODY_MULTIPART;
+}
+
+nsIMAPBodypartMultipart::~nsIMAPBodypartMultipart() {
+ for (int i = m_partList.Length() - 1; i >= 0; i--) {
+ delete m_partList[i];
+ }
+}
+
+void nsIMAPBodypartMultipart::SetBodySubType(char* bodySubType) {
+ PR_FREEIF(m_bodySubType);
+ PR_FREEIF(m_contentType);
+ m_bodySubType = bodySubType;
+ if (m_bodyType && m_bodySubType)
+ m_contentType = PR_smprintf("%s/%s", m_bodyType, m_bodySubType);
+}
+
+int32_t nsIMAPBodypartMultipart::Generate(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ int32_t len = 0;
+
+ if (GetIsValid()) {
+ if (stream && !prefetch)
+ conn->Log("SHELL", "GENERATE-Multipart", m_partNumberString);
+
+ // Stream out the MIME header of this part
+
+ bool parentIsMessageType =
+ GetParentPart()
+ ? (GetParentPart()->GetType() == IMAP_BODY_MESSAGE_RFC822)
+ : true;
+
+ // If this is multipart/signed, then we always want to generate the MIME
+ // headers of this multipart. Otherwise, we only want to do it if the parent
+ // is not of type "message"
+ bool needMIMEHeader =
+ !parentIsMessageType; // !PL_strcasecmp(m_bodySubType, "signed") ? true
+ // : !parentIsMessageType;
+ if (needMIMEHeader &&
+ !conn->GetPseudoInterrupted()) // not a message body's type
+ {
+ len += GenerateMIMEHeader(aShell, conn, stream, prefetch);
+ }
+
+ if (ShouldFetchInline(aShell)) {
+ for (auto part : m_partList) {
+ if (!conn->GetPseudoInterrupted())
+ len += GenerateBoundary(aShell, conn, stream, prefetch, false);
+ if (!conn->GetPseudoInterrupted())
+ len += part->Generate(aShell, conn, stream, prefetch);
+ }
+ if (!conn->GetPseudoInterrupted())
+ len += GenerateBoundary(aShell, conn, stream, prefetch, true);
+ } else {
+ // fill in the filling within the empty part
+ if (!conn->GetPseudoInterrupted())
+ len += GenerateEmptyFilling(aShell, conn, stream, prefetch);
+ }
+ }
+ m_contentLength = len;
+ return m_contentLength;
+}
+
+bool nsIMAPBodypartMultipart::ShouldFetchInline(nsImapBodyShell* aShell) {
+ char* generatingPart = aShell->GetGeneratingPart();
+ if (generatingPart) {
+ // If we are generating a specific part
+ // Always generate containers (just don't fill them in)
+ // because it is low cost (everything is cached)
+ // and it gives the message its full MIME structure,
+ // to avoid any potential mishap.
+ return true;
+ }
+
+ // Generating whole message
+ if (ShouldExplicitlyFetchInline()) return true;
+ if (ShouldExplicitlyNotFetchInline()) return false;
+
+ if (!PL_strcasecmp(m_bodySubType, "alternative")) return true;
+
+ nsIMAPBodypart* grandparentPart = m_parentPart->GetParentPart();
+
+ // if we're a multipart sub-part of multipart alternative, we need to
+ // be fetched because mime will always display us.
+ if (!PL_strcasecmp(m_parentPart->GetBodySubType(), "alternative") &&
+ GetType() == IMAP_BODY_MULTIPART)
+ return true;
+ // If "Show Attachments as Links" is on, and
+ // the parent of this multipart is not a message,
+ // then it's not inline.
+ if (!(aShell->GetContentModified() == IMAP_CONTENT_MODIFIED_VIEW_INLINE) &&
+ (m_parentPart->GetType() != IMAP_BODY_MESSAGE_RFC822) &&
+ (m_parentPart->GetType() == IMAP_BODY_MULTIPART
+ ? (grandparentPart
+ ? grandparentPart->GetType() != IMAP_BODY_MESSAGE_RFC822
+ : true)
+ : true))
+ return false;
+
+ // multiparts are always inline (even multipart/appledouble)
+ // (their children might not be, though)
+ return true;
+}
+
+bool nsIMAPBodypartMultipart::PreflightCheckAllInline(nsImapBodyShell* aShell) {
+ bool rv = ShouldFetchInline(aShell);
+
+ size_t i = 0;
+ while (rv && (i < m_partList.Length())) {
+ rv = m_partList[i]->PreflightCheckAllInline(aShell);
+ i++;
+ }
+
+ return rv;
+}
+
+nsIMAPBodypart* nsIMAPBodypartMultipart::FindPartWithNumber(
+ const char* partNum) {
+ NS_ASSERTION(partNum, "null part passed into FindPartWithNumber");
+
+ // check this
+ if (!PL_strcmp(partNum, m_partNumberString)) return this;
+
+ // check children
+ for (int i = m_partList.Length() - 1; i >= 0; i--) {
+ nsIMAPBodypart* foundPart = m_partList[i]->FindPartWithNumber(partNum);
+ if (foundPart) return foundPart;
+ }
+
+ // not this, or any of this's children
+ return NULL;
+}
+
+///////////// nsIMAPMessageHeaders ////////////////////////////////////
+
+nsIMAPMessageHeaders::nsIMAPMessageHeaders(char* partNum,
+ nsIMAPBodypart* parentPart)
+ : nsIMAPBodypart(partNum, parentPart) {
+ if (!partNum) {
+ SetIsValid(false);
+ return;
+ }
+ m_partNumberString = NS_xstrdup(partNum);
+ if (!m_partNumberString) {
+ SetIsValid(false);
+ return;
+ }
+ if (!m_parentPart || !m_parentPart->GetnsIMAPBodypartMessage()) {
+ // Message headers created without a valid Message parent
+ NS_ASSERTION(false, "creating message headers with invalid message parent");
+ SetIsValid(false);
+ }
+}
+
+nsIMAPBodypartType nsIMAPMessageHeaders::GetType() {
+ return IMAP_BODY_MESSAGE_HEADER;
+}
+
+void nsIMAPMessageHeaders::QueuePrefetchMessageHeaders(
+ nsImapBodyShell* aShell) {
+ if (!m_parentPart->GetnsIMAPBodypartMessage()
+ ->GetIsTopLevelMessage()) // not top-level headers
+ aShell->AddPrefetchToQueue(kRFC822HeadersOnly, m_partNumberString);
+ else
+ aShell->AddPrefetchToQueue(kRFC822HeadersOnly, NULL);
+}
+
+int32_t nsIMAPMessageHeaders::Generate(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch) {
+ // prefetch the header
+ if (prefetch && !m_partData && !conn->DeathSignalReceived()) {
+ QueuePrefetchMessageHeaders(aShell);
+ }
+
+ if (stream && !prefetch)
+ conn->Log("SHELL", "GENERATE-MessageHeaders", m_partNumberString);
+
+ // stream out the part data
+ if (ShouldFetchInline(aShell)) {
+ if (!conn->GetPseudoInterrupted())
+ m_contentLength = GeneratePart(aShell, conn, stream, prefetch);
+ } else {
+ m_contentLength = 0; // don't fill in any filling for the headers
+ }
+ return m_contentLength;
+}
+
+bool nsIMAPMessageHeaders::ShouldFetchInline(nsImapBodyShell* aShell) {
+ return m_parentPart->ShouldFetchInline(aShell);
+}
+
+///////////// nsImapBodyShellCache ////////////////////////////////////
+
+nsImapBodyShellCache::nsImapBodyShellCache()
+ : m_shellList(kMaxEntries), m_shellHash(kMaxEntries) {}
+
+// We'll use an LRU scheme here.
+// We will add shells in numerical order, so the
+// least recently used one will be in slot 0.
+void nsImapBodyShellCache::EjectEntry() {
+ MOZ_ASSERT(!m_shellList.IsEmpty());
+
+ nsImapBodyShell* removedShell = m_shellList.ElementAt(0);
+
+ m_shellList.RemoveElementAt(0);
+ m_shellHash.Remove(removedShell->GetUID());
+}
+
+void nsImapBodyShellCache::Clear() {
+ m_shellList.ClearAndRetainStorage();
+ m_shellHash.Clear();
+}
+
+void nsImapBodyShellCache::AddShellToCache(nsImapBodyShell* shell) {
+ // If it's already in the cache, then just return.
+ // This has the side-effect of re-ordering the LRU list
+ // to put this at the top, which is good, because it's what we want.
+ if (FindShellForUID(shell->GetUID_validity(), shell->GetFolderName(),
+ shell->GetContentModified())) {
+ return;
+ }
+
+ // OK, so it's not in the cache currently.
+
+ // First, for safety sake, remove any entry with the given UID,
+ // just in case we have a collision between two messages in different
+ // folders with the same UID.
+ RefPtr<nsImapBodyShell> foundShell;
+ m_shellHash.Get(shell->GetUID_validity(), getter_AddRefs(foundShell));
+ if (foundShell) {
+ m_shellHash.Remove(foundShell->GetUID_validity());
+ m_shellList.RemoveElement(foundShell);
+ }
+
+ // Make sure there's enough room
+ while (m_shellList.Length() > (kMaxEntries - 1)) {
+ EjectEntry();
+ }
+
+ // Add the new one to the cache
+ m_shellList.AppendElement(shell);
+
+ m_shellHash.InsertOrUpdate(shell->GetUID_validity(), RefPtr{shell});
+ shell->SetIsCached(true);
+}
+
+nsImapBodyShell* nsImapBodyShellCache::FindShellForUID(
+ nsACString const& UID, nsACString const& mailboxName,
+ IMAP_ContentModifiedType modType) {
+ RefPtr<nsImapBodyShell> foundShell;
+ m_shellHash.Get(UID, getter_AddRefs(foundShell));
+ if (!foundShell) return nullptr;
+ // Make sure the content-modified types are compatible.
+ // This allows us to work seamlessly while people switch between
+ // View Attachments Inline and View Attachments As Links.
+ // Enforce the invariant that any cached shell we use
+ // match the current content-modified settings.
+ if (modType != foundShell->GetContentModified()) return nullptr;
+
+ // mailbox names must match also.
+ if (!mailboxName.Equals(foundShell->GetFolderName())) return nullptr;
+
+ // adjust the LRU stuff. This defeats the performance gain of the hash if
+ // it actually is found since this is linear.
+ m_shellList.RemoveElement(foundShell);
+ m_shellList.AppendElement(foundShell); // Adds to end
+
+ return foundShell;
+}
+
+///////////// nsIMAPMessagePartID ////////////////////////////////////
+
+nsIMAPMessagePartID::nsIMAPMessagePartID(nsIMAPeFetchFields fields,
+ const char* partNumberString)
+ : m_partNumberString(partNumberString), m_fields(fields) {}
diff --git a/comm/mailnews/imap/src/nsImapBodyShell.h b/comm/mailnews/imap/src/nsImapBodyShell.h
new file mode 100644
index 0000000000..c2ab8c5898
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapBodyShell.h
@@ -0,0 +1,357 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+nsImapBodyShell and associated classes
+*/
+
+#ifndef IMAPBODY_H
+#define IMAPBODY_H
+
+#include "mozilla/Attributes.h"
+#include "nsImapCore.h"
+#include "nsString.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+
+class nsImapProtocol;
+
+typedef enum _nsIMAPBodypartType {
+ IMAP_BODY_MESSAGE_RFC822,
+ IMAP_BODY_MESSAGE_HEADER,
+ IMAP_BODY_LEAF,
+ IMAP_BODY_MULTIPART
+} nsIMAPBodypartType;
+
+class nsImapBodyShell;
+class nsIMAPBodypartMessage;
+
+class nsIMAPBodypart {
+ public:
+ // Construction
+ virtual bool GetIsValid() { return m_isValid; }
+ virtual void SetIsValid(bool valid);
+ virtual nsIMAPBodypartType GetType() = 0;
+
+ // Generation
+ // Generates an HTML representation of this part. Returns content length
+ // generated, -1 if failed.
+ virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool /*stream*/, bool /* prefetch */) {
+ return -1;
+ }
+ virtual void AdoptPartDataBuffer(
+ char* buf); // Adopts storage for part data buffer. If NULL, sets
+ // isValid to false.
+ virtual void AdoptHeaderDataBuffer(
+ char* buf); // Adopts storage for header data buffer. If NULL, sets
+ // isValid to false.
+ virtual bool ShouldFetchInline(nsImapBodyShell* aShell) {
+ return true;
+ } // returns true if this part should be fetched inline for generation.
+ virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) { return true; }
+
+ virtual bool ShouldExplicitlyFetchInline();
+ virtual bool ShouldExplicitlyNotFetchInline();
+ virtual bool IsLastTextPart(const char* partNumberString) { return true; }
+
+ protected:
+ // If stream is false, simply returns the content length that will be
+ // generated the body of the part itself
+ virtual int32_t GeneratePart(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool stream, bool prefetch);
+ // the MIME headers of the part
+ virtual int32_t GenerateMIMEHeader(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch);
+ // Generates the MIME boundary wrapper for this part.
+ virtual int32_t GenerateBoundary(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch, bool lastBoundary);
+ // lastBoundary indicates whether or not this should be the boundary for the
+ // final MIME part of the multipart message.
+ // Generates (possibly empty) filling for a part that won't be filled in
+ // inline.
+ virtual int32_t GenerateEmptyFilling(nsImapBodyShell* aShell,
+ nsImapProtocol* conn, bool stream,
+ bool prefetch);
+
+ // Part Numbers / Hierarchy
+ public:
+ virtual char* GetPartNumberString() { return m_partNumberString; }
+ virtual nsIMAPBodypart* FindPartWithNumber(
+ const char* partNum); // Returns the part object with the given number
+ virtual nsIMAPBodypart* GetParentPart() {
+ return m_parentPart;
+ } // Returns the parent of this part.
+ // We will define a part of type message/rfc822 to be the
+ // parent of its body and header.
+ // A multipart is a parent of its child parts.
+ // All other leafs do not have children.
+
+ // Other / Helpers
+ public:
+ virtual ~nsIMAPBodypart();
+ virtual nsIMAPBodypartMessage* GetnsIMAPBodypartMessage() { return NULL; }
+
+ const char* GetBodyType() { return m_bodyType; }
+ const char* GetBodySubType() { return m_bodySubType; }
+ void SetBoundaryData(char* boundaryData) { m_boundaryData = boundaryData; }
+
+ protected:
+ virtual void QueuePrefetchMIMEHeader(nsImapBodyShell* aShell);
+ // virtual void PrefetchMIMEHeader(); // Initiates a prefetch for the MIME
+ // header of this part.
+ nsIMAPBodypart(char* partNumber, nsIMAPBodypart* parentPart);
+
+ protected:
+ bool m_isValid; // If this part is valid.
+ char* m_partNumberString; // string representation of this part's
+ // full-hierarchy number. Define 0 to be the
+ // top-level message
+ char* m_partData; // data for this part. NULL if not filled in yet.
+ char* m_headerData; // data for this part's MIME header. NULL if not filled
+ // in yet.
+ char* m_boundaryData; // MIME boundary for this part
+ int32_t m_partLength;
+ int32_t m_contentLength; // Total content length which will be Generate()'d.
+ // -1 if not filled in yet.
+ nsIMAPBodypart* m_parentPart; // Parent of this part
+
+ // Fields - Filled in from parsed BODYSTRUCTURE response (as well as others)
+ char* m_contentType; // constructed from m_bodyType and m_bodySubType
+ char* m_bodyType;
+ char* m_bodySubType;
+ char* m_bodyID;
+ char* m_bodyDescription;
+ char* m_bodyEncoding;
+ // we ignore extension data for now
+};
+
+// Message headers
+// A special type of nsIMAPBodypart
+// These may be headers for the top-level message,
+// or any body part of type message/rfc822.
+class nsIMAPMessageHeaders : public nsIMAPBodypart {
+ public:
+ nsIMAPMessageHeaders(char* partNum, nsIMAPBodypart* parentPart);
+ virtual nsIMAPBodypartType GetType() override;
+ // Generates an HTML representation of this part. Returns content length
+ // generated, -1 if failed.
+ virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool stream, bool prefetch) override;
+ virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override;
+ virtual void QueuePrefetchMessageHeaders(nsImapBodyShell* aShell);
+};
+
+class nsIMAPBodypartMultipart : public nsIMAPBodypart {
+ public:
+ nsIMAPBodypartMultipart(char* partNum, nsIMAPBodypart* parentPart);
+ virtual nsIMAPBodypartType GetType() override;
+ virtual ~nsIMAPBodypartMultipart();
+ virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override;
+ virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override;
+ // Generates an HTML representation of this part. Returns content length
+ // generated, -1 if failed.
+ virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool stream, bool prefetch) override;
+ // Returns the part object with the given number
+ virtual nsIMAPBodypart* FindPartWithNumber(const char* partNum) override;
+ virtual bool IsLastTextPart(const char* partNumberString) override;
+ void AppendPart(nsIMAPBodypart* part) { m_partList.AppendElement(part); }
+ void SetBodySubType(char* bodySubType);
+
+ protected:
+ // An ordered list of top-level body parts for this shell
+ nsTArray<nsIMAPBodypart*> m_partList;
+};
+
+// The name "leaf" is somewhat misleading, since a part of type message/rfc822
+// is technically a leaf, even though it can contain other parts within it.
+class nsIMAPBodypartLeaf : public nsIMAPBodypart {
+ public:
+ nsIMAPBodypartLeaf(char* partNum, nsIMAPBodypart* parentPart, char* bodyType,
+ char* bodySubType, char* bodyID, char* bodyDescription,
+ char* bodyEncoding, int32_t partLength,
+ bool preferPlainText);
+ virtual nsIMAPBodypartType GetType() override;
+ // Generates an HTML representation of this part. Returns content length
+ // generated, -1 if failed.
+ virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool stream, bool prefetch) override;
+ // returns true if this part should be fetched inline for generation.
+ virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override;
+ virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override;
+
+ private:
+ bool mPreferPlainText;
+};
+
+class nsIMAPBodypartMessage : public nsIMAPBodypartLeaf {
+ public:
+ nsIMAPBodypartMessage(char* partNum, nsIMAPBodypart* parentPart,
+ bool topLevelMessage, char* bodyType, char* bodySubType,
+ char* bodyID, char* bodyDescription, char* bodyEncoding,
+ int32_t partLength, bool preferPlainText);
+ void SetBody(nsIMAPBodypart* body);
+ virtual nsIMAPBodypartType GetType() override;
+ virtual ~nsIMAPBodypartMessage();
+ virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn,
+ bool stream, bool prefetch) override;
+ virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override;
+ virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override;
+ // Returns the part object with the given number
+ virtual nsIMAPBodypart* FindPartWithNumber(const char* partNum) override;
+ void AdoptMessageHeaders(
+ char* headers); // Fills in buffer (and adopts storage) for header object
+ // partNum specifies the message part number to which the
+ // headers correspond. NULL indicates the top-level
+ // message
+ virtual nsIMAPBodypartMessage* GetnsIMAPBodypartMessage() override {
+ return this;
+ }
+ virtual bool GetIsTopLevelMessage() { return m_topLevelMessage; }
+
+ protected:
+ nsIMAPMessageHeaders* m_headers; // Every body shell should have headers
+ nsIMAPBodypart* m_body;
+ bool m_topLevelMessage; // Whether or not this is the top-level message
+};
+
+// MessagePartID and an array of them are used for pipelining prefetches.
+
+class nsIMAPMessagePartID {
+ public:
+ nsIMAPMessagePartID(nsIMAPeFetchFields fields, const char* partNumberString);
+ nsIMAPeFetchFields GetFields() { return m_fields; }
+ const char* GetPartNumberString() { return m_partNumberString; }
+
+ protected:
+ const char* m_partNumberString;
+ nsIMAPeFetchFields m_fields;
+};
+
+// We will refer to a Body "Shell" as a hierarchical object representation of a
+// parsed BODYSTRUCTURE response. A shell contains representations of Shell
+// "Parts." A Body Shell can undergo essentially two operations: Construction
+// and Generation. Shell Construction occurs from a parsed a BODYSTRUCTURE
+// response, split into empty parts. Shell Generation generates a "MIME Shell"
+// of the message and streams it to libmime for display. The MIME Shell has
+// selected (inline) parts filled in, and leaves all others for on-demand
+// retrieval through explicit part fetches.
+
+class nsImapBodyShell : public nsISupports {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ nsImapBodyShell(nsIMAPBodypartMessage* message, uint32_t UID,
+ uint32_t UIDValidity, const char* folderName,
+ bool showAttachmentsInline);
+ // To be used after a shell is uncached
+ bool GetIsValid() { return m_isValid; }
+ void SetIsValid(bool valid);
+
+ // Prefetch
+ // Adds a message body part to the queue to be prefetched
+ // in a single, pipelined command
+ void AddPrefetchToQueue(nsIMAPeFetchFields, const char* partNum);
+ // Fills in buffer (and adopts storage) for header object
+ // partNum specifies the message part number to which the
+ // headers correspond. NULL indicates the top-level message
+ void AdoptMessageHeaders(char* headers, const char* partNum);
+ // Fills in buffer (and adopts storage) for MIME headers in appropriate
+ // object. If object can't be found, sets isValid to false.
+ void AdoptMimeHeader(const char* partNum, char* mimeHeader);
+
+ // Generation
+ // Streams out an HTML representation of this IMAP message, going along and
+ // fetching parts it thinks it needs, and leaving empty shells for the parts
+ // it doesn't.
+ // Returns number of bytes generated, or -1 if invalid.
+ // If partNum is not NULL, then this works to generates a MIME part that
+ // hasn't been downloaded yet and leaves out all other parts. By default, to
+ // generate a normal message, partNum should be NULL.
+ int32_t Generate(nsImapProtocol* conn, char* partNum);
+
+ // Returns TRUE if the user has the pref "Show Attachments Inline" set.
+ // Returns FALSE if the setting is "Show Attachments as Links"
+ bool GetShowAttachmentsInline();
+ // Returns true if all parts are inline, false otherwise. Does not generate
+ // anything.
+ bool PreflightCheckAllInline();
+
+ // Helpers
+ nsCString& GetUID() { return m_UID; }
+ nsCString& GetUID_validity() { return m_UID_validity; }
+ nsCString const& GetFolderName() const { return m_folderName; }
+ char* GetGeneratingPart() { return m_generatingPart; }
+ // Returns true if this is in the process of being generated,
+ // so we don't re-enter
+ bool IsBeingGenerated() { return m_isBeingGenerated; }
+ bool IsShellCached() { return m_cached; }
+ void SetIsCached(bool isCached) { m_cached = isCached; }
+ bool GetGeneratingWholeMessage() { return m_generatingWholeMessage; }
+ IMAP_ContentModifiedType GetContentModified() { return m_contentModified; }
+
+ protected:
+ virtual ~nsImapBodyShell();
+
+ nsIMAPBodypartMessage* m_message;
+
+ // Array of pipelined part prefetches.
+ nsTArray<nsIMAPMessagePartID> m_prefetchQueue;
+
+ bool m_isValid;
+ nsCString m_UID; // UID of this message
+ nsCString m_UID_validity; // appended UID and UID-validity of this message
+ nsCString m_folderName; // folder that contains this message
+ char* m_generatingPart; // If a specific part is being generated, this is it.
+ // Otherwise, NULL.
+ bool m_isBeingGenerated; // true if this body shell is in the process of
+ // being generated
+ bool m_cached; // Whether or not this shell is cached
+ bool m_generatingWholeMessage; // whether or not we are generating the whole
+ // (non-MPOD) message Set to false if we are
+ // generating by parts
+ // under what conditions the content has been modified.
+ // Either IMAP_CONTENT_MODIFIED_VIEW_INLINE or
+ // IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS
+ IMAP_ContentModifiedType m_contentModified;
+};
+
+// This class caches shells, so we don't have to always go and re-fetch them.
+// This does not cache any of the filled-in inline parts; those are cached
+// individually in the libnet memory cache. (ugh, how will we do that?) Since
+// we'll only be retrieving shells for messages over a given size, and since the
+// shells themselves won't be very large, this cache will not grow very big
+// (relatively) and should handle most common usage scenarios.
+
+// A body cache is associated with a given host, spanning folders, so
+// it uses both UID and UIDVALIDITY .
+
+class nsImapBodyShellCache {
+ public:
+ nsImapBodyShellCache();
+
+ // Adds shell to cache, possibly ejecting
+ // another entry based on scheme in EjectEntry().
+ void AddShellToCache(nsImapBodyShell* shell);
+ // Looks up a shell in the cache given the message's UID.
+ nsImapBodyShell* FindShellForUID(nsACString const& UID,
+ nsACString const& mailboxName,
+ IMAP_ContentModifiedType modType);
+ void Clear();
+
+ protected:
+ static constexpr int kMaxEntries = 20;
+ // Chooses an entry to eject; deletes that entry; and ejects it from the
+ // cache, clearing up a new space.
+ void EjectEntry();
+
+ nsTArray<nsImapBodyShell*> m_shellList;
+ // For quick lookup based on UID
+ nsRefPtrHashtable<nsCStringHashKey, nsImapBodyShell> m_shellHash;
+};
+
+#endif // IMAPBODY_H
diff --git a/comm/mailnews/imap/src/nsImapCore.h b/comm/mailnews/imap/src/nsImapCore.h
new file mode 100644
index 0000000000..4c4e213d45
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapCore.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// clang-format off
+#ifndef _nsImapCore_H_
+#define _nsImapCore_H_
+
+#include "MailNewsTypes.h"
+#include "nsString.h"
+
+/* imap message flags */
+typedef uint16_t imapMessageFlagsType;
+
+/* used for communication between imap thread and event sinks */
+#define kNoFlags 0x00 /* RFC flags */
+#define kMarked 0x01
+#define kUnmarked 0x02
+#define kNoinferiors 0x04
+#define kNoselect 0x08
+#define kImapTrash 0x10 /* Navigator flag */
+#define kJustExpunged 0x20 /* This update is a post expunge url update. */
+#define kPersonalMailbox 0x40 /* this mailbox is in the personal namespace */
+#define kPublicMailbox 0x80 /* this mailbox is in the public namespace */
+#define kOtherUsersMailbox 0x100 /* this mailbox is in the other users' namespace */
+#define kNameSpace 0x200 /* this mailbox IS a namespace */
+#define kNewlyCreatedFolder 0x400 /* this folder was just created */
+#define kImapDrafts 0x800 /* XLIST says this is the drafts folder */
+#define kImapSpam 0x1000 /* XLIST says this is the spam folder */
+#define kImapSent 0x2000 /* XLIST says this is the sent folder */
+#define kImapInbox 0x4000 /* XLIST says this is the INBOX folder */
+#define kImapAllMail 0x8000 /* XLIST says this is AllMail (GMail) */
+#define kImapXListTrash 0x10000 /* XLIST says this is the trash */
+#define kNonExistent 0x20000 /* RFC 5258, LIST-EXTENDED */
+#define kSubscribed 0x40000 /* RFC 5258, LIST-EXTENDED */
+#define kRemote 0x80000 /* RFC 5258, LIST-EXTENDED */
+#define kHasChildren 0x100000 /* RFC 5258, LIST-EXTENDED */
+#define kHasNoChildren 0x200000 /* RFC 5258, LIST-EXTENDED */
+#define kImapArchive 0x400000 /* RFC 5258, LIST-EXTENDED */
+
+/* flags for individual messages */
+/* currently the ui only offers \Seen and \Flagged */
+#define kNoImapMsgFlag 0x0000
+#define kImapMsgSeenFlag 0x0001
+#define kImapMsgAnsweredFlag 0x0002
+#define kImapMsgFlaggedFlag 0x0004
+#define kImapMsgDeletedFlag 0x0008
+#define kImapMsgDraftFlag 0x0010
+#define kImapMsgRecentFlag 0x0020
+#define kImapMsgForwardedFlag 0x0040 /* Not always supported, check mailbox folder */
+#define kImapMsgMDNSentFlag 0x0080 /* Not always supported. check mailbox folder */
+#define kImapMsgCustomKeywordFlag 0x0100 /* this msg has a custom keyword */
+#define kImapMsgSupportMDNSentFlag 0x2000
+#define kImapMsgSupportForwardedFlag 0x4000
+/**
+ * We use a separate xlist trash flag so we can prefer the GMail trash
+ * over an existing Trash folder we may have created.
+ */
+#define kImapMsgSupportUserFlag 0x8000
+/* This seems to be the most cost effective way of
+* piggying back the server support user flag info.
+*/
+
+/* if a url creator does not know the hierarchyDelimiter, use this */
+#define kOnlineHierarchySeparatorUnknown '^'
+#define kOnlineHierarchySeparatorNil '|'
+
+#define IMAP_URL_TOKEN_SEPARATOR ">"
+#define kUidUnknown -1
+// Special initial value meaning ACLs need to be loaded from DB.
+#define kAclInvalid ((uint32_t) -1)
+
+// this has to do with Mime Parts on Demand. It used to live in net.h
+// I'm not sure where this will live, but here is OK temporarily
+typedef enum {
+ IMAP_CONTENT_NOT_MODIFIED = 0,
+ IMAP_CONTENT_MODIFIED_VIEW_INLINE,
+ IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS,
+ IMAP_CONTENT_FORCE_CONTENT_NOT_MODIFIED
+} IMAP_ContentModifiedType;
+
+// I think this should really go in an imap.h equivalent file
+typedef enum {
+ kPersonalNamespace = 0,
+ kOtherUsersNamespace,
+ kPublicNamespace,
+ kDefaultNamespace,
+ kUnknownNamespace
+} EIMAPNamespaceType;
+
+
+/**
+ * IMAP server feature, mostly CAPABILITY responses
+ *
+ * one of the cap flags below
+ */
+typedef uint64_t eIMAPCapabilityFlag;
+/**
+ * IMAP server features, mostly CAPABILITY responses
+ *
+ * any set of the cap flags below, i.e.
+ * i.e. 0, 1 or more |eIMAPCapabilityFlag|.
+ */
+typedef uint64_t eIMAPCapabilityFlags;
+
+const eIMAPCapabilityFlag kCapabilityUndefined = 0x00000000;
+const eIMAPCapabilityFlag kCapabilityDefined = 0x00000001;
+const eIMAPCapabilityFlag kHasAuthLoginCapability = 0x00000002; /* AUTH LOGIN (not the same as kHasAuthOldLoginCapability) */
+const eIMAPCapabilityFlag kHasAuthOldLoginCapability = 0x00000004; /* original IMAP login method */
+const eIMAPCapabilityFlag kHasXSenderCapability = 0x00000008;
+const eIMAPCapabilityFlag kIMAP4Capability = 0x00000010; /* RFC1734 */
+const eIMAPCapabilityFlag kIMAP4rev1Capability = 0x00000020; /* RFC2060 */
+const eIMAPCapabilityFlag kIMAP4other = 0x00000040; /* future rev?? */
+const eIMAPCapabilityFlag kNoHierarchyRename = 0x00000080; /* no hierarchy rename */
+const eIMAPCapabilityFlag kACLCapability = 0x00000100; /* ACL extension */
+const eIMAPCapabilityFlag kNamespaceCapability = 0x00000200; /* IMAP4 Namespace Extension */
+const eIMAPCapabilityFlag kHasIDCapability = 0x00000400; /* client user agent id extension */
+const eIMAPCapabilityFlag kXServerInfoCapability = 0x00000800; /* XSERVERINFO extension for admin urls */
+const eIMAPCapabilityFlag kHasAuthPlainCapability = 0x00001000; /* new form of auth plain base64 login */
+const eIMAPCapabilityFlag kUidplusCapability = 0x00002000; /* RFC 2359 UIDPLUS extension */
+const eIMAPCapabilityFlag kLiteralPlusCapability = 0x00004000; /* RFC 2088 LITERAL+ extension */
+const eIMAPCapabilityFlag kAOLImapCapability = 0x00008000; /* aol imap extensions */
+const eIMAPCapabilityFlag kHasLanguageCapability = 0x00010000; /* language extensions */
+const eIMAPCapabilityFlag kHasCRAMCapability = 0x00020000; /* CRAM auth extension */
+const eIMAPCapabilityFlag kQuotaCapability = 0x00040000; /* RFC 2087 quota extension */
+const eIMAPCapabilityFlag kHasIdleCapability = 0x00080000; /* RFC 2177 idle extension */
+const eIMAPCapabilityFlag kHasAuthNTLMCapability = 0x00100000; /* AUTH NTLM extension */
+const eIMAPCapabilityFlag kHasAuthMSNCapability = 0x00200000; /* AUTH MSN extension */
+const eIMAPCapabilityFlag kHasStartTLSCapability = 0x00400000; /* STARTTLS support */
+const eIMAPCapabilityFlag kHasAuthNoneCapability = 0x00800000; /* needs no login */
+const eIMAPCapabilityFlag kHasAuthGssApiCapability = 0x01000000; /* GSSAPI AUTH */
+const eIMAPCapabilityFlag kHasCondStoreCapability = 0x02000000; /* RFC 3551 CondStore extension */
+const eIMAPCapabilityFlag kHasEnableCapability = 0x04000000; /* RFC 5161 ENABLE extension */
+const eIMAPCapabilityFlag kHasXListCapability = 0x08000000; /* XLIST extension */
+const eIMAPCapabilityFlag kHasCompressDeflateCapability = 0x10000000; /* RFC 4978 COMPRESS extension */
+const eIMAPCapabilityFlag kHasAuthExternalCapability = 0x20000000; /* RFC 2222 SASL AUTH EXTERNAL */
+const eIMAPCapabilityFlag kHasMoveCapability = 0x40000000; /* Proposed MOVE RFC */
+const eIMAPCapabilityFlag kHasHighestModSeqCapability = 0x80000000; /* Subset of RFC 3551 */
+// above are 32bit; below start the uint64_t bits 33-64
+const eIMAPCapabilityFlag kHasListExtendedCapability = 0x100000000LL; /* RFC 5258 */
+const eIMAPCapabilityFlag kHasSpecialUseCapability = 0x200000000LL; /* RFC 6154: Sent, Draft etc. folders */
+const eIMAPCapabilityFlag kGmailImapCapability = 0x400000000LL; /* X-GM-EXT-1 capability extension for gmail */
+const eIMAPCapabilityFlag kHasXOAuth2Capability = 0x800000000LL; /* AUTH XOAUTH2 extension */
+const eIMAPCapabilityFlag kHasClientIDCapability = 0x1000000000LL; /* ClientID capability */
+const eIMAPCapabilityFlag kHasUTF8AcceptCapability = 0x2000000000LL; /* RFC 6855: UTF8=ACCEPT */
+
+
+// this used to be part of the connection object class - maybe we should move it into
+// something similar
+typedef enum {
+ kEveryThingRFC822,
+ kEveryThingRFC822Peek,
+ kHeadersRFC822andUid,
+ kUid,
+ kFlags,
+ kRFC822Size,
+ kRFC822HeadersOnly,
+ kMIMEPart,
+ kMIMEHeader,
+ kBodyStart
+} nsIMAPeFetchFields;
+
+typedef struct _utf_name_struct {
+ bool toUtf7Imap;
+ unsigned char *sourceString;
+ unsigned char *convertedString;
+} utf_name_struct;
+
+typedef struct _ProgressInfo {
+ char16_t *message;
+ int32_t currentProgress;
+ int32_t maxProgress;
+} ProgressInfo;
+
+typedef enum {
+ eContinue,
+ eContinueNew,
+ eListMyChildren,
+ eNewServerDirectory,
+ eCancelled
+} EMailboxDiscoverStatus;
+
+typedef enum {
+ kInvalidateQuota,
+ kStoreQuota,
+ kValidateQuota
+} nsImapQuotaAction;
+
+#endif
+// clang-format on
diff --git a/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp b/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp
new file mode 100644
index 0000000000..cc3217d780
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+
+#include "nsImapCore.h"
+#include "nsImapFlagAndUidState.h"
+#include "nsMsgUtils.h"
+#include "prcmon.h"
+#include "nspr.h"
+
+NS_IMPL_ISUPPORTS(nsImapFlagAndUidState, nsIImapFlagAndUidState)
+
+using namespace mozilla;
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfMessages(int32_t* result) {
+ if (!result) return NS_ERROR_NULL_POINTER;
+ *result = fUids.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetUidOfMessage(int32_t zeroBasedIndex,
+ uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ PR_CEnterMonitor(this);
+ *aResult = fUids.SafeElementAt(zeroBasedIndex, nsMsgKey_None);
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapFlagAndUidState::HasMessage(uint32_t uid, bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = fUids.Contains(uid);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetMessageFlags(int32_t zeroBasedIndex,
+ uint16_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = fFlags.SafeElementAt(zeroBasedIndex, kNoImapMsgFlag);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::SetMessageFlags(int32_t zeroBasedIndex,
+ unsigned short flags) {
+ if (zeroBasedIndex < (int32_t)fUids.Length()) fFlags[zeroBasedIndex] = flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfRecentMessages(
+ int32_t* result) {
+ if (!result) return NS_ERROR_NULL_POINTER;
+
+ PR_CEnterMonitor(this);
+ uint32_t counter = 0;
+ int32_t numUnseenMessages = 0;
+
+ for (counter = 0; counter < fUids.Length(); counter++) {
+ if (fFlags[counter] & kImapMsgRecentFlag) numUnseenMessages++;
+ }
+ PR_CExitMonitor(this);
+
+ *result = numUnseenMessages;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetPartialUIDFetch(
+ bool* aPartialUIDFetch) {
+ NS_ENSURE_ARG_POINTER(aPartialUIDFetch);
+ *aPartialUIDFetch = fPartialUIDFetch;
+ return NS_OK;
+}
+
+/* amount to expand for imap entry flags when we need more */
+
+nsImapFlagAndUidState::nsImapFlagAndUidState(int32_t numberOfMessages)
+ : fUids(numberOfMessages),
+ fFlags(numberOfMessages),
+ m_customFlagsHash(10),
+ m_customAttributesHash(10),
+ mLock("nsImapFlagAndUidState.mLock") {
+ fSupportedUserFlags = 0;
+ fNumberDeleted = 0;
+ fPartialUIDFetch = true;
+ fStartCapture = false;
+ fNumAdded = 0;
+}
+
+nsImapFlagAndUidState::~nsImapFlagAndUidState() {}
+
+NS_IMETHODIMP
+nsImapFlagAndUidState::OrSupportedUserFlags(uint16_t flags) {
+ fSupportedUserFlags |= flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapFlagAndUidState::GetSupportedUserFlags(uint16_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = fSupportedUserFlags;
+ return NS_OK;
+}
+
+// we need to reset our flags, (re-read all) but chances are the memory
+// allocation needed will be very close to what we were already using
+
+NS_IMETHODIMP nsImapFlagAndUidState::Reset() {
+ PR_CEnterMonitor(this);
+ fNumberDeleted = 0;
+ m_customFlagsHash.Clear();
+ fUids.Clear();
+ fFlags.Clear();
+ fPartialUIDFetch = true;
+ fStartCapture = false;
+ fNumAdded = 0;
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+// Remove (expunge) a message from our array, since now it is gone for good
+
+NS_IMETHODIMP nsImapFlagAndUidState::ExpungeByIndex(uint32_t msgIndex) {
+ // protect ourselves in case the server gave us an index key of -1 or 0
+ if ((int32_t)msgIndex <= 0) return NS_ERROR_INVALID_ARG;
+
+ if ((uint32_t)fUids.Length() < msgIndex) return NS_ERROR_INVALID_ARG;
+
+ PR_CEnterMonitor(this);
+ msgIndex--; // msgIndex is 1-relative
+ if (fFlags[msgIndex] &
+ kImapMsgDeletedFlag) // see if we already had counted this one as deleted
+ fNumberDeleted--;
+ fUids.RemoveElementAt(msgIndex);
+ fFlags.RemoveElementAt(msgIndex);
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+// adds to sorted list, protects against duplicates and going past array bounds.
+NS_IMETHODIMP nsImapFlagAndUidState::AddUidFlagPair(uint32_t uid,
+ imapMessageFlagsType flags,
+ uint32_t zeroBasedIndex) {
+ if (uid == nsMsgKey_None) // ignore uid of -1
+ return NS_OK;
+ // check for potential overflow in buffer size for uid array
+ if (zeroBasedIndex > 0x3FFFFFFF) return NS_ERROR_INVALID_ARG;
+ PR_CEnterMonitor(this);
+ // make sure there is room for this pair
+ if (zeroBasedIndex >= fUids.Length()) {
+ int32_t sizeToGrowBy = zeroBasedIndex - fUids.Length() + 1;
+ fUids.InsertElementsAt(fUids.Length(), sizeToGrowBy, 0);
+ fFlags.InsertElementsAt(fFlags.Length(), sizeToGrowBy, 0);
+ if (fStartCapture) {
+ // A new partial (CONDSTORE/CHANGEDSINCE) fetch response is occurring
+ // so need to start the count of number of uid/flag combos added.
+ fNumAdded = 0;
+ fStartCapture = false;
+ }
+ fNumAdded++;
+ }
+
+ fUids[zeroBasedIndex] = uid;
+ fFlags[zeroBasedIndex] = flags;
+ if (flags & kImapMsgDeletedFlag) fNumberDeleted++;
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfDeletedMessages(
+ int32_t* numDeletedMessages) {
+ NS_ENSURE_ARG_POINTER(numDeletedMessages);
+ *numDeletedMessages = NumberOfDeletedMessages();
+ return NS_OK;
+}
+
+int32_t nsImapFlagAndUidState::NumberOfDeletedMessages() {
+ return fNumberDeleted;
+}
+
+// since the uids are sorted, start from the back (rb)
+
+uint32_t nsImapFlagAndUidState::GetHighestNonDeletedUID() {
+ uint32_t msgIndex = fUids.Length();
+ do {
+ if (msgIndex <= 0) return (0);
+ msgIndex--;
+ if (fUids[msgIndex] && !(fFlags[msgIndex] & kImapMsgDeletedFlag))
+ return fUids[msgIndex];
+ } while (msgIndex > 0);
+ return 0;
+}
+
+// Has the user read the last message here ? Used when we first open the inbox
+// to see if there really is new mail there.
+
+bool nsImapFlagAndUidState::IsLastMessageUnseen() {
+ uint32_t msgIndex = fUids.Length();
+
+ if (msgIndex <= 0) return false;
+ msgIndex--;
+ // if last message is deleted, it was probably filtered the last time around
+ if (fUids[msgIndex] &&
+ (fFlags[msgIndex] & (kImapMsgSeenFlag | kImapMsgDeletedFlag)))
+ return false;
+ return true;
+}
+
+// find a message flag given a key with non-recursive binary search, since some
+// folders may have thousand of messages, once we find the key set its index, or
+// the index of where the key should be inserted
+
+imapMessageFlagsType nsImapFlagAndUidState::GetMessageFlagsFromUID(
+ uint32_t uid, bool* foundIt, int32_t* ndx) {
+ PR_CEnterMonitor(this);
+ *ndx = (int32_t)fUids.IndexOfFirstElementGt(uid) - 1;
+ *foundIt = *ndx >= 0 && fUids[*ndx] == uid;
+ imapMessageFlagsType retFlags = (*foundIt) ? fFlags[*ndx] : kNoImapMsgFlag;
+ PR_CExitMonitor(this);
+ return retFlags;
+}
+
+NS_IMETHODIMP
+nsImapFlagAndUidState::GetMessageFlagsByUid(uint32_t uid,
+ imapMessageFlagsType* retFlags) {
+ PR_CEnterMonitor(this);
+ int32_t ndx = (int32_t)fUids.IndexOf(uid);
+ bool foundIt = ndx >= 0;
+ *retFlags = foundIt ? fFlags[ndx] : kNoImapMsgFlag;
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::AddUidCustomFlagPair(
+ uint32_t uid, const char* customFlag) {
+ if (!customFlag) return NS_OK;
+
+ MutexAutoLock mon(mLock);
+ nsCString ourCustomFlags;
+ nsCString oldValue;
+ if (m_customFlagsHash.Get(uid, &oldValue)) {
+ // We'll store multiple keys as space-delimited since space is not
+ // a valid character in a keyword. First, we need to look for the
+ // customFlag in the existing flags;
+ nsDependentCString customFlagString(customFlag);
+ int32_t existingCustomFlagPos = oldValue.Find(customFlagString);
+ uint32_t customFlagLen = customFlagString.Length();
+ while (existingCustomFlagPos != kNotFound) {
+ // if existing flags ends with this exact flag, or flag + ' '
+ // and the flag is at the beginning of the string or there is ' ' + flag
+ // then we have this flag already;
+ if (((oldValue.Length() == existingCustomFlagPos + customFlagLen) ||
+ (oldValue.CharAt(existingCustomFlagPos + customFlagLen) == ' ')) &&
+ ((existingCustomFlagPos == 0) ||
+ (oldValue.CharAt(existingCustomFlagPos - 1) == ' ')))
+ return NS_OK;
+ // else, advance to next flag
+ existingCustomFlagPos = oldValue.Find(
+ customFlagString, existingCustomFlagPos + customFlagLen);
+ }
+ ourCustomFlags.Assign(oldValue);
+ ourCustomFlags.Append(' ');
+ ourCustomFlags.Append(customFlag);
+ m_customFlagsHash.Remove(uid);
+ } else {
+ ourCustomFlags.Assign(customFlag);
+ }
+ m_customFlagsHash.InsertOrUpdate(uid, ourCustomFlags);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetCustomFlags(uint32_t uid,
+ char** customFlags) {
+ MutexAutoLock mon(mLock);
+ nsCString value;
+ if (m_customFlagsHash.Get(uid, &value)) {
+ *customFlags = NS_xstrdup(value.get());
+ return (*customFlags) ? NS_OK : NS_ERROR_FAILURE;
+ }
+ *customFlags = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::ClearCustomFlags(uint32_t uid) {
+ MutexAutoLock mon(mLock);
+ m_customFlagsHash.Remove(uid);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::SetCustomAttribute(
+ uint32_t aUid, const nsACString& aCustomAttributeName,
+ const nsACString& aCustomAttributeValue) {
+ nsCString key;
+ key.AppendInt((int64_t)aUid);
+ key.Append(aCustomAttributeName);
+ nsCString value;
+ value.Assign(aCustomAttributeValue);
+ m_customAttributesHash.InsertOrUpdate(key, value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapFlagAndUidState::GetCustomAttribute(
+ uint32_t aUid, const nsACString& aCustomAttributeName,
+ nsACString& aCustomAttributeValue) {
+ nsCString key;
+ key.AppendInt((int64_t)aUid);
+ key.Append(aCustomAttributeName);
+ nsCString val = m_customAttributesHash.Get(key);
+ aCustomAttributeValue.Assign(val);
+ return NS_OK;
+}
diff --git a/comm/mailnews/imap/src/nsImapFlagAndUidState.h b/comm/mailnews/imap/src/nsImapFlagAndUidState.h
new file mode 100644
index 0000000000..b8d2fa2b45
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapFlagAndUidState.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImapFlagAndUidState_h___
+#define nsImapFlagAndUidState_h___
+
+#include "MailNewsTypes.h"
+#include "nsIImapFlagAndUidState.h"
+#include "nsImapCore.h"
+#include "nsTArray.h"
+#include "mozilla/Mutex.h"
+
+const int32_t kImapFlagAndUidStateSize = 100;
+
+#include "nsTHashMap.h"
+
+class nsImapFlagAndUidState : public nsIImapFlagAndUidState {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ explicit nsImapFlagAndUidState(int numberOfMessages);
+
+ NS_DECL_NSIIMAPFLAGANDUIDSTATE
+
+ int32_t NumberOfDeletedMessages();
+
+ imapMessageFlagsType GetMessageFlagsFromUID(uint32_t uid, bool* foundIt,
+ int32_t* ndx);
+
+ bool IsLastMessageUnseen(void);
+ bool GetPartialUIDFetch() { return fPartialUIDFetch; }
+ void SetPartialUIDFetch(bool isPartial) { fPartialUIDFetch = isPartial; }
+ uint32_t GetHighestNonDeletedUID();
+ uint16_t GetSupportedUserFlags() { return fSupportedUserFlags; }
+ void StartCapture() { fStartCapture = true; }
+ uint32_t GetNumAdded() { return fNumAdded; }
+
+ private:
+ virtual ~nsImapFlagAndUidState();
+
+ nsTArray<nsMsgKey> fUids;
+ nsTArray<imapMessageFlagsType> fFlags;
+ // Hash table, mapping uids to extra flags
+ nsTHashMap<nsUint32HashKey, nsCString> m_customFlagsHash;
+ // Hash table, mapping UID+customAttributeName to customAttributeValue.
+ nsTHashMap<nsCStringHashKey, nsCString> m_customAttributesHash;
+ uint16_t fSupportedUserFlags;
+ int32_t fNumberDeleted;
+ bool fPartialUIDFetch;
+ uint32_t fNumAdded;
+ bool fStartCapture;
+ mozilla::Mutex mLock;
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapGenericParser.cpp b/comm/mailnews/imap/src/nsImapGenericParser.cpp
new file mode 100644
index 0000000000..009c7c1e5a
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapGenericParser.cpp
@@ -0,0 +1,407 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+
+#include "nsImapGenericParser.h"
+#include "nsString.h"
+
+////////////////// nsImapGenericParser /////////////////////////
+
+nsImapGenericParser::nsImapGenericParser()
+ : fNextToken(nullptr),
+ fCurrentLine(nullptr),
+ fLineOfTokens(nullptr),
+ fStartOfLineOfTokens(nullptr),
+ fCurrentTokenPlaceHolder(nullptr),
+ fAtEndOfLine(false),
+ fParserState(stateOK) {}
+
+nsImapGenericParser::~nsImapGenericParser() {
+ PR_FREEIF(fCurrentLine);
+ PR_FREEIF(fStartOfLineOfTokens);
+}
+
+void nsImapGenericParser::HandleMemoryFailure() { SetConnected(false); }
+
+void nsImapGenericParser::ResetLexAnalyzer() {
+ PR_FREEIF(fCurrentLine);
+ PR_FREEIF(fStartOfLineOfTokens);
+
+ fNextToken = fCurrentLine = fLineOfTokens = fStartOfLineOfTokens =
+ fCurrentTokenPlaceHolder = nullptr;
+ fAtEndOfLine = false;
+}
+
+bool nsImapGenericParser::LastCommandSuccessful() {
+ return fParserState == stateOK;
+}
+
+void nsImapGenericParser::SetSyntaxError(bool error, const char* msg) {
+ if (error)
+ fParserState |= stateSyntaxErrorFlag;
+ else
+ fParserState &= ~stateSyntaxErrorFlag;
+ NS_ASSERTION(!error, "syntax error in generic parser");
+}
+
+void nsImapGenericParser::SetConnected(bool connected) {
+ if (connected)
+ fParserState &= ~stateDisconnectedFlag;
+ else
+ fParserState |= stateDisconnectedFlag;
+}
+
+void nsImapGenericParser::skip_to_CRLF() {
+ while (Connected() && !fAtEndOfLine) AdvanceToNextToken();
+}
+
+// fNextToken initially should point to
+// a string after the initial open paren ("(")
+// After this call, fNextToken points to the
+// first character after the matching close
+// paren. Only call AdvanceToNextToken() to get the NEXT
+// token after the one returned in fNextToken.
+void nsImapGenericParser::skip_to_close_paren() {
+ int numberOfCloseParensNeeded = 1;
+ while (ContinueParse()) {
+ // go through fNextToken, account for nested parens
+ const char* loc;
+ for (loc = fNextToken; loc && *loc; loc++) {
+ if (*loc == '(')
+ numberOfCloseParensNeeded++;
+ else if (*loc == ')') {
+ numberOfCloseParensNeeded--;
+ if (numberOfCloseParensNeeded == 0) {
+ fNextToken = loc + 1;
+ if (!fNextToken || !*fNextToken) AdvanceToNextToken();
+ return;
+ }
+ } else if (*loc == '{' || *loc == '"') {
+ // quoted or literal
+ fNextToken = loc;
+ char* a = CreateString();
+ PR_FREEIF(a);
+ break; // move to next token
+ }
+ }
+ if (ContinueParse()) AdvanceToNextToken();
+ }
+}
+
+void nsImapGenericParser::AdvanceToNextToken() {
+ if (!fCurrentLine || fAtEndOfLine) AdvanceToNextLine();
+ if (Connected()) {
+ if (!fStartOfLineOfTokens) {
+ // this is the first token of the line; setup tokenizer now
+ fStartOfLineOfTokens = PL_strdup(fCurrentLine);
+ if (!fStartOfLineOfTokens) {
+ HandleMemoryFailure();
+ return;
+ }
+ fLineOfTokens = fStartOfLineOfTokens;
+ fCurrentTokenPlaceHolder = fStartOfLineOfTokens;
+ }
+ fNextToken = NS_strtok(WHITESPACE, &fCurrentTokenPlaceHolder);
+ if (!fNextToken) {
+ fAtEndOfLine = true;
+ fNextToken = CRLF;
+ }
+ }
+}
+
+void nsImapGenericParser::AdvanceToNextLine() {
+ PR_FREEIF(fCurrentLine);
+ PR_FREEIF(fStartOfLineOfTokens);
+
+ bool ok = GetNextLineForParser(&fCurrentLine);
+ if (!ok) {
+ SetConnected(false);
+ fStartOfLineOfTokens = nullptr;
+ fLineOfTokens = nullptr;
+ fCurrentTokenPlaceHolder = nullptr;
+ fAtEndOfLine = true;
+ fNextToken = CRLF;
+ } else if (!fCurrentLine) {
+ HandleMemoryFailure();
+ } else {
+ fNextToken = nullptr;
+ // determine if there are any tokens (without calling AdvanceToNextToken);
+ // otherwise we are already at end of line
+ NS_ASSERTION(strlen(WHITESPACE) == 3, "assume 3 chars of whitespace");
+ char* firstToken = fCurrentLine;
+ while (*firstToken &&
+ (*firstToken == WHITESPACE[0] || *firstToken == WHITESPACE[1] ||
+ *firstToken == WHITESPACE[2]))
+ firstToken++;
+ fAtEndOfLine = (*firstToken == '\0');
+ }
+}
+
+// advances |fLineOfTokens| by |bytesToAdvance| bytes
+void nsImapGenericParser::AdvanceTokenizerStartingPoint(
+ int32_t bytesToAdvance) {
+ NS_ASSERTION(bytesToAdvance >= 0, "bytesToAdvance must not be negative");
+ if (!fStartOfLineOfTokens) {
+ AdvanceToNextToken(); // the tokenizer was not yet initialized, do it now
+ if (!fStartOfLineOfTokens) return;
+ }
+
+ if (!fStartOfLineOfTokens) return;
+ // The last call to AdvanceToNextToken() cleared the token separator to '\0'
+ // iff |fCurrentTokenPlaceHolder|. We must recover this token separator now.
+ if (fCurrentTokenPlaceHolder) {
+ int endTokenOffset = fCurrentTokenPlaceHolder - fStartOfLineOfTokens - 1;
+ if (endTokenOffset >= 0)
+ fStartOfLineOfTokens[endTokenOffset] = fCurrentLine[endTokenOffset];
+ }
+
+ NS_ASSERTION(bytesToAdvance + (fLineOfTokens - fStartOfLineOfTokens) <=
+ (int32_t)strlen(fCurrentLine),
+ "cannot advance beyond end of fLineOfTokens");
+ fLineOfTokens += bytesToAdvance;
+ fCurrentTokenPlaceHolder = fLineOfTokens;
+}
+
+// RFC3501: astring = 1*ASTRING-CHAR / string
+// string = quoted / literal
+// This function leaves us off with fCurrentTokenPlaceHolder immediately after
+// the end of the Astring. Call AdvanceToNextToken() to get the token after it.
+char* nsImapGenericParser::CreateAstring() {
+ if (*fNextToken == '{') return CreateLiteral(); // literal
+ if (*fNextToken == '"') return CreateQuoted(); // quoted
+ return CreateAtom(true); // atom
+}
+
+// Create an atom
+// This function does not advance the parser.
+// Call AdvanceToNextToken() to get the next token after the atom.
+// RFC3501: atom = 1*ATOM-CHAR
+// ASTRING-CHAR = ATOM-CHAR / resp-specials
+// ATOM-CHAR = <any CHAR except atom-specials>
+// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards /
+// quoted-specials / resp-specials
+// list-wildcards = "%" / "*"
+// quoted-specials = DQUOTE / "\"
+// resp-specials = "]"
+// "Characters are 7-bit US-ASCII unless otherwise specified." [RFC3501, 1.2.]
+char* nsImapGenericParser::CreateAtom(bool isAstring) {
+ char* rv = PL_strdup(fNextToken);
+ if (!rv) {
+ HandleMemoryFailure();
+ return nullptr;
+ }
+ // We wish to stop at the following characters (in decimal ascii)
+ // 1-31 (CTL), 32 (SP), 34 '"', 37 '%', 40-42 "()*", 92 '\\', 123 '{'
+ // also, ']' is only allowed in astrings
+ char* last = rv;
+ char c = *last;
+ while ((c > 42 || c == 33 || c == 35 || c == 36 || c == 38 || c == 39) &&
+ c != '\\' && c != '{' && (isAstring || c != ']'))
+ c = *++last;
+ if (rv == last) {
+ SetSyntaxError(true, "no atom characters found");
+ PL_strfree(rv);
+ return nullptr;
+ }
+ if (*last) {
+ // not the whole token was consumed
+ *last = '\0';
+ AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + (last - rv));
+ }
+ return rv;
+}
+
+// CreateNilString return either NULL (for "NIL") or a string
+// Call with fNextToken pointing to the thing which we think is the nilstring.
+// This function leaves us off with fCurrentTokenPlaceHolder immediately after
+// the end of the string.
+// Regardless of type, call AdvanceToNextToken() to get the token after it.
+// RFC3501: nstring = string / nil
+// nil = "NIL"
+char* nsImapGenericParser::CreateNilString() {
+ if (!PL_strncasecmp(fNextToken, "NIL", 3)) {
+ // check if there is text after "NIL" in fNextToken,
+ // equivalent handling as in CreateQuoted
+ if (fNextToken[3])
+ AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + 3);
+ return NULL;
+ }
+ return CreateString();
+}
+
+// Create a string, which can either be quoted or literal,
+// but not an atom.
+// This function leaves us off with fCurrentTokenPlaceHolder immediately after
+// the end of the String. Call AdvanceToNextToken() to get the token after it.
+char* nsImapGenericParser::CreateString() {
+ if (*fNextToken == '{') {
+ char* rv = CreateLiteral(); // literal
+ return (rv);
+ }
+ if (*fNextToken == '"') {
+ char* rv = CreateQuoted(); // quoted
+ return (rv);
+ }
+ SetSyntaxError(true, "string does not start with '{' or '\"'");
+ return NULL;
+}
+
+// This function sets fCurrentTokenPlaceHolder immediately after the end of the
+// closing quote. Call AdvanceToNextToken() to get the token after it.
+// QUOTED_CHAR ::= <any TEXT_CHAR except quoted_specials> /
+// "\" quoted_specials
+// TEXT_CHAR ::= <any CHAR except CR and LF>
+// quoted_specials ::= <"> / "\"
+// Note that according to RFC 1064 and RFC 2060, CRs and LFs are not allowed
+// inside a quoted string. It is sufficient to read from the current line only.
+char* nsImapGenericParser::CreateQuoted(bool /*skipToEnd*/) {
+ // one char past opening '"'
+ char* currentChar = fCurrentLine + (fNextToken - fStartOfLineOfTokens) + 1;
+
+ int escapeCharsCut = 0;
+ nsCString returnString(currentChar);
+ int charIndex;
+ for (charIndex = 0; returnString.CharAt(charIndex) != '"'; charIndex++) {
+ if (!returnString.CharAt(charIndex)) {
+ SetSyntaxError(true, "no closing '\"' found in quoted");
+ return nullptr;
+ }
+ if (returnString.CharAt(charIndex) == '\\') {
+ // eat the escape character, but keep the escaped character
+ returnString.Cut(charIndex, 1);
+ escapeCharsCut++;
+ }
+ }
+ // +2 because of the start and end quotes
+ AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + charIndex +
+ escapeCharsCut + 2);
+
+ returnString.SetLength(charIndex);
+ return ToNewCString(returnString);
+}
+
+// This function leaves us off with fCurrentTokenPlaceHolder immediately after
+// the end of the literal string. Call AdvanceToNextToken() to get the token
+// after the literal string.
+// RFC3501: literal = "{" number "}" CRLF *CHAR8
+// ; Number represents the number of CHAR8s
+// CHAR8 = %x01-ff
+// ; any OCTET except NUL, %x00
+char* nsImapGenericParser::CreateLiteral() {
+ int32_t numberOfCharsInMessage = atoi(fNextToken + 1);
+ uint32_t numBytes = numberOfCharsInMessage + 1;
+ NS_ASSERTION(numBytes, "overflow!");
+ if (!numBytes) return nullptr;
+ char* returnString = (char*)PR_Malloc(numBytes);
+ if (!returnString) {
+ HandleMemoryFailure();
+ return nullptr;
+ }
+
+ int32_t currentLineLength = 0;
+ int32_t charsReadSoFar = 0;
+ int32_t bytesToCopy = 0;
+ while (charsReadSoFar < numberOfCharsInMessage) {
+ AdvanceToNextLine();
+ if (!ContinueParse()) break;
+
+ currentLineLength = strlen(fCurrentLine);
+ bytesToCopy = (currentLineLength > numberOfCharsInMessage - charsReadSoFar
+ ? numberOfCharsInMessage - charsReadSoFar
+ : currentLineLength);
+ NS_ASSERTION(bytesToCopy, "zero-length line?");
+ memcpy(returnString + charsReadSoFar, fCurrentLine, bytesToCopy);
+ charsReadSoFar += bytesToCopy;
+ }
+
+ if (ContinueParse()) {
+ if (currentLineLength == bytesToCopy) {
+ // We have consumed the entire line.
+ // Consider the input "{4}\r\n" "L1\r\n" " A2\r\n" which is read
+ // line-by-line. Reading an Astring, this should result in "L1\r\n".
+ // Note that the second line is "L1\r\n", where the "\r\n" is part of
+ // the literal. Hence, we now read the next line to ensure that the
+ // next call to AdvanceToNextToken() leads to fNextToken=="A2" in our
+ // example.
+ AdvanceToNextLine();
+ } else
+ AdvanceTokenizerStartingPoint(bytesToCopy);
+ }
+
+ returnString[charsReadSoFar] = 0;
+ return returnString;
+}
+
+// Call this to create a buffer containing all characters within
+// a given set of parentheses.
+// Call this with fNextToken[0]=='(', that is, the open paren
+// of the group.
+// It will allocate and return all characters up to and including the
+// corresponding closing paren, and leave the parser in the right place
+// afterwards.
+char* nsImapGenericParser::CreateParenGroup() {
+ NS_ASSERTION(fNextToken[0] == '(', "we don't have a paren group!");
+
+ int numOpenParens = 0;
+ AdvanceTokenizerStartingPoint(fNextToken - fLineOfTokens);
+
+ // Build up a buffer containing the paren group.
+ nsCString returnString;
+ char* parenGroupStart = fCurrentTokenPlaceHolder;
+ NS_ASSERTION(parenGroupStart[0] == '(', "we don't have a paren group (2)!");
+ while (*fCurrentTokenPlaceHolder) {
+ if (*fCurrentTokenPlaceHolder == '{') // literal
+ {
+ // Ensure it is a properly formatted literal.
+ NS_ASSERTION(!strcmp("}\r\n", fCurrentTokenPlaceHolder +
+ strlen(fCurrentTokenPlaceHolder) - 3),
+ "not a literal");
+
+ // Append previous characters and the "{xx}\r\n" to buffer.
+ returnString.Append(parenGroupStart);
+
+ // Append literal itself.
+ AdvanceToNextToken();
+ if (!ContinueParse()) break;
+ char* lit = CreateLiteral();
+ NS_ASSERTION(lit, "syntax error or out of memory");
+ if (!lit) break;
+ returnString.Append(lit);
+ PR_Free(lit);
+ if (!ContinueParse()) break;
+ parenGroupStart = fCurrentTokenPlaceHolder;
+ } else if (*fCurrentTokenPlaceHolder == '"') // quoted
+ {
+ // Append the _escaped_ version of the quoted string:
+ // just skip it (because the quoted string must be on the same line).
+ AdvanceToNextToken();
+ if (!ContinueParse()) break;
+ char* q = CreateQuoted();
+ if (!q) break;
+ PR_Free(q);
+ if (!ContinueParse()) break;
+ } else {
+ // Append this character to the buffer.
+ char c = *fCurrentTokenPlaceHolder++;
+ if (c == '(')
+ numOpenParens++;
+ else if (c == ')') {
+ numOpenParens--;
+ if (numOpenParens == 0) break;
+ }
+ }
+ }
+
+ if (numOpenParens != 0 || !ContinueParse()) {
+ SetSyntaxError(true, "closing ')' not found in paren group");
+ return nullptr;
+ }
+
+ returnString.Append(parenGroupStart,
+ fCurrentTokenPlaceHolder - parenGroupStart);
+ AdvanceToNextToken();
+ return ToNewCString(returnString);
+}
diff --git a/comm/mailnews/imap/src/nsImapGenericParser.h b/comm/mailnews/imap/src/nsImapGenericParser.h
new file mode 100644
index 0000000000..50abffa5e1
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapGenericParser.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+nsImapGenericParser is the base parser class used by the server parser and body
+shell parser
+*/
+
+#ifndef nsImapGenericParser_H
+#define nsImapGenericParser_H
+
+#define WHITESPACE " \015\012" // token delimiter
+
+class nsImapGenericParser {
+ public:
+ nsImapGenericParser();
+ virtual ~nsImapGenericParser();
+
+ // Add any specific stuff in the derived class
+ virtual bool LastCommandSuccessful();
+
+ bool SyntaxError() { return (fParserState & stateSyntaxErrorFlag) != 0; }
+ bool ContinueParse() { return fParserState == stateOK; }
+ bool Connected() { return !(fParserState & stateDisconnectedFlag); }
+ void SetConnected(bool error);
+
+ protected:
+ // This is a pure virtual member which must be overridden in the derived class
+ // for each different implementation of a nsImapGenericParser.
+ // For instance, one implementation (the nsIMAPServerState) might get the next
+ // line from an open socket, whereas another implementation might just get it
+ // from a buffer somewhere. This fills in nextLine with the buffer, and
+ // returns true if everything is OK. Returns false if there was some error
+ // encountered. In that case, we reset the parser.
+ virtual bool GetNextLineForParser(char** nextLine) = 0;
+
+ virtual void HandleMemoryFailure();
+ void skip_to_CRLF();
+ void skip_to_close_paren();
+ char* CreateString();
+ char* CreateAstring();
+ char* CreateNilString();
+ char* CreateLiteral();
+ char* CreateAtom(bool isAstring = false);
+ char* CreateQuoted(bool skipToEnd = true);
+ char* CreateParenGroup();
+ virtual void SetSyntaxError(bool error, const char* msg);
+
+ void AdvanceToNextToken();
+ void AdvanceToNextLine();
+ void AdvanceTokenizerStartingPoint(int32_t bytesToAdvance);
+ void ResetLexAnalyzer();
+
+ protected:
+ // use with care
+ const char* fNextToken;
+ char* fCurrentLine;
+ char* fLineOfTokens;
+ char* fStartOfLineOfTokens;
+ char* fCurrentTokenPlaceHolder;
+ bool fAtEndOfLine;
+
+ private:
+ enum nsImapGenericParserState {
+ stateOK = 0,
+ stateSyntaxErrorFlag = 0x1,
+ stateDisconnectedFlag = 0x2
+ };
+ uint32_t fParserState;
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapHostSessionList.cpp b/comm/mailnews/imap/src/nsImapHostSessionList.cpp
new file mode 100644
index 0000000000..fbdbeac231
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapHostSessionList.cpp
@@ -0,0 +1,595 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsImapHostSessionList.h"
+#include "nsImapNamespace.h"
+#include "nsIImapIncomingServer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Services.h"
+
+nsIMAPHostInfo::nsIMAPHostInfo(const char* serverKey,
+ nsIImapIncomingServer* server) {
+ fServerKey = serverKey;
+ NS_ASSERTION(server, "*** Fatal null imap incoming server...");
+ server->GetServerDirectory(fOnlineDir);
+ fNextHost = NULL;
+ fCapabilityFlags = kCapabilityUndefined;
+ fHierarchyDelimiters = NULL;
+#ifdef DEBUG_bienvenu1
+ fHaveWeEverDiscoveredFolders =
+ true; // try this, see what bad happens - we'll need to
+ // figure out a way to make new accounts have it be false
+#else
+ fHaveWeEverDiscoveredFolders = false; // try this, see what bad happens
+#endif
+ fDiscoveryForHostInProgress = false;
+ fCanonicalOnlineSubDir = NULL;
+ fNamespaceList = nsImapNamespaceList::CreatensImapNamespaceList();
+ fUsingSubscription = true;
+ server->GetUsingSubscription(&fUsingSubscription);
+ fOnlineTrashFolderExists = false;
+ fShouldAlwaysListInbox = true;
+ fPasswordVerifiedOnline = false;
+ fDeleteIsMoveToTrash = true;
+ fShowDeletedMessages = false;
+ fGotNamespaces = false;
+ fHaveAdminURL = false;
+ fNamespacesOverridable = true;
+ server->GetOverrideNamespaces(&fNamespacesOverridable);
+ fTempNamespaceList = nsImapNamespaceList::CreatensImapNamespaceList();
+}
+
+nsIMAPHostInfo::~nsIMAPHostInfo() {
+ PR_Free(fHierarchyDelimiters);
+ delete fNamespaceList;
+ delete fTempNamespaceList;
+}
+
+NS_IMPL_ISUPPORTS(nsImapHostSessionList, nsIImapHostSessionList, nsIObserver,
+ nsISupportsWeakReference)
+
+nsImapHostSessionList::nsImapHostSessionList() {
+ gCachedHostInfoMonitor = PR_NewMonitor(/* "accessing-hostlist-monitor"*/);
+ fHostInfoList = nullptr;
+}
+
+nsImapHostSessionList::~nsImapHostSessionList() {
+ ResetAll();
+ PR_DestroyMonitor(gCachedHostInfoMonitor);
+}
+
+nsresult nsImapHostSessionList::Init() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+ observerService->AddObserver(this, "profile-before-change", true);
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "profile-before-change"))
+ ResetAll();
+ else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ observerService->RemoveObserver(this, "profile-before-change");
+ }
+ return NS_OK;
+}
+
+nsIMAPHostInfo* nsImapHostSessionList::FindHost(const char* serverKey) {
+ nsIMAPHostInfo* host;
+
+ // ### should also check userName here, if NON NULL
+ for (host = fHostInfoList; host; host = host->fNextHost) {
+ if (host->fServerKey.Equals(serverKey, nsCaseInsensitiveCStringComparator))
+ return host;
+ }
+ return host;
+}
+
+// reset any cached connection info - delete the lot of 'em
+NS_IMETHODIMP nsImapHostSessionList::ResetAll() {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* nextHost = NULL;
+ for (nsIMAPHostInfo* host = fHostInfoList; host; host = nextHost) {
+ nextHost = host->fNextHost;
+ delete host;
+ }
+ fHostInfoList = NULL;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapHostSessionList::AddHostToList(const char* serverKey,
+ nsIImapIncomingServer* server) {
+ nsIMAPHostInfo* newHost = NULL;
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ if (!FindHost(serverKey)) {
+ // stick it on the front
+ newHost = new nsIMAPHostInfo(serverKey, server);
+ if (newHost) {
+ newHost->fNextHost = fHostInfoList;
+ fHostInfoList = newHost;
+ }
+ }
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (newHost == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetPasswordForHost(const char* serverKey,
+ nsString& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fCachedPassword;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetPasswordForHost(
+ const char* serverKey, const nsAString& password) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fCachedPassword = password;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetPasswordVerifiedOnline(
+ const char* serverKey) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fPasswordVerifiedOnline = true;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetPasswordVerifiedOnline(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fPasswordVerifiedOnline;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetOnlineDirForHost(const char* serverKey,
+ nsString& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) CopyASCIItoUTF16(host->fOnlineDir, result);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetOnlineDirForHost(
+ const char* serverKey, const char* onlineDir) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) {
+ if (onlineDir) host->fOnlineDir = onlineDir;
+ }
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetDeleteIsMoveToTrashForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fDeleteIsMoveToTrash;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetShowDeletedMessagesForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fShowDeletedMessages;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetDeleteIsMoveToTrashForHost(
+ const char* serverKey, bool isMoveToTrash) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fDeleteIsMoveToTrash = isMoveToTrash;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetShowDeletedMessagesForHost(
+ const char* serverKey, bool showDeletedMessages) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fShowDeletedMessages = showDeletedMessages;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetGotNamespacesForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fGotNamespaces;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetGotNamespacesForHost(
+ const char* serverKey, bool gotNamespaces) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fGotNamespaces = gotNamespaces;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetHostIsUsingSubscription(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fUsingSubscription;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetHostIsUsingSubscription(
+ const char* serverKey, bool usingSubscription) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fUsingSubscription = usingSubscription;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetHostHasAdminURL(const char* serverKey,
+ bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fHaveAdminURL;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetHostHasAdminURL(const char* serverKey,
+ bool haveAdminURL) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fHaveAdminURL = haveAdminURL;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetHaveWeEverDiscoveredFoldersForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fHaveWeEverDiscoveredFolders;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetHaveWeEverDiscoveredFoldersForHost(
+ const char* serverKey, bool discovered) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fHaveWeEverDiscoveredFolders = discovered;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetDiscoveryForHostInProgress(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host)
+ result = host->fDiscoveryForHostInProgress;
+ else
+ result = false;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetDiscoveryForHostInProgress(
+ const char* serverKey, bool inProgress) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fDiscoveryForHostInProgress = inProgress;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetOnlineTrashFolderExistsForHost(
+ const char* serverKey, bool exists) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fOnlineTrashFolderExists = exists;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetOnlineTrashFolderExistsForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fOnlineTrashFolderExists;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::AddNewNamespaceForHost(
+ const char* serverKey, nsImapNamespace* ns) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fNamespaceList->AddNewNamespace(ns);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetNamespaceFromPrefForHost(
+ const char* serverKey, const char* namespacePref,
+ EIMAPNamespaceType nstype) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) {
+ if (namespacePref) {
+ int numNamespaces = host->fNamespaceList->UnserializeNamespaces(
+ namespacePref, nullptr, 0);
+ char** prefixes = (char**)PR_CALLOC(numNamespaces * sizeof(char*));
+ if (prefixes) {
+ int len = host->fNamespaceList->UnserializeNamespaces(
+ namespacePref, prefixes, numNamespaces);
+ for (int i = 0; i < len; i++) {
+ char* thisns = prefixes[i];
+ char delimiter = '/'; // a guess
+ if (PL_strlen(thisns) >= 1) delimiter = thisns[PL_strlen(thisns) - 1];
+ nsImapNamespace* ns =
+ new nsImapNamespace(nstype, thisns, delimiter, true);
+ if (ns) host->fNamespaceList->AddNewNamespace(ns);
+ PR_FREEIF(thisns);
+ }
+ PR_Free(prefixes);
+ }
+ }
+ }
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetNamespaceForMailboxForHost(
+ const char* serverKey, const char* mailbox_name, nsImapNamespace*& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fNamespaceList->GetNamespaceForMailbox(mailbox_name);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::ClearPrefsNamespacesForHost(
+ const char* serverKey) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fNamespaceList->ClearNamespaces(true, false, true);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::ClearServerAdvertisedNamespacesForHost(
+ const char* serverKey) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fNamespaceList->ClearNamespaces(false, true, true);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetDefaultNamespaceOfTypeForHost(
+ const char* serverKey, EIMAPNamespaceType type, nsImapNamespace*& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fNamespaceList->GetDefaultNamespaceOfType(type);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetNamespacesOverridableForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fNamespacesOverridable;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetNamespacesOverridableForHost(
+ const char* serverKey, bool overridable) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fNamespacesOverridable = overridable;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetNumberOfNamespacesForHost(
+ const char* serverKey, uint32_t& result) {
+ int32_t intResult = 0;
+
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) intResult = host->fNamespaceList->GetNumberOfNamespaces();
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ NS_ASSERTION(intResult >= 0, "negative number of namespaces");
+ result = (uint32_t)intResult;
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetNamespaceNumberForHost(
+ const char* serverKey, int32_t n, nsImapNamespace*& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) result = host->fNamespaceList->GetNamespaceNumber(n);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+nsresult nsImapHostSessionList::SetNamespacesPrefForHost(
+ nsIImapIncomingServer* aHost, EIMAPNamespaceType type, const char* pref) {
+ if (type == kPersonalNamespace)
+ aHost->SetPersonalNamespace(nsDependentCString(pref));
+ else if (type == kPublicNamespace)
+ aHost->SetPublicNamespace(nsDependentCString(pref));
+ else if (type == kOtherUsersNamespace)
+ aHost->SetOtherUsersNamespace(nsDependentCString(pref));
+ else
+ NS_ASSERTION(false, "bogus namespace type");
+ return NS_OK;
+}
+// do we need this? What should we do about the master thing?
+// Make sure this is running in the Mozilla thread when called
+NS_IMETHODIMP nsImapHostSessionList::CommitNamespacesForHost(
+ nsIImapIncomingServer* aHost) {
+ NS_ENSURE_ARG_POINTER(aHost);
+ nsCString serverKey;
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer = do_QueryInterface(aHost);
+ if (!incomingServer) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv = incomingServer->GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey.get());
+ if (host) {
+ host->fGotNamespaces =
+ true; // so we only issue NAMESPACE once per host per session.
+ EIMAPNamespaceType type = kPersonalNamespace;
+ for (int i = 1; i <= 3; i++) {
+ switch (i) {
+ case 1:
+ type = kPersonalNamespace;
+ break;
+ case 2:
+ type = kPublicNamespace;
+ break;
+ case 3:
+ type = kOtherUsersNamespace;
+ break;
+ default:
+ type = kPersonalNamespace;
+ break;
+ }
+
+ int32_t numInNS = host->fNamespaceList->GetNumberOfNamespaces(type);
+ if (numInNS == 0)
+ SetNamespacesPrefForHost(aHost, type, "");
+ else if (numInNS >= 1) {
+ char* pref = PR_smprintf("");
+ for (int count = 1; count <= numInNS; count++) {
+ nsImapNamespace* ns =
+ host->fNamespaceList->GetNamespaceNumber(count, type);
+ if (ns) {
+ if (count > 1) {
+ // append the comma
+ char* tempPref = PR_smprintf("%s,", pref);
+ PR_FREEIF(pref);
+ pref = tempPref;
+ }
+ char* tempPref = PR_smprintf("%s\"%s\"", pref, ns->GetPrefix());
+ PR_FREEIF(pref);
+ pref = tempPref;
+ }
+ }
+ if (pref) {
+ SetNamespacesPrefForHost(aHost, type, pref);
+ PR_Free(pref);
+ }
+ }
+ }
+ // clear, but don't delete the entries in, the temp namespace list
+ host->fTempNamespaceList->ClearNamespaces(true, true, false);
+
+ // Now reset all of libmsg's namespace references.
+ // Did I mention this needs to be running in the mozilla thread?
+ aHost->ResetNamespaceReferences();
+ }
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::FlushUncommittedNamespacesForHost(
+ const char* serverKey, bool& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fTempNamespaceList->ClearNamespaces(true, true, true);
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+// Returns NULL if there is no personal namespace on the given host
+NS_IMETHODIMP nsImapHostSessionList::GetOnlineInboxPathForHost(
+ const char* serverKey, nsString& result) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) {
+ nsImapNamespace* ns = NULL;
+ ns = host->fNamespaceList->GetDefaultNamespaceOfType(kPersonalNamespace);
+ if (ns) {
+ CopyASCIItoUTF16(nsDependentCString(ns->GetPrefix()), result);
+ result.AppendLiteral("INBOX");
+ }
+ } else
+ result.Truncate();
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::GetShouldAlwaysListInboxForHost(
+ const char* /*serverKey*/, bool& result) {
+ result = true;
+
+ /*
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo *host = FindHost(serverKey);
+ if (host)
+ ret = host->fShouldAlwaysListInbox;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ */
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapHostSessionList::SetShouldAlwaysListInboxForHost(
+ const char* serverKey, bool shouldList) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) host->fShouldAlwaysListInbox = shouldList;
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapHostSessionList::SetNamespaceHierarchyDelimiterFromMailboxForHost(
+ const char* serverKey, const char* boxName, char delimiter) {
+ PR_EnterMonitor(gCachedHostInfoMonitor);
+ nsIMAPHostInfo* host = FindHost(serverKey);
+ if (host) {
+ nsImapNamespace* ns = host->fNamespaceList->GetNamespaceForMailbox(boxName);
+ if (ns && !ns->GetIsDelimiterFilledIn()) ns->SetDelimiter(delimiter, true);
+ }
+ PR_ExitMonitor(gCachedHostInfoMonitor);
+ return (host) ? NS_OK : NS_ERROR_ILLEGAL_VALUE;
+}
diff --git a/comm/mailnews/imap/src/nsImapHostSessionList.h b/comm/mailnews/imap/src/nsImapHostSessionList.h
new file mode 100644
index 0000000000..325cbae868
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapHostSessionList.h
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsImapHostSessionList_H_
+#define _nsImapHostSessionList_H_
+
+#include "mozilla/Attributes.h"
+#include "nsImapCore.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nspr.h"
+
+class nsImapNamespaceList;
+class nsIImapIncomingServer;
+
+class nsIMAPHostInfo {
+ public:
+ friend class nsImapHostSessionList;
+
+ nsIMAPHostInfo(const char* serverKey, nsIImapIncomingServer* server);
+ ~nsIMAPHostInfo();
+
+ protected:
+ nsCString fServerKey;
+ nsString fCachedPassword;
+ nsCString fOnlineDir;
+ nsIMAPHostInfo* fNextHost;
+ eIMAPCapabilityFlags fCapabilityFlags;
+ char* fHierarchyDelimiters; // string of top-level hierarchy delimiters
+ bool fHaveWeEverDiscoveredFolders;
+ bool fDiscoveryForHostInProgress;
+ char* fCanonicalOnlineSubDir;
+ nsImapNamespaceList *fNamespaceList, *fTempNamespaceList;
+ bool fNamespacesOverridable;
+ bool fUsingSubscription;
+ bool fOnlineTrashFolderExists;
+ bool fShouldAlwaysListInbox;
+ bool fHaveAdminURL;
+ bool fPasswordVerifiedOnline;
+ bool fDeleteIsMoveToTrash;
+ bool fShowDeletedMessages;
+ bool fGotNamespaces;
+};
+
+// this is an interface to a linked list of host info's
+class nsImapHostSessionList : public nsIImapHostSessionList,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ nsImapHostSessionList();
+ nsresult Init();
+ // Host List
+ NS_IMETHOD AddHostToList(const char* serverKey,
+ nsIImapIncomingServer* server) override;
+ NS_IMETHOD ResetAll() override;
+
+ // Capabilities
+ NS_IMETHOD GetHostHasAdminURL(const char* serverKey, bool& result) override;
+ NS_IMETHOD SetHostHasAdminURL(const char* serverKey,
+ bool hasAdminUrl) override;
+ // Subscription
+ NS_IMETHOD GetHostIsUsingSubscription(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetHostIsUsingSubscription(const char* serverKey,
+ bool usingSubscription) override;
+
+ // Passwords
+ NS_IMETHOD GetPasswordForHost(const char* serverKey,
+ nsString& result) override;
+ NS_IMETHOD SetPasswordForHost(const char* serverKey,
+ const nsAString& password) override;
+ NS_IMETHOD GetPasswordVerifiedOnline(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetPasswordVerifiedOnline(const char* serverKey) override;
+
+ // OnlineDir
+ NS_IMETHOD GetOnlineDirForHost(const char* serverKey,
+ nsString& result) override;
+ NS_IMETHOD SetOnlineDirForHost(const char* serverKey,
+ const char* onlineDir) override;
+
+ // Delete is move to trash folder
+ NS_IMETHOD GetDeleteIsMoveToTrashForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetDeleteIsMoveToTrashForHost(const char* serverKey,
+ bool isMoveToTrash) override;
+ // imap delete model (or not)
+ NS_IMETHOD GetShowDeletedMessagesForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetShowDeletedMessagesForHost(const char* serverKey,
+ bool showDeletedMessages) override;
+
+ // Get namespaces
+ NS_IMETHOD GetGotNamespacesForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetGotNamespacesForHost(const char* serverKey,
+ bool gotNamespaces) override;
+ // Folders
+ NS_IMETHOD SetHaveWeEverDiscoveredFoldersForHost(const char* serverKey,
+ bool discovered) override;
+ NS_IMETHOD GetHaveWeEverDiscoveredFoldersForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetDiscoveryForHostInProgress(const char* serverKey,
+ bool inProgress) override;
+ NS_IMETHOD GetDiscoveryForHostInProgress(const char* serverKey,
+ bool& result) override;
+
+ // Trash Folder
+ NS_IMETHOD SetOnlineTrashFolderExistsForHost(const char* serverKey,
+ bool exists) override;
+ NS_IMETHOD GetOnlineTrashFolderExistsForHost(const char* serverKey,
+ bool& result) override;
+
+ // INBOX
+ NS_IMETHOD GetOnlineInboxPathForHost(const char* serverKey,
+ nsString& result) override;
+ NS_IMETHOD GetShouldAlwaysListInboxForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD SetShouldAlwaysListInboxForHost(const char* serverKey,
+ bool shouldList) override;
+
+ // Namespaces
+ NS_IMETHOD GetNamespaceForMailboxForHost(const char* serverKey,
+ const char* mailbox_name,
+ nsImapNamespace*& result) override;
+ NS_IMETHOD SetNamespaceFromPrefForHost(const char* serverKey,
+ const char* namespacePref,
+ EIMAPNamespaceType type) override;
+ NS_IMETHOD AddNewNamespaceForHost(const char* serverKey,
+ nsImapNamespace* ns) override;
+ NS_IMETHOD ClearServerAdvertisedNamespacesForHost(
+ const char* serverKey) override;
+ NS_IMETHOD ClearPrefsNamespacesForHost(const char* serverKey) override;
+ NS_IMETHOD GetDefaultNamespaceOfTypeForHost(
+ const char* serverKey, EIMAPNamespaceType type,
+ nsImapNamespace*& result) override;
+ NS_IMETHOD SetNamespacesOverridableForHost(const char* serverKey,
+ bool overridable) override;
+ NS_IMETHOD GetNamespacesOverridableForHost(const char* serverKey,
+ bool& result) override;
+ NS_IMETHOD GetNumberOfNamespacesForHost(const char* serverKey,
+ uint32_t& result) override;
+ NS_IMETHOD GetNamespaceNumberForHost(const char* serverKey, int32_t n,
+ nsImapNamespace*& result) override;
+ // ### dmb hoo boy, how are we going to do this?
+ NS_IMETHOD CommitNamespacesForHost(nsIImapIncomingServer* host) override;
+ NS_IMETHOD FlushUncommittedNamespacesForHost(const char* serverKey,
+ bool& result) override;
+
+ // Hierarchy Delimiters
+ NS_IMETHOD SetNamespaceHierarchyDelimiterFromMailboxForHost(
+ const char* serverKey, const char* boxName, char delimiter) override;
+
+ PRMonitor* gCachedHostInfoMonitor;
+ nsIMAPHostInfo* fHostInfoList;
+
+ protected:
+ virtual ~nsImapHostSessionList();
+ nsresult SetNamespacesPrefForHost(nsIImapIncomingServer* aHost,
+ EIMAPNamespaceType type, const char* pref);
+ nsIMAPHostInfo* FindHost(const char* serverKey);
+};
+#endif
diff --git a/comm/mailnews/imap/src/nsImapIncomingServer.cpp b/comm/mailnews/imap/src/nsImapIncomingServer.cpp
new file mode 100644
index 0000000000..67b5df6d53
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapIncomingServer.cpp
@@ -0,0 +1,3032 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+
+#include "netCore.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsImapIncomingServer.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgIdentity.h"
+#include "nsIImapUrl.h"
+#include "nsIUrlListener.h"
+#include "nsThreadUtils.h"
+#include "nsImapProtocol.h"
+#include "nsCOMPtr.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsMsgFolderFlags.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgWindow.h"
+#include "nsImapMailFolder.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIImapService.h"
+#include "nsMsgI18N.h"
+#include "nsIImapMockChannel.h"
+// for the memory cache...
+#include "nsICacheEntry.h"
+#include "nsImapUrl.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIMsgMailSession.h"
+#include "nsImapNamespace.h"
+#include "nsArrayUtils.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCRTGlue.h"
+#include "mozilla/Components.h"
+#include "nsNetUtil.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/LoadInfo.h"
+
+using namespace mozilla;
+
+// Despite its name, this contains a folder path, for example INBOX/Trash.
+#define PREF_TRASH_FOLDER_PATH "trash_folder_name"
+#define DEFAULT_TRASH_FOLDER_PATH "Trash" // XXX Is this a useful default?
+
+#define NS_SUBSCRIBABLESERVER_CID \
+ { \
+ 0x8510876a, 0x1dd2, 0x11b2, { \
+ 0x82, 0x53, 0x91, 0xf7, 0x1b, 0x34, 0x8a, 0x25 \
+ } \
+ }
+static NS_DEFINE_CID(kSubscribableServerCID, NS_SUBSCRIBABLESERVER_CID);
+#define NS_IIMAPHOSTSESSIONLIST_CID \
+ { \
+ 0x479ce8fc, 0xe725, 0x11d2, { \
+ 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \
+ } \
+ }
+static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID);
+
+NS_IMPL_ADDREF_INHERITED(nsImapIncomingServer, nsMsgIncomingServer)
+NS_IMPL_RELEASE_INHERITED(nsImapIncomingServer, nsMsgIncomingServer)
+
+NS_INTERFACE_MAP_BEGIN(nsImapIncomingServer)
+ NS_INTERFACE_MAP_ENTRY(nsIImapServerSink)
+ NS_INTERFACE_MAP_ENTRY(nsIImapIncomingServer)
+ NS_INTERFACE_MAP_ENTRY(nsISubscribableServer)
+ NS_INTERFACE_MAP_ENTRY(nsIUrlListener)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgIncomingServer)
+
+nsImapIncomingServer::nsImapIncomingServer()
+ : mLock("nsImapIncomingServer.mLock") {
+ m_capability = kCapabilityUndefined;
+ mDoingSubscribeDialog = false;
+ mDoingLsub = false;
+ m_canHaveFilters = true;
+ m_userAuthenticated = false;
+ m_shuttingDown = false;
+ mUtf8AcceptEnabled = false;
+}
+
+nsImapIncomingServer::~nsImapIncomingServer() {
+ mozilla::DebugOnly<nsresult> rv = ClearInner();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "ClearInner failed");
+ CloseCachedConnections();
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetKey(
+ const nsACString& aKey) // override nsMsgIncomingServer's implementation...
+{
+ nsMsgIncomingServer::SetKey(aKey);
+
+ // okay now that the key has been set, we need to add ourselves to the
+ // host session list...
+
+ // every time we create an imap incoming server, we need to add it to the
+ // host session list!!
+
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString key(aKey);
+ hostSession->AddHostToList(key.get(), this);
+ nsMsgImapDeleteModel deleteModel =
+ nsMsgImapDeleteModels::MoveToTrash; // default to trash
+ GetDeleteModel(&deleteModel);
+ hostSession->SetDeleteIsMoveToTrashForHost(
+ key.get(), deleteModel == nsMsgImapDeleteModels::MoveToTrash);
+ hostSession->SetShowDeletedMessagesForHost(
+ key.get(), deleteModel == nsMsgImapDeleteModels::IMAPDelete);
+
+ nsAutoCString onlineDir;
+ rv = GetServerDirectory(onlineDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!onlineDir.IsEmpty())
+ hostSession->SetOnlineDirForHost(key.get(), onlineDir.get());
+
+ nsCString personalNamespace;
+ nsCString publicNamespace;
+ nsCString otherUsersNamespace;
+
+ rv = GetPersonalNamespace(personalNamespace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetPublicNamespace(publicNamespace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetOtherUsersNamespace(otherUsersNamespace);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (personalNamespace.IsEmpty() && publicNamespace.IsEmpty() &&
+ otherUsersNamespace.IsEmpty())
+ personalNamespace.AssignLiteral("\"\"");
+
+ hostSession->SetNamespaceFromPrefForHost(key.get(), personalNamespace.get(),
+ kPersonalNamespace);
+
+ if (!publicNamespace.IsEmpty())
+ hostSession->SetNamespaceFromPrefForHost(key.get(), publicNamespace.get(),
+ kPublicNamespace);
+
+ if (!otherUsersNamespace.IsEmpty())
+ hostSession->SetNamespaceFromPrefForHost(
+ key.get(), otherUsersNamespace.get(), kOtherUsersNamespace);
+ return rv;
+}
+
+// construct the pretty name to show to the user if they haven't
+// specified one. This should be overridden for news and mail.
+NS_IMETHODIMP
+nsImapIncomingServer::GetConstructedPrettyName(nsAString& retval) {
+ nsAutoCString username;
+ nsAutoCString hostName;
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv =
+ accountManager->GetFirstIdentityForServer(this, getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString emailAddress;
+
+ if (NS_SUCCEEDED(rv) && identity) {
+ nsCString identityEmailAddress;
+ identity->GetEmail(identityEmailAddress);
+ CopyASCIItoUTF16(identityEmailAddress, emailAddress);
+ } else {
+ rv = GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!username.IsEmpty() && !hostName.IsEmpty()) {
+ CopyASCIItoUTF16(username, emailAddress);
+ emailAddress.Append('@');
+ emailAddress.Append(NS_ConvertASCIItoUTF16(hostName));
+ }
+ }
+
+ return GetFormattedStringFromName(emailAddress, "imapDefaultAccountName",
+ retval);
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetLocalStoreType(nsACString& type) {
+ type.AssignLiteral("imap");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetLocalDatabaseType(nsACString& type) {
+ type.AssignLiteral("imap");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetServerDirectory(nsACString& serverDirectory) {
+ return GetCharValue("server_sub_directory", serverDirectory);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetServerDirectory(const nsACString& serverDirectory) {
+ nsCString serverKey;
+ nsresult rv = GetKey(serverKey);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_SUCCEEDED(rv))
+ hostSession->SetOnlineDirForHost(
+ serverKey.get(), PromiseFlatCString(serverDirectory).get());
+ }
+ return SetCharValue("server_sub_directory", serverDirectory);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetOverrideNamespaces(bool* bVal) {
+ return GetBoolValue("override_namespaces", bVal);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetOverrideNamespaces(bool bVal) {
+ nsCString serverKey;
+ GetKey(serverKey);
+ if (!serverKey.IsEmpty()) {
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_SUCCEEDED(rv))
+ hostSession->SetNamespacesOverridableForHost(serverKey.get(), bVal);
+ }
+ return SetBoolValue("override_namespaces", bVal);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetUsingSubscription(bool* bVal) {
+ return GetBoolValue("using_subscription", bVal);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetUsingSubscription(bool bVal) {
+ nsCString serverKey;
+ GetKey(serverKey);
+ if (!serverKey.IsEmpty()) {
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_SUCCEEDED(rv))
+ hostSession->SetHostIsUsingSubscription(serverKey.get(), bVal);
+ }
+ return SetBoolValue("using_subscription", bVal);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetMaximumConnectionsNumber(int32_t* aMaxConnections) {
+ NS_ENSURE_ARG_POINTER(aMaxConnections);
+
+ nsresult rv = GetIntValue("max_cached_connections", aMaxConnections);
+ // Get our maximum connection count. We need at least 1. If the value is 0,
+ // we use the default of 5. If it's negative, we treat that as 1.
+ if (NS_SUCCEEDED(rv) && *aMaxConnections > 0) return NS_OK;
+
+ *aMaxConnections = (NS_FAILED(rv) || (*aMaxConnections == 0)) ? 5 : 1;
+ (void)SetMaximumConnectionsNumber(*aMaxConnections);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetMaximumConnectionsNumber(int32_t aMaxConnections) {
+ return SetIntValue("max_cached_connections", aMaxConnections);
+}
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, ForceSelect, "force_select_imap")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, DualUseFolders,
+ "dual_use_folders")
+
+NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, AdminUrl, "admin_url")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, CleanupInboxOnExit,
+ "cleanup_inbox_on_exit")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, OfflineDownload,
+ "offline_download")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, DownloadBodiesOnGetNewMail,
+ "download_bodies_on_get_new_mail")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, AutoSyncOfflineStores,
+ "autosync_offline_stores")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseIdle, "use_idle")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, CheckAllFoldersForNew,
+ "check_all_folders_for_new")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseCondStore, "use_condstore")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, IsGMailServer, "is_gmail")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseCompressDeflate,
+ "use_compress_deflate")
+
+NS_IMPL_SERVERPREF_INT(nsImapIncomingServer, AutoSyncMaxAgeDays,
+ "autosync_max_age_days")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, AllowUTF8Accept,
+ "allow_utf8_accept")
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetShuttingDown(bool* retval) {
+ NS_ENSURE_ARG_POINTER(retval);
+ *retval = m_shuttingDown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetShuttingDown(bool val) {
+ m_shuttingDown = val;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetDeleteModel(int32_t* retval) {
+ NS_ENSURE_ARG(retval);
+ return GetIntValue("delete_model", retval);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetDeleteModel(int32_t ivalue) {
+ nsresult rv = SetIntValue("delete_model", ivalue);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hostSession->SetDeleteIsMoveToTrashForHost(
+ m_serverKey.get(), ivalue == nsMsgImapDeleteModels::MoveToTrash);
+ hostSession->SetShowDeletedMessagesForHost(
+ m_serverKey.get(), ivalue == nsMsgImapDeleteModels::IMAPDelete);
+
+ // Despite its name, this returns the trash folder path, for example
+ // INBOX/Trash.
+ nsAutoString trashFolderName;
+ nsresult rv = GetTrashFolderName(trashFolderName);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString trashFolderNameUtf7or8;
+ bool useUTF8 = false;
+ GetUtf8AcceptEnabled(&useUTF8);
+ if (useUTF8) {
+ CopyUTF16toUTF8(trashFolderName, trashFolderNameUtf7or8);
+ } else {
+ CopyUTF16toMUTF7(trashFolderName, trashFolderNameUtf7or8);
+ }
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ // 'trashFolderName' being a path here works well since this is appended
+ // to the server's root folder in GetFolder().
+ rv = GetFolder(trashFolderNameUtf7or8, getter_AddRefs(trashFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString trashURI;
+ trashFolder->GetURI(trashURI);
+ rv = GetMsgFolderFromURI(trashFolder, trashURI,
+ getter_AddRefs(trashFolder));
+ if (NS_SUCCEEDED(rv) && trashFolder) {
+ // If the trash folder is used, set the flag, otherwise clear it.
+ if (ivalue == nsMsgImapDeleteModels::MoveToTrash)
+ trashFolder->SetFlag(nsMsgFolderFlags::Trash);
+ else
+ trashFolder->ClearFlag(nsMsgFolderFlags::Trash);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMPL_SERVERPREF_INT(nsImapIncomingServer, TimeOutLimits, "timeout")
+
+NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, ServerIDPref, "serverIDResponse")
+
+NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, PersonalNamespace,
+ "namespace.personal")
+
+NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, PublicNamespace,
+ "namespace.public")
+
+NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, OtherUsersNamespace,
+ "namespace.other_users")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, FetchByChunks, "fetch_by_chunks")
+
+NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, SendID, "send_client_info")
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetIsAOLServer(bool* aBool) {
+ NS_ENSURE_ARG_POINTER(aBool);
+ *aBool = ((m_capability & kAOLImapCapability) != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetIsAOLServer(bool aBool) {
+ if (aBool)
+ m_capability |= kAOLImapCapability;
+ else
+ m_capability &= ~kAOLImapCapability;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::UpdateTrySTARTTLSPref(bool aStartTLSSucceeded) {
+ SetSocketType(aStartTLSSucceeded ? nsMsgSocketType::alwaysSTARTTLS
+ : nsMsgSocketType::plain);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl,
+ nsISupports* aConsumer) {
+ nsCOMPtr<nsIImapProtocol> aProtocol;
+
+ nsresult rv = GetImapConnection(aImapUrl, getter_AddRefs(aProtocol));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl, &rv);
+ if (aProtocol) {
+ rv = aProtocol->LoadImapUrl(mailnewsurl, aConsumer);
+ // *** jt - in case of the time out situation or the connection gets
+ // terminated by some unforeseen problems let's give it a second chance
+ // to run the url
+ if (NS_FAILED(rv) && rv != NS_ERROR_ILLEGAL_VALUE) {
+ rv = aProtocol->LoadImapUrl(mailnewsurl, aConsumer);
+ }
+ } else { // unable to get an imap connection to run the url; add to the url
+ // queue
+ nsImapProtocol::LogImapUrl("queuing url", aImapUrl);
+ PR_CEnterMonitor(this);
+ m_urlQueue.AppendObject(aImapUrl);
+ m_urlConsumers.AppendElement(aConsumer);
+ NS_IF_ADDREF(aConsumer);
+ PR_CExitMonitor(this);
+ // let's try running it now - maybe the connection is free now.
+ bool urlRun;
+ rv = LoadNextQueuedUrl(nullptr, &urlRun);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::PrepareToRetryUrl(nsIImapUrl* aImapUrl,
+ nsIImapMockChannel** aChannel) {
+ NS_ENSURE_ARG_POINTER(aChannel);
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+ // maybe there's more we could do here, but this is all we need now.
+ return aImapUrl->GetMockChannel(aChannel);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SuspendUrl(nsIImapUrl* aImapUrl) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+ nsImapProtocol::LogImapUrl("suspending url", aImapUrl);
+ PR_CEnterMonitor(this);
+ m_urlQueue.AppendObject(aImapUrl);
+ m_urlConsumers.AppendElement(nullptr);
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::RetryUrl(nsIImapUrl* aImapUrl,
+ nsIImapMockChannel* aChannel) {
+ nsresult rv;
+ // Get current thread envent queue
+ aImapUrl->SetMockChannel(aChannel);
+ nsCOMPtr<nsIImapProtocol> protocolInstance;
+ nsImapProtocol::LogImapUrl("creating protocol instance to retry queued url",
+ aImapUrl);
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+ rv = GetImapConnection(aImapUrl, getter_AddRefs(protocolInstance));
+ if (NS_SUCCEEDED(rv) && protocolInstance) {
+ nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl, &rv);
+ if (NS_SUCCEEDED(rv) && url) {
+ nsImapProtocol::LogImapUrl("retrying url", aImapUrl);
+ rv = protocolInstance->LoadImapUrl(
+ url, nullptr); // ### need to save the display consumer.
+ }
+ }
+ return rv;
+}
+
+// checks to see if there are any queued urls on this incoming server,
+// and if so, tries to run the oldest one. Returns true if the url is run
+// on the passed in protocol connection.
+NS_IMETHODIMP
+nsImapIncomingServer::LoadNextQueuedUrl(nsIImapProtocol* aProtocol,
+ bool* aResult) {
+ if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE;
+
+ nsresult rv = NS_OK;
+ bool urlRun = false;
+ bool keepGoing = true;
+ nsCOMPtr<nsIImapProtocol> protocolInstance;
+
+ MutexAutoLock mon(mLock);
+ int32_t cnt = m_urlQueue.Count();
+
+ while (cnt > 0 && !urlRun && keepGoing) {
+ nsCOMPtr<nsIImapUrl> aImapUrl(m_urlQueue[0]);
+
+ bool removeUrlFromQueue = false;
+ if (aImapUrl) {
+ nsImapProtocol::LogImapUrl("considering playing queued url", aImapUrl);
+ rv = DoomUrlIfChannelHasError(aImapUrl, &removeUrlFromQueue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // if we didn't doom the url, lets run it.
+ if (!removeUrlFromQueue) {
+ nsISupports* aConsumer = m_urlConsumers.ElementAt(0);
+ NS_IF_ADDREF(aConsumer);
+
+ nsImapProtocol::LogImapUrl(
+ "creating protocol instance to play queued url", aImapUrl);
+ rv = GetImapConnection(aImapUrl, getter_AddRefs(protocolInstance));
+ if (NS_SUCCEEDED(rv) && protocolInstance) {
+ nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl, &rv);
+ if (NS_SUCCEEDED(rv) && url) {
+ nsImapProtocol::LogImapUrl("playing queued url", aImapUrl);
+ rv = protocolInstance->LoadImapUrl(url, aConsumer);
+ if (NS_SUCCEEDED(rv)) {
+ bool isInbox;
+ protocolInstance->IsBusy(&urlRun, &isInbox);
+ if (!urlRun)
+ nsImapProtocol::LogImapUrl("didn't need to run", aImapUrl);
+ removeUrlFromQueue = true;
+ } else {
+ nsImapProtocol::LogImapUrl("playing queued url failed", aImapUrl);
+ }
+ }
+ } else {
+ nsImapProtocol::LogImapUrl(
+ "failed creating protocol instance to play queued url", aImapUrl);
+ keepGoing = false;
+ }
+ NS_IF_RELEASE(aConsumer);
+ }
+ if (removeUrlFromQueue) {
+ m_urlQueue.RemoveObjectAt(0);
+ m_urlConsumers.RemoveElementAt(0);
+ }
+ }
+ cnt = m_urlQueue.Count();
+ }
+ if (aResult) *aResult = urlRun && aProtocol && aProtocol == protocolInstance;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::AbortQueuedUrls() {
+ nsresult rv = NS_OK;
+
+ MutexAutoLock mon(mLock);
+ int32_t cnt = m_urlQueue.Count();
+
+ while (cnt > 0) {
+ nsCOMPtr<nsIImapUrl> aImapUrl(m_urlQueue[cnt - 1]);
+ bool removeUrlFromQueue = false;
+
+ if (aImapUrl) {
+ rv = DoomUrlIfChannelHasError(aImapUrl, &removeUrlFromQueue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (removeUrlFromQueue) {
+ m_urlQueue.RemoveObjectAt(cnt - 1);
+ m_urlConsumers.RemoveElementAt(cnt - 1);
+ }
+ }
+ cnt--;
+ }
+
+ return rv;
+}
+
+// if this url has a channel with an error, doom it and its mem cache entries,
+// and notify url listeners.
+nsresult nsImapIncomingServer::DoomUrlIfChannelHasError(nsIImapUrl* aImapUrl,
+ bool* urlDoomed) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> aMailNewsUrl(do_QueryInterface(aImapUrl, &rv));
+
+ if (aMailNewsUrl && aImapUrl) {
+ nsCOMPtr<nsIImapMockChannel> mockChannel;
+
+ if (NS_SUCCEEDED(aImapUrl->GetMockChannel(getter_AddRefs(mockChannel))) &&
+ mockChannel) {
+ nsresult requestStatus;
+ mockChannel->GetStatus(&requestStatus);
+ if (NS_FAILED(requestStatus)) {
+ nsresult res;
+ *urlDoomed = true;
+ nsImapProtocol::LogImapUrl("dooming url", aImapUrl);
+
+ mockChannel
+ ->Close(); // try closing it to get channel listener nulled out.
+
+ if (aMailNewsUrl) {
+ nsCOMPtr<nsICacheEntry> cacheEntry;
+ res = aMailNewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry));
+ if (NS_SUCCEEDED(res) && cacheEntry) cacheEntry->AsyncDoom(nullptr);
+ // we're aborting this url - tell listeners
+ aMailNewsUrl->SetUrlState(false, NS_MSG_ERROR_URL_ABORTED);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::RemoveConnection(nsIImapProtocol* aImapConnection) {
+ PR_CEnterMonitor(this);
+ if (aImapConnection) m_connectionCache.RemoveObject(aImapConnection);
+
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+bool nsImapIncomingServer::ConnectionTimeOut(nsIImapProtocol* aConnection) {
+ bool retVal = false;
+ if (!aConnection) return retVal;
+ nsresult rv;
+
+ int32_t timeoutInMinutes = 0;
+ rv = GetTimeOutLimits(&timeoutInMinutes);
+ if (NS_FAILED(rv) || timeoutInMinutes <= 0 || timeoutInMinutes > 29) {
+ timeoutInMinutes = 29;
+ SetTimeOutLimits(timeoutInMinutes);
+ }
+
+ PRTime cacheTimeoutLimits = timeoutInMinutes * 60 * PR_USEC_PER_SEC;
+ PRTime lastActiveTimeStamp;
+ rv = aConnection->GetLastActiveTimeStamp(&lastActiveTimeStamp);
+
+ if (PR_Now() - lastActiveTimeStamp >= cacheTimeoutLimits) {
+ RemoveConnection(aConnection);
+ aConnection->TellThreadToDie(false);
+ retVal = true;
+ }
+ return retVal;
+}
+
+nsresult nsImapIncomingServer::GetImapConnection(
+ nsIImapUrl* aImapUrl, nsIImapProtocol** aImapConnection) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+
+ nsresult rv = NS_OK;
+ bool canRunUrlImmediately = false;
+ bool canRunButBusy = false;
+ nsCOMPtr<nsIImapProtocol> connection;
+ nsCOMPtr<nsIImapProtocol> freeConnection;
+ bool isBusy = false;
+ bool isInboxConnection = false;
+
+ PR_CEnterMonitor(this);
+
+ int32_t maxConnections;
+ (void)GetMaximumConnectionsNumber(&maxConnections);
+
+ int32_t cnt = m_connectionCache.Count();
+ *aImapConnection = nullptr;
+
+ // iterate through the connection cache for a connection that can handle this
+ // url.
+ // loop until we find a connection that can run the url, or doesn't have to
+ // wait?
+ for (int32_t i = cnt - 1; i >= 0 && !canRunUrlImmediately && !canRunButBusy;
+ i--) {
+ connection = m_connectionCache[i];
+ if (connection) {
+ bool badConnection = ConnectionTimeOut(connection);
+ if (!badConnection) {
+ badConnection = NS_FAILED(connection->CanHandleUrl(
+ aImapUrl, &canRunUrlImmediately, &canRunButBusy));
+#ifdef DEBUG_bienvenu
+ nsAutoCString curSelectedFolderName;
+ if (connection)
+ connection->GetSelectedMailboxName(
+ getter_Copies(curSelectedFolderName));
+ // check that no other connection is in the same selected state.
+ if (!curSelectedFolderName.IsEmpty()) {
+ for (uint32_t j = 0; j < cnt; j++) {
+ if (j != i) {
+ nsCOMPtr<nsIImapProtocol> otherConnection =
+ do_QueryElementAt(m_connectionCache, j);
+ if (otherConnection) {
+ nsAutoCString otherSelectedFolderName;
+ otherConnection->GetSelectedMailboxName(
+ getter_Copies(otherSelectedFolderName));
+ NS_ASSERTION(
+ !curSelectedFolderName.Equals(otherSelectedFolderName),
+ "two connections selected on same folder");
+ }
+ }
+ }
+ }
+#endif // DEBUG_bienvenu
+ }
+ if (badConnection) {
+ connection = nullptr;
+ continue;
+ }
+ }
+
+ // if this connection is wrong, but it's not busy, check if we should
+ // designate it as the free connection.
+ if (!canRunUrlImmediately && !canRunButBusy && connection) {
+ rv = connection->IsBusy(&isBusy, &isInboxConnection);
+ if (NS_FAILED(rv)) continue;
+ // if max connections is <= 1, we have to re-use the inbox connection.
+ if (!isBusy && (!isInboxConnection || maxConnections <= 1)) {
+ if (!freeConnection)
+ freeConnection = connection;
+ else // check which is the better free connection to use.
+ { // We prefer one not in the selected state.
+ nsAutoCString selectedFolderName;
+ connection->GetSelectedMailboxName(getter_Copies(selectedFolderName));
+ if (selectedFolderName.IsEmpty()) freeConnection = connection;
+ }
+ }
+ }
+ // don't leave this loop with connection set if we can't use it!
+ if (!canRunButBusy && !canRunUrlImmediately) connection = nullptr;
+ }
+
+ nsImapState requiredState;
+ aImapUrl->GetRequiredImapState(&requiredState);
+ // refresh cnt in case we killed one or more dead connections. This
+ // will prevent us from not spinning up a new connection when all
+ // connections were dead.
+ cnt = m_connectionCache.Count();
+ // if we got here and we have a connection, then we should return it!
+ if (canRunUrlImmediately && connection) {
+ connection.forget(aImapConnection);
+ } else if (canRunButBusy) {
+ // do nothing; return NS_OK; for queuing
+ }
+ // CanHandleUrl will pretend that some types of urls require a selected state
+ // url (e.g., a folder delete or msg append) but we shouldn't create new
+ // connections for these types of urls if we have a free connection. So we
+ // check the actual required state here.
+ else if (cnt < maxConnections &&
+ (!freeConnection ||
+ requiredState == nsIImapUrl::nsImapSelectedState)) {
+ rv = CreateProtocolInstance(aImapConnection);
+ } else if (freeConnection) {
+ freeConnection.forget(aImapConnection);
+ } else {
+ if (cnt >= maxConnections)
+ nsImapProtocol::LogImapUrl("exceeded connection cache limit", aImapUrl);
+ // caller will queue the url
+ }
+
+ PR_CExitMonitor(this);
+ return rv;
+}
+
+nsresult nsImapIncomingServer::CreateProtocolInstance(
+ nsIImapProtocol** aImapConnection) {
+ // create a new connection and add it to the connection cache
+ // we may need to flag the protocol connection as busy so we don't get
+ // a race condition where someone else goes through this code
+
+ int32_t authMethod;
+ GetAuthMethod(&authMethod);
+ nsresult rv;
+ // pre-flight that we have nss - on the ui thread - for MD5 etc.
+ switch (authMethod) {
+ case nsMsgAuthMethod::passwordEncrypted:
+ case nsMsgAuthMethod::secure:
+ case nsMsgAuthMethod::anything: {
+ nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } break;
+ default:
+ break;
+ }
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<nsImapProtocol> protocolInstance(new nsImapProtocol());
+ rv = protocolInstance->Initialize(hostSession, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // It implements nsIChannel, and all channels require loadInfo.
+ protocolInstance->SetLoadInfo(new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER));
+
+ // take the protocol instance and add it to the connectionCache
+ m_connectionCache.AppendObject(protocolInstance);
+ protocolInstance.forget(aImapConnection);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::CloseConnectionForFolder(
+ nsIMsgFolder* aMsgFolder) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapProtocol> connection;
+ bool isBusy = false, isInbox = false;
+ nsCString inFolderName;
+ nsCString connectionFolderName;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aMsgFolder);
+
+ if (!imapFolder) return NS_ERROR_NULL_POINTER;
+
+ int32_t cnt = m_connectionCache.Count();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ imapFolder->GetOnlineName(inFolderName);
+ PR_CEnterMonitor(this);
+
+ for (int32_t i = 0; i < cnt; ++i) {
+ connection = m_connectionCache[i];
+ if (connection) {
+ rv = connection->GetSelectedMailboxName(
+ getter_Copies(connectionFolderName));
+ if (connectionFolderName.Equals(inFolderName)) {
+ rv = connection->IsBusy(&isBusy, &isInbox);
+ if (!isBusy) rv = connection->TellThreadToDie(true);
+ break; // found it, end of the loop
+ }
+ }
+ }
+
+ PR_CExitMonitor(this);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::ResetConnection(
+ const nsACString& folderName) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapProtocol> connection;
+ bool isBusy = false, isInbox = false;
+ nsCString curFolderName;
+
+ int32_t cnt = m_connectionCache.Count();
+
+ PR_CEnterMonitor(this);
+
+ for (int32_t i = 0; i < cnt; ++i) {
+ connection = m_connectionCache[i];
+ if (connection) {
+ rv = connection->GetSelectedMailboxName(getter_Copies(curFolderName));
+ if (curFolderName.Equals(folderName)) {
+ rv = connection->IsBusy(&isBusy, &isInbox);
+ if (!isBusy) rv = connection->ResetToAuthenticatedState();
+ break; // found it, end of the loop
+ }
+ }
+ }
+
+ PR_CExitMonitor(this);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::PerformExpand(nsIMsgWindow* aMsgWindow) {
+ nsString password;
+ nsresult rv;
+ rv = GetPassword(password);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (password.IsEmpty()) {
+ // Check if this is due to oauth2 showing empty password. If so, keep going.
+ int32_t authMethod = 0;
+ GetAuthMethod(&authMethod);
+ if (authMethod != nsMsgAuthMethod::OAuth2) return NS_OK;
+ }
+
+ rv = ResetFoldersToUnverified(nullptr);
+
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = GetRootFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!rootMsgFolder) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+ rv = imapService->DiscoverAllFolders(rootMsgFolder, this, aMsgWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapHostSessionList> hostSessionList =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString serverKey;
+ rv = GetKey(serverKey);
+ if (!serverKey.IsEmpty())
+ hostSessionList->SetDiscoveryForHostInProgress(serverKey.get(), true);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::VerifyLogon(nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ nsresult rv;
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ // this will create the resource if it doesn't exist, but it shouldn't
+ // do anything on disk.
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->VerifyLogon(rootFolder, aUrlListener, aMsgWindow, aURL);
+}
+
+NS_IMETHODIMP nsImapIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) {
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ if (NS_SUCCEEDED(rv)) {
+ SetPerformingBiff(true);
+ rv = rootMsgFolder->GetNewMessages(aMsgWindow, nullptr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::CloseCachedConnections() {
+ nsCOMPtr<nsIImapProtocol> connection;
+ PR_CEnterMonitor(this);
+
+ // iterate through the connection cache closing open connections.
+ int32_t cnt = m_connectionCache.Count();
+
+ for (int32_t i = cnt; i > 0; --i) {
+ connection = m_connectionCache[i - 1];
+ if (connection) connection->TellThreadToDie(true);
+ }
+
+ PR_CExitMonitor(this);
+ return NS_OK;
+}
+
+nsresult nsImapIncomingServer::CreateRootFolderFromUri(
+ const nsACString& serverUri, nsIMsgFolder** rootFolder) {
+ nsImapMailFolder* newRootFolder = new nsImapMailFolder;
+ newRootFolder->Init(serverUri);
+ NS_ADDREF(*rootFolder = newRootFolder);
+ return NS_OK;
+}
+
+// nsIImapServerSink impl
+// aNewFolder will not be set if we're listing for the subscribe UI, since
+// that's the way 4.x worked.
+NS_IMETHODIMP nsImapIncomingServer::PossibleImapMailbox(
+ const nsACString& folderPath, char hierarchyDelimiter, int32_t boxFlags,
+ bool* aNewFolder) {
+ NS_ENSURE_ARG_POINTER(aNewFolder);
+ NS_ENSURE_TRUE(!folderPath.IsEmpty(), NS_ERROR_FAILURE);
+
+ // folderPath is in canonical format, i.e., hierarchy separator has been
+ // replaced with '/'
+ nsresult rv;
+ bool found = false;
+ bool haveParent = false;
+ nsCOMPtr<nsIMsgImapMailFolder> hostFolder;
+ nsCOMPtr<nsIMsgFolder> aFolder;
+ bool explicitlyVerify = false;
+
+ *aNewFolder = false;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString dupFolderPath(folderPath);
+ if (dupFolderPath.Last() == '/') {
+ dupFolderPath.SetLength(dupFolderPath.Length() - 1);
+ if (dupFolderPath.IsEmpty()) return NS_ERROR_FAILURE;
+ // *** this is what we did in 4.x in order to list uw folder only
+ // mailbox in order to get the \NoSelect flag
+ explicitlyVerify = !(boxFlags & kNameSpace);
+ }
+ if (mDoingSubscribeDialog) {
+ // Make sure the imapmailfolder object has the right delimiter because the
+ // unsubscribed folders (those not in the 'lsub' list) have the delimiter
+ // set to the default ('^').
+ if (rootFolder && !dupFolderPath.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ bool isNamespace = false;
+ bool noSelect = false;
+
+ rv = rootFolder->FindSubFolder(dupFolderPath, getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_subscribeFolders.AppendObject(msgFolder);
+ noSelect = (boxFlags & kNoselect) != 0;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(msgFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ isNamespace = (boxFlags & kNameSpace) != 0;
+ if (!isNamespace)
+ rv = AddTo(dupFolderPath,
+ mDoingLsub && !noSelect /* add as subscribed */, !noSelect,
+ mDoingLsub /* change if exists */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+ }
+ }
+
+ hostFolder = do_QueryInterface(rootFolder, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString tempFolderName(dupFolderPath);
+ nsAutoCString tokenStr, remStr, changedStr;
+ int32_t slashPos = tempFolderName.FindChar('/');
+ if (slashPos > 0) {
+ tokenStr = StringHead(tempFolderName, slashPos);
+ remStr = Substring(tempFolderName, slashPos);
+ } else
+ tokenStr.Assign(tempFolderName);
+
+ if ((int32_t(PL_strcasecmp(tokenStr.get(), "INBOX")) == 0) &&
+ (strcmp(tokenStr.get(), "INBOX") != 0))
+ changedStr.AppendLiteral("INBOX");
+ else
+ changedStr.Append(tokenStr);
+
+ if (slashPos > 0) changedStr.Append(remStr);
+
+ dupFolderPath.Assign(changedStr);
+ nsAutoCString folderName(dupFolderPath);
+
+ nsAutoCString uri;
+ nsCString serverUri;
+ GetServerURI(serverUri);
+ uri.Assign(serverUri);
+ int32_t leafPos = folderName.RFindChar('/');
+ nsAutoCString parentName(folderName);
+ nsAutoCString parentUri(uri);
+
+ if (leafPos > 0) {
+ // If there is a hierarchy, there is a parent.
+ // Don't strip off slash if it's the first character
+ parentName.SetLength(leafPos);
+ folderName.Cut(0, leafPos + 1); // get rid of the parent name
+ haveParent = true;
+ parentUri.Append('/');
+ parentUri.Append(parentName);
+ }
+ if (folderPath.LowerCaseEqualsLiteral("inbox") &&
+ hierarchyDelimiter == kOnlineHierarchySeparatorNil) {
+ hierarchyDelimiter = '/'; // set to default in this case (as in 4.x)
+ hostFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ }
+
+ nsCOMPtr<nsIMsgFolder> child;
+
+ // nsCString possibleName(aSpec->allocatedPathName);
+ uri.Append('/');
+ uri.Append(dupFolderPath);
+ bool caseInsensitive = dupFolderPath.LowerCaseEqualsLiteral("inbox");
+ rootFolder->GetChildWithURI(uri, true, caseInsensitive,
+ getter_AddRefs(child));
+ // if we couldn't find this folder by URI, tell the imap code it's a new
+ // folder to us
+ *aNewFolder = !child;
+ if (child) found = true;
+ if (!found) {
+ // trying to find/discover the parent
+ if (haveParent) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ bool parentIsNew;
+ caseInsensitive = parentName.LowerCaseEqualsLiteral("inbox");
+ rootFolder->GetChildWithURI(parentUri, true, caseInsensitive,
+ getter_AddRefs(parent));
+ if (!parent /* || parentFolder->GetFolderNeedsAdded()*/) {
+ PossibleImapMailbox(
+ parentName, hierarchyDelimiter,
+ kNoselect | // be defensive
+ ((boxFlags & // only inherit certain flags from the child
+ (kPublicMailbox | kOtherUsersMailbox | kPersonalMailbox))),
+ &parentIsNew);
+ }
+ }
+ rv = hostFolder->CreateClientSubfolderInfo(
+ dupFolderPath, hierarchyDelimiter, boxFlags, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ caseInsensitive = dupFolderPath.LowerCaseEqualsLiteral("inbox");
+ rootFolder->GetChildWithURI(uri, true, caseInsensitive,
+ getter_AddRefs(child));
+ }
+ if (child) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(child);
+ if (imapFolder) {
+ nsAutoCString onlineName;
+ nsAutoString unicodeName;
+ imapFolder->SetVerifiedAsOnlineFolder(true);
+ imapFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ if (boxFlags & kImapTrash) {
+ int32_t deleteModel;
+ GetDeleteModel(&deleteModel);
+ if (deleteModel == nsMsgImapDeleteModels::MoveToTrash)
+ child->SetFlag(nsMsgFolderFlags::Trash);
+ }
+
+ imapFolder->SetBoxFlags(boxFlags);
+ imapFolder->SetExplicitlyVerify(explicitlyVerify);
+ imapFolder->GetOnlineName(onlineName);
+
+ // online name needs to use the correct hierarchy delimiter (I think...)
+ // or the canonical path - one or the other, but be consistent.
+ dupFolderPath.ReplaceChar('/', hierarchyDelimiter);
+ if (hierarchyDelimiter != '/') {
+ nsImapUrl::UnescapeSlashes(dupFolderPath);
+ }
+
+ // GMail gives us a localized name for the inbox but doesn't let
+ // us select that localized name.
+ if (boxFlags & kImapInbox)
+ imapFolder->SetOnlineName("INBOX"_ns);
+ else if (onlineName.IsEmpty() || !onlineName.Equals(dupFolderPath))
+ imapFolder->SetOnlineName(dupFolderPath);
+
+ if (hierarchyDelimiter != '/') {
+ nsImapUrl::UnescapeSlashes(folderName);
+ }
+ if (NS_SUCCEEDED(CopyFolderNameToUTF16(folderName, unicodeName)))
+ child->SetPrettyName(unicodeName);
+ }
+ }
+ if (!found && child)
+ child->SetMsgDatabase(nullptr); // close the db, so we don't hold open all
+ // the .msf files for new folders
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::AddFolderRights(
+ const nsACString& mailboxName, const nsACString& userName,
+ const nsACString& rights) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ if (imapRoot) {
+ nsCOMPtr<nsIMsgImapMailFolder> foundFolder;
+ rv = imapRoot->FindOnlineSubFolder(mailboxName,
+ getter_AddRefs(foundFolder));
+ if (NS_SUCCEEDED(rv) && foundFolder)
+ return foundFolder->AddFolderRights(userName, rights);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::FolderNeedsACLInitialized(
+ const nsACString& folderPath, bool* aNeedsACLInitialized) {
+ NS_ENSURE_ARG_POINTER(aNeedsACLInitialized);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ if (imapRoot) {
+ nsCOMPtr<nsIMsgImapMailFolder> foundFolder;
+ rv = imapRoot->FindOnlineSubFolder(folderPath,
+ getter_AddRefs(foundFolder));
+ if (NS_SUCCEEDED(rv) && foundFolder) {
+ nsCOMPtr<nsIImapMailFolderSink> folderSink =
+ do_QueryInterface(foundFolder);
+ if (folderSink)
+ return folderSink->GetFolderNeedsACLListed(aNeedsACLInitialized);
+ }
+ }
+ }
+ *aNeedsACLInitialized = false; // maybe we want to say TRUE here...
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::RefreshFolderRights(
+ const nsACString& folderPath) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ if (imapRoot) {
+ nsCOMPtr<nsIMsgImapMailFolder> foundFolder;
+ rv = imapRoot->FindOnlineSubFolder(folderPath,
+ getter_AddRefs(foundFolder));
+ if (NS_SUCCEEDED(rv) && foundFolder)
+ return foundFolder->RefreshFolderRights();
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapIncomingServer::GetFolder(const nsACString& name,
+ nsIMsgFolder** pFolder) {
+ NS_ENSURE_ARG_POINTER(pFolder);
+ NS_ENSURE_TRUE(!name.IsEmpty(), NS_ERROR_FAILURE);
+ nsresult rv;
+ *pFolder = nullptr;
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCString uri;
+ rv = rootFolder->GetURI(uri);
+ if (NS_SUCCEEDED(rv) && !uri.IsEmpty()) {
+ nsAutoCString uriString(uri);
+ uriString.Append('/');
+ uriString.Append(name);
+ rv = GetOrCreateFolder(uriString, pFolder);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::OnlineFolderDelete(
+ const nsACString& aFolderName) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::OnlineFolderCreateFailed(
+ const nsACString& aFolderName) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::OnlineFolderRename(
+ nsIMsgWindow* msgWindow, const nsACString& oldName,
+ const nsACString& newName) {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (!newName.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> me;
+ rv = GetFolder(oldName, getter_AddRefs(me));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgFolder> parent;
+ nsCString tmpNewName(newName);
+ int32_t folderStart = tmpNewName.RFindChar('/');
+ if (folderStart > 0) {
+ rv = GetFolder(StringHead(tmpNewName, folderStart),
+ getter_AddRefs(parent));
+ } else // root is the parent
+ rv = GetRootFolder(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ nsCOMPtr<nsIMsgImapMailFolder> folder;
+ folder = do_QueryInterface(me, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ folder->RenameLocal(tmpNewName, parent);
+ nsCOMPtr<nsIMsgImapMailFolder> parentImapFolder =
+ do_QueryInterface(parent);
+
+ if (parentImapFolder)
+ parentImapFolder->RenameClient(msgWindow, me, oldName, tmpNewName);
+
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ nsString unicodeNewName;
+ // `tmpNewName` is in MUTF-7 or UTF-8. It needs to be convert to UTF-8.
+ CopyFolderNameToUTF16(tmpNewName, unicodeNewName);
+ CopyUTF16toUTF8(unicodeNewName, tmpNewName);
+ rv = GetFolder(tmpNewName, getter_AddRefs(newFolder));
+ if (NS_SUCCEEDED(rv)) {
+ newFolder->NotifyFolderEvent(kRenameCompleted);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::FolderIsNoSelect(
+ const nsACString& aFolderName, bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nsresult rv = GetFolder(aFolderName, getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) {
+ uint32_t flags;
+ msgFolder->GetFlags(&flags);
+ *result = ((flags & nsMsgFolderFlags::ImapNoselect) != 0);
+ } else
+ *result = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetFolderAdminURL(
+ const nsACString& aFolderName, const nsACString& aFolderAdminUrl) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ if (imapRoot) {
+ nsCOMPtr<nsIMsgImapMailFolder> foundFolder;
+ rv = imapRoot->FindOnlineSubFolder(aFolderName,
+ getter_AddRefs(foundFolder));
+ if (NS_SUCCEEDED(rv) && foundFolder)
+ return foundFolder->SetAdminUrl(aFolderAdminUrl);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::FolderVerifiedOnline(
+ const nsACString& folderName, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = rootFolder->FindSubFolder(folderName, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ if (imapFolder) imapFolder->GetVerifiedAsOnlineFolder(aResult);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::DiscoveryDone() {
+ if (mDoingSubscribeDialog) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootMsgFolder));
+ if (NS_SUCCEEDED(rv) && rootMsgFolder) {
+ // GetResource() may return a node which is not in the folder
+ // tree hierarchy but in the rdf cache in case of the non-existing default
+ // Sent, Drafts, and Templates folders. The resource will be eventually
+ // released when the rdf service shuts down. When we create the default
+ // folders later on in the imap server, the subsequent GetResource() of the
+ // same uri will get us the cached rdf resource which should have the folder
+ // flag set appropriately.
+
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = accountMgr->GetFirstIdentityForServer(this, getter_AddRefs(identity));
+ if (NS_SUCCEEDED(rv) && identity) {
+ nsCString folderUri;
+ identity->GetFccFolder(folderUri);
+ nsCString existingUri;
+
+ if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::SentMail,
+ existingUri)) {
+ identity->SetFccFolder(existingUri);
+ identity->SetFccFolderPickerMode("1"_ns);
+ }
+ identity->GetDraftFolder(folderUri);
+ if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::Drafts,
+ existingUri)) {
+ identity->SetDraftFolder(existingUri);
+ identity->SetDraftsFolderPickerMode("1"_ns);
+ }
+ bool archiveEnabled;
+ identity->GetArchiveEnabled(&archiveEnabled);
+ if (archiveEnabled) {
+ identity->GetArchiveFolder(folderUri);
+ if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::Archive,
+ existingUri)) {
+ identity->SetArchiveFolder(existingUri);
+ identity->SetArchivesFolderPickerMode("1"_ns);
+ }
+ }
+ identity->GetStationeryFolder(folderUri);
+ if (!folderUri.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderUri, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) rv = folder->SetFlag(nsMsgFolderFlags::Templates);
+ }
+ }
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = GetSpamSettings(getter_AddRefs(spamSettings));
+ if (NS_SUCCEEDED(rv) && spamSettings) {
+ nsCString spamFolderUri, existingUri;
+ spamSettings->GetSpamFolderURI(spamFolderUri);
+ if (CheckSpecialFolder(spamFolderUri, nsMsgFolderFlags::Junk,
+ existingUri)) {
+ // This only sets the cached values in the spam settings object.
+ spamSettings->SetActionTargetFolder(existingUri);
+ spamSettings->SetMoveTargetMode(
+ nsISpamSettings::MOVE_TARGET_MODE_FOLDER);
+ // Set the preferences too so that the values persist.
+ SetUnicharValue("spamActionTargetFolder",
+ NS_ConvertUTF8toUTF16(existingUri));
+ SetIntValue("moveTargetMode", nsISpamSettings::MOVE_TARGET_MODE_FOLDER);
+ }
+ }
+
+ bool isGMailServer;
+ GetIsGMailServer(&isGMailServer);
+
+ // Verify there is only one trash folder. Another might be present if
+ // the trash name has been changed. Or we might be a gmail server and
+ // want to switch to gmail's trash folder.
+ nsTArray<RefPtr<nsIMsgFolder>> trashFolders;
+ rv = rootMsgFolder->GetFoldersWithFlags(nsMsgFolderFlags::Trash,
+ trashFolders);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString trashName;
+ if (NS_SUCCEEDED(GetTrashFolderName(trashName))) {
+ for (auto trashFolder : trashFolders) {
+ // If we're a gmail server, we clear the trash flags from folder(s)
+ // without the kImapXListTrash flag. For normal servers, we clear
+ // the trash folder flag if the folder name doesn't match the
+ // pref trash folder name.
+ nsAutoString retval;
+ rv = GetUnicharValue(PREF_TRASH_FOLDER_PATH, retval);
+ if (isGMailServer && (NS_FAILED(rv) || retval.IsEmpty())) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(
+ do_QueryInterface(trashFolder));
+ int32_t boxFlags;
+ imapFolder->GetBoxFlags(&boxFlags);
+ if (boxFlags & kImapXListTrash) {
+ continue;
+ }
+ } else {
+ // Store the trash folder path. We maintain the full path in the
+ // trash_folder_name preference since the full path is stored
+ // there when selecting a trash folder in the Account Manager.
+ nsAutoCString trashURL;
+ rv = trashFolder->GetFolderURL(trashURL);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), trashURL);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsAutoCString trashPath;
+ uri->GetPathQueryRef(trashPath);
+ nsAutoCString unescapedName;
+ MsgUnescapeString(Substring(trashPath, 1), // Skip leading slash.
+ nsINetUtil::ESCAPE_URL_PATH, unescapedName);
+ nsAutoString nameUnicode;
+ if (NS_FAILED(CopyFolderNameToUTF16(unescapedName, nameUnicode)) ||
+ trashName.Equals(nameUnicode)) {
+ continue;
+ }
+ if (trashFolders.Length() == 1) {
+ // We got here because the preferred trash folder does not
+ // exist, but a folder got discovered to be the trash folder.
+ SetUnicharValue(PREF_TRASH_FOLDER_PATH, nameUnicode);
+ continue;
+ }
+ }
+ // We clear the trash folder flag if the trash folder path doesn't
+ // match mail.server.serverX.trash_folder_name.
+ trashFolder->ClearFlag(nsMsgFolderFlags::Trash);
+ }
+ }
+ }
+ }
+
+ bool usingSubscription = true;
+ GetUsingSubscription(&usingSubscription);
+
+ nsCOMArray<nsIMsgImapMailFolder> unverifiedFolders;
+ GetUnverifiedFolders(unverifiedFolders);
+
+ int32_t count = unverifiedFolders.Count();
+ for (int32_t k = 0; k < count; ++k) {
+ bool explicitlyVerify = false;
+ bool hasSubFolders = false;
+ uint32_t folderFlags;
+ nsCOMPtr<nsIMsgImapMailFolder> currentImapFolder(unverifiedFolders[k]);
+ nsCOMPtr<nsIMsgFolder> currentFolder(
+ do_QueryInterface(currentImapFolder, &rv));
+ if (NS_FAILED(rv)) continue;
+
+ currentFolder->GetFlags(&folderFlags);
+ if (folderFlags &
+ nsMsgFolderFlags::Virtual) // don't remove virtual folders
+ continue;
+
+ if ((!usingSubscription ||
+ (NS_SUCCEEDED(
+ currentImapFolder->GetExplicitlyVerify(&explicitlyVerify)) &&
+ explicitlyVerify)) ||
+ ((NS_SUCCEEDED(currentFolder->GetHasSubFolders(&hasSubFolders)) &&
+ hasSubFolders) &&
+ !NoDescendentsAreVerified(currentFolder))) {
+ bool isNamespace;
+ currentImapFolder->GetIsNamespace(&isNamespace);
+ if (!isNamespace) // don't list namespaces explicitly
+ {
+ // If there are no subfolders and this is unverified, we don't want to
+ // run this url. That is, we want to undiscover the folder.
+ // If there are subfolders and no descendants are verified, we want to
+ // undiscover all of the folders.
+ // Only if there are subfolders and at least one of them is verified
+ // do we want to refresh that folder's flags, because it won't be going
+ // away.
+ currentImapFolder->SetExplicitlyVerify(false);
+ currentImapFolder->List();
+ }
+ } else {
+ nsCOMPtr<nsIMsgFolder> parent;
+ currentFolder->GetParent(getter_AddRefs(parent));
+ if (parent) {
+ currentImapFolder->RemoveLocalSelf();
+ }
+ }
+ }
+
+ return rv;
+}
+
+// Check if the special folder corresponding to the uri exists. If not, check
+// if there already exists a folder with the special folder flag (the server may
+// have told us about a folder to use through XLIST). If so, return the uri of
+// the existing special folder. If not, set the special flag on the folder so
+// it will be there if and when the folder is created.
+// Return true if we found an existing special folder different than
+// the one specified in prefs, and the one specified by prefs doesn't exist.
+bool nsImapIncomingServer::CheckSpecialFolder(nsCString& folderUri,
+ uint32_t folderFlag,
+ nsCString& existingUri) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, false);
+ nsCOMPtr<nsIMsgFolder> existingFolder;
+ rootMsgFolder->GetFolderWithFlags(folderFlag, getter_AddRefs(existingFolder));
+
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ folder->GetParent(getter_AddRefs(parent));
+ if (parent) {
+ existingFolder = nullptr;
+ }
+ if (!existingFolder) {
+ folder->SetFlag(folderFlag);
+ }
+
+ nsString folderName;
+ folder->GetPrettyName(folderName);
+ // this will set the localized name based on the folder flag.
+ folder->SetPrettyName(folderName);
+ }
+
+ if (existingFolder) {
+ existingFolder->GetURI(existingUri);
+ return true;
+ }
+
+ return false;
+}
+
+bool nsImapIncomingServer::NoDescendentsAreVerified(
+ nsIMsgFolder* parentFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = parentFolder->GetSubFolders(subFolders);
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* child : subFolders) {
+ nsCOMPtr<nsIMsgImapMailFolder> childImapFolder =
+ do_QueryInterface(child, &rv);
+ if (NS_SUCCEEDED(rv) && childImapFolder) {
+ bool childVerified = false;
+ rv = childImapFolder->GetVerifiedAsOnlineFolder(&childVerified);
+ if (NS_SUCCEEDED(rv) && childVerified) {
+ return false;
+ }
+ if (!NoDescendentsAreVerified(child)) {
+ return false;
+ }
+ }
+ }
+ }
+ // If we get this far we didn't find any verified.
+ return true;
+}
+
+bool nsImapIncomingServer::AllDescendentsAreNoSelect(
+ nsIMsgFolder* parentFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = parentFolder->GetSubFolders(subFolders);
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* child : subFolders) {
+ nsCOMPtr<nsIMsgImapMailFolder> childImapFolder =
+ do_QueryInterface(child, &rv);
+ if (NS_SUCCEEDED(rv) && childImapFolder) {
+ uint32_t flags;
+ rv = child->GetFlags(&flags);
+ bool isNoSelect =
+ NS_SUCCEEDED(rv) && (flags & nsMsgFolderFlags::ImapNoselect);
+ if (!isNoSelect) {
+ return false;
+ }
+ if (!AllDescendentsAreNoSelect(child)) {
+ return false;
+ }
+ }
+ }
+ }
+ // If we get this far we found none without the Noselect flag.
+ return true;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::PromptLoginFailed(nsIMsgWindow* aMsgWindow,
+ int32_t* aResult) {
+ nsAutoCString hostName;
+ GetHostName(hostName);
+
+ nsAutoCString userName;
+ GetUsername(userName);
+
+ nsAutoString accountName;
+ GetPrettyName(accountName);
+
+ return MsgPromptLoginFailed(aMsgWindow, hostName, userName, accountName,
+ aResult);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::FEAlert(const nsAString& aAlertString,
+ nsIMsgMailNewsUrl* aUrl) {
+ GetStringBundle();
+
+ if (m_stringBundle) {
+ nsAutoString hostName;
+ nsresult rv = GetPrettyName(hostName);
+ if (NS_SUCCEEDED(rv)) {
+ nsString message;
+ nsString tempString(aAlertString);
+ AutoTArray<nsString, 2> params = {hostName, tempString};
+
+ rv = m_stringBundle->FormatStringFromName("imapServerAlert", params,
+ message);
+ if (NS_SUCCEEDED(rv)) {
+ aUrl->SetErrorCode("imap-server-alert"_ns);
+ aUrl->SetErrorMessage(message);
+
+ return AlertUser(message, aUrl);
+ }
+ }
+ }
+ return AlertUser(aAlertString, aUrl);
+}
+
+nsresult nsImapIncomingServer::AlertUser(const nsAString& aString,
+ nsIMsgMailNewsUrl* aUrl) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return mailSession->AlertUser(aString, aUrl);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::FEAlertWithName(const char* aMsgName,
+ nsIMsgMailNewsUrl* aUrl) {
+ // don't bother the user if we're shutting down.
+ if (m_shuttingDown) return NS_OK;
+
+ GetStringBundle();
+
+ nsString message;
+
+ if (m_stringBundle) {
+ nsAutoCString hostName;
+ nsresult rv = GetHostName(hostName);
+ if (NS_SUCCEEDED(rv)) {
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(hostName, *params.AppendElement());
+ rv = m_stringBundle->FormatStringFromName(aMsgName, params, message);
+ if (NS_SUCCEEDED(rv)) {
+ aUrl->SetErrorCode(nsDependentCString(aMsgName));
+ aUrl->SetErrorMessage(message);
+
+ return AlertUser(message, aUrl);
+ }
+ }
+ }
+
+ // Error condition
+ message.AssignLiteral("String Name ");
+ message.AppendASCII(aMsgName);
+ FEAlert(message, aUrl);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::FEAlertFromServer(
+ const nsACString& aServerString, nsIMsgMailNewsUrl* aUrl) {
+ NS_ENSURE_TRUE(!aServerString.IsEmpty(), NS_OK);
+
+ nsCString message(aServerString);
+ message.Trim(" \t\b\r\n");
+ NS_ENSURE_TRUE(!message.IsEmpty(), NS_OK);
+ if (message.Last() != '.') message.Append('.');
+
+ // Skip over the first two words (the command tag and "NO").
+ // Find the first word break.
+ int32_t pos = message.FindChar(' ');
+
+ // Find the second word break.
+ if (pos != -1) pos = message.FindChar(' ', pos + 1);
+
+ // Adjust the message.
+ if (pos != -1) message = Substring(message, pos + 1);
+
+ nsString hostName;
+ GetPrettyName(hostName);
+
+ AutoTArray<nsString, 3> formatStrings = {hostName};
+
+ const char* msgName;
+ nsString fullMessage;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl);
+ NS_ENSURE_TRUE(imapUrl, NS_ERROR_INVALID_ARG);
+
+ nsImapState imapState;
+ nsImapAction imapAction;
+
+ imapUrl->GetRequiredImapState(&imapState);
+ imapUrl->GetImapAction(&imapAction);
+ nsString folderName;
+
+ NS_ConvertUTF8toUTF16 unicodeMsg(message);
+
+ aUrl->SetErrorCode("imap-server-error"_ns);
+ aUrl->SetErrorMessage(unicodeMsg);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ if (imapState == nsIImapUrl::nsImapSelectedState ||
+ imapAction == nsIImapUrl::nsImapFolderStatus) {
+ aUrl->GetFolder(getter_AddRefs(folder));
+ if (folder) folder->GetPrettyName(folderName);
+ msgName = "imapFolderCommandFailed";
+ formatStrings.AppendElement(folderName);
+ } else {
+ msgName = "imapServerCommandFailed";
+ }
+
+ formatStrings.AppendElement(unicodeMsg);
+
+ nsresult rv = GetStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_stringBundle) {
+ rv = m_stringBundle->FormatStringFromName(msgName, formatStrings,
+ fullMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return AlertUser(fullMessage, aUrl);
+}
+
+#define IMAP_MSGS_URL "chrome://messenger/locale/imapMsgs.properties"
+
+nsresult nsImapIncomingServer::GetStringBundle() {
+ if (m_stringBundle) return NS_OK;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ return sBundleService->CreateBundle(IMAP_MSGS_URL,
+ getter_AddRefs(m_stringBundle));
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetImapStringByName(const char* msgName,
+ nsAString& aString) {
+ nsresult rv = NS_OK;
+ GetStringBundle();
+ if (m_stringBundle) {
+ nsString res_str;
+ rv = m_stringBundle->GetStringFromName(msgName, res_str);
+ aString.Assign(res_str);
+ if (NS_SUCCEEDED(rv)) return rv;
+ }
+ aString.AssignLiteral("String Name ");
+ // mscott: FIX ME
+ aString.AppendASCII(msgName);
+ return NS_OK;
+}
+
+nsresult nsImapIncomingServer::ResetFoldersToUnverified(
+ nsIMsgFolder* parentFolder) {
+ nsresult rv = NS_OK;
+ if (!parentFolder) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ResetFoldersToUnverified(rootFolder);
+ }
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(parentFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapFolder->SetVerifiedAsOnlineFolder(false);
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = parentFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* child : subFolders) {
+ rv = ResetFoldersToUnverified(child);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+void nsImapIncomingServer::GetUnverifiedFolders(
+ nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ if (NS_FAILED(GetRootFolder(getter_AddRefs(rootFolder))) || !rootFolder)
+ return;
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot(do_QueryInterface(rootFolder));
+ // don't need to verify the root.
+ if (imapRoot) imapRoot->SetVerifiedAsOnlineFolder(true);
+
+ GetUnverifiedSubFolders(rootFolder, aFoldersArray);
+}
+
+void nsImapIncomingServer::GetUnverifiedSubFolders(
+ nsIMsgFolder* parentFolder,
+ nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(parentFolder));
+
+ bool verified = false, explicitlyVerify = false;
+ if (imapFolder) {
+ nsresult rv = imapFolder->GetVerifiedAsOnlineFolder(&verified);
+ if (NS_SUCCEEDED(rv))
+ rv = imapFolder->GetExplicitlyVerify(&explicitlyVerify);
+
+ if (NS_SUCCEEDED(rv) && (!verified || explicitlyVerify))
+ aFoldersArray.AppendObject(imapFolder);
+ }
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ if (NS_SUCCEEDED(parentFolder->GetSubFolders(subFolders))) {
+ for (nsIMsgFolder* child : subFolders) {
+ GetUnverifiedSubFolders(child, aFoldersArray);
+ }
+ }
+}
+
+NS_IMETHODIMP nsImapIncomingServer::ForgetSessionPassword(bool modifyLogin) {
+ bool usingOauth2 = false;
+ if (modifyLogin) {
+ // Only need to check for oauth2 if modifyLogin is true.
+ int32_t authMethod = 0;
+ GetAuthMethod(&authMethod);
+ usingOauth2 = (authMethod == nsMsgAuthMethod::OAuth2);
+ }
+
+ // Clear the cached password if not using Oauth2 or if modifyLogin is false.
+ if (!usingOauth2 || !modifyLogin) {
+ nsresult rv = nsMsgIncomingServer::ForgetSessionPassword(modifyLogin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // fix for bugscape bug #15485
+ // if we use turbo, and we logout, we need to make sure
+ // the server doesn't think it's authenticated.
+ // the biff timer continues to fire when you use turbo
+ // (see #143848). if we exited, we've set the password to null
+ // but if we're authenticated, and the biff timer goes off
+ // we'll still perform biff, because we use m_userAuthenticated
+ // to determine if we require a password for biff.
+ // (if authenticated, we don't require a password
+ // see nsMsgBiffManager::PerformBiff())
+ // performing biff without a password will pop up the prompt dialog
+ // which is pretty wacky, when it happens after you quit the application
+ m_userAuthenticated = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) {
+ NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff);
+ // if the user has already been authenticated, we've got the password
+ *aServerRequiresPasswordForBiff = !m_userAuthenticated;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::ForgetPassword() {
+ return nsMsgIncomingServer::ForgetPassword();
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::AsyncGetPassword(nsIImapProtocol* aProtocol,
+ bool aNewPasswordRequested,
+ nsAString& aPassword) {
+ if (m_password.IsEmpty()) {
+ // We're now going to need to do something that will end up with us either
+ // poking login manager or prompting the user. We need to ensure we only
+ // do one prompt at a time (and login manager could cause a master password
+ // prompt), so we need to use the async prompter.
+ nsresult rv;
+ nsCOMPtr<nsIMsgAsyncPrompter> asyncPrompter =
+ do_GetService("@mozilla.org/messenger/msgAsyncPrompter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAsyncPromptListener> promptListener(
+ do_QueryInterface(aProtocol));
+ rv = asyncPrompter->QueueAsyncAuthPrompt(m_serverKey, aNewPasswordRequested,
+ promptListener);
+ // Explicit NS_ENSURE_SUCCESS for debug purposes as errors tend to get
+ // hidden.
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (!m_password.IsEmpty()) aPassword = m_password;
+ return NS_OK;
+}
+
+// Get password already stored in login manager. This won't trigger a prompt
+// if no password string is present.
+NS_IMETHODIMP
+nsImapIncomingServer::SyncGetPassword(nsAString& aPassword) {
+ nsresult rv = NS_OK;
+ if (NS_SUCCEEDED(GetPasswordWithoutUI()) && !m_password.IsEmpty())
+ aPassword = m_password;
+ else
+ rv = NS_ERROR_NOT_AVAILABLE;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::PromptPassword(nsIMsgWindow* aMsgWindow,
+ nsAString& aPassword) {
+ nsAutoCString userName;
+ GetUsername(userName);
+
+ nsAutoCString hostName;
+ GetHostName(hostName);
+
+ nsresult rv = GetStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 1> formatStrings;
+ CopyUTF8toUTF16(userName, *formatStrings.AppendElement());
+
+ nsString passwordTitle;
+ rv = m_stringBundle->FormatStringFromName(
+ "imapEnterPasswordPromptTitleWithUsername", formatStrings, passwordTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 2> formatStrings2;
+ CopyUTF8toUTF16(userName, *formatStrings2.AppendElement());
+ CopyUTF8toUTF16(hostName, *formatStrings2.AppendElement());
+
+ nsString passwordText;
+ rv = m_stringBundle->FormatStringFromName("imapEnterServerPasswordPrompt",
+ formatStrings2, passwordText);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetPasswordWithUI(passwordText, passwordTitle, aPassword);
+ if (NS_SUCCEEDED(rv)) m_password = aPassword;
+ return rv;
+}
+
+// for the nsIImapServerSink interface
+NS_IMETHODIMP nsImapIncomingServer::SetCapability(
+ eIMAPCapabilityFlags capability) {
+ m_capability = capability;
+ SetIsGMailServer((capability & kGmailImapCapability) != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCapability(eIMAPCapabilityFlags* capability) {
+ NS_ENSURE_ARG_POINTER(capability);
+ *capability = m_capability;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetServerID(const nsACString& aServerID) {
+ return SetServerIDPref(aServerID);
+}
+
+NS_IMETHODIMP nsImapIncomingServer::CommitNamespaces() {
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return hostSession->CommitNamespacesForHost(this);
+}
+
+NS_IMETHODIMP nsImapIncomingServer::PseudoInterruptMsgLoad(
+ nsIMsgFolder* aImapFolder, nsIMsgWindow* aMsgWindow, bool* interrupted) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapProtocol> connection;
+ PR_CEnterMonitor(this);
+ // iterate through the connection cache for a connection that is loading
+ // a message in this folder and should be pseudo-interrupted.
+ int32_t cnt = m_connectionCache.Count();
+
+ for (int32_t i = 0; i < cnt; ++i) {
+ connection = m_connectionCache[i];
+ if (connection)
+ rv = connection->PseudoInterruptMsgLoad(aImapFolder, aMsgWindow,
+ interrupted);
+ }
+
+ PR_CExitMonitor(this);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::ResetNamespaceReferences() {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(rootFolder);
+ if (imapFolder) rv = imapFolder->ResetNamespaceReferences();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetUserAuthenticated(
+ bool aUserAuthenticated) {
+ m_userAuthenticated = aUserAuthenticated;
+ if (aUserAuthenticated) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ accountManager->SetUserNeedsToAuthenticate(false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetUserAuthenticated(
+ bool* aUserAuthenticated) {
+ NS_ENSURE_ARG_POINTER(aUserAuthenticated);
+ *aUserAuthenticated = m_userAuthenticated;
+ return NS_OK;
+}
+
+/* void SetMailServerUrls (in string manageMailAccount, in string manageLists,
+ * in string manageFilters); */
+NS_IMETHODIMP nsImapIncomingServer::SetMailServerUrls(
+ const nsACString& manageMailAccount, const nsACString& manageLists,
+ const nsACString& manageFilters) {
+ return SetManageMailAccountUrl(manageMailAccount);
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetManageMailAccountUrl(
+ const nsACString& manageMailAccountUrl) {
+ m_manageMailAccountUrl = manageMailAccountUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetManageMailAccountUrl(
+ nsACString& manageMailAccountUrl) {
+ manageMailAccountUrl = m_manageMailAccountUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::StartPopulatingWithUri(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer /*ignored*/,
+ const nsACString& uri) {
+ nsresult rv;
+ mDoingSubscribeDialog = true;
+
+ rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mInner->StartPopulatingWithUri(aMsgWindow, aForceToServer, uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // imap always uses the canonical delimiter form of paths for subscribe ui.
+ rv = SetDelimiter('/');
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetShowFullName(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverUri;
+ rv = GetServerURI(serverUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /*
+ if uri = imap://user@host/foo/bar, the serverUri is imap://user@host
+ to get path from uri, skip over imap://user@host + 1 (for the /)
+ */
+ return imapService->GetListOfFoldersWithPath(
+ this, aMsgWindow, Substring(uri, serverUri.Length() + 1));
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::StartPopulating(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer /*ignored*/,
+ bool aGetOnlyNew) {
+ nsresult rv;
+ mDoingSubscribeDialog = true;
+
+ rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mInner->StartPopulating(aMsgWindow, aForceToServer, aGetOnlyNew);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // imap always uses the canonical delimiter form of paths for subscribe ui.
+ rv = SetDelimiter('/');
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetShowFullName(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->GetListOfFoldersOnServer(this, aMsgWindow);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
+
+NS_IMETHODIMP
+nsImapIncomingServer::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+
+ // xxx todo get msgWindow from url
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url);
+ if (imapUrl) {
+ nsImapAction imapAction = nsIImapUrl::nsImapTest;
+ imapUrl->GetImapAction(&imapAction);
+ switch (imapAction) {
+ case nsIImapUrl::nsImapDiscoverAllAndSubscribedBoxesUrl:
+ case nsIImapUrl::nsImapDiscoverChildrenUrl:
+ rv = UpdateSubscribed();
+ NS_ENSURE_SUCCESS(rv, rv);
+ mDoingSubscribeDialog = false;
+ rv = StopPopulating(msgWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case nsIImapUrl::nsImapDiscoverAllBoxesUrl:
+ if (NS_SUCCEEDED(exitCode)) DiscoveryDone();
+ break;
+ case nsIImapUrl::nsImapFolderStatus: {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(imapUrl);
+ mailUrl->GetFolder(getter_AddRefs(msgFolder));
+ if (msgFolder) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool folderOpen;
+ rv = session->IsFolderOpenInWindow(msgFolder, &folderOpen);
+ if (NS_SUCCEEDED(rv) && !folderOpen && msgFolder)
+ msgFolder->SetMsgDatabase(nullptr);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(msgFolder);
+ m_foldersToStat.RemoveObject(imapFolder);
+ }
+ // if we get an error running the url, it's better
+ // not to chain the next url.
+ if (NS_FAILED(exitCode) && exitCode != NS_MSG_ERROR_IMAP_COMMAND_FAILED)
+ m_foldersToStat.Clear();
+ if (m_foldersToStat.Count() > 0)
+ m_foldersToStat[0]->UpdateStatus(this, nullptr);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetIncomingServer(nsIMsgIncomingServer* aServer) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetIncomingServer(aServer);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetShowFullName(bool showFullName) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetShowFullName(showFullName);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetDelimiter(char* aDelimiter) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetDelimiter(aDelimiter);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetDelimiter(char aDelimiter) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetDelimiter(aDelimiter);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetAsSubscribed(const nsACString& path) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetAsSubscribed(path);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::UpdateSubscribed() { return NS_OK; }
+
+NS_IMETHODIMP
+nsImapIncomingServer::AddTo(const nsACString& aName, bool addAsSubscribed,
+ bool aSubscribable, bool changeIfExists) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // RFC 3501 allows UTF-8 in addition to MUTF-7.
+ // If it's not UTF-8, it's not 7bit-ASCII and cannot be MUTF-7 either.
+ // We just ignore it.
+ if (!mozilla::IsUtf8(aName)) return NS_OK;
+ // Now handle subscription folder names as UTF-8 so don't convert to MUTF-7.
+ return mInner->AddTo(aName, addAsSubscribed, aSubscribable, changeIfExists);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::StopPopulating(nsIMsgWindow* aMsgWindow) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->StopPopulating(aMsgWindow);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SubscribeCleanup() {
+ m_subscribeFolders.Clear();
+ return ClearInner();
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetSubscribeListener(nsISubscribeListener* aListener) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetSubscribeListener(aListener);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetSubscribeListener(nsISubscribeListener** aListener) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetSubscribeListener(aListener);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::Subscribe(const char16_t* aName) {
+ NS_ENSURE_ARG_POINTER(aName);
+ return SubscribeToFolder(nsDependentString(aName), true, nullptr);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::Unsubscribe(const char16_t* aName) {
+ NS_ENSURE_ARG_POINTER(aName);
+
+ return SubscribeToFolder(nsDependentString(aName), false, nullptr);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SubscribeToFolder(const nsAString& aName, bool subscribe,
+ nsIURI** aUri) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = GetRootFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Locate the folder so that the correct hierarchical delimiter is used in the
+ // folder pathnames, otherwise root's (ie, '^') is used and this is wrong.
+
+ // aName is not a genuine UTF-16 but just a zero-padded MUTF-7.
+ NS_ConvertUTF16toUTF8 folderCName(aName);
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ if (rootMsgFolder && !aName.IsEmpty())
+ rv = rootMsgFolder->FindSubFolder(folderCName, getter_AddRefs(msgFolder));
+
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+
+ if (subscribe)
+ rv = imapService->SubscribeFolder(msgFolder, aName, nullptr, aUri);
+ else
+ rv = imapService->UnsubscribeFolder(msgFolder, aName, nullptr, nullptr);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetDoingLsub(bool doingLsub) {
+ mDoingLsub = doingLsub;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetDoingLsub(bool* doingLsub) {
+ NS_ENSURE_ARG_POINTER(doingLsub);
+ *doingLsub = mDoingLsub;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetUtf8AcceptEnabled(bool enabled) {
+ mUtf8AcceptEnabled = enabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetUtf8AcceptEnabled(bool* enabled) {
+ NS_ENSURE_ARG_POINTER(enabled);
+ *enabled = mUtf8AcceptEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::ReDiscoverAllFolders() { return PerformExpand(nullptr); }
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetState(const nsACString& path, bool state,
+ bool* stateChanged) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->SetState(path, state, stateChanged);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::HasChildren(const nsACString& path, bool* aHasChildren) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->HasChildren(path, aHasChildren);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::IsSubscribed(const nsACString& path,
+ bool* aIsSubscribed) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->IsSubscribed(path, aIsSubscribed);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::IsSubscribable(const nsACString& path,
+ bool* aIsSubscribable) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->IsSubscribable(path, aIsSubscribable);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetLeafName(const nsACString& path,
+ nsAString& aLeafName) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetLeafName(path, aLeafName);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetFirstChildURI(const nsACString& path,
+ nsACString& aResult) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetFirstChildURI(path, aResult);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetChildURIs(const nsACString& aPath,
+ nsTArray<nsCString>& aResult) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetChildURIs(aPath, aResult);
+}
+
+nsresult nsImapIncomingServer::EnsureInner() {
+ nsresult rv = NS_OK;
+
+ if (mInner) return NS_OK;
+
+ mInner = do_CreateInstance(kSubscribableServerCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetIncomingServer(this);
+}
+
+nsresult nsImapIncomingServer::ClearInner() {
+ nsresult rv = NS_OK;
+ if (mInner) {
+ rv = mInner->SetSubscribeListener(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mInner->SetIncomingServer(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mInner = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::CommitSubscribeChanges() {
+ return ReDiscoverAllFolders();
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanBeDefaultServer(bool* canBeDefaultServer) {
+ NS_ENSURE_ARG_POINTER(canBeDefaultServer);
+ *canBeDefaultServer = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanCompactFoldersOnServer(
+ bool* canCompactFoldersOnServer) {
+ NS_ENSURE_ARG_POINTER(canCompactFoldersOnServer);
+ // Initialize canCompactFoldersOnServer true, a default value for IMAP
+ *canCompactFoldersOnServer = true;
+ GetPrefForServerAttribute("canCompactFoldersOnServer",
+ canCompactFoldersOnServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) {
+ NS_ENSURE_ARG_POINTER(canUndoDeleteOnServer);
+ // Initialize canUndoDeleteOnServer true, a default value for IMAP
+ *canUndoDeleteOnServer = true;
+ GetPrefForServerAttribute("canUndoDeleteOnServer", canUndoDeleteOnServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanSearchMessages(bool* canSearchMessages) {
+ NS_ENSURE_ARG_POINTER(canSearchMessages);
+ // Initialize canSearchMessages true, a default value for IMAP
+ *canSearchMessages = true;
+ GetPrefForServerAttribute("canSearchMessages", canSearchMessages);
+ return NS_OK;
+}
+
+nsresult nsImapIncomingServer::CreateHostSpecificPrefName(
+ const char* prefPrefix, nsAutoCString& prefName) {
+ NS_ENSURE_ARG_POINTER(prefPrefix);
+
+ nsCString hostName;
+ nsresult rv = GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ prefName = prefPrefix;
+ prefName.Append('.');
+ prefName.Append(hostName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetSupportsDiskSpace(bool* aSupportsDiskSpace) {
+ NS_ENSURE_ARG_POINTER(aSupportsDiskSpace);
+ nsAutoCString prefName;
+ nsresult rv =
+ CreateHostSpecificPrefName("default_supports_diskspace", prefName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = prefBranch->GetBoolPref(prefName.get(), aSupportsDiskSpace);
+
+ // Couldn't get the default value with the hostname.
+ // Fall back on IMAP default value
+ if (NS_FAILED(rv)) // set default value
+ *aSupportsDiskSpace = true;
+ return NS_OK;
+}
+
+// count number of non-busy connections in cache
+NS_IMETHODIMP
+nsImapIncomingServer::GetNumIdleConnections(int32_t* aNumIdleConnections) {
+ NS_ENSURE_ARG_POINTER(aNumIdleConnections);
+ *aNumIdleConnections = 0;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapProtocol> connection;
+ bool isBusy = false;
+ bool isInboxConnection;
+ PR_CEnterMonitor(this);
+
+ int32_t cnt = m_connectionCache.Count();
+
+ // loop counting idle connections
+ for (int32_t i = 0; i < cnt; ++i) {
+ connection = m_connectionCache[i];
+ if (connection) {
+ rv = connection->IsBusy(&isBusy, &isInboxConnection);
+ if (NS_FAILED(rv)) continue;
+ if (!isBusy) (*aNumIdleConnections)++;
+ }
+ }
+ PR_CExitMonitor(this);
+ return rv;
+}
+
+/**
+ * Get the preference that tells us whether the imap server in question allows
+ * us to create subfolders. Some ISPs might not want users to create any folders
+ * besides the existing ones.
+ * We do want to identify all those servers that don't allow creation of
+ * subfolders and take them out of the account picker in the Copies and Folder
+ * panel.
+ */
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanCreateFoldersOnServer(
+ bool* aCanCreateFoldersOnServer) {
+ NS_ENSURE_ARG_POINTER(aCanCreateFoldersOnServer);
+ // Initialize aCanCreateFoldersOnServer true, a default value for IMAP
+ *aCanCreateFoldersOnServer = true;
+ GetPrefForServerAttribute("canCreateFolders", aCanCreateFoldersOnServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetOfflineSupportLevel(int32_t* aSupportLevel) {
+ NS_ENSURE_ARG_POINTER(aSupportLevel);
+ nsresult rv = NS_OK;
+
+ rv = GetIntValue("offline_support_level", aSupportLevel);
+ if (*aSupportLevel != OFFLINE_SUPPORT_LEVEL_UNDEFINED) return rv;
+
+ nsAutoCString prefName;
+ rv = CreateHostSpecificPrefName("default_offline_support_level", prefName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = prefBranch->GetIntPref(prefName.get(), aSupportLevel);
+
+ // Couldn't get the pref value with the hostname.
+ // Fall back on IMAP default value
+ if (NS_FAILED(rv)) // set default value
+ *aSupportLevel = OFFLINE_SUPPORT_LEVEL_REGULAR;
+ return NS_OK;
+}
+
+// Called only during the migration process. This routine enables the generation
+// of unique account name based on the username, hostname and the port. If the
+// port is valid and not a default one, it will be appended to the account name.
+NS_IMETHODIMP
+nsImapIncomingServer::GeneratePrettyNameForMigration(nsAString& aPrettyName) {
+ nsCString userName;
+ nsCString hostName;
+
+ /**
+ * Pretty name for migrated account is of format username@hostname:<port>,
+ * provided the port is valid and not the default
+ */
+ // Get user name to construct pretty name
+ nsresult rv = GetUsername(userName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get host name to construct pretty name
+ rv = GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t defaultServerPort;
+ int32_t defaultSecureServerPort;
+
+ // Here, the final contract ID is already known, so use it directly for
+ // efficiency.
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo =
+ do_GetService("@mozilla.org/messenger/protocol/info;1?type=imap", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the default port
+ rv = protocolInfo->GetDefaultServerPort(false, &defaultServerPort);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the default secure port
+ rv = protocolInfo->GetDefaultServerPort(true, &defaultSecureServerPort);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current server port
+ int32_t serverPort = PORT_NOT_SET;
+ rv = GetPort(&serverPort);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Is the server secure ?
+ int32_t socketType;
+ rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isSecure = (socketType == nsMsgSocketType::SSL);
+
+ // Is server port a default port ?
+ bool isItDefaultPort = false;
+ if (((serverPort == defaultServerPort) && !isSecure) ||
+ ((serverPort == defaultSecureServerPort) && isSecure))
+ isItDefaultPort = true;
+
+ // Construct pretty name from username and hostname
+ nsAutoString constructedPrettyName;
+ CopyASCIItoUTF16(userName, constructedPrettyName);
+ constructedPrettyName.Append('@');
+ constructedPrettyName.Append(NS_ConvertASCIItoUTF16(hostName));
+
+ // If the port is valid and not default, add port value to the pretty name
+ if ((serverPort > 0) && (!isItDefaultPort)) {
+ constructedPrettyName.Append(':');
+ constructedPrettyName.AppendInt(serverPort);
+ }
+
+ // Format the pretty name
+ return GetFormattedStringFromName(constructedPrettyName,
+ "imapDefaultAccountName", aPrettyName);
+}
+
+nsresult nsImapIncomingServer::GetFormattedStringFromName(
+ const nsAString& aValue, const char* aName, nsAString& aResult) {
+ nsresult rv = GetStringBundle();
+ if (m_stringBundle) {
+ nsString tmpVal(aValue);
+ AutoTArray<nsString, 1> formatStrings = {tmpVal};
+
+ nsString result;
+ rv = m_stringBundle->FormatStringFromName(aName, formatStrings, result);
+ aResult.Assign(result);
+ }
+ return rv;
+}
+
+nsresult nsImapIncomingServer::GetPrefForServerAttribute(const char* prefSuffix,
+ bool* prefValue) {
+ // Any caller of this function must initialize prefValue with a default value
+ // as this code will not set prefValue when the pref does not exist and return
+ // NS_OK anyway
+
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ NS_ENSURE_ARG_POINTER(prefValue);
+
+ if (NS_FAILED(mPrefBranch->GetBoolPref(prefSuffix, prefValue)))
+ mDefPrefBranch->GetBoolPref(prefSuffix, prefValue);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetCanFileMessagesOnServer(
+ bool* aCanFileMessagesOnServer) {
+ NS_ENSURE_ARG_POINTER(aCanFileMessagesOnServer);
+ // Initialize aCanFileMessagesOnServer true, a default value for IMAP
+ *aCanFileMessagesOnServer = true;
+ GetPrefForServerAttribute("canFileMessages", aCanFileMessagesOnServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetSearchValue(const nsAString& searchValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetSupportsSubscribeSearch(bool* retVal) {
+ NS_ENSURE_ARG_POINTER(retVal);
+ *retVal = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetFolderView(nsITreeView** aView) {
+ nsresult rv = EnsureInner();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mInner->GetFolderView(aView);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetFilterScope(nsMsgSearchScopeValue* filterScope) {
+ NS_ENSURE_ARG_POINTER(filterScope);
+ // If the inbox is enabled for offline use, then use the offline filter
+ // scope, else use the online filter scope.
+ //
+ // XXX We use the same scope for all folders with the same incoming server,
+ // yet it is possible to set the offline flag separately for each folder.
+ // Manual filters could perhaps check the offline status of each folder,
+ // though it's hard to see how to make that work since we only store filters
+ // per server.
+ //
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> offlineInboxMsgFolder;
+ rv = rootMsgFolder->GetFolderWithFlags(
+ nsMsgFolderFlags::Inbox | nsMsgFolderFlags::Offline,
+ getter_AddRefs(offlineInboxMsgFolder));
+
+ *filterScope = offlineInboxMsgFolder ? nsMsgSearchScope::offlineMailFilter
+ : nsMsgSearchScope::onlineMailFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetSearchScope(nsMsgSearchScopeValue* searchScope) {
+ NS_ENSURE_ARG_POINTER(searchScope);
+ *searchScope = WeAreOffline() ? nsMsgSearchScope::offlineMail
+ : nsMsgSearchScope::onlineMail;
+ return NS_OK;
+}
+
+// This is a recursive function. It gets new messages for current folder
+// first if it is marked, then calls itself recursively for each subfolder.
+NS_IMETHODIMP
+nsImapIncomingServer::GetNewMessagesForNonInboxFolders(nsIMsgFolder* aFolder,
+ nsIMsgWindow* aWindow,
+ bool forceAllFolders,
+ bool performingBiff) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ static bool gGotStatusPref = false;
+ static bool gUseStatus = false;
+
+ bool isServer;
+ (void)aFolder->GetIsServer(&isServer);
+ // Check this folder for new messages if it is marked to be checked
+ // or if we are forced to check all folders
+ uint32_t flags = 0;
+ aFolder->GetFlags(&flags);
+ nsresult rv;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool canOpen;
+ imapFolder->GetCanOpenFolder(&canOpen);
+ if (canOpen &&
+ ((forceAllFolders &&
+ !(flags & (nsMsgFolderFlags::Inbox | nsMsgFolderFlags::Trash |
+ nsMsgFolderFlags::Junk | nsMsgFolderFlags::Virtual))) ||
+ flags & nsMsgFolderFlags::CheckNew)) {
+ // Get new messages for this folder.
+ aFolder->SetGettingNewMessages(true);
+ if (performingBiff) imapFolder->SetPerformingBiff(true);
+ bool isOpen = false;
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (mailSession && aFolder)
+ mailSession->IsFolderOpenInWindow(aFolder, &isOpen);
+ // eventually, the gGotStatusPref should go away, once we work out the kinks
+ // from using STATUS.
+ if (!gGotStatusPref) {
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefBranch)
+ prefBranch->GetBoolPref("mail.imap.use_status_for_biff", &gUseStatus);
+ gGotStatusPref = true;
+ }
+ if (gUseStatus && !isOpen) {
+ if (!isServer && m_foldersToStat.IndexOf(imapFolder) == -1)
+ m_foldersToStat.AppendObject(imapFolder);
+ } else
+ aFolder->UpdateFolder(aWindow);
+ }
+
+ // Loop through all subfolders to get new messages for them.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = aFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ GetNewMessagesForNonInboxFolders(msgFolder, aWindow, forceAllFolders,
+ performingBiff);
+ }
+ if (isServer && m_foldersToStat.Count() > 0)
+ m_foldersToStat[0]->UpdateStatus(this, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetArbitraryHeaders(nsACString& aResult) {
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ nsresult rv = GetFilterList(nullptr, getter_AddRefs(filterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return filterList->GetArbitraryHeaders(aResult);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetShowAttachmentsInline(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true; // true per default
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ prefBranch->GetBoolPref("mail.inline_attachments", aResult);
+ return NS_OK; // In case this pref is not set we need to return NS_OK.
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetSocketType(int32_t aSocketType) {
+ int32_t oldSocketType;
+ nsresult rv = GetSocketType(&oldSocketType);
+ if (NS_SUCCEEDED(rv) && oldSocketType != aSocketType)
+ CloseCachedConnections();
+ return nsMsgIncomingServer::SetSocketType(aSocketType);
+}
+
+// use canonical format in originalUri & convertedUri
+NS_IMETHODIMP
+nsImapIncomingServer::GetUriWithNamespacePrefixIfNecessary(
+ int32_t namespaceType, const nsACString& originalUri,
+ nsACString& convertedUri) {
+ nsresult rv = NS_OK;
+ nsAutoCString serverKey;
+ rv = GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapHostSessionList> hostSessionList =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ nsImapNamespace* ns = nullptr;
+ rv = hostSessionList->GetDefaultNamespaceOfTypeForHost(
+ serverKey.get(), (EIMAPNamespaceType)namespaceType, ns);
+ if (ns) {
+ nsAutoCString namespacePrefix(ns->GetPrefix());
+ if (!namespacePrefix.IsEmpty()) {
+ // check if namespacePrefix is the same as the online directory; if so,
+ // ignore it.
+ nsAutoCString onlineDir;
+ rv = GetServerDirectory(onlineDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!onlineDir.IsEmpty()) {
+ char delimiter = ns->GetDelimiter();
+ if (onlineDir.Last() != delimiter) onlineDir += delimiter;
+ if (onlineDir.Equals(namespacePrefix)) return NS_OK;
+ }
+
+ namespacePrefix.ReplaceChar(ns->GetDelimiter(),
+ '/'); // use canonical format
+ nsCString uri(originalUri);
+ int32_t index = uri.Find("//"); // find scheme
+ index = uri.FindChar('/', index + 2); // find '/' after scheme
+ // it may be the case that this is the INBOX uri, in which case
+ // we don't want to prepend the namespace. In that case, the uri ends with
+ // "INBOX", but the namespace is "INBOX/", so they don't match.
+ if (uri.Find(namespacePrefix, index + 1) != index + 1 &&
+ !Substring(uri, index + 1).LowerCaseEqualsLiteral("inbox"))
+ uri.Insert(namespacePrefix, index + 1); // insert namespace prefix
+ convertedUri = uri;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::GetTrashFolderName(nsAString& retval) {
+ // Despite its name, this returns a path, for example INBOX/Trash.
+ nsresult rv = GetUnicharValue(PREF_TRASH_FOLDER_PATH, retval);
+ if (NS_FAILED(rv)) return rv;
+ if (retval.IsEmpty())
+ retval = NS_LITERAL_STRING_FROM_CSTRING(DEFAULT_TRASH_FOLDER_PATH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapIncomingServer::SetTrashFolderName(
+ const nsAString& chvalue) {
+ // Clear trash flag from the old pref.
+ // Despite its name, this returns the trash folder path, for example
+ // INBOX/Trash.
+ bool useUTF8 = false;
+ GetUtf8AcceptEnabled(&useUTF8);
+ nsAutoString oldTrashName;
+ nsresult rv = GetTrashFolderName(oldTrashName);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString oldTrashNameUtf7or8;
+ nsCOMPtr<nsIMsgFolder> oldFolder;
+ // 'trashFolderName' being a path here works well since this is appended
+ // to the server's root folder in GetFolder().
+ if (useUTF8) {
+ CopyUTF16toUTF8(oldTrashName, oldTrashNameUtf7or8);
+ } else {
+ CopyUTF16toMUTF7(oldTrashName, oldTrashNameUtf7or8);
+ }
+ rv = GetFolder(oldTrashNameUtf7or8, getter_AddRefs(oldFolder));
+ if (NS_SUCCEEDED(rv) && oldFolder)
+ oldFolder->ClearFlag(nsMsgFolderFlags::Trash);
+ }
+
+ // If the user configured delete mode (model) is currently "move to trash",
+ // mark the newly designated trash folder name as the active trash
+ // destination folder.
+ int32_t deleteModel;
+ rv = GetDeleteModel(&deleteModel);
+ if (NS_SUCCEEDED(rv) && (deleteModel == nsMsgImapDeleteModels::MoveToTrash)) {
+ nsAutoCString newTrashNameUtf7or8;
+ if (useUTF8) {
+ CopyUTF16toUTF8(PromiseFlatString(chvalue), newTrashNameUtf7or8);
+ } else {
+ CopyUTF16toMUTF7(PromiseFlatString(chvalue), newTrashNameUtf7or8);
+ }
+ nsCOMPtr<nsIMsgFolder> newTrashFolder;
+ rv = GetFolder(newTrashNameUtf7or8, getter_AddRefs(newTrashFolder));
+ if (NS_SUCCEEDED(rv) && newTrashFolder)
+ newTrashFolder->SetFlag(nsMsgFolderFlags::Trash);
+ }
+
+ return SetUnicharValue(PREF_TRASH_FOLDER_PATH, chvalue);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetMsgFolderFromURI(nsIMsgFolder* aFolderResource,
+ const nsACString& aURI,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ bool namespacePrefixAdded = false;
+ nsCString folderUriWithNamespace;
+
+ // clang-format off
+ // Check if the folder exists as is...
+ nsresult rv = GetExistingMsgFolder(aURI, folderUriWithNamespace,
+ namespacePrefixAdded, false,
+ getter_AddRefs(msgFolder));
+
+ // Or try again with a case-insensitive lookup
+ if (NS_FAILED(rv) || !msgFolder)
+ rv = GetExistingMsgFolder(aURI, folderUriWithNamespace,
+ namespacePrefixAdded, true,
+ getter_AddRefs(msgFolder));
+ // clang-format on
+
+ if (NS_FAILED(rv) || !msgFolder) {
+ // we didn't find the folder so we will have to create a new one.
+ if (namespacePrefixAdded) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderUriWithNamespace, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgFolder = folder;
+ } else
+ msgFolder = aFolderResource;
+ }
+
+ msgFolder.forget(aFolder);
+ return (aFolder ? NS_OK : NS_ERROR_FAILURE);
+}
+
+nsresult nsImapIncomingServer::GetExistingMsgFolder(
+ const nsACString& aURI, nsACString& aFolderUriWithNamespace,
+ bool& aNamespacePrefixAdded, bool aCaseInsensitive,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aNamespacePrefixAdded = false;
+ // Check if the folder exists as is...Even if we have a personal namespace,
+ // it might be in another namespace (e.g., shared) and this will catch that.
+ rv = rootMsgFolder->GetChildWithURI(aURI, true, aCaseInsensitive, aFolder);
+
+ // If we couldn't find the folder as is, check if we need to prepend the
+ // personal namespace
+ if (!*aFolder) {
+ GetUriWithNamespacePrefixIfNecessary(kPersonalNamespace, aURI,
+ aFolderUriWithNamespace);
+ if (!aFolderUriWithNamespace.IsEmpty()) {
+ aNamespacePrefixAdded = true;
+ rv = rootMsgFolder->GetChildWithURI(aFolderUriWithNamespace, true,
+ aCaseInsensitive, aFolder);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::CramMD5Hash(const char* decodedChallenge, const char* key,
+ char** result) {
+ NS_ENSURE_ARG_POINTER(decodedChallenge);
+ NS_ENSURE_ARG_POINTER(key);
+
+ unsigned char resultDigest[DIGEST_LENGTH];
+ nsresult rv = MSGCramMD5(decodedChallenge, strlen(decodedChallenge), key,
+ strlen(key), resultDigest);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *result = (char*)malloc(DIGEST_LENGTH);
+ if (*result) memcpy(*result, resultDigest, DIGEST_LENGTH);
+ return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetLoginUsername(nsACString& aLoginUsername) {
+ return GetUsername(aLoginUsername);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetOriginalUsername(nsACString& aUsername) {
+ return GetUsername(aUsername);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetServerKey(nsACString& aServerKey) {
+ return GetKey(aServerKey);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetServerPassword(nsAString& aPassword) {
+ return GetPassword(aPassword);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::RemoveServerConnection(nsIImapProtocol* aProtocol) {
+ return RemoveConnection(aProtocol);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::GetServerShuttingDown(bool* aShuttingDown) {
+ return GetShuttingDown(aShuttingDown);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::ResetServerConnection(const nsACString& aFolderName) {
+ return ResetConnection(aFolderName);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetServerDoingLsub(bool aDoingLsub) {
+ return SetDoingLsub(aDoingLsub);
+}
+
+NS_IMETHODIMP
+nsImapIncomingServer::SetServerUtf8AcceptEnabled(bool enabled) {
+ return SetUtf8AcceptEnabled(enabled);
+}
diff --git a/comm/mailnews/imap/src/nsImapIncomingServer.h b/comm/mailnews/imap/src/nsImapIncomingServer.h
new file mode 100644
index 0000000000..03f4f29ca6
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapIncomingServer.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsImapIncomingServer_h
+#define __nsImapIncomingServer_h
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsImapCore.h"
+#include "nsIImapIncomingServer.h"
+#include "nsMsgIncomingServer.h"
+#include "nsIImapServerSink.h"
+#include "nsIStringBundle.h"
+#include "nsISubscribableServer.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsCOMArray.h"
+#include "nsTArray.h"
+#include "mozilla/Mutex.h"
+
+/* get some implementation from nsMsgIncomingServer */
+class nsImapIncomingServer : public nsMsgIncomingServer,
+ public nsIImapIncomingServer,
+ public nsIImapServerSink,
+ public nsISubscribableServer,
+ public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsImapIncomingServer();
+
+ // overriding nsMsgIncomingServer methods
+ NS_IMETHOD SetKey(const nsACString& aKey)
+ override; // override nsMsgIncomingServer's implementation...
+ NS_IMETHOD GetLocalStoreType(nsACString& type) override;
+ NS_IMETHOD GetLocalDatabaseType(nsACString& type) override;
+
+ NS_DECL_NSIIMAPINCOMINGSERVER
+ NS_DECL_NSIIMAPSERVERSINK
+ NS_DECL_NSISUBSCRIBABLESERVER
+ NS_DECL_NSIURLLISTENER
+
+ NS_IMETHOD PerformBiff(nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD PerformExpand(nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD CloseCachedConnections() override;
+ NS_IMETHOD GetConstructedPrettyName(nsAString& retval) override;
+ NS_IMETHOD GetCanBeDefaultServer(bool* canBeDefaultServer) override;
+ NS_IMETHOD GetCanCompactFoldersOnServer(
+ bool* canCompactFoldersOnServer) override;
+ NS_IMETHOD GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) override;
+ NS_IMETHOD GetCanSearchMessages(bool* canSearchMessages) override;
+ NS_IMETHOD GetOfflineSupportLevel(int32_t* aSupportLevel) override;
+ NS_IMETHOD GeneratePrettyNameForMigration(nsAString& aPrettyName) override;
+ NS_IMETHOD GetSupportsDiskSpace(bool* aSupportsDiskSpace) override;
+ NS_IMETHOD GetCanCreateFoldersOnServer(
+ bool* aCanCreateFoldersOnServer) override;
+ NS_IMETHOD GetCanFileMessagesOnServer(
+ bool* aCanFileMessagesOnServer) override;
+ NS_IMETHOD GetFilterScope(nsMsgSearchScopeValue* filterScope) override;
+ NS_IMETHOD GetSearchScope(nsMsgSearchScopeValue* searchScope) override;
+ NS_IMETHOD GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) override;
+ NS_IMETHOD GetNumIdleConnections(int32_t* aNumIdleConnections);
+ NS_IMETHOD ForgetSessionPassword(bool modifyLogin) override;
+ NS_IMETHOD GetMsgFolderFromURI(nsIMsgFolder* aFolderResource,
+ const nsACString& aURI,
+ nsIMsgFolder** aFolder) override;
+ NS_IMETHOD SetSocketType(int32_t aSocketType) override;
+ NS_IMETHOD VerifyLogon(nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow,
+ nsIURI** aURL) override;
+
+ protected:
+ virtual ~nsImapIncomingServer();
+ nsresult GetFolder(const nsACString& name, nsIMsgFolder** pFolder);
+ virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) override;
+ nsresult ResetFoldersToUnverified(nsIMsgFolder* parentFolder);
+ void GetUnverifiedSubFolders(nsIMsgFolder* parentFolder,
+ nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray);
+ void GetUnverifiedFolders(nsCOMArray<nsIMsgImapMailFolder>& aFolderArray);
+ bool NoDescendentsAreVerified(nsIMsgFolder* parentFolder);
+ bool AllDescendentsAreNoSelect(nsIMsgFolder* parentFolder);
+
+ nsresult GetStringBundle();
+ static nsresult AlertUser(const nsAString& aString, nsIMsgMailNewsUrl* aUrl);
+
+ private:
+ nsresult SubscribeToFolder(const char16_t* aName, bool subscribe);
+ nsresult GetImapConnection(nsIImapUrl* aImapUrl,
+ nsIImapProtocol** aImapConnection);
+ nsresult CreateProtocolInstance(nsIImapProtocol** aImapConnection);
+ nsresult CreateHostSpecificPrefName(const char* prefPrefix,
+ nsAutoCString& prefName);
+
+ nsresult DoomUrlIfChannelHasError(nsIImapUrl* aImapUrl, bool* urlDoomed);
+ bool ConnectionTimeOut(nsIImapProtocol* aImapConnection);
+ nsresult GetFormattedStringFromName(const nsAString& aValue,
+ const char* aName, nsAString& aResult);
+ nsresult GetPrefForServerAttribute(const char* prefSuffix, bool* prefValue);
+ bool CheckSpecialFolder(nsCString& folderUri, uint32_t folderFlag,
+ nsCString& existingUri);
+
+ nsCOMArray<nsIImapProtocol> m_connectionCache;
+
+ /**
+ * All requests waiting for a real connection.
+ * Each URL object holds a reference to the nsIImapMockChannel that
+ * represents the request.
+ */
+ nsCOMArray<nsIImapUrl> m_urlQueue;
+
+ /**
+ * Consumers for the queued urls. The number of elements here should match
+ * that of m_urlQueue. So requests with no consumer should have a nullptr
+ * entry here.
+ */
+ nsTArray<nsISupports*> m_urlConsumers;
+
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+ nsCOMArray<nsIMsgFolder>
+ m_subscribeFolders; // used to keep folder resources around while
+ // subscribe UI is up.
+ nsCOMArray<nsIMsgImapMailFolder>
+ m_foldersToStat; // folders to check for new mail with Status
+ eIMAPCapabilityFlags m_capability;
+ nsCString m_manageMailAccountUrl;
+ bool m_userAuthenticated;
+ bool mDoingSubscribeDialog;
+ bool mDoingLsub;
+ bool m_shuttingDown;
+ bool mUtf8AcceptEnabled;
+
+ mozilla::Mutex mLock;
+ // subscribe dialog stuff
+ nsresult AddFolderToSubscribeDialog(const char* parentUri, const char* uri,
+ const char* folderName);
+ nsCOMPtr<nsISubscribableServer> mInner;
+ nsresult EnsureInner();
+ nsresult ClearInner();
+
+ // Utility function for checking folder existence
+ nsresult GetExistingMsgFolder(const nsACString& aURI,
+ nsACString& folderUriWithNamespace,
+ bool& namespacePrefixAdded,
+ bool caseInsensitive, nsIMsgFolder** aFolder);
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapMailFolder.cpp b/comm/mailnews/imap/src/nsImapMailFolder.cpp
new file mode 100644
index 0000000000..1ec8482383
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapMailFolder.cpp
@@ -0,0 +1,9095 @@
+/* -*- Mode:
+ C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "prmem.h"
+#include "nsImapMailFolder.h"
+#include "nsIImapService.h"
+#include "nsIFile.h"
+#include "nsAnonymousTemporaryFile.h"
+#include "nsIUrlListener.h"
+#include "nsCOMPtr.h"
+#include "nsMsgFolderFlags.h"
+#include "nsISeekableStream.h"
+#include "nsThreadUtils.h"
+#include "nsIImapUrl.h"
+#include "nsImapUtils.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgMailSession.h"
+#include "nsITransactionManager.h"
+#include "nsImapUndoTxn.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsIMsgCopyService.h"
+#include "nsICopyMessageStreamListener.h"
+#include "nsImapStringBundle.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsTextFormatter.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsMsgI18N.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgSearchCustomTerm.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsImapMoveCoalescer.h"
+#include "nsIPrompt.h"
+#include "nsIDocShell.h"
+#include "nsUnicharUtils.h"
+#include "nsIImapFlagAndUidState.h"
+#include "nsIImapHeaderXferInfo.h"
+#include "nsIMessenger.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsIImapMockChannel.h"
+#include "nsIProgressEventSink.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgOfflineImapOperation.h"
+#include "nsImapOfflineSync.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapServerSink.h"
+#include "nsIMsgAccountManager.h"
+#include "nsQuickSort.h"
+#include "nsIImapMockChannel.h"
+#include "nsNetUtil.h"
+#include "nsImapNamespace.h"
+#include "nsIMsgFolderCompactor.h"
+#include "nsMsgMessageFlags.h"
+#include "nsISpamSettings.h"
+#include <time.h>
+#include "nsIMsgMailNewsUrl.h"
+#include "nsEmbedCID.h"
+#include "nsIMsgComposeService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIMsgIdentity.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIExternalProtocolService.h"
+#include "nsCExternalHandlerService.h"
+#include "prprf.h"
+#include "nsAutoSyncManager.h"
+#include "nsIMsgFilterCustomAction.h"
+#include "nsMsgReadStateTxn.h"
+#include "nsStringEnumerator.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsMsgLineBuffer.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsStringStream.h"
+#include "nsIStreamListener.h"
+#include "nsITimer.h"
+#include "nsReadableUtils.h"
+#include "UrlListener.h"
+#include "nsIObserverService.h"
+
+#define NS_PARSEMAILMSGSTATE_CID \
+ { /* 2B79AC51-1459-11d3-8097-006008128C4E */ \
+ 0x2b79ac51, 0x1459, 0x11d3, { \
+ 0x80, 0x97, 0x0, 0x60, 0x8, 0x12, 0x8c, 0x4e \
+ } \
+ }
+static NS_DEFINE_CID(kParseMailMsgStateCID, NS_PARSEMAILMSGSTATE_CID);
+
+#define NS_IIMAPHOSTSESSIONLIST_CID \
+ { \
+ 0x479ce8fc, 0xe725, 0x11d2, { \
+ 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \
+ } \
+ }
+static NS_DEFINE_CID(kCImapHostSessionList, NS_IIMAPHOSTSESSIONLIST_CID);
+
+#define MAILNEWS_CUSTOM_HEADERS "mailnews.customHeaders"
+
+using namespace mozilla;
+
+extern LazyLogModule gAutoSyncLog; // defined in nsAutoSyncManager.cpp
+extern LazyLogModule IMAP; // defined in nsImapProtocol.cpp
+extern LazyLogModule IMAP_CS; // For CONDSTORE, defined in nsImapProtocol.cpp
+extern LazyLogModule FILTERLOGMODULE; // defined in nsMsgFilterService.cpp
+LazyLogModule IMAP_KW("IMAP_KW"); // for logging keyword (tag) processing
+
+/*
+ Copies the contents of srcDir into destDir.
+ destDir will be created if it doesn't exist.
+*/
+
+static nsresult RecursiveCopy(nsIFile* srcDir, nsIFile* destDir) {
+ bool isDir;
+ nsresult rv = srcDir->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) return NS_ERROR_INVALID_ARG;
+
+ bool exists;
+ rv = destDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ rv = destDir->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIDirectoryEnumerator> dirIterator;
+ rv = srcDir->GetDirectoryEntries(getter_AddRefs(dirIterator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(dirIterator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIFile> dirEntry;
+ rv = dirIterator->GetNextFile(getter_AddRefs(dirEntry));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!dirEntry) continue;
+ rv = dirEntry->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isDir) {
+ nsCOMPtr<nsIFile> newChild;
+ rv = destDir->Clone(getter_AddRefs(newChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString leafName;
+ dirEntry->GetLeafName(leafName);
+ newChild->AppendRelativePath(leafName);
+ rv = newChild->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ rv = newChild->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = RecursiveCopy(dirEntry, newChild);
+ } else {
+ rv = dirEntry->CopyTo(destDir, EmptyString());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return rv;
+}
+
+//
+// nsMsgQuota
+//
+NS_IMPL_ISUPPORTS(nsMsgQuota, nsIMsgQuota)
+
+nsMsgQuota::nsMsgQuota(const nsACString& aName, const uint64_t& aUsage,
+ const uint64_t& aLimit)
+ : mName(aName), mUsage(aUsage), mLimit(aLimit) {}
+
+nsMsgQuota::~nsMsgQuota() {}
+
+/**
+ * Note: These quota access function are not called but still must be defined
+ * for the linker.
+ */
+NS_IMETHODIMP nsMsgQuota::GetName(nsACString& aName) {
+ aName = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuota::SetName(const nsACString& aName) {
+ mName = aName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuota::GetUsage(uint64_t* aUsage) {
+ *aUsage = mUsage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuota::SetUsage(uint64_t aUsage) {
+ mUsage = aUsage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuota::GetLimit(uint64_t* aLimit) {
+ *aLimit = mLimit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuota::SetLimit(uint64_t aLimit) {
+ mLimit = aLimit;
+ return NS_OK;
+}
+
+//
+// nsImapMailFolder
+//
+nsImapMailFolder::nsImapMailFolder()
+ : m_initialized(false),
+ m_haveDiscoveredAllFolders(false),
+ m_curMsgUid(0),
+ m_nextMessageByteLength(0),
+ m_urlRunning(false),
+ m_verifiedAsOnlineFolder(false),
+ m_explicitlyVerify(false),
+ m_folderIsNamespace(false),
+ m_folderNeedsSubscribing(false),
+ m_folderNeedsAdded(false),
+ m_folderNeedsACLListed(true),
+ m_performingBiff(false),
+ m_updatingFolder(false),
+ m_applyIncomingFilters(false),
+ m_downloadingFolderForOfflineUse(false),
+ m_filterListRequiresBody(false),
+ m_folderQuotaCommandIssued(false),
+ m_folderQuotaDataIsValid(false) {
+ m_boxFlags = 0;
+ m_uidValidity = kUidUnknown;
+ m_numServerRecentMessages = 0;
+ m_numServerUnseenMessages = 0;
+ m_numServerTotalMessages = 0;
+ m_nextUID = nsMsgKey_None;
+ m_hierarchyDelimiter = kOnlineHierarchySeparatorUnknown;
+ m_folderACL = nullptr;
+ m_aclFlags = 0;
+ m_supportedUserFlags = 0;
+ m_namespace = nullptr;
+ m_pendingPlaybackReq = nullptr;
+}
+
+nsImapMailFolder::~nsImapMailFolder() {
+ delete m_folderACL;
+
+ // cleanup any pending request
+ delete m_pendingPlaybackReq;
+}
+
+NS_IMPL_ADDREF_INHERITED(nsImapMailFolder, nsMsgDBFolder)
+NS_IMPL_RELEASE_INHERITED(nsImapMailFolder, nsMsgDBFolder)
+NS_IMPL_QUERY_HEAD(nsImapMailFolder)
+NS_IMPL_QUERY_BODY(nsIMsgImapMailFolder)
+NS_IMPL_QUERY_BODY(nsICopyMessageListener)
+NS_IMPL_QUERY_BODY(nsIImapMailFolderSink)
+NS_IMPL_QUERY_BODY(nsIImapMessageSink)
+NS_IMPL_QUERY_BODY(nsIUrlListener)
+NS_IMPL_QUERY_BODY(nsIMsgFilterHitNotify)
+NS_IMPL_QUERY_TAIL_INHERITING(nsMsgDBFolder)
+
+nsresult nsImapMailFolder::AddDirectorySeparator(nsIFile* path) {
+ if (mURI.Equals(kImapRootURI)) {
+ // don't concat the full separator with .sbd
+ } else {
+ // see if there's a dir with the same name ending with .sbd
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ path->SetLeafName(leafName);
+ }
+
+ return NS_OK;
+}
+
+static bool nsShouldIgnoreFile(nsString& name) {
+ if (StringEndsWith(name, NS_LITERAL_STRING_FROM_CSTRING(SUMMARY_SUFFIX),
+ nsCaseInsensitiveStringComparator)) {
+ name.SetLength(name.Length() -
+ SUMMARY_SUFFIX_LENGTH); // truncate the string
+ return false;
+ }
+ return true;
+}
+
+nsresult nsImapMailFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ nsImapMailFolder* newFolder = new nsImapMailFolder;
+ newFolder->Init(uri);
+ NS_ADDREF(*folder = newFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::AddSubfolder(const nsAString& aName,
+ nsIMsgFolder** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+
+ int32_t flags = 0;
+ nsresult rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(aName, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uri += escapedName.get();
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = GetChildWithURI(uri, false /*deep*/, true /*case Insensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Ensure the containing dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ folder->GetFlags((uint32_t*)&flags);
+
+ flags |= nsMsgFolderFlags::Mail;
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) {
+ bool setNewFoldersForOffline = false;
+ rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline);
+ if (NS_SUCCEEDED(rv) && setNewFoldersForOffline)
+ flags |= nsMsgFolderFlags::Offline;
+ }
+
+ folder->SetParent(this);
+
+ folder->SetFlags(flags);
+
+ mSubFolders.AppendObject(folder);
+ folder.forget(aChild);
+
+ // New child needs to inherit hierarchyDelimiter.
+ nsCOMPtr<nsIMsgImapMailFolder> imapChild = do_QueryInterface(*aChild);
+ if (imapChild) {
+ imapChild->SetHierarchyDelimiter(m_hierarchyDelimiter);
+ }
+ NotifyFolderAdded(*aChild);
+ return rv;
+}
+
+// Creates a new child nsIMsgFolder locally, with no IMAP traffic.
+nsresult nsImapMailFolder::AddSubfolderWithPath(nsAString& name,
+ nsIFile* dbPath,
+ nsIMsgFolder** child,
+ bool brandNew) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsresult rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+ AppendUTF16toUTF8(name, uri);
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isInbox = isServer && name.LowerCaseEqualsLiteral("inbox");
+
+ // will make sure mSubFolders does not have duplicates because of bogus msf
+ // files.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = GetChildWithURI(uri, false /*deep*/, isInbox /*case Insensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ folder->SetFilePath(dbPath);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder, &rv);
+ mozilla::Unused << imapFolder;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t flags = 0;
+ folder->GetFlags(&flags);
+
+ folder->SetParent(this);
+ flags |= nsMsgFolderFlags::Mail;
+
+ uint32_t pFlags;
+ GetFlags(&pFlags);
+ bool isParentInbox = pFlags & nsMsgFolderFlags::Inbox;
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only set these if these are top level children or parent is inbox
+ if (isInbox)
+ flags |= nsMsgFolderFlags::Inbox;
+ else if (isServer || isParentInbox) {
+ nsMsgImapDeleteModel deleteModel;
+ imapServer->GetDeleteModel(&deleteModel);
+ if (deleteModel == nsMsgImapDeleteModels::MoveToTrash) {
+ nsAutoString trashName;
+ GetTrashFolderName(trashName);
+ if (name.Equals(trashName)) flags |= nsMsgFolderFlags::Trash;
+ }
+ }
+
+ // Make the folder offline if it is newly created and the offline_download
+ // pref is true, unless it's the Trash or Junk folder.
+ if (brandNew &&
+ !(flags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) {
+ bool setNewFoldersForOffline = false;
+ rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline);
+ if (NS_SUCCEEDED(rv) && setNewFoldersForOffline)
+ flags |= nsMsgFolderFlags::Offline;
+ }
+
+ folder->SetFlags(flags);
+
+ if (folder) mSubFolders.AppendObject(folder);
+ folder.forget(child);
+ return NS_OK;
+}
+
+// Create child nsIMsgFolders by scanning the filesystem to find .msf files.
+// No IMAP traffic.
+nsresult nsImapMailFolder::CreateSubFolders(nsIFile* path) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ rv = path->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For each .msf file in the directory...
+ bool hasMore = false;
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIFile> currentFolderPath;
+ rv = directoryEnumerator->GetNextFile(getter_AddRefs(currentFolderPath));
+ if (NS_FAILED(rv) || !currentFolderPath) continue;
+
+ nsAutoString currentFolderNameStr; // online name
+ nsAutoString currentFolderDBNameStr; // possibly munged name
+ currentFolderPath->GetLeafName(currentFolderNameStr);
+ // Skip if not an .msf file.
+ // (NOTE: nsShouldIgnoreFile() strips the trailing ".msf" here)
+ if (nsShouldIgnoreFile(currentFolderNameStr)) continue;
+
+ // OK, here we need to get the online name from the folder cache if we can.
+ // If we can, use that to create the sub-folder
+ nsCOMPtr<nsIFile> curFolder =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> dbFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbFile->InitWithFile(currentFolderPath);
+ curFolder->InitWithFile(currentFolderPath);
+ // don't strip off the .msf in currentFolderPath.
+ currentFolderPath->SetLeafName(currentFolderNameStr);
+ currentFolderDBNameStr = currentFolderNameStr;
+ nsAutoString utfLeafName = currentFolderNameStr;
+
+ if (curFolder) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ rv = GetFolderCacheElemFromFile(dbFile, getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(rv) && cacheElement) {
+ nsCString onlineFullUtfName;
+
+ uint32_t folderFlags;
+ rv = cacheElement->GetCachedUInt32("flags", &folderFlags);
+ if (NS_SUCCEEDED(rv) &&
+ folderFlags & nsMsgFolderFlags::Virtual) // ignore virtual folders
+ continue;
+ int32_t hierarchyDelimiter;
+ rv = cacheElement->GetCachedInt32("hierDelim", &hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) &&
+ hierarchyDelimiter == kOnlineHierarchySeparatorUnknown) {
+ currentFolderPath->Remove(false);
+ continue; // blow away .msf files for folders with unknown delimiter.
+ }
+ rv = cacheElement->GetCachedString("onlineName", onlineFullUtfName);
+ if (NS_SUCCEEDED(rv) && !onlineFullUtfName.IsEmpty()) {
+ CopyFolderNameToUTF16(onlineFullUtfName, currentFolderNameStr);
+ char delimiter = 0;
+ GetHierarchyDelimiter(&delimiter);
+ int32_t leafPos = currentFolderNameStr.RFindChar(delimiter);
+ if (leafPos > 0) currentFolderNameStr.Cut(0, leafPos + 1);
+
+ // Take the full online name, and determine the leaf name.
+ CopyUTF8toUTF16(onlineFullUtfName, utfLeafName);
+ leafPos = utfLeafName.RFindChar(delimiter);
+ if (leafPos > 0) utfLeafName.Cut(0, leafPos + 1);
+ }
+ }
+ }
+ // make the imap folder remember the file spec it was created with.
+ nsCOMPtr<nsIFile> msfFilePath =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msfFilePath->InitWithFile(currentFolderPath);
+ if (NS_SUCCEEDED(rv) && msfFilePath) {
+ // leaf name is the db name w/o .msf (nsShouldIgnoreFile strips it off)
+ // so this trims the .msf off the file spec.
+ msfFilePath->SetLeafName(currentFolderDBNameStr);
+ }
+ // Use the name as the uri for the folder.
+ nsCOMPtr<nsIMsgFolder> child;
+ AddSubfolderWithPath(utfLeafName, msfFilePath, getter_AddRefs(child));
+ if (child) {
+ // use the unicode name as the "pretty" name. Set it so it won't be
+ // automatically computed from the URI.
+ if (!currentFolderNameStr.IsEmpty())
+ child->SetPrettyName(currentFolderNameStr);
+ child->SetMsgDatabase(nullptr);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetSubFolders(
+ nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ bool isServer;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_initialized) {
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ // host directory does not need .sbd tacked on
+ if (!isServer) {
+ rv = AddDirectorySeparator(pathFile);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ m_initialized = true; // need to set this here to avoid infinite recursion
+ // from CreateSubfolders.
+ // we have to treat the root folder specially, because it's name
+ // doesn't end with .sbd
+
+ int32_t newFlags = nsMsgFolderFlags::Mail;
+ bool isDirectory = false;
+ pathFile->IsDirectory(&isDirectory);
+ if (isDirectory) {
+ newFlags |= (nsMsgFolderFlags::Directory | nsMsgFolderFlags::Elided);
+ if (!mIsServer) SetFlag(newFlags);
+ rv = CreateSubFolders(pathFile);
+ }
+ if (isServer) {
+ nsCOMPtr<nsIMsgFolder> inboxFolder;
+
+ GetFolderWithFlags(nsMsgFolderFlags::Inbox, getter_AddRefs(inboxFolder));
+ if (!inboxFolder) {
+ // create an inbox if we don't have one.
+ CreateClientSubfolderInfo("INBOX"_ns, kOnlineHierarchySeparatorUnknown,
+ 0, true);
+ }
+ }
+
+ // Force initialisation recursively.
+ for (nsIMsgFolder* f : mSubFolders) {
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ rv = f->GetSubFolders(dummy);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+
+ UpdateSummaryTotals(false);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ return nsMsgDBFolder::GetSubFolders(folders);
+}
+
+// Makes sure the database is open and exists. If the database is valid then
+// returns NS_OK. Otherwise returns a failure error value.
+nsresult nsImapMailFolder::GetDatabase() {
+ nsresult rv = NS_OK;
+ if (!mDatabase) {
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the database, blowing it away if it needs to be rebuilt
+ rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase));
+ if (NS_FAILED(rv))
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // UpdateNewMessages/UpdateSummaryTotals can null mDatabase, so we save a
+ // local copy
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ UpdateNewMessages();
+ if (mAddListener) database->AddListener(this);
+ UpdateSummaryTotals(true);
+ mDatabase = database;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::UpdateFolder(nsIMsgWindow* inMsgWindow) {
+ return UpdateFolderWithListener(inMsgWindow, nullptr);
+}
+
+NS_IMETHODIMP nsImapMailFolder::UpdateFolderWithListener(
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener) {
+ nsresult rv;
+ // If this is the inbox, filters will be applied. Otherwise, we test the
+ // inherited folder property "applyIncomingFilters" (which defaults to empty).
+ // If this inherited property has the string value "true", we will apply
+ // filters even if this is not the inbox folder.
+ nsCString applyIncomingFilters;
+ GetInheritedStringProperty("applyIncomingFilters", applyIncomingFilters);
+ m_applyIncomingFilters = applyIncomingFilters.EqualsLiteral("true");
+
+ nsString folderName;
+ GetPrettyName(folderName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Imap) nsImapMailFolder::UpdateFolderWithListener() on folder '%s'",
+ NS_ConvertUTF16toUTF8(folderName).get()));
+ if (mFlags & nsMsgFolderFlags::Inbox || m_applyIncomingFilters) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Preparing filter run on folder '%s'",
+ NS_ConvertUTF16toUTF8(folderName).get()));
+
+ if (!m_filterList) {
+ rv = GetFilterList(aMsgWindow, getter_AddRefs(m_filterList));
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Imap) Loading of filter list failed"));
+ }
+ }
+
+ // if there's no msg window, but someone is updating the inbox, we're
+ // doing something biff-like, and may download headers, so make biff notify.
+ if (!aMsgWindow && mFlags & nsMsgFolderFlags::Inbox)
+ SetPerformingBiff(true);
+ }
+
+ if (m_filterList) {
+ nsCString listId;
+ m_filterList->GetListId(listId);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Preparing filter list %s", listId.get()));
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool canFileMessagesOnServer = true;
+ rv = server->GetCanFileMessagesOnServer(&canFileMessagesOnServer);
+ // the mdn filter is for filing return receipts into the sent folder
+ // some servers (like AOL mail servers)
+ // can't file to the sent folder, so we don't add the filter for those
+ // servers
+ if (canFileMessagesOnServer) {
+ rv = server->ConfigureTemporaryFilters(m_filterList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If a body filter is enabled for an offline folder, delay the filter
+ // application until after message has been downloaded.
+ m_filterListRequiresBody = false;
+
+ if (mFlags & nsMsgFolderFlags::Offline) {
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ uint32_t filterCount = 0;
+ m_filterList->GetFilterCount(&filterCount);
+ for (uint32_t index = 0; index < filterCount && !m_filterListRequiresBody;
+ ++index) {
+ nsCOMPtr<nsIMsgFilter> filter;
+ m_filterList->GetFilterAt(index, getter_AddRefs(filter));
+ if (!filter) continue;
+ nsMsgFilterTypeType filterType;
+ filter->GetFilterType(&filterType);
+ if (!(filterType & nsMsgFilterType::Incoming)) continue;
+ bool enabled = false;
+ filter->GetEnabled(&enabled);
+ if (!enabled) continue;
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ filter->GetSearchTerms(searchTerms);
+ for (nsIMsgSearchTerm* term : searchTerms) {
+ nsMsgSearchAttribValue attrib;
+ rv = term->GetAttrib(&attrib);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (attrib == nsMsgSearchAttrib::Body)
+ m_filterListRequiresBody = true;
+ else if (attrib == nsMsgSearchAttrib::Custom) {
+ nsAutoCString customId;
+ rv = term->GetCustomId(customId);
+ nsCOMPtr<nsIMsgSearchCustomTerm> customTerm;
+ if (NS_SUCCEEDED(rv) && filterService)
+ rv = filterService->GetCustomTerm(customId,
+ getter_AddRefs(customTerm));
+ bool needsBody = false;
+ if (NS_SUCCEEDED(rv) && customTerm)
+ rv = customTerm->GetNeedsBody(&needsBody);
+ if (NS_SUCCEEDED(rv) && needsBody) m_filterListRequiresBody = true;
+ }
+ if (m_filterListRequiresBody) {
+ break;
+ }
+ }
+
+ // Also check if filter actions need the body, as this
+ // is supported in custom actions.
+ uint32_t numActions = 0;
+ filter->GetActionCount(&numActions);
+ for (uint32_t actionIndex = 0;
+ actionIndex < numActions && !m_filterListRequiresBody;
+ actionIndex++) {
+ nsCOMPtr<nsIMsgRuleAction> action;
+ rv = filter->GetActionAt(actionIndex, getter_AddRefs(action));
+ if (NS_FAILED(rv) || !action) continue;
+
+ nsCOMPtr<nsIMsgFilterCustomAction> customAction;
+ rv = action->GetCustomAction(getter_AddRefs(customAction));
+ if (NS_FAILED(rv) || !customAction) continue;
+
+ bool needsBody = false;
+ customAction->GetNeedsBody(&needsBody);
+ if (needsBody) m_filterListRequiresBody = true;
+ }
+ }
+ }
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Filters require the message body: %s",
+ (m_filterListRequiresBody ? "true" : "false")));
+ }
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+ if (NS_SUCCEEDED(rv) && isServer) {
+ if (!m_haveDiscoveredAllFolders) {
+ bool hasSubFolders = false;
+ GetHasSubFolders(&hasSubFolders);
+ if (!hasSubFolders) {
+ rv = CreateClientSubfolderInfo(
+ "Inbox"_ns, kOnlineHierarchySeparatorUnknown, 0, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ m_haveDiscoveredAllFolders = true;
+ }
+ }
+
+ rv = GetDatabase();
+ if (NS_FAILED(rv)) {
+ ThrowAlertMsg("errorGettingDB", aMsgWindow);
+ return rv;
+ }
+
+ bool hasOfflineEvents = false;
+ GetFlag(nsMsgFolderFlags::OfflineEvents, &hasOfflineEvents);
+
+ if (!WeAreOffline()) {
+ if (hasOfflineEvents) {
+ // hold a reference to the offline sync object. If ProcessNextOperation
+ // runs a url, a reference will be added to it. Otherwise, it will get
+ // destroyed when the refptr goes out of scope.
+ RefPtr<nsImapOfflineSync> goOnline = new nsImapOfflineSync();
+ goOnline->Init(aMsgWindow, this, this, false);
+ if (goOnline) {
+ m_urlListener = aUrlListener;
+ return goOnline->ProcessNextOperation();
+ }
+ }
+ }
+
+ // Check it we're password protecting the local store.
+ if (!PromptForMasterPasswordIfNecessary()) return NS_ERROR_FAILURE;
+
+ bool canOpenThisFolder = true;
+ GetCanOpenFolder(&canOpenThisFolder);
+ // Don't run select if we can't select the folder...
+ if (!m_urlRunning && canOpenThisFolder && !isServer) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Do a discovery in its own url if needed. Do before SELECT url. */
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionList, &rv);
+ if (NS_SUCCEEDED(rv) && hostSession) {
+ bool foundMailboxesAlready = false;
+ nsCString serverKey;
+ GetServerKey(serverKey);
+ hostSession->GetHaveWeEverDiscoveredFoldersForHost(serverKey.get(),
+ foundMailboxesAlready);
+ if (!foundMailboxesAlready) {
+ bool discoveryInProgress = false;
+ // See if discovery in progress and not yet finished.
+ hostSession->GetDiscoveryForHostInProgress(serverKey.get(),
+ discoveryInProgress);
+ if (!discoveryInProgress) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ rv = imapService->DiscoverAllFolders(rootFolder, this, aMsgWindow);
+ if (NS_SUCCEEDED(rv))
+ hostSession->SetDiscoveryForHostInProgress(serverKey.get(), true);
+ }
+ }
+ }
+ }
+
+ nsCOMPtr<nsIURI> url;
+ rv = imapService->SelectFolder(this, m_urlListener, aMsgWindow,
+ getter_AddRefs(url));
+ if (NS_SUCCEEDED(rv)) {
+ m_urlRunning = true;
+ m_updatingFolder = true;
+ }
+ if (url) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(url, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mailnewsUrl->RegisterListener(this);
+ m_urlListener = aUrlListener;
+ }
+
+ // Allow IMAP folder auto-compact to occur when online or offline.
+ if (aMsgWindow) AutoCompact(aMsgWindow);
+
+ if (rv == NS_MSG_ERROR_OFFLINE || rv == NS_BINDING_ABORTED) {
+ rv = NS_OK;
+ NotifyFolderEvent(kFolderLoaded);
+ }
+ } else {
+ // Tell the front end that the folder is loaded if we're not going to
+ // actually run a url.
+ if (!m_updatingFolder) // if we're already running an update url, we'll let
+ // that one send the folder loaded
+ NotifyFolderEvent(kFolderLoaded);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) {
+ if (folderName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsresult rv;
+ nsAutoString trashName;
+ GetTrashFolderName(trashName);
+ if (folderName.Equals(trashName)) // Trash , a special folder
+ {
+ ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ }
+ if (mIsServer &&
+ folderName.LowerCaseEqualsLiteral("inbox")) // Inbox, a special folder
+ {
+ ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ }
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> url;
+ return imapService->CreateFolder(this, folderName, this, getter_AddRefs(url));
+}
+
+NS_IMETHODIMP nsImapMailFolder::CreateClientSubfolderInfo(
+ const nsACString& folderName, char hierarchyDelimiter, int32_t flags,
+ bool suppressNotification) {
+ nsresult rv = NS_OK;
+
+ // Get a directory based on our current path.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ NS_ConvertUTF8toUTF16 leafName(folderName);
+ nsAutoString folderNameStr;
+ nsAutoString parentName = leafName;
+ // use RFind, because folder can start with a delimiter and
+ // not be a leaf folder.
+ int32_t folderStart = leafName.RFindChar('/');
+ if (folderStart > 0) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder;
+ nsAutoCString uri(mURI);
+ leafName.Assign(Substring(parentName, folderStart + 1));
+ parentName.SetLength(folderStart);
+
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uri.Append('/');
+ uri.Append(NS_ConvertUTF16toUTF8(parentName));
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapFolder = do_QueryInterface(folder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString leafnameC;
+ CopyUTF16toUTF8(leafName, leafnameC);
+ return imapFolder->CreateClientSubfolderInfo(leafnameC, hierarchyDelimiter,
+ flags, suppressNotification);
+ }
+
+ // if we get here, it's really a leaf, and "this" is the parent.
+ folderNameStr = leafName;
+
+ // Create an empty database for this mail folder, set its name from the user
+ nsCOMPtr<nsIMsgDatabase> mailDBFactory;
+ nsCOMPtr<nsIMsgFolder> child;
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> unusedDB;
+ nsCOMPtr<nsIFile> dbFile;
+
+ // warning, path will be changed
+ rv = CreateFileForDB(folderNameStr, path, getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now let's create the actual new folder
+ rv = AddSubfolderWithPath(folderNameStr, dbFile, getter_AddRefs(child), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(dbFile, child, true, true,
+ getter_AddRefs(unusedDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) rv = NS_OK;
+
+ if (NS_SUCCEEDED(rv) && unusedDB) {
+ // need to set the folder name
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(child, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString onlineName(m_onlineFolderName);
+ if (!onlineName.IsEmpty()) onlineName.Append(hierarchyDelimiter);
+ onlineName.Append(NS_ConvertUTF16toUTF8(folderNameStr));
+ imapFolder->SetVerifiedAsOnlineFolder(true);
+ imapFolder->SetOnlineName(onlineName);
+ imapFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ imapFolder->SetBoxFlags(flags);
+
+ // Now that the child is created and the boxflags are set we can be sure
+ // all special folder flags are known. The child may get its flags already
+ // in AddSubfolderWithPath if they were in FolderCache, but that's
+ // not always the case.
+ uint32_t flags = 0;
+ child->GetFlags(&flags);
+
+ // Set the offline use flag for the newly created folder if the
+ // offline_download preference is true, unless it's the Trash or Junk
+ // folder.
+ if (!(flags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool setNewFoldersForOffline = false;
+ rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline);
+ if (NS_SUCCEEDED(rv) && setNewFoldersForOffline)
+ flags |= nsMsgFolderFlags::Offline;
+ } else {
+ flags &= ~nsMsgFolderFlags::Offline; // clear offline flag if set
+ }
+
+ flags |= nsMsgFolderFlags::Elided;
+ child->SetFlags(flags);
+
+ nsString unicodeName;
+ rv = CopyFolderNameToUTF16(nsCString(folderName), unicodeName);
+ if (NS_SUCCEEDED(rv)) child->SetPrettyName(unicodeName);
+
+ // store the online name as the mailbox name in the db folder info
+ // I don't think anyone uses the mailbox name, so we'll use it
+ // to restore the online name when blowing away an imap db.
+ if (folderInfo)
+ folderInfo->SetMailboxName(NS_ConvertUTF8toUTF16(onlineName));
+ }
+
+ unusedDB->SetSummaryValid(true);
+ unusedDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ unusedDB->Close(true);
+ // don't want to hold onto this newly created db.
+ child->SetMsgDatabase(nullptr);
+ }
+
+ if (!suppressNotification) {
+ if (NS_SUCCEEDED(rv) && child) {
+ NotifyFolderAdded(child);
+ child->NotifyFolderEvent(kFolderCreateCompleted);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(child);
+ } else {
+ NotifyFolderEvent(kFolderCreateFailed);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::List() {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->ListFolder(this, this);
+}
+
+NS_IMETHODIMP nsImapMailFolder::RemoveLocalSelf() {
+ // Kill the local folder and its storage.
+ return nsMsgDBFolder::DeleteSelf(nullptr);
+}
+
+NS_IMETHODIMP nsImapMailFolder::CreateStorageIfMissing(
+ nsIUrlListener* urlListener) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ GetParent(getter_AddRefs(msgParent));
+
+ // parent is probably not set because *this* was probably created by rdf
+ // and not by folder discovery. So, we have to compute the parent.
+ if (!msgParent) {
+ nsAutoCString folderName(mURI);
+
+ int32_t leafPos = folderName.RFindChar('/');
+ nsAutoCString parentName(folderName);
+
+ if (leafPos > 0) {
+ // If there is a hierarchy, there is a parent.
+ // Don't strip off slash if it's the first character
+ parentName.SetLength(leafPos);
+ rv = GetOrCreateFolder(parentName, getter_AddRefs(msgParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ if (msgParent) {
+ nsString folderName;
+ GetName(folderName);
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapService->EnsureFolderExists(msgParent, folderName, nullptr,
+ urlListener);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetVerifiedAsOnlineFolder(
+ bool* aVerifiedAsOnlineFolder) {
+ NS_ENSURE_ARG_POINTER(aVerifiedAsOnlineFolder);
+ *aVerifiedAsOnlineFolder = m_verifiedAsOnlineFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetVerifiedAsOnlineFolder(
+ bool aVerifiedAsOnlineFolder) {
+ m_verifiedAsOnlineFolder = aVerifiedAsOnlineFolder;
+ // mark ancestors as verified as well
+ if (aVerifiedAsOnlineFolder) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ do {
+ GetParent(getter_AddRefs(parent));
+ if (parent) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapParent = do_QueryInterface(parent);
+ if (imapParent) {
+ bool verifiedOnline;
+ imapParent->GetVerifiedAsOnlineFolder(&verifiedOnline);
+ if (verifiedOnline) break;
+ imapParent->SetVerifiedAsOnlineFolder(true);
+ }
+ }
+ } while (parent);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetOnlineDelimiter(char* onlineDelimiter) {
+ return GetHierarchyDelimiter(onlineDelimiter);
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetHierarchyDelimiter(
+ char aHierarchyDelimiter) {
+ m_hierarchyDelimiter = aHierarchyDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetHierarchyDelimiter(
+ char* aHierarchyDelimiter) {
+ NS_ENSURE_ARG_POINTER(aHierarchyDelimiter);
+ if (mIsServer) {
+ // if it's the root folder, we don't know the delimiter. So look at the
+ // first child.
+ int32_t count = mSubFolders.Count();
+ if (count > 0) {
+ nsCOMPtr<nsIMsgImapMailFolder> childFolder(
+ do_QueryInterface(mSubFolders[0]));
+ if (childFolder) {
+ nsresult rv = childFolder->GetHierarchyDelimiter(aHierarchyDelimiter);
+ // some code uses m_hierarchyDelimiter directly, so we should set it.
+ m_hierarchyDelimiter = *aHierarchyDelimiter;
+ return rv;
+ }
+ }
+ }
+ ReadDBFolderInfo(false); // update cache first.
+ *aHierarchyDelimiter = m_hierarchyDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetBoxFlags(int32_t aBoxFlags) {
+ ReadDBFolderInfo(false);
+
+ m_boxFlags = aBoxFlags;
+ uint32_t newFlags = mFlags;
+
+ newFlags |= nsMsgFolderFlags::ImapBox;
+
+ if (m_boxFlags & kNoinferiors)
+ newFlags |= nsMsgFolderFlags::ImapNoinferiors;
+ else
+ newFlags &= ~nsMsgFolderFlags::ImapNoinferiors;
+ if (m_boxFlags & kNoselect)
+ newFlags |= nsMsgFolderFlags::ImapNoselect;
+ else
+ newFlags &= ~nsMsgFolderFlags::ImapNoselect;
+ if (m_boxFlags & kPublicMailbox)
+ newFlags |= nsMsgFolderFlags::ImapPublic;
+ else
+ newFlags &= ~nsMsgFolderFlags::ImapPublic;
+ if (m_boxFlags & kOtherUsersMailbox)
+ newFlags |= nsMsgFolderFlags::ImapOtherUser;
+ else
+ newFlags &= ~nsMsgFolderFlags::ImapOtherUser;
+ if (m_boxFlags & kPersonalMailbox)
+ newFlags |= nsMsgFolderFlags::ImapPersonal;
+ else
+ newFlags &= ~nsMsgFolderFlags::ImapPersonal;
+
+ // The following are all flags returned by XLIST.
+ // nsImapIncomingServer::DiscoveryDone checks for these folders.
+ if (m_boxFlags & kImapDrafts) newFlags |= nsMsgFolderFlags::Drafts;
+
+ if (m_boxFlags & kImapSpam) newFlags |= nsMsgFolderFlags::Junk;
+
+ if (m_boxFlags & kImapSent) newFlags |= nsMsgFolderFlags::SentMail;
+
+ if (m_boxFlags & kImapInbox) newFlags |= nsMsgFolderFlags::Inbox;
+
+ if (m_boxFlags & kImapXListTrash) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsMsgImapDeleteModel deleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ (void)GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) imapServer->GetDeleteModel(&deleteModel);
+ if (deleteModel == nsMsgImapDeleteModels::MoveToTrash)
+ newFlags |= nsMsgFolderFlags::Trash;
+ }
+ // Treat the GMail all mail folder as the archive folder.
+ if (m_boxFlags & (kImapAllMail | kImapArchive))
+ newFlags |= nsMsgFolderFlags::Archive;
+
+ SetFlags(newFlags);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetBoxFlags(int32_t* aBoxFlags) {
+ NS_ENSURE_ARG_POINTER(aBoxFlags);
+ *aBoxFlags = m_boxFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetExplicitlyVerify(bool* aExplicitlyVerify) {
+ NS_ENSURE_ARG_POINTER(aExplicitlyVerify);
+ *aExplicitlyVerify = m_explicitlyVerify;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetExplicitlyVerify(bool aExplicitlyVerify) {
+ m_explicitlyVerify = aExplicitlyVerify;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetNoSelect(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ return GetFlag(nsMsgFolderFlags::ImapNoselect, aResult);
+}
+
+NS_IMETHODIMP nsImapMailFolder::ApplyRetentionSettings() {
+ int32_t numDaysToKeepOfflineMsgs = -1;
+
+ // Check if we've limited the offline storage by age.
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapServer->GetAutoSyncMaxAgeDays(&numDaysToKeepOfflineMsgs);
+
+ nsCOMPtr<nsIMsgDatabase> holdDBOpen;
+ if (numDaysToKeepOfflineMsgs > 0) {
+ bool dbWasCached = mDatabase != nullptr;
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ rv = mDatabase->EnumerateMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool hasMore = false;
+
+ PRTime cutOffDay =
+ MsgConvertAgeInDaysToCutoffDate(numDaysToKeepOfflineMsgs);
+
+ // so now cutOffDay is the PRTime cut-off point. Any offline msg with
+ // a date less than that will get marked for pending removal.
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = hdrs->GetNext(getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t msgFlags;
+ PRTime msgDate;
+ header->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::Offline) {
+ header->GetDate(&msgDate);
+ MarkPendingRemoval(header, msgDate < cutOffDay);
+ // I'm horribly tempted to break out of the loop if we've found
+ // a message after the cut-off date, because messages will most likely
+ // be in date order in the db, but there are always edge cases.
+ }
+ }
+ if (!dbWasCached) {
+ holdDBOpen = mDatabase;
+ mDatabase = nullptr;
+ }
+ }
+ return nsMsgDBFolder::ApplyRetentionSettings();
+}
+
+/**
+ * The listener will get called when both the online expunge and the offline
+ * store compaction are finished (if the latter is needed).
+ */
+nsresult nsImapMailFolder::ExpungeAndCompact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ GetDatabase();
+ // now's a good time to apply the retention settings. If we do delete any
+ // messages, the expunge is going to have to wait until the delete to
+ // finish before it can run, but the multiple-connection protection code
+ // should handle that.
+ if (mDatabase) ApplyRetentionSettings();
+
+ // Things to hold in existence until both expunge and compact are complete.
+ RefPtr<nsImapMailFolder> folder = this;
+ nsCOMPtr<nsIUrlListener> finalListener = aListener;
+ nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow;
+
+ // doCompact implements OnStopRunningUrl()
+ // NOTE: The caller will be expecting that their listener will be invoked, so
+ // we need to be careful that all execution paths in here do that. We either
+ // call it directly, or pass it along to the foldercompactor to call.
+ auto doCompact = [folder, finalListener, msgWindow](
+ nsIURI* url, nsresult status) -> nsresult {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_FAILED(rv)) {
+ if (finalListener) {
+ return finalListener->OnStopRunningUrl(nullptr, rv);
+ }
+ return rv;
+ }
+ bool storeSupportsCompaction;
+ msgStore->GetSupportsCompaction(&storeSupportsCompaction);
+ if (storeSupportsCompaction && folder->mFlags & nsMsgFolderFlags::Offline) {
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor =
+ do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv);
+ if (NS_FAILED(rv)) {
+ if (finalListener) {
+ return finalListener->OnStopRunningUrl(nullptr, rv);
+ }
+ return rv;
+ }
+ return folderCompactor->CompactFolders({folder}, finalListener,
+ msgWindow);
+ }
+ // Not going to run a compaction, so signal that we're all done.
+ if (finalListener) {
+ return finalListener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ return NS_OK;
+ };
+
+ if (WeAreOffline()) {
+ // Can't run an expunge. Kick off the next stage (compact) immediately.
+ return doCompact(nullptr, NS_OK);
+ }
+
+ // Run the expunge, followed by the compaction.
+ RefPtr<UrlListener> expungeListener = new UrlListener();
+ expungeListener->mStopFn = doCompact;
+ return Expunge(expungeListener, aMsgWindow);
+}
+
+// IMAP compact implies an Expunge.
+NS_IMETHODIMP nsImapMailFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ return ExpungeAndCompact(aListener, aMsgWindow);
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::NotifyCompactCompleted() { return NS_OK; }
+
+NS_IMETHODIMP nsImapMailFolder::MarkPendingRemoval(nsIMsgDBHdr* aHdr,
+ bool aMark) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ uint32_t offlineMessageSize;
+ aHdr->GetOfflineMessageSize(&offlineMessageSize);
+ aHdr->SetStringProperty("pendingRemoval", aMark ? "1"_ns : ""_ns);
+ if (!aMark) return NS_OK;
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return dbFolderInfo->ChangeExpungedBytes(offlineMessageSize);
+}
+
+NS_IMETHODIMP nsImapMailFolder::Expunge(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return imapService->Expunge(this, aListener, aMsgWindow);
+}
+
+NS_IMETHODIMP nsImapMailFolder::CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor =
+ do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow;
+
+ // Set up a callable which will start the compaction phase.
+ auto doCompact = [folderCompactor, rootFolder,
+ listener = nsCOMPtr<nsIUrlListener>(aListener),
+ msgWindow]() {
+ // Collect all the compactable folders.
+ nsTArray<RefPtr<nsIMsgFolder>> foldersToCompact;
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rootFolder->GetDescendants(allDescendants);
+ for (auto folder : allDescendants) {
+ uint32_t flags;
+ folder->GetFlags(&flags);
+ if (flags &
+ (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect)) {
+ continue;
+ }
+ // Folder can be compacted?
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ folder->GetMsgStore(getter_AddRefs(msgStore));
+ if (!msgStore) {
+ continue;
+ }
+ bool storeSupportsCompaction;
+ msgStore->GetSupportsCompaction(&storeSupportsCompaction);
+ if (storeSupportsCompaction) {
+ foldersToCompact.AppendElement(folder);
+ }
+ }
+ nsresult rv =
+ folderCompactor->CompactFolders(foldersToCompact, listener, msgWindow);
+ if (NS_FAILED(rv) && listener) {
+ // Make sure the listener hears about the failure.
+ listener->OnStopRunningUrl(nullptr, rv);
+ }
+ };
+
+ // Collect all the expungeable folders.
+ nsTArray<RefPtr<nsIMsgImapMailFolder>> foldersToExpunge;
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rootFolder->GetDescendants(allDescendants);
+ for (auto folder : allDescendants) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder));
+ if (!imapFolder) {
+ continue;
+ }
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ if (!(folderFlags &
+ (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect))) {
+ foldersToExpunge.AppendElement(imapFolder);
+ }
+ }
+
+ if (!WeAreOffline() && !foldersToExpunge.IsEmpty()) {
+ // Kick off expunge on all the folders (the IMAP protocol will handle
+ // queuing them up as needed).
+
+ // A listener to track the completed expunges.
+ RefPtr<UrlListener> l = new UrlListener();
+ l->mStopFn = [expungeCount = foldersToExpunge.Length(), doCompact](
+ nsIURI* url, nsresult status) mutable -> nsresult {
+ // NOTE: we're ignoring expunge result code - nothing much we can do
+ // here to recover, so just plough on.
+ --expungeCount;
+ if (expungeCount == 0) {
+ // All the expunges are done so start compacting.
+ doCompact();
+ }
+ return NS_OK;
+ };
+ // Go!
+ for (auto& imapFolder : foldersToExpunge) {
+ rv = imapFolder->Expunge(l, aMsgWindow);
+ if (NS_FAILED(rv)) {
+ // Make sure expungeCount is kept in sync!
+ l->OnStopRunningUrl(nullptr, rv);
+ }
+ }
+ } else {
+ // No expunging. Start the compaction immediately.
+ doCompact();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::UpdateStatus(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = imapService->UpdateFolderStatus(this, aListener, getter_AddRefs(uri));
+ if (uri && !aMsgWindow) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(uri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // if no msg window, we won't put up error messages (this is almost
+ // certainly a biff-inspired status)
+ mailNewsUrl->SetSuppressErrorMsgs(true);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::EmptyTrash(nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ nsresult rv = GetTrashFolder(getter_AddRefs(trashFolder));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // if we are emptying trash on exit and we are an aol server then don't
+ // perform this operation because it's causing a hang that we haven't been
+ // able to figure out yet this is an rtm fix and we'll look for the right
+ // solution post rtm.
+ bool empytingOnExit = false;
+ accountManager->GetEmptyTrashInProgress(&empytingOnExit);
+ if (empytingOnExit) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) {
+ bool isAOLServer = false;
+ imapServer->GetIsAOLServer(&isAOLServer);
+ if (isAOLServer)
+ return NS_ERROR_FAILURE; // we will not be performing an empty
+ // trash....
+ } // if we fetched an imap server
+ } // if emptying trash on exit which is done through the account manager.
+
+ if (WeAreOffline()) {
+ nsCOMPtr<nsIMsgDatabase> trashDB;
+ rv = trashFolder->GetMsgDatabase(getter_AddRefs(trashDB));
+ if (trashDB) {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(trashDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey fakeKey;
+ opsDb->GetNextFakeOfflineMsgKey(&fakeKey);
+
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ rv = opsDb->GetOfflineOpForKey(fakeKey, true, getter_AddRefs(op));
+ trashFolder->SetFlag(nsMsgFolderFlags::OfflineEvents);
+ op->SetOperation(nsIMsgOfflineImapOperation::kDeleteAllMsgs);
+ }
+ return rv;
+ }
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aListener)
+ rv = imapService->DeleteAllMessages(trashFolder, aListener);
+ else {
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(trashFolder);
+ rv = imapService->DeleteAllMessages(trashFolder, urlListener);
+ }
+ // Return an error if this failed. We want the empty trash on exit code
+ // to know if this fails so that it doesn't block waiting for empty trash to
+ // finish.
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Delete any subfolders under Trash.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = trashFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (!subFolders.IsEmpty()) {
+ RefPtr<nsIMsgFolder> f = subFolders.PopLastElement();
+ rv = trashFolder->PropagateDelete(f, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ rv = trashFolder->GetDBTransferInfo(getter_AddRefs(transferInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Bulk-delete all the messages by deleting the msf file and storage.
+ // This is a little kludgy.
+ rv = trashFolder->DeleteStorage();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (transferInfo) trashFolder->SetDBTransferInfo(transferInfo);
+ trashFolder->SetSizeOnDisk(0);
+
+ // The trash folder has effectively been deleted.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderDeleted(trashFolder);
+
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::DeleteStorage() {
+ nsresult rv = nsMsgDBFolder::DeleteStorage();
+
+ // Should notify nsIMsgFolderListeners about the folder getting deleted?
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::Rename(const nsAString& newName,
+ nsIMsgWindow* msgWindow) {
+ if (mFlags & nsMsgFolderFlags::Virtual)
+ return nsMsgDBFolder::Rename(newName, msgWindow);
+ nsresult rv;
+ nsAutoString newNameStr(newName);
+ if (newNameStr.FindChar(m_hierarchyDelimiter, 0) != kNotFound) {
+ nsCOMPtr<nsIDocShell> docShell;
+ if (msgWindow) msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle) {
+ AutoTArray<nsString, 1> formatStrings;
+ formatStrings.AppendElement()->Append(m_hierarchyDelimiter);
+ nsString alertString;
+ rv = bundle->FormatStringFromName("imapSpecialChar2", formatStrings,
+ alertString);
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ // setting up the dialog title
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString dialogTitle;
+ nsString accountName;
+ rv = server->GetPrettyName(accountName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoTArray<nsString, 1> titleParams = {accountName};
+ rv = bundle->FormatStringFromName("imapAlertDialogTitle", titleParams,
+ dialogTitle);
+
+ if (dialog && !alertString.IsEmpty())
+ dialog->Alert(dialogTitle.get(), alertString.get());
+ }
+ }
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIImapIncomingServer> incomingImapServer;
+ GetImapIncomingServer(getter_AddRefs(incomingImapServer));
+ if (incomingImapServer) RecursiveCloseActiveConnections(incomingImapServer);
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->RenameLeaf(this, newName, this, msgWindow);
+}
+
+NS_IMETHODIMP nsImapMailFolder::RecursiveCloseActiveConnections(
+ nsIImapIncomingServer* incomingImapServer) {
+ NS_ENSURE_ARG(incomingImapServer);
+
+ nsCOMPtr<nsIMsgImapMailFolder> folder;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ folder = do_QueryInterface(mSubFolders[i]);
+ if (folder) folder->RecursiveCloseActiveConnections(incomingImapServer);
+
+ incomingImapServer->CloseConnectionForFolder(mSubFolders[i]);
+ }
+ return NS_OK;
+}
+
+// this is called *after* we've done the rename on the server.
+NS_IMETHODIMP nsImapMailFolder::PrepareToRename() {
+ nsCOMPtr<nsIMsgImapMailFolder> folder;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ folder = do_QueryInterface(mSubFolders[i]);
+ if (folder) folder->PrepareToRename();
+ }
+
+ SetOnlineName(EmptyCString());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::RenameLocal(const nsACString& newName,
+ nsIMsgFolder* parent) {
+ nsAutoCString leafname(newName);
+ nsAutoCString parentName;
+ // newName always in the canonical form "greatparent/parentname/leafname"
+ int32_t leafpos = leafname.RFindChar('/');
+ if (leafpos > 0) leafname.Cut(0, leafpos + 1);
+ m_msgParser = nullptr;
+ PrepareToRename();
+ CloseAndBackupFolderDB(leafname);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIFile> oldPathFile;
+ rv = GetFilePath(getter_AddRefs(oldPathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFile> parentPathFile;
+ rv = parent->GetFilePath(getter_AddRefs(parentPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory = false;
+ parentPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) AddDirectorySeparator(parentPathFile);
+
+ nsCOMPtr<nsIFile> dirFile;
+
+ int32_t count = mSubFolders.Count();
+ if (count > 0) {
+ rv = CreateDirectoryForFolder(getter_AddRefs(dirFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = GetSummaryFileLocation(oldPathFile, getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString newNameStr;
+ oldSummaryFile->Remove(false);
+ if (count > 0) {
+ newNameStr = leafname;
+ NS_MsgHashIfNecessary(newNameStr);
+ newNameStr.AppendLiteral(FOLDER_SUFFIX8);
+ nsAutoCString leafName;
+ dirFile->GetNativeLeafName(leafName);
+ if (!leafName.Equals(newNameStr))
+ return dirFile->MoveToNative(
+ nullptr,
+ newNameStr); // in case of rename operation leaf names will differ
+
+ parentPathFile->AppendNative(
+ newNameStr); // only for move we need to progress further in case the
+ // parent differs
+ bool isDirectory = false;
+ parentPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) {
+ rv = parentPathFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ NS_ERROR("Directory already exists.");
+ }
+ rv = RecursiveCopy(dirFile, parentPathFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dirFile->Remove(true); // moving folders
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetPrettyName(nsAString& prettyName) {
+ return GetName(prettyName);
+}
+
+NS_IMETHODIMP nsImapMailFolder::UpdateSummaryTotals(bool force) {
+ // bug 72871 inserted the mIsServer check for IMAP
+ return mIsServer ? NS_OK : nsMsgDBFolder::UpdateSummaryTotals(force);
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+
+ bool isServer;
+ GetIsServer(&isServer);
+
+ *deletable = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ // If this is the rootFolder, return 0 as a safe value.
+ if (NS_FAILED(rv) || isServer) mFolderSize = 0;
+
+ *size = mFolderSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = !(mFlags &
+ (nsMsgFolderFlags::ImapNoinferiors | nsMsgFolderFlags::Virtual));
+
+ bool isServer = false;
+ GetIsServer(&isServer);
+ if (!isServer) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ bool dualUseFolders = true;
+ if (NS_SUCCEEDED(rv) && imapServer)
+ imapServer->GetDualUseFolders(&dualUseFolders);
+ if (!dualUseFolders && *aResult)
+ *aResult = (mFlags & nsMsgFolderFlags::ImapNoselect);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ bool isImapServer = false;
+ nsresult rv = GetIsServer(&isImapServer);
+ if (NS_FAILED(rv)) return rv;
+ // you can only subscribe to imap servers, not imap folders
+ *aResult = isImapServer;
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::GetServerKey(nsACString& serverKey) {
+ // look for matching imap folders, then pop folders
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv)) rv = server->GetKey(serverKey);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetImapIncomingServer(
+ nsIImapIncomingServer** aImapIncomingServer) {
+ NS_ENSURE_ARG(aImapIncomingServer);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server) {
+ nsCOMPtr<nsIImapIncomingServer> incomingServer = do_QueryInterface(server);
+ NS_ENSURE_TRUE(incomingServer, NS_ERROR_NO_INTERFACE);
+ incomingServer.forget(aImapIncomingServer);
+ return NS_OK;
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) {
+ nsMsgDBFolder::AddMessageDispositionState(aMessage, aDispositionFlag);
+
+ // set the mark message answered flag on the server for this message...
+ if (aMessage) {
+ nsMsgKey msgKey;
+ aMessage->GetMessageKey(&msgKey);
+
+ if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Replied)
+ StoreImapFlags(kImapMsgAnsweredFlag, true, {msgKey}, nullptr);
+ else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Forwarded)
+ StoreImapFlags(kImapMsgForwardedFlag, true, {msgKey}, nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::MarkMessagesRead(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markRead) {
+ // tell the folder to do it, which will mark them read in the db.
+ nsresult rv = nsMsgDBFolder::MarkMessagesRead(messages, markRead);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> keysToMarkRead;
+ rv = BuildIdsAndKeyArray(messages, messageIds, keysToMarkRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ StoreImapFlags(kImapMsgSeenFlag, markRead, keysToMarkRead, nullptr);
+ rv = GetDatabase();
+ if (NS_SUCCEEDED(rv)) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) {
+ nsresult rv = GetDatabase();
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<nsMsgKey> thoseMarked;
+ EnableNotifications(allMessageCountNotifications, false);
+ rv = mDatabase->MarkAllRead(thoseMarked);
+ EnableNotifications(allMessageCountNotifications, true);
+ if (NS_SUCCEEDED(rv) && thoseMarked.Length() > 0) {
+ rv = StoreImapFlags(kImapMsgSeenFlag, true, thoseMarked, nullptr);
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+
+ // Setup a undo-state
+ if (aMsgWindow)
+ rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(),
+ thoseMarked.Length());
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::MarkThreadRead(nsIMsgThread* thread) {
+ nsresult rv = GetDatabase();
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<nsMsgKey> keys;
+ rv = mDatabase->MarkThreadRead(thread, nullptr, keys);
+ if (NS_SUCCEEDED(rv) && keys.Length() > 0) {
+ rv = StoreImapFlags(kImapMsgSeenFlag, true, keys, nullptr);
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = nsMsgDBFolder::ReadFromFolderCacheElem(element);
+ int32_t hierarchyDelimiter = kOnlineHierarchySeparatorUnknown;
+ nsCString onlineName;
+
+ element->GetCachedUInt32("boxFlags", (uint32_t*)&m_boxFlags);
+ if (NS_SUCCEEDED(element->GetCachedInt32("hierDelim", &hierarchyDelimiter)) &&
+ hierarchyDelimiter != kOnlineHierarchySeparatorUnknown)
+ m_hierarchyDelimiter = (char)hierarchyDelimiter;
+ rv = element->GetCachedString("onlineName", onlineName);
+ if (NS_SUCCEEDED(rv) && !onlineName.IsEmpty())
+ m_onlineFolderName.Assign(onlineName);
+
+ m_aclFlags = kAclInvalid; // init to invalid value.
+ element->GetCachedUInt32("aclFlags", &m_aclFlags);
+ element->GetCachedInt32("serverTotal", &m_numServerTotalMessages);
+ element->GetCachedInt32("serverUnseen", &m_numServerUnseenMessages);
+ element->GetCachedInt32("serverRecent", &m_numServerRecentMessages);
+ element->GetCachedInt32("nextUID", &m_nextUID);
+ int32_t lastSyncTimeInSec;
+ if (NS_FAILED(element->GetCachedInt32("lastSyncTimeInSec",
+ (int32_t*)&lastSyncTimeInSec)))
+ lastSyncTimeInSec = 0U;
+
+ // make sure that auto-sync state object is created
+ InitAutoSyncState();
+ m_autoSyncStateObj->SetLastSyncTimeInSec(lastSyncTimeInSec);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::WriteToFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = nsMsgDBFolder::WriteToFolderCacheElem(element);
+ element->SetCachedUInt32("boxFlags", (uint32_t)m_boxFlags);
+ element->SetCachedInt32("hierDelim", (int32_t)m_hierarchyDelimiter);
+ element->SetCachedString("onlineName", m_onlineFolderName);
+ element->SetCachedUInt32("aclFlags", m_aclFlags);
+ element->SetCachedInt32("serverTotal", m_numServerTotalMessages);
+ element->SetCachedInt32("serverUnseen", m_numServerUnseenMessages);
+ element->SetCachedInt32("serverRecent", m_numServerRecentMessages);
+ if (m_nextUID != (int32_t)nsMsgKey_None)
+ element->SetCachedInt32("nextUID", m_nextUID);
+
+ // store folder's last sync time
+ if (m_autoSyncStateObj) {
+ PRTime lastSyncTime;
+ m_autoSyncStateObj->GetLastSyncTime(&lastSyncTime);
+ // store in sec
+ element->SetCachedInt32("lastSyncTimeInSec",
+ (int32_t)(lastSyncTime / PR_USEC_PER_SEC));
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::MarkMessagesFlagged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markFlagged) {
+ nsresult rv;
+ // tell the folder to do it, which will mark them read in the db.
+ rv = nsMsgDBFolder::MarkMessagesFlagged(messages, markFlagged);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> keysToMarkFlagged;
+ rv = BuildIdsAndKeyArray(messages, messageIds, keysToMarkFlagged);
+ if (NS_FAILED(rv)) return rv;
+ rv = StoreImapFlags(kImapMsgFlaggedFlag, markFlagged, keysToMarkFlagged,
+ nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetOnlineName(
+ const nsACString& aOnlineFolderName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ // do this after GetDBFolderInfoAndDB, because it crunches m_onlineFolderName
+ // (not sure why)
+ m_onlineFolderName = aOnlineFolderName;
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+ nsAutoString onlineName;
+ CopyUTF8toUTF16(aOnlineFolderName, onlineName);
+ rv = folderInfo->SetProperty("onlineName", onlineName);
+ rv = folderInfo->SetMailboxName(onlineName);
+ // so, when are we going to commit this? Definitely not every time!
+ // We could check if the online name has changed.
+ db->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ folderInfo = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetOnlineName(nsACString& aOnlineFolderName) {
+ ReadDBFolderInfo(false); // update cache first.
+ aOnlineFolderName = m_onlineFolderName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) {
+ NS_ENSURE_ARG_POINTER(folderInfo);
+ NS_ENSURE_ARG_POINTER(db);
+
+ nsresult rv = GetDatabase();
+ if (NS_FAILED(rv)) return rv;
+
+ NS_ADDREF(*db = mDatabase);
+
+ rv = (*db)->GetDBFolderInfo(folderInfo);
+ if (NS_FAILED(rv))
+ return rv; // GetDBFolderInfo can't return NS_OK if !folderInfo
+
+ nsCString onlineName;
+ rv = (*folderInfo)->GetCharProperty("onlineName", onlineName);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!onlineName.IsEmpty())
+ m_onlineFolderName.Assign(onlineName);
+ else {
+ nsAutoString autoOnlineName;
+ (*folderInfo)->GetMailboxName(autoOnlineName);
+ if (autoOnlineName.IsEmpty()) {
+ nsCString uri;
+ rv = GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ rv = GetHostname(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString onlineCName;
+ rv = nsImapURI2FullName(kImapRootURI, hostname.get(), uri.get(),
+ getter_Copies(onlineCName));
+ // Note: check for unknown separator '^' only became needed
+ // with UTF8=ACCEPT modification and haven't found why. Online name
+ // contained the '^' delimiter and gmail said "NO" when folder under
+ // [Gmail] is created and selected.
+ if ((m_hierarchyDelimiter != '/') &&
+ (m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown))
+ onlineCName.ReplaceChar('/', m_hierarchyDelimiter);
+ // XXX: What if online name contains slashes? Breaks?
+ m_onlineFolderName.Assign(onlineCName);
+ CopyUTF8toUTF16(onlineCName, autoOnlineName);
+ }
+ (*folderInfo)->SetProperty("onlineName", autoOnlineName);
+ }
+ return rv;
+}
+
+/* static */
+nsresult nsImapMailFolder::BuildIdsAndKeyArray(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, nsCString& msgIds,
+ nsTArray<nsMsgKey>& keyArray) {
+ keyArray.Clear();
+ keyArray.SetCapacity(messages.Length());
+ // build up message keys.
+ for (auto msgDBHdr : messages) {
+ nsMsgKey key;
+ nsresult rv = msgDBHdr->GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) keyArray.AppendElement(key);
+ }
+ return AllocateUidStringFromKeys(keyArray, msgIds);
+}
+
+/* static */
+nsresult nsImapMailFolder::AllocateUidStringFromKeys(
+ const nsTArray<nsMsgKey>& keys, nsCString& msgIds) {
+ if (keys.IsEmpty()) return NS_ERROR_INVALID_ARG;
+ nsresult rv = NS_OK;
+ uint32_t startSequence;
+ startSequence = keys[0];
+ uint32_t curSequenceEnd = startSequence;
+ uint32_t total = keys.Length();
+ // sort keys and then generate ranges instead of singletons!
+ nsTArray<nsMsgKey> sorted(keys.Clone());
+ sorted.Sort();
+ for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) {
+ uint32_t curKey = sorted[keyIndex];
+ uint32_t nextKey =
+ (keyIndex + 1 < total) ? sorted[keyIndex + 1] : 0xFFFFFFFF;
+ bool lastKey = (nextKey == 0xFFFFFFFF);
+
+ if (lastKey) curSequenceEnd = curKey;
+ if (nextKey == (uint32_t)curSequenceEnd + 1 && !lastKey) {
+ curSequenceEnd = nextKey;
+ continue;
+ }
+ if (curSequenceEnd > startSequence) {
+ AppendUid(msgIds, startSequence);
+ msgIds += ':';
+ AppendUid(msgIds, curSequenceEnd);
+ if (!lastKey) msgIds += ',';
+ startSequence = nextKey;
+ curSequenceEnd = startSequence;
+ } else {
+ startSequence = nextKey;
+ curSequenceEnd = startSequence;
+ AppendUid(msgIds, sorted[keyIndex]);
+ if (!lastKey) msgIds += ',';
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapMailFolder::MarkMessagesImapDeleted(nsTArray<nsMsgKey>* keyArray,
+ bool deleted,
+ nsIMsgDatabase* db) {
+ for (uint32_t kindex = 0; kindex < keyArray->Length(); kindex++) {
+ nsMsgKey key = keyArray->ElementAt(kindex);
+ db->MarkImapDeleted(key, deleted, nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::DeleteMessages(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHeaders, nsIMsgWindow* msgWindow,
+ bool deleteStorage, bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ // *** jt - assuming delete is move to the trash folder for now
+ nsAutoCString uri;
+ bool deleteImmediatelyNoTrash = false;
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> srcKeyArray;
+ bool deleteMsgs = true; // used for toggling delete status - default is true
+ nsMsgImapDeleteModel deleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ imapMessageFlagsType messageFlags = kImapMsgDeletedFlag;
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsresult rv = GetFlag(nsMsgFolderFlags::Trash, &deleteImmediatelyNoTrash);
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+
+ if (NS_SUCCEEDED(rv) && imapServer) {
+ imapServer->GetDeleteModel(&deleteModel);
+ if (deleteModel != nsMsgImapDeleteModels::MoveToTrash || deleteStorage)
+ deleteImmediatelyNoTrash = true;
+ // if we're deleting a message, we should pseudo-interrupt the msg
+ // load of the current message.
+ bool interrupted = false;
+ imapServer->PseudoInterruptMsgLoad(this, msgWindow, &interrupted);
+ }
+
+ rv = BuildIdsAndKeyArray(msgHeaders, messageIds, srcKeyArray);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+
+ if (!deleteImmediatelyNoTrash) {
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash,
+ getter_AddRefs(trashFolder));
+ NS_ASSERTION(trashFolder, "couldn't find trash");
+ // if we can't find the trash, we'll just have to do an imap delete and
+ // pretend this is the trash
+ if (!trashFolder) deleteImmediatelyNoTrash = true;
+ }
+ }
+
+ if ((NS_SUCCEEDED(rv) && deleteImmediatelyNoTrash) ||
+ deleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ if (allowUndo) {
+ // need to take care of these two delete models
+ RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn;
+ if (!undoMsgTxn ||
+ NS_FAILED(undoMsgTxn->Init(this, &srcKeyArray, messageIds.get(),
+ nullptr, true, isMove)))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg);
+ // we're adding this undo action before the delete is successful. This is
+ // evil, but 4.5 did it as well.
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ if (msgWindow) msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) txnMgr->DoTransaction(undoMsgTxn);
+ }
+
+ if (deleteModel == nsMsgImapDeleteModels::IMAPDelete && !deleteStorage) {
+ deleteMsgs = false;
+ for (nsIMsgDBHdr* msgHdr : msgHeaders) {
+ if (!msgHdr) {
+ continue;
+ }
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (!(flags & nsMsgMessageFlags::IMAPDeleted)) {
+ deleteMsgs = true;
+ break;
+ }
+ }
+ }
+ // if copy service listener is also a url listener, pass that
+ // url listener into StoreImapFlags.
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(listener);
+ if (deleteMsgs) messageFlags |= kImapMsgSeenFlag;
+ rv = StoreImapFlags(messageFlags, deleteMsgs, srcKeyArray, urlListener);
+
+ if (NS_SUCCEEDED(rv)) {
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ if (deleteModel == nsMsgImapDeleteModels::IMAPDelete)
+ MarkMessagesImapDeleted(&srcKeyArray, deleteMsgs, database);
+ else {
+ EnableNotifications(allMessageCountNotifications,
+ false); //"remove it immediately" model
+ // Notify if this is an actual delete.
+ if (!isMove) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(
+ "@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsDeleted(msgHeaders);
+ }
+ DeleteStoreMessages(msgHeaders);
+ database->DeleteMessages(srcKeyArray, nullptr);
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+ NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ }
+ }
+ return rv;
+ }
+
+ // have to move the messages to the trash
+ if (trashFolder) {
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ nsCOMPtr<nsISupports> srcSupport;
+
+ rv = QueryInterface(NS_GET_IID(nsIMsgFolder), getter_AddRefs(srcFolder));
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyService->CopyMessages(srcFolder, msgHeaders, trashFolder, true,
+ listener, msgWindow, allowUndo);
+ }
+
+ return rv;
+}
+
+// check if folder is the trash, or a descendent of the trash
+// so we can tell if the folders we're deleting from it should
+// be *really* deleted.
+bool nsImapMailFolder::TrashOrDescendentOfTrash(nsIMsgFolder* folder) {
+ NS_ENSURE_TRUE(folder, false);
+ nsCOMPtr<nsIMsgFolder> parent;
+ nsCOMPtr<nsIMsgFolder> curFolder = folder;
+ nsresult rv;
+ uint32_t flags = 0;
+ do {
+ rv = curFolder->GetFlags(&flags);
+ if (NS_FAILED(rv)) return false;
+ if (flags & nsMsgFolderFlags::Trash) return true;
+ curFolder->GetParent(getter_AddRefs(parent));
+ if (!parent) return false;
+ curFolder = parent;
+ } while (NS_SUCCEEDED(rv) && curFolder);
+ return false;
+}
+NS_IMETHODIMP
+nsImapMailFolder::DeleteSelf(nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ nsresult rv;
+ uint32_t folderFlags;
+
+ // No IMAP shenanigans required for virtual folders.
+ GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ return nsMsgDBFolder::DeleteSelf(nullptr);
+ }
+
+ // "this" is the folder we're deleting from
+ bool deleteNoTrash = TrashOrDescendentOfTrash(this) || !DeleteIsMoveToTrash();
+ bool confirmDeletion = true;
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!deleteNoTrash) {
+ rv = GetTrashFolder(getter_AddRefs(trashFolder));
+ // If we can't find the trash folder and we are supposed to move it to the
+ // trash return failure.
+ if (NS_FAILED(rv) || !trashFolder) return NS_ERROR_FAILURE;
+ bool canHaveSubFoldersOfTrash = true;
+ trashFolder->GetCanCreateSubfolders(&canHaveSubFoldersOfTrash);
+ if (canHaveSubFoldersOfTrash) // UW server doesn't set NOINFERIORS - check
+ // dual use pref
+ {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool serverSupportsDualUseFolders;
+ imapServer->GetDualUseFolders(&serverSupportsDualUseFolders);
+ if (!serverSupportsDualUseFolders) canHaveSubFoldersOfTrash = false;
+ }
+ if (!canHaveSubFoldersOfTrash) deleteNoTrash = true;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mailnews.confirm.moveFoldersToTrash",
+ &confirmDeletion);
+ }
+
+ // If we are deleting folder immediately, ask user for confirmation.
+ bool confirmed = false;
+ if (confirmDeletion || deleteNoTrash) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoTArray<nsString, 1> formatStrings = {folderName};
+
+ nsAutoString deleteFolderDialogTitle;
+ rv = bundle->GetStringFromName("imapDeleteFolderDialogTitle",
+ deleteFolderDialogTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString deleteFolderButtonLabel;
+ rv = bundle->GetStringFromName("imapDeleteFolderButtonLabel",
+ deleteFolderButtonLabel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString confirmationStr;
+ rv = bundle->FormatStringFromName(
+ (deleteNoTrash) ? "imapDeleteNoTrash" : "imapMoveFolderToTrash",
+ formatStrings, confirmationStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!msgWindow) return NS_ERROR_NULL_POINTER;
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ nsCOMPtr<nsIPrompt> dialog;
+ if (docShell) dialog = do_GetInterface(docShell);
+ if (dialog) {
+ int32_t buttonPressed = 0;
+ // Default the dialog to "cancel".
+ const uint32_t buttonFlags =
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1);
+
+ bool dummyValue = false;
+ rv = dialog->ConfirmEx(deleteFolderDialogTitle.get(),
+ confirmationStr.get(), buttonFlags,
+ deleteFolderButtonLabel.get(), nullptr, nullptr,
+ nullptr, &dummyValue, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ confirmed = !buttonPressed; // "ok" is in position 0
+ }
+ } else {
+ confirmed = true;
+ }
+
+ if (confirmed) {
+ if (deleteNoTrash) {
+ rv = imapService->DeleteFolder(this, this, msgWindow);
+ nsMsgDBFolder::DeleteSelf(msgWindow);
+ } else {
+ bool match = false;
+ rv = MatchOrChangeFilterDestination(nullptr, false, &match);
+ if (match) {
+ bool confirm = false;
+ ConfirmFolderDeletionForFilter(msgWindow, &confirm);
+ if (!confirm) return NS_OK;
+ }
+ rv = imapService->MoveFolder(this, trashFolder, this, msgWindow);
+ }
+ }
+ return rv;
+}
+
+// FIXME: helper function to know whether we should check all IMAP folders
+// for new mail; this is necessary because of a legacy hidden preference
+// mail.check_all_imap_folders_for_new (now replaced by per-server preference
+// mail.server.%serverkey%.check_all_folders_for_new), still present in some
+// profiles.
+/*static*/
+bool nsImapMailFolder::ShouldCheckAllFolders(
+ nsIImapIncomingServer* imapServer) {
+ // Check legacy global preference to see if we should check all folders for
+ // new messages, or just the inbox and marked ones.
+ bool checkAllFolders = false;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+ // This pref might not exist, which is OK.
+ (void)prefBranch->GetBoolPref("mail.check_all_imap_folders_for_new",
+ &checkAllFolders);
+
+ if (checkAllFolders) return true;
+
+ // If the legacy preference doesn't exist or has its default value (False),
+ // the true preference is read.
+ imapServer->GetCheckAllFoldersForNew(&checkAllFolders);
+ return checkAllFolders;
+}
+
+// Called by Biff, or when user presses GetMsg button.
+NS_IMETHODIMP nsImapMailFolder::GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool performingBiff = false;
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer =
+ do_QueryInterface(imapServer, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ incomingServer->GetPerformingBiff(&performingBiff);
+ m_urlListener = aListener;
+
+ // See if we should check all folders for new messages, or just the inbox
+ // and marked ones
+ bool checkAllFolders = ShouldCheckAllFolders(imapServer);
+
+ // Get new messages for inbox
+ nsCOMPtr<nsIMsgFolder> inbox;
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox,
+ getter_AddRefs(inbox));
+ if (inbox) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(inbox, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapFolder->SetPerformingBiff(performingBiff);
+ inbox->SetGettingNewMessages(true);
+ rv = inbox->UpdateFolder(aWindow);
+ }
+ // Get new messages for other folders if marked, or all of them if the pref
+ // is set
+ rv = imapServer->GetNewMessagesForNonInboxFolders(
+ rootFolder, aWindow, checkAllFolders, performingBiff);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::Shutdown(bool shutdownChildren) {
+ m_filterList = nullptr;
+ m_initialized = false;
+ // mPath is used to decide if folder pathname needs to be reconstructed in
+ // GetPath().
+ mPath = nullptr;
+ m_moveCoalescer = nullptr;
+ m_msgParser = nullptr;
+ if (m_playbackTimer) {
+ m_playbackTimer->Cancel();
+ m_playbackTimer = nullptr;
+ }
+ m_pendingOfflineMoves.Clear();
+ return nsMsgDBFolder::Shutdown(shutdownChildren);
+}
+
+nsresult nsImapMailFolder::GetBodysToDownload(
+ nsTArray<nsMsgKey>* keysOfMessagesToDownload) {
+ NS_ENSURE_ARG(keysOfMessagesToDownload);
+ NS_ENSURE_TRUE(mDatabase, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ nsresult rv = mDatabase->EnumerateMessages(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMore;
+ nsCOMPtr<nsIMsgDBHdr> header;
+ nsMsgKey msgKey;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ rv = enumerator->GetNext(getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool shouldStoreMsgOffline = false;
+ header->GetMessageKey(&msgKey);
+ // MsgFitsDownloadCriteria ignores nsMsgFolderFlags::Offline, which we
+ // want
+ if (m_downloadingFolderForOfflineUse)
+ MsgFitsDownloadCriteria(msgKey, &shouldStoreMsgOffline);
+ else
+ ShouldStoreMsgOffline(msgKey, &shouldStoreMsgOffline);
+ if (shouldStoreMsgOffline)
+ keysOfMessagesToDownload->AppendElement(msgKey);
+ }
+ if (MOZ_LOG_TEST(gAutoSyncLog, mozilla::LogLevel::Debug) && header) {
+ // Log this only if folder is not empty.
+ uint32_t msgFlags = 0;
+ header->GetFlags(&msgFlags);
+ MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug,
+ ("%s: num keys to download=%zu, last key=%d, last msg flag=0x%x "
+ "nsMsgMessageFlags::Offline=0x%x",
+ __func__, keysOfMessagesToDownload->Length(), msgKey, msgFlags,
+ nsMsgMessageFlags::Offline));
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::OnNewIdleMessages() {
+ nsresult rv;
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool checkAllFolders = ShouldCheckAllFolders(imapServer);
+
+ // only trigger biff if we're checking all new folders for new messages, or
+ // this particular folder, but excluding trash,junk, sent, and no select
+ // folders, by default.
+ if ((checkAllFolders &&
+ !(mFlags &
+ (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk |
+ nsMsgFolderFlags::SentMail | nsMsgFolderFlags::ImapNoselect))) ||
+ (mFlags & (nsMsgFolderFlags::CheckNew | nsMsgFolderFlags::Inbox)))
+ SetPerformingBiff(true);
+ return UpdateFolder(nullptr);
+}
+
+NS_IMETHODIMP nsImapMailFolder::UpdateImapMailboxInfo(
+ nsIImapProtocol* aProtocol, nsIMailboxSpec* aSpec) {
+ nsresult rv;
+ ChangeNumPendingTotalMessages(-mNumPendingTotalMessages);
+ ChangeNumPendingUnread(-mNumPendingUnreadMessages);
+ m_numServerRecentMessages = 0; // clear this since we selected the folder.
+
+ if (!mDatabase) GetDatabase();
+
+ bool folderSelected;
+ rv = aSpec->GetFolderSelected(&folderSelected);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<nsMsgKey> existingKeys;
+ nsTArray<nsMsgKey> keysToDelete;
+ uint32_t numNewUnread;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ int32_t imapUIDValidity = 0;
+ if (mDatabase) {
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (NS_SUCCEEDED(rv) && dbFolderInfo) {
+ dbFolderInfo->GetImapUidValidity(&imapUIDValidity);
+ uint64_t mailboxHighestModSeq;
+ aSpec->GetHighestModSeq(&mailboxHighestModSeq);
+ MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug,
+ ("UpdateImapMailboxInfo(): Store highest MODSEQ=%" PRIu64
+ " for folder=%s",
+ mailboxHighestModSeq, m_onlineFolderName.get()));
+ char intStrBuf[40];
+ PR_snprintf(intStrBuf, sizeof(intStrBuf), "%llu", mailboxHighestModSeq);
+ dbFolderInfo->SetCharProperty(kModSeqPropertyName,
+ nsDependentCString(intStrBuf));
+ }
+ nsTArray<nsMsgKey> keys;
+ rv = mDatabase->ListAllKeys(keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ existingKeys.AppendElements(keys);
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(mDatabase, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ opsDb->ListAllOfflineDeletes(existingKeys);
+ }
+ int32_t folderValidity;
+ aSpec->GetFolder_UIDVALIDITY(&folderValidity);
+ nsCOMPtr<nsIImapFlagAndUidState> flagState;
+ aSpec->GetFlagState(getter_AddRefs(flagState));
+
+ // remember what the supported user flags are.
+ uint32_t supportedUserFlags;
+ aSpec->GetSupportedUserFlags(&supportedUserFlags);
+ SetSupportedUserFlags(supportedUserFlags);
+
+ m_uidValidity = folderValidity;
+
+ if (imapUIDValidity != folderValidity) {
+ NS_ASSERTION(imapUIDValidity == kUidUnknown,
+ "uid validity seems to have changed, blowing away db");
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ if (dbFolderInfo)
+ dbFolderInfo->GetTransferInfo(getter_AddRefs(transferInfo));
+
+ // A backup message database might have been created earlier, for example
+ // if the user requested a reindex. We'll use the earlier one if we can,
+ // otherwise we'll try to backup at this point.
+ nsresult rvbackup = OpenBackupMsgDatabase();
+ if (mDatabase) {
+ dbFolderInfo = nullptr;
+ if (NS_FAILED(rvbackup)) {
+ CloseAndBackupFolderDB(EmptyCString());
+ if (NS_FAILED(OpenBackupMsgDatabase()) && mBackupDatabase) {
+ mBackupDatabase->RemoveListener(this);
+ mBackupDatabase = nullptr;
+ }
+ } else
+ mDatabase->ForceClosed();
+ }
+ mDatabase = nullptr;
+
+ nsCOMPtr<nsIFile> summaryFile;
+ rv = GetSummaryFileLocation(pathFile, getter_AddRefs(summaryFile));
+ // Remove summary file.
+ if (NS_SUCCEEDED(rv) && summaryFile) summaryFile->Remove(false);
+
+ // Create a new summary file, update the folder message counts, and
+ // Close the summary file db.
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+
+ if (NS_FAILED(rv) && mDatabase) {
+ mDatabase->ForceClosed();
+ mDatabase = nullptr;
+ } else if (NS_SUCCEEDED(rv) && mDatabase) {
+ if (transferInfo) SetDBTransferInfo(transferInfo);
+
+ SummaryChanged();
+ if (mDatabase) {
+ if (mAddListener) mDatabase->AddListener(this);
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ }
+ }
+ // store the new UIDVALIDITY value
+
+ if (NS_SUCCEEDED(rv) && dbFolderInfo) {
+ dbFolderInfo->SetImapUidValidity(folderValidity);
+ // need to forget highest mod seq when uid validity rolls.
+ MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug,
+ ("UpdateImapMailboxInfo(): UIDVALIDITY changed, reset highest "
+ "MODSEQ and UID for folder=%s",
+ m_onlineFolderName.get()));
+ dbFolderInfo->SetCharProperty(kModSeqPropertyName, EmptyCString());
+ dbFolderInfo->SetUint32Property(kHighestRecordedUIDPropertyName, 0);
+ }
+ // delete all my msgs, the keys are bogus now
+ // add every message in this folder
+ existingKeys.Clear();
+ // keysToDelete.CopyArray(&existingKeys);
+
+ if (flagState) {
+ nsTArray<nsMsgKey> no_existingKeys;
+ FindKeysToAdd(no_existingKeys, m_keysToFetch, numNewUnread, flagState);
+ }
+ if (NS_FAILED(rv)) pathFile->Remove(false);
+
+ } else if (!flagState /*&& !NET_IsOffline() */) // if there are no messages
+ // on the server
+ keysToDelete = existingKeys.Clone();
+ else /* if ( !NET_IsOffline()) */
+ {
+ uint32_t boxFlags;
+ aSpec->GetBox_flags(&boxFlags);
+ // FindKeysToDelete and FindKeysToAdd require sorted lists
+ existingKeys.Sort();
+ FindKeysToDelete(existingKeys, keysToDelete, flagState, boxFlags);
+ // if this is the result of an expunge then don't grab headers
+ if (!(boxFlags & kJustExpunged))
+ FindKeysToAdd(existingKeys, m_keysToFetch, numNewUnread, flagState);
+ }
+ m_totalKeysToFetch = m_keysToFetch.Length();
+ if (!keysToDelete.IsEmpty() && mDatabase) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete;
+ MsgGetHeadersFromKeys(mDatabase, keysToDelete, hdrsToDelete);
+ // Notify nsIMsgFolderListeners of a mass delete, but only if we actually
+ // have headers
+ if (!hdrsToDelete.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsDeleted(hdrsToDelete);
+ }
+ DeleteStoreMessages(hdrsToDelete);
+ EnableNotifications(nsIMsgFolder::allMessageCountNotifications, false);
+ mDatabase->DeleteMessages(keysToDelete, nullptr);
+ EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true);
+ }
+ int32_t numUnreadFromServer;
+ aSpec->GetNumUnseenMessages(&numUnreadFromServer);
+
+ bool partialUIDFetch;
+ flagState->GetPartialUIDFetch(&partialUIDFetch);
+
+ // For partial UID fetches (i.e., occurs when CONDSTORE in effect), we can
+ // only trust the numUnread from the server. However, even that will only be
+ // correct if a recent imap STATUS occurred as indicated by
+ // numUnreadFromServer greater than -1.
+ if (partialUIDFetch) numNewUnread = numUnreadFromServer;
+
+ // If we are performing biff for this folder, tell the
+ // stand-alone biff about the new high water mark
+ if (m_performingBiff && numNewUnread &&
+ static_cast<int32_t>(numNewUnread) != -1) {
+ // We must ensure that the server knows that we are performing biff.
+ // Otherwise the stand-alone biff won't fire.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server)
+ server->SetPerformingBiff(true);
+ SetNumNewMessages(numNewUnread);
+ }
+ SyncFlags(flagState);
+ if (mDatabase && numUnreadFromServer > -1 &&
+ (int32_t)(mNumUnreadMessages + m_keysToFetch.Length()) >
+ numUnreadFromServer)
+ mDatabase->SyncCounts();
+
+ if (!m_keysToFetch.IsEmpty() && aProtocol)
+ PrepareToAddHeadersToMailDB(aProtocol);
+ else {
+ bool gettingNewMessages;
+ GetGettingNewMessages(&gettingNewMessages);
+ if (gettingNewMessages)
+ ProgressStatusString(aProtocol, "imapNoNewMessages", nullptr);
+ SetPerformingBiff(false);
+ }
+ aSpec->GetNumMessages(&m_numServerTotalMessages);
+ if (numUnreadFromServer > -1) m_numServerUnseenMessages = numUnreadFromServer;
+ aSpec->GetNumRecentMessages(&m_numServerRecentMessages);
+
+ // some servers don't return UIDNEXT on SELECT - don't crunch
+ // existing values in that case.
+ int32_t nextUID;
+ aSpec->GetNextUID(&nextUID);
+ if (nextUID != (int32_t)nsMsgKey_None) m_nextUID = nextUID;
+
+ return rv;
+}
+
+/**
+ * Called after successful imap STATUS response occurs. Have valid unseen value
+ * if folderstatus URL produced an imap STATUS. If a NOOP occurs instead (doing
+ * folderstatus from a connection SELECTed on the same folder) there is no
+ * UNSEEN returned by NOOP.
+ */
+NS_IMETHODIMP nsImapMailFolder::UpdateImapMailboxStatus(
+ nsIImapProtocol* aProtocol, nsIMailboxSpec* aSpec) {
+ NS_ENSURE_ARG_POINTER(aSpec);
+ int32_t numUnread, numTotal;
+ aSpec->GetNumUnseenMessages(&numUnread);
+ aSpec->GetNumMessages(&numTotal);
+ aSpec->GetNumRecentMessages(&m_numServerRecentMessages);
+ int32_t prevNextUID = m_nextUID;
+ aSpec->GetNextUID(&m_nextUID);
+ bool summaryChanged = false;
+
+ // If m_numServerUnseenMessages is 0, it means
+ // this is the first time we've done a Status.
+ // In that case, we count all the previous pending unread messages we know
+ // about as unread messages. We may want to do similar things with total
+ // messages, but the total messages include deleted messages if the folder
+ // hasn't been expunged.
+ int32_t previousUnreadMessages =
+ (m_numServerUnseenMessages)
+ ? m_numServerUnseenMessages
+ : mNumPendingUnreadMessages + mNumUnreadMessages;
+ if (numUnread == -1) {
+ // A noop occurred so don't know server's UNSEEN number, keep using the
+ // previously known unread count.
+ MOZ_LOG(IMAP, mozilla::LogLevel::Debug,
+ ("%s: folder=%s, unread was -1, set numUnread to previousUnread=%d",
+ __func__, m_onlineFolderName.get(), previousUnreadMessages));
+ numUnread = previousUnreadMessages;
+ }
+ if (numUnread != previousUnreadMessages || m_nextUID != prevNextUID) {
+ int32_t unreadDelta =
+ numUnread - (mNumPendingUnreadMessages + mNumUnreadMessages);
+ if (numUnread - previousUnreadMessages != unreadDelta)
+ NS_WARNING("unread count should match server count");
+ ChangeNumPendingUnread(unreadDelta);
+ if (unreadDelta > 0 &&
+ !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) {
+ SetHasNewMessages(true);
+ SetNumNewMessages(unreadDelta);
+ SetBiffState(nsMsgBiffState_NewMail);
+ }
+ summaryChanged = true;
+ }
+ SetPerformingBiff(false);
+ if (m_numServerUnseenMessages != numUnread ||
+ m_numServerTotalMessages != numTotal) {
+ if (numUnread > m_numServerUnseenMessages ||
+ m_numServerTotalMessages > numTotal)
+ NotifyHasPendingMsgs();
+ summaryChanged = true;
+ m_numServerUnseenMessages = numUnread;
+ m_numServerTotalMessages = numTotal;
+ }
+ if (summaryChanged) SummaryChanged();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::ParseMsgHdrs(
+ nsIImapProtocol* aProtocol, nsIImapHeaderXferInfo* aHdrXferInfo) {
+ NS_ENSURE_ARG_POINTER(aHdrXferInfo);
+ int32_t numHdrs;
+ nsCOMPtr<nsIImapHeaderInfo> headerInfo;
+ nsCOMPtr<nsIImapUrl> aImapUrl;
+ nsImapAction imapAction = nsIImapUrl::nsImapTest; // unused value.
+ if (!mDatabase) GetDatabase();
+
+ nsresult rv = aHdrXferInfo->GetNumHeaders(&numHdrs);
+ if (aProtocol) {
+ (void)aProtocol->GetRunningImapURL(getter_AddRefs(aImapUrl));
+ if (aImapUrl) aImapUrl->GetImapAction(&imapAction);
+ }
+ for (uint32_t i = 0; NS_SUCCEEDED(rv) && (int32_t)i < numHdrs; i++) {
+ rv = aHdrXferInfo->GetHeader(i, getter_AddRefs(headerInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!headerInfo) break;
+ int32_t msgSize;
+ nsMsgKey msgKey;
+ bool containsKey;
+ nsCString msgHdrs;
+ headerInfo->GetMsgSize(&msgSize);
+ headerInfo->GetMsgUid(&msgKey);
+ if (msgKey == nsMsgKey_None) // not a valid uid.
+ continue;
+ if (imapAction == nsIImapUrl::nsImapMsgPreview) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ headerInfo->GetMsgHdrs(msgHdrs);
+ // create an input stream based on the hdr string.
+ nsCOMPtr<nsIStringInputStream> inputStream =
+ do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ inputStream->ShareData(msgHdrs.get(), msgHdrs.Length());
+ GetMessageHeader(msgKey, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ GetMsgPreviewTextFromStream(msgHdr, inputStream);
+ }
+ continue;
+ }
+ if (mDatabase &&
+ NS_SUCCEEDED(mDatabase->ContainsKey(msgKey, &containsKey)) &&
+ containsKey) {
+ NS_ERROR("downloading hdrs for hdr we already have");
+ continue;
+ }
+ nsresult rv = SetupHeaderParseStream(msgSize, EmptyCString(), nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ headerInfo->GetMsgHdrs(msgHdrs);
+ rv = ParseAdoptedHeaderLine(msgHdrs.get(), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NormalEndHeaderParseStream(aProtocol, aImapUrl);
+ }
+ return rv;
+}
+
+nsresult nsImapMailFolder::SetupHeaderParseStream(
+ uint32_t aSize, const nsACString& content_type, nsIMailboxSpec* boxSpec) {
+ if (!mDatabase) GetDatabase();
+ m_nextMessageByteLength = aSize;
+ if (!m_msgParser) {
+ nsresult rv;
+ m_msgParser = do_CreateInstance(kParseMailMsgStateCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else
+ m_msgParser->Clear();
+
+ m_msgParser->SetMailDB(mDatabase);
+ if (mBackupDatabase) m_msgParser->SetBackupMailDB(mBackupDatabase);
+ return m_msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState);
+}
+
+nsresult nsImapMailFolder::ParseAdoptedHeaderLine(const char* aMessageLine,
+ nsMsgKey aMsgKey) {
+ // we can get blocks that contain more than one line,
+ // but they never contain partial lines
+ const char* str = aMessageLine;
+ m_curMsgUid = aMsgKey;
+ m_msgParser->SetNewKey(m_curMsgUid);
+ // m_envelope_pos, for local folders,
+ // is the msg key. Setting this will set the msg key for the new header.
+
+ int32_t len = strlen(str);
+ char* currentEOL = PL_strstr(str, MSG_LINEBREAK);
+ const char* currentLine = str;
+ while (currentLine < (str + len)) {
+ if (currentEOL) {
+ m_msgParser->ParseAFolderLine(
+ currentLine, (currentEOL + MSG_LINEBREAK_LEN) - currentLine);
+ currentLine = currentEOL + MSG_LINEBREAK_LEN;
+ currentEOL = PL_strstr(currentLine, MSG_LINEBREAK);
+ } else {
+ m_msgParser->ParseAFolderLine(currentLine, PL_strlen(currentLine));
+ currentLine = str + len + 1;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::NormalEndHeaderParseStream(
+ nsIImapProtocol* aProtocol, nsIImapUrl* imapUrl) {
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+ nsresult rv;
+ NS_ENSURE_TRUE(m_msgParser, NS_ERROR_NULL_POINTER);
+
+ nsMailboxParseState parseState;
+ m_msgParser->GetState(&parseState);
+ if (parseState == nsIMsgParseMailMsgState::ParseHeadersState)
+ m_msgParser->ParseAFolderLine(CRLF, 2);
+ rv = m_msgParser->GetNewMsgHdr(getter_AddRefs(newMsgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char* headers;
+ int32_t headersSize;
+
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl;
+ if (imapUrl) {
+ msgUrl = do_QueryInterface(imapUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ rv = imapServer->GetIsGMailServer(&m_isGmailServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newMsgHdr->SetMessageKey(m_curMsgUid);
+ TweakHeaderFlags(aProtocol, newMsgHdr);
+ uint32_t messageSize;
+ if (NS_SUCCEEDED(newMsgHdr->GetMessageSize(&messageSize)))
+ mFolderSize += messageSize;
+ m_msgMovedByFilter = false;
+
+ nsMsgKey highestUID = 0;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ if (mDatabase) mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo)
+ dbFolderInfo->GetUint32Property(kHighestRecordedUIDPropertyName, 0,
+ &highestUID);
+
+ // If this is the inbox, try to apply filters. Otherwise, test the inherited
+ // folder property "applyIncomingFilters" (which defaults to empty). If this
+ // inherited property has the string value "true", then apply filters even
+ // if this is not the Inbox folder.
+ if (mFlags & nsMsgFolderFlags::Inbox || m_applyIncomingFilters) {
+ // Use highwater to determine whether to filter?
+ bool filterOnHighwater = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch)
+ prefBranch->GetBoolPref("mail.imap.filter_on_new", &filterOnHighwater);
+
+ uint32_t msgFlags;
+ newMsgHdr->GetFlags(&msgFlags);
+
+ // clang-format off
+ bool doFilter = filterOnHighwater
+ // Filter on largest UUID and not deleted.
+ ? m_curMsgUid > highestUID && !(msgFlags & nsMsgMessageFlags::IMAPDeleted)
+ // Filter on unread and not deleted.
+ : !(msgFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::IMAPDeleted));
+ // clang-format on
+
+ if (doFilter)
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) New message parsed, and filters will be run on it"));
+ else
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) New message parsed, but filters will not be run on it"));
+
+ if (doFilter) {
+ int32_t duplicateAction = nsIMsgIncomingServer::keepDups;
+ if (server) server->GetIncomingDuplicateAction(&duplicateAction);
+ if ((duplicateAction != nsIMsgIncomingServer::keepDups) &&
+ mFlags & nsMsgFolderFlags::Inbox) {
+ bool isDup;
+ server->IsNewHdrDuplicate(newMsgHdr, &isDup);
+ if (isDup) {
+ // we want to do something similar to applying filter hits.
+ // if a dup is marked read, it shouldn't trigger biff.
+ // Same for deleting it or moving it to trash.
+ switch (duplicateAction) {
+ case nsIMsgIncomingServer::deleteDups: {
+ uint32_t newFlags;
+ newMsgHdr->OrFlags(
+ nsMsgMessageFlags::Read | nsMsgMessageFlags::IMAPDeleted,
+ &newFlags);
+ StoreImapFlags(kImapMsgSeenFlag | kImapMsgDeletedFlag, true,
+ {m_curMsgUid}, nullptr);
+ m_msgMovedByFilter = true;
+ } break;
+ case nsIMsgIncomingServer::moveDupsToTrash: {
+ nsCOMPtr<nsIMsgFolder> trash;
+ GetTrashFolder(getter_AddRefs(trash));
+ if (trash) {
+ nsCString trashUri;
+ trash->GetURI(trashUri);
+ nsresult err = MoveIncorporatedMessage(
+ newMsgHdr, mDatabase, trashUri, nullptr, msgWindow);
+ if (NS_SUCCEEDED(err)) m_msgMovedByFilter = true;
+ }
+ } break;
+ case nsIMsgIncomingServer::markDupsRead: {
+ uint32_t newFlags;
+ newMsgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags);
+ StoreImapFlags(kImapMsgSeenFlag, true, {m_curMsgUid}, nullptr);
+ } break;
+ }
+ int32_t numNewMessages;
+ GetNumNewMessages(false, &numNewMessages);
+ SetNumNewMessages(numNewMessages - 1);
+ }
+ }
+ rv = m_msgParser->GetAllHeaders(&headers, &headersSize);
+
+ if (NS_SUCCEEDED(rv) && headers && !m_msgMovedByFilter &&
+ !m_filterListRequiresBody) {
+ if (m_filterList) {
+ GetMoveCoalescer(); // not sure why we're doing this here.
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Imap) ApplyFilterToHdr from "
+ "nsImapMailFolder::NormalEndHeaderParseStream()"));
+ m_filterList->ApplyFiltersToHdr(
+ nsMsgFilterType::InboxRule, newMsgHdr, this, mDatabase,
+ nsDependentCSubstring(headers, headersSize), this, msgWindow);
+ NotifyFolderEvent(kFiltersApplied);
+ }
+ }
+ }
+ }
+ // here we need to tweak flags from uid state..
+ if (mDatabase && (!m_msgMovedByFilter || ShowDeletedMessages())) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ // Check if this header corresponds to a pseudo header
+ // we have from doing a pseudo-offline move and then downloading
+ // the real header from the server. In that case, we notify
+ // db/folder listeners that the pseudo-header has become the new
+ // header, i.e., the key has changed.
+ nsCString newMessageId;
+ newMsgHdr->GetMessageId(getter_Copies(newMessageId));
+ nsMsgKey pseudoKey =
+ m_pseudoHdrs.MaybeGet(newMessageId).valueOr(nsMsgKey_None);
+ if (notifier && pseudoKey != nsMsgKey_None) {
+ notifier->NotifyMsgKeyChanged(pseudoKey, newMsgHdr);
+ m_pseudoHdrs.Remove(newMessageId);
+ }
+ mDatabase->AddNewHdrToDB(newMsgHdr, true);
+ if (notifier) notifier->NotifyMsgAdded(newMsgHdr);
+ // mark the header as not yet reported classified
+ OrProcessingFlags(m_curMsgUid, nsMsgProcessingFlags::NotReportedClassified);
+ }
+ // adjust highestRecordedUID
+ if (dbFolderInfo) {
+ if (m_curMsgUid > highestUID) {
+ MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug,
+ ("NormalEndHeaderParseStream(): Store new highest UID=%" PRIu32
+ " for folder=%s",
+ m_curMsgUid, m_onlineFolderName.get()));
+ dbFolderInfo->SetUint32Property(kHighestRecordedUIDPropertyName,
+ m_curMsgUid);
+ }
+ }
+
+ if (m_isGmailServer) {
+ nsCOMPtr<nsIImapFlagAndUidState> flagState;
+ aProtocol->GetFlagAndUidState(getter_AddRefs(flagState));
+ nsCString msgIDValue;
+ nsCString threadIDValue;
+ nsCString labelsValue;
+ flagState->GetCustomAttribute(m_curMsgUid, "X-GM-MSGID"_ns, msgIDValue);
+ flagState->GetCustomAttribute(m_curMsgUid, "X-GM-THRID"_ns, threadIDValue);
+ flagState->GetCustomAttribute(m_curMsgUid, "X-GM-LABELS"_ns, labelsValue);
+ newMsgHdr->SetStringProperty("X-GM-MSGID", msgIDValue);
+ newMsgHdr->SetStringProperty("X-GM-THRID", threadIDValue);
+ newMsgHdr->SetStringProperty("X-GM-LABELS", labelsValue);
+ }
+
+ m_msgParser->Clear(); // clear out parser, because it holds onto a msg hdr.
+ m_msgParser->SetMailDB(nullptr); // tell it to let go of the db too.
+ // I don't think we want to do this - it does bad things like set the size
+ // incorrectly.
+ // m_msgParser->FinishHeader();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::AbortHeaderParseStream(
+ nsIImapProtocol* aProtocol) {
+ nsresult rv = NS_ERROR_FAILURE;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::BeginCopy() {
+ NS_ENSURE_TRUE(m_copyState, NS_ERROR_NULL_POINTER);
+ nsresult rv;
+ if (m_copyState->m_tmpFile) // leftover file spec nuke it
+ {
+ rv = m_copyState->m_tmpFile->Remove(false);
+ if (NS_FAILED(rv)) {
+ nsCString nativePath = m_copyState->m_tmpFile->HumanReadablePath();
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("couldn't remove prev temp file %s: %" PRIx32, nativePath.get(),
+ static_cast<uint32_t>(rv)));
+ }
+ m_copyState->m_tmpFile = nullptr;
+ }
+
+ rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(m_copyState->m_tmpFile));
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("Couldn't create temp file: %" PRIx32, static_cast<uint32_t>(rv)));
+ OnCopyCompleted(m_copyState->m_srcSupport, rv);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIOutputStream> fileOutputStream;
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(m_copyState->m_msgFileStream), m_copyState->m_tmpFile, -1,
+ 00600);
+ if (NS_FAILED(rv))
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("couldn't create output file stream: %" PRIx32,
+ static_cast<uint32_t>(rv)));
+
+ if (!m_copyState->m_dataBuffer)
+ m_copyState->m_dataBuffer = (char*)PR_CALLOC(COPY_BUFFER_SIZE + 1);
+ NS_ENSURE_TRUE(m_copyState->m_dataBuffer, NS_ERROR_OUT_OF_MEMORY);
+ m_copyState->m_dataBufferSize = COPY_BUFFER_SIZE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::CopyDataToOutputStreamForAppend(
+ nsIInputStream* aIStream, int32_t aLength, nsIOutputStream* outputStream) {
+ uint32_t readCount;
+ uint32_t writeCount;
+ if (!m_copyState) m_copyState = new nsImapMailCopyState();
+
+ if (aLength + m_copyState->m_leftOver > m_copyState->m_dataBufferSize) {
+ char* newBuffer = (char*)PR_REALLOC(m_copyState->m_dataBuffer,
+ aLength + m_copyState->m_leftOver + 1);
+ NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
+ m_copyState->m_dataBuffer = newBuffer;
+ m_copyState->m_dataBufferSize = aLength + m_copyState->m_leftOver;
+ }
+
+ char *start, *end;
+ uint32_t linebreak_len = 1;
+
+ nsresult rv = aIStream->Read(
+ m_copyState->m_dataBuffer + m_copyState->m_leftOver, aLength, &readCount);
+ if (NS_FAILED(rv)) return rv;
+
+ m_copyState->m_leftOver += readCount;
+ m_copyState->m_dataBuffer[m_copyState->m_leftOver] = '\0';
+
+ start = m_copyState->m_dataBuffer;
+ if (m_copyState->m_eatLF) {
+ if (*start == '\n') start++;
+ m_copyState->m_eatLF = false;
+ }
+ end = PL_strpbrk(start, "\r\n");
+ if (end && *end == '\r' && *(end + 1) == '\n') linebreak_len = 2;
+
+ while (start && end) {
+ if (PL_strncasecmp(start, "X-Mozilla-Status:", 17) &&
+ PL_strncasecmp(start, "X-Mozilla-Status2:", 18) &&
+ PL_strncmp(start, "From - ", 7)) {
+ rv = outputStream->Write(start, end - start, &writeCount);
+ rv = outputStream->Write(CRLF, 2, &writeCount);
+ }
+ start = end + linebreak_len;
+ if (start >= m_copyState->m_dataBuffer + m_copyState->m_leftOver) {
+ m_copyState->m_leftOver = 0;
+ break;
+ }
+ linebreak_len = 1;
+
+ end = PL_strpbrk(start, "\r\n");
+ if (end && *end == '\r') {
+ if (*(end + 1) == '\n')
+ linebreak_len = 2;
+ else if (!*(end + 1)) // block might have split CRLF so remember if
+ m_copyState->m_eatLF = true; // we should eat LF
+ }
+
+ if (start && !end) {
+ m_copyState->m_leftOver -= (start - m_copyState->m_dataBuffer);
+ memcpy(m_copyState->m_dataBuffer, start,
+ m_copyState->m_leftOver + 1); // including null
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::CopyDataDone() {
+ m_copyState = nullptr;
+ return NS_OK;
+}
+
+// sICopyMessageListener methods, BeginCopy, CopyData, EndCopy, EndMove,
+// StartMessage, EndMessage
+NS_IMETHODIMP nsImapMailFolder::CopyData(nsIInputStream* aIStream,
+ int32_t aLength) {
+ NS_ENSURE_TRUE(
+ m_copyState && m_copyState->m_msgFileStream && m_copyState->m_dataBuffer,
+ NS_ERROR_NULL_POINTER);
+ nsresult rv = CopyDataToOutputStreamForAppend(aIStream, aLength,
+ m_copyState->m_msgFileStream);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyData failed: %" PRIx32, static_cast<uint32_t>(rv)));
+ OnCopyCompleted(m_copyState->m_srcSupport, rv);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::EndCopy(bool copySucceeded) {
+ nsresult rv = copySucceeded ? NS_OK : NS_ERROR_FAILURE;
+ if (copySucceeded && m_copyState && m_copyState->m_msgFileStream) {
+ nsCOMPtr<nsIUrlListener> urlListener;
+ m_copyState->m_msgFileStream->Close();
+ // m_tmpFile can be stale because we wrote to it
+ nsCOMPtr<nsIFile> tmpFile;
+ m_copyState->m_tmpFile->Clone(getter_AddRefs(tmpFile));
+ m_copyState->m_tmpFile = tmpFile;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv =
+ QueryInterface(NS_GET_IID(nsIUrlListener), getter_AddRefs(urlListener));
+ rv = imapService->AppendMessageFromFile(
+ m_copyState->m_tmpFile, this, EmptyCString(), true,
+ m_copyState->m_selectedState, urlListener, m_copyState,
+ m_copyState->m_msgWindow);
+ }
+ if (NS_FAILED(rv) || !copySucceeded)
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("EndCopy failed: %" PRIx32, static_cast<uint32_t>(rv)));
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::EndMove(bool moveSucceeded) { return NS_OK; }
+// this is the beginning of the next message copied
+NS_IMETHODIMP nsImapMailFolder::StartMessage() { return NS_OK; }
+
+// just finished the current message.
+NS_IMETHODIMP nsImapMailFolder::EndMessage(nsMsgKey key) { return NS_OK; }
+
+NS_IMETHODIMP nsImapMailFolder::ApplyFilterHit(nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow,
+ bool* applyMore) {
+ //
+ // This routine is called indirectly from ApplyFiltersToHdr in two
+ // circumstances, controlled by m_filterListRequiresBody:
+ //
+ // If false, after headers are parsed in NormalEndHeaderParseStream.
+ // If true, after the message body is downloaded in NormalEndMsgWriteStream.
+ //
+ // In NormalEndHeaderParseStream, the message has not been added to the
+ // database, and it is important that database notifications and count
+ // updates do not occur. In NormalEndMsgWriteStream, the message has been
+ // added to the database, and database notifications and count updates
+ // should be performed.
+ //
+
+ NS_ENSURE_ARG_POINTER(filter);
+ NS_ENSURE_ARG_POINTER(applyMore);
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (m_filterListRequiresBody)
+ GetMessageHeader(m_curMsgUid, getter_AddRefs(msgHdr));
+ else if (m_msgParser)
+ m_msgParser->GetNewMsgHdr(getter_AddRefs(msgHdr));
+ NS_ENSURE_TRUE(msgHdr,
+ NS_ERROR_NULL_POINTER); // fatal error, cannot apply filters
+
+ bool deleteToTrash = DeleteIsMoveToTrash();
+
+ nsTArray<RefPtr<nsIMsgRuleAction>> filterActionList;
+ rv = filter->GetSortedActionList(filterActionList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t numActions = filterActionList.Length();
+
+ nsCString msgId;
+ msgHdr->GetMessageId(getter_Copies(msgId));
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Applying %" PRIu32
+ " filter actions on message with key %" PRIu32,
+ numActions, msgKeyToInt(msgKey)));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Imap) Message ID: %s", msgId.get()));
+
+ bool loggingEnabled = false;
+ if (m_filterList && numActions)
+ (void)m_filterList->GetLoggingEnabled(&loggingEnabled);
+
+ bool msgIsNew = true;
+
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsresult finalResult = NS_OK; // result of all actions
+ for (uint32_t actionIndex = 0; actionIndex < numActions; actionIndex++) {
+ nsCOMPtr<nsIMsgRuleAction> filterAction(filterActionList[actionIndex]);
+ if (!filterAction) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
+ ("(Imap) Filter action at index %" PRIu32 " invalid, skipping",
+ actionIndex));
+ continue;
+ }
+
+ rv = NS_OK; // result of the current action
+ nsMsgRuleActionType actionType;
+ if (NS_SUCCEEDED(filterAction->GetType(&actionType))) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Running filter action at index %" PRIu32
+ ", action type = %i",
+ actionIndex, actionType));
+ if (loggingEnabled) (void)filter->LogRuleHit(filterAction, msgHdr);
+
+ nsCString actionTargetFolderUri;
+ if (actionType == nsMsgFilterAction::MoveToFolder ||
+ actionType == nsMsgFilterAction::CopyToFolder) {
+ rv = filterAction->GetTargetFolderUri(actionTargetFolderUri);
+ if (NS_FAILED(rv) || actionTargetFolderUri.IsEmpty()) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
+ ("(Imap) Target URI for Copy/Move action is empty, skipping"));
+ // clang-format on
+ NS_ASSERTION(false, "actionTargetFolderUri is empty");
+ continue;
+ }
+ }
+
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ bool isRead = (msgFlags & nsMsgMessageFlags::Read);
+
+ switch (actionType) {
+ case nsMsgFilterAction::Delete: {
+ if (deleteToTrash) {
+ // set value to trash folder
+ nsCOMPtr<nsIMsgFolder> mailTrash;
+ rv = GetTrashFolder(getter_AddRefs(mailTrash));
+ if (NS_SUCCEEDED(rv) && mailTrash) {
+ rv = mailTrash->GetURI(actionTargetFolderUri);
+ if (NS_FAILED(rv)) break;
+ }
+ // msgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags); // mark
+ // read in trash.
+ } else {
+ mDatabase->MarkHdrRead(msgHdr, true, nullptr);
+ mDatabase->MarkImapDeleted(msgKey, true, nullptr);
+ rv = StoreImapFlags(kImapMsgSeenFlag | kImapMsgDeletedFlag, true,
+ {msgKey}, nullptr);
+ if (NS_FAILED(rv)) break;
+ // this will prevent us from adding the header to the db.
+ m_msgMovedByFilter = true;
+ }
+ msgIsNew = false;
+ }
+ // note that delete falls through to move.
+ [[fallthrough]];
+ case nsMsgFilterAction::MoveToFolder: {
+ // if moving to a different file, do it.
+ nsCString uri;
+ rv = GetURI(uri);
+ if (NS_FAILED(rv)) break;
+
+ if (!actionTargetFolderUri.Equals(uri)) {
+ msgHdr->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) {
+ mDatabase->MarkMDNNeeded(msgKey, false, nullptr);
+ mDatabase->MarkMDNSent(msgKey, true, nullptr);
+ }
+ nsresult rv = MoveIncorporatedMessage(
+ msgHdr, mDatabase, actionTargetFolderUri, filter, msgWindow);
+ if (NS_SUCCEEDED(rv)) {
+ m_msgMovedByFilter = true;
+ } else {
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureMoveFailed"_ns);
+ }
+ }
+ }
+ // don't apply any more filters, even if it was a move to the same
+ // folder
+ *applyMore = false;
+ } break;
+ case nsMsgFilterAction::CopyToFolder: {
+ nsCString uri;
+ rv = GetURI(uri);
+ if (NS_FAILED(rv)) break;
+
+ if (!actionTargetFolderUri.Equals(uri)) {
+ // XXXshaver I'm not actually 100% what the right semantics are for
+ // MDNs and copied messages, but I suspect deep down inside that
+ // we probably want to suppress them only on the copies.
+ msgHdr->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) {
+ mDatabase->MarkMDNNeeded(msgKey, false, nullptr);
+ mDatabase->MarkMDNSent(msgKey, true, nullptr);
+ }
+
+ nsCOMPtr<nsIMsgFolder> dstFolder;
+ rv = GetExistingFolder(actionTargetFolderUri,
+ getter_AddRefs(dstFolder));
+ if (NS_FAILED(rv)) break;
+
+ nsCOMPtr<nsIMsgCopyService> copyService = do_GetService(
+ "@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (NS_FAILED(rv)) break;
+ rv = copyService->CopyMessages(this, {&*msgHdr}, dstFolder, false,
+ nullptr, msgWindow, false);
+ if (NS_FAILED(rv)) {
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureCopyFailed"_ns);
+ }
+ }
+ }
+ } break;
+ case nsMsgFilterAction::MarkRead: {
+ mDatabase->MarkHdrRead(msgHdr, true, nullptr);
+ rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
+ msgIsNew = false;
+ } break;
+ case nsMsgFilterAction::MarkUnread: {
+ mDatabase->MarkHdrRead(msgHdr, false, nullptr);
+ rv = StoreImapFlags(kImapMsgSeenFlag, false, {msgKey}, nullptr);
+ msgIsNew = true;
+ } break;
+ case nsMsgFilterAction::MarkFlagged: {
+ mDatabase->MarkHdrMarked(msgHdr, true, nullptr);
+ rv = StoreImapFlags(kImapMsgFlaggedFlag, true, {msgKey}, nullptr);
+ } break;
+ case nsMsgFilterAction::KillThread:
+ case nsMsgFilterAction::WatchThread: {
+ nsCOMPtr<nsIMsgThread> msgThread;
+ nsMsgKey threadKey;
+ mDatabase->GetThreadContainingMsgHdr(msgHdr,
+ getter_AddRefs(msgThread));
+ if (msgThread) {
+ msgThread->GetThreadKey(&threadKey);
+ if (actionType == nsMsgFilterAction::KillThread)
+ rv = mDatabase->MarkThreadIgnored(msgThread, threadKey, true,
+ nullptr);
+ else
+ rv = mDatabase->MarkThreadWatched(msgThread, threadKey, true,
+ nullptr);
+ } else {
+ if (actionType == nsMsgFilterAction::KillThread)
+ rv = msgHdr->SetUint32Property("ProtoThreadFlags",
+ nsMsgMessageFlags::Ignored);
+ else
+ rv = msgHdr->SetUint32Property("ProtoThreadFlags",
+ nsMsgMessageFlags::Watched);
+ }
+ if (actionType == nsMsgFilterAction::KillThread) {
+ mDatabase->MarkHdrRead(msgHdr, true, nullptr);
+ rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
+ msgIsNew = false;
+ }
+ } break;
+ case nsMsgFilterAction::KillSubthread: {
+ mDatabase->MarkHeaderKilled(msgHdr, true, nullptr);
+ mDatabase->MarkHdrRead(msgHdr, true, nullptr);
+ rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
+ msgIsNew = false;
+ } break;
+ case nsMsgFilterAction::ChangePriority: {
+ nsMsgPriorityValue filterPriority; // a int32_t
+ filterAction->GetPriority(&filterPriority);
+ rv = mDatabase->SetUint32PropertyByHdr(
+ msgHdr, "priority", static_cast<uint32_t>(filterPriority));
+ } break;
+ case nsMsgFilterAction::AddTag: {
+ nsCString keyword;
+ filterAction->GetStrValue(keyword);
+ rv = AddKeywordsToMessages({&*msgHdr}, keyword);
+ } break;
+ case nsMsgFilterAction::JunkScore: {
+ nsAutoCString junkScoreStr;
+ int32_t junkScore;
+ filterAction->GetJunkScore(&junkScore);
+ junkScoreStr.AppendInt(junkScore);
+ rv = mDatabase->SetStringProperty(msgKey, "junkscore", junkScoreStr);
+ mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "filter"_ns);
+
+ // If score is available, set up to store junk status on server.
+ if (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE ||
+ junkScore == nsIJunkMailPlugin::IS_HAM_SCORE) {
+ nsTArray<nsMsgKey>* keysToClassify = m_moveCoalescer->GetKeyBucket(
+ (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) ? 0 : 1);
+ NS_ASSERTION(keysToClassify, "error getting key bucket");
+ if (keysToClassify) keysToClassify->AppendElement(msgKey);
+ if (msgIsNew && junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) {
+ msgIsNew = false;
+ mDatabase->MarkHdrNotNew(msgHdr, nullptr);
+ // nsMsgDBFolder::SendFlagNotifications by the call to
+ // SetBiffState(nsMsgBiffState_NoMail) will reset numNewMessages
+ // only if the message is also read and database notifications
+ // are active, but we are not going to mark it read in this
+ // action, preferring to leave the choice to the user.
+ // So correct numNewMessages.
+ if (m_filterListRequiresBody) {
+ msgHdr->GetFlags(&msgFlags);
+ if (!(msgFlags & nsMsgMessageFlags::Read)) {
+ int32_t numNewMessages;
+ GetNumNewMessages(false, &numNewMessages);
+ SetNumNewMessages(--numNewMessages);
+ SetHasNewMessages(numNewMessages != 0);
+ }
+ }
+ }
+ }
+ } break;
+ case nsMsgFilterAction::Forward: {
+ nsCString forwardTo;
+ filterAction->GetStrValue(forwardTo);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) break;
+ if (!forwardTo.IsEmpty()) {
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ if (NS_FAILED(rv)) break;
+ rv = compService->ForwardMessage(
+ NS_ConvertUTF8toUTF16(forwardTo), msgHdr, msgWindow, server,
+ nsIMsgComposeService::kForwardAsDefault);
+ }
+ } break;
+
+ case nsMsgFilterAction::Reply: {
+ nsCString replyTemplateUri;
+ filterAction->GetStrValue(replyTemplateUri);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) break;
+ if (!replyTemplateUri.IsEmpty()) {
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ if (NS_SUCCEEDED(rv) && compService) {
+ rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri,
+ msgWindow, server);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ReplyWithTemplate failed");
+ if (rv == NS_ERROR_ABORT) {
+ (void)filter->LogRuleHitFail(
+ filterAction, msgHdr, rv,
+ "filterFailureSendingReplyAborted"_ns);
+ } else {
+ (void)filter->LogRuleHitFail(
+ filterAction, msgHdr, rv,
+ "filterFailureSendingReplyError"_ns);
+ }
+ }
+ }
+ }
+ } break;
+
+ case nsMsgFilterAction::StopExecution: {
+ // don't apply any more filters
+ *applyMore = false;
+ rv = NS_OK;
+ } break;
+
+ case nsMsgFilterAction::Custom: {
+ nsCOMPtr<nsIMsgFilterCustomAction> customAction;
+ rv = filterAction->GetCustomAction(getter_AddRefs(customAction));
+ if (NS_FAILED(rv)) break;
+
+ nsAutoCString value;
+ rv = filterAction->GetStrValue(value);
+ if (NS_FAILED(rv)) break;
+
+ rv = customAction->ApplyAction({&*msgHdr}, value, nullptr,
+ nsMsgFilterType::InboxRule, msgWindow);
+ // allow custom action to affect new
+ msgHdr->GetFlags(&msgFlags);
+ if (!(msgFlags & nsMsgMessageFlags::New)) msgIsNew = false;
+ } break;
+
+ default:
+ NS_ERROR("unexpected filter action");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+ }
+ if (NS_FAILED(rv)) {
+ finalResult = rv;
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Imap) Action execution failed with error: %" PRIx32,
+ static_cast<uint32_t>(rv)));
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureAction"_ns);
+ }
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Action execution succeeded"));
+ }
+ }
+ if (!msgIsNew) {
+ int32_t numNewMessages;
+ GetNumNewMessages(false, &numNewMessages);
+ // When database notifications are active, new counts will be reset
+ // to zero in nsMsgDBFolder::SendFlagNotifications by the call to
+ // SetBiffState(nsMsgBiffState_NoMail), so don't repeat them here.
+ if (!m_filterListRequiresBody) SetNumNewMessages(--numNewMessages);
+ if (mDatabase) mDatabase->MarkHdrNotNew(msgHdr, nullptr);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Message will not be marked new"));
+ }
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Imap) Finished executing actions"));
+ return finalResult;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetImapFlags(const char* uids, int32_t flags,
+ nsIURI** url) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return imapService->SetMessageFlags(this, this, url, nsAutoCString(uids),
+ flags, true);
+}
+
+// "this" is the parent folder
+NS_IMETHODIMP nsImapMailFolder::PlaybackOfflineFolderCreate(
+ const nsAString& aFolderName, nsIMsgWindow* aWindow, nsIURI** url) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->CreateFolder(this, aFolderName, this, url);
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::ReplayOfflineMoveCopy(const nsTArray<nsMsgKey>& aMsgKeys,
+ bool isMove, nsIMsgFolder* aDstFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aWindow,
+ bool srcFolderOffline) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aDstFolder);
+ if (imapFolder) {
+ nsImapMailFolder* destImapFolder =
+ static_cast<nsImapMailFolder*>(aDstFolder);
+ nsCOMPtr<nsIMsgDatabase> dstFolderDB;
+ aDstFolder->GetMsgDatabase(getter_AddRefs(dstFolderDB));
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(dstFolderDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (opsDb) {
+ // find the fake header in the destination db, and use that to
+ // set the pending attributes on the real headers. To do this,
+ // we need to iterate over the offline ops in the destination db,
+ // looking for ones with matching keys and source folder uri.
+ // If we find that offline op, its "key" will be the key of the fake
+ // header, so we just need to get the header for that key
+ // from the dest db.
+ nsTArray<nsMsgKey> offlineOps;
+ if (NS_SUCCEEDED(opsDb->ListAllOfflineOpIds(offlineOps))) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ nsCString srcFolderUri;
+ GetURI(srcFolderUri);
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp;
+ for (uint32_t opIndex = 0; opIndex < offlineOps.Length(); opIndex++) {
+ opsDb->GetOfflineOpForKey(offlineOps[opIndex], false,
+ getter_AddRefs(currentOp));
+ if (currentOp) {
+ nsCString opSrcUri;
+ currentOp->GetSourceFolderURI(opSrcUri);
+ if (opSrcUri.Equals(srcFolderUri)) {
+ nsMsgKey srcMessageKey;
+ currentOp->GetSrcMessageKey(&srcMessageKey);
+ for (auto key : aMsgKeys) {
+ if (srcMessageKey == key) {
+ nsCOMPtr<nsIMsgDBHdr> fakeDestHdr;
+ dstFolderDB->GetMsgHdrForKey(offlineOps[opIndex],
+ getter_AddRefs(fakeDestHdr));
+ if (fakeDestHdr) messages.AppendElement(fakeDestHdr);
+ break;
+ }
+ }
+ }
+ }
+ }
+ // 3rd parameter: Sets offline flag.
+ destImapFolder->SetPendingAttributes(messages, isMove,
+ srcFolderOffline);
+ }
+ }
+ // if we can't get the dst folder db, we should still try to playback
+ // the offline move/copy.
+ }
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> resultUrl;
+ nsAutoCString uids;
+ AllocateUidStringFromKeys(aMsgKeys, uids);
+ rv = imapService->OnlineMessageCopy(this, uids, aDstFolder, true, isMove,
+ aUrlListener, getter_AddRefs(resultUrl),
+ nullptr, aWindow);
+ if (resultUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(resultUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> folderListener = do_QueryInterface(aDstFolder);
+ if (folderListener) mailnewsUrl->RegisterListener(folderListener);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::AddMoveResultPseudoKey(nsMsgKey aMsgKey) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> pseudoHdr;
+ rv = mDatabase->GetMsgHdrForKey(aMsgKey, getter_AddRefs(pseudoHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString messageId;
+ pseudoHdr->GetMessageId(getter_Copies(messageId));
+ // err on the side of caution and ignore messages w/o messageid.
+ if (messageId.IsEmpty()) return NS_OK;
+ m_pseudoHdrs.InsertOrUpdate(messageId, aMsgKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::StoreImapFlags(int32_t flags, bool addFlags,
+ const nsTArray<nsMsgKey>& keys,
+ nsIUrlListener* aUrlListener) {
+ nsresult rv;
+ if (!WeAreOffline()) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString msgIds;
+ AllocateUidStringFromKeys(keys, msgIds);
+ if (addFlags)
+ imapService->AddMessageFlags(this, aUrlListener ? aUrlListener : this,
+ msgIds, flags, true);
+ else
+ imapService->SubtractMessageFlags(
+ this, aUrlListener ? aUrlListener : this, msgIds, flags, true);
+ } else {
+ rv = GetDatabase();
+ if (NS_SUCCEEDED(rv) && mDatabase) {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(mDatabase, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto key : keys) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ rv = opsDb->GetOfflineOpForKey(key, true, getter_AddRefs(op));
+ SetFlag(nsMsgFolderFlags::OfflineEvents);
+ if (NS_SUCCEEDED(rv) && op) {
+ imapMessageFlagsType newFlags;
+ op->GetNewFlags(&newFlags);
+ op->SetFlagOperation(addFlags ? newFlags | flags : newFlags & ~flags);
+ }
+ }
+ opsDb->Commit(nsMsgDBCommitType::kLargeCommit); // flush offline flags
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::LiteSelect(nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> outUri;
+ return imapService->LiteSelectFolder(this, aUrlListener, aMsgWindow,
+ getter_AddRefs(outUri));
+}
+
+nsresult nsImapMailFolder::GetFolderOwnerUserName(nsACString& userName) {
+ if ((mFlags & nsMsgFolderFlags::ImapPersonal) ||
+ !(mFlags &
+ (nsMsgFolderFlags::ImapPublic | nsMsgFolderFlags::ImapOtherUser))) {
+ // this is one of our personal mail folders
+ // return our username on this host
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ return NS_FAILED(rv) ? rv : server->GetUsername(userName);
+ }
+
+ // the only other type of owner is if it's in the other users' namespace
+ if (!(mFlags & nsMsgFolderFlags::ImapOtherUser)) return NS_OK;
+
+ if (m_ownerUserName.IsEmpty()) {
+ nsCString onlineName;
+ GetOnlineName(onlineName);
+ m_ownerUserName = nsImapNamespaceList::GetFolderOwnerNameFromPath(
+ GetNamespaceForFolder(), onlineName.get());
+ }
+ userName = m_ownerUserName;
+ return NS_OK;
+}
+
+nsImapNamespace* nsImapMailFolder::GetNamespaceForFolder() {
+ if (!m_namespace) {
+#ifdef DEBUG_bienvenu
+ // Make sure this isn't causing us to open the database
+ NS_ASSERTION(m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown,
+ "haven't set hierarchy delimiter");
+#endif
+ nsCString serverKey;
+ nsCString onlineName;
+ GetServerKey(serverKey);
+ GetOnlineName(onlineName);
+ char hierarchyDelimiter;
+ GetHierarchyDelimiter(&hierarchyDelimiter);
+
+ m_namespace = nsImapNamespaceList::GetNamespaceForFolder(
+ serverKey.get(), onlineName.get(), hierarchyDelimiter);
+ NS_ASSERTION(m_namespace, "didn't get namespace for folder");
+ if (m_namespace) {
+ nsImapNamespaceList::SuggestHierarchySeparatorForNamespace(
+ m_namespace, hierarchyDelimiter);
+ m_folderIsNamespace = nsImapNamespaceList::GetFolderIsNamespace(
+ serverKey.get(), onlineName.get(), hierarchyDelimiter, m_namespace);
+ }
+ }
+ return m_namespace;
+}
+
+void nsImapMailFolder::SetNamespaceForFolder(nsImapNamespace* ns) {
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(ns, "null namespace");
+#endif
+ m_namespace = ns;
+}
+
+NS_IMETHODIMP nsImapMailFolder::FolderPrivileges(nsIMsgWindow* window) {
+ NS_ENSURE_ARG_POINTER(window);
+ nsresult rv = NS_OK; // if no window...
+ if (!m_adminUrl.IsEmpty()) {
+ nsCOMPtr<nsIExternalProtocolService> extProtService =
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
+ if (extProtService) {
+ nsAutoCString scheme;
+ nsCOMPtr<nsIURI> uri;
+ if (NS_FAILED(rv = NS_NewURI(getter_AddRefs(uri), m_adminUrl.get())))
+ return rv;
+ uri->GetScheme(scheme);
+ if (!scheme.IsEmpty()) {
+ // if the URL scheme does not correspond to an exposed protocol, then we
+ // need to hand this link click over to the external protocol handler.
+ bool isExposed;
+ rv = extProtService->IsExposedProtocol(scheme.get(), &isExposed);
+ if (NS_SUCCEEDED(rv) && !isExposed)
+ return extProtService->LoadURI(uri, nullptr, nullptr, nullptr, false,
+ false);
+ }
+ }
+ } else {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapService->GetFolderAdminUrl(this, window, this, nullptr);
+ if (NS_SUCCEEDED(rv)) m_urlRunning = true;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetHasAdminUrl(bool* aBool) {
+ NS_ENSURE_ARG_POINTER(aBool);
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ nsCString manageMailAccountUrl;
+ if (NS_SUCCEEDED(rv) && imapServer)
+ rv = imapServer->GetManageMailAccountUrl(manageMailAccountUrl);
+ *aBool = (NS_SUCCEEDED(rv) && !manageMailAccountUrl.IsEmpty());
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetAdminUrl(nsACString& aResult) {
+ aResult = m_adminUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetAdminUrl(const nsACString& adminUrl) {
+ m_adminUrl = adminUrl;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetHdrParser(
+ nsIMsgParseMailMsgState** aHdrParser) {
+ NS_ENSURE_ARG_POINTER(aHdrParser);
+ NS_IF_ADDREF(*aHdrParser = m_msgParser);
+ return NS_OK;
+}
+
+// this is used to issue an arbitrary imap command on the passed in msgs.
+// It assumes the command needs to be run in the selected state.
+NS_IMETHODIMP nsImapMailFolder::IssueCommandOnMsgs(const nsACString& command,
+ const char* uids,
+ nsIMsgWindow* aWindow,
+ nsIURI** url) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->IssueCommandOnMsgs(this, aWindow, command,
+ nsDependentCString(uids), url);
+}
+
+NS_IMETHODIMP nsImapMailFolder::FetchCustomMsgAttribute(
+ const nsACString& attribute, const char* uids, nsIMsgWindow* aWindow,
+ nsIURI** url) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return imapService->FetchCustomMsgAttribute(this, aWindow, attribute,
+ nsDependentCString(uids), url);
+}
+
+nsresult nsImapMailFolder::MoveIncorporatedMessage(
+ nsIMsgDBHdr* mailHdr, nsIMsgDatabase* sourceDB,
+ const nsACString& destFolderUri, nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv;
+ if (m_moveCoalescer) {
+ nsCOMPtr<nsIMsgFolder> destIFolder;
+ rv = GetOrCreateFolder(destFolderUri, getter_AddRefs(destIFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (destIFolder) {
+ // check if the destination is a real folder (by checking for null parent)
+ // and if it can file messages (e.g., servers or news folders can't file
+ // messages). Or read only imap folders...
+ bool canFileMessages = true;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ destIFolder->GetParent(getter_AddRefs(parentFolder));
+ if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages);
+ if (filter && (!parentFolder || !canFileMessages)) {
+ filter->SetEnabled(false);
+ m_filterList->SaveToDefaultFile();
+ destIFolder->ThrowAlertMsg("filterDisabled", msgWindow);
+ return NS_MSG_NOT_A_MAIL_FOLDER;
+ }
+ // put the header into the source db, since it needs to be there when we
+ // copy it and we need a valid header to pass to
+ // StartAsyncCopyMessagesInto
+ nsMsgKey keyToFilter;
+ mailHdr->GetMessageKey(&keyToFilter);
+
+ if (sourceDB && destIFolder) {
+ bool imapDeleteIsMoveToTrash = DeleteIsMoveToTrash();
+ m_moveCoalescer->AddMove(destIFolder, keyToFilter);
+ // For each folder, we need to keep track of the ids we want to move to
+ // that folder - we used to store them in the MSG_FolderInfo and then
+ // when we'd finished downloading headers, we'd iterate through all the
+ // folders looking for the ones that needed messages moved into them -
+ // perhaps instead we could keep track of nsIMsgFolder,
+ // nsTArray<nsMsgKey> pairs here in the imap code. nsTArray<nsMsgKey>
+ // *idsToMoveFromInbox = msgFolder->GetImapIdsToMoveFromInbox();
+ // idsToMoveFromInbox->AppendElement(keyToFilter);
+ if (imapDeleteIsMoveToTrash) {
+ }
+ bool isRead = false;
+ mailHdr->GetIsRead(&isRead);
+ if (imapDeleteIsMoveToTrash) rv = NS_OK;
+ }
+ }
+ } else
+ rv = NS_ERROR_UNEXPECTED;
+
+ // we have to return an error because we do not actually move the message
+ // it is done async and that can fail
+ return rv;
+}
+
+/**
+ * This method assumes that key arrays and flag states are sorted by increasing
+ * key.
+ */
+void nsImapMailFolder::FindKeysToDelete(const nsTArray<nsMsgKey>& existingKeys,
+ nsTArray<nsMsgKey>& keysToDelete,
+ nsIImapFlagAndUidState* flagState,
+ uint32_t boxFlags) {
+ bool showDeletedMessages = ShowDeletedMessages();
+ int32_t numMessageInFlagState;
+ bool partialUIDFetch;
+ uint32_t uidOfMessage;
+ imapMessageFlagsType flags;
+
+ flagState->GetNumberOfMessages(&numMessageInFlagState);
+ flagState->GetPartialUIDFetch(&partialUIDFetch);
+
+ // if we're doing a partialUIDFetch, just delete the keys from the db
+ // that have the deleted flag set (if not using imap delete model)
+ // and return.
+ if (partialUIDFetch) {
+ if (!showDeletedMessages) {
+ for (uint32_t i = 0; (int32_t)i < numMessageInFlagState; i++) {
+ flagState->GetUidOfMessage(i, &uidOfMessage);
+ // flag state will be zero filled up to first real uid, so ignore those.
+ if (uidOfMessage) {
+ flagState->GetMessageFlags(i, &flags);
+ if (flags & kImapMsgDeletedFlag)
+ keysToDelete.AppendElement(uidOfMessage);
+ }
+ }
+ } else if (boxFlags & kJustExpunged) {
+ // we've just issued an expunge with a partial flag state. We should
+ // delete headers with the imap deleted flag set, because we can't
+ // tell from the expunge response which messages were deleted.
+ nsCOMPtr<nsIMsgEnumerator> hdrs;
+ nsresult rv = GetMessages(getter_AddRefs(hdrs));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = hdrs->GetNext(getter_AddRefs(header));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::IMAPDeleted) {
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ keysToDelete.AppendElement(msgKey);
+ }
+ }
+ }
+ return;
+ }
+ // otherwise, we have a complete set of uid's and flags, so we delete
+ // anything that's in existingKeys but not in the flag state, as well
+ // as messages with the deleted flag set.
+ uint32_t total = existingKeys.Length();
+ int onlineIndex = 0; // current index into flagState
+ for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) {
+ while (
+ (onlineIndex < numMessageInFlagState) &&
+ NS_SUCCEEDED(flagState->GetUidOfMessage(onlineIndex, &uidOfMessage)) &&
+ (existingKeys[keyIndex] > uidOfMessage))
+ onlineIndex++;
+
+ flagState->GetUidOfMessage(onlineIndex, &uidOfMessage);
+ flagState->GetMessageFlags(onlineIndex, &flags);
+ // delete this key if it is not there or marked deleted
+ if ((onlineIndex >= numMessageInFlagState) ||
+ (existingKeys[keyIndex] != uidOfMessage) ||
+ ((flags & kImapMsgDeletedFlag) && !showDeletedMessages)) {
+ nsMsgKey doomedKey = existingKeys[keyIndex];
+ if ((int32_t)doomedKey <= 0 && doomedKey != nsMsgKey_None) continue;
+
+ keysToDelete.AppendElement(existingKeys[keyIndex]);
+ }
+
+ flagState->GetUidOfMessage(onlineIndex, &uidOfMessage);
+ if (existingKeys[keyIndex] == uidOfMessage) onlineIndex++;
+ }
+}
+
+void nsImapMailFolder::FindKeysToAdd(const nsTArray<nsMsgKey>& existingKeys,
+ nsTArray<nsMsgKey>& keysToFetch,
+ uint32_t& numNewUnread,
+ nsIImapFlagAndUidState* flagState) {
+ bool showDeletedMessages = ShowDeletedMessages();
+ int dbIndex = 0; // current index into existingKeys
+ int32_t existTotal, numberOfKnownKeys;
+ int32_t messageIndex;
+
+ numNewUnread = 0;
+ existTotal = numberOfKnownKeys = existingKeys.Length();
+ flagState->GetNumberOfMessages(&messageIndex);
+ bool partialUIDFetch;
+ flagState->GetPartialUIDFetch(&partialUIDFetch);
+
+ for (int32_t flagIndex = 0; flagIndex < messageIndex; flagIndex++) {
+ uint32_t uidOfMessage;
+ flagState->GetUidOfMessage(flagIndex, &uidOfMessage);
+ while ((flagIndex < numberOfKnownKeys) && (dbIndex < existTotal) &&
+ existingKeys[dbIndex] < uidOfMessage)
+ dbIndex++;
+
+ if ((flagIndex >= numberOfKnownKeys) || (dbIndex >= existTotal) ||
+ (existingKeys[dbIndex] != uidOfMessage)) {
+ numberOfKnownKeys++;
+
+ imapMessageFlagsType flags;
+ flagState->GetMessageFlags(flagIndex, &flags);
+ NS_ASSERTION(uidOfMessage != nsMsgKey_None, "got invalid msg key");
+ if (uidOfMessage && uidOfMessage != nsMsgKey_None &&
+ (showDeletedMessages || !(flags & kImapMsgDeletedFlag))) {
+ if (mDatabase) {
+ bool dbContainsKey;
+ if (NS_SUCCEEDED(
+ mDatabase->ContainsKey(uidOfMessage, &dbContainsKey)) &&
+ dbContainsKey) {
+ // this is expected in the partial uid fetch case because the
+ // flag state does not contain all messages, so the db has
+ // messages the flag state doesn't know about.
+ if (!partialUIDFetch) NS_ERROR("db has key - flagState messed up?");
+ continue;
+ }
+ }
+ keysToFetch.AppendElement(uidOfMessage);
+ if (!(flags & kImapMsgSeenFlag)) numNewUnread++;
+ }
+ }
+ }
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetMsgHdrsToDownload(
+ bool* aMoreToDownload, int32_t* aTotalCount, nsTArray<nsMsgKey>& aKeys) {
+ NS_ENSURE_ARG_POINTER(aMoreToDownload);
+ NS_ENSURE_ARG_POINTER(aTotalCount);
+ aKeys.Clear();
+
+ *aMoreToDownload = false;
+ *aTotalCount = m_totalKeysToFetch;
+ if (m_keysToFetch.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // if folder isn't open in a window, no reason to limit the number of headers
+ // we download.
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ bool folderOpen = false;
+ if (session) session->IsFolderOpenInWindow(this, &folderOpen);
+
+ int32_t hdrChunkSize = 200;
+ if (folderOpen) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (prefBranch)
+ prefBranch->GetIntPref("mail.imap.hdr_chunk_size", &hdrChunkSize);
+ }
+ int32_t numKeysToFetch = m_keysToFetch.Length();
+ int32_t startIndex = 0;
+ if (folderOpen && hdrChunkSize > 0 &&
+ (int32_t)m_keysToFetch.Length() > hdrChunkSize) {
+ numKeysToFetch = hdrChunkSize;
+ *aMoreToDownload = true;
+ startIndex = m_keysToFetch.Length() - hdrChunkSize;
+ }
+ aKeys.AppendElements(&m_keysToFetch[startIndex], numKeysToFetch);
+ // Remove these for the incremental header download case, so that
+ // we know we don't have to download them again.
+ m_keysToFetch.RemoveElementsAt(startIndex, numKeysToFetch);
+
+ return NS_OK;
+}
+
+void nsImapMailFolder::PrepareToAddHeadersToMailDB(nsIImapProtocol* aProtocol) {
+ // now, tell it we don't need any bodies.
+ nsTArray<nsMsgKey> noBodies;
+ aProtocol->NotifyBodysToDownload(noBodies);
+}
+
+void nsImapMailFolder::TweakHeaderFlags(nsIImapProtocol* aProtocol,
+ nsIMsgDBHdr* tweakMe) {
+ if (mDatabase && aProtocol && tweakMe) {
+ tweakMe->SetMessageKey(m_curMsgUid);
+ tweakMe->SetMessageSize(m_nextMessageByteLength);
+
+ bool foundIt = false;
+
+ nsCOMPtr<nsIImapFlagAndUidState> flagState;
+ nsresult rv = aProtocol->GetFlagAndUidState(getter_AddRefs(flagState));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ rv = flagState->HasMessage(m_curMsgUid, &foundIt);
+
+ if (NS_SUCCEEDED(rv) && foundIt) {
+ imapMessageFlagsType imap_flags;
+ nsCString customFlags;
+ flagState->GetMessageFlagsByUid(m_curMsgUid, &imap_flags);
+ if (imap_flags & kImapMsgCustomKeywordFlag) {
+ flagState->GetCustomFlags(m_curMsgUid, getter_Copies(customFlags));
+ }
+
+ // make a mask and clear these message flags
+ uint32_t mask = nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied |
+ nsMsgMessageFlags::Marked |
+ nsMsgMessageFlags::IMAPDeleted |
+ nsMsgMessageFlags::Labels;
+ uint32_t dbHdrFlags;
+
+ tweakMe->GetFlags(&dbHdrFlags);
+ tweakMe->AndFlags(~mask, &dbHdrFlags);
+
+ // set the new value for these flags
+ uint32_t newFlags = 0;
+ if (imap_flags & kImapMsgSeenFlag)
+ newFlags |= nsMsgMessageFlags::Read;
+ else // if (imap_flags & kImapMsgRecentFlag)
+ newFlags |= nsMsgMessageFlags::New;
+
+ // Okay here is the MDN needed logic (if DNT header seen):
+ /* if server support user defined flag:
+ XXX TODO: Fix badly formatted comment which doesn't reflect the code.
+ MDNSent flag set => clear kMDNNeeded flag
+ MDNSent flag not set => do nothing, leave kMDNNeeded on
+ else if
+ not nsMsgMessageFlags::New => clear kMDNNeeded flag
+ nsMsgMessageFlags::New => do nothing, leave kMDNNeeded on
+ */
+ uint16_t userFlags;
+ rv = aProtocol->GetSupportedUserFlags(&userFlags);
+ if (NS_SUCCEEDED(rv) && (userFlags & (kImapMsgSupportUserFlag |
+ kImapMsgSupportMDNSentFlag))) {
+ if (imap_flags & kImapMsgMDNSentFlag) {
+ newFlags |= nsMsgMessageFlags::MDNReportSent;
+ if (dbHdrFlags & nsMsgMessageFlags::MDNReportNeeded)
+ tweakMe->AndFlags(~nsMsgMessageFlags::MDNReportNeeded, &dbHdrFlags);
+ }
+ }
+
+ if (imap_flags & kImapMsgAnsweredFlag)
+ newFlags |= nsMsgMessageFlags::Replied;
+ if (imap_flags & kImapMsgFlaggedFlag)
+ newFlags |= nsMsgMessageFlags::Marked;
+ if (imap_flags & kImapMsgDeletedFlag)
+ newFlags |= nsMsgMessageFlags::IMAPDeleted;
+ if (imap_flags & kImapMsgForwardedFlag)
+ newFlags |= nsMsgMessageFlags::Forwarded;
+ if (newFlags) tweakMe->OrFlags(newFlags, &dbHdrFlags);
+ if (!customFlags.IsEmpty())
+ (void)HandleCustomFlags(m_curMsgUid, tweakMe, userFlags, customFlags);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetupMsgWriteStream(nsIFile* aFile, bool addDummyEnvelope) {
+ nsresult rv;
+ aFile->Remove(false);
+ m_tempMessageStreamBytesWritten = 0;
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(m_tempMessageStream), aFile,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 00700);
+ if (m_tempMessageStream && addDummyEnvelope) {
+ nsAutoCString result;
+ char* ct;
+ uint32_t writeCount;
+ time_t now = time((time_t*)0);
+ ct = ctime(&now);
+ ct[24] = 0;
+ result = "From - ";
+ result += ct;
+ result += MSG_LINEBREAK;
+
+ rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+
+ result = "X-Mozilla-Status: 0001";
+ result += MSG_LINEBREAK;
+ rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+
+ result = "X-Mozilla-Status2: 00000000";
+ result += MSG_LINEBREAK;
+ rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow* window) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> srcKeyArray;
+ nsresult rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray);
+ if (NS_FAILED(rv) || messageIds.IsEmpty()) return rv;
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AcquireSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (NS_FAILED(rv)) {
+ ThrowAlertMsg("operationFailedFolderBusy", window);
+ return rv;
+ }
+ return imapService->DownloadMessagesForOffline(messageIds, this, this,
+ window);
+}
+
+NS_IMETHODIMP nsImapMailFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> runningURI;
+ bool noSelect;
+ GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect);
+
+ if (!noSelect) {
+ nsAutoCString messageIdsToDownload;
+ nsTArray<nsMsgKey> msgsToDownload;
+
+ GetDatabase();
+ m_downloadingFolderForOfflineUse = true;
+
+ rv = AcquireSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (NS_FAILED(rv)) {
+ m_downloadingFolderForOfflineUse = false;
+ ThrowAlertMsg("operationFailedFolderBusy", msgWindow);
+ return rv;
+ }
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Selecting the folder with nsIImapUrl::shouldStoreMsgOffline true will
+ // cause us to fetch any message bodies we don't have.
+ m_urlListener = listener;
+ rv = imapService->SelectFolder(this, this, msgWindow,
+ getter_AddRefs(runningURI));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(runningURI));
+ if (imapUrl) imapUrl->SetStoreResultsOffline(true);
+ m_urlRunning = true;
+ }
+ } else
+ rv = NS_MSG_FOLDER_UNREADABLE;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::ParseAdoptedMsgLine(const char* adoptedMessageLine,
+ nsMsgKey uidOfMessage,
+ nsIImapUrl* aImapUrl) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+ uint32_t count = 0;
+ nsresult rv;
+ // remember the uid of the message we're downloading.
+ m_curMsgUid = uidOfMessage;
+ if (!m_offlineHeader) {
+ rv = GetMessageHeader(uidOfMessage, getter_AddRefs(m_offlineHeader));
+ if (NS_SUCCEEDED(rv) && !m_offlineHeader) rv = NS_ERROR_UNEXPECTED;
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = StartNewOfflineMessage();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // adoptedMessageLine is actually a string with a lot of message lines,
+ // separated by native line terminators we need to count the number of
+ // MSG_LINEBREAK's to determine how much to increment m_numOfflineMsgLines by.
+ const char* nextLine = adoptedMessageLine;
+ do {
+ m_numOfflineMsgLines++;
+ nextLine = PL_strstr(nextLine, MSG_LINEBREAK);
+ if (nextLine) nextLine += MSG_LINEBREAK_LEN;
+ } while (nextLine && *nextLine);
+
+ if (m_tempMessageStream) {
+ rv = m_tempMessageStream->Write(adoptedMessageLine,
+ PL_strlen(adoptedMessageLine), &count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += count;
+ }
+ return NS_OK;
+}
+
+void nsImapMailFolder::EndOfflineDownload() {
+ if (m_tempMessageStream) {
+ m_tempMessageStream->Close();
+ m_tempMessageStream = nullptr;
+ ReleaseSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ m_offlineHeader = nullptr;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::NormalEndMsgWriteStream(nsMsgKey uidOfMessage, bool markRead,
+ nsIImapUrl* imapUrl,
+ int32_t updatedMessageSize) {
+ if (updatedMessageSize != -1) {
+ // retrieve the message header to update size, if we don't already have it
+ nsCOMPtr<nsIMsgDBHdr> msgHeader = m_offlineHeader;
+ if (!msgHeader) GetMessageHeader(uidOfMessage, getter_AddRefs(msgHeader));
+ if (msgHeader) {
+ uint32_t msgSize;
+ msgHeader->GetMessageSize(&msgSize);
+ MOZ_LOG(IMAP, mozilla::LogLevel::Debug,
+ ("Updating stored message size from %u, new size %d", msgSize,
+ updatedMessageSize));
+ msgHeader->SetMessageSize(updatedMessageSize);
+ // only commit here if this isn't an offline message
+ // offline header gets committed in EndNewOfflineMessage() called below
+ if (mDatabase && !m_offlineHeader)
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ } else
+ NS_WARNING(
+ "Failed to get message header when trying to update message size");
+ }
+
+ if (m_offlineHeader) EndNewOfflineMessage(NS_OK);
+
+ m_curMsgUid = uidOfMessage;
+
+ // Apply filter now if it needed a body
+ if (m_filterListRequiresBody) {
+ if (m_filterList) {
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+ GetMessageHeader(uidOfMessage, getter_AddRefs(newMsgHdr));
+ GetMoveCoalescer();
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ if (imapUrl) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl;
+ msgUrl = do_QueryInterface(imapUrl, &rv);
+ if (msgUrl && NS_SUCCEEDED(rv))
+ msgUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ }
+ m_filterList->ApplyFiltersToHdr(nsMsgFilterType::InboxRule, newMsgHdr,
+ this, mDatabase, EmptyCString(), this,
+ msgWindow);
+ NotifyFolderEvent(kFiltersApplied);
+ }
+ // Process filter plugins and other items normally done at the end of
+ // HeaderFetchCompleted.
+ bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves();
+ PlaybackCoalescedOperations();
+
+ bool filtersRun;
+ CallFilterPlugins(nullptr, &filtersRun);
+ int32_t numNewBiffMsgs = 0;
+ if (m_performingBiff) GetNumNewMessages(false, &numNewBiffMsgs);
+
+ if (!filtersRun && m_performingBiff && mDatabase && numNewBiffMsgs > 0 &&
+ (!pendingMoves || !ShowPreviewText())) {
+ // If we are performing biff for this folder, tell the
+ // stand-alone biff about the new high water mark
+ // We must ensure that the server knows that we are performing biff.
+ // Otherwise the stand-alone biff won't fire.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server)
+ server->SetPerformingBiff(true);
+
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail);
+ if (server) server->SetPerformingBiff(false);
+ m_performingBiff = false;
+ }
+
+ if (m_filterList) (void)m_filterList->FlushLogIfNecessary();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::AbortMsgWriteStream() {
+ m_offlineHeader = nullptr;
+ return NS_ERROR_FAILURE;
+}
+
+// message move/copy related methods
+NS_IMETHODIMP
+nsImapMailFolder::OnlineCopyCompleted(nsIImapProtocol* aProtocol,
+ ImapOnlineCopyState aCopyState) {
+ NS_ENSURE_ARG_POINTER(aProtocol);
+
+ nsresult rv;
+ if (aCopyState == ImapOnlineCopyStateType::kSuccessfulCopy) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ rv = aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl));
+ if (NS_FAILED(rv) || !imapUrl) return NS_ERROR_FAILURE;
+ nsImapAction action;
+ rv = imapUrl->GetImapAction(&action);
+ if (NS_FAILED(rv)) return rv;
+ if (action != nsIImapUrl::nsImapOnlineToOfflineMove)
+ return NS_ERROR_FAILURE; // don't assert here...
+ nsCString messageIds;
+ rv = imapUrl->GetListOfMessageIds(messageIds);
+ if (NS_FAILED(rv)) return rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->AddMessageFlags(this, nullptr, messageIds,
+ kImapMsgDeletedFlag, true);
+ }
+ /* unhandled copystate */
+ if (m_copyState) // whoops, this is the wrong folder - should use the source
+ // folder
+ {
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ srcFolder = do_QueryInterface(m_copyState->m_srcSupport, &rv);
+ if (srcFolder) srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ } else
+ rv = NS_ERROR_FAILURE;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::CloseMockChannel(nsIImapMockChannel* aChannel) {
+ aChannel->Close();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::ReleaseUrlCacheEntry(nsIMsgMailNewsUrl* aUrl) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ return aUrl->SetMemCacheEntry(nullptr);
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::BeginMessageUpload() { return NS_ERROR_FAILURE; }
+
+nsresult nsImapMailFolder::HandleCustomFlags(nsMsgKey uidOfMessage,
+ nsIMsgDBHdr* dbHdr,
+ uint16_t userFlags,
+ nsCString& keywords) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ToLowerCase(keywords);
+ bool messageClassified = true;
+ // ### TODO: we really should parse the keywords into space delimited keywords
+ // before checking
+ // Mac Mail, Yahoo uses "NotJunk"
+ if (FindInReadable("NonJunk"_ns, keywords,
+ nsCaseInsensitiveCStringComparator) ||
+ FindInReadable("NotJunk"_ns, keywords,
+ nsCaseInsensitiveCStringComparator)) {
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_HAM_SCORE);
+ mDatabase->SetStringProperty(uidOfMessage, "junkscore", msgJunkScore);
+ } else if (FindInReadable("Junk"_ns, keywords,
+ nsCaseInsensitiveCStringComparator)) {
+ uint32_t newFlags;
+ dbHdr->AndFlags(~nsMsgMessageFlags::New, &newFlags);
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_SPAM_SCORE);
+ mDatabase->SetStringProperty(uidOfMessage, "junkscore", msgJunkScore);
+ } else
+ messageClassified = false;
+ if (messageClassified) {
+ // only set the junkscore origin if it wasn't set before.
+ nsCString existingProperty;
+ dbHdr->GetStringProperty("junkscoreorigin", existingProperty);
+ if (existingProperty.IsEmpty())
+ dbHdr->SetStringProperty("junkscoreorigin", "imapflag"_ns);
+ }
+
+ if (!(userFlags & kImapMsgSupportUserFlag)) {
+ nsCString localKeywords;
+ nsCString prevKeywords;
+ dbHdr->GetStringProperty("keywords", localKeywords);
+ dbHdr->GetStringProperty("prevkeywords", prevKeywords);
+ // localKeywords: tags currently stored in database for the message.
+ // keywords: tags stored in server and obtained when flags for the message
+ // were last fetched. (Parameter of this function.)
+ // prevKeywords: saved keywords from previous call of this function.
+ // clang-format off
+ MOZ_LOG(IMAP_KW, mozilla::LogLevel::Debug,
+ ("UID=%" PRIu32 ", localKeywords=|%s| keywords=|%s|, prevKeywords=|%s|",
+ uidOfMessage, localKeywords.get(), keywords.get(), prevKeywords.get()));
+ // clang-format on
+
+ // Store keywords to detect changes on next call of this function.
+ dbHdr->SetStringProperty("prevkeywords", keywords);
+
+ // Parse the space separated strings into arrays.
+ nsTArray<nsCString> localKeywordArray;
+ nsTArray<nsCString> keywordArray;
+ nsTArray<nsCString> prevKeywordArray;
+ ParseString(localKeywords, ' ', localKeywordArray);
+ ParseString(keywords, ' ', keywordArray);
+ ParseString(prevKeywords, ' ', prevKeywordArray);
+
+ // If keyword not received now but was the last time, remove it from
+ // the localKeywords. This means the keyword was removed by another user
+ // sharing the folder.
+ for (uint32_t i = 0; i < prevKeywordArray.Length(); i++) {
+ bool inRcvd = keywordArray.Contains(prevKeywordArray[i]);
+ bool inLocal = localKeywordArray.Contains(prevKeywordArray[i]);
+ if (!inRcvd && inLocal)
+ localKeywordArray.RemoveElement(prevKeywordArray[i]);
+ }
+
+ // Combine local and rcvd keyword arrays into a single string
+ // so it can be passed to SetStringProperty(). If element of
+ // local already in rcvd, avoid duplicates in combined string.
+ nsAutoCString combinedKeywords;
+ for (uint32_t i = 0; i < localKeywordArray.Length(); i++) {
+ if (!keywordArray.Contains(localKeywordArray[i])) {
+ combinedKeywords.Append(localKeywordArray[i]);
+ combinedKeywords.Append(' ');
+ }
+ }
+ for (uint32_t i = 0; i < keywordArray.Length(); i++) {
+ combinedKeywords.Append(keywordArray[i]);
+ combinedKeywords.Append(' ');
+ }
+ MOZ_LOG(IMAP_KW, mozilla::LogLevel::Debug,
+ ("combinedKeywords stored = |%s|", combinedKeywords.get()));
+ // combinedKeywords are tags being stored in database for the message.
+ return dbHdr->SetStringProperty("keywords", combinedKeywords);
+ }
+ return (userFlags & kImapMsgSupportUserFlag)
+ ? dbHdr->SetStringProperty("keywords", keywords)
+ : NS_OK;
+}
+
+// synchronize the message flags in the database with the server flags
+nsresult nsImapMailFolder::SyncFlags(nsIImapFlagAndUidState* flagState) {
+ nsresult rv = GetDatabase(); // we need a database for this
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool partialUIDFetch;
+ flagState->GetPartialUIDFetch(&partialUIDFetch);
+
+ // update all of the database flags
+ int32_t messageIndex;
+ uint32_t messageSize;
+
+ // Take this opportunity to recalculate the folder size, if we're not a
+ // partial (condstore) fetch.
+ int64_t newFolderSize = 0;
+
+ flagState->GetNumberOfMessages(&messageIndex);
+
+ uint16_t supportedUserFlags;
+ flagState->GetSupportedUserFlags(&supportedUserFlags);
+
+ for (int32_t flagIndex = 0; flagIndex < messageIndex; flagIndex++) {
+ uint32_t uidOfMessage;
+ flagState->GetUidOfMessage(flagIndex, &uidOfMessage);
+ imapMessageFlagsType flags;
+ flagState->GetMessageFlags(flagIndex, &flags);
+ bool containsKey;
+ rv = mDatabase->ContainsKey(uidOfMessage, &containsKey);
+ // if we don't have the header, don't diddle the flags.
+ // GetMsgHdrForKey will create the header if it doesn't exist.
+ if (NS_FAILED(rv) || !containsKey) continue;
+
+ nsCOMPtr<nsIMsgDBHdr> dbHdr;
+ rv = mDatabase->GetMsgHdrForKey(uidOfMessage, getter_AddRefs(dbHdr));
+ if (NS_FAILED(rv)) continue;
+ if (NS_SUCCEEDED(dbHdr->GetMessageSize(&messageSize)))
+ newFolderSize += messageSize;
+
+ nsCString keywords;
+ if (NS_SUCCEEDED(
+ flagState->GetCustomFlags(uidOfMessage, getter_Copies(keywords))))
+ HandleCustomFlags(uidOfMessage, dbHdr, supportedUserFlags, keywords);
+
+ NotifyMessageFlagsFromHdr(dbHdr, uidOfMessage, flags);
+ }
+ if (!partialUIDFetch && newFolderSize != mFolderSize) {
+ int64_t oldFolderSize = mFolderSize;
+ mFolderSize = newFolderSize;
+ NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize);
+ }
+
+ return NS_OK;
+}
+
+// helper routine to sync the flags on a given header
+nsresult nsImapMailFolder::NotifyMessageFlagsFromHdr(nsIMsgDBHdr* dbHdr,
+ nsMsgKey msgKey,
+ uint32_t flags) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Although it may seem strange to keep a local reference of mDatabase here,
+ // the current lifetime management of databases requires that methods
+ // sometimes null the database when they think they opened it. Unfortunately
+ // experience shows this happens when we don't expect, so for crash protection
+ // best practice with the current flawed database management is to keep a
+ // local reference when there will be complex calls in a method. See bug
+ // 1312254.
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ NS_ENSURE_STATE(database);
+
+ database->MarkHdrRead(dbHdr, (flags & kImapMsgSeenFlag) != 0, nullptr);
+ database->MarkHdrReplied(dbHdr, (flags & kImapMsgAnsweredFlag) != 0, nullptr);
+ database->MarkHdrMarked(dbHdr, (flags & kImapMsgFlaggedFlag) != 0, nullptr);
+ database->MarkImapDeleted(msgKey, (flags & kImapMsgDeletedFlag) != 0,
+ nullptr);
+
+ uint32_t supportedFlags;
+ GetSupportedUserFlags(&supportedFlags);
+ if (supportedFlags & kImapMsgSupportForwardedFlag)
+ database->MarkForwarded(msgKey, (flags & kImapMsgForwardedFlag) != 0,
+ nullptr);
+ if (supportedFlags & kImapMsgSupportMDNSentFlag)
+ database->MarkMDNSent(msgKey, (flags & kImapMsgMDNSentFlag) != 0, nullptr);
+
+ return NS_OK;
+}
+
+// message flags operation - this is called from the imap protocol,
+// proxied over from the imap thread to the ui thread, when a flag changes
+NS_IMETHODIMP
+nsImapMailFolder::NotifyMessageFlags(uint32_t aFlags,
+ const nsACString& aKeywords,
+ nsMsgKey aMsgKey,
+ uint64_t aHighestModSeq) {
+ if (NS_SUCCEEDED(GetDatabase()) && mDatabase) {
+ bool msgDeleted = aFlags & kImapMsgDeletedFlag;
+ if (aHighestModSeq || msgDeleted) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ if (aHighestModSeq) {
+ char intStrBuf[40];
+ PR_snprintf(intStrBuf, sizeof(intStrBuf), "%llu", aHighestModSeq);
+ MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug,
+ ("NotifyMessageFlags(): Store highest MODSEQ=%" PRIu64
+ " for folder=%s",
+ aHighestModSeq, m_onlineFolderName.get()));
+ dbFolderInfo->SetCharProperty(kModSeqPropertyName,
+ nsDependentCString(intStrBuf));
+ }
+ if (msgDeleted) {
+ uint32_t oldDeletedCount;
+ dbFolderInfo->GetUint32Property(kDeletedHdrCountPropertyName, 0,
+ &oldDeletedCount);
+ dbFolderInfo->SetUint32Property(kDeletedHdrCountPropertyName,
+ oldDeletedCount + 1);
+ }
+ }
+ }
+ nsCOMPtr<nsIMsgDBHdr> dbHdr;
+ bool containsKey;
+ nsresult rv = mDatabase->ContainsKey(aMsgKey, &containsKey);
+ // if we don't have the header, don't diddle the flags.
+ // GetMsgHdrForKey will create the header if it doesn't exist.
+ if (NS_FAILED(rv) || !containsKey) return rv;
+ rv = mDatabase->GetMsgHdrForKey(aMsgKey, getter_AddRefs(dbHdr));
+ if (NS_SUCCEEDED(rv) && dbHdr) {
+ uint32_t supportedUserFlags;
+ GetSupportedUserFlags(&supportedUserFlags);
+ NotifyMessageFlagsFromHdr(dbHdr, aMsgKey, aFlags);
+ nsCString keywords(aKeywords);
+ HandleCustomFlags(aMsgKey, dbHdr, supportedUserFlags, keywords);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::NotifyMessageDeleted(const char* onlineFolderName,
+ bool deleteAllMsgs,
+ const char* msgIdString) {
+ if (deleteAllMsgs) return NS_OK;
+
+ if (!msgIdString) return NS_OK;
+
+ nsTArray<nsMsgKey> affectedMessages;
+ ParseUidString(msgIdString, affectedMessages);
+
+ if (!ShowDeletedMessages()) {
+ GetDatabase();
+ NS_ENSURE_TRUE(mDatabase, NS_OK);
+ if (!ShowDeletedMessages()) {
+ if (!affectedMessages.IsEmpty()) // perhaps Search deleted these messages
+ {
+ DeleteStoreMessages(affectedMessages);
+ mDatabase->DeleteMessages(affectedMessages, nullptr);
+ }
+ } else // && !imapDeleteIsMoveToTrash // TODO: can this ever be executed?
+ SetIMAPDeletedFlag(mDatabase, affectedMessages, false);
+ }
+ return NS_OK;
+}
+
+bool nsImapMailFolder::ShowDeletedMessages() {
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionList, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool showDeleted = false;
+ nsCString serverKey;
+ GetServerKey(serverKey);
+ hostSession->GetShowDeletedMessagesForHost(serverKey.get(), showDeleted);
+
+ return showDeleted;
+}
+
+bool nsImapMailFolder::DeleteIsMoveToTrash() {
+ nsresult err;
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionList, &err);
+ NS_ENSURE_SUCCESS(err, true);
+ bool rv = true;
+
+ nsCString serverKey;
+ GetServerKey(serverKey);
+ hostSession->GetDeleteIsMoveToTrashForHost(serverKey.get(), rv);
+ return rv;
+}
+
+nsresult nsImapMailFolder::GetTrashFolder(nsIMsgFolder** pTrashFolder) {
+ NS_ENSURE_ARG_POINTER(pTrashFolder);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, pTrashFolder);
+ if (!*pTrashFolder) rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+// store nsMsgMessageFlags::IMAPDeleted in the specified mailhdr records
+void nsImapMailFolder::SetIMAPDeletedFlag(nsIMsgDatabase* mailDB,
+ const nsTArray<nsMsgKey>& msgids,
+ bool markDeleted) {
+ nsresult markStatus = NS_OK;
+ uint32_t total = msgids.Length();
+
+ for (uint32_t msgIndex = 0; NS_SUCCEEDED(markStatus) && (msgIndex < total);
+ msgIndex++)
+ markStatus =
+ mailDB->MarkImapDeleted(msgids[msgIndex], markDeleted, nullptr);
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetMessageSizeFromDB(const char* id, uint32_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+
+ *size = 0;
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (id) {
+ nsMsgKey key = msgKeyFromInt(ParseUint64Str(id));
+ nsCOMPtr<nsIMsgDBHdr> mailHdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) rv = mailHdr->GetMessageSize(size);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetCurMoveCopyMessageInfo(nsIImapUrl* runningUrl,
+ PRTime* aDate,
+ nsACString& aKeywords,
+ uint32_t* aResult) {
+ nsCOMPtr<nsISupports> copyState;
+ runningUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) {
+ nsCOMPtr<nsImapMailCopyState> mailCopyState = do_QueryInterface(copyState);
+ uint32_t supportedFlags = 0;
+ GetSupportedUserFlags(&supportedFlags);
+ if (mailCopyState &&
+ mailCopyState->m_curIndex < mailCopyState->m_messages.Length()) {
+ nsIMsgDBHdr* message =
+ mailCopyState->m_messages[mailCopyState->m_curIndex];
+ message->GetFlags(aResult);
+ if (aDate) message->GetDate(aDate);
+ if (supportedFlags & kImapMsgSupportUserFlag) {
+ // setup the custom imap keywords, which includes the message keywords
+ // plus any junk status
+ nsCString junkscore;
+ message->GetStringProperty("junkscore", junkscore);
+ bool isJunk = false, isNotJunk = false;
+ if (!junkscore.IsEmpty()) {
+ if (junkscore.EqualsLiteral("0"))
+ isNotJunk = true;
+ else
+ isJunk = true;
+ }
+
+ nsCString keywords; // MsgFindKeyword can't use nsACString
+ message->GetStringProperty("keywords", keywords);
+ int32_t start;
+ int32_t length;
+ bool hasJunk = MsgFindKeyword("junk"_ns, keywords, &start, &length);
+ if (hasJunk && !isJunk)
+ keywords.Cut(start, length);
+ else if (!hasJunk && isJunk)
+ keywords.AppendLiteral(" Junk");
+ bool hasNonJunk =
+ MsgFindKeyword("nonjunk"_ns, keywords, &start, &length);
+ if (!hasNonJunk)
+ hasNonJunk = MsgFindKeyword("notjunk"_ns, keywords, &start, &length);
+ if (hasNonJunk && !isNotJunk)
+ keywords.Cut(start, length);
+ else if (!hasNonJunk && isNotJunk)
+ keywords.AppendLiteral(" NonJunk");
+
+ // Cleanup extra spaces
+ while (!keywords.IsEmpty() && keywords.First() == ' ')
+ keywords.Cut(0, 1);
+ while (!keywords.IsEmpty() && keywords.Last() == ' ')
+ keywords.Cut(keywords.Length() - 1, 1);
+ while (!keywords.IsEmpty() && (start = keywords.Find(" "_ns)) >= 0)
+ keywords.Cut(start, 1);
+ aKeywords.Assign(keywords);
+ }
+ }
+ // if we don't have a source header, and it's not the drafts folder,
+ // then mark the message read, since it must be an append to the
+ // fcc or templates folder.
+ else if (mailCopyState) {
+ *aResult = mailCopyState->m_newMsgFlags;
+ if (supportedFlags & kImapMsgSupportUserFlag)
+ aKeywords.Assign(mailCopyState->m_newMsgKeywords);
+ }
+ }
+ return NS_OK;
+}
+
+// nsIUrlListener implementation.
+NS_IMETHODIMP
+nsImapMailFolder::OnStartRunningUrl(nsIURI* aUrl) {
+ NS_ASSERTION(aUrl, "sanity check - need to be be running non-null url");
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) {
+ bool updatingFolder;
+ mailUrl->GetUpdatingFolder(&updatingFolder);
+ m_updatingFolder = updatingFolder;
+ }
+ m_urlRunning = true;
+ return NS_OK;
+}
+
+// nsIUrlListener implementation.
+// nsImapMailFolder passes itself as a listener when it kicks off operations
+// on the nsIImapService. So, when the operation completes, this gets called
+// to handle all the different operations, using a big switch statement.
+NS_IMETHODIMP
+nsImapMailFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ nsresult rv;
+ bool endedOfflineDownload = false;
+ nsImapAction imapAction = nsIImapUrl::nsImapTest;
+ m_urlRunning = false;
+ m_updatingFolder = false;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aUrl) {
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool downloadingForOfflineUse;
+ imapUrl->GetStoreResultsOffline(&downloadingForOfflineUse);
+ bool hasSemaphore = false;
+ // if we have the folder locked, clear it.
+ TestSemaphore(static_cast<nsIMsgFolder*>(this), &hasSemaphore);
+ if (hasSemaphore) ReleaseSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (downloadingForOfflineUse) {
+ endedOfflineDownload = true;
+ EndOfflineDownload();
+ }
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ bool folderOpen = false;
+ if (mailUrl) mailUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ if (session) session->IsFolderOpenInWindow(this, &folderOpen);
+#ifdef DEBUG_bienvenu
+ printf("stop running url %s\n", aUrl->GetSpecOrDefault().get());
+#endif
+
+ if (imapUrl) {
+ DisplayStatusMsg(imapUrl, EmptyString());
+ imapUrl->GetImapAction(&imapAction);
+ if (imapAction == nsIImapUrl::nsImapMsgFetch ||
+ imapAction == nsIImapUrl::nsImapMsgDownloadForOffline) {
+ ReleaseSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (!endedOfflineDownload) EndOfflineDownload();
+ }
+
+ // Notify move, copy or delete (online operations)
+ // Not sure whether nsImapDeleteMsg is even used, deletes in all three
+ // models use nsImapAddMsgFlags.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier && m_copyState) {
+ if (imapAction == nsIImapUrl::nsImapOnlineMove) {
+ notifier->NotifyMsgsMoveCopyCompleted(true, m_copyState->m_messages,
+ this, {});
+ } else if (imapAction == nsIImapUrl::nsImapOnlineCopy) {
+ notifier->NotifyMsgsMoveCopyCompleted(false, m_copyState->m_messages,
+ this, {});
+ } else if (imapAction == nsIImapUrl::nsImapDeleteMsg) {
+ notifier->NotifyMsgsDeleted(m_copyState->m_messages);
+ }
+ }
+
+ switch (imapAction) {
+ case nsIImapUrl::nsImapDeleteMsg:
+ case nsIImapUrl::nsImapOnlineMove:
+ case nsIImapUrl::nsImapOnlineCopy:
+ if (NS_SUCCEEDED(aExitCode)) {
+ if (folderOpen)
+ UpdateFolder(msgWindow);
+ else
+ UpdatePendingCounts();
+ }
+
+ if (m_copyState) {
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(m_copyState->m_srcSupport, &rv);
+ if (m_copyState->m_isMove && !m_copyState->m_isCrossServerOp) {
+ if (NS_SUCCEEDED(aExitCode)) {
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ if (srcFolder)
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (NS_SUCCEEDED(rv) && srcDB) {
+ RefPtr<nsImapMoveCopyMsgTxn> msgTxn;
+ nsTArray<nsMsgKey> srcKeyArray;
+ if (m_copyState->m_allowUndo) {
+ msgTxn = m_copyState->m_undoMsgTxn;
+ if (msgTxn) msgTxn->GetSrcKeyArray(srcKeyArray);
+ } else {
+ nsAutoCString messageIds;
+ rv = BuildIdsAndKeyArray(m_copyState->m_messages,
+ messageIds, srcKeyArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!ShowDeletedMessages()) {
+ // We only reach here for same-server operations
+ // (!m_copyState->m_isCrossServerOp in if above), so we can
+ // assume that the src is also imap that uses offline
+ // storage.
+ DeleteStoreMessages(srcKeyArray, srcFolder);
+ srcDB->DeleteMessages(srcKeyArray, nullptr);
+ } else
+ MarkMessagesImapDeleted(&srcKeyArray, true, srcDB);
+ }
+ srcFolder->EnableNotifications(allMessageCountNotifications,
+ true);
+ // even if we're showing deleted messages,
+ // we still need to notify FE so it will show the imap deleted
+ // flag
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ // is there a way to see that we think we have new msgs?
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ bool showPreviewText;
+ prefBranch->GetBoolPref("mail.biff.alert.show_preview",
+ &showPreviewText);
+ // if we're showing preview text, update ourselves if we got a
+ // new unread message copied so that we can download the new
+ // headers and have a chance to preview the msg bodies.
+ if (!folderOpen && showPreviewText &&
+ m_copyState->m_unreadCount > 0 &&
+ !(mFlags &
+ (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk)))
+ UpdateFolder(msgWindow);
+ }
+ } else {
+ srcFolder->EnableNotifications(allMessageCountNotifications,
+ true);
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ }
+ }
+ if (m_copyState->m_msgWindow &&
+ m_copyState->m_undoMsgTxn && // may be null from filters
+ NS_SUCCEEDED(
+ aExitCode)) // we should do this only if move/copy succeeds
+ {
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ m_copyState->m_msgWindow->GetTransactionManager(
+ getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ RefPtr<nsImapMoveCopyMsgTxn> txn = m_copyState->m_undoMsgTxn;
+ mozilla::DebugOnly<nsresult> rv2 = txnMgr->DoTransaction(txn);
+ NS_ASSERTION(NS_SUCCEEDED(rv2), "doing transaction failed");
+ }
+ }
+ // nsImapUrl can hold a pointer to our m_copyState, so force a
+ // release here (see Bug 1586494).
+ imapUrl->SetCopyState(nullptr);
+ (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode);
+ }
+
+ // we're the dest folder of a move/copy - if we're not open in the ui,
+ // then we should clear our nsMsgDatabase pointer. Otherwise, the db
+ // would be open until the user selected it and then selected another
+ // folder. but don't do this for the trash or inbox - we'll leave them
+ // open
+ if (!folderOpen &&
+ !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
+ SetMsgDatabase(nullptr);
+ break;
+ case nsIImapUrl::nsImapSubtractMsgFlags: {
+ // this isn't really right - we'd like to know we were
+ // deleting a message to start with, but it probably
+ // won't do any harm.
+ imapMessageFlagsType flags = 0;
+ imapUrl->GetMsgFlags(&flags);
+ // we need to subtract the delete flag in db only in case when we show
+ // deleted msgs
+ if (flags & kImapMsgDeletedFlag && ShowDeletedMessages()) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ rv = GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ nsTArray<nsMsgKey> keyArray;
+ nsCString keyString;
+ imapUrl->GetListOfMessageIds(keyString);
+ ParseUidString(keyString.get(), keyArray);
+ MarkMessagesImapDeleted(&keyArray, false, db);
+ db->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ } break;
+ case nsIImapUrl::nsImapAddMsgFlags: {
+ imapMessageFlagsType flags = 0;
+ imapUrl->GetMsgFlags(&flags);
+ if (flags & kImapMsgDeletedFlag) {
+ // we need to delete headers from db only when we don't show deleted
+ // msgs
+ if (!ShowDeletedMessages()) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ rv = GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ nsTArray<nsMsgKey> keyArray;
+ nsCString keyString;
+ imapUrl->GetListOfMessageIds(keyString);
+ ParseUidString(keyString.get(), keyArray);
+
+ // For pluggable stores that do not support compaction, we need
+ // to delete the messages now.
+ bool supportsCompaction = false;
+ nsCOMPtr<nsIMsgPluggableStore> offlineStore;
+ (void)GetMsgStore(getter_AddRefs(offlineStore));
+ if (offlineStore)
+ offlineStore->GetSupportsCompaction(&supportsCompaction);
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrs;
+ if (notifier || !supportsCompaction) {
+ MsgGetHeadersFromKeys(db, keyArray, msgHdrs);
+ }
+
+ // Notify listeners of delete.
+ if (notifier && !msgHdrs.IsEmpty()) {
+ // XXX Currently, the DeleteMessages below gets executed twice
+ // on deletes. Once in DeleteMessages, once here. The second
+ // time, it silently fails to delete. This is why we're also
+ // checking whether the array is empty.
+ notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ if (!supportsCompaction && !msgHdrs.IsEmpty())
+ DeleteStoreMessages(msgHdrs);
+
+ db->DeleteMessages(keyArray, nullptr);
+ db->SetSummaryValid(true);
+ db->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ }
+ } break;
+ case nsIImapUrl::nsImapAppendMsgFromFile:
+ case nsIImapUrl::nsImapAppendDraftFromFile:
+ if (m_copyState) {
+ if (NS_SUCCEEDED(aExitCode)) {
+ UpdatePendingCounts();
+
+ m_copyState->m_curIndex++;
+ if (m_copyState->m_curIndex >= m_copyState->m_messages.Length()) {
+ nsCOMPtr<nsIUrlListener> saveUrlListener = m_urlListener;
+ if (folderOpen) {
+ // This gives a way for the caller to get notified
+ // when the UpdateFolder url is done.
+ // (if the nsIMsgCopyServiceListener also implements
+ // nsIUrlListener)
+ if (m_copyState->m_listener)
+ m_urlListener = do_QueryInterface(m_copyState->m_listener);
+ }
+ if (m_copyState->m_msgWindow && m_copyState->m_undoMsgTxn) {
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ m_copyState->m_msgWindow->GetTransactionManager(
+ getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ RefPtr<nsImapMoveCopyMsgTxn> txn =
+ m_copyState->m_undoMsgTxn;
+ txnMgr->DoTransaction(txn);
+ }
+ }
+ (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode);
+ if (folderOpen ||
+ imapAction == nsIImapUrl::nsImapAppendDraftFromFile) {
+ UpdateFolderWithListener(msgWindow, m_urlListener);
+ m_urlListener = saveUrlListener;
+ }
+ }
+ } else {
+ // clear the copyState if copy has failed
+ (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode);
+ }
+ }
+ break;
+ case nsIImapUrl::nsImapMoveFolderHierarchy:
+ if (m_copyState) // delete folder gets here, but w/o an m_copyState
+ {
+ nsCOMPtr<nsIMsgCopyService> copyService = do_GetService(
+ "@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(m_copyState->m_srcSupport);
+ if (srcFolder) {
+ copyService->NotifyCompletion(m_copyState->m_srcSupport, this,
+ aExitCode);
+ }
+ m_copyState = nullptr;
+ }
+ break;
+ case nsIImapUrl::nsImapRenameFolder:
+ if (NS_FAILED(aExitCode)) {
+ NotifyFolderEvent(kRenameCompleted);
+ }
+ break;
+ case nsIImapUrl::nsImapDeleteAllMsgs:
+ if (NS_SUCCEEDED(aExitCode)) {
+ if (folderOpen)
+ UpdateFolder(msgWindow);
+ else {
+ ChangeNumPendingTotalMessages(-mNumPendingTotalMessages);
+ ChangeNumPendingUnread(-mNumPendingUnreadMessages);
+ m_numServerUnseenMessages = 0;
+ }
+ }
+ break;
+ case nsIImapUrl::nsImapListFolder:
+ if (NS_SUCCEEDED(aExitCode)) {
+ // listing folder will open db; don't leave the db open.
+ SetMsgDatabase(nullptr);
+ if (!m_verifiedAsOnlineFolder) {
+ // If folder is not verified, we remove it.
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapParent =
+ do_QueryInterface(parent);
+ if (imapParent) this->RemoveLocalSelf();
+ }
+ }
+ }
+ break;
+ case nsIImapUrl::nsImapRefreshFolderUrls:
+ // we finished getting an admin url for the folder.
+ if (!m_adminUrl.IsEmpty()) FolderPrivileges(msgWindow);
+ break;
+ case nsIImapUrl::nsImapCreateFolder:
+ if (NS_FAILED(aExitCode)) // if success notification already done
+ {
+ NotifyFolderEvent(kFolderCreateFailed);
+ }
+ break;
+ case nsIImapUrl::nsImapSubscribe:
+ if (NS_SUCCEEDED(aExitCode) && msgWindow) {
+ nsCString canonicalFolderName;
+ imapUrl->CreateCanonicalSourceFolderPathString(
+ getter_Copies(canonicalFolderName));
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot =
+ do_QueryInterface(rootFolder);
+ if (imapRoot) {
+ nsCOMPtr<nsIMsgImapMailFolder> foundFolder;
+ rv = imapRoot->FindOnlineSubFolder(canonicalFolderName,
+ getter_AddRefs(foundFolder));
+ if (NS_SUCCEEDED(rv) && foundFolder) {
+ nsCString uri;
+ nsCOMPtr<nsIMsgFolder> msgFolder =
+ do_QueryInterface(foundFolder);
+ if (msgFolder) {
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ obsServ->NotifyObservers(msgFolder, "folder-subscribed",
+ nullptr);
+ }
+ }
+ }
+ }
+ }
+ break;
+ case nsIImapUrl::nsImapExpungeFolder:
+ break;
+ default:
+ break;
+ }
+ }
+ // give base class a chance to send folder loaded notification...
+ rv = nsMsgDBFolder::OnStopRunningUrl(aUrl, aExitCode);
+ }
+ // if we're not running a url, we must not be getting new mail.
+ SetGettingNewMessages(false);
+
+ // If we're planning to inform another listener then do that now.
+ // Some folder methods can take a listener to inform when the operation
+ // is complete e.g. UpdateFolderWithListener(), DownloadAllForOffline(),
+ // GetNewMessages(). That listener is stashed in m_urlListener.
+ if (m_urlListener) {
+ nsCOMPtr<nsIUrlListener> saveListener = m_urlListener;
+ m_urlListener = nullptr;
+ saveListener->OnStopRunningUrl(aUrl, aExitCode);
+ }
+ return rv;
+}
+
+void nsImapMailFolder::UpdatePendingCounts() {
+ if (m_copyState) {
+ int32_t delta =
+ m_copyState->m_isCrossServerOp ? 1 : m_copyState->m_messages.Length();
+ if (!m_copyState->m_selectedState && m_copyState->m_messages.IsEmpty()) {
+ // special case from CopyFileMessage():
+ // - copied a single message in from a file
+ // - no previously-existing messages are involved
+ delta = 1;
+ }
+ ChangePendingTotal(delta);
+
+ // count the moves that were unread
+ int numUnread = m_copyState->m_unreadCount;
+ if (numUnread) {
+ m_numServerUnseenMessages +=
+ numUnread; // adjust last status count by this delta.
+ ChangeNumPendingUnread(numUnread);
+ }
+ SummaryChanged();
+ }
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::ClearFolderRights() {
+ SetFolderNeedsACLListed(false);
+ delete m_folderACL;
+ m_folderACL = new nsMsgIMAPFolderACL(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::AddFolderRights(const nsACString& userName,
+ const nsACString& rights) {
+ SetFolderNeedsACLListed(false);
+ GetFolderACL()->SetFolderRightsForUser(userName, rights);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::RefreshFolderRights() {
+ if (GetFolderACL()->GetIsFolderShared())
+ SetFlag(nsMsgFolderFlags::PersonalShared);
+ else
+ ClearFlag(nsMsgFolderFlags::PersonalShared);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetCopyResponseUid(const char* msgIdString,
+ nsIImapUrl* aUrl) { // CopyMessages() only
+ nsresult rv = NS_OK;
+ RefPtr<nsImapMoveCopyMsgTxn> msgTxn;
+ nsCOMPtr<nsISupports> copyState;
+
+ if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState));
+
+ if (copyState) {
+ nsCOMPtr<nsImapMailCopyState> mailCopyState =
+ do_QueryInterface(copyState, &rv);
+ if (NS_FAILED(rv)) return rv;
+ if (mailCopyState->m_undoMsgTxn) msgTxn = mailCopyState->m_undoMsgTxn;
+ } else if (aUrl && m_pendingOfflineMoves.Length()) {
+ nsCString urlSourceMsgIds, undoTxnSourceMsgIds;
+ aUrl->GetListOfMessageIds(urlSourceMsgIds);
+ RefPtr<nsImapMoveCopyMsgTxn> imapUndo = m_pendingOfflineMoves[0];
+ if (imapUndo) {
+ imapUndo->GetSrcMsgIds(undoTxnSourceMsgIds);
+ if (undoTxnSourceMsgIds.Equals(urlSourceMsgIds)) msgTxn = imapUndo;
+ // ### we should handle batched moves, but lets keep it simple for a2.
+ m_pendingOfflineMoves.Clear();
+ }
+ }
+ if (msgTxn) msgTxn->SetCopyResponseUid(msgIdString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::StartMessage(nsIMsgMailNewsUrl* aUrl) {
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl));
+ nsCOMPtr<nsISupports> copyState;
+ NS_ENSURE_TRUE(imapUrl, NS_ERROR_FAILURE);
+
+ imapUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) {
+ nsCOMPtr<nsICopyMessageStreamListener> listener =
+ do_QueryInterface(copyState);
+ if (listener) listener->StartMessage();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::EndMessage(nsIMsgMailNewsUrl* aUrl, nsMsgKey uidOfMessage) {
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl));
+ nsCOMPtr<nsISupports> copyState;
+ NS_ENSURE_TRUE(imapUrl, NS_ERROR_FAILURE);
+ imapUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) {
+ nsCOMPtr<nsICopyMessageStreamListener> listener =
+ do_QueryInterface(copyState);
+ if (listener) listener->EndMessage(uidOfMessage);
+ }
+ return NS_OK;
+}
+
+#define WHITESPACE " \015\012" // token delimiter
+
+NS_IMETHODIMP
+nsImapMailFolder::NotifySearchHit(nsIMsgMailNewsUrl* aUrl,
+ const char* searchHitLine) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // expect search results in the form of "* SEARCH <hit> <hit> ..."
+ // expect search results in the form of "* SEARCH <hit> <hit> ..."
+ nsCString tokenString(searchHitLine);
+ char* currentPosition = PL_strcasestr(tokenString.get(), "SEARCH");
+ if (currentPosition) {
+ currentPosition += strlen("SEARCH");
+ bool shownUpdateAlert = false;
+ char* hitUidToken = NS_strtok(WHITESPACE, &currentPosition);
+ while (hitUidToken) {
+ long naturalLong; // %l is 64 bits on OSF1
+ sscanf(hitUidToken, "%ld", &naturalLong);
+ nsMsgKey hitUid = (nsMsgKey)naturalLong;
+
+ nsCOMPtr<nsIMsgDBHdr> hitHeader;
+ rv = mDatabase->GetMsgHdrForKey(hitUid, getter_AddRefs(hitHeader));
+ if (NS_SUCCEEDED(rv) && hitHeader) {
+ nsCOMPtr<nsIMsgSearchSession> searchSession;
+ nsCOMPtr<nsIMsgSearchAdapter> searchAdapter;
+ aUrl->GetSearchSession(getter_AddRefs(searchSession));
+ if (searchSession) {
+ searchSession->GetRunningAdapter(getter_AddRefs(searchAdapter));
+ if (searchAdapter) searchAdapter->AddResultElement(hitHeader);
+ }
+ } else if (!shownUpdateAlert) {
+ }
+
+ hitUidToken = NS_strtok(WHITESPACE, &currentPosition);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetAppendMsgUid(nsMsgKey aKey, nsIImapUrl* aUrl) {
+ nsresult rv;
+ nsCOMPtr<nsISupports> copyState;
+ if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) {
+ nsCOMPtr<nsImapMailCopyState> mailCopyState =
+ do_QueryInterface(copyState, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ if (mailCopyState->m_undoMsgTxn) // CopyMessages()
+ {
+ RefPtr<nsImapMoveCopyMsgTxn> msgTxn;
+ msgTxn = mailCopyState->m_undoMsgTxn;
+ msgTxn->AddDstKey(aKey);
+ } else if (mailCopyState->m_listener) // CopyFileMessage();
+ // Draft/Template goes here
+ {
+ mailCopyState->m_appendUID = aKey;
+ mailCopyState->m_listener->SetMessageKey(aKey);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetMessageId(nsIImapUrl* aUrl, nsACString& messageId) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsISupports> copyState;
+
+ if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) {
+ nsCOMPtr<nsImapMailCopyState> mailCopyState =
+ do_QueryInterface(copyState, &rv);
+ if (NS_FAILED(rv)) return rv;
+ if (mailCopyState->m_listener)
+ rv = mailCopyState->m_listener->GetMessageId(messageId);
+ }
+ if (NS_SUCCEEDED(rv) && messageId.Length() > 0) {
+ if (messageId.First() == '<') messageId.Cut(0, 1);
+ if (messageId.Last() == '>') messageId.SetLength(messageId.Length() - 1);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::HeaderFetchCompleted(nsIImapProtocol* aProtocol) {
+ nsCOMPtr<nsIMsgWindow>
+ msgWindow; // we might need this for the filter plugins.
+ if (mBackupDatabase) RemoveBackupMsgDatabase();
+
+ SetSizeOnDisk(mFolderSize);
+ int32_t numNewBiffMsgs = 0;
+ if (m_performingBiff) GetNumNewMessages(false, &numNewBiffMsgs);
+
+ bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves();
+ PlaybackCoalescedOperations();
+ if (aProtocol) {
+ // check if we should download message bodies because it's the inbox and
+ // the server is specified as one where where we download msg bodies
+ // automatically. Or if we autosyncing all offline folders.
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ GetImapIncomingServer(getter_AddRefs(imapServer));
+
+ bool autoDownloadNewHeaders = false;
+ bool autoSyncOfflineStores = false;
+
+ if (imapServer) {
+ imapServer->GetAutoSyncOfflineStores(&autoSyncOfflineStores);
+ imapServer->GetDownloadBodiesOnGetNewMail(&autoDownloadNewHeaders);
+ if (m_filterListRequiresBody) autoDownloadNewHeaders = true;
+ }
+ bool notifiedBodies = false;
+ if (m_downloadingFolderForOfflineUse || autoSyncOfflineStores ||
+ autoDownloadNewHeaders) {
+ nsTArray<nsMsgKey> keysToDownload;
+ GetBodysToDownload(&keysToDownload);
+ if (!keysToDownload.IsEmpty() &&
+ (m_downloadingFolderForOfflineUse || autoDownloadNewHeaders)) {
+ // this is the case when DownloadAllForOffline is called.
+ notifiedBodies = true;
+ aProtocol->NotifyBodysToDownload(keysToDownload);
+ } else {
+ // create auto-sync state object lazily
+ InitAutoSyncState();
+ if (MOZ_LOG_TEST(gAutoSyncLog, mozilla::LogLevel::Debug)) {
+ int32_t flags = 0;
+ GetFlags((uint32_t*)&flags);
+ nsString folderName;
+ GetName(folderName);
+ nsCString utfLeafName;
+ CopyUTF16toUTF8(folderName, utfLeafName);
+ MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug,
+ ("%s: foldername=%s, flags=0x%X, "
+ "isOffline=%s, nsMsgFolderFlags::Offline=0x%X",
+ __func__, utfLeafName.get(), flags,
+ (flags & nsMsgFolderFlags::Offline) ? "true" : "false",
+ nsMsgFolderFlags::Offline));
+ MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug,
+ ("%s: created autosync obj, have keys to download=%s",
+ __func__, keysToDownload.IsEmpty() ? "false" : "true"));
+ }
+ // make enough room for new downloads
+ m_autoSyncStateObj->ManageStorageSpace(); // currently a no-op
+
+ m_autoSyncStateObj->SetServerCounts(
+ m_numServerTotalMessages, m_numServerRecentMessages,
+ m_numServerUnseenMessages, m_nextUID);
+ m_autoSyncStateObj->OnNewHeaderFetchCompleted(keysToDownload);
+ }
+ }
+ if (!notifiedBodies) {
+ nsTArray<nsMsgKey> noBodies;
+ aProtocol->NotifyBodysToDownload(noBodies);
+ }
+
+ nsCOMPtr<nsIURI> runningUri;
+ aProtocol->GetRunningUrl(getter_AddRefs(runningUri));
+ if (runningUri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(runningUri);
+ if (mailnewsUrl) mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ }
+ }
+
+ // delay calling plugins if filter application is also delayed
+ if (!m_filterListRequiresBody) {
+ bool filtersRun;
+ CallFilterPlugins(msgWindow, &filtersRun);
+ if (!filtersRun && m_performingBiff && mDatabase && numNewBiffMsgs > 0 &&
+ (!pendingMoves || !ShowPreviewText())) {
+ // If we are performing biff for this folder, tell the
+ // stand-alone biff about the new high water mark
+ // We must ensure that the server knows that we are performing biff.
+ // Otherwise the stand-alone biff won't fire.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server)
+ server->SetPerformingBiff(true);
+
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail);
+ if (server) server->SetPerformingBiff(false);
+ m_performingBiff = false;
+ }
+
+ if (m_filterList) (void)m_filterList->FlushLogIfNecessary();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetBiffStateAndUpdate(nsMsgBiffState biffState) {
+ SetBiffState(biffState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetUidValidity(int32_t* uidValidity) {
+ NS_ENSURE_ARG(uidValidity);
+ if ((int32_t)m_uidValidity == kUidUnknown) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ (void)GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (db) db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+
+ if (dbFolderInfo)
+ dbFolderInfo->GetImapUidValidity((int32_t*)&m_uidValidity);
+ }
+ *uidValidity = m_uidValidity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetUidValidity(int32_t uidValidity) {
+ m_uidValidity = uidValidity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::FillInFolderProps(nsIMsgImapFolderProps* aFolderProps) {
+ NS_ENSURE_ARG(aFolderProps);
+ const char* folderTypeStringID;
+ const char* folderTypeDescStringID = nullptr;
+ const char* folderQuotaStatusStringID;
+ nsString folderType;
+ nsString folderTypeDesc;
+ nsString folderQuotaStatusDesc;
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ // if for some bizarre reason this fails, we'll still fall through to the
+ // normal sharing code
+ if (NS_SUCCEEDED(rv)) {
+ // get the latest committed imap capabilities bit mask.
+ eIMAPCapabilityFlags capability = kCapabilityUndefined;
+ imapServer->GetCapability(&capability);
+ bool haveACL = capability & kACLCapability;
+ bool haveQuota = capability & kQuotaCapability;
+
+ // Figure out what to display in the Quota tab of the folder properties.
+ // Does the server support quotas? This depends on the latest imap
+ // CAPABILITY response.
+ if (haveQuota) {
+ // Have quota capability. Have we asked the server for quota information?
+ if (m_folderQuotaCommandIssued) {
+ // Has the server replied with all the quota info?
+ if (m_folderQuotaDataIsValid) {
+ if (!m_folderQuota.IsEmpty()) {
+ // If so, set quota data to show in the quota tab
+ folderQuotaStatusStringID = nullptr;
+ aFolderProps->SetQuotaData(m_folderQuota);
+ } else {
+ // The server reported no quota limits on this folder.
+ folderQuotaStatusStringID = "imapQuotaStatusNoQuota2";
+ }
+ } else {
+ // The getquotaroot command was sent to the server but the complete
+ // response was not yet received when the folder properties were
+ // requested. This is rare. Request the folder properties again to
+ // obtain the quota data.
+ folderQuotaStatusStringID = "imapQuotaStatusInProgress";
+ }
+ } else {
+ // The folder is not open, so no quota information is available
+ folderQuotaStatusStringID = "imapQuotaStatusFolderNotOpen";
+ }
+ } else {
+ // Either the server doesn't support quotas, or we don't know if it does
+ // (e.g., because we don't have a connection yet). If the latter, we fall
+ // back to saying that no information is available because the folder is
+ // not yet open.
+ folderQuotaStatusStringID = (capability == kCapabilityUndefined)
+ ? "imapQuotaStatusFolderNotOpen"
+ : "imapQuotaStatusNotSupported";
+ }
+
+ if (!folderQuotaStatusStringID) {
+ // Display quota data
+ aFolderProps->ShowQuotaData(true);
+ } else {
+ // Hide quota data and show reason why it is not available
+ aFolderProps->ShowQuotaData(false);
+
+ rv = IMAPGetStringByName(folderQuotaStatusStringID,
+ getter_Copies(folderQuotaStatusDesc));
+ if (NS_SUCCEEDED(rv)) aFolderProps->SetQuotaStatus(folderQuotaStatusDesc);
+ }
+
+ // See if the server supports ACL.
+ // If not, just set the folder description to a string that says
+ // the server doesn't support sharing, and return.
+ if (!haveACL) {
+ rv = IMAPGetStringByName("imapServerDoesntSupportAcl",
+ getter_Copies(folderTypeDesc));
+ if (NS_SUCCEEDED(rv))
+ aFolderProps->SetFolderTypeDescription(folderTypeDesc);
+ aFolderProps->ServerDoesntSupportACL();
+ return NS_OK;
+ }
+ }
+ if (mFlags & nsMsgFolderFlags::ImapPublic) {
+ folderTypeStringID = "imapPublicFolderTypeName";
+ folderTypeDescStringID = "imapPublicFolderTypeDescription";
+ } else if (mFlags & nsMsgFolderFlags::ImapOtherUser) {
+ folderTypeStringID = "imapOtherUsersFolderTypeName";
+ nsCString owner;
+ nsString uniOwner;
+ GetFolderOwnerUserName(owner);
+ if (owner.IsEmpty()) {
+ IMAPGetStringByName(folderTypeStringID, getter_Copies(uniOwner));
+ // Another user's folder, for which we couldn't find an owner name
+ NS_ASSERTION(false, "couldn't get owner name for other user's folder");
+ } else {
+ CopyUTF8toUTF16(owner, uniOwner);
+ }
+ AutoTArray<nsString, 1> params = {uniOwner};
+ bundle->FormatStringFromName("imapOtherUsersFolderTypeDescription", params,
+ folderTypeDesc);
+ } else if (GetFolderACL()->GetIsFolderShared()) {
+ folderTypeStringID = "imapPersonalSharedFolderTypeName";
+ folderTypeDescStringID = "imapPersonalSharedFolderTypeDescription";
+ } else {
+ folderTypeStringID = "imapPersonalSharedFolderTypeName";
+ folderTypeDescStringID = "imapPersonalFolderTypeDescription";
+ }
+
+ rv = IMAPGetStringByName(folderTypeStringID, getter_Copies(folderType));
+ if (NS_SUCCEEDED(rv)) aFolderProps->SetFolderType(folderType);
+
+ if (folderTypeDesc.IsEmpty() && folderTypeDescStringID)
+ IMAPGetStringByName(folderTypeDescStringID, getter_Copies(folderTypeDesc));
+ if (!folderTypeDesc.IsEmpty())
+ aFolderProps->SetFolderTypeDescription(folderTypeDesc);
+
+ nsString rightsString;
+ rv = CreateACLRightsStringForFolder(rightsString);
+ if (NS_SUCCEEDED(rv)) aFolderProps->SetFolderPermissions(rightsString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetAclFlags(uint32_t aclFlags) {
+ nsresult rv = NS_OK;
+ if (m_aclFlags != aclFlags) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ bool dbWasOpen = (mDatabase != nullptr);
+ rv = GetDatabase();
+
+ m_aclFlags = aclFlags;
+ if (mDatabase) {
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (NS_SUCCEEDED(rv) && dbFolderInfo)
+ dbFolderInfo->SetUint32Property("aclFlags", aclFlags);
+ // if setting the acl flags caused us to open the db, release the ref
+ // because on startup, we might get acl on all folders,which will
+ // leave a lot of db's open.
+ if (!dbWasOpen) {
+ mDatabase->Close(true /* commit changes */);
+ mDatabase = nullptr;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetAclFlags(uint32_t* aclFlags) {
+ NS_ENSURE_ARG_POINTER(aclFlags);
+ nsresult rv;
+ ReadDBFolderInfo(false); // update cache first.
+ if (m_aclFlags == kAclInvalid) // -1 means invalid value, so get it from db.
+ {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ bool dbWasOpen = (mDatabase != nullptr);
+ rv = GetDatabase();
+
+ if (mDatabase) {
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (NS_SUCCEEDED(rv) && dbFolderInfo) {
+ rv = dbFolderInfo->GetUint32Property("aclFlags", 0, aclFlags);
+ m_aclFlags = *aclFlags;
+ }
+ // if getting the acl flags caused us to open the db, release the ref
+ // because on startup, we might get acl on all folders,which will
+ // leave a lot of db's open.
+ if (!dbWasOpen) {
+ mDatabase->Close(true /* commit changes */);
+ mDatabase = nullptr;
+ }
+ }
+ } else
+ *aclFlags = m_aclFlags;
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::SetSupportedUserFlags(uint32_t userFlags) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = GetDatabase();
+
+ m_supportedUserFlags = userFlags;
+ if (mDatabase) {
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (NS_SUCCEEDED(rv) && dbFolderInfo)
+ dbFolderInfo->SetUint32Property("imapFlags", userFlags);
+ }
+ return rv;
+}
+
+nsresult nsImapMailFolder::GetSupportedUserFlags(uint32_t* userFlags) {
+ NS_ENSURE_ARG_POINTER(userFlags);
+
+ nsresult rv = NS_OK;
+
+ ReadDBFolderInfo(false); // update cache first.
+ if (m_supportedUserFlags == 0) // 0 means invalid value, so get it from db.
+ {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = GetDatabase();
+
+ if (mDatabase) {
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (NS_SUCCEEDED(rv) && dbFolderInfo) {
+ rv = dbFolderInfo->GetUint32Property("imapFlags", 0, userFlags);
+ m_supportedUserFlags = *userFlags;
+ }
+ }
+ } else
+ *userFlags = m_supportedUserFlags;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetCanOpenFolder(bool* aBool) {
+ NS_ENSURE_ARG_POINTER(aBool);
+ bool noSelect;
+ GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect);
+ *aBool = (noSelect) ? false : GetFolderACL()->GetCanIReadFolder();
+ return NS_OK;
+}
+
+///////// nsMsgIMAPFolderACL class ///////////////////////////////
+
+// This string is defined in the ACL RFC to be "anyone"
+#define IMAP_ACL_ANYONE_STRING "anyone"
+
+nsMsgIMAPFolderACL::nsMsgIMAPFolderACL(nsImapMailFolder* folder)
+ : m_rightsHash(24) {
+ NS_ASSERTION(folder, "need folder");
+ m_folder = folder;
+ m_aclCount = 0;
+ BuildInitialACLFromCache();
+}
+
+nsMsgIMAPFolderACL::~nsMsgIMAPFolderACL() {}
+
+// We cache most of our own rights in the MSG_FOLDER_PREF_* flags
+void nsMsgIMAPFolderACL::BuildInitialACLFromCache() {
+ nsAutoCString myrights;
+
+ uint32_t startingFlags;
+ m_folder->GetAclFlags(&startingFlags);
+
+ if (startingFlags & IMAP_ACL_READ_FLAG) myrights += "r";
+ if (startingFlags & IMAP_ACL_STORE_SEEN_FLAG) myrights += "s";
+ if (startingFlags & IMAP_ACL_WRITE_FLAG) myrights += "w";
+ if (startingFlags & IMAP_ACL_INSERT_FLAG) myrights += "i";
+ if (startingFlags & IMAP_ACL_POST_FLAG) myrights += "p";
+ if (startingFlags & IMAP_ACL_CREATE_SUBFOLDER_FLAG) myrights += "c";
+ if (startingFlags & IMAP_ACL_DELETE_FLAG) myrights += "dt";
+ if (startingFlags & IMAP_ACL_ADMINISTER_FLAG) myrights += "a";
+ if (startingFlags & IMAP_ACL_EXPUNGE_FLAG) myrights += "e";
+
+ if (!myrights.IsEmpty()) SetFolderRightsForUser(EmptyCString(), myrights);
+}
+
+void nsMsgIMAPFolderACL::UpdateACLCache() {
+ uint32_t startingFlags = 0;
+ m_folder->GetAclFlags(&startingFlags);
+
+ if (GetCanIReadFolder())
+ startingFlags |= IMAP_ACL_READ_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_READ_FLAG;
+
+ if (GetCanIStoreSeenInFolder())
+ startingFlags |= IMAP_ACL_STORE_SEEN_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_STORE_SEEN_FLAG;
+
+ if (GetCanIWriteFolder())
+ startingFlags |= IMAP_ACL_WRITE_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_WRITE_FLAG;
+
+ if (GetCanIInsertInFolder())
+ startingFlags |= IMAP_ACL_INSERT_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_INSERT_FLAG;
+
+ if (GetCanIPostToFolder())
+ startingFlags |= IMAP_ACL_POST_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_POST_FLAG;
+
+ if (GetCanICreateSubfolder())
+ startingFlags |= IMAP_ACL_CREATE_SUBFOLDER_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_CREATE_SUBFOLDER_FLAG;
+
+ if (GetCanIDeleteInFolder())
+ startingFlags |= IMAP_ACL_DELETE_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_DELETE_FLAG;
+
+ if (GetCanIAdministerFolder())
+ startingFlags |= IMAP_ACL_ADMINISTER_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_ADMINISTER_FLAG;
+
+ if (GetCanIExpungeFolder())
+ startingFlags |= IMAP_ACL_EXPUNGE_FLAG;
+ else
+ startingFlags &= ~IMAP_ACL_EXPUNGE_FLAG;
+
+ m_folder->SetAclFlags(startingFlags);
+}
+
+bool nsMsgIMAPFolderACL::SetFolderRightsForUser(const nsACString& userName,
+ const nsACString& rights) {
+ nsCString myUserName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ server->GetUsername(myUserName);
+
+ nsAutoCString ourUserName;
+ if (userName.IsEmpty())
+ ourUserName.Assign(myUserName);
+ else
+ ourUserName.Assign(userName);
+
+ if (ourUserName.IsEmpty()) return false;
+
+ ToLowerCase(ourUserName);
+ nsCString oldValue = m_rightsHash.Get(ourUserName);
+ if (!oldValue.IsEmpty()) {
+ m_rightsHash.Remove(ourUserName);
+ m_aclCount--;
+ NS_ASSERTION(m_aclCount >= 0, "acl count can't go negative");
+ }
+ m_aclCount++;
+ m_rightsHash.InsertOrUpdate(ourUserName, PromiseFlatCString(rights));
+
+ if (myUserName.Equals(ourUserName) ||
+ ourUserName.EqualsLiteral(IMAP_ACL_ANYONE_STRING))
+ // if this is setting an ACL for me, cache it in the folder pref flags
+ UpdateACLCache();
+
+ return true;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetOtherUsersWithAccess(
+ nsIUTF8StringEnumerator** aResult) {
+ return GetFolderACL()->GetOtherUsers(aResult);
+}
+
+nsresult nsMsgIMAPFolderACL::GetOtherUsers(nsIUTF8StringEnumerator** aResult) {
+ nsCString myUserName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ server->GetUsername(myUserName);
+
+ // We need to filter out myUserName from m_rightsHash.
+ nsTArray<nsCString>* resultArray = new nsTArray<nsCString>;
+ for (auto iter = m_rightsHash.Iter(); !iter.Done(); iter.Next()) {
+ if (!iter.Key().Equals(myUserName)) resultArray->AppendElement(iter.Key());
+ }
+
+ // enumerator will free resultArray
+ return NS_NewAdoptingUTF8StringEnumerator(aResult, resultArray);
+}
+
+nsresult nsImapMailFolder::GetPermissionsForUser(const nsACString& otherUser,
+ nsACString& aResult) {
+ nsCString str;
+ nsresult rv = GetFolderACL()->GetRightsStringForUser(otherUser, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResult = str;
+ return NS_OK;
+}
+
+nsresult nsMsgIMAPFolderACL::GetRightsStringForUser(
+ const nsACString& inUserName, nsCString& rights) {
+ nsCString userName;
+ userName.Assign(inUserName);
+ if (userName.IsEmpty()) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we need the real user name to match with what the imap server returns
+ // in the acl response.
+ server->GetUsername(userName);
+ }
+ ToLowerCase(userName);
+ rights = m_rightsHash.Get(userName);
+ return NS_OK;
+}
+
+// First looks for individual user; then looks for 'anyone' if the user isn't
+// found. Returns defaultIfNotFound, if neither are found.
+bool nsMsgIMAPFolderACL::GetFlagSetInRightsForUser(const nsACString& userName,
+ char flag,
+ bool defaultIfNotFound) {
+ nsCString flags;
+ nsresult rv = GetRightsStringForUser(userName, flags);
+ NS_ENSURE_SUCCESS(rv, defaultIfNotFound);
+ if (flags.IsEmpty()) {
+ nsCString anyoneFlags;
+ GetRightsStringForUser(nsLiteralCString(IMAP_ACL_ANYONE_STRING),
+ anyoneFlags);
+ if (anyoneFlags.IsEmpty()) return defaultIfNotFound;
+ return (anyoneFlags.FindChar(flag) != kNotFound);
+ }
+ return (flags.FindChar(flag) != kNotFound);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserLookupFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'l', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserReadFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'r', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserStoreSeenInFolder(
+ const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 's', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserWriteFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'w', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserInsertInFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'i', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserPostToFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'p', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserCreateSubfolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'c', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserDeleteInFolder(const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'd', false) ||
+ GetFlagSetInRightsForUser(userName, 't', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanUserAdministerFolder(
+ const nsACString& userName) {
+ return GetFlagSetInRightsForUser(userName, 'a', false);
+}
+
+bool nsMsgIMAPFolderACL::GetCanILookupFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'l', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIReadFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'r', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIStoreSeenInFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 's', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIWriteFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'w', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIInsertInFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'i', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIPostToFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'p', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanICreateSubfolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'c', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIDeleteInFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'd', true) ||
+ GetFlagSetInRightsForUser(EmptyCString(), 't', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIAdministerFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'a', true);
+}
+
+bool nsMsgIMAPFolderACL::GetCanIExpungeFolder() {
+ return GetFlagSetInRightsForUser(EmptyCString(), 'e', true) ||
+ GetFlagSetInRightsForUser(EmptyCString(), 'd', true);
+}
+
+// We use this to see if the ACLs think a folder is shared or not.
+// We will define "Shared" in 5.0 to mean:
+// At least one user other than the currently authenticated user has at least
+// one explicitly-listed ACL right on that folder.
+bool nsMsgIMAPFolderACL::GetIsFolderShared() {
+ // If we have more than one ACL count for this folder, which means that
+ // someone other than ourself has rights on it, then it is "shared."
+ if (m_aclCount > 1) return true;
+
+ // Or, if "anyone" has rights to it, it is shared.
+ nsCString anyonesRights =
+ m_rightsHash.Get(nsLiteralCString(IMAP_ACL_ANYONE_STRING));
+ return (!anyonesRights.IsEmpty());
+}
+
+bool nsMsgIMAPFolderACL::GetDoIHaveFullRightsForFolder() {
+ return (GetCanIReadFolder() && GetCanIWriteFolder() &&
+ GetCanIInsertInFolder() && GetCanIAdministerFolder() &&
+ GetCanICreateSubfolder() && GetCanIDeleteInFolder() &&
+ GetCanILookupFolder() && GetCanIStoreSeenInFolder() &&
+ GetCanIExpungeFolder() && GetCanIPostToFolder());
+}
+
+// Returns a newly allocated string describing these rights
+nsresult nsMsgIMAPFolderACL::CreateACLRightsString(nsAString& aRightsString) {
+ nsString curRight;
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (GetDoIHaveFullRightsForFolder()) {
+ nsAutoString result;
+ rv = bundle->GetStringFromName("imapAclFullRights", result);
+ aRightsString.Assign(result);
+ return rv;
+ }
+
+ if (GetCanIReadFolder()) {
+ bundle->GetStringFromName("imapAclReadRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIWriteFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclWriteRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIInsertInFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclInsertRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanILookupFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclLookupRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIStoreSeenInFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclSeenRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIDeleteInFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclDeleteRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIExpungeFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclExpungeRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanICreateSubfolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclCreateRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIPostToFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclPostRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ if (GetCanIAdministerFolder()) {
+ if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", ");
+ bundle->GetStringFromName("imapAclAdministerRight", curRight);
+ aRightsString.Append(curRight);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFilePath(nsIFile** aPathName) {
+ // this will return a copy of mPath, which is what we want.
+ // this will also initialize mPath using parseURI if it isn't already done
+ return nsMsgDBFolder::GetFilePath(aPathName);
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFilePath(nsIFile* aPathName) {
+ return nsMsgDBFolder::SetFilePath(
+ aPathName); // call base class so mPath will get set
+}
+
+nsresult nsImapMailFolder::DisplayStatusMsg(nsIImapUrl* aImapUrl,
+ const nsAString& msg) {
+ nsCOMPtr<nsIImapMockChannel> mockChannel;
+ aImapUrl->GetMockChannel(getter_AddRefs(mockChannel));
+ if (mockChannel) {
+ nsCOMPtr<nsIProgressEventSink> progressSink;
+ mockChannel->GetProgressEventSink(getter_AddRefs(progressSink));
+ if (progressSink) {
+ progressSink->OnStatus(mockChannel, NS_OK,
+ PromiseFlatString(msg).get()); // XXX i18n message
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::ProgressStatusString(nsIImapProtocol* aProtocol,
+ const char* aMsgName,
+ const char16_t* extraInfo) {
+ nsString progressMsg;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ nsCOMPtr<nsIImapServerSink> serverSink = do_QueryInterface(server);
+ if (serverSink) serverSink->GetImapStringByName(aMsgName, progressMsg);
+ }
+ if (progressMsg.IsEmpty())
+ IMAPGetStringByName(aMsgName, getter_Copies(progressMsg));
+
+ if (aProtocol && !progressMsg.IsEmpty()) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl));
+ if (imapUrl) {
+ if (extraInfo) {
+ nsString printfString;
+ nsTextFormatter::ssprintf(printfString, progressMsg.get(), extraInfo);
+ progressMsg = printfString;
+ }
+
+ DisplayStatusMsg(imapUrl, progressMsg);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::PercentProgress(nsIImapProtocol* aProtocol,
+ nsACString const& aFmtStringName,
+ nsAString const& aMailboxName,
+ int64_t aCurrentProgress,
+ int64_t aMaxProgress) {
+ if (aProtocol) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl));
+ if (imapUrl) {
+ nsCOMPtr<nsIImapMockChannel> mockChannel;
+ imapUrl->GetMockChannel(getter_AddRefs(mockChannel));
+ if (mockChannel) {
+ nsCOMPtr<nsIProgressEventSink> progressSink;
+ mockChannel->GetProgressEventSink(getter_AddRefs(progressSink));
+ if (progressSink) {
+ progressSink->OnProgress(mockChannel, aCurrentProgress, aMaxProgress);
+
+ if (!aFmtStringName.IsEmpty()) {
+ // There's a progress message to format (the progress messages are
+ // all localized and expect three params).
+ nsAutoString current;
+ current.AppendInt(aCurrentProgress);
+ nsAutoString expected;
+ expected.AppendInt(aMaxProgress);
+ nsAutoString mailbox(aMailboxName);
+ AutoTArray<nsString, 3> params = {current, expected, mailbox};
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString progressText;
+ rv = bundle->FormatStringFromName(
+ PromiseFlatCString(aFmtStringName).get(), params, progressText);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!progressText.IsEmpty()) {
+ progressSink->OnStatus(mockChannel, NS_OK, progressText.get());
+ }
+ }
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::CopyNextStreamMessage(bool copySucceeded,
+ nsISupports* copyState) {
+ // if copy has failed it could be either user interrupted it or for some other
+ // reason don't do any subsequent copies or delete src messages if it is move
+ if (!copySucceeded) return NS_OK;
+ nsresult rv;
+ nsCOMPtr<nsImapMailCopyState> mailCopyState =
+ do_QueryInterface(copyState, &rv);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("QI copyState failed: %" PRIx32, static_cast<uint32_t>(rv)));
+ return rv; // this can fail...
+ }
+
+ if (!mailCopyState->m_streamCopy) return NS_OK;
+
+ uint32_t idx = mailCopyState->m_curIndex;
+ if (mailCopyState->m_isMove && idx) {
+ nsCOMPtr<nsIMsgFolder> srcFolder(
+ do_QueryInterface(mailCopyState->m_srcSupport, &rv));
+ if (NS_SUCCEEDED(rv) && srcFolder) {
+ // Create "array" of one message header to delete
+ idx--;
+ if (idx < mailCopyState->m_messages.Length()) {
+ RefPtr<nsIMsgDBHdr> msg = mailCopyState->m_messages[idx];
+ srcFolder->DeleteMessages({msg}, nullptr, true, true, nullptr, false);
+ }
+ }
+ }
+
+ if (mailCopyState->m_curIndex < mailCopyState->m_messages.Length()) {
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyNextStreamMessage: %s %u of %u",
+ mailCopyState->m_isMove ? "Moving" : "Copying",
+ mailCopyState->m_curIndex,
+ (uint32_t)mailCopyState->m_messages.Length()));
+ nsIMsgDBHdr* message = mailCopyState->m_messages[mailCopyState->m_curIndex];
+ bool isRead;
+ message->GetIsRead(&isRead);
+ mailCopyState->m_unreadCount = (isRead) ? 0 : 1;
+ rv = CopyStreamMessage(message, this, mailCopyState->m_msgWindow,
+ mailCopyState->m_isMove);
+ } else {
+ // Notify of move/copy completion in case we have some source headers
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier && !mailCopyState->m_messages.IsEmpty()) {
+ notifier->NotifyMsgsMoveCopyCompleted(
+ mailCopyState->m_isMove, mailCopyState->m_messages, this, {});
+ }
+ if (mailCopyState->m_isMove) {
+ nsCOMPtr<nsIMsgFolder> srcFolder(
+ do_QueryInterface(mailCopyState->m_srcSupport, &rv));
+ if (NS_SUCCEEDED(rv) && srcFolder) {
+ // we want to send this notification now that the source messages have
+ // been deleted.
+ nsCOMPtr<nsIMsgLocalMailFolder> popFolder(do_QueryInterface(srcFolder));
+ if (popFolder) // needed if move pop->imap to notify FE
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ }
+ }
+ }
+ if (NS_FAILED(rv)) (void)OnCopyCompleted(mailCopyState->m_srcSupport, rv);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetUrlState(nsIImapProtocol* aProtocol,
+ nsIMsgMailNewsUrl* aUrl, bool isRunning,
+ bool aSuspend, nsresult statusCode) {
+ // If we have no path, then the folder has been shutdown, and there's
+ // no point in doing anything...
+ if (!mPath) return NS_OK;
+ if (!isRunning) {
+ ProgressStatusString(aProtocol, "imapDone", nullptr);
+ m_urlRunning = false;
+ // if no protocol, then we're reading from the mem or disk cache
+ // and we don't want to end the offline download just yet.
+ if (aProtocol) {
+ EndOfflineDownload();
+ m_downloadingFolderForOfflineUse = false;
+ }
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl));
+ if (imapUrl) {
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+ // if the server doesn't support copyUID, then SetCopyResponseUid won't
+ // get called, so we need to clear m_pendingOfflineMoves when the online
+ // move operation has finished.
+ if (imapAction == nsIImapUrl::nsImapOnlineMove)
+ m_pendingOfflineMoves.Clear();
+ }
+ }
+ if (aUrl && !aSuspend) return aUrl->SetUrlState(isRunning, statusCode);
+ return statusCode;
+}
+
+// used when copying from local mail folder, or other imap server)
+nsresult nsImapMailFolder::CopyMessagesWithStream(
+ nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, bool isCrossServerOp, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener, bool allowUndo) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ nsresult rv;
+ rv = InitCopyState(srcFolder, messages, isMove, false, isCrossServerOp, 0,
+ EmptyCString(), listener, msgWindow, allowUndo);
+ if (NS_FAILED(rv)) return rv;
+
+ m_copyState->m_streamCopy = true;
+
+ // ** jt - needs to create server to server move/copy undo msg txn
+ if (m_copyState->m_allowUndo) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> srcKeyArray;
+ rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray);
+
+ RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn;
+
+ if (!undoMsgTxn ||
+ NS_FAILED(undoMsgTxn->Init(srcFolder, &srcKeyArray, messageIds.get(),
+ this, true, isMove)))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ if (isMove) {
+ if (mFlags & nsMsgFolderFlags::Trash)
+ undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg);
+ else
+ undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ } else
+ undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ m_copyState->m_undoMsgTxn = undoMsgTxn;
+ }
+ if (NS_SUCCEEDED(rv)) CopyStreamMessage(messages[0], this, msgWindow, isMove);
+ return rv; // we are clearing copy state in CopyMessages on failure
+}
+
+nsresult nsImapMailFolder::GetClearedOriginalOp(
+ nsIMsgOfflineImapOperation* op, nsIMsgOfflineImapOperation** originalOp,
+ nsIMsgDatabase** originalDB) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> returnOp;
+ nsOfflineImapOperationType opType;
+ op->GetOperation(&opType);
+ NS_ASSERTION(opType & nsIMsgOfflineImapOperation::kMoveResult,
+ "not an offline move op");
+
+ nsCString sourceFolderURI;
+ op->GetSourceFolderURI(sourceFolderURI);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> sourceFolder;
+ rv = GetOrCreateFolder(sourceFolderURI, getter_AddRefs(sourceFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ sourceFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), originalDB);
+ if (*originalDB) {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(*originalDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey originalKey;
+ op->GetMessageKey(&originalKey);
+ rv =
+ opsDb->GetOfflineOpForKey(originalKey, false, getter_AddRefs(returnOp));
+ if (NS_SUCCEEDED(rv) && returnOp) {
+ nsCString moveDestination;
+ nsCString thisFolderURI;
+ GetURI(thisFolderURI);
+ returnOp->GetDestinationFolderURI(moveDestination);
+ if (moveDestination.Equals(thisFolderURI))
+ returnOp->ClearOperation(nsIMsgOfflineImapOperation::kMoveResult);
+ }
+ }
+ returnOp.forget(originalOp);
+ return rv;
+}
+
+nsresult nsImapMailFolder::GetOriginalOp(
+ nsIMsgOfflineImapOperation* op, nsIMsgOfflineImapOperation** originalOp,
+ nsIMsgDatabase** originalDB) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> returnOp;
+ nsCString sourceFolderURI;
+ op->GetSourceFolderURI(sourceFolderURI);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> sourceFolder;
+ rv = GetOrCreateFolder(sourceFolderURI, getter_AddRefs(sourceFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ sourceFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), originalDB);
+ if (*originalDB) {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(*originalDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey originalKey;
+ op->GetMessageKey(&originalKey);
+ rv =
+ opsDb->GetOfflineOpForKey(originalKey, false, getter_AddRefs(returnOp));
+ }
+ returnOp.forget(originalOp);
+ return rv;
+}
+
+nsresult nsImapMailFolder::FindOpenRange(nsMsgKey& fakeBase,
+ uint32_t srcCount) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey newBase = fakeBase - 1;
+ uint32_t freeCount = 0;
+ while (freeCount != srcCount && newBase > 0) {
+ bool containsKey;
+ if (NS_SUCCEEDED(mDatabase->ContainsKey(newBase, &containsKey)) &&
+ !containsKey)
+ freeCount++;
+ else
+ freeCount = 0;
+ newBase--;
+ }
+ if (!newBase) return NS_ERROR_FAILURE;
+ fakeBase = newBase;
+ return NS_OK;
+}
+
+// Helper to synchronously copy a message from one msgStore to another.
+static nsresult CopyStoreMessage(nsIMsgDBHdr* srcHdr, nsIMsgDBHdr* destHdr,
+ uint64_t& bytesCopied) {
+ nsresult rv;
+
+ // Boilerplate setup.
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ rv = srcHdr->GetFolder(getter_AddRefs(srcFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> destFolder;
+ rv = destHdr->GetFolder(getter_AddRefs(destFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> destStore;
+ rv = destFolder->GetMsgStore(getter_AddRefs(destStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copy message into the msgStore.
+ nsCOMPtr<nsIInputStream> srcStream;
+ rv = srcFolder->GetLocalMsgStream(srcHdr, getter_AddRefs(srcStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIOutputStream> destStream;
+ rv = destFolder->GetOfflineStoreOutputStream(destHdr,
+ getter_AddRefs(destStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SyncCopyStream(srcStream, destStream, bytesCopied);
+ if (NS_SUCCEEDED(rv)) {
+ rv = destStore->FinishNewMessage(destStream, destHdr);
+ } else {
+ destStore->DiscardNewMessage(destStream, destHdr);
+ }
+ return rv;
+}
+
+// This imap folder is the destination of an offline move/copy.
+// We are either offline, or doing a pseudo-offline delete (where we do an
+// offline delete, load the next message, then playback the offline delete).
+nsresult nsImapMailFolder::CopyMessagesOffline(
+ nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener) {
+ nsresult rv;
+ nsresult stopit = NS_OK;
+ nsCOMPtr<nsIMsgDatabase> sourceMailDB;
+ nsCOMPtr<nsIDBFolderInfo> srcDbFolderInfo;
+ srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(srcDbFolderInfo),
+ getter_AddRefs(sourceMailDB));
+ bool deleteToTrash = false;
+ bool deleteImmediately = false;
+ uint32_t srcCount = messages.Length();
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsCopied;
+ nsTArray<RefPtr<nsIMsgDBHdr>> destMsgHdrs;
+
+ if (NS_SUCCEEDED(rv) && imapServer) {
+ nsMsgImapDeleteModel deleteModel;
+ imapServer->GetDeleteModel(&deleteModel);
+ deleteToTrash = (deleteModel == nsMsgImapDeleteModels::MoveToTrash);
+ deleteImmediately = (deleteModel == nsMsgImapDeleteModels::DeleteNoTrash);
+ }
+
+ // This array is used only when we are actually removing the messages from the
+ // source database.
+ nsTArray<nsMsgKey> keysToDelete(
+ (isMove && (deleteToTrash || deleteImmediately)) ? srcCount : 0);
+
+ if (sourceMailDB) {
+ // save the future ops in the source DB, if this is not a imap->local
+ // copy/move
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ if (msgWindow) msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) txnMgr->BeginBatch(nullptr);
+ nsCOMPtr<nsIMsgDatabase> database;
+ GetMsgDatabase(getter_AddRefs(database));
+ if (database) {
+ // get the highest key in the dest db, so we can make up our fake keys
+ nsMsgKey fakeBase = 1;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = database->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey highWaterMark = nsMsgKey_None;
+ folderInfo->GetHighWater(&highWaterMark);
+ fakeBase += highWaterMark;
+ nsMsgKey fakeTop = fakeBase + srcCount;
+ // Check that we have enough room for the fake headers. If fakeTop
+ // is <= highWaterMark, we've overflowed.
+ if (fakeTop <= highWaterMark || fakeTop == nsMsgKey_None) {
+ rv = FindOpenRange(fakeBase, srcCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // N.B. We must not return out of the for loop - we need the matching
+ // end notifications to be sent.
+ // We don't need to acquire the semaphor since this is synchronous
+ // on the UI thread but we should check if the offline store is locked.
+ bool isLocked;
+ GetLocked(&isLocked);
+ nsTArray<nsMsgKey> addedKeys;
+ nsTArray<nsMsgKey> srcKeyArray;
+ nsCOMArray<nsIMsgDBHdr> addedHdrs;
+ nsCOMArray<nsIMsgDBHdr> srcMsgs;
+ nsOfflineImapOperationType moveCopyOpType;
+ nsOfflineImapOperationType deleteOpType =
+ nsIMsgOfflineImapOperation::kDeletedMsg;
+ if (!deleteToTrash)
+ deleteOpType = nsIMsgOfflineImapOperation::kMsgMarkedDeleted;
+ nsCString messageIds;
+ rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray);
+ // put fake message in destination db, delete source if move
+ EnableNotifications(nsIMsgFolder::allMessageCountNotifications, false);
+ nsCString originalSrcFolderURI;
+ srcFolder->GetURI(originalSrcFolderURI);
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(sourceMailDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t sourceKeyIndex = 0;
+ NS_SUCCEEDED(stopit) && (sourceKeyIndex < srcCount);
+ sourceKeyIndex++) {
+ bool messageReturningHome = false;
+ RefPtr<nsIMsgDBHdr> message = messages[sourceKeyIndex];
+ nsMsgKey originalKey;
+ if (message) {
+ rv = message->GetMessageKey(&originalKey);
+ } else {
+ NS_ERROR("bad msg in src array");
+ continue;
+ }
+ nsCOMPtr<nsIMsgOfflineImapOperation> sourceOp;
+ rv = opsDb->GetOfflineOpForKey(originalKey, true,
+ getter_AddRefs(sourceOp));
+ if (NS_SUCCEEDED(rv) && sourceOp) {
+ srcFolder->SetFlag(nsMsgFolderFlags::OfflineEvents);
+ nsCOMPtr<nsIMsgDatabase> originalDB;
+ nsOfflineImapOperationType opType;
+ sourceOp->GetOperation(&opType);
+ // if we already have an offline op for this key, then we need to see
+ // if it was moved into the source folder while offline
+ if (opType ==
+ nsIMsgOfflineImapOperation::kMoveResult) // offline move
+ {
+ // gracious me, we are moving something we already moved while
+ // offline! find the original operation and clear it!
+ nsCOMPtr<nsIMsgOfflineImapOperation> originalOp;
+ GetClearedOriginalOp(sourceOp, getter_AddRefs(originalOp),
+ getter_AddRefs(originalDB));
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDbOriginal =
+ do_QueryInterface(originalDB, &rv);
+ if (NS_SUCCEEDED(rv) && originalOp) {
+ nsCString srcFolderURI;
+ srcFolder->GetURI(srcFolderURI);
+ sourceOp->GetSourceFolderURI(originalSrcFolderURI);
+ sourceOp->GetMessageKey(&originalKey);
+ if (isMove) opsDb->RemoveOfflineOp(sourceOp);
+ sourceOp = originalOp;
+ if (originalSrcFolderURI.Equals(srcFolderURI)) {
+ messageReturningHome = true;
+ opsDbOriginal->RemoveOfflineOp(originalOp);
+ }
+ }
+ }
+ if (!messageReturningHome) {
+ nsCString folderURI;
+ GetURI(folderURI);
+ if (isMove) {
+ uint32_t msgSize;
+ uint32_t msgFlags;
+ imapMessageFlagsType newImapFlags = 0;
+ message->GetMessageSize(&msgSize);
+ message->GetFlags(&msgFlags);
+ sourceOp->SetDestinationFolderURI(folderURI); // offline move
+ sourceOp->SetOperation(nsIMsgOfflineImapOperation::kMsgMoved);
+ sourceOp->SetMsgSize(msgSize);
+ newImapFlags = msgFlags & 0x7;
+ if (msgFlags & nsMsgMessageFlags::Forwarded)
+ newImapFlags |= kImapMsgForwardedFlag;
+ sourceOp->SetNewFlags(newImapFlags);
+ } else {
+ sourceOp->AddMessageCopyOperation(folderURI); // offline copy
+ }
+
+ sourceOp->GetOperation(&moveCopyOpType);
+ srcMsgs.AppendObject(message);
+ }
+ } else {
+ stopit = NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> mailHdr;
+ rv =
+ sourceMailDB->GetMsgHdrForKey(originalKey, getter_AddRefs(mailHdr));
+
+ if (NS_SUCCEEDED(rv) && mailHdr) {
+ // Copy the DB hdr into the destination folder.
+ bool successfulCopy = false;
+ nsMsgKey srcDBhighWaterMark;
+ srcDbFolderInfo->GetHighWater(&srcDBhighWaterMark);
+
+ nsCOMPtr<nsIMsgDBHdr> newMailHdr;
+ rv = database->CopyHdrFromExistingHdr(fakeBase + sourceKeyIndex,
+ mailHdr, true,
+ getter_AddRefs(newMailHdr));
+ if (!newMailHdr || NS_FAILED(rv)) {
+ NS_ASSERTION(false, "failed to copy hdr");
+ stopit = rv;
+ }
+
+ if (NS_SUCCEEDED(stopit)) {
+ bool hasMsgOffline = false;
+
+ destMsgHdrs.AppendElement(newMailHdr);
+ srcFolder->HasMsgOffline(originalKey, &hasMsgOffline);
+ newMailHdr->SetUint32Property("pseudoHdr", 1);
+
+ if (hasMsgOffline && !isLocked) {
+ uint64_t bytesCopied;
+ stopit = CopyStoreMessage(mailHdr, newMailHdr, bytesCopied);
+ if (NS_SUCCEEDED(stopit)) {
+ uint32_t unused;
+ newMailHdr->OrFlags(nsMsgMessageFlags::Offline, &unused);
+ newMailHdr->SetOfflineMessageSize(bytesCopied);
+ }
+ } else {
+ database->MarkOffline(fakeBase + sourceKeyIndex, false, nullptr);
+ }
+
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(database, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgOfflineImapOperation> destOp;
+ opsDb->GetOfflineOpForKey(fakeBase + sourceKeyIndex, true,
+ getter_AddRefs(destOp));
+ if (destOp) {
+ // check if this is a move back to the original mailbox, in which
+ // case we just delete the offline operation.
+ if (messageReturningHome) {
+ opsDb->RemoveOfflineOp(destOp);
+ } else {
+ SetFlag(nsMsgFolderFlags::OfflineEvents);
+ destOp->SetSourceFolderURI(originalSrcFolderURI);
+ destOp->SetSrcMessageKey(originalKey);
+ addedKeys.AppendElement(fakeBase + sourceKeyIndex);
+ addedHdrs.AppendObject(newMailHdr);
+ }
+ } else {
+ stopit = NS_ERROR_FAILURE;
+ }
+ }
+ successfulCopy = NS_SUCCEEDED(stopit);
+ nsMsgKey msgKey;
+ mailHdr->GetMessageKey(&msgKey);
+ if (isMove && successfulCopy) {
+ if (deleteToTrash || deleteImmediately)
+ keysToDelete.AppendElement(msgKey);
+ else
+ sourceMailDB->MarkImapDeleted(msgKey, true,
+ nullptr); // offline delete
+ }
+ if (successfulCopy) {
+ // This is for both moves and copies
+ msgHdrsCopied.AppendElement(mailHdr);
+ }
+ }
+ } // End message loop.
+ EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true);
+ RefPtr<nsImapOfflineTxn> addHdrMsgTxn = new nsImapOfflineTxn(
+ this, &addedKeys, nullptr, this, isMove,
+ nsIMsgOfflineImapOperation::kAddedHeader, addedHdrs);
+ if (addHdrMsgTxn && txnMgr) txnMgr->DoTransaction(addHdrMsgTxn);
+ RefPtr<nsImapOfflineTxn> undoMsgTxn =
+ new nsImapOfflineTxn(srcFolder, &srcKeyArray, messageIds.get(), this,
+ isMove, moveCopyOpType, srcMsgs);
+ if (undoMsgTxn) {
+ if (isMove) {
+ undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ nsCOMPtr<nsIMsgImapMailFolder> srcIsImap(
+ do_QueryInterface(srcFolder));
+ // remember this undo transaction so we can hook up the result
+ // msg ids in the undo transaction.
+ if (srcIsImap) {
+ nsImapMailFolder* srcImapFolder =
+ static_cast<nsImapMailFolder*>(srcFolder);
+ srcImapFolder->m_pendingOfflineMoves.AppendElement(undoMsgTxn);
+ }
+ } else {
+ undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ }
+ // we're adding this undo action before the delete is successful. This
+ // is evil, but 4.5 did it as well.
+ if (txnMgr) txnMgr->DoTransaction(undoMsgTxn);
+ }
+ undoMsgTxn =
+ new nsImapOfflineTxn(srcFolder, &srcKeyArray, messageIds.get(), this,
+ isMove, deleteOpType, srcMsgs);
+ if (undoMsgTxn) {
+ if (isMove) {
+ if (mFlags & nsMsgFolderFlags::Trash) {
+ undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg);
+ } else {
+ undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ }
+ } else {
+ undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ }
+ if (txnMgr) txnMgr->DoTransaction(undoMsgTxn);
+ }
+
+ if (isMove) sourceMailDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ database->Commit(nsMsgDBCommitType::kLargeCommit);
+ SummaryChanged();
+ srcFolder->SummaryChanged();
+ }
+ if (txnMgr) txnMgr->EndBatch(false);
+ }
+
+ if (!msgHdrsCopied.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgsMoveCopyCompleted(isMove, msgHdrsCopied, this,
+ destMsgHdrs);
+ }
+ }
+
+ // NOTE (Bug 1787963):
+ // If we're performing a move, by rights we should be deleting the source
+ // message(s) here. But that would mean they won't be available when we try
+ // to run the offline move operation once we're back online. So we'll just
+ // leave things as they are:
+ // - the message(s) copied into the destination folder
+ // - the original message(s) left in the source folder
+ // - the offline move operation all queued up for when we go back online
+ // When we do go back online, the offline move op will be performed and
+ // the source message(s) will be deleted. For real.
+ // Would be nice to have some marker to hide or grey out messages which are
+ // in this state of impending doom... but it's a pretty obscure corner case
+ // and we've already got quite enough of those.
+ //
+ // BUT... CopyMessagesOffline() is also used when online (ha!), *if* we're
+ // copying between folders on the same nsIMsgIncomingServer, in order to
+ // support undo. In that case we _do_ want to go ahead with the delete now.
+
+ bool sameServer;
+ rv = IsOnSameServer(srcFolder, this, &sameServer);
+
+ if (NS_SUCCEEDED(rv) && sameServer && isMove &&
+ (deleteToTrash || deleteImmediately)) {
+ DeleteStoreMessages(keysToDelete, srcFolder);
+ srcFolder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ false);
+ sourceMailDB->DeleteMessages(keysToDelete, nullptr);
+ srcFolder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ true);
+ }
+ nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder);
+ OnCopyCompleted(srcSupport, rv);
+
+ if (isMove) {
+ srcFolder->NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+ }
+ return rv;
+}
+
+void nsImapMailFolder::SetPendingAttributes(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool aIsMove,
+ bool aSetOffline) {
+ GetDatabase();
+ if (!mDatabase) return;
+
+ uint32_t supportedUserFlags;
+ GetSupportedUserFlags(&supportedUserFlags);
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCString dontPreserve;
+
+ // These preferences exist so that extensions can control which properties
+ // are preserved in the database when a message is moved or copied. All
+ // properties are preserved except those listed in these preferences
+ if (aIsMove)
+ prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnMove",
+ dontPreserve);
+ else
+ prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnCopy",
+ dontPreserve);
+
+ // We'll add spaces at beginning and end so we can search for space-name-space
+ nsCString dontPreserveEx(" "_ns);
+ dontPreserveEx.Append(dontPreserve);
+ dontPreserveEx.Append(' ');
+
+ // these properties are set as integers below, so don't set them again
+ // in the iteration through the properties
+ dontPreserveEx.AppendLiteral(
+ "offlineMsgSize msgOffset flags priority pseudoHdr ");
+
+ // these fields are either copied separately when the server does not support
+ // custom IMAP flags, or managed directly through the flags
+ dontPreserveEx.AppendLiteral("keywords label ");
+
+ // check if any msg hdr has special flags or properties set
+ // that we need to set on the dest hdr
+ for (auto msgDBHdr : messages) {
+ if (!(supportedUserFlags & kImapMsgSupportUserFlag)) {
+ nsCString keywords;
+ msgDBHdr->GetStringProperty("keywords", keywords);
+ if (!keywords.IsEmpty())
+ mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "keywords",
+ keywords.get());
+ }
+
+ nsTArray<nsCString> properties;
+ nsresult rv = msgDBHdr->GetProperties(properties);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCString sourceString;
+ for (auto property : properties) {
+ nsAutoCString propertyEx(" "_ns);
+ propertyEx.Append(property);
+ propertyEx.Append(' ');
+ if (dontPreserveEx.Find(propertyEx) != kNotFound) continue;
+
+ nsCString sourceString;
+ msgDBHdr->GetStringProperty(property.get(), sourceString);
+ mDatabase->SetAttributeOnPendingHdr(msgDBHdr, property.get(),
+ sourceString.get());
+ }
+
+ // Carry over HasRe flag.
+ uint32_t flags;
+ uint32_t storeFlags = 0;
+ msgDBHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::HasRe) {
+ storeFlags = nsMsgMessageFlags::HasRe;
+ mDatabase->SetUint32AttributeOnPendingHdr(msgDBHdr, "flags", storeFlags);
+ }
+
+ uint32_t messageSize;
+ uint64_t messageOffset;
+ nsCString storeToken;
+ msgDBHdr->GetMessageOffset(&messageOffset);
+ msgDBHdr->GetOfflineMessageSize(&messageSize);
+ msgDBHdr->GetStringProperty("storeToken", storeToken);
+ if (messageSize) {
+ mDatabase->SetUint32AttributeOnPendingHdr(msgDBHdr, "offlineMsgSize",
+ messageSize);
+ mDatabase->SetUint64AttributeOnPendingHdr(msgDBHdr, "msgOffset",
+ messageOffset);
+ // Not always setting "flags" attribute to nsMsgMessageFlags::Offline
+ // here because it can cause missing parts (inline or attachments)
+ // when messages are moved or copied manually or by filter action.
+ if (aSetOffline)
+ mDatabase->SetUint32AttributeOnPendingHdr(
+ msgDBHdr, "flags", storeFlags | nsMsgMessageFlags::Offline);
+ mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "storeToken",
+ storeToken.get());
+ }
+ nsMsgPriorityValue priority;
+ msgDBHdr->GetPriority(&priority);
+ if (priority != 0) {
+ nsAutoCString priorityStr;
+ priorityStr.AppendInt(priority);
+ mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "priority",
+ priorityStr.get());
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::CopyMessages(
+ nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener,
+ bool isFolder, // isFolder for future use when we do cross-server folder
+ // move/copy
+ bool allowUndo) {
+ UpdateTimestamps(allowUndo);
+
+ nsresult rv;
+ nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder);
+
+ bool sameServer;
+ rv = IsOnSameServer(srcFolder, this, &sameServer);
+ if (NS_FAILED(rv)) goto done;
+
+ // in theory, if allowUndo is true, then this is a user initiated
+ // action, and we should do it pseudo-offline. If it's not
+ // user initiated (e.g., mail filters firing), then allowUndo is
+ // false, and we should just do the action.
+ if (!WeAreOffline() && sameServer && allowUndo) {
+ // complete the copy operation as in offline mode
+ rv = CopyMessagesOffline(srcFolder, messages, isMove, msgWindow, listener);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "error offline copy");
+ // We'll warn if this fails, but we should still try to play back
+ // offline ops, because it's possible the copy got far enough to
+ // create the offline ops.
+
+ // We make sure that the source folder is an imap folder by limiting
+ // pseudo-offline operations to the same imap server. If we extend the code
+ // to cover non imap folders in the future (i.e. imap folder->local folder),
+ // then the following downcast will cause either a crash or compiler error.
+ // Do not forget to change it accordingly.
+ nsImapMailFolder* srcImapFolder = static_cast<nsImapMailFolder*>(srcFolder);
+
+ // if there is no pending request, create a new one, and set the timer.
+ // Otherwise use the existing one to reset the timer. it is callback
+ // function's responsibility to delete the new request object
+ if (!srcImapFolder->m_pendingPlaybackReq) {
+ srcImapFolder->m_pendingPlaybackReq =
+ new nsPlaybackRequest(srcImapFolder, msgWindow);
+ }
+
+ // Create and start a new playback one-shot timer. If there is already a
+ // timer created that has not timed out, cancel it.
+ if (srcImapFolder->m_playbackTimer)
+ srcImapFolder->m_playbackTimer->Cancel();
+ rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(srcImapFolder->m_playbackTimer), PlaybackTimerCallback,
+ (void*)srcImapFolder->m_pendingPlaybackReq,
+ PLAYBACK_TIMER_INTERVAL_IN_MS, nsITimer::TYPE_ONE_SHOT,
+ "nsImapMailFolder::PlaybackTimerCallback", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start m_playbackTimer timer");
+ }
+ return rv;
+ } else {
+ // sort the message array by key
+
+ nsTArray<nsMsgKey> keyArray(messages.Length());
+ for (nsIMsgDBHdr* aMessage : messages) {
+ if (!aMessage) {
+ continue;
+ }
+ nsMsgKey key;
+ aMessage->GetMessageKey(&key);
+ keyArray.AppendElement(key);
+ }
+ keyArray.Sort();
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> sortedMsgs;
+ rv = MessagesInKeyOrder(keyArray, srcFolder, sortedMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (WeAreOffline())
+ return CopyMessagesOffline(srcFolder, sortedMsgs, isMove, msgWindow,
+ listener);
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 3rd parameter: Do not set offline flag.
+ SetPendingAttributes(sortedMsgs, isMove, false);
+
+ // if the folders aren't on the same server, do a stream base copy
+ if (!sameServer) {
+ rv = CopyMessagesWithStream(srcFolder, sortedMsgs, isMove, true,
+ msgWindow, listener, allowUndo);
+ goto done;
+ }
+
+ nsAutoCString messageIds;
+ rv = AllocateUidStringFromKeys(keyArray, messageIds);
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIUrlListener> urlListener;
+ rv =
+ QueryInterface(NS_GET_IID(nsIUrlListener), getter_AddRefs(urlListener));
+ rv = InitCopyState(srcSupport, sortedMsgs, isMove, true, false, 0,
+ EmptyCString(), listener, msgWindow, allowUndo);
+ if (NS_FAILED(rv)) goto done;
+
+ m_copyState->m_curIndex = m_copyState->m_messages.Length();
+
+ if (isMove)
+ srcFolder->EnableNotifications(
+ allMessageCountNotifications,
+ false); // disable message count notification
+
+ nsCOMPtr<nsIURI> resultUrl;
+ nsCOMPtr<nsISupports> copySupport = do_QueryInterface(m_copyState);
+ rv = imapService->OnlineMessageCopy(
+ srcFolder, messageIds, this, true, isMove, urlListener,
+ getter_AddRefs(resultUrl), copySupport, msgWindow);
+ if (NS_SUCCEEDED(rv) && m_copyState->m_allowUndo) {
+ RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn;
+ if (!undoMsgTxn ||
+ NS_FAILED(undoMsgTxn->Init(srcFolder, &keyArray, messageIds.get(),
+ this, true, isMove)))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ if (isMove) {
+ if (mFlags & nsMsgFolderFlags::Trash)
+ undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg);
+ else
+ undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ } else
+ undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ m_copyState->m_undoMsgTxn = undoMsgTxn;
+ }
+
+ } // endif
+
+done:
+ if (NS_FAILED(rv)) {
+ (void)OnCopyCompleted(srcSupport, rv);
+ if (isMove) {
+ srcFolder->EnableNotifications(
+ allMessageCountNotifications,
+ true); // enable message count notification
+ NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ }
+ }
+ return rv;
+}
+
+// This is used when copying an imap or local/pop3 folder to an imap server.
+// It does not allow completely moving an imap or local/pop3 folder to an imap
+// server since only the messages can be moved between servers.
+class nsImapFolderCopyState final : public nsIUrlListener,
+ public nsIMsgCopyServiceListener {
+ public:
+ nsImapFolderCopyState(nsIMsgFolder* destParent, nsIMsgFolder* srcFolder,
+ bool isMoveMessages, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ nsresult StartNextCopy();
+ nsresult AdvanceToNextFolder(nsresult aStatus);
+
+ protected:
+ ~nsImapFolderCopyState();
+ RefPtr<nsImapMailFolder> m_newDestFolder;
+ nsCOMPtr<nsISupports> m_origSrcFolder;
+ nsCOMPtr<nsIMsgFolder> m_curDestParent;
+ nsCOMPtr<nsIMsgFolder> m_curSrcFolder;
+ bool m_isMoveMessages;
+ nsCOMPtr<nsIMsgCopyServiceListener> m_copySrvcListener;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ int32_t m_childIndex;
+ nsCOMArray<nsIMsgFolder> m_srcChildFolders;
+ nsCOMArray<nsIMsgFolder> m_destParents;
+};
+
+NS_IMPL_ISUPPORTS(nsImapFolderCopyState, nsIUrlListener,
+ nsIMsgCopyServiceListener)
+
+nsImapFolderCopyState::nsImapFolderCopyState(
+ nsIMsgFolder* destParent, nsIMsgFolder* srcFolder, bool isMoveMessages,
+ nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener) {
+ m_origSrcFolder = do_QueryInterface(srcFolder);
+ m_curDestParent = destParent;
+ m_curSrcFolder = srcFolder;
+ m_isMoveMessages = isMoveMessages;
+ m_msgWindow = msgWindow;
+ m_copySrvcListener = listener;
+ m_childIndex = -1;
+ // NOTE: The nsImapMailFolder doesn't keep a reference to us, so we're
+ // relying on our use as a listener by nsImapService and nsMsgCopyService
+ // to keep our refcount from zeroing!
+ // Might be safer to add a kungfudeathgrip on ourselves for the duration
+ // of the operation? Would need to make sure we catch all error conditions.
+}
+
+nsImapFolderCopyState::~nsImapFolderCopyState() {}
+
+nsresult nsImapFolderCopyState::StartNextCopy() {
+ nsresult rv;
+ // Create the destination folder (our OnStopRunningUrl() will be called
+ // when done).
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString folderName;
+ m_curSrcFolder->GetName(folderName);
+ return imapService->EnsureFolderExists(m_curDestParent, folderName,
+ m_msgWindow, this);
+}
+
+nsresult nsImapFolderCopyState::AdvanceToNextFolder(nsresult aStatus) {
+ nsresult rv = NS_OK;
+ m_childIndex++;
+ if (m_childIndex >= m_srcChildFolders.Count()) {
+ if (m_newDestFolder)
+ m_newDestFolder->OnCopyCompleted(m_origSrcFolder, aStatus);
+ } else {
+ m_curDestParent = m_destParents[m_childIndex];
+ m_curSrcFolder = m_srcChildFolders[m_childIndex];
+ rv = StartNextCopy();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapFolderCopyState::OnStartRunningUrl(nsIURI* aUrl) {
+ NS_ASSERTION(aUrl, "sanity check - need to be be running non-null url");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapFolderCopyState::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ if (NS_FAILED(aExitCode)) {
+ if (m_copySrvcListener) m_copySrvcListener->OnStopCopy(aExitCode);
+ return aExitCode; // or NS_OK???
+ }
+ nsresult rv = NS_OK;
+ if (aUrl) {
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl);
+ if (imapUrl) {
+ nsImapAction imapAction = nsIImapUrl::nsImapTest;
+ imapUrl->GetImapAction(&imapAction);
+
+ switch (imapAction) {
+ case nsIImapUrl::nsImapEnsureExistsFolder: {
+ // Our EnsureFolderExists() call has completed successfully,
+ // so our dest folder is ready.
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ nsString folderName;
+ nsCString utfLeafName;
+ m_curSrcFolder->GetName(folderName);
+ bool utf8AcceptEnabled;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_curDestParent);
+ rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (utf8AcceptEnabled) {
+ CopyUTF16toUTF8(folderName, utfLeafName);
+ } else {
+ CopyUTF16toMUTF7(folderName, utfLeafName);
+ }
+ // Create the nsIMsgFolder object which represents the folder on
+ // the IMAP server.
+ rv = m_curDestParent->FindSubFolder(utfLeafName,
+ getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Save the first new folder so we can send a notification to the
+ // copy service when this whole process is done.
+ if (!m_newDestFolder)
+ m_newDestFolder =
+ static_cast<nsImapMailFolder*>(newMsgFolder.get());
+
+ // Check if the source folder has children. If it does, list them
+ // into m_srcChildFolders, and set m_destParents for the
+ // corresponding indexes to the newly created folder.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = m_curSrcFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t childIndex = 0;
+ for (nsIMsgFolder* folder : subFolders) {
+ m_srcChildFolders.InsertElementAt(m_childIndex + childIndex + 1,
+ folder);
+ m_destParents.InsertElementAt(m_childIndex + childIndex + 1,
+ newMsgFolder);
+ ++childIndex;
+ }
+
+ // Now kick off a copy (or move) of messages to the new folder.
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = m_curSrcFolder->GetMessages(getter_AddRefs(enumerator));
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgArray;
+ bool hasMore = false;
+
+ if (enumerator) rv = enumerator->HasMoreElements(&hasMore);
+
+ // Early-out for empty folder.
+ if (!hasMore) return AdvanceToNextFolder(NS_OK);
+
+ while (NS_SUCCEEDED(rv) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = enumerator->GetNext(getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgArray.AppendElement(hdr);
+ rv = enumerator->HasMoreElements(&hasMore);
+ }
+
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyService->CopyMessages(m_curSrcFolder, msgArray, newMsgFolder,
+ m_isMoveMessages, this, m_msgWindow,
+ false /* allowUndo */);
+ } break;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapFolderCopyState::OnStartCopy() { return NS_OK; }
+
+/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */
+NS_IMETHODIMP nsImapFolderCopyState::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void SetMessageKey (in nsMsgKey aKey); */
+NS_IMETHODIMP nsImapFolderCopyState::SetMessageKey(nsMsgKey aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* [noscript] void GetMessageId (in nsCString aMessageId); */
+NS_IMETHODIMP nsImapFolderCopyState::GetMessageId(nsACString& messageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnStopCopy (in nsresult aStatus); */
+NS_IMETHODIMP nsImapFolderCopyState::OnStopCopy(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) return AdvanceToNextFolder(aStatus);
+ if (m_copySrvcListener) {
+ (void)m_copySrvcListener->OnStopCopy(aStatus);
+ m_copySrvcListener = nullptr;
+ }
+
+ return NS_OK;
+}
+
+// "this" is the destination (parent) imap folder that srcFolder is copied to.
+// srcFolder may be another imap or a local/pop3 folder.
+NS_IMETHODIMP
+nsImapMailFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ nsresult rv;
+ bool sameServer;
+ rv = IsOnSameServer(this, srcFolder, &sameServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (sameServer && isMoveFolder) {
+ // Do a pure folder move within the same IMAP account/server, where
+ // "pure" means the folder AND messages are copied to the destination and
+ // then both are removed from source account.
+ uint32_t folderFlags = 0;
+ if (srcFolder) srcFolder->GetFlags(&folderFlags);
+
+ // if our source folder is a virtual folder
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ nsString folderName;
+ srcFolder->GetName(folderName);
+
+ nsAutoString safeFolderName(folderName);
+ NS_MsgHashIfNecessary(safeFolderName);
+
+ srcFolder->ForceDBClosed();
+
+ nsCOMPtr<nsIFile> oldPathFile;
+ rv = srcFolder->GetFilePath(getter_AddRefs(oldPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> summaryFile;
+ GetSummaryFileLocation(oldPathFile, getter_AddRefs(summaryFile));
+
+ nsCOMPtr<nsIFile> newPathFile;
+ rv = GetFilePath(getter_AddRefs(newPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory = false;
+ newPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) {
+ AddDirectorySeparator(newPathFile);
+ rv = newPathFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = CheckIfFolderExists(folderName, this, msgWindow);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = summaryFile->CopyTo(newPathFile, EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddSubfolder(safeFolderName, getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newMsgFolder->SetPrettyName(folderName);
+
+ uint32_t flags;
+ srcFolder->GetFlags(&flags);
+ newMsgFolder->SetFlags(flags);
+
+ NotifyFolderAdded(newMsgFolder);
+
+ // now remove the old folder
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ srcFolder->GetParent(getter_AddRefs(msgParent));
+ srcFolder->SetParent(nullptr);
+ if (msgParent) {
+ // The files have already been moved, so delete storage false.
+ msgParent->PropagateDelete(srcFolder, false);
+ oldPathFile->Remove(false); // berkeley mailbox
+ srcFolder->DeleteStorage();
+
+ nsCOMPtr<nsIFile> parentPathFile;
+ rv = msgParent->GetFilePath(getter_AddRefs(parentPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDirectorySeparator(parentPathFile);
+ nsCOMPtr<nsIDirectoryEnumerator> children;
+ parentPathFile->GetDirectoryEntries(getter_AddRefs(children));
+ bool more;
+ // checks if the directory is empty or not
+ if (children && NS_SUCCEEDED(children->HasMoreElements(&more)) && !more)
+ parentPathFile->Remove(true);
+ }
+ } else // non-virtual folder
+ {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder);
+ bool match = false;
+ bool confirmed = false;
+ if (mFlags & nsMsgFolderFlags::Trash) {
+ rv = srcFolder->MatchOrChangeFilterDestination(nullptr, false, &match);
+ if (match) {
+ srcFolder->ConfirmFolderDeletionForFilter(msgWindow, &confirmed);
+ // should we return an error to copy service?
+ // or send a notification?
+ if (!confirmed) return NS_OK;
+ }
+ }
+ rv = InitCopyState(srcSupport, {}, false, false, false, 0, EmptyCString(),
+ listener, msgWindow, false);
+ if (NS_FAILED(rv)) return OnCopyCompleted(srcSupport, rv);
+
+ rv = imapService->MoveFolder(srcFolder, this, this, msgWindow);
+ }
+ } else {
+ // !sameServer OR it's a copy. Unit tests expect a successful folder
+ // copy within the same IMAP server even though the UI forbids copy and
+ // only allows moves inside the same server. folderCopier, set below,
+ // handles the folder copy within an IMAP server (needed by unit tests) and
+ // the folder move or copy from another account or server into an IMAP
+ // account/server. The folder move from another account is "impure" since
+ // just the messages are moved and the source folder remains in place.
+ RefPtr<nsImapFolderCopyState> folderCopier = new nsImapFolderCopyState(
+ this, srcFolder,
+ isMoveFolder, // Always copy folders; if true only move the messages
+ msgWindow, listener);
+ // NOTE: the copystate object must hold itself in existence until complete,
+ // as we're not keeping hold of it here.
+ rv = folderCopier->StartNextCopy();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::CopyFileMessage(nsIFile* file, nsIMsgDBHdr* msgToReplace,
+ bool isDraftOrTemplate, uint32_t aNewMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ nsMsgKey key = nsMsgKey_None;
+ nsAutoCString messageId;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ if (NS_FAILED(rv)) return OnCopyCompleted(file, rv);
+
+ if (msgToReplace) {
+ rv = msgToReplace->GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) {
+ messageId.AppendInt((int32_t)key);
+ // We have an existing message to replace because the user has deleted or
+ // detached one or more attachments. So tell SetPendingAttributes() to
+ // not set several pending offline items (offset, message size, etc.) for
+ // the message to be replaced by setting message size temporarily to zero.
+ // The original message is not actually "replaced" but is imap deleted
+ // and a new message with the same body but with some deleted or detached
+ // attachments is imap appended from the file to the folder.
+ uint32_t saveMsgSize;
+ msgToReplace->GetOfflineMessageSize(&saveMsgSize);
+ msgToReplace->SetOfflineMessageSize(0);
+ SetPendingAttributes({msgToReplace}, false, false);
+ msgToReplace->SetOfflineMessageSize(saveMsgSize);
+ messages.AppendElement(msgToReplace);
+ }
+ }
+
+ bool isMove = (msgToReplace ? true : false);
+ rv = InitCopyState(file, messages, isMove, isDraftOrTemplate, false,
+ aNewMsgFlags, aNewMsgKeywords, listener, msgWindow, false);
+ if (NS_FAILED(rv)) return OnCopyCompleted(file, rv);
+
+ m_copyState->m_streamCopy = true;
+ rv = imapService->AppendMessageFromFile(file, this, messageId, true,
+ isDraftOrTemplate, this, m_copyState,
+ msgWindow);
+ if (NS_FAILED(rv)) return OnCopyCompleted(file, rv);
+
+ return rv;
+}
+
+nsresult nsImapMailFolder::CopyStreamMessage(
+ nsIMsgDBHdr* message,
+ nsIMsgFolder* dstFolder, // should be this
+ nsIMsgWindow* aMsgWindow, bool isMove) {
+ NS_ENSURE_ARG_POINTER(message);
+ if (!m_copyState)
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyStreamMessage failed with null m_copyState"));
+ NS_ENSURE_TRUE(m_copyState, NS_ERROR_NULL_POINTER);
+ nsresult rv;
+ nsCOMPtr<nsICopyMessageStreamListener> copyStreamListener = do_CreateInstance(
+ "@mozilla.org/messenger/copymessagestreamlistener;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsICopyMessageListener> copyListener(
+ do_QueryInterface(dstFolder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> srcFolder(
+ do_QueryInterface(m_copyState->m_srcSupport, &rv));
+ if (NS_FAILED(rv))
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyStreaMessage failed with null m_copyState->m_srcSupport"));
+ if (NS_FAILED(rv)) return rv;
+ rv = copyStreamListener->Init(copyListener);
+ if (NS_FAILED(rv))
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyStreaMessage failed in copyStreamListener->Init"));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCString uri;
+ srcFolder->GetUriForMsg(message, uri);
+
+ if (!m_copyState->m_msgService)
+ rv = GetMessageServiceFromURI(uri,
+ getter_AddRefs(m_copyState->m_msgService));
+
+ if (NS_SUCCEEDED(rv) && m_copyState->m_msgService) {
+ nsCOMPtr<nsIStreamListener> streamListener(
+ do_QueryInterface(copyStreamListener, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // put up status message here, if copying more than one message.
+ if (m_copyState->m_messages.Length() > 1) {
+ nsString dstFolderName, progressText;
+ GetName(dstFolderName);
+ nsAutoString curMsgString;
+ nsAutoString totalMsgString;
+ totalMsgString.AppendInt((int32_t)m_copyState->m_messages.Length());
+ curMsgString.AppendInt(m_copyState->m_curIndex + 1);
+
+ AutoTArray<nsString, 3> formatStrings = {curMsgString, totalMsgString,
+ dstFolderName};
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = bundle->FormatStringFromName("imapCopyingMessageOf2", formatStrings,
+ progressText);
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ if (m_copyState->m_msgWindow)
+ m_copyState->m_msgWindow->GetStatusFeedback(
+ getter_AddRefs(statusFeedback));
+ if (statusFeedback) {
+ statusFeedback->ShowStatusString(progressText);
+ int32_t percent;
+ percent = (100 * m_copyState->m_curIndex) /
+ (int32_t)m_copyState->m_messages.Length();
+ statusFeedback->ShowProgress(percent);
+ }
+ }
+ rv = m_copyState->m_msgService->CopyMessage(
+ uri, streamListener, isMove && !m_copyState->m_isCrossServerOp, nullptr,
+ aMsgWindow);
+ if (NS_FAILED(rv))
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("CopyMessage failed: uri %s", uri.get()));
+ }
+ return rv;
+}
+
+nsImapMailCopyState::nsImapMailCopyState()
+ : m_isMove(false),
+ m_selectedState(false),
+ m_isCrossServerOp(false),
+ m_curIndex(0),
+ m_streamCopy(false),
+ m_dataBuffer(nullptr),
+ m_dataBufferSize(0),
+ m_leftOver(0),
+ m_allowUndo(false),
+ m_eatLF(false),
+ m_newMsgFlags(0),
+ m_appendUID(nsMsgKey_None) {}
+
+nsImapMailCopyState::~nsImapMailCopyState() {
+ PR_Free(m_dataBuffer);
+ if (m_tmpFile) m_tmpFile->Remove(false);
+}
+
+NS_IMPL_ISUPPORTS(nsImapMailCopyState, nsImapMailCopyState)
+
+nsresult nsImapMailFolder::InitCopyState(
+ nsISupports* srcSupport, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, bool selectedState, bool acrossServers, uint32_t newMsgFlags,
+ const nsACString& newMsgKeywords, nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* msgWindow, bool allowUndo) {
+ NS_ENSURE_ARG_POINTER(srcSupport);
+
+ NS_ENSURE_TRUE(!m_copyState, NS_ERROR_FAILURE);
+
+ m_copyState = new nsImapMailCopyState();
+
+ m_copyState->m_isCrossServerOp = acrossServers;
+ m_copyState->m_srcSupport = srcSupport;
+
+ m_copyState->m_messages = messages.Clone();
+ if (!m_copyState->m_isCrossServerOp) {
+ uint32_t numUnread = 0;
+ for (nsIMsgDBHdr* message : m_copyState->m_messages) {
+ // if the message is not there, then assume what the caller tells us to.
+ bool isRead = false;
+ uint32_t flags;
+ if (message) {
+ message->GetFlags(&flags);
+ isRead = flags & nsMsgMessageFlags::Read;
+ }
+ if (!isRead) numUnread++;
+ }
+ m_copyState->m_unreadCount = numUnread;
+ } else {
+ nsIMsgDBHdr* message = m_copyState->m_messages[m_copyState->m_curIndex];
+ // if the key is not there, then assume what the caller tells us to.
+ bool isRead = false;
+ uint32_t flags;
+ if (message) {
+ message->GetFlags(&flags);
+ isRead = flags & nsMsgMessageFlags::Read;
+ }
+ m_copyState->m_unreadCount = (isRead) ? 0 : 1;
+ }
+
+ m_copyState->m_isMove = isMove;
+ m_copyState->m_newMsgFlags = newMsgFlags;
+ m_copyState->m_newMsgKeywords = newMsgKeywords;
+ m_copyState->m_allowUndo = allowUndo;
+ m_copyState->m_selectedState = selectedState;
+ m_copyState->m_msgWindow = msgWindow;
+ if (listener) m_copyState->m_listener = listener;
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::CopyFileToOfflineStore(nsIFile* srcFile,
+ nsMsgKey msgKey) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool storeOffline = (mFlags & nsMsgFolderFlags::Offline) && !WeAreOffline();
+
+ if (msgKey == nsMsgKey_None) {
+ // To support send filters, we need to store the message in the database
+ // when it is copied to the FCC folder. In that case, we know the UID of the
+ // message and therefore have the correct msgKey. In other cases, where
+ // we don't need the offline message copied, don't add to db.
+ if (!storeOffline) return NS_OK;
+
+ mDatabase->GetNextFakeOfflineMsgKey(&msgKey);
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> fakeHdr;
+ rv = mDatabase->CreateNewHdr(msgKey, getter_AddRefs(fakeHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ fakeHdr->SetUint32Property("pseudoHdr", 1);
+
+ // Should we add this to the offline store?
+ nsCOMPtr<nsIOutputStream> offlineStore;
+ if (storeOffline) {
+ rv = GetOfflineStoreOutputStream(fakeHdr, getter_AddRefs(offlineStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // We set an offline kMoveResult because in any case we want to update this
+ // msgHdr with one downloaded from the server, with possible additional
+ // headers added.
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(mDatabase, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op));
+ if (NS_SUCCEEDED(rv) && op) {
+ nsCString destFolderUri;
+ GetURI(destFolderUri);
+ op->SetOperation(nsIMsgOfflineImapOperation::kMoveResult);
+ op->SetDestinationFolderURI(destFolderUri);
+ SetFlag(nsMsgFolderFlags::OfflineEvents);
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIMsgParseMailMsgState> msgParser =
+ do_CreateInstance("@mozilla.org/messenger/messagestateparser;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgParser->SetMailDB(mDatabase);
+
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), srcFile);
+ if (NS_SUCCEEDED(rv) && inputStream) {
+ // Now, parse the temp file to (optionally) copy to
+ // the offline store for the cur folder.
+ RefPtr<nsMsgLineStreamBuffer> inputStreamBuffer =
+ new nsMsgLineStreamBuffer(FILE_IO_BUFFER_SIZE, true, false);
+ int64_t fileSize;
+ srcFile->GetFileSize(&fileSize);
+ uint32_t bytesWritten;
+ rv = NS_OK;
+ msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState);
+ msgParser->SetNewMsgHdr(fakeHdr);
+ bool needMoreData = false;
+ char* newLine = nullptr;
+ uint32_t numBytesInLine = 0;
+ if (offlineStore) {
+ const char* envelope = "From " CRLF;
+ offlineStore->Write(envelope, strlen(envelope), &bytesWritten);
+ fileSize += bytesWritten;
+ }
+ do {
+ newLine = inputStreamBuffer->ReadNextLine(inputStream, numBytesInLine,
+ needMoreData);
+ if (newLine) {
+ msgParser->ParseAFolderLine(newLine, numBytesInLine);
+ if (offlineStore)
+ rv = offlineStore->Write(newLine, numBytesInLine, &bytesWritten);
+
+ free(newLine);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } while (newLine);
+
+ msgParser->FinishHeader();
+ uint32_t resultFlags;
+ if (offlineStore)
+ fakeHdr->OrFlags(nsMsgMessageFlags::Offline | nsMsgMessageFlags::Read,
+ &resultFlags);
+ else
+ fakeHdr->OrFlags(nsMsgMessageFlags::Read, &resultFlags);
+ if (offlineStore) fakeHdr->SetOfflineMessageSize(fileSize);
+ mDatabase->AddNewHdrToDB(fakeHdr, true /* notify */);
+
+ // Call FinishNewMessage before setting pending attributes, as in
+ // maildir it copies from tmp to cur and may change the storeToken
+ // to get a unique filename.
+ if (offlineStore) {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ GetMsgStore(getter_AddRefs(msgStore));
+ if (msgStore) msgStore->FinishNewMessage(offlineStore, fakeHdr);
+ }
+
+ // We are copying from a file to offline store so set offline flag.
+ SetPendingAttributes({&*fakeHdr}, false, true);
+
+ // Gloda needs this notification to index the fake message.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsClassified({&*fakeHdr}, false, false);
+ inputStream->Close();
+ inputStream = nullptr;
+ }
+ if (offlineStore) offlineStore->Close();
+ return rv;
+}
+
+nsresult nsImapMailFolder::OnCopyCompleted(nsISupports* srcSupport,
+ nsresult rv) {
+ // if it's a file, and the copy succeeded, then fcc the offline
+ // store, and add a kMoveResult offline op.
+ if (NS_SUCCEEDED(rv) && m_copyState) {
+ nsCOMPtr<nsIFile> srcFile(do_QueryInterface(srcSupport));
+ if (srcFile)
+ (void)CopyFileToOfflineStore(srcFile, m_copyState->m_appendUID);
+ }
+ m_copyState = nullptr;
+ nsresult result;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &result);
+ NS_ENSURE_SUCCESS(result, result);
+ return copyService->NotifyCompletion(srcSupport, this, rv);
+}
+
+nsresult nsImapMailFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ return nsCreateImapBaseMessageURI(aURI, mBaseMessageURI);
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFolderURL(nsACString& aFolderURL) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rootFolder->GetURI(aFolderURL);
+ if (rootFolder == this) return NS_OK;
+
+ NS_ASSERTION(mURI.Length() > aFolderURL.Length(),
+ "Should match with a folder name!");
+ nsCString escapedName;
+ MsgEscapeString(Substring(mURI, aFolderURL.Length()),
+ nsINetUtil::ESCAPE_URL_PATH, escapedName);
+ if (escapedName.IsEmpty()) return NS_ERROR_OUT_OF_MEMORY;
+ aFolderURL.Append(escapedName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsSubscribing(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_folderNeedsSubscribing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsSubscribing(bool bVal) {
+ m_folderNeedsSubscribing = bVal;
+ return NS_OK;
+}
+
+nsMsgIMAPFolderACL* nsImapMailFolder::GetFolderACL() {
+ if (!m_folderACL) m_folderACL = new nsMsgIMAPFolderACL(this);
+ return m_folderACL;
+}
+
+nsresult nsImapMailFolder::CreateACLRightsStringForFolder(
+ nsAString& rightsString) {
+ GetFolderACL(); // lazy create
+ NS_ENSURE_TRUE(m_folderACL, NS_ERROR_NULL_POINTER);
+ return m_folderACL->CreateACLRightsString(rightsString);
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsACLListed(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ bool dontNeedACLListed = !m_folderNeedsACLListed;
+ // if we haven't acl listed, and it's not a no select folder or the inbox,
+ // then we'll list the acl if it's not a namespace.
+ if (m_folderNeedsACLListed &&
+ !(mFlags & (nsMsgFolderFlags::ImapNoselect | nsMsgFolderFlags::Inbox)))
+ GetIsNamespace(&dontNeedACLListed);
+ *bVal = !dontNeedACLListed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsACLListed(bool bVal) {
+ m_folderNeedsACLListed = bVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetIsNamespace(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsresult rv = NS_OK;
+ if (!m_namespace) {
+#ifdef DEBUG_bienvenu
+ // Make sure this isn't causing us to open the database
+ NS_ASSERTION(m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown,
+ "hierarchy delimiter not set");
+#endif
+
+ nsCString onlineName, serverKey;
+ GetServerKey(serverKey);
+ GetOnlineName(onlineName);
+ char hierarchyDelimiter;
+ GetHierarchyDelimiter(&hierarchyDelimiter);
+
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionList, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_namespace = nsImapNamespaceList::GetNamespaceForFolder(
+ serverKey.get(), onlineName.get(), hierarchyDelimiter);
+ if (m_namespace == nullptr) {
+ if (mFlags & nsMsgFolderFlags::ImapOtherUser)
+ rv = hostSession->GetDefaultNamespaceOfTypeForHost(
+ serverKey.get(), kOtherUsersNamespace, m_namespace);
+ else if (mFlags & nsMsgFolderFlags::ImapPublic)
+ rv = hostSession->GetDefaultNamespaceOfTypeForHost(
+ serverKey.get(), kPublicNamespace, m_namespace);
+ else
+ rv = hostSession->GetDefaultNamespaceOfTypeForHost(
+ serverKey.get(), kPersonalNamespace, m_namespace);
+ }
+ NS_ASSERTION(m_namespace, "failed to get namespace");
+ if (m_namespace) {
+ nsImapNamespaceList::SuggestHierarchySeparatorForNamespace(
+ m_namespace, hierarchyDelimiter);
+ m_folderIsNamespace = nsImapNamespaceList::GetFolderIsNamespace(
+ serverKey.get(), onlineName.get(), hierarchyDelimiter, m_namespace);
+ }
+ }
+ *aResult = m_folderIsNamespace;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetIsNamespace(bool isNamespace) {
+ m_folderIsNamespace = isNamespace;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::ResetNamespaceReferences() {
+ nsCString serverKey;
+ nsCString onlineName;
+ GetServerKey(serverKey);
+ GetOnlineName(onlineName);
+ char hierarchyDelimiter;
+ GetHierarchyDelimiter(&hierarchyDelimiter);
+ m_namespace = nsImapNamespaceList::GetNamespaceForFolder(
+ serverKey.get(), onlineName.get(), hierarchyDelimiter);
+ m_folderIsNamespace = m_namespace ? nsImapNamespaceList::GetFolderIsNamespace(
+ serverKey.get(), onlineName.get(),
+ hierarchyDelimiter, m_namespace)
+ : false;
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* f : subFolders) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(f, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapFolder->ResetNamespaceReferences();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::FindOnlineSubFolder(
+ const nsACString& targetOnlineName, nsIMsgImapMailFolder** aResultFolder) {
+ *aResultFolder = nullptr;
+ nsresult rv = NS_OK;
+
+ nsCString onlineName;
+ GetOnlineName(onlineName);
+
+ if (onlineName.Equals(targetOnlineName)) {
+ return QueryInterface(NS_GET_IID(nsIMsgImapMailFolder),
+ (void**)aResultFolder);
+ }
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsIMsgFolder* f : subFolders) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(f, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapFolder->FindOnlineSubFolder(targetOnlineName, aResultFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*aResultFolder) {
+ return NS_OK; // Found it!
+ }
+ }
+ return NS_OK; // Not found.
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsAdded(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_folderNeedsAdded;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsAdded(bool bVal) {
+ m_folderNeedsAdded = bVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetFolderQuotaCommandIssued(bool* aCmdIssued) {
+ NS_ENSURE_ARG_POINTER(aCmdIssued);
+ *aCmdIssued = m_folderQuotaCommandIssued;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFolderQuotaCommandIssued(bool aCmdIssued) {
+ m_folderQuotaCommandIssued = aCmdIssued;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::SetFolderQuotaData(
+ uint32_t aAction, const nsACString& aFolderQuotaRoot,
+ uint64_t aFolderQuotaUsage, uint64_t aFolderQuotaLimit) {
+ switch (aAction) {
+ case kInvalidateQuota:
+ // Reset to initialize evaluation of a new quotaroot imap response. This
+ // clears any previous array data and marks the quota data for this folder
+ // invalid.
+ m_folderQuotaDataIsValid = false;
+ m_folderQuota.Clear();
+ break;
+ case kStoreQuota:
+ // Store folder's quota data to an array. This will occur zero or more
+ // times for a folder.
+ m_folderQuota.AppendElement(new nsMsgQuota(
+ aFolderQuotaRoot, aFolderQuotaUsage, aFolderQuotaLimit));
+ break;
+ case kValidateQuota:
+ // GETQUOTAROOT command was successful and OK response has occurred. This
+ // indicates that all the untagged QUOTA responses have occurred so mark
+ // as valid.
+ m_folderQuotaDataIsValid = true;
+ break;
+ default:
+ // Called with undefined aAction parameter.
+ NS_ASSERTION(false, "undefined action");
+ }
+ return NS_OK;
+}
+
+// Provide the quota array for status bar notification.
+NS_IMETHODIMP nsImapMailFolder::GetQuota(
+ nsTArray<RefPtr<nsIMsgQuota>>& aArray) {
+ if (m_folderQuotaDataIsValid) {
+ aArray = m_folderQuota.Clone();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::PerformExpand(nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ bool usingSubscription = false;
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapServer->GetUsingSubscription(&usingSubscription);
+ if (NS_SUCCEEDED(rv) && !usingSubscription) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapService->DiscoverChildren(this, this, m_onlineFolderName);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::RenameClient(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* msgFolder,
+ const nsACString& oldName,
+ const nsACString& newName) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgImapMailFolder> oldImapFolder =
+ do_QueryInterface(msgFolder, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ char hierarchyDelimiter = '/';
+ oldImapFolder->GetHierarchyDelimiter(&hierarchyDelimiter);
+ int32_t boxflags = 0;
+ oldImapFolder->GetBoxFlags(&boxflags);
+
+ nsAutoString newLeafName;
+ NS_ConvertUTF8toUTF16 newNameString(newName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newLeafName = newNameString;
+ nsAutoString folderNameStr;
+ int32_t folderStart = newLeafName.RFindChar(
+ '/'); // internal use of hierarchyDelimiter is always '/'
+ if (folderStart > 0) {
+ newLeafName = Substring(newNameString, folderStart + 1);
+ CreateDirectoryForFolder(
+ getter_AddRefs(pathFile)); // needed when we move a folder to a folder
+ // with no subfolders.
+ }
+
+ // if we get here, it's really a leaf, and "this" is the parent.
+ folderNameStr = newLeafName;
+
+ // Create an empty database for this mail folder, set its name from the user
+ nsCOMPtr<nsIMsgDatabase> mailDBFactory;
+ nsCOMPtr<nsIMsgFolder> child;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder;
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDatabase> unusedDB;
+ nsCOMPtr<nsIFile> dbFile;
+
+ // warning, path will be changed
+ rv = CreateFileForDB(folderNameStr, pathFile, getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use openMailDBFromFile() and not OpenFolderDB() here, since we don't use
+ // the DB.
+ rv = msgDBService->OpenMailDBFromFile(dbFile, nullptr, true, true,
+ getter_AddRefs(unusedDB));
+ if (NS_SUCCEEDED(rv) && unusedDB) {
+ // need to set the folder name
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+
+ // Now let's create the actual new folder
+ rv = AddSubfolderWithPath(folderNameStr, dbFile, getter_AddRefs(child));
+ if (!child || NS_FAILED(rv)) return rv;
+ nsAutoString unicodeName;
+ rv = CopyFolderNameToUTF16(NS_ConvertUTF16toUTF8(folderNameStr),
+ unicodeName);
+ if (NS_SUCCEEDED(rv)) child->SetPrettyName(unicodeName);
+ imapFolder = do_QueryInterface(child);
+ if (imapFolder) {
+ nsAutoCString onlineName(m_onlineFolderName);
+
+ if (!onlineName.IsEmpty()) onlineName.Append(hierarchyDelimiter);
+ onlineName.Append(NS_ConvertUTF16toUTF8(folderNameStr));
+ imapFolder->SetVerifiedAsOnlineFolder(true);
+ imapFolder->SetOnlineName(onlineName);
+ imapFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ imapFolder->SetBoxFlags(boxflags);
+ // store the online name as the mailbox name in the db folder info
+ // I don't think anyone uses the mailbox name, so we'll use it
+ // to restore the online name when blowing away an imap db.
+ if (folderInfo) {
+ nsAutoString unicodeOnlineName;
+ CopyUTF8toUTF16(onlineName, unicodeOnlineName);
+ folderInfo->SetMailboxName(unicodeOnlineName);
+ }
+ bool changed = false;
+ msgFolder->MatchOrChangeFilterDestination(
+ child, false /*caseInsensitive*/, &changed);
+ if (changed) msgFolder->AlertFilterChanged(msgWindow);
+ }
+ unusedDB->SetSummaryValid(true);
+ unusedDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ unusedDB->Close(true);
+ child->RenameSubFolders(msgWindow, msgFolder);
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ msgFolder->GetParent(getter_AddRefs(msgParent));
+ msgFolder->SetParent(nullptr);
+ // Reset online status now that the folder is renamed.
+ nsCOMPtr<nsIMsgImapMailFolder> oldImapFolder = do_QueryInterface(msgFolder);
+ if (oldImapFolder) oldImapFolder->SetVerifiedAsOnlineFolder(false);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderRenamed(msgFolder, child);
+
+ // Do not propagate the deletion until after we have (synchronously)
+ // notified all listeners about the rename. This allows them to access
+ // properties on the source folder without experiencing failures.
+ if (msgParent) msgParent->PropagateDelete(msgFolder, true);
+ NotifyFolderAdded(child);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) {
+ m_initialized = true;
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = oldFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ nsCOMPtr<nsIMsgImapMailFolder> folder(do_QueryInterface(msgFolder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char hierarchyDelimiter = '/';
+ folder->GetHierarchyDelimiter(&hierarchyDelimiter);
+
+ int32_t boxflags;
+ folder->GetBoxFlags(&boxflags);
+
+ bool verified;
+ folder->GetVerifiedAsOnlineFolder(&verified);
+
+ nsCOMPtr<nsIFile> oldPathFile;
+ rv = msgFolder->GetFilePath(getter_AddRefs(oldPathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFile> newParentPathFile;
+ rv = GetFilePath(getter_AddRefs(newParentPathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = AddDirectorySeparator(newParentPathFile);
+ nsAutoCString oldLeafName;
+ oldPathFile->GetNativeLeafName(oldLeafName);
+ newParentPathFile->AppendNative(oldLeafName);
+
+ nsCOMPtr<nsIFile> newPathFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newPathFile->InitWithFile(newParentPathFile);
+
+ nsCOMPtr<nsIFile> dbFilePath = newPathFile;
+
+ nsCOMPtr<nsIMsgFolder> child;
+
+ nsString folderName;
+ rv = msgFolder->GetName(folderName);
+ if (folderName.IsEmpty() || NS_FAILED(rv)) return rv;
+
+ nsCString utfLeafName;
+ bool utf8AcceptEnabled;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder);
+ rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (utf8AcceptEnabled) {
+ CopyUTF16toUTF8(folderName, utfLeafName);
+ } else {
+ CopyUTF16toMUTF7(folderName, utfLeafName);
+ }
+
+ // XXX : Fix this non-sense by fixing AddSubfolderWithPath
+ nsAutoString unicodeLeafName;
+ CopyUTF8toUTF16(utfLeafName, unicodeLeafName);
+
+ rv = AddSubfolderWithPath(unicodeLeafName, dbFilePath,
+ getter_AddRefs(child));
+ if (!child || NS_FAILED(rv)) return rv;
+
+ child->SetName(folderName);
+ imapFolder = do_QueryInterface(child);
+ nsCString onlineName;
+ GetOnlineName(onlineName);
+ nsAutoCString onlineCName(onlineName);
+ onlineCName.Append(hierarchyDelimiter);
+ onlineCName.Append(utfLeafName);
+ if (imapFolder) {
+ imapFolder->SetVerifiedAsOnlineFolder(verified);
+ imapFolder->SetOnlineName(onlineCName);
+ imapFolder->SetHierarchyDelimiter(hierarchyDelimiter);
+ imapFolder->SetBoxFlags(boxflags);
+
+ bool changed = false;
+ msgFolder->MatchOrChangeFilterDestination(
+ child, false /*caseInsensitive*/, &changed);
+ if (changed) msgFolder->AlertFilterChanged(msgWindow);
+ child->RenameSubFolders(msgWindow, msgFolder);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::IsCommandEnabled(const nsACString& command,
+ bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = !(WeAreOffline() && (command.EqualsLiteral("cmd_renameFolder") ||
+ command.EqualsLiteral("cmd_compactFolder") ||
+ command.EqualsLiteral("button_compact") ||
+ command.EqualsLiteral("cmd_delete") ||
+ command.EqualsLiteral("button_delete")));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetCanFileMessages(bool* aCanFileMessages) {
+ nsresult rv;
+ *aCanFileMessages = true;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server)
+ rv = server->GetCanFileMessagesOnServer(aCanFileMessages);
+
+ if (*aCanFileMessages)
+ rv = nsMsgDBFolder::GetCanFileMessages(aCanFileMessages);
+
+ if (*aCanFileMessages) {
+ bool noSelect;
+ GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect);
+ *aCanFileMessages =
+ (noSelect) ? false : GetFolderACL()->GetCanIInsertInFolder();
+ return NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetCanDeleteMessages(bool* aCanDeleteMessages) {
+ NS_ENSURE_ARG_POINTER(aCanDeleteMessages);
+ *aCanDeleteMessages = GetFolderACL()->GetCanIDeleteInFolder();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetPerformingBiff(bool* aPerformingBiff) {
+ NS_ENSURE_ARG_POINTER(aPerformingBiff);
+ *aPerformingBiff = m_performingBiff;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetPerformingBiff(bool aPerformingBiff) {
+ m_performingBiff = aPerformingBiff;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetFilterList(nsIMsgFilterList* aMsgFilterList) {
+ m_filterList = aMsgFilterList;
+ return nsMsgDBFolder::SetFilterList(aMsgFilterList);
+}
+
+nsresult nsImapMailFolder::GetMoveCoalescer() {
+ if (!m_moveCoalescer)
+ m_moveCoalescer = new nsImapMoveCoalescer(this, nullptr /* msgWindow */);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::StoreCustomKeywords(nsIMsgWindow* aMsgWindow,
+ const nsACString& aFlagsToAdd,
+ const nsACString& aFlagsToSubtract,
+ const nsTArray<nsMsgKey>& aKeysToStore,
+ nsIURI** _retval) {
+ if (aKeysToStore.IsEmpty()) return NS_OK;
+ nsresult rv = NS_OK;
+ if (WeAreOffline()) {
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_UNEXPECTED;
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb =
+ do_QueryInterface(mDatabase, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto key : aKeysToStore) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ nsresult rv2 = opsDb->GetOfflineOpForKey(key, true, getter_AddRefs(op));
+ if (NS_FAILED(rv2)) rv = rv2;
+ SetFlag(nsMsgFolderFlags::OfflineEvents);
+ if (NS_SUCCEEDED(rv2) && op) {
+ if (!aFlagsToAdd.IsEmpty())
+ op->AddKeywordToAdd(PromiseFlatCString(aFlagsToAdd).get());
+ if (!aFlagsToSubtract.IsEmpty())
+ op->AddKeywordToRemove(PromiseFlatCString(aFlagsToSubtract).get());
+ }
+ }
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); // flush offline ops
+ return rv;
+ }
+
+ nsCOMPtr<nsIImapService> imapService(
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString msgIds;
+ AllocateUidStringFromKeys(aKeysToStore, msgIds);
+ nsCOMPtr<nsIURI> retUri;
+ rv = imapService->StoreCustomKeywords(this, aMsgWindow, aFlagsToAdd,
+ aFlagsToSubtract, msgIds,
+ getter_AddRefs(retUri));
+ if (_retval) {
+ retUri.forget(_retval);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::NotifyIfNewMail() {
+ return PerformBiffNotifications();
+}
+
+bool nsImapMailFolder::ShowPreviewText() {
+ bool showPreviewText = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch)
+ prefBranch->GetBoolPref("mail.biff.alert.show_preview", &showPreviewText);
+ return showPreviewText;
+}
+
+nsresult nsImapMailFolder::PlaybackCoalescedOperations() {
+ if (m_moveCoalescer) {
+ nsTArray<nsMsgKey>* junkKeysToClassify = m_moveCoalescer->GetKeyBucket(0);
+ if (junkKeysToClassify && !junkKeysToClassify->IsEmpty())
+ StoreCustomKeywords(m_moveCoalescer->GetMsgWindow(), "Junk"_ns,
+ EmptyCString(), *junkKeysToClassify, nullptr);
+ junkKeysToClassify->Clear();
+ nsTArray<nsMsgKey>* nonJunkKeysToClassify =
+ m_moveCoalescer->GetKeyBucket(1);
+ if (nonJunkKeysToClassify && !nonJunkKeysToClassify->IsEmpty())
+ StoreCustomKeywords(m_moveCoalescer->GetMsgWindow(), "NonJunk"_ns,
+ EmptyCString(), *nonJunkKeysToClassify, nullptr);
+ nonJunkKeysToClassify->Clear();
+ return m_moveCoalescer->PlaybackMoves(ShowPreviewText());
+ }
+ return NS_OK; // must not be any coalesced operations
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::SetJunkScoreForMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aJunkScore) {
+ nsresult rv = nsMsgDBFolder::SetJunkScoreForMessages(aMessages, aJunkScore);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> keys;
+ nsresult rv = BuildIdsAndKeyArray(aMessages, messageIds, keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ StoreCustomKeywords(
+ nullptr, aJunkScore.EqualsLiteral("0") ? "NonJunk"_ns : "Junk"_ns,
+ aJunkScore.EqualsLiteral("0") ? "Junk"_ns : "NonJunk"_ns, keys,
+ nullptr);
+ if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!aMsgURI.IsEmpty()) // not end of batch
+ {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if this message needs junk classification
+
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) {
+ nsMsgDBFolder::OnMessageClassified(aMsgURI, aClassification,
+ aJunkPercent);
+
+ GetMoveCoalescer();
+ if (m_moveCoalescer) {
+ nsTArray<nsMsgKey>* keysToClassify = m_moveCoalescer->GetKeyBucket(
+ (aClassification == nsIJunkMailPlugin::JUNK) ? 0 : 1);
+ NS_ASSERTION(keysToClassify, "error getting key bucket");
+ if (keysToClassify) keysToClassify->AppendElement(msgKey);
+ }
+ if (aClassification == nsIJunkMailPlugin::JUNK) {
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool markAsReadOnSpam;
+ (void)spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam);
+ if (markAsReadOnSpam) {
+ m_junkMessagesToMarkAsRead.AppendElement(msgHdr);
+ }
+
+ bool willMoveMessage = false;
+
+ // don't do the move when we are opening up
+ // the junk mail folder or the trash folder
+ // or when manually classifying messages in those folders
+ if (!(mFlags & nsMsgFolderFlags::Junk ||
+ mFlags & nsMsgFolderFlags::Trash)) {
+ bool moveOnSpam;
+ (void)spamSettings->GetMoveOnSpam(&moveOnSpam);
+ if (moveOnSpam) {
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!spamFolderURI.IsEmpty()) {
+ rv = FindFolder(spamFolderURI, getter_AddRefs(mSpamFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mSpamFolder) {
+ rv = mSpamFolder->SetFlag(nsMsgFolderFlags::Junk);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamKeysToMove.AppendElement(msgKey);
+ willMoveMessage = true;
+ } else {
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // the listener should do
+ // rv = folder->SetFlag(nsMsgFolderFlags::Junk);
+ // NS_ENSURE_SUCCESS(rv,rv);
+ // if (NS_SUCCEEDED(GetMoveCoalescer())) {
+ // m_moveCoalescer->AddMove(folder, msgKey);
+ // willMoveMessage = true;
+ // }
+ rv = GetOrCreateJunkFolder(spamFolderURI,
+ nullptr /* aListener */);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed");
+ }
+ }
+ }
+ }
+ rv = spamSettings->LogJunkHit(msgHdr, willMoveMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ else // end of batch
+ {
+ // Parent will apply post bayes filters.
+ nsMsgDBFolder::OnMessageClassified(EmptyCString(),
+ nsIJunkMailPlugin::UNCLASSIFIED, 0);
+
+ if (!m_junkMessagesToMarkAsRead.IsEmpty()) {
+ rv = MarkMessagesRead(m_junkMessagesToMarkAsRead, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_junkMessagesToMarkAsRead.Clear();
+ }
+ if (!mSpamKeysToMove.IsEmpty()) {
+ GetMoveCoalescer();
+ for (uint32_t keyIndex = 0; keyIndex < mSpamKeysToMove.Length();
+ keyIndex++) {
+ // If an upstream filter moved this message, don't move it here.
+ nsMsgKey msgKey = mSpamKeysToMove.ElementAt(keyIndex);
+ nsMsgProcessingFlagType processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+ if (!(processingFlags & nsMsgProcessingFlags::FilterToMove)) {
+ if (m_moveCoalescer && mSpamFolder)
+ m_moveCoalescer->AddMove(mSpamFolder, msgKey);
+ } else {
+ // We don't need the FilterToMove flag anymore.
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::FilterToMove);
+ }
+ }
+ mSpamKeysToMove.Clear();
+ }
+
+ // Let's not hold onto the spam folder reference longer than necessary.
+ mSpamFolder = nullptr;
+
+ bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves();
+ PlaybackCoalescedOperations();
+ // If we are performing biff for this folder, tell the server object
+ if ((!pendingMoves || !ShowPreviewText()) && m_performingBiff) {
+ // we don't need to adjust the num new messages in this folder because
+ // the playback moves code already did that.
+ (void)PerformBiffNotifications();
+ server->SetPerformingBiff(false);
+ m_performingBiff = false;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMailFolder::GetShouldDownloadAllHeaders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // for just the inbox, we check if the filter list has arbitrary headers.
+ // for all folders, check if we have a spam plugin that requires all headers
+ if (mFlags & nsMsgFolderFlags::Inbox) {
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ nsresult rv = GetFilterList(nullptr, getter_AddRefs(filterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterList->GetShouldDownloadAllHeaders(aResult);
+ if (*aResult) return rv;
+ }
+ nsCOMPtr<nsIMsgFilterPlugin> filterPlugin;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))))
+ server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin));
+
+ return (filterPlugin) ? filterPlugin->GetShouldDownloadAllHeaders(aResult)
+ : NS_OK;
+}
+
+void nsImapMailFolder::GetTrashFolderName(nsAString& aFolderName) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ nsresult rv;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return;
+ imapServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return;
+ imapServer->GetTrashFolderName(aFolderName);
+ return;
+}
+NS_IMETHODIMP nsImapMailFolder::FetchMsgPreviewText(
+ nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) {
+ NS_ENSURE_ARG_POINTER(aAsyncResults);
+
+ nsTArray<nsMsgKey> keysToFetchFromServer;
+
+ *aAsyncResults = false;
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService("@mozilla.org/messenger/messageservice;1?type=imap", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < aKeysToFetch.Length(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCString prevBody;
+ rv = GetMessageHeader(aKeysToFetch[i], getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ignore messages that already have a preview body.
+ msgHdr->GetStringProperty("preview", prevBody);
+ if (!prevBody.IsEmpty()) continue;
+
+ /* check if message is in memory cache or offline store. */
+ nsCOMPtr<nsIURI> url;
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCString messageUri;
+ rv = GetUriForMsg(msgHdr, messageUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgService->GetUrlForUri(messageUri, nullptr, getter_AddRefs(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Lets look in the offline store.
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ if (msgFlags & nsMsgMessageFlags::Offline) {
+ rv = GetLocalMsgStream(msgHdr, getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetMsgPreviewTextFromStream(msgHdr, inputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ keysToFetchFromServer.AppendElement(msgKey);
+ }
+ }
+ if (!keysToFetchFromServer.IsEmpty()) {
+ uint32_t msgCount = keysToFetchFromServer.Length();
+ nsAutoCString messageIds;
+ AllocateImapUidString(keysToFetchFromServer.Elements(), msgCount, nullptr,
+ messageIds);
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->GetBodyStart(this, aUrlListener, messageIds, 2048,
+ getter_AddRefs(outUri));
+ *aAsyncResults = true; // the preview text will be available async...
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = nsMsgDBFolder::AddKeywordsToMessages(aMessages, aKeywords);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> keys;
+ rv = BuildIdsAndKeyArray(aMessages, messageIds, keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = StoreCustomKeywords(nullptr, aKeywords, EmptyCString(), keys, nullptr);
+ if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = nsMsgDBFolder::RemoveKeywordsFromMessages(aMessages, aKeywords);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString messageIds;
+ nsTArray<nsMsgKey> keys;
+ nsresult rv = BuildIdsAndKeyArray(aMessages, messageIds, keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = StoreCustomKeywords(nullptr, EmptyCString(), aKeywords, keys, nullptr);
+ if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetCustomIdentity(nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ if (mFlags & nsMsgFolderFlags::ImapOtherUser) {
+ nsresult rv;
+ bool delegateOtherUsersFolders = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mail.imap.delegateOtherUsersFolders",
+ &delegateOtherUsersFolders);
+ // if we're automatically delegating other user's folders, we need to
+ // cons up an e-mail address for the other user. We do that by
+ // taking the other user's name and the current user's domain name,
+ // assuming they'll be the same. So, <otherUsersName>@<ourDomain>
+ if (delegateOtherUsersFolders) {
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgIdentity> ourIdentity;
+ nsCOMPtr<nsIMsgIdentity> retIdentity;
+ nsCOMPtr<nsIMsgAccount> account;
+ nsCString foldersUserName;
+ nsCString ourEmailAddress;
+
+ accountManager->FindAccountForServer(server, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+ account->GetDefaultIdentity(getter_AddRefs(ourIdentity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ ourIdentity->GetEmail(ourEmailAddress);
+ int32_t atPos = ourEmailAddress.FindChar('@');
+ if (atPos != kNotFound) {
+ nsCString otherUsersEmailAddress;
+ GetFolderOwnerUserName(otherUsersEmailAddress);
+ otherUsersEmailAddress.Append(
+ Substring(ourEmailAddress, atPos, ourEmailAddress.Length()));
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = accountManager->GetIdentitiesForServer(server, identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto identity : identities) {
+ if (!identity) continue;
+ nsCString identityEmail;
+ identity->GetEmail(identityEmail);
+ if (identityEmail.Equals(otherUsersEmailAddress)) {
+ retIdentity = identity;
+ break;
+ }
+ }
+ if (!retIdentity) {
+ // create the identity
+ rv = accountManager->CreateIdentity(getter_AddRefs(retIdentity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ retIdentity->SetEmail(otherUsersEmailAddress);
+ nsCOMPtr<nsIMsgAccount> account;
+ accountManager->FindAccountForServer(server, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+ account->AddIdentity(retIdentity);
+ }
+ }
+ if (retIdentity) {
+ retIdentity.forget(aIdentity);
+ return NS_OK;
+ }
+ }
+ }
+ return nsMsgDBFolder::GetCustomIdentity(aIdentity);
+}
+
+NS_IMETHODIMP nsImapMailFolder::ChangePendingTotal(int32_t aDelta) {
+ ChangeNumPendingTotalMessages(aDelta);
+ if (aDelta > 0) NotifyHasPendingMsgs();
+ return NS_OK;
+}
+
+void nsImapMailFolder::NotifyHasPendingMsgs() {
+ InitAutoSyncState();
+ nsresult rv;
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) autoSyncMgr->OnFolderHasPendingMsgs(m_autoSyncStateObj);
+}
+
+/* void changePendingUnread (in long aDelta); */
+NS_IMETHODIMP nsImapMailFolder::ChangePendingUnread(int32_t aDelta) {
+ ChangeNumPendingUnread(aDelta);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetServerRecent(int32_t* aServerRecent) {
+ NS_ENSURE_ARG_POINTER(aServerRecent);
+ *aServerRecent = m_numServerRecentMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetServerTotal(int32_t* aServerTotal) {
+ NS_ENSURE_ARG_POINTER(aServerTotal);
+ *aServerTotal = m_numServerTotalMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetServerUnseen(int32_t* aServerUnseen) {
+ NS_ENSURE_ARG_POINTER(aServerUnseen);
+ *aServerUnseen = m_numServerUnseenMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetServerNextUID(int32_t* aNextUID) {
+ NS_ENSURE_ARG_POINTER(aNextUID);
+ *aNextUID = m_nextUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetAutoSyncStateObj(
+ nsIAutoSyncState** autoSyncStateObj) {
+ NS_ENSURE_ARG_POINTER(autoSyncStateObj);
+
+ // create auto-sync state object lazily
+ InitAutoSyncState();
+
+ NS_IF_ADDREF(*autoSyncStateObj = m_autoSyncStateObj);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::InitiateAutoSync(nsIUrlListener* aUrlListener) {
+ nsCString folderName;
+ GetURI(folderName);
+ MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug,
+ ("%s: Updating folder: %s", __func__, folderName.get()));
+
+ // HACK: if UpdateFolder finds out that it can't open
+ // the folder, it doesn't set the url listener and returns
+ // no error. In this case, we return success from this call
+ // but the caller never gets a notification on its url listener.
+ bool canOpenThisFolder = true;
+ GetCanOpenFolder(&canOpenThisFolder);
+
+ if (!canOpenThisFolder) {
+ MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug,
+ ("%s: Cannot update folder: %s", __func__, folderName.get()));
+ return NS_ERROR_FAILURE;
+ }
+
+ // create auto-sync state object lazily
+ InitAutoSyncState();
+
+ // make sure we get the counts from the folder cache.
+ ReadDBFolderInfo(false);
+
+ nsresult rv = m_autoSyncStateObj->ManageStorageSpace();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t syncState;
+ m_autoSyncStateObj->GetState(&syncState);
+ if (syncState == nsAutoSyncState::stUpdateNeeded)
+ return m_autoSyncStateObj->UpdateFolder();
+
+ // We only want to init the autosyncStateObj server counts the first time
+ // we update, and update it when the STATUS call finishes. This deals with
+ // the case where biff is doing a STATUS on a non-inbox folder, which
+ // can make autosync think the counts aren't changing.
+ PRTime lastUpdateTime;
+ m_autoSyncStateObj->GetLastUpdateTime(&lastUpdateTime);
+ if (!lastUpdateTime)
+ m_autoSyncStateObj->SetServerCounts(m_numServerTotalMessages,
+ m_numServerRecentMessages,
+ m_numServerUnseenMessages, m_nextUID);
+ // Issue a STATUS command and see if any counts changed.
+ m_autoSyncStateObj->SetState(nsAutoSyncState::stStatusIssued);
+ // The OnStopRunningUrl method of the autosync state obj
+ // will check if the counts or next uid have changed,
+ // and if so, will issue an UpdateFolder().
+ rv = UpdateStatus(m_autoSyncStateObj, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // record the last update time
+ m_autoSyncStateObj->SetLastUpdateTime(PR_Now());
+
+ return NS_OK;
+}
+
+/* static */
+void nsImapMailFolder::PlaybackTimerCallback(nsITimer* aTimer, void* aClosure) {
+ nsPlaybackRequest* request = static_cast<nsPlaybackRequest*>(aClosure);
+
+ NS_ASSERTION(request->SrcFolder->m_pendingPlaybackReq == request,
+ "wrong playback request pointer");
+
+ RefPtr<nsImapOfflineSync> offlineSync = new nsImapOfflineSync();
+ offlineSync->Init(request->MsgWindow, nullptr, request->SrcFolder, true);
+ if (offlineSync) {
+ mozilla::DebugOnly<nsresult> rv = offlineSync->ProcessNextOperation();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "pseudo-offline playback is not successful");
+ }
+
+ // release request struct and timer
+ request->SrcFolder->m_pendingPlaybackReq = nullptr;
+ request->SrcFolder->m_playbackTimer = nullptr; // Just to flag timed out
+ delete request;
+}
+
+void nsImapMailFolder::InitAutoSyncState() {
+ if (!m_autoSyncStateObj) m_autoSyncStateObj = new nsAutoSyncState(this);
+}
+
+NS_IMETHODIMP nsImapMailFolder::HasMsgOffline(nsMsgKey msgKey, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nsresult rv = GetOfflineMsgFolder(msgKey, getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) *_retval = true;
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::GetOfflineMsgFolder(nsMsgKey msgKey,
+ nsIMsgFolder** aMsgFolder) {
+ // Check if we have the message in the current folder.
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ nsCOMPtr<nsIMsgFolder> subMsgFolder;
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t msgFlags = 0;
+ hdr->GetFlags(&msgFlags);
+ // Check if we already have this message body offline
+ if ((msgFlags & nsMsgMessageFlags::Offline)) {
+ NS_IF_ADDREF(*aMsgFolder = this);
+ return NS_OK;
+ }
+ }
+
+ if (!*aMsgFolder) {
+ // Checking the existence of message in other folders in case of GMail
+ // Server
+ bool isGMail;
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ rv = GetImapIncomingServer(getter_AddRefs(imapServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapServer->GetIsGMailServer(&isGMail);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isGMail) {
+ nsCString labels;
+ nsTArray<nsCString> labelNames;
+ hdr->GetStringProperty("X-GM-LABELS", labels);
+ ParseString(labels, ' ', labelNames);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsCOMPtr<nsIMsgImapMailFolder> subFolder;
+ for (uint32_t i = 0; i < labelNames.Length(); i++) {
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && (rootFolder)) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapRootFolder =
+ do_QueryInterface(rootFolder);
+ if (labelNames[i].EqualsLiteral("\"\\\\Draft\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Drafts,
+ getter_AddRefs(subMsgFolder));
+ if (labelNames[i].EqualsLiteral("\"\\\\Inbox\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox,
+ getter_AddRefs(subMsgFolder));
+ if (labelNames[i].EqualsLiteral("\"\\\\All Mail\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Archive,
+ getter_AddRefs(subMsgFolder));
+ if (labelNames[i].EqualsLiteral("\"\\\\Trash\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash,
+ getter_AddRefs(subMsgFolder));
+ if (labelNames[i].EqualsLiteral("\"\\\\Spam\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Junk,
+ getter_AddRefs(subMsgFolder));
+ if (labelNames[i].EqualsLiteral("\"\\\\Sent\""))
+ rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::SentMail,
+ getter_AddRefs(subMsgFolder));
+ if (FindInReadable("[Imap]/"_ns, labelNames[i],
+ nsCaseInsensitiveCStringComparator)) {
+ labelNames[i].ReplaceSubstring("[Imap]/", "");
+ imapRootFolder->FindOnlineSubFolder(labelNames[i],
+ getter_AddRefs(subFolder));
+ subMsgFolder = do_QueryInterface(subFolder);
+ }
+ if (!subMsgFolder) {
+ imapRootFolder->FindOnlineSubFolder(labelNames[i],
+ getter_AddRefs(subFolder));
+ subMsgFolder = do_QueryInterface(subFolder);
+ }
+ if (subMsgFolder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ subMsgFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ nsCOMPtr<nsIMsgDBHdr> retHdr;
+ nsCString gmMsgID;
+ hdr->GetStringProperty("X-GM-MSGID", gmMsgID);
+ rv = db->GetMsgHdrForGMMsgID(gmMsgID.get(),
+ getter_AddRefs(retHdr));
+ if (NS_FAILED(rv)) return rv;
+ if (retHdr) {
+ uint32_t gmFlags = 0;
+ retHdr->GetFlags(&gmFlags);
+ if ((gmFlags & nsMsgMessageFlags::Offline)) {
+ subMsgFolder.forget(aMsgFolder);
+ // Focus on first positive result.
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsImapMailFolder::GetOfflineFileStream(nsMsgKey msgKey,
+ uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream) {
+ NS_ENSURE_ARG(aFileStream);
+ nsCOMPtr<nsIMsgFolder> offlineFolder;
+ nsresult rv = GetOfflineMsgFolder(msgKey, getter_AddRefs(offlineFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!offlineFolder) return NS_ERROR_FAILURE;
+
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (offlineFolder == this) {
+ return nsMsgDBFolder::GetOfflineFileStream(msgKey, offset, size,
+ aFileStream);
+ }
+
+ // The message we want is stored in a different folder (hackery for gmail).
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString gmMsgID;
+ hdr->GetStringProperty("X-GM-MSGID", gmMsgID);
+ nsCOMPtr<nsIMsgDatabase> db;
+ offlineFolder->GetMsgDatabase(getter_AddRefs(db));
+ rv = db->GetMsgHdrForGMMsgID(gmMsgID.get(), getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hdr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsMsgKey newMsgKey;
+ hdr->GetMessageKey(&newMsgKey);
+
+ // We _know_ it's a nsImapMailFolder.
+ nsImapMailFolder* other = static_cast<nsImapMailFolder*>(offlineFolder.get());
+ return other->GetOfflineFileStream(newMsgKey, offset, size, aFileStream);
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ nsMsgKey key;
+ hdr->GetMessageKey(&key);
+
+ uint64_t offset = 0;
+ uint32_t size = 0;
+ nsCOMPtr<nsIInputStream> rawStream;
+ nsresult rv =
+ GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<SlicedInputStream> slicedStream =
+ new SlicedInputStream(rawStream.forget(), offset, uint64_t(size));
+ slicedStream.forget(stream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetIncomingServerType(nsACString& serverType) {
+ serverType.AssignLiteral("imap");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailFolder::GetShouldUseUtf8FolderName(bool* aUseUTF8) {
+ *aUseUTF8 = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ imapServer->GetUtf8AcceptEnabled(aUseUTF8);
+ return NS_OK;
+}
+
+void nsImapMailFolder::DeleteStoreMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages) {
+ // Delete messages for pluggable stores that do not support compaction.
+ nsCOMPtr<nsIMsgPluggableStore> offlineStore;
+ (void)GetMsgStore(getter_AddRefs(offlineStore));
+
+ if (offlineStore) {
+ bool supportsCompaction;
+ offlineStore->GetSupportsCompaction(&supportsCompaction);
+ if (!supportsCompaction) offlineStore->DeleteMessages(aMessages);
+ }
+}
+
+void nsImapMailFolder::DeleteStoreMessages(
+ const nsTArray<nsMsgKey>& aMessages) {
+ DeleteStoreMessages(aMessages, this);
+}
+
+void nsImapMailFolder::DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages,
+ nsIMsgFolder* aFolder) {
+ // Delete messages for pluggable stores that do not support compaction.
+ NS_ASSERTION(aFolder, "Missing Source Folder");
+ nsCOMPtr<nsIMsgPluggableStore> offlineStore;
+ (void)aFolder->GetMsgStore(getter_AddRefs(offlineStore));
+ if (offlineStore) {
+ bool supportsCompaction;
+ offlineStore->GetSupportsCompaction(&supportsCompaction);
+ if (!supportsCompaction) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ aFolder->GetMsgDatabase(getter_AddRefs(db));
+ nsresult rv = NS_ERROR_FAILURE;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ if (db) rv = MsgGetHeadersFromKeys(db, aMessages, messages);
+ if (NS_SUCCEEDED(rv))
+ offlineStore->DeleteMessages(messages);
+ else
+ NS_WARNING("Failed to get database");
+ }
+ }
+}
diff --git a/comm/mailnews/imap/src/nsImapMailFolder.h b/comm/mailnews/imap/src/nsImapMailFolder.h
new file mode 100644
index 0000000000..0f7f6ce406
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapMailFolder.h
@@ -0,0 +1,599 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsImapMailFolder_h__
+#define nsImapMailFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "nsImapCore.h" // so that consumers including ImapMailFolder.h also get the kImapMsg* constants
+#include "nsMsgDBFolder.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapMessageSink.h"
+#include "nsICopyMessageListener.h"
+#include "nsIUrlListener.h"
+#include "nsIImapIncomingServer.h" // we need this for its IID
+#include "nsIMsgParseMailMsgState.h"
+#include "nsImapUndoTxn.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgFilterHitNotify.h"
+#include "nsIMsgFilterList.h"
+#include "prmon.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIMsgThread.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIStringEnumerator.h"
+#include "nsTHashMap.h"
+#include "nsITimer.h"
+#include "nsCOMArray.h"
+#include "nsAutoSyncState.h"
+
+class nsImapMoveCoalescer;
+class nsIMsgIdentity;
+class nsIMsgOfflineImapOperation;
+
+#define COPY_BUFFER_SIZE 16384
+
+#define NS_IMAPMAILCOPYSTATE_IID \
+ { \
+ 0xb64534f0, 0x3d53, 0x11d3, { \
+ 0xac, 0x2a, 0x00, 0x80, 0x5f, 0x8a, 0xc9, 0x68 \
+ } \
+ }
+
+class nsImapMailCopyState : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMAPMAILCOPYSTATE_IID)
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsImapMailCopyState();
+
+ nsCOMPtr<nsISupports> m_srcSupport; // source file spec or folder
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_messages; // array of source messages
+ RefPtr<nsImapMoveCopyMsgTxn>
+ m_undoMsgTxn; // undo object with this copy operation
+ nsCOMPtr<nsIMsgCopyServiceListener> m_listener; // listener of this copy
+ // operation
+ nsCOMPtr<nsIFile> m_tmpFile; // temp file spec for copy operation
+ nsCOMPtr<nsIMsgWindow> m_msgWindow; // msg window for copy operation
+
+ nsCOMPtr<nsIMsgMessageService>
+ m_msgService; // source folder message service; can
+ // be Nntp, Mailbox, or Imap
+ bool m_isMove; // is a move
+ bool m_selectedState; // needs to be in selected state; append msg
+ bool m_isCrossServerOp; // are we copying between imap servers?
+ uint32_t m_curIndex; // message index to the message array which we are
+ // copying
+ uint32_t m_unreadCount; // num unread messages we're moving
+ bool m_streamCopy;
+ char* m_dataBuffer; // temporary buffer for this copy operation
+ nsCOMPtr<nsIOutputStream> m_msgFileStream; // temporary file (processed mail)
+ uint32_t m_dataBufferSize;
+ uint32_t m_leftOver;
+ bool m_allowUndo;
+ bool m_eatLF;
+ uint32_t m_newMsgFlags; // only used if m_messages is empty
+ nsCString m_newMsgKeywords; // ditto
+ // If the server supports UIDPLUS, this is the UID for the append,
+ // if we're doing an append.
+ nsMsgKey m_appendUID;
+
+ private:
+ virtual ~nsImapMailCopyState();
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsImapMailCopyState, NS_IMAPMAILCOPYSTATE_IID)
+
+// ACLs for this folder.
+// Generally, we will try to always query this class when performing
+// an operation on the folder.
+// If the server doesn't support ACLs, none of this data will be filled in.
+// Therefore, we can assume that if we look up ourselves and don't find
+// any info (and also look up "anyone") then we have full rights, that is, ACLs
+// don't exist.
+class nsImapMailFolder;
+
+// clang-format off
+#define IMAP_ACL_READ_FLAG 0x0000001 // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder
+#define IMAP_ACL_STORE_SEEN_FLAG 0x0000002 // STORE SEEN flag
+#define IMAP_ACL_WRITE_FLAG 0x0000004 // STORE flags other than SEEN and DELETED
+#define IMAP_ACL_INSERT_FLAG 0x0000008 // APPEND, COPY into folder
+#define IMAP_ACL_POST_FLAG 0x0000010 // Can I send mail to the submission address for folder?
+#define IMAP_ACL_CREATE_SUBFOLDER_FLAG 0x0000020 // Can I CREATE a subfolder of this folder?
+#define IMAP_ACL_DELETE_FLAG 0x0000040 // STORE DELETED flag
+#define IMAP_ACL_ADMINISTER_FLAG 0x0000080 // perform SETACL
+#define IMAP_ACL_RETRIEVED_FLAG 0x0000100 // ACL info for this folder has been initialized
+#define IMAP_ACL_EXPUNGE_FLAG 0x0000200 // can EXPUNGE or do implicit EXPUNGE on CLOSE
+#define IMAP_ACL_DELETE_FOLDER 0x0000400 // can DELETE/RENAME folder
+// clang-format on
+
+class nsMsgIMAPFolderACL {
+ public:
+ explicit nsMsgIMAPFolderACL(nsImapMailFolder* folder);
+ ~nsMsgIMAPFolderACL();
+
+ bool SetFolderRightsForUser(const nsACString& userName,
+ const nsACString& rights);
+
+ public:
+ // generic for any user, although we might not use them in
+ // DO NOT use these for looking up information about the currently
+ // authenticated user. (There are some different checks and defaults we do).
+ // Instead, use the functions below, GetICan....()
+ // clang-format off
+ bool GetCanUserLookupFolder(const nsACString& userName); // Is folder visible to LIST/LSUB?
+ bool GetCanUserReadFolder(const nsACString& userName); // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder?
+ bool GetCanUserStoreSeenInFolder(const nsACString& userName); // STORE SEEN flag?
+ bool GetCanUserWriteFolder(const nsACString& userName); // STORE flags other than SEEN and DELETED?
+ bool GetCanUserInsertInFolder(const nsACString& userName); // APPEND, COPY into folder?
+ bool GetCanUserPostToFolder(const nsACString& userName); // Can I send mail to the submission address for folder?
+ bool GetCanUserCreateSubfolder(const nsACString& userName); // Can I CREATE a subfolder of this folder?
+ bool GetCanUserDeleteInFolder(const nsACString& userName); // STORE DELETED flag, perform EXPUNGE?
+ bool GetCanUserAdministerFolder(const nsACString& userName); // perform SETACL?
+
+ // Functions to find out rights for the currently authenticated user.
+
+ bool GetCanILookupFolder(); // Is folder visible to LIST/LSUB?
+ bool GetCanIReadFolder(); // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder?
+ bool GetCanIStoreSeenInFolder(); // STORE SEEN flag?
+ bool GetCanIWriteFolder(); // STORE flags other than SEEN and DELETED?
+ bool GetCanIInsertInFolder(); // APPEND, COPY into folder?
+ bool GetCanIPostToFolder(); // Can I send mail to the submission address for folder?
+ bool GetCanICreateSubfolder(); // Can I CREATE a subfolder of this folder?
+ bool GetCanIDeleteInFolder(); // STORE DELETED flag?
+ bool GetCanIAdministerFolder(); // perform SETACL?
+ bool GetCanIExpungeFolder(); // perform EXPUNGE?
+ // clang-format on
+
+ bool GetDoIHaveFullRightsForFolder(); // Returns TRUE if I have full rights
+ // on this folder (all of the above
+ // return TRUE)
+
+ bool GetIsFolderShared(); // We use this to see if the ACLs think a folder is
+ // shared or not.
+ // We will define "Shared" in 5.0 to mean:
+ // At least one user other than the currently authenticated user has at least
+ // one explicitly-listed ACL right on that folder.
+
+ // Returns a newly allocated string describing these rights
+ nsresult CreateACLRightsString(nsAString& rightsString);
+
+ nsresult GetRightsStringForUser(const nsACString& userName,
+ nsCString& rights);
+
+ nsresult GetOtherUsers(nsIUTF8StringEnumerator** aResult);
+
+ protected:
+ bool GetFlagSetInRightsForUser(const nsACString& userName, char flag,
+ bool defaultIfNotFound);
+ void BuildInitialACLFromCache();
+ void UpdateACLCache();
+
+ protected:
+ nsTHashMap<nsCStringHashKey, nsCString>
+ m_rightsHash; // Hash table, mapping username strings to rights strings.
+ nsImapMailFolder* m_folder;
+ int32_t m_aclCount;
+};
+
+/**
+ * Encapsulates parameters required to playback offline ops
+ * on given folder.
+ */
+struct nsPlaybackRequest {
+ explicit nsPlaybackRequest(nsImapMailFolder* srcFolder,
+ nsIMsgWindow* msgWindow)
+ : SrcFolder(srcFolder), MsgWindow(msgWindow) {}
+ nsImapMailFolder* SrcFolder;
+ nsCOMPtr<nsIMsgWindow> MsgWindow;
+};
+
+class nsMsgQuota final : public nsIMsgQuota {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGQUOTA
+
+ nsMsgQuota(const nsACString& aName, const uint64_t& aUsage,
+ const uint64_t& aLimit);
+
+ protected:
+ ~nsMsgQuota();
+
+ nsCString mName;
+ uint64_t mUsage, mLimit;
+};
+
+class nsImapMailFolder : public nsMsgDBFolder,
+ public nsIMsgImapMailFolder,
+ public nsIImapMailFolderSink,
+ public nsIImapMessageSink,
+ public nsICopyMessageListener,
+ public nsIMsgFilterHitNotify {
+ static const uint32_t PLAYBACK_TIMER_INTERVAL_IN_MS = 500;
+
+ public:
+ nsImapMailFolder();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIMsgFolder methods:
+ NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override;
+
+ NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override;
+
+ NS_IMETHOD CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD AddSubfolder(const nsAString& aName,
+ nsIMsgFolder** aChild) override;
+ NS_IMETHODIMP CreateStorageIfMissing(nsIUrlListener* urlListener) override;
+
+ NS_IMETHOD Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD EmptyTrash(nsIUrlListener* aListener) override;
+ NS_IMETHOD CopyDataToOutputStreamForAppend(
+ nsIInputStream* aIStream, int32_t aLength,
+ nsIOutputStream* outputStream) override;
+ NS_IMETHOD CopyDataDone() override;
+ NS_IMETHOD DeleteStorage() override;
+ NS_IMETHOD Rename(const nsAString& newName, nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) override;
+ NS_IMETHOD GetNoSelect(bool* aResult) override;
+
+ NS_IMETHOD GetPrettyName(nsAString& prettyName)
+ override; // Override of the base, for top-level mail folder
+
+ NS_IMETHOD GetFolderURL(nsACString& url) override;
+
+ NS_IMETHOD UpdateSummaryTotals(bool force) override;
+
+ NS_IMETHOD GetDeletable(bool* deletable) override;
+
+ NS_IMETHOD GetSizeOnDisk(int64_t* size) override;
+
+ NS_IMETHOD GetCanCreateSubfolders(bool* aResult) override;
+ NS_IMETHOD GetCanSubscribe(bool* aResult) override;
+
+ NS_IMETHOD ApplyRetentionSettings() override;
+
+ NS_IMETHOD AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) override;
+ NS_IMETHOD MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages,
+ bool markRead) override;
+ NS_IMETHOD MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD MarkMessagesFlagged(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages,
+ bool markFlagged) override;
+ NS_IMETHOD MarkThreadRead(nsIMsgThread* thread) override;
+ NS_IMETHOD SetJunkScoreForMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aJunkScore) override;
+ NS_IMETHOD DeleteSelf(nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) override;
+ NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element) override;
+
+ NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
+ DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHeaders,
+ nsIMsgWindow* msgWindow, bool deleteStorage, bool isMove,
+ nsIMsgCopyServiceListener* listener, bool allowUndo) override;
+ NS_IMETHOD CopyMessages(nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener, bool isFolder,
+ bool allowUndo) override;
+ NS_IMETHOD CopyFolder(nsIMsgFolder* srcFolder, bool isMove,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) override;
+ NS_IMETHOD CopyFileMessage(nsIFile* file, nsIMsgDBHdr* msgToReplace,
+ bool isDraftOrTemplate, uint32_t aNewMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) override;
+ NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) override;
+
+ NS_IMETHOD GetFilePath(nsIFile** aPathName) override;
+ NS_IMETHOD SetFilePath(nsIFile* aPath) override;
+
+ NS_IMETHOD Shutdown(bool shutdownChildren) override;
+
+ NS_IMETHOD DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD GetCanFileMessages(bool* aCanFileMessages) override;
+ NS_IMETHOD GetCanDeleteMessages(bool* aCanDeleteMessages) override;
+ NS_IMETHOD FetchMsgPreviewText(nsTArray<nsMsgKey> const& aKeysToFetch,
+ nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) override;
+
+ NS_IMETHOD AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) override;
+ NS_IMETHOD RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) override;
+
+ NS_IMETHOD NotifyCompactCompleted() override;
+
+ // overrides nsMsgDBFolder::HasMsgOffline()
+ NS_IMETHOD HasMsgOffline(nsMsgKey msgKey, bool* _retval) override;
+ NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) override;
+
+ NS_DECL_NSIMSGIMAPMAILFOLDER
+ NS_DECL_NSIIMAPMAILFOLDERSINK
+ NS_DECL_NSIIMAPMESSAGESINK
+ NS_DECL_NSICOPYMESSAGELISTENER
+
+ // nsIUrlListener methods
+ NS_IMETHOD OnStartRunningUrl(nsIURI* aUrl) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
+ OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) override;
+
+ NS_DECL_NSIMSGFILTERHITNOTIFY
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+
+ NS_IMETHOD IsCommandEnabled(const nsACString& command, bool* result) override;
+ NS_IMETHOD SetFilterList(nsIMsgFilterList* aMsgFilterList) override;
+ NS_IMETHOD GetCustomIdentity(nsIMsgIdentity** aIdentity) override;
+
+ NS_IMETHOD GetIncomingServerType(nsACString& serverType) override;
+
+ nsresult AddSubfolderWithPath(nsAString& name, nsIFile* dbPath,
+ nsIMsgFolder** child, bool brandNew = false);
+ nsresult MoveIncorporatedMessage(nsIMsgDBHdr* mailHdr,
+ nsIMsgDatabase* sourceDB,
+ const nsACString& destFolder,
+ nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow);
+
+ // send notification to copy service listener.
+ nsresult OnCopyCompleted(nsISupports* srcSupport, nsresult exitCode);
+
+ static nsresult AllocateUidStringFromKeys(const nsTArray<nsMsgKey>& keys,
+ nsCString& msgIds);
+ static nsresult BuildIdsAndKeyArray(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, nsCString& msgIds,
+ nsTArray<nsMsgKey>& keyArray);
+
+ // these might end up as an nsIImapMailFolder attribute.
+ nsresult SetSupportedUserFlags(uint32_t userFlags);
+ nsresult GetSupportedUserFlags(uint32_t* userFlags);
+
+ // Find the start of a range of msgKeys that can hold srcCount headers.
+ nsresult FindOpenRange(nsMsgKey& fakeBase, uint32_t srcCount);
+
+ protected:
+ virtual ~nsImapMailFolder();
+ // Helper methods
+
+ nsresult ExpungeAndCompact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow);
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+ void FindKeysToAdd(const nsTArray<nsMsgKey>& existingKeys,
+ nsTArray<nsMsgKey>& keysToFetch, uint32_t& numNewUnread,
+ nsIImapFlagAndUidState* flagState);
+ void FindKeysToDelete(const nsTArray<nsMsgKey>& existingKeys,
+ nsTArray<nsMsgKey>& keysToFetch,
+ nsIImapFlagAndUidState* flagState, uint32_t boxFlags);
+ void PrepareToAddHeadersToMailDB(nsIImapProtocol* aProtocol);
+ void TweakHeaderFlags(nsIImapProtocol* aProtocol, nsIMsgDBHdr* tweakMe);
+
+ nsresult SyncFlags(nsIImapFlagAndUidState* flagState);
+ nsresult HandleCustomFlags(nsMsgKey uidOfMessage, nsIMsgDBHdr* dbHdr,
+ uint16_t userFlags, nsCString& keywords);
+ nsresult NotifyMessageFlagsFromHdr(nsIMsgDBHdr* dbHdr, nsMsgKey msgKey,
+ uint32_t flags);
+
+ nsresult SetupHeaderParseStream(uint32_t size, const nsACString& content_type,
+ nsIMailboxSpec* boxSpec);
+ nsresult ParseAdoptedHeaderLine(const char* messageLine, nsMsgKey msgKey);
+ nsresult NormalEndHeaderParseStream(nsIImapProtocol* aProtocol,
+ nsIImapUrl* imapUrl);
+
+ void EndOfflineDownload();
+
+ /**
+ * At the end of a file-to-folder copy operation, copy the file to the
+ * offline store and/or add to the message database, (if needed).
+ *
+ * @param srcFile file containing the message key
+ * @param msgKey key to use for the new messages
+ */
+ nsresult CopyFileToOfflineStore(nsIFile* srcFile, nsMsgKey msgKey);
+
+ nsresult MarkMessagesImapDeleted(nsTArray<nsMsgKey>* keyArray, bool deleted,
+ nsIMsgDatabase* db);
+
+ // Notifies imap autosync that it should update this folder when it
+ // gets a chance.
+ void NotifyHasPendingMsgs();
+ void UpdatePendingCounts();
+ void SetIMAPDeletedFlag(nsIMsgDatabase* mailDB,
+ const nsTArray<nsMsgKey>& msgids, bool markDeleted);
+ virtual bool ShowDeletedMessages();
+ virtual bool DeleteIsMoveToTrash();
+ nsresult GetFolder(const nsACString& name, nsIMsgFolder** pFolder);
+ nsresult GetTrashFolder(nsIMsgFolder** pTrashFolder);
+ bool TrashOrDescendentOfTrash(nsIMsgFolder* folder);
+ static bool ShouldCheckAllFolders(nsIImapIncomingServer* imapServer);
+ nsresult GetServerKey(nsACString& serverKey);
+ nsresult DisplayStatusMsg(nsIImapUrl* aImapUrl, const nsAString& msg);
+
+ // nsresult RenameLocal(const char *newName);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult CreateSubFolders(nsIFile* path);
+ nsresult GetDatabase() override;
+
+ nsresult GetFolderOwnerUserName(nsACString& userName);
+ nsImapNamespace* GetNamespaceForFolder();
+ void SetNamespaceForFolder(nsImapNamespace* ns);
+
+ nsMsgIMAPFolderACL* GetFolderACL();
+ nsresult CreateACLRightsStringForFolder(nsAString& rightsString);
+ nsresult GetBodysToDownload(nsTArray<nsMsgKey>* keysOfMessagesToDownload);
+ // Uber message copy service
+ nsresult CopyMessagesWithStream(nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, bool isCrossServerOp,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener,
+ bool allowUndo);
+ nsresult CopyStreamMessage(nsIMsgDBHdr* message, nsIMsgFolder* dstFolder,
+ nsIMsgWindow* msgWindow, bool isMove);
+ nsresult InitCopyState(nsISupports* srcSupport,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, bool selectedState, bool acrossServers,
+ uint32_t newMsgFlags, const nsACString& newMsgKeywords,
+ nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* msgWindow, bool allowUndo);
+ nsresult GetMoveCoalescer();
+ nsresult PlaybackCoalescedOperations();
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override;
+ // offline-ish methods
+ nsresult GetClearedOriginalOp(nsIMsgOfflineImapOperation* op,
+ nsIMsgOfflineImapOperation** originalOp,
+ nsIMsgDatabase** originalDB);
+ nsresult GetOriginalOp(nsIMsgOfflineImapOperation* op,
+ nsIMsgOfflineImapOperation** originalOp,
+ nsIMsgDatabase** originalDB);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult CopyMessagesOffline(
+ nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener);
+ void SetPendingAttributes(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages,
+ bool aIsMove, bool aSetOffline);
+
+ nsresult CopyOfflineMsgBody(nsIMsgFolder* srcFolder, nsIMsgDBHdr* destHdr,
+ nsIMsgDBHdr* origHdr, nsIInputStream* inputStream,
+ nsIOutputStream* outputStream);
+
+ void GetTrashFolderName(nsAString& aFolderName);
+ bool ShowPreviewText();
+
+ // Pseudo-Offline operation playback timer
+ static void PlaybackTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ // Allocate and initialize associated auto-sync state object.
+ void InitAutoSyncState();
+
+ virtual nsresult GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream) override;
+
+ bool m_initialized;
+ bool m_haveDiscoveredAllFolders;
+ nsCOMPtr<nsIMsgParseMailMsgState> m_msgParser;
+ nsCOMPtr<nsIMsgFilterList> m_filterList;
+ nsCOMPtr<nsIMsgFilterPlugin> m_filterPlugin; // XXX should be a list
+ // used with filter plugins to know when we've finished classifying and can
+ // playback moves
+ bool m_msgMovedByFilter;
+ RefPtr<nsImapMoveCoalescer>
+ m_moveCoalescer; // strictly owned by the nsImapMailFolder
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_junkMessagesToMarkAsRead;
+ /// list of keys to be moved to the junk folder
+ nsTArray<nsMsgKey> mSpamKeysToMove;
+ /// the junk destination folder
+ nsCOMPtr<nsIMsgFolder> mSpamFolder;
+ nsMsgKey m_curMsgUid;
+ uint32_t m_uidValidity;
+
+ // These three vars are used to store counts from STATUS or SELECT command
+ // They include deleted messages, so they can differ from the generic
+ // folder total and unread counts.
+ int32_t m_numServerRecentMessages;
+ int32_t m_numServerUnseenMessages;
+ int32_t m_numServerTotalMessages;
+ // if server supports UIDNEXT, we store it here.
+ int32_t m_nextUID;
+
+ int32_t m_nextMessageByteLength;
+ nsCOMPtr<nsIUrlListener> m_urlListener;
+ bool m_urlRunning;
+
+ // undo move/copy transaction support
+ RefPtr<nsMsgTxn> m_pendingUndoTxn;
+ RefPtr<nsImapMailCopyState> m_copyState;
+ char m_hierarchyDelimiter;
+ int32_t m_boxFlags;
+ nsCString m_onlineFolderName;
+ nsCString m_ownerUserName; // username of the "other user," as in
+ // "Other Users' Mailboxes"
+
+ nsCString m_adminUrl; // url to run to set admin privileges for this folder
+ nsImapNamespace* m_namespace;
+ bool m_verifiedAsOnlineFolder;
+ bool m_explicitlyVerify; // whether or not we need to explicitly verify this
+ // through LIST
+ bool m_folderIsNamespace;
+ bool m_folderNeedsSubscribing;
+ bool m_folderNeedsAdded;
+ bool m_folderNeedsACLListed;
+ bool m_performingBiff;
+ bool m_updatingFolder;
+ bool m_applyIncomingFilters; // apply filters to this folder, even if not the
+ // inbox
+ nsMsgIMAPFolderACL* m_folderACL;
+ uint32_t m_aclFlags;
+ uint32_t m_supportedUserFlags;
+
+ // determines if we are on GMail server
+ bool m_isGmailServer;
+ // offline imap support
+ bool m_downloadingFolderForOfflineUse;
+ bool m_filterListRequiresBody;
+
+ // auto-sync (automatic message download) support
+ RefPtr<nsAutoSyncState> m_autoSyncStateObj;
+
+ // Quota support.
+ nsTArray<RefPtr<nsIMsgQuota>> m_folderQuota;
+ bool m_folderQuotaCommandIssued;
+ bool m_folderQuotaDataIsValid;
+
+ // Pseudo-Offline Playback support
+ nsPlaybackRequest* m_pendingPlaybackReq;
+ nsCOMPtr<nsITimer> m_playbackTimer;
+ nsTArray<RefPtr<nsImapMoveCopyMsgTxn>> m_pendingOfflineMoves;
+ // hash table of mapping between messageids and message keys
+ // for pseudo hdrs.
+ nsTHashMap<nsCStringHashKey, nsMsgKey> m_pseudoHdrs;
+
+ nsTArray<nsMsgKey> m_keysToFetch;
+ uint32_t m_totalKeysToFetch;
+
+ /**
+ * delete if appropriate local storage for messages in this folder
+ *
+ * @parm aMessages array (of nsIMsgDBHdr) of messages to delete
+ * (or an array of message keys)
+ * @parm aSrcFolder the folder containing the messages (optional)
+ */
+ void DeleteStoreMessages(const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages);
+ void DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages);
+ static void DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages,
+ nsIMsgFolder* aFolder);
+ /**
+ * This method is used to locate a folder where a msg could be present, not
+ * just the folder where the message first arrives, this method searches for
+ * the existence of msg in all the folders/labels that we retrieve from
+ * X-GM-LABELS also.
+ * @param msgKey key of the msg for which we are trying to get the folder;
+ * @param aMsgFolder required folder;
+ */
+ nsresult GetOfflineMsgFolder(nsMsgKey msgKey, nsIMsgFolder** aMsgFolder);
+};
+#endif
diff --git a/comm/mailnews/imap/src/nsImapNamespace.cpp b/comm/mailnews/imap/src/nsImapNamespace.cpp
new file mode 100644
index 0000000000..4fd2dcf7db
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapNamespace.cpp
@@ -0,0 +1,513 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+
+#include "nsImapCore.h"
+#include "nsImapNamespace.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsImapUrl.h"
+#include "nsString.h"
+#include "nsServiceManagerUtils.h"
+
+//////////////////// nsImapNamespace
+////////////////////////////////////////////////////////////////
+
+#define NS_IIMAPHOSTSESSIONLIST_CID \
+ { \
+ 0x479ce8fc, 0xe725, 0x11d2, { \
+ 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \
+ } \
+ }
+static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID);
+
+nsImapNamespace::nsImapNamespace(EIMAPNamespaceType type, const char* prefix,
+ char delimiter, bool from_prefs) {
+ m_namespaceType = type;
+ m_prefix = PL_strdup(prefix);
+ m_fromPrefs = from_prefs;
+
+ m_delimiter = delimiter;
+ m_delimiterFilledIn =
+ !m_fromPrefs; // if it's from the prefs, we can't be sure about the
+ // delimiter until we list it.
+}
+
+nsImapNamespace::~nsImapNamespace() { PR_FREEIF(m_prefix); }
+
+void nsImapNamespace::SetDelimiter(char delimiter, bool delimiterFilledIn) {
+ m_delimiter = delimiter;
+ m_delimiterFilledIn = delimiterFilledIn;
+}
+
+// returns -1 if this box is not part of this namespace,
+// or the length of the prefix if it is part of this namespace
+int nsImapNamespace::MailboxMatchesNamespace(const char* boxname) {
+ if (!boxname) return -1;
+
+ // If the namespace is part of the boxname
+ if (!m_prefix || !*m_prefix) return 0;
+
+ if (PL_strstr(boxname, m_prefix) == boxname) return PL_strlen(m_prefix);
+
+ // If the boxname is part of the prefix
+ // (Used for matching Personal mailbox with Personal/ namespace, etc.)
+ if (PL_strstr(m_prefix, boxname) == m_prefix) return PL_strlen(boxname);
+ return -1;
+}
+
+nsImapNamespaceList* nsImapNamespaceList::CreatensImapNamespaceList() {
+ nsImapNamespaceList* rv = new nsImapNamespaceList();
+ return rv;
+}
+
+nsImapNamespaceList::nsImapNamespaceList() {}
+
+int nsImapNamespaceList::GetNumberOfNamespaces() {
+ return m_NamespaceList.Length();
+}
+
+nsresult nsImapNamespaceList::InitFromString(const char* nameSpaceString,
+ EIMAPNamespaceType nstype) {
+ nsresult rv = NS_OK;
+ if (nameSpaceString) {
+ int numNamespaces = UnserializeNamespaces(nameSpaceString, nullptr, 0);
+ char** prefixes = (char**)PR_CALLOC(numNamespaces * sizeof(char*));
+ if (prefixes) {
+ int len = UnserializeNamespaces(nameSpaceString, prefixes, numNamespaces);
+ for (int i = 0; i < len; i++) {
+ char* thisns = prefixes[i];
+ char delimiter = '/'; // a guess
+ if (PL_strlen(thisns) >= 1) delimiter = thisns[PL_strlen(thisns) - 1];
+ nsImapNamespace* ns =
+ new nsImapNamespace(nstype, thisns, delimiter, true);
+ if (ns) AddNewNamespace(ns);
+ PR_FREEIF(thisns);
+ }
+ PR_Free(prefixes);
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsImapNamespaceList::OutputToString(nsCString& string) {
+ nsresult rv = NS_OK;
+ return rv;
+}
+
+int nsImapNamespaceList::GetNumberOfNamespaces(EIMAPNamespaceType type) {
+ int nodeIndex = 0, count = 0;
+ for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) {
+ nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex);
+ if (nspace->GetType() == type) {
+ count++;
+ }
+ }
+ return count;
+}
+
+int nsImapNamespaceList::AddNewNamespace(nsImapNamespace* ns) {
+ // If the namespace is from the NAMESPACE response, then we should see if
+ // there are any namespaces previously set by the preferences, or the default
+ // namespace. If so, remove these.
+
+ if (!ns->GetIsNamespaceFromPrefs()) {
+ int nodeIndex;
+ // iterate backwards because we delete elements
+ for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0;
+ nodeIndex--) {
+ nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex);
+ // if we find existing namespace(s) that matches the
+ // new one, we'll just remove the old ones and let the
+ // new one get added when we've finished checking for
+ // matching namespaces or namespaces that came from prefs.
+ if (nspace && (nspace->GetIsNamespaceFromPrefs() ||
+ (!PL_strcmp(ns->GetPrefix(), nspace->GetPrefix()) &&
+ ns->GetType() == nspace->GetType() &&
+ ns->GetDelimiter() == nspace->GetDelimiter()))) {
+ m_NamespaceList.RemoveElementAt(nodeIndex);
+ delete nspace;
+ }
+ }
+ }
+
+ // Add the new namespace to the list. This must come after the removing code,
+ // or else we could never add the initial kDefaultNamespace type to the list.
+ m_NamespaceList.AppendElement(ns);
+
+ return 0;
+}
+
+// chrisf - later, fix this to know the real concept of "default" namespace of a
+// given type
+nsImapNamespace* nsImapNamespaceList::GetDefaultNamespaceOfType(
+ EIMAPNamespaceType type) {
+ nsImapNamespace *rv = 0, *firstOfType = 0;
+
+ int nodeIndex, count = m_NamespaceList.Length();
+ for (nodeIndex = 0; nodeIndex < count && !rv; nodeIndex++) {
+ nsImapNamespace* ns = m_NamespaceList.ElementAt(nodeIndex);
+ if (ns->GetType() == type) {
+ if (!firstOfType) firstOfType = ns;
+ if (!(*(ns->GetPrefix()))) {
+ // This namespace's prefix is ""
+ // Therefore it is the default
+ rv = ns;
+ }
+ }
+ }
+ if (!rv) rv = firstOfType;
+ return rv;
+}
+
+nsImapNamespaceList::~nsImapNamespaceList() {
+ ClearNamespaces(true, true, true);
+}
+
+// ClearNamespaces removes and deletes the namespaces specified, and if there
+// are no namespaces left,
+void nsImapNamespaceList::ClearNamespaces(bool deleteFromPrefsNamespaces,
+ bool deleteServerAdvertisedNamespaces,
+ bool reallyDelete) {
+ int nodeIndex;
+
+ // iterate backwards because we delete elements
+ for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) {
+ nsImapNamespace* ns = m_NamespaceList.ElementAt(nodeIndex);
+ if (ns->GetIsNamespaceFromPrefs()) {
+ if (deleteFromPrefsNamespaces) {
+ m_NamespaceList.RemoveElementAt(nodeIndex);
+ if (reallyDelete) delete ns;
+ }
+ } else if (deleteServerAdvertisedNamespaces) {
+ m_NamespaceList.RemoveElementAt(nodeIndex);
+ if (reallyDelete) delete ns;
+ }
+ }
+}
+
+nsImapNamespace* nsImapNamespaceList::GetNamespaceNumber(int nodeIndex) {
+ NS_ASSERTION(nodeIndex >= 0 && nodeIndex < GetNumberOfNamespaces(),
+ "invalid IMAP namespace node index");
+ if (nodeIndex < 0) nodeIndex = 0;
+
+ // XXX really could be just ElementAt; that's why we have the assertion
+ return m_NamespaceList.SafeElementAt(nodeIndex);
+}
+
+nsImapNamespace* nsImapNamespaceList::GetNamespaceNumber(
+ int nodeIndex, EIMAPNamespaceType type) {
+ int nodeCount, count = 0;
+ for (nodeCount = m_NamespaceList.Length() - 1; nodeCount >= 0; nodeCount--) {
+ nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeCount);
+ if (nspace->GetType() == type) {
+ count++;
+ if (count == nodeIndex) return nspace;
+ }
+ }
+ return nullptr;
+}
+
+nsImapNamespace* nsImapNamespaceList::GetNamespaceForMailbox(
+ const char* boxname) {
+ // We want to find the LONGEST substring that matches the beginning of this
+ // mailbox's path. This accounts for nested namespaces (i.e. "Public/" and
+ // "Public/Users/")
+
+ // Also, we want to match the namespace's mailbox to that namespace also:
+ // The Personal box will match the Personal/ namespace, etc.
+
+ // these lists shouldn't be too long (99% chance there won't be more than 3 or
+ // 4) so just do a linear search
+
+ int lengthMatched = -1;
+ int currentMatchedLength = -1;
+ nsImapNamespace* rv = nullptr;
+ int nodeIndex = 0;
+
+ if (!PL_strcasecmp(boxname, "INBOX"))
+ return GetDefaultNamespaceOfType(kPersonalNamespace);
+
+ for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) {
+ nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex);
+ currentMatchedLength = nspace->MailboxMatchesNamespace(boxname);
+ if (currentMatchedLength > lengthMatched) {
+ rv = nspace;
+ lengthMatched = currentMatchedLength;
+ }
+ }
+
+ return rv;
+}
+
+#define SERIALIZER_SEPARATORS ","
+
+/**
+ * If len is one, copies the first element of prefixes into
+ * serializedNamespaces. If len > 1, copies len strings from prefixes into
+ * serializedNamespaces as a comma-separated list of quoted strings.
+ */
+nsresult nsImapNamespaceList::SerializeNamespaces(
+ char** prefixes, int len, nsCString& serializedNamespaces) {
+ if (len <= 0) return NS_OK;
+
+ if (len == 1) {
+ serializedNamespaces.Assign(prefixes[0]);
+ return NS_OK;
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (i > 0) serializedNamespaces.Append(',');
+
+ serializedNamespaces.Append('"');
+ serializedNamespaces.Append(prefixes[i]);
+ serializedNamespaces.Append('"');
+ }
+ return NS_OK;
+}
+
+/* str is the string which needs to be unserialized.
+ If prefixes is NULL, simply returns the number of namespaces in str. (len is
+ ignored) If prefixes is not NULL, it should be an array of length len which
+ is to be filled in with newly-allocated string. Returns the number of
+ strings filled in.
+*/
+int nsImapNamespaceList::UnserializeNamespaces(const char* str, char** prefixes,
+ int len) {
+ if (!str) return 0;
+ if (!prefixes) {
+ if (str[0] != '"') return 1;
+
+ int count = 0;
+ char* ourstr = PL_strdup(str);
+ char* origOurStr = ourstr;
+ if (ourstr) {
+ char* token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr);
+ while (token != nullptr) {
+ token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr);
+ count++;
+ }
+ PR_Free(origOurStr);
+ }
+ return count;
+ }
+
+ if ((str[0] != '"') && (len >= 1)) {
+ prefixes[0] = PL_strdup(str);
+ return 1;
+ }
+
+ int count = 0;
+ char* ourstr = PL_strdup(str);
+ char* origOurStr = ourstr;
+ if (ourstr) {
+ char* token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr);
+ while ((count < len) && (token != nullptr)) {
+ char *current = PL_strdup(token), *where = current;
+ if (where[0] == '"') where++;
+ if (where[PL_strlen(where) - 1] == '"') where[PL_strlen(where) - 1] = 0;
+ prefixes[count] = PL_strdup(where);
+ PR_FREEIF(current);
+ token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr);
+ count++;
+ }
+ PR_Free(origOurStr);
+ }
+ return count;
+}
+
+// static
+nsCString nsImapNamespaceList::AllocateCanonicalFolderName(
+ const char* onlineFolderName, char delimiter) {
+ nsCString canonicalPath;
+ if (delimiter) {
+ char* tmp =
+ nsImapUrl::ReplaceCharsInCopiedString(onlineFolderName, delimiter, '/');
+ canonicalPath.Assign(tmp);
+ PR_Free(tmp);
+ } else {
+ canonicalPath.Assign(onlineFolderName);
+ }
+ canonicalPath.ReplaceSubstring("\\/", "/");
+ return canonicalPath;
+}
+
+nsImapNamespace* nsImapNamespaceList::GetNamespaceForFolder(
+ const char* hostName, const char* canonicalFolderName, char delimiter) {
+ if (!hostName || !canonicalFolderName) return nullptr;
+
+ nsImapNamespace* resultNamespace = nullptr;
+ nsresult rv;
+ char* convertedFolderName = nsImapNamespaceList::AllocateServerFolderName(
+ canonicalFolderName, delimiter);
+
+ if (convertedFolderName) {
+ nsCOMPtr<nsIImapHostSessionList> hostSessionList =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_FAILED(rv)) return nullptr;
+ hostSessionList->GetNamespaceForMailboxForHost(
+ hostName, convertedFolderName, resultNamespace);
+ PR_Free(convertedFolderName);
+ } else {
+ NS_ASSERTION(false, "couldn't get converted folder name");
+ }
+
+ return resultNamespace;
+}
+
+/* static */
+char* nsImapNamespaceList::AllocateServerFolderName(
+ const char* canonicalFolderName, char delimiter) {
+ if (delimiter)
+ return nsImapUrl::ReplaceCharsInCopiedString(canonicalFolderName, '/',
+ delimiter);
+ return NS_xstrdup(canonicalFolderName);
+}
+
+/*
+ GetFolderOwnerNameFromPath takes as inputs a folder name
+ in canonical form, and a namespace for that folder.
+ The namespace MUST be of type kOtherUsersNamespace, hence the folder MUST be
+ owned by another user. This function extracts the folder owner's name from
+ the canonical name of the folder, and returns the owner's name.
+*/
+/* static */
+nsCString nsImapNamespaceList::GetFolderOwnerNameFromPath(
+ nsImapNamespace* namespaceForFolder, const char* canonicalFolderName) {
+ if (!namespaceForFolder || !canonicalFolderName) {
+ NS_ERROR("null namespace or canonical folder name");
+ return ""_ns;
+ }
+
+ // Convert the canonical path to the online path.
+ nsAutoCString convertedFolderName(canonicalFolderName);
+ char delimiter = namespaceForFolder->GetDelimiter();
+ if (delimiter) {
+ convertedFolderName.ReplaceChar('/', delimiter);
+ }
+
+ // Trim off the prefix.
+ uint32_t prefixLen =
+ nsDependentCString(namespaceForFolder->GetPrefix()).Length();
+ if (convertedFolderName.Length() <= prefixLen) {
+ NS_ERROR("server folder name invalid");
+ return ""_ns;
+ }
+
+ // Trim off anything after the owner name.
+ nsCString owner(Substring(convertedFolderName, prefixLen));
+ int32_t i = owner.FindChar(delimiter);
+ if (i != kNotFound) {
+ owner.Truncate(i);
+ }
+ return owner;
+}
+
+/*
+GetFolderIsNamespace returns TRUE if the given folder is the folder representing
+a namespace.
+*/
+
+bool nsImapNamespaceList::GetFolderIsNamespace(
+ const char* hostName, const char* canonicalFolderName, char delimiter,
+ nsImapNamespace* namespaceForFolder) {
+ NS_ASSERTION(namespaceForFolder, "null namespace");
+
+ bool rv = false;
+
+ const char* prefix = namespaceForFolder->GetPrefix();
+ NS_ASSERTION(prefix, "namespace has no prefix");
+ if (!prefix || !*prefix) // empty namespace prefix
+ return false;
+
+ char* convertedFolderName =
+ AllocateServerFolderName(canonicalFolderName, delimiter);
+ if (convertedFolderName) {
+ bool lastCharIsDelimiter = (prefix[strlen(prefix) - 1] == delimiter);
+
+ if (lastCharIsDelimiter) {
+ rv = ((strncmp(convertedFolderName, prefix,
+ strlen(convertedFolderName)) == 0) &&
+ (strlen(convertedFolderName) == strlen(prefix) - 1));
+ } else {
+ rv = (strcmp(convertedFolderName, prefix) == 0);
+ }
+
+ PR_Free(convertedFolderName);
+ } else {
+ NS_ASSERTION(false, "couldn't allocate server folder name");
+ }
+
+ return rv;
+}
+
+/*
+ SuggestHierarchySeparatorForNamespace takes a namespace from libmsg
+ and a hierarchy delimiter. If the namespace has not been filled in from
+ online NAMESPACE command yet, it fills in the suggested delimiter to be
+ used from then on (until it is overridden by an online response).
+*/
+
+void nsImapNamespaceList::SuggestHierarchySeparatorForNamespace(
+ nsImapNamespace* namespaceForFolder, char delimiterFromFolder) {
+ NS_ASSERTION(namespaceForFolder, "need namespace");
+ if (namespaceForFolder && !namespaceForFolder->GetIsDelimiterFilledIn())
+ namespaceForFolder->SetDelimiter(delimiterFromFolder, false);
+}
+
+/*
+ GenerateFullFolderNameWithDefaultNamespace takes a folder name in canonical
+ form, converts to online form and calculates the full online server name
+ including the namespace prefix of the default namespace of the
+ given type, in the form: PR_smprintf("%s%s", prefix, onlineServerName) if
+ there is a NULL owner PR_smprintf("%s%s%c%s", prefix, owner, delimiter,
+ onlineServerName) if there is an owner. It then converts this back to
+ canonical form and returns it.
+ It returns empty string if there is no namespace of the given type.
+ If nsUsed is not passed in as NULL, then *nsUsed is filled in and returned; it
+ is the namespace used for generating the folder name.
+*/
+nsCString nsImapNamespaceList::GenerateFullFolderNameWithDefaultNamespace(
+ const char* hostName, const char* canonicalFolderName, const char* owner,
+ EIMAPNamespaceType nsType, nsImapNamespace** nsUsed) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIImapHostSessionList> hostSession =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, ""_ns);
+ nsImapNamespace* ns;
+ nsCString fullFolderName;
+ rv = hostSession->GetDefaultNamespaceOfTypeForHost(hostName, nsType, ns);
+ NS_ENSURE_SUCCESS(rv, ""_ns);
+ if (ns) {
+ if (nsUsed) *nsUsed = ns;
+ const char* prefix = ns->GetPrefix();
+ char* convertedFolderName =
+ AllocateServerFolderName(canonicalFolderName, ns->GetDelimiter());
+ if (convertedFolderName) {
+ char* convertedReturnName = nullptr;
+ if (owner) {
+ convertedReturnName = PR_smprintf(
+ "%s%s%c%s", prefix, owner, ns->GetDelimiter(), convertedFolderName);
+ } else {
+ convertedReturnName = PR_smprintf("%s%s", prefix, convertedFolderName);
+ }
+
+ if (convertedReturnName) {
+ fullFolderName = AllocateCanonicalFolderName(convertedReturnName,
+ ns->GetDelimiter());
+ PR_Free(convertedReturnName);
+ }
+ PR_Free(convertedFolderName);
+ } else {
+ NS_ASSERTION(false, "couldn't allocate server folder name");
+ }
+ } else {
+ // Could not find other users namespace on the given host
+ NS_WARNING("couldn't find namespace for given host");
+ }
+ return fullFolderName;
+}
diff --git a/comm/mailnews/imap/src/nsImapNamespace.h b/comm/mailnews/imap/src/nsImapNamespace.h
new file mode 100644
index 0000000000..5d11d73120
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapNamespace.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsImapNamespace_H_
+#define _nsImapNamespace_H_
+
+#include "nsImapCore.h"
+#include "nsTArray.h"
+
+class nsImapNamespace {
+ public:
+ nsImapNamespace(EIMAPNamespaceType type, const char* prefix, char delimiter,
+ bool from_prefs);
+
+ ~nsImapNamespace();
+
+ EIMAPNamespaceType GetType() { return m_namespaceType; }
+ const char* GetPrefix() { return m_prefix; }
+ char GetDelimiter() { return m_delimiter; }
+ void SetDelimiter(char delimiter, bool delimiterFilledIn);
+ bool GetIsDelimiterFilledIn() { return m_delimiterFilledIn; }
+ bool GetIsNamespaceFromPrefs() { return m_fromPrefs; }
+
+ // returns -1 if this box is not part of this namespace,
+ // or the length of the prefix if it is part of this namespace
+ int MailboxMatchesNamespace(const char* boxname);
+
+ protected:
+ EIMAPNamespaceType m_namespaceType;
+ char* m_prefix;
+ char m_delimiter;
+ bool m_fromPrefs;
+ bool m_delimiterFilledIn;
+};
+
+// represents an array of namespaces for a given host
+class nsImapNamespaceList {
+ public:
+ ~nsImapNamespaceList();
+
+ static nsImapNamespaceList* CreatensImapNamespaceList();
+
+ nsresult InitFromString(const char* nameSpaceString,
+ EIMAPNamespaceType nstype);
+ nsresult OutputToString(nsCString& OutputString);
+ int UnserializeNamespaces(const char* str, char** prefixes, int len);
+ nsresult SerializeNamespaces(char** prefixes, int len,
+ nsCString& serializedNamespace);
+
+ void ClearNamespaces(bool deleteFromPrefsNamespaces,
+ bool deleteServerAdvertisedNamespaces,
+ bool reallyDelete);
+ int GetNumberOfNamespaces();
+ int GetNumberOfNamespaces(EIMAPNamespaceType);
+ nsImapNamespace* GetNamespaceNumber(int nodeIndex);
+ nsImapNamespace* GetNamespaceNumber(int nodeIndex, EIMAPNamespaceType);
+
+ nsImapNamespace* GetDefaultNamespaceOfType(EIMAPNamespaceType type);
+ int AddNewNamespace(nsImapNamespace* ns);
+ nsImapNamespace* GetNamespaceForMailbox(const char* boxname);
+ static nsImapNamespace* GetNamespaceForFolder(const char* hostName,
+ const char* canonicalFolderName,
+ char delimiter);
+ static bool GetFolderIsNamespace(const char* hostName,
+ const char* canonicalFolderName,
+ char delimiter,
+ nsImapNamespace* namespaceForFolder);
+ static nsCString GetFolderOwnerNameFromPath(
+ nsImapNamespace* namespaceForFolder, const char* canonicalFolderName);
+ static void SuggestHierarchySeparatorForNamespace(
+ nsImapNamespace* namespaceForFolder, char delimiterFromFolder);
+ static nsCString GenerateFullFolderNameWithDefaultNamespace(
+ const char* hostName, const char* canonicalFolderName, const char* owner,
+ EIMAPNamespaceType nsType, nsImapNamespace** nsUsed);
+
+ protected:
+ static char* AllocateServerFolderName(const char* canonicalFolderName,
+ char delimiter);
+ static nsCString AllocateCanonicalFolderName(const char* onlineFolderName,
+ char delimiter);
+ nsImapNamespaceList(); // use CreatensImapNamespaceList to create one
+
+ nsTArray<nsImapNamespace*> m_NamespaceList;
+};
+#endif
diff --git a/comm/mailnews/imap/src/nsImapOfflineSync.cpp b/comm/mailnews/imap/src/nsImapOfflineSync.cpp
new file mode 100644
index 0000000000..6b43a106bb
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapOfflineSync.cpp
@@ -0,0 +1,1175 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "netCore.h"
+#include "nsNetUtil.h"
+#include "nsImapOfflineSync.h"
+#include "nsImapMailFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgAccountManager.h"
+#include "nsINntpIncomingServer.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsISeekableStream.h"
+#include "nsIMsgCopyService.h"
+#include "nsImapProtocol.h"
+#include "nsMsgUtils.h"
+#include "nsIAutoSyncManager.h"
+#include "mozilla/Unused.h"
+
+NS_IMPL_ISUPPORTS(nsImapOfflineSync, nsIUrlListener, nsIMsgCopyServiceListener,
+ nsIDBChangeListener, nsIImapOfflineSync)
+
+nsImapOfflineSync::nsImapOfflineSync() {
+ m_singleFolderToUpdate = nullptr;
+ m_window = nullptr;
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kFlagsChanged;
+ m_mailboxupdatesStarted = false;
+ m_mailboxupdatesFinished = false;
+ m_createdOfflineFolders = false;
+ m_pseudoOffline = false;
+ m_KeyIndex = 0;
+ mCurrentUIDValidity = nsMsgKey_None;
+ m_listener = nullptr;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::Init(nsIMsgWindow* window, nsIUrlListener* listener,
+ nsIMsgFolder* singleFolderOnly, bool isPseudoOffline) {
+ m_window = window;
+ m_listener = listener;
+ m_singleFolderToUpdate = singleFolderOnly;
+ m_pseudoOffline = isPseudoOffline;
+
+ // not the perfect place for this, but I think it will work.
+ if (m_window) m_window->SetStopped(false);
+
+ return NS_OK;
+}
+
+nsImapOfflineSync::~nsImapOfflineSync() {}
+
+void nsImapOfflineSync::SetWindow(nsIMsgWindow* window) { m_window = window; }
+
+NS_IMETHODIMP nsImapOfflineSync::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+
+ // where do we make sure this gets cleared when we start running urls?
+ bool stopped = false;
+ if (m_window) m_window->GetStopped(&stopped);
+
+ if (m_curTempFile) {
+ m_curTempFile->Remove(false);
+ m_curTempFile = nullptr;
+ }
+ // NS_BINDING_ABORTED is used for the user pressing stop, which
+ // should cause us to abort the offline process. Other errors
+ // should allow us to continue.
+ if (stopped) {
+ if (m_listener) m_listener->OnStopRunningUrl(url, NS_BINDING_ABORTED);
+ return NS_OK;
+ }
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url);
+
+ if (imapUrl)
+ nsImapProtocol::LogImapUrl(NS_SUCCEEDED(rv) ? "offline imap url succeeded "
+ : "offline imap url failed ",
+ imapUrl);
+
+ // If we succeeded, or it was an imap move/copy that timed out, clear the
+ // operation.
+ bool moveCopy =
+ mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kMsgCopy ||
+ mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kMsgMoved;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_ERROR_IMAP_COMMAND_FAILED ||
+ (moveCopy && exitCode == NS_ERROR_NET_TIMEOUT)) {
+ ClearCurrentOps();
+ rv = ProcessNextOperation();
+ }
+ // else if it's a non-stop error, and we're doing multiple folders,
+ // go to the next folder.
+ else if (!m_singleFolderToUpdate) {
+ if (AdvanceToNextFolder())
+ rv = ProcessNextOperation();
+ else if (m_listener)
+ m_listener->OnStopRunningUrl(url, rv);
+ }
+
+ return rv;
+}
+
+/**
+ * Leaves m_currentServer at the next imap or local mail "server" that
+ * might have offline events to playback, and m_folderQueue holding
+ * a (reversed) list of all the folders to consider for that server.
+ * If no more servers, m_currentServer will be left at nullptr and the
+ * function returns false.
+ */
+bool nsImapOfflineSync::AdvanceToNextServer() {
+ nsresult rv = NS_OK;
+
+ if (m_allServers.IsEmpty()) {
+ NS_ASSERTION(!m_currentServer, "this shouldn't be set");
+ m_currentServer = nullptr;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ASSERTION(accountManager && NS_SUCCEEDED(rv),
+ "couldn't get account mgr");
+ if (!accountManager || NS_FAILED(rv)) return false;
+
+ rv = accountManager->GetAllServers(m_allServers);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ size_t serverIndex = 0;
+ if (m_currentServer) {
+ serverIndex = m_allServers.IndexOf(m_currentServer);
+ if (serverIndex == m_allServers.NoIndex) {
+ serverIndex = 0;
+ } else {
+ // Move to the next server
+ ++serverIndex;
+ }
+ }
+ m_currentServer = nullptr;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ while (serverIndex < m_allServers.Length()) {
+ nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]);
+ serverIndex++;
+
+ nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server);
+ if (newsServer) // news servers aren't involved in offline imap
+ continue;
+
+ if (server) {
+ m_currentServer = server;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ rv = rootFolder->GetDescendants(m_folderQueue);
+ if (NS_SUCCEEDED(rv)) {
+ if (!m_folderQueue.IsEmpty()) {
+ // We'll be popping folders off the end as they are processed.
+ m_folderQueue.Reverse();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Sets m_currentFolder to the next folder to process.
+ *
+ * @return True if next folder to process was found, otherwise false.
+ */
+bool nsImapOfflineSync::AdvanceToNextFolder() {
+ // we always start by changing flags
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kFlagsChanged;
+
+ if (m_currentFolder) {
+ m_currentFolder->SetMsgDatabase(nullptr);
+ m_currentFolder = nullptr;
+ }
+
+ bool hasMore = false;
+ if (m_currentServer) {
+ hasMore = !m_folderQueue.IsEmpty();
+ }
+ if (!hasMore) {
+ hasMore = AdvanceToNextServer();
+ }
+ if (hasMore) {
+ m_currentFolder = m_folderQueue.PopLastElement();
+ }
+ ClearDB();
+ return m_currentFolder;
+}
+
+void nsImapOfflineSync::AdvanceToFirstIMAPFolder() {
+ m_currentServer = nullptr;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder;
+ while (!imapFolder && AdvanceToNextFolder()) {
+ imapFolder = do_QueryInterface(m_currentFolder);
+ }
+}
+
+void nsImapOfflineSync::ProcessFlagOperation(nsIMsgOfflineImapOperation* op) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op;
+ nsTArray<nsMsgKey> matchingFlagKeys;
+ uint32_t currentKeyIndex = m_KeyIndex;
+
+ imapMessageFlagsType matchingFlags;
+ currentOp->GetNewFlags(&matchingFlags);
+ bool flagsMatch = true;
+ do { // loop for all messages with the same flags
+ if (flagsMatch) {
+ nsMsgKey curKey;
+ currentOp->GetMessageKey(&curKey);
+ matchingFlagKeys.AppendElement(curKey);
+ currentOp->SetPlayingBack(true);
+ m_currentOpsToClear.AppendObject(currentOp);
+ }
+ currentOp = nullptr;
+ imapMessageFlagsType newFlags = kNoImapMsgFlag;
+ imapMessageFlagsType flagOperation = kNoImapMsgFlag;
+ if (++currentKeyIndex < m_CurrentKeys.Length())
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], false,
+ getter_AddRefs(currentOp));
+ if (currentOp) {
+ currentOp->GetFlagOperation(&flagOperation);
+ currentOp->GetNewFlags(&newFlags);
+ }
+ flagsMatch = (flagOperation & nsIMsgOfflineImapOperation::kFlagsChanged) &&
+ (newFlags == matchingFlags);
+ } while (currentOp);
+
+ if (!matchingFlagKeys.IsEmpty()) {
+ nsAutoCString uids;
+ nsImapMailFolder::AllocateUidStringFromKeys(matchingFlagKeys, uids);
+ uint32_t curFolderFlags;
+ m_currentFolder->GetFlags(&curFolderFlags);
+
+ if (uids.get() && (curFolderFlags & nsMsgFolderFlags::ImapBox)) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder);
+ nsCOMPtr<nsIURI> uriToSetFlags;
+ if (imapFolder) {
+ rv = imapFolder->SetImapFlags(uids.get(), matchingFlags,
+ getter_AddRefs(uriToSetFlags));
+ if (NS_SUCCEEDED(rv) && uriToSetFlags) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(uriToSetFlags);
+ if (mailnewsUrl) mailnewsUrl->RegisterListener(this);
+ }
+ }
+ }
+ } else
+ ProcessNextOperation();
+}
+
+void nsImapOfflineSync::ProcessKeywordOperation(
+ nsIMsgOfflineImapOperation* op) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op;
+ nsTArray<nsMsgKey> matchingKeywordKeys;
+ uint32_t currentKeyIndex = m_KeyIndex;
+
+ nsAutoCString keywords;
+ if (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords)
+ currentOp->GetKeywordsToAdd(getter_Copies(keywords));
+ else
+ currentOp->GetKeywordsToRemove(getter_Copies(keywords));
+ bool keywordsMatch = true;
+ do { // loop for all messages with the same keywords
+ if (keywordsMatch) {
+ nsMsgKey curKey;
+ currentOp->GetMessageKey(&curKey);
+ matchingKeywordKeys.AppendElement(curKey);
+ currentOp->SetPlayingBack(true);
+ m_currentOpsToClear.AppendObject(currentOp);
+ }
+ currentOp = nullptr;
+ if (++currentKeyIndex < m_CurrentKeys.Length())
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], false,
+ getter_AddRefs(currentOp));
+ if (currentOp) {
+ nsAutoCString curOpKeywords;
+ nsOfflineImapOperationType operation;
+ currentOp->GetOperation(&operation);
+ if (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords)
+ currentOp->GetKeywordsToAdd(getter_Copies(curOpKeywords));
+ else
+ currentOp->GetKeywordsToRemove(getter_Copies(curOpKeywords));
+ keywordsMatch = (operation & mCurrentPlaybackOpType) &&
+ (curOpKeywords.Equals(keywords));
+ }
+ } while (currentOp);
+
+ if (!matchingKeywordKeys.IsEmpty()) {
+ uint32_t curFolderFlags;
+ m_currentFolder->GetFlags(&curFolderFlags);
+
+ if (curFolderFlags & nsMsgFolderFlags::ImapBox) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder);
+ nsCOMPtr<nsIURI> uriToStoreCustomKeywords;
+ if (imapFolder) {
+ rv = imapFolder->StoreCustomKeywords(
+ m_window,
+ (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords)
+ ? keywords
+ : EmptyCString(),
+ (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kRemoveKeywords)
+ ? keywords
+ : EmptyCString(),
+ matchingKeywordKeys, getter_AddRefs(uriToStoreCustomKeywords));
+ if (NS_SUCCEEDED(rv) && uriToStoreCustomKeywords) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(uriToStoreCustomKeywords);
+ if (mailnewsUrl) mailnewsUrl->RegisterListener(this);
+ }
+ }
+ }
+ } else
+ ProcessNextOperation();
+}
+
+// XXX This should not be void but return an error to indicate which low
+// level routine failed.
+void nsImapOfflineSync::ProcessAppendMsgOperation(
+ nsIMsgOfflineImapOperation* currentOp, int32_t opType) {
+ nsMsgKey msgKey;
+ currentOp->GetMessageKey(&msgKey);
+ nsCOMPtr<nsIMsgDBHdr> mailHdr;
+ nsresult rv = m_currentDB->GetMsgHdrForKey(msgKey, getter_AddRefs(mailHdr));
+ if (NS_FAILED(rv) || !mailHdr) {
+ m_currentDB->RemoveOfflineOp(currentOp);
+ ProcessNextOperation();
+ return;
+ }
+
+ uint64_t messageOffset;
+ uint32_t messageSize;
+ mailHdr->GetMessageOffset(&messageOffset);
+ mailHdr->GetOfflineMessageSize(&messageSize);
+ nsCOMPtr<nsIFile> tmpFile;
+
+ if (NS_WARN_IF(NS_FAILED(GetSpecialDirectoryWithFileName(
+ NS_OS_TEMP_DIR, "nscpmsg.txt", getter_AddRefs(tmpFile)))))
+ return;
+
+ if (NS_WARN_IF(
+ NS_FAILED(tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600))))
+ return;
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(outputStream), tmpFile,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+ if (NS_WARN_IF(NS_FAILED(rv) || !outputStream)) return;
+
+ // We break out of the loop to get to the clean-up code.
+ bool setPlayingBack = false;
+ do {
+ nsCString moveDestination;
+ currentOp->GetDestinationFolderURI(moveDestination);
+
+ nsCOMPtr<nsIMsgFolder> destFolder;
+ rv = GetOrCreateFolder(moveDestination, getter_AddRefs(destFolder));
+ if (NS_WARN_IF(NS_FAILED(rv))) break;
+
+ nsCOMPtr<nsIInputStream> offlineStoreInputStream;
+ rv = destFolder->GetMsgInputStream(mailHdr,
+ getter_AddRefs(offlineStoreInputStream));
+ if (NS_WARN_IF((NS_FAILED(rv) || !offlineStoreInputStream))) break;
+
+ nsCOMPtr<nsISeekableStream> seekStream =
+ do_QueryInterface(offlineStoreInputStream);
+ MOZ_ASSERT(seekStream, "non seekable stream - can't read from offline msg");
+ if (!seekStream) break;
+
+ // From this point onwards, we need to set "playing back".
+ setPlayingBack = true;
+
+ rv = seekStream->Seek(PR_SEEK_SET, messageOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) break;
+
+ // Copy the dest folder offline store msg to the temp file.
+ int32_t inputBufferSize = FILE_IO_BUFFER_SIZE;
+ char* inputBuffer = (char*)PR_Malloc(inputBufferSize);
+ int32_t bytesLeft;
+ uint32_t bytesRead, bytesWritten;
+
+ bytesLeft = messageSize;
+ rv = inputBuffer ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+ while (bytesLeft > 0 && NS_SUCCEEDED(rv)) {
+ int32_t bytesToRead = std::min(inputBufferSize, bytesLeft);
+ rv = offlineStoreInputStream->Read(inputBuffer, bytesToRead, &bytesRead);
+ if (NS_WARN_IF(NS_FAILED(rv)) || bytesRead == 0) break;
+ rv = outputStream->Write(inputBuffer, bytesRead, &bytesWritten);
+ if (NS_WARN_IF(NS_FAILED(rv))) break;
+ MOZ_ASSERT(bytesWritten == bytesRead,
+ "wrote out incorrect number of bytes");
+ bytesLeft -= bytesRead;
+ }
+ PR_FREEIF(inputBuffer);
+
+ // rv could have an error from Read/Write.
+ nsresult rv2 = outputStream->Close();
+ if (NS_FAILED(rv2)) {
+ NS_WARNING("ouputStream->Close() failed");
+ }
+ outputStream = nullptr; // Don't try to close it again below.
+
+ // rv: Read/Write, rv2: Close
+ if (NS_FAILED(rv) || NS_FAILED(rv2)) {
+ // This Remove() will fail under Windows if the output stream
+ // fails to close above.
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(tmpFile->Remove(false)));
+ break;
+ }
+
+ nsCOMPtr<nsIFile> cloneTmpFile;
+ // clone the tmp file to defeat nsIFile's stat/size caching.
+ tmpFile->Clone(getter_AddRefs(cloneTmpFile));
+ m_curTempFile = cloneTmpFile;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+
+ // CopyFileMessage returns error async to this->OnStopCopy
+ // if copyService is null, let's crash here and now.
+ rv = copyService->CopyFileMessage(cloneTmpFile, destFolder,
+ nullptr, // nsIMsgDBHdr* msgToReplace
+ true, // isDraftOrTemplate
+ 0, // new msg flags
+ EmptyCString(), this, m_window);
+ MOZ_ASSERT(NS_SUCCEEDED(rv),
+ "CopyFileMessage() failed. Fatal. Error in call setup?");
+ } while (false);
+
+ if (setPlayingBack) {
+ currentOp->SetPlayingBack(true);
+ m_currentOpsToClear.AppendObject(currentOp);
+ m_currentDB->DeleteHeader(mailHdr, nullptr, true, true);
+ }
+
+ // Close the output stream if it's not already closed.
+ if (outputStream)
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(outputStream->Close()));
+}
+
+void nsImapOfflineSync::ClearCurrentOps() {
+ int32_t opCount = m_currentOpsToClear.Count();
+ for (int32_t i = opCount - 1; i >= 0; i--) {
+ m_currentOpsToClear[i]->SetPlayingBack(false);
+ m_currentOpsToClear[i]->ClearOperation(mCurrentPlaybackOpType);
+ m_currentOpsToClear.RemoveObjectAt(i);
+ }
+}
+
+void nsImapOfflineSync::ProcessMoveOperation(nsIMsgOfflineImapOperation* op) {
+ nsTArray<nsMsgKey> matchingFlagKeys;
+ uint32_t currentKeyIndex = m_KeyIndex;
+ nsCString moveDestination;
+ op->GetDestinationFolderURI(moveDestination);
+ bool moveMatches = true;
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op;
+ do { // loop for all messages with the same destination
+ if (moveMatches) {
+ nsMsgKey curKey;
+ currentOp->GetMessageKey(&curKey);
+ matchingFlagKeys.AppendElement(curKey);
+ currentOp->SetPlayingBack(true);
+ m_currentOpsToClear.AppendObject(currentOp);
+ }
+ currentOp = nullptr;
+
+ if (++currentKeyIndex < m_CurrentKeys.Length()) {
+ nsCString nextDestination;
+ nsresult rv = m_currentDB->GetOfflineOpForKey(
+ m_CurrentKeys[currentKeyIndex], false, getter_AddRefs(currentOp));
+ moveMatches = false;
+ if (NS_SUCCEEDED(rv) && currentOp) {
+ nsOfflineImapOperationType opType;
+ currentOp->GetOperation(&opType);
+ if (opType & nsIMsgOfflineImapOperation::kMsgMoved) {
+ currentOp->GetDestinationFolderURI(nextDestination);
+ moveMatches = moveDestination.Equals(nextDestination);
+ }
+ }
+ }
+ } while (currentOp);
+
+ nsCOMPtr<nsIMsgFolder> destFolder;
+ FindFolder(moveDestination, getter_AddRefs(destFolder));
+ // if the dest folder doesn't really exist, these operations are
+ // going to fail, so clear them out and move on.
+ if (!destFolder) {
+ NS_WARNING("trying to playing back move to non-existent folder");
+ ClearCurrentOps();
+ ProcessNextOperation();
+ return;
+ }
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder);
+ if (imapFolder && DestFolderOnSameServer(destFolder)) {
+ uint32_t curFolderFlags;
+ m_currentFolder->GetFlags(&curFolderFlags);
+ bool curFolderOffline = curFolderFlags & nsMsgFolderFlags::Offline;
+ imapFolder->ReplayOfflineMoveCopy(matchingFlagKeys, true, destFolder, this,
+ m_window, curFolderOffline);
+ } else {
+ nsresult rv;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ for (uint32_t keyIndex = 0; keyIndex < matchingFlagKeys.Length();
+ keyIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr;
+ rv = m_currentFolder->GetMessageHeader(
+ matchingFlagKeys.ElementAt(keyIndex), getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) {
+ uint32_t msgSize;
+ // in case of a move, the header has already been deleted,
+ // so we've really got a fake header. We need to get its flags and
+ // size from the offline op to have any chance of doing the move.
+ mailHdr->GetMessageSize(&msgSize);
+ if (!msgSize) {
+ imapMessageFlagsType newImapFlags;
+ uint32_t msgFlags = 0;
+ op->GetMsgSize(&msgSize);
+ op->GetNewFlags(&newImapFlags);
+ // first three bits are the same
+ msgFlags |= (newImapFlags & 0x07);
+ if (newImapFlags & kImapMsgForwardedFlag)
+ msgFlags |= nsMsgMessageFlags::Forwarded;
+ mailHdr->SetFlags(msgFlags);
+ mailHdr->SetMessageSize(msgSize);
+ }
+ messages.AppendElement(mailHdr);
+ }
+ }
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (copyService) {
+ copyService->CopyMessages(m_currentFolder, messages, destFolder, true,
+ this, m_window, false);
+ }
+ }
+}
+
+// I'm tempted to make this a method on nsIMsgFolder, but that interface
+// is already so huge, and there are only a few places in the code that do this.
+// If there end up to be more places that need this, then we can reconsider.
+bool nsImapOfflineSync::DestFolderOnSameServer(nsIMsgFolder* destFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> srcServer;
+ nsCOMPtr<nsIMsgIncomingServer> dstServer;
+
+ bool sameServer = false;
+ if (NS_SUCCEEDED(m_currentFolder->GetServer(getter_AddRefs(srcServer))) &&
+ NS_SUCCEEDED(destFolder->GetServer(getter_AddRefs(dstServer))))
+ dstServer->Equals(srcServer, &sameServer);
+ return sameServer;
+}
+
+void nsImapOfflineSync::ProcessCopyOperation(
+ nsIMsgOfflineImapOperation* aCurrentOp) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = aCurrentOp;
+
+ nsTArray<nsMsgKey> matchingFlagKeys;
+ uint32_t currentKeyIndex = m_KeyIndex;
+ nsCString copyDestination;
+ currentOp->GetCopyDestination(0, getter_Copies(copyDestination));
+ bool copyMatches = true;
+ nsresult rv;
+
+ do { // loop for all messages with the same destination
+ if (copyMatches) {
+ nsMsgKey curKey;
+ currentOp->GetMessageKey(&curKey);
+ matchingFlagKeys.AppendElement(curKey);
+ currentOp->SetPlayingBack(true);
+ m_currentOpsToClear.AppendObject(currentOp);
+ }
+ currentOp = nullptr;
+
+ if (++currentKeyIndex < m_CurrentKeys.Length()) {
+ nsCString nextDestination;
+ rv = m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex],
+ false, getter_AddRefs(currentOp));
+ copyMatches = false;
+ if (NS_SUCCEEDED(rv) && currentOp) {
+ nsOfflineImapOperationType opType;
+ currentOp->GetOperation(&opType);
+ if (opType & nsIMsgOfflineImapOperation::kMsgCopy) {
+ currentOp->GetCopyDestination(0, getter_Copies(nextDestination));
+ copyMatches = copyDestination.Equals(nextDestination);
+ }
+ }
+ }
+ } while (currentOp);
+
+ nsAutoCString uids;
+ nsCOMPtr<nsIMsgFolder> destFolder;
+ FindFolder(copyDestination, getter_AddRefs(destFolder));
+ // if the dest folder doesn't really exist, these operations are
+ // going to fail, so clear them out and move on.
+ if (!destFolder) {
+ NS_ERROR("trying to playing back copy to non-existent folder");
+ ClearCurrentOps();
+ ProcessNextOperation();
+ return;
+ }
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder);
+ if (imapFolder && DestFolderOnSameServer(destFolder)) {
+ uint32_t curFolderFlags;
+ m_currentFolder->GetFlags(&curFolderFlags);
+ bool curFolderOffline = curFolderFlags & nsMsgFolderFlags::Offline;
+ rv = imapFolder->ReplayOfflineMoveCopy(matchingFlagKeys, false, destFolder,
+ this, m_window, curFolderOffline);
+ } else {
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ for (uint32_t keyIndex = 0; keyIndex < matchingFlagKeys.Length();
+ keyIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr;
+ rv = m_currentFolder->GetMessageHeader(
+ matchingFlagKeys.ElementAt(keyIndex), getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) messages.AppendElement(mailHdr);
+ }
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (copyService)
+ copyService->CopyMessages(m_currentFolder, messages, destFolder, false,
+ this, m_window, false);
+ }
+}
+
+void nsImapOfflineSync::ProcessEmptyTrash() {
+ m_currentFolder->EmptyTrash(this);
+ ClearDB(); // EmptyTrash closes and deletes the trash db.
+}
+
+// returns true if we found a folder to create, false if we're done creating
+// folders.
+bool nsImapOfflineSync::CreateOfflineFolders() {
+ while (m_currentFolder) {
+ uint32_t flags;
+ m_currentFolder->GetFlags(&flags);
+ bool offlineCreate = (flags & nsMsgFolderFlags::CreatedOffline) != 0;
+ if (offlineCreate) {
+ if (CreateOfflineFolder(m_currentFolder)) return true;
+ }
+ AdvanceToNextFolder();
+ }
+ return false;
+}
+
+bool nsImapOfflineSync::CreateOfflineFolder(nsIMsgFolder* folder) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ folder->GetParent(getter_AddRefs(parent));
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent);
+ nsCOMPtr<nsIURI> createFolderURI;
+ nsCString onlineName;
+ imapFolder->GetOnlineName(onlineName);
+
+ NS_ConvertASCIItoUTF16 folderName(onlineName);
+ nsresult rv = imapFolder->PlaybackOfflineFolderCreate(
+ folderName, nullptr, getter_AddRefs(createFolderURI));
+ if (createFolderURI && NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(createFolderURI);
+ if (mailnewsUrl) mailnewsUrl->RegisterListener(this);
+ }
+ return NS_SUCCEEDED(rv) ? true
+ : false; // this is asynch, we have to return and be
+ // called again by the OfflineOpExitFunction
+}
+
+int32_t nsImapOfflineSync::GetCurrentUIDValidity() {
+ if (m_currentFolder) {
+ nsCOMPtr<nsIImapMailFolderSink> imapFolderSink =
+ do_QueryInterface(m_currentFolder);
+ if (imapFolderSink) imapFolderSink->GetUidValidity(&mCurrentUIDValidity);
+ }
+ return mCurrentUIDValidity;
+}
+
+/**
+ * Playing back offline operations is one giant state machine that runs through
+ * ProcessNextOperation.
+ * The first state is creating online any folders created offline (we do this
+ * first, so we can play back any operations in them in the next pass)
+ */
+NS_IMETHODIMP
+nsImapOfflineSync::ProcessNextOperation() {
+ nsresult rv = NS_OK;
+
+ // if we haven't created offline folders, and we're updating all folders,
+ // first, find offline folders to create.
+ if (!m_createdOfflineFolders) {
+ if (m_singleFolderToUpdate) {
+ if (!m_pseudoOffline) {
+ AdvanceToFirstIMAPFolder();
+ if (CreateOfflineFolders()) return NS_OK;
+ }
+ } else {
+ if (CreateOfflineFolders()) return NS_OK;
+ m_currentServer = nullptr;
+ AdvanceToNextFolder();
+ }
+ m_createdOfflineFolders = true;
+ }
+ // if updating one folder only, restore m_currentFolder to that folder
+ if (m_singleFolderToUpdate) m_currentFolder = m_singleFolderToUpdate;
+
+ uint32_t folderFlags;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ while (m_currentFolder && !m_currentDB) {
+ m_currentFolder->GetFlags(&folderFlags);
+ // need to check if folder has offline events, /* or is configured for
+ // offline */ shouldn't need to check if configured for offline use, since
+ // any folder with events should have nsMsgFolderFlags::OfflineEvents set.
+ if (folderFlags &
+ (nsMsgFolderFlags::OfflineEvents /* | nsMsgFolderFlags::Offline */)) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ m_currentFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (db) {
+ m_currentDB = do_QueryInterface(db, &rv);
+ m_currentDB->AddListener(this);
+ }
+ }
+
+ if (m_currentDB) {
+ m_CurrentKeys.Clear();
+ m_KeyIndex = 0;
+ if (NS_FAILED(m_currentDB->ListAllOfflineOpIds(m_CurrentKeys)) ||
+ m_CurrentKeys.IsEmpty()) {
+ ClearDB();
+ folderInfo = nullptr; // can't hold onto folderInfo longer than db
+ m_currentFolder->ClearFlag(nsMsgFolderFlags::OfflineEvents);
+ } else {
+ // trash any ghost msgs
+ bool deletedGhostMsgs = false;
+ for (uint32_t fakeIndex = 0; fakeIndex < m_CurrentKeys.Length();
+ fakeIndex++) {
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp;
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[fakeIndex], false,
+ getter_AddRefs(currentOp));
+ if (currentOp) {
+ nsOfflineImapOperationType opType;
+ currentOp->GetOperation(&opType);
+
+ if (opType == nsIMsgOfflineImapOperation::kMoveResult) {
+ nsMsgKey curKey;
+ currentOp->GetMessageKey(&curKey);
+ m_currentDB->RemoveOfflineOp(currentOp);
+ deletedGhostMsgs = true;
+
+ // Remember the pseudo headers before we delete them,
+ // and when we download new headers, tell listeners about the
+ // message key change between the pseudo headers and the real
+ // downloaded headers. Note that we're not currently sending
+ // a msgsDeleted notification for these headers, but the
+ // db listeners are notified about the deletion.
+ // for imap folders, we should adjust the pending counts, because
+ // we have a header that we know about, but don't have in the db.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder);
+ if (imapFolder) {
+ bool hdrIsRead;
+ m_currentDB->IsRead(curKey, &hdrIsRead);
+ imapFolder->ChangePendingTotal(1);
+ if (!hdrIsRead) imapFolder->ChangePendingUnread(1);
+ imapFolder->AddMoveResultPseudoKey(curKey);
+ }
+ m_currentDB->DeleteMessage(curKey, nullptr, false);
+ }
+ }
+ }
+
+ if (deletedGhostMsgs) m_currentFolder->SummaryChanged();
+
+ m_CurrentKeys.Clear();
+ if (NS_FAILED(m_currentDB->ListAllOfflineOpIds(m_CurrentKeys)) ||
+ m_CurrentKeys.IsEmpty()) {
+ ClearDB();
+ } else if (folderFlags & nsMsgFolderFlags::ImapBox) {
+ // if pseudo offline, falls through to playing ops back.
+ if (!m_pseudoOffline) {
+ // there are operations to playback so check uid validity
+ SetCurrentUIDValidity(0); // force initial invalid state
+ // do a lite select here and hook ourselves up as a listener.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_currentFolder, &rv);
+ if (imapFolder) rv = imapFolder->LiteSelect(this, m_window);
+ // this is async, we will be called again by OnStopRunningUrl.
+ return rv;
+ }
+ }
+ }
+ }
+
+ if (!m_currentDB) {
+ // only advance if we are doing all folders
+ if (!m_singleFolderToUpdate)
+ AdvanceToNextFolder();
+ else
+ m_currentFolder = nullptr; // force update of this folder now.
+ }
+ }
+
+ if (m_currentFolder) m_currentFolder->GetFlags(&folderFlags);
+ // do the current operation
+ if (m_currentDB) {
+ bool currentFolderFinished = false;
+ if (!folderInfo) m_currentDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ // user canceled the lite select! if GetCurrentUIDValidity() == 0
+ if (folderInfo && (m_KeyIndex < m_CurrentKeys.Length()) &&
+ (m_pseudoOffline || (GetCurrentUIDValidity() != 0) ||
+ !(folderFlags & nsMsgFolderFlags::ImapBox))) {
+ int32_t curFolderUidValidity;
+ folderInfo->GetImapUidValidity(&curFolderUidValidity);
+ bool uidvalidityChanged =
+ (!m_pseudoOffline && folderFlags & nsMsgFolderFlags::ImapBox) &&
+ (GetCurrentUIDValidity() != curFolderUidValidity);
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp;
+ if (uidvalidityChanged)
+ DeleteAllOfflineOpsForCurrentDB();
+ else
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false,
+ getter_AddRefs(currentOp));
+
+ if (currentOp) {
+ nsOfflineImapOperationType opType;
+ currentOp->GetOperation(&opType);
+ // loop until we find the next db record that matches the current
+ // playback operation
+ while (currentOp && !(opType & mCurrentPlaybackOpType)) {
+ // remove operations with no type.
+ if (!opType) m_currentDB->RemoveOfflineOp(currentOp);
+ currentOp = nullptr;
+ ++m_KeyIndex;
+ if (m_KeyIndex < m_CurrentKeys.Length())
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false,
+ getter_AddRefs(currentOp));
+ if (currentOp) currentOp->GetOperation(&opType);
+ }
+ // if we did not find a db record that matches the current playback
+ // operation, then move to the next playback operation and recurse.
+ if (!currentOp) {
+ // we are done with the current type
+ if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kFlagsChanged) {
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kAddKeywords;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAddKeywords) {
+ mCurrentPlaybackOpType =
+ nsIMsgOfflineImapOperation::kRemoveKeywords;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kRemoveKeywords) {
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kMsgCopy;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kMsgCopy) {
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kMsgMoved;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kMsgMoved) {
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kAppendDraft;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAppendDraft) {
+ mCurrentPlaybackOpType =
+ nsIMsgOfflineImapOperation::kAppendTemplate;
+ // recurse to deal with next type of operation
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAppendTemplate) {
+ mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kDeleteAllMsgs;
+ m_KeyIndex = 0;
+ ProcessNextOperation();
+ } else {
+ DeleteAllOfflineOpsForCurrentDB();
+ currentFolderFinished = true;
+ }
+
+ } else {
+ if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kFlagsChanged)
+ ProcessFlagOperation(currentOp);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAddKeywords ||
+ mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kRemoveKeywords)
+ ProcessKeywordOperation(currentOp);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kMsgCopy)
+ ProcessCopyOperation(currentOp);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kMsgMoved)
+ ProcessMoveOperation(currentOp);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAppendDraft)
+ ProcessAppendMsgOperation(currentOp,
+ nsIMsgOfflineImapOperation::kAppendDraft);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kAppendTemplate)
+ ProcessAppendMsgOperation(
+ currentOp, nsIMsgOfflineImapOperation::kAppendTemplate);
+ else if (mCurrentPlaybackOpType ==
+ nsIMsgOfflineImapOperation::kDeleteAllMsgs) {
+ // empty trash is going to delete the db, so we'd better release the
+ // reference to the offline operation first.
+ currentOp = nullptr;
+ ProcessEmptyTrash();
+ } else
+ NS_WARNING("invalid playback op type");
+ }
+ } else
+ currentFolderFinished = true;
+ } else
+ currentFolderFinished = true;
+
+ if (currentFolderFinished) {
+ ClearDB();
+ if (!m_singleFolderToUpdate) {
+ AdvanceToNextFolder();
+ ProcessNextOperation();
+ return NS_OK;
+ }
+ m_currentFolder = nullptr;
+ }
+ }
+
+ if (!m_currentFolder && !m_mailboxupdatesStarted) {
+ m_mailboxupdatesStarted = true;
+
+ // if we are updating more than one folder then we need the iterator
+ if (!m_singleFolderToUpdate) {
+ m_currentServer = nullptr;
+ AdvanceToNextFolder();
+ }
+ if (m_singleFolderToUpdate) {
+ m_singleFolderToUpdate->ClearFlag(nsMsgFolderFlags::OfflineEvents);
+ m_singleFolderToUpdate->UpdateFolder(m_window);
+ }
+ }
+ // if we get here, then I *think* we're done. Not sure, though.
+#ifdef DEBUG_bienvenu
+ printf("done with offline imap sync\n");
+#endif
+ nsCOMPtr<nsIUrlListener> saveListener = m_listener;
+ m_listener = nullptr;
+
+ if (saveListener)
+ saveListener->OnStopRunningUrl(nullptr /* don't know url */, rv);
+ return rv;
+}
+
+void nsImapOfflineSync::DeleteAllOfflineOpsForCurrentDB() {
+ m_KeyIndex = 0;
+ nsCOMPtr<nsIMsgOfflineImapOperation> currentOp;
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false,
+ getter_AddRefs(currentOp));
+ while (currentOp) {
+ // NS_ASSERTION(currentOp->GetOperationFlags() == 0);
+ // delete any ops that have already played back
+ m_currentDB->RemoveOfflineOp(currentOp);
+ currentOp = nullptr;
+
+ if (++m_KeyIndex < m_CurrentKeys.Length())
+ m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false,
+ getter_AddRefs(currentOp));
+ }
+ m_currentDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ // turn off nsMsgFolderFlags::OfflineEvents
+ if (m_currentFolder)
+ m_currentFolder->ClearFlag(nsMsgFolderFlags::OfflineEvents);
+}
+
+nsImapOfflineDownloader::nsImapOfflineDownloader(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener)
+ : nsImapOfflineSync() {
+ Init(aMsgWindow, aListener, nullptr, false);
+ // pause auto-sync service
+ nsresult rv;
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) autoSyncMgr->Pause();
+}
+
+nsImapOfflineDownloader::~nsImapOfflineDownloader() {}
+
+NS_IMETHODIMP
+nsImapOfflineDownloader::ProcessNextOperation() {
+ nsresult rv = NS_OK;
+ m_mailboxupdatesStarted = true;
+
+ if (!m_mailboxupdatesFinished) {
+ if (AdvanceToNextServer()) {
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ m_currentServer->GetRootFolder(getter_AddRefs(rootMsgFolder));
+ nsCOMPtr<nsIMsgFolder> inbox;
+ if (rootMsgFolder) {
+ // Update the INBOX first so the updates on the remaining
+ // folders pickup the results of any filter moves.
+ rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox,
+ getter_AddRefs(inbox));
+ if (inbox) {
+ nsCOMPtr<nsIMsgFolder> offlineImapFolder;
+ nsCOMPtr<nsIMsgImapMailFolder> imapInbox = do_QueryInterface(inbox);
+ if (imapInbox) {
+ rootMsgFolder->GetFolderWithFlags(
+ nsMsgFolderFlags::Offline, getter_AddRefs(offlineImapFolder));
+ if (!offlineImapFolder) {
+ // no imap folders configured for offline use - check if the
+ // account is set up so that we always download inbox msg bodies
+ // for offline use
+ nsCOMPtr<nsIImapIncomingServer> imapServer =
+ do_QueryInterface(m_currentServer);
+ if (imapServer) {
+ bool downloadBodiesOnGetNewMail = false;
+ imapServer->GetDownloadBodiesOnGetNewMail(
+ &downloadBodiesOnGetNewMail);
+ if (downloadBodiesOnGetNewMail) offlineImapFolder = inbox;
+ }
+ }
+ }
+ // if this isn't an imap inbox, or we have an offline imap sub-folder,
+ // then update the inbox. otherwise, it's an imap inbox for an account
+ // with no folders configured for offline use, so just advance to the
+ // next server.
+ if (!imapInbox || offlineImapFolder) {
+ // here we should check if this a pop3 server/inbox, and the user
+ // doesn't want to download pop3 mail for offline use.
+ if (!imapInbox) {
+ }
+ rv = inbox->GetNewMessages(m_window, this);
+ if (NS_SUCCEEDED(rv)) return rv; // otherwise, fall through.
+ }
+ }
+ }
+ return ProcessNextOperation(); // recurse and do next server.
+ }
+ m_allServers.Clear();
+ m_mailboxupdatesFinished = true;
+ }
+
+ while (AdvanceToNextFolder()) {
+ uint32_t folderFlags;
+
+ ClearDB();
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder;
+ if (m_currentFolder) imapFolder = do_QueryInterface(m_currentFolder);
+ m_currentFolder->GetFlags(&folderFlags);
+ // need to check if folder has offline events, or is configured for offline
+ if (imapFolder && folderFlags & nsMsgFolderFlags::Offline &&
+ !(folderFlags & nsMsgFolderFlags::Virtual)) {
+ rv = m_currentFolder->DownloadAllForOffline(this, m_window);
+ if (NS_SUCCEEDED(rv) || rv == NS_BINDING_ABORTED) return rv;
+ // if this fails and the user didn't cancel/stop, fall through to code
+ // that advances to next folder
+ }
+ }
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapOfflineSync::OnStartCopy() {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */
+NS_IMETHODIMP nsImapOfflineSync::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void SetMessageKey (in uint32_t aKey); */
+NS_IMETHODIMP nsImapOfflineSync::SetMessageKey(uint32_t aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* [noscript] void GetMessageId (in nsCString aMessageId); */
+NS_IMETHODIMP nsImapOfflineSync::GetMessageId(nsACString& messageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnStopCopy (in nsresult aStatus); */
+NS_IMETHODIMP nsImapOfflineSync::OnStopCopy(nsresult aStatus) {
+ return OnStopRunningUrl(nullptr, aStatus);
+}
+
+void nsImapOfflineSync::ClearDB() {
+ m_currentOpsToClear.Clear();
+ if (m_currentDB) m_currentDB->RemoveListener(this);
+ m_currentDB = nullptr;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property,
+ bool aPreChange, uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged,
+ uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapOfflineSync::OnHdrAdded(nsIMsgDBHdr* aHdrAdded, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+/* void OnParentChanged (in nsMsgKey aKeyChanged, in nsMsgKey oldParent, in
+ * nsMsgKey newParent, in nsIDBChangeListener aInstigator); */
+NS_IMETHODIMP
+nsImapOfflineSync::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+/* void OnAnnouncerGoingAway (in nsIDBChangeAnnouncer instigator); */
+NS_IMETHODIMP
+nsImapOfflineSync::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) {
+ ClearDB();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapOfflineSync::OnEvent(nsIMsgDatabase* aDB,
+ const char* aEvent) {
+ return NS_OK;
+}
+
+/* void OnReadChanged (in nsIDBChangeListener instigator); */
+NS_IMETHODIMP
+nsImapOfflineSync::OnReadChanged(nsIDBChangeListener* instigator) {
+ return NS_OK;
+}
+
+/* void OnJunkScoreChanged (in nsIDBChangeListener instigator); */
+NS_IMETHODIMP
+nsImapOfflineSync::OnJunkScoreChanged(nsIDBChangeListener* instigator) {
+ return NS_OK;
+}
diff --git a/comm/mailnews/imap/src/nsImapOfflineSync.h b/comm/mailnews/imap/src/nsImapOfflineSync.h
new file mode 100644
index 0000000000..6a8ccc8f99
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapOfflineSync.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsImapOfflineSync_H_
+#define _nsImapOfflineSync_H_
+
+#include "mozilla/Attributes.h"
+#include "nsIMsgDatabase.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgOfflineImapOperation.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgFolder.h"
+#include "nsCOMArray.h"
+#include "nsIDBChangeListener.h"
+#include "nsIImapOfflineSync.h"
+
+class nsImapOfflineSync : public nsIUrlListener,
+ public nsIMsgCopyServiceListener,
+ public nsIDBChangeListener,
+ public nsIImapOfflineSync {
+ public: // set to one folder to playback one folder only
+ nsImapOfflineSync();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+ NS_DECL_NSIDBCHANGELISTENER
+ NS_DECL_NSIIMAPOFFLINESYNC
+
+ int32_t GetCurrentUIDValidity();
+ void SetCurrentUIDValidity(int32_t uidvalidity) {
+ mCurrentUIDValidity = uidvalidity;
+ }
+
+ void SetPseudoOffline(bool pseudoOffline) { m_pseudoOffline = pseudoOffline; }
+ bool ProcessingStaleFolderUpdate() {
+ return m_singleFolderToUpdate != nullptr;
+ }
+
+ bool CreateOfflineFolder(nsIMsgFolder* folder);
+ void SetWindow(nsIMsgWindow* window);
+
+ protected:
+ virtual ~nsImapOfflineSync();
+
+ bool CreateOfflineFolders();
+ bool DestFolderOnSameServer(nsIMsgFolder* destFolder);
+ bool AdvanceToNextServer();
+ bool AdvanceToNextFolder();
+ void AdvanceToFirstIMAPFolder();
+ void DeleteAllOfflineOpsForCurrentDB();
+ void ClearCurrentOps();
+ // Clears m_currentDB, and unregister listener.
+ void ClearDB();
+ void ProcessFlagOperation(nsIMsgOfflineImapOperation* currentOp);
+ void ProcessKeywordOperation(nsIMsgOfflineImapOperation* op);
+ void ProcessMoveOperation(nsIMsgOfflineImapOperation* currentOp);
+ void ProcessCopyOperation(nsIMsgOfflineImapOperation* currentOp);
+ void ProcessEmptyTrash();
+ void ProcessAppendMsgOperation(nsIMsgOfflineImapOperation* currentOp,
+ nsOfflineImapOperationType opType);
+
+ nsCOMPtr<nsIMsgFolder> m_currentFolder;
+ nsCOMPtr<nsIMsgFolder> m_singleFolderToUpdate;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsTArray<RefPtr<nsIMsgIncomingServer>> m_allServers;
+ nsCOMPtr<nsIMsgIncomingServer> m_currentServer;
+ // Folders left to consider on m_currentServer.
+ nsTArray<RefPtr<nsIMsgFolder>> m_folderQueue;
+
+ nsCOMPtr<nsIFile> m_curTempFile;
+
+ nsTArray<nsMsgKey> m_CurrentKeys;
+ nsCOMArray<nsIMsgOfflineImapOperation> m_currentOpsToClear;
+ uint32_t m_KeyIndex;
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> m_currentDB;
+ nsCOMPtr<nsIUrlListener> m_listener;
+ int32_t mCurrentUIDValidity;
+ int32_t mCurrentPlaybackOpType; // kFlagsChanged -> kMsgCopy -> kMsgMoved
+ bool m_mailboxupdatesStarted;
+ bool m_mailboxupdatesFinished;
+ bool m_pseudoOffline; // for queueing online events in offline db
+ bool m_createdOfflineFolders;
+};
+
+class nsImapOfflineDownloader : public nsImapOfflineSync {
+ public:
+ nsImapOfflineDownloader(nsIMsgWindow* window, nsIUrlListener* listener);
+ virtual ~nsImapOfflineDownloader();
+ NS_IMETHOD ProcessNextOperation() override; // this kicks off download
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapProtocol.cpp b/comm/mailnews/imap/src/nsImapProtocol.cpp
new file mode 100644
index 0000000000..b0a9214749
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapProtocol.cpp
@@ -0,0 +1,9915 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// as does this
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMsgUtils.h"
+
+#include "nsImapStringBundle.h"
+#include "nsVersionComparator.h"
+
+#include "nsThreadUtils.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsImapCore.h"
+#include "nsImapProtocol.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsImapMailFolder.h"
+#include "nsIMsgAccountManager.h"
+#include "nsImapServerResponseParser.h"
+#include "nspr.h"
+#include "plbase64.h"
+#include "nsIEventTarget.h"
+#include "nsIImapService.h"
+#include "nsISocketTransportService.h"
+#include "nsIStreamListenerTee.h"
+#include "nsIInputStreamPump.h"
+#include "nsNetUtil.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIPipe.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgMessageFlags.h"
+#include "nsTextFormatter.h"
+#include "nsTransportUtils.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgI18N.h"
+// for the memory cache...
+#include "nsICacheEntry.h"
+#include "nsICacheStorage.h"
+#include "nsICacheEntryOpenCallback.h"
+#include "CacheObserver.h"
+#include "nsIURIMutator.h"
+
+#include "nsIDocShell.h"
+#include "nsILoadInfo.h"
+#include "nsCOMPtr.h"
+#include "nsMimeTypes.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsXPCOMCIDInternal.h"
+#include "nsIXULAppInfo.h"
+#include "nsSocketTransportService2.h"
+#include "nsSyncRunnableHelpers.h"
+#include "nsICancelable.h"
+
+// netlib required files
+#include "nsIStreamListener.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsImapUtils.h"
+#include "nsIStreamConverterService.h"
+#include "nsIProxyInfo.h"
+#include "nsITLSSocketControl.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsProxyRelease.h"
+#include "nsDebug.h"
+#include "nsMsgCompressIStream.h"
+#include "nsMsgCompressOStream.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsIPrincipal.h"
+#include "nsContentSecurityManager.h"
+
+// imap event sinks
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapServerSink.h"
+#include "nsIImapMessageSink.h"
+
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/NullPrincipal.h"
+
+// TLS alerts
+#include "NSSErrorsService.h"
+
+#include "mozilla/SyncRunnable.h"
+
+using namespace mozilla;
+
+LazyLogModule IMAP("IMAP");
+LazyLogModule IMAP_CS("IMAP_CS");
+LazyLogModule IMAPCache("IMAPCache");
+
+#define ONE_SECOND ((uint32_t)1000) // one second
+
+#define OUTPUT_BUFFER_SIZE (4096 * 2)
+
+#define IMAP_ENV_HEADERS "From To Cc Bcc Subject Date Message-ID "
+#define IMAP_DB_HEADERS \
+ "Priority X-Priority References Newsgroups In-Reply-To Content-Type " \
+ "Reply-To"
+#define IMAP_ENV_AND_DB_HEADERS IMAP_ENV_HEADERS IMAP_DB_HEADERS
+static const PRIntervalTime kImapSleepTime = PR_MillisecondsToInterval(60000);
+static int32_t gPromoteNoopToCheckCount = 0;
+static const uint32_t kFlagChangesBeforeCheck = 10;
+static const int32_t kMaxSecondsBeforeCheck = 600;
+
+class AutoProxyReleaseMsgWindow {
+ public:
+ AutoProxyReleaseMsgWindow() : mMsgWindow() {}
+ ~AutoProxyReleaseMsgWindow() {
+ NS_ReleaseOnMainThread("AutoProxyReleaseMsgWindow::mMsgWindow",
+ dont_AddRef(mMsgWindow));
+ }
+ nsIMsgWindow** StartAssignment() {
+ MOZ_ASSERT(!mMsgWindow);
+ return &mMsgWindow;
+ }
+ operator nsIMsgWindow*() { return mMsgWindow; }
+
+ private:
+ nsIMsgWindow* mMsgWindow;
+};
+
+nsIMsgWindow** getter_AddRefs(AutoProxyReleaseMsgWindow& aSmartPtr) {
+ return aSmartPtr.StartAssignment();
+}
+
+NS_IMPL_ISUPPORTS(nsMsgImapHdrXferInfo, nsIImapHeaderXferInfo)
+
+nsMsgImapHdrXferInfo::nsMsgImapHdrXferInfo() : m_hdrInfos(kNumHdrsToXfer) {
+ m_nextFreeHdrInfo = 0;
+}
+
+nsMsgImapHdrXferInfo::~nsMsgImapHdrXferInfo() {}
+
+NS_IMETHODIMP nsMsgImapHdrXferInfo::GetNumHeaders(int32_t* aNumHeaders) {
+ *aNumHeaders = m_nextFreeHdrInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgImapHdrXferInfo::GetHeader(int32_t hdrIndex,
+ nsIImapHeaderInfo** aResult) {
+ // If the header index is more than (or equal to) our next free pointer, then
+ // its a header we haven't really got and the caller has done something
+ // wrong.
+ NS_ENSURE_TRUE(hdrIndex < m_nextFreeHdrInfo, NS_ERROR_NULL_POINTER);
+
+ NS_IF_ADDREF(*aResult = m_hdrInfos.SafeObjectAt(hdrIndex));
+ if (!*aResult) return NS_ERROR_NULL_POINTER;
+ return NS_OK;
+}
+
+static const int32_t kInitLineHdrCacheSize = 512; // should be about right
+
+nsIImapHeaderInfo* nsMsgImapHdrXferInfo::StartNewHdr() {
+ if (m_nextFreeHdrInfo >= kNumHdrsToXfer) return nullptr;
+
+ nsIImapHeaderInfo* result = m_hdrInfos.SafeObjectAt(m_nextFreeHdrInfo++);
+ if (result) return result;
+
+ nsMsgImapLineDownloadCache* lineCache = new nsMsgImapLineDownloadCache();
+ if (!lineCache) return nullptr;
+
+ lineCache->GrowBuffer(kInitLineHdrCacheSize);
+
+ m_hdrInfos.AppendObject(lineCache);
+
+ return lineCache;
+}
+
+// maybe not needed...
+void nsMsgImapHdrXferInfo::FinishCurrentHdr() {
+ // nothing to do?
+}
+
+void nsMsgImapHdrXferInfo::ResetAll() {
+ int32_t count = m_hdrInfos.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsIImapHeaderInfo* hdrInfo = m_hdrInfos[i];
+ if (hdrInfo) hdrInfo->ResetCache();
+ }
+ m_nextFreeHdrInfo = 0;
+}
+
+void nsMsgImapHdrXferInfo::ReleaseAll() {
+ m_hdrInfos.Clear();
+ m_nextFreeHdrInfo = 0;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgImapLineDownloadCache, nsIImapHeaderInfo)
+
+// **** helper class for downloading line ****
+nsMsgImapLineDownloadCache::nsMsgImapLineDownloadCache() {
+ fLineInfo = (msg_line_info*)PR_CALLOC(sizeof(msg_line_info));
+ fLineInfo->uidOfMessage = nsMsgKey_None;
+ m_msgSize = 0;
+}
+
+nsMsgImapLineDownloadCache::~nsMsgImapLineDownloadCache() {
+ PR_Free(fLineInfo);
+}
+
+uint32_t nsMsgImapLineDownloadCache::CurrentUID() {
+ return fLineInfo->uidOfMessage;
+}
+
+uint32_t nsMsgImapLineDownloadCache::SpaceAvailable() {
+ MOZ_ASSERT(kDownLoadCacheSize >= m_bufferPos);
+ if (kDownLoadCacheSize <= m_bufferPos) return 0;
+ return kDownLoadCacheSize - m_bufferPos;
+}
+
+msg_line_info* nsMsgImapLineDownloadCache::GetCurrentLineInfo() {
+ AppendBuffer("", 1); // null terminate the buffer
+ fLineInfo->adoptedMessageLine = GetBuffer();
+ return fLineInfo;
+}
+
+NS_IMETHODIMP nsMsgImapLineDownloadCache::ResetCache() {
+ ResetWritePos();
+ return NS_OK;
+}
+
+bool nsMsgImapLineDownloadCache::CacheEmpty() { return m_bufferPos == 0; }
+
+NS_IMETHODIMP nsMsgImapLineDownloadCache::CacheLine(const char* line,
+ uint32_t uid) {
+ fLineInfo->uidOfMessage = uid;
+ return AppendString(line);
+}
+
+/* attribute nsMsgKey msgUid; */
+NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgUid(nsMsgKey* aMsgUid) {
+ *aMsgUid = fLineInfo->uidOfMessage;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgImapLineDownloadCache::SetMsgUid(nsMsgKey aMsgUid) {
+ fLineInfo->uidOfMessage = aMsgUid;
+ return NS_OK;
+}
+
+/* attribute long msgSize; */
+NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgSize(int32_t* aMsgSize) {
+ *aMsgSize = m_msgSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgImapLineDownloadCache::SetMsgSize(int32_t aMsgSize) {
+ m_msgSize = aMsgSize;
+ return NS_OK;
+}
+
+/* readonly attribute ACString msgHdrs; */
+NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgHdrs(nsACString& aMsgHdrs) {
+ AppendBuffer("", 1); // null terminate the buffer
+ aMsgHdrs.Assign(GetBuffer());
+ return NS_OK;
+}
+
+// The following macros actually implement addref, release and query interface
+// for our component.
+NS_IMPL_ADDREF_INHERITED(nsImapProtocol, nsMsgProtocol)
+NS_IMPL_RELEASE_INHERITED(nsImapProtocol, nsMsgProtocol)
+
+NS_INTERFACE_MAP_BEGIN(nsImapProtocol)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIImapProtocol)
+ NS_INTERFACE_MAP_ENTRY(nsIImapProtocol)
+ NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIImapProtocolSink)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgAsyncPromptListener)
+NS_INTERFACE_MAP_END
+
+static int32_t gTooFastTime = 2;
+static int32_t gIdealTime = 4;
+static int32_t gChunkAddSize = 16384;
+static int32_t gChunkSize = 250000;
+static int32_t gChunkThreshold = gChunkSize + gChunkSize / 2;
+static bool gChunkSizeDirty = false;
+static bool gFetchByChunks = true;
+static bool gInitialized = false;
+static bool gHideUnusedNamespaces = true;
+static bool gHideOtherUsersFromList = false;
+static bool gUseEnvelopeCmd = false;
+static bool gUseLiteralPlus = true;
+static bool gExpungeAfterDelete = false;
+static bool gCheckDeletedBeforeExpunge = false; // bug 235004
+static int32_t gResponseTimeout = 100;
+static int32_t gAppendTimeout = gResponseTimeout / 5;
+static nsImapProtocol::TCPKeepalive gTCPKeepalive;
+static bool gUseDiskCache2 = true; // Use disk cache instead of memory cache
+static nsCOMPtr<nsICacheStorage> gCache2Storage;
+
+// let delete model control expunging, i.e., don't ever expunge when the
+// user chooses the imap delete model, otherwise, expunge when over the
+// threshold. This is the normal TB behavior.
+static const int32_t kAutoExpungeDeleteModel = 0; // default
+// Expunge whenever the folder is opened regardless of delete model or number
+// of marked deleted messages present in the folder.
+static const int32_t kAutoExpungeAlways = 1;
+// Expunge when over the threshold, independent of the delete model.
+static const int32_t kAutoExpungeOnThreshold = 2;
+// Set mail.imap.expunge_option to kAutoExpungeNever to NEVER do an auto-
+// expunge. This is useful when doing a bulk transfer of folders and messages
+// between imap servers.
+static const int32_t kAutoExpungeNever = 3;
+
+static int32_t gExpungeOption = kAutoExpungeDeleteModel;
+static int32_t gExpungeThreshold = 20;
+
+const int32_t kAppBufSize = 100;
+// can't use static nsCString because it shows up as a leak.
+static char gAppName[kAppBufSize];
+static char gAppVersion[kAppBufSize];
+
+nsresult nsImapProtocol::GlobalInitialization(nsIPrefBranch* aPrefBranch) {
+ gInitialized = true;
+
+ aPrefBranch->GetIntPref("mail.imap.chunk_fast",
+ &gTooFastTime); // secs we read too little too fast
+ aPrefBranch->GetIntPref("mail.imap.chunk_ideal",
+ &gIdealTime); // secs we read enough in good time
+ aPrefBranch->GetIntPref(
+ "mail.imap.chunk_add",
+ &gChunkAddSize); // buffer size to add when wasting time
+ aPrefBranch->GetIntPref("mail.imap.chunk_size", &gChunkSize);
+ aPrefBranch->GetIntPref("mail.imap.min_chunk_size_threshold",
+ &gChunkThreshold);
+ aPrefBranch->GetBoolPref("mail.imap.hide_other_users",
+ &gHideOtherUsersFromList);
+ aPrefBranch->GetBoolPref("mail.imap.hide_unused_namespaces",
+ &gHideUnusedNamespaces);
+ aPrefBranch->GetIntPref("mail.imap.noop_check_count",
+ &gPromoteNoopToCheckCount);
+ aPrefBranch->GetBoolPref("mail.imap.use_envelope_cmd", &gUseEnvelopeCmd);
+ aPrefBranch->GetBoolPref("mail.imap.use_literal_plus", &gUseLiteralPlus);
+ aPrefBranch->GetBoolPref("mail.imap.expunge_after_delete",
+ &gExpungeAfterDelete);
+ aPrefBranch->GetBoolPref("mail.imap.use_disk_cache2", &gUseDiskCache2);
+ aPrefBranch->GetBoolPref("mail.imap.check_deleted_before_expunge",
+ &gCheckDeletedBeforeExpunge);
+ aPrefBranch->GetIntPref("mail.imap.expunge_option", &gExpungeOption);
+ aPrefBranch->GetIntPref("mail.imap.expunge_threshold_number",
+ &gExpungeThreshold);
+ aPrefBranch->GetIntPref("mailnews.tcptimeout", &gResponseTimeout);
+ gAppendTimeout = gResponseTimeout / 5;
+
+ gTCPKeepalive.enabled.store(false, std::memory_order_relaxed);
+ gTCPKeepalive.idleTimeS.store(-1, std::memory_order_relaxed);
+ gTCPKeepalive.retryIntervalS.store(-1, std::memory_order_relaxed);
+
+ nsCOMPtr<nsIXULAppInfo> appInfo(do_GetService(XULAPPINFO_SERVICE_CONTRACTID));
+
+ if (appInfo) {
+ nsCString appName, appVersion;
+ appInfo->GetName(appName);
+ appInfo->GetVersion(appVersion);
+ PL_strncpyz(gAppName, appName.get(), kAppBufSize);
+ PL_strncpyz(gAppVersion, appVersion.get(), kAppBufSize);
+ }
+ return NS_OK;
+}
+
+class nsImapTransportEventSink final : public nsITransportEventSink {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITRANSPORTEVENTSINK
+
+ private:
+ friend class nsImapProtocol;
+
+ virtual ~nsImapTransportEventSink() = default;
+ nsresult ApplyTCPKeepalive(nsISocketTransport* aTransport);
+
+ nsCOMPtr<nsITransportEventSink> m_proxy;
+};
+
+NS_IMPL_ISUPPORTS(nsImapTransportEventSink, nsITransportEventSink)
+
+NS_IMETHODIMP
+nsImapTransportEventSink::OnTransportStatus(nsITransport* aTransport,
+ nsresult aStatus, int64_t aProgress,
+ int64_t aProgressMax) {
+ if (aStatus == NS_NET_STATUS_CONNECTED_TO) {
+ nsCOMPtr<nsISocketTransport> sockTrans(do_QueryInterface(aTransport));
+ if (!NS_WARN_IF(!sockTrans)) ApplyTCPKeepalive(sockTrans);
+ }
+
+ if (NS_WARN_IF(!m_proxy)) return NS_OK;
+
+ return m_proxy->OnTransportStatus(aTransport, aStatus, aProgress,
+ aProgressMax);
+}
+
+nsresult nsImapTransportEventSink::ApplyTCPKeepalive(
+ nsISocketTransport* aTransport) {
+ nsresult rv;
+
+ bool kaEnabled = gTCPKeepalive.enabled.load(std::memory_order_relaxed);
+ if (kaEnabled) {
+ // TCP keepalive idle time, don't mistake with IMAP IDLE.
+ int32_t kaIdleTime =
+ gTCPKeepalive.idleTimeS.load(std::memory_order_relaxed);
+ int32_t kaRetryInterval =
+ gTCPKeepalive.retryIntervalS.load(std::memory_order_relaxed);
+
+ if (kaIdleTime < 0 || kaRetryInterval < 0) {
+ if (NS_WARN_IF(!net::gSocketTransportService))
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (kaIdleTime < 0) {
+ rv = net::gSocketTransportService->GetKeepaliveIdleTime(&kaIdleTime);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("GetKeepaliveIdleTime() failed, %" PRIx32,
+ static_cast<uint32_t>(rv)));
+ return rv;
+ }
+ }
+ if (kaRetryInterval < 0) {
+ rv = net::gSocketTransportService->GetKeepaliveRetryInterval(
+ &kaRetryInterval);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("GetKeepaliveRetryInterval() failed, %" PRIx32,
+ static_cast<uint32_t>(rv)));
+ return rv;
+ }
+ }
+
+ MOZ_ASSERT(kaIdleTime > 0);
+ MOZ_ASSERT(kaRetryInterval > 0);
+ rv = aTransport->SetKeepaliveVals(kaIdleTime, kaRetryInterval);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("SetKeepaliveVals(%" PRId32 ", %" PRId32 ") failed, %" PRIx32,
+ kaIdleTime, kaRetryInterval, static_cast<uint32_t>(rv)));
+ return rv;
+ }
+ }
+
+ rv = aTransport->SetKeepaliveEnabled(kaEnabled);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("SetKeepaliveEnabled(%s) failed, %" PRIx32,
+ kaEnabled ? "true" : "false", static_cast<uint32_t>(rv)));
+ return rv;
+ }
+ return NS_OK;
+}
+
+// This runnable runs on IMAP thread.
+class nsImapProtocolMainLoopRunnable final : public mozilla::Runnable {
+ public:
+ explicit nsImapProtocolMainLoopRunnable(nsImapProtocol* aProtocol)
+ : mozilla::Runnable("nsImapProtocolEventLoopRunnable"),
+ mProtocol(aProtocol) {}
+
+ NS_IMETHOD Run() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (!mProtocol->RunImapThreadMainLoop()) {
+ // We already run another IMAP event loop.
+ return NS_OK;
+ }
+
+ // Release protocol object on the main thread to avoid destruction of
+ // nsImapProtocol on the IMAP thread, which causes grief for weak
+ // references.
+ NS_ReleaseOnMainThread("nsImapProtocol::this", mProtocol.forget());
+
+ // shutdown this thread, but do it from the main thread
+ nsCOMPtr<nsIThread> imapThread(do_GetCurrentThread());
+ if (NS_FAILED(NS_DispatchToMainThread(
+ NS_NewRunnableFunction("nsImapProtorolMainLoopRunnable::Run",
+ [imapThread = std::move(imapThread)]() {
+ imapThread->Shutdown();
+ })))) {
+ NS_WARNING("Failed to dispatch nsImapThreadShutdownEvent");
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<nsImapProtocol> mProtocol;
+};
+
+nsImapProtocol::nsImapProtocol()
+ : nsMsgProtocol(nullptr),
+ m_dataAvailableMonitor("imapDataAvailable"),
+ m_urlReadyToRunMonitor("imapUrlReadyToRun"),
+ m_pseudoInterruptMonitor("imapPseudoInterrupt"),
+ m_dataMemberMonitor("imapDataMember"),
+ m_threadDeathMonitor("imapThreadDeath"),
+ m_waitForBodyIdsMonitor("imapWaitForBodyIds"),
+ m_fetchBodyListMonitor("imapFetchBodyList"),
+ m_passwordReadyMonitor("imapPasswordReady"),
+ mLock("nsImapProtocol.mLock"),
+ m_parser(*this) {
+ m_urlInProgress = false;
+ m_idle = false;
+ m_retryUrlOnError = false;
+ m_useIdle = true; // by default, use it
+ m_useCondStore = true;
+ m_useCompressDeflate = true;
+ m_ignoreExpunges = false;
+ m_prefAuthMethods = kCapabilityUndefined;
+ m_failedAuthMethods = 0;
+ m_currentAuthMethod = kCapabilityUndefined;
+ m_socketType = nsMsgSocketType::trySTARTTLS;
+ m_connectionStatus = NS_OK;
+ m_safeToCloseConnection = false;
+ m_hostSessionList = nullptr;
+ m_isGmailServer = false;
+ m_fetchingWholeMessage = false;
+ m_allowUTF8Accept = false;
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ NS_ASSERTION(prefBranch, "FAILED to create the preference service");
+
+ // read in the accept languages preference
+ if (prefBranch) {
+ if (!gInitialized) GlobalInitialization(prefBranch);
+
+ nsCOMPtr<nsIPrefLocalizedString> prefString;
+ prefBranch->GetComplexValue("intl.accept_languages",
+ NS_GET_IID(nsIPrefLocalizedString),
+ getter_AddRefs(prefString));
+ if (prefString) prefString->ToString(getter_Copies(mAcceptLanguages));
+
+ nsCString customDBHeaders;
+ prefBranch->GetCharPref("mailnews.customDBHeaders", customDBHeaders);
+
+ ParseString(customDBHeaders, ' ', mCustomDBHeaders);
+ prefBranch->GetBoolPref("mailnews.display.prefer_plaintext",
+ &m_preferPlainText);
+
+ nsAutoCString customHeaders;
+ prefBranch->GetCharPref("mailnews.customHeaders", customHeaders);
+ customHeaders.StripWhitespace();
+ ParseString(customHeaders, ':', mCustomHeaders);
+
+ nsresult rv;
+ bool bVal = false;
+ rv = prefBranch->GetBoolPref("mail.imap.tcp_keepalive.enabled", &bVal);
+ if (NS_SUCCEEDED(rv))
+ gTCPKeepalive.enabled.store(bVal, std::memory_order_relaxed);
+
+ if (bVal) {
+ int32_t val;
+ // TCP keepalive idle time, don't mistake with IMAP IDLE.
+ rv = prefBranch->GetIntPref("mail.imap.tcp_keepalive.idle_time", &val);
+ if (NS_SUCCEEDED(rv) && val >= 0)
+ gTCPKeepalive.idleTimeS.store(
+ std::min<int32_t>(std::max(val, 1), net::kMaxTCPKeepIdle),
+ std::memory_order_relaxed);
+
+ rv = prefBranch->GetIntPref("mail.imap.tcp_keepalive.retry_interval",
+ &val);
+ if (NS_SUCCEEDED(rv) && val >= 0)
+ gTCPKeepalive.retryIntervalS.store(
+ std::min<int32_t>(std::max(val, 1), net::kMaxTCPKeepIntvl),
+ std::memory_order_relaxed);
+ }
+ }
+
+ // ***** Thread support *****
+ m_thread = nullptr;
+ m_imapThreadIsRunning = false;
+ m_currentServerCommandTagNumber = 0;
+ m_active = false;
+ m_folderNeedsSubscribing = false;
+ m_folderNeedsACLRefreshed = false;
+ m_threadShouldDie = false;
+ m_inThreadShouldDie = false;
+ m_pseudoInterrupted = false;
+ m_nextUrlReadyToRun = false;
+ m_idleResponseReadyToHandle = false;
+ m_trackingTime = false;
+ m_curFetchSize = 0;
+ m_startTime = 0;
+ m_endTime = 0;
+ m_lastActiveTime = 0;
+ m_lastProgressTime = 0;
+ ResetProgressInfo();
+
+ m_tooFastTime = 0;
+ m_idealTime = 0;
+ m_chunkAddSize = 0;
+ m_chunkStartSize = 0;
+ m_fetchByChunks = true;
+ m_sendID = true;
+ m_chunkSize = 0;
+ m_chunkThreshold = 0;
+ m_fromHeaderSeen = false;
+ m_closeNeededBeforeSelect = false;
+ m_needNoop = false;
+ m_noopCount = 0;
+ m_fetchBodyListIsNew = false;
+ m_flagChangeCount = 0;
+ m_lastCheckTime = PR_Now();
+
+ m_hierarchyNameState = kNoOperationInProgress;
+ m_discoveryStatus = eContinue;
+
+ // m_dataOutputBuf is used by Send Data
+ m_dataOutputBuf = (char*)PR_CALLOC(sizeof(char) * OUTPUT_BUFFER_SIZE);
+
+ // used to buffer incoming data by ReadNextLine
+ m_inputStreamBuffer = new nsMsgLineStreamBuffer(
+ OUTPUT_BUFFER_SIZE, true /* allocate new lines */,
+ false /* leave CRLFs on the returned string */);
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown;
+ m_progressStringName.Truncate();
+ m_stringIndex = IMAP_EMPTY_STRING_INDEX;
+ m_progressExpectedNumber = 0;
+ memset(m_progressCurrentNumber, 0, sizeof m_progressCurrentNumber);
+
+ // since these are embedded in the nsImapProtocol object, but passed
+ // through proxied xpcom methods, just AddRef them here.
+ m_hdrDownloadCache = new nsMsgImapHdrXferInfo();
+ m_downloadLineCache = new nsMsgImapLineDownloadCache();
+
+ // subscription
+ m_autoSubscribe = true;
+ m_autoUnsubscribe = true;
+ m_autoSubscribeOnOpen = true;
+ m_deletableChildren = nullptr;
+
+ mFolderLastModSeq = 0;
+
+ Configure(gTooFastTime, gIdealTime, gChunkAddSize, gChunkSize,
+ gChunkThreshold, gFetchByChunks);
+ m_forceSelect = false;
+ m_capabilityResponseOccurred = true;
+
+ m_imapAction = 0;
+ m_bytesToChannel = 0;
+ m_passwordStatus = NS_OK;
+ m_passwordObtained = false;
+ mFolderTotalMsgCount = 0;
+ mFolderHighestUID = 0;
+ m_notifySearchHit = false;
+ m_preferPlainText = false;
+ m_uidValidity = kUidUnknown;
+}
+
+nsresult nsImapProtocol::Configure(int32_t TooFastTime, int32_t IdealTime,
+ int32_t ChunkAddSize, int32_t ChunkSize,
+ int32_t ChunkThreshold, bool FetchByChunks) {
+ m_tooFastTime = TooFastTime; // secs we read too little too fast
+ m_idealTime = IdealTime; // secs we read enough in good time
+ m_chunkAddSize = ChunkAddSize; // buffer size to add when wasting time
+ m_chunkStartSize = m_chunkSize = ChunkSize;
+ m_chunkThreshold = ChunkThreshold;
+ m_fetchByChunks = FetchByChunks;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapProtocol::Initialize(nsIImapHostSessionList* aHostSessionList,
+ nsIImapIncomingServer* aServer) {
+ NS_ASSERTION(
+ aHostSessionList && aServer,
+ "oops...trying to initialize with a null host session list or server!");
+ if (!aHostSessionList || !aServer) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv = m_downloadLineCache->GrowBuffer(kDownLoadCacheSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_flagState = new nsImapFlagAndUidState(kImapFlagAndUidStateSize);
+ if (!m_flagState) return NS_ERROR_OUT_OF_MEMORY;
+
+ aServer->GetUseIdle(&m_useIdle);
+ aServer->GetForceSelect(&m_forceSelect);
+ aServer->GetUseCondStore(&m_useCondStore);
+ aServer->GetUseCompressDeflate(&m_useCompressDeflate);
+ aServer->GetAllowUTF8Accept(&m_allowUTF8Accept);
+
+ m_hostSessionList = aHostSessionList;
+ m_parser.SetHostSessionList(aHostSessionList);
+ m_parser.SetFlagState(m_flagState);
+
+ // Initialize the empty mime part string on the main thread.
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = bundle->GetStringFromName("imapEmptyMimePart", m_emptyMimePartString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now initialize the thread for the connection
+ if (m_thread == nullptr) {
+ nsCOMPtr<nsIThread> imapThread;
+ nsresult rv = NS_NewNamedThread("IMAP", getter_AddRefs(imapThread));
+ if (NS_FAILED(rv)) {
+ NS_ASSERTION(imapThread, "Unable to create imap thread.");
+ return rv;
+ }
+ RefPtr<nsImapProtocolMainLoopRunnable> runnable =
+ new nsImapProtocolMainLoopRunnable(this);
+ imapThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ imapThread->GetPRThread(&m_thread);
+ }
+ return NS_OK;
+}
+
+nsImapProtocol::~nsImapProtocol() {
+ PR_Free(m_dataOutputBuf);
+
+ // **** We must be out of the thread main loop function
+ NS_ASSERTION(!m_imapThreadIsRunning, "Oops, thread is still running.");
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+}
+
+const nsCString& nsImapProtocol::GetImapHostName() {
+ if (m_runningUrl && m_hostName.IsEmpty()) {
+ nsCOMPtr<nsIURI> url = do_QueryInterface(m_runningUrl);
+ url->GetAsciiHost(m_hostName);
+ }
+
+ return m_hostName;
+}
+
+const nsCString& nsImapProtocol::GetImapUserName() {
+ if (m_userName.IsEmpty() && m_imapServerSink) {
+ m_imapServerSink->GetOriginalUsername(m_userName);
+ }
+ return m_userName;
+}
+
+const char* nsImapProtocol::GetImapServerKey() {
+ if (m_serverKey.IsEmpty() && m_imapServerSink) {
+ m_imapServerSink->GetServerKey(m_serverKey);
+ }
+ return m_serverKey.get();
+}
+
+nsresult nsImapProtocol::SetupSinkProxy() {
+ if (!m_runningUrl) return NS_OK;
+ nsresult res;
+ bool newFolderSink = false;
+ if (!m_imapMailFolderSink) {
+ nsCOMPtr<nsIImapMailFolderSink> aImapMailFolderSink;
+ (void)m_runningUrl->GetImapMailFolderSink(
+ getter_AddRefs(aImapMailFolderSink));
+ if (aImapMailFolderSink) {
+ m_imapMailFolderSink = new ImapMailFolderSinkProxy(aImapMailFolderSink);
+ newFolderSink = true;
+ }
+ }
+ if (newFolderSink) Log("SetupSinkProxy", nullptr, "got m_imapMailFolderSink");
+
+ if (!m_imapMessageSink) {
+ nsCOMPtr<nsIImapMessageSink> aImapMessageSink;
+ (void)m_runningUrl->GetImapMessageSink(getter_AddRefs(aImapMessageSink));
+ if (aImapMessageSink) {
+ m_imapMessageSink = new ImapMessageSinkProxy(aImapMessageSink);
+ } else {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ }
+ if (!m_imapServerSink) {
+ nsCOMPtr<nsIImapServerSink> aImapServerSink;
+ res = m_runningUrl->GetImapServerSink(getter_AddRefs(aImapServerSink));
+ if (aImapServerSink) {
+ m_imapServerSink = new ImapServerSinkProxy(aImapServerSink);
+ m_imapServerSinkLatest = m_imapServerSink;
+ } else {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ }
+ if (!m_imapProtocolSink) {
+ nsCOMPtr<nsIImapProtocolSink> anImapProxyHelper(
+ do_QueryInterface(NS_ISUPPORTS_CAST(nsIImapProtocolSink*, this), &res));
+ m_imapProtocolSink = new ImapProtocolSinkProxy(anImapProxyHelper);
+ }
+ return NS_OK;
+}
+
+static void SetSecurityCallbacksFromChannel(nsISocketTransport* aTrans,
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsIInterfaceRequestor> callbacks;
+ aChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ nsCOMPtr<nsIInterfaceRequestor> securityCallbacks;
+ NS_NewNotificationCallbacksAggregation(callbacks, loadGroup,
+ getter_AddRefs(securityCallbacks));
+ if (securityCallbacks) aTrans->SetSecurityCallbacks(securityCallbacks);
+}
+
+// Setup With Url is intended to set up data which is held on a PER URL basis
+// and not a per connection basis. If you have data which is independent of the
+// url we are currently running, then you should put it in Initialize(). This is
+// only ever called from the UI thread. It is called from LoadImapUrl, right
+// before the url gets run - i.e., the url is next in line to run.
+// See also ReleaseUrlState(), which frees a bunch of the things set up in here.
+nsresult nsImapProtocol::SetupWithUrl(nsIURI* aURL, nsISupports* aConsumer) {
+ nsresult rv = NS_ERROR_FAILURE;
+ NS_ASSERTION(aURL, "null URL passed into Imap Protocol");
+ if (aURL) {
+ nsCOMPtr<nsIImapUrl> imapURL = do_QueryInterface(aURL, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_runningUrl = imapURL;
+ m_runningUrlLatest = m_runningUrl;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(m_server);
+ if (!server) {
+ rv = mailnewsUrl->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_server = do_GetWeakReference(server);
+ }
+ nsCOMPtr<nsIMsgFolder> folder;
+ mailnewsUrl->GetFolder(getter_AddRefs(folder));
+ mFolderLastModSeq = 0;
+ mFolderTotalMsgCount = 0;
+ mFolderHighestUID = 0;
+ m_uidValidity = kUidUnknown;
+ if (folder) {
+ nsCOMPtr<nsIMsgDatabase> folderDB;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(folderDB));
+ if (folderInfo) {
+ nsCString modSeqStr;
+ folderInfo->GetCharProperty(kModSeqPropertyName, modSeqStr);
+ mFolderLastModSeq = ParseUint64Str(modSeqStr.get());
+ folderInfo->GetNumMessages(&mFolderTotalMsgCount);
+ folderInfo->GetUint32Property(kHighestRecordedUIDPropertyName, 0,
+ &mFolderHighestUID);
+ folderInfo->GetImapUidValidity(&m_uidValidity);
+ }
+ }
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ nsCOMPtr<nsIStreamListener> aRealStreamListener =
+ do_QueryInterface(aConsumer);
+ m_runningUrl->GetMockChannel(getter_AddRefs(m_mockChannel));
+ imapServer->GetIsGMailServer(&m_isGmailServer);
+ if (!m_mockChannel) {
+ nsCOMPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+
+ // there are several imap operations that aren't initiated via a
+ // nsIChannel::AsyncOpen call on the mock channel. such as selecting a
+ // folder. nsImapProtocol now insists on a mock channel when processing a
+ // url.
+ nsCOMPtr<nsIChannel> channel;
+ rv =
+ NS_NewChannel(getter_AddRefs(channel), aURL, nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ m_mockChannel = do_QueryInterface(channel);
+ NS_ASSERTION(m_mockChannel,
+ "failed to get a mock channel in nsImapProtocol");
+
+ // Certain imap operations (not initiated by the IO Service via AsyncOpen)
+ // can be interrupted by the stop button on the toolbar. We do this by
+ // using the loadgroup of the docshell for the message pane. We really
+ // shouldn't be doing this.. See the comment in
+ // nsMsgMailNewsUrl::GetLoadGroup.
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ mailnewsUrl->GetLoadGroup(
+ getter_AddRefs(loadGroup)); // get the message pane load group
+ if (loadGroup)
+ loadGroup->AddRequest(m_mockChannel, nullptr /* context isupports */);
+ }
+
+ if (m_mockChannel) {
+ m_mockChannel->SetImapProtocol(this);
+ // if we have a listener from a mock channel, over-ride the consumer that
+ // was passed in
+ nsCOMPtr<nsIStreamListener> channelListener;
+ m_mockChannel->GetChannelListener(getter_AddRefs(channelListener));
+ if (channelListener) // only over-ride if we have a non null channel
+ // listener
+ aRealStreamListener = channelListener;
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ GetMsgWindow(getter_AddRefs(msgWindow));
+ if (!msgWindow) GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ if (msgWindow) {
+ // Set up the MockChannel to attempt nsIProgressEventSink callbacks on
+ // the messageWindow, with fallback to the docShell (and the
+ // loadgroup).
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetMessageWindowDocShell(getter_AddRefs(docShell));
+ nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(docShell));
+ nsCOMPtr<nsIInterfaceRequestor> interfaceRequestor;
+ msgWindow->GetNotificationCallbacks(getter_AddRefs(interfaceRequestor));
+ nsCOMPtr<nsIInterfaceRequestor> aggregateIR;
+ NS_NewInterfaceRequestorAggregation(interfaceRequestor, ir,
+ getter_AddRefs(aggregateIR));
+ m_mockChannel->SetNotificationCallbacks(aggregateIR);
+ }
+ }
+
+ // since we'll be making calls directly from the imap thread to the channel
+ // listener, we need to turn it into a proxy object....we'll assume that the
+ // listener is on the same thread as the event sink queue
+ if (aRealStreamListener) {
+ NS_ASSERTION(!m_channelListener,
+ "shouldn't already have a channel listener");
+ m_channelListener = new StreamListenerProxy(aRealStreamListener);
+ }
+
+ server->GetHostName(m_hostName);
+ int32_t authMethod;
+ (void)server->GetAuthMethod(&authMethod);
+ InitPrefAuthMethods(authMethod, server);
+ (void)server->GetSocketType(&m_socketType);
+ bool shuttingDown;
+ (void)imapServer->GetShuttingDown(&shuttingDown);
+ if (!shuttingDown)
+ (void)imapServer->GetUseIdle(&m_useIdle);
+ else
+ m_useIdle = false;
+ imapServer->GetFetchByChunks(&m_fetchByChunks);
+ imapServer->GetSendID(&m_sendID);
+
+ nsAutoString trashFolderPath;
+ if (NS_SUCCEEDED(imapServer->GetTrashFolderName(trashFolderPath))) {
+ if (m_allowUTF8Accept)
+ CopyUTF16toUTF8(trashFolderPath, m_trashFolderPath);
+ else
+ CopyUTF16toMUTF7(trashFolderPath, m_trashFolderPath);
+ }
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch) {
+ bool preferPlainText;
+ prefBranch->GetBoolPref("mailnews.display.prefer_plaintext",
+ &preferPlainText);
+ // If the pref has changed since the last time we ran a url,
+ // clear the shell cache for this host. (bodyshell no longer exists.)
+ if (preferPlainText != m_preferPlainText) {
+ m_preferPlainText = preferPlainText;
+ }
+ }
+ // If enabled, retrieve the clientid so that we can use it later.
+ bool clientidEnabled = false;
+ if (NS_SUCCEEDED(server->GetClientidEnabled(&clientidEnabled)) &&
+ clientidEnabled)
+ server->GetClientid(m_clientId);
+ else {
+ m_clientId.Truncate();
+ }
+
+ bool proxyCallback = false;
+ if (m_runningUrl && !m_transport /* and we don't have a transport yet */) {
+ if (m_mockChannel) {
+ rv = MsgExamineForProxyAsync(m_mockChannel, this,
+ getter_AddRefs(m_proxyRequest));
+ if (NS_FAILED(rv)) {
+ rv = SetupWithUrlCallback(nullptr);
+ } else {
+ proxyCallback = true;
+ }
+ }
+ }
+
+ if (!proxyCallback) rv = LoadImapUrlInternal();
+ }
+
+ return rv;
+}
+
+// nsIProtocolProxyCallback
+NS_IMETHODIMP
+nsImapProtocol::OnProxyAvailable(nsICancelable* aRequest, nsIChannel* aChannel,
+ nsIProxyInfo* aProxyInfo, nsresult aStatus) {
+ // If we're called with NS_BINDING_ABORTED, the IMAP thread already died,
+ // so we can't carry on. Otherwise, no checking of 'aStatus' here, see
+ // nsHttpChannel::OnProxyAvailable(). Status is non-fatal and we just kick on.
+ if (aStatus == NS_BINDING_ABORTED) return NS_ERROR_FAILURE;
+
+ nsresult rv = SetupWithUrlCallback(aProxyInfo);
+ if (NS_FAILED(rv)) {
+ // Cancel the protocol and be done.
+ if (m_mockChannel) m_mockChannel->Cancel(rv);
+ return rv;
+ }
+
+ rv = LoadImapUrlInternal();
+ if (NS_FAILED(rv)) {
+ if (m_mockChannel) m_mockChannel->Cancel(rv);
+ }
+
+ return rv;
+}
+
+nsresult nsImapProtocol::SetupWithUrlCallback(nsIProxyInfo* aProxyInfo) {
+ m_proxyRequest = nullptr;
+
+ nsresult rv;
+
+ nsCOMPtr<nsISocketTransportService> socketService =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ Log("SetupWithUrlCallback", nullptr, "clearing IMAP_CONNECTION_IS_OPEN");
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ const char* connectionType = nullptr;
+
+ if (m_socketType == nsMsgSocketType::SSL)
+ connectionType = "ssl";
+ else if (m_socketType == nsMsgSocketType::alwaysSTARTTLS)
+ connectionType = "starttls";
+ // This can go away once we think everyone is migrated
+ // away from the trySTARTTLS socket type.
+ else if (m_socketType == nsMsgSocketType::trySTARTTLS)
+ connectionType = "starttls";
+
+ int32_t port = -1;
+ nsCOMPtr<nsIURI> uri = do_QueryInterface(m_runningUrl, &rv);
+ if (NS_FAILED(rv)) return rv;
+ uri->GetPort(&port);
+
+ AutoTArray<nsCString, 1> connectionTypeArray;
+ if (connectionType) connectionTypeArray.AppendElement(connectionType);
+ // NOTE: Some errors won't show up until the first read attempt (SSL bad
+ // certificate errors, for example).
+ rv = socketService->CreateTransport(connectionTypeArray, m_hostName, port,
+ aProxyInfo, nullptr,
+ getter_AddRefs(m_transport));
+ if (NS_FAILED(rv) && m_socketType == nsMsgSocketType::trySTARTTLS) {
+ connectionType = nullptr;
+ m_socketType = nsMsgSocketType::plain;
+ rv = socketService->CreateTransport(connectionTypeArray, m_hostName, port,
+ aProxyInfo, nullptr,
+ getter_AddRefs(m_transport));
+ }
+
+ // remember so we can know whether we can issue a start tls or not...
+ m_connectionType = connectionType;
+ if (m_transport && m_mockChannel) {
+ uint8_t qos;
+ rv = GetQoSBits(&qos);
+ if (NS_SUCCEEDED(rv)) m_transport->SetQoSBits(qos);
+
+ // Ensure that the socket can get the notification callbacks
+ SetSecurityCallbacksFromChannel(m_transport, m_mockChannel);
+
+ // open buffered, blocking input stream
+ rv = m_transport->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0,
+ getter_AddRefs(m_inputStream));
+ if (NS_FAILED(rv)) return rv;
+
+ // open buffered, blocking output stream
+ rv = m_transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0,
+ getter_AddRefs(m_outputStream));
+ if (NS_FAILED(rv)) return rv;
+ SetFlag(IMAP_CONNECTION_IS_OPEN);
+ }
+
+ return rv;
+}
+
+// when the connection is done processing the current state, free any per url
+// state data...
+void nsImapProtocol::ReleaseUrlState(bool rerunning) {
+ // clear out the socket's reference to the notification callbacks for this
+ // transaction
+ {
+ MutexAutoLock mon(mLock);
+ if (m_transport) {
+ m_transport->SetSecurityCallbacks(nullptr);
+ m_transport->SetEventSink(nullptr, nullptr);
+ }
+ }
+
+ if (m_mockChannel && !rerunning) {
+ // Proxy the close of the channel to the ui thread.
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->CloseMockChannel(m_mockChannel);
+ else
+ m_mockChannel->Close();
+
+ {
+ // grab a lock so m_mockChannel doesn't get cleared out
+ // from under us.
+ MutexAutoLock mon(mLock);
+ if (m_mockChannel) {
+ // Proxy the release of the channel to the main thread. This is
+ // something that the xpcom proxy system should do for us!
+ NS_ReleaseOnMainThread("nsImapProtocol::m_mockChannel",
+ m_mockChannel.forget());
+ }
+ }
+ }
+
+ m_imapMessageSink = nullptr;
+
+ // Proxy the release of the listener to the main thread. This is something
+ // that the xpcom proxy system should do for us!
+ {
+ // grab a lock so the m_channelListener doesn't get cleared.
+ MutexAutoLock mon(mLock);
+ if (m_channelListener) {
+ NS_ReleaseOnMainThread("nsImapProtocol::m_channelListener",
+ m_channelListener.forget());
+ }
+ }
+ m_channelInputStream = nullptr;
+ m_channelOutputStream = nullptr;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl;
+ nsCOMPtr<nsIImapMailFolderSink> saveFolderSink;
+
+ {
+ MutexAutoLock mon(mLock);
+ if (m_runningUrl) {
+ mailnewsurl = do_QueryInterface(m_runningUrl);
+ // It is unclear what 'saveFolderSink' is used for, most likely to hold
+ // a reference for a little longer. See bug 1324893 and bug 391259.
+ saveFolderSink = m_imapMailFolderSink;
+
+ m_runningUrl =
+ nullptr; // force us to release our last reference on the url
+ m_urlInProgress = false;
+ }
+ }
+ // Need to null this out whether we have an m_runningUrl or not
+ m_imapMailFolderSink = nullptr;
+
+ // we want to make sure the imap protocol's last reference to the url gets
+ // released back on the UI thread. This ensures that the objects the imap url
+ // hangs on to properly get released back on the UI thread.
+ if (mailnewsurl) {
+ NS_ReleaseOnMainThread("nsImapProtocol::m_runningUrl",
+ mailnewsurl.forget());
+ }
+ saveFolderSink = nullptr;
+}
+
+class nsImapCancelProxy : public mozilla::Runnable {
+ public:
+ explicit nsImapCancelProxy(nsICancelable* aProxyRequest)
+ : mozilla::Runnable("nsImapCancelProxy"), mRequest(aProxyRequest) {}
+ NS_IMETHOD Run() {
+ if (mRequest) mRequest->Cancel(NS_BINDING_ABORTED);
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsICancelable> mRequest;
+};
+
+bool nsImapProtocol::RunImapThreadMainLoop() {
+ PR_CEnterMonitor(this);
+ NS_ASSERTION(!m_imapThreadIsRunning,
+ "Oh. oh. thread is already running. What's wrong here?");
+ if (m_imapThreadIsRunning) {
+ PR_CExitMonitor(this);
+ return false;
+ }
+
+ m_imapThreadIsRunning = true;
+ PR_CExitMonitor(this);
+
+ // call the platform specific main loop ....
+ ImapThreadMainLoop();
+
+ if (m_proxyRequest) {
+ // Cancel proxy on main thread.
+ RefPtr<nsImapCancelProxy> cancelProxy =
+ new nsImapCancelProxy(m_proxyRequest);
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "nsImapProtocol::RunImapThreadMainLoop"_ns,
+ GetMainThreadSerialEventTarget(), cancelProxy.forget());
+ m_proxyRequest = nullptr;
+ }
+
+ if (m_runningUrl) {
+ NS_ReleaseOnMainThread("nsImapProtocol::m_runningUrl",
+ m_runningUrl.forget());
+ }
+
+ // close streams via UI thread if it's not already done
+ if (m_imapProtocolSink) m_imapProtocolSink->CloseStreams();
+
+ m_imapMailFolderSink = nullptr;
+ m_imapMailFolderSinkSelected = nullptr;
+ m_imapMessageSink = nullptr;
+
+ return true;
+}
+
+//
+// Must be called from UI thread only
+//
+NS_IMETHODIMP nsImapProtocol::CloseStreams() {
+ // make sure that it is called by the UI thread
+ MOZ_ASSERT(NS_IsMainThread(),
+ "CloseStreams() should not be called from an off UI thread");
+
+ {
+ MutexAutoLock mon(mLock);
+ if (m_transport) {
+ // make sure the transport closes (even if someone is still indirectly
+ // referencing it).
+ m_transport->Close(NS_ERROR_ABORT);
+ m_transport = nullptr;
+ }
+ m_inputStream = nullptr;
+ m_outputStream = nullptr;
+ m_channelListener = nullptr;
+ if (m_mockChannel) {
+ m_mockChannel->Close();
+ m_mockChannel = nullptr;
+ }
+ m_channelInputStream = nullptr;
+ m_channelOutputStream = nullptr;
+
+ // Close scope because we must let go of the monitor before calling
+ // RemoveConnection to unblock anyone who tries to get a monitor to the
+ // protocol object while holding onto a monitor to the server.
+ }
+ nsCOMPtr<nsIMsgIncomingServer> me_server = do_QueryReferent(m_server);
+ if (me_server) {
+ nsresult result;
+ nsCOMPtr<nsIImapIncomingServer> aImapServer(
+ do_QueryInterface(me_server, &result));
+ if (NS_SUCCEEDED(result)) aImapServer->RemoveConnection(this);
+ me_server = nullptr;
+ }
+ m_server = nullptr;
+ // take this opportunity of being on the UI thread to
+ // persist chunk prefs if they've changed
+ if (gChunkSizeDirty) {
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch) {
+ prefBranch->SetIntPref("mail.imap.chunk_size", gChunkSize);
+ prefBranch->SetIntPref("mail.imap.min_chunk_size_threshold",
+ gChunkThreshold);
+ gChunkSizeDirty = false;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetUrlWindow(nsIMsgMailNewsUrl* aUrl,
+ nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+ return aUrl->GetMsgWindow(aMsgWindow);
+}
+
+NS_IMETHODIMP nsImapProtocol::SetupMainThreadProxies() {
+ return SetupSinkProxy();
+}
+
+NS_IMETHODIMP nsImapProtocol::OnInputStreamReady(nsIAsyncInputStream* inStr) {
+ // should we check if it's a close vs. data available?
+ if (m_idle) {
+ uint64_t bytesAvailable = 0;
+ (void)inStr->Available(&bytesAvailable);
+ // check if data available - might be a close
+ if (bytesAvailable != 0) {
+ ReentrantMonitorAutoEnter mon(m_urlReadyToRunMonitor);
+ m_lastActiveTime = PR_Now();
+ m_idleResponseReadyToHandle = true;
+ mon.Notify();
+ }
+ }
+ return NS_OK;
+}
+
+// this is to be called from the UI thread. It sets m_threadShouldDie,
+// and then signals the imap thread, which, when it wakes up, should exit.
+// The imap thread cleanup code will check m_safeToCloseConnection.
+NS_IMETHODIMP
+nsImapProtocol::TellThreadToDie(bool aIsSafeToClose) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ NS_IsMainThread(),
+ "TellThreadToDie(aIsSafeToClose) should only be called from UI thread");
+ MutexAutoLock mon(mLock);
+
+ nsCOMPtr<nsIMsgIncomingServer> me_server = do_QueryReferent(m_server);
+ if (me_server) {
+ nsresult rv;
+ nsCOMPtr<nsIImapIncomingServer> aImapServer(
+ do_QueryInterface(me_server, &rv));
+ if (NS_SUCCEEDED(rv)) aImapServer->RemoveConnection(this);
+ m_server = nullptr;
+ me_server = nullptr;
+ }
+ {
+ ReentrantMonitorAutoEnter deathMon(m_threadDeathMonitor);
+ m_safeToCloseConnection = aIsSafeToClose;
+ m_threadShouldDie = true;
+ }
+ ReentrantMonitorAutoEnter readyMon(m_urlReadyToRunMonitor);
+ m_nextUrlReadyToRun = true;
+ readyMon.Notify();
+ return NS_OK;
+}
+
+/**
+ * Dispatch socket thread to to determine if connection is alive.
+ */
+nsresult nsImapProtocol::IsTransportAlive(bool* alive) {
+ nsresult rv;
+ auto GetIsAlive = [transport = nsCOMPtr{m_transport}, &rv, alive]() mutable {
+ rv = transport->IsAlive(alive);
+ };
+ nsCOMPtr<nsIEventTarget> socketThread(
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID));
+ if (socketThread) {
+ mozilla::SyncRunnable::DispatchToThread(
+ socketThread,
+ NS_NewRunnableFunction("nsImapProtocol::IsTransportAlive", GetIsAlive));
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+ return rv;
+}
+
+/**
+ * Dispatch socket thread to initiate STARTTLS handshakes.
+ */
+nsresult nsImapProtocol::TransportStartTLS() {
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ nsCOMPtr<nsITLSSocketControl> tlsSocketControl;
+ if (m_transport &&
+ NS_SUCCEEDED(
+ m_transport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl))) &&
+ tlsSocketControl) {
+ auto CallStartTLS = [sockCon = nsCOMPtr{tlsSocketControl}, &rv]() mutable {
+ rv = sockCon->StartTLS();
+ };
+ nsCOMPtr<nsIEventTarget> socketThread(
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID));
+ if (socketThread) {
+ mozilla::SyncRunnable::DispatchToThread(
+ socketThread, NS_NewRunnableFunction(
+ "nsImapProtocol::TransportStartTLS", CallStartTLS));
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ return rv;
+}
+
+/**
+ * Dispatch socket thread to obtain transport security information.
+ */
+void nsImapProtocol::GetTransportSecurityInfo(
+ nsITransportSecurityInfo** aSecurityInfo) {
+ *aSecurityInfo = nullptr;
+ nsCOMPtr<nsIEventTarget> socketThread(
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID));
+ nsCOMPtr<nsITLSSocketControl> tlsSocketControl;
+ if (socketThread &&
+ NS_SUCCEEDED(
+ m_transport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl))) &&
+ tlsSocketControl) {
+ if (socketThread) {
+ nsCOMPtr<nsITransportSecurityInfo> secInfo;
+ auto GetSecurityInfo = [&tlsSocketControl, &secInfo]() mutable {
+ tlsSocketControl->GetSecurityInfo(getter_AddRefs(secInfo));
+ };
+ mozilla::SyncRunnable::DispatchToThread(
+ socketThread,
+ NS_NewRunnableFunction("nsImapProtocol::GetTransportSecurityInfo",
+ GetSecurityInfo));
+ NS_IF_ADDREF(*aSecurityInfo = secInfo);
+ }
+ }
+}
+
+void nsImapProtocol::TellThreadToDie() {
+ nsresult rv = NS_OK;
+ MOZ_DIAGNOSTIC_ASSERT(
+ !NS_IsMainThread(),
+ "TellThreadToDie() should not be called from UI thread");
+
+ // prevent re-entering this method because it may lock the UI.
+ if (m_inThreadShouldDie) return;
+ m_inThreadShouldDie = true;
+
+ // This routine is called only from the imap protocol thread.
+ // The UI thread causes this to be called by calling TellThreadToDie.
+ // In that case, m_safeToCloseConnection will be FALSE if it's dropping a
+ // timed out connection, true when closing a cached connection.
+ // We're using PR_CEnter/ExitMonitor because Monitors don't like having
+ // us to hold one monitor and call code that gets a different monitor. And
+ // some of the methods we call here use Monitors.
+ PR_CEnterMonitor(this);
+
+ m_urlInProgress = true; // let's say it's busy so no one tries to use
+ // this about to die connection.
+ bool urlWritingData = false;
+ bool connectionIdle = !m_runningUrl;
+
+ if (!connectionIdle)
+ urlWritingData = m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile ||
+ m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile;
+
+ bool closeNeeded = GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected &&
+ m_safeToCloseConnection;
+ nsCString command;
+ // if a url is writing data, we can't even logout, so we're just
+ // going to close the connection as if the user pressed stop.
+ if (m_currentServerCommandTagNumber > 0 && !urlWritingData) {
+ bool isAlive = false;
+ if (m_transport) rv = IsTransportAlive(&isAlive);
+
+ if (TestFlag(IMAP_CONNECTION_IS_OPEN) && m_idle && isAlive) EndIdle(false);
+
+ if (NS_SUCCEEDED(rv) && isAlive && closeNeeded &&
+ GetDeleteIsMoveToTrash() && TestFlag(IMAP_CONNECTION_IS_OPEN) &&
+ m_outputStream)
+ ImapClose(true, connectionIdle);
+
+ if (NS_SUCCEEDED(rv) && isAlive && TestFlag(IMAP_CONNECTION_IS_OPEN) &&
+ NS_SUCCEEDED(GetConnectionStatus()) && m_outputStream)
+ Logout(true, connectionIdle);
+ }
+ PR_CExitMonitor(this);
+ // close streams via UI thread
+ if (m_imapProtocolSink) {
+ m_imapProtocolSink->CloseStreams();
+ m_imapProtocolSink = nullptr;
+ }
+ Log("TellThreadToDie", nullptr, "close socket connection");
+
+ {
+ ReentrantMonitorAutoEnter mon(m_threadDeathMonitor);
+ m_threadShouldDie = true;
+ }
+ {
+ ReentrantMonitorAutoEnter dataMon(m_dataAvailableMonitor);
+ dataMon.Notify();
+ }
+ ReentrantMonitorAutoEnter urlReadyMon(m_urlReadyToRunMonitor);
+ urlReadyMon.NotifyAll();
+}
+
+NS_IMETHODIMP
+nsImapProtocol::GetLastActiveTimeStamp(PRTime* aTimeStamp) {
+ if (aTimeStamp) *aTimeStamp = m_lastActiveTime;
+ return NS_OK;
+}
+
+static void DoomCacheEntry(nsIMsgMailNewsUrl* url);
+NS_IMETHODIMP
+nsImapProtocol::PseudoInterruptMsgLoad(nsIMsgFolder* aImapFolder,
+ nsIMsgWindow* aMsgWindow,
+ bool* interrupted) {
+ NS_ENSURE_ARG(interrupted);
+
+ *interrupted = false;
+
+ PR_CEnterMonitor(this);
+
+ if (m_runningUrl && !TestFlag(IMAP_CLEAN_UP_URL_STATE)) {
+ nsImapAction imapAction;
+ m_runningUrl->GetImapAction(&imapAction);
+
+ if (imapAction == nsIImapUrl::nsImapMsgFetch) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapUrl> runningImapURL;
+
+ rv = GetRunningImapURL(getter_AddRefs(runningImapURL));
+ if (NS_SUCCEEDED(rv) && runningImapURL) {
+ nsCOMPtr<nsIMsgFolder> runningImapFolder;
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(runningImapURL);
+ mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ mailnewsUrl->GetFolder(getter_AddRefs(runningImapFolder));
+ if (aImapFolder == runningImapFolder && msgWindow == aMsgWindow) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Set PseudoInterrupt", __func__));
+ PseudoInterrupt(true);
+ *interrupted = true;
+ }
+ // If we're pseudo-interrupted, doom any incomplete cache entry.
+ // But if mock channel indicates fetch is complete so cache write is
+ // done, then don't doom the cache entry.
+ bool cacheWriteInProgress = true;
+ if (m_mockChannel)
+ m_mockChannel->GetWritingToCache(&cacheWriteInProgress);
+ if (cacheWriteInProgress) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Call DoomCacheEntry()", __func__));
+ DoomCacheEntry(mailnewsUrl);
+ }
+ }
+ }
+ }
+ PR_CExitMonitor(this);
+#ifdef DEBUG_bienvenu
+ printf("interrupt msg load : %s\n", (*interrupted) ? "TRUE" : "FALSE");
+#endif
+ return NS_OK;
+}
+
+void nsImapProtocol::ImapThreadMainLoop() {
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("ImapThreadMainLoop entering [this=%p]", this));
+
+ PRIntervalTime sleepTime = kImapSleepTime;
+ bool idlePending = false;
+ while (!DeathSignalReceived()) {
+ nsresult rv = NS_OK;
+ bool urlReadyToRun;
+
+ // wait for a URL or idle response to process...
+ {
+ ReentrantMonitorAutoEnter mon(m_urlReadyToRunMonitor);
+
+ while (NS_SUCCEEDED(rv) && !DeathSignalReceived() &&
+ !m_nextUrlReadyToRun && !m_idleResponseReadyToHandle &&
+ !m_threadShouldDie) {
+ rv = mon.Wait(sleepTime);
+ if (idlePending) break;
+ }
+
+ urlReadyToRun = m_nextUrlReadyToRun;
+ m_nextUrlReadyToRun = false;
+ }
+ // This will happen if the UI thread signals us to die
+ if (m_threadShouldDie) {
+ TellThreadToDie();
+ break;
+ }
+
+ if (NS_FAILED(rv) && PR_PENDING_INTERRUPT_ERROR == PR_GetError()) {
+ printf("error waiting for monitor\n");
+ break;
+ }
+
+ // If an idle response has occurred, handle only it on this pass through
+ // the loop.
+ if (m_idleResponseReadyToHandle && !m_threadShouldDie) {
+ m_idleResponseReadyToHandle = false;
+ HandleIdleResponses();
+ if (urlReadyToRun) {
+ // A URL is also ready. Process it on next loop.
+ m_nextUrlReadyToRun = true;
+ urlReadyToRun = false;
+ }
+ }
+
+ if (urlReadyToRun && m_runningUrl) {
+ if (m_currentServerCommandTagNumber && m_transport) {
+ bool isAlive;
+ rv = IsTransportAlive(&isAlive);
+ // if the transport is not alive, and we've ever sent a command with
+ // this connection, kill it. otherwise, we've probably just not finished
+ // setting it so don't kill it!
+ if (NS_FAILED(rv) || !isAlive) {
+ // This says we never started running the url, which is the case.
+ m_runningUrl->SetRerunningUrl(false);
+ RetryUrl();
+ return;
+ }
+ }
+ //
+ // NOTE: Although we cleared m_nextUrlReadyToRun above, it may now be set
+ // again by LoadImapUrlInternal(), which runs on the main thread.
+ // Because of this, we must not clear m_nextUrlReadyToRun here.
+ //
+ if (ProcessCurrentURL()) {
+ // Another URL has been setup to run. Process it on next loop.
+ m_nextUrlReadyToRun = true;
+ m_imapMailFolderSink = nullptr;
+ } else {
+ // No more URLs setup to run. Set idle pending if user has configured
+ // idle and if a URL is not in progress and if the server has IDLE
+ // capability. Just set idlePending since want to wait a short time
+ // to see if more URLs occurs before actually entering idle.
+ if (!idlePending && m_useIdle && !m_urlInProgress &&
+ GetServerStateParser().GetCapabilityFlag() & kHasIdleCapability &&
+ GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected) {
+ // Set-up short wait time in milliseconds
+ static const PRIntervalTime kIdleWait =
+ PR_MillisecondsToInterval(2000);
+ sleepTime = kIdleWait;
+ idlePending = true;
+ Log("ImapThreadMainLoop", nullptr, "idlePending set");
+ } else {
+ // if not idle used, don't need to remember folder sink
+ m_imapMailFolderSink = nullptr;
+ }
+ }
+ } else {
+ // No URL to run detected on wake up.
+ if (idlePending) {
+ // Have seen no URLs for the short time (kIdleWait) so go into idle mode
+ // and set the loop sleep time back to its original longer time.
+ Idle();
+ if (!m_idle) {
+ // Server rejected IDLE. Treat like IDLE not enabled or available.
+ m_imapMailFolderSink = nullptr;
+ }
+ idlePending = false;
+ sleepTime = kImapSleepTime;
+ }
+ }
+ if (!GetServerStateParser().Connected()) break;
+#ifdef DEBUG_bienvenu
+ else
+ printf("ready to run but no url and not idle\n");
+#endif
+ // This can happen if the UI thread closes cached connections in the
+ // OnStopRunningUrl notification.
+ if (m_threadShouldDie) TellThreadToDie();
+ }
+ m_imapThreadIsRunning = false;
+
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("ImapThreadMainLoop leaving [this=%p]", this));
+}
+
+// This handles the response to Idle() command and handles responses sent by
+// the server after in idle mode. Returns true if a BAD or NO response is
+// not seen which is needed when called from Idle().
+bool nsImapProtocol::HandleIdleResponses() {
+ bool rvIdleOk = true;
+ bool untagged = false;
+ NS_ASSERTION(PL_strstr(m_currentCommand.get(), " IDLE"), "not IDLE!");
+ do {
+ ParseIMAPandCheckForNewMail();
+ rvIdleOk = rvIdleOk && !GetServerStateParser().CommandFailed();
+ untagged = untagged || GetServerStateParser().UntaggedResponse();
+ } while (m_inputStreamBuffer->NextLineAvailable() &&
+ GetServerStateParser().Connected());
+
+ // If still connected and rvIdleOk is true and an untagged response was
+ // detected and have the sink pointer, OnNewIdleMessage will invoke a URL to
+ // update the folder. Otherwise just setup so we get notified of idle response
+ // data on the socket transport thread by OnInputStreamReady() above, which
+ // will trigger the imap thread main loop to run and call this function again.
+ if (GetServerStateParser().Connected() && rvIdleOk) {
+ if (m_imapMailFolderSinkSelected && untagged) {
+ Log("HandleIdleResponses", nullptr, "idle response");
+ m_imapMailFolderSinkSelected->OnNewIdleMessages();
+ } else {
+ // Enable async wait mode. Occurs when Idle() called.
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
+ do_QueryInterface(m_inputStream);
+ if (asyncInputStream) {
+ asyncInputStream->AsyncWait(this, 0, 0, nullptr);
+ Log("HandleIdleResponses", nullptr, "idle mode async waiting");
+ }
+ }
+ }
+ return rvIdleOk;
+}
+
+void nsImapProtocol::EstablishServerConnection() {
+#define ESC_LENGTH(x) (sizeof(x) - 1)
+#define ESC_OK "* OK"
+#define ESC_OK_LEN ESC_LENGTH(ESC_OK)
+#define ESC_PREAUTH "* PREAUTH"
+#define ESC_PREAUTH_LEN ESC_LENGTH(ESC_PREAUTH)
+#define ESC_CAPABILITY_STAR "* "
+#define ESC_CAPABILITY_STAR_LEN ESC_LENGTH(ESC_CAPABILITY_STAR)
+#define ESC_CAPABILITY_OK "* OK ["
+#define ESC_CAPABILITY_OK_LEN ESC_LENGTH(ESC_CAPABILITY_OK)
+#define ESC_CAPABILITY_GREETING (ESC_CAPABILITY_OK "CAPABILITY")
+#define ESC_CAPABILITY_GREETING_LEN ESC_LENGTH(ESC_CAPABILITY_GREETING)
+
+ char* serverResponse = CreateNewLineFromSocket(); // read in the greeting
+ // record the fact that we've received a greeting for this connection so we
+ // don't ever try to do it again..
+ if (serverResponse) SetFlag(IMAP_RECEIVED_GREETING);
+
+ if (!PL_strncasecmp(serverResponse, ESC_OK, ESC_OK_LEN)) {
+ SetConnectionStatus(NS_OK);
+
+ if (!PL_strncasecmp(serverResponse, ESC_CAPABILITY_GREETING,
+ ESC_CAPABILITY_GREETING_LEN)) {
+ nsAutoCString tmpstr(serverResponse);
+ int32_t endIndex = tmpstr.FindChar(']', ESC_CAPABILITY_GREETING_LEN);
+ if (endIndex >= 0) {
+ // Allocate the new buffer here. This buffer will be passed to
+ // ParseIMAPServerResponse() where it will be used to fill the
+ // fCurrentLine field and will be freed by the next call to
+ // ResetLexAnalyzer().
+ char* fakeServerResponse = (char*)PR_Malloc(PL_strlen(serverResponse));
+ // Munge the greeting into something that would pass for an IMAP
+ // server's response to a "CAPABILITY" command.
+ strcpy(fakeServerResponse, ESC_CAPABILITY_STAR);
+ strcat(fakeServerResponse, serverResponse + ESC_CAPABILITY_OK_LEN);
+ fakeServerResponse[endIndex - ESC_CAPABILITY_OK_LEN +
+ ESC_CAPABILITY_STAR_LEN] = '\0';
+ // Tell the response parser that we just issued a "CAPABILITY" and
+ // got the following back.
+ GetServerStateParser().ParseIMAPServerResponse("1 CAPABILITY", true,
+ fakeServerResponse);
+ }
+ }
+ } else if (!PL_strncasecmp(serverResponse, ESC_PREAUTH, ESC_PREAUTH_LEN)) {
+ // PREAUTH greeting received. We've been pre-authenticated by the server.
+ // We can skip sending a password and transition right into the
+ // kAuthenticated state; but we won't if the user has configured STARTTLS.
+ // (STARTTLS can only occur with the server in non-authenticated state.)
+ if (!(m_socketType == nsMsgSocketType::alwaysSTARTTLS ||
+ m_socketType == nsMsgSocketType::trySTARTTLS)) {
+ GetServerStateParser().PreauthSetAuthenticatedState();
+
+ if (GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined)
+ Capability();
+
+ if (!(GetServerStateParser().GetCapabilityFlag() &
+ (kIMAP4Capability | kIMAP4rev1Capability | kIMAP4other))) {
+ // AlertUserEventUsingId(MK_MSG_IMAP_SERVER_NOT_IMAP4);
+ SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib
+ } else {
+ // let's record the user as authenticated.
+ m_imapServerSink->SetUserAuthenticated(true);
+ m_hostSessionList->SetPasswordVerifiedOnline(GetImapServerKey());
+
+ ProcessAfterAuthenticated();
+ // the connection was a success
+ SetConnectionStatus(NS_OK);
+ }
+ } else {
+ // STARTTLS is configured so don't transition to authenticated state. Just
+ // alert the user, log the error and drop the connection. This may
+ // indicate a man-in-the middle attack if the user is not expecting
+ // PREAUTH. The user must change the connection security setting to other
+ // than STARTTLS to allow PREAUTH to be accepted on subsequent IMAP
+ // connections.
+ AlertUserEventUsingName("imapServerDisconnected");
+ const nsCString& hostName = GetImapHostName();
+ MOZ_LOG(
+ IMAP, LogLevel::Error,
+ ("PREAUTH received from IMAP server %s because STARTTLS selected. "
+ "Connection dropped",
+ hostName.get()));
+ SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib
+ }
+ }
+
+ PR_Free(serverResponse); // we don't care about the greeting yet...
+
+#undef ESC_LENGTH
+#undef ESC_OK
+#undef ESC_OK_LEN
+#undef ESC_PREAUTH
+#undef ESC_PREAUTH_LEN
+#undef ESC_CAPABILITY_STAR
+#undef ESC_CAPABILITY_STAR_LEN
+#undef ESC_CAPABILITY_OK
+#undef ESC_CAPABILITY_OK_LEN
+#undef ESC_CAPABILITY_GREETING
+#undef ESC_CAPABILITY_GREETING_LEN
+}
+
+// This can get called from the UI thread or an imap thread.
+// It makes sure we don't get left with partial messages in
+// the memory cache.
+static void DoomCacheEntry(nsIMsgMailNewsUrl* url) {
+ bool readingFromMemCache = false;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url);
+ imapUrl->GetMsgLoadingFromCache(&readingFromMemCache);
+ if (!readingFromMemCache) {
+ nsCOMPtr<nsICacheEntry> cacheEntry;
+ url->GetMemCacheEntry(getter_AddRefs(cacheEntry));
+ if (cacheEntry) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Call AsyncDoom(), url=%s", __func__,
+ url->GetSpecOrDefault().get()));
+ cacheEntry->AsyncDoom(nullptr);
+ }
+ }
+}
+
+/**
+ * ProcessCurrentURL() runs the current URL (m_runningUrl).
+ * Things to remember:
+ * - IMAP protocol URLs don't correspond directly to IMAP commands. A single
+ * URL might cause multiple IMAP commands to be issued.
+ * - This is all synchronous. But that's OK, because we're running in our
+ * own thread.
+ *
+ * @return true if another url was run, false otherwise.
+ */
+bool nsImapProtocol::ProcessCurrentURL() {
+ nsresult rv = NS_OK;
+ if (m_idle) EndIdle();
+
+ if (m_retryUrlOnError) {
+ // we clear this flag if we're re-running immediately, because that
+ // means we never sent a start running url notification, and later we
+ // don't send start running notification if we think we're rerunning
+ // the url (see first call to SetUrlState below). This means we won't
+ // send a start running notification, which means our stop running
+ // notification will be ignored because we don't think we were running.
+ m_runningUrl->SetRerunningUrl(false);
+ return RetryUrl();
+ }
+ Log("ProcessCurrentURL", nullptr, "entering");
+ (void)GetImapHostName(); // force m_hostName to get set.
+
+ bool logonFailed = false;
+ bool anotherUrlRun = false;
+ bool rerunningUrl = false;
+ bool isExternalUrl;
+ bool validUrl = true;
+
+ PseudoInterrupt(false); // clear this if left over from previous url.
+
+ m_runningUrl->GetRerunningUrl(&rerunningUrl);
+ m_runningUrl->GetExternalLinkUrl(&isExternalUrl);
+ m_runningUrl->GetValidUrl(&validUrl);
+ m_runningUrl->GetImapAction(&m_imapAction);
+
+ if (isExternalUrl) {
+ if (m_imapAction == nsIImapUrl::nsImapSelectFolder) {
+ // we need to send a start request so that the doc loader
+ // will call HandleContent on the imap service so we
+ // can abort this url, and run a new url in a new msg window
+ // to run the folder load url and get off this crazy merry-go-round.
+ if (m_channelListener) {
+ m_channelListener->OnStartRequest(m_mockChannel);
+ }
+ return false;
+ }
+ }
+
+ if (!m_imapMailFolderSink && m_imapProtocolSink) {
+ // This occurs when running another URL in the main thread loop
+ rv = m_imapProtocolSink->SetupMainThreadProxies();
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+
+ // Reinitialize the parser
+ GetServerStateParser().InitializeState();
+ GetServerStateParser().SetConnected(true);
+
+ // acknowledge that we are running the url now..
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl =
+ do_QueryInterface(m_runningUrl, &rv);
+ nsAutoCString urlSpec;
+ rv = mailnewsurl->GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+ Log("ProcessCurrentURL", urlSpec.get(),
+ (validUrl) ? " = currentUrl" : " is not valid");
+ if (!validUrl) return false;
+
+ if (NS_SUCCEEDED(rv) && mailnewsurl && m_imapMailFolderSink && !rerunningUrl)
+ m_imapMailFolderSink->SetUrlState(this, mailnewsurl, true, false, NS_OK);
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are starting... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (m_channelListener) // ### not sure we want to do this if rerunning url...
+ {
+ m_channelListener->OnStartRequest(m_mockChannel);
+ }
+ // If we haven't received the greeting yet, we need to make sure we strip
+ // it out of the input before we start to do useful things...
+ if (!TestFlag(IMAP_RECEIVED_GREETING)) EstablishServerConnection();
+
+ // Step 1: If we have not moved into the authenticated state yet then do so
+ // by attempting to logon.
+ if (!DeathSignalReceived() && NS_SUCCEEDED(GetConnectionStatus()) &&
+ (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kNonAuthenticated)) {
+ /* if we got here, the server's greeting should not have been PREAUTH */
+ // If greeting did not contain a capability response and if user has not
+ // configured STARTTLS, request capabilities. If STARTTLS configured,
+ // capabilities will be requested after TLS handshakes are complete.
+ if ((GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined) &&
+ (m_socketType != nsMsgSocketType::alwaysSTARTTLS)) {
+ Capability();
+ }
+
+ // If capability response has yet to occur and STARTTLS is not
+ // configured then drop the connection since this should not happen. Also
+ // drop the connection if capability response has occurred and
+ // the imap version is unacceptable. Show alert only for wrong version.
+ if (((GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined) &&
+ (m_socketType != nsMsgSocketType::alwaysSTARTTLS)) ||
+ (GetServerStateParser().GetCapabilityFlag() &&
+ !(GetServerStateParser().GetCapabilityFlag() &
+ (kIMAP4Capability | kIMAP4rev1Capability | kIMAP4other)))) {
+ if (!DeathSignalReceived() && NS_SUCCEEDED(GetConnectionStatus()) &&
+ GetServerStateParser().GetCapabilityFlag())
+ AlertUserEventUsingName("imapServerNotImap4");
+
+ SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib
+ } else {
+ if ((m_connectionType.EqualsLiteral("starttls") &&
+ (m_socketType == nsMsgSocketType::trySTARTTLS &&
+ (GetServerStateParser().GetCapabilityFlag() &
+ kHasStartTLSCapability))) ||
+ m_socketType == nsMsgSocketType::alwaysSTARTTLS) {
+ StartTLS(); // Send imap STARTTLS command
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ NS_ENSURE_TRUE(m_transport, false);
+ MOZ_ASSERT(!NS_IsMainThread());
+ rv = TransportStartTLS(); // Initiate STARTTLS handshakes
+ if (NS_SUCCEEDED(rv)) {
+ // Transition to secure state is now enabled but handshakes and
+ // negotiation has not yet occurred. Make sure that
+ // the stream input response buffer is drained to avoid false
+ // responses to subsequent commands (capability, login etc),
+ // i.e., due to possible MitM attack doing pre-TLS response
+ // injection. We are discarding any possible malicious data
+ // stored prior to TransportStartTLS().
+ // Note: If any non-TLS related data arrives while transitioning
+ // to secure state (after TransportStartTLS()), it will
+ // cause the TLS negotiation to fail so any injected data is never
+ // accessed since the transport connection will be dropped.
+ char discardBuf[80];
+ uint64_t numBytesInStream = 0;
+ uint32_t numBytesRead;
+ rv = m_inputStream->Available(&numBytesInStream);
+ nsCOMPtr<nsIInputStream> kungFuGrip = m_inputStream;
+ // Read and discard any data available in socket buffer.
+ while (numBytesInStream > 0 && NS_SUCCEEDED(rv)) {
+ rv = m_inputStream->Read(
+ discardBuf,
+ std::min(uint64_t(sizeof discardBuf), numBytesInStream),
+ &numBytesRead);
+ numBytesInStream -= numBytesRead;
+ }
+ kungFuGrip = nullptr;
+
+ // Discard any data lines previously read from socket buffer.
+ m_inputStreamBuffer->ClearBuffer();
+
+ // Force re-issue of "capability", because servers may
+ // enable other auth features (e.g. remove LOGINDISABLED
+ // and add AUTH=PLAIN). Sending imap data here first triggers
+ // the TLS negotiation handshakes.
+ Capability();
+
+ // If user has set pref mail.server.serverX.socketType to 1
+ // (trySTARTTLS, now deprecated in UI) and Capability()
+ // succeeds, indicating TLS handshakes succeeded, set and
+ // latch the socketType to 2 (alwaysSTARTTLS) for this server.
+ if ((m_socketType == nsMsgSocketType::trySTARTTLS) &&
+ GetServerStateParser().LastCommandSuccessful())
+ m_imapServerSink->UpdateTrySTARTTLSPref(true);
+
+ // Courier imap doesn't return STARTTLS capability if we've done
+ // a STARTTLS! But we need to remember this capability so we'll
+ // try to use STARTTLS next time.
+ // Update: This may not be a problem since "next time" will be
+ // on a new connection that is not yet in secure state. So the
+ // capability greeting *will* contain STARTTLS. I observed and
+ // tested this on Courier imap server. But keep this to be sure.
+ eIMAPCapabilityFlags capabilityFlag =
+ GetServerStateParser().GetCapabilityFlag();
+ if (!(capabilityFlag & kHasStartTLSCapability)) {
+ capabilityFlag |= kHasStartTLSCapability;
+ GetServerStateParser().SetCapabilityFlag(capabilityFlag);
+ CommitCapability();
+ }
+ }
+ if (NS_FAILED(rv)) {
+ nsAutoCString logLine("Enable of STARTTLS failed. Error 0x");
+ logLine.AppendInt(static_cast<uint32_t>(rv), 16);
+ Log("ProcessCurrentURL", nullptr, logLine.get());
+ if (m_socketType == nsMsgSocketType::alwaysSTARTTLS) {
+ SetConnectionStatus(rv); // stop netlib
+ if (m_transport) m_transport->Close(rv);
+ } else if (m_socketType == nsMsgSocketType::trySTARTTLS)
+ m_imapServerSink->UpdateTrySTARTTLSPref(false);
+ }
+ } else if (m_socketType == nsMsgSocketType::alwaysSTARTTLS) {
+ SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib
+ if (m_transport) m_transport->Close(rv);
+ } else if (m_socketType == nsMsgSocketType::trySTARTTLS) {
+ // STARTTLS failed, so downgrade socket type
+ m_imapServerSink->UpdateTrySTARTTLSPref(false);
+ }
+ } else if (m_socketType == nsMsgSocketType::trySTARTTLS) {
+ // we didn't know the server supported TLS when we created
+ // the socket, so we're going to retry with a STARTTLS socket
+ if (GetServerStateParser().GetCapabilityFlag() &
+ kHasStartTLSCapability) {
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ TellThreadToDie();
+ SetConnectionStatus(NS_ERROR_FAILURE);
+ return RetryUrl();
+ }
+ // trySTARTTLS set, but server doesn't have TLS capability,
+ // so downgrade socket type
+ m_imapServerSink->UpdateTrySTARTTLSPref(false);
+ m_socketType = nsMsgSocketType::plain;
+ }
+ if (!DeathSignalReceived() && (NS_SUCCEEDED(GetConnectionStatus()))) {
+ logonFailed = !TryToLogon();
+ }
+ if (m_retryUrlOnError) return RetryUrl();
+ }
+ } // if death signal not received
+
+ // We assume one IMAP thread is used for exactly one server, only.
+ if (m_transport && !m_securityInfo) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ GetTransportSecurityInfo(getter_AddRefs(m_securityInfo));
+ }
+
+ if (!DeathSignalReceived() && (NS_SUCCEEDED(GetConnectionStatus()))) {
+ // if the server supports a language extension then we should
+ // attempt to issue the language extension.
+ if (GetServerStateParser().GetCapabilityFlag() & kHasLanguageCapability)
+ Language();
+
+ if (m_runningUrl) {
+ bool foundMailboxesAlready = false;
+ m_hostSessionList->GetHaveWeEverDiscoveredFoldersForHost(
+ GetImapServerKey(), foundMailboxesAlready);
+ if (!foundMailboxesAlready) FindMailboxesIfNecessary();
+ }
+
+ nsImapState imapState = nsIImapUrl::ImapStatusNone;
+ if (m_runningUrl) m_runningUrl->GetRequiredImapState(&imapState);
+
+ if (imapState == nsIImapUrl::nsImapAuthenticatedState)
+ ProcessAuthenticatedStateURL();
+ else // must be a url that requires us to be in the selected state
+ ProcessSelectedStateURL();
+
+ if (m_retryUrlOnError) return RetryUrl();
+
+ // The URL has now been processed
+ if ((!logonFailed && NS_FAILED(GetConnectionStatus())) ||
+ DeathSignalReceived())
+ HandleCurrentUrlError();
+
+ } else if (!logonFailed)
+ HandleCurrentUrlError();
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are stopping... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (m_channelListener) {
+ NS_ASSERTION(m_mockChannel, "no request");
+ if (m_mockChannel) {
+ nsresult status;
+ m_mockChannel->GetStatus(&status);
+ if (!GetServerStateParser().LastCommandSuccessful() &&
+ NS_SUCCEEDED(status))
+ status = NS_MSG_ERROR_IMAP_COMMAND_FAILED;
+ rv = m_channelListener->OnStopRequest(m_mockChannel, status);
+ }
+ }
+ bool suspendUrl = false;
+ m_runningUrl->GetMoreHeadersToDownload(&suspendUrl);
+ if (mailnewsurl && m_imapMailFolderSink) {
+ rv = GetConnectionStatus();
+ // There are error conditions to check even if the connection is OK.
+ if (NS_SUCCEEDED(rv)) {
+ if (logonFailed) {
+ rv = NS_ERROR_FAILURE;
+ } else if (GetServerStateParser().CommandFailed()) {
+ rv = NS_MSG_ERROR_IMAP_COMMAND_FAILED;
+ }
+ }
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(
+ IMAP, LogLevel::Debug,
+ ("URL failed with code 0x%" PRIx32 " (%s)", static_cast<uint32_t>(rv),
+ mailnewsurl->GetSpecOrDefault().get()));
+ // If discovery URL fails, clear the in-progress flag.
+ if (m_imapAction == nsIImapUrl::nsImapDiscoverAllBoxesUrl) {
+ m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(),
+ false);
+ }
+ }
+ // Inform any nsIUrlListeners that the URL has finished. This will invoke
+ // nsIUrlListener.onStopRunningUrl().
+ m_imapMailFolderSink->SetUrlState(this, mailnewsurl, false, suspendUrl, rv);
+
+ // Doom the cache entry if shutting down or thread is terminated.
+ if (NS_FAILED(rv) && DeathSignalReceived() && m_mockChannel) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("ProcessCurrentURL(): Call DoomCacheEntry()"));
+ DoomCacheEntry(mailnewsurl);
+ }
+ } else {
+ // That's seen at times in debug sessions.
+ NS_WARNING("missing url or sink");
+ }
+
+ // disable timeouts before caching connection.
+ if (m_transport)
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE,
+ PR_UINT32_MAX);
+
+ SetFlag(IMAP_CLEAN_UP_URL_STATE);
+
+ nsCOMPtr<nsISupports> copyState;
+ if (m_runningUrl) m_runningUrl->GetCopyState(getter_AddRefs(copyState));
+ // this is so hokey...we MUST clear any local references to the url
+ // BEFORE calling ReleaseUrlState
+ mailnewsurl = nullptr;
+
+ if (suspendUrl) m_imapServerSink->SuspendUrl(m_runningUrl);
+ // save the imap folder sink since we need it to do the CopyNextStreamMessage
+ RefPtr<ImapMailFolderSinkProxy> imapMailFolderSink = m_imapMailFolderSink;
+ // release the url as we are done with it...
+ ReleaseUrlState(false);
+ ResetProgressInfo();
+
+ ClearFlag(IMAP_CLEAN_UP_URL_STATE);
+
+ if (imapMailFolderSink) {
+ if (copyState) {
+ rv = imapMailFolderSink->CopyNextStreamMessage(
+ GetServerStateParser().LastCommandSuccessful() &&
+ NS_SUCCEEDED(GetConnectionStatus()),
+ copyState);
+ if (NS_FAILED(rv))
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("CopyNextStreamMessage failed: %" PRIx32,
+ static_cast<uint32_t>(rv)));
+
+ NS_ReleaseOnMainThread("nsImapProtocol, copyState", copyState.forget());
+ }
+ // we might need this to stick around for IDLE support
+ m_imapMailFolderSink = imapMailFolderSink;
+ imapMailFolderSink = nullptr;
+ } else
+ MOZ_LOG(IMAP, LogLevel::Info, ("null imapMailFolderSink"));
+
+ // now try queued urls, now that we've released this connection.
+ if (m_imapServerSink) {
+ if (NS_SUCCEEDED(GetConnectionStatus()))
+ rv = m_imapServerSink->LoadNextQueuedUrl(this, &anotherUrlRun);
+ else // if we don't do this, they'll just sit and spin until
+ // we run some other url on this server.
+ {
+ Log("ProcessCurrentURL", nullptr, "aborting queued urls");
+ rv = m_imapServerSink->AbortQueuedUrls();
+ }
+ }
+
+ // if we didn't run another url, release the server sink to
+ // cut circular refs.
+ if (!anotherUrlRun) m_imapServerSink = nullptr;
+
+ if (NS_FAILED(GetConnectionStatus()) || !GetServerStateParser().Connected() ||
+ GetServerStateParser().SyntaxError()) {
+ if (m_imapServerSink) m_imapServerSink->RemoveServerConnection(this);
+
+ if (!DeathSignalReceived()) {
+ TellThreadToDie();
+ }
+ } else {
+ if (m_imapServerSink) {
+ bool shuttingDown;
+ m_imapServerSink->GetServerShuttingDown(&shuttingDown);
+ if (shuttingDown) m_useIdle = false;
+ }
+ }
+ return anotherUrlRun;
+}
+
+bool nsImapProtocol::RetryUrl() {
+ nsCOMPtr<nsIImapUrl> kungFuGripImapUrl = m_runningUrl;
+ nsCOMPtr<nsIImapMockChannel> saveMockChannel;
+
+ // the mock channel might be null - that's OK.
+ if (m_imapServerSink)
+ (void)m_imapServerSink->PrepareToRetryUrl(kungFuGripImapUrl,
+ getter_AddRefs(saveMockChannel));
+
+ ReleaseUrlState(true);
+ if (m_imapServerSink) {
+ m_imapServerSink->RemoveServerConnection(this);
+ m_imapServerSink->RetryUrl(kungFuGripImapUrl, saveMockChannel);
+ }
+
+ // Hack for Bug 1586494.
+ // (this is a workaround to try and prevent a specific crash, and
+ // does nothing clarify the threading mess!)
+ // RetryUrl() is only ever called from the imap thread.
+ // Mockchannel dtor insists upon being run on the main thread.
+ // So make sure we don't accidentally cause the mockchannel to die right now.
+ if (saveMockChannel) {
+ NS_ReleaseOnMainThread("nsImapProtocol::RetryUrl",
+ saveMockChannel.forget());
+ }
+
+ return (m_imapServerSink != nullptr); // we're running a url (the same url)
+}
+
+// ignoreBadAndNOResponses --> don't throw a error dialog if this command
+// results in a NO or Bad response from the server..in other words the command
+// is "exploratory" and we don't really care if it succeeds or fails.
+void nsImapProtocol::ParseIMAPandCheckForNewMail(
+ const char* commandString, bool aIgnoreBadAndNOResponses) {
+ if (commandString)
+ GetServerStateParser().ParseIMAPServerResponse(commandString,
+ aIgnoreBadAndNOResponses);
+ else
+ GetServerStateParser().ParseIMAPServerResponse(m_currentCommand.get(),
+ aIgnoreBadAndNOResponses);
+ // **** fix me for new mail biff state *****
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// End of nsIStreamListenerSupport
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsImapProtocol::GetRunningUrl(nsIURI** result) {
+ if (result && m_runningUrl)
+ return m_runningUrl->QueryInterface(NS_GET_IID(nsIURI), (void**)result);
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetRunningImapURL(nsIImapUrl** aImapUrl) {
+ if (aImapUrl && m_runningUrl)
+ return m_runningUrl->QueryInterface(NS_GET_IID(nsIImapUrl),
+ (void**)aImapUrl);
+ return NS_ERROR_NULL_POINTER;
+}
+
+/*
+ * Writes the data contained in dataBuffer into the current output stream. It
+ * also informs the transport layer that this data is now available for
+ * transmission. Returns a positive number for success, 0 for failure (not all
+ * the bytes were written to the stream, etc). We need to make another pass
+ * through this file to install an error system (mscott)
+ */
+
+nsresult nsImapProtocol::SendData(const char* dataBuffer,
+ bool aSuppressLogging) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+
+ if (!m_transport) {
+ Log("SendData", nullptr, "clearing IMAP_CONNECTION_IS_OPEN");
+ // the connection died unexpectedly! so clear the open connection flag
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ TellThreadToDie();
+ SetConnectionStatus(NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (dataBuffer && m_outputStream) {
+ m_currentCommand = dataBuffer;
+ if (!aSuppressLogging)
+ Log("SendData", nullptr, dataBuffer);
+ else
+ Log("SendData", nullptr,
+ "Logging suppressed for this command (it probably contained "
+ "authentication information)");
+
+ {
+ // don't allow someone to close the stream/transport out from under us
+ // this can happen when the ui thread calls TellThreadToDie.
+ PR_CEnterMonitor(this);
+ uint32_t n;
+ if (m_outputStream)
+ rv = m_outputStream->Write(dataBuffer, PL_strlen(dataBuffer), &n);
+ PR_CExitMonitor(this);
+ }
+ if (NS_FAILED(rv)) {
+ Log("SendData", nullptr, "clearing IMAP_CONNECTION_IS_OPEN");
+ // the connection died unexpectedly! so clear the open connection flag
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ TellThreadToDie();
+ SetConnectionStatus(rv);
+ if (m_runningUrl && !m_retryUrlOnError) {
+ bool alreadyRerunningUrl;
+ m_runningUrl->GetRerunningUrl(&alreadyRerunningUrl);
+ if (!alreadyRerunningUrl) {
+ m_runningUrl->SetRerunningUrl(true);
+ m_retryUrlOnError = true;
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// Begin protocol state machine functions...
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+// ProcessProtocolState - we override this only so we'll link - it should never
+// get called.
+
+nsresult nsImapProtocol::ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) {
+ return NS_OK;
+}
+
+class UrlListenerNotifierEvent : public mozilla::Runnable {
+ public:
+ UrlListenerNotifierEvent(nsIMsgMailNewsUrl* aUrl, nsIImapProtocol* aProtocol)
+ : mozilla::Runnable("UrlListenerNotifierEvent"),
+ mUrl(aUrl),
+ mProtocol(aProtocol) {}
+
+ NS_IMETHOD Run() {
+ if (mUrl) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ mUrl->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_TRUE(folder, NS_OK);
+ nsCOMPtr<nsIImapMailFolderSink> folderSink(do_QueryInterface(folder));
+ // This causes the url listener to get OnStart and Stop notifications.
+ folderSink->SetUrlState(mProtocol, mUrl, true, false, NS_OK);
+ folderSink->SetUrlState(mProtocol, mUrl, false, false, NS_OK);
+ }
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIMsgMailNewsUrl> mUrl;
+ nsCOMPtr<nsIImapProtocol> mProtocol;
+};
+
+bool nsImapProtocol::TryToRunUrlLocally(nsIURI* aURL, nsISupports* aConsumer) {
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aURL, &rv));
+ NS_ENSURE_SUCCESS(rv, false);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aURL);
+ nsCString messageIdString;
+ imapUrl->GetListOfMessageIds(messageIdString);
+ bool useLocalCache = false;
+ if (!messageIdString.IsEmpty() &&
+ !HandlingMultipleMessages(messageIdString)) {
+ nsImapAction action;
+ imapUrl->GetImapAction(&action);
+ nsCOMPtr<nsIMsgFolder> folder;
+ mailnewsUrl->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_TRUE(folder, false);
+
+ folder->HasMsgOffline(strtoul(messageIdString.get(), nullptr, 10),
+ &useLocalCache);
+ mailnewsUrl->SetMsgIsInLocalCache(useLocalCache);
+ // We're downloading a single message for offline use, and it's
+ // already offline. So we shouldn't do anything, but we do
+ // need to notify the url listener.
+ if (useLocalCache && action == nsIImapUrl::nsImapMsgDownloadForOffline) {
+ nsCOMPtr<nsIRunnable> event =
+ new UrlListenerNotifierEvent(mailnewsUrl, this);
+ // Post this as an event because it can lead to re-entrant calls to
+ // LoadNextQueuedUrl if the listener runs a new url.
+ if (event) NS_DispatchToCurrentThread(event);
+ return true;
+ }
+ }
+ if (!useLocalCache) return false;
+
+ nsCOMPtr<nsIImapMockChannel> mockChannel;
+ imapUrl->GetMockChannel(getter_AddRefs(mockChannel));
+ if (!mockChannel) return false;
+
+ nsImapMockChannel* imapChannel =
+ static_cast<nsImapMockChannel*>(mockChannel.get());
+ if (!imapChannel) return false;
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ imapChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (!loadGroup) // if we don't have one, the url will snag one from the msg
+ // window...
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ if (loadGroup)
+ loadGroup->RemoveRequest((nsIRequest*)mockChannel,
+ nullptr /* context isupports */, NS_OK);
+
+ if (imapChannel->ReadFromLocalCache()) {
+ (void)imapChannel->NotifyStartEndReadFromCache(true);
+ return true;
+ }
+ return false;
+}
+
+// LoadImapUrl takes a url, initializes all of our url specific data by calling
+// SetupUrl. Finally, we signal the url to run monitor to let the imap main
+// thread loop process the current url (it is waiting on this monitor). There
+// is a contract that the imap thread has already been started before we
+// attempt to load a url...
+// LoadImapUrl() is called by nsImapIncomingServer to run a queued url on a free
+// connection.
+NS_IMETHODIMP nsImapProtocol::LoadImapUrl(nsIURI* aURL,
+ nsISupports* aConsumer) {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (aURL) {
+#ifdef DEBUG_bienvenu
+ printf("loading url %s\n", aURL->GetSpecOrDefault().get());
+#endif
+ // We might be able to fulfill the request locally (e.g. fetching a message
+ // which is already stored offline).
+ if (TryToRunUrlLocally(aURL, aConsumer)) return NS_OK;
+ m_urlInProgress = true;
+ m_imapMailFolderSink = nullptr;
+ rv = SetupWithUrl(aURL, aConsumer);
+ m_lastActiveTime = PR_Now();
+ }
+ return rv;
+}
+
+nsresult nsImapProtocol::LoadImapUrlInternal() {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ if (m_transport && m_mockChannel) {
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT,
+ gResponseTimeout + 60);
+ int32_t readWriteTimeout = gResponseTimeout;
+ if (m_runningUrl) {
+ m_runningUrl->GetImapAction(&m_imapAction);
+ // This is a silly hack, but the default of 100 seconds is typically way
+ // too long for things like APPEND, which should come back immediately.
+ // However, for large messages on some servers the final append response
+ // time can be longer. So now it is one-fifth of the configured
+ // `mailnews.tcptimeout' which defaults to 20 seconds.
+ if (m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile ||
+ m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile) {
+ readWriteTimeout = gAppendTimeout;
+ } else if (m_imapAction == nsIImapUrl::nsImapOnlineMove ||
+ m_imapAction == nsIImapUrl::nsImapOnlineCopy) {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ uint32_t copyCount = CountMessagesInIdString(messageIdString.get());
+ // If we're move/copying a large number of messages,
+ // which should be rare, increase the timeout based on number
+ // of messages. 40 messages per second should be sufficiently slow.
+ if (copyCount > 2400) // 40 * 60, 60 is default read write timeout
+ readWriteTimeout =
+ std::max(readWriteTimeout, (int32_t)copyCount / 40);
+ }
+ }
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE,
+ readWriteTimeout);
+ // Set the security info for the mock channel to be the security info for
+ // our underlying transport.
+ if (m_securityInfo) {
+ m_mockChannel->SetSecurityInfo(m_securityInfo);
+ }
+
+ SetSecurityCallbacksFromChannel(m_transport, m_mockChannel);
+
+ nsCOMPtr<nsITransportEventSink> sinkMC = do_QueryInterface(m_mockChannel);
+ if (sinkMC) {
+ nsCOMPtr<nsIThread> thread = do_GetMainThread();
+ RefPtr<nsImapTransportEventSink> sink = new nsImapTransportEventSink;
+ rv = net_NewTransportEventSinkProxy(getter_AddRefs(sink->m_proxy), sinkMC,
+ thread);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_transport->SetEventSink(sink, nullptr);
+ }
+
+ // And if we have a cache2 entry that we are saving the message to, set the
+ // security info on it too.
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ if (mailnewsUrl && m_securityInfo) {
+ nsCOMPtr<nsICacheEntry> cacheEntry;
+ mailnewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry));
+ if (cacheEntry) {
+ cacheEntry->SetSecurityInfo(m_securityInfo);
+ }
+ }
+ }
+
+ rv = SetupSinkProxy(); // generate proxies for all of the event sinks in the
+ // url
+ if (NS_FAILED(rv)) // URL can be invalid.
+ return rv;
+
+ if (m_transport && m_runningUrl) {
+ nsImapAction imapAction;
+ m_runningUrl->GetImapAction(&imapAction);
+ // if we're shutting down, and not running the kinds of urls we run at
+ // shutdown, then this should fail because running urls during
+ // shutdown will very likely fail and potentially hang.
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool shuttingDown = false;
+ (void)accountMgr->GetShutdownInProgress(&shuttingDown);
+ if (shuttingDown && imapAction != nsIImapUrl::nsImapExpungeFolder &&
+ imapAction != nsIImapUrl::nsImapDeleteAllMsgs &&
+ imapAction != nsIImapUrl::nsImapDeleteFolder)
+ return NS_ERROR_FAILURE;
+
+ // if we're running a select or delete all, do a noop first.
+ // this should really be in the connection cache code when we know
+ // we're pulling out a selected state connection, but maybe we
+ // can get away with this.
+ m_needNoop = (imapAction == nsIImapUrl::nsImapSelectFolder ||
+ imapAction == nsIImapUrl::nsImapDeleteAllMsgs);
+
+ // We now have a url to run so signal the monitor for url ready to be
+ // processed...
+ ReentrantMonitorAutoEnter urlReadyMon(m_urlReadyToRunMonitor);
+ m_nextUrlReadyToRun = true;
+ urlReadyMon.Notify();
+
+ } // if we have an imap url and a transport
+ else {
+ NS_ASSERTION(false, "missing channel or running url");
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapProtocol::IsBusy(bool* aIsConnectionBusy,
+ bool* isInboxConnection) {
+ if (!aIsConnectionBusy || !isInboxConnection) return NS_ERROR_NULL_POINTER;
+ nsresult rv = NS_OK;
+ *aIsConnectionBusy = false;
+ *isInboxConnection = false;
+ if (!m_transport) {
+ // this connection might not be fully set up yet.
+ rv = NS_ERROR_FAILURE;
+ } else {
+ if (m_urlInProgress) // do we have a url? That means we're working on it...
+ *aIsConnectionBusy = true;
+
+ if (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected &&
+ GetServerStateParser().GetSelectedMailboxName() &&
+ PL_strcasecmp(GetServerStateParser().GetSelectedMailboxName(),
+ "Inbox") == 0)
+ *isInboxConnection = true;
+ }
+ return rv;
+}
+
+#define IS_SUBSCRIPTION_RELATED_ACTION(action) \
+ (action == nsIImapUrl::nsImapSubscribe || \
+ action == nsIImapUrl::nsImapUnsubscribe || \
+ action == nsIImapUrl::nsImapDiscoverAllBoxesUrl || \
+ action == nsIImapUrl::nsImapListFolder)
+
+// canRunUrl means the connection is not busy, and is in the selected state
+// for the desired folder (or authenticated).
+// has to wait means it's in the right selected state, but busy.
+NS_IMETHODIMP nsImapProtocol::CanHandleUrl(nsIImapUrl* aImapUrl,
+ bool* aCanRunUrl, bool* hasToWait) {
+ if (!aCanRunUrl || !hasToWait || !aImapUrl) return NS_ERROR_NULL_POINTER;
+ nsresult rv = NS_OK;
+ MutexAutoLock mon(mLock);
+
+ *aCanRunUrl = false; // assume guilty until proven otherwise...
+ *hasToWait = false;
+
+ if (DeathSignalReceived()) return NS_ERROR_FAILURE;
+
+ bool isBusy = false;
+ bool isInboxConnection = false;
+
+ if (!m_transport) {
+ // this connection might not be fully set up yet.
+ return NS_ERROR_FAILURE;
+ }
+ IsBusy(&isBusy, &isInboxConnection);
+ bool inSelectedState = GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected;
+
+ nsAutoCString curSelectedUrlFolderName;
+ nsAutoCString pendingUrlFolderName;
+ if (inSelectedState)
+ curSelectedUrlFolderName = GetServerStateParser().GetSelectedMailboxName();
+
+ if (isBusy) {
+ nsImapState curUrlImapState;
+ NS_ASSERTION(m_runningUrl, "isBusy, but no running url.");
+ if (m_runningUrl) {
+ m_runningUrl->GetRequiredImapState(&curUrlImapState);
+ if (curUrlImapState == nsIImapUrl::nsImapSelectedState) {
+ char* folderName = GetFolderPathString();
+ if (!curSelectedUrlFolderName.Equals(folderName))
+ pendingUrlFolderName.Assign(folderName);
+ inSelectedState = true;
+ PR_Free(folderName);
+ }
+ }
+ }
+
+ nsImapState imapState;
+ nsImapAction actionForProposedUrl;
+ aImapUrl->GetImapAction(&actionForProposedUrl);
+ aImapUrl->GetRequiredImapState(&imapState);
+
+ // OK, this is a bit of a hack - we're going to pretend that
+ // these types of urls requires a selected state connection on
+ // the folder in question. This isn't technically true,
+ // but we would much rather use that connection for several reasons,
+ // one is that some UW servers require us to use that connection
+ // the other is that we don't want to leave a connection dangling in
+ // the selected state for the deleted folder.
+ // If we don't find a connection in that selected state,
+ // we'll fall back to the first free connection.
+ bool isSelectedStateUrl =
+ imapState == nsIImapUrl::nsImapSelectedState ||
+ actionForProposedUrl == nsIImapUrl::nsImapDeleteFolder ||
+ actionForProposedUrl == nsIImapUrl::nsImapRenameFolder ||
+ actionForProposedUrl == nsIImapUrl::nsImapMoveFolderHierarchy ||
+ actionForProposedUrl == nsIImapUrl::nsImapAppendDraftFromFile ||
+ actionForProposedUrl == nsIImapUrl::nsImapAppendMsgFromFile ||
+ actionForProposedUrl == nsIImapUrl::nsImapFolderStatus;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(aImapUrl);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = msgUrl->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv)) {
+ // compare host/user between url and connection.
+ nsCString urlHostName;
+ nsCString urlUserName;
+ rv = server->GetHostName(urlHostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = server->GetUsername(urlUserName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if ((GetImapHostName().IsEmpty() ||
+ urlHostName.Equals(GetImapHostName(),
+ nsCaseInsensitiveCStringComparator)) &&
+ (GetImapUserName().IsEmpty() ||
+ urlUserName.Equals(GetImapUserName(),
+ nsCaseInsensitiveCStringComparator))) {
+ if (isSelectedStateUrl) {
+ if (inSelectedState) {
+ // *** jt - in selected state can only run url with
+ // matching foldername
+ char* folderNameForProposedUrl = nullptr;
+ rv = aImapUrl->CreateServerSourceFolderPathString(
+ &folderNameForProposedUrl);
+ if (NS_SUCCEEDED(rv) && folderNameForProposedUrl) {
+ bool isInbox =
+ PL_strcasecmp("Inbox", folderNameForProposedUrl) == 0;
+ if (!curSelectedUrlFolderName.IsEmpty() ||
+ !pendingUrlFolderName.IsEmpty()) {
+ bool matched = isInbox
+ ? PL_strcasecmp(curSelectedUrlFolderName.get(),
+ folderNameForProposedUrl) == 0
+ : PL_strcmp(curSelectedUrlFolderName.get(),
+ folderNameForProposedUrl) == 0;
+ if (!matched && !pendingUrlFolderName.IsEmpty()) {
+ matched = isInbox ? PL_strcasecmp(pendingUrlFolderName.get(),
+ folderNameForProposedUrl) == 0
+ : PL_strcmp(pendingUrlFolderName.get(),
+ folderNameForProposedUrl) == 0;
+ }
+ if (matched) {
+ if (isBusy)
+ *hasToWait = true;
+ else
+ *aCanRunUrl = true;
+ }
+ }
+ }
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("proposed url = %s folder for connection %s has To Wait = "
+ "%s can run = %s",
+ folderNameForProposedUrl, curSelectedUrlFolderName.get(),
+ (*hasToWait) ? "true" : "false",
+ (*aCanRunUrl) ? "true" : "false"));
+ PR_FREEIF(folderNameForProposedUrl);
+ }
+ } else // *** jt - an authenticated state url can be run in either
+ // authenticated or selected state
+ {
+ nsImapAction actionForRunningUrl;
+
+ // If proposed url is subscription related, and we are currently running
+ // a subscription url, then we want to queue the proposed url after the
+ // current url. Otherwise, we can run this url if we're not busy. If we
+ // never find a running subscription-related url, the caller will just
+ // use whatever free connection it can find, which is what we want.
+ if (IS_SUBSCRIPTION_RELATED_ACTION(actionForProposedUrl)) {
+ if (isBusy && m_runningUrl) {
+ m_runningUrl->GetImapAction(&actionForRunningUrl);
+ if (IS_SUBSCRIPTION_RELATED_ACTION(actionForRunningUrl)) {
+ *aCanRunUrl = false;
+ *hasToWait = true;
+ }
+ }
+ } else {
+ if (!isBusy) *aCanRunUrl = true;
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+// Command tag handling stuff.
+// Zero tag number indicates never used so set it to an initial random number
+// between 1 and 100. Otherwise just increment the uint32_t value unless it
+// rolls to zero then set it to 1. Then convert the tag number to a string for
+// use in IMAP commands.
+void nsImapProtocol::IncrementCommandTagNumber() {
+ if (m_currentServerCommandTagNumber == 0) {
+ srand((unsigned)m_lastCheckTime);
+ m_currentServerCommandTagNumber = 1 + (rand() % 100);
+ } else if (++m_currentServerCommandTagNumber == 0) {
+ m_currentServerCommandTagNumber = 1;
+ }
+ sprintf(m_currentServerCommandTag, "%u", m_currentServerCommandTagNumber);
+}
+
+const char* nsImapProtocol::GetServerCommandTag() {
+ return m_currentServerCommandTag;
+}
+
+/**
+ * ProcessSelectedStateURL() is a helper for ProcessCurrentURL(). It handles
+ * running URLs which require the connection to be in the selected state.
+ * It will issue SELECT commands if needed to make sure the correct mailbox
+ * is selected.
+ */
+void nsImapProtocol::ProcessSelectedStateURL() {
+ nsCString mailboxName;
+ bool bMessageIdsAreUids = true;
+ bool moreHeadersToDownload;
+ imapMessageFlagsType msgFlags = 0;
+ nsCString urlHost;
+
+ // this can't fail, can it?
+ nsresult res;
+ res = m_runningUrl->GetImapAction(&m_imapAction);
+ m_runningUrl->MessageIdsAreUids(&bMessageIdsAreUids);
+ m_runningUrl->GetMsgFlags(&msgFlags);
+ m_runningUrl->GetMoreHeadersToDownload(&moreHeadersToDownload);
+
+ res = CreateServerSourceFolderPathString(getter_Copies(mailboxName));
+ if (NS_FAILED(res))
+ Log("ProcessSelectedStateURL", nullptr,
+ "error getting source folder path string");
+
+ if (NS_SUCCEEDED(res) && !DeathSignalReceived()) {
+ bool selectIssued = false;
+ if (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected) {
+ if (GetServerStateParser().GetSelectedMailboxName() &&
+ PL_strcmp(GetServerStateParser().GetSelectedMailboxName(),
+ mailboxName.get())) { // we are selected in another folder
+ if (m_closeNeededBeforeSelect) ImapClose();
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ selectIssued = true;
+ SelectMailbox(mailboxName.get());
+ }
+ } else if (!GetServerStateParser()
+ .GetSelectedMailboxName()) { // why are we in the
+ // selected state with no
+ // box name?
+ SelectMailbox(mailboxName.get());
+ selectIssued = true;
+ } else if (moreHeadersToDownload &&
+ m_imapMailFolderSink) // we need to fetch older headers
+ {
+ nsTArray<nsMsgKey> msgIdList;
+ bool more;
+ m_imapMailFolderSink->GetMsgHdrsToDownload(
+ &more, &m_progressExpectedNumber, msgIdList);
+ if (msgIdList.Length() > 0) {
+ FolderHeaderDump(msgIdList.Elements(), msgIdList.Length());
+ m_runningUrl->SetMoreHeadersToDownload(more);
+ // We're going to be re-running this url.
+ if (more) m_runningUrl->SetRerunningUrl(true);
+ }
+ HeaderFetchCompleted();
+ } else {
+ // get new message counts, if any, from server
+ if (m_needNoop) {
+ // For some IMAP servers, to detect new email we must send imap
+ // SELECT even if already SELECTed on the same mailbox.
+ if (m_forceSelect) {
+ SelectMailbox(mailboxName.get());
+ selectIssued = true;
+ }
+
+ m_noopCount++;
+ if ((gPromoteNoopToCheckCount > 0 &&
+ (m_noopCount % gPromoteNoopToCheckCount) == 0) ||
+ CheckNeeded())
+ Check();
+ else
+ Noop(); // I think this is needed when we're using a cached
+ // connection
+ m_needNoop = false;
+ }
+ }
+ } else {
+ // go to selected state
+ SelectMailbox(mailboxName.get());
+ selectIssued = GetServerStateParser().LastCommandSuccessful();
+ }
+
+ if (selectIssued) RefreshACLForFolderIfNecessary(mailboxName.get());
+
+ bool uidValidityOk = true;
+ if (GetServerStateParser().LastCommandSuccessful() && selectIssued &&
+ (m_imapAction != nsIImapUrl::nsImapSelectFolder) &&
+ (m_imapAction != nsIImapUrl::nsImapLiteSelectFolder)) {
+ // error on the side of caution, if the fe event fails to set
+ // uidStruct->returnValidity, then assume that UIDVALIDITY did not roll.
+ // This is a common case event for attachments that are fetched within a
+ // browser context.
+ if (!DeathSignalReceived())
+ uidValidityOk = m_uidValidity == kUidUnknown ||
+ m_uidValidity == GetServerStateParser().FolderUID();
+ }
+
+ if (!uidValidityOk)
+ Log("ProcessSelectedStateURL", nullptr, "uid validity not ok");
+ if (GetServerStateParser().LastCommandSuccessful() &&
+ !DeathSignalReceived() &&
+ (uidValidityOk || m_imapAction == nsIImapUrl::nsImapDeleteAllMsgs)) {
+ if (GetServerStateParser().CurrentFolderReadOnly()) {
+ Log("ProcessSelectedStateURL", nullptr, "current folder read only");
+ if (m_imapAction == nsIImapUrl::nsImapAddMsgFlags ||
+ m_imapAction == nsIImapUrl::nsImapSubtractMsgFlags) {
+ bool canChangeFlag = false;
+ if (GetServerStateParser().ServerHasACLCapability() &&
+ m_imapMailFolderSink) {
+ uint32_t aclFlags = 0;
+
+ if (NS_SUCCEEDED(m_imapMailFolderSink->GetAclFlags(&aclFlags)) &&
+ aclFlags != 0) // make sure we have some acl flags
+ canChangeFlag = ((msgFlags & kImapMsgSeenFlag) &&
+ (aclFlags & IMAP_ACL_STORE_SEEN_FLAG));
+ } else
+ canChangeFlag = (GetServerStateParser().SettablePermanentFlags() &
+ msgFlags) == msgFlags;
+ if (!canChangeFlag) return;
+ }
+ if (m_imapAction == nsIImapUrl::nsImapExpungeFolder ||
+ m_imapAction == nsIImapUrl::nsImapDeleteMsg ||
+ m_imapAction == nsIImapUrl::nsImapDeleteAllMsgs)
+ return;
+ }
+ switch (m_imapAction) {
+ case nsIImapUrl::nsImapLiteSelectFolder:
+ if (GetServerStateParser().LastCommandSuccessful() &&
+ m_imapMailFolderSink && !moreHeadersToDownload) {
+ m_imapMailFolderSink->SetUidValidity(
+ GetServerStateParser().FolderUID());
+ ProcessMailboxUpdate(false); // handle uidvalidity change
+ }
+ break;
+ case nsIImapUrl::nsImapSaveMessageToDisk:
+ case nsIImapUrl::nsImapMsgFetch:
+ case nsIImapUrl::nsImapMsgFetchPeek:
+ case nsIImapUrl::nsImapMsgDownloadForOffline:
+ case nsIImapUrl::nsImapMsgPreview: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ // we don't want to send the flags back in a group
+ if (HandlingMultipleMessages(messageIdString) ||
+ m_imapAction == nsIImapUrl::nsImapMsgDownloadForOffline ||
+ m_imapAction == nsIImapUrl::nsImapMsgPreview) {
+ // multiple messages, fetch them all
+ SetProgressString(IMAP_MESSAGES_STRING_INDEX);
+
+ m_progressCurrentNumber[m_stringIndex] = 0;
+ m_progressExpectedNumber =
+ CountMessagesInIdString(messageIdString.get());
+
+ FetchMessage(messageIdString,
+ (m_imapAction == nsIImapUrl::nsImapMsgPreview)
+ ? kBodyStart
+ : kEveryThingRFC822Peek);
+ if (m_imapAction == nsIImapUrl::nsImapMsgPreview)
+ HeaderFetchCompleted();
+ SetProgressString(IMAP_EMPTY_STRING_INDEX);
+ } else {
+ // A single message ID
+ nsIMAPeFetchFields whatToFetch = kEveryThingRFC822;
+ if (m_imapAction == nsIImapUrl::nsImapMsgFetchPeek)
+ whatToFetch = kEveryThingRFC822Peek;
+
+ // Note: Should no longer fetch a specific imap section (part).
+ // First, let's see if we're requesting a specific MIME part.
+ char* imappart = nullptr;
+ m_runningUrl->GetImapPartToFetch(&imappart);
+ MOZ_ASSERT(!imappart, "no longer fetching imap section/imappart");
+ // downloading a single message: try to do it by bodystructure,
+ // and/or do it by chunks
+ // Note: No longer doing bodystructure.
+ uint32_t messageSize = GetMessageSize(messageIdString);
+
+ // The "wontFit" and cache parameter calculations (customLimit,
+ // realLimit) are only for debug information logging below.
+ if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl =
+ do_QueryInterface(m_runningUrl);
+ if (mailnewsurl) {
+ bool wontFit = net::CacheObserver::EntryIsTooBig(
+ messageSize, gUseDiskCache2);
+ int64_t customLimit;
+ int64_t realLimit;
+ if (gUseDiskCache2) {
+ customLimit = net::CacheObserver::MaxDiskEntrySize();
+ realLimit = net::CacheObserver::DiskCacheCapacity();
+ } else {
+ customLimit = net::CacheObserver::MaxMemoryEntrySize();
+ realLimit = net::CacheObserver::MemoryCacheCapacity();
+ }
+ if (!(customLimit & (int64_t)0x80000000))
+ customLimit <<= 10; // multiply by 1024 to get num bytes
+ else
+ customLimit = (int32_t)customLimit; // make it negative
+ realLimit <<= (10 - 3); // 1/8th capacity, num bytes.
+ if (customLimit > -1 && customLimit < realLimit)
+ realLimit = customLimit;
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: customLimit=%" PRId64 ", realLimit=%" PRId64,
+ __func__, customLimit, realLimit));
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: URL=%s, messageSize=%d, cache too small=%d(bool)",
+ __func__, mailnewsurl->GetSpecOrDefault().get(),
+ messageSize, wontFit));
+ }
+ }
+ // Note again: No longer doing bodystructure.
+ // Fetch the whole thing, and try to do it in chunks.
+ MOZ_LOG(
+ IMAPCache, LogLevel::Debug,
+ ("%s: Fetch entire message with FetchTryChunking", __func__));
+ FetchTryChunking(messageIdString, whatToFetch, bMessageIdsAreUids,
+ NULL, messageSize, true);
+ // If fetch was not a peek, ensure that the message displays as
+ // read (not bold) in case the server fails to mark the message
+ // as SEEN.
+ if (GetServerStateParser().LastCommandSuccessful() &&
+ m_imapAction != nsIImapUrl::nsImapMsgFetchPeek) {
+ uint32_t uid = strtoul(messageIdString.get(), nullptr, 10);
+ int32_t index;
+ bool foundIt;
+ imapMessageFlagsType flags =
+ m_flagState->GetMessageFlagsFromUID(uid, &foundIt, &index);
+ if (foundIt) {
+ flags |= kImapMsgSeenFlag;
+ m_flagState->SetMessageFlags(index, flags);
+ }
+ }
+ }
+ } break;
+ case nsIImapUrl::nsImapExpungeFolder:
+ Expunge();
+ // note fall through to next cases.
+ [[fallthrough]];
+ case nsIImapUrl::nsImapSelectFolder:
+ case nsIImapUrl::nsImapSelectNoopFolder:
+ if (!moreHeadersToDownload) ProcessMailboxUpdate(true);
+ break;
+ case nsIImapUrl::nsImapMsgHeader: {
+ nsCString messageIds;
+ m_runningUrl->GetListOfMessageIds(messageIds);
+
+ FetchMessage(messageIds, kHeadersRFC822andUid);
+ // if we explicitly ask for headers, as opposed to getting them as a
+ // result of selecting the folder, or biff, send the
+ // headerFetchCompleted notification to flush out the header cache.
+ HeaderFetchCompleted();
+ } break;
+ case nsIImapUrl::nsImapSearch: {
+ nsAutoCString searchCriteriaString;
+ m_runningUrl->CreateSearchCriteriaString(
+ getter_Copies(searchCriteriaString));
+ Search(searchCriteriaString.get(), bMessageIdsAreUids);
+ // drop the results on the floor for now
+ } break;
+ case nsIImapUrl::nsImapUserDefinedMsgCommand: {
+ nsCString messageIdString;
+ nsCString command;
+
+ m_runningUrl->GetCommand(command);
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ IssueUserDefinedMsgCommand(command.get(), messageIdString.get());
+ } break;
+ case nsIImapUrl::nsImapUserDefinedFetchAttribute: {
+ nsCString messageIdString;
+ nsCString attribute;
+
+ m_runningUrl->GetCustomAttributeToFetch(attribute);
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ FetchMsgAttribute(messageIdString, attribute);
+ } break;
+ case nsIImapUrl::nsImapMsgStoreCustomKeywords: {
+ // If the server doesn't support user defined flags, don't try to
+ // define/set new ones. But if this is an attempt by TB to set or
+ // reset flags "Junk" or "NonJunk", change "Junk" or "NonJunk" to
+ // "$Junk" or "$NotJunk" respectively and store the modified flag
+ // name if the server doesn't support storing user defined flags
+ // and the server does allow storing the almost-standard flag names
+ // "$Junk" and "$NotJunk". Yahoo imap server is an example of this.
+ uint16_t userFlags = 0;
+ GetSupportedUserFlags(&userFlags);
+ bool userDefinedSettable = userFlags & kImapMsgSupportUserFlag;
+ bool stdJunkOk = GetServerStateParser().IsStdJunkNotJunkUseOk();
+
+ nsCString messageIdString;
+ nsCString addFlags;
+ nsCString subtractFlags;
+
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ m_runningUrl->GetCustomAddFlags(addFlags);
+ m_runningUrl->GetCustomSubtractFlags(subtractFlags);
+ if (!addFlags.IsEmpty()) {
+ if (!userDefinedSettable) {
+ if (stdJunkOk) {
+ if (addFlags.EqualsIgnoreCase("junk"))
+ addFlags = "$Junk";
+ else if (addFlags.EqualsIgnoreCase("nonjunk"))
+ addFlags = "$NotJunk";
+ else
+ break;
+ } else
+ break;
+ }
+ nsAutoCString storeString("+FLAGS (");
+ storeString.Append(addFlags);
+ storeString.Append(')');
+ Store(messageIdString, storeString.get(), true);
+ }
+ if (!subtractFlags.IsEmpty()) {
+ if (!userDefinedSettable) {
+ if (stdJunkOk) {
+ if (subtractFlags.EqualsIgnoreCase("junk"))
+ subtractFlags = "$Junk";
+ else if (subtractFlags.EqualsIgnoreCase("nonjunk"))
+ subtractFlags = "$NotJunk";
+ else
+ break;
+ } else
+ break;
+ }
+ nsAutoCString storeString("-FLAGS (");
+ storeString.Append(subtractFlags);
+ storeString.Append(')');
+ Store(messageIdString, storeString.get(), true);
+ }
+ } break;
+ case nsIImapUrl::nsImapDeleteMsg: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+
+ ProgressEventFunctionUsingName(
+ HandlingMultipleMessages(messageIdString)
+ ? "imapDeletingMessages"
+ : "imapDeletingMessage");
+
+ Store(messageIdString, "+FLAGS (\\Deleted)", bMessageIdsAreUids);
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsCString canonicalName;
+ const char* selectedMailboxName =
+ GetServerStateParser().GetSelectedMailboxName();
+ if (selectedMailboxName) {
+ m_runningUrl->AllocateCanonicalPath(
+ selectedMailboxName, kOnlineHierarchySeparatorUnknown,
+ getter_Copies(canonicalName));
+ }
+
+ if (m_imapMessageSink)
+ m_imapMessageSink->NotifyMessageDeleted(
+ canonicalName.get(), false, messageIdString.get());
+ // notice we don't wait for this to finish...
+ } else
+ HandleMemoryFailure();
+ } break;
+ case nsIImapUrl::nsImapDeleteFolderAndMsgs:
+ DeleteFolderAndMsgs(mailboxName.get());
+ break;
+ case nsIImapUrl::nsImapDeleteAllMsgs: {
+ uint32_t numberOfMessages = GetServerStateParser().NumberOfMessages();
+ if (numberOfMessages) {
+ Store("1:*"_ns, "+FLAGS.SILENT (\\Deleted)",
+ false); // use sequence #'s
+
+ if (GetServerStateParser().LastCommandSuccessful())
+ Expunge(); // expunge messages with deleted flag
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsCString canonicalName;
+ const char* selectedMailboxName =
+ GetServerStateParser().GetSelectedMailboxName();
+ if (selectedMailboxName) {
+ m_runningUrl->AllocateCanonicalPath(
+ selectedMailboxName, kOnlineHierarchySeparatorUnknown,
+ getter_Copies(canonicalName));
+ }
+
+ if (m_imapMessageSink)
+ m_imapMessageSink->NotifyMessageDeleted(canonicalName.get(),
+ true, nullptr);
+ }
+ }
+ bool deleteSelf = false;
+ DeleteSubFolders(mailboxName.get(), deleteSelf); // don't delete self
+ } break;
+ case nsIImapUrl::nsImapAppendDraftFromFile: {
+ OnAppendMsgFromFile();
+ } break;
+ case nsIImapUrl::nsImapAddMsgFlags: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+
+ ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags,
+ true);
+ } break;
+ case nsIImapUrl::nsImapSubtractMsgFlags: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+
+ ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags,
+ false);
+ } break;
+ case nsIImapUrl::nsImapSetMsgFlags: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+
+ ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags,
+ true);
+ ProcessStoreFlags(messageIdString, bMessageIdsAreUids, ~msgFlags,
+ false);
+ } break;
+ case nsIImapUrl::nsImapBiff:
+ PeriodicBiff();
+ break;
+ case nsIImapUrl::nsImapOnlineCopy:
+ case nsIImapUrl::nsImapOnlineMove: {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ char* destinationMailbox =
+ OnCreateServerDestinationFolderPathString();
+
+ if (destinationMailbox) {
+ if (m_imapAction == nsIImapUrl::nsImapOnlineMove) {
+ if (HandlingMultipleMessages(messageIdString))
+ ProgressEventFunctionUsingNameWithString("imapMovingMessages",
+ destinationMailbox);
+ else
+ ProgressEventFunctionUsingNameWithString("imapMovingMessage",
+ destinationMailbox);
+ } else {
+ if (HandlingMultipleMessages(messageIdString))
+ ProgressEventFunctionUsingNameWithString("imapCopyingMessages",
+ destinationMailbox);
+ else
+ ProgressEventFunctionUsingNameWithString("imapCopyingMessage",
+ destinationMailbox);
+ }
+ Copy(messageIdString.get(), destinationMailbox, bMessageIdsAreUids);
+ PR_FREEIF(destinationMailbox);
+ ImapOnlineCopyState copyState;
+ if (DeathSignalReceived())
+ copyState = ImapOnlineCopyStateType::kInterruptedState;
+ else
+ copyState = GetServerStateParser().LastCommandSuccessful()
+ ? (ImapOnlineCopyState)
+ ImapOnlineCopyStateType::kSuccessfulCopy
+ : (ImapOnlineCopyState)
+ ImapOnlineCopyStateType::kFailedCopy;
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->OnlineCopyCompleted(this, copyState);
+ // Don't mark message 'Deleted' for AOL servers or standard imap
+ // servers that support MOVE since we already issued an 'xaol-move'
+ // or 'move' command.
+ if (GetServerStateParser().LastCommandSuccessful() &&
+ (m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
+ !(GetServerStateParser().ServerIsAOLServer() ||
+ GetServerStateParser().GetCapabilityFlag() &
+ kHasMoveCapability)) {
+ // Simulate MOVE for servers that don't support MOVE: do
+ // COPY-DELETE-EXPUNGE.
+ Store(messageIdString, "+FLAGS (\\Deleted \\Seen)",
+ bMessageIdsAreUids);
+ bool storeSuccessful =
+ GetServerStateParser().LastCommandSuccessful();
+ if (storeSuccessful) {
+ if (gExpungeAfterDelete) {
+ // This will expunge all emails marked as deleted in mailbox,
+ // not just the ones marked as deleted above.
+ Expunge();
+ } else {
+ // Check if UIDPLUS capable so we can just expunge emails we
+ // just copied and marked as deleted. This prevents expunging
+ // emails that other clients may have marked as deleted in the
+ // mailbox and don't want them to disappear. Only do
+ // UidExpunge() when user selected delete method is "Move it
+ // to this folder" or "Remove it immediately", not when the
+ // delete method is "Just mark it as deleted".
+ if (!GetShowDeletedMessages() &&
+ (GetServerStateParser().GetCapabilityFlag() &
+ kUidplusCapability)) {
+ UidExpunge(messageIdString);
+ }
+ }
+ }
+ if (m_imapMailFolderSink) {
+ copyState = storeSuccessful
+ ? (ImapOnlineCopyState)
+ ImapOnlineCopyStateType::kSuccessfulDelete
+ : (ImapOnlineCopyState)
+ ImapOnlineCopyStateType::kFailedDelete;
+ m_imapMailFolderSink->OnlineCopyCompleted(this, copyState);
+ }
+ }
+ } else
+ HandleMemoryFailure();
+ } break;
+ case nsIImapUrl::nsImapOnlineToOfflineCopy:
+ case nsIImapUrl::nsImapOnlineToOfflineMove: {
+ nsCString messageIdString;
+ nsresult rv = m_runningUrl->GetListOfMessageIds(messageIdString);
+ if (NS_SUCCEEDED(rv)) {
+ SetProgressString(IMAP_MESSAGES_STRING_INDEX);
+ m_progressCurrentNumber[m_stringIndex] = 0;
+ m_progressExpectedNumber =
+ CountMessagesInIdString(messageIdString.get());
+
+ FetchMessage(messageIdString, kEveryThingRFC822Peek);
+
+ SetProgressString(IMAP_EMPTY_STRING_INDEX);
+ if (m_imapMailFolderSink) {
+ ImapOnlineCopyState copyStatus;
+ copyStatus = GetServerStateParser().LastCommandSuccessful()
+ ? ImapOnlineCopyStateType::kSuccessfulCopy
+ : ImapOnlineCopyStateType::kFailedCopy;
+
+ m_imapMailFolderSink->OnlineCopyCompleted(this, copyStatus);
+ if (GetServerStateParser().LastCommandSuccessful() &&
+ (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineMove)) {
+ Store(messageIdString, "+FLAGS (\\Deleted \\Seen)",
+ bMessageIdsAreUids);
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ copyStatus = ImapOnlineCopyStateType::kSuccessfulDelete;
+ if (gExpungeAfterDelete) Expunge();
+ } else
+ copyStatus = ImapOnlineCopyStateType::kFailedDelete;
+
+ m_imapMailFolderSink->OnlineCopyCompleted(this, copyStatus);
+ }
+ }
+ } else
+ HandleMemoryFailure();
+ } break;
+ default:
+ if (GetServerStateParser().LastCommandSuccessful() && !uidValidityOk)
+ ProcessMailboxUpdate(false); // handle uidvalidity change
+ break;
+ }
+ }
+ } else if (!DeathSignalReceived())
+ HandleMemoryFailure();
+}
+
+nsresult nsImapProtocol::BeginMessageDownLoad(
+ uint32_t total_message_size, // for user, headers and body
+ const char* content_type) {
+ nsresult rv = NS_OK;
+ char* sizeString = PR_smprintf("OPEN Size: %ld", total_message_size);
+ Log("STREAM", sizeString, "Begin Message Download Stream");
+ PR_Free(sizeString);
+ // start counting how many bytes we see in this message after all
+ // transformations
+ m_bytesToChannel = 0;
+
+ if (content_type) {
+ m_fromHeaderSeen = false;
+ if (GetServerStateParser().GetDownloadingHeaders()) {
+ // if we get multiple calls to BeginMessageDownload w/o intervening
+ // calls to NormalEndMessageDownload or Abort, then we're just
+ // going to fake a NormalMessageEndDownload. This will most likely
+ // cause an empty header to get written to the db, and the user
+ // will have to delete the empty header themselves, which
+ // should remove the message from the server as well.
+ if (m_curHdrInfo) NormalMessageEndDownload();
+ if (!m_curHdrInfo) m_curHdrInfo = m_hdrDownloadCache->StartNewHdr();
+ if (m_curHdrInfo) m_curHdrInfo->SetMsgSize(total_message_size);
+ return NS_OK;
+ }
+ // if we have a mock channel, that means we have a channel listener who
+ // wants the message. So set up a pipe. We'll write the message into one end
+ // of the pipe and they will read it out of the other end.
+ if (m_channelListener) {
+ // create a pipe to pump the message into...the output will go to whoever
+ // is consuming the message display
+ // we create an "infinite" pipe in case we get extremely long lines from
+ // the imap server, and the consumer is waiting for a whole line
+ nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1");
+ rv = pipe->Init(false, false, 4096, PR_UINT32_MAX);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // These always succeed because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(
+ pipe->GetInputStream(getter_AddRefs(m_channelInputStream)));
+ MOZ_ALWAYS_SUCCEEDS(
+ pipe->GetOutputStream(getter_AddRefs(m_channelOutputStream)));
+ }
+ // else, if we are saving the message to disk!
+ else if (m_imapMessageSink /* && m_imapAction == nsIImapUrl::nsImapSaveMessageToDisk */)
+ {
+ // we get here when download the inbox for offline use
+ nsCOMPtr<nsIFile> file;
+ bool addDummyEnvelope = true;
+ nsCOMPtr<nsIMsgMessageUrl> msgurl = do_QueryInterface(m_runningUrl);
+ msgurl->GetMessageFile(getter_AddRefs(file));
+ msgurl->GetAddDummyEnvelope(&addDummyEnvelope);
+ if (file)
+ rv = m_imapMessageSink->SetupMsgWriteStream(file, addDummyEnvelope);
+ }
+ if (m_imapMailFolderSink && m_runningUrl) {
+ nsCOMPtr<nsISupports> copyState;
+ if (m_runningUrl) {
+ m_runningUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) // only need this notification during copy
+ {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailurl = do_QueryInterface(m_runningUrl);
+ m_imapMailFolderSink->StartMessage(mailurl);
+ }
+ }
+ }
+
+ } else
+ HandleMemoryFailure();
+ return rv;
+}
+
+void nsImapProtocol::GetShouldDownloadAllHeaders(bool* aResult) {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->GetShouldDownloadAllHeaders(aResult);
+}
+
+void nsImapProtocol::GetArbitraryHeadersToDownload(nsCString& aResult) {
+ if (m_imapServerSink) m_imapServerSink->GetArbitraryHeaders(aResult);
+}
+
+void nsImapProtocol::AdjustChunkSize() {
+ int32_t deltaInSeconds;
+
+ m_endTime = PR_Now();
+ PRTime2Seconds(m_endTime - m_startTime, &deltaInSeconds);
+ m_trackingTime = false;
+ if (deltaInSeconds < 0) return; // bogus for some reason
+
+ if (deltaInSeconds <= m_tooFastTime && m_curFetchSize >= m_chunkSize) {
+ m_chunkSize += m_chunkAddSize;
+ m_chunkThreshold = m_chunkSize + (m_chunkSize / 2);
+ // we used to have a max for the chunk size - I don't think that's needed.
+ } else if (deltaInSeconds <= m_idealTime)
+ return;
+ else {
+ if (m_chunkSize > m_chunkStartSize)
+ m_chunkSize = m_chunkStartSize;
+ else if (m_chunkSize > (m_chunkAddSize * 2))
+ m_chunkSize -= m_chunkAddSize;
+ m_chunkThreshold = m_chunkSize + (m_chunkSize / 2);
+ }
+ // remember these new values globally so new connections
+ // can take advantage of them.
+ if (gChunkSize != m_chunkSize) {
+ // will cause chunk size pref to be written in CloseStream.
+ gChunkSizeDirty = true;
+ gChunkSize = m_chunkSize;
+ gChunkThreshold = m_chunkThreshold;
+ }
+}
+
+// authenticated state commands
+
+// escape any backslashes or quotes. Backslashes are used a lot with our NT
+// server
+void nsImapProtocol::CreateEscapedMailboxName(const char* rawName,
+ nsCString& escapedName) {
+ escapedName.Assign(rawName);
+
+ for (int32_t strIndex = 0; *rawName; strIndex++) {
+ char currentChar = *rawName++;
+ if ((currentChar == '\\') || (currentChar == '\"'))
+ escapedName.Insert('\\', strIndex++);
+ }
+}
+void nsImapProtocol::SelectMailbox(const char* mailboxName) {
+ ProgressEventFunctionUsingNameWithString("imapStatusSelectingMailbox",
+ mailboxName);
+ IncrementCommandTagNumber();
+
+ m_closeNeededBeforeSelect = false; // initial value
+ GetServerStateParser().ResetFlagInfo();
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+ nsCString commandBuffer(GetServerCommandTag());
+ commandBuffer.AppendLiteral(" select \"");
+ commandBuffer.Append(escapedName.get());
+ commandBuffer.Append('"');
+ if (UseCondStore()) commandBuffer.AppendLiteral(" (CONDSTORE)");
+ commandBuffer.Append(CRLF);
+
+ nsresult res;
+ res = SendData(commandBuffer.get());
+ if (NS_FAILED(res)) return;
+ ParseIMAPandCheckForNewMail();
+
+ // Save the folder sink obtained in SetupSinkProxy() for whatever URL just
+ // caused this SELECT. Needed so idle and noop responses are using the correct
+ // folder when detecting changed flags or new messages.
+ m_imapMailFolderSinkSelected = m_imapMailFolderSink;
+ MOZ_ASSERT(m_imapMailFolderSinkSelected);
+ Log("SelectMailbox", nullptr, "got m_imapMailFolderSinkSelected");
+
+ int32_t numOfMessagesInFlagState = 0;
+ nsImapAction imapAction;
+ m_flagState->GetNumberOfMessages(&numOfMessagesInFlagState);
+ res = m_runningUrl->GetImapAction(&imapAction);
+ // if we've selected a mailbox, and we're not going to do an update because of
+ // the url type, but don't have the flags, go get them!
+ if (GetServerStateParser().LastCommandSuccessful() && NS_SUCCEEDED(res) &&
+ imapAction != nsIImapUrl::nsImapSelectFolder &&
+ imapAction != nsIImapUrl::nsImapExpungeFolder &&
+ imapAction != nsIImapUrl::nsImapLiteSelectFolder &&
+ imapAction != nsIImapUrl::nsImapDeleteAllMsgs &&
+ ((GetServerStateParser().NumberOfMessages() !=
+ numOfMessagesInFlagState) &&
+ (numOfMessagesInFlagState == 0))) {
+ ProcessMailboxUpdate(false);
+ }
+}
+
+void nsImapProtocol::FetchMsgAttribute(const nsCString& messageIds,
+ const nsCString& attribute) {
+ IncrementCommandTagNumber();
+
+ nsAutoCString commandString(GetServerCommandTag());
+ commandString.AppendLiteral(" UID fetch ");
+ commandString.Append(messageIds);
+ commandString.AppendLiteral(" (");
+ commandString.Append(attribute);
+ commandString.AppendLiteral(")" CRLF);
+ nsresult rv = SendData(commandString.get());
+
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(commandString.get());
+ GetServerStateParser().SetFetchingFlags(false);
+ // Always clear this flag after every fetch.
+ m_fetchingWholeMessage = false;
+}
+
+// this routine is used to fetch a message or messages, or headers for a
+// message...
+
+void nsImapProtocol::FallbackToFetchWholeMsg(const nsCString& messageId,
+ uint32_t messageSize) {
+ if (m_imapMessageSink && m_runningUrl) {
+ bool shouldStoreMsgOffline;
+ m_runningUrl->GetStoreOfflineOnFallback(&shouldStoreMsgOffline);
+ m_runningUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
+ }
+ FetchTryChunking(messageId,
+ m_imapAction == nsIImapUrl::nsImapMsgFetchPeek
+ ? kEveryThingRFC822Peek
+ : kEveryThingRFC822,
+ true, nullptr, messageSize, true);
+}
+
+void nsImapProtocol::FetchMessage(const nsCString& messageIds,
+ nsIMAPeFetchFields whatToFetch,
+ const char* fetchModifier, uint32_t startByte,
+ uint32_t numBytes, char* part) {
+ IncrementCommandTagNumber();
+
+ nsCString commandString;
+ commandString = "%s UID fetch";
+
+ switch (whatToFetch) {
+ case kEveryThingRFC822:
+ m_flagChangeCount++;
+ m_fetchingWholeMessage = true;
+ if (m_trackingTime) AdjustChunkSize(); // we started another segment
+ m_startTime = PR_Now(); // save start of download time
+ m_trackingTime = true;
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("FetchMessage everything: curFetchSize %u numBytes %u",
+ m_curFetchSize, numBytes));
+ if (numBytes > 0) m_curFetchSize = numBytes;
+
+ if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) {
+ if (GetServerStateParser().GetCapabilityFlag() & kHasXSenderCapability)
+ commandString.AppendLiteral(" %s (XSENDER UID RFC822.SIZE BODY[]");
+ else
+ commandString.AppendLiteral(" %s (UID RFC822.SIZE BODY[]");
+ } else {
+ if (GetServerStateParser().GetCapabilityFlag() & kHasXSenderCapability)
+ commandString.AppendLiteral(" %s (XSENDER UID RFC822.SIZE RFC822");
+ else
+ commandString.AppendLiteral(" %s (UID RFC822.SIZE RFC822");
+ }
+ if (numBytes > 0) {
+ // if we are retrieving chunks
+ char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes);
+ if (byterangeString) {
+ commandString.Append(byterangeString);
+ PR_Free(byterangeString);
+ }
+ }
+ commandString.Append(')');
+
+ break;
+
+ case kEveryThingRFC822Peek: {
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("FetchMessage peek: curFetchSize %u numBytes %u", m_curFetchSize,
+ numBytes));
+ if (numBytes > 0) m_curFetchSize = numBytes;
+ const char* formatString = "";
+ eIMAPCapabilityFlags server_capabilityFlags =
+ GetServerStateParser().GetCapabilityFlag();
+
+ m_fetchingWholeMessage = true;
+ if (server_capabilityFlags & kIMAP4rev1Capability) {
+ // use body[].peek since rfc822.peek is not in IMAP4rev1
+ if (server_capabilityFlags & kHasXSenderCapability)
+ formatString = " %s (XSENDER UID RFC822.SIZE BODY.PEEK[]";
+ else
+ formatString = " %s (UID RFC822.SIZE BODY.PEEK[]";
+ } else {
+ if (server_capabilityFlags & kHasXSenderCapability)
+ formatString = " %s (XSENDER UID RFC822.SIZE RFC822.peek";
+ else
+ formatString = " %s (UID RFC822.SIZE RFC822.peek";
+ }
+
+ commandString.Append(formatString);
+ if (numBytes > 0) {
+ // if we are retrieving chunks
+ char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes);
+ if (byterangeString) {
+ commandString.Append(byterangeString);
+ PR_Free(byterangeString);
+ }
+ }
+ commandString.Append(')');
+ } break;
+ case kHeadersRFC822andUid:
+ if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) {
+ eIMAPCapabilityFlags server_capabilityFlags =
+ GetServerStateParser().GetCapabilityFlag();
+ bool aolImapServer =
+ ((server_capabilityFlags & kAOLImapCapability) != 0);
+ bool downloadAllHeaders = false;
+ // checks if we're filtering on "any header" or running a spam filter
+ // requiring all headers
+ GetShouldDownloadAllHeaders(&downloadAllHeaders);
+
+ if (!downloadAllHeaders) // if it's ok -- no filters on any header,
+ // etc.
+ {
+ char* headersToDL = nullptr;
+ char* what = nullptr;
+ const char* dbHeaders =
+ (gUseEnvelopeCmd) ? IMAP_DB_HEADERS : IMAP_ENV_AND_DB_HEADERS;
+ nsCString arbitraryHeaders;
+ GetArbitraryHeadersToDownload(arbitraryHeaders);
+ for (uint32_t i = 0; i < mCustomDBHeaders.Length(); i++) {
+ if (!FindInReadable(mCustomDBHeaders[i], arbitraryHeaders,
+ nsCaseInsensitiveCStringComparator)) {
+ if (!arbitraryHeaders.IsEmpty()) arbitraryHeaders.Append(' ');
+ arbitraryHeaders.Append(mCustomDBHeaders[i]);
+ }
+ }
+ for (uint32_t i = 0; i < mCustomHeaders.Length(); i++) {
+ if (!FindInReadable(mCustomHeaders[i], arbitraryHeaders,
+ nsCaseInsensitiveCStringComparator)) {
+ if (!arbitraryHeaders.IsEmpty()) arbitraryHeaders.Append(' ');
+ arbitraryHeaders.Append(mCustomHeaders[i]);
+ }
+ }
+ if (arbitraryHeaders.IsEmpty())
+ headersToDL = strdup(dbHeaders);
+ else
+ headersToDL =
+ PR_smprintf("%s %s", dbHeaders, arbitraryHeaders.get());
+
+ if (gUseEnvelopeCmd)
+ what = PR_smprintf(" ENVELOPE BODY.PEEK[HEADER.FIELDS (%s)])",
+ headersToDL);
+ else
+ what = PR_smprintf(" BODY.PEEK[HEADER.FIELDS (%s)])", headersToDL);
+ free(headersToDL);
+ if (what) {
+ commandString.AppendLiteral(" %s (UID ");
+ if (m_isGmailServer)
+ commandString.AppendLiteral("X-GM-MSGID X-GM-THRID X-GM-LABELS ");
+ if (aolImapServer)
+ commandString.AppendLiteral(" XAOL.SIZE");
+ else
+ commandString.AppendLiteral("RFC822.SIZE");
+ commandString.AppendLiteral(" FLAGS");
+ commandString.Append(what);
+ PR_Free(what);
+ } else {
+ commandString.AppendLiteral(
+ " %s (UID RFC822.SIZE BODY.PEEK[HEADER] FLAGS)");
+ }
+ } else
+ commandString.AppendLiteral(
+ " %s (UID RFC822.SIZE BODY.PEEK[HEADER] FLAGS)");
+ } else
+ commandString.AppendLiteral(
+ " %s (UID RFC822.SIZE RFC822.HEADER FLAGS)");
+ break;
+ case kUid:
+ commandString.AppendLiteral(" %s (UID)");
+ break;
+ case kFlags:
+ GetServerStateParser().SetFetchingFlags(true);
+ commandString.AppendLiteral(" %s (FLAGS)");
+ break;
+ case kRFC822Size:
+ commandString.AppendLiteral(" %s (RFC822.SIZE)");
+ break;
+ case kBodyStart: {
+ int32_t numBytesToFetch;
+ m_runningUrl->GetNumBytesToFetch(&numBytesToFetch);
+
+ commandString.AppendLiteral(
+ " %s (UID BODY.PEEK[HEADER.FIELDS (Content-Type "
+ "Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.");
+ commandString.AppendInt(numBytesToFetch);
+ commandString.AppendLiteral(">)");
+ } break;
+ case kRFC822HeadersOnly:
+ if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) {
+ if (part) {
+ commandString.AppendLiteral(" %s (BODY[");
+ char* what = PR_smprintf("%s.HEADER])", part);
+ if (what) {
+ commandString.Append(what);
+ PR_Free(what);
+ } else
+ HandleMemoryFailure();
+ } else {
+ // headers for the top-level message
+ commandString.AppendLiteral(" %s (BODY[HEADER])");
+ }
+ } else
+ commandString.AppendLiteral(" %s (RFC822.HEADER)");
+ break;
+ case kMIMEPart:
+ commandString.AppendLiteral(" %s (BODY.PEEK[%s]");
+ if (numBytes > 0) {
+ // if we are retrieving chunks
+ char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes);
+ if (byterangeString) {
+ commandString.Append(byterangeString);
+ PR_Free(byterangeString);
+ }
+ }
+ commandString.Append(')');
+ break;
+ case kMIMEHeader:
+ commandString.AppendLiteral(" %s (BODY[%s.MIME])");
+ break;
+ }
+
+ if (fetchModifier) commandString.Append(fetchModifier);
+
+ commandString.Append(CRLF);
+
+ // since messageIds can be infinitely long, use a dynamic buffer rather than
+ // the fixed one
+ const char* commandTag = GetServerCommandTag();
+ int protocolStringSize = commandString.Length() + messageIds.Length() +
+ PL_strlen(commandTag) + 1 +
+ (part ? PL_strlen(part) : 0);
+ char* protocolString = (char*)PR_CALLOC(protocolStringSize);
+
+ if (protocolString) {
+ char* cCommandStr = ToNewCString(commandString);
+ if ((whatToFetch == kMIMEPart) || (whatToFetch == kMIMEHeader)) {
+ PR_snprintf(protocolString, // string to create
+ protocolStringSize, // max size
+ cCommandStr, // format string
+ commandTag, // command tag
+ messageIds.get(), part);
+ } else {
+ PR_snprintf(protocolString, // string to create
+ protocolStringSize, // max size
+ cCommandStr, // format string
+ commandTag, // command tag
+ messageIds.get());
+ }
+
+ nsresult rv = SendData(protocolString);
+
+ free(cCommandStr);
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString);
+ PR_Free(protocolString);
+ GetServerStateParser().SetFetchingFlags(false);
+ // Always clear this flag after every fetch.
+ m_fetchingWholeMessage = false;
+ if (GetServerStateParser().LastCommandSuccessful() && CheckNeeded())
+ Check();
+ } else
+ HandleMemoryFailure();
+}
+
+void nsImapProtocol::FetchTryChunking(const nsCString& messageIds,
+ nsIMAPeFetchFields whatToFetch,
+ bool idIsUid, char* part,
+ uint32_t downloadSize, bool tryChunking) {
+ GetServerStateParser().SetTotalDownloadSize(downloadSize);
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("FetchTryChunking: curFetchSize %u", downloadSize));
+ MOZ_ASSERT(!part, "fetching a part should no longer occur");
+ m_curFetchSize = downloadSize; // we'll change this if chunking.
+ if (m_fetchByChunks && tryChunking &&
+ GetServerStateParser().ServerHasIMAP4Rev1Capability() &&
+ (downloadSize > (uint32_t)m_chunkThreshold)) {
+ uint32_t startByte = 0;
+ m_curFetchSize = m_chunkSize;
+ GetServerStateParser().ClearLastFetchChunkReceived();
+ while (!DeathSignalReceived() && !GetPseudoInterrupted() &&
+ !GetServerStateParser().GetLastFetchChunkReceived() &&
+ GetServerStateParser().ContinueParse()) {
+ GetServerStateParser().ClearNumBytesFetched();
+ // This chunk is a fetch of m_chunkSize bytes. But m_chunkSize can be
+ // changed inside FetchMessage(). Save the original value of m_chunkSize
+ // to set the correct offset (startByte) for the next chunk.
+ int32_t bytesFetched = m_chunkSize;
+ FetchMessage(messageIds, whatToFetch, nullptr, startByte, bytesFetched,
+ part);
+ if (!GetServerStateParser().GetNumBytesFetched()) {
+ // Fetch returned zero bytes chunk from server. This occurs if the
+ // message was expunged during the fetch.
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("FetchTryChunking: Zero bytes chunk fetched; message probably "
+ "expunged"));
+ break;
+ }
+ startByte += bytesFetched;
+ }
+
+ // Only abort the stream if this is a normal message download
+ // Otherwise, let the body shell abort the stream.
+ if ((whatToFetch == kEveryThingRFC822) &&
+ ((startByte > 0 && (startByte < downloadSize) &&
+ (DeathSignalReceived() || GetPseudoInterrupted())) ||
+ !GetServerStateParser().ContinueParse())) {
+ AbortMessageDownLoad();
+ PseudoInterrupt(false);
+ }
+ } else {
+ // small message, or (we're not chunking and not doing bodystructure),
+ // or the server is not rev1.
+ // Just fetch the whole thing.
+ FetchMessage(messageIds, whatToFetch, nullptr, 0, 0, part);
+ }
+}
+
+void nsImapProtocol::PostLineDownLoadEvent(const char* line,
+ uint32_t uidOfMessage) {
+ if (!GetServerStateParser().GetDownloadingHeaders()) {
+ uint32_t byteCount = PL_strlen(line);
+ bool echoLineToMessageSink = false;
+ // if we have a channel listener, then just spool the message
+ // directly to the listener
+ if (m_channelListener) {
+ uint32_t count = 0;
+ if (m_channelOutputStream) {
+ nsresult rv = m_channelOutputStream->Write(line, byteCount, &count);
+ NS_ASSERTION(count == byteCount,
+ "IMAP channel pipe couldn't buffer entire write");
+ if (NS_SUCCEEDED(rv)) {
+ m_channelListener->OnDataAvailable(m_mockChannel,
+ m_channelInputStream, 0, count);
+ }
+ // else some sort of explosion?
+ }
+ }
+ if (m_runningUrl)
+ m_runningUrl->GetStoreResultsOffline(&echoLineToMessageSink);
+
+ m_bytesToChannel += byteCount;
+ if (m_imapMessageSink && line && echoLineToMessageSink &&
+ !GetPseudoInterrupted())
+ m_imapMessageSink->ParseAdoptedMsgLine(line, uidOfMessage, m_runningUrl);
+ }
+ // ***** We need to handle the pseudo interrupt here *****
+}
+
+// Handle a line seen by the parser.
+// * The argument |lineCopy| must be nullptr or should contain the same string
+// as |line|. |lineCopy| will be modified.
+// * A line may be passed by parts, e.g., "part1 part2\r\n" may be passed as
+// HandleMessageDownLoadLine("part 1 ", 1);
+// HandleMessageDownLoadLine("part 2\r\n", 0);
+// However, it is assumed that a CRLF or a CRCRLF is never split (i.e., this
+// is ensured *before* invoking this method).
+void nsImapProtocol::HandleMessageDownLoadLine(const char* line,
+ bool isPartialLine,
+ char* lineCopy) {
+ NS_ENSURE_TRUE_VOID(line);
+ NS_ASSERTION(lineCopy == nullptr || !PL_strcmp(line, lineCopy),
+ "line and lineCopy must contain the same string");
+ const char* messageLine = line;
+ uint32_t lineLength = strlen(messageLine);
+ const char* cEndOfLine = messageLine + lineLength;
+ char* localMessageLine = nullptr;
+
+ // If we obtain a partial line (due to fetching by chunks), we do not
+ // add/modify the end-of-line terminator.
+ if (!isPartialLine) {
+ // Change this line to native line termination, duplicate if necessary.
+ // Do not assume that the line really ends in CRLF
+ // to start with, even though it is supposed to be RFC822
+
+ // normalize line endings to CRLF unless we are saving the message to disk
+ bool canonicalLineEnding = true;
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(m_runningUrl);
+
+ if (m_imapAction == nsIImapUrl::nsImapSaveMessageToDisk && msgUrl)
+ msgUrl->GetCanonicalLineEnding(&canonicalLineEnding);
+
+ NS_ASSERTION(MSG_LINEBREAK_LEN == 1 || (MSG_LINEBREAK_LEN == 2 &&
+ !PL_strcmp(CRLF, MSG_LINEBREAK)),
+ "violated assumptions on MSG_LINEBREAK");
+ if (MSG_LINEBREAK_LEN == 1 && !canonicalLineEnding) {
+ bool lineEndsWithCRorLF =
+ lineLength >= 1 && (cEndOfLine[-1] == '\r' || cEndOfLine[-1] == '\n');
+ char* endOfLine;
+ if (lineCopy && lineEndsWithCRorLF) // true for most lines
+ {
+ endOfLine = lineCopy + lineLength;
+ messageLine = lineCopy;
+ } else {
+ // leave enough room for one more char, MSG_LINEBREAK[0]
+ localMessageLine = (char*)PR_MALLOC(lineLength + 2);
+ if (!localMessageLine) // memory failure
+ return;
+ PL_strcpy(localMessageLine, line);
+ endOfLine = localMessageLine + lineLength;
+ messageLine = localMessageLine;
+ }
+
+ if (lineLength >= 2 && endOfLine[-2] == '\r' && endOfLine[-1] == '\n') {
+ if (lineLength >= 3 && endOfLine[-3] == '\r') // CRCRLF
+ {
+ endOfLine--;
+ lineLength--;
+ }
+ /* CRLF -> CR or LF */
+ endOfLine[-2] = MSG_LINEBREAK[0];
+ endOfLine[-1] = '\0';
+ lineLength--;
+ } else if (lineLength >= 1 &&
+ ((endOfLine[-1] == '\r') || (endOfLine[-1] == '\n'))) {
+ /* CR -> LF or LF -> CR */
+ endOfLine[-1] = MSG_LINEBREAK[0];
+ } else // no eol characters at all
+ {
+ endOfLine[0] = MSG_LINEBREAK[0]; // CR or LF
+ endOfLine[1] = '\0';
+ lineLength++;
+ }
+ } else // enforce canonical CRLF linebreaks
+ {
+ if (lineLength == 0 || (lineLength == 1 && cEndOfLine[-1] == '\n')) {
+ messageLine = CRLF;
+ lineLength = 2;
+ } else if (cEndOfLine[-1] != '\n' || cEndOfLine[-2] != '\r' ||
+ (lineLength >= 3 && cEndOfLine[-3] == '\r')) {
+ // The line does not end in CRLF (or it ends in CRCRLF).
+ // Copy line and leave enough room for two more chars (CR and LF).
+ localMessageLine = (char*)PR_MALLOC(lineLength + 3);
+ if (!localMessageLine) // memory failure
+ return;
+ PL_strcpy(localMessageLine, line);
+ char* endOfLine = localMessageLine + lineLength;
+ messageLine = localMessageLine;
+
+ if (lineLength >= 3 && endOfLine[-1] == '\n' && endOfLine[-2] == '\r') {
+ // CRCRLF -> CRLF
+ endOfLine[-2] = '\n';
+ endOfLine[-1] = '\0';
+ lineLength--;
+ } else if ((endOfLine[-1] == '\r') || (endOfLine[-1] == '\n')) {
+ // LF -> CRLF or CR -> CRLF
+ endOfLine[-1] = '\r';
+ endOfLine[0] = '\n';
+ endOfLine[1] = '\0';
+ lineLength++;
+ } else // no eol characters at all
+ {
+ endOfLine[0] = '\r';
+ endOfLine[1] = '\n';
+ endOfLine[2] = '\0';
+ lineLength += 2;
+ }
+ }
+ }
+ }
+ NS_ASSERTION(lineLength == PL_strlen(messageLine), "lineLength not accurate");
+
+ // check if sender obtained via XSENDER server extension matches "From:" field
+ const char* xSenderInfo = GetServerStateParser().GetXSenderInfo();
+ if (xSenderInfo && *xSenderInfo && !m_fromHeaderSeen) {
+ if (!PL_strncmp("From: ", messageLine, 6)) {
+ m_fromHeaderSeen = true;
+ if (PL_strstr(messageLine, xSenderInfo) != NULL)
+ // Adding a X-Mozilla-Status line here is not very elegant but it
+ // works. Another X-Mozilla-Status line is added to the message when
+ // downloading to a local folder; this new line will also contain the
+ // 'authed' flag we are adding here. (If the message is again
+ // uploaded to the server, this flag is lost.)
+ // 0x0200 == nsMsgMessageFlags::SenderAuthed
+ HandleMessageDownLoadLine("X-Mozilla-Status: 0200\r\n", false);
+ GetServerStateParser().FreeXSenderInfo();
+ }
+ }
+
+ if (GetServerStateParser().GetDownloadingHeaders()) {
+ if (!m_curHdrInfo)
+ BeginMessageDownLoad(GetServerStateParser().SizeOfMostRecentMessage(),
+ MESSAGE_RFC822);
+ if (m_curHdrInfo) {
+ if (NS_FAILED(m_curHdrInfo->CacheLine(
+ messageLine, GetServerStateParser().CurrentResponseUID())))
+ NS_ERROR("CacheLine for a header failed");
+ }
+ PR_Free(localMessageLine);
+ return;
+ }
+ // if this line is for a different message, or the incoming line is too big
+ if (((m_downloadLineCache->CurrentUID() !=
+ GetServerStateParser().CurrentResponseUID()) &&
+ !m_downloadLineCache->CacheEmpty()) ||
+ (m_downloadLineCache->SpaceAvailable() < lineLength + 1))
+ FlushDownloadCache();
+
+ // so now the cache is flushed, but this string might still be too big
+ if (m_downloadLineCache->SpaceAvailable() < lineLength + 1)
+ PostLineDownLoadEvent(messageLine,
+ GetServerStateParser().CurrentResponseUID());
+ else {
+ NS_ASSERTION(
+ (PL_strlen(messageLine) + 1) <= m_downloadLineCache->SpaceAvailable(),
+ "Oops... line length greater than space available");
+ if (NS_FAILED(m_downloadLineCache->CacheLine(
+ messageLine, GetServerStateParser().CurrentResponseUID())))
+ NS_ERROR("CacheLine for message body failed");
+ }
+ PR_Free(localMessageLine);
+}
+
+void nsImapProtocol::FlushDownloadCache() {
+ if (!m_downloadLineCache->CacheEmpty()) {
+ msg_line_info* downloadLine = m_downloadLineCache->GetCurrentLineInfo();
+ PostLineDownLoadEvent(downloadLine->adoptedMessageLine,
+ downloadLine->uidOfMessage);
+ m_downloadLineCache->ResetCache();
+ }
+}
+
+void nsImapProtocol::NormalMessageEndDownload() {
+ Log("STREAM", "CLOSE", "Normal Message End Download Stream");
+
+ if (m_trackingTime) AdjustChunkSize();
+ if (m_imapMailFolderSink && m_curHdrInfo &&
+ GetServerStateParser().GetDownloadingHeaders()) {
+ m_curHdrInfo->SetMsgSize(GetServerStateParser().SizeOfMostRecentMessage());
+ m_curHdrInfo->SetMsgUid(GetServerStateParser().CurrentResponseUID());
+ m_hdrDownloadCache->FinishCurrentHdr();
+ int32_t numHdrsCached;
+ m_hdrDownloadCache->GetNumHeaders(&numHdrsCached);
+ if (numHdrsCached == kNumHdrsToXfer) {
+ m_imapMailFolderSink->ParseMsgHdrs(this, m_hdrDownloadCache);
+ m_hdrDownloadCache->ResetAll();
+ }
+ }
+ FlushDownloadCache();
+
+ if (!GetServerStateParser().GetDownloadingHeaders()) {
+ int32_t updatedMessageSize = -1;
+ if (m_fetchingWholeMessage) {
+ updatedMessageSize = m_bytesToChannel;
+ if (m_bytesToChannel !=
+ GetServerStateParser().SizeOfMostRecentMessage()) {
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("STREAM:CLOSE Server's RFC822.SIZE %u, actual size %u",
+ GetServerStateParser().SizeOfMostRecentMessage(),
+ m_bytesToChannel));
+ }
+ }
+ // need to know if we're downloading for display or not. We'll use action ==
+ // nsImapMsgFetch for now
+ nsImapAction imapAction =
+ nsIImapUrl::nsImapSelectFolder; // just set it to some legal value
+ if (m_runningUrl) m_runningUrl->GetImapAction(&imapAction);
+
+ if (m_imapMessageSink) {
+ if (m_mockChannel) {
+ // Have a mock channel, tell channel that write to cache is done.
+ m_mockChannel->SetWritingToCache(false);
+ MOZ_LOG(IMAP, LogLevel::Debug, ("%s: End cache write", __func__));
+ }
+ m_imapMessageSink->NormalEndMsgWriteStream(
+ m_downloadLineCache->CurrentUID(),
+ imapAction == nsIImapUrl::nsImapMsgFetch, m_runningUrl,
+ updatedMessageSize);
+ }
+
+ if (m_runningUrl && m_imapMailFolderSink) {
+ nsCOMPtr<nsISupports> copyState;
+ m_runningUrl->GetCopyState(getter_AddRefs(copyState));
+ if (copyState) // only need this notification during copy
+ {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl(do_QueryInterface(m_runningUrl));
+ m_imapMailFolderSink->EndMessage(mailUrl,
+ m_downloadLineCache->CurrentUID());
+ }
+ }
+ }
+ m_curHdrInfo = nullptr;
+}
+
+void nsImapProtocol::AbortMessageDownLoad() {
+ Log("STREAM", "CLOSE", "Abort Message Download Stream");
+
+ if (m_trackingTime) AdjustChunkSize();
+ FlushDownloadCache();
+ if (GetServerStateParser().GetDownloadingHeaders()) {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->AbortHeaderParseStream(this);
+ } else if (m_imapMessageSink)
+ m_imapMessageSink->AbortMsgWriteStream();
+
+ m_curHdrInfo = nullptr;
+}
+
+void nsImapProtocol::ProcessMailboxUpdate(bool handlePossibleUndo) {
+ if (DeathSignalReceived()) return;
+
+ // Update quota information
+ char* boxName;
+ GetSelectedMailboxName(&boxName);
+ GetQuotaDataIfSupported(boxName);
+ PR_Free(boxName);
+
+ // fetch the flags and uids of all existing messages or new ones
+ if (!DeathSignalReceived() && GetServerStateParser().NumberOfMessages()) {
+ if (handlePossibleUndo) {
+ // undo any delete flags we may have asked to
+ nsCString undoIdsStr;
+ nsAutoCString undoIds;
+
+ GetCurrentUrl()->GetListOfMessageIds(undoIdsStr);
+ undoIds.Assign(undoIdsStr);
+ if (!undoIds.IsEmpty()) {
+ char firstChar = (char)undoIds.CharAt(0);
+ undoIds.Cut(0, 1); // remove first character
+ // if this string started with a '-', then this is an undo of a delete
+ // if its a '+' its a redo
+ if (firstChar == '-')
+ Store(undoIds, "-FLAGS (\\Deleted)",
+ true); // most servers will fail silently on a failure, deal
+ // with it?
+ else if (firstChar == '+')
+ Store(undoIds, "+FLAGS (\\Deleted)",
+ true); // most servers will fail silently on a failure, deal
+ // with it?
+ else
+ NS_ASSERTION(false, "bogus undo Id's");
+ }
+ }
+
+ // make the parser record these flags
+ nsCString fetchStr;
+ int32_t added = 0, deleted = 0;
+
+ m_flagState->GetNumberOfMessages(&added);
+ deleted = m_flagState->NumberOfDeletedMessages();
+ bool flagStateEmpty = !added;
+ bool useCS = UseCondStore();
+
+ // Figure out if we need to do a full sync (UID Fetch Flags 1:*),
+ // a partial sync using CHANGEDSINCE, or a sync from the previous
+ // highwater mark.
+
+ // If the folder doesn't know about the highest uid, or the flag state
+ // is empty, and we're not using CondStore, we definitely need a full sync.
+ //
+ // Print to log items affecting needFullFolderSync:
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Do full sync?: mFolderHighestUID=%" PRIu32 ", added=%" PRId32
+ ", useCS=%s",
+ mFolderHighestUID, added, useCS ? "true" : "false"));
+ bool needFullFolderSync = !mFolderHighestUID || (flagStateEmpty && !useCS);
+ bool needFolderSync = false;
+
+ if (!needFullFolderSync) {
+ // Figure out if we need to do a non-highwater mark sync.
+ // Set needFolderSync true when at least 1 of these 3 cases is true:
+ // 1. Have no uids in flag array or all flag elements are marked deleted
+ // AND not using CONDSTORE.
+ // 2. Have no uids in flag array or all flag elements are marked deleted
+ // AND using "just mark as deleted" and EXISTS response count differs from
+ // stored message count for folder.
+ // 3. Using CONDSTORE and highest MODSEQ response is not equal to stored
+ // mod seq for folder.
+
+ // Print to log items affecting needFolderSync:
+ // clang-format off
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("1. Do a sync?: added=%" PRId32 ", deleted=%" PRId32 ", useCS=%s",
+ added, deleted, useCS ? "true" : "false"));
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("2. Do a sync?: ShowDeletedMsgs=%s, exists=%" PRId32
+ ", mFolderTotalMsgCount=%" PRId32,
+ GetShowDeletedMessages() ? "true" : "false",
+ GetServerStateParser().NumberOfMessages(), mFolderTotalMsgCount));
+ // clang-format on
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("3. Do a sync?: fHighestModSeq=%" PRIu64
+ ", mFolderLastModSeq=%" PRIu64,
+ GetServerStateParser().fHighestModSeq, mFolderLastModSeq));
+
+ needFolderSync =
+ ((flagStateEmpty || added == deleted) &&
+ (!useCS || (GetShowDeletedMessages() &&
+ GetServerStateParser().NumberOfMessages() !=
+ mFolderTotalMsgCount))) ||
+ (useCS && GetServerStateParser().fHighestModSeq != mFolderLastModSeq);
+ }
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("needFullFolderSync=%s, needFolderSync=%s",
+ needFullFolderSync ? "true" : "false",
+ needFolderSync ? "true" : "false"));
+
+ if (needFullFolderSync || needFolderSync) {
+ nsCString idsToFetch("1:*");
+ char fetchModifier[40] = "";
+ if (!needFullFolderSync && !GetShowDeletedMessages() && useCS) {
+ m_flagState->StartCapture();
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Doing UID fetch 1:* (CHANGEDSINCE %" PRIu64 ")",
+ mFolderLastModSeq));
+ PR_snprintf(fetchModifier, sizeof(fetchModifier),
+ " (CHANGEDSINCE %llu)", mFolderLastModSeq);
+ } else
+ m_flagState->SetPartialUIDFetch(false);
+
+ FetchMessage(idsToFetch, kFlags, fetchModifier);
+ // lets see if we should expunge during a full sync of flags.
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ // if we did a CHANGEDSINCE fetch, do a sanity check on the msg counts
+ // to see if some other client may have done an expunge.
+ if (m_flagState->GetPartialUIDFetch()) {
+ uint32_t numExists = GetServerStateParser().NumberOfMessages();
+ uint32_t numPrevExists = mFolderTotalMsgCount;
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Sanity, deleted=%" PRId32 ", numPrevExists=%" PRIu32
+ ", numExists=%" PRIu32,
+ m_flagState->NumberOfDeletedMessages(), numPrevExists,
+ numExists));
+ // Determine the number of new UIDs just fetched that are greater than
+ // the saved highest UID for the folder. numToCheck will contain the
+ // number of UIDs just fetched and, of course, not all are new.
+ uint32_t numNewUIDs = 0;
+ uint32_t numToCheck = m_flagState->GetNumAdded();
+ bool flagChangeDetected = false;
+ bool expungeHappened = false;
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("numToCheck=%" PRIu32, numToCheck));
+ if (numToCheck && mFolderHighestUID) {
+ uint32_t uid;
+ int32_t topIndex;
+ m_flagState->GetNumberOfMessages(&topIndex);
+ MOZ_LOG(
+ IMAP_CS, LogLevel::Debug,
+ ("Partial fetching. Number of UIDs stored=%" PRId32, topIndex));
+ do {
+ topIndex--;
+ // Check for potential infinite loop here. This has happened but
+ // don't know why. If topIndex is negative at this point, set
+ // expungeHappened true to recover by doing a full flag fetch.
+ if (topIndex < 0) {
+ expungeHappened = true;
+ MOZ_LOG(IMAP_CS, LogLevel::Error,
+ ("Zero or negative number of UIDs stored, do full flag "
+ "fetch"));
+ break;
+ }
+ m_flagState->GetUidOfMessage(topIndex, &uid);
+ if (uid && uid != nsMsgKey_None) {
+ if (uid > mFolderHighestUID) {
+ numNewUIDs++;
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("numNewUIDs=%" PRIu32 ", Added new UID=%" PRIu32,
+ numNewUIDs, uid));
+ numToCheck--;
+ } else {
+ // Just a flag change on an existing UID. No more new UIDs
+ // will be found. This does not detect an expunged message.
+ flagChangeDetected = true;
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Not new uid=%" PRIu32, uid));
+ break;
+ }
+ } else {
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("UID is 0 or a gap, uid=0x%" PRIx32, uid));
+ break;
+ }
+ } while (numToCheck);
+ }
+
+ // Another client expunged at least one message if the number of new
+ // UIDs is not equal to the observed change in the number of messages
+ // existing in the folder.
+ expungeHappened =
+ expungeHappened || numNewUIDs != (numExists - numPrevExists);
+ if (expungeHappened) {
+ // Sanity check failed - need full fetch to remove expunged msgs.
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Other client expunged msgs, do full fetch to remove "
+ "expunged msgs"));
+ m_flagState->Reset();
+ m_flagState->SetPartialUIDFetch(false);
+ FetchMessage("1:*"_ns, kFlags);
+ } else if (numNewUIDs == 0) {
+ // Nothing has been expunged and no new UIDs, so if just a flag
+ // change on existing message(s), avoid unneeded fetch of flags for
+ // messages with UIDs at and above uid (see var uid above) when
+ // "highwater mark" fetch occurs below.
+ if (mFolderHighestUID && flagChangeDetected) {
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Avoid unneeded fetches after just flag changes"));
+ GetServerStateParser().ResetHighestRecordedUID();
+ }
+ }
+ }
+ int32_t numDeleted = m_flagState->NumberOfDeletedMessages();
+ // Don't do expunge when we are lite selecting folder (because we
+ // could be doing undo) or if gExpungeOption is kAutoExpungeNever.
+ // Expunge if we're always expunging, or the number of deleted messages
+ // is over the threshold, and we're either always respecting the
+ // threshold, or we're expunging based on the delete model, and the
+ // delete model is not "just mark it as deleted" (imap delete model).
+ if (m_imapAction != nsIImapUrl::nsImapLiteSelectFolder &&
+ gExpungeOption != kAutoExpungeNever &&
+ (gExpungeOption == kAutoExpungeAlways ||
+ (numDeleted >= gExpungeThreshold &&
+ (gExpungeOption == kAutoExpungeOnThreshold ||
+ (gExpungeOption == kAutoExpungeDeleteModel &&
+ !GetShowDeletedMessages())))))
+ Expunge();
+ }
+ } else {
+ // Obtain the highest (highwater mark) UID seen since the last UIDVALIDITY
+ // response occurred (associated with the most recent SELECT for the
+ // folder).
+ uint32_t highestRecordedUID = GetServerStateParser().HighestRecordedUID();
+ // if we're using CONDSTORE, and the parser hasn't seen any UIDs, use
+ // the highest UID previously seen and saved for the folder instead.
+ if (useCS && !highestRecordedUID) highestRecordedUID = mFolderHighestUID;
+ // clang-format off
+ MOZ_LOG(IMAP_CS, LogLevel::Debug,
+ ("Check for new messages above UID=%" PRIu32, highestRecordedUID));
+ // clang-format on
+ AppendUid(fetchStr, highestRecordedUID + 1);
+ fetchStr.AppendLiteral(":*");
+ FetchMessage(fetchStr, kFlags); // only new messages please
+ }
+ } else if (GetServerStateParser().LastCommandSuccessful()) {
+ GetServerStateParser().ResetFlagInfo();
+ // the flag state is empty, but not partial.
+ m_flagState->SetPartialUIDFetch(false);
+ }
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsImapAction imapAction;
+ nsresult res = m_runningUrl->GetImapAction(&imapAction);
+ if (NS_SUCCEEDED(res) && imapAction == nsIImapUrl::nsImapLiteSelectFolder)
+ return;
+ }
+
+ nsTArray<nsMsgKey> msgIdList;
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ ReentrantMonitorAutoEnter mon(m_waitForBodyIdsMonitor);
+ RefPtr<nsImapMailboxSpec> new_spec =
+ GetServerStateParser().CreateCurrentMailboxSpec();
+ nsImapAction imapAction;
+ nsresult res = m_runningUrl->GetImapAction(&imapAction);
+ if (NS_SUCCEEDED(res) && imapAction == nsIImapUrl::nsImapExpungeFolder)
+ new_spec->mBoxFlags |= kJustExpunged;
+
+ if (m_imapMailFolderSink) {
+ bool more;
+ m_imapMailFolderSink->UpdateImapMailboxInfo(this, new_spec);
+ m_imapMailFolderSink->GetMsgHdrsToDownload(
+ &more, &m_progressExpectedNumber, msgIdList);
+ // Assert that either it's empty string OR it must be header string.
+ MOZ_ASSERT((m_stringIndex == IMAP_EMPTY_STRING_INDEX) ||
+ (m_stringIndex == IMAP_HEADERS_STRING_INDEX));
+ m_progressCurrentNumber[m_stringIndex] = 0;
+ m_runningUrl->SetMoreHeadersToDownload(more);
+ // We're going to be re-running this url if there are more headers.
+ if (more) m_runningUrl->SetRerunningUrl(true);
+ }
+ }
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ if (msgIdList.Length() > 0) {
+ FolderHeaderDump(msgIdList.Elements(), msgIdList.Length());
+ }
+ HeaderFetchCompleted();
+ // this might be bogus, how are we going to do pane notification and stuff
+ // when we fetch bodies without headers!
+ }
+
+ // wait for a list of bodies to fetch.
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsTArray<nsMsgKey> msgIds;
+ WaitForPotentialListOfBodysToFetch(msgIds);
+ if (msgIds.Length() > 0 && GetServerStateParser().LastCommandSuccessful()) {
+ // Tell the url that it should store the msg fetch results offline,
+ // while we're dumping the messages, and then restore the setting.
+ bool wasStoringOffline;
+ m_runningUrl->GetStoreResultsOffline(&wasStoringOffline);
+ m_runningUrl->SetStoreResultsOffline(true);
+ // Assert that either it's empty string OR it must be message string.
+ MOZ_ASSERT((m_stringIndex == IMAP_EMPTY_STRING_INDEX) ||
+ (m_stringIndex == IMAP_MESSAGES_STRING_INDEX));
+ m_progressCurrentNumber[m_stringIndex] = 0;
+ m_progressExpectedNumber = msgIds.Length();
+ FolderMsgDump(msgIds.Elements(), msgIds.Length(), kEveryThingRFC822Peek);
+ m_runningUrl->SetStoreResultsOffline(wasStoringOffline);
+ }
+ }
+ if (!GetServerStateParser().LastCommandSuccessful())
+ GetServerStateParser().ResetFlagInfo();
+}
+
+void nsImapProtocol::FolderHeaderDump(uint32_t* msgUids, uint32_t msgCount) {
+ FolderMsgDump(msgUids, msgCount, kHeadersRFC822andUid);
+}
+
+void nsImapProtocol::FolderMsgDump(uint32_t* msgUids, uint32_t msgCount,
+ nsIMAPeFetchFields fields) {
+ // lets worry about this progress stuff later.
+ switch (fields) {
+ case kHeadersRFC822andUid:
+ SetProgressString(IMAP_HEADERS_STRING_INDEX);
+ break;
+ case kFlags:
+ SetProgressString(IMAP_FLAGS_STRING_INDEX);
+ break;
+ default:
+ SetProgressString(IMAP_MESSAGES_STRING_INDEX);
+ break;
+ }
+
+ FolderMsgDumpLoop(msgUids, msgCount, fields);
+
+ SetProgressString(IMAP_EMPTY_STRING_INDEX);
+}
+
+void nsImapProtocol::WaitForPotentialListOfBodysToFetch(
+ nsTArray<nsMsgKey>& msgIdList) {
+ PRIntervalTime sleepTime = kImapSleepTime;
+
+ ReentrantMonitorAutoEnter fetchListMon(m_fetchBodyListMonitor);
+ while (!m_fetchBodyListIsNew && !DeathSignalReceived())
+ fetchListMon.Wait(sleepTime);
+ m_fetchBodyListIsNew = false;
+
+ msgIdList = m_fetchBodyIdList.Clone();
+}
+
+// libmsg uses this to notify a running imap url about message bodies it should
+// download. why not just have libmsg explicitly download the message bodies?
+NS_IMETHODIMP nsImapProtocol::NotifyBodysToDownload(
+ const nsTArray<nsMsgKey>& keys) {
+ ReentrantMonitorAutoEnter fetchListMon(m_fetchBodyListMonitor);
+ m_fetchBodyIdList = keys.Clone();
+ m_fetchBodyListIsNew = true;
+ fetchListMon.Notify();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetFlagsForUID(uint32_t uid, bool* foundIt,
+ imapMessageFlagsType* resultFlags,
+ char** customFlags) {
+ int32_t i;
+
+ imapMessageFlagsType flags =
+ m_flagState->GetMessageFlagsFromUID(uid, foundIt, &i);
+ if (*foundIt) {
+ *resultFlags = flags;
+ if ((flags & kImapMsgCustomKeywordFlag) && customFlags)
+ m_flagState->GetCustomFlags(uid, customFlags);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetFlagAndUidState(
+ nsIImapFlagAndUidState** aFlagState) {
+ NS_ENSURE_ARG_POINTER(aFlagState);
+ NS_IF_ADDREF(*aFlagState = m_flagState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetSupportedUserFlags(uint16_t* supportedFlags) {
+ if (!supportedFlags) return NS_ERROR_NULL_POINTER;
+
+ *supportedFlags = m_flagState->GetSupportedUserFlags();
+ return NS_OK;
+}
+void nsImapProtocol::FolderMsgDumpLoop(uint32_t* msgUids, uint32_t msgCount,
+ nsIMAPeFetchFields fields) {
+ int32_t msgCountLeft = msgCount;
+ uint32_t msgsDownloaded = 0;
+ do {
+ nsCString idString;
+ uint32_t msgsToDownload = msgCountLeft;
+ AllocateImapUidString(msgUids + msgsDownloaded, msgsToDownload, m_flagState,
+ idString); // 20 * 200
+ FetchMessage(idString, fields);
+ msgsDownloaded += msgsToDownload;
+ msgCountLeft -= msgsToDownload;
+ } while (msgCountLeft > 0 && !DeathSignalReceived());
+}
+
+void nsImapProtocol::HeaderFetchCompleted() {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->ParseMsgHdrs(this, m_hdrDownloadCache);
+ m_hdrDownloadCache->ReleaseAll();
+
+ if (m_imapMailFolderSink) m_imapMailFolderSink->HeaderFetchCompleted(this);
+}
+
+// Use the noop to tell the server we are still here, and therefore we are
+// willing to receive status updates. The recent or exists response from the
+// server could tell us that there is more mail waiting for us, but we need to
+// check the flags of the mail and the high water mark to make sure that we do
+// not tell the user that there is new mail when perhaps they have already read
+// it in another machine.
+
+void nsImapProtocol::PeriodicBiff() {
+ nsMsgBiffState startingState = m_currentBiffState;
+
+ if (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected) {
+ Noop(); // check the latest number of messages
+ int32_t numMessages = 0;
+ m_flagState->GetNumberOfMessages(&numMessages);
+ if (GetServerStateParser().NumberOfMessages() != numMessages) {
+ uint32_t id = GetServerStateParser().HighestRecordedUID() + 1;
+ nsCString fetchStr; // only update flags
+ uint32_t added = 0, deleted = 0;
+
+ deleted = m_flagState->NumberOfDeletedMessages();
+ added = numMessages;
+ if (!added || (added == deleted)) // empty keys, get them all
+ id = 1;
+
+ // sprintf(fetchStr, "%ld:%ld", id, id +
+ // GetServerStateParser().NumberOfMessages() -
+ // fFlagState->GetNumberOfMessages());
+ AppendUid(fetchStr, id);
+ fetchStr.AppendLiteral(":*");
+ FetchMessage(fetchStr, kFlags);
+ if (((uint32_t)m_flagState->GetHighestNonDeletedUID() >= id) &&
+ m_flagState->IsLastMessageUnseen())
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NewMail;
+ else
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail;
+ } else
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail;
+ } else
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown;
+
+ if (startingState != m_currentBiffState)
+ SendSetBiffIndicatorEvent(m_currentBiffState);
+}
+
+void nsImapProtocol::SendSetBiffIndicatorEvent(nsMsgBiffState newState) {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->SetBiffStateAndUpdate(newState);
+}
+
+/* static */ void nsImapProtocol::LogImapUrl(const char* logMsg,
+ nsIImapUrl* imapUrl) {
+ if (MOZ_LOG_TEST(IMAP, LogLevel::Info)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl);
+ if (mailnewsUrl) {
+ nsAutoCString urlSpec, unescapedUrlSpec;
+ nsresult rv = mailnewsUrl->GetSpec(urlSpec);
+ if (NS_FAILED(rv)) return;
+ MsgUnescapeString(urlSpec, 0, unescapedUrlSpec);
+ MOZ_LOG(IMAP, LogLevel::Info, ("%s:%s", logMsg, unescapedUrlSpec.get()));
+ }
+ }
+}
+
+// log info including current state...
+void nsImapProtocol::Log(const char* logSubName, const char* extraInfo,
+ const char* logData) {
+ if (MOZ_LOG_TEST(IMAP, LogLevel::Info)) {
+ static const char nonAuthStateName[] = "NA";
+ static const char authStateName[] = "A";
+ static const char selectedStateName[] = "S";
+ const nsCString& hostName =
+ GetImapHostName(); // initialize to empty string
+
+ int32_t logDataLen = PL_strlen(logData); // PL_strlen checks for null
+ nsCString logDataLines;
+ const char* logDataToLog;
+ int32_t lastLineEnd;
+
+ // nspr line length is 512, and we allow some space for the log preamble.
+ const int kLogDataChunkSize = 400;
+
+ // break up buffers > 400 bytes on line boundaries.
+ if (logDataLen > kLogDataChunkSize) {
+ logDataLines.Assign(logData);
+ lastLineEnd = logDataLines.RFindChar('\n', kLogDataChunkSize);
+ // null terminate the last line
+ if (lastLineEnd == kNotFound) lastLineEnd = kLogDataChunkSize - 1;
+
+ logDataLines.Insert('\0', lastLineEnd + 1);
+ logDataToLog = logDataLines.get();
+ } else {
+ logDataToLog = logData;
+ lastLineEnd = logDataLen;
+ }
+ switch (GetServerStateParser().GetIMAPstate()) {
+ case nsImapServerResponseParser::kFolderSelected:
+ if (extraInfo)
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("%p:%s:%s-%s:%s:%s: %.400s", this, hostName.get(),
+ selectedStateName,
+ GetServerStateParser().GetSelectedMailboxName(), logSubName,
+ extraInfo, logDataToLog));
+ else
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("%p:%s:%s-%s:%s: %.400s", this, hostName.get(),
+ selectedStateName,
+ GetServerStateParser().GetSelectedMailboxName(), logSubName,
+ logDataToLog));
+ break;
+ case nsImapServerResponseParser::kNonAuthenticated:
+ case nsImapServerResponseParser::kAuthenticated: {
+ const char* stateName = (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kNonAuthenticated)
+ ? nonAuthStateName
+ : authStateName;
+ if (extraInfo)
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("%p:%s:%s:%s:%s: %.400s", this, hostName.get(), stateName,
+ logSubName, extraInfo, logDataToLog));
+ else
+ MOZ_LOG(IMAP, LogLevel::Info,
+ ("%p:%s:%s:%s: %.400s", this, hostName.get(), stateName,
+ logSubName, logDataToLog));
+ }
+ }
+
+ // dump the rest of the string in < 400 byte chunks
+ while (logDataLen > kLogDataChunkSize) {
+ logDataLines.Cut(
+ 0,
+ lastLineEnd + 2); // + 2 to account for the LF and the '\0' we added
+ logDataLen = logDataLines.Length();
+ lastLineEnd = (logDataLen > kLogDataChunkSize)
+ ? logDataLines.RFindChar('\n', kLogDataChunkSize)
+ : kNotFound;
+ // null terminate the last line
+ if (lastLineEnd == kNotFound) lastLineEnd = kLogDataChunkSize - 1;
+ logDataLines.Insert('\0', lastLineEnd + 1);
+ logDataToLog = logDataLines.get();
+ MOZ_LOG(IMAP, LogLevel::Info, ("%.400s", logDataToLog));
+ }
+ }
+}
+
+// In 4.5, this posted an event back to libmsg and blocked until it got a
+// response. We may still have to do this.It would be nice if we could preflight
+// this value, but we may not always know when we'll need it.
+uint32_t nsImapProtocol::GetMessageSize(const nsACString& messageId) {
+ uint32_t size = 0;
+ if (m_imapMessageSink)
+ m_imapMessageSink->GetMessageSizeFromDB(PromiseFlatCString(messageId).get(),
+ &size);
+ if (DeathSignalReceived()) size = 0;
+ return size;
+}
+
+// message id string utility functions
+/* static */ bool nsImapProtocol::HandlingMultipleMessages(
+ const nsCString& messageIdString) {
+ return (MsgFindCharInSet(messageIdString, ",:") != kNotFound);
+}
+
+uint32_t nsImapProtocol::CountMessagesInIdString(const char* idString) {
+ uint32_t numberOfMessages = 0;
+ char* uidString = PL_strdup(idString);
+
+ if (uidString) {
+ // This is in the form <id>,<id>, or <id1>:<id2>
+ char curChar = *uidString;
+ bool isRange = false;
+ int32_t curToken;
+ int32_t saveStartToken = 0;
+
+ for (char* curCharPtr = uidString; curChar && *curCharPtr;) {
+ char* currentKeyToken = curCharPtr;
+ curChar = *curCharPtr;
+ while (curChar != ':' && curChar != ',' && curChar != '\0')
+ curChar = *curCharPtr++;
+ *(curCharPtr - 1) = '\0';
+ curToken = atol(currentKeyToken);
+ if (isRange) {
+ while (saveStartToken < curToken) {
+ numberOfMessages++;
+ saveStartToken++;
+ }
+ }
+
+ numberOfMessages++;
+ isRange = (curChar == ':');
+ if (isRange) saveStartToken = curToken + 1;
+ }
+ PR_Free(uidString);
+ }
+ return numberOfMessages;
+}
+
+// It would be really nice not to have to use this method nearly as much as we
+// did in 4.5 - we need to think about this some. Some of it may just go away in
+// the new world order
+bool nsImapProtocol::DeathSignalReceived() {
+ // ignore mock channel status if we've been pseudo interrupted
+ // ### need to make sure we clear pseudo interrupted status appropriately.
+ if (!GetPseudoInterrupted() && m_mockChannel) {
+ nsresult returnValue;
+ m_mockChannel->GetStatus(&returnValue);
+ if (NS_FAILED(returnValue)) return false;
+ }
+
+ // Check the other way of cancelling.
+ ReentrantMonitorAutoEnter threadDeathMon(m_threadDeathMonitor);
+ return m_threadShouldDie;
+}
+
+NS_IMETHODIMP nsImapProtocol::ResetToAuthenticatedState() {
+ GetServerStateParser().PreauthSetAuthenticatedState();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapProtocol::GetSelectedMailboxName(char** folderName) {
+ if (!folderName) return NS_ERROR_NULL_POINTER;
+ if (GetServerStateParser().GetSelectedMailboxName())
+ *folderName = PL_strdup((GetServerStateParser().GetSelectedMailboxName()));
+ return NS_OK;
+}
+
+bool nsImapProtocol::GetPseudoInterrupted() {
+ ReentrantMonitorAutoEnter pseudoInterruptMon(m_pseudoInterruptMonitor);
+ return m_pseudoInterrupted;
+}
+
+NS_IMETHODIMP
+nsImapProtocol::PseudoInterrupt(bool interrupt) {
+ ReentrantMonitorAutoEnter pseudoInterruptMon(m_pseudoInterruptMonitor);
+ m_pseudoInterrupted = interrupt;
+ if (interrupt) Log("CONTROL", NULL, "PSEUDO-Interrupted");
+ return NS_OK;
+}
+
+void nsImapProtocol::SetActive(bool active) {
+ ReentrantMonitorAutoEnter dataMemberMon(m_dataMemberMonitor);
+ m_active = active;
+}
+
+bool nsImapProtocol::GetActive() {
+ ReentrantMonitorAutoEnter dataMemberMon(m_dataMemberMonitor);
+ return m_active;
+}
+
+bool nsImapProtocol::GetShowAttachmentsInline() {
+ bool showAttachmentsInline = true;
+ if (m_imapServerSink)
+ m_imapServerSink->GetShowAttachmentsInline(&showAttachmentsInline);
+ return showAttachmentsInline;
+}
+
+// Adds a set of rights for a given user on a given mailbox on the current host.
+// if userName is NULL, it means "me," or MYRIGHTS.
+void nsImapProtocol::AddFolderRightsForUser(const char* mailboxName,
+ const char* userName,
+ const char* rights) {
+ if (!userName) userName = "";
+ if (m_imapServerSink)
+ m_imapServerSink->AddFolderRights(nsDependentCString(mailboxName),
+ nsDependentCString(userName),
+ nsDependentCString(rights));
+}
+
+void nsImapProtocol::SetCopyResponseUid(const char* msgIdString) {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->SetCopyResponseUid(msgIdString, m_runningUrl);
+}
+
+void nsImapProtocol::CommitNamespacesForHostEvent() {
+ if (m_imapServerSink) m_imapServerSink->CommitNamespaces();
+}
+
+// notifies libmsg that we have new capability data for the current host
+void nsImapProtocol::CommitCapability() {
+ if (m_imapServerSink) {
+ m_imapServerSink->SetCapability(GetServerStateParser().GetCapabilityFlag());
+ }
+}
+
+// rights is a single string of rights, as specified by RFC2086, the IMAP ACL
+// extension. Clears all rights for a given folder, for all users.
+void nsImapProtocol::ClearAllFolderRights() {
+ if (m_imapMailFolderSink) m_imapMailFolderSink->ClearFolderRights();
+}
+
+// Reads a line from the socket.
+// Upon failure, the thread will be flagged for shutdown, and
+// m_connectionStatus will be set to a failing code.
+// Remember that some socket errors are deferred until the first read
+// attempt, so this function could be the first place we hear about
+// connection issues (e.g. bad certificates for SSL).
+char* nsImapProtocol::CreateNewLineFromSocket() {
+ bool needMoreData = false;
+ char* newLine = nullptr;
+ uint32_t numBytesInLine = 0;
+ nsresult rv = NS_OK;
+ // we hold a ref to the input stream in case we get cancelled from the
+ // ui thread, which releases our ref to the input stream, and can
+ // cause the pipe to get deleted before the monitor the read is
+ // blocked on gets notified. When that happens, the imap thread
+ // will stay blocked.
+ nsCOMPtr<nsIInputStream> kungFuGrip = m_inputStream;
+
+ if (m_mockChannel) {
+ nsImapMockChannel* imapChannel =
+ static_cast<nsImapMockChannel*>(m_mockChannel.get());
+
+ mozilla::MonitorAutoLock lock(imapChannel->mSuspendedMonitor);
+
+ bool suspended = imapChannel->mSuspended;
+ if (suspended)
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("Waiting until [imapChannel=%p] is resumed.", imapChannel));
+ while (imapChannel->mSuspended) {
+ lock.Wait();
+ }
+ if (suspended)
+ MOZ_LOG(
+ IMAP, LogLevel::Debug,
+ ("Done waiting, [imapChannel=%p] has been resumed.", imapChannel));
+ }
+
+ do {
+ newLine = m_inputStreamBuffer->ReadNextLine(m_inputStream, numBytesInLine,
+ needMoreData, &rv);
+ MOZ_LOG(IMAP, LogLevel::Verbose,
+ ("ReadNextLine [rv=0x%" PRIx32 " stream=%p nb=%u needmore=%u]",
+ static_cast<uint32_t>(rv), m_inputStream.get(), numBytesInLine,
+ needMoreData));
+
+ } while (!newLine && NS_SUCCEEDED(rv) &&
+ !DeathSignalReceived()); // until we get the next line and haven't
+ // been interrupted
+
+ kungFuGrip = nullptr;
+
+ if (NS_FAILED(rv)) {
+ switch (rv) {
+ case NS_ERROR_UNKNOWN_HOST:
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ AlertUserEventUsingName("imapUnknownHostError");
+ break;
+ case NS_ERROR_CONNECTION_REFUSED:
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ AlertUserEventUsingName("imapConnectionRefusedError");
+ break;
+ case NS_ERROR_NET_TIMEOUT:
+ case NS_ERROR_NET_RESET:
+ case NS_BASE_STREAM_CLOSED:
+ case NS_ERROR_NET_INTERRUPT:
+ // we should retry on RESET, especially for SSL...
+ if ((TestFlag(IMAP_RECEIVED_GREETING) || rv == NS_ERROR_NET_RESET) &&
+ m_runningUrl && !m_retryUrlOnError) {
+ bool rerunningUrl;
+ nsImapAction imapAction;
+ m_runningUrl->GetRerunningUrl(&rerunningUrl);
+ m_runningUrl->GetImapAction(&imapAction);
+ // don't rerun if we already were rerunning. And don't rerun
+ // online move/copies that timeout.
+ if (!rerunningUrl && (rv != NS_ERROR_NET_TIMEOUT ||
+ (imapAction != nsIImapUrl::nsImapOnlineCopy &&
+ imapAction != nsIImapUrl::nsImapOnlineMove))) {
+ m_runningUrl->SetRerunningUrl(true);
+ m_retryUrlOnError = true;
+ break;
+ }
+ }
+ if (rv == NS_ERROR_NET_TIMEOUT)
+ AlertUserEventUsingName("imapNetTimeoutError");
+ else
+ AlertUserEventUsingName(TestFlag(IMAP_RECEIVED_GREETING)
+ ? "imapServerDisconnected"
+ : "imapServerDroppedConnection");
+ break;
+ default:
+ // This is probably a TLS error. Usually TLS errors won't show up until
+ // we do ReadNextLine() above. Since we're in the IMAP thread we can't
+ // call NSSErrorsService::GetErrorClass() to determine if the error
+ // should result in an non-fatal override dialog (usually certificate
+ // issues) or if it's a fatal protocol error that the user must be
+ // alerted to. Instead, we use some publicly-accessible macros and a
+ // function to determine this.
+ if (NS_ERROR_GET_MODULE(rv) == NS_ERROR_MODULE_SECURITY &&
+ NS_ERROR_GET_SEVERITY(rv) == NS_ERROR_SEVERITY_ERROR) {
+ // It's an error of class 21 (SSL/TLS/Security), e.g., overridable
+ // SSL_ERROR_BAD_CERT_DOMAIN from security/nss/lib/ssl/sslerr.h
+ // rv = 0x80000000 + 0x00450000 + 0x00150000 + 0x00002ff4 = 0x805A2ff4
+ int32_t sec_error = -1 * NS_ERROR_GET_CODE(rv); // = 0xFFFFD00C
+ if (!mozilla::psm::ErrorIsOverridable(sec_error)) {
+ AlertUserEventUsingName("imapTlsError");
+ }
+
+ // Stash the socket transport securityInfo on the URL so it will be
+ // available in nsIUrlListener OnStopRunningUrl() callbacks to trigger
+ // the override dialog or a security related error message.
+ // Currently this is only used to trigger the override dialog.
+ if (m_runningUrl) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl =
+ do_QueryInterface(m_runningUrl);
+ if (mailNewsUrl) {
+ nsCOMPtr<nsITransportSecurityInfo> securityInfo;
+ GetTransportSecurityInfo(getter_AddRefs(securityInfo));
+ if (securityInfo) {
+ nsAutoCString logMsg("Security error - error code=");
+ nsAutoString errorCodeString;
+ securityInfo->GetErrorCodeString(errorCodeString);
+ logMsg.Append(NS_ConvertUTF16toUTF8(errorCodeString));
+ Log("CreateNewLineFromSocket", nullptr, logMsg.get());
+
+ mailNewsUrl->SetFailedSecInfo(securityInfo);
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ nsAutoCString logMsg("clearing IMAP_CONNECTION_IS_OPEN - rv = ");
+ logMsg.AppendInt(static_cast<uint32_t>(rv), 16);
+ Log("CreateNewLineFromSocket", nullptr, logMsg.get());
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ TellThreadToDie();
+ }
+ Log("CreateNewLineFromSocket", nullptr, newLine);
+ SetConnectionStatus(newLine && numBytesInLine
+ ? NS_OK
+ : rv); // set > 0 if string is not null or empty
+ return newLine;
+}
+
+nsresult nsImapProtocol::GetConnectionStatus() { return m_connectionStatus; }
+
+void nsImapProtocol::SetConnectionStatus(nsresult status) {
+ // Log failure at Debug level, otherwise use Verbose to avoid huge logs
+ MOZ_LOG(
+ IMAP, NS_SUCCEEDED(status) ? LogLevel::Verbose : LogLevel::Debug,
+ ("SetConnectionStatus(0x%" PRIx32 ")", static_cast<uint32_t>(status)));
+ m_connectionStatus = status;
+}
+
+void nsImapProtocol::NotifyMessageFlags(imapMessageFlagsType flags,
+ const nsACString& keywords,
+ nsMsgKey key, uint64_t highestModSeq) {
+ if (m_imapMessageSink) {
+ // if we're selecting the folder, don't need to report the flags; we've
+ // already fetched them.
+ if (m_imapAction != nsIImapUrl::nsImapSelectFolder)
+ m_imapMessageSink->NotifyMessageFlags(flags, keywords, key,
+ highestModSeq);
+ }
+}
+
+void nsImapProtocol::NotifySearchHit(const char* hitLine) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(m_runningUrl, &rv);
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->NotifySearchHit(mailnewsUrl, hitLine);
+}
+
+void nsImapProtocol::SetMailboxDiscoveryStatus(EMailboxDiscoverStatus status) {
+ ReentrantMonitorAutoEnter mon(m_dataMemberMonitor);
+ m_discoveryStatus = status;
+}
+
+EMailboxDiscoverStatus nsImapProtocol::GetMailboxDiscoveryStatus() {
+ ReentrantMonitorAutoEnter mon(m_dataMemberMonitor);
+ return m_discoveryStatus;
+}
+
+bool nsImapProtocol::GetSubscribingNow() {
+ // ***** code me *****
+ return false; // ***** for now
+}
+
+void nsImapProtocol::DiscoverMailboxSpec(nsImapMailboxSpec* adoptedBoxSpec) {
+ nsImapNamespace* ns = nullptr;
+
+ NS_ASSERTION(m_hostSessionList, "fatal null host session list");
+ if (!m_hostSessionList) return;
+
+ m_hostSessionList->GetDefaultNamespaceOfTypeForHost(GetImapServerKey(),
+ kPersonalNamespace, ns);
+ const char* nsPrefix = ns ? ns->GetPrefix() : 0;
+
+ if (m_specialXListMailboxes.Count() > 0) {
+ nsCString strHashKey(adoptedBoxSpec->mAllocatedPathName);
+ int32_t hashValue = m_specialXListMailboxes.Get(strHashKey);
+ adoptedBoxSpec->mBoxFlags |= hashValue;
+ }
+
+ switch (m_hierarchyNameState) {
+ case kXListing:
+ if (adoptedBoxSpec->mBoxFlags &
+ (kImapXListTrash | kImapAllMail | kImapInbox | kImapSent | kImapSpam |
+ kImapDrafts)) {
+ nsCString mailboxName(adoptedBoxSpec->mAllocatedPathName);
+ m_specialXListMailboxes.InsertOrUpdate(mailboxName,
+ adoptedBoxSpec->mBoxFlags);
+ // Remember hierarchy delimiter in case this is the first time we've
+ // connected to the server and we need it to be correct for the
+ // two-level XLIST we send (INBOX is guaranteed to be in the first
+ // response).
+ if (adoptedBoxSpec->mBoxFlags & kImapInbox)
+ m_runningUrl->SetOnlineSubDirSeparator(
+ adoptedBoxSpec->mHierarchySeparator);
+ }
+ break;
+ case kListingForFolderFlags: {
+ // store mailbox flags from LIST for use by LSUB
+ nsCString mailboxName(adoptedBoxSpec->mAllocatedPathName);
+ m_standardListMailboxes.InsertOrUpdate(mailboxName,
+ adoptedBoxSpec->mBoxFlags);
+ } break;
+ case kListingForCreate:
+ case kNoOperationInProgress:
+ case kDiscoverTrashFolderInProgress:
+ case kListingForInfoAndDiscovery: {
+ // standard mailbox specs are stored in m_standardListMailboxes
+ // because LSUB does necessarily return all mailbox flags.
+ // count should be > 0 only when we are looking at response of LSUB
+ if (m_standardListMailboxes.Count() > 0) {
+ int32_t hashValue = 0;
+ nsCString strHashKey(adoptedBoxSpec->mAllocatedPathName);
+ if (m_standardListMailboxes.Get(strHashKey, &hashValue))
+ adoptedBoxSpec->mBoxFlags |= hashValue;
+ else
+ // if mailbox is not in hash list, then it is subscribed but does not
+ // exist, so we make sure it can't be selected
+ adoptedBoxSpec->mBoxFlags |= kNoselect;
+ }
+ if (ns &&
+ nsPrefix) // if no personal namespace, there can be no Trash folder
+ {
+ bool onlineTrashFolderExists = false;
+ if (m_hostSessionList) {
+ if (adoptedBoxSpec->mBoxFlags & (kImapTrash | kImapXListTrash)) {
+ m_hostSessionList->SetOnlineTrashFolderExistsForHost(
+ GetImapServerKey(), true);
+ onlineTrashFolderExists = true;
+ } else {
+ m_hostSessionList->GetOnlineTrashFolderExistsForHost(
+ GetImapServerKey(), onlineTrashFolderExists);
+ }
+ }
+
+ // Don't set the Trash flag if not using the Trash model
+ if (GetDeleteIsMoveToTrash() && !onlineTrashFolderExists &&
+ FindInReadable(m_trashFolderPath,
+ adoptedBoxSpec->mAllocatedPathName,
+ nsCaseInsensitiveCStringComparator)) {
+ bool trashExists = false;
+ if (StringBeginsWith(m_trashFolderPath, "INBOX/"_ns,
+ nsCaseInsensitiveCStringComparator)) {
+ nsAutoCString pathName(adoptedBoxSpec->mAllocatedPathName.get() +
+ 6);
+ trashExists =
+ StringBeginsWith(
+ adoptedBoxSpec->mAllocatedPathName, m_trashFolderPath,
+ nsCaseInsensitiveCStringComparator) && /* "INBOX/" */
+ pathName.Equals(Substring(m_trashFolderPath, 6),
+ nsCaseInsensitiveCStringComparator);
+ } else
+ trashExists = adoptedBoxSpec->mAllocatedPathName.Equals(
+ m_trashFolderPath, nsCaseInsensitiveCStringComparator);
+
+ if (m_hostSessionList)
+ m_hostSessionList->SetOnlineTrashFolderExistsForHost(
+ GetImapServerKey(), trashExists);
+
+ if (trashExists) adoptedBoxSpec->mBoxFlags |= kImapTrash;
+ }
+ }
+
+ // Discover the folder (shuttle over to libmsg, yay)
+ // Do this only if the folder name is not empty (i.e. the root)
+ if (!adoptedBoxSpec->mAllocatedPathName.IsEmpty()) {
+ if (m_hierarchyNameState == kListingForCreate)
+ adoptedBoxSpec->mBoxFlags |= kNewlyCreatedFolder;
+
+ if (m_imapServerSink) {
+ bool newFolder;
+
+ m_imapServerSink->PossibleImapMailbox(
+ adoptedBoxSpec->mAllocatedPathName,
+ adoptedBoxSpec->mHierarchySeparator, adoptedBoxSpec->mBoxFlags,
+ &newFolder);
+ // if it's a new folder to the server sink, setting discovery status
+ // to eContinueNew will cause us to get the ACL for the new folder.
+ if (newFolder) SetMailboxDiscoveryStatus(eContinueNew);
+
+ bool useSubscription = false;
+
+ if (m_hostSessionList)
+ m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(),
+ useSubscription);
+
+ if ((GetMailboxDiscoveryStatus() != eContinue) &&
+ (GetMailboxDiscoveryStatus() != eContinueNew) &&
+ (GetMailboxDiscoveryStatus() != eListMyChildren)) {
+ SetConnectionStatus(NS_ERROR_FAILURE);
+ } else if (!adoptedBoxSpec->mAllocatedPathName.IsEmpty() &&
+ (GetMailboxDiscoveryStatus() == eListMyChildren) &&
+ (!useSubscription || GetSubscribingNow())) {
+ NS_ASSERTION(false, "we should never get here anymore");
+ SetMailboxDiscoveryStatus(eContinue);
+ } else if (GetMailboxDiscoveryStatus() == eContinueNew) {
+ if (m_hierarchyNameState == kListingForInfoAndDiscovery &&
+ !adoptedBoxSpec->mAllocatedPathName.IsEmpty() &&
+ !(adoptedBoxSpec->mBoxFlags & kNameSpace)) {
+ // remember the info here also
+ nsIMAPMailboxInfo* mb =
+ new nsIMAPMailboxInfo(adoptedBoxSpec->mAllocatedPathName,
+ adoptedBoxSpec->mHierarchySeparator);
+ m_listedMailboxList.AppendElement(mb);
+ }
+ SetMailboxDiscoveryStatus(eContinue);
+ }
+ }
+ }
+ } break;
+ case kDeleteSubFoldersInProgress: {
+ NS_ASSERTION(m_deletableChildren, "Oops .. null m_deletableChildren");
+ m_deletableChildren->AppendElement(adoptedBoxSpec->mAllocatedPathName);
+ } break;
+ case kListingForInfoOnly: {
+ // UpdateProgressWindowForUpgrade(adoptedBoxSpec->allocatedPathName);
+ ProgressEventFunctionUsingNameWithString(
+ "imapDiscoveringMailbox", adoptedBoxSpec->mAllocatedPathName.get());
+ nsIMAPMailboxInfo* mb =
+ new nsIMAPMailboxInfo(adoptedBoxSpec->mAllocatedPathName,
+ adoptedBoxSpec->mHierarchySeparator);
+ m_listedMailboxList.AppendElement(mb);
+ } break;
+ case kDiscoveringNamespacesOnly: {
+ } break;
+ default:
+ NS_ASSERTION(false, "we aren't supposed to be here");
+ break;
+ }
+}
+
+void nsImapProtocol::AlertUserEventUsingName(const char* aMessageName) {
+ if (m_imapServerSink) {
+ bool suppressErrorMsg = false;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ if (mailnewsUrl) mailnewsUrl->GetSuppressErrorMsgs(&suppressErrorMsg);
+
+ if (!suppressErrorMsg)
+ m_imapServerSink->FEAlertWithName(aMessageName, mailnewsUrl);
+ }
+}
+
+void nsImapProtocol::AlertUserEvent(const char* message) {
+ if (m_imapServerSink) {
+ bool suppressErrorMsg = false;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ if (mailnewsUrl) mailnewsUrl->GetSuppressErrorMsgs(&suppressErrorMsg);
+
+ if (!suppressErrorMsg)
+ m_imapServerSink->FEAlert(NS_ConvertASCIItoUTF16(message), mailnewsUrl);
+ }
+}
+
+void nsImapProtocol::AlertUserEventFromServer(const char* aServerEvent,
+ bool aForIdle) {
+ if (aServerEvent) {
+ // If called due to BAD/NO imap IDLE response, the server sink and running
+ // url are typically null when IDLE command is sent. So use the stored
+ // latest values for these so that the error alert notification occurs.
+ if (aForIdle && !m_imapServerSink && !m_runningUrl &&
+ m_imapServerSinkLatest) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(m_runningUrlLatest);
+ m_imapServerSinkLatest->FEAlertFromServer(
+ nsDependentCString(aServerEvent), mailnewsUrl);
+ } else if (m_imapServerSink) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ m_imapServerSink->FEAlertFromServer(nsDependentCString(aServerEvent),
+ mailnewsUrl);
+ }
+ }
+}
+
+void nsImapProtocol::ResetProgressInfo() {
+ m_lastProgressTime = 0;
+ m_lastPercent = -1;
+ m_lastProgressStringName.Truncate();
+}
+
+void nsImapProtocol::SetProgressString(uint32_t aStringIndex) {
+ m_stringIndex = aStringIndex;
+ MOZ_ASSERT(m_stringIndex <= IMAP_EMPTY_STRING_INDEX);
+ switch (m_stringIndex) {
+ case IMAP_HEADERS_STRING_INDEX:
+ m_progressStringName = "imapReceivingMessageHeaders3";
+ break;
+ case IMAP_MESSAGES_STRING_INDEX:
+ m_progressStringName = "imapFolderReceivingMessageOf3";
+ break;
+ case IMAP_FLAGS_STRING_INDEX:
+ m_progressStringName = "imapReceivingMessageFlags3";
+ break;
+ case IMAP_EMPTY_STRING_INDEX:
+ default:
+ break;
+ }
+}
+
+void nsImapProtocol::ShowProgress() {
+ if (m_imapServerSink && (m_stringIndex != IMAP_EMPTY_STRING_INDEX)) {
+ nsString progressString;
+ const char* mailboxName = GetServerStateParser().GetSelectedMailboxName();
+ nsString unicodeMailboxName;
+ nsresult rv = CopyFolderNameToUTF16(nsDependentCString(mailboxName),
+ unicodeMailboxName);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ int32_t progressCurrentNumber = ++m_progressCurrentNumber[m_stringIndex];
+
+ PercentProgressUpdateEvent(m_progressStringName, unicodeMailboxName,
+ progressCurrentNumber, m_progressExpectedNumber);
+ }
+}
+
+void nsImapProtocol::ProgressEventFunctionUsingName(const char* aMsgName) {
+ if (m_imapMailFolderSink && !m_lastProgressStringName.Equals(aMsgName)) {
+ m_imapMailFolderSink->ProgressStatusString(this, aMsgName, nullptr);
+ m_lastProgressStringName.Assign(aMsgName);
+ // who's going to free this? Does ProgressStatusString complete
+ // synchronously?
+ }
+}
+
+void nsImapProtocol::ProgressEventFunctionUsingNameWithString(
+ const char* aMsgName, const char* aExtraInfo) {
+ if (m_imapMailFolderSink) {
+ nsString unicodeStr;
+ nsresult rv =
+ CopyFolderNameToUTF16(nsDependentCString(aExtraInfo), unicodeStr);
+ if (NS_SUCCEEDED(rv))
+ m_imapMailFolderSink->ProgressStatusString(this, aMsgName,
+ unicodeStr.get());
+ }
+}
+
+void nsImapProtocol::PercentProgressUpdateEvent(nsACString const& fmtStringName,
+ nsAString const& mailbox,
+ int64_t currentProgress,
+ int64_t maxProgress) {
+ int64_t nowMS = 0;
+ int32_t percent = (100 * currentProgress) / maxProgress;
+ if (percent == m_lastPercent)
+ return; // hasn't changed, right? So just return. Do we need to clear this
+ // anywhere?
+
+ if (percent < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS - m_lastProgressTime < 750) return;
+ }
+
+ m_lastPercent = percent;
+ m_lastProgressTime = nowMS;
+
+ // set our max progress on the running URL
+ if (m_runningUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(m_runningUrl));
+ mailnewsUrl->SetMaxProgress(maxProgress);
+ }
+
+ if (m_imapMailFolderSink) {
+ m_imapMailFolderSink->PercentProgress(this, fmtStringName, mailbox,
+ currentProgress, maxProgress);
+ }
+}
+
+// imap commands issued by the parser
+void nsImapProtocol::Store(const nsCString& messageList,
+ const char* messageData, bool idsAreUid) {
+ // turn messageList back into key array and then back into a message id list,
+ // but use the flag state to handle ranges correctly.
+ nsCString messageIdList;
+ nsTArray<nsMsgKey> msgKeys;
+ if (idsAreUid) ParseUidString(messageList.get(), msgKeys);
+
+ int32_t msgCountLeft = msgKeys.Length();
+ uint32_t msgsHandled = 0;
+ do {
+ nsCString idString;
+
+ uint32_t msgsToHandle = msgCountLeft;
+ if (idsAreUid)
+ AllocateImapUidString(msgKeys.Elements() + msgsHandled, msgsToHandle,
+ m_flagState, idString); // 20 * 200
+ else
+ idString.Assign(messageList);
+
+ msgsHandled += msgsToHandle;
+ msgCountLeft -= msgsToHandle;
+
+ IncrementCommandTagNumber();
+ const char* formatString;
+ if (idsAreUid)
+ formatString = "%s uid store %s %s\015\012";
+ else
+ formatString = "%s store %s %s\015\012";
+
+ // we might need to close this mailbox after this
+ m_closeNeededBeforeSelect =
+ GetDeleteIsMoveToTrash() && (PL_strcasestr(messageData, "\\Deleted"));
+
+ const char* commandTag = GetServerCommandTag();
+ int protocolStringSize = PL_strlen(formatString) + messageList.Length() +
+ PL_strlen(messageData) + PL_strlen(commandTag) + 1;
+ char* protocolString = (char*)PR_CALLOC(protocolStringSize);
+
+ if (protocolString) {
+ PR_snprintf(protocolString, // string to create
+ protocolStringSize, // max size
+ formatString, // format string
+ commandTag, // command tag
+ idString.get(), messageData);
+
+ nsresult rv = SendData(protocolString);
+ if (NS_SUCCEEDED(rv)) {
+ m_flagChangeCount++;
+ ParseIMAPandCheckForNewMail(protocolString);
+ if (GetServerStateParser().LastCommandSuccessful() && CheckNeeded())
+ Check();
+ }
+ PR_Free(protocolString);
+ } else
+ HandleMemoryFailure();
+ } while (msgCountLeft > 0 && !DeathSignalReceived());
+}
+
+void nsImapProtocol::IssueUserDefinedMsgCommand(const char* command,
+ const char* messageList) {
+ IncrementCommandTagNumber();
+
+ const char* formatString;
+ formatString = "%s uid %s %s\015\012";
+
+ const char* commandTag = GetServerCommandTag();
+ int protocolStringSize = PL_strlen(formatString) + PL_strlen(messageList) +
+ PL_strlen(command) + PL_strlen(commandTag) + 1;
+ char* protocolString = (char*)PR_CALLOC(protocolStringSize);
+
+ if (protocolString) {
+ PR_snprintf(protocolString, // string to create
+ protocolStringSize, // max size
+ formatString, // format string
+ commandTag, // command tag
+ command, messageList);
+
+ nsresult rv = SendData(protocolString);
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString);
+ PR_Free(protocolString);
+ } else
+ HandleMemoryFailure();
+}
+
+void nsImapProtocol::UidExpunge(const nsCString& messageSet) {
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" uid expunge ");
+ command.Append(messageSet);
+ command.Append(CRLF);
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Expunge() {
+ uint32_t aclFlags = 0;
+ if (GetServerStateParser().ServerHasACLCapability() && m_imapMailFolderSink)
+ m_imapMailFolderSink->GetAclFlags(&aclFlags);
+
+ if (aclFlags && !(aclFlags & IMAP_ACL_EXPUNGE_FLAG)) return;
+ ProgressEventFunctionUsingName("imapStatusExpungingMailbox");
+
+ if (gCheckDeletedBeforeExpunge) {
+ GetServerStateParser().ResetSearchResultSequence();
+ Search("SEARCH DELETED", false, false);
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsImapSearchResultIterator* search =
+ GetServerStateParser().CreateSearchResultIterator();
+ nsMsgKey key = search->GetNextMessageNumber();
+ delete search;
+ if (key == 0) return; // no deleted messages to expunge (bug 235004)
+ }
+ }
+
+ IncrementCommandTagNumber();
+ nsAutoCString command(GetServerCommandTag());
+ command.AppendLiteral(" expunge" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::HandleMemoryFailure() {
+ PR_CEnterMonitor(this);
+ // **** jefft fix me!!!!!! ******
+ // m_imapThreadIsRunning = false;
+ // SetConnectionStatus(-1);
+ PR_CExitMonitor(this);
+}
+
+void nsImapProtocol::HandleCurrentUrlError() {
+ // This is to handle a move/copy failing, especially because the user
+ // cancelled the password prompt.
+ (void)m_runningUrl->GetImapAction(&m_imapAction);
+ if (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineMove ||
+ m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile ||
+ m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile) {
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->OnlineCopyCompleted(
+ this, ImapOnlineCopyStateType::kFailedCopy);
+ }
+}
+
+void nsImapProtocol::StartTLS() {
+ IncrementCommandTagNumber();
+ nsCString tag(GetServerCommandTag());
+ nsCString command(tag);
+
+ command.AppendLiteral(" STARTTLS" CRLF);
+ nsresult rv = SendData(command.get());
+ bool ok = false;
+ if (NS_SUCCEEDED(rv)) {
+ nsCString expectOkResponse = tag + " OK "_ns;
+ char* serverResponse = nullptr;
+ do {
+ // This reads and discards lines not starting with "<tag> OK " or
+ // "<tag> BAD " and exits when when either are found. Otherwise, this
+ // exits on timeout when all lines in the buffer are read causing
+ // serverResponse to be set null. Usually just "<tag> OK " is present.
+ serverResponse = CreateNewLineFromSocket();
+ ok = serverResponse &&
+ !PL_strncasecmp(serverResponse, expectOkResponse.get(),
+ expectOkResponse.Length());
+ if (!ok && serverResponse) {
+ // Check for possible BAD response, e.g., server not STARTTLS capable.
+ nsCString expectBadResponse = tag + " BAD "_ns;
+ if (!PL_strncasecmp(serverResponse, expectBadResponse.get(),
+ expectBadResponse.Length())) {
+ PR_Free(serverResponse);
+ break;
+ }
+ }
+ PR_Free(serverResponse);
+ } while (serverResponse && !ok);
+ }
+ // ok == false implies a "<tag> BAD " response or time out on socket read.
+ // It could also be due to failure on SendData() above.
+ GetServerStateParser().SetCommandFailed(!ok);
+}
+
+void nsImapProtocol::Capability() {
+ ProgressEventFunctionUsingName("imapStatusCheckCompat");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" capability" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::ID() {
+ if (!gAppName[0]) return;
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" ID (\"name\" \"");
+ command.Append(gAppName);
+ command.AppendLiteral("\" \"version\" \"");
+ command.Append(gAppVersion);
+ command.AppendLiteral("\")" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::EnableUTF8Accept() {
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" ENABLE UTF8=ACCEPT" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::EnableCondStore() {
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" ENABLE CONDSTORE" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::StartCompressDeflate() {
+ // only issue a compression request if we haven't already
+ if (!TestFlag(IMAP_ISSUED_COMPRESS_REQUEST)) {
+ SetFlag(IMAP_ISSUED_COMPRESS_REQUEST);
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" COMPRESS DEFLATE" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) {
+ ParseIMAPandCheckForNewMail();
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ rv = BeginCompressing();
+ if (NS_FAILED(rv)) {
+ Log("CompressDeflate", nullptr, "failed to enable compression");
+ // we can't use this connection without compression any more, so die
+ ClearFlag(IMAP_CONNECTION_IS_OPEN);
+ TellThreadToDie();
+ SetConnectionStatus(rv);
+ return;
+ }
+ }
+ }
+ }
+}
+
+nsresult nsImapProtocol::BeginCompressing() {
+ // wrap the streams in compression layers that compress or decompress
+ // all traffic.
+ RefPtr<nsMsgCompressIStream> new_in = new nsMsgCompressIStream();
+ if (!new_in) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv = new_in->InitInputStream(m_inputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_inputStream = new_in;
+
+ RefPtr<nsMsgCompressOStream> new_out = new nsMsgCompressOStream();
+ if (!new_out) return NS_ERROR_OUT_OF_MEMORY;
+
+ rv = new_out->InitOutputStream(m_outputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_outputStream = new_out;
+ return rv;
+}
+
+void nsImapProtocol::Language() {
+ // only issue the language request if we haven't done so already...
+ if (!TestFlag(IMAP_ISSUED_LANGUAGE_REQUEST)) {
+ SetFlag(IMAP_ISSUED_LANGUAGE_REQUEST);
+ ProgressEventFunctionUsingName("imapStatusCheckCompat");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ // extract the desired language attribute from prefs
+ nsresult rv = NS_OK;
+
+ // we need to parse out the first language out of this comma separated
+ // list.... i.e if we have en,ja we only want to send en to the server.
+ if (mAcceptLanguages.get()) {
+ nsAutoCString extractedLanguage;
+ LossyCopyUTF16toASCII(mAcceptLanguages, extractedLanguage);
+ int32_t pos = extractedLanguage.FindChar(',');
+ if (pos > 0) // we have a comma separated list of languages...
+ extractedLanguage.SetLength(pos); // truncate everything after the
+ // first comma (including the comma)
+
+ if (extractedLanguage.IsEmpty()) return;
+
+ command.AppendLiteral(" LANGUAGE ");
+ command.Append(extractedLanguage);
+ command.Append(CRLF);
+
+ rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv))
+ ParseIMAPandCheckForNewMail(nullptr, true /* ignore bad or no result from the server for this command */);
+ }
+ }
+}
+
+void nsImapProtocol::EscapeUserNamePasswordString(const char* strToEscape,
+ nsCString* resultStr) {
+ if (strToEscape) {
+ uint32_t i = 0;
+ uint32_t escapeStrlen = strlen(strToEscape);
+ for (i = 0; i < escapeStrlen; i++) {
+ if (strToEscape[i] == '\\' || strToEscape[i] == '\"') {
+ resultStr->Append('\\');
+ }
+ resultStr->Append(strToEscape[i]);
+ }
+ }
+}
+
+void nsImapProtocol::InitPrefAuthMethods(int32_t authMethodPrefValue,
+ nsIMsgIncomingServer* aServer) {
+ // for m_prefAuthMethods, using the same flags as server capabilities.
+ switch (authMethodPrefValue) {
+ case nsMsgAuthMethod::none:
+ m_prefAuthMethods = kHasAuthNoneCapability;
+ break;
+ case nsMsgAuthMethod::old:
+ m_prefAuthMethods = kHasAuthOldLoginCapability;
+ break;
+ case nsMsgAuthMethod::passwordCleartext:
+ m_prefAuthMethods = kHasAuthOldLoginCapability | kHasAuthLoginCapability |
+ kHasAuthPlainCapability;
+ break;
+ case nsMsgAuthMethod::passwordEncrypted:
+ m_prefAuthMethods = kHasCRAMCapability;
+ break;
+ case nsMsgAuthMethod::NTLM:
+ m_prefAuthMethods = kHasAuthNTLMCapability | kHasAuthMSNCapability;
+ break;
+ case nsMsgAuthMethod::GSSAPI:
+ m_prefAuthMethods = kHasAuthGssApiCapability;
+ break;
+ case nsMsgAuthMethod::External:
+ m_prefAuthMethods = kHasAuthExternalCapability;
+ break;
+ case nsMsgAuthMethod::secure:
+ m_prefAuthMethods = kHasCRAMCapability | kHasAuthGssApiCapability |
+ kHasAuthNTLMCapability | kHasAuthMSNCapability;
+ break;
+ case nsMsgAuthMethod::OAuth2:
+ m_prefAuthMethods = kHasXOAuth2Capability;
+ break;
+ default:
+ NS_ASSERTION(false, "IMAP: authMethod pref invalid");
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("IMAP: bad pref authMethod = %d", authMethodPrefValue));
+ // fall to any
+ [[fallthrough]];
+ case nsMsgAuthMethod::anything:
+ m_prefAuthMethods = kHasAuthOldLoginCapability | kHasAuthLoginCapability |
+ kHasAuthPlainCapability | kHasCRAMCapability |
+ kHasAuthGssApiCapability | kHasAuthNTLMCapability |
+ kHasAuthMSNCapability | kHasAuthExternalCapability |
+ kHasXOAuth2Capability;
+ break;
+ }
+
+ if (m_prefAuthMethods & kHasXOAuth2Capability) {
+ mOAuth2Support = new mozilla::mailnews::OAuth2ThreadHelper(aServer);
+ if (!mOAuth2Support || !mOAuth2Support->SupportsOAuth2()) {
+ // Disable OAuth2 support if we don't have the prefs installed.
+ m_prefAuthMethods &= ~kHasXOAuth2Capability;
+ mOAuth2Support = nullptr;
+ MOZ_LOG(IMAP, LogLevel::Warning,
+ ("IMAP: no OAuth2 support for this server."));
+ }
+ }
+}
+
+/**
+ * Changes m_currentAuthMethod to pick the best remaining one
+ * which is allowed by server and prefs and not marked failed.
+ * The order of preference and trying of auth methods is encoded here.
+ */
+nsresult nsImapProtocol::ChooseAuthMethod() {
+ eIMAPCapabilityFlags serverCaps = GetServerStateParser().GetCapabilityFlag();
+ eIMAPCapabilityFlags availCaps =
+ serverCaps & m_prefAuthMethods & ~m_failedAuthMethods;
+
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("IMAP auth: server caps 0x%" PRIx64 ", pref 0x%" PRIx64
+ ", failed 0x%" PRIx64 ", avail caps 0x%" PRIx64,
+ serverCaps, m_prefAuthMethods, m_failedAuthMethods, availCaps));
+ // clang-format off
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("(GSSAPI = 0x%" PRIx64 ", CRAM = 0x%" PRIx64 ", NTLM = 0x%" PRIx64
+ ", MSN = 0x%" PRIx64 ", PLAIN = 0x%" PRIx64 ", LOGIN = 0x%" PRIx64
+ ", old-style IMAP login = 0x%" PRIx64
+ ", auth external IMAP login = 0x%" PRIx64 ", OAUTH2 = 0x%" PRIx64 ")",
+ kHasAuthGssApiCapability, kHasCRAMCapability, kHasAuthNTLMCapability,
+ kHasAuthMSNCapability, kHasAuthPlainCapability, kHasAuthLoginCapability,
+ kHasAuthOldLoginCapability, kHasAuthExternalCapability,
+ kHasXOAuth2Capability));
+ // clang-format on
+
+ if (kHasAuthExternalCapability & availCaps)
+ m_currentAuthMethod = kHasAuthExternalCapability;
+ else if (kHasAuthGssApiCapability & availCaps)
+ m_currentAuthMethod = kHasAuthGssApiCapability;
+ else if (kHasCRAMCapability & availCaps)
+ m_currentAuthMethod = kHasCRAMCapability;
+ else if (kHasAuthNTLMCapability & availCaps)
+ m_currentAuthMethod = kHasAuthNTLMCapability;
+ else if (kHasAuthMSNCapability & availCaps)
+ m_currentAuthMethod = kHasAuthMSNCapability;
+ else if (kHasXOAuth2Capability & availCaps)
+ m_currentAuthMethod = kHasXOAuth2Capability;
+ else if (kHasAuthPlainCapability & availCaps)
+ m_currentAuthMethod = kHasAuthPlainCapability;
+ else if (kHasAuthLoginCapability & availCaps)
+ m_currentAuthMethod = kHasAuthLoginCapability;
+ else if (kHasAuthOldLoginCapability & availCaps)
+ m_currentAuthMethod = kHasAuthOldLoginCapability;
+ else {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("No remaining auth method"));
+ m_currentAuthMethod = kCapabilityUndefined;
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("Trying auth method 0x%" PRIx64, m_currentAuthMethod));
+ return NS_OK;
+}
+
+void nsImapProtocol::MarkAuthMethodAsFailed(
+ eIMAPCapabilityFlags failedAuthMethod) {
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("Marking auth method 0x%" PRIx64 " failed", failedAuthMethod));
+ m_failedAuthMethods |= failedAuthMethod;
+}
+
+/**
+ * Start over, trying all auth methods again
+ */
+void nsImapProtocol::ResetAuthMethods() {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("resetting (failed) auth methods"));
+ m_currentAuthMethod = kCapabilityUndefined;
+ m_failedAuthMethods = 0;
+}
+
+nsresult nsImapProtocol::SendDataParseIMAPandCheckForNewMail(
+ const char* aData, const char* aCommand) {
+ nsresult rv;
+ bool isResend = false;
+ while (true) {
+ // Send authentication string (true: suppress logging the string).
+ rv = SendData(aData, true);
+ if (NS_FAILED(rv)) break;
+ ParseIMAPandCheckForNewMail(aCommand);
+ if (!GetServerStateParser().WaitingForMoreClientInput()) break;
+
+ // The server is asking for the authentication string again. So we send
+ // the same string again although we know that it might be rejected again.
+ // We do that to get a firm authentication failure instead of a resend
+ // request. That keeps things in order before failing authentication and
+ // trying another method if capable.
+ if (isResend) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ isResend = true;
+ }
+
+ return rv;
+}
+
+nsresult nsImapProtocol::ClientID() {
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+ command += " CLIENTID UUID ";
+ command += m_clientId;
+ command += CRLF;
+ nsresult rv = SendDataParseIMAPandCheckForNewMail(command.get(), nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!GetServerStateParser().LastCommandSuccessful()) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+nsresult nsImapProtocol::AuthLogin(const char* userName,
+ const nsString& aPassword,
+ eIMAPCapabilityFlag flag) {
+ ProgressEventFunctionUsingName("imapStatusSendingAuthLogin");
+ IncrementCommandTagNumber();
+
+ char* currentCommand = nullptr;
+ nsresult rv;
+ NS_ConvertUTF16toUTF8 password(aPassword);
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("IMAP: trying auth method 0x%" PRIx64, m_currentAuthMethod));
+
+ if (flag & kHasAuthExternalCapability) {
+ char* base64UserName = PL_Base64Encode(userName, strlen(userName), nullptr);
+ nsAutoCString command(GetServerCommandTag());
+ command.AppendLiteral(" authenticate EXTERNAL ");
+ command.Append(base64UserName);
+ command.Append(CRLF);
+ PR_Free(base64UserName);
+ rv = SendData(command.get());
+ ParseIMAPandCheckForNewMail();
+ nsImapServerResponseParser& parser = GetServerStateParser();
+ if (parser.LastCommandSuccessful()) return NS_OK;
+ parser.SetCapabilityFlag(parser.GetCapabilityFlag() &
+ ~kHasAuthExternalCapability);
+ } else if (flag & kHasCRAMCapability) {
+ NS_ENSURE_TRUE(m_imapServerSink, NS_ERROR_NULL_POINTER);
+ MOZ_LOG(IMAP, LogLevel::Debug, ("MD5 auth"));
+ // inform the server that we want to begin a CRAM authentication
+ // procedure...
+ nsAutoCString command(GetServerCommandTag());
+ command.AppendLiteral(" authenticate CRAM-MD5" CRLF);
+ rv = SendData(command.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail();
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ char* digest = nullptr;
+ char* cramDigest = GetServerStateParser().fAuthChallenge;
+ char* decodedChallenge =
+ PL_Base64Decode(cramDigest, strlen(cramDigest), nullptr);
+ rv = m_imapServerSink->CramMD5Hash(decodedChallenge, password.get(),
+ &digest);
+ PR_Free(decodedChallenge);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(digest, NS_ERROR_NULL_POINTER);
+ // The encoded digest is the hexadecimal representation of
+ // DIGEST_LENGTH characters, so it will be twice that length.
+ nsAutoCStringN<2 * DIGEST_LENGTH> encodedDigest;
+
+ for (uint32_t j = 0; j < DIGEST_LENGTH; j++) {
+ char hexVal[3];
+ PR_snprintf(hexVal, 3, "%.2x", 0x0ff & (unsigned short)(digest[j]));
+ encodedDigest.Append(hexVal);
+ }
+
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%.255s %s", userName,
+ encodedDigest.get());
+ char* base64Str =
+ PL_Base64Encode(m_dataOutputBuf, strlen(m_dataOutputBuf), nullptr);
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str);
+ PR_Free(base64Str);
+ PR_Free(digest);
+ rv = SendData(m_dataOutputBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail(command.get());
+ }
+ } // if CRAM response was received
+ else if (flag & kHasAuthGssApiCapability) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("MD5 auth"));
+
+ // Only try GSSAPI once - if it fails, its going to be because we don't
+ // have valid credentials
+ // MarkAuthMethodAsFailed(kHasAuthGssApiCapability);
+
+ // We do step1 first, so we don't try GSSAPI against a server which
+ // we can't get credentials for.
+ nsAutoCString response;
+
+ nsAutoCString service("imap@");
+ service.Append(m_hostName);
+ rv = DoGSSAPIStep1(service, userName, response);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString command(GetServerCommandTag());
+ command.AppendLiteral(" authenticate GSSAPI" CRLF);
+ rv = SendData(command.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ParseIMAPandCheckForNewMail("AUTH GSSAPI");
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ response += CRLF;
+ rv = SendData(response.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail(command.get());
+ nsresult gssrv = NS_OK;
+
+ while (GetServerStateParser().LastCommandSuccessful() &&
+ NS_SUCCEEDED(gssrv) && gssrv != NS_SUCCESS_AUTH_FINISHED) {
+ nsCString challengeStr(GetServerStateParser().fAuthChallenge);
+ gssrv = DoGSSAPIStep2(challengeStr, response);
+ if (NS_SUCCEEDED(gssrv)) {
+ response += CRLF;
+ rv = SendData(response.get());
+ } else
+ rv = SendData("*" CRLF);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail(command.get());
+ }
+ // TODO: whether it worked or not is shown by LastCommandSuccessful(), not
+ // gssrv, right?
+ }
+ } else if (flag & (kHasAuthNTLMCapability | kHasAuthMSNCapability)) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("NTLM auth"));
+ nsAutoCString command(GetServerCommandTag());
+ command.Append((flag & kHasAuthNTLMCapability) ? " authenticate NTLM" CRLF
+ : " authenticate MSN" CRLF);
+ rv = SendData(command.get());
+ ParseIMAPandCheckForNewMail(
+ "AUTH NTLM"); // this just waits for ntlm step 1
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsAutoCString cmd;
+ rv = DoNtlmStep1(nsDependentCString(userName), aPassword, cmd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ cmd += CRLF;
+ rv = SendData(cmd.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail(command.get());
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsCString challengeStr(GetServerStateParser().fAuthChallenge);
+ nsCString response;
+ rv = DoNtlmStep2(challengeStr, response);
+ NS_ENSURE_SUCCESS(rv, rv);
+ response += CRLF;
+ rv = SendData(response.get());
+ ParseIMAPandCheckForNewMail(command.get());
+ }
+ }
+ } else if (flag & kHasAuthPlainCapability) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("PLAIN auth"));
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE,
+ "%s authenticate PLAIN" CRLF, GetServerCommandTag());
+ rv = SendData(m_dataOutputBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ currentCommand = PL_strdup(
+ m_dataOutputBuf); /* StrAllocCopy(currentCommand, GetOutputBuffer()); */
+ ParseIMAPandCheckForNewMail();
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ // RFC 4616
+ char plain_string[513];
+ memset(plain_string, 0, 513);
+ PR_snprintf(&plain_string[1], 256, "%.255s", userName);
+ uint32_t len = std::min<uint32_t>(PL_strlen(userName), 255u) +
+ 2; // We include two <NUL> characters.
+ PR_snprintf(&plain_string[len], 256, "%.255s", password.get());
+ len += std::min<uint32_t>(password.Length(), 255u);
+ char* base64Str = PL_Base64Encode(plain_string, len, nullptr);
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str);
+ PR_Free(base64Str);
+
+ rv = SendDataParseIMAPandCheckForNewMail(m_dataOutputBuf, currentCommand);
+ } // if the last command succeeded
+ } // if auth plain capability
+ else if (flag & kHasAuthLoginCapability) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("LOGIN auth"));
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE,
+ "%s authenticate LOGIN" CRLF, GetServerCommandTag());
+ rv = SendData(m_dataOutputBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ currentCommand = PL_strdup(m_dataOutputBuf);
+ ParseIMAPandCheckForNewMail();
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ char* base64Str = PL_Base64Encode(
+ userName, std::min<uint32_t>(PL_strlen(userName), 255u), nullptr);
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str);
+ PR_Free(base64Str);
+ rv = SendData(m_dataOutputBuf, true /* suppress logging */);
+ if (NS_SUCCEEDED(rv)) {
+ ParseIMAPandCheckForNewMail(currentCommand);
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ base64Str = PL_Base64Encode(
+ password.get(), std::min<uint32_t>(password.Length(), 255u),
+ nullptr);
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF,
+ base64Str);
+ PR_Free(base64Str);
+ rv = SendData(m_dataOutputBuf, true /* suppress logging */);
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(currentCommand);
+ } // if last command successful
+ } // if last command successful
+ } // if last command successful
+ } // if has auth login capability
+ else if (flag & kHasAuthOldLoginCapability) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("old-style auth"));
+ ProgressEventFunctionUsingName("imapStatusSendingLogin");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+ nsAutoCString escapedUserName;
+ command.AppendLiteral(" login \"");
+ EscapeUserNamePasswordString(userName, &escapedUserName);
+ command.Append(escapedUserName);
+ command.AppendLiteral("\" \"");
+
+ // if the password contains a \, login will fail
+ // turn foo\bar into foo\\bar
+ nsAutoCString correctedPassword;
+ // We're assuming old style login doesn't want UTF-8
+ EscapeUserNamePasswordString(NS_LossyConvertUTF16toASCII(aPassword).get(),
+ &correctedPassword);
+ command.Append(correctedPassword);
+ command.AppendLiteral("\"" CRLF);
+ rv = SendData(command.get(), true /* suppress logging */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ParseIMAPandCheckForNewMail();
+ } else if (flag & kHasXOAuth2Capability) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("XOAUTH2 auth"));
+
+ // Get the XOAuth2 base64 string.
+ NS_ASSERTION(mOAuth2Support,
+ "What are we doing here without OAuth2 helper?");
+ if (!mOAuth2Support) return NS_ERROR_UNEXPECTED;
+ nsAutoCString base64Str;
+ mOAuth2Support->GetXOAuth2String(base64Str);
+ mOAuth2Support = nullptr; // Its purpose has been served.
+ if (base64Str.IsEmpty()) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("OAuth2 failed"));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Send the data on the network.
+ nsAutoCString command(GetServerCommandTag());
+ command += " AUTHENTICATE XOAUTH2 ";
+ command += base64Str;
+ command += CRLF;
+
+ rv = SendDataParseIMAPandCheckForNewMail(command.get(), nullptr);
+ } else if (flag & kHasAuthNoneCapability) {
+ // TODO What to do? "login <username>" like POP?
+ return NS_ERROR_NOT_IMPLEMENTED;
+ } else {
+ MOZ_LOG(IMAP, LogLevel::Error, ("flags param has no auth scheme selected"));
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ PR_Free(currentCommand);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetServerStateParser().LastCommandSuccessful() ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+void nsImapProtocol::OnLSubFolders() {
+ // **** use to find out whether Drafts, Sent, & Templates folder
+ // exists or not even the user didn't subscribe to it
+ char* mailboxName = OnCreateServerSourceFolderPathString();
+ if (mailboxName) {
+ ProgressEventFunctionUsingName("imapStatusLookingForMailbox");
+ IncrementCommandTagNumber();
+ PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s list \"\" \"%s\"" CRLF,
+ GetServerCommandTag(), mailboxName);
+ nsresult rv = SendData(m_dataOutputBuf);
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+ PR_Free(mailboxName);
+ } else {
+ HandleMemoryFailure();
+ }
+}
+
+void nsImapProtocol::OnAppendMsgFromFile() {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_OK;
+ rv = m_runningUrl->GetMsgFile(getter_AddRefs(file));
+ if (NS_SUCCEEDED(rv) && file) {
+ char* mailboxName = OnCreateServerSourceFolderPathString();
+ if (mailboxName) {
+ imapMessageFlagsType flagsToSet = 0;
+ uint32_t msgFlags = 0;
+ PRTime date = 0;
+ nsCString keywords;
+ if (m_imapMessageSink)
+ m_imapMessageSink->GetCurMoveCopyMessageInfo(m_runningUrl, &date,
+ keywords, &msgFlags);
+
+ if (msgFlags & nsMsgMessageFlags::Read) flagsToSet |= kImapMsgSeenFlag;
+ if (msgFlags & nsMsgMessageFlags::MDNReportSent)
+ flagsToSet |= kImapMsgMDNSentFlag;
+ // convert msg flag label (0xE000000) to imap flag label (0x0E00)
+ if (msgFlags & nsMsgMessageFlags::Labels)
+ flagsToSet |= (msgFlags & nsMsgMessageFlags::Labels) >> 16;
+ if (msgFlags & nsMsgMessageFlags::Marked)
+ flagsToSet |= kImapMsgFlaggedFlag;
+ if (msgFlags & nsMsgMessageFlags::Replied)
+ flagsToSet |= kImapMsgAnsweredFlag;
+ if (msgFlags & nsMsgMessageFlags::Forwarded)
+ flagsToSet |= kImapMsgForwardedFlag;
+
+ // If the message copied was a draft, flag it as such
+ nsImapAction imapAction;
+ rv = m_runningUrl->GetImapAction(&imapAction);
+ if (NS_SUCCEEDED(rv) &&
+ (imapAction == nsIImapUrl::nsImapAppendDraftFromFile))
+ flagsToSet |= kImapMsgDraftFlag;
+ UploadMessageFromFile(file, mailboxName, date, flagsToSet, keywords);
+ PR_Free(mailboxName);
+ } else {
+ HandleMemoryFailure();
+ }
+ }
+}
+
+void nsImapProtocol::UploadMessageFromFile(nsIFile* file,
+ const char* mailboxName, PRTime date,
+ imapMessageFlagsType flags,
+ nsCString& keywords) {
+ if (!file || !mailboxName) return;
+ IncrementCommandTagNumber();
+
+ int64_t fileSize = 0;
+ int64_t totalSize;
+ uint32_t readCount;
+ char* dataBuffer = nullptr;
+ nsCString command(GetServerCommandTag());
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+ nsresult rv;
+ bool urlOk = false;
+ nsCString flagString;
+
+ nsCOMPtr<nsIInputStream> fileInputStream;
+
+ if (!escapedName.IsEmpty()) {
+ command.AppendLiteral(" append \"");
+ command.Append(escapedName);
+ command.Append('"');
+ if (flags || keywords.Length()) {
+ command.AppendLiteral(" (");
+
+ if (flags) {
+ SetupMessageFlagsString(flagString, flags,
+ GetServerStateParser().SupportsUserFlags());
+ command.Append(flagString);
+ }
+ if (keywords.Length()) {
+ if (flags) command.Append(' ');
+ command.Append(keywords);
+ }
+ command.Append(')');
+ }
+
+ // date should never be 0, but just in case...
+ if (date) {
+ /* Use PR_FormatTimeUSEnglish() to format the date in US English format,
+ then figure out what our local GMT offset is, and append it (since
+ PR_FormatTimeUSEnglish() can't do that.) Generate four digit years as
+ per RFC 1123 (superseding RFC 822.)
+ */
+ char szDateTime[64];
+ char dateStr[100];
+ PRExplodedTime exploded;
+ PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(szDateTime, sizeof(szDateTime),
+ "%d-%b-%Y %H:%M:%S", &exploded);
+ PRExplodedTime now;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);
+ int gmtoffset =
+ (now.tm_params.tp_gmt_offset + now.tm_params.tp_dst_offset) / 60;
+ PR_snprintf(dateStr, sizeof(dateStr), " \"%s %c%02d%02d\"", szDateTime,
+ (gmtoffset >= 0 ? '+' : '-'),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) / 60),
+ ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) % 60));
+
+ command.Append(dateStr);
+ }
+ if (m_allowUTF8Accept)
+ command.AppendLiteral(" UTF8 (~{");
+ else
+ command.AppendLiteral(" {");
+
+ dataBuffer = (char*)PR_CALLOC(COPY_BUFFER_SIZE + 1);
+ if (!dataBuffer) goto done;
+ rv = file->GetFileSize(&fileSize);
+ NS_ASSERTION(fileSize, "got empty file in UploadMessageFromFile");
+ if (NS_FAILED(rv) || !fileSize) goto done;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file);
+ if (NS_FAILED(rv) || !fileInputStream) goto done;
+ command.AppendInt((int32_t)fileSize);
+
+ // Set useLiteralPlus to true if server has capability LITERAL+ and
+ // LITERAL+ usage is enabled in the config editor,
+ // i.e., "mail.imap.use_literal_plus" = true.
+ bool useLiteralPlus =
+ (GetServerStateParser().GetCapabilityFlag() & kLiteralPlusCapability) &&
+ gUseLiteralPlus;
+ if (useLiteralPlus)
+ command.AppendLiteral("+}" CRLF);
+ else
+ command.AppendLiteral("}" CRLF);
+
+ rv = SendData(command.get());
+ if (NS_FAILED(rv)) goto done;
+
+ if (!useLiteralPlus) {
+ ParseIMAPandCheckForNewMail();
+ if (!GetServerStateParser().LastCommandSuccessful()) goto done;
+ }
+
+ totalSize = fileSize;
+ readCount = 0;
+ while (NS_SUCCEEDED(rv) && totalSize > 0) {
+ if (DeathSignalReceived()) goto done;
+ rv = fileInputStream->Read(dataBuffer, COPY_BUFFER_SIZE, &readCount);
+ if (NS_SUCCEEDED(rv) && !readCount) rv = NS_ERROR_FAILURE;
+
+ if (NS_SUCCEEDED(rv)) {
+ NS_ASSERTION(readCount <= (uint32_t)totalSize,
+ "got more bytes than there should be");
+ dataBuffer[readCount] = 0;
+ rv = SendData(dataBuffer);
+ totalSize -= readCount;
+ PercentProgressUpdateEvent(""_ns, u""_ns, fileSize - totalSize,
+ fileSize);
+ if (!totalSize) {
+ // The full message has been queued for sending, but the actual send
+ // is just now starting and can still potentially fail. From this
+ // point the progress cannot be determined, so just set the progress
+ // to "indeterminate" so that the user does not see an incorrect 100%
+ // complete on the progress bar while waiting for the retry dialog to
+ // appear if the send should fail.
+ m_lastProgressTime = 0; // Force progress bar update
+ m_lastPercent = -1; // Force progress bar update
+ PercentProgressUpdateEvent(""_ns, u""_ns, 0, -1); // Indeterminate
+ }
+ }
+ } // end while appending chunks
+
+ if (NS_SUCCEEDED(rv)) { // complete the append
+ if (m_allowUTF8Accept)
+ rv = SendData(")" CRLF);
+ else
+ rv = SendData(CRLF);
+ if (NS_FAILED(rv)) goto done;
+
+ ParseIMAPandCheckForNewMail(command.get());
+ if (!GetServerStateParser().LastCommandSuccessful()) goto done;
+
+ // If reached, the append completed without error. No more goto's!
+ // May still find problems in imap responses below so urlOk may still
+ // become false.
+ urlOk = true;
+
+ nsImapAction imapAction;
+ m_runningUrl->GetImapAction(&imapAction);
+
+ if (imapAction == nsIImapUrl::nsImapAppendDraftFromFile ||
+ imapAction == nsIImapUrl::nsImapAppendMsgFromFile) {
+ if (GetServerStateParser().GetCapabilityFlag() & kUidplusCapability) {
+ nsMsgKey newKey = GetServerStateParser().CurrentResponseUID();
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->SetAppendMsgUid(newKey, m_runningUrl);
+
+ // Courier imap server seems to have problems with recently
+ // appended messages. Noop seems to clear its confusion.
+ if (FolderIsSelected(mailboxName)) Noop();
+
+ nsCString oldMsgId;
+ rv = m_runningUrl->GetListOfMessageIds(oldMsgId);
+ if (NS_SUCCEEDED(rv) && !oldMsgId.IsEmpty()) {
+ bool idsAreUids = true;
+ m_runningUrl->MessageIdsAreUids(&idsAreUids);
+ Store(oldMsgId, "+FLAGS (\\Deleted)", idsAreUids);
+ UidExpunge(oldMsgId);
+ }
+ // Only checks the last imap command in sequence above.
+ if (!GetServerStateParser().LastCommandSuccessful()) urlOk = false;
+ }
+ // for non UIDPLUS servers this code used to check for
+ // imapAction==nsIImapUrl::nsImapAppendMsgFromFile, which meant we'd get
+ // into this code whenever sending a message, as well as when copying
+ // messages to an imap folder from local folders or an other imap
+ // server. This made sending a message slow when there was a large sent
+ // folder. I don't believe this code worked anyway.
+ // *** code me to search for the newly appended message
+ else if (m_imapMailFolderSink &&
+ imapAction == nsIImapUrl::nsImapAppendDraftFromFile) {
+ // go to selected state
+ nsCString messageId;
+ rv = m_imapMailFolderSink->GetMessageId(m_runningUrl, messageId);
+ if (NS_SUCCEEDED(rv) && !messageId.IsEmpty()) {
+ // if the appended to folder isn't selected in the connection,
+ // select it.
+ if (!FolderIsSelected(mailboxName))
+ SelectMailbox(mailboxName);
+ else
+ Noop(); // See if this makes SEARCH work on the newly appended
+ // msg.
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ command = "SEARCH UNDELETED HEADER Message-ID ";
+ command.Append(messageId);
+
+ // Clean up result sequence before issuing the cmd.
+ GetServerStateParser().ResetSearchResultSequence();
+
+ Search(command.get(), true, false);
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ nsMsgKey newkey = nsMsgKey_None;
+ nsImapSearchResultIterator* searchResult =
+ GetServerStateParser().CreateSearchResultIterator();
+ newkey = searchResult->GetNextMessageNumber();
+ delete searchResult;
+ if (newkey != nsMsgKey_None)
+ m_imapMailFolderSink->SetAppendMsgUid(newkey, m_runningUrl);
+ } else
+ urlOk = false;
+ } else
+ urlOk = false;
+ }
+ }
+ }
+ }
+ }
+done:
+ // If imap command fails or network goes down, make sure URL sees the failure.
+ if (!urlOk) GetServerStateParser().SetCommandFailed(true);
+
+ PR_Free(dataBuffer);
+ if (fileInputStream) fileInputStream->Close();
+}
+
+// caller must free using PR_Free
+char* nsImapProtocol::OnCreateServerSourceFolderPathString() {
+ char* sourceMailbox = nullptr;
+ char hierarchyDelimiter = 0;
+ char onlineDelimiter = 0;
+ m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter);
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter);
+
+ if (onlineDelimiter != kOnlineHierarchySeparatorUnknown &&
+ onlineDelimiter != hierarchyDelimiter)
+ m_runningUrl->SetOnlineSubDirSeparator(onlineDelimiter);
+
+ m_runningUrl->CreateServerSourceFolderPathString(&sourceMailbox);
+
+ return sourceMailbox;
+}
+
+// caller must free using PR_Free, safe to call from ui thread
+char* nsImapProtocol::GetFolderPathString() {
+ char* sourceMailbox = nullptr;
+ char onlineSubDirDelimiter = 0;
+ char hierarchyDelimiter = 0;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+
+ m_runningUrl->GetOnlineSubDirSeparator(&onlineSubDirDelimiter);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ mailnewsUrl->GetFolder(getter_AddRefs(msgFolder));
+ if (msgFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder);
+ if (imapFolder) {
+ imapFolder->GetHierarchyDelimiter(&hierarchyDelimiter);
+ if (hierarchyDelimiter != kOnlineHierarchySeparatorUnknown &&
+ onlineSubDirDelimiter != hierarchyDelimiter)
+ m_runningUrl->SetOnlineSubDirSeparator(hierarchyDelimiter);
+ }
+ }
+ m_runningUrl->CreateServerSourceFolderPathString(&sourceMailbox);
+
+ return sourceMailbox;
+}
+
+nsresult nsImapProtocol::CreateServerSourceFolderPathString(char** result) {
+ NS_ENSURE_ARG(result);
+ *result = OnCreateServerSourceFolderPathString();
+ return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+// caller must free using PR_Free
+char* nsImapProtocol::OnCreateServerDestinationFolderPathString() {
+ char* destinationMailbox = nullptr;
+ char hierarchyDelimiter = 0;
+ char onlineDelimiter = 0;
+ m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter);
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter);
+ if (onlineDelimiter != kOnlineHierarchySeparatorUnknown &&
+ onlineDelimiter != hierarchyDelimiter)
+ m_runningUrl->SetOnlineSubDirSeparator(onlineDelimiter);
+
+ m_runningUrl->CreateServerDestinationFolderPathString(&destinationMailbox);
+
+ return destinationMailbox;
+}
+
+void nsImapProtocol::OnCreateFolder(const char* aSourceMailbox) {
+ bool created = CreateMailboxRespectingSubscriptions(aSourceMailbox);
+ if (created) {
+ m_hierarchyNameState = kListingForCreate;
+ nsCString mailboxWODelim(aSourceMailbox);
+ RemoveHierarchyDelimiter(mailboxWODelim);
+ List(mailboxWODelim.get(), false);
+ m_hierarchyNameState = kNoOperationInProgress;
+ } else
+ FolderNotCreated(aSourceMailbox);
+}
+
+void nsImapProtocol::OnEnsureExistsFolder(const char* aSourceMailbox) {
+ // We need to handle the following edge case where the destination server
+ // wasn't authenticated when the folder name was encoded in
+ // `EnsureFolderExists()'. In this case we always get MUTF-7. Here we are
+ // authenticated and can rely on `m_allowUTF8Accept'. If the folder appears
+ // to be MUTF-7 and we need UTF-8, we re-encode it. If it's not ASCII, it
+ // must be already correct in UTF-8. And if it was ASCII to start with, it
+ // doesn't matter that we MUTF-7 decode and UTF-8 re-encode.
+
+ // `aSourceMailbox' is a path with hierarchy delimiters possibly. To determine
+ // if the edge case is in effect, we only want to check the leaf node for
+ // ASCII and this is only necessary when `m_allowUTF8Accept' is true.
+
+ // `fullPath' is modified below if leaf re-encoding is necessary and it must
+ // be defined here at top level so it stays in scope.
+ nsAutoCString fullPath(aSourceMailbox);
+
+ if (m_allowUTF8Accept) {
+ char onlineDirSeparator = kOnlineHierarchySeparatorUnknown;
+ m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator);
+
+ int32_t leafStart = fullPath.RFindChar(onlineDirSeparator);
+ nsAutoCString leafName;
+ if (leafStart == kNotFound) {
+ // This is a root level mailbox
+ leafName = fullPath;
+ fullPath.SetLength(0);
+ } else {
+ leafName = Substring(fullPath, leafStart + 1);
+ fullPath.SetLength(leafStart + 1);
+ }
+
+ if (NS_IsAscii(leafName.get())) {
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("re-encode leaf of mailbox %s to UTF-8", aSourceMailbox));
+ nsAutoString utf16LeafName;
+ CopyMUTF7toUTF16(leafName, utf16LeafName);
+
+ // Convert UTF-16 to UTF-8 to create the folder.
+ nsAutoCString utf8LeafName;
+ CopyUTF16toUTF8(utf16LeafName, utf8LeafName);
+ fullPath.Append(utf8LeafName);
+ aSourceMailbox = fullPath.get();
+ MOZ_LOG(IMAP, LogLevel::Debug,
+ ("re-encoded leaf of mailbox %s to UTF-8", aSourceMailbox));
+ }
+ }
+ List(aSourceMailbox, false); // how to tell if that succeeded?
+ // If List() produces OK tagged response and an untagged "* LIST" response
+ // then the folder exists on the server. For simplicity, just look for
+ // a general untagged response.
+ bool folderExists = false;
+ if (GetServerStateParser().LastCommandSuccessful())
+ folderExists = GetServerStateParser().UntaggedResponse();
+
+ // try converting aSourceMailbox to canonical format
+ nsImapNamespace* nsForMailbox = nullptr;
+ m_hostSessionList->GetNamespaceForMailboxForHost(
+ GetImapServerKey(), aSourceMailbox, nsForMailbox);
+ // NS_ASSERTION (nsForMailbox, "Oops .. null nsForMailbox");
+
+ nsCString name;
+
+ if (nsForMailbox)
+ m_runningUrl->AllocateCanonicalPath(
+ aSourceMailbox, nsForMailbox->GetDelimiter(), getter_Copies(name));
+ else
+ m_runningUrl->AllocateCanonicalPath(
+ aSourceMailbox, kOnlineHierarchySeparatorUnknown, getter_Copies(name));
+
+ // Also check that the folder has been verified to exist in server sink.
+ bool verifiedExists = false;
+ if (folderExists && m_imapServerSink)
+ m_imapServerSink->FolderVerifiedOnline(name, &verifiedExists);
+
+ // If folder exists on server and is verified and known to exists in server
+ // sink, just subscribe the folder. Otherwise, create a new folder and
+ // then subscribe and do list again to make sure it's created.
+ if (folderExists && verifiedExists) {
+ Subscribe(aSourceMailbox);
+ } else {
+ bool created = CreateMailboxRespectingSubscriptions(aSourceMailbox);
+ if (created) {
+ List(aSourceMailbox, false);
+ // Check that we see an untagged response indicating folder now exists.
+ folderExists = GetServerStateParser().UntaggedResponse();
+ }
+ }
+ if (!GetServerStateParser().LastCommandSuccessful() || !folderExists)
+ FolderNotCreated(aSourceMailbox);
+}
+
+void nsImapProtocol::OnSubscribe(const char* sourceMailbox) {
+ Subscribe(sourceMailbox);
+}
+
+void nsImapProtocol::OnUnsubscribe(const char* sourceMailbox) {
+ // When we try to auto-unsubscribe from \Noselect folders,
+ // some servers report errors if we were already unsubscribed
+ // from them.
+ bool lastReportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ Unsubscribe(sourceMailbox);
+ GetServerStateParser().SetReportingErrors(lastReportingErrors);
+}
+
+void nsImapProtocol::RefreshACLForFolderIfNecessary(const char* mailboxName) {
+ if (GetServerStateParser().ServerHasACLCapability()) {
+ if (!m_folderNeedsACLRefreshed && m_imapMailFolderSink)
+ m_imapMailFolderSink->GetFolderNeedsACLListed(&m_folderNeedsACLRefreshed);
+ if (m_folderNeedsACLRefreshed) {
+ RefreshACLForFolder(mailboxName);
+ m_folderNeedsACLRefreshed = false;
+ }
+ }
+}
+
+void nsImapProtocol::RefreshACLForFolder(const char* mailboxName) {
+ nsImapNamespace* ns = nullptr;
+ m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(),
+ mailboxName, ns);
+ if (ns) {
+ switch (ns->GetType()) {
+ case kPersonalNamespace:
+ // It's a personal folder, most likely.
+ // I find it hard to imagine a server that supports ACL that doesn't
+ // support NAMESPACE, so most likely we KNOW that this is a personal,
+ // rather than the default, namespace.
+
+ // First, clear what we have.
+ ClearAllFolderRights();
+ // Now, get the new one.
+ GetMyRightsForFolder(mailboxName);
+ if (m_imapMailFolderSink) {
+ uint32_t aclFlags = 0;
+ if (NS_SUCCEEDED(m_imapMailFolderSink->GetAclFlags(&aclFlags)) &&
+ aclFlags & IMAP_ACL_ADMINISTER_FLAG)
+ GetACLForFolder(mailboxName);
+ }
+
+ // We're all done, refresh the icon/flags for this folder
+ RefreshFolderACLView(mailboxName, ns);
+ break;
+ default:
+ // We know it's a public folder or other user's folder.
+ // We only want our own rights
+
+ // First, clear what we have
+ ClearAllFolderRights();
+ // Now, get the new one.
+ GetMyRightsForFolder(mailboxName);
+ // We're all done, refresh the icon/flags for this folder
+ RefreshFolderACLView(mailboxName, ns);
+ break;
+ }
+ } else {
+ // no namespace, not even default... can this happen?
+ NS_ASSERTION(false, "couldn't get namespace");
+ }
+}
+
+void nsImapProtocol::RefreshFolderACLView(const char* mailboxName,
+ nsImapNamespace* nsForMailbox) {
+ nsCString canonicalMailboxName;
+
+ if (nsForMailbox)
+ m_runningUrl->AllocateCanonicalPath(mailboxName,
+ nsForMailbox->GetDelimiter(),
+ getter_Copies(canonicalMailboxName));
+ else
+ m_runningUrl->AllocateCanonicalPath(mailboxName,
+ kOnlineHierarchySeparatorUnknown,
+ getter_Copies(canonicalMailboxName));
+
+ if (m_imapServerSink)
+ m_imapServerSink->RefreshFolderRights(canonicalMailboxName);
+}
+
+void nsImapProtocol::GetACLForFolder(const char* mailboxName) {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+ command.AppendLiteral(" getacl \"");
+ command.Append(escapedName);
+ command.AppendLiteral("\"" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::OnRefreshAllACLs() {
+ m_hierarchyNameState = kListingForInfoOnly;
+ nsIMAPMailboxInfo* mb = NULL;
+
+ // This will fill in the list
+ List("*", true);
+
+ int32_t total = m_listedMailboxList.Length(), count = 0;
+ GetServerStateParser().SetReportingErrors(false);
+ for (int32_t i = 0; i < total; i++) {
+ mb = m_listedMailboxList.ElementAt(i);
+ if (mb) // paranoia
+ {
+ char* onlineName = nullptr;
+ m_runningUrl->AllocateServerPath(
+ PromiseFlatCString(mb->GetMailboxName()).get(), mb->GetDelimiter(),
+ &onlineName);
+ if (onlineName) {
+ RefreshACLForFolder(onlineName);
+ free(onlineName);
+ }
+ PercentProgressUpdateEvent(""_ns, u""_ns, count, total);
+ delete mb;
+ count++;
+ }
+ }
+ m_listedMailboxList.Clear();
+
+ PercentProgressUpdateEvent(""_ns, u""_ns, 100, 100);
+ GetServerStateParser().SetReportingErrors(true);
+ m_hierarchyNameState = kNoOperationInProgress;
+}
+
+// any state commands
+void nsImapProtocol::Logout(bool shuttingDown /* = false */,
+ bool waitForResponse /* = true */) {
+ if (!shuttingDown) ProgressEventFunctionUsingName("imapStatusLoggingOut");
+
+ /******************************************************************
+ * due to the undo functionality we cannot issue ImapClose when logout; there
+ * is no way to do an undo if the message has been permanently expunge
+ * jt - 07/12/1999
+
+ bool closeNeeded = GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected;
+
+ if (closeNeeded && GetDeleteIsMoveToTrash())
+ ImapClose();
+ ********************/
+
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" logout" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (m_transport && shuttingDown)
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5);
+ // the socket may be dead before we read the response, so drop it.
+ if (NS_SUCCEEDED(rv) && waitForResponse) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Noop() {
+ // ProgressUpdateEvent("noop...");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" noop" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::XServerInfo() {
+ ProgressEventFunctionUsingName("imapGettingServerInfo");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(
+ " XSERVERINFO MANAGEACCOUNTURL MANAGELISTSURL MANAGEFILTERSURL" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Netscape() {
+ ProgressEventFunctionUsingName("imapGettingServerInfo");
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" netscape" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::XMailboxInfo(const char* mailboxName) {
+ ProgressEventFunctionUsingName("imapGettingMailboxInfo");
+ IncrementCommandTagNumber();
+ nsCString command(GetServerCommandTag());
+
+ command.AppendLiteral(" XMAILBOXINFO \"");
+ command.Append(mailboxName);
+ command.AppendLiteral("\" MANAGEURL POSTURL" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Namespace() {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" namespace" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::MailboxData() {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" mailboxdata" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::GetMyRightsForFolder(const char* mailboxName) {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+
+ if (MailboxIsNoSelectMailbox(escapedName.get()))
+ return; // Don't issue myrights on Noselect folder
+
+ command.AppendLiteral(" myrights \"");
+ command.Append(escapedName);
+ command.AppendLiteral("\"" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+bool nsImapProtocol::FolderIsSelected(const char* mailboxName) {
+ return (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected &&
+ GetServerStateParser().GetSelectedMailboxName() &&
+ PL_strcmp(GetServerStateParser().GetSelectedMailboxName(),
+ mailboxName) == 0);
+}
+
+void nsImapProtocol::OnStatusForFolder(const char* mailboxName) {
+ bool untaggedResponse;
+ // RFC 3501 says:
+ // "the STATUS command SHOULD NOT be used on the currently selected mailbox",
+ // so use NOOP instead if mailboxName is the selected folder on this
+ // connection.
+ if (FolderIsSelected(mailboxName)) {
+ Noop();
+ // Did untagged responses occur during the NOOP response? If so, this
+ // indicates new mail or other changes in the mailbox. Handle this like an
+ // IDLE response which will cause a folder update.
+ if ((untaggedResponse = GetServerStateParser().UntaggedResponse()) &&
+ m_imapMailFolderSinkSelected) {
+ Log("OnStatusForFolder", nullptr,
+ "mailbox change on selected folder during noop");
+ m_imapMailFolderSinkSelected->OnNewIdleMessages();
+ }
+ mailboxName = nullptr; // for new_spec below. Obtain SELECTed mailbox data.
+ } else {
+ // Imap connection is not in selected state or imap connection is selected
+ // on a mailbox other than than the mailbox folderstatus URL is requesting
+ // status for.
+ untaggedResponse = true; // STATUS always produces an untagged response
+ IncrementCommandTagNumber();
+
+ nsAutoCString command(GetServerCommandTag());
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+
+ command.AppendLiteral(" STATUS \"");
+ command.Append(escapedName);
+ command.AppendLiteral("\" (UIDNEXT MESSAGES UNSEEN RECENT)" CRLF);
+
+ int32_t prevNumMessages = GetServerStateParser().NumberOfMessages();
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+
+ // Respond to possible untagged responses EXISTS and RECENT for the SELECTed
+ // folder. Handle as though this were an IDLE response. Can't check for any
+ // untagged as for Noop() above since STATUS always produces an untagged
+ // response for the target mailbox and possibly also for the SELECTed box.
+ // Of cource, this won't occur if imap connection is not in selected state.
+ if (GetServerStateParser().GetIMAPstate() ==
+ nsImapServerResponseParser::kFolderSelected &&
+ m_imapMailFolderSinkSelected &&
+ (GetServerStateParser().NumberOfRecentMessages() ||
+ prevNumMessages != GetServerStateParser().NumberOfMessages())) {
+ Log("OnStatusForFolder", nullptr,
+ "new mail on selected folder during status");
+ m_imapMailFolderSinkSelected->OnNewIdleMessages();
+ }
+ MOZ_ASSERT(m_imapMailFolderSink != m_imapMailFolderSinkSelected);
+ }
+
+ // Do this to ensure autosync detects changes in server counts and thus
+ // triggers a full body fetch for when NOOP or STATUS is sent above.
+ // But if NOOP didn't produce an untagged response, no need to do this.
+ // Note: For SELECTed noop() above, "folder sink" and "folder sink selected"
+ // both reference the same folder but are not always equal. So OK to use
+ // m_imapMailFolderSink below since it is correct for NOOP and STATUS cases.
+ if (untaggedResponse && GetServerStateParser().LastCommandSuccessful()) {
+ RefPtr<nsImapMailboxSpec> new_spec =
+ GetServerStateParser().CreateCurrentMailboxSpec(mailboxName);
+ if (new_spec && m_imapMailFolderSink)
+ m_imapMailFolderSink->UpdateImapMailboxStatus(this, new_spec);
+ }
+}
+
+void nsImapProtocol::OnListFolder(const char* aSourceMailbox, bool aBool) {
+ List(aSourceMailbox, aBool);
+}
+
+// Returns true if the mailbox is a NoSelect mailbox.
+// If we don't know about it, returns false.
+bool nsImapProtocol::MailboxIsNoSelectMailbox(const char* mailboxName) {
+ bool rv = false;
+
+ nsImapNamespace* nsForMailbox = nullptr;
+ m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(),
+ mailboxName, nsForMailbox);
+ // NS_ASSERTION (nsForMailbox, "Oops .. null nsForMailbox");
+
+ nsCString name;
+
+ if (nsForMailbox)
+ m_runningUrl->AllocateCanonicalPath(
+ mailboxName, nsForMailbox->GetDelimiter(), getter_Copies(name));
+ else
+ m_runningUrl->AllocateCanonicalPath(
+ mailboxName, kOnlineHierarchySeparatorUnknown, getter_Copies(name));
+
+ if (name.IsEmpty()) return false;
+
+ NS_ASSERTION(m_imapServerSink,
+ "unexpected, no imap server sink, see bug #194335");
+ if (m_imapServerSink) m_imapServerSink->FolderIsNoSelect(name, &rv);
+ return rv;
+}
+
+nsresult nsImapProtocol::SetFolderAdminUrl(const char* mailboxName) {
+ nsresult rv =
+ NS_ERROR_NULL_POINTER; // if m_imapServerSink is null, rv will be this.
+
+ nsImapNamespace* nsForMailbox = nullptr;
+ m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(),
+ mailboxName, nsForMailbox);
+
+ nsCString name;
+
+ if (nsForMailbox)
+ m_runningUrl->AllocateCanonicalPath(
+ mailboxName, nsForMailbox->GetDelimiter(), getter_Copies(name));
+ else
+ m_runningUrl->AllocateCanonicalPath(
+ mailboxName, kOnlineHierarchySeparatorUnknown, getter_Copies(name));
+
+ if (m_imapServerSink)
+ rv = m_imapServerSink->SetFolderAdminURL(
+ name, nsDependentCString(GetServerStateParser().GetManageFolderUrl()));
+ return rv;
+}
+// returns true is the delete succeeded (regardless of subscription changes)
+bool nsImapProtocol::DeleteMailboxRespectingSubscriptions(
+ const char* mailboxName) {
+ bool rv = true;
+ if (!MailboxIsNoSelectMailbox(mailboxName)) {
+ // Only try to delete it if it really exists
+ DeleteMailbox(mailboxName);
+ rv = GetServerStateParser().LastCommandSuccessful();
+ }
+
+ // We can unsubscribe even if the mailbox doesn't exist.
+ if (rv && m_autoUnsubscribe) // auto-unsubscribe is on
+ {
+ bool reportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ Unsubscribe(mailboxName);
+ GetServerStateParser().SetReportingErrors(reportingErrors);
+ }
+ return (rv);
+}
+
+// returns true is the rename succeeded (regardless of subscription changes)
+// reallyRename tells us if we should really do the rename (true) or if we
+// should just move subscriptions (false)
+bool nsImapProtocol::RenameMailboxRespectingSubscriptions(
+ const char* existingName, const char* newName, bool reallyRename) {
+ bool rv = true;
+ if (reallyRename && !MailboxIsNoSelectMailbox(existingName)) {
+ RenameMailbox(existingName, newName);
+ rv = GetServerStateParser().LastCommandSuccessful();
+ }
+
+ if (rv) {
+ if (m_autoSubscribe) // if auto-subscribe is on
+ {
+ bool reportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ Subscribe(newName);
+ GetServerStateParser().SetReportingErrors(reportingErrors);
+ }
+ if (m_autoUnsubscribe) // if auto-unsubscribe is on
+ {
+ bool reportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ Unsubscribe(existingName);
+ GetServerStateParser().SetReportingErrors(reportingErrors);
+ }
+ }
+ return (rv);
+}
+
+bool nsImapProtocol::RenameHierarchyByHand(const char* oldParentMailboxName,
+ const char* newParentMailboxName) {
+ bool renameSucceeded = true;
+ char onlineDirSeparator = kOnlineHierarchySeparatorUnknown;
+ m_deletableChildren = new nsTArray<nsCString>();
+
+ bool nonHierarchicalRename =
+ ((GetServerStateParser().GetCapabilityFlag() & kNoHierarchyRename) ||
+ MailboxIsNoSelectMailbox(oldParentMailboxName));
+
+ m_hierarchyNameState = kDeleteSubFoldersInProgress;
+ nsImapNamespace* ns = nullptr;
+ m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(),
+ oldParentMailboxName,
+ ns); // for delimiter
+ if (!ns) {
+ if (!PL_strcasecmp(oldParentMailboxName, "INBOX"))
+ m_hostSessionList->GetDefaultNamespaceOfTypeForHost(
+ GetImapServerKey(), kPersonalNamespace, ns);
+ }
+ if (ns) {
+ nsCString pattern(oldParentMailboxName);
+ pattern += ns->GetDelimiter();
+ pattern += "*";
+ bool isUsingSubscription = false;
+ m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(),
+ isUsingSubscription);
+
+ if (isUsingSubscription)
+ Lsub(pattern.get(), false);
+ else
+ List(pattern.get(), false);
+ }
+ m_hierarchyNameState = kNoOperationInProgress;
+
+ if (GetServerStateParser().LastCommandSuccessful())
+ renameSucceeded = // rename this, and move subscriptions
+ RenameMailboxRespectingSubscriptions(oldParentMailboxName,
+ newParentMailboxName, true);
+
+ size_t numberToDelete = m_deletableChildren->Length();
+
+ for (size_t childIndex = 0; (childIndex < numberToDelete) && renameSucceeded;
+ childIndex++) {
+ nsCString name = m_deletableChildren->ElementAt(childIndex);
+ char* serverName = nullptr;
+ m_runningUrl->AllocateServerPath(name.get(), onlineDirSeparator,
+ &serverName);
+ if (!serverName) {
+ renameSucceeded = false;
+ break;
+ }
+ char* currentName = serverName;
+
+ // calculate the new name and do the rename
+ nsCString newChildName(newParentMailboxName);
+ newChildName += (currentName + PL_strlen(oldParentMailboxName));
+ // Pass in 'nonHierarchicalRename' to determine if we should really
+ // rename, or just move subscriptions.
+ renameSucceeded = RenameMailboxRespectingSubscriptions(
+ currentName, newChildName.get(), nonHierarchicalRename);
+ PR_FREEIF(currentName);
+ }
+
+ delete m_deletableChildren;
+ m_deletableChildren = nullptr;
+
+ return renameSucceeded;
+}
+
+bool nsImapProtocol::DeleteSubFolders(const char* selectedMailbox,
+ bool& aDeleteSelf) {
+ bool deleteSucceeded = true;
+ m_deletableChildren = new nsTArray<nsCString>;
+
+ bool folderDeleted = false;
+
+ m_hierarchyNameState = kDeleteSubFoldersInProgress;
+ nsCString pattern(selectedMailbox);
+ char onlineDirSeparator = kOnlineHierarchySeparatorUnknown;
+ m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator);
+ pattern.Append(onlineDirSeparator);
+ pattern.Append('*');
+
+ if (!pattern.IsEmpty()) {
+ List(pattern.get(), false);
+ }
+ m_hierarchyNameState = kNoOperationInProgress;
+
+ // this should be a short list so perform a sequential search for the
+ // longest name mailbox. Deleting the longest first will hopefully
+ // prevent the server from having problems about deleting parents
+ // ** jt - why? I don't understand this.
+ size_t numberToDelete = m_deletableChildren->Length();
+ size_t outerIndex, innerIndex;
+
+ // intelligently decide if myself(either plain format or following the
+ // dir-separator) is in the sub-folder list
+ bool folderInSubfolderList = false; // For Performance
+ char* selectedMailboxDir = nullptr;
+ {
+ int32_t length = strlen(selectedMailbox);
+ selectedMailboxDir = (char*)PR_MALLOC(length + 2);
+ if (selectedMailboxDir) // only do the intelligent test if there is
+ // enough memory
+ {
+ strcpy(selectedMailboxDir, selectedMailbox);
+ selectedMailboxDir[length] = onlineDirSeparator;
+ selectedMailboxDir[length + 1] = '\0';
+ size_t i;
+ for (i = 0; i < numberToDelete && !folderInSubfolderList; i++) {
+ const char* currentName = m_deletableChildren->ElementAt(i).get();
+ if (!strcmp(currentName, selectedMailbox) ||
+ !strcmp(currentName, selectedMailboxDir))
+ folderInSubfolderList = true;
+ }
+ }
+ }
+
+ deleteSucceeded = GetServerStateParser().LastCommandSuccessful();
+ for (outerIndex = 0; (outerIndex < numberToDelete) && deleteSucceeded;
+ outerIndex++) {
+ char* longestName = nullptr;
+ size_t longestIndex = 0; // fix bogus warning by initializing
+ for (innerIndex = 0; innerIndex < m_deletableChildren->Length();
+ innerIndex++) {
+ const char* currentName =
+ m_deletableChildren->ElementAt(innerIndex).get();
+ if (!longestName || strlen(longestName) < strlen(currentName)) {
+ longestName = (char*)currentName;
+ longestIndex = innerIndex;
+ }
+ }
+ if (longestName) {
+ char* serverName = nullptr;
+ m_runningUrl->AllocateServerPath(longestName, onlineDirSeparator,
+ &serverName);
+ m_deletableChildren->RemoveElementAt(longestIndex);
+ longestName = serverName;
+ }
+
+ // some imap servers include the selectedMailbox in the list of
+ // subfolders of the selectedMailbox. Check for this so we don't
+ // delete the selectedMailbox (usually the trash and doing an
+ // empty trash)
+ // The Cyrus imap server ignores the "INBOX.Trash" constraining
+ // string passed to the list command. Be defensive and make sure
+ // we only delete children of the trash
+ if (longestName && strcmp(selectedMailbox, longestName) &&
+ !strncmp(selectedMailbox, longestName, strlen(selectedMailbox))) {
+ if (selectedMailboxDir &&
+ !strcmp(selectedMailboxDir, longestName)) // just myself
+ {
+ if (aDeleteSelf) {
+ bool deleted = DeleteMailboxRespectingSubscriptions(longestName);
+ if (deleted) FolderDeleted(longestName);
+ folderDeleted = deleted;
+ deleteSucceeded = deleted;
+ }
+ } else {
+ if (m_imapServerSink)
+ m_imapServerSink->ResetServerConnection(
+ nsDependentCString(longestName));
+ bool deleted = false;
+ if (folderInSubfolderList) // for performance
+ {
+ nsTArray<nsCString>* pDeletableChildren = m_deletableChildren;
+ m_deletableChildren = nullptr;
+ bool folderDeleted = true;
+ deleted = DeleteSubFolders(longestName, folderDeleted);
+ // longestName may have subfolder list including itself
+ if (!folderDeleted) {
+ if (deleted)
+ deleted = DeleteMailboxRespectingSubscriptions(longestName);
+ if (deleted) FolderDeleted(longestName);
+ }
+ m_deletableChildren = pDeletableChildren;
+ } else {
+ deleted = DeleteMailboxRespectingSubscriptions(longestName);
+ if (deleted) FolderDeleted(longestName);
+ }
+ deleteSucceeded = deleted;
+ }
+ }
+ PR_FREEIF(longestName);
+ }
+
+ aDeleteSelf = folderDeleted; // feedback if myself is deleted
+ PR_Free(selectedMailboxDir);
+
+ delete m_deletableChildren;
+ m_deletableChildren = nullptr;
+
+ return deleteSucceeded;
+}
+
+void nsImapProtocol::FolderDeleted(const char* mailboxName) {
+ char onlineDelimiter = kOnlineHierarchySeparatorUnknown;
+ nsCString orphanedMailboxName;
+
+ if (mailboxName) {
+ m_runningUrl->AllocateCanonicalPath(mailboxName, onlineDelimiter,
+ getter_Copies(orphanedMailboxName));
+ if (m_imapServerSink)
+ m_imapServerSink->OnlineFolderDelete(orphanedMailboxName);
+ }
+}
+
+void nsImapProtocol::FolderNotCreated(const char* folderName) {
+ if (folderName && m_imapServerSink)
+ m_imapServerSink->OnlineFolderCreateFailed(nsDependentCString(folderName));
+}
+
+void nsImapProtocol::FolderRenamed(const char* oldName, const char* newName) {
+ char onlineDelimiter = kOnlineHierarchySeparatorUnknown;
+
+ if ((m_hierarchyNameState == kNoOperationInProgress) ||
+ (m_hierarchyNameState == kListingForInfoAndDiscovery))
+
+ {
+ nsCString canonicalOldName, canonicalNewName;
+ m_runningUrl->AllocateCanonicalPath(oldName, onlineDelimiter,
+ getter_Copies(canonicalOldName));
+ m_runningUrl->AllocateCanonicalPath(newName, onlineDelimiter,
+ getter_Copies(canonicalNewName));
+ AutoProxyReleaseMsgWindow msgWindow;
+ GetMsgWindow(getter_AddRefs(msgWindow));
+ m_imapServerSink->OnlineFolderRename(msgWindow, canonicalOldName,
+ canonicalNewName);
+ }
+}
+
+void nsImapProtocol::OnDeleteFolder(const char* sourceMailbox) {
+ // intelligently delete the folder
+ bool folderDeleted = true;
+ bool deleted = DeleteSubFolders(sourceMailbox, folderDeleted);
+ if (!folderDeleted) {
+ if (deleted) deleted = DeleteMailboxRespectingSubscriptions(sourceMailbox);
+ if (deleted) FolderDeleted(sourceMailbox);
+ }
+}
+
+void nsImapProtocol::RemoveMsgsAndExpunge() {
+ uint32_t numberOfMessages = GetServerStateParser().NumberOfMessages();
+ if (numberOfMessages) {
+ // Remove all msgs and expunge the folder (ie, compact it).
+ Store("1:*"_ns, "+FLAGS.SILENT (\\Deleted)",
+ false); // use sequence #'s
+ if (GetServerStateParser().LastCommandSuccessful()) Expunge();
+ }
+}
+
+void nsImapProtocol::DeleteFolderAndMsgs(const char* sourceMailbox) {
+ RemoveMsgsAndExpunge();
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ // All msgs are deleted successfully - let's remove the folder itself.
+ bool reportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ OnDeleteFolder(sourceMailbox);
+ GetServerStateParser().SetReportingErrors(reportingErrors);
+ }
+}
+
+void nsImapProtocol::OnRenameFolder(const char* sourceMailbox) {
+ char* destinationMailbox = OnCreateServerDestinationFolderPathString();
+
+ if (destinationMailbox) {
+ bool renamed = RenameHierarchyByHand(sourceMailbox, destinationMailbox);
+ if (renamed) FolderRenamed(sourceMailbox, destinationMailbox);
+
+ // Cause a LIST and re-discovery when slash and/or ^ are escaped. Also
+ // needed when folder renamed to non-ASCII UTF-8 when UTF8=ACCEPT in
+ // effect.
+ m_hierarchyNameState = kListingForCreate;
+ nsCString mailboxWODelim(destinationMailbox);
+ RemoveHierarchyDelimiter(mailboxWODelim);
+ List(mailboxWODelim.get(), false);
+ m_hierarchyNameState = kNoOperationInProgress;
+
+ PR_Free(destinationMailbox);
+ } else
+ HandleMemoryFailure();
+}
+
+void nsImapProtocol::OnMoveFolderHierarchy(const char* sourceMailbox) {
+ char* destinationMailbox = OnCreateServerDestinationFolderPathString();
+
+ if (destinationMailbox) {
+ nsCString newBoxName;
+ newBoxName.Adopt(destinationMailbox);
+
+ char onlineDirSeparator = kOnlineHierarchySeparatorUnknown;
+ m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator);
+
+ nsCString oldBoxName(sourceMailbox);
+ int32_t leafStart = oldBoxName.RFindChar(onlineDirSeparator);
+ nsCString leafName;
+
+ if (-1 == leafStart)
+ leafName = oldBoxName; // this is a root level box
+ else
+ leafName = Substring(oldBoxName, leafStart + 1);
+
+ if (!newBoxName.IsEmpty()) newBoxName.Append(onlineDirSeparator);
+ newBoxName.Append(leafName);
+ bool renamed = RenameHierarchyByHand(sourceMailbox, newBoxName.get());
+ if (renamed) FolderRenamed(sourceMailbox, newBoxName.get());
+ } else
+ HandleMemoryFailure();
+}
+
+// This is called to do mailbox discovery if discovery not already complete
+// for the "host" (i.e., server or account). Discovery still only occurs if
+// the imap action is appropriate and if discovery is not in progress due to
+// a running "discoverallboxes" URL.
+void nsImapProtocol::FindMailboxesIfNecessary() {
+ // biff should not discover mailboxes
+ nsImapAction imapAction;
+ (void)m_runningUrl->GetImapAction(&imapAction);
+ if ((imapAction != nsIImapUrl::nsImapBiff) &&
+ (imapAction != nsIImapUrl::nsImapVerifylogon) &&
+ (imapAction != nsIImapUrl::nsImapDiscoverAllBoxesUrl) &&
+ (imapAction != nsIImapUrl::nsImapUpgradeToSubscription) &&
+ !GetSubscribingNow()) {
+ // If discovery in progress, don't kick-off another discovery.
+ bool discoveryInProgress = false;
+ m_hostSessionList->GetDiscoveryForHostInProgress(GetImapServerKey(),
+ discoveryInProgress);
+ if (!discoveryInProgress) {
+ m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(),
+ true);
+ DiscoverMailboxList();
+ }
+ }
+}
+
+void nsImapProtocol::DiscoverAllAndSubscribedBoxes() {
+ // used for subscribe pane
+ // iterate through all namespaces
+ uint32_t count = 0;
+ m_hostSessionList->GetNumberOfNamespacesForHost(GetImapServerKey(), count);
+
+ for (uint32_t i = 0; i < count; i++) {
+ nsImapNamespace* ns = nullptr;
+ m_hostSessionList->GetNamespaceNumberForHost(GetImapServerKey(), i, ns);
+ if (!ns) {
+ continue;
+ }
+ if ((gHideOtherUsersFromList && (ns->GetType() != kOtherUsersNamespace)) ||
+ !gHideOtherUsersFromList) {
+ const char* prefix = ns->GetPrefix();
+ if (prefix) {
+ nsAutoCString inboxNameWithDelim("INBOX");
+ inboxNameWithDelim.Append(ns->GetDelimiter());
+
+ // Only do it for non-empty namespace prefixes.
+ if (!gHideUnusedNamespaces && *prefix &&
+ PL_strcasecmp(prefix, inboxNameWithDelim.get())) {
+ // Explicitly discover each Namespace, just so they're
+ // there in the subscribe UI
+ RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec;
+ boxSpec->mFolderSelected = false;
+ boxSpec->mHostName.Assign(GetImapHostName());
+ boxSpec->mConnection = this;
+ boxSpec->mFlagState = nullptr;
+ boxSpec->mDiscoveredFromLsub = true;
+ boxSpec->mOnlineVerified = true;
+ boxSpec->mBoxFlags = kNoselect;
+ boxSpec->mHierarchySeparator = ns->GetDelimiter();
+
+ m_runningUrl->AllocateCanonicalPath(
+ ns->GetPrefix(), ns->GetDelimiter(),
+ getter_Copies(boxSpec->mAllocatedPathName));
+ boxSpec->mNamespaceForFolder = ns;
+ boxSpec->mBoxFlags |= kNameSpace;
+
+ switch (ns->GetType()) {
+ case kPersonalNamespace:
+ boxSpec->mBoxFlags |= kPersonalMailbox;
+ break;
+ case kPublicNamespace:
+ boxSpec->mBoxFlags |= kPublicMailbox;
+ break;
+ case kOtherUsersNamespace:
+ boxSpec->mBoxFlags |= kOtherUsersMailbox;
+ break;
+ default: // (kUnknownNamespace)
+ break;
+ }
+
+ DiscoverMailboxSpec(boxSpec);
+ }
+
+ nsAutoCString allPattern(prefix);
+ allPattern += '*';
+
+ if (!m_imapServerSink) return;
+
+ m_imapServerSink->SetServerDoingLsub(true);
+ Lsub(allPattern.get(), true); // LSUB all the subscribed
+
+ m_imapServerSink->SetServerDoingLsub(false);
+ List(allPattern.get(), true); // LIST all folders
+ }
+ }
+ }
+}
+
+// DiscoverMailboxList() is used to actually do the discovery of folders
+// for a host. This is used both when we initially start up (and re-sync)
+// and also when the user manually requests a re-sync, by collapsing and
+// expanding a host in the folder pane. This is not used for the subscribe
+// pane.
+// DiscoverMailboxList() also gets the ACLs for each newly discovered folder
+void nsImapProtocol::DiscoverMailboxList() {
+ bool usingSubscription = false;
+
+ m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(),
+ usingSubscription);
+ // Pretend that the Trash folder doesn't exist, so we will rediscover it if we
+ // need to.
+ m_hostSessionList->SetOnlineTrashFolderExistsForHost(GetImapServerKey(),
+ false);
+
+ // should we check a pref here, to be able to turn off XList?
+ bool hasXLIST =
+ GetServerStateParser().GetCapabilityFlag() & kHasXListCapability;
+ if (hasXLIST && usingSubscription) {
+ m_hierarchyNameState = kXListing;
+ nsAutoCString pattern("%");
+ List("%", true, true);
+ // We list the first and second levels since special folders are unlikely
+ // to be more than 2 levels deep.
+ char separator = 0;
+ m_runningUrl->GetOnlineSubDirSeparator(&separator);
+ pattern.Append(separator);
+ pattern += '%';
+ List(pattern.get(), true, true);
+ }
+
+ SetMailboxDiscoveryStatus(eContinue);
+ if (GetServerStateParser().ServerHasACLCapability())
+ m_hierarchyNameState = kListingForInfoAndDiscovery;
+ else
+ m_hierarchyNameState = kNoOperationInProgress;
+
+ // iterate through all namespaces and LSUB them.
+ uint32_t count = 0;
+ m_hostSessionList->GetNumberOfNamespacesForHost(GetImapServerKey(), count);
+ for (uint32_t i = 0; i < count; i++) {
+ nsImapNamespace* ns = nullptr;
+ m_hostSessionList->GetNamespaceNumberForHost(GetImapServerKey(), i, ns);
+ if (ns) {
+ const char* prefix = ns->GetPrefix();
+ if (prefix) {
+ nsAutoCString inboxNameWithDelim("INBOX");
+ inboxNameWithDelim.Append(ns->GetDelimiter());
+
+ // static bool gHideUnusedNamespaces = true;
+ // mscott -> WARNING!!! i where are we going to get this
+ // global variable for unused name spaces from???
+ // dmb - we should get this from a per-host preference,
+ // I'd say. But for now, just make it true.
+ // Only do it for non-empty namespace prefixes, and for non-INBOX prefix
+ if (!gHideUnusedNamespaces && *prefix &&
+ PL_strcasecmp(prefix, inboxNameWithDelim.get())) {
+ // Explicitly discover each Namespace, so that we can
+ // create subfolders of them,
+ RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec;
+ boxSpec->mFolderSelected = false;
+ boxSpec->mHostName = GetImapHostName();
+ boxSpec->mConnection = this;
+ boxSpec->mFlagState = nullptr;
+ boxSpec->mDiscoveredFromLsub = true;
+ boxSpec->mOnlineVerified = true;
+ boxSpec->mBoxFlags = kNoselect;
+ boxSpec->mHierarchySeparator = ns->GetDelimiter();
+ // Until |AllocateCanonicalPath()| gets updated:
+ m_runningUrl->AllocateCanonicalPath(
+ ns->GetPrefix(), ns->GetDelimiter(),
+ getter_Copies(boxSpec->mAllocatedPathName));
+ boxSpec->mNamespaceForFolder = ns;
+ boxSpec->mBoxFlags |= kNameSpace;
+
+ switch (ns->GetType()) {
+ case kPersonalNamespace:
+ boxSpec->mBoxFlags |= kPersonalMailbox;
+ break;
+ case kPublicNamespace:
+ boxSpec->mBoxFlags |= kPublicMailbox;
+ break;
+ case kOtherUsersNamespace:
+ boxSpec->mBoxFlags |= kOtherUsersMailbox;
+ break;
+ default: // (kUnknownNamespace)
+ break;
+ }
+
+ DiscoverMailboxSpec(boxSpec);
+ }
+
+ // now do the folders within this namespace
+ nsCString pattern;
+ nsCString pattern2;
+ if (usingSubscription) {
+ pattern.Append(prefix);
+ pattern.Append('*');
+ } else {
+ pattern.Append(prefix);
+ pattern.Append('%'); // mscott just need one percent right?
+ // pattern = PR_smprintf("%s%%", prefix);
+ char delimiter = ns->GetDelimiter();
+ if (delimiter) {
+ // delimiter might be NIL, in which case there's no hierarchy anyway
+ pattern2 = prefix;
+ pattern2 += "%";
+ pattern2 += delimiter;
+ pattern2 += "%";
+ // pattern2 = PR_smprintf("%s%%%c%%", prefix, delimiter);
+ }
+ }
+ // Note: It is important to make sure we are respecting the
+ // server_sub_directory preference when calling List and Lsub (2nd arg =
+ // true), otherwise we end up with performance issues or even crashes
+ // when connecting to servers that expose the users entire home
+ // directory (like UW-IMAP).
+ if (usingSubscription) { // && !GetSubscribingNow()) should never get
+ // here from subscribe pane
+ if (GetServerStateParser().GetCapabilityFlag() &
+ kHasListExtendedCapability)
+ Lsub(pattern.get(), true); // do LIST (SUBSCRIBED)
+ else {
+ // store mailbox flags from LIST
+ EMailboxHierarchyNameState currentState = m_hierarchyNameState;
+ m_hierarchyNameState = kListingForFolderFlags;
+ List(pattern.get(), true);
+ m_hierarchyNameState = currentState;
+ // then do LSUB using stored flags
+ Lsub(pattern.get(), true);
+ m_standardListMailboxes.Clear();
+ }
+ } else {
+ List(pattern.get(), true, hasXLIST);
+ List(pattern2.get(), true, hasXLIST);
+ }
+ }
+ }
+ }
+
+ // explicitly LIST the INBOX if (a) we're not using subscription, or (b) we
+ // are using subscription and the user wants us to always show the INBOX.
+ bool listInboxForHost = false;
+ m_hostSessionList->GetShouldAlwaysListInboxForHost(GetImapServerKey(),
+ listInboxForHost);
+ if (!usingSubscription || listInboxForHost) List("INBOX", true);
+
+ m_hierarchyNameState = kNoOperationInProgress;
+
+ MailboxDiscoveryFinished();
+
+ // Get the ACLs for newly discovered folders
+ if (GetServerStateParser().ServerHasACLCapability()) {
+ int32_t total = m_listedMailboxList.Length(), cnt = 0;
+ // Let's not turn this off here, since we don't turn it on after
+ // GetServerStateParser().SetReportingErrors(false);
+ if (total) {
+ ProgressEventFunctionUsingName("imapGettingACLForFolder");
+ nsIMAPMailboxInfo* mb = nullptr;
+ do {
+ if (m_listedMailboxList.Length() == 0) break;
+
+ mb = m_listedMailboxList[0]; // get top element
+ m_listedMailboxList.RemoveElementAt(
+ 0); // XP_ListRemoveTopObject(fListedMailboxList);
+ if (mb) {
+ if (FolderNeedsACLInitialized(
+ PromiseFlatCString(mb->GetMailboxName()).get())) {
+ char* onlineName = nullptr;
+ m_runningUrl->AllocateServerPath(
+ PromiseFlatCString(mb->GetMailboxName()).get(),
+ mb->GetDelimiter(), &onlineName);
+ if (onlineName) {
+ RefreshACLForFolder(onlineName);
+ PR_Free(onlineName);
+ }
+ }
+ PercentProgressUpdateEvent(""_ns, u""_ns, cnt, total);
+ delete mb; // this is the last time we're using the list, so delete
+ // the entries here
+ cnt++;
+ }
+ } while (mb && !DeathSignalReceived());
+ }
+ }
+}
+
+bool nsImapProtocol::FolderNeedsACLInitialized(const char* folderName) {
+ bool rv = false;
+ m_imapServerSink->FolderNeedsACLInitialized(nsDependentCString(folderName),
+ &rv);
+ return rv;
+}
+
+void nsImapProtocol::MailboxDiscoveryFinished() {
+ if (!DeathSignalReceived() && !GetSubscribingNow() &&
+ ((m_hierarchyNameState == kNoOperationInProgress) ||
+ (m_hierarchyNameState == kListingForInfoAndDiscovery))) {
+ nsImapNamespace* ns = nullptr;
+ m_hostSessionList->GetDefaultNamespaceOfTypeForHost(GetImapServerKey(),
+ kPersonalNamespace, ns);
+ const char* personalDir = ns ? ns->GetPrefix() : 0;
+
+ bool trashFolderExists = false;
+ bool usingSubscription = false;
+ m_hostSessionList->GetOnlineTrashFolderExistsForHost(GetImapServerKey(),
+ trashFolderExists);
+ m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(),
+ usingSubscription);
+ if (!trashFolderExists && GetDeleteIsMoveToTrash() && usingSubscription) {
+ // maybe we're not subscribed to the Trash folder
+ if (personalDir) {
+ m_hierarchyNameState = kDiscoverTrashFolderInProgress;
+ List(m_trashFolderPath.get(), true);
+ m_hierarchyNameState = kNoOperationInProgress;
+ }
+ }
+
+ // There is no Trash folder (either LIST'd or LSUB'd), and we're using the
+ // Delete-is-move-to-Trash model, and there is a personal namespace
+ if (!trashFolderExists && GetDeleteIsMoveToTrash() && ns) {
+ nsCString onlineTrashName;
+ m_runningUrl->AllocateServerPath(m_trashFolderPath.get(),
+ ns->GetDelimiter(),
+ getter_Copies(onlineTrashName));
+
+ GetServerStateParser().SetReportingErrors(false);
+ bool created =
+ CreateMailboxRespectingSubscriptions(onlineTrashName.get());
+ GetServerStateParser().SetReportingErrors(true);
+
+ // force discovery of new trash folder.
+ if (created) {
+ m_hierarchyNameState = kDiscoverTrashFolderInProgress;
+ List(onlineTrashName.get(), false);
+ m_hierarchyNameState = kNoOperationInProgress;
+ } else
+ m_hostSessionList->SetOnlineTrashFolderExistsForHost(GetImapServerKey(),
+ true);
+ } // if trash folder doesn't exist
+ m_hostSessionList->SetHaveWeEverDiscoveredFoldersForHost(GetImapServerKey(),
+ true);
+ // notify front end that folder discovery is complete....
+ if (m_imapServerSink) m_imapServerSink->DiscoveryDone();
+
+ // Clear the discovery in progress flag.
+ m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(), false);
+ }
+}
+
+// returns the mailboxName with the IMAP delimiter removed from the tail end
+void nsImapProtocol::RemoveHierarchyDelimiter(nsCString& mailboxName) {
+ char onlineDelimiter[2] = {0, 0};
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter[0]);
+ // take the hierarchy delimiter off the end, if any.
+ if (onlineDelimiter[0]) mailboxName.Trim(onlineDelimiter, false, true);
+}
+
+// returns true is the create succeeded (regardless of subscription changes)
+bool nsImapProtocol::CreateMailboxRespectingSubscriptions(
+ const char* mailboxName) {
+ CreateMailbox(mailboxName);
+ bool rv = GetServerStateParser().LastCommandSuccessful();
+ if (rv && m_autoSubscribe) // auto-subscribe is on
+ {
+ // create succeeded - let's subscribe to it
+ bool reportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(false);
+ nsCString mailboxWODelim(mailboxName);
+ RemoveHierarchyDelimiter(mailboxWODelim);
+ OnSubscribe(mailboxWODelim.get());
+ GetServerStateParser().SetReportingErrors(reportingErrors);
+ }
+ return rv;
+}
+
+void nsImapProtocol::CreateMailbox(const char* mailboxName) {
+ ProgressEventFunctionUsingName("imapStatusCreatingMailbox");
+
+ IncrementCommandTagNumber();
+
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+ nsCString command(GetServerCommandTag());
+ command += " create \"";
+ command += escapedName;
+ command += "\"" CRLF;
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+ // If that failed, let's list the parent folder to see if
+ // it allows inferiors, so we won't try to create sub-folders
+ // of the parent folder again in the current session.
+ if (GetServerStateParser().CommandFailed()) {
+ // Figure out parent folder name.
+ nsCString parentName(mailboxName);
+ char hierarchyDelimiter;
+ m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter);
+ int32_t leafPos = parentName.RFindChar(hierarchyDelimiter);
+ if (leafPos > 0) {
+ parentName.SetLength(leafPos);
+ List(parentName.get(), false);
+ // We still want the caller to know the create failed, so restore that.
+ GetServerStateParser().SetCommandFailed(true);
+ }
+ }
+}
+
+void nsImapProtocol::DeleteMailbox(const char* mailboxName) {
+ // check if this connection currently has the folder to be deleted selected.
+ // If so, we should close it because at least some UW servers don't like you
+ // deleting a folder you have open.
+ if (FolderIsSelected(mailboxName)) ImapClose();
+
+ ProgressEventFunctionUsingNameWithString("imapStatusDeletingMailbox",
+ mailboxName);
+
+ IncrementCommandTagNumber();
+
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+ nsCString command(GetServerCommandTag());
+ command += " delete \"";
+ command += escapedName;
+ command += "\"" CRLF;
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::RenameMailbox(const char* existingName,
+ const char* newName) {
+ // just like DeleteMailbox; Some UW servers don't like it.
+ if (FolderIsSelected(existingName)) ImapClose();
+
+ ProgressEventFunctionUsingNameWithString("imapStatusRenamingMailbox",
+ existingName);
+
+ IncrementCommandTagNumber();
+
+ nsCString escapedExistingName;
+ nsCString escapedNewName;
+ CreateEscapedMailboxName(existingName, escapedExistingName);
+ CreateEscapedMailboxName(newName, escapedNewName);
+ nsCString command(GetServerCommandTag());
+ command += " rename \"";
+ command += escapedExistingName;
+ command += "\" \"";
+ command += escapedNewName;
+ command += "\"" CRLF;
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+bool nsImapProtocol::GetListSubscribedIsBrokenOnServer() {
+ // This is a workaround for an issue with LIST(SUBSCRIBED) crashing older
+ // versions of Zimbra
+ if (FindInReadable("\"NAME\" \"Zimbra\""_ns,
+ GetServerStateParser().GetServerID(),
+ nsCaseInsensitiveCStringComparator)) {
+ nsCString serverID(GetServerStateParser().GetServerID());
+ int start = serverID.LowerCaseFindASCII("\"version\" \"") + 11;
+ int length = serverID.LowerCaseFindASCII("\" ", start);
+ const nsDependentCSubstring serverVersionSubstring =
+ Substring(serverID, start, length);
+ nsCString serverVersionStr(serverVersionSubstring);
+ Version serverVersion(serverVersionStr.get());
+ Version sevenTwoThree("7.2.3_");
+ Version eightZeroZero("8.0.0_");
+ Version eightZeroThree("8.0.3_");
+ if ((serverVersion < sevenTwoThree) ||
+ ((serverVersion >= eightZeroZero) && (serverVersion < eightZeroThree)))
+ return true;
+ }
+ return false;
+}
+
+void nsImapProtocol::Lsub(const char* mailboxPattern,
+ bool addDirectoryIfNecessary) {
+ ProgressEventFunctionUsingName("imapStatusLookingForMailbox");
+
+ IncrementCommandTagNumber();
+
+ char* boxnameWithOnlineDirectory = nullptr;
+ if (addDirectoryIfNecessary)
+ m_runningUrl->AddOnlineDirectoryIfNecessary(mailboxPattern,
+ &boxnameWithOnlineDirectory);
+
+ nsCString escapedPattern;
+ CreateEscapedMailboxName(
+ boxnameWithOnlineDirectory ? boxnameWithOnlineDirectory : mailboxPattern,
+ escapedPattern);
+
+ nsCString command(GetServerCommandTag());
+ eIMAPCapabilityFlags flag = GetServerStateParser().GetCapabilityFlag();
+ bool useListSubscribed = (flag & kHasListExtendedCapability) &&
+ !GetListSubscribedIsBrokenOnServer();
+ if (useListSubscribed)
+ command += " list (subscribed)";
+ else
+ command += " lsub";
+ command += " \"\" \"";
+ command += escapedPattern;
+ if (useListSubscribed && (flag & kHasSpecialUseCapability))
+ command += "\" return (special-use)" CRLF;
+ else
+ command += "\"" CRLF;
+
+ PR_Free(boxnameWithOnlineDirectory);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(command.get(), true);
+}
+
+void nsImapProtocol::List(const char* mailboxPattern,
+ bool addDirectoryIfNecessary, bool useXLIST) {
+ ProgressEventFunctionUsingName("imapStatusLookingForMailbox");
+
+ IncrementCommandTagNumber();
+
+ char* boxnameWithOnlineDirectory = nullptr;
+ if (addDirectoryIfNecessary)
+ m_runningUrl->AddOnlineDirectoryIfNecessary(mailboxPattern,
+ &boxnameWithOnlineDirectory);
+
+ nsCString escapedPattern;
+ CreateEscapedMailboxName(
+ boxnameWithOnlineDirectory ? boxnameWithOnlineDirectory : mailboxPattern,
+ escapedPattern);
+
+ nsCString command(GetServerCommandTag());
+ command += useXLIST ? " xlist \"\" \"" : " list \"\" \"";
+ command += escapedPattern;
+ command += "\"" CRLF;
+
+ PR_Free(boxnameWithOnlineDirectory);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(command.get(), true);
+}
+
+void nsImapProtocol::Subscribe(const char* mailboxName) {
+ ProgressEventFunctionUsingNameWithString("imapStatusSubscribeToMailbox",
+ mailboxName);
+
+ IncrementCommandTagNumber();
+
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+
+ nsCString command(GetServerCommandTag());
+ command += " subscribe \"";
+ command += escapedName;
+ command += "\"" CRLF;
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Unsubscribe(const char* mailboxName) {
+ ProgressEventFunctionUsingNameWithString("imapStatusUnsubscribeMailbox",
+ mailboxName);
+ IncrementCommandTagNumber();
+
+ nsCString escapedName;
+ CreateEscapedMailboxName(mailboxName, escapedName);
+
+ nsCString command(GetServerCommandTag());
+ command += " unsubscribe \"";
+ command += escapedName;
+ command += "\"" CRLF;
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Idle() {
+ IncrementCommandTagNumber();
+
+ if (m_urlInProgress) return;
+ nsAutoCString command(GetServerCommandTag());
+ command += " IDLE" CRLF;
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) {
+ // Typically, we'll just get back only a continuation char on IDLE response,
+ // "+ idling". However, it is possible untagged responses will occur before
+ // and/or after the '+' which we treat the same as later untagged responses
+ // signaled by the socket thread. If untagged responses occur on IDLE,
+ // HandleIdleResponses() will trigger a select URL which will exit idle mode
+ // and update the selected folder. Finally, if IDLE responds with tagged BAD
+ // or NO, HandleIdleResponses() will return false.
+ m_idle = HandleIdleResponses();
+ }
+}
+
+// until we can fix the hang on shutdown waiting for server
+// responses, we need to not wait for the server response
+// on shutdown.
+void nsImapProtocol::EndIdle(bool waitForResponse /* = true */) {
+ // clear the async wait - otherwise, we have trouble doing a blocking read
+ // below.
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
+ do_QueryInterface(m_inputStream);
+ if (asyncInputStream) asyncInputStream->AsyncWait(nullptr, 0, 0, nullptr);
+ nsresult rv = SendData("DONE" CRLF);
+ // set a short timeout if we don't want to wait for a response
+ if (m_transport && !waitForResponse)
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5);
+ if (NS_SUCCEEDED(rv)) {
+ m_idle = false;
+ ParseIMAPandCheckForNewMail();
+ // If waiting for response (i.e., not shutting down), check for IDLE
+ // untagged response(s) occurring after DONE is sent, which can occur and is
+ // mentioned in the IDLE rfc as a possibility. This is similar to the checks
+ // done in OnStatusForFolder().
+ if (waitForResponse && m_imapMailFolderSinkSelected &&
+ GetServerStateParser().UntaggedResponse()) {
+ Log("EndIdle", nullptr, "idle response after idle DONE");
+ m_imapMailFolderSinkSelected->OnNewIdleMessages();
+ }
+ }
+ // Set m_imapMailFolderSink null only if shutting down or if DONE succeeds.
+ // We need to keep m_imapMailFolderSink if DONE fails or times out when not
+ // shutting down so the URL that is attempting to run on this connection can
+ // retry or signal a failed status when SetUrlState is called in
+ // ProcessCurrentUrl to invoke nsIUrlListener.onStopRunningUrl.
+ if (!waitForResponse || GetServerStateParser().LastCommandSuccessful())
+ m_imapMailFolderSink = nullptr;
+}
+
+void nsImapProtocol::Search(const char* searchCriteria, bool useUID,
+ bool notifyHit /* true */) {
+ m_notifySearchHit = notifyHit;
+ ProgressEventFunctionUsingName("imapStatusSearchMailbox");
+ IncrementCommandTagNumber();
+
+ nsCString protocolString(GetServerCommandTag());
+ // the searchCriteria string contains the 'search ....' string
+ if (useUID) protocolString.AppendLiteral(" uid");
+ protocolString.Append(' ');
+ protocolString.Append(searchCriteria);
+ // the search criteria can contain string literals, which means we
+ // need to break up the protocol string by CRLF's, and after sending CRLF,
+ // wait for the server to respond OK before sending more data
+ nsresult rv;
+ int32_t crlfIndex;
+ while ((crlfIndex = protocolString.Find(CRLF)) != kNotFound &&
+ !DeathSignalReceived()) {
+ nsAutoCString tempProtocolString;
+ tempProtocolString = StringHead(protocolString, crlfIndex + 2);
+ rv = SendData(tempProtocolString.get());
+ if (NS_FAILED(rv)) return;
+ ParseIMAPandCheckForNewMail();
+ protocolString.Cut(0, crlfIndex + 2);
+ }
+ protocolString.Append(CRLF);
+
+ rv = SendData(protocolString.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Copy(const char* messageList,
+ const char* destinationMailbox, bool idsAreUid) {
+ IncrementCommandTagNumber();
+
+ nsCString escapedDestination;
+ CreateEscapedMailboxName(destinationMailbox, escapedDestination);
+
+ // turn messageList back into key array and then back into a message id list,
+ // but use the flag state to handle ranges correctly.
+ nsCString messageIdList;
+ nsTArray<nsMsgKey> msgKeys;
+ if (idsAreUid) ParseUidString(messageList, msgKeys);
+
+ int32_t msgCountLeft = msgKeys.Length();
+ uint32_t msgsHandled = 0;
+
+ do {
+ nsCString idString;
+
+ uint32_t msgsToHandle = msgCountLeft;
+ if (idsAreUid)
+ AllocateImapUidString(msgKeys.Elements() + msgsHandled, msgsToHandle,
+ m_flagState, idString);
+ else
+ idString.Assign(messageList);
+
+ msgsHandled += msgsToHandle;
+ msgCountLeft -= msgsToHandle;
+
+ IncrementCommandTagNumber();
+ nsAutoCString protocolString(GetServerCommandTag());
+ if (idsAreUid) protocolString.AppendLiteral(" uid");
+ // If it's a MOVE operation on aol servers then use 'xaol-move' cmd.
+ if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
+ GetServerStateParser().ServerIsAOLServer())
+ protocolString.AppendLiteral(" xaol-move ");
+ else if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) &&
+ GetServerStateParser().GetCapabilityFlag() & kHasMoveCapability)
+ protocolString.AppendLiteral(" move ");
+ else
+ protocolString.AppendLiteral(" copy ");
+
+ protocolString.Append(idString);
+ protocolString.AppendLiteral(" \"");
+ protocolString.Append(escapedDestination);
+ protocolString.AppendLiteral("\"" CRLF);
+
+ nsresult rv = SendData(protocolString.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString.get());
+ } while (msgCountLeft > 0 && !DeathSignalReceived());
+}
+
+void nsImapProtocol::NthLevelChildList(const char* onlineMailboxPrefix,
+ int32_t depth) {
+ NS_ASSERTION(depth >= 0, "Oops ... depth must be equal or greater than 0");
+ if (depth < 0) return;
+
+ nsCString truncatedPrefix(onlineMailboxPrefix);
+ char16_t slash = '/';
+ if (truncatedPrefix.Last() == slash)
+ truncatedPrefix.SetLength(truncatedPrefix.Length() - 1);
+
+ nsAutoCString pattern(truncatedPrefix);
+ nsAutoCString suffix;
+ int count = 0;
+ char separator = 0;
+ m_runningUrl->GetOnlineSubDirSeparator(&separator);
+ suffix.Assign(separator);
+ suffix += '%';
+
+ while (count < depth) {
+ pattern += suffix;
+ count++;
+ List(pattern.get(), false);
+ }
+}
+
+/**
+ * ProcessAuthenticatedStateURL() is a helper for ProcessCurrentURL() which
+ * handles running URLs which require the connection to be in the
+ * Authenticated state.
+ */
+void nsImapProtocol::ProcessAuthenticatedStateURL() {
+ nsImapAction imapAction;
+ char* sourceMailbox = nullptr;
+ m_runningUrl->GetImapAction(&imapAction);
+
+ // switch off of the imap url action and take an appropriate action
+ switch (imapAction) {
+ case nsIImapUrl::nsImapLsubFolders:
+ OnLSubFolders();
+ break;
+ case nsIImapUrl::nsImapAppendMsgFromFile:
+ OnAppendMsgFromFile();
+ break;
+ case nsIImapUrl::nsImapDiscoverAllBoxesUrl:
+ NS_ASSERTION(!GetSubscribingNow(),
+ "Oops ... should not get here from subscribe UI");
+ DiscoverMailboxList();
+ break;
+ case nsIImapUrl::nsImapDiscoverAllAndSubscribedBoxesUrl:
+ DiscoverAllAndSubscribedBoxes();
+ break;
+ case nsIImapUrl::nsImapCreateFolder:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnCreateFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapEnsureExistsFolder:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnEnsureExistsFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapDiscoverChildrenUrl: {
+ char* canonicalParent = nullptr;
+ m_runningUrl->CreateServerSourceFolderPathString(&canonicalParent);
+ if (canonicalParent) {
+ NthLevelChildList(canonicalParent, 2);
+ PR_Free(canonicalParent);
+ }
+ break;
+ }
+ case nsIImapUrl::nsImapSubscribe:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnSubscribe(sourceMailbox); // used to be called subscribe
+
+ if (GetServerStateParser().LastCommandSuccessful()) {
+ bool shouldList;
+ // if url is an external click url, then we should list the folder
+ // after subscribing to it, so we can select it.
+ m_runningUrl->GetExternalLinkUrl(&shouldList);
+ if (shouldList) OnListFolder(sourceMailbox, true);
+ }
+ break;
+ case nsIImapUrl::nsImapUnsubscribe:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnUnsubscribe(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapRefreshACL:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ RefreshACLForFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapRefreshAllACLs:
+ OnRefreshAllACLs();
+ break;
+ case nsIImapUrl::nsImapListFolder:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnListFolder(sourceMailbox, false);
+ break;
+ case nsIImapUrl::nsImapFolderStatus:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnStatusForFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapRefreshFolderUrls:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ XMailboxInfo(sourceMailbox);
+ if (GetServerStateParser().LastCommandSuccessful())
+ SetFolderAdminUrl(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapDeleteFolder:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnDeleteFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapRenameFolder:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnRenameFolder(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapMoveFolderHierarchy:
+ sourceMailbox = OnCreateServerSourceFolderPathString();
+ OnMoveFolderHierarchy(sourceMailbox);
+ break;
+ case nsIImapUrl::nsImapVerifylogon:
+ break;
+ default:
+ break;
+ }
+ PR_Free(sourceMailbox);
+}
+
+void nsImapProtocol::ProcessAfterAuthenticated() {
+ // if we're a netscape server, and we haven't got the admin url, get it
+ bool hasAdminUrl = true;
+
+ // If a capability response didn't occur during authentication, request
+ // the capabilities again to ensure the full capability set is known.
+ if (!m_capabilityResponseOccurred) Capability();
+
+ if (NS_SUCCEEDED(m_hostSessionList->GetHostHasAdminURL(GetImapServerKey(),
+ hasAdminUrl)) &&
+ !hasAdminUrl) {
+ if (GetServerStateParser().ServerHasServerInfo()) {
+ XServerInfo();
+ if (GetServerStateParser().LastCommandSuccessful() && m_imapServerSink) {
+ m_imapServerSink->SetMailServerUrls(
+ GetServerStateParser().GetMailAccountUrl(),
+ GetServerStateParser().GetManageListsUrl(),
+ GetServerStateParser().GetManageFiltersUrl());
+ // we've tried to ask for it, so don't try again this session.
+ m_hostSessionList->SetHostHasAdminURL(GetImapServerKey(), true);
+ }
+ } else if (GetServerStateParser().ServerIsNetscape3xServer()) {
+ Netscape();
+ if (GetServerStateParser().LastCommandSuccessful() && m_imapServerSink)
+ m_imapServerSink->SetMailServerUrls(
+ GetServerStateParser().GetMailAccountUrl(), EmptyCString(),
+ EmptyCString());
+ }
+ }
+
+ if (GetServerStateParser().ServerHasNamespaceCapability()) {
+ bool nameSpacesOverridable = false;
+ bool haveNameSpacesForHost = false;
+ m_hostSessionList->GetNamespacesOverridableForHost(GetImapServerKey(),
+ nameSpacesOverridable);
+ m_hostSessionList->GetGotNamespacesForHost(GetImapServerKey(),
+ haveNameSpacesForHost);
+
+ // mscott: VERIFY THIS CLAUSE!!!!!!!
+ if (nameSpacesOverridable && !haveNameSpacesForHost) Namespace();
+ }
+
+ // If the server supports compression, turn it on now.
+ // Choosing this spot (after login has finished) because
+ // many proxies (e.g. perdition, nginx) talk IMAP to the
+ // client until login is finished, then hand off to the
+ // backend. If we enable compression early the proxy
+ // will be confused.
+ if (UseCompressDeflate()) StartCompressDeflate();
+
+ if ((GetServerStateParser().GetCapabilityFlag() & kHasEnableCapability) &&
+ UseCondStore())
+ EnableCondStore();
+
+ if ((GetServerStateParser().GetCapabilityFlag() & kHasIDCapability) &&
+ m_sendID) {
+ ID();
+ if (m_imapServerSink && !GetServerStateParser().GetServerID().IsEmpty())
+ m_imapServerSink->SetServerID(GetServerStateParser().GetServerID());
+ }
+
+ bool utf8AcceptAllowed = m_allowUTF8Accept;
+ m_allowUTF8Accept = false;
+ if (utf8AcceptAllowed &&
+ ((GetServerStateParser().GetCapabilityFlag() &
+ (kHasEnableCapability | kHasUTF8AcceptCapability)) ==
+ (kHasEnableCapability | kHasUTF8AcceptCapability))) {
+ if (m_imapServerSink) {
+ EnableUTF8Accept();
+ m_allowUTF8Accept = GetServerStateParser().fUtf8AcceptEnabled;
+ // m_allowUTF8Accept affects imap append handling. See
+ // UploadMessageFromFile().
+ m_imapServerSink->SetServerUtf8AcceptEnabled(m_allowUTF8Accept);
+ GetServerStateParser().fUtf8AcceptEnabled = false;
+ } else {
+ NS_WARNING("UTF8=ACCEPT not enabled due to null m_imapServerSink");
+ }
+ }
+}
+
+void nsImapProtocol::SetupMessageFlagsString(nsCString& flagString,
+ imapMessageFlagsType flags,
+ uint16_t userFlags) {
+ if (flags & kImapMsgSeenFlag) flagString.AppendLiteral("\\Seen ");
+ if (flags & kImapMsgAnsweredFlag) flagString.AppendLiteral("\\Answered ");
+ if (flags & kImapMsgFlaggedFlag) flagString.AppendLiteral("\\Flagged ");
+ if (flags & kImapMsgDeletedFlag) flagString.AppendLiteral("\\Deleted ");
+ if (flags & kImapMsgDraftFlag) flagString.AppendLiteral("\\Draft ");
+ if (flags & kImapMsgRecentFlag) flagString.AppendLiteral("\\Recent ");
+ if ((flags & kImapMsgForwardedFlag) &&
+ (userFlags & kImapMsgSupportForwardedFlag))
+ flagString.AppendLiteral("$Forwarded "); // Not always available
+ if ((flags & kImapMsgMDNSentFlag) && (userFlags & kImapMsgSupportMDNSentFlag))
+ flagString.AppendLiteral("$MDNSent "); // Not always available
+
+ // eat the last space
+ if (!flagString.IsEmpty()) flagString.SetLength(flagString.Length() - 1);
+}
+
+void nsImapProtocol::ProcessStoreFlags(const nsCString& messageIdsString,
+ bool idsAreUids,
+ imapMessageFlagsType flags,
+ bool addFlags) {
+ nsCString flagString;
+
+ uint16_t userFlags = GetServerStateParser().SupportsUserFlags();
+ uint16_t settableFlags = GetServerStateParser().SettablePermanentFlags();
+
+ if (!addFlags && (flags & userFlags) && !(flags & settableFlags)) {
+ if (m_runningUrl)
+ m_runningUrl->SetExtraStatus(nsIImapUrl::ImapStatusFlagsNotSettable);
+ return; // if cannot set any of the flags bail out
+ }
+
+ if (addFlags)
+ flagString = "+Flags (";
+ else
+ flagString = "-Flags (";
+
+ if (flags & kImapMsgSeenFlag && kImapMsgSeenFlag & settableFlags)
+ flagString.AppendLiteral("\\Seen ");
+ if (flags & kImapMsgAnsweredFlag && kImapMsgAnsweredFlag & settableFlags)
+ flagString.AppendLiteral("\\Answered ");
+ if (flags & kImapMsgFlaggedFlag && kImapMsgFlaggedFlag & settableFlags)
+ flagString.AppendLiteral("\\Flagged ");
+ if (flags & kImapMsgDeletedFlag && kImapMsgDeletedFlag & settableFlags)
+ flagString.AppendLiteral("\\Deleted ");
+ if (flags & kImapMsgDraftFlag && kImapMsgDraftFlag & settableFlags)
+ flagString.AppendLiteral("\\Draft ");
+ if (flags & kImapMsgForwardedFlag && kImapMsgSupportForwardedFlag & userFlags)
+ flagString.AppendLiteral("$Forwarded "); // if supported
+ if (flags & kImapMsgMDNSentFlag && kImapMsgSupportMDNSentFlag & userFlags)
+ flagString.AppendLiteral("$MDNSent "); // if supported
+
+ if (flagString.Length() > 8) // if more than "+Flags ("
+ {
+ // replace the final space with ')'
+ flagString.SetCharAt(')', flagString.Length() - 1);
+
+ Store(messageIdsString, flagString.get(), idsAreUids);
+ if (m_runningUrl && idsAreUids) {
+ nsCString messageIdString;
+ m_runningUrl->GetListOfMessageIds(messageIdString);
+ nsTArray<nsMsgKey> msgKeys;
+ ParseUidString(messageIdString.get(), msgKeys);
+
+ int32_t msgCount = msgKeys.Length();
+ for (int32_t i = 0; i < msgCount; i++) {
+ bool found;
+ imapMessageFlagsType resultFlags;
+ // check if the flags were added/removed, and if the uid really exists.
+ nsresult rv = GetFlagsForUID(msgKeys[i], &found, &resultFlags, nullptr);
+ if (NS_FAILED(rv) || !found ||
+ (addFlags && ((flags & resultFlags) != flags)) ||
+ (!addFlags && ((flags & resultFlags) != 0))) {
+ m_runningUrl->SetExtraStatus(nsIImapUrl::ImapStatusFlagChangeFailed);
+ break;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * This will cause all messages marked deleted to be expunged with no untagged
+ * response so it can cause unexpected data loss if used improperly.
+ */
+void nsImapProtocol::ImapClose(bool shuttingDown /* = false */,
+ bool waitForResponse /* = true */) {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" close" CRLF);
+
+ if (!shuttingDown) ProgressEventFunctionUsingName("imapStatusCloseMailbox");
+
+ GetServerStateParser().ResetFlagInfo();
+
+ nsresult rv = SendData(command.get());
+ if (m_transport && shuttingDown)
+ m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5);
+
+ if (NS_SUCCEEDED(rv) && waitForResponse) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::XAOL_Option(const char* option) {
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" XAOL-OPTION ");
+ command.Append(option);
+ command.Append(CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail();
+}
+
+void nsImapProtocol::Check() {
+ // ProgressUpdateEvent("Checking mailbox...");
+
+ IncrementCommandTagNumber();
+
+ nsCString command(GetServerCommandTag());
+ command.AppendLiteral(" check" CRLF);
+
+ nsresult rv = SendData(command.get());
+ if (NS_SUCCEEDED(rv)) {
+ m_flagChangeCount = 0;
+ m_lastCheckTime = PR_Now();
+ ParseIMAPandCheckForNewMail();
+ }
+}
+
+nsresult nsImapProtocol::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ nsresult rv;
+ *aMsgWindow = nullptr;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(m_runningUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!m_imapProtocolSink) return NS_ERROR_FAILURE;
+ return m_imapProtocolSink->GetUrlWindow(mailnewsUrl, aMsgWindow);
+}
+
+/**
+ * Get password from RAM, disk (password manager) or user (dialog)
+ * @return NS_MSG_PASSWORD_PROMPT_CANCELLED
+ * (which is NS_SUCCEEDED!) when user cancelled
+ * NS_FAILED(rv) for other errors
+ */
+nsresult nsImapProtocol::GetPassword(nsString& password,
+ bool newPasswordRequested) {
+ // we are in the imap thread so *NEVER* try to extract the password with UI
+ NS_ENSURE_TRUE(m_imapServerSink, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(m_server, NS_ERROR_NULL_POINTER);
+ nsresult rv;
+
+ password = nsString();
+ // Get the password already stored in mem
+ rv = m_imapServerSink->GetServerPassword(password);
+ if (NS_FAILED(rv) || password.IsEmpty()) {
+ // First see if there's an associated window. We don't want to produce a
+ // password prompt if there is no window, e.g., during biff.
+ AutoProxyReleaseMsgWindow msgWindow;
+ GetMsgWindow(getter_AddRefs(msgWindow));
+ if (msgWindow) {
+ m_passwordStatus = NS_OK;
+ m_passwordObtained = false;
+
+ // Get the password from pw manager (harddisk) or user (dialog)
+ rv = m_imapServerSink->AsyncGetPassword(this, newPasswordRequested,
+ password);
+
+ if (NS_SUCCEEDED(rv)) {
+ while (password.IsEmpty()) {
+ bool shuttingDown = false;
+ (void)m_imapServerSink->GetServerShuttingDown(&shuttingDown);
+ if (shuttingDown) {
+ // Note: If we fix bug 1783573 this check could be ditched.
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+
+ ReentrantMonitorAutoEnter mon(m_passwordReadyMonitor);
+ if (!m_passwordObtained && !NS_FAILED(m_passwordStatus) &&
+ m_passwordStatus != NS_MSG_PASSWORD_PROMPT_CANCELLED &&
+ !DeathSignalReceived()) {
+ mon.Wait(PR_MillisecondsToInterval(1000));
+ }
+
+ if (NS_FAILED(m_passwordStatus) ||
+ m_passwordStatus == NS_MSG_PASSWORD_PROMPT_CANCELLED) {
+ rv = m_passwordStatus;
+ break;
+ }
+
+ if (DeathSignalReceived()) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+
+ if (m_passwordObtained) {
+ rv = m_passwordStatus;
+ password = m_password;
+ break;
+ }
+ } // end while
+ }
+ } else {
+ // If no msgWindow (i.e., unattended operation like biff, filtering or
+ // autosync) try to get the password directly from login mgr. If it's not
+ // there, will return NS_ERROR_NOT_AVAILABLE and the connection will fail
+ // with only the IMAP log message: `password prompt failed or user
+ // canceled it'. No password prompt occurs.
+ rv = m_imapServerSink->SyncGetPassword(password);
+ }
+ }
+ if (!password.IsEmpty()) m_lastPasswordSent = password;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapProtocol::OnPromptStartAsync(
+ nsIMsgAsyncPromptCallback* aCallback) {
+ bool result = false;
+ OnPromptStart(&result);
+ return aCallback->OnAuthResult(result);
+}
+
+// This is called from the UI thread.
+NS_IMETHODIMP
+nsImapProtocol::OnPromptStart(bool* aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryReferent(m_server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+
+ *aResult = false;
+ GetMsgWindow(getter_AddRefs(msgWindow));
+ nsString password = m_lastPasswordSent;
+ rv = imapServer->PromptPassword(msgWindow, password);
+
+ ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor);
+
+ m_password = password;
+ m_passwordStatus = rv;
+ if (!m_password.IsEmpty()) *aResult = true;
+
+ // Notify the imap thread that we have a password.
+ m_passwordObtained = true;
+ passwordMon.Notify();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapProtocol::OnPromptAuthAvailable() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> imapServer = do_QueryReferent(m_server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsresult status = imapServer->GetPassword(m_password);
+
+ ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor);
+
+ m_passwordStatus = status;
+ // Notify the imap thread that we have a password.
+ m_passwordObtained = true;
+ passwordMon.Notify();
+ return m_passwordStatus;
+}
+
+NS_IMETHODIMP
+nsImapProtocol::OnPromptCanceled() {
+ // A prompt was cancelled, so notify the imap thread.
+ ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor);
+ m_passwordStatus = NS_MSG_PASSWORD_PROMPT_CANCELLED;
+ passwordMon.Notify();
+ return NS_OK;
+}
+
+// Called when capability response is parsed.
+void nsImapProtocol::SetCapabilityResponseOccurred() {
+ m_capabilityResponseOccurred = true;
+}
+
+bool nsImapProtocol::TryToLogon() {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("Try to log in"));
+ NS_ENSURE_TRUE(m_imapServerSink, false);
+ bool loginSucceeded = false;
+ bool skipLoop = false;
+ nsAutoString password;
+ nsAutoCString userName;
+
+ // If remains false when authentication is complete it means that a
+ // capability response didn't occur within the authentication response so
+ // capabilities will be requested explicitly.
+ m_capabilityResponseOccurred = false;
+
+ nsresult rv = ChooseAuthMethod();
+ if (NS_FAILED(rv)) // all methods failed
+ {
+ // are there any matching login schemes at all?
+ if (!(GetServerStateParser().GetCapabilityFlag() & m_prefAuthMethods)) {
+ // Pref doesn't match server. Now, find an appropriate error msg.
+
+ // pref has plaintext pw & server claims to support encrypted pw
+ if (m_prefAuthMethods ==
+ (kHasAuthOldLoginCapability | kHasAuthLoginCapability |
+ kHasAuthPlainCapability) &&
+ GetServerStateParser().GetCapabilityFlag() & kHasCRAMCapability)
+ // tell user to change to encrypted pw
+ AlertUserEventUsingName("imapAuthChangePlainToEncrypt");
+ // pref has encrypted pw & server claims to support plaintext pw
+ else if (m_prefAuthMethods == kHasCRAMCapability &&
+ GetServerStateParser().GetCapabilityFlag() &
+ (kHasAuthOldLoginCapability | kHasAuthLoginCapability |
+ kHasAuthPlainCapability)) {
+ // have SSL
+ if (m_socketType == nsMsgSocketType::SSL ||
+ m_socketType == nsMsgSocketType::alwaysSTARTTLS)
+ // tell user to change to plaintext pw
+ AlertUserEventUsingName("imapAuthChangeEncryptToPlainSSL");
+ else
+ // tell user to change to plaintext pw, with big warning
+ AlertUserEventUsingName("imapAuthChangeEncryptToPlainNoSSL");
+ } else
+ // just "change auth method"
+ AlertUserEventUsingName("imapAuthMechNotSupported");
+
+ skipLoop = true;
+ } else {
+ // try to reset failed methods and try them again
+ ResetAuthMethods();
+ rv = ChooseAuthMethod();
+ if (NS_FAILED(rv)) // all methods failed
+ {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("huch? there are auth methods, and we reset failed ones, but "
+ "ChooseAuthMethod still fails."));
+ return false;
+ }
+ }
+ }
+
+ // Check the uri host for localhost indicators to see if we
+ // should bypass the SSL check for clientid.
+ // Unfortunately we cannot call IsOriginPotentiallyTrustworthy
+ // here because it can only be called from the main thread.
+ bool isLocalhostConnection = false;
+ if (m_mockChannel) {
+ nsCOMPtr<nsIURI> uri;
+ m_mockChannel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsCString uriHost;
+ uri->GetHost(uriHost);
+ if (uriHost.Equals("127.0.0.1") || uriHost.Equals("::1") ||
+ uriHost.Equals("localhost")) {
+ isLocalhostConnection = true;
+ }
+ }
+ }
+
+ // Whether our connection can be considered 'secure' and whether
+ // we should allow the CLIENTID to be sent over this channel.
+ bool isSecureConnection =
+ (m_connectionType.EqualsLiteral("starttls") ||
+ m_connectionType.EqualsLiteral("ssl") || isLocalhostConnection);
+
+ // Before running the ClientID command we check for clientid
+ // support by checking the server capability flags for the
+ // flag kHasClientIDCapability.
+ // We check that the m_clientId string is not empty, and
+ // we ensure the connection can be considered secure.
+ if ((GetServerStateParser().GetCapabilityFlag() & kHasClientIDCapability) &&
+ !m_clientId.IsEmpty() && isSecureConnection) {
+ rv = ClientID();
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("TryToLogon: Could not issue CLIENTID command"));
+ skipLoop = true;
+ }
+ }
+
+ // Get username, either the stored one or from user
+ rv = m_imapServerSink->GetLoginUsername(userName);
+ if (NS_FAILED(rv) || userName.IsEmpty()) {
+ // The user hit "Cancel" on the dialog box
+ skipLoop = true;
+ }
+
+ // clang-format off
+ /*
+ * Login can fail for various reasons:
+ * 1. Server claims to support GSSAPI, but it really doesn't.
+ * Or the client doesn't support GSSAPI, or is not logged in yet.
+ * (GSSAPI is a mechanism without password in apps).
+ * 2. Server claims to support CRAM-MD5, but it's broken and will fail despite correct password.
+ * 2.1. Some servers say they support CRAM but are so badly broken that trying it causes
+ * all subsequent login attempts to fail during this connection (bug 231303).
+ * So we use CRAM/NTLM/MSN only if enabled in prefs.
+ * Update: if it affects only some ISPs, we can maybe use the ISP DB
+ * and disable CRAM specifically for these.
+ * 3. Prefs are set to require auth methods which the server doesn't support
+ * (per CAPS or we tried and they failed).
+ * 4. User provided wrong password.
+ * 5. We tried too often and the server shut us down, so even a correct attempt
+ * will now (currently) fail.
+ * The above problems may overlap, e.g. 3. with 1. and 2., and we can't differentiate
+ * between 2. and 4., which is really unfortunate.
+ */
+ // clang-format on
+
+ bool newPasswordRequested = false;
+ // remember the msgWindow before we start trying to logon, because if the
+ // server drops the connection on errors, TellThreadToDie will null out the
+ // protocolsink and we won't be able to get the msgWindow.
+ AutoProxyReleaseMsgWindow msgWindow;
+ GetMsgWindow(getter_AddRefs(msgWindow));
+
+ // This loops over 1) auth methods (only one per loop) and 2) password tries
+ // (with UI)
+ while (!loginSucceeded && !skipLoop && !DeathSignalReceived()) {
+ // Get password
+ if (m_currentAuthMethod !=
+ kHasAuthGssApiCapability && // GSSAPI uses no pw in apps
+ m_currentAuthMethod != kHasAuthExternalCapability &&
+ m_currentAuthMethod != kHasXOAuth2Capability &&
+ m_currentAuthMethod != kHasAuthNoneCapability) {
+ rv = GetPassword(password, newPasswordRequested);
+ newPasswordRequested = false;
+ if (rv == NS_MSG_PASSWORD_PROMPT_CANCELLED || NS_FAILED(rv)) {
+ MOZ_LOG(IMAP, LogLevel::Error,
+ ("IMAP: password prompt failed or user canceled it"));
+ break;
+ }
+ MOZ_LOG(IMAP, LogLevel::Debug, ("got new password"));
+ }
+
+ bool lastReportingErrors = GetServerStateParser().GetReportingErrors();
+ GetServerStateParser().SetReportingErrors(
+ false); // turn off errors - we'll put up our own.
+
+ rv = AuthLogin(userName.get(), password, m_currentAuthMethod);
+
+ GetServerStateParser().SetReportingErrors(
+ lastReportingErrors); // restore error reports
+ loginSucceeded = NS_SUCCEEDED(rv);
+
+ if (!loginSucceeded) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("authlogin failed"));
+ MarkAuthMethodAsFailed(m_currentAuthMethod);
+ rv = ChooseAuthMethod(); // change m_currentAuthMethod to try other one
+ // next round
+
+ if (NS_FAILED(rv)) // all methods failed
+ {
+ if (m_prefAuthMethods == kHasAuthGssApiCapability) {
+ // GSSAPI failed, and it's the only available method,
+ // and it's password-less, so nothing left to do.
+ AlertUserEventUsingName("imapAuthGssapiFailed");
+ break;
+ }
+
+ if (m_prefAuthMethods & kHasXOAuth2Capability) {
+ // OAuth2 failed. Entering password does not help.
+ AlertUserEventUsingName("imapOAuth2Error");
+ break;
+ }
+
+ // The reason that we failed might be a wrong password, so
+ // ask user what to do
+ MOZ_LOG(IMAP, LogLevel::Warning,
+ ("IMAP: ask user what to do (after login failed): new "
+ "passwort, retry, cancel"));
+ if (!m_imapServerSink) break;
+ // if there's no msg window, don't forget the password
+ if (!msgWindow) break;
+ int32_t buttonPressed = 1;
+ rv = m_imapServerSink->PromptLoginFailed(msgWindow, &buttonPressed);
+ if (NS_FAILED(rv)) break;
+ if (buttonPressed == 2) // 'New password' button
+ {
+ MOZ_LOG(IMAP, LogLevel::Warning, ("new password button pressed."));
+ // Forget the current password
+ password.Truncate();
+ m_hostSessionList->SetPasswordForHost(GetImapServerKey(),
+ EmptyString());
+ m_imapServerSink->ForgetPassword();
+ m_password.Truncate();
+ MOZ_LOG(IMAP, LogLevel::Warning, ("password reset (nulled)"));
+ newPasswordRequested = true;
+ // Will call GetPassword() in beginning of next loop
+
+ // Try all possible auth methods again with the new password.
+ ResetAuthMethods();
+ } else if (buttonPressed == 0) // Retry button
+ {
+ MOZ_LOG(IMAP, LogLevel::Warning, ("retry button pressed"));
+ // Try all possible auth methods again
+ ResetAuthMethods();
+ } else if (buttonPressed == 1) // Cancel button
+ {
+ MOZ_LOG(IMAP, LogLevel::Warning, ("cancel button pressed"));
+ break; // Abort quickly
+ }
+
+ // TODO what is this for? When does it get set to != unknown again?
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown;
+ SendSetBiffIndicatorEvent(m_currentBiffState);
+ } // all methods failed
+ } // login failed
+ } // while
+
+ if (loginSucceeded) {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("login succeeded"));
+ bool passwordAlreadyVerified;
+ m_hostSessionList->SetPasswordForHost(GetImapServerKey(), password);
+ rv = m_hostSessionList->GetPasswordVerifiedOnline(GetImapServerKey(),
+ passwordAlreadyVerified);
+ if (NS_SUCCEEDED(rv) && !passwordAlreadyVerified) {
+ // First successful login for this server/host during this session.
+ m_hostSessionList->SetPasswordVerifiedOnline(GetImapServerKey());
+ }
+
+ bool imapPasswordIsNew = !passwordAlreadyVerified;
+ if (imapPasswordIsNew) {
+ if (m_currentBiffState == nsIMsgFolder::nsMsgBiffState_Unknown) {
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail;
+ SendSetBiffIndicatorEvent(m_currentBiffState);
+ }
+ m_imapServerSink->SetUserAuthenticated(true);
+ }
+
+ nsImapAction imapAction;
+ m_runningUrl->GetImapAction(&imapAction);
+ // We don't want to do any more processing if we're just
+ // verifying the ability to logon because it can leave us in
+ // a half-constructed state.
+ if (imapAction != nsIImapUrl::nsImapVerifylogon)
+ ProcessAfterAuthenticated();
+ } else // login failed
+ {
+ MOZ_LOG(IMAP, LogLevel::Error, ("login failed entirely"));
+ m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown;
+ SendSetBiffIndicatorEvent(m_currentBiffState);
+ HandleCurrentUrlError();
+ SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib
+ }
+
+ return loginSucceeded;
+}
+
+void nsImapProtocol::UpdateFolderQuotaData(nsImapQuotaAction aAction,
+ nsCString& aQuotaRoot,
+ uint64_t aUsed, uint64_t aMax) {
+ NS_ASSERTION(m_imapMailFolderSink, "m_imapMailFolderSink is null!");
+
+ m_imapMailFolderSink->SetFolderQuotaData(aAction, aQuotaRoot, aUsed, aMax);
+}
+
+void nsImapProtocol::GetQuotaDataIfSupported(const char* aBoxName) {
+ // If server doesn't have quota support, don't do anything
+ if (!(GetServerStateParser().GetCapabilityFlag() & kQuotaCapability)) return;
+
+ nsCString escapedName;
+ CreateEscapedMailboxName(aBoxName, escapedName);
+
+ IncrementCommandTagNumber();
+
+ nsAutoCString quotacommand(GetServerCommandTag());
+ quotacommand.AppendLiteral(" getquotaroot \"");
+ quotacommand.Append(escapedName);
+ quotacommand.AppendLiteral("\"" CRLF);
+
+ NS_ASSERTION(m_imapMailFolderSink, "m_imapMailFolderSink is null!");
+ if (m_imapMailFolderSink)
+ m_imapMailFolderSink->SetFolderQuotaCommandIssued(true);
+
+ nsresult quotarv = SendData(quotacommand.get());
+ if (NS_SUCCEEDED(quotarv))
+ ParseIMAPandCheckForNewMail(nullptr, true); // don't display errors.
+}
+
+bool nsImapProtocol::GetDeleteIsMoveToTrash() {
+ bool rv = false;
+ NS_ASSERTION(m_hostSessionList, "fatal... null host session list");
+ if (m_hostSessionList)
+ m_hostSessionList->GetDeleteIsMoveToTrashForHost(GetImapServerKey(), rv);
+ return rv;
+}
+
+bool nsImapProtocol::GetShowDeletedMessages() {
+ bool rv = false;
+ if (m_hostSessionList)
+ m_hostSessionList->GetShowDeletedMessagesForHost(GetImapServerKey(), rv);
+ return rv;
+}
+
+bool nsImapProtocol::CheckNeeded() {
+ if (m_flagChangeCount >= kFlagChangesBeforeCheck) return true;
+
+ int32_t deltaInSeconds;
+
+ PRTime2Seconds(PR_Now() - m_lastCheckTime, &deltaInSeconds);
+
+ return (deltaInSeconds >= kMaxSecondsBeforeCheck);
+}
+
+bool nsImapProtocol::UseCondStore() {
+ // Check that the server is capable of cond store, and the user
+ // hasn't disabled the use of constore for this server.
+ return m_useCondStore &&
+ GetServerStateParser().GetCapabilityFlag() & kHasCondStoreCapability &&
+ GetServerStateParser().fUseModSeq;
+}
+
+bool nsImapProtocol::UseCompressDeflate() {
+ // Check that the server is capable of compression, and the user
+ // hasn't disabled the use of compression for this server.
+ return m_useCompressDeflate && GetServerStateParser().GetCapabilityFlag() &
+ kHasCompressDeflateCapability;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////
+// The following is the implementation of nsImapMockChannel and an intermediary
+// imap steam listener. The stream listener is used to make a clean binding
+// between the imap mock channel and the memory cache channel (if we are reading
+// from the cache)
+// Used by both offline storage "cache" and by the system cache called "cache2".
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+// WARNING: the cache stream listener is intended to be accessed from the UI
+// thread! it will NOT create another proxy for the stream listener that gets
+// passed in...
+class nsImapCacheStreamListener : public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsImapCacheStreamListener();
+
+ nsresult Init(nsIStreamListener* aStreamListener,
+ nsIImapMockChannel* aMockChannelToUse, bool cache2 = false);
+
+ protected:
+ virtual ~nsImapCacheStreamListener();
+ nsCOMPtr<nsIImapMockChannel> mChannelToUse;
+ nsCOMPtr<nsIStreamListener> mListener;
+ bool mCache2; // Initialized for cache2 usage
+ bool mStarting; // Used with cache2. Indicates 1st data segment is read.
+
+ private:
+ static bool mGoodCache2;
+ static const uint32_t kPeekBufSize;
+ static nsresult Peeker(nsIInputStream* aInStr, void* aClosure,
+ const char* aBuffer, uint32_t aOffset, uint32_t aCount,
+ uint32_t* aCountWritten);
+};
+
+NS_IMPL_ADDREF(nsImapCacheStreamListener)
+NS_IMPL_RELEASE(nsImapCacheStreamListener)
+
+NS_INTERFACE_MAP_BEGIN(nsImapCacheStreamListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+NS_INTERFACE_MAP_END
+
+nsImapCacheStreamListener::nsImapCacheStreamListener() {}
+bool nsImapCacheStreamListener::mGoodCache2 = false;
+const uint32_t nsImapCacheStreamListener::kPeekBufSize = 101;
+
+nsImapCacheStreamListener::~nsImapCacheStreamListener() { mStarting = true; }
+
+nsresult nsImapCacheStreamListener::Init(nsIStreamListener* aStreamListener,
+ nsIImapMockChannel* aMockChannelToUse,
+ bool aCache2 /*false*/) {
+ NS_ENSURE_ARG(aStreamListener);
+ NS_ENSURE_ARG(aMockChannelToUse);
+
+ mChannelToUse = aMockChannelToUse;
+ mListener = aStreamListener;
+ mCache2 = aCache2;
+ mStarting = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapCacheStreamListener::OnStartRequest(nsIRequest* request) {
+ if (!mChannelToUse) {
+ NS_ERROR("OnStartRequest called after OnStopRequest");
+ return NS_ERROR_NULL_POINTER;
+ }
+ if (!mCache2 || !mStarting) {
+ return mListener->OnStartRequest(mChannelToUse);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapCacheStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ if (!mListener) {
+ NS_ERROR("OnStopRequest called twice");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsresult rv = NS_OK;
+ if (!mCache2 || !mStarting) {
+ rv = mListener->OnStopRequest(mChannelToUse, aStatus);
+
+ mListener = nullptr;
+ mChannelToUse->Close();
+ mChannelToUse = nullptr;
+ }
+ return rv;
+}
+
+/*
+ * Called when cache2 is in effect on first available data segment returned
+ * to check that cache entry looks like it it a valid email header. With
+ * cache2 memory cache this could be done synchronously. But with disk cache
+ * it can only be done asynchronously like this.
+ * Note: If NS_OK returned, the peeked at bytes are consumed here and not passed
+ * on to the listener so a special return value is used.
+ */
+nsresult nsImapCacheStreamListener::Peeker(nsIInputStream* aInStr,
+ void* aClosure, const char* aBuffer,
+ uint32_t aOffset, uint32_t aCount,
+ uint32_t* aCountWritten) {
+ char peekBuf[kPeekBufSize];
+ aCount = aCount > sizeof peekBuf ? sizeof peekBuf : aCount;
+ memcpy(peekBuf, aBuffer, aCount);
+ peekBuf[aCount] = 0; // Null terminate the starting header data.
+ int32_t findPos = MsgFindCharInSet(nsDependentCString(peekBuf), ":\n\r", 0);
+ // Check that the first line is a header line, i.e., with a ':' in it
+ // Or that it begins with "From " because some IMAP servers allow that,
+ // even though it's technically invalid.
+ mGoodCache2 = ((findPos != -1 && peekBuf[findPos] == ':') ||
+ !(strncmp(peekBuf, "From ", 5)));
+ return NS_BASE_STREAM_WOULD_BLOCK; // So stream buffer not "consumed"
+}
+
+NS_IMETHODIMP
+nsImapCacheStreamListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* aInStream,
+ uint64_t aSourceOffset,
+ uint32_t aCount) {
+ if (mCache2 && mStarting) {
+ // Peeker() does check of leading bytes and sets mGoodCache2.
+ uint32_t numRead;
+ aInStream->ReadSegments(Peeker, nullptr, kPeekBufSize - 1, &numRead);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: mGoodCache2=%d(bool)", __func__, mGoodCache2));
+
+ if (mGoodCache2) {
+ // Do deferred setup of loadGroup and OnStartRequest and then forward
+ // the verified first segment to the actual listener.
+ mStarting = false;
+ mListener->OnStartRequest(mChannelToUse);
+ } else {
+ MOZ_LOG(IMAPCache, LogLevel::Error,
+ ("%s: cache entry bad so just read imap here", __func__));
+ mChannelToUse->ReadFromImapConnection();
+ return NS_ERROR_FAILURE; // no more starts, one more stop occurs
+ }
+ }
+ // Forward the segment to the actual listener.
+ return mListener->OnDataAvailable(mChannelToUse, aInStream, aSourceOffset,
+ aCount);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsImapMockChannel, nsHashPropertyBag,
+ nsIImapMockChannel, nsIMailChannel, nsIChannel,
+ nsIRequest, nsICacheEntryOpenCallback,
+ nsITransportEventSink, nsISupportsWeakReference)
+
+nsImapMockChannel::nsImapMockChannel()
+ : mSuspendedMonitor("nsImapMockChannel"), mSuspended(false) {
+ m_cancelStatus = NS_OK;
+ mLoadFlags = 0;
+ mChannelClosed = false;
+ mReadingFromCache = false;
+ mContentLength = mozilla::dom::InternalResponse::UNKNOWN_BODY_SIZE;
+ mContentDisposition = nsIChannel::DISPOSITION_INLINE;
+ mWritingToCache = false;
+}
+
+nsImapMockChannel::~nsImapMockChannel() {
+ // if we're offline, we may not get to close the channel correctly.
+ // we need to do this to send the url state change notification in
+ // the case of mem and disk cache reads.
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "should only access mock channel on ui thread");
+ if (!mChannelClosed) Close();
+}
+
+nsresult nsImapMockChannel::NotifyStartEndReadFromCache(bool start) {
+ nsresult rv = NS_OK;
+ mReadingFromCache = start;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url, &rv);
+ nsCOMPtr<nsIImapProtocol> imapProtocol = do_QueryReferent(mProtocol);
+ if (imapUrl) {
+ nsCOMPtr<nsIImapMailFolderSink> folderSink;
+ rv = imapUrl->GetImapMailFolderSink(getter_AddRefs(folderSink));
+ if (folderSink) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(m_url);
+ rv = folderSink->SetUrlState(nullptr /* we don't know the protocol */,
+ mailUrl, start, false, m_cancelStatus);
+
+ // Required for killing ImapProtocol thread
+ if (NS_FAILED(m_cancelStatus) && imapProtocol)
+ imapProtocol->TellThreadToDie(false);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMockChannel::Close() {
+ if (mReadingFromCache)
+ NotifyStartEndReadFromCache(false);
+ else {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsICacheEntry> cacheEntry;
+ mailnewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry));
+ if (cacheEntry) cacheEntry->MarkValid();
+ // remove the channel from the load group
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ GetLoadGroup(getter_AddRefs(loadGroup));
+ // if the mock channel wasn't initialized with a load group then
+ // use our load group (they may differ)
+ if (!loadGroup) mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (loadGroup)
+ loadGroup->RemoveRequest((nsIRequest*)this, nullptr, NS_OK);
+ }
+ }
+
+ m_channelListener = nullptr;
+ mChannelClosed = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetProgressEventSink(
+ nsIProgressEventSink** aProgressEventSink) {
+ NS_IF_ADDREF(*aProgressEventSink = mProgressEventSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetProgressEventSink(
+ nsIProgressEventSink* aProgressEventSink) {
+ mProgressEventSink = aProgressEventSink;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetChannelListener(
+ nsIStreamListener** aChannelListener) {
+ NS_IF_ADDREF(*aChannelListener = m_channelListener);
+ return NS_OK;
+}
+
+// now implement our mock implementation of the channel interface...we forward
+// all calls to the real channel if we have one...otherwise we return something
+// bogus...
+
+NS_IMETHODIMP nsImapMockChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ m_loadGroup = aLoadGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ NS_IF_ADDREF(*aLoadGroup = m_loadGroup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ NS_IF_ADDREF(*aLoadInfo = m_loadInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) {
+ m_loadInfo = aLoadInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetOriginalURI(nsIURI** aURI) {
+ // IMap does not seem to have the notion of an original URI :-(
+ // *aURI = m_originalUrl ? m_originalUrl : m_url;
+ NS_IF_ADDREF(*aURI = m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetOriginalURI(nsIURI* aURI) {
+ // IMap does not seem to have the notion of an original URI :-(
+ // MOZ_ASSERT_UNREACHABLE("nsImapMockChannel::SetOriginalURI");
+ // return NS_ERROR_NOT_IMPLEMENTED;
+ return NS_OK; // ignore
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetURI(nsIURI* aURI) {
+ m_url = aURI;
+#ifdef DEBUG_bienvenu
+ if (!aURI) printf("Clearing URI\n");
+#endif
+ if (m_url) {
+ // if we don't have a progress event sink yet, get it from the url for
+ // now...
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ if (mailnewsUrl && !mProgressEventSink) {
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mailnewsUrl->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ mProgressEventSink = do_QueryInterface(statusFeedback);
+ }
+ // If this is a fetch URL and we can, get the message size from the message
+ // header and set it to be the content length.
+ // Note that for an attachment URL, this will set the content length to be
+ // equal to the size of the entire message.
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url));
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+ if (imapAction == nsIImapUrl::nsImapMsgFetch) {
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url));
+ if (msgUrl) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ // A failure to get a message header isn't an error
+ msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t messageSize;
+ if (NS_SUCCEEDED(msgHdr->GetMessageSize(&messageSize)))
+ SetContentLength(messageSize);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::Open(nsIInputStream** _retval) {
+ nsCOMPtr<nsIStreamListener> listener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_url) {
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+ // If we're shutting down, and not running the kinds of urls we run at
+ // shutdown, then this should fail because running urls during
+ // shutdown will very likely fail and potentially hang.
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool shuttingDown = false;
+ (void)accountMgr->GetShutdownInProgress(&shuttingDown);
+ if (shuttingDown && imapAction != nsIImapUrl::nsImapExpungeFolder &&
+ imapAction != nsIImapUrl::nsImapDeleteAllMsgs &&
+ imapAction != nsIImapUrl::nsImapDeleteFolder)
+ return NS_ERROR_FAILURE;
+ }
+ return NS_ImplementChannelOpen(this, _retval);
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::OnCacheEntryAvailable(nsICacheEntry* entry, bool aNew,
+ nsresult status) {
+ if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Create/write new cache entry=%s", __func__,
+ aNew ? "true" : "false"));
+ if (NS_SUCCEEDED(status)) {
+ nsAutoCString key;
+ entry->GetKey(key);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Cache entry key = |%s|", __func__, key.get()));
+ }
+ }
+
+ // make sure we didn't close the channel before the async call back came in...
+ // hmmm....if we had write access and we canceled this mock channel then I
+ // wonder if we should be invalidating the cache entry before kicking out...
+ if (mChannelClosed) {
+ if (NS_SUCCEEDED(status)) {
+ entry->AsyncDoom(nullptr);
+ }
+ return NS_OK;
+ }
+
+ if (!m_url) {
+ // Something has gone terribly wrong.
+ NS_WARNING("m_url is null in OnCacheEntryAvailable");
+ return Cancel(NS_ERROR_UNEXPECTED);
+ }
+
+ do {
+ // For "normal" read/write access we always see status == NS_OK here. aNew
+ // indicates whether the cache entry is new and needs to be written, or not
+ // new and can be read. If AsyncOpenURI() was called with access read-only,
+ // status == NS_ERROR_CACHE_KEY_NOT_FOUND can be received here and we just
+ // read the data directly from imap.
+ if (NS_FAILED(status)) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: status parameter bad, preference "
+ "browser.cache.memory.enable not true?",
+ __func__));
+ break;
+ }
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url, &rv);
+ mailnewsUrl->SetMemCacheEntry(entry);
+
+ if (aNew) {
+ // Writing cache so insert a "stream listener Tee" into the stream from
+ // the imap fetch to direct the message into the cache and to our current
+ // channel listener. But first get the size of the message to be fetched.
+ // If message too big to fit in cache, the message just goes to the
+ // stream listener. If unable to get the size, messageSize remains 0 so
+ // assume it fits in cache, right or wrong.
+ uint32_t messageSize = 0;
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url));
+ if (msgUrl) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ msgHdr->GetMessageSize(&messageSize);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: messageSize=%d", __func__, messageSize));
+ } else
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Can't get msgHdr", __func__));
+ }
+ // Check if message fits in a cache entry. If too big, or if unable to
+ // create or initialize the tee or open the stream to the entry, will
+ // fall thought and only do ReadFromImapConnection() called below and the
+ // message will not be cached.
+ bool tooBig =
+ net::CacheObserver::EntryIsTooBig(messageSize, gUseDiskCache2);
+ if (!tooBig) {
+ // Message fits in cache. Create the tee.
+ nsCOMPtr<nsIStreamListenerTee> tee =
+ do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIOutputStream> out;
+ rv = entry->OpenOutputStream(0, -1, getter_AddRefs(out));
+ if (NS_SUCCEEDED(rv)) {
+ rv = tee->Init(m_channelListener, out, nullptr);
+ m_channelListener = tee;
+ } else
+ NS_WARNING(
+ "IMAP Protocol failed to open output stream to Necko cache");
+ }
+ }
+ if (tooBig || NS_FAILED(rv)) {
+ // Need this so next OpenCacheEntry() triggers OnCacheEntryAvailable()
+ // since nothing was actually written to cache. Without this there is no
+ // response to next OpenCacheEntry call.
+ entry->AsyncDoom(nullptr);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Not writing to cache, msg too big or other errors",
+ __func__));
+ } else {
+ mWritingToCache = true;
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Begin cache WRITE", __func__));
+ }
+ } else {
+ // We are reading cache (!aNew)
+ mWritingToCache = false;
+ if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) {
+ int64_t size = 0;
+ rv = entry->GetDataSize(&size);
+ if (rv == NS_ERROR_IN_PROGRESS)
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Concurrent cache READ, no size available", __func__));
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Begin cache READ, size=%" PRIi64, __func__, size));
+ }
+ rv = ReadFromCache2(entry);
+ if (NS_SUCCEEDED(rv)) {
+ NotifyStartEndReadFromCache(true);
+ entry->MarkValid();
+ return NS_OK; // Return here since reading from the cache succeeded.
+ }
+ entry->AsyncDoom(nullptr); // Doom entry if we failed to read from cache.
+ mailnewsUrl->SetMemCacheEntry(
+ nullptr); // We aren't going to be reading from the cache.
+ }
+ } while (false);
+
+ // If reading from the cache failed or if we are writing into the cache, or if
+ // or message is too big for cache or other errors occur, do
+ // ReadFromImapConnection to fetch message from imap server.
+ if (!aNew)
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Cache READ failed so read from imap", __func__));
+ return ReadFromImapConnection();
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::OnCacheEntryCheck(nsICacheEntry* entry, uint32_t* aResult) {
+ *aResult = nsICacheEntryOpenCallback::ENTRY_WANTED;
+
+ // Check concurrent read: We can't read concurrently since we don't know
+ // that the entry will ever be written successfully. It may be aborted
+ // due to a size limitation. If reading concurrently, the following function
+ // will return NS_ERROR_IN_PROGRESS. Then we tell the cache to wait until
+ // the write is finished.
+ int64_t size = 0;
+ nsresult rv = entry->GetDataSize(&size);
+ if (rv == NS_ERROR_IN_PROGRESS) {
+ *aResult = nsICacheEntryOpenCallback::RECHECK_AFTER_WRITE_FINISHED;
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("OnCacheEntryCheck(): Attempted cache write while reading, will "
+ "try again"));
+ }
+ return NS_OK;
+}
+
+nsresult nsImapMockChannel::OpenCacheEntry() {
+ nsresult rv;
+ if (!gCache2Storage) {
+ // Only need to do this once since cache2 is used by all accounts and
+ // folders. Get the cache storage object from the imap service.
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Obtain the cache storage object used by all channels in this session.
+ // This will return disk cache (default) or memory cache as determined by
+ // the boolean pref "mail.imap.use_disk_cache2"
+ rv = imapService->GetCacheStorage(getter_AddRefs(gCache2Storage));
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Obtained storage obj for |%s| cache2", __func__,
+ gUseDiskCache2 ? "disk" : "mem"));
+ }
+
+ int32_t uidValidity = -1;
+ uint32_t cacheAccess = nsICacheStorage::OPEN_NORMALLY;
+
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapMailFolderSink> folderSink;
+ rv = imapUrl->GetImapMailFolderSink(getter_AddRefs(folderSink));
+ if (folderSink) folderSink->GetUidValidity(&uidValidity);
+
+ // If we're storing the message in the offline store, don't
+ // write/save to cache2 cache. (Not sure if this even happens!)
+ bool storeResultsOffline;
+ imapUrl->GetStoreResultsOffline(&storeResultsOffline);
+ if (storeResultsOffline) cacheAccess = nsICacheStorage::OPEN_READONLY;
+
+ // clang-format off
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: For URL = |%s|", __func__, m_url->GetSpecOrDefault().get()));
+ // clang-format on
+
+ // Use the uid validity as part of the cache key, so that if the uid validity
+ // changes, we won't re-use the wrong cache entries.
+ nsAutoCString extension;
+ extension.AppendInt(uidValidity, 16);
+
+ // Open a cache entry where the key is the potentially modified URL.
+ nsAutoCString path;
+ m_url->GetPathQueryRef(path);
+
+ // First we need to "normalise" the URL by extracting ?part= and &filename.
+ // The path should only contain: ?part=x.y&filename=file.ext
+ // These are seen in the wild:
+ // /;section=2?part=1.2&filename=A01.JPG
+ // ?section=2?part=1.2&filename=A01.JPG&type=image/jpeg&filename=A01.JPG
+ // ?part=1.2&type=image/jpeg&filename=IMG_C0030.jpg
+ // ?header=quotebody&part=1.2&filename=lijbmghmkilicioj.png
+ nsCString partQuery = MsgExtractQueryPart(path, "?part=");
+ if (partQuery.IsEmpty()) {
+ partQuery = MsgExtractQueryPart(path, "&part=");
+ if (!partQuery.IsEmpty()) {
+ // ? indicates a part query, so set the first character to that.
+ partQuery.SetCharAt('?', 0);
+ }
+ }
+ nsCString filenameQuery = MsgExtractQueryPart(path, "&filename=");
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: part = |%s|, filename = |%s|", __func__, partQuery.get(),
+ filenameQuery.get()));
+
+ // Truncate path at either /; or ?
+ MsgRemoveQueryPart(path);
+
+ nsCOMPtr<nsIURI> newUri;
+ rv = NS_MutateURI(m_url).SetPathQueryRef(path).Finalize(newUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (partQuery.IsEmpty()) {
+ // Not accessing a part but the whole message.
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Call AsyncOpenURI on entire message", __func__));
+ } else {
+ // Access just a part. Set up part extraction and read in the part from the
+ // whole cached message. Note: Parts are now never individually written to
+ // or read from cache.
+ SetupPartExtractorListener(imapUrl, m_channelListener);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Call AsyncOpenURI to read part from entire message cache",
+ __func__));
+ }
+ return gCache2Storage->AsyncOpenURI(newUri, extension, cacheAccess, this);
+}
+
+// Pumps content of cache2 entry to channel listener. If a part was
+// requested in the original URL seen in OpenCacheEntry(), it will be extracted
+// from the whole message by the channel listener. So to obtain a single part
+// always requires reading the complete message from cache.
+nsresult nsImapMockChannel::ReadFromCache2(nsICacheEntry* entry) {
+ NS_ENSURE_ARG(entry);
+
+ bool useCacheEntry = true;
+ nsresult rv;
+ nsAutoCString entryKey;
+
+ entry->GetKey(entryKey);
+
+ // Compare cache entry size with message size. Init to an invalid value.
+ int64_t entrySize = -1;
+
+ // We don't expect concurrent read here, so this call should always work.
+ rv = entry->GetDataSize(&entrySize);
+
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url));
+ if (msgUrl && NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ // A failure to get a message header isn't an automatic error
+ msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t messageSize;
+ if (NS_SUCCEEDED(rv = msgHdr->GetMessageSize(&messageSize)) &&
+ messageSize != entrySize) {
+ // clang-format off
+ MOZ_LOG(IMAP, LogLevel::Warning,
+ ("%s: Size mismatch for %s: message %" PRIu32
+ ", cache %" PRIi64,
+ __func__, entryKey.get(), messageSize, entrySize));
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Size mismatch for %s: message %" PRIu32
+ ", cache %" PRIi64,
+ __func__, entryKey.get(), messageSize, entrySize));
+ // clang-format on
+ useCacheEntry = false;
+ }
+ }
+ }
+ // Cache entry is invalid if GetDataSize() or GetMessageSize failed or if
+ // otherwise unable to obtain the cache entry size. (Not sure if it's possible
+ // to have a 0 length cache entry but negative is definitely invalid.)
+ if (NS_FAILED(rv) || entrySize < 1) useCacheEntry = false;
+
+ nsCOMPtr<nsIInputStream> ins;
+ if (useCacheEntry) {
+ if (NS_SUCCEEDED(rv = entry->OpenInputStream(0, getter_AddRefs(ins)))) {
+ uint64_t bytesAvailable = 0;
+ rv = ins->Available(&bytesAvailable);
+ // Note: bytesAvailable will usually be zero (at least for disk cache
+ // since only async access occurs) so don't check it.
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Input stream for disk cache not useable", __func__));
+ useCacheEntry = false;
+ }
+ }
+ }
+
+ if (NS_SUCCEEDED(rv) && useCacheEntry) {
+ nsCOMPtr<nsIInputStreamPump> pump;
+ if (NS_SUCCEEDED(
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), ins.forget()))) {
+ // Create and use a cache listener object.
+ RefPtr<nsImapCacheStreamListener> cacheListener =
+ new nsImapCacheStreamListener();
+
+ cacheListener->Init(m_channelListener, this, true);
+ rv = pump->AsyncRead(cacheListener);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url);
+ imapUrl->SetMsgLoadingFromCache(true);
+ // Set the cache entry's security info status as our security
+ // info status...
+ nsCOMPtr<nsITransportSecurityInfo> securityInfo;
+ entry->GetSecurityInfo(getter_AddRefs(securityInfo));
+ SetSecurityInfo(securityInfo);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Cache entry accepted and being read", __func__));
+ } // if AsyncRead succeeded.
+ } // new pump
+ } // if useCacheEntry
+
+ if (!useCacheEntry || NS_FAILED(rv)) {
+ // Cache entry appears to be unusable. Return an error so will still attempt
+ // to read the data via just an imap fetch (the "old fashioned" way).
+ if (NS_SUCCEEDED(rv)) rv = NS_ERROR_FAILURE;
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Cache entry rejected, returning error %" PRIx32, __func__,
+ static_cast<uint32_t>(rv)));
+ }
+ return rv;
+}
+
+class nsReadFromImapConnectionFailure : public mozilla::Runnable {
+ public:
+ explicit nsReadFromImapConnectionFailure(nsImapMockChannel* aChannel)
+ : mozilla::Runnable("nsReadFromImapConnectionFailure"),
+ mImapMockChannel(aChannel) {}
+
+ NS_IMETHOD Run() {
+ if (mImapMockChannel) {
+ mImapMockChannel->RunOnStopRequestFailure();
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<nsImapMockChannel> mImapMockChannel;
+};
+
+nsresult nsImapMockChannel::RunOnStopRequestFailure() {
+ if (m_channelListener) {
+ m_channelListener->OnStopRequest(this, NS_MSG_ERROR_MSG_NOT_OFFLINE);
+ }
+ return NS_OK;
+}
+
+// This is called when the message requested by the url isn't yet in offline
+// store or not yet in cache. It is also called if the storage is corrupt. This
+// creates an imap connection to process the url. This is usually called from
+// the mock channel or possibly from nsImapCacheStreamListener::OnDataAvailable.
+NS_IMETHODIMP nsImapMockChannel::ReadFromImapConnection() {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+
+ bool localOnly = false;
+ imapUrl->GetLocalFetchOnly(&localOnly);
+ if (localOnly) {
+ // This will cause an OnStartRunningUrl, and the subsequent close
+ // will then cause an OnStopRunningUrl with the cancel status.
+ NotifyStartEndReadFromCache(true);
+ Cancel(NS_MSG_ERROR_MSG_NOT_OFFLINE);
+
+ // Dispatch error notification, so ReadFromImapConnection() returns *before*
+ // the error is sent to the listener's OnStopRequest(). This avoids
+ // endless recursion where the caller relies on async execution.
+ nsCOMPtr<nsIRunnable> event = new nsReadFromImapConnectionFailure(this);
+ NS_DispatchToCurrentThread(event);
+ return NS_MSG_ERROR_MSG_NOT_OFFLINE;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ GetLoadGroup(getter_AddRefs(loadGroup));
+ if (!loadGroup) // if we don't have one, the url will snag one from the msg
+ // window...
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ // okay, add the mock channel to the load group..
+ if (loadGroup)
+ loadGroup->AddRequest((nsIRequest*)this, nullptr /* context isupports */);
+
+ // loading the url consists of asking the server to add the url to it's imap
+ // event queue....
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = mailnewsUrl->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapIncomingServer> imapServer(do_QueryInterface(server, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Assume AsyncRead is always called from the UI thread.....
+ return imapServer->GetImapConnectionAndLoadUrl(imapUrl, m_channelListener);
+}
+
+// for messages stored in our offline cache, we have special code to handle
+// that... If it's in the local cache, we return true and we can abort the
+// download because this method does the rest of the work.
+bool nsImapMockChannel::ReadFromLocalCache() {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url, &rv);
+
+ bool useLocalCache = false;
+ mailnewsUrl->GetMsgIsInLocalCache(&useLocalCache);
+ if (!useLocalCache) {
+ return false;
+ }
+
+ nsAutoCString messageIdString;
+
+ SetupPartExtractorListener(imapUrl, m_channelListener);
+
+ imapUrl->GetListOfMessageIds(messageIdString);
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = mailnewsUrl->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, false);
+ if (!folder) {
+ return false;
+ }
+ // we want to create a file channel and read the msg from there.
+ nsMsgKey msgKey = strtoul(messageIdString.get(), nullptr, 10);
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = folder->GetMessageHeader(msgKey, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, false);
+ nsCOMPtr<nsIInputStream> msgStream;
+ rv = folder->GetLocalMsgStream(hdr, getter_AddRefs(msgStream));
+ NS_ENSURE_SUCCESS(rv, false);
+ // dougt - This may break the ablity to "cancel" a read from offline
+ // mail reading. fileChannel->SetLoadGroup(m_loadGroup);
+ RefPtr<nsImapCacheStreamListener> cacheListener =
+ new nsImapCacheStreamListener();
+ cacheListener->Init(m_channelListener, this);
+
+ // create a stream pump that will async read the message.
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), msgStream.forget());
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = pump->AsyncRead(cacheListener);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // if the msg is unread, we should mark it read on the server. This lets
+ // the code running this url know we're loading from the cache, if it cares.
+ imapUrl->SetMsgLoadingFromCache(true);
+ return true;
+}
+
+NS_IMETHODIMP nsImapMockChannel::AsyncOpen(nsIStreamListener* aListener) {
+ nsCOMPtr<nsIStreamListener> listener = aListener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ if (!m_url) return NS_ERROR_NULL_POINTER;
+ rv = m_url->GetPort(&port);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = NS_CheckPortSafety(port, "imap");
+ if (NS_FAILED(rv)) return rv;
+
+ // set the stream listener and then load the url
+ NS_ASSERTION(!m_channelListener, "shouldn't already have a listener");
+ m_channelListener = listener;
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url));
+
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+
+ bool externalLink = true;
+ imapUrl->GetExternalLinkUrl(&externalLink);
+
+ if (externalLink) {
+ // for security purposes, only allow imap urls originating from external
+ // sources perform a limited set of actions. Currently the allowed set
+ // includes: 1) folder selection 2) message fetch 3) message part fetch
+
+ if (!(imapAction == nsIImapUrl::nsImapSelectFolder ||
+ imapAction == nsIImapUrl::nsImapMsgFetch ||
+ imapAction == nsIImapUrl::nsImapOpenMimePart ||
+ imapAction == nsIImapUrl::nsImapMsgFetchPeek))
+ return NS_ERROR_FAILURE; // abort the running of this url....it failed a
+ // security check
+ }
+
+ if (ReadFromLocalCache()) {
+ (void)NotifyStartEndReadFromCache(true);
+ return NS_OK;
+ }
+
+ // okay, it's not in the local cache, now check the memory cache...
+ // but we can't download for offline use from the memory cache
+ if (imapAction != nsIImapUrl::nsImapMsgDownloadForOffline) {
+ rv = OpenCacheEntry();
+ if (NS_SUCCEEDED(rv)) return rv;
+ }
+
+ SetupPartExtractorListener(imapUrl, m_channelListener);
+ // if for some reason open cache entry failed then just default to opening an
+ // imap connection for the url
+ return ReadFromImapConnection();
+}
+
+nsresult nsImapMockChannel::SetupPartExtractorListener(
+ nsIImapUrl* aUrl, nsIStreamListener* aConsumer) {
+ // if the url we are loading refers to a specific part then we need
+ // libmime to extract that part from the message for us.
+ bool refersToPart = false;
+ aUrl->GetMimePartSelectorDetected(&refersToPart);
+ if (refersToPart) {
+ nsCOMPtr<nsIStreamConverterService> converter =
+ do_GetService("@mozilla.org/streamConverters;1");
+ if (converter && aConsumer) {
+ nsCOMPtr<nsIStreamListener> newConsumer;
+ converter->AsyncConvertData("message/rfc822", "*/*", aConsumer,
+ static_cast<nsIChannel*>(this),
+ getter_AddRefs(newConsumer));
+ if (newConsumer) m_channelListener = newConsumer;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ //*aLoadFlags = nsIRequest::LOAD_NORMAL;
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK; // don't fail when trying to set this
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetContentType(nsACString& aContentType) {
+ if (mContentType.IsEmpty()) {
+ nsImapAction imapAction = 0;
+ if (m_url) {
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url);
+ if (imapUrl) {
+ imapUrl->GetImapAction(&imapAction);
+ }
+ }
+ if (imapAction == nsIImapUrl::nsImapSelectFolder)
+ aContentType.AssignLiteral("x-application-imapfolder");
+ else
+ aContentType.AssignLiteral("message/rfc822");
+ } else
+ aContentType = mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetContentType(
+ const nsACString& aContentType) {
+ nsAutoCString charset;
+ nsresult rv =
+ NS_ParseResponseContentType(aContentType, mContentType, charset);
+ if (NS_FAILED(rv) || mContentType.IsEmpty())
+ mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetContentCharset(
+ nsACString& aContentCharset) {
+ aContentCharset.Assign(mCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetContentCharset(
+ const nsACString& aContentCharset) {
+ mCharset.Assign(aContentCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::GetContentDisposition(uint32_t* aContentDisposition) {
+ *aContentDisposition = mContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::SetContentDisposition(uint32_t aContentDisposition) {
+ mContentDisposition = aContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetContentLength(int64_t* aContentLength) {
+ *aContentLength = mContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::SetContentLength(int64_t aContentLength) {
+ mContentLength = aContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetOwner(nsISupports** aPrincipal) {
+ NS_IF_ADDREF(*aPrincipal = mOwner);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetOwner(nsISupports* aPrincipal) {
+ mOwner = aPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetSecurityInfo(
+ nsITransportSecurityInfo** aSecurityInfo) {
+ NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetSecurityInfo(
+ nsITransportSecurityInfo* aSecurityInfo) {
+ mSecurityInfo = aSecurityInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::GetIsDocument(bool* aIsDocument) {
+ return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// From nsIRequest
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsImapMockChannel::GetName(nsACString& result) {
+ if (m_url) return m_url->GetSpec(result);
+ result.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::IsPending(bool* result) {
+ *result = m_channelListener != nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetStatus(nsresult* status) {
+ *status = m_cancelStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetWritingToCache(bool aWriting) {
+ mWritingToCache = aWriting;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetWritingToCache(bool* result) {
+ *result = mWritingToCache;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetImapProtocol(nsIImapProtocol* aProtocol) {
+ mProtocol = do_GetWeakReference(aProtocol);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsImapMockChannel::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP nsImapMockChannel::Cancel(nsresult status) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ NS_IsMainThread(),
+ "nsImapMockChannel::Cancel should only be called from UI thread");
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("nsImapMockChannel::%s: entering", __func__));
+ m_cancelStatus = status;
+ nsCOMPtr<nsIImapProtocol> imapProtocol = do_QueryReferent(mProtocol);
+
+ // if we aren't reading from the cache and we get canceled...doom our cache
+ // entry if write is still in progress...
+ if (m_url) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ MOZ_LOG(IMAPCache, LogLevel::Debug,
+ ("%s: Doom cache entry only if writing=%d(bool), url=%s", __func__,
+ mWritingToCache, m_url->GetSpecOrDefault().get()));
+ if (mWritingToCache) DoomCacheEntry(mailnewsUrl);
+ }
+
+ // The associated ImapProtocol thread must be unblocked before being killed.
+ // Otherwise, it will be deadlocked.
+ ResumeAndNotifyOne();
+
+ // Required for killing ImapProtocol thread
+ if (imapProtocol) imapProtocol->TellThreadToDie(false);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMockChannel::GetCanceled(bool* aCanceled) {
+ nsresult status = NS_ERROR_FAILURE;
+ GetStatus(&status);
+ *aCanceled = NS_FAILED(status);
+ return NS_OK;
+}
+
+/**
+ * Suspends the current request. This may have the effect of closing
+ * any underlying transport (in order to free up resources), although
+ * any open streams remain logically opened and will continue delivering
+ * data when the transport is resumed.
+ *
+ * Calling cancel() on a suspended request must not send any
+ * notifications (such as onstopRequest) until the request is resumed.
+ *
+ * NOTE: some implementations are unable to immediately suspend, and
+ * may continue to deliver events already posted to an event queue. In
+ * general, callers should be capable of handling events even after
+ * suspending a request.
+ */
+NS_IMETHODIMP nsImapMockChannel::Suspend() {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("Suspending [this=%p].", this));
+
+ mozilla::MonitorAutoLock lock(mSuspendedMonitor);
+ NS_ENSURE_TRUE(!mSuspended, NS_ERROR_NOT_AVAILABLE);
+ mSuspended = true;
+
+ MOZ_LOG(IMAP, LogLevel::Debug, ("Suspended [this=%p].", this));
+
+ return NS_OK;
+}
+
+/**
+ * Resumes the current request. This may have the effect of re-opening
+ * any underlying transport and will resume the delivery of data to
+ * any open streams.
+ */
+NS_IMETHODIMP nsImapMockChannel::Resume() {
+ MOZ_LOG(IMAP, LogLevel::Debug, ("Resuming [this=%p].", this));
+
+ nsresult rv = ResumeAndNotifyOne();
+
+ MOZ_LOG(IMAP, LogLevel::Debug, ("Resumed [this=%p].", this));
+
+ return rv;
+}
+
+nsresult nsImapMockChannel::ResumeAndNotifyOne() {
+ mozilla::MonitorAutoLock lock(mSuspendedMonitor);
+ NS_ENSURE_TRUE(mSuspended, NS_ERROR_NOT_AVAILABLE);
+ mSuspended = false;
+ lock.Notify();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aNotificationCallbacks) {
+ NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aNotificationCallbacks) {
+ mCallbacks = aNotificationCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMockChannel::OnTransportStatus(nsITransport* transport, nsresult status,
+ int64_t progress, int64_t progressMax) {
+ if (NS_FAILED(m_cancelStatus) || (mLoadFlags & LOAD_BACKGROUND) || !m_url)
+ return NS_OK;
+
+ // these transport events should not generate any status messages
+ if (status == NS_NET_STATUS_RECEIVING_FROM ||
+ status == NS_NET_STATUS_SENDING_TO)
+ return NS_OK;
+
+ if (!mProgressEventSink) {
+ NS_QueryNotificationCallbacks(mCallbacks, m_loadGroup, mProgressEventSink);
+ if (!mProgressEventSink) return NS_OK;
+ }
+
+ nsAutoCString host;
+ m_url->GetHost(host);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ mailnewsUrl->GetServer(getter_AddRefs(server));
+ if (server) server->GetHostName(host);
+ }
+ mProgressEventSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get());
+
+ return NS_OK;
+}
+
+nsIMAPMailboxInfo::nsIMAPMailboxInfo(const nsACString& aName, char aDelimiter) {
+ mMailboxName.Assign(aName);
+ mDelimiter = aDelimiter;
+ mChildrenListed = false;
+}
+
+nsIMAPMailboxInfo::~nsIMAPMailboxInfo() {}
+
+void nsIMAPMailboxInfo::SetChildrenListed(bool childrenListed) {
+ mChildrenListed = childrenListed;
+}
+
+bool nsIMAPMailboxInfo::GetChildrenListed() { return mChildrenListed; }
+
+const nsACString& nsIMAPMailboxInfo::GetMailboxName() { return mMailboxName; }
+
+char nsIMAPMailboxInfo::GetDelimiter() { return mDelimiter; }
diff --git a/comm/mailnews/imap/src/nsImapProtocol.h b/comm/mailnews/imap/src/nsImapProtocol.h
new file mode 100644
index 0000000000..815ac103dc
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapProtocol.h
@@ -0,0 +1,848 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsImapProtocol_h___
+#define nsImapProtocol_h___
+
+#include "mozilla/Attributes.h"
+#include "nsIImapProtocol.h"
+#include "nsIImapUrl.h"
+
+#include "nsMsgProtocol.h"
+#include "nsIStreamListener.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsImapCore.h"
+#include "nsString.h"
+#include "nsIProgressEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsISocketTransport.h"
+
+// UI Thread proxy helper
+#include "nsIImapProtocolSink.h"
+
+#include "../public/nsIImapHostSessionList.h"
+#include "nsImapServerResponseParser.h"
+#include "nsImapFlagAndUidState.h"
+#include "nsImapNamespace.h"
+#include "nsTArray.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsMsgLineBuffer.h" // we need this to use the nsMsgLineStreamBuffer helper class...
+#include "nsIInputStream.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsCOMArray.h"
+#include "nsIThread.h"
+#include "nsIRunnable.h"
+#include "nsIImapMockChannel.h"
+#include "nsILoadGroup.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgWindow.h"
+#include "nsIImapHeaderXferInfo.h"
+#include "nsMsgLineBuffer.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgAsyncPrompter.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "nsSyncRunnableHelpers.h"
+#include "nsICacheEntryOpenCallback.h"
+#include "nsIProtocolProxyCallback.h"
+#include "nsIStringBundle.h"
+#include "nsHashPropertyBag.h"
+#include "nsMailChannel.h"
+
+#include "mozilla/Monitor.h"
+
+class nsIMAPMessagePartID;
+class nsIPrefBranch;
+
+#define kDownLoadCacheSize 16000u // was 1536 - try making it bigger
+
+typedef struct _msg_line_info {
+ const char* adoptedMessageLine;
+ uint32_t uidOfMessage;
+} msg_line_info;
+
+class nsMsgImapLineDownloadCache : public nsIImapHeaderInfo,
+ public nsByteArray {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPHEADERINFO
+ nsMsgImapLineDownloadCache();
+ uint32_t CurrentUID();
+ uint32_t SpaceAvailable();
+ bool CacheEmpty();
+
+ msg_line_info* GetCurrentLineInfo();
+
+ private:
+ virtual ~nsMsgImapLineDownloadCache();
+
+ msg_line_info* fLineInfo;
+ int32_t m_msgSize;
+};
+
+#define kNumHdrsToXfer 10
+
+class nsMsgImapHdrXferInfo : public nsIImapHeaderXferInfo {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPHEADERXFERINFO
+ nsMsgImapHdrXferInfo();
+ void ResetAll(); // reset HeaderInfos for re-use
+ void ReleaseAll(); // release HeaderInfos (frees up memory)
+ // this will return null if we're full, in which case the client code
+ // should transfer the headers and retry.
+ nsIImapHeaderInfo* StartNewHdr();
+ // call when we've finished adding lines to current hdr
+ void FinishCurrentHdr();
+
+ private:
+ virtual ~nsMsgImapHdrXferInfo();
+ nsCOMArray<nsIImapHeaderInfo> m_hdrInfos;
+ int32_t m_nextFreeHdrInfo;
+};
+
+// This class contains the name of a mailbox and whether or not
+// its children have been listed.
+class nsIMAPMailboxInfo {
+ public:
+ nsIMAPMailboxInfo(const nsACString& aName, char aDelimiter);
+ virtual ~nsIMAPMailboxInfo();
+
+ void SetChildrenListed(bool childrenListed);
+ bool GetChildrenListed();
+ const nsACString& GetMailboxName();
+ char GetDelimiter();
+
+ protected:
+ nsCString mMailboxName;
+ bool mChildrenListed;
+ char mDelimiter;
+};
+
+// State Flags (Note, I use the word state in terms of storing
+// state information about the connection (authentication, have we sent
+// commands, etc. I do not intend it to refer to protocol state)
+// Use these flags in conjunction with SetFlag/TestFlag/ClearFlag instead
+// of creating PRBools for everything....
+// clang-format off
+#define IMAP_RECEIVED_GREETING 0x00000001 // should we pause for the next read
+#define IMAP_CONNECTION_IS_OPEN 0x00000004 // is the connection currently open?
+#define IMAP_WAITING_FOR_DATA 0x00000008
+#define IMAP_CLEAN_UP_URL_STATE 0x00000010 // processing clean up url state
+#define IMAP_ISSUED_LANGUAGE_REQUEST 0x00000020 // make sure we only issue the language
+ // request once per connection...
+#define IMAP_ISSUED_COMPRESS_REQUEST 0x00000040 // make sure we only request compression once
+
+// There are 3 types of progress strings for items downloaded from IMAP servers.
+// An index is needed to keep track of the current count of the number of each
+// item type downloaded. The IMAP_EMPTY_STRING_INDEX means no string displayed.
+#define IMAP_NUMBER_OF_PROGRESS_STRINGS 4
+#define IMAP_HEADERS_STRING_INDEX 0
+#define IMAP_FLAGS_STRING_INDEX 1
+#define IMAP_MESSAGES_STRING_INDEX 2
+#define IMAP_EMPTY_STRING_INDEX 3
+// clang-format on
+
+/**
+ * nsImapProtocol is, among other things, the underlying nsIChannel
+ * implementation for the IMAP protocol. However, it's usually hidden away
+ * behind nsImapMockChannel objects. It also represents the 'real' connection
+ * to the IMAP server - it maintains the nsISocketTransport.
+ * Because there can be multiple IMAP requests queued up, NS_NewChannel()
+ * will return nsImapMockChannel objects instead, to keep the request in a
+ * holding pattern until the connection is free. At which time the mock
+ * channel will just forward calls onward to the nsImapProtocol.
+ *
+ * The url scheme we implement here encodes various IMAP commands as URLs.
+ * Some URLs are just traditional I/O based nsIChannel transactions, but
+ * many others have side effects. For example, an IMAP folder discovery
+ * command might cause the creation of the nsImapMailFolder hierarchy under
+ * the the nsImapIncomingServer.
+ * Such side effects are communicated via the various "Sink" interfaces. This
+ * helps decouple the IMAP code from the rest of the system:
+ *
+ * - nsIImapServerSink (implemented by nsImapIncomingServer)
+ * - nsIImapMailFolderSink (implemented by nsImapMailFolder)
+ * - nsIImapMessageSink (implemented by nsImapMailFolder)
+ *
+ * Internal to nsImapProtocol, these sink classes all have corresponding proxy
+ * implementations (ImapServerSinkProxy, ImapMailFolderSinkProxy and
+ * ImapMessageSinkProxy). These allow us to safely call the sink objects, in
+ * a synchronous fashion, from I/O threads (threads other than the main one).
+ * When an IMAP routine calls a member function of one of these sink proxies,
+ * it dispatches a call to the real sink object on the main thread, then
+ * blocks until the call is completed.
+ */
+class nsImapProtocol : public nsIImapProtocol,
+ public nsIInputStreamCallback,
+ public nsSupportsWeakReference,
+ public nsMsgProtocol,
+ public nsIImapProtocolSink,
+ public nsIMsgAsyncPromptListener,
+ public nsIProtocolProxyCallback {
+ public:
+ struct TCPKeepalive {
+ // For enabling and setting TCP keepalive (not related to IMAP IDLE).
+ std::atomic<bool> enabled;
+ std::atomic<int32_t> idleTimeS;
+ std::atomic<int32_t> retryIntervalS;
+ };
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSIPROTOCOLPROXYCALLBACK
+ nsImapProtocol();
+
+ virtual nsresult ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) override;
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImapProtocol interface
+ //////////////////////////////////////////////////////////////////////////////////
+ NS_DECL_NSIIMAPPROTOCOL
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImapProtocolSink interface
+ //////////////////////////////////////////////////////////////////////////////////
+ NS_DECL_NSIIMAPPROTOCOLSINK
+
+ NS_DECL_NSIMSGASYNCPROMPTLISTENER
+
+ // message id string utilities.
+ uint32_t CountMessagesInIdString(const char* idString);
+ static bool HandlingMultipleMessages(const nsCString& messageIdString);
+ // escape slashes and double quotes in username/passwords for insecure login.
+ static void EscapeUserNamePasswordString(const char* strToEscape,
+ nsCString* resultStr);
+
+ // used to start fetching a message.
+ void GetShouldDownloadAllHeaders(bool* aResult);
+ void GetArbitraryHeadersToDownload(nsCString& aResult);
+ virtual void AdjustChunkSize();
+ virtual void FetchMessage(const nsCString& messageIds,
+ nsIMAPeFetchFields whatToFetch,
+ const char* fetchModifier = nullptr,
+ uint32_t startByte = 0, uint32_t numBytes = 0,
+ char* part = 0);
+ void FetchTryChunking(const nsCString& messageIds,
+ nsIMAPeFetchFields whatToFetch, bool idIsUid,
+ char* part, uint32_t downloadSize, bool tryChunking);
+ void FallbackToFetchWholeMsg(const nsCString& messageId,
+ uint32_t messageSize);
+ // used when streaming a message fetch
+ virtual nsresult BeginMessageDownLoad(
+ uint32_t totalSize, // for user, headers and body
+ const char* contentType); // some downloads are header only
+ virtual void HandleMessageDownLoadLine(const char* line, bool isPartialLine,
+ char* lineCopy = nullptr);
+ virtual void NormalMessageEndDownload();
+ virtual void AbortMessageDownLoad();
+ virtual void PostLineDownLoadEvent(const char* line, uint32_t uid);
+ void FlushDownloadCache();
+
+ virtual void SetMailboxDiscoveryStatus(EMailboxDiscoverStatus status);
+ virtual EMailboxDiscoverStatus GetMailboxDiscoveryStatus();
+
+ virtual void ProcessMailboxUpdate(bool handlePossibleUndo);
+ // Send log output...
+ void Log(const char* logSubName, const char* extraInfo, const char* logData);
+ static void LogImapUrl(const char* logMsg, nsIImapUrl* imapUrl);
+ // Comment from 4.5: We really need to break out the thread synchronizer from
+ // the connection class...Not sure what this means
+ bool GetPseudoInterrupted();
+
+ uint32_t GetMessageSize(const nsACString& messageId);
+ bool GetSubscribingNow();
+
+ bool DeathSignalReceived();
+ void ResetProgressInfo();
+ void SetActive(bool active);
+ bool GetActive();
+
+ bool GetShowAttachmentsInline();
+
+ // Sets whether or not the content referenced by the current ActiveEntry has
+ // been modified. Used for MIME parts on demand.
+ void SetContentModified(IMAP_ContentModifiedType modified);
+ bool GetShouldFetchAllParts();
+ bool GetIgnoreExpunges() { return m_ignoreExpunges; }
+ // Generic accessors required by the imap parser
+ char* CreateNewLineFromSocket();
+ nsresult GetConnectionStatus();
+ void SetConnectionStatus(nsresult status);
+
+ // Cleanup the connection and shutdown the thread.
+ void TellThreadToDie();
+
+ const nsCString&
+ GetImapHostName(); // return the host name from the url for the
+ // current connection
+ const nsCString& GetImapUserName(); // return the user name from the identity
+ const char*
+ GetImapServerKey(); // return the user name from the incoming server;
+
+ // state set by the imap parser...
+ void NotifyMessageFlags(imapMessageFlagsType flags,
+ const nsACString& keywords, nsMsgKey key,
+ uint64_t highestModSeq);
+ void NotifySearchHit(const char* hitLine);
+
+ // Event handlers for the imap parser.
+ void DiscoverMailboxSpec(nsImapMailboxSpec* adoptedBoxSpec);
+ void AlertUserEventUsingName(const char* aMessageId);
+ void AlertUserEvent(const char* message);
+ void AlertUserEventFromServer(const char* aServerEvent,
+ bool aForIdle = false);
+
+ void ProgressEventFunctionUsingName(const char* aMsgId);
+ void ProgressEventFunctionUsingNameWithString(const char* aMsgName,
+ const char* aExtraInfo);
+ void PercentProgressUpdateEvent(nsACString const& fmtStringName,
+ nsAString const& mailbox,
+ int64_t currentProgress, int64_t maxProgress);
+ void ShowProgress();
+
+ // utility function calls made by the server
+
+ void Copy(const char* messageList, const char* destinationMailbox,
+ bool idsAreUid);
+ void Search(const char* searchCriteria, bool useUID, bool notifyHit = true);
+ // imap commands issued by the parser
+ void Store(const nsCString& aMessageList, const char* aMessageData,
+ bool aIdsAreUid);
+ void ProcessStoreFlags(const nsCString& messageIds, bool idsAreUids,
+ imapMessageFlagsType flags, bool addFlags);
+ void IssueUserDefinedMsgCommand(const char* command, const char* messageList);
+ void FetchMsgAttribute(const nsCString& messageIds,
+ const nsCString& attribute);
+ void Expunge();
+ void UidExpunge(const nsCString& messageSet);
+ void ImapClose(bool shuttingDown = false, bool waitForResponse = true);
+ void Check();
+ void SelectMailbox(const char* mailboxName);
+ // more imap commands
+ void Logout(bool shuttingDown = false, bool waitForResponse = true);
+ void Noop();
+ void XServerInfo();
+ void Netscape();
+ void XMailboxInfo(const char* mailboxName);
+ void XAOL_Option(const char* option);
+ void MailboxData();
+ void GetMyRightsForFolder(const char* mailboxName);
+ void Bodystructure(const nsCString& messageId, bool idIsUid);
+
+ // this function does not ref count!!! be careful!!!
+ nsIImapUrl* GetCurrentUrl() { return m_runningUrl; }
+
+ // acl and namespace stuff
+ // notifies libmsg that we have a new personal/default namespace that we're
+ // using
+ void CommitNamespacesForHostEvent();
+ // notifies libmsg that we have new capability data for the current host
+ void CommitCapability();
+
+ // Adds a set of rights for a given user on a given mailbox on the current
+ // host. if userName is NULL, it means "me," or MYRIGHTS. rights is a single
+ // string of rights, as specified by RFC2086, the IMAP ACL extension.
+ void AddFolderRightsForUser(const char* mailboxName, const char* userName,
+ const char* rights);
+ // Clears all rights for the current folder, for all users.
+ void ClearAllFolderRights();
+ void RefreshFolderACLView(const char* mailboxName,
+ nsImapNamespace* nsForMailbox);
+
+ nsresult SetFolderAdminUrl(const char* mailboxName);
+ void HandleMemoryFailure();
+ void HandleCurrentUrlError();
+
+ // UIDPLUS extension
+ void SetCopyResponseUid(const char* msgIdString);
+
+ // Quota support
+ void UpdateFolderQuotaData(nsImapQuotaAction aAction, nsCString& aQuotaRoot,
+ uint64_t aUsed, uint64_t aMax);
+
+ bool GetPreferPlainText() { return m_preferPlainText; }
+
+ int32_t GetCurFetchSize() { return m_curFetchSize; }
+
+ const nsString& GetEmptyMimePartString() { return m_emptyMimePartString; }
+ void SetCapabilityResponseOccurred();
+
+ // Start event loop. This is called on IMAP thread
+ bool RunImapThreadMainLoop();
+
+ private:
+ virtual ~nsImapProtocol();
+ // the following flag is used to determine when a url is currently being run.
+ // It is cleared when we finish processng a url and it is set whenever we call
+ // Load on a url
+ bool m_urlInProgress;
+
+ /** The nsIImapURL that is currently running. */
+ nsCOMPtr<nsIImapUrl> m_runningUrl;
+ nsCOMPtr<nsIImapUrl> m_runningUrlLatest;
+ /** Current imap action associated with this connection. */
+ nsImapAction m_imapAction;
+
+ nsCString m_hostName;
+ nsCString m_userName;
+ nsCString m_serverKey;
+ char* m_dataOutputBuf;
+ RefPtr<nsMsgLineStreamBuffer> m_inputStreamBuffer;
+ nsCString m_trashFolderPath;
+
+ /** The socket connection to the IMAP server. */
+ nsCOMPtr<nsISocketTransport> m_transport;
+ nsCOMPtr<nsITransportSecurityInfo> m_securityInfo;
+
+ /** Stream to handle data coming in from the IMAP server. */
+ nsCOMPtr<nsIInputStream> m_inputStream;
+
+ nsCOMPtr<nsIAsyncInputStream> m_channelInputStream;
+ nsCOMPtr<nsIAsyncOutputStream> m_channelOutputStream;
+
+ /** The currently running request. */
+ nsCOMPtr<nsIImapMockChannel> m_mockChannel;
+
+ uint32_t m_bytesToChannel;
+ bool m_fetchingWholeMessage;
+ // nsCOMPtr<nsIRequest> mAsyncReadRequest; // we're going to cancel this when
+ // we're done with the conn.
+
+ // ******* Thread support *******
+ PRThread* m_thread;
+ mozilla::ReentrantMonitor
+ m_dataAvailableMonitor; // used to notify the arrival of data from the
+ // server
+ mozilla::ReentrantMonitor
+ m_urlReadyToRunMonitor; // used to notify the arrival of a new url to be
+ // processed
+ mozilla::ReentrantMonitor m_pseudoInterruptMonitor;
+ mozilla::ReentrantMonitor m_dataMemberMonitor;
+ mozilla::ReentrantMonitor m_threadDeathMonitor;
+ mozilla::ReentrantMonitor m_waitForBodyIdsMonitor;
+ mozilla::ReentrantMonitor m_fetchBodyListMonitor;
+ mozilla::ReentrantMonitor m_passwordReadyMonitor;
+ mozilla::Mutex mLock;
+ // If we get an async password prompt, this is where the UI thread
+ // stores the password, before notifying the imap thread of the password
+ // via the m_passwordReadyMonitor.
+ nsString m_password;
+ // Set to the result of nsImapServer::PromptPassword
+ nsresult m_passwordStatus;
+ bool m_passwordObtained;
+
+ bool m_imapThreadIsRunning;
+ void ImapThreadMainLoop(void);
+ nsresult m_connectionStatus;
+ nsCString m_connectionType;
+
+ bool m_nextUrlReadyToRun;
+ bool m_idleResponseReadyToHandle;
+ nsWeakPtr m_server;
+
+ RefPtr<ImapMailFolderSinkProxy> m_imapMailFolderSink;
+ RefPtr<ImapMailFolderSinkProxy> m_imapMailFolderSinkSelected;
+ RefPtr<ImapMessageSinkProxy> m_imapMessageSink;
+ RefPtr<ImapServerSinkProxy> m_imapServerSink;
+ RefPtr<ImapServerSinkProxy> m_imapServerSinkLatest;
+ RefPtr<ImapProtocolSinkProxy> m_imapProtocolSink;
+
+ // helper function to setup imap sink interface proxies
+ nsresult SetupSinkProxy();
+ // End thread support stuff
+ nsresult LoadImapUrlInternal();
+
+ bool GetDeleteIsMoveToTrash();
+ bool GetShowDeletedMessages();
+ nsCString m_currentCommand;
+ nsImapServerResponseParser m_parser;
+ nsImapServerResponseParser& GetServerStateParser() { return m_parser; }
+
+ bool HandleIdleResponses();
+ virtual bool ProcessCurrentURL();
+ void EstablishServerConnection();
+ virtual void ParseIMAPandCheckForNewMail(const char* commandString = nullptr,
+ bool ignoreBadNOResponses = false);
+ // biff
+ void PeriodicBiff();
+ void SendSetBiffIndicatorEvent(nsMsgBiffState newState);
+
+ // folder opening and listing header functions
+ void FolderHeaderDump(uint32_t* msgUids, uint32_t msgCount);
+ void FolderMsgDump(uint32_t* msgUids, uint32_t msgCount,
+ nsIMAPeFetchFields fields);
+ void FolderMsgDumpLoop(uint32_t* msgUids, uint32_t msgCount,
+ nsIMAPeFetchFields fields);
+ void WaitForPotentialListOfBodysToFetch(nsTArray<nsMsgKey>& msgIdList);
+ void HeaderFetchCompleted();
+ void UploadMessageFromFile(nsIFile* file, const char* mailboxName,
+ PRTime date, imapMessageFlagsType flags,
+ nsCString& keywords);
+
+ // mailbox name utilities.
+ void CreateEscapedMailboxName(const char* rawName, nsCString& escapedName);
+ void SetupMessageFlagsString(nsCString& flagString,
+ imapMessageFlagsType flags, uint16_t userFlags);
+
+ // body fetching listing data
+ bool m_fetchBodyListIsNew;
+ nsTArray<nsMsgKey> m_fetchBodyIdList;
+
+ // initialization function given a new url and transport layer
+ nsresult SetupWithUrl(nsIURI* aURL, nsISupports* aConsumer);
+ nsresult SetupWithUrlCallback(nsIProxyInfo* proxyInfo);
+ void ReleaseUrlState(bool rerunningUrl); // release any state that is stored
+ // on a per action basis.
+ /**
+ * Last ditch effort to run the url without using an imap connection.
+ * If it turns out that we don't need to run the url at all (e.g., we're
+ * trying to download a single message for offline use and it has already
+ * been downloaded, this function will send the appropriate notifications.
+ *
+ * @returns true if the url has been run locally, or doesn't need to be run.
+ */
+ bool TryToRunUrlLocally(nsIURI* aURL, nsISupports* aConsumer);
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // Communication methods --> Reading and writing protocol
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ // SendData not only writes the NULL terminated data in dataBuffer to our
+ // output stream but it also informs the consumer that the data has been
+ // written to the stream. aSuppressLogging --> set to true if you wish to
+ // suppress logging for this particular command. this is useful for making
+ // sure we don't log authentication information like the user's password
+ // (which was encoded anyway), but still we shouldn't add that information to
+ // the log.
+ nsresult SendData(const char* dataBuffer,
+ bool aSuppressLogging = false) override;
+
+ // state ported over from 4.5
+ bool m_pseudoInterrupted;
+ bool m_active;
+ bool m_folderNeedsSubscribing;
+ bool m_folderNeedsACLRefreshed;
+
+ bool m_threadShouldDie;
+
+ // use to prevent re-entering TellThreadToDie.
+ bool m_inThreadShouldDie;
+ // If the UI thread has signalled the IMAP thread to die, and the
+ // connection has timed out, this will be set to FALSE.
+ bool m_safeToCloseConnection;
+
+ RefPtr<nsImapFlagAndUidState> m_flagState;
+ nsMsgBiffState m_currentBiffState;
+ // manage the IMAP server command tags
+ // 11 = enough memory for the decimal representation of MAX_UINT + trailing
+ // nul
+ char m_currentServerCommandTag[11];
+ uint32_t m_currentServerCommandTagNumber;
+ void IncrementCommandTagNumber();
+ const char* GetServerCommandTag();
+
+ void StartTLS();
+
+ // login related methods.
+ nsresult GetPassword(nsString& password, bool aNewPasswordRequested);
+ void InitPrefAuthMethods(int32_t authMethodPrefValue,
+ nsIMsgIncomingServer* aServer);
+ nsresult ChooseAuthMethod();
+ void MarkAuthMethodAsFailed(eIMAPCapabilityFlags failedAuthMethod);
+ void ResetAuthMethods();
+
+ // All of these methods actually issue protocol
+ void Capability(); // query host for capabilities.
+ void ID(); // send RFC 2971 app info to server
+ void EnableUTF8Accept();
+ void EnableCondStore();
+ void StartCompressDeflate();
+ nsresult BeginCompressing();
+ void Language(); // set the language on the server if it supports it
+ void Namespace();
+ void InsecureLogin(const char* userName, const nsCString& password);
+ nsresult ClientID();
+ nsresult AuthLogin(const char* userName, const nsString& password,
+ eIMAPCapabilityFlag flag);
+ nsresult SendDataParseIMAPandCheckForNewMail(const char* data,
+ const char* command);
+ void ProcessAuthenticatedStateURL();
+ void ProcessAfterAuthenticated();
+ void ProcessSelectedStateURL();
+ bool TryToLogon();
+
+ // ProcessAuthenticatedStateURL() used to be one giant if statement. I've
+ // broken out a set of actions based on the imap action passed into the url.
+ // The following functions are imap protocol handlers for each action. They
+ // are called by ProcessAuthenticatedStateUrl.
+ void OnLSubFolders();
+ void OnAppendMsgFromFile();
+
+ char* GetFolderPathString(); // OK to call from UI thread
+
+ char* OnCreateServerSourceFolderPathString();
+ char* OnCreateServerDestinationFolderPathString();
+ nsresult CreateServerSourceFolderPathString(char** result);
+ void OnCreateFolder(const char* aSourceMailbox);
+ void OnEnsureExistsFolder(const char* aSourceMailbox);
+ void OnSubscribe(const char* aSourceMailbox);
+ void OnUnsubscribe(const char* aSourceMailbox);
+ void RefreshACLForFolderIfNecessary(const char* mailboxName);
+ void RefreshACLForFolder(const char* aSourceMailbox);
+ void GetACLForFolder(const char* aMailboxName);
+ void OnRefreshAllACLs();
+ void OnListFolder(const char* aSourceMailbox, bool aBool);
+ void OnStatusForFolder(const char* sourceMailbox);
+ void OnDeleteFolder(const char* aSourceMailbox);
+ void OnRenameFolder(const char* aSourceMailbox);
+ void OnMoveFolderHierarchy(const char* aSourceMailbox);
+ void DeleteFolderAndMsgs(const char* aSourceMailbox);
+ void RemoveMsgsAndExpunge();
+ void FindMailboxesIfNecessary();
+ void CreateMailbox(const char* mailboxName);
+ void DeleteMailbox(const char* mailboxName);
+ void RenameMailbox(const char* existingName, const char* newName);
+ void RemoveHierarchyDelimiter(nsCString& mailboxName);
+ bool CreateMailboxRespectingSubscriptions(const char* mailboxName);
+ bool DeleteMailboxRespectingSubscriptions(const char* mailboxName);
+ bool RenameMailboxRespectingSubscriptions(const char* existingName,
+ const char* newName,
+ bool reallyRename);
+ // notify the fe that a folder was deleted
+ void FolderDeleted(const char* mailboxName);
+ // notify the fe that a folder creation failed
+ void FolderNotCreated(const char* mailboxName);
+ // notify the fe that a folder was deleted
+ void FolderRenamed(const char* oldName, const char* newName);
+
+ bool FolderIsSelected(const char* mailboxName);
+
+ bool MailboxIsNoSelectMailbox(const char* mailboxName);
+ bool FolderNeedsACLInitialized(const char* folderName);
+ void DiscoverMailboxList();
+ void DiscoverAllAndSubscribedBoxes();
+ void MailboxDiscoveryFinished();
+ void NthLevelChildList(const char* onlineMailboxPrefix, int32_t depth);
+ // LIST SUBSCRIBED command (from RFC 5258) crashes some servers. so we need to
+ // identify those servers
+ bool GetListSubscribedIsBrokenOnServer();
+ void Lsub(const char* mailboxPattern, bool addDirectoryIfNecessary);
+ void List(const char* mailboxPattern, bool addDirectoryIfNecessary,
+ bool useXLIST = false);
+ void Subscribe(const char* mailboxName);
+ void Unsubscribe(const char* mailboxName);
+ void Idle();
+ void EndIdle(bool waitForResponse = true);
+ // Some imap servers include the mailboxName following the dir-separator in
+ // the list of subfolders of the mailboxName. In fact, they are the same. So
+ // we should decide if we should delete such subfolder and provide feedback if
+ // the delete operation succeed.
+ bool DeleteSubFolders(const char* aMailboxName, bool& aDeleteSelf);
+ bool RenameHierarchyByHand(const char* oldParentMailboxName,
+ const char* newParentMailboxName);
+ bool RetryUrl();
+
+ nsresult GlobalInitialization(nsIPrefBranch* aPrefBranch);
+ nsresult Configure(int32_t TooFastTime, int32_t IdealTime,
+ int32_t ChunkAddSize, int32_t ChunkSize,
+ int32_t ChunkThreshold, bool FetchByChunks);
+ nsresult GetMsgWindow(nsIMsgWindow** aMsgWindow);
+ // End Process AuthenticatedState Url helper methods
+
+ virtual char const* GetType() override { return "imap"; }
+
+ // Quota support
+ void GetQuotaDataIfSupported(const char* aBoxName);
+
+ // CondStore support - true if server supports it, and the user hasn't
+ // disabled it.
+ bool UseCondStore();
+ // false if pref "mail.server.serverxxx.use_condstore" is false;
+ bool m_useCondStore;
+ // COMPRESS=DEFLATE support - true if server supports it, and the user hasn't
+ // disabled it.
+ bool UseCompressDeflate();
+ // false if pref "mail.server.serverxxx.use_compress_deflate" is false;
+ bool m_useCompressDeflate;
+ // these come from the nsIDBFolderInfo in the msgDatabase and
+ // are initialized in nsImapProtocol::SetupWithUrl.
+ uint64_t mFolderLastModSeq;
+ int32_t mFolderTotalMsgCount;
+ uint32_t mFolderHighestUID;
+ bool m_allowUTF8Accept;
+
+ bool m_isGmailServer;
+ nsTArray<nsCString> mCustomDBHeaders;
+ nsTArray<nsCString> mCustomHeaders;
+ bool m_trackingTime;
+ PRTime m_startTime;
+ PRTime m_endTime;
+ PRTime m_lastActiveTime;
+ int32_t m_tooFastTime;
+ int32_t m_idealTime;
+ int32_t m_chunkAddSize;
+ int32_t m_chunkStartSize;
+ bool m_fetchByChunks;
+ bool m_sendID;
+ int32_t m_curFetchSize;
+ bool m_ignoreExpunges;
+ eIMAPCapabilityFlags m_prefAuthMethods; // set of capability flags (in
+ // nsImapCore.h) for auth methods
+ eIMAPCapabilityFlags m_failedAuthMethods; // ditto
+ eIMAPCapabilityFlag m_currentAuthMethod; // exactly one capability flag, or 0
+ int32_t m_socketType;
+ int32_t m_chunkSize;
+ int32_t m_chunkThreshold;
+ RefPtr<nsMsgImapLineDownloadCache> m_downloadLineCache;
+ RefPtr<nsMsgImapHdrXferInfo> m_hdrDownloadCache;
+ nsCOMPtr<nsIImapHeaderInfo> m_curHdrInfo;
+ // mapping between mailboxes and the corresponding folder flags
+ nsTHashMap<nsCStringHashKey, int32_t> m_standardListMailboxes;
+ // mapping between special xlist mailboxes and the corresponding folder flags
+ nsTHashMap<nsCStringHashKey, int32_t> m_specialXListMailboxes;
+
+ nsCOMPtr<nsIImapHostSessionList> m_hostSessionList;
+
+ bool m_fromHeaderSeen;
+
+ nsString mAcceptLanguages;
+
+ nsCString m_clientId;
+
+ // progress stuff
+ void SetProgressString(uint32_t aStringIndex);
+
+ nsCString m_progressStringName;
+ uint32_t m_stringIndex;
+ int32_t m_progressCurrentNumber[IMAP_NUMBER_OF_PROGRESS_STRINGS];
+ int32_t m_progressExpectedNumber;
+ nsCString m_lastProgressStringName;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+
+ bool m_notifySearchHit;
+ bool m_needNoop;
+ bool m_idle;
+ bool m_useIdle;
+ int32_t m_noopCount;
+ bool m_autoSubscribe, m_autoUnsubscribe, m_autoSubscribeOnOpen;
+ bool m_closeNeededBeforeSelect;
+ bool m_retryUrlOnError;
+ bool m_preferPlainText;
+ bool m_forceSelect;
+
+ int32_t m_uidValidity; // stored uid validity for the selected folder.
+
+ enum EMailboxHierarchyNameState {
+ kNoOperationInProgress,
+ // kDiscoverBaseFolderInProgress, - Unused. Keeping for historical reasons.
+ kDiscoverTrashFolderInProgress,
+ kDeleteSubFoldersInProgress,
+ kListingForInfoOnly,
+ kListingForInfoAndDiscovery,
+ kDiscoveringNamespacesOnly,
+ kXListing,
+ kListingForFolderFlags,
+ kListingForCreate
+ };
+ EMailboxHierarchyNameState m_hierarchyNameState;
+ EMailboxDiscoverStatus m_discoveryStatus;
+ nsTArray<nsIMAPMailboxInfo*> m_listedMailboxList;
+ nsTArray<nsCString>* m_deletableChildren;
+ uint32_t m_flagChangeCount;
+ PRTime m_lastCheckTime;
+
+ bool CheckNeeded();
+
+ nsString m_emptyMimePartString;
+
+ RefPtr<mozilla::mailnews::OAuth2ThreadHelper> mOAuth2Support;
+ bool m_capabilityResponseOccurred;
+
+ nsresult IsTransportAlive(bool* alive);
+ nsresult TransportStartTLS();
+ void GetTransportSecurityInfo(nsITransportSecurityInfo** aSecurityInfo);
+};
+
+// This small class is a "mock" channel because it is a mockery of the imap
+// channel's implementation... it's a light weight channel that we can return to
+// necko when they ask for a channel on a url before we actually have an imap
+// protocol instance around which can run the url. Please see my comments in
+// nsIImapMockChannel.idl for more details..
+//
+// Threading concern: This class lives entirely in the UI thread.
+
+class nsICacheEntry;
+
+class nsImapMockChannel : public nsIImapMockChannel,
+ public nsICacheEntryOpenCallback,
+ public nsITransportEventSink,
+ public nsSupportsWeakReference,
+ public nsMailChannel,
+ public nsHashPropertyBag {
+ public:
+ friend class nsImapProtocol;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIIMAPMOCKCHANNEL
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIREQUEST
+ NS_DECL_NSICACHEENTRYOPENCALLBACK
+ NS_DECL_NSITRANSPORTEVENTSINK
+
+ nsImapMockChannel();
+ static nsresult Create(const nsIID& iid, void** result);
+ nsresult RunOnStopRequestFailure();
+
+ protected:
+ virtual ~nsImapMockChannel();
+ nsCOMPtr<nsIURI> m_url;
+
+ nsCOMPtr<nsIURI> m_originalUrl;
+ nsCOMPtr<nsILoadGroup> m_loadGroup;
+ nsCOMPtr<nsILoadInfo> m_loadInfo;
+ nsCOMPtr<nsIStreamListener> m_channelListener;
+ nsresult m_cancelStatus;
+ nsLoadFlags mLoadFlags;
+ nsCOMPtr<nsIProgressEventSink> mProgressEventSink;
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsISupports> mOwner;
+ nsCOMPtr<nsITransportSecurityInfo> mSecurityInfo;
+ nsCString mContentType;
+ nsCString mCharset;
+ nsWeakPtr mProtocol;
+
+ bool mChannelClosed;
+ bool mReadingFromCache;
+ int64_t mContentLength;
+ bool mWritingToCache;
+
+ mozilla::Monitor mSuspendedMonitor;
+ bool mSuspended;
+
+ nsresult ResumeAndNotifyOne();
+
+ // cache related helper methods
+ nsresult OpenCacheEntry(); // makes a request to the cache service for a
+ // cache entry for a url
+ bool ReadFromLocalCache(); // attempts to read the url out of our local
+ // (offline) cache....
+ nsresult ReadFromCache2(nsICacheEntry* entry); // pipes message from cache2
+ // entry to channel listener
+ nsresult NotifyStartEndReadFromCache(bool start);
+
+ // we end up daisy chaining multiple nsIStreamListeners into the load process.
+ nsresult SetupPartExtractorListener(nsIImapUrl* aUrl,
+ nsIStreamListener* aConsumer);
+
+ uint32_t mContentDisposition;
+};
+
+#endif // nsImapProtocol_h___
diff --git a/comm/mailnews/imap/src/nsImapSearchResults.cpp b/comm/mailnews/imap/src/nsImapSearchResults.cpp
new file mode 100644
index 0000000000..0932126a46
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapSearchResults.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+
+#include "nsImapSearchResults.h"
+#include "prmem.h"
+#include "nsCRT.h"
+
+nsImapSearchResultSequence::nsImapSearchResultSequence() {}
+
+nsImapSearchResultSequence*
+nsImapSearchResultSequence::CreateSearchResultSequence() {
+ return new nsImapSearchResultSequence;
+}
+
+void nsImapSearchResultSequence::Clear(void) {
+ int32_t i = Length();
+ while (0 <= --i) {
+ char* string = ElementAt(i);
+ PR_Free(string);
+ }
+ nsTArray<char*>::Clear();
+}
+
+nsImapSearchResultSequence::~nsImapSearchResultSequence() { Clear(); }
+
+void nsImapSearchResultSequence::ResetSequence() { Clear(); }
+
+void nsImapSearchResultSequence::AddSearchResultLine(const char* searchLine) {
+ // The first add becomes node 2. Fix this.
+ char* copiedSequence = PL_strdup(searchLine + 9); // 9 == "* SEARCH "
+
+ if (copiedSequence) // if we can't allocate this then the search won't hit
+ AppendElement(copiedSequence);
+}
+
+nsImapSearchResultIterator::nsImapSearchResultIterator(
+ nsImapSearchResultSequence& sequence)
+ : fSequence(sequence) {
+ ResetIterator();
+}
+
+nsImapSearchResultIterator::~nsImapSearchResultIterator() {}
+
+void nsImapSearchResultIterator::ResetIterator() {
+ fSequenceIndex = 0;
+ fCurrentLine = (char*)fSequence.SafeElementAt(fSequenceIndex);
+ fPositionInCurrentLine = fCurrentLine;
+}
+
+int32_t nsImapSearchResultIterator::GetNextMessageNumber() {
+ int32_t returnValue = 0;
+ if (fPositionInCurrentLine) {
+ returnValue = atoi(fPositionInCurrentLine);
+
+ // eat the current number
+ while (isdigit(*++fPositionInCurrentLine))
+ ;
+
+ if (*fPositionInCurrentLine == 0xD) // found CR, no more digits on line
+ {
+ fCurrentLine = (char*)fSequence.SafeElementAt(++fSequenceIndex);
+ fPositionInCurrentLine = fCurrentLine;
+ } else // eat the space
+ fPositionInCurrentLine++;
+ }
+
+ return returnValue;
+}
diff --git a/comm/mailnews/imap/src/nsImapSearchResults.h b/comm/mailnews/imap/src/nsImapSearchResults.h
new file mode 100644
index 0000000000..4eca197e29
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapSearchResults.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImapSearchResults_h___
+#define nsImapSearchResults_h___
+
+#include "nsTArray.h"
+
+class nsImapSearchResultSequence : public nsTArray<char*> {
+ public:
+ virtual ~nsImapSearchResultSequence();
+ static nsImapSearchResultSequence* CreateSearchResultSequence();
+
+ virtual void AddSearchResultLine(const char* searchLine);
+ virtual void ResetSequence();
+ void Clear();
+
+ friend class nsImapSearchResultIterator;
+
+ private:
+ nsImapSearchResultSequence();
+};
+
+class nsImapSearchResultIterator {
+ public:
+ explicit nsImapSearchResultIterator(nsImapSearchResultSequence& sequence);
+ virtual ~nsImapSearchResultIterator();
+
+ void ResetIterator();
+ int32_t GetNextMessageNumber(); // returns 0 at end of list
+ private:
+ nsImapSearchResultSequence& fSequence;
+ int32_t fSequenceIndex;
+ char* fCurrentLine;
+ char* fPositionInCurrentLine;
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapServerResponseParser.cpp b/comm/mailnews/imap/src/nsImapServerResponseParser.cpp
new file mode 100644
index 0000000000..a52279b6c9
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapServerResponseParser.cpp
@@ -0,0 +1,2640 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMimeTypes.h"
+#include "nsImapCore.h"
+#include "nsImapProtocol.h"
+#include "nsImapServerResponseParser.h"
+#include "nsIImapFlagAndUidState.h"
+#include "nsImapNamespace.h"
+#include "nsImapUtils.h"
+#include "nsCRT.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Logging.h"
+
+////////////////// nsImapServerResponseParser /////////////////////////
+
+extern mozilla::LazyLogModule IMAP; // defined in nsImapProtocol.cpp
+
+nsImapServerResponseParser::nsImapServerResponseParser(
+ nsImapProtocol& imapProtocolConnection)
+ : nsImapGenericParser(),
+ fReportingErrors(true),
+ fCurrentFolderReadOnly(false),
+ fCurrentLineContainedFlagInfo(false),
+ fServerIsNetscape3xServer(false),
+ fSeqNumOfFirstUnseenMsg(0),
+ fNumberOfExistingMessages(0),
+ fNumberOfRecentMessages(0),
+ fSizeOfMostRecentMessage(0),
+ fTotalDownloadSize(0),
+ fCurrentCommandTag(nullptr),
+ fSelectedMailboxName(nullptr),
+ fIMAPstate(kNonAuthenticated),
+ fLastChunk(false),
+ fNextChunkStartsWithNewline(false),
+ fServerConnection(imapProtocolConnection),
+ fHostSessionList(nullptr) {
+ fSearchResults = nsImapSearchResultSequence::CreateSearchResultSequence();
+ fFolderAdminUrl = nullptr;
+ fNetscapeServerVersionString = nullptr;
+ fXSenderInfo = nullptr;
+ fSupportsUserDefinedFlags = 0;
+ fSettablePermanentFlags = 0;
+ fCapabilityFlag = kCapabilityUndefined;
+ fLastAlert = nullptr;
+ fDownloadingHeaders = false;
+ fGotPermanentFlags = false;
+ fFolderUIDValidity = 0;
+ fHighestModSeq = 0;
+ fAuthChallenge = nullptr;
+ fStatusUnseenMessages = 0;
+ fStatusRecentMessages = 0;
+ fStatusNextUID = nsMsgKey_None;
+ fNextUID = nsMsgKey_None;
+ fStatusExistingMessages = 0;
+ fReceivedHeaderOrSizeForUID = nsMsgKey_None;
+ fUtf8AcceptEnabled = false;
+ fStdJunkNotJunkUseOk = false;
+}
+
+nsImapServerResponseParser::~nsImapServerResponseParser() {
+ PR_Free(fCurrentCommandTag);
+ delete fSearchResults;
+ PR_Free(fFolderAdminUrl);
+ PR_Free(fNetscapeServerVersionString);
+ PR_Free(fXSenderInfo);
+ PR_Free(fLastAlert);
+ PR_Free(fSelectedMailboxName);
+ PR_Free(fAuthChallenge);
+}
+
+bool nsImapServerResponseParser::LastCommandSuccessful() {
+ return (!CommandFailed() && !fServerConnection.DeathSignalReceived() &&
+ nsImapGenericParser::LastCommandSuccessful());
+}
+
+// returns true if things look ok to continue
+bool nsImapServerResponseParser::GetNextLineForParser(char** nextLine) {
+ bool rv = true;
+ *nextLine = fServerConnection.CreateNewLineFromSocket();
+ if (fServerConnection.DeathSignalReceived() ||
+ NS_FAILED(fServerConnection.GetConnectionStatus()))
+ rv = false;
+ // we'd really like to try to silently reconnect, but we shouldn't put this
+ // message up just in the interrupt case
+ if (NS_FAILED(fServerConnection.GetConnectionStatus()) &&
+ !fServerConnection.DeathSignalReceived())
+ fServerConnection.AlertUserEventUsingName("imapServerDisconnected");
+ return rv;
+}
+
+bool nsImapServerResponseParser::CommandFailed() {
+ return fCurrentCommandFailed;
+}
+
+void nsImapServerResponseParser::SetCommandFailed(bool failed) {
+ fCurrentCommandFailed = failed;
+}
+
+bool nsImapServerResponseParser::UntaggedResponse() {
+ return fUntaggedResponse;
+}
+
+void nsImapServerResponseParser::SetFlagState(nsIImapFlagAndUidState* state) {
+ fFlagState = state;
+}
+
+uint32_t nsImapServerResponseParser::SizeOfMostRecentMessage() {
+ return fSizeOfMostRecentMessage;
+}
+
+// Call this when adding a pipelined command to the session
+void nsImapServerResponseParser::IncrementNumberOfTaggedResponsesExpected(
+ const char* newExpectedTag) {
+ fNumberOfTaggedResponsesExpected++;
+ PR_Free(fCurrentCommandTag);
+ fCurrentCommandTag = PL_strdup(newExpectedTag);
+ if (!fCurrentCommandTag) HandleMemoryFailure();
+}
+
+void nsImapServerResponseParser::InitializeState() {
+ fCurrentCommandFailed = false;
+ fNumberOfRecentMessages = 0;
+ fReceivedHeaderOrSizeForUID = nsMsgKey_None;
+ fUntaggedResponse = false;
+}
+
+// RFC3501: response = *(continue-req / response-data) response-done
+// response-data = "*" SP (resp-cond-state / resp-cond-bye /
+// mailbox-data / message-data / capability-data) CRLF
+// continue-req = "+" SP (resp-text / base64) CRLF
+void nsImapServerResponseParser::ParseIMAPServerResponse(
+ const char* aCurrentCommand, bool aIgnoreBadAndNOResponses,
+ char* aGreetingWithCapability) {
+ NS_ASSERTION(aCurrentCommand && *aCurrentCommand != '\r' &&
+ *aCurrentCommand != '\n' && *aCurrentCommand != ' ',
+ "Invalid command string");
+ bool sendingIdleDone = !strcmp(aCurrentCommand, "DONE" CRLF);
+ if (sendingIdleDone) fWaitingForMoreClientInput = false;
+
+ // Reinitialize the parser
+ SetConnected(true);
+ SetSyntaxError(false);
+
+ // Reinitialize our state
+ InitializeState();
+
+ // the default is to not pipeline
+ fNumberOfTaggedResponsesExpected = 1;
+ int numberOfTaggedResponsesReceived = 0;
+
+ nsCString copyCurrentCommand(aCurrentCommand);
+ if (!fServerConnection.DeathSignalReceived()) {
+ char* placeInTokenString = nullptr;
+ char* tagToken = nullptr;
+ const char* commandToken = nullptr;
+ bool inIdle = false;
+ if (!sendingIdleDone) {
+ placeInTokenString = copyCurrentCommand.BeginWriting();
+ tagToken = NS_strtok(WHITESPACE, &placeInTokenString);
+ commandToken = NS_strtok(WHITESPACE, &placeInTokenString);
+ } else
+ commandToken = "DONE";
+ if (tagToken) {
+ PR_Free(fCurrentCommandTag);
+ fCurrentCommandTag = PL_strdup(tagToken);
+ if (!fCurrentCommandTag) HandleMemoryFailure();
+ inIdle = commandToken && !strcmp(commandToken, "IDLE");
+ }
+
+ if (commandToken && ContinueParse())
+ PreProcessCommandToken(commandToken, aCurrentCommand);
+
+ if (ContinueParse()) {
+ ResetLexAnalyzer();
+
+ if (aGreetingWithCapability) {
+ PR_FREEIF(fCurrentLine);
+ fCurrentLine = aGreetingWithCapability;
+ }
+
+ // When inIdle, only one pass through "do" and "while" below occurs.
+ do {
+ AdvanceToNextToken();
+
+ // untagged responses [RFC3501, Sec. 2.2.2]
+ while (ContinueParse() && fNextToken && *fNextToken == '*') {
+ response_data();
+ if (ContinueParse()) {
+ if (!fAtEndOfLine)
+ SetSyntaxError(true);
+ else if (!inIdle && !fCurrentCommandFailed &&
+ !aGreetingWithCapability)
+ AdvanceToNextToken();
+ }
+ // For checking expected response to IDLE command below and
+ // for checking if an untagged response occurred by the caller.
+ fUntaggedResponse = true;
+ }
+
+ // command continuation request [RFC3501, Sec. 7.5]
+ if (ContinueParse() && fNextToken &&
+ *fNextToken == '+') // never pipeline APPEND or AUTHENTICATE
+ {
+ NS_ASSERTION(
+ (fNumberOfTaggedResponsesExpected -
+ numberOfTaggedResponsesReceived) == 1,
+ " didn't get the number of tagged responses we expected");
+ numberOfTaggedResponsesReceived = fNumberOfTaggedResponsesExpected;
+ if (commandToken && !PL_strcasecmp(commandToken, "authenticate") &&
+ placeInTokenString &&
+ (!PL_strncasecmp(placeInTokenString, "CRAM-MD5",
+ strlen("CRAM-MD5")) ||
+ !PL_strncasecmp(placeInTokenString, "NTLM", strlen("NTLM")) ||
+ !PL_strncasecmp(placeInTokenString, "GSSAPI",
+ strlen("GSSAPI")) ||
+ !PL_strncasecmp(placeInTokenString, "MSN", strlen("MSN")))) {
+ // we need to store the challenge from the server if we are using
+ // CRAM-MD5 or NTLM.
+ authChallengeResponse_data();
+ }
+ } else
+ numberOfTaggedResponsesReceived++;
+
+ if (numberOfTaggedResponsesReceived < fNumberOfTaggedResponsesExpected)
+ response_tagged();
+
+ } while (
+ ContinueParse() && !inIdle &&
+ (numberOfTaggedResponsesReceived < fNumberOfTaggedResponsesExpected));
+
+ // check and see if the server is waiting for more input
+ // it's possible that we ate this + while parsing certain responses (like
+ // cram data), in these cases, the parsing routine for that specific
+ // command will manually set fWaitingForMoreClientInput so we don't lose
+ // that information....
+ if ((fNextToken && *fNextToken == '+') || inIdle) {
+ if (inIdle &&
+ !((fNextToken && *fNextToken == '+') || fUntaggedResponse)) {
+ // IDLE "response" + will not be "eaten" as described above since it
+ // is not an authentication response. So if IDLE response does not
+ // begin with '+' (continuation) or '*' (untagged and probably useful
+ // response) then something is wrong and it is probably a tagged
+ // NO or BAD due to transient error or bad configuration of the
+ // server.
+ if (!PL_strcmp(fCurrentCommandTag, fNextToken)) {
+ response_tagged();
+ } else {
+ // Expected tag doesn't match the received tag. Not good, start
+ // over.
+ response_fatal();
+ }
+ // Show an alert notification containing the server response to bad
+ // IDLE.
+ fServerConnection.AlertUserEventFromServer(fCurrentLine, true);
+ } else {
+ fWaitingForMoreClientInput = true;
+ }
+ }
+ // if we aren't still waiting for more input....
+ else if (!fWaitingForMoreClientInput && !aGreetingWithCapability) {
+ if (ContinueParse()) response_done();
+
+ if (ContinueParse() && !CommandFailed()) {
+ // a successful command may change the eIMAPstate
+ ProcessOkCommand(commandToken);
+ } else if (CommandFailed()) {
+ // a failed command may change the eIMAPstate
+ ProcessBadCommand(commandToken);
+ if (fReportingErrors && !aIgnoreBadAndNOResponses)
+ fServerConnection.AlertUserEventFromServer(fCurrentLine, false);
+ }
+ }
+ }
+ } else
+ SetConnected(false);
+}
+
+void nsImapServerResponseParser::HandleMemoryFailure() {
+ fServerConnection.AlertUserEventUsingName("imapOutOfMemory");
+ nsImapGenericParser::HandleMemoryFailure();
+}
+
+// SEARCH is the only command that requires pre-processing for now.
+// others will be added here.
+void nsImapServerResponseParser::PreProcessCommandToken(
+ const char* commandToken, const char* currentCommand) {
+ fCurrentCommandIsSingleMessageFetch = false;
+ fWaitingForMoreClientInput = false;
+
+ if (!PL_strcasecmp(commandToken, "SEARCH"))
+ fSearchResults->ResetSequence();
+ else if (!PL_strcasecmp(commandToken, "SELECT") && currentCommand) {
+ // the mailbox name must be quoted, so strip the quotes
+ const char* openQuote = PL_strchr(currentCommand, '"');
+ NS_ASSERTION(openQuote, "expected open quote in imap server response");
+ if (!openQuote) { // ill formed select command
+ openQuote = PL_strchr(currentCommand, ' ');
+ }
+ PR_Free(fSelectedMailboxName);
+ fSelectedMailboxName = PL_strdup(openQuote + 1);
+ if (fSelectedMailboxName) {
+ // strip the escape chars and the ending quote
+ char* currentChar = fSelectedMailboxName;
+ while (*currentChar) {
+ if (*currentChar == '\\') {
+ PL_strcpy(currentChar, currentChar + 1);
+ currentChar++; // skip what we are escaping
+ } else if (*currentChar == '\"')
+ *currentChar = 0; // end quote
+ else
+ currentChar++;
+ }
+ } else
+ HandleMemoryFailure();
+
+ // we don't want bogus info for this new box
+ // delete fFlagState; // not our object
+ // fFlagState = nullptr;
+ } else if (!PL_strcasecmp(commandToken, "CLOSE")) {
+ return; // just for debugging
+ // we don't want bogus info outside the selected state
+ // delete fFlagState; // not our object
+ // fFlagState = nullptr;
+ } else if (!PL_strcasecmp(commandToken, "UID")) {
+ nsCString copyCurrentCommand(currentCommand);
+ if (!fServerConnection.DeathSignalReceived()) {
+ char* placeInTokenString = copyCurrentCommand.BeginWriting();
+ (void)NS_strtok(WHITESPACE, &placeInTokenString); // skip tag token
+ (void)NS_strtok(WHITESPACE, &placeInTokenString); // skip uid token
+ char* fetchToken = NS_strtok(WHITESPACE, &placeInTokenString);
+ if (!PL_strcasecmp(fetchToken, "FETCH")) {
+ char* uidStringToken = NS_strtok(WHITESPACE, &placeInTokenString);
+ // , and : are uid delimiters
+ if (!PL_strchr(uidStringToken, ',') && !PL_strchr(uidStringToken, ':'))
+ fCurrentCommandIsSingleMessageFetch = true;
+ }
+ }
+ }
+}
+
+const char* nsImapServerResponseParser::GetSelectedMailboxName() {
+ return fSelectedMailboxName;
+}
+
+nsImapSearchResultIterator*
+nsImapServerResponseParser::CreateSearchResultIterator() {
+ return new nsImapSearchResultIterator(*fSearchResults);
+}
+
+nsImapServerResponseParser::eIMAPstate
+nsImapServerResponseParser::GetIMAPstate() {
+ return fIMAPstate;
+}
+
+void nsImapServerResponseParser::PreauthSetAuthenticatedState() {
+ fIMAPstate = kAuthenticated;
+}
+
+void nsImapServerResponseParser::ProcessOkCommand(const char* commandToken) {
+ if (!PL_strcasecmp(commandToken, "LOGIN") ||
+ !PL_strcasecmp(commandToken, "AUTHENTICATE"))
+ fIMAPstate = kAuthenticated;
+ else if (!PL_strcasecmp(commandToken, "LOGOUT"))
+ fIMAPstate = kNonAuthenticated;
+ else if (!PL_strcasecmp(commandToken, "SELECT") ||
+ !PL_strcasecmp(commandToken, "EXAMINE"))
+ fIMAPstate = kFolderSelected;
+ else if (!PL_strcasecmp(commandToken, "CLOSE")) {
+ fIMAPstate = kAuthenticated;
+ // we no longer have a selected mailbox.
+ PR_FREEIF(fSelectedMailboxName);
+ } else if ((!PL_strcasecmp(commandToken, "LIST")) ||
+ (!PL_strcasecmp(commandToken, "LSUB")) ||
+ (!PL_strcasecmp(commandToken, "XLIST"))) {
+ // fServerConnection.MailboxDiscoveryFinished();
+ // This used to be reporting that we were finished
+ // discovering folders for each time we issued a
+ // LIST or LSUB. So if we explicitly listed the
+ // INBOX, or Trash, or namespaces, we would get multiple
+ // "done" states, even though we hadn't finished.
+ // Move this to be called from the connection object
+ // itself.
+ } else if (!PL_strcasecmp(commandToken, "FETCH")) {
+ if (!fZeroLengthMessageUidString.IsEmpty()) {
+ // "Deleting zero length message");
+ fServerConnection.Store(fZeroLengthMessageUidString, "+Flags (\\Deleted)",
+ true);
+ if (LastCommandSuccessful()) fServerConnection.Expunge();
+
+ fZeroLengthMessageUidString.Truncate();
+ }
+ } else if (!PL_strcasecmp(commandToken, "GETQUOTAROOT")) {
+ if (LastCommandSuccessful()) {
+ nsCString str;
+ fServerConnection.UpdateFolderQuotaData(kValidateQuota, str, 0, 0);
+ }
+ }
+}
+
+void nsImapServerResponseParser::ProcessBadCommand(const char* commandToken) {
+ if (!PL_strcasecmp(commandToken, "LOGIN") ||
+ !PL_strcasecmp(commandToken, "AUTHENTICATE"))
+ fIMAPstate = kNonAuthenticated;
+ else if (!PL_strcasecmp(commandToken, "LOGOUT"))
+ fIMAPstate = kNonAuthenticated; // ??
+ else if (!PL_strcasecmp(commandToken, "SELECT") ||
+ !PL_strcasecmp(commandToken, "EXAMINE"))
+ fIMAPstate = kAuthenticated; // nothing selected
+ else if (!PL_strcasecmp(commandToken, "CLOSE"))
+ fIMAPstate = kAuthenticated; // nothing selected
+}
+
+// RFC3501: response-data = "*" SP (resp-cond-state / resp-cond-bye /
+// mailbox-data / message-data / capability-data) CRLF
+// These are ``untagged'' responses [RFC3501, Sec. 2.2.2]
+/*
+ The RFC1730 grammar spec did not allow one symbol look ahead to determine
+ between mailbox_data / message_data so I combined the numeric possibilities
+ of mailbox_data and all of message_data into numeric_mailbox_data.
+
+ It is assumed that the initial "*" is already consumed before calling this
+ method. The production implemented here is
+ response_data ::= (resp_cond_state / resp_cond_bye /
+ mailbox_data / numeric_mailbox_data /
+ capability_data)
+ CRLF
+*/
+void nsImapServerResponseParser::response_data() {
+ AdvanceToNextToken();
+
+ if (ContinueParse()) {
+ // Instead of comparing lots of strings and make function calls, try to
+ // pre-flight the possibilities based on the first letter of the token.
+ switch (NS_ToUpper(fNextToken[0])) {
+ case 'O': // OK
+ if (NS_ToUpper(fNextToken[1]) == 'K')
+ resp_cond_state(false);
+ else
+ SetSyntaxError(true);
+ break;
+ case 'N': // NO
+ if (NS_ToUpper(fNextToken[1]) == 'O')
+ resp_cond_state(false);
+ else if (!PL_strcasecmp(fNextToken, "NAMESPACE"))
+ namespace_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'B': // BAD
+ if (!PL_strcasecmp(fNextToken, "BAD"))
+ resp_cond_state(false);
+ else if (!PL_strcasecmp(fNextToken, "BYE"))
+ resp_cond_bye();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'F':
+ if (!PL_strcasecmp(fNextToken, "FLAGS"))
+ mailbox_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'P':
+ if (PL_strcasecmp(fNextToken, "PERMANENTFLAGS"))
+ mailbox_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'L':
+ if (!PL_strcasecmp(fNextToken, "LIST") ||
+ !PL_strcasecmp(fNextToken, "LSUB"))
+ mailbox_data();
+ else if (!PL_strcasecmp(fNextToken, "LANGUAGE"))
+ language_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'M':
+ if (!PL_strcasecmp(fNextToken, "MAILBOX"))
+ mailbox_data();
+ else if (!PL_strcasecmp(fNextToken, "MYRIGHTS"))
+ myrights_data(false);
+ else
+ SetSyntaxError(true);
+ break;
+ case 'S':
+ if (!PL_strcasecmp(fNextToken, "SEARCH"))
+ mailbox_data();
+ else if (!PL_strcasecmp(fNextToken, "STATUS")) {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ char* mailboxName = CreateAstring();
+ PL_strfree(mailboxName);
+ }
+ while (ContinueParse() && !fAtEndOfLine) {
+ AdvanceToNextToken();
+ if (!fNextToken) break;
+
+ if (*fNextToken == '(') fNextToken++;
+ if (!PL_strcasecmp(fNextToken, "UIDNEXT")) {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ fStatusNextUID = strtoul(fNextToken, nullptr, 10);
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ if (*(fNextToken + strlen(fNextToken) - 1) == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ }
+ } else if (!PL_strcasecmp(fNextToken, "MESSAGES")) {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ fStatusExistingMessages = strtoul(fNextToken, nullptr, 10);
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ if (*(fNextToken + strlen(fNextToken) - 1) == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ }
+ } else if (!PL_strcasecmp(fNextToken, "UNSEEN")) {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ fStatusUnseenMessages = strtoul(fNextToken, nullptr, 10);
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ if (*(fNextToken + strlen(fNextToken) - 1) == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ }
+ } else if (!PL_strcasecmp(fNextToken, "RECENT")) {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ fStatusRecentMessages = strtoul(fNextToken, nullptr, 10);
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ if (*(fNextToken + strlen(fNextToken) - 1) == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ }
+ } else if (*fNextToken == ')')
+ break;
+ else if (!fAtEndOfLine)
+ SetSyntaxError(true);
+ }
+ } else
+ SetSyntaxError(true);
+ break;
+ case 'C':
+ if (!PL_strcasecmp(fNextToken, "CAPABILITY"))
+ capability_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'V':
+ if (!PL_strcasecmp(fNextToken, "VERSION")) {
+ // figure out the version of the Netscape server here
+ PR_FREEIF(fNetscapeServerVersionString);
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fNetscapeServerVersionString = CreateAstring();
+ AdvanceToNextToken();
+ if (fNetscapeServerVersionString) {
+ fServerIsNetscape3xServer =
+ (*fNetscapeServerVersionString == '3');
+ }
+ }
+ skip_to_CRLF();
+ } else
+ SetSyntaxError(true);
+ break;
+ case 'A':
+ if (!PL_strcasecmp(fNextToken, "ACL")) {
+ acl_data();
+ } else if (!PL_strcasecmp(fNextToken, "ACCOUNT-URL")) {
+ fMailAccountUrl.Truncate();
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fMailAccountUrl.Adopt(CreateAstring());
+ AdvanceToNextToken();
+ }
+ } else
+ SetSyntaxError(true);
+ break;
+ case 'E':
+ if (!PL_strcasecmp(fNextToken, "ENABLED")) enable_data();
+ break;
+ case 'X':
+ if (!PL_strcasecmp(fNextToken, "XSERVERINFO"))
+ xserverinfo_data();
+ else if (!PL_strcasecmp(fNextToken, "XMAILBOXINFO"))
+ xmailboxinfo_data();
+ else if (!PL_strcasecmp(fNextToken, "XAOL-OPTION"))
+ skip_to_CRLF();
+ else if (!PL_strcasecmp(fNextToken, "XLIST"))
+ mailbox_data();
+ else {
+ // check if custom command
+ nsAutoCString customCommand;
+ fServerConnection.GetCurrentUrl()->GetCommand(customCommand);
+ if (customCommand.Equals(fNextToken)) {
+ nsAutoCString customCommandResponse;
+ while (Connected() && !fAtEndOfLine) {
+ AdvanceToNextToken();
+ customCommandResponse.Append(fNextToken);
+ customCommandResponse.Append(' ');
+ }
+ fServerConnection.GetCurrentUrl()->SetCustomCommandResult(
+ customCommandResponse);
+ } else
+ SetSyntaxError(true);
+ }
+ break;
+ case 'Q':
+ if (!PL_strcasecmp(fNextToken, "QUOTAROOT") ||
+ !PL_strcasecmp(fNextToken, "QUOTA"))
+ quota_data();
+ else
+ SetSyntaxError(true);
+ break;
+ case 'I':
+ id_data();
+ break;
+ default:
+ if (IsNumericString(fNextToken))
+ numeric_mailbox_data();
+ else
+ SetSyntaxError(true);
+ break;
+ }
+
+ if (ContinueParse()) PostProcessEndOfLine();
+ }
+}
+
+void nsImapServerResponseParser::PostProcessEndOfLine() {
+ // for now we only have to do one thing here
+ // a fetch response to a 'uid store' command might return the flags
+ // before it returns the uid of the message. So we need both before
+ // we report the new flag info to the front end
+
+ // also check and be sure that there was a UID in the current response
+ if (fCurrentLineContainedFlagInfo && CurrentResponseUID()) {
+ fCurrentLineContainedFlagInfo = false;
+ nsCString customFlags;
+ fFlagState->GetCustomFlags(CurrentResponseUID(),
+ getter_Copies(customFlags));
+ fServerConnection.NotifyMessageFlags(fSavedFlagInfo, customFlags,
+ CurrentResponseUID(), fHighestModSeq);
+ }
+}
+
+/*
+ mailbox_data ::= "FLAGS" SPACE flag_list /
+ "LIST" SPACE mailbox_list /
+ "LSUB" SPACE mailbox_list /
+ "XLIST" SPACE mailbox_list /
+ "MAILBOX" SPACE text /
+ "SEARCH" [SPACE 1#nz_number] /
+ number SPACE "EXISTS" / number SPACE "RECENT"
+
+This production was changed to accommodate predictive parsing
+
+ mailbox_data ::= "FLAGS" SPACE flag_list /
+ "LIST" SPACE mailbox_list /
+ "LSUB" SPACE mailbox_list /
+ "XLIST" SPACE mailbox_list /
+ "MAILBOX" SPACE text /
+ "SEARCH" [SPACE 1#nz_number]
+*/
+void nsImapServerResponseParser::mailbox_data() {
+ if (!PL_strcasecmp(fNextToken, "FLAGS")) {
+ // this handles the case where we got the permanent flags response
+ // before the flags response, in which case, we want to ignore these flags.
+ if (fGotPermanentFlags)
+ skip_to_CRLF();
+ else
+ parse_folder_flags(true);
+ } else if (!PL_strcasecmp(fNextToken, "LIST") ||
+ !PL_strcasecmp(fNextToken, "XLIST")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) mailbox_list(false);
+ } else if (!PL_strcasecmp(fNextToken, "LSUB")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) mailbox_list(true);
+ } else if (!PL_strcasecmp(fNextToken, "MAILBOX"))
+ skip_to_CRLF();
+ else if (!PL_strcasecmp(fNextToken, "SEARCH")) {
+ fSearchResults->AddSearchResultLine(fCurrentLine);
+ fServerConnection.NotifySearchHit(fCurrentLine);
+ skip_to_CRLF();
+ }
+}
+
+/*
+ mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
+ "\Noselect" / "\Unmarked" / flag_extension) ")"
+ SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
+*/
+
+void nsImapServerResponseParser::mailbox_list(bool discoveredFromLsub) {
+ RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec;
+ boxSpec->mFolderSelected = false;
+ boxSpec->mBoxFlags = kNoFlags;
+ boxSpec->mAllocatedPathName.Truncate();
+ boxSpec->mHostName.Truncate();
+ boxSpec->mConnection = &fServerConnection;
+ boxSpec->mFlagState = nullptr;
+ boxSpec->mDiscoveredFromLsub = discoveredFromLsub;
+ boxSpec->mOnlineVerified = true;
+ boxSpec->mBoxFlags &= ~kNameSpace;
+
+ bool endOfFlags = false;
+ fNextToken++; // eat the first "("
+ do {
+ if (!PL_strncasecmp(fNextToken, "\\Marked", 7))
+ boxSpec->mBoxFlags |= kMarked;
+ else if (!PL_strncasecmp(fNextToken, "\\Unmarked", 9))
+ boxSpec->mBoxFlags |= kUnmarked;
+ else if (!PL_strncasecmp(fNextToken, "\\Noinferiors", 12)) {
+ boxSpec->mBoxFlags |= kNoinferiors;
+ // RFC 5258 \Noinferiors implies \HasNoChildren
+ if (fCapabilityFlag & kHasListExtendedCapability)
+ boxSpec->mBoxFlags |= kHasNoChildren;
+ } else if (!PL_strncasecmp(fNextToken, "\\Noselect", 9))
+ boxSpec->mBoxFlags |= kNoselect;
+ else if (!PL_strncasecmp(fNextToken, "\\Drafts", 7))
+ boxSpec->mBoxFlags |= kImapDrafts;
+ else if (!PL_strncasecmp(fNextToken, "\\Trash", 6))
+ boxSpec->mBoxFlags |= kImapXListTrash;
+ else if (!PL_strncasecmp(fNextToken, "\\Sent", 5))
+ boxSpec->mBoxFlags |= kImapSent;
+ else if (!PL_strncasecmp(fNextToken, "\\Spam", 5) ||
+ !PL_strncasecmp(fNextToken, "\\Junk", 5))
+ boxSpec->mBoxFlags |= kImapSpam;
+ else if (!PL_strncasecmp(fNextToken, "\\Archive", 8))
+ boxSpec->mBoxFlags |= kImapArchive;
+ else if (!PL_strncasecmp(fNextToken, "\\All", 4) ||
+ !PL_strncasecmp(fNextToken, "\\AllMail", 8))
+ boxSpec->mBoxFlags |= kImapAllMail;
+ else if (!PL_strncasecmp(fNextToken, "\\Inbox", 6))
+ boxSpec->mBoxFlags |= kImapInbox;
+ else if (!PL_strncasecmp(fNextToken, "\\NonExistent", 11)) {
+ boxSpec->mBoxFlags |= kNonExistent;
+ // RFC 5258 \NonExistent implies \Noselect
+ boxSpec->mBoxFlags |= kNoselect;
+ } else if (!PL_strncasecmp(fNextToken, "\\Subscribed", 10))
+ boxSpec->mBoxFlags |= kSubscribed;
+ else if (!PL_strncasecmp(fNextToken, "\\Remote", 6))
+ boxSpec->mBoxFlags |= kRemote;
+ else if (!PL_strncasecmp(fNextToken, "\\HasChildren", 11))
+ boxSpec->mBoxFlags |= kHasChildren;
+ else if (!PL_strncasecmp(fNextToken, "\\HasNoChildren", 13))
+ boxSpec->mBoxFlags |= kHasNoChildren;
+ // we ignore flag other extensions
+
+ endOfFlags = *(fNextToken + strlen(fNextToken) - 1) == ')';
+ AdvanceToNextToken();
+ } while (!endOfFlags && ContinueParse());
+
+ if (ContinueParse()) {
+ if (*fNextToken == '"') {
+ fNextToken++;
+ if (*fNextToken == '\\') // handle escaped char
+ boxSpec->mHierarchySeparator = *(fNextToken + 1);
+ else
+ boxSpec->mHierarchySeparator = *fNextToken;
+ } else // likely NIL. Discovered late in 4.02 that we do not handle
+ // literals here (e.g. {10} <10 chars>), although this is almost
+ // impossibly unlikely
+ boxSpec->mHierarchySeparator = kOnlineHierarchySeparatorNil;
+ AdvanceToNextToken();
+ if (ContinueParse()) mailbox(boxSpec);
+ }
+}
+
+/* mailbox ::= "INBOX" / astring
+ */
+void nsImapServerResponseParser::mailbox(nsImapMailboxSpec* boxSpec) {
+ char* boxname = nullptr;
+ const char* serverKey = fServerConnection.GetImapServerKey();
+ bool xlistInbox = boxSpec->mBoxFlags & kImapInbox;
+
+ if (!PL_strcasecmp(fNextToken, "INBOX") || xlistInbox) {
+ boxname = PL_strdup("INBOX");
+ if (xlistInbox) PR_Free(CreateAstring());
+ AdvanceToNextToken();
+ } else {
+ boxname = CreateAstring();
+ AdvanceToNextToken();
+ }
+
+ if (boxname && fHostSessionList) {
+ fHostSessionList->SetNamespaceHierarchyDelimiterFromMailboxForHost(
+ serverKey, boxname, boxSpec->mHierarchySeparator);
+
+ nsImapNamespace* ns = nullptr;
+ fHostSessionList->GetNamespaceForMailboxForHost(serverKey, boxname, ns);
+ if (ns) {
+ switch (ns->GetType()) {
+ case kPersonalNamespace:
+ boxSpec->mBoxFlags |= kPersonalMailbox;
+ break;
+ case kPublicNamespace:
+ boxSpec->mBoxFlags |= kPublicMailbox;
+ break;
+ case kOtherUsersNamespace:
+ boxSpec->mBoxFlags |= kOtherUsersMailbox;
+ break;
+ default: // (kUnknownNamespace)
+ break;
+ }
+ boxSpec->mNamespaceForFolder = ns;
+ }
+ }
+
+ if (!boxname) {
+ if (!fServerConnection.DeathSignalReceived()) HandleMemoryFailure();
+ } else if (boxSpec->mConnection && boxSpec->mConnection->GetCurrentUrl()) {
+ boxSpec->mConnection->GetCurrentUrl()->AllocateCanonicalPath(
+ boxname, boxSpec->mHierarchySeparator,
+ getter_Copies(boxSpec->mAllocatedPathName));
+ nsIURI* aURL = nullptr;
+ boxSpec->mConnection->GetCurrentUrl()->QueryInterface(NS_GET_IID(nsIURI),
+ (void**)&aURL);
+ if (aURL) aURL->GetHost(boxSpec->mHostName);
+
+ NS_IF_RELEASE(aURL);
+ // storage for the boxSpec is now owned by server connection
+ fServerConnection.DiscoverMailboxSpec(boxSpec);
+
+ // if this was cancelled by the user,then we sure don't want to
+ // send more mailboxes their way
+ if (NS_FAILED(fServerConnection.GetConnectionStatus())) SetConnected(false);
+ }
+
+ if (boxname) PL_strfree(boxname);
+}
+
+/*
+ message_data ::= nz_number SPACE ("EXPUNGE" /
+ ("FETCH" SPACE msg_fetch) / msg_obsolete)
+
+was changed to
+
+numeric_mailbox_data ::= number SPACE "EXISTS" / number SPACE "RECENT"
+ / nz_number SPACE ("EXPUNGE" /
+ ("FETCH" SPACE msg_fetch) / msg_obsolete)
+
+*/
+void nsImapServerResponseParser::numeric_mailbox_data() {
+ int32_t tokenNumber = atoi(fNextToken);
+ AdvanceToNextToken();
+
+ if (ContinueParse()) {
+ if (!PL_strcasecmp(fNextToken, "FETCH")) {
+ fFetchResponseIndex = tokenNumber;
+ AdvanceToNextToken();
+ if (ContinueParse()) msg_fetch();
+ } else if (!PL_strcasecmp(fNextToken, "EXISTS")) {
+ fNumberOfExistingMessages = tokenNumber;
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "RECENT")) {
+ fNumberOfRecentMessages = tokenNumber;
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "EXPUNGE")) {
+ if (!fServerConnection.GetIgnoreExpunges())
+ fFlagState->ExpungeByIndex((uint32_t)tokenNumber);
+ skip_to_CRLF();
+ } else
+ msg_obsolete();
+ }
+}
+
+/*
+msg_fetch ::= "(" 1#("BODY" SPACE body /
+"BODYSTRUCTURE" SPACE body /
+"BODY[" section "]" SPACE nstring /
+"ENVELOPE" SPACE envelope /
+"FLAGS" SPACE "(" #(flag / "\Recent") ")" /
+"INTERNALDATE" SPACE date_time /
+"MODSEQ" SPACE "(" nz_number ")" /
+"RFC822" [".HEADER" / ".TEXT"] SPACE nstring /
+"RFC822.SIZE" SPACE number /
+"UID" SPACE uniqueid) ")"
+
+*/
+
+void nsImapServerResponseParser::msg_fetch() {
+ bool bNeedEndMessageDownload = false;
+
+ // we have not seen a uid response or flags for this fetch, yet
+ fCurrentResponseUID = 0;
+ fCurrentLineContainedFlagInfo = false;
+ fSizeOfMostRecentMessage = 0;
+ // show any incremental progress, for instance, for header downloading
+ fServerConnection.ShowProgress();
+
+ fNextToken++; // eat the '(' character
+
+ // some of these productions are ignored for now
+ while (ContinueParse() && (*fNextToken != ')')) {
+ if (!PL_strcasecmp(fNextToken, "FLAGS")) {
+ if (fCurrentResponseUID == 0)
+ fFlagState->GetUidOfMessage(fFetchResponseIndex - 1,
+ &fCurrentResponseUID);
+
+ AdvanceToNextToken();
+ if (ContinueParse()) flags();
+
+ if (ContinueParse()) { // eat the closing ')'
+ fNextToken++;
+ // there may be another ')' to close out
+ // msg_fetch. If there is then don't advance
+ if (*fNextToken != ')') AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "UID")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fCurrentResponseUID = strtoul(fNextToken, nullptr, 10);
+ if (fCurrentResponseUID > fHighestRecordedUID)
+ fHighestRecordedUID = fCurrentResponseUID;
+ // size came before UID
+ if (fSizeOfMostRecentMessage)
+ fReceivedHeaderOrSizeForUID = CurrentResponseUID();
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ char lastTokenChar = *(fNextToken + strlen(fNextToken) - 1);
+ if (lastTokenChar == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ else if (lastTokenChar < '0' || lastTokenChar > '9') {
+ // GIANT HACK
+ // this is a corrupt uid - see if it's pre 5.08 Zimbra omitting
+ // a space between the UID and MODSEQ
+ if (strlen(fNextToken) > 6 &&
+ !strcmp("MODSEQ", fNextToken + strlen(fNextToken) - 6))
+ fNextToken += strlen(fNextToken) - 6;
+ } else
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "MODSEQ")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fNextToken++; // eat '('
+ uint64_t modSeq = ParseUint64Str(fNextToken);
+ if (modSeq > fHighestModSeq) fHighestModSeq = modSeq;
+
+ if (PL_strcasestr(fNextToken, ")")) {
+ // eat token chars until we get the ')'
+ fNextToken = strchr(fNextToken, ')');
+ if (fNextToken) {
+ fNextToken++;
+ if (*fNextToken != ')') AdvanceToNextToken();
+ } else
+ SetSyntaxError(true);
+ } else {
+ SetSyntaxError(true);
+ }
+ }
+ } else if (!PL_strcasecmp(fNextToken, "RFC822") ||
+ !PL_strcasecmp(fNextToken, "RFC822.HEADER") ||
+ !PL_strncasecmp(fNextToken, "BODY[HEADER", 11) ||
+ !PL_strncasecmp(fNextToken, "BODY[]", 6) ||
+ !PL_strcasecmp(fNextToken, "RFC822.TEXT") ||
+ (!PL_strncasecmp(fNextToken, "BODY[", 5) &&
+ PL_strstr(fNextToken, "HEADER"))) {
+ if (fCurrentResponseUID == 0)
+ fFlagState->GetUidOfMessage(fFetchResponseIndex - 1,
+ &fCurrentResponseUID);
+
+ if (!PL_strcasecmp(fNextToken, "RFC822.HEADER") ||
+ !PL_strcasecmp(fNextToken, "BODY[HEADER]")) {
+ // all of this message's headers
+ AdvanceToNextToken();
+ fDownloadingHeaders = true;
+ BeginMessageDownload(MESSAGE_RFC822); // initialize header parser
+ bNeedEndMessageDownload = false;
+ if (ContinueParse()) msg_fetch_headers(nullptr);
+ } else if (!PL_strncasecmp(fNextToken, "BODY[HEADER.FIELDS", 19)) {
+ fDownloadingHeaders = true;
+ BeginMessageDownload(MESSAGE_RFC822); // initialize header parser
+ // specific message header fields
+ while (ContinueParse() && fNextToken[strlen(fNextToken) - 1] != ']')
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ bNeedEndMessageDownload = false;
+ AdvanceToNextToken();
+ if (ContinueParse()) msg_fetch_headers(nullptr);
+ }
+ } else {
+ char* whereHeader = PL_strstr(fNextToken, "HEADER");
+ if (whereHeader) {
+ const char* startPartNum = fNextToken + 5;
+ if (whereHeader > startPartNum) {
+ int32_t partLength =
+ whereHeader - startPartNum - 1; //-1 for the dot!
+ char* partNum = (char*)PR_CALLOC((partLength + 1) * sizeof(char));
+ if (partNum) {
+ PL_strncpy(partNum, startPartNum, partLength);
+ if (ContinueParse()) {
+ if (PL_strstr(fNextToken, "FIELDS")) {
+ while (ContinueParse() &&
+ fNextToken[strlen(fNextToken) - 1] != ']')
+ AdvanceToNextToken();
+ }
+ if (ContinueParse()) {
+ AdvanceToNextToken();
+ if (ContinueParse()) msg_fetch_headers(partNum);
+ }
+ }
+ PR_Free(partNum);
+ }
+ } else
+ SetSyntaxError(true);
+ } else {
+ fDownloadingHeaders = false;
+
+ bool chunk = false;
+ int32_t origin = 0;
+ if (!PL_strncasecmp(fNextToken, "BODY[]<", 7)) {
+ char* tokenCopy = 0;
+ tokenCopy = PL_strdup(fNextToken);
+ if (tokenCopy) {
+ char* originString =
+ tokenCopy + 7; // where the byte number starts
+ char* closeBracket = PL_strchr(tokenCopy, '>');
+ if (closeBracket && originString && *originString) {
+ *closeBracket = 0;
+ origin = atoi(originString);
+ chunk = true;
+ }
+ PR_Free(tokenCopy);
+ }
+ }
+
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ msg_fetch_content(chunk, origin, MESSAGE_RFC822);
+ }
+ }
+ }
+ } else if (!PL_strcasecmp(fNextToken, "RFC822.SIZE") ||
+ !PL_strcasecmp(fNextToken, "XAOL.SIZE")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ bool sendEndMsgDownload =
+ (GetDownloadingHeaders() &&
+ fReceivedHeaderOrSizeForUID == CurrentResponseUID());
+ fSizeOfMostRecentMessage = strtoul(fNextToken, nullptr, 10);
+ fReceivedHeaderOrSizeForUID = CurrentResponseUID();
+ if (sendEndMsgDownload) {
+ fServerConnection.NormalMessageEndDownload();
+ fReceivedHeaderOrSizeForUID = nsMsgKey_None;
+ }
+
+ if (fSizeOfMostRecentMessage == 0 && CurrentResponseUID()) {
+ // on no, bogus Netscape 2.0 mail server bug
+ char uidString[100];
+ sprintf(uidString, "%ld", (long)CurrentResponseUID());
+
+ if (!fZeroLengthMessageUidString.IsEmpty())
+ fZeroLengthMessageUidString += ",";
+
+ fZeroLengthMessageUidString += uidString;
+ }
+
+ // if this token ends in ')', then it is the last token
+ // else we advance
+ if (*(fNextToken + strlen(fNextToken) - 1) == ')')
+ fNextToken += strlen(fNextToken) - 1;
+ else
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "XSENDER")) {
+ PR_FREEIF(fXSenderInfo);
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fXSenderInfo = CreateAstring();
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "X-GM-MSGID")) {
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fMsgID = CreateAtom();
+ AdvanceToNextToken();
+ nsCString msgIDValue;
+ msgIDValue.Assign(fMsgID);
+ if (fCurrentResponseUID == 0)
+ fFlagState->GetUidOfMessage(fFetchResponseIndex - 1,
+ &fCurrentResponseUID);
+ fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-MSGID"_ns,
+ msgIDValue);
+ PR_FREEIF(fMsgID);
+ }
+ } else if (!PL_strcasecmp(fNextToken, "X-GM-THRID")) {
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fThreadID = CreateAtom();
+ AdvanceToNextToken();
+ nsCString threadIDValue;
+ threadIDValue.Assign(fThreadID);
+ if (fCurrentResponseUID == 0)
+ fFlagState->GetUidOfMessage(fFetchResponseIndex - 1,
+ &fCurrentResponseUID);
+ fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-THRID"_ns,
+ threadIDValue);
+ PR_FREEIF(fThreadID);
+ }
+ } else if (!PL_strcasecmp(fNextToken, "X-GM-LABELS")) {
+ AdvanceToNextToken();
+ if (!fNextToken)
+ SetSyntaxError(true);
+ else {
+ fLabels = CreateParenGroup();
+ nsCString labelsValue;
+ labelsValue.Assign(fLabels);
+ labelsValue.Cut(0, 1);
+ labelsValue.Cut(labelsValue.Length() - 1, 1);
+ if (fCurrentResponseUID == 0)
+ fFlagState->GetUidOfMessage(fFetchResponseIndex - 1,
+ &fCurrentResponseUID);
+ fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-LABELS"_ns,
+ labelsValue);
+ PR_FREEIF(fLabels);
+ }
+ }
+
+ // I only fetch RFC822 so I should never see these BODY responses
+ else if (!PL_strcasecmp(fNextToken, "BODY"))
+ skip_to_CRLF(); // I never ask for this
+ else if (!PL_strncasecmp(fNextToken, "BODY[TEXT", 9)) {
+ // This parses the "preview" response (first 2048 bytes of body text).
+ mime_part_data(); // Note: TEXT is not an actual mime part.
+ } else if (!PL_strcasecmp(fNextToken, "ENVELOPE")) {
+ fDownloadingHeaders = true;
+ bNeedEndMessageDownload = true;
+ BeginMessageDownload(MESSAGE_RFC822);
+ envelope_data();
+ } else if (!PL_strcasecmp(fNextToken, "INTERNALDATE")) {
+ fDownloadingHeaders =
+ true; // we only request internal date while downloading headers
+ if (!bNeedEndMessageDownload) BeginMessageDownload(MESSAGE_RFC822);
+ bNeedEndMessageDownload = true;
+ internal_date();
+ } else if (!PL_strcasecmp(fNextToken, "XAOL-ENVELOPE")) {
+ fDownloadingHeaders = true;
+ if (!bNeedEndMessageDownload) BeginMessageDownload(MESSAGE_RFC822);
+ bNeedEndMessageDownload = true;
+ xaolenvelope_data();
+ } else {
+ nsImapAction imapAction;
+ if (!fServerConnection.GetCurrentUrl()) return;
+ fServerConnection.GetCurrentUrl()->GetImapAction(&imapAction);
+ nsAutoCString userDefinedFetchAttribute;
+ fServerConnection.GetCurrentUrl()->GetCustomAttributeToFetch(
+ userDefinedFetchAttribute);
+ if ((imapAction == nsIImapUrl::nsImapUserDefinedFetchAttribute &&
+ !strcmp(userDefinedFetchAttribute.get(), fNextToken)) ||
+ imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand) {
+ AdvanceToNextToken();
+ char* fetchResult;
+ if (fNextToken[0] == '(')
+ // look through the tokens until we find the closing ')'
+ // we can have a result like the following:
+ // ((A B) (C D) (E F))
+ fetchResult = CreateParenGroup();
+ else {
+ fetchResult = CreateAstring();
+ AdvanceToNextToken();
+ }
+ if (imapAction == nsIImapUrl::nsImapUserDefinedFetchAttribute)
+ fServerConnection.GetCurrentUrl()->SetCustomAttributeResult(
+ nsDependentCString(fetchResult));
+ if (imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand)
+ fServerConnection.GetCurrentUrl()->SetCustomCommandResult(
+ nsDependentCString(fetchResult));
+ PR_Free(fetchResult);
+ } else
+ SetSyntaxError(true);
+ }
+ }
+
+ if (ContinueParse()) {
+ if (CurrentResponseUID() && CurrentResponseUID() != nsMsgKey_None &&
+ fCurrentLineContainedFlagInfo && fFlagState) {
+ fFlagState->AddUidFlagPair(CurrentResponseUID(), fSavedFlagInfo,
+ fFetchResponseIndex - 1);
+ for (uint32_t i = 0; i < fCustomFlags.Length(); i++)
+ fFlagState->AddUidCustomFlagPair(CurrentResponseUID(),
+ fCustomFlags[i].get());
+ fCustomFlags.Clear();
+ }
+
+ if (fFetchingAllFlags)
+ fCurrentLineContainedFlagInfo =
+ false; // do not fire if in PostProcessEndOfLine
+
+ AdvanceToNextToken(); // eat the ')' ending token
+ // should be at end of line
+ if (bNeedEndMessageDownload) {
+ if (ContinueParse()) {
+ // complete the message download
+ fServerConnection.NormalMessageEndDownload();
+ } else
+ fServerConnection.AbortMessageDownLoad();
+ }
+ }
+}
+
+typedef enum _envelopeItemType {
+ envelopeString,
+ envelopeAddress
+} envelopeItemType;
+
+typedef struct {
+ const char* name;
+ envelopeItemType type;
+} envelopeItem;
+
+// RFC3501: envelope = "(" env-date SP env-subject SP env-from SP
+// env-sender SP env-reply-to SP env-to SP env-cc SP
+// env-bcc SP env-in-reply-to SP env-message-id ")"
+// env-date = nstring
+// env-subject = nstring
+// env-from = "(" 1*address ")" / nil
+// env-sender = "(" 1*address ")" / nil
+// env-reply-to= "(" 1*address ")" / nil
+// env-to = "(" 1*address ")" / nil
+// env-cc = "(" 1*address ")" / nil
+// env-bcc = "(" 1*address ")" / nil
+// env-in-reply-to = nstring
+// env-message-id = nstring
+
+static const envelopeItem EnvelopeTable[] = {
+ {"Date", envelopeString}, {"Subject", envelopeString},
+ {"From", envelopeAddress}, {"Sender", envelopeAddress},
+ {"Reply-to", envelopeAddress}, {"To", envelopeAddress},
+ {"Cc", envelopeAddress}, {"Bcc", envelopeAddress},
+ {"In-reply-to", envelopeString}, {"Message-id", envelopeString}};
+
+void nsImapServerResponseParser::envelope_data() {
+ AdvanceToNextToken();
+ fNextToken++; // eat '('
+ for (int tableIndex = 0;
+ tableIndex < (int)(sizeof(EnvelopeTable) / sizeof(EnvelopeTable[0]));
+ tableIndex++) {
+ if (!ContinueParse()) break;
+ if (*fNextToken == ')') {
+ SetSyntaxError(true); // envelope too short
+ break;
+ }
+
+ nsAutoCString headerLine(EnvelopeTable[tableIndex].name);
+ headerLine += ": ";
+ bool headerNonNil = true;
+ if (EnvelopeTable[tableIndex].type == envelopeString) {
+ nsAutoCString strValue;
+ strValue.Adopt(CreateNilString());
+ if (!strValue.IsEmpty())
+ headerLine.Append(strValue);
+ else
+ headerNonNil = false;
+ } else {
+ nsAutoCString address;
+ parse_address(address);
+ headerLine += address;
+ if (address.IsEmpty()) headerNonNil = false;
+ }
+ if (headerNonNil)
+ fServerConnection.HandleMessageDownLoadLine(headerLine.get(), false);
+
+ if (ContinueParse()) AdvanceToNextToken();
+ }
+ // Now we should be at the end of the envelope and have *fToken == ')'.
+ // Skip this last parenthesis.
+ AdvanceToNextToken();
+}
+
+void nsImapServerResponseParser::xaolenvelope_data() {
+ // eat the opening '('
+ fNextToken++;
+
+ if (ContinueParse() && (*fNextToken != ')')) {
+ AdvanceToNextToken();
+ fNextToken++; // eat '('
+ nsAutoCString subject;
+ subject.Adopt(CreateNilString());
+ nsAutoCString subjectLine("Subject: ");
+ subjectLine += subject;
+ fServerConnection.HandleMessageDownLoadLine(subjectLine.get(), false);
+ fNextToken++; // eat the next '('
+ if (ContinueParse()) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ nsAutoCString fromLine;
+ if (!strcmp(GetSelectedMailboxName(), "Sent Items")) {
+ // xaol envelope switches the From with the To, so we switch them back
+ // and create a fake from line From: user@aol.com
+ fromLine.AppendLiteral("To: ");
+ nsAutoCString fakeFromLine("From: "_ns);
+ fakeFromLine.Append(fServerConnection.GetImapUserName());
+ fakeFromLine.AppendLiteral("@aol.com");
+ fServerConnection.HandleMessageDownLoadLine(fakeFromLine.get(),
+ false);
+ } else {
+ fromLine.AppendLiteral("From: ");
+ }
+ parse_address(fromLine);
+ fServerConnection.HandleMessageDownLoadLine(fromLine.get(), false);
+ if (ContinueParse()) {
+ AdvanceToNextToken(); // ge attachment size
+ int32_t attachmentSize = atoi(fNextToken);
+ if (attachmentSize != 0) {
+ nsAutoCString attachmentLine("X-attachment-size: ");
+ attachmentLine.AppendInt(attachmentSize);
+ fServerConnection.HandleMessageDownLoadLine(attachmentLine.get(),
+ false);
+ }
+ }
+ if (ContinueParse()) {
+ AdvanceToNextToken(); // skip image size
+ int32_t imageSize = atoi(fNextToken);
+ if (imageSize != 0) {
+ nsAutoCString imageLine("X-image-size: ");
+ imageLine.AppendInt(imageSize);
+ fServerConnection.HandleMessageDownLoadLine(imageLine.get(), false);
+ }
+ }
+ if (ContinueParse()) AdvanceToNextToken(); // skip )
+ }
+ }
+ }
+}
+
+void nsImapServerResponseParser::parse_address(nsAutoCString& addressLine) {
+ // NOTE: Not sure this function correctly handling group address syntax.
+ // See Bug 1609846.
+ if (!strcmp(fNextToken, "NIL")) return;
+ bool firstAddress = true;
+ // should really look at chars here
+ NS_ASSERTION(*fNextToken == '(', "address should start with '('");
+ fNextToken++; // eat the next '('
+ while (ContinueParse() && *fNextToken == '(') {
+ NS_ASSERTION(*fNextToken == '(', "address should start with '('");
+ fNextToken++; // eat the next '('
+
+ if (!firstAddress) addressLine += ", ";
+
+ firstAddress = false;
+ char* personalName = CreateNilString();
+ AdvanceToNextToken();
+ char* atDomainList = CreateNilString();
+ if (ContinueParse()) {
+ AdvanceToNextToken();
+ char* mailboxName = CreateNilString();
+ if (ContinueParse()) {
+ AdvanceToNextToken();
+ char* hostName = CreateNilString();
+ AdvanceToNextToken();
+ if (mailboxName) {
+ addressLine += mailboxName;
+ }
+ if (hostName) {
+ addressLine += '@';
+ addressLine += hostName;
+ PR_Free(hostName);
+ }
+ if (personalName) {
+ addressLine += " (";
+ addressLine += personalName;
+ addressLine += ')';
+ }
+ }
+ PR_Free(mailboxName);
+ }
+ PR_Free(personalName);
+ PR_Free(atDomainList);
+
+ if (*fNextToken == ')') fNextToken++;
+ // if the next token isn't a ')' for the address term,
+ // then we must have another address pair left....so get the next
+ // token and continue parsing in this loop...
+ if (*fNextToken == '\0') AdvanceToNextToken();
+ }
+ if (*fNextToken == ')') fNextToken++;
+ // AdvanceToNextToken(); // skip "))"
+}
+
+void nsImapServerResponseParser::internal_date() {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ nsAutoCString dateLine("Date: ");
+ char* strValue = CreateNilString();
+ if (strValue) {
+ dateLine += strValue;
+ free(strValue);
+ }
+ fServerConnection.HandleMessageDownLoadLine(dateLine.get(), false);
+ }
+ // advance the parser.
+ AdvanceToNextToken();
+}
+
+void nsImapServerResponseParser::flags() {
+ imapMessageFlagsType messageFlags = kNoImapMsgFlag;
+ fCustomFlags.Clear();
+
+ // clear the custom flags for this message
+ // otherwise the old custom flags will stay around
+ // see bug #191042
+ if (fFlagState && CurrentResponseUID() != nsMsgKey_None)
+ fFlagState->ClearCustomFlags(CurrentResponseUID());
+
+ // eat the opening '('
+ fNextToken++;
+ while (ContinueParse() && (*fNextToken != ')')) {
+ bool knownFlag = false;
+ if (*fNextToken == '\\') {
+ switch (NS_ToUpper(fNextToken[1])) {
+ case 'S':
+ if (!PL_strncasecmp(fNextToken, "\\Seen", 5)) {
+ messageFlags |= kImapMsgSeenFlag;
+ knownFlag = true;
+ }
+ break;
+ case 'A':
+ if (!PL_strncasecmp(fNextToken, "\\Answered", 9)) {
+ messageFlags |= kImapMsgAnsweredFlag;
+ knownFlag = true;
+ }
+ break;
+ case 'F':
+ if (!PL_strncasecmp(fNextToken, "\\Flagged", 8)) {
+ messageFlags |= kImapMsgFlaggedFlag;
+ knownFlag = true;
+ }
+ break;
+ case 'D':
+ if (!PL_strncasecmp(fNextToken, "\\Deleted", 8)) {
+ messageFlags |= kImapMsgDeletedFlag;
+ knownFlag = true;
+ } else if (!PL_strncasecmp(fNextToken, "\\Draft", 6)) {
+ messageFlags |= kImapMsgDraftFlag;
+ knownFlag = true;
+ }
+ break;
+ case 'R':
+ if (!PL_strncasecmp(fNextToken, "\\Recent", 7)) {
+ messageFlags |= kImapMsgRecentFlag;
+ knownFlag = true;
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (*fNextToken == '$') {
+ switch (NS_ToUpper(fNextToken[1])) {
+ case 'M':
+ if ((fSupportsUserDefinedFlags &
+ (kImapMsgSupportUserFlag | kImapMsgSupportMDNSentFlag)) &&
+ !PL_strncasecmp(fNextToken, "$MDNSent", 8)) {
+ messageFlags |= kImapMsgMDNSentFlag;
+ knownFlag = true;
+ }
+ break;
+ case 'F':
+ if ((fSupportsUserDefinedFlags &
+ (kImapMsgSupportUserFlag | kImapMsgSupportForwardedFlag)) &&
+ !PL_strncasecmp(fNextToken, "$Forwarded", 10)) {
+ messageFlags |= kImapMsgForwardedFlag;
+ knownFlag = true;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ if (!knownFlag && fFlagState) {
+ nsAutoCString flag(fNextToken);
+ int32_t parenIndex = flag.FindChar(')');
+ if (parenIndex > 0) flag.SetLength(parenIndex);
+ messageFlags |= kImapMsgCustomKeywordFlag;
+ if (CurrentResponseUID() != nsMsgKey_None && CurrentResponseUID() != 0)
+ fFlagState->AddUidCustomFlagPair(CurrentResponseUID(), flag.get());
+ else
+ fCustomFlags.AppendElement(flag);
+ }
+ if (PL_strcasestr(fNextToken, ")")) {
+ // eat token chars until we get the ')'
+ while (*fNextToken != ')') fNextToken++;
+ } else
+ AdvanceToNextToken();
+ }
+
+ if (ContinueParse())
+ while (*fNextToken != ')') fNextToken++;
+
+ fCurrentLineContainedFlagInfo = true; // handled in PostProcessEndOfLine
+ fSavedFlagInfo = messageFlags;
+}
+
+// RFC3501: resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
+// ; Status condition
+void nsImapServerResponseParser::resp_cond_state(bool isTagged) {
+ // According to RFC3501, Sec. 7.1, the untagged NO response "indicates a
+ // warning; the command can still complete successfully."
+ // However, the untagged BAD response "indicates a protocol-level error for
+ // which the associated command can not be determined; it can also indicate an
+ // internal server failure."
+ // Thus, we flag an error for a tagged NO response and for any BAD response.
+ if ((isTagged && !PL_strcasecmp(fNextToken, "NO")) ||
+ !PL_strcasecmp(fNextToken, "BAD"))
+ fCurrentCommandFailed = true;
+
+ AdvanceToNextToken();
+ if (ContinueParse()) resp_text();
+}
+
+/*
+resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
+
+ was changed to in order to enable a one symbol look ahead predictive
+ parser.
+
+ resp_text ::= ["[" resp_text_code SPACE] (text_mime2 / text)
+*/
+void nsImapServerResponseParser::resp_text() {
+ if (ContinueParse() && (*fNextToken == '[')) resp_text_code();
+
+ if (ContinueParse()) {
+ if (!PL_strcmp(fNextToken, "=?"))
+ text_mime2();
+ else
+ text();
+ }
+}
+/*
+ text_mime2 ::= "=?" <charset> "?" <encoding> "?"
+ <encoded-text> "?="
+ ;; Syntax defined in [MIME-2]
+*/
+void nsImapServerResponseParser::text_mime2() { skip_to_CRLF(); }
+
+/*
+ text ::= 1*TEXT_CHAR
+
+*/
+void nsImapServerResponseParser::text() { skip_to_CRLF(); }
+
+void nsImapServerResponseParser::parse_folder_flags(bool calledForFlags) {
+ uint16_t junkNotJunkFlags = 0;
+
+ do {
+ AdvanceToNextToken();
+ if (*fNextToken == '(') fNextToken++;
+ if (!PL_strncasecmp(fNextToken, "\\Seen", 5))
+ fSettablePermanentFlags |= kImapMsgSeenFlag;
+ else if (!PL_strncasecmp(fNextToken, "\\Answered", 9))
+ fSettablePermanentFlags |= kImapMsgAnsweredFlag;
+ else if (!PL_strncasecmp(fNextToken, "\\Flagged", 8))
+ fSettablePermanentFlags |= kImapMsgFlaggedFlag;
+ else if (!PL_strncasecmp(fNextToken, "\\Deleted", 8))
+ fSettablePermanentFlags |= kImapMsgDeletedFlag;
+ else if (!PL_strncasecmp(fNextToken, "\\Draft", 6))
+ fSettablePermanentFlags |= kImapMsgDraftFlag;
+ else if (!PL_strncasecmp(fNextToken, "\\*", 2)) {
+ // User defined and special keywords (tags) can be defined and set for
+ // mailbox. Should only occur in PERMANENTFLAGS response.
+ fSupportsUserDefinedFlags |= kImapMsgSupportUserFlag;
+ fSupportsUserDefinedFlags |= kImapMsgSupportForwardedFlag;
+ fSupportsUserDefinedFlags |= kImapMsgSupportMDNSentFlag;
+ }
+ // Treat special and built-in $LabelX's as user defined and include
+ // $Junk/$NotJunk too.
+ else if (!PL_strncasecmp(fNextToken, "$MDNSent", 8))
+ fSupportsUserDefinedFlags |= kImapMsgSupportMDNSentFlag;
+ else if (!PL_strncasecmp(fNextToken, "$Forwarded", 10))
+ fSupportsUserDefinedFlags |= kImapMsgSupportForwardedFlag;
+ else if (!PL_strncasecmp(fNextToken, "$Junk", 5))
+ junkNotJunkFlags |= 1;
+ else if (!PL_strncasecmp(fNextToken, "$NotJunk", 8))
+ junkNotJunkFlags |= 2;
+ } while (!fAtEndOfLine && ContinueParse());
+
+ if (fFlagState) fFlagState->OrSupportedUserFlags(fSupportsUserDefinedFlags);
+
+ if (calledForFlags) {
+ // Set true if both "$Junk" and "$NotJunk" appear in FLAGS.
+ fStdJunkNotJunkUseOk = (junkNotJunkFlags == 3);
+ }
+}
+/*
+ resp_text_code ::= ("ALERT" / "PARSE" /
+ "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
+ "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
+ "UIDVALIDITY" SPACE nz_number /
+ "UNSEEN" SPACE nz_number /
+ "HIGHESTMODSEQ" SPACE nz_number /
+ "NOMODSEQ" /
+ atom [SPACE 1*<any TEXT_CHAR except "]">] )
+ "]"
+
+
+*/
+void nsImapServerResponseParser::resp_text_code() {
+ // this is a special case way of advancing the token
+ // strtok won't break up "[ALERT]" into separate tokens
+ if (strlen(fNextToken) > 1)
+ fNextToken++;
+ else
+ AdvanceToNextToken();
+
+ if (ContinueParse()) {
+ if (!PL_strcasecmp(fNextToken, "ALERT]")) {
+ char* alertMsg = fCurrentTokenPlaceHolder; // advance past ALERT]
+ if (alertMsg && *alertMsg &&
+ (!fLastAlert || PL_strcmp(fNextToken, fLastAlert))) {
+ fServerConnection.AlertUserEvent(alertMsg);
+ PR_Free(fLastAlert);
+ fLastAlert = PL_strdup(alertMsg);
+ }
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "PARSE]")) {
+ // do nothing for now
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "NETSCAPE]")) {
+ skip_to_CRLF();
+ } else if (!PL_strcasecmp(fNextToken, "PERMANENTFLAGS")) {
+ uint32_t saveSettableFlags = fSettablePermanentFlags;
+ fSupportsUserDefinedFlags = 0; // assume no unless told
+ fSettablePermanentFlags = 0; // assume none, unless told otherwise.
+ parse_folder_flags(false);
+ // if the server tells us there are no permanent flags, we're
+ // just going to pretend that the FLAGS response flags, if any, are
+ // permanent in case the server is broken. This will allow us
+ // to store delete and seen flag changes - if they're not permanent,
+ // they're not permanent, but at least we'll try to set them.
+ if (!fSettablePermanentFlags) fSettablePermanentFlags = saveSettableFlags;
+ fGotPermanentFlags = true;
+ } else if (!PL_strcasecmp(fNextToken, "READ-ONLY]")) {
+ fCurrentFolderReadOnly = true;
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "READ-WRITE]")) {
+ fCurrentFolderReadOnly = false;
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "TRYCREATE]")) {
+ // do nothing for now
+ AdvanceToNextToken();
+ } else if (!PL_strcasecmp(fNextToken, "UIDVALIDITY")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fFolderUIDValidity = strtoul(fNextToken, nullptr, 10);
+ fHighestRecordedUID = 0;
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "UNSEEN")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ // Note: As a response code, "UNSEEN" is NOT the number of
+ // unseen/unread messages. It is the lowest sequence number of the first
+ // unseen/unread message in the mailbox.
+ fSeqNumOfFirstUnseenMsg = strtoul(fNextToken, nullptr, 10);
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "UIDNEXT")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fNextUID = strtoul(fNextToken, nullptr, 10);
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "APPENDUID")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ // ** jt -- the returned uidvalidity is the destination folder
+ // uidvalidity; don't use it for current folder
+ // fFolderUIDValidity = atoi(fNextToken);
+ // fHighestRecordedUID = 0; ??? this should be wrong
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fCurrentResponseUID = strtoul(fNextToken, nullptr, 10);
+ AdvanceToNextToken();
+ }
+ }
+ } else if (!PL_strcasecmp(fNextToken, "COPYUID")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ // ** jt -- destination folder uidvalidity
+ // fFolderUIDValidity = atoi(fNextToken);
+ // original message set; ignore it
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ // the resulting message set; should be in the form of
+ // either uid or uid1:uid2
+ AdvanceToNextToken();
+ // clear copy response uid
+ fServerConnection.SetCopyResponseUid(fNextToken);
+ }
+ if (ContinueParse()) AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "HIGHESTMODSEQ")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ fHighestModSeq = ParseUint64Str(fNextToken);
+ fUseModSeq = true;
+ AdvanceToNextToken();
+ }
+ } else if (!PL_strcasecmp(fNextToken, "NOMODSEQ]")) {
+ fHighestModSeq = 0;
+ fUseModSeq = false;
+ skip_to_CRLF();
+ } else if (!PL_strcasecmp(fNextToken, "CAPABILITY")) {
+ capability_data();
+ } else if (!PL_strcasecmp(fNextToken, "MYRIGHTS")) {
+ myrights_data(true);
+ } else // just text
+ {
+ // do nothing but eat tokens until we see the ] or CRLF
+ // we should see the ] but we don't want to go into an
+ // endless loop if the CRLF is not there
+ do {
+ AdvanceToNextToken();
+ } while (!PL_strcasestr(fNextToken, "]") && !fAtEndOfLine &&
+ ContinueParse());
+ }
+ }
+}
+
+// RFC3501: response-done = response-tagged / response-fatal
+void nsImapServerResponseParser::response_done() {
+ if (ContinueParse()) {
+ if (!PL_strcmp(fCurrentCommandTag, fNextToken))
+ response_tagged();
+ else
+ response_fatal();
+ }
+}
+
+// RFC3501: response-tagged = tag SP resp-cond-state CRLF
+void nsImapServerResponseParser::response_tagged() {
+ // eat the tag
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ resp_cond_state(true);
+ if (ContinueParse()) {
+ if (!fAtEndOfLine)
+ SetSyntaxError(true);
+ else if (!fCurrentCommandFailed)
+ ResetLexAnalyzer();
+ }
+ }
+}
+
+// RFC3501: response-fatal = "*" SP resp-cond-bye CRLF
+// ; Server closes connection immediately
+void nsImapServerResponseParser::response_fatal() {
+ // eat the "*"
+ AdvanceToNextToken();
+ if (ContinueParse()) resp_cond_bye();
+}
+
+// RFC3501: resp-cond-bye = "BYE" SP resp-text
+void nsImapServerResponseParser::resp_cond_bye() {
+ SetConnected(false);
+ fIMAPstate = kNonAuthenticated;
+}
+
+void nsImapServerResponseParser::msg_fetch_headers(const char* partNum) {
+ msg_fetch_content(false, 0, MESSAGE_RFC822);
+}
+
+/* nstring ::= string / nil
+string ::= quoted / literal
+nil ::= "NIL"
+
+*/
+void nsImapServerResponseParser::msg_fetch_content(bool chunk, int32_t origin,
+ const char* content_type) {
+ // setup the stream for downloading this message.
+ // Note: no longer concerned with body shell issues since now we only fetch
+ // full message.
+ if ((!chunk || (origin == 0)) && !GetDownloadingHeaders()) {
+ if (NS_FAILED(BeginMessageDownload(content_type))) return;
+ }
+
+ if (PL_strcasecmp(fNextToken, "NIL")) {
+ if (*fNextToken == '"')
+ fLastChunk = msg_fetch_quoted();
+ else
+ fLastChunk = msg_fetch_literal(chunk, origin);
+ } else
+ AdvanceToNextToken(); // eat "NIL"
+
+ if (fLastChunk) {
+ // complete the message download
+ if (ContinueParse()) {
+ if (fReceivedHeaderOrSizeForUID == CurrentResponseUID()) {
+ fServerConnection.NormalMessageEndDownload();
+ fReceivedHeaderOrSizeForUID = nsMsgKey_None;
+ } else
+ fReceivedHeaderOrSizeForUID = CurrentResponseUID();
+ } else
+ fServerConnection.AbortMessageDownLoad();
+ }
+}
+
+void nsImapServerResponseParser::mime_part_data() {
+ char* checkOriginToken = PL_strdup(fNextToken);
+ if (checkOriginToken) {
+ uint32_t origin = 0;
+ bool originFound = false;
+ char* whereStart = PL_strchr(checkOriginToken, '<');
+ if (whereStart) {
+ char* whereEnd = PL_strchr(whereStart, '>');
+ if (whereEnd) {
+ *whereEnd = 0;
+ whereStart++;
+ origin = atoi(whereStart);
+ originFound = true;
+ }
+ }
+ PR_Free(checkOriginToken);
+ AdvanceToNextToken();
+ msg_fetch_content(originFound, origin,
+ MESSAGE_RFC822); // keep content type as message/rfc822,
+ // even though the
+ // MIME part might not be, because then libmime will
+ // still handle and decode it.
+ } else
+ HandleMemoryFailure();
+}
+
+/*
+quoted ::= <"> *QUOTED_CHAR <">
+
+ QUOTED_CHAR ::= <any TEXT_CHAR except quoted_specials> /
+ "\" quoted_specials
+
+ quoted_specials ::= <"> / "\"
+*/
+
+bool nsImapServerResponseParser::msg_fetch_quoted() {
+ // *Should* never get a quoted string in response to a chunked download,
+ // but the RFCs don't forbid it
+ char* q = CreateQuoted();
+ if (q) {
+ numberOfCharsInThisChunk = PL_strlen(q);
+ fServerConnection.HandleMessageDownLoadLine(q, false, q);
+ PR_Free(q);
+ } else
+ numberOfCharsInThisChunk = 0;
+
+ AdvanceToNextToken();
+ bool lastChunk =
+ ((fServerConnection.GetCurFetchSize() == 0) ||
+ (numberOfCharsInThisChunk != fServerConnection.GetCurFetchSize()));
+ return lastChunk;
+}
+
+/* msg_obsolete ::= "COPY" / ("STORE" SPACE msg_fetch)
+;; OBSOLETE untagged data responses */
+void nsImapServerResponseParser::msg_obsolete() {
+ if (!PL_strcasecmp(fNextToken, "COPY"))
+ AdvanceToNextToken();
+ else if (!PL_strcasecmp(fNextToken, "STORE")) {
+ AdvanceToNextToken();
+ if (ContinueParse()) msg_fetch();
+ } else
+ SetSyntaxError(true);
+}
+
+void nsImapServerResponseParser::capability_data() {
+ int32_t endToken = -1;
+ fCapabilityFlag = kCapabilityDefined | kHasAuthOldLoginCapability;
+ do {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ nsCString token(fNextToken);
+ endToken = token.FindChar(']');
+ if (endToken >= 0) token.SetLength(endToken);
+
+ if (token.Equals("AUTH=LOGIN", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthLoginCapability;
+ else if (token.Equals("AUTH=PLAIN", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthPlainCapability;
+ else if (token.Equals("AUTH=CRAM-MD5",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasCRAMCapability;
+ else if (token.Equals("AUTH=NTLM", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthNTLMCapability;
+ else if (token.Equals("AUTH=GSSAPI", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthGssApiCapability;
+ else if (token.Equals("AUTH=MSN", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthMSNCapability;
+ else if (token.Equals("AUTH=EXTERNAL",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasAuthExternalCapability;
+ else if (token.Equals("AUTH=XOAUTH2", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasXOAuth2Capability;
+ else if (token.Equals("STARTTLS", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasStartTLSCapability;
+ else if (token.Equals("LOGINDISABLED",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag &= ~kHasAuthOldLoginCapability; // remove flag
+ else if (token.Equals("XSENDER", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasXSenderCapability;
+ else if (token.Equals("IMAP4", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kIMAP4Capability;
+ else if (token.Equals("IMAP4rev1", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kIMAP4rev1Capability;
+ else if (Substring(token, 0, 5)
+ .Equals("IMAP4", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kIMAP4other;
+ else if (token.Equals("X-NO-ATOMIC-RENAME",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kNoHierarchyRename;
+ else if (token.Equals("X-NON-HIERARCHICAL-RENAME",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kNoHierarchyRename;
+ else if (token.Equals("NAMESPACE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kNamespaceCapability;
+ else if (token.Equals("ID", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasIDCapability;
+ else if (token.Equals("ACL", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kACLCapability;
+ else if (token.Equals("XSERVERINFO", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kXServerInfoCapability;
+ else if (token.Equals("UIDPLUS", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kUidplusCapability;
+ else if (token.Equals("LITERAL+", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kLiteralPlusCapability;
+ else if (token.Equals("XAOL-OPTION", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kAOLImapCapability;
+ else if (token.Equals("X-GM-EXT-1", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kGmailImapCapability;
+ else if (token.Equals("QUOTA", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kQuotaCapability;
+ else if (token.Equals("LANGUAGE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasLanguageCapability;
+ else if (token.Equals("IDLE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasIdleCapability;
+ else if (token.Equals("CONDSTORE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasCondStoreCapability;
+ else if (token.Equals("ENABLE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasEnableCapability;
+ else if (token.Equals("LIST-EXTENDED",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasListExtendedCapability;
+ else if (token.Equals("XLIST", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasXListCapability;
+ else if (token.Equals("SPECIAL-USE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasSpecialUseCapability;
+ else if (token.Equals("COMPRESS=DEFLATE",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasCompressDeflateCapability;
+ else if (token.Equals("MOVE", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasMoveCapability;
+ else if (token.Equals("HIGHESTMODSEQ",
+ nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasHighestModSeqCapability;
+ else if (token.Equals("CLIENTID", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasClientIDCapability;
+ else if (token.Equals("UTF8=ACCEPT",
+ nsCaseInsensitiveCStringComparator) ||
+ token.Equals("UTF8=ONLY", nsCaseInsensitiveCStringComparator))
+ fCapabilityFlag |= kHasUTF8AcceptCapability;
+ }
+ } while (fNextToken && endToken < 0 && !fAtEndOfLine && ContinueParse());
+
+ nsImapProtocol* navCon = &fServerConnection;
+ NS_ASSERTION(navCon,
+ "null imap protocol connection while parsing capability "
+ "response"); // we should always have this
+ if (navCon) {
+ navCon->CommitCapability();
+ fServerConnection.SetCapabilityResponseOccurred();
+ }
+ skip_to_CRLF();
+}
+
+void nsImapServerResponseParser::xmailboxinfo_data() {
+ AdvanceToNextToken();
+ if (!fNextToken) return;
+
+ char* mailboxName = CreateAstring(); // PL_strdup(fNextToken);
+ if (mailboxName) {
+ do {
+ AdvanceToNextToken();
+ if (fNextToken) {
+ if (!PL_strcmp("MANAGEURL", fNextToken)) {
+ AdvanceToNextToken();
+ fFolderAdminUrl = CreateAstring();
+ } else if (!PL_strcmp("POSTURL", fNextToken)) {
+ AdvanceToNextToken();
+ // ignore this for now...
+ }
+ }
+ } while (fNextToken && !fAtEndOfLine && ContinueParse());
+ }
+}
+
+void nsImapServerResponseParser::xserverinfo_data() {
+ do {
+ AdvanceToNextToken();
+ if (!fNextToken) break;
+ if (!PL_strcmp("MANAGEACCOUNTURL", fNextToken)) {
+ AdvanceToNextToken();
+ fMailAccountUrl.Adopt(CreateNilString());
+ } else if (!PL_strcmp("MANAGELISTSURL", fNextToken)) {
+ AdvanceToNextToken();
+ fManageListsUrl.Adopt(CreateNilString());
+ } else if (!PL_strcmp("MANAGEFILTERSURL", fNextToken)) {
+ AdvanceToNextToken();
+ fManageFiltersUrl.Adopt(CreateNilString());
+ }
+ } while (fNextToken && !fAtEndOfLine && ContinueParse());
+}
+
+void nsImapServerResponseParser::enable_data() {
+ do {
+ // eat each enable response;
+ AdvanceToNextToken();
+ if (!PL_strcasecmp("UTF8=ACCEPT", fNextToken)) fUtf8AcceptEnabled = true;
+ } while (fNextToken && !fAtEndOfLine && ContinueParse());
+}
+
+void nsImapServerResponseParser::language_data() {
+ // we may want to go out and store the language returned to us
+ // by the language command in the host info session stuff.
+
+ // for now, just eat the language....
+ do {
+ // eat each language returned to us
+ AdvanceToNextToken();
+ } while (fNextToken && !fAtEndOfLine && ContinueParse());
+}
+
+// cram/auth response data ::= "+" SPACE challenge CRLF
+// the server expects more client data after issuing its challenge
+
+void nsImapServerResponseParser::authChallengeResponse_data() {
+ AdvanceToNextToken();
+ fAuthChallenge = strdup(fNextToken);
+ fWaitingForMoreClientInput = true;
+
+ skip_to_CRLF();
+}
+
+void nsImapServerResponseParser::namespace_data() {
+ EIMAPNamespaceType namespaceType = kPersonalNamespace;
+ bool namespacesCommitted = false;
+ const char* serverKey = fServerConnection.GetImapServerKey();
+ while ((namespaceType != kUnknownNamespace) && ContinueParse()) {
+ AdvanceToNextToken();
+ while (fAtEndOfLine && ContinueParse()) AdvanceToNextToken();
+ if (!PL_strcasecmp(fNextToken, "NIL")) {
+ // No namespace for this type.
+ // Don't add anything to the Namespace object.
+ } else if (fNextToken[0] == '(') {
+ // There may be multiple namespaces of the same type.
+ // Go through each of them and add them to our Namespace object.
+
+ fNextToken++;
+ while (fNextToken[0] == '(' && ContinueParse()) {
+ // we have another namespace for this namespace type
+ fNextToken++;
+ if (fNextToken[0] != '"') {
+ SetSyntaxError(true);
+ } else {
+ char* namespacePrefix = CreateQuoted(false);
+
+ AdvanceToNextToken();
+ const char* quotedDelimiter = fNextToken;
+ char namespaceDelimiter = '\0';
+
+ if (quotedDelimiter[0] == '"') {
+ quotedDelimiter++;
+ namespaceDelimiter = quotedDelimiter[0];
+ } else if (!PL_strncasecmp(quotedDelimiter, "NIL", 3)) {
+ // NIL hierarchy delimiter. Leave namespace delimiter nullptr.
+ } else {
+ // not quoted or NIL.
+ SetSyntaxError(true);
+ }
+ if (ContinueParse()) {
+ // Add code to parse the TRANSLATE attribute if it is present....
+ // we'll also need to expand the name space code to take in the
+ // translated prefix name.
+
+ // Add it to a temporary list in the host.
+ if (fHostSessionList) {
+ nsImapNamespace* newNamespace = new nsImapNamespace(
+ namespaceType, namespacePrefix, namespaceDelimiter, false);
+ fHostSessionList->AddNewNamespaceForHost(serverKey, newNamespace);
+ }
+
+ skip_to_close_paren(); // Ignore any extension data
+
+ bool endOfThisNamespaceType = (fNextToken[0] == ')');
+ if (!endOfThisNamespaceType &&
+ fNextToken[0] !=
+ '(') // no space between namespaces of the same type
+ {
+ SetSyntaxError(true);
+ }
+ }
+ PR_Free(namespacePrefix);
+ }
+ }
+ } else {
+ SetSyntaxError(true);
+ }
+ switch (namespaceType) {
+ case kPersonalNamespace:
+ namespaceType = kOtherUsersNamespace;
+ break;
+ case kOtherUsersNamespace:
+ namespaceType = kPublicNamespace;
+ break;
+ default:
+ namespaceType = kUnknownNamespace;
+ break;
+ }
+ }
+ if (ContinueParse()) {
+ nsImapProtocol* navCon = &fServerConnection;
+ NS_ASSERTION(
+ navCon,
+ "null protocol connection while parsing namespace"); // we should
+ // always have
+ // this
+ if (navCon) {
+ navCon->CommitNamespacesForHostEvent();
+ namespacesCommitted = true;
+ }
+ }
+ skip_to_CRLF();
+
+ if (!namespacesCommitted && fHostSessionList) {
+ bool success;
+ fHostSessionList->FlushUncommittedNamespacesForHost(serverKey, success);
+ }
+}
+
+void nsImapServerResponseParser::myrights_data(bool unsolicited) {
+ AdvanceToNextToken();
+ if (ContinueParse() && !fAtEndOfLine) {
+ char* mailboxName;
+ // an unsolicited myrights response won't have the mailbox name in
+ // the response, so we use the selected mailbox name.
+ if (unsolicited) {
+ mailboxName = strdup(fSelectedMailboxName);
+ } else {
+ mailboxName = CreateAstring();
+ if (mailboxName) AdvanceToNextToken();
+ }
+ if (mailboxName) {
+ if (ContinueParse()) {
+ char* myrights = CreateAstring();
+ if (myrights) {
+ nsImapProtocol* navCon = &fServerConnection;
+ NS_ASSERTION(
+ navCon, "null connection parsing my rights"); // we should always
+ // have this
+ if (navCon)
+ navCon->AddFolderRightsForUser(mailboxName,
+ nullptr /* means "me" */, myrights);
+ PR_Free(myrights);
+ } else {
+ HandleMemoryFailure();
+ }
+ if (ContinueParse()) AdvanceToNextToken();
+ }
+ PR_Free(mailboxName);
+ } else {
+ HandleMemoryFailure();
+ }
+ } else {
+ SetSyntaxError(true);
+ }
+}
+
+void nsImapServerResponseParser::acl_data() {
+ AdvanceToNextToken();
+ if (ContinueParse() && !fAtEndOfLine) {
+ char* mailboxName = CreateAstring(); // PL_strdup(fNextToken);
+ if (mailboxName && ContinueParse()) {
+ AdvanceToNextToken();
+ while (ContinueParse() && !fAtEndOfLine) {
+ char* userName = CreateAstring(); // PL_strdup(fNextToken);
+ if (userName && ContinueParse()) {
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ char* rights = CreateAstring(); // PL_strdup(fNextToken);
+ if (rights) {
+ fServerConnection.AddFolderRightsForUser(mailboxName, userName,
+ rights);
+ PR_Free(rights);
+ } else
+ HandleMemoryFailure();
+
+ if (ContinueParse()) AdvanceToNextToken();
+ }
+ PR_Free(userName);
+ } else
+ HandleMemoryFailure();
+ }
+ PR_Free(mailboxName);
+ } else
+ HandleMemoryFailure();
+ }
+}
+
+// RFC2087: quotaroot_response = "QUOTAROOT" SP astring *(SP astring)
+// quota_response = "QUOTA" SP astring SP quota_list
+// quota_list = "(" [quota_resource *(SP quota_resource)] ")"
+// quota_resource = atom SP number SP number
+// draft-melnikov-extra-quota-00 proposes some additions to RFC2087 and
+// improves the documentation. We still only support RFC2087 capability QUOTA
+// and command GETQUOTAROOT and its untagged QUOTAROOT and QUOTA responses.
+void nsImapServerResponseParser::quota_data() {
+ if (!PL_strcasecmp(fNextToken, "QUOTAROOT")) {
+ // Ignore QUOTAROOT response (except to invalidate previously stored data).
+ nsCString quotaroot;
+ AdvanceToNextToken();
+ while (ContinueParse() && !fAtEndOfLine) {
+ quotaroot.Adopt(CreateAstring());
+ AdvanceToNextToken();
+ }
+ // Invalidate any previously stored quota data. Updated QUOTA data follows.
+ fServerConnection.UpdateFolderQuotaData(kInvalidateQuota, quotaroot, 0, 0);
+ } else if (!PL_strcasecmp(fNextToken, "QUOTA")) {
+ // Should have one QUOTA response per QUOTAROOT.
+ uint64_t usage, limit;
+ AdvanceToNextToken();
+ if (ContinueParse()) {
+ nsCString quotaroot;
+ quotaroot.Adopt(CreateAstring());
+ nsCString resource;
+ AdvanceToNextToken();
+ if (fNextToken) {
+ if (fNextToken[0] == '(') fNextToken++;
+ // Should have zero or more "resource|usage|limit" triplet per quotaroot
+ // name. See draft-melnikov-extra-quota-00 for specific examples. Well
+ // known resources are STORAGE (in Kbytes), number of MESSAGEs and
+ // number of MAILBOXes. However, servers typically only set a quota on
+ // STORAGE in KBytes. A mailbox can have multiple quotaroots but
+ // typically only one and with a single resource.
+ while (ContinueParse() && !fAtEndOfLine) {
+ resource.Adopt(CreateAstring());
+ AdvanceToNextToken();
+ usage = atoll(fNextToken);
+ AdvanceToNextToken();
+ nsAutoCString limitToken(fNextToken);
+ if (fNextToken[strlen(fNextToken) - 1] == ')')
+ limitToken.SetLength(strlen(fNextToken) - 1);
+ limit = atoll(limitToken.get());
+ // Some servers don't define a quotaroot name which we displays as
+ // blank.
+ nsCString quotaRootResource(quotaroot);
+ if (!quotaRootResource.IsEmpty()) {
+ quotaRootResource.AppendLiteral(" / ");
+ }
+ quotaRootResource.Append(resource);
+ fServerConnection.UpdateFolderQuotaData(
+ kStoreQuota, quotaRootResource, usage, limit);
+ AdvanceToNextToken();
+ }
+ }
+ }
+ } else {
+ SetSyntaxError(true);
+ }
+}
+
+void nsImapServerResponseParser::id_data() {
+ AdvanceToNextToken();
+ if (!PL_strcasecmp(fNextToken, "NIL"))
+ AdvanceToNextToken();
+ else
+ fServerIdResponse.Adopt(CreateParenGroup());
+ skip_to_CRLF();
+}
+
+bool nsImapServerResponseParser::GetDownloadingHeaders() {
+ return fDownloadingHeaders;
+}
+
+void nsImapServerResponseParser::ResetCapabilityFlag() {}
+
+/*
+ literal ::= "{" number "}" CRLF *CHAR8
+ Number represents the number of CHAR8 octets
+ */
+
+// Processes a message body, header or message part fetch response. Typically
+// the full message, header or part are processed in one call (effectively, one
+// chunk), and parameter `chunk` is false and `origin` (offset into the
+// response) is 0. But under some conditions and larger messages, multiple calls
+// will occur to process the message in multiple chunks and parameter `chunk`
+// will be true and parameter `origin` will increase by the chunk size from
+// initially 0 with each call. This function returns true if this is the last or
+// only chunk. This signals the caller that the stream should be closed since
+// the message response has been processed.
+bool nsImapServerResponseParser::msg_fetch_literal(bool chunk, int32_t origin) {
+ numberOfCharsInThisChunk = atoi(fNextToken + 1);
+ // If we didn't request a specific size, or the server isn't returning exactly
+ // as many octets as we requested, this must be the last or only chunk
+ bool lastChunk = (!chunk || (numberOfCharsInThisChunk !=
+ fServerConnection.GetCurFetchSize()));
+
+ // clang-format off
+ if (lastChunk)
+ MOZ_LOG(IMAP, mozilla::LogLevel::Debug,
+ ("PARSER: msg_fetch_literal() chunking=%s, requested=%d, receiving=%d",
+ chunk ? "true":"false", fServerConnection.GetCurFetchSize(),
+ numberOfCharsInThisChunk));
+ // clang-format on
+
+ charsReadSoFar = 0;
+
+ while (ContinueParse() && !fServerConnection.DeathSignalReceived() &&
+ (charsReadSoFar < numberOfCharsInThisChunk)) {
+ AdvanceToNextLine();
+ if (ContinueParse()) {
+ // When "\r\n" (CRLF) is split across two chunks, the '\n' at the
+ // beginning of the next chunk might be set to an empty line consisting
+ // only of "\r\n". This is observed while running unit tests with the imap
+ // "fake" server. The unexpected '\r' is discarded here. However, with
+ // several real world servers tested, e.g., Dovecot, Gmail, Outlook, Yahoo
+ // etc., the leading
+ // '\r' is not inserted so the beginning line of the next chunk remains
+ // just '\n' and no discard is required.
+ // In any case, this "orphan" line is ignored and not processed below.
+ if (fNextChunkStartsWithNewline && (*fCurrentLine == '\r')) {
+ // Cause fCurrentLine to point to '\n' which discards the '\r'.
+ char* usableCurrentLine = PL_strdup(fCurrentLine + 1);
+ PR_Free(fCurrentLine);
+ fCurrentLine = usableCurrentLine;
+ }
+
+ // strlen() *would* fail on data containing \0, but the above
+ // AdvanceToNextLine() in nsMsgLineStreamBuffer::ReadNextLine() we replace
+ // '\0' with ' ' (blank) because who cares about binary transparency, and
+ // anyway \0 in this context violates RFCs.
+ charsReadSoFar += strlen(fCurrentLine);
+ if (!fDownloadingHeaders && fCurrentCommandIsSingleMessageFetch) {
+ fServerConnection.ProgressEventFunctionUsingName(
+ "imapDownloadingMessage");
+ if (fTotalDownloadSize > 0)
+ fServerConnection.PercentProgressUpdateEvent(
+ ""_ns, u""_ns, charsReadSoFar + origin, fTotalDownloadSize);
+ }
+ if (charsReadSoFar > numberOfCharsInThisChunk) {
+ // This is the last line of a chunk. "Literal" here means actual email
+ // data and its EOLs, without imap protocol elements and their EOLs. End
+ // of line is defined by two characters \r\n (i.e., CRLF, 0xd,0xa)
+ // specified by RFC822. Here is an example the most typical last good
+ // line of a chunk: "1s8AA5i4AAvF4QAG6+sAAD0bAPsAAAAA1OAAC)\r\n", where
+ // ")\r\n" are non-literals. This an example of the last "good" line of
+ // a chunk that terminates with \r\n
+ // "FxcA/wAAAALN2gADu80ACS0nAPpVVAD1wNAABF5YAPhAJgD31+QABAAAAP8oMQD+HBwA/umj\r\n"
+ // followed by another line of non-literal data:
+ // " UID 1004)\r\n". These two are concatenated into a single string
+ // pointed to by fCurrentLine. The extra "non-literal data" on the last
+ // chunk line makes the charsReadSoFar greater than
+ // numberOfCharsInThisChunk (the configured chunk size). A problem
+ // occurs if the \r\n of the long line above is split between chunks and
+ // \n is contained in the next chunk. For example, if last lines of
+ // chunk X are:
+ // "/gAOC/wA/QAAAAAAAAAA8wACCvz+AgIEAAD8/P4ABQUAAPoAAAD+AAEA/voHAAQGBQD/BAQA\r"
+ // ")\r\n"
+ // and the first two lines of chunk X+1 are:
+ // "\n"
+ // "APwAAAAAmZkA/wAAAAAREQD/AAAAAquVAAbk8QAHCBAAAPD0AAP5+wABRCoA+0BgAP0AAAAA\r\n"
+ // The missing '\n' on the last line of chunk X must be added back and
+ // the line consisting only of "\n" in chunk X+1 must be ignored in
+ // order to produce the the correct output. This is needed to insure
+ // that the signature verification of cryptographically signed emails
+ // does not fail due to missing or extra EOL characters. Otherwise, the
+ // extra or missing \n or \r doesn't really matter.
+ //
+ // Special case observed only with the "fake" imap server used with TB
+ // unit test. When the "\r\n" at the end of a chunk is split as
+ // described above, the \n at the beginning of the next chunk may
+ // actually be "\r\n" like this example: Last lines of chunk X
+ // "/gAOC/wA/QAAAAAAAAAA8wACCvz+AgIEAAD8/P4ABQUAAPoAAAD+AAEA/voHAAQGBQD/BAQA\r"
+ // ")\r\n"
+ // and the first two lines of chunk X+1:
+ // "\r\n" <-- The code changes this to just "\n" like it should be.
+ // "APwAAAAAmZkA/wAAAAAREQD/AAAAAquVAAbk8QAHCBAAAPD0AAP5+wABRCoA+0BgAP0AAAAA\r\n"
+ //
+ // Implementation:
+ // Obtain pointer to last literal in chunk X, e.g., 'C' in 1st example
+ // above, or to the \n or \r in the other examples.
+ char* displayEndOfLine =
+ (fCurrentLine + strlen(fCurrentLine) -
+ (charsReadSoFar - numberOfCharsInThisChunk + 1));
+ // Save so original unmodified fCurrentLine is restored below.
+ char saveit1 = displayEndOfLine[1];
+ char saveit2 = 0; // Keep compiler happy.
+ // Determine if EOL is split such that Chunk X has the \r and chunk
+ // X+1 has the \n.
+ fNextChunkStartsWithNewline = (displayEndOfLine[0] == '\r');
+ if (fNextChunkStartsWithNewline) {
+ saveit2 = displayEndOfLine[2];
+ // Add the missing newline and terminate the string.
+ displayEndOfLine[1] = '\n';
+ displayEndOfLine[2] = 0;
+ // This is a good thing to log.
+ MOZ_LOG(IMAP, mozilla::LogLevel::Info,
+ ("PARSER: CR/LF split at chunk boundary"));
+ } else {
+ // Typical case where EOLs are not split. Terminate the string.
+ displayEndOfLine[1] = 0;
+ }
+ // Process this modified string pointed to by fCurrentLine.
+ fServerConnection.HandleMessageDownLoadLine(fCurrentLine, !lastChunk);
+ // Restore fCurrentLine's original content.
+ displayEndOfLine[1] = saveit1;
+ if (fNextChunkStartsWithNewline) displayEndOfLine[2] = saveit2;
+ } else {
+ // Not the last line of a chunk.
+ bool processTheLine = true;
+ if (fNextChunkStartsWithNewline && origin > 0) {
+ // A split of the \r\n between chunks was detected. Ignore orphan \n
+ // on line by itself which can occur on the first line of a 2nd or
+ // later chunk. Line length should be 1 and the only character should
+ // be \n. Note: If previous message ended with just \r, don't expect
+ // the first chunk of a message (origin == 0) to begin with \n.
+ // (Typically, there is only one chunk required for a message or
+ // header response unless its size exceeds the chunking threshold.)
+ if (strlen(fCurrentLine) > 1 || fCurrentLine[0] != '\n') {
+ // In case expected orphan \n is not really there, go ahead and
+ // process the line. This should theoretically not occur but rarely,
+ // and for yet to be determined reasons, it does. Logging may help.
+ NS_WARNING(
+ "'\\n' is not the only character in this line as expected!");
+ MOZ_LOG(IMAP, mozilla::LogLevel::Debug,
+ ("PARSER: expecting just '\\n' but line is = |%s|",
+ fCurrentLine));
+ } else {
+ // Discard the line containing only \n.
+ processTheLine = false;
+ MOZ_LOG(IMAP, mozilla::LogLevel::Debug,
+ ("PARSER: discarding lone '\\n'"));
+ }
+ }
+ if (processTheLine) {
+ fServerConnection.HandleMessageDownLoadLine(
+ fCurrentLine,
+ !lastChunk && (charsReadSoFar == numberOfCharsInThisChunk),
+ fCurrentLine);
+ }
+ fNextChunkStartsWithNewline = false;
+ }
+ }
+ }
+
+ if (ContinueParse()) {
+ if (charsReadSoFar > numberOfCharsInThisChunk) {
+ // move the lexical analyzer state to the end of this message because this
+ // message fetch ends in the middle of this line.
+ AdvanceTokenizerStartingPoint(
+ strlen(fCurrentLine) - (charsReadSoFar - numberOfCharsInThisChunk));
+ AdvanceToNextToken();
+ } else {
+ skip_to_CRLF();
+ AdvanceToNextToken();
+ }
+ } else {
+ // Don't typically (maybe never?) see this.
+ fNextChunkStartsWithNewline = false;
+ }
+ return lastChunk;
+}
+
+bool nsImapServerResponseParser::CurrentFolderReadOnly() {
+ return fCurrentFolderReadOnly;
+}
+
+int32_t nsImapServerResponseParser::NumberOfMessages() {
+ return fNumberOfExistingMessages;
+}
+
+int32_t nsImapServerResponseParser::NumberOfRecentMessages() {
+ return fNumberOfRecentMessages;
+}
+
+int32_t nsImapServerResponseParser::FolderUID() { return fFolderUIDValidity; }
+
+void nsImapServerResponseParser::SetCurrentResponseUID(uint32_t uid) {
+ if (uid > 0) fCurrentResponseUID = uid;
+}
+
+uint32_t nsImapServerResponseParser::CurrentResponseUID() {
+ return fCurrentResponseUID;
+}
+
+uint32_t nsImapServerResponseParser::HighestRecordedUID() {
+ return fHighestRecordedUID;
+}
+
+void nsImapServerResponseParser::ResetHighestRecordedUID() {
+ fHighestRecordedUID = 0;
+}
+
+bool nsImapServerResponseParser::IsNumericString(const char* string) {
+ int i;
+ for (i = 0; i < (int)PL_strlen(string); i++) {
+ if (!isdigit(string[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Capture the mailbox state for folder select/update and for status.
+// If mailboxName is null, we've done imap SELECT; otherwise STATUS.
+already_AddRefed<nsImapMailboxSpec>
+nsImapServerResponseParser::CreateCurrentMailboxSpec(
+ const char* mailboxName /* = nullptr */) {
+ RefPtr<nsImapMailboxSpec> returnSpec = new nsImapMailboxSpec;
+ const char* mailboxNameToConvert =
+ (mailboxName) ? mailboxName : fSelectedMailboxName;
+ if (mailboxNameToConvert) {
+ const char* serverKey = fServerConnection.GetImapServerKey();
+ nsImapNamespace* ns = nullptr;
+ if (serverKey && fHostSessionList)
+ fHostSessionList->GetNamespaceForMailboxForHost(
+ serverKey, mailboxNameToConvert, ns); // for
+ // delimiter
+ returnSpec->mHierarchySeparator = (ns) ? ns->GetDelimiter() : '/';
+ }
+
+ returnSpec->mFolderSelected = !mailboxName;
+ returnSpec->mFolder_UIDVALIDITY = fFolderUIDValidity;
+ returnSpec->mHighestModSeq = fHighestModSeq;
+ // clang-format off
+ returnSpec->mNumOfMessages =
+ (mailboxName) ? fStatusExistingMessages : fNumberOfExistingMessages;
+ returnSpec->mNumOfUnseenMessages =
+ (mailboxName) ? fStatusUnseenMessages : -1;
+ returnSpec->mNumOfRecentMessages =
+ (mailboxName) ? fStatusRecentMessages : fNumberOfRecentMessages;
+ returnSpec->mNextUID =
+ (mailboxName) ? fStatusNextUID : fNextUID;
+ // clang-format on
+
+ returnSpec->mSupportedUserFlags = fSupportsUserDefinedFlags;
+
+ returnSpec->mBoxFlags = kNoFlags; // stub
+ returnSpec->mOnlineVerified = false; // Fabricated. Flags aren't verified.
+ returnSpec->mAllocatedPathName.Assign(mailboxNameToConvert);
+ returnSpec->mConnection = &fServerConnection;
+ if (returnSpec->mConnection) {
+ nsIURI* aUrl = nullptr;
+ nsresult rv = NS_OK;
+ returnSpec->mConnection->GetCurrentUrl()->QueryInterface(NS_GET_IID(nsIURI),
+ (void**)&aUrl);
+ if (NS_SUCCEEDED(rv) && aUrl) aUrl->GetHost(returnSpec->mHostName);
+
+ NS_IF_RELEASE(aUrl);
+ } else
+ returnSpec->mHostName.Truncate();
+
+ if (fFlagState)
+ returnSpec->mFlagState = fFlagState; // copies flag state
+ else
+ returnSpec->mFlagState = nullptr;
+
+ return returnSpec.forget();
+}
+// Reset the flag state.
+void nsImapServerResponseParser::ResetFlagInfo() {
+ if (fFlagState) fFlagState->Reset();
+}
+
+bool nsImapServerResponseParser::GetLastFetchChunkReceived() {
+ return fLastChunk;
+}
+
+void nsImapServerResponseParser::ClearLastFetchChunkReceived() {
+ fLastChunk = false;
+}
+
+int32_t nsImapServerResponseParser::GetNumBytesFetched() {
+ return numberOfCharsInThisChunk;
+}
+
+void nsImapServerResponseParser::ClearNumBytesFetched() {
+ numberOfCharsInThisChunk = 0;
+}
+
+void nsImapServerResponseParser::SetHostSessionList(
+ nsIImapHostSessionList* aHostSessionList) {
+ fHostSessionList = aHostSessionList;
+}
+
+void nsImapServerResponseParser::SetSyntaxError(bool error, const char* msg) {
+ nsImapGenericParser::SetSyntaxError(error, msg);
+ if (error) {
+ if (!fCurrentLine) {
+ HandleMemoryFailure();
+ fServerConnection.Log("PARSER", ("Internal Syntax Error: %s: <no line>"),
+ msg);
+ } else {
+ if (!strcmp(fCurrentLine, CRLF))
+ fServerConnection.Log("PARSER", "Internal Syntax Error: %s: <CRLF>",
+ msg);
+ else {
+ if (msg)
+ fServerConnection.Log("PARSER", "Internal Syntax Error: %s:", msg);
+ fServerConnection.Log("PARSER", "Internal Syntax Error on line: %s",
+ fCurrentLine);
+ }
+ }
+ }
+}
+
+nsresult nsImapServerResponseParser::BeginMessageDownload(
+ const char* content_type) {
+ nsresult rv = fServerConnection.BeginMessageDownLoad(fSizeOfMostRecentMessage,
+ content_type);
+ if (NS_FAILED(rv)) {
+ skip_to_CRLF();
+ fServerConnection.PseudoInterrupt(true);
+ fServerConnection.AbortMessageDownLoad();
+ }
+ return rv;
+}
diff --git a/comm/mailnews/imap/src/nsImapServerResponseParser.h b/comm/mailnews/imap/src/nsImapServerResponseParser.h
new file mode 100644
index 0000000000..43d6a73611
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapServerResponseParser.h
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsIMAPServerResponseParser_H_
+#define _nsIMAPServerResponseParser_H_
+
+#include "mozilla/Attributes.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsImapSearchResults.h"
+#include "nsString.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsImapUtils.h"
+
+class nsImapSearchResultIterator;
+class nsIImapFlagAndUidState;
+
+#include "nsImapGenericParser.h"
+
+class nsImapServerResponseParser : public nsImapGenericParser {
+ public:
+ explicit nsImapServerResponseParser(nsImapProtocol& imapConnection);
+ virtual ~nsImapServerResponseParser();
+
+ // Overridden from the base parser class
+ virtual bool LastCommandSuccessful() override;
+ virtual void HandleMemoryFailure() override;
+
+ // aignoreBadAndNOResponses --> don't throw a error dialog if this command
+ // results in a NO or Bad response from the server..in other words the command
+ // is "exploratory" and we don't really care if it succeeds or fails. This
+ // value is typically FALSE for almost all cases.
+ virtual void ParseIMAPServerResponse(const char* aCurrentCommand,
+ bool aIgnoreBadAndNOResponses,
+ char* aGreetingWithCapability = NULL);
+ virtual void InitializeState();
+ bool CommandFailed();
+ void SetCommandFailed(bool failed);
+ bool UntaggedResponse();
+
+ enum eIMAPstate { kNonAuthenticated, kAuthenticated, kFolderSelected };
+
+ virtual eIMAPstate GetIMAPstate();
+ virtual bool WaitingForMoreClientInput() {
+ return fWaitingForMoreClientInput;
+ }
+ const char* GetSelectedMailboxName(); // can be NULL
+ bool IsStdJunkNotJunkUseOk() { return fStdJunkNotJunkUseOk; }
+
+ // if we get a PREAUTH greeting from the server, initialize the parser to
+ // begin in the kAuthenticated state
+ void PreauthSetAuthenticatedState();
+
+ // these functions represent the state of the currently selected
+ // folder
+ bool CurrentFolderReadOnly();
+ int32_t NumberOfMessages();
+ int32_t NumberOfRecentMessages();
+ int32_t FolderUID();
+ uint32_t CurrentResponseUID();
+ uint32_t HighestRecordedUID();
+ void ResetHighestRecordedUID();
+ void SetCurrentResponseUID(uint32_t uid);
+ bool IsNumericString(const char* string);
+ uint32_t SizeOfMostRecentMessage();
+ void SetTotalDownloadSize(int32_t newSize) { fTotalDownloadSize = newSize; }
+
+ nsImapSearchResultIterator* CreateSearchResultIterator();
+ void ResetSearchResultSequence() { fSearchResults->ResetSequence(); }
+
+ // create a struct mailbox_spec from our info, used in
+ // libmsg c interface
+ already_AddRefed<nsImapMailboxSpec> CreateCurrentMailboxSpec(
+ const char* mailboxName = nullptr);
+
+ // Resets the flags state.
+ void ResetFlagInfo();
+
+ // set this to false if you don't want to alert the user to server
+ // error messages
+ void SetReportingErrors(bool reportThem) { fReportingErrors = reportThem; }
+ bool GetReportingErrors() { return fReportingErrors; }
+
+ eIMAPCapabilityFlags GetCapabilityFlag() { return fCapabilityFlag; }
+ void SetCapabilityFlag(eIMAPCapabilityFlags capability) {
+ fCapabilityFlag = capability;
+ }
+ bool ServerHasIMAP4Rev1Capability() {
+ return ((fCapabilityFlag & kIMAP4rev1Capability) != 0);
+ }
+ bool ServerHasACLCapability() {
+ return ((fCapabilityFlag & kACLCapability) != 0);
+ }
+ bool ServerHasNamespaceCapability() {
+ return ((fCapabilityFlag & kNamespaceCapability) != 0);
+ }
+ bool ServerIsNetscape3xServer() { return fServerIsNetscape3xServer; }
+ bool ServerHasServerInfo() {
+ return ((fCapabilityFlag & kXServerInfoCapability) != 0);
+ }
+ bool ServerIsAOLServer() {
+ return ((fCapabilityFlag & kAOLImapCapability) != 0);
+ }
+ void SetFetchingFlags(bool aFetchFlags) { fFetchingAllFlags = aFetchFlags; }
+ void ResetCapabilityFlag();
+
+ nsCString& GetMailAccountUrl() { return fMailAccountUrl; }
+ const char* GetXSenderInfo() { return fXSenderInfo; }
+ void FreeXSenderInfo() { PR_FREEIF(fXSenderInfo); }
+ nsCString& GetManageListsUrl() { return fManageListsUrl; }
+ nsCString& GetManageFiltersUrl() { return fManageFiltersUrl; }
+ const char* GetManageFolderUrl() { return fFolderAdminUrl; }
+ nsCString& GetServerID() { return fServerIdResponse; }
+
+ // Call this when adding a pipelined command to the session
+ void IncrementNumberOfTaggedResponsesExpected(const char* newExpectedTag);
+
+ // Interrupt a Fetch, without really Interrupting (through netlib)
+ bool GetLastFetchChunkReceived();
+ void ClearLastFetchChunkReceived();
+ int32_t GetNumBytesFetched();
+ void ClearNumBytesFetched();
+ virtual uint16_t SupportsUserFlags() { return fSupportsUserDefinedFlags; }
+ virtual uint16_t SettablePermanentFlags() { return fSettablePermanentFlags; }
+ void SetFlagState(nsIImapFlagAndUidState* state);
+ bool GetDownloadingHeaders();
+ void SetHostSessionList(nsIImapHostSessionList* aHostSession);
+ char* fAuthChallenge; // the challenge returned by the server in
+ // response to authenticate using CRAM-MD5 or NTLM
+ bool fUtf8AcceptEnabled;
+ bool fUseModSeq; // can use mod seq for currently selected folder
+ uint64_t fHighestModSeq;
+
+ protected:
+ virtual void flags();
+ virtual void envelope_data();
+ virtual void xaolenvelope_data();
+ virtual void parse_address(nsAutoCString& addressLine);
+ virtual void internal_date();
+ virtual nsresult BeginMessageDownload(const char* content_type);
+
+ virtual void response_data();
+ virtual void resp_text();
+ virtual void resp_cond_state(bool isTagged);
+ virtual void text_mime2();
+ virtual void text();
+ virtual void parse_folder_flags(bool calledForFlags);
+ virtual void enable_data();
+ virtual void language_data();
+ virtual void authChallengeResponse_data();
+ virtual void resp_text_code();
+ virtual void response_done();
+ virtual void response_tagged();
+ virtual void response_fatal();
+ virtual void resp_cond_bye();
+ virtual void id_data();
+ virtual void mailbox_data();
+ virtual void numeric_mailbox_data();
+ virtual void capability_data();
+ virtual void xserverinfo_data();
+ virtual void xmailboxinfo_data();
+ virtual void namespace_data();
+ virtual void myrights_data(bool unsolicited);
+ virtual void acl_data();
+ virtual void mime_part_data();
+ virtual void quota_data();
+ virtual void msg_fetch();
+ virtual void msg_obsolete();
+ virtual void msg_fetch_headers(const char* partNum);
+ virtual void msg_fetch_content(bool chunk, int32_t origin,
+ const char* content_type);
+ virtual bool msg_fetch_quoted();
+ virtual bool msg_fetch_literal(bool chunk, int32_t origin);
+ virtual void mailbox_list(bool discoveredFromLsub);
+ virtual void mailbox(nsImapMailboxSpec* boxSpec);
+
+ virtual void ProcessOkCommand(const char* commandToken);
+ virtual void ProcessBadCommand(const char* commandToken);
+ virtual void PreProcessCommandToken(const char* commandToken,
+ const char* currentCommand);
+ virtual void PostProcessEndOfLine();
+
+ // Overridden from the nsImapGenericParser, to retrieve the next line
+ // from the open socket.
+ virtual bool GetNextLineForParser(char** nextLine) override;
+ // overridden to do logging
+ virtual void SetSyntaxError(bool error, const char* msg = nullptr) override;
+
+ private:
+ bool fCurrentCommandFailed;
+ bool fUntaggedResponse;
+ bool fReportingErrors;
+
+ bool fCurrentFolderReadOnly;
+ bool fCurrentLineContainedFlagInfo;
+ bool fFetchingAllFlags;
+ bool fWaitingForMoreClientInput;
+ // Is the server a Netscape 3.x Messaging Server?
+ bool fServerIsNetscape3xServer;
+ bool fDownloadingHeaders;
+ bool fCurrentCommandIsSingleMessageFetch;
+ bool fGotPermanentFlags;
+ bool fStdJunkNotJunkUseOk;
+ imapMessageFlagsType fSavedFlagInfo;
+ nsTArray<nsCString> fCustomFlags;
+
+ uint16_t fSupportsUserDefinedFlags;
+ uint16_t fSettablePermanentFlags;
+
+ int32_t fFolderUIDValidity;
+ int32_t fSeqNumOfFirstUnseenMsg;
+ int32_t fNumberOfExistingMessages;
+ int32_t fNumberOfRecentMessages;
+ uint32_t fCurrentResponseUID;
+ uint32_t fHighestRecordedUID;
+ // used to handle server that sends msg size after headers
+ uint32_t fReceivedHeaderOrSizeForUID;
+ int32_t fSizeOfMostRecentMessage;
+ int32_t fTotalDownloadSize;
+
+ int32_t fStatusUnseenMessages;
+ int32_t fStatusRecentMessages;
+ uint32_t fStatusNextUID;
+ int32_t fStatusExistingMessages;
+ uint32_t fNextUID;
+
+ int fNumberOfTaggedResponsesExpected;
+
+ char* fCurrentCommandTag;
+
+ nsCString fZeroLengthMessageUidString;
+
+ char* fSelectedMailboxName;
+
+ nsImapSearchResultSequence* fSearchResults;
+
+ nsCOMPtr<nsIImapFlagAndUidState>
+ fFlagState; // NOT owned by us, it's a copy, do not destroy
+
+ eIMAPstate fIMAPstate;
+
+ eIMAPCapabilityFlags fCapabilityFlag;
+ nsCString fMailAccountUrl;
+ char* fNetscapeServerVersionString;
+ char* fXSenderInfo; /* changed per message download */
+ char* fLastAlert; /* used to avoid displaying the same alert over and over */
+ char* fMsgID; /* MessageID for Gmail only (X-GM-MSGID) */
+ char* fThreadID; /* ThreadID for Gmail only (X-GM-THRID) */
+ char* fLabels; /* Labels for Gmail only (X-GM-LABELS) [will include parens,
+ removed while passing to hashTable ]*/
+ nsCString fManageListsUrl;
+ nsCString fManageFiltersUrl;
+ char* fFolderAdminUrl;
+ nsCString fServerIdResponse; // RFC
+
+ int32_t fFetchResponseIndex;
+
+ // used for aborting a fetch stream when we're pseudo-Interrupted
+ int32_t numberOfCharsInThisChunk;
+ int32_t charsReadSoFar;
+ bool fLastChunk;
+
+ // Flags split of \r and \n between chunks in msg_fetch_literal().
+ bool fNextChunkStartsWithNewline;
+
+ // The connection object
+ nsImapProtocol& fServerConnection;
+
+ RefPtr<nsIImapHostSessionList> fHostSessionList;
+ nsTArray<nsMsgKey> fCopyResponseKeyArray;
+};
+
+#endif
diff --git a/comm/mailnews/imap/src/nsImapService.cpp b/comm/mailnews/imap/src/nsImapService.cpp
new file mode 100644
index 0000000000..8c02d02dcd
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapService.cpp
@@ -0,0 +1,3091 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsImapService.h"
+#include "nsImapCore.h"
+#include "netCore.h"
+
+#include "nsImapUrl.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapMessageSink.h"
+#include "nsIImapServerSink.h"
+#include "nsIImapMockChannel.h"
+#include "nsImapUtils.h"
+#include "nsImapNamespace.h"
+#include "nsIDocShell.h"
+#include "nsIProgressEventSink.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsILoadGroup.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsIWebNavigation.h"
+#include "nsImapStringBundle.h"
+#include "plbase64.h"
+#include "nsImapOfflineSync.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgUtils.h"
+#include "nsICacheStorage.h"
+#include "nsICacheStorageService.h"
+#include "nsIStreamListener.h"
+#include "nsIUrlListener.h"
+#include "nsNetCID.h"
+#include "nsMsgI18N.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsMsgLineBuffer.h"
+#include "nsIMsgParseMailMsgState.h"
+#include "nsIOutputStream.h"
+#include "nsIDocShell.h"
+#include "nsIMessengerWindowService.h"
+#include "nsIWindowMediator.h"
+#include "nsIPrompt.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgMailSession.h"
+#include "nsIStreamConverterService.h"
+#include "nsIAutoSyncManager.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgPluggableStore.h"
+#include "../../base/src/MailnewsLoadContextInfo.h"
+#include "nsDocShellLoadState.h"
+#include "nsContentUtils.h"
+#include "mozilla/LoadInfo.h"
+
+#define PREF_MAIL_ROOT_IMAP_REL "mail.root.imap-rel"
+// old - for backward compatibility only
+#define PREF_MAIL_ROOT_IMAP "mail.root.imap"
+
+#define NS_IMAPURL_CID \
+ { \
+ 0x21a89611, 0xdc0d, 0x11d2, { \
+ 0x80, 0x6c, 0x0, 0x60, 0x8, 0x12, 0x8c, 0x4e \
+ } \
+ }
+static NS_DEFINE_CID(kImapUrlCID, NS_IMAPURL_CID);
+
+#define NS_IMAPMOCKCHANNEL_CID \
+ { \
+ 0x4eca51df, 0x6734, 0x11d3, { \
+ 0x98, 0x9a, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b \
+ } \
+ }
+static NS_DEFINE_CID(kCImapMockChannel, NS_IMAPMOCKCHANNEL_CID);
+
+static const char sequenceString[] = "SEQUENCE";
+static const char uidString[] = "UID";
+
+static bool gInitialized = false;
+
+NS_IMPL_ISUPPORTS(nsImapService, nsIImapService, nsIMsgMessageService,
+ nsIProtocolHandler, nsIMsgProtocolInfo,
+ nsIMsgMessageFetchPartService, nsIContentHandler)
+
+nsImapService::nsImapService() {
+ if (!gInitialized) {
+ nsresult rv;
+
+ nsCOMPtr<nsIIOService> ioServ = do_GetIOService();
+ ioServ->RegisterProtocolHandler(
+ "imap"_ns, this,
+ nsIProtocolHandler::URI_NORELATIVE |
+ nsIProtocolHandler::URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ nsIProtocolHandler::URI_DANGEROUS_TO_LOAD |
+ nsIProtocolHandler::ALLOWS_PROXY |
+ nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS |
+ nsIProtocolHandler::ORIGIN_IS_FULL_SPEC,
+ nsIImapUrl::DEFAULT_IMAP_PORT);
+
+ // initialize auto-sync service
+ nsCOMPtr<nsIAutoSyncManager> autoSyncMgr =
+ do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && autoSyncMgr) {
+ // auto-sync manager initialization goes here
+ // assign new strategy objects here...
+ }
+ NS_ASSERTION(autoSyncMgr != nullptr,
+ "*** Cannot initialize nsAutoSyncManager service.");
+
+ gInitialized = true;
+ }
+}
+
+nsImapService::~nsImapService() {}
+
+char nsImapService::GetHierarchyDelimiter(nsIMsgFolder* aMsgFolder) {
+ char delimiter = '/';
+ if (aMsgFolder) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aMsgFolder);
+ if (imapFolder) imapFolder->GetHierarchyDelimiter(&delimiter);
+ }
+ return delimiter;
+}
+
+// N.B., this returns an escaped folder name, appropriate for putting in a url.
+nsresult nsImapService::GetFolderName(nsIMsgFolder* aImapFolder,
+ nsACString& aFolderName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgImapMailFolder> aFolder(do_QueryInterface(aImapFolder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString onlineName;
+ // Online name is in MUTF-7 or UTF-8.
+ rv = aFolder->GetOnlineName(onlineName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (onlineName.IsEmpty()) {
+ nsCString uri;
+ rv = aImapFolder->GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString hostname;
+ rv = aImapFolder->GetHostname(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsImapURI2FullName(kImapRootURI, hostname.get(), uri.get(),
+ getter_Copies(onlineName));
+ }
+ // if the hierarchy delimiter is not '/', then we want to escape slashes;
+ // otherwise, we do want to escape slashes.
+ // we want to escape slashes and '^' first, otherwise, nsEscape will lose them
+ bool escapeSlashes = (GetHierarchyDelimiter(aImapFolder) != '/');
+ if (escapeSlashes && !onlineName.IsEmpty()) {
+ char* escapedOnlineName;
+ rv = nsImapUrl::EscapeSlashes(onlineName.get(), &escapedOnlineName);
+ if (NS_SUCCEEDED(rv)) onlineName.Adopt(escapedOnlineName);
+ }
+ // need to escape everything else
+ MsgEscapeString(onlineName, nsINetUtil::ESCAPE_URL_PATH, aFolderName);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::SelectFolder(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE;
+
+ bool canOpenThisFolder = true;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(aImapMailFolder);
+ if (imapFolder) imapFolder->GetCanOpenFolder(&canOpenThisFolder);
+
+ if (!canOpenThisFolder) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ // nsImapUrl::SetSpec() will set the imap action properly
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapSelectFolder);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ // if no msg window, we won't put up error messages (this is almost
+ // certainly a biff-inspired get new msgs)
+ if (!aMsgWindow) mailNewsUrl->SetSuppressErrorMsgs(true);
+ mailNewsUrl->SetMsgWindow(aMsgWindow);
+ mailNewsUrl->SetUpdatingFolder(true);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.AppendLiteral("/select>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ } // if we have a url to run....
+
+ return rv;
+}
+
+// lite select, used to verify UIDVALIDITY while going on/offline
+NS_IMETHODIMP nsImapService::LiteSelectFolder(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/liteselect>",
+ nsIImapUrl::nsImapLiteSelectFolder, aMsgWindow, aURL);
+}
+
+NS_IMETHODIMP nsImapService::GetUrlForUri(const nsACString& aMessageURI,
+ nsIMsgWindow* aMsgWindow,
+ nsIURI** aURL) {
+ nsAutoCString messageURI(aMessageURI);
+
+ if (messageURI.Find("&type=application/x-message-display"_ns) != kNotFound)
+ return NS_NewURI(aURL, aMessageURI);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString msgKey;
+ nsresult rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(messageURI, getter_AddRefs(imapUrl), folder,
+ nullptr, urlSpec, hierarchyDelimiter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetImapUrlSink(folder, imapUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl);
+ bool useLocalCache = false;
+ folder->HasMsgOffline(strtoul(msgKey.get(), nullptr, 10), &useLocalCache);
+ mailnewsUrl->SetMsgIsInLocalCache(useLocalCache);
+
+ nsCOMPtr<nsIURI> url = do_QueryInterface(imapUrl);
+ rv = url->GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ urlSpec.AppendLiteral("fetch>UID>");
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsAutoCString folderName;
+ GetFolderName(folder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(msgKey);
+ rv = mailnewsUrl->SetSpecInternal(urlSpec);
+ imapUrl->QueryInterface(NS_GET_IID(nsIURI), (void**)aURL);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::FetchMimePart(
+ nsIURI* aURI, const nsACString& aMessageURI, nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener, nsIURI** aURL) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString messageURI(aMessageURI);
+ nsAutoCString msgKey;
+ nsAutoCString mimePart;
+ nsAutoCString folderURI;
+ nsMsgKey key;
+
+ nsresult rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsParseImapMessageURI(aMessageURI, folderURI, &key,
+ getter_Copies(mimePart));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(
+ do_QueryInterface(folder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aURI);
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(aURI, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgurl->SetMsgWindow(aMsgWindow);
+ msgurl->RegisterListener(aUrlListener);
+
+ if (!mimePart.IsEmpty()) {
+ return FetchMimePart(imapUrl, nsIImapUrl::nsImapMsgFetch, folder,
+ imapMessageSink, aURL, aDisplayConsumer, msgKey,
+ mimePart);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::LoadMessage(const nsACString& aMessageURI,
+ nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ bool aAutodetectCharset) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString msgKey;
+ nsAutoCString mimePart;
+ nsAutoCString folderURI;
+ nsMsgKey key;
+ nsAutoCString messageURI(aMessageURI);
+
+ int32_t typeIndex = messageURI.Find("&type=application/x-message-display");
+ if (typeIndex != kNotFound) {
+ // This happens with forward inline of a message/rfc822 attachment opened in
+ // a standalone msg window.
+ // So, just cut to the chase and call AsyncOpen on a channel.
+ nsCOMPtr<nsIURI> uri;
+ messageURI.Cut(typeIndex,
+ sizeof("&type=application/x-message-display") - 1);
+ rv = NS_NewURI(getter_AddRefs(uri), messageURI.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIStreamListener> aStreamListener =
+ do_QueryInterface(aDisplayConsumer, &rv);
+ if (NS_SUCCEEDED(rv) && aStreamListener) {
+ nsCOMPtr<nsIChannel> aChannel;
+ nsCOMPtr<nsILoadGroup> aLoadGroup;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(uri, &rv);
+ if (NS_SUCCEEDED(rv) && mailnewsUrl)
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ rv = NewChannel(uri, loadInfo, getter_AddRefs(aChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now try to open the channel passing in our display consumer as the
+ // listener
+ rv = aChannel->AsyncOpen(aStreamListener);
+ return rv;
+ }
+ }
+
+ rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ rv = nsParseImapMessageURI(aMessageURI, folderURI, &key,
+ getter_Copies(mimePart));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(
+ do_QueryInterface(folder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(messageURI, getter_AddRefs(imapUrl), folder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mimePart.IsEmpty()) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ rv = AddImapFetchToUrl(mailnewsurl, folder, msgKey + mimePart,
+ EmptyCString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> dummyURI;
+ return FetchMimePart(imapUrl, nsIImapUrl::nsImapMsgFetch, folder,
+ imapMessageSink, getter_AddRefs(dummyURI),
+ aDisplayConsumer, msgKey, mimePart);
+ }
+
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl));
+ nsCOMPtr<nsIMsgI18NUrl> i18nurl(do_QueryInterface(imapUrl));
+ i18nurl->SetAutodetectCharset(aAutodetectCharset);
+
+ bool shouldStoreMsgOffline = false;
+ bool hasMsgOffline = false;
+
+ msgurl->SetMsgWindow(aMsgWindow);
+
+ if (folder) {
+ folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline);
+ folder->HasMsgOffline(key, &hasMsgOffline);
+ }
+ imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
+
+ if (hasMsgOffline) msgurl->SetMsgIsInLocalCache(true);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ // Should the message fetch force a peek or a traditional fetch?
+ // Force peek if there is a delay in marking read (or no auto-marking at
+ // all). This is because a FETCH (BODY[]) will implicitly set the \Seen
+ // flag on the msg, but a FETCH (BODY.PEEK[]) won't.
+ bool forcePeek = false;
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ nsAutoCString uriStr(aMessageURI);
+ int32_t dontMarkAsReadPos = uriStr.Find("&markRead=false");
+ bool markReadAuto = true;
+ prefBranch->GetBoolPref("mailnews.mark_message_read.auto",
+ &markReadAuto);
+ bool markReadDelay = false;
+ prefBranch->GetBoolPref("mailnews.mark_message_read.delay",
+ &markReadDelay);
+ forcePeek = (!markReadAuto || markReadDelay ||
+ (dontMarkAsReadPos != kNotFound));
+ }
+
+ if (!forcePeek) {
+ // If we're loading a message in an inactive docShell, don't let it
+ // be marked as read immediately.
+ nsCOMPtr<nsIDocShell> docShell =
+ do_QueryInterface(aDisplayConsumer, &rv);
+ if (NS_SUCCEEDED(rv) && docShell) {
+ auto* bc = docShell->GetBrowsingContext();
+ forcePeek = !bc->IsActive();
+ }
+ }
+
+ nsCOMPtr<nsIURI> dummyURI;
+ rv = FetchMessage(imapUrl,
+ forcePeek ? nsIImapUrl::nsImapMsgFetchPeek
+ : nsIImapUrl::nsImapMsgFetch,
+ folder, imapMessageSink, aMsgWindow, aDisplayConsumer,
+ msgKey, false, getter_AddRefs(dummyURI));
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapService::FetchMimePart(
+ nsIImapUrl* aImapUrl, nsImapAction aImapAction,
+ nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage,
+ nsIURI** aURL, nsISupports* aDisplayConsumer,
+ const nsACString& messageIdentifierList, const nsACString& mimePart) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+ NS_ENSURE_ARG_POINTER(aImapMessage);
+
+ // create a protocol instance to handle the request.
+ // NOTE: once we start working with multiple connections, this step will be
+ // much more complicated...but for now just create a connection and process
+ // the request.
+ nsAutoCString urlSpec;
+ nsresult rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
+ nsImapAction actionToUse = aImapAction;
+ if (actionToUse == nsImapUrl::nsImapOpenMimePart)
+ actionToUse = nsIImapUrl::nsImapMsgFetch;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(aImapUrl));
+ if (aImapMailFolder && msgurl && !messageIdentifierList.IsEmpty()) {
+ bool useLocalCache = false;
+ aImapMailFolder->HasMsgOffline(
+ strtoul(PromiseFlatCString(messageIdentifierList).get(), nullptr, 10),
+ &useLocalCache);
+ msgurl->SetMsgIsInLocalCache(useLocalCache);
+ }
+ rv = aImapUrl->SetImapMessageSink(aImapMessage);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl);
+ if (aURL) NS_IF_ADDREF(*aURL = url);
+
+ rv = url->GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = msgurl->SetSpecInternal(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aImapUrl->SetImapAction(actionToUse /* nsIImapUrl::nsImapMsgFetch */);
+ if (aImapMailFolder && aDisplayConsumer) {
+ nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer;
+ rv = aImapMailFolder->GetServer(getter_AddRefs(aMsgIncomingServer));
+ if (NS_SUCCEEDED(rv) && aMsgIncomingServer) {
+ bool interrupted;
+ nsCOMPtr<nsIImapIncomingServer> aImapServer(
+ do_QueryInterface(aMsgIncomingServer, &rv));
+ if (NS_SUCCEEDED(rv) && aImapServer)
+ aImapServer->PseudoInterruptMsgLoad(aImapMailFolder, nullptr,
+ &interrupted);
+ }
+ }
+ // if the display consumer is a docshell, then we should run the url in the
+ // docshell. otherwise, it should be a stream listener....so open a channel
+ // using AsyncRead and the provided stream listener....
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aDisplayConsumer, &rv));
+ if (NS_SUCCEEDED(rv) && docShell) {
+ // DIRTY LITTLE HACK --> if we are opening an attachment we want the
+ // docshell to treat this load as if it were a user click event. Then the
+ // dispatching stuff will be much happier.
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url);
+ loadState->SetLoadFlags(aImapAction == nsImapUrl::nsImapOpenMimePart
+ ? nsIWebNavigation::LOAD_FLAGS_IS_LINK
+ : nsIWebNavigation::LOAD_FLAGS_NONE);
+ if (aImapAction == nsImapUrl::nsImapOpenMimePart)
+ loadState->SetLoadType(LOAD_LINK);
+ loadState->SetFirstParty(false);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ rv = docShell->LoadURI(loadState, false);
+ } else {
+ nsCOMPtr<nsIStreamListener> aStreamListener =
+ do_QueryInterface(aDisplayConsumer, &rv);
+ if (NS_SUCCEEDED(rv) && aStreamListener) {
+ nsCOMPtr<nsIChannel> aChannel;
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(aImapUrl, &rv);
+ if (NS_SUCCEEDED(rv) && mailnewsUrl)
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ rv = NewChannel(url, loadInfo, getter_AddRefs(aChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need a load group to hold onto the channel. When the request is
+ // finished, it'll get removed from the load group, and the channel will
+ // go away, which will free the load group.
+ if (!loadGroup) loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID);
+
+ aChannel->SetLoadGroup(loadGroup);
+
+ // now try to open the channel passing in our display consumer as the
+ // listener
+ rv = aChannel->AsyncOpen(aStreamListener);
+ } else // do what we used to do before
+ {
+ // I'd like to get rid of this code as I believe that we always get a
+ // docshell or stream listener passed into us in this method but i'm not
+ // sure yet... I'm going to use an assert for now to figure out if this
+ // is ever getting called
+#if defined(DEBUG_mscott) || defined(DEBUG_bienvenu)
+ NS_ERROR("oops...someone still is reaching this part of the code");
+#endif
+ rv = GetImapConnectionAndLoadUrl(aImapUrl, aDisplayConsumer, aURL);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::CopyMessage(const nsACString& aSrcMailboxURI,
+ nsIStreamListener* aMailboxCopy,
+ bool moveMessage,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMailboxCopy);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString msgKey;
+ rv = DecomposeImapURI(aSrcMailboxURI, getter_AddRefs(folder), msgKey);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(
+ do_QueryInterface(folder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ bool hasMsgOffline = false;
+ nsMsgKey key = strtoul(msgKey.get(), nullptr, 10);
+
+ rv = CreateStartOfImapUrl(aSrcMailboxURI, getter_AddRefs(imapUrl), folder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ if (folder) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl));
+ folder->HasMsgOffline(key, &hasMsgOffline);
+ if (msgurl) msgurl->SetMsgIsInLocalCache(hasMsgOffline);
+ }
+ // now try to download the message
+ nsImapAction imapAction = nsIImapUrl::nsImapOnlineToOfflineCopy;
+ if (moveMessage) imapAction = nsIImapUrl::nsImapOnlineToOfflineMove;
+ nsCOMPtr<nsIURI> dummyURI;
+ rv =
+ FetchMessage(imapUrl, imapAction, folder, imapMessageSink, aMsgWindow,
+ aMailboxCopy, msgKey, false, getter_AddRefs(dummyURI));
+ } // if we got an imap message sink
+ } // if we decomposed the imap message
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::CopyMessages(
+ const nsTArray<nsMsgKey>& aKeys, nsIMsgFolder* srcFolder,
+ nsIStreamListener* aMailboxCopy, bool moveMessage,
+ nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aMailboxCopy);
+ NS_ENSURE_TRUE(!aKeys.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = srcFolder;
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(do_QueryInterface(folder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ // we generate the uri for the first message so that way on down the line,
+ // GetMessage in nsCopyMessageStreamListener will get an unescaped
+ // username and be able to find the msg hdr. See bug 259656 for details
+ nsCString uri;
+ srcFolder->GenerateMessageURI(aKeys[0], uri);
+
+ nsCString messageIds;
+ // TODO: AllocateImapUidString() maxes out at 950 keys or so... it
+ // updates the numKeys passed in, but here the resulting value is
+ // ignored. Does this need sorting out?
+ uint32_t numKeys = aKeys.Length();
+ AllocateImapUidString(aKeys.Elements(), numKeys, nullptr, messageIds);
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(uri, getter_AddRefs(imapUrl), folder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ nsImapAction action;
+ if (moveMessage) // don't use ?: syntax here, it seems to break the Mac.
+ action = nsIImapUrl::nsImapOnlineToOfflineMove;
+ else
+ action = nsIImapUrl::nsImapOnlineToOfflineCopy;
+ imapUrl->SetCopyState(aMailboxCopy);
+ // now try to display the message
+ rv = FetchMessage(imapUrl, action, folder, imapMessageSink, aMsgWindow,
+ aMailboxCopy, messageIds, false, aURL);
+ // ### end of copy operation should know how to do the delete.if this is a
+ // move
+
+ } // if we got an imap message sink
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::Search(nsIMsgSearchSession* aSearchSession,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgFolder* aMsgFolder,
+ const nsACString& aSearchUri) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ nsresult rv;
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(aSearchSession, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(aMsgFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aMsgFolder,
+ urlListener, urlSpec, hierarchyDelimiter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl));
+
+ msgurl->SetMsgWindow(aMsgWindow);
+ msgurl->SetSearchSession(aSearchSession);
+ rv = SetImapUrlSink(aMsgFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString folderName;
+ GetFolderName(aMsgFolder, folderName);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ if (!aMsgWindow) mailNewsUrl->SetSuppressErrorMsgs(true);
+
+ urlSpec.AppendLiteral("/search>UID>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ // escape aSearchUri so that IMAP special characters (i.e. '\')
+ // won't be replaced with '/' in NECKO.
+ // it will be unescaped in nsImapUrl::ParseUrl().
+ nsCString escapedSearchUri;
+
+ MsgEscapeString(aSearchUri, nsINetUtil::ESCAPE_XALPHAS, escapedSearchUri);
+ urlSpec.Append(escapedSearchUri);
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ return rv;
+}
+
+// just a helper method to break down imap message URIs....
+nsresult nsImapService::DecomposeImapURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder,
+ nsACString& aMsgKey) {
+ nsMsgKey msgKey;
+ nsresult rv = DecomposeImapURI(aMessageURI, aFolder, &msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (msgKey) {
+ nsAutoCString messageIdString;
+ messageIdString.AppendInt(msgKey);
+ aMsgKey = messageIdString;
+ }
+
+ return rv;
+}
+
+// just a helper method to break down imap message URIs....
+nsresult nsImapService::DecomposeImapURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder,
+ nsMsgKey* aMsgKey) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aMsgKey);
+
+ nsAutoCString folderURI;
+ nsresult rv = nsParseImapMessageURI(aMessageURI, folderURI, aMsgKey, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::SaveMessageToDisk(
+ const nsACString& aMessageURI, nsIFile* aFile, bool aAddDummyEnvelope,
+ nsIUrlListener* aUrlListener, nsIURI** aURL, bool canonicalLineEnding,
+ nsIMsgWindow* aMsgWindow) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString msgKey;
+
+ nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMsgOffline = false;
+
+ if (folder)
+ folder->HasMsgOffline(strtoul(msgKey.get(), nullptr, 10), &hasMsgOffline);
+
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(aMessageURI, getter_AddRefs(imapUrl), folder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(
+ do_QueryInterface(folder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(imapUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgUrl->SetMessageFile(aFile);
+ msgUrl->SetAddDummyEnvelope(aAddDummyEnvelope);
+ msgUrl->SetCanonicalLineEnding(canonicalLineEnding);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(msgUrl);
+ if (mailnewsUrl) mailnewsUrl->SetMsgIsInLocalCache(hasMsgOffline);
+
+ nsCOMPtr<nsIStreamListener> saveAsListener;
+ mailnewsUrl->GetSaveAsListener(aAddDummyEnvelope, aFile,
+ getter_AddRefs(saveAsListener));
+
+ return FetchMessage(imapUrl, nsIImapUrl::nsImapSaveMessageToDisk, folder,
+ imapMessageSink, aMsgWindow, saveAsListener, msgKey,
+ false, aURL);
+ }
+ return rv;
+}
+
+/* fetching RFC822 messages */
+/* imap4://HOST>fetch><UID>>MAILBOXPATH>x */
+/* 'x' is the message UID */
+/* will set the 'SEEN' flag */
+NS_IMETHODIMP nsImapService::AddImapFetchToUrl(
+ nsIMsgMailNewsUrl* aUrl, nsIMsgFolder* aImapMailFolder,
+ const nsACString& aMessageIdentifierList,
+ const nsACString& aAdditionalHeader) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+
+ nsAutoCString urlSpec;
+ nsresult rv = aUrl->GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+
+ urlSpec.AppendLiteral("fetch>UID>");
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsAutoCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+
+ urlSpec.Append('>');
+ urlSpec.Append(aMessageIdentifierList);
+
+ if (!aAdditionalHeader.IsEmpty()) {
+ urlSpec.AppendLiteral("?header=");
+ urlSpec.Append(aAdditionalHeader);
+ }
+
+ return aUrl->SetSpecInternal(urlSpec);
+}
+
+NS_IMETHODIMP nsImapService::FetchMessage(
+ nsIImapUrl* aImapUrl, nsImapAction aImapAction,
+ nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage,
+ nsIMsgWindow* aMsgWindow, nsISupports* aDisplayConsumer,
+ const nsACString& messageIdentifierList, bool aConvertDataToText,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+ NS_ENSURE_ARG_POINTER(aImapMessage);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl);
+
+ rv = AddImapFetchToUrl(mailnewsurl, aImapMailFolder, messageIdentifierList,
+ ""_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (WeAreOffline()) {
+ bool msgIsInCache = false;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(aImapUrl));
+ msgUrl->GetMsgIsInLocalCache(&msgIsInCache);
+ if (!msgIsInCache)
+ IsMsgInMemCache(mailnewsurl, aImapMailFolder, &msgIsInCache);
+
+ // Display the "offline" message if we didn't find it in the memory cache
+ // either
+ if (!msgIsInCache) {
+ return NS_ERROR_OFFLINE;
+ }
+ }
+
+ if (aURL) mailnewsurl.forget(aURL);
+
+ return GetMessageFromUrl(aImapUrl, aImapAction, aImapMailFolder, aImapMessage,
+ aMsgWindow, aDisplayConsumer, aConvertDataToText,
+ aURL);
+}
+
+nsresult nsImapService::GetMessageFromUrl(
+ nsIImapUrl* aImapUrl, nsImapAction aImapAction,
+ nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage,
+ nsIMsgWindow* aMsgWindow, nsISupports* aDisplayConsumer,
+ bool aConvertDataToText, nsIURI** aURL) {
+ nsresult rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aImapUrl->SetImapMessageSink(aImapMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aImapUrl->SetImapAction(aImapAction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> url(do_QueryInterface(aImapUrl));
+
+ // if the display consumer is a docshell, then we should run the url in the
+ // docshell. otherwise, it should be a stream listener....so open a channel
+ // using AsyncRead and the provided stream listener....
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aDisplayConsumer, &rv));
+ if (aImapMailFolder && docShell) {
+ nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer;
+ rv = aImapMailFolder->GetServer(getter_AddRefs(aMsgIncomingServer));
+ if (NS_SUCCEEDED(rv) && aMsgIncomingServer) {
+ bool interrupted;
+ nsCOMPtr<nsIImapIncomingServer> aImapServer(
+ do_QueryInterface(aMsgIncomingServer, &rv));
+ if (NS_SUCCEEDED(rv) && aImapServer)
+ aImapServer->PseudoInterruptMsgLoad(aImapMailFolder, aMsgWindow,
+ &interrupted);
+ }
+ }
+ if (NS_SUCCEEDED(rv) && docShell) {
+ NS_ASSERTION(!aConvertDataToText,
+ "can't convert to text when using docshell");
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url);
+ loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE);
+ loadState->SetFirstParty(false);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ rv = docShell->LoadURI(loadState, false);
+ } else {
+ nsCOMPtr<nsIStreamListener> streamListener =
+ do_QueryInterface(aDisplayConsumer, &rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl, &rv);
+ if (aMsgWindow && mailnewsUrl) mailnewsUrl->SetMsgWindow(aMsgWindow);
+ if (NS_SUCCEEDED(rv) && streamListener) {
+ nsCOMPtr<nsIChannel> channel;
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ if (NS_SUCCEEDED(rv) && mailnewsUrl)
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ rv = NewChannel(url, loadInfo, getter_AddRefs(channel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need a load group to hold onto the channel. When the request is
+ // finished, it'll get removed from the load group, and the channel will
+ // go away, which will free the load group.
+ if (!loadGroup) loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID);
+
+ rv = channel->SetLoadGroup(loadGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aConvertDataToText) {
+ nsCOMPtr<nsIStreamListener> conversionListener;
+ nsCOMPtr<nsIStreamConverterService> streamConverter =
+ do_GetService("@mozilla.org/streamConverters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = streamConverter->AsyncConvertData(
+ "message/rfc822", "*/*", streamListener, channel,
+ getter_AddRefs(conversionListener));
+ NS_ENSURE_SUCCESS(rv, rv);
+ streamListener = conversionListener; // this is our new listener.
+ }
+
+ // now try to open the channel passing in our display consumer as the
+ // listener
+ rv = channel->AsyncOpen(streamListener);
+ } else // do what we used to do before
+ {
+ // I'd like to get rid of this code as I believe that we always get a
+ // docshell or stream listener passed into us in this method but i'm not
+ // sure yet... I'm going to use an assert for now to figure out if this is
+ // ever getting called
+#if defined(DEBUG_mscott) || defined(DEBUG_bienvenu)
+ NS_ERROR("oops...someone still is reaching this part of the code");
+#endif
+ rv = GetImapConnectionAndLoadUrl(aImapUrl, aDisplayConsumer, aURL);
+ }
+ }
+ return rv;
+}
+
+// this method streams a message to the passed in consumer, with an optional
+// stream converter and additional header (e.g., "header=filter")
+NS_IMETHODIMP nsImapService::StreamMessage(
+ const nsACString& aMessageURI, nsISupports* aConsumer,
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener, bool aConvertData,
+ const nsACString& aAdditionalHeader, bool aLocalOnly, nsIURI** aURL) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString msgKey;
+ nsAutoCString mimePart;
+ nsAutoCString folderURI;
+ nsMsgKey key;
+ nsAutoCString messageURI(aMessageURI);
+
+ int32_t typeIndex = messageURI.Find("&type=application/x-message-display");
+ if (typeIndex != kNotFound) {
+ // This happens with forward inline of a message/rfc822 attachment opened in
+ // a standalone msg window.
+ // So, just cut to the chase and call AsyncOpen on a channel.
+ nsCOMPtr<nsIURI> uri;
+ messageURI.Cut(typeIndex,
+ sizeof("&type=application/x-message-display") - 1);
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), messageURI.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aURL) NS_IF_ADDREF(*aURL = uri);
+ nsCOMPtr<nsIStreamListener> aStreamListener =
+ do_QueryInterface(aConsumer, &rv);
+ if (NS_SUCCEEDED(rv) && aStreamListener) {
+ nsCOMPtr<nsIChannel> aChannel;
+ nsCOMPtr<nsILoadGroup> aLoadGroup;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(uri, &rv);
+ if (NS_SUCCEEDED(rv) && mailnewsUrl)
+ mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ rv = NewChannel(uri, loadInfo, getter_AddRefs(aChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now try to open the channel passing in our display consumer as the
+ // listener
+ rv = aChannel->AsyncOpen(aStreamListener);
+ return rv;
+ }
+ }
+
+ nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND;
+ rv = nsParseImapMessageURI(aMessageURI, folderURI, &key,
+ getter_Copies(mimePart));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(do_QueryInterface(folder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(aMessageURI, getter_AddRefs(imapUrl), folder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl(do_QueryInterface(imapUrl));
+
+ // This option is used by the JS Mime Emitter, in case we want a cheap
+ // streaming, for example, if we just want a quick look at some header,
+ // without having to download all the attachments...
+
+ // We need to add the fetch command here for the cache lookup to behave
+ // correctly
+ nsAutoCString additionalHeader(aAdditionalHeader);
+ rv = AddImapFetchToUrl(mailnewsurl, folder, msgKey, additionalHeader);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer;
+
+ mailnewsurl->SetMsgWindow(aMsgWindow);
+ rv = mailnewsurl->GetServer(getter_AddRefs(aMsgIncomingServer));
+
+ // Try to check if the message is offline
+ bool hasMsgOffline = false;
+ folder->HasMsgOffline(key, &hasMsgOffline);
+ mailnewsurl->SetMsgIsInLocalCache(hasMsgOffline);
+ imapUrl->SetLocalFetchOnly(aLocalOnly);
+
+ // If we don't have the message available locally, and we can't get it
+ // over the network, return with an error
+ if (aLocalOnly || WeAreOffline()) {
+ bool isMsgInMemCache = false;
+ if (!hasMsgOffline) {
+ rv = IsMsgInMemCache(mailnewsurl, folder, &isMsgInMemCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!isMsgInMemCache) return NS_ERROR_FAILURE;
+ }
+ }
+
+ bool shouldStoreMsgOffline = false;
+ folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline);
+ imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
+ rv = GetMessageFromUrl(imapUrl, nsIImapUrl::nsImapMsgFetchPeek, folder,
+ imapMessageSink, aMsgWindow, aConsumer, aConvertData,
+ aURL);
+ return rv;
+}
+
+// this method streams a message's headers to the passed in consumer.
+NS_IMETHODIMP nsImapService::StreamHeaders(const nsACString& aMessageURI,
+ nsIStreamListener* aConsumer,
+ nsIUrlListener* aUrlListener,
+ bool aLocalOnly, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aConsumer);
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsAutoCString msgKey;
+ nsAutoCString folderURI;
+ nsCString mimePart;
+ nsMsgKey key;
+
+ nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND;
+ rv = nsParseImapMessageURI(aMessageURI, folderURI, &key,
+ getter_Copies(mimePart));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ bool hasMsgOffline = false;
+ folder->HasMsgOffline(key, &hasMsgOffline);
+ if (hasMsgOffline) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = folder->GetMessageHeader(key, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetLocalMsgStream(hdr, getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return MsgStreamMsgHeaders(inputStream, aConsumer);
+ }
+
+ if (aLocalOnly) return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::IsMsgInMemCache(nsIURI* aUrl,
+ nsIMsgFolder* aImapMailFolder,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+ *aResult = false;
+
+ // Poke around in the memory cache
+ if (mCacheStorage) {
+ nsAutoCString urlSpec;
+ aUrl->GetSpec(urlSpec);
+
+ // Strip any query qualifiers.
+ bool truncated = false;
+ int32_t ind = urlSpec.FindChar('?');
+ if (ind != kNotFound) {
+ urlSpec.SetLength(ind);
+ truncated = true;
+ }
+ ind = urlSpec.Find("/;");
+ if (ind != kNotFound) {
+ urlSpec.SetLength(ind);
+ truncated = true;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIImapMailFolderSink> folderSink(
+ do_QueryInterface(aImapMailFolder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t uidValidity = -1;
+ folderSink->GetUidValidity(&uidValidity);
+ // stick the uid validity in front of the url, so that if the uid validity
+ // changes, we won't re-use the wrong cache entries.
+ nsAutoCString extension;
+ extension.AppendInt(uidValidity, 16);
+
+ bool exists;
+ if (truncated) {
+ nsCOMPtr<nsIURI> newUri;
+ rv = NS_NewURI(getter_AddRefs(newUri), urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mCacheStorage->Exists(newUri, extension, &exists);
+ } else {
+ rv = mCacheStorage->Exists(aUrl, extension, &exists);
+ }
+ if (NS_SUCCEEDED(rv) && exists) {
+ *aResult = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsImapService::CreateStartOfImapUrl(const nsACString& aImapURI,
+ nsIImapUrl** imapUrl,
+ nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsACString& urlSpec,
+ char& hierarchyDelimiter) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsCString hostname;
+ nsCString username;
+ nsCString escapedUsername;
+
+ nsresult rv = aImapMailFolder->GetHostname(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aImapMailFolder->GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!username.IsEmpty())
+ MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+
+ int32_t port = nsIImapUrl::DEFAULT_IMAP_PORT;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = aImapMailFolder->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv)) {
+ server->GetPort(&port);
+ if (port == -1 || port == 0) port = nsIImapUrl::DEFAULT_IMAP_PORT;
+ }
+
+ // now we need to create an imap url to load into the connection. The url
+ // needs to represent a select folder action.
+ rv = CallCreateInstance(kImapUrlCID, imapUrl);
+ if (NS_SUCCEEDED(rv) && *imapUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(*imapUrl, &rv);
+ if (NS_SUCCEEDED(rv) && mailnewsUrl && aUrlListener)
+ mailnewsUrl->RegisterListener(aUrlListener);
+ nsCOMPtr<nsIMsgMessageUrl> msgurl(do_QueryInterface(*imapUrl));
+ (*imapUrl)->SetExternalLinkUrl(false);
+ msgurl->SetUri(aImapURI);
+
+ urlSpec = "imap://";
+ urlSpec.Append(escapedUsername);
+ urlSpec.Append('@');
+ urlSpec.Append(hostname);
+ urlSpec.Append(':');
+
+ nsAutoCString portStr;
+ portStr.AppendInt(port);
+ urlSpec.Append(portStr);
+
+ // *** jefft - force to parse the urlSpec in order to search for
+ // the correct incoming server
+ rv = mailnewsUrl->SetSpecInternal(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ hierarchyDelimiter = kOnlineHierarchySeparatorUnknown;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(aImapMailFolder);
+ if (imapFolder) imapFolder->GetHierarchyDelimiter(&hierarchyDelimiter);
+ }
+ return rv;
+}
+
+/* fetching the headers of RFC822 messages */
+/* imap4://HOST>header><UID/SEQUENCE>>MAILBOXPATH>x */
+/* 'x' is the message UID or sequence number list */
+/* will not affect the 'SEEN' flag */
+NS_IMETHODIMP nsImapService::GetHeaders(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIURI** aURL,
+ const nsACString& messageIdentifierList,
+ bool messageIdsAreUID) {
+ // create a protocol instance to handle the request.
+ // NOTE: once we start working with multiple connections, this step will be
+ // much more complicated...but for now just create a connection and process
+ // the request.
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl);
+
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ urlSpec.AppendLiteral("/header>");
+ urlSpec.Append(messageIdsAreUID ? uidString : sequenceString);
+ urlSpec.Append('>');
+ urlSpec.Append(char(hierarchyDelimiter));
+
+ nsCString folderName;
+
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIdentifierList);
+ rv = mailnewsUrl->SetSpecInternal(urlSpec);
+
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ }
+ return rv;
+}
+
+/* peeking at the start of msg bodies */
+/* imap4://HOST>header><UID>>MAILBOXPATH>x>n */
+/* 'x' is the message UID */
+/* 'n' is the number of bytes to fetch */
+/* will not affect the 'SEEN' flag */
+NS_IMETHODIMP nsImapService::GetBodyStart(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener,
+ const nsACString& messageIdentifierList, int32_t numBytes, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgPreview);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl);
+
+ urlSpec.AppendLiteral("/previewBody>");
+ urlSpec.Append(uidString);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIdentifierList);
+ urlSpec.Append('>');
+ urlSpec.AppendInt(numBytes);
+ rv = mailnewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapService::FolderCommand(nsIMsgFolder* imapMailFolder,
+ nsIUrlListener* urlListener,
+ const char* aCommand,
+ nsImapAction imapAction,
+ nsIMsgWindow* msgWindow, nsIURI** url) {
+ NS_ENSURE_ARG_POINTER(imapMailFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(imapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ imapMailFolder, urlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = imapUrl->SetImapAction(imapAction);
+ rv = SetImapUrlSink(imapMailFolder, imapUrl);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+ if (mailnewsurl) mailnewsurl->SetMsgWindow(msgWindow);
+
+ if (NS_SUCCEEDED(rv)) {
+ urlSpec.Append(aCommand);
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(imapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapService::VerifyLogon(nsIMsgFolder* aFolder, nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char delimiter = '/'; // shouldn't matter what is is.
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aFolder, aUrlListener, urlSpec, delimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ mailNewsUrl->SetSuppressErrorMsgs(true);
+ mailNewsUrl->SetMsgWindow(aMsgWindow);
+ rv = SetImapUrlSink(aFolder, imapUrl);
+ urlSpec.AppendLiteral("/verifyLogon");
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ if (aURL) mailnewsurl.forget(aURL);
+ }
+ return rv;
+}
+
+// Noop, used to update a folder (causes server to send changes).
+NS_IMETHODIMP nsImapService::Noop(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/selectnoop>",
+ nsIImapUrl::nsImapSelectNoopFolder, nullptr, aURL);
+}
+
+// FolderStatus, used to update message counts
+NS_IMETHODIMP nsImapService::UpdateFolderStatus(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/folderstatus>",
+ nsIImapUrl::nsImapFolderStatus, nullptr, aURL);
+}
+
+// Expunge, used to "compress" an imap folder,removes deleted messages.
+NS_IMETHODIMP nsImapService::Expunge(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/Expunge>",
+ nsIImapUrl::nsImapExpungeFolder, aMsgWindow, nullptr);
+}
+
+/* old-stle biff that doesn't download headers */
+NS_IMETHODIMP nsImapService::Biff(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener, nsIURI** aURL,
+ uint32_t uidHighWater) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ // static const char *formatString = "biff>%c%s>%ld";
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapExpungeFolder);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ urlSpec.AppendLiteral("/Biff>");
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.AppendInt(uidHighWater);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::DeleteFolder(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ // If it's an aol server then use 'deletefolder' url to
+ // remove all msgs first and then remove the folder itself.
+ bool removeFolderAndMsgs = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(aImapMailFolder->GetServer(getter_AddRefs(server))) &&
+ server) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ if (imapServer) imapServer->GetIsAOLServer(&removeFolderAndMsgs);
+ }
+
+ return FolderCommand(aImapMailFolder, aUrlListener,
+ removeFolderAndMsgs ? "/deletefolder>" : "/delete>",
+ nsIImapUrl::nsImapDeleteFolder, aMsgWindow, nullptr);
+}
+
+NS_IMETHODIMP nsImapService::DeleteMessages(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, nsIURI** aURL,
+ const nsACString& messageIdentifierList, bool messageIdsAreUID) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ // create a protocol instance to handle the request.
+ // NOTE: once we start working with multiple connections, this step will be
+ // much more complicated...but for now just create a connection and process
+ // the request.
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ urlSpec.AppendLiteral("/deletemsg>");
+ urlSpec.Append(messageIdsAreUID ? uidString : sequenceString);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIdentifierList);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ }
+ return rv;
+}
+
+// Delete all messages in a folder, used to empty trash
+NS_IMETHODIMP nsImapService::DeleteAllMessages(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/deleteallmsgs>",
+ nsIImapUrl::nsImapSelectNoopFolder, nullptr, nullptr);
+}
+
+NS_IMETHODIMP nsImapService::AddMessageFlags(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener,
+ const nsACString& messageIdentifierList, imapMessageFlagsType flags,
+ bool messageIdsAreUID) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return DiddleFlags(aImapMailFolder, aUrlListener, nullptr,
+ messageIdentifierList, "addmsgflags", flags,
+ messageIdsAreUID);
+}
+
+NS_IMETHODIMP nsImapService::SubtractMessageFlags(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener,
+ const nsACString& messageIdentifierList, imapMessageFlagsType flags,
+ bool messageIdsAreUID) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return DiddleFlags(aImapMailFolder, aUrlListener, nullptr,
+ messageIdentifierList, "subtractmsgflags", flags,
+ messageIdsAreUID);
+}
+
+NS_IMETHODIMP nsImapService::SetMessageFlags(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, nsIURI** aURL,
+ const nsACString& messageIdentifierList, imapMessageFlagsType flags,
+ bool messageIdsAreUID) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return DiddleFlags(aImapMailFolder, aUrlListener, aURL, messageIdentifierList,
+ "setmsgflags", flags, messageIdsAreUID);
+}
+
+nsresult nsImapService::DiddleFlags(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener, nsIURI** aURL,
+ const nsACString& messageIdentifierList,
+ const char* howToDiddle,
+ imapMessageFlagsType flags,
+ bool messageIdsAreUID) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ // create a protocol instance to handle the request.
+ // NOTE: once we start working with multiple connections,
+ // this step will be much more complicated...but for now
+ // just create a connection and process the request.
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch);
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ urlSpec.Append('/');
+ urlSpec.Append(howToDiddle);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIdsAreUID ? uidString : sequenceString);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+ nsCString folderName;
+ GetFolderName(aImapMailFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIdentifierList);
+ urlSpec.Append('>');
+ urlSpec.AppendInt(flags);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapService::SetImapUrlSink(nsIMsgFolder* aMsgFolder,
+ nsIImapUrl* aImapUrl) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ nsCOMPtr<nsIImapServerSink> imapServerSink;
+
+ rv = aMsgFolder->GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ imapServerSink = do_QueryInterface(incomingServer);
+ if (imapServerSink) aImapUrl->SetImapServerSink(imapServerSink);
+ }
+
+ nsCOMPtr<nsIImapMailFolderSink> imapMailFolderSink =
+ do_QueryInterface(aMsgFolder);
+ if (NS_SUCCEEDED(rv) && imapMailFolderSink)
+ aImapUrl->SetImapMailFolderSink(imapMailFolderSink);
+
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink = do_QueryInterface(aMsgFolder);
+ if (NS_SUCCEEDED(rv) && imapMessageSink)
+ aImapUrl->SetImapMessageSink(imapMessageSink);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl);
+ mailnewsUrl->SetFolder(aMsgFolder);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::DiscoverAllFolders(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ rv = SetImapUrlSink(aImapMailFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+ mailnewsurl->SetMsgWindow(aMsgWindow);
+ urlSpec.AppendLiteral("/discoverallboxes");
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::DiscoverAllAndSubscribedFolders(
+ nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsCOMPtr<nsIImapUrl> aImapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(aImapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && aImapUrl) {
+ rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl);
+ urlSpec.AppendLiteral("/discoverallandsubscribedboxes");
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+
+ if (aMsgWindow) mailnewsurl->SetMsgWindow(aMsgWindow);
+
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(aImapUrl, nullptr, nullptr);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::DiscoverChildren(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener,
+ const nsACString& folderPath) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ nsCOMPtr<nsIImapUrl> aImapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder);
+ nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(aImapUrl),
+ aImapMailFolder, aUrlListener, urlSpec,
+ hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ if (!folderPath.IsEmpty()) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl);
+ urlSpec.AppendLiteral("/discoverchildren>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderPath);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+
+ // Make sure the uri has the same hierarchy separator as the one in msg
+ // folder obj if it's not kOnlineHierarchySeparatorUnknown (ie, '^').
+ char uriDelimiter;
+ nsresult rv1 = aImapUrl->GetOnlineSubDirSeparator(&uriDelimiter);
+ if (NS_SUCCEEDED(rv1) &&
+ hierarchyDelimiter != kOnlineHierarchySeparatorUnknown &&
+ uriDelimiter != hierarchyDelimiter)
+ aImapUrl->SetOnlineSubDirSeparator(hierarchyDelimiter);
+
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(aImapUrl, nullptr, nullptr);
+ } else
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::OnlineMessageCopy(
+ nsIMsgFolder* aSrcFolder, const nsACString& messageIds,
+ nsIMsgFolder* aDstFolder, bool idsAreUids, bool isMove,
+ nsIUrlListener* aUrlListener, nsIURI** aURL, nsISupports* copyState,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aSrcFolder);
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> srcServer;
+ nsCOMPtr<nsIMsgIncomingServer> dstServer;
+
+ rv = aSrcFolder->GetServer(getter_AddRefs(srcServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aDstFolder->GetServer(getter_AddRefs(dstServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool sameServer;
+ rv = dstServer->Equals(srcServer, &sameServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!sameServer) {
+ NS_ASSERTION(false, "can't use this method to copy across servers");
+ // *** can only take message from the same imap host and user accnt
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aSrcFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aSrcFolder,
+ aUrlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ SetImapUrlSink(aSrcFolder, imapUrl);
+ imapUrl->SetCopyState(copyState);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl(do_QueryInterface(imapUrl));
+ mailnewsurl->SetMsgWindow(aMsgWindow);
+
+ if (isMove)
+ urlSpec.AppendLiteral("/onlinemove>");
+ else
+ urlSpec.AppendLiteral("/onlinecopy>");
+ if (idsAreUids)
+ urlSpec.Append(uidString);
+ else
+ urlSpec.Append(sequenceString);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(aSrcFolder, folderName);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(messageIds);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+ folderName.Adopt(strdup(""));
+ GetFolderName(aDstFolder, folderName);
+ urlSpec.Append(folderName);
+
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ return rv;
+}
+
+nsresult nsImapService::OfflineAppendFromFile(
+ nsIFile* aFile, nsIURI* aUrl, nsIMsgFolder* aDstFolder,
+ const nsACString& messageId, // to be replaced
+ bool inSelectedState, // needs to be in
+ nsIUrlListener* aListener, nsIURI** aURL, nsISupports* aCopyState) {
+ nsCOMPtr<nsIMsgDatabase> destDB;
+ nsresult rv = aDstFolder->GetMsgDatabase(getter_AddRefs(destDB));
+ // ### might need to send some notifications instead of just returning
+
+ bool isLocked;
+ aDstFolder->GetLocked(&isLocked);
+ if (isLocked) return NS_MSG_FOLDER_BUSY;
+
+ if (NS_SUCCEEDED(rv) && destDB) {
+ nsMsgKey fakeKey;
+ destDB->GetNextFakeOfflineMsgKey(&fakeKey);
+
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(destDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = opsDb->GetOfflineOpForKey(fakeKey, true, getter_AddRefs(op));
+ if (NS_SUCCEEDED(rv) && op) {
+ nsCString destFolderUri;
+ aDstFolder->GetURI(destFolderUri);
+ op->SetOperation(
+ nsIMsgOfflineImapOperation::kAppendDraft); // ### do we care if it's
+ // a template?
+ op->SetDestinationFolderURI(destFolderUri);
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsCOMPtr<nsIMsgIncomingServer> dstServer;
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+
+ aDstFolder->GetServer(getter_AddRefs(dstServer));
+ rv = dstServer->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = destDB->CreateNewHdr(fakeKey, getter_AddRefs(newMsgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aDstFolder->GetOfflineStoreOutputStream(
+ newMsgHdr, getter_AddRefs(outputStream));
+
+ if (NS_SUCCEEDED(rv) && outputStream) {
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIMsgParseMailMsgState> msgParser = do_CreateInstance(
+ "@mozilla.org/messenger/messagestateparser;1", &rv);
+ msgParser->SetMailDB(destDB);
+
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+ if (NS_SUCCEEDED(rv) && inputStream) {
+ // now, copy the temp file to the offline store for the dest folder.
+ RefPtr<nsMsgLineStreamBuffer> inputStreamBuffer =
+ new nsMsgLineStreamBuffer(
+ FILE_IO_BUFFER_SIZE,
+ true, // allocate new lines
+ false); // leave CRLFs on the returned string
+ int64_t fileSize;
+ aFile->GetFileSize(&fileSize);
+ uint32_t bytesWritten;
+ rv = NS_OK;
+ // rv = inputStream->Read(inputBuffer, inputBufferSize, &bytesRead);
+ // if (NS_SUCCEEDED(rv) && bytesRead > 0)
+ msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState);
+ msgParser->SetNewMsgHdr(newMsgHdr);
+ // set the new key to fake key so the msg hdr will have that for a key
+ msgParser->SetNewKey(fakeKey);
+ bool needMoreData = false;
+ char* newLine = nullptr;
+ uint32_t numBytesInLine = 0;
+ do {
+ newLine = inputStreamBuffer->ReadNextLine(
+ inputStream, numBytesInLine, needMoreData);
+ if (newLine) {
+ msgParser->ParseAFolderLine(newLine, numBytesInLine);
+ rv = outputStream->Write(newLine, numBytesInLine, &bytesWritten);
+ free(newLine);
+ }
+ } while (newLine);
+ msgParser->FinishHeader();
+
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t resultFlags;
+ newMsgHdr->OrFlags(
+ nsMsgMessageFlags::Offline | nsMsgMessageFlags::Read,
+ &resultFlags);
+ newMsgHdr->SetOfflineMessageSize(fileSize);
+ destDB->AddNewHdrToDB(newMsgHdr, true /* notify */);
+ aDstFolder->SetFlag(nsMsgFolderFlags::OfflineEvents);
+ if (msgStore) msgStore->FinishNewMessage(outputStream, newMsgHdr);
+ }
+ // tell the listener we're done.
+ inputStream->Close();
+ inputStream = nullptr;
+ aListener->OnStopRunningUrl(aUrl, NS_OK);
+ }
+ outputStream->Close();
+ }
+ }
+ }
+
+ if (destDB) destDB->Close(true);
+ return rv;
+}
+
+/* append message from file url */
+/* imap://HOST>appendmsgfromfile>DESTINATIONMAILBOXPATH */
+/* imap://HOST>appenddraftfromfile>DESTINATIONMAILBOXPATH>UID>messageId */
+NS_IMETHODIMP nsImapService::AppendMessageFromFile(
+ nsIFile* aFile, nsIMsgFolder* aDstFolder,
+ const nsACString& messageId, // to be replaced
+ bool idsAreUids,
+ bool inSelectedState, // needs to be in
+ nsIUrlListener* aListener, nsISupports* aCopyState,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(aDstFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aDstFolder,
+ aListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(imapUrl);
+ if (msgUrl && aMsgWindow) {
+ // we get the loadGroup from msgWindow
+ msgUrl->SetMsgWindow(aMsgWindow);
+ }
+
+ SetImapUrlSink(aDstFolder, imapUrl);
+ imapUrl->SetMsgFile(aFile);
+ imapUrl->SetCopyState(aCopyState);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ if (inSelectedState)
+ urlSpec.AppendLiteral("/appenddraftfromfile>");
+ else
+ urlSpec.AppendLiteral("/appendmsgfromfile>");
+
+ urlSpec.Append(hierarchyDelimiter);
+
+ nsCString folderName;
+ GetFolderName(aDstFolder, folderName);
+ urlSpec.Append(folderName);
+
+ if (inSelectedState) {
+ urlSpec.Append('>');
+ if (idsAreUids)
+ urlSpec.Append(uidString);
+ else
+ urlSpec.Append(sequenceString);
+ urlSpec.Append('>');
+ if (!messageId.IsEmpty()) urlSpec.Append(messageId);
+ }
+
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (WeAreOffline()) {
+ // handle offline append to drafts or templates folder here.
+ return OfflineAppendFromFile(aFile, mailnewsurl, aDstFolder, messageId,
+ inSelectedState, aListener, nullptr,
+ aCopyState);
+ }
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ return rv;
+}
+
+nsresult nsImapService::GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl,
+ nsISupports* aConsumer,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapUrl);
+
+ bool isValidUrl;
+ aImapUrl->GetValidUrl(&isValidUrl);
+ if (!isValidUrl) return NS_ERROR_FAILURE;
+
+ if (WeAreOffline()) {
+ nsImapAction imapAction;
+
+ // the only thing we can do offline is fetch messages.
+ // ### TODO - need to look at msg copy, save attachment, etc. when we
+ // have offline message bodies.
+ aImapUrl->GetImapAction(&imapAction);
+ if (imapAction != nsIImapUrl::nsImapMsgFetch &&
+ imapAction != nsIImapUrl::nsImapSaveMessageToDisk)
+ return NS_MSG_ERROR_OFFLINE;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(aImapUrl);
+ nsresult rv = msgUrl->GetServer(getter_AddRefs(aMsgIncomingServer));
+
+ if (aURL) {
+ msgUrl.forget(aURL);
+ }
+
+ if (NS_SUCCEEDED(rv) && aMsgIncomingServer) {
+ nsCOMPtr<nsIImapIncomingServer> aImapServer(
+ do_QueryInterface(aMsgIncomingServer, &rv));
+ if (NS_SUCCEEDED(rv) && aImapServer)
+ rv = aImapServer->GetImapConnectionAndLoadUrl(aImapUrl, aConsumer);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::MoveFolder(nsIMsgFolder* srcFolder,
+ nsIMsgFolder* dstFolder,
+ nsIUrlListener* urlListener,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+
+ char default_hierarchyDelimiter = GetHierarchyDelimiter(dstFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), dstFolder,
+ urlListener, urlSpec, default_hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = SetImapUrlSink(dstFolder, imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ if (mailNewsUrl) mailNewsUrl->SetMsgWindow(msgWindow);
+ char hierarchyDelimiter = kOnlineHierarchySeparatorUnknown;
+ nsCString folderName;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+ GetFolderName(srcFolder, folderName);
+ urlSpec.AppendLiteral("/movefolderhierarchy>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ GetFolderName(dstFolder, folderName);
+ if (!folderName.IsEmpty()) {
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ }
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv)) {
+ GetFolderName(srcFolder, folderName);
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::RenameLeaf(nsIMsgFolder* srcFolder,
+ const nsAString& newLeafName,
+ nsIUrlListener* urlListener,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(srcFolder);
+ nsresult rv =
+ CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), srcFolder,
+ urlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv)) {
+ rv = SetImapUrlSink(srcFolder, imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ mailNewsUrl->SetMsgWindow(msgWindow);
+ nsCString folderName;
+ GetFolderName(srcFolder, folderName);
+ urlSpec.AppendLiteral("/rename>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+ nsAutoCString cStrFolderName;
+ // Unescape the name before looking for parent path
+ MsgUnescapeString(folderName, 0, cStrFolderName);
+ int32_t leafNameStart = cStrFolderName.RFindChar(hierarchyDelimiter);
+ if (leafNameStart != -1) {
+ cStrFolderName.SetLength(leafNameStart + 1);
+ urlSpec.Append(cStrFolderName);
+ }
+
+ nsAutoCString utfNewName;
+ bool utf8AcceptEnabled;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(srcFolder);
+ rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (utf8AcceptEnabled) {
+ CopyUTF16toUTF8(newLeafName, utfNewName);
+ } else {
+ CopyUTF16toMUTF7(newLeafName, utfNewName);
+ }
+ nsCString escapedNewName;
+ MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH, escapedNewName);
+ nsCString escapedSlashName;
+ rv = nsImapUrl::EscapeSlashes(escapedNewName.get(),
+ getter_Copies(escapedSlashName));
+ NS_ENSURE_SUCCESS(rv, rv);
+ urlSpec.Append(escapedSlashName);
+
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::CreateFolder(nsIMsgFolder* parent,
+ const nsAString& newFolderName,
+ nsIUrlListener* urlListener,
+ nsIURI** url) {
+ NS_ENSURE_ARG_POINTER(parent);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(parent);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), parent,
+ urlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = SetImapUrlSink(parent, imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ nsCString folderName;
+ GetFolderName(parent, folderName);
+ urlSpec.AppendLiteral("/create>");
+ urlSpec.Append(hierarchyDelimiter);
+ if (!folderName.IsEmpty()) {
+ nsCString canonicalName;
+ nsImapUrl::ConvertToCanonicalFormat(
+ folderName.get(), hierarchyDelimiter, getter_Copies(canonicalName));
+ urlSpec.Append(canonicalName);
+ urlSpec.Append(hierarchyDelimiter);
+ }
+
+ nsAutoCString utfNewName;
+ bool utf8AcceptEnabled;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent);
+ rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (utf8AcceptEnabled) {
+ CopyUTF16toUTF8(newFolderName, utfNewName);
+ } else {
+ CopyUTF16toMUTF7(newFolderName, utfNewName);
+ }
+ nsCString escapedFolderName;
+ MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH,
+ escapedFolderName);
+ urlSpec.Append(escapedFolderName);
+
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::EnsureFolderExists(nsIMsgFolder* parent,
+ const nsAString& newFolderName,
+ nsIMsgWindow* msgWindow,
+ nsIUrlListener* urlListener) {
+ NS_ENSURE_ARG_POINTER(parent);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+
+ char hierarchyDelimiter = GetHierarchyDelimiter(parent);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), parent,
+ urlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = SetImapUrlSink(parent, imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+
+ nsCString folderName;
+ GetFolderName(parent, folderName);
+ urlSpec.AppendLiteral("/ensureExists>");
+ urlSpec.Append(hierarchyDelimiter);
+ if (!folderName.IsEmpty()) {
+ urlSpec.Append(folderName);
+ urlSpec.Append(hierarchyDelimiter);
+ }
+ nsAutoCString utfNewName;
+ bool utf8AcceptEnabled;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent);
+ rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (utf8AcceptEnabled) {
+ CopyUTF16toUTF8(newFolderName, utfNewName);
+ } else {
+ CopyUTF16toMUTF7(newFolderName, utfNewName);
+ }
+ nsCString escapedFolderName;
+ MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH,
+ escapedFolderName);
+ urlSpec.Append(escapedFolderName);
+
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+
+ if (msgWindow) mailnewsurl->SetMsgWindow(msgWindow);
+
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::ListFolder(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/listfolder>",
+ nsIImapUrl::nsImapListFolder, nullptr, nullptr);
+}
+
+NS_IMETHODIMP nsImapService::GetScheme(nsACString& aScheme) {
+ aScheme.AssignLiteral("imap");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::AllowPort(int32_t port, const char* scheme,
+ bool* aRetVal) {
+ // allow imap to run on any port
+ *aRetVal = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetDefaultDoBiff(bool* aDoBiff) {
+ NS_ENSURE_ARG_POINTER(aDoBiff);
+ // by default, do biff for IMAP servers
+ *aDoBiff = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetDefaultServerPort(bool isSecure,
+ int32_t* aDefaultPort) {
+ // Return Secure IMAP Port if secure option chosen i.e., if isSecure is TRUE
+ if (isSecure)
+ *aDefaultPort = nsIImapUrl::DEFAULT_IMAPS_PORT;
+ else
+ *aDefaultPort = nsIImapUrl::DEFAULT_IMAP_PORT;
+
+ return NS_OK;
+}
+
+// this method first tries to find an exact username and hostname match with the
+// given url then, tries to find any account on the passed in imap host in case
+// this is a url to a shared imap folder.
+nsresult nsImapService::GetServerFromUrl(nsIImapUrl* aImapUrl,
+ nsIMsgIncomingServer** aServer) {
+ nsresult rv;
+ nsCString folderName;
+ nsAutoCString userPass;
+ nsAutoCString hostName;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl);
+
+ // if we can't get a folder name out of the url then I think this is an error
+ aImapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName));
+ if (folderName.IsEmpty()) {
+ rv = mailnewsUrl->GetFileName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = accountManager->FindServerByURI(mailnewsUrl, aServer);
+
+ // look for server with any user name, in case we're trying to subscribe
+ // to a folder with some one else's user name like the following
+ // "IMAP://userSharingFolder@server1/SharedFolderName"
+ if (NS_FAILED(rv) || !aServer) {
+ nsAutoCString turl;
+ rv = mailnewsUrl->GetSpec(turl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(turl)
+ .SetUserPass(EmptyCString())
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = accountManager->FindServerByURI(url, aServer);
+ if (*aServer) aImapUrl->SetExternalLinkUrl(true);
+ }
+
+ // if we can't extract the imap server from this url then give up!!!
+ NS_ENSURE_TRUE(*aServer, NS_ERROR_FAILURE);
+ return rv;
+}
+
+nsresult nsImapService::NewURI(const nsACString& aSpec,
+ const char* aOriginCharset, // ignored
+ nsIURI* aBaseURI, nsIURI** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> aImapUrl = do_CreateInstance(kImapUrlCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now extract lots of fun information...
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl);
+ // nsAutoCString unescapedSpec(aSpec);
+ // nsUnescape(unescapedSpec.BeginWriting());
+
+ // set the spec
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ aBaseURI->Resolve(aSpec, newSpec);
+ rv = mailnewsUrl->SetSpecInternal(newSpec);
+ } else {
+ rv = mailnewsUrl->SetSpecInternal(aSpec);
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString folderName;
+ // if we can't get a folder name out of the url then I think this is an error
+ aImapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName));
+ if (folderName.IsEmpty()) {
+ rv = mailnewsUrl->GetFileName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServerFromUrl(aImapUrl, getter_AddRefs(server));
+ // if we can't extract the imap server from this url then give up!!!
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(server, NS_ERROR_FAILURE);
+
+ // now try to get the folder in question...
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ bool ready;
+ if (rootFolder && !folderName.IsEmpty() &&
+ // Skip folder processing if folder names aren't ready yet.
+ // They may not be available during early initialization.
+ // XXX TODO: This hack can be removed when the localization system gets
+ // initialized in M-C code before, for example, the permission manager
+ // which creates all sorts of URIs incl. imap: URIs.
+ NS_SUCCEEDED(rootFolder->FolderNamesReady(&ready)) && ready) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ nsCOMPtr<nsIMsgImapMailFolder> subFolder;
+ if (imapRoot) {
+ imapRoot->FindOnlineSubFolder(folderName, getter_AddRefs(subFolder));
+ folder = do_QueryInterface(subFolder);
+ }
+
+ // If we can't find the folder, we can still create the URI
+ // in this low-level service. Cloning URIs where the folder
+ // isn't found is common when folders are renamed or moved.
+ // We also ignore return statuses here.
+ if (folder) {
+ nsCOMPtr<nsIImapMessageSink> msgSink = do_QueryInterface(folder);
+ (void)aImapUrl->SetImapMessageSink(msgSink);
+
+ (void)SetImapUrlSink(folder, aImapUrl);
+
+ nsCString messageIdString;
+ aImapUrl->GetListOfMessageIds(messageIdString);
+ if (!messageIdString.IsEmpty()) {
+ bool useLocalCache = false;
+ folder->HasMsgOffline(strtoul(messageIdString.get(), nullptr, 10),
+ &useLocalCache);
+ mailnewsUrl->SetMsgIsInLocalCache(useLocalCache);
+ }
+ }
+ }
+
+ // we got an imap url, so be sure to return it...
+ nsCOMPtr<nsIURI> imapUri = do_QueryInterface(aImapUrl);
+
+ imapUri.forget(aRetVal);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aRetVal);
+ MOZ_ASSERT(aLoadInfo);
+ *aRetVal = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aURI, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // imap can't open and return a channel right away...the url needs to go in
+ // the imap url queue until we find a connection which can run the url..in
+ // order to satisfy necko, we're going to return a mock imap channel....
+ nsCOMPtr<nsIImapMockChannel> channel =
+ do_CreateInstance(kCImapMockChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ channel->SetURI(aURI);
+
+ rv = channel->SetLoadInfo(aLoadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec;
+ rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the attachment disposition. This forces docShell to open the
+ // attachment instead of displaying it. Content types we have special
+ // handlers for are white-listed. This white list also exists in
+ // nsMailboxService::NewChannel and nsNntpService::NewChannel, so if you're
+ // changing this, update those too.
+ if (spec.Find("part=") >= 0 && spec.Find("type=message/rfc822") < 0 &&
+ spec.Find("type=application/x-message-display") < 0 &&
+ spec.Find("type=application/pdf") < 0) {
+ rv = channel->SetContentDisposition(nsIChannel::DISPOSITION_ATTACHMENT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+ if (msgWindow) {
+ nsCOMPtr<nsIDocShell> msgDocShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(msgDocShell));
+ if (msgDocShell) {
+ nsCOMPtr<nsIProgressEventSink> prevEventSink;
+ channel->GetProgressEventSink(getter_AddRefs(prevEventSink));
+ nsCOMPtr<nsIInterfaceRequestor> docIR(do_QueryInterface(msgDocShell));
+ channel->SetNotificationCallbacks(docIR);
+ // we want to use our existing event sink.
+ if (prevEventSink) channel->SetProgressEventSink(prevEventSink);
+ }
+ } else {
+ // This might not be a call resulting from user action (e.g. we might be
+ // getting a new message via nsImapMailFolder::OnNewIdleMessages(), or via
+ // nsAutoSyncManager, etc). In this case, try to retrieve the top-most
+ // message window to update its status feedback.
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ if (NS_SUCCEEDED(rv) && msgWindow) {
+ // If we could retrieve a window, get its nsIMsgStatusFeedback and set it
+ // to the URL so that other components interacting with it can correctly
+ // feed status updates to the UI.
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ msgWindow->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ mailnewsUrl->SetStatusFeedback(statusFeedback);
+ // We also need to set the status feedback as the channel's progress event
+ // sink, since that's how nsImapProtocol feeds some of the progress
+ // changes (e.g. downloading incoming messages) to the UI.
+ nsCOMPtr<nsIProgressEventSink> eventSink =
+ do_QueryInterface(statusFeedback);
+ channel->SetProgressEventSink(eventSink);
+ }
+
+ // This function ends by checking the final value of rv and deciding whether
+ // to set aRetVal to our channel according to it. We don't want this to be
+ // impacted if we fail to retrieve a window (which might not work if we're
+ // being called through the command line, or through a test), so let's just
+ // reset rv to an OK value.
+ rv = NS_OK;
+ }
+
+ // the imap url holds a weak reference so we can pass the channel into the
+ // imap protocol when we actually run the url.
+ imapUrl->SetMockChannel(channel);
+
+ bool externalLinkUrl;
+ imapUrl->GetExternalLinkUrl(&externalLinkUrl);
+
+ // Only external imap links with no action are supported. Ignore links that
+ // attempt to cause an effect such as fetching a mime part. This avoids
+ // spurious prompts to subscribe to folders due to "imap://...Fetch..." links
+ // residing in legacy emails residing in an imap mailbox.
+ if (externalLinkUrl) {
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+ if (imapAction != 0) externalLinkUrl = false;
+ }
+
+ if (externalLinkUrl) {
+ // Everything after here is to handle clicking on an external link. We only
+ // want to do this if we didn't run the url through the various
+ // nsImapService methods, which we can tell by seeing if the sinks have been
+ // setup on the url or not.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServerFromUrl(imapUrl, getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString folderName;
+ imapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName));
+ if (folderName.IsEmpty()) {
+ nsCString escapedFolderName;
+ rv = mailnewsUrl->GetFileName(escapedFolderName);
+ if (!escapedFolderName.IsEmpty()) {
+ MsgUnescapeString(escapedFolderName, 0, folderName);
+ }
+ }
+ // if the parent is null, then the folder doesn't really exist, so see if
+ // the user wants to subscribe to it./
+ nsCOMPtr<nsIMsgFolder> urlFolder;
+ // now try to get the folder in question...
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder);
+ nsCOMPtr<nsIMsgImapMailFolder> subFolder;
+ if (imapRoot) {
+ imapRoot->FindOnlineSubFolder(folderName, getter_AddRefs(subFolder));
+ urlFolder = do_QueryInterface(subFolder);
+ }
+ nsCOMPtr<nsIMsgFolder> parent;
+ if (urlFolder) urlFolder->GetParent(getter_AddRefs(parent));
+ nsCString serverKey;
+ nsAutoCString userPass;
+ rv = mailnewsUrl->GetUserPass(userPass);
+ server->GetKey(serverKey);
+ nsCString fullFolderName;
+ if (parent) fullFolderName = folderName;
+ if (!parent && !folderName.IsEmpty() && imapRoot) {
+ // Check if this folder is another user's folder.
+ fullFolderName =
+ nsImapNamespaceList::GenerateFullFolderNameWithDefaultNamespace(
+ serverKey.get(), folderName.get(), userPass.get(),
+ kOtherUsersNamespace, nullptr);
+ // if this is another user's folder, let's see if we're already subscribed
+ // to it.
+ rv = imapRoot->FindOnlineSubFolder(fullFolderName,
+ getter_AddRefs(subFolder));
+ urlFolder = do_QueryInterface(subFolder);
+ if (urlFolder) urlFolder->GetParent(getter_AddRefs(parent));
+ }
+ // if we couldn't get the fullFolderName, then we probably couldn't find
+ // the other user's namespace, in which case, we shouldn't try to subscribe
+ // to it.
+ if (!parent && !folderName.IsEmpty() && !fullFolderName.IsEmpty()) {
+ // this folder doesn't exist - check if the user wants to subscribe to
+ // this folder.
+ nsCOMPtr<nsIPrompt> dialog;
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ wwatch->GetNewPrompter(nullptr, getter_AddRefs(dialog));
+
+ nsString statusString, confirmText;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = IMAPGetStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Need to convert folder name, can be MUTF-7 or UTF-8 depending on the
+ // server.
+ nsAutoString unescapedName;
+ if (NS_FAILED(CopyFolderNameToUTF16(fullFolderName, unescapedName)))
+ CopyASCIItoUTF16(fullFolderName, unescapedName);
+ AutoTArray<nsString, 1> formatStrings = {unescapedName};
+
+ rv = bundle->FormatStringFromName("imapSubscribePrompt", formatStrings,
+ confirmText);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool confirmResult = false;
+ rv = dialog->Confirm(nullptr, confirmText.get(), &confirmResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (confirmResult) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ if (imapServer) {
+ nsCOMPtr<nsIURI> subscribeURI;
+ // Now we have the real folder name to try to subscribe to. Let's try
+ // running a subscribe url and returning that as the uri we've
+ // created. We need to convert this to unicode because that's what
+ // subscribe wants.
+ nsAutoString unicodeName;
+ CopyFolderNameToUTF16(fullFolderName, unicodeName);
+ rv = imapServer->SubscribeToFolder(unicodeName, true,
+ getter_AddRefs(subscribeURI));
+ if (NS_SUCCEEDED(rv) && subscribeURI) {
+ nsCOMPtr<nsIImapUrl> imapSubscribeUrl =
+ do_QueryInterface(subscribeURI);
+ if (imapSubscribeUrl) imapSubscribeUrl->SetExternalLinkUrl(true);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl =
+ do_QueryInterface(subscribeURI);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsIMsgMailSession> mailSession = do_GetService(
+ "@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ if (NS_SUCCEEDED(rv) && msgWindow) {
+ mailnewsUrl->SetMsgWindow(msgWindow);
+ nsCOMPtr<nsIUrlListener> listener =
+ do_QueryInterface(rootFolder);
+ if (listener) mailnewsUrl->RegisterListener(listener);
+ }
+ }
+ }
+ }
+ }
+ // error out this channel, so it'll stop trying to run the url.
+ rv = NS_ERROR_FAILURE;
+ *aRetVal = nullptr;
+ }
+ // this folder exists - check if this is a click on a link to the folder
+ // in which case, we'll select it.
+ else if (!fullFolderName.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> imapFolder;
+ mailnewsUrl->GetFolder(getter_AddRefs(imapFolder));
+ NS_ASSERTION(
+ imapFolder,
+ nsPrintfCString("No folder for imap url: %s", spec.get()).get());
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ if (NS_SUCCEEDED(rv) && msgWindow) {
+ // Clicked IMAP folder URL in the window.
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ obsServ->NotifyObservers(imapFolder, "folder-attention", nullptr);
+ // null out this channel, so it'll stop trying to run the url.
+ *aRetVal = nullptr;
+ rv = NS_OK;
+ } else {
+ // Got IMAP folder URL from command line (most likely).
+ // Set action to nsImapSelectFolder (x-application-imapfolder), so
+ // ::HandleContent will handle it.
+ imapUrl->SetImapAction(nsIImapUrl::nsImapSelectFolder);
+ HandleContent("x-application-imapfolder", nullptr, channel);
+ }
+ }
+ }
+ if (NS_SUCCEEDED(rv)) channel.forget(aRetVal);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::SetDefaultLocalPath(nsIFile* aPath) {
+ NS_ENSURE_ARG_POINTER(aPath);
+
+ return NS_SetPersistentFile(PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP,
+ aPath);
+}
+
+NS_IMETHODIMP nsImapService::GetDefaultLocalPath(nsIFile** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+
+ bool havePref;
+ nsCOMPtr<nsIFile> localFile;
+ nsresult rv = NS_GetPersistentFile(
+ PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP, NS_APP_IMAP_MAIL_50_DIR,
+ havePref, getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(localFile, NS_ERROR_FAILURE);
+
+ bool exists;
+ rv = localFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = localFile->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!havePref || !exists) {
+ rv = NS_SetPersistentFile(PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP,
+ localFile);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to set root dir pref.");
+ }
+
+ localFile.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetServerIID(nsIID** aServerIID) {
+ *aServerIID = new nsIID(NS_GET_IID(nsIImapIncomingServer));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetRequiresUsername(bool* aRequiresUsername) {
+ NS_ENSURE_ARG_POINTER(aRequiresUsername);
+
+ *aRequiresUsername = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetPreflightPrettyNameWithEmailAddress(
+ bool* aPreflightPrettyNameWithEmailAddress) {
+ NS_ENSURE_ARG_POINTER(aPreflightPrettyNameWithEmailAddress);
+
+ *aPreflightPrettyNameWithEmailAddress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetCanLoginAtStartUp(bool* aCanLoginAtStartUp) {
+ NS_ENSURE_ARG_POINTER(aCanLoginAtStartUp);
+ *aCanLoginAtStartUp = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetCanDelete(bool* aCanDelete) {
+ NS_ENSURE_ARG_POINTER(aCanDelete);
+ *aCanDelete = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetCanDuplicate(bool* aCanDuplicate) {
+ NS_ENSURE_ARG_POINTER(aCanDuplicate);
+ *aCanDuplicate = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetCanGetMessages(bool* aCanGetMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetMessages);
+ *aCanGetMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetCanGetIncomingMessages(
+ bool* aCanGetIncomingMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetIncomingMessages);
+ *aCanGetIncomingMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetShowComposeMsgLink(bool* showComposeMsgLink) {
+ NS_ENSURE_ARG_POINTER(showComposeMsgLink);
+ *showComposeMsgLink = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetFoldersCreatedAsync(bool* aAsyncCreation) {
+ NS_ENSURE_ARG_POINTER(aAsyncCreation);
+ *aAsyncCreation = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::GetListOfFoldersWithPath(
+ nsIImapIncomingServer* aServer, nsIMsgWindow* aMsgWindow,
+ const nsACString& folderPath) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(aServer);
+ if (!server) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && rootMsgFolder, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIUrlListener> listener = do_QueryInterface(aServer, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!listener) return NS_ERROR_FAILURE;
+
+ // Locate the folder so that the correct hierarchical delimiter is used in the
+ // folder pathnames, otherwise root's (ie, '^') is used and this is wrong.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ if (rootMsgFolder && !folderPath.IsEmpty()) {
+ // If the folder path contains 'INBOX' of any forms, we need to convert it
+ // to uppercase before finding it under the root folder. We do the same in
+ // PossibleImapMailbox().
+ nsAutoCString tempFolderName(folderPath);
+ nsAutoCString tokenStr, remStr, changedStr;
+ int32_t slashPos = tempFolderName.FindChar('/');
+ if (slashPos > 0) {
+ tokenStr = StringHead(tempFolderName, slashPos);
+ remStr = Substring(tempFolderName, slashPos);
+ } else
+ tokenStr.Assign(tempFolderName);
+
+ if (tokenStr.LowerCaseEqualsLiteral("inbox") &&
+ !tokenStr.EqualsLiteral("INBOX"))
+ changedStr.AppendLiteral("INBOX");
+ else
+ changedStr.Append(tokenStr);
+
+ if (slashPos > 0) changedStr.Append(remStr);
+
+ rv = rootMsgFolder->FindSubFolder(changedStr, getter_AddRefs(msgFolder));
+ }
+ return DiscoverChildren(msgFolder, listener, folderPath);
+}
+
+NS_IMETHODIMP nsImapService::GetListOfFoldersOnServer(
+ nsIImapIncomingServer* aServer, nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(aServer);
+ if (!server) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!rootMsgFolder) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIUrlListener> listener = do_QueryInterface(aServer, &rv);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && listener, NS_ERROR_FAILURE);
+
+ return DiscoverAllAndSubscribedFolders(rootMsgFolder, listener, aMsgWindow);
+}
+
+NS_IMETHODIMP nsImapService::SubscribeFolder(nsIMsgFolder* aFolder,
+ const nsAString& aFolderName,
+ nsIUrlListener* urlListener,
+ nsIURI** url) {
+ return ChangeFolderSubscription(aFolder, aFolderName, "/subscribe>",
+ urlListener, url);
+}
+
+nsresult nsImapService::ChangeFolderSubscription(nsIMsgFolder* folder,
+ const nsAString& folderName,
+ const char* command,
+ nsIUrlListener* urlListener,
+ nsIURI** url) {
+ NS_ENSURE_ARG_POINTER(folder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+ char hierarchyDelimiter = GetHierarchyDelimiter(folder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), folder,
+ urlListener, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ rv = SetImapUrlSink(folder, imapUrl);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl);
+ urlSpec.Append(command);
+ urlSpec.Append(hierarchyDelimiter);
+ // `folderName` contains MUFT-7 or UTF-8 as required by the server here.
+ NS_ConvertUTF16toUTF8 utfFolderName(folderName);
+ nsCString escapedFolderName;
+ MsgEscapeString(utfFolderName, nsINetUtil::ESCAPE_URL_PATH,
+ escapedFolderName);
+ urlSpec.Append(escapedFolderName);
+ rv = mailnewsurl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::UnsubscribeFolder(nsIMsgFolder* aFolder,
+ const nsAString& aFolderName,
+ nsIUrlListener* aUrlListener,
+ nsIURI** aUrl) {
+ return ChangeFolderSubscription(aFolder, aFolderName, "/unsubscribe>",
+ aUrlListener, aUrl);
+}
+
+NS_IMETHODIMP nsImapService::GetFolderAdminUrl(nsIMsgFolder* aImapMailFolder,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolder);
+
+ return FolderCommand(aImapMailFolder, aUrlListener, "/refreshfolderurls>",
+ nsIImapUrl::nsImapRefreshFolderUrls, aMsgWindow, aURL);
+}
+
+NS_IMETHODIMP nsImapService::IssueCommandOnMsgs(nsIMsgFolder* anImapFolder,
+ nsIMsgWindow* aMsgWindow,
+ const nsACString& aCommand,
+ const nsACString& uids,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(anImapFolder);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+ char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ anImapFolder, nullptr, urlSpec, hierarchyDelimiter);
+
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ // nsImapUrl::SetSpec() will set the imap action properly
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapUserDefinedMsgCommand);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ mailNewsUrl->SetMsgWindow(aMsgWindow);
+ mailNewsUrl->SetUpdatingFolder(true);
+ rv = SetImapUrlSink(anImapFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString folderName;
+ GetFolderName(anImapFolder, folderName);
+ urlSpec.Append('/');
+ urlSpec.Append(aCommand);
+ urlSpec.Append('>');
+ urlSpec.Append(uidString);
+ urlSpec.Append('>');
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(uids);
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ } // if we have a url to run....
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::FetchCustomMsgAttribute(
+ nsIMsgFolder* anImapFolder, nsIMsgWindow* aMsgWindow,
+ const nsACString& aAttribute, const nsACString& uids, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(anImapFolder);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+ char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ anImapFolder, nullptr, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ // nsImapUrl::SetSpec() will set the imap action properly
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapUserDefinedFetchAttribute);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ mailNewsUrl->SetMsgWindow(aMsgWindow);
+ mailNewsUrl->SetUpdatingFolder(true);
+ rv = SetImapUrlSink(anImapFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString folderName;
+ GetFolderName(anImapFolder, folderName);
+ urlSpec.AppendLiteral("/customFetch>UID>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(uids);
+ urlSpec.Append('>');
+ urlSpec.Append(aAttribute);
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ } // if we have a url to run....
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::StoreCustomKeywords(
+ nsIMsgFolder* anImapFolder, nsIMsgWindow* aMsgWindow,
+ const nsACString& flagsToAdd, const nsACString& flagsToSubtract,
+ const nsACString& uids, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(anImapFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+ char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl),
+ anImapFolder, nullptr, urlSpec, hierarchyDelimiter);
+
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ // nsImapUrl::SetSpec() will set the imap action properly
+ rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgStoreCustomKeywords);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl);
+ mailNewsUrl->SetMsgWindow(aMsgWindow);
+ mailNewsUrl->SetUpdatingFolder(true);
+ rv = SetImapUrlSink(anImapFolder, imapUrl);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString folderName;
+ GetFolderName(anImapFolder, folderName);
+ urlSpec.AppendLiteral("/customKeywords>UID>");
+ urlSpec.Append(hierarchyDelimiter);
+ urlSpec.Append(folderName);
+ urlSpec.Append('>');
+ urlSpec.Append(uids);
+ urlSpec.Append('>');
+ urlSpec.Append(flagsToAdd);
+ urlSpec.Append('>');
+ urlSpec.Append(flagsToSubtract);
+ rv = mailNewsUrl->SetSpecInternal(urlSpec);
+ if (NS_SUCCEEDED(rv))
+ rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL);
+ }
+ } // if we have a url to run....
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::DownloadMessagesForOffline(
+ const nsACString& messageIds, nsIMsgFolder* aFolder,
+ nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ nsCOMPtr<nsIImapUrl> imapUrl;
+ nsAutoCString urlSpec;
+ nsresult rv;
+ char hierarchyDelimiter = GetHierarchyDelimiter(aFolder);
+ rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aFolder,
+ nullptr, urlSpec, hierarchyDelimiter);
+ if (NS_SUCCEEDED(rv) && imapUrl) {
+ nsCOMPtr<nsIURI> runningURI;
+ // need to pass in stream listener in order to get the channel created
+ // correctly
+ nsCOMPtr<nsIImapMessageSink> imapMessageSink(
+ do_QueryInterface(aFolder, &rv));
+ rv = FetchMessage(imapUrl, nsImapUrl::nsImapMsgDownloadForOffline, aFolder,
+ imapMessageSink, aMsgWindow, nullptr, messageIds, false,
+ getter_AddRefs(runningURI));
+ if (runningURI && aUrlListener) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(runningURI));
+ nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(runningURI));
+ if (msgurl) msgurl->RegisterListener(aUrlListener);
+ if (imapUrl) imapUrl->SetStoreResultsOffline(true);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::MessageURIToMsgHdr(const nsACString& uri,
+ nsIMsgDBHdr** aRetVal) {
+ NS_ENSURE_ARG_POINTER(aRetVal);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsMsgKey msgKey;
+ nsresult rv = DecomposeImapURI(uri, getter_AddRefs(folder), &msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetMessageHeader(msgKey, aRetVal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapService::PlaybackAllOfflineOperations(
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aListener,
+ nsISupports** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsresult rv;
+ nsImapOfflineSync* goOnline = new nsImapOfflineSync();
+ goOnline->Init(aMsgWindow, aListener, nullptr, false);
+ rv = goOnline->QueryInterface(NS_GET_IID(nsISupports), (void**)aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (NS_SUCCEEDED(rv) && *aResult) return goOnline->ProcessNextOperation();
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsImapService::DownloadAllOffineImapFolders(
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aListener) {
+ RefPtr<nsImapOfflineDownloader> downloadForOffline =
+ new nsImapOfflineDownloader(aMsgWindow, aListener);
+ if (downloadForOffline) {
+ // hold reference to this so it won't get deleted out from under itself.
+ nsresult rv = downloadForOffline->ProcessNextOperation();
+ return rv;
+ }
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsImapService::GetCacheStorage(nsICacheStorage** result) {
+ nsresult rv = NS_OK;
+ if (!mCacheStorage) {
+ nsCOMPtr<nsICacheStorageService> cacheStorageService =
+ do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<MailnewsLoadContextInfo> lci =
+ new MailnewsLoadContextInfo(false, false, mozilla::OriginAttributes());
+
+ // Determine if disk cache or memory cache is in use.
+ // Note: This is mozilla system cache, not offline storage (mbox, maildir)
+ // which is also sometimes referred to as cache at places in the code.
+ if (mozilla::Preferences::GetBool("mail.imap.use_disk_cache2", true))
+ rv = cacheStorageService->DiskCacheStorage(lci,
+ getter_AddRefs(mCacheStorage));
+ else
+ rv = cacheStorageService->MemoryCacheStorage(
+ lci, getter_AddRefs(mCacheStorage));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*result = mCacheStorage);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapService::HandleContent(
+ const char* aContentType, nsIInterfaceRequestor* aWindowContext,
+ nsIRequest* request) {
+ NS_ENSURE_ARG_POINTER(request);
+
+ nsresult rv;
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (PL_strcasecmp(aContentType, "x-application-imapfolder") == 0) {
+ nsCOMPtr<nsIURI> uri;
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (uri) {
+ request->Cancel(NS_BINDING_ABORTED);
+ nsCOMPtr<nsIWindowMediator> mediator(
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString uriStr;
+ rv = uri->GetSpec(uriStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // imap uri's are unescaped, so unescape the url.
+ nsCString unescapedUriStr;
+ MsgUnescapeString(uriStr, 0, unescapedUriStr);
+ nsCOMPtr<nsIMessengerWindowService> messengerWindowService =
+ do_GetService("@mozilla.org/messenger/windowservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = messengerWindowService->OpenMessengerWindowWithUri(
+ "mail:3pane", unescapedUriStr, nsMsgKey_None);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // The content-type was not x-application-imapfolder
+ return NS_ERROR_WONT_HANDLE_CONTENT;
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/imap/src/nsImapService.h b/comm/mailnews/imap/src/nsImapService.h
new file mode 100644
index 0000000000..86d38aa949
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapService.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsImapService_h___
+#define nsImapService_h___
+
+#include "nsIImapService.h"
+#include "nsIMsgMessageService.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsString.h"
+#include "nsIProtocolHandler.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIContentHandler.h"
+#include "nsICacheStorage.h"
+
+class nsIImapUrl;
+class nsIMsgFolder;
+class nsIMsgIncomingServer;
+
+/**
+ * nsImapService implements the IMAP protocol.
+ * So, whenever someone opens an "imap://" url, the resultant nsIChannel
+ * is created here (via newChannel()).
+ *
+ * It also provides a bunch of methods to provide more egonomic ways to
+ * initiate IMAP operations, rather than manually composing an "imap://..."
+ * URL. See nsIImapService for these.
+ */
+class nsImapService : public nsIImapService,
+ public nsIMsgMessageService,
+ public nsIMsgMessageFetchPartService,
+ public nsIProtocolHandler,
+ public nsIMsgProtocolInfo,
+ public nsIContentHandler {
+ public:
+ nsImapService();
+ static nsresult NewURI(const nsACString& aSpec,
+ const char* aOriginCharset, // ignored
+ nsIURI* aBaseURI, nsIURI** aRetVal);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGPROTOCOLINFO
+ NS_DECL_NSIIMAPSERVICE
+ NS_DECL_NSIMSGMESSAGESERVICE
+ NS_DECL_NSIPROTOCOLHANDLER
+ NS_DECL_NSIMSGMESSAGEFETCHPARTSERVICE
+ NS_DECL_NSICONTENTHANDLER
+
+ protected:
+ virtual ~nsImapService();
+ char GetHierarchyDelimiter(nsIMsgFolder* aMsgFolder);
+
+ nsresult GetFolderName(nsIMsgFolder* aImapFolder, nsACString& aFolderName);
+
+ // This is called by both FetchMessage and StreamMessage
+ nsresult GetMessageFromUrl(nsIImapUrl* aImapUrl, nsImapAction aImapAction,
+ nsIMsgFolder* aImapMailFolder,
+ nsIImapMessageSink* aImapMessage,
+ nsIMsgWindow* aMsgWindow,
+ nsISupports* aDisplayConsumer,
+ bool aConvertDataToText, nsIURI** aURL);
+
+ nsresult CreateStartOfImapUrl(
+ const nsACString&
+ aImapURI, // a RDF URI for the current message/folder, can be empty
+ nsIImapUrl** imapUrl, nsIMsgFolder* aImapFolder,
+ nsIUrlListener* aUrlListener, nsACString& urlSpec,
+ char& hierarchyDelimiter);
+
+ nsresult GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl,
+ nsISupports* aConsumer, nsIURI** aURL);
+
+ static nsresult SetImapUrlSink(nsIMsgFolder* aMsgFolder,
+ nsIImapUrl* aImapUrl);
+
+ nsresult FetchMimePart(nsIImapUrl* aImapUrl, nsImapAction aImapAction,
+ nsIMsgFolder* aImapMailFolder,
+ nsIImapMessageSink* aImapMessage, nsIURI** aURL,
+ nsISupports* aDisplayConsumer,
+ const nsACString& messageIdentifierList,
+ const nsACString& mimePart);
+
+ nsresult FolderCommand(nsIMsgFolder* imapMailFolder,
+ nsIUrlListener* urlListener, const char* aCommand,
+ nsImapAction imapAction, nsIMsgWindow* msgWindow,
+ nsIURI** url);
+
+ nsresult ChangeFolderSubscription(nsIMsgFolder* folder,
+ const nsAString& folderName,
+ const char* aCommand,
+ nsIUrlListener* urlListener, nsIURI** url);
+
+ nsresult DiddleFlags(nsIMsgFolder* aImapMailFolder,
+ nsIUrlListener* aUrlListener, nsIURI** aURL,
+ const nsACString& messageIdentifierList,
+ const char* howToDiddle, imapMessageFlagsType flags,
+ bool messageIdsAreUID);
+
+ nsresult OfflineAppendFromFile(nsIFile* aFile, nsIURI* aUrl,
+ nsIMsgFolder* aDstFolder,
+ const nsACString& messageId, // to be replaced
+ bool inSelectedState, // needs to be in
+ nsIUrlListener* aListener, nsIURI** aURL,
+ nsISupports* aCopyState);
+
+ static nsresult GetServerFromUrl(nsIImapUrl* aImapUrl,
+ nsIMsgIncomingServer** aServer);
+
+ // just a little helper method...maybe it should be a macro? which helps break
+ // down a imap message uri into the folder and message key equivalents
+ nsresult DecomposeImapURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder, nsACString& msgKey);
+ nsresult DecomposeImapURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder, nsMsgKey* msgKey);
+
+ nsCOMPtr<nsICacheStorage> mCacheStorage;
+};
+
+#endif /* nsImapService_h___ */
diff --git a/comm/mailnews/imap/src/nsImapStringBundle.cpp b/comm/mailnews/imap/src/nsImapStringBundle.cpp
new file mode 100644
index 0000000000..ab20f6e20f
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapStringBundle.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "prprf.h"
+#include "prmem.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIStringBundle.h"
+#include "nsImapStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+
+#define IMAP_MSGS_URL "chrome://messenger/locale/imapMsgs.properties"
+
+extern "C" nsresult IMAPGetStringByName(const char* stringName,
+ char16_t** aString) {
+ nsCOMPtr<nsIStringBundle> sBundle;
+ nsresult rv = IMAPGetStringBundle(getter_AddRefs(sBundle));
+ if (NS_SUCCEEDED(rv) && sBundle) {
+ nsAutoString string;
+ rv = sBundle->GetStringFromName(stringName, string);
+ *aString = ToNewUnicode(string);
+ }
+ return rv;
+}
+
+nsresult IMAPGetStringBundle(nsIStringBundle** aBundle) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (!stringService) return NS_ERROR_NULL_POINTER;
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ rv = stringService->CreateBundle(IMAP_MSGS_URL, getter_AddRefs(stringBundle));
+ stringBundle.forget(aBundle);
+ return rv;
+}
diff --git a/comm/mailnews/imap/src/nsImapStringBundle.h b/comm/mailnews/imap/src/nsImapStringBundle.h
new file mode 100644
index 0000000000..a929391e43
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapStringBundle.h
@@ -0,0 +1,17 @@
+/* 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/. */
+
+#ifndef _nsImapStringBundle_H__
+#define _nsImapStringBundle_H__
+
+#include "nsIStringBundle.h"
+
+PR_BEGIN_EXTERN_C
+
+nsresult IMAPGetStringByName(const char* stringName, char16_t** aString);
+nsresult IMAPGetStringBundle(nsIStringBundle** aBundle);
+
+PR_END_EXTERN_C
+
+#endif /* _nsImapStringBundle_H__ */
diff --git a/comm/mailnews/imap/src/nsImapUndoTxn.cpp b/comm/mailnews/imap/src/nsImapUndoTxn.cpp
new file mode 100644
index 0000000000..04fa74c90a
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUndoTxn.cpp
@@ -0,0 +1,647 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // for precompiled headers
+#include "nsIMsgHdr.h"
+#include "nsImapUndoTxn.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsImapMailFolder.h"
+#include "nsIImapService.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgUtils.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+
+nsImapMoveCopyMsgTxn::nsImapMoveCopyMsgTxn()
+ : m_idsAreUids(false), m_isMove(false), m_srcIsPop3(false) {}
+
+nsresult nsImapMoveCopyMsgTxn::Init(nsIMsgFolder* srcFolder,
+ nsTArray<nsMsgKey>* srcKeyArray,
+ const char* srcMsgIdString,
+ nsIMsgFolder* dstFolder, bool idsAreUids,
+ bool isMove) {
+ m_srcMsgIdString = srcMsgIdString;
+ m_idsAreUids = idsAreUids;
+ m_isMove = isMove;
+ m_srcFolder = do_GetWeakReference(srcFolder);
+ m_dstFolder = do_GetWeakReference(dstFolder);
+ m_srcKeyArray = srcKeyArray->Clone();
+ m_dupKeyArray = srcKeyArray->Clone();
+ nsCString uri;
+ nsresult rv = srcFolder->GetURI(uri);
+ nsCString protocolType(uri);
+ protocolType.SetLength(protocolType.FindChar(':'));
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t i, count = m_srcKeyArray.Length();
+ nsCOMPtr<nsIMsgDBHdr> srcHdr;
+ nsCOMPtr<nsIMsgDBHdr> copySrcHdr;
+ nsCString messageId;
+
+ for (i = 0; i < count; i++) {
+ rv = srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(srcHdr));
+ if (NS_SUCCEEDED(rv)) {
+ // ** jt -- only do this for mailbox protocol
+ if (protocolType.LowerCaseEqualsLiteral("mailbox")) {
+ m_srcIsPop3 = true;
+ uint32_t msgSize;
+ rv = srcHdr->GetMessageSize(&msgSize);
+ if (NS_SUCCEEDED(rv)) m_srcSizeArray.AppendElement(msgSize);
+ if (isMove) {
+ rv = srcDB->CopyHdrFromExistingHdr(nsMsgKey_None, srcHdr, false,
+ getter_AddRefs(copySrcHdr));
+ nsMsgKey pseudoKey = nsMsgKey_None;
+ if (NS_SUCCEEDED(rv)) {
+ copySrcHdr->GetMessageKey(&pseudoKey);
+ m_srcHdrs.AppendObject(copySrcHdr);
+ }
+ m_dupKeyArray[i] = pseudoKey;
+ }
+ }
+ srcHdr->GetMessageId(getter_Copies(messageId));
+ m_srcMessageIds.AppendElement(messageId);
+ }
+ }
+ return nsMsgTxn::Init();
+}
+
+nsImapMoveCopyMsgTxn::~nsImapMoveCopyMsgTxn() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsImapMoveCopyMsgTxn, nsMsgTxn, nsIUrlListener)
+
+NS_IMETHODIMP
+nsImapMoveCopyMsgTxn::UndoTransaction(void) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool finishInOnStopRunningUrl = false;
+
+ if (m_isMove || !m_dstFolder) {
+ if (m_srcIsPop3) {
+ rv = UndoMailboxDelete();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+ nsCOMPtr<nsIUrlListener> srcListener = do_QueryInterface(srcFolder, &rv);
+ if (NS_FAILED(rv)) return rv;
+ m_onStopListener = do_GetWeakReference(srcListener);
+
+ // ** make sure we are in the selected state; use lite select
+ // folder so we won't hit performance hard
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->LiteSelectFolder(srcFolder, srcListener, nullptr,
+ getter_AddRefs(outUri));
+ if (NS_FAILED(rv)) return rv;
+ bool deletedMsgs = true; // default is true unless imapDelete model
+ nsMsgImapDeleteModel deleteModel;
+ rv = GetImapDeleteModel(srcFolder, &deleteModel);
+
+ // protect against a bogus undo txn without any source keys
+ // see bug #179856 for details
+ NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys");
+ if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED;
+
+ if (!m_srcMsgIdString.IsEmpty()) {
+ if (NS_SUCCEEDED(rv) &&
+ deleteModel == nsMsgImapDeleteModels::IMAPDelete)
+ CheckForToggleDelete(srcFolder, m_srcKeyArray[0], &deletedMsgs);
+
+ if (deletedMsgs)
+ rv = imapService->SubtractMessageFlags(
+ srcFolder, this, m_srcMsgIdString, kImapMsgDeletedFlag,
+ m_idsAreUids);
+ else
+ rv = imapService->AddMessageFlags(srcFolder, srcListener,
+ m_srcMsgIdString,
+ kImapMsgDeletedFlag, m_idsAreUids);
+ if (NS_FAILED(rv)) return rv;
+
+ finishInOnStopRunningUrl = true;
+ if (deleteModel != nsMsgImapDeleteModels::IMAPDelete)
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->GetHeaders(srcFolder, srcListener,
+ getter_AddRefs(outUri), m_srcMsgIdString,
+ true);
+ }
+ }
+ }
+ if (!finishInOnStopRunningUrl && !m_dstMsgIdString.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ if (NS_FAILED(rv) || !dstFolder) return rv;
+
+ nsCOMPtr<nsIUrlListener> dstListener;
+
+ dstListener = do_QueryInterface(dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ** make sure we are in the selected state; use lite select folder
+ // so we won't potentially download a bunch of headers.
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr,
+ getter_AddRefs(outUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapService->AddMessageFlags(dstFolder, dstListener, m_dstMsgIdString,
+ kImapMsgDeletedFlag, m_idsAreUids);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMoveCopyMsgTxn::RedoTransaction(void) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_isMove || !m_dstFolder) {
+ if (m_srcIsPop3) {
+ rv = RedoMailboxDelete();
+ if (NS_FAILED(rv)) return rv;
+ } else if (!m_srcMsgIdString.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+ nsCOMPtr<nsIUrlListener> srcListener = do_QueryInterface(srcFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool deletedMsgs = false; // default will be false unless
+ // imapDeleteModel;
+ nsMsgImapDeleteModel deleteModel;
+ rv = GetImapDeleteModel(srcFolder, &deleteModel);
+
+ // protect against a bogus undo txn without any source keys
+ // see bug #179856 for details
+ NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys");
+ if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED;
+
+ if (NS_SUCCEEDED(rv) && deleteModel == nsMsgImapDeleteModels::IMAPDelete)
+ rv = CheckForToggleDelete(srcFolder, m_srcKeyArray[0], &deletedMsgs);
+
+ // Make sure we are in the selected state; use lite select
+ // folder so performance won't suffer.
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->LiteSelectFolder(srcFolder, srcListener, nullptr,
+ getter_AddRefs(outUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (deletedMsgs) {
+ rv = imapService->SubtractMessageFlags(
+ srcFolder, srcListener, m_srcMsgIdString, kImapMsgDeletedFlag,
+ m_idsAreUids);
+ } else {
+ rv = imapService->AddMessageFlags(srcFolder, srcListener,
+ m_srcMsgIdString, kImapMsgDeletedFlag,
+ m_idsAreUids);
+ }
+ }
+ }
+ if (!m_dstMsgIdString.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ if (NS_FAILED(rv) || !dstFolder) return rv;
+
+ nsCOMPtr<nsIUrlListener> dstListener;
+
+ dstListener = do_QueryInterface(dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ** make sure we are in the selected state; use lite select
+ // folder so we won't hit performance hard
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr,
+ getter_AddRefs(outUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapService->SubtractMessageFlags(dstFolder, dstListener,
+ m_dstMsgIdString,
+ kImapMsgDeletedFlag, m_idsAreUids);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgImapDeleteModel deleteModel;
+ rv = GetImapDeleteModel(dstFolder, &deleteModel);
+ if (NS_FAILED(rv) || deleteModel == nsMsgImapDeleteModels::MoveToTrash) {
+ rv = imapService->GetHeaders(dstFolder, dstListener, nullptr,
+ m_dstMsgIdString, true);
+ }
+ }
+ return rv;
+}
+
+nsresult nsImapMoveCopyMsgTxn::SetCopyResponseUid(const char* aMsgIdString) {
+ if (!aMsgIdString) return NS_ERROR_NULL_POINTER;
+ m_dstMsgIdString = aMsgIdString;
+ if (m_dstMsgIdString.Last() == ']') {
+ int32_t len = m_dstMsgIdString.Length();
+ m_dstMsgIdString.SetLength(len - 1);
+ }
+ return NS_OK;
+}
+
+nsresult nsImapMoveCopyMsgTxn::GetSrcKeyArray(nsTArray<nsMsgKey>& srcKeyArray) {
+ srcKeyArray = m_srcKeyArray.Clone();
+ return NS_OK;
+}
+
+nsresult nsImapMoveCopyMsgTxn::AddDstKey(nsMsgKey aKey) {
+ if (!m_dstMsgIdString.IsEmpty()) m_dstMsgIdString.Append(',');
+ m_dstMsgIdString.AppendInt((int32_t)aKey);
+ return NS_OK;
+}
+
+nsresult nsImapMoveCopyMsgTxn::UndoMailboxDelete() {
+ nsresult rv = NS_ERROR_FAILURE;
+ // ** jt -- only do this for mailbox protocol
+ if (m_srcIsPop3) {
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ if (NS_FAILED(rv) || !dstFolder) return rv;
+
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgDatabase> dstDB;
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (NS_FAILED(rv)) return rv;
+ rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
+ if (NS_FAILED(rv)) return rv;
+
+ uint32_t count = m_srcKeyArray.Length();
+ uint32_t i;
+ nsCOMPtr<nsIMsgDBHdr> oldHdr;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ for (i = 0; i < count; i++) {
+ oldHdr = m_srcHdrs[i];
+ NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header");
+ rv = srcDB->CopyHdrFromExistingHdr(m_srcKeyArray[i], oldHdr, true,
+ getter_AddRefs(newHdr));
+ NS_ASSERTION(newHdr, "fatal ... cannot create new header");
+
+ if (NS_SUCCEEDED(rv) && newHdr) {
+ if (i < m_srcSizeArray.Length())
+ newHdr->SetMessageSize(m_srcSizeArray[i]);
+ srcDB->UndoDelete(newHdr);
+ }
+ }
+ srcDB->SetSummaryValid(true);
+ return NS_OK; // always return NS_OK
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsImapMoveCopyMsgTxn::RedoMailboxDelete() {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (m_srcIsPop3) {
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (NS_SUCCEEDED(rv)) {
+ srcDB->DeleteMessages(m_srcKeyArray, nullptr);
+ srcDB->SetSummaryValid(true);
+ }
+ return NS_OK; // always return NS_OK
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsImapMoveCopyMsgTxn::GetImapDeleteModel(
+ nsIMsgFolder* aFolder, nsMsgImapDeleteModel* aDeleteModel) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (!aFolder) return NS_ERROR_NULL_POINTER;
+ rv = aFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv);
+ if (NS_SUCCEEDED(rv) && imapServer)
+ rv = imapServer->GetDeleteModel(aDeleteModel);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapMoveCopyMsgTxn::OnStartRunningUrl(nsIURI* aUrl) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMoveCopyMsgTxn::OnStopRunningUrl(nsIURI* aUrl,
+ nsresult aExitCode) {
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryReferent(m_onStopListener);
+ if (urlListener) urlListener->OnStopRunningUrl(aUrl, aExitCode);
+
+ nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl);
+ if (imapUrl) {
+ nsresult rv;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsImapAction imapAction;
+ imapUrl->GetImapAction(&imapAction);
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (imapAction == nsIImapUrl::nsImapSubtractMsgFlags) {
+ int32_t extraStatus;
+ imapUrl->GetExtraStatus(&extraStatus);
+ if (extraStatus != nsIImapUrl::ImapStatusNone) {
+ // If subtracting the deleted flag didn't work, try
+ // moving the message back from the target folder to the src folder
+ if (!m_dstMsgIdString.IsEmpty())
+ imapService->OnlineMessageCopy(dstFolder, m_dstMsgIdString, srcFolder,
+ true, true, nullptr, /* listener */
+ nullptr, nullptr, nullptr);
+ else {
+ // server doesn't support COPYUID, so we're going to update the dest
+ // folder, and when that's done, use the db to find the messages
+ // to move back, looking them up by message-id.
+ nsCOMPtr<nsIMsgImapMailFolder> imapDest =
+ do_QueryInterface(dstFolder);
+ if (imapDest) imapDest->UpdateFolderWithListener(nullptr, this);
+ }
+ } else if (!m_dstMsgIdString.IsEmpty()) {
+ nsCOMPtr<nsIUrlListener> dstListener;
+
+ dstListener = do_QueryInterface(dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ** make sure we are in the selected state; use lite select folder
+ // so we won't potentially download a bunch of headers.
+ nsCOMPtr<nsIURI> outUri;
+ rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr,
+ getter_AddRefs(outUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = imapService->AddMessageFlags(dstFolder, dstListener,
+ m_dstMsgIdString, kImapMsgDeletedFlag,
+ m_idsAreUids);
+ }
+ } else if (imapAction == nsIImapUrl::nsImapSelectFolder) {
+ // Now we should have the headers from the dest folder.
+ // Look them up and move them back to the source folder.
+ uint32_t count = m_srcMessageIds.Length();
+ uint32_t i;
+ nsCString messageId;
+ nsTArray<nsMsgKey> dstKeys;
+ nsCOMPtr<nsIMsgDatabase> destDB;
+ nsCOMPtr<nsIMsgDBHdr> dstHdr;
+
+ rv = dstFolder->GetMsgDatabase(getter_AddRefs(destDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (i = 0; i < count; i++) {
+ rv = destDB->GetMsgHdrForMessageID(m_srcMessageIds[i].get(),
+ getter_AddRefs(dstHdr));
+ if (NS_SUCCEEDED(rv) && dstHdr) {
+ nsMsgKey dstKey;
+ dstHdr->GetMessageKey(&dstKey);
+ dstKeys.AppendElement(dstKey);
+ }
+ }
+ if (dstKeys.Length()) {
+ nsAutoCString uids;
+ nsImapMailFolder::AllocateUidStringFromKeys(dstKeys, uids);
+ rv = imapService->OnlineMessageCopy(dstFolder, uids, srcFolder, true,
+ true, nullptr, nullptr, nullptr,
+ nullptr);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsImapOfflineTxn::nsImapOfflineTxn(nsIMsgFolder* srcFolder,
+ nsTArray<nsMsgKey>* srcKeyArray,
+ const char* srcMsgIdString,
+ nsIMsgFolder* dstFolder, bool isMove,
+ nsOfflineImapOperationType opType,
+ nsCOMArray<nsIMsgDBHdr>& srcHdrs) {
+ Init(srcFolder, srcKeyArray, srcMsgIdString, dstFolder, true, isMove);
+
+ m_opType = opType;
+ m_flags = 0;
+ m_addFlags = false;
+ if (opType == nsIMsgOfflineImapOperation::kDeletedMsg) {
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+
+ nsresult rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(srcDB));
+ if (NS_SUCCEEDED(rv) && srcDB) {
+ nsMsgKey pseudoKey;
+ nsCOMPtr<nsIMsgDBHdr> copySrcHdr;
+
+ // Imap protocols have conflated key/UUID so we cannot use
+ // auto key with them.
+ nsCString protocolType;
+ srcFolder->GetURI(protocolType);
+ protocolType.SetLength(protocolType.FindChar(':'));
+ for (int32_t i = 0; i < srcHdrs.Count(); i++) {
+ if (protocolType.EqualsLiteral("imap")) {
+ srcDB->GetNextPseudoMsgKey(&pseudoKey);
+ pseudoKey--;
+ } else {
+ pseudoKey = nsMsgKey_None;
+ }
+ rv = srcDB->CopyHdrFromExistingHdr(pseudoKey, srcHdrs[i], false,
+ getter_AddRefs(copySrcHdr));
+ if (NS_SUCCEEDED(rv)) {
+ copySrcHdr->GetMessageKey(&pseudoKey);
+ m_srcHdrs.AppendObject(copySrcHdr);
+ }
+ m_dupKeyArray[i] = pseudoKey;
+ }
+ }
+ } else
+ m_srcHdrs.AppendObjects(srcHdrs);
+}
+
+nsImapOfflineTxn::~nsImapOfflineTxn() {}
+
+// Open the database and find the key for the offline operation that we want to
+// undo, then remove it from the database, we also hold on to this
+// data for a redo operation.
+NS_IMETHODIMP nsImapOfflineTxn::UndoTransaction(void) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgDatabase> destDB;
+
+ rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(srcDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (m_opType) {
+ case nsIMsgOfflineImapOperation::kMsgMoved:
+ case nsIMsgOfflineImapOperation::kMsgCopy:
+ case nsIMsgOfflineImapOperation::kAddedHeader:
+ case nsIMsgOfflineImapOperation::kFlagsChanged:
+ case nsIMsgOfflineImapOperation::kDeletedMsg: {
+ if (m_srcHdrs.IsEmpty()) {
+ NS_ASSERTION(false, "No msg header to apply undo.");
+ break;
+ }
+ nsCOMPtr<nsIMsgDBHdr> firstHdr = m_srcHdrs[0];
+ nsMsgKey hdrKey;
+ firstHdr->GetMessageKey(&hdrKey);
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = opsDb->GetOfflineOpForKey(hdrKey, false, getter_AddRefs(op));
+ bool offlineOpPlayedBack = true;
+ if (NS_SUCCEEDED(rv) && op) {
+ op->GetPlayingBack(&offlineOpPlayedBack);
+ opsDb->RemoveOfflineOp(op);
+ op = nullptr;
+ }
+ if (!WeAreOffline() && offlineOpPlayedBack) {
+ // couldn't find offline op - it must have been played back already
+ // so we should undo the transaction online.
+ return nsImapMoveCopyMsgTxn::UndoTransaction();
+ }
+
+ if (!firstHdr) break;
+ nsMsgKey msgKey;
+ if (m_opType == nsIMsgOfflineImapOperation::kAddedHeader) {
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ nsCOMPtr<nsIMsgDBHdr> mailHdr;
+ rv = srcDB->GetMsgHdrForKey(msgKey, getter_AddRefs(mailHdr));
+ if (mailHdr) srcDB->DeleteHeader(mailHdr, nullptr, false, false);
+ }
+ srcDB->Commit(true);
+ } else if (m_opType == nsIMsgOfflineImapOperation::kDeletedMsg) {
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> undeletedHdr = m_srcHdrs[i];
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ if (undeletedHdr) {
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ srcDB->CopyHdrFromExistingHdr(msgKey, undeletedHdr, true,
+ getter_AddRefs(newHdr));
+ }
+ }
+ srcDB->Close(true);
+ srcFolder->SummaryChanged();
+ }
+ break;
+ }
+ case nsIMsgOfflineImapOperation::kMsgMarkedDeleted: {
+ nsMsgKey msgKey;
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ srcDB->MarkImapDeleted(msgKey, false, nullptr);
+ }
+ } break;
+ default:
+ break;
+ }
+ srcDB->Close(true);
+ srcFolder->SummaryChanged();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapOfflineTxn::RedoTransaction(void) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ if (NS_FAILED(rv) || !srcFolder) return rv;
+ nsCOMPtr<nsIMsgOfflineImapOperation> op;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgDatabase> destDB;
+ rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(srcDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (m_opType) {
+ case nsIMsgOfflineImapOperation::kMsgMoved:
+ case nsIMsgOfflineImapOperation::kMsgCopy: {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsMsgKey hdrKey;
+ m_srcHdrs[i]->GetMessageKey(&hdrKey);
+ rv = opsDb->GetOfflineOpForKey(hdrKey, false, getter_AddRefs(op));
+ if (NS_SUCCEEDED(rv) && op) {
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ if (dstFolder) {
+ nsCString folderURI;
+ dstFolder->GetURI(folderURI);
+
+ if (m_opType == nsIMsgOfflineImapOperation::kMsgMoved)
+ op->SetDestinationFolderURI(folderURI); // offline move
+ if (m_opType == nsIMsgOfflineImapOperation::kMsgCopy) {
+ op->SetOperation(nsIMsgOfflineImapOperation::kMsgMoved);
+ op->AddMessageCopyOperation(folderURI); // offline copy
+ }
+ dstFolder->SummaryChanged();
+ }
+ } else if (!WeAreOffline()) {
+ // couldn't find offline op - it must have been played back already
+ // so we should redo the transaction online.
+ return nsImapMoveCopyMsgTxn::RedoTransaction();
+ }
+ }
+ break;
+ }
+ case nsIMsgOfflineImapOperation::kAddedHeader: {
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(destDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(destDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> restoreHdr;
+ nsMsgKey msgKey;
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ destDB->CopyHdrFromExistingHdr(msgKey, m_srcHdrs[i], true,
+ getter_AddRefs(restoreHdr));
+ rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op));
+ if (NS_SUCCEEDED(rv) && op) {
+ nsCString folderURI;
+ srcFolder->GetURI(folderURI);
+ op->SetSourceFolderURI(folderURI);
+ }
+ }
+ dstFolder->SummaryChanged();
+ destDB->Close(true);
+ } break;
+ case nsIMsgOfflineImapOperation::kDeletedMsg:
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsMsgKey msgKey;
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ srcDB->DeleteMessage(msgKey, nullptr, true);
+ }
+ break;
+ case nsIMsgOfflineImapOperation::kMsgMarkedDeleted:
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsMsgKey msgKey;
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ srcDB->MarkImapDeleted(msgKey, true, nullptr);
+ }
+ break;
+ case nsIMsgOfflineImapOperation::kFlagsChanged: {
+ nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t i = 0; i < m_srcHdrs.Count(); i++) {
+ nsMsgKey msgKey;
+ m_srcHdrs[i]->GetMessageKey(&msgKey);
+ rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op));
+ if (NS_SUCCEEDED(rv) && op) {
+ imapMessageFlagsType newMsgFlags;
+ op->GetNewFlags(&newMsgFlags);
+ if (m_addFlags)
+ op->SetFlagOperation(newMsgFlags | m_flags);
+ else
+ op->SetFlagOperation(newMsgFlags & ~m_flags);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ srcDB->Close(true);
+ srcDB = nullptr;
+ srcFolder->SummaryChanged();
+ return NS_OK;
+}
diff --git a/comm/mailnews/imap/src/nsImapUndoTxn.h b/comm/mailnews/imap/src/nsImapUndoTxn.h
new file mode 100644
index 0000000000..9b0f2b30f9
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUndoTxn.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImapUndoTxn_h__
+#define nsImapUndoTxn_h__
+
+#include "mozilla/Attributes.h"
+#include "nsIMsgFolder.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIUrlListener.h"
+#include "nsMsgTxn.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMsgOfflineImapOperation.h"
+#include "nsCOMPtr.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsCOMArray.h"
+
+class nsImapMoveCopyMsgTxn : public nsMsgTxn, nsIUrlListener {
+ public:
+ nsImapMoveCopyMsgTxn();
+ nsImapMoveCopyMsgTxn(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray,
+ const char* srcMsgIdString, nsIMsgFolder* dstFolder,
+ bool isMove);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIURLLISTENER
+
+ NS_IMETHOD UndoTransaction(void) override;
+ NS_IMETHOD RedoTransaction(void) override;
+
+ // helper
+ nsresult SetCopyResponseUid(const char* msgIdString);
+ nsresult GetSrcKeyArray(nsTArray<nsMsgKey>& srcKeyArray);
+ void GetSrcMsgIds(nsCString& srcMsgIds) { srcMsgIds = m_srcMsgIdString; }
+ nsresult AddDstKey(nsMsgKey aKey);
+ nsresult UndoMailboxDelete();
+ nsresult RedoMailboxDelete();
+ nsresult Init(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray,
+ const char* srcMsgIdString, nsIMsgFolder* dstFolder,
+ bool idsAreUids, bool isMove);
+
+ protected:
+ virtual ~nsImapMoveCopyMsgTxn();
+
+ nsWeakPtr m_srcFolder;
+ nsCOMArray<nsIMsgDBHdr> m_srcHdrs;
+ nsTArray<nsMsgKey> m_dupKeyArray;
+ nsTArray<nsMsgKey> m_srcKeyArray;
+ nsTArray<nsCString> m_srcMessageIds;
+ nsCString m_srcMsgIdString;
+ nsWeakPtr m_dstFolder;
+ nsCString m_dstMsgIdString;
+ bool m_idsAreUids;
+ bool m_isMove;
+ bool m_srcIsPop3;
+ nsTArray<uint32_t> m_srcSizeArray;
+ // this is used when we chain urls for imap undo, since "this" needs
+ // to be the listener, but the folder may need to also be notified.
+ nsWeakPtr m_onStopListener;
+
+ nsresult GetImapDeleteModel(nsIMsgFolder* aFolder,
+ nsMsgImapDeleteModel* aDeleteModel);
+};
+
+class nsImapOfflineTxn : public nsImapMoveCopyMsgTxn {
+ public:
+ nsImapOfflineTxn(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray,
+ const char* srcMsgIdString, nsIMsgFolder* dstFolder,
+ bool isMove, nsOfflineImapOperationType opType,
+ nsCOMArray<nsIMsgDBHdr>& srcHdrs);
+
+ NS_IMETHOD UndoTransaction(void) override;
+ NS_IMETHOD RedoTransaction(void) override;
+ void SetAddFlags(bool addFlags) { m_addFlags = addFlags; }
+ void SetFlags(uint32_t flags) { m_flags = flags; }
+
+ protected:
+ virtual ~nsImapOfflineTxn();
+ nsOfflineImapOperationType m_opType;
+ // these two are used to undo flag changes, which we don't currently do.
+ bool m_addFlags;
+ uint32_t m_flags;
+};
+#endif
diff --git a/comm/mailnews/imap/src/nsImapUrl.cpp b/comm/mailnews/imap/src/nsImapUrl.cpp
new file mode 100644
index 0000000000..4460b5ab05
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUrl.cpp
@@ -0,0 +1,1276 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsImapUrl.h"
+#include "../public/nsIImapHostSessionList.h"
+#include "nsThreadUtils.h"
+#include "nsString.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prprf.h"
+#include "nsMemory.h"
+#include "nsCOMPtr.h"
+#include "nsImapUtils.h"
+#include "nsIImapMockChannel.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapMessageSink.h"
+#include "nsIImapServerSink.h"
+#include "nsImapNamespace.h"
+#include "nsICacheEntry.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgHdr.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+extern LazyLogModule IMAPCache; // defined in nsImapProtocol.cpp
+
+#define NS_IIMAPHOSTSESSIONLIST_CID \
+ { \
+ 0x479ce8fc, 0xe725, 0x11d2, { \
+ 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \
+ } \
+ }
+static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID);
+
+nsImapUrl::nsImapUrl() : mLock("nsImapUrl.mLock") {
+ m_listOfMessageIds = nullptr;
+ m_sourceCanonicalFolderPathSubString = nullptr;
+ m_destinationCanonicalFolderPathSubString = nullptr;
+ m_listOfMessageIds = nullptr;
+ m_tokenPlaceHolder = nullptr;
+ m_urlidSubString = nullptr;
+ m_searchCriteriaString = nullptr;
+ m_idsAreUids = false;
+ m_mimePartSelectorDetected = false;
+ m_msgLoadingFromCache = false;
+ m_storeResultsOffline = false;
+ m_storeOfflineOnFallback = false;
+ m_localFetchOnly = false;
+ m_rerunningUrl = false;
+ m_moreHeadersToDownload = false;
+ m_externalLinkUrl = true; // we'll start this at true, and set it false in
+ // nsImapService::CreateStartOfImapUrl
+ m_numBytesToFetch = 0;
+ m_validUrl = true; // assume the best.
+ m_runningUrl = false;
+ m_flags = 0;
+ m_extraStatus = ImapStatusNone;
+ m_onlineSubDirSeparator = '/';
+ m_imapAction = 0;
+ mAutodetectCharset = false;
+
+ // ** jt - the following are not ref counted
+ m_copyState = nullptr;
+ m_file = nullptr;
+ m_imapMailFolderSink = nullptr;
+ m_imapMessageSink = nullptr;
+ m_addDummyEnvelope = false;
+ m_canonicalLineEnding = false;
+}
+
+nsImapUrl::~nsImapUrl() {
+ PR_FREEIF(m_listOfMessageIds);
+ PR_FREEIF(m_destinationCanonicalFolderPathSubString);
+ PR_FREEIF(m_sourceCanonicalFolderPathSubString);
+ PR_FREEIF(m_searchCriteriaString);
+}
+
+NS_IMPL_ADDREF_INHERITED(nsImapUrl, nsMsgMailNewsUrl)
+
+NS_IMPL_RELEASE_INHERITED(nsImapUrl, nsMsgMailNewsUrl)
+
+NS_INTERFACE_MAP_BEGIN(nsImapUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIImapUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl)
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIImapUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsImapUrl::GetRequiredImapState(nsImapState* aImapUrlState) {
+ if (aImapUrlState) {
+ // the imap action determines the state we must be in...check the
+ // the imap action.
+
+ if (m_imapAction & 0x10000000)
+ *aImapUrlState = nsImapSelectedState;
+ else
+ *aImapUrlState = nsImapAuthenticatedState;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetImapAction(nsImapAction* aImapAction) {
+ *aImapAction = m_imapAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetImapAction(nsImapAction aImapAction) {
+ m_imapAction = aImapAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_ENSURE_ARG_POINTER(m_imapFolder);
+
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_imapFolder);
+ folder.forget(aMsgFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetFolder(nsIMsgFolder* aMsgFolder) {
+ nsresult rv;
+ m_imapFolder = do_GetWeakReference(aMsgFolder, &rv);
+ if (aMsgFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ aMsgFolder->GetServer(getter_AddRefs(incomingServer));
+ if (incomingServer) incomingServer->GetKey(m_serverKey);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImapUrl::GetImapMailFolderSink(
+ nsIImapMailFolderSink** aImapMailFolderSink) {
+ NS_ENSURE_ARG_POINTER(aImapMailFolderSink);
+ if (!m_imapMailFolderSink)
+ return NS_ERROR_NULL_POINTER; // no assert, so don't use NS_ENSURE_POINTER.
+
+ nsCOMPtr<nsIImapMailFolderSink> folderSink =
+ do_QueryReferent(m_imapMailFolderSink);
+ folderSink.forget(aImapMailFolderSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetImapMailFolderSink(
+ nsIImapMailFolderSink* aImapMailFolderSink) {
+ nsresult rv;
+ m_imapMailFolderSink = do_GetWeakReference(aImapMailFolderSink, &rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapUrl::GetImapMessageSink(
+ nsIImapMessageSink** aImapMessageSink) {
+ NS_ENSURE_ARG_POINTER(aImapMessageSink);
+ NS_ENSURE_ARG_POINTER(m_imapMessageSink);
+
+ nsCOMPtr<nsIImapMessageSink> messageSink =
+ do_QueryReferent(m_imapMessageSink);
+ messageSink.forget(aImapMessageSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetImapMessageSink(
+ nsIImapMessageSink* aImapMessageSink) {
+ nsresult rv;
+ m_imapMessageSink = do_GetWeakReference(aImapMessageSink, &rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapUrl::GetImapServerSink(
+ nsIImapServerSink** aImapServerSink) {
+ NS_ENSURE_ARG_POINTER(aImapServerSink);
+ NS_ENSURE_ARG_POINTER(m_imapServerSink);
+
+ nsCOMPtr<nsIImapServerSink> serverSink = do_QueryReferent(m_imapServerSink);
+ serverSink.forget(aImapServerSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetImapServerSink(nsIImapServerSink* aImapServerSink) {
+ nsresult rv;
+ m_imapServerSink = do_GetWeakReference(aImapServerSink, &rv);
+ return rv;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// End nsIImapUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsImapUrl::SetSpecInternal(const nsACString& aSpec) {
+ nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(aSpec);
+ if (NS_SUCCEEDED(rv)) {
+ m_validUrl = true; // assume the best.
+ rv = ParseUrl();
+ }
+ return rv;
+}
+
+nsresult nsImapUrl::SetQuery(const nsACString& aQuery) {
+ nsresult rv = nsMsgMailNewsUrl::SetQuery(aQuery);
+ if (NS_SUCCEEDED(rv)) rv = ParseUrl();
+ return rv;
+}
+
+nsresult nsImapUrl::ParseUrl() {
+ nsresult rv = NS_OK;
+ // extract the user name
+ GetUserPass(m_userName);
+
+ nsAutoCString imapPartOfUrl;
+ rv = GetPathQueryRef(imapPartOfUrl);
+ nsAutoCString unescapedImapPartOfUrl;
+ MsgUnescapeString(imapPartOfUrl, 0, unescapedImapPartOfUrl);
+ if (NS_SUCCEEDED(rv) && !unescapedImapPartOfUrl.IsEmpty()) {
+ ParseImapPart(unescapedImapPartOfUrl.BeginWriting() +
+ 1); // GetPath leaves leading '/' in the path!!!
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::CreateSearchCriteriaString(char** aResult) {
+ // this method should only be called from the imap thread...
+ // o.t. add lock protection..
+ if (nullptr == aResult || !m_searchCriteriaString)
+ return NS_ERROR_NULL_POINTER;
+ *aResult = strdup(m_searchCriteriaString);
+ return NS_OK;
+}
+
+// this method gets called from the UI thread and the imap thread
+NS_IMETHODIMP nsImapUrl::GetListOfMessageIds(nsACString& aResult) {
+ MutexAutoLock mon(mLock);
+ if (!m_listOfMessageIds) return NS_ERROR_NULL_POINTER;
+
+ int32_t bytesToCopy = strlen(m_listOfMessageIds);
+
+ // mime may have glommed a "&part=" for a part download
+ // we return the entire message and let mime extract
+ // the part. Pop and news work this way also.
+ // this algorithm truncates the "&part" string.
+ char* currentChar = m_listOfMessageIds;
+ while (*currentChar && (*currentChar != '?')) currentChar++;
+ if (*currentChar == '?') bytesToCopy = currentChar - m_listOfMessageIds;
+
+ // we should also strip off anything after "/;section="
+ // since that can specify an IMAP MIME part
+ char* wherePart = PL_strstr(m_listOfMessageIds, "/;section=");
+ if (wherePart)
+ bytesToCopy =
+ std::min(bytesToCopy, int32_t(wherePart - m_listOfMessageIds));
+
+ aResult.Assign(m_listOfMessageIds, bytesToCopy);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCommand(nsACString& result) {
+ result = m_command;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCustomAttributeToFetch(nsACString& result) {
+ result = m_msgFetchAttribute;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCustomAttributeResult(nsACString& result) {
+ result = m_customAttributeResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetCustomAttributeResult(const nsACString& result) {
+ m_customAttributeResult = result;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCustomCommandResult(nsACString& result) {
+ result = m_customCommandResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetCustomCommandResult(const nsACString& result) {
+ m_customCommandResult = result;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCustomAddFlags(nsACString& aResult) {
+ aResult = m_customAddFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetCustomSubtractFlags(nsACString& aResult) {
+ aResult = m_customSubtractFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetImapPartToFetch(char** result) {
+ // here's the old code....
+
+ // unfortunately an imap part can have the form: /;section= OR
+ // it can have the form ?section=. We need to look for both.
+ if (m_listOfMessageIds) {
+ char* wherepart = PL_strstr(m_listOfMessageIds, ";section=");
+ if (!wherepart) // look for ?section too....
+ wherepart = PL_strstr(m_listOfMessageIds, "?section=");
+ if (wherepart) {
+ wherepart += 9; // strlen("/;section=")
+ char* wherelibmimepart = PL_strstr(wherepart, "&part=");
+ if (!wherelibmimepart) wherelibmimepart = PL_strstr(wherepart, "?part=");
+ int numCharsToCopy = (wherelibmimepart)
+ ? wherelibmimepart - wherepart
+ : PL_strlen(m_listOfMessageIds) -
+ (wherepart - m_listOfMessageIds);
+ if (numCharsToCopy) {
+ *result = (char*)PR_Malloc(sizeof(char) * (numCharsToCopy + 1));
+ if (*result) {
+ PL_strncpy(*result, wherepart, numCharsToCopy + 1); // appends a \0
+ (*result)[numCharsToCopy] = '\0';
+ }
+ }
+ } // if we got a wherepart
+ } // if we got a m_listOfMessageIds
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetOnlineSubDirSeparator(char* separator) {
+ if (separator) {
+ *separator = m_onlineSubDirSeparator;
+ return NS_OK;
+ }
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsImapUrl::GetNumBytesToFetch(int32_t* aNumBytesToFetch) {
+ NS_ENSURE_ARG_POINTER(aNumBytesToFetch);
+ *aNumBytesToFetch = m_numBytesToFetch;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapUrl::SetOnlineSubDirSeparator(char onlineDirSeparator) {
+ m_onlineSubDirSeparator = onlineDirSeparator;
+ return NS_OK;
+}
+
+// this method is only called from the imap thread
+NS_IMETHODIMP nsImapUrl::MessageIdsAreUids(bool* result) {
+ *result = m_idsAreUids;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapUrl::SetExtraStatus(int32_t aExtraStatus) {
+ m_extraStatus = aExtraStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetExtraStatus(int32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_extraStatus;
+ return NS_OK;
+}
+
+// this method is only called from the imap thread
+NS_IMETHODIMP nsImapUrl::GetMsgFlags(
+ imapMessageFlagsType* result) // kAddMsgFlags or kSubtractMsgFlags only
+{
+ *result = m_flags;
+ return NS_OK;
+}
+
+void nsImapUrl::ParseImapPart(char* imapPartOfUrl) {
+ m_tokenPlaceHolder = imapPartOfUrl;
+ m_urlidSubString = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)NULL;
+
+ if (!m_urlidSubString) {
+ m_validUrl = false;
+ return;
+ }
+
+ if (!PL_strcasecmp(m_urlidSubString, "fetch")) {
+ m_imapAction = nsImapMsgFetch;
+ ParseUidChoice();
+ PR_FREEIF(m_sourceCanonicalFolderPathSubString);
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ // if fetched by spam filter, the action will be changed to
+ // nsImapMsgFetchPeek
+ } else {
+ if (!PL_strcasecmp(m_urlidSubString, "header")) {
+ m_imapAction = nsImapMsgHeader;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ } else if (!PL_strcasecmp(m_urlidSubString, "customFetch")) {
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseCustomMsgFetchAttribute();
+ } else if (!PL_strcasecmp(m_urlidSubString, "previewBody")) {
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseNumBytes();
+ } else if (!PL_strcasecmp(m_urlidSubString, "deletemsg")) {
+ m_imapAction = nsImapDeleteMsg;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ } else if (!PL_strcasecmp(m_urlidSubString, "uidexpunge")) {
+ m_imapAction = nsImapUidExpunge;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ } else if (!PL_strcasecmp(m_urlidSubString, "deleteallmsgs")) {
+ m_imapAction = nsImapDeleteAllMsgs;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "addmsgflags")) {
+ m_imapAction = nsImapAddMsgFlags;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseMsgFlags();
+ } else if (!PL_strcasecmp(m_urlidSubString, "subtractmsgflags")) {
+ m_imapAction = nsImapSubtractMsgFlags;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseMsgFlags();
+ } else if (!PL_strcasecmp(m_urlidSubString, "setmsgflags")) {
+ m_imapAction = nsImapSetMsgFlags;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseMsgFlags();
+ } else if (!PL_strcasecmp(m_urlidSubString, "onlinecopy")) {
+ m_imapAction = nsImapOnlineCopy;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "onlinemove")) {
+ m_imapAction = nsImapOnlineMove;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "onlinetoofflinecopy")) {
+ m_imapAction = nsImapOnlineToOfflineCopy;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "onlinetoofflinemove")) {
+ m_imapAction = nsImapOnlineToOfflineMove;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "offlinetoonlinecopy")) {
+ m_imapAction = nsImapOfflineToOnlineMove;
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "search")) {
+ m_imapAction = nsImapSearch;
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseSearchCriteriaString();
+ } else if (!PL_strcasecmp(m_urlidSubString, "test")) {
+ m_imapAction = nsImapTest;
+ } else if (!PL_strcasecmp(m_urlidSubString, "select")) {
+ m_imapAction = nsImapSelectFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ if (m_tokenPlaceHolder && *m_tokenPlaceHolder)
+ ParseListOfMessageIds();
+ else
+ m_listOfMessageIds = PL_strdup("");
+ } else if (!PL_strcasecmp(m_urlidSubString, "liteselect")) {
+ m_imapAction = nsImapLiteSelectFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "selectnoop")) {
+ m_imapAction = nsImapSelectNoopFolder;
+ m_listOfMessageIds = PL_strdup("");
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "expunge")) {
+ m_imapAction = nsImapExpungeFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ m_listOfMessageIds = PL_strdup(""); // no ids to UNDO
+ } else if (!PL_strcasecmp(m_urlidSubString, "create")) {
+ m_imapAction = nsImapCreateFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "ensureExists")) {
+ m_imapAction = nsImapEnsureExistsFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "discoverchildren")) {
+ m_imapAction = nsImapDiscoverChildrenUrl;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "discoverallboxes")) {
+ m_imapAction = nsImapDiscoverAllBoxesUrl;
+ } else if (!PL_strcasecmp(m_urlidSubString,
+ "discoverallandsubscribedboxes")) {
+ m_imapAction = nsImapDiscoverAllAndSubscribedBoxesUrl;
+ } else if (!PL_strcasecmp(m_urlidSubString, "delete")) {
+ m_imapAction = nsImapDeleteFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "deletefolder")) {
+ m_imapAction = nsImapDeleteFolderAndMsgs;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "rename")) {
+ m_imapAction = nsImapRenameFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "movefolderhierarchy")) {
+ m_imapAction = nsImapMoveFolderHierarchy;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ if (m_tokenPlaceHolder && *m_tokenPlaceHolder) // handle promote to root
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "list")) {
+ m_imapAction = nsImapLsubFolders;
+ ParseFolderPath(&m_destinationCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "biff")) {
+ m_imapAction = nsImapBiff;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ } else if (!PL_strcasecmp(m_urlidSubString, "netscape")) {
+ m_imapAction = nsImapGetMailAccountUrl;
+ } else if (!PL_strcasecmp(m_urlidSubString, "appendmsgfromfile")) {
+ m_imapAction = nsImapAppendMsgFromFile;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "appenddraftfromfile")) {
+ m_imapAction = nsImapAppendDraftFromFile;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseUidChoice();
+ if (m_tokenPlaceHolder && *m_tokenPlaceHolder)
+ ParseListOfMessageIds();
+ else
+ m_listOfMessageIds = strdup("");
+ } else if (!PL_strcasecmp(m_urlidSubString, "subscribe")) {
+ m_imapAction = nsImapSubscribe;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "unsubscribe")) {
+ m_imapAction = nsImapUnsubscribe;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "refreshacl")) {
+ m_imapAction = nsImapRefreshACL;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "refreshfolderurls")) {
+ m_imapAction = nsImapRefreshFolderUrls;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "refreshallacls")) {
+ m_imapAction = nsImapRefreshAllACLs;
+ } else if (!PL_strcasecmp(m_urlidSubString, "listfolder")) {
+ m_imapAction = nsImapListFolder;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "upgradetosubscription")) {
+ m_imapAction = nsImapUpgradeToSubscription;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "folderstatus")) {
+ m_imapAction = nsImapFolderStatus;
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ } else if (!PL_strcasecmp(m_urlidSubString, "verifyLogon")) {
+ m_imapAction = nsImapVerifylogon;
+ } else if (m_imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand) {
+ m_command = m_urlidSubString; // save this
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ } else if (m_imapAction == nsIImapUrl::nsImapMsgStoreCustomKeywords) {
+ ParseUidChoice();
+ ParseFolderPath(&m_sourceCanonicalFolderPathSubString);
+ ParseListOfMessageIds();
+ bool addKeyword = (m_tokenPlaceHolder && *m_tokenPlaceHolder != '>');
+ // if we're not adding a keyword, m_tokenPlaceHolder will now look like
+ // >keywordToSubtract> and strtok will leave flagsPtr pointing to
+ // keywordToSubtract. So detect this case and only set the
+ // customSubtractFlags.
+ char* flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)nullptr;
+ if (addKeyword) {
+ m_customAddFlags.Assign(flagsPtr);
+ flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)nullptr;
+ }
+ m_customSubtractFlags.Assign(flagsPtr);
+ } else {
+ m_validUrl = false;
+ }
+ }
+}
+
+// Returns NULL if nothing was done.
+// Otherwise, returns a newly allocated name.
+NS_IMETHODIMP nsImapUrl::AddOnlineDirectoryIfNecessary(
+ const char* onlineMailboxName, char** directory) {
+ nsresult rv;
+ nsString onlineDirString;
+ char* newOnlineName = nullptr;
+
+ nsCOMPtr<nsIImapHostSessionList> hostSessionList =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ if (NS_FAILED(rv)) return rv;
+ rv = hostSessionList->GetOnlineDirForHost(m_serverKey.get(), onlineDirString);
+ nsAutoCString onlineDir;
+ LossyCopyUTF16toASCII(onlineDirString, onlineDir);
+
+ nsImapNamespace* ns = nullptr;
+ rv = hostSessionList->GetNamespaceForMailboxForHost(m_serverKey.get(),
+ onlineMailboxName, ns);
+ if (!ns)
+ hostSessionList->GetDefaultNamespaceOfTypeForHost(m_serverKey.get(),
+ kPersonalNamespace, ns);
+
+ if (onlineDir.IsEmpty() && ns) onlineDir = ns->GetPrefix();
+
+ // If this host has an online server directory configured
+ if (onlineMailboxName && !onlineDir.IsEmpty()) {
+ if (PL_strcasecmp(onlineMailboxName, "INBOX")) {
+ NS_ASSERTION(ns, "couldn't find namespace for host");
+ nsAutoCString onlineDirWithDelimiter(onlineDir);
+ // make sure the onlineDir ends with the hierarchy delimiter
+ if (ns) {
+ char delimiter = ns->GetDelimiter();
+ if (delimiter && delimiter != kOnlineHierarchySeparatorUnknown) {
+ // try to change the canonical online dir name to real dir name first
+ onlineDirWithDelimiter.ReplaceChar('/', delimiter);
+ // make sure the last character is the delimiter
+ if (onlineDirWithDelimiter.Last() != delimiter)
+ onlineDirWithDelimiter += delimiter;
+ if (!*onlineMailboxName)
+ onlineDirWithDelimiter.SetLength(onlineDirWithDelimiter.Length() -
+ 1);
+ }
+ }
+ if (ns && (PL_strlen(ns->GetPrefix()) != 0) &&
+ !onlineDirWithDelimiter.Equals(ns->GetPrefix())) {
+ // check that onlineMailboxName doesn't start with the namespace. If
+ // that's the case, we don't want to prepend the online dir.
+ if (PL_strncmp(onlineMailboxName, ns->GetPrefix(),
+ PL_strlen(ns->GetPrefix()))) {
+ // The namespace for this mailbox is the root ("").
+ // Prepend the online server directory
+ int finalLen =
+ onlineDirWithDelimiter.Length() + strlen(onlineMailboxName) + 1;
+ newOnlineName = (char*)PR_Malloc(finalLen);
+ if (newOnlineName) {
+ PL_strcpy(newOnlineName, onlineDirWithDelimiter.get());
+ PL_strcat(newOnlineName, onlineMailboxName);
+ }
+ }
+ }
+ // just prepend the online server directory if it doesn't start with it
+ // already
+ else if (strncmp(onlineMailboxName, onlineDirWithDelimiter.get(),
+ onlineDirWithDelimiter.Length())) {
+ newOnlineName = (char*)PR_Malloc(strlen(onlineMailboxName) +
+ onlineDirWithDelimiter.Length() + 1);
+ if (newOnlineName) {
+ PL_strcpy(newOnlineName, onlineDirWithDelimiter.get());
+ PL_strcat(newOnlineName, onlineMailboxName);
+ }
+ }
+ }
+ }
+ if (directory)
+ *directory = newOnlineName;
+ else if (newOnlineName)
+ free(newOnlineName);
+ return rv;
+}
+
+// Converts from canonical format (hierarchy is indicated by '/' and all real
+// slashes ('/') are escaped) to the real online name on the server.
+NS_IMETHODIMP nsImapUrl::AllocateServerPath(const char* canonicalPath,
+ char onlineDelimiter,
+ char** aAllocatedPath) {
+ nsresult retVal = NS_OK;
+ char* rv = NULL;
+ char delimiterToUse = onlineDelimiter;
+ if (onlineDelimiter == kOnlineHierarchySeparatorUnknown)
+ GetOnlineSubDirSeparator(&delimiterToUse);
+ NS_ASSERTION(delimiterToUse != kOnlineHierarchySeparatorUnknown,
+ "hierarchy separator unknown");
+ if (canonicalPath)
+ rv = ReplaceCharsInCopiedString(canonicalPath, '/', delimiterToUse);
+ else
+ rv = strdup("");
+
+ if (delimiterToUse != '/') UnescapeSlashes(rv);
+ char* onlineNameAdded = nullptr;
+ AddOnlineDirectoryIfNecessary(rv, &onlineNameAdded);
+ if (onlineNameAdded) {
+ free(rv);
+ rv = onlineNameAdded;
+ }
+
+ if (aAllocatedPath)
+ *aAllocatedPath = rv;
+ else
+ free(rv);
+
+ return retVal;
+}
+
+// escape '/' as ^, ^ -> ^^ - use UnescapeSlashes to revert
+/* static */ nsresult nsImapUrl::EscapeSlashes(const char* sourcePath,
+ char** resultPath) {
+ NS_ENSURE_ARG(sourcePath);
+ NS_ENSURE_ARG(resultPath);
+ int32_t extra = 0;
+ int32_t len = strlen(sourcePath);
+ const char* src = sourcePath;
+ int32_t i;
+ for (i = 0; i < len; i++) {
+ if (*src == '^') extra += 1; /* ^ -> ^^ */
+ src++;
+ }
+ char* result = (char*)moz_xmalloc(len + extra + 1);
+ if (!result) return NS_ERROR_OUT_OF_MEMORY;
+
+ unsigned char* dst = (unsigned char*)result;
+ src = sourcePath;
+ for (i = 0; i < len; i++) {
+ unsigned char c = *src++;
+ if (c == '/')
+ *dst++ = '^';
+ else if (c == '^') {
+ *dst++ = '^';
+ *dst++ = '^';
+ } else
+ *dst++ = c;
+ }
+ *dst = '\0'; /* tack on eos */
+ *resultPath = result;
+ return NS_OK;
+}
+
+static void unescapeSlashes(char* path, size_t* newLength) {
+ char* src = path;
+ char* start = src;
+ char* dst = path;
+
+ while (*src) {
+ if (*src == '^') {
+ if (*(src + 1) == '^') {
+ *dst++ = '^';
+ src++; // skip over second '^'
+ } else
+ *dst++ = '/';
+ src++;
+ } else
+ *dst++ = *src++;
+ }
+
+ *newLength = dst - start;
+}
+
+/* static */ nsresult nsImapUrl::UnescapeSlashes(char* path) {
+ size_t newLength;
+ unescapeSlashes(path, &newLength);
+ path[newLength] = 0;
+ return NS_OK;
+}
+
+/* static */ nsresult nsImapUrl::UnescapeSlashes(nsACString& path) {
+ size_t newLength;
+ unescapeSlashes(path.BeginWriting(), &newLength);
+ path.SetLength(newLength);
+ return NS_OK;
+}
+
+/* static */ nsresult nsImapUrl::ConvertToCanonicalFormat(
+ const char* folderName, char onlineDelimiter,
+ char** resultingCanonicalPath) {
+ // Now, start the conversion to canonical form.
+
+ char* canonicalPath;
+ if (onlineDelimiter != '/') {
+ nsCString escapedPath;
+
+ EscapeSlashes(folderName, getter_Copies(escapedPath));
+ canonicalPath =
+ ReplaceCharsInCopiedString(escapedPath.get(), onlineDelimiter, '/');
+ } else {
+ canonicalPath = strdup(folderName);
+ }
+ if (canonicalPath) *resultingCanonicalPath = canonicalPath;
+
+ return (canonicalPath) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+// Converts the real online name on the server to canonical format:
+// result is hierarchy is indicated by '/' and all real slashes ('/') are
+// escaped. This method is only called from the IMAP thread.
+NS_IMETHODIMP nsImapUrl::AllocateCanonicalPath(const char* serverPath,
+ char onlineDelimiter,
+ char** allocatedPath) {
+ NS_ENSURE_ARG_POINTER(serverPath);
+
+ char delimiterToUse = onlineDelimiter;
+ *allocatedPath = nullptr;
+
+ char* currentPath = (char*)serverPath;
+
+ nsresult rv;
+ nsCOMPtr<nsIImapHostSessionList> hostSessionList =
+ do_GetService(kCImapHostSessionListCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (onlineDelimiter == kOnlineHierarchySeparatorUnknown ||
+ onlineDelimiter == 0)
+ GetOnlineSubDirSeparator(&delimiterToUse);
+
+ nsString dir;
+ hostSessionList->GetOnlineDirForHost(m_serverKey.get(), dir);
+ // First we have to check to see if we should strip off an online server
+ // subdirectory
+ // If this host has an online server directory configured
+ nsAutoCString onlineDir;
+ LossyCopyUTF16toASCII(dir, onlineDir);
+
+ if (!onlineDir.IsEmpty()) {
+ // By definition, the online dir must be at the root.
+ if (delimiterToUse && delimiterToUse != kOnlineHierarchySeparatorUnknown) {
+ // try to change the canonical online dir name to real dir name first
+ onlineDir.ReplaceChar('/', delimiterToUse);
+ // Add the delimiter
+ if (onlineDir.Last() != delimiterToUse) onlineDir += delimiterToUse;
+ }
+ int len = onlineDir.Length();
+ if (!PL_strncmp(onlineDir.get(), currentPath, len)) {
+ // This online path begins with the server sub directory
+ currentPath += len;
+
+ // This might occur, but it's most likely something not good.
+ // Basically, it means we're doing something on the online sub directory
+ // itself.
+ NS_ASSERTION(*currentPath, "Oops ... null currentPath");
+ // Also make sure that the first character in the mailbox name is not '/'.
+ NS_ASSERTION(*currentPath != '/',
+ "Oops ... currentPath starts with a slash");
+ }
+ }
+
+ return ConvertToCanonicalFormat(currentPath, delimiterToUse, allocatedPath);
+}
+
+// this method is only called from the imap thread
+NS_IMETHODIMP nsImapUrl::CreateServerSourceFolderPathString(char** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ AllocateServerPath(m_sourceCanonicalFolderPathSubString,
+ kOnlineHierarchySeparatorUnknown, result);
+ return NS_OK;
+}
+
+// this method is called from the imap thread AND the UI thread...
+NS_IMETHODIMP nsImapUrl::CreateCanonicalSourceFolderPathString(char** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ MutexAutoLock mon(mLock);
+ *result = strdup(m_sourceCanonicalFolderPathSubString
+ ? m_sourceCanonicalFolderPathSubString
+ : "");
+ return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+// this method is called from the imap thread AND the UI thread...
+NS_IMETHODIMP nsImapUrl::CreateServerDestinationFolderPathString(
+ char** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ MutexAutoLock mon(mLock);
+ nsresult rv = AllocateServerPath(m_destinationCanonicalFolderPathSubString,
+ kOnlineHierarchySeparatorUnknown, result);
+ return (*result) ? rv : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsImapUrl::SetMimePartSelectorDetected(
+ bool mimePartSelectorDetected) {
+ m_mimePartSelectorDetected = mimePartSelectorDetected;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetMimePartSelectorDetected(
+ bool* mimePartSelectorDetected) {
+ if (!mimePartSelectorDetected) return NS_ERROR_NULL_POINTER;
+
+ *mimePartSelectorDetected = m_mimePartSelectorDetected;
+ return NS_OK;
+}
+
+// this method is only called from the UI thread.
+NS_IMETHODIMP nsImapUrl::SetCopyState(nsISupports* copyState) {
+ MutexAutoLock mon(mLock);
+ m_copyState = copyState;
+ return NS_OK;
+}
+
+// this method is only called from the imap thread..but we still
+// need a monitor 'cause the setter is called from the UI thread.
+NS_IMETHODIMP nsImapUrl::GetCopyState(nsISupports** copyState) {
+ NS_ENSURE_ARG_POINTER(copyState);
+ MutexAutoLock mon(mLock);
+ NS_IF_ADDREF(*copyState = m_copyState);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapUrl::SetMsgFile(nsIFile* aFile) {
+ nsresult rv = NS_OK;
+ MutexAutoLock mon(mLock);
+ m_file = aFile;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapUrl::GetMsgFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ MutexAutoLock mon(mLock);
+ NS_IF_ADDREF(*aFile = m_file);
+ return NS_OK;
+}
+
+// this method is called from the UI thread..
+NS_IMETHODIMP nsImapUrl::GetMockChannel(nsIImapMockChannel** aChannel) {
+ NS_ENSURE_ARG_POINTER(aChannel);
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "should only access mock channel on ui thread");
+ *aChannel = nullptr;
+ nsCOMPtr<nsIImapMockChannel> channel(do_QueryReferent(m_channelWeakPtr));
+ channel.forget(aChannel);
+ return *aChannel ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsImapUrl::SetMockChannel(nsIImapMockChannel* aChannel) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "should only access mock channel on ui thread");
+ m_channelWeakPtr = do_GetWeakReference(aChannel);
+ return NS_OK;
+}
+
+nsresult nsImapUrl::Clone(nsIURI** _retval) {
+ nsresult rv = nsMsgMailNewsUrl::Clone(_retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // also clone the mURI member, because GetUri below won't work if
+ // mURI isn't set due to escaping issues.
+ nsCOMPtr<nsIMsgMessageUrl> clonedUrl = do_QueryInterface(*_retval);
+ if (clonedUrl) clonedUrl->SetUri(mURI);
+ return rv;
+}
+
+NS_IMETHODIMP nsImapUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ // URLs look like this:
+ // imap://user@domain@server:port/fetch>UID>folder>nn
+ // We simply strip any query part beginning with ? & or /;
+ // Normalized spec: imap://user@domain@server:port/fetch>UID>folder>nn
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsURL;
+ QueryInterface(NS_GET_IID(nsIMsgMailNewsUrl), getter_AddRefs(mailnewsURL));
+
+ nsAutoCString spec;
+ mailnewsURL->GetSpecIgnoringRef(spec);
+
+ // Strip any query part beginning with ? or /;
+ MsgRemoveQueryPart(spec);
+
+ aPrincipalSpec.Assign(spec);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetUri(const nsACString& aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetUri(nsACString& aURI) {
+ nsresult rv = NS_OK;
+ if (!mURI.IsEmpty())
+ aURI = mURI;
+ else {
+ uint32_t key =
+ m_listOfMessageIds ? strtoul(m_listOfMessageIds, nullptr, 10) : 0;
+ nsCString canonicalPath;
+ AllocateCanonicalPath(m_sourceCanonicalFolderPathSubString,
+ m_onlineSubDirSeparator,
+ (getter_Copies(canonicalPath)));
+ nsCString fullFolderPath("/");
+ fullFolderPath.Append(m_userName);
+ nsAutoCString hostName;
+ rv = GetHost(hostName);
+ fullFolderPath.Append('@');
+ fullFolderPath.Append(hostName);
+ fullFolderPath.Append('/');
+ fullFolderPath.Append(canonicalPath);
+
+ nsCString baseMessageURI;
+ nsCreateImapBaseMessageURI(fullFolderPath, baseMessageURI);
+ rv = nsBuildImapMessageURI(baseMessageURI.get(), key, aURI);
+ }
+ return rv;
+}
+
+NS_IMPL_GETSET(nsImapUrl, AddDummyEnvelope, bool, m_addDummyEnvelope)
+NS_IMPL_GETSET(nsImapUrl, CanonicalLineEnding, bool, m_canonicalLineEnding)
+NS_IMETHODIMP nsImapUrl::GetMsgLoadingFromCache(bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_msgLoadingFromCache;
+ return NS_OK;
+}
+NS_IMPL_GETSET(nsImapUrl, LocalFetchOnly, bool, m_localFetchOnly)
+NS_IMPL_GETSET(nsImapUrl, ExternalLinkUrl, bool, m_externalLinkUrl)
+NS_IMPL_GETSET(nsImapUrl, RerunningUrl, bool, m_rerunningUrl)
+NS_IMPL_GETSET(nsImapUrl, ValidUrl, bool, m_validUrl)
+NS_IMPL_GETSET(nsImapUrl, MoreHeadersToDownload, bool, m_moreHeadersToDownload)
+
+NS_IMETHODIMP nsImapUrl::SetMsgLoadingFromCache(bool loadingFromCache) {
+ nsresult rv = NS_OK;
+ m_msgLoadingFromCache = loadingFromCache;
+ return rv;
+}
+
+NS_IMETHODIMP nsImapUrl::SetMessageFile(nsIFile* aFile) {
+ m_messageFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetMessageFile(nsIFile** aFile) {
+ if (aFile) NS_IF_ADDREF(*aFile = m_messageFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+
+ switch (type) {
+ case nsIMsgMailNewsUrl::eCopy:
+ *isType = ((m_imapAction == nsIImapUrl::nsImapOnlineCopy) ||
+ (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineCopy) ||
+ (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineCopy));
+ break;
+ case nsIMsgMailNewsUrl::eMove:
+ *isType = ((m_imapAction == nsIImapUrl::nsImapOnlineMove) ||
+ (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineMove) ||
+ (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineMove));
+ break;
+ case nsIMsgMailNewsUrl::eDisplay:
+ *isType = (m_imapAction == nsIImapUrl::nsImapMsgFetch ||
+ m_imapAction == nsIImapUrl::nsImapMsgFetchPeek);
+ break;
+ default:
+ *isType = false;
+ };
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapUrl::GetOriginalSpec(nsACString& aSpec) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsImapUrl::SetOriginalSpec(const nsACString& aSpec) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+char* nsImapUrl::ReplaceCharsInCopiedString(const char* stringToCopy,
+ char oldChar, char newChar) {
+ char oldCharString[2];
+ *oldCharString = oldChar;
+ *(oldCharString + 1) = 0;
+
+ char* translatedString = PL_strdup(stringToCopy);
+ char* currentSeparator = PL_strstr(translatedString, oldCharString);
+
+ while (currentSeparator) {
+ *currentSeparator = newChar;
+ currentSeparator = PL_strstr(currentSeparator + 1, oldCharString);
+ }
+
+ return translatedString;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// End of functions which should be made obsolete after modifying nsIURI
+////////////////////////////////////////////////////////////////////////////////////
+
+void nsImapUrl::ParseFolderPath(char** resultingCanonicalPath) {
+ char* resultPath = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)NULL;
+
+ if (!resultPath) {
+ m_validUrl = false;
+ return;
+ }
+ NS_ASSERTION(*resultingCanonicalPath == nullptr, "whoops, mem leak");
+
+ char dirSeparator = *resultPath;
+
+ nsCString unescapedResultingCanonicalPath;
+ MsgUnescapeString(nsDependentCString(resultPath + 1), 0,
+ unescapedResultingCanonicalPath);
+ *resultingCanonicalPath = ToNewCString(unescapedResultingCanonicalPath);
+ // The delimiter will be set for a given URL, but will not be statically
+ // available from an arbitrary URL. It is the creator's responsibility to
+ // fill in the correct delimiter from the folder's namespace when creating the
+ // URL.
+ if (dirSeparator != kOnlineHierarchySeparatorUnknown)
+ SetOnlineSubDirSeparator(dirSeparator);
+
+ // if dirSeparator == kOnlineHierarchySeparatorUnknown, then this must be a
+ // create of a top level imap box. If there is an online subdir, we will
+ // automatically use its separator. If there is not an online subdir, we
+ // don't need a separator.
+}
+
+void nsImapUrl::ParseSearchCriteriaString() {
+ if (m_tokenPlaceHolder) {
+ int quotedFlag = false;
+
+ // skip initial separator
+ while (*m_tokenPlaceHolder == *IMAP_URL_TOKEN_SEPARATOR)
+ m_tokenPlaceHolder++;
+
+ char* saveTokenPlaceHolder = m_tokenPlaceHolder;
+
+ // m_searchCriteriaString = m_tokenPlaceHolder;
+
+ // looking for another separator outside quoted string
+ while (*m_tokenPlaceHolder) {
+ if (*m_tokenPlaceHolder == '\\' && *(m_tokenPlaceHolder + 1) == '"')
+ m_tokenPlaceHolder++;
+ else if (*m_tokenPlaceHolder == '"')
+ quotedFlag = !quotedFlag;
+ else if (!quotedFlag &&
+ *m_tokenPlaceHolder == *IMAP_URL_TOKEN_SEPARATOR) {
+ *m_tokenPlaceHolder = '\0';
+ m_tokenPlaceHolder++;
+ break;
+ }
+ m_tokenPlaceHolder++;
+ }
+ m_searchCriteriaString = PL_strdup(saveTokenPlaceHolder);
+ if (*m_tokenPlaceHolder == '\0') m_tokenPlaceHolder = NULL;
+
+ if (*m_searchCriteriaString == '\0') m_searchCriteriaString = (char*)NULL;
+ } else
+ m_searchCriteriaString = (char*)NULL;
+ if (!m_searchCriteriaString) m_validUrl = false;
+}
+
+void nsImapUrl::ParseUidChoice() {
+ char* uidChoiceString =
+ m_tokenPlaceHolder
+ ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, &m_tokenPlaceHolder)
+ : (char*)NULL;
+ if (!uidChoiceString)
+ m_validUrl = false;
+ else
+ m_idsAreUids = strcmp(uidChoiceString, "UID") == 0;
+}
+
+void nsImapUrl::ParseMsgFlags() {
+ char* flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)NULL;
+ if (flagsPtr) {
+ // the url is encodes the flags byte as ascii
+ int intFlags = atoi(flagsPtr);
+ m_flags = (imapMessageFlagsType)intFlags; // cast here
+ } else
+ m_flags = 0;
+}
+
+void nsImapUrl::ParseListOfMessageIds() {
+ m_listOfMessageIds = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)NULL;
+ if (!m_listOfMessageIds)
+ m_validUrl = false;
+ else {
+ m_listOfMessageIds = strdup(m_listOfMessageIds);
+ m_mimePartSelectorDetected = PL_strstr(m_listOfMessageIds, "&part=") != 0 ||
+ PL_strstr(m_listOfMessageIds, "?part=") != 0;
+
+ // if it's a spam filter trying to fetch the msg, don't let it get marked
+ // read.
+ if (PL_strstr(m_listOfMessageIds, "?header=filter") != 0)
+ m_imapAction = nsImapMsgFetchPeek;
+ }
+}
+
+void nsImapUrl::ParseCustomMsgFetchAttribute() {
+ m_msgFetchAttribute = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR,
+ &m_tokenPlaceHolder)
+ : (char*)nullptr;
+}
+
+void nsImapUrl::ParseNumBytes() {
+ const char* numBytes =
+ (m_tokenPlaceHolder)
+ ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, &m_tokenPlaceHolder)
+ : 0;
+ m_numBytesToFetch = numBytes ? atoi(numBytes) : 0;
+}
+
+// nsIMsgI18NUrl support
+
+nsresult nsImapUrl::GetMsgFolder(nsIMsgFolder** msgFolder) {
+ // if we have a RDF URI, then try to get the folder for that URI and then ask
+ // the folder for it's charset....
+
+ nsCString uri;
+ GetUri(uri);
+ NS_ENSURE_TRUE(!uri.IsEmpty(), NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ GetMsgDBHdrFromURI(uri, getter_AddRefs(msg));
+ NS_ENSURE_TRUE(msg, NS_ERROR_FAILURE);
+ nsresult rv = msg->GetFolder(msgFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(msgFolder, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetAutodetectCharset(bool* aAutodetectCharset) {
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetAutodetectCharset(bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetStoreResultsOffline(bool* aStoreResultsOffline) {
+ NS_ENSURE_ARG_POINTER(aStoreResultsOffline);
+ *aStoreResultsOffline = m_storeResultsOffline;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetStoreResultsOffline(bool aStoreResultsOffline) {
+ m_storeResultsOffline = aStoreResultsOffline;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetStoreOfflineOnFallback(
+ bool* aStoreOfflineOnFallback) {
+ NS_ENSURE_ARG_POINTER(aStoreOfflineOnFallback);
+ *aStoreOfflineOnFallback = m_storeOfflineOnFallback;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetStoreOfflineOnFallback(
+ bool aStoreOfflineOnFallback) {
+ m_storeOfflineOnFallback = aStoreOfflineOnFallback;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) {
+ nsCString uri;
+ nsresult rv = GetUri(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetMsgDBHdrFromURI(uri, aMsgHdr);
+}
diff --git a/comm/mailnews/imap/src/nsImapUrl.h b/comm/mailnews/imap/src/nsImapUrl.h
new file mode 100644
index 0000000000..c5cbdb15cf
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUrl.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsImapUrl_h___
+#define nsImapUrl_h___
+
+#include "mozilla/Attributes.h"
+#include "nsIImapUrl.h"
+#include "nsCOMPtr.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIFile.h"
+#include "mozilla/Mutex.h"
+
+class nsImapUrl : public nsIImapUrl,
+ public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public nsIMsgI18NUrl {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsMsgMailNewsUrl overrides
+ nsresult SetSpecInternal(const nsACString& aSpec) override;
+ nsresult SetQuery(const nsACString& aQuery) override;
+ nsresult Clone(nsIURI** _retval) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImapUrl interface
+ //////////////////////////////////////////////////////////////////////////////
+ NS_DECL_NSIIMAPURL
+
+ // nsIMsgMailNewsUrl overrides
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+ NS_IMETHOD GetFolder(nsIMsgFolder** aFolder) override;
+ NS_IMETHOD SetFolder(nsIMsgFolder* aFolder) override;
+ // nsIMsgMessageUrl
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_NSIMSGI18NURL
+
+ // nsImapUrl
+ nsImapUrl();
+
+ static nsresult ConvertToCanonicalFormat(const char* folderName,
+ char onlineDelimiter,
+ char** resultingCanonicalPath);
+ static nsresult EscapeSlashes(const char* sourcePath, char** resultPath);
+ static nsresult UnescapeSlashes(nsACString& path);
+ static nsresult UnescapeSlashes(char* path);
+ static char* ReplaceCharsInCopiedString(const char* stringToCopy,
+ char oldChar, char newChar);
+
+ protected:
+ virtual ~nsImapUrl();
+ virtual nsresult ParseUrl();
+
+ char* m_listOfMessageIds;
+
+ // handle the imap specific parsing
+ void ParseImapPart(char* imapPartOfUrl);
+
+ void ParseFolderPath(char** resultingCanonicalPath);
+ void ParseSearchCriteriaString();
+ void ParseUidChoice();
+ void ParseMsgFlags();
+ void ParseListOfMessageIds();
+ void ParseCustomMsgFetchAttribute();
+ void ParseNumBytes();
+
+ nsresult GetMsgFolder(nsIMsgFolder** msgFolder);
+
+ char* m_sourceCanonicalFolderPathSubString;
+ char* m_destinationCanonicalFolderPathSubString;
+ char* m_tokenPlaceHolder;
+ char* m_urlidSubString;
+ char m_onlineSubDirSeparator;
+ char* m_searchCriteriaString; // should we use m_search, or is this special?
+ nsCString m_command; // for custom commands
+ nsCString m_msgFetchAttribute; // for fetching custom msg attributes
+ nsCString m_customAttributeResult; // for fetching custom msg attributes
+ nsCString m_customCommandResult; // custom command response
+ nsCString
+ m_customAddFlags; // these two are for setting and clearing custom flags
+ nsCString m_customSubtractFlags;
+ int32_t m_numBytesToFetch; // when doing a msg body preview, how many bytes
+ // to read
+ bool m_validUrl;
+ bool m_runningUrl;
+ bool m_idsAreUids;
+ bool m_mimePartSelectorDetected;
+ bool m_msgLoadingFromCache; // if true, we might need to mark read on server
+ bool m_externalLinkUrl; // if true, we're running this url because the user
+ // True if the fetch results should be put in the offline store.
+ bool m_storeResultsOffline;
+ bool m_storeOfflineOnFallback;
+ bool m_localFetchOnly;
+ bool m_rerunningUrl; // first attempt running this failed with connection
+ // error; retrying
+ bool m_moreHeadersToDownload;
+ nsImapContentModifiedType m_contentModified;
+
+ int32_t m_extraStatus;
+
+ nsCString m_userName;
+ nsCString m_serverKey;
+ // event sinks
+ imapMessageFlagsType m_flags;
+ nsImapAction m_imapAction;
+
+ nsWeakPtr m_imapFolder;
+ nsWeakPtr m_imapMailFolderSink;
+ nsWeakPtr m_imapMessageSink;
+
+ nsWeakPtr m_imapServerSink;
+
+ // online message copy support; i don't have a better solution yet
+ nsCOMPtr<nsISupports> m_copyState; // now, refcounted.
+ nsCOMPtr<nsIFile> m_file;
+ nsWeakPtr m_channelWeakPtr;
+
+ // used by save message to disk
+ nsCOMPtr<nsIFile> m_messageFile;
+ bool m_addDummyEnvelope;
+ bool m_canonicalLineEnding; // CRLF
+
+ nsCString mURI; // the RDF URI associated with this url.
+ bool mAutodetectCharset; // used by nsIMsgI18NUrl...
+ mozilla::Mutex mLock;
+};
+
+#endif /* nsImapUrl_h___ */
diff --git a/comm/mailnews/imap/src/nsImapUtils.cpp b/comm/mailnews/imap/src/nsImapUtils.cpp
new file mode 100644
index 0000000000..9a5811511c
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUtils.cpp
@@ -0,0 +1,336 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsImapUtils.h"
+#include "nsCOMPtr.h"
+#include "prsystem.h"
+#include "prprf.h"
+#include "nsNetCID.h"
+
+#include "nsMsgUtils.h"
+#include "nsImapFlagAndUidState.h"
+#include "nsImapNamespace.h"
+#include "nsIImapFlagAndUidState.h"
+
+nsresult nsImapURI2FullName(const char* rootURI, const char* hostName,
+ const char* uriStr, char** name) {
+ nsAutoCString uri(uriStr);
+ nsAutoCString fullName;
+ if (uri.Find(rootURI) != 0) return NS_ERROR_FAILURE;
+ fullName = Substring(uri, strlen(rootURI));
+ uri = fullName;
+ int32_t hostStart = uri.Find(hostName);
+ if (hostStart <= 0) return NS_ERROR_FAILURE;
+ fullName = Substring(uri, hostStart);
+ uri = fullName;
+ int32_t hostEnd = uri.FindChar('/');
+ if (hostEnd <= 0) return NS_ERROR_FAILURE;
+ fullName = Substring(uri, hostEnd + 1);
+ if (fullName.IsEmpty()) return NS_ERROR_FAILURE;
+ *name = ToNewCString(fullName);
+ return NS_OK;
+}
+
+/* parses ImapMessageURI */
+nsresult nsParseImapMessageURI(const nsACString& uri, nsCString& folderURI,
+ uint32_t* key, char** part) {
+ if (!key) return NS_ERROR_NULL_POINTER;
+
+ const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri);
+ int32_t folderEnd = -1;
+ // imap-message uri's can have imap:// url strings tacked on the end,
+ // e.g., when opening/saving attachments. We don't want to look for '#'
+ // in that part of the uri, if the attachment name contains '#',
+ // so check for that here.
+ if (StringBeginsWith(uriStr, "imap-message"_ns))
+ folderEnd = uriStr.Find("imap://");
+
+ int32_t keySeparator = uriStr.RFindChar('#', folderEnd);
+ if (keySeparator != -1) {
+ int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "/?&", keySeparator);
+ nsAutoString folderPath;
+ folderURI = StringHead(uriStr, keySeparator);
+ folderURI.Cut(4, 8); // cut out the _message part of imap-message:
+ // folder uri's don't have fully escaped usernames.
+ int32_t atPos = folderURI.FindChar('@');
+ if (atPos != -1) {
+ nsCString unescapedName, escapedName;
+ int32_t userNamePos = folderURI.Find("//") + 2;
+ uint32_t origUserNameLen = atPos - userNamePos;
+ if (NS_SUCCEEDED(MsgUnescapeString(
+ Substring(folderURI, userNamePos, origUserNameLen), 0,
+ unescapedName))) {
+ // Re-escape the username, matching the way we do it in uris, not the
+ // way necko escapes urls. See nsMsgIncomingServer::GetServerURI.
+ MsgEscapeString(unescapedName, nsINetUtil::ESCAPE_XALPHAS, escapedName);
+ folderURI.Replace(userNamePos, origUserNameLen, escapedName);
+ }
+ }
+ nsAutoCString keyStr;
+ if (keyEndSeparator != -1)
+ keyStr = Substring(uriStr, keySeparator + 1,
+ keyEndSeparator - (keySeparator + 1));
+ else
+ keyStr = Substring(uriStr, keySeparator + 1);
+
+ *key = strtoul(keyStr.get(), nullptr, 10);
+
+ if (part && keyEndSeparator != -1) {
+ int32_t partPos = uriStr.Find("part=", keyEndSeparator);
+ if (partPos != -1) {
+ *part = ToNewCString(Substring(uriStr, keyEndSeparator));
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsBuildImapMessageURI(const char* baseURI, uint32_t key,
+ nsACString& uri) {
+ uri.Append(baseURI);
+ uri.Append('#');
+ uri.AppendInt(key);
+ return NS_OK;
+}
+
+nsresult nsCreateImapBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI) {
+ nsAutoCString tailURI(baseURI);
+ // chop off imap:/
+ if (tailURI.Find(kImapRootURI) == 0) tailURI.Cut(0, PL_strlen(kImapRootURI));
+ baseMessageURI = kImapMessageRootURI;
+ baseMessageURI += tailURI;
+ return NS_OK;
+}
+
+// nsImapMailboxSpec definition
+NS_IMPL_ISUPPORTS(nsImapMailboxSpec, nsIMailboxSpec)
+
+nsImapMailboxSpec::nsImapMailboxSpec() {
+ mFolder_UIDVALIDITY = 0;
+ mHighestModSeq = 0;
+ mNumOfMessages = 0;
+ mNumOfUnseenMessages = 0;
+ mNumOfRecentMessages = 0;
+ mNextUID = 0;
+
+ mBoxFlags = 0;
+ mSupportedUserFlags = 0;
+
+ mHierarchySeparator = '\0';
+
+ mFolderSelected = false;
+ mDiscoveredFromLsub = false;
+
+ mOnlineVerified = false;
+ mNamespaceForFolder = nullptr;
+}
+
+nsImapMailboxSpec::~nsImapMailboxSpec() {}
+
+NS_IMPL_GETSET(nsImapMailboxSpec, Folder_UIDVALIDITY, int32_t,
+ mFolder_UIDVALIDITY)
+NS_IMPL_GETSET(nsImapMailboxSpec, HighestModSeq, uint64_t, mHighestModSeq)
+NS_IMPL_GETSET(nsImapMailboxSpec, NumMessages, int32_t, mNumOfMessages)
+NS_IMPL_GETSET(nsImapMailboxSpec, NumUnseenMessages, int32_t,
+ mNumOfUnseenMessages)
+NS_IMPL_GETSET(nsImapMailboxSpec, NumRecentMessages, int32_t,
+ mNumOfRecentMessages)
+NS_IMPL_GETSET(nsImapMailboxSpec, NextUID, int32_t, mNextUID)
+NS_IMPL_GETSET(nsImapMailboxSpec, HierarchyDelimiter, char, mHierarchySeparator)
+NS_IMPL_GETSET(nsImapMailboxSpec, FolderSelected, bool, mFolderSelected)
+NS_IMPL_GETSET(nsImapMailboxSpec, DiscoveredFromLsub, bool, mDiscoveredFromLsub)
+NS_IMPL_GETSET(nsImapMailboxSpec, OnlineVerified, bool, mOnlineVerified)
+NS_IMPL_GETSET(nsImapMailboxSpec, SupportedUserFlags, uint32_t,
+ mSupportedUserFlags)
+NS_IMPL_GETSET(nsImapMailboxSpec, Box_flags, uint32_t, mBoxFlags)
+NS_IMPL_GETSET(nsImapMailboxSpec, NamespaceForFolder, nsImapNamespace*,
+ mNamespaceForFolder)
+
+NS_IMETHODIMP nsImapMailboxSpec::GetAllocatedPathName(
+ nsACString& aAllocatedPathName) {
+ aAllocatedPathName = mAllocatedPathName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::SetAllocatedPathName(
+ const nsACString& aAllocatedPathName) {
+ mAllocatedPathName = aAllocatedPathName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::GetUnicharPathName(
+ nsAString& aUnicharPathName) {
+ aUnicharPathName = mUnicharPathName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::SetUnicharPathName(
+ const nsAString& aUnicharPathName) {
+ mUnicharPathName = aUnicharPathName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::GetHostName(nsACString& aHostName) {
+ aHostName = mHostName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::SetHostName(const nsACString& aHostName) {
+ mHostName = aHostName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::GetFlagState(
+ nsIImapFlagAndUidState** aFlagState) {
+ NS_ENSURE_ARG_POINTER(aFlagState);
+ NS_IF_ADDREF(*aFlagState = mFlagState);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImapMailboxSpec::SetFlagState(
+ nsIImapFlagAndUidState* aFlagState) {
+ NS_ENSURE_ARG_POINTER(aFlagState);
+ mFlagState = aFlagState;
+ return NS_OK;
+}
+
+nsImapMailboxSpec& nsImapMailboxSpec::operator=(
+ const nsImapMailboxSpec& aCopy) {
+ mFolder_UIDVALIDITY = aCopy.mFolder_UIDVALIDITY;
+ mHighestModSeq = aCopy.mHighestModSeq;
+ mNumOfMessages = aCopy.mNumOfMessages;
+ mNumOfUnseenMessages = aCopy.mNumOfUnseenMessages;
+ mNumOfRecentMessages = aCopy.mNumOfRecentMessages;
+
+ mBoxFlags = aCopy.mBoxFlags;
+ mSupportedUserFlags = aCopy.mSupportedUserFlags;
+
+ mAllocatedPathName.Assign(aCopy.mAllocatedPathName);
+ mUnicharPathName.Assign(aCopy.mUnicharPathName);
+ mHostName.Assign(aCopy.mHostName);
+
+ mFlagState = aCopy.mFlagState;
+ mNamespaceForFolder = aCopy.mNamespaceForFolder;
+
+ mFolderSelected = aCopy.mFolderSelected;
+ mDiscoveredFromLsub = aCopy.mDiscoveredFromLsub;
+
+ mOnlineVerified = aCopy.mOnlineVerified;
+
+ return *this;
+}
+
+// use the flagState to determine if the gaps in the msgUids correspond to gaps
+// in the mailbox, in which case we can still use ranges. If flagState is null,
+// we won't do this.
+void AllocateImapUidString(const uint32_t* msgUids, uint32_t& msgCount,
+ nsImapFlagAndUidState* flagState,
+ nsCString& returnString) {
+ uint32_t startSequence = (msgCount > 0) ? msgUids[0] : 0xFFFFFFFF;
+ uint32_t curSequenceEnd = startSequence;
+ uint32_t total = msgCount;
+ int32_t curFlagStateIndex = -1;
+
+ // a partial fetch flag state doesn't help us, so don't use it.
+ if (flagState && flagState->GetPartialUIDFetch()) flagState = nullptr;
+
+ for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) {
+ uint32_t curKey = msgUids[keyIndex];
+ uint32_t nextKey =
+ (keyIndex + 1 < total) ? msgUids[keyIndex + 1] : 0xFFFFFFFF;
+ bool lastKey = (nextKey == 0xFFFFFFFF);
+
+ if (lastKey) curSequenceEnd = curKey;
+
+ if (!lastKey) {
+ if (nextKey == curSequenceEnd + 1) {
+ curSequenceEnd = nextKey;
+ curFlagStateIndex++;
+ continue;
+ }
+ if (flagState) {
+ if (curFlagStateIndex == -1) {
+ bool foundIt;
+ flagState->GetMessageFlagsFromUID(curSequenceEnd, &foundIt,
+ &curFlagStateIndex);
+ if (!foundIt) {
+ NS_WARNING("flag state missing key");
+ // The start of this sequence is missing from flag state, so move
+ // on to the next key.
+ curFlagStateIndex = -1;
+ curSequenceEnd = startSequence = nextKey;
+ continue;
+ }
+ }
+ curFlagStateIndex++;
+ uint32_t nextUidInFlagState;
+ nsresult rv =
+ flagState->GetUidOfMessage(curFlagStateIndex, &nextUidInFlagState);
+ if (NS_SUCCEEDED(rv) && nextUidInFlagState == nextKey) {
+ curSequenceEnd = nextKey;
+ continue;
+ }
+ }
+ }
+ if (curSequenceEnd > startSequence) {
+ returnString.AppendInt((int64_t)startSequence);
+ returnString += ':';
+ returnString.AppendInt((int64_t)curSequenceEnd);
+ startSequence = nextKey;
+ curSequenceEnd = startSequence;
+ curFlagStateIndex = -1;
+ } else {
+ startSequence = nextKey;
+ curSequenceEnd = startSequence;
+ returnString.AppendInt((int64_t)msgUids[keyIndex]);
+ curFlagStateIndex = -1;
+ }
+ // check if we've generated too long a string - if there's no flag state,
+ // it means we just need to go ahead and generate a too long string
+ // because the calling code won't handle breaking up the strings.
+ if (flagState && returnString.Length() > 950) {
+ msgCount = keyIndex;
+ break;
+ }
+ // If we are not the last item then we need to add the comma
+ // but it's important we do it here, after the length check
+ if (!lastKey) returnString += ',';
+ }
+}
+
+void ParseUidString(const char* uidString, nsTArray<nsMsgKey>& keys) {
+ // This is in the form <id>,<id>, or <id1>:<id2>
+ if (!uidString) return;
+
+ char curChar = *uidString;
+ bool isRange = false;
+ uint32_t curToken;
+ uint32_t saveStartToken = 0;
+
+ for (const char* curCharPtr = uidString; curChar && *curCharPtr;) {
+ const char* currentKeyToken = curCharPtr;
+ curChar = *curCharPtr;
+ while (curChar != ':' && curChar != ',' && curChar != '\0')
+ curChar = *curCharPtr++;
+
+ // we don't need to null terminate currentKeyToken because strtoul
+ // stops at non-numeric chars.
+ curToken = strtoul(currentKeyToken, nullptr, 10);
+ if (isRange) {
+ while (saveStartToken < curToken) keys.AppendElement(saveStartToken++);
+ }
+ keys.AppendElement(curToken);
+ isRange = (curChar == ':');
+ if (isRange) saveStartToken = curToken + 1;
+ }
+}
+
+void AppendUid(nsCString& msgIds, uint32_t uid) {
+ char buf[20];
+ PR_snprintf(buf, sizeof(buf), "%u", uid);
+ msgIds.Append(buf);
+}
diff --git a/comm/mailnews/imap/src/nsImapUtils.h b/comm/mailnews/imap/src/nsImapUtils.h
new file mode 100644
index 0000000000..8dc2c55ef3
--- /dev/null
+++ b/comm/mailnews/imap/src/nsImapUtils.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NS_IMAPUTILS_H
+#define NS_IMAPUTILS_H
+
+#include "nsString.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMailboxSpec.h"
+#include "nsCOMPtr.h"
+
+class nsImapFlagAndUidState;
+class nsImapProtocol;
+
+static const char kImapRootURI[] = "imap:/";
+static const char kImapMessageRootURI[] = "imap-message:/";
+static const char kModSeqPropertyName[] = "highestModSeq";
+static const char kHighestRecordedUIDPropertyName[] = "highestRecordedUID";
+static const char kDeletedHdrCountPropertyName[] = "numDeletedHeaders";
+
+extern nsresult nsImapURI2FullName(const char* rootURI, const char* hostname,
+ const char* uriStr, char** name);
+
+extern nsresult nsParseImapMessageURI(const nsACString& uri,
+ nsCString& folderURI, uint32_t* key,
+ char** part);
+
+extern nsresult nsBuildImapMessageURI(const char* baseURI, uint32_t key,
+ nsACString& uri);
+
+extern nsresult nsCreateImapBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI);
+
+void AllocateImapUidString(const uint32_t* msgUids, uint32_t& msgCount,
+ nsImapFlagAndUidState* flagState,
+ nsCString& returnString);
+void ParseUidString(const char* uidString, nsTArray<nsMsgKey>& keys);
+void AppendUid(nsCString& msgIds, uint32_t uid);
+
+class nsImapMailboxSpec : public nsIMailboxSpec {
+ public:
+ nsImapMailboxSpec();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMAILBOXSPEC
+
+ nsImapMailboxSpec& operator=(const nsImapMailboxSpec& aCopy);
+
+ nsCOMPtr<nsIImapFlagAndUidState> mFlagState;
+ nsImapNamespace* mNamespaceForFolder;
+
+ uint32_t mBoxFlags;
+ uint32_t mSupportedUserFlags;
+ int32_t mFolder_UIDVALIDITY;
+ uint64_t mHighestModSeq;
+ int32_t mNumOfMessages;
+ int32_t mNumOfUnseenMessages;
+ int32_t mNumOfRecentMessages;
+ int32_t mNextUID;
+ nsCString mAllocatedPathName;
+ nsCString mHostName;
+ nsString mUnicharPathName;
+ char mHierarchySeparator;
+ bool mFolderSelected;
+ bool mDiscoveredFromLsub;
+ bool mOnlineVerified;
+
+ nsImapProtocol* mConnection; // do we need this? It seems evil
+
+ private:
+ virtual ~nsImapMailboxSpec();
+};
+
+#endif // NS_IMAPUTILS_H
diff --git a/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp b/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp
new file mode 100644
index 0000000000..14028fef03
--- /dev/null
+++ b/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp
@@ -0,0 +1,596 @@
+/* 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/. */
+
+#include "nsSyncRunnableHelpers.h"
+#include "nsImapCore.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIMsgWindow.h"
+#include "nsIImapMailFolderSink.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Monitor.h"
+
+NS_IMPL_ISUPPORTS(StreamListenerProxy, nsIStreamListener)
+NS_IMPL_ISUPPORTS(ImapMailFolderSinkProxy, nsIImapMailFolderSink)
+NS_IMPL_ISUPPORTS(ImapServerSinkProxy, nsIImapServerSink)
+NS_IMPL_ISUPPORTS(ImapMessageSinkProxy, nsIImapMessageSink)
+NS_IMPL_ISUPPORTS(ImapProtocolSinkProxy, nsIImapProtocolSink)
+namespace {
+
+// Traits class for a reference type, specialized for parameters which are
+// already references.
+template <typename T>
+struct RefType {
+ typedef T& type;
+};
+
+template <>
+struct RefType<nsAString&> {
+ typedef nsAString& type;
+};
+
+template <>
+struct RefType<const nsAString&> {
+ typedef const nsAString& type;
+};
+
+template <>
+struct RefType<nsACString&> {
+ typedef nsACString& type;
+};
+
+template <>
+struct RefType<const nsACString&> {
+ typedef const nsACString& type;
+};
+
+template <>
+struct RefType<const nsIID&> {
+ typedef const nsIID& type;
+};
+
+class SyncRunnableBase : public mozilla::Runnable {
+ public:
+ nsresult Result() { return mResult; }
+
+ mozilla::Monitor& Monitor() { return mMonitor; }
+
+ protected:
+ SyncRunnableBase()
+ : mozilla::Runnable("SyncRunnableBase"),
+ mResult(NS_ERROR_UNEXPECTED),
+ mMonitor("SyncRunnableBase") {}
+
+ nsresult mResult;
+ mozilla::Monitor mMonitor;
+};
+
+template <typename Receiver>
+class SyncRunnable0 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)();
+
+ SyncRunnable0(Receiver* receiver, ReceiverMethod method)
+ : mReceiver(receiver), mMethod(method) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)();
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+};
+
+template <typename Receiver, typename Arg1>
+class SyncRunnable1 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1);
+ typedef typename RefType<Arg1>::type Arg1Ref;
+
+ SyncRunnable1(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1)
+ : mReceiver(receiver), mMethod(method), mArg1(arg1) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)(mArg1);
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+ Arg1Ref mArg1;
+};
+
+template <typename Receiver, typename Arg1, typename Arg2>
+class SyncRunnable2 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2);
+ typedef typename RefType<Arg1>::type Arg1Ref;
+ typedef typename RefType<Arg2>::type Arg2Ref;
+
+ SyncRunnable2(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1,
+ Arg2Ref arg2)
+ : mReceiver(receiver), mMethod(method), mArg1(arg1), mArg2(arg2) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)(mArg1, mArg2);
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+ Arg1Ref mArg1;
+ Arg2Ref mArg2;
+};
+
+template <typename Receiver, typename Arg1, typename Arg2, typename Arg3>
+class SyncRunnable3 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3);
+ typedef typename RefType<Arg1>::type Arg1Ref;
+ typedef typename RefType<Arg2>::type Arg2Ref;
+ typedef typename RefType<Arg3>::type Arg3Ref;
+
+ SyncRunnable3(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1,
+ Arg2Ref arg2, Arg3Ref arg3)
+ : mReceiver(receiver),
+ mMethod(method),
+ mArg1(arg1),
+ mArg2(arg2),
+ mArg3(arg3) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3);
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+ Arg1Ref mArg1;
+ Arg2Ref mArg2;
+ Arg3Ref mArg3;
+};
+
+template <typename Receiver, typename Arg1, typename Arg2, typename Arg3,
+ typename Arg4>
+class SyncRunnable4 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3,
+ Arg4);
+ typedef typename RefType<Arg1>::type Arg1Ref;
+ typedef typename RefType<Arg2>::type Arg2Ref;
+ typedef typename RefType<Arg3>::type Arg3Ref;
+ typedef typename RefType<Arg4>::type Arg4Ref;
+
+ SyncRunnable4(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1,
+ Arg2Ref arg2, Arg3Ref arg3, Arg4Ref arg4)
+ : mReceiver(receiver),
+ mMethod(method),
+ mArg1(arg1),
+ mArg2(arg2),
+ mArg3(arg3),
+ mArg4(arg4) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3, mArg4);
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+ Arg1Ref mArg1;
+ Arg2Ref mArg2;
+ Arg3Ref mArg3;
+ Arg4Ref mArg4;
+};
+
+template <typename Receiver, typename Arg1, typename Arg2, typename Arg3,
+ typename Arg4, typename Arg5>
+class SyncRunnable5 : public SyncRunnableBase {
+ public:
+ typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3,
+ Arg4, Arg5);
+ typedef typename RefType<Arg1>::type Arg1Ref;
+ typedef typename RefType<Arg2>::type Arg2Ref;
+ typedef typename RefType<Arg3>::type Arg3Ref;
+ typedef typename RefType<Arg4>::type Arg4Ref;
+ typedef typename RefType<Arg5>::type Arg5Ref;
+
+ SyncRunnable5(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1,
+ Arg2Ref arg2, Arg3Ref arg3, Arg4Ref arg4, Arg5Ref arg5)
+ : mReceiver(receiver),
+ mMethod(method),
+ mArg1(arg1),
+ mArg2(arg2),
+ mArg3(arg3),
+ mArg4(arg4),
+ mArg5(arg5) {}
+
+ NS_IMETHOD Run() {
+ mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3, mArg4, mArg5);
+ mozilla::MonitorAutoLock(mMonitor).Notify();
+ return NS_OK;
+ }
+
+ private:
+ Receiver* mReceiver;
+ ReceiverMethod mMethod;
+ Arg1Ref mArg1;
+ Arg2Ref mArg2;
+ Arg3Ref mArg3;
+ Arg4Ref mArg4;
+ Arg5Ref mArg5;
+};
+
+nsresult DispatchSyncRunnable(SyncRunnableBase* r) {
+ if (NS_IsMainThread()) {
+ r->Run();
+ } else {
+ mozilla::MonitorAutoLock lock(r->Monitor());
+ nsresult rv = NS_DispatchToMainThread(r);
+ if (NS_FAILED(rv)) return rv;
+ lock.Wait();
+ }
+ return r->Result();
+}
+
+} // anonymous namespace
+
+#define NS_SYNCRUNNABLEMETHOD0(iface, method) \
+ NS_IMETHODIMP iface##Proxy::method() { \
+ RefPtr<SyncRunnableBase> r = \
+ new SyncRunnable0<nsI##iface>(mReceiver, &nsI##iface::method); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEMETHOD1(iface, method, arg1) \
+ NS_IMETHODIMP iface##Proxy::method(arg1 a1) { \
+ RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, arg1>( \
+ mReceiver, &nsI##iface::method, a1); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEMETHOD2(iface, method, arg1, arg2) \
+ NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2) { \
+ RefPtr<SyncRunnableBase> r = new SyncRunnable2<nsI##iface, arg1, arg2>( \
+ mReceiver, &nsI##iface::method, a1, a2); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEMETHOD3(iface, method, arg1, arg2, arg3) \
+ NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3) { \
+ RefPtr<SyncRunnableBase> r = \
+ new SyncRunnable3<nsI##iface, arg1, arg2, arg3>( \
+ mReceiver, &nsI##iface::method, a1, a2, a3); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEMETHOD4(iface, method, arg1, arg2, arg3, arg4) \
+ NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3, arg4 a4) { \
+ RefPtr<SyncRunnableBase> r = \
+ new SyncRunnable4<nsI##iface, arg1, arg2, arg3, arg4>( \
+ mReceiver, &nsI##iface::method, a1, a2, a3, a4); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEMETHOD5(iface, method, arg1, arg2, arg3, arg4, arg5) \
+ NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3, arg4 a4, \
+ arg5 a5) { \
+ RefPtr<SyncRunnableBase> r = \
+ new SyncRunnable5<nsI##iface, arg1, arg2, arg3, arg4, arg5>( \
+ mReceiver, &nsI##iface::method, a1, a2, a3, a4, a5); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_SYNCRUNNABLEATTRIBUTE(iface, attribute, type) \
+ NS_IMETHODIMP iface##Proxy::Get##attribute(type* a1) { \
+ RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, type*>( \
+ mReceiver, &nsI##iface::Get##attribute, a1); \
+ return DispatchSyncRunnable(r); \
+ } \
+ NS_IMETHODIMP iface##Proxy::Set##attribute(type a1) { \
+ RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, type>( \
+ mReceiver, &nsI##iface::Set##attribute, a1); \
+ return DispatchSyncRunnable(r); \
+ }
+
+#define NS_NOTIMPLEMENTED \
+ { \
+ NS_RUNTIMEABORT("Not implemented"); \
+ return NS_ERROR_UNEXPECTED; \
+ }
+
+NS_SYNCRUNNABLEMETHOD4(StreamListener, OnDataAvailable, nsIRequest*,
+ nsIInputStream*, uint64_t, uint32_t)
+
+NS_SYNCRUNNABLEMETHOD1(StreamListener, OnStartRequest, nsIRequest*)
+
+NS_SYNCRUNNABLEMETHOD2(StreamListener, OnStopRequest, nsIRequest*, nsresult)
+
+NS_SYNCRUNNABLEMETHOD2(ImapProtocolSink, GetUrlWindow, nsIMsgMailNewsUrl*,
+ nsIMsgWindow**)
+
+NS_SYNCRUNNABLEMETHOD0(ImapProtocolSink, CloseStreams)
+NS_SYNCRUNNABLEMETHOD0(ImapProtocolSink, SetupMainThreadProxies)
+
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsACLListed, bool)
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsSubscribing, bool)
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsAdded, bool)
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, AclFlags, uint32_t)
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, UidValidity, int32_t)
+NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderQuotaCommandIssued, bool)
+NS_SYNCRUNNABLEMETHOD4(ImapMailFolderSink, SetFolderQuotaData, uint32_t,
+ const nsACString&, uint64_t, uint64_t)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, GetShouldDownloadAllHeaders, bool*)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, GetOnlineDelimiter, char*)
+NS_SYNCRUNNABLEMETHOD0(ImapMailFolderSink, OnNewIdleMessages)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, UpdateImapMailboxStatus,
+ nsIImapProtocol*, nsIMailboxSpec*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, UpdateImapMailboxInfo,
+ nsIImapProtocol*, nsIMailboxSpec*)
+NS_SYNCRUNNABLEMETHOD3(ImapMailFolderSink, GetMsgHdrsToDownload, bool*,
+ int32_t*, nsTArray<nsMsgKey>&)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, ParseMsgHdrs, nsIImapProtocol*,
+ nsIImapHeaderXferInfo*)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, AbortHeaderParseStream,
+ nsIImapProtocol*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, OnlineCopyCompleted,
+ nsIImapProtocol*, ImapOnlineCopyState)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, StartMessage, nsIMsgMailNewsUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, EndMessage, nsIMsgMailNewsUrl*,
+ nsMsgKey)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, NotifySearchHit, nsIMsgMailNewsUrl*,
+ const char*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, CopyNextStreamMessage, bool,
+ nsISupports*)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, CloseMockChannel,
+ nsIImapMockChannel*)
+NS_SYNCRUNNABLEMETHOD5(ImapMailFolderSink, SetUrlState, nsIImapProtocol*,
+ nsIMsgMailNewsUrl*, bool, bool, nsresult)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, ReleaseUrlCacheEntry,
+ nsIMsgMailNewsUrl*)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, HeaderFetchCompleted,
+ nsIImapProtocol*)
+NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, SetBiffStateAndUpdate, int32_t)
+NS_SYNCRUNNABLEMETHOD3(ImapMailFolderSink, ProgressStatusString,
+ nsIImapProtocol*, const char*, const char16_t*)
+NS_SYNCRUNNABLEMETHOD5(ImapMailFolderSink, PercentProgress, nsIImapProtocol*,
+ nsACString const&, nsAString const&, int64_t, int64_t)
+NS_SYNCRUNNABLEMETHOD0(ImapMailFolderSink, ClearFolderRights)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, SetCopyResponseUid, const char*,
+ nsIImapUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, SetAppendMsgUid, nsMsgKey,
+ nsIImapUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, GetMessageId, nsIImapUrl*,
+ nsACString&)
+
+NS_SYNCRUNNABLEMETHOD2(ImapMessageSink, SetupMsgWriteStream, nsIFile*, bool)
+NS_SYNCRUNNABLEMETHOD3(ImapMessageSink, ParseAdoptedMsgLine, const char*,
+ nsMsgKey, nsIImapUrl*)
+NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, NormalEndMsgWriteStream, nsMsgKey, bool,
+ nsIImapUrl*, int32_t)
+NS_SYNCRUNNABLEMETHOD0(ImapMessageSink, AbortMsgWriteStream)
+NS_SYNCRUNNABLEMETHOD0(ImapMessageSink, BeginMessageUpload)
+NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, NotifyMessageFlags, uint32_t,
+ const nsACString&, nsMsgKey, uint64_t)
+NS_SYNCRUNNABLEMETHOD3(ImapMessageSink, NotifyMessageDeleted, const char*, bool,
+ const char*)
+NS_SYNCRUNNABLEMETHOD2(ImapMessageSink, GetMessageSizeFromDB, const char*,
+ uint32_t*)
+NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, GetCurMoveCopyMessageInfo, nsIImapUrl*,
+ PRTime*, nsACString&, uint32_t*)
+
+NS_SYNCRUNNABLEMETHOD4(ImapServerSink, PossibleImapMailbox, const nsACString&,
+ char, int32_t, bool*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderNeedsACLInitialized,
+ const nsACString&, bool*)
+NS_SYNCRUNNABLEMETHOD3(ImapServerSink, AddFolderRights, const nsACString&,
+ const nsACString&, const nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, RefreshFolderRights, const nsACString&)
+NS_SYNCRUNNABLEMETHOD0(ImapServerSink, DiscoveryDone)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, OnlineFolderDelete, const nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, OnlineFolderCreateFailed,
+ const nsACString&)
+NS_SYNCRUNNABLEMETHOD3(ImapServerSink, OnlineFolderRename, nsIMsgWindow*,
+ const nsACString&, const nsACString&)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderIsNoSelect, const nsACString&,
+ bool*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, SetFolderAdminURL, const nsACString&,
+ const nsACString&)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderVerifiedOnline, const nsACString&,
+ bool*)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetCapability, eIMAPCapabilityFlags)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerID, const nsACString&)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, LoadNextQueuedUrl, nsIImapProtocol*,
+ bool*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, PrepareToRetryUrl, nsIImapUrl*,
+ nsIImapMockChannel**)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SuspendUrl, nsIImapUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, RetryUrl, nsIImapUrl*,
+ nsIImapMockChannel*)
+NS_SYNCRUNNABLEMETHOD0(ImapServerSink, AbortQueuedUrls)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, GetImapStringByName, const char*,
+ nsAString&)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, PromptLoginFailed, nsIMsgWindow*,
+ int32_t*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlert, const nsAString&,
+ nsIMsgMailNewsUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlertWithName, const char*,
+ nsIMsgMailNewsUrl*)
+NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlertFromServer, const nsACString&,
+ nsIMsgMailNewsUrl*)
+NS_SYNCRUNNABLEMETHOD0(ImapServerSink, CommitNamespaces)
+NS_SYNCRUNNABLEMETHOD3(ImapServerSink, AsyncGetPassword, nsIImapProtocol*, bool,
+ nsAString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SyncGetPassword, nsAString&)
+NS_SYNCRUNNABLEATTRIBUTE(ImapServerSink, UserAuthenticated, bool)
+NS_SYNCRUNNABLEMETHOD3(ImapServerSink, SetMailServerUrls, const nsACString&,
+ const nsACString&, const nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetArbitraryHeaders, nsACString&)
+NS_SYNCRUNNABLEMETHOD0(ImapServerSink, ForgetPassword)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetShowAttachmentsInline, bool*)
+NS_SYNCRUNNABLEMETHOD3(ImapServerSink, CramMD5Hash, const char*, const char*,
+ char**)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetLoginUsername, nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, UpdateTrySTARTTLSPref, bool)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetOriginalUsername, nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerKey, nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerPassword, nsAString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, RemoveServerConnection, nsIImapProtocol*)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerShuttingDown, bool*)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, ResetServerConnection, const nsACString&)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerDoingLsub, bool)
+NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerUtf8AcceptEnabled, bool)
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS(OAuth2ThreadHelper, msgIOAuth2ModuleListener)
+
+OAuth2ThreadHelper::OAuth2ThreadHelper(nsIMsgIncomingServer* aServer)
+ : mMonitor("OAuth thread lock"), mServer(aServer) {}
+
+OAuth2ThreadHelper::~OAuth2ThreadHelper() {
+ if (mOAuth2Support) {
+ NS_ReleaseOnMainThread("OAuth2ThreadHelper::mOAuth2Support",
+ mOAuth2Support.forget());
+ }
+}
+
+bool OAuth2ThreadHelper::SupportsOAuth2() {
+ // Acquire a lock early, before reading anything. Guarantees memory visibility
+ // issues.
+ MonitorAutoLock lockGuard(mMonitor);
+
+ // If we don't have a server, we can't init, and therefore, we don't support
+ // OAuth2.
+ if (!mServer) return false;
+
+ // If we have this, then we support OAuth2.
+ if (mOAuth2Support) return true;
+
+ // Initialize. This needs to be done on-main-thread: if we're off that thread,
+ // synchronously dispatch to the main thread.
+ if (NS_IsMainThread()) {
+ MonitorAutoUnlock lockGuard(mMonitor);
+ Init();
+ } else {
+ nsCOMPtr<nsIRunnable> runInit = NewRunnableMethod(
+ "OAuth2ThreadHelper::SupportsOAuth2", this, &OAuth2ThreadHelper::Init);
+ nsresult rv = NS_DispatchToMainThread(runInit);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ mMonitor.Wait();
+ }
+
+ // After synchronously initializing, if we didn't get an object, then we don't
+ // support XOAuth2.
+ return mOAuth2Support != nullptr;
+}
+
+void OAuth2ThreadHelper::GetXOAuth2String(nsACString& base64Str) {
+ MOZ_ASSERT(!NS_IsMainThread(), "This method cannot run on the main thread");
+
+ // Acquire a lock early, before reading anything. Guarantees memory visibility
+ // issues.
+ MonitorAutoLock lockGuard(mMonitor);
+
+ // Umm... what are you trying to do?
+ if (!mOAuth2Support) return;
+
+ nsCOMPtr<nsIRunnable> runInit =
+ NewRunnableMethod("OAuth2ThreadHelper::GetXOAuth2String", this,
+ &OAuth2ThreadHelper::Connect);
+ nsresult rv = NS_DispatchToMainThread(runInit);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ mMonitor.Wait();
+
+ nsCOMPtr<nsIRunnable> shutdownNotify = NS_NewRunnableFunction(
+ "GetXOAuth2StringShutdownNotifier", [self = RefPtr(this)]() {
+ mozilla::RunOnShutdown([self]() {
+ // Notify anyone waiting that we're done.
+ self->mMonitor.Notify();
+ });
+ });
+ rv = NS_DispatchToMainThread(shutdownNotify.forget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Now we either have the string, or we failed (in which case the string is
+ // empty).
+ base64Str = mOAuth2String;
+}
+
+void OAuth2ThreadHelper::Init() {
+ MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
+ MonitorAutoLock lockGuard(mMonitor);
+
+ // Create the OAuth2 helper module and initialize it. If the preferences are
+ // not set up on this server, we don't support OAuth2, and we nullify our
+ // members to indicate this.
+ mOAuth2Support = do_CreateInstance(MSGIOAUTH2MODULE_CONTRACTID);
+ if (mOAuth2Support) {
+ bool supportsOAuth = false;
+ mOAuth2Support->InitFromMail(mServer, &supportsOAuth);
+ if (!supportsOAuth) mOAuth2Support = nullptr;
+ }
+
+ // There is now no longer any need for the server. Kill it now--this helps
+ // prevent us from maintaining a refcount cycle.
+ mServer = nullptr;
+
+ // Notify anyone waiting that we're done.
+ mMonitor.Notify();
+}
+
+void OAuth2ThreadHelper::Connect() {
+ MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
+ MOZ_ASSERT(mOAuth2Support, "Should not be here if no OAuth2 support");
+
+ // OK to delay lock since mOAuth2Support is only written on main thread.
+ nsresult rv = mOAuth2Support->Connect(true, this);
+ // If the method failed, we'll never get a callback, so notify the monitor
+ // immediately so that IMAP can react.
+ if (NS_FAILED(rv)) {
+ MonitorAutoLock lockGuard(mMonitor);
+ mMonitor.Notify();
+ }
+}
+
+nsresult OAuth2ThreadHelper::OnSuccess(const nsACString& aOAuth2String) {
+ MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
+ MonitorAutoLock lockGuard(mMonitor);
+
+ MOZ_ASSERT(mOAuth2Support, "Should not be here if no OAuth2 support");
+ mOAuth2String = aOAuth2String;
+ mMonitor.Notify();
+ return NS_OK;
+}
+
+nsresult OAuth2ThreadHelper::OnFailure(nsresult aError) {
+ MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
+ MonitorAutoLock lockGuard(mMonitor);
+
+ mOAuth2String.Truncate();
+ mMonitor.Notify();
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/imap/src/nsSyncRunnableHelpers.h b/comm/mailnews/imap/src/nsSyncRunnableHelpers.h
new file mode 100644
index 0000000000..9be1aafef6
--- /dev/null
+++ b/comm/mailnews/imap/src/nsSyncRunnableHelpers.h
@@ -0,0 +1,149 @@
+/* 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/. */
+
+#ifndef nsSyncRunnableHelpers_h
+#define nsSyncRunnableHelpers_h
+
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+
+#include "mozilla/Monitor.h"
+#include "msgIOAuth2Module.h"
+#include "nsIStreamListener.h"
+#include "nsIImapMailFolderSink.h"
+#include "nsIImapServerSink.h"
+#include "nsIImapProtocolSink.h"
+#include "nsIImapMessageSink.h"
+
+// The classes in this file proxy method calls to the main thread
+// synchronously. The main thread must not block on this thread, or a
+// deadlock condition can occur.
+
+class StreamListenerProxy final : public nsIStreamListener {
+ public:
+ explicit StreamListenerProxy(nsIStreamListener* receiver)
+ : mReceiver(receiver) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ receiver, "Null receiver, crash now to get feedback instead of later");
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ private:
+ ~StreamListenerProxy() {
+ NS_ReleaseOnMainThread("StreamListenerProxy::mReceiver",
+ mReceiver.forget());
+ }
+ nsCOMPtr<nsIStreamListener> mReceiver;
+};
+
+class ImapMailFolderSinkProxy final : public nsIImapMailFolderSink {
+ public:
+ explicit ImapMailFolderSinkProxy(nsIImapMailFolderSink* receiver)
+ : mReceiver(receiver) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ receiver, "Null receiver, crash now to get feedback instead of later");
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPMAILFOLDERSINK
+
+ private:
+ ~ImapMailFolderSinkProxy() {
+ NS_ReleaseOnMainThread("ImapMailFolderSinkProxy::mReceiver",
+ mReceiver.forget());
+ }
+ nsCOMPtr<nsIImapMailFolderSink> mReceiver;
+};
+
+class ImapServerSinkProxy final : public nsIImapServerSink {
+ public:
+ explicit ImapServerSinkProxy(nsIImapServerSink* receiver)
+ : mReceiver(receiver) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ receiver, "Null receiver, crash now to get feedback instead of later");
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPSERVERSINK
+
+ private:
+ ~ImapServerSinkProxy() {
+ NS_ReleaseOnMainThread("ImapServerSinkProxy::mReceiver",
+ mReceiver.forget());
+ }
+ nsCOMPtr<nsIImapServerSink> mReceiver;
+};
+
+class ImapMessageSinkProxy final : public nsIImapMessageSink {
+ public:
+ explicit ImapMessageSinkProxy(nsIImapMessageSink* receiver)
+ : mReceiver(receiver) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ receiver, "Null receiver, crash now to get feedback instead of later");
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPMESSAGESINK
+
+ private:
+ ~ImapMessageSinkProxy() {
+ NS_ReleaseOnMainThread("ImapMessageSinkProxy::mReceiver",
+ mReceiver.forget());
+ }
+ nsCOMPtr<nsIImapMessageSink> mReceiver;
+};
+
+class ImapProtocolSinkProxy final : public nsIImapProtocolSink {
+ public:
+ explicit ImapProtocolSinkProxy(nsIImapProtocolSink* receiver)
+ : mReceiver(receiver) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ receiver, "Null receiver, crash now to get feedback instead of later");
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMAPPROTOCOLSINK
+
+ private:
+ ~ImapProtocolSinkProxy() {
+ NS_ReleaseOnMainThread("ImapProtocolSinkProxy::mReceiver",
+ mReceiver.forget());
+ }
+ nsCOMPtr<nsIImapProtocolSink> mReceiver;
+};
+
+class msgIOAuth2Module;
+class nsIMsgIncomingServer;
+
+namespace mozilla {
+namespace mailnews {
+
+class OAuth2ThreadHelper final : public msgIOAuth2ModuleListener {
+ public:
+ explicit OAuth2ThreadHelper(nsIMsgIncomingServer* aServer);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MSGIOAUTH2MODULELISTENER
+
+ bool SupportsOAuth2();
+ void GetXOAuth2String(nsACString& base64Str);
+
+ private:
+ ~OAuth2ThreadHelper();
+ void Init();
+ void Connect();
+
+ Monitor mMonitor;
+ nsCOMPtr<msgIOAuth2Module> mOAuth2Support;
+ nsCOMPtr<nsIMsgIncomingServer> mServer;
+ nsCString mOAuth2String;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif // nsSyncRunnableHelpers_h
diff --git a/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp b/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp
new file mode 100644
index 0000000000..a7658322b8
--- /dev/null
+++ b/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include <stdio.h>
+#include "TestHarness.h"
+#include "nsCOMPtr.h"
+#include "msgCore.h"
+#include "nsImapProtocol.h"
+
+struct msgState {
+ uint32_t uid;
+ uint16_t flag;
+ uint32_t index;
+};
+
+char errorMsg[200];
+
+const char* MainChecks(nsImapFlagAndUidState* flagState,
+ struct msgState* expectedState, uint32_t numMessages,
+ uint32_t expectedNumUnread) {
+ // Verify that flag state matches the expected state.
+ for (uint32_t i = 0; i < numMessages; i++) {
+ uint32_t uid;
+ uint16_t flag;
+ flagState->GetUidOfMessage(expectedState[i].index, &uid);
+ flagState->GetMessageFlags(expectedState[i].index, &flag);
+ if (uid != expectedState[i].uid) {
+ PR_snprintf(errorMsg, sizeof(errorMsg),
+ "expected uid %d, got %d at index %d\n", expectedState[i].uid,
+ uid, i);
+ return errorMsg;
+ }
+ if (flag != expectedState[i].flag) {
+ PR_snprintf(errorMsg, sizeof(errorMsg),
+ "expected flag %d, got %d at index %d\n",
+ expectedState[i].flag, flag, i);
+ return errorMsg;
+ }
+ }
+ int32_t numMsgsInFlagState;
+ int32_t numUnread = 0;
+ int32_t expectedMsgIndex = 0;
+
+ flagState->GetNumberOfMessages(&numMsgsInFlagState);
+ for (int32_t msgIndex = 0; msgIndex < numMsgsInFlagState; msgIndex++) {
+ uint32_t uidOfMessage;
+ flagState->GetUidOfMessage(msgIndex, &uidOfMessage);
+ if (!uidOfMessage || uidOfMessage == nsMsgKey_None) continue;
+ if (uidOfMessage != expectedState[expectedMsgIndex++].uid) {
+ PR_snprintf(
+ errorMsg, sizeof(errorMsg),
+ "got a uid w/o a match in expected state, uid %d at index %d\n",
+ uidOfMessage, msgIndex);
+ return errorMsg;
+ }
+ imapMessageFlagsType flags;
+ flagState->GetMessageFlags(msgIndex, &flags);
+ if (!(flags & kImapMsgSeenFlag)) numUnread++;
+ }
+ if (numUnread != expectedNumUnread) {
+ PR_snprintf(errorMsg, sizeof(errorMsg),
+ "expected %d unread message, got %d\n", expectedNumUnread,
+ numUnread);
+ return errorMsg;
+ }
+ return nullptr;
+}
+
+// General note about return values:
+// return 1 for a setup or xpcom type failure, return 2 for a real test failure
+int main(int argc, char** argv) {
+ ScopedXPCOM xpcom("TestImapFlagAndUidState.cpp");
+ if (xpcom.failed()) return 1;
+
+ struct msgState msgState1[] = {{10, kImapMsgSeenFlag, 0},
+ {15, kImapMsgSeenFlag, 1},
+ {16, kImapMsgSeenFlag, 2},
+ {17, kImapMsgSeenFlag, 3},
+ {18, kImapMsgSeenFlag, 4}};
+
+ RefPtr<nsImapFlagAndUidState> flagState = new nsImapFlagAndUidState(10);
+ int32_t numMsgs = sizeof(msgState1) / sizeof(msgState1[0]);
+ for (int32_t i = 0; i < numMsgs; i++)
+ flagState->AddUidFlagPair(msgState1[i].uid, msgState1[i].flag,
+ msgState1[i].index);
+
+ const char* error = MainChecks(flagState, msgState1, numMsgs, 0);
+ if (error) {
+ printf("TEST-UNEXPECTED-FAIL | %s | %s\n", __FILE__, error);
+ return 1;
+ }
+
+ // Now reset all
+ flagState->Reset();
+
+ // This tests adding some messages to a partial uid flag state,
+ // i.e., CONDSTORE.
+ struct msgState msgState2[] = {{68, kImapMsgSeenFlag, 69},
+ {71, kImapMsgSeenFlag, 70},
+ {73, kImapMsgSeenFlag, 71}};
+ numMsgs = sizeof(msgState2) / sizeof(msgState2[0]);
+ for (int32_t i = 0; i < numMsgs; i++)
+ flagState->AddUidFlagPair(msgState2[i].uid, msgState2[i].flag,
+ msgState2[i].index);
+ error = MainChecks(flagState, msgState2, numMsgs, 0);
+ if (error) {
+ printf("TEST-UNEXPECTED-FAIL | %s | %s\n", __FILE__, error);
+ return 1;
+ }
+ // Reset all
+ flagState->Reset();
+ // This tests generating a uid string from a non-sequential set of
+ // messages where the first message is not in the flag state, but the
+ // missing message from the sequence is in the set. I.e., we're
+ // generating a uid string from 69,71, but only 70 and 71 are in
+ // the flag state.
+ struct msgState msgState3[] = {{10, kImapMsgSeenFlag, 0},
+ {69, kImapMsgSeenFlag, 1},
+ {70, kImapMsgSeenFlag, 2},
+ {71, kImapMsgSeenFlag, 3}};
+
+ flagState->SetPartialUIDFetch(false);
+ numMsgs = sizeof(msgState3) / sizeof(msgState3[0]);
+ for (int32_t i = 0; i < numMsgs; i++)
+ flagState->AddUidFlagPair(msgState3[i].uid, msgState3[i].flag,
+ msgState3[i].index);
+ flagState->ExpungeByIndex(2);
+ nsCString uidString;
+ uint32_t msgUids[] = {69, 71};
+ uint32_t msgCount = 2;
+ AllocateImapUidString(&msgUids[0], msgCount, flagState, uidString);
+ if (!uidString.EqualsLiteral("71")) {
+ printf("TEST-UNEXPECTED-FAIL | uid String is %s, not 71 | %s\n",
+ uidString.get(), __FILE__);
+ return -1;
+ }
+ // Reset all
+ flagState->Reset();
+ // This tests the middle message missing from the flag state.
+ struct msgState msgState4[] = {{10, kImapMsgSeenFlag, 0},
+ {69, kImapMsgSeenFlag, 1},
+ {70, kImapMsgSeenFlag, 2},
+ {71, kImapMsgSeenFlag, 3},
+ {73, kImapMsgSeenFlag, 4}};
+
+ flagState->SetPartialUIDFetch(false);
+ numMsgs = sizeof(msgState4) / sizeof(msgState4[0]);
+ for (int32_t i = 0; i < numMsgs; i++)
+ flagState->AddUidFlagPair(msgState4[i].uid, msgState4[i].flag,
+ msgState4[i].index);
+ flagState->ExpungeByIndex(4);
+ uint32_t msgUids2[] = {69, 71, 73};
+ msgCount = 3;
+ nsCString uidString2;
+
+ AllocateImapUidString(&msgUids2[0], msgCount, flagState, uidString2);
+ if (!uidString2.EqualsLiteral("69,73")) {
+ printf("TEST-UNEXPECTED-FAIL | uid String is %s, not 71 | %s\n",
+ uidString.get(), __FILE__);
+ return -1;
+ }
+
+ printf("TEST-PASS | %s | all tests passed\n", __FILE__);
+ return 0;
+}
diff --git a/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp b/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp
new file mode 100644
index 0000000000..442ca03567
--- /dev/null
+++ b/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include <stdio.h>
+#include "TestHarness.h"
+#include "nsCOMPtr.h"
+#include "msgCore.h"
+#include "nsImapProtocol.h"
+
+// What to check
+enum EHdrArrayCheck { eAddNoCheck, eCheckSame };
+
+int MainChecks(nsMsgImapHdrXferInfo* hdrInfo, nsIImapHeaderInfo** hdrArray,
+ EHdrArrayCheck hdrArrayCheck) {
+ nsCOMPtr<nsIImapHeaderInfo> hdr;
+ int32_t numHdrs = -1;
+
+ // Check the number of headers initially is zero
+ if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1;
+
+ if (numHdrs != 0) return 2;
+
+ // Get a header that doesn't exist
+ if (hdrInfo->GetHeader(1, getter_AddRefs(hdr)) != NS_ERROR_NULL_POINTER)
+ return 3;
+
+ int32_t i;
+ for (i = 0; i < kNumHdrsToXfer; ++i) {
+ // Now kick off a new one.
+ hdr = hdrInfo->StartNewHdr();
+ if (!hdr) return 4;
+
+ // Check pointers are different or not depending on which cycle we are in
+ switch (hdrArrayCheck) {
+ case eAddNoCheck:
+ hdrArray[i] = hdr;
+ break;
+ case eCheckSame:
+ if (hdrArray[i] != hdr) return 5;
+ break;
+ default:
+ return 1;
+ }
+
+ if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1;
+
+ if (numHdrs != i + 1) return 7;
+ }
+
+ // Now try and get one more (this should return null)
+ if (hdrInfo->StartNewHdr()) return 8;
+
+ // Now check the number of headers
+ if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1;
+
+ if (numHdrs != kNumHdrsToXfer) return 9;
+
+ // Now check our pointers align with those from GetHeader
+ if (hdrArrayCheck != 2) {
+ for (i = 0; i < kNumHdrsToXfer; ++i) {
+ if (NS_FAILED(hdrInfo->GetHeader(i, getter_AddRefs(hdr)))) return 1;
+
+ if (hdr != hdrArray[i]) return 10;
+ }
+ }
+ return 0;
+}
+
+// General note about return values:
+// return 1 for a setup or xpcom type failure, return 2 for a real test failure
+int main(int argc, char** argv) {
+ ScopedXPCOM xpcom("TestImapHdrXferInfo.cpp");
+ if (xpcom.failed()) return 1;
+
+ RefPtr<nsMsgImapHdrXferInfo> hdrInfo = new nsMsgImapHdrXferInfo();
+ // Purposely not reference counted to ensure we get the same pointers the
+ // second time round MainChecks.
+ nsIImapHeaderInfo* hdrArray[kNumHdrsToXfer] = {nullptr};
+
+ int result = MainChecks(hdrInfo, hdrArray, eAddNoCheck);
+ if (result) {
+ printf("TEST-UNEXPECTED-FAIL | %s | %d\n", __FILE__, result);
+ return result;
+ }
+
+ // Now reset all
+ hdrInfo->ResetAll();
+
+ // and repeat
+ result = MainChecks(hdrInfo, hdrArray, eCheckSame);
+ if (result) {
+ // add 100 to differentiate results
+ result += 100;
+ printf("TEST-UNEXPECTED-FAIL | %s | %d\n", __FILE__, result);
+ return result;
+ }
+
+ printf("TEST-PASS | %s | all tests passed\n", __FILE__);
+ return result;
+}
diff --git a/comm/mailnews/imap/test/moz.build b/comm/mailnews/imap/test/moz.build
new file mode 100644
index 0000000000..8b6d9424ce
--- /dev/null
+++ b/comm/mailnews/imap/test/moz.build
@@ -0,0 +1,27 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell-cpp.ini",
+ "unit/xpcshell.ini",
+ "unit/xpcshell_maildir-cpp.ini",
+ "unit/xpcshell_maildir.ini",
+]
+
+LOCAL_INCLUDES += [
+ "../../base/src",
+ "../src",
+ "/xpcom/tests",
+]
+
+USE_LIBS += [
+ "msgbsutl_s",
+ "msgimap_s",
+ "nspr",
+ "xpcomglue_s",
+ "xul",
+]
+
+OS_LIBS += CONFIG["MOZ_ZLIB_LIBS"]
diff --git a/comm/mailnews/imap/test/unit/head_imap_maildir.js b/comm/mailnews/imap/test/unit/head_imap_maildir.js
new file mode 100644
index 0000000000..676becd52a
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/head_imap_maildir.js
@@ -0,0 +1,9 @@
+/* import-globals-from head_server.js */
+load("head_server.js");
+
+info("Running test with maildir");
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/maildirstore;1"
+);
diff --git a/comm/mailnews/imap/test/unit/head_server.js b/comm/mailnews/imap/test/unit/head_server.js
new file mode 100644
index 0000000000..b092b9a21b
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/head_server.js
@@ -0,0 +1,201 @@
+// We can be executed from multiple depths
+// Provide gDEPTH if not already defined
+if (typeof gDEPTH == "undefined") {
+ var gDEPTH = "../../../../";
+}
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo.
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+// Import fakeserver
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+
+var {
+ ImapDaemon,
+ ImapMessage,
+ configurations,
+ IMAP_RFC3501_handler,
+ mixinExtension,
+ IMAP_RFC2197_extension,
+ IMAP_RFC2342_extension,
+ IMAP_RFC3348_extension,
+ IMAP_RFC4315_extension,
+ IMAP_RFC5258_extension,
+ IMAP_RFC2195_extension,
+} = ChromeUtils.import("resource://testing-common/mailnews/Imapd.jsm");
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+
+function makeServer(daemon, infoString, otherProps) {
+ if (infoString in configurations) {
+ return makeServer(daemon, configurations[infoString].join(","), otherProps);
+ }
+
+ function createHandler(d) {
+ var handler = new IMAP_RFC3501_handler(d);
+ if (!infoString) {
+ infoString = "RFC2195";
+ }
+
+ var parts = infoString.split(/ *, */);
+ for (var part of parts) {
+ if (part.startsWith("RFC")) {
+ let ext;
+ switch (part) {
+ case "RFC2197":
+ ext = IMAP_RFC2197_extension;
+ break;
+ case "RFC2342":
+ ext = IMAP_RFC2342_extension;
+ break;
+ case "RFC3348":
+ ext = IMAP_RFC3348_extension;
+ break;
+ case "RFC4315":
+ ext = IMAP_RFC4315_extension;
+ break;
+ case "RFC5258":
+ ext = IMAP_RFC5258_extension;
+ break;
+ case "RFC2195":
+ ext = IMAP_RFC2195_extension;
+ break;
+ default:
+ throw new Error("Unknown extension: " + part);
+ }
+ mixinExtension(handler, ext);
+ }
+ }
+ if (otherProps) {
+ for (var prop in otherProps) {
+ handler[prop] = otherProps[prop];
+ }
+ }
+ return handler;
+ }
+ var server = new nsMailServer(createHandler, daemon);
+ server.start();
+ return server;
+}
+
+function createLocalIMAPServer(port, hostname = "localhost") {
+ let server = localAccountUtils.create_incoming_server(
+ "imap",
+ port,
+ "user",
+ "password",
+ hostname
+ );
+ server.QueryInterface(Ci.nsIImapIncomingServer);
+ return server;
+}
+
+// <copied from="head_maillocal.js">
+/**
+ * @param fromServer server.playTransaction
+ * @param expected ["command", "command", ...]
+ * @param withParams if false,
+ * everything apart from the IMAP command will the stripped.
+ * E.g. 'lsub "" "*"' will be compared as 'lsub'.
+ * Exception is "authenticate", which also get its first parameter in upper case,
+ * e.g. "authenticate CRAM-MD5".
+ */
+function do_check_transaction(fromServer, expected, withParams) {
+ // If we don't spin the event loop before starting the next test, the readers
+ // aren't expired. In this case, the "real" real transaction is the last one.
+ if (fromServer instanceof Array) {
+ fromServer = fromServer[fromServer.length - 1];
+ }
+
+ let realTransaction = [];
+ for (let i = 0; i < fromServer.them.length; i++) {
+ var line = fromServer.them[i]; // e.g. '1 login "user" "password"'
+ var components = line.split(" ");
+ if (components.length < 2) {
+ throw new Error("IMAP command in transaction log missing: " + line);
+ }
+ if (withParams) {
+ realTransaction.push(line.substr(components[0].length + 1));
+ } else if (components[1].toUpperCase() == "AUTHENTICATE") {
+ realTransaction.push(components[1] + " " + components[2].toUpperCase());
+ } else {
+ realTransaction.push(components[1]);
+ }
+ }
+
+ Assert.equal(
+ realTransaction.join(", ").toUpperCase(),
+ expected.join(", ").toUpperCase()
+ );
+}
+
+/**
+ * add a simple message to the IMAP pump mailbox
+ */
+function addImapMessage() {
+ let messages = [];
+ let messageGenerator = new MessageGenerator(); // eslint-disable-line no-undef
+ messages = messages.concat(messageGenerator.makeMessage());
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(imapMsg);
+}
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
+
+// Setup the SMTP daemon and server
+function setupSmtpServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ var server = new nsMailServer(handler, new SmtpDaemon());
+ return server;
+}
+
+// profile-after-change is not triggered in xpcshell tests, manually run the
+// getService to load the correct imap modules.
+Cc["@mozilla.org/messenger/imap-module-loader;1"].getService();
diff --git a/comm/mailnews/imap/test/unit/test_ImapResponse.js b/comm/mailnews/imap/test/unit/test_ImapResponse.js
new file mode 100644
index 0000000000..4dd60e6d32
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_ImapResponse.js
@@ -0,0 +1,288 @@
+/* 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 { ImapResponse } = ChromeUtils.import(
+ "resource:///modules/ImapResponse.jsm"
+);
+var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+
+/**
+ * Test CAPABILITY response can be correctly parsed.
+ */
+add_task(function test_CapabilityResponse() {
+ let response = new ImapResponse();
+ response.parse(
+ "32 OK [CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN] server ready\r\n"
+ );
+
+ deepEqual(response.authMethods, ["LOGIN", "PLAIN"]);
+ deepEqual(response.capabilities, ["IMAP4REV1", "IDLE", "STARTTLS"]);
+
+ response = new ImapResponse();
+ response.parse("* CAPABILITY IMAP4rev1 ID IDLE STARTTLS AUTH=PLAIN\r\n");
+
+ deepEqual(response.authMethods, ["PLAIN"]);
+ deepEqual(response.capabilities, ["IMAP4REV1", "ID", "IDLE", "STARTTLS"]);
+});
+
+/**
+ * Test flags from a FETCH response can be correctly parsed.
+ */
+add_task(function test_FetchResponse_flags() {
+ let response = new ImapResponse();
+ response.parse(
+ [
+ "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded))",
+ "* 2 FETCH (UID 600 FLAGS (\\Seen))",
+ "",
+ ].join("\r\n")
+ );
+ ok(!response.done);
+
+ response.parse(
+ ["* 3 FETCH (UID 601 FLAGS ())", "40 OK Fetch completed", ""].join("\r\n")
+ );
+
+ ok(response.done);
+ deepEqual(response.messages[0], {
+ sequence: 1,
+ uid: 500,
+ flags:
+ ImapUtils.FLAG_ANSWERED | ImapUtils.FLAG_SEEN | ImapUtils.FLAG_FORWARDED,
+ keywords: "$Forwarded",
+ customAttributes: {},
+ });
+ deepEqual(response.messages[1], {
+ sequence: 2,
+ uid: 600,
+ flags: ImapUtils.FLAG_SEEN,
+ keywords: "",
+ customAttributes: {},
+ });
+ deepEqual(response.messages[2], {
+ sequence: 3,
+ uid: 601,
+ flags: 0,
+ keywords: "",
+ customAttributes: {},
+ });
+});
+
+/**
+ * Test body from a FETCH response can be correctly parsed.
+ */
+add_task(function test_messageBody() {
+ let response = new ImapResponse();
+ response.parse(
+ [
+ "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded) BODY[HEADER.FIELDS (FROM TO)] {12}",
+ "abcd",
+ "efgh",
+ ")",
+ "* 2 FETCH (UID 600 FLAGS (\\Seen) BODY[] {15}",
+ "Hello ",
+ "world",
+ ")",
+ "40 OK Fetch completed",
+ "",
+ ].join("\r\n")
+ );
+
+ equal(response.messages[0].body, "abcd\r\nefgh\r\n");
+ equal(response.messages[1].body, "Hello \r\nworld\r\n");
+});
+
+/**
+ * Test msg body spanning multiple chuncks can be correctly parsed.
+ */
+add_task(function test_messageBodyIncremental() {
+ let response = new ImapResponse();
+ // Chunk 1.
+ response.parse(
+ [
+ "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded) BODY[HEADER.FIELDS (FROM TO)] {12}",
+ "abcd",
+ "efgh",
+ ")",
+ "* 2 FETCH (UID 600 FLAGS (\\Seen) BODY[] {15}",
+ "Hel",
+ ].join("\r\n")
+ );
+ equal(response.messages[0].body, "abcd\r\nefgh\r\n");
+ ok(!response.done);
+
+ // Chunk 2.
+ response.parse("lo \r\nworld\r\n");
+ ok(!response.done);
+
+ // Chunk 3.
+ response.parse(")\r\n40 OK Fetch completed\r\n");
+ ok(response.done);
+ equal(response.messages[1].body, "Hello \r\nworld\r\n");
+});
+
+/**
+ * Test FLAGS response can be correctly parsed.
+ */
+add_task(function test_FlagsResponse() {
+ let response = new ImapResponse();
+ response.parse(
+ [
+ "* FLAGS (\\Seen \\Draft $Forwarded)",
+ "* OK [PERMANENTFLAGS (\\Seen \\Draft $Forwarded \\*)] Flags permitted.",
+ "* 6 EXISTS",
+ "* OK [UNSEEN 2] First unseen.",
+ "* OK [UIDVALIDITY 1594877893] UIDs valid",
+ "* OK [UIDNEXT 625] Predicted next UID",
+ "* OK [HIGHESTMODSEQ 1148] Highest",
+ "42 OK [READ-WRITE] Select completed",
+ "",
+ ].join("\r\n")
+ );
+
+ equal(
+ response.flags,
+ ImapUtils.FLAG_SEEN | ImapUtils.FLAG_DRAFT | ImapUtils.FLAG_FORWARDED
+ );
+ equal(
+ response.permanentflags,
+ ImapUtils.FLAG_SEEN |
+ ImapUtils.FLAG_DRAFT |
+ ImapUtils.FLAG_FORWARDED |
+ ImapUtils.FLAG_LABEL |
+ ImapUtils.FLAG_MDN_SENT |
+ ImapUtils.FLAG_FORWARDED |
+ ImapUtils.FLAG_SUPPORT_USER_FLAG
+ );
+ equal(response.highestmodseq, 1148);
+ equal(response.exists, 6);
+});
+
+/**
+ * Test mailbox updates can be correctly parsed.
+ */
+add_task(function test_MailboxResponse() {
+ let response = new ImapResponse();
+ response.parse("* 7 EXISTS\r\n");
+ response.parse("* 1 EXPUNGE\r\n* 3 EXPUNGE\r\n");
+ equal(response.exists, 7);
+ deepEqual(response.expunged, [1, 3]);
+});
+
+/**
+ * Test LIST response can be correctly parsed.
+ */
+add_task(function test_ListResponse() {
+ let response = new ImapResponse();
+ response.parse(
+ [
+ '* LIST (\\Subscribed \\NoInferiors \\Marked \\Trash) "/" "Trash"',
+ '* LIST () "/" "a \\"b\\" c"',
+ '* LIST (\\Subscribed) "/" INBOX',
+ "84 OK List completed (0.002 + 0.000 + 0.001 secs).",
+ "",
+ ].join("\r\n")
+ );
+ equal(response.mailboxes.length, 3);
+ deepEqual(response.mailboxes[0], {
+ name: "Trash",
+ delimiter: "/",
+ flags:
+ ImapUtils.FLAG_SUBSCRIBED |
+ ImapUtils.FLAG_NO_INFERIORS |
+ ImapUtils.FLAG_HAS_NO_CHILDREN |
+ ImapUtils.FLAG_MARKED |
+ ImapUtils.FLAG_IMAP_TRASH |
+ ImapUtils.FLAG_IMAP_XLIST_TRASH,
+ });
+ deepEqual(response.mailboxes[1], {
+ name: 'a "b" c',
+ delimiter: "/",
+ flags: 0,
+ });
+ deepEqual(response.mailboxes[2], {
+ name: "INBOX",
+ delimiter: "/",
+ flags: ImapUtils.FLAG_SUBSCRIBED,
+ });
+});
+
+/**
+ * Test folder names containg [] or () or "" can be correctly parsed.
+ */
+add_task(function test_parseFolderNames() {
+ let response = new ImapResponse();
+ response.parse(
+ [
+ '* LSUB () "/" "[Gmail]"',
+ '* LSUB () "/" "[Gmail]/All Mail"',
+ '* LSUB () "/" "[Gmail]/Sent"',
+ '* LSUB () "/" "[a(b)])"',
+ '* LSUB () "/" "a \\"b \\"c\\""',
+ "84 OK LSUB completed",
+ "",
+ ].join("\r\n")
+ );
+ equal(response.mailboxes.length, 5);
+ deepEqual(
+ response.mailboxes.map(x => x.name),
+ ["[Gmail]", "[Gmail]/All Mail", "[Gmail]/Sent", "[a(b)])", 'a "b "c"']
+ );
+});
+
+/**
+ * Test STATUS response can be correctly parsed.
+ */
+add_task(function test_StatusResponse() {
+ let response = new ImapResponse();
+ response.parse(
+ '* STATUS "sub folder 2" (UIDNEXT 2 MESSAGES 1 UNSEEN 1 RECENT 0)\r\n'
+ );
+ deepEqual(response.attributes, {
+ mailbox: "sub folder 2",
+ uidnext: 2,
+ messages: 1,
+ unseen: 1,
+ recent: 0,
+ });
+});
+
+/**
+ * Test GETQUOTAROOT response can be correctly parsed.
+ */
+add_task(function test_QuotaResponse() {
+ let response = new ImapResponse();
+ response.parse(
+ ["* QUOTAROOT Sent INBOX", "* QUOTA INBOX (STORAGE 123 456)", ""].join(
+ "\r\n"
+ )
+ );
+ deepEqual(response.quotaRoots, ["INBOX"]);
+ deepEqual(response.quotas, [["INBOX", "STORAGE", 123, 456]]);
+});
+
+/**
+ * Test IDLE and DONE response can be correctly parsed.
+ */
+add_task(function test_IdleDoneResponse() {
+ let response = new ImapResponse();
+ response.parse("+ idling\r\n");
+ deepEqual(
+ [response.tag, response.status, response.done],
+ ["+", "idling", true]
+ );
+
+ response = new ImapResponse();
+ response.parse(["+ idling", "75 OK Completed", ""].join("\r\n"));
+ deepEqual([response.tag, response.status, response.done], [75, "OK", true]);
+});
+
+/**
+ * Test SEARCH response can be correctly parsed.
+ */
+add_task(function test_SearchResponse() {
+ let response = new ImapResponse();
+ response.parse("* SEARCH 1 4 9\r\n90 OK SEARCH COMPLETED\r\n");
+ deepEqual(response.search, [1, 4, 9]);
+});
diff --git a/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js b/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js
new file mode 100644
index 0000000000..101413d491
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js
@@ -0,0 +1,88 @@
+/* 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/. */
+
+/*
+ * Test autosync date constraints
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gMsgImapInboxFolder;
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox) {
+ // Create the ImapMessages and store them on the mailbox
+ messages.forEach(function (message) {
+ let dataUri = "data:text/plain," + message.toMessageString();
+ mailbox.addMessage(new ImapMessage(dataUri, mailbox.uidnext++, []));
+ });
+}
+
+add_setup(function () {
+ Services.prefs.setIntPref("mail.server.server1.autosync_max_age_days", 4);
+
+ setupIMAPPump();
+
+ gMsgImapInboxFolder = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder);
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ gMsgImapInboxFolder.hierarchyDelimiter = "/";
+ gMsgImapInboxFolder.verifiedAsOnlineFolder = true;
+
+ // Add a couple of messages to the INBOX
+ // this is synchronous, afaik
+ let messageGenerator = new MessageGenerator();
+
+ // build up a diverse list of messages
+ let messages = [];
+ messages = messages.concat(
+ messageGenerator.makeMessage({ age: { days: 2, hours: 1 } })
+ );
+ messages = messages.concat(
+ messageGenerator.makeMessage({ age: { days: 8, hours: 1 } })
+ );
+ messages = messages.concat(
+ messageGenerator.makeMessage({ age: { days: 10, hours: 1 } })
+ );
+
+ addMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX"));
+});
+
+add_task(async function downloadForOffline() {
+ // ...and download for offline use.
+ // This downloads all messages, ignoring the autosync age constraints.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(function test_applyRetentionSettings() {
+ IMAPPump.inbox.applyRetentionSettings();
+ let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages();
+ if (enumerator) {
+ let now = new Date();
+ let dateInSeconds = now.getSeconds();
+ let cutOffDateInSeconds = dateInSeconds - 5 * 60 * 24;
+ for (let header of enumerator) {
+ if (header instanceof Ci.nsIMsgDBHdr) {
+ if (header.dateInSeconds < cutOffDateInSeconds) {
+ Assert.equal(header.getStringProperty("pendingRemoval"), "1");
+ } else {
+ Assert.equal(header.getStringProperty("pendingRemoval"), "");
+ }
+ }
+ }
+ }
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_bccProperty.js b/comm/mailnews/imap/test/unit/test_bccProperty.js
new file mode 100644
index 0000000000..42795245c7
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_bccProperty.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+/*
+ * Test to ensure that BCC gets added to message headers on IMAP download
+ *
+ * adapted from test_downloadOffline.js
+ *
+ * original author Kent James <kent@caspia.com>
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFileName = "draft1";
+var gMsgFile = do_get_file("../../../data/" + gFileName);
+
+add_setup(async function () {
+ setupIMAPPump();
+
+ /*
+ * Ok, prelude done. Read the original message from disk
+ * (through a file URI), and add it to the Inbox.
+ */
+ let msgfileuri = Services.io
+ .newFileURI(gMsgFile)
+ .QueryInterface(Ci.nsIFileURL);
+
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, [])
+ );
+
+ // ...and download for offline use.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(function checkBccs() {
+ // locate the new message by enumerating through the database
+ for (let hdr of IMAPPump.inbox.msgDatabase.enumerateMessages()) {
+ Assert.ok(hdr.bccList.includes("Another Person"));
+ Assert.ok(hdr.bccList.includes("<u1@example.com>"));
+ Assert.ok(!hdr.bccList.includes("IDoNotExist"));
+ }
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_bug460636.js b/comm/mailnews/imap/test/unit/test_bug460636.js
new file mode 100644
index 0000000000..6cb8b60056
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_bug460636.js
@@ -0,0 +1,82 @@
+/*
+ * Test bug 460636 - nsMsgSaveAsListener sometimes inserts extra LF characters
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gSavedMsgFile;
+
+var gIMAPService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+].getService(Ci.nsIMsgMessageService);
+
+var gFileName = "bug460636";
+var gMsgFile = do_get_file("../../../data/" + gFileName);
+
+add_task(async function run_the_test() {
+ await setup();
+ await checkSavedMessage();
+ teardown();
+});
+
+async function setup() {
+ setupIMAPPump();
+
+ // Ok, prelude done. Read the original message from disk
+ // (through a file URI), and add it to the Inbox.
+ var msgfileuri = Services.io
+ .newFileURI(gMsgFile)
+ .QueryInterface(Ci.nsIFileURL);
+
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+
+ // Save the message to a local file. IMapMD corresponds to
+ // <profile_dir>/mailtest/ImapMail (where fakeserver puts the IMAP mailbox
+ // files). If we pass the test, we'll remove the file afterwards
+ // (cf. UrlListener), otherwise it's kept in IMapMD.
+ gSavedMsgFile = Services.dirsvc.get("IMapMD", Ci.nsIFile);
+ gSavedMsgFile.append(gFileName + ".eml");
+
+ // From nsIMsgMessageService.idl:
+ // void SaveMessageToDisk(in string aMessageURI, in nsIFile aFile,
+ // in boolean aGenerateDummyEnvelope,
+ // in nsIUrlListener aUrlListener, out nsIURI aURL,
+ // in boolean canonicalLineEnding,
+ // in nsIMsgWindow aMsgWindow);
+ // Enforcing canonicalLineEnding (i.e., CRLF) makes sure that the
+ let promiseUrlListener2 = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPService.SaveMessageToDisk(
+ "imap-message://user@localhost/INBOX#" + (IMAPPump.mailbox.uidnext - 1),
+ gSavedMsgFile,
+ false,
+ promiseUrlListener2,
+ {},
+ true,
+ null
+ );
+ await promiseUrlListener2.promise;
+}
+
+async function checkSavedMessage() {
+ Assert.equal(
+ await IOUtils.readUTF8(gMsgFile.path),
+ await IOUtils.readUTF8(gSavedMsgFile.path)
+ );
+}
+
+function teardown() {
+ try {
+ gSavedMsgFile.remove(false);
+ } catch (ex) {
+ dump(ex);
+ do_throw(ex);
+ }
+ teardownIMAPPump();
+}
diff --git a/comm/mailnews/imap/test/unit/test_chunkLastLF.js b/comm/mailnews/imap/test/unit/test_chunkLastLF.js
new file mode 100644
index 0000000000..003320ad2d
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_chunkLastLF.js
@@ -0,0 +1,108 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test content length for the IMAP protocol. This focuses on necko URLs
+ * that are run externally.
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFile = do_get_file("../../../data/bug92111b");
+var gIMAPDaemon, gIMAPServer, gIMAPIncomingServer;
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessageToServer(file, mailbox) {
+ let URI = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ let msg = new ImapMessage(URI.spec, mailbox.uidnext++, []);
+ // underestimate the actual file size, like some IMAP servers do
+ msg.setSize(file.fileSize - 55);
+ mailbox.addMessage(msg);
+}
+
+add_task(async function verifyContentLength() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+
+ // Crank down the message chunk size to make test cases easier
+ Services.prefs.setBoolPref("mail.server.default.fetch_by_chunks", true);
+ Services.prefs.setIntPref("mail.imap.chunk_size", 1000);
+ Services.prefs.setIntPref("mail.imap.min_chunk_size_threshold", 1500);
+ Services.prefs.setIntPref("mail.imap.chunk_add", 0);
+
+ // set up IMAP fakeserver and incoming server
+ gIMAPDaemon = new ImapDaemon();
+ gIMAPServer = makeServer(gIMAPDaemon, "");
+ gIMAPIncomingServer = createLocalIMAPServer(gIMAPServer.port);
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1);
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);
+
+ dump("adding message to server\n");
+ // Add a message to the IMAP server
+ addMessageToServer(gFile, gIMAPDaemon.getMailbox("INBOX"));
+
+ let imapS = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+ ].getService(Ci.nsIMsgMessageService);
+
+ let uri = imapS.getUrlForUri("imap-message://user@localhost/INBOX#1");
+
+ // Get a channel from this URI, and check its content length
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ let promiseStreamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Read all the contents
+ channel.asyncOpen(promiseStreamListener, null);
+ let streamData = (await promiseStreamListener.promise).replace(/\r\n/g, "\n");
+
+ // Now check whether our stream listener got the right bytes
+ // First, clean up line endings to avoid CRLF vs. LF differences
+ let origData = (await IOUtils.readUTF8(gFile.path)).replace(/\r\n/g, "\n");
+ Assert.equal(origData.length, streamData.length);
+ Assert.equal(origData, streamData);
+
+ // Now try an attachment. &part=1.2
+ // let attachmentURL = Services.io.newURI(neckoURL.value.spec + "&part=1.2",
+ // null, null);
+ // let attachmentChannel = Services.io.newChannelFromURI(attachmentURL,
+ // null,
+ // Services.scriptSecurityManager.getSystemPrincipal(),
+ // null,
+ // Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ // Ci.nsIContentPolicy.TYPE_OTHER);
+ // Currently attachments have their content length set to the length of the
+ // entire message
+ // do_check_eq(attachmentChannel.contentLength, gFile.fileSize);
+
+ gIMAPIncomingServer.closeCachedConnections();
+ gIMAPServer.stop();
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/imap/test/unit/test_compactOfflineStore.js b/comm/mailnews/imap/test/unit/test_compactOfflineStore.js
new file mode 100644
index 0000000000..9c3f0ef898
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_compactOfflineStore.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+/*
+ * Test to ensure that compacting offline stores works correctly with imap folders
+ * and returns success.
+ */
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Globals
+var gRootFolder;
+var gImapInboxOfflineStoreSize;
+
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgFile2 = do_get_file("../../../data/bugmail11");
+// var gMsgFile3 = do_get_file("../../../data/draft1");
+var gMsgFile4 = do_get_file("../../../data/bugmail7");
+var gMsgFile5 = do_get_file("../../../data/bugmail6");
+
+// Copied straight from the example files
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+// var gMsgId3 = "4849BF7B.2030800@example.com";
+var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org";
+var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org";
+
+// Adds some messages directly to a mailbox (e.g. new mail).
+function addMessagesToServer(messages, mailbox) {
+ // For every message we have, we need to convert it to a file:/// URI
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ });
+}
+
+function addGeneratedMessagesToServer(messages, mailbox) {
+ // Create the ImapMessages and store them on the mailbox
+ messages.forEach(function (message) {
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message.toMessageString())
+ );
+ mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, []));
+ });
+}
+
+function checkOfflineStore(prevOfflineStoreSize) {
+ let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages();
+ if (enumerator) {
+ for (let header of enumerator) {
+ // this will verify that the message in the offline store
+ // starts with "From " - otherwise, it returns an error.
+ if (
+ header instanceof Ci.nsIMsgDBHdr &&
+ header.flags & Ci.nsMsgMessageFlags.Offline
+ ) {
+ IMAPPump.inbox.getLocalMsgStream(header).close();
+ }
+ }
+ }
+ // check that the offline store shrunk by at least 100 bytes.
+ // (exact calculation might be fragile).
+ Assert.ok(prevOfflineStoreSize > IMAPPump.inbox.filePath.fileSize + 100);
+}
+
+add_setup(function () {
+ setupIMAPPump();
+
+ gRootFolder = IMAPPump.incomingServer.rootFolder;
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+
+ let messageGenerator = new MessageGenerator();
+ let messages = [];
+ for (let i = 0; i < 50; i++) {
+ messages = messages.concat(messageGenerator.makeMessage());
+ }
+
+ addGeneratedMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX"));
+
+ // Add a couple of messages to the INBOX
+ // this is synchronous, afaik
+ addMessagesToServer(
+ [
+ { file: gMsgFile1, messageId: gMsgId1 },
+ { file: gMsgFile4, messageId: gMsgId4 },
+ { file: gMsgFile2, messageId: gMsgId2 },
+ { file: gMsgFile5, messageId: gMsgId5 },
+ ],
+ IMAPPump.daemon.getMailbox("INBOX")
+ );
+});
+
+add_task(async function downloadForOffline() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(async function markOneMsgDeleted() {
+ // mark a message deleted, and then do a compact of just
+ // that folder.
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId5);
+ // store the deleted flag
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.storeImapFlags(0x0008, true, [msgHdr.messageKey], listener);
+ await listener.promise;
+});
+
+add_task(async function compactOneFolder() {
+ IMAPPump.incomingServer.deleteModel = Ci.nsMsgImapDeleteModels.IMAPDelete;
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.compact(listener, null);
+ await listener.promise;
+});
+
+add_task(async function test_deleteOneMessage() {
+ // check that nstmp file has been cleaned up.
+ let tmpFile = gRootFolder.filePath;
+ tmpFile.append("nstmp");
+ Assert.ok(!tmpFile.exists());
+ // Deleting one message.
+ IMAPPump.incomingServer.deleteModel = Ci.nsMsgImapDeleteModels.MoveToTrash;
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ IMAPPump.inbox.deleteMessages(
+ [msgHdr],
+ null,
+ false,
+ true,
+ copyListener,
+ false
+ );
+ await copyListener.promise;
+
+ let trashFolder = gRootFolder.getChildNamed("Trash");
+ // hack to force uid validity to get initialized for trash.
+ trashFolder.updateFolder(null);
+});
+
+add_task(async function compactOfflineStore() {
+ gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize;
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gRootFolder.compactAll(listener, null);
+ await listener.promise;
+});
+
+add_task(function test_checkCompactionResult1() {
+ checkOfflineStore(gImapInboxOfflineStoreSize);
+});
+
+add_task(async function pendingRemoval() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+ IMAPPump.inbox.markPendingRemoval(msgHdr, true);
+ gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize;
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gRootFolder.compactAll(listener, null);
+ await listener.promise;
+});
+
+add_task(function test_checkCompactionResult2() {
+ let tmpFile = gRootFolder.filePath;
+ tmpFile.append("nstmp");
+ Assert.ok(!tmpFile.exists());
+ checkOfflineStore(gImapInboxOfflineStoreSize);
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+ Assert.equal(msgHdr.flags & Ci.nsMsgMessageFlags.Offline, 0);
+});
+
+add_task(function endTest() {
+ gRootFolder = null;
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_converterImap.js b/comm/mailnews/imap/test/unit/test_converterImap.js
new file mode 100644
index 0000000000..a05e236ce6
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_converterImap.js
@@ -0,0 +1,110 @@
+/* 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 { convertMailStoreTo } = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Globals
+let gMsgFile1 = do_get_file("../../../data/bugmail10");
+let gTestMsgs = [gMsgFile1, gMsgFile1, gMsgFile1, gMsgFile1];
+
+function checkConversion(aSource, aTarget) {
+ for (let sourceContent of aSource.directoryEntries) {
+ let sourceContentName = sourceContent.leafName;
+ let ext = sourceContentName.slice(-4);
+ let targetFile = FileUtils.File(
+ PathUtils.join(aTarget.path, sourceContentName)
+ );
+
+ // Checking path.
+ if (ext == ".msf" || ext == ".dat") {
+ Assert.ok(targetFile.exists());
+ } else if (sourceContent.isDirectory()) {
+ Assert.ok(targetFile.exists());
+ checkConversion(sourceContent, targetFile);
+ } else {
+ Assert.ok(targetFile.exists());
+ let cur = FileUtils.File(PathUtils.join(targetFile.path, "cur"));
+ Assert.ok(cur.exists());
+ let tmp = FileUtils.File(PathUtils.join(targetFile.path, "tmp"));
+ Assert.ok(tmp.exists());
+ if (targetFile.leafName == "INBOX") {
+ let curContentsCount = [...cur.directoryEntries].length;
+ Assert.equal(curContentsCount, 8);
+ }
+ }
+ }
+}
+
+var EventTarget = function () {
+ this.dispatchEvent = function (aEvent) {
+ if (aEvent.type == "progress") {
+ dump("Progress: " + aEvent.detail + "\n");
+ }
+ };
+};
+
+add_setup(async function () {
+ // Force mbox.
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+ );
+
+ setupIMAPPump();
+
+ // These hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+
+ // Add our test messages to the INBOX.
+ let mailbox = IMAPPump.daemon.getMailbox("INBOX");
+ for (let file of gTestMsgs) {
+ let URI = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ }
+});
+
+add_task(async function downloadForOffline() {
+ // Download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+});
+
+add_task(async function convert() {
+ let mailstoreContractId = Services.prefs.getCharPref(
+ "mail.server." + IMAPPump.incomingServer.key + ".storeContractID"
+ );
+ let eventTarget = new EventTarget();
+ let originalRootFolder = IMAPPump.incomingServer.rootFolder.filePath;
+ await convertMailStoreTo(
+ mailstoreContractId,
+ IMAPPump.incomingServer,
+ eventTarget
+ );
+ // Conversion done.
+ let newRootFolder = IMAPPump.incomingServer.rootFolder.filePath;
+ checkConversion(originalRootFolder, newRootFolder);
+ let newRootFolderMsf = FileUtils.File(newRootFolder.path + ".msf");
+ Assert.ok(newRootFolderMsf.exists());
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_copyThenMove.js b/comm/mailnews/imap/test/unit/test_copyThenMove.js
new file mode 100644
index 0000000000..9abdb5e45d
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_copyThenMove.js
@@ -0,0 +1,200 @@
+/* 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/. */
+
+/**
+ * This file extends test_imapFolderCopy.js to test message
+ * moves from a local folder to an IMAP folder.
+ *
+ * Original Author: Kent James <kent@caspia.com>
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gEmptyLocal1, gEmptyLocal2;
+var gLastKey;
+var gMessages = [];
+
+add_setup(function () {
+ // Turn off autosync_offline_stores because
+ // fetching messages is invoked after copying the messages.
+ // (i.e. The fetching process will be invoked after OnStopCopy)
+ // It will cause crash with an assertion
+ // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown.
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+
+ setupIMAPPump();
+
+ gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1");
+ gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2");
+
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+});
+
+add_task(async function copyFolder1() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ MailServices.copy.copyFolder(
+ gEmptyLocal1,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function copyFolder2() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ MailServices.copy.copyFolder(
+ gEmptyLocal2,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function getLocalMessage1() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ let file = do_get_file("../../../data/bugmail1");
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function getLocalMessage2() {
+ gMessages.push(localAccountUtils.inboxFolder.GetMessageHeader(gLastKey));
+ let file = do_get_file("../../../data/draft1");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function copyMessages() {
+ gMessages.push(localAccountUtils.inboxFolder.GetMessageHeader(gLastKey));
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ gMessages,
+ folder1,
+ false,
+ copyListener,
+ null,
+ false
+ );
+ await copyListener.promise;
+});
+
+add_task(async function moveMessages() {
+ let folder2 = IMAPPump.inbox.getChildNamed("empty 2");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ gLastKey = aKey;
+ },
+ });
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ gMessages,
+ folder2,
+ true,
+ copyListener,
+ null,
+ false
+ );
+ await copyListener.promise;
+});
+
+add_task(async function update1() {
+ let folder1 = IMAPPump.inbox
+ .getChildNamed("empty 1")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ folder1.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function update2() {
+ let folder2 = IMAPPump.inbox
+ .getChildNamed("empty 2")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ folder2.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function verifyFolders() {
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ Assert.equal(folderCount(folder1), 2);
+ let folder2 = IMAPPump.inbox.getChildNamed("empty 2");
+ Assert.ok(folder2 !== null);
+ // folder 1 and 2 should each now have two messages in them.
+ Assert.ok(folder1 !== null);
+ Assert.equal(folderCount(folder2), 2);
+ // The local inbox folder should now be empty, since the second
+ // operation was a move.
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+});
+add_task(function endTest() {
+ gMessages = [];
+ teardownIMAPPump();
+});
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
diff --git a/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js b/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js
new file mode 100644
index 0000000000..abf45ab3e3
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+/*
+ * Test to ensure that imap customCommandResult function works properly
+ * Bug 778246
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// IMAP pump
+
+// Globals
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessageFileName = "bugmail10"; // message file used as the test message
+var gMessage, gExpectedLength;
+
+var gCustomList = ["Custom1", "Custom2", "Custom3"];
+
+var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+setupIMAPPump("CUSTOM1");
+
+add_setup(async function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ // Load and update a message in the imap fake server.
+
+ gMessage = new ImapMessage(
+ specForFileName(gMessageFileName),
+ IMAPPump.mailbox.uidnext++,
+ []
+ );
+ gMessage.xCustomList = [];
+ IMAPPump.mailbox.addMessage(gMessage);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function testStoreCustomList() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ gExpectedLength = gCustomList.length;
+ let uri = IMAPPump.inbox.issueCommandOnMsgs(
+ "STORE",
+ msgHdr.messageKey + " X-CUSTOM-LIST (" + gCustomList.join(" ") + ")",
+ gMsgWindow
+ );
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ // Listens for response from customCommandResult request for X-CUSTOM-LIST.
+ let storeCustomListSetListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(aUrl, aExitCode) {
+ aUrl.QueryInterface(Ci.nsIImapUrl);
+ Assert.equal(
+ aUrl.customCommandResult,
+ "(" + gMessage.xCustomList.join(" ") + ")"
+ );
+ Assert.equal(gMessage.xCustomList.length, gExpectedLength);
+ },
+ });
+ uri.RegisterListener(storeCustomListSetListener);
+ await storeCustomListSetListener.promise;
+});
+
+add_task(async function testStoreMinusCustomList() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ gExpectedLength--;
+ let uri = IMAPPump.inbox.issueCommandOnMsgs(
+ "STORE",
+ msgHdr.messageKey + " -X-CUSTOM-LIST (" + gCustomList[0] + ")",
+ gMsgWindow
+ );
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ // Listens for response from customCommandResult request for X-CUSTOM-LIST.
+ let storeCustomListRemovedListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(aUrl, aExitCode) {
+ aUrl.QueryInterface(Ci.nsIImapUrl);
+ Assert.equal(
+ aUrl.customCommandResult,
+ "(" + gMessage.xCustomList.join(" ") + ")"
+ );
+ Assert.equal(gMessage.xCustomList.length, gExpectedLength);
+ },
+ });
+ uri.RegisterListener(storeCustomListRemovedListener);
+ await storeCustomListRemovedListener.promise;
+});
+
+add_task(async function testStorePlusCustomList() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ gExpectedLength++;
+ let uri = IMAPPump.inbox.issueCommandOnMsgs(
+ "STORE",
+ msgHdr.messageKey + ' +X-CUSTOM-LIST ("Custom4")',
+ gMsgWindow
+ );
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ let storeCustomListAddedListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(aUrl, aExitCode) {
+ aUrl.QueryInterface(Ci.nsIImapUrl);
+ Assert.equal(
+ aUrl.customCommandResult,
+ "(" + gMessage.xCustomList.join(" ") + ")"
+ );
+ Assert.equal(gMessage.xCustomList.length, gExpectedLength);
+ },
+ });
+ uri.RegisterListener(storeCustomListAddedListener);
+ await storeCustomListAddedListener.promise;
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js b/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js
new file mode 100644
index 0000000000..7c05cc0c1f
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+// This file tests that checking folders for new mail with STATUS
+// doesn't try to STAT noselect folders.
+
+var gServer, gImapServer;
+var gIMAPInbox;
+var gFolder2Mailbox;
+var gFolder1, gFolder2;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_setup(function () {
+ var daemon = new ImapDaemon();
+ daemon.createMailbox("folder 1", { subscribed: true });
+ let folder1Mailbox = daemon.getMailbox("folder 1");
+ folder1Mailbox.flags.push("\\Noselect");
+ daemon.createMailbox("folder 2", { subscribed: true });
+ gFolder2Mailbox = daemon.getMailbox("folder 2");
+ addMessageToFolder(gFolder2Mailbox);
+ gServer = makeServer(daemon, "");
+
+ gImapServer = createLocalIMAPServer(gServer.port);
+
+ // Bug 1050840: check a newly created server has the default number of connections
+ Assert.equal(gImapServer.maximumConnectionsNumber, 5);
+ gImapServer.maximumConnectionsNumber = 1;
+
+ localAccountUtils.loadLocalMailAccount();
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+ localAccount.incomingServer = localAccountUtils.incomingServer;
+
+ // Let's also have another account, using the same identity
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = gImapServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // Get the folder list...
+ gImapServer.performExpand(null);
+ gServer.performTest("SUBSCRIBE");
+ // pref tuning: one connection only, turn off notifications
+ // Make sure no biff notifications happen
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ let rootFolder = gImapServer.rootFolder;
+ gIMAPInbox = rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ gFolder1 = rootFolder.getChildNamed("folder 1");
+ gFolder2 = rootFolder.getChildNamed("folder 2");
+ gFolder1.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+ gFolder2.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+});
+
+add_task(function checkStatSelect() {
+ // imap fake server's resetTest resets the authentication state - charming.
+ // So poke the _test member directly.
+ gServer._test = true;
+ gIMAPInbox.getNewMessages(null, null);
+ gServer.performTest("STATUS");
+ // We want to wait for the STATUS to be really done before we issue
+ // more STATUS commands, so we do a NOOP on the
+ // INBOX, and since we only have one connection with the fake server,
+ // that will essentially serialize things.
+ gServer._test = true;
+ gIMAPInbox.updateFolder(null);
+ gServer.performTest("NOOP");
+});
+
+add_task(async function checkStatNoSelect() {
+ // folder 2 should have been stat'd, but not folder 1. All we can really check
+ // is that folder 2 was stat'd and that its unread msg count is 1
+ Assert.equal(gFolder2.getNumUnread(false), 1);
+ addMessageToFolder(gFolder2Mailbox);
+ gFolder1.clearFlag(Ci.nsMsgFolderFlags.ImapNoselect);
+ gServer._test = true;
+
+ let folderListener = new FolderListener();
+
+ // we've cleared the ImapNoselect flag, so we will attempt to STAT folder 1,
+ // which will fail. So we verify that we go on and STAT folder 2, and that
+ // it picks up the message we added to it above.
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.boolPropertyChanged
+ );
+ gIMAPInbox.getNewMessages(null, null);
+ // Wait for the folder listener to get told about new messages.
+ await folderListener.promise;
+});
+
+add_task(function endTest() {
+ Assert.equal(gFolder2.getNumUnread(false), 2);
+
+ // Clean up the server in preparation
+ gServer.resetTest();
+ gImapServer.closeCachedConnections();
+ gServer.performTest();
+ gServer.stop();
+});
+
+function addMessageToFolder(mbox) {
+ // make a couple of messages
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let message = new ImapMessage(msgURI.spec, mbox.uidnext++);
+ mbox.addMessage(message);
+}
+
+function FolderListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+
+FolderListener.prototype = {
+ onFolderBoolPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {
+ // This means that the STAT on "folder 2" has finished.
+ if (aProperty == "NewMessages" && aNewValue) {
+ this._resolve();
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/imap/test/unit/test_downloadOffline.js b/comm/mailnews/imap/test/unit/test_downloadOffline.js
new file mode 100644
index 0000000000..931995ee01
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_downloadOffline.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+/*
+ * Test to ensure that downloadAllForOffline works correctly with imap folders
+ * and returns success.
+ */
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/logHelper.js");
+load("../../../resources/MessageGenerator.jsm");
+
+var gFileName = "bug460636";
+var gMsgFile = do_get_file("../../../data/" + gFileName);
+
+var tests = [setup, downloadAllForOffline, verifyDownloaded, teardownIMAPPump];
+
+async function setup() {
+ setupIMAPPump();
+
+ /*
+ * Ok, prelude done. Read the original message from disk
+ * (through a file URI), and add it to the Inbox.
+ */
+ let msgfileuri = Services.io
+ .newFileURI(gMsgFile)
+ .QueryInterface(Ci.nsIFileURL);
+
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, [])
+ );
+
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []);
+ imapMsg.setSize(5000);
+ IMAPPump.mailbox.addMessage(imapMsg);
+
+ // ...and download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+}
+
+async function downloadAllForOffline() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+}
+
+function verifyDownloaded() {
+ // verify that the message headers have the offline flag set.
+ for (let header of IMAPPump.inbox.msgDatabase.enumerateMessages()) {
+ // Verify that each message has been downloaded and looks OK.
+ if (
+ header instanceof Ci.nsIMsgDBHdr &&
+ header.flags & Ci.nsMsgMessageFlags.Offline
+ ) {
+ IMAPPump.inbox.getLocalMsgStream(header).close();
+ } else {
+ do_throw("Message not downloaded for offline use");
+ }
+ }
+}
+
+function run_test() {
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js b/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js
new file mode 100644
index 0000000000..631e925cfa
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js
@@ -0,0 +1,105 @@
+/* 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/. */
+
+/*
+ * Test to ensure that imap fetchCustomMsgAttribute function works properly
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// IMAP pump
+
+// Globals
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail10"; // message file used as the test message
+
+var gCustomValue = "Custom";
+var gCustomList = ["Custom1", "Custom2", "Custom3"];
+
+var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+add_setup(async function () {
+ setupIMAPPump("CUSTOM1");
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ // Load and update a message in the imap fake server.
+ let message = new ImapMessage(
+ specForFileName(gMessage),
+ IMAPPump.mailbox.uidnext++,
+ []
+ );
+ message.xCustomValue = gCustomValue;
+ message.xCustomList = gCustomList;
+ IMAPPump.mailbox.addMessage(message);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// Used to verify that nsIServerResponseParser.msg_fetch() can handle
+// not in a parenthesis group - Bug 750012
+add_task(async function testFetchCustomValue() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let uri = IMAPPump.inbox.fetchCustomMsgAttribute(
+ "X-CUSTOM-VALUE",
+ msgHdr.messageKey,
+ gMsgWindow
+ );
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ // Listens for response from fetchCustomMsgAttribute request for X-CUSTOM-VALUE.
+ let fetchCustomValueListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(aUrl, aExitCode) {
+ aUrl.QueryInterface(Ci.nsIImapUrl);
+ Assert.equal(aUrl.customAttributeResult, gCustomValue);
+ },
+ });
+ uri.RegisterListener(fetchCustomValueListener);
+ await fetchCustomValueListener.promise;
+});
+
+// Used to verify that nsIServerResponseParser.msg_fetch() can handle a parenthesis group - Bug 735542
+add_task(async function testFetchCustomList() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let uri = IMAPPump.inbox.fetchCustomMsgAttribute(
+ "X-CUSTOM-LIST",
+ msgHdr.messageKey,
+ gMsgWindow
+ );
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ // Listens for response from fetchCustomMsgAttribute request for X-CUSTOM-VALUE.
+ let fetchCustomListListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(aUrl, aExitCode) {
+ aUrl.QueryInterface(Ci.nsIImapUrl);
+ Assert.equal(
+ aUrl.customAttributeResult,
+ "(" + gCustomList.join(" ") + ")"
+ );
+ },
+ });
+ uri.RegisterListener(fetchCustomListListener);
+ await fetchCustomListListener.promise;
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js b/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js
new file mode 100644
index 0000000000..05e2a71b66
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+/*
+ * This file tests hdr parsing in the filter running context, specifically
+ * filters on custom headers.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=655578
+ * for more info.
+ *
+ * Original author: David Bienvenu <bienvenu@mozilla.com>
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// IMAP pump
+
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+
+add_setup(async function () {
+ setupIMAPPump();
+ // Create a test filter.
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ let filter = filterList.createFilter("test list-id");
+ let searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ let value = searchTerm.value;
+ value.attrib = Ci.nsMsgSearchAttrib.OtherHeader;
+ value.str = "gnupg-users.gnupg.org";
+ searchTerm.value = value;
+ searchTerm.booleanAnd = false;
+ searchTerm.arbitraryHeader = "List-Id";
+ filter.appendTerm(searchTerm);
+ filter.enabled = true;
+
+ // create a mark read action
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(action);
+ filterList.insertFilterAt(0, filter);
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ let file = do_get_file("../../../data/bugmail19");
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, [])
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function checkFilterResults() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr.isRead);
+ IMAPPump.server.performTest("UID STORE");
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_filterNeedsBody.js b/comm/mailnews/imap/test/unit/test_filterNeedsBody.js
new file mode 100644
index 0000000000..e7720b3222
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_filterNeedsBody.js
@@ -0,0 +1,113 @@
+/*
+ * This file tests the needsBody attribute added to a
+ * custom filter action in bug 555051.
+ *
+ * Original author: Kent James <kent@caspia.com>
+ * adapted from test_imapFilterActions.js
+ */
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+
+// Globals
+var gFilter; // a message filter with a subject search
+var gAction; // current message action (reused)
+var gMessage = "draft1"; // message file used as the test message
+
+// Definition of tests
+var tests = [
+ setupIMAPPump,
+ setup,
+ function NeedsBodyTrue() {
+ gAction.type = Ci.nsMsgFilterAction.Custom;
+ gAction.customId = "mailnews@mozilla.org#testOffline";
+ actionTestOffline.needsBody = true;
+ gAction.strValue = "true";
+ },
+ runFilterAction,
+ function NeedsBodyFalse() {
+ gAction.type = Ci.nsMsgFilterAction.Custom;
+ gAction.customId = "mailnews@mozilla.org#testOffline";
+ actionTestOffline.needsBody = false;
+ gAction.strValue = "false";
+ },
+ runFilterAction,
+ teardownIMAPPump,
+];
+
+function setup() {
+ // Create a test filter.
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ gFilter = filterList.createFilter("test offline");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.matchAll = true;
+
+ gFilter.appendTerm(searchTerm);
+ gFilter.enabled = true;
+
+ // an action that can be modified by tests
+ gAction = gFilter.createAction();
+
+ // add the custom actions
+ MailServices.filters.addCustomAction(actionTestOffline);
+}
+
+// basic preparation done for each test
+async function runFilterAction() {
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ while (filterList.filterCount) {
+ filterList.removeFilterAt(0);
+ }
+ if (gFilter) {
+ gFilter.clearActionList();
+ if (gAction) {
+ gFilter.appendAction(gAction);
+ filterList.insertFilterAt(0, gFilter);
+ }
+ }
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+}
+
+function run_test() {
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
+
+// custom action to test offline status
+var actionTestOffline = {
+ id: "mailnews@mozilla.org#testOffline",
+ name: "test if offline",
+ applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ for (let msgHdr of aMsgHdrs) {
+ let isOffline = msgHdr.flags & Ci.nsMsgMessageFlags.Offline;
+ Assert.equal(!!isOffline, aActionValue == "true");
+ }
+ },
+ isValidForType(type, scope) {
+ return true;
+ },
+
+ validateActionValue(value, folder, type) {
+ return null;
+ },
+
+ allowDuplicates: false,
+
+ needsBody: false, // set during test setup
+};
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file(gDEPTH + "mailnews/data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js b/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js
new file mode 100644
index 0000000000..b1c26069c6
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js
@@ -0,0 +1,108 @@
+/* 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/. */
+
+/**
+ * Test that the folders that should get flagged for offline use do, and that
+ * those that shouldn't don't.
+ */
+
+// make SOLO_FILE="test_folderOfflineFlags.js" -C mailnews/imap/test check-one
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/**
+ * Setup the mailboxes that will be used for this test.
+ */
+add_setup(async function () {
+ setupIMAPPump("GMail");
+
+ IMAPPump.mailbox.subscribed = true;
+ IMAPPump.mailbox.specialUseFlag = "\\Inbox";
+ IMAPPump.daemon.createMailbox("[Gmail]", {
+ flags: ["\\Noselect"],
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/All Mail", {
+ specialUseFlag: "\\AllMail",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Drafts", {
+ specialUseFlag: "\\Drafts",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Sent", {
+ specialUseFlag: "\\Sent",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Spam", {
+ specialUseFlag: "\\Spam",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Starred", {
+ specialUseFlag: "\\Starred",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Trash", {
+ specialUseFlag: "\\Trash",
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("folder1", { subscribed: true });
+ IMAPPump.daemon.createMailbox("folder2", { subscribed: true });
+
+ // select the inbox to force folder discovery, etc.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+/**
+ * Test that folders generally are marked for offline use by default.
+ */
+add_task(function testGeneralFoldersOffline() {
+ Assert.ok(IMAPPump.inbox.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]");
+
+ let allmail = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Archive);
+ Assert.ok(allmail.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ let drafts = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Drafts);
+ Assert.ok(drafts.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ let sent = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.SentMail);
+ Assert.ok(sent.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+
+ let folder1 = rootFolder.getChildNamed("folder1");
+ Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ let folder2 = rootFolder.getChildNamed("folder2");
+ Assert.ok(folder2.getFlag(Ci.nsMsgFolderFlags.Offline));
+});
+
+/**
+ * Test that Trash isn't flagged for offline use by default.
+ */
+add_task(function testTrashNotOffline() {
+ let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]");
+ let trash = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
+ Assert.ok(!trash.getFlag(Ci.nsMsgFolderFlags.Offline));
+});
+
+/**
+ * Test that Junk isn't flagged for offline use by default.
+ */
+add_task(function testJunkNotOffline() {
+ let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]");
+ let spam = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Junk);
+ Assert.ok(!spam.getFlag(Ci.nsMsgFolderFlags.Offline));
+});
+
+/** Cleanup at the end. */
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_gmailAttributes.js b/comm/mailnews/imap/test/unit/test_gmailAttributes.js
new file mode 100644
index 0000000000..5b424462c5
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_gmailAttributes.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+/*
+ * Test to ensure that, in case of GMail server, fetching of custom GMail
+ * attributes works properly.
+ *
+ * Bug 721316
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=721316
+ * for more info.
+ *
+ * Original Author: Atul Jangra<atuljangra66@gmail.com>
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail10"; // message file used as the test message
+
+var gXGmMsgid = "1278455344230334865";
+var gXGmThrid = "1266894439832287888";
+var gXGmLabels = '(\\Inbox \\Sent Important "Muy Importante" foo)';
+
+add_setup(async function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ setupIMAPPump("GMail");
+ IMAPPump.mailbox.specialUseFlag = "\\Inbox";
+ IMAPPump.mailbox.subscribed = true;
+
+ // need all mail folder to identify this as gmail server.
+ IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\NoSelect"] });
+ IMAPPump.daemon.createMailbox("[Gmail]/All Mail", {
+ subscribed: true,
+ specialUseFlag: "\\AllMail",
+ });
+ // Load and update a message in the imap fake server.
+ let message = new ImapMessage(
+ specForFileName(gMessage),
+ IMAPPump.mailbox.uidnext++,
+ []
+ );
+ message.xGmMsgid = gXGmMsgid;
+ message.xGmThrid = gXGmThrid;
+ message.xGmLabels = gXGmLabels;
+ IMAPPump.mailbox.addMessage(message);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function testFetchXGmMsgid() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let val = msgHdr.getStringProperty("X-GM-MSGID");
+ Assert.equal(val, gXGmMsgid);
+});
+
+add_task(function testFetchXGmThrid() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let val = msgHdr.getStringProperty("X-GM-THRID");
+ Assert.equal(val, gXGmThrid);
+});
+
+add_task(function testFetchXGmLabels() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let val = msgHdr.getStringProperty("X-GM-LABELS");
+ // We need to remove the starting "(" and ending ")" from gXGmLabels while comparing
+ Assert.equal(val, gXGmLabels.substring(1, gXGmLabels.length - 1));
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js b/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js
new file mode 100644
index 0000000000..5ab93d1ad9
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+/*
+ * Test to ensure that, in case of GMail server, fetching of a message, which is
+ * already present in offline store of some folder, from a folder doesn't make
+ * us add it to the offline store twice(in this case, in general it can be any
+ * number of times).
+ *
+ * Bug 721316
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=721316
+ * for more info.
+ *
+ * Original Author: Atul Jangra<atuljangra66@gmail.com>
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Messages to load must have CRLF line endings, that is Windows style.
+
+var gMessage1 = "bugmail10"; // message file used as the test message for Inbox and fooFolder.
+var gXGmMsgid1 = "1278455344230334865";
+var gXGmThrid1 = "1266894439832287888";
+// We need to have different X-GM-LABELS for different folders. I am doing it here manually, but this issue will be tackled in Bug 781443.
+var gXGmLabels11 = '( "\\\\Sent" foo bar)'; // for message in Inbox.
+var gXGmLabels12 = '("\\\\Inbox" "\\\\Sent" bar)'; // for message in fooFolder.
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+
+var gMessage2 = "bugmail11"; // message file used as the test message for fooFolder.
+var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+var gXGmMsgid2 = "1278455345230334555";
+var gXGmThrid2 = "1266894639832287111";
+var gXGmLabels2 = '("\\\\Sent")';
+
+var fooBox;
+var fooFolder;
+
+var gImapInboxOfflineStoreSizeInitial;
+var gImapInboxOfflineStoreSizeFinal;
+
+var gFooOfflineStoreSizeInitial;
+var gFooOfflineStoreSizeFinal;
+
+add_setup(async function () {
+ // We aren't interested in downloading messages automatically.
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ Services.prefs.setBoolPref("mail.server.server1.offline_download", true);
+ Services.prefs.setBoolPref("mail.biff.alert.show_preview", false);
+
+ setupIMAPPump("GMail");
+
+ IMAPPump.mailbox.specialUseFlag = "\\Inbox";
+ IMAPPump.mailbox.subscribed = true;
+
+ // need all mail folder to identify this as gmail server.
+ IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\NoSelect"] });
+ IMAPPump.daemon.createMailbox("[Gmail]/All Mail", {
+ subscribed: true,
+ specialUseFlag: "\\AllMail",
+ });
+
+ // Creating the mailbox "foo"
+ IMAPPump.daemon.createMailbox("foo", { subscribed: true });
+ fooBox = IMAPPump.daemon.getMailbox("foo");
+
+ // Add message1 to inbox.
+ let message = new ImapMessage(
+ specForFileName(gMessage1),
+ IMAPPump.mailbox.uidnext++,
+ []
+ );
+ message.messageId = gMsgId1;
+ message.xGmMsgid = gXGmMsgid1;
+ message.xGmThrid = gXGmThrid1;
+ message.xGmLabels = gXGmLabels11; // With labels excluding "//INBOX".
+ IMAPPump.mailbox.addMessage(message);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function selectInboxMsg() {
+ // Select mesasage1 from inbox which makes message1 available in offline store.
+ let imapService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+ ].getService(Ci.nsIMsgMessageService);
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg1 = db.getMsgHdrForMessageID(gMsgId1);
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ imapService.loadMessage(
+ IMAPPump.inbox.getUriForMsg(msg1),
+ streamListener,
+ null,
+ urlListener,
+ false
+ );
+ await urlListener.promise;
+});
+
+add_task(async function StreamMessageInbox() {
+ // Stream message1 from inbox.
+ let newMsgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ let streamLister = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamMessage(msgURI, streamLister, null, null, false, "", false);
+ gImapInboxOfflineStoreSizeInitial = IMAPPump.inbox.filePath.fileSize; // Initial Size of Inbox.
+ await streamLister.promise;
+});
+
+add_task(async function createAndUpdate() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ fooFolder = rootFolder
+ .getChildNamed("foo")
+ .QueryInterface(Ci.nsIMsgImapMailFolder); // We have created the mailbox earlier.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ fooFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function addToFoo() {
+ // Adding our test message.
+ let message = new ImapMessage(
+ specForFileName(gMessage1),
+ fooBox.uidnext++,
+ []
+ );
+ message.messageId = gMsgId1;
+ message.xGmMsgid = gXGmMsgid1;
+ message.xGmThrid = gXGmThrid1;
+ message.xGmLabels = gXGmLabels12; // With labels excluding "foo".
+ fooBox.addMessage(message);
+ // Adding another message so that fooFolder behaves as LocalFolder while calculating it's size.
+ let message1 = new ImapMessage(
+ specForFileName(gMessage2),
+ fooBox.uidnext++,
+ []
+ );
+ message1.messageId = gMsgId2;
+ message1.xGmMsgid = gXGmMsgid2;
+ message1.xGmThrid = gXGmThrid2;
+ message1.xGmLabels = gXGmLabels2;
+ fooBox.addMessage(message1);
+});
+
+add_task(async function updateFoo() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ fooFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function selectFooMsg() {
+ // Select message2 from fooFolder, which makes fooFolder a local folder.
+ let imapService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+ ].getService(Ci.nsIMsgMessageService);
+ let msg1 = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ imapService.loadMessage(
+ fooFolder.getUriForMsg(msg1),
+ streamListener,
+ null,
+ urlListener,
+ false
+ );
+ await urlListener.promise;
+});
+
+add_task(async function StreamMessageFoo() {
+ // Stream message2 from fooFolder.
+ let newMsgHdr = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+ let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamMessage(msgURI, streamListener, null, null, false, "", false);
+ gFooOfflineStoreSizeInitial = fooFolder.filePath.fileSize;
+ await streamListener.promise;
+});
+
+add_task(async function crossStreaming() {
+ /**
+ * Streaming message1 from fooFolder. message1 is present in
+ * offline store of inbox. We now test that streaming the message1
+ * from fooFolder does not make us add message1 to offline store of
+ * fooFolder. We check this by comparing the sizes of inbox and fooFolder
+ * before and after streaming.
+ */
+ let msg2 = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ Assert.ok(msg2 !== null);
+ let msgURI = fooFolder.getUriForMsg(msg2);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ // pass true for aLocalOnly since message should be in offline store of Inbox.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamMessage(msgURI, streamListener, null, null, false, "", true);
+ await streamListener.promise;
+ gFooOfflineStoreSizeFinal = fooFolder.filePath.fileSize;
+ gImapInboxOfflineStoreSizeFinal = IMAPPump.inbox.filePath.fileSize;
+ Assert.equal(gFooOfflineStoreSizeFinal, gFooOfflineStoreSizeInitial);
+ Assert.equal(
+ gImapInboxOfflineStoreSizeFinal,
+ gImapInboxOfflineStoreSizeInitial
+ );
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+/**
+ * Given a test file, return the file uri spec.
+ */
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js b/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js
new file mode 100644
index 0000000000..d3e3951492
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js
@@ -0,0 +1,199 @@
+/* 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/. */
+
+/*
+ * Tests imap save and detach attachments.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// javascript mime emitter functions
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+var kAttachFileName = "bob.txt";
+function SaveAttachmentCallback() {
+ this.attachments = null;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+}
+
+SaveAttachmentCallback.prototype = {
+ callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) {
+ this.attachments = aMimeMessage.allAttachments;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var gCallbackObject = new SaveAttachmentCallback();
+
+// Dummy message window so we can say the inbox is open in a window.
+var dummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+function MsgsDeletedListener() {
+ this._promise = new Promise(resolve => (this._resolve = resolve));
+}
+MsgsDeletedListener.prototype = {
+ msgsDeleted(aMsgArray) {
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var trackDeletionMessageListener = new MsgsDeletedListener();
+
+add_setup(function () {
+ setupIMAPPump();
+
+ // Add folder listeners that will capture async events
+ MailServices.mfn.addListener(
+ trackDeletionMessageListener,
+ Ci.nsIMsgFolderNotificationService.msgsDeleted
+ );
+
+ // We need to register the dummyMsgWindow so that when we've finished running
+ // the append url, in nsImapMailFolder::OnStopRunningUrl, we'll think the
+ // Inbox is open in a folder and update it, which the detach code relies
+ // on to finish the detach.
+
+ dummyMsgWindow.openFolder = IMAPPump.inbox;
+ MailServices.mailSession.AddMsgWindow(dummyMsgWindow);
+});
+
+// load and update a message in the imap fake server
+add_task(async function loadImapMessage() {
+ let gMessageGenerator = new MessageGenerator();
+ // create a synthetic message with attachment
+ let smsg = gMessageGenerator.makeMessage({
+ attachments: [{ filename: kAttachFileName, body: "I like cheese!" }],
+ });
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(smsg.toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(message);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+});
+
+// process the message through mime
+add_task(async function startMime() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ gCallbackObject,
+ gCallbackObject.callback,
+ true // allowDownload
+ );
+ await gCallbackObject.promise;
+});
+
+// detach any found attachments
+add_task(async function startDetach() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey);
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let attachment = gCallbackObject.attachments[0];
+
+ messenger.detachAttachmentsWOPrompts(
+ do_get_profile(),
+ [attachment.contentType],
+ [attachment.url],
+ [attachment.name],
+ [msgURI],
+ null
+ );
+ // deletion of original message should kick async_driver.
+ await trackDeletionMessageListener.promise;
+});
+
+// test that the detachment was successful
+add_task(async function testDetach() {
+ // Check that the file attached to the message now exists in the profile
+ // directory.
+ let checkFile = do_get_profile().clone();
+ checkFile.append(kAttachFileName);
+ Assert.ok(checkFile.exists());
+
+ // The message should now have a detached attachment. Read the message,
+ // and search for "AttachmentDetached" which is added on detachment.
+
+ // Get the message header - detached copy has UID 2.
+ let msgHdr = IMAPPump.inbox.GetMessageHeader(2);
+ Assert.ok(msgHdr !== null);
+ let messageContent = await getContentFromMessage(msgHdr);
+ Assert.ok(messageContent.includes("AttachmentDetached"));
+});
+
+// Cleanup
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/**
+ * Get the full message content.
+ *
+ * @param aMsgHdr - nsIMsgDBHdr object whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ // Pass true for aLocalOnly since message should be in offline store.
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ true
+ );
+ });
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapAuthMethods.js b/comm/mailnews/imap/test/unit/test_imapAuthMethods.js
new file mode 100644
index 0000000000..18e5d54396
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapAuthMethods.js
@@ -0,0 +1,165 @@
+/**
+ * Login tests for IMAP
+ *
+ * Test code <copied from="test_mailboxes.js">
+ * and <copied from="test_pop3AuthMethods.js">
+ *
+ * BUGS:
+ * - cleanup after each test doesn't seem to work correctly. Effects:
+ * - one more "lsub" per test, e.g. "capability", "auth...", "lsub", "lsub", "lsub", "list" in the 3. test.,
+ * - root folder check succeeds although login failed
+ * - removeIncomingServer(..., true); (cleanup files) fails.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+// const kUsername = "fred";
+// const kPassword = "wilma";
+
+var thisTest;
+
+var tests = [
+ {
+ title: "Cleartext password, with server only supporting old-style login",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: [],
+ expectSuccess: true,
+ transaction: ["CAPABILITY", "LOGIN", "CAPABILITY", "LIST", "LSUB"],
+ },
+ {
+ // Just to make sure we clean up properly - in the test and in TB, e.g. don't cache stuff
+ title:
+ "Second time Cleartext password, with server only supporting old-style login",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: [],
+ expectSuccess: true,
+ transaction: ["CAPABILITY", "LOGIN", "CAPABILITY", "LIST", "LSUB"],
+ },
+ {
+ title:
+ "Cleartext password, with server supporting AUTH PLAIN, LOGIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: [
+ "CAPABILITY",
+ "AUTHENTICATE PLAIN",
+ "CAPABILITY",
+ "LIST",
+ "LSUB",
+ ],
+ },
+ {
+ title: "Cleartext password, with server supporting only AUTH LOGIN",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["LOGIN"],
+ expectSuccess: true,
+ transaction: [
+ "CAPABILITY",
+ "AUTHENTICATE LOGIN",
+ "CAPABILITY",
+ "LIST",
+ "LSUB",
+ ],
+ },
+ {
+ title: "Encrypted password, with server supporting PLAIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: [
+ "CAPABILITY",
+ "AUTHENTICATE CRAM-MD5",
+ "CAPABILITY",
+ "LIST",
+ "LSUB",
+ ],
+ },
+ {
+ title:
+ "Encrypted password, with server only supporting AUTH PLAIN and LOGIN (must fail)",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN", "LOGIN"],
+ expectSuccess: false,
+ transaction: ["CAPABILITY"],
+ },
+];
+
+function nextTest() {
+ try {
+ thisTest = tests.shift();
+ if (!thisTest) {
+ endTest();
+ return;
+ }
+
+ dump("NEXT test: " + thisTest.title + "\n");
+
+ // (re)create fake server
+ var daemon = new ImapDaemon();
+ var server = makeServer(daemon, "", {
+ kAuthSchemes: thisTest.serverAuthMethods,
+ });
+ server.setDebugLevel(fsDebugAll);
+
+ // If Mailnews ever caches server capabilities, delete and re-create the incomingServer here
+ var incomingServer = createLocalIMAPServer(server.port);
+
+ let msgServer = incomingServer;
+ msgServer.QueryInterface(Ci.nsIMsgIncomingServer);
+ msgServer.authMethod = thisTest.clientAuthMethod;
+
+ // connect
+ incomingServer.performExpand(null);
+ server.performTest("LSUB");
+
+ dump("should " + (thisTest.expectSuccess ? "" : "not ") + "be logged in\n");
+ Assert.equal(true, incomingServer instanceof Ci.nsIImapServerSink);
+ do_check_transaction(server.playTransaction(), thisTest.transaction, false);
+
+ do {
+ incomingServer.closeCachedConnections();
+ } while (incomingServer.serverBusy);
+ incomingServer.shutdown();
+ deleteIMAPServer(incomingServer);
+ incomingServer = null;
+ MailServices.accounts.closeCachedConnections();
+ MailServices.accounts.shutdownServers();
+ MailServices.accounts.unloadAccounts();
+ server.stop();
+ } catch (e) {
+ // server.stop();
+ // endTest();
+ do_throw(e);
+ }
+
+ nextTest();
+}
+
+function deleteIMAPServer(incomingServer) {
+ if (!incomingServer) {
+ return;
+ }
+ MailServices.accounts.removeIncomingServer(incomingServer, true);
+}
+
+function run_test() {
+ do_test_pending();
+
+ registerAlertTestUtils();
+
+ nextTest();
+}
+
+function endTest() {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapAutoSync.js b/comm/mailnews/imap/test/unit/test_imapAutoSync.js
new file mode 100644
index 0000000000..1f49a408cc
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapAutoSync.js
@@ -0,0 +1,239 @@
+/* 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/. */
+
+// This tests various features of imap autosync
+// N.B. We need to beware of MessageInjection, since it turns off
+// imap autosync.
+
+// Our general approach is to attach an nsIAutoSyncMgrListener to the
+// autoSyncManager, and listen for the expected events. We simulate idle
+// by directly poking the nsIAutoSyncManager QI'd to nsIObserver with app
+// idle events. If we really go idle, duplicate idle events are ignored.
+
+// We test that checking non-inbox folders for new messages isn't
+// interfering with autoSync's detection of new messages.
+
+// We also test that folders that have messages added to them via move/copy
+// get put in the front of the queue.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var msgFlagOffline = Ci.nsMsgMessageFlags.Offline;
+var nsIAutoSyncMgrListener = Ci.nsIAutoSyncMgrListener;
+
+var gAutoSyncManager = Cc["@mozilla.org/imap/autosyncmgr;1"].getService(
+ Ci.nsIAutoSyncManager
+);
+var gTargetFolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ addMessageToFolder(IMAPPump.inbox);
+});
+
+add_task(async function test_createTargetFolder() {
+ gAutoSyncManager.addListener(gAutoSyncListener);
+
+ IMAPPump.incomingServer.rootFolder.createSubfolder("targetFolder", null);
+ await PromiseTestUtils.promiseFolderAdded("targetFolder");
+ gTargetFolder =
+ IMAPPump.incomingServer.rootFolder.getChildNamed("targetFolder");
+ Assert.ok(gTargetFolder instanceof Ci.nsIMsgImapMailFolder);
+ // set folder to be checked for new messages when inbox is checked.
+ gTargetFolder.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+});
+
+add_task(function test_checkForNewMessages() {
+ addMessageToFolder(gTargetFolder);
+ // This will update the INBOX and STATUS targetFolder. We only care about
+ // the latter.
+ IMAPPump.inbox.getNewMessages(null, null);
+ IMAPPump.server.performTest("STATUS");
+ // Now we'd like to make autosync update folders it knows about, to
+ // get the initial autosync out of the way.
+});
+
+add_task(function test_triggerAutoSyncIdle() {
+ // wait for both folders to get updated.
+ gAutoSyncListener._waitingForDiscoveryList.push(IMAPPump.inbox);
+ gAutoSyncListener._waitingForDiscoveryList.push(gTargetFolder);
+ gAutoSyncListener._waitingForDiscovery = true;
+ let observer = gAutoSyncManager.QueryInterface(Ci.nsIObserver);
+ observer.observe(null, "mail-startup-done", "");
+ observer.observe(null, "mail:appIdle", "idle");
+});
+
+// move the message to a diffent folder
+add_task(async function test_moveMessageToTargetFolder() {
+ let observer = gAutoSyncManager.QueryInterface(Ci.nsIObserver);
+ observer.observe(null, "mail:appIdle", "back");
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr !== null);
+
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ // Now move this message to the target folder.
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msgHdr],
+ gTargetFolder,
+ true,
+ listener,
+ null,
+ false
+ );
+ await listener.promise;
+});
+
+add_task(async function test_waitForTargetUpdate() {
+ // After the copy, now we expect to get notified of the gTargetFolder
+ // getting updated, after we simulate going idle.
+ gAutoSyncListener._waitingForUpdate = true;
+ gAutoSyncListener._waitingForUpdateList.push(gTargetFolder);
+ gAutoSyncManager
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "mail:appIdle", "idle");
+ await gAutoSyncListener.promiseOnDownloadCompleted;
+ await gAutoSyncListener.promiseOnDiscoveryQProcessed;
+});
+
+// Cleanup
+add_task(function endTest() {
+ let numMsgs = 0;
+ for (let header of gTargetFolder.messages) {
+ numMsgs++;
+ Assert.notEqual(header.flags & Ci.nsMsgMessageFlags.Offline, 0);
+ }
+ Assert.equal(2, numMsgs);
+ Assert.equal(gAutoSyncListener._waitingForUpdateList.length, 0);
+ Assert.ok(!gAutoSyncListener._waitingForDiscovery);
+ Assert.ok(!gAutoSyncListener._waitingForUpdate);
+ teardownIMAPPump();
+});
+
+function autoSyncListenerPromise() {
+ this._inQFolderList = [];
+ this._runnning = false;
+ this._lastMessage = {};
+ this._waitingForUpdateList = [];
+ this._waitingForUpdate = false;
+ this._waitingForDiscoveryList = [];
+ this._waitingForDiscovery = false;
+
+ this._promiseOnDownloadCompleted = new Promise(resolve => {
+ this._resolveOnDownloadCompleted = resolve;
+ });
+ this._promiseOnDiscoveryQProcessed = new Promise(resolve => {
+ this._resolveOnDiscoveryQProcessed = resolve;
+ });
+}
+autoSyncListenerPromise.prototype = {
+ onStateChanged(running) {
+ this._runnning = running;
+ },
+
+ onFolderAddedIntoQ(queue, folder) {
+ dump("Folder added into Q " + this.qName(queue) + " " + folder.URI + "\n");
+ },
+ onFolderRemovedFromQ(queue, folder) {
+ dump(
+ "Folder removed from Q " + this.qName(queue) + " " + folder.URI + "\n"
+ );
+ },
+ onDownloadStarted(folder, numOfMessages, totalPending) {
+ dump("Folder download started" + folder.URI + "\n");
+ },
+
+ onDownloadCompleted(folder) {
+ dump("Folder download completed" + folder.URI + "\n");
+ if (folder instanceof Ci.nsIMsgFolder) {
+ let index = mailTestUtils.non_strict_index_of(
+ this._waitingForUpdateList,
+ folder
+ );
+ if (index != -1) {
+ this._waitingForUpdateList.splice(index, 1);
+ }
+ if (this._waitingForUpdate && this._waitingForUpdateList.length == 0) {
+ dump("Got last folder update looking for.\n");
+ this._waitingForUpdate = false;
+ this._resolveOnDownloadCompleted();
+ }
+ }
+ },
+
+ onDownloadError(folder) {
+ if (folder instanceof Ci.nsIMsgFolder) {
+ dump("OnDownloadError: " + folder.prettyName + "\n");
+ }
+ },
+
+ onDiscoveryQProcessed(folder, numOfHdrsProcessed, leftToProcess) {
+ dump("onDiscoveryQProcessed: " + folder.prettyName + "\n");
+ let index = mailTestUtils.non_strict_index_of(
+ this._waitingForDiscoveryList,
+ folder
+ );
+ if (index != -1) {
+ this._waitingForDiscoveryList.splice(index, 1);
+ }
+ if (
+ this._waitingForDiscovery &&
+ this._waitingForDiscoveryList.length == 0
+ ) {
+ dump("Got last folder discovery looking for\n");
+ this._waitingForDiscovery = false;
+ this._resolveOnDiscoveryQProcessed();
+ }
+ },
+
+ onAutoSyncInitiated(folder) {},
+ qName(queueType) {
+ if (queueType == Ci.nsIAutoSyncMgrListener.PriorityQueue) {
+ return "priorityQ";
+ }
+ if (queueType == Ci.nsIAutoSyncMgrListener.UpdateQueue) {
+ return "updateQ";
+ }
+ if (queueType == Ci.nsIAutoSyncMgrListener.DiscoveryQueue) {
+ return "discoveryQ";
+ }
+ return "";
+ },
+ get promiseOnDownloadCompleted() {
+ return this._promiseOnDownloadCompleted;
+ },
+ get promiseOnDiscoveryQProcessed() {
+ return this._promiseOnDiscoveryQProcessed;
+ },
+};
+var gAutoSyncListener = new autoSyncListenerPromise();
+
+/*
+ * helper functions
+ */
+
+// load and update a message in the imap fake server
+function addMessageToFolder(folder) {
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let ImapMailbox = IMAPPump.daemon.getMailbox(folder.name);
+ // We add messages with \Seen flag set so that we won't accidentally
+ // trigger the code that updates imap folders that have unread messages moved
+ // into them.
+ let message = new ImapMessage(msgURI.spec, ImapMailbox.uidnext++, ["\\Seen"]);
+ ImapMailbox.addMessage(message);
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapChunks.js b/comm/mailnews/imap/test/unit/test_imapChunks.js
new file mode 100644
index 0000000000..4de0d6c2b3
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapChunks.js
@@ -0,0 +1,114 @@
+/**
+ * Test bug 92111 - imap download-by-chunks doesn't download complete file if the
+ * server lies about rfc822.size (known to happen for Exchange and gmail)
+ */
+
+var gIMAPDaemon, gServer, gIMAPIncomingServer, gSavedMsgFile;
+
+var gIMAPService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+].getService(Ci.nsIMsgMessageService);
+
+var gFileName = "bug92111";
+var gMsgFile = do_get_file("../../../data/" + gFileName);
+
+add_task(async function run_the_test() {
+ /*
+ * Set up an IMAP server. The bug is only triggered when nsMsgSaveAsListener
+ * is used (i.e., for IMAP and NNTP).
+ */
+ gIMAPDaemon = new ImapDaemon();
+ gServer = makeServer(gIMAPDaemon, "");
+ gIMAPIncomingServer = createLocalIMAPServer(gServer.port);
+
+ // pref tuning: one connection only, turn off notifications
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1);
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ // Crank down the message chunk size to make test cases easier
+ Services.prefs.setBoolPref("mail.server.default.fetch_by_chunks", true);
+ Services.prefs.setIntPref("mail.imap.chunk_size", 1000);
+ Services.prefs.setIntPref("mail.imap.min_chunk_size_threshold", 1500);
+ Services.prefs.setIntPref("mail.imap.chunk_add", 0);
+
+ var inbox = gIMAPDaemon.getMailbox("INBOX");
+
+ /*
+ * Ok, prelude done. Read the original message from disk
+ * (through a file URI), and add it to the Inbox.
+ */
+ var msgfileuri = Services.io
+ .newFileURI(gMsgFile)
+ .QueryInterface(Ci.nsIFileURL);
+
+ let message = new ImapMessage(msgfileuri.spec, inbox.uidnext++, []);
+ // report an artificially low size, like gmail and Exchange do
+ message.setSize(gMsgFile.fileSize - 100);
+ inbox.addMessage(message);
+
+ // Save the message to a local file. IMapMD corresponds to
+ // <profile_dir>/mailtest/ImapMail (where fakeserver puts the IMAP mailbox
+ // files). If we pass the test, we'll remove the file afterwards
+ // (cf. UrlListener), otherwise it's kept in IMapMD.
+ gSavedMsgFile = Services.dirsvc.get("IMapMD", Ci.nsIFile);
+ gSavedMsgFile.append(gFileName + ".eml");
+
+ do_test_pending();
+ do_timeout(10000, function () {
+ do_throw(
+ "SaveMessageToDisk did not complete within 10 seconds" +
+ "(incorrect messageURI?). ABORTING."
+ );
+ });
+
+ // Enforcing canonicalLineEnding (i.e., CRLF) makes sure that the
+ // test also runs successfully on platforms not using CRLF by default.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPService.SaveMessageToDisk(
+ "imap-message://user@localhost/INBOX#" + (inbox.uidnext - 1),
+ gSavedMsgFile,
+ false,
+ promiseUrlListener,
+ {},
+ true,
+ null
+ );
+ await promiseUrlListener.promise;
+
+ let msgFileContent = await IOUtils.readUTF8(gMsgFile.path);
+ let savedMsgFileContent = await IOUtils.readUTF8(gSavedMsgFile.path);
+ // File contents should not have been modified.
+ Assert.equal(msgFileContent, savedMsgFileContent);
+
+ // The file doesn't get closed straight away, but does after a little bit.
+ // So wait, and then remove it. We need to test this to ensure we don't
+ // indefinitely lock the file.
+ do_timeout(1000, endTest);
+});
+
+function endTest() {
+ gIMAPIncomingServer.closeCachedConnections();
+ gServer.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ try {
+ gSavedMsgFile.remove(false);
+ } catch (ex) {
+ dump(ex);
+ do_throw(ex);
+ }
+ do_test_finished();
+}
+
+// XXX IRVING we need a separate check somehow to make sure we store the correct
+// content size for chunked messages where the server lied
diff --git a/comm/mailnews/imap/test/unit/test_imapClientid.js b/comm/mailnews/imap/test/unit/test_imapClientid.js
new file mode 100644
index 0000000000..bceb7c05bf
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapClientid.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var incomingServer, server;
+
+const kUserName = "user";
+const kValidPassword = "password";
+
+var gTests = [
+ {
+ title: "Cleartext password, with server only supporting old-style login",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: [],
+ expectSuccess: true,
+ transaction: [
+ "capability",
+ "CLIENTID",
+ "authenticate PLAIN",
+ "capability",
+ "list",
+ "lsub",
+ ],
+ },
+];
+
+add_task(async function () {
+ let daemon = new ImapDaemon();
+ server = makeServer(daemon, "", {
+ // Make username of server match the singons.txt file
+ // (pw there is intentionally invalid)
+ kUsername: kUserName,
+ kPassword: kValidPassword,
+ });
+ server.setDebugLevel(fsDebugAll);
+ incomingServer = createLocalIMAPServer(server.port);
+
+ // Turn on CLIENTID and populate the clientid with a uuid.
+ incomingServer.clientidEnabled = true;
+ incomingServer.clientid = "4d8776ca-0251-11ea-8d71-362b9e155667";
+
+ // Connect.
+ incomingServer.performExpand(null);
+ server.performTest("LSUB");
+
+ do_check_transaction(server.playTransaction(), gTests[0].transaction, false);
+
+ server.resetTest();
+});
+
+registerCleanupFunction(function () {
+ incomingServer.closeCachedConnections();
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapContentLength.js b/comm/mailnews/imap/test/unit/test_imapContentLength.js
new file mode 100644
index 0000000000..acb5001242
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapContentLength.js
@@ -0,0 +1,98 @@
+/* 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/. */
+
+/*
+ * Test content length for the IMAP protocol. This focuses on necko URLs
+ * that are run externally.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gMsgHdr = null;
+
+// Take a multipart message as we're testing attachment URLs as well
+var gFile = do_get_file("../../../data/multipart-complex2");
+
+add_setup(function () {
+ setupIMAPPump();
+
+ // Set up nsIMsgFolderListener to get the header when it's received
+ MailServices.mfn.addListener(msgAddedListener, MailServices.mfn.msgAdded);
+
+ IMAPPump.inbox.clearFlag(Ci.nsMsgFolderFlags.Offline);
+});
+
+// Adds some messages directly to a mailbox (eg new mail)
+add_task(async function addMessageToServer() {
+ let URI = Services.io.newFileURI(gFile).QueryInterface(Ci.nsIFileURL);
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(URI.spec, IMAPPump.mailbox.uidnext++, [])
+ );
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+function MsgAddedListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+MsgAddedListener.prototype = {
+ msgAdded(aMsgHdr) {
+ gMsgHdr = aMsgHdr;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var msgAddedListener = new MsgAddedListener();
+
+add_task(async function verifyContentLength() {
+ await msgAddedListener.promise;
+ let messageUri = IMAPPump.inbox.getUriForMsg(gMsgHdr);
+ // Convert this to a URI that necko can run
+ let messageService = MailServices.messageServiceFromURI(messageUri);
+ let neckoURL = messageService.getUrlForUri(messageUri);
+ // Don't use the necko URL directly. Instead, get the spec and create a new
+ // URL using the IO service
+ let urlToRun = Services.io.newURI(neckoURL.spec);
+
+ // Get a channel from this URI, and check its content length
+ let channel = Services.io.newChannelFromURI(
+ urlToRun,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ Assert.equal(channel.contentLength, gFile.fileSize);
+
+ // Now try an attachment. &part=1.2
+ let attachmentURL = Services.io.newURI(neckoURL.spec + "&part=1.2");
+ let attachmentChannel = Services.io.newChannelFromURI(
+ attachmentURL,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ // Currently attachments have their content length set to the length of the
+ // entire message
+ Assert.equal(attachmentChannel.contentLength, gFile.fileSize);
+});
+
+add_task(function endTest() {
+ MailServices.mfn.removeListener(msgAddedListener);
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js b/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js
new file mode 100644
index 0000000000..75d13159f1
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js
@@ -0,0 +1,120 @@
+/* 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/. */
+
+// This tests our handling of server timeouts during online move of
+// an imap message. The move is done as an offline operation and then
+// played back, to copy what the apps do.
+
+Services.prefs.setIntPref("mailnews.tcptimeout", 2);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gTargetFolder;
+var alertResolve;
+var alertPromise = new Promise(resolve => {
+ alertResolve = resolve;
+});
+
+function alertPS(parent, aDialogTitle, aText) {
+ alertResolve(aText);
+}
+
+add_setup(function () {
+ registerAlertTestUtils();
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+add_task(async function createTargetFolder() {
+ IMAPPump.daemon.copySleep = 5000;
+ IMAPPump.incomingServer.rootFolder.createSubfolder("targetFolder", null);
+ await PromiseTestUtils.promiseFolderAdded("targetFolder");
+ gTargetFolder =
+ IMAPPump.incomingServer.rootFolder.getChildNamed("targetFolder");
+ Assert.ok(gTargetFolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gTargetFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// load and update a message in the imap fake server
+add_task(async function loadImapMessage() {
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ var gMessage = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(gMessage);
+
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+});
+
+// move the message to a diffent folder
+add_task(async function moveMessageToTargetFolder() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ // This should cause the move to be done as an offline imap operation
+ // that's played back immediately.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msgHdr],
+ gTargetFolder,
+ true,
+ copyListener,
+ gDummyMsgWindow,
+ true
+ );
+ await copyListener.promise;
+});
+
+add_task(async function waitForOfflinePlayback() {
+ // Just wait for the alert about timed out connection.
+ let alertText = await alertPromise;
+ Assert.ok(alertText.startsWith("Connection to server localhost timed out."));
+});
+
+add_task(async function updateTargetFolderAndInbox() {
+ let urlListenerTargetFolder = new PromiseTestUtils.PromiseUrlListener();
+ gTargetFolder.updateFolderWithListener(null, urlListenerTargetFolder);
+ await urlListenerTargetFolder.promise;
+ let urlListenerInbox = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, urlListenerInbox);
+ await urlListenerInbox.promise;
+});
+
+// Cleanup
+add_task(async function endTest() {
+ // Make sure neither source nor target folder have offline events.
+ Assert.ok(!IMAPPump.inbox.getFlag(Ci.nsMsgFolderFlags.OfflineEvents));
+ Assert.ok(!gTargetFolder.getFlag(Ci.nsMsgFolderFlags.OfflineEvents));
+
+ // fake server does the copy, but then times out, so make sure the target
+ // folder has only 1 message, not the multiple ones it would have if we
+ // retried.
+ Assert.equal(gTargetFolder.getTotalMessages(false), 1);
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapFilterActions.js b/comm/mailnews/imap/test/unit/test_imapFilterActions.js
new file mode 100644
index 0000000000..21cd2d01aa
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapFilterActions.js
@@ -0,0 +1,597 @@
+/*
+ * This file tests imap filter actions, particularly as affected by the
+ * addition of body searches in bug 127250. Actions that involves sending
+ * mail are not tested. The tests check various counts, and the effects
+ * on the message database of the filters. Effects on IMAP server
+ * flags, if any, are not tested.
+ *
+ * Original author: Kent James <kent@caspia.com>
+ * adapted from test_localToImapFilter.js
+ */
+
+var Is = Ci.nsMsgSearchOp.Is;
+var Contains = Ci.nsMsgSearchOp.Contains;
+var Subject = Ci.nsMsgSearchAttrib.Subject;
+var Body = Ci.nsMsgSearchAttrib.Body;
+
+// Globals
+var gSubfolder; // a local message folder used as a target for moves and copies
+var gFilter; // a message filter with a subject search
+var gAction; // current message action (reused)
+var gBodyFilter; // a message filter with a body search
+var gInboxListener; // database listener object
+var gHeader; // the current message db header
+var gInboxCount; // the previous number of messages in the Inbox
+var gSubfolderCount; // the previous number of messages in the subfolder
+var gMessage = "image-attach-test"; // message file used as the test message
+
+// subject of the test message
+var gMessageSubject = "image attach test";
+
+// a string in the body of the test message
+var gMessageInBody = "01234567890test";
+
+// various object references
+var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+);
+
+var gSmtpServerD = setupSmtpServerDaemon();
+
+function setupSmtpServer() {
+ gSmtpServerD.start();
+ var gSmtpServer = localAccountUtils.create_outgoing_server(
+ gSmtpServerD.port,
+ "user",
+ "password",
+ "localhost"
+ );
+ MailServices.accounts.defaultAccount.defaultIdentity.email =
+ "from@tinderbox.invalid";
+ MailServices.accounts.defaultAccount.defaultIdentity.smtpServerKey =
+ gSmtpServer.key;
+
+ registerCleanupFunction(() => {
+ gSmtpServerD.stop();
+ Services.prefs.clearUserPref("mail.forward_message_mode");
+ });
+}
+
+// Definition of tests. The test function name is the filter action
+// being tested, with "Body" appended to tests that use delayed
+// application of filters due to a body search
+var gTestArray = [
+ setupIMAPPump,
+ // optionally set server parameters, here enabling debug messages
+ // function serverParms() {
+ // IMAPPump.server.setDebugLevel(fsDebugAll);
+ // },
+ setupSmtpServer,
+ setupFilters,
+ // The initial tests do not result in new messages added.
+ async function MoveToFolder() {
+ gAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ },
+ // do it again, sometimes that causes multiple downloads
+ async function MoveToFolder2() {
+ gAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ },
+ /**/
+ async function MoveToFolderBody() {
+ gAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ // no net messages were added to the inbox
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ },
+ async function MoveToFolderBody2() {
+ gAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ // no net messages were added to the inbox
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ },
+ async function MarkRead() {
+ gAction.type = Ci.nsMsgFilterAction.MarkRead;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ await setupTest(gFilter, gAction);
+ testCounts(false, 0, 0, 0);
+ Assert.ok(gHeader.isRead);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ },
+ async function MarkReadBody() {
+ gAction.type = Ci.nsMsgFilterAction.MarkRead;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.ok(gHeader.isRead);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ },
+ async function KillThread() {
+ gAction.type = Ci.nsMsgFilterAction.KillThread;
+ await setupTest(gFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ async function KillThreadBody() {
+ gAction.type = Ci.nsMsgFilterAction.KillThread;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ async function KillSubthread() {
+ gAction.type = Ci.nsMsgFilterAction.KillSubthread;
+ await setupTest(gFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ async function KillSubthreadBody() {
+ gAction.type = Ci.nsMsgFilterAction.KillSubthread;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ async function DoNothing() {
+ gAction.type = Ci.nsMsgFilterAction.StopExecution;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ },
+ async function DoNothingBody() {
+ gAction.type = Ci.nsMsgFilterAction.StopExecution;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ },
+ // this tests for marking message as junk
+ async function JunkScore() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 100;
+ await setupTest(gFilter, gAction);
+
+ // marking as junk resets new but not unread
+ testCounts(false, 1, 0, 0);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "100");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ // this tests for marking message as junk
+ async function JunkScoreBody() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 100;
+ await setupTest(gBodyFilter, gAction);
+
+ // marking as junk resets new but not unread
+ testCounts(false, 1, 0, 0);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "100");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ // The remaining tests add new messages
+ async function MarkUnread() {
+ gAction.type = Ci.nsMsgFilterAction.MarkUnread;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.ok(!gHeader.isRead);
+ },
+ async function MarkUnreadBody() {
+ gAction.type = Ci.nsMsgFilterAction.MarkUnread;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.ok(!gHeader.isRead);
+ },
+ async function WatchThread() {
+ gAction.type = Ci.nsMsgFilterAction.WatchThread;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched);
+ },
+ async function WatchThreadBody() {
+ gAction.type = Ci.nsMsgFilterAction.WatchThread;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched);
+ },
+ async function MarkFlagged() {
+ gAction.type = Ci.nsMsgFilterAction.MarkFlagged;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.ok(gHeader.isFlagged);
+ },
+ async function MarkFlaggedBody() {
+ gAction.type = Ci.nsMsgFilterAction.MarkFlagged;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.ok(gHeader.isFlagged);
+ },
+ async function ChangePriority() {
+ gAction.type = Ci.nsMsgFilterAction.ChangePriority;
+ gAction.priority = Ci.nsMsgPriority.highest;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority);
+ },
+ async function ChangePriorityBody() {
+ gAction.type = Ci.nsMsgFilterAction.ChangePriority;
+ gAction.priority = Ci.nsMsgPriority.highest;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority);
+ },
+ async function AddTag() {
+ gAction.type = Ci.nsMsgFilterAction.AddTag;
+ gAction.strValue = "TheTag";
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("keywords"), "thetag");
+ },
+ async function AddTagBody() {
+ gAction.type = Ci.nsMsgFilterAction.AddTag;
+ gAction.strValue = "TheTag2";
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("keywords"), "thetag2");
+ },
+ // this tests for marking message as good
+ async function JunkScoreAsGood() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 0;
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "0");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ // this tests for marking message as good
+ async function JunkScoreAsGoodBody() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 0;
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "0");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ async function CopyToFolder() {
+ gAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ },
+ async function CopyToFolderBody() {
+ gAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gBodyFilter, gAction);
+
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ },
+ async function ForwardInline() {
+ return testForward(2);
+ },
+ async function ForwardAsAttachment() {
+ return testForward(0);
+ },
+ /**/
+ endTest,
+];
+
+function run_test() {
+ // Services.prefs.setBoolPref("mail.server.default.autosync_offline_stores", false);
+ gTestArray.forEach(x => add_task(x));
+ run_next_test();
+}
+
+function setupFilters() {
+ // Create a non-body filter.
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ gFilter = filterList.createFilter("subject");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.attrib = Subject;
+ searchTerm.op = Is;
+ var value = searchTerm.value;
+ value.attrib = Subject;
+ value.str = gMessageSubject;
+ searchTerm.value = value;
+ searchTerm.booleanAnd = false;
+ gFilter.appendTerm(searchTerm);
+ gFilter.enabled = true;
+
+ // Create a filter with a body term that that forces delayed application of
+ // filters until after body download.
+ gBodyFilter = filterList.createFilter("body");
+ searchTerm = gBodyFilter.createTerm();
+ searchTerm.attrib = Body;
+ searchTerm.op = Contains;
+ value = searchTerm.value;
+ value.attrib = Body;
+ value.str = gMessageInBody;
+ searchTerm.value = value;
+ searchTerm.booleanAnd = false;
+ gBodyFilter.appendTerm(searchTerm);
+ gBodyFilter.enabled = true;
+
+ // an action that can be modified by tests
+ gAction = gFilter.createAction();
+
+ gSubfolder = localAccountUtils.rootFolder.createLocalSubfolder("Subfolder");
+
+ MailServices.mailSession.AddFolderListener(
+ FolderListener,
+ Ci.nsIFolderListener.event
+ );
+ gPreviousUnread = 0;
+
+ // When a message body is not downloaded, and then later a filter is
+ // applied that requires a download of message bodies, then the previous
+ // bodies are downloaded - and the message filters are applied twice!
+ // See bug 1116228, but for now workaround by always downloading bodies.
+ IMAPPump.incomingServer.downloadBodiesOnGetNewMail = true;
+}
+
+/*
+ * functions used to support test setup and execution
+ */
+
+// basic preparation done for each test
+async function setupTest(aFilter, aAction) {
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ while (filterList.filterCount) {
+ filterList.removeFilterAt(0);
+ }
+ if (aFilter) {
+ aFilter.clearActionList();
+ if (aAction) {
+ aFilter.appendAction(aAction);
+ filterList.insertFilterAt(0, aFilter);
+ }
+ }
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+
+ IMAPPump.inbox.clearNewMessages();
+
+ gInboxListener = new DBListener();
+ gDbService.registerPendingListener(IMAPPump.inbox, gInboxListener);
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ await PromiseTestUtils.promiseDelay(200);
+}
+
+// Cleanup, null out everything, close all cached connections and stop the
+// server
+function endTest() {
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+ gInboxListener = null;
+ MailServices.mailSession.RemoveFolderListener(FolderListener);
+ teardownIMAPPump();
+}
+
+/*
+ * listener objects
+ */
+
+// nsIFolderListener implementation
+var FolderListener = {
+ onFolderEvent(aEventFolder, aEvent) {
+ dump(
+ "received folder event " + aEvent + " folder " + aEventFolder.name + "\n"
+ );
+ },
+};
+
+// nsIDBChangeListener implementation. Counts of calls are kept, but not
+// currently used in the tests. Current role is to provide a reference
+// to the new message header (plus give some examples of using db listeners
+// in javascript).
+function DBListener() {
+ this.counts = {};
+ let counts = this.counts;
+ counts.onHdrFlagsChanged = 0;
+ counts.onHdrDeleted = 0;
+ counts.onHdrAdded = 0;
+ counts.onParentChanged = 0;
+ counts.onAnnouncerGoingAway = 0;
+ counts.onReadChanged = 0;
+ counts.onJunkScoreChanged = 0;
+ counts.onHdrPropertyChanged = 0;
+ counts.onEvent = 0;
+}
+
+DBListener.prototype = {
+ onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) {
+ this.counts.onHdrFlagsChanged++;
+ },
+
+ onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) {
+ this.counts.onHdrDeleted++;
+ },
+
+ onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) {
+ this.counts.onHdrAdded++;
+ gHeader = aHdrChanged;
+ },
+
+ onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) {
+ this.counts.onParentChanged++;
+ },
+
+ onAnnouncerGoingAway(instigator) {
+ if (gInboxListener) {
+ try {
+ IMAPPump.inbox.msgDatabase.removeListener(gInboxListener);
+ } catch (e) {
+ dump(" listener not found\n");
+ }
+ }
+ this.counts.onAnnouncerGoingAway++;
+ },
+
+ onReadChanged(aInstigator) {
+ this.counts.onReadChanged++;
+ },
+
+ onJunkScoreChanged(aInstigator) {
+ this.counts.onJunkScoreChanged++;
+ },
+
+ onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) {
+ this.counts.onHdrPropertyChanged++;
+ },
+
+ onEvent(aDB, aEvent) {
+ this.counts.onEvent++;
+ },
+};
+
+/*
+ * helper functions
+ */
+
+// return the number of messages in a folder (and check that the
+// folder counts match the database counts)
+function folderCount(folder) {
+ // count using the database
+ let dbCount = [...folder.msgDatabase.enumerateMessages()].length;
+
+ // count using the folder
+ let count = folder.getTotalMessages(false);
+
+ // compare the two
+ Assert.equal(dbCount, count);
+ return dbCount;
+}
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
+
+// shorthand for the inbox message summary database
+function db() {
+ return IMAPPump.inbox.msgDatabase;
+}
+
+// static variables used in testCounts
+var gPreviousUnread = 0;
+
+// Test various counts.
+//
+// aHasNew: folder hasNew flag
+// aUnreadDelta: change in unread count for the folder
+// aFolderNewDelta: change in new count for the folder
+// aDbNewDelta: change in new count for the database
+//
+function testCounts(aHasNew, aUnreadDelta, aFolderNewDelta, aDbNewDelta) {
+ try {
+ let folderNew = IMAPPump.inbox.getNumNewMessages(false);
+ let hasNew = IMAPPump.inbox.hasNewMessages;
+ let unread = IMAPPump.inbox.getNumUnread(false);
+ let arrayOut = db().getNewList();
+ let dbNew = arrayOut.length;
+ dump(
+ " hasNew: " +
+ hasNew +
+ " unread: " +
+ unread +
+ " folderNew: " +
+ folderNew +
+ " dbNew: " +
+ dbNew +
+ " prevUnread " +
+ gPreviousUnread +
+ "\n"
+ );
+ Assert.equal(aHasNew, hasNew);
+ Assert.equal(aUnreadDelta, unread - gPreviousUnread);
+ gPreviousUnread = unread;
+ // This seems to be reset for each folder update.
+ //
+ // This check seems to be failing in SeaMonkey builds, yet I can see no ill
+ // effects of this in the actual program. Fixing this is complex because of
+ // the messiness of new count management (see bug 507638 for a
+ // refactoring proposal, and attachment 398899 on bug 514801 for one possible
+ // fix to this particular test). So I am disabling this.
+ // Assert.equal(aFolderNewDelta, folderNew);
+ Assert.equal(aDbNewDelta, dbNew);
+ } catch (e) {
+ dump(e);
+ }
+}
+
+/**
+ * Test that Ci.nsMsgFilterAction.Forward works.
+ *
+ * @param {number} mode - 0 means forward as attachment, 2 means forward inline.
+ */
+async function testForward(mode) {
+ Services.prefs.setIntPref("mail.forward_message_mode", mode);
+
+ gSmtpServerD.resetTest();
+ gAction.type = Ci.nsMsgFilterAction.Forward;
+ gAction.strValue = "to@local";
+ await setupTest(gFilter, gAction);
+ let msgData = gSmtpServerD._daemon.post;
+ Assert.ok(msgData.includes(`Subject: Fwd: ${gMessageSubject}`));
+ Assert.ok(msgData.includes(`${gMessageInBody}`));
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js b/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js
new file mode 100644
index 0000000000..2d7e00efcc
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js
@@ -0,0 +1,428 @@
+/*
+ * This file tests imap filter actions post-plugin, which uses nsMsgFilterAfterTheFact
+ *
+ * Original author: Kent James <kent@caspia.com>
+ * adapted from test_imapFilterActions.js
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Is = Ci.nsMsgSearchOp.Is;
+var Subject = Ci.nsMsgSearchAttrib.Subject;
+
+// Globals
+var gSubfolder; // a local message folder used as a target for moves and copies
+var gFilter; // a message filter with a subject search
+var gAction; // current message action (reused)
+var gInboxListener; // database listener object
+var gHeader; // the current message db header
+var gInboxCount; // the previous number of messages in the Inbox
+var gSubfolderCount; // the previous number of messages in the subfolder
+var gMessage = "draft1"; // message file used as the test message
+
+// subject of the test message
+var gMessageSubject = "Hello, did you receive my bugmail?";
+
+// various object references
+var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+);
+
+// Definition of tests. The test function name is the filter action
+// being tested, with "Body" appended to tests that use delayed
+// application of filters due to a body search
+var gTestArray = [
+ setupIMAPPump,
+ setupFilters,
+ async function DoNothing() {
+ gAction.type = Ci.nsMsgFilterAction.StopExecution;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ await setupTest(gFilter, gAction);
+ testCounts(false, 1, 0, 0);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ },
+ async function Delete() {
+ gAction.type = Ci.nsMsgFilterAction.Delete;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ await setupTest(gFilter, gAction);
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ },
+ async function MoveToFolder() {
+ gAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gFilter, gAction);
+
+ testCounts(false, 0, 0, 0);
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ // no net messages were added to the inbox
+ Assert.equal(gInboxCount, folderCount(IMAPPump.inbox));
+ },
+ async function MarkRead() {
+ gAction.type = Ci.nsMsgFilterAction.MarkRead;
+ await setupTest(gFilter, gAction);
+ testCounts(false, 0, 0, 0);
+ Assert.ok(gHeader.isRead);
+ },
+ async function KillThread() {
+ gAction.type = Ci.nsMsgFilterAction.KillThread;
+ await setupTest(gFilter, gAction);
+ // In non-postplugin, count here is 0 and not 1. Need to investigate.
+ testCounts(false, 1, 0, 0);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ async function WatchThread() {
+ gAction.type = Ci.nsMsgFilterAction.WatchThread;
+ await setupTest(gFilter, gAction);
+ // In non-postplugin, count here is 0 and not 1. Need to investigate.
+ testCounts(false, 1, 0, 0);
+ let thread = db().getThreadContainingMsgHdr(gHeader);
+ Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched);
+ },
+ async function KillSubthread() {
+ gAction.type = Ci.nsMsgFilterAction.KillSubthread;
+ await setupTest(gFilter, gAction);
+ // In non-postplugin, count here is 0 and not 1. Need to investigate.
+ testCounts(false, 1, 0, 0);
+ Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored);
+ },
+ // this tests for marking message as junk
+ async function JunkScore() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 100;
+ await setupTest(gFilter, gAction);
+ // marking as junk resets new but not unread
+ testCounts(false, 1, 0, 0);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "100");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ async function MarkUnread() {
+ gAction.type = Ci.nsMsgFilterAction.MarkUnread;
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.ok(!gHeader.isRead);
+ },
+ async function MarkFlagged() {
+ gAction.type = Ci.nsMsgFilterAction.MarkFlagged;
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.ok(gHeader.isFlagged);
+ },
+ async function ChangePriority() {
+ gAction.type = Ci.nsMsgFilterAction.ChangePriority;
+ gAction.priority = Ci.nsMsgPriority.highest;
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority);
+ },
+ async function AddTag() {
+ gAction.type = Ci.nsMsgFilterAction.AddTag;
+ gAction.strValue = "TheTag";
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("keywords"), "thetag");
+ },
+ // this tests for marking message as good
+ async function JunkScoreAsGood() {
+ gAction.type = Ci.nsMsgFilterAction.JunkScore;
+ gAction.junkScore = 0;
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gHeader.getStringProperty("junkscore"), "0");
+ Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter");
+ },
+ async function CopyToFolder() {
+ gAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ gAction.targetFolderUri = gSubfolder.URI;
+ gInboxCount = folderCount(IMAPPump.inbox);
+ gSubfolderCount = folderCount(gSubfolder);
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox));
+ Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder));
+ },
+ async function Custom() {
+ gAction.type = Ci.nsMsgFilterAction.Custom;
+ gAction.customId = "mailnews@mozilla.org#testOffline";
+ gAction.strValue = "true";
+ actionTestOffline.needsBody = true;
+ await setupTest(gFilter, gAction);
+ testCounts(true, 1, 1, 1);
+ },
+ /**/
+ endTest,
+];
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ gTestArray.forEach(x => add_task(x));
+ run_next_test();
+}
+
+function setupFilters() {
+ // Create a non-body filter.
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ gFilter = filterList.createFilter("subject");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.attrib = Subject;
+ searchTerm.op = Is;
+ var value = searchTerm.value;
+ value.attrib = Subject;
+ value.str = gMessageSubject;
+ searchTerm.value = value;
+ searchTerm.booleanAnd = false;
+ gFilter.appendTerm(searchTerm);
+ gFilter.filterType = Ci.nsMsgFilterType.PostPlugin;
+ gFilter.enabled = true;
+
+ // an action that can be modified by tests
+ gAction = gFilter.createAction();
+
+ MailServices.filters.addCustomAction(actionTestOffline);
+ MailServices.mailSession.AddFolderListener(
+ FolderListener,
+ Ci.nsIFolderListener.event
+ );
+ gSubfolder = localAccountUtils.rootFolder.createLocalSubfolder("Subfolder");
+ gPreviousUnread = 0;
+}
+
+/*
+ * functions used to support test setup and execution
+ */
+
+// basic preparation done for each test
+async function setupTest(aFilter, aAction) {
+ let filterList = IMAPPump.incomingServer.getFilterList(null);
+ while (filterList.filterCount) {
+ filterList.removeFilterAt(0);
+ }
+ if (aFilter) {
+ aFilter.clearActionList();
+ if (aAction) {
+ aFilter.appendAction(aAction);
+ filterList.insertFilterAt(0, aFilter);
+ }
+ }
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+
+ gInboxListener = new DBListener();
+ gDbService.registerPendingListener(IMAPPump.inbox, gInboxListener);
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ await PromiseTestUtils.promiseDelay(200);
+}
+
+// Cleanup, null out everything, close all cached connections and stop the
+// server
+function endTest() {
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+ gInboxListener = null;
+ MailServices.mailSession.RemoveFolderListener(FolderListener);
+ teardownIMAPPump();
+}
+
+/*
+ * listener objects
+ */
+
+// nsIFolderListener implementation
+var FolderListener = {
+ onFolderEvent(aEventFolder, aEvent) {
+ dump(
+ "received folder event " + aEvent + " folder " + aEventFolder.name + "\n"
+ );
+ },
+};
+
+// nsIDBChangeListener implementation. Counts of calls are kept, but not
+// currently used in the tests. Current role is to provide a reference
+// to the new message header (plus give some examples of using db listeners
+// in javascript).
+function DBListener() {
+ this.counts = {};
+ let counts = this.counts;
+ counts.onHdrFlagsChanged = 0;
+ counts.onHdrDeleted = 0;
+ counts.onHdrAdded = 0;
+ counts.onParentChanged = 0;
+ counts.onAnnouncerGoingAway = 0;
+ counts.onReadChanged = 0;
+ counts.onJunkScoreChanged = 0;
+ counts.onHdrPropertyChanged = 0;
+ counts.onEvent = 0;
+}
+
+DBListener.prototype = {
+ onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) {
+ this.counts.onHdrFlagsChanged++;
+ },
+
+ onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) {
+ this.counts.onHdrDeleted++;
+ },
+
+ onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) {
+ this.counts.onHdrAdded++;
+ gHeader = aHdrChanged;
+ },
+
+ onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) {
+ this.counts.onParentChanged++;
+ },
+
+ onAnnouncerGoingAway(instigator) {
+ if (gInboxListener) {
+ try {
+ IMAPPump.inbox.msgDatabase.removeListener(gInboxListener);
+ } catch (e) {
+ dump(" listener not found\n");
+ }
+ }
+ this.counts.onAnnouncerGoingAway++;
+ },
+
+ onReadChanged(aInstigator) {
+ this.counts.onReadChanged++;
+ },
+
+ onJunkScoreChanged(aInstigator) {
+ this.counts.onJunkScoreChanged++;
+ },
+
+ onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) {
+ this.counts.onHdrPropertyChanged++;
+ },
+
+ onEvent(aDB, aEvent) {
+ this.counts.onEvent++;
+ },
+};
+
+/*
+ * helper functions
+ */
+
+// return the number of messages in a folder (and check that the
+// folder counts match the database counts)
+function folderCount(folder) {
+ // count using the database
+ let dbCount = [...folder.msgDatabase.enumerateMessages()].length;
+
+ // count using the folder
+ let count = folder.getTotalMessages(false);
+
+ // compare the two
+ Assert.equal(dbCount, count);
+ return dbCount;
+}
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
+
+// shorthand for the inbox message summary database
+function db() {
+ return IMAPPump.inbox.msgDatabase;
+}
+
+// static variables used in testCounts
+var gPreviousUnread;
+
+// Test various counts.
+//
+// aHasNew: folder hasNew flag
+// aUnreadDelta: change in unread count for the folder
+// aFolderNewDelta: change in new count for the folder
+// aDbNewDelta: change in new count for the database
+//
+function testCounts(aHasNew, aUnreadDelta, aFolderNewDelta, aDbNewDelta) {
+ try {
+ let folderNew = IMAPPump.inbox.getNumNewMessages(false);
+ let hasNew = IMAPPump.inbox.hasNewMessages;
+ let unread = IMAPPump.inbox.getNumUnread(false);
+ let arrayOut = db().getNewList();
+ let dbNew = arrayOut.length;
+ dump(
+ " hasNew: " +
+ hasNew +
+ " unread: " +
+ unread +
+ " folderNew: " +
+ folderNew +
+ " dbNew: " +
+ dbNew +
+ " prevUnread " +
+ gPreviousUnread +
+ "\n"
+ );
+ // Assert.equal(aHasNew, hasNew);
+ Assert.equal(aUnreadDelta, unread - gPreviousUnread);
+ gPreviousUnread = unread;
+ // This seems to be reset for each folder update.
+ //
+ // This check seems to be failing in SeaMonkey builds, yet I can see no ill
+ // effects of this in the actual program. Fixing this is complex because of
+ // the messiness of new count management (see bug 507638 for a
+ // refactoring proposal, and attachment 398899 on bug 514801 for one possible
+ // fix to this particular test). So I am disabling this.
+ // Assert.equal(aFolderNewDelta, folderNew);
+ // Assert.equal(aDbNewDelta, dbNew - gPreviousDbNew);
+ // gPreviousDbNew = dbNew;
+ } catch (e) {
+ dump(e);
+ }
+}
+
+// custom action to test offline status
+var actionTestOffline = {
+ id: "mailnews@mozilla.org#testOffline",
+ name: "test if offline",
+ applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ for (let msgHdr of aMsgHdrs) {
+ let isOffline = !!(msgHdr.flags & Ci.nsMsgMessageFlags.Offline);
+ dump(
+ "in actionTestOffline, flags are " +
+ msgHdr.flags +
+ " subject is " +
+ msgHdr.subject +
+ " isOffline is " +
+ isOffline +
+ "\n"
+ );
+ // XXX TODO: the offline flag is not set here when it should be in postplugin filters
+ // Assert.equal(isOffline, aActionValue == 'true');
+ Assert.equal(msgHdr.subject, gMessageSubject);
+ }
+ },
+ isValidForType(type, scope) {
+ return true;
+ },
+
+ validateActionValue(value, folder, type) {
+ return null;
+ },
+
+ allowDuplicates: false,
+
+ needsBody: true, // set during test setup
+};
diff --git a/comm/mailnews/imap/test/unit/test_imapFlagChange.js b/comm/mailnews/imap/test/unit/test_imapFlagChange.js
new file mode 100644
index 0000000000..332e434527
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapFlagChange.js
@@ -0,0 +1,216 @@
+/* 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/. */
+
+/*
+ * Test to ensure that imap flag changes made from a different profile/machine
+ * are stored in db.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gMessage;
+var gSecondFolder;
+var gSynthMessage;
+
+add_setup(async function () {
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+
+ setupIMAPPump();
+
+ IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true });
+
+ // build up a diverse list of messages
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+ gSynthMessage = messages[0];
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(gSynthMessage.toMessageString())
+ );
+ gMessage = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(gMessage);
+
+ // update folder to download header.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function switchAwayFromInbox() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ gSecondFolder = rootFolder
+ .getChildNamed("secondFolder")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+
+ // Selecting the second folder will close the cached connection
+ // on the inbox because fake server only supports one connection at a time.
+ // Then, we can poke at the message on the imap server directly, which
+ // simulates the user changing the message from a different machine,
+ // and Thunderbird discovering the change when it does a flag sync
+ // upon reselecting the Inbox.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function simulateForwardFlagSet() {
+ gMessage.setFlag("$Forwarded");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkForwardedFlagSet() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ Assert.equal(
+ msgHdr.flags & Ci.nsMsgMessageFlags.Forwarded,
+ Ci.nsMsgMessageFlags.Forwarded
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function clearForwardedFlag() {
+ gMessage.clearFlag("$Forwarded");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkForwardedFlagCleared() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ Assert.equal(msgHdr.flags & Ci.nsMsgMessageFlags.Forwarded, 0);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function setSeenFlag() {
+ gMessage.setFlag("\\Seen");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkSeenFlagSet() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ Assert.equal(
+ msgHdr.flags & Ci.nsMsgMessageFlags.Read,
+ Ci.nsMsgMessageFlags.Read
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function simulateRepliedFlagSet() {
+ gMessage.setFlag("\\Answered");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkRepliedFlagSet() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ Assert.equal(
+ msgHdr.flags & Ci.nsMsgMessageFlags.Replied,
+ Ci.nsMsgMessageFlags.Replied
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function simulateTagAdded() {
+ gMessage.setFlag("randomtag");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkTagSet() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ let keywords = msgHdr.getStringProperty("keywords");
+ Assert.ok(keywords.includes("randomtag"));
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+/** Test that the NonJunk tag from the server is noticed. */
+add_task(async function checkNonJunkTagSet() {
+ gMessage.clearFlag("NotJunk");
+ gMessage.setFlag("NonJunk");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ Assert.equal(
+ junkScore,
+ Ci.nsIJunkMailPlugin.IS_HAM_SCORE,
+ "NonJunk flag on server should mark as ham"
+ );
+});
+
+/** Test that the NotJunk tag from the server is noticed. */
+add_task(async function checkNotJunkTagSet() {
+ gMessage.clearFlag("NonJunk");
+ gMessage.setFlag("NotJunk");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ Assert.equal(
+ junkScore,
+ Ci.nsIJunkMailPlugin.IS_HAM_SCORE,
+ "NotJunk flag on server should mark as ham"
+ );
+});
+
+add_task(async function clearTag() {
+ gMessage.clearFlag("randomtag");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function checkTagCleared() {
+ let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage.messageId
+ );
+ let keywords = msgHdr.getStringProperty("keywords");
+ Assert.ok(!keywords.includes("randomtag"));
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapFolderCopy.js b/comm/mailnews/imap/test/unit/test_imapFolderCopy.js
new file mode 100644
index 0000000000..4b7e1bdb18
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapFolderCopy.js
@@ -0,0 +1,137 @@
+/* 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/. */
+
+// This file tests the folder copying with IMAP. In particular, we're
+// going to test copying local folders to imap servers, but other tests
+// could be added.
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gEmptyLocal1, gEmptyLocal2, gEmptyLocal3, gNotEmptyLocal4;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_setup(function () {
+ setupIMAPPump();
+
+ gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1");
+ gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2");
+ gEmptyLocal3 = localAccountUtils.rootFolder.createLocalSubfolder("empty 3");
+ gNotEmptyLocal4 =
+ localAccountUtils.rootFolder.createLocalSubfolder("not empty 4");
+
+ let messageGenerator = new MessageGenerator();
+ let message = messageGenerator.makeMessage();
+ gNotEmptyLocal4.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ gNotEmptyLocal4.addMessage(message.toMboxString());
+
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+});
+
+add_task(async function copyFolder1() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gEmptyLocal1,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function copyFolder2() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gEmptyLocal2,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function copyFolder3() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gEmptyLocal3,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(function verifyFolders() {
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ let folder2 = IMAPPump.inbox.getChildNamed("empty 2");
+ let folder3 = IMAPPump.inbox.getChildNamed("empty 3");
+ Assert.ok(folder1 !== null);
+ Assert.ok(folder2 !== null);
+ Assert.ok(folder3 !== null);
+});
+
+add_task(async function moveImapFolder1() {
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ let folder2 = IMAPPump.inbox.getChildNamed("empty 2");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(folder2, folder1, true, copyListener, null);
+ await copyListener.promise;
+});
+
+add_task(async function moveImapFolder2() {
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ let folder3 = IMAPPump.inbox.getChildNamed("empty 3");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(folder3, folder1, true, copyListener, null);
+ await copyListener.promise;
+});
+
+add_task(function verifyImapFolders() {
+ let folder1 = IMAPPump.inbox.getChildNamed("empty 1");
+ let folder2 = folder1.getChildNamed("empty 2");
+ let folder3 = folder1.getChildNamed("empty 3");
+ Assert.ok(folder1 !== null);
+ Assert.ok(folder2 !== null);
+ Assert.ok(folder3 !== null);
+});
+
+add_task(async function testImapFolderCopyFailure() {
+ IMAPPump.daemon.commandToFail = "APPEND";
+ // we expect NS_MSG_ERROR_IMAP_COMMAND_FAILED;
+ const NS_MSG_ERROR_IMAP_COMMAND_FAILED = 0x80550021;
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gNotEmptyLocal4,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await Assert.rejects(
+ copyListener.promise,
+ e => {
+ return e === NS_MSG_ERROR_IMAP_COMMAND_FAILED;
+ },
+ "NS_MSG_ERROR_IMAP_COMMAND_FAILED should be the cause of the error"
+ );
+});
+
+add_task(function teardown() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapHdrChunking.js b/comm/mailnews/imap/test/unit/test_imapHdrChunking.js
new file mode 100644
index 0000000000..8fc23e8502
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapHdrChunking.js
@@ -0,0 +1,168 @@
+/* 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/. */
+
+/*
+ * Tests imap msg header download chunking
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+/**
+ * Keep it so that OVERALL_MESSAGES % CHUNKING_SIZE !== 0.
+ * With a modulo operator for CHUNKING_SIZE and a prime number for
+ * OVERALL_MESSAGES this should prove that there have been a
+ * chunking process without being depended on the first chunk.
+ */
+const CHUNKING_SIZE = 3;
+const OVERALL_MESSAGES = 137;
+
+// Dummy message window so we can say the inbox is open in a window.
+var dummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+function FolderIntPropertyChangedListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+ this._gotNewMailBiff = false;
+}
+
+FolderIntPropertyChangedListener.prototype = {
+ onFolderIntPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {
+ if (
+ aProperty == "BiffState" &&
+ aNewValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail
+ ) {
+ this._gotNewMailBiff = true;
+ this._resolve();
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+ get gotNewMailBiff() {
+ return this._gotNewMailBiff;
+ },
+};
+
+var gFolderListener = new FolderIntPropertyChangedListener();
+/** Used to store a listener between tasks for inspecting chunking behaviour. */
+var gListener = new PromiseTestUtils.PromiseUrlListener();
+
+add_setup(async function () {
+ Assert.equal(
+ OVERALL_MESSAGES % CHUNKING_SIZE !== 0,
+ true,
+ "const sanity check"
+ );
+ setupIMAPPump();
+ // We need to register the dummyMsgWindow so that we'll think the
+ // Inbox is open in a folder and fetch headers in chunks.
+ dummyMsgWindow.openFolder = IMAPPump.inbox;
+ MailServices.mailSession.AddMsgWindow(dummyMsgWindow);
+ MailServices.mailSession.AddFolderListener(
+ gFolderListener,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Set chunk size to CHUNKING_SIZE, so we'll have to chain several requests to get
+ // OVERALL_MESSAGES headers.
+ Services.prefs.setIntPref("mail.imap.hdr_chunk_size", CHUNKING_SIZE);
+ // Turn off offline sync to avoid complications in verifying that we can
+ // run a url after the first header chunk.
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// Upload messages to the imap fake server Inbox.
+add_task(async function uploadImapMessages() {
+ // make OVERALL_MESSAGES messages
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+ // build up a list of messages
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(OVERALL_MESSAGES));
+
+ // Add OVERALL_MESSAGES messages with uids 1,2,3...,OVERALL_MESSAGES.
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ // Create the ImapMessages and store them on the mailbox.
+ messages.forEach(function (message) {
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message.toMessageString())
+ );
+ imapInbox.addMessage(
+ new ImapMessage(dataUri.spec, imapInbox.uidnext++, [])
+ );
+ });
+ // Do not wait for the listener to finish.
+ // We want to observe the message batches in the update process.
+ // updateFolderWithListener with null for nsIMsgWindow makes biff notify.
+ IMAPPump.inbox.updateFolderWithListener(null, gListener);
+});
+
+add_task(async function testMessageFetched() {
+ // If we're really chunking, then the message fetch should have started before
+ // we finished the updateFolder URL.
+ await TestUtils.waitForCondition(() => {
+ return gFolderListener.gotNewMailBiff === true;
+ });
+ Assert.ok(gFolderListener.gotNewMailBiff);
+
+ // We do not check for the first chunk as this is unreliable without explicit
+ // listeners/events.
+ // Instead we are checking if there's no rest of the division with
+ // CHUNKING_SIZE while the chunking process is ongoing.
+ // It's important that the chunking is intact and as well not failing
+ // randomly in the test infrastructure.
+ // See at the CHUNKING_SIZE and OVERALL_MESSAGES declarations.
+ //
+ // HINT:
+ // If this causes future problems because stuff getting faster,
+ // try to increase the overall message count.
+ await TestUtils.waitForCondition(() => {
+ let messagesDBFolder = IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages;
+ if (messagesDBFolder !== 0) {
+ Assert.equal(
+ messagesDBFolder % CHUNKING_SIZE,
+ 0,
+ `${messagesDBFolder} messages in folder should be of chunk size ${CHUNKING_SIZE}`
+ ); // This is the primary test.
+ return true;
+ } else if (messagesDBFolder === OVERALL_MESSAGES) {
+ throw new Error(
+ `Batching failed in sizes of ${CHUNKING_SIZE} found instead ${OVERALL_MESSAGES} immediately`
+ );
+ }
+ return false; // Rerun waitForCondition.
+ }, 50);
+});
+
+add_task(async function testHdrsDownloaded() {
+ await gListener.promise; // Now we wait for the finished update of the Folder.
+ // Make sure that we got all OVERALL_MESSAGES headers.
+ Assert.equal(
+ IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages,
+ OVERALL_MESSAGES
+ );
+});
+
+// Cleanup
+add_task(async function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js b/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js
new file mode 100644
index 0000000000..ca148dace6
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+/* 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/. */
+
+/**
+ * This test checks if the imap message service code streams headers correctly.
+ * It checks that streaming headers for messages stored for offline use works.
+ * It doesn't test streaming messages that haven't been stored for offline use
+ * because that's not implemented yet, and it's unclear if anyone will want it.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+setupIMAPPump();
+
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+
+// Adds some messages directly to a mailbox (e.g. new mail).
+function addMessagesToServer(messages, mailbox) {
+ // For every message we have, we need to convert it to a file:/// URI.
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ });
+}
+
+add_setup(async function () {
+ // Add a couple of messages to the INBOX
+ // this is synchronous, afaik.
+ addMessagesToServer(
+ [{ file: gMsgFile1, messageId: gMsgId1 }],
+ IMAPPump.daemon.getMailbox("INBOX")
+ );
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ // Update IMAP Folder.
+ let listenerUpdate = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listenerUpdate);
+ await listenerUpdate.promise;
+ // Download all for offline.
+ let listenerDownload = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listenerDownload, null);
+ await listenerDownload.promise;
+});
+
+add_task(async function test_streamHeaders() {
+ let newMsgHdr = IMAPPump.inbox.GetMessageHeader(1);
+ let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ // We use this as a display consumer
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamHeaders(msgURI, streamListener, null, true);
+ let data = await streamListener.promise;
+ Assert.ok(data.includes("Content-Type"));
+});
+
+add_task(async function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapHighWater.js b/comm/mailnews/imap/test/unit/test_imapHighWater.js
new file mode 100644
index 0000000000..3fcd4bcc23
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapHighWater.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gIMAPDaemon, gServer, gIMAPIncomingServer;
+
+var gIMAPInbox;
+var gFolder1, gRootFolder;
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox) {
+ // Create the ImapMessages and store them on the mailbox
+ messages.forEach(function (message) {
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message.toMessageString())
+ );
+ mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, []));
+ });
+}
+
+add_setup(function () {
+ localAccountUtils.loadLocalMailAccount();
+
+ /*
+ * Set up an IMAP server.
+ */
+ gIMAPDaemon = new ImapDaemon();
+ gServer = makeServer(gIMAPDaemon, "");
+ gIMAPDaemon.createMailbox("folder 1", { subscribed: true });
+ gIMAPIncomingServer = createLocalIMAPServer(gServer.port);
+ gIMAPIncomingServer.maximumConnectionsNumber = 1;
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+ localAccount.incomingServer = localAccountUtils.incomingServer;
+
+ // Let's also have another account, using the same identity
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = gIMAPIncomingServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // pref tuning: one connection only, turn off notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ // Don't prompt about offline download when going offline
+ Services.prefs.setIntPref("offline.download.download_messages", 2);
+});
+
+add_setup(function () {
+ // make 10 messages
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+ // build up a list of messages
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(10));
+
+ // Add 10 messages with uids 1-10.
+ let imapInbox = gIMAPDaemon.getMailbox("INBOX");
+ addMessagesToServer(messages, imapInbox);
+ messages = [];
+ messages = messages.concat(messageGenerator.makeMessage());
+ // Add a single message to move target folder.
+ addMessagesToServer(messages, gIMAPDaemon.getMailbox("folder 1"));
+
+ // Get the IMAP inbox...
+ gRootFolder = gIMAPIncomingServer.rootFolder;
+ gIMAPInbox = gRootFolder
+ .getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox)
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+});
+
+add_task(async function doMoves() {
+ // update folders to download headers.
+ let urlListenerInbox = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPInbox.updateFolderWithListener(null, urlListenerInbox);
+ await urlListenerInbox.promise;
+ gFolder1 = gRootFolder
+ .getChildNamed("folder 1")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let urlListenerFolder1 = new PromiseTestUtils.PromiseUrlListener();
+ gFolder1.updateFolderWithListener(null, urlListenerFolder1);
+ await urlListenerFolder1.promise;
+ // get five messages to move from Inbox to folder 1.
+ let headers1 = [];
+ let count = 0;
+ for (let header of gIMAPInbox.msgDatabase.enumerateMessages()) {
+ if (count >= 5) {
+ break;
+ }
+ if (header instanceof Ci.nsIMsgDBHdr) {
+ headers1.push(header);
+ }
+ count++;
+ }
+ // this will add dummy headers with keys > 0xffffff80
+ let copyListenerDummyHeaders = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gIMAPInbox,
+ headers1,
+ gFolder1,
+ true,
+ copyListenerDummyHeaders,
+ gDummyMsgWindow,
+ true
+ );
+ await copyListenerDummyHeaders.promise;
+
+ let urlListenerInboxAfterDummy = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPInbox.updateFolderWithListener(null, urlListenerInboxAfterDummy);
+ await urlListenerInboxAfterDummy.promise;
+
+ let urlListenerFolder1AfterDummy = new PromiseTestUtils.PromiseUrlListener();
+ gFolder1.updateFolderWithListener(
+ gDummyMsgWindow,
+ urlListenerFolder1AfterDummy
+ );
+ await urlListenerFolder1AfterDummy.promise;
+
+ // Check that playing back offline events gets rid of dummy
+ // headers, and thus highWater is recalculated.
+ Assert.equal(gFolder1.msgDatabase.dBFolderInfo.highWater, 6);
+ headers1 = [];
+ count = 0;
+ for (let header of gIMAPInbox.msgDatabase.enumerateMessages()) {
+ if (count >= 5) {
+ break;
+ }
+ if (header instanceof Ci.nsIMsgDBHdr) {
+ headers1.push(header);
+ }
+ count++;
+ }
+ // Check that copyMessages will handle having a high highwater mark.
+ // It will thrown an exception if it can't.
+ let msgHdr = gFolder1.msgDatabase.createNewHdr(0xfffffffd);
+ gFolder1.msgDatabase.addNewHdrToDB(msgHdr, false);
+ let copyListenerHighWater = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gIMAPInbox,
+ headers1,
+ gFolder1,
+ true,
+ copyListenerHighWater,
+ gDummyMsgWindow,
+ true
+ );
+ await copyListenerHighWater.promise;
+ gServer.performTest("UID COPY");
+
+ gFolder1.msgDatabase.deleteHeader(msgHdr, null, true, false);
+ let urlListenerInboxAfterDelete = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPInbox.updateFolderWithListener(null, urlListenerInboxAfterDelete);
+ await urlListenerInboxAfterDelete.promise;
+ // this should clear the dummy headers.
+ let urlListenerFolder1AfterDelete = new PromiseTestUtils.PromiseUrlListener();
+ gFolder1.updateFolderWithListener(
+ gDummyMsgWindow,
+ urlListenerFolder1AfterDelete
+ );
+ await urlListenerFolder1AfterDelete.promise;
+ Assert.equal(gFolder1.msgDatabase.dBFolderInfo.highWater, 11);
+});
+
+add_task(function endTest() {
+ Services.io.offline = true;
+ gServer.performTest("LOGOUT");
+ gIMAPIncomingServer.closeCachedConnections();
+ gServer.stop();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapID.js b/comm/mailnews/imap/test/unit/test_imapID.js
new file mode 100644
index 0000000000..fe1f70fbdf
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapID.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/*
+ * Test to ensure that we handle the RFC2197 ID command.
+ */
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var kIDResponse =
+ '("name" "GImap" "vendor" "Google, Inc." "support-url" "http://mail.google.com/support")';
+
+add_setup(async function () {
+ setupIMAPPump("GMail");
+ IMAPPump.daemon.idResponse = kIDResponse;
+
+ // update folder to kick start tests.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+});
+
+add_task(async function updateInbox() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+});
+
+add_task(function checkIDHandling() {
+ Assert.equal(IMAPPump.daemon.clientID, '("name" "xpcshell" "version" "1")');
+ Assert.equal(IMAPPump.incomingServer.serverIDPref, kIDResponse);
+});
+
+add_task(teardownIMAPPump);
diff --git a/comm/mailnews/imap/test/unit/test_imapMove.js b/comm/mailnews/imap/test/unit/test_imapMove.js
new file mode 100644
index 0000000000..e19896ef90
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapMove.js
@@ -0,0 +1,88 @@
+/* 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/. */
+
+// This tests that we use IMAP move if the IMAP server supports it.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/logHelper.js");
+load("../../../resources/MessageGenerator.jsm");
+
+var gFolder1;
+
+var tests = [setupCUSTOM1, startTest, doMove, testMove, teardownIMAPPump];
+
+function setupCUSTOM1() {
+ setupIMAPPump("CUSTOM1");
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+}
+
+async function startTest() {
+ IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null);
+ await PromiseTestUtils.promiseFolderAdded("folder 1");
+
+ addImapMessage();
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ // ...and download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+}
+
+async function doMove() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ gFolder1 = rootFolder
+ .getChildNamed("folder 1")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let msg = IMAPPump.inbox.msgDatabase.getMsgHdrForKey(
+ IMAPPump.mailbox.uidnext - 1
+ );
+ IMAPPump.server._test = true;
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msg],
+ gFolder1,
+ true,
+ listener,
+ null,
+ false
+ );
+ IMAPPump.server.performTest("UID MOVE");
+ await listener.promise;
+}
+
+async function testMove() {
+ Assert.equal(IMAPPump.inbox.getTotalMessages(false), 0);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gFolder1.updateFolderWithListener(null, listener);
+ await listener.promise;
+ Assert.equal(gFolder1.getTotalMessages(false), 1);
+
+ // maildir should also delete the files.
+ if (IMAPPump.inbox.msgStore.storeType == "maildir") {
+ let curDir = IMAPPump.inbox.filePath.clone();
+ curDir.append("cur");
+ Assert.ok(curDir.exists());
+ Assert.ok(curDir.isDirectory());
+ let curEnum = curDir.directoryEntries;
+ // the directory should be empty, fails from bug 771643
+ Assert.ok(!curEnum.hasMoreElements());
+ }
+}
+
+function run_test() {
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js b/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js
new file mode 100644
index 0000000000..b28c646907
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js
@@ -0,0 +1,179 @@
+/**
+ * This test checks to see if the imap password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var kUserName = "user";
+var kInvalidPassword = "imaptest";
+var kValidPassword = "password";
+
+var incomingServer, server;
+var attempt = 0;
+
+function confirmExPS(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting retry\n");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ dump("\nCancelling login attempt\n");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ dump("\nAttempting Retry\n");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ dump("\nEnter new password\n");
+ return 2;
+ default:
+ do_throw("unexpected attempt number " + attempt);
+ return 1;
+ }
+}
+
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
+
+add_task(async function () {
+ do_test_pending();
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-imap.json");
+
+ registerAlertTestUtils();
+
+ let daemon = new ImapDaemon();
+ daemon.createMailbox("Subscribed", { subscribed: true });
+ server = makeServer(daemon, "", {
+ // Make username of server match the singons.txt file
+ // (pw there is intentionally invalid)
+ kUsername: kUserName,
+ kPassword: kValidPassword,
+ });
+ server.setDebugLevel(fsDebugAll);
+
+ incomingServer = createLocalIMAPServer(server.port);
+
+ // PerformExpand expects us to already have a password loaded into the
+ // incomingServer when we call it, so force a get password call to get it
+ // out of the signons file (first removing the value that
+ // createLocalIMAPServer puts in there).
+ incomingServer.password = "";
+ let password = incomingServer.getPasswordWithUI(
+ "Prompt Message",
+ "Prompt Title"
+ );
+
+ // The fake server expects one password, but we're feeding it an invalid one
+ // initially so that we can check what happens when password is denied.
+ Assert.equal(password, kInvalidPassword);
+
+ // First step, try and perform a subscribe where we won't be able to log in.
+ // This covers attempts 1 and 2 in confirmEx.
+ dump("\nperformExpand 1\n\n");
+
+ incomingServer.performExpand(gDummyMsgWindow);
+ server.performTest("SUBSCRIBE");
+
+ dump("\nfinished subscribe 1\n\n");
+
+ Assert.equal(attempt, 2);
+
+ let rootFolder = incomingServer.rootFolder;
+ Assert.ok(rootFolder.containsChildNamed("Inbox"));
+ Assert.ok(!rootFolder.containsChildNamed("Subscribed"));
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ let logins = Services.logins.findLogins(
+ "imap://localhost",
+ null,
+ "imap://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+
+ dump("\nperformExpand 2\n\n");
+
+ incomingServer.performExpand(gDummyMsgWindow);
+ server.performTest("SUBSCRIBE");
+
+ dump("\nfinished subscribe 2\n");
+
+ Assert.ok(rootFolder.containsChildNamed("Inbox"));
+ Assert.ok(rootFolder.containsChildNamed("Subscribed"));
+
+ // Now check the new one has been saved.
+ logins = Services.logins.findLogins(
+ "imap://localhost",
+ null,
+ "imap://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+
+ // Remove the login via the incoming server.
+ incomingServer.forgetPassword();
+ logins = Services.logins.findLogins(
+ "imap://localhost",
+ null,
+ "imap://localhost"
+ );
+
+ Assert.equal(logins.length, 0);
+
+ do_timeout(500, endTest);
+});
+
+function endTest() {
+ incomingServer.closeCachedConnections();
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapProtocols.js b/comm/mailnews/imap/test/unit/test_imapProtocols.js
new file mode 100644
index 0000000000..2043c44567
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapProtocols.js
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for IMAP nsIProtocolHandler implementations.
+ */
+
+var defaultProtocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS |
+ Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC;
+
+var protocols = [
+ {
+ protocol: "imap",
+ urlSpec: "imap://user@localhost/",
+ defaultPort: Ci.nsIImapUrl.DEFAULT_IMAP_PORT,
+ },
+ // XXX Imaps protocol not available via nsIProtocolHandler yet.
+ // {
+ // protocol: "imaps",
+ // urlSpec: "iamps://user@localhost/",
+ // defaultPort: Ci.nsIImapUrl.DEFAULT_IMAPS_PORT,
+ // },
+];
+
+function run_test() {
+ // We need a server to match the urlSpecs above.
+ createLocalIMAPServer();
+
+ for (var part = 0; part < protocols.length; ++part) {
+ print("protocol: " + protocols[part].protocol);
+
+ var pH = Cc[
+ "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol
+ ].createInstance(Ci.nsIProtocolHandler);
+
+ Assert.equal(pH.scheme, protocols[part].protocol);
+ Assert.equal(
+ Services.io.getDefaultPort(pH.scheme),
+ protocols[part].defaultPort
+ );
+ Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags);
+
+ // Whip through some of the ports to check we get the right results.
+ // IMAP allows connecting to any port.
+ for (let i = 0; i < 1024; ++i) {
+ Assert.ok(pH.allowPort(i, ""));
+ }
+
+ // Check we get a URI when we ask for one
+ var uri = Services.io.newURI(protocols[part].urlSpec);
+
+ uri.QueryInterface(Ci.nsIImapUrl);
+
+ Assert.equal(uri.spec, protocols[part].urlSpec);
+ }
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapProxy.js b/comm/mailnews/imap/test/unit/test_imapProxy.js
new file mode 100644
index 0000000000..aa935fff68
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapProxy.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Test that IMAP over a SOCKS proxy works.
+
+var { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+var server, daemon, incomingServer;
+
+const PORT = 143;
+
+add_setup(async function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ daemon = new ImapDaemon();
+ server = makeServer(daemon, "");
+
+ let messages = [];
+ let messageGenerator = new MessageGenerator();
+ messages = messages.concat(messageGenerator.makeMessage());
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapMsg = new ImapMessage(dataUri.spec, daemon.inbox.uidnext++, []);
+ daemon.inbox.addMessage(imapMsg);
+
+ NetworkTestUtils.configureProxy("imap.tinderbox.invalid", PORT, server.port);
+
+ // Set up the basic accounts and folders
+ incomingServer = createLocalIMAPServer(PORT, "imap.tinderbox.invalid");
+ let identity = MailServices.accounts.createIdentity();
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = incomingServer;
+});
+
+add_task(async function downloadEmail() {
+ let inboxFolder = incomingServer.rootFolder.getChildNamed("INBOX");
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(inboxFolder.getTotalMessages(false), 0);
+
+ // Now get the mail
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ inboxFolder.getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+
+ // We downloaded a message, so it works!
+ Assert.equal(inboxFolder.getTotalMessages(false), 1);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ incomingServer.closeCachedConnections();
+ server.stop();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapRename.js b/comm/mailnews/imap/test/unit/test_imapRename.js
new file mode 100644
index 0000000000..f346729a92
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapRename.js
@@ -0,0 +1,43 @@
+/* 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/. */
+
+// This tests that renaming non-ASCII name folder works.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_setup(async function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null);
+ await PromiseTestUtils.promiseFolderAdded("folder 1");
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function test_rename() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ let targetFolder = rootFolder.getChildNamed("folder 1");
+
+ targetFolder.rename("folder \u00e1", null);
+
+ IMAPPump.server.performTest("RENAME");
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ let folder = rootFolder.getChildNamed("folder \u00e1");
+ Assert.ok(folder.msgDatabase.summaryValid);
+ Assert.equal("folder &AOE-", folder.filePath.leafName);
+ Assert.equal("folder \u00e1", folder.prettyName);
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapSearch.js b/comm/mailnews/imap/test/unit/test_imapSearch.js
new file mode 100644
index 0000000000..975d1f2dae
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapSearch.js
@@ -0,0 +1,348 @@
+/* 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/. */
+
+/* Tests traditional (non-gloda) search on IMAP folders.
+ * Derived from a combination of test_imapPump.js and test_search.js
+ * Original author: Kent James <kent@caspia.com>
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// headers we will store in db
+// set value of headers we want parsed into the db
+Services.prefs.setCharPref(
+ "mailnews.customDBHeaders",
+ "x-spam-status oneliner twoliner threeliner nospace withspace"
+);
+Assert.equal(
+ Services.prefs.getCharPref("mailnews.customDBHeaders"),
+ "x-spam-status oneliner twoliner threeliner nospace withspace"
+);
+
+// set customHeaders, which post-bug 363238 should get added to the db. Note that all headers but the last
+// seem to end in colon.
+Services.prefs.setCharPref(
+ "mailnews.customHeaders",
+ "x-uidl: x-bugzilla-watch-reason: x-bugzilla-component: received: x-spam-checker-version"
+);
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail12"; // message file used as the test message
+
+/*
+/*
+ * Testing of general mail search features.
+ *
+ * This tests some search attributes not tested by other specific tests,
+ * e.g., test_searchTag.js or test_searchJunk.js
+ */
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var Is = Ci.nsMsgSearchOp.Is;
+var Contains = Ci.nsMsgSearchOp.Contains;
+var BeginsWith = Ci.nsMsgSearchOp.BeginsWith;
+var EndsWith = Ci.nsMsgSearchOp.EndsWith;
+
+var OtherHeader = Ci.nsMsgSearchAttrib.OtherHeader;
+var From = Ci.nsMsgSearchAttrib.Sender;
+var Subject = Ci.nsMsgSearchAttrib.Subject;
+
+var searchTests = [
+ // test the To: header
+ {
+ testString: "PrimaryEmail1@test.invalid",
+ testAttribute: From,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: "PrimaryEmail1@test.invalid",
+ testAttribute: From,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ testString: "PrimaryEmail",
+ testAttribute: From,
+ op: BeginsWith,
+ count: 1,
+ },
+ {
+ testString: "invalid",
+ testAttribute: From,
+ op: BeginsWith,
+ count: 0,
+ },
+ {
+ testString: "invalid",
+ testAttribute: From,
+ op: EndsWith,
+ count: 1,
+ },
+ {
+ testString: "Primary",
+ testAttribute: From,
+ op: EndsWith,
+ count: 0,
+ },
+ {
+ testString: "QAContact",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ count: 1,
+ },
+ {
+ testString: "filters",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ count: 0,
+ },
+ {
+ testString: "mail.bugs",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ count: 1,
+ },
+ {
+ testString: "QAContact",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ count: 0,
+ },
+ {
+ testString: "QAcontact filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: "filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Is,
+ count: 0,
+ },
+ {
+ testString: "QAcontact filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ testString: "QAcontact",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ testString: "filters",
+ testAttribute: OtherHeader,
+ op: Contains,
+ count: 1,
+ },
+ {
+ testString: "foobar",
+ testAttribute: OtherHeader,
+ op: Contains,
+ count: 0,
+ },
+ // test accumulation of received header
+ {
+ // only in first received
+ testString: "caspiaco",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "Received",
+ count: 1,
+ },
+ {
+ // only in second
+ testString: "webapp01.sj.mozilla.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "received",
+ count: 1,
+ },
+ {
+ // in neither
+ testString: "not there",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "received",
+ count: 0,
+ },
+ // test multiple line arbitrary headers
+ {
+ // in the first line
+ testString: "SpamAssassin 3.2.3",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ {
+ // in the second line
+ testString: "host29.example.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ {
+ // spans two lines with space
+ testString: "on host29.example.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ // subject spanning several lines
+ {
+ // on the first line
+ testString: "A filter will",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ {
+ testString: "I do not exist",
+ testAttribute: Subject,
+ op: Contains,
+ count: 0,
+ },
+ {
+ // on the second line
+ testString: "this message",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ {
+ // spanning second and third line
+ testString: "over many",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ // tests of custom headers db values
+ {
+ testString: "a one line header",
+ dbHeader: "oneliner",
+ },
+ {
+ testString: "a two line header",
+ dbHeader: "twoliner",
+ },
+ {
+ testString: "a three line header with lotsa space and tabs",
+ dbHeader: "threeliner",
+ },
+ {
+ testString: "I have no space",
+ dbHeader: "nospace",
+ },
+ {
+ testString: "too much space",
+ dbHeader: "withspace",
+ },
+ // tests of custom db headers in a search
+ {
+ testString: "one line",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "oneliner",
+ count: 1,
+ },
+ {
+ testString: "two line header",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "twoliner",
+ count: 1,
+ },
+ {
+ testString: "three line header with lotsa",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "threeliner",
+ count: 1,
+ },
+ {
+ testString: "I have no space",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "nospace",
+ count: 1,
+ },
+ {
+ testString: "too much space",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "withspace",
+ count: 1,
+ },
+];
+
+add_setup(async function () {
+ setupIMAPPump();
+
+ // don't use offline store
+ IMAPPump.inbox.clearFlag(Ci.nsMsgFolderFlags.Offline);
+
+ // Load imap message.
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+});
+
+// process each test from queue, calls itself upon completion of each search
+add_task(async function testSearch() {
+ while (searchTests.length) {
+ let test = searchTests.shift();
+ if (test.dbHeader) {
+ // Test of a custom db header.
+ let customValue = mailTestUtils
+ .firstMsgHdr(IMAPPump.inbox)
+ .getStringProperty(test.dbHeader);
+ Assert.equal(customValue, test.testString);
+ } else {
+ await new Promise(resolve => {
+ new TestSearch(
+ IMAPPump.inbox,
+ test.testString,
+ test.testAttribute,
+ test.op,
+ test.count,
+ resolve,
+ null,
+ test.customHeader ? test.customHeader : "X-Bugzilla-Watch-Reason"
+ );
+ });
+ }
+ }
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper function
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js b/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js
new file mode 100644
index 0000000000..e81e5a120d
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+// This file tests that checking folders for new mail with STATUS
+// doesn't leave db's open.
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFolder1, gFolder2;
+
+add_setup(function () {
+ Services.prefs.setBoolPref("mail.check_all_imap_folders_for_new", true);
+ Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 2);
+
+ setupIMAPPump();
+
+ IMAPPump.daemon.createMailbox("folder 1", { subscribed: true });
+ IMAPPump.daemon.createMailbox("folder 2", { subscribed: true });
+
+ IMAPPump.server.performTest("SUBSCRIBE");
+
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ gFolder1 = rootFolder.getChildNamed("folder 1");
+ gFolder2 = rootFolder.getChildNamed("folder 2");
+
+ IMAPPump.inbox.getNewMessages(null, null);
+ IMAPPump.server.performTest("STATUS");
+ Assert.ok(IMAPPump.server.isTestFinished());
+ // don't know if this will work, but we'll try. Wait for
+ // second status response
+ IMAPPump.server.performTest("STATUS");
+ Assert.ok(IMAPPump.server.isTestFinished());
+});
+
+add_task(function check() {
+ const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+ Assert.ok(gDbService.cachedDBForFolder(IMAPPump.inbox) !== null);
+ Assert.ok(gDbService.cachedDBForFolder(gFolder1) === null);
+ Assert.ok(gDbService.cachedDBForFolder(gFolder2) === null);
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js b/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js
new file mode 100644
index 0000000000..4601d4b509
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+/**
+ * This test checks if the imap protocol code saves message to
+ * offline stores correctly, when we fetch the message for display.
+ * It checks:
+ * - Normal messages, no attachments.
+ * - Message with inline attachment (e.g., image)
+ * - Message with non-inline attachment (e.g., .doc file)
+ * - Message with mix of attachment types.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gMessageGenerator = new MessageGenerator();
+
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgFile2 = do_get_file("../../../data/image-attach-test");
+var gMsgId2 = "4A947F73.5030709@example.com";
+var gMsgFile3 = do_get_file("../../../data/external-attach-test");
+var gMsgId3 = "876TY.5030709@example.com";
+
+var gFirstNewMsg;
+var gFirstMsgSize;
+var gImapInboxOfflineStoreSize;
+
+// Adds some messages directly to a mailbox (e.g. new mail).
+function addMessagesToServer(messages, mailbox) {
+ // For every message we have, we need to convert it to a file:/// URI.
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ });
+}
+
+add_setup(async function () {
+ // We aren't interested in downloading messages automatically.
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ Services.prefs.setBoolPref("mail.server.server1.offline_download", true);
+
+ setupIMAPPump();
+
+ // These hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+
+ addMessagesToServer(
+ [
+ { file: gMsgFile1, messageId: gMsgId1 },
+ { file: gMsgFile2, messageId: gMsgId2 },
+ { file: gMsgFile3, messageId: gMsgId3 },
+ ],
+ IMAPPump.daemon.getMailbox("INBOX")
+ );
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+var gIMAPService;
+
+add_task(async function selectFirstMsg() {
+ // We postpone creating the imap service until after we've set the prefs
+ // that it reads on its startup.
+ gIMAPService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+ ].getService(Ci.nsIMsgMessageService);
+
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg1 = db.getMsgHdrForMessageID(gMsgId1);
+ let listener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl: (aUrl, aExitCode) => {
+ Assert.equal(aExitCode, 0);
+ },
+ });
+ // We use the streamListener as a display consumer.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ gIMAPService.loadMessage(
+ IMAPPump.inbox.getUriForMsg(msg1),
+ streamListener,
+ null,
+ listener,
+ false
+ );
+ await listener.promise;
+});
+
+add_task(async function select2ndMsg() {
+ let msg1 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ Assert.notEqual(msg1.flags & Ci.nsMsgMessageFlags.Offline, 0);
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg2 = db.getMsgHdrForMessageID(gMsgId2);
+ let listener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl: (aUrl, aExitCode) => {
+ Assert.equal(aExitCode, 0);
+ },
+ });
+ // We use the streamListener as a display consumer.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ gIMAPService.loadMessage(
+ IMAPPump.inbox.getUriForMsg(msg2),
+ streamListener,
+ null,
+ listener,
+ false
+ );
+ await listener.promise;
+});
+
+add_task(async function select3rdMsg() {
+ let msg2 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+ Assert.notEqual(msg2.flags & Ci.nsMsgMessageFlags.Offline, 0);
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg3 = db.getMsgHdrForMessageID(gMsgId3);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ // We use the streamListener as a display consumer.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ gIMAPService.loadMessage(
+ IMAPPump.inbox.getUriForMsg(msg3),
+ streamListener,
+ null,
+ listener,
+ false
+ );
+ await listener.promise;
+});
+
+add_task(
+ {
+ // Can't turn this on because our fake server doesn't support body structure.
+ skip_if: () => true,
+ },
+ function verify3rdMsg() {
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg3 = db.getMsgHdrForMessageID(gMsgId3);
+ Assert.equal(msg3.flags & Ci.nsMsgMessageFlags.Offline, 0);
+ }
+);
+
+add_task(async function addNewMsgs() {
+ let mbox = IMAPPump.daemon.getMailbox("INBOX");
+ // Make a couple of messages.
+ let messages = [];
+ let bodyString = "";
+ for (let i = 0; i < 100; i++) {
+ bodyString +=
+ "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\r\n";
+ }
+
+ gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(
+ gMessageGenerator.makeMessage({
+ body: { body: bodyString, contentType: "text/plain" },
+ })
+ );
+
+ gFirstNewMsg = mbox.uidnext;
+ // Need to account for x-mozilla-status, status2, and envelope.
+ gFirstMsgSize = messages[0].toMessageString().length + 102;
+
+ messages.forEach(function (message) {
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message.toMessageString())
+ );
+ mbox.addMessage(new ImapMessage(dataUri.spec, mbox.uidnext++, []));
+ });
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+add_task(async function test_queuedOfflineDownload() {
+ // Make sure that streaming the same message and then trying to download
+ // it for offline use doesn't end up in it getting added to the offline
+ // store twice.
+ gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize + gFirstMsgSize;
+ let newMsgHdr = IMAPPump.inbox.GetMessageHeader(gFirstNewMsg);
+ let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamMessage(msgURI, listener, null, null, false, "", false);
+ await listener.promise;
+});
+add_task(async function firstStreamFinished() {
+ // nsIMsgFolder.downloadMessagesForOffline does not take a listener, so
+ // we invoke nsIImapService.downloadMessagesForOffline directly
+ // with a listener.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.imap.downloadMessagesForOffline(
+ gFirstNewMsg,
+ IMAPPump.inbox,
+ listener,
+ null
+ );
+ await listener.promise;
+});
+add_task(function checkOfflineStoreSize() {
+ Assert.ok(IMAPPump.inbox.filePath.fileSize <= gImapInboxOfflineStoreSize);
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapUndo.js b/comm/mailnews/imap/test/unit/test_imapUndo.js
new file mode 100644
index 0000000000..8db0ad6903
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapUndo.js
@@ -0,0 +1,160 @@
+/* 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/. */
+
+// This file tests undoing of an imap delete to the trash.
+// There are three main cases:
+// 1. Normal undo
+// 2. Undo after the source folder has been compacted.
+// 2.1 Same, but the server doesn't support COPYUID (GMail case)
+//
+// Original Author: David Bienvenu <bienvenu@nventure.com>
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gRootFolder;
+var gMessages = [];
+var gMsgWindow;
+
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgFile2 = do_get_file("../../../data/bugmail11");
+// var gMsgFile3 = do_get_file("../../../data/draft1");
+var gMsgFile4 = do_get_file("../../../data/bugmail7");
+var gMsgFile5 = do_get_file("../../../data/bugmail6");
+
+// Copied straight from the example files
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+// var gMsgId3 = "4849BF7B.2030800@example.com";
+var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org";
+var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org";
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox) {
+ // For every message we have, we need to convert it to a file:/// URI
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ });
+}
+
+function alertListener() {}
+
+alertListener.prototype = {
+ reset() {},
+
+ onAlert(aMessage, aMsgWindow) {
+ throw new Error("got alert - TEST FAILED " + aMessage);
+ },
+};
+
+add_setup(function () {
+ setupIMAPPump();
+
+ var listener1 = new alertListener();
+
+ MailServices.mailSession.addUserFeedbackListener(listener1);
+
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ Services.prefs.setBoolPref("mail.server.server1.offline_download", false);
+
+ gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+ );
+
+ gRootFolder = IMAPPump.incomingServer.rootFolder;
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+
+ // Add a couple of messages to the INBOX
+ // this is synchronous, afaik
+ addMessagesToServer(
+ [
+ { file: gMsgFile1, messageId: gMsgId1 },
+ { file: gMsgFile4, messageId: gMsgId4 },
+ { file: gMsgFile5, messageId: gMsgId5 },
+ { file: gMsgFile2, messageId: gMsgId2 },
+ ],
+ IMAPPump.mailbox
+ );
+});
+
+add_task(async function updateFolder() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function deleteMessage() {
+ let msgToDelete = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ gMessages.push(msgToDelete);
+ IMAPPump.inbox.deleteMessages(
+ gMessages,
+ gMsgWindow,
+ false,
+ true,
+ copyListener,
+ true
+ );
+ await copyListener.promise;
+});
+
+add_task(async function expunge() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.expunge(listener, gMsgWindow);
+ await listener.promise;
+
+ // Ensure that the message has been surely deleted.
+ Assert.equal(IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages, 3);
+});
+
+add_task(async function undoDelete() {
+ gMsgWindow.transactionManager.undoTransaction();
+ // after undo, we select the trash and then the inbox, so that we sync
+ // up with the server, and clear out the effects of having done the
+ // delete offline.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ let trash = gRootFolder.getChildNamed("Trash");
+ trash
+ .QueryInterface(Ci.nsIMsgImapMailFolder)
+ .updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function goBackToInbox() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(gMsgWindow, listener);
+ await listener.promise;
+});
+
+add_task(function verifyFolders() {
+ let msgRestored = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ Assert.ok(msgRestored !== null);
+ Assert.equal(IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages, 4);
+});
+
+add_task(function endTest() {
+ // Cleanup, null out everything, close all cached connections and stop the
+ // server
+ gMessages = [];
+ gMsgWindow.closeWindow();
+ gMsgWindow = null;
+ gRootFolder = null;
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_imapUrls.js b/comm/mailnews/imap/test/unit/test_imapUrls.js
new file mode 100644
index 0000000000..e310705169
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_imapUrls.js
@@ -0,0 +1,31 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// IMAP pump
+
+setupIMAPPump();
+
+/*
+ * Test parsing of imap uri's with very large UID's.
+ */
+
+function run_test() {
+ let imapS = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=imap"
+ ].getService(Ci.nsIMsgMessageService);
+ let uri = imapS.getUrlForUri(
+ "imap-message://user@localhost/INBOX#4294967168"
+ );
+ Assert.equal(
+ uri.spec,
+ "imap://user@localhost:" +
+ IMAPPump.server.port +
+ "/fetch%3EUID%3E%5EINBOX%3E4294967168"
+ );
+ teardownIMAPPump();
+}
diff --git a/comm/mailnews/imap/test/unit/test_largeOfflineStore.js b/comm/mailnews/imap/test/unit/test_largeOfflineStore.js
new file mode 100644
index 0000000000..6e0d2e9dd1
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_largeOfflineStore.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+/*
+ * Test to ensure that downloadAllForOffline works correctly for large imap
+ * stores, i.e., over 4 GiB.
+ */
+
+var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+var gOfflineStoreSize;
+
+add_setup(async function () {
+ setupIMAPPump();
+
+ // Figure out the name of the IMAP inbox
+ let inboxFile = IMAPPump.incomingServer.rootMsgFolder.filePath;
+ inboxFile.append("INBOX");
+ if (!inboxFile.exists()) {
+ inboxFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8));
+ }
+
+ let neededFreeSpace = 0x200000000;
+ // On Windows, check whether the drive is NTFS. If it is, mark the file as
+ // sparse. If it isn't, then bail out now, because in all probability it is
+ // FAT32, which doesn't support file sizes greater than 4 GB.
+ if (
+ "@mozilla.org/windows-registry-key;1" in Cc &&
+ mailTestUtils.get_file_system(inboxFile) != "NTFS"
+ ) {
+ throw new Error("On Windows, this test only works on NTFS volumes.\n");
+ }
+
+ let isFileSparse = mailTestUtils.mark_file_region_sparse(
+ inboxFile,
+ 0,
+ 0x10000000f
+ );
+ let freeDiskSpace = inboxFile.diskSpaceAvailable;
+ Assert.ok(
+ isFileSparse && freeDiskSpace > neededFreeSpace,
+ "This test needs " +
+ mailTestUtils.toMiBString(neededFreeSpace) +
+ " free space to run."
+ );
+});
+
+add_task(async function addOfflineMessages() {
+ // Create a couple test messages on the IMAP server.
+ let messages = [];
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+ messages = messages.concat(scenarioFactory.directReply(2));
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(imapMsg);
+
+ dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[1].toMessageString())
+ );
+ imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(imapMsg);
+
+ // Extend local IMAP inbox to over 4 GiB.
+ let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream)
+ .QueryInterface(Ci.nsISeekableStream);
+ // Open in write-only mode, no truncate.
+ outputStream.init(IMAPPump.inbox.filePath, 0x02, -1, 0);
+ // seek to 15 bytes past 4GB.
+ outputStream.seek(0, 0x10000000f);
+ // Write an empty "from" line.
+ outputStream.write("from\r\n", 6);
+ outputStream.close();
+
+ // Save initial file size.
+ gOfflineStoreSize = IMAPPump.inbox.filePath.fileSize;
+ dump(
+ "Offline store size (before 1st downloadAllForOffline()) = " +
+ gOfflineStoreSize +
+ "\n"
+ );
+
+ // Download for offline use, to append created messages to local IMAP inbox.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(async function check_result() {
+ // Call downloadAllForOffline() a second time.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+
+ // Make sure offline store grew (i.e., we were not writing over data).
+ let offlineStoreSize = IMAPPump.inbox.filePath.fileSize;
+ dump(
+ "Offline store size (after 2nd downloadAllForOffline()): " +
+ offlineStoreSize +
+ " Msg hdr offsets should be close to it.\n"
+ );
+ Assert.ok(offlineStoreSize > gOfflineStoreSize);
+
+ // Verify that the message headers have the offline flag set.
+ for (let header of IMAPPump.inbox.msgDatabase.enumerateMessages()) {
+ // Verify that each message has been downloaded and looks OK.
+ Assert.ok(
+ header instanceof Ci.nsIMsgDBHdr &&
+ header.flags & Ci.nsMsgMessageFlags.Offline,
+ "Message downloaded for offline use"
+ );
+
+ // Make sure we don't fall over if we ask to read the message.
+ IMAPPump.inbox.getLocalMsgStream(header).close();
+ }
+});
+
+add_task(function teardown() {
+ // Free up disk space - if you want to look at the file after running
+ // this test, comment out this line.
+ if (IMAPPump.inbox) {
+ IMAPPump.inbox.filePath.remove(false);
+ }
+
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_listClosesDB.js b/comm/mailnews/imap/test/unit/test_listClosesDB.js
new file mode 100644
index 0000000000..63ab5b70d4
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_listClosesDB.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+// This file tests that listing folders on startup because we're not using
+// subscription doesn't leave db's open.
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gSub3;
+
+add_setup(async function () {
+ setupIMAPPump();
+
+ IMAPPump.daemon.createMailbox("folder1", { subscribed: true });
+ IMAPPump.daemon.createMailbox("folder1/sub1", "", { subscribed: true });
+ IMAPPump.daemon.createMailbox("folder1/sub1/sub2", "", { subscribed: true });
+ IMAPPump.daemon.createMailbox("folder1/sub1/sub2/sub3", "", {
+ subscribed: true,
+ });
+
+ IMAPPump.incomingServer.usingSubscription = false;
+
+ let rootFolder = IMAPPump.incomingServer.rootFolder.QueryInterface(
+ Ci.nsIMsgImapMailFolder
+ );
+ rootFolder.hierarchyDelimiter = "/";
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ let folder1 = rootFolder.addSubfolder("folder1");
+ let sub1 = folder1.addSubfolder("sub1");
+ let sub2 = sub1.addSubfolder("sub2");
+ gSub3 = sub2.addSubfolder("sub3");
+ IMAPPump.server.performTest("LIST");
+
+ await PromiseTestUtils.promiseDelay(1000);
+});
+
+add_task(async function updateInbox() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function checkCachedDBForFolder() {
+ const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+ Assert.equal(gDbService.cachedDBForFolder(gSub3), null);
+});
+
+add_task(function teardown() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_listSubscribed.js b/comm/mailnews/imap/test/unit/test_listSubscribed.js
new file mode 100644
index 0000000000..0d536dd45c
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_listSubscribed.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+/** Test that listing subscribed mailboxes uses LIST (SUBSCRIBED) instead of LSUB
+ * for servers that have LIST-EXTENDED capability
+ */
+/* References:
+ * RFC 5258 - http://tools.ietf.org/html/rfc5258
+ * Bug 495318
+ * Bug 816028
+ * http://bugzilla.zimbra.com/show_bug.cgi?id=78794
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_setup(async function () {
+ // Zimbra is one of the servers that supports LIST-EXTENDED
+ // it also has a bug that causes a server crash in certain setups
+ setupIMAPPump("Zimbra");
+
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+
+ // Setup the mailboxes that will be used for this test.
+ IMAPPump.mailbox.subscribed = true;
+ IMAPPump.daemon.createMailbox("folder1", {
+ subscribed: true,
+ flags: ["\\Noselect"],
+ });
+ IMAPPump.daemon.createMailbox("folder1/folder11", {
+ subscribed: true,
+ flags: ["\\Noinferiors"],
+ });
+ IMAPPump.daemon.createMailbox("folder2", {
+ subscribed: true,
+ nonExistent: true,
+ });
+ IMAPPump.daemon.createMailbox("folder3", {});
+
+ // select the inbox to force folder discovery, etc.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// tests that LIST (SUBSCRIBED) returns the proper response
+add_task(async function testListSubscribed() {
+ // check that we have \Noselect and \Noinferiors flags - these would not have
+ // been returned if we had used LSUB instead of LIST(SUBSCRIBED)
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ let folder1 = rootFolder.getChildNamed("folder1");
+ Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+ Assert.ok(!folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors));
+
+ // make sure the above test was not a fluke
+ let folder11 = folder1.getChildNamed("folder11");
+ Assert.ok(!folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+ Assert.ok(folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors));
+
+ // test that \NonExistent implies \Noselect
+ rootFolder.getChildNamed("folder2");
+ Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+
+ // should not get a folder3 since it is not subscribed
+ let folder3;
+ try {
+ folder3 = rootFolder.getChildNamed("folder3");
+ } catch (ex) {}
+ // do_check_false(folder1.getFlag(Ci.nsMsgFolderFlags.Subscribed));
+ Assert.equal(null, folder3);
+});
+
+add_task(async function testZimbraServerVersions() {
+ if (Services.prefs.getBoolPref("mailnews.imap.jsmodule", false)) {
+ return;
+ }
+
+ // older versions of Zimbra can crash if we send LIST (SUBSCRIBED) so we want
+ // to make sure that we are checking for versions
+
+ let testValues = [
+ { version: "6.3.1_GA_2790", expectedResult: false },
+ { version: "7.2.2_GA_2790", expectedResult: false },
+ { version: "7.2.3_GA_2790", expectedResult: true },
+ { version: "8.0.2_GA_2790", expectedResult: false },
+ { version: "8.0.3_GA_2790", expectedResult: true },
+ { version: "9.0.0_GA_2790", expectedResult: true },
+ ];
+
+ for (let i = 0; i < testValues.length; i++) {
+ IMAPPump.daemon.idResponse =
+ '("NAME" "Zimbra" ' +
+ '"VERSION" "' +
+ testValues[i].version +
+ '" ' +
+ '"RELEASE" "20120815212257" ' +
+ '"USER" "user@domain.com" ' +
+ '"SERVER" "14b63305-d002-4f1b-bcd9-23d402d4ef40")';
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.incomingServer.performExpand(null);
+ // select inbox is just to wait on performExpand since performExpand does not have listener
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+ // if we send LSUB instead of LIST(SUBSCRIBED), then we should not have \NoSelect flag
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ let folder1 = rootFolder.getChildNamed("folder1");
+ Assert.equal(
+ folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect),
+ testValues[i].expectedResult
+ );
+ }
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_localToImapFilter.js b/comm/mailnews/imap/test/unit/test_localToImapFilter.js
new file mode 100644
index 0000000000..b0a28aedda
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_localToImapFilter.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+/*
+ * This file tests copies of multiple messages using filters
+ * from incoming POP3, with filter actions copying and moving
+ * messages to IMAP folders. This test is adapted from
+ * test_imapFolderCopy.js
+ *
+ * Original author: Kent James <kent@caspia.com>
+ */
+
+/**
+ * NOTE:
+ * There's a problem with this test in chaos mode (mach xpcshell-test --verify)
+ * with the filter applying.
+ * It's either a problem with the POP3Pump implementation (testing infrastructure failure)
+ * or a problem with the copy filter.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gEmptyLocal1, gEmptyLocal2;
+var gFiles = ["../../../data/bugmail1", "../../../data/draft1"];
+
+add_setup(async function () {
+ setupIMAPPump();
+ let emptyFolder1Listener = PromiseTestUtils.promiseFolderAdded("empty 1");
+ gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1");
+ await emptyFolder1Listener;
+ let emptyFolder2Listener = PromiseTestUtils.promiseFolderAdded("empty 2");
+ gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2");
+ await emptyFolder2Listener;
+
+ // These hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+});
+
+add_task(async function copyFolder1() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gEmptyLocal1,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function updateTrash() {
+ let trashFolder = IMAPPump.incomingServer.rootFolder
+ .getChildNamed("Trash")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ // hack to force uid validity to get initialized for trash.
+ trashFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function copyFolder2() {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFolder(
+ gEmptyLocal2,
+ IMAPPump.inbox,
+ false,
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+add_task(async function getLocalMessages() {
+ // setup copy then move mail filters on the inbox
+ let filterList = gPOP3Pump.fakeServer.getFilterList(null);
+ let filter = filterList.createFilter("copyThenMoveAll");
+ let searchTerm = filter.createTerm();
+ searchTerm.matchAll = true;
+ filter.appendTerm(searchTerm);
+ let copyAction = filter.createAction();
+ copyAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ copyAction.targetFolderUri = IMAPPump.inbox.getChildNamed("empty 1").URI;
+ filter.appendAction(copyAction);
+ let moveAction = filter.createAction();
+ moveAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ moveAction.targetFolderUri = IMAPPump.inbox.getChildNamed("empty 2").URI;
+ filter.appendAction(moveAction);
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+ let resolveOnDone;
+ let promiseOnDone = new Promise(resolve => {
+ resolveOnDone = resolve;
+ });
+ gPOP3Pump.files = gFiles;
+ gPOP3Pump.onDone = resolveOnDone;
+ gPOP3Pump.run();
+
+ await promiseOnDone;
+});
+
+add_task(async function test_update1_copyFilter() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ let folder1 = IMAPPump.inbox
+ .getChildNamed("empty 1")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ folder1.updateFolderWithListener(null, listener);
+ await listener.promise;
+ Assert.ok(folder1 !== null);
+ Assert.equal(
+ folderCount(folder1),
+ 2,
+ "the two filtered messages should be in empty 1"
+ );
+});
+
+add_task(async function test_update2_moveFilter() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ let folder2 = IMAPPump.inbox
+ .getChildNamed("empty 2")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ folder2.updateFolderWithListener(null, listener);
+ await listener.promise;
+ Assert.ok(folder2 !== null);
+ Assert.equal(
+ folderCount(folder2),
+ 2,
+ "the two filtered messages should be in empty 2"
+ );
+});
+
+add_task(async function verifyLocalFolder() {
+ // the local inbox folder should now be empty, since the second
+ // operation was a move
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+});
+
+add_task(function endTest() {
+ gEmptyLocal1 = null;
+ gEmptyLocal2 = null;
+ gPOP3Pump = null;
+ teardownIMAPPump();
+});
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
diff --git a/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js b/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js
new file mode 100644
index 0000000000..130a03e403
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+/*
+ * This file tests copies of multiple messages using filters
+ * from incoming POP3, with filter actions copying and moving
+ * messages to an IMAP folder, when the POP3 message uses
+ * quarantining to help antivirus software. See bug 387361.
+ *
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var gSubfolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ // quarantine messages
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", true);
+});
+
+add_task(async function createSubfolder() {
+ let folderAddedListener = PromiseTestUtils.promiseFolderAdded("subfolder");
+ IMAPPump.incomingServer.rootFolder.createSubfolder("subfolder", null);
+ await folderAddedListener;
+ gSubfolder = IMAPPump.incomingServer.rootFolder.getChildNamed("subfolder");
+ Assert.ok(gSubfolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSubfolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function getLocalMessages() {
+ // setup copy then move mail filters on the inbox
+ let filterList = gPOP3Pump.fakeServer.getFilterList(null);
+ let filter = filterList.createFilter("copyThenMoveAll");
+ let searchTerm = filter.createTerm();
+ searchTerm.matchAll = true;
+ filter.appendTerm(searchTerm);
+ let copyAction = filter.createAction();
+ copyAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ copyAction.targetFolderUri = gSubfolder.URI;
+ filter.appendAction(copyAction);
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+
+ let resolveDone;
+ let promise = new Promise(resolve => {
+ resolveDone = resolve;
+ });
+ gPOP3Pump.files = ["../../../data/bugmail1"];
+ gPOP3Pump.onDone = resolveDone;
+ gPOP3Pump.run();
+ await promise;
+});
+
+add_task(async function updateSubfolderAndTest() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ let folderLoaded = PromiseTestUtils.promiseFolderEvent(
+ gSubfolder,
+ "FolderLoaded"
+ );
+ gSubfolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+ await folderLoaded;
+
+ Assert.equal(folderCount(gSubfolder), 1);
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 1);
+});
+
+add_task(async function get2Messages() {
+ let resolveDone;
+ let promise = new Promise(resolve => {
+ resolveDone = resolve;
+ });
+ gPOP3Pump.files = ["../../../data/bugmail10", "../../../data/draft1"];
+ gPOP3Pump.onDone = resolveDone;
+ gPOP3Pump.run();
+ await promise;
+});
+
+add_task(async function updateSubfolderAndTest2() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ let folderLoaded = PromiseTestUtils.promiseFolderEvent(
+ gSubfolder,
+ "FolderLoaded"
+ );
+ gSubfolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+ await folderLoaded;
+ Assert.equal(folderCount(gSubfolder), 3);
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 3);
+});
+
+add_task(function endTest() {
+ // Cleanup, null out everything, close all cached connections and stop the
+ // server
+ gPOP3Pump = null;
+ teardownIMAPPump();
+});
+
+// helper functions
+
+// count of messages in a folder, using the database
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
diff --git a/comm/mailnews/imap/test/unit/test_lsub.js b/comm/mailnews/imap/test/unit/test_lsub.js
new file mode 100644
index 0000000000..1d7f479a48
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_lsub.js
@@ -0,0 +1,76 @@
+/* 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/. */
+
+// Test that listing subscribed mailboxes uses LIST (SUBSCRIBED) instead of LSUB
+// for servers that have LIST-EXTENDED capability
+// see: bug 495318
+// see: RFC 5258 - http://tools.ietf.org/html/rfc5258
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_setup(function () {
+ setupIMAPPump();
+
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// Setup the mailboxes that will be used for this test.
+add_setup(async function () {
+ IMAPPump.mailbox.subscribed = true;
+ IMAPPump.daemon.createMailbox("folder1", {
+ subscribed: true,
+ flags: ["\\Noselect"],
+ });
+ IMAPPump.daemon.createMailbox("folder1/folder11", {
+ subscribed: true,
+ flags: ["\\Noinferiors"],
+ });
+ IMAPPump.daemon.createMailbox("folder2", {
+ subscribed: true,
+ nonExistent: true,
+ });
+ IMAPPump.daemon.createMailbox("folder3", {});
+
+ // select the inbox to force folder discovery, etc.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// Tests that LSUB returns the proper response.
+add_task(function testLsub() {
+ // Check that we have \Noselect and \Noinferiors flags - these would not have
+ // been returned if we had used LSUB instead of LIST(SUBSCRIBED).
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ let folder1 = rootFolder.getChildNamed("folder1");
+ Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+ Assert.ok(!folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors));
+
+ // Make sure the above test was not a fluke.
+ let folder11 = folder1.getChildNamed("folder11");
+ Assert.ok(!folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+ Assert.ok(folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors));
+
+ // Test that \NonExistent implies \Noselect.
+ rootFolder.getChildNamed("folder2");
+ Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect));
+
+ // Should not get a folder3 since it is not subscribed.
+ let folder3;
+ try {
+ folder3 = rootFolder.getChildNamed("folder3");
+ } catch (ex) {}
+ Assert.equal(false, folder1.getFlag(Ci.nsMsgFolderFlags.Subscribed));
+ Assert.equal(null, folder3);
+});
+
+// Cleanup at end.
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_mailboxes.js b/comm/mailnews/imap/test/unit/test_mailboxes.js
new file mode 100644
index 0000000000..0f13f6b328
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_mailboxes.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/**
+ * Tests basic mailbox handling of IMAP, like discovery, rename and empty folder.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// The following folder names are not pure ASCII and will be MUTF-7 encoded.
+const folderName1 = "I18N box\u00E1"; // I18N boxá
+const folderName2 = "test \u00E4"; // test ä
+
+add_setup(async function () {
+ setupIMAPPump();
+
+ IMAPPump.daemon.createMailbox(folderName1, { subscribed: true });
+ IMAPPump.daemon.createMailbox("Unsubscribed box");
+ // Create an all upper case trash folder name to make sure
+ // we handle special folder names case-insensitively.
+ IMAPPump.daemon.createMailbox("TRASH", { subscribed: true });
+
+ // Get the server list...
+ IMAPPump.server.performTest("LIST");
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function checkDiscovery() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ // Check that we've subscribed to the boxes returned by LSUB. We also get
+ // checking of proper i18n in mailboxes for free here.
+ Assert.ok(rootFolder.containsChildNamed("Inbox"));
+ Assert.ok(rootFolder.containsChildNamed("TRASH"));
+ // Make sure we haven't created an extra "Trash" folder.
+ let trashes = rootFolder.getFoldersWithFlags(Ci.nsMsgFolderFlags.Trash);
+ Assert.equal(trashes.length, 1);
+ Assert.equal(rootFolder.numSubFolders, 3);
+ Assert.ok(rootFolder.containsChildNamed(folderName1));
+ // This is not a subscribed box, so we shouldn't be subscribing to it.
+ Assert.ok(!rootFolder.containsChildNamed("Unsubscribed box"));
+
+ let i18nChild = rootFolder.getChildNamed(folderName1);
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.imap.renameLeaf(i18nChild, folderName2, listener, null);
+ await listener.promise;
+});
+
+add_task(async function checkRename() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ Assert.ok(rootFolder.containsChildNamed(folderName2));
+ let newChild = rootFolder
+ .getChildNamed(folderName2)
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ newChild.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function checkEmptyFolder() {
+ try {
+ let serverSink = IMAPPump.server.QueryInterface(Ci.nsIImapServerSink);
+ serverSink.possibleImapMailbox("/", "/", 0);
+ } catch (ex) {
+ // We expect this to fail, but not crash or assert.
+ }
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js b/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
new file mode 100644
index 0000000000..7b733ab9e4
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
@@ -0,0 +1,363 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for nsIMsgFolderListener events due to IMAP operations
+ *
+ * Currently tested
+ * - Adding new folders
+ * - Copying messages from files to mailboxes
+ * - Adding new messages directly to mailboxes
+ *
+ * NOTE (See Bug 1632022):
+ * Running this test by itself...
+ *
+ * $ ./mach xpcshell-test comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
+ * ...will fail.
+ *
+ * This is because all the IMAP tests run twice - once with mbox storage and
+ * once with maildir storage. For this test, the two parallel instances
+ * interact badly.
+ *
+ */
+
+/* import-globals-from ../../../test/resources/msgFolderListenerSetup.js */
+load("../../../resources/msgFolderListenerSetup.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// Globals
+var gRootFolder;
+var gIMAPInbox, gIMAPFolder2, gIMAPFolder3;
+var gIMAPDaemon, gServer, gIMAPIncomingServer;
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgFile2 = do_get_file("../../../data/bugmail11");
+var gMsgFile3 = do_get_file("../../../data/draft1");
+var gMsgFile4 = do_get_file("../../../data/bugmail7");
+var gMsgFile5 = do_get_file("../../../data/bugmail6");
+
+// Copied straight from the example files
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+var gMsgId3 = "4849BF7B.2030800@example.com";
+var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org";
+var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org";
+var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+function addFolder(parent, folderName, storeIn) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderAdded, parent, folderName, storeIn],
+ ];
+ // No copy listener notification for this
+ gCurrStatus |= kStatus.onStopCopyDone;
+ parent.createSubfolder(folderName, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ gServer.performTest("LIST");
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function copyFileMessage(file, messageId, destFolder) {
+ copyListener.mFolderStoredIn = destFolder;
+
+ // This *needs* to be a draft (fourth parameter), as for non-UIDPLUS servers,
+ // nsImapProtocol::UploadMessageFromFile is hardcoded not to send a copy
+ // listener notification. The same function also asks for the message id from
+ // the copy listener, without which it will *not* send the notification.
+
+ // ...but wait, nsImapProtocol.cpp requires SEARCH afterwards to retrieve the
+ // message header, and fakeserver doesn't implement it yet. So get it to fail
+ // earlier by *not* sending the message id.
+ // copyListener.mMessageId = messageId;
+
+ // Instead store the message id in gExpectedEvents, so we can match that up
+ gExpectedEvents = [
+ [MailServices.mfn.msgAdded, { expectedMessageId: messageId }],
+ [MailServices.mfn.msgsClassified, [messageId], false, false],
+ ];
+ destFolder.updateFolder(null);
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ true,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ gServer.performTest("APPEND");
+ // Allow some time for the append operation to complete, so update folder
+ // every second
+ gFolderBeingUpdated = destFolder;
+ doUpdateFolder(gTest);
+}
+
+var gFolderBeingUpdated = null;
+function doUpdateFolder(test) {
+ // In case we've moved on to the next test, exit
+ if (gTest > test) {
+ return;
+ }
+
+ gFolderBeingUpdated.updateFolder(null);
+
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ } else {
+ do_timeout(1000, function () {
+ doUpdateFolder(test);
+ });
+ }
+}
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox, localFolder) {
+ // For every message we have, we need to convert it to a file:/// URI
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ // We can't get the headers again, so just pass on the message id
+ gExpectedEvents.push([
+ MailServices.mfn.msgAdded,
+ { expectedMessageId: message.messageId },
+ ]);
+ });
+ gExpectedEvents.push([
+ MailServices.mfn.msgsClassified,
+ messages.map(hdr => hdr.messageId),
+ false,
+ false,
+ ]);
+
+ // No copy listener notification for this
+ gCurrStatus |= kStatus.functionCallDone | kStatus.onStopCopyDone;
+
+ gFolderBeingUpdated = localFolder;
+ doUpdateFolder(gTest);
+}
+
+function copyMessages(messages, isMove, srcFolder, destFolder) {
+ gExpectedEvents = [
+ [
+ MailServices.mfn.msgsMoveCopyCompleted,
+ isMove,
+ messages,
+ destFolder,
+ true,
+ ],
+ ];
+ // We'll also get the msgAdded events when we go and update the destination
+ // folder
+ messages.forEach(function (message) {
+ // We can't use the headers directly, because the notifications we'll
+ // receive are for message headers in the destination folder
+ gExpectedEvents.push([
+ MailServices.mfn.msgKeyChanged,
+ { expectedMessageId: message.messageId },
+ ]);
+ gExpectedEvents.push([
+ MailServices.mfn.msgAdded,
+ { expectedMessageId: message.messageId },
+ ]);
+ });
+ gExpectedEvents.push([
+ MailServices.mfn.msgsClassified,
+ messages.map(hdr => hdr.messageId),
+ false,
+ false,
+ ]);
+ MailServices.copy.copyMessages(
+ srcFolder,
+ messages,
+ destFolder,
+ isMove,
+ copyListener,
+ gMsgWindow,
+ true
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+
+ gServer.performTest("COPY");
+
+ gFolderBeingUpdated = destFolder;
+ doUpdateFolder(gTest);
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+var gTestArray = [
+ // Adding folders
+ // Create another folder to move and copy messages around, and force initialization.
+ function testAddFolder1() {
+ addFolder(gRootFolder, "folder2", function (folder) {
+ gIMAPFolder2 = folder;
+ });
+ },
+ function testAddFolder2() {
+ addFolder(gRootFolder, "folder3", function (folder) {
+ gIMAPFolder3 = folder;
+ });
+ },
+
+ // Adding messages to folders
+ function testCopyFileMessage1() {
+ // Make sure the offline flag is not set for any of the folders
+ [gIMAPInbox, gIMAPFolder2, gIMAPFolder3].forEach(function (folder) {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ });
+ copyFileMessage(gMsgFile1, gMsgId1, gIMAPInbox);
+ },
+ function testCopyFileMessage2() {
+ copyFileMessage(gMsgFile2, gMsgId2, gIMAPInbox);
+ },
+
+ // Add message straight to the server, so that we get a message added
+ // notification on the next folder update
+ function testNewMessageArrival1() {
+ addMessagesToServer(
+ [{ file: gMsgFile3, messageId: gMsgId3 }],
+ gIMAPDaemon.getMailbox("INBOX"),
+ gIMAPInbox
+ );
+ },
+
+ // Add another couple of messages, this time to another folder on the server
+ function testNewMessageArrival2() {
+ addMessagesToServer(
+ [
+ { file: gMsgFile4, messageId: gMsgId4 },
+ { file: gMsgFile5, messageId: gMsgId5 },
+ ],
+ gIMAPDaemon.getMailbox("INBOX"),
+ gIMAPInbox
+ );
+ },
+
+ // Moving/copying messages (this doesn't work right now)
+ function testCopyMessages1() {
+ copyMessages(
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ false,
+ gIMAPInbox,
+ gIMAPFolder3
+ );
+ },
+];
+
+function run_test() {
+ // This is before any of the actual tests, so...
+ gTest = 0;
+
+ gIMAPDaemon = new ImapDaemon();
+ gServer = makeServer(gIMAPDaemon, "");
+
+ gIMAPIncomingServer = createLocalIMAPServer(gServer.port);
+
+ // Also make sure a local folders server is created, as that's what is used
+ // for sent items
+ localAccountUtils.loadLocalMailAccount();
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+ localAccount.incomingServer = localAccountUtils.incomingServer;
+
+ // Let's also have another account, using the same identity
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = gIMAPIncomingServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1);
+ // Make sure no biff notifications happen
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);
+
+ // Add a listener so that we can check all folder events from this point.
+ MailServices.mfn.addListener(gMFListener, allTestedEvents);
+
+ // Get the server list...
+ gIMAPIncomingServer.performExpand(null);
+
+ // We get these notifications on initial discovery
+ gRootFolder = gIMAPIncomingServer.rootFolder;
+ gIMAPInbox = gRootFolder.getChildNamed("Inbox");
+ gExpectedEvents = [
+ [MailServices.mfn.folderAdded, gRootFolder, "Trash", function (folder) {}],
+ ];
+ gCurrStatus |= kStatus.onStopCopyDone | kStatus.functionCallDone;
+
+ gServer.performTest("SUBSCRIBE");
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of
+ // all the operations.
+ do_test_pending();
+}
+
+function doTest(test) {
+ // eslint-disable-line no-unused-vars
+ if (test <= gTestArray.length) {
+ let testFn = gTestArray[test - 1];
+
+ dump(`Doing test ${test} (${testFn.name})\n`);
+
+ // Set a limit of ten seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gTest == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " +
+ testFn.name +
+ ", current status is " +
+ gCurrStatus
+ );
+ }
+ });
+ testFn();
+ } else {
+ MailServices.mfn.removeListener(gMFListener);
+ // Cleanup, null out everything, close all cached connections and stop the
+ // server
+ gRootFolder = null;
+ gIMAPInbox.msgDatabase = null;
+ gIMAPInbox = null;
+ gIMAPFolder2 = null;
+ gIMAPFolder3 = null;
+ do_timeout(1000, endTest);
+ }
+}
+
+function endTest() {
+ gIMAPIncomingServer.closeCachedConnections();
+ gServer.performTest();
+ gServer.stop();
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished(); // for the one in run_test()
+}
diff --git a/comm/mailnews/imap/test/unit/test_offlineCopy.js b/comm/mailnews/imap/test/unit/test_offlineCopy.js
new file mode 100644
index 0000000000..0bbf80e352
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_offlineCopy.js
@@ -0,0 +1,271 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This test checks pseudo-offline message copies (which is triggered
+ * by allowUndo == true in copyMessages).
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+
+var gMsgFile1 = do_get_file("../../../data/bugmail10");
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgFile2 = do_get_file("../../../data/image-attach-test");
+var gMsgId2 = "4A947F73.5030709@example.com";
+var gMsgFile3 = do_get_file("../../../data/SpamAssassinYes");
+var gMsg3Id = "bugmail7.m47LtAEf007543@mrapp51.mozilla.org";
+var gMsgFile4 = do_get_file("../../../data/bug460636");
+var gMsg4Id = "foo.12345@example";
+
+var gFolder1;
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox) {
+ // For every message we have, we need to convert it to a file:/// URI
+ messages.forEach(function (message) {
+ let URI = Services.io
+ .newFileURI(message.file)
+ .QueryInterface(Ci.nsIFileURL);
+ // Create the ImapMessage and store it on the mailbox.
+ mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, []));
+ });
+}
+
+var tests = [
+ async function setup() {
+ // Turn off autosync_offline_stores because
+ // fetching messages is invoked after copying the messages.
+ // (i.e. The fetching process will be invoked after OnStopCopy)
+ // It will cause crash with an assertion
+ // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown.
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+
+ setupIMAPPump();
+
+ let promiseFolderAdded = PromiseTestUtils.promiseFolderAdded("folder 1");
+ IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null);
+ await promiseFolderAdded;
+
+ gFolder1 = IMAPPump.incomingServer.rootFolder.getChildNamed("folder 1");
+ Assert.ok(gFolder1 instanceof Ci.nsIMsgFolder);
+
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+
+ // Add messages to the INBOX
+ // this is synchronous, afaik
+ addMessagesToServer(
+ [
+ { file: gMsgFile1, messageId: gMsgId1 },
+ { file: gMsgFile2, messageId: gMsgId2 },
+ ],
+ IMAPPump.daemon.getMailbox("INBOX")
+ );
+ },
+ async function updateFolder() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ },
+ async function downloadAllForOffline() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ },
+ async function copyMessagesToInbox() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ gMsgFile3,
+ IMAPPump.inbox,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+
+ promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ gMsgFile4,
+ IMAPPump.inbox,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+ await promiseCopyListener.promise;
+
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+
+ let db = IMAPPump.inbox.msgDatabase;
+
+ // test the headers in the inbox
+ let count = 0;
+ for (let message of db.enumerateMessages()) {
+ count++;
+ message instanceof Ci.nsIMsgDBHdr;
+ dump(
+ "message <" +
+ message.subject +
+ "> storeToken: <" +
+ message.getStringProperty("storeToken") +
+ "> offset: <" +
+ message.messageOffset +
+ "> id: <" +
+ message.messageId +
+ ">\n"
+ );
+ // This fails for file copies in bug 790912. Without this, messages that
+ // are copied are not visible in pre-pluggableStores versions of TB (pre TB 12)
+ if (IMAPPump.inbox.msgStore.storeType == "mbox") {
+ Assert.equal(
+ message.messageOffset,
+ parseInt(message.getStringProperty("storeToken"))
+ );
+ }
+ }
+ Assert.equal(count, 4);
+ },
+ function copyMessagesToSubfolder() {
+ // a message created from IMAP download
+ let db = IMAPPump.inbox.msgDatabase;
+ let msg1 = db.getMsgHdrForMessageID(gMsgId1);
+ // this is sync, I believe?
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msg1],
+ gFolder1,
+ false,
+ null,
+ null,
+ true
+ );
+
+ // two messages originally created from file copies (like in Send)
+ let msg3 = db.getMsgHdrForMessageID(gMsg3Id);
+ Assert.ok(msg3 instanceof Ci.nsIMsgDBHdr);
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msg3],
+ gFolder1,
+ false,
+ null,
+ null,
+ true
+ );
+
+ let msg4 = db.getMsgHdrForMessageID(gMsg4Id);
+ Assert.ok(msg4 instanceof Ci.nsIMsgDBHdr);
+
+ // because bug 790912 created messages with correct storeToken but messageOffset=0,
+ // these messages may not copy correctly. Make sure that they do, as fixed in bug 790912
+ msg4.messageOffset = 0;
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msg4],
+ gFolder1,
+ false,
+ null,
+ null,
+ true
+ );
+
+ // test the db headers in folder1
+ db = gFolder1.msgDatabase;
+ let count = 0;
+ for (let message of db.enumerateMessages()) {
+ count++;
+ message instanceof Ci.nsIMsgDBHdr;
+ dump(
+ "message <" +
+ message.subject +
+ "> storeToken: <" +
+ message.getStringProperty("storeToken") +
+ "> offset: <" +
+ message.messageOffset +
+ "> id: <" +
+ message.messageId +
+ ">\n"
+ );
+ if (gFolder1.msgStore.storeType == "mbox") {
+ Assert.equal(
+ message.messageOffset,
+ parseInt(message.getStringProperty("storeToken"))
+ );
+ }
+ }
+ Assert.equal(count, 3);
+ },
+ async function test_headers() {
+ let msgIds = [gMsgId1, gMsg3Id, gMsg4Id];
+ for (let msgId of msgIds) {
+ let newMsgHdr = gFolder1.msgDatabase.getMsgHdrForMessageID(msgId);
+ Assert.ok(newMsgHdr.flags & Ci.nsMsgMessageFlags.Offline);
+ let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ let promiseStreamListener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamHeaders(msgURI, promiseStreamListener, null, true);
+ let data = await promiseStreamListener.promise;
+ dump("\nheaders for messageId " + msgId + "\n" + data + "\n\n");
+ Assert.ok(data.includes(msgId));
+ }
+ },
+ function moveMessagesToSubfolder() {
+ let db = IMAPPump.inbox.msgDatabase;
+ let messages = [...db.enumerateMessages()];
+ Assert.ok(messages.length > 0);
+ // this is sync, I believe?
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ messages,
+ gFolder1,
+ true,
+ null,
+ null,
+ true
+ );
+
+ // the inbox should now be empty
+ Assert.ok([...db.enumerateMessages()].length == 0);
+
+ // maildir should also delete the files.
+ if (IMAPPump.inbox.msgStore.storeType == "maildir") {
+ let curDir = IMAPPump.inbox.filePath.clone();
+ curDir.append("cur");
+ Assert.ok(curDir.exists());
+ Assert.ok(curDir.isDirectory());
+ let curEnum = curDir.directoryEntries;
+ // the directory should be empty, fails from bug 771643
+ Assert.ok(!curEnum.hasMoreElements());
+ }
+ },
+ teardownIMAPPump,
+];
+
+function run_test() {
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js b/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js
new file mode 100644
index 0000000000..d13e07a367
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js
@@ -0,0 +1,152 @@
+/* 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/. */
+
+/**
+ * This file tests that a message saved as draft in an IMAP folder in offline
+ * mode is not lost when going back online
+ * See Bug 805626
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+var gDraftsFolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+add_task(async function createDraftsFolder() {
+ IMAPPump.incomingServer.rootFolder.createSubfolder("Drafts", null);
+ await PromiseTestUtils.promiseFolderAdded("Drafts");
+ gDraftsFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Drafts");
+ Assert.ok(gDraftsFolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gDraftsFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function goOffline() {
+ // Don't prompt about offline download when going offline.
+ Services.prefs.setIntPref("offline.download.download_messages", 2);
+
+ IMAPPump.incomingServer.closeCachedConnections();
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ IMAPPump.server.stop();
+ Services.io.offline = true;
+});
+
+add_task(async function saveDraft() {
+ let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.from = "Nobody <nobody@tinderbox.test>";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+ msgCompose.initialize(params);
+
+ // Set up the identity.
+ let identity = MailServices.accounts.createIdentity();
+ identity.draftFolder = gDraftsFolder.URI;
+
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let progressListener = new WebProgressListener();
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await progressListener.promise;
+ // Verify that message is not on the server yet.
+ Assert.equal(IMAPPump.daemon.getMailbox("Drafts")._messages.length, 0);
+});
+
+add_task(async function goOnline() {
+ let offlineManager = Cc[
+ "@mozilla.org/messenger/offline-manager;1"
+ ].getService(Ci.nsIMsgOfflineManager);
+ IMAPPump.daemon.closing = false;
+ Services.io.offline = false;
+
+ IMAPPump.server.start();
+ offlineManager.inProgress = true;
+ offlineManager.goOnline(false, true, null);
+ // There seem to be some untraceable postprocessing with 100ms.
+ // (Found through xpcshell-test --verify)
+ await PromiseTestUtils.promiseDelay(100);
+ await TestUtils.waitForCondition(
+ () => !offlineManager.inProgress,
+ "wait for offlineManager not in progress"
+ );
+ // Verify that message is now on the server.
+ Assert.equal(IMAPPump.daemon.getMailbox("Drafts")._messages.length, 1);
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+function WebProgressListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+WebProgressListener.prototype = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ this._resolve();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js b/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js
new file mode 100644
index 0000000000..c98a26470a
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js
@@ -0,0 +1,125 @@
+/* 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/. */
+
+/**
+ * Test to check that offline IMAP operation for a local->IMAP message
+ * move completes correctly once we go back online.
+ */
+
+// NOTE: PromiseTestUtils and MailServices already imported
+
+const { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+function setupTest() {
+ // Turn off autosync_offline_stores because
+ // fetching messages is invoked after copying the messages.
+ // (i.e. The fetching process will be invoked after OnStopCopy)
+ // It will cause crash with an assertion
+ // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown.
+
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+
+ setupIMAPPump();
+
+ // These hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ IMAPPump.inbox.hierarchyDelimiter = "/";
+ IMAPPump.inbox.verifiedAsOnlineFolder = true;
+}
+
+function teardownTest() {
+ teardownIMAPPump();
+}
+
+function goOffline() {
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.server.stop();
+ Services.io.offline = true;
+}
+
+// Go back into online mode, and wait until offline IMAP operations are completed.
+async function goOnline() {
+ IMAPPump.daemon.closing = false;
+ Services.io.offline = false;
+ IMAPPump.server.start();
+
+ let offlineManager = Cc[
+ "@mozilla.org/messenger/offline-manager;1"
+ ].getService(Ci.nsIMsgOfflineManager);
+ offlineManager.goOnline(
+ false, // sendUnsentMessages
+ true, // playbackOfflineImapOperations
+ null // msgWindow
+ );
+ // No way to signal when offline IMAP operations are complete... so we
+ // just blindly wait and cross our fingers :-(
+ await PromiseTestUtils.promiseDelay(2000);
+}
+
+async function loadTestMessage(folder) {
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ let file = do_get_file("../../../data/bugmail1");
+ MailServices.copy.copyFileMessage(
+ file,
+ folder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+}
+
+add_task(async function testOfflineMoveLocalToIMAP() {
+ setupTest();
+
+ // Install a test message in the local folder.
+ await loadTestMessage(localAccountUtils.inboxFolder);
+
+ goOffline();
+
+ // Move messages in local folder to the IMAP inbox.
+ // We're offline so this should result in a queued-up offline IMAP
+ // operation, which will execute when we go back online.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ let msgs = [...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()];
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ msgs,
+ IMAPPump.inbox, // dest folder
+ true, // move
+ copyListener,
+ null,
+ false // undo?
+ );
+ await copyListener.promise;
+
+ // Now, go back online and see if the operation has been performed
+
+ await goOnline();
+
+ let imapINBOX = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ imapINBOX.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ // Local folder should be empty, contents now in IMAP inbox.
+ let localCount = [
+ ...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages(),
+ ].length;
+ let imapCount = [...IMAPPump.inbox.msgDatabase.enumerateMessages()].length;
+ Assert.equal(imapCount, msgs.length);
+ Assert.equal(localCount, 0);
+
+ teardownTest();
+});
diff --git a/comm/mailnews/imap/test/unit/test_offlinePlayback.js b/comm/mailnews/imap/test/unit/test_offlinePlayback.js
new file mode 100644
index 0000000000..ae4361e16b
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_offlinePlayback.js
@@ -0,0 +1,187 @@
+/* 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/. */
+
+/*
+ * Test to ensure that changes made while offline are played back when we
+ * go back online.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+var gSecondFolder, gThirdFolder;
+var gSynthMessage1, gSynthMessage2;
+// the message id of bugmail10
+var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gOfflineManager;
+
+var tests = [
+ setupIMAPPump,
+ function serverParms() {
+ var { fsDebugAll } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+ );
+ IMAPPump.server.setDebugLevel(fsDebugAll);
+ },
+ setup,
+
+ function prepareToGoOffline() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ gSecondFolder = rootFolder
+ .getChildNamed("secondFolder")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ gThirdFolder = rootFolder
+ .getChildNamed("thirdFolder")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ IMAPPump.incomingServer.closeCachedConnections();
+ },
+ async function doOfflineOps() {
+ IMAPPump.server.stop();
+ Services.io.offline = true;
+
+ // Flag the two messages, and then copy them to different folders. Since
+ // we're offline, these operations are synchronous.
+ let msgHdr1 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage1.messageId
+ );
+ let msgHdr2 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage2.messageId
+ );
+ let headers1 = [msgHdr1];
+ let headers2 = [msgHdr2];
+ msgHdr1.folder.markMessagesFlagged(headers1, true);
+ msgHdr2.folder.markMessagesFlagged(headers2, true);
+ let promiseCopyListener1 = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ headers1,
+ gSecondFolder,
+ true,
+ promiseCopyListener1,
+ null,
+ true
+ );
+ let promiseCopyListener2 = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ headers2,
+ gThirdFolder,
+ true,
+ promiseCopyListener2,
+ null,
+ true
+ );
+ var file = do_get_file("../../../data/bugmail10");
+ let promiseCopyListener3 = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ file,
+ IMAPPump.inbox,
+ null,
+ false,
+ 0,
+ "",
+ promiseCopyListener3,
+ null
+ );
+ await Promise.all([
+ promiseCopyListener1.promise,
+ promiseCopyListener2.promise,
+ promiseCopyListener3.promise,
+ ]);
+ },
+ async function goOnline() {
+ gOfflineManager = Cc["@mozilla.org/messenger/offline-manager;1"].getService(
+ Ci.nsIMsgOfflineManager
+ );
+ IMAPPump.daemon.closing = false;
+ Services.io.offline = false;
+
+ IMAPPump.server.start();
+ gOfflineManager.goOnline(false, true, null);
+ await PromiseTestUtils.promiseDelay(2000);
+ },
+ async function updateSecondFolder() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ },
+ async function updateThirdFolder() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ gThirdFolder.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ },
+ async function updateInbox() {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+ },
+ function checkDone() {
+ let msgHdr1 = gSecondFolder.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage1.messageId
+ );
+ let msgHdr2 = gThirdFolder.msgDatabase.getMsgHdrForMessageID(
+ gSynthMessage2.messageId
+ );
+ let msgHdr3 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ Assert.notEqual(msgHdr1, null);
+ Assert.notEqual(msgHdr2, null);
+ Assert.notEqual(msgHdr3, null);
+ },
+ teardownIMAPPump,
+];
+
+async function setup() {
+ /*
+ * Set up an IMAP server.
+ */
+ IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true });
+ IMAPPump.daemon.createMailbox("thirdFolder", { subscribed: true });
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ // Don't prompt about offline download when going offline
+ Services.prefs.setIntPref("offline.download.download_messages", 2);
+
+ // make a couple of messages
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+ messages = messages.concat(gMessageGenerator.makeMessage());
+ gSynthMessage1 = messages[0];
+ gSynthMessage2 = messages[1];
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[0].toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, ["\\Seen"]);
+ imapInbox.addMessage(message);
+ msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messages[1].toMessageString())
+ );
+ message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, ["\\Seen"]);
+ imapInbox.addMessage(message);
+
+ // update folder to download header.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+}
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js b/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js
new file mode 100644
index 0000000000..e3a5da62b2
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js
@@ -0,0 +1,258 @@
+/* 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/. */
+
+/*
+ * Test to ensure that code that writes to the imap offline store deals
+ * with offline store locking correctly.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// Globals
+var gIMAPTrashFolder, gMsgImapInboxFolder;
+var gMovedMsgId;
+
+var gAlertResolve;
+var gGotAlert = new Promise(resolve => {
+ gAlertResolve = resolve;
+});
+
+/* exported alert to alertTestUtils.js */
+function alertPS(parent, aDialogTitle, aText) {
+ gAlertResolve(aText);
+}
+
+function addGeneratedMessagesToServer(messages, mailbox) {
+ // Create the ImapMessages and store them on the mailbox
+ messages.forEach(function (message) {
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message.toMessageString())
+ );
+ mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, []));
+ });
+}
+
+var gStreamedHdr = null;
+
+add_setup(async function () {
+ registerAlertTestUtils();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+
+ setupIMAPPump();
+
+ gMsgImapInboxFolder = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder);
+ // these hacks are required because we've created the inbox before
+ // running initial folder discovery, and adding the folder bails
+ // out before we set it as verified online, so we bail out, and
+ // then remove the INBOX folder since it's not verified.
+ gMsgImapInboxFolder.hierarchyDelimiter = "/";
+ gMsgImapInboxFolder.verifiedAsOnlineFolder = true;
+
+ let messageGenerator = new MessageGenerator();
+ let messages = [];
+ let bodyString = "";
+ for (let i = 0; i < 100; i++) {
+ bodyString +=
+ "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\r\n";
+ }
+
+ for (let i = 0; i < 50; i++) {
+ messages = messages.concat(
+ messageGenerator.makeMessage({
+ body: { body: bodyString, contentType: "text/plain" },
+ })
+ );
+ }
+
+ addGeneratedMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX"));
+ // ...and download for offline use.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function downloadForOffline() {
+ // ...and download for offline use.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(async function deleteOneMsg() {
+ let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages();
+ let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ IMAPPump.inbox.deleteMessages(
+ [msgHdr],
+ null,
+ false,
+ true,
+ copyListener,
+ false
+ );
+ await copyListener.promise;
+});
+
+add_task(async function compactOneFolder() {
+ let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages();
+ let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr);
+ gStreamedHdr = msgHdr;
+ // Mark the message as not being offline, and then we'll make sure that
+ // streaming the message while we're compacting doesn't result in the
+ // message being marked for offline use.
+ // Luckily, compaction compacts the offline store first, so it should
+ // lock the offline store.
+ IMAPPump.inbox.msgDatabase.markOffline(msgHdr.messageKey, false, null);
+ let msgURI = msgHdr.folder.getUriForMsg(msgHdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ // UrlListener will get called when both expunge and offline store
+ // compaction are finished. dummyMsgWindow is required to make the backend
+ // compact the offline store.
+ let compactUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.compact(compactUrlListener, gDummyMsgWindow);
+ // Stream the message w/o a stream listener in an attempt to get the url
+ // started more quickly, while the compact is still going on.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener({});
+ await PromiseTestUtils.promiseDelay(100); // But don't be too fast.
+ msgServ.streamMessage(
+ msgURI,
+ new PromiseTestUtils.PromiseStreamListener(),
+ null,
+ urlListener,
+ false,
+ "",
+ false
+ );
+ await compactUrlListener.promise;
+
+ // Because we're streaming the message while compaction is going on,
+ // we should not have stored it for offline use.
+ Assert.equal(false, gStreamedHdr.flags & Ci.nsMsgMessageFlags.Offline);
+
+ await urlListener.promise;
+});
+
+add_task(async function deleteAnOtherMsg() {
+ let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages();
+ let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ IMAPPump.inbox.deleteMessages(
+ [msgHdr],
+ null,
+ false,
+ true,
+ copyListener,
+ false
+ );
+ await copyListener.promise;
+});
+
+add_task(async function updateTrash() {
+ gIMAPTrashFolder = IMAPPump.incomingServer.rootFolder
+ .getChildNamed("Trash")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ // hack to force uid validity to get initialized for trash.
+ gIMAPTrashFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function downloadTrashForOffline() {
+ // ...and download for offline use.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gIMAPTrashFolder.downloadAllForOffline(listener, null);
+ await listener.promise;
+});
+
+add_task(async function testOfflineBodyCopy() {
+ // In order to check that offline copy of messages doesn't try to copy
+ // the body if the offline store is locked, we're going to go offline.
+ // Thunderbird itself does move/copies pseudo-offline, but that's too
+ // hard to test because of the half-second delay.
+ IMAPPump.server.stop();
+ Services.io.offline = true;
+ let enumerator = gIMAPTrashFolder.msgDatabase.enumerateMessages();
+ let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr);
+ gMovedMsgId = msgHdr.messageId;
+ let compactionListener = new PromiseTestUtils.PromiseUrlListener();
+ // NOTE: calling compact() even if msgStore doesn't support compaction.
+ // It should be a safe no-op, and we're just testing that the listener is
+ // still invoked.
+ IMAPPump.inbox.compact(compactionListener, gDummyMsgWindow);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gIMAPTrashFolder,
+ [msgHdr],
+ IMAPPump.inbox,
+ true,
+ copyListener,
+ null,
+ true
+ );
+
+ // Verify that the moved Msg is not offline.
+ try {
+ let movedMsg =
+ IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMovedMsgId);
+ Assert.equal(0, movedMsg.flags & Ci.nsMsgMessageFlags.Offline);
+ } catch (ex) {
+ throw new Error(ex);
+ }
+ await compactionListener.promise;
+ await copyListener.promise;
+});
+
+add_task(async function test_checkAlert() {
+ // Check if testing maildir which doesn't produce an the alert like mbox.
+ // If so, don't wait for an alert.
+ let storageCID = Services.prefs.getCharPref(
+ "mail.serverDefaultStoreContractID"
+ );
+ if (storageCID == "@mozilla.org/msgstore/maildirstore;1") {
+ return;
+ }
+
+ let alertText = await gGotAlert;
+ Assert.ok(
+ alertText.startsWith(
+ "The folder 'Inbox on Mail for ' cannot be compacted because another operation is in progress. Please try again later."
+ )
+ );
+});
+
+add_task(function teardown() {
+ gMsgImapInboxFolder = null;
+ gIMAPTrashFolder = null;
+
+ // IMAPPump.server has already stopped, we do not need to IMAPPump.server.stop().
+ IMAPPump.inbox = null;
+ try {
+ IMAPPump.incomingServer.closeCachedConnections();
+ let serverSink = IMAPPump.incomingServer.QueryInterface(
+ Ci.nsIImapServerSink
+ );
+ serverSink.abortQueuedUrls();
+ } catch (ex) {
+ throw new Error(ex);
+ }
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js b/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js
new file mode 100644
index 0000000000..a1f08cc8f0
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+// This tests that arbitrary message header properties are preserved
+// during online move of an imap message.
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gMessage = "bugmail10"; // message file used as the test message
+var gSubfolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+add_task(async function createSubfolder() {
+ IMAPPump.incomingServer.rootFolder.createSubfolder("Subfolder", null);
+ await PromiseTestUtils.promiseFolderAdded("Subfolder");
+ gSubfolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Subfolder");
+ Assert.ok(gSubfolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSubfolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// load and update a message in the imap fake server
+add_task(async function loadImapMessage() {
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ IMAPPump.inbox.updateFolder(null);
+ await PromiseTestUtils.promiseFolderNotification(IMAPPump.inbox, "msgAdded");
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+
+ // set an arbitrary property
+ msgHdr.setStringProperty("testprop", "somevalue");
+});
+
+// move the message to a subfolder
+add_task(async function moveMessageToSubfolder() {
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox, // srcFolder
+ [msgHdr], // messages
+ gSubfolder, // dstFolder
+ true, // isMove
+ copyListener, // listener
+ null, // msgWindow
+ false // allowUndo
+ );
+ await copyListener.promise;
+});
+
+add_task(async function testPropertyOnMove() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSubfolder.updateFolderWithListener(null, listener);
+ await PromiseTestUtils.promiseFolderNotification(gSubfolder, "msgAdded");
+ await listener.promise;
+ let msgHdr = mailTestUtils.firstMsgHdr(gSubfolder);
+ Assert.equal(msgHdr.getStringProperty("testprop"), "somevalue");
+});
+
+// Cleanup
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_saveImapDraft.js b/comm/mailnews/imap/test/unit/test_saveImapDraft.js
new file mode 100644
index 0000000000..ec2b96b3de
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_saveImapDraft.js
@@ -0,0 +1,119 @@
+/* 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/. */
+
+/*
+ * This file tests that a message saved as draft in an IMAP folder is correctly
+ * marked as unread.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gDraftsFolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+add_task(async function createDraftsFolder() {
+ IMAPPump.incomingServer.rootFolder.createSubfolder("Drafts", null);
+ await PromiseTestUtils.promiseFolderAdded("Drafts");
+ gDraftsFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Drafts");
+ Assert.ok(gDraftsFolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gDraftsFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function saveDraft() {
+ let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.from = "Nobody <nobody@tinderbox.test>";
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = fields;
+ msgCompose.initialize(params);
+
+ // Set up the identity
+ let identity = MailServices.accounts.createIdentity();
+ identity.draftFolder = gDraftsFolder.URI;
+
+ let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance(
+ Ci.nsIMsgProgress
+ );
+ let progressListener = new ProgressListener();
+ progress.registerListener(progressListener);
+ msgCompose.sendMsg(
+ Ci.nsIMsgSend.nsMsgSaveAsDraft,
+ identity,
+ "",
+ null,
+ progress
+ );
+ await progressListener.promise;
+});
+
+add_task(async function updateDrafts() {
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gDraftsFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function checkResult() {
+ Assert.equal(gDraftsFolder.getTotalMessages(false), 1);
+ Assert.equal(gDraftsFolder.getNumUnread(false), 1);
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+function ProgressListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+
+ProgressListener.prototype = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ this._resolve();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {},
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/imap/test/unit/test_saveTemplate.js b/comm/mailnews/imap/test/unit/test_saveTemplate.js
new file mode 100644
index 0000000000..12560411ee
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_saveTemplate.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+/*
+ * Tests imap save of message as a template, and test initial save right after
+ * creation of folder.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_setup(function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+// load and update a message in the imap fake server
+add_task(async function loadImapMessage() {
+ let gMessageGenerator = new MessageGenerator();
+ // create a synthetic message with attachment
+ let smsg = gMessageGenerator.makeMessage();
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(smsg.toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(message);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// This is similar to the method in mailCommands.js, to test the way that
+// it creates a new templates folder before saving the message as a template.
+add_task(async function saveAsTemplate() {
+ // Prepare msgAddedListener for this test.
+ let msgAddedListener = new MsgAddedListener();
+ MailServices.mfn.addListener(msgAddedListener, MailServices.mfn.msgAdded);
+
+ let hdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let uri = IMAPPump.inbox.getUriForMsg(hdr);
+ let identity = MailServices.accounts.getFirstIdentityForServer(
+ IMAPPump.incomingServer
+ );
+ identity.stationeryFolder =
+ IMAPPump.incomingServer.rootFolder.URI + "/Templates";
+ let templates = MailUtils.getOrCreateFolder(identity.stationeryFolder);
+ // Verify that Templates folder doesn't exist, and then create it.
+ Assert.equal(templates.parent, null);
+ templates.setFlag(Ci.nsMsgFolderFlags.Templates);
+ let listener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl() {
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ messenger.saveAs(uri, false, identity, null);
+ },
+ });
+ templates.createStorageIfMissing(listener);
+ await listener.promise;
+
+ await msgAddedListener.promise;
+});
+
+// Cleanup
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+// listener for saveAsTemplate adding a message to the templates folder.
+function MsgAddedListener() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+
+MsgAddedListener.prototype = {
+ msgAdded(aMsg) {
+ // Check this is the templates folder.
+ Assert.equal(aMsg.folder.prettyName, "Templates");
+ this._resolve();
+ },
+};
diff --git a/comm/mailnews/imap/test/unit/test_starttlsFailure.js b/comm/mailnews/imap/test/unit/test_starttlsFailure.js
new file mode 100644
index 0000000000..59c7f2e70f
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_starttlsFailure.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+/**
+ * This test checks that we handle the server dropping the connection
+ * on starttls. Since fakeserver doesn't support STARTTLS, I've made
+ * it drop the connection when it's attempted.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gAlertResolve;
+var gGotAlert = new Promise(resolve => {
+ gAlertResolve = resolve;
+});
+
+/* exported alert to alertTestUtils.js */
+function alertPS(parent, aDialogTitle, aText) {
+ gAlertResolve(aText);
+}
+
+add_setup(async function () {
+ // Set up IMAP fakeserver and incoming server.
+ IMAPPump.daemon = new ImapDaemon();
+ IMAPPump.server = makeServer(IMAPPump.daemon, "", { dropOnStartTLS: true });
+ IMAPPump.incomingServer = createLocalIMAPServer(IMAPPump.server.port);
+ IMAPPump.incomingServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+ // We need a local account for the IMAP server to have its sent messages in.
+ localAccountUtils.loadLocalMailAccount();
+
+ // We need an identity so that updateFolder doesn't fail.
+ let imapAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = IMAPPump.incomingServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // The server doesn't support more than one connection.
+ Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1);
+ // We aren't interested in downloading messages automatically.
+ Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);
+
+ IMAPPump.inbox = IMAPPump.incomingServer.rootFolder
+ .getChildNamed("Inbox")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+
+ registerAlertTestUtils();
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(gDummyMsgWindow, listener);
+ await listener.promise
+ .then(res => {
+ throw new Error("updateFolderWithListener has to fail");
+ })
+ .catch(exitCode => {
+ Assert.ok(!Components.isSuccessCode(exitCode));
+ });
+});
+
+add_task(async function check_alert() {
+ let alertText = await gGotAlert;
+ Assert.ok(alertText.startsWith("Server localhost has disconnected"));
+});
+
+add_task(function teardown() {
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.server.stop();
+});
diff --git a/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js b/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js
new file mode 100644
index 0000000000..9a7718332f
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+/* Test that the message failed to move to a local folder remains on IMAP
+ * server. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+function stop_server() {
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.server.stop();
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
+
+add_setup(function () {
+ setupIMAPPump();
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+});
+
+add_setup(async function () {
+ let messageGenerator = new MessageGenerator();
+ let messageString = messageGenerator.makeMessage().toMessageString();
+ let dataUri = Services.io.newURI(
+ "data:text/plain;base64," + btoa(messageString)
+ );
+ let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(imapMsg);
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function move_messages() {
+ let msg = IMAPPump.inbox.msgDatabase.getMsgHdrForKey(
+ IMAPPump.mailbox.uidnext - 1
+ );
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ OnProgress(aProgress, aProgressMax) {
+ stop_server();
+ },
+ });
+ MailServices.copy.copyMessages(
+ IMAPPump.inbox,
+ [msg],
+ localAccountUtils.inboxFolder,
+ true,
+ copyListener,
+ null,
+ false
+ );
+ await copyListener.promise;
+});
+
+add_task(function check_messages() {
+ Assert.equal(IMAPPump.inbox.getTotalMessages(false), 1);
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+});
+
+add_task(function endTest() {
+ // IMAPPump.server.performTest() brings this test to a halt,
+ // so we need teardownIMAPPump() without IMAPPump.server.performTest().
+ IMAPPump.inbox = null;
+ IMAPPump.server.resetTest();
+ try {
+ IMAPPump.incomingServer.closeCachedConnections();
+ let serverSink = IMAPPump.incomingServer.QueryInterface(
+ Ci.nsIImapServerSink
+ );
+ serverSink.abortQueuedUrls();
+ } catch (ex) {
+ dump(ex);
+ }
+ IMAPPump.server.stop();
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/imap/test/unit/test_subfolderLocation.js b/comm/mailnews/imap/test/unit/test_subfolderLocation.js
new file mode 100644
index 0000000000..7cbf1b7bb8
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_subfolderLocation.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test proper location of new imap offline subfolders for maildir.
+
+// async support
+/* import-globals-from ../../../test/resources/logHelper.js */
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/logHelper.js");
+load("../../../resources/alertTestUtils.js");
+
+// Globals
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail10"; // message file used as the test message
+
+add_task(function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+ setupIMAPPump();
+});
+
+// load and update a message in the imap fake server
+add_task(async function loadImapMessage() {
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(gDummyMsgWindow, promiseUrlListener);
+ await promiseUrlListener.promise;
+
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+});
+
+add_task(async function downloadOffline() {
+ // ...and download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+});
+
+var folderName1 = "sub1";
+var folderName2 = "sub2";
+
+// use a folder method to add a subfolder
+add_task(async function addSubfolder() {
+ let promiseFolder1 = PromiseTestUtils.promiseFolderAdded(folderName1);
+ IMAPPump.inbox.createSubfolder(folderName1, null);
+ await promiseFolder1;
+});
+
+// use a store method to add a subfolder
+add_task(function storeAddSubfolder() {
+ IMAPPump.incomingServer.msgStore.createFolder(IMAPPump.inbox, folderName2);
+});
+
+// test that folders created with store and folder have the same parent
+add_task(function testSubfolder() {
+ let subfolder1 = IMAPPump.inbox.getChildNamed(folderName1);
+ let subfolder2 = IMAPPump.inbox.getChildNamed(folderName2);
+ Assert.equal(
+ subfolder1.filePath.parent.path,
+ subfolder2.filePath.parent.path
+ );
+});
+
+// Cleanup at end
+add_task(teardownIMAPPump);
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/imap/test/unit/test_syncChanges.js b/comm/mailnews/imap/test/unit/test_syncChanges.js
new file mode 100644
index 0000000000..e4b46f35a8
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_syncChanges.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/*
+ * Test to ensure that changes made from a different profile/machine
+ * are synced correctly. In particular, we're checking that emptying out
+ * an imap folder on the server makes us delete all the headers from our db.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gMessage;
+var gSecondFolder;
+var gSynthMessage;
+
+add_setup(async function () {
+ /*
+ * Set up an IMAP server.
+ */
+ setupIMAPPump();
+
+ IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true });
+
+ let messages = [];
+ let gMessageGenerator = new MessageGenerator();
+ messages = messages.concat(gMessageGenerator.makeMessage());
+ gSynthMessage = messages[0];
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(gSynthMessage.toMessageString())
+ );
+ gMessage = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(gMessage);
+
+ // update folder to download header.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function switchAwayFromInbox() {
+ let rootFolder = IMAPPump.incomingServer.rootFolder;
+ gSecondFolder = rootFolder
+ .getChildNamed("secondFolder")
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+
+ // Selecting the second folder will close the cached connection
+ // on the inbox because fake server only supports one connection at a time.
+ // Then, we can poke at the message on the imap server directly, which
+ // simulates the user changing the message from a different machine,
+ // and Thunderbird discovering the change when it does a flag sync
+ // upon reselecting the Inbox.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gSecondFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(async function simulateMailboxEmptied() {
+ gMessage.setFlag("\\Deleted");
+ let expungeListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.expunge(expungeListener, null);
+ await expungeListener.promise;
+ let updateListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, updateListener);
+ await updateListener.promise;
+});
+
+add_task(function checkMailboxEmpty() {
+ Assert.equal(IMAPPump.inbox.getTotalMessages(false), 0);
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js b/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js
new file mode 100644
index 0000000000..89c223b206
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js
@@ -0,0 +1,146 @@
+/* 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/. */
+
+/*
+ * This file tests recognizing a message as junk due to
+ * SpamAssassin headers, and marking that as good
+ * without having the message return to the junk folder,
+ * as discussed in bug 540385.
+ *
+ * adapted from test_filterNeedsBody.js
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Globals
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gMessage = "SpamAssassinYes"; // message file used as the test message
+var gJunkFolder;
+
+add_setup(function () {
+ setupIMAPPump();
+ let server = IMAPPump.incomingServer;
+ let spamSettings = server.spamSettings;
+ server.setBoolValue("useServerFilter", true);
+ server.setCharValue("serverFilterName", "SpamAssassin");
+ server.setIntValue(
+ "serverFilterTrustFlags",
+ Ci.nsISpamSettings.TRUST_POSITIVES
+ );
+ server.setBoolValue("moveOnSpam", true);
+ server.setIntValue(
+ "moveTargetMode",
+ Ci.nsISpamSettings.MOVE_TARGET_MODE_ACCOUNT
+ );
+ server.setCharValue("spamActionTargetAccount", server.serverURI);
+
+ spamSettings.initialize(server);
+});
+
+add_task(async function createJunkFolder() {
+ IMAPPump.incomingServer.rootFolder.createSubfolder("Junk", null);
+ await PromiseTestUtils.promiseFolderAdded("Junk");
+ gJunkFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Junk");
+ Assert.ok(gJunkFolder instanceof Ci.nsIMsgImapMailFolder);
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gJunkFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+/*
+ * Load and update a message in the imap fake server, should move
+ * SpamAssassin-marked junk message to junk folder
+ */
+add_task(async function loadImapMessage() {
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ /*
+ * The message matched the SpamAssassin header, so it moved
+ * to the junk folder
+ */
+ IMAPPump.inbox.updateFolder(null);
+ await PromiseTestUtils.promiseFolderNotification(
+ gJunkFolder,
+ "msgsMoveCopyCompleted"
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ gJunkFolder.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+add_task(function testMessageInJunk() {
+ Assert.equal(0, IMAPPump.inbox.getTotalMessages(false));
+ Assert.equal(1, gJunkFolder.getTotalMessages(false));
+});
+
+add_task(async function markMessageAsGood() {
+ /*
+ * This is done in the application in nsMsgDBView, which is difficult
+ * to test in xpcshell tests. We aren't really trying to test that here
+ * though, since the point of this test is working with the server-based
+ * filters. So I will simply simulate the operations that would typically
+ * be done by a manual marking of the messages.
+ */
+ let msgHdr = mailTestUtils.firstMsgHdr(gJunkFolder);
+ msgHdr.setStringProperty("junkscoreorigin", "user");
+ msgHdr.setStringProperty("junkpercent", "0"); // good percent
+ msgHdr.setStringProperty("junkscore", "0"); // good score
+
+ /*
+ * Now move this message to the inbox. In bug 540385, the message just
+ * gets moved back to the junk folder again. We'll test that we
+ * are now preventing that.
+ */
+ MailServices.copy.copyMessages(
+ gJunkFolder, // srcFolder
+ [msgHdr], // messages
+ IMAPPump.inbox, // dstFolder
+ true, // isMove
+ null, // listener
+ null, // msgWindow
+ false // allowUndo
+ );
+ await PromiseTestUtils.promiseFolderNotification(
+ IMAPPump.inbox,
+ "msgsMoveCopyCompleted"
+ );
+});
+
+add_task(async function updateFoldersAndCheck() {
+ let inboxUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, inboxUrlListener);
+ await inboxUrlListener.promise;
+ let junkUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ gJunkFolder.updateFolderWithListener(null, junkUrlListener);
+ await junkUrlListener.promise;
+ // bug 540385 causes this test to fail
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ Assert.equal(0, gJunkFolder.getTotalMessages(false));
+});
+
+add_task(function endTest() {
+ teardownIMAPPump();
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file("../../../data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
+
+// quick shorthand for output of a line of text.
+function dl(text) {
+ dump(text + "\n");
+}
diff --git a/comm/mailnews/imap/test/unit/xpcshell-cpp.ini b/comm/mailnews/imap/test/unit/xpcshell-cpp.ini
new file mode 100644
index 0000000000..394ca5fb1d
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/xpcshell-cpp.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+head = head_server.js
+tags = mbox cpp
+dupe-manifest =
+run-sequentially =
+prefs =
+ mailnews.imap.jsmodule=false
+
+[test_chunkLastLF.js]
+[test_customCommandReturnsFetchResponse.js]
+[test_imapChunks.js]
+[test_imapHdrChunking.js]
+
+[include:xpcshell-shared.ini]
diff --git a/comm/mailnews/imap/test/unit/xpcshell-shared.ini b/comm/mailnews/imap/test/unit/xpcshell-shared.ini
new file mode 100644
index 0000000000..b169aee8d1
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/xpcshell-shared.ini
@@ -0,0 +1,58 @@
+[test_autosync_date_constraints.js]
+[test_bccProperty.js]
+[test_bug460636.js]
+[test_compactOfflineStore.js]
+[test_converterImap.js]
+[test_copyThenMove.js]
+[test_dontStatNoSelect.js]
+[test_downloadOffline.js]
+[test_fetchCustomAttribute.js]
+[test_filterCustomHeaders.js]
+[test_filterNeedsBody.js]
+[test_folderOfflineFlags.js]
+[test_gmailAttributes.js]
+[test_gmailOfflineMsgStore.js]
+[test_imapAttachmentSaves.js]
+[test_imapAutoSync.js]
+[test_imapClientid.js]
+[test_imapContentLength.js]
+[test_imapCopyTimeout.js]
+[test_imapFilterActions.js]
+[test_imapFilterActionsPostplugin.js]
+[test_imapFlagChange.js]
+[test_imapFolderCopy.js]
+[test_imapHdrStreaming.js]
+[test_imapHighWater.js]
+[test_imapID.js]
+[test_imapMove.js]
+[test_imapPasswordFailure.js]
+[test_imapProtocols.js]
+[test_imapProxy.js]
+[test_imapRename.js]
+[test_imapSearch.js]
+[test_imapStatusCloseDBs.js]
+[test_imapStoreMsgOffline.js]
+[test_imapUndo.js]
+[test_imapUrls.js]
+[test_largeOfflineStore.js]
+skip-if = os == 'mac'
+[test_listClosesDB.js]
+[test_listSubscribed.js]
+[test_localToImapFilter.js]
+[test_localToImapFilterQuarantine.js]
+[test_lsub.js]
+[test_mailboxes.js]
+[test_nsIMsgFolderListenerIMAP.js]
+[test_offlineCopy.js]
+[test_offlineDraftDataloss.js]
+[test_offlineMoveLocalToIMAP.js]
+[test_offlinePlayback.js]
+[test_offlineStoreLocking.js]
+[test_preserveDataOnMove.js]
+[test_saveImapDraft.js]
+[test_saveTemplate.js]
+[test_starttlsFailure.js]
+[test_stopMovingToLocalFolder.js]
+[test_subfolderLocation.js]
+[test_syncChanges.js]
+[test_trustSpamAssassin.js]
diff --git a/comm/mailnews/imap/test/unit/xpcshell.ini b/comm/mailnews/imap/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..b0be23ad01
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head_server.js
+tags = mbox jsm
+dupe-manifest =
+run-sequentially =
+prefs =
+ mailnews.imap.jsmodule=true
+
+[test_ImapResponse.js]
+[test_imapAuthMethods.js]
+
+[include:xpcshell-shared.ini]
diff --git a/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini b/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini
new file mode 100644
index 0000000000..4c345d69e3
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+head = head_imap_maildir.js
+tags = maildir cpp
+dupe-manifest =
+run-sequentially =
+prefs =
+ mailnews.imap.jsmodule=false
+
+[test_chunkLastLF.js]
+[test_customCommandReturnsFetchResponse.js]
+[test_imapChunks.js]
+[test_imapHdrChunking.js]
+
+[include:xpcshell-shared.ini]
diff --git a/comm/mailnews/imap/test/unit/xpcshell_maildir.ini b/comm/mailnews/imap/test/unit/xpcshell_maildir.ini
new file mode 100644
index 0000000000..6bb40162a8
--- /dev/null
+++ b/comm/mailnews/imap/test/unit/xpcshell_maildir.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = head_imap_maildir.js
+tags = maildir jsm
+dupe-manifest =
+run-sequentially =
+prefs =
+ mailnews.imap.jsmodule=true
+
+[include:xpcshell-shared.ini]
diff --git a/comm/mailnews/import/build/moz.build b/comm/mailnews/import/build/moz.build
new file mode 100644
index 0000000000..b6f8aa8baa
--- /dev/null
+++ b/comm/mailnews/import/build/moz.build
@@ -0,0 +1,28 @@
+# vim: set filetype=python:
+# 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/.
+
+USE_LIBS += [
+ "nspr",
+]
+
+Library("import")
+FINAL_LIBRARY = "xul"
+
+# js needs to come after xul for now, because it is an archive and its content
+# is discarded when it comes first.
+USE_LIBS += [
+ "js",
+]
+
+LOCAL_INCLUDES += [
+ "../src",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ OS_LIBS += CONFIG["TK_LIBS"]
+ OS_LIBS += ["-framework Cocoa"]
+
+if CONFIG["OS_ARCH"] != "WINNT":
+ OS_LIBS += CONFIG["MOZ_ZLIB_LIBS"]
diff --git a/comm/mailnews/import/content/aboutImport.js b/comm/mailnews/import/content/aboutImport.js
new file mode 100644
index 0000000000..1f0d06d38d
--- /dev/null
+++ b/comm/mailnews/import/content/aboutImport.js
@@ -0,0 +1,1511 @@
+/* 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/. */
+
+/* global MozElements */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ MailServices: "resource:///modules/MailServices.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ AddrBookFileImporter: "resource:///modules/AddrBookFileImporter.jsm",
+ CalendarFileImporter: "resource:///modules/CalendarFileImporter.jsm",
+ ProfileExporter: "resource:///modules/ProfileExporter.jsm",
+ cal: "resource:///modules/calendar/calUtils.jsm",
+});
+
+/**
+ * An object to represent a source profile to import from.
+ *
+ * @typedef {object} SourceProfile
+ * @property {string} [name] - The profile name.
+ * @property {nsIFile} dir - The profile location.
+ */
+
+/**
+ * @typedef {object} Step
+ * @property {Function} returnTo - Function that resets to this step. Should end
+ * up calling |updateSteps()| with this step again.
+ */
+
+const Steps = {
+ _pastSteps: [],
+ /**
+ * Toggle visibility of the navigation steps.
+ *
+ * @param {boolean} visible - If the navigation steps should be shown.
+ */
+ toggle(visible) {
+ document.getElementById("stepNav").hidden = !visible;
+ },
+ /**
+ * Update the currently displayed steps by adding a new step and updating the
+ * forecast of remaining steps.
+ *
+ * @param {Step} currentStep
+ * @param {number} plannedSteps - Amount of steps to follow this step,
+ * including summary.
+ */
+ updateSteps(currentStep, plannedSteps) {
+ this._pastSteps.push(currentStep);
+ let confirm = document.getElementById("navConfirm");
+ const isConfirmStep = plannedSteps === 0;
+ confirm.classList.toggle("current", isConfirmStep);
+ confirm.toggleAttribute("disabled", isConfirmStep);
+ confirm.removeAttribute("aria-current");
+ document.getElementById("stepNav").replaceChildren(
+ ...this._pastSteps.map((step, index) => {
+ const li = document.createElement("li");
+ const button = document.createElement("button");
+ if (step === currentStep) {
+ if (isConfirmStep) {
+ confirm.setAttribute("aria-current", "step");
+ return confirm;
+ }
+ li.classList.add("current");
+ li.setAttribute("aria-current", "step");
+ button.setAttribute("disabled", "disabled");
+ } else {
+ li.classList.add("completed");
+ button.addEventListener("click", () => {
+ this.backTo(index);
+ });
+ }
+ document.l10n.setAttributes(button, "step-count", {
+ number: index + 1,
+ });
+ li.append(button);
+ //TODO tooltips
+ return li;
+ }),
+ ...new Array(Math.max(plannedSteps - 1, 0))
+ .fill(null)
+ .map((item, index) => {
+ const li = document.createElement("li");
+ const button = document.createElement("button");
+ document.l10n.setAttributes(button, "step-count", {
+ number: this._pastSteps.length + index + 1,
+ });
+ button.setAttribute("disabled", "disabled");
+ li.append(button);
+ //TODO tooltips
+ return li;
+ }),
+ isConfirmStep ? "" : confirm
+ );
+ },
+ /**
+ * Return to a previous step.
+ *
+ * @param {number} [stepIndex=-1] - The absolute index of the step to return
+ * to. By default goes back one step.
+ * @returns {boolean} if a previous step was recalled.
+ */
+ backTo(stepIndex = -1) {
+ if (!this._pastSteps.length || stepIndex >= this._pastSteps.length) {
+ return false;
+ }
+ if (stepIndex < 0) {
+ // Make relative step index absolute
+ stepIndex = this._pastSteps.length + stepIndex - 1;
+ }
+ let targetStep = this._pastSteps[stepIndex];
+ this._pastSteps = this._pastSteps.slice(0, stepIndex);
+ targetStep.returnTo();
+ return true;
+ },
+ /**
+ * If any previous steps have been recorded.
+ *
+ * @returns {boolean} If there are steps preceding the current state.
+ */
+ hasStepHistory() {
+ return this._pastSteps.length > 0;
+ },
+ /**
+ * Reset step state.
+ */
+ reset() {
+ this._pastSteps = [];
+ },
+};
+
+/**
+ * The base controller for an importing process.
+ */
+class ImporterController {
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+
+ /**
+ * @param {string} elementId - The root element id.
+ * @param {string} paneIdPrefix - The prefix of sub pane id.
+ */
+ constructor(elementId, paneIdPrefix) {
+ this._el = document.getElementById(elementId);
+ this._paneIdPrefix = paneIdPrefix;
+ }
+
+ /**
+ * Show a specific pane, hide all the others.
+ *
+ * @param {string} id - The pane id to show.
+ */
+ showPane(id) {
+ this._currentPane = id;
+ id = `${this._paneIdPrefix}-${id}`;
+ for (let pane of this._el.querySelectorAll(":scope > section")) {
+ pane.hidden = pane.id != id;
+ }
+ }
+
+ /**
+ * Show the previous pane.
+ */
+ back() {
+ ImporterController.notificationBox.removeAllNotifications();
+ Steps.backTo();
+ }
+
+ /**
+ * Show the next pane.
+ */
+ next() {
+ if (this._restartOnOk) {
+ window.close();
+ MailUtils.restartApplication();
+ return;
+ }
+ ImporterController.notificationBox.removeAllNotifications();
+ }
+
+ /**
+ * Show the first pane.
+ */
+ reset() {
+ this._el.classList.remove(
+ "restart-only",
+ "progress",
+ "complete",
+ "final-step"
+ );
+ this._toggleBackButton(true);
+ }
+
+ /**
+ * Show the progress bar.
+ *
+ * @param {string} progressL10nId - Fluent ID to use for the progress
+ * description. Should have a |progressPercent| variable expecting the
+ * current progress like "50%".
+ */
+ showProgress(progressL10nId) {
+ this._progressL10nId = progressL10nId;
+ this.updateProgress(0);
+ this._el.classList.add("progress");
+ this._toggleBackButton(false);
+ this._inProgress = true;
+ }
+
+ /**
+ * Update the progress bar.
+ *
+ * @param {number} value - A number between 0 and 1 to represent the progress.
+ */
+ updateProgress(value) {
+ this._el.querySelector(".progressPaneProgressBar").value = value;
+ document.l10n.setAttributes(
+ this._el.querySelector(".progressPaneDesc"),
+ this._progressL10nId,
+ {
+ progressPercent: ImporterController.percentFormatter.format(value),
+ }
+ );
+ }
+
+ /**
+ * Show the finish text.
+ *
+ * @param {boolean} [restartNeeded=false] - Whether restart is needed to
+ * finish the importing.
+ */
+ finish(restartNeeded = false) {
+ this._restartOnOk = restartNeeded;
+ this._el.classList.toggle("restart-required", restartNeeded);
+ this._el.classList.add("complete");
+ document.l10n.setAttributes(
+ this._el.querySelector(".progressPaneDesc"),
+ "progress-pane-finished-desc2"
+ );
+ this._inProgress = false;
+ }
+
+ /**
+ * Show the error pane, with an error message.
+ *
+ * @param {string} msgId - The error message fluent id.
+ */
+ showError(msgId) {
+ if (this._inProgress) {
+ this._toggleBackButton(true);
+ this._el.classList.remove("progress");
+ this._restartOnOk = false;
+ this._inProgress = false;
+ }
+ ImporterController.notificationBox.removeAllNotifications();
+ let notification = ImporterController.notificationBox.appendNotification(
+ "error",
+ {
+ label: {
+ "l10n-id": msgId,
+ },
+ priority: ImporterController.notificationBox.PRIORITY_CRITICAL_HIGH,
+ },
+ null
+ );
+ notification.removeAttribute("dismissable");
+ }
+
+ /**
+ * Disable/enable the back button.
+ *
+ * @param {boolean} enable - If the back button should be enabled
+ */
+ _toggleBackButton(enable) {
+ if (this._el.querySelector(".buttons-container")) {
+ this._el.querySelector(".back").disabled = !enable;
+ }
+ }
+}
+
+XPCOMUtils.defineLazyGetter(
+ ImporterController,
+ "percentFormatter",
+ () =>
+ new Intl.NumberFormat(undefined, {
+ style: "percent",
+ })
+);
+XPCOMUtils.defineLazyGetter(
+ ImporterController,
+ "notificationBox",
+ () =>
+ new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("errorNotifications").append(element);
+ })
+);
+
+/**
+ * Control the #tabPane-app element, to support importing from an application.
+ */
+class ProfileImporterController extends ImporterController {
+ constructor() {
+ super("tabPane-app", "app");
+ document.getElementById("appItemsList").addEventListener(
+ "input",
+ () => {
+ let state = this._getItemsChecked(true);
+ document.getElementById("profileNextButton").disabled = Object.values(
+ state
+ ).every(isChecked => !isChecked);
+ },
+ {
+ capture: true,
+ passive: true,
+ }
+ );
+ }
+
+ /**
+ * A map from radio input value to the importer module name.
+ */
+ _sourceModules = {
+ Thunderbird: "ThunderbirdProfileImporter",
+ Seamonkey: "SeamonkeyProfileImporter",
+ Outlook: "OutlookProfileImporter",
+ Becky: "BeckyProfileImporter",
+ AppleMail: "AppleMailProfileImporter",
+ };
+
+ /**
+ * Maps app radio input values to their respective representations in l10n
+ * ids.
+ */
+ _sourceL10nIds = {
+ Thunderbird: "thunderbird",
+ Seamonkey: "seamonkey",
+ Outlook: "outlook",
+ Becky: "becky",
+ AppleMail: "apple-mail",
+ };
+ _sourceAppName = "thunderbird";
+
+ next() {
+ super.next();
+ switch (this._currentPane) {
+ case "profiles":
+ this._onSelectProfile();
+ break;
+ case "items":
+ this._onSelectItems();
+ break;
+ case "summary":
+ window.close();
+ break;
+ }
+ }
+
+ /**
+ * Handler for the Continue button on the sources pane.
+ *
+ * @param {string} source - Profile source to import.
+ */
+ async _onSelectSource(source) {
+ this._sourceAppName = this._sourceL10nIds[source];
+ let sourceModule = this._sourceModules[source];
+
+ let module = ChromeUtils.import(`resource:///modules/${sourceModule}.jsm`);
+ this._importer = new module[sourceModule]();
+
+ let sourceProfiles = await this._importer.getSourceProfiles();
+ if (sourceProfiles.length > 1 || this._importer.USE_FILE_PICKER) {
+ // Let the user pick a profile if there are multiple options.
+ this._showProfiles(sourceProfiles, this._importer.USE_FILE_PICKER);
+ } else if (sourceProfiles.length == 1) {
+ // Let the user pick what to import.
+ this._showItems(sourceProfiles[0]);
+ } else {
+ this.showError("error-message-no-profile");
+ throw new Error("No profile found, do not advance to app flow.");
+ }
+ }
+
+ /**
+ * Show the profiles pane, with a list of profiles and optional file pickers.
+ *
+ * @param {SourceProfile[]} profiles - An array of profiles.
+ * @param {boolean} useFilePicker - Whether to render file pickers.
+ */
+ _showProfiles(profiles, useFilePicker) {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showProfiles(profiles, useFilePicker);
+ },
+ },
+ 2
+ );
+ this._sourceProfiles = profiles;
+ document.l10n.setAttributes(
+ document.getElementById("profilesPaneTitle"),
+ `from-app-${this._sourceAppName}`
+ );
+ document.l10n.setAttributes(
+ document.getElementById("profilesPaneSubtitle"),
+ `profiles-pane-title-${this._sourceAppName}`
+ );
+ let elProfileList = document.getElementById("profileList");
+ elProfileList.hidden = !profiles.length;
+ elProfileList.innerHTML = "";
+ document.getElementById("filePickerList").hidden = !useFilePicker;
+
+ for (let profile of profiles) {
+ let label = document.createElement("label");
+ label.className = "toggle-container-with-text";
+
+ let input = document.createElement("input");
+ input.type = "radio";
+ input.name = "appProfile";
+ input.value = profile.dir.path;
+ label.append(input);
+
+ let name = document.createElement("p");
+ if (profile.name) {
+ document.l10n.setAttributes(name, "profile-source-named", {
+ profileName: profile.name,
+ });
+ } else {
+ document.l10n.setAttributes(name, "profile-source");
+ }
+ label.append(name);
+
+ let profileDetails = document.createElement("dl");
+ profileDetails.className = "result-indent tip-caption";
+ let profilePathLabel = document.createElement("dt");
+ document.l10n.setAttributes(profilePathLabel, "items-pane-directory");
+ let profilePath = document.createElement("dd");
+ profilePath.textContent = profile.dir.path;
+ profileDetails.append(profilePathLabel, profilePath);
+ label.append(profileDetails);
+
+ elProfileList.append(label);
+ }
+ document.querySelector("input[name=appProfile]").checked = true;
+ document.getElementById("profileNextButton").disabled = false;
+
+ this.showPane("profiles");
+ }
+
+ /**
+ * Handler for the Continue button on the profiles pane.
+ */
+ _onSelectProfile() {
+ let index = [
+ ...document.querySelectorAll("input[name=appProfile]"),
+ ].findIndex(el => el.checked);
+ if (this._sourceProfiles[index]) {
+ this._showItems(this._sourceProfiles[index]);
+ } else {
+ this._openFilePicker(
+ index == this._sourceProfiles.length ? "dir" : "zip"
+ );
+ }
+ }
+
+ /**
+ * Open a file picker to select a folder or a zip file.
+ *
+ * @param {'dir' | 'zip'} type - Whether to pick a folder or a zip file.
+ */
+ async _openFilePicker(type) {
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ let [filePickerTitleDir, filePickerTitleZip] =
+ await document.l10n.formatValues([
+ "profile-file-picker-directory",
+ "profile-file-picker-archive-title",
+ ]);
+ if (type == "zip") {
+ filePicker.init(window, filePickerTitleZip, filePicker.modeOpen);
+ filePicker.appendFilter("", "*.zip");
+ } else {
+ filePicker.init(window, filePickerTitleDir, filePicker.modeGetFolder);
+ }
+ let rv = await new Promise(resolve => filePicker.open(resolve));
+ if (rv != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+ let selectedFile = filePicker.file;
+ if (!selectedFile.isDirectory()) {
+ if (selectedFile.fileSize > 2147483647) {
+ // nsIZipReader only supports zip file less than 2GB.
+ this.showError("error-message-zip-file-too-big2");
+ return;
+ }
+ this._importingFromZip = true;
+ }
+ this._showItems({ dir: selectedFile });
+ }
+
+ /**
+ * Show the items pane, with a list of items to import.
+ *
+ * @param {SourceProfile} profile - The profile to import from.
+ */
+ _showItems(profile) {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showItems(profile);
+ },
+ },
+ 1
+ );
+ this._el.classList.remove("final-step", "progress");
+ this._sourceProfile = profile;
+ document.l10n.setAttributes(
+ this._el.querySelector("#app-items h1"),
+ `from-app-${this._sourceAppName}`
+ );
+ document.getElementById("appSourceProfilePath").textContent =
+ profile.dir.path;
+ document.getElementById("appSourceProfilePath").textContent =
+ this._sourceProfile.dir.path;
+ document.getElementById("appSourceProfileNameWrapper").hidden =
+ !this._sourceProfile.name;
+ if (this._sourceProfile.name) {
+ document.getElementById("appSourceProfileName").textContent =
+ this._sourceProfile.name;
+ }
+ this._setItemsChecked(this._importer.SUPPORTED_ITEMS);
+ document.getElementById("profileNextButton").disabled = Object.values(
+ this._importer.SUPPORTED_ITEMS
+ ).every(isChecked => !isChecked);
+
+ this.showPane("items");
+ }
+
+ /** A map from checkbox id to ImportItems field */
+ _itemCheckboxes = {
+ checkAccounts: "accounts",
+ checkAddressBooks: "addressBooks",
+ checkCalendars: "calendars",
+ checkMailMessages: "mailMessages",
+ };
+
+ /**
+ * Map of fluent IDs from ImportItems if they differ.
+ *
+ * @type {Object<string>}
+ */
+ _importItemFluentId = {
+ addressBooks: "address-books",
+ mailMessages: "mail-messages",
+ };
+
+ /**
+ * Set checkbox states according to an ImportItems object.
+ *
+ * @param {ImportItems} items.
+ */
+ _setItemsChecked(items) {
+ for (let [id, field] of Object.entries(this._itemCheckboxes)) {
+ let supported = items[field];
+ let checkbox = document.getElementById(id);
+ checkbox.checked = supported;
+ checkbox.disabled = !supported;
+ }
+ }
+
+ /**
+ * Construct an ImportItems object from the checkbox states.
+ *
+ * @param {boolean} [onlySupported=false] - Only return supported ImportItems.
+ * @returns {ImportItems}
+ */
+ _getItemsChecked(onlySupported = false) {
+ let items = {};
+ for (let id in this._itemCheckboxes) {
+ let checkbox = document.getElementById(id);
+ if (!onlySupported || !checkbox.disabled) {
+ items[this._itemCheckboxes[id]] = checkbox.checked;
+ }
+ }
+ return items;
+ }
+
+ /**
+ * Handler for the Continue button on the items pane.
+ */
+ _onSelectItems() {
+ let checkedItems = this._getItemsChecked(true);
+ if (Object.values(checkedItems).some(isChecked => isChecked)) {
+ this._showSummary();
+ }
+ }
+
+ _showSummary() {
+ Steps.updateSteps({}, 0);
+ this._el.classList.add("final-step");
+ document.l10n.setAttributes(
+ this._el.querySelector("#app-summary h1"),
+ `from-app-${this._sourceAppName}`
+ );
+ document.getElementById("appSummaryProfilePath").textContent =
+ this._sourceProfile.dir.path;
+ document.getElementById("appSummaryProfileNameWrapper").hidden =
+ !this._sourceProfile.name;
+ if (this._sourceProfile.name) {
+ document.getElementById("appSummaryProfileName").textContent =
+ this._sourceProfile.name;
+ }
+ document.getElementById("appSummaryItems").replaceChildren(
+ ...Object.entries(this._getItemsChecked(true))
+ .filter(([item, checked]) => checked)
+ .map(([item]) => {
+ let li = document.createElement("li");
+ let fluentId = this._importItemFluentId[item] ?? item;
+ document.l10n.setAttributes(li, `items-pane-checkbox-${fluentId}`);
+ return li;
+ })
+ );
+ this.showPane("summary");
+ }
+
+ /**
+ * Extract the zip file to a tmp dir, set _sourceProfile.dir to the tmp dir.
+ */
+ async _extractZipFile() {
+ // Extract the zip file to a tmp dir.
+ let targetDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ targetDir.append("tmp-profile");
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let ZipReader = Components.Constructor(
+ "@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader",
+ "open"
+ );
+ let zip = ZipReader(this._sourceProfile.dir);
+ for (let entry of zip.findEntries(null)) {
+ let parts = entry.split("/");
+ if (
+ this._importer.IGNORE_DIRS.includes(parts[1]) ||
+ entry.endsWith("/")
+ ) {
+ continue;
+ }
+ // Folders can not be unzipped recursively, have to iterate and
+ // extract all file entries one by one.
+ let target = targetDir.clone();
+ for (let part of parts.slice(1)) {
+ // Drop the root folder name in the zip file.
+ target.append(part);
+ }
+ if (!target.parent.exists()) {
+ target.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ try {
+ this._logger.debug(`Extracting ${entry} to ${target.path}`);
+ zip.extract(entry, target);
+ this._extractedFileCount++;
+ if (this._extractedFileCount % 10 == 0) {
+ let progress = Math.min((this._extractedFileCount / 200) * 0.2, 0.2);
+ this.updateProgress(progress);
+ await new Promise(resolve => setTimeout(resolve));
+ }
+ } catch (e) {
+ this._logger.error(e);
+ }
+ }
+ // Use the tmp dir as source profile dir.
+ this._sourceProfile = { dir: targetDir };
+ this.updateProgress(0.2);
+ }
+
+ async startImport() {
+ this.showProgress("progress-pane-importing2");
+ if (this._importingFromZip) {
+ this._extractedFileCount = 0;
+ try {
+ await this._extractZipFile();
+ } catch (e) {
+ this.showError("error-message-extract-zip-file-failed2");
+ throw e;
+ }
+ }
+ this._importer.onProgress = (current, total) => {
+ this.updateProgress(
+ this._importingFromZip ? 0.2 + (0.8 * current) / total : current / total
+ );
+ };
+ try {
+ this.finish(
+ await this._importer.startImport(
+ this._sourceProfile.dir,
+ this._getItemsChecked()
+ )
+ );
+ } catch (e) {
+ this.showError("error-message-failed");
+ throw e;
+ } finally {
+ if (this._importingFromZip) {
+ IOUtils.remove(this._sourceProfile.dir.path, { recursive: true });
+ }
+ }
+ }
+}
+
+/**
+ * Control the #tabPane-addressBook element, to support importing from an
+ * address book file.
+ */
+class AddrBookImporterController extends ImporterController {
+ constructor() {
+ super("tabPane-addressBook", "addr-book");
+ }
+
+ /**
+ * Show the next pane.
+ */
+ next() {
+ super.next();
+ switch (this._currentPane) {
+ case "sources":
+ this._onSelectSource();
+ break;
+ case "csvFieldMap":
+ this._onSubmitCsvFieldMap();
+ break;
+ case "directories":
+ this._onSelectDirectory();
+ break;
+ case "summary":
+ window.close();
+ break;
+ }
+ }
+
+ showInitialStep() {
+ this._showSources();
+ }
+
+ /**
+ * Show the sources pane.
+ */
+ _showSources() {
+ document.getElementById("addrBookBackButton").hidden =
+ !Steps.hasStepHistory();
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showSources();
+ },
+ },
+ 2
+ );
+ this.showPane("sources");
+ }
+
+ /**
+ * Handler for the Continue button on the sources pane.
+ */
+ async _onSelectSource() {
+ this._fileType = document.querySelector(
+ "input[name=addrBookSource]:checked"
+ ).value;
+ this._importer = new AddrBookFileImporter(this._fileType);
+
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ let [filePickerTitle] = await document.l10n.formatValues([
+ "addr-book-file-picker",
+ ]);
+ filePicker.init(window, filePickerTitle, filePicker.modeOpen);
+ let filter = {
+ csv: "*.csv; *.tsv; *.tab",
+ ldif: "*.ldif",
+ vcard: "*.vcf",
+ sqlite: "*.sqlite",
+ mab: "*.mab",
+ }[this._fileType];
+ if (filter) {
+ filePicker.appendFilter("", filter);
+ }
+ filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
+ let rv = await new Promise(resolve => filePicker.open(resolve));
+ if (rv != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ this._sourceFile = filePicker.file;
+ document.getElementById("addrBookSourcePath").textContent =
+ filePicker.file.path;
+
+ if (this._fileType == "csv") {
+ let unmatchedRows = await this._importer.parseCsvFile(filePicker.file);
+ if (unmatchedRows.length) {
+ document.getElementById("csvFieldMap").data = unmatchedRows;
+ this._showCsvFieldMap();
+ return;
+ }
+ }
+ this._showDirectories();
+ }
+
+ /**
+ * Show the csvFieldMap pane, user can map source CSV fields to address book
+ * fields.
+ */
+ _showCsvFieldMap() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showCsvFieldMap();
+ },
+ },
+ 2
+ );
+ document.getElementById("addrBookBackButton").hidden = false;
+ this.showPane("csvFieldMap");
+ }
+
+ /**
+ * Handler for the Continue button on the csvFieldMap pane.
+ */
+ async _onSubmitCsvFieldMap() {
+ this._importer.setCsvFields(document.getElementById("csvFieldMap").value);
+ this._showDirectories();
+ }
+
+ /**
+ * Show the directories pane, with a list of existing directories and an
+ * option to create a new directory.
+ */
+ async _showDirectories() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showDirectories();
+ },
+ },
+ 1
+ );
+ document.getElementById("addrBookBackButton").hidden = false;
+ this._el.classList.remove("final-step", "progress");
+ let sourceFileName = this._sourceFile.leafName;
+ this._fallbackABName = sourceFileName.slice(
+ 0,
+ sourceFileName.lastIndexOf(".") == -1
+ ? Infinity
+ : sourceFileName.lastIndexOf(".")
+ );
+ document.l10n.setAttributes(
+ document.getElementById("newDirectoryLabel"),
+ "addr-book-import-into-new-directory2",
+ {
+ addressBookName: this._fallbackABName,
+ }
+ );
+ let elList = document.getElementById("directoryList");
+ elList.innerHTML = "";
+ this._directories = MailServices.ab.directories.filter(
+ dir => dir.dirType == Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ for (let directory of this._directories) {
+ let label = document.createElement("label");
+ label.className = "toggle-container-with-text";
+
+ let input = document.createElement("input");
+ input.type = "radio";
+ input.name = "addrBookDirectory";
+ input.value = directory.dirPrefId;
+ label.append(input);
+
+ let name = document.createElement("div");
+ name.className = "strong";
+ name.textContent = directory.dirName;
+ label.append(name);
+
+ elList.append(label);
+ }
+ document.querySelector("input[name=addrBookDirectory]").checked = true;
+
+ this.showPane("directories");
+ }
+
+ /**
+ * Handler for the Continue button on the directories pane.
+ */
+ _onSelectDirectory() {
+ let index = [
+ ...document.querySelectorAll("input[name=addrBookDirectory]"),
+ ].findIndex(el => el.checked);
+ this._selectedAddressBook = this._directories[index];
+ this._showSummary();
+ }
+
+ _showSummary() {
+ Steps.updateSteps({}, 0);
+ this._el.classList.add("final-step");
+ document.getElementById("addrBookSummaryPath").textContent =
+ this._sourceFile.path;
+ let targetAddressBook = this._selectedAddressBook?.dirName;
+ let newAddressBook = false;
+ if (!targetAddressBook) {
+ targetAddressBook = this._fallbackABName;
+ newAddressBook = true;
+ }
+ let description = this._el.querySelector("#addr-book-summary .description");
+ description.hidden = !newAddressBook;
+ if (newAddressBook) {
+ document.l10n.setAttributes(
+ description,
+ "addr-book-summary-description",
+ {
+ addressBookName: targetAddressBook,
+ }
+ );
+ }
+ document.l10n.setAttributes(
+ document.getElementById("addrBookSummarySubtitle"),
+ "addr-book-summary-title",
+ {
+ addressBookName: targetAddressBook,
+ }
+ );
+ this.showPane("summary");
+ }
+
+ async startImport() {
+ let targetDirectory = this._selectedAddressBook;
+ if (!targetDirectory) {
+ // User selected to create a new address book and import into it. Create
+ // one based on the file name.
+ let dirId = MailServices.ab.newAddressBook(
+ this._fallbackABName,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ targetDirectory = MailServices.ab.getDirectoryFromId(dirId);
+ }
+
+ this.showProgress("progress-pane-importing2");
+ this._importer.onProgress = (current, total) => {
+ this.updateProgress(current / total);
+ };
+ try {
+ this.finish(
+ await this._importer.startImport(this._sourceFile, targetDirectory)
+ );
+ } catch (e) {
+ this.showError("error-message-failed");
+ throw e;
+ }
+ }
+}
+
+/**
+ * Control the #tabPane-calendar element, to support importing from a calendar
+ * file.
+ */
+class CalendarImporterController extends ImporterController {
+ constructor() {
+ super("tabPane-calendar", "calendar");
+ }
+
+ next() {
+ super.next();
+ switch (this._currentPane) {
+ case "sources":
+ this._onSelectSource();
+ break;
+ case "items":
+ this._onSelectItems();
+ break;
+ case "calendars":
+ this._onSelectCalendar();
+ break;
+ case "summary":
+ window.close();
+ break;
+ }
+ }
+
+ showInitialStep() {
+ this._showSources();
+ }
+
+ /**
+ * When filter changes, re-render the item list.
+ *
+ * @param {HTMLInputElement} filterInput - The filter input.
+ */
+ onFilterChange(filterInput) {
+ let term = filterInput.value.toLowerCase();
+ this._filteredItems = [];
+ for (let item of this._items) {
+ let element = this._itemElements[item.id];
+ if (item.title.toLowerCase().includes(term)) {
+ element.hidden = false;
+ this._filteredItems.push(item);
+ } else {
+ element.hidden = true;
+ }
+ }
+ }
+
+ /**
+ * Select or deselect all visible items.
+ *
+ * @param {boolean} selected - Select all if true, otherwise deselect all.
+ */
+ selectAllItems(selected) {
+ for (let item of this._filteredItems) {
+ let element = this._itemElements[item.id];
+ element.querySelector("input").checked = selected;
+ if (selected) {
+ this._selectedItems.add(item);
+ } else {
+ this._selectedItems.delete(item);
+ }
+ }
+ document.getElementById("calendarNextButton").disabled =
+ this._selectedItems.size == 0;
+ }
+
+ /**
+ * Show the sources pane.
+ */
+ _showSources() {
+ document.getElementById("calendarBackButton").hidden =
+ !Steps.hasStepHistory();
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showSources();
+ },
+ },
+ 3
+ );
+ this.showPane("sources");
+ }
+
+ /**
+ * Handler for the Continue button on the sources pane.
+ */
+ async _onSelectSource() {
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ filePicker.appendFilter("", "*.ics");
+ filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
+ filePicker.init(
+ window,
+ await document.l10n.formatValue("file-calendar-description"),
+ filePicker.modeOpen
+ );
+ let rv = await new Promise(resolve => filePicker.open(resolve));
+ if (rv != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ this._sourceFile = filePicker.file;
+ this._importer = new CalendarFileImporter();
+
+ document.getElementById("calendarSourcePath").textContent =
+ filePicker.file.path;
+
+ this._showItems();
+ }
+
+ /**
+ * Show the sources pane.
+ */
+ async _showItems() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showItems();
+ },
+ },
+ 2
+ );
+ document.getElementById("calendarBackButton").hidden = false;
+ let elItemList = document.getElementById("calendar-item-list");
+ document.getElementById("calendarItemsTools").hidden = true;
+ document.l10n.setAttributes(elItemList, "calendar-items-loading");
+ this.showPane("items");
+
+ // Give the UI a chance to render.
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ try {
+ this._items = await this._importer.parseIcsFile(this._sourceFile);
+ } catch (e) {
+ this.showError("error-failed-to-parse-ics-file");
+ throw e;
+ }
+
+ document.getElementById("calendarItemsTools").hidden =
+ this._items.length < 2;
+ elItemList.innerHTML = "";
+ this._filteredItems = this._items;
+ this._selectedItems = new Set(this._items);
+ this._itemElements = {};
+
+ for (let item of this._items) {
+ let wrapper = document.createElement("div");
+ wrapper.className = "calendar-item-wrapper";
+ elItemList.appendChild(wrapper);
+ this._itemElements[item.id] = wrapper;
+
+ let summary = document.createXULElement("calendar-item-summary");
+ wrapper.appendChild(summary);
+ summary.item = item;
+ summary.updateItemDetails();
+
+ let input = document.createElement("input");
+ input.type = "checkbox";
+ input.checked = true;
+ wrapper.appendChild(input);
+
+ wrapper.addEventListener("click", e => {
+ if (e.target != input) {
+ input.checked = !input.checked;
+ }
+ if (input.checked) {
+ this._selectedItems.add(item);
+ } else {
+ this._selectedItems.delete(item);
+ }
+ document.getElementById("calendarNextButton").disabled =
+ this._selectedItems.size == 0;
+ });
+ }
+ }
+
+ /**
+ * Handler for the Continue button on the items pane.
+ */
+ _onSelectItems() {
+ this._showCalendars();
+ }
+
+ /**
+ * Show the calendars pane, with a list of existing writable calendars and an
+ * option to create a new calendar.
+ */
+ _showCalendars() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ this._showCalendars();
+ },
+ },
+ 1
+ );
+ this._el.classList.remove("final-step", "progress");
+ document.getElementById("calendarCalPath").textContent =
+ this._sourceFile.path;
+ let elList = document.getElementById("calendarList");
+ elList.innerHTML = "";
+
+ let sourceFileName = this._sourceFile.leafName;
+ this._fallbackCalendarName = sourceFileName.slice(
+ 0,
+ sourceFileName.lastIndexOf(".") == -1
+ ? Infinity
+ : sourceFileName.lastIndexOf(".")
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("newCalendarLabel"),
+ "calendar-import-into-new-calendar2",
+ {
+ targetCalendar: this._fallbackCalendarName,
+ }
+ );
+
+ this._calendars = this._importer.getTargetCalendars();
+ for (let calendar of this._calendars) {
+ let label = document.createElement("label");
+ label.className = "toggle-container-with-text";
+
+ let input = document.createElement("input");
+ input.type = "radio";
+ input.name = "targetCalendar";
+ input.value = calendar.id;
+ label.append(input);
+
+ let name = document.createElement("div");
+ name.className = "strong";
+ name.textContent = calendar.name;
+ label.append(name);
+
+ elList.append(label);
+ }
+ document.querySelector("input[name=targetCalendar]").checked = true;
+
+ this.showPane("calendars");
+ }
+
+ _onSelectCalendar() {
+ let index = [
+ ...document.querySelectorAll("input[name=targetCalendar]"),
+ ].findIndex(el => el.checked);
+ this._selectedCalendar = this._calendars[index];
+ this._showSummary();
+ }
+
+ _showSummary() {
+ Steps.updateSteps({}, 0);
+ this._el.classList.add("final-step");
+ document.getElementById("calendarSummaryPath").textContent =
+ this._sourceFile.path;
+ let targetCalendar = this._selectedCalendar?.name;
+ let newCalendar = false;
+ if (!targetCalendar) {
+ targetCalendar = this._fallbackCalendarName;
+ newCalendar = true;
+ }
+ let description = this._el.querySelector("#calendar-summary .description");
+ description.hidden = !newCalendar;
+ if (newCalendar) {
+ document.l10n.setAttributes(description, "calendar-summary-description", {
+ targetCalendar,
+ });
+ }
+ document.l10n.setAttributes(
+ document.getElementById("calendarSummarySubtitle"),
+ "calendar-summary-title",
+ {
+ itemCount: this._selectedItems.size,
+ targetCalendar,
+ }
+ );
+ this.showPane("summary");
+ }
+
+ /**
+ * Handler for the Continue button on the calendars pane.
+ */
+ async startImport() {
+ let targetCalendar = this._selectedCalendar;
+ if (!targetCalendar) {
+ // Create a new calendar.
+ targetCalendar = cal.manager.createCalendar(
+ "storage",
+ Services.io.newURI("moz-storage-calendar://")
+ );
+ targetCalendar.name = this._fallbackCalendarName;
+ cal.manager.registerCalendar(targetCalendar);
+ }
+ this.showProgress("progress-pane-importing2");
+ this._importer.onProgress = (current, total) => {
+ this.updateProgress(current / total);
+ };
+ try {
+ await this._importer.startImport(
+ [...this._selectedItems],
+ targetCalendar
+ );
+ this.finish();
+ } catch (e) {
+ this.showError("error-message-failed");
+ throw e;
+ }
+ }
+}
+
+/**
+ * Control the #tabPane-export element, to support exporting the current profile
+ * to a zip file.
+ */
+class ExportController extends ImporterController {
+ constructor() {
+ super("tabPane-export", "");
+ }
+
+ back() {
+ window.close();
+ }
+
+ async next() {
+ super.next();
+ let [filePickerTitle, brandName] = await document.l10n.formatValues([
+ "export-file-picker2",
+ "export-brand-name",
+ ]);
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ filePicker.init(window, filePickerTitle, Ci.nsIFilePicker.modeSave);
+ filePicker.defaultString = `${brandName}_profile_backup.zip`;
+ filePicker.defaultExtension = "zip";
+ filePicker.appendFilter("", "*.zip");
+ let rv = await new Promise(resolve => filePicker.open(resolve));
+ if (
+ ![Ci.nsIFilePicker.returnOK, Ci.nsIFilePicker.returnReplace].includes(rv)
+ ) {
+ return;
+ }
+
+ let exporter = new ProfileExporter();
+ this.showProgress("progress-pane-exporting2");
+ exporter.onProgress = (current, total) => {
+ this.updateProgress(current / total);
+ };
+ try {
+ await exporter.startExport(filePicker.file);
+ this.finish();
+ } catch (e) {
+ this.showError("error-export-failed");
+ throw e;
+ }
+ }
+
+ openProfileFolder() {
+ Services.dirsvc.get("ProfD", Ci.nsIFile).reveal();
+ }
+}
+
+class StartController extends ImporterController {
+ constructor() {
+ super("tabPane-start", "start");
+ }
+
+ next() {
+ super.next();
+ switch (this._currentPane) {
+ case "sources":
+ this._onSelectSource();
+ break;
+ case "file":
+ this._onSelectFile();
+ break;
+ }
+ }
+
+ showInitialStep() {
+ this._showSources();
+ }
+
+ /**
+ * Show the sources pane.
+ */
+ _showSources() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ showTab("tab-start");
+ //showTab will always call showInitialStep
+ },
+ },
+ 3
+ );
+ document.getElementById("startBackButton").hidden = true;
+ this.showPane("sources");
+ }
+
+ /**
+ * Handler for the Continue button on the sources pane.
+ */
+ async _onSelectSource() {
+ let checkedInput = document.querySelector("input[name=appSource]:checked");
+
+ switch (checkedInput.value) {
+ case "file":
+ this._showFile();
+ break;
+ default:
+ await profileController._onSelectSource(checkedInput.value);
+ showTab("tab-app");
+ // Don't change back button state, since we switch to app flow.
+ return;
+ }
+
+ document.getElementById("startBackButton").hidden = false;
+ }
+
+ _showFile() {
+ Steps.updateSteps(
+ {
+ returnTo: () => {
+ this.reset();
+ showTab("tab-start");
+ this._showFile();
+ },
+ },
+ 3
+ );
+ this.showPane("file");
+ }
+
+ async _onSelectFile() {
+ let checkedInput = document.querySelector("input[name=startFile]:checked");
+ switch (checkedInput.value) {
+ case "profile":
+ // Go to the import profile from zip file step in profile flow for TB.
+ profileController.reset();
+ await profileController._onSelectSource("Thunderbird");
+ document.getElementById("appFilePickerZip").checked = true;
+ await profileController._onSelectProfile();
+ showTab("tab-app");
+ break;
+ case "calendar":
+ calendarController.reset();
+ showTab("tab-calendar");
+ calendarController.showInitialStep();
+ await calendarController._onSelectSource();
+ break;
+ case "addressbook":
+ addrBookController.reset();
+ showTab("tab-addressBook");
+ addrBookController.showInitialStep();
+ break;
+ }
+ }
+}
+
+/**
+ * Show a specific importing tab.
+ *
+ * @param {"tab-app"|"tab-addressBook"|"tab-calendar"|"tab-export"|"tab-start"} tabId -
+ * Tab to show.
+ * @param {boolean} [reset=false] - If the state should be reset as if this was
+ * the initial tab shown.
+ */
+function showTab(tabId, reset = false) {
+ if (reset) {
+ Steps.reset();
+ restart();
+ }
+ let selectedPaneId = `tabPane-${tabId.split("-")[1]}`;
+ let isExport = tabId === "tab-export";
+ document.getElementById("importDocs").hidden = isExport;
+ document.getElementById("exportDocs").hidden = !isExport;
+ Steps.toggle(!isExport);
+ document.l10n.setAttributes(
+ document.querySelector("title"),
+ isExport ? "export-page-title" : "import-page-title"
+ );
+ document.querySelector("link[rel=icon]").href = isExport
+ ? "chrome://messenger/skin/icons/new/compact/export.svg"
+ : "chrome://messenger/skin/icons/new/compact/import.svg";
+ location.hash = isExport ? "export" : "";
+ for (let tabPane of document.querySelectorAll("[id^=tabPane-]")) {
+ tabPane.hidden = tabPane.id != selectedPaneId;
+ }
+ for (let el of document.querySelectorAll("[id^=tab-]")) {
+ el.classList.toggle("is-selected", el.id == tabId);
+ }
+ if (!Steps.hasStepHistory()) {
+ switch (tabId) {
+ case "tab-start":
+ startController.showInitialStep();
+ break;
+ case "tab-addressBook":
+ addrBookController.showInitialStep();
+ break;
+ case "tab-calendar":
+ calendarController.showInitialStep();
+ break;
+ default:
+ }
+ }
+}
+
+/**
+ * Restart the import wizard. Resets all previous choices.
+ */
+function restart() {
+ startController.reset();
+ profileController.reset();
+ addrBookController.reset();
+ calendarController.reset();
+ Steps.backTo(0);
+}
+
+let profileController;
+let addrBookController;
+let calendarController;
+let exportController;
+let startController;
+
+document.addEventListener("DOMContentLoaded", () => {
+ profileController = new ProfileImporterController();
+ addrBookController = new AddrBookImporterController();
+ calendarController = new CalendarImporterController();
+ exportController = new ExportController();
+ startController = new StartController();
+ showTab(location.hash === "#export" ? "tab-export" : "tab-start", true);
+});
diff --git a/comm/mailnews/import/content/aboutImport.xhtml b/comm/mailnews/import/content/aboutImport.xhtml
new file mode 100644
index 0000000000..e4a259d1f5
--- /dev/null
+++ b/comm/mailnews/import/content/aboutImport.xhtml
@@ -0,0 +1,477 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+ <title data-l10n-id="import-page-title"></title>
+
+ <link rel="stylesheet" href="chrome://messenger/skin/messenger.css"/>
+ <link rel="stylesheet" href="chrome://global/skin/global.css"/>
+ <link rel="stylesheet" href="chrome://calendar/skin/shared/calendar-attendees.css"/>
+ <link rel="stylesheet" href="chrome://calendar/skin/shared/calendar-item-summary.css"/>
+ <link rel="stylesheet" href="chrome://messenger/skin/accountSetup.css"/>
+ <link rel="stylesheet" href="chrome://messenger/skin/aboutImport.css"/>
+
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/aboutImport.ftl"/>
+
+ <link rel="icon" href="chrome://messenger/skin/icons/new/compact/import.svg" sizes="any"/>
+
+ <script defer="" src="chrome://messenger/content/aboutImport.js"></script>
+ <script defer="" src="chrome://messenger/content/csv-field-map.js"></script>
+ <script defer="" src="chrome://calendar/content/widgets/calendar-item-summary.js"></script>
+</head>
+<body>
+ <main id="main">
+ <nav>
+ <ol id="stepNav" data-l10n-id="step-list">
+ <li id="navConfirm">
+ <button data-l10n-id="step-confirm" disabled="disabled"></button>
+ </li>
+ </ol>
+ </nav>
+ <section id="errorNotifications"></section>
+ <div id="tabPane-start" class="tabPane">
+ <section id="start-sources">
+ <h1 id="startSource" data-l10n-id="import-start"></h1>
+ <h2 data-l10n-id="import-start-title"></h2>
+ <p data-l10n-id="import-start-description" class="description"></p>
+ <div class="source-list indent">
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="Thunderbird"
+ name="appSource"
+ checked="checked"/>
+ <p data-l10n-id="source-thunderbird"></p>
+ <p class="tip-caption" data-l10n-id="source-thunderbird-description"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="Seamonkey"
+ name="appSource"/>
+ <p data-l10n-id="source-seamonkey"></p>
+ <p class="tip-caption" data-l10n-id="source-seamonkey-description"></p>
+ </label>
+#ifdef XP_WIN
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="Outlook"
+ name="appSource"/>
+ <p data-l10n-id="source-outlook"></p>
+ <p class="tip-caption" data-l10n-id="source-outlook-description"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="Becky"
+ name="appSource"/>
+ <p data-l10n-id="source-becky"></p>
+ <p class="tip-caption" data-l10n-id="source-becky-description"></p>
+ </label>
+#endif
+#ifdef XP_MACOSX
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="AppleMail"
+ name="appSource"/>
+ <p data-l10n-id="source-apple-mail"></p>
+ <p class="tip-caption" data-l10n-id="source-apple-mail-description"></p>
+ </label>
+#endif
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="file"
+ name="appSource"/>
+ <p data-l10n-id="source-file2"></p>
+ <p class="tip-caption" data-l10n-id="source-file-description"></p>
+ </label>
+ </div>
+ </section>
+ <section id="start-file">
+ <h1 id="startFile" data-l10n-id="import-file"></h1>
+ <h2 data-l10n-id="import-file-title"></h2>
+ <p data-l10n-id="import-file-description" class="description"></p>
+ <div class="option-list indent">
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="profile"
+ name="startFile"
+ checked="checked"/>
+ <p data-l10n-id="file-profile2"></p>
+ <p data-l10n-id="file-profile-description" class="tip-caption"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="calendar"
+ name="startFile"/>
+ <p data-l10n-id="file-calendar"></p>
+ <p data-l10n-id="file-calendar-description" class="tip-caption"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="addressbook"
+ name="startFile"/>
+ <p data-l10n-id="file-addressbook"></p>
+ <p data-l10n-id="file-addressbook-description" class="tip-caption"></p>
+ </label>
+ </div>
+ </section>
+ <footer class="buttons-container">
+ <button id="startBackButton"
+ class="back"
+ onclick="startController.back()"
+ data-l10n-id="button-back"></button>
+ <button class="primary continue"
+ onclick="startController.next()"
+ data-l10n-id="button-continue"></button>
+ </footer>
+ </div>
+ <div id="tabPane-app" class="tabPane restart-required">
+ <section id="app-profiles">
+ <h1 id="profilesPaneTitle" data-l10n-id="import-from-app"></h1>
+ <h2 id="profilesPaneSubtitle"></h2>
+ <div class="profile-list indent" id="profileList"></div>
+ <div class="profile-list indent" id="filePickerList">
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="file-picker-dir"
+ name="appProfile"/>
+ <p data-l10n-id="profile-file-picker-directory"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="file-picker-zip"
+ name="appProfile"
+ id="appFilePickerZip"/>
+ <p data-l10n-id="profile-file-picker-archive"></p>
+ <p data-l10n-id="profile-file-picker-archive-description" class="tip-caption"></p>
+ </label>
+ </div>
+ </section>
+ <section id="app-items">
+ <h1 data-l10n-id="import-from-app"></h1>
+ <dl>
+ <div id="appSourceProfileNameWrapper">
+ <dt data-l10n-id="items-pane-profile-name"></dt>
+ <dd id="appSourceProfileName"></dd>
+ </div>
+ <dt data-l10n-id="items-pane-directory"></dt>
+ <dd id="appSourceProfilePath"></dd>
+ </dl>
+ <h2 data-l10n-id="items-pane-title2" class="light-heading"></h2>
+ <p>
+ <img src="chrome://messenger/skin/icons/new/compact/info.svg"
+ class="info icon"
+ alt=""/>
+ <span data-l10n-id="items-pane-override"></span>
+ </p>
+ <div class="option-list indent" id="appItemsList">
+ <label class="toggle-container-with-text">
+ <input type="checkbox" id="checkAccounts"/>
+ <span data-l10n-id="items-pane-checkbox-accounts"></span>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="checkbox" id="checkAddressBooks"/>
+ <span data-l10n-id="items-pane-checkbox-address-books"></span>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="checkbox" id="checkCalendars"/>
+ <span data-l10n-id="items-pane-checkbox-calendars"></span>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="checkbox" id="checkMailMessages"/>
+ <span data-l10n-id="items-pane-checkbox-mail-messages"></span>
+ </label>
+ </div>
+ <p class="center">
+ <img src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ class="icon warn"
+ alt=""/>
+ <span data-l10n-id="summary-pane-warning"></span>
+ </p>
+ </section>
+ <section id="app-summary">
+ <h1 data-l10n-id="import-from-app"></h1>
+ <dl>
+ <div id="appSummaryProfileNameWrapper">
+ <dt data-l10n-id="items-pane-profile-name"></dt>
+ <dd id="appSummaryProfileName"></dd>
+ </div>
+ <dt data-l10n-id="items-pane-directory"></dt>
+ <dd id="appSummaryProfilePath"></dd>
+ </dl>
+ <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2>
+ <ul id="appSummaryItems" class="summary-items indent">
+ </ul>
+ <button id="appStartImport"
+ onclick="profileController.startImport()"
+ class="primary before-progress center-button"
+ data-l10n-id="summary-pane-start"></button>
+ <div class="progressPane">
+ <section class="progressPane-progress">
+ <progress class="progressPaneProgressBar"></progress>
+ <p class="progressPaneDesc tip-caption"></p>
+ </section>
+ </div>
+ <button class="progressFinish primary center-button"
+ onclick="profileController.next()"
+ data-l10n-id="button-finish"></button>
+ <button data-l10n-id="summary-pane-start-over"
+ class="progressFinish no-restart center-button btn-link"
+ onclick="restart()"></button>
+ <p class="restart-only center">
+ <img src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ class="icon warn"
+ alt=""/>
+ <span data-l10n-id="summary-pane-warning"></span>
+ </p>
+ </section>
+ <footer class="buttons-container">
+ <button id="profileBackButton"
+ class="back"
+ onclick="profileController.back()"
+ data-l10n-id="button-back"></button>
+ <button id="profileNextButton"
+ class="primary next-button"
+ onclick="profileController.next()"
+ data-l10n-id="button-continue"></button>
+ </footer>
+ </div>
+
+ <div id="tabPane-addressBook" class="tabPane">
+ <section id="addr-book-sources">
+ <h1 id="importAddressBook" data-l10n-id="import-address-book-title"></h1>
+ <h2 data-l10n-id="import-file-title"></h2>
+ <p data-l10n-id="import-from-addr-book-file-description" class="description"></p>
+ <div class="source-list indent">
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="csv"
+ name="addrBookSource"
+ checked=""/>
+ <p data-l10n-id="addr-book-csv-file"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="ldif"
+ name="addrBookSource"/>
+ <p data-l10n-id="addr-book-ldif-file"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="vcard"
+ name="addrBookSource"/>
+ <p data-l10n-id="addr-book-vcard-file"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="sqlite"
+ name="addrBookSource"/>
+ <p data-l10n-id="addr-book-sqlite-file"></p>
+ </label>
+ <label class="toggle-container-with-text">
+ <input type="radio"
+ value="mab"
+ name="addrBookSource"/>
+ <p data-l10n-id="addr-book-mab-file"></p>
+ </label>
+ </div>
+ </section>
+ <section id="addr-book-csvFieldMap">
+ <h1 data-l10n-id="import-address-book-title"></h1>
+ <h2 data-l10n-id="addr-book-csv-field-map-title"></h2>
+ <p data-l10n-id="addr-book-csv-field-map-desc" class="description"></p>
+ <csv-field-map id="csvFieldMap"/>
+ </section>
+ <section id="addr-book-directories">
+ <h1 data-l10n-id="import-address-book-title"></h1>
+ <h2 data-l10n-id="addr-book-directories-title"></h2>
+ <dl>
+ <dt data-l10n-id="addr-book-directories-pane-source"></dt>
+ <dd id="addrBookSourcePath"></dd>
+ </dl>
+ <div class="profile-list indent" id="directoryList"></div>
+ <label class="toggle-container-with-text indent">
+ <input type="radio"
+ value=".new"
+ name="addrBookDirectory"/>
+ <p id="newDirectoryLabel"></p>
+ </label>
+ </section>
+ <section id="addr-book-summary">
+ <h1 data-l10n-id="import-address-book-title"></h1>
+ <h2 id="addrBookSummarySubtitle"></h2>
+ <p class="description"></p>
+ <dl>
+ <dt data-l10n-id="addr-book-directories-pane-source"></dt>
+ <dd id="addrBookSummaryPath"></dd>
+ </dl>
+ <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2>
+ <ul class="summary-items indent">
+ <li data-l10n-id="items-pane-checkbox-address-books"></li>
+ </ul>
+ <button id="addrBookStartImport"
+ onclick="addrBookController.startImport()"
+ class="primary before-progress center-button"
+ data-l10n-id="summary-pane-start"></button>
+ <div class="progressPane">
+ <section class="progressPane-progress">
+ <progress class="progressPaneProgressBar"></progress>
+ <p class="progressPaneDesc tip-caption"></p>
+ </section>
+ </div>
+ <button class="progressFinish primary center-button"
+ onclick="addrBookController.next()"
+ data-l10n-id="button-finish"></button>
+ <button data-l10n-id="summary-pane-start-over"
+ class="progressFinish no-restart center-button btn-link"
+ onclick="restart()"></button>
+ <p class="restart-only center">
+ <img src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ class="icon warn"
+ alt=""/>
+ <span data-l10n-id="summary-pane-warning"></span>
+ </p>
+ </section>
+ <footer class="buttons-container">
+ <button id="addrBookBackButton"
+ class="back"
+ onclick="addrBookController.back()"
+ data-l10n-id="button-back"></button>
+ <button class="primary next-button continue"
+ id="addrBookNextButton"
+ onclick="addrBookController.next()"
+ data-l10n-id="button-continue"></button>
+ </footer>
+ </div>
+
+ <div id="tabPane-calendar" class="tabPane">
+ <section id="calendar-sources">
+ <h1 data-l10n-id="import-calendar-title"></h1>
+ <h2 data-l10n-id="import-from-calendar-file-desc"></h2>
+ </section>
+ <section id="calendar-items">
+ <h1 data-l10n-id="import-calendar-title"></h1>
+ <dl>
+ <dt data-l10n-id="addr-book-directories-pane-source"></dt>
+ <dd id="calendarSourcePath"></dd>
+ </dl>
+ <h2 data-l10n-id="calendar-items-title"></h2>
+ <div id="calendarItemsTools">
+ <input type="search"
+ data-l10n-id="calendar-items-filter-input"
+ oninput="calendarController.onFilterChange(this)"/>
+ <button data-l10n-id="calendar-deselect-all-items"
+ onclick="calendarController.selectAllItems(false)"></button>
+ <button data-l10n-id="calendar-select-all-items"
+ onclick="calendarController.selectAllItems(true)"></button>
+ </div>
+ <div id="calendar-item-list"></div>
+ </section>
+ <section id="calendar-calendars">
+ <h1 data-l10n-id="import-calendar-title"></h1>
+ <dl>
+ <dt data-l10n-id="addr-book-directories-pane-source"></dt>
+ <dd id="calendarCalPath"></dd>
+ </dl>
+ <h2 data-l10n-id="calendar-target-title"></h2>
+ <div class="profile-list indent" id="calendarList"></div>
+ <label class="toggle-container-with-text indent">
+ <input type="radio"
+ value=".new"
+ name="targetCalendar"/>
+ <p id="newCalendarLabel"></p>
+ </label>
+ </section>
+ <section id="calendar-summary">
+ <h1 data-l10n-id="import-calendar-title"></h1>
+ <h2 id="calendarSummarySubtitle"></h2>
+ <p class="description"></p>
+ <dl>
+ <dt data-l10n-id="addr-book-directories-pane-source"></dt>
+ <dd id="calendarSummaryPath"></dd>
+ </dl>
+ <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2>
+ <ul class="summary-items indent">
+ <li data-l10n-id="items-pane-checkbox-calendars"></li>
+ </ul>
+ <button id="calendarStartImport"
+ onclick="calendarController.startImport()"
+ class="primary before-progress center-button"
+ data-l10n-id="summary-pane-start"></button>
+ <div class="progressPane">
+ <section class="progressPane-progress">
+ <progress class="progressPaneProgressBar"></progress>
+ <p class="progressPaneDesc tip-caption"></p>
+ </section>
+ </div>
+ <button class="progressFinish primary center-button"
+ onclick="calendarController.next()"
+ data-l10n-id="button-finish"></button>
+ <button data-l10n-id="summary-pane-start-over"
+ class="progressFinish no-restart center-button btn-link"
+ onclick="restart()"></button>
+ <p class="restart-only center">
+ <img src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ class="icon warn"
+ alt=""/>
+ <span data-l10n-id="summary-pane-warning"></span>
+ </p>
+ </section>
+ <footer class="buttons-container">
+ <button id="calendarBackButton"
+ class="back"
+ onclick="calendarController.back()"
+ data-l10n-id="button-back"></button>
+ <button class="primary next-button continue"
+ id="calendarNextButton"
+ onclick="calendarController.next()"
+ data-l10n-id="button-continue"></button>
+ </footer>
+ </div>
+
+ <div id="tabPane-export" class="tabPane">
+ <section>
+ <h1 data-l10n-id="export-profile"></h1>
+ <h2 data-l10n-id="export-profile-title"></h2>
+ <p class="description">
+ <img src="chrome://messenger/skin/icons/new/compact/info.svg"
+ class="info icon"
+ alt=""/>
+ <span data-l10n-id="export-profile-description"></span>
+ <button data-l10n-id="export-open-profile-folder"
+ onclick="exportController.openProfileFolder()"
+ class="btn-link"></button>
+ </p>
+ <button id="exportButton"
+ onclick="exportController.next()"
+ class="primary before-progress center-button"
+ data-l10n-id="button-export"></button>
+ <div class="progressPane">
+ <section class="progressPane-progress">
+ <progress class="progressPaneProgressBar"></progress>
+ <p class="progressPaneDesc tip-caption"></p>
+ </section>
+ </div>
+ <button class="progressFinish primary center-button"
+ onclick="exportController.back()"
+ data-l10n-id="button-finish"></button>
+ </section>
+ </div>
+
+ <footer id="importFooter" class="tip-caption">
+ <p data-l10n-id="footer-help"></p>
+ <a id="importDocs"
+ data-l10n-id="footer-import-documentation"
+ href="https://support.mozilla.org/kb/thunderbird-import"></a>
+ <a id="exportDocs"
+ data-l10n-id="footer-export-documentation"
+ href="https://support.mozilla.org/kb/thunderbird-export"></a>
+ -
+ <a data-l10n-id="footer-support-forum"
+ href="https://support.mozilla.org/products/thunderbird"></a>
+ </footer>
+ </main>
+</body>
+</html>
diff --git a/comm/mailnews/import/content/csv-field-map.js b/comm/mailnews/import/content/csv-field-map.js
new file mode 100644
index 0000000000..544a9cf50c
--- /dev/null
+++ b/comm/mailnews/import/content/csv-field-map.js
@@ -0,0 +1,280 @@
+/* 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 { exportAttributes } = ChromeUtils.import(
+ "resource:///modules/AddrBookUtils.jsm"
+);
+
+/**
+ * A component to config the mapping between CSV fields and address book fields.
+ * For each CSV field, there is a <select> with address book fields as options.
+ * If an address book field is selected for one CSV field, it can't be used for
+ * another CSV field.
+ */
+class CsvFieldMap extends HTMLElement {
+ /** Render the first two rows from the source CSV data. */
+ DATA_ROWS_LIMIT = 2;
+
+ /** @type {string[]} - The indexes of target address book fields. */
+ get value() {
+ return [...this._elTbody.querySelectorAll("select")].map(
+ select => select.value
+ );
+ }
+
+ /** @type {string[][]} - An array of rows, each row is an array of columns. */
+ set data(rows) {
+ this._init();
+ this._rows = rows.slice(0, this.DATA_ROWS_LIMIT);
+ this._render();
+ }
+
+ /**
+ * Init internal states.
+ */
+ _init() {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+ this._supportedFields = [];
+ for (let [, stringId] of exportAttributes) {
+ if (stringId) {
+ this._supportedFields.push(bundle.GetStringFromID(stringId));
+ }
+ }
+ // Create an index array ["0", "1", "2", ..., "<length -1>"].
+ this._allFieldIndexes = Array.from({
+ length: this._supportedFields.length,
+ }).map((_, index) => index.toString());
+ }
+
+ /**
+ * Init <option> list for all <select> elements.
+ */
+ _initSelectOptions() {
+ let fields;
+ let fieldIndexes = Services.prefs.getCharPref("mail.import.csv.fields", "");
+ if (fieldIndexes) {
+ // If the user has done CSV importing before, show the same field mapping.
+ fieldIndexes = fieldIndexes.split(",");
+ fields = fieldIndexes.map(i => (i == "" ? i : this._supportedFields[+i]));
+ } else {
+ // Show the same field orders as in an exported CSV file.
+ fields = this._supportedFields;
+ fieldIndexes = this._allFieldIndexes;
+ }
+
+ let i = 0;
+ for (let select of this.querySelectorAll("select")) {
+ if (fields[i]) {
+ let option = document.createElement("option");
+ option.value = fieldIndexes[i];
+ option.textContent = fields[i];
+ select.add(option);
+ } else {
+ select.disabled = true;
+ select
+ .closest("tr")
+ .querySelector("input[type=checkbox]").checked = false;
+ }
+ i++;
+ }
+
+ this._updateSelectOptions();
+ }
+
+ /**
+ * When a <select> is disabled, we remove all its options. This function is to
+ * add all available options back.
+ *
+ * @param {HTMLSelectElement} select - The <select> element.
+ */
+ _enableSelect(select) {
+ let selects = [...this._elTbody.querySelectorAll("select")];
+ let selectedFieldIndexes = selects.map(select => select.value);
+ let availableFieldIndexes = this._allFieldIndexes.filter(
+ index => !selectedFieldIndexes.includes(index)
+ );
+ for (let i = 0; i < availableFieldIndexes.length; i++) {
+ let option = document.createElement("option");
+ option.value = availableFieldIndexes[i];
+ option.textContent = this._supportedFields[option.value];
+ select.add(option);
+ }
+ }
+
+ /**
+ * Update the options of all <select> elements. The result is if an option is
+ * selected by a <select>, this option should no longer be shown as an option
+ * for other <select>.
+ *
+ * @param {HTMLSelectElement} [changedSelect] - This param is present only
+ * when an option is selected, we don't need to update the options of this
+ * <select> element.
+ */
+ _updateSelectOptions(changedSelect) {
+ let selects = [...this._elTbody.querySelectorAll("select")];
+ let selectedFieldIndexes = selects.map(select => select.value);
+ let availableFieldIndexes = this._allFieldIndexes.filter(
+ index => !selectedFieldIndexes.includes(index)
+ );
+
+ for (let select of selects) {
+ if (select.disabled || select == changedSelect) {
+ continue;
+ }
+ for (let i = select.options.length - 1; i >= 0; i--) {
+ // Remove unselected options first.
+ if (i != select.selectedIndex) {
+ select.remove(i);
+ }
+ }
+ for (let i = 0; i < availableFieldIndexes.length; i++) {
+ // Add all available options.
+ let option = document.createElement("option");
+ option.value = availableFieldIndexes[i];
+ option.textContent = this._supportedFields[option.value];
+ select.add(option);
+ }
+ }
+ }
+
+ /**
+ * Handle the change event of <select> and <input type="checkbox">.
+ */
+ _bindEvents() {
+ this._elTbody.addEventListener("change", e => {
+ let el = e.target;
+ if (el.tagName == "select") {
+ this._updateSelectOptions(el);
+ } else if (el.tagName == "input" && el.type == "checkbox") {
+ let select = el.closest("tr").querySelector("select");
+ select.disabled = !el.checked;
+ if (select.disabled) {
+ // Because it's disabled, remove all the options.
+ for (let i = select.options.length - 1; i >= 0; i--) {
+ select.remove(i);
+ }
+ } else {
+ this._enableSelect(select);
+ }
+ this._updateSelectOptions();
+ }
+ });
+ }
+
+ /**
+ * Render the table structure.
+ */
+ async _renderLayout() {
+ this.innerHTML = "";
+ let [
+ firstRowContainsHeaders,
+ sourceField,
+ sourceFirstRecord,
+ sourceSecondRecord,
+ targetField,
+ ] = await document.l10n.formatValues([
+ "csv-first-row-contains-headers",
+ "csv-source-field",
+ "csv-source-first-record",
+ "csv-source-second-record",
+ "csv-target-field",
+ ]);
+
+ let label = document.createElement("label");
+ label.className = "toggle-container-with-text";
+ let checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.checked = Services.prefs.getBoolPref(
+ "mail.import.csv.skipfirstrow",
+ true
+ );
+ let labelText = document.createElement("span");
+ labelText.textContent = firstRowContainsHeaders;
+ label.appendChild(checkbox);
+ label.appendChild(labelText);
+ this.appendChild(label);
+
+ let table = document.createElement("table");
+
+ let thead = document.createElement("thead");
+ let tr = document.createElement("tr");
+ let headers = [];
+ for (let colName of [sourceField, sourceFirstRecord, targetField, ""]) {
+ let th = document.createElement("th");
+ th.textContent = colName;
+ tr.appendChild(th);
+ headers.push(th);
+ }
+ thead.appendChild(tr);
+ table.appendChild(thead);
+
+ this._elTbody = document.createElement("tbody");
+ table.appendChild(this._elTbody);
+
+ this.appendChild(table);
+ this._bindEvents();
+
+ checkbox.addEventListener("change", () => {
+ if (checkbox.checked) {
+ headers[0].textContent = sourceField;
+ headers[1].textContent = sourceFirstRecord;
+ } else {
+ headers[0].textContent = sourceFirstRecord;
+ headers[1].textContent = sourceSecondRecord;
+ }
+ Services.prefs.setBoolPref(
+ "mail.import.csv.skipfirstrow",
+ checkbox.checked
+ );
+ });
+ }
+
+ /**
+ * Render the table content. Each row contains four columns:
+ * Source field | Source Data | Address book field | <checkbox>
+ */
+ _renderTable() {
+ let colCount = this._rows[0].length;
+ for (let i = 0; i < colCount; i++) {
+ let tr = document.createElement("tr");
+
+ // Render the source field name and source data.
+ for (let j = 0; j < this.DATA_ROWS_LIMIT; j++) {
+ let td = document.createElement("td");
+ td.textContent = this._rows[j]?.[i] || "";
+ tr.appendChild(td);
+ }
+
+ // Render a <select> for target field name.
+ let td = document.createElement("td");
+ let select = document.createElement("select");
+ td.appendChild(select);
+ tr.appendChild(td);
+
+ // Render a checkbox.
+ td = document.createElement("td");
+ let checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.checked = true;
+ td.appendChild(checkbox);
+ tr.appendChild(td);
+
+ this._elTbody.appendChild(tr);
+ }
+
+ this._initSelectOptions();
+ }
+
+ /**
+ * Render the table layout and content.
+ */
+ async _render() {
+ await this._renderLayout();
+ this._renderTable();
+ }
+}
+
+customElements.define("csv-field-map", CsvFieldMap);
diff --git a/comm/mailnews/import/content/fieldMapImport.js b/comm/mailnews/import/content/fieldMapImport.js
new file mode 100644
index 0000000000..3f777c0cb6
--- /dev/null
+++ b/comm/mailnews/import/content/fieldMapImport.js
@@ -0,0 +1,259 @@
+/* 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 importService;
+var fieldMap = null;
+var gRecordNum = 0;
+var addInterface = null;
+var dialogResult = null;
+var gPreviousButton;
+var gNextButton;
+var gMoveUpButton;
+var gMoveDownButton;
+var gListbox;
+var gSkipFirstRecordButton;
+
+window.addEventListener("DOMContentLoaded", event => {
+ OnLoadFieldMapImport();
+});
+window.addEventListener("load", resizeColumns, { once: true });
+window.addEventListener("resize", resizeColumns);
+
+document.addEventListener("dialogaccept", FieldImportOKButton);
+
+function resizeColumns() {
+ let list = document.getElementById("fieldList");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty(
+ "--column1width",
+ cols[0].getBoundingClientRect().width + "px"
+ );
+ list.style.setProperty(
+ "--column2width",
+ cols[1].getBoundingClientRect().width + "px"
+ );
+}
+
+function OnLoadFieldMapImport() {
+ top.importService = Cc["@mozilla.org/import/import-service;1"].getService(
+ Ci.nsIImportService
+ );
+
+ // We need a field map object...
+ // assume we have one passed in? or just make one?
+ if (window.arguments && window.arguments[0]) {
+ top.fieldMap = window.arguments[0].fieldMap;
+ top.addInterface = window.arguments[0].addInterface;
+ top.dialogResult = window.arguments[0].result;
+ }
+ if (top.fieldMap == null) {
+ top.fieldMap = top.importService.CreateNewFieldMap();
+ top.fieldMap.DefaultFieldMap(top.fieldMap.numMozFields);
+ }
+
+ gMoveUpButton = document.getElementById("upButton");
+ gMoveDownButton = document.getElementById("downButton");
+ gPreviousButton = document.getElementById("previous");
+ gNextButton = document.getElementById("next");
+ gListbox = document.getElementById("fieldList");
+ gSkipFirstRecordButton = document.getElementById("skipFirstRecord");
+
+ // Set the state of the skip first record button
+ gSkipFirstRecordButton.checked = top.fieldMap.skipFirstRecord;
+
+ ListFields();
+ browseDataPreview(1);
+ gListbox.selectedItem = gListbox.getItemAtIndex(0);
+ disableMoveButtons();
+}
+
+function IndexInMap(index) {
+ var count = top.fieldMap.mapSize;
+ for (var i = 0; i < count; i++) {
+ if (top.fieldMap.GetFieldMap(i) == index) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function ListFields() {
+ if (top.fieldMap == null) {
+ return;
+ }
+
+ // Add rows for every mapped field.
+ let count = top.fieldMap.mapSize;
+ for (let i = 0; i < count; i++) {
+ let index = top.fieldMap.GetFieldMap(i);
+ if (index == -1) {
+ continue;
+ }
+ AddFieldToList(
+ top.fieldMap.GetFieldDescription(index),
+ index,
+ top.fieldMap.GetFieldActive(i)
+ );
+ }
+
+ // Add rows every possible field we don't already have a row for.
+ count = top.fieldMap.numMozFields;
+ for (let i = 0; i < count; i++) {
+ if (!IndexInMap(i)) {
+ AddFieldToList(top.fieldMap.GetFieldDescription(i), i, false);
+ }
+ }
+
+ // Add dummy rows if the data has more fields than Thunderbird does.
+ let data = top.addInterface.GetData("sampleData-0");
+ if (!(data instanceof Ci.nsISupportsString)) {
+ return;
+ }
+ count = data.data.split("\n").length;
+ for (let i = gListbox.itemCount; i < count; i++) {
+ AddFieldToList(null, -1, false);
+ }
+}
+
+function CreateField(name, index, on) {
+ var item = document.createXULElement("richlistitem");
+ item.setAttribute("align", "center");
+ item.setAttribute("field-index", index);
+ item.setAttribute("allowevents", "true");
+
+ var checkboxCell = document.createXULElement("hbox");
+ checkboxCell.setAttribute("style", "width: var(--column1width)");
+ let checkbox = document.createXULElement("checkbox");
+ if (!name) {
+ checkbox.disabled = true;
+ } else if (on) {
+ checkbox.setAttribute("checked", "true");
+ }
+ checkboxCell.appendChild(checkbox);
+
+ var firstCell = document.createXULElement("label");
+ firstCell.setAttribute("style", "width: var(--column2width)");
+ firstCell.setAttribute("value", name || "");
+
+ var secondCell = document.createXULElement("label");
+ secondCell.setAttribute("class", "importsampledata");
+ secondCell.setAttribute("flex", "1");
+
+ item.appendChild(checkboxCell);
+ item.appendChild(firstCell);
+ item.appendChild(secondCell);
+ return item;
+}
+
+function AddFieldToList(name, index, on) {
+ var item = CreateField(name, index, on);
+ gListbox.appendChild(item);
+}
+
+// The "Move Up/Move Down" buttons should move the items in the left column
+// up/down but the values in the right column should not change.
+function moveItem(up) {
+ var selectedItem = gListbox.selectedItem;
+ var swapPartner = up
+ ? gListbox.getPreviousItem(selectedItem, 1)
+ : gListbox.getNextItem(selectedItem, 1);
+
+ var tmpLabel = swapPartner.lastElementChild.getAttribute("value");
+ swapPartner.lastElementChild.setAttribute(
+ "value",
+ selectedItem.lastElementChild.getAttribute("value")
+ );
+ selectedItem.lastElementChild.setAttribute("value", tmpLabel);
+
+ var newItemPosition = up ? selectedItem.nextElementSibling : selectedItem;
+ gListbox.insertBefore(swapPartner, newItemPosition);
+ gListbox.ensureElementIsVisible(selectedItem);
+ disableMoveButtons();
+}
+
+function disableMoveButtons() {
+ var selectedIndex = gListbox.selectedIndex;
+ gMoveUpButton.disabled = selectedIndex == 0;
+ gMoveDownButton.disabled = selectedIndex == gListbox.getRowCount() - 1;
+}
+
+function ShowSampleData(data) {
+ var fields = data.split("\n");
+ for (var i = 0; i < gListbox.getRowCount(); i++) {
+ gListbox
+ .getItemAtIndex(i)
+ .lastElementChild.setAttribute(
+ "value",
+ i < fields.length ? fields[i] : ""
+ );
+ }
+}
+
+function FetchSampleData(num) {
+ if (!top.addInterface) {
+ return false;
+ }
+
+ var data = top.addInterface.GetData("sampleData-" + num);
+ if (!(data instanceof Ci.nsISupportsString)) {
+ return false;
+ }
+ ShowSampleData(data.data);
+ return true;
+}
+
+/**
+ * Handle the command event of #next and #previous buttons.
+ *
+ * @param {Event} event - The command event of #next or #previous button.
+ */
+function nextPreviousOnCommand(event) {
+ browseDataPreview(event.target.id == "next" ? 1 : -1);
+}
+
+/**
+ * Browse the import data preview by moving to the next or previous record.
+ * Also handle the disabled status of the #next and #previous buttons at the
+ * first or last record.
+ *
+ * @param {integer} step - How many records to move forwards or backwards.
+ * Used by the #next and #previous buttons with step of 1 or -1.
+ */
+function browseDataPreview(step) {
+ gRecordNum += step;
+ if (FetchSampleData(gRecordNum - 1)) {
+ document.l10n.setAttributes(
+ document.getElementById("labelRecordNumber"),
+ "import-ab-csv-preview-record-number",
+ {
+ recordNumber: gRecordNum,
+ }
+ );
+ }
+
+ gPreviousButton.disabled = gRecordNum == 1;
+ gNextButton.disabled =
+ addInterface.GetData("sampleData-" + gRecordNum) == null;
+}
+
+function FieldImportOKButton() {
+ var max = gListbox.getRowCount();
+ // Ensure field map is the right size
+ top.fieldMap.SetFieldMapSize(max);
+
+ for (let i = 0; i < max; i++) {
+ let fIndex = gListbox.getItemAtIndex(i).getAttribute("field-index");
+ let on = gListbox
+ .getItemAtIndex(i)
+ .querySelector("checkbox")
+ .getAttribute("checked");
+ top.fieldMap.SetFieldMap(i, fIndex);
+ top.fieldMap.SetFieldActive(i, on == "true");
+ }
+
+ top.fieldMap.skipFirstRecord = gSkipFirstRecordButton.checked;
+
+ top.dialogResult.ok = true;
+}
diff --git a/comm/mailnews/import/content/fieldMapImport.xhtml b/comm/mailnews/import/content/fieldMapImport.xhtml
new file mode 100644
index 0000000000..8278436321
--- /dev/null
+++ b/comm/mailnews/import/content/fieldMapImport.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/fieldMapImport.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/fieldMapImport.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ width="640"
+ height="480"
+ scrolling="false"
+>
+ <head>
+ <link rel="localization" href="messenger/addressbook/fieldMapImport.ftl" />
+ <title data-l10n-id="import-ab-csv-dialog-title"></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/fieldMapImport.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="import-ab-csv-dialog"
+ data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept"
+ >
+ <hbox align="center">
+ <label id="labelRecordNumber" />
+ <spacer flex="1" />
+ <button
+ id="previous"
+ oncommand="nextPreviousOnCommand(event);"
+ label="&fieldMapImport.previous.label;"
+ accesskey="&fieldMapImport.previous.accesskey;"
+ />
+ <button
+ id="next"
+ oncommand="nextPreviousOnCommand(event);"
+ label="&fieldMapImport.next.label;"
+ accesskey="&fieldMapImport.next.accesskey;"
+ />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ id="skipFirstRecord"
+ label="&fieldMapImport.skipFirstRecord.label;"
+ accesskey="&fieldMapImport.skipFirstRecord.accessKey;"
+ />
+ </hbox>
+
+ <separator class="thin" />
+ <label control="fieldList">&fieldMapImport.text;</label>
+ <separator class="thin" />
+
+ <!-- field list -->
+ <hbox flex="1">
+ <richlistbox id="fieldList" flex="1" onselect="disableMoveButtons();">
+ <treecols>
+ <treecol id="checkedHeader" />
+ <treecol
+ id="fieldNameHeader"
+ label="&fieldMapImport.fieldListTitle;"
+ />
+ <treecol id="sampleDataHeader" label="&fieldMapImport.dataTitle;" />
+ </treecols>
+ </richlistbox>
+
+ <vbox>
+ <spacer flex="1" />
+ <button
+ id="upButton"
+ class="up"
+ label="&fieldMapImport.up.label;"
+ accesskey="&fieldMapImport.up.accesskey;"
+ oncommand="moveItem(true);"
+ />
+ <button
+ id="downButton"
+ class="down"
+ label="&fieldMapImport.down.label;"
+ accesskey="&fieldMapImport.down.accesskey;"
+ oncommand="moveItem(false);"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/import/content/importDialog.js b/comm/mailnews/import/content/importDialog.js
new file mode 100644
index 0000000000..cf029d4989
--- /dev/null
+++ b/comm/mailnews/import/content/importDialog.js
@@ -0,0 +1,1184 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+"use strict";
+
+/* import-globals-from ../../extensions/newsblog/feed-subscriptions.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gImportType = null;
+var gImportMsgsBundle;
+var gFeedsBundle;
+var gImportService = null;
+var gSuccessStr = null;
+var gErrorStr = null;
+var gInputStr = null;
+var gProgressInfo = null;
+var gSelectedModuleName = null;
+var gAddInterface = null;
+var gNewFeedAcctCreated = false;
+
+window.addEventListener("DOMContentLoaded", OnLoadImportDialog);
+window.addEventListener("unload", OnUnloadImportDialog);
+
+function OnLoadImportDialog() {
+ gImportMsgsBundle = document.getElementById("bundle_importMsgs");
+ gFeedsBundle = document.getElementById("bundle_feeds");
+ gImportService = Cc["@mozilla.org/import/import-service;1"].getService(
+ Ci.nsIImportService
+ );
+
+ gProgressInfo = {};
+ gProgressInfo.progressWindow = null;
+ gProgressInfo.importInterface = null;
+ gProgressInfo.mainWindow = window;
+ gProgressInfo.intervalState = 0;
+ gProgressInfo.importSuccess = false;
+ gProgressInfo.importType = null;
+ gProgressInfo.localFolderExists = false;
+
+ gSuccessStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ gErrorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ gInputStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ // look in arguments[0] for parameters
+ if (
+ "arguments" in window &&
+ window.arguments.length >= 1 &&
+ "importType" in window.arguments[0] &&
+ window.arguments[0].importType
+ ) {
+ // keep parameters in global for later
+ gImportType = window.arguments[0].importType;
+ gProgressInfo.importType = gImportType;
+ } else {
+ gImportType = "all";
+ gProgressInfo.importType = "all";
+ }
+
+ SetUpImportType();
+
+ // on startup, set the focus to the control element
+ // for accessibility reasons.
+ // if we used the wizardOverlay, we would get this for free.
+ // see bug #101874
+ document.getElementById("importFields").focus();
+}
+
+/**
+ * After importing, need to restart so that imported address books and mail
+ * accounts can show up.
+ */
+function OnUnloadImportDialog() {
+ let nextButton = document.getElementById("forward");
+ if (
+ gImportType == "settings" &&
+ !gErrorStr.data &&
+ nextButton.label == nextButton.getAttribute("finishedval")
+ ) {
+ MailUtils.restartApplication();
+ }
+}
+
+function SetUpImportType() {
+ // set dialog title
+ document.getElementById("importFields").value = gImportType;
+
+ // Mac migration not working right now, so disable it.
+ if (Services.appinfo.OS == "Darwin") {
+ document.getElementById("allRadio").setAttribute("disabled", "true");
+ if (gImportType == "all") {
+ document.getElementById("importFields").value = "addressbook";
+ }
+ }
+
+ let fileLabel = document.getElementById("fileLabel");
+ let accountLabel = document.getElementById("accountLabel");
+ if (gImportType == "feeds") {
+ accountLabel.hidden = false;
+ fileLabel.hidden = true;
+ ListFeedAccounts();
+ } else {
+ accountLabel.hidden = true;
+ fileLabel.hidden = false;
+ ListModules();
+ }
+}
+
+function SetDivText(id, text) {
+ var div = document.getElementById(id);
+
+ if (div) {
+ if (!div.hasChildNodes()) {
+ var textNode = document.createTextNode(text);
+ div.appendChild(textNode);
+ } else if (div.childNodes.length == 1) {
+ div.firstChild.nodeValue = text;
+ }
+ }
+}
+
+function CheckIfLocalFolderExists() {
+ try {
+ if (MailServices.accounts.localFoldersServer) {
+ gProgressInfo.localFolderExists = true;
+ }
+ } catch (ex) {
+ gProgressInfo.localFolderExists = false;
+ }
+}
+
+function showWizardBox(index) {
+ let stateBox = document.getElementById("stateBox");
+ for (let i = 0; i < stateBox.children.length; i++) {
+ stateBox.children[i].hidden = i != index;
+ }
+}
+
+function getWizardBoxIndex() {
+ let selectedIndex = 0;
+ for (let element of document.getElementById("stateBox").children) {
+ if (!element.hidden) {
+ return selectedIndex;
+ }
+ selectedIndex++;
+ }
+ return selectedIndex - 1;
+}
+
+async function ImportDialogOKButton() {
+ var listbox = document.getElementById("moduleList");
+ var header = document.getElementById("header");
+ var progressMeterEl = document.getElementById("progressMeter");
+ progressMeterEl.value = 0;
+ var progressStatusEl = document.getElementById("progressStatus");
+ var progressTitleEl = document.getElementById("progressTitle");
+
+ // better not mess around with navigation at this point
+ var nextButton = document.getElementById("forward");
+ nextButton.setAttribute("disabled", "true");
+ var backButton = document.getElementById("back");
+ backButton.setAttribute("disabled", "true");
+
+ if (listbox && listbox.selectedCount == 1) {
+ let module = "";
+ let name = "";
+ gImportType = document.getElementById("importFields").value;
+ let index = listbox.selectedItem.getAttribute("list-index");
+ if (index == -1) {
+ return false;
+ }
+ if (gImportType == "feeds") {
+ module = "Feeds";
+ } else {
+ module = gImportService.GetModule(gImportType, index);
+ name = gImportService.GetModuleName(gImportType, index);
+ }
+ gSelectedModuleName = name;
+ if (module) {
+ // Fix for Bug 57839 & 85219
+ // We use localFoldersServer(in nsIMsgAccountManager) to check if Local Folder exists.
+ // We need to check localFoldersServer before importing "mail", "settings", or "filters".
+ // Reason: We will create an account with an incoming server of type "none" after
+ // importing "mail", so the localFoldersServer is valid even though the Local Folder
+ // is not created.
+ if (
+ gImportType == "mail" ||
+ gImportType == "settings" ||
+ gImportType == "filters"
+ ) {
+ CheckIfLocalFolderExists();
+ }
+
+ let meterText = "";
+ let error = {};
+ switch (gImportType) {
+ case "mail":
+ if (await ImportMail(module, gSuccessStr, gErrorStr)) {
+ // We think it was a success, either, we need to
+ // wait for the import to finish
+ // or we are done!
+ if (gProgressInfo.importInterface == null) {
+ ShowImportResults(true, "Mail");
+ return true;
+ }
+
+ meterText = gImportMsgsBundle.getFormattedString(
+ "MailProgressMeterText",
+ [name]
+ );
+ header.setAttribute("description", meterText);
+
+ progressStatusEl.setAttribute("label", "");
+ progressTitleEl.setAttribute("label", meterText);
+
+ showWizardBox(2);
+ gProgressInfo.progressWindow = window;
+ gProgressInfo.intervalState = setInterval(
+ ContinueImportCallback,
+ 100
+ );
+ return true;
+ }
+
+ ShowImportResults(false, "Mail");
+ // Re-enable the next button, as we are here, because the user cancelled the picking.
+ // Enable next, so they can try again.
+ nextButton.removeAttribute("disabled");
+ // Also enable back button so that users can pick other import options.
+ backButton.removeAttribute("disabled");
+ return false;
+
+ case "feeds":
+ if (await ImportFeeds()) {
+ // Successful completion of pre processing and launch of async import.
+ meterText = document.getElementById("description").textContent;
+ header.setAttribute("description", meterText);
+
+ progressStatusEl.setAttribute("label", "");
+ progressTitleEl.setAttribute("label", meterText);
+ progressMeterEl.removeAttribute("value");
+
+ showWizardBox(2);
+ return true;
+ }
+
+ // Re-enable the next button, as we are here, because the user cancelled the picking.
+ // Enable next, so they can try again.
+ nextButton.removeAttribute("disabled");
+ // Also enable back button so that users can pick other import options.
+ backButton.removeAttribute("disabled");
+ return false;
+
+ case "addressbook":
+ if (await ImportAddress(module, gSuccessStr, gErrorStr)) {
+ // We think it was a success, either, we need to
+ // wait for the import to finish
+ // or we are done!
+ if (gProgressInfo.importInterface == null) {
+ ShowImportResults(true, "Address");
+ return true;
+ }
+
+ meterText = gImportMsgsBundle.getFormattedString(
+ "AddrProgressMeterText",
+ [name]
+ );
+ header.setAttribute("description", meterText);
+
+ progressStatusEl.setAttribute("label", "");
+ progressTitleEl.setAttribute("label", meterText);
+
+ showWizardBox(2);
+ gProgressInfo.progressWindow = window;
+ gProgressInfo.intervalState = setInterval(
+ ContinueImportCallback,
+ 100
+ );
+
+ return true;
+ }
+
+ ShowImportResults(false, "Address");
+ // Re-enable the next button, as we are here, because the user cancelled the picking.
+ // Enable next, so they can try again.
+ nextButton.removeAttribute("disabled");
+ // Also enable back button so that users can pick other import options.
+ backButton.removeAttribute("disabled");
+ return false;
+
+ case "settings":
+ error.value = null;
+ let newAccount = {};
+ if (!(await ImportSettings(module, newAccount, error))) {
+ if (error.value) {
+ ShowImportResultsRaw(
+ gImportMsgsBundle.getString("ImportSettingsFailed"),
+ null,
+ false
+ );
+ }
+ // Re-enable the next button, as we are here, because the user cancelled the picking.
+ // Enable next, so they can try again.
+ nextButton.removeAttribute("disabled");
+ // Also enable back button so that users can pick other import options.
+ backButton.removeAttribute("disabled");
+ return false;
+ }
+ ShowImportResultsRaw(
+ gImportMsgsBundle.getFormattedString("ImportSettingsSuccess", [
+ name,
+ ]),
+ null,
+ true
+ );
+ break;
+
+ case "filters":
+ error.value = null;
+ if (!ImportFilters(module, error)) {
+ if (error.value) {
+ ShowImportResultsRaw(
+ gImportMsgsBundle.getFormattedString("ImportFiltersFailed", [
+ name,
+ ]),
+ error.value,
+ false
+ );
+ }
+ // Re-enable the next button, as we are here, because the user cancelled the picking.
+ // Enable next, so they can try again.
+ nextButton.removeAttribute("disabled");
+ // Also enable back button so that users can pick other import options.
+ backButton.removeAttribute("disabled");
+ return false;
+ }
+
+ if (error.value) {
+ ShowImportResultsRaw(
+ gImportMsgsBundle.getFormattedString("ImportFiltersPartial", [
+ name,
+ ]),
+ error.value,
+ true
+ );
+ } else {
+ ShowImportResultsRaw(
+ gImportMsgsBundle.getFormattedString("ImportFiltersSuccess", [
+ name,
+ ]),
+ null,
+ true
+ );
+ }
+
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+function SetStatusText(val) {
+ var progressStatus = document.getElementById("progressStatus");
+ progressStatus.setAttribute("label", val);
+}
+
+function SetProgress(val) {
+ var progressMeter = document.getElementById("progressMeter");
+ progressMeter.value = val;
+}
+
+function ContinueImportCallback() {
+ gProgressInfo.mainWindow.ContinueImport(gProgressInfo);
+}
+
+function ImportSelectionChanged() {
+ let listbox = document.getElementById("moduleList");
+ let acctNameBox = document.getElementById("acctName-box");
+ if (listbox && listbox.selectedCount == 1) {
+ let index = listbox.selectedItem.getAttribute("list-index");
+ if (index == -1) {
+ return;
+ }
+ acctNameBox.setAttribute("style", "visibility: hidden;");
+ if (gImportType == "feeds") {
+ if (index == 0) {
+ SetDivText(
+ "description",
+ gFeedsBundle.getString("ImportFeedsNewAccount")
+ );
+ let defaultName = gFeedsBundle.getString("feeds-accountname");
+ document.getElementById("acctName").value = defaultName;
+ acctNameBox.removeAttribute("style");
+ } else {
+ SetDivText(
+ "description",
+ gFeedsBundle.getString("ImportFeedsExistingAccount")
+ );
+ }
+ } else {
+ SetDivText(
+ "description",
+ gImportService.GetModuleDescription(gImportType, index)
+ );
+ }
+ }
+}
+
+function CompareImportModuleName(a, b) {
+ if (a.name > b.name) {
+ return 1;
+ }
+ if (a.name < b.name) {
+ return -1;
+ }
+ return 0;
+}
+
+function ListModules() {
+ if (gImportService == null) {
+ return;
+ }
+
+ var body = document.getElementById("moduleList");
+ while (body.hasChildNodes()) {
+ body.lastChild.remove();
+ }
+
+ var count = gImportService.GetModuleCount(gImportType);
+ var i;
+
+ var moduleArray = new Array(count);
+ for (i = 0; i < count; i++) {
+ moduleArray[i] = {
+ name: gImportService.GetModuleName(gImportType, i),
+ index: i,
+ };
+ }
+
+ // sort the array of modules by name, so that they'll show up in the right order
+ moduleArray.sort(CompareImportModuleName);
+
+ for (i = 0; i < count; i++) {
+ AddModuleToList(moduleArray[i].name, moduleArray[i].index);
+ }
+}
+
+function AddModuleToList(moduleName, index) {
+ var body = document.getElementById("moduleList");
+
+ let item = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ label.setAttribute("value", moduleName);
+ item.appendChild(label);
+ item.setAttribute("list-index", index);
+ body.appendChild(item);
+}
+
+function ListFeedAccounts() {
+ let body = document.getElementById("moduleList");
+ while (body.hasChildNodes()) {
+ body.lastChild.remove();
+ }
+
+ // Add item to allow for new account creation.
+ let item = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ label.setAttribute(
+ "value",
+ gFeedsBundle.getString("ImportFeedsCreateNewListItem")
+ );
+ item.appendChild(label);
+ item.setAttribute("list-index", 0);
+ body.appendChild(item);
+
+ let index = 0;
+ let feedRootFolders = FeedUtils.getAllRssServerRootFolders();
+
+ feedRootFolders.forEach(function (rootFolder) {
+ item = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ label.setAttribute("value", rootFolder.prettyName);
+ item.appendChild(label);
+ item.setAttribute("list-index", ++index);
+ item.server = rootFolder.server;
+ body.appendChild(item);
+ }, this);
+
+ if (index) {
+ // If there is an existing feed account, select the first one.
+ body.selectedIndex = 1;
+ }
+}
+
+function ContinueImport(info) {
+ var isMail = info.importType == "mail";
+ var clear = true;
+ var pcnt;
+
+ if (info.importInterface) {
+ if (!info.importInterface.ContinueImport()) {
+ info.importSuccess = false;
+ clearInterval(info.intervalState);
+ if (info.progressWindow != null) {
+ showWizardBox(3);
+ info.progressWindow = null;
+ }
+
+ ShowImportResults(false, isMail ? "Mail" : "Address");
+ } else if ((pcnt = info.importInterface.GetProgress()) < 100) {
+ clear = false;
+ if (info.progressWindow != null) {
+ if (pcnt < 5) {
+ pcnt = 5;
+ }
+ SetProgress(pcnt);
+ if (isMail) {
+ let mailName = info.importInterface.GetData("currentMailbox");
+ if (mailName) {
+ mailName = mailName.QueryInterface(Ci.nsISupportsString);
+ if (mailName) {
+ SetStatusText(mailName.data);
+ }
+ }
+ }
+ }
+ } else {
+ dump("*** WARNING! sometimes this shows results too early. \n");
+ dump(" something screwy here. this used to work fine.\n");
+ clearInterval(info.intervalState);
+ info.importSuccess = true;
+ if (info.progressWindow) {
+ showWizardBox(3);
+ info.progressWindow = null;
+ }
+
+ ShowImportResults(true, isMail ? "Mail" : "Address");
+ }
+ }
+ if (clear) {
+ info.intervalState = null;
+ info.importInterface = null;
+ }
+}
+
+function ShowResults(doesWantProgress, result) {
+ if (result) {
+ if (doesWantProgress) {
+ let header = document.getElementById("header");
+ let progressStatusEl = document.getElementById("progressStatus");
+ let progressTitleEl = document.getElementById("progressTitle");
+
+ let meterText = gImportMsgsBundle.getFormattedString(
+ "AddrProgressMeterText",
+ [name]
+ );
+ header.setAttribute("description", meterText);
+
+ progressStatusEl.setAttribute("label", "");
+ progressTitleEl.setAttribute("label", meterText);
+
+ showWizardBox(2);
+ gProgressInfo.progressWindow = window;
+ gProgressInfo.intervalState = setInterval(ContinueImportCallback, 100);
+ } else {
+ ShowImportResults(true, "Address");
+ }
+ } else {
+ ShowImportResults(false, "Address");
+ }
+
+ return true;
+}
+
+function ShowImportResults(good, module) {
+ // String keys for ImportSettingsSuccess, ImportSettingsFailed,
+ // ImportMailSuccess, ImportMailFailed, ImportAddressSuccess,
+ // ImportAddressFailed, ImportFiltersSuccess, and ImportFiltersFailed.
+ var modSuccess = "Import" + module + "Success";
+ var modFailed = "Import" + module + "Failed";
+
+ // The callers seem to set 'good' to true even if there's something
+ // in the error log. So we should only make it a success case if
+ // error log/str is empty.
+ var results, title;
+ var moduleName = gSelectedModuleName ? gSelectedModuleName : "";
+ if (good && !gErrorStr.data) {
+ title = gImportMsgsBundle.getFormattedString(modSuccess, [moduleName]);
+ results = gSuccessStr.data;
+ } else if (gErrorStr.data) {
+ title = gImportMsgsBundle.getFormattedString(modFailed, [moduleName]);
+ results = gErrorStr.data;
+ }
+
+ if (results && title) {
+ ShowImportResultsRaw(title, results, good);
+ }
+}
+
+function ShowImportResultsRaw(title, results, good) {
+ SetDivText("status", title);
+ var header = document.getElementById("header");
+ header.setAttribute("description", title);
+ dump("*** results = " + results + "\n");
+ attachStrings("results", results);
+ showWizardBox(3);
+ var nextButton = document.getElementById("forward");
+ nextButton.label = nextButton.getAttribute("finishedval");
+ nextButton.removeAttribute("disabled");
+ var cancelButton = document.getElementById("cancel");
+ cancelButton.setAttribute("disabled", "true");
+ var backButton = document.getElementById("back");
+ backButton.setAttribute("disabled", "true");
+
+ // If the Local Folder doesn't exist, create it after successfully
+ // importing "mail" and "settings"
+ var checkLocalFolder =
+ gProgressInfo.importType == "mail" ||
+ gProgressInfo.importType == "settings";
+ if (good && checkLocalFolder && !gProgressInfo.localFolderExists) {
+ MailServices.accounts.createLocalMailAccount();
+ }
+}
+
+function attachStrings(aNode, aString) {
+ var attachNode = document.getElementById(aNode);
+ if (!aString) {
+ attachNode.parentNode.setAttribute("hidden", "true");
+ return;
+ }
+ var strings = aString.split("\n");
+ for (let string of strings) {
+ if (string) {
+ let currNode = document.createTextNode(string);
+ attachNode.appendChild(currNode);
+ let br = document.createElementNS("http://www.w3.org/1999/xhtml", "br");
+ attachNode.appendChild(br);
+ }
+ }
+}
+
+/**
+ * Show the file picker.
+ *
+ * @returns {Promise} the selected file, or null
+ */
+function promptForFile(fp) {
+ return new Promise(resolve => {
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ resolve(null);
+ return;
+ }
+ resolve(fp.file);
+ });
+ });
+}
+
+/*
+ Import Settings from a specific module, returns false if it failed
+ and true if successful. A "local mail" account is returned in newAccount.
+ This is only useful in upgrading - import the settings first, then
+ import mail into the account returned from ImportSettings, then
+ import address books.
+ An error string is returned as error.value
+*/
+async function ImportSettings(module, newAccount, error) {
+ var setIntf = module.GetImportInterface("settings");
+ if (!(setIntf instanceof Ci.nsIImportSettings)) {
+ error.value = gImportMsgsBundle.getString("ImportSettingsBadModule");
+ return false;
+ }
+
+ // determine if we can auto find the settings or if we need to ask the user
+ var location = {};
+ var description = {};
+ var result = setIntf.AutoLocate(description, location);
+ if (!result) {
+ // In this case, we couldn't find the settings
+ if (location.value != null) {
+ // Settings were not found, however, they are specified
+ // in a file, so ask the user for the settings file.
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance();
+ if (filePicker instanceof Ci.nsIFilePicker) {
+ let file = null;
+ try {
+ filePicker.init(
+ window,
+ gImportMsgsBundle.getString("ImportSelectSettings"),
+ filePicker.modeOpen
+ );
+ filePicker.appendFilters(filePicker.filterAll);
+
+ file = await promptForFile(filePicker);
+ } catch (ex) {
+ console.error(ex);
+ error.value = null;
+ return false;
+ }
+ if (file != null) {
+ setIntf.SetLocation(file);
+ } else {
+ error.value = null;
+ return false;
+ }
+ } else {
+ error.value = gImportMsgsBundle.getString("ImportSettingsNotFound");
+ return false;
+ }
+ } else {
+ error.value = gImportMsgsBundle.getString("ImportSettingsNotFound");
+ return false;
+ }
+ }
+
+ // interesting, we need to return the account that new
+ // mail should be imported into?
+ // that's really only useful for "Upgrade"
+ result = setIntf.Import(newAccount);
+ if (!result) {
+ error.value = gImportMsgsBundle.getString("ImportSettingsFailed");
+ }
+ return result;
+}
+
+async function ImportMail(module, success, error) {
+ if (gProgressInfo.importInterface || gProgressInfo.intervalState) {
+ error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress");
+ return false;
+ }
+
+ gProgressInfo.importSuccess = false;
+
+ var mailInterface = module.GetImportInterface("mail");
+ if (!(mailInterface instanceof Ci.nsIImportGeneric)) {
+ error.data = gImportMsgsBundle.getString("ImportMailBadModule");
+ return false;
+ }
+
+ var loc = mailInterface.GetData("mailLocation");
+
+ if (loc == null) {
+ // No location found, check to see if we can ask the user.
+ if (mailInterface.GetStatus("canUserSetLocation") != 0) {
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance();
+ if (filePicker instanceof Ci.nsIFilePicker) {
+ try {
+ filePicker.init(
+ window,
+ gImportMsgsBundle.getString("ImportSelectMailDir"),
+ filePicker.modeGetFolder
+ );
+ filePicker.appendFilters(filePicker.filterAll);
+ let file = await promptForFile(filePicker);
+ if (!file) {
+ return false;
+ }
+ mailInterface.SetData("mailLocation", file);
+ } catch (ex) {
+ console.error(ex);
+ // don't show an error when we return!
+ return false;
+ }
+ } else {
+ error.data = gImportMsgsBundle.getString("ImportMailNotFound");
+ return false;
+ }
+ } else {
+ error.data = gImportMsgsBundle.getString("ImportMailNotFound");
+ return false;
+ }
+ }
+
+ if (mailInterface.WantsProgress()) {
+ if (mailInterface.BeginImport(success, error)) {
+ gProgressInfo.importInterface = mailInterface;
+ // intervalState = setInterval(ContinueImport, 100);
+ return true;
+ }
+ return false;
+ }
+ return mailInterface.BeginImport(success, error);
+}
+
+// The address import! A little more complicated than the mail import
+// due to field maps...
+async function ImportAddress(module, success, error) {
+ if (gProgressInfo.importInterface || gProgressInfo.intervalState) {
+ error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress");
+ return false;
+ }
+
+ gProgressInfo.importSuccess = false;
+
+ gAddInterface = module.GetImportInterface("addressbook");
+ if (!(gAddInterface instanceof Ci.nsIImportGeneric)) {
+ error.data = gImportMsgsBundle.getString("ImportAddressBadModule");
+ return false;
+ }
+
+ var loc = gAddInterface.GetStatus("autoFind");
+ if (loc == 0) {
+ loc = gAddInterface.GetData("addressLocation");
+ if (loc instanceof Ci.nsIFile && !loc.exists) {
+ loc = null;
+ }
+ }
+
+ if (loc == null) {
+ // Couldn't find the address book, see if we can
+ // as the user for the location or not?
+ if (gAddInterface.GetStatus("canUserSetLocation") == 0) {
+ // an autofind address book that could not be found!
+ error.data = gImportMsgsBundle.getString("ImportAddressNotFound");
+ return false;
+ }
+
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance();
+ if (!(filePicker instanceof Ci.nsIFilePicker)) {
+ error.data = gImportMsgsBundle.getString("ImportAddressNotFound");
+ return false;
+ }
+
+ // The address book location was not found.
+ // Determine if we need to ask for a directory
+ // or a single file.
+ let file = null;
+ let fileIsDirectory = false;
+ if (gAddInterface.GetStatus("supportsMultiple") != 0) {
+ // ask for dir
+ try {
+ filePicker.init(
+ window,
+ gImportMsgsBundle.getString("ImportSelectAddrDir"),
+ filePicker.modeGetFolder
+ );
+ filePicker.appendFilters(filePicker.filterAll);
+ file = await promptForFile(filePicker);
+ if (file && file.path) {
+ fileIsDirectory = true;
+ }
+ } catch (ex) {
+ console.error(ex);
+ file = null;
+ }
+ } else {
+ // ask for file
+ try {
+ filePicker.init(
+ window,
+ gImportMsgsBundle.getString("ImportSelectAddrFile"),
+ filePicker.modeOpen
+ );
+ let addressbookBundle = document.getElementById("bundle_addressbook");
+ if (
+ gSelectedModuleName ==
+ document
+ .getElementById("bundle_vcardImportMsgs")
+ .getString("vCardImportName")
+ ) {
+ filePicker.appendFilter(
+ addressbookBundle.getString("VCFFiles"),
+ "*.vcf"
+ );
+ } else if (
+ gSelectedModuleName ==
+ document
+ .getElementById("bundle_morkImportMsgs")
+ .getString("morkImportName")
+ ) {
+ filePicker.appendFilter(
+ document
+ .getElementById("bundle_morkImportMsgs")
+ .getString("MABFiles"),
+ "*.mab"
+ );
+ } else {
+ filePicker.appendFilter(
+ addressbookBundle.getString("LDIFFiles"),
+ "*.ldi; *.ldif"
+ );
+ filePicker.appendFilter(
+ addressbookBundle.getString("CSVFiles"),
+ "*.csv"
+ );
+ filePicker.appendFilter(
+ addressbookBundle.getString("TABFiles"),
+ "*.tab; *.txt"
+ );
+ filePicker.appendFilter(
+ addressbookBundle.getString("SupportedABFiles"),
+ "*.csv; *.ldi; *.ldif; *.tab; *.txt"
+ );
+ filePicker.appendFilters(filePicker.filterAll);
+ // Use "Supported Address Book Files" as default file filter.
+ filePicker.filterIndex = 3;
+ }
+
+ file = await promptForFile(filePicker);
+ } catch (ex) {
+ console.error(ex);
+ file = null;
+ }
+ }
+
+ if (!file) {
+ return false;
+ }
+
+ if (!fileIsDirectory && file.fileSize == 0) {
+ let errorText = gImportMsgsBundle.getFormattedString(
+ "ImportEmptyAddressBook",
+ [file.leafName]
+ );
+
+ Services.prompt.alert(window, document.title, errorText);
+ return false;
+ }
+ gAddInterface.SetData("addressLocation", file);
+ }
+
+ var map = gAddInterface.GetData("fieldMap");
+ if (map instanceof Ci.nsIImportFieldMap) {
+ let result = {};
+ result.ok = false;
+ window.openDialog(
+ "chrome://messenger/content/fieldMapImport.xhtml",
+ "",
+ "chrome,modal,titlebar",
+ {
+ fieldMap: map,
+ addInterface: gAddInterface,
+ result,
+ }
+ );
+
+ if (!result.ok) {
+ return false;
+ }
+ }
+
+ if (gAddInterface.WantsProgress()) {
+ if (gAddInterface.BeginImport(success, error)) {
+ gProgressInfo.importInterface = gAddInterface;
+ // intervalState = setInterval(ContinueImport, 100);
+ return true;
+ }
+ return false;
+ }
+
+ return gAddInterface.BeginImport(success, error);
+}
+
+/*
+ Import filters from a specific module.
+ Returns false if it failed and true if it succeeded.
+ An error string is returned as error.value.
+*/
+function ImportFilters(module, error) {
+ if (gProgressInfo.importInterface || gProgressInfo.intervalState) {
+ error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress");
+ return false;
+ }
+
+ gProgressInfo.importSuccess = false;
+
+ var filtersInterface = module.GetImportInterface("filters");
+ if (!(filtersInterface instanceof Ci.nsIImportFilters)) {
+ error.data = gImportMsgsBundle.getString("ImportFiltersBadModule");
+ return false;
+ }
+
+ return filtersInterface.Import(error);
+}
+
+/*
+ Import feeds.
+*/
+async function ImportFeeds() {
+ // Get file and file url to open from filepicker.
+ let [openFile, openFileUrl] = await FeedSubscriptions.opmlPickOpenFile();
+
+ let acctName;
+ let acctNewExist = gFeedsBundle.getString("ImportFeedsExisting");
+ let fileName = openFile.path;
+ let server = document.getElementById("moduleList").selectedItem.server;
+ gNewFeedAcctCreated = false;
+
+ if (!server) {
+ // Create a new Feeds account.
+ acctName = document.getElementById("acctName").value;
+ server = FeedUtils.createRssAccount(acctName).incomingServer;
+ acctNewExist = gFeedsBundle.getString("ImportFeedsNew");
+ gNewFeedAcctCreated = true;
+ }
+
+ acctName = server.rootFolder.prettyName;
+
+ let callback = function (aStatusReport, aLastFolder, aFeedWin) {
+ let message = gFeedsBundle.getFormattedString("ImportFeedsDone", [
+ fileName,
+ acctNewExist,
+ acctName,
+ ]);
+ ShowImportResultsRaw(message + " " + aStatusReport, null, true);
+ document.getElementById("back").removeAttribute("disabled");
+
+ let subscriptionsWindow = Services.wm.getMostRecentWindow(
+ "Mail:News-BlogSubscriptions"
+ );
+ if (subscriptionsWindow) {
+ let feedWin = subscriptionsWindow.FeedSubscriptions;
+ if (aLastFolder) {
+ feedWin.FolderListener.folderAdded(aLastFolder);
+ }
+
+ feedWin.mActionMode = null;
+ feedWin.updateButtons(feedWin.mView.currentItem);
+ feedWin.clearStatusInfo();
+ feedWin.updateStatusItem("statusText", aStatusReport);
+ }
+ };
+
+ if (
+ !(await FeedSubscriptions.importOPMLFile(
+ openFile,
+ openFileUrl,
+ server,
+ callback
+ ))
+ ) {
+ return false;
+ }
+
+ let subscriptionsWindow = Services.wm.getMostRecentWindow(
+ "Mail:News-BlogSubscriptions"
+ );
+ if (subscriptionsWindow) {
+ let feedWin = subscriptionsWindow.FeedSubscriptions;
+ feedWin.mActionMode = feedWin.kImportingOPML;
+ feedWin.updateButtons(null);
+ let statusReport = gFeedsBundle.getString("subscribe-loading");
+ feedWin.updateStatusItem("statusText", statusReport);
+ feedWin.updateStatusItem("progressMeter", "?");
+ }
+
+ return true;
+}
+
+function SwitchType(newType) {
+ if (gImportType == newType) {
+ return;
+ }
+
+ gImportType = newType;
+ gProgressInfo.importType = newType;
+
+ SetUpImportType();
+
+ SetDivText("description", "");
+}
+
+function next() {
+ switch (getWizardBoxIndex()) {
+ case 0:
+ let backButton = document.getElementById("back");
+ backButton.removeAttribute("disabled");
+ let radioGroup = document.getElementById("importFields");
+
+ if (radioGroup.value == "all") {
+ let args = { closeMigration: true };
+ let SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}";
+ if (Services.appinfo.ID == SEAMONKEY_ID) {
+ window.openDialog(
+ "chrome://communicator/content/migration/migration.xhtml",
+ "",
+ "chrome,dialog,modal,centerscreen"
+ );
+ } else {
+ // Running as Thunderbird or its clone.
+ window.openDialog(
+ "chrome://messenger/content/migration/migration.xhtml",
+ "",
+ "chrome,dialog,modal,centerscreen",
+ null,
+ null,
+ null,
+ args
+ );
+ }
+ if (args.closeMigration) {
+ close();
+ }
+ } else {
+ SwitchType(radioGroup.value);
+ showWizardBox(1);
+ let moduleBox = document.getElementById("moduleBox");
+ let noModuleLabel = document.getElementById("noModuleLabel");
+ if (document.getElementById("moduleList").itemCount > 0) {
+ moduleBox.hidden = false;
+ noModuleLabel.hidden = true;
+ } else {
+ moduleBox.hidden = true;
+ noModuleLabel.hidden = false;
+ }
+ SelectFirstItem();
+ enableAdvance();
+ }
+ break;
+ case 1:
+ ImportDialogOKButton();
+ break;
+ case 3:
+ close();
+ break;
+ }
+}
+
+function SelectFirstItem() {
+ var listbox = document.getElementById("moduleList");
+ if (listbox.selectedIndex == -1 && listbox.itemCount > 0) {
+ listbox.selectedIndex = 0;
+ }
+ ImportSelectionChanged();
+}
+
+function enableAdvance() {
+ var listbox = document.getElementById("moduleList");
+ var nextButton = document.getElementById("forward");
+ if (listbox.selectedCount > 0) {
+ nextButton.removeAttribute("disabled");
+ } else {
+ nextButton.setAttribute("disabled", "true");
+ }
+}
+
+function back() {
+ var backButton = document.getElementById("back");
+ var nextButton = document.getElementById("forward");
+ switch (getWizardBoxIndex()) {
+ case 1:
+ backButton.setAttribute("disabled", "true");
+ nextButton.label = nextButton.getAttribute("nextval");
+ nextButton.removeAttribute("disabled");
+ showWizardBox(0);
+ break;
+ case 3:
+ // Clear out the results box.
+ let results = document.getElementById("results");
+ while (results.hasChildNodes()) {
+ results.lastChild.remove();
+ }
+
+ // Reset the next button.
+ nextButton.label = nextButton.getAttribute("nextval");
+ nextButton.removeAttribute("disabled");
+
+ // Enable the cancel button again.
+ document.getElementById("cancel").removeAttribute("disabled");
+
+ // If a new Feed account has been created, rebuild the list.
+ if (gNewFeedAcctCreated) {
+ ListFeedAccounts();
+ }
+
+ // Now go back to the second page.
+ showWizardBox(1);
+ break;
+ }
+}
diff --git a/comm/mailnews/import/content/importDialog.xhtml b/comm/mailnews/import/content/importDialog.xhtml
new file mode 100644
index 0000000000..c3147d8c99
--- /dev/null
+++ b/comm/mailnews/import/content/importDialog.xhtml
@@ -0,0 +1,225 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % importDTD SYSTEM "chrome://messenger/locale/importDialog.dtd" >
+%importDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ width="720"
+ height="520"
+ scrolling="false"
+>
+ <head>
+ <title>&importDialog.windowTitle;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger-newsblog/content/feed-subscriptions.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/importDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <stringbundle
+ id="bundle_importMsgs"
+ src="chrome://messenger/locale/importMsgs.properties"
+ />
+ <stringbundle
+ id="bundle_addressbook"
+ src="chrome://messenger/locale/addressbook/addressBook.properties"
+ />
+ <stringbundle
+ id="bundle_morkImportMsgs"
+ src="chrome://messenger/locale/morkImportMsgs.properties"
+ />
+ <stringbundle
+ id="bundle_vcardImportMsgs"
+ src="chrome://messenger/locale/vCardImportMsgs.properties"
+ />
+ <stringbundle
+ id="bundle_feeds"
+ src="chrome://messenger-newsblog/locale/newsblog.properties"
+ />
+
+ <hbox
+ class="box-header"
+ id="header"
+ title="&importTitle.label;"
+ description="&importShortDesc.label;"
+ />
+
+ <vbox id="stateBox" flex="1" style="min-height: 30em">
+ <vbox class="wizard-box">
+ <description>&importDescription1.label;</description>
+ <description>&importDescription2.label;</description>
+ <separator />
+ <radiogroup id="importFields">
+ <radio
+ id="allRadio"
+ value="all"
+ label="&importAll.label;"
+ accesskey="&importAll.accesskey;"
+ />
+ <separator />
+ <label control="importFields">&select.label;</label>
+ <separator class="thin" />
+ <vbox class="indent">
+ <radio
+ id="addressbookRadio"
+ value="addressbook"
+ label="&importAddressbook.label;"
+ accesskey="&importAddressbook.accesskey;"
+ />
+ <radio
+ id="mailRadio"
+ value="mail"
+ label="&importMail.label;"
+ accesskey="&importMail.accesskey;"
+ />
+ <radio
+ id="feedsRadio"
+ value="feeds"
+ label="&importFeeds.label;"
+ accesskey="&importFeeds.accesskey;"
+ />
+ <radio
+ id="settingsRadio"
+ value="settings"
+ label="&importSettings.label;"
+ accesskey="&importSettings.accesskey;"
+ />
+ <radio
+ id="filtersRadio"
+ value="filters"
+ label="&importFilters.label;"
+ accesskey="&importFilters.accesskey;"
+ />
+ </vbox>
+ </radiogroup>
+ </vbox>
+ <vbox class="wizard-box" hidden="true">
+ <vbox>
+ <vbox id="moduleBox">
+ <vbox>
+ <label
+ id="fileLabel"
+ control="moduleList"
+ value="&selectDescription.label;"
+ accesskey="&selectDescription.accesskey;"
+ />
+ <label
+ id="accountLabel"
+ control="moduleList"
+ value="&selectDescriptionB.label;"
+ accesskey="&selectDescription.accesskey;"
+ hidden="true"
+ />
+ </vbox>
+ <richlistbox
+ id="moduleList"
+ height="200px"
+ onselect="ImportSelectionChanged(); enableAdvance();"
+ />
+ </vbox>
+ <label id="noModuleLabel" hidden="true">&noModulesFound.label;</label>
+ </vbox>
+ <vbox>
+ <hbox flex="1">
+ <description
+ flex="1"
+ control="moduleList"
+ id="description"
+ class="box-padded"
+ />
+ </hbox>
+ <hbox
+ id="acctName-box"
+ flex="1"
+ class="input-container"
+ style="visibility: hidden"
+ >
+ <label
+ control="acctName"
+ class="box-padded"
+ accesskey="&acctName.accesskey;"
+ value="&acctName.label;"
+ />
+ <html:input
+ id="acctName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="acctName"
+ />
+ </hbox>
+ </vbox>
+ </vbox>
+ <vbox class="wizard-box" hidden="true">
+ <spacer flex="1" />
+ <html:fieldset>
+ <label id="progressTitle" class="header">&title.label;</label>
+ <label
+ class="indent"
+ id="progressStatus"
+ value="&processing.label;"
+ />
+ <vbox class="box-padded">
+ <html:progress id="progressMeter" value="5" max="100" />
+ </vbox>
+ </html:fieldset>
+ </vbox>
+ <vbox class="wizard-box" flex="1" hidden="true">
+ <description id="status" />
+ <hbox style="overflow: auto" class="inset" flex="1">
+ <description id="results" flex="1" />
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <separator />
+
+ <separator class="groove" />
+
+ <hbox class="box-padded">
+ <spacer flex="1" />
+ <button
+ id="back"
+ label="&back.label;"
+ disabled="true"
+ oncommand="back();"
+ />
+ <button
+ id="forward"
+ label="&forward.label;"
+ nextval="&forward.label;"
+ finishedval="&finish.label;"
+ oncommand="next();"
+ />
+ <separator orient="vertical" />
+ <button id="cancel" label="&cancel.label;" oncommand="window.close();" />
+ </hbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/import/modules/AddrBookFileImporter.jsm b/comm/mailnews/import/modules/AddrBookFileImporter.jsm
new file mode 100644
index 0000000000..c9cea94893
--- /dev/null
+++ b/comm/mailnews/import/modules/AddrBookFileImporter.jsm
@@ -0,0 +1,356 @@
+/* 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 = ["AddrBookFileImporter"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+ exportAttributes: "resource:///modules/AddrBookUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "d3", () => {
+ let d3Scope = Cu.Sandbox(null);
+ Services.scriptloader.loadSubScript(
+ "chrome://global/content/third_party/d3/d3.js",
+ d3Scope
+ );
+ return Cu.waiveXrays(d3Scope.d3);
+});
+
+/**
+ * A module to import address book files.
+ */
+class AddrBookFileImporter {
+ /**
+ * @param {string} type - Source file type, currently supporting "csv",
+ * "ldif", "vcard" and "mab".
+ */
+ constructor(type) {
+ this._type = type;
+ }
+
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+
+ /**
+ * Actually start importing records into a directory.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @param {nsIAbDirectory} targetDirectory - The directory to import into.
+ */
+ async startImport(sourceFile, targetDirectory) {
+ this._logger.debug(
+ `Importing ${this._type} file from ${sourceFile.path} into ${targetDirectory.dirName}`
+ );
+ this._sourceFile = sourceFile;
+ this._targetDirectory = targetDirectory;
+
+ switch (this._type) {
+ case "csv":
+ await this._importCsvFile();
+ break;
+ case "ldif":
+ await this._importLdifFile();
+ break;
+ case "vcard":
+ await this._importVCardFile();
+ break;
+ case "sqlite":
+ await this._importSqliteFile();
+ break;
+ case "mab":
+ await this._importMabFile();
+ break;
+ default:
+ throw Components.Exception(
+ `Importing ${this._type} file is not supported`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ }
+
+ /**
+ * Parse a CSV/TSV file to an array of rows, each row is an array of columns.
+ * The first row is expected to contain field names. If we recognize all the
+ * field names, return an empty array, which means everything is parsed fine.
+ * Otherwise, return all the rows.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @returns {string[][]}
+ */
+ async parseCsvFile(sourceFile) {
+ let content = await lazy.MailStringUtils.readEncoded(sourceFile.path);
+
+ let csvRows = lazy.d3.csv.parseRows(content);
+ let tsvRows = lazy.d3.tsv.parseRows(content);
+ let dsvRows = lazy.d3.dsv(";").parseRows(content);
+ if (!csvRows.length && !tsvRows.length && !dsvRows.length) {
+ this._csvRows = [];
+ return [];
+ }
+ // If we have more CSV columns, then it's a CSV file, otherwise a TSV file.
+ this._csvRows = csvRows[0]?.length > tsvRows[0]?.length ? csvRows : tsvRows;
+ // See if it's semicolon separated.
+ if (this._csvRows[0]?.length < dsvRows[0]?.length) {
+ this._csvRows = dsvRows;
+ }
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+ let supportedFieldNames = [];
+ this._supportedCsvProperties = [];
+ // Collect field names in an exported CSV file, and their corresponding
+ // nsIAbCard property names.
+ for (let [property, stringId] of lazy.exportAttributes) {
+ if (stringId) {
+ this._supportedCsvProperties.push(property);
+ supportedFieldNames.push(
+ bundle.GetStringFromID(stringId).toLowerCase()
+ );
+ }
+ }
+ this._csvSkipFirstRow = true;
+ this._csvProperties = [];
+ // Get the nsIAbCard properties corresponding to the user supplied file.
+ for (let field of this._csvRows[0]) {
+ if (
+ !field &&
+ this._csvRows[0].length > 1 &&
+ field == this._csvRows[0].at(-1)
+ ) {
+ // This is the last field and empty, caused by a trailing comma, which
+ // is OK.
+ return [];
+ }
+ let index = supportedFieldNames.indexOf(field.toLowerCase());
+ if (index == -1) {
+ return this._csvRows;
+ }
+ this._csvProperties.push(this._supportedCsvProperties[index]);
+ }
+ return [];
+ }
+
+ /**
+ * Set the address book properties to use when importing.
+ *
+ * @param {number[]} fieldIndexes - An array of indexes representing the
+ * mapping between the source fields and nsIAbCard fields. For example, [2,
+ * 4] means the first field maps to the 2nd property, the second field maps
+ * to the 4th property.
+ */
+ setCsvFields(fieldIndexes) {
+ Services.prefs.setCharPref(
+ "mail.import.csv.fields",
+ fieldIndexes.join(",")
+ );
+ this._csvProperties = fieldIndexes.map(
+ i => this._supportedCsvProperties[i]
+ );
+ this._csvSkipFirstRow = Services.prefs.getBoolPref(
+ "mail.import.csv.skipfirstrow",
+ true
+ );
+ }
+
+ /**
+ * Import the .csv/.tsv source file into the target directory.
+ */
+ async _importCsvFile() {
+ let totalLines = this._csvRows.length - 1;
+ let currentLine = 0;
+
+ let startRow = this._csvSkipFirstRow ? 1 : 0;
+ for (let row of this._csvRows.slice(startRow)) {
+ currentLine++;
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (let i = 0; i < row.length; i++) {
+ let property = this._csvProperties[i];
+ if (!property) {
+ continue;
+ }
+ // Set the field value to the property.
+ card.setProperty(property, row[i]);
+ }
+ this._targetDirectory.addCard(card);
+ if (currentLine % 10 == 0) {
+ this.onProgress(currentLine, totalLines);
+ // Give UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(totalLines, totalLines);
+ }
+
+ /**
+ * Import the .ldif source file into the target directory.
+ */
+ async _importLdifFile() {
+ this.onProgress(2, 10);
+ let ldifService = Cc["@mozilla.org/addressbook/abldifservice;1"].getService(
+ Ci.nsIAbLDIFService
+ );
+ let progress = {};
+ ldifService.importLDIFFile(
+ this._targetDirectory,
+ this._sourceFile,
+ false,
+ progress
+ );
+ this.onProgress(10, 10);
+ }
+
+ /**
+ * Import the .vcf source file into the target directory.
+ */
+ async _importVCardFile() {
+ let vcardService = Cc[
+ "@mozilla.org/addressbook/msgvcardservice;1"
+ ].getService(Ci.nsIMsgVCardService);
+
+ let content = await IOUtils.readUTF8(this._sourceFile.path);
+ // According to rfc6350, \r\n should be used as line break.
+ let sep = content.includes("\r\n") ? "\r\n" : "\n";
+ let lines = content.trim().split(sep);
+
+ let totalLines = lines.length;
+ let currentLine = 0;
+ let record = [];
+
+ for (let line of lines) {
+ currentLine++;
+ if (!line) {
+ continue;
+ }
+
+ if (line.toLowerCase().trimEnd() == "begin:vcard") {
+ if (record.length) {
+ throw Components.Exception(
+ "Expecting END:VCARD but got BEGIN:VCARD",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ record.push(line);
+ continue;
+ } else if (!record.length) {
+ throw Components.Exception(
+ `Expecting BEGIN:VCARD but got ${line}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ record.push(line);
+
+ if (line.toLowerCase().trimEnd() == "end:vcard") {
+ this._targetDirectory.addCard(
+ vcardService.vCardToAbCard(record.join("\n") + "\n")
+ );
+ record = [];
+ this.onProgress(currentLine, totalLines);
+ // Give UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(totalLines, totalLines);
+ }
+
+ /**
+ * Import the .sqlite source file into the target directory.
+ */
+ async _importSqliteFile() {
+ this.onProgress(2, 10);
+ // Create a temporary address book.
+ let dirId = MailServices.ab.newAddressBook(
+ "tmp",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let tmpDirectory = MailServices.ab.getDirectoryFromId(dirId);
+
+ try {
+ // Close the connection to release the file handler.
+ await tmpDirectory.cleanUp();
+ // Overwrite the sqlite database file.
+ this._sourceFile.copyTo(
+ Services.dirsvc.get("ProfD", Ci.nsIFile),
+ tmpDirectory.fileName
+ );
+ // Write-Ahead Logging file contains changes not written to .sqlite file
+ // yet.
+ let sourceWalFile = this._sourceFile.parent.clone();
+ sourceWalFile.append(this._sourceFile.leafName + "-wal");
+ if (sourceWalFile.exists()) {
+ sourceWalFile.copyTo(
+ Services.dirsvc.get("ProfD", Ci.nsIFile),
+ tmpDirectory.fileName + "-wal"
+ );
+ }
+ // Open a new connection to use the new database file.
+ let uri = tmpDirectory.URI;
+ tmpDirectory = Cc[
+ "@mozilla.org/addressbook/directory;1?type=jsaddrbook"
+ ].createInstance(Ci.nsIAbDirectory);
+ tmpDirectory.init(uri);
+
+ for (let card of tmpDirectory.childCards) {
+ this._targetDirectory.addCard(card);
+ }
+ this.onProgress(8, 10);
+
+ for (let sourceList of tmpDirectory.childNodes) {
+ let targetList = this._targetDirectory.getMailListFromName(
+ sourceList.dirName
+ );
+ if (!targetList) {
+ targetList = this._targetDirectory.addMailList(sourceList);
+ }
+ for (let card of sourceList.childCards) {
+ targetList.addCard(card);
+ }
+ }
+ this.onProgress(10, 10);
+ } finally {
+ MailServices.ab.deleteAddressBook(tmpDirectory.URI);
+ }
+ }
+
+ /**
+ * Import the .mab source file into the target directory.
+ */
+ async _importMabFile() {
+ this.onProgress(2, 10);
+ let importMab = Cc[
+ "@mozilla.org/import/import-ab-file;1?type=mab"
+ ].createInstance(Ci.nsIImportABFile);
+ importMab.readFileToDirectory(this._sourceFile, this._targetDirectory);
+ this.onProgress(10, 10);
+ }
+}
diff --git a/comm/mailnews/import/modules/AppleMailProfileImporter.jsm b/comm/mailnews/import/modules/AppleMailProfileImporter.jsm
new file mode 100644
index 0000000000..c6c6e5b4ba
--- /dev/null
+++ b/comm/mailnews/import/modules/AppleMailProfileImporter.jsm
@@ -0,0 +1,92 @@
+/* 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 = ["AppleMailProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an apple mail profile dir into the current
+ * profile.
+ */
+class AppleMailProfileImporter extends BaseProfileImporter {
+ USE_FILE_PICKER = true;
+
+ SUPPORTED_ITEMS = {
+ accounts: false,
+ addressBooks: false,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-applemail;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ this._importMailGeneric.SetData("mailLocation", sourceProfileDir);
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/BaseProfileImporter.jsm b/comm/mailnews/import/modules/BaseProfileImporter.jsm
new file mode 100644
index 0000000000..18c5dce16b
--- /dev/null
+++ b/comm/mailnews/import/modules/BaseProfileImporter.jsm
@@ -0,0 +1,100 @@
+/* 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 = ["BaseProfileImporter"];
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * An object to represent a source profile to import from.
+ *
+ * @typedef {object} SourceProfile
+ * @property {string} name - The profile name.
+ * @property {nsIFile} dir - The profile location.
+ *
+ * An object to represent items to import.
+ * @typedef {object} ImportItems
+ * @property {boolean} accounts - Whether to import accounts and settings.
+ * @property {boolean} addressBooks - Whether to import address books.
+ * @property {boolean} calendars - Whether to import calendars.
+ * @property {boolean} mailMessages - Whether to import mail messages.
+ */
+
+/**
+ * Common interfaces shared by profile importers.
+ *
+ * @abstract
+ */
+class BaseProfileImporter {
+ /** @type boolean - Whether to allow importing from a user picked dir. */
+ USE_FILE_PICKER = true;
+
+ /** @type ImportItems */
+ SUPPORTED_ITEMS = {
+ accounts: true,
+ addressBooks: true,
+ calendars: true,
+ mailMessages: true,
+ };
+
+ /** When importing from a zip file, ignoring these folders. */
+ IGNORE_DIRS = [];
+
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ /**
+ * @returns {SourceProfile[]} Profiles found on this machine.
+ */
+ async getSourceProfiles() {
+ throw Components.Exception(
+ `getSourceProfiles not implemented in ${this.constructor.name}`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Actually start importing things to the current profile.
+ *
+ * @param {nsIFile} sourceProfileDir - The source location to import from.
+ * @param {ImportItems} items - The items to import.
+ * @returns {boolean} Returns true when accounts have been imported, which
+ * means a restart is needed. Otherwise, no restart is needed.
+ */
+ async startImport(sourceProfileDir, items) {
+ throw Components.Exception(
+ `startImport not implemented in ${this.constructor.name}`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Reset use_without_mail_account, so that imported accounts are correctly
+ * rendered in the folderPane.
+ */
+ _onImportAccounts() {
+ Services.prefs.setBoolPref("app.use_without_mail_account", false);
+ }
+
+ /**
+ * Increase _itemsImportedCount by one, and call onProgress.
+ */
+ async _updateProgress() {
+ this.onProgress(++this._itemsImportedCount, this._itemsTotalCount);
+ return new Promise(resolve => setTimeout(resolve));
+ }
+
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+}
diff --git a/comm/mailnews/import/modules/BeckyProfileImporter.jsm b/comm/mailnews/import/modules/BeckyProfileImporter.jsm
new file mode 100644
index 0000000000..7c81ca7b66
--- /dev/null
+++ b/comm/mailnews/import/modules/BeckyProfileImporter.jsm
@@ -0,0 +1,125 @@
+/* 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 = ["BeckyProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an becky profile dir into the current
+ * profile.
+ */
+class BeckyProfileImporter extends BaseProfileImporter {
+ SUPPORTED_ITEMS = {
+ accounts: false,
+ addressBooks: true,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-becky;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ this._importMailGeneric.SetData("mailLocation", sourceProfileDir);
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ if (items.addressBooks) {
+ successStr.data = "";
+ errorStr.data = "";
+
+ let importABGeneric = this._importModule
+ .GetImportInterface("addressbook")
+ .QueryInterface(Ci.nsIImportGeneric);
+ importABGeneric.SetData("addressLocation", sourceProfileDir);
+
+ // @see nsIImportGeneric.
+ let wantsProgress = importABGeneric.WantsProgress();
+ importABGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (importABGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import address books progress:",
+ importABGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ importABGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing address books:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import address books:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/CalendarFileImporter.jsm b/comm/mailnews/import/modules/CalendarFileImporter.jsm
new file mode 100644
index 0000000000..ffa69feafb
--- /dev/null
+++ b/comm/mailnews/import/modules/CalendarFileImporter.jsm
@@ -0,0 +1,127 @@
+/* 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 = ["CalendarFileImporter"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ cal: "resource:///modules/calendar/calUtils.jsm",
+});
+
+/**
+ * A module to import iCalendar (.ics) file.
+ */
+class CalendarFileImporter {
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+
+ /**
+ * Parse an ics file to an array of items.
+ *
+ * @param {string} file - The file path of an ics file.
+ * @returns {calIItemBase[]}
+ */
+ async parseIcsFile(file) {
+ this._logger.debug(`Getting items from ${file.path}`);
+ let importer = Cc["@mozilla.org/calendar/import;1?type=ics"].getService(
+ Ci.calIImporter
+ );
+
+ let inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ let items = [];
+
+ try {
+ // 0x01 means MODE_RDONLY.
+ inputStream.init(file, 0x01, 0o444, {});
+ items = importer.importFromStream(inputStream);
+ if (!items.length) {
+ throw new Error("noItemsFound");
+ }
+ } catch (e) {
+ this._logger.error(e);
+ throw e;
+ } finally {
+ inputStream.close();
+ }
+
+ return items;
+ }
+
+ /**
+ * Get all calendars that the current user can import items to.
+ *
+ * @returns {calICalendar[]}
+ */
+ getTargetCalendars() {
+ let calendars = lazy.cal.manager
+ .getCalendars()
+ .filter(
+ calendar =>
+ !calendar.getProperty("disabled") &&
+ !calendar.readOnly &&
+ lazy.cal.acl.userCanAddItemsToCalendar(calendar)
+ );
+ let sortOrderPref = Services.prefs.getCharPref(
+ "calendar.list.sortOrder",
+ ""
+ );
+ let sortOrder = sortOrderPref ? sortOrderPref.split(" ") : [];
+ return calendars.sort(
+ (x, y) => sortOrder.indexOf(x.id) - sortOrder.indexOf(y.id)
+ );
+ }
+
+ /**
+ * Actually start importing items into a calendar.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @param {calICalendar} targetCalendar - The calendar to import into.
+ */
+ async startImport(items, targetCalendar) {
+ let count = 0;
+ let total = items.length;
+
+ this._logger.debug(`Importing ${total} items into ${targetCalendar.name}`);
+
+ for (let item of items) {
+ try {
+ await targetCalendar.addItem(item);
+ } catch (e) {
+ this._logger.error(e);
+ throw e;
+ }
+
+ count++;
+
+ if (count % 10 == 0) {
+ this.onProgress(count, total);
+ // Give the UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(total, total);
+ }
+}
diff --git a/comm/mailnews/import/modules/OutlookProfileImporter.jsm b/comm/mailnews/import/modules/OutlookProfileImporter.jsm
new file mode 100644
index 0000000000..6c2b5932f2
--- /dev/null
+++ b/comm/mailnews/import/modules/OutlookProfileImporter.jsm
@@ -0,0 +1,143 @@
+/* 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 = ["OutlookProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an outlook profile dir into the current
+ * profile.
+ */
+class OutlookProfileImporter extends BaseProfileImporter {
+ USE_FILE_PICKER = false;
+
+ SUPPORTED_ITEMS = {
+ accounts: true,
+ addressBooks: true,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-outlook;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ if (items.accounts) {
+ let importSettings = this._importModule
+ .GetImportInterface("settings")
+ .QueryInterface(Ci.nsIImportSettings);
+ let outLocalAccount = {};
+ importSettings.Import(outLocalAccount);
+ await this._updateProgress();
+ this._onImportAccounts();
+ }
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ if (items.addressBooks) {
+ successStr.data = "";
+ errorStr.data = "";
+
+ let importABGeneric = this._importModule
+ .GetImportInterface("addressbook")
+ .QueryInterface(Ci.nsIImportGeneric);
+ // Set import destination to the personal address book.
+ let addressDestination = Cc[
+ "@mozilla.org/supports-string;1"
+ ].createInstance(Ci.nsISupportsString);
+ addressDestination.data = "jsaddrbook://abooks.sqlite";
+ importABGeneric.SetData("addressDestination", addressDestination);
+
+ // @see nsIImportGeneric.
+ let wantsProgress = importABGeneric.WantsProgress();
+ importABGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (importABGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import address books progress:",
+ importABGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ importABGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing address books:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import address books:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ return items.accounts;
+ }
+}
diff --git a/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm b/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm
new file mode 100644
index 0000000000..e96cc01a2d
--- /dev/null
+++ b/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm
@@ -0,0 +1,75 @@
+/* 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 = ["SeamonkeyProfileImporter"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { ThunderbirdProfileImporter } = ChromeUtils.import(
+ "resource:///modules/ThunderbirdProfileImporter.jsm"
+);
+
+/**
+ * A module to import things from a seamonkey profile dir into the current
+ * profile.
+ */
+class SeamonkeyProfileImporter extends ThunderbirdProfileImporter {
+ NAME = "SeaMonkey";
+
+ /** @see BaseProfileImporter */
+ async getSourceProfiles() {
+ let slugs = {
+ win: ["AppData", "Mozilla", "SeaMonkey"],
+ macosx: ["ULibDir", "Application Support", "SeaMonkey"],
+ linux: ["Home", ".mozilla", "seamonkey"],
+ }[AppConstants.platform];
+ if (!slugs) {
+ // We don't recognize this OS.
+ return [];
+ }
+
+ let seamonkeyRoot = Services.dirsvc.get(slugs[0], Ci.nsIFile);
+ slugs.slice(1).forEach(seamonkeyRoot.append);
+ let profilesIni = seamonkeyRoot.clone();
+ profilesIni.append("profiles.ini");
+ if (!profilesIni.exists()) {
+ // No Seamonkey profile found in the well known location.
+ return [];
+ }
+
+ let profiles = [];
+ let ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory)
+ .createINIParser(profilesIni);
+ for (let section of ini.getSections()) {
+ let keys = [...ini.getKeys(section)];
+ if (!keys.includes("Path")) {
+ // Not a profile section.
+ continue;
+ }
+
+ let name = keys.includes("Name") ? ini.getString(section, "Name") : null;
+ let path = ini.getString(section, "Path");
+ let isRelative = keys.includes("IsRelative")
+ ? ini.getString(section, "IsRelative") == "1"
+ : false;
+
+ let dir;
+ if (isRelative) {
+ dir = seamonkeyRoot.clone();
+ dir.append(path);
+ } else {
+ dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(path);
+ }
+ if (!dir.exists()) {
+ // Not a valid profile.
+ continue;
+ }
+ profiles.push({ name, dir });
+ }
+ return profiles;
+ }
+}
diff --git a/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm b/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm
new file mode 100644
index 0000000000..082813309e
--- /dev/null
+++ b/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm
@@ -0,0 +1,1024 @@
+/* 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 = ["ThunderbirdProfileImporter"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { AddrBookFileImporter } = ChromeUtils.import(
+ "resource:///modules/AddrBookFileImporter.jsm"
+);
+
+/**
+ * A pref is represented as [type, name, value].
+ *
+ * @typedef {["Bool"|"Char"|"Int", string, number|string|boolean]} PrefItem
+ *
+ * A map from source smtp server key to target smtp server key.
+ * @typedef {Map<string, string>} SmtpServerKeyMap
+ *
+ * A map from source identity key to target identity key.
+ * @typedef {Map<string, string>} IdentityKeyMap
+ *
+ * A map from source IM account key to target IM account key.
+ * @typedef {Map<string, string>} IMAccountKeyMap
+ *
+ * A map from source incoming server key to target incoming server key.
+ * @typedef {Map<string, string>} IncomingServerKeyMap
+ */
+
+// Pref branches that need special handling.
+const ACCOUNT_MANAGER = "mail.accountmanager.";
+const MAIL_IDENTITY = "mail.identity.";
+const MAIL_SERVER = "mail.server.";
+const MAIL_ACCOUNT = "mail.account.";
+const IM_ACCOUNT = "messenger.account.";
+const MAIL_SMTP = "mail.smtp.";
+const SMTP_SERVER = "mail.smtpserver.";
+const ADDRESS_BOOK = "ldap_2.servers.";
+const LDAP_AUTO_COMPLETE = "ldap_2.autoComplete.";
+const CALENDAR = "calendar.registry.";
+const CALENDAR_LIST = "calendar.list.";
+
+// Prefs (branches) that we do not want to copy directly.
+const IGNORE_PREFS = [
+ "app.update.",
+ "browser.",
+ "calendar.timezone",
+ "devtools.",
+ "extensions.",
+ "mail.cloud_files.accounts.",
+ "mail.newsrc_root",
+ "mail.root.",
+ "mail.smtpservers",
+ "messenger.accounts",
+ "print.",
+ "services.",
+ "toolkit.telemetry.",
+];
+
+/**
+ * A module to import things from another thunderbird profile dir into the
+ * current profile.
+ */
+class ThunderbirdProfileImporter extends BaseProfileImporter {
+ NAME = "Thunderbird";
+
+ IGNORE_DIRS = [
+ "chrome_debugger_profile",
+ "crashes",
+ "datareporting",
+ "extensions",
+ "extension-store",
+ "logs",
+ "minidumps",
+ "saved-telemetry-pings",
+ "security_state",
+ "storage",
+ "xulstore",
+ ];
+
+ async getSourceProfiles() {
+ let profileService = Cc[
+ "@mozilla.org/toolkit/profile-service;1"
+ ].getService(Ci.nsIToolkitProfileService);
+ let sourceProfiles = [];
+ for (let profile of profileService.profiles) {
+ if (profile == profileService.currentProfile) {
+ continue;
+ }
+ sourceProfiles.push({
+ name: profile.name,
+ dir: profile.rootDir,
+ });
+ }
+ return sourceProfiles;
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+
+ this._sourceProfileDir = sourceProfileDir;
+ this._items = items;
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ try {
+ this._localServer = MailServices.accounts.localFoldersServer;
+ } catch (e) {}
+
+ if (items.accounts || items.addressBooks || items.calendars) {
+ await this._loadPreferences();
+ }
+
+ if (this._items.accounts) {
+ await this._importServersAndAccounts();
+ this._importOtherPrefs(this._otherPrefs);
+ await this._updateProgress();
+ }
+
+ if (this._items.addressBooks) {
+ await this._importAddressBooks(
+ this._branchPrefsMap.get(ADDRESS_BOOK),
+ this._collectPrefsToObject(this._branchPrefsMap.get(LDAP_AUTO_COMPLETE))
+ );
+ await this._updateProgress();
+ }
+
+ if (this._items.calendars) {
+ this._importCalendars(
+ this._branchPrefsMap.get(CALENDAR),
+ this._collectPrefsToObject(this._branchPrefsMap.get(CALENDAR_LIST))
+ );
+ await this._updateProgress();
+ }
+
+ if (!this._items.accounts && this._items.mailMessages) {
+ this._importMailMessagesToLocal();
+ }
+
+ await this._updateProgress();
+
+ return true;
+ }
+
+ /**
+ * Collect interested prefs from this._sourceProfileDir.
+ */
+ async _loadPreferences() {
+ // A Map to collect all prefs in interested pref branches.
+ // @type {Map<string, PrefItem[]>}
+ this._branchPrefsMap = new Map([
+ [ACCOUNT_MANAGER, []],
+ [MAIL_IDENTITY, []],
+ [MAIL_SERVER, []],
+ [MAIL_ACCOUNT, []],
+ [IM_ACCOUNT, []],
+ [MAIL_SMTP, []],
+ [SMTP_SERVER, []],
+ [ADDRESS_BOOK, []],
+ [LDAP_AUTO_COMPLETE, []],
+ [CALENDAR, []],
+ [CALENDAR_LIST, []],
+ ]);
+ this._otherPrefs = [];
+
+ let sourcePrefsFile = this._sourceProfileDir.clone();
+ sourcePrefsFile.append("prefs.js");
+ let sourcePrefsBuffer = await IOUtils.read(sourcePrefsFile.path);
+
+ let savePref = (type, name, value) => {
+ for (let [branchName, branchPrefs] of this._branchPrefsMap) {
+ if (name.startsWith(branchName)) {
+ branchPrefs.push([type, name.slice(branchName.length), value]);
+ return;
+ }
+ }
+ if (IGNORE_PREFS.some(ignore => name.startsWith(ignore))) {
+ return;
+ }
+ // Collect all the other prefs.
+ this._otherPrefs.push([type, name, value]);
+ };
+
+ Services.prefs.parsePrefsFromBuffer(sourcePrefsBuffer, {
+ onStringPref: (kind, name, value) => savePref("Char", name, value),
+ onIntPref: (kind, name, value) => savePref("Int", name, value),
+ onBoolPref: (kind, name, value) => savePref("Bool", name, value),
+ onError: msg => {
+ throw new Error(msg);
+ },
+ });
+ }
+
+ /**
+ * Import all the servers and accounts.
+ */
+ async _importServersAndAccounts() {
+ // Import SMTP servers first, the importing order is important.
+ let smtpServerKeyMap = this._importSmtpServers(
+ this._branchPrefsMap.get(SMTP_SERVER),
+ this._collectPrefsToObject(this._branchPrefsMap.get(MAIL_SMTP))
+ .defaultserver
+ );
+
+ // mail.identity.idN.smtpServer depends on transformed smtp server key.
+ let identityKeyMap = this._importIdentities(
+ this._branchPrefsMap.get(MAIL_IDENTITY),
+ smtpServerKeyMap
+ );
+ let imAccountKeyMap = await this._importIMAccounts(
+ this._branchPrefsMap.get(IM_ACCOUNT)
+ );
+
+ let accountManager = this._collectPrefsToObject(
+ this._branchPrefsMap.get(ACCOUNT_MANAGER)
+ );
+ // Officially we only support one Local Folders account, if we already have
+ // one, do not import a new one.
+ this._sourceLocalServerKeyToSkip = this._localServer
+ ? accountManager.localfoldersserver
+ : null;
+ this._sourceLocalServerAttrs = {};
+
+ // mail.server.serverN.imAccount depends on transformed im account key.
+ let incomingServerKeyMap = await this._importIncomingServers(
+ this._branchPrefsMap.get(MAIL_SERVER),
+ imAccountKeyMap
+ );
+
+ // mail.account.accountN.{identities, server} depends on previous steps.
+ this._importAccounts(
+ this._branchPrefsMap.get(MAIL_ACCOUNT),
+ accountManager.accounts,
+ accountManager.defaultaccount,
+ identityKeyMap,
+ incomingServerKeyMap
+ );
+
+ await this._importMailMessages(incomingServerKeyMap);
+ if (this._sourceLocalServerKeyToSkip) {
+ this._mergeLocalFolders();
+ }
+
+ if (accountManager.accounts) {
+ this._onImportAccounts();
+ }
+ }
+
+ /**
+ * Collect an array of prefs to an object.
+ *
+ * @param {PrefItem[]} prefs - An array of prefs.
+ * @returns {object} An object mapping pref name to pref value.
+ */
+ _collectPrefsToObject(prefs) {
+ let obj = {};
+ for (let [, name, value] of prefs) {
+ obj[name] = value;
+ }
+ return obj;
+ }
+
+ /**
+ * Import SMTP servers.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the SMTP_SERVER branch.
+ * @param {string} sourceDefaultServer - The value of mail.smtp.defaultserver
+ * in the source profile.
+ * @returns {smtpServerKeyMap} A map from source server key to new server key.
+ */
+ _importSmtpServers(prefs, sourceDefaultServer) {
+ let smtpServerKeyMap = new Map();
+ let branch = Services.prefs.getBranch(SMTP_SERVER);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newServerKey = smtpServerKeyMap.get(key);
+ if (!newServerKey) {
+ // For every smtp server, create a new one to avoid conflicts.
+ let server = MailServices.smtp.createServer();
+ newServerKey = server.key;
+ smtpServerKeyMap.set(key, newServerKey);
+ this._logger.debug(
+ `Mapping SMTP server from ${key} to ${newServerKey}`
+ );
+ }
+
+ let newName = `${newServerKey}${name.slice(key.length)}`;
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ // Set defaultserver if it doesn't already exist.
+ let defaultServer = Services.prefs.getCharPref(
+ "mail.smtp.defaultserver",
+ ""
+ );
+ if (sourceDefaultServer && !defaultServer) {
+ Services.prefs.setCharPref(
+ "mail.smtp.defaultserver",
+ smtpServerKeyMap.get(sourceDefaultServer)
+ );
+ }
+ return smtpServerKeyMap;
+ }
+
+ /**
+ * Import mail identites.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_IDENTITY branch.
+ * @param {SmtpServerKeyMap} smtpServerKeyMap - A map from the source SMTP
+ * server key to new SMTP server key.
+ * @returns {IdentityKeyMap} A map from the source identity key to new identity
+ * key.
+ */
+ _importIdentities(prefs, smtpServerKeyMap) {
+ let identityKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_IDENTITY);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newIdentityKey = identityKeyMap.get(key);
+ if (!newIdentityKey) {
+ // For every identity, create a new one to avoid conflicts.
+ let identity = MailServices.accounts.createIdentity();
+ newIdentityKey = identity.key;
+ identityKeyMap.set(key, newIdentityKey);
+ this._logger.debug(`Mapping identity from ${key} to ${newIdentityKey}`);
+ }
+
+ let newName = `${newIdentityKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (name.endsWith(".smtpServer")) {
+ newValue = smtpServerKeyMap.get(value) || newValue;
+ }
+ branch[`set${type}Pref`](newName, newValue);
+ }
+ return identityKeyMap;
+ }
+
+ /**
+ * Import IM accounts.
+ *
+ * @param {Array<[string, string, number|string|boolean]>} prefs - All source
+ * prefs in the IM_ACCOUNT branch.
+ * @returns {IMAccountKeyMap} A map from the source account key to new account
+ * key.
+ */
+ async _importIMAccounts(prefs) {
+ let imAccountKeyMap = new Map();
+ let branch = Services.prefs.getBranch(IM_ACCOUNT);
+
+ let lastKey = 1;
+ function _getUniqueAccountKey() {
+ let key = `account${lastKey++}`;
+ if (Services.prefs.getCharPref(`messenger.account.${key}.name`, "")) {
+ return _getUniqueAccountKey();
+ }
+ return key;
+ }
+
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newAccountKey = imAccountKeyMap.get(key);
+ if (!newAccountKey) {
+ // For every account, create a new one to avoid conflicts.
+ newAccountKey = _getUniqueAccountKey();
+ imAccountKeyMap.set(key, newAccountKey);
+ this._logger.debug(
+ `Mapping IM account from ${key} to ${newAccountKey}`
+ );
+ }
+
+ let newName = `${newAccountKey}${name.slice(key.length)}`;
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ return imAccountKeyMap;
+ }
+
+ /**
+ * Import incoming servers.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_SERVER branch.
+ * @param {IMAccountKeyMap} imAccountKeyMap - A map from the source account
+ * key to new account key.
+ * @returns {IncomingServerKeyMap} A map from the source server key to new
+ * server key.
+ */
+ async _importIncomingServers(prefs, imAccountKeyMap) {
+ let incomingServerKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_SERVER);
+
+ let lastKey = 1;
+ function _getUniqueIncomingServerKey() {
+ let key = `server${lastKey++}`;
+ if (branch.getCharPref(`${key}.type`, "")) {
+ return _getUniqueIncomingServerKey();
+ }
+ return key;
+ }
+
+ for (let [type, name, value] of prefs) {
+ let [key, attr] = name.split(".");
+ if (key == this._sourceLocalServerKeyToSkip) {
+ if (["directory", "directory-rel"].includes(attr)) {
+ this._sourceLocalServerAttrs[attr] = value;
+ }
+ // We already have a Local Folders account.
+ continue;
+ }
+ if (attr == "deferred_to_account") {
+ // Handling deferred account is a bit complicated, to prevent potential
+ // problems, just skip this pref so it becomes a normal account.
+ continue;
+ }
+ let newServerKey = incomingServerKeyMap.get(key);
+ if (!newServerKey) {
+ // For every incoming server, create a new one to avoid conflicts.
+ newServerKey = _getUniqueIncomingServerKey();
+ incomingServerKeyMap.set(key, newServerKey);
+ this._logger.debug(`Mapping server from ${key} to ${newServerKey}`);
+ }
+
+ let newName = `${newServerKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (newName.endsWith(".imAccount")) {
+ newValue = imAccountKeyMap.get(value);
+ }
+ branch[`set${type}Pref`](newName, newValue || value);
+ }
+ return incomingServerKeyMap;
+ }
+
+ /**
+ * Import mail accounts.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_ACCOUNT branch.
+ * @param {string} sourceAccounts - The value of mail.accountmanager.accounts
+ * in the source profile.
+ * @param {string} sourceDefaultAccount - The value of
+ * mail.accountmanager.defaultaccount in the source profile.
+ * @param {IdentityKeyMap} identityKeyMap - A map from the source identity key
+ * to new identity key.
+ * @param {IncomingServerKeyMap} incomingServerKeyMap - A map from the source
+ * server key to new server key.
+ */
+ _importAccounts(
+ prefs,
+ sourceAccounts,
+ sourceDefaultAccount,
+ identityKeyMap,
+ incomingServerKeyMap
+ ) {
+ let accountKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_ACCOUNT);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ if (key == "lastKey" || value == this._sourceLocalServerKeyToSkip) {
+ continue;
+ }
+ let newAccountKey = accountKeyMap.get(key);
+ if (!newAccountKey) {
+ // For every account, create a new one to avoid conflicts.
+ newAccountKey = MailServices.accounts.getUniqueAccountKey();
+ accountKeyMap.set(key, newAccountKey);
+ }
+
+ let newName = `${newAccountKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (name.endsWith(".identities")) {
+ // An account can have multiple identities.
+ newValue = value
+ .split(",")
+ .map(v => identityKeyMap.get(v))
+ .filter(Boolean)
+ .join(",");
+ } else if (name.endsWith(".server")) {
+ newValue = incomingServerKeyMap.get(value);
+ }
+ branch[`set${type}Pref`](newName, newValue || value);
+ }
+
+ // Append newly create accounts to mail.accountmanager.accounts.
+ let accounts = Services.prefs
+ .getCharPref("mail.accountmanager.accounts", "")
+ .split(",");
+ if (sourceAccounts) {
+ for (let sourceAccountKey of sourceAccounts.split(",")) {
+ accounts.push(accountKeyMap.get(sourceAccountKey));
+ }
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ accounts.filter(Boolean).join(",")
+ );
+ }
+
+ // Set defaultaccount if it doesn't already exist.
+ let defaultAccount = Services.prefs.getCharPref(
+ "mail.accountmanager.defaultaccount",
+ ""
+ );
+ if (sourceDefaultAccount && !defaultAccount) {
+ Services.prefs.setCharPref(
+ "mail.accountmanager.defaultaccount",
+ accountKeyMap.get(sourceDefaultAccount)
+ );
+ }
+ }
+
+ /**
+ * Try to locate a file specified by the relative path, if not possible, use
+ * the absolute path.
+ *
+ * @param {string} relValue - The pref value for the relative file path.
+ * @param {string} absValue - The pref value for the absolute file path.
+ * @returns {nsIFile}
+ */
+ _getSourceFileFromPaths(relValue, absValue) {
+ let relPath = relValue.slice("[ProfD]".length);
+ let parts = relPath.split("/");
+ if (!relValue.startsWith("[ProfD]") || parts.includes("..")) {
+ // If we don't recognize this path or if it's a path outside the ProfD,
+ // use absValue instead.
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ try {
+ file.initWithPath(absValue);
+ } catch (e) {
+ this._logger.warn("nsIFile.initWithPath failed for path=", absValue);
+ return null;
+ }
+ return file;
+ }
+
+ let sourceFile = this._sourceProfileDir.clone();
+ for (let part of parts) {
+ sourceFile.append(part);
+ }
+ return sourceFile;
+ }
+
+ /**
+ * Copy mail folders from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {PrefKeyMap} incomingServerKeyMap - A map from the source server key
+ * to new server key.
+ */
+ async _importMailMessages(incomingServerKeyMap) {
+ for (let key of incomingServerKeyMap.values()) {
+ let branch = Services.prefs.getBranch(`${MAIL_SERVER}${key}.`);
+ if (!branch) {
+ continue;
+ }
+ let type = branch.getCharPref("type", "");
+ let hostname = branch.getCharPref("hostname", "");
+ if (!type || !hostname) {
+ continue;
+ }
+
+ let targetDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ if (type == "imap") {
+ targetDir.append("ImapMail");
+ } else if (type == "nntp") {
+ targetDir.append("News");
+ } else if (["none", "pop3", "rss"].includes(type)) {
+ targetDir.append("Mail");
+ } else {
+ continue;
+ }
+
+ this._logger.debug("Importing mail messages for", key);
+
+ let sourceDir = this._getSourceFileFromPaths(
+ branch.getCharPref("directory-rel", ""),
+ branch.getCharPref("directory", "")
+ );
+ if (sourceDir?.exists()) {
+ // Use the hostname as mail folder name and ensure it's unique.
+ targetDir.append(hostname);
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ this._recursivelyCopyMsgFolder(sourceDir, targetDir);
+ branch.setCharPref("directory", targetDir.path);
+ // .directory-rel may be outdated, it will be created when first needed.
+ branch.clearUserPref("directory-rel");
+ }
+
+ if (type == "nntp") {
+ let targetNewsrc = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ targetNewsrc.append("News");
+ targetNewsrc.append(`newsrc-${hostname}`);
+ targetNewsrc.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ let sourceNewsrc = this._getSourceFileFromPaths(
+ branch.getCharPref("newsrc.file-rel", ""),
+ branch.getCharPref("newsrc.file", "")
+ );
+ if (sourceNewsrc?.exists()) {
+ this._logger.debug(
+ `Copying ${sourceNewsrc.path} to ${targetNewsrc.path}`
+ );
+ sourceNewsrc.copyTo(targetNewsrc.parent, targetNewsrc.leafName);
+ }
+
+ branch.setCharPref("newsrc.file", targetNewsrc.path);
+ // .file-rel may be outdated, it will be created when first needed.
+ branch.clearUserPref("newsrc.file-rel");
+ }
+ }
+ }
+
+ /**
+ * Merge Local Folders from the source profile into the current profile.
+ * Source Local Folders become a subfoler of the current Local Folders.
+ */
+ _mergeLocalFolders() {
+ let sourceDir = this._getSourceFileFromPaths(
+ this._sourceLocalServerAttrs["directory-rel"],
+ this._sourceLocalServerAttrs.directory
+ );
+ if (!sourceDir?.exists()) {
+ return;
+ }
+ let rootMsgFolder = this._localServer.rootMsgFolder;
+ let folderName = rootMsgFolder.generateUniqueSubfolderName(
+ "Local Folders",
+ null
+ );
+ rootMsgFolder.createSubfolder(folderName, null);
+ let targetDir = rootMsgFolder.filePath;
+ targetDir.append(folderName + ".sbd");
+ this._logger.debug(
+ `Copying ${sourceDir.path} to ${targetDir.path} in Local Folders`
+ );
+ this._recursivelyCopyMsgFolder(sourceDir, targetDir, true);
+ }
+
+ /**
+ * Copy a source msg folder to a destination.
+ *
+ * @param {nsIFile} sourceDir - The source msg folder location.
+ * @param {nsIFile} targetDir - The target msg folder location.
+ * @param {boolean} isTargetLocal - Whether the targetDir is a subfolder in
+ * the Local Folders.
+ */
+ _recursivelyCopyMsgFolder(sourceDir, targetDir, isTargetLocal) {
+ this._logger.debug(`Copying ${sourceDir.path} to ${targetDir.path}`);
+
+ // Copy the whole sourceDir.
+ if (!isTargetLocal && this._items.accounts && this._items.mailMessages) {
+ // Remove the folder so that nsIFile.copyTo doesn't copy into targetDir.
+ targetDir.remove(false);
+ sourceDir.copyTo(targetDir.parent, targetDir.leafName);
+ return;
+ }
+
+ for (let entry of sourceDir.directoryEntries) {
+ if (entry.isDirectory()) {
+ let newFolder = targetDir.clone();
+ newFolder.append(entry.leafName);
+ newFolder.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ this._recursivelyCopyMsgFolder(entry, newFolder);
+ } else {
+ let leafName = entry.leafName;
+ let extName = leafName.slice(leafName.lastIndexOf(".") + 1);
+ if (isTargetLocal) {
+ // When copying to Local Folders, drop database files so that special
+ // folders (Inbox, Trash) become normal folders. Otherwise, imported
+ // special folders can't be deleted.
+ if (extName != "msf") {
+ entry.copyTo(targetDir, leafName);
+ }
+ } else if (
+ this._items.accounts &&
+ extName != leafName &&
+ ["msf", "dat"].includes(extName)
+ ) {
+ // Copy only the folder structure, databases and filter rules.
+ // Ignore the messages themselves.
+ entry.copyTo(targetDir, leafName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import msg folders from this._sourceProfileDir into the Local Folders of
+ * the current profile.
+ */
+ _importMailMessagesToLocal() {
+ // Make sure Local Folders exist first.
+ if (!this._localServer) {
+ MailServices.accounts.createLocalMailAccount();
+ this._localServer = MailServices.accounts.localFoldersServer;
+ }
+ let localMsgFolder = this._localServer.rootMsgFolder;
+ let localRootDir = this._localServer.rootMsgFolder.filePath;
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+
+ // Create a "Thunderbird Import" folder, and import into it.
+ let wrapFolderName = localMsgFolder.generateUniqueSubfolderName(
+ bundle.formatStringFromName("ImportModuleFolderName", [this.NAME]),
+ null
+ );
+ localMsgFolder.createSubfolder(wrapFolderName, null);
+ let targetRootMsgFolder = localMsgFolder.getChildNamed(wrapFolderName);
+
+ // Import mail folders.
+ for (let name of ["ImapMail", "News", "Mail"]) {
+ let sourceDir = this._sourceProfileDir.clone();
+ sourceDir.append(name);
+ if (!sourceDir.exists()) {
+ continue;
+ }
+
+ for (let entry of sourceDir.directoryEntries) {
+ if (entry.isDirectory()) {
+ if (name == "Mail" && entry.leafName == "Feeds") {
+ continue;
+ }
+ let targetDir = localRootDir.clone();
+ let folderName = targetRootMsgFolder.generateUniqueSubfolderName(
+ entry.leafName,
+ null
+ );
+ targetRootMsgFolder.createSubfolder(folderName, null);
+ targetDir.append(wrapFolderName + ".sbd");
+ targetDir.append(folderName + ".sbd");
+ targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ this._recursivelyCopyMsgFolder(entry, targetDir, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import a pref from source only when this pref has no user value in the
+ * current profile.
+ *
+ * @param {PrefItem[]} prefs - All source prefs to try to import.
+ */
+ _importOtherPrefs(prefs) {
+ let tags = {};
+ for (let [type, name, value] of prefs) {
+ if (name.startsWith("mailnews.tags.")) {
+ let [, , key, attr] = name.split(".");
+ if (!tags[key]) {
+ tags[key] = {};
+ }
+ tags[key][attr] = value;
+ continue;
+ }
+ if (!Services.prefs.prefHasUserValue(name)) {
+ Services.prefs[`set${type}Pref`](name, value);
+ }
+ }
+
+ // Import tags, but do not overwrite existing customized tags.
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ for (let [key, { color, tag }] of Object.entries(tags)) {
+ if (!color || !tag) {
+ continue;
+ }
+ let currentTagColor, currentTagTag;
+ try {
+ currentTagColor = MailServices.tags.getColorForKey(key);
+ currentTagTag = MailServices.tags.getTagForKey(key);
+ } catch (e) {
+ // No tag exists for this key in the current profile, safe to write.
+ Services.prefs.setCharPref(`mailnews.tags.${key}.color`, color);
+ Services.prefs.setCharPref(`mailnews.tags.${key}.tag`, tag);
+ }
+ if (currentTagColor == color && currentTagTag == tag) {
+ continue;
+ }
+ if (
+ ["$label1", "$label2", "$label3", "$label4", "$label5"].includes(key)
+ ) {
+ let seq = key.at(-1);
+ let defaultColor = Services.prefs.getCharPref(
+ `mailnews.labels.color.${seq}`
+ );
+ let defaultTag = bundle.GetStringFromName(
+ `mailnews.labels.description.${seq}`
+ );
+ if (currentTagColor == defaultColor && currentTagTag == defaultTag) {
+ // The existing tag is in default state, safe to write.
+ Services.prefs.setCharPref(`mailnews.tags.${key}.color`, color);
+ Services.prefs.setCharPref(`mailnews.tags.${key}.tag`, tag);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import address books.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the ADDRESS_BOOK branch.
+ * @param {object} ldapAutoComplete - Pref values of LDAP_AUTO_COMPLETE branch.
+ * @param {boolean} ldapAutoComplete.useDirectory
+ * @param {string} ldapAutoComplete.directoryServer
+ */
+ async _importAddressBooks(prefs, ldapAutoComplete) {
+ let keyMap = new Map();
+ let branch = Services.prefs.getBranch(ADDRESS_BOOK);
+ for (let [type, name, value] of prefs) {
+ let [key, attr] = name.split(".");
+ if (["pab", "history"].includes(key)) {
+ continue;
+ }
+ if (attr == "uid") {
+ // Prevent duplicated uids when importing back, uid will be created when
+ // first used.
+ continue;
+ }
+ let newKey = keyMap.get(key);
+ if (!newKey) {
+ // For every address book, create a new one to avoid conflicts.
+ let uniqueCount = 0;
+ newKey = key;
+ while (true) {
+ if (!branch.getCharPref(`${newKey}.filename`, "")) {
+ break;
+ }
+ newKey = `${key}${++uniqueCount}`;
+ }
+ keyMap.set(key, newKey);
+ }
+
+ let newName = `${newKey}${name.slice(key.length)}`;
+ if (newName.endsWith(".dirType") && value == 2) {
+ // dirType=2 is a Mab file, we will migrate it in _copyAddressBookDatabases.
+ value = Ci.nsIAbManager.JS_DIRECTORY_TYPE;
+ }
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ // Transform the value of ldap_2.autoComplete.directoryServer if needed.
+ if (
+ ldapAutoComplete.useDirectory &&
+ ldapAutoComplete.directoryServer &&
+ !Services.prefs.getBoolPref(`${LDAP_AUTO_COMPLETE}useDirectory`, false)
+ ) {
+ let key = ldapAutoComplete.directoryServer.split("/").slice(-1)[0];
+ let newKey = keyMap.get(key);
+ if (newKey) {
+ Services.prefs.setBoolPref(`${LDAP_AUTO_COMPLETE}useDirectory`, true);
+ Services.prefs.setCharPref(
+ `${LDAP_AUTO_COMPLETE}directoryServer`,
+ `ldap_2.servers.${newKey}`
+ );
+ }
+ }
+
+ await this._copyAddressBookDatabases(keyMap);
+ }
+
+ /**
+ * Copy sqlite files from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {Map<string, string>} keyMap - A map from the source address
+ * book key to new address book key.
+ */
+ async _copyAddressBookDatabases(keyMap) {
+ let hasMabFile = false;
+
+ // Copy user created address books.
+ for (let key of keyMap.values()) {
+ let branch = Services.prefs.getBranch(`${ADDRESS_BOOK}${key}.`);
+ let filename = branch.getCharPref("filename", "");
+ if (!filename) {
+ continue;
+ }
+ let sourceFile = this._sourceProfileDir.clone();
+ sourceFile.append(filename);
+ if (!sourceFile.exists()) {
+ this._logger.debug(
+ `Ignoring non-existing address book file ${sourceFile.path}`
+ );
+ continue;
+ }
+
+ let leafName = sourceFile.leafName;
+ let isMabFile = leafName.endsWith(".mab");
+ if (isMabFile) {
+ leafName = leafName.slice(0, -4) + ".sqlite";
+ hasMabFile = true;
+ }
+ let targetFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ targetFile.append(leafName);
+ targetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ branch.setCharPref("filename", targetFile.leafName);
+ this._logger.debug(`Copying ${sourceFile.path} to ${targetFile.path}`);
+ if (isMabFile) {
+ await this._migrateMabToSqlite(sourceFile, targetFile);
+ } else {
+ sourceFile.copyTo(targetFile.parent, targetFile.leafName);
+ // Write-Ahead Logging file contains changes not written to .sqlite file
+ // yet.
+ let sourceWalFile = this._sourceProfileDir.clone();
+ sourceWalFile.append(filename + "-wal");
+ if (sourceWalFile.exists()) {
+ sourceWalFile.copyTo(targetFile.parent, targetFile.leafName + "-wal");
+ }
+ }
+ }
+
+ if (hasMabFile) {
+ await this._importMorkDatabase("abook");
+ await this._importMorkDatabase("history");
+ } else {
+ // Copy or import Personal Address Book.
+ await this._importAddressBookDatabase("abook.sqlite");
+ // Copy or import Collected Addresses.
+ await this._importAddressBookDatabase("history.sqlite");
+ }
+ }
+
+ /**
+ * Copy a sqlite file from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {string} filename - The name of the sqlite file.
+ */
+ async _importAddressBookDatabase(filename) {
+ let sourceFile = this._sourceProfileDir.clone();
+ sourceFile.append(filename);
+ if (!sourceFile.exists()) {
+ return;
+ }
+
+ let targetDirectory = MailServices.ab.getDirectory(
+ `jsaddrbook://${filename}`
+ );
+ if (!targetDirectory) {
+ sourceFile.copyTo(Services.dirsvc.get("ProfD", Ci.nsIFile), "");
+ return;
+ }
+
+ let importer = new AddrBookFileImporter("sqlite");
+ await importer.startImport(sourceFile, targetDirectory);
+ }
+
+ /**
+ * Migrate an address book .mab file to a .sqlite file.
+ *
+ * @param {nsIFile} sourceMabFile - The source .mab file.
+ * @param {nsIFile} targetSqliteFile - The target .sqlite file, should already
+ * exists in the profile dir.
+ */
+ async _migrateMabToSqlite(sourceMabFile, targetSqliteFile) {
+ // It's better to use MailServices.ab.getDirectory, but we need to refresh
+ // AddrBookManager first.
+ let targetDirectory = Cc[
+ "@mozilla.org/addressbook/directory;1?type=jsaddrbook"
+ ].createInstance(Ci.nsIAbDirectory);
+ targetDirectory.init(`jsaddrbook://${targetSqliteFile.leafName}`);
+
+ let importer = new AddrBookFileImporter("mab");
+ await importer.startImport(sourceMabFile, targetDirectory);
+ }
+
+ /**
+ * Import pab/history address book from mab file into the corresponding sqlite
+ * file.
+ *
+ * @param {string} basename - The filename without extension, e.g. "abook".
+ */
+ async _importMorkDatabase(basename) {
+ this._logger.debug(`Importing ${basename}.mab into ${basename}.sqlite`);
+
+ let sourceMabFile = this._sourceProfileDir.clone();
+ sourceMabFile.append(`${basename}.mab`);
+ if (!sourceMabFile.exists()) {
+ return;
+ }
+
+ let targetDirectory;
+ try {
+ targetDirectory = MailServices.ab.getDirectory(
+ `jsaddrbook://${basename}.sqlite`
+ );
+ } catch (e) {
+ this._logger.warn(`Failed to open ${basename}.sqlite`, e);
+ return;
+ }
+
+ let importer = new AddrBookFileImporter("mab");
+ await importer.startImport(sourceMabFile, targetDirectory);
+ }
+
+ /**
+ * Import calendars.
+ *
+ * For storage calendars, we need to import everything from the source
+ * local.sqlite to the target local.sqlite, which is not implemented yet, see
+ * bug 1719582.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the CALENDAR branch.
+ * @param {object} calendarList - Pref values of CALENDAR_LIST branch.
+ */
+ _importCalendars(prefs, calendarList) {
+ let branch = Services.prefs.getBranch(CALENDAR);
+ for (let [type, name, value] of prefs) {
+ branch[`set${type}Pref`](name, value);
+ }
+
+ if (calendarList.sortOrder) {
+ let prefName = `${CALENDAR_LIST}sortOrder`;
+ let prefValue =
+ Services.prefs.getCharPref(prefName, "") + " " + calendarList.sortOrder;
+ Services.prefs.setCharPref(prefName, prefValue.trim());
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/moz.build b/comm/mailnews/import/modules/moz.build
new file mode 100644
index 0000000000..5173f10d46
--- /dev/null
+++ b/comm/mailnews/import/modules/moz.build
@@ -0,0 +1,22 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "AddrBookFileImporter.jsm",
+ "BaseProfileImporter.jsm",
+ "CalendarFileImporter.jsm",
+ "SeamonkeyProfileImporter.jsm",
+ "ThunderbirdProfileImporter.jsm",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ EXTRA_JS_MODULES += [
+ "BeckyProfileImporter.jsm",
+ "OutlookProfileImporter.jsm",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ EXTRA_JS_MODULES += [
+ "AppleMailProfileImporter.jsm",
+ ]
diff --git a/comm/mailnews/import/public/moz.build b/comm/mailnews/import/public/moz.build
new file mode 100644
index 0000000000..1ece3d15f5
--- /dev/null
+++ b/comm/mailnews/import/public/moz.build
@@ -0,0 +1,20 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIImportABDescriptor.idl",
+ "nsIImportABFile.idl",
+ "nsIImportAddressBooks.idl",
+ "nsIImportFieldMap.idl",
+ "nsIImportFilters.idl",
+ "nsIImportGeneric.idl",
+ "nsIImportMail.idl",
+ "nsIImportMailboxDescriptor.idl",
+ "nsIImportModule.idl",
+ "nsIImportService.idl",
+ "nsIImportSettings.idl",
+]
+
+XPIDL_MODULE = "import"
diff --git a/comm/mailnews/import/public/nsIImportABDescriptor.idl b/comm/mailnews/import/public/nsIImportABDescriptor.idl
new file mode 100644
index 0000000000..fe0ca1b313
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportABDescriptor.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+
+ Interface for importing mail - ui provided by the import module. If
+ you wish to provide your own UI then implement the nsIImportGeneric
+ interface.
+
+ Can I get an attribute set method to take a const value???
+
+ */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+/**
+ * Implementation Note:
+ *
+ * The default implementation can be obtained from
+ * nsIImportService::CreateNewABDescriptor();
+ *
+ * You should only be interested in using this class if you implement
+ * the nsIImportAddressBooks interface in which case, just using the service to
+ * create new ones should work fine for you. If not, implement your
+ * own.
+ */
+[scriptable, uuid(2d8983b2-cea6-4ae2-9145-eb772481fa18)]
+interface nsIImportABDescriptor : nsISupports
+{
+ /**
+ * use the following 2 attributes however you'd like to
+ * refer to a specific address book
+ */
+ attribute unsigned long identifier;
+ attribute unsigned long ref;
+
+ /**
+ * Doesn't have to be accurate, this is merely used to report progress.
+ * If you're importing a file, using file size and reporting progress
+ * as the number of bytes processed so far makes sense. For other formats
+ * returning the number of records may make more sense.
+ */
+ attribute unsigned long size;
+
+ /**
+ * The preferred name for this address book. Depending upon how the
+ * user selected import, the caller of the nsIImportAddressBooks interface
+ * may use this name to create the destination address book or it may
+ * ignore it. However, this must be provided in all cases as it is
+ * also displayed in the UI to the user.
+ */
+ attribute AString preferredName;
+
+ /**
+ * For address books that want a file descriptor to locate the address book.
+ * For formats that do not, use identifier & ref to refer to the address book
+ * OR implement your own nsIImportABDescriptor that contains additional data
+ * necessary to identify specific address books,
+ */
+ attribute nsIFile abFile;
+
+ /**
+ * Set by the UI to indicate whether or not this address book should be imported.
+ */
+ attribute boolean import;
+};
diff --git a/comm/mailnews/import/public/nsIImportABFile.idl b/comm/mailnews/import/public/nsIImportABFile.idl
new file mode 100644
index 0000000000..b6e5334f0a
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportABFile.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/**
+ * General interface for importing from a file to an address book.
+ */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIAbDirectory;
+
+[scriptable, uuid(265209a6-9e1c-4910-97d4-d5ee3f13479b)]
+interface nsIImportABFile : nsISupports {
+ /**
+ * Import entries from a file into an address book directory.
+ * @param sourceFile - The source file.
+ * @param targetDirectory - The target address book directory.
+ */
+ void readFileToDirectory(in nsIFile sourceFile,
+ in nsIAbDirectory targetDirectory);
+};
diff --git a/comm/mailnews/import/public/nsIImportAddressBooks.idl b/comm/mailnews/import/public/nsIImportAddressBooks.idl
new file mode 100644
index 0000000000..1b5e1b8e38
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportAddressBooks.idl
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/**
+ * Interface for importing address books using the standard UI. Address book
+ * import occurs in several forms (yuck!).
+ * The destination can be 1..n new address books corresponding to the source
+ * format. For instance a text file would import into a new address book with
+ * the same name as the text file.
+ * The destination can be 1 pre-defined address book, all entries will be added
+ * to the supplied address book - this allows the address book UI so provide an
+ * import command specific for an individual address book.
+ *
+ * The source can import 1 or multiple address books.
+ * The address books can be auto-discoverable or user specified.
+ * The address books can require field mapping or not.
+ *
+ * All of this is rather complicated but it should work out OK.
+ * 1) The first UI panel will allow selection of the address book and will
+ * indicate to the user if the address book will be imported into an
+ * existing address book or new address books. (This could be 2 separate xul
+ * UI's?).
+ * 2) The second panel will show field mapping if it is required - if it is
+ * required then there will be one panel per address book for formats that
+ * support multiple address books. If it is not required then there will be
+ * no second panel.
+ * 3) Show the progress dialog for the import - this could be per address book
+ * if mapping is required? what to do, what to doooooo.....
+ * 4) All done, maybe a what was done panel??
+ */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIImportABDescriptor;
+interface nsIAbDirectory;
+interface nsIImportFieldMap;
+
+[scriptable, uuid(6bba48be-331c-41e3-bc9f-c2ea3754d977)]
+interface nsIImportAddressBooks : nsISupports {
+ /**
+ * Does this interface supports 1 or 1..n address books. You only get to
+ * choose 1 location so for formats where 1..n address books are imported
+ * from a directory, then return true. For a 1 to 1 relationship between
+ * location and address books return false.
+ */
+ boolean GetSupportsMultiple();
+
+ /**
+ * If the address book is not found via a file location, then return true
+ * along with a description string of how or where the address book is
+ * located. If it is a file location then return false.
+ * If true, return a string like: "Outlook address book".
+ * If false, GetDefaultLocation will be called.
+ */
+ boolean GetAutoFind(out wstring description);
+
+ /**
+ * Returns true if the address book needs the user to specify a field map for
+ * address books imported from this format.
+ */
+ boolean GetNeedsFieldMap(in nsIFile location);
+
+ /**
+ * If found and userVerify BOTH return false, then it is assumed that this
+ * means an error - address book cannot be found on this machine.
+ * If userVerify is true, the user will have an opportunity to specify a
+ * different location to import address book from.
+ */
+ void GetDefaultLocation(out nsIFile location,
+ out boolean found,
+ out boolean userVerify);
+ /**
+ * Returns an array containing an nsIImportABDescriptor for each
+ * address book. The array is not sorted before display to the user.
+ * location is null if GetAutoFind returned true.
+ */
+ Array<nsIImportABDescriptor> findAddressBooks(in nsIFile location);
+
+ /**
+ * Fill in defaults (if any) for a field map for importing address books from
+ * this location.
+ */
+ void InitFieldMap(in nsIImportFieldMap fieldMap);
+
+ /**
+ * Import a specific address book into the destination file supplied.
+ * If an error occurs that is non-fatal, the destination will be deleted and
+ * other address book will be imported. If a fatal error occurs, the
+ * destination will be deleted and the import operation will abort.
+ *
+ * @param aSource The source data for the import.
+ * @param aDestination The proxy database for the destination of the
+ * import.
+ * @param aFieldMap The field map containing the mapping of fields to
+ * be used in cvs and tab type imports.
+ * @param aSupportService An optional proxy support service (nullptr is
+ * acceptable if it is not required), may be required
+ * for certain import types (e.g. nsIAbLDIFService for
+ * LDIF import).
+ * @param aErrorLog The error log from the import.
+ * @param aSuccessLog The success log from the import.
+ * @param aFatalError True if there was a fatal error doing the import.
+ */
+ void ImportAddressBook(in nsIImportABDescriptor aSource,
+ in nsIAbDirectory aDestination,
+ in nsIImportFieldMap aFieldMap,
+ in nsISupports aSupportService,
+ out wstring aErrorLog,
+ out wstring aSuccessLog,
+ out boolean aFatalError);
+
+ /**
+ * Return the amount of the address book that has been imported so far. This
+ * number is used to present progress information and must never be larger
+ * than the size specified in nsIImportABDescriptor.GetSize(); May be called
+ * from a different thread than ImportAddressBook()
+ */
+ unsigned long GetImportProgress();
+
+ /**
+ * Set the location for reading sample data, this should be the same as what
+ * is passed later to FindAddressBooks.
+ */
+ void SetSampleLocation(in nsIFile location);
+
+ /**
+ * Return a string of sample data for a record, each field is separated by a
+ * newline (which means no newlines in the fields!)
+ * This is only supported by address books which use field maps and is used
+ * by the field map UI to allow the user to properly align fields to be
+ * imported.
+ *
+ * @param recordNumber index of the recrds, starting from 0.
+ * @param recordExists true if the record exists.
+ *
+ * @returns a string of sample data for the desired record
+ */
+ wstring GetSampleData(in long recordNumber, out boolean recordExists);
+};
diff --git a/comm/mailnews/import/public/nsIImportFieldMap.idl b/comm/mailnews/import/public/nsIImportFieldMap.idl
new file mode 100644
index 0000000000..83dc0ddfb1
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportFieldMap.idl
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+ Field map interface for importing address books
+
+ A field map is an arbitrary sized list of mozilla address book fields.
+ The field map is used by import to map fields from the import format
+ to mozilla fields.
+ For export, the map contains the ordered list of mozilla fields to
+ export!
+*/
+
+#include "nsISupports.idl"
+
+interface nsIAbCard;
+interface nsIAbDirectory;
+
+[scriptable, uuid(deee9264-1fe3-47b1-b745-47b22de454e2)]
+interface nsIImportFieldMap : nsISupports
+{
+ /*
+ Flag to indicate whether or not to skip the first record,
+ for instance csv files often have field names as the first
+ record
+ */
+ attribute boolean skipFirstRecord;
+
+ readonly attribute long numMozFields;
+ readonly attribute long mapSize;
+
+ wstring GetFieldDescription(in long index);
+
+ /*
+ Set the size of the field map, all unpopulated entries
+ will default to -1
+ */
+ void SetFieldMapSize(in long size);
+
+ /*
+ Initialize the field map to a given size with default values
+ */
+ void DefaultFieldMap(in long size);
+
+ /*
+ Return the field number that this index maps to, -1 for no field
+ */
+ long GetFieldMap(in long index);
+
+ /*
+ Set the field that this index maps to, -1 for no field
+ */
+ void SetFieldMap(in long index, in long fieldNum);
+
+ /*
+ Return if this field is "active" in the map.
+ */
+ boolean GetFieldActive(in long index);
+
+ /*
+ Set the active state of this field
+ */
+ void SetFieldActive(in long index, in boolean active);
+
+ /*
+ Set the value of the given field in the database row
+ */
+ void SetFieldValue(in nsIAbDirectory database, in nsIAbCard row, in long fieldNum, in AString value);
+};
diff --git a/comm/mailnews/import/public/nsIImportFilters.idl b/comm/mailnews/import/public/nsIImportFilters.idl
new file mode 100644
index 0000000000..cac242e827
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportFilters.idl
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ Interface for importing filters.
+*/
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(f2680ccf-d110-4b5b-954d-e072d4a16129)]
+interface nsIImportFilters : nsISupports
+{
+ boolean AutoLocate(out wstring aDescription, out nsIFile aLocation);
+
+ void SetLocation(in nsIFile aLocation);
+
+ /*
+ Import filters and put any problems in the error out parameter.
+ */
+ boolean Import(out wstring aError);
+};
diff --git a/comm/mailnews/import/public/nsIImportGeneric.idl b/comm/mailnews/import/public/nsIImportGeneric.idl
new file mode 100644
index 0000000000..eab152973b
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportGeneric.idl
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+ Interface for importing anything. You are responsible for opening
+ up UI and doing all of the work to make it happen.
+
+*/
+
+#include "nsISupports.idl"
+
+interface nsISupportsString;
+
+[scriptable, uuid(469d7d5f-144c-4f07-9661-e49e40156348)]
+interface nsIImportGeneric : nsISupports
+{
+ /* Use these to prepare for the import */
+ /*
+ "mailInterface" - nsIImportMail interface
+ "mailLocation" - nsIFile, source location for mail
+
+ "addressInterface" - nsIImportAddressBooks interface
+ "addressLocation" - src location of address books (if needed!)
+ "addressDestination" - uri of destination address book or null if
+ new address books will be created.
+ */
+ nsISupports GetData(in string dataId);
+
+ void SetData(in string dataId, in nsISupports pData);
+
+ /*
+ "isInstalled" - if true then mail can be automatically located.
+ "canUserSetLocation" - if true then the user can specify the location
+ to look for mail. If both are false, then there is no way
+ to import mail from this format!
+ TBD: How to specify whether or not a file or a directory
+ should be specified?
+ "autoFind" - for address books, is the address book located without
+ using the file system - i.e. addressLocation is irrelevant.
+ "supportsMultiple" - 1 or 1..n address books are imported by this format?
+
+ */
+ long GetStatus(in string statusKind);
+
+ /*
+ When you are ready to import call this. If it returns TRUE then
+ you must call BeginImport and then repeatedly call GetProgress until
+ it returns 100 % done or until ContinueImport returns FALSE.
+ If this returns FALSE then BeginImport will begin and finish the import
+ before it returns.
+ */
+ boolean WantsProgress();
+
+ /* Use these for the actual import */
+ /* Begin import is expected to start a new thread UNLESS WantsProgress returned
+ FALSE. It is REQUIRED to call WantsProgress before calling BeginImport.
+ If WantsProgress was false then this will return the success or
+ failure of the import. Failure can be reported even if WantsProgress
+ returned TRUE.
+ */
+ boolean BeginImport(in nsISupportsString successLog,
+ in nsISupportsString errorLog);
+ /*
+ If WantsProgress returned TRUE then this will indicate if the import should
+ continue. If this returns FALSE then no other methods should be called
+ and the error log should be shown to the user.
+ */
+ boolean ContinueImport();
+ /*
+ Returns the percentage done. When this returns 100 then the import is done.
+ (only valid if WantsProgress returned true)
+ */
+ long GetProgress();
+ /*
+ Cancel an import in progress. Again, this is only valid if WantsProgress
+ returned true.
+ */
+ void CancelImport();
+};
diff --git a/comm/mailnews/import/public/nsIImportMail.idl b/comm/mailnews/import/public/nsIImportMail.idl
new file mode 100644
index 0000000000..336fede8f4
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportMail.idl
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+
+ Interface for importing mail - ui provided by the import module. If
+ you wish to provide your own UI then implement the nsIImportGeneric
+ interface.
+
+*/
+
+/*
+ If you support this interface then the standard mailbox import UI
+ can be used to drive your import of mailboxes, which means you don't have
+ to worry about anything other than implementing this interface
+ (and nsIImportModule) to import mailboxes.
+*/
+
+/*
+ The general process is:
+ 1) Do you know where the mail is located
+ 2) Do you want the user to "verify" this location and have
+ the option of specifying a different mail directory?
+ 3) Given a directory (either specified in 1 or 2) build a list
+ of all of the mailboxes to be imported.
+ 4) Import each mail box to the destination provided!
+ 5) Update the portion of the mailbox imported so far. This should
+ always be less than the mailbox size until you are done. This
+ is used for progress bar updating and MAY BE CALLED FROM ANOTHER
+ THREAD!
+
+*/
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIImportMailboxDescriptor;
+interface nsIMsgFolder;
+
+[scriptable, uuid(a14a3308-0849-420b-86d3-13a2948b5504)]
+interface nsIImportMail : nsISupports
+{
+ /*
+ If found and userVerify BOTH return false, then it is assumed that this
+ means an error - mail cannot be found on this machine.
+ If userVerify is true, the user will have an opportunity to specify
+ a different location to import mail from.
+ */
+ void GetDefaultLocation(out nsIFile location,
+ out boolean found,
+ out boolean userVerify);
+ /*
+ Returns an array which contains an nsIImportMailboxDescriptor for each
+ mailbox. The array is not sorted before display to the user.
+ */
+ Array<nsIImportMailboxDescriptor> findMailboxes(in nsIFile location);
+
+ /*
+ Import a specific mailbox into the destination folder supplied. If an error
+ occurs that is non-fatal, the destination will be deleted and other mailboxes
+ will be imported. If a fatal error occurs, the destination will be deleted
+ and the import operation will abort.
+ */
+ void ImportMailbox(in nsIImportMailboxDescriptor source,
+ in nsIMsgFolder dstFolder,
+ out wstring errorLog,
+ out wstring successLog,
+ out boolean fatalError);
+
+ /*
+ Return the amount of the mailbox that has been imported so far. This number
+ is used to present progress information and must never be larger than the
+ size specified in nsIImportMailboxID.GetSize(); May be called from
+ a different thread than ImportMailbox()
+ */
+ unsigned long GetImportProgress();
+
+ /*
+ * When migrating the local folders from the import source into mozilla,
+ * we want to translate reserved folder names from the import source to
+ * equivalent values for Mozilla.
+ * Localization Impact is unknown here.
+ */
+ AString translateFolderName(in AString aFolderName);
+};
+
+%{ C++
+#define kDestTrashFolderName "Trash"
+#define kDestUnsentMessagesFolderName "Unsent Messages"
+#define kDestSentFolderName "Sent"
+#define kDestInboxFolderName "Inbox"
+%}
diff --git a/comm/mailnews/import/public/nsIImportMailboxDescriptor.idl b/comm/mailnews/import/public/nsIImportMailboxDescriptor.idl
new file mode 100644
index 0000000000..f0da7f7f79
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportMailboxDescriptor.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+
+ Interface for importing mail - ui provided by the import module. If
+ you wish to provide your own UI then implement the nsIImportGeneric
+ interface.
+
+ */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(69eba744-9c4f-4f79-a964-2134746b3656)]
+interface nsIImportMailboxDescriptor : nsISupports
+{
+ attribute unsigned long identifier;
+ attribute unsigned long depth;
+ attribute unsigned long size;
+
+ wstring GetDisplayName();
+ void SetDisplayName([const] in wstring name);
+
+ attribute boolean import;
+ readonly attribute nsIFile file;
+};
+
+%{ C++
+
+/*
+ The default implementation can be obtained from
+ nsIImportService::CreateNewMailboxDescriptor();
+
+ You should only be interested in using this class if you implement
+ the nsIImportMail interface in which case, just using the service to
+ create new ones should work fine for you. If not, implement your
+ own.
+*/
+
+%}
diff --git a/comm/mailnews/import/public/nsIImportModule.idl b/comm/mailnews/import/public/nsIImportModule.idl
new file mode 100644
index 0000000000..00eb10601c
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportModule.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+
+ An import module.
+
+ */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(624f0280-173f-11d3-a206-00a0cc26da63)]
+interface nsIImportModule : nsISupports
+{
+ readonly attribute wstring name;
+ readonly attribute wstring description;
+ readonly attribute string supports;
+ readonly attribute boolean supportsUpgrade;
+
+ nsISupports GetImportInterface( in string importType);
+};
+
+%{ C++
+#define NS_IMPORT_MAIL_STR "mail"
+#define NS_IMPORT_ADDRESS_STR "addressbook"
+#define NS_IMPORT_SETTINGS_STR "settings"
+#define NS_IMPORT_FILTERS_STR "filters"
+%}
diff --git a/comm/mailnews/import/public/nsIImportService.idl b/comm/mailnews/import/public/nsIImportService.idl
new file mode 100644
index 0000000000..1cfaf7fbaa
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportService.idl
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+
+ The import service.
+
+ */
+
+#include "nsISupports.idl"
+
+interface nsIImportModule;
+interface nsIImportMailboxDescriptor;
+interface nsIImportABDescriptor;
+interface nsIImportGeneric;
+interface nsIImportFieldMap;
+interface nsIMsgSendListener;
+interface nsIMsgIdentity;
+interface nsIMsgCompFields;
+interface nsIMsgAttachedFile;
+interface nsIMsgEmbeddedImageData;
+
+[scriptable, uuid(d0ed4c50-5997-49c9-8a6a-045f0680ed29)]
+interface nsIImportService : nsISupports
+{
+ void DiscoverModules();
+
+ long GetModuleCount(in string filter);
+ void GetModuleInfo(in string filter, in long index, out AString name, out AString description);
+ AString GetModuleName(in string filter, in long index);
+ AString GetModuleDescription(in string filter, in long index);
+ nsIImportModule GetModule(in string filter, in long index);
+
+ nsIImportFieldMap CreateNewFieldMap();
+ nsIImportMailboxDescriptor CreateNewMailboxDescriptor();
+ nsIImportABDescriptor CreateNewABDescriptor();
+ nsIImportGeneric CreateNewGenericMail();
+ nsIImportGeneric CreateNewGenericAddressBooks();
+ void CreateRFC822Message(in nsIMsgIdentity aIdentity,
+ in nsIMsgCompFields aMsgFields,
+ in string aBodytype,
+ in ACString aBody,
+ in boolean aCreateAsDraft,
+ in Array<nsIMsgAttachedFile> aLoadedAttachments,
+ in Array<nsIMsgEmbeddedImageData> aEmbeddedObjects,
+ in nsIMsgSendListener aListener);
+
+};
+
+%{ C++
+#define NS_IMPORTSERVICE_CID \
+{ /* 5df96d60-1726-11d3-a206-00a0cc26da63 */ \
+ 0x5df96d60, 0x1726, 0x11d3, \
+ {0xa2, 0x06, 0x0, 0xa0, 0xcc, 0x26, 0xda, 0x63}}
+
+#define NS_IMPORTSERVICE_CONTRACTID "@mozilla.org/import/import-service;1"
+%}
diff --git a/comm/mailnews/import/public/nsIImportSettings.idl b/comm/mailnews/import/public/nsIImportSettings.idl
new file mode 100644
index 0000000000..b9a123bfc6
--- /dev/null
+++ b/comm/mailnews/import/public/nsIImportSettings.idl
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+ Interface for importing settings. Settings can be auto-located or
+ specified by a specific file. Depends upon the app that the settings
+ are coming from.
+
+*/
+
+#include "nsISupports.idl"
+
+interface nsIMsgAccount;
+interface nsIFile;
+
+[scriptable, uuid(1c0e3012-bc4d-4fb2-be6a-0335c7bab9ac)]
+interface nsIImportSettings : nsISupports
+{
+ boolean AutoLocate(out wstring description, out nsIFile location);
+
+ void SetLocation(in nsIFile location);
+
+ /*
+ Create all of the accounts, identities, and servers. Return an
+ account where any local mail from this app should be imported.
+ The returned account can be null which indicates that no suitable
+ account for local mail was created and a new account specifically for
+ the imported mail should be created.
+ */
+ boolean Import(out nsIMsgAccount localMailAccount);
+};
diff --git a/comm/mailnews/import/src/ImportCharSet.cpp b/comm/mailnews/import/src/ImportCharSet.cpp
new file mode 100644
index 0000000000..ea79221210
--- /dev/null
+++ b/comm/mailnews/import/src/ImportCharSet.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "ImportCharSet.h"
+
+char ImportCharSet::m_upperCaseMap[256];
+char ImportCharSet::m_Ascii[256] = {0}; // the initialiser makes it strong
+
+class UInitMaps {
+ public:
+ UInitMaps();
+};
+
+UInitMaps gInitMaps;
+
+UInitMaps::UInitMaps() {
+ int i;
+
+ for (i = 0; i < 256; i++) ImportCharSet::m_upperCaseMap[i] = i;
+ for (i = 'a'; i <= 'z'; i++) ImportCharSet::m_upperCaseMap[i] = i - 'a' + 'A';
+
+ for (i = 0; i < 256; i++) ImportCharSet::m_Ascii[i] = 0;
+
+ for (i = ImportCharSet::cUpperAChar; i <= ImportCharSet::cUpperZChar; i++)
+ ImportCharSet::m_Ascii[i] |=
+ (ImportCharSet::cAlphaNumChar | ImportCharSet::cAlphaChar);
+ for (i = ImportCharSet::cLowerAChar; i <= ImportCharSet::cLowerZChar; i++)
+ ImportCharSet::m_Ascii[i] |=
+ (ImportCharSet::cAlphaNumChar | ImportCharSet::cAlphaChar);
+ for (i = ImportCharSet::cZeroChar; i <= ImportCharSet::cNineChar; i++)
+ ImportCharSet::m_Ascii[i] |=
+ (ImportCharSet::cAlphaNumChar | ImportCharSet::cDigitChar);
+
+ ImportCharSet::m_Ascii[ImportCharSet::cTabChar] |=
+ ImportCharSet::cWhiteSpaceChar;
+ ImportCharSet::m_Ascii[ImportCharSet::cCRChar] |=
+ ImportCharSet::cWhiteSpaceChar;
+ ImportCharSet::m_Ascii[ImportCharSet::cLinefeedChar] |=
+ ImportCharSet::cWhiteSpaceChar;
+ ImportCharSet::m_Ascii[ImportCharSet::cSpaceChar] |=
+ ImportCharSet::cWhiteSpaceChar;
+
+ ImportCharSet::m_Ascii[uint8_t('(')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t(')')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('<')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('>')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('@')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t(',')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t(';')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t(':')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('\\')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('"')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('.')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t('[')] |= ImportCharSet::c822SpecialChar;
+ ImportCharSet::m_Ascii[uint8_t(']')] |= ImportCharSet::c822SpecialChar;
+}
diff --git a/comm/mailnews/import/src/ImportCharSet.h b/comm/mailnews/import/src/ImportCharSet.h
new file mode 100644
index 0000000000..0feb8d2a98
--- /dev/null
+++ b/comm/mailnews/import/src/ImportCharSet.h
@@ -0,0 +1,201 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef ImportCharSet_h___
+#define ImportCharSet_h___
+
+#include "nscore.h"
+
+// Some useful ASCII values
+// 'A' = 65, 0x41
+// 'Z' = 90, 0x5a
+// '_' = 95, 0x5f
+// 'a' = 97, 0x61
+// 'z' = 122, 0x7a
+// '0' = 48, 0x30
+// '1' = 49, 0x31
+// '9' = 57, 0x39
+// ' ' = 32, 0x20
+// whitespace, 10, 13, 32, 9 (linefeed, cr, space, tab) - 0x0a, 0x0d, 0x20,
+// 0x09
+// ':' = 58, 0x3a
+
+// a typedef enum would be nicer but some compilers still have trouble with
+// treating enum's as plain numbers when needed
+
+class ImportCharSet {
+ public:
+ enum {
+ cTabChar = 9,
+ cLinefeedChar = 10,
+ cCRChar = 13,
+ cSpaceChar = 32,
+ cUpperAChar = 65,
+ cUpperZChar = 90,
+ cUnderscoreChar = 95,
+ cLowerAChar = 97,
+ cLowerZChar = 122,
+ cZeroChar = 48,
+ cNineChar = 57,
+
+ cAlphaNumChar = 1,
+ cAlphaChar = 2,
+ cWhiteSpaceChar = 4,
+ cDigitChar = 8,
+ c822SpecialChar = 16
+ };
+
+ static char m_upperCaseMap[256];
+ static char m_Ascii[256];
+
+ inline static bool IsUSAscii(uint8_t ch) {
+ return (((ch & (uint8_t)0x80) == 0));
+ }
+ inline static bool Is822CtlChar(uint8_t ch) { return (ch < 32); }
+ inline static bool Is822SpecialChar(uint8_t ch) {
+ return ((m_Ascii[ch] & c822SpecialChar) == c822SpecialChar);
+ }
+ inline static bool IsWhiteSpace(uint8_t ch) {
+ return ((m_Ascii[ch] & cWhiteSpaceChar) == cWhiteSpaceChar);
+ }
+ inline static bool IsAlphaNum(uint8_t ch) {
+ return ((m_Ascii[ch] & cAlphaNumChar) == cAlphaNumChar);
+ }
+ inline static bool IsDigit(uint8_t ch) {
+ return ((m_Ascii[ch] & cDigitChar) == cDigitChar);
+ }
+
+ inline static uint8_t ToLower(uint8_t ch) {
+ if ((m_Ascii[ch] & cAlphaChar) == cAlphaChar) {
+ return cLowerAChar + (m_upperCaseMap[ch] - cUpperAChar);
+ } else
+ return ch;
+ }
+
+ inline static long AsciiToLong(const uint8_t* pChar, uint32_t len) {
+ long num = 0;
+ while (len) {
+ if ((m_Ascii[*pChar] & cDigitChar) == 0) return num;
+ num *= 10;
+ num += (*pChar - cZeroChar);
+ len--;
+ pChar++;
+ }
+ return num;
+ }
+
+ inline static void ByteToHex(uint8_t byte, uint8_t* pHex) {
+ uint8_t val = byte;
+ val /= 16;
+ if (val < 10)
+ *pHex = '0' + val;
+ else
+ *pHex = 'A' + (val - 10);
+ pHex++;
+ val = byte;
+ val &= 0x0F;
+ if (val < 10)
+ *pHex = '0' + val;
+ else
+ *pHex = 'A' + (val - 10);
+ }
+
+ inline static void LongToHexBytes(uint32_t type, uint8_t* pStr) {
+ ByteToHex((uint8_t)(type >> 24), pStr);
+ pStr += 2;
+ ByteToHex((uint8_t)((type >> 16) & 0x0FF), pStr);
+ pStr += 2;
+ ByteToHex((uint8_t)((type >> 8) & 0x0FF), pStr);
+ pStr += 2;
+ ByteToHex((uint8_t)(type & 0x0FF), pStr);
+ }
+
+ inline static void SkipWhiteSpace(const uint8_t*& pChar, uint32_t& pos,
+ uint32_t max) {
+ while ((pos < max) && (IsWhiteSpace(*pChar))) {
+ pos++;
+ pChar++;
+ }
+ }
+
+ inline static void SkipSpaceTab(const uint8_t*& pChar, uint32_t& pos,
+ uint32_t max) {
+ while ((pos < max) &&
+ ((*pChar == (uint8_t)cSpaceChar) || (*pChar == (uint8_t)cTabChar))) {
+ pos++;
+ pChar++;
+ }
+ }
+
+ inline static void SkipTilSpaceTab(const uint8_t*& pChar, uint32_t& pos,
+ uint32_t max) {
+ while ((pos < max) && (*pChar != (uint8_t)cSpaceChar) &&
+ (*pChar != (uint8_t)cTabChar)) {
+ pos++;
+ pChar++;
+ }
+ }
+
+ inline static bool StrNICmp(const uint8_t* pChar, const uint8_t* pSrc,
+ uint32_t len) {
+ while (len && (m_upperCaseMap[*pChar] == m_upperCaseMap[*pSrc])) {
+ pChar++;
+ pSrc++;
+ len--;
+ }
+ return len == 0;
+ }
+
+ inline static bool StrNCmp(const uint8_t* pChar, const uint8_t* pSrc,
+ uint32_t len) {
+ while (len && (*pChar == *pSrc)) {
+ pChar++;
+ pSrc++;
+ len--;
+ }
+ return len == 0;
+ }
+
+ inline static int FindChar(const uint8_t* pChar, uint8_t ch, uint32_t max) {
+ uint32_t pos = 0;
+ while ((pos < max) && (*pChar != ch)) {
+ pos++;
+ pChar++;
+ }
+ if (pos < max)
+ return (int)pos;
+ else
+ return -1;
+ }
+
+ inline static bool NextChar(const uint8_t*& pChar, uint8_t ch, uint32_t& pos,
+ uint32_t max) {
+ if ((pos < max) && (*pChar == ch)) {
+ pos++;
+ pChar++;
+ return true;
+ }
+ return false;
+ }
+
+ inline static int32_t strcmp(const char* pS1, const char* pS2) {
+ while (*pS1 && *pS2 && (*pS1 == *pS2)) {
+ pS1++;
+ pS2++;
+ }
+ return *pS1 - *pS2;
+ }
+
+ inline static int32_t stricmp(const char* pS1, const char* pS2) {
+ while (*pS1 && *pS2 &&
+ (m_upperCaseMap[uint8_t(*pS1)] == m_upperCaseMap[uint8_t(*pS2)])) {
+ pS1++;
+ pS2++;
+ }
+ return m_upperCaseMap[uint8_t(*pS1)] - m_upperCaseMap[uint8_t(*pS2)];
+ }
+};
+
+#endif /* ImportCharSet_h__ */
diff --git a/comm/mailnews/import/src/ImportDebug.h b/comm/mailnews/import/src/ImportDebug.h
new file mode 100644
index 0000000000..f1698ed442
--- /dev/null
+++ b/comm/mailnews/import/src/ImportDebug.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef ImportDebug_h___
+#define ImportDebug_h___
+
+#ifdef NS_DEBUG
+# define IMPORT_DEBUG 1
+#endif
+
+#include "mozilla/Logging.h"
+extern mozilla::LazyLogModule
+ IMPORTLOGMODULE; // defined in nsImportService.cpp
+
+#define IMPORT_LOG0(x) MOZ_LOG(IMPORTLOGMODULE, mozilla::LogLevel::Debug, (x))
+#define IMPORT_LOG1(x, y) \
+ MOZ_LOG(IMPORTLOGMODULE, mozilla::LogLevel::Debug, (x, y))
+#define IMPORT_LOG2(x, y, z) \
+ MOZ_LOG(IMPORTLOGMODULE, mozilla::LogLevel::Debug, (x, y, z))
+#define IMPORT_LOG3(a, b, c, d) \
+ MOZ_LOG(IMPORTLOGMODULE, mozilla::LogLevel::Debug, (a, b, c, d))
+
+#endif
diff --git a/comm/mailnews/import/src/ImportOutFile.cpp b/comm/mailnews/import/src/ImportOutFile.cpp
new file mode 100644
index 0000000000..3622b56ad7
--- /dev/null
+++ b/comm/mailnews/import/src/ImportOutFile.cpp
@@ -0,0 +1,257 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsString.h"
+#include "prio.h"
+#include "nsNetUtil.h"
+#include "nsISeekableStream.h"
+#include "nsMsgUtils.h"
+#include "ImportOutFile.h"
+#include "ImportCharSet.h"
+
+#include "ImportDebug.h"
+
+/*
+#ifdef _MAC
+#define kMacNoCreator '????'
+#define kMacTextFile 'TEXT'
+#else
+#define kMacNoCreator 0
+#define kMacTextFile 0
+#endif
+*/
+
+ImportOutFile::ImportOutFile() {
+ m_ownsFileAndBuffer = false;
+ m_pos = 0;
+ m_pBuf = nullptr;
+ m_bufSz = 0;
+ m_pTrans = nullptr;
+ m_pTransOut = nullptr;
+ m_pTransBuf = nullptr;
+}
+
+ImportOutFile::ImportOutFile(nsIFile* pFile, uint8_t* pBuf, uint32_t sz) {
+ m_pTransBuf = nullptr;
+ m_pTransOut = nullptr;
+ m_pTrans = nullptr;
+ m_ownsFileAndBuffer = false;
+ InitOutFile(pFile, pBuf, sz);
+}
+
+ImportOutFile::~ImportOutFile() {
+ if (m_ownsFileAndBuffer) {
+ Flush();
+ delete[] m_pBuf;
+ }
+
+ delete m_pTrans;
+ delete m_pTransOut;
+ delete[] m_pTransBuf;
+}
+
+bool ImportOutFile::Set8bitTranslator(nsImportTranslator* pTrans) {
+ if (!Flush()) return false;
+
+ m_engaged = false;
+ m_pTrans = pTrans;
+ m_supports8to7 = pTrans->Supports8bitEncoding();
+
+ return true;
+}
+
+bool ImportOutFile::End8bitTranslation(bool* pEngaged, nsCString& useCharset,
+ nsCString& encoding) {
+ if (!m_pTrans) return false;
+
+ bool bResult = Flush();
+ if (m_supports8to7 && m_pTransOut) {
+ if (bResult) bResult = m_pTrans->FinishConvertToFile(m_pTransOut);
+ if (bResult) bResult = Flush();
+ }
+
+ if (m_supports8to7) {
+ m_pTrans->GetCharset(useCharset);
+ m_pTrans->GetEncoding(encoding);
+ } else
+ useCharset.Truncate();
+ *pEngaged = m_engaged;
+ delete m_pTrans;
+ m_pTrans = nullptr;
+ delete m_pTransOut;
+ m_pTransOut = nullptr;
+ delete[] m_pTransBuf;
+ m_pTransBuf = nullptr;
+
+ return bResult;
+}
+
+bool ImportOutFile::InitOutFile(nsIFile* pFile, uint32_t bufSz) {
+ if (!bufSz) bufSz = 32 * 1024;
+ if (!m_pBuf) m_pBuf = new uint8_t[bufSz];
+
+ if (!m_outputStream) {
+ nsresult rv;
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(m_outputStream), pFile,
+ PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE, 0644);
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("Couldn't create outfile\n");
+ delete[] m_pBuf;
+ m_pBuf = nullptr;
+ return false;
+ }
+ }
+ m_pFile = pFile;
+ m_ownsFileAndBuffer = true;
+ m_pos = 0;
+ m_bufSz = bufSz;
+ return true;
+}
+
+void ImportOutFile::InitOutFile(nsIFile* pFile, uint8_t* pBuf, uint32_t sz) {
+ m_ownsFileAndBuffer = false;
+ m_pFile = pFile;
+ m_pBuf = pBuf;
+ m_bufSz = sz;
+ m_pos = 0;
+}
+
+bool ImportOutFile::Flush(void) {
+ if (!m_pos) return true;
+
+ uint32_t transLen;
+ bool duddleyDoWrite = false;
+
+ // handle translations if appropriate
+ if (m_pTrans) {
+ if (m_engaged && m_supports8to7) {
+ // Markers can get confused by this crap!!!
+ // TLR: FIXME: Need to update the markers based on
+ // the difference between the translated len and untranslated len
+
+ if (!m_pTrans->ConvertToFile(m_pBuf, m_pos, m_pTransOut, &transLen))
+ return false;
+ if (!m_pTransOut->Flush()) return false;
+ // now update our buffer...
+ if (transLen < m_pos) {
+ memcpy(m_pBuf, m_pBuf + transLen, m_pos - transLen);
+ }
+ m_pos -= transLen;
+ } else if (m_engaged) {
+ // does not actually support translation!
+ duddleyDoWrite = true;
+ } else {
+ // should we engage?
+ uint8_t* pChar = m_pBuf;
+ uint32_t len = m_pos;
+ while (len) {
+ if (!ImportCharSet::IsUSAscii(*pChar)) break;
+ pChar++;
+ len--;
+ }
+ if (len) {
+ m_engaged = true;
+ if (m_supports8to7) {
+ // allocate our translation output buffer and file...
+ m_pTransBuf = new uint8_t[m_bufSz];
+ m_pTransOut = new ImportOutFile(m_pFile, m_pTransBuf, m_bufSz);
+ return Flush();
+ } else
+ duddleyDoWrite = true;
+ } else {
+ duddleyDoWrite = true;
+ }
+ }
+ } else
+ duddleyDoWrite = true;
+
+ if (duddleyDoWrite) {
+ uint32_t written = 0;
+ nsresult rv =
+ m_outputStream->Write((const char*)m_pBuf, (int32_t)m_pos, &written);
+ if (NS_FAILED(rv) || ((uint32_t)written != m_pos)) return false;
+ m_pos = 0;
+ }
+
+ return true;
+}
+
+bool ImportOutFile::WriteU8NullTerm(const uint8_t* pSrc, bool includeNull) {
+ while (*pSrc) {
+ if (m_pos >= m_bufSz) {
+ if (!Flush()) return false;
+ }
+ *(m_pBuf + m_pos) = *pSrc;
+ m_pos++;
+ pSrc++;
+ }
+ if (includeNull) {
+ if (m_pos >= m_bufSz) {
+ if (!Flush()) return false;
+ }
+ *(m_pBuf + m_pos) = 0;
+ m_pos++;
+ }
+
+ return true;
+}
+
+bool ImportOutFile::SetMarker(int markerID) {
+ if (!Flush()) {
+ return false;
+ }
+
+ if (markerID < kMaxMarkers) {
+ int64_t pos = 0;
+ if (m_outputStream) {
+ // do we need to flush for the seek to give us the right pos?
+ m_outputStream->Flush();
+ nsresult rv;
+ nsCOMPtr<nsISeekableStream> seekStream =
+ do_QueryInterface(m_outputStream, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = seekStream->Tell(&pos);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error, Tell failed on output stream\n");
+ return false;
+ }
+ }
+ m_markers[markerID] = (uint32_t)pos + m_pos;
+ }
+
+ return true;
+}
+
+void ImportOutFile::ClearMarker(int markerID) {
+ if (markerID < kMaxMarkers) m_markers[markerID] = 0;
+}
+
+bool ImportOutFile::WriteStrAtMarker(int markerID, const char* pStr) {
+ if (markerID >= kMaxMarkers) return false;
+
+ if (!Flush()) return false;
+ int64_t pos;
+ m_outputStream->Flush();
+ nsresult rv;
+ nsCOMPtr<nsISeekableStream> seekStream =
+ do_QueryInterface(m_outputStream, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = seekStream->Tell(&pos);
+ if (NS_FAILED(rv)) return false;
+ rv = seekStream->Seek(nsISeekableStream::NS_SEEK_SET,
+ (int32_t)m_markers[markerID]);
+ if (NS_FAILED(rv)) return false;
+ uint32_t written;
+ rv = m_outputStream->Write(pStr, strlen(pStr), &written);
+ if (NS_FAILED(rv)) return false;
+
+ rv = seekStream->Seek(nsISeekableStream::NS_SEEK_SET, pos);
+ if (NS_FAILED(rv)) return false;
+
+ return true;
+}
diff --git a/comm/mailnews/import/src/ImportOutFile.h b/comm/mailnews/import/src/ImportOutFile.h
new file mode 100644
index 0000000000..f682c4dc4f
--- /dev/null
+++ b/comm/mailnews/import/src/ImportOutFile.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef ImportOutFile_h___
+#define ImportOutFile_h___
+
+#include "nsImportTranslator.h"
+#include "nsIOutputStream.h"
+#include "nsIFile.h"
+
+#define kMaxMarkers 10
+
+class ImportOutFile;
+
+class ImportOutFile {
+ public:
+ ImportOutFile();
+ ImportOutFile(nsIFile* pFile, uint8_t* pBuf, uint32_t sz);
+ ~ImportOutFile();
+
+ bool InitOutFile(nsIFile* pFile, uint32_t bufSz = 4096);
+ void InitOutFile(nsIFile* pFile, uint8_t* pBuf, uint32_t sz);
+ inline bool WriteData(const uint8_t* pSrc, uint32_t len);
+ inline bool WriteByte(uint8_t byte);
+ bool WriteStr(const char* pStr) {
+ return WriteU8NullTerm((const uint8_t*)pStr, false);
+ }
+ bool WriteU8NullTerm(const uint8_t* pSrc, bool includeNull);
+ bool WriteEol(void) { return WriteStr("\x0D\x0A"); }
+ bool Done(void) { return Flush(); }
+
+ // Marker support
+ bool SetMarker(int markerID);
+ void ClearMarker(int markerID);
+ bool WriteStrAtMarker(int markerID, const char* pStr);
+
+ // 8-bit to 7-bit translation
+ bool Set8bitTranslator(nsImportTranslator* pTrans);
+ bool End8bitTranslation(bool* pEngaged, nsCString& useCharset,
+ nsCString& encoding);
+
+ protected:
+ bool Flush(void);
+
+ protected:
+ nsCOMPtr<nsIFile> m_pFile;
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ uint8_t* m_pBuf;
+ uint32_t m_bufSz;
+ uint32_t m_pos;
+ bool m_ownsFileAndBuffer;
+
+ // markers
+ uint32_t m_markers[kMaxMarkers];
+
+ // 8 bit to 7 bit translations
+ nsImportTranslator* m_pTrans;
+ bool m_engaged;
+ bool m_supports8to7;
+ ImportOutFile* m_pTransOut;
+ uint8_t* m_pTransBuf;
+};
+
+inline bool ImportOutFile::WriteData(const uint8_t* pSrc, uint32_t len) {
+ while ((len + m_pos) > m_bufSz) {
+ if ((m_bufSz - m_pos)) {
+ memcpy(m_pBuf + m_pos, pSrc, m_bufSz - m_pos);
+ len -= (m_bufSz - m_pos);
+ pSrc += (m_bufSz - m_pos);
+ m_pos = m_bufSz;
+ }
+ if (!Flush()) return false;
+ }
+
+ if (len) {
+ memcpy(m_pBuf + m_pos, pSrc, len);
+ m_pos += len;
+ }
+
+ return true;
+}
+
+inline bool ImportOutFile::WriteByte(uint8_t byte) {
+ if (m_pos == m_bufSz) {
+ if (!Flush()) return false;
+ }
+ *(m_pBuf + m_pos) = byte;
+ m_pos++;
+ return true;
+}
+
+#endif /* ImportOutFile_h__ */
diff --git a/comm/mailnews/import/src/ImportTranslate.cpp b/comm/mailnews/import/src/ImportTranslate.cpp
new file mode 100644
index 0000000000..64fc09bce5
--- /dev/null
+++ b/comm/mailnews/import/src/ImportTranslate.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "ImportTranslate.h"
+
+int ImportTranslate::m_useTranslator = -1;
+
+bool ImportTranslate::ConvertString(const nsCString& inStr, nsCString& outStr,
+ bool mimeHeader) {
+ if (inStr.IsEmpty()) {
+ outStr = inStr;
+ return true;
+ }
+
+ nsImportTranslator* pTrans = GetTranslator();
+ // int maxLen = (int) pTrans->GetMaxBufferSize(inStr.Length());
+ // int hLen = 0;
+ nsCString set;
+ nsCString lang;
+
+ if (mimeHeader) {
+ // add the charset and language
+ pTrans->GetCharset(set);
+ pTrans->GetLanguage(lang);
+ }
+
+ // Unfortunately, we didn't implement ConvertBuffer for all translators,
+ // just ConvertToFile. This means that this data will not always
+ // be converted to the charset of pTrans. In that case...
+ // We don't always have the data in the same charset as the current
+ // translator...
+ // It is safer to leave the charset and language field blank
+ set.Truncate();
+ lang.Truncate();
+
+ uint8_t* pBuf;
+ /*
+ pBuf = (P_U8) outStr.GetBuffer(maxLen);
+ if (!pBuf) {
+ delete pTrans;
+ return FALSE;
+ }
+ pTrans->ConvertBuffer((PC_U8)(PC_S8)inStr, inStr.GetLength(), pBuf);
+ outStr.ReleaseBuffer();
+ */
+ outStr = inStr;
+ delete pTrans;
+
+ // Now I need to run the string through the mime-header special char
+ // encoder.
+
+ pTrans = new CMHTranslator;
+ pBuf = new uint8_t[pTrans->GetMaxBufferSize(outStr.Length())];
+ pTrans->ConvertBuffer((const uint8_t*)(outStr.get()), outStr.Length(), pBuf);
+ delete pTrans;
+ outStr.Truncate();
+ if (mimeHeader) {
+ outStr = set;
+ outStr += "'";
+ outStr += lang;
+ outStr += "'";
+ }
+ outStr += (const char*)pBuf;
+ delete[] pBuf;
+
+ return true;
+}
+
+nsImportTranslator* ImportTranslate::GetTranslator(void) {
+ if (m_useTranslator == -1) {
+ // get the translator to use...
+ // CString trans;
+ // trans.LoadString(IDS_LANGUAGE_TRANSLATION);
+ m_useTranslator = 0;
+ // if (!trans.CompareNoCase("iso-2022-jp"))
+ // gWizData.m_useTranslator = 1;
+ }
+
+ switch (m_useTranslator) {
+ case 0:
+ return new nsImportTranslator;
+ // case 1:
+ // return new CSJis2JisTranslator;
+ default:
+ return new nsImportTranslator;
+ }
+}
+
+nsImportTranslator* ImportTranslate::GetMatchingTranslator(
+ const char* pCharSet) {
+ /*
+ CString jp = "iso-2022-jp";
+ if (!jp.CompareNoCase(pCharSet))
+ return new CSJis2JisTranslator;
+ */
+
+ return nullptr;
+}
diff --git a/comm/mailnews/import/src/ImportTranslate.h b/comm/mailnews/import/src/ImportTranslate.h
new file mode 100644
index 0000000000..5e31207395
--- /dev/null
+++ b/comm/mailnews/import/src/ImportTranslate.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef ImportTranslate_h___
+#define ImportTranslate_h___
+
+#include "nsString.h"
+#include "nsImportTranslator.h"
+
+class ImportTranslate {
+ public:
+ static bool ConvertString(const nsCString& inStr, nsCString& outStr,
+ bool mimeHeader);
+ static nsImportTranslator* GetTranslator(void);
+ static nsImportTranslator* GetMatchingTranslator(const char* pCharSet);
+
+ protected:
+ static int m_useTranslator;
+};
+
+#endif /* ImportTranslate_h__ */
diff --git a/comm/mailnews/import/src/MapiApi.cpp b/comm/mailnews/import/src/MapiApi.cpp
new file mode 100644
index 0000000000..5490b6bc11
--- /dev/null
+++ b/comm/mailnews/import/src/MapiApi.cpp
@@ -0,0 +1,1842 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "MapiDbgLog.h"
+#include "MapiApi.h"
+
+#include <sstream>
+#include "rtfMailDecoder.h"
+
+#include "prprf.h"
+#include "nsMemory.h"
+#include "nsMsgUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsNativeCharsetUtils.h"
+
+int CMapiApi::m_clients = 0;
+BOOL CMapiApi::m_initialized = false;
+nsTArray<CMsgStore*>* CMapiApi::m_pStores = NULL;
+LPMAPISESSION CMapiApi::m_lpSession = NULL;
+LPMDB CMapiApi::m_lpMdb = NULL;
+HRESULT CMapiApi::m_lastError;
+/*
+Type: 1, name: Calendar, class: IPF.Appointment
+Type: 1, name: Contacts, class: IPF.Contact
+Type: 1, name: Journal, class: IPF.Journal
+Type: 1, name: Notes, class: IPF.StickyNote
+Type: 1, name: Tasks, class: IPF.Task
+Type: 1, name: Drafts, class: IPF.Note
+*/
+
+HINSTANCE CMapiApi::m_hMapi32 = NULL;
+
+LPMAPIUNINITIALIZE gpMapiUninitialize = NULL;
+LPMAPIINITIALIZE gpMapiInitialize = NULL;
+LPMAPIALLOCATEBUFFER gpMapiAllocateBuffer = NULL;
+LPMAPIFREEBUFFER gpMapiFreeBuffer = NULL;
+LPMAPILOGONEX gpMapiLogonEx = NULL;
+LPOPENSTREAMONFILE gpMapiOpenStreamOnFile = NULL;
+
+typedef HRESULT(STDMETHODCALLTYPE WRAPCOMPRESSEDRTFSTREAM)(
+ LPSTREAM lpCompressedRTFStream, ULONG ulFlags,
+ LPSTREAM FAR* lpUncompressedRTFStream);
+typedef WRAPCOMPRESSEDRTFSTREAM* LPWRAPCOMPRESSEDRTFSTREAM;
+LPWRAPCOMPRESSEDRTFSTREAM gpWrapCompressedRTFStream = NULL;
+
+// WrapCompressedRTFStreamEx related stuff - see
+// http://support.microsoft.com/kb/839560
+typedef struct {
+ ULONG size;
+ ULONG ulFlags;
+ ULONG ulInCodePage;
+ ULONG ulOutCodePage;
+} RTF_WCSINFO;
+typedef struct {
+ ULONG size;
+ ULONG ulStreamFlags;
+} RTF_WCSRETINFO;
+
+typedef HRESULT(STDMETHODCALLTYPE WRAPCOMPRESSEDRTFSTREAMEX)(
+ LPSTREAM lpCompressedRTFStream, CONST RTF_WCSINFO* pWCSInfo,
+ LPSTREAM* lppUncompressedRTFStream, RTF_WCSRETINFO* pRetInfo);
+typedef WRAPCOMPRESSEDRTFSTREAMEX* LPWRAPCOMPRESSEDRTFSTREAMEX;
+LPWRAPCOMPRESSEDRTFSTREAMEX gpWrapCompressedRTFStreamEx = NULL;
+
+BOOL CMapiApi::LoadMapiEntryPoints(void) {
+ if (!(gpMapiUninitialize =
+ (LPMAPIUNINITIALIZE)GetProcAddress(m_hMapi32, "MAPIUninitialize")))
+ return FALSE;
+ if (!(gpMapiInitialize =
+ (LPMAPIINITIALIZE)GetProcAddress(m_hMapi32, "MAPIInitialize")))
+ return FALSE;
+ if (!(gpMapiAllocateBuffer = (LPMAPIALLOCATEBUFFER)GetProcAddress(
+ m_hMapi32, "MAPIAllocateBuffer")))
+ return FALSE;
+ if (!(gpMapiFreeBuffer =
+ (LPMAPIFREEBUFFER)GetProcAddress(m_hMapi32, "MAPIFreeBuffer")))
+ return FALSE;
+ if (!(gpMapiLogonEx =
+ (LPMAPILOGONEX)GetProcAddress(m_hMapi32, "MAPILogonEx")))
+ return FALSE;
+ if (!(gpMapiOpenStreamOnFile =
+ (LPOPENSTREAMONFILE)GetProcAddress(m_hMapi32, "OpenStreamOnFile")))
+ return FALSE;
+
+ // Available from the Outlook 2002 post-SP3 hotfix
+ // (http://support.microsoft.com/kb/883924/) Exported by msmapi32.dll; so it's
+ // unavailable to us using mapi32.dll
+ gpWrapCompressedRTFStreamEx = (LPWRAPCOMPRESSEDRTFSTREAMEX)GetProcAddress(
+ m_hMapi32, "WrapCompressedRTFStreamEx");
+ // Available always
+ gpWrapCompressedRTFStream = (LPWRAPCOMPRESSEDRTFSTREAM)GetProcAddress(
+ m_hMapi32, "WrapCompressedRTFStream");
+
+ return TRUE;
+}
+
+// Gets the PR_RTF_COMPRESSED tag property
+// Codepage is used only if the WrapCompressedRTFStreamEx is available
+BOOL CMapiApi::GetRTFPropertyDecodedAsUTF16(LPMAPIPROP pProp, nsString& val,
+ unsigned long& nativeBodyType,
+ unsigned long codepage) {
+ if (!m_hMapi32 || !(gpWrapCompressedRTFStreamEx || gpWrapCompressedRTFStream))
+ return FALSE; // Fallback to the default processing
+
+ LPSTREAM icstream = 0; // for the compressed stream
+ LPSTREAM iunstream = 0; // for the uncompressed stream
+ HRESULT hr =
+ pProp->OpenProperty(PR_RTF_COMPRESSED, &IID_IStream,
+ STGM_READ | STGM_DIRECT, 0, (LPUNKNOWN*)&icstream);
+ if (HR_FAILED(hr)) return FALSE;
+
+ if (gpWrapCompressedRTFStreamEx) { // Impossible - we use mapi32.dll!
+ RTF_WCSINFO wcsinfo = {0};
+ RTF_WCSRETINFO retinfo = {0};
+
+ retinfo.size = sizeof(RTF_WCSRETINFO);
+
+ wcsinfo.size = sizeof(RTF_WCSINFO);
+ wcsinfo.ulFlags = MAPI_NATIVE_BODY;
+ wcsinfo.ulInCodePage = codepage;
+ wcsinfo.ulOutCodePage = CP_UTF8;
+
+ if (HR_SUCCEEDED(hr = gpWrapCompressedRTFStreamEx(icstream, &wcsinfo,
+ &iunstream, &retinfo)))
+ nativeBodyType = retinfo.ulStreamFlags;
+ } else { // mapi32.dll
+ gpWrapCompressedRTFStream(icstream, 0, &iunstream);
+ }
+ icstream->Release();
+
+ if (iunstream) { // Succeeded
+ std::string streamData;
+ // Stream.Stat doesn't work for this stream!
+ bool done = false;
+ while (!done) {
+ // I think 10K is a good guess to minimize the number of reads while
+ // keeping memory usage low
+ const int bufsize = 10240;
+ char buf[bufsize];
+ ULONG read;
+ hr = iunstream->Read(buf, bufsize, &read);
+ done = (read < bufsize) || (hr != S_OK);
+ if (read) streamData.append(buf, read);
+ }
+ iunstream->Release();
+ // if rtf -> convert to plain text.
+ if (!gpWrapCompressedRTFStreamEx ||
+ (nativeBodyType == MAPI_NATIVE_BODY_TYPE_RTF)) {
+ std::stringstream s(streamData);
+ CRTFMailDecoder decoder;
+ DecodeRTF(s, decoder);
+ if (decoder.mode() == CRTFMailDecoder::mHTML)
+ nativeBodyType = MAPI_NATIVE_BODY_TYPE_HTML;
+ else if (decoder.mode() == CRTFMailDecoder::mText)
+ nativeBodyType = MAPI_NATIVE_BODY_TYPE_PLAINTEXT;
+ else
+ nativeBodyType = MAPI_NATIVE_BODY_TYPE_RTF;
+ val.Assign(decoder.text(), decoder.textSize());
+ } else { // WrapCompressedRTFStreamEx available and original type is not
+ // rtf
+ CopyUTF8toUTF16(nsDependentCString(streamData.c_str()), val);
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void CMapiApi::MAPIUninitialize(void) {
+ if (m_hMapi32 && gpMapiUninitialize) (*gpMapiUninitialize)();
+}
+
+HRESULT CMapiApi::MAPIInitialize(LPVOID lpInit) {
+ return (m_hMapi32 && gpMapiInitialize) ? (*gpMapiInitialize)(lpInit)
+ : MAPI_E_NOT_INITIALIZED;
+}
+
+SCODE CMapiApi::MAPIAllocateBuffer(ULONG cbSize, LPVOID FAR* lppBuffer) {
+ return (m_hMapi32 && gpMapiAllocateBuffer)
+ ? (*gpMapiAllocateBuffer)(cbSize, lppBuffer)
+ : MAPI_E_NOT_INITIALIZED;
+}
+
+ULONG CMapiApi::MAPIFreeBuffer(LPVOID lpBuff) {
+ return (m_hMapi32 && gpMapiFreeBuffer) ? (*gpMapiFreeBuffer)(lpBuff)
+ : MAPI_E_NOT_INITIALIZED;
+}
+
+HRESULT CMapiApi::MAPILogonEx(ULONG ulUIParam, LPTSTR lpszProfileName,
+ LPTSTR lpszPassword, FLAGS flFlags,
+ LPMAPISESSION FAR* lppSession) {
+ return (m_hMapi32 && gpMapiLogonEx)
+ ? (*gpMapiLogonEx)(ulUIParam, lpszProfileName, lpszPassword,
+ flFlags, lppSession)
+ : MAPI_E_NOT_INITIALIZED;
+}
+
+HRESULT CMapiApi::OpenStreamOnFile(LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer, ULONG ulFlags,
+ LPCTSTR lpszFileName, LPTSTR lpszPrefix,
+ LPSTREAM FAR* lppStream) {
+ return (m_hMapi32 && gpMapiOpenStreamOnFile)
+ ? (*gpMapiOpenStreamOnFile)(lpAllocateBuffer, lpFreeBuffer,
+ ulFlags, lpszFileName, lpszPrefix,
+ lppStream)
+ : MAPI_E_NOT_INITIALIZED;
+}
+
+void CMapiApi::FreeProws(LPSRowSet prows) {
+ ULONG irow;
+ if (!prows) return;
+ for (irow = 0; irow < prows->cRows; ++irow)
+ MAPIFreeBuffer(prows->aRow[irow].lpProps);
+ MAPIFreeBuffer(prows);
+}
+
+BOOL CMapiApi::LoadMapi(void) {
+ if (m_hMapi32) return TRUE;
+
+ HINSTANCE hInst = ::LoadLibraryW(L"MAPI32.DLL");
+ if (!hInst) return FALSE;
+ FARPROC pProc = GetProcAddress(hInst, "MAPIGetNetscapeVersion");
+ if (pProc) {
+ ::FreeLibrary(hInst);
+ hInst = ::LoadLibraryW(L"MAPI32BAK.DLL");
+ if (!hInst) return FALSE;
+ }
+
+ m_hMapi32 = hInst;
+ return LoadMapiEntryPoints();
+}
+
+void CMapiApi::UnloadMapi(void) {
+ if (m_hMapi32) ::FreeLibrary(m_hMapi32);
+ m_hMapi32 = NULL;
+}
+
+CMapiApi::CMapiApi() {
+ m_clients++;
+ LoadMapi();
+ if (!m_pStores) m_pStores = new nsTArray<CMsgStore*>();
+}
+
+CMapiApi::~CMapiApi() {
+ m_clients--;
+ if (!m_clients) {
+ HRESULT hr;
+
+ ClearMessageStores();
+ delete m_pStores;
+ m_pStores = NULL;
+
+ m_lpMdb = NULL;
+
+ if (m_lpSession) {
+ hr = m_lpSession->Logoff(NULL, 0, 0);
+ if (FAILED(hr)) {
+ MAPI_TRACE2("Logoff failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ }
+ m_lpSession->Release();
+ m_lpSession = NULL;
+ }
+
+ if (m_initialized) {
+ MAPIUninitialize();
+ m_initialized = FALSE;
+ }
+
+ UnloadMapi();
+ }
+}
+
+void CMapiApi::CStrToUnicode(const char* pStr, nsString& result) {
+ NS_CopyNativeToUnicode(nsDependentCString(pStr), result);
+}
+
+BOOL CMapiApi::Initialize(void) {
+ if (m_initialized) return TRUE;
+
+ HRESULT hr;
+
+ hr = MAPIInitialize(NULL);
+
+ if (FAILED(hr)) {
+ MAPI_TRACE2("MAPI Initialize failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ m_initialized = TRUE;
+ MAPI_TRACE0("MAPI Initialized\n");
+
+ return TRUE;
+}
+
+BOOL CMapiApi::LogOn(void) {
+ if (!m_initialized) {
+ MAPI_TRACE0("Tried to LogOn before initializing MAPI\n");
+ return FALSE;
+ }
+
+ if (m_lpSession) return TRUE;
+
+ HRESULT hr;
+
+ hr = MAPILogonEx(
+ 0, // might need to be passed in HWND
+ NULL, // profile name, 64 char max (LPTSTR)
+ NULL, // profile password, 64 char max (LPTSTR)
+ // MAPI_NEW_SESSION | MAPI_NO_MAIL | MAPI_LOGON_UI |
+ // MAPI_EXPLICIT_PROFILE, MAPI_NEW_SESSION | MAPI_NO_MAIL | MAPI_LOGON_UI,
+ // MAPI_NO_MAIL | MAPI_LOGON_UI,
+ MAPI_NO_MAIL | MAPI_USE_DEFAULT | MAPI_EXTENDED | MAPI_NEW_SESSION,
+ &m_lpSession);
+
+ if (FAILED(hr)) {
+ m_lpSession = NULL;
+ MAPI_TRACE2("LogOn failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ MAPI_TRACE0("MAPI Logged on\n");
+ return TRUE;
+}
+
+class CGetStoreFoldersIter : public CMapiHierarchyIter {
+ public:
+ CGetStoreFoldersIter(CMapiApi* pApi, CMapiFolderList& folders, int depth,
+ BOOL isMail = TRUE);
+
+ virtual BOOL HandleHierarchyItem(ULONG oType, ULONG cb, LPENTRYID pEntry);
+
+ protected:
+ BOOL ExcludeFolderClass(const char16_t* pName);
+
+ BOOL m_isMail;
+ CMapiApi* m_pApi;
+ CMapiFolderList* m_pList;
+ int m_depth;
+};
+
+CGetStoreFoldersIter::CGetStoreFoldersIter(CMapiApi* pApi,
+ CMapiFolderList& folders, int depth,
+ BOOL isMail) {
+ m_pApi = pApi;
+ m_pList = &folders;
+ m_depth = depth;
+ m_isMail = isMail;
+}
+
+BOOL CGetStoreFoldersIter::ExcludeFolderClass(const char16_t* pName) {
+ BOOL bResult;
+ nsDependentString pNameStr(pName);
+ if (m_isMail) {
+ bResult = FALSE;
+ if (pNameStr.EqualsLiteral("IPF.Appointment"))
+ bResult = TRUE;
+ else if (pNameStr.EqualsLiteral("IPF.Contact"))
+ bResult = TRUE;
+ else if (pNameStr.EqualsLiteral("IPF.Journal"))
+ bResult = TRUE;
+ else if (pNameStr.EqualsLiteral("IPF.StickyNote"))
+ bResult = TRUE;
+ else if (pNameStr.EqualsLiteral("IPF.Task"))
+ bResult = TRUE;
+ // Skip IMAP folders
+ else if (pNameStr.EqualsLiteral("IPF.Imap"))
+ bResult = TRUE;
+ // else if (!stricmp(pName, "IPF.Note"))
+ // bResult = TRUE;
+ } else {
+ bResult = TRUE;
+ if (pNameStr.EqualsLiteral("IPF.Contact")) bResult = FALSE;
+ }
+
+ return bResult;
+}
+
+BOOL CGetStoreFoldersIter::HandleHierarchyItem(ULONG oType, ULONG cb,
+ LPENTRYID pEntry) {
+ if (oType == MAPI_FOLDER) {
+ LPMAPIFOLDER pFolder;
+ if (m_pApi->OpenEntry(cb, pEntry, (LPUNKNOWN*)&pFolder)) {
+ LPSPropValue pVal;
+ nsString name;
+
+ pVal = m_pApi->GetMapiProperty(pFolder, PR_CONTAINER_CLASS);
+ if (pVal)
+ m_pApi->GetStringFromProp(pVal, name);
+ else
+ name.Truncate();
+
+ if ((name.IsEmpty() && m_isMail) || (!ExcludeFolderClass(name.get()))) {
+ pVal = m_pApi->GetMapiProperty(pFolder, PR_DISPLAY_NAME);
+ m_pApi->GetStringFromProp(pVal, name);
+ CMapiFolder* pNewFolder =
+ new CMapiFolder(name.get(), cb, pEntry, m_depth);
+ m_pList->AddItem(pNewFolder);
+
+ pVal = m_pApi->GetMapiProperty(pFolder, PR_FOLDER_TYPE);
+ MAPI_TRACE2("Type: %d, name: %s\n", m_pApi->GetLongFromProp(pVal),
+ name.get());
+ // m_pApi->ListProperties(pFolder);
+
+ CGetStoreFoldersIter nextIter(m_pApi, *m_pList, m_depth + 1, m_isMail);
+ m_pApi->IterateHierarchy(&nextIter, pFolder);
+ }
+ pFolder->Release();
+ } else {
+ MAPI_TRACE0(
+ "GetStoreFolders - HandleHierarchyItem: Error opening folder "
+ "entry.\n");
+ return FALSE;
+ }
+ } else
+ MAPI_TRACE1(
+ "GetStoreFolders - HandleHierarchyItem: Unhandled ObjectType: %ld\n",
+ oType);
+ return TRUE;
+}
+
+BOOL CMapiApi::GetStoreFolders(ULONG cbEid, LPENTRYID lpEid,
+ CMapiFolderList& folders, int startDepth) {
+ // Fill in the array with the folders in the given store
+ if (!m_initialized || !m_lpSession) {
+ MAPI_TRACE0("MAPI not initialized for GetStoreFolders\n");
+ return FALSE;
+ }
+
+ m_lpMdb = NULL;
+
+ CMsgStore* pStore = FindMessageStore(cbEid, lpEid);
+ BOOL bResult = FALSE;
+ LPSPropValue pVal;
+
+ if (pStore && pStore->Open(m_lpSession, &m_lpMdb)) {
+ // Successful open, do the iteration of the store
+ pVal = GetMapiProperty(m_lpMdb, PR_IPM_SUBTREE_ENTRYID);
+ if (pVal) {
+ ULONG cbEntry;
+ LPENTRYID pEntry;
+ LPMAPIFOLDER lpSubTree = NULL;
+
+ if (GetEntryIdFromProp(pVal, cbEntry, pEntry)) {
+ // Open up the folder!
+ bResult = OpenEntry(cbEntry, pEntry, (LPUNKNOWN*)&lpSubTree);
+ MAPIFreeBuffer(pEntry);
+ if (bResult && lpSubTree) {
+ // Iterate the subtree with the results going into the folder list
+ CGetStoreFoldersIter iterHandler(this, folders, startDepth);
+ bResult = IterateHierarchy(&iterHandler, lpSubTree);
+ lpSubTree->Release();
+ } else {
+ MAPI_TRACE0("GetStoreFolders: Error opening sub tree.\n");
+ }
+ } else {
+ MAPI_TRACE0(
+ "GetStoreFolders: Error getting entryID from sub tree property "
+ "val.\n");
+ }
+ } else {
+ MAPI_TRACE0("GetStoreFolders: Error getting sub tree property.\n");
+ }
+ } else {
+ MAPI_TRACE0("GetStoreFolders: Error opening message store.\n");
+ }
+
+ return bResult;
+}
+
+BOOL CMapiApi::GetStoreAddressFolders(ULONG cbEid, LPENTRYID lpEid,
+ CMapiFolderList& folders) {
+ // Fill in the array with the folders in the given store
+ if (!m_initialized || !m_lpSession) {
+ MAPI_TRACE0("MAPI not initialized for GetStoreAddressFolders\n");
+ return FALSE;
+ }
+
+ m_lpMdb = NULL;
+
+ CMsgStore* pStore = FindMessageStore(cbEid, lpEid);
+ BOOL bResult = FALSE;
+ LPSPropValue pVal;
+
+ if (pStore && pStore->Open(m_lpSession, &m_lpMdb)) {
+ // Successful open, do the iteration of the store
+ pVal = GetMapiProperty(m_lpMdb, PR_IPM_SUBTREE_ENTRYID);
+ if (pVal) {
+ ULONG cbEntry;
+ LPENTRYID pEntry;
+ LPMAPIFOLDER lpSubTree = NULL;
+
+ if (GetEntryIdFromProp(pVal, cbEntry, pEntry)) {
+ // Open up the folder!
+ bResult = OpenEntry(cbEntry, pEntry, (LPUNKNOWN*)&lpSubTree);
+ MAPIFreeBuffer(pEntry);
+ if (bResult && lpSubTree) {
+ // Iterate the subtree with the results going into the folder list
+ CGetStoreFoldersIter iterHandler(this, folders, 1, FALSE);
+ bResult = IterateHierarchy(&iterHandler, lpSubTree);
+ lpSubTree->Release();
+ } else {
+ MAPI_TRACE0("GetStoreAddressFolders: Error opening sub tree.\n");
+ }
+ } else {
+ MAPI_TRACE0(
+ "GetStoreAddressFolders: Error getting entryID from sub tree "
+ "property val.\n");
+ }
+ } else {
+ MAPI_TRACE0("GetStoreAddressFolders: Error getting sub tree property.\n");
+ }
+ } else
+ MAPI_TRACE0("GetStoreAddressFolders: Error opening message store.\n");
+
+ return bResult;
+}
+
+BOOL CMapiApi::OpenStore(ULONG cbEid, LPENTRYID lpEid, LPMDB* ppMdb) {
+ if (!m_lpSession) {
+ MAPI_TRACE0("OpenStore called before a session was opened\n");
+ return FALSE;
+ }
+
+ CMsgStore* pStore = FindMessageStore(cbEid, lpEid);
+ if (pStore && pStore->Open(m_lpSession, ppMdb)) return TRUE;
+ return FALSE;
+}
+
+BOOL CMapiApi::OpenEntry(ULONG cbEntry, LPENTRYID pEntryId, LPUNKNOWN* ppOpen) {
+ if (!m_lpMdb) {
+ MAPI_TRACE0("OpenEntry called before the message store is open\n");
+ return FALSE;
+ }
+
+ return OpenMdbEntry(m_lpMdb, cbEntry, pEntryId, ppOpen);
+}
+
+BOOL CMapiApi::OpenMdbEntry(LPMDB lpMdb, ULONG cbEntry, LPENTRYID pEntryId,
+ LPUNKNOWN* ppOpen) {
+ ULONG ulObjType;
+ HRESULT hr;
+ hr = m_lpSession->OpenEntry(cbEntry, pEntryId, NULL, 0, &ulObjType,
+ (LPUNKNOWN*)ppOpen);
+ if (FAILED(hr)) {
+ MAPI_TRACE2("OpenMdbEntry failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+enum { ieidPR_ENTRYID = 0, ieidPR_OBJECT_TYPE, ieidMax };
+
+static const SizedSPropTagArray(ieidMax, ptaEid) = {ieidMax,
+ {
+ PR_ENTRYID,
+ PR_OBJECT_TYPE,
+ }};
+
+BOOL CMapiApi::IterateContents(CMapiContentIter* pIter, LPMAPIFOLDER pFolder,
+ ULONG flags) {
+ // flags can be 0 or MAPI_ASSOCIATED
+ // MAPI_ASSOCIATED is usually used for forms and views
+
+ HRESULT hr;
+ LPMAPITABLE lpTable;
+ hr = pFolder->GetContentsTable(flags, &lpTable);
+ if (FAILED(hr)) {
+ MAPI_TRACE2("GetContentsTable failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ ULONG rowCount;
+ hr = lpTable->GetRowCount(0, &rowCount);
+ if (!rowCount) {
+ MAPI_TRACE0(" Empty Table\n");
+ }
+
+ hr = lpTable->SetColumns((LPSPropTagArray)&ptaEid, 0);
+ if (FAILED(hr)) {
+ lpTable->Release();
+ MAPI_TRACE2("SetColumns failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ hr = lpTable->SeekRow(BOOKMARK_BEGINNING, 0, NULL);
+ if (FAILED(hr)) {
+ lpTable->Release();
+ MAPI_TRACE2("SeekRow failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ int cNumRows = 0;
+ LPSRowSet lpRow;
+ BOOL keepGoing = TRUE;
+ BOOL bResult = TRUE;
+ do {
+ lpRow = NULL;
+ hr = lpTable->QueryRows(1, 0, &lpRow);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("QueryRows failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ bResult = FALSE;
+ break;
+ }
+
+ if (lpRow) {
+ cNumRows = lpRow->cRows;
+ if (cNumRows) {
+ LPENTRYID lpEID =
+ (LPENTRYID)lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.lpb;
+ ULONG cbEID = lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.cb;
+ ULONG oType = lpRow->aRow[0].lpProps[ieidPR_OBJECT_TYPE].Value.ul;
+ keepGoing = HandleContentsItem(oType, cbEID, lpEID);
+ MAPI_TRACE1(" ObjectType: %ld\n", oType);
+ }
+ FreeProws(lpRow);
+ }
+
+ } while (SUCCEEDED(hr) && cNumRows && lpRow && keepGoing);
+
+ lpTable->Release();
+ return bResult;
+}
+
+BOOL CMapiApi::HandleContentsItem(ULONG oType, ULONG cb, LPENTRYID pEntry) {
+ if (oType == MAPI_MESSAGE) {
+ LPMESSAGE pMsg;
+ if (OpenEntry(cb, pEntry, (LPUNKNOWN*)&pMsg)) {
+ LPSPropValue pVal;
+ pVal = GetMapiProperty(pMsg, PR_SUBJECT);
+ ReportStringProp("PR_SUBJECT:", pVal);
+ pVal = GetMapiProperty(pMsg, PR_DISPLAY_BCC);
+ ReportStringProp("PR_DISPLAY_BCC:", pVal);
+ pVal = GetMapiProperty(pMsg, PR_DISPLAY_CC);
+ ReportStringProp("PR_DISPLAY_CC:", pVal);
+ pVal = GetMapiProperty(pMsg, PR_DISPLAY_TO);
+ ReportStringProp("PR_DISPLAY_TO:", pVal);
+ pVal = GetMapiProperty(pMsg, PR_MESSAGE_CLASS);
+ ReportStringProp("PR_MESSAGE_CLASS:", pVal);
+ ListProperties(pMsg);
+ pMsg->Release();
+ } else {
+ MAPI_TRACE0(" Folder type - error opening\n");
+ }
+ } else
+ MAPI_TRACE1(" ObjectType: %ld\n", oType);
+
+ return TRUE;
+}
+
+void CMapiApi::ListProperties(LPMAPIPROP lpProp, BOOL getValues) {
+ LPSPropTagArray pArray;
+ HRESULT hr = lpProp->GetPropList(0, &pArray);
+ if (FAILED(hr)) {
+ MAPI_TRACE0(" Unable to retrieve property list\n");
+ return;
+ }
+ ULONG count = 0;
+ LPMAPINAMEID FAR* lppPropNames;
+ SPropTagArray tagArray;
+ LPSPropTagArray lpTagArray = &tagArray;
+ tagArray.cValues = (ULONG)1;
+ nsCString desc;
+ for (ULONG i = 0; i < pArray->cValues; i++) {
+ GetPropTagName(pArray->aulPropTag[i], desc);
+ if (getValues) {
+ tagArray.aulPropTag[0] = pArray->aulPropTag[i];
+ hr = lpProp->GetNamesFromIDs(&lpTagArray, nullptr, 0, &count,
+ &lppPropNames);
+ if (hr == S_OK) MAPIFreeBuffer(lppPropNames);
+
+ LPSPropValue pVal = GetMapiProperty(lpProp, pArray->aulPropTag[i]);
+ if (pVal) {
+ desc += ", ";
+ ListPropertyValue(pVal, desc);
+ MAPIFreeBuffer(pVal);
+ }
+ }
+ MAPI_TRACE2(" Tag #%d: %s\n", (int)i, desc.get());
+ }
+
+ MAPIFreeBuffer(pArray);
+}
+
+ULONG CMapiApi::GetEmailPropertyTag(LPMAPIPROP lpProp, LONG nameID) {
+ static GUID emailGUID = {0x00062004,
+ 0x0000,
+ 0x0000,
+ {0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};
+
+ MAPINAMEID mapiNameID;
+ mapiNameID.lpguid = &emailGUID;
+ mapiNameID.ulKind = MNID_ID;
+ mapiNameID.Kind.lID = nameID;
+
+ LPMAPINAMEID lpMapiNames = &mapiNameID;
+ LPSPropTagArray lpMailTagArray = nullptr;
+
+ HRESULT result =
+ lpProp->GetIDsFromNames(1L, &lpMapiNames, 0, &lpMailTagArray);
+ if (result == S_OK) {
+ ULONG lTag = lpMailTagArray->aulPropTag[0];
+ MAPIFreeBuffer(lpMailTagArray);
+ return lTag;
+ } else
+ return 0L;
+}
+
+BOOL CMapiApi::HandleHierarchyItem(ULONG oType, ULONG cb, LPENTRYID pEntry) {
+ if (oType == MAPI_FOLDER) {
+ LPMAPIFOLDER pFolder;
+ if (OpenEntry(cb, pEntry, (LPUNKNOWN*)&pFolder)) {
+ LPSPropValue pVal;
+ pVal = GetMapiProperty(pFolder, PR_DISPLAY_NAME);
+ ReportStringProp("Folder name:", pVal);
+ IterateContents(NULL, pFolder);
+ IterateHierarchy(NULL, pFolder);
+ pFolder->Release();
+ } else {
+ MAPI_TRACE0(" Folder type - error opening\n");
+ }
+ } else
+ MAPI_TRACE1(" ObjectType: %ld\n", oType);
+
+ return TRUE;
+}
+
+BOOL CMapiApi::IterateHierarchy(CMapiHierarchyIter* pIter, LPMAPIFOLDER pFolder,
+ ULONG flags) {
+ // flags can be CONVENIENT_DEPTH or 0
+ // CONVENIENT_DEPTH will return all depths I believe instead
+ // of just children
+ HRESULT hr;
+ LPMAPITABLE lpTable;
+ hr = pFolder->GetHierarchyTable(flags, &lpTable);
+ if (HR_FAILED(hr)) {
+ m_lastError = hr;
+ MAPI_TRACE2("IterateHierarchy: GetContentsTable failed: 0x%lx, %d\n",
+ (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ ULONG rowCount;
+ hr = lpTable->GetRowCount(0, &rowCount);
+ if (!rowCount) {
+ lpTable->Release();
+ return TRUE;
+ }
+
+ hr = lpTable->SetColumns((LPSPropTagArray)&ptaEid, 0);
+ if (HR_FAILED(hr)) {
+ m_lastError = hr;
+ lpTable->Release();
+ MAPI_TRACE2("IterateHierarchy: SetColumns failed: 0x%lx, %d\n", (long)hr,
+ (int)hr);
+ return FALSE;
+ }
+
+ hr = lpTable->SeekRow(BOOKMARK_BEGINNING, 0, NULL);
+ if (HR_FAILED(hr)) {
+ m_lastError = hr;
+ lpTable->Release();
+ MAPI_TRACE2("IterateHierarchy: SeekRow failed: 0x%lx, %d\n", (long)hr,
+ (int)hr);
+ return FALSE;
+ }
+
+ int cNumRows = 0;
+ LPSRowSet lpRow;
+ BOOL keepGoing = TRUE;
+ BOOL bResult = TRUE;
+ do {
+ lpRow = NULL;
+ hr = lpTable->QueryRows(1, 0, &lpRow);
+
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("QueryRows failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ m_lastError = hr;
+ bResult = FALSE;
+ break;
+ }
+
+ if (lpRow) {
+ cNumRows = lpRow->cRows;
+
+ if (cNumRows) {
+ LPENTRYID lpEntry =
+ (LPENTRYID)lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.lpb;
+ ULONG cb = lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.cb;
+ ULONG oType = lpRow->aRow[0].lpProps[ieidPR_OBJECT_TYPE].Value.ul;
+
+ if (pIter)
+ keepGoing = pIter->HandleHierarchyItem(oType, cb, lpEntry);
+ else
+ keepGoing = HandleHierarchyItem(oType, cb, lpEntry);
+ }
+ FreeProws(lpRow);
+ }
+ } while (SUCCEEDED(hr) && cNumRows && lpRow && keepGoing);
+
+ lpTable->Release();
+
+ if (bResult && !keepGoing) bResult = FALSE;
+
+ return bResult;
+}
+
+enum { itblPR_DISPLAY_NAME, itblPR_ENTRYID, itblMax };
+
+static const SizedSPropTagArray(itblMax, ptaTbl) = {itblMax,
+ {
+ PR_DISPLAY_NAME,
+ PR_ENTRYID,
+ }};
+
+BOOL CMapiApi::IterateStores(CMapiFolderList& stores) {
+ stores.ClearAll();
+
+ if (!m_lpSession) {
+ MAPI_TRACE0("IterateStores called before session is open\n");
+ m_lastError = E_UNEXPECTED;
+ return FALSE;
+ }
+
+ HRESULT hr;
+
+ /* -- Some Microsoft sample code just to see if things are working --- */ /*
+
+ ULONG cbEIDStore;
+ LPENTRYID lpEIDStore;
+
+ hr = HrMAPIFindDefaultMsgStore(m_lpSession, &cbEIDStore, &lpEIDStore);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE0("Default message store not found\n");
+ // MessageBoxW(NULL, L"Message Store Not Found", NULL, MB_OK);
+ }
+ else {
+ LPMDB lpStore;
+ MAPI_TRACE0("Default Message store FOUND\n");
+ hr = m_lpSession->OpenMsgStore(NULL, cbEIDStore,
+ lpEIDStore, NULL,
+ MDB_NO_MAIL | MDB_NO_DIALOG, &lpStore);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE1("Unable to open default message store: 0x%lx\n", hr);
+ }
+ else {
+ MAPI_TRACE0("Default message store OPENED\n");
+ lpStore->Release();
+ }
+ }
+ */
+
+ LPMAPITABLE lpTable;
+
+ hr = m_lpSession->GetMsgStoresTable(0, &lpTable);
+ if (FAILED(hr)) {
+ MAPI_TRACE0("GetMsgStoresTable failed\n");
+ m_lastError = hr;
+ return FALSE;
+ }
+
+ ULONG rowCount;
+ hr = lpTable->GetRowCount(0, &rowCount);
+ MAPI_TRACE1("MsgStores Table rowCount: %ld\n", rowCount);
+
+ hr = lpTable->SetColumns((LPSPropTagArray)&ptaTbl, 0);
+ if (FAILED(hr)) {
+ lpTable->Release();
+ MAPI_TRACE2("SetColumns failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ m_lastError = hr;
+ return FALSE;
+ }
+
+ hr = lpTable->SeekRow(BOOKMARK_BEGINNING, 0, NULL);
+ if (FAILED(hr)) {
+ lpTable->Release();
+ MAPI_TRACE2("SeekRow failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ m_lastError = hr;
+ return FALSE;
+ }
+
+ int cNumRows = 0;
+ LPSRowSet lpRow;
+ BOOL keepGoing = TRUE;
+ BOOL bResult = TRUE;
+ do {
+ lpRow = NULL;
+ hr = lpTable->QueryRows(1, 0, &lpRow);
+
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("QueryRows failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ bResult = FALSE;
+ m_lastError = hr;
+ break;
+ }
+
+ if (lpRow) {
+ cNumRows = lpRow->cRows;
+
+ if (cNumRows) {
+ LPCTSTR lpStr =
+ (LPCTSTR)lpRow->aRow[0].lpProps[itblPR_DISPLAY_NAME].Value.LPSZ;
+ LPENTRYID lpEID =
+ (LPENTRYID)lpRow->aRow[0].lpProps[itblPR_ENTRYID].Value.bin.lpb;
+ ULONG cbEID = lpRow->aRow[0].lpProps[itblPR_ENTRYID].Value.bin.cb;
+
+ // In the future, GetStoreInfo needs to somehow return
+ // whether or not the store is from an IMAP server.
+ // Currently, GetStoreInfo opens the store and attempts
+ // to get the hierarchy tree. If the tree is empty or
+ // does not exist, then szContents will be zero. We'll
+ // assume that any store that doesn't have anything in
+ // it's hierarchy tree is not a store we want to import -
+ // there would be nothing to import from anyway!
+ // Currently, this does exclude IMAP server accounts
+ // which is the desired behaviour.
+
+ int strLen = strlen(lpStr);
+ char16_t* pwszStr =
+ (char16_t*)moz_xmalloc((strLen + 1) * sizeof(WCHAR));
+ if (!pwszStr) {
+ // out of memory
+ FreeProws(lpRow);
+ lpTable->Release();
+ return FALSE;
+ }
+ ::MultiByteToWideChar(CP_ACP, 0, lpStr, strlen(lpStr) + 1,
+ reinterpret_cast<wchar_t*>(pwszStr),
+ (strLen + 1) * sizeof(WCHAR));
+ CMapiFolder* pFolder =
+ new CMapiFolder(pwszStr, cbEID, lpEID, 0, MAPI_STORE);
+ free(pwszStr);
+
+ long szContents = 1;
+ GetStoreInfo(pFolder, &szContents);
+
+ MAPI_TRACE1(" DisplayName: %s\n", lpStr);
+ if (szContents)
+ stores.AddItem(pFolder);
+ else {
+ delete pFolder;
+ MAPI_TRACE0(" ^^^^^ Not added to store list\n");
+ }
+
+ keepGoing = TRUE;
+ }
+ FreeProws(lpRow);
+ }
+ } while (SUCCEEDED(hr) && cNumRows && lpRow && keepGoing);
+
+ lpTable->Release();
+
+ return bResult;
+}
+
+void CMapiApi::GetStoreInfo(CMapiFolder* pFolder, long* pSzContents) {
+ HRESULT hr;
+ LPMDB lpMdb;
+
+ if (pSzContents) *pSzContents = 0;
+
+ if (!OpenStore(pFolder->GetCBEntryID(), pFolder->GetEntryID(), &lpMdb))
+ return;
+
+ LPSPropValue pVal;
+ /*
+ pVal = GetMapiProperty(lpMdb, PR_DISPLAY_NAME);
+ ReportStringProp(" Message store name:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_MDB_PROVIDER);
+ ReportUIDProp(" Message store provider:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_COMMENT);
+ ReportStringProp(" Message comment:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_ACCESS_LEVEL);
+ ReportLongProp(" Message store Access Level:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_STORE_SUPPORT_MASK);
+ ReportLongProp(" Message store support mask:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_STORE_STATE);
+ ReportLongProp(" Message store state:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_OBJECT_TYPE);
+ ReportLongProp(" Message store object type:", pVal);
+ pVal = GetMapiProperty(lpMdb, PR_VALID_FOLDER_MASK);
+ ReportLongProp(" Message store valid folder mask:", pVal);
+
+ pVal = GetMapiProperty(lpMdb, 0x8001001e);
+ ReportStringProp(" Message prop 0x8001001e:", pVal);
+
+ // This key appears to be the OMI Account Manager account that corresponds
+ // to this message store. This is important for IMAP accounts
+ // since we may not want to import messages from an IMAP store!
+ // Seems silly if you ask me!
+ // In order to test this, we'll need the registry key to look under to
+ determine
+ // if it contains the "IMAP Server" value, if it does then we are an
+ // IMAP store, if not, then we are a non-IMAP store - which may always mean
+ // a regular store that should be imported.
+
+ pVal = GetMapiProperty(lpMdb, 0x80000003);
+ ReportLongProp(" Message prop 0x80000003:", pVal);
+
+ // ListProperties(lpMdb);
+ */
+
+ pVal = GetMapiProperty(lpMdb, PR_IPM_SUBTREE_ENTRYID);
+ if (pVal) {
+ ULONG cbEntry;
+ LPENTRYID pEntry;
+ LPMAPIFOLDER lpSubTree = NULL;
+
+ if (GetEntryIdFromProp(pVal, cbEntry, pEntry)) {
+ // Open up the folder!
+ ULONG ulObjType;
+ hr = lpMdb->OpenEntry(cbEntry, pEntry, NULL, 0, &ulObjType,
+ (LPUNKNOWN*)&lpSubTree);
+ MAPIFreeBuffer(pEntry);
+ if (SUCCEEDED(hr) && lpSubTree) {
+ // Find out if there are any contents in the
+ // tree.
+ LPMAPITABLE lpTable;
+ hr = lpSubTree->GetHierarchyTable(0, &lpTable);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("GetStoreInfo: GetHierarchyTable failed: 0x%lx, %d\n",
+ (long)hr, (int)hr);
+ } else {
+ ULONG rowCount;
+ hr = lpTable->GetRowCount(0, &rowCount);
+ lpTable->Release();
+ if (SUCCEEDED(hr) && pSzContents) *pSzContents = (long)rowCount;
+ }
+
+ lpSubTree->Release();
+ }
+ }
+ }
+}
+
+void CMapiApi::ClearMessageStores(void) {
+ if (m_pStores) {
+ CMsgStore* pStore;
+ for (size_t i = 0; i < m_pStores->Length(); i++) {
+ pStore = m_pStores->ElementAt(i);
+ delete pStore;
+ }
+ m_pStores->Clear();
+ }
+}
+
+void CMapiApi::AddMessageStore(CMsgStore* pStore) {
+ if (m_pStores) m_pStores->AppendElement(pStore);
+}
+
+CMsgStore* CMapiApi::FindMessageStore(ULONG cbEid, LPENTRYID lpEid) {
+ if (!m_lpSession) {
+ MAPI_TRACE0("FindMessageStore called before session is open\n");
+ m_lastError = E_UNEXPECTED;
+ return NULL;
+ }
+
+ ULONG result;
+ HRESULT hr;
+ CMsgStore* pStore;
+ for (size_t i = 0; i < m_pStores->Length(); i++) {
+ pStore = m_pStores->ElementAt(i);
+ hr = m_lpSession->CompareEntryIDs(cbEid, lpEid, pStore->GetCBEntryID(),
+ pStore->GetLPEntryID(), 0, &result);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("CompareEntryIDs failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ m_lastError = hr;
+ return NULL;
+ }
+ if (result) {
+ return pStore;
+ }
+ }
+
+ pStore = new CMsgStore(cbEid, lpEid);
+ AddMessageStore(pStore);
+ return pStore;
+}
+
+// --------------------------------------------------------------------
+// Utility stuff
+// --------------------------------------------------------------------
+
+LPSPropValue CMapiApi::GetMapiProperty(LPMAPIPROP pProp, ULONG tag) {
+ if (!pProp) return NULL;
+
+ int sz = CbNewSPropTagArray(1);
+ SPropTagArray* pTag = (SPropTagArray*)new char[sz];
+ pTag->cValues = 1;
+ pTag->aulPropTag[0] = tag;
+ LPSPropValue lpProp = NULL;
+ ULONG cValues = 0;
+ HRESULT hr = pProp->GetProps(pTag, 0, &cValues, &lpProp);
+ delete[] pTag;
+ if (HR_FAILED(hr) || (cValues != 1)) {
+ if (lpProp) MAPIFreeBuffer(lpProp);
+ return NULL;
+ } else {
+ if (PROP_TYPE(lpProp->ulPropTag) == PT_ERROR) {
+ if (lpProp->Value.l == MAPI_E_NOT_FOUND) {
+ MAPIFreeBuffer(lpProp);
+ lpProp = NULL;
+ }
+ }
+ }
+
+ return lpProp;
+}
+
+BOOL CMapiApi::IsLargeProperty(LPSPropValue pVal) {
+ return ((PROP_TYPE(pVal->ulPropTag) == PT_ERROR) &&
+ (pVal->Value.l == E_OUTOFMEMORY));
+}
+
+// The output buffer (result) must be freed with operator delete[]
+BOOL CMapiApi::GetLargeProperty(LPMAPIPROP pProp, ULONG tag, void** result) {
+ LPSTREAM lpStream;
+ HRESULT hr =
+ pProp->OpenProperty(tag, &IID_IStream, 0, 0, (LPUNKNOWN*)&lpStream);
+ if (HR_FAILED(hr)) return FALSE;
+ STATSTG st;
+ BOOL bResult = TRUE;
+ hr = lpStream->Stat(&st, STATFLAG_NONAME);
+ if (HR_FAILED(hr))
+ bResult = FALSE;
+ else {
+ if (!st.cbSize.QuadPart) st.cbSize.QuadPart = 1;
+ char* pVal = new char[(int)st.cbSize.QuadPart + 2];
+ if (pVal) {
+ ULONG sz;
+ hr = lpStream->Read(pVal, (ULONG)st.cbSize.QuadPart, &sz);
+ if (HR_FAILED(hr)) {
+ bResult = FALSE;
+ delete[] pVal;
+ } else {
+ // Just in case it's a UTF16 string
+ pVal[(int)st.cbSize.QuadPart] = pVal[(int)st.cbSize.QuadPart + 1] = 0;
+ *result = pVal;
+ }
+ } else
+ bResult = FALSE;
+ }
+
+ lpStream->Release();
+
+ return bResult;
+}
+
+BOOL CMapiApi::GetLargeStringProperty(LPMAPIPROP pProp, ULONG tag,
+ nsCString& val) {
+ void* result;
+ if (!GetLargeProperty(pProp, tag, &result)) return FALSE;
+ if (PROP_TYPE(tag) == PT_UNICODE) // unicode string
+ LossyCopyUTF16toASCII(nsDependentString(static_cast<wchar_t*>(result)),
+ val);
+ else // either PT_STRING8 or some other binary - use as is
+ val.Assign(static_cast<char*>(result));
+ // Despite being used as wchar_t*, result it allocated as "new char[]" in
+ // GetLargeProperty().
+ delete[] static_cast<char*>(result);
+ return TRUE;
+}
+
+BOOL CMapiApi::GetLargeStringProperty(LPMAPIPROP pProp, ULONG tag,
+ nsString& val) {
+ void* result;
+ if (!GetLargeProperty(pProp, tag, &result)) return FALSE;
+ if (PROP_TYPE(tag) == PT_UNICODE) // We already get the unicode string
+ val.Assign(static_cast<wchar_t*>(result));
+ else // either PT_STRING8 or some other binary
+ CStrToUnicode(static_cast<char*>(result), val);
+ // Despite being used as wchar_t*, result it allocated as "new char[]" in
+ // GetLargeProperty().
+ delete[] static_cast<char*>(result);
+ return TRUE;
+}
+// If the value is a string, get it...
+BOOL CMapiApi::GetEntryIdFromProp(LPSPropValue pVal, ULONG& cbEntryId,
+ LPENTRYID& lpEntryId, BOOL delVal) {
+ if (!pVal) return FALSE;
+
+ BOOL bResult = TRUE;
+ switch (PROP_TYPE(pVal->ulPropTag)) {
+ case PT_BINARY:
+ cbEntryId = pVal->Value.bin.cb;
+ MAPIAllocateBuffer(cbEntryId, (LPVOID*)&lpEntryId);
+ memcpy(lpEntryId, pVal->Value.bin.lpb, cbEntryId);
+ break;
+
+ default:
+ MAPI_TRACE0("EntryId not in BINARY prop value\n");
+ bResult = FALSE;
+ break;
+ }
+
+ if (pVal && delVal) MAPIFreeBuffer(pVal);
+
+ return bResult;
+}
+
+BOOL CMapiApi::GetStringFromProp(LPSPropValue pVal, nsCString& val,
+ BOOL delVal) {
+ BOOL bResult = TRUE;
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_STRING8))
+ val = pVal->Value.lpszA;
+ else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_UNICODE))
+ LossyCopyUTF16toASCII(nsDependentString(pVal->Value.lpszW), val);
+ else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL))
+ val.Truncate();
+ else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ val.Truncate();
+ bResult = FALSE;
+ } else {
+ if (pVal) {
+ MAPI_TRACE1("GetStringFromProp: invalid value, expecting string - %d\n",
+ (int)PROP_TYPE(pVal->ulPropTag));
+ } else {
+ MAPI_TRACE0(
+ "GetStringFromProp: invalid value, expecting string, got null "
+ "pointer\n");
+ }
+ val.Truncate();
+ bResult = FALSE;
+ }
+ if (pVal && delVal) MAPIFreeBuffer(pVal);
+
+ return bResult;
+}
+
+BOOL CMapiApi::GetStringFromProp(LPSPropValue pVal, nsString& val,
+ BOOL delVal) {
+ BOOL bResult = TRUE;
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_STRING8)) {
+ CStrToUnicode((const char*)pVal->Value.lpszA, val);
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_UNICODE)) {
+ val = (char16_t*)pVal->Value.lpszW;
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) {
+ val.Truncate();
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ val.Truncate();
+ bResult = FALSE;
+ } else {
+ if (pVal) {
+ MAPI_TRACE1("GetStringFromProp: invalid value, expecting string - %d\n",
+ (int)PROP_TYPE(pVal->ulPropTag));
+ } else {
+ MAPI_TRACE0(
+ "GetStringFromProp: invalid value, expecting string, got null "
+ "pointer\n");
+ }
+ val.Truncate();
+ bResult = FALSE;
+ }
+ if (pVal && delVal) MAPIFreeBuffer(pVal);
+
+ return bResult;
+}
+
+LONG CMapiApi::GetLongFromProp(LPSPropValue pVal, BOOL delVal) {
+ LONG val = 0;
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_LONG)) {
+ val = pVal->Value.l;
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) {
+ val = 0;
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ val = 0;
+ MAPI_TRACE0("GetLongFromProp: Error retrieving property\n");
+ } else {
+ MAPI_TRACE0("GetLongFromProp: invalid value, expecting long\n");
+ }
+ if (pVal && delVal) MAPIFreeBuffer(pVal);
+
+ return val;
+}
+
+void CMapiApi::ReportUIDProp(const char* pTag, LPSPropValue pVal) {
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_BINARY)) {
+ if (pVal->Value.bin.cb != 16) {
+ MAPI_TRACE1("%s - INVALID, expecting 16 bytes of binary data for UID\n",
+ pTag);
+ } else {
+ nsIID uid;
+ memcpy(&uid, pVal->Value.bin.lpb, 16);
+ const char* pStr = uid.ToString().get();
+ if (pStr) {
+ MAPI_TRACE2("%s %s\n", pTag, pStr);
+ }
+ }
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) {
+ MAPI_TRACE1("%s {NULL}\n", pTag);
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ MAPI_TRACE1("%s {Error retrieving property}\n", pTag);
+ } else {
+ MAPI_TRACE1("%s invalid value, expecting binary\n", pTag);
+ }
+ if (pVal) MAPIFreeBuffer(pVal);
+}
+
+void CMapiApi::ReportLongProp(const char* pTag, LPSPropValue pVal) {
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_LONG)) {
+ nsCString num;
+ nsCString num2;
+
+ num.AppendInt((int32_t)pVal->Value.l);
+ num2.AppendInt((int32_t)pVal->Value.l, 16);
+ MAPI_TRACE3("%s %s, 0x%s\n", pTag, num, num2);
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) {
+ MAPI_TRACE1("%s {NULL}\n", pTag);
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ MAPI_TRACE1("%s {Error retrieving property}\n", pTag);
+ } else {
+ MAPI_TRACE1("%s invalid value, expecting long\n", pTag);
+ }
+ if (pVal) MAPIFreeBuffer(pVal);
+}
+
+void CMapiApi::ReportStringProp(const char* pTag, LPSPropValue pVal) {
+ if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_TSTRING)) {
+ nsCString val((LPCTSTR)(pVal->Value.LPSZ));
+ MAPI_TRACE2("%s %s\n", pTag, val.get());
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_NULL)) {
+ MAPI_TRACE1("%s {NULL}\n", pTag);
+ } else if (pVal && (PROP_TYPE(pVal->ulPropTag) == PT_ERROR)) {
+ MAPI_TRACE1("%s {Error retrieving property}\n", pTag);
+ } else {
+ MAPI_TRACE1("%s invalid value, expecting string\n", pTag);
+ }
+ if (pVal) MAPIFreeBuffer(pVal);
+}
+
+void CMapiApi::GetPropTagName(ULONG tag, nsCString& s) {
+ char numStr[256];
+ PR_snprintf(numStr, 256, "0x%lx, %ld", tag, tag);
+ s = numStr;
+ switch (tag) {
+#include "MapiTagStrs.cpp"
+ }
+ s += ", data: ";
+ switch (PROP_TYPE(tag)) {
+ case PT_UNSPECIFIED:
+ s += "PT_UNSPECIFIED";
+ break;
+ case PT_NULL:
+ s += "PT_NULL";
+ break;
+ case PT_I2:
+ s += "PT_I2";
+ break;
+ case PT_LONG:
+ s += "PT_LONG";
+ break;
+ case PT_R4:
+ s += "PT_R4";
+ break;
+ case PT_DOUBLE:
+ s += "PT_DOUBLE";
+ break;
+ case PT_CURRENCY:
+ s += "PT_CURRENCY";
+ break;
+ case PT_APPTIME:
+ s += "PT_APPTIME";
+ break;
+ case PT_ERROR:
+ s += "PT_ERROR";
+ break;
+ case PT_BOOLEAN:
+ s += "PT_BOOLEAN";
+ break;
+ case PT_OBJECT:
+ s += "PT_OBJECT";
+ break;
+ case PT_I8:
+ s += "PT_I8";
+ break;
+ case PT_STRING8:
+ s += "PT_STRING8";
+ break;
+ case PT_UNICODE:
+ s += "PT_UNICODE";
+ break;
+ case PT_SYSTIME:
+ s += "PT_SYSTIME";
+ break;
+ case PT_CLSID:
+ s += "PT_CLSID";
+ break;
+ case PT_BINARY:
+ s += "PT_BINARY";
+ break;
+ case PT_MV_I2:
+ s += "PT_MV_I2";
+ break;
+ case PT_MV_LONG:
+ s += "PT_MV_LONG";
+ break;
+ case PT_MV_R4:
+ s += "PT_MV_R4";
+ break;
+ case PT_MV_DOUBLE:
+ s += "PT_MV_DOUBLE";
+ break;
+ case PT_MV_CURRENCY:
+ s += "PT_MV_CURRENCY";
+ break;
+ case PT_MV_APPTIME:
+ s += "PT_MV_APPTIME";
+ break;
+ case PT_MV_SYSTIME:
+ s += "PT_MV_SYSTIME";
+ break;
+ case PT_MV_STRING8:
+ s += "PT_MV_STRING8";
+ break;
+ case PT_MV_BINARY:
+ s += "PT_MV_BINARY";
+ break;
+ case PT_MV_UNICODE:
+ s += "PT_MV_UNICODE";
+ break;
+ case PT_MV_CLSID:
+ s += "PT_MV_CLSID";
+ break;
+ case PT_MV_I8:
+ s += "PT_MV_I8";
+ break;
+ default:
+ s += "Unknown";
+ }
+}
+
+void CMapiApi::ListPropertyValue(LPSPropValue pVal, nsCString& s) {
+ nsCString strVal;
+ char nBuff[64];
+
+ s += "value: ";
+ switch (PROP_TYPE(pVal->ulPropTag)) {
+ case PT_STRING8:
+ GetStringFromProp(pVal, strVal, FALSE);
+ if (strVal.Length() > 60) {
+ strVal.SetLength(60);
+ strVal += "...";
+ }
+ strVal.ReplaceSubstring("\r", "\\r");
+ strVal.ReplaceSubstring("\n", "\\n");
+ s += strVal;
+ break;
+ case PT_LONG:
+ s.AppendInt((int32_t)pVal->Value.l);
+ s += ", 0x";
+ s.AppendInt((int32_t)pVal->Value.l, 16);
+ s += nBuff;
+ break;
+ case PT_BOOLEAN:
+ if (pVal->Value.b)
+ s += "True";
+ else
+ s += "False";
+ break;
+ case PT_NULL:
+ s += "--NULL--";
+ break;
+ case PT_SYSTIME: {
+ /*
+ COleDateTime tm(pVal->Value.ft);
+ s += tm.Format();
+ */
+ s += "-- Figure out how to format time in mozilla, PT_SYSTIME --";
+ } break;
+ default:
+ s += "?";
+ }
+}
+
+// -------------------------------------------------------------------
+// Folder list stuff
+// -------------------------------------------------------------------
+CMapiFolderList::CMapiFolderList() {}
+
+CMapiFolderList::~CMapiFolderList() { ClearAll(); }
+
+void CMapiFolderList::AddItem(CMapiFolder* pFolder) {
+ EnsureUniqueName(pFolder);
+ GenerateFilePath(pFolder);
+ m_array.AppendElement(pFolder);
+}
+
+void CMapiFolderList::ChangeName(nsString& name) {
+ if (name.IsEmpty()) {
+ name.Assign('1');
+ return;
+ }
+ char16_t lastC = name.Last();
+ if ((lastC >= '0') && (lastC <= '9')) {
+ lastC++;
+ if (lastC > '9') {
+ lastC = '1';
+ name.SetCharAt(lastC, name.Length() - 1);
+ name.Append('0');
+ } else {
+ name.SetCharAt(lastC, name.Length() - 1);
+ }
+ } else {
+ name.AppendLiteral(" 2");
+ }
+}
+
+void CMapiFolderList::EnsureUniqueName(CMapiFolder* pFolder) {
+ // For everybody in the array before me with the SAME
+ // depth, my name must be unique
+ CMapiFolder* pCurrent;
+ int i;
+ BOOL done;
+ nsString name;
+ nsString cName;
+
+ pFolder->GetDisplayName(name);
+ do {
+ done = TRUE;
+ i = m_array.Length() - 1;
+ while (i >= 0) {
+ pCurrent = GetAt(i);
+ if (pCurrent->GetDepth() == pFolder->GetDepth()) {
+ pCurrent->GetDisplayName(cName);
+ if (cName.Equals(name, nsCaseInsensitiveStringComparator)) {
+ ChangeName(name);
+ pFolder->SetDisplayName(name.get());
+ done = FALSE;
+ break;
+ }
+ } else if (pCurrent->GetDepth() < pFolder->GetDepth())
+ break;
+ i--;
+ }
+ } while (!done);
+}
+
+void CMapiFolderList::GenerateFilePath(CMapiFolder* pFolder) {
+ // A file path, includes all of my parent's path, plus mine
+ nsString name;
+ nsString path;
+ if (!pFolder->GetDepth()) {
+ pFolder->GetDisplayName(name);
+ pFolder->SetFilePath(name.get());
+ return;
+ }
+
+ CMapiFolder* pCurrent;
+ int i = m_array.Length() - 1;
+ while (i >= 0) {
+ pCurrent = GetAt(i);
+ if (pCurrent->GetDepth() == (pFolder->GetDepth() - 1)) {
+ pCurrent->GetFilePath(path);
+ path.AppendLiteral(".sbd\\");
+ pFolder->GetDisplayName(name);
+ path += name;
+ pFolder->SetFilePath(path.get());
+ return;
+ }
+ i--;
+ }
+ pFolder->GetDisplayName(name);
+ pFolder->SetFilePath(name.get());
+}
+
+void CMapiFolderList::ClearAll(void) {
+ CMapiFolder* pFolder;
+ for (size_t i = 0; i < m_array.Length(); i++) {
+ pFolder = GetAt(i);
+ delete pFolder;
+ }
+ m_array.Clear();
+}
+
+void CMapiFolderList::DumpList(void) {
+ CMapiFolder* pFolder;
+ nsString str;
+ int depth;
+ char prefix[256];
+
+ MAPI_TRACE0("Folder List ---------------------------------\n");
+ for (size_t i = 0; i < m_array.Length(); i++) {
+ pFolder = GetAt(i);
+ depth = pFolder->GetDepth();
+ pFolder->GetDisplayName(str);
+ depth *= 2;
+ if (depth > 255) depth = 255;
+ memset(prefix, ' ', depth);
+ prefix[depth] = 0;
+#ifdef MAPI_DEBUG
+ char* ansiStr = ToNewCString(str);
+ MAPI_TRACE2("%s%s: ", prefix, ansiStr);
+ free(ansiStr);
+#endif
+ pFolder->GetFilePath(str);
+#ifdef MAPI_DEBUG
+ ansiStr = ToNewCString(str);
+ MAPI_TRACE2("depth=%d, filePath=%s\n", pFolder->GetDepth(), ansiStr);
+ free(ansiStr);
+#endif
+ }
+ MAPI_TRACE0("---------------------------------------------\n");
+}
+
+CMapiFolder::CMapiFolder() {
+ m_objectType = MAPI_FOLDER;
+ m_cbEid = 0;
+ m_lpEid = NULL;
+ m_depth = 0;
+ m_doImport = TRUE;
+}
+
+CMapiFolder::CMapiFolder(const char16_t* pDisplayName, ULONG cbEid,
+ LPENTRYID lpEid, int depth, LONG oType) {
+ m_cbEid = 0;
+ m_lpEid = NULL;
+ SetDisplayName(pDisplayName);
+ SetEntryID(cbEid, lpEid);
+ SetDepth(depth);
+ SetObjectType(oType);
+ SetDoImport(TRUE);
+}
+
+CMapiFolder::CMapiFolder(const CMapiFolder* pCopyFrom) {
+ m_lpEid = NULL;
+ m_cbEid = 0;
+ SetDoImport(pCopyFrom->GetDoImport());
+ SetDisplayName(pCopyFrom->m_displayName.get());
+ SetObjectType(pCopyFrom->GetObjectType());
+ SetEntryID(pCopyFrom->GetCBEntryID(), pCopyFrom->GetEntryID());
+ SetDepth(pCopyFrom->GetDepth());
+ SetFilePath(pCopyFrom->m_mailFilePath.get());
+}
+
+CMapiFolder::~CMapiFolder() {
+ if (m_lpEid) delete m_lpEid;
+}
+
+void CMapiFolder::SetEntryID(ULONG cbEid, LPENTRYID lpEid) {
+ if (m_lpEid) delete m_lpEid;
+ m_lpEid = NULL;
+ m_cbEid = cbEid;
+ if (cbEid) {
+ m_lpEid = new BYTE[cbEid];
+ memcpy(m_lpEid, lpEid, cbEid);
+ }
+}
+
+// ---------------------------------------------------------------------
+// Message store stuff
+// ---------------------------------------------------------------------
+
+CMsgStore::CMsgStore(ULONG cbEid, LPENTRYID lpEid) {
+ m_lpEid = NULL;
+ m_lpMdb = NULL;
+ SetEntryID(cbEid, lpEid);
+}
+
+CMsgStore::~CMsgStore() {
+ if (m_lpEid) delete m_lpEid;
+
+ if (m_lpMdb) {
+ ULONG flags = LOGOFF_NO_WAIT;
+ m_lpMdb->StoreLogoff(&flags);
+ m_lpMdb->Release();
+ m_lpMdb = NULL;
+ }
+}
+
+void CMsgStore::SetEntryID(ULONG cbEid, LPENTRYID lpEid) {
+ if (m_lpEid) delete m_lpEid;
+
+ m_lpEid = NULL;
+ if (cbEid) {
+ m_lpEid = new BYTE[cbEid];
+ memcpy(m_lpEid, lpEid, cbEid);
+ }
+ m_cbEid = cbEid;
+
+ if (m_lpMdb) {
+ ULONG flags = LOGOFF_NO_WAIT;
+ m_lpMdb->StoreLogoff(&flags);
+ m_lpMdb->Release();
+ m_lpMdb = NULL;
+ }
+}
+
+BOOL CMsgStore::Open(LPMAPISESSION pSession, LPMDB* ppMdb) {
+ if (m_lpMdb) {
+ if (ppMdb) *ppMdb = m_lpMdb;
+ return TRUE;
+ }
+
+ BOOL bResult = TRUE;
+ HRESULT hr = pSession->OpenMsgStore(NULL, m_cbEid, (LPENTRYID)m_lpEid, NULL,
+ MDB_NO_MAIL, &m_lpMdb); // MDB pointer
+ if (HR_FAILED(hr)) {
+ m_lpMdb = NULL;
+ MAPI_TRACE2("OpenMsgStore failed: 0x%lx, %d\n", (long)hr, (int)hr);
+ bResult = FALSE;
+ }
+
+ if (ppMdb) *ppMdb = m_lpMdb;
+ return bResult;
+}
+
+// ------------------------------------------------------------
+// Contents Iterator
+// -----------------------------------------------------------
+
+CMapiFolderContents::CMapiFolderContents(LPMDB lpMdb, ULONG cbEid,
+ LPENTRYID lpEid) {
+ m_lpMdb = lpMdb;
+ m_fCbEid = cbEid;
+ m_fLpEid = new BYTE[cbEid];
+ memcpy(m_fLpEid, lpEid, cbEid);
+ m_count = 0;
+ m_iterCount = 0;
+ m_failure = FALSE;
+ m_lastError = 0;
+ m_lpFolder = NULL;
+ m_lpTable = NULL;
+ m_lastLpEid = NULL;
+ m_lastCbEid = 0;
+}
+
+CMapiFolderContents::~CMapiFolderContents() {
+ if (m_lastLpEid) delete m_lastLpEid;
+ delete m_fLpEid;
+ if (m_lpTable) m_lpTable->Release();
+ if (m_lpFolder) m_lpFolder->Release();
+}
+
+BOOL CMapiFolderContents::SetUpIter(void) {
+ // First, open up the MAPIFOLDER object
+ ULONG ulObjType;
+ HRESULT hr;
+ hr = m_lpMdb->OpenEntry(m_fCbEid, (LPENTRYID)m_fLpEid, NULL, 0, &ulObjType,
+ (LPUNKNOWN*)&m_lpFolder);
+
+ if (FAILED(hr) || !m_lpFolder) {
+ m_lpFolder = NULL;
+ m_lastError = hr;
+ MAPI_TRACE2("CMapiFolderContents OpenEntry failed: 0x%lx, %d\n", (long)hr,
+ (int)hr);
+ return FALSE;
+ }
+
+ if (ulObjType != MAPI_FOLDER) {
+ m_lastError = E_UNEXPECTED;
+ MAPI_TRACE0("CMapiFolderContents - bad object type, not a folder.\n");
+ return FALSE;
+ }
+
+ hr = m_lpFolder->GetContentsTable(0, &m_lpTable);
+ if (FAILED(hr) || !m_lpTable) {
+ m_lastError = hr;
+ m_lpTable = NULL;
+ MAPI_TRACE2("CMapiFolderContents - GetContentsTable failed: 0x%lx, %d\n",
+ (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ hr = m_lpTable->GetRowCount(0, &m_count);
+ if (FAILED(hr)) {
+ m_lastError = hr;
+ MAPI_TRACE0("CMapiFolderContents - GetRowCount failed\n");
+ return FALSE;
+ }
+
+ hr = m_lpTable->SetColumns((LPSPropTagArray)&ptaEid, 0);
+ if (FAILED(hr)) {
+ m_lastError = hr;
+ MAPI_TRACE2("CMapiFolderContents - SetColumns failed: 0x%lx, %d\n",
+ (long)hr, (int)hr);
+ return FALSE;
+ }
+
+ hr = m_lpTable->SeekRow(BOOKMARK_BEGINNING, 0, NULL);
+ if (FAILED(hr)) {
+ m_lastError = hr;
+ MAPI_TRACE2("CMapiFolderContents - SeekRow failed: 0x%lx, %d\n", (long)hr,
+ (int)hr);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+BOOL CMapiFolderContents::GetNext(ULONG* pcbEid, LPENTRYID* ppEid,
+ ULONG* poType, BOOL* pDone) {
+ *pDone = FALSE;
+ if (m_failure) return FALSE;
+ if (!m_lpFolder) {
+ if (!SetUpIter()) {
+ m_failure = TRUE;
+ return FALSE;
+ }
+ if (!m_count) {
+ *pDone = TRUE;
+ return TRUE;
+ }
+ }
+
+ int cNumRows = 0;
+ LPSRowSet lpRow = NULL;
+ HRESULT hr = m_lpTable->QueryRows(1, 0, &lpRow);
+
+ if (HR_FAILED(hr)) {
+ m_lastError = hr;
+ m_failure = TRUE;
+ MAPI_TRACE2("CMapiFolderContents - QueryRows failed: 0x%lx, %d\n", (long)hr,
+ (int)hr);
+ return FALSE;
+ }
+
+ if (lpRow) {
+ cNumRows = lpRow->cRows;
+ if (cNumRows) {
+ LPENTRYID lpEID =
+ (LPENTRYID)lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.lpb;
+ ULONG cbEID = lpRow->aRow[0].lpProps[ieidPR_ENTRYID].Value.bin.cb;
+ ULONG oType = lpRow->aRow[0].lpProps[ieidPR_OBJECT_TYPE].Value.ul;
+
+ if (m_lastCbEid != cbEID) {
+ if (m_lastLpEid) delete m_lastLpEid;
+ m_lastLpEid = new BYTE[cbEID];
+ m_lastCbEid = cbEID;
+ }
+ memcpy(m_lastLpEid, lpEID, cbEID);
+
+ *ppEid = (LPENTRYID)m_lastLpEid;
+ *pcbEid = cbEID;
+ *poType = oType;
+ } else
+ *pDone = TRUE;
+ CMapiApi::FreeProws(lpRow);
+ } else
+ *pDone = TRUE;
+
+ return TRUE;
+}
diff --git a/comm/mailnews/import/src/MapiApi.h b/comm/mailnews/import/src/MapiApi.h
new file mode 100644
index 0000000000..4d9dc7be2a
--- /dev/null
+++ b/comm/mailnews/import/src/MapiApi.h
@@ -0,0 +1,284 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef MapiApi_h___
+#define MapiApi_h___
+
+#include "nscore.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+#include <stdio.h>
+
+#include <windows.h>
+#include <mapi.h>
+#include <mapix.h>
+#include <mapidefs.h>
+#include <mapicode.h>
+#include <mapitags.h>
+#include <mapiutil.h>
+// wabutil.h expects mapiutil to define _MAPIUTIL_H but it actually
+// defines _MAPIUTIL_H_
+#define _MAPIUTIL_H
+
+#ifndef PR_INTERNET_CPID
+# define PR_INTERNET_CPID (PROP_TAG(PT_LONG, 0x3FDE))
+#endif
+#ifndef MAPI_NATIVE_BODY
+# define MAPI_NATIVE_BODY (0x00010000)
+#endif
+#ifndef MAPI_NATIVE_BODY_TYPE_RTF
+# define MAPI_NATIVE_BODY_TYPE_RTF (0x00000001)
+#endif
+#ifndef MAPI_NATIVE_BODY_TYPE_HTML
+# define MAPI_NATIVE_BODY_TYPE_HTML (0x00000002)
+#endif
+#ifndef MAPI_NATIVE_BODY_TYPE_PLAINTEXT
+# define MAPI_NATIVE_BODY_TYPE_PLAINTEXT (0x00000004)
+#endif
+#ifndef PR_BODY_HTML_A
+# define PR_BODY_HTML_A (PROP_TAG(PT_STRING8, 0x1013))
+#endif
+#ifndef PR_BODY_HTML_W
+# define PR_BODY_HTML_W (PROP_TAG(PT_UNICODE, 0x1013))
+#endif
+#ifndef PR_BODY_HTML
+# define PR_BODY_HTML (PROP_TAG(PT_TSTRING, 0x1013))
+#endif
+
+class CMapiFolderList;
+class CMsgStore;
+class CMapiFolder;
+
+class CMapiContentIter {
+ public:
+ virtual BOOL HandleContentItem(ULONG oType, ULONG cb, LPENTRYID pEntry) = 0;
+};
+
+class CMapiHierarchyIter {
+ public:
+ virtual BOOL HandleHierarchyItem(ULONG oType, ULONG cb, LPENTRYID pEntry) = 0;
+};
+
+class CMapiApi {
+ public:
+ CMapiApi();
+ ~CMapiApi();
+
+ static BOOL LoadMapi(void);
+ static BOOL LoadMapiEntryPoints(void);
+ static void UnloadMapi(void);
+
+ static HINSTANCE m_hMapi32;
+
+ static void MAPIUninitialize(void);
+ static HRESULT MAPIInitialize(LPVOID lpInit);
+ static SCODE MAPIAllocateBuffer(ULONG cbSize, LPVOID FAR* lppBuffer);
+ static ULONG MAPIFreeBuffer(LPVOID lpBuff);
+ static HRESULT MAPILogonEx(ULONG ulUIParam, LPTSTR lpszProfileName,
+ LPTSTR lpszPassword, FLAGS flFlags,
+ LPMAPISESSION FAR* lppSession);
+ static HRESULT OpenStreamOnFile(LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer, ULONG ulFlags,
+ LPCTSTR lpszFileName, LPTSTR lpszPrefix,
+ LPSTREAM FAR* lppStream);
+ static void FreeProws(LPSRowSet prows);
+
+ BOOL Initialize(void);
+ BOOL LogOn(void);
+
+ void AddMessageStore(CMsgStore* pStore);
+ void SetCurrentMsgStore(LPMDB lpMdb) { m_lpMdb = lpMdb; }
+
+ // Open any given entry from the current Message Store
+ BOOL OpenEntry(ULONG cbEntry, LPENTRYID pEntryId, LPUNKNOWN* ppOpen);
+ static BOOL OpenMdbEntry(LPMDB lpMdb, ULONG cbEntry, LPENTRYID pEntryId,
+ LPUNKNOWN* ppOpen);
+
+ // Fill in the folders list with the hierarchy from the given
+ // message store.
+ BOOL GetStoreFolders(ULONG cbEid, LPENTRYID lpEid, CMapiFolderList& folders,
+ int startDepth);
+ BOOL GetStoreAddressFolders(ULONG cbEid, LPENTRYID lpEid,
+ CMapiFolderList& folders);
+ BOOL OpenStore(ULONG cbEid, LPENTRYID lpEid, LPMDB* ppMdb);
+
+ // Iteration
+ BOOL IterateStores(CMapiFolderList& list);
+ BOOL IterateContents(CMapiContentIter* pIter, LPMAPIFOLDER pFolder,
+ ULONG flags = 0);
+ BOOL IterateHierarchy(CMapiHierarchyIter* pIter, LPMAPIFOLDER pFolder,
+ ULONG flags = 0);
+
+ // Properties
+ static LPSPropValue GetMapiProperty(LPMAPIPROP pProp, ULONG tag);
+ // If delVal is true, functions will call CMapiApi::MAPIFreeBuffer on pVal.
+ static BOOL GetEntryIdFromProp(LPSPropValue pVal, ULONG& cbEntryId,
+ LPENTRYID& lpEntryId, BOOL delVal = TRUE);
+ static BOOL GetStringFromProp(LPSPropValue pVal, nsCString& val,
+ BOOL delVal = TRUE);
+ static BOOL GetStringFromProp(LPSPropValue pVal, nsString& val,
+ BOOL delVal = TRUE);
+ static LONG GetLongFromProp(LPSPropValue pVal, BOOL delVal = TRUE);
+ static BOOL GetLargeStringProperty(LPMAPIPROP pProp, ULONG tag,
+ nsCString& val);
+ static BOOL GetLargeStringProperty(LPMAPIPROP pProp, ULONG tag,
+ nsString& val);
+ static BOOL IsLargeProperty(LPSPropValue pVal);
+ static ULONG GetEmailPropertyTag(LPMAPIPROP lpProp, LONG nameID);
+
+ static BOOL GetRTFPropertyDecodedAsUTF16(LPMAPIPROP pProp, nsString& val,
+ unsigned long& nativeBodyType,
+ unsigned long codepage = 0);
+
+ // Debugging & reporting stuff
+ static void ListProperties(LPMAPIPROP lpProp, BOOL getValues = TRUE);
+ static void ListPropertyValue(LPSPropValue pVal, nsCString& s);
+
+ protected:
+ BOOL HandleHierarchyItem(ULONG oType, ULONG cb, LPENTRYID pEntry);
+ BOOL HandleContentsItem(ULONG oType, ULONG cb, LPENTRYID pEntry);
+ void GetStoreInfo(CMapiFolder* pFolder, long* pSzContents);
+
+ // array of available message stores, cached so that
+ // message stores are only opened once, preventing multiple
+ // logon's by the user if the store requires a logon.
+ CMsgStore* FindMessageStore(ULONG cbEid, LPENTRYID lpEid);
+ void ClearMessageStores(void);
+
+ static void CStrToUnicode(const char* pStr, nsString& result);
+
+ // Debugging & reporting stuff
+ static void GetPropTagName(ULONG tag, nsCString& s);
+ static void ReportStringProp(const char* pTag, LPSPropValue pVal);
+ static void ReportUIDProp(const char* pTag, LPSPropValue pVal);
+ static void ReportLongProp(const char* pTag, LPSPropValue pVal);
+
+ private:
+ static int m_clients;
+ static BOOL m_initialized;
+ static nsTArray<CMsgStore*>* m_pStores;
+ static LPMAPISESSION m_lpSession;
+ static LPMDB m_lpMdb;
+ static HRESULT m_lastError;
+ static char16_t* m_pUniBuff;
+ static int m_uniBuffLen;
+
+ static BOOL GetLargeProperty(LPMAPIPROP pProp, ULONG tag, void** result);
+};
+
+class CMapiFolder {
+ public:
+ CMapiFolder();
+ explicit CMapiFolder(const CMapiFolder* pCopyFrom);
+ CMapiFolder(const char16_t* pDisplayName, ULONG cbEid, LPENTRYID lpEid,
+ int depth, LONG oType = MAPI_FOLDER);
+ ~CMapiFolder();
+
+ void SetDoImport(BOOL doIt) { m_doImport = doIt; }
+ void SetObjectType(long oType) { m_objectType = oType; }
+ void SetDisplayName(const char16_t* pDisplayName) {
+ m_displayName = pDisplayName;
+ }
+ void SetEntryID(ULONG cbEid, LPENTRYID lpEid);
+ void SetDepth(int depth) { m_depth = depth; }
+ void SetFilePath(const char16_t* pFilePath) { m_mailFilePath = pFilePath; }
+
+ BOOL GetDoImport(void) const { return m_doImport; }
+ LONG GetObjectType(void) const { return m_objectType; }
+ void GetDisplayName(nsString& name) const { name = m_displayName; }
+ void GetFilePath(nsString& path) const { path = m_mailFilePath; }
+ BOOL IsStore(void) const { return m_objectType == MAPI_STORE; }
+ BOOL IsFolder(void) const { return m_objectType == MAPI_FOLDER; }
+ int GetDepth(void) const { return m_depth; }
+
+ LPENTRYID GetEntryID(ULONG* pCb = NULL) const {
+ if (pCb) *pCb = m_cbEid;
+ return (LPENTRYID)m_lpEid;
+ }
+ ULONG GetCBEntryID(void) const { return m_cbEid; }
+
+ private:
+ LONG m_objectType;
+ ULONG m_cbEid;
+ BYTE* m_lpEid;
+ nsString m_displayName;
+ int m_depth;
+ nsString m_mailFilePath;
+ BOOL m_doImport;
+};
+
+class CMapiFolderList {
+ public:
+ CMapiFolderList();
+ ~CMapiFolderList();
+
+ void AddItem(CMapiFolder* pFolder);
+ CMapiFolder* GetItem(int index) {
+ if ((index >= 0) && (index < (int)m_array.Length()))
+ return GetAt(index);
+ else
+ return NULL;
+ }
+ void ClearAll(void);
+
+ // Debugging and reporting
+ void DumpList(void);
+
+ CMapiFolder* GetAt(int index) { return m_array.ElementAt(index); }
+ int GetSize(void) { return m_array.Length(); }
+
+ protected:
+ void EnsureUniqueName(CMapiFolder* pFolder);
+ void GenerateFilePath(CMapiFolder* pFolder);
+ void ChangeName(nsString& name);
+
+ private:
+ nsTArray<CMapiFolder*> m_array;
+};
+
+class CMsgStore {
+ public:
+ explicit CMsgStore(ULONG cbEid = 0, LPENTRYID lpEid = NULL);
+ ~CMsgStore();
+
+ void SetEntryID(ULONG cbEid, LPENTRYID lpEid);
+ BOOL Open(LPMAPISESSION pSession, LPMDB* ppMdb);
+
+ ULONG GetCBEntryID(void) { return m_cbEid; }
+ LPENTRYID GetLPEntryID(void) { return (LPENTRYID)m_lpEid; }
+
+ private:
+ ULONG m_cbEid;
+ BYTE* m_lpEid;
+ LPMDB m_lpMdb;
+};
+
+class CMapiFolderContents {
+ public:
+ CMapiFolderContents(LPMDB lpMdb, ULONG cbEID, LPENTRYID lpEid);
+ ~CMapiFolderContents();
+
+ BOOL GetNext(ULONG* pcbEid, LPENTRYID* ppEid, ULONG* poType, BOOL* pDone);
+
+ ULONG GetCount(void) { return m_count; }
+
+ protected:
+ BOOL SetUpIter(void);
+
+ private:
+ HRESULT m_lastError;
+ BOOL m_failure;
+ LPMDB m_lpMdb;
+ LPMAPIFOLDER m_lpFolder;
+ LPMAPITABLE m_lpTable;
+ ULONG m_fCbEid;
+ BYTE* m_fLpEid;
+ ULONG m_count;
+ ULONG m_iterCount;
+ BYTE* m_lastLpEid;
+ ULONG m_lastCbEid;
+};
+
+#endif /* MapiApi_h__ */
diff --git a/comm/mailnews/import/src/MapiDbgLog.h b/comm/mailnews/import/src/MapiDbgLog.h
new file mode 100644
index 0000000000..56580920ea
--- /dev/null
+++ b/comm/mailnews/import/src/MapiDbgLog.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef MapiDbgLog_h___
+#define MapiDbgLog_h___
+
+/*
+#ifdef NS_DEBUG
+#define MAPI_DEBUG 1
+#endif
+*/
+
+#ifdef MAPI_DEBUG
+# include <stdio.h>
+
+# define MAPI_DUMP_STRING(x) printf("%s", (const char*)x)
+# define MAPI_TRACE0(x) printf(x)
+# define MAPI_TRACE1(x, y) printf(x, y)
+# define MAPI_TRACE2(x, y, z) printf(x, y, z)
+# define MAPI_TRACE3(x, y, z, a) printf(x, y, z, a)
+# define MAPI_TRACE4(x, y, z, a, b) printf(x, y, z, a, b)
+
+#else
+
+# define MAPI_DUMP_STRING(x)
+# define MAPI_TRACE0(x)
+# define MAPI_TRACE1(x, y)
+# define MAPI_TRACE2(x, y, z)
+# define MAPI_TRACE3(x, y, z, a)
+# define MAPI_TRACE4(x, y, z, a, b)
+
+#endif
+
+#endif /* MapiDbgLog_h___ */
diff --git a/comm/mailnews/import/src/MapiMessage.cpp b/comm/mailnews/import/src/MapiMessage.cpp
new file mode 100644
index 0000000000..af57aa1d4c
--- /dev/null
+++ b/comm/mailnews/import/src/MapiMessage.cpp
@@ -0,0 +1,1383 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef INITGUID
+# define INITGUID
+#endif
+
+#ifndef USES_IID_IMessage
+# define USES_IID_IMessage
+#endif
+
+#include "nscore.h"
+#include <time.h>
+#include "nsString.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsMsgUtils.h"
+#include "nsMimeTypes.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIOutputStream.h"
+
+#include "MapiDbgLog.h"
+#include "MapiApi.h"
+
+#include "MapiMimeTypes.h"
+
+#include "nsMsgI18N.h"
+#include "nsCRT.h"
+#include "nsNetUtil.h"
+#include "MapiMessage.h"
+
+#include "nsOutlookMail.h"
+
+#include "mozilla/Encoding.h"
+
+#include <stdlib.h>
+#include <tuple>
+
+// needed for the call the OpenStreamOnFile
+extern LPMAPIALLOCATEBUFFER gpMapiAllocateBuffer;
+extern LPMAPIFREEBUFFER gpMapiFreeBuffer;
+
+// Sample From line: From - 1 Jan 1965 00:00:00
+
+typedef const char* PC_S8;
+
+static const char* kWhitespace = "\b\t\r\n ";
+static const char* sFromLine = "From - ";
+static const char* sFromDate = "Mon Jan 1 00:00:00 1965";
+static const char* sDaysOfWeek[7] = {"Sun", "Mon", "Tue", "Wed",
+ "Thu", "Fri", "Sat"};
+
+static const char* sMonths[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+
+CMapiMessage::CMapiMessage(LPMESSAGE lpMsg)
+ : m_lpMsg(lpMsg), m_dldStateHeadersOnly(false), m_msgFlags(0) {
+ nsresult rv;
+ m_pIOService = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return;
+
+ FetchHeaders();
+ if (ValidState()) {
+ BuildFromLine();
+ FetchFlags();
+ GetDownloadState();
+ if (FullMessageDownloaded()) {
+ FetchBody();
+ ProcessAttachments();
+ }
+ }
+}
+
+CMapiMessage::~CMapiMessage() {
+ ClearAttachments();
+ if (m_lpMsg) m_lpMsg->Release();
+}
+
+void CMapiMessage::FormatDateTime(SYSTEMTIME& tm, nsCString& s,
+ bool includeTZ) {
+ long offset = _timezone;
+ s += sDaysOfWeek[tm.wDayOfWeek];
+ s += ", ";
+ s.AppendInt((int32_t)tm.wDay);
+ s += " ";
+ s += sMonths[tm.wMonth - 1];
+ s += " ";
+ s.AppendInt((int32_t)tm.wYear);
+ s += " ";
+ int val = tm.wHour;
+ if (val < 10) s += "0";
+ s.AppendInt((int32_t)val);
+ s += ":";
+ val = tm.wMinute;
+ if (val < 10) s += "0";
+ s.AppendInt((int32_t)val);
+ s += ":";
+ val = tm.wSecond;
+ if (val < 10) s += "0";
+ s.AppendInt((int32_t)val);
+ if (includeTZ) {
+ s += " ";
+ if (offset < 0) {
+ offset *= -1;
+ s += "+";
+ } else
+ s += "-";
+ offset /= 60;
+ val = (int)(offset / 60);
+ if (val < 10) s += "0";
+ s.AppendInt((int32_t)val);
+ val = (int)(offset % 60);
+ if (val < 10) s += "0";
+ s.AppendInt((int32_t)val);
+ }
+}
+
+bool CMapiMessage::EnsureHeader(CMapiMessageHeaders::SpecialHeader special,
+ ULONG mapiTag) {
+ if (m_headers.Value(special)) return true;
+
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, mapiTag);
+ bool success = false;
+ if (pVal) {
+ if (PROP_TYPE(pVal->ulPropTag) == PT_STRING8) {
+ if (pVal->Value.lpszA && strlen(pVal->Value.lpszA)) {
+ m_headers.SetValue(special, pVal->Value.lpszA);
+ success = true;
+ }
+ } else if (PROP_TYPE(pVal->ulPropTag) == PT_UNICODE) {
+ if (pVal->Value.lpszW && wcslen(pVal->Value.lpszW)) {
+ m_headers.SetValue(special,
+ NS_ConvertUTF16toUTF8(pVal->Value.lpszW).get());
+ success = true;
+ }
+ }
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+
+ return success;
+}
+
+bool CMapiMessage::EnsureDate() {
+ if (m_headers.Value(CMapiMessageHeaders::hdrDate)) return true;
+
+ LPSPropValue pVal =
+ CMapiApi::GetMapiProperty(m_lpMsg, PR_MESSAGE_DELIVERY_TIME);
+ if (!pVal) pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_CREATION_TIME);
+ if (pVal) {
+ SYSTEMTIME st;
+ // the following call returns UTC
+ ::FileTimeToSystemTime(&(pVal->Value.ft), &st);
+ CMapiApi::MAPIFreeBuffer(pVal);
+ // FormatDateTime would append the local time zone, so don't use it.
+ // Instead, we just append +0000 for GMT/UTC here.
+ nsCString str;
+ FormatDateTime(st, str, false);
+ str += " +0000";
+ m_headers.SetValue(CMapiMessageHeaders::hdrDate, str.get());
+ return true;
+ }
+
+ return false;
+}
+
+void CMapiMessage::BuildFromLine(void) {
+ m_fromLine = sFromLine;
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_CREATION_TIME);
+ if (pVal) {
+ SYSTEMTIME st;
+ ::FileTimeToSystemTime(&(pVal->Value.ft), &st);
+ CMapiApi::MAPIFreeBuffer(pVal);
+ FormatDateTime(st, m_fromLine, FALSE);
+ } else
+ m_fromLine += sFromDate;
+
+ m_fromLine += "\x0D\x0A";
+}
+
+#ifndef dispidHeaderItem
+# define dispidHeaderItem 0x8578
+#endif
+DEFINE_OLEGUID(PSETID_Common, MAKELONG(0x2000 + (8), 0x0006), 0, 0);
+
+void CMapiMessage::GetDownloadState() {
+ // See http://support.microsoft.com/kb/912239
+ ULONG ulVal = 0;
+ LPSPropValue lpPropVal = NULL;
+ LPSPropTagArray lpNamedPropTag = NULL;
+ MAPINAMEID NamedID = {0};
+ LPMAPINAMEID lpNamedID = NULL;
+
+ NamedID.lpguid = (LPGUID)&PSETID_Common;
+ NamedID.ulKind = MNID_ID;
+ NamedID.Kind.lID = dispidHeaderItem;
+ lpNamedID = &NamedID;
+
+ m_lpMsg->GetIDsFromNames(1, &lpNamedID, NULL, &lpNamedPropTag);
+
+ if (lpNamedPropTag && 1 == lpNamedPropTag->cValues) {
+ lpNamedPropTag->aulPropTag[0] =
+ CHANGE_PROP_TYPE(lpNamedPropTag->aulPropTag[0], PT_LONG);
+
+ // Get the value of the property.
+ m_lpMsg->GetProps(lpNamedPropTag, 0, &ulVal, &lpPropVal);
+ if (lpPropVal && 1 == ulVal && PT_LONG == PROP_TYPE(lpPropVal->ulPropTag) &&
+ lpPropVal->Value.ul)
+ m_dldStateHeadersOnly = true;
+ }
+
+ CMapiApi::MAPIFreeBuffer(lpPropVal);
+ CMapiApi::MAPIFreeBuffer(lpNamedPropTag);
+}
+
+// Headers - fetch will get PR_TRANSPORT_MESSAGE_HEADERS
+// or if they do not exist will build a header from
+// PR_DISPLAY_TO, _CC, _BCC
+// PR_SUBJECT
+// PR_MESSAGE_RECIPIENTS
+// and PR_CREATION_TIME if needed?
+bool CMapiMessage::FetchHeaders(void) {
+ ULONG tag = PR_TRANSPORT_MESSAGE_HEADERS_A;
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, tag);
+ if (!pVal)
+ pVal = CMapiApi::GetMapiProperty(m_lpMsg,
+ tag = PR_TRANSPORT_MESSAGE_HEADERS_W);
+ if (pVal) {
+ if (CMapiApi::IsLargeProperty(pVal)) {
+ nsCString headers;
+ CMapiApi::GetLargeStringProperty(m_lpMsg, tag, headers);
+ m_headers.Assign(headers.get());
+ } else if ((PROP_TYPE(pVal->ulPropTag) == PT_STRING8) &&
+ (pVal->Value.lpszA) && (*(pVal->Value.lpszA)))
+ m_headers.Assign(pVal->Value.lpszA);
+ else if ((PROP_TYPE(pVal->ulPropTag) == PT_UNICODE) &&
+ (pVal->Value.lpszW) && (*(pVal->Value.lpszW))) {
+ nsCString headers;
+ LossyCopyUTF16toASCII(nsDependentString(pVal->Value.lpszW), headers);
+ m_headers.Assign(headers.get());
+ }
+
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+
+ EnsureDate();
+ if (!EnsureHeader(CMapiMessageHeaders::hdrFrom, PR_SENDER_NAME_W))
+ EnsureHeader(CMapiMessageHeaders::hdrFrom, PR_SENDER_EMAIL_ADDRESS_W);
+ EnsureHeader(CMapiMessageHeaders::hdrSubject, PR_SUBJECT_W);
+ EnsureHeader(CMapiMessageHeaders::hdrTo, PR_DISPLAY_TO_W);
+ EnsureHeader(CMapiMessageHeaders::hdrCc, PR_DISPLAY_CC_W);
+ EnsureHeader(CMapiMessageHeaders::hdrBcc, PR_DISPLAY_BCC_W);
+
+ ProcessContentType();
+
+ return !m_headers.IsEmpty();
+}
+
+// Mime-Version: 1.0
+// Content-Type: text/plain; charset="US-ASCII"
+// Content-Type: multipart/mixed; boundary="=====================_874475278==_"
+
+void CMapiMessage::ProcessContentType() {
+ m_mimeContentType.Truncate();
+ m_mimeBoundary.Truncate();
+ m_mimeCharset.Truncate();
+
+ const char* contentType =
+ m_headers.Value(CMapiMessageHeaders::hdrContentType);
+ if (!contentType) return;
+
+ const char *begin = contentType, *end;
+ nsCString tStr;
+
+ // Note: this isn't a complete parser, the content type
+ // we extract could have rfc822 comments in it
+ while (*begin && IsSpace(*begin)) begin++;
+ if (!(*begin)) return;
+ end = begin;
+ while (*end && (*end != ';')) end++;
+ m_mimeContentType.Assign(begin, end - begin);
+ if (!(*end)) return;
+ // look for "boundary="
+ begin = end + 1;
+ bool haveB;
+ bool haveC;
+ while (*begin) {
+ haveB = false;
+ haveC = false;
+ while (*begin && IsSpace(*begin)) begin++;
+ if (!(*begin)) return;
+ end = begin;
+ while (*end && (*end != '=')) end++;
+ if (end - begin) {
+ tStr.Assign(begin, end - begin);
+ if (tStr.LowerCaseEqualsLiteral("boundary"))
+ haveB = true;
+ else if (tStr.LowerCaseEqualsLiteral("charset"))
+ haveC = true;
+ }
+ if (!(*end)) return;
+ begin = end + 1;
+ while (*begin && IsSpace(*begin)) begin++;
+ if (*begin == '"') {
+ begin++;
+ bool slash = false;
+ tStr.Truncate();
+ while (*begin) {
+ if (slash) {
+ slash = false;
+ tStr.Append(*begin);
+ } else if (*begin == '"')
+ break;
+ else if (*begin != '\\')
+ tStr.Append(*begin);
+ else
+ slash = true;
+ begin++;
+ }
+ if (haveB) {
+ m_mimeBoundary = tStr;
+ haveB = false;
+ }
+ if (haveC) {
+ m_mimeCharset = tStr;
+ haveC = false;
+ }
+ if (!(*begin)) return;
+ begin++;
+ }
+ tStr.Truncate();
+ while (*begin && (*begin != ';')) {
+ tStr.Append(*(begin++));
+ }
+ if (haveB) {
+ tStr.Trim(kWhitespace);
+ m_mimeBoundary = tStr;
+ }
+ if (haveC) {
+ tStr.Trim(kWhitespace);
+ m_mimeCharset = tStr;
+ }
+ if (*begin) begin++;
+ }
+}
+
+const char* CpToCharset(unsigned int cp) {
+ struct CODEPAGE_TO_CHARSET {
+ unsigned long cp;
+ const char* charset;
+ };
+
+ // This table is based on
+ // http://msdn.microsoft.com/en-us/library/dd317756(v=VS.85).aspx#1; Please
+ // extend as appropriate. The codepage values are sorted ascending.
+ static const CODEPAGE_TO_CHARSET cptocharset[] = {
+ {37, "IBM037"}, // IBM EBCDIC US-Canada
+ {437, "IBM437"}, // OEM United States
+ {500, "IBM500"}, // IBM EBCDIC International
+ {708, "ASMO-708"}, // Arabic (ASMO 708)
+ // 709 Arabic (ASMO-449+, BCON V4)
+ // 710 Arabic - Transparent Arabic
+ {720, "DOS-720"}, // Arabic (Transparent ASMO); Arabic (DOS)
+ {737, "ibm737"}, // OEM Greek (formerly 437G); Greek (DOS)
+ {775, "ibm775"}, // OEM Baltic; Baltic (DOS)
+ {850, "ibm850"}, // OEM Multilingual Latin 1; Western European (DOS)
+ {852, "ibm852"}, // OEM Latin 2; Central European (DOS)
+ {855, "IBM855"}, // OEM Cyrillic (primarily Russian)
+ {857, "ibm857"}, // OEM Turkish; Turkish (DOS)
+ {858, "IBM00858"}, // OEM Multilingual Latin 1 + Euro symbol
+ {860, "IBM860"}, // OEM Portuguese; Portuguese (DOS)
+ {861, "ibm861"}, // OEM Icelandic; Icelandic (DOS)
+ {862, "DOS-862"}, // OEM Hebrew; Hebrew (DOS)
+ {863, "IBM863"}, // OEM French Canadian; French Canadian (DOS)
+ {864, "IBM864"}, // OEM Arabic; Arabic (864)
+ {865, "IBM865"}, // OEM Nordic; Nordic (DOS)
+ {866, "cp866"}, // OEM Russian; Cyrillic (DOS)
+ {869, "ibm869"}, // OEM Modern Greek; Greek, Modern (DOS)
+ {870, "IBM870"}, // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC
+ // Multilingual Latin 2
+ {874, "windows-874"}, // ANSI/OEM Thai (same as 28605, ISO 8859-15); Thai
+ // (Windows)
+ {875, "cp875"}, // IBM EBCDIC Greek Modern
+ {932, "shift_jis"}, // ANSI/OEM Japanese; Japanese (Shift-JIS)
+ {936, "gb2312"}, // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese
+ // Simplified (GB2312)
+ {949, "ks_c_5601-1987"}, // ANSI/OEM Korean (Unified Hangul Code)
+ {950, "big5"}, // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR,
+ // PRC); Chinese Traditional (Big5)
+ {1026, "IBM1026"}, // IBM EBCDIC Turkish (Latin 5)
+ {1047, "IBM01047"}, // IBM EBCDIC Latin 1/Open System
+ {1140, "IBM01140"}, // IBM EBCDIC US-Canada (037 + Euro symbol); IBM
+ // EBCDIC (US-Canada-Euro)
+ {1141, "IBM01141"}, // IBM EBCDIC Germany (20273 + Euro symbol); IBM
+ // EBCDIC (Germany-Euro)
+ {1142, "IBM01142"}, // IBM EBCDIC Denmark-Norway (20277 + Euro symbol);
+ // IBM EBCDIC (Denmark-Norway-Euro)
+ {1143, "IBM01143"}, // IBM EBCDIC Finland-Sweden (20278 + Euro symbol);
+ // IBM EBCDIC (Finland-Sweden-Euro)
+ {1144, "IBM01144"}, // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC
+ // (Italy-Euro)
+ {1145, "IBM01145"}, // IBM EBCDIC Latin America-Spain (20284 + Euro
+ // symbol); IBM EBCDIC (Spain-Euro)
+ {1146, "IBM01146"}, // IBM EBCDIC United Kingdom (20285 + Euro symbol);
+ // IBM EBCDIC (UK-Euro)
+ {1147, "IBM01147"}, // IBM EBCDIC France (20297 + Euro symbol); IBM
+ // EBCDIC (France-Euro)
+ {1148, "IBM01148"}, // IBM EBCDIC International (500 + Euro symbol); IBM
+ // EBCDIC (International-Euro)
+ {1149, "IBM01149"}, // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM
+ // EBCDIC (Icelandic-Euro)
+ {1200, "utf-16"}, // Unicode UTF-16, little endian byte order (BMP of ISO
+ // 10646); available only to managed applications
+ {1201, "unicodeFFFE"}, // Unicode UTF-16, big endian byte order;
+ // available only to managed applications
+ {1250,
+ "windows-1250"}, // ANSI Central European; Central European (Windows)
+ {1251, "windows-1251"}, // ANSI Cyrillic; Cyrillic (Windows)
+ {1252, "windows-1252"}, // ANSI Latin 1; Western European (Windows)
+ {1253, "windows-1253"}, // ANSI Greek; Greek (Windows)
+ {1254, "windows-1254"}, // ANSI Turkish; Turkish (Windows)
+ {1255, "windows-1255"}, // ANSI Hebrew; Hebrew (Windows)
+ {1256, "windows-1256"}, // ANSI Arabic; Arabic (Windows)
+ {1257, "windows-1257"}, // ANSI Baltic; Baltic (Windows)
+ {1258, "windows-1258"}, // ANSI/OEM Vietnamese; Vietnamese (Windows)
+ {1361, "Johab"}, // Korean (Johab)
+ {10000, "macintosh"}, // MAC Roman; Western European (Mac)
+ {10001, "x-mac-japanese"}, // Japanese (Mac)
+ {10002, "x-mac-chinesetrad"}, // MAC Traditional Chinese (Big5); Chinese
+ // Traditional (Mac)
+ {10003, "x-mac-korean"}, // Korean (Mac)
+ {10004, "x-mac-arabic"}, // Arabic (Mac)
+ {10005, "x-mac-hebrew"}, // Hebrew (Mac)
+ {10006, "x-mac-greek"}, // Greek (Mac)
+ {10007, "x-mac-cyrillic"}, // Cyrillic (Mac)
+ {10008, "x-mac-chinesesimp"}, // MAC Simplified Chinese (GB 2312);
+ // Chinese Simplified (Mac)
+ {10010, "x-mac-romanian"}, // Romanian (Mac)
+ {10017, "x-mac-ukrainian"}, // Ukrainian (Mac)
+ {10021, "x-mac-thai"}, // Thai (Mac)
+ {10029, "x-mac-ce"}, // MAC Latin 2; Central European (Mac)
+ {10079, "x-mac-icelandic"}, // Icelandic (Mac)
+ {10081, "x-mac-turkish"}, // Turkish (Mac)
+ {10082, "x-mac-croatian"}, // Croatian (Mac)
+ // Unicode UTF-32, little endian byte order; available only to managed
+ // applications impossible in 8-bit mail
+ {12000, "utf-32"},
+ // Unicode UTF-32, big endian byte order; available only to managed
+ // applications impossible in 8-bit mail
+ {12001, "utf-32BE"},
+ {20000, "x-Chinese_CNS"}, // CNS Taiwan; Chinese Traditional (CNS)
+ {20001, "x-cp20001"}, // TCA Taiwan
+ {20002, "x_Chinese-Eten"}, // Eten Taiwan; Chinese Traditional (Eten)
+ {20003, "x-cp20003"}, // IBM5550 Taiwan
+ {20004, "x-cp20004"}, // TeleText Taiwan
+ {20005, "x-cp20005"}, // Wang Taiwan
+ {20105, "x-IA5"}, // IA5 (IRV International Alphabet No. 5, 7-bit);
+ // Western European (IA5)
+ {20106, "x-IA5-German"}, // IA5 German (7-bit)
+ {20107, "x-IA5-Swedish"}, // IA5 Swedish (7-bit)
+ {20108, "x-IA5-Norwegian"}, // IA5 Norwegian (7-bit)
+ {20127, "us-ascii"}, // US-ASCII (7-bit)
+ {20261, "x-cp20261"}, // T.61
+ {20269, "x-cp20269"}, // ISO 6937 Non-Spacing Accent
+ {20273, "IBM273"}, // IBM EBCDIC Germany
+ {20277, "IBM277"}, // IBM EBCDIC Denmark-Norway
+ {20278, "IBM278"}, // IBM EBCDIC Finland-Sweden
+ {20280, "IBM280"}, // IBM EBCDIC Italy
+ {20284, "IBM284"}, // IBM EBCDIC Latin America-Spain
+ {20285, "IBM285"}, // IBM EBCDIC United Kingdom
+ {20290, "IBM290"}, // IBM EBCDIC Japanese Katakana Extended
+ {20297, "IBM297"}, // IBM EBCDIC France
+ {20420, "IBM420"}, // IBM EBCDIC Arabic
+ {20423, "IBM423"}, // IBM EBCDIC Greek
+ {20424, "IBM424"}, // IBM EBCDIC Hebrew
+ {20833, "x-EBCDIC-KoreanExtended"}, // IBM EBCDIC Korean Extended
+ {20838, "IBM-Thai"}, // IBM EBCDIC Thai
+ {20866, "koi8-r"}, // Russian (KOI8-R); Cyrillic (KOI8-R)
+ {20871, "IBM871"}, // IBM EBCDIC Icelandic
+ {20880, "IBM880"}, // IBM EBCDIC Cyrillic Russian
+ {20905, "IBM905"}, // IBM EBCDIC Turkish
+ {20924,
+ "IBM00924"}, // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
+ {20932, "EUC-JP"}, // Japanese (JIS 0208-1990 and 0121-1990)
+ {20936, "x-cp20936"}, // Simplified Chinese (GB2312); Chinese Simplified
+ // (GB2312-80)
+ {20949, "x-cp20949"}, // Korean Wansung
+ {21025, "cp1025"}, // IBM EBCDIC Cyrillic Serbian-Bulgarian
+ // 21027 (deprecated)
+ {21866, "koi8-u"}, // Ukrainian (KOI8-U); Cyrillic (KOI8-U)
+ {28591, "iso-8859-1"}, // ISO 8859-1 Latin 1; Western European (ISO)
+ {28592,
+ "iso-8859-2"}, // ISO 8859-2 Central European; Central European (ISO)
+ {28593, "iso-8859-3"}, // ISO 8859-3 Latin 3
+ {28594, "iso-8859-4"}, // ISO 8859-4 Baltic
+ {28595, "iso-8859-5"}, // ISO 8859-5 Cyrillic
+ {28596, "iso-8859-6"}, // ISO 8859-6 Arabic
+ {28597, "iso-8859-7"}, // ISO 8859-7 Greek
+ {28598, "iso-8859-8"}, // ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
+ {28599, "iso-8859-9"}, // ISO 8859-9 Turkish
+ {28603, "iso-8859-13"}, // ISO 8859-13 Estonian
+ {28605, "iso-8859-15"}, // ISO 8859-15 Latin 9
+ {29001, "x-Europa"}, // Europa 3
+ {38598, "iso-8859-8-i"}, // ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
+ {50220, "iso-2022-jp"}, // ISO 2022 Japanese with no halfwidth Katakana;
+ // Japanese (JIS)
+ {50221, "csISO2022JP"}, // ISO 2022 Japanese with halfwidth Katakana;
+ // Japanese (JIS-Allow 1 byte Kana)
+ {50222, "iso-2022-jp"}, // ISO 2022 Japanese JIS X 0201-1989; Japanese
+ // (JIS-Allow 1 byte Kana - SO/SI)
+ {50225, "iso-2022-kr"}, // ISO 2022 Korean
+ {50227, "x-cp50227"}, // ISO 2022 Simplified Chinese; Chinese Simplified
+ // (ISO 2022)
+ // 50229 ISO 2022 Traditional Chinese
+ // 50930 EBCDIC Japanese (Katakana) Extended
+ // 50931 EBCDIC US-Canada and Japanese
+ // 50933 EBCDIC Korean Extended and Korean
+ // 50935 EBCDIC Simplified Chinese Extended and Simplified Chinese
+ // 50936 EBCDIC Simplified Chinese
+ // 50937 EBCDIC US-Canada and Traditional Chinese
+ // 50939 EBCDIC Japanese (Latin) Extended and Japanese
+ {51932, "euc-jp"}, // EUC Japanese
+ {51936, "EUC-CN"}, // EUC Simplified Chinese; Chinese Simplified (EUC)
+ {51949, "euc-kr"}, // EUC Korean
+ // 51950 EUC Traditional Chinese
+ {52936,
+ "hz-gb-2312"}, // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
+ {54936, "GB18030"}, // Windows XP and later: GB18030 Simplified Chinese
+ // (4 byte); Chinese Simplified (GB18030)
+ {57002, "x-iscii-de"}, // ISCII Devanagari
+ {57003, "x-iscii-be"}, // ISCII Bengali
+ {57004, "x-iscii-ta"}, // ISCII Tamil
+ {57005, "x-iscii-te"}, // ISCII Telugu
+ {57006, "x-iscii-as"}, // ISCII Assamese
+ {57007, "x-iscii-or"}, // ISCII Oriya (Odia)
+ {57008, "x-iscii-ka"}, // ISCII Kannada
+ {57009, "x-iscii-ma"}, // ISCII Malayalam
+ {57010, "x-iscii-gu"}, // ISCII Gujarati
+ {57011, "x-iscii-pa"}, // ISCII Punjabi
+ {65000, "utf-7"}, // Unicode (UTF-7)
+ {65001, "utf-8"}, // Unicode (UTF-8)
+ };
+
+ // Binary search
+ int begin = 0, end = sizeof(cptocharset) / sizeof(cptocharset[0]) - 1;
+ while (begin <= end) {
+ int mid = (begin + end) / 2;
+ unsigned int mid_cp = cptocharset[mid].cp;
+ if (cp == mid_cp) return cptocharset[mid].charset;
+ if (cp < mid_cp)
+ end = mid - 1;
+ else // cp > cptocharset[mid].cp
+ begin = mid + 1;
+ }
+ return 0; // not found
+}
+
+// This function returns true only if the unicode (utf-16) text can be
+// losslessly represented in specified charset
+bool CMapiMessage::CheckBodyInCharsetRange(const char* charset) {
+ if (m_body.IsEmpty()) return true;
+ if (!_stricmp(charset, "utf-8")) return true;
+
+ auto encoding =
+ mozilla::Encoding::ForLabelNoReplacement(nsDependentCString(charset));
+ if (!encoding) return false;
+ auto encoder = encoding->NewEncoder();
+
+ uint8_t buffer[512];
+ auto src = mozilla::Span(m_body);
+ auto dst = mozilla::Span(buffer);
+ while (true) {
+ uint32_t result;
+ size_t read;
+ std::tie(result, read, std::ignore) =
+ encoder->EncodeFromUTF16WithoutReplacement(src, dst, false);
+ if (result == mozilla::kInputEmpty) {
+ // All converted successfully.
+ break;
+ } else if (result != mozilla::kOutputFull) {
+ // Didn't use all the input but the output isn't full, hence
+ // there was an unencodable character.
+ return false;
+ }
+ src = src.From(read);
+ }
+
+ return true;
+}
+
+bool CaseInsensitiveComp(wchar_t elem1, wchar_t elem2) {
+ return _wcsnicmp(&elem1, &elem2, 1) == 0;
+}
+
+void ExtractMetaCharset(const wchar_t* body, int bodySz,
+ /*out*/ nsCString& charset) {
+ charset.Truncate();
+ const wchar_t* body_end = body + bodySz;
+ const wchar_t str_eohd[] = L"/head";
+ const wchar_t* str_eohd_end =
+ str_eohd + sizeof(str_eohd) / sizeof(str_eohd[0]) - 1;
+ const wchar_t* eohd_pos =
+ std::search(body, body_end, str_eohd, str_eohd_end, CaseInsensitiveComp);
+ if (eohd_pos == body_end) // No header!
+ return;
+ const wchar_t str_chset[] = L"charset=";
+ const wchar_t* str_chset_end =
+ str_chset + sizeof(str_chset) / sizeof(str_chset[0]) - 1;
+ const wchar_t* chset_pos = std::search(body, eohd_pos, str_chset,
+ str_chset_end, CaseInsensitiveComp);
+ if (chset_pos == eohd_pos) // No charset!
+ return;
+ chset_pos += 8;
+
+ // remove everything from the string after the next ; or " or space,
+ // whichever comes first.
+ // The initial string looks something like
+ // <META content="text/html; charset=utf-8" http-equiv=Content-Type>
+ // <META content="text/html; charset=utf-8;" http-equiv=Content-Type>
+ // <META content="text/html; charset=utf-8 ;" http-equiv=Content-Type>
+ // <META content="text/html; charset=utf-8 " http-equiv=Content-Type>
+ const wchar_t term[] = L";\" ",
+ *term_end = term + sizeof(term) / sizeof(term[0]) - 1;
+ const wchar_t* chset_end =
+ std::find_first_of(chset_pos, eohd_pos, term, term_end);
+ if (chset_end != eohd_pos)
+ LossyCopyUTF16toASCII(
+ Substring(char16ptr_t(chset_pos), char16ptr_t(chset_end)), charset);
+}
+
+bool CMapiMessage::FetchBody(void) {
+ m_bodyIsHtml = false;
+ m_body.Truncate();
+
+ // Get the Outlook codepage info; if unsuccessful then it defaults to 0
+ // (CP_ACP) -> system default Maybe we can use this info later?
+ unsigned int codepage = 0;
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_INTERNET_CPID);
+ if (pVal) {
+ if (PROP_TYPE(pVal->ulPropTag) == PT_LONG) codepage = pVal->Value.l;
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+
+ unsigned long nativeBodyType = 0;
+ if (CMapiApi::GetRTFPropertyDecodedAsUTF16(m_lpMsg, m_body, nativeBodyType,
+ codepage)) {
+ m_bodyIsHtml = nativeBodyType == MAPI_NATIVE_BODY_TYPE_HTML;
+ } else { // Cannot get RTF version
+ // Is it html?
+ pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_BODY_HTML_W);
+ if (pVal) {
+ if (CMapiApi::IsLargeProperty(pVal))
+ CMapiApi::GetLargeStringProperty(m_lpMsg, PR_BODY_HTML_W, m_body);
+ else if ((PROP_TYPE(pVal->ulPropTag) == PT_UNICODE) &&
+ (pVal->Value.lpszW) && (*(pVal->Value.lpszW)))
+ m_body.Assign(pVal->Value.lpszW);
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+
+ // Kind-hearted Outlook will give us html even for a plain text message.
+ // But it will include a comment saying it did the conversion.
+ // We'll use this as a hack to really use the plain text part.
+ //
+ // Sadly there are cases where this string is returned despite the fact
+ // that the message is indeed HTML.
+ //
+ // To detect the "true" plain text messages, we look for our string
+ // immediately following the <BODY> tag.
+ if (!m_body.IsEmpty() &&
+ m_body.Find(u"<BODY>\r\n<!-- Converted from text/plain format -->") ==
+ kNotFound) {
+ m_bodyIsHtml = true;
+ } else {
+ pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_BODY_W);
+ if (pVal) {
+ if (CMapiApi::IsLargeProperty(pVal))
+ CMapiApi::GetLargeStringProperty(m_lpMsg, PR_BODY_W, m_body);
+ else if ((PROP_TYPE(pVal->ulPropTag) == PT_UNICODE) &&
+ (pVal->Value.lpszW) && (*(pVal->Value.lpszW)))
+ m_body.Assign(pVal->Value.lpszW);
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+ }
+ }
+
+ // OK, now let's restore the original encoding!
+ // 1. We may have a header defining the charset (we already called the
+ // FetchHeaders(), and there ProcessHeaders();
+ // in this case, the m_mimeCharset is set. See
+ // nsOutlookMail::ImportMailbox())
+ // 2. We may have the codepage walue provided by Outlook ("codepage" at the
+ // very beginning of this function)
+ // 3. We may have an HTML charset header.
+
+ bool bFoundCharset = false;
+
+ if (!m_mimeCharset.IsEmpty()) // The top-level header data
+ bFoundCharset = CheckBodyInCharsetRange(m_mimeCharset.get());
+ // No valid charset in the message header - try the HTML header.
+ // arguably may be useless
+ if (!bFoundCharset && m_bodyIsHtml) {
+ ExtractMetaCharset(m_body.get(), m_body.Length(), m_mimeCharset);
+ if (!m_mimeCharset.IsEmpty())
+ bFoundCharset = CheckBodyInCharsetRange(m_mimeCharset.get());
+ }
+ // Get from Outlook (seems like it keeps the MIME part header encoding info)
+ if (!bFoundCharset && codepage) {
+ const char* charset = CpToCharset(codepage);
+ if (charset) {
+ bFoundCharset = CheckBodyInCharsetRange(charset);
+ if (bFoundCharset) m_mimeCharset.Assign(charset);
+ }
+ }
+ if (!bFoundCharset) // Everything else failed, let's use the lossless
+ // utf-8...
+ m_mimeCharset.AssignLiteral("utf-8");
+
+ MAPI_DUMP_STRING(m_body.get());
+ MAPI_TRACE0("\r\n");
+
+ return true;
+}
+
+void CMapiMessage::GetBody(nsCString& dest) const {
+ nsMsgI18NConvertFromUnicode(m_mimeCharset, m_body, dest);
+}
+
+void CMapiMessage::FetchFlags(void) {
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_MESSAGE_FLAGS);
+ if (pVal) m_msgFlags = CMapiApi::GetLongFromProp(pVal);
+ pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_LAST_VERB_EXECUTED);
+ if (pVal) m_msgLastVerb = CMapiApi::GetLongFromProp(pVal);
+}
+
+enum { ieidPR_ATTACH_NUM = 0, ieidAttachMax };
+
+static const SizedSPropTagArray(ieidAttachMax, ptaEid) = {ieidAttachMax,
+ {PR_ATTACH_NUM}};
+
+bool CMapiMessage::IterateAttachTable(LPMAPITABLE lpTable) {
+ ULONG rowCount;
+ HRESULT hr = lpTable->GetRowCount(0, &rowCount);
+ if (!rowCount) {
+ return true;
+ }
+
+ hr = lpTable->SetColumns((LPSPropTagArray)&ptaEid, 0);
+ if (FAILED(hr)) {
+ MAPI_TRACE2("SetColumns for attachment table failed: 0x%lx, %d\r\n",
+ (long)hr, (int)hr);
+ return false;
+ }
+
+ hr = lpTable->SeekRow(BOOKMARK_BEGINNING, 0, NULL);
+ if (FAILED(hr)) {
+ MAPI_TRACE2("SeekRow for attachment table failed: 0x%lx, %d\r\n", (long)hr,
+ (int)hr);
+ return false;
+ }
+
+ int cNumRows = 0;
+ LPSRowSet lpRow;
+ bool bResult = true;
+ do {
+ lpRow = NULL;
+ hr = lpTable->QueryRows(1, 0, &lpRow);
+
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2("QueryRows for attachment table failed: 0x%lx, %d\n",
+ (long)hr, (int)hr);
+ bResult = false;
+ break;
+ }
+
+ if (lpRow) {
+ cNumRows = lpRow->cRows;
+
+ if (cNumRows) {
+ DWORD aNum = lpRow->aRow[0].lpProps[ieidPR_ATTACH_NUM].Value.ul;
+ AddAttachment(aNum);
+ MAPI_TRACE1("\t\t****Attachment found - #%d\r\n", (int)aNum);
+ }
+ CMapiApi::FreeProws(lpRow);
+ }
+
+ } while (SUCCEEDED(hr) && cNumRows && lpRow);
+
+ return bResult;
+}
+
+bool CMapiMessage::GetTmpFile(/*out*/ nsIFile** aResult) {
+ nsCOMPtr<nsIFile> tmpFile;
+ nsresult rv = GetSpecialDirectoryWithFileName(
+ NS_OS_TEMP_DIR, "mapiattach.tmp", getter_AddRefs(tmpFile));
+ if (NS_FAILED(rv)) return false;
+
+ rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ if (NS_FAILED(rv)) return false;
+
+ tmpFile.forget(aResult);
+ return true;
+}
+
+bool CMapiMessage::CopyMsgAttachToFile(LPATTACH lpAttach,
+ /*out*/ nsIFile** tmp_file) {
+ LPMESSAGE lpMsg;
+ HRESULT hr = lpAttach->OpenProperty(PR_ATTACH_DATA_OBJ, &IID_IMessage, 0, 0,
+ reinterpret_cast<LPUNKNOWN*>(&lpMsg));
+ if (HR_FAILED(hr)) return false;
+
+ if (!GetTmpFile(tmp_file)) return false;
+
+ nsCOMPtr<nsIOutputStream> destOutputStream;
+ nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(destOutputStream),
+ *tmp_file, -1, 0600);
+ if (NS_SUCCEEDED(rv))
+ rv = ImportMailboxRunnable::ImportMessage(lpMsg, destOutputStream,
+ nsIMsgSend::nsMsgSaveAsDraft);
+
+ if (NS_FAILED(rv)) {
+ (*tmp_file)->Remove(false);
+ (*tmp_file)->Release();
+ *tmp_file = 0;
+ }
+
+ return NS_SUCCEEDED(rv);
+}
+
+bool CMapiMessage::CopyBinAttachToFile(LPATTACH lpAttach, nsIFile** tmp_file) {
+ nsCOMPtr<nsIFile> _tmp_file;
+ nsresult rv = GetSpecialDirectoryWithFileName(
+ NS_OS_TEMP_DIR, "mapiattach.tmp", getter_AddRefs(_tmp_file));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = _tmp_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsString tmpPath = _tmp_file->NativePath();
+ // We have to use native charset unless we migrate to Outlook 2013 "W" API.
+ nsCString tmpNativePath;
+ rv = NS_CopyUnicodeToNative(tmpPath, tmpNativePath);
+ NS_ENSURE_SUCCESS(rv, false);
+ LPSTREAM lpStreamFile;
+ HRESULT hr = CMapiApi::OpenStreamOnFile(
+ gpMapiAllocateBuffer, gpMapiFreeBuffer, STGM_READWRITE | STGM_CREATE,
+ tmpNativePath.get(), NULL, &lpStreamFile);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE1("~~ERROR~~ OpenStreamOnFile failed - temp path: %s\r\n",
+ tmpNativePath.get());
+ return false;
+ }
+
+ bool bResult = true;
+ LPSTREAM lpAttachStream;
+ hr = lpAttach->OpenProperty(PR_ATTACH_DATA_BIN, &IID_IStream, 0, 0,
+ (LPUNKNOWN*)&lpAttachStream);
+
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE0("~~ERROR~~ OpenProperty failed for PR_ATTACH_DATA_BIN.\r\n");
+ lpAttachStream = NULL;
+ bResult = false;
+ } else {
+ STATSTG st;
+ hr = lpAttachStream->Stat(&st, STATFLAG_NONAME);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE0("~~ERROR~~ Stat failed for attachment stream\r\n");
+ bResult = false;
+ } else {
+ hr = lpAttachStream->CopyTo(lpStreamFile, st.cbSize, NULL, NULL);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE0("~~ERROR~~ Attach Stream CopyTo temp file failed.\r\n");
+ bResult = false;
+ }
+ }
+ }
+
+ if (lpAttachStream) lpAttachStream->Release();
+ lpStreamFile->Release();
+ if (!bResult)
+ _tmp_file->Remove(false);
+ else
+ _tmp_file.forget(tmp_file);
+
+ return bResult;
+}
+
+bool CMapiMessage::GetURL(nsIFile* aFile, nsIURI** url) {
+ if (!m_pIOService) return false;
+
+ nsresult rv = m_pIOService->NewFileURI(aFile, url);
+ return NS_SUCCEEDED(rv);
+}
+
+bool CMapiMessage::AddAttachment(DWORD aNum) {
+ LPATTACH lpAttach = NULL;
+ HRESULT hr = m_lpMsg->OpenAttach(aNum, NULL, 0, &lpAttach);
+ if (HR_FAILED(hr)) {
+ MAPI_TRACE2(
+ "\t\t****Attachment error, unable to open attachment: %d, 0x%lx\r\n",
+ idx, hr);
+ return false;
+ }
+
+ bool bResult = false;
+ attach_data* data = new attach_data;
+ ULONG aMethod;
+ if (data) {
+ bResult = true;
+
+ // 1. Get the file that contains the attachment data
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_METHOD);
+ if (pVal) {
+ aMethod = CMapiApi::GetLongFromProp(pVal);
+ switch (aMethod) {
+ case ATTACH_BY_VALUE:
+ MAPI_TRACE1("\t\t** Attachment #%d by value.\r\n", aNum);
+ bResult =
+ CopyBinAttachToFile(lpAttach, getter_AddRefs(data->tmp_file));
+ data->delete_file = true;
+ break;
+ case ATTACH_BY_REFERENCE:
+ case ATTACH_BY_REF_RESOLVE:
+ case ATTACH_BY_REF_ONLY:
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_PATHNAME_W);
+ if (pVal) {
+ nsCString path;
+ CMapiApi::GetStringFromProp(pVal, path);
+ nsresult rv;
+ data->tmp_file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || !data->tmp_file) {
+ MAPI_TRACE0("*** Error creating file spec for attachment\n");
+ bResult = false;
+ } else
+ data->tmp_file->InitWithNativePath(path);
+ }
+ MAPI_TRACE2("\t\t** Attachment #%d by ref: %s\r\n", aNum,
+ m_attachPath.get());
+ break;
+ case ATTACH_EMBEDDED_MSG:
+ MAPI_TRACE1("\t\t** Attachment #%d by Embedded Message??\r\n", aNum);
+ // Convert the embedded IMessage from PR_ATTACH_DATA_OBJ to rfc822
+ // attachment (see
+ // http://msdn.microsoft.com/en-us/library/cc842329.aspx) This is a
+ // recursive call.
+ bResult =
+ CopyMsgAttachToFile(lpAttach, getter_AddRefs(data->tmp_file));
+ data->delete_file = true;
+ break;
+ case ATTACH_OLE:
+ MAPI_TRACE1("\t\t** Attachment #%d by OLE - yuck!!!\r\n", aNum);
+ break;
+ default:
+ MAPI_TRACE2(
+ "\t\t** Attachment #%d unknown attachment method - 0x%lx\r\n",
+ aNum, aMethod);
+ bResult = false;
+ }
+ } else
+ bResult = false;
+
+ if (bResult) bResult = data->tmp_file;
+
+ if (bResult) {
+ bool isFile = false;
+ bool exists = false;
+ data->tmp_file->Exists(&exists);
+ data->tmp_file->IsFile(&isFile);
+
+ if (!exists || !isFile) {
+ bResult = false;
+ MAPI_TRACE0("Attachment file does not exist\n");
+ }
+ }
+
+ if (bResult)
+ bResult = GetURL(data->tmp_file, getter_AddRefs(data->orig_url));
+
+ if (bResult) {
+ // Now we have the file; proceed to the other properties
+
+ data->encoding = NS_xstrdup(ENCODING_BINARY);
+
+ nsString fname, fext;
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_LONG_FILENAME_W);
+ if (!pVal)
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_FILENAME_W);
+ CMapiApi::GetStringFromProp(pVal, fname);
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_EXTENSION_W);
+ CMapiApi::GetStringFromProp(pVal, fext);
+ MAPI_TRACE2("\t\t\t--- File name: %s, extension: %s\r\n", fname.get(),
+ fext.get());
+
+ if (fext.IsEmpty()) {
+ int idx = fname.RFindChar(L'.');
+ if (idx != -1) fext = Substring(fname, idx);
+ } else if (fname.RFindChar(L'.') == -1) {
+ fname += L".";
+ fname += fext;
+ }
+ if (fname.IsEmpty()) {
+ // If no description use "Attachment i" format.
+ fname = L"Attachment ";
+ fname.AppendInt(static_cast<uint32_t>(aNum));
+ }
+ data->real_name = ToNewUTF8String(fname);
+
+ nsCString tmp;
+ // We have converted it to the rfc822 document
+ if (aMethod == ATTACH_EMBEDDED_MSG) {
+ data->type = NS_xstrdup(MESSAGE_RFC822);
+ } else {
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_MIME_TAG_A);
+ CMapiApi::GetStringFromProp(pVal, tmp);
+ MAPI_TRACE1("\t\t\t--- Mime type: %s\r\n", tmp.get());
+ if (tmp.IsEmpty()) {
+ uint8_t* pType = NULL;
+ if (!fext.IsEmpty()) {
+ pType = CMimeTypes::GetMimeType(fext);
+ }
+ if (pType)
+ data->type = NS_xstrdup((PC_S8)pType);
+ else
+ data->type = NS_xstrdup(APPLICATION_OCTET_STREAM);
+ } else
+ data->type = ToNewCString(tmp);
+ }
+
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_CONTENT_ID_A);
+ CMapiApi::GetStringFromProp(pVal, tmp);
+ if (!tmp.IsEmpty()) data->cid = ToNewCString(tmp);
+ }
+ if (bResult) {
+ // Now we need to decide if this attachment is embedded or not.
+ // At first, I tried to simply check for the presence of the Content-Id.
+ // But it turned out that this method is unreliable, since there exist
+ // cases when an attachment has a Content-Id while isn't embedded (even in
+ // a message with a plain-text body!). So next I tried to look for <img>
+ // tags that contain the found Content-Id. But this is unreliable, too,
+ // because there exist cases where other places of HTML reference the
+ // embedded messages (e.g. it may be a background of a table cell, or some
+ // CSS; further, it is possible that the reference to an embedded object
+ // is not in the main body, but in another embedded object - like body
+ // references a CSS attachment that in turn references a picture as a
+ // background of its element). From the other hand, it's unreliable to
+ // relax the search criteria to any occurrence of the Content-Id string in
+ // the body - partly because the string may be simply in a text or other
+ // non-referencing part, partly because of the abovementioned possibility
+ // that the reference is outside the body at all. There exist the
+ // PR_ATTACH_FLAGS property of the attachment. The MS documentation tells
+ // about two possible flags in it: ATT_INVISIBLE_IN_HTML and
+ // ATT_INVISIBLE_IN_RTF. There is at least one more undocumented flag:
+ // ATT_MHTML_REF. Some sources in Internet suggest simply check for the
+ // latter flag to distinguish between the embedded and ordinary
+ // attachments. But my observations indicate that even if the flags don't
+ // include ATT_MHTML_REF, the attachment is still may be embedded.
+ // However, my observations always show that the message is embedded if
+ // the flags is not 0. So now I will simply test for the non-zero flags to
+ // decide whether the attachment is embedded or not. Possible advantage is
+ // reliability (I hope). Another advantage is that it's much faster than
+ // search the body for Content-Id.
+
+ DWORD flags = 0;
+
+ pVal = CMapiApi::GetMapiProperty(lpAttach, PR_ATTACH_FLAGS);
+ if (pVal) flags = CMapiApi::GetLongFromProp(pVal);
+ if (m_bodyIsHtml && data->cid &&
+ (flags != 0)) // this is the embedded attachment
+ m_embattachments.push_back(data);
+ else // this is ordinary attachment
+ m_stdattachments.push_back(data);
+ } else {
+ delete data;
+ }
+ }
+
+ lpAttach->Release();
+ return bResult;
+}
+
+void CMapiMessage::ClearAttachment(attach_data* data) {
+ if (data->delete_file && data->tmp_file) data->tmp_file->Remove(false);
+
+ if (data->type) free(data->type);
+ if (data->encoding) free(data->encoding);
+ if (data->real_name) free(data->real_name);
+ if (data->cid) free(data->cid);
+
+ delete data;
+}
+
+void CMapiMessage::ClearAttachments() {
+ std::for_each(m_stdattachments.begin(), m_stdattachments.end(),
+ ClearAttachment);
+ m_stdattachments.clear();
+ std::for_each(m_embattachments.begin(), m_embattachments.end(),
+ ClearAttachment);
+ m_embattachments.clear();
+}
+
+// This method must be called AFTER the retrieval of the body,
+// since the decision if an attachment is embedded or not is made
+// based on the body type and contents
+void CMapiMessage::ProcessAttachments() {
+ LPSPropValue pVal = CMapiApi::GetMapiProperty(m_lpMsg, PR_HASATTACH);
+ bool hasAttach = true;
+
+ if (pVal) {
+ if (PROP_TYPE(pVal->ulPropTag) == PT_BOOLEAN)
+ hasAttach = (pVal->Value.b != 0);
+ CMapiApi::MAPIFreeBuffer(pVal);
+ }
+
+ if (!hasAttach) return;
+
+ // Get the attachment table?
+ LPMAPITABLE pTable = NULL;
+ HRESULT hr = m_lpMsg->GetAttachmentTable(0, &pTable);
+ if (FAILED(hr) || !pTable) return;
+ IterateAttachTable(pTable);
+ pTable->Release();
+}
+
+nsresult CMapiMessage::GetAttachments(
+ nsTArray<RefPtr<nsIMsgAttachedFile>>& attachments) {
+ attachments.Clear();
+ attachments.SetCapacity(m_stdattachments.size());
+
+ for (std::vector<attach_data*>::const_iterator it = m_stdattachments.begin();
+ it != m_stdattachments.end(); it++) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAttachedFile> a(
+ do_CreateInstance("@mozilla.org/messengercompose/attachedfile;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ a->SetOrigUrl((*it)->orig_url);
+ a->SetTmpFile((*it)->tmp_file);
+ a->SetEncoding(nsDependentCString((*it)->encoding));
+ a->SetRealName(nsDependentCString((*it)->real_name));
+ a->SetType(nsDependentCString((*it)->type));
+ attachments.AppendElement(a);
+ }
+ return NS_OK;
+}
+
+bool CMapiMessage::GetEmbeddedAttachmentInfo(unsigned int i, nsIURI** uri,
+ const char** cid,
+ const char** name) const {
+ if (i >= m_embattachments.size()) return false;
+ attach_data* data = m_embattachments[i];
+ if (!data) return false;
+ *uri = data->orig_url;
+ *cid = data->cid;
+ *name = data->real_name;
+ return true;
+}
+
+//////////////////////////////////////////////////////
+
+// begin and end MUST point to the same string
+char* dup(const char* begin, const char* end) {
+ if (begin >= end) return 0;
+ char* str = new char[end - begin + 1];
+ memcpy(str, begin, (end - begin) * sizeof(begin[0]));
+ str[end - begin] = 0;
+ return str;
+}
+
+// See RFC822
+// 9 = '\t', 32 = ' '.
+inline bool IsPrintableASCII(char c) { return (c > ' ') && (c < 127); }
+inline bool IsWSP(char c) { return (c == ' ') || (c == '\t'); }
+
+CMapiMessageHeaders::CHeaderField::CHeaderField(const char* begin, int len)
+ : m_fname(0), m_fbody(0), m_fbody_utf8(false) {
+ const char *end = begin + len, *fname_end = begin;
+ while ((fname_end < end) && IsPrintableASCII(*fname_end) &&
+ (*fname_end != ':'))
+ ++fname_end;
+ if ((fname_end == end) || (*fname_end != ':')) return; // Not a valid header!
+ m_fname = dup(begin, fname_end + 1); // including colon
+ m_fbody = dup(fname_end + 1, end);
+}
+
+CMapiMessageHeaders::CHeaderField::CHeaderField(const char* name,
+ const char* body, bool utf8)
+ : m_fname(dup(name, name + strlen(name))),
+ m_fbody(dup(body, body + strlen(body))),
+ m_fbody_utf8(utf8) {}
+
+CMapiMessageHeaders::CHeaderField::~CHeaderField() {
+ delete[] m_fname;
+ delete[] m_fbody;
+}
+
+void CMapiMessageHeaders::CHeaderField::set_fbody(const char* txt) {
+ if (m_fbody == txt) return; // to avoid assigning to self
+ char* oldbody = m_fbody;
+ m_fbody = dup(txt, txt + strlen(txt));
+ delete[] oldbody;
+ m_fbody_utf8 = true;
+}
+
+void CMapiMessageHeaders::CHeaderField::GetUnfoldedString(
+ nsString& dest, const char* fallbackCharset) const {
+ dest.Truncate();
+ if (!m_fbody) return;
+
+ nsCString unfolded;
+ const char* pos = m_fbody;
+ while (*pos) {
+ if ((*pos == nsCRT::CR) && (*(pos + 1) == nsCRT::LF) && IsWSP(*(pos + 2)))
+ pos += 2; // Skip CRLF if it is followed by SPACE or TAB
+ else
+ unfolded.Append(*(pos++));
+ }
+ if (m_fbody_utf8)
+ CopyUTF8toUTF16(unfolded, dest);
+ else
+ nsMsgI18NConvertToUnicode(
+ fallbackCharset ? nsDependentCString(fallbackCharset) : EmptyCString(),
+ unfolded, dest);
+}
+
+////////////////////////////////////////
+
+const char* CMapiMessageHeaders::Specials[hdrMax] = {
+ "Date:", "From:", "Sender:", "Reply-To:",
+ "To:", "Cc:", "Bcc:", "Message-ID:",
+ "Subject:", "Mime-Version:", "Content-Type:", "Content-Transfer-Encoding:"};
+
+CMapiMessageHeaders::~CMapiMessageHeaders() { ClearHeaderFields(); }
+
+void CMapiMessageHeaders::Delete(CHeaderField* p) { delete p; }
+
+void CMapiMessageHeaders::ClearHeaderFields() {
+ std::for_each(m_headerFields.begin(), m_headerFields.end(), Delete);
+ m_headerFields.clear();
+}
+
+void CMapiMessageHeaders::Assign(const char* headers) {
+ for (int i = 0; i < hdrMax; i++) m_SpecialHeaders[i] = 0;
+ ClearHeaderFields();
+ if (!headers) return;
+
+ const char *start = headers, *end = headers;
+ while (*end) {
+ if ((*end == nsCRT::CR) && (*(end + 1) == nsCRT::LF)) {
+ if (!IsWSP(
+ *(end +
+ 2))) { // Not SPACE nor TAB (avoid FSP) -> next header or EOF
+ Add(new CHeaderField(start, end - start));
+ start = ++end + 1;
+ }
+ }
+ ++end;
+ }
+
+ if (start < end) { // Last header left
+ Add(new CHeaderField(start, end - start));
+ }
+}
+
+void CMapiMessageHeaders::Add(CHeaderField* f) {
+ if (!f) return;
+ if (!f->Valid()) {
+ delete f;
+ return;
+ }
+
+ SpecialHeader idx = CheckSpecialHeader(f->fname());
+ if (idx != hdrNone) {
+ // Now check if the special header was already inserted;
+ // if so, remove previous and add this new
+ CHeaderField* PrevSpecial = m_SpecialHeaders[idx];
+ if (PrevSpecial) {
+ std::vector<CHeaderField*>::iterator iter =
+ std::find(m_headerFields.begin(), m_headerFields.end(), PrevSpecial);
+ if (iter != m_headerFields.end()) m_headerFields.erase(iter);
+ delete PrevSpecial;
+ }
+ m_SpecialHeaders[idx] = f;
+ }
+ m_headerFields.push_back(f);
+}
+
+CMapiMessageHeaders::SpecialHeader CMapiMessageHeaders::CheckSpecialHeader(
+ const char* fname) {
+ for (int i = hdrFirst; i < hdrMax; i++)
+ if (stricmp(fname, Specials[i]) == 0) return static_cast<SpecialHeader>(i);
+
+ return hdrNone;
+}
+
+const CMapiMessageHeaders::CHeaderField* CMapiMessageHeaders::CFind(
+ const char* name) const {
+ SpecialHeader special = CheckSpecialHeader(name);
+ if ((special > hdrNone) && (special < hdrMax))
+ return m_SpecialHeaders[special]; // No need to search further, because it
+ // MUST be here
+
+ std::vector<CHeaderField*>::const_iterator iter = std::find_if(
+ m_headerFields.begin(), m_headerFields.end(), fname_equals(name));
+ if (iter == m_headerFields.end()) return 0;
+ return *iter;
+}
+
+const char* CMapiMessageHeaders::SpecialName(SpecialHeader special) {
+ if ((special <= hdrNone) || (special >= hdrMax)) return 0;
+ return Specials[special];
+}
+
+const char* CMapiMessageHeaders::Value(SpecialHeader special) const {
+ if ((special <= hdrNone) || (special >= hdrMax)) return 0;
+ return (m_SpecialHeaders[special]) ? m_SpecialHeaders[special]->fbody() : 0;
+}
+
+const char* CMapiMessageHeaders::Value(const char* name) const {
+ const CHeaderField* result = CFind(name);
+ return result ? result->fbody() : 0;
+}
+
+void CMapiMessageHeaders::UnfoldValue(const char* name, nsString& dest,
+ const char* fallbackCharset) const {
+ const CHeaderField* result = CFind(name);
+ if (result)
+ result->GetUnfoldedString(dest, fallbackCharset);
+ else
+ dest.Truncate();
+}
+
+void CMapiMessageHeaders::UnfoldValue(SpecialHeader special, nsString& dest,
+ const char* fallbackCharset) const {
+ if ((special <= hdrNone) || (special >= hdrMax) ||
+ (!m_SpecialHeaders[special]))
+ dest.Truncate();
+ else
+ m_SpecialHeaders[special]->GetUnfoldedString(dest, fallbackCharset);
+}
+
+int CMapiMessageHeaders::SetValue(const char* name, const char* value,
+ bool replace) {
+ if (!replace) {
+ CHeaderField* result = Find(name);
+ if (result) {
+ result->set_fbody(value);
+ return 0;
+ }
+ }
+ Add(new CHeaderField(name, value, true));
+ return 0; // No sensible result is returned; maybe do something senseful
+ // later
+}
+
+int CMapiMessageHeaders::SetValue(SpecialHeader special, const char* value) {
+ CHeaderField* result = m_SpecialHeaders[special];
+ if (result)
+ result->set_fbody(value);
+ else
+ Add(new CHeaderField(Specials[special], value, true));
+ return 0;
+}
+
+void CMapiMessageHeaders::write_to_stream::operator()(const CHeaderField* f) {
+ if (!f || NS_FAILED(m_rv)) return;
+
+ uint32_t written;
+ m_rv = m_pDst->Write(f->fname(), strlen(f->fname()), &written);
+ NS_ENSURE_SUCCESS_VOID(m_rv);
+ if (f->fbody()) {
+ m_rv = m_pDst->Write(f->fbody(), strlen(f->fbody()), &written);
+ NS_ENSURE_SUCCESS_VOID(m_rv);
+ }
+ m_rv = m_pDst->Write("\x0D\x0A", 2, &written);
+}
+
+nsresult CMapiMessageHeaders::ToStream(nsIOutputStream* pDst) const {
+ nsresult rv = std::for_each(m_headerFields.begin(), m_headerFields.end(),
+ write_to_stream(pDst));
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t written;
+ rv = pDst->Write("\x0D\x0A", 2, &written); // Separator line
+ }
+ return rv;
+}
diff --git a/comm/mailnews/import/src/MapiMessage.h b/comm/mailnews/import/src/MapiMessage.h
new file mode 100644
index 0000000000..c06a84c57a
--- /dev/null
+++ b/comm/mailnews/import/src/MapiMessage.h
@@ -0,0 +1,290 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef MapiMessage_h___
+#define MapiMessage_h___
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsIMsgSend.h"
+#include "MapiApi.h"
+
+#include <vector>
+
+#ifndef PR_LAST_VERB_EXECUTED
+# define PR_LAST_VERB_EXECUTED PROP_TAG(PT_LONG, 0x1081)
+#endif
+
+#define EXCHIVERB_REPLYTOSENDER (102)
+#define EXCHIVERB_REPLYTOALL (103)
+#define EXCHIVERB_FORWARD (104)
+
+#ifndef PR_ATTACH_CONTENT_ID
+# define PR_ATTACH_CONTENT_ID PROP_TAG(PT_TSTRING, 0x3712)
+#endif
+#ifndef PR_ATTACH_CONTENT_ID_W
+# define PR_ATTACH_CONTENT_ID_W PROP_TAG(PT_UNICODE, 0x3712)
+#endif
+#ifndef PR_ATTACH_CONTENT_ID_A
+# define PR_ATTACH_CONTENT_ID_A PROP_TAG(PT_STRING8, 0x3712)
+#endif
+
+#ifndef PR_ATTACH_FLAGS
+# define PR_ATTACH_FLAGS PROP_TAG(PT_LONG, 0x3714)
+#endif
+
+#ifndef ATT_INVISIBLE_IN_HTML
+# define ATT_INVISIBLE_IN_HTML (0x1)
+#endif
+#ifndef ATT_INVISIBLE_IN_RTF
+# define ATT_INVISIBLE_IN_RTF (0x2)
+#endif
+#ifndef ATT_MHTML_REF
+# define ATT_MHTML_REF (0x4)
+#endif
+
+//////////////////////////////////////////////////////////////////////////////
+
+class CMapiMessageHeaders {
+ public:
+ // Special headers that MUST appear at most once (see RFC822)
+ enum SpecialHeader {
+ hdrNone = -1,
+ hdrFirst = 0, // utility values
+ hdrDate = hdrFirst,
+ hdrFrom,
+ hdrSender,
+ hdrReplyTo,
+ hdrTo,
+ hdrCc,
+ hdrBcc,
+ hdrMessageID,
+ hdrSubject,
+ hdrMimeVersion,
+ hdrContentType,
+ hdrContentTransferEncoding,
+ hdrMax // utility value
+ };
+
+ explicit CMapiMessageHeaders(const char* headers = 0) { Assign(headers); }
+ ~CMapiMessageHeaders();
+ void Assign(const char* headers);
+
+ inline bool IsEmpty() const { return m_headerFields.empty(); }
+ // if no such header exists then 0 is returned, else the first value returned
+ const char* Value(const char* name) const;
+ // if no such header exists then 0 is returned
+ const char* Value(SpecialHeader special) const;
+
+ void UnfoldValue(const char* name, nsString& dest,
+ const char* fallbackCharset) const;
+ void UnfoldValue(SpecialHeader special, nsString& dest,
+ const char* fallbackCharset) const;
+
+ // value must be utf-8 or 7-bit; supposed that this function will be called
+ // when the charset of the value is known
+ // TODO: if replace is set, then all headers with this name will be removed
+ // and one with this value will be added, otherwise a new header is added
+ // (Unnecessary for now)
+ int SetValue(const char* name, const char* value, bool replace = true);
+ int SetValue(SpecialHeader special, const char* value);
+
+ static const char* SpecialName(SpecialHeader special);
+
+ nsresult ToStream(nsIOutputStream* pDst) const;
+
+ private:
+ class CHeaderField {
+ public:
+ CHeaderField(const char* begin, int len);
+ CHeaderField(const char* name, const char* body, bool utf8 = false);
+ ~CHeaderField();
+ inline bool Valid() const { return m_fname; }
+ inline const char* fname() const { return m_fname; }
+ inline const char* fbody() const { return m_fbody; }
+
+ // txt must be utf-8 or 7-bit; supposed that this function will be called
+ // when the charset of the txt is known
+ void set_fbody(const char* txt);
+
+ void GetUnfoldedString(nsString& dest, const char* fallbackCharset) const;
+
+ private:
+ char* m_fname;
+ char* m_fbody;
+ bool m_fbody_utf8;
+ }; // class HeaderField
+
+ class write_to_stream {
+ public:
+ explicit write_to_stream(nsIOutputStream* pDst)
+ : m_pDst(pDst), m_rv(NS_OK) {}
+ void operator()(const CHeaderField* f);
+ inline operator nsresult() const { return m_rv; }
+
+ private:
+ nsIOutputStream* m_pDst;
+ nsresult m_rv;
+ };
+
+ // Search helper
+ class fname_equals {
+ public:
+ explicit fname_equals(const char* search) : m_search(search) {}
+ inline bool operator()(const CHeaderField* f) const {
+ return stricmp(f->fname(), m_search) == 0;
+ }
+
+ private:
+ const char* m_search;
+ }; // class fname_equals
+
+ // The common array of special headers' names
+ static const char* Specials[hdrMax];
+
+ std::vector<CHeaderField*> m_headerFields;
+ CHeaderField* m_SpecialHeaders[hdrMax]; // Pointers into the m_headerFields
+
+ void ClearHeaderFields();
+ void Add(CHeaderField* f);
+ static void Delete(CHeaderField* p);
+ static SpecialHeader CheckSpecialHeader(const char* fname);
+ const CHeaderField* CFind(const char* name) const;
+ inline CHeaderField* Find(const char* name) {
+ return const_cast<CHeaderField*>(CFind(name));
+ }
+
+}; // class CMapiMessageHeaders
+
+//////////////////////////////////////////////////////
+
+class CMapiMessage {
+ public:
+ explicit CMapiMessage(LPMESSAGE lpMsg);
+ ~CMapiMessage();
+
+ // Attachments
+ // Ordinary (not embedded) attachments.
+ nsresult GetAttachments(nsTArray<RefPtr<nsIMsgAttachedFile>>& attachments);
+ // Embedded attachments
+ size_t EmbeddedAttachmentsCount() const { return m_embattachments.size(); }
+ bool GetEmbeddedAttachmentInfo(unsigned int i, nsIURI** uri, const char** cid,
+ const char** name) const;
+ // We don't check MSGFLAG_HASATTACH, since it returns true even if there are
+ // only embedded attachmentsin the message. TB only counts the ordinary
+ // attachments when shows the message status, so here we check only for the
+ // ordinary attachments.
+ inline bool HasAttach() const { return !m_stdattachments.empty(); }
+
+ // Retrieve info for message
+ inline bool BodyIsHtml(void) const { return m_bodyIsHtml; }
+ const char* GetFromLine(int& len) const {
+ if (m_fromLine.IsEmpty())
+ return NULL;
+ else {
+ len = m_fromLine.Length();
+ return m_fromLine.get();
+ }
+ }
+ inline CMapiMessageHeaders* GetHeaders() { return &m_headers; }
+ inline const wchar_t* GetBody(void) const { return m_body.get(); }
+ inline size_t GetBodyLen(void) const { return m_body.Length(); }
+ void GetBody(nsCString& dest) const;
+ inline const char* GetBodyCharset(void) const { return m_mimeCharset.get(); }
+ inline bool IsRead() const { return m_msgFlags & MSGFLAG_READ; }
+ inline bool IsReplied() const {
+ return (m_msgLastVerb == EXCHIVERB_REPLYTOSENDER) ||
+ (m_msgLastVerb == EXCHIVERB_REPLYTOALL);
+ }
+ inline bool IsForvarded() const { return m_msgLastVerb == EXCHIVERB_FORWARD; }
+
+ bool HasContentHeader(void) const { return !m_mimeContentType.IsEmpty(); }
+ bool HasMimeVersion(void) const {
+ return m_headers.Value(CMapiMessageHeaders::hdrMimeVersion);
+ }
+ const char* GetMimeContent(void) const { return m_mimeContentType.get(); }
+ int32_t GetMimeContentLen(void) const { return m_mimeContentType.Length(); }
+ const char* GetMimeBoundary(void) const { return m_mimeBoundary.get(); }
+
+ // The only required part of a message is its header
+ inline bool ValidState() const { return !m_headers.IsEmpty(); }
+ inline bool FullMessageDownloaded() const { return !m_dldStateHeadersOnly; }
+
+ private:
+ struct attach_data {
+ nsCOMPtr<nsIURI> orig_url;
+ nsCOMPtr<nsIFile> tmp_file;
+ char* type;
+ char* encoding;
+ char* real_name;
+ char* cid;
+ bool delete_file;
+ attach_data()
+ : type(0), encoding(0), real_name(0), cid(0), delete_file(false) {}
+ };
+
+ static const nsCString m_whitespace;
+
+ LPMESSAGE m_lpMsg;
+
+ bool m_dldStateHeadersOnly; // if the message has not been downloaded yet
+ CMapiMessageHeaders m_headers;
+ nsCString m_fromLine; // utf-8
+ nsCString m_mimeContentType; // utf-8
+ nsCString m_mimeBoundary; // utf-8
+ nsCString m_mimeCharset; // utf-8
+
+ std::vector<attach_data*> m_stdattachments;
+ std::vector<attach_data*> m_embattachments; // Embedded
+
+ nsString m_body; // to be converted from UTF-16 using m_mimeCharset
+ bool m_bodyIsHtml;
+
+ uint32_t m_msgFlags;
+ uint32_t m_msgLastVerb;
+
+ nsCOMPtr<nsIIOService> m_pIOService;
+
+ void GetDownloadState();
+
+ // Headers - fetch will get PR_TRANSPORT_MESSAGE_HEADERS
+ // or if they do not exist will build a header from
+ // PR_DISPLAY_TO, _CC, _BCC
+ // PR_SUBJECT
+ // PR_MESSAGE_RECIPIENTS
+ // and PR_CREATION_TIME if needed?
+ bool FetchHeaders(void);
+ bool FetchBody(void);
+ void FetchFlags(void);
+
+ static bool GetTmpFile(/*out*/ nsIFile** aResult);
+ static bool CopyMsgAttachToFile(LPATTACH lpAttach,
+ /*out*/ nsIFile** tmp_file);
+ static bool CopyBinAttachToFile(LPATTACH lpAttach, nsIFile** tmp_file);
+
+ static void ClearAttachment(attach_data* data);
+ void ClearAttachments();
+ bool AddAttachment(DWORD aNum);
+ bool IterateAttachTable(LPMAPITABLE tbl);
+ bool GetURL(nsIFile* aFile, nsIURI** url);
+ void ProcessAttachments();
+
+ bool EnsureHeader(CMapiMessageHeaders::SpecialHeader special, ULONG mapiTag);
+ bool EnsureDate();
+
+ void ProcessContentType();
+ bool CheckBodyInCharsetRange(const char* charset);
+ void FormatDateTime(SYSTEMTIME& tm, nsCString& s, bool includeTZ = true);
+ void BuildFromLine(void);
+
+ inline static bool IsSpace(char c) {
+ return c == ' ' || c == '\r' || c == '\n' || c == '\b' || c == '\t';
+ }
+ inline static bool IsSpace(wchar_t c) {
+ return ((c & 0xFF) == c) && IsSpace(static_cast<char>(c));
+ } // Avoid false detections
+};
+
+#endif /* MapiMessage_h__ */
diff --git a/comm/mailnews/import/src/MapiMimeTypes.cpp b/comm/mailnews/import/src/MapiMimeTypes.cpp
new file mode 100644
index 0000000000..554c5694d9
--- /dev/null
+++ b/comm/mailnews/import/src/MapiMimeTypes.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsString.h"
+#include "MapiMimeTypes.h"
+
+uint8_t CMimeTypes::m_mimeBuffer[kMaxMimeTypeSize];
+
+BOOL CMimeTypes::GetKey(HKEY root, LPCWSTR pName, PHKEY pKey) {
+ LONG result = RegOpenKeyExW(root, pName, 0,
+ KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, pKey);
+ return result == ERROR_SUCCESS;
+}
+
+BOOL CMimeTypes::GetValueBytes(HKEY rootKey, LPCWSTR pValName,
+ LPBYTE* ppBytes) {
+ LONG err;
+ DWORD bufSz;
+
+ *ppBytes = NULL;
+ // Get the installed directory
+ err = RegQueryValueExW(rootKey, pValName, NULL, NULL, NULL, &bufSz);
+ if (err == ERROR_SUCCESS) {
+ *ppBytes = new BYTE[bufSz];
+ err = RegQueryValueExW(rootKey, pValName, NULL, NULL, *ppBytes, &bufSz);
+ if (err == ERROR_SUCCESS) {
+ return TRUE;
+ }
+ delete *ppBytes;
+ *ppBytes = NULL;
+ }
+ return FALSE;
+}
+
+void CMimeTypes::ReleaseValueBytes(LPBYTE pBytes) {
+ if (pBytes) delete pBytes;
+}
+
+BOOL CMimeTypes::GetMimeTypeFromReg(const nsString& ext, LPBYTE* ppBytes) {
+ HKEY extensionKey;
+ BOOL result = FALSE;
+ *ppBytes = NULL;
+ if (GetKey(HKEY_CLASSES_ROOT, ext.get(), &extensionKey)) {
+ result = GetValueBytes(extensionKey, L"Content Type", ppBytes);
+ RegCloseKey(extensionKey);
+ }
+
+ return result;
+}
+
+uint8_t* CMimeTypes::GetMimeType(const nsString& theExt) {
+ nsString ext = theExt;
+ if (ext.Length()) {
+ if (ext.First() != '.') {
+ ext = L".";
+ ext += theExt;
+ }
+ }
+
+ BOOL result = FALSE;
+ int len;
+
+ if (!ext.Length()) return NULL;
+ LPBYTE pByte;
+ if (GetMimeTypeFromReg(ext, &pByte)) {
+ len = strlen((const char*)pByte);
+ if (len && (len < kMaxMimeTypeSize)) {
+ memcpy(m_mimeBuffer, pByte, len);
+ m_mimeBuffer[len] = 0;
+ result = TRUE;
+ }
+ ReleaseValueBytes(pByte);
+ }
+
+ if (result) return m_mimeBuffer;
+
+ return NULL;
+}
diff --git a/comm/mailnews/import/src/MapiMimeTypes.h b/comm/mailnews/import/src/MapiMimeTypes.h
new file mode 100644
index 0000000000..d870893559
--- /dev/null
+++ b/comm/mailnews/import/src/MapiMimeTypes.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef MapiMimeTypes_h___
+#define MapiMimeTypes_h___
+
+#include <windows.h>
+
+#define kMaxMimeTypeSize 256
+
+class CMimeTypes {
+ public:
+ static uint8_t* GetMimeType(const nsString& theExt);
+
+ protected:
+ // Registry stuff
+ static BOOL GetKey(HKEY root, LPCWSTR pName, PHKEY pKey);
+ static BOOL GetValueBytes(HKEY rootKey, LPCWSTR pValName, LPBYTE* ppBytes);
+ static void ReleaseValueBytes(LPBYTE pBytes);
+ static BOOL GetMimeTypeFromReg(const nsString& ext, LPBYTE* ppBytes);
+
+ static uint8_t m_mimeBuffer[kMaxMimeTypeSize];
+};
+
+#endif /* MapiMimeTypes_h__ */
diff --git a/comm/mailnews/import/src/MapiTagStrs.cpp b/comm/mailnews/import/src/MapiTagStrs.cpp
new file mode 100644
index 0000000000..43a796ae24
--- /dev/null
+++ b/comm/mailnews/import/src/MapiTagStrs.cpp
@@ -0,0 +1,1473 @@
+ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ */
+ /* 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/. */
+
+ /*
+ * Message envelope properties
+ */
+
+case PR_ACKNOWLEDGEMENT_MODE:
+ s = "PR_ACKNOWLEDGEMENT_MODE";
+ break;
+case PR_ALTERNATE_RECIPIENT_ALLOWED:
+ s = "PR_ALTERNATE_RECIPIENT_ALLOWED";
+ break;
+case PR_AUTHORIZING_USERS:
+ s = "PR_AUTHORIZING_USERS";
+ break;
+case PR_AUTO_FORWARD_COMMENT:
+ s = "PR_AUTO_FORWARD_COMMENT";
+ break;
+case PR_AUTO_FORWARDED:
+ s = "PR_AUTO_FORWARDED";
+ break;
+case PR_CONTENT_CONFIDENTIALITY_ALGORITHM_ID:
+ s = "PR_CONTENT_CONFIDENTIALITY_ALGORITHM_ID";
+ break;
+case PR_CONTENT_CORRELATOR:
+ s = "PR_CONTENT_CORRELATOR";
+ break;
+case PR_CONTENT_IDENTIFIER:
+ s = "PR_CONTENT_IDENTIFIER";
+ break;
+case PR_CONTENT_LENGTH:
+ s = "PR_CONTENT_LENGTH";
+ break;
+case PR_CONTENT_RETURN_REQUESTED:
+ s = "PR_CONTENT_RETURN_REQUESTED";
+ break;
+
+case PR_CONVERSATION_KEY:
+ s = "PR_CONVERSATION_KEY";
+ break;
+
+case PR_CONVERSION_EITS:
+ s = "PR_CONVERSION_EITS";
+ break;
+case PR_CONVERSION_WITH_LOSS_PROHIBITED:
+ s = "PR_CONVERSION_WITH_LOSS_PROHIBITED";
+ break;
+case PR_CONVERTED_EITS:
+ s = "PR_CONVERTED_EITS";
+ break;
+case PR_DEFERRED_DELIVERY_TIME:
+ s = "PR_DEFERRED_DELIVERY_TIME";
+ break;
+case PR_DELIVER_TIME:
+ s = "PR_DELIVER_TIME";
+ break;
+case PR_DISCARD_REASON:
+ s = "PR_DISCARD_REASON";
+ break;
+case PR_DISCLOSURE_OF_RECIPIENTS:
+ s = "PR_DISCLOSURE_OF_RECIPIENTS";
+ break;
+case PR_DL_EXPANSION_HISTORY:
+ s = "PR_DL_EXPANSION_HISTORY";
+ break;
+case PR_DL_EXPANSION_PROHIBITED:
+ s = "PR_DL_EXPANSION_PROHIBITED";
+ break;
+case PR_EXPIRY_TIME:
+ s = "PR_EXPIRY_TIME";
+ break;
+case PR_IMPLICIT_CONVERSION_PROHIBITED:
+ s = "PR_IMPLICIT_CONVERSION_PROHIBITED";
+ break;
+case PR_IMPORTANCE:
+ s = "PR_IMPORTANCE";
+ break;
+case PR_IPM_ID:
+ s = "PR_IPM_ID";
+ break;
+case PR_LATEST_DELIVERY_TIME:
+ s = "PR_LATEST_DELIVERY_TIME";
+ break;
+case PR_MESSAGE_CLASS:
+ s = "PR_MESSAGE_CLASS";
+ break;
+case PR_MESSAGE_DELIVERY_ID:
+ s = "PR_MESSAGE_DELIVERY_ID";
+ break;
+
+case PR_MESSAGE_SECURITY_LABEL:
+ s = "PR_MESSAGE_SECURITY_LABEL";
+ break;
+case PR_OBSOLETED_IPMS:
+ s = "PR_OBSOLETED_IPMS";
+ break;
+case PR_ORIGINALLY_INTENDED_RECIPIENT_NAME:
+ s = "PR_ORIGINALLY_INTENDED_RECIPIENT_NAME";
+ break;
+case PR_ORIGINAL_EITS:
+ s = "PR_ORIGINAL_EITS";
+ break;
+case PR_ORIGINATOR_CERTIFICATE:
+ s = "PR_ORIGINATOR_CERTIFICATE";
+ break;
+case PR_ORIGINATOR_DELIVERY_REPORT_REQUESTED:
+ s = "PR_ORIGINATOR_DELIVERY_REPORT_REQUESTED";
+ break;
+case PR_ORIGINATOR_RETURN_ADDRESS:
+ s = "PR_ORIGINATOR_RETURN_ADDRESS";
+ break;
+
+case PR_PARENT_KEY:
+ s = "PR_PARENT_KEY";
+ break;
+case PR_PRIORITY:
+ s = "PR_PRIORITY";
+ break;
+
+case PR_ORIGIN_CHECK:
+ s = "PR_ORIGIN_CHECK";
+ break;
+case PR_PROOF_OF_SUBMISSION_REQUESTED:
+ s = "PR_PROOF_OF_SUBMISSION_REQUESTED";
+ break;
+case PR_READ_RECEIPT_REQUESTED:
+ s = "PR_READ_RECEIPT_REQUESTED";
+ break;
+case PR_RECEIPT_TIME:
+ s = "PR_RECEIPT_TIME";
+ break;
+case PR_RECIPIENT_REASSIGNMENT_PROHIBITED:
+ s = "PR_RECIPIENT_REASSIGNMENT_PROHIBITED";
+ break;
+case PR_REDIRECTION_HISTORY:
+ s = "PR_REDIRECTION_HISTORY";
+ break;
+case PR_RELATED_IPMS:
+ s = "PR_RELATED_IPMS";
+ break;
+case PR_ORIGINAL_SENSITIVITY:
+ s = "PR_ORIGINAL_SENSITIVITY";
+ break;
+case PR_LANGUAGES:
+ s = "PR_LANGUAGES";
+ break;
+case PR_REPLY_TIME:
+ s = "PR_REPLY_TIME";
+ break;
+case PR_REPORT_TAG:
+ s = "PR_REPORT_TAG";
+ break;
+case PR_REPORT_TIME:
+ s = "PR_REPORT_TIME";
+ break;
+case PR_RETURNED_IPM:
+ s = "PR_RETURNED_IPM";
+ break;
+case PR_SECURITY:
+ s = "PR_SECURITY";
+ break;
+case PR_INCOMPLETE_COPY:
+ s = "PR_INCOMPLETE_COPY";
+ break;
+case PR_SENSITIVITY:
+ s = "PR_SENSITIVITY";
+ break;
+case PR_SUBJECT:
+ s = "PR_SUBJECT";
+ break;
+case PR_SUBJECT_IPM:
+ s = "PR_SUBJECT_IPM";
+ break;
+case PR_CLIENT_SUBMIT_TIME:
+ s = "PR_CLIENT_SUBMIT_TIME";
+ break;
+case PR_REPORT_NAME:
+ s = "PR_REPORT_NAME";
+ break;
+case PR_SENT_REPRESENTING_SEARCH_KEY:
+ s = "PR_SENT_REPRESENTING_SEARCH_KEY";
+ break;
+case PR_X400_CONTENT_TYPE:
+ s = "PR_X400_CONTENT_TYPE";
+ break;
+case PR_SUBJECT_PREFIX:
+ s = "PR_SUBJECT_PREFIX";
+ break;
+case PR_NON_RECEIPT_REASON:
+ s = "PR_NON_RECEIPT_REASON";
+ break;
+case PR_RECEIVED_BY_ENTRYID:
+ s = "PR_RECEIVED_BY_ENTRYID";
+ break;
+case PR_RECEIVED_BY_NAME:
+ s = "PR_RECEIVED_BY_NAME";
+ break;
+case PR_SENT_REPRESENTING_ENTRYID:
+ s = "PR_SENT_REPRESENTING_ENTRYID";
+ break;
+case PR_SENT_REPRESENTING_NAME:
+ s = "PR_SENT_REPRESENTING_NAME";
+ break;
+case PR_RCVD_REPRESENTING_ENTRYID:
+ s = "PR_RCVD_REPRESENTING_ENTRYID";
+ break;
+case PR_RCVD_REPRESENTING_NAME:
+ s = "PR_RCVD_REPRESENTING_NAME";
+ break;
+case PR_REPORT_ENTRYID:
+ s = "PR_REPORT_ENTRYID";
+ break;
+case PR_READ_RECEIPT_ENTRYID:
+ s = "PR_READ_RECEIPT_ENTRYID";
+ break;
+case PR_MESSAGE_SUBMISSION_ID:
+ s = "PR_MESSAGE_SUBMISSION_ID";
+ break;
+case PR_PROVIDER_SUBMIT_TIME:
+ s = "PR_PROVIDER_SUBMIT_TIME";
+ break;
+case PR_ORIGINAL_SUBJECT:
+ s = "PR_ORIGINAL_SUBJECT";
+ break;
+case PR_DISC_VAL:
+ s = "PR_DISC_VAL";
+ break;
+case PR_ORIG_MESSAGE_CLASS:
+ s = "PR_ORIG_MESSAGE_CLASS";
+ break;
+case PR_ORIGINAL_AUTHOR_ENTRYID:
+ s = "PR_ORIGINAL_AUTHOR_ENTRYID";
+ break;
+case PR_ORIGINAL_AUTHOR_NAME:
+ s = "PR_ORIGINAL_AUTHOR_NAME";
+ break;
+case PR_ORIGINAL_SUBMIT_TIME:
+ s = "PR_ORIGINAL_SUBMIT_TIME";
+ break;
+case PR_REPLY_RECIPIENT_ENTRIES:
+ s = "PR_REPLY_RECIPIENT_ENTRIES";
+ break;
+case PR_REPLY_RECIPIENT_NAMES:
+ s = "PR_REPLY_RECIPIENT_NAMES";
+ break;
+
+case PR_RECEIVED_BY_SEARCH_KEY:
+ s = "PR_RECEIVED_BY_SEARCH_KEY";
+ break;
+case PR_RCVD_REPRESENTING_SEARCH_KEY:
+ s = "PR_RCVD_REPRESENTING_SEARCH_KEY";
+ break;
+case PR_READ_RECEIPT_SEARCH_KEY:
+ s = "PR_READ_RECEIPT_SEARCH_KEY";
+ break;
+case PR_REPORT_SEARCH_KEY:
+ s = "PR_REPORT_SEARCH_KEY";
+ break;
+case PR_ORIGINAL_DELIVERY_TIME:
+ s = "PR_ORIGINAL_DELIVERY_TIME";
+ break;
+case PR_ORIGINAL_AUTHOR_SEARCH_KEY:
+ s = "PR_ORIGINAL_AUTHOR_SEARCH_KEY";
+ break;
+
+case PR_MESSAGE_TO_ME:
+ s = "PR_MESSAGE_TO_ME";
+ break;
+case PR_MESSAGE_CC_ME:
+ s = "PR_MESSAGE_CC_ME";
+ break;
+case PR_MESSAGE_RECIP_ME:
+ s = "PR_MESSAGE_RECIP_ME";
+ break;
+
+case PR_ORIGINAL_SENDER_NAME:
+ s = "PR_ORIGINAL_SENDER_NAME";
+ break;
+case PR_ORIGINAL_SENDER_ENTRYID:
+ s = "PR_ORIGINAL_SENDER_ENTRYID";
+ break;
+case PR_ORIGINAL_SENDER_SEARCH_KEY:
+ s = "PR_ORIGINAL_SENDER_SEARCH_KEY";
+ break;
+case PR_ORIGINAL_SENT_REPRESENTING_NAME:
+ s = "PR_ORIGINAL_SENT_REPRESENTING_NAME";
+ break;
+case PR_ORIGINAL_SENT_REPRESENTING_ENTRYID:
+ s = "PR_ORIGINAL_SENT_REPRESENTING_ENTRYID";
+ break;
+case PR_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY:
+ s = "PR_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY";
+ break;
+
+case PR_START_DATE:
+ s = "PR_START_DATE";
+ break;
+case PR_END_DATE:
+ s = "PR_END_DATE";
+ break;
+case PR_OWNER_APPT_ID:
+ s = "PR_OWNER_APPT_ID";
+ break;
+case PR_RESPONSE_REQUESTED:
+ s = "PR_RESPONSE_REQUESTED";
+ break;
+
+case PR_SENT_REPRESENTING_ADDRTYPE:
+ s = "PR_SENT_REPRESENTING_ADDRTYPE";
+ break;
+case PR_SENT_REPRESENTING_EMAIL_ADDRESS:
+ s = "PR_SENT_REPRESENTING_EMAIL_ADDRESS";
+ break;
+
+case PR_ORIGINAL_SENDER_ADDRTYPE:
+ s = "PR_ORIGINAL_SENDER_ADDRTYPE";
+ break;
+case PR_ORIGINAL_SENDER_EMAIL_ADDRESS:
+ s = "PR_ORIGINAL_SENDER_EMAIL_ADDRESS";
+ break;
+
+case PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE:
+ s = "PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE";
+ break;
+case PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS:
+ s = "PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS";
+ break;
+
+case PR_CONVERSATION_TOPIC:
+ s = "PR_CONVERSATION_TOPIC";
+ break;
+case PR_CONVERSATION_INDEX:
+ s = "PR_CONVERSATION_INDEX";
+ break;
+
+case PR_ORIGINAL_DISPLAY_BCC:
+ s = "PR_ORIGINAL_DISPLAY_BCC";
+ break;
+case PR_ORIGINAL_DISPLAY_CC:
+ s = "PR_ORIGINAL_DISPLAY_CC";
+ break;
+case PR_ORIGINAL_DISPLAY_TO:
+ s = "PR_ORIGINAL_DISPLAY_TO";
+ break;
+
+case PR_RECEIVED_BY_ADDRTYPE:
+ s = "PR_RECEIVED_BY_ADDRTYPE";
+ break;
+case PR_RECEIVED_BY_EMAIL_ADDRESS:
+ s = "PR_RECEIVED_BY_EMAIL_ADDRESS";
+ break;
+
+case PR_RCVD_REPRESENTING_ADDRTYPE:
+ s = "PR_RCVD_REPRESENTING_ADDRTYPE";
+ break;
+case PR_RCVD_REPRESENTING_EMAIL_ADDRESS:
+ s = "PR_RCVD_REPRESENTING_EMAIL_ADDRESS";
+ break;
+
+case PR_ORIGINAL_AUTHOR_ADDRTYPE:
+ s = "PR_ORIGINAL_AUTHOR_ADDRTYPE";
+ break;
+case PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS:
+ s = "PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS";
+ break;
+
+case PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE:
+ s = "PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE";
+ break;
+case PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS:
+ s = "PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS";
+ break;
+
+case PR_TRANSPORT_MESSAGE_HEADERS:
+ s = "PR_TRANSPORT_MESSAGE_HEADERS";
+ break;
+
+case PR_DELEGATION:
+ s = "PR_DELEGATION";
+ break;
+
+case PR_TNEF_CORRELATION_KEY:
+ s = "PR_TNEF_CORRELATION_KEY";
+ break;
+
+ /*
+ * Message content properties
+ */
+
+case PR_BODY:
+ s = "PR_BODY";
+ break;
+case PR_REPORT_TEXT:
+ s = "PR_REPORT_TEXT";
+ break;
+case PR_ORIGINATOR_AND_DL_EXPANSION_HISTORY:
+ s = "PR_ORIGINATOR_AND_DL_EXPANSION_HISTORY";
+ break;
+case PR_REPORTING_DL_NAME:
+ s = "PR_REPORTING_DL_NAME";
+ break;
+case PR_REPORTING_MTA_CERTIFICATE:
+ s = "PR_REPORTING_MTA_CERTIFICATE";
+ break;
+
+ /* Removed PR_REPORT_ORIGIN_AUTHENTICATION_CHECK with DCR 3865, use
+ * PR_ORIGIN_CHECK */
+
+case PR_RTF_SYNC_BODY_CRC:
+ s = "PR_RTF_SYNC_BODY_CRC";
+ break;
+case PR_RTF_SYNC_BODY_COUNT:
+ s = "PR_RTF_SYNC_BODY_COUNT";
+ break;
+case PR_RTF_SYNC_BODY_TAG:
+ s = "PR_RTF_SYNC_BODY_TAG";
+ break;
+case PR_RTF_COMPRESSED:
+ s = "PR_RTF_COMPRESSED";
+ break;
+case PR_RTF_SYNC_PREFIX_COUNT:
+ s = "PR_RTF_SYNC_PREFIX_COUNT";
+ break;
+case PR_RTF_SYNC_TRAILING_COUNT:
+ s = "PR_RTF_SYNC_TRAILING_COUNT";
+ break;
+case PR_ORIGINALLY_INTENDED_RECIP_ENTRYID:
+ s = "PR_ORIGINALLY_INTENDED_RECIP_ENTRYID";
+ break;
+
+ /*
+ * Reserved 0x1100-0x1200
+ */
+
+ /*
+ * Message recipient properties
+ */
+
+case PR_CONTENT_INTEGRITY_CHECK:
+ s = "PR_CONTENT_INTEGRITY_CHECK";
+ break;
+case PR_EXPLICIT_CONVERSION:
+ s = "PR_EXPLICIT_CONVERSION";
+ break;
+case PR_IPM_RETURN_REQUESTED:
+ s = "PR_IPM_RETURN_REQUESTED";
+ break;
+case PR_MESSAGE_TOKEN:
+ s = "PR_MESSAGE_TOKEN";
+ break;
+case PR_NDR_REASON_CODE:
+ s = "PR_NDR_REASON_CODE";
+ break;
+case PR_NDR_DIAG_CODE:
+ s = "PR_NDR_DIAG_CODE";
+ break;
+case PR_NON_RECEIPT_NOTIFICATION_REQUESTED:
+ s = "PR_NON_RECEIPT_NOTIFICATION_REQUESTED";
+ break;
+case PR_DELIVERY_POINT:
+ s = "PR_DELIVERY_POINT";
+ break;
+
+case PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED:
+ s = "PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED";
+ break;
+case PR_ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT:
+ s = "PR_ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT";
+ break;
+case PR_PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY:
+ s = "PR_PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY";
+ break;
+case PR_PHYSICAL_DELIVERY_MODE:
+ s = "PR_PHYSICAL_DELIVERY_MODE";
+ break;
+case PR_PHYSICAL_DELIVERY_REPORT_REQUEST:
+ s = "PR_PHYSICAL_DELIVERY_REPORT_REQUEST";
+ break;
+case PR_PHYSICAL_FORWARDING_ADDRESS:
+ s = "PR_PHYSICAL_FORWARDING_ADDRESS";
+ break;
+case PR_PHYSICAL_FORWARDING_ADDRESS_REQUESTED:
+ s = "PR_PHYSICAL_FORWARDING_ADDRESS_REQUESTED";
+ break;
+case PR_PHYSICAL_FORWARDING_PROHIBITED:
+ s = "PR_PHYSICAL_FORWARDING_PROHIBITED";
+ break;
+case PR_PHYSICAL_RENDITION_ATTRIBUTES:
+ s = "PR_PHYSICAL_RENDITION_ATTRIBUTES";
+ break;
+case PR_PROOF_OF_DELIVERY:
+ s = "PR_PROOF_OF_DELIVERY";
+ break;
+case PR_PROOF_OF_DELIVERY_REQUESTED:
+ s = "PR_PROOF_OF_DELIVERY_REQUESTED";
+ break;
+case PR_RECIPIENT_CERTIFICATE:
+ s = "PR_RECIPIENT_CERTIFICATE";
+ break;
+case PR_RECIPIENT_NUMBER_FOR_ADVICE:
+ s = "PR_RECIPIENT_NUMBER_FOR_ADVICE";
+ break;
+case PR_RECIPIENT_TYPE:
+ s = "PR_RECIPIENT_TYPE";
+ break;
+case PR_REGISTERED_MAIL_TYPE:
+ s = "PR_REGISTERED_MAIL_TYPE";
+ break;
+case PR_REPLY_REQUESTED:
+ s = "PR_REPLY_REQUESTED";
+ break;
+case PR_REQUESTED_DELIVERY_METHOD:
+ s = "PR_REQUESTED_DELIVERY_METHOD";
+ break;
+case PR_SENDER_ENTRYID:
+ s = "PR_SENDER_ENTRYID";
+ break;
+case PR_SENDER_NAME:
+ s = "PR_SENDER_NAME";
+ break;
+case PR_SUPPLEMENTARY_INFO:
+ s = "PR_SUPPLEMENTARY_INFO";
+ break;
+case PR_TYPE_OF_MTS_USER:
+ s = "PR_TYPE_OF_MTS_USER";
+ break;
+case PR_SENDER_SEARCH_KEY:
+ s = "PR_SENDER_SEARCH_KEY";
+ break;
+case PR_SENDER_ADDRTYPE:
+ s = "PR_SENDER_ADDRTYPE";
+ break;
+case PR_SENDER_EMAIL_ADDRESS:
+ s = "PR_SENDER_EMAIL_ADDRESS";
+ break;
+
+ /*
+ * Message non-transmittable properties
+ */
+
+ /*
+ * The two tags, PR_MESSAGE_RECIPIENTS and PR_MESSAGE_ATTACHMENTS,
+ * are to be used in the exclude list passed to
+ * IMessage::CopyTo when the caller wants either the recipients or attachments
+ * of the message to not get copied. It is also used in the ProblemArray
+ * return from IMessage::CopyTo when an error is encountered copying them
+ */
+
+case PR_CURRENT_VERSION:
+ s = "PR_CURRENT_VERSION";
+ break;
+case PR_DELETE_AFTER_SUBMIT:
+ s = "PR_DELETE_AFTER_SUBMIT";
+ break;
+case PR_DISPLAY_BCC:
+ s = "PR_DISPLAY_BCC";
+ break;
+case PR_DISPLAY_CC:
+ s = "PR_DISPLAY_CC";
+ break;
+case PR_DISPLAY_TO:
+ s = "PR_DISPLAY_TO";
+ break;
+case PR_PARENT_DISPLAY:
+ s = "PR_PARENT_DISPLAY";
+ break;
+case PR_MESSAGE_DELIVERY_TIME:
+ s = "PR_MESSAGE_DELIVERY_TIME";
+ break;
+case PR_MESSAGE_FLAGS:
+ s = "PR_MESSAGE_FLAGS";
+ break;
+case PR_MESSAGE_SIZE:
+ s = "PR_MESSAGE_SIZE";
+ break;
+case PR_PARENT_ENTRYID:
+ s = "PR_PARENT_ENTRYID";
+ break;
+case PR_SENTMAIL_ENTRYID:
+ s = "PR_SENTMAIL_ENTRYID";
+ break;
+case PR_CORRELATE:
+ s = "PR_CORRELATE";
+ break;
+case PR_CORRELATE_MTSID:
+ s = "PR_CORRELATE_MTSID";
+ break;
+case PR_DISCRETE_VALUES:
+ s = "PR_DISCRETE_VALUES";
+ break;
+case PR_RESPONSIBILITY:
+ s = "PR_RESPONSIBILITY";
+ break;
+case PR_SPOOLER_STATUS:
+ s = "PR_SPOOLER_STATUS";
+ break;
+case PR_TRANSPORT_STATUS:
+ s = "PR_TRANSPORT_STATUS";
+ break;
+case PR_MESSAGE_RECIPIENTS:
+ s = "PR_MESSAGE_RECIPIENTS";
+ break;
+case PR_MESSAGE_ATTACHMENTS:
+ s = "PR_MESSAGE_ATTACHMENTS";
+ break;
+case PR_SUBMIT_FLAGS:
+ s = "PR_SUBMIT_FLAGS";
+ break;
+case PR_RECIPIENT_STATUS:
+ s = "PR_RECIPIENT_STATUS";
+ break;
+case PR_TRANSPORT_KEY:
+ s = "PR_TRANSPORT_KEY";
+ break;
+case PR_MSG_STATUS:
+ s = "PR_MSG_STATUS";
+ break;
+case PR_MESSAGE_DOWNLOAD_TIME:
+ s = "PR_MESSAGE_DOWNLOAD_TIME";
+ break;
+case PR_CREATION_VERSION:
+ s = "PR_CREATION_VERSION";
+ break;
+case PR_MODIFY_VERSION:
+ s = "PR_MODIFY_VERSION";
+ break;
+case PR_HASATTACH:
+ s = "PR_HASATTACH";
+ break;
+case PR_BODY_CRC:
+ s = "PR_BODY_CRC";
+ break;
+case PR_NORMALIZED_SUBJECT:
+ s = "PR_NORMALIZED_SUBJECT";
+ break;
+case PR_RTF_IN_SYNC:
+ s = "PR_RTF_IN_SYNC";
+ break;
+case PR_ATTACH_SIZE:
+ s = "PR_ATTACH_SIZE";
+ break;
+case PR_ATTACH_NUM:
+ s = "PR_ATTACH_NUM";
+ break;
+case PR_PREPROCESS:
+ s = "PR_PREPROCESS";
+ break;
+
+ /* PR_ORIGINAL_DISPLAY_TO, _CC, and _BCC moved to transmittible range 03/09/95
+ */
+
+case PR_ORIGINATING_MTA_CERTIFICATE:
+ s = "PR_ORIGINATING_MTA_CERTIFICATE";
+ break;
+case PR_PROOF_OF_SUBMISSION:
+ s = "PR_PROOF_OF_SUBMISSION";
+ break;
+
+ /*
+ * The range of non-message and non-recipient property IDs (0x3000 - 0x3FFF)
+ * is further broken down into ranges to make assigning new property IDs
+ * easier.
+ *
+ * From To Kind of property
+ * --------------------------------
+ * 3000 32FF MAPI_defined common property
+ * 3200 33FF MAPI_defined form property
+ * 3400 35FF MAPI_defined message store property
+ * 3600 36FF MAPI_defined Folder or AB Container property
+ * 3700 38FF MAPI_defined attachment property
+ * 3900 39FF MAPI_defined address book property
+ * 3A00 3BFF MAPI_defined mailuser property
+ * 3C00 3CFF MAPI_defined DistList property
+ * 3D00 3DFF MAPI_defined Profile Section property
+ * 3E00 3EFF MAPI_defined Status property
+ * 3F00 3FFF MAPI_defined display table property
+ */
+
+ /*
+ * Properties common to numerous MAPI objects.
+ *
+ * Those properties that can appear on messages are in the
+ * non-transmittable range for messages. They start at the high
+ * end of that range and work down.
+ *
+ * Properties that never appear on messages are defined in the common
+ * property range (see above).
+ */
+
+ /*
+ * properties that are common to multiple objects (including message objects)
+ * -- these ids are in the non-transmittable range
+ */
+
+case PR_ENTRYID:
+ s = "PR_ENTRYID";
+ break;
+case PR_OBJECT_TYPE:
+ s = "PR_OBJECT_TYPE";
+ break;
+case PR_ICON:
+ s = "PR_ICON";
+ break;
+case PR_MINI_ICON:
+ s = "PR_MINI_ICON";
+ break;
+case PR_STORE_ENTRYID:
+ s = "PR_STORE_ENTRYID";
+ break;
+case PR_STORE_RECORD_KEY:
+ s = "PR_STORE_RECORD_KEY";
+ break;
+case PR_RECORD_KEY:
+ s = "PR_RECORD_KEY";
+ break;
+case PR_MAPPING_SIGNATURE:
+ s = "PR_MAPPING_SIGNATURE";
+ break;
+case PR_ACCESS_LEVEL:
+ s = "PR_ACCESS_LEVEL";
+ break;
+case PR_INSTANCE_KEY:
+ s = "PR_INSTANCE_KEY";
+ break;
+case PR_ROW_TYPE:
+ s = "PR_ROW_TYPE";
+ break;
+case PR_ACCESS:
+ s = "PR_ACCESS";
+ break;
+
+ /*
+ * properties that are common to multiple objects (usually not including
+ * message objects)
+ * -- these ids are in the transmittable range
+ */
+
+case PR_ROWID:
+ s = "PR_ROWID";
+ break;
+case PR_DISPLAY_NAME:
+ s = "PR_DISPLAY_NAME";
+ break;
+case PR_ADDRTYPE:
+ s = "PR_ADDRTYPE";
+ break;
+case PR_EMAIL_ADDRESS:
+ s = "PR_EMAIL_ADDRESS";
+ break;
+case PR_COMMENT:
+ s = "PR_COMMENT";
+ break;
+case PR_DEPTH:
+ s = "PR_DEPTH";
+ break;
+case PR_PROVIDER_DISPLAY:
+ s = "PR_PROVIDER_DISPLAY";
+ break;
+case PR_CREATION_TIME:
+ s = "PR_CREATION_TIME";
+ break;
+case PR_LAST_MODIFICATION_TIME:
+ s = "PR_LAST_MODIFICATION_TIME";
+ break;
+case PR_RESOURCE_FLAGS:
+ s = "PR_RESOURCE_FLAGS";
+ break;
+case PR_PROVIDER_DLL_NAME:
+ s = "PR_PROVIDER_DLL_NAME";
+ break;
+case PR_SEARCH_KEY:
+ s = "PR_SEARCH_KEY";
+ break;
+case PR_PROVIDER_UID:
+ s = "PR_PROVIDER_UID";
+ break;
+case PR_PROVIDER_ORDINAL:
+ s = "PR_PROVIDER_ORDINAL";
+ break;
+
+/*
+ * MAPI Form properties
+ */
+case PR_FORM_VERSION:
+ s = "PR_FORM_VERSION";
+ break;
+case PR_FORM_CLSID:
+ s = "PR_FORM_CLSID";
+ break;
+case PR_FORM_CONTACT_NAME:
+ s = "PR_FORM_CONTACT_NAME";
+ break;
+case PR_FORM_CATEGORY:
+ s = "PR_FORM_CATEGORY";
+ break;
+case PR_FORM_CATEGORY_SUB:
+ s = "PR_FORM_CATEGORY_SUB";
+ break;
+case PR_FORM_HOST_MAP:
+ s = "PR_FORM_HOST_MAP";
+ break;
+case PR_FORM_HIDDEN:
+ s = "PR_FORM_HIDDEN";
+ break;
+case PR_FORM_DESIGNER_NAME:
+ s = "PR_FORM_DESIGNER_NAME";
+ break;
+case PR_FORM_DESIGNER_GUID:
+ s = "PR_FORM_DESIGNER_GUID";
+ break;
+case PR_FORM_MESSAGE_BEHAVIOR:
+ s = "PR_FORM_MESSAGE_BEHAVIOR";
+ break;
+
+ /*
+ * Message store properties
+ */
+
+case PR_DEFAULT_STORE:
+ s = "PR_DEFAULT_STORE";
+ break;
+case PR_STORE_SUPPORT_MASK:
+ s = "PR_STORE_SUPPORT_MASK";
+ break;
+case PR_STORE_STATE:
+ s = "PR_STORE_STATE";
+ break;
+
+case PR_IPM_SUBTREE_SEARCH_KEY:
+ s = "PR_IPM_SUBTREE_SEARCH_KEY";
+ break;
+case PR_IPM_OUTBOX_SEARCH_KEY:
+ s = "PR_IPM_OUTBOX_SEARCH_KEY";
+ break;
+case PR_IPM_WASTEBASKET_SEARCH_KEY:
+ s = "PR_IPM_WASTEBASKET_SEARCH_KEY";
+ break;
+case PR_IPM_SENTMAIL_SEARCH_KEY:
+ s = "PR_IPM_SENTMAIL_SEARCH_KEY";
+ break;
+case PR_MDB_PROVIDER:
+ s = "PR_MDB_PROVIDER";
+ break;
+case PR_RECEIVE_FOLDER_SETTINGS:
+ s = "PR_RECEIVE_FOLDER_SETTINGS";
+ break;
+
+case PR_VALID_FOLDER_MASK:
+ s = "PR_VALID_FOLDER_MASK";
+ break;
+case PR_IPM_SUBTREE_ENTRYID:
+ s = "PR_IPM_SUBTREE_ENTRYID";
+ break;
+
+case PR_IPM_OUTBOX_ENTRYID:
+ s = "PR_IPM_OUTBOX_ENTRYID";
+ break;
+case PR_IPM_WASTEBASKET_ENTRYID:
+ s = "PR_IPM_WASTEBASKET_ENTRYID";
+ break;
+case PR_IPM_SENTMAIL_ENTRYID:
+ s = "PR_IPM_SENTMAIL_ENTRYID";
+ break;
+case PR_VIEWS_ENTRYID:
+ s = "PR_VIEWS_ENTRYID";
+ break;
+case PR_COMMON_VIEWS_ENTRYID:
+ s = "PR_COMMON_VIEWS_ENTRYID";
+ break;
+case PR_FINDER_ENTRYID:
+ s = "PR_FINDER_ENTRYID";
+ break;
+
+ /* Proptags 0x35E8-0x35FF reserved for folders "guaranteed" by
+ * PR_VALID_FOLDER_MASK */
+
+ /*
+ * Folder and AB Container properties
+ */
+
+case PR_CONTAINER_FLAGS:
+ s = "PR_CONTAINER_FLAGS";
+ break;
+case PR_FOLDER_TYPE:
+ s = "PR_FOLDER_TYPE";
+ break;
+case PR_CONTENT_COUNT:
+ s = "PR_CONTENT_COUNT";
+ break;
+case PR_CONTENT_UNREAD:
+ s = "PR_CONTENT_UNREAD";
+ break;
+case PR_CREATE_TEMPLATES:
+ s = "PR_CREATE_TEMPLATES";
+ break;
+case PR_DETAILS_TABLE:
+ s = "PR_DETAILS_TABLE";
+ break;
+case PR_SEARCH:
+ s = "PR_SEARCH";
+ break;
+case PR_SELECTABLE:
+ s = "PR_SELECTABLE";
+ break;
+case PR_SUBFOLDERS:
+ s = "PR_SUBFOLDERS";
+ break;
+case PR_STATUS:
+ s = "PR_STATUS";
+ break;
+case PR_ANR:
+ s = "PR_ANR";
+ break;
+case PR_CONTENTS_SORT_ORDER:
+ s = "PR_CONTENTS_SORT_ORDER";
+ break;
+case PR_CONTAINER_HIERARCHY:
+ s = "PR_CONTAINER_HIERARCHY";
+ break;
+case PR_CONTAINER_CONTENTS:
+ s = "PR_CONTAINER_CONTENTS";
+ break;
+case PR_FOLDER_ASSOCIATED_CONTENTS:
+ s = "PR_FOLDER_ASSOCIATED_CONTENTS";
+ break;
+case PR_DEF_CREATE_DL:
+ s = "PR_DEF_CREATE_DL";
+ break;
+case PR_DEF_CREATE_MAILUSER:
+ s = "PR_DEF_CREATE_MAILUSER";
+ break;
+case PR_CONTAINER_CLASS:
+ s = "PR_CONTAINER_CLASS";
+ break;
+case PR_CONTAINER_MODIFY_VERSION:
+ s = "PR_CONTAINER_MODIFY_VERSION";
+ break;
+case PR_AB_PROVIDER_ID:
+ s = "PR_AB_PROVIDER_ID";
+ break;
+case PR_DEFAULT_VIEW_ENTRYID:
+ s = "PR_DEFAULT_VIEW_ENTRYID";
+ break;
+case PR_ASSOC_CONTENT_COUNT:
+ s = "PR_ASSOC_CONTENT_COUNT";
+ break;
+
+ /* Reserved 0x36C0-0x36FF */
+
+ /*
+ * Attachment properties
+ */
+
+case PR_ATTACHMENT_X400_PARAMETERS:
+ s = "PR_ATTACHMENT_X400_PARAMETERS";
+ break;
+case PR_ATTACH_DATA_OBJ:
+ s = "PR_ATTACH_DATA_OBJ";
+ break;
+case PR_ATTACH_DATA_BIN:
+ s = "PR_ATTACH_DATA_BIN";
+ break;
+case PR_ATTACH_ENCODING:
+ s = "PR_ATTACH_ENCODING";
+ break;
+case PR_ATTACH_EXTENSION:
+ s = "PR_ATTACH_EXTENSION";
+ break;
+case PR_ATTACH_FILENAME:
+ s = "PR_ATTACH_FILENAME";
+ break;
+case PR_ATTACH_METHOD:
+ s = "PR_ATTACH_METHOD";
+ break;
+case PR_ATTACH_LONG_FILENAME:
+ s = "PR_ATTACH_LONG_FILENAME";
+ break;
+case PR_ATTACH_PATHNAME:
+ s = "PR_ATTACH_PATHNAME";
+ break;
+case PR_ATTACH_RENDERING:
+ s = "PR_ATTACH_RENDERING";
+ break;
+case PR_ATTACH_TAG:
+ s = "PR_ATTACH_TAG";
+ break;
+case PR_RENDERING_POSITION:
+ s = "PR_RENDERING_POSITION";
+ break;
+case PR_ATTACH_TRANSPORT_NAME:
+ s = "PR_ATTACH_TRANSPORT_NAME";
+ break;
+case PR_ATTACH_LONG_PATHNAME:
+ s = "PR_ATTACH_LONG_PATHNAME";
+ break;
+case PR_ATTACH_MIME_TAG:
+ s = "PR_ATTACH_MIME_TAG";
+ break;
+case PR_ATTACH_ADDITIONAL_INFO:
+ s = "PR_ATTACH_ADDITIONAL_INFO";
+ break;
+
+ /*
+ * AB Object properties
+ */
+
+case PR_DISPLAY_TYPE:
+ s = "PR_DISPLAY_TYPE";
+ break;
+case PR_TEMPLATEID:
+ s = "PR_TEMPLATEID";
+ break;
+case PR_PRIMARY_CAPABILITY:
+ s = "PR_PRIMARY_CAPABILITY";
+ break;
+
+/*
+ * Mail user properties
+ */
+case PR_7BIT_DISPLAY_NAME:
+ s = "PR_7BIT_DISPLAY_NAME";
+ break;
+case PR_ACCOUNT:
+ s = "PR_ACCOUNT";
+ break;
+case PR_ALTERNATE_RECIPIENT:
+ s = "PR_ALTERNATE_RECIPIENT";
+ break;
+case PR_CALLBACK_TELEPHONE_NUMBER:
+ s = "PR_CALLBACK_TELEPHONE_NUMBER";
+ break;
+case PR_CONVERSION_PROHIBITED:
+ s = "PR_CONVERSION_PROHIBITED";
+ break;
+case PR_DISCLOSE_RECIPIENTS:
+ s = "PR_DISCLOSE_RECIPIENTS";
+ break;
+case PR_GENERATION:
+ s = "PR_GENERATION";
+ break;
+case PR_GIVEN_NAME:
+ s = "PR_GIVEN_NAME";
+ break;
+case PR_GOVERNMENT_ID_NUMBER:
+ s = "PR_GOVERNMENT_ID_NUMBER";
+ break;
+case PR_BUSINESS_TELEPHONE_NUMBER:
+ s = "PR_BUSINESS_TELEPHONE_NUMBER or PR_OFFICE_TELEPHONE_NUMBER";
+ break;
+case PR_HOME_TELEPHONE_NUMBER:
+ s = "PR_HOME_TELEPHONE_NUMBER";
+ break;
+case PR_INITIALS:
+ s = "PR_INITIALS";
+ break;
+case PR_KEYWORD:
+ s = "PR_KEYWORD";
+ break;
+case PR_LANGUAGE:
+ s = "PR_LANGUAGE";
+ break;
+case PR_LOCATION:
+ s = "PR_LOCATION";
+ break;
+case PR_MAIL_PERMISSION:
+ s = "PR_MAIL_PERMISSION";
+ break;
+case PR_MHS_COMMON_NAME:
+ s = "PR_MHS_COMMON_NAME";
+ break;
+case PR_ORGANIZATIONAL_ID_NUMBER:
+ s = "PR_ORGANIZATIONAL_ID_NUMBER";
+ break;
+case PR_SURNAME:
+ s = "PR_SURNAME";
+ break;
+case PR_ORIGINAL_ENTRYID:
+ s = "PR_ORIGINAL_ENTRYID";
+ break;
+case PR_ORIGINAL_DISPLAY_NAME:
+ s = "PR_ORIGINAL_DISPLAY_NAME";
+ break;
+case PR_ORIGINAL_SEARCH_KEY:
+ s = "PR_ORIGINAL_SEARCH_KEY";
+ break;
+case PR_POSTAL_ADDRESS:
+ s = "PR_POSTAL_ADDRESS";
+ break;
+case PR_COMPANY_NAME:
+ s = "PR_COMPANY_NAME";
+ break;
+case PR_TITLE:
+ s = "PR_TITLE";
+ break;
+case PR_DEPARTMENT_NAME:
+ s = "PR_DEPARTMENT_NAME";
+ break;
+case PR_OFFICE_LOCATION:
+ s = "PR_OFFICE_LOCATION";
+ break;
+case PR_PRIMARY_TELEPHONE_NUMBER:
+ s = "PR_PRIMARY_TELEPHONE_NUMBER";
+ break;
+case PR_BUSINESS2_TELEPHONE_NUMBER:
+ s = "PR_BUSINESS2_TELEPHONE_NUMBER or PR_OFFICE2_TELEPHONE_NUMBER";
+ break;
+case PR_MOBILE_TELEPHONE_NUMBER:
+ s = "PR_MOBILE_TELEPHONE_NUMBER or PR_CELLULAR_TELEPHONE_NUMBER";
+ break;
+case PR_RADIO_TELEPHONE_NUMBER:
+ s = "PR_RADIO_TELEPHONE_NUMBER";
+ break;
+case PR_CAR_TELEPHONE_NUMBER:
+ s = "PR_CAR_TELEPHONE_NUMBER";
+ break;
+case PR_OTHER_TELEPHONE_NUMBER:
+ s = "PR_OTHER_TELEPHONE_NUMBER";
+ break;
+case PR_TRANSMITABLE_DISPLAY_NAME:
+ s = "PR_TRANSMITABLE_DISPLAY_NAME";
+ break;
+case PR_PAGER_TELEPHONE_NUMBER:
+ s = "PR_PAGER_TELEPHONE_NUMBER or PR_BEEPER_TELEPHONE_NUMBER";
+ break;
+case PR_USER_CERTIFICATE:
+ s = "PR_USER_CERTIFICATE";
+ break;
+case PR_PRIMARY_FAX_NUMBER:
+ s = "PR_PRIMARY_FAX_NUMBER";
+ break;
+case PR_BUSINESS_FAX_NUMBER:
+ s = "PR_BUSINESS_FAX_NUMBER";
+ break;
+case PR_HOME_FAX_NUMBER:
+ s = "PR_HOME_FAX_NUMBER";
+ break;
+case PR_COUNTRY:
+ s = "PR_COUNTRY or PR_BUSINESS_ADDRESS_COUNTRY";
+ break;
+
+case PR_LOCALITY:
+ s = "PR_LOCALITY or PR_BUSINESS_ADDRESS_CITY";
+ break;
+
+case PR_STATE_OR_PROVINCE:
+ s = "PR_STATE_OR_PROVINCE or PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE";
+ break;
+
+case PR_STREET_ADDRESS:
+ s = "PR_STREET_ADDRESS or PR_BUSINESS_ADDRESS_STREET";
+ break;
+
+case PR_POSTAL_CODE:
+ s = "PR_POSTAL_CODE or PR_BUSINESS_ADDRESS_POSTAL_CODE";
+ break;
+
+case PR_POST_OFFICE_BOX:
+ s = "PR_POST_OFFICE_BOX or PR_BUSINESS_ADDRESS_POST_OFFICE_BOX";
+ break;
+
+case PR_TELEX_NUMBER:
+ s = "PR_TELEX_NUMBER";
+ break;
+case PR_ISDN_NUMBER:
+ s = "PR_ISDN_NUMBER";
+ break;
+case PR_ASSISTANT_TELEPHONE_NUMBER:
+ s = "PR_ASSISTANT_TELEPHONE_NUMBER";
+ break;
+case PR_HOME2_TELEPHONE_NUMBER:
+ s = "PR_HOME2_TELEPHONE_NUMBER";
+ break;
+case PR_ASSISTANT:
+ s = "PR_ASSISTANT";
+ break;
+case PR_SEND_RICH_INFO:
+ s = "PR_SEND_RICH_INFO";
+ break;
+
+case PR_WEDDING_ANNIVERSARY:
+ s = "PR_WEDDING_ANNIVERSARY";
+ break;
+case PR_BIRTHDAY:
+ s = "PR_BIRTHDAY";
+ break;
+
+case PR_HOBBIES:
+ s = "PR_HOBBIES";
+ break;
+
+case PR_MIDDLE_NAME:
+ s = "PR_MIDDLE_NAME";
+ break;
+
+case PR_DISPLAY_NAME_PREFIX:
+ s = "PR_DISPLAY_NAME_PREFIX";
+ break;
+
+case PR_PROFESSION:
+ s = "PR_PROFESSION";
+ break;
+
+case PR_PREFERRED_BY_NAME:
+ s = "PR_PREFERRED_BY_NAME";
+ break;
+
+case PR_SPOUSE_NAME:
+ s = "PR_SPOUSE_NAME";
+ break;
+
+case PR_COMPUTER_NETWORK_NAME:
+ s = "PR_COMPUTER_NETWORK_NAME";
+ break;
+
+case PR_CUSTOMER_ID:
+ s = "PR_CUSTOMER_ID";
+ break;
+
+case PR_TTYTDD_PHONE_NUMBER:
+ s = "PR_TTYTDD_PHONE_NUMBER";
+ break;
+
+case PR_FTP_SITE:
+ s = "PR_FTP_SITE";
+ break;
+
+case PR_GENDER:
+ s = "PR_GENDER";
+ break;
+
+case PR_MANAGER_NAME:
+ s = "PR_MANAGER_NAME";
+ break;
+
+case PR_NICKNAME:
+ s = "PR_NICKNAME";
+ break;
+
+case PR_PERSONAL_HOME_PAGE:
+ s = "PR_PERSONAL_HOME_PAGE";
+ break;
+
+case PR_BUSINESS_HOME_PAGE:
+ s = "PR_BUSINESS_HOME_PAGE";
+ break;
+
+case PR_CONTACT_VERSION:
+ s = "PR_CONTACT_VERSION";
+ break;
+case PR_CONTACT_ENTRYIDS:
+ s = "PR_CONTACT_ENTRYIDS";
+ break;
+
+case PR_CONTACT_ADDRTYPES:
+ s = "PR_CONTACT_ADDRTYPES";
+ break;
+
+case PR_CONTACT_DEFAULT_ADDRESS_INDEX:
+ s = "PR_CONTACT_DEFAULT_ADDRESS_INDEX";
+ break;
+
+case PR_CONTACT_EMAIL_ADDRESSES:
+ s = "PR_CONTACT_EMAIL_ADDRESSES";
+ break;
+
+case PR_COMPANY_MAIN_PHONE_NUMBER:
+ s = "PR_COMPANY_MAIN_PHONE_NUMBER";
+ break;
+
+case PR_CHILDRENS_NAMES:
+ s = "PR_CHILDRENS_NAMES";
+ break;
+
+case PR_HOME_ADDRESS_CITY:
+ s = "PR_HOME_ADDRESS_CITY";
+ break;
+
+case PR_HOME_ADDRESS_COUNTRY:
+ s = "PR_HOME_ADDRESS_COUNTRY";
+ break;
+
+case PR_HOME_ADDRESS_POSTAL_CODE:
+ s = "PR_HOME_ADDRESS_POSTAL_CODE";
+ break;
+
+case PR_HOME_ADDRESS_STATE_OR_PROVINCE:
+ s = "PR_HOME_ADDRESS_STATE_OR_PROVINCE";
+ break;
+
+case PR_HOME_ADDRESS_STREET:
+ s = "PR_HOME_ADDRESS_STREET";
+ break;
+
+case PR_HOME_ADDRESS_POST_OFFICE_BOX:
+ s = "PR_HOME_ADDRESS_POST_OFFICE_BOX";
+ break;
+
+case PR_OTHER_ADDRESS_CITY:
+ s = "PR_OTHER_ADDRESS_CITY";
+ break;
+
+case PR_OTHER_ADDRESS_COUNTRY:
+ s = "PR_OTHER_ADDRESS_COUNTRY";
+ break;
+
+case PR_OTHER_ADDRESS_POSTAL_CODE:
+ s = "PR_OTHER_ADDRESS_POSTAL_CODE";
+ break;
+
+case PR_OTHER_ADDRESS_STATE_OR_PROVINCE:
+ s = "PR_OTHER_ADDRESS_STATE_OR_PROVINCE";
+ break;
+
+case PR_OTHER_ADDRESS_STREET:
+ s = "PR_OTHER_ADDRESS_STREET";
+ break;
+
+case PR_OTHER_ADDRESS_POST_OFFICE_BOX:
+ s = "PR_OTHER_ADDRESS_POST_OFFICE_BOX";
+ break;
+
+ /*
+ * Profile section properties
+ */
+
+case PR_STORE_PROVIDERS:
+ s = "PR_STORE_PROVIDERS";
+ break;
+case PR_AB_PROVIDERS:
+ s = "PR_AB_PROVIDERS";
+ break;
+case PR_TRANSPORT_PROVIDERS:
+ s = "PR_TRANSPORT_PROVIDERS";
+ break;
+
+case PR_DEFAULT_PROFILE:
+ s = "PR_DEFAULT_PROFILE";
+ break;
+case PR_AB_SEARCH_PATH:
+ s = "PR_AB_SEARCH_PATH";
+ break;
+case PR_AB_DEFAULT_DIR:
+ s = "PR_AB_DEFAULT_DIR";
+ break;
+case PR_AB_DEFAULT_PAB:
+ s = "PR_AB_DEFAULT_PAB";
+ break;
+
+case PR_FILTERING_HOOKS:
+ s = "PR_FILTERING_HOOKS";
+ break;
+case PR_SERVICE_NAME:
+ s = "PR_SERVICE_NAME";
+ break;
+case PR_SERVICE_DLL_NAME:
+ s = "PR_SERVICE_DLL_NAME";
+ break;
+case PR_SERVICE_ENTRY_NAME:
+ s = "PR_SERVICE_ENTRY_NAME";
+ break;
+case PR_SERVICE_UID:
+ s = "PR_SERVICE_UID";
+ break;
+case PR_SERVICE_EXTRA_UIDS:
+ s = "PR_SERVICE_EXTRA_UIDS";
+ break;
+case PR_SERVICES:
+ s = "PR_SERVICES";
+ break;
+case PR_SERVICE_SUPPORT_FILES:
+ s = "PR_SERVICE_SUPPORT_FILES";
+ break;
+case PR_SERVICE_DELETE_FILES:
+ s = "PR_SERVICE_DELETE_FILES";
+ break;
+case PR_AB_SEARCH_PATH_UPDATE:
+ s = "PR_AB_SEARCH_PATH_UPDATE";
+ break;
+case PR_PROFILE_NAME:
+ s = "PR_PROFILE_NAME";
+ break;
+
+ /*
+ * Status object properties
+ */
+
+case PR_IDENTITY_DISPLAY:
+ s = "PR_IDENTITY_DISPLAY";
+ break;
+case PR_IDENTITY_ENTRYID:
+ s = "PR_IDENTITY_ENTRYID";
+ break;
+case PR_RESOURCE_METHODS:
+ s = "PR_RESOURCE_METHODS";
+ break;
+case PR_RESOURCE_TYPE:
+ s = "PR_RESOURCE_TYPE";
+ break;
+case PR_STATUS_CODE:
+ s = "PR_STATUS_CODE";
+ break;
+case PR_IDENTITY_SEARCH_KEY:
+ s = "PR_IDENTITY_SEARCH_KEY";
+ break;
+case PR_OWN_STORE_ENTRYID:
+ s = "PR_OWN_STORE_ENTRYID";
+ break;
+case PR_RESOURCE_PATH:
+ s = "PR_RESOURCE_PATH";
+ break;
+case PR_STATUS_STRING:
+ s = "PR_STATUS_STRING";
+ break;
+case PR_X400_DEFERRED_DELIVERY_CANCEL:
+ s = "PR_X400_DEFERRED_DELIVERY_CANCEL";
+ break;
+case PR_HEADER_FOLDER_ENTRYID:
+ s = "PR_HEADER_FOLDER_ENTRYID";
+ break;
+case PR_REMOTE_PROGRESS:
+ s = "PR_REMOTE_PROGRESS";
+ break;
+case PR_REMOTE_PROGRESS_TEXT:
+ s = "PR_REMOTE_PROGRESS_TEXT";
+ break;
+case PR_REMOTE_VALIDATE_OK:
+ s = "PR_REMOTE_VALIDATE_OK";
+ break;
+
+ /*
+ * Display table properties
+ */
+
+case PR_CONTROL_FLAGS:
+ s = "PR_CONTROL_FLAGS";
+ break;
+case PR_CONTROL_STRUCTURE:
+ s = "PR_CONTROL_STRUCTURE";
+ break;
+case PR_CONTROL_TYPE:
+ s = "PR_CONTROL_TYPE";
+ break;
+case PR_DELTAX:
+ s = "PR_DELTAX";
+ break;
+case PR_DELTAY:
+ s = "PR_DELTAY";
+ break;
+case PR_XPOS:
+ s = "PR_XPOS";
+ break;
+case PR_YPOS:
+ s = "PR_YPOS";
+ break;
+case PR_CONTROL_ID:
+ s = "PR_CONTROL_ID";
+ break;
+case PR_INITIAL_DETAILS_PANE:
+ s = "PR_INITIAL_DETAILS_PANE";
+ break;
+/*
+ * Secure property id range
+ */
+case PROP_ID_SECURE_MIN:
+ s = "PROP_ID_SECURE_MIN";
+ break;
+case PROP_ID_SECURE_MAX:
+ s = "PROP_ID_SECURE_MAX";
+ break;
diff --git a/comm/mailnews/import/src/MorkImport.cpp b/comm/mailnews/import/src/MorkImport.cpp
new file mode 100644
index 0000000000..4cdecb8067
--- /dev/null
+++ b/comm/mailnews/import/src/MorkImport.cpp
@@ -0,0 +1,343 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+/*
+ * Mork import addressbook interfaces
+ */
+
+#include "MorkImport.h"
+
+#include "nsCOMPtr.h"
+#include "nsIImportService.h"
+#include "nsIImportGeneric.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIImportFieldMap.h"
+#include "nsImportStringBundle.h"
+#include "nsIComponentManager.h"
+#include "nsIAbDirectory.h"
+#include "nsAddrDatabase.h"
+#include "nsInterfaceHashtable.h"
+#include "nsHashKeys.h"
+
+static const char kRowIDProperty[] = "DbRowID";
+
+class MorkImportAddressImpl final : public nsIImportAddressBooks {
+ public:
+ explicit MorkImportAddressImpl(nsIStringBundle* aStringBundle);
+
+ static nsresult Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTADDRESSBOOKS
+
+ private:
+ ~MorkImportAddressImpl() {}
+ nsCOMPtr<nsIFile> mFileLocation;
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+};
+
+MorkImport::MorkImport() {
+ nsImportStringBundle::GetStringBundle(
+ "chrome://messenger/locale/morkImportMsgs.properties",
+ getter_AddRefs(mStringBundle));
+}
+
+MorkImport::~MorkImport() {}
+
+NS_IMPL_ISUPPORTS(MorkImport, nsIImportModule)
+
+NS_IMETHODIMP MorkImport::GetName(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ *name =
+ nsImportStringBundle::GetStringByName("morkImportName", mStringBundle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImport::GetDescription(char16_t** description) {
+ NS_ENSURE_ARG_POINTER(description);
+ *description = nsImportStringBundle::GetStringByName("morkImportDescription",
+ mStringBundle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImport::GetSupports(char** supports) {
+ NS_ENSURE_ARG_POINTER(supports);
+ *supports = strdup(NS_IMPORT_ADDRESS_STR);
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImport::GetSupportsUpgrade(bool* upgrade) {
+ NS_ENSURE_ARG_POINTER(upgrade);
+ *upgrade = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImport::GetImportInterface(const char* importType,
+ nsISupports** interface) {
+ NS_ENSURE_ARG_POINTER(importType);
+ NS_ENSURE_ARG_POINTER(interface);
+
+ *interface = nullptr;
+ nsresult rv;
+
+ if (strcmp(importType, "addressbook")) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIImportAddressBooks> pAddress;
+ nsCOMPtr<nsIImportGeneric> pGeneric;
+ rv = MorkImportAddressImpl::Create(getter_AddRefs(pAddress), mStringBundle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = impSvc->CreateNewGenericAddressBooks(getter_AddRefs(pGeneric));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pGeneric->SetData("addressInterface", pAddress);
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pGeneric));
+ pInterface.forget(interface);
+
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+
+nsresult MorkImportAddressImpl::Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new MorkImportAddressImpl(aStringBundle));
+ return NS_OK;
+}
+
+MorkImportAddressImpl::MorkImportAddressImpl(nsIStringBundle* aStringBundle)
+ : mStringBundle(aStringBundle) {}
+
+NS_IMPL_ISUPPORTS(MorkImportAddressImpl, nsIImportAddressBooks)
+
+NS_IMETHODIMP MorkImportAddressImpl::GetSupportsMultiple(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::GetAutoFind(char16_t** addrDescription,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(addrDescription);
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::GetNeedsFieldMap(nsIFile* aLocation,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::GetDefaultLocation(nsIFile** ppLoc,
+ bool* found,
+ bool* userVerify) {
+ NS_ENSURE_ARG_POINTER(ppLoc);
+ NS_ENSURE_ARG_POINTER(found);
+ NS_ENSURE_ARG_POINTER(userVerify);
+
+ *ppLoc = nullptr;
+ *found = false;
+ *userVerify = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::FindAddressBooks(
+ nsIFile* pLoc, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ NS_ENSURE_ARG_POINTER(pLoc);
+
+ books.Clear();
+ bool exists = false;
+ nsresult rv = pLoc->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_ERROR_FAILURE;
+
+ bool isFile = false;
+ rv = pLoc->IsFile(&isFile);
+ if (NS_FAILED(rv) || !isFile) return NS_ERROR_FAILURE;
+
+ mFileLocation = pLoc;
+
+ /* Build an address book descriptor based on the file passed in! */
+ nsString name;
+ rv = mFileLocation->GetLeafName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t idx = name.RFindChar('.');
+ if ((idx != -1) && (idx > 0) && ((name.Length() - idx - 1) < 5)) {
+ name.SetLength(idx);
+ }
+
+ nsCOMPtr<nsIImportABDescriptor> desc;
+
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = impSvc->CreateNewABDescriptor(getter_AddRefs(desc));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t sz = 0;
+ pLoc->GetFileSize(&sz);
+ desc->SetPreferredName(name);
+ desc->SetSize((uint32_t)sz);
+ desc->SetAbFile(mFileLocation);
+ books.AppendElement(desc);
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::InitFieldMap(nsIImportFieldMap* fieldMap) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MorkImportAddressImpl::ImportAddressBook(
+ nsIImportABDescriptor* pSource, nsIAbDirectory* pDestination,
+ nsIImportFieldMap* fieldMap, nsISupports* aSupportService,
+ char16_t** pErrorLog, char16_t** pSuccessLog, bool* fatalError) {
+ NS_ENSURE_ARG_POINTER(pSource);
+ NS_ENSURE_ARG_POINTER(pDestination);
+ NS_ENSURE_ARG_POINTER(fatalError);
+
+ nsCOMPtr<nsIFile> oldFile;
+ pSource->GetAbFile(getter_AddRefs(oldFile));
+
+ nsresult rv = ReadMABToDirectory(oldFile, pDestination);
+
+ *pSuccessLog =
+ nsImportStringBundle::GetStringByName("morkImportSuccess", mStringBundle);
+ return rv;
+}
+
+nsresult ReadMABToDirectory(nsIFile* oldFile, nsIAbDirectory* newDirectory) {
+ nsresult rv;
+
+ nsAddrDatabase database = nsAddrDatabase();
+ database.SetDbPath(oldFile);
+ database.OpenMDB(oldFile, false);
+
+ nsInterfaceHashtable<nsUint32HashKey, nsIAbCard> cardMap;
+
+ nsCOMPtr<nsISimpleEnumerator> enumerator;
+ database.EnumerateCards(getter_AddRefs(enumerator));
+
+ nsCOMPtr<nsISupports> supports;
+ nsCOMPtr<nsIAbCard> card;
+ bool isMailList;
+ while (NS_SUCCEEDED(enumerator->GetNext(getter_AddRefs(supports))) &&
+ supports) {
+ card = do_QueryInterface(supports);
+
+ card->GetIsMailList(&isMailList);
+ if (isMailList) {
+ continue;
+ }
+
+ uint32_t rowId;
+ card->GetPropertyAsUint32(kRowIDProperty, &rowId);
+ cardMap.InsertOrUpdate(rowId, card);
+
+ nsIAbCard* outCard;
+ newDirectory->AddCard(card, &outCard);
+ }
+
+ database.EnumerateCards(getter_AddRefs(enumerator));
+
+ while (NS_SUCCEEDED(enumerator->GetNext(getter_AddRefs(supports))) &&
+ supports) {
+ card = do_QueryInterface(supports);
+ card->GetIsMailList(&isMailList);
+
+ if (!isMailList) {
+ continue;
+ }
+
+ nsCOMPtr<nsIAbDirectory> mailList =
+ do_CreateInstance("@mozilla.org/addressbook/directoryproperty;1");
+ mailList->SetIsMailList(true);
+
+ nsAutoString listName;
+ card->GetDisplayName(listName);
+ mailList->SetDirName(listName);
+
+ nsAutoString nickName;
+ rv = card->GetPropertyAsAString("NickName", nickName);
+ if (NS_SUCCEEDED(rv)) {
+ mailList->SetListNickName(nickName);
+ }
+
+ nsAutoString description;
+ rv = card->GetPropertyAsAString("Notes", description);
+ if (NS_SUCCEEDED(rv)) {
+ mailList->SetDescription(description);
+ }
+
+ nsIAbDirectory* outList;
+ rv = newDirectory->AddMailList(mailList, &outList);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ uint32_t listRowId;
+ card->GetPropertyAsUint32(kRowIDProperty, &listRowId);
+
+ nsCOMPtr<nsISimpleEnumerator> listEnumerator;
+ database.EnumerateListAddresses(listRowId, getter_AddRefs(listEnumerator));
+
+ nsCOMPtr<nsISupports> listSupports;
+ nsCOMPtr<nsIAbCard> listCard;
+ while (
+ NS_SUCCEEDED(listEnumerator->GetNext(getter_AddRefs(listSupports))) &&
+ listSupports) {
+ listCard = do_QueryInterface(listSupports);
+
+ uint32_t rowId;
+ listCard->GetPropertyAsUint32(kRowIDProperty, &rowId);
+ cardMap.Get(rowId, getter_AddRefs(listCard));
+
+ nsIAbCard* outCard;
+ outList->AddCard(listCard, &outCard);
+ }
+ }
+
+ database.ForceClosed();
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::GetImportProgress(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::SetSampleLocation(nsIFile* pLocation) {
+ NS_ENSURE_ARG_POINTER(pLocation);
+ return NS_OK;
+}
+
+NS_IMETHODIMP MorkImportAddressImpl::GetSampleData(int32_t index, bool* pFound,
+ char16_t** pStr) {
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsImportABFromMab, nsIImportABFile)
+
+nsImportABFromMab::nsImportABFromMab() {}
+
+NS_IMETHODIMP
+nsImportABFromMab::ReadFileToDirectory(nsIFile* sourceFile,
+ nsIAbDirectory* targetDirectory) {
+ return ReadMABToDirectory(sourceFile, targetDirectory);
+}
diff --git a/comm/mailnews/import/src/MorkImport.h b/comm/mailnews/import/src/MorkImport.h
new file mode 100644
index 0000000000..80a0d25bec
--- /dev/null
+++ b/comm/mailnews/import/src/MorkImport.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#ifndef MorkImport_h___
+#define MorkImport_h___
+
+#include "nsIImportABFile.h"
+#include "nsIImportModule.h"
+#include "nsCOMPtr.h"
+#include "nsIStringBundle.h"
+
+#include "nsIFile.h"
+#include "nsIAbDirectory.h"
+
+#define MORKIMPORT_CID \
+ { /* 54d48d9f-1bac-47be-9190-c4dc74e837e2 */ \
+ 0x54d48d9f, 0x1bac, 0x47be, { \
+ 0x91, 0x90, 0xc4, 0xdc, 0x74, 0xe8, 0x37, 0xe2 \
+ } \
+ }
+
+nsresult ReadMABToDirectory(nsIFile* oldFile, nsIAbDirectory* newDirectory);
+
+class nsImportABFromMab : public nsIImportABFile {
+ public:
+ nsImportABFromMab();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTABFILE
+
+ protected:
+ virtual ~nsImportABFromMab(){};
+};
+
+class MorkImport : public nsIImportModule {
+ public:
+ MorkImport();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTMODULE
+
+ protected:
+ virtual ~MorkImport();
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+};
+
+#endif /* MorkImport_h___ */
diff --git a/comm/mailnews/import/src/SeamonkeyImport.jsm b/comm/mailnews/import/src/SeamonkeyImport.jsm
new file mode 100644
index 0000000000..c88f1830d7
--- /dev/null
+++ b/comm/mailnews/import/src/SeamonkeyImport.jsm
@@ -0,0 +1,253 @@
+/* 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/. */
+
+let { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+let seamonkeyImportMsgs = Services.strings.createBundle(
+ "chrome://messenger/locale/seamonkeyImportMsgs.properties"
+);
+
+var EXPORTED_SYMBOLS = ["SeamonkeyImport"];
+
+/**
+ * Implements nsIImportGeneric instead of nsIImportAddressBook. The actual
+ * importing is delegated to nsSeamonkeyProfileMigrator.
+ */
+function SeamonkeyImportAddressbook() {
+ this.migrator = Cc[
+ "@mozilla.org/profile/migrator;1?app=mail&type=seamonkey"
+ ].createInstance(Ci.nsIMailProfileMigrator);
+ this.sourceProfileName = null;
+ this.sourceProfileLocation = null;
+}
+
+SeamonkeyImportAddressbook.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIImportGeneric"]),
+
+ /**
+ * Return the location of addressbook.
+ */
+ GetData() {
+ if (!this.sourceProfileName || !this.sourceProfileLocation) {
+ try {
+ this.sourceProfileName = this.migrator.sourceProfiles[0];
+ this.sourceProfileLocation = this.migrator.sourceProfileLocations[0];
+ } catch (e) {
+ return null;
+ }
+ }
+
+ return this.sourceProfileLocation;
+ },
+
+ SetData() {
+ return 0;
+ },
+
+ WantsProgress() {
+ return false;
+ },
+
+ GetProgress() {
+ return 0;
+ },
+
+ GetStatus() {
+ return 0;
+ },
+
+ CancelImport() {
+ return 0;
+ },
+
+ ContinueImport() {
+ return 0;
+ },
+
+ BeginImport(successLog, errorLog) {
+ this.migrator.migrate(
+ Ci.nsIMailProfileMigrator.ADDRESSBOOK_DATA,
+ null,
+ this.sourceProfileName
+ );
+ successLog.data = seamonkeyImportMsgs.GetStringFromName(
+ "SeamonkeyImportAddressSuccess"
+ );
+ return true;
+ },
+};
+
+/**
+ * Implements nsIImportMail. The importing process is managed by nsImportMail.
+ */
+function SeamonkeyImportMail() {}
+
+SeamonkeyImportMail.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIImportMail"]),
+
+ GetDefaultLocation(location, found, userVerify) {
+ let migrator = Cc[
+ "@mozilla.org/profile/migrator;1?app=mail&type=seamonkey"
+ ].createInstance(Ci.nsIMailProfileMigrator);
+
+ try {
+ let sourceProfile = migrator.sourceProfileLocations[0];
+ location.value = sourceProfile;
+ found.value = true;
+ } catch (e) {
+ found.value = false;
+ }
+ userVerify.value = false;
+ },
+
+ _createMailboxDescriptor(path, name, depth) {
+ let importService = Cc[
+ "@mozilla.org/import/import-service;1"
+ ].createInstance(Ci.nsIImportService);
+ let descriptor = importService.CreateNewMailboxDescriptor();
+ descriptor.size = 100;
+ descriptor.depth = depth;
+ descriptor.SetDisplayName(name);
+ descriptor.file.initWithPath(path);
+
+ return descriptor;
+ },
+
+ _collectMailboxesInDirectory(directory, depth) {
+ let result = [];
+ let name = directory.leafName;
+ if (depth > 0 && !name.endsWith(".msf") && !name.endsWith(".dat")) {
+ if (name.endsWith(".sbd")) {
+ name = name.slice(0, name.lastIndexOf("."));
+ }
+ let descriptor = this._createMailboxDescriptor(
+ directory.path,
+ name,
+ depth
+ );
+ result.push(descriptor);
+ }
+ if (directory.isDirectory()) {
+ for (let entry of directory.directoryEntries) {
+ if (
+ (depth == 0 &&
+ entry.leafName != "ImapMail" &&
+ entry.leafName != "Mail") ||
+ (depth == 1 && entry.leafName == "Feeds")
+ ) {
+ continue;
+ }
+ result.push(...this._collectMailboxesInDirectory(entry, depth + 1));
+ }
+ }
+ return result;
+ },
+
+ // Collect mailboxes in a Seamonkey profile.
+ findMailboxes(location) {
+ return this._collectMailboxesInDirectory(location, 0);
+ },
+
+ // Copy mailboxes a Seamonkey profile to Thunderbird profile.
+ ImportMailbox(source, dstFolder, errorLog, successLog, fatalError) {
+ if (source.file.isFile()) {
+ source.file.copyTo(
+ dstFolder.filePath.parent,
+ dstFolder.filePath.leafName
+ );
+ successLog.value = `Import ${source.file.leafName} succeeded.\n`;
+ }
+ },
+};
+
+/**
+ * Implements nsIImportSettings. The actual importing is delegated to
+ * nsSeamonkeyProfileMigrator.
+ */
+function SeamonkeyImportSettings() {
+ this.migrator = Cc[
+ "@mozilla.org/profile/migrator;1?app=mail&type=seamonkey"
+ ].createInstance(Ci.nsIMailProfileMigrator);
+ this.sourceProfileName = null;
+ this.sourceProfileLocation = null;
+}
+
+SeamonkeyImportSettings.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIImportSettings"]),
+
+ AutoLocate(desc, loc) {
+ if (!this.sourceProfileName || !this.sourceProfileLocation) {
+ try {
+ this.sourceProfileName = this.migrator.sourceProfiles[0];
+ this.sourceProfileLocation = this.migrator.sourceProfileLocations[0];
+ } catch (e) {
+ return false;
+ }
+ }
+ loc = this.sourceProfileLocation;
+ return true;
+ },
+
+ Import() {
+ this.migrator.migrate(
+ Ci.nsIMailProfileMigrator.SETTINGS,
+ null,
+ this.sourceProfileName
+ );
+
+ // Reload accounts so that `CheckIfLocalFolderExists` in importDialog works
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+ return true;
+ },
+};
+
+/**
+ * Implements nsIImportModule so that Seamonkey is shown as an option in the
+ * importDialog.xhtml. Currently supports importing addressbook and mail, see
+ * the GetImportInterface function.
+ */
+function SeamonkeyImport() {}
+
+SeamonkeyImport.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIImportModule"]),
+
+ get name() {
+ return seamonkeyImportMsgs.GetStringFromName("SeamonkeyImportName");
+ },
+
+ get description() {
+ return seamonkeyImportMsgs.GetStringFromName("SeamonkeyImportDescription");
+ },
+
+ get supports() {
+ return "addressbook,mail,settings";
+ },
+
+ get supportsUpgrade() {
+ return false;
+ },
+
+ GetImportInterface(type) {
+ if (type == "addressbook") {
+ return new SeamonkeyImportAddressbook();
+ } else if (type == "mail") {
+ let importService = Cc[
+ "@mozilla.org/import/import-service;1"
+ ].createInstance(Ci.nsIImportService);
+ let genericInterface = importService.CreateNewGenericMail();
+ genericInterface.SetData("mailInterface", new SeamonkeyImportMail());
+ let name = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ name.data = "SeaMonkey";
+ genericInterface.SetData("name", name);
+ return genericInterface;
+ } else if (type == "settings") {
+ return new SeamonkeyImportSettings();
+ }
+ return null;
+ },
+};
diff --git a/comm/mailnews/import/src/ThunderbirdImport.jsm b/comm/mailnews/import/src/ThunderbirdImport.jsm
new file mode 100644
index 0000000000..8263fa7098
--- /dev/null
+++ b/comm/mailnews/import/src/ThunderbirdImport.jsm
@@ -0,0 +1,145 @@
+/* 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 = ["ThunderbirdImport"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization(["messenger/importDialog.ftl"], true)
+);
+
+/**
+ * The importing process is managed by importDialog.js and nsImportMail.cpp.
+ *
+ * @implements {nsIImportMail}
+ */
+class ThunderbirdImportMail {
+ QueryInterface = ChromeUtils.generateQI(["nsIImportMail"]);
+
+ GetDefaultLocation(location, found, userVerify) {
+ userVerify.value = true;
+ }
+
+ /**
+ * Create a nsIImportMailboxDescriptor instance.
+ *
+ * @param {string} path - The mailbox path.
+ * @param {string} name - The mailbox name.
+ * @param {number} depth - The depth of the mailbox folder.
+ * @returns {nsIImportMailboxDescriptor}
+ */
+ _createMailboxDescriptor(path, name, depth) {
+ let importService = Cc[
+ "@mozilla.org/import/import-service;1"
+ ].createInstance(Ci.nsIImportService);
+ let descriptor = importService.CreateNewMailboxDescriptor();
+ descriptor.size = 100;
+ descriptor.depth = depth;
+ descriptor.SetDisplayName(name);
+ descriptor.file.initWithPath(path);
+
+ return descriptor;
+ }
+
+ /**
+ * Recursively find mailboxes in a directory.
+ *
+ * @param {nsIFile} directory - The directory to find mailboxes.
+ * @param {number} depth - The depth of the current directory.
+ * @returns {nsIImportMailboxDescriptor[]} - All mailboxes found.
+ */
+ _collectMailboxesInDirectory(directory, depth) {
+ let result = [];
+ let name = directory.leafName;
+ if (depth > 0 && !name.endsWith(".msf") && !name.endsWith(".dat")) {
+ if (name.endsWith(".sbd")) {
+ name = name.slice(0, name.lastIndexOf("."));
+ }
+ let descriptor = this._createMailboxDescriptor(
+ directory.path,
+ name,
+ depth
+ );
+ result.push(descriptor);
+ }
+ if (directory.isDirectory()) {
+ for (let entry of directory.directoryEntries) {
+ if (
+ (depth == 0 &&
+ entry.leafName != "ImapMail" &&
+ entry.leafName != "Mail") ||
+ (depth == 1 && entry.leafName == "Feeds")
+ ) {
+ continue;
+ }
+ result.push(...this._collectMailboxesInDirectory(entry, depth + 1));
+ }
+ }
+ return result;
+ }
+
+ findMailboxes(location) {
+ return this._collectMailboxesInDirectory(location, 0);
+ }
+
+ ImportMailbox(source, dstFolder, errorLog, successLog, fatalError) {
+ if (source.file.isFile()) {
+ source.file.copyTo(
+ dstFolder.filePath.parent,
+ dstFolder.filePath.leafName
+ );
+ successLog.value = `Import ${source.file.leafName} succeeded.\n`;
+ }
+ }
+}
+
+/**
+ * With this class, Thunderbird is shown as an option in the importDialog.xhtml.
+ * Currently supports importing mail, see the GetImportInterface function.
+ *
+ * @implements {nsIImportModule}
+ */
+class ThunderbirdImport {
+ QueryInterface = ChromeUtils.generateQI(["nsIImportModule"]);
+
+ get name() {
+ return lazy.l10n.formatValueSync("thunderbird-import-name");
+ }
+
+ get description() {
+ return lazy.l10n.formatValueSync("thunderbird-import-description");
+ }
+
+ get supports() {
+ return "mail";
+ }
+
+ get supportsUpgrade() {
+ return false;
+ }
+
+ GetImportInterface(type) {
+ if (type == "mail") {
+ let importService = Cc[
+ "@mozilla.org/import/import-service;1"
+ ].createInstance(Ci.nsIImportService);
+ let genericInterface = importService.CreateNewGenericMail();
+ genericInterface.SetData("mailInterface", new ThunderbirdImportMail());
+ let name = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ name.data = "Thunderbird";
+ genericInterface.SetData("name", name);
+ return genericInterface;
+ }
+ return null;
+ }
+}
diff --git a/comm/mailnews/import/src/components.conf b/comm/mailnews/import/src/components.conf
new file mode 100644
index 0000000000..e8891fca57
--- /dev/null
+++ b/comm/mailnews/import/src/components.conf
@@ -0,0 +1,104 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{991f078e-a6d5-44f2-b91e-c52efcfa3360}",
+ "contract_ids": ["@mozilla.org/import/import-seamonkey;1"],
+ "jsm": "resource:///modules/SeamonkeyImport.jsm",
+ "constructor": "SeamonkeyImport",
+ "categories": {"mailnewsimport": "seamonkey"},
+ },
+ {
+ "cid": "{c6988841-d916-44a3-bb4d-f0838a98e95a}",
+ "contract_ids": ["@mozilla.org/import/import-thunderbird;1"],
+ "jsm": "resource:///modules/ThunderbirdImport.jsm",
+ "constructor": "ThunderbirdImport",
+ "categories": {"mailnewsimport": "thunderbird"},
+ },
+ {
+ "cid": "{a6629718-9a97-4073-ab48-442fcceaea5d}",
+ "contract_ids": ["@mozilla.org/import/import-ab-file;1?type=mab"],
+ "type": "nsImportABFromMab",
+ "headers": ["/comm/mailnews/import/src/MorkImport.h"],
+ },
+ {
+ "cid": "{5df96d60-1726-11d3-a206-00a0cc26da63}",
+ "contract_ids": ["@mozilla.org/import/import-service;1"],
+ "type": "nsImportService",
+ "headers": ["/comm/mailnews/import/src/nsImportService.h"],
+ "name": "Import",
+ "interfaces": ["nsIImportService"],
+ },
+ {
+ "cid": "{a5991d01-ada7-11d3-a9c2-00a0cc26da63}",
+ "contract_ids": ["@mozilla.org/import/import-text;1"],
+ "type": "nsTextImport",
+ "headers": ["/comm/mailnews/import/src/nsTextImport.h"],
+ "categories": {"mailnewsimport": "text"},
+ },
+ {
+ "cid": "{0eb034a3-964a-4e2f-92ebcc55d9ae9dd2}",
+ "contract_ids": ["@mozilla.org/import/import-vcard;1"],
+ "type": "nsVCardImport",
+ "headers": ["/comm/mailnews/import/src/nsVCardImport.h"],
+ "categories": {"mailnewsimport": "vcard"},
+ },
+ {
+ "cid": "{54d48d9f-1bac-47be-9190-c4dc74e837e2}",
+ "contract_ids": ["@mozilla.org/import/import-mork;1"],
+ "type": "MorkImport",
+ "headers": ["/comm/mailnews/import/src/MorkImport.h"],
+ "categories": {"mailnewsimport": "mork"},
+ },
+]
+
+if buildconfig.substs["OS_ARCH"] == "Darwin":
+ Classes += [
+ {
+ "cid": "{6d3f101c-70ec-4e04-b68d-9908d1aeddf3}",
+ "contract_ids": ["@mozilla.org/import/import-applemail;1"],
+ "type": "nsAppleMailImportModule",
+ "headers": ["/comm/mailnews/import/src/nsAppleMailImport.h"],
+ "categories": {"mailnewsimport": "applemail"},
+ },
+ {
+ "cid": "{9117a1ea-e012-43b5-a020-cb8a66cc09e1}",
+ "contract_ids": ["@mozilla.org/import/import-appleMailImpl;1"],
+ "type": "nsAppleMailImportMail",
+ "init_method": "Initialize",
+ "headers": ["/comm/mailnews/import/src/nsAppleMailImport.h"],
+ },
+ ]
+
+if buildconfig.substs["OS_ARCH"] == "WINNT":
+ Classes += [
+ {
+ "cid": "{42bc82bc-8e9f-4597-8b6e-e529daaf3af1}",
+ "contract_ids": ["@mozilla.org/import/import-wm;1"],
+ "type": "nsWMImport",
+ "headers": ["/comm/mailnews/import/src/nsWMImport.h"],
+ "categories": {"mailnewsimport": "winlivemail"},
+ },
+ {
+ "cid": "{7952a6cf-2442-4c04-9f02-150b15a0a841}",
+ "contract_ids": ["@mozilla.org/import/import-becky;1"],
+ "type": "nsBeckyImport",
+ "headers": ["/comm/mailnews/import/src/nsBeckyImport.h"],
+ "categories": {"mailnewsimport": "becky"},
+ },
+ ]
+
+ if buildconfig.substs["MOZ_MAPI_SUPPORT"]:
+ Classes += [
+ {
+ "cid": "{1db469a0-8b00-11d3-a206-00a0cc26da63}",
+ "contract_ids": ["@mozilla.org/import/import-outlook;1"],
+ "type": "nsOutlookImport",
+ "headers": ["/comm/mailnews/import/src/nsOutlookImport.h"],
+ "categories": {"mailnewsimport": "outlook"},
+ },
+ ]
diff --git a/comm/mailnews/import/src/moz.build b/comm/mailnews/import/src/moz.build
new file mode 100644
index 0000000000..d587d87fa5
--- /dev/null
+++ b/comm/mailnews/import/src/moz.build
@@ -0,0 +1,86 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "ImportCharSet.cpp",
+ "ImportOutFile.cpp",
+ "ImportTranslate.cpp",
+ "MorkImport.cpp",
+ "nsAddrDatabase.cpp",
+ "nsImportABDescriptor.cpp",
+ "nsImportAddressBooks.cpp",
+ "nsImportEmbeddedImageData.cpp",
+ "nsImportEncodeScan.cpp",
+ "nsImportFieldMap.cpp",
+ "nsImportMail.cpp",
+ "nsImportMailboxDescriptor.cpp",
+ "nsImportScanFile.cpp",
+ "nsImportService.cpp",
+ "nsImportStringBundle.cpp",
+ "nsImportTranslator.cpp",
+ "nsTextAddress.cpp",
+ "nsTextImport.cpp",
+ "nsVCardAddress.cpp",
+ "nsVCardImport.cpp",
+]
+
+if not CONFIG["MOZ_SUITE"]:
+ EXTRA_JS_MODULES += [
+ "SeamonkeyImport.jsm",
+ "ThunderbirdImport.jsm",
+ ]
+
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += [
+ "nsAppleMailImport.cpp",
+ "nsEmlxHelperUtils.mm",
+ ]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ UNIFIED_SOURCES += [
+ "nsBeckyAddressBooks.cpp",
+ "nsBeckyFilters.cpp",
+ "nsBeckyImport.cpp",
+ "nsBeckyMail.cpp",
+ "nsBeckySettings.cpp",
+ "nsBeckyStringBundle.cpp",
+ "nsBeckyUtils.cpp",
+ ]
+
+ if CONFIG["MOZ_MAPI_SUPPORT"]:
+ SOURCES += [
+ "MapiApi.cpp",
+ "MapiMessage.cpp",
+ "MapiMimeTypes.cpp",
+ "nsOutlookCompose.cpp",
+ "nsOutlookImport.cpp",
+ "nsOutlookMail.cpp",
+ "nsOutlookSettings.cpp",
+ "nsOutlookStringBundle.cpp",
+ "rtfDecoder.cpp",
+ "rtfMailDecoder.cpp",
+ ]
+
+ SOURCES["rtfDecoder.cpp"].flags += ["-Wno-switch"]
+ LOCAL_INCLUDES += ["/comm/mailnews/mapi/include"]
+
+ if CONFIG["CC_TYPE"] in ("msvc", "clang-cl"):
+ SOURCES += [
+ "nsWMImport.cpp",
+ "nsWMSettings.cpp",
+ "nsWMStringBundle.cpp",
+ "nsWMUtils.cpp",
+ ]
+
+EXPORTS += [
+ "ImportDebug.h",
+ "nsVCardAddress.h",
+]
+
+FINAL_LIBRARY = "import"
diff --git a/comm/mailnews/import/src/nsAddrDatabase.cpp b/comm/mailnews/import/src/nsAddrDatabase.cpp
new file mode 100644
index 0000000000..1bc0df1795
--- /dev/null
+++ b/comm/mailnews/import/src/nsAddrDatabase.cpp
@@ -0,0 +1,864 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// this file implements the nsAddrDatabase interface using the MDB Interface.
+
+#include "nsAddrDatabase.h"
+#include "nsMsgUtils.h"
+#include "nsIMdbFactoryFactory.h"
+#include "nsSimpleEnumerator.h"
+
+#define kAddressCharSetColumn "AddrCharSet"
+#define kMailListName "ListName"
+#define kMailListNickName "ListNickName"
+#define kMailListDescription "ListDescription"
+#define kMailListTotalAddresses "ListTotalAddresses"
+// not shown in the UI
+#define kLowerPriEmailColumn "LowercasePrimaryEmail"
+#define kLower2ndEmailColumn "LowercaseSecondEmail"
+
+#define ID_PAB_TABLE 1
+
+static const char kPabTableKind[] = "ns:addrbk:db:table:kind:pab";
+static const char kDeletedCardsTableKind[] =
+ "ns:addrbk:db:table:kind:deleted"; // this table is used to keep the
+ // deleted cards
+
+static const char kCardRowScope[] = "ns:addrbk:db:row:scope:card:all";
+static const char kListRowScope[] = "ns:addrbk:db:row:scope:list:all";
+static const char kDataRowScope[] = "ns:addrbk:db:row:scope:data:all";
+
+#define COLUMN_STR_MAX 16
+
+static const char kRecordKeyColumn[] = "RecordKey";
+static const char kLastRecordKeyColumn[] = "LastRecordKey";
+static const char kRowIDProperty[] = "DbRowID";
+
+static const char kLowerListNameColumn[] = "LowercaseListName";
+
+struct mdbOid gAddressBookTableOID;
+
+static const char kMailListAddressFormat[] = "Address%d";
+
+nsAddrDatabase::nsAddrDatabase()
+ : m_mdbEnv(nullptr),
+ m_mdbStore(nullptr),
+ m_mdbPabTable(nullptr),
+ m_mdbTokensInitialized(false),
+ m_PabTableKind(0),
+ m_DeletedCardsTableKind(0),
+ m_CardRowScopeToken(0),
+ m_UIDColumnToken(0),
+ m_FirstNameColumnToken(0),
+ m_LastNameColumnToken(0),
+ m_PhoneticFirstNameColumnToken(0),
+ m_PhoneticLastNameColumnToken(0),
+ m_DisplayNameColumnToken(0),
+ m_NickNameColumnToken(0),
+ m_PriEmailColumnToken(0),
+ m_2ndEmailColumnToken(0),
+ m_WorkPhoneColumnToken(0),
+ m_HomePhoneColumnToken(0),
+ m_FaxColumnToken(0),
+ m_PagerColumnToken(0),
+ m_CellularColumnToken(0),
+ m_WorkPhoneTypeColumnToken(0),
+ m_HomePhoneTypeColumnToken(0),
+ m_FaxTypeColumnToken(0),
+ m_PagerTypeColumnToken(0),
+ m_CellularTypeColumnToken(0),
+ m_HomeAddressColumnToken(0),
+ m_HomeAddress2ColumnToken(0),
+ m_HomeCityColumnToken(0),
+ m_HomeStateColumnToken(0),
+ m_HomeZipCodeColumnToken(0),
+ m_HomeCountryColumnToken(0),
+ m_WorkAddressColumnToken(0),
+ m_WorkAddress2ColumnToken(0),
+ m_WorkCityColumnToken(0),
+ m_WorkStateColumnToken(0),
+ m_WorkZipCodeColumnToken(0),
+ m_WorkCountryColumnToken(0),
+ m_CompanyColumnToken(0),
+ m_AimScreenNameColumnToken(0),
+ m_AnniversaryYearColumnToken(0),
+ m_AnniversaryMonthColumnToken(0),
+ m_AnniversaryDayColumnToken(0),
+ m_SpouseNameColumnToken(0),
+ m_FamilyNameColumnToken(0),
+ m_DefaultAddressColumnToken(0),
+ m_CategoryColumnToken(0),
+ m_WebPage1ColumnToken(0),
+ m_WebPage2ColumnToken(0),
+ m_BirthYearColumnToken(0),
+ m_BirthMonthColumnToken(0),
+ m_BirthDayColumnToken(0),
+ m_Custom1ColumnToken(0),
+ m_Custom2ColumnToken(0),
+ m_Custom3ColumnToken(0),
+ m_Custom4ColumnToken(0),
+ m_NotesColumnToken(0),
+ m_LastModDateColumnToken(0),
+ m_PopularityIndexColumnToken(0),
+ m_AddressCharSetColumnToken(0) {}
+
+nsAddrDatabase::~nsAddrDatabase() {
+ Close(false); // better have already been closed.
+
+ // RemoveFromCache(this);
+ // clean up after ourself!
+ if (m_mdbPabTable) m_mdbPabTable->Release();
+ NS_IF_RELEASE(m_mdbStore);
+ NS_IF_RELEASE(m_mdbEnv);
+}
+
+nsresult nsAddrDatabase::GetMDBFactory(nsIMdbFactory** aMdbFactory) {
+ if (!mMdbFactory) {
+ nsresult rv;
+ nsCOMPtr<nsIMdbFactoryService> mdbFactoryService =
+ do_GetService("@mozilla.org/db/mork;1", &rv);
+ if (NS_SUCCEEDED(rv) && mdbFactoryService) {
+ rv = mdbFactoryService->GetMdbFactory(getter_AddRefs(mMdbFactory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mMdbFactory) return NS_ERROR_FAILURE;
+ }
+ }
+ NS_ADDREF(*aMdbFactory = mMdbFactory);
+ return NS_OK;
+}
+
+nsresult nsAddrDatabase::SetDbPath(nsIFile* aDbPath) {
+ return aDbPath->Clone(getter_AddRefs(m_dbName));
+}
+
+// Open the MDB database synchronously. If successful, this routine
+// will set up the m_mdbStore and m_mdbEnv of the database object
+// so other database calls can work.
+nsresult nsAddrDatabase::OpenMDB(nsIFile* dbName, bool create) {
+ nsCOMPtr<nsIMdbFactory> mdbFactory;
+ nsresult ret = GetMDBFactory(getter_AddRefs(mdbFactory));
+ NS_ENSURE_SUCCESS(ret, ret);
+
+ ret = mdbFactory->MakeEnv(NULL, &m_mdbEnv);
+ if (NS_SUCCEEDED(ret)) {
+ nsIMdbThumb* thumb = nullptr;
+
+ PathString filePath = dbName->NativePath();
+
+ nsIMdbHeap* dbHeap = nullptr;
+
+ if (m_mdbEnv) m_mdbEnv->SetAutoClear(true);
+
+ bool dbNameExists = false;
+ ret = dbName->Exists(&dbNameExists);
+ NS_ENSURE_SUCCESS(ret, ret);
+
+ if (!dbNameExists)
+ ret = NS_ERROR_FILE_NOT_FOUND;
+ else {
+ mdbOpenPolicy inOpenPolicy;
+ mdb_bool canOpen;
+ mdbYarn outFormatVersion;
+ nsIMdbFile* oldFile = nullptr;
+ int64_t fileSize;
+ ret = dbName->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(ret, ret);
+
+ ret = mdbFactory->OpenOldFile(
+ m_mdbEnv, dbHeap, filePath.get(),
+ mdbBool_kFalse, // not readonly, we want modifiable
+ &oldFile);
+ if (oldFile) {
+ if (NS_SUCCEEDED(ret)) {
+ ret = mdbFactory->CanOpenFilePort(m_mdbEnv,
+ oldFile, // the file to investigate
+ &canOpen, &outFormatVersion);
+ if (NS_SUCCEEDED(ret) && canOpen) {
+ inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0;
+ inOpenPolicy.mOpenPolicy_MinMemory = 0;
+ inOpenPolicy.mOpenPolicy_MaxLazy = 0;
+
+ ret = mdbFactory->OpenFileStore(m_mdbEnv, dbHeap, oldFile,
+ &inOpenPolicy, &thumb);
+ } else if (fileSize != 0)
+ ret = NS_ERROR_FILE_ACCESS_DENIED;
+ }
+ NS_RELEASE(oldFile); // always release our file ref, store has own
+ }
+ if (NS_FAILED(ret)) ret = NS_ERROR_FILE_ACCESS_DENIED;
+ }
+
+ if (NS_SUCCEEDED(ret) && thumb) {
+ mdb_count outTotal; // total somethings to do in operation
+ mdb_count outCurrent; // subportion of total completed so far
+ mdb_bool outDone = false; // is operation finished?
+ mdb_bool outBroken; // is operation irreparably dead and broken?
+ do {
+ ret = thumb->DoMore(m_mdbEnv, &outTotal, &outCurrent, &outDone,
+ &outBroken);
+ if (NS_FAILED(ret)) {
+ outDone = true;
+ break;
+ }
+ } while (NS_SUCCEEDED(ret) && !outBroken && !outDone);
+ if (NS_SUCCEEDED(ret) && outDone) {
+ ret = mdbFactory->ThumbToOpenStore(m_mdbEnv, thumb, &m_mdbStore);
+ if (NS_SUCCEEDED(ret) && m_mdbStore) {
+ ret = InitExistingDB();
+ create = false;
+ }
+ }
+ } else if (create && ret != NS_ERROR_FILE_ACCESS_DENIED) {
+ ret = NS_ERROR_NOT_IMPLEMENTED;
+ }
+ NS_IF_RELEASE(thumb);
+ }
+ return ret;
+}
+
+nsresult nsAddrDatabase::CloseMDB(bool commit) {
+ if (commit) return NS_ERROR_NOT_IMPLEMENTED;
+ //??? RemoveFromCache(this); // if we've closed it, better not leave it in
+ // the cache.
+ return NS_OK;
+}
+
+// force the database to close - this'll flush out anybody holding onto
+// a database without having a listener!
+// This is evil in the com world, but there are times we need to delete the
+// file.
+nsresult nsAddrDatabase::ForceClosed() {
+ nsresult err = NS_OK;
+
+ // make sure someone has a reference so object won't get deleted out from
+ // under us.
+ // NS_ADDREF_THIS();
+ // OK, remove from cache first and close the store.
+ // RemoveFromCache(this);
+
+ err = CloseMDB(false); // since we're about to delete it, no need to commit.
+ NS_IF_RELEASE(m_mdbStore);
+ // NS_RELEASE_THIS();
+ return err;
+}
+
+nsresult nsAddrDatabase::Close(bool forceCommit /* = TRUE */) {
+ return CloseMDB(forceCommit);
+}
+
+nsresult nsAddrDatabase::InitExistingDB() {
+ nsresult err = InitMDBInfo();
+ if (NS_SUCCEEDED(err)) {
+ if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NULL_POINTER;
+
+ err = m_mdbStore->GetTable(m_mdbEnv, &gAddressBookTableOID, &m_mdbPabTable);
+ if (NS_SUCCEEDED(err) && m_mdbPabTable) {
+ // This code has always run here. Removing it fails an assertion in the
+ // Mork code which indicates a bad state. In the interest of saving
+ // effort, and since this whole file is doomed after the next release,
+ // I'm leaving it behind.
+ nsIMdbTableRowCursor* rowCursor = nullptr;
+ nsIMdbRow* findRow = nullptr;
+ mdb_pos rowPos = 0;
+
+ err = m_mdbPabTable->GetTableRowCursor(m_mdbEnv, -1, &rowCursor);
+ if (NS_SUCCEEDED(err) && rowCursor) {
+ do {
+ err = rowCursor->NextRow(m_mdbEnv, &findRow, &rowPos);
+ } while (NS_SUCCEEDED(err) && findRow);
+ rowCursor->Release();
+ }
+ }
+ }
+ return err;
+}
+
+// initialize the various tokens and tables in our db's env
+nsresult nsAddrDatabase::InitMDBInfo() {
+ nsresult err = NS_OK;
+
+ if (!m_mdbTokensInitialized && m_mdbStore && m_mdbEnv) {
+ m_mdbTokensInitialized = true;
+ err = m_mdbStore->StringToToken(m_mdbEnv, kCardRowScope,
+ &m_CardRowScopeToken);
+ err = m_mdbStore->StringToToken(m_mdbEnv, kListRowScope,
+ &m_ListRowScopeToken);
+ err = m_mdbStore->StringToToken(m_mdbEnv, kDataRowScope,
+ &m_DataRowScopeToken);
+ gAddressBookTableOID.mOid_Scope = m_CardRowScopeToken;
+ gAddressBookTableOID.mOid_Id = ID_PAB_TABLE;
+ if (NS_SUCCEEDED(err)) {
+ m_mdbStore->StringToToken(m_mdbEnv, kUIDProperty, &m_UIDColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kFirstNameProperty,
+ &m_FirstNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLastNameProperty,
+ &m_LastNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPhoneticFirstNameProperty,
+ &m_PhoneticFirstNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPhoneticLastNameProperty,
+ &m_PhoneticLastNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kDisplayNameProperty,
+ &m_DisplayNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kNicknameProperty,
+ &m_NickNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPriEmailProperty,
+ &m_PriEmailColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLowerPriEmailColumn,
+ &m_LowerPriEmailColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, k2ndEmailProperty,
+ &m_2ndEmailColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLower2ndEmailColumn,
+ &m_Lower2ndEmailColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPopularityIndexProperty,
+ &m_PopularityIndexColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkPhoneProperty,
+ &m_WorkPhoneColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomePhoneProperty,
+ &m_HomePhoneColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kFaxProperty, &m_FaxColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPagerProperty, &m_PagerColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCellularProperty,
+ &m_CellularColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkPhoneTypeProperty,
+ &m_WorkPhoneTypeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomePhoneTypeProperty,
+ &m_HomePhoneTypeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kFaxTypeProperty,
+ &m_FaxTypeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kPagerTypeProperty,
+ &m_PagerTypeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCellularTypeProperty,
+ &m_CellularTypeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeAddressProperty,
+ &m_HomeAddressColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeAddress2Property,
+ &m_HomeAddress2ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeCityProperty,
+ &m_HomeCityColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeStateProperty,
+ &m_HomeStateColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeZipCodeProperty,
+ &m_HomeZipCodeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeCountryProperty,
+ &m_HomeCountryColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkAddressProperty,
+ &m_WorkAddressColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkAddress2Property,
+ &m_WorkAddress2ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkCityProperty,
+ &m_WorkCityColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkStateProperty,
+ &m_WorkStateColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkZipCodeProperty,
+ &m_WorkZipCodeColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkCountryProperty,
+ &m_WorkCountryColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kJobTitleProperty,
+ &m_JobTitleColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kDepartmentProperty,
+ &m_DepartmentColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCompanyProperty,
+ &m_CompanyColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kScreenNameProperty,
+ &m_AimScreenNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kAnniversaryYearProperty,
+ &m_AnniversaryYearColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kAnniversaryMonthProperty,
+ &m_AnniversaryMonthColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kAnniversaryDayProperty,
+ &m_AnniversaryDayColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kSpouseNameProperty,
+ &m_SpouseNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kFamilyNameProperty,
+ &m_FamilyNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kWorkWebPageProperty,
+ &m_WebPage1ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kHomeWebPageProperty,
+ &m_WebPage2ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kBirthYearProperty,
+ &m_BirthYearColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kBirthMonthProperty,
+ &m_BirthMonthColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kBirthDayProperty,
+ &m_BirthDayColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCustom1Property,
+ &m_Custom1ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCustom2Property,
+ &m_Custom2ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCustom3Property,
+ &m_Custom3ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kCustom4Property,
+ &m_Custom4ColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kNotesProperty, &m_NotesColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLastModifiedDateProperty,
+ &m_LastModDateColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kRecordKeyColumn,
+ &m_RecordKeyColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kAddressCharSetColumn,
+ &m_AddressCharSetColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLastRecordKeyColumn,
+ &m_LastRecordKeyColumnToken);
+
+ err = m_mdbStore->StringToToken(m_mdbEnv, kPabTableKind, &m_PabTableKind);
+
+ m_mdbStore->StringToToken(m_mdbEnv, kMailListName,
+ &m_ListNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kMailListNickName,
+ &m_ListNickNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kMailListDescription,
+ &m_ListDescriptionColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kMailListTotalAddresses,
+ &m_ListTotalColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kLowerListNameColumn,
+ &m_LowerListNameColumnToken);
+ m_mdbStore->StringToToken(m_mdbEnv, kDeletedCardsTableKind,
+ &m_DeletedCardsTableKind);
+ }
+ }
+ return err;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+uint32_t nsAddrDatabase::GetListAddressTotal(nsIMdbRow* listRow) {
+ uint32_t count = 0;
+ GetIntColumn(listRow, m_ListTotalColumnToken, &count, 0);
+ return count;
+}
+
+nsresult nsAddrDatabase::GetAddressRowByPos(nsIMdbRow* listRow, uint16_t pos,
+ nsIMdbRow** cardRow) {
+ if (!m_mdbStore || !listRow || !cardRow || !m_mdbEnv)
+ return NS_ERROR_NULL_POINTER;
+
+ mdb_token listAddressColumnToken;
+
+ char columnStr[COLUMN_STR_MAX];
+ PR_snprintf(columnStr, COLUMN_STR_MAX, kMailListAddressFormat, pos);
+ m_mdbStore->StringToToken(m_mdbEnv, columnStr, &listAddressColumnToken);
+
+ nsAutoString tempString;
+ mdb_id rowID;
+ nsresult err =
+ GetIntColumn(listRow, listAddressColumnToken, (uint32_t*)&rowID, 0);
+ NS_ENSURE_SUCCESS(err, err);
+
+ return GetCardRowByRowID(rowID, cardRow);
+}
+
+nsresult nsAddrDatabase::GetStringColumn(nsIMdbRow* cardRow, mdb_token outToken,
+ nsString& str) {
+ nsresult err = NS_ERROR_NULL_POINTER;
+ nsIMdbCell* cardCell;
+
+ if (cardRow && m_mdbEnv) {
+ err = cardRow->GetCell(m_mdbEnv, outToken, &cardCell);
+ if (NS_SUCCEEDED(err) && cardCell) {
+ struct mdbYarn yarn;
+ cardCell->AliasYarn(m_mdbEnv, &yarn);
+ NS_ConvertUTF8toUTF16 uniStr((const char*)yarn.mYarn_Buf,
+ yarn.mYarn_Fill);
+ if (!uniStr.IsEmpty())
+ str.Assign(uniStr);
+ else
+ err = NS_ERROR_FAILURE;
+ cardCell->Release(); // always release ref
+ } else
+ err = NS_ERROR_FAILURE;
+ }
+ return err;
+}
+
+void nsAddrDatabase::YarnToUInt32(struct mdbYarn* yarn, uint32_t* pResult) {
+ uint8_t numChars = std::min<mdb_fill>(8, yarn->mYarn_Fill);
+ *pResult = MsgUnhex((char*)yarn->mYarn_Buf, numChars);
+}
+
+nsresult nsAddrDatabase::GetIntColumn(nsIMdbRow* cardRow, mdb_token outToken,
+ uint32_t* pValue, uint32_t defaultValue) {
+ nsresult err = NS_ERROR_NULL_POINTER;
+ nsIMdbCell* cardCell;
+
+ if (pValue) *pValue = defaultValue;
+ if (cardRow && m_mdbEnv) {
+ err = cardRow->GetCell(m_mdbEnv, outToken, &cardCell);
+ if (NS_SUCCEEDED(err) && cardCell) {
+ struct mdbYarn yarn;
+ cardCell->AliasYarn(m_mdbEnv, &yarn);
+ YarnToUInt32(&yarn, pValue);
+ cardCell->Release();
+ } else
+ err = NS_ERROR_FAILURE;
+ }
+ return err;
+}
+
+nsresult nsAddrDatabase::InitCardFromRow(nsIAbCard* newCard,
+ nsIMdbRow* cardRow) {
+ nsresult rv = NS_OK;
+ if (!newCard || !cardRow || !m_mdbEnv) return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIMdbRowCellCursor> cursor;
+ nsCOMPtr<nsIMdbCell> cell;
+
+ rv = cardRow->GetRowCellCursor(m_mdbEnv, -1, getter_AddRefs(cursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mdb_column columnNumber;
+ char columnName[100];
+ struct mdbYarn colYarn = {columnName, 0, sizeof(columnName), 0, 0, nullptr};
+ struct mdbYarn cellYarn;
+
+ do {
+ rv = cursor->NextCell(m_mdbEnv, getter_AddRefs(cell), &columnNumber,
+ nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!cell) break;
+
+ // Get the value of the cell
+ cell->AliasYarn(m_mdbEnv, &cellYarn);
+ NS_ConvertUTF8toUTF16 value(static_cast<const char*>(cellYarn.mYarn_Buf),
+ cellYarn.mYarn_Fill);
+
+ if (!value.IsEmpty()) {
+ // Get the column of the cell
+ // Mork makes this so hard...
+ rv = m_mdbStore->TokenToString(m_mdbEnv, columnNumber, &colYarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char* name =
+ PL_strndup(static_cast<char*>(colYarn.mYarn_Buf), colYarn.mYarn_Fill);
+ newCard->SetPropertyAsAString(name, value);
+ PL_strfree(name);
+ }
+ } while (true);
+
+ uint32_t key = 0;
+ rv = GetIntColumn(cardRow, m_RecordKeyColumnToken, &key, 0);
+ if (NS_SUCCEEDED(rv)) newCard->SetPropertyAsUint32(kRecordKeyColumn, key);
+
+ return NS_OK;
+}
+
+nsresult nsAddrDatabase::GetListCardFromDB(nsIAbCard* listCard,
+ nsIMdbRow* listRow) {
+ nsresult err = NS_OK;
+ if (!listCard || !listRow) return NS_ERROR_NULL_POINTER;
+
+ nsAutoString tempString;
+
+ err = GetStringColumn(listRow, m_UIDColumnToken, tempString);
+ if (NS_SUCCEEDED(err) && !tempString.IsEmpty()) {
+ listCard->SetPropertyAsAString(kUIDProperty, tempString);
+ }
+ err = GetStringColumn(listRow, m_ListNameColumnToken, tempString);
+ if (NS_SUCCEEDED(err) && !tempString.IsEmpty()) {
+ listCard->SetDisplayName(tempString);
+ listCard->SetLastName(tempString);
+ }
+ err = GetStringColumn(listRow, m_ListNickNameColumnToken, tempString);
+ if (NS_SUCCEEDED(err) && !tempString.IsEmpty()) {
+ listCard->SetPropertyAsAString(kNicknameProperty, tempString);
+ }
+ err = GetStringColumn(listRow, m_ListDescriptionColumnToken, tempString);
+ if (NS_SUCCEEDED(err) && !tempString.IsEmpty()) {
+ listCard->SetPropertyAsAString(kNotesProperty, tempString);
+ }
+ uint32_t key = 0;
+ err = GetIntColumn(listRow, m_RecordKeyColumnToken, &key, 0);
+ if (NS_SUCCEEDED(err)) listCard->SetPropertyAsUint32(kRecordKeyColumn, key);
+ return err;
+}
+
+class nsAddrDBEnumerator : public nsSimpleEnumerator {
+ public:
+ const nsID& DefaultInterface() override { return NS_GET_IID(nsIAbCard); }
+
+ // nsISimpleEnumerator methods:
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ // nsAddrDBEnumerator methods:
+ explicit nsAddrDBEnumerator(nsAddrDatabase* aDb);
+ void Clear();
+
+ protected:
+ nsAddrDatabase* mDb;
+ nsIMdbTable* mDbTable;
+ nsCOMPtr<nsIMdbTableRowCursor> mRowCursor;
+ nsCOMPtr<nsIMdbRow> mCurrentRow;
+ mdb_pos mRowPos;
+};
+
+nsAddrDBEnumerator::nsAddrDBEnumerator(nsAddrDatabase* aDb)
+ : mDb(aDb), mDbTable(aDb->GetPabTable()), mRowPos(-1) {}
+
+NS_IMETHODIMP
+nsAddrDBEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ if (!mDbTable || !mDb->GetEnv()) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
+ mDbTable->GetTableRowCursor(mDb->GetEnv(), mRowPos,
+ getter_AddRefs(rowCursor));
+ NS_ENSURE_TRUE(rowCursor, NS_ERROR_FAILURE);
+
+ mdbOid rowOid;
+ rowCursor->NextRowOid(mDb->GetEnv(), &rowOid, nullptr);
+ while (rowOid.mOid_Id != (mdb_id)-1) {
+ if (mDb->IsListRowScopeToken(rowOid.mOid_Scope) ||
+ mDb->IsCardRowScopeToken(rowOid.mOid_Scope)) {
+ *aResult = true;
+
+ return NS_OK;
+ }
+
+ if (!mDb->IsDataRowScopeToken(rowOid.mOid_Scope)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rowCursor->NextRowOid(mDb->GetEnv(), &rowOid, nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAddrDBEnumerator::GetNext(nsISupports** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+
+ if (!mDbTable || !mDb->GetEnv()) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!mRowCursor) {
+ mDbTable->GetTableRowCursor(mDb->GetEnv(), -1, getter_AddRefs(mRowCursor));
+ NS_ENSURE_TRUE(mRowCursor, NS_ERROR_FAILURE);
+ }
+
+ nsCOMPtr<nsIAbCard> resultCard;
+ mRowCursor->NextRow(mDb->GetEnv(), getter_AddRefs(mCurrentRow), &mRowPos);
+ while (mCurrentRow) {
+ mdbOid rowOid;
+ if (NS_SUCCEEDED(mCurrentRow->GetOid(mDb->GetEnv(), &rowOid))) {
+ nsresult rv;
+ if (mDb->IsListRowScopeToken(rowOid.mOid_Scope)) {
+ rv = mDb->CreateABListCard(mCurrentRow, getter_AddRefs(resultCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (mDb->IsCardRowScopeToken(rowOid.mOid_Scope)) {
+ rv = mDb->CreateABCard(mCurrentRow, 0, getter_AddRefs(resultCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (!mDb->IsDataRowScopeToken(rowOid.mOid_Scope)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (resultCard) {
+ return CallQueryInterface(resultCard, aResult);
+ }
+ }
+
+ mRowCursor->NextRow(mDb->GetEnv(), getter_AddRefs(mCurrentRow), &mRowPos);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+class nsListAddressEnumerator final : public nsSimpleEnumerator {
+ public:
+ const nsID& DefaultInterface() override { return NS_GET_IID(nsIAbCard); }
+
+ // nsISimpleEnumerator methods:
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ // nsListAddressEnumerator methods:
+ nsListAddressEnumerator(nsAddrDatabase* aDb, mdb_id aRowID);
+
+ protected:
+ ~nsListAddressEnumerator() override = default;
+ nsAddrDatabase* mDb;
+ nsIMdbTable* mDbTable;
+ nsCOMPtr<nsIMdbRow> mListRow;
+ mdb_id mListRowID;
+ uint32_t mAddressTotal;
+ uint16_t mAddressPos;
+};
+
+nsListAddressEnumerator::nsListAddressEnumerator(nsAddrDatabase* aDb,
+ mdb_id aRowID)
+ : mDb(aDb),
+ mDbTable(aDb->GetPabTable()),
+ mListRowID(aRowID),
+ mAddressPos(0) {
+ mDb->GetListRowByRowID(mListRowID, getter_AddRefs(mListRow));
+ mAddressTotal = aDb->GetListAddressTotal(mListRow);
+}
+
+NS_IMETHODIMP
+nsListAddressEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+
+ if (!mDbTable || !mDb->GetEnv()) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // In some cases it is possible that GetAddressRowByPos returns success,
+ // but currentRow is null. This is typically due to the fact that a card
+ // has been deleted from the parent and not the list. Whilst we have fixed
+ // that there are still a few dbs around there that we need to support
+ // correctly. Therefore, whilst processing lists ensure that we don't return
+ // false if the only thing stopping us is a blank row, just skip it and try
+ // the next one.
+ while (mAddressPos < mAddressTotal) {
+ nsCOMPtr<nsIMdbRow> currentRow;
+ nsresult rv = mDb->GetAddressRowByPos(mListRow, mAddressPos + 1,
+ getter_AddRefs(currentRow));
+
+ if (NS_SUCCEEDED(rv) && currentRow) {
+ *aResult = true;
+ break;
+ }
+
+ ++mAddressPos;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsListAddressEnumerator::GetNext(nsISupports** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = nullptr;
+
+ if (!mDbTable || !mDb->GetEnv()) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ while (++mAddressPos <= mAddressTotal) {
+ nsCOMPtr<nsIMdbRow> currentRow;
+ nsresult rv = mDb->GetAddressRowByPos(mListRow, mAddressPos,
+ getter_AddRefs(currentRow));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIAbCard> resultCard;
+ rv =
+ mDb->CreateABCard(currentRow, mListRowID, getter_AddRefs(resultCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(resultCard, aResult);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsAddrDatabase::EnumerateCards(nsISimpleEnumerator** result) {
+ NS_ADDREF(*result = new nsAddrDBEnumerator(this));
+ return NS_OK;
+}
+
+nsresult nsAddrDatabase::EnumerateListAddresses(uint32_t listRowID,
+ nsISimpleEnumerator** result) {
+ NS_ADDREF(*result = new nsListAddressEnumerator(this, listRowID));
+ return NS_OK;
+}
+
+nsresult nsAddrDatabase::CreateCard(nsIMdbRow* cardRow, mdb_id listRowID,
+ nsIAbCard** result) {
+ if (!cardRow || !m_mdbEnv || !result) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv = NS_OK;
+
+ mdbOid outOid;
+ mdb_id rowID = 0;
+
+ if (NS_SUCCEEDED(cardRow->GetOid(m_mdbEnv, &outOid))) rowID = outOid.mOid_Id;
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIAbCard> personCard;
+ personCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ InitCardFromRow(personCard, cardRow);
+ personCard->SetPropertyAsUint32(kRowIDProperty, rowID);
+
+ personCard.forget(result);
+ }
+
+ return rv;
+}
+
+nsresult nsAddrDatabase::CreateABCard(nsIMdbRow* cardRow, mdb_id listRowID,
+ nsIAbCard** result) {
+ return CreateCard(cardRow, listRowID, result);
+}
+
+/* create a card for mailing list in the address book */
+nsresult nsAddrDatabase::CreateABListCard(nsIMdbRow* listRow,
+ nsIAbCard** result) {
+ if (!listRow || !m_mdbEnv || !result) return NS_ERROR_NULL_POINTER;
+
+ nsresult rv = NS_OK;
+
+ mdbOid outOid;
+ mdb_id rowID = 0;
+
+ if (NS_SUCCEEDED(listRow->GetOid(m_mdbEnv, &outOid))) rowID = outOid.mOid_Id;
+
+ char* listURI = nullptr;
+
+ nsAutoString fileName;
+ rv = m_dbName->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ listURI = PR_smprintf("MailList%ld", rowID);
+
+ nsCOMPtr<nsIAbCard> personCard;
+ personCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (personCard) {
+ GetListCardFromDB(personCard, listRow);
+
+ personCard->SetPropertyAsUint32(kRowIDProperty, rowID);
+ personCard->SetIsMailList(true);
+ personCard->SetMailListURI(listURI);
+ }
+
+ personCard.forget(result);
+ if (listURI) PR_smprintf_free(listURI);
+
+ return rv;
+}
+
+nsresult nsAddrDatabase::GetCardRowByRowID(mdb_id rowID, nsIMdbRow** dbRow) {
+ if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NULL_POINTER;
+
+ mdbOid rowOid;
+ rowOid.mOid_Scope = m_CardRowScopeToken;
+ rowOid.mOid_Id = rowID;
+
+ return m_mdbStore->GetRow(m_mdbEnv, &rowOid, dbRow);
+}
+
+nsresult nsAddrDatabase::GetListRowByRowID(mdb_id rowID, nsIMdbRow** dbRow) {
+ if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NULL_POINTER;
+
+ mdbOid rowOid;
+ rowOid.mOid_Scope = m_ListRowScopeToken;
+ rowOid.mOid_Id = rowID;
+
+ return m_mdbStore->GetRow(m_mdbEnv, &rowOid, dbRow);
+}
diff --git a/comm/mailnews/import/src/nsAddrDatabase.h b/comm/mailnews/import/src/nsAddrDatabase.h
new file mode 100644
index 0000000000..d667c92e47
--- /dev/null
+++ b/comm/mailnews/import/src/nsAddrDatabase.h
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsAddrDatabase_H_
+#define _nsAddrDatabase_H_
+
+#include "nsIAbCard.h"
+#include "nsIFile.h"
+#include "mdb.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+class nsAddrDatabase {
+ using PathString = mozilla::PathString;
+
+ public:
+ nsresult SetDbPath(nsIFile* aDbPath);
+ nsresult Close(bool forceCommit);
+ nsresult OpenMDB(nsIFile* dbName, bool create);
+ nsresult CloseMDB(bool commit);
+ nsresult ForceClosed(void);
+ nsresult EnumerateCards(nsISimpleEnumerator** _retval);
+ nsresult EnumerateListAddresses(uint32_t listRowID,
+ nsISimpleEnumerator** _retval);
+
+ nsAddrDatabase();
+ virtual ~nsAddrDatabase();
+
+ nsresult GetMDBFactory(nsIMdbFactory** aMdbFactory);
+ nsIMdbEnv* GetEnv() { return m_mdbEnv; }
+ uint32_t GetCurVersion();
+ nsIMdbTableRowCursor* GetTableRowCursor();
+ nsIMdbTable* GetPabTable() { return m_mdbPabTable; }
+
+ nsresult CreateABCard(nsIMdbRow* cardRow, mdb_id listRowID,
+ nsIAbCard** result);
+ nsresult CreateABListCard(nsIMdbRow* listRow, nsIAbCard** result);
+
+ bool IsListRowScopeToken(mdb_scope scope) {
+ return (scope == m_ListRowScopeToken) ? true : false;
+ }
+ bool IsCardRowScopeToken(mdb_scope scope) {
+ return (scope == m_CardRowScopeToken) ? true : false;
+ }
+ bool IsDataRowScopeToken(mdb_scope scope) {
+ return (scope == m_DataRowScopeToken) ? true : false;
+ }
+ nsresult GetCardRowByRowID(mdb_id rowID, nsIMdbRow** dbRow);
+ nsresult GetListRowByRowID(mdb_id rowID, nsIMdbRow** dbRow);
+
+ uint32_t GetListAddressTotal(nsIMdbRow* listRow);
+ nsresult GetAddressRowByPos(nsIMdbRow* listRow, uint16_t pos,
+ nsIMdbRow** cardRow);
+
+ nsresult InitCardFromRow(nsIAbCard* aNewCard, nsIMdbRow* aCardRow);
+
+ protected:
+ void YarnToUInt32(struct mdbYarn* yarn, uint32_t* pResult);
+ nsresult GetStringColumn(nsIMdbRow* cardRow, mdb_token outToken,
+ nsString& str);
+ nsresult GetIntColumn(nsIMdbRow* cardRow, mdb_token outToken,
+ uint32_t* pValue, uint32_t defaultValue);
+ nsresult GetListCardFromDB(nsIAbCard* listCard, nsIMdbRow* listRow);
+ nsresult CreateCard(nsIMdbRow* cardRow, mdb_id listRowID, nsIAbCard** result);
+
+ // mdb bookkeeping stuff
+ nsresult InitExistingDB();
+ nsresult InitMDBInfo();
+
+ nsIMdbEnv* m_mdbEnv; // to be used in all the db calls.
+ nsIMdbStore* m_mdbStore;
+ nsIMdbTable* m_mdbPabTable;
+ nsCOMPtr<nsIFile> m_dbName;
+ bool m_mdbTokensInitialized;
+
+ mdb_kind m_PabTableKind;
+ mdb_kind m_DeletedCardsTableKind;
+
+ mdb_scope m_CardRowScopeToken;
+ mdb_scope m_ListRowScopeToken;
+ mdb_scope m_DataRowScopeToken;
+
+ mdb_token m_UIDColumnToken;
+ mdb_token m_FirstNameColumnToken;
+ mdb_token m_LastNameColumnToken;
+ mdb_token m_PhoneticFirstNameColumnToken;
+ mdb_token m_PhoneticLastNameColumnToken;
+ mdb_token m_DisplayNameColumnToken;
+ mdb_token m_NickNameColumnToken;
+ mdb_token m_PriEmailColumnToken;
+ mdb_token m_2ndEmailColumnToken;
+ mdb_token m_DefaultEmailColumnToken;
+ mdb_token m_CardTypeColumnToken;
+ mdb_token m_WorkPhoneColumnToken;
+ mdb_token m_HomePhoneColumnToken;
+ mdb_token m_FaxColumnToken;
+ mdb_token m_PagerColumnToken;
+ mdb_token m_CellularColumnToken;
+ mdb_token m_WorkPhoneTypeColumnToken;
+ mdb_token m_HomePhoneTypeColumnToken;
+ mdb_token m_FaxTypeColumnToken;
+ mdb_token m_PagerTypeColumnToken;
+ mdb_token m_CellularTypeColumnToken;
+ mdb_token m_HomeAddressColumnToken;
+ mdb_token m_HomeAddress2ColumnToken;
+ mdb_token m_HomeCityColumnToken;
+ mdb_token m_HomeStateColumnToken;
+ mdb_token m_HomeZipCodeColumnToken;
+ mdb_token m_HomeCountryColumnToken;
+ mdb_token m_WorkAddressColumnToken;
+ mdb_token m_WorkAddress2ColumnToken;
+ mdb_token m_WorkCityColumnToken;
+ mdb_token m_WorkStateColumnToken;
+ mdb_token m_WorkZipCodeColumnToken;
+ mdb_token m_WorkCountryColumnToken;
+ mdb_token m_JobTitleColumnToken;
+ mdb_token m_DepartmentColumnToken;
+ mdb_token m_CompanyColumnToken;
+ mdb_token m_AimScreenNameColumnToken;
+ mdb_token m_AnniversaryYearColumnToken;
+ mdb_token m_AnniversaryMonthColumnToken;
+ mdb_token m_AnniversaryDayColumnToken;
+ mdb_token m_SpouseNameColumnToken;
+ mdb_token m_FamilyNameColumnToken;
+ mdb_token m_DefaultAddressColumnToken;
+ mdb_token m_CategoryColumnToken;
+ mdb_token m_WebPage1ColumnToken;
+ mdb_token m_WebPage2ColumnToken;
+ mdb_token m_BirthYearColumnToken;
+ mdb_token m_BirthMonthColumnToken;
+ mdb_token m_BirthDayColumnToken;
+ mdb_token m_Custom1ColumnToken;
+ mdb_token m_Custom2ColumnToken;
+ mdb_token m_Custom3ColumnToken;
+ mdb_token m_Custom4ColumnToken;
+ mdb_token m_NotesColumnToken;
+ mdb_token m_LastModDateColumnToken;
+ mdb_token m_RecordKeyColumnToken;
+ mdb_token m_LowerPriEmailColumnToken;
+ mdb_token m_Lower2ndEmailColumnToken;
+
+ mdb_token m_PopularityIndexColumnToken;
+
+ mdb_token m_AddressCharSetColumnToken;
+ mdb_token m_LastRecordKeyColumnToken;
+
+ mdb_token m_ListNameColumnToken;
+ mdb_token m_ListNickNameColumnToken;
+ mdb_token m_ListDescriptionColumnToken;
+ mdb_token m_ListTotalColumnToken;
+ mdb_token m_LowerListNameColumnToken;
+
+ nsCOMPtr<nsIMdbFactory> mMdbFactory;
+};
+
+#endif
diff --git a/comm/mailnews/import/src/nsAppleMailImport.cpp b/comm/mailnews/import/src/nsAppleMailImport.cpp
new file mode 100644
index 0000000000..0404014527
--- /dev/null
+++ b/comm/mailnews/import/src/nsAppleMailImport.cpp
@@ -0,0 +1,609 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIImportService.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIImportGeneric.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIFile.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsNetUtil.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+
+#include "nsEmlxHelperUtils.h"
+#include "nsAppleMailImport.h"
+#include "nsIOutputStream.h"
+
+// some hard-coded strings
+#define DEFAULT_MAIL_FOLDER "~/Library/Mail/"
+#define POP_MBOX_SUFFIX ".mbox"
+#define IMAP_MBOX_SUFFIX ".imapmbox"
+
+// stringbundle URI
+#define APPLEMAIL_MSGS_URL \
+ "chrome://messenger/locale/appleMailImportMsgs.properties"
+
+// magic constants
+#define kAccountMailboxID 1234
+
+nsAppleMailImportModule::nsAppleMailImportModule() {
+ IMPORT_LOG0("nsAppleMailImportModule Created");
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (bundleService)
+ bundleService->CreateBundle(APPLEMAIL_MSGS_URL, getter_AddRefs(mBundle));
+}
+
+nsAppleMailImportModule::~nsAppleMailImportModule() {
+ IMPORT_LOG0("nsAppleMailImportModule Deleted");
+}
+
+NS_IMPL_ISUPPORTS(nsAppleMailImportModule, nsIImportModule)
+
+NS_IMETHODIMP nsAppleMailImportModule::GetName(char16_t** aName) {
+ if (!mBundle) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString name;
+ nsresult rv = mBundle->GetStringFromName("ApplemailImportName", name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aName = ToNewUnicode(name);
+ return rv;
+}
+
+NS_IMETHODIMP nsAppleMailImportModule::GetDescription(char16_t** aName) {
+ if (!mBundle) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString name;
+ nsresult rv = mBundle->GetStringFromName("ApplemailImportDescription", name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aName = ToNewUnicode(name);
+ return rv;
+}
+
+NS_IMETHODIMP nsAppleMailImportModule::GetSupports(char** aSupports) {
+ NS_ENSURE_ARG_POINTER(aSupports);
+ *aSupports = strdup(NS_IMPORT_MAIL_STR);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAppleMailImportModule::GetSupportsUpgrade(bool* aUpgrade) {
+ NS_ENSURE_ARG_POINTER(aUpgrade);
+ *aUpgrade = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAppleMailImportModule::GetImportInterface(
+ const char* aImportType, nsISupports** aInterface) {
+ NS_ENSURE_ARG_POINTER(aImportType);
+ NS_ENSURE_ARG_POINTER(aInterface);
+ *aInterface = nullptr;
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ if (!strcmp(aImportType, "mail")) {
+ nsCOMPtr<nsIImportMail> mail(
+ do_CreateInstance(NS_APPLEMAILIMPL_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportGeneric> generic;
+ rv = impSvc->CreateNewGenericMail(getter_AddRefs(generic));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString name;
+ rv = mBundle->GetStringFromName("ApplemailImportName", name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> nameString(
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nameString->SetData(name);
+
+ generic->SetData("name", nameString);
+ generic->SetData("mailInterface", mail);
+
+ generic.forget(aInterface);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+#pragma mark -
+
+nsAppleMailImportMail::nsAppleMailImportMail() : mProgress(0), mCurDepth(0) {
+ IMPORT_LOG0("nsAppleMailImportMail created");
+}
+
+nsresult nsAppleMailImportMail::Initialize() {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ return bundleService->CreateBundle(APPLEMAIL_MSGS_URL,
+ getter_AddRefs(mBundle));
+}
+
+nsAppleMailImportMail::~nsAppleMailImportMail() {
+ IMPORT_LOG0("nsAppleMailImportMail destroyed");
+}
+
+NS_IMPL_ISUPPORTS(nsAppleMailImportMail, nsIImportMail)
+
+NS_IMETHODIMP nsAppleMailImportMail::GetDefaultLocation(nsIFile** aLocation,
+ bool* aFound,
+ bool* aUserVerify) {
+ NS_ENSURE_ARG_POINTER(aFound);
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(aUserVerify);
+
+ *aLocation = nullptr;
+ *aFound = false;
+ *aUserVerify = true;
+
+ // try to find current user's top-level Mail folder
+ nsCOMPtr<nsIFile> mailFolder(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+ if (mailFolder) {
+ nsresult rv =
+ mailFolder->InitWithNativePath(nsLiteralCString(DEFAULT_MAIL_FOLDER));
+ if (NS_SUCCEEDED(rv)) {
+ *aFound = true;
+ *aUserVerify = false;
+ mailFolder.forget(aLocation);
+ }
+ }
+
+ return NS_OK;
+}
+
+// this is the method that initiates all searching for mailboxes.
+// it will assume that it has a directory like ~/Library/Mail/
+NS_IMETHODIMP nsAppleMailImportMail::FindMailboxes(
+ nsIFile* aMailboxFile,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes) {
+ NS_ENSURE_ARG_POINTER(aMailboxFile);
+
+ IMPORT_LOG0("FindMailboxes for Apple mail invoked");
+
+ boxes.Clear();
+ bool exists = false;
+ nsresult rv = aMailboxFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIImportService> importService(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCurDepth = 1;
+
+ // 1. look for accounts with mailboxes
+ FindAccountMailDirs(aMailboxFile, boxes, importService);
+ mCurDepth--;
+
+ if (NS_SUCCEEDED(rv)) {
+ // 2. look for "global" mailboxes, that don't belong to any specific
+ // account. they are inside the
+ // root's Mailboxes/ folder
+ nsCOMPtr<nsIFile> mailboxesDir(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ mailboxesDir->InitWithFile(aMailboxFile);
+ rv = mailboxesDir->Append(u"Mailboxes"_ns);
+ if (NS_SUCCEEDED(rv)) {
+ IMPORT_LOG0("Looking for global Apple mailboxes");
+
+ mCurDepth++;
+ rv = FindMboxDirs(mailboxesDir, boxes, importService);
+ mCurDepth--;
+ }
+ }
+ }
+ return rv;
+}
+
+// operates on the Mail/ directory root, trying to find accounts (which are
+// folders named something like "POP-hwaara@gmail.com") and add their .mbox dirs
+void nsAppleMailImportMail::FindAccountMailDirs(
+ nsIFile* aRoot, nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService) {
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ nsresult rv = aRoot->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ if (NS_FAILED(rv)) return;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ // get the next file entry
+ nsCOMPtr<nsIFile> currentEntry;
+ directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry));
+ if (!currentEntry) continue;
+
+ // make sure it's a directory
+ bool isDirectory = false;
+ currentEntry->IsDirectory(&isDirectory);
+
+ if (isDirectory) {
+ // now let's see if it's an account folder. if so, we want to traverse it
+ // for .mbox children
+ nsAutoString folderName;
+ currentEntry->GetLeafName(folderName);
+ bool isAccountFolder = false;
+
+ if (StringBeginsWith(folderName, u"POP-"_ns)) {
+ // cut off "POP-" prefix so we get a nice folder name
+ folderName.Cut(0, 4);
+ isAccountFolder = true;
+ } else if (StringBeginsWith(folderName, u"IMAP-"_ns)) {
+ // cut off "IMAP-" prefix so we get a nice folder name
+ folderName.Cut(0, 5);
+ isAccountFolder = true;
+ }
+
+ if (isAccountFolder) {
+ IMPORT_LOG1("Found account: %s\n",
+ NS_ConvertUTF16toUTF8(folderName).get());
+
+ // create a mailbox for this account, so we get a parent for "Inbox",
+ // "Sent Messages", etc.
+ nsCOMPtr<nsIImportMailboxDescriptor> desc;
+ rv = aImportService->CreateNewMailboxDescriptor(getter_AddRefs(desc));
+ if (NS_FAILED(rv)) continue;
+ desc->SetSize(1);
+ desc->SetDepth(mCurDepth);
+ desc->SetDisplayName(folderName.get());
+ desc->SetIdentifier(kAccountMailboxID);
+
+ nsCOMPtr<nsIFile> mailboxDescFile;
+ rv = desc->GetFile(getter_AddRefs(mailboxDescFile));
+ if (NS_FAILED(rv) || !mailboxDescFile) continue;
+
+ mailboxDescFile->InitWithFile(currentEntry);
+
+ // add this mailbox descriptor to the list
+ aMailboxDescs.AppendElement(desc);
+
+ // now add all the children mailboxes
+ mCurDepth++;
+ FindMboxDirs(currentEntry, aMailboxDescs, aImportService);
+ mCurDepth--;
+ }
+ }
+ }
+}
+
+// adds the specified file as a mailboxdescriptor to the array
+nsresult nsAppleMailImportMail::AddMboxDir(
+ nsIFile* aFolder,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService) {
+ nsAutoString folderName;
+ aFolder->GetLeafName(folderName);
+
+ // cut off the suffix, if any, or prefix if this is an account folder.
+ if (StringEndsWith(folderName,
+ NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX)))
+ folderName.SetLength(folderName.Length() - 5);
+ else if (StringEndsWith(folderName,
+ NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX)))
+ folderName.SetLength(folderName.Length() - 9);
+ else if (StringBeginsWith(folderName, u"POP-"_ns))
+ folderName.Cut(4, folderName.Length());
+ else if (StringBeginsWith(folderName, u"IMAP-"_ns))
+ folderName.Cut(5, folderName.Length());
+
+ nsCOMPtr<nsIImportMailboxDescriptor> desc;
+ nsresult rv =
+ aImportService->CreateNewMailboxDescriptor(getter_AddRefs(desc));
+ if (NS_SUCCEEDED(rv)) {
+ // find out number of messages in this .mbox
+ uint32_t numMessages = 0;
+ {
+ // move to the .mbox's Messages folder
+ nsCOMPtr<nsIFile> messagesFolder;
+ aFolder->Clone(getter_AddRefs(messagesFolder));
+ nsresult rv = messagesFolder->Append(u"Messages"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // count the number of messages in this folder. it sucks that we have to
+ // iterate through the folder but XPCOM doesn't give us any way to just
+ // get the file count, unfortunately. :-(
+ nsCOMPtr<nsIDirectoryEnumerator> dirEnumerator;
+ messagesFolder->GetDirectoryEntries(getter_AddRefs(dirEnumerator));
+ if (dirEnumerator) {
+ bool hasMore = false;
+ while (NS_SUCCEEDED(dirEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIFile> file;
+ dirEnumerator->GetNextFile(getter_AddRefs(file));
+ if (file) {
+ bool isFile = false;
+ file->IsFile(&isFile);
+ if (isFile) numMessages++;
+ }
+ }
+ }
+ }
+
+ desc->SetSize(numMessages);
+ desc->SetDisplayName(folderName.get());
+ desc->SetDepth(mCurDepth);
+
+ IMPORT_LOG3("Will import %s with approx %d messages, depth is %d",
+ NS_ConvertUTF16toUTF8(folderName).get(), numMessages,
+ mCurDepth);
+
+ // XXX: this is silly. there's no setter for the mailbox descriptor's file,
+ // so we need to get it, and then modify it.
+ nsCOMPtr<nsIFile> mailboxDescFile;
+ rv = desc->GetFile(getter_AddRefs(mailboxDescFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mailboxDescFile) mailboxDescFile->InitWithFile(aFolder);
+
+ // add this mailbox descriptor to the list
+ aMailboxDescs.AppendElement(desc);
+ }
+
+ return NS_OK;
+}
+
+// Starts looking for .mbox dirs in the specified dir. The .mbox dirs contain
+// messages and can be considered leafs in a tree of nested mailboxes
+// (subfolders).
+//
+// If a mailbox has sub-mailboxes, they are contained in a sibling folder with
+// the same name without the ".mbox" part. example:
+// MyParentMailbox.mbox/
+// MyParentMailbox/
+// MyChildMailbox.mbox/
+// MyOtherChildMailbox.mbox/
+//
+nsresult nsAppleMailImportMail::FindMboxDirs(
+ nsIFile* aFolder,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aImportService);
+
+ // make sure this is a directory.
+ bool isDir = false;
+ if (NS_FAILED(aFolder->IsDirectory(&isDir)) || !isDir)
+ return NS_ERROR_FAILURE;
+
+ // iterate through the folder contents
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ nsresult rv =
+ aFolder->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ if (NS_FAILED(rv) || !directoryEnumerator) return rv;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ // get the next file entry
+ nsCOMPtr<nsIFile> currentEntry;
+ directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry));
+ if (!currentEntry) continue;
+
+ // we only care about directories...
+ if (NS_FAILED(currentEntry->IsDirectory(&isDir)) || !isDir) continue;
+
+ // now find out if this is a .mbox dir
+ nsAutoString currentFolderName;
+ if (NS_SUCCEEDED(currentEntry->GetLeafName(currentFolderName)) &&
+ (StringEndsWith(currentFolderName,
+ NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX)) ||
+ StringEndsWith(currentFolderName,
+ NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX)))) {
+ IMPORT_LOG1("Adding .mbox dir: %s",
+ NS_ConvertUTF16toUTF8(currentFolderName).get());
+
+ // add this .mbox
+ rv = AddMboxDir(currentEntry, aMailboxDescs, aImportService);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1("Couldn't add .mbox for import: %s ... continuing anyway",
+ NS_ConvertUTF16toUTF8(currentFolderName).get());
+ continue;
+ }
+
+ // see if this .mbox dir has any sub-mailboxes
+ nsAutoString siblingMailboxDirPath;
+ currentEntry->GetPath(siblingMailboxDirPath);
+
+ // cut off suffix
+ if (StringEndsWith(siblingMailboxDirPath,
+ NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX)))
+ siblingMailboxDirPath.SetLength(siblingMailboxDirPath.Length() - 9);
+ else if (StringEndsWith(siblingMailboxDirPath,
+ NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX)))
+ siblingMailboxDirPath.SetLength(siblingMailboxDirPath.Length() - 5);
+
+ IMPORT_LOG1("trying to locate a '%s'",
+ NS_ConvertUTF16toUTF8(siblingMailboxDirPath).get());
+ nsCOMPtr<nsIFile> siblingMailboxDir(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) continue;
+
+ rv = siblingMailboxDir->InitWithPath(siblingMailboxDirPath);
+ bool reallyExists = false;
+ siblingMailboxDir->Exists(&reallyExists);
+
+ if (NS_SUCCEEDED(rv) && reallyExists) {
+ IMPORT_LOG1("Found what looks like an .mbox container: %s",
+ NS_ConvertUTF16toUTF8(currentFolderName).get());
+
+ // traverse this folder for other .mboxes
+ mCurDepth++;
+ FindMboxDirs(siblingMailboxDir, aMailboxDescs, aImportService);
+ mCurDepth--;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppleMailImportMail::ImportMailbox(nsIImportMailboxDescriptor* aMailbox,
+ nsIMsgFolder* aDstFolder,
+ char16_t** aErrorLog,
+ char16_t** aSuccessLog,
+ bool* aFatalError) {
+ nsAutoString errorLog, successLog;
+
+ // reset progress
+ mProgress = 0;
+
+ nsAutoString mailboxName;
+ aMailbox->GetDisplayName(getter_Copies(mailboxName));
+
+ nsCOMPtr<nsIFile> mboxFolder;
+ nsresult rv = aMailbox->GetFile(getter_AddRefs(mboxFolder));
+ if (NS_FAILED(rv) || !mboxFolder) {
+ ReportStatus(u"ApplemailImportMailboxConverterror", mailboxName, errorLog);
+ SetLogs(successLog, errorLog, aSuccessLog, aErrorLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ // if we're an account mailbox, nothing do. if we're a real mbox
+ // then we've got some messages to import!
+ uint32_t mailboxIdentifier;
+ aMailbox->GetIdentifier(&mailboxIdentifier);
+
+ if (mailboxIdentifier != kAccountMailboxID) {
+ // move to the .mbox's Messages folder
+ nsCOMPtr<nsIFile> messagesFolder;
+ mboxFolder->Clone(getter_AddRefs(messagesFolder));
+ rv = messagesFolder->Append(u"Messages"_ns);
+ if (NS_FAILED(rv)) {
+ // even if there are no messages, it might still be a valid mailbox, or
+ // even a parent for other mailboxes.
+ //
+ // just indicate that we're done, using the same number that we used to
+ // estimate number of messages earlier.
+ uint32_t finalSize;
+ aMailbox->GetSize(&finalSize);
+ mProgress = finalSize;
+
+ // report that we successfully imported this mailbox
+ ReportStatus(u"ApplemailImportMailboxSuccess", mailboxName, successLog);
+ SetLogs(successLog, errorLog, aSuccessLog, aErrorLog);
+ return NS_OK;
+ }
+
+ // let's import the messages!
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ rv = messagesFolder->GetDirectoryEntries(
+ getter_AddRefs(directoryEnumerator));
+ if (NS_FAILED(rv)) {
+ ReportStatus(u"ApplemailImportMailboxConvertError", mailboxName,
+ errorLog);
+ SetLogs(successLog, errorLog, aSuccessLog, aErrorLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ // prepare an outstream to the destination file
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = aDstFolder->GetMsgStore(getter_AddRefs(msgStore));
+ if (!msgStore || NS_FAILED(rv)) {
+ ReportStatus(u"ApplemailImportMailboxConverterror", mailboxName,
+ errorLog);
+ SetLogs(successLog, errorLog, aSuccessLog, aErrorLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ bool hasMore = false;
+ nsCOMPtr<nsIOutputStream> outStream;
+
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ // get the next file entry
+ nsCOMPtr<nsIFile> currentEntry;
+ directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry));
+ if (!currentEntry) continue;
+
+ // make sure it's an .emlx file
+ bool isFile = false;
+ currentEntry->IsFile(&isFile);
+ if (!isFile) continue;
+
+ nsAutoString leafName;
+ currentEntry->GetLeafName(leafName);
+ if (!StringEndsWith(leafName, u".emlx"_ns)) continue;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgStore->GetNewMsgOutputStream(aDstFolder, getter_AddRefs(msgHdr),
+ getter_AddRefs(outStream));
+ if (NS_FAILED(rv)) break;
+
+ // Add the data to the mbox stream.
+ if (NS_SUCCEEDED(nsEmlxHelperUtils::AddEmlxMessageToStream(currentEntry,
+ outStream))) {
+ mProgress++;
+ msgStore->FinishNewMessage(outStream, msgHdr);
+ outStream = nullptr;
+ } else {
+ msgStore->DiscardNewMessage(outStream, msgHdr);
+ outStream = nullptr;
+ break;
+ }
+ }
+ }
+ // just indicate that we're done, using the same number that we used to
+ // estimate number of messages earlier.
+ uint32_t finalSize;
+ aMailbox->GetSize(&finalSize);
+ mProgress = finalSize;
+
+ // report that we successfully imported this mailbox
+ ReportStatus(u"ApplemailImportMailboxSuccess", mailboxName, successLog);
+ SetLogs(successLog, errorLog, aSuccessLog, aErrorLog);
+
+ return NS_OK;
+}
+
+void nsAppleMailImportMail::ReportStatus(const char16_t* aErrorName,
+ nsString& aName, nsAString& aStream) {
+ // get (and format, if needed) the error string from the bundle
+ nsAutoString outString;
+ AutoTArray<nsString, 1> fmt = {aName};
+ nsresult rv = mBundle->FormatStringFromName(
+ NS_ConvertUTF16toUTF8(aErrorName).get(), fmt, outString);
+ // write it out the stream
+ if (NS_SUCCEEDED(rv)) {
+ aStream.Append(outString);
+ aStream.Append(char16_t('\n'));
+ }
+}
+
+void nsAppleMailImportMail::SetLogs(const nsAString& aSuccess,
+ const nsAString& aError,
+ char16_t** aOutSuccess,
+ char16_t** aOutError) {
+ if (aOutError && !*aOutError) *aOutError = ToNewUnicode(aError);
+ if (aOutSuccess && !*aOutSuccess) *aOutSuccess = ToNewUnicode(aSuccess);
+}
+
+NS_IMETHODIMP nsAppleMailImportMail::GetImportProgress(uint32_t* aDoneSoFar) {
+ NS_ENSURE_ARG_POINTER(aDoneSoFar);
+ *aDoneSoFar = mProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAppleMailImportMail::TranslateFolderName(
+ const nsAString& aFolderName, nsAString& aResult) {
+ aResult = aFolderName;
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsAppleMailImport.h b/comm/mailnews/import/src/nsAppleMailImport.h
new file mode 100644
index 0000000000..dd799b06be
--- /dev/null
+++ b/comm/mailnews/import/src/nsAppleMailImport.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsAppleMailImport_h___
+#define nsAppleMailImport_h___
+
+#include "mozilla/Logging.h"
+#include "nsIImportModule.h"
+#include "nsCOMPtr.h"
+#include "nsIStringBundle.h"
+#include "nsIImportMail.h"
+#include "ImportDebug.h"
+
+#define NS_APPLEMAILIMPL_CID \
+ { \
+ 0x9117a1ea, 0xe012, 0x43b5, { \
+ 0xa0, 0x20, 0xcb, 0x8a, 0x66, 0xcc, 0x09, 0xe1 \
+ } \
+ }
+
+#define NS_APPLEMAILIMPORT_CID \
+ { \
+ 0x6d3f101c, 0x70ec, 0x4e04, { \
+ 0xb6, 0x8d, 0x99, 0x08, 0xd1, 0xae, 0xdd, 0xf3 \
+ } \
+ }
+
+#define NS_APPLEMAILIMPL_CONTRACTID "@mozilla.org/import/import-appleMailImpl;1"
+
+#define kAppleMailSupportsString "mail"
+
+class nsIImportService;
+
+class nsAppleMailImportModule : public nsIImportModule {
+ public:
+ nsAppleMailImportModule();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTMODULE
+
+ private:
+ virtual ~nsAppleMailImportModule();
+
+ nsCOMPtr<nsIStringBundle> mBundle;
+};
+
+class nsAppleMailImportMail : public nsIImportMail {
+ public:
+ nsAppleMailImportMail();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTMAIL
+
+ nsresult Initialize();
+
+ private:
+ virtual ~nsAppleMailImportMail();
+
+ void FindAccountMailDirs(
+ nsIFile* aRoot,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService);
+ nsresult FindMboxDirs(
+ nsIFile* aFolder,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService);
+ nsresult AddMboxDir(
+ nsIFile* aFolder,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aMailboxDescs,
+ nsIImportService* aImportService);
+
+ // aInfoString is the format to a "foo %s" string. It may be NULL if the error
+ // string needs no such format.
+ void ReportStatus(const char16_t* aErrorName, nsString& aName,
+ nsAString& aStream);
+ static void SetLogs(const nsAString& success, const nsAString& error,
+ char16_t** aOutErrorLog, char16_t** aSuccessLog);
+
+ nsCOMPtr<nsIStringBundle> mBundle;
+ uint32_t mProgress;
+ uint16_t mCurDepth;
+};
+
+#endif /* nsAppleMailImport_h___ */
diff --git a/comm/mailnews/import/src/nsBeckyAddressBooks.cpp b/comm/mailnews/import/src/nsBeckyAddressBooks.cpp
new file mode 100644
index 0000000000..651bbbeb94
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyAddressBooks.cpp
@@ -0,0 +1,311 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIFile.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsString.h"
+#include "nsIImportService.h"
+#include "nsIImportABDescriptor.h"
+#include "nsMsgUtils.h"
+#include "nsVCardAddress.h"
+
+#include "nsBeckyAddressBooks.h"
+#include "nsBeckyStringBundle.h"
+#include "nsBeckyUtils.h"
+
+NS_IMPL_ISUPPORTS(nsBeckyAddressBooks, nsIImportAddressBooks)
+
+nsresult nsBeckyAddressBooks::Create(nsIImportAddressBooks** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsBeckyAddressBooks());
+ return NS_OK;
+}
+
+nsBeckyAddressBooks::nsBeckyAddressBooks() : mReadBytes(0) {}
+
+nsBeckyAddressBooks::~nsBeckyAddressBooks() {}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetSupportsMultiple(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetAutoFind(char16_t** aDescription, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aDescription);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *aDescription =
+ nsBeckyStringBundle::GetStringByName("BeckyImportDescription");
+ *_retval = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetNeedsFieldMap(nsIFile* aLocation, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = false;
+ return NS_OK;
+}
+
+nsresult nsBeckyAddressBooks::FindAddressBookDirectory(
+ nsIFile** aAddressBookDirectory) {
+ nsCOMPtr<nsIFile> userDirectory;
+ nsresult rv = nsBeckyUtils::FindUserDirectory(getter_AddRefs(userDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = userDirectory->Append(u"AddrBook"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ rv = userDirectory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ bool isDirectory = false;
+ rv = userDirectory->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDirectory) return NS_ERROR_FILE_NOT_FOUND;
+
+ userDirectory.forget(aAddressBookDirectory);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetDefaultLocation(nsIFile** aLocation, bool* aFound,
+ bool* aUserVerify) {
+ NS_ENSURE_ARG_POINTER(aFound);
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(aUserVerify);
+
+ *aLocation = nullptr;
+ *aFound = false;
+ *aUserVerify = true;
+
+ if (NS_SUCCEEDED(nsBeckyAddressBooks::FindAddressBookDirectory(aLocation))) {
+ *aFound = true;
+ *aUserVerify = false;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBeckyAddressBooks::CreateAddressBookDescriptor(
+ nsIImportABDescriptor** aDescriptor) {
+ nsresult rv;
+ nsCOMPtr<nsIImportService> importService =
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return importService->CreateNewABDescriptor(aDescriptor);
+}
+
+bool nsBeckyAddressBooks::IsAddressBookFile(nsIFile* aFile) {
+ if (!aFile) return false;
+
+ nsresult rv;
+ bool isFile = false;
+ rv = aFile->IsFile(&isFile);
+ if (NS_FAILED(rv) && !isFile) return false;
+
+ nsAutoString name;
+ rv = aFile->GetLeafName(name);
+ return StringEndsWith(name, u".bab"_ns);
+}
+
+bool nsBeckyAddressBooks::HasAddressBookFile(nsIFile* aDirectory) {
+ if (!aDirectory) return false;
+
+ nsresult rv;
+ bool isDirectory = false;
+ rv = aDirectory->IsDirectory(&isDirectory);
+ if (NS_FAILED(rv) || !isDirectory) return false;
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool more;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, false);
+ if (IsAddressBookFile(file)) return true;
+ }
+
+ return false;
+}
+
+uint32_t nsBeckyAddressBooks::CountAddressBookSize(nsIFile* aDirectory) {
+ if (!aDirectory) return 0;
+
+ nsresult rv;
+ bool isDirectory = false;
+ rv = aDirectory->IsDirectory(&isDirectory);
+ if (NS_FAILED(rv) || !isDirectory) return 0;
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ uint32_t total = 0;
+ bool more;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ int64_t size;
+ file->GetFileSize(&size);
+ if (total + size > std::numeric_limits<uint32_t>::max())
+ return std::numeric_limits<uint32_t>::max();
+
+ total += static_cast<uint32_t>(size);
+ }
+
+ return total;
+}
+
+nsresult nsBeckyAddressBooks::AppendAddressBookDescriptor(
+ nsIFile* aEntry, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ if (!HasAddressBookFile(aEntry)) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIImportABDescriptor> descriptor;
+ rv = CreateAddressBookDescriptor(getter_AddRefs(descriptor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t size = CountAddressBookSize(aEntry);
+ descriptor->SetSize(size);
+ descriptor->SetAbFile(aEntry);
+
+ nsAutoString name;
+ aEntry->GetLeafName(name);
+ descriptor->SetPreferredName(name);
+
+ books.AppendElement(descriptor);
+ return NS_OK;
+}
+
+// Recursively descend down the dirs, appending to the books array.
+nsresult nsBeckyAddressBooks::CollectAddressBooks(
+ nsIFile* aTarget, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ nsresult rv = AppendAddressBookDescriptor(aTarget, books);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = aTarget->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory = false;
+ rv = file->IsDirectory(&isDirectory);
+ if (NS_SUCCEEDED(rv) && isDirectory) {
+ rv = CollectAddressBooks(file, books);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::FindAddressBooks(
+ nsIFile* aLocation, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ books.Clear();
+ bool isDirectory = false;
+ nsresult rv = aLocation->IsDirectory(&isDirectory);
+ if (NS_FAILED(rv) || !isDirectory) return NS_ERROR_FAILURE;
+
+ rv = CollectAddressBooks(aLocation, books);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::InitFieldMap(nsIImportFieldMap* aFieldMap) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::ImportAddressBook(
+ nsIImportABDescriptor* aSource, nsIAbDirectory* aDestination,
+ nsIImportFieldMap* aFieldMap, nsISupports* aSupportService,
+ char16_t** aErrorLog, char16_t** aSuccessLog, bool* aFatalError) {
+ NS_ENSURE_ARG_POINTER(aSource);
+ NS_ENSURE_ARG_POINTER(aDestination);
+ NS_ENSURE_ARG_POINTER(aErrorLog);
+ NS_ENSURE_ARG_POINTER(aSuccessLog);
+ NS_ENSURE_ARG_POINTER(aFatalError);
+
+ mReadBytes = 0;
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = aSource->GetAbFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = file->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ nsAutoString error;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!IsAddressBookFile(file)) continue;
+
+ bool aborted = false;
+ nsAutoString name;
+ aSource->GetPreferredName(name);
+ nsVCardAddress vcard;
+ rv = vcard.ImportAddresses(&aborted, name.get(), file, aDestination, error,
+ &mReadBytes);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+
+ if (!error.IsEmpty())
+ *aErrorLog = ToNewUnicode(error);
+ else
+ *aSuccessLog =
+ nsBeckyStringBundle::GetStringByName("BeckyImportAddressSuccess");
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetImportProgress(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mReadBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::SetSampleLocation(nsIFile* aLocation) { return NS_OK; }
+
+NS_IMETHODIMP
+nsBeckyAddressBooks::GetSampleData(int32_t aRecordNumber, bool* aRecordExists,
+ char16_t** _retval) {
+ return NS_ERROR_FAILURE;
+}
diff --git a/comm/mailnews/import/src/nsBeckyAddressBooks.h b/comm/mailnews/import/src/nsBeckyAddressBooks.h
new file mode 100644
index 0000000000..af19ea1917
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyAddressBooks.h
@@ -0,0 +1,36 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsBeckyAddressBooks_h___
+#define nsBeckyAddressBooks_h___
+
+#include "nsIImportAddressBooks.h"
+#include "nsIFile.h"
+
+class nsBeckyAddressBooks final : public nsIImportAddressBooks {
+ public:
+ nsBeckyAddressBooks();
+ static nsresult Create(nsIImportAddressBooks** aImport);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTADDRESSBOOKS
+
+ private:
+ virtual ~nsBeckyAddressBooks();
+
+ uint32_t mReadBytes;
+
+ nsresult CollectAddressBooks(nsIFile* aTarget,
+ nsTArray<RefPtr<nsIImportABDescriptor>>& books);
+ nsresult FindAddressBookDirectory(nsIFile** aAddressBookDirectory);
+ nsresult AppendAddressBookDescriptor(
+ nsIFile* aEntry, nsTArray<RefPtr<nsIImportABDescriptor>>& books);
+ uint32_t CountAddressBookSize(nsIFile* aDirectory);
+ bool HasAddressBookFile(nsIFile* aDirectory);
+ bool IsAddressBookFile(nsIFile* aFile);
+ nsresult CreateAddressBookDescriptor(nsIImportABDescriptor** aDescriptor);
+};
+
+#endif /* nsBeckyAddressBooks_h___ */
diff --git a/comm/mailnews/import/src/nsBeckyFilters.cpp b/comm/mailnews/import/src/nsBeckyFilters.cpp
new file mode 100644
index 0000000000..196560311b
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyFilters.cpp
@@ -0,0 +1,711 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsILineInputStream.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgFilterList.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgAccount.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgFolder.h"
+#include "nsCOMPtr.h"
+#include "nsMsgSearchCore.h"
+#include "nsMsgUtils.h"
+#include "msgCore.h"
+
+#include "nsBeckyFilters.h"
+#include "nsBeckyStringBundle.h"
+#include "nsBeckyUtils.h"
+
+NS_IMPL_ISUPPORTS(nsBeckyFilters, nsIImportFilters)
+
+nsresult nsBeckyFilters::Create(nsIImportFilters** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsBeckyFilters());
+ return NS_OK;
+}
+
+nsBeckyFilters::nsBeckyFilters()
+ : mLocation(nullptr), mServer(nullptr), mConvertedFile(nullptr) {}
+
+nsBeckyFilters::~nsBeckyFilters() {}
+
+nsresult nsBeckyFilters::GetDefaultFilterLocation(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> filterDir;
+ rv = nsBeckyUtils::GetDefaultMailboxDirectory(getter_AddRefs(filterDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterDir.forget(aFile);
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::GetFilterFile(bool aIncoming, nsIFile* aLocation,
+ nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ // We assume the caller has already checked that aLocation is a directory,
+ // otherwise it would not make sense to call us.
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> filter;
+ aLocation->Clone(getter_AddRefs(filter));
+ if (aIncoming)
+ rv = filter->Append(u"IFilter.def"_ns);
+ else
+ rv = filter->Append(u"OFilter.def"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ rv = filter->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ filter.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyFilters::AutoLocate(char16_t** aDescription, nsIFile** aLocation,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ if (aDescription) {
+ *aDescription =
+ nsBeckyStringBundle::GetStringByName("BeckyImportDescription");
+ }
+ *aLocation = nullptr;
+ *_retval = false;
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> location;
+ rv = GetDefaultFilterLocation(getter_AddRefs(location));
+ if (NS_FAILED(rv))
+ location = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ else
+ *_retval = true;
+
+ location.forget(aLocation);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyFilters::SetLocation(nsIFile* aLocation) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ bool exists = false;
+ nsresult rv = aLocation->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ mLocation = aLocation;
+ return NS_OK;
+}
+
+static nsMsgSearchAttribValue ConvertSearchKeyToAttrib(const nsACString& aKey) {
+ if (aKey.EqualsLiteral("From") || aKey.EqualsLiteral("Sender") ||
+ aKey.EqualsLiteral("From, Sender, X-Sender")) {
+ return nsMsgSearchAttrib::Sender;
+ } else if (aKey.EqualsLiteral("Subject")) {
+ return nsMsgSearchAttrib::Subject;
+ } else if (aKey.EqualsLiteral("[body]")) {
+ return nsMsgSearchAttrib::Body;
+ } else if (aKey.EqualsLiteral("Date")) {
+ return nsMsgSearchAttrib::Date;
+ } else if (aKey.EqualsLiteral("To")) {
+ return nsMsgSearchAttrib::To;
+ } else if (aKey.EqualsLiteral("Cc")) {
+ return nsMsgSearchAttrib::CC;
+ } else if (aKey.EqualsLiteral("To, Cc, Bcc:")) {
+ return nsMsgSearchAttrib::ToOrCC;
+ }
+ return -1;
+}
+
+static nsMsgSearchOpValue ConvertSearchFlagsToOperator(
+ const nsACString& aFlags) {
+ nsCString flags(aFlags);
+ int32_t lastTabPosition = flags.RFindChar('\t');
+ if ((lastTabPosition == -1) ||
+ ((int32_t)aFlags.Length() == lastTabPosition - 1)) {
+ return -1;
+ }
+
+ switch (aFlags.CharAt(0)) {
+ case 'X':
+ return nsMsgSearchOp::DoesntContain;
+ case 'O':
+ if (aFlags.FindChar('T', lastTabPosition + 1) >= 0)
+ return nsMsgSearchOp::BeginsWith;
+ return nsMsgSearchOp::Contains;
+ default:
+ return -1;
+ }
+}
+
+nsresult nsBeckyFilters::ParseRuleLine(const nsCString& aLine,
+ nsMsgSearchAttribValue* aSearchAttribute,
+ nsMsgSearchOpValue* aSearchOperator,
+ nsString& aSearchKeyword) {
+ int32_t firstColonPosition = aLine.FindChar(':');
+ if (firstColonPosition == -1 ||
+ (int32_t)aLine.Length() == firstColonPosition - 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t secondColonPosition = aLine.FindChar(':', firstColonPosition + 1);
+ if (secondColonPosition == -1 ||
+ (int32_t)aLine.Length() == secondColonPosition - 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t length = secondColonPosition - firstColonPosition - 1;
+ nsMsgSearchAttribValue searchAttribute;
+ searchAttribute = ConvertSearchKeyToAttrib(
+ Substring(aLine, firstColonPosition + 1, length));
+ if (searchAttribute < 0) return NS_ERROR_FAILURE;
+
+ int32_t tabPosition = aLine.FindChar('\t');
+ if (tabPosition == -1 || (int32_t)aLine.Length() == tabPosition - 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsMsgSearchOpValue searchOperator;
+ searchOperator =
+ ConvertSearchFlagsToOperator(Substring(aLine, tabPosition + 1));
+ if (searchOperator < 0) return NS_ERROR_FAILURE;
+
+ *aSearchOperator = searchOperator;
+ *aSearchAttribute = searchAttribute;
+ length = tabPosition - secondColonPosition - 1;
+ CopyUTF8toUTF16(Substring(aLine, secondColonPosition + 1, length),
+ aSearchKeyword);
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::SetSearchTerm(const nsCString& aLine,
+ nsIMsgFilter* aFilter) {
+ NS_ENSURE_ARG_POINTER(aFilter);
+
+ nsresult rv;
+ nsMsgSearchAttribValue searchAttribute = -1;
+ nsMsgSearchOpValue searchOperator = -1;
+ nsAutoString searchKeyword;
+ rv = ParseRuleLine(aLine, &searchAttribute, &searchOperator, searchKeyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ rv = aFilter->CreateTerm(getter_AddRefs(term));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = term->SetAttrib(searchAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = term->SetOp(searchOperator);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgSearchValue> value;
+ rv = term->GetValue(getter_AddRefs(value));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = value->SetAttrib(searchAttribute);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = value->SetStr(searchKeyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = term->SetValue(value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = term->SetBooleanAnd(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!searchKeyword.IsEmpty())
+ rv = aFilter->SetFilterName(searchKeyword);
+ else
+ rv = aFilter->SetFilterName(u"No name"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aFilter->AppendTerm(term);
+}
+
+nsresult nsBeckyFilters::CreateRuleAction(nsIMsgFilter* aFilter,
+ nsMsgRuleActionType actionType,
+ nsIMsgRuleAction** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgRuleAction> action;
+ rv = aFilter->CreateAction(getter_AddRefs(action));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = action->SetType(actionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ action.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::GetActionTarget(const nsCString& aLine,
+ nsCString& aTarget) {
+ int32_t firstColonPosition = aLine.FindChar(':');
+ if (firstColonPosition < -1 ||
+ aLine.Length() == static_cast<uint32_t>(firstColonPosition)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aTarget.Assign(Substring(aLine, firstColonPosition + 1));
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::GetResendTarget(const nsCString& aLine,
+ nsCString& aTemplate,
+ nsCString& aTargetAddress) {
+ nsresult rv;
+ nsAutoCString target;
+ rv = GetActionTarget(aLine, target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t asteriskPosition = target.FindChar('*');
+ if (asteriskPosition < 0) {
+ aTemplate.Assign(target);
+ return NS_OK;
+ }
+
+ if (target.Length() == static_cast<uint32_t>(asteriskPosition))
+ return NS_ERROR_FAILURE;
+
+ aTemplate.Assign(StringHead(target, asteriskPosition - 1));
+ aTargetAddress.Assign(Substring(target, asteriskPosition + 1));
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::CreateResendAction(
+ const nsCString& aLine, nsIMsgFilter* aFilter,
+ const nsMsgRuleActionType& aActionType, nsIMsgRuleAction** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgRuleAction> action;
+ rv = CreateRuleAction(aFilter, aActionType, getter_AddRefs(action));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString templateString;
+ nsAutoCString targetAddress;
+ rv = GetResendTarget(aLine, templateString, targetAddress);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aActionType == nsMsgFilterAction::Forward)
+ rv = action->SetStrValue(targetAddress);
+ else
+ rv = action->SetStrValue(templateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ action.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::GetFolderNameFromTarget(const nsCString& aTarget,
+ nsAString& aName) {
+ int32_t backslashPosition = aTarget.RFindChar('\\');
+ if (backslashPosition > 0) {
+ NS_ConvertUTF8toUTF16 utf16String(
+ Substring(aTarget, backslashPosition + 1));
+ nsBeckyUtils::TranslateFolderName(utf16String, aName);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::GetDistributeTarget(const nsCString& aLine,
+ nsCString& aTargetFolder) {
+ nsresult rv;
+ nsAutoCString target;
+ rv = GetActionTarget(aLine, target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ target.Trim("\\", false, true);
+ nsAutoString folderName;
+ rv = GetFolderNameFromTarget(target, folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetMessageFolder(folderName, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!folder) {
+ rv = mServer->GetRootMsgFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return folder->GetURI(aTargetFolder);
+}
+
+nsresult nsBeckyFilters::CreateDistributeAction(
+ const nsCString& aLine, nsIMsgFilter* aFilter,
+ const nsMsgRuleActionType& aActionType, nsIMsgRuleAction** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgRuleAction> action;
+ rv = CreateRuleAction(aFilter, aActionType, getter_AddRefs(action));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString targetFolder;
+ rv = GetDistributeTarget(aLine, targetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = action->SetTargetFolderUri(targetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ action.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::CreateLeaveOrDeleteAction(const nsCString& aLine,
+ nsIMsgFilter* aFilter,
+ nsIMsgRuleAction** _retval) {
+ nsresult rv;
+ nsMsgRuleActionType actionType;
+ if (aLine.CharAt(3) == '0') {
+ actionType = nsMsgFilterAction::LeaveOnPop3Server;
+ } else if (aLine.CharAt(3) == '1') {
+ if (aLine.CharAt(5) == '1')
+ actionType = nsMsgFilterAction::Delete;
+ else
+ actionType = nsMsgFilterAction::DeleteFromPop3Server;
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIMsgRuleAction> action;
+ rv = CreateRuleAction(aFilter, actionType, getter_AddRefs(action));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ action.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::SetRuleAction(const nsCString& aLine,
+ nsIMsgFilter* aFilter) {
+ if (!aFilter || aLine.Length() < 4) return NS_ERROR_FAILURE;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgRuleAction> action;
+ switch (aLine.CharAt(1)) {
+ case 'R': // Reply
+ rv = CreateResendAction(aLine, aFilter, nsMsgFilterAction::Reply,
+ getter_AddRefs(action));
+ break;
+ case 'F': // Forward
+ rv = CreateResendAction(aLine, aFilter, nsMsgFilterAction::Forward,
+ getter_AddRefs(action));
+ break;
+ case 'L': // Leave or delete
+ rv = CreateLeaveOrDeleteAction(aLine, aFilter, getter_AddRefs(action));
+ break;
+ case 'Y': // Copy
+ rv = CreateDistributeAction(aLine, aFilter,
+ nsMsgFilterAction::CopyToFolder,
+ getter_AddRefs(action));
+ break;
+ case 'M': // Move
+ rv = CreateDistributeAction(aLine, aFilter,
+ nsMsgFilterAction::MoveToFolder,
+ getter_AddRefs(action));
+ break;
+ case 'G': // Set flag
+ if (aLine.CharAt(3) == 'R') // Read
+ rv = CreateRuleAction(aFilter, nsMsgFilterAction::MarkRead,
+ getter_AddRefs(action));
+ break;
+ default:
+ return NS_OK;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (action) {
+ rv = aFilter->AppendAction(action);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::CreateFilter(bool aIncoming, nsIMsgFilter** _retval) {
+ NS_ENSURE_STATE(mServer);
+
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ mServer->GetFilterList(nullptr, getter_AddRefs(filterList));
+ NS_ENSURE_STATE(filterList);
+
+ nsCOMPtr<nsIMsgFilter> filter;
+ nsresult rv = filterList->CreateFilter(EmptyString(), getter_AddRefs(filter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aIncoming)
+ filter->SetFilterType(nsMsgFilterType::InboxRule | nsMsgFilterType::Manual);
+ else
+ filter->SetFilterType(nsMsgFilterType::PostOutgoing |
+ nsMsgFilterType::Manual);
+
+ filter->SetEnabled(true);
+ filter.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::AppendFilter(nsIMsgFilter* aFilter) {
+ NS_ENSURE_STATE(mServer);
+
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ mServer->GetFilterList(nullptr, getter_AddRefs(filterList));
+ NS_ENSURE_STATE(filterList);
+
+ uint32_t count;
+ nsresult rv = filterList->GetFilterCount(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return filterList->InsertFilterAt(count, aFilter);
+}
+
+nsresult nsBeckyFilters::ParseFilterFile(nsIFile* aFile, bool aIncoming) {
+ nsresult rv;
+ nsCOMPtr<nsILineInputStream> lineStream;
+ rv = nsBeckyUtils::CreateLineInputStream(aFile, getter_AddRefs(lineStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsAutoCString line;
+
+ nsCOMPtr<nsIMsgFilter> filter;
+ while (NS_SUCCEEDED(rv) && more) {
+ rv = lineStream->ReadLine(line, &more);
+
+ switch (line.CharAt(0)) {
+ case ':':
+ if (line.EqualsLiteral(":Begin \"\"")) {
+ CreateFilter(aIncoming, getter_AddRefs(filter));
+ } else if (line.EqualsLiteral(":End \"\"")) {
+ if (filter) AppendFilter(filter);
+ filter = nullptr;
+ }
+ break;
+ case '!':
+ SetRuleAction(line, filter);
+ break;
+ case '@':
+ SetSearchTerm(line, filter);
+ break;
+ case '$': // $X: disabled
+ if (StringBeginsWith(line, "$X"_ns) && filter) {
+ filter->SetEnabled(false);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyFilters::Import(char16_t** aError, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aError);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ // If mLocation is null, set it to the default filter directory.
+ // If mLocation is a file, we import it as incoming folder.
+ // If mLocation is a directory, we try to import incoming and outgoing folders
+ // from it (in default files).
+
+ *_retval = false;
+ nsresult rv;
+ nsCOMPtr<nsIFile> filterFile;
+
+ bool haveFile = false;
+
+ if (!mLocation) {
+ bool retval = false;
+ rv = AutoLocate(nullptr, getter_AddRefs(mLocation), &retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!retval) return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ // What type of location do we have?
+ bool isDirectory = false;
+ rv = mLocation->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isDirectory) {
+ haveFile = false;
+ } else {
+ bool isFile = false;
+ rv = mLocation->IsFile(&isFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isFile) {
+ haveFile = true;
+ mLocation->Clone(getter_AddRefs(filterFile));
+ } else {
+ // mLocation is neither file nor directory.
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ bool haveIncoming = true;
+ if (haveFile) {
+ // If the passed filename equals OFilter.def, import as outgoing filters.
+ // Everything else is considered incoming.
+ nsAutoString fileName;
+ rv = mLocation->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (fileName.EqualsLiteral("OFilter.def")) haveIncoming = false;
+ }
+
+ // Try importing from the passed in file or the default incoming filters file.
+ if ((haveFile && haveIncoming) ||
+ (!haveFile && NS_SUCCEEDED(GetFilterFile(true, mLocation,
+ getter_AddRefs(filterFile))))) {
+ rv = CollectServers();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsBeckyUtils::ConvertToUTF8File(filterFile,
+ getter_AddRefs(mConvertedFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ParseFilterFile(mConvertedFile, true);
+ if (NS_SUCCEEDED(rv)) *_retval = true;
+
+ (void)RemoveConvertedFile();
+ }
+
+ // If we didn't have a file passed (but a directory), try finding also
+ // outgoing filters.
+ if ((haveFile && !haveIncoming) ||
+ (!haveFile && NS_SUCCEEDED(GetFilterFile(false, mLocation,
+ getter_AddRefs(filterFile))))) {
+ rv = CollectServers();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsBeckyUtils::ConvertToUTF8File(filterFile,
+ getter_AddRefs(mConvertedFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ParseFilterFile(mConvertedFile, false);
+ if (NS_SUCCEEDED(rv)) *_retval = true;
+
+ (void)RemoveConvertedFile();
+ }
+
+ return rv;
+}
+
+nsresult nsBeckyFilters::FindMessageFolder(const nsAString& aName,
+ nsIMsgFolder* aParentFolder,
+ nsIMsgFolder** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> found;
+ rv = aParentFolder->GetChildNamed(aName, getter_AddRefs(found));
+ if (found) {
+ NS_ADDREF(*_retval = found);
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIMsgFolder>> children;
+ rv = aParentFolder->GetSubFolders(children);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* child : children) {
+ rv = FindMessageFolder(aName, child, getter_AddRefs(found));
+ if (found) {
+ NS_ADDREF(*_retval = found);
+ return NS_OK;
+ }
+ }
+
+ return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+}
+
+nsresult nsBeckyFilters::FindMessageFolderInServer(
+ const nsAString& aName, nsIMsgIncomingServer* aServer,
+ nsIMsgFolder** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = aServer->GetRootMsgFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return FindMessageFolder(aName, rootFolder, _retval);
+}
+
+nsresult nsBeckyFilters::GetMessageFolder(const nsAString& aName,
+ nsIMsgFolder** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager;
+ accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgAccount>> accounts;
+ rv = accountManager->GetAccounts(accounts);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> found;
+ for (auto account : accounts) {
+ if (!account) continue;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ account->GetIncomingServer(getter_AddRefs(server));
+ if (!server) continue;
+ FindMessageFolderInServer(aName, server, getter_AddRefs(found));
+ if (found) break;
+ }
+
+ if (!found) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ FindMessageFolderInServer(aName, server, getter_AddRefs(found));
+ }
+
+ if (!found) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ found.forget(_retval);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::CollectServers() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager;
+ accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> defaultAccount;
+ rv = accountManager->GetDefaultAccount(getter_AddRefs(defaultAccount));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (defaultAccount)
+ return defaultAccount->GetIncomingServer(getter_AddRefs(mServer));
+
+ // We can also import filters into the Local Folders account.
+ rv = accountManager->GetLocalFoldersServer(getter_AddRefs(mServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mServer) return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+nsresult nsBeckyFilters::RemoveConvertedFile() {
+ nsresult rv = NS_OK;
+ if (mConvertedFile) {
+ bool exists = false;
+ mConvertedFile->Exists(&exists);
+ if (exists) {
+ rv = mConvertedFile->Remove(false);
+ if (NS_SUCCEEDED(rv)) mConvertedFile = nullptr;
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/import/src/nsBeckyFilters.h b/comm/mailnews/import/src/nsBeckyFilters.h
new file mode 100644
index 0000000000..91ee2ed813
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyFilters.h
@@ -0,0 +1,73 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsBeckyFilters_h___
+#define nsBeckyFilters_h___
+
+#include "nsIImportFilters.h"
+#include "nsIFile.h"
+#include "nsString.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsMsgFilterCore.h"
+
+class nsIMsgFilter;
+class nsIMsgRuleAction;
+
+class nsBeckyFilters final : public nsIImportFilters {
+ public:
+ nsBeckyFilters();
+ static nsresult Create(nsIImportFilters** aImport);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTFILTERS
+
+ private:
+ virtual ~nsBeckyFilters();
+
+ nsCOMPtr<nsIFile> mLocation;
+ nsCOMPtr<nsIMsgIncomingServer> mServer;
+ nsCOMPtr<nsIFile> mConvertedFile;
+
+ nsresult GetDefaultFilterLocation(nsIFile** aFile);
+ nsresult GetFilterFile(bool aIncoming, nsIFile* aLocation, nsIFile** aFile);
+ nsresult ParseFilterFile(nsIFile* aFile, bool aIncoming);
+ nsresult ParseRuleLine(const nsCString& aLine,
+ nsMsgSearchAttribValue* aSearchAttribute,
+ nsMsgSearchOpValue* aSearchOperator,
+ nsString& aSearchKeyword);
+ nsresult CollectServers();
+ nsresult FindMessageFolder(const nsAString& aName,
+ nsIMsgFolder* aParantFolder,
+ nsIMsgFolder** _retval);
+ nsresult FindMessageFolderInServer(const nsAString& aName,
+ nsIMsgIncomingServer* aServer,
+ nsIMsgFolder** _retval);
+ nsresult GetMessageFolder(const nsAString& aName, nsIMsgFolder** _retval);
+ nsresult GetActionTarget(const nsCString& aLine, nsCString& aTarget);
+ nsresult GetFolderNameFromTarget(const nsCString& aTarget, nsAString& aName);
+ nsresult GetDistributeTarget(const nsCString& aLine,
+ nsCString& aTargetFolder);
+ nsresult GetResendTarget(const nsCString& aLine, nsCString& aTemplate,
+ nsCString& aTargetAddress);
+ nsresult CreateRuleAction(nsIMsgFilter* aFilter,
+ nsMsgRuleActionType actionType,
+ nsIMsgRuleAction** _retval);
+ nsresult CreateDistributeAction(const nsCString& aLine, nsIMsgFilter* aFilter,
+ const nsMsgRuleActionType& aActionType,
+ nsIMsgRuleAction** _retval);
+ nsresult CreateLeaveOrDeleteAction(const nsCString& aLine,
+ nsIMsgFilter* aFilter,
+ nsIMsgRuleAction** _retval);
+ nsresult CreateResendAction(const nsCString& aLine, nsIMsgFilter* aFilter,
+ const nsMsgRuleActionType& aActionType,
+ nsIMsgRuleAction** _retval);
+ nsresult CreateFilter(bool aIncoming, nsIMsgFilter** _retval);
+ nsresult AppendFilter(nsIMsgFilter* aFilter);
+ nsresult SetRuleAction(const nsCString& aLine, nsIMsgFilter* aFilter);
+ nsresult SetSearchTerm(const nsCString& aLine, nsIMsgFilter* aFilter);
+ nsresult RemoveConvertedFile();
+};
+
+#endif /* nsBeckyFilters_h___ */
diff --git a/comm/mailnews/import/src/nsBeckyImport.cpp b/comm/mailnews/import/src/nsBeckyImport.cpp
new file mode 100644
index 0000000000..8b56ba95d3
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyImport.cpp
@@ -0,0 +1,143 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsIImportService.h"
+#include "nsIImportMail.h"
+#include "nsIImportGeneric.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportSettings.h"
+#include "nsIImportFilters.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsTextFormatter.h"
+#include "nsUnicharUtils.h"
+#include "nsCOMPtr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+
+#include "nsBeckyImport.h"
+#include "nsBeckyMail.h"
+#include "nsBeckyAddressBooks.h"
+#include "nsBeckySettings.h"
+#include "nsBeckyFilters.h"
+#include "nsBeckyStringBundle.h"
+
+nsBeckyImport::nsBeckyImport() {}
+
+nsBeckyImport::~nsBeckyImport() {}
+
+NS_IMPL_ISUPPORTS(nsBeckyImport, nsIImportModule)
+
+NS_IMETHODIMP
+nsBeckyImport::GetName(char16_t** aName) {
+ NS_ENSURE_ARG_POINTER(aName);
+ *aName = nsBeckyStringBundle::GetStringByName("BeckyImportName");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyImport::GetDescription(char16_t** aDescription) {
+ NS_ENSURE_ARG_POINTER(aDescription);
+ *aDescription =
+ nsBeckyStringBundle::GetStringByName("BeckyImportDescription");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyImport::GetSupports(char** aSupports) {
+ NS_ENSURE_ARG_POINTER(aSupports);
+ *aSupports = strdup(kBeckySupportsString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyImport::GetSupportsUpgrade(bool* aUpgrade) {
+ NS_ENSURE_ARG_POINTER(aUpgrade);
+ *aUpgrade = true;
+ return NS_OK;
+}
+
+nsresult nsBeckyImport::GetMailImportInterface(nsISupports** aInterface) {
+ nsCOMPtr<nsIImportMail> importer;
+ nsresult rv = nsBeckyMail::Create(getter_AddRefs(importer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImportService> importService(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIImportGeneric> generic;
+ rv = importService->CreateNewGenericMail(getter_AddRefs(generic));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ generic->SetData("mailInterface", importer);
+
+ nsString name;
+ name.Adopt(nsBeckyStringBundle::GetStringByName("BeckyImportName"));
+
+ nsCOMPtr<nsISupportsString> nameString(
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nameString->SetData(name);
+ generic->SetData("name", nameString);
+
+ return CallQueryInterface(generic, aInterface);
+}
+
+nsresult nsBeckyImport::GetAddressBookImportInterface(
+ nsISupports** aInterface) {
+ nsresult rv;
+ nsCOMPtr<nsIImportAddressBooks> importer;
+ rv = nsBeckyAddressBooks::Create(getter_AddRefs(importer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImportService> importService(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIImportGeneric> generic;
+ rv = importService->CreateNewGenericAddressBooks(getter_AddRefs(generic));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ generic->SetData("addressInterface", importer);
+ return CallQueryInterface(generic, aInterface);
+}
+
+nsresult nsBeckyImport::GetSettingsImportInterface(nsISupports** aInterface) {
+ nsresult rv;
+ nsCOMPtr<nsIImportSettings> importer;
+ rv = nsBeckySettings::Create(getter_AddRefs(importer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(importer, aInterface);
+}
+
+nsresult nsBeckyImport::GetFiltersImportInterface(nsISupports** aInterface) {
+ nsresult rv;
+ nsCOMPtr<nsIImportFilters> importer;
+ rv = nsBeckyFilters::Create(getter_AddRefs(importer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(importer, aInterface);
+}
+
+NS_IMETHODIMP
+nsBeckyImport::GetImportInterface(const char* aImportType,
+ nsISupports** aInterface) {
+ NS_ENSURE_ARG_POINTER(aImportType);
+ NS_ENSURE_ARG_POINTER(aInterface);
+
+ *aInterface = nullptr;
+ if (!strcmp(aImportType, "mail")) return GetMailImportInterface(aInterface);
+ if (!strcmp(aImportType, "addressbook"))
+ return GetAddressBookImportInterface(aInterface);
+ if (!strcmp(aImportType, "settings"))
+ return GetSettingsImportInterface(aInterface);
+ if (!strcmp(aImportType, "filters"))
+ return GetFiltersImportInterface(aInterface);
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
diff --git a/comm/mailnews/import/src/nsBeckyImport.h b/comm/mailnews/import/src/nsBeckyImport.h
new file mode 100644
index 0000000000..0884d417fb
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyImport.h
@@ -0,0 +1,38 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsBeckyImport_h___
+#define nsBeckyImport_h___
+
+#include "nsIImportModule.h"
+
+#define NS_BECKYIMPORT_CID \
+ { \
+ 0x7952a6cf, 0x2442, 0x4c04, { \
+ 0x9f, 0x02, 0x15, 0x0b, 0x15, 0xa0, 0xa8, 0x41 \
+ } \
+ }
+
+#define kBeckySupportsString \
+ NS_IMPORT_MAIL_STR "," NS_IMPORT_ADDRESS_STR "," NS_IMPORT_SETTINGS_STR \
+ "," NS_IMPORT_FILTERS_STR
+
+class nsBeckyImport final : public nsIImportModule {
+ public:
+ nsBeckyImport();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTMODULE
+
+ private:
+ virtual ~nsBeckyImport();
+
+ nsresult GetMailImportInterface(nsISupports** aInterface);
+ nsresult GetAddressBookImportInterface(nsISupports** aInterface);
+ nsresult GetSettingsImportInterface(nsISupports** aInterface);
+ nsresult GetFiltersImportInterface(nsISupports** aInterface);
+};
+
+#endif /* nsBeckyImport_h___ */
diff --git a/comm/mailnews/import/src/nsBeckyMail.cpp b/comm/mailnews/import/src/nsBeckyMail.cpp
new file mode 100644
index 0000000000..36ab1e1104
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyMail.cpp
@@ -0,0 +1,566 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsILineInputStream.h"
+#include "nsNetUtil.h"
+#include "nsIImportService.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsMsgUtils.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsMsgMessageFlags.h"
+#include "nsTArray.h"
+#include "nspr.h"
+#include "nsThreadUtils.h"
+#include "nsIDirectoryEnumerator.h"
+
+#include "nsBeckyMail.h"
+#include "nsBeckyUtils.h"
+#include "nsBeckyStringBundle.h"
+
+#define FROM_LINE "From - Mon Jan 1 00:00:00 1965" MSG_LINEBREAK
+#define X_BECKY_STATUS_HEADER "X-Becky-Status"
+#define X_BECKY_INCLUDE_HEADER "X-Becky-Include"
+
+enum {
+ BECKY_STATUS_READ = 1 << 0,
+ BECKY_STATUS_FORWARDED = 1 << 1,
+ BECKY_STATUS_REPLIED = 1 << 2
+};
+
+NS_IMPL_ISUPPORTS(nsBeckyMail, nsIImportMail)
+
+nsresult nsBeckyMail::Create(nsIImportMail** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsBeckyMail());
+ return NS_OK;
+}
+
+nsBeckyMail::nsBeckyMail() : mReadBytes(0) {}
+
+nsBeckyMail::~nsBeckyMail() {}
+
+NS_IMETHODIMP
+nsBeckyMail::GetDefaultLocation(nsIFile** aLocation, bool* aFound,
+ bool* aUserVerify) {
+ NS_ENSURE_ARG_POINTER(aFound);
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(aUserVerify);
+
+ *aLocation = nullptr;
+ *aUserVerify = true;
+ *aFound = false;
+ if (NS_SUCCEEDED(nsBeckyUtils::GetDefaultMailboxDirectory(aLocation)))
+ *aFound = true;
+
+ return NS_OK;
+}
+
+nsresult nsBeckyMail::CreateMailboxDescriptor(
+ nsIImportMailboxDescriptor** aDescriptor) {
+ nsresult rv;
+ nsCOMPtr<nsIImportService> importService;
+ importService = do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return importService->CreateNewMailboxDescriptor(aDescriptor);
+}
+
+nsresult nsBeckyMail::GetMailboxName(nsIFile* aMailbox, nsAString& aName) {
+ nsCOMPtr<nsIFile> iniFile;
+ nsBeckyUtils::GetMailboxINIFile(aMailbox, getter_AddRefs(iniFile));
+ if (iniFile) {
+ nsCOMPtr<nsIFile> convertedFile;
+ nsBeckyUtils::ConvertToUTF8File(iniFile, getter_AddRefs(convertedFile));
+ if (convertedFile) {
+ nsAutoCString utf8Name;
+ nsBeckyUtils::GetMailboxNameFromINIFile(convertedFile, utf8Name);
+ convertedFile->Remove(false);
+ CopyUTF8toUTF16(utf8Name, aName);
+ }
+ }
+
+ if (aName.IsEmpty()) {
+ nsAutoString name;
+ aMailbox->GetLeafName(name);
+ name.Trim("!", true, false);
+ aName.Assign(name);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBeckyMail::AppendMailboxDescriptor(
+ nsIFile* aEntry, const nsString& aName, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected) {
+ nsresult rv;
+ nsCOMPtr<nsIImportMailboxDescriptor> descriptor;
+ rv = CreateMailboxDescriptor(getter_AddRefs(descriptor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t size;
+ rv = aEntry->GetFileSize(&size);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = descriptor->SetSize(size);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = descriptor->SetDisplayName(aName.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> mailboxFile;
+ rv = descriptor->GetFile(getter_AddRefs(mailboxFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ descriptor->SetDepth(aDepth);
+
+ mailboxFile->InitWithFile(aEntry);
+ aCollected.AppendElement(descriptor);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyMail::CollectMailboxesInFolderListFile(
+ nsIFile* aListFile, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected) {
+ nsresult rv;
+ nsCOMPtr<nsILineInputStream> lineStream;
+ rv = nsBeckyUtils::CreateLineInputStream(aListFile,
+ getter_AddRefs(lineStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> parent;
+ rv = aListFile->GetParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsAutoCString folderName;
+ bool isEmpty = true;
+ while (more && NS_SUCCEEDED(rv)) {
+ rv = lineStream->ReadLine(folderName, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (folderName.IsEmpty()) continue;
+
+ nsCOMPtr<nsIFile> folder;
+ rv = parent->Clone(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->AppendNative(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ isEmpty = false;
+ rv = CollectMailboxesInDirectory(folder, aDepth + 1, aCollected);
+ }
+
+ return isEmpty ? NS_ERROR_FILE_NOT_FOUND : NS_OK;
+}
+
+nsresult nsBeckyMail::CollectMailboxesInDirectory(
+ nsIFile* aDirectory, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected) {
+ nsAutoString mailboxName;
+ nsresult rv = GetMailboxName(aDirectory, mailboxName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDepth != 0)
+ AppendMailboxDescriptor(aDirectory, mailboxName, aDepth, aCollected);
+
+ nsCOMPtr<nsIFile> folderListFile;
+ rv = nsBeckyUtils::GetFolderListFile(aDirectory,
+ getter_AddRefs(folderListFile));
+ bool folderListExists = false;
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = CollectMailboxesInFolderListFile(folderListFile, aDepth, aCollected);
+ folderListExists = true;
+ }
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString name;
+ rv = file->GetLeafName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringEndsWith(name, u".bmf"_ns)) {
+ AppendMailboxDescriptor(file, mailboxName, aDepth, aCollected);
+ }
+
+ // The Folder.lst file is not created if there is only one sub folder,
+ // so we need to find the sub folder by our hands.
+ // The folder name does not begin with # or ! maybe. Yes, maybe...
+ if (!folderListExists) {
+ if (StringBeginsWith(name, u"#"_ns) || StringBeginsWith(name, u"!"_ns))
+ continue;
+
+ bool isDirectory = false;
+ rv = file->IsDirectory(&isDirectory);
+ if (isDirectory) {
+ CollectMailboxesInDirectory(file, aDepth + 1, aCollected);
+ continue;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyMail::FindMailboxes(
+ nsIFile* aLocation, nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ boxes.Clear();
+ nsresult rv = CollectMailboxesInDirectory(aLocation, 0, boxes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+static nsresult GetBeckyStatusValue(const nsCString& aHeader,
+ nsACString& aValue) {
+ int32_t valueStartPosition;
+
+ valueStartPosition = aHeader.FindChar(':');
+ if (valueStartPosition < 0) return NS_ERROR_UNEXPECTED;
+
+ valueStartPosition++;
+
+ int32_t commaPosition = aHeader.FindChar(',', valueStartPosition);
+ if (commaPosition < 0) return NS_ERROR_UNEXPECTED;
+
+ nsAutoCString value(Substring(aHeader, valueStartPosition,
+ commaPosition - valueStartPosition));
+ value.Trim(" \t");
+
+ aValue.Assign(value);
+
+ return NS_OK;
+}
+
+static nsresult GetBeckyIncludeValue(const nsCString& aHeader,
+ nsACString& aValue) {
+ int32_t valueStartPosition;
+
+ valueStartPosition = aHeader.FindChar(':');
+ if (valueStartPosition < 0) return NS_ERROR_FAILURE;
+
+ valueStartPosition++;
+ nsAutoCString value(Substring(aHeader, valueStartPosition));
+ value.Trim(" \t");
+
+ aValue.Assign(value);
+
+ return NS_OK;
+}
+
+static bool ConvertBeckyStatusToMozillaStatus(
+ const nsCString& aHeader, nsMsgMessageFlagType* aMozillaStatusFlag) {
+ nsresult rv;
+ nsAutoCString statusString;
+ rv = GetBeckyStatusValue(aHeader, statusString);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsresult errorCode;
+ uint32_t beckyStatusFlag =
+ static_cast<uint32_t>(statusString.ToInteger(&errorCode, 16));
+ if (NS_FAILED(errorCode)) return false;
+
+ if (beckyStatusFlag & BECKY_STATUS_READ)
+ *aMozillaStatusFlag |= nsMsgMessageFlags::Read;
+ if (beckyStatusFlag & BECKY_STATUS_FORWARDED)
+ *aMozillaStatusFlag |= nsMsgMessageFlags::Forwarded;
+ if (beckyStatusFlag & BECKY_STATUS_REPLIED)
+ *aMozillaStatusFlag |= nsMsgMessageFlags::Replied;
+
+ return true;
+}
+
+static inline bool CheckHeaderKey(const nsCString& aHeader,
+ const char* aKeyString) {
+ nsAutoCString key(StringHead(aHeader, aHeader.FindChar(':')));
+ key.Trim(" \t");
+ return key.Equals(aKeyString);
+}
+
+static inline bool IsBeckyStatusHeader(const nsCString& aHeader) {
+ return CheckHeaderKey(aHeader, X_BECKY_STATUS_HEADER);
+}
+
+static inline bool IsBeckyIncludeLine(const nsCString& aLine) {
+ return CheckHeaderKey(aLine, X_BECKY_INCLUDE_HEADER);
+}
+
+static inline bool IsEndOfHeaders(const nsCString& aLine) {
+ return aLine.IsEmpty();
+}
+
+static inline bool IsEndOfMessage(const nsCString& aLine) {
+ return aLine.EqualsLiteral(".");
+}
+
+class ImportMessageRunnable : public mozilla::Runnable {
+ public:
+ ImportMessageRunnable(nsIFile* aMessageFile, nsIMsgFolder* aFolder);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ private:
+ nsresult WriteHeaders(nsCString& aHeaders, nsIOutputStream* aOutputStream);
+ nsresult HandleHeaderLine(const nsCString& aHeaderLine, nsACString& aHeaders);
+ nsresult GetAttachmentFile(nsIFile* aMailboxFile, const nsCString& aHeader,
+ nsIFile** _retval);
+ nsresult WriteAttachmentFile(nsIFile* aMailboxFile, const nsCString& aHeader,
+ nsIOutputStream* aOutputStream);
+
+ nsCOMPtr<nsIFile> mMessageFile;
+ nsCOMPtr<nsIMsgFolder> mFolder;
+};
+
+ImportMessageRunnable::ImportMessageRunnable(nsIFile* aMessageFile,
+ nsIMsgFolder* aFolder)
+ : mozilla::Runnable("ImportMessageRunnable"),
+ mMessageFile(aMessageFile),
+ mFolder(aFolder) {}
+
+nsresult ImportMessageRunnable::WriteHeaders(nsCString& aHeaders,
+ nsIOutputStream* aOutputStream) {
+ nsresult rv;
+ uint32_t writtenBytes = 0;
+
+ rv = aOutputStream->Write(FROM_LINE, strlen(FROM_LINE), &writtenBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aOutputStream->Write(aHeaders.get(), aHeaders.Length(), &writtenBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv =
+ aOutputStream->Write(MSG_LINEBREAK, strlen(MSG_LINEBREAK), &writtenBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aHeaders.Truncate();
+
+ return NS_OK;
+}
+
+nsresult ImportMessageRunnable::HandleHeaderLine(const nsCString& aHeaderLine,
+ nsACString& aHeaders) {
+ aHeaders.Append(aHeaderLine);
+ aHeaders.AppendLiteral(MSG_LINEBREAK);
+
+ nsMsgMessageFlagType flag = 0;
+ if (IsBeckyStatusHeader(aHeaderLine) &&
+ ConvertBeckyStatusToMozillaStatus(aHeaderLine, &flag)) {
+ char* statusLine;
+ statusLine = PR_smprintf(X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK, flag);
+ aHeaders.Append(statusLine);
+ PR_smprintf_free(statusLine);
+ aHeaders.AppendLiteral(X_MOZILLA_KEYWORDS);
+ }
+
+ return NS_OK;
+}
+
+nsresult ImportMessageRunnable::GetAttachmentFile(nsIFile* aMailboxFile,
+ const nsCString& aHeader,
+ nsIFile** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> attachmentFile;
+
+ rv = aMailboxFile->Clone(getter_AddRefs(attachmentFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = attachmentFile->Append(u"#Attach"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString nativeAttachmentPath;
+ rv = GetBeckyIncludeValue(aHeader, nativeAttachmentPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = attachmentFile->AppendRelativeNativePath(nativeAttachmentPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ attachmentFile->Exists(&exists);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ attachmentFile.forget(_retval);
+ return NS_OK;
+}
+
+nsresult ImportMessageRunnable::WriteAttachmentFile(
+ nsIFile* aMailboxFile, const nsCString& aHeader,
+ nsIOutputStream* aOutputStream) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> parentDirectory;
+ rv = aMailboxFile->GetParent(getter_AddRefs(parentDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> attachmentFile;
+ rv = GetAttachmentFile(parentDirectory, aHeader,
+ getter_AddRefs(attachmentFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), attachmentFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char buffer[FILE_IO_BUFFER_SIZE];
+ uint32_t readBytes = 0;
+ uint32_t writtenBytes = 0;
+ rv =
+ aOutputStream->Write(MSG_LINEBREAK, strlen(MSG_LINEBREAK), &writtenBytes);
+ while (NS_SUCCEEDED(inputStream->Read(buffer, sizeof(buffer), &readBytes)) &&
+ readBytes > 0) {
+ rv = aOutputStream->Write(buffer, readBytes, &writtenBytes);
+ if (NS_FAILED(rv)) break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP ImportMessageRunnable::Run() {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ mResult = mFolder->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(mResult, NS_OK);
+
+ nsCOMPtr<nsILineInputStream> lineStream;
+ mResult = nsBeckyUtils::CreateLineInputStream(mMessageFile,
+ getter_AddRefs(lineStream));
+ NS_ENSURE_SUCCESS(mResult, NS_OK);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIOutputStream> outputStream;
+ mResult = msgStore->GetNewMsgOutputStream(mFolder, getter_AddRefs(msgHdr),
+ getter_AddRefs(outputStream));
+ NS_ENSURE_SUCCESS(mResult, NS_OK);
+
+ bool inHeader = true;
+ bool more = true;
+ nsAutoCString headers;
+ while (NS_SUCCEEDED(mResult) && more) {
+ nsAutoCString line;
+ mResult = lineStream->ReadLine(line, &more);
+ if (NS_FAILED(mResult)) break;
+
+ if (inHeader) {
+ if (IsEndOfHeaders(line)) {
+ inHeader = false;
+ mResult = WriteHeaders(headers, outputStream);
+ } else {
+ mResult = HandleHeaderLine(line, headers);
+ }
+ } else if (IsEndOfMessage(line)) {
+ inHeader = true;
+ mResult = msgStore->FinishNewMessage(outputStream, msgHdr);
+ // outputStream is closed by FinishNewMessage().
+ outputStream = nullptr;
+ mResult = msgStore->GetNewMsgOutputStream(mFolder, getter_AddRefs(msgHdr),
+ getter_AddRefs(outputStream));
+ } else if (IsBeckyIncludeLine(line)) {
+ mResult = WriteAttachmentFile(mMessageFile, line, outputStream);
+ } else {
+ uint32_t writtenBytes = 0;
+ if (StringBeginsWith(line, ".."_ns))
+ line.Cut(0, 1);
+ else if (CheckHeaderKey(line, "From"))
+ line.Insert('>', 0);
+
+ line.AppendLiteral(MSG_LINEBREAK);
+ mResult = outputStream->Write(line.get(), line.Length(), &writtenBytes);
+ }
+ }
+
+ if (outputStream) {
+ // DiscardNewMessage() closes outputStream.
+ if (NS_FAILED(mResult))
+ msgStore->DiscardNewMessage(outputStream, msgHdr);
+ else
+ outputStream->Close(); /* No check? */
+ }
+
+ return NS_OK;
+}
+
+static nsresult ProxyImportMessage(nsIFile* aMessageFile,
+ nsIMsgFolder* aFolder) {
+ RefPtr<ImportMessageRunnable> importMessage =
+ new ImportMessageRunnable(aMessageFile, aFolder);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyImportMessage"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(importMessage));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return importMessage->mResult;
+}
+
+nsresult nsBeckyMail::ImportMailFile(nsIFile* aMailFile,
+ nsIMsgFolder* aDestination) {
+ int64_t size;
+ aMailFile->GetFileSize(&size);
+ if (size == 0) return NS_OK;
+
+ return ProxyImportMessage(aMailFile, aDestination);
+}
+
+NS_IMETHODIMP
+nsBeckyMail::ImportMailbox(nsIImportMailboxDescriptor* aSource,
+ nsIMsgFolder* aDestination, char16_t** aErrorLog,
+ char16_t** aSuccessLog, bool* aFatalError) {
+ NS_ENSURE_ARG_POINTER(aSource);
+ NS_ENSURE_ARG_POINTER(aDestination);
+ NS_ENSURE_ARG_POINTER(aErrorLog);
+ NS_ENSURE_ARG_POINTER(aSuccessLog);
+ NS_ENSURE_ARG_POINTER(aFatalError);
+
+ mReadBytes = 0;
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> mailboxFolder;
+ rv = aSource->GetFile(getter_AddRefs(mailboxFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ImportMailFile(mailboxFolder, aDestination);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t finalSize;
+ aSource->GetSize(&finalSize);
+ mReadBytes = finalSize;
+
+ nsAutoString name;
+ aSource->GetDisplayName(getter_Copies(name));
+
+ nsAutoString successMessage;
+ AutoTArray<nsString, 1> format = {name};
+ rv = nsBeckyStringBundle::FormatStringFromName("BeckyImportMailboxSuccess",
+ format, successMessage);
+ successMessage.AppendLiteral("\n");
+ *aSuccessLog = ToNewUnicode(successMessage);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBeckyMail::GetImportProgress(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mReadBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckyMail::TranslateFolderName(const nsAString& aFolderName,
+ nsAString& _retval) {
+ return nsBeckyUtils::TranslateFolderName(aFolderName, _retval);
+}
diff --git a/comm/mailnews/import/src/nsBeckyMail.h b/comm/mailnews/import/src/nsBeckyMail.h
new file mode 100644
index 0000000000..e0b445b1fe
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyMail.h
@@ -0,0 +1,41 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsBeckyMail_h___
+#define nsBeckyMail_h___
+
+#include "nsIImportMail.h"
+
+class nsIFile;
+class nsIMsgFolder;
+
+class nsBeckyMail final : public nsIImportMail {
+ public:
+ nsBeckyMail();
+ static nsresult Create(nsIImportMail** aImport);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTMAIL
+
+ private:
+ virtual ~nsBeckyMail();
+
+ uint32_t mReadBytes;
+
+ nsresult CollectMailboxesInDirectory(
+ nsIFile* aDirectory, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected);
+ nsresult CollectMailboxesInFolderListFile(
+ nsIFile* aListFile, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected);
+ nsresult AppendMailboxDescriptor(
+ nsIFile* aEntry, const nsString& aName, uint32_t aDepth,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& aCollected);
+ nsresult ImportMailFile(nsIFile* aMailFile, nsIMsgFolder* aDestination);
+ nsresult CreateMailboxDescriptor(nsIImportMailboxDescriptor** aDescriptor);
+ nsresult GetMailboxName(nsIFile* aMailbox, nsAString& aName);
+};
+
+#endif /* nsBeckyMail_h___ */
diff --git a/comm/mailnews/import/src/nsBeckySettings.cpp b/comm/mailnews/import/src/nsBeckySettings.cpp
new file mode 100644
index 0000000000..9722a62007
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckySettings.cpp
@@ -0,0 +1,379 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsIMsgAccountManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIINIParser.h"
+#include "nsISmtpService.h"
+#include "nsISmtpServer.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "msgCore.h"
+#include "nsBeckySettings.h"
+#include "nsBeckyStringBundle.h"
+#include "nsBeckyUtils.h"
+
+NS_IMPL_ISUPPORTS(nsBeckySettings, nsIImportSettings)
+
+nsresult nsBeckySettings::Create(nsIImportSettings** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsBeckySettings());
+ return NS_OK;
+}
+
+nsBeckySettings::nsBeckySettings() {}
+
+nsBeckySettings::~nsBeckySettings() {}
+
+NS_IMETHODIMP
+nsBeckySettings::AutoLocate(char16_t** aDescription, nsIFile** aLocation,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aDescription);
+ NS_ENSURE_ARG_POINTER(aLocation);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *aDescription = nsBeckyStringBundle::GetStringByName("BeckyImportName");
+ *aLocation = nullptr;
+ *_retval = false;
+
+ nsCOMPtr<nsIFile> location;
+ nsresult rv =
+ nsBeckyUtils::GetDefaultMailboxINIFile(getter_AddRefs(location));
+ if (NS_FAILED(rv))
+ location = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ else
+ *_retval = true;
+
+ location.forget(aLocation);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBeckySettings::SetLocation(nsIFile* aLocation) {
+ mLocation = aLocation;
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::CreateParser() {
+ if (!mLocation) {
+ nsresult rv =
+ nsBeckyUtils::GetDefaultMailboxINIFile(getter_AddRefs(mLocation));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // nsIINIParser accepts only UTF-8 encoding, so we need to convert the file
+ // first.
+ nsresult rv;
+ rv = nsBeckyUtils::ConvertToUTF8File(mLocation,
+ getter_AddRefs(mConvertedFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nsBeckyUtils::CreateINIParserForFile(mConvertedFile,
+ getter_AddRefs(mParser));
+}
+
+nsresult nsBeckySettings::CreateSmtpServer(const nsCString& aUserName,
+ const nsCString& aServerName,
+ nsISmtpServer** aServer,
+ bool* existing) {
+ nsresult rv;
+
+ nsCOMPtr<nsISmtpService> smtpService =
+ do_GetService("@mozilla.org/messengercompose/smtp;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISmtpServer> server;
+ rv = smtpService->FindServer(aUserName.get(), aServerName.get(),
+ getter_AddRefs(server));
+
+ if (NS_FAILED(rv) || !server) {
+ rv = smtpService->CreateServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ server->SetHostname(aServerName);
+ server->SetUsername(aUserName);
+ *existing = false;
+ } else {
+ *existing = true;
+ }
+
+ server.forget(aServer);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::CreateIncomingServer(const nsCString& aUserName,
+ const nsCString& aServerName,
+ const nsCString& aProtocol,
+ nsIMsgIncomingServer** aServer) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ accountManager->FindServer(aUserName, aServerName, aProtocol, 0,
+ getter_AddRefs(incomingServer));
+
+ if (!incomingServer) {
+ rv = accountManager->CreateIncomingServer(aUserName, aServerName, aProtocol,
+ getter_AddRefs(incomingServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ incomingServer.forget(aServer);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::SetupSmtpServer(nsISmtpServer** aServer) {
+ nsresult rv;
+ nsAutoCString userName, serverName;
+
+ mParser->GetString("Account"_ns, "SMTPServer"_ns, serverName);
+ mParser->GetString("Account"_ns, "UserID"_ns, userName);
+
+ nsCOMPtr<nsISmtpServer> server;
+ bool existing = false;
+ rv =
+ CreateSmtpServer(userName, serverName, getter_AddRefs(server), &existing);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we already have an existing server, do not touch it's settings.
+ if (existing) {
+ server.forget(aServer);
+ return NS_OK;
+ }
+
+ nsAutoCString value;
+ rv = mParser->GetString("Account"_ns, "SMTPPort"_ns, value);
+ int32_t port = 25;
+ if (NS_SUCCEEDED(rv)) {
+ nsresult errorCode;
+ port = value.ToInteger(&errorCode, 10);
+ }
+ server->SetPort(port);
+
+ mParser->GetString("Account"_ns, "SSLSMTP"_ns, value);
+ if (value.EqualsLiteral("1")) server->SetSocketType(nsMsgSocketType::SSL);
+
+ mParser->GetString("Account"_ns, "SMTPAUTH"_ns, value);
+ if (value.EqualsLiteral("1")) {
+ mParser->GetString("Account"_ns, "SMTPAUTHMODE"_ns, value);
+ nsMsgAuthMethodValue authMethod = nsMsgAuthMethod::none;
+ if (value.EqualsLiteral("1")) {
+ authMethod = nsMsgAuthMethod::passwordEncrypted;
+ } else if (value.EqualsLiteral("2") || value.EqualsLiteral("4") ||
+ value.EqualsLiteral("6")) {
+ authMethod = nsMsgAuthMethod::passwordCleartext;
+ } else {
+ authMethod = nsMsgAuthMethod::anything;
+ }
+ server->SetAuthMethod(authMethod);
+ }
+
+ server.forget(aServer);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::SetPop3ServerProperties(
+ nsIMsgIncomingServer* aServer) {
+ nsCOMPtr<nsIPop3IncomingServer> pop3Server = do_QueryInterface(aServer);
+
+ nsAutoCString value;
+ mParser->GetString("Account"_ns, "POP3Auth"_ns,
+ value); // 0: plain, 1: APOP, 2: CRAM-MD5, 3: NTLM
+ nsMsgAuthMethodValue authMethod;
+ if (value.IsEmpty() || value.EqualsLiteral("0")) {
+ authMethod = nsMsgAuthMethod::passwordCleartext;
+ } else if (value.EqualsLiteral("1")) {
+ authMethod = nsMsgAuthMethod::old;
+ } else if (value.EqualsLiteral("2")) {
+ authMethod = nsMsgAuthMethod::passwordEncrypted;
+ } else if (value.EqualsLiteral("3")) {
+ authMethod = nsMsgAuthMethod::NTLM;
+ } else {
+ authMethod = nsMsgAuthMethod::none;
+ }
+ aServer->SetAuthMethod(authMethod);
+
+ mParser->GetString("Account"_ns, "LeaveServer"_ns, value);
+ if (value.EqualsLiteral("1")) {
+ pop3Server->SetLeaveMessagesOnServer(true);
+ nsresult rv = mParser->GetString("Account"_ns, "KeepDays"_ns, value);
+ if (NS_FAILED(rv)) return NS_OK;
+
+ nsresult errorCode;
+ int32_t leftDays = value.ToInteger(&errorCode, 10);
+ if (NS_SUCCEEDED(errorCode)) {
+ pop3Server->SetNumDaysToLeaveOnServer(leftDays);
+ pop3Server->SetDeleteByAgeFromServer(true);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::SetupIncomingServer(nsIMsgIncomingServer** aServer) {
+ nsAutoCString value;
+ mParser->GetString("Account"_ns, "Protocol"_ns, value);
+ nsCString protocol;
+ if (value.EqualsLiteral("1")) {
+ protocol = "imap"_ns;
+ } else {
+ protocol = "pop3"_ns;
+ }
+
+ nsAutoCString userName, serverName;
+ mParser->GetString("Account"_ns, "MailServer"_ns, serverName);
+ mParser->GetString("Account"_ns, "UserID"_ns, userName);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = CreateIncomingServer(userName, serverName, protocol,
+ getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isSecure = false;
+ int32_t port = 0;
+ nsresult errorCode;
+ if (protocol.EqualsLiteral("pop3")) {
+ SetPop3ServerProperties(server);
+ rv = mParser->GetString("Account"_ns, "POP3Port"_ns, value);
+ if (NS_SUCCEEDED(rv))
+ port = value.ToInteger(&errorCode, 10);
+ else
+ port = 110;
+ mParser->GetString("Account"_ns, "SSLPOP"_ns, value);
+ if (value.EqualsLiteral("1")) isSecure = true;
+ } else if (protocol.EqualsLiteral("imap")) {
+ rv = mParser->GetString("Account"_ns, "IMAP4Port"_ns, value);
+ if (NS_SUCCEEDED(rv))
+ port = value.ToInteger(&errorCode, 10);
+ else
+ port = 143;
+ mParser->GetString("Account"_ns, "SSLIMAP"_ns, value);
+ if (value.EqualsLiteral("1")) isSecure = true;
+ }
+
+ server->SetPort(port);
+ if (isSecure) server->SetSocketType(nsMsgSocketType::SSL);
+
+ mParser->GetString("Account"_ns, "CheckInt"_ns, value);
+ if (value.EqualsLiteral("1")) server->SetDoBiff(true);
+ rv = mParser->GetString("Account"_ns, "CheckEvery"_ns, value);
+ if (NS_SUCCEEDED(rv)) {
+ int32_t minutes = value.ToInteger(&errorCode, 10);
+ if (NS_SUCCEEDED(errorCode)) server->SetBiffMinutes(minutes);
+ }
+
+ server.forget(aServer);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::CreateIdentity(nsIMsgIdentity** aIdentity) {
+ nsAutoCString email, fullName, identityName, bccAddress;
+
+ mParser->GetString("Account"_ns, "Name"_ns, identityName);
+ mParser->GetString("Account"_ns, "YourName"_ns, fullName);
+ mParser->GetString("Account"_ns, "MailAddress"_ns, email);
+ mParser->GetString("Account"_ns, "PermBcc"_ns, bccAddress);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = accountManager->CreateIdentity(getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ identity->SetLabel(NS_ConvertUTF8toUTF16(identityName));
+ identity->SetFullName(NS_ConvertUTF8toUTF16(fullName));
+ identity->SetEmail(email);
+ if (!bccAddress.IsEmpty()) {
+ identity->SetDoBcc(true);
+ identity->SetDoBccList(bccAddress);
+ }
+
+ identity.forget(aIdentity);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::CreateAccount(nsIMsgIdentity* aIdentity,
+ nsIMsgIncomingServer* aIncomingServer,
+ nsIMsgAccount** aAccount) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->CreateAccount(getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = account->AddIdentity(aIdentity);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = account->SetIncomingServer(aIncomingServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ account.forget(aAccount);
+
+ return NS_OK;
+}
+
+nsresult nsBeckySettings::RemoveConvertedFile() {
+ if (mConvertedFile) {
+ bool exists;
+ mConvertedFile->Exists(&exists);
+ if (exists) mConvertedFile->Remove(false);
+ mConvertedFile = nullptr;
+ }
+ return NS_OK;
+}
+
+#define NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(expr, rv) \
+ if (NS_FAILED(expr)) { \
+ RemoveConvertedFile(); \
+ return rv; \
+ }
+
+NS_IMETHODIMP
+nsBeckySettings::Import(nsIMsgAccount** aLocalMailAccount, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aLocalMailAccount);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv = CreateParser();
+ NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = SetupIncomingServer(getter_AddRefs(incomingServer));
+ NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(rv, rv);
+
+ nsCOMPtr<nsISmtpServer> smtpServer;
+ rv = SetupSmtpServer(getter_AddRefs(smtpServer));
+ NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = CreateIdentity(getter_AddRefs(identity));
+ NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(rv, rv);
+
+ nsAutoCString smtpKey;
+ smtpServer->GetKey(getter_Copies(smtpKey));
+ identity->SetSmtpServerKey(smtpKey);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = CreateAccount(identity, incomingServer, getter_AddRefs(account));
+ NS_RETURN_IF_FAILED_WITH_REMOVE_CONVERTED_FILE(rv, rv);
+
+ RemoveConvertedFile();
+ if (aLocalMailAccount) account.forget(aLocalMailAccount);
+ *_retval = true;
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsBeckySettings.h b/comm/mailnews/import/src/nsBeckySettings.h
new file mode 100644
index 0000000000..bbbe9fe268
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckySettings.h
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsBeckySettings_h___
+#define nsBeckySettings_h___
+
+#include "nsIImportSettings.h"
+#include "nsIFile.h"
+#include "nsIINIParser.h"
+
+class nsIMsgIncomingServer;
+class nsIMsgIdentity;
+class nsISmtpServer;
+
+class nsBeckySettings final : public nsIImportSettings {
+ public:
+ nsBeckySettings();
+ static nsresult Create(nsIImportSettings** aImport);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTSETTINGS
+
+ private:
+ virtual ~nsBeckySettings();
+
+ nsCOMPtr<nsIFile> mLocation;
+ nsCOMPtr<nsIFile> mConvertedFile;
+ nsCOMPtr<nsIINIParser> mParser;
+
+ nsresult CreateParser();
+ nsresult CreateIdentity(nsIMsgIdentity** aIdentity);
+ nsresult CreateAccount(nsIMsgIdentity* aIdentity,
+ nsIMsgIncomingServer* aIncomingServer,
+ nsIMsgAccount** aAccount);
+ nsresult CreateSmtpServer(const nsCString& aUserName,
+ const nsCString& aServerName,
+ nsISmtpServer** aServer, bool* existing);
+ nsresult CreateIncomingServer(const nsCString& aUserName,
+ const nsCString& aServerName,
+ const nsCString& aProtocol,
+ nsIMsgIncomingServer** aServer);
+ nsresult SetupIncomingServer(nsIMsgIncomingServer** aServer);
+ nsresult SetupSmtpServer(nsISmtpServer** aServer);
+ nsresult SetPop3ServerProperties(nsIMsgIncomingServer* aServer);
+ nsresult RemoveConvertedFile();
+};
+
+#endif /* nsBeckySettings_h___ */
diff --git a/comm/mailnews/import/src/nsBeckyStringBundle.cpp b/comm/mailnews/import/src/nsBeckyStringBundle.cpp
new file mode 100644
index 0000000000..1037fdafb2
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyStringBundle.cpp
@@ -0,0 +1,55 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "prprf.h"
+#include "prmem.h"
+#include "nsCOMPtr.h"
+#include "nsIStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "nsXPCOMCIDInternal.h"
+
+#include "nsBeckyStringBundle.h"
+
+#define BECKY_MESSAGES_URL \
+ "chrome://messenger/locale/beckyImportMsgs.properties"
+
+nsCOMPtr<nsIStringBundle> nsBeckyStringBundle::mBundle = nullptr;
+
+void nsBeckyStringBundle::GetStringBundle(void) {
+ if (mBundle) return;
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && bundleService)
+ rv = bundleService->CreateBundle(BECKY_MESSAGES_URL,
+ getter_AddRefs(mBundle));
+}
+
+void nsBeckyStringBundle::EnsureStringBundle(void) {
+ if (!mBundle) GetStringBundle();
+}
+
+char16_t* nsBeckyStringBundle::GetStringByName(const char* aName) {
+ EnsureStringBundle();
+
+ if (mBundle) {
+ nsAutoString string;
+ mBundle->GetStringFromName(aName, string);
+ return ToNewUnicode(string);
+ }
+
+ return nullptr;
+}
+
+nsresult nsBeckyStringBundle::FormatStringFromName(const char* name,
+ nsTArray<nsString>& params,
+ nsAString& _retval) {
+ EnsureStringBundle();
+
+ return mBundle->FormatStringFromName(name, params, _retval);
+}
+
+void nsBeckyStringBundle::Cleanup(void) { mBundle = nullptr; }
diff --git a/comm/mailnews/import/src/nsBeckyStringBundle.h b/comm/mailnews/import/src/nsBeckyStringBundle.h
new file mode 100644
index 0000000000..2b7045ebcc
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyStringBundle.h
@@ -0,0 +1,32 @@
+/* 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/. */
+#ifndef _nsBeckyStringBundle_H__
+#define _nsBeckyStringBundle_H__
+
+#include "nsString.h"
+
+class nsIStringBundle;
+
+class nsBeckyStringBundle final {
+ public:
+ static char16_t* GetStringByName(const char* name);
+ static nsresult FormatStringFromName(const char* name,
+ nsTArray<nsString>& params,
+ nsAString& _retval);
+ static void GetStringBundle(void);
+ static void EnsureStringBundle(void);
+ static void Cleanup(void);
+
+ private:
+ static nsCOMPtr<nsIStringBundle> mBundle;
+};
+
+#define BECKYIMPORT_NAME 2000
+#define BECKYIMPORT_DESCRIPTION 2001
+#define BECKYIMPORT_MAILBOX_SUCCESS 2002
+#define BECKYIMPORT_MAILBOX_BADPARAM 2003
+#define BECKYIMPORT_MAILBOX_CONVERTERROR 2004
+#define BECKYIMPORT_ADDRESS_SUCCESS 2005
+
+#endif /* _nsBeckyStringBundle_H__ */
diff --git a/comm/mailnews/import/src/nsBeckyUtils.cpp b/comm/mailnews/import/src/nsBeckyUtils.cpp
new file mode 100644
index 0000000000..ecb1c35a53
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyUtils.cpp
@@ -0,0 +1,302 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsString.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsILineInputStream.h"
+#include "nsIConverterInputStream.h"
+#include "nsIConverterOutputStream.h"
+#include "nsMsgI18N.h"
+#include "nsNetUtil.h"
+#include "nsIINIParser.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsMsgUtils.h"
+#include "msgCore.h"
+#include "nsIImportMail.h"
+#include "nsThreadUtils.h"
+
+#include "nsBeckyUtils.h"
+#include "SpecialSystemDirectory.h"
+
+nsresult nsBeckyUtils::FindUserDirectoryOnWindows7(nsIFile** aLocation) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> directory;
+ rv = GetSpecialSystemDirectory(Win_Documents, getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = directory->AppendNative("Becky"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ rv = directory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ bool isDirectory = false;
+ rv = directory->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDirectory) return NS_ERROR_FILE_NOT_FOUND;
+
+ directory.forget(aLocation);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::FindUserDirectoryOnWindowsXP(nsIFile** aLocation) {
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> directory =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = directory->InitWithPath(u"C:\\Becky!"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ rv = directory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ bool isDirectory = false;
+ rv = directory->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDirectory) return NS_ERROR_FILE_NOT_FOUND;
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = directory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory = false;
+ rv = file->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isDirectory) {
+ file.forget(aLocation);
+ return NS_OK;
+ }
+ }
+
+ directory.forget(aLocation);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::FindUserDirectory(nsIFile** aLocation) {
+ nsresult rv = FindUserDirectoryOnWindows7(aLocation);
+ if (rv == NS_ERROR_FILE_NOT_FOUND) {
+ rv = FindUserDirectoryOnWindowsXP(aLocation);
+ }
+ return rv;
+}
+
+nsresult nsBeckyUtils::ConvertNativeStringToUTF8(const nsACString& aOriginal,
+ nsACString& _retval) {
+ nsresult rv;
+ nsAutoString unicodeString;
+ rv = NS_CopyNativeToUnicode(aOriginal, unicodeString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CopyUTF16toUTF8(unicodeString, _retval);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::CreateLineInputStream(nsIFile* aFile,
+ nsILineInputStream** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(inputStream, _retval);
+}
+
+nsresult nsBeckyUtils::GetFolderListFile(nsIFile* aLocation,
+ nsIFile** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> folderListFile;
+ rv = aLocation->Clone(getter_AddRefs(folderListFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folderListFile->Append(u"Folder.lst"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists;
+ rv = folderListFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ folderListFile.forget(_retval);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::GetDefaultFolderName(nsIFile* aFolderListFile,
+ nsACString& name) {
+ nsresult rv;
+ nsCOMPtr<nsILineInputStream> lineStream;
+ rv = CreateLineInputStream(aFolderListFile, getter_AddRefs(lineStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ rv = lineStream->ReadLine(name, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::GetDefaultMailboxDirectory(nsIFile** _retval) {
+ nsCOMPtr<nsIFile> userDirectory;
+ nsresult rv = FindUserDirectory(getter_AddRefs(userDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> folderListFile;
+ rv = GetFolderListFile(userDirectory, getter_AddRefs(folderListFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString defaultFolderName;
+ rv = GetDefaultFolderName(folderListFile, defaultFolderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = userDirectory->AppendNative(defaultFolderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists;
+ rv = userDirectory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ bool isDirectory = false;
+ rv = userDirectory->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDirectory) return NS_ERROR_FILE_NOT_FOUND;
+
+ userDirectory.forget(_retval);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::GetDefaultMailboxINIFile(nsIFile** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> mailboxDirectory;
+ rv = GetDefaultMailboxDirectory(getter_AddRefs(mailboxDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetMailboxINIFile(mailboxDirectory, _retval);
+}
+
+nsresult nsBeckyUtils::GetMailboxINIFile(nsIFile* aDirectory,
+ nsIFile** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> target;
+ rv = aDirectory->Clone(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = target->Append(u"Mailbox.ini"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool exists;
+ rv = target->Exists(&exists);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+
+ target.forget(_retval);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::CreateINIParserForFile(nsIFile* aFile,
+ nsIINIParser** aParser) {
+ nsresult rv;
+ nsCOMPtr<nsIINIParserFactory> factory =
+ do_GetService("@mozilla.org/xpcom/ini-processor-factory;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return factory->CreateINIParser(aFile, aParser);
+}
+
+nsresult nsBeckyUtils::GetMailboxNameFromINIFile(nsIFile* aFile,
+ nsCString& aName) {
+ nsresult rv;
+ nsCOMPtr<nsIINIParser> parser;
+ rv = CreateINIParserForFile(aFile, getter_AddRefs(parser));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return parser->GetString("Account"_ns, "Name"_ns, aName);
+}
+
+nsresult nsBeckyUtils::ConvertToUTF8File(nsIFile* aSourceFile,
+ nsIFile** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> convertedFile;
+ rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR,
+ "thunderbird-becky-import",
+ getter_AddRefs(convertedFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = convertedFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> source;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(source), aSourceFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString sourceCharset;
+ rv = MsgDetectCharsetFromFile(aSourceFile, sourceCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIOutputStream> destination;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(destination), convertedFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const uint32_t kBlock = 8192;
+
+ nsCOMPtr<nsIConverterInputStream> convertedInput =
+ do_CreateInstance("@mozilla.org/intl/converter-input-stream;1");
+ convertedInput->Init(source, sourceCharset.get(), kBlock, 0x0000);
+
+ nsCOMPtr<nsIConverterOutputStream> convertedOutput =
+ do_CreateInstance("@mozilla.org/intl/converter-output-stream;1");
+ convertedOutput->Init(destination, "UTF-8");
+
+ char16_t* line = (char16_t*)moz_xmalloc(kBlock);
+ uint32_t readBytes = kBlock;
+ bool writtenBytes;
+ while (readBytes == kBlock) {
+ rv = convertedInput->Read(line, kBlock, &readBytes);
+ rv = convertedOutput->Write(readBytes, line, &writtenBytes);
+ }
+ convertedOutput->Close();
+ convertedInput->Close();
+
+ convertedFile.forget(_retval);
+ return NS_OK;
+}
+
+nsresult nsBeckyUtils::TranslateFolderName(const nsAString& aFolderName,
+ nsAString& _retval) {
+ if (aFolderName.LowerCaseEqualsLiteral("!trash"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestTrashFolderName);
+ else if (aFolderName.LowerCaseEqualsLiteral("!!!!inbox"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestInboxFolderName);
+ else if (aFolderName.LowerCaseEqualsLiteral("!!!!outbox"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestSentFolderName);
+ else if (aFolderName.LowerCaseEqualsLiteral("!!!!unsent"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestUnsentMessagesFolderName);
+ else
+ _retval = aFolderName;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsBeckyUtils.h b/comm/mailnews/import/src/nsBeckyUtils.h
new file mode 100644
index 0000000000..9a0529b5dc
--- /dev/null
+++ b/comm/mailnews/import/src/nsBeckyUtils.h
@@ -0,0 +1,35 @@
+/* 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/. */
+#ifndef _nsBeckyUtils_H__
+#define _nsBeckyUtils_H__
+
+#include "nsString.h"
+class nsIFile;
+class nsILineInputStream;
+class nsIINIParser;
+
+class nsBeckyUtils final {
+ public:
+ static nsresult FindUserDirectoryOnWindows7(nsIFile** aLocation);
+ static nsresult FindUserDirectoryOnWindowsXP(nsIFile** aLocation);
+ static nsresult FindUserDirectory(nsIFile** aFile);
+ static nsresult ConvertNativeStringToUTF8(const nsACString& aOriginal,
+ nsACString& _retval);
+ static nsresult CreateLineInputStream(nsIFile* aFile,
+ nsILineInputStream** _retval);
+ static nsresult GetDefaultMailboxDirectory(nsIFile** _retval);
+ static nsresult GetFolderListFile(nsIFile* aLocation, nsIFile** _retval);
+ static nsresult GetDefaultFolderName(nsIFile* aFolderListFile,
+ nsACString& name);
+ static nsresult GetDefaultMailboxINIFile(nsIFile** _retval);
+ static nsresult GetMailboxINIFile(nsIFile* aDirectory, nsIFile** _retval);
+ static nsresult CreateINIParserForFile(nsIFile* aFile,
+ nsIINIParser** aParser);
+ static nsresult GetMailboxNameFromINIFile(nsIFile* aFile, nsCString& aName);
+ static nsresult ConvertToUTF8File(nsIFile* aSourceFile, nsIFile** _retval);
+ static nsresult TranslateFolderName(const nsAString& aFolderName,
+ nsAString& _retval);
+};
+
+#endif /* _nsBeckyUtils_H__ */
diff --git a/comm/mailnews/import/src/nsEmlxHelperUtils.h b/comm/mailnews/import/src/nsEmlxHelperUtils.h
new file mode 100644
index 0000000000..a7e0e69f11
--- /dev/null
+++ b/comm/mailnews/import/src/nsEmlxHelperUtils.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsEmlxHelperUtils_h___
+#define nsEmlxHelperUtils_h___
+
+#include "nscore.h"
+#include "nsString.h"
+
+class nsIOutputStream;
+class nsIFile;
+
+class nsEmlxHelperUtils {
+ /* All emlx messages have a "flags" number in the metadata.
+ These are the masks to decode that, found via
+ http://jwz.livejournal.com/505711.html */
+ enum EmlxMetadataMask {
+ kRead = 1 << 0, // read
+ // 1 << 1, // deleted
+ kAnswered = 1 << 2, // answered
+ // 1 << 3, // encrypted
+ kFlagged = 1 << 4, // flagged
+ // 1 << 5, // recent
+ // 1 << 6, // draft
+ // 1 << 7, // initial (no longer used)
+ kForwarded = 1 << 8, // forwarded
+ // 1 << 9, // redirected
+ // 3F << 10, // attachment count (6 bits)
+ // 7F << 16, // priority level (7 bits)
+ // 1 << 23, // signed
+ // 1 << 24, // is junk
+ // 1 << 25, // is not junk
+ // 1 << 26, // font size delta 7 (3 bits)
+ // 1 << 29, // junk mail level recorded
+ // 1 << 30, // highlight text in toc
+ // 1 << 31 // (unused)
+ };
+
+ // This method will scan the raw EMLX message buffer for "dangerous" so-called
+ // "From-lines" that we need to escape. If it needs to modify any lines, it
+ // will return a non-NULL aOutBuffer. If aOutBuffer is NULL, no modification
+ // needed to be made.
+ static nsresult ConvertToMboxRD(const char* aMessageBufferStart,
+ const char* aMessageBufferEnd,
+ nsCString& aOutBuffer);
+
+ // returns an int representing the X-Mozilla-Status flags set (e.g. "read",
+ // "flagged") converted from EMLX flags.
+ static nsresult ConvertToMozillaStatusFlags(const char* aXMLBufferStart,
+ const char* aXMLBufferEnd,
+ uint32_t* aMozillaStatusFlags);
+
+ public:
+ // add an .emlx message to the mbox output
+ static nsresult AddEmlxMessageToStream(nsIFile* aEmlxFile,
+ nsIOutputStream* aOutoutStream);
+};
+
+#endif // nsEmlxHelperUtils_h___
diff --git a/comm/mailnews/import/src/nsEmlxHelperUtils.mm b/comm/mailnews/import/src/nsEmlxHelperUtils.mm
new file mode 100644
index 0000000000..a0c5aaaee2
--- /dev/null
+++ b/comm/mailnews/import/src/nsEmlxHelperUtils.mm
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsEmlxHelperUtils.h"
+#include "nsIOutputStream.h"
+#include "nsNetUtil.h"
+#include "nsCOMPtr.h"
+#include "nsObjCExceptions.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "msgCore.h"
+#include "nsTArray.h"
+#include "nsAppleMailImport.h"
+#include "prprf.h"
+#include "nsIFile.h"
+
+#import <Cocoa/Cocoa.h>
+
+nsresult nsEmlxHelperUtils::ConvertToMozillaStatusFlags(const char* aXMLBufferStart,
+ const char* aXMLBufferEnd,
+ uint32_t* aMozillaStatusFlags) {
+ // create a NSData wrapper around the buffer, so we can use the Cocoa call below
+ NSData* metadata = [[[NSData alloc] initWithBytesNoCopy:(void*)aXMLBufferStart
+ length:(aXMLBufferEnd - aXMLBufferStart)
+ freeWhenDone:NO] autorelease];
+
+ // get the XML data as a dictionary
+ NSPropertyListFormat format;
+ id plist = [NSPropertyListSerialization propertyListWithData:metadata
+ options:NSPropertyListImmutable
+ format:&format
+ error:NULL];
+
+ if (!plist) return NS_ERROR_FAILURE;
+
+ // find the <flags>...</flags> value and convert to int
+ const uint32_t emlxMessageFlags = [[(NSDictionary*)plist objectForKey:@"flags"] intValue];
+
+ if (emlxMessageFlags == 0) return NS_ERROR_FAILURE;
+
+ if (emlxMessageFlags & nsEmlxHelperUtils::kRead) *aMozillaStatusFlags |= nsMsgMessageFlags::Read;
+ if (emlxMessageFlags & nsEmlxHelperUtils::kForwarded)
+ *aMozillaStatusFlags |= nsMsgMessageFlags::Forwarded;
+ if (emlxMessageFlags & nsEmlxHelperUtils::kAnswered)
+ *aMozillaStatusFlags |= nsMsgMessageFlags::Replied;
+ if (emlxMessageFlags & nsEmlxHelperUtils::kFlagged)
+ *aMozillaStatusFlags |= nsMsgMessageFlags::Marked;
+
+ return NS_OK;
+}
+
+nsresult nsEmlxHelperUtils::ConvertToMboxRD(const char* aMessageBufferStart,
+ const char* aMessageBufferEnd, nsCString& aOutBuffer) {
+ nsTArray<const char*> foundFromLines;
+
+ const char* cur = aMessageBufferStart;
+ while (cur < aMessageBufferEnd) {
+ const char* foundFromStr = strnstr(cur, "From ", aMessageBufferEnd - cur);
+
+ if (foundFromStr) {
+ // skip all prepending '>' chars
+ const char* fromLineStart = foundFromStr;
+ while (fromLineStart-- >= aMessageBufferStart) {
+ if (*fromLineStart == '\n' || fromLineStart == aMessageBufferStart) {
+ if (fromLineStart > aMessageBufferStart) fromLineStart++;
+ foundFromLines.AppendElement(fromLineStart);
+ break;
+ } else if (*fromLineStart != '>')
+ break;
+ }
+
+ // advance past the last found From string.
+ cur = foundFromStr + 5;
+
+ // look for more From lines.
+ continue;
+ }
+
+ break;
+ }
+
+ // go through foundFromLines
+ if (foundFromLines.Length()) {
+ const char* chunkStart = aMessageBufferStart;
+ for (unsigned i = 0; i < foundFromLines.Length(); ++i) {
+ aOutBuffer.Append(chunkStart, (foundFromLines[i] - chunkStart));
+ aOutBuffer.Append(">"_ns);
+
+ chunkStart = foundFromLines[i];
+ }
+ aOutBuffer.Append(chunkStart, (aMessageBufferEnd - chunkStart));
+ }
+
+ return NS_OK;
+}
+
+nsresult nsEmlxHelperUtils::AddEmlxMessageToStream(nsIFile* aMessage, nsIOutputStream* aOut) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // needed to be sure autoreleased objects are released too, which they might not
+ // in a C++ environment where the main event loop has no autorelease pool (e.g on a XPCOM thread)
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsAutoCString path;
+ aMessage->GetNativePath(path);
+
+ NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:path.get()]];
+ if (!data) {
+ [pool release];
+ return NS_ERROR_FAILURE;
+ }
+
+ char* startOfMessageData = NULL;
+ uint32_t actualBytesWritten = 0;
+
+ // The anatomy of an EMLX file:
+ //
+ // -------------------------------
+ // < A number describing how many bytes ahead there is message data >
+ // < Message data >
+ // < XML metadata for this message >
+ // -------------------------------
+
+ // read the first line of the emlx file, which is a number of how many bytes ahead the actual
+ // message data is.
+ uint64_t numberOfBytesToRead = strtol((char*)[data bytes], &startOfMessageData, 10);
+ if (numberOfBytesToRead <= 0 || !startOfMessageData) {
+ [pool release];
+ return NS_ERROR_FAILURE;
+ }
+
+ // skip whitespace
+ while (*startOfMessageData == ' ' || *startOfMessageData == '\n' || *startOfMessageData == '\r' ||
+ *startOfMessageData == '\t')
+ ++startOfMessageData;
+
+ constexpr auto kBogusFromLine = "From \n"_ns;
+ constexpr auto kEndOfMessage = "\n\n"_ns;
+
+ // write the bogus "From " line which is a magic separator in the mbox format
+ rv = aOut->Write(kBogusFromLine.get(), kBogusFromLine.Length(), &actualBytesWritten);
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ // now read the XML metadata, so we can extract info like which flags (read? replied? flagged?
+ // etc) this message has.
+ const char* startOfXMLMetadata = startOfMessageData + numberOfBytesToRead;
+ const char* endOfXMLMetadata = (char*)[data bytes] + [data length];
+
+ uint32_t x_mozilla_flags = 0;
+ ConvertToMozillaStatusFlags(startOfXMLMetadata, endOfXMLMetadata, &x_mozilla_flags);
+
+ // write the X-Mozilla-Status header according to which flags we've gathered above.
+ uint32_t dummyRv;
+ nsAutoCString buf(PR_smprintf(X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK, x_mozilla_flags));
+ NS_ASSERTION(!buf.IsEmpty(), "printf error with X-Mozilla-Status header");
+ if (buf.IsEmpty()) {
+ [pool release];
+ return rv;
+ }
+
+ rv = aOut->Write(buf.get(), buf.Length(), &dummyRv);
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ // write out X-Mozilla-Keywords header as well to reserve some space for it
+ // in the mbox file.
+ rv = aOut->Write(X_MOZILLA_KEYWORDS, X_MOZILLA_KEYWORDS_LEN, &dummyRv);
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ // write out empty X-Mozilla_status2 header
+ buf.Adopt(PR_smprintf(X_MOZILLA_STATUS2_FORMAT MSG_LINEBREAK, 0));
+ NS_ASSERTION(!buf.IsEmpty(), "printf error with X-Mozilla-Status2 header");
+ if (buf.IsEmpty()) {
+ [pool release];
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ rv = aOut->Write(buf.get(), buf.Length(), &dummyRv);
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ // do any conversion needed for the mbox data to be valid mboxrd.
+ nsCString convertedData;
+ rv = ConvertToMboxRD(startOfMessageData, (startOfMessageData + numberOfBytesToRead),
+ convertedData);
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ // write the actual message data.
+ if (convertedData.IsEmpty())
+ rv = aOut->Write(startOfMessageData, (uint32_t)numberOfBytesToRead, &actualBytesWritten);
+ else {
+ IMPORT_LOG1("Escaped From-lines in %s!", path.get());
+ rv = aOut->Write(convertedData.get(), convertedData.Length(), &actualBytesWritten);
+ }
+
+ if (NS_FAILED(rv)) {
+ [pool release];
+ return rv;
+ }
+
+ NS_ASSERTION(actualBytesWritten ==
+ (convertedData.IsEmpty() ? numberOfBytesToRead : convertedData.Length()),
+ "Didn't write as many bytes as expected for .emlx file?");
+
+ // add newlines to denote the end of this message in the mbox
+ rv = aOut->Write(kEndOfMessage.get(), kEndOfMessage.Length(), &actualBytesWritten);
+
+ [pool release];
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/comm/mailnews/import/src/nsImportABDescriptor.cpp b/comm/mailnews/import/src/nsImportABDescriptor.cpp
new file mode 100644
index 0000000000..983f1c6ffb
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportABDescriptor.cpp
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsImportABDescriptor.h"
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsImportABDescriptor::Create(REFNSIID aIID, void** aResult) {
+ RefPtr<nsImportABDescriptor> it = new nsImportABDescriptor();
+ return it->QueryInterface(aIID, aResult);
+}
+
+NS_IMPL_ISUPPORTS(nsImportABDescriptor, nsIImportABDescriptor)
+
+nsImportABDescriptor::nsImportABDescriptor()
+ : mId(0), mRef(0), mSize(0), mImport(true) {}
diff --git a/comm/mailnews/import/src/nsImportABDescriptor.h b/comm/mailnews/import/src/nsImportABDescriptor.h
new file mode 100644
index 0000000000..681ed8e83f
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportABDescriptor.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsImportABDescriptor_h___
+#define nsImportABDescriptor_h___
+
+#include "mozilla/Attributes.h"
+#include "nscore.h"
+#include "nsString.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+
+////////////////////////////////////////////////////////////////////////
+
+class nsImportABDescriptor : public nsIImportABDescriptor {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD GetIdentifier(uint32_t* pIdentifier) override {
+ *pIdentifier = mId;
+ return NS_OK;
+ }
+ NS_IMETHOD SetIdentifier(uint32_t ident) override {
+ mId = ident;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetRef(uint32_t* pRef) override {
+ *pRef = mRef;
+ return NS_OK;
+ }
+ NS_IMETHOD SetRef(uint32_t ref) override {
+ mRef = ref;
+ return NS_OK;
+ }
+
+ /* attribute unsigned long size; */
+ NS_IMETHOD GetSize(uint32_t* pSize) override {
+ *pSize = mSize;
+ return NS_OK;
+ }
+ NS_IMETHOD SetSize(uint32_t theSize) override {
+ mSize = theSize;
+ return NS_OK;
+ }
+
+ /* attribute AString displayName; */
+ NS_IMETHOD GetPreferredName(nsAString& aName) override {
+ aName = mDisplayName;
+ return NS_OK;
+ }
+ NS_IMETHOD SetPreferredName(const nsAString& aName) override {
+ mDisplayName = aName;
+ return NS_OK;
+ }
+
+ /* readonly attribute nsIFile fileSpec; */
+ NS_IMETHOD GetAbFile(nsIFile** aFile) override {
+ if (!mFile) return NS_ERROR_NULL_POINTER;
+
+ return mFile->Clone(aFile);
+ }
+
+ NS_IMETHOD SetAbFile(nsIFile* aFile) override {
+ if (!aFile) {
+ mFile = nullptr;
+ return NS_OK;
+ }
+
+ return aFile->Clone(getter_AddRefs(mFile));
+ }
+
+ /* attribute boolean import; */
+ NS_IMETHOD GetImport(bool* pImport) override {
+ *pImport = mImport;
+ return NS_OK;
+ }
+ NS_IMETHOD SetImport(bool doImport) override {
+ mImport = doImport;
+ return NS_OK;
+ }
+
+ nsImportABDescriptor();
+
+ static nsresult Create(REFNSIID aIID, void** aResult);
+
+ private:
+ virtual ~nsImportABDescriptor() {}
+ uint32_t mId; // used by creator of the structure
+ uint32_t mRef; // depth in the hierarchy
+ nsString mDisplayName; // name of this mailbox
+ nsCOMPtr<nsIFile> mFile; // source file (if applicable)
+ uint32_t mSize; // size
+ bool mImport; // import it or not?
+};
+
+#endif
diff --git a/comm/mailnews/import/src/nsImportAddressBooks.cpp b/comm/mailnews/import/src/nsImportAddressBooks.cpp
new file mode 100644
index 0000000000..b39e7c68ef
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportAddressBooks.cpp
@@ -0,0 +1,571 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsImportAddressBooks.h"
+
+#include "plstr.h"
+#include "nsIImportService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIAbManager.h"
+#include "nsImportStringBundle.h"
+#include "nsTextFormatter.h"
+#include "msgCore.h"
+#include "ImportDebug.h"
+
+nsresult NS_NewGenericAddressBooks(nsIImportGeneric** aImportGeneric) {
+ NS_ASSERTION(aImportGeneric != nullptr, "null ptr");
+ if (!aImportGeneric) return NS_ERROR_NULL_POINTER;
+
+ RefPtr<nsImportGenericAddressBooks> pGen = new nsImportGenericAddressBooks();
+ return pGen->QueryInterface(NS_GET_IID(nsIImportGeneric),
+ (void**)aImportGeneric);
+}
+
+nsImportGenericAddressBooks::nsImportGenericAddressBooks() {
+ m_totalSize = 0;
+ m_doImport = false;
+ m_pThreadData = nullptr;
+
+ m_autoFind = false;
+ m_description = nullptr;
+ m_gotLocation = false;
+ m_found = false;
+ m_userVerify = false;
+
+ nsImportStringBundle::GetStringBundle(IMPORT_MSGS_URL,
+ getter_AddRefs(m_stringBundle));
+}
+
+nsImportGenericAddressBooks::~nsImportGenericAddressBooks() {
+ if (m_description) free(m_description);
+}
+
+NS_IMPL_ISUPPORTS(nsImportGenericAddressBooks, nsIImportGeneric)
+
+NS_IMETHODIMP nsImportGenericAddressBooks::GetData(const char* dataId,
+ nsISupports** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv;
+ *_retval = nullptr;
+ if (!PL_strcasecmp(dataId, "addressInterface")) {
+ NS_IF_ADDREF(*_retval = m_pInterface);
+ }
+
+ if (!PL_strcasecmp(dataId, "addressLocation")) {
+ if (!m_pLocation) GetDefaultLocation();
+ NS_IF_ADDREF(*_retval = m_pLocation);
+ }
+
+ if (!PL_strcasecmp(dataId, "addressDestination")) {
+ if (!m_pDestinationUri.IsEmpty()) {
+ nsCOMPtr<nsISupportsCString> abString =
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ abString->SetData(m_pDestinationUri);
+ abString.forget(_retval);
+ }
+ }
+
+ if (!PL_strcasecmp(dataId, "fieldMap")) {
+ if (m_pFieldMap) {
+ NS_ADDREF(*_retval = m_pFieldMap);
+ } else {
+ if (m_pInterface && m_pLocation) {
+ bool needsIt = false;
+ m_pInterface->GetNeedsFieldMap(m_pLocation, &needsIt);
+ if (needsIt) {
+ GetDefaultFieldMap();
+ if (m_pFieldMap) {
+ NS_ADDREF(*_retval = m_pFieldMap);
+ }
+ }
+ }
+ }
+ }
+
+ if (!PL_strncasecmp(dataId, "sampleData-", 11)) {
+ // extra the record number
+ const char* pNum = dataId + 11;
+ int32_t rNum = 0;
+ while (*pNum) {
+ rNum *= 10;
+ rNum += (*pNum - '0');
+ pNum++;
+ }
+ IMPORT_LOG1("Requesting sample data #: %ld\n", (long)rNum);
+ if (m_pInterface) {
+ nsCOMPtr<nsISupportsString> data =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+ char16_t* pData = nullptr;
+ bool found = false;
+ rv = m_pInterface->GetSampleData(rNum, &found, &pData);
+ if (NS_FAILED(rv)) return rv;
+ if (found) {
+ data->SetData(nsDependentString(pData));
+ data.forget(_retval);
+ }
+ free(pData);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::SetData(const char* dataId,
+ nsISupports* item) {
+ NS_ASSERTION(dataId != nullptr, "null ptr");
+ if (!dataId) return NS_ERROR_NULL_POINTER;
+
+ if (!PL_strcasecmp(dataId, "addressInterface")) {
+ m_pInterface = nullptr;
+ if (item) m_pInterface = do_QueryInterface(item);
+ }
+
+ if (!PL_strcasecmp(dataId, "addressLocation")) {
+ m_pLocation = nullptr;
+
+ if (item) {
+ nsresult rv;
+ m_pLocation = do_QueryInterface(item, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (m_pInterface) m_pInterface->SetSampleLocation(m_pLocation);
+ }
+
+ if (!PL_strcasecmp(dataId, "addressDestination")) {
+ if (item) {
+ nsCOMPtr<nsISupportsCString> abString = do_QueryInterface(item);
+ if (abString) {
+ abString->GetData(m_pDestinationUri);
+ }
+ }
+ }
+
+ if (!PL_strcasecmp(dataId, "fieldMap")) {
+ m_pFieldMap = nullptr;
+ if (item) m_pFieldMap = do_QueryInterface(item);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::GetStatus(const char* statusKind,
+ int32_t* _retval) {
+ NS_ASSERTION(statusKind != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!statusKind || !_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = 0;
+
+ if (!PL_strcasecmp(statusKind, "isInstalled")) {
+ GetDefaultLocation();
+ *_retval = (int32_t)m_found;
+ }
+
+ if (!PL_strcasecmp(statusKind, "canUserSetLocation")) {
+ GetDefaultLocation();
+ *_retval = (int32_t)m_userVerify;
+ }
+
+ if (!PL_strcasecmp(statusKind, "autoFind")) {
+ GetDefaultLocation();
+ *_retval = (int32_t)m_autoFind;
+ }
+
+ if (!PL_strcasecmp(statusKind, "supportsMultiple")) {
+ bool multi = false;
+ if (m_pInterface) m_pInterface->GetSupportsMultiple(&multi);
+ *_retval = (int32_t)multi;
+ }
+
+ if (!PL_strcasecmp(statusKind, "needsFieldMap")) {
+ bool needs = false;
+ if (m_pInterface && m_pLocation)
+ m_pInterface->GetNeedsFieldMap(m_pLocation, &needs);
+ *_retval = (int32_t)needs;
+ }
+
+ return NS_OK;
+}
+
+void nsImportGenericAddressBooks::GetDefaultLocation(void) {
+ if (!m_pInterface) return;
+
+ if ((m_pLocation && m_gotLocation) || m_autoFind) return;
+
+ if (m_description) free(m_description);
+ m_description = nullptr;
+ m_pInterface->GetAutoFind(&m_description, &m_autoFind);
+ m_gotLocation = true;
+ if (m_autoFind) {
+ m_found = true;
+ m_userVerify = false;
+ return;
+ }
+
+ nsCOMPtr<nsIFile> pLoc;
+ m_pInterface->GetDefaultLocation(getter_AddRefs(pLoc), &m_found,
+ &m_userVerify);
+ if (!m_pLocation) m_pLocation = pLoc;
+}
+
+void nsImportGenericAddressBooks::GetDefaultBooks(void) {
+ if (!m_pInterface) return;
+
+ if (!m_pLocation && !m_autoFind) return;
+
+ nsresult rv = m_pInterface->FindAddressBooks(m_pLocation, m_Books);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error: FindAddressBooks failed\n");
+ }
+}
+
+void nsImportGenericAddressBooks::GetDefaultFieldMap(void) {
+ if (!m_pInterface || !m_pLocation) return;
+
+ nsresult rv;
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Unable to get nsIImportService.\n");
+ return;
+ }
+
+ rv = impSvc->CreateNewFieldMap(getter_AddRefs(m_pFieldMap));
+ if (NS_FAILED(rv)) return;
+
+ int32_t sz = 0;
+ rv = m_pFieldMap->GetNumMozFields(&sz);
+ if (NS_SUCCEEDED(rv)) rv = m_pFieldMap->DefaultFieldMap(sz);
+ if (NS_SUCCEEDED(rv)) rv = m_pInterface->InitFieldMap(m_pFieldMap);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error: Unable to initialize field map\n");
+ m_pFieldMap = nullptr;
+ }
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::WantsProgress(bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ GetDefaultLocation();
+ GetDefaultBooks();
+
+ bool result = false;
+ uint32_t totalSize = 0;
+
+ for (nsIImportABDescriptor* book : m_Books) {
+ bool doImport = false;
+ nsresult rv = book->GetImport(&doImport);
+ if (NS_SUCCEEDED(rv) && doImport) {
+ uint32_t size = 0;
+ (void)book->GetSize(&size);
+ result = true;
+ totalSize += size;
+ }
+ }
+ m_totalSize = totalSize;
+ m_doImport = result;
+
+ *_retval = result;
+
+ return NS_OK;
+}
+
+void nsImportGenericAddressBooks::SetLogs(nsString& success, nsString& error,
+ nsISupportsString* pSuccess,
+ nsISupportsString* pError) {
+ nsAutoString str;
+ if (pSuccess) {
+ pSuccess->GetData(str);
+ str.Append(success);
+ pSuccess->SetData(success);
+ }
+ if (pError) {
+ pError->GetData(str);
+ str.Append(error);
+ pError->SetData(error);
+ }
+}
+
+already_AddRefed<nsIAbDirectory> GetAddressBookFromUri(const char* pUri) {
+ if (!pUri) return nullptr;
+
+ nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1");
+ if (!abManager) return nullptr;
+
+ nsCOMPtr<nsIAbDirectory> directory;
+ abManager->GetDirectory(nsDependentCString(pUri), getter_AddRefs(directory));
+ if (!directory) return nullptr;
+
+ return directory.forget();
+}
+
+already_AddRefed<nsIAbDirectory> GetAddressBook(nsString name, bool makeNew) {
+ if (!makeNew) {
+ // FIXME: How do I get the list of address books and look for a
+ // specific name. Major bogosity!
+ // For now, assume we didn't find anything with that name
+ }
+
+ IMPORT_LOG0("In GetAddressBook\n");
+
+ nsresult rv;
+ nsCOMPtr<nsIAbDirectory> directory;
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString dirPrefId;
+ rv = abManager->NewAddressBook(name, EmptyCString(),
+ nsIAbManager::JS_DIRECTORY_TYPE,
+ EmptyCString(), dirPrefId);
+ if (NS_SUCCEEDED(rv)) {
+ rv = abManager->GetDirectoryFromId(dirPrefId, getter_AddRefs(directory));
+ }
+ }
+
+ return directory.forget();
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::BeginImport(
+ nsISupportsString* successLog, nsISupportsString* errorLog, bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ nsString success;
+ nsString error;
+
+ if (!m_doImport) {
+ *_retval = true;
+ nsImportStringBundle::GetStringByID(IMPORT_NO_ADDRBOOKS, m_stringBundle,
+ success);
+ SetLogs(success, error, successLog, errorLog);
+ return NS_OK;
+ }
+
+ if (!m_pInterface) {
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_AB_NOTINITIALIZED,
+ m_stringBundle, error);
+ SetLogs(success, error, successLog, errorLog);
+ *_retval = false;
+ return NS_OK;
+ }
+
+ bool needsFieldMap = false;
+
+ if (NS_FAILED(m_pInterface->GetNeedsFieldMap(m_pLocation, &needsFieldMap)) ||
+ (needsFieldMap && !m_pFieldMap)) {
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_AB_NOTINITIALIZED,
+ m_stringBundle, error);
+ SetLogs(success, error, successLog, errorLog);
+ *_retval = false;
+ return NS_OK;
+ }
+
+ m_pSuccessLog = successLog;
+ m_pErrorLog = errorLog;
+
+ // create the info need to drive address book import. We're
+ // not going to create a new thread for this since address books
+ // don't tend to be large, and import is rare.
+ m_pThreadData = new AddressThreadData();
+ m_pThreadData->books = m_Books.Clone();
+ m_pThreadData->addressImport = m_pInterface;
+ m_pThreadData->fieldMap = m_pFieldMap;
+ m_pThreadData->errorLog = m_pErrorLog;
+ m_pThreadData->successLog = m_pSuccessLog;
+ m_pThreadData->pDestinationUri = m_pDestinationUri;
+
+ // Create/obtain any address books that we need here, so that we don't need
+ // to do so inside the import thread which would just proxy the create
+ // operations back to the main thread anyway.
+ nsCOMPtr<nsIAbDirectory> db;
+ if (!m_pDestinationUri.IsEmpty()) {
+ db = GetAddressBookFromUri(m_pDestinationUri.get());
+ }
+ for (nsIImportABDescriptor* book : m_Books) {
+ if (!db) {
+ nsString name;
+ book->GetPreferredName(name);
+ db = GetAddressBook(name, true);
+ }
+ m_DBs.AppendObject(db);
+ }
+ m_pThreadData->dBs = &m_DBs;
+
+ m_pThreadData->stringBundle = m_stringBundle;
+
+ nsresult rv;
+ m_pThreadData->ldifService =
+ do_GetService("@mozilla.org/addressbook/abldifservice;1", &rv);
+
+ ImportAddressThread(m_pThreadData);
+ delete m_pThreadData;
+ m_pThreadData = nullptr;
+ *_retval = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::ContinueImport(bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = true;
+ if (m_pThreadData) {
+ if (m_pThreadData->fatalError) *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::GetProgress(int32_t* _retval) {
+ // This returns the progress from the the currently
+ // running import mail or import address book thread.
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ if (!m_pThreadData || !(m_pThreadData->threadAlive)) {
+ *_retval = 100;
+ return NS_OK;
+ }
+
+ uint32_t sz = 0;
+ if (m_pThreadData->currentSize && m_pInterface) {
+ if (NS_FAILED(m_pInterface->GetImportProgress(&sz))) sz = 0;
+ }
+
+ if (m_totalSize)
+ *_retval = ((m_pThreadData->currentTotal + sz) * 100) / m_totalSize;
+ else
+ *_retval = 0;
+
+ // never return less than 5 so it looks like we are doing something!
+ if (*_retval < 5) *_retval = 5;
+
+ // as long as the thread is alive don't return completely
+ // done.
+ if (*_retval > 99) *_retval = 99;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericAddressBooks::CancelImport(void) {
+ if (m_pThreadData) {
+ m_pThreadData->abort = true;
+ m_pThreadData = nullptr;
+ }
+
+ return NS_OK;
+}
+
+AddressThreadData::AddressThreadData() {
+ fatalError = false;
+ driverAlive = true;
+ threadAlive = true;
+ abort = false;
+ currentTotal = 0;
+ currentSize = 0;
+}
+
+AddressThreadData::~AddressThreadData() {}
+
+void nsImportGenericAddressBooks::ReportError(const char16_t* pName,
+ nsString* pStream,
+ nsIStringBundle* aBundle) {
+ if (!pStream) return;
+ // load the error string
+ char16_t* pFmt =
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_GETABOOK, aBundle);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, pName);
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->AppendLiteral(MSG_LINEBREAK);
+}
+
+static void ImportAddressThread(void* stuff) {
+ IMPORT_LOG0("In Begin ImportAddressThread\n");
+
+ AddressThreadData* pData = (AddressThreadData*)stuff;
+
+ nsString success;
+ nsString error;
+
+ uint32_t count = pData->books.Length();
+ for (uint32_t i = 0; (i < count) && !(pData->abort); i++) {
+ nsIImportABDescriptor* book = pData->books[i];
+
+ uint32_t size = 0;
+ bool doImport = false;
+ nsresult rv = book->GetImport(&doImport);
+ if (NS_SUCCEEDED(rv) && doImport) rv = book->GetSize(&size);
+
+ if (NS_SUCCEEDED(rv) && size && doImport) {
+ nsString name;
+ book->GetPreferredName(name);
+
+ nsCOMPtr<nsIAbDirectory> db = pData->dBs->ObjectAt(i);
+
+ bool fatalError = false;
+ pData->currentSize = size;
+ if (db) {
+ char16_t* pSuccess = nullptr;
+ char16_t* pError = nullptr;
+
+ /*
+ if (pData->fieldMap) {
+ int32_t sz = 0;
+ int32_t mapIndex;
+ bool active;
+ pData->fieldMap->GetMapSize(&sz);
+ IMPORT_LOG1("**** Field Map Size: %d\n", (int) sz);
+ for (int32_t i = 0; i < sz; i++) {
+ pData->fieldMap->GetFieldMap(i, &mapIndex);
+ pData->fieldMap->GetFieldActive(i, &active);
+ IMPORT_LOG3("Field map #%d: index=%d, active=%d\n", (int) i, (int)
+ mapIndex, (int) active);
+ }
+ }
+ */
+
+ rv = pData->addressImport->ImportAddressBook(
+ book, db, pData->fieldMap, pData->ldifService, &pError, &pSuccess,
+ &fatalError);
+ if (NS_SUCCEEDED(rv) && pSuccess) {
+ success.Append(pSuccess);
+ free(pSuccess);
+ }
+ if (pError) {
+ error.Append(pError);
+ free(pError);
+ }
+ } else {
+ nsImportGenericAddressBooks::ReportError(name.get(), &error,
+ pData->stringBundle);
+ }
+
+ pData->currentSize = 0;
+ pData->currentTotal += size;
+
+ if (fatalError) {
+ pData->fatalError = true;
+ break;
+ }
+ }
+ }
+
+ nsImportGenericAddressBooks::SetLogs(success, error, pData->successLog,
+ pData->errorLog);
+
+ if (pData->abort || pData->fatalError) {
+ // FIXME: do what is necessary to get rid of what has been imported so far.
+ // Nothing if we went into an existing address book! Otherwise, delete
+ // the ones we created?
+ }
+}
diff --git a/comm/mailnews/import/src/nsImportAddressBooks.h b/comm/mailnews/import/src/nsImportAddressBooks.h
new file mode 100644
index 0000000000..8d0edd2c67
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportAddressBooks.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportGeneric.h"
+#include "nsIImportFieldMap.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbLDIFService.h"
+#include "nsIStringBundle.h"
+#include "nsIArray.h"
+#include "nsCOMArray.h"
+
+static void ImportAddressThread(void* stuff);
+
+class AddressThreadData;
+
+class nsImportGenericAddressBooks : public nsIImportGeneric {
+ public:
+ nsImportGenericAddressBooks();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTGENERIC
+
+ private:
+ virtual ~nsImportGenericAddressBooks();
+ void GetDefaultLocation(void);
+ void GetDefaultBooks(void);
+ void GetDefaultFieldMap(void);
+
+ public:
+ static void SetLogs(nsString& success, nsString& error,
+ nsISupportsString* pSuccess, nsISupportsString* pError);
+ static void ReportError(const char16_t* pName, nsString* pStream,
+ nsIStringBundle* aBundle);
+
+ private:
+ nsCOMPtr<nsIImportAddressBooks> m_pInterface;
+ nsTArray<RefPtr<nsIImportABDescriptor>> m_Books;
+ nsCOMArray<nsIAbDirectory> m_DBs;
+ nsCOMPtr<nsIFile> m_pLocation;
+ nsCOMPtr<nsIImportFieldMap> m_pFieldMap;
+ bool m_autoFind;
+ char16_t* m_description;
+ bool m_gotLocation;
+ bool m_found;
+ bool m_userVerify;
+ nsCOMPtr<nsISupportsString> m_pSuccessLog;
+ nsCOMPtr<nsISupportsString> m_pErrorLog;
+ uint32_t m_totalSize;
+ bool m_doImport;
+ AddressThreadData* m_pThreadData;
+ nsCString m_pDestinationUri;
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+};
+
+class AddressThreadData {
+ public:
+ bool driverAlive;
+ bool threadAlive;
+ bool abort;
+ bool fatalError;
+ uint32_t currentTotal;
+ uint32_t currentSize;
+ nsTArray<RefPtr<nsIImportABDescriptor>> books;
+ nsCOMArray<nsIAbDirectory>* dBs;
+ nsCOMPtr<nsIAbLDIFService> ldifService;
+ nsCOMPtr<nsIImportAddressBooks> addressImport;
+ nsCOMPtr<nsIImportFieldMap> fieldMap;
+ nsCOMPtr<nsISupportsString> successLog;
+ nsCOMPtr<nsISupportsString> errorLog;
+ nsCString pDestinationUri;
+ nsCOMPtr<nsIStringBundle> stringBundle;
+
+ AddressThreadData();
+ ~AddressThreadData();
+};
diff --git a/comm/mailnews/import/src/nsImportEmbeddedImageData.cpp b/comm/mailnews/import/src/nsImportEmbeddedImageData.cpp
new file mode 100644
index 0000000000..d49bccdbfc
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportEmbeddedImageData.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsImportEmbeddedImageData.h"
+
+NS_IMPL_ISUPPORTS(nsImportEmbeddedImageData, nsIMsgEmbeddedImageData)
+
+nsImportEmbeddedImageData::nsImportEmbeddedImageData() {}
+
+nsImportEmbeddedImageData::nsImportEmbeddedImageData(nsIURI* aUri,
+ const nsACString& aCid)
+ : m_uri(aUri), m_cid(aCid) {}
+
+nsImportEmbeddedImageData::nsImportEmbeddedImageData(nsIURI* aUri,
+ const nsACString& aCid,
+ const nsACString& aName)
+ : m_uri(aUri), m_cid(aCid), m_name(aName) {}
+
+nsImportEmbeddedImageData::~nsImportEmbeddedImageData() {}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::GetUri(nsIURI** aUri) {
+ NS_ENSURE_ARG_POINTER(aUri);
+ NS_IF_ADDREF(*aUri = m_uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::SetUri(nsIURI* aUri) {
+ m_uri = aUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::GetCid(nsACString& aCid) {
+ aCid = m_cid;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::SetCid(const nsACString& aCid) {
+ m_cid = aCid;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::GetName(nsACString& aName) {
+ aName = m_name;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportEmbeddedImageData::SetName(const nsACString& aName) {
+ m_name = aName;
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsImportEmbeddedImageData.h b/comm/mailnews/import/src/nsImportEmbeddedImageData.h
new file mode 100644
index 0000000000..0eaa08b113
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportEmbeddedImageData.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __IMPORTEMBEDDEDIMAGETDATA_H__
+#define __IMPORTEMBEDDEDIMAGETDATA_H__
+
+#include "nsIMsgSend.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+
+class nsImportEmbeddedImageData final : public nsIMsgEmbeddedImageData {
+ public:
+ nsImportEmbeddedImageData(nsIURI* aUri, const nsACString& aCID);
+ nsImportEmbeddedImageData(nsIURI* aUri, const nsACString& aCID,
+ const nsACString& aName);
+ nsImportEmbeddedImageData();
+ NS_DECL_NSIMSGEMBEDDEDIMAGEDATA
+ NS_DECL_ISUPPORTS
+
+ nsCOMPtr<nsIURI> m_uri;
+ nsCString m_cid;
+ nsCString m_name;
+
+ private:
+ ~nsImportEmbeddedImageData();
+};
+
+#endif
diff --git a/comm/mailnews/import/src/nsImportEncodeScan.cpp b/comm/mailnews/import/src/nsImportEncodeScan.cpp
new file mode 100644
index 0000000000..64607688ef
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportEncodeScan.cpp
@@ -0,0 +1,334 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsImportEncodeScan.h"
+#include "nsNetUtil.h"
+
+#define kBeginAppleSingle 0
+#define kBeginDataFork 1
+#define kBeginResourceFork 2
+#define kAddEntries 3
+#define kScanningDataFork 4
+#define kScanningRsrcFork 5
+#define kDoneWithFile 6
+
+uint32_t gAppleSingleHeader[6] = {0x00051600, 0x00020000, 0, 0, 0, 0};
+#define kAppleSingleHeaderSize (6 * sizeof(uint32_t))
+
+#ifdef _MAC_IMPORT_CODE
+# include "MoreDesktopMgr.h"
+
+CInfoPBRec gCatInfoPB;
+U32 g2000Secs = 0;
+long gGMTDelta = 0;
+
+long GetGmtDelta(void);
+U32 Get2000Secs(void);
+
+long GetGmtDelta(void) {
+ MachineLocation myLocation;
+ ReadLocation(&myLocation);
+ long myDelta = BitAnd(myLocation.u.gmtDelta, 0x00FFFFFF);
+ if (BitTst(&myDelta, 23)) myDelta = BitOr(myDelta, 0xFF000000);
+ return myDelta;
+}
+
+U32 Get2000Secs(void) {
+ DateTimeRec dr;
+ dr.year = 2000;
+ dr.month = 1;
+ dr.day = 1;
+ dr.hour = 0;
+ dr.minute = 0;
+ dr.second = 0;
+ dr.dayOfWeek = 0;
+ U32 result;
+ DateToSeconds(&dr, &result);
+ return result;
+}
+#endif
+
+nsImportEncodeScan::nsImportEncodeScan() {
+ m_isAppleSingle = false;
+ m_encodeScanState = 0;
+ m_resourceForkSize = 0;
+ m_dataForkSize = 0;
+}
+
+nsImportEncodeScan::~nsImportEncodeScan() {}
+
+bool nsImportEncodeScan::InitEncodeScan(bool appleSingleEncode,
+ nsIFile* fileLoc, const char* pName,
+ uint8_t* pBuf, uint32_t sz) {
+ CleanUpEncodeScan();
+ m_isAppleSingle = appleSingleEncode;
+ m_encodeScanState = kBeginAppleSingle;
+ m_pInputFile = fileLoc;
+ m_useFileName = pName;
+ m_pBuf = pBuf;
+ m_bufSz = sz;
+ if (!m_isAppleSingle) {
+ if (!m_inputStream) {
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(m_inputStream),
+ m_pInputFile);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+
+ InitScan(m_inputStream, pBuf, sz);
+ } else {
+#ifdef _MAC_IMPORT_CODE
+ // Fill in the file sizes
+ m_resourceForkSize = fileLoc.GetMacFileSize(UFileLocation::eResourceFork);
+ m_dataForkSize = fileLoc.GetMacFileSize(UFileLocation::eDataFork);
+#endif
+ }
+
+ return true;
+}
+
+void nsImportEncodeScan::CleanUpEncodeScan(void) {
+ m_pInputStream->Close();
+ m_pInputStream = nullptr;
+ m_pInputFile = nullptr;
+}
+
+// 26 + 12 per entry
+
+void nsImportEncodeScan::FillInEntries(int numEntries) {
+#ifdef _MAC_IMPORT_CODE
+ int len = m_useFileName.GetLength();
+ if (len < 32) len = 32;
+ long entry[3];
+ long fileOffset = 26 + (12 * numEntries);
+ entry[0] = 3;
+ entry[1] = fileOffset;
+ entry[2] = m_useFileName.GetLength();
+ fileOffset += len;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+
+ Str255 comment;
+ comment[0] = 0;
+ OSErr err = FSpDTGetComment(m_inputFileLoc, comment);
+ if (comment[0] > 200) comment[0] = 200;
+ entry[0] = 4;
+ entry[1] = fileOffset;
+ entry[2] = comment[0];
+ fileOffset += 200;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+
+ entry[0] = 8;
+ entry[1] = fileOffset;
+ entry[2] = 16;
+ fileOffset += 16;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+
+ entry[0] = 9;
+ entry[1] = fileOffset;
+ entry[2] = 32;
+ fileOffset += 32;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+
+ entry[0] = 10;
+ entry[1] = fileOffset;
+ entry[2] = 4;
+ fileOffset += 4;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+
+ if (m_resourceForkSize) {
+ entry[0] = 2;
+ entry[1] = fileOffset;
+ entry[2] = m_resourceForkSize;
+ fileOffset += m_resourceForkSize;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+ }
+
+ if (m_dataForkSize) {
+ entry[0] = 1;
+ entry[1] = fileOffset;
+ entry[2] = m_dataForkSize;
+ fileOffset += m_dataForkSize;
+ MemCpy(m_pBuf + m_bytesInBuf, entry, 12);
+ m_bytesInBuf += 12;
+ }
+
+#endif
+}
+
+bool nsImportEncodeScan::AddEntries(void) {
+#ifdef _MAC_IMPORT_CODE
+ if (!g2000Secs) {
+ g2000Secs = Get2000Secs();
+ gGMTDelta = GetGmtDelta();
+ }
+ MemCpy(m_pBuf + m_bytesInBuf, (PC_S8)m_useFileName,
+ m_useFileName.GetLength());
+ m_bytesInBuf += m_useFileName.GetLength();
+ if (m_useFileName.GetLength() < 32) {
+ int len = m_useFileName.GetLength();
+ while (len < 32) {
+ *((P_S8)m_pBuf + m_bytesInBuf) = 0;
+ m_bytesInBuf++;
+ len++;
+ }
+ }
+
+ Str255 comment;
+ comment[0] = 0;
+ OSErr err = FSpDTGetComment(m_inputFileLoc, comment);
+ comment[0] = 200;
+ MemCpy(m_pBuf + m_bytesInBuf, &(comment[1]), comment[0]);
+ m_bytesInBuf += comment[0];
+
+ long dates[4];
+ dates[0] = gCatInfoPB.hFileInfo.ioFlCrDat;
+ dates[1] = gCatInfoPB.hFileInfo.ioFlMdDat;
+ dates[2] = gCatInfoPB.hFileInfo.ioFlBkDat;
+ dates[3] = 0x80000000;
+ for (short i = 0; i < 3; i++) {
+ dates[i] -= g2000Secs;
+ dates[i] += gGMTDelta;
+ }
+ MemCpy(m_pBuf + m_bytesInBuf, dates, 16);
+ m_bytesInBuf += 16;
+
+ FInfo fInfo = gCatInfoPB.hFileInfo.ioFlFndrInfo;
+ FXInfo fxInfo = gCatInfoPB.hFileInfo.ioFlXFndrInfo;
+ fInfo.fdFlags = 0;
+ fInfo.fdLocation.h = 0;
+ fInfo.fdLocation.v = 0;
+ fInfo.fdFldr = 0;
+ MemSet(&fxInfo, 0, sizeof(fxInfo));
+ MemCpy(m_pBuf + m_bytesInBuf, &fInfo, 16);
+ m_bytesInBuf += 16;
+ MemCpy(m_pBuf + m_bytesInBuf, &fxInfo, 16);
+ m_bytesInBuf += 16;
+
+ dates[0] = 0;
+ if ((gCatInfoPB.hFileInfo.ioFlAttrib & 1) != 0) dates[0] |= 1;
+ MemCpy(m_pBuf + m_bytesInBuf, dates, 4);
+ m_bytesInBuf += 4;
+
+#endif
+ return true;
+}
+
+bool nsImportEncodeScan::Scan(bool* pDone) {
+ nsresult rv;
+
+ *pDone = false;
+ if (m_isAppleSingle) {
+ // Stuff the buffer with things needed to encode the file...
+ // then just allow UScanFile to handle each fork, but be careful
+ // when handling eof.
+ switch (m_encodeScanState) {
+ case kBeginAppleSingle: {
+#ifdef _MAC_IMPORT_CODE
+ OSErr err = GetCatInfoNoName(
+ m_inputFileLoc.GetVRefNum(), m_inputFileLoc.GetParID(),
+ m_inputFileLoc.GetFileNamePtr(), &gCatInfoPB);
+ if (err != noErr) return FALSE;
+#endif
+ m_eof = false;
+ m_pos = 0;
+ memcpy(m_pBuf, gAppleSingleHeader, kAppleSingleHeaderSize);
+ m_bytesInBuf = kAppleSingleHeaderSize;
+ int numEntries = 5;
+ if (m_dataForkSize) numEntries++;
+ if (m_resourceForkSize) numEntries++;
+ memcpy(m_pBuf + m_bytesInBuf, &numEntries, sizeof(numEntries));
+ m_bytesInBuf += sizeof(numEntries);
+ FillInEntries(numEntries);
+ m_encodeScanState = kAddEntries;
+ return ScanBuffer(pDone);
+ } break;
+
+ case kBeginDataFork: {
+ if (!m_dataForkSize) {
+ m_encodeScanState = kDoneWithFile;
+ return true;
+ }
+ // Initialize the scan of the data fork...
+ if (!m_inputStream) {
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(m_inputStream),
+ m_pInputFile);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ m_encodeScanState = kScanningDataFork;
+ return true;
+ } break;
+
+ case kScanningDataFork: {
+ bool result = FillBufferFromFile();
+ if (!result) return false;
+ if (m_eof) {
+ m_eof = false;
+ result = ScanBuffer(pDone);
+ if (!result) return false;
+ m_inputStream->Close();
+ m_inputStream = nullptr;
+ m_encodeScanState = kDoneWithFile;
+ return true;
+ } else
+ return ScanBuffer(pDone);
+ } break;
+
+ case kScanningRsrcFork: {
+ bool result = FillBufferFromFile();
+ if (!result) return false;
+ if (m_eof) {
+ m_eof = false;
+ result = ScanBuffer(pDone);
+ if (!result) return false;
+ m_inputStream->Close();
+ m_inputStream = nullptr;
+ m_encodeScanState = kBeginDataFork;
+ return true;
+ } else
+ return ScanBuffer(pDone);
+ } break;
+
+ case kBeginResourceFork: {
+ if (!m_resourceForkSize) {
+ m_encodeScanState = kBeginDataFork;
+ return true;
+ }
+ /*
+ // FIXME: Open the resource fork on the Mac!!!
+ m_fH = UFile::OpenRsrcFileRead(m_inputFileLoc);
+ if (m_fH == TR_FILE_ERROR)
+ return FALSE;
+ */
+ m_encodeScanState = kScanningRsrcFork;
+ return true;
+ } break;
+
+ case kAddEntries: {
+ ShiftBuffer();
+ if (!AddEntries()) return false;
+ m_encodeScanState = kBeginResourceFork;
+ return ScanBuffer(pDone);
+ } break;
+
+ case kDoneWithFile: {
+ ShiftBuffer();
+ m_eof = true;
+ if (!ScanBuffer(pDone)) return false;
+ *pDone = true;
+ return true;
+ } break;
+ }
+
+ } else
+ return nsImportScanFile::Scan(pDone);
+
+ return false;
+}
diff --git a/comm/mailnews/import/src/nsImportEncodeScan.h b/comm/mailnews/import/src/nsImportEncodeScan.h
new file mode 100644
index 0000000000..4c9b784fc6
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportEncodeScan.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImportEncodeScan_h___
+#define nsImportEncodeScan_h___
+
+#include "mozilla/Attributes.h"
+#include "nsIFile.h"
+#include "nsImportScanFile.h"
+#include "nsString.h"
+
+class nsImportEncodeScan : public nsImportScanFile {
+ public:
+ nsImportEncodeScan();
+ ~nsImportEncodeScan();
+
+ bool InitEncodeScan(bool appleSingleEncode, nsIFile* pFile, const char* pName,
+ uint8_t* pBuf, uint32_t sz);
+ void CleanUpEncodeScan(void);
+
+ virtual bool Scan(bool* pDone) override;
+
+ protected:
+ void FillInEntries(int numEntries);
+ bool AddEntries(void);
+
+ protected:
+ bool m_isAppleSingle;
+ nsCOMPtr<nsIFile> m_pInputFile;
+ nsCOMPtr<nsIInputStream> m_inputStream;
+ int m_encodeScanState;
+ long m_resourceForkSize;
+ long m_dataForkSize;
+ nsCString m_useFileName;
+};
+
+#endif /* nsImportEncodeScan_h__ */
diff --git a/comm/mailnews/import/src/nsImportFieldMap.cpp b/comm/mailnews/import/src/nsImportFieldMap.cpp
new file mode 100644
index 0000000000..74e15f11fe
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportFieldMap.cpp
@@ -0,0 +1,325 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsIAbCard.h"
+#include "nsIStringBundle.h"
+#include "nsImportFieldMap.h"
+#include "nsImportStringBundle.h"
+#include "nsCRTGlue.h"
+#include "ImportDebug.h"
+#include "nsCOMPtr.h"
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsImportFieldMap::Create(nsIStringBundle* aBundle, REFNSIID aIID,
+ void** aResult) {
+ RefPtr<nsImportFieldMap> it = new nsImportFieldMap(aBundle);
+ return it->QueryInterface(aIID, aResult);
+}
+
+NS_IMPL_ISUPPORTS(nsImportFieldMap, nsIImportFieldMap)
+
+NS_IMETHODIMP nsImportFieldMap::GetSkipFirstRecord(bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_skipFirstRecord;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::SetSkipFirstRecord(bool aResult) {
+ m_skipFirstRecord = aResult;
+ return NS_OK;
+}
+
+nsImportFieldMap::nsImportFieldMap(nsIStringBundle* aBundle) {
+ m_numFields = 0;
+ m_pFields = nullptr;
+ m_pActive = nullptr;
+ m_allocated = 0;
+ // need to init the description array
+ m_mozFieldCount = 0;
+ m_skipFirstRecord = false;
+ nsCOMPtr<nsIStringBundle> pBundle = aBundle;
+
+ nsString* pStr;
+ for (int32_t i = IMPORT_FIELD_DESC_START; i <= IMPORT_FIELD_DESC_END;
+ i++, m_mozFieldCount++) {
+ pStr = new nsString();
+ if (pBundle) {
+ nsImportStringBundle::GetStringByID(i, pBundle, *pStr);
+ } else
+ pStr->AppendInt(i);
+ m_descriptions.AppendElement(pStr);
+ }
+}
+
+nsImportFieldMap::~nsImportFieldMap() {
+ if (m_pFields) delete[] m_pFields;
+ if (m_pActive) delete[] m_pActive;
+
+ nsString* pStr;
+ for (int32_t i = 0; i < m_mozFieldCount; i++) {
+ pStr = m_descriptions.ElementAt(i);
+ delete pStr;
+ }
+ m_descriptions.Clear();
+}
+
+NS_IMETHODIMP nsImportFieldMap::GetNumMozFields(int32_t* aNumFields) {
+ NS_ASSERTION(aNumFields != nullptr, "null ptr");
+ if (!aNumFields) return NS_ERROR_NULL_POINTER;
+
+ *aNumFields = m_mozFieldCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::GetMapSize(int32_t* aNumFields) {
+ NS_ASSERTION(aNumFields != nullptr, "null ptr");
+ if (!aNumFields) return NS_ERROR_NULL_POINTER;
+
+ *aNumFields = m_numFields;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::GetFieldDescription(int32_t index,
+ char16_t** _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = nullptr;
+ if ((index < 0) || ((size_t)index >= m_descriptions.Length()))
+ return NS_ERROR_FAILURE;
+
+ *_retval = ToNewUnicode(*(m_descriptions.ElementAt(index)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::SetFieldMapSize(int32_t size) {
+ nsresult rv = Allocate(size);
+ if (NS_FAILED(rv)) return rv;
+
+ m_numFields = size;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::DefaultFieldMap(int32_t size) {
+ nsresult rv = SetFieldMapSize(size);
+ if (NS_FAILED(rv)) return rv;
+ for (int32_t i = 0; i < size; i++) {
+ m_pFields[i] = i;
+ m_pActive[i] = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::GetFieldMap(int32_t index, int32_t* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ if ((index < 0) || (index >= m_numFields)) return NS_ERROR_FAILURE;
+
+ *_retval = m_pFields[index];
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::SetFieldMap(int32_t index, int32_t fieldNum) {
+ if (index == -1) {
+ nsresult rv = Allocate(m_numFields + 1);
+ if (NS_FAILED(rv)) return rv;
+ index = m_numFields;
+ m_numFields++;
+ } else {
+ if ((index < 0) || (index >= m_numFields)) return NS_ERROR_FAILURE;
+ }
+
+ if ((fieldNum != -1) && ((fieldNum < 0) || (fieldNum >= m_mozFieldCount)))
+ return NS_ERROR_FAILURE;
+
+ m_pFields[index] = fieldNum;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::GetFieldActive(int32_t index, bool* active) {
+ NS_ASSERTION(active != nullptr, "null ptr");
+ if (!active) return NS_ERROR_NULL_POINTER;
+ if ((index < 0) || (index >= m_numFields)) return NS_ERROR_FAILURE;
+
+ *active = m_pActive[index];
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::SetFieldActive(int32_t index, bool active) {
+ if ((index < 0) || (index >= m_numFields)) return NS_ERROR_FAILURE;
+
+ m_pActive[index] = active;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportFieldMap::SetFieldValue(nsIAbDirectory* database,
+ nsIAbCard* row, int32_t fieldNum,
+ const nsAString& value) {
+ // Allow the special value for a null field
+ if (fieldNum == -1) return NS_OK;
+
+ if ((fieldNum < 0) || (fieldNum >= m_mozFieldCount)) return NS_ERROR_FAILURE;
+
+ // UGGG!!!!! lot's of typing here!
+ nsresult rv;
+
+ switch (fieldNum) {
+ case 0:
+ rv = row->SetFirstName(value);
+ break;
+ case 1:
+ rv = row->SetLastName(value);
+ break;
+ case 2:
+ rv = row->SetDisplayName(value);
+ break;
+ case 3:
+ rv = row->SetPropertyAsAString(kNicknameProperty, value);
+ break;
+ case 4:
+ rv = row->SetPrimaryEmail(value);
+ break;
+ case 5:
+ rv = row->SetPropertyAsAString(k2ndEmailProperty, value);
+ break;
+ case 6:
+ rv = row->SetPropertyAsAString(kWorkPhoneProperty, value);
+ break;
+ case 7:
+ rv = row->SetPropertyAsAString(kHomePhoneProperty, value);
+ break;
+ case 8:
+ rv = row->SetPropertyAsAString(kFaxProperty, value);
+ break;
+ case 9:
+ rv = row->SetPropertyAsAString(kPagerProperty, value);
+ break;
+ case 10:
+ rv = row->SetPropertyAsAString(kCellularProperty, value);
+ break;
+ case 11:
+ rv = row->SetPropertyAsAString(kHomeAddressProperty, value);
+ break;
+ case 12:
+ rv = row->SetPropertyAsAString(kHomeAddress2Property, value);
+ break;
+ case 13:
+ rv = row->SetPropertyAsAString(kHomeCityProperty, value);
+ break;
+ case 14:
+ rv = row->SetPropertyAsAString(kHomeStateProperty, value);
+ break;
+ case 15:
+ rv = row->SetPropertyAsAString(kHomeZipCodeProperty, value);
+ break;
+ case 16:
+ rv = row->SetPropertyAsAString(kHomeCountryProperty, value);
+ break;
+ case 17:
+ rv = row->SetPropertyAsAString(kWorkAddressProperty, value);
+ break;
+ case 18:
+ rv = row->SetPropertyAsAString(kWorkAddress2Property, value);
+ break;
+ case 19:
+ rv = row->SetPropertyAsAString(kWorkCityProperty, value);
+ break;
+ case 20:
+ rv = row->SetPropertyAsAString(kWorkStateProperty, value);
+ break;
+ case 21:
+ rv = row->SetPropertyAsAString(kWorkZipCodeProperty, value);
+ break;
+ case 22:
+ rv = row->SetPropertyAsAString(kWorkCountryProperty, value);
+ break;
+ case 23:
+ rv = row->SetPropertyAsAString(kJobTitleProperty, value);
+ break;
+ case 24:
+ rv = row->SetPropertyAsAString(kDepartmentProperty, value);
+ break;
+ case 25:
+ rv = row->SetPropertyAsAString(kCompanyProperty, value);
+ break;
+ case 26:
+ rv = row->SetPropertyAsAString(kWorkWebPageProperty, value);
+ break;
+ case 27:
+ rv = row->SetPropertyAsAString(kHomeWebPageProperty, value);
+ break;
+ case 28:
+ rv = row->SetPropertyAsAString(kBirthYearProperty, value);
+ break;
+ case 29:
+ rv = row->SetPropertyAsAString(kBirthMonthProperty, value);
+ break;
+ case 30:
+ rv = row->SetPropertyAsAString(kBirthDayProperty, value);
+ break;
+ case 31:
+ rv = row->SetPropertyAsAString(kCustom1Property, value);
+ break;
+ case 32:
+ rv = row->SetPropertyAsAString(kCustom2Property, value);
+ break;
+ case 33:
+ rv = row->SetPropertyAsAString(kCustom3Property, value);
+ break;
+ case 34:
+ rv = row->SetPropertyAsAString(kCustom4Property, value);
+ break;
+ case 35:
+ rv = row->SetPropertyAsAString(kNotesProperty, value);
+ break;
+ case 36:
+ rv = row->SetPropertyAsAString(kAIMProperty, value);
+ break;
+ default:
+ /* Get the field description, and add it as an anonymous attr? */
+ /* OR WHAT???? */
+ { rv = NS_ERROR_FAILURE; }
+ }
+
+ return rv;
+}
+
+nsresult nsImportFieldMap::Allocate(int32_t newSize) {
+ if (newSize <= m_allocated) return NS_OK;
+
+ int32_t sz = m_allocated;
+ while (sz < newSize) sz += 30;
+
+ int32_t* pData = new int32_t[sz];
+ if (!pData) return NS_ERROR_OUT_OF_MEMORY;
+ bool* pActive = new bool[sz];
+ if (!pActive) {
+ delete[] pData;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int32_t i;
+ for (i = 0; i < sz; i++) {
+ pData[i] = -1;
+ pActive[i] = true;
+ }
+ if (m_numFields) {
+ for (i = 0; i < m_numFields; i++) {
+ pData[i] = m_pFields[i];
+ pActive[i] = m_pActive[i];
+ }
+ delete[] m_pFields;
+ delete[] m_pActive;
+ }
+ m_allocated = sz;
+ m_pFields = pData;
+ m_pActive = pActive;
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsImportFieldMap.h b/comm/mailnews/import/src/nsImportFieldMap.h
new file mode 100644
index 0000000000..1d13df70a1
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportFieldMap.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsImportFieldMap_h___
+#define nsImportFieldMap_h___
+
+#include "nscore.h"
+#include "nsIImportFieldMap.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsIStringBundle.h"
+
+////////////////////////////////////////////////////////////////////////
+
+class nsIStringBundle;
+
+class nsImportFieldMap : public nsIImportFieldMap {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIIMPORTFIELDMAP
+
+ explicit nsImportFieldMap(nsIStringBundle* aBundle);
+
+ static nsresult Create(nsIStringBundle* aBundle, REFNSIID aIID,
+ void** aResult);
+
+ private:
+ virtual ~nsImportFieldMap();
+ nsresult Allocate(int32_t newSize);
+
+ private:
+ int32_t m_numFields;
+ int32_t* m_pFields;
+ bool* m_pActive;
+ int32_t m_allocated;
+ nsTArray<nsString*> m_descriptions;
+ int32_t m_mozFieldCount;
+ bool m_skipFirstRecord;
+};
+
+#endif
diff --git a/comm/mailnews/import/src/nsImportMail.cpp b/comm/mailnews/import/src/nsImportMail.cpp
new file mode 100644
index 0000000000..e845f773f8
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportMail.cpp
@@ -0,0 +1,1007 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsImportMail.h"
+
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIMsgAccountManager.h"
+#include "nsImportStringBundle.h"
+#include "nsTextFormatter.h"
+#include "ImportDebug.h"
+#include "plstr.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Components.h"
+#include "msgCore.h"
+
+// forward decl for proxy methods
+nsresult ProxyGetSubFolders(nsIMsgFolder* aFolder);
+nsresult ProxyGetChildNamed(nsIMsgFolder* aFolder, const nsAString& aName,
+ nsIMsgFolder** aChild);
+nsresult ProxyGetParent(nsIMsgFolder* aFolder, nsIMsgFolder** aParent);
+nsresult ProxyContainsChildNamed(nsIMsgFolder* aFolder, const nsAString& aName,
+ bool* aResult);
+nsresult ProxyGenerateUniqueSubfolderName(nsIMsgFolder* aFolder,
+ const nsAString& aPrefix,
+ nsIMsgFolder* aOtherFolder,
+ nsAString& aName);
+nsresult ProxyCreateSubfolder(nsIMsgFolder* aFolder, const nsAString& aName);
+nsresult ProxyForceDBClosed(nsIMsgFolder* aFolder);
+
+nsresult NS_NewGenericMail(nsIImportGeneric** aImportGeneric) {
+ NS_ASSERTION(aImportGeneric != nullptr, "null ptr");
+ if (!aImportGeneric) return NS_ERROR_NULL_POINTER;
+
+ RefPtr<nsImportGenericMail> pGen = new nsImportGenericMail();
+ return pGen->QueryInterface(NS_GET_IID(nsIImportGeneric),
+ (void**)aImportGeneric);
+}
+
+nsImportGenericMail::nsImportGenericMail() {
+ m_found = false;
+ m_userVerify = false;
+ m_gotLocation = false;
+ m_gotDefaultMailboxes = false;
+ m_totalSize = 0;
+ m_doImport = false;
+ m_pThreadData = nullptr;
+
+ m_pDestFolder = nullptr;
+ m_deleteDestFolder = false;
+ m_createdFolder = false;
+ m_performingMigration = false;
+
+ nsresult rv = nsImportStringBundle::GetStringBundle(
+ IMPORT_MSGS_URL, getter_AddRefs(m_stringBundle));
+ if (NS_FAILED(rv))
+ IMPORT_LOG0("Failed to get string bundle for Importing Mail");
+}
+
+nsImportGenericMail::~nsImportGenericMail() {
+ if (m_pThreadData) {
+ m_pThreadData->DriverAbort();
+ m_pThreadData = nullptr;
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsImportGenericMail, nsIImportGeneric)
+
+NS_IMETHODIMP nsImportGenericMail::GetData(const char* dataId,
+ nsISupports** _retval) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = nullptr;
+ if (!PL_strcasecmp(dataId, "mailInterface")) {
+ NS_IF_ADDREF(*_retval = m_pInterface);
+ }
+
+ if (!PL_strcasecmp(dataId, "mailLocation")) {
+ if (!m_pSrcLocation) GetDefaultLocation();
+ NS_IF_ADDREF(*_retval = m_pSrcLocation);
+ }
+
+ if (!PL_strcasecmp(dataId, "mailDestination")) {
+ if (!m_pDestFolder) GetDefaultDestination();
+ NS_IF_ADDREF(*_retval = m_pDestFolder);
+ }
+
+ if (!PL_strcasecmp(dataId, "migration")) {
+ nsCOMPtr<nsISupportsPRBool> migrationString =
+ do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ migrationString->SetData(m_performingMigration);
+ migrationString.forget(_retval);
+ }
+
+ if (!PL_strcasecmp(dataId, "currentMailbox")) {
+ // create an nsISupportsString, get the current mailbox
+ // name being imported and put it in the string
+ nsCOMPtr<nsISupportsString> data =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+ if (m_pThreadData) {
+ GetMailboxName(m_pThreadData->currentMailbox, data);
+ }
+ data.forget(_retval);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsImportGenericMail::SetData(const char* dataId,
+ nsISupports* item) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(dataId != nullptr, "null ptr");
+ if (!dataId) return NS_ERROR_NULL_POINTER;
+
+ if (!PL_strcasecmp(dataId, "mailInterface")) {
+ m_pInterface = nullptr;
+ if (item) m_pInterface = do_QueryInterface(item);
+ }
+
+ if (!PL_strcasecmp(dataId, "mailLocation")) {
+ m_mailboxes.Clear();
+ m_gotDefaultMailboxes = false;
+ m_pSrcLocation = nullptr;
+ if (item) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> location = do_QueryInterface(item, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_pSrcLocation = location;
+ }
+ }
+
+ if (!PL_strcasecmp(dataId, "mailDestination")) {
+ m_pDestFolder = nullptr;
+ if (item) m_pDestFolder = do_QueryInterface(item);
+ m_deleteDestFolder = false;
+ }
+
+ if (!PL_strcasecmp(dataId, "name")) {
+ if (item) {
+ nsCOMPtr<nsISupportsString> nameString = do_QueryInterface(item, &rv);
+ if (NS_SUCCEEDED(rv)) rv = nameString->GetData(m_pName);
+ }
+ }
+
+ if (!PL_strcasecmp(dataId, "migration")) {
+ if (item) {
+ nsCOMPtr<nsISupportsPRBool> migrationString =
+ do_QueryInterface(item, &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = migrationString->GetData(&m_performingMigration);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsImportGenericMail::GetStatus(const char* statusKind,
+ int32_t* _retval) {
+ NS_ASSERTION(statusKind != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!statusKind || !_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = 0;
+
+ if (!PL_strcasecmp(statusKind, "isInstalled")) {
+ GetDefaultLocation();
+ *_retval = (int32_t)m_found;
+ }
+
+ if (!PL_strcasecmp(statusKind, "canUserSetLocation")) {
+ GetDefaultLocation();
+ *_retval = (int32_t)m_userVerify;
+ }
+
+ return NS_OK;
+}
+
+void nsImportGenericMail::GetDefaultLocation(void) {
+ if (!m_pInterface) return;
+
+ if (m_pSrcLocation && m_gotLocation) return;
+
+ m_gotLocation = true;
+
+ nsCOMPtr<nsIFile> pLoc;
+ m_pInterface->GetDefaultLocation(getter_AddRefs(pLoc), &m_found,
+ &m_userVerify);
+ if (!m_pSrcLocation) m_pSrcLocation = pLoc;
+}
+
+void nsImportGenericMail::GetDefaultMailboxes(void) {
+ if (!m_pInterface || !m_pSrcLocation) return;
+ if (m_gotDefaultMailboxes) return;
+ m_pInterface->FindMailboxes(m_pSrcLocation, m_mailboxes);
+ m_gotDefaultMailboxes = true;
+}
+
+void nsImportGenericMail::GetDefaultDestination(void) {
+ if (m_pDestFolder) return;
+ if (!m_pInterface) return;
+
+ nsIMsgFolder* rootFolder;
+ m_deleteDestFolder = false;
+ m_createdFolder = false;
+ if (CreateFolder(&rootFolder)) {
+ m_pDestFolder = rootFolder;
+ m_deleteDestFolder = true;
+ m_createdFolder = true;
+ return;
+ }
+ IMPORT_LOG0(
+ "*** GetDefaultDestination: Failed to create a default import "
+ "destination folder.");
+}
+
+NS_IMETHODIMP nsImportGenericMail::WantsProgress(bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ if (m_pThreadData) {
+ m_pThreadData->DriverAbort();
+ m_pThreadData = nullptr;
+ }
+
+ GetDefaultLocation();
+ GetDefaultMailboxes();
+
+ if (!m_pDestFolder) {
+ GetDefaultDestination();
+ }
+
+ bool result = false;
+ uint32_t totalSize = 0;
+ for (nsIImportMailboxDescriptor* box : m_mailboxes) {
+ bool doImport = false;
+ uint32_t size = 0;
+ nsresult rv = box->GetImport(&doImport);
+ if (NS_SUCCEEDED(rv) && doImport) {
+ (void)box->GetSize(&size);
+ result = true;
+ }
+ totalSize += size;
+ }
+ m_totalSize = totalSize;
+ m_doImport = result;
+ *_retval = result;
+ return NS_OK;
+}
+
+void nsImportGenericMail::GetMailboxName(uint32_t index,
+ nsISupportsString* pStr) {
+ if (index >= m_mailboxes.Length()) {
+ return;
+ }
+ nsAutoString name;
+ m_mailboxes[index]->GetDisplayName(getter_Copies(name));
+ if (!name.IsEmpty()) {
+ pStr->SetData(name);
+ }
+}
+
+NS_IMETHODIMP nsImportGenericMail::BeginImport(nsISupportsString* successLog,
+ nsISupportsString* errorLog,
+ bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ nsString success;
+ nsString error;
+
+ if (!m_doImport) {
+ nsImportStringBundle::GetStringByID(IMPORT_NO_MAILBOXES, m_stringBundle,
+ success);
+ SetLogs(success, error, successLog, errorLog);
+ *_retval = true;
+ return NS_OK;
+ }
+
+ if (!m_pInterface || !m_gotDefaultMailboxes) {
+ IMPORT_LOG0(
+ "*** BeginImport: Either the interface or source mailbox is not set "
+ "properly.");
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_MB_NOTINITIALIZED,
+ m_stringBundle, error);
+ SetLogs(success, error, successLog, errorLog);
+ *_retval = false;
+ return NS_OK;
+ }
+
+ if (!m_pDestFolder) {
+ IMPORT_LOG0(
+ "*** BeginImport: The destination mailbox is not set properly.");
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_MB_NODESTFOLDER,
+ m_stringBundle, error);
+ SetLogs(success, error, successLog, errorLog);
+ *_retval = false;
+ return NS_OK;
+ }
+
+ if (m_pThreadData) {
+ m_pThreadData->DriverAbort();
+ m_pThreadData = nullptr;
+ }
+
+ m_pSuccessLog = successLog;
+ m_pErrorLog = errorLog;
+
+ // kick off the thread to do the import!!!!
+ m_pThreadData = new ImportThreadData();
+ m_pThreadData->boxes = m_mailboxes.Clone();
+ m_pThreadData->mailImport = m_pInterface;
+ m_pThreadData->errorLog = m_pErrorLog;
+ m_pThreadData->successLog = m_pSuccessLog;
+
+ m_pThreadData->ownsDestRoot = m_deleteDestFolder;
+ m_pThreadData->destRoot = m_pDestFolder;
+ m_pThreadData->performingMigration = m_performingMigration;
+
+ m_pThreadData->stringBundle = m_stringBundle;
+
+ // Previously this was run in a sub-thread, after introducing
+ // SeamonkeyImport.jsm and because JS XPCOM can only run in the main thread,
+ // this has been changed to run in the main thread.
+ ImportMailThread(m_pThreadData);
+ *_retval = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericMail::ContinueImport(bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = true;
+ if (m_pThreadData) {
+ if (m_pThreadData->fatalError) *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportGenericMail::GetProgress(int32_t* _retval) {
+ // This returns the progress from the the currently
+ // running import mail or import address book thread.
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ if (!m_pThreadData || !(m_pThreadData->threadAlive)) {
+ *_retval = 100;
+ return NS_OK;
+ }
+
+ uint32_t sz = 0;
+ if (m_pThreadData->currentSize && m_pInterface) {
+ if (NS_FAILED(m_pInterface->GetImportProgress(&sz))) sz = 0;
+ }
+
+ // *_retval = (int32_t) (((uint32_t)(m_pThreadData->currentTotal + sz) *
+ // (uint32_t)100) / m_totalSize);
+
+ if (m_totalSize) {
+ double perc;
+ perc = (double)m_pThreadData->currentTotal;
+ perc += sz;
+ perc *= 100;
+ perc /= m_totalSize;
+ *_retval = (int32_t)perc;
+ if (*_retval > 100) *_retval = 100;
+ } else
+ *_retval = 0;
+
+ // never return 100% while the thread is still alive
+ if (*_retval > 99) *_retval = 99;
+
+ return NS_OK;
+}
+
+void nsImportGenericMail::ReportError(int32_t id, const char16_t* pName,
+ nsString* pStream,
+ nsIStringBundle* aBundle) {
+ if (!pStream) return;
+
+ // load the error string
+ char16_t* pFmt = nsImportStringBundle::GetStringByID(id, aBundle);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, pName);
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->Append(NS_ConvertASCIItoUTF16(MSG_LINEBREAK));
+}
+
+void nsImportGenericMail::SetLogs(nsString& success, nsString& error,
+ nsISupportsString* pSuccess,
+ nsISupportsString* pError) {
+ nsAutoString str;
+ if (pSuccess) {
+ pSuccess->GetData(str);
+ str.Append(success);
+ pSuccess->SetData(str);
+ }
+ if (pError) {
+ pError->GetData(str);
+ str.Append(error);
+ pError->SetData(str);
+ }
+}
+
+NS_IMETHODIMP nsImportGenericMail::CancelImport(void) {
+ if (m_pThreadData) {
+ m_pThreadData->abort = true;
+ m_pThreadData->DriverAbort();
+ m_pThreadData = nullptr;
+ }
+
+ return NS_OK;
+}
+
+ImportThreadData::ImportThreadData() {
+ fatalError = false;
+ driverAlive = true;
+ threadAlive = true;
+ abort = false;
+ currentTotal = 0;
+ currentSize = 0;
+ destRoot = nullptr;
+ ownsDestRoot = false;
+}
+
+ImportThreadData::~ImportThreadData() {}
+
+void ImportThreadData::DriverDelete(void) {
+ driverAlive = false;
+ if (!driverAlive && !threadAlive) delete this;
+}
+
+void ImportThreadData::ThreadDelete() {
+ threadAlive = false;
+ if (!driverAlive && !threadAlive) delete this;
+}
+
+void ImportThreadData::DriverAbort() {
+ if (abort && !threadAlive && destRoot) {
+ if (ownsDestRoot) {
+ destRoot->RecursiveDelete(true);
+ } else {
+ // FIXME: just delete the stuff we created?
+ }
+ } else
+ abort = true;
+ DriverDelete();
+}
+
+static void ImportMailThread(void* stuff) {
+ ImportThreadData* pData = (ImportThreadData*)stuff;
+
+ IMPORT_LOG0("ImportMailThread: Starting...");
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> destRoot(pData->destRoot);
+
+ uint32_t count = pData->boxes.Length();
+
+ uint32_t size;
+ uint32_t depth = 1;
+ uint32_t newDepth;
+ nsString lastName;
+
+ nsCOMPtr<nsIMsgFolder> curFolder(destRoot);
+
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ nsCOMPtr<nsIMsgFolder> subFolder;
+
+ bool exists;
+
+ nsString success;
+ nsString error;
+
+ // GetSubFolders() will initialize folders if they are not already
+ // initialized.
+ ProxyGetSubFolders(curFolder);
+
+ IMPORT_LOG1("ImportMailThread: Total number of folders to import = %d.",
+ count);
+
+ // Note that the front-end js script only displays one import result string so
+ // we combine both good and bad import status into one string (in var
+ // 'success').
+
+ for (uint32_t i = 0; (i < count) && !(pData->abort); i++) {
+ nsIImportMailboxDescriptor* box = pData->boxes[i];
+ pData->currentMailbox = i;
+
+ bool doImport = false;
+ size = 0;
+ rv = box->GetImport(&doImport);
+ if (doImport) rv = box->GetSize(&size);
+ rv = box->GetDepth(&newDepth);
+ if (newDepth > depth) {
+ // OK, we are going to add a subfolder under the last/previous folder we
+ // processed, so find this folder (stored in 'lastName') who is going to
+ // be the new parent folder.
+ IMPORT_LOG1("ImportMailThread: Processing child folder '%s'.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ rv = ProxyGetChildNamed(curFolder, lastName, getter_AddRefs(subFolder));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1(
+ "*** ImportMailThread: Failed to get the interface for child "
+ "folder '%s'.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ nsImportGenericMail::ReportError(IMPORT_ERROR_MB_FINDCHILD,
+ lastName.get(), &error,
+ pData->stringBundle);
+ pData->fatalError = true;
+ break;
+ }
+ curFolder = subFolder;
+ // Make sure this new parent folder obj has the correct subfolder list
+ // so far.
+ rv = ProxyGetSubFolders(curFolder);
+ } else if (newDepth < depth) {
+ rv = NS_OK;
+ while ((newDepth < depth) && NS_SUCCEEDED(rv)) {
+ rv = curFolder->GetParent(getter_AddRefs(curFolder));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1(
+ "*** ImportMailThread: Failed to get the interface for parent "
+ "folder '%s'.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ nsImportGenericMail::ReportError(IMPORT_ERROR_MB_FINDCHILD,
+ lastName.get(), &error,
+ pData->stringBundle);
+ pData->fatalError = true;
+ break;
+ }
+ depth--;
+ }
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1(
+ "*** ImportMailThread: Failed to get the proxy interface for "
+ "parent folder '%s'.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ nsImportStringBundle::GetStringByID(IMPORT_ERROR_MB_NOPROXY,
+ pData->stringBundle, error);
+ pData->fatalError = true;
+ break;
+ }
+ }
+ depth = newDepth;
+ char16_t* pName = nullptr;
+ box->GetDisplayName(&pName);
+ if (pName) {
+ lastName = pName;
+ free(pName);
+ } else
+ lastName.AssignLiteral("Unknown!");
+
+ // translate the folder name if we are doing migration, but
+ // only for special folders which are at the root level
+ if (pData->performingMigration && depth == 1)
+ pData->mailImport->TranslateFolderName(lastName, lastName);
+
+ exists = false;
+ rv = ProxyContainsChildNamed(curFolder, lastName, &exists);
+
+ // If we are performing profile migration (as opposed to importing) then
+ // we are starting with empty local folders. In that case, always choose
+ // to over-write the existing local folder with this name. Don't create a
+ // unique subfolder name. Otherwise you end up with "Inbox, Inbox0" or
+ // "Unsent Folders, UnsentFolders0"
+ if (exists && !pData->performingMigration) {
+ nsString subName;
+ ProxyGenerateUniqueSubfolderName(curFolder, lastName, nullptr, subName);
+ if (!subName.IsEmpty()) lastName.Assign(subName);
+ }
+
+ IMPORT_LOG1("ImportMailThread: Creating new import folder '%s'.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ ProxyCreateSubfolder(
+ curFolder,
+ lastName); // this may fail if the folder already exists..that's ok
+
+ rv = ProxyGetChildNamed(curFolder, lastName, getter_AddRefs(newFolder));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1(
+ "*** ImportMailThread: Failed to locate subfolder '%s' after it's "
+ "been created.",
+ NS_ConvertUTF16toUTF8(lastName).get());
+ nsImportGenericMail::ReportError(IMPORT_ERROR_MB_CREATE, lastName.get(),
+ &error, pData->stringBundle);
+ }
+
+ if (size && doImport && newFolder && NS_SUCCEEDED(rv)) {
+ bool fatalError = false;
+ pData->currentSize = size;
+ char16_t* pSuccess = nullptr;
+ char16_t* pError = nullptr;
+ rv = pData->mailImport->ImportMailbox(box, newFolder, &pError, &pSuccess,
+ &fatalError);
+ if (pError) {
+ error.Append(pError);
+ free(pError);
+ }
+ if (pSuccess) {
+ success.Append(pSuccess);
+ free(pSuccess);
+ }
+
+ pData->currentSize = 0;
+ pData->currentTotal += size;
+
+ // commit to the db synchronously, but using a proxy since it doesn't
+ // like being used elsewhere than from the main thread. OK, we've copied
+ // the actual folder/file over if the folder size is not 0 (ie, the msg
+ // summary is no longer valid) so close the msg database so that when
+ // the folder is reopened the folder db can be reconstructed (which
+ // validates msg summary and forces folder to be reparsed).
+ rv = ProxyForceDBClosed(newFolder);
+ fatalError = NS_FAILED(rv);
+
+ if (fatalError) {
+ IMPORT_LOG1(
+ "*** ImportMailThread: ImportMailbox returned fatalError, "
+ "mailbox #%d\n",
+ (int)i);
+ pData->fatalError = true;
+ break;
+ }
+ }
+ }
+
+ // Now save the new acct info to pref file.
+ nsCOMPtr<nsIMsgAccountManager> accMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv) && accMgr) {
+ rv = accMgr->SaveAccountInfo();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Can't save account info to pref file");
+ }
+
+ nsImportGenericMail::SetLogs(success, error, pData->successLog,
+ pData->errorLog);
+
+ if (pData->abort || pData->fatalError) {
+ IMPORT_LOG0("*** ImportMailThread: Abort or fatalError flag was set\n");
+ if (pData->ownsDestRoot) {
+ IMPORT_LOG0("Calling destRoot->RecursiveDelete\n");
+ destRoot->RecursiveDelete(true);
+ } else {
+ // FIXME: just delete the stuff we created?
+ }
+ }
+
+ IMPORT_LOG1("Import mailbox thread done: %d\n", (int)pData->currentTotal);
+
+ pData->ThreadDelete();
+}
+
+// Creates a folder in Local Folders with the module name + mail
+// for e.g: Outlook Mail
+bool nsImportGenericMail::CreateFolder(nsIMsgFolder** ppFolder) {
+ nsresult rv;
+ *ppFolder = nullptr;
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (!bundleService) return false;
+ rv = bundleService->CreateBundle(IMPORT_MSGS_URL, getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return false;
+ nsString folderName;
+ if (!m_pName.IsEmpty()) {
+ AutoTArray<nsString, 1> moduleName = {m_pName};
+ rv = bundle->FormatStringFromName("ImportModuleFolderName", moduleName,
+ folderName);
+ } else {
+ rv = bundle->GetStringFromName("DefaultFolderName", folderName);
+ }
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to get Folder Name!\n");
+ return false;
+ }
+ nsCOMPtr<nsIMsgAccountManager> accMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create account manager!\n");
+ return false;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accMgr->GetLocalFoldersServer(getter_AddRefs(server));
+ // if Local Folders does not exist already, create it
+ if (NS_FAILED(rv) || !server) {
+ rv = accMgr->CreateLocalMailAccount();
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create Local Folders!\n");
+ return false;
+ }
+
+ rv = accMgr->GetLocalFoldersServer(getter_AddRefs(server));
+ }
+
+ if (NS_SUCCEEDED(rv) && server) {
+ nsCOMPtr<nsIMsgFolder> localRootFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(localRootFolder));
+ if (localRootFolder) {
+ // we need to call GetSubFolders() so that the folders get initialized
+ // if they are not initialized yet.
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ rv = localRootFolder->GetSubFolders(dummy);
+ if (NS_SUCCEEDED(rv)) {
+ // check if the folder name we picked already exists.
+ bool exists = false;
+ rv = localRootFolder->ContainsChildNamed(folderName, &exists);
+ if (exists) {
+ nsString name;
+ localRootFolder->GenerateUniqueSubfolderName(folderName, nullptr,
+ name);
+ if (!name.IsEmpty())
+ folderName.Assign(name);
+ else {
+ IMPORT_LOG0("*** Failed to find a unique folder name!\n");
+ return false;
+ }
+ }
+ IMPORT_LOG1("Creating folder for importing mail: '%s'\n",
+ NS_ConvertUTF16toUTF8(folderName).get());
+
+ // Bug 564162 identifies a dataloss design flaw.
+ // A working Thunderbird client can have mail in Local Folders and a
+ // subsequent import 'Everything' will trigger a migration which
+ // overwrites existing mailboxes with the imported mailboxes.
+ rv = localRootFolder->CreateSubfolder(folderName, nullptr);
+ if (NS_SUCCEEDED(rv)) {
+ rv = localRootFolder->GetChildNamed(folderName, ppFolder);
+ if (*ppFolder) {
+ IMPORT_LOG1("Folder '%s' created successfully\n",
+ NS_ConvertUTF16toUTF8(folderName).get());
+ return true;
+ }
+ }
+ }
+ } // if localRootFolder
+ } // if server
+ IMPORT_LOG0("****** FAILED TO CREATE FOLDER FOR IMPORT\n");
+ return false;
+}
+
+/**
+ * These are the proxy objects we use to proxy nsIMsgFolder methods back
+ * the the main thread. Since there are only five, we can hand roll them.
+ * A better design might be a co-routine-ish design where the ui thread
+ * hands off each folder to the import thread and when the thread finishes
+ * the folder, the main thread hands it the next folder.
+ */
+
+class GetSubFoldersRunnable : public mozilla::Runnable {
+ public:
+ explicit GetSubFoldersRunnable(nsIMsgFolder* aFolder);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ private:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+};
+
+GetSubFoldersRunnable::GetSubFoldersRunnable(nsIMsgFolder* aFolder)
+ : mozilla::Runnable("GetSubFoldersRunnable"), m_folder(aFolder) {}
+
+NS_IMETHODIMP GetSubFoldersRunnable::Run() {
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ mResult = m_folder->GetSubFolders(dummy);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyGetSubFolders(nsIMsgFolder* aFolder) {
+ RefPtr<GetSubFoldersRunnable> getSubFolders =
+ new GetSubFoldersRunnable(aFolder);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyGetSubFolders"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(getSubFolders));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return getSubFolders->mResult;
+}
+
+class GetChildNamedRunnable : public mozilla::Runnable {
+ public:
+ GetChildNamedRunnable(nsIMsgFolder* aFolder, const nsAString& aName,
+ nsIMsgFolder** aChild);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsString m_name;
+ nsIMsgFolder** m_child;
+};
+
+GetChildNamedRunnable::GetChildNamedRunnable(nsIMsgFolder* aFolder,
+ const nsAString& aName,
+ nsIMsgFolder** aChild)
+ : mozilla::Runnable("GetChildNamedRunnable"),
+ mResult(NS_OK),
+ m_folder(aFolder),
+ m_name(aName),
+ m_child(aChild) {}
+
+NS_IMETHODIMP GetChildNamedRunnable::Run() {
+ mResult = m_folder->GetChildNamed(m_name, m_child);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyGetChildNamed(nsIMsgFolder* aFolder, const nsAString& aName,
+ nsIMsgFolder** aChild) {
+ RefPtr<GetChildNamedRunnable> getChildNamed =
+ new GetChildNamedRunnable(aFolder, aName, aChild);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyGetChildNamed"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(getChildNamed));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return getChildNamed->mResult;
+}
+
+class GetParentRunnable : public mozilla::Runnable {
+ public:
+ GetParentRunnable(nsIMsgFolder* aFolder, nsIMsgFolder** aParent);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsIMsgFolder** m_parent;
+};
+
+GetParentRunnable::GetParentRunnable(nsIMsgFolder* aFolder,
+ nsIMsgFolder** aParent)
+ : mozilla::Runnable("GetParentRunnable"),
+ mResult(NS_OK),
+ m_folder(aFolder),
+ m_parent(aParent) {}
+
+NS_IMETHODIMP GetParentRunnable::Run() {
+ mResult = m_folder->GetParent(m_parent);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyGetParent(nsIMsgFolder* aFolder, nsIMsgFolder** aParent) {
+ RefPtr<GetParentRunnable> getParent = new GetParentRunnable(aFolder, aParent);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyGetParent"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(getParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return getParent->mResult;
+}
+
+class ContainsChildNamedRunnable : public mozilla::Runnable {
+ public:
+ ContainsChildNamedRunnable(nsIMsgFolder* aFolder, const nsAString& aName,
+ bool* aResult);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsString m_name;
+ bool* m_result;
+};
+
+ContainsChildNamedRunnable::ContainsChildNamedRunnable(nsIMsgFolder* aFolder,
+ const nsAString& aName,
+ bool* aResult)
+ : mozilla::Runnable("ContainsChildNamedRunnable"),
+ mResult(NS_OK),
+ m_folder(aFolder),
+ m_name(aName),
+ m_result(aResult) {}
+
+NS_IMETHODIMP ContainsChildNamedRunnable::Run() {
+ mResult = m_folder->ContainsChildNamed(m_name, m_result);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyContainsChildNamed(nsIMsgFolder* aFolder, const nsAString& aName,
+ bool* aResult) {
+ NS_ENSURE_ARG(aFolder);
+ RefPtr<ContainsChildNamedRunnable> containsChildNamed =
+ new ContainsChildNamedRunnable(aFolder, aName, aResult);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyContainsChildNamed"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(containsChildNamed));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return containsChildNamed->mResult;
+}
+
+class GenerateUniqueSubfolderNameRunnable : public mozilla::Runnable {
+ public:
+ GenerateUniqueSubfolderNameRunnable(nsIMsgFolder* aFolder,
+ const nsAString& prefix,
+ nsIMsgFolder* otherFolder,
+ nsAString& name);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsString m_prefix;
+ nsCOMPtr<nsIMsgFolder> m_otherFolder;
+ nsString m_name;
+};
+
+GenerateUniqueSubfolderNameRunnable::GenerateUniqueSubfolderNameRunnable(
+ nsIMsgFolder* aFolder, const nsAString& aPrefix, nsIMsgFolder* aOtherFolder,
+ nsAString& aName)
+ : mozilla::Runnable("GenerateUniqueSubfolderNameRunnable"),
+ mResult(NS_OK),
+ m_folder(aFolder),
+ m_prefix(aPrefix),
+ m_otherFolder(aOtherFolder),
+ m_name(aName) {}
+
+NS_IMETHODIMP GenerateUniqueSubfolderNameRunnable::Run() {
+ mResult =
+ m_folder->GenerateUniqueSubfolderName(m_prefix, m_otherFolder, m_name);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyGenerateUniqueSubfolderName(nsIMsgFolder* aFolder,
+ const nsAString& aPrefix,
+ nsIMsgFolder* aOtherFolder,
+ nsAString& aName)
+
+{
+ RefPtr<GenerateUniqueSubfolderNameRunnable> generateUniqueSubfolderName =
+ new GenerateUniqueSubfolderNameRunnable(aFolder, aPrefix, aOtherFolder,
+ aName);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyGenerateUniqueSubfolderName"_ns,
+ mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(generateUniqueSubfolderName));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return generateUniqueSubfolderName->mResult;
+}
+
+class CreateSubfolderRunnable : public mozilla::Runnable {
+ public:
+ CreateSubfolderRunnable(nsIMsgFolder* aFolder, const nsAString& aName);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsString m_name;
+};
+
+CreateSubfolderRunnable::CreateSubfolderRunnable(nsIMsgFolder* aFolder,
+ const nsAString& aName)
+ : mozilla::Runnable("CreateSubfolderRunnable"),
+ mResult(NS_OK),
+ m_folder(aFolder),
+ m_name(aName) {}
+
+NS_IMETHODIMP CreateSubfolderRunnable::Run() {
+ mResult = m_folder->CreateSubfolder(m_name, nullptr);
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyCreateSubfolder(nsIMsgFolder* aFolder, const nsAString& aName) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ RefPtr<CreateSubfolderRunnable> createSubfolder =
+ new CreateSubfolderRunnable(aFolder, aName);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyCreateSubfolder"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(createSubfolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return createSubfolder->mResult;
+}
+
+class ForceDBClosedRunnable : public mozilla::Runnable {
+ public:
+ explicit ForceDBClosedRunnable(nsIMsgFolder* aFolder);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsCOMPtr<nsIMsgFolder> m_folder;
+};
+
+ForceDBClosedRunnable::ForceDBClosedRunnable(nsIMsgFolder* aFolder)
+ : mozilla::Runnable("ForceDBClosedRunnable"), m_folder(aFolder) {}
+
+NS_IMETHODIMP ForceDBClosedRunnable::Run() {
+ mResult = m_folder->ForceDBClosed();
+ return NS_OK; // Sync runnable must return OK.
+}
+
+nsresult ProxyForceDBClosed(nsIMsgFolder* aFolder) {
+ RefPtr<ForceDBClosedRunnable> forceDBClosed =
+ new ForceDBClosedRunnable(aFolder);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyForceDBClosed"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(forceDBClosed));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return forceDBClosed->mResult;
+}
diff --git a/comm/mailnews/import/src/nsImportMail.h b/comm/mailnews/import/src/nsImportMail.h
new file mode 100644
index 0000000000..fcd2a0c40c
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportMail.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsIImportMail.h"
+#include "nsIImportGeneric.h"
+#include "nsString.h"
+#include "nsIMsgFolder.h"
+#include "nsIStringBundle.h"
+
+#define IMPORT_MSGS_URL "chrome://messenger/locale/importMsgs.properties"
+
+////////////////////////////////////////////////////////////////////////
+
+static void ImportMailThread(void* stuff);
+
+class ImportThreadData;
+
+class nsImportGenericMail : public nsIImportGeneric {
+ public:
+ nsImportGenericMail();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTGENERIC
+
+ private:
+ virtual ~nsImportGenericMail();
+ bool CreateFolder(nsIMsgFolder** ppFolder);
+ void GetDefaultMailboxes(void);
+ void GetDefaultLocation(void);
+ void GetDefaultDestination(void);
+ void GetMailboxName(uint32_t index, nsISupportsString* pStr);
+
+ public:
+ static void SetLogs(nsString& success, nsString& error,
+ nsISupportsString* pSuccess, nsISupportsString* pError);
+ static void ReportError(int32_t id, const char16_t* pName, nsString* pStream,
+ nsIStringBundle* aBundle);
+
+ private:
+ nsString m_pName; // module name that created this interface
+ nsCOMPtr<nsIMsgFolder> m_pDestFolder;
+ bool m_deleteDestFolder;
+ bool m_createdFolder;
+ nsCOMPtr<nsIFile> m_pSrcLocation;
+ bool m_gotLocation;
+ bool m_gotDefaultMailboxes;
+ bool m_found;
+ bool m_userVerify;
+ nsCOMPtr<nsIImportMail> m_pInterface;
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>> m_mailboxes;
+ nsCOMPtr<nsISupportsString> m_pSuccessLog;
+ nsCOMPtr<nsISupportsString> m_pErrorLog;
+ uint32_t m_totalSize;
+ bool m_doImport;
+ ImportThreadData* m_pThreadData;
+ bool m_performingMigration;
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+};
+
+class ImportThreadData {
+ public:
+ bool driverAlive;
+ bool threadAlive;
+ bool abort;
+ bool fatalError;
+ uint32_t currentTotal;
+ uint32_t currentSize;
+ nsCOMPtr<nsIMsgFolder> destRoot;
+ bool ownsDestRoot;
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>> boxes;
+ nsCOMPtr<nsIImportMail> mailImport;
+ nsCOMPtr<nsISupportsString> successLog;
+ nsCOMPtr<nsISupportsString> errorLog;
+ uint32_t currentMailbox;
+ bool performingMigration;
+ nsCOMPtr<nsIStringBundle> stringBundle;
+
+ ImportThreadData();
+ ~ImportThreadData();
+ void DriverDelete();
+ void ThreadDelete();
+ void DriverAbort();
+};
diff --git a/comm/mailnews/import/src/nsImportMailboxDescriptor.cpp b/comm/mailnews/import/src/nsImportMailboxDescriptor.cpp
new file mode 100644
index 0000000000..8dc17b9317
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportMailboxDescriptor.cpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsImportMailboxDescriptor.h"
+#include "nsComponentManagerUtils.h"
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsImportMailboxDescriptor::Create(REFNSIID aIID, void** aResult) {
+ RefPtr<nsImportMailboxDescriptor> it = new nsImportMailboxDescriptor();
+ return it->QueryInterface(aIID, aResult);
+}
+
+NS_IMPL_ISUPPORTS(nsImportMailboxDescriptor, nsIImportMailboxDescriptor)
+
+nsImportMailboxDescriptor::nsImportMailboxDescriptor() {
+ m_import = true;
+ m_size = 0;
+ m_depth = 0;
+ m_id = 0;
+ m_pFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+}
diff --git a/comm/mailnews/import/src/nsImportMailboxDescriptor.h b/comm/mailnews/import/src/nsImportMailboxDescriptor.h
new file mode 100644
index 0000000000..b760cbd266
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportMailboxDescriptor.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImportMailboxDescriptor_h___
+#define nsImportMailboxDescriptor_h___
+
+#include "mozilla/Attributes.h"
+#include "nscore.h"
+#include "nsString.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+
+////////////////////////////////////////////////////////////////////////
+
+class nsImportMailboxDescriptor : public nsIImportMailboxDescriptor {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD GetIdentifier(uint32_t* pIdentifier) override {
+ *pIdentifier = m_id;
+ return NS_OK;
+ }
+ NS_IMETHOD SetIdentifier(uint32_t ident) override {
+ m_id = ident;
+ return NS_OK;
+ }
+
+ /* attribute unsigned long depth; */
+ NS_IMETHOD GetDepth(uint32_t* pDepth) override {
+ *pDepth = m_depth;
+ return NS_OK;
+ }
+ NS_IMETHOD SetDepth(uint32_t theDepth) override {
+ m_depth = theDepth;
+ return NS_OK;
+ }
+
+ /* attribute unsigned long size; */
+ NS_IMETHOD GetSize(uint32_t* pSize) override {
+ *pSize = m_size;
+ return NS_OK;
+ }
+ NS_IMETHOD SetSize(uint32_t theSize) override {
+ m_size = theSize;
+ return NS_OK;
+ }
+
+ /* attribute wstring displayName; */
+ NS_IMETHOD GetDisplayName(char16_t** pName) override {
+ *pName = ToNewUnicode(m_displayName);
+ return NS_OK;
+ }
+ NS_IMETHOD SetDisplayName(const char16_t* pName) override {
+ m_displayName = pName;
+ return NS_OK;
+ }
+
+ /* attribute boolean import; */
+ NS_IMETHOD GetImport(bool* pImport) override {
+ *pImport = m_import;
+ return NS_OK;
+ }
+ NS_IMETHOD SetImport(bool doImport) override {
+ m_import = doImport;
+ return NS_OK;
+ }
+
+ /* readonly attribute nsIFile file; */
+ NS_IMETHOD GetFile(nsIFile** aFile) override {
+ if (m_pFile) {
+ NS_ADDREF(*aFile = m_pFile);
+ return NS_OK;
+ } else
+ return NS_ERROR_FAILURE;
+ }
+
+ nsImportMailboxDescriptor();
+
+ static nsresult Create(REFNSIID aIID, void** aResult);
+
+ private:
+ virtual ~nsImportMailboxDescriptor() {}
+ uint32_t m_id; // used by creator of the structure
+ uint32_t m_depth; // depth in the hierarchy
+ nsString m_displayName; // name of this mailbox
+ nsCOMPtr<nsIFile> m_pFile; // source file (if applicable)
+ uint32_t m_size;
+ bool m_import; // import it or not?
+};
+
+#endif
diff --git a/comm/mailnews/import/src/nsImportScanFile.cpp b/comm/mailnews/import/src/nsImportScanFile.cpp
new file mode 100644
index 0000000000..79971ae10a
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportScanFile.cpp
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nscore.h"
+#include "nsImportScanFile.h"
+#include "ImportCharSet.h"
+
+nsImportScanFile::nsImportScanFile() {
+ m_allocated = false;
+ m_eof = false;
+ m_pBuf = nullptr;
+}
+
+nsImportScanFile::~nsImportScanFile() {
+ if (m_allocated) CleanUpScan();
+}
+
+void nsImportScanFile::InitScan(nsIInputStream* pInputStream, uint8_t* pBuf,
+ uint32_t sz) {
+ m_pInputStream = pInputStream;
+ m_pBuf = pBuf;
+ m_bufSz = sz;
+ m_bytesInBuf = 0;
+ m_pos = 0;
+}
+
+void nsImportScanFile::CleanUpScan(void) {
+ m_pInputStream = nullptr;
+ if (m_allocated) {
+ delete[] m_pBuf;
+ m_pBuf = NULL;
+ }
+}
+
+void nsImportScanFile::ShiftBuffer(void) {
+ uint8_t* pTop;
+ uint8_t* pCurrent;
+
+ if (m_pos < m_bytesInBuf) {
+ pTop = m_pBuf;
+ pCurrent = pTop + m_pos;
+ uint32_t cnt = m_bytesInBuf - m_pos;
+ while (cnt) {
+ *pTop = *pCurrent;
+ pTop++;
+ pCurrent++;
+ cnt--;
+ }
+ }
+
+ m_bytesInBuf -= m_pos;
+ m_pos = 0;
+}
+
+bool nsImportScanFile::FillBufferFromFile(void) {
+ uint64_t available;
+ nsresult rv = m_pInputStream->Available(&available);
+ if (NS_FAILED(rv)) return false;
+
+ // Fill up a buffer and scan it
+ ShiftBuffer();
+
+ // Read in some more bytes
+ uint32_t cnt = m_bufSz - m_bytesInBuf;
+ // To distinguish from disk errors
+ // Check first for end of file?
+ // Set a done flag if true...
+ uint32_t read;
+ char* pBuf = (char*)m_pBuf;
+ pBuf += m_bytesInBuf;
+ rv = m_pInputStream->Read(pBuf, (int32_t)cnt, &read);
+
+ if (NS_FAILED(rv)) return false;
+ rv = m_pInputStream->Available(&available);
+ if (NS_FAILED(rv)) m_eof = true;
+
+ m_bytesInBuf += cnt;
+ return true;
+}
+
+bool nsImportScanFile::Scan(bool* pDone) {
+ uint64_t available;
+ nsresult rv = m_pInputStream->Available(&available);
+ if (NS_FAILED(rv)) {
+ if (m_pos < m_bytesInBuf) ScanBuffer(pDone);
+ *pDone = true;
+ return true;
+ }
+
+ // Fill up a buffer and scan it
+ if (!FillBufferFromFile()) return false;
+
+ return ScanBuffer(pDone);
+}
+
+bool nsImportScanFile::ScanBuffer(bool*) { return true; }
+
+bool nsImportScanFileLines::ScanBuffer(bool* pDone) {
+ // m_pos, m_bytesInBuf, m_eof, m_pBuf are relevant
+
+ uint32_t pos = m_pos;
+ uint32_t max = m_bytesInBuf;
+ uint8_t* pChar = m_pBuf + pos;
+ uint32_t startPos;
+
+ while (pos < max) {
+ if (m_needEol) {
+ // Find the next eol...
+ while ((pos < max) && (*pChar != ImportCharSet::cCRChar) &&
+ (*pChar != ImportCharSet::cLinefeedChar)) {
+ pos++;
+ pChar++;
+ }
+ m_pos = pos;
+ if (pos < max) m_needEol = false;
+ if (pos == max) // need more buffer for an end of line
+ break;
+ }
+ // Skip past any eol characters
+ while ((pos < max) && ((*pChar == ImportCharSet::cCRChar) ||
+ (*pChar == ImportCharSet::cLinefeedChar))) {
+ pos++;
+ pChar++;
+ }
+ m_pos = pos;
+ if (pos == max) break;
+ // Make sure we can find either the eof or the
+ // next end of line
+ startPos = pos;
+ while ((pos < max) && (*pChar != ImportCharSet::cCRChar) &&
+ (*pChar != ImportCharSet::cLinefeedChar)) {
+ pos++;
+ pChar++;
+ }
+
+ // Is line too big for our buffer?
+ if ((pos == max) && !m_eof) {
+ if (!m_pos) { // line too big for our buffer
+ m_pos = pos;
+ m_needEol = true;
+ }
+ break;
+ }
+
+ if (!ProcessLine(m_pBuf + startPos, pos - startPos, pDone)) {
+ return false;
+ }
+ m_pos = pos;
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/import/src/nsImportScanFile.h b/comm/mailnews/import/src/nsImportScanFile.h
new file mode 100644
index 0000000000..e5704aaf36
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportScanFile.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImportScanFile_h__
+#define nsImportScanFile_h__
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+
+class nsImportScanFile {
+ public:
+ nsImportScanFile();
+ virtual ~nsImportScanFile();
+
+ void InitScan(nsIInputStream* pInputStream, uint8_t* pBuf, uint32_t sz);
+
+ void CleanUpScan(void);
+
+ virtual bool Scan(bool* pDone);
+
+ protected:
+ void ShiftBuffer(void);
+ bool FillBufferFromFile(void);
+ virtual bool ScanBuffer(bool* pDone);
+
+ protected:
+ nsCOMPtr<nsIInputStream> m_pInputStream;
+ uint8_t* m_pBuf;
+ uint32_t m_bufSz;
+ uint32_t m_bytesInBuf;
+ uint32_t m_pos;
+ bool m_eof;
+ bool m_allocated;
+};
+
+class nsImportScanFileLines : public nsImportScanFile {
+ public:
+ nsImportScanFileLines() { m_needEol = false; }
+
+ void ResetLineScan(void) { m_needEol = false; }
+
+ virtual bool ProcessLine(uint8_t* /* pLine */, uint32_t /* len */,
+ bool* /* pDone */) {
+ return true;
+ }
+
+ protected:
+ virtual bool ScanBuffer(bool* pDone) override;
+
+ bool m_needEol;
+};
+
+#endif /* nsImportScanFile_h__ */
diff --git a/comm/mailnews/import/src/nsImportService.cpp b/comm/mailnews/import/src/nsImportService.cpp
new file mode 100644
index 0000000000..062e7d664c
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportService.cpp
@@ -0,0 +1,293 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsIImportModule.h"
+#include "nsIImportService.h"
+#include "nsImportMailboxDescriptor.h"
+#include "nsImportABDescriptor.h"
+#include "nsIImportGeneric.h"
+#include "nsImportFieldMap.h"
+#include "nsICategoryManager.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsThreadUtils.h"
+#include "ImportDebug.h"
+#include "nsImportService.h"
+#include "nsImportStringBundle.h"
+#include "nsCRTGlue.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIMsgSend.h"
+#include "nsMsgUtils.h"
+#include "mozilla/SimpleEnumerator.h"
+
+mozilla::LazyLogModule IMPORTLOGMODULE("Import");
+
+////////////////////////////////////////////////////////////////////////
+
+nsImportService::nsImportService() {
+ IMPORT_LOG0("* nsImport Service Created\n");
+
+ m_didDiscovery = false;
+
+ nsresult rv = nsImportStringBundle::GetStringBundle(
+ IMPORT_MSGS_URL, getter_AddRefs(m_stringBundle));
+ if (NS_FAILED(rv))
+ IMPORT_LOG0("Failed to get string bundle for Importing Mail");
+}
+
+nsImportService::~nsImportService() {
+ IMPORT_LOG0("* nsImport Service Deleted\n");
+}
+
+NS_IMPL_ISUPPORTS(nsImportService, nsIImportService)
+
+NS_IMETHODIMP nsImportService::DiscoverModules(void) {
+ m_didDiscovery = false;
+ return DoDiscover();
+}
+
+NS_IMETHODIMP nsImportService::CreateNewFieldMap(nsIImportFieldMap** _retval) {
+ return nsImportFieldMap::Create(m_stringBundle, NS_GET_IID(nsIImportFieldMap),
+ (void**)_retval);
+}
+
+NS_IMETHODIMP nsImportService::CreateNewMailboxDescriptor(
+ nsIImportMailboxDescriptor** _retval) {
+ return nsImportMailboxDescriptor::Create(
+ NS_GET_IID(nsIImportMailboxDescriptor), (void**)_retval);
+}
+
+NS_IMETHODIMP nsImportService::CreateNewABDescriptor(
+ nsIImportABDescriptor** _retval) {
+ return nsImportABDescriptor::Create(NS_GET_IID(nsIImportABDescriptor),
+ (void**)_retval);
+}
+
+extern nsresult NS_NewGenericMail(nsIImportGeneric** aImportGeneric);
+
+NS_IMETHODIMP nsImportService::CreateNewGenericMail(
+ nsIImportGeneric** _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ return NS_NewGenericMail(_retval);
+}
+
+extern nsresult NS_NewGenericAddressBooks(nsIImportGeneric** aImportGeneric);
+
+NS_IMETHODIMP nsImportService::CreateNewGenericAddressBooks(
+ nsIImportGeneric** _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ return NS_NewGenericAddressBooks(_retval);
+}
+
+NS_IMETHODIMP nsImportService::GetModuleCount(const char* filter,
+ int32_t* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ DoDiscover();
+
+ nsCString filterStr(filter);
+ int32_t count = 0;
+ for (auto& importModule : m_importModules) {
+ if (importModule.SupportsThings(filterStr)) count++;
+ }
+ *_retval = count;
+
+ return NS_OK;
+}
+
+ImportModuleDesc* nsImportService::GetImportModule(const char* filter,
+ int32_t index) {
+ DoDiscover();
+
+ nsCString filterStr(filter);
+ int32_t count = 0;
+ for (auto& importModule : m_importModules) {
+ if (importModule.SupportsThings(filterStr)) {
+ if (count++ == index) {
+ return &importModule;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP nsImportService::GetModuleInfo(const char* filter, int32_t index,
+ nsAString& name,
+ nsAString& moduleDescription) {
+ ImportModuleDesc* importModule = GetImportModule(filter, index);
+ if (!importModule) return NS_ERROR_FAILURE;
+
+ name = importModule->GetName();
+ moduleDescription = importModule->GetDescription();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportService::GetModuleName(const char* filter, int32_t index,
+ nsAString& _retval) {
+ ImportModuleDesc* importModule = GetImportModule(filter, index);
+ if (!importModule) return NS_ERROR_FAILURE;
+
+ _retval = importModule->GetName();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsImportService::GetModuleDescription(const char* filter,
+ int32_t index,
+ nsAString& _retval) {
+ ImportModuleDesc* importModule = GetImportModule(filter, index);
+ if (!importModule) return NS_ERROR_FAILURE;
+
+ _retval = importModule->GetDescription();
+ return NS_OK;
+}
+
+class nsProxySendRunnable : public mozilla::Runnable {
+ public:
+ nsProxySendRunnable(
+ nsIMsgIdentity* aIdentity, nsIMsgCompFields* aMsgFields,
+ const char* attachment1_type, const nsACString& attachment1_body,
+ bool aIsDraft,
+ nsTArray<RefPtr<nsIMsgAttachedFile>> const& aLoadedAttachments,
+ nsTArray<RefPtr<nsIMsgEmbeddedImageData>> const& aEmbeddedAttachments,
+ nsIMsgSendListener* aListener);
+ NS_DECL_NSIRUNNABLE
+ private:
+ nsCOMPtr<nsIMsgIdentity> m_identity;
+ nsCOMPtr<nsIMsgCompFields> m_compFields;
+ bool m_isDraft;
+ nsCString m_bodyType;
+ nsCString m_body;
+ nsTArray<RefPtr<nsIMsgAttachedFile>> m_loadedAttachments;
+ nsTArray<RefPtr<nsIMsgEmbeddedImageData>> m_embeddedAttachments;
+ nsCOMPtr<nsIMsgSendListener> m_listener;
+};
+
+nsProxySendRunnable::nsProxySendRunnable(
+ nsIMsgIdentity* aIdentity, nsIMsgCompFields* aMsgFields,
+ const char* aBodyType, const nsACString& aBody, bool aIsDraft,
+ nsTArray<RefPtr<nsIMsgAttachedFile>> const& aLoadedAttachments,
+ nsTArray<RefPtr<nsIMsgEmbeddedImageData>> const& aEmbeddedAttachments,
+ nsIMsgSendListener* aListener)
+ : mozilla::Runnable("nsProxySendRunnable"),
+ m_identity(aIdentity),
+ m_compFields(aMsgFields),
+ m_isDraft(aIsDraft),
+ m_bodyType(aBodyType),
+ m_body(aBody),
+ m_loadedAttachments(aLoadedAttachments.Clone()),
+ m_embeddedAttachments(aEmbeddedAttachments.Clone()),
+ m_listener(aListener) {}
+
+NS_IMETHODIMP nsProxySendRunnable::Run() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSend> msgSend =
+ do_CreateInstance("@mozilla.org/messengercompose/send;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return msgSend->CreateRFC822Message(
+ m_identity, m_compFields, m_bodyType.get(), m_body, m_isDraft,
+ m_loadedAttachments, m_embeddedAttachments, m_listener);
+}
+
+NS_IMETHODIMP
+nsImportService::CreateRFC822Message(
+ nsIMsgIdentity* aIdentity, nsIMsgCompFields* aMsgFields,
+ const char* aBodyType, const nsACString& aBody, bool aIsDraft,
+ nsTArray<RefPtr<nsIMsgAttachedFile>> const& aLoadedAttachments,
+ nsTArray<RefPtr<nsIMsgEmbeddedImageData>> const& aEmbeddedAttachments,
+ nsIMsgSendListener* aListener) {
+ RefPtr<nsProxySendRunnable> runnable = new nsProxySendRunnable(
+ aIdentity, aMsgFields, aBodyType, aBody, aIsDraft, aLoadedAttachments,
+ aEmbeddedAttachments, aListener);
+ // invoke the callback
+ return NS_DispatchToMainThread(runnable);
+}
+
+NS_IMETHODIMP nsImportService::GetModule(const char* filter, int32_t index,
+ nsIImportModule** _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+ *_retval = nullptr;
+
+ ImportModuleDesc* importModule = GetImportModule(filter, index);
+ if (!importModule) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIImportModule> modulePtr = importModule->GetModule();
+ modulePtr.forget(_retval);
+ return NS_OK;
+}
+
+nsresult nsImportService::DoDiscover(void) {
+ if (m_didDiscovery) return NS_OK;
+
+ m_importModules.Clear();
+
+ nsresult rv;
+
+ nsCOMPtr<nsICategoryManager> catMan =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> e;
+ rv = catMan->EnumerateCategory("mailnewsimport", getter_AddRefs(e));
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto& key : mozilla::SimpleEnumerator<nsISupportsCString>(e)) {
+ nsCString keyStr;
+ key->ToString(getter_Copies(keyStr));
+ nsCString contractIdStr;
+ rv = catMan->GetCategoryEntry("mailnewsimport", keyStr, contractIdStr);
+ if (NS_SUCCEEDED(rv)) LoadModuleInfo(contractIdStr);
+ }
+
+ m_didDiscovery = true;
+
+ return NS_OK;
+}
+
+nsresult nsImportService::LoadModuleInfo(const nsCString& contractId) {
+ // load the component and get all of the info we need from it....
+ nsresult rv;
+ nsCOMPtr<nsIImportModule> module = do_CreateInstance(contractId.get(), &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ m_importModules.EmplaceBack(module);
+
+ return NS_OK;
+}
+
+ImportModuleDesc::ImportModuleDesc(nsIImportModule* importModule)
+ : m_pModule(importModule) {
+ nsresult rv;
+ rv = importModule->GetName(getter_Copies(m_name));
+ if (NS_FAILED(rv)) m_name.AssignLiteral("Unknown");
+
+ rv = importModule->GetDescription(getter_Copies(m_description));
+ if (NS_FAILED(rv)) m_description.AssignLiteral("Unknown description");
+
+ importModule->GetSupports(getter_Copies(m_supports));
+
+#ifdef IMPORT_DEBUG
+ IMPORT_LOG3("* nsImportService registered import module: %s, %s, %s\n",
+ NS_LossyConvertUTF16toASCII(m_name).get(),
+ NS_LossyConvertUTF16toASCII(m_description).get(),
+ m_supports.get());
+#endif
+}
+
+bool ImportModuleDesc::SupportsThings(const nsACString& thing) {
+ for (auto& item : m_supports.Split(',')) {
+ if (item == thing) return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/import/src/nsImportService.h b/comm/mailnews/import/src/nsImportService.h
new file mode 100644
index 0000000000..37dd95b935
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportService.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImportService_h__
+#define nsImportService_h__
+
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsIImportModule.h"
+#include "nsIImportService.h"
+#include "nsIStringBundle.h"
+#include "nsTArray.h"
+
+class ImportModuleDesc {
+ public:
+ explicit ImportModuleDesc(nsIImportModule* importModule);
+
+ const nsAString& GetName(void) { return m_name; }
+ const nsAString& GetDescription(void) { return m_description; }
+
+ nsCOMPtr<nsIImportModule>& GetModule() { return m_pModule; }
+
+ bool SupportsThings(const nsACString& pThings);
+
+ private:
+ nsString m_name;
+ nsString m_description;
+ nsCString m_supports;
+ nsCOMPtr<nsIImportModule> m_pModule;
+};
+
+class nsImportService : public nsIImportService {
+ public:
+ nsImportService();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIMPORTSERVICE
+
+ private:
+ virtual ~nsImportService();
+ nsresult LoadModuleInfo(const nsCString& contractId);
+ nsresult DoDiscover(void);
+ ImportModuleDesc* GetImportModule(const char* filter, int32_t index);
+
+ private:
+ AutoTArray<ImportModuleDesc, 10> m_importModules;
+ bool m_didDiscovery;
+ nsCString m_sysCharset;
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+};
+
+#endif // nsImportService_h__
diff --git a/comm/mailnews/import/src/nsImportStringBundle.cpp b/comm/mailnews/import/src/nsImportStringBundle.cpp
new file mode 100644
index 0000000000..0ef79bfcfc
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportStringBundle.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "prprf.h"
+#include "prmem.h"
+#include "nsCOMPtr.h"
+#include "nsIStringBundle.h"
+#include "nsImportStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/Components.h"
+
+nsresult nsImportStringBundle::GetStringBundle(const char* aPropertyURL,
+ nsIStringBundle** aBundle) {
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ rv = sBundleService->CreateBundle(aPropertyURL, aBundle);
+
+ return rv;
+}
+
+void nsImportStringBundle::GetStringByID(int32_t aStringID,
+ nsIStringBundle* aBundle,
+ nsString& aResult) {
+ aResult.Adopt(GetStringByID(aStringID, aBundle));
+}
+
+char16_t* nsImportStringBundle::GetStringByID(int32_t aStringID,
+ nsIStringBundle* aBundle) {
+ if (aBundle) {
+ nsAutoString str;
+ nsresult rv = aBundle->GetStringFromID(aStringID, str);
+ if (NS_SUCCEEDED(rv)) return ToNewUnicode(str);
+ }
+
+ nsString resultString(u"[StringID "_ns);
+ resultString.AppendInt(aStringID);
+ resultString.AppendLiteral("?]");
+
+ return ToNewUnicode(resultString);
+}
+
+void nsImportStringBundle::GetStringByName(const char* aName,
+ nsIStringBundle* aBundle,
+ nsString& aResult) {
+ aResult.Adopt(GetStringByName(aName, aBundle));
+}
+
+char16_t* nsImportStringBundle::GetStringByName(const char* aName,
+ nsIStringBundle* aBundle) {
+ if (aBundle) {
+ nsAutoString str;
+ nsresult rv = aBundle->GetStringFromName(aName, str);
+ if (NS_SUCCEEDED(rv)) return ToNewUnicode(str);
+ }
+
+ nsString resultString(u"[StringName "_ns);
+ resultString.Append(NS_ConvertUTF8toUTF16(aName).get());
+ resultString.AppendLiteral("?]");
+
+ return ToNewUnicode(resultString);
+}
diff --git a/comm/mailnews/import/src/nsImportStringBundle.h b/comm/mailnews/import/src/nsImportStringBundle.h
new file mode 100644
index 0000000000..ba234888fc
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportStringBundle.h
@@ -0,0 +1,43 @@
+/* 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/. */
+
+#ifndef _nsImportStringBundle_H__
+#define _nsImportStringBundle_H__
+
+#include "nsString.h"
+
+class nsIStringBundle;
+
+class nsImportStringBundle {
+ public:
+ static char16_t* GetStringByID(int32_t aStringID,
+ nsIStringBundle* aBundle = nullptr);
+ static void GetStringByID(int32_t aStringID, nsIStringBundle* aBundle,
+ nsString& aResult);
+ static char16_t* GetStringByName(const char* aName,
+ nsIStringBundle* aBundle = nullptr);
+ static void GetStringByName(const char* aName, nsIStringBundle* aBundle,
+ nsString& aResult);
+ static nsresult GetStringBundle(const char* aPropertyURL,
+ nsIStringBundle** aBundle);
+};
+
+#define IMPORT_MSGS_URL "chrome://messenger/locale/importMsgs.properties"
+
+#define IMPORT_NO_ADDRBOOKS 2000
+#define IMPORT_ERROR_AB_NOTINITIALIZED 2001
+#define IMPORT_ERROR_AB_NOTHREAD 2002
+#define IMPORT_ERROR_GETABOOK 2003
+#define IMPORT_NO_MAILBOXES 2004
+#define IMPORT_ERROR_MB_NOTINITIALIZED 2005
+#define IMPORT_ERROR_MB_NOTHREAD 2006
+#define IMPORT_ERROR_MB_NOPROXY 2007
+#define IMPORT_ERROR_MB_FINDCHILD 2008
+#define IMPORT_ERROR_MB_CREATE 2009
+#define IMPORT_ERROR_MB_NODESTFOLDER 2010
+
+#define IMPORT_FIELD_DESC_START 2100
+#define IMPORT_FIELD_DESC_END 2136
+
+#endif /* _nsImportStringBundle_H__ */
diff --git a/comm/mailnews/import/src/nsImportTranslator.cpp b/comm/mailnews/import/src/nsImportTranslator.cpp
new file mode 100644
index 0000000000..f988e7035d
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportTranslator.cpp
@@ -0,0 +1,308 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "ImportOutFile.h"
+#include "nsImportTranslator.h"
+
+#include "ImportCharSet.h"
+
+bool nsImportTranslator::ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed) {
+ if (pProcessed) *pProcessed = inLen;
+ return (pOutFile->WriteData(pIn, inLen));
+}
+
+void CMHTranslator::ConvertBuffer(const uint8_t* pIn, uint32_t inLen,
+ uint8_t* pOut) {
+ while (inLen) {
+ if (!ImportCharSet::IsUSAscii(*pIn) ||
+ ImportCharSet::Is822SpecialChar(*pIn) ||
+ ImportCharSet::Is822CtlChar(*pIn) ||
+ (*pIn == ImportCharSet::cSpaceChar) || (*pIn == '*') ||
+ (*pIn == '\'') || (*pIn == '%')) {
+ // needs to be encode as %hex val
+ *pOut = '%';
+ pOut++;
+ ImportCharSet::ByteToHex(*pIn, pOut);
+ pOut += 2;
+ } else {
+ *pOut = *pIn;
+ pOut++;
+ }
+ pIn++;
+ inLen--;
+ }
+ *pOut = 0;
+}
+
+bool CMHTranslator::ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed) {
+ uint8_t hex[2];
+ while (inLen) {
+ if (!ImportCharSet::IsUSAscii(*pIn) ||
+ ImportCharSet::Is822SpecialChar(*pIn) ||
+ ImportCharSet::Is822CtlChar(*pIn) ||
+ (*pIn == ImportCharSet::cSpaceChar) || (*pIn == '*') ||
+ (*pIn == '\'') || (*pIn == '%')) {
+ // needs to be encode as %hex val
+ if (!pOutFile->WriteByte('%')) return false;
+ ImportCharSet::ByteToHex(*pIn, hex);
+ if (!pOutFile->WriteData(hex, 2)) return false;
+ } else {
+ if (!pOutFile->WriteByte(*pIn)) return false;
+ }
+ pIn++;
+ inLen--;
+ }
+
+ if (pProcessed) *pProcessed = inLen;
+
+ return true;
+}
+
+bool C2047Translator::ConvertToFileQ(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed) {
+ if (!inLen) return true;
+
+ int maxLineLen = 64;
+ int curLineLen = m_startLen;
+ bool startLine = true;
+
+ uint8_t hex[2];
+ while (inLen) {
+ if (startLine) {
+ if (!pOutFile->WriteStr(" =?")) return false;
+ if (!pOutFile->WriteStr(m_charset.get())) return false;
+ if (!pOutFile->WriteStr("?q?")) return false;
+ curLineLen += (6 + m_charset.Length());
+ startLine = false;
+ }
+
+ if (!ImportCharSet::IsUSAscii(*pIn) ||
+ ImportCharSet::Is822SpecialChar(*pIn) ||
+ ImportCharSet::Is822CtlChar(*pIn) ||
+ (*pIn == ImportCharSet::cSpaceChar) || (*pIn == '?') || (*pIn == '=')) {
+ // needs to be encode as =hex val
+ if (!pOutFile->WriteByte('=')) return false;
+ ImportCharSet::ByteToHex(*pIn, hex);
+ if (!pOutFile->WriteData(hex, 2)) return false;
+ curLineLen += 3;
+ } else {
+ if (!pOutFile->WriteByte(*pIn)) return false;
+ curLineLen++;
+ }
+ pIn++;
+ inLen--;
+ if (curLineLen > maxLineLen) {
+ if (!pOutFile->WriteStr("?=")) return false;
+ if (inLen) {
+ if (!pOutFile->WriteStr("\x0D\x0A ")) return false;
+ }
+
+ startLine = true;
+ curLineLen = 0;
+ }
+ }
+
+ if (!startLine) {
+ // end the encoding!
+ if (!pOutFile->WriteStr("?=")) return false;
+ }
+
+ if (pProcessed) *pProcessed = inLen;
+
+ return true;
+}
+
+bool C2047Translator::ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed) {
+ if (m_useQuotedPrintable)
+ return ConvertToFileQ(pIn, inLen, pOutFile, pProcessed);
+
+ if (!inLen) return true;
+
+ int maxLineLen = 64;
+ int curLineLen = m_startLen;
+ bool startLine = true;
+ int encodeMax;
+ uint8_t* pEncoded = new uint8_t[maxLineLen * 2];
+
+ while (inLen) {
+ if (startLine) {
+ if (!pOutFile->WriteStr(" =?")) {
+ delete[] pEncoded;
+ return false;
+ }
+ if (!pOutFile->WriteStr(m_charset.get())) {
+ delete[] pEncoded;
+ return false;
+ }
+ if (!pOutFile->WriteStr("?b?")) {
+ delete[] pEncoded;
+ return false;
+ }
+ curLineLen += (6 + m_charset.Length());
+ startLine = false;
+ }
+ encodeMax = maxLineLen - curLineLen;
+ encodeMax *= 3;
+ encodeMax /= 4;
+ if ((uint32_t)encodeMax > inLen) encodeMax = (int)inLen;
+
+ // encode the line, end the line
+ // then continue. Update curLineLen, pIn, startLine, and inLen
+ UMimeEncode::ConvertBuffer(pIn, encodeMax, pEncoded, maxLineLen, maxLineLen,
+ "\x0D\x0A");
+
+ if (!pOutFile->WriteStr((const char*)pEncoded)) {
+ delete[] pEncoded;
+ return false;
+ }
+
+ pIn += encodeMax;
+ inLen -= encodeMax;
+ startLine = true;
+ curLineLen = 0;
+ if (!pOutFile->WriteStr("?=")) {
+ delete[] pEncoded;
+ return false;
+ }
+ if (inLen) {
+ if (!pOutFile->WriteStr("\x0D\x0A ")) {
+ delete[] pEncoded;
+ return false;
+ }
+ }
+ }
+
+ delete[] pEncoded;
+
+ if (pProcessed) *pProcessed = inLen;
+
+ return true;
+}
+
+uint32_t UMimeEncode::GetBufferSize(uint32_t inBytes) {
+ // it takes 4 base64 bytes to represent 3 regular bytes
+ inBytes += 3;
+ inBytes /= 3;
+ inBytes *= 4;
+ // This should be plenty, but just to be safe
+ inBytes += 4;
+
+ // now allow for end of line characters
+ inBytes += ((inBytes + 39) / 40) * 4;
+
+ return inBytes;
+}
+
+static uint8_t gBase64[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+uint32_t UMimeEncode::ConvertBuffer(const uint8_t* pIn, uint32_t inLen,
+ uint8_t* pOut, uint32_t maxLen,
+ uint32_t firstLineLen,
+ const char* pEolStr) {
+ uint32_t pos = 0;
+ uint32_t len = 0;
+ uint32_t lineLen = 0;
+ uint32_t maxLine = firstLineLen;
+ int eolLen = 0;
+ if (pEolStr) eolLen = strlen(pEolStr);
+
+ while ((pos + 2) < inLen) {
+ // Encode 3 bytes
+ *pOut = gBase64[*pIn >> 2];
+ pOut++;
+ len++;
+ lineLen++;
+ *pOut = gBase64[(((*pIn) & 0x3) << 4) | (((*(pIn + 1)) & 0xF0) >> 4)];
+ pIn++;
+ pOut++;
+ len++;
+ lineLen++;
+ *pOut = gBase64[(((*pIn) & 0xF) << 2) | (((*(pIn + 1)) & 0xC0) >> 6)];
+ pIn++;
+ pOut++;
+ len++;
+ lineLen++;
+ *pOut = gBase64[(*pIn) & 0x3F];
+ pIn++;
+ pOut++;
+ len++;
+ lineLen++;
+ pos += 3;
+ if (lineLen >= maxLine) {
+ lineLen = 0;
+ maxLine = maxLen;
+ if (pEolStr) {
+ memcpy(pOut, pEolStr, eolLen);
+ pOut += eolLen;
+ len += eolLen;
+ }
+ }
+ }
+
+ if ((pos < inLen) && ((lineLen + 3) > maxLine)) {
+ lineLen = 0;
+ maxLine = maxLen;
+ if (pEolStr) {
+ memcpy(pOut, pEolStr, eolLen);
+ pOut += eolLen;
+ len += eolLen;
+ }
+ }
+
+ if (pos < inLen) {
+ // Get the last few bytes!
+ *pOut = gBase64[*pIn >> 2];
+ pOut++;
+ len++;
+ pos++;
+ if (pos < inLen) {
+ *pOut = gBase64[(((*pIn) & 0x3) << 4) | (((*(pIn + 1)) & 0xF0) >> 4)];
+ pIn++;
+ pOut++;
+ pos++;
+ len++;
+ if (pos < inLen) {
+ // Should be dead code!! (Then why is it here doofus?)
+ *pOut = gBase64[(((*pIn) & 0xF) << 2) | (((*(pIn + 1)) & 0xC0) >> 6)];
+ pIn++;
+ pOut++;
+ len++;
+ *pOut = gBase64[(*pIn) & 0x3F];
+ pos++;
+ pOut++;
+ len++;
+ } else {
+ *pOut = gBase64[(((*pIn) & 0xF) << 2)];
+ pOut++;
+ len++;
+ *pOut = '=';
+ pOut++;
+ len++;
+ }
+ } else {
+ *pOut = gBase64[(((*pIn) & 0x3) << 4)];
+ pOut++;
+ len++;
+ *pOut = '=';
+ pOut++;
+ len++;
+ *pOut = '=';
+ pOut++;
+ len++;
+ }
+ }
+
+ *pOut = 0;
+
+ return len;
+}
diff --git a/comm/mailnews/import/src/nsImportTranslator.h b/comm/mailnews/import/src/nsImportTranslator.h
new file mode 100644
index 0000000000..c475700ad6
--- /dev/null
+++ b/comm/mailnews/import/src/nsImportTranslator.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsImportTranslator_h___
+#define nsImportTranslator_h___
+
+#include "mozilla/Attributes.h"
+#include "nscore.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+class ImportOutFile;
+
+class UMimeEncode {
+ public:
+ static uint32_t GetBufferSize(uint32_t inByes);
+ static uint32_t ConvertBuffer(const uint8_t* pIn, uint32_t inLen,
+ uint8_t* pOut, uint32_t maxLen = 72,
+ uint32_t firstLineLen = 72,
+ const char* pEolStr = nullptr);
+};
+
+class nsImportTranslator {
+ public:
+ virtual ~nsImportTranslator() {}
+ virtual bool Supports8bitEncoding(void) { return false; }
+ virtual uint32_t GetMaxBufferSize(uint32_t inLen) { return inLen + 1; }
+ virtual void ConvertBuffer(const uint8_t* pIn, uint32_t inLen,
+ uint8_t* pOut) {
+ memcpy(pOut, pIn, inLen);
+ pOut[inLen] = 0;
+ }
+ virtual bool ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed = nullptr);
+ virtual bool FinishConvertToFile(ImportOutFile* /* pOutFile */) {
+ return true;
+ }
+
+ virtual void GetCharset(nsCString& charSet) { charSet = "us-ascii"; }
+ virtual void GetLanguage(nsCString& lang) { lang = "en"; }
+ virtual void GetEncoding(nsCString& encoding) { encoding.Truncate(); }
+};
+
+// Specialized encoder, not a valid language translator, used for Mime headers.
+// rfc2231
+class CMHTranslator : public nsImportTranslator {
+ public:
+ virtual uint32_t GetMaxBufferSize(uint32_t inLen) override {
+ return (inLen * 3) + 1;
+ }
+ virtual void ConvertBuffer(const uint8_t* pIn, uint32_t inLen,
+ uint8_t* pOut) override;
+ virtual bool ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed = nullptr) override;
+};
+
+// Specialized encoder, not a valid language translator, used for mail headers
+// rfc2047
+class C2047Translator : public nsImportTranslator {
+ public:
+ virtual ~C2047Translator() {}
+
+ C2047Translator(const char* pCharset, uint32_t headerLen) {
+ m_charset = pCharset;
+ m_startLen = headerLen;
+ m_useQuotedPrintable = false;
+ }
+
+ void SetUseQuotedPrintable(void) { m_useQuotedPrintable = true; }
+
+ virtual bool ConvertToFile(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile,
+ uint32_t* pProcessed = nullptr) override;
+ bool ConvertToFileQ(const uint8_t* pIn, uint32_t inLen,
+ ImportOutFile* pOutFile, uint32_t* pProcessed);
+
+ protected:
+ bool m_useQuotedPrintable;
+ nsCString m_charset;
+ uint32_t m_startLen;
+};
+
+#endif /* nsImportTranslator_h__ */
diff --git a/comm/mailnews/import/src/nsOutlookCompose.cpp b/comm/mailnews/import/src/nsOutlookCompose.cpp
new file mode 100644
index 0000000000..1098dcbab7
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookCompose.cpp
@@ -0,0 +1,669 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nscore.h"
+#include "prthread.h"
+#include "nsString.h"
+#include "nsMsgUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIURI.h"
+#include "nsMsgI18N.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsMsgAttachmentData.h"
+#include "nsIMsgCompFields.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgSend.h"
+#include "nsImportEmbeddedImageData.h"
+#include "nsNetCID.h"
+#include "nsCRT.h"
+#include "nsOutlookCompose.h"
+#include "nsTArray.h"
+
+#include "ImportDebug.h"
+
+#include "nsMimeTypes.h"
+#include "nsMsgUtils.h"
+
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#include "nsMsgMessageFlags.h"
+#include "nsMsgLocalFolderHdrs.h"
+
+#define NS_MSGCOMPFIELDS_CID \
+ { /* e64b0f51-0d7b-4e2f-8c60-3862ee8c174f */ \
+ 0xe64b0f51, 0x0d7b, 0x4e2f, { \
+ 0x8c, 0x60, 0x38, 0x62, 0xee, 0x8c, 0x17, 0x4f \
+ } \
+ }
+static NS_DEFINE_CID(kMsgCompFieldsCID, NS_MSGCOMPFIELDS_CID);
+
+#ifdef IMPORT_DEBUG
+static const char* p_test_headers =
+ "Received: from netppl.invalid (IDENT:monitor@get.freebsd.because.microsoftsucks.invalid [209.3.31.115])\n\
+ by mail4.sirius.invalid (8.9.1/8.9.1) with SMTP id PAA27232;\n\
+ Mon, 17 May 1999 15:27:43 -0700 (PDT)\n\
+Message-ID: <ikGD3jRTsKklU.Ggm2HmE2A1Jsqd0p@netppl.invalid>\n\
+From: \"adsales@qualityservice.invalid\" <adsales@qualityservice.invalid>\n\
+Subject: Re: Your College Diploma (36822)\n\
+Date: Mon, 17 May 1999 15:09:29 -0400 (EDT)\n\
+MIME-Version: 1.0\n\
+Content-Type: TEXT/PLAIN; charset=\"US-ASCII\"\n\
+Content-Transfer-Encoding: 7bit\n\
+X-UIDL: 19990517.152941\n\
+Status: RO";
+
+static const char* p_test_body =
+ "Hello world?\n\
+";
+#else
+# define p_test_headers nullptr
+# define p_test_body nullptr
+#endif
+
+#define kWhitespace "\b\t\r\n "
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+
+// A replacement for SimpleBufferTonyRCopiedTwice round-robin buffer and
+// ReadFileState classes
+class CCompositionFile {
+ public:
+ // fifoBuffer is used for memory allocation optimization
+ // convertCRs controls if we want to convert standalone CRs to CRLFs
+ CCompositionFile(nsIFile* aFile, void* fifoBuffer, uint32_t fifoBufferSize,
+ bool convertCRs = false);
+
+ explicit operator bool() const { return m_fileSize && m_pInputStream; }
+
+ // Reads up to and including the term sequence, or entire file if term isn't
+ // found termSize may be used to include NULLs in the terminator sequences.
+ // termSize value of -1 means "zero-terminated string" -> size is calculated
+ // with strlen
+ nsresult ToString(nsCString& dest, const char* term = 0, int termSize = -1);
+ nsresult ToStream(nsIOutputStream* dest, const char* term = 0,
+ int termSize = -1);
+ char LastChar() { return m_lastChar; }
+
+ private:
+ nsCOMPtr<nsIFile> m_pFile;
+ nsCOMPtr<nsIInputStream> m_pInputStream;
+ int64_t m_fileSize;
+ int64_t m_fileReadPos;
+ char* m_fifoBuffer;
+ uint32_t m_fifoBufferSize;
+ char* m_fifoBufferReadPos; // next character to read
+ char* m_fifoBufferWrittenPos; // if we have read less than buffer size then
+ // this will show it
+ bool m_convertCRs;
+ char m_lastChar;
+
+ nsresult EnsureHasDataInBuffer();
+ template <class _OutFn>
+ nsresult ToDest(_OutFn dest, const char* term, int termSize);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+
+// First off, a listener
+class OutlookSendListener : public nsIMsgSendListener {
+ public:
+ OutlookSendListener() { m_done = false; }
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* void OnStartSending (in string aMsgID, in uint32_t aMsgSize); */
+ NS_IMETHOD OnStartSending(const char* aMsgID, uint32_t aMsgSize) {
+ return NS_OK;
+ }
+
+ /* void OnProgress (in string aMsgID, in uint32_t aProgress, in uint32_t
+ * aProgressMax); */
+ NS_IMETHOD OnProgress(const char* aMsgID, uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_OK;
+ }
+
+ /* void OnStatus (in string aMsgID, in wstring aMsg); */
+ NS_IMETHOD OnStatus(const char* aMsgID, const char16_t* aMsg) {
+ return NS_OK;
+ }
+
+ /* void OnStopSending (in string aMsgID, in nsresult aStatus, in wstring aMsg,
+ * in nsIFile returnFile); */
+ NS_IMETHOD OnStopSending(const char* aMsgID, nsresult aStatus,
+ const char16_t* aMsg, nsIFile* returnFile) {
+ m_done = true;
+ m_location = returnFile;
+ return NS_OK;
+ }
+
+ /* void OnTransportSecurityError( in string msgID, in nsresult status, in
+ * nsITransportSecurityInfo secInfo, in ACString location); */
+ NS_IMETHOD OnTransportSecurityError(const char* msgID, nsresult status,
+ nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ return NS_OK;
+ }
+
+ /* void OnSendNotPerformed */
+ NS_IMETHOD OnSendNotPerformed(const char* aMsgID, nsresult aStatus) {
+ return NS_OK;
+ }
+
+ /* void OnGetDraftFolderURI (); */
+ NS_IMETHOD OnGetDraftFolderURI(const char* aMsgID,
+ const nsACString& aFolderURI) {
+ return NS_OK;
+ }
+
+ static nsresult CreateSendListener(nsIMsgSendListener** ppListener);
+ void Reset() {
+ m_done = false;
+ m_location = nullptr;
+ }
+
+ public:
+ virtual ~OutlookSendListener() {}
+
+ bool m_done;
+ nsCOMPtr<nsIFile> m_location;
+};
+
+NS_IMPL_ISUPPORTS(OutlookSendListener, nsIMsgSendListener)
+
+nsresult OutlookSendListener::CreateSendListener(
+ nsIMsgSendListener** ppListener) {
+ NS_ENSURE_ARG_POINTER(ppListener);
+ NS_ADDREF(*ppListener = new OutlookSendListener());
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////
+
+nsOutlookCompose::nsOutlookCompose() {
+ m_optimizationBuffer = new char[FILE_IO_BUFFER_SIZE];
+}
+
+nsOutlookCompose::~nsOutlookCompose() {
+ if (m_pIdentity) {
+ nsresult rv = m_pIdentity->ClearAllValues();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to clear values");
+ if (NS_FAILED(rv)) return;
+ }
+ delete[] m_optimizationBuffer;
+}
+
+nsCOMPtr<nsIMsgIdentity> nsOutlookCompose::m_pIdentity = nullptr;
+
+nsresult nsOutlookCompose::CreateIdentity(void) {
+ if (m_pIdentity) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accMgr->CreateIdentity(getter_AddRefs(m_pIdentity));
+ nsString name;
+ name.AssignLiteral("Import Identity");
+ if (m_pIdentity) {
+ m_pIdentity->SetFullName(name);
+ m_pIdentity->SetEmail("import@service.invalid"_ns);
+ }
+ return rv;
+}
+
+void nsOutlookCompose::ReleaseIdentity() { m_pIdentity = nullptr; }
+
+nsresult nsOutlookCompose::CreateComponents(void) {
+ nsresult rv = NS_OK;
+
+ m_pMsgFields = nullptr;
+ if (!m_pListener)
+ rv = OutlookSendListener::CreateSendListener(getter_AddRefs(m_pListener));
+
+ if (NS_SUCCEEDED(rv)) {
+ m_pMsgFields = do_CreateInstance(kMsgCompFieldsCID, &rv);
+ if (NS_SUCCEEDED(rv) && m_pMsgFields) {
+ // IMPORT_LOG0("nsOutlookCompose - CreateComponents succeeded\n");
+ m_pMsgFields->SetForcePlainText(false);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsOutlookCompose::ComposeTheMessage(nsMsgDeliverMode mode,
+ CMapiMessage& msg,
+ nsIFile** pMsg) {
+ nsresult rv = CreateComponents();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = CreateIdentity();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // IMPORT_LOG0("Outlook Compose created necessary components\n");
+
+ CMapiMessageHeaders* headers = msg.GetHeaders();
+
+ nsString unival;
+ headers->UnfoldValue(CMapiMessageHeaders::hdrFrom, unival,
+ msg.GetBodyCharset());
+ m_pMsgFields->SetFrom(unival);
+ headers->UnfoldValue(CMapiMessageHeaders::hdrTo, unival,
+ msg.GetBodyCharset());
+ m_pMsgFields->SetTo(unival);
+ headers->UnfoldValue(CMapiMessageHeaders::hdrSubject, unival,
+ msg.GetBodyCharset());
+ m_pMsgFields->SetSubject(unival);
+ headers->UnfoldValue(CMapiMessageHeaders::hdrCc, unival,
+ msg.GetBodyCharset());
+ m_pMsgFields->SetCc(unival);
+ headers->UnfoldValue(CMapiMessageHeaders::hdrReplyTo, unival,
+ msg.GetBodyCharset());
+ m_pMsgFields->SetReplyTo(unival);
+ m_pMsgFields->SetMessageId(headers->Value(CMapiMessageHeaders::hdrMessageID));
+
+ // We only use those headers that may need to be processed by Thunderbird
+ // to create a good rfc822 document, or need to be encoded (like To and Cc).
+ // These will replace the originals on import. All the other headers
+ // will be copied to the destination unaltered in CopyComposedMessage().
+
+ nsTArray<RefPtr<nsIMsgAttachedFile>> attachments;
+ msg.GetAttachments(attachments);
+
+ nsString bodyW;
+ bodyW = msg.GetBody();
+
+ nsTArray<RefPtr<nsIMsgEmbeddedImageData>> embeddedObjects;
+
+ if (msg.BodyIsHtml()) {
+ for (unsigned int i = 0; i < msg.EmbeddedAttachmentsCount(); i++) {
+ nsIURI* uri;
+ const char* cid;
+ const char* name;
+ if (msg.GetEmbeddedAttachmentInfo(i, &uri, &cid, &name)) {
+ nsCOMPtr<nsIMsgEmbeddedImageData> imageData =
+ new nsImportEmbeddedImageData(uri, nsDependentCString(cid),
+ nsDependentCString(name));
+ embeddedObjects.AppendElement(imageData);
+ }
+ }
+ }
+
+ nsCString bodyA;
+ const char* charset = msg.GetBodyCharset();
+ nsMsgI18NConvertFromUnicode(
+ charset ? nsDependentCString(charset) : EmptyCString(), bodyW, bodyA);
+
+ nsCOMPtr<nsIImportService> impService(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nsIImportService::CreateRFC822Message creates a runnable and dispatches to
+ // the main thread.
+ rv = impService->CreateRFC822Message(
+ m_pIdentity, // dummy identity
+ m_pMsgFields, // message fields
+ msg.BodyIsHtml() ? "text/html" : "text/plain",
+ bodyA, // body pointer
+ mode == nsIMsgSend::nsMsgSaveAsDraft,
+ attachments, // local attachments
+ embeddedObjects,
+ m_pListener); // listener
+
+ OutlookSendListener* pListen =
+ static_cast<OutlookSendListener*>(m_pListener.get());
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1("*** Error, CreateAndSendMessage FAILED: 0x%x\n", rv);
+ } else {
+ // Wait for the listener to get done.
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+ while (!pListen->m_done) {
+ NS_ProcessNextEvent(thread, true);
+ }
+ }
+
+ if (pListen->m_location) {
+ pListen->m_location->Clone(pMsg);
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_FAILURE;
+ IMPORT_LOG0("*** Error, Outlook compose unsuccessful\n");
+ }
+
+ pListen->Reset();
+ return rv;
+}
+
+nsresult nsOutlookCompose::CopyComposedMessage(nsIFile* pSrc,
+ nsIOutputStream* pDst,
+ CMapiMessage& origMsg) {
+ // I'm unsure if we really need the convertCRs feature here.
+ // The headers in the file are generated by TB, the body was generated by rtf
+ // reader that always used CRLF, and the attachments were processed by TB
+ // either... However, I let it stay as it was in the original code.
+ CCompositionFile f(pSrc, m_optimizationBuffer, FILE_IO_BUFFER_SIZE, true);
+ if (!f) {
+ IMPORT_LOG0("*** Error, unexpected zero file size for composed message\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ // The "From ..." separates the messages. Without it, TB cannot see the
+ // messages in the mailbox file. Thus, the lines that look like "From ..." in
+ // the message must be escaped (see EscapeFromSpaceLine())
+ int fromLineLen;
+ const char* fromLine = origMsg.GetFromLine(fromLineLen);
+ uint32_t written;
+ nsresult rv = pDst->Write(fromLine, fromLineLen, &written);
+
+ // Bug 219269
+ // Write out the x-mozilla-status headers.
+ char statusLine[50];
+ uint32_t msgFlags = 0;
+ if (origMsg.IsRead()) msgFlags |= nsMsgMessageFlags::Read;
+ if (!origMsg.FullMessageDownloaded()) msgFlags |= nsMsgMessageFlags::Partial;
+ if (origMsg.IsForvarded()) msgFlags |= nsMsgMessageFlags::Forwarded;
+ if (origMsg.IsReplied()) msgFlags |= nsMsgMessageFlags::Replied;
+ if (origMsg.HasAttach()) msgFlags |= nsMsgMessageFlags::Attachment;
+ _snprintf(statusLine, sizeof(statusLine),
+ X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK, msgFlags & 0xFFFF);
+ rv = pDst->Write(statusLine, strlen(statusLine), &written);
+ _snprintf(statusLine, sizeof(statusLine),
+ X_MOZILLA_STATUS2_FORMAT MSG_LINEBREAK, msgFlags & 0xFFFF0000);
+ rv = pDst->Write(statusLine, strlen(statusLine), &written);
+ // End Bug 219269
+
+ // well, isn't this a hoot!
+ // Read the headers from the new message, get the ones we like
+ // and write out only the headers we want from the new message,
+ // along with all of the other headers from the "old" message!
+
+ nsCString newHeadersStr;
+ rv = f.ToString(newHeadersStr,
+ MSG_LINEBREAK MSG_LINEBREAK); // Read all the headers
+ NS_ENSURE_SUCCESS(rv, rv);
+ UpdateHeaders(*origMsg.GetHeaders(),
+ CMapiMessageHeaders(newHeadersStr.get()));
+ rv = origMsg.GetHeaders()->ToStream(pDst);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // I use the terminating sequence here to avoid a possible situation when a
+ // "From " line gets split over two sequential reads and thus will not be
+ // escaped. This is done by reading up to CRLF (one line every time), though
+ // it may be slower
+
+ // Here I revert the changes that were made when the multipart/related message
+ // was composed in nsMsgSend::ProcessMultipartRelated() - the Content-Ids of
+ // attachments were replaced with new ones.
+ nsCString line;
+ while (NS_SUCCEEDED(f.ToString(line, MSG_LINEBREAK))) {
+ EscapeFromSpaceLine(pDst, const_cast<char*>(line.get()),
+ line.get() + line.Length());
+ }
+
+ if (f.LastChar() != nsCRT::LF) {
+ rv = pDst->Write(MSG_LINEBREAK, 2, &written);
+ if (written != 2) rv = NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+nsresult nsOutlookCompose::ProcessMessage(nsMsgDeliverMode mode,
+ CMapiMessage& msg,
+ nsIOutputStream* pDst) {
+ nsCOMPtr<nsIFile> compositionFile;
+ nsresult rv = ComposeTheMessage(mode, msg, getter_AddRefs(compositionFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = CopyComposedMessage(compositionFile, pDst, msg);
+ compositionFile->Remove(false);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error copying composed message to destination mailbox\n");
+ }
+ return rv;
+}
+
+void nsOutlookCompose::UpdateHeader(CMapiMessageHeaders& oldHeaders,
+ const CMapiMessageHeaders& newHeaders,
+ CMapiMessageHeaders::SpecialHeader header,
+ bool addIfAbsent) {
+ const char* oldVal = oldHeaders.Value(header);
+ if (!addIfAbsent && !oldVal) return;
+ const char* newVal = newHeaders.Value(header);
+ if (!newVal) return;
+ // Bug 145150 - Turn "Content-Type: application/ms-tnef" into "Content-Type:
+ // text/plain"
+ // so the body text can be displayed normally (instead of in an
+ // attachment).
+ if (header == CMapiMessageHeaders::hdrContentType)
+ if (stricmp(newVal, "application/ms-tnef") == 0) newVal = "text/plain";
+ // End Bug 145150
+ if (oldVal) {
+ if (strcmp(oldVal, newVal) == 0) return;
+ // Backup the old header value
+ nsCString backupHdrName("X-MozillaBackup-");
+ backupHdrName += CMapiMessageHeaders::SpecialName(header);
+ oldHeaders.SetValue(backupHdrName.get(), oldVal, false);
+ }
+ // Now replace it with new value
+ oldHeaders.SetValue(header, newVal);
+}
+
+void nsOutlookCompose::UpdateHeaders(CMapiMessageHeaders& oldHeaders,
+ const CMapiMessageHeaders& newHeaders) {
+ // Well, ain't this a peach?
+ // This is rather disgusting but there really isn't much to be done about
+ // it....
+
+ // 1. For each "old" header, replace it with the new one if we want,
+ // then right it out.
+ // 2. Then if we haven't written the "important" new headers, write them out
+ // 3. Terminate the headers with an extra eol.
+
+ // Important headers:
+ // "Content-type",
+ // "MIME-Version",
+ // "Content-transfer-encoding"
+ // consider "X-Accept-Language"?
+
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrContentType);
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrMimeVersion);
+ UpdateHeader(oldHeaders, newHeaders,
+ CMapiMessageHeaders::hdrContentTransferEncoding);
+
+ // Other replaced headers (only if they exist):
+ // "From",
+ // "To",
+ // "Subject",
+ // "Reply-to",
+ // "Cc"
+
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrFrom, false);
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrTo, false);
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrSubject, false);
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrReplyTo, false);
+ UpdateHeader(oldHeaders, newHeaders, CMapiMessageHeaders::hdrCc, false);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+CCompositionFile::CCompositionFile(nsIFile* aFile, void* fifoBuffer,
+ uint32_t fifoBufferSize, bool convertCRs)
+ : m_pFile(aFile),
+ m_fileSize(0),
+ m_fileReadPos(0),
+ m_fifoBuffer(static_cast<char*>(fifoBuffer)),
+ m_fifoBufferSize(fifoBufferSize),
+ m_fifoBufferReadPos(static_cast<char*>(fifoBuffer)),
+ m_fifoBufferWrittenPos(static_cast<char*>(fifoBuffer)),
+ m_convertCRs(convertCRs),
+ m_lastChar(0) {
+ m_pFile->GetFileSize(&m_fileSize);
+ if (!m_fileSize) {
+ IMPORT_LOG0("*** Error, unexpected zero file size for composed message\n");
+ return;
+ }
+
+ nsresult rv =
+ NS_NewLocalFileInputStream(getter_AddRefs(m_pInputStream), m_pFile);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error, unable to open composed message file\n");
+ return;
+ }
+}
+
+nsresult CCompositionFile::EnsureHasDataInBuffer() {
+ if (m_fifoBufferReadPos < m_fifoBufferWrittenPos) return NS_OK;
+ // Populate the buffer with new data!
+ uint32_t count = m_fifoBufferSize;
+ if ((m_fileReadPos + count) > m_fileSize) count = m_fileSize - m_fileReadPos;
+ if (!count) return NS_ERROR_FAILURE; // Isn't there a "No more data" error?
+
+ uint32_t bytesRead = 0;
+ nsresult rv = m_pInputStream->Read(m_fifoBuffer, count, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bytesRead || (bytesRead > count)) return NS_ERROR_FAILURE;
+ m_fifoBufferWrittenPos = m_fifoBuffer + bytesRead;
+ m_fifoBufferReadPos = m_fifoBuffer;
+ m_fileReadPos += bytesRead;
+
+ return NS_OK;
+}
+
+class CTermGuard {
+ public:
+ CTermGuard(const char* term, int termSize)
+ : m_term(term),
+ m_termSize(term ? ((termSize != -1) ? termSize : strlen(term)) : 0),
+ m_matchPos(0) {}
+
+ // if the guard triggered
+ inline bool IsTriggered() const {
+ return m_termSize && (m_matchPos == m_termSize);
+ }
+ // indicates if the guard has something to check
+ inline bool IsChecking() const { return m_termSize; }
+
+ bool Check(char c) // returns true only if the whole sequence passed
+ {
+ if (!m_termSize) // no guard
+ return false;
+ if (m_matchPos >= m_termSize) // check past success!
+ m_matchPos = 0;
+ if (m_term[m_matchPos] != c) // Reset sequence
+ m_matchPos = 0;
+ if (m_term[m_matchPos] == c) { // Sequence continues
+ return ++m_matchPos == m_termSize; // If equal then sequence complete!
+ }
+ // Sequence broken
+ return false;
+ }
+
+ private:
+ const char* m_term;
+ int m_termSize;
+ int m_matchPos;
+};
+
+template <class _OutFn>
+nsresult CCompositionFile::ToDest(_OutFn dest, const char* term, int termSize) {
+ CTermGuard guard(term, termSize);
+
+ // We already know the required string size, so reduce future reallocations
+ if (!guard.IsChecking() && !m_convertCRs)
+ dest.SetCapacity(m_fileSize - m_fileReadPos);
+
+ bool wasCR = false;
+ char c = 0;
+ nsresult rv;
+ while (NS_SUCCEEDED(rv = EnsureHasDataInBuffer())) {
+ if (!guard.IsChecking() && !m_convertCRs) { // Use efficient algorithm
+ dest.Append(m_fifoBufferReadPos,
+ m_fifoBufferWrittenPos - m_fifoBufferReadPos);
+ } else { // Check character by character to convert CRs and find
+ // terminating sequence
+ while (m_fifoBufferReadPos < m_fifoBufferWrittenPos) {
+ c = *m_fifoBufferReadPos;
+ if (m_convertCRs && wasCR) {
+ wasCR = false;
+ if (c != nsCRT::LF) {
+ const char kTmpLF = nsCRT::LF;
+ dest.Append(&kTmpLF, 1);
+ if (guard.Check(nsCRT::LF)) {
+ c = nsCRT::LF; // save last char
+ break;
+ }
+ }
+ }
+ dest.Append(&c, 1);
+ m_fifoBufferReadPos++;
+
+ if (guard.Check(c)) break;
+
+ if (m_convertCRs && (c == nsCRT::CR)) wasCR = true;
+ }
+ if (guard.IsTriggered()) break;
+ }
+ }
+
+ // check for trailing CR (only if caller didn't specify the terminating
+ // sequence that ends with CR - in this case he knows what he does!)
+ if (m_convertCRs && !guard.IsTriggered() && (c == nsCRT::CR)) {
+ c = nsCRT::LF;
+ dest.Append(&c, 1);
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_lastChar = c;
+ return NS_OK;
+}
+
+class dest_nsCString {
+ public:
+ explicit dest_nsCString(nsCString& str) : m_str(str) { m_str.Truncate(); }
+ void SetCapacity(int32_t sz) { m_str.SetCapacity(sz); }
+ nsresult Append(const char* buf, uint32_t count) {
+ m_str.Append(buf, count);
+ return NS_OK;
+ }
+
+ private:
+ nsCString& m_str;
+};
+
+class dest_Stream {
+ public:
+ explicit dest_Stream(nsIOutputStream* dest) : m_stream(dest) {}
+ void SetCapacity(int32_t) { /*do nothing*/
+ }
+ // const_cast here is due to the poor design of the EscapeFromSpaceLine()
+ // that requires a non-constant pointer while doesn't modify its data
+ nsresult Append(const char* buf, uint32_t count) {
+ return EscapeFromSpaceLine(m_stream, const_cast<char*>(buf), buf + count);
+ }
+
+ private:
+ nsIOutputStream* m_stream;
+};
+
+nsresult CCompositionFile::ToString(nsCString& dest, const char* term,
+ int termSize) {
+ return ToDest(dest_nsCString(dest), term, termSize);
+}
+
+nsresult CCompositionFile::ToStream(nsIOutputStream* dest, const char* term,
+ int termSize) {
+ return ToDest(dest_Stream(dest), term, termSize);
+}
diff --git a/comm/mailnews/import/src/nsOutlookCompose.h b/comm/mailnews/import/src/nsOutlookCompose.h
new file mode 100644
index 0000000000..8d49157e68
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookCompose.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#ifndef nsOutlookCompose_h__
+#define nsOutlookCompose_h__
+
+#include "nscore.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsIImportService.h"
+#include "nsIOutputStream.h"
+
+class nsIMsgSend;
+class nsIMsgCompFields;
+class nsIMsgIdentity;
+class nsIMsgSendListener;
+
+#include "nsIMsgSend.h"
+#include "nsNetUtil.h"
+
+#include "MapiMessage.h"
+
+#include <list>
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+class nsOutlookCompose {
+ public:
+ nsOutlookCompose();
+ ~nsOutlookCompose();
+
+ nsresult ProcessMessage(nsMsgDeliverMode mode, CMapiMessage& msg,
+ nsIOutputStream* pDst);
+ static nsresult CreateIdentity(void);
+ static void ReleaseIdentity(void);
+
+ private:
+ nsresult CreateComponents(void);
+
+ void UpdateHeader(CMapiMessageHeaders& oldHeaders,
+ const CMapiMessageHeaders& newHeaders,
+ CMapiMessageHeaders::SpecialHeader header,
+ bool addIfAbsent = true);
+ void UpdateHeaders(CMapiMessageHeaders& oldHeaders,
+ const CMapiMessageHeaders& newHeaders);
+
+ nsresult ComposeTheMessage(nsMsgDeliverMode mode, CMapiMessage& msg,
+ nsIFile** pMsg);
+ nsresult CopyComposedMessage(nsIFile* pSrc, nsIOutputStream* pDst,
+ CMapiMessage& origMsg);
+
+ private:
+ nsCOMPtr<nsIMsgSendListener> m_pListener;
+ nsCOMPtr<nsIMsgCompFields> m_pMsgFields;
+ static nsCOMPtr<nsIMsgIdentity> m_pIdentity;
+ char* m_optimizationBuffer;
+ nsCOMPtr<nsIImportService> m_pImportService;
+};
+
+#endif /* nsOutlookCompose_h__ */
diff --git a/comm/mailnews/import/src/nsOutlookImport.cpp b/comm/mailnews/import/src/nsOutlookImport.cpp
new file mode 100644
index 0000000000..ceafa2d7a1
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookImport.cpp
@@ -0,0 +1,522 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ Outlook (Win32) import mail and addressbook interfaces
+*/
+#include "nscore.h"
+#include "nsString.h"
+#include "nsMsgUtils.h"
+#include "nsIImportService.h"
+#include "nsOutlookImport.h"
+#include "nsIImportService.h"
+#include "nsIImportMail.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIImportGeneric.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIImportFieldMap.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIAbDirectory.h"
+#include "nsOutlookSettings.h"
+#include "nsTextFormatter.h"
+#include "nsOutlookStringBundle.h"
+#include "ImportDebug.h"
+#include "nsUnicharUtils.h"
+
+#include "nsOutlookMail.h"
+
+#include "MapiApi.h"
+
+class ImportOutlookMailImpl : public nsIImportMail {
+ public:
+ ImportOutlookMailImpl();
+
+ static nsresult Create(nsIImportMail** aImport);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIImportmail interface
+
+ /* void GetDefaultLocation (out nsIFile location, out boolean found, out
+ * boolean userVerify); */
+ NS_IMETHOD GetDefaultLocation(nsIFile** location, bool* found,
+ bool* userVerify);
+
+ NS_IMETHOD FindMailboxes(nsIFile* location,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes);
+
+ NS_IMETHOD ImportMailbox(nsIImportMailboxDescriptor* source,
+ nsIMsgFolder* dstFolder, char16_t** pErrorLog,
+ char16_t** pSuccessLog, bool* fatalError);
+
+ /* unsigned long GetImportProgress (); */
+ NS_IMETHOD GetImportProgress(uint32_t* _retval);
+
+ NS_IMETHOD TranslateFolderName(const nsAString& aFolderName,
+ nsAString& _retval);
+
+ public:
+ static void ReportSuccess(nsString& name, int32_t count, nsString* pStream);
+ static void ReportError(int32_t errorNum, nsString& name, nsString* pStream);
+ static void AddLinebreak(nsString* pStream);
+ static void SetLogs(nsString& success, nsString& error, char16_t** pError,
+ char16_t** pSuccess);
+
+ private:
+ virtual ~ImportOutlookMailImpl();
+ nsOutlookMail m_mail;
+ uint32_t m_bytesDone;
+};
+
+class ImportOutlookAddressImpl : public nsIImportAddressBooks {
+ public:
+ ImportOutlookAddressImpl();
+
+ static nsresult Create(nsIImportAddressBooks** aImport);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIImportAddressBooks interface
+
+ NS_IMETHOD GetSupportsMultiple(bool* _retval) {
+ *_retval = true;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetAutoFind(char16_t** description, bool* _retval);
+
+ NS_IMETHOD GetNeedsFieldMap(nsIFile* location, bool* _retval) {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetDefaultLocation(nsIFile** location, bool* found,
+ bool* userVerify) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IMETHOD FindAddressBooks(nsIFile* location,
+ nsTArray<RefPtr<nsIImportABDescriptor>>& books);
+
+ NS_IMETHOD InitFieldMap(nsIImportFieldMap* fieldMap) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IMETHOD ImportAddressBook(nsIImportABDescriptor* source,
+ nsIAbDirectory* destination,
+ nsIImportFieldMap* fieldMap,
+ nsISupports* aSupportService,
+ char16_t** errorLog, char16_t** successLog,
+ bool* fatalError);
+
+ NS_IMETHOD GetImportProgress(uint32_t* _retval);
+
+ NS_IMETHOD GetSampleData(int32_t index, bool* pFound, char16_t** pStr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IMETHOD SetSampleLocation(nsIFile*) { return NS_OK; }
+
+ private:
+ virtual ~ImportOutlookAddressImpl();
+ void ReportSuccess(nsString& name, nsString* pStream);
+
+ private:
+ uint32_t m_msgCount;
+ uint32_t m_msgTotal;
+ nsOutlookMail m_address;
+};
+////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////
+
+nsOutlookImport::nsOutlookImport() {
+ IMPORT_LOG0("nsOutlookImport Module Created\n");
+
+ nsOutlookStringBundle::GetStringBundle();
+}
+
+nsOutlookImport::~nsOutlookImport() {
+ IMPORT_LOG0("nsOutlookImport Module Deleted\n");
+}
+
+NS_IMPL_ISUPPORTS(nsOutlookImport, nsIImportModule)
+
+NS_IMETHODIMP nsOutlookImport::GetName(char16_t** name) {
+ NS_ASSERTION(name != nullptr, "null ptr");
+ if (!name) return NS_ERROR_NULL_POINTER;
+
+ *name = nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_NAME);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookImport::GetDescription(char16_t** name) {
+ NS_ASSERTION(name != nullptr, "null ptr");
+ if (!name) return NS_ERROR_NULL_POINTER;
+
+ *name = nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_DESCRIPTION);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookImport::GetSupports(char** supports) {
+ NS_ASSERTION(supports != nullptr, "null ptr");
+ if (!supports) return NS_ERROR_NULL_POINTER;
+
+ *supports = strdup(kOutlookSupportsString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookImport::GetSupportsUpgrade(bool* pUpgrade) {
+ NS_ASSERTION(pUpgrade != nullptr, "null ptr");
+ if (!pUpgrade) return NS_ERROR_NULL_POINTER;
+
+ *pUpgrade = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookImport::GetImportInterface(const char* pImportType,
+ nsISupports** ppInterface) {
+ NS_ASSERTION(pImportType != nullptr, "null ptr");
+ if (!pImportType) return NS_ERROR_NULL_POINTER;
+ NS_ASSERTION(ppInterface != nullptr, "null ptr");
+ if (!ppInterface) return NS_ERROR_NULL_POINTER;
+
+ *ppInterface = nullptr;
+ nsresult rv;
+ if (!strcmp(pImportType, "mail")) {
+ // create the nsIImportMail interface and return it!
+ nsCOMPtr<nsIImportMail> pMail;
+ nsCOMPtr<nsIImportGeneric> pGeneric;
+ rv = ImportOutlookMailImpl::Create(getter_AddRefs(pMail));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = impSvc->CreateNewGenericMail(getter_AddRefs(pGeneric));
+ if (NS_SUCCEEDED(rv)) {
+ pGeneric->SetData("mailInterface", pMail);
+ nsString name;
+ nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_NAME, name);
+ nsCOMPtr<nsISupportsString> nameString(
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nameString->SetData(name);
+ pGeneric->SetData("name", nameString);
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pGeneric));
+ pInterface.forget(ppInterface);
+ }
+ }
+ }
+ }
+ return rv;
+ }
+
+ if (!strcmp(pImportType, "addressbook")) {
+ // create the nsIImportAddressBook interface and return it!
+ nsCOMPtr<nsIImportAddressBooks> pAddress;
+ nsCOMPtr<nsIImportGeneric> pGeneric;
+ rv = ImportOutlookAddressImpl::Create(getter_AddRefs(pAddress));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = impSvc->CreateNewGenericAddressBooks(getter_AddRefs(pGeneric));
+ if (NS_SUCCEEDED(rv)) {
+ pGeneric->SetData("addressInterface", pAddress);
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pGeneric));
+ pInterface.forget(ppInterface);
+ }
+ }
+ }
+ return rv;
+ }
+
+ if (!strcmp(pImportType, "settings")) {
+ nsCOMPtr<nsIImportSettings> pSettings;
+ rv = nsOutlookSettings::Create(getter_AddRefs(pSettings));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pSettings));
+ pInterface.forget(ppInterface);
+ }
+ return rv;
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+nsresult ImportOutlookMailImpl::Create(nsIImportMail** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new ImportOutlookMailImpl());
+ return NS_OK;
+}
+
+ImportOutlookMailImpl::ImportOutlookMailImpl() {
+ nsOutlookCompose::CreateIdentity();
+}
+
+ImportOutlookMailImpl::~ImportOutlookMailImpl() {
+ nsOutlookCompose::ReleaseIdentity();
+}
+
+NS_IMPL_ISUPPORTS(ImportOutlookMailImpl, nsIImportMail)
+
+NS_IMETHODIMP ImportOutlookMailImpl::GetDefaultLocation(nsIFile** ppLoc,
+ bool* found,
+ bool* userVerify) {
+ NS_ASSERTION(ppLoc != nullptr, "null ptr");
+ NS_ASSERTION(found != nullptr, "null ptr");
+ NS_ASSERTION(userVerify != nullptr, "null ptr");
+ if (!ppLoc || !found || !userVerify) return NS_ERROR_NULL_POINTER;
+
+ *found = false;
+ *ppLoc = nullptr;
+ *userVerify = false;
+ // We need to verify here that we can get the mail, if true then
+ // return a dummy location, otherwise return no location
+ CMapiApi mapi;
+ if (!mapi.Initialize()) return NS_OK;
+ if (!mapi.LogOn()) return NS_OK;
+
+ CMapiFolderList store;
+ if (!mapi.IterateStores(store)) return NS_OK;
+
+ if (store.GetSize() == 0) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> resultFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ *found = true;
+ resultFile.forget(ppLoc);
+ *userVerify = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportOutlookMailImpl::FindMailboxes(
+ nsIFile* pLoc, nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes) {
+ NS_ASSERTION(pLoc != nullptr, "null ptr");
+ if (!pLoc) return NS_ERROR_NULL_POINTER;
+ return m_mail.GetMailFolders(boxes);
+}
+
+void ImportOutlookMailImpl::AddLinebreak(nsString* pStream) {
+ if (pStream) pStream->Append(char16_t('\n'));
+}
+
+void ImportOutlookMailImpl::ReportSuccess(nsString& name, int32_t count,
+ nsString* pStream) {
+ if (!pStream) return;
+ // load the success string
+ char16_t* pFmt =
+ nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_MAILBOX_SUCCESS);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get(), count);
+ pStream->Append(pText);
+ nsOutlookStringBundle::FreeString(pFmt);
+ AddLinebreak(pStream);
+}
+
+void ImportOutlookMailImpl::ReportError(int32_t errorNum, nsString& name,
+ nsString* pStream) {
+ if (!pStream) return;
+ // load the error string
+ char16_t* pFmt = nsOutlookStringBundle::GetStringByID(errorNum);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ nsOutlookStringBundle::FreeString(pFmt);
+ AddLinebreak(pStream);
+}
+
+void ImportOutlookMailImpl::SetLogs(nsString& success, nsString& error,
+ char16_t** pError, char16_t** pSuccess) {
+ if (pError) *pError = ToNewUnicode(error);
+ if (pSuccess) *pSuccess = ToNewUnicode(success);
+}
+
+NS_IMETHODIMP
+ImportOutlookMailImpl::ImportMailbox(nsIImportMailboxDescriptor* pSource,
+ nsIMsgFolder* dstFolder,
+ char16_t** pErrorLog,
+ char16_t** pSuccessLog, bool* fatalError) {
+ NS_ENSURE_ARG_POINTER(pSource);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+ NS_ENSURE_ARG_POINTER(fatalError);
+
+ nsString success;
+ nsString error;
+ bool abort = false;
+ nsString name;
+ char16_t* pName;
+ if (NS_SUCCEEDED(pSource->GetDisplayName(&pName))) {
+ name = pName;
+ free(pName);
+ }
+
+ uint32_t mailSize = 0;
+ pSource->GetSize(&mailSize);
+ if (mailSize == 0) {
+ ReportSuccess(name, 0, &success);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_OK;
+ }
+
+ uint32_t index = 0;
+ pSource->GetIdentifier(&index);
+ int32_t msgCount = 0;
+ nsresult rv = NS_OK;
+
+ m_bytesDone = 0;
+
+ rv = m_mail.ImportMailbox(&m_bytesDone, &abort, (int32_t)index, name.get(),
+ dstFolder, &msgCount);
+
+ if (NS_SUCCEEDED(rv))
+ ReportSuccess(name, msgCount, &success);
+ else
+ ReportError(OUTLOOKIMPORT_MAILBOX_CONVERTERROR, name, &error);
+
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+
+ return rv;
+}
+
+NS_IMETHODIMP ImportOutlookMailImpl::GetImportProgress(uint32_t* pDoneSoFar) {
+ NS_ASSERTION(pDoneSoFar != nullptr, "null ptr");
+ if (!pDoneSoFar) return NS_ERROR_NULL_POINTER;
+
+ *pDoneSoFar = m_bytesDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportOutlookMailImpl::TranslateFolderName(
+ const nsAString& aFolderName, nsAString& _retval) {
+ if (aFolderName.LowerCaseEqualsLiteral("deleted items"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestTrashFolderName);
+ else if (aFolderName.LowerCaseEqualsLiteral("sent items"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestSentFolderName);
+ else if (aFolderName.LowerCaseEqualsLiteral("outbox"))
+ _retval = NS_LITERAL_STRING_FROM_CSTRING(kDestUnsentMessagesFolderName);
+ else
+ _retval = aFolderName;
+ return NS_OK;
+}
+
+nsresult ImportOutlookAddressImpl::Create(nsIImportAddressBooks** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new ImportOutlookAddressImpl());
+ return NS_OK;
+}
+
+ImportOutlookAddressImpl::ImportOutlookAddressImpl() {
+ m_msgCount = 0;
+ m_msgTotal = 0;
+}
+
+ImportOutlookAddressImpl::~ImportOutlookAddressImpl() {}
+
+NS_IMPL_ISUPPORTS(ImportOutlookAddressImpl, nsIImportAddressBooks)
+
+NS_IMETHODIMP ImportOutlookAddressImpl::GetAutoFind(char16_t** description,
+ bool* _retval) {
+ NS_ASSERTION(description != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!description || !_retval) return NS_ERROR_NULL_POINTER;
+
+ *_retval = true;
+ nsString str;
+ nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_ADDRNAME, str);
+ *description = ToNewUnicode(str);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportOutlookAddressImpl::FindAddressBooks(
+ nsIFile* location, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ return m_address.GetAddressBooks(books);
+}
+
+NS_IMETHODIMP ImportOutlookAddressImpl::ImportAddressBook(
+ nsIImportABDescriptor* source, nsIAbDirectory* destination,
+ nsIImportFieldMap* fieldMap, nsISupports* aSupportService,
+ char16_t** pErrorLog, char16_t** pSuccessLog, bool* fatalError) {
+ m_msgCount = 0;
+ m_msgTotal = 0;
+ NS_ASSERTION(source != nullptr, "null ptr");
+ NS_ASSERTION(destination != nullptr, "null ptr");
+ NS_ASSERTION(fatalError != nullptr, "null ptr");
+
+ nsString success;
+ nsString error;
+ if (!source || !destination || !fatalError) {
+ IMPORT_LOG0("*** Bad param passed to outlook address import\n");
+ nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_ADDRESS_BADPARAM, error);
+ if (fatalError) *fatalError = true;
+ ImportOutlookMailImpl::SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsString name;
+ source->GetPreferredName(name);
+
+ uint32_t id;
+ if (NS_FAILED(source->GetIdentifier(&id))) {
+ ImportOutlookMailImpl::ReportError(OUTLOOKIMPORT_ADDRESS_BADSOURCEFILE,
+ name, &error);
+ ImportOutlookMailImpl::SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = NS_OK;
+ rv = m_address.ImportAddresses(&m_msgCount, &m_msgTotal, name.get(), id,
+ destination, error);
+ if (NS_SUCCEEDED(rv) && error.IsEmpty())
+ ReportSuccess(name, &success);
+ else
+ ImportOutlookMailImpl::ReportError(OUTLOOKIMPORT_ADDRESS_CONVERTERROR, name,
+ &error);
+
+ ImportOutlookMailImpl::SetLogs(success, error, pErrorLog, pSuccessLog);
+ IMPORT_LOG0("*** Returning from outlook address import\n");
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportOutlookAddressImpl::GetImportProgress(uint32_t* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!_retval) return NS_ERROR_NULL_POINTER;
+
+ uint32_t result = m_msgCount;
+ if (m_msgTotal) {
+ result *= 100;
+ result /= m_msgTotal;
+ } else
+ result = 0;
+
+ if (result > 100) result = 100;
+
+ *_retval = result;
+
+ return NS_OK;
+}
+
+void ImportOutlookAddressImpl::ReportSuccess(nsString& name,
+ nsString* pStream) {
+ if (!pStream) return;
+ // load the success string
+ char16_t* pFmt =
+ nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_ADDRESS_SUCCESS);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ nsOutlookStringBundle::FreeString(pFmt);
+ ImportOutlookMailImpl::AddLinebreak(pStream);
+}
diff --git a/comm/mailnews/import/src/nsOutlookImport.h b/comm/mailnews/import/src/nsOutlookImport.h
new file mode 100644
index 0000000000..fb94b31502
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookImport.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsOutlookImport_h___
+#define nsOutlookImport_h___
+
+#include "nsIImportModule.h"
+#include "nsCOMPtr.h"
+
+#define NS_OUTLOOKIMPORT_CID \
+ { /* 1DB469A0-8B00-11d3-A206-00A0CC26DA63 */ \
+ 0x1db469a0, 0x8b00, 0x11d3, { \
+ 0xa2, 0x6, 0x0, 0xa0, 0xcc, 0x26, 0xda, 0x63 \
+ } \
+ }
+
+#define kOutlookSupportsString \
+ NS_IMPORT_MAIL_STR "," NS_IMPORT_ADDRESS_STR "," NS_IMPORT_SETTINGS_STR
+
+class nsOutlookImport : public nsIImportModule {
+ public:
+ nsOutlookImport();
+
+ NS_DECL_ISUPPORTS
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImportModule interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ NS_DECL_NSIIMPORTMODULE
+
+ protected:
+ virtual ~nsOutlookImport();
+};
+
+#endif /* nsOutlookImport_h___ */
diff --git a/comm/mailnews/import/src/nsOutlookMail.cpp b/comm/mailnews/import/src/nsOutlookMail.cpp
new file mode 100644
index 0000000000..3569c9096d
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookMail.cpp
@@ -0,0 +1,830 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ Outlook mail import
+*/
+
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsMsgUtils.h"
+#include "nsIImportService.h"
+#include "nsIImportFieldMap.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsIImportABDescriptor.h"
+#include "nsOutlookStringBundle.h"
+#include "nsIAbCard.h"
+#include "mdb.h"
+#include "ImportDebug.h"
+#include "nsOutlookMail.h"
+#include "nsUnicharUtils.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgI18N.h"
+#include "nsNetUtil.h"
+
+/* ------------ Address book stuff ----------------- */
+typedef struct {
+ int32_t mozField;
+ int32_t multiLine;
+ ULONG mapiTag;
+} MAPIFields;
+
+/*
+ Fields in MAPI, not in Mozilla
+ PR_OFFICE_LOCATION
+ FIX - PR_BIRTHDAY - stored as PT_SYSTIME - FIX to extract for moz address book
+ birthday PR_DISPLAY_NAME_PREFIX - Mr., Mrs. Dr., etc. PR_SPOUSE_NAME PR_GENDER
+ - integer, not text FIX - PR_CONTACT_EMAIL_ADDRESSES - multiuline strings for
+ email addresses, needs parsing to get secondary email address for mozilla
+*/
+
+#define kIsMultiLine -2
+#define kNoMultiLine -1
+
+static MAPIFields gMapiFields[] = {
+ {35, kIsMultiLine, PR_BODY},
+ {6, kNoMultiLine, PR_BUSINESS_TELEPHONE_NUMBER},
+ {7, kNoMultiLine, PR_HOME_TELEPHONE_NUMBER},
+ {25, kNoMultiLine, PR_COMPANY_NAME},
+ {23, kNoMultiLine, PR_TITLE},
+ {10, kNoMultiLine, PR_CELLULAR_TELEPHONE_NUMBER},
+ {9, kNoMultiLine, PR_PAGER_TELEPHONE_NUMBER},
+ {8, kNoMultiLine, PR_BUSINESS_FAX_NUMBER},
+ {8, kNoMultiLine, PR_HOME_FAX_NUMBER},
+ {22, kNoMultiLine, PR_COUNTRY},
+ {19, kNoMultiLine, PR_LOCALITY},
+ {20, kNoMultiLine, PR_STATE_OR_PROVINCE},
+ {17, 18, PR_STREET_ADDRESS},
+ {21, kNoMultiLine, PR_POSTAL_CODE},
+ {27, kNoMultiLine, PR_PERSONAL_HOME_PAGE},
+ {26, kNoMultiLine, PR_BUSINESS_HOME_PAGE},
+ {13, kNoMultiLine, PR_HOME_ADDRESS_CITY},
+ {16, kNoMultiLine, PR_HOME_ADDRESS_COUNTRY},
+ {15, kNoMultiLine, PR_HOME_ADDRESS_POSTAL_CODE},
+ {14, kNoMultiLine, PR_HOME_ADDRESS_STATE_OR_PROVINCE},
+ {11, 12, PR_HOME_ADDRESS_STREET},
+ {24, kNoMultiLine, PR_DEPARTMENT_NAME}};
+/* ---------------------------------------------------- */
+
+#define kCopyBufferSize (16 * 1024)
+
+// The email address in Outlook Contacts doesn't have a named
+// property, we need to use this mapi name ID to access the email
+// The MAPINAMEID for email address has ulKind=MNID_ID
+// Outlook stores each email address in two IDs, 32899/32900 for Email1
+// 32915/32916 for Email2, 32931/32932 for Email3
+// Current we use OUTLOOK_EMAIL1_MAPI_ID1 for primary email
+// OUTLOOK_EMAIL2_MAPI_ID1 for secondary email
+#define OUTLOOK_EMAIL1_MAPI_ID1 32899
+#define OUTLOOK_EMAIL1_MAPI_ID2 32900
+#define OUTLOOK_EMAIL2_MAPI_ID1 32915
+#define OUTLOOK_EMAIL2_MAPI_ID2 32916
+#define OUTLOOK_EMAIL3_MAPI_ID1 32931
+#define OUTLOOK_EMAIL3_MAPI_ID2 32932
+
+nsOutlookMail::nsOutlookMail() {
+ m_gotAddresses = false;
+ m_gotFolders = false;
+ m_haveMapi = CMapiApi::LoadMapi();
+ m_lpMdb = NULL;
+}
+
+nsOutlookMail::~nsOutlookMail() {
+ // EmptyAttachments();
+}
+
+nsresult nsOutlookMail::GetMailFolders(
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes) {
+ if (!m_haveMapi) {
+ IMPORT_LOG0("GetMailFolders called before Mapi is initialized\n");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv;
+ boxes.Clear();
+
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ m_gotFolders = true;
+
+ m_folderList.ClearAll();
+
+ m_mapi.Initialize();
+ m_mapi.LogOn();
+
+ if (m_storeList.GetSize() == 0) m_mapi.IterateStores(m_storeList);
+
+ int i = 0;
+ CMapiFolder* pFolder;
+ if (m_storeList.GetSize() > 1) {
+ while ((pFolder = m_storeList.GetItem(i))) {
+ CMapiFolder* pItem = new CMapiFolder(pFolder);
+ pItem->SetDepth(1);
+ m_folderList.AddItem(pItem);
+ if (!m_mapi.GetStoreFolders(pItem->GetCBEntryID(), pItem->GetEntryID(),
+ m_folderList, 2)) {
+ IMPORT_LOG1("GetStoreFolders for index %d failed.\n", i);
+ }
+ i++;
+ }
+ } else {
+ if ((pFolder = m_storeList.GetItem(i))) {
+ if (!m_mapi.GetStoreFolders(pFolder->GetCBEntryID(),
+ pFolder->GetEntryID(), m_folderList, 1)) {
+ IMPORT_LOG1("GetStoreFolders for index %d failed.\n", i);
+ }
+ }
+ }
+
+ // Create the mailbox descriptors for the list of folders
+ nsCOMPtr<nsIImportMailboxDescriptor> pID;
+ nsString name;
+ nsString uniName;
+
+ for (i = 0; i < m_folderList.GetSize(); i++) {
+ pFolder = m_folderList.GetItem(i);
+ rv = impSvc->CreateNewMailboxDescriptor(getter_AddRefs(pID));
+ if (NS_SUCCEEDED(rv)) {
+ pID->SetDepth(pFolder->GetDepth());
+ pID->SetIdentifier(i);
+
+ pFolder->GetDisplayName(name);
+ pID->SetDisplayName(name.get());
+
+ pID->SetSize(1000);
+ boxes.AppendElement(pID);
+ }
+ }
+ return NS_OK;
+}
+
+bool nsOutlookMail::IsAddressBookNameUnique(nsString& name, nsString& list) {
+ nsString usedName;
+ usedName.Append('[');
+ usedName.Append(name);
+ usedName.AppendLiteral("],");
+
+ return list.Find(usedName) == -1;
+}
+
+void nsOutlookMail::MakeAddressBookNameUnique(nsString& name, nsString& list) {
+ nsString newName;
+ int idx = 1;
+
+ newName = name;
+ while (!IsAddressBookNameUnique(newName, list)) {
+ newName = name;
+ newName.Append(char16_t(' '));
+ newName.AppendInt((int32_t)idx);
+ idx++;
+ }
+
+ name = newName;
+ list.Append('[');
+ list.Append(name);
+ list.AppendLiteral("],");
+}
+
+nsresult nsOutlookMail::GetAddressBooks(
+ nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ books.Clear();
+ if (!m_haveMapi) {
+ IMPORT_LOG0("GetAddressBooks called before Mapi is initialized\n");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv;
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ m_gotAddresses = true;
+
+ m_addressList.ClearAll();
+ m_mapi.Initialize();
+ m_mapi.LogOn();
+ if (m_storeList.GetSize() == 0) m_mapi.IterateStores(m_storeList);
+
+ int i = 0;
+ CMapiFolder* pFolder;
+ if (m_storeList.GetSize() > 1) {
+ while ((pFolder = m_storeList.GetItem(i))) {
+ CMapiFolder* pItem = new CMapiFolder(pFolder);
+ pItem->SetDepth(1);
+ m_addressList.AddItem(pItem);
+ if (!m_mapi.GetStoreAddressFolders(pItem->GetCBEntryID(),
+ pItem->GetEntryID(), m_addressList)) {
+ IMPORT_LOG1("GetStoreAddressFolders for index %d failed.\n", i);
+ }
+ i++;
+ }
+ } else {
+ if ((pFolder = m_storeList.GetItem(i))) {
+ if (!m_mapi.GetStoreAddressFolders(
+ pFolder->GetCBEntryID(), pFolder->GetEntryID(), m_addressList)) {
+ IMPORT_LOG1("GetStoreFolders for index %d failed.\n", i);
+ }
+ }
+ }
+
+ // Create the mailbox descriptors for the list of folders
+ nsCOMPtr<nsIImportABDescriptor> pID;
+ nsString name;
+ nsString list;
+
+ for (i = 0; i < m_addressList.GetSize(); i++) {
+ pFolder = m_addressList.GetItem(i);
+ if (!pFolder->IsStore()) {
+ rv = impSvc->CreateNewABDescriptor(getter_AddRefs(pID));
+ if (NS_SUCCEEDED(rv)) {
+ pID->SetIdentifier(i);
+ pFolder->GetDisplayName(name);
+ MakeAddressBookNameUnique(name, list);
+ pID->SetPreferredName(name);
+ pID->SetSize(100);
+ books.AppendElement(pID);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+void nsOutlookMail::OpenMessageStore(CMapiFolder* pNextFolder) {
+ // Open the store specified
+ if (pNextFolder->IsStore()) {
+ if (!m_mapi.OpenStore(pNextFolder->GetCBEntryID(),
+ pNextFolder->GetEntryID(), &m_lpMdb)) {
+ m_lpMdb = NULL;
+ IMPORT_LOG0("CMapiApi::OpenStore failed\n");
+ }
+
+ return;
+ }
+
+ // Check to see if we should open the one and only store
+ if (!m_lpMdb) {
+ if (m_storeList.GetSize() == 1) {
+ CMapiFolder* pFolder = m_storeList.GetItem(0);
+ if (pFolder) {
+ if (!m_mapi.OpenStore(pFolder->GetCBEntryID(), pFolder->GetEntryID(),
+ &m_lpMdb)) {
+ m_lpMdb = NULL;
+ IMPORT_LOG0("CMapiApi::OpenStore failed\n");
+ }
+ } else {
+ IMPORT_LOG0("Error retrieving the one & only message store\n");
+ }
+ } else {
+ IMPORT_LOG0(
+ "*** Error importing a folder without a valid message store\n");
+ }
+ }
+}
+
+// Roles and responsibilities:
+// nsOutlookMail
+// - Connect to Outlook
+// - Enumerate the mailboxes
+// - Iterate the mailboxes
+// - For each mail, create one nsOutlookCompose object
+// - For each mail, create one CMapiMessage object
+//
+// nsOutlookCompose
+// - Establish a TB session
+// - Connect to all required services
+// - Perform the composition of the RC822 document from the data gathered by
+// CMapiMessage
+// - Save the composed message to the TB mailbox
+// - Ensure the proper cleanup
+//
+// CMapiMessage
+// - Encapsulate the MAPI message interface
+// - Gather the information required to (re)compose the message
+
+ImportMailboxRunnable::ImportMailboxRunnable(
+ uint32_t* pDoneSoFar, bool* pAbort, int32_t index, const char16_t* pName,
+ nsIMsgFolder* dstFolder, int32_t* pMsgCount, nsOutlookMail* aCaller)
+ : mozilla::Runnable("ImportMailboxRunnable"),
+ mResult(NS_OK),
+ mCaller(aCaller),
+ mDoneSoFar(pDoneSoFar),
+ mAbort(pAbort),
+ mIndex(index),
+ mName(pName),
+ mDstFolder(dstFolder),
+ mMsgCount(pMsgCount) {}
+NS_IMETHODIMP ImportMailboxRunnable::Run() {
+ if ((mIndex < 0) || (mIndex >= mCaller->m_folderList.GetSize())) {
+ IMPORT_LOG0("*** Bad mailbox identifier, unable to import\n");
+ *mAbort = true;
+ mResult = NS_ERROR_FAILURE;
+ return NS_OK; // Sync runnable must return OK.
+ }
+
+ int32_t dummyMsgCount = 0;
+ if (mMsgCount)
+ *mMsgCount = 0;
+ else
+ mMsgCount = &dummyMsgCount;
+
+ CMapiFolder* pFolder = mCaller->m_folderList.GetItem(mIndex);
+ mCaller->OpenMessageStore(pFolder);
+ if (!mCaller->m_lpMdb) {
+ IMPORT_LOG1("*** Unable to obtain mapi message store for mailbox: %S\n",
+ (const wchar_t*)mName);
+ mResult = NS_ERROR_FAILURE;
+ return NS_OK; // Sync runnable must return OK.
+ }
+
+ if (pFolder->IsStore()) return NS_OK;
+
+ // now what?
+ CMapiFolderContents contents(mCaller->m_lpMdb, pFolder->GetCBEntryID(),
+ pFolder->GetEntryID());
+
+ BOOL done = FALSE;
+ ULONG cbEid;
+ LPENTRYID lpEid;
+ ULONG oType;
+ LPMESSAGE lpMsg = nullptr;
+ ULONG totalCount;
+ double doneCalc;
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = mDstFolder->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (!done) {
+ if (!contents.GetNext(&cbEid, &lpEid, &oType, &done)) {
+ IMPORT_LOG1("*** Error iterating mailbox: %S\n", (const wchar_t*)mName);
+ mResult = NS_ERROR_FAILURE;
+ return NS_OK; // Sync runnable must return OK.
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgStore->GetNewMsgOutputStream(mDstFolder, getter_AddRefs(msgHdr),
+ getter_AddRefs(outputStream));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG1("*** Error getting nsIOutputStream of mailbox: %S\n",
+ (const wchar_t*)mName);
+ mResult = rv;
+ return NS_OK; // Sync runnable must return OK.
+ }
+ totalCount = contents.GetCount();
+ doneCalc = *mMsgCount;
+ doneCalc /= totalCount;
+ doneCalc *= 1000;
+ if (mDoneSoFar) {
+ *mDoneSoFar = (uint32_t)doneCalc;
+ if (*mDoneSoFar > 1000) *mDoneSoFar = 1000;
+ }
+
+ if (!done && (oType == MAPI_MESSAGE)) {
+ if (!mCaller->m_mapi.OpenMdbEntry(mCaller->m_lpMdb, cbEid, lpEid,
+ (LPUNKNOWN*)&lpMsg)) {
+ IMPORT_LOG1("*** Error opening messages in mailbox: %S\n",
+ (const wchar_t*)mName);
+ mResult = NS_ERROR_FAILURE;
+ return NS_OK; // Sync runnable must return OK.
+ }
+
+ // See if it's a drafts folder. Outlook doesn't allow drafts
+ // folder to be configured so it's ok to hard code it here.
+ nsAutoString folderName(mName);
+ nsMsgDeliverMode mode = nsIMsgSend::nsMsgDeliverNow;
+ mode = nsIMsgSend::nsMsgSaveAsDraft;
+ if (folderName.LowerCaseEqualsLiteral("drafts"))
+ mode = nsIMsgSend::nsMsgSaveAsDraft;
+
+ rv = ImportMessage(lpMsg, outputStream, mode);
+ if (NS_SUCCEEDED(rv)) { // No errors & really imported
+ (*mMsgCount)++;
+ msgStore->FinishNewMessage(outputStream, msgHdr);
+ outputStream = nullptr;
+ } else {
+ IMPORT_LOG1("*** Error reading message from mailbox: %S\n",
+ (const wchar_t*)mName);
+ msgStore->DiscardNewMessage(outputStream, msgHdr);
+ outputStream = nullptr;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult ProxyImportMailbox(uint32_t* pDoneSoFar, bool* pAbort, int32_t index,
+ const char16_t* pName, nsIMsgFolder* dstFolder,
+ int32_t* pMsgCount, nsOutlookMail* aCaller) {
+ RefPtr<ImportMailboxRunnable> importMailbox = new ImportMailboxRunnable(
+ pDoneSoFar, pAbort, index, pName, dstFolder, pMsgCount, aCaller);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxyImportMailbox"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(importMailbox));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return importMailbox->mResult;
+}
+
+nsresult nsOutlookMail::ImportMailbox(uint32_t* pDoneSoFar, bool* pAbort,
+ int32_t index, const char16_t* pName,
+ nsIMsgFolder* dstFolder,
+ int32_t* pMsgCount) {
+ return ProxyImportMailbox(pDoneSoFar, pAbort, index, pName, dstFolder,
+ pMsgCount, this);
+}
+
+nsresult ImportMailboxRunnable::ImportMessage(LPMESSAGE lpMsg,
+ nsIOutputStream* pDest,
+ nsMsgDeliverMode mode) {
+ CMapiMessage msg(lpMsg);
+ // If we wanted to skip messages that were downloaded in header only mode, we
+ // would return NS_ERROR_FAILURE if !msg.FullMessageDownloaded. However, we
+ // don't do this because it may cause seemingly wrong import results.
+ // A user will get less mails in his imported folder than were in the original
+ // folder, and this may make user feel like TB import is bad. In reality, the
+ // skipped messages are those that have not been downloaded yet, because they
+ // were downloaded in the "headers-only" mode. This is different from the case
+ // when the message is downloaded completely, but consists only of headers -
+ // in this case the message will be imported anyway.
+
+ if (!msg.ValidState()) return NS_ERROR_FAILURE;
+
+ // I have to create a composer for each message, since it turns out that if we
+ // create one composer for several messages, the Send Proxy object that is
+ // shared between those messages isn't reset properly (at least in the current
+ // implementation), which leads to crash. If there's a proper way to
+ // reinitialize the Send Proxy object, then we could slightly optimize the
+ // send process.
+ nsOutlookCompose compose;
+ nsresult rv = compose.ProcessMessage(mode, msg, pDest);
+
+ // Just for YUCKS, let's try an extra endline
+ nsOutlookMail::WriteData(pDest, "\x0D\x0A", 2);
+
+ return rv;
+}
+
+BOOL nsOutlookMail::WriteData(nsIOutputStream* pDest, const char* pData,
+ uint32_t len) {
+ uint32_t written;
+ nsresult rv = pDest->Write(pData, len, &written);
+ return NS_SUCCEEDED(rv) && written == len;
+}
+
+nsresult nsOutlookMail::ImportAddresses(uint32_t* pCount, uint32_t* pTotal,
+ const char16_t* pName, uint32_t id,
+ nsIAbDirectory* pDirectory,
+ nsString& errors) {
+ if (id >= (uint32_t)(m_addressList.GetSize())) {
+ IMPORT_LOG0("*** Bad address identifier, unable to import\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t dummyCount = 0;
+ if (pCount)
+ *pCount = 0;
+ else
+ pCount = &dummyCount;
+
+ CMapiFolder* pFolder;
+ if (id > 0) {
+ int32_t idx = (int32_t)id;
+ idx--;
+ while (idx >= 0) {
+ pFolder = m_addressList.GetItem(idx);
+ if (pFolder->IsStore()) {
+ OpenMessageStore(pFolder);
+ break;
+ }
+ idx--;
+ }
+ }
+
+ pFolder = m_addressList.GetItem(id);
+ OpenMessageStore(pFolder);
+ if (!m_lpMdb) {
+ IMPORT_LOG1(
+ "*** Unable to obtain mapi message store for address book: %S\n",
+ (const wchar_t*)pName);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (pFolder->IsStore()) return NS_OK;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIImportFieldMap> pFieldMap;
+
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = impSvc->CreateNewFieldMap(getter_AddRefs(pFieldMap));
+ }
+
+ CMapiFolderContents contents(m_lpMdb, pFolder->GetCBEntryID(),
+ pFolder->GetEntryID());
+
+ BOOL done = FALSE;
+ ULONG cbEid;
+ LPENTRYID lpEid;
+ ULONG oType;
+ LPMESSAGE lpMsg;
+ nsCString type;
+ LPSPropValue pVal;
+ nsString subject;
+
+ while (!done) {
+ (*pCount)++;
+
+ if (!contents.GetNext(&cbEid, &lpEid, &oType, &done)) {
+ IMPORT_LOG1("*** Error iterating address book: %S\n",
+ (const wchar_t*)pName);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (pTotal && (*pTotal == 0)) *pTotal = contents.GetCount();
+
+ if (!done && (oType == MAPI_MESSAGE)) {
+ if (!m_mapi.OpenMdbEntry(m_lpMdb, cbEid, lpEid, (LPUNKNOWN*)&lpMsg)) {
+ IMPORT_LOG1("*** Error opening messages in mailbox: %S\n",
+ (const wchar_t*)pName);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the PR_MESSAGE_CLASS attribute,
+ // ensure that it is IPM.Contact
+ pVal = m_mapi.GetMapiProperty(lpMsg, PR_MESSAGE_CLASS);
+ if (pVal) {
+ type.Truncate();
+ m_mapi.GetStringFromProp(pVal, type);
+ if (type.EqualsLiteral("IPM.Contact")) {
+ // This is a contact, add it to the address book!
+ subject.Truncate();
+ pVal = m_mapi.GetMapiProperty(lpMsg, PR_SUBJECT);
+ if (pVal) m_mapi.GetStringFromProp(pVal, subject);
+
+ nsCOMPtr<nsIAbCard> newCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ if (newCard) {
+ if (BuildCard(subject.get(), pDirectory, newCard, lpMsg,
+ pFieldMap)) {
+ nsIAbCard* outCard;
+ pDirectory->AddCard(newCard, &outCard);
+ }
+ }
+ } else if (type.EqualsLiteral("IPM.DistList")) {
+ // This is a list/group, add it to the address book!
+ subject.Truncate();
+ pVal = m_mapi.GetMapiProperty(lpMsg, PR_SUBJECT);
+ if (pVal) m_mapi.GetStringFromProp(pVal, subject);
+ CreateList(subject, pDirectory, lpMsg, pFieldMap);
+ }
+ }
+
+ lpMsg->Release();
+ }
+ }
+
+ return rv;
+}
+nsresult nsOutlookMail::CreateList(const nsString& pName,
+ nsIAbDirectory* pDirectory,
+ LPMAPIPROP pUserList,
+ nsIImportFieldMap* pFieldMap) {
+ // If no name provided then we're done.
+ if (pName.IsEmpty()) return NS_OK;
+
+ nsresult rv = NS_ERROR_FAILURE;
+ // Make sure we have db to work with.
+ if (!pDirectory) return rv;
+
+ nsCOMPtr<nsIAbDirectory> newList =
+ do_CreateInstance("@mozilla.org/addressbook/directoryproperty;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = newList->SetDirName(pName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ HRESULT hr;
+ LPSPropValue value = NULL;
+ ULONG valueCount = 0;
+
+ LPSPropTagArray properties = NULL;
+ m_mapi.MAPIAllocateBuffer(CbNewSPropTagArray(1), (void**)&properties);
+ properties->cValues = 1;
+ properties->aulPropTag[0] = m_mapi.GetEmailPropertyTag(pUserList, 0x8054);
+ hr = pUserList->GetProps(properties, 0, &valueCount, &value);
+ m_mapi.MAPIFreeBuffer(properties);
+ if (HR_FAILED(hr)) return NS_ERROR_FAILURE;
+ if (!value) return NS_ERROR_NOT_AVAILABLE;
+ // XXX from here out, value must be freed with MAPIFreeBuffer
+
+ SBinaryArray* sa = (SBinaryArray*)&value->Value.bin;
+ if (!sa || !sa->lpbin) {
+ m_mapi.MAPIFreeBuffer(value);
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ LPENTRYID lpEid;
+ ULONG cbEid;
+ ULONG idx;
+ LPMESSAGE lpMsg;
+ nsCString type;
+ LPSPropValue pVal;
+ nsString subject;
+ ULONG total;
+
+ total = sa->cValues;
+ for (idx = 0; idx < total; idx++) {
+ lpEid = (LPENTRYID)sa->lpbin[idx].lpb;
+ cbEid = sa->lpbin[idx].cb;
+
+ if (!m_mapi.OpenEntry(cbEid, lpEid, (LPUNKNOWN*)&lpMsg)) {
+ IMPORT_LOG1("*** Error opening messages in mailbox: %S\n",
+ static_cast<const wchar_t*>(pName.get()));
+ m_mapi.MAPIFreeBuffer(value);
+ return NS_ERROR_FAILURE;
+ }
+ // This is a contact, add it to the address book!
+ subject.Truncate();
+ pVal = m_mapi.GetMapiProperty(lpMsg, PR_SUBJECT);
+ if (pVal) m_mapi.GetStringFromProp(pVal, subject);
+
+ nsCOMPtr<nsIAbCard> newCard =
+ do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv);
+ if (newCard) {
+ if (BuildCard(subject.get(), pDirectory, newCard, lpMsg, pFieldMap)) {
+ nsIAbCard* outCard;
+ newList->AddCard(newCard, &outCard);
+ }
+ }
+ }
+ m_mapi.MAPIFreeBuffer(value);
+
+ nsIAbDirectory* outList;
+ rv = pDirectory->AddMailList(newList, &outList);
+ return rv;
+}
+
+void nsOutlookMail::SanitizeValue(nsString& val) {
+ val.ReplaceSubstring(u"\r\n"_ns, u", "_ns);
+ val.ReplaceChar(u"\r\n", u',');
+}
+
+void nsOutlookMail::SplitString(nsString& val1, nsString& val2) {
+ // Find the last line if there is more than one!
+ int32_t idx = val1.RFind(u"\x0D\x0A");
+ int32_t cnt = 2;
+ if (idx == -1) {
+ cnt = 1;
+ idx = val1.RFindChar(13);
+ }
+ if (idx == -1) idx = val1.RFindChar(10);
+ if (idx != -1) {
+ val2 = Substring(val1, idx + cnt);
+ val1.SetLength(idx);
+ SanitizeValue(val1);
+ }
+}
+
+bool nsOutlookMail::BuildCard(const char16_t* pName, nsIAbDirectory* pDirectory,
+ nsIAbCard* newCard, LPMAPIPROP pUser,
+ nsIImportFieldMap* pFieldMap) {
+ nsString lastName;
+ nsString firstName;
+ nsString eMail;
+ nsString nickName;
+ nsString middleName;
+ nsString secondEMail;
+ ULONG emailTag;
+
+ LPSPropValue pProp = m_mapi.GetMapiProperty(pUser, PR_EMAIL_ADDRESS);
+ if (!pProp) {
+ emailTag = m_mapi.GetEmailPropertyTag(pUser, OUTLOOK_EMAIL1_MAPI_ID1);
+ if (emailTag) {
+ pProp = m_mapi.GetMapiProperty(pUser, emailTag);
+ }
+ }
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, eMail);
+ SanitizeValue(eMail);
+ }
+
+ // for secondary email
+ emailTag = m_mapi.GetEmailPropertyTag(pUser, OUTLOOK_EMAIL2_MAPI_ID1);
+ if (emailTag) {
+ pProp = m_mapi.GetMapiProperty(pUser, emailTag);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, secondEMail);
+ SanitizeValue(secondEMail);
+ }
+ }
+
+ pProp = m_mapi.GetMapiProperty(pUser, PR_GIVEN_NAME);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, firstName);
+ SanitizeValue(firstName);
+ }
+ pProp = m_mapi.GetMapiProperty(pUser, PR_SURNAME);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, lastName);
+ SanitizeValue(lastName);
+ }
+ pProp = m_mapi.GetMapiProperty(pUser, PR_MIDDLE_NAME);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, middleName);
+ SanitizeValue(middleName);
+ }
+ pProp = m_mapi.GetMapiProperty(pUser, PR_NICKNAME);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, nickName);
+ SanitizeValue(nickName);
+ }
+ if (firstName.IsEmpty() && lastName.IsEmpty()) {
+ firstName = pName;
+ }
+
+ nsString displayName;
+ pProp = m_mapi.GetMapiProperty(pUser, PR_DISPLAY_NAME);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, displayName);
+ SanitizeValue(displayName);
+ }
+ if (displayName.IsEmpty()) {
+ if (firstName.IsEmpty())
+ displayName = pName;
+ else {
+ displayName = firstName;
+ if (!middleName.IsEmpty()) {
+ displayName.Append(char16_t(' '));
+ displayName.Append(middleName);
+ }
+ if (!lastName.IsEmpty()) {
+ displayName.Append(char16_t(' '));
+ displayName.Append(lastName);
+ }
+ }
+ }
+
+ // We now have the required fields
+ // write them out followed by any optional fields!
+ if (!displayName.IsEmpty()) {
+ newCard->SetDisplayName(displayName);
+ }
+ if (!firstName.IsEmpty()) {
+ newCard->SetFirstName(firstName);
+ }
+ if (!lastName.IsEmpty()) {
+ newCard->SetLastName(lastName);
+ }
+ if (!nickName.IsEmpty()) {
+ newCard->SetPropertyAsAString(kNicknameProperty, nickName);
+ }
+ if (!eMail.IsEmpty()) {
+ newCard->SetPrimaryEmail(eMail);
+ }
+ if (!secondEMail.IsEmpty()) {
+ newCard->SetPropertyAsAString(k2ndEmailProperty, secondEMail);
+ }
+
+ // Do all of the extra fields!
+
+ nsString value;
+ nsString line2;
+
+ if (pFieldMap) {
+ int max = sizeof(gMapiFields) / sizeof(MAPIFields);
+ for (int i = 0; i < max; i++) {
+ pProp = m_mapi.GetMapiProperty(pUser, gMapiFields[i].mapiTag);
+ if (pProp) {
+ m_mapi.GetStringFromProp(pProp, value);
+ if (!value.IsEmpty()) {
+ if (gMapiFields[i].multiLine == kNoMultiLine) {
+ SanitizeValue(value);
+ pFieldMap->SetFieldValue(pDirectory, newCard,
+ gMapiFields[i].mozField, value);
+ } else if (gMapiFields[i].multiLine == kIsMultiLine) {
+ pFieldMap->SetFieldValue(pDirectory, newCard,
+ gMapiFields[i].mozField, value);
+ } else {
+ line2.Truncate();
+ SplitString(value, line2);
+ if (!value.IsEmpty())
+ pFieldMap->SetFieldValue(pDirectory, newCard,
+ gMapiFields[i].mozField, value);
+ if (!line2.IsEmpty())
+ pFieldMap->SetFieldValue(pDirectory, newCard,
+ gMapiFields[i].multiLine, line2);
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/import/src/nsOutlookMail.h b/comm/mailnews/import/src/nsOutlookMail.h
new file mode 100644
index 0000000000..8f28d6425d
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookMail.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsOutlookMail_h___
+#define nsOutlookMail_h___
+
+#include "nsIArray.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsOutlookCompose.h"
+#include "nsIFile.h"
+#include "MapiApi.h"
+#include "MapiMessage.h"
+#include "nsIAbDirectory.h"
+#include "nsThreadUtils.h"
+
+class nsIImportFieldMap;
+
+class nsOutlookMail {
+ public:
+ nsOutlookMail();
+ ~nsOutlookMail();
+
+ nsresult GetMailFolders(nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes);
+ nsresult GetAddressBooks(nsTArray<RefPtr<nsIImportABDescriptor>>& books);
+ nsresult ImportMailbox(uint32_t* pDoneSoFar, bool* pAbort, int32_t index,
+ const char16_t* pName, nsIMsgFolder* pDest,
+ int32_t* pMsgCount);
+ nsresult ImportAddresses(uint32_t* pCount, uint32_t* pTotal,
+ const char16_t* pName, uint32_t id,
+ nsIAbDirectory* pDirectory, nsString& errors);
+ void OpenMessageStore(CMapiFolder* pNextFolder);
+ static BOOL WriteData(nsIOutputStream* pDest, const char* pData,
+ uint32_t len);
+
+ private:
+ bool IsAddressBookNameUnique(nsString& name, nsString& list);
+ void MakeAddressBookNameUnique(nsString& name, nsString& list);
+ void SanitizeValue(nsString& val);
+ void SplitString(nsString& val1, nsString& val2);
+ bool BuildCard(const char16_t* pName, nsIAbDirectory* pDirectory,
+ nsIAbCard* newCard, LPMAPIPROP pUser,
+ nsIImportFieldMap* pFieldMap);
+ nsresult CreateList(const nsString& pName, nsIAbDirectory* pDirectory,
+ LPMAPIPROP pUserList, nsIImportFieldMap* pFieldMap);
+
+ private:
+ bool m_gotFolders;
+ bool m_gotAddresses;
+ bool m_haveMapi;
+ CMapiFolderList m_addressList;
+ CMapiFolderList m_storeList;
+
+ public:
+ // Needed for the proxy class.
+ CMapiApi m_mapi;
+ CMapiFolderList m_folderList;
+ LPMDB m_lpMdb;
+};
+
+class ImportMailboxRunnable : public mozilla::Runnable {
+ public:
+ ImportMailboxRunnable(uint32_t* pDoneSoFar, bool* pAbort, int32_t index,
+ const char16_t* pName, nsIMsgFolder* dstFolder,
+ int32_t* pMsgCount, nsOutlookMail* aCaller);
+ NS_DECL_NSIRUNNABLE
+ static nsresult ImportMessage(LPMESSAGE lpMsg, nsIOutputStream* pDest,
+ nsMsgDeliverMode mode);
+ nsresult mResult;
+
+ private:
+ nsOutlookMail* mCaller;
+ uint32_t* mDoneSoFar;
+ bool* mAbort;
+ int32_t mIndex;
+ const char16_t* mName;
+ nsCOMPtr<nsIFile> mMessageFile;
+ nsCOMPtr<nsIMsgFolder> mDstFolder;
+ int32_t* mMsgCount;
+};
+
+#endif /* nsOutlookMail_h___ */
diff --git a/comm/mailnews/import/src/nsOutlookSettings.cpp b/comm/mailnews/import/src/nsOutlookSettings.cpp
new file mode 100644
index 0000000000..2e202a5600
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookSettings.cpp
@@ -0,0 +1,500 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ Outlook (Win32) settings
+*/
+
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsMsgUtils.h"
+#include "nsOutlookImport.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgAccount.h"
+#include "nsIImportSettings.h"
+#include "nsOutlookSettings.h"
+#include "nsISmtpService.h"
+#include "nsISmtpServer.h"
+#include "nsOutlookStringBundle.h"
+#include "ImportDebug.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsMsgI18N.h"
+#include <windows.h>
+#include "nsIWindowsRegKey.h"
+#include "nsComponentManagerUtils.h"
+#include "nsNativeCharsetUtils.h"
+
+class OutlookSettings {
+ public:
+ static nsresult FindAccountsKey(nsIWindowsRegKey** aKey);
+ static nsresult QueryAccountSubKey(nsIWindowsRegKey** aKey);
+ static nsresult GetDefaultMailAccountName(nsAString& aName);
+
+ static bool DoImport(nsIMsgAccount** aAccount);
+
+ static bool DoIMAPServer(nsIMsgAccountManager* aMgr, nsIWindowsRegKey* aKey,
+ const nsString& aServerName,
+ nsIMsgAccount** aAccount);
+ static bool DoPOP3Server(nsIMsgAccountManager* aMgr, nsIWindowsRegKey* aKey,
+ const nsString& aServerName,
+ nsIMsgAccount** aAccount);
+
+ static void SetIdentities(nsIMsgAccountManager* pMgr, nsIMsgAccount* pAcc,
+ nsIWindowsRegKey* aKey);
+
+ static nsresult SetSmtpServer(nsIMsgAccountManager* aMgr, nsIMsgAccount* aAcc,
+ nsIMsgIdentity* aId, const nsString& aServer,
+ const nsString& aUser);
+ static nsresult SetSmtpServerKey(nsIMsgIdentity* aId, nsISmtpServer* aServer);
+ static nsresult GetAccountName(nsIWindowsRegKey* aKey,
+ const nsString& aDefaultName,
+ nsAString& aAccountName);
+};
+
+#define OUTLOOK2003_REGISTRY_KEY \
+ "Software\\Microsoft\\Office\\Outlook\\OMI Account Manager"
+#define OUTLOOK98_REGISTRY_KEY \
+ "Software\\Microsoft\\Office\\8.0\\Outlook\\OMI Account Manager"
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsOutlookSettings::Create(nsIImportSettings** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsOutlookSettings());
+ return NS_OK;
+}
+
+nsOutlookSettings::nsOutlookSettings() {}
+
+nsOutlookSettings::~nsOutlookSettings() {}
+
+NS_IMPL_ISUPPORTS(nsOutlookSettings, nsIImportSettings)
+
+NS_IMETHODIMP nsOutlookSettings::AutoLocate(char16_t** description,
+ nsIFile** location, bool* _retval) {
+ NS_ASSERTION(description != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!description || !_retval) return NS_ERROR_NULL_POINTER;
+
+ *description = nsOutlookStringBundle::GetStringByID(OUTLOOKIMPORT_NAME);
+ *_retval = false;
+
+ if (location) *location = nullptr;
+
+ // look for the registry key for the accounts
+ nsCOMPtr<nsIWindowsRegKey> key;
+ *_retval =
+ NS_SUCCEEDED(OutlookSettings::FindAccountsKey(getter_AddRefs(key)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookSettings::SetLocation(nsIFile* location) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOutlookSettings::Import(nsIMsgAccount** localMailAccount,
+ bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+
+ if (OutlookSettings::DoImport(localMailAccount)) {
+ *_retval = true;
+ IMPORT_LOG0("Settings import appears successful\n");
+ } else {
+ *_retval = false;
+ IMPORT_LOG0("Settings import returned FALSE\n");
+ }
+
+ return NS_OK;
+}
+
+nsresult OutlookSettings::FindAccountsKey(nsIWindowsRegKey** aKey) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> key =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ NS_LITERAL_STRING_FROM_CSTRING(OUTLOOK2003_REGISTRY_KEY),
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE |
+ nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS);
+
+ if (NS_FAILED(rv)) {
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ NS_LITERAL_STRING_FROM_CSTRING(OUTLOOK98_REGISTRY_KEY),
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE |
+ nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS);
+ }
+
+ if (NS_SUCCEEDED(rv)) key.forget(aKey);
+
+ return rv;
+}
+
+nsresult OutlookSettings::QueryAccountSubKey(nsIWindowsRegKey** aKey) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> key =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ NS_LITERAL_STRING_FROM_CSTRING(OUTLOOK2003_REGISTRY_KEY),
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE |
+ nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS);
+ if (NS_SUCCEEDED(rv)) {
+ key.forget(aKey);
+ return rv;
+ }
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ NS_LITERAL_STRING_FROM_CSTRING(OUTLOOK98_REGISTRY_KEY),
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE |
+ nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS);
+ if (NS_SUCCEEDED(rv)) {
+ key.forget(aKey);
+ return rv;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult OutlookSettings::GetDefaultMailAccountName(nsAString& aName) {
+ nsCOMPtr<nsIWindowsRegKey> key;
+ nsresult rv = QueryAccountSubKey(getter_AddRefs(key));
+ if (NS_FAILED(rv)) return rv;
+
+ return key->ReadStringValue(u"Default Mail Account"_ns, aName);
+}
+
+bool OutlookSettings::DoImport(nsIMsgAccount** aAccount) {
+ nsCOMPtr<nsIWindowsRegKey> key;
+ nsresult rv = OutlookSettings::FindAccountsKey(getter_AddRefs(key));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error finding Outlook registry account keys\n");
+ return false;
+ }
+
+ nsCOMPtr<nsIMsgAccountManager> accMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create a account manager!\n");
+ return false;
+ }
+
+ nsAutoString defMailName;
+ rv = GetDefaultMailAccountName(defMailName);
+
+ uint32_t childCount;
+ key->GetChildCount(&childCount);
+
+ uint32_t accounts = 0;
+ uint32_t popCount = 0;
+ for (uint32_t i = 0; i < childCount; i++) {
+ nsAutoString keyName;
+ key->GetChildName(i, keyName);
+ nsCOMPtr<nsIWindowsRegKey> subKey;
+ rv = key->OpenChild(keyName, nsIWindowsRegKey::ACCESS_QUERY_VALUE,
+ getter_AddRefs(subKey));
+ if (NS_FAILED(rv)) continue;
+
+ // Get the values for this account.
+ nsAutoCString nativeKeyName;
+ NS_CopyUnicodeToNative(keyName, nativeKeyName);
+ IMPORT_LOG1("Opened Outlook account: %s\n", nativeKeyName.get());
+
+ nsCOMPtr<nsIMsgAccount> account;
+ nsAutoString value;
+ rv = subKey->ReadStringValue(u"IMAP Server"_ns, value);
+ if (NS_SUCCEEDED(rv) &&
+ DoIMAPServer(accMgr, subKey, value, getter_AddRefs(account)))
+ accounts++;
+
+ rv = subKey->ReadStringValue(u"POP3 Server"_ns, value);
+ if (NS_SUCCEEDED(rv) &&
+ DoPOP3Server(accMgr, subKey, value, getter_AddRefs(account))) {
+ popCount++;
+ accounts++;
+ if (aAccount && account) {
+ // If we created a mail account, get rid of it since
+ // we have 2 POP accounts!
+ if (popCount > 1)
+ NS_RELEASE(*aAccount);
+ else
+ NS_ADDREF(*aAccount = account);
+ }
+ }
+
+ // Is this the default account?
+ if (account && keyName.Equals(defMailName))
+ accMgr->SetDefaultAccount(account);
+ }
+
+ // Now save the new acct info to pref file.
+ rv = accMgr->SaveAccountInfo();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Can't save account info to pref file");
+
+ return accounts != 0;
+}
+
+nsresult OutlookSettings::GetAccountName(nsIWindowsRegKey* aKey,
+ const nsString& aDefaultName,
+ nsAString& aAccountName) {
+ nsresult rv;
+ rv = aKey->ReadStringValue(u"Account Name"_ns, aAccountName);
+ if (NS_FAILED(rv)) aAccountName.Assign(aDefaultName);
+
+ return NS_OK;
+}
+
+bool OutlookSettings::DoIMAPServer(nsIMsgAccountManager* aMgr,
+ nsIWindowsRegKey* aKey,
+ const nsString& aServerName,
+ nsIMsgAccount** aAccount) {
+ nsAutoString userName;
+ nsresult rv;
+ rv = aKey->ReadStringValue(u"IMAP User Name"_ns, userName);
+ if (NS_FAILED(rv)) return false;
+
+ bool result = false;
+
+ // I now have a user name/server name pair, find out if it already exists?
+ nsAutoCString nativeUserName;
+ NS_CopyUnicodeToNative(userName, nativeUserName);
+ nsAutoCString nativeServerName;
+ NS_CopyUnicodeToNative(aServerName, nativeServerName);
+ nsCOMPtr<nsIMsgIncomingServer> in;
+ aMgr->FindServer(nativeUserName, nativeServerName, "imap"_ns, 0,
+ getter_AddRefs(in));
+ if (!in) {
+ // Create the incoming server and an account for it?
+ rv = aMgr->CreateIncomingServer(nativeUserName, nativeServerName, "imap"_ns,
+ getter_AddRefs(in));
+ if (NS_SUCCEEDED(rv) && in) {
+ rv = in->SetType("imap"_ns);
+ // TODO SSL, auth method
+
+ IMPORT_LOG2("Created IMAP server named: %s, userName: %s\n",
+ nativeServerName.get(), nativeUserName.get());
+
+ nsAutoString prettyName;
+ if (NS_SUCCEEDED(GetAccountName(aKey, aServerName, prettyName)))
+ rv = in->SetPrettyName(prettyName);
+ // We have a server, create an account.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = aMgr->CreateAccount(getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ rv = account->SetIncomingServer(in);
+
+ IMPORT_LOG0(
+ "Created an account and set the IMAP server as the incoming "
+ "server\n");
+
+ // Fiddle with the identities
+ SetIdentities(aMgr, account, aKey);
+ result = true;
+ if (aAccount) account.forget(aAccount);
+ }
+ }
+ } else
+ result = true;
+
+ return result;
+}
+
+bool OutlookSettings::DoPOP3Server(nsIMsgAccountManager* aMgr,
+ nsIWindowsRegKey* aKey,
+ const nsString& aServerName,
+ nsIMsgAccount** aAccount) {
+ nsAutoString userName;
+ nsresult rv;
+ rv = aKey->ReadStringValue(u"POP3 User Name"_ns, userName);
+ if (NS_FAILED(rv)) return false;
+
+ // I now have a user name/server name pair, find out if it already exists?
+ nsAutoCString nativeUserName;
+ NS_CopyUnicodeToNative(userName, nativeUserName);
+ nsAutoCString nativeServerName;
+ NS_CopyUnicodeToNative(aServerName, nativeServerName);
+ nsCOMPtr<nsIMsgIncomingServer> in;
+ aMgr->FindServer(nativeUserName, nativeServerName, "pop3"_ns, 0,
+ getter_AddRefs(in));
+ if (in) return true;
+
+ // Create the incoming server and an account for it?
+ rv = aMgr->CreateIncomingServer(nativeUserName, nativeServerName, "pop3"_ns,
+ getter_AddRefs(in));
+ rv = in->SetType("pop3"_ns);
+
+ // TODO SSL, auth method
+
+ nsCOMPtr<nsIPop3IncomingServer> pop3Server = do_QueryInterface(in);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // set local folders as the Inbox to use for this POP3 server
+ nsCOMPtr<nsIMsgIncomingServer> localFoldersServer;
+ aMgr->GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+
+ if (!localFoldersServer) {
+ // XXX: We may need to move this local folder creation code to the generic
+ // nsImportSettings code if the other import modules end up needing to do
+ // this too. if Local Folders does not exist already, create it
+ rv = aMgr->CreateLocalMailAccount();
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create Local Folders!\n");
+ return false;
+ }
+ aMgr->GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+ }
+
+ // now get the account for this server
+ nsCOMPtr<nsIMsgAccount> localFoldersAccount;
+ aMgr->FindAccountForServer(localFoldersServer,
+ getter_AddRefs(localFoldersAccount));
+ if (localFoldersAccount) {
+ nsCString localFoldersAcctKey;
+ localFoldersAccount->GetKey(localFoldersAcctKey);
+ pop3Server->SetDeferredToAccount(localFoldersAcctKey);
+ pop3Server->SetDeferGetNewMail(true);
+ }
+
+ IMPORT_LOG2("Created POP3 server named: %s, userName: %s\n",
+ nativeServerName.get(), nativeUserName.get());
+
+ nsString prettyName;
+ rv = GetAccountName(aKey, aServerName, prettyName);
+ if (NS_FAILED(rv)) return false;
+
+ rv = in->SetPrettyName(prettyName);
+ // We have a server, create an account.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = aMgr->CreateAccount(getter_AddRefs(account));
+ if (NS_FAILED(rv)) return false;
+
+ rv = account->SetIncomingServer(in);
+
+ IMPORT_LOG0(
+ "Created a new account and set the incoming server to the POP3 "
+ "server.\n");
+
+ uint32_t leaveOnServer;
+ rv = aKey->ReadIntValue(u"Leave Mail On Server"_ns, &leaveOnServer);
+ if (NS_SUCCEEDED(rv))
+ pop3Server->SetLeaveMessagesOnServer(leaveOnServer == 1 ? true : false);
+
+ // Fiddle with the identities
+ SetIdentities(aMgr, account, aKey);
+
+ if (aAccount) account.forget(aAccount);
+
+ return true;
+}
+
+void OutlookSettings::SetIdentities(nsIMsgAccountManager* aMgr,
+ nsIMsgAccount* aAcc,
+ nsIWindowsRegKey* aKey) {
+ // Get the relevant information for an identity
+ nsAutoString name;
+ aKey->ReadStringValue(u"SMTP Display Name"_ns, name);
+
+ nsAutoString server;
+ aKey->ReadStringValue(u"SMTP Server"_ns, server);
+
+ nsAutoString email;
+ aKey->ReadStringValue(u"SMTP Email Address"_ns, email);
+
+ nsAutoString reply;
+ aKey->ReadStringValue(u"SMTP Reply To Email Address"_ns, reply);
+
+ nsAutoString userName;
+ aKey->ReadStringValue(u"SMTP User Name"_ns, userName);
+
+ nsAutoString orgName;
+ aKey->ReadStringValue(u"SMTP Organization Name"_ns, orgName);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgIdentity> id;
+ if (!email.IsEmpty() && !name.IsEmpty() && !server.IsEmpty()) {
+ // The default identity, nor any other identities matched,
+ // create a new one and add it to the account.
+ rv = aMgr->CreateIdentity(getter_AddRefs(id));
+ if (id) {
+ id->SetFullName(name);
+ id->SetOrganization(orgName);
+
+ nsAutoCString nativeEmail;
+ NS_CopyUnicodeToNative(email, nativeEmail);
+ id->SetEmail(nativeEmail);
+ if (!reply.IsEmpty()) {
+ nsAutoCString nativeReply;
+ NS_CopyUnicodeToNative(reply, nativeReply);
+ id->SetReplyTo(nativeReply);
+ }
+ aAcc->AddIdentity(id);
+
+ nsAutoCString nativeName;
+ NS_CopyUnicodeToNative(name, nativeName);
+ IMPORT_LOG0("Created identity and added to the account\n");
+ IMPORT_LOG1("\tname: %s\n", nativeName.get());
+ IMPORT_LOG1("\temail: %s\n", nativeEmail.get());
+ }
+ }
+
+ if (userName.IsEmpty()) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = aAcc->GetIncomingServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ nsAutoCString nativeUserName;
+ rv = incomingServer->GetUsername(nativeUserName);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Unable to get UserName from incomingServer");
+ NS_CopyNativeToUnicode(nativeUserName, userName);
+ }
+ }
+
+ SetSmtpServer(aMgr, aAcc, id, server, userName);
+}
+
+nsresult OutlookSettings::SetSmtpServerKey(nsIMsgIdentity* aId,
+ nsISmtpServer* aServer) {
+ nsAutoCString smtpServerKey;
+ aServer->GetKey(getter_Copies(smtpServerKey));
+ return aId->SetSmtpServerKey(smtpServerKey);
+}
+
+nsresult OutlookSettings::SetSmtpServer(nsIMsgAccountManager* aMgr,
+ nsIMsgAccount* aAcc,
+ nsIMsgIdentity* aId,
+ const nsString& aServer,
+ const nsString& aUser) {
+ nsresult rv;
+ nsCOMPtr<nsISmtpService> smtpService(
+ do_GetService("@mozilla.org/messengercompose/smtp;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString nativeUserName;
+ NS_CopyUnicodeToNative(aUser, nativeUserName);
+ nsAutoCString nativeServerName;
+ NS_CopyUnicodeToNative(aServer, nativeServerName);
+ nsCOMPtr<nsISmtpServer> foundServer;
+ rv = smtpService->FindServer(nativeUserName.get(), nativeServerName.get(),
+ getter_AddRefs(foundServer));
+ if (NS_SUCCEEDED(rv) && foundServer) {
+ if (aId) SetSmtpServerKey(aId, foundServer);
+ IMPORT_LOG1("SMTP server already exists: %s\n", nativeServerName.get());
+ return rv;
+ }
+
+ nsCOMPtr<nsISmtpServer> smtpServer;
+ rv = smtpService->CreateServer(getter_AddRefs(smtpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ smtpServer->SetHostname(nativeServerName);
+ if (!aUser.IsEmpty()) smtpServer->SetUsername(nativeUserName);
+
+ if (aId) SetSmtpServerKey(aId, smtpServer);
+
+ // TODO SSL, auth method
+ IMPORT_LOG1("Created new SMTP server: %s\n", nativeServerName.get());
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsOutlookSettings.h b/comm/mailnews/import/src/nsOutlookSettings.h
new file mode 100644
index 0000000000..7410200c3d
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookSettings.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsOutlookSettings_h___
+#define nsOutlookSettings_h___
+
+#include "nsIImportSettings.h"
+
+class nsOutlookSettings : public nsIImportSettings {
+ public:
+ nsOutlookSettings();
+
+ static nsresult Create(nsIImportSettings** aImport);
+
+ // nsISupports interface
+ NS_DECL_ISUPPORTS
+
+ // nsIImportSettings interface
+ NS_DECL_NSIIMPORTSETTINGS
+
+ private:
+ virtual ~nsOutlookSettings();
+};
+
+#endif /* nsOutlookSettings_h___ */
diff --git a/comm/mailnews/import/src/nsOutlookStringBundle.cpp b/comm/mailnews/import/src/nsOutlookStringBundle.cpp
new file mode 100644
index 0000000000..140eb9a541
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookStringBundle.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "prprf.h"
+#include "prmem.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsIStringBundle.h"
+#include "nsOutlookStringBundle.h"
+#include "mozilla/Components.h"
+
+#define OUTLOOK_MSGS_URL \
+ "chrome://messenger/locale/outlookImportMsgs.properties"
+
+nsCOMPtr<nsIStringBundle> nsOutlookStringBundle::m_pBundle = nullptr;
+
+void nsOutlookStringBundle::GetStringBundle(void) {
+ if (m_pBundle) return;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ if (sBundleService) {
+ sBundleService->CreateBundle(OUTLOOK_MSGS_URL, getter_AddRefs(m_pBundle));
+ }
+}
+
+void nsOutlookStringBundle::GetStringByID(int32_t stringID, nsString& result) {
+ char16_t* ptrv = GetStringByID(stringID);
+ result = ptrv;
+ FreeString(ptrv);
+}
+
+char16_t* nsOutlookStringBundle::GetStringByID(int32_t stringID) {
+ if (m_pBundle) GetStringBundle();
+
+ if (m_pBundle) {
+ nsAutoString str;
+ nsresult rv = m_pBundle->GetStringFromID(stringID, str);
+
+ if (NS_SUCCEEDED(rv)) return ToNewUnicode(str);
+ }
+
+ nsString resultString;
+ resultString.AppendLiteral("[StringID ");
+ resultString.AppendInt(stringID);
+ resultString.AppendLiteral("?]");
+
+ return ToNewUnicode(resultString);
+}
+
+void nsOutlookStringBundle::Cleanup(void) { m_pBundle = nullptr; }
diff --git a/comm/mailnews/import/src/nsOutlookStringBundle.h b/comm/mailnews/import/src/nsOutlookStringBundle.h
new file mode 100644
index 0000000000..491bbfaa64
--- /dev/null
+++ b/comm/mailnews/import/src/nsOutlookStringBundle.h
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#ifndef _nsOutlookStringBundle_H__
+#define _nsOutlookStringBundle_H__
+
+#include "nsCRTGlue.h"
+#include "nsString.h"
+
+class nsIStringBundle;
+
+class nsOutlookStringBundle {
+ public:
+ static char16_t* GetStringByID(int32_t stringID);
+ static void GetStringByID(int32_t stringID, nsString& result);
+ static void GetStringBundle(void);
+ static void FreeString(char16_t* pStr) { free(pStr); }
+ static void Cleanup(void);
+
+ private:
+ static nsCOMPtr<nsIStringBundle> m_pBundle;
+};
+
+#define OUTLOOKIMPORT_NAME 2000
+#define OUTLOOKIMPORT_DESCRIPTION 2010
+#define OUTLOOKIMPORT_MAILBOX_SUCCESS 2002
+#define OUTLOOKIMPORT_MAILBOX_BADPARAM 2003
+#define OUTLOOKIMPORT_MAILBOX_CONVERTERROR 2004
+#define OUTLOOKIMPORT_ADDRNAME 2005
+#define OUTLOOKIMPORT_ADDRESS_SUCCESS 2006
+#define OUTLOOKIMPORT_ADDRESS_BADPARAM 2007
+#define OUTLOOKIMPORT_ADDRESS_BADSOURCEFILE 2008
+#define OUTLOOKIMPORT_ADDRESS_CONVERTERROR 2009
+
+#endif /* _nsOutlookStringBundle_H__ */
diff --git a/comm/mailnews/import/src/nsTextAddress.cpp b/comm/mailnews/import/src/nsTextAddress.cpp
new file mode 100644
index 0000000000..497bd5b398
--- /dev/null
+++ b/comm/mailnews/import/src/nsTextAddress.cpp
@@ -0,0 +1,426 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsTextAddress.h"
+#include "nsIAbCard.h"
+#include "nsIAbDirectory.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "nsMsgI18N.h"
+#include "nsMsgUtils.h"
+#include "nsIConverterInputStream.h"
+#include "nsIUnicharLineInputStream.h"
+#include "nsMsgUtils.h"
+
+#include "ImportDebug.h"
+#include "plstr.h"
+#include "msgCore.h"
+
+#define kWhitespace " \t\b\r\n"
+
+nsTextAddress::nsTextAddress() {
+ m_LFCount = 0;
+ m_CRCount = 0;
+}
+
+nsTextAddress::~nsTextAddress() {}
+
+nsresult nsTextAddress::GetUnicharLineStreamForFile(
+ nsIFile* aFile, nsIInputStream* aInputStream,
+ nsIUnicharLineInputStream** aStream) {
+ nsAutoCString charset;
+ nsresult rv = MsgDetectCharsetFromFile(aFile, charset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIConverterInputStream> converterStream =
+ do_CreateInstance("@mozilla.org/intl/converter-input-stream;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = converterStream->Init(
+ aInputStream, charset.get(), 8192,
+ nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER);
+ }
+
+ return CallQueryInterface(converterStream, aStream);
+}
+
+nsresult nsTextAddress::ImportAddresses(bool* pAbort, const char16_t* pName,
+ nsIFile* pSrc,
+ nsIAbDirectory* pDirectory,
+ nsIImportFieldMap* fieldMap,
+ nsString& errors, uint32_t* pProgress) {
+ // Open the source file for reading, read each line and process it!
+ m_directory = pDirectory;
+ m_fieldMap = fieldMap;
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), pSrc);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening address file for reading\n");
+ return rv;
+ }
+
+ // Here we use this to work out the size of the file, so we can update
+ // an integer as we go through the file which will update a progress
+ // bar if required by the caller.
+ uint64_t bytesLeft = 0;
+ rv = inputStream->Available(&bytesLeft);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error checking address file for size\n");
+ inputStream->Close();
+ return rv;
+ }
+
+ uint64_t totalBytes = bytesLeft;
+ bool skipRecord = false;
+
+ rv = m_fieldMap->GetSkipFirstRecord(&skipRecord);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0(
+ "*** Error checking to see if we should skip the first record\n");
+ return rv;
+ }
+
+ nsCOMPtr<nsIUnicharLineInputStream> lineStream;
+ rv = GetUnicharLineStreamForFile(pSrc, inputStream,
+ getter_AddRefs(lineStream));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening converter stream for importer\n");
+ return rv;
+ }
+
+ bool more = true;
+ nsAutoString line;
+
+ // Skip the first record if the user has requested it.
+ if (skipRecord) rv = ReadRecord(lineStream, line, &more);
+
+ while (!(*pAbort) && more && NS_SUCCEEDED(rv)) {
+ // Read the line in
+ rv = ReadRecord(lineStream, line, &more);
+ if (NS_SUCCEEDED(rv)) {
+ // Now process it to add it to the database
+ rv = ProcessLine(line, errors);
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error processing text record.\n");
+ }
+ }
+ if (NS_SUCCEEDED(rv) && pProgress) {
+ // This won't be totally accurate, but its the best we can do
+ // considering that lineStream won't give us how many bytes
+ // are actually left.
+ bytesLeft -= line.Length();
+ *pProgress = std::min(totalBytes - bytesLeft, uint64_t(PR_UINT32_MAX));
+ }
+ }
+
+ inputStream->Close();
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0(
+ "*** Error reading the address book - probably incorrect ending\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTextAddress::ReadRecord(nsIUnicharLineInputStream* aLineStream,
+ nsAString& aLine, bool* aMore) {
+ bool more = true;
+ uint32_t numQuotes = 0;
+ nsresult rv;
+ nsAutoString line;
+
+ // ensure aLine is empty
+ aLine.Truncate();
+
+ do {
+ if (!more) {
+ // No more, so we must have an incorrect file.
+ rv = NS_ERROR_FAILURE;
+ } else {
+ // Read the line and append it
+ rv = aLineStream->ReadLine(line, &more);
+ if (NS_SUCCEEDED(rv)) {
+ if (!aLine.IsEmpty()) aLine.AppendLiteral(MSG_LINEBREAK);
+ aLine.Append(line);
+
+ numQuotes += line.CountChar(char16_t('"'));
+ }
+ }
+ // Continue whilst everything is ok, and we have an odd number of quotes.
+ } while (NS_SUCCEEDED(rv) && (numQuotes % 2 != 0));
+
+ *aMore = more;
+ return rv;
+}
+
+nsresult nsTextAddress::ReadRecordNumber(nsIFile* aSrc, nsAString& aLine,
+ int32_t rNum) {
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening address file for reading\n");
+ return rv;
+ }
+
+ int32_t rIndex = 0;
+ uint64_t bytesLeft = 0;
+
+ rv = inputStream->Available(&bytesLeft);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error checking address file for eof\n");
+ inputStream->Close();
+ return rv;
+ }
+
+ nsCOMPtr<nsIUnicharLineInputStream> lineStream;
+ rv = GetUnicharLineStreamForFile(aSrc, inputStream,
+ getter_AddRefs(lineStream));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening converter stream for importer\n");
+ return rv;
+ }
+
+ bool more = true;
+
+ while (more && (rIndex <= rNum)) {
+ rv = ReadRecord(lineStream, aLine, &more);
+ if (NS_FAILED(rv)) {
+ inputStream->Close();
+ return rv;
+ }
+ if (rIndex == rNum) {
+ inputStream->Close();
+ return NS_OK;
+ }
+
+ rIndex++;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+int32_t nsTextAddress::CountFields(const nsAString& aLine, char16_t delim) {
+ int32_t pos = 0;
+ int32_t maxLen = aLine.Length();
+ int32_t count = 0;
+ char16_t tab = char16_t('\t');
+ char16_t doubleQuote = char16_t('"');
+
+ if (delim == tab) tab = char16_t('\0');
+
+ while (pos < maxLen) {
+ while (((aLine[pos] == char16_t(' ')) || (aLine[pos] == tab)) &&
+ (pos < maxLen)) {
+ pos++;
+ }
+ if ((pos < maxLen) && (aLine[pos] == doubleQuote)) {
+ pos++;
+ while ((pos < maxLen) && (aLine[pos] != doubleQuote)) {
+ pos++;
+ if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
+ (aLine[pos + 1] == doubleQuote)) {
+ pos += 2;
+ }
+ }
+ if (pos < maxLen) pos++;
+ }
+ while ((pos < maxLen) && (aLine[pos] != delim)) pos++;
+
+ count++;
+ pos++;
+ }
+
+ return count;
+}
+
+bool nsTextAddress::GetField(const nsAString& aLine, int32_t index,
+ nsString& field, char16_t delim) {
+ bool result = false;
+ int32_t pos = 0;
+ int32_t maxLen = aLine.Length();
+ char16_t tab = char16_t('\t');
+ char16_t doubleQuote = char16_t('"');
+
+ field.Truncate();
+
+ if (delim == tab) tab = 0;
+
+ while (index && (pos < maxLen)) {
+ while (((aLine[pos] == char16_t(' ')) || (aLine[pos] == tab)) &&
+ (pos < maxLen)) {
+ pos++;
+ }
+ if (pos >= maxLen) break;
+ if (aLine[pos] == doubleQuote) {
+ do {
+ pos++;
+ if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
+ (aLine[pos + 1] == doubleQuote)) {
+ pos += 2;
+ }
+ } while ((pos < maxLen) && (aLine[pos] != doubleQuote));
+ if (pos < maxLen) pos++;
+ }
+ if (pos >= maxLen) break;
+
+ while ((pos < maxLen) && (aLine[pos] != delim)) pos++;
+
+ if (pos >= maxLen) break;
+
+ index--;
+ pos++;
+ }
+
+ if (pos >= maxLen) return result;
+
+ result = true;
+
+ while ((pos < maxLen) && ((aLine[pos] == ' ') || (aLine[pos] == tab))) pos++;
+
+ int32_t fLen = 0;
+ int32_t startPos = pos;
+ bool quoted = false;
+ if (aLine[pos] == '"') {
+ startPos++;
+ fLen = -1;
+ do {
+ pos++;
+ fLen++;
+ if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
+ (aLine[pos + 1] == doubleQuote)) {
+ quoted = true;
+ pos += 2;
+ fLen += 2;
+ }
+ } while ((pos < maxLen) && (aLine[pos] != doubleQuote));
+ } else {
+ while ((pos < maxLen) && (aLine[pos] != delim)) {
+ pos++;
+ fLen++;
+ }
+ }
+
+ if (!fLen) {
+ return result;
+ }
+
+ field.Append(nsDependentSubstring(aLine, startPos, fLen));
+ field.Trim(kWhitespace);
+
+ if (quoted) {
+ int32_t offset = field.Find(u"\"\"");
+ while (offset != -1) {
+ field.Cut(offset, 1);
+ offset = field.Find(u"\"\"", offset + 1);
+ }
+ }
+
+ return result;
+}
+
+nsresult nsTextAddress::DetermineDelim(nsIFile* aSrc) {
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening address file for reading\n");
+ return rv;
+ }
+
+ int32_t lineCount = 0;
+ int32_t tabCount = 0;
+ int32_t commaCount = 0;
+ int32_t tabLines = 0;
+ int32_t commaLines = 0;
+ nsAutoString line;
+ bool more = true;
+
+ nsCOMPtr<nsIUnicharLineInputStream> lineStream;
+ rv = GetUnicharLineStreamForFile(aSrc, inputStream,
+ getter_AddRefs(lineStream));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening converter stream for importer\n");
+ return rv;
+ }
+
+ while (more && NS_SUCCEEDED(rv) && (lineCount < 100)) {
+ rv = lineStream->ReadLine(line, &more);
+ if (NS_SUCCEEDED(rv)) {
+ tabCount = CountFields(line, char16_t('\t'));
+ commaCount = CountFields(line, char16_t(','));
+ if (tabCount > commaCount)
+ tabLines++;
+ else if (commaCount)
+ commaLines++;
+ }
+ lineCount++;
+ }
+
+ rv = inputStream->Close();
+
+ if (tabLines > commaLines)
+ m_delim = char16_t('\t');
+ else
+ m_delim = char16_t(',');
+
+ IMPORT_LOG2("Tab count = %d, Comma count = %d\n", tabLines, commaLines);
+
+ return rv;
+}
+
+/*
+ This is where the real work happens!
+ Go through the field map and set the data in a new database row
+*/
+nsresult nsTextAddress::ProcessLine(const nsAString& aLine, nsString& errors) {
+ if (!m_fieldMap) {
+ IMPORT_LOG0("*** Error, text import needs a field map\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+ // Wait until we get our first non-empty field, then create a new row,
+ // fill in the data, then add the row to the database.
+ nsCOMPtr<nsIAbCard> newCard;
+ nsAutoString fieldVal;
+ int32_t fieldNum;
+ int32_t numFields = 0;
+ bool active;
+ rv = m_fieldMap->GetMapSize(&numFields);
+ for (int32_t i = 0; (i < numFields) && NS_SUCCEEDED(rv); i++) {
+ active = false;
+ rv = m_fieldMap->GetFieldMap(i, &fieldNum);
+ if (NS_SUCCEEDED(rv)) rv = m_fieldMap->GetFieldActive(i, &active);
+ if (NS_SUCCEEDED(rv) && active) {
+ if (GetField(aLine, i, fieldVal, m_delim)) {
+ if (!fieldVal.IsEmpty()) {
+ if (!newCard) {
+ newCard = do_CreateInstance(
+ "@mozilla.org/addressbook/cardproperty;1", &rv);
+ }
+ if (newCard) {
+ rv = m_fieldMap->SetFieldValue(m_directory, newCard, fieldNum,
+ fieldVal);
+ }
+ }
+ } else {
+ break;
+ }
+ } else if (active) {
+ IMPORT_LOG1("*** Error getting field map for index %ld\n", (long)i);
+ }
+ }
+
+ nsIAbCard* outCard;
+ rv = m_directory->AddCard(newCard, &outCard);
+
+ return rv;
+}
diff --git a/comm/mailnews/import/src/nsTextAddress.h b/comm/mailnews/import/src/nsTextAddress.h
new file mode 100644
index 0000000000..58b37755b3
--- /dev/null
+++ b/comm/mailnews/import/src/nsTextAddress.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#ifndef nsTextAddress_h__
+#define nsTextAddress_h__
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIImportFieldMap.h"
+#include "nsIImportService.h"
+
+class nsIAbDirectory;
+class nsIFile;
+class nsIInputStream;
+class nsIUnicharLineInputStream;
+
+/////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////
+
+class nsTextAddress {
+ public:
+ nsTextAddress();
+ virtual ~nsTextAddress();
+
+ nsresult ImportAddresses(bool* pAbort, const char16_t* pName, nsIFile* pSrc,
+ nsIAbDirectory* pDirectory,
+ nsIImportFieldMap* fieldMap, nsString& errors,
+ uint32_t* pProgress);
+
+ nsresult DetermineDelim(nsIFile* pSrc);
+ char16_t GetDelim(void) { return m_delim; }
+
+ static nsresult ReadRecordNumber(nsIFile* pSrc, nsAString& aLine,
+ int32_t rNum);
+ static bool GetField(const nsAString& aLine, int32_t index, nsString& field,
+ char16_t delim);
+
+ private:
+ nsresult ProcessLine(const nsAString& aLine, nsString& errors);
+
+ static int32_t CountFields(const nsAString& aLine, char16_t delim);
+ static nsresult ReadRecord(nsIUnicharLineInputStream* pSrc, nsAString& aLine,
+ bool* aMore);
+ static nsresult GetUnicharLineStreamForFile(
+ nsIFile* aFile, nsIInputStream* aInputStream,
+ nsIUnicharLineInputStream** aStream);
+
+ char16_t m_delim;
+ int32_t m_LFCount;
+ int32_t m_CRCount;
+ nsCOMPtr<nsIAbDirectory> m_directory;
+ nsCOMPtr<nsIImportFieldMap> m_fieldMap;
+ nsCOMPtr<nsIImportService> m_pService;
+};
+
+#endif /* nsTextAddress_h__ */
diff --git a/comm/mailnews/import/src/nsTextImport.cpp b/comm/mailnews/import/src/nsTextImport.cpp
new file mode 100644
index 0000000000..961f1631ae
--- /dev/null
+++ b/comm/mailnews/import/src/nsTextImport.cpp
@@ -0,0 +1,646 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+/*
+ * Text import addressbook interfaces
+ */
+
+#include "nscore.h"
+#include "nsCOMPtr.h"
+#include "nsIImportService.h"
+#include "nsMsgI18N.h"
+#include "nsTextImport.h"
+#include "nsIImportGeneric.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIImportFieldMap.h"
+#include "nsIAbLDIFService.h"
+#include "nsTextFormatter.h"
+#include "nsImportStringBundle.h"
+#include "nsTextAddress.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "ImportDebug.h"
+#include "nsNetUtil.h"
+#include "nsMsgUtils.h"
+
+#define TEXT_MSGS_URL "chrome://messenger/locale/textImportMsgs.properties"
+#define TEXTIMPORT_NAME 2000
+#define TEXTIMPORT_DESCRIPTION 2001
+#define TEXTIMPORT_ADDRESS_NAME 2002
+#define TEXTIMPORT_ADDRESS_SUCCESS 2003
+#define TEXTIMPORT_ADDRESS_BADPARAM 2004
+#define TEXTIMPORT_ADDRESS_BADSOURCEFILE 2005
+#define TEXTIMPORT_ADDRESS_CONVERTERROR 2006
+
+class ImportAddressImpl final : public nsIImportAddressBooks {
+ public:
+ explicit ImportAddressImpl(nsIStringBundle* aStringBundle);
+
+ static nsresult Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIImportAddressBooks interface
+
+ NS_IMETHOD GetSupportsMultiple(bool* _retval) override {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetAutoFind(char16_t** description, bool* _retval) override;
+
+ NS_IMETHOD GetNeedsFieldMap(nsIFile* location, bool* _retval) override;
+
+ NS_IMETHOD GetDefaultLocation(nsIFile** location, bool* found,
+ bool* userVerify) override;
+
+ NS_IMETHOD FindAddressBooks(
+ nsIFile* location,
+ nsTArray<RefPtr<nsIImportABDescriptor>>& books) override;
+
+ NS_IMETHOD InitFieldMap(nsIImportFieldMap* fieldMap) override;
+
+ NS_IMETHOD ImportAddressBook(nsIImportABDescriptor* source,
+ nsIAbDirectory* destination,
+ nsIImportFieldMap* fieldMap,
+ nsISupports* aSupportService,
+ char16_t** errorLog, char16_t** successLog,
+ bool* fatalError) override;
+
+ NS_IMETHOD GetImportProgress(uint32_t* _retval) override;
+
+ NS_IMETHOD GetSampleData(int32_t index, bool* pFound,
+ char16_t** pStr) override;
+
+ NS_IMETHOD SetSampleLocation(nsIFile*) override;
+
+ private:
+ void ClearSampleFile(void);
+ void SaveFieldMap(nsIImportFieldMap* pMap);
+
+ static void ReportSuccess(nsString& name, nsString* pStream,
+ nsIStringBundle* pBundle);
+ static void SetLogs(nsString& success, nsString& error, char16_t** pError,
+ char16_t** pSuccess);
+ static void ReportError(int32_t errorNum, nsString& name, nsString* pStream,
+ nsIStringBundle* pBundle);
+ static void SanitizeSampleData(nsString& val);
+
+ private:
+ ~ImportAddressImpl() {}
+ nsTextAddress m_text;
+ bool m_haveDelim;
+ nsCOMPtr<nsIFile> m_fileLoc;
+ nsCOMPtr<nsIStringBundle> m_notProxyBundle;
+ char16_t m_delim;
+ uint32_t m_bytesImported;
+};
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+
+nsTextImport::nsTextImport() {
+ IMPORT_LOG0("nsTextImport Module Created\n");
+
+ nsImportStringBundle::GetStringBundle(TEXT_MSGS_URL,
+ getter_AddRefs(m_stringBundle));
+}
+
+nsTextImport::~nsTextImport() { IMPORT_LOG0("nsTextImport Module Deleted\n"); }
+
+NS_IMPL_ISUPPORTS(nsTextImport, nsIImportModule)
+
+NS_IMETHODIMP nsTextImport::GetName(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ *name = nsImportStringBundle::GetStringByID(TEXTIMPORT_NAME, m_stringBundle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTextImport::GetDescription(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ *name = nsImportStringBundle::GetStringByID(TEXTIMPORT_DESCRIPTION,
+ m_stringBundle);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTextImport::GetSupports(char** supports) {
+ NS_ENSURE_ARG_POINTER(supports);
+ *supports = strdup(kTextSupportsString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTextImport::GetSupportsUpgrade(bool* pUpgrade) {
+ NS_ASSERTION(pUpgrade != nullptr, "null ptr");
+ if (!pUpgrade) return NS_ERROR_NULL_POINTER;
+
+ *pUpgrade = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTextImport::GetImportInterface(const char* pImportType,
+ nsISupports** ppInterface) {
+ NS_ENSURE_ARG_POINTER(pImportType);
+ NS_ENSURE_ARG_POINTER(ppInterface);
+
+ *ppInterface = nullptr;
+ nsresult rv;
+
+ if (!strcmp(pImportType, "addressbook")) {
+ // create the nsIImportMail interface and return it!
+ nsCOMPtr<nsIImportAddressBooks> pAddress;
+ nsCOMPtr<nsIImportGeneric> pGeneric;
+ rv = ImportAddressImpl::Create(getter_AddRefs(pAddress), m_stringBundle);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = impSvc->CreateNewGenericAddressBooks(getter_AddRefs(pGeneric));
+ if (NS_SUCCEEDED(rv)) {
+ pGeneric->SetData("addressInterface", pAddress);
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pGeneric));
+ pInterface.forget(ppInterface);
+ }
+ }
+ }
+ return rv;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+
+nsresult ImportAddressImpl::Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new ImportAddressImpl(aStringBundle));
+ return NS_OK;
+}
+
+ImportAddressImpl::ImportAddressImpl(nsIStringBundle* aStringBundle)
+ : m_notProxyBundle(aStringBundle) {
+ m_haveDelim = false;
+}
+
+NS_IMPL_ISUPPORTS(ImportAddressImpl, nsIImportAddressBooks)
+
+NS_IMETHODIMP ImportAddressImpl::GetAutoFind(char16_t** addrDescription,
+ bool* _retval) {
+ NS_ASSERTION(addrDescription != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!addrDescription || !_retval) return NS_ERROR_NULL_POINTER;
+
+ nsString str;
+ *_retval = false;
+
+ if (!m_notProxyBundle) return NS_ERROR_FAILURE;
+
+ nsImportStringBundle::GetStringByID(TEXTIMPORT_ADDRESS_NAME, m_notProxyBundle,
+ str);
+ *addrDescription = ToNewUnicode(str);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportAddressImpl::GetDefaultLocation(nsIFile** ppLoc,
+ bool* found,
+ bool* userVerify) {
+ NS_ASSERTION(found != nullptr, "null ptr");
+ NS_ASSERTION(ppLoc != nullptr, "null ptr");
+ NS_ASSERTION(userVerify != nullptr, "null ptr");
+ if (!found || !userVerify || !ppLoc) return NS_ERROR_NULL_POINTER;
+
+ *ppLoc = nullptr;
+ *found = false;
+ *userVerify = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportAddressImpl::FindAddressBooks(
+ nsIFile* pLoc, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ NS_ASSERTION(pLoc != nullptr, "null ptr");
+ if (!pLoc) return NS_ERROR_NULL_POINTER;
+
+ books.Clear();
+ ClearSampleFile();
+
+ bool exists = false;
+ nsresult rv = pLoc->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_ERROR_FAILURE;
+
+ bool isFile = false;
+ rv = pLoc->IsFile(&isFile);
+ if (NS_FAILED(rv) || !isFile) return NS_ERROR_FAILURE;
+
+ rv = m_text.DetermineDelim(pLoc);
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error determining delimitter\n");
+ return rv;
+ }
+ m_haveDelim = true;
+ m_delim = m_text.GetDelim();
+
+ m_fileLoc = pLoc;
+
+ /* Build an address book descriptor based on the file passed in! */
+ nsString name;
+ m_fileLoc->GetLeafName(name);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed getting leaf name of file\n");
+ return rv;
+ }
+
+ int32_t idx = name.RFindChar('.');
+ if ((idx != -1) && (idx > 0) && ((name.Length() - idx - 1) < 5)) {
+ name.SetLength(idx);
+ }
+
+ nsCOMPtr<nsIImportABDescriptor> desc;
+
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to obtain the import service\n");
+ return rv;
+ }
+
+ rv = impSvc->CreateNewABDescriptor(getter_AddRefs(desc));
+ if (NS_SUCCEEDED(rv)) {
+ int64_t sz = 0;
+ pLoc->GetFileSize(&sz);
+ desc->SetPreferredName(name);
+ desc->SetSize((uint32_t)sz);
+ desc->SetAbFile(m_fileLoc);
+ books.AppendElement(desc);
+ }
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error creating address book descriptor for text import\n");
+ return rv;
+ }
+ return NS_OK;
+}
+
+void ImportAddressImpl::ReportSuccess(nsString& name, nsString* pStream,
+ nsIStringBundle* pBundle) {
+ if (!pStream) return;
+
+ // load the success string
+ char16_t* pFmt =
+ nsImportStringBundle::GetStringByID(TEXTIMPORT_ADDRESS_SUCCESS, pBundle);
+
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->Append(char16_t('\n'));
+}
+
+void ImportAddressImpl::ReportError(int32_t errorNum, nsString& name,
+ nsString* pStream,
+ nsIStringBundle* pBundle) {
+ if (!pStream) return;
+
+ // load the error string
+ char16_t* pFmt = nsImportStringBundle::GetStringByID(errorNum, pBundle);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->Append(char16_t('\n'));
+}
+
+void ImportAddressImpl::SetLogs(nsString& success, nsString& error,
+ char16_t** pError, char16_t** pSuccess) {
+ if (pError) *pError = ToNewUnicode(error);
+ if (pSuccess) *pSuccess = ToNewUnicode(success);
+}
+
+NS_IMETHODIMP
+ImportAddressImpl::ImportAddressBook(nsIImportABDescriptor* pSource,
+ nsIAbDirectory* pDestination,
+ nsIImportFieldMap* fieldMap,
+ nsISupports* aSupportService,
+ char16_t** pErrorLog,
+ char16_t** pSuccessLog, bool* fatalError) {
+ NS_ASSERTION(pSource != nullptr, "null ptr");
+ NS_ASSERTION(pDestination != nullptr, "null ptr");
+ NS_ASSERTION(fatalError != nullptr, "null ptr");
+
+ m_bytesImported = 0;
+
+ nsString success, error;
+ if (!pSource || !pDestination || !fatalError) {
+ IMPORT_LOG0("*** Bad param passed to text address import\n");
+ nsImportStringBundle::GetStringByID(TEXTIMPORT_ADDRESS_BADPARAM,
+ m_notProxyBundle, error);
+
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+
+ if (fatalError) *fatalError = true;
+
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ ClearSampleFile();
+
+ bool addrAbort = false;
+ nsString name;
+ pSource->GetPreferredName(name);
+
+ uint32_t addressSize = 0;
+ pSource->GetSize(&addressSize);
+ if (addressSize == 0) {
+ IMPORT_LOG0("Address book size is 0, skipping import.\n");
+ ReportSuccess(name, &success, m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> inFile;
+ if (NS_FAILED(pSource->GetAbFile(getter_AddRefs(inFile)))) {
+ ReportError(TEXTIMPORT_ADDRESS_BADSOURCEFILE, name, &error,
+ m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aSupportService) {
+ IMPORT_LOG0("Missing support service to import call");
+ return NS_ERROR_FAILURE;
+ }
+
+ bool isLDIF = false;
+ nsresult rv;
+ nsCOMPtr<nsIAbLDIFService> ldifService(
+ do_QueryInterface(aSupportService, &rv));
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = ldifService->IsLDIFFile(inFile, &isLDIF);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error reading address file\n");
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ ReportError(TEXTIMPORT_ADDRESS_CONVERTERROR, name, &error,
+ m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return rv;
+ }
+
+ if (isLDIF) {
+ if (ldifService)
+ rv = ldifService->ImportLDIFFile(pDestination, inFile, false,
+ &m_bytesImported);
+ else
+ return NS_ERROR_FAILURE;
+ } else {
+ rv = m_text.ImportAddresses(&addrAbort, name.get(), inFile, pDestination,
+ fieldMap, error, &m_bytesImported);
+ SaveFieldMap(fieldMap);
+ }
+
+ if (NS_SUCCEEDED(rv) && error.IsEmpty()) {
+ ReportSuccess(name, &success, m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ } else {
+ ReportError(TEXTIMPORT_ADDRESS_CONVERTERROR, name, &error,
+ m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ }
+
+ IMPORT_LOG0("*** Text address import done\n");
+ return rv;
+}
+
+NS_IMETHODIMP ImportAddressImpl::GetImportProgress(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_bytesImported;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportAddressImpl::GetNeedsFieldMap(nsIFile* aLocation,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ NS_ENSURE_ARG_POINTER(aLocation);
+
+ *_retval = true;
+ bool exists = false;
+ bool isFile = false;
+
+ nsresult rv = aLocation->Exists(&exists);
+ rv = aLocation->IsFile(&isFile);
+
+ if (!exists || !isFile) return NS_ERROR_FAILURE;
+
+ bool isLDIF = false;
+ nsCOMPtr<nsIAbLDIFService> ldifService =
+ do_GetService("@mozilla.org/addressbook/abldifservice;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) rv = ldifService->IsLDIFFile(aLocation, &isLDIF);
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error determining if file is of type LDIF\n");
+ return rv;
+ }
+
+ if (isLDIF) *_retval = false;
+
+ return NS_OK;
+}
+
+void ImportAddressImpl::SanitizeSampleData(nsString& val) {
+ // remove any line-feeds...
+ int32_t offset = val.Find(u"\x0D\x0A"_ns);
+ while (offset != -1) {
+ val.Replace(offset, 2, u", "_ns);
+ offset = val.Find(u"\x0D\x0A"_ns, offset + 2);
+ }
+ offset = val.FindChar(13);
+ while (offset != -1) {
+ val.Replace(offset, 1, ',');
+ offset = val.FindChar(13, offset + 2);
+ }
+ offset = val.FindChar(10);
+ while (offset != -1) {
+ val.Replace(offset, 1, ',');
+ offset = val.FindChar(10, offset + 2);
+ }
+}
+
+NS_IMETHODIMP ImportAddressImpl::GetSampleData(int32_t index, bool* pFound,
+ char16_t** pStr) {
+ NS_ASSERTION(pFound != nullptr, "null ptr");
+ NS_ASSERTION(pStr != nullptr, "null ptr");
+ if (!pFound || !pStr) return NS_ERROR_NULL_POINTER;
+
+ if (!m_fileLoc) {
+ IMPORT_LOG0("*** Error, called GetSampleData before SetSampleLocation\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ *pStr = nullptr;
+ char16_t term = 0;
+
+ if (!m_haveDelim) {
+ rv = m_text.DetermineDelim(m_fileLoc);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_haveDelim = true;
+ m_delim = m_text.GetDelim();
+ }
+
+ bool fileExists;
+ rv = m_fileLoc->Exists(&fileExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!fileExists) {
+ *pFound = false;
+ *pStr = NS_xstrdup(&term);
+ return NS_OK;
+ }
+
+ nsAutoString line;
+ rv = nsTextAddress::ReadRecordNumber(m_fileLoc, line, index);
+ if (NS_SUCCEEDED(rv)) {
+ nsString str;
+ nsString field;
+ int32_t fNum = 0;
+ while (nsTextAddress::GetField(line, fNum, field, m_delim)) {
+ if (fNum) str.Append(char16_t('\n'));
+ SanitizeSampleData(field);
+ str.Append(field);
+ fNum++;
+ field.Truncate();
+ }
+
+ *pStr = ToNewUnicode(str);
+ *pFound = true;
+
+ /* IMPORT_LOG1("Sample data: %S\n", str.get()); */
+ } else {
+ *pFound = false;
+ *pStr = NS_xstrdup(&term);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportAddressImpl::SetSampleLocation(nsIFile* pLocation) {
+ NS_ENSURE_ARG_POINTER(pLocation);
+
+ m_fileLoc = pLocation;
+ m_haveDelim = false;
+ return NS_OK;
+}
+
+void ImportAddressImpl::ClearSampleFile(void) {
+ m_fileLoc = nullptr;
+ m_haveDelim = false;
+}
+
+NS_IMETHODIMP ImportAddressImpl::InitFieldMap(nsIImportFieldMap* fieldMap) {
+ // Let's remember the last one the user used!
+ // This should be normal for someone importing multiple times, it's usually
+ // from the same file format.
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCString prefStr;
+ rv = prefs->GetCharPref("mailnews.import.text.fieldmap", prefStr);
+ if (NS_SUCCEEDED(rv)) {
+ const char* pStr = prefStr.get();
+ if (pStr) {
+ fieldMap->SetFieldMapSize(0);
+ long fNum;
+ bool active;
+ long fIndex = 0;
+ while (*pStr) {
+ while (*pStr && (*pStr != '+') && (*pStr != '-')) pStr++;
+ if (*pStr == '+')
+ active = true;
+ else if (*pStr == '-')
+ active = false;
+ else
+ break;
+ fNum = 0;
+ while (*pStr && ((*pStr < '0') || (*pStr > '9'))) pStr++;
+ if (!(*pStr)) break;
+ while (*pStr && (*pStr >= '0') && (*pStr <= '9')) {
+ fNum *= 10;
+ fNum += (*pStr - '0');
+ pStr++;
+ }
+ while (*pStr && (*pStr != ',')) pStr++;
+ if (*pStr == ',') pStr++;
+ if (!active) {
+ fNum *= -1; // Re-add the stripped minus sign.
+ }
+ fieldMap->SetFieldMap(-1, fNum);
+ fieldMap->SetFieldActive(fIndex, active);
+ fIndex++;
+ }
+ if (!fIndex) {
+ int num;
+ fieldMap->GetNumMozFields(&num);
+ fieldMap->DefaultFieldMap(num);
+ }
+ }
+ }
+
+ // Now also get the last used skip first record value.
+ bool skipFirstRecord = false;
+ rv = prefs->GetBoolPref("mailnews.import.text.skipfirstrecord",
+ &skipFirstRecord);
+ if (NS_SUCCEEDED(rv)) fieldMap->SetSkipFirstRecord(skipFirstRecord);
+ }
+
+ return NS_OK;
+}
+
+void ImportAddressImpl::SaveFieldMap(nsIImportFieldMap* pMap) {
+ if (!pMap) return;
+
+ int size;
+ int index;
+ bool active;
+ nsCString str;
+
+ pMap->GetMapSize(&size);
+ for (long i = 0; i < size; i++) {
+ index = i;
+ active = false;
+ pMap->GetFieldMap(i, &index);
+ pMap->GetFieldActive(i, &active);
+ if (active)
+ str.Append('+');
+ else
+ str.Append('-');
+
+ str.AppendInt(index);
+ str.Append(',');
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString prefStr;
+ rv = prefs->GetCharPref("mailnews.import.text.fieldmap", prefStr);
+ if (NS_FAILED(rv) || !str.Equals(prefStr))
+ rv = prefs->SetCharPref("mailnews.import.text.fieldmap", str);
+ }
+
+ // Now also save last used skip first record value.
+ bool skipFirstRecord = false;
+ rv = pMap->GetSkipFirstRecord(&skipFirstRecord);
+ if (NS_SUCCEEDED(rv))
+ prefs->SetBoolPref("mailnews.import.text.skipfirstrecord", skipFirstRecord);
+}
diff --git a/comm/mailnews/import/src/nsTextImport.h b/comm/mailnews/import/src/nsTextImport.h
new file mode 100644
index 0000000000..1106a0d098
--- /dev/null
+++ b/comm/mailnews/import/src/nsTextImport.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#ifndef nsTextImport_h___
+#define nsTextImport_h___
+
+#include "nsIImportModule.h"
+#include "nsCOMPtr.h"
+#include "nsIStringBundle.h"
+
+#define NS_TEXTIMPORT_CID \
+ { /* A5991D01-ADA7-11d3-A9C2-00A0CC26DA63 */ \
+ 0xa5991d01, 0xada7, 0x11d3, { \
+ 0xa9, 0xc2, 0x0, 0xa0, 0xcc, 0x26, 0xda, 0x63 \
+ } \
+ }
+
+#define kTextSupportsString NS_IMPORT_ADDRESS_STR
+
+class nsTextImport : public nsIImportModule {
+ public:
+ nsTextImport();
+
+ NS_DECL_ISUPPORTS
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImportModule interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ NS_DECL_NSIIMPORTMODULE
+
+ protected:
+ virtual ~nsTextImport();
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+};
+
+#endif /* nsTextImport_h___ */
diff --git a/comm/mailnews/import/src/nsVCardAddress.cpp b/comm/mailnews/import/src/nsVCardAddress.cpp
new file mode 100644
index 0000000000..2ced1d7c0b
--- /dev/null
+++ b/comm/mailnews/import/src/nsVCardAddress.cpp
@@ -0,0 +1,151 @@
+/* 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/. */
+
+#include "nsNativeCharsetUtils.h"
+#include "nsNetUtil.h"
+#include "nsVCardAddress.h"
+
+#include "nsIAbCard.h"
+#include "nsIAbDirectory.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIUnicharLineInputStream.h"
+#include "nsIConverterInputStream.h"
+#include "nsIMsgVCardService.h"
+
+#include "plstr.h"
+#include "msgCore.h"
+#include "nsMsgUtils.h"
+
+nsVCardAddress::nsVCardAddress() {}
+
+nsVCardAddress::~nsVCardAddress() {}
+
+nsresult nsVCardAddress::ImportAddresses(bool* pAbort, const char16_t* pName,
+ nsIFile* pSrc,
+ nsIAbDirectory* pDirectory,
+ nsString& errors,
+ uint32_t* pProgress) {
+ // Open the source file for reading, read each line and process it!
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), pSrc);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error opening address file for reading\n");
+ return rv;
+ }
+
+ // Open the source file for reading, read each line and process it!
+ // Here we use this to work out the size of the file, so we can update
+ // an integer as we go through the file which will update a progress
+ // bar if required by the caller.
+ uint64_t bytesLeft = 0;
+ rv = inputStream->Available(&bytesLeft);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error checking address file for size\n");
+ inputStream->Close();
+ return rv;
+ }
+ uint64_t totalBytes = bytesLeft;
+
+ // Try to detect the character set and decode. Only UTF-8 is valid from
+ // vCard 4.0, but we support older versions, so other charsets are possible.
+
+ nsAutoCString sourceCharset;
+ rv = MsgDetectCharsetFromFile(pSrc, sourceCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIConverterInputStream> converterStream =
+ do_CreateInstance("@mozilla.org/intl/converter-input-stream;1");
+ NS_ENSURE_TRUE(converterStream, NS_ERROR_FAILURE);
+
+ rv = converterStream->Init(
+ inputStream, sourceCharset.get(), 8192,
+ nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUnicharLineInputStream> lineStream(
+ do_QueryInterface(converterStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgVCardService> vCardService =
+ do_GetService("@mozilla.org/addressbook/msgvcardservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsAutoString record;
+ while (!(*pAbort) && more && NS_SUCCEEDED(rv)) {
+ rv = ReadRecord(lineStream, record, &more);
+ if (NS_SUCCEEDED(rv) && !record.IsEmpty()) {
+ // Parse the vCard and build an nsIAbCard from it
+ nsCOMPtr<nsIAbCard> cardFromVCard;
+ rv = vCardService->VCardToAbCard(record, getter_AddRefs(cardFromVCard));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIAbCard* outCard;
+ rv = pDirectory->AddCard(cardFromVCard, &outCard);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Error processing vCard record.\n");
+ }
+ }
+ if (NS_SUCCEEDED(rv) && pProgress) {
+ // This won't be totally accurate, but its the best we can do
+ // considering that converterStream won't give us how many bytes
+ // are actually left.
+ bytesLeft -= record.Length();
+ *pProgress = totalBytes - bytesLeft;
+ }
+ }
+ inputStream->Close();
+
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0(
+ "*** Error reading the address book - probably incorrect ending\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsVCardAddress::ReadRecord(nsIUnicharLineInputStream* aLineStream,
+ nsString& aRecord, bool* aMore) {
+ bool more = true;
+ nsresult rv;
+ nsAutoString line;
+
+ aRecord.Truncate();
+
+ // remove the empty lines.
+ do {
+ rv = aLineStream->ReadLine(line, aMore);
+ } while (line.IsEmpty() && *aMore);
+ if (!*aMore) return rv;
+
+ // read BEGIN:VCARD
+ if (!line.LowerCaseEqualsLiteral("begin:vcard")) {
+ IMPORT_LOG0(
+ "*** Expected case-insensitive BEGIN:VCARD at start of vCard\n");
+ rv = NS_ERROR_FAILURE;
+ *aMore = more;
+ return rv;
+ }
+ aRecord.Append(line);
+
+ // read until END:VCARD
+ do {
+ if (!more) {
+ IMPORT_LOG0(
+ "*** Expected case-insensitive END:VCARD at start of vCard\n");
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ rv = aLineStream->ReadLine(line, &more);
+ aRecord.AppendLiteral(MSG_LINEBREAK);
+ aRecord.Append(line);
+ } while (!line.LowerCaseEqualsLiteral("end:vcard"));
+
+ *aMore = more;
+ return rv;
+}
diff --git a/comm/mailnews/import/src/nsVCardAddress.h b/comm/mailnews/import/src/nsVCardAddress.h
new file mode 100644
index 0000000000..4cdb4de2b4
--- /dev/null
+++ b/comm/mailnews/import/src/nsVCardAddress.h
@@ -0,0 +1,28 @@
+/* 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/. */
+
+#ifndef nsVCardAddress_h__
+#define nsVCardAddress_h__
+
+#include "ImportDebug.h"
+
+class nsIAbDirectory;
+class nsIFile;
+class nsIUnicharLineInputStream;
+
+class nsVCardAddress {
+ public:
+ nsVCardAddress();
+ virtual ~nsVCardAddress();
+
+ nsresult ImportAddresses(bool* pAbort, const char16_t* pName, nsIFile* pSrc,
+ nsIAbDirectory* pDirectory, nsString& errors,
+ uint32_t* pProgress);
+
+ private:
+ static nsresult ReadRecord(nsIUnicharLineInputStream* aLineStream,
+ nsString& aRecord, bool* aMore);
+};
+
+#endif /* nsVCardAddress_h__ */
diff --git a/comm/mailnews/import/src/nsVCardImport.cpp b/comm/mailnews/import/src/nsVCardImport.cpp
new file mode 100644
index 0000000000..98f7482ddf
--- /dev/null
+++ b/comm/mailnews/import/src/nsVCardImport.cpp
@@ -0,0 +1,352 @@
+/* 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/. */
+
+/*
+ VCard import addressbook interfaces
+*/
+#include "nscore.h"
+#include "nsIFile.h"
+#include "nsIImportABDescriptor.h"
+#include "nsIImportAddressBooks.h"
+#include "nsIImportFieldMap.h"
+#include "nsIImportGeneric.h"
+#include "nsCOMPtr.h"
+#include "nsIImportService.h"
+#include "nsIFile.h"
+#include "nsImportStringBundle.h"
+#include "nsMsgUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTextFormatter.h"
+#include "nsVCardAddress.h"
+#include "nsVCardImport.h"
+
+class ImportVCardAddressImpl : public nsIImportAddressBooks {
+ public:
+ explicit ImportVCardAddressImpl(nsIStringBundle* aStringBundle);
+
+ static nsresult Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIImportAddressBooks interface
+
+ // TODO: support multiple vCard files in future - shouldn't be too hard,
+ // since you just import each file in turn.
+ NS_IMETHOD GetSupportsMultiple(bool* _retval) override {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetAutoFind(char16_t** description, bool* _retval) override;
+
+ NS_IMETHOD GetNeedsFieldMap(nsIFile* location, bool* _retval) override {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetDefaultLocation(nsIFile** location, bool* found,
+ bool* userVerify) override;
+
+ NS_IMETHOD FindAddressBooks(
+ nsIFile* location,
+ nsTArray<RefPtr<nsIImportABDescriptor>>& books) override;
+
+ NS_IMETHOD InitFieldMap(nsIImportFieldMap* fieldMap) override {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IMETHOD ImportAddressBook(nsIImportABDescriptor* source,
+ nsIAbDirectory* destination,
+ nsIImportFieldMap* fieldMap,
+ nsISupports* aSupportService,
+ char16_t** errorLog, char16_t** successLog,
+ bool* fatalError) override;
+
+ NS_IMETHOD GetImportProgress(uint32_t* _retval) override;
+
+ NS_IMETHOD GetSampleData(int32_t index, bool* pFound,
+ char16_t** pStr) override {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IMETHOD SetSampleLocation(nsIFile*) override { return NS_ERROR_FAILURE; }
+
+ private:
+ virtual ~ImportVCardAddressImpl();
+ static void ReportSuccess(nsString& name, nsString* pStream,
+ nsIStringBundle* pBundle);
+ static void SetLogs(nsString& success, nsString& error, char16_t** pError,
+ char16_t** pSuccess);
+ static void ReportError(const char* errorName, nsString& name,
+ nsString* pStream, nsIStringBundle* pBundle);
+
+ private:
+ nsVCardAddress m_vCard;
+ nsCOMPtr<nsIFile> m_fileLoc;
+ uint32_t m_bytesImported;
+ nsCOMPtr<nsIStringBundle> m_notProxyBundle;
+};
+
+nsVCardImport::nsVCardImport() {
+ nsImportStringBundle::GetStringBundle(VCARDIMPORT_MSGS_URL,
+ getter_AddRefs(m_stringBundle));
+
+ IMPORT_LOG0("nsVCardImport Module Created\n");
+}
+
+nsVCardImport::~nsVCardImport() {
+ IMPORT_LOG0("nsVCardImport Module Deleted\n");
+}
+
+NS_IMPL_ISUPPORTS(nsVCardImport, nsIImportModule)
+
+NS_IMETHODIMP nsVCardImport::GetName(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ *name =
+ nsImportStringBundle::GetStringByName("vCardImportName", m_stringBundle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsVCardImport::GetDescription(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ *name = nsImportStringBundle::GetStringByName("vCardImportDescription",
+ m_stringBundle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsVCardImport::GetSupports(char** supports) {
+ NS_ENSURE_ARG_POINTER(supports);
+ *supports = strdup(NS_IMPORT_ADDRESS_STR);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsVCardImport::GetSupportsUpgrade(bool* pUpgrade) {
+ NS_ENSURE_ARG_POINTER(pUpgrade);
+ *pUpgrade = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsVCardImport::GetImportInterface(const char* pImportType,
+ nsISupports** ppInterface) {
+ NS_ENSURE_ARG_POINTER(pImportType);
+ NS_ENSURE_ARG_POINTER(ppInterface);
+ *ppInterface = nullptr;
+ if (!strcmp(pImportType, "addressbook")) {
+ nsresult rv;
+ // create the nsIImportMail interface and return it!
+ nsCOMPtr<nsIImportAddressBooks> pAddress;
+ nsCOMPtr<nsIImportGeneric> pGeneric;
+ rv = ImportVCardAddressImpl::Create(getter_AddRefs(pAddress),
+ m_stringBundle);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = impSvc->CreateNewGenericAddressBooks(getter_AddRefs(pGeneric));
+ if (NS_SUCCEEDED(rv)) {
+ pGeneric->SetData("addressInterface", pAddress);
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pGeneric));
+ pInterface.forget(ppInterface);
+ }
+ }
+ }
+ return rv;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult ImportVCardAddressImpl::Create(nsIImportAddressBooks** aImport,
+ nsIStringBundle* aStringBundle) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new ImportVCardAddressImpl(aStringBundle));
+ return NS_OK;
+}
+
+ImportVCardAddressImpl::ImportVCardAddressImpl(nsIStringBundle* aStringBundle)
+ : m_notProxyBundle(aStringBundle) {}
+
+ImportVCardAddressImpl::~ImportVCardAddressImpl() {}
+
+NS_IMPL_ISUPPORTS(ImportVCardAddressImpl, nsIImportAddressBooks)
+
+NS_IMETHODIMP ImportVCardAddressImpl::GetAutoFind(char16_t** addrDescription,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(addrDescription);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsString str;
+ *_retval = false;
+
+ if (!m_notProxyBundle) return NS_ERROR_FAILURE;
+
+ nsImportStringBundle::GetStringByName("vCardImportAddressName",
+ m_notProxyBundle, str);
+ *addrDescription = ToNewUnicode(str);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportVCardAddressImpl::GetDefaultLocation(nsIFile** ppLoc,
+ bool* found,
+ bool* userVerify) {
+ NS_ENSURE_ARG_POINTER(found);
+ NS_ENSURE_ARG_POINTER(ppLoc);
+ NS_ENSURE_ARG_POINTER(userVerify);
+
+ *ppLoc = nullptr;
+ *found = false;
+ *userVerify = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ImportVCardAddressImpl::FindAddressBooks(
+ nsIFile* pLoc, nsTArray<RefPtr<nsIImportABDescriptor>>& books) {
+ NS_ENSURE_ARG_POINTER(pLoc);
+
+ books.Clear();
+ bool exists = false;
+ nsresult rv = pLoc->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_ERROR_FAILURE;
+
+ bool isFile = false;
+ rv = pLoc->IsFile(&isFile);
+ if (NS_FAILED(rv) || !isFile) return NS_ERROR_FAILURE;
+
+ m_fileLoc = pLoc;
+
+ /* Build an address book descriptor based on the file passed in! */
+ nsString name;
+ m_fileLoc->GetLeafName(name);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed getting leaf name of file\n");
+ return rv;
+ }
+
+ int32_t idx = name.RFindChar('.');
+ if ((idx != -1) && (idx > 0) && ((name.Length() - idx - 1) < 5)) {
+ name.SetLength(idx);
+ }
+
+ nsCOMPtr<nsIImportABDescriptor> desc;
+ nsCOMPtr<nsIImportService> impSvc(
+ do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to obtain the import service\n");
+ return rv;
+ }
+
+ rv = impSvc->CreateNewABDescriptor(getter_AddRefs(desc));
+ if (NS_SUCCEEDED(rv)) {
+ int64_t sz = 0;
+ pLoc->GetFileSize(&sz);
+ desc->SetPreferredName(name);
+ desc->SetSize((uint32_t)sz);
+ desc->SetAbFile(m_fileLoc);
+ books.AppendElement(desc);
+ }
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0(
+ "*** Error creating address book descriptor for vCard import\n");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void ImportVCardAddressImpl::ReportSuccess(nsString& name, nsString* pStream,
+ nsIStringBundle* pBundle) {
+ if (!pStream) return;
+
+ // load the success string
+ char16_t* pFmt = nsImportStringBundle::GetStringByName(
+ "vCardImportAddressSuccess", pBundle);
+
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->Append(char16_t('\n'));
+}
+
+void ImportVCardAddressImpl::ReportError(const char* errorName, nsString& name,
+ nsString* pStream,
+ nsIStringBundle* pBundle) {
+ if (!pStream) return;
+
+ // load the error string
+ char16_t* pFmt = nsImportStringBundle::GetStringByName(errorName, pBundle);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ free(pFmt);
+ pStream->Append(char16_t('\n'));
+}
+
+void ImportVCardAddressImpl::SetLogs(nsString& success, nsString& error,
+ char16_t** pError, char16_t** pSuccess) {
+ if (pError) *pError = ToNewUnicode(error);
+ if (pSuccess) *pSuccess = ToNewUnicode(success);
+}
+
+NS_IMETHODIMP ImportVCardAddressImpl::ImportAddressBook(
+ nsIImportABDescriptor* pSource, nsIAbDirectory* pDestination,
+ nsIImportFieldMap* fieldMap, nsISupports* aSupportService,
+ char16_t** pErrorLog, char16_t** pSuccessLog, bool* fatalError) {
+ NS_ENSURE_ARG_POINTER(pSource);
+ NS_ENSURE_ARG_POINTER(pDestination);
+ NS_ENSURE_ARG_POINTER(fatalError);
+
+ if (!m_notProxyBundle) return NS_ERROR_FAILURE;
+
+ m_bytesImported = 0;
+ nsString success, error;
+ bool addrAbort = false;
+ nsString name;
+ pSource->GetPreferredName(name);
+
+ uint32_t addressSize = 0;
+ pSource->GetSize(&addressSize);
+ if (addressSize == 0) {
+ IMPORT_LOG0("Address book size is 0, skipping import.\n");
+ ReportSuccess(name, &success, m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> inFile;
+ if (NS_FAILED(pSource->GetAbFile(getter_AddRefs(inFile)))) {
+ ReportError("vCardImportAddressBadSourceFile", name, &error,
+ m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aSupportService) {
+ IMPORT_LOG0("Missing support service to import call\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = m_vCard.ImportAddresses(&addrAbort, name.get(), inFile,
+ pDestination, error, &m_bytesImported);
+
+ if (NS_SUCCEEDED(rv) && error.IsEmpty()) {
+ ReportSuccess(name, &success, m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ } else {
+ ReportError("vCardImportAddressConvertError", name, &error,
+ m_notProxyBundle);
+ SetLogs(success, error, pErrorLog, pSuccessLog);
+ }
+
+ IMPORT_LOG0("*** VCard address import done\n");
+ return rv;
+}
+
+NS_IMETHODIMP ImportVCardAddressImpl::GetImportProgress(uint32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_bytesImported;
+ return NS_OK;
+}
diff --git a/comm/mailnews/import/src/nsVCardImport.h b/comm/mailnews/import/src/nsVCardImport.h
new file mode 100644
index 0000000000..f35e5eb3d3
--- /dev/null
+++ b/comm/mailnews/import/src/nsVCardImport.h
@@ -0,0 +1,39 @@
+/* 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/. */
+
+#ifndef nsVCardImport_h___
+#define nsVCardImport_h___
+
+#include "nsIImportModule.h"
+#include "nsIStringBundle.h"
+#include "nsCOMPtr.h"
+
+#define NS_VCARDIMPORT_CID \
+ { /* 0EB034A3-964A-4E2F-92EBCC55D9AE9DD2 */ \
+ 0x0eb034a3, 0x964a, 0x4e2f, { \
+ 0x92, 0xeb, 0xcc, 0x55, 0xd9, 0xae, 0x9d, 0xd2 \
+ } \
+ }
+
+#define VCARDIMPORT_MSGS_URL \
+ "chrome://messenger/locale/vCardImportMsgs.properties"
+
+class nsVCardImport : public nsIImportModule {
+ public:
+ nsVCardImport();
+
+ NS_DECL_ISUPPORTS
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImportModule interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ NS_DECL_NSIIMPORTMODULE
+
+ protected:
+ virtual ~nsVCardImport();
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+};
+
+#endif /* nsVCardImport_h___ */
diff --git a/comm/mailnews/import/src/nsWMImport.cpp b/comm/mailnews/import/src/nsWMImport.cpp
new file mode 100644
index 0000000000..a3b5dd45b2
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMImport.cpp
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * Windows Live Mail (Win32) import mail and addressbook interfaces
+ */
+
+#include "nscore.h"
+#include "nsString.h"
+#include "nsMsgUtils.h"
+#include "nsWMImport.h"
+#include "nsIImportMail.h"
+#include "nsIImportMailboxDescriptor.h"
+#include "nsXPCOM.h"
+#include "nsWMSettings.h"
+#include "nsTextFormatter.h"
+#include "nsWMStringBundle.h"
+#include "nsUnicharUtils.h"
+
+#include "ImportDebug.h"
+
+class ImportWMMailImpl : public nsIImportMail {
+ public:
+ ImportWMMailImpl();
+
+ static nsresult Create(nsIImportMail** aImport);
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIImportmail interface
+
+ /* void GetDefaultLocation (out nsIFile location, out boolean found, out
+ * boolean userVerify); */
+ NS_IMETHOD GetDefaultLocation(nsIFile** location, bool* found,
+ bool* userVerify);
+
+ /* nsIArray FindMailboxes (in nsIFile location); */
+ NS_IMETHOD FindMailboxes(nsIFile* location,
+ nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes);
+
+ NS_IMETHOD ImportMailbox(nsIImportMailboxDescriptor* source,
+ nsIMsgFolder* dstFolder, char16_t** pErrorLog,
+ char16_t** pSuccessLog, bool* fatalError);
+
+ /* unsigned long GetImportProgress (); */
+ NS_IMETHOD GetImportProgress(uint32_t* _retval);
+
+ NS_IMETHOD TranslateFolderName(const nsAString& aFolderName,
+ nsAString& _retval);
+
+ public:
+ static void ReportSuccess(nsString& name, int32_t count, nsString* pStream);
+ static void ReportError(int32_t errorNum, nsString& name, nsString* pStream);
+ static void AddLinebreak(nsString* pStream);
+ static void SetLogs(nsString& success, nsString& error, char16_t** pError,
+ char16_t** pSuccess);
+
+ private:
+ virtual ~ImportWMMailImpl();
+};
+
+nsWMImport::nsWMImport() {
+ IMPORT_LOG0("nsWMImport Module Created\n");
+ nsWMStringBundle::GetStringBundle();
+}
+
+nsWMImport::~nsWMImport() { IMPORT_LOG0("nsWMImport Module Deleted\n"); }
+
+NS_IMPL_ISUPPORTS(nsWMImport, nsIImportModule)
+
+NS_IMETHODIMP nsWMImport::GetName(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+ // nsString title = "Windows Live Mail";
+ // *name = ToNewUnicode(title);
+ *name = nsWMStringBundle::GetStringByID(WMIMPORT_NAME);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsWMImport::GetDescription(char16_t** name) {
+ NS_ENSURE_ARG_POINTER(name);
+
+ // nsString desc = "Windows Live Mail mail and address books";
+ // *name = ToNewUnicode(desc);
+ *name = nsWMStringBundle::GetStringByID(WMIMPORT_DESCRIPTION);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsWMImport::GetSupports(char** supports) {
+ NS_ASSERTION(supports != nullptr, "null ptr");
+ if (!supports) return NS_ERROR_NULL_POINTER;
+
+ *supports = strdup(kWMSupportsString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsWMImport::GetSupportsUpgrade(bool* pUpgrade) {
+ NS_ASSERTION(pUpgrade != nullptr, "null ptr");
+ if (!pUpgrade) return NS_ERROR_NULL_POINTER;
+
+ *pUpgrade = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsWMImport::GetImportInterface(const char* pImportType,
+ nsISupports** ppInterface) {
+ NS_ENSURE_ARG_POINTER(pImportType);
+ NS_ENSURE_ARG_POINTER(ppInterface);
+
+ *ppInterface = nullptr;
+ nsresult rv;
+
+ if (!strcmp(pImportType, "settings")) {
+ nsCOMPtr<nsIImportSettings> pSettings;
+ rv = nsWMSettings::Create(getter_AddRefs(pSettings));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsISupports> pInterface(do_QueryInterface(pSettings));
+ pInterface.forget(ppInterface);
+ }
+ return rv;
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+nsresult ImportWMMailImpl::Create(nsIImportMail** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new ImportWMMailImpl());
+ return NS_OK;
+}
+
+ImportWMMailImpl::ImportWMMailImpl() {}
+
+ImportWMMailImpl::~ImportWMMailImpl() {}
+
+NS_IMPL_ISUPPORTS(ImportWMMailImpl, nsIImportMail)
+
+NS_IMETHODIMP ImportWMMailImpl::TranslateFolderName(
+ const nsAString& aFolderName, nsAString& _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ImportWMMailImpl::GetDefaultLocation(nsIFile** ppLoc, bool* found,
+ bool* userVerify) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ImportWMMailImpl::FindMailboxes(
+ nsIFile* pLoc, nsTArray<RefPtr<nsIImportMailboxDescriptor>>& boxes) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void ImportWMMailImpl::AddLinebreak(nsString* pStream) {
+ if (pStream) pStream->Append(char16_t('\n'));
+}
+
+void ImportWMMailImpl::ReportSuccess(nsString& name, int32_t count,
+ nsString* pStream) {
+ if (!pStream) return;
+ // load the success string
+ char16_t* pFmt = nsWMStringBundle::GetStringByID(WMIMPORT_MAILBOX_SUCCESS);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get(), count);
+ pStream->Append(pText);
+ nsWMStringBundle::FreeString(pFmt);
+ AddLinebreak(pStream);
+}
+
+void ImportWMMailImpl::ReportError(int32_t errorNum, nsString& name,
+ nsString* pStream) {
+ if (!pStream) return;
+ // load the error string
+ char16_t* pFmt = nsWMStringBundle::GetStringByID(errorNum);
+ nsString pText;
+ nsTextFormatter::ssprintf(pText, pFmt, name.get());
+ pStream->Append(pText);
+ nsWMStringBundle::FreeString(pFmt);
+ AddLinebreak(pStream);
+}
+
+void ImportWMMailImpl::SetLogs(nsString& success, nsString& error,
+ char16_t** pError, char16_t** pSuccess) {
+ if (pError) *pError = ToNewUnicode(error);
+ if (pSuccess) *pSuccess = ToNewUnicode(success);
+}
+
+NS_IMETHODIMP ImportWMMailImpl::ImportMailbox(
+ nsIImportMailboxDescriptor* pSource, nsIMsgFolder* pDstFolder,
+ char16_t** pErrorLog, char16_t** pSuccessLog, bool* fatalError) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ImportWMMailImpl::GetImportProgress(uint32_t* pDoneSoFar) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/import/src/nsWMImport.h b/comm/mailnews/import/src/nsWMImport.h
new file mode 100644
index 0000000000..1f14b1331f
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMImport.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsWMImport_h___
+#define nsWMImport_h___
+
+#include "nsIImportModule.h"
+#include "nsCOMPtr.h"
+
+#define NS_WMIMPORT_CID \
+ { /* 42bc82bc-8e9f-4597-8b6e-e529daaf3af1 */ \
+ 0x42bc82bc, 0x8e9f, 0x4597, { \
+ 0x8b, 0x6e, 0xe5, 0x29, 0xda, 0xaf, 0x3a, 0xf1 \
+ } \
+ }
+
+// currently only support setting import
+#define kWMSupportsString NS_IMPORT_SETTINGS_STR
+
+class nsWMImport : public nsIImportModule {
+ public:
+ nsWMImport();
+
+ NS_DECL_ISUPPORTS
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIImportModule interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ NS_DECL_NSIIMPORTMODULE
+
+ protected:
+ virtual ~nsWMImport();
+};
+
+#endif /* nsWMImport_h___ */
diff --git a/comm/mailnews/import/src/nsWMSettings.cpp b/comm/mailnews/import/src/nsWMSettings.cpp
new file mode 100644
index 0000000000..ee741fa053
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMSettings.cpp
@@ -0,0 +1,679 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ Windows Live Mail (Win32) settings
+*/
+
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsString.h"
+#include "nsMsgUtils.h"
+#include "nsWMImport.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgAccount.h"
+#include "nsIImportSettings.h"
+#include "nsWMSettings.h"
+#include "nsMsgI18N.h"
+#include "nsISmtpService.h"
+#include "nsISmtpServer.h"
+#include "nsWMStringBundle.h"
+#include "ImportDebug.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsIImapIncomingServer.h"
+#include "nsINntpIncomingServer.h"
+#include "stdlib.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsTArray.h"
+#include <windows.h>
+#include "nsIWindowsRegKey.h"
+#include "nsCOMArray.h"
+#include "nsWMUtils.h"
+
+class WMSettings {
+ public:
+ static bool DoImport(nsIMsgAccount** ppAccount);
+ static bool DoIMAPServer(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount);
+ static bool DoPOP3Server(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount);
+ static bool DoNNTPServer(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount);
+ static void SetIdentities(nsIMsgAccountManager* pMgr, nsIMsgAccount* pAcc,
+ mozilla::dom::Document* xmlDoc,
+ nsAutoString& userName, int32_t authMethodIncoming,
+ bool isNNTP);
+ static void SetSmtpServer(mozilla::dom::Document* xmlDoc, nsIMsgIdentity* id,
+ nsAutoString& inUserName,
+ int32_t authMethodIncoming);
+};
+
+static int32_t checkNewMailTime; // WM global setting, let's default to 30
+static bool checkNewMail; // WM global setting, let's default to false
+ // This won't cause unwanted autodownloads-
+ // user can set prefs after import
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsWMSettings::Create(nsIImportSettings** aImport) {
+ NS_ENSURE_ARG_POINTER(aImport);
+ NS_ADDREF(*aImport = new nsWMSettings());
+ return NS_OK;
+}
+
+nsWMSettings::nsWMSettings() {}
+
+nsWMSettings::~nsWMSettings() {}
+
+NS_IMPL_ISUPPORTS(nsWMSettings, nsIImportSettings)
+
+NS_IMETHODIMP nsWMSettings::AutoLocate(char16_t** description,
+ nsIFile** location, bool* _retval) {
+ NS_ASSERTION(description != nullptr, "null ptr");
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+ if (!description || !_retval) return NS_ERROR_NULL_POINTER;
+
+ *description = nsWMStringBundle::GetStringByID(WMIMPORT_NAME);
+ *_retval = false;
+
+ if (location) *location = nullptr;
+ nsCOMPtr<nsIWindowsRegKey> key;
+ if (NS_SUCCEEDED(nsWMUtils::FindWMKey(getter_AddRefs(key)))) *_retval = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsWMSettings::SetLocation(nsIFile* location) { return NS_OK; }
+
+NS_IMETHODIMP nsWMSettings::Import(nsIMsgAccount** localMailAccount,
+ bool* _retval) {
+ NS_ASSERTION(_retval != nullptr, "null ptr");
+
+ if (WMSettings::DoImport(localMailAccount)) {
+ *_retval = true;
+ IMPORT_LOG0("Settings import appears successful\n");
+ } else {
+ *_retval = false;
+ IMPORT_LOG0("Settings import returned FALSE\n");
+ }
+
+ return NS_OK;
+}
+
+bool WMSettings::DoImport(nsIMsgAccount** ppAccount) {
+ // do the windows registry stuff first
+ nsCOMPtr<nsIWindowsRegKey> key;
+ if (NS_FAILED(nsWMUtils::FindWMKey(getter_AddRefs(key)))) {
+ IMPORT_LOG0("*** Error finding Windows Live Mail registry account keys\n");
+ return false;
+ }
+ // 'poll for messages' setting in WM is a global setting-Like OE
+ // for all accounts dword ==0xffffffff for don't poll else 1/60000 = minutes
+ checkNewMailTime = 30;
+ checkNewMail = false;
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> subKey;
+ if (NS_SUCCEEDED(key->OpenChild(u"mail"_ns,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE,
+ getter_AddRefs(subKey)))) {
+ uint32_t dwordResult = 0xffffffff;
+ rv = subKey->ReadIntValue(u"Poll For Mail"_ns,
+ &dwordResult); // reg_dword
+ subKey->Close();
+ if (NS_SUCCEEDED(rv) && dwordResult != 0xffffffff) {
+ checkNewMail = true;
+ checkNewMailTime = dwordResult / 60000;
+ }
+ }
+ // these are in main windowsmail key and if they don't exist-not to worry
+ // (less than 64 chars) e.g.
+ // account{4A18B81E-83CA-472A-8D7F-5301C0B97B8D}.oeaccount
+ nsAutoString defMailAcct, defNewsAcct;
+ key->ReadStringValue(u"Default Mail Account"_ns,
+ defMailAcct); // ref_sz
+ key->ReadStringValue(u"Default News Account"_ns,
+ defNewsAcct); // ref_sz
+
+ nsCOMPtr<nsIMsgAccountManager> accMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create an account manager!\n");
+ return false;
+ }
+
+ nsCOMArray<nsIFile> fileArray;
+ if (NS_FAILED(nsWMUtils::GetOEAccountFiles(fileArray))) {
+ IMPORT_LOG0("*** Failed to get .oeaccount file!\n");
+ return false;
+ }
+
+ // Loop through *.oeaccounts files looking for POP3 & IMAP & NNTP accounts
+ // Ignore LDAP for now!
+ int accounts = 0;
+ nsCOMPtr<mozilla::dom::Document> xmlDoc;
+
+ for (int32_t i = fileArray.Count() - 1; i >= 0; i--) {
+ nsWMUtils::MakeXMLdoc(getter_AddRefs(xmlDoc), fileArray[i]);
+
+ nsAutoCString name;
+ fileArray[i]->GetNativeLeafName(name);
+ nsAutoString value;
+ nsCOMPtr<nsIMsgAccount> anAccount;
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "IMAP_Server", value)))
+ if (DoIMAPServer(accMgr, xmlDoc, value, getter_AddRefs(anAccount)))
+ accounts++;
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "NNTP_Server", value)))
+ if (DoNNTPServer(accMgr, xmlDoc, value, getter_AddRefs(anAccount)))
+ accounts++;
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "POP3_Server", value)))
+ if (DoPOP3Server(accMgr, xmlDoc, value, getter_AddRefs(anAccount)))
+ accounts++;
+
+ if (anAccount) {
+ nsString name;
+ // Is this the default account?
+ fileArray[i]->GetLeafName(name);
+ if (defMailAcct.Equals(name)) accMgr->SetDefaultAccount(anAccount);
+ }
+ }
+
+ // Now save the new acct info to pref file.
+ rv = accMgr->SaveAccountInfo();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Can't save account info to pref file");
+
+ return accounts != 0;
+}
+
+bool WMSettings::DoIMAPServer(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount) {
+ int32_t authMethod; // Secure Password Authentication (SPA)
+ nsresult errorCode;
+ if (ppAccount) *ppAccount = nullptr;
+
+ nsAutoString userName, value;
+ if (NS_FAILED(nsWMUtils::GetValueForTag(xmlDoc, "IMAP_User_Name", userName)))
+ return false;
+ bool result = false;
+ // I now have a user name/server name pair, find out if it already exists?
+ nsCOMPtr<nsIMsgIncomingServer> in;
+ nsresult rv = pMgr->FindServer(NS_ConvertUTF16toUTF8(userName),
+ NS_ConvertUTF16toUTF8(serverName), "imap"_ns,
+ 0, getter_AddRefs(in));
+ if (NS_FAILED(rv) || (in == nullptr)) {
+ // Create the incoming server and an account for it?
+ rv = pMgr->CreateIncomingServer(NS_ConvertUTF16toUTF8(userName),
+ NS_ConvertUTF16toUTF8(serverName),
+ "imap"_ns, getter_AddRefs(in));
+ if (NS_SUCCEEDED(rv) && in) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(in);
+ if (!imapServer) {
+ IMPORT_LOG1("*** Failed to create nsIImapIncomingServer for %S!\n",
+ static_cast<const wchar_t*>(serverName.get()));
+ return false;
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "IMAP_Root_Folder", value))) {
+ imapServer->SetServerDirectory(NS_ConvertUTF16toUTF8(value));
+ }
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc, "IMAP_Secure_Connection", value))) {
+ if (value.ToInteger(&errorCode, 16))
+ in->SetSocketType(nsMsgSocketType::SSL);
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "IMAP_Use_Sicily", value))) {
+ bool secAuth = (bool)value.ToInteger(&errorCode, 16);
+ authMethod = secAuth ? nsMsgAuthMethod::secure
+ : nsMsgAuthMethod::passwordCleartext;
+ in->SetAuthMethod(authMethod);
+ }
+
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "IMAP_Port", value))) {
+ in->SetPort(value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "Account_Name", value))) {
+ rv = in->SetPrettyName(value);
+ }
+ in->SetDoBiff(checkNewMail);
+ in->SetBiffMinutes(checkNewMailTime);
+
+ IMPORT_LOG2("Created IMAP server named: %S, userName: %S\n",
+ static_cast<const wchar_t*>(serverName.get()),
+ static_cast<const wchar_t*>(userName.get()));
+
+ // We have a server, create an account.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->CreateAccount(getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ rv = account->SetIncomingServer(in);
+
+ IMPORT_LOG0(
+ "Created an account and set the IMAP server "
+ "as the incoming server\n");
+
+ // Fiddle with the identities
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, false);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ }
+ } else if (NS_SUCCEEDED(rv) && in) {
+ // for an existing server we create another identity,
+ // TB lists under 'manage identities'
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->FindAccountForServer(in, getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ IMPORT_LOG0(
+ "Created an identity and added to existing "
+ "IMAP incoming server\n");
+ // Fiddle with the identities
+ in->GetAuthMethod(&authMethod);
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, false);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ } else
+ result = true;
+ return result;
+}
+
+bool WMSettings::DoPOP3Server(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount) {
+ int32_t authMethod; // Secure Password Authentication (SPA)
+ nsresult errorCode;
+ if (ppAccount) *ppAccount = nullptr;
+
+ nsAutoString userName, value;
+ if (NS_FAILED(nsWMUtils::GetValueForTag(xmlDoc, "POP3_User_Name", userName)))
+ return false;
+ bool result = false;
+ // I now have a user name/server name pair, find out if it already exists?
+ nsCOMPtr<nsIMsgIncomingServer> in;
+ nsresult rv = pMgr->FindServer(NS_ConvertUTF16toUTF8(userName),
+ NS_ConvertUTF16toUTF8(serverName), "pop3"_ns,
+ 0, getter_AddRefs(in));
+ if (NS_FAILED(rv) || (in == nullptr)) {
+ // Create the incoming server and an account for it?
+ rv = pMgr->CreateIncomingServer(NS_ConvertUTF16toUTF8(userName),
+ NS_ConvertUTF16toUTF8(serverName),
+ "pop3"_ns, getter_AddRefs(in));
+ if (NS_SUCCEEDED(rv) && in) {
+ nsCOMPtr<nsIPop3IncomingServer> pop3Server = do_QueryInterface(in);
+ if (!pop3Server) {
+ IMPORT_LOG1("*** Failed to create nsIPop3IncomingServer for %S!\n",
+ static_cast<const wchar_t*>(serverName.get()));
+ return false;
+ }
+
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc, "POP3_Secure_Connection", value)) &&
+ value.ToInteger(&errorCode, 16)) {
+ in->SetSocketType(nsMsgSocketType::SSL);
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "POP3_Use_Sicily", value))) {
+ bool secAuth = (bool)value.ToInteger(&errorCode, 16);
+ authMethod = secAuth ? nsMsgAuthMethod::secure
+ : nsMsgAuthMethod::passwordCleartext;
+ in->SetAuthMethod(authMethod);
+ }
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "POP3_Port", value))) {
+ in->SetPort(value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "POP3_Skip_Account", value))) {
+ if (!value.IsEmpty())
+ // OE:0=='Include this account when receiving mail or synchronizing'==
+ // TB:1==ActMgr:Server:advanced:Include this server when getting new
+ // mail
+ pop3Server->SetDeferGetNewMail(value.ToInteger(&errorCode, 16) == 0);
+ else
+ pop3Server->SetDeferGetNewMail(false);
+ }
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "Leave_Mail_On_Server",
+ value))) {
+ pop3Server->SetLeaveMessagesOnServer(
+ (bool)value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "Remove_When_Deleted",
+ value))) {
+ pop3Server->SetDeleteMailLeftOnServer(
+ (bool)value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "Remove_When_Expired",
+ value))) {
+ pop3Server->SetDeleteByAgeFromServer(
+ (bool)value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "Expire_Days", value))) {
+ pop3Server->SetNumDaysToLeaveOnServer(value.ToInteger(&errorCode, 16));
+ }
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "Account_Name", value))) {
+ rv = in->SetPrettyName(value);
+ }
+
+ in->SetDoBiff(checkNewMail);
+ in->SetBiffMinutes(checkNewMailTime);
+
+ // set local folders as the Inbox to use for this POP3 server
+ nsCOMPtr<nsIMsgIncomingServer> localFoldersServer;
+ pMgr->GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+ if (!localFoldersServer) {
+ // XXX: We may need to move this local folder creation
+ // code to the generic nsImportSettings code
+ // if the other import modules end up needing to do this too.
+ // if Local Folders does not exist already, create it
+ rv = pMgr->CreateLocalMailAccount();
+ if (NS_FAILED(rv)) {
+ IMPORT_LOG0("*** Failed to create Local Folders!\n");
+ return false;
+ }
+ pMgr->GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+ }
+
+ // now get the account for this server
+ nsCOMPtr<nsIMsgAccount> localFoldersAccount;
+ pMgr->FindAccountForServer(localFoldersServer,
+ getter_AddRefs(localFoldersAccount));
+ if (localFoldersAccount) {
+ nsCString localFoldersAcctKey;
+ localFoldersAccount->GetKey(localFoldersAcctKey);
+ pop3Server->SetDeferredToAccount(localFoldersAcctKey);
+ }
+
+ IMPORT_LOG2("Created POP3 server named: %S, userName: %S\n",
+ static_cast<const wchar_t*>(serverName.get()),
+ static_cast<const wchar_t*>(userName.get()));
+
+ // We have a server, create an account.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->CreateAccount(getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ rv = account->SetIncomingServer(in);
+ IMPORT_LOG0(
+ "Created a new account and set the incoming "
+ "server to the POP3 server.\n");
+
+ // Fiddle with the identities
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, false);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ }
+ } else if (NS_SUCCEEDED(rv) && in) {
+ IMPORT_LOG2("Existing POP3 server named: %S, userName: %S\n",
+ static_cast<const wchar_t*>(serverName.get()),
+ static_cast<const wchar_t*>(userName.get()));
+ // for an existing server we create another identity,
+ // TB listed under 'manage identities'
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->FindAccountForServer(in, getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ IMPORT_LOG0(
+ "Created identity and added to existing POP3 incoming server.\n");
+ // Fiddle with the identities
+ in->GetAuthMethod(&authMethod);
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, false);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ } else
+ result = true;
+ return result;
+}
+
+bool WMSettings::DoNNTPServer(nsIMsgAccountManager* pMgr,
+ mozilla::dom::Document* xmlDoc,
+ const nsString& serverName,
+ nsIMsgAccount** ppAccount) {
+ int32_t authMethod;
+ nsresult errorCode;
+ if (ppAccount) *ppAccount = nullptr;
+
+ nsAutoString userName, value;
+ // this only exists if NNTP server requires it or not, anonymous login
+ nsWMUtils::GetValueForTag(xmlDoc, "NNTP_User_Name", userName);
+ bool result = false;
+
+ // I now have a user name/server name pair, find out if it already exists?
+ // NNTP can have empty user name. This is wild card in findserver
+ nsCOMPtr<nsIMsgIncomingServer> in;
+ nsresult rv =
+ pMgr->FindServer(EmptyCString(), NS_ConvertUTF16toUTF8(serverName),
+ "nntp"_ns, 0, getter_AddRefs(in));
+ if (NS_FAILED(rv) || (in == nullptr)) {
+ // Create the incoming server and an account for it?
+ rv = pMgr->CreateIncomingServer(EmptyCString(),
+ NS_ConvertUTF16toUTF8(serverName),
+ "nntp"_ns, getter_AddRefs(in));
+ if (NS_SUCCEEDED(rv) && in) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(in);
+ if (!nntpServer) {
+ IMPORT_LOG1("*** Failed to create nsINnntpIncomingServer for %S!\n",
+ static_cast<const wchar_t*>(serverName.get()));
+ return false;
+ }
+ if (!userName.IsEmpty()) { // if username req'd then auth req'd
+ nntpServer->SetPushAuth(true);
+ in->SetUsername(NS_ConvertUTF16toUTF8(userName));
+ }
+
+ nsAutoString value;
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(xmlDoc, "NNTP_Port", value))) {
+ in->SetPort(value.ToInteger(&errorCode, 16));
+ }
+
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "Account_Name", value))) {
+ in->SetPrettyName(value);
+ }
+
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "NNTP_Use_Sicily", value))) {
+ bool secAuth = (bool)value.ToInteger(&errorCode, 16);
+ authMethod = secAuth ? nsMsgAuthMethod::secure
+ : nsMsgAuthMethod::passwordCleartext;
+ in->SetAuthMethod(authMethod);
+ }
+
+ IMPORT_LOG2("Created NNTP server named: %S, userName: %S\n",
+ static_cast<const wchar_t*>(serverName.get()),
+ static_cast<const wchar_t*>(userName.get()));
+
+ // We have a server, create an account.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->CreateAccount(getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ rv = account->SetIncomingServer(in);
+
+ IMPORT_LOG0(
+ "Created an account and set the NNTP server "
+ "as the incoming server\n");
+
+ // Fiddle with the identities
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, true);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ }
+ } else if (NS_SUCCEEDED(rv) && in) {
+ // for the existing server...
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = pMgr->FindAccountForServer(in, getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ IMPORT_LOG0(
+ "Using existing account and set the "
+ "NNTP server as the incoming server\n");
+ // Fiddle with the identities
+ in->GetAuthMethod(&authMethod);
+ SetIdentities(pMgr, account, xmlDoc, userName, authMethod, true);
+ result = true;
+ if (ppAccount) account.forget(ppAccount);
+ }
+ } else
+ result = true;
+ return result;
+}
+
+void WMSettings::SetIdentities(nsIMsgAccountManager* pMgr, nsIMsgAccount* pAcc,
+ mozilla::dom::Document* xmlDoc,
+ nsAutoString& inUserName,
+ int32_t authMethodIncoming, bool isNNTP) {
+ // Get the relevant information for an identity
+ nsAutoString value;
+
+ nsCOMPtr<nsIMsgIdentity> id;
+ pMgr->CreateIdentity(getter_AddRefs(id));
+ if (id) {
+ IMPORT_LOG0("Created identity and added to the account\n");
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc, isNNTP ? "NNTP_Display_Name" : "SMTP_Display_Name",
+ value))) {
+ id->SetFullName(value);
+ IMPORT_LOG1("\tname: %S\n", static_cast<const wchar_t*>(value.get()));
+ }
+
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc,
+ isNNTP ? "NNTP_Organization_Name" : "SMTP_Organization_Name",
+ value))) {
+ id->SetOrganization(value);
+ }
+
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc, isNNTP ? "NNTP_Email_Address" : "SMTP_Email_Address",
+ value))) {
+ id->SetEmail(NS_ConvertUTF16toUTF8(value));
+ IMPORT_LOG1("\temail: %S\n", static_cast<const wchar_t*>(value.get()));
+ }
+
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc,
+ isNNTP ? "NNTP_Reply_To_Email_Address"
+ : "SMTP_Reply_To_Email_Address",
+ value))) {
+ id->SetReplyTo(NS_ConvertUTF16toUTF8(value));
+ }
+
+ // Windows users are used to top style quoting.
+ id->SetReplyOnTop(isNNTP ? 0 : 1);
+ pAcc->AddIdentity(id);
+ }
+
+ if (!isNNTP) // NNTP does not use SMTP in OE or TB
+ SetSmtpServer(xmlDoc, id, inUserName, authMethodIncoming);
+}
+
+void WMSettings::SetSmtpServer(mozilla::dom::Document* xmlDoc,
+ nsIMsgIdentity* id, nsAutoString& inUserName,
+ int32_t authMethodIncoming) {
+ nsresult errorCode;
+
+ // set the id.smtpserver accordingly
+ if (!id) return;
+ nsCString smtpServerKey, userName;
+ nsAutoString value, smtpName;
+ if (NS_FAILED(nsWMUtils::GetValueForTag(xmlDoc, "SMTP_Server", smtpName)))
+ return;
+
+ // first we have to calculate the smtp user name which is based on sicily
+ // smtp user name depends on sicily which may or not exist
+ int32_t useSicily = 0;
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "SMTP_Use_Sicily", value))) {
+ useSicily = (int32_t)value.ToInteger(&errorCode, 16);
+ }
+ switch (useSicily) {
+ case 1:
+ case 3:
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "SMTP_User_Name", value))) {
+ CopyUTF16toUTF8(value, userName);
+ } else {
+ CopyUTF16toUTF8(inUserName, userName);
+ }
+ break;
+ case 2:
+ CopyUTF16toUTF8(inUserName, userName);
+ break;
+ default:
+ break; // initial userName == ""
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISmtpService> smtpService(
+ do_GetService("@mozilla.org/messengercompose/smtp;1", &rv));
+ if (NS_SUCCEEDED(rv) && smtpService) {
+ nsCOMPtr<nsISmtpServer> extgServer;
+ // don't try to make another server
+ // regardless if username doesn't match
+ rv = smtpService->FindServer(userName.get(),
+ NS_ConvertUTF16toUTF8(smtpName).get(),
+ getter_AddRefs(extgServer));
+ if (NS_SUCCEEDED(rv) && extgServer) {
+ // set our account keyed to this smptserver key
+ extgServer->GetKey(getter_Copies(smtpServerKey));
+ id->SetSmtpServerKey(smtpServerKey);
+
+ IMPORT_LOG1("SMTP server already exists: %s\n",
+ NS_ConvertUTF16toUTF8(smtpName).get());
+ } else {
+ nsCOMPtr<nsISmtpServer> smtpServer;
+ rv = smtpService->CreateServer(getter_AddRefs(smtpServer));
+ if (NS_SUCCEEDED(rv) && smtpServer) {
+ if (NS_SUCCEEDED(
+ nsWMUtils::GetValueForTag(xmlDoc, "SMTP_Port", value))) {
+ smtpServer->SetPort(value.ToInteger(&errorCode, 16));
+ }
+
+ if (NS_SUCCEEDED(nsWMUtils::GetValueForTag(
+ xmlDoc, "SMTP_Secure_Connection", value))) {
+ if (value.ToInteger(&errorCode, 16) == 1)
+ smtpServer->SetSocketType(nsMsgSocketType::SSL);
+ else
+ smtpServer->SetSocketType(nsMsgSocketType::plain);
+ }
+ smtpServer->SetUsername(userName);
+ switch (useSicily) {
+ case 1:
+ smtpServer->SetAuthMethod(nsMsgAuthMethod::secure);
+ break;
+ case 2: // requires SMTP authentication to use the incoming server
+ // settings
+ smtpServer->SetAuthMethod(authMethodIncoming);
+ break;
+ case 3:
+ smtpServer->SetAuthMethod(nsMsgAuthMethod::passwordCleartext);
+ break;
+ default:
+ smtpServer->SetAuthMethod(nsMsgAuthMethod::none);
+ }
+
+ smtpServer->SetHostname(NS_ConvertUTF16toUTF8(smtpName));
+
+ smtpServer->GetKey(getter_Copies(smtpServerKey));
+ id->SetSmtpServerKey(smtpServerKey);
+
+ IMPORT_LOG1("Created new SMTP server: %s\n",
+ NS_ConvertUTF16toUTF8(smtpName).get());
+ }
+ }
+ }
+}
diff --git a/comm/mailnews/import/src/nsWMSettings.h b/comm/mailnews/import/src/nsWMSettings.h
new file mode 100644
index 0000000000..d1b770d0a1
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMSettings.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsWMSettings_h___
+#define nsWMSettings_h___
+
+#include "nsIImportSettings.h"
+
+class nsWMSettings : public nsIImportSettings {
+ public:
+ nsWMSettings();
+ static nsresult Create(nsIImportSettings** aImport);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIMPORTSETTINGS
+
+ private:
+ virtual ~nsWMSettings();
+};
+
+#endif /* nsWMSettings_h___ */
diff --git a/comm/mailnews/import/src/nsWMStringBundle.cpp b/comm/mailnews/import/src/nsWMStringBundle.cpp
new file mode 100644
index 0000000000..bd60597cbc
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMStringBundle.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "prprf.h"
+#include "prmem.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsIStringBundle.h"
+#include "nsWMStringBundle.h"
+#include "mozilla/Components.h"
+
+#define WM_MSGS_URL "chrome://messenger/locale/wmImportMsgs.properties"
+
+nsCOMPtr<nsIStringBundle> nsWMStringBundle::m_pBundle = nullptr;
+
+void nsWMStringBundle::GetStringBundle(void) {
+ if (m_pBundle) return;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ if (sBundleService) {
+ sBundleService->CreateBundle(WM_MSGS_URL, getter_AddRefs(m_pBundle));
+ }
+}
+
+void nsWMStringBundle::GetStringByID(int32_t stringID, nsString& result) {
+ char16_t* ptrv = GetStringByID(stringID);
+ result = ptrv;
+ FreeString(ptrv);
+}
+
+char16_t* nsWMStringBundle::GetStringByID(int32_t stringID) {
+ if (!m_pBundle) GetStringBundle();
+
+ if (m_pBundle) {
+ nsAutoString str;
+ nsresult rv = m_pBundle->GetStringFromID(stringID, str);
+
+ if (NS_SUCCEEDED(rv)) return ToNewUnicode(str);
+ }
+
+ nsString resultString;
+ resultString.AppendLiteral("[StringID ");
+ resultString.AppendInt(stringID);
+ resultString.AppendLiteral("?]");
+
+ return ToNewUnicode(resultString);
+}
+
+void nsWMStringBundle::Cleanup(void) { m_pBundle = nullptr; }
diff --git a/comm/mailnews/import/src/nsWMStringBundle.h b/comm/mailnews/import/src/nsWMStringBundle.h
new file mode 100644
index 0000000000..45c92f75d6
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMStringBundle.h
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#ifndef _nsWMStringBundle_H__
+#define _nsWMStringBundle_H__
+
+#include "nsString.h"
+
+class nsIStringBundle;
+
+class nsWMStringBundle {
+ public:
+ static char16_t* GetStringByID(int32_t stringID);
+ static void GetStringByID(int32_t stringID, nsString& result);
+ static void GetStringBundle(void);
+ static void FreeString(char16_t* pStr) { free(pStr); }
+ static void Cleanup(void);
+
+ private:
+ static nsCOMPtr<nsIStringBundle> m_pBundle;
+};
+
+#define WMIMPORT_NAME 2000
+#define WMIMPORT_DESCRIPTION 2001
+#define WMIMPORT_MAILBOX_SUCCESS 2002
+#define WMIMPORT_MAILBOX_BADPARAM 2003
+#define WMIMPORT_MAILBOX_BADSOURCEFILE 2004
+#define WMIMPORT_MAILBOX_CONVERTERROR 2005
+#define WMIMPORT_DEFAULT_NAME 2006
+#define WMIMPORT_AUTOFIND 2007
+#define WMIMPORT_ADDRESS_SUCCESS 2008
+#define WMIMPORT_ADDRESS_CONVERTERROR 2009
+#define WMIMPORT_ADDRESS_BADPARAM 2010
+
+#endif /* _nsWMStringBundle_H__ */
diff --git a/comm/mailnews/import/src/nsWMUtils.cpp b/comm/mailnews/import/src/nsWMUtils.cpp
new file mode 100644
index 0000000000..0172a30a82
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMUtils.cpp
@@ -0,0 +1,153 @@
+/* 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/. */
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsNetCID.h"
+#include "nsString.h"
+#include "mozilla/dom/Document.h"
+#include "nsWMUtils.h"
+#include "nsINodeList.h"
+#include "nsContentList.h"
+#include "nsINode.h"
+#include "nsIFileStreams.h"
+#include "nsIFile.h"
+#include "nsIDirectoryEnumerator.h"
+#include "ImportDebug.h"
+#include "prio.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/DOMParser.h"
+
+nsresult nsWMUtils::FindWMKey(nsIWindowsRegKey** aKey) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> key =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Software\\Microsoft\\Windows Live Mail"_ns,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ key.forget(aKey);
+ return rv;
+ }
+
+ rv = key->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Software\\Microsoft\\Windows Mail"_ns,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ key.forget(aKey);
+ return rv;
+}
+
+nsresult nsWMUtils::GetRootFolder(nsIFile** aRootFolder) {
+ nsCOMPtr<nsIWindowsRegKey> key;
+ if (NS_FAILED(nsWMUtils::FindWMKey(getter_AddRefs(key)))) {
+ IMPORT_LOG0("*** Error finding Windows Live Mail registry account keys\n");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // This is essential to proceed; it is the location on disk of xml-type
+ // account files; it is in reg_expand_sz so it will need expanding to absolute
+ // path.
+ nsString storeRoot;
+ nsresult rv = key->ReadStringValue(u"Store Root"_ns, storeRoot);
+ key->Close(); // Finished with windows registry key. We do not want to return
+ // before this closing
+ if (NS_FAILED(rv) || storeRoot.IsEmpty()) {
+ IMPORT_LOG0("*** Error finding Windows Live Mail Store Root\n");
+ return rv;
+ }
+
+ uint32_t size =
+ ::ExpandEnvironmentStringsW((LPCWSTR)storeRoot.get(), nullptr, 0);
+ nsString expandedStoreRoot;
+ expandedStoreRoot.SetLength(size - 1);
+ if (expandedStoreRoot.Length() != size - 1) return NS_ERROR_FAILURE;
+ ::ExpandEnvironmentStringsW((LPCWSTR)storeRoot.get(),
+ (LPWSTR)expandedStoreRoot.BeginWriting(), size);
+ storeRoot = expandedStoreRoot;
+
+ nsCOMPtr<nsIFile> rootFolder(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = rootFolder->InitWithPath(storeRoot);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rootFolder.forget(aRootFolder);
+
+ return NS_OK;
+}
+
+nsresult nsWMUtils::GetOEAccountFiles(nsCOMArray<nsIFile>& aFileArray) {
+ nsCOMPtr<nsIFile> rootFolder;
+
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetOEAccountFilesInFolder(rootFolder, aFileArray);
+}
+
+nsresult nsWMUtils::GetOEAccountFilesInFolder(nsIFile* aFolder,
+ nsCOMArray<nsIFile>& aFileArray) {
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ nsresult rv = aFolder->GetDirectoryEntries(getter_AddRefs(entries));
+ if (NS_FAILED(rv) || !entries) return NS_ERROR_FAILURE;
+
+ bool hasMore;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIFile> file;
+ rv = entries->GetNextFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory;
+ rv = file->IsDirectory(&isDirectory);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isDirectory) {
+ GetOEAccountFilesInFolder(file, aFileArray);
+ } else {
+ nsString name;
+ rv = file->GetLeafName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (StringEndsWith(name, u".oeaccount"_ns)) aFileArray.AppendObject(file);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsWMUtils::MakeXMLdoc(mozilla::dom::Document** aXmlDoc,
+ nsIFile* aFile) {
+ nsresult rv;
+ nsCOMPtr<nsIFileInputStream> stream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stream->Init(aFile, PR_RDONLY, -1, 0);
+ mozilla::ErrorResult rv2;
+ RefPtr<mozilla::dom::DOMParser> parser =
+ mozilla::dom::DOMParser::CreateWithoutGlobal(rv2);
+ if (rv2.Failed()) {
+ return rv2.StealNSResult();
+ }
+ int64_t filesize;
+ aFile->GetFileSize(&filesize);
+ nsCOMPtr<mozilla::dom::Document> xmldoc = parser->ParseFromStream(
+ stream, EmptyString(), int32_t(filesize),
+ mozilla::dom::SupportedType::Application_xml, rv2);
+ xmldoc.forget(aXmlDoc);
+ return rv2.StealNSResult();
+}
+
+nsresult nsWMUtils::GetValueForTag(mozilla::dom::Document* aXmlDoc,
+ const char* aTagName, nsAString& aValue) {
+ nsAutoString tagName;
+ tagName.AssignASCII(aTagName);
+ nsCOMPtr<nsINodeList> list = aXmlDoc->GetElementsByTagName(tagName);
+ nsCOMPtr<nsINode> node = list->Item(0);
+ if (!node) return NS_ERROR_FAILURE;
+ mozilla::ErrorResult rv2;
+ node->GetTextContent(aValue, rv2);
+ return rv2.StealNSResult();
+}
diff --git a/comm/mailnews/import/src/nsWMUtils.h b/comm/mailnews/import/src/nsWMUtils.h
new file mode 100644
index 0000000000..02f15c6379
--- /dev/null
+++ b/comm/mailnews/import/src/nsWMUtils.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsWMUtils_h___
+#define nsWMUtils_h___
+
+#include <windows.h>
+#include "nsIWindowsRegKey.h"
+
+class nsWMUtils {
+ public:
+ static nsresult FindWMKey(nsIWindowsRegKey** aKey);
+ static nsresult GetRootFolder(nsIFile** aRootFolder);
+ static nsresult GetOEAccountFiles(nsCOMArray<nsIFile>& aFileArray);
+ static nsresult GetOEAccountFilesInFolder(nsIFile* aFolder,
+ nsCOMArray<nsIFile>& aFileArray);
+ static nsresult MakeXMLdoc(mozilla::dom::Document** aXmlDoc, nsIFile* aFile);
+ static nsresult GetValueForTag(mozilla::dom::Document* aXmlDoc,
+ const char* aTagName, nsAString& aValue);
+};
+
+#endif /* nsWMUtils_h___ */
diff --git a/comm/mailnews/import/src/rtfDecoder.cpp b/comm/mailnews/import/src/rtfDecoder.cpp
new file mode 100644
index 0000000000..86a8151618
--- /dev/null
+++ b/comm/mailnews/import/src/rtfDecoder.cpp
@@ -0,0 +1,561 @@
+/* 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/. */
+
+#include <stack>
+#include <map>
+#include <sstream>
+#include "windows.h"
+#include "rtfDecoder.h"
+
+#define SIZEOF(x) (sizeof(x) / sizeof((x)[0]))
+#define IS_DIGIT(i) ((i) >= '0' && (i) <= '9')
+#define IS_ALPHA(VAL) \
+ (((VAL) >= 'a' && (VAL) <= 'z') || ((VAL) >= 'A' && (VAL) <= 'Z'))
+
+inline int HexToInt(char ch) {
+ switch (ch) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ return ch - '0';
+ case 'A':
+ case 'B':
+ case 'C':
+ case 'D':
+ case 'E':
+ case 'F':
+ return ch - 'A' + 10;
+ case 'a':
+ case 'b':
+ case 'c':
+ case 'd':
+ case 'e':
+ case 'f':
+ return ch - 'a' + 10;
+ default:
+ return 0;
+ }
+}
+
+inline int CharsetToCP(int charset) {
+ // We don't know the Code page for the commented out charsets.
+ switch (charset) {
+ case 0:
+ return 1252; // ANSI
+ case 1:
+ return 0; // Default
+ // case 2: return 42; // Symbol
+ case 2:
+ return 1252; // Symbol
+ case 77:
+ return 10000; // Mac Roman
+ case 78:
+ return 10001; // Mac Shift Jis
+ case 79:
+ return 10003; // Mac Hangul
+ case 80:
+ return 10008; // Mac GB2312
+ case 81:
+ return 10002; // Mac Big5
+ // case 82: Mac Johab (old)
+ case 83:
+ return 10005; // Mac Hebrew
+ case 84:
+ return 10004; // Mac Arabic
+ case 85:
+ return 10006; // Mac Greek
+ case 86:
+ return 10081; // Mac Turkish
+ case 87:
+ return 10021; // Mac Thai
+ case 88:
+ return 10029; // Mac East Europe
+ case 89:
+ return 10007; // Mac Russian
+ case 128:
+ return 932; // Shift JIS
+ case 129:
+ return 949; // Hangul
+ case 130:
+ return 1361; // Johab
+ case 134:
+ return 936; // GB2312
+ case 136:
+ return 950; // Big5
+ case 161:
+ return 1253; // Greek
+ case 162:
+ return 1254; // Turkish
+ case 163:
+ return 1258; // Vietnamese
+ case 177:
+ return 1255; // Hebrew
+ case 178:
+ return 1256; // Arabic
+ // case 179: Arabic Traditional (old)
+ // case 180: Arabic user (old)
+ // case 181: Hebrew user (old)
+ case 186:
+ return 1257; // Baltic
+ case 204:
+ return 1251; // Russian
+ case 222:
+ return 874; // Thai
+ case 238:
+ return 1250; // Eastern European
+ case 254:
+ return 437; // PC 437
+ case 255:
+ return 850; // OEM
+ default:
+ return CP_ACP;
+ }
+}
+
+struct FontInfo {
+ enum Options { has_fcharset = 0x0001, has_cpg = 0x0002 };
+ unsigned int options;
+ int fcharset;
+ unsigned int cpg;
+ FontInfo() : options(0), fcharset(0), cpg(0xFFFFFFFF) {}
+ unsigned int Codepage() {
+ if (options & has_cpg)
+ return cpg;
+ else if (options & has_fcharset)
+ return CharsetToCP(fcharset);
+ else
+ return 0xFFFFFFFF;
+ }
+};
+typedef std::map<int, FontInfo> Fonttbl;
+
+struct LocalState {
+ bool fonttbl; // When fonts are being defined
+ int f; // Index of the font being defined/used; defines the codepage if no
+ // \cpg
+ unsigned int uc; // ucN keyword value; its default is 1
+ unsigned int codepage; // defined by \cpg
+};
+typedef std::stack<LocalState> StateStack;
+
+struct GlobalState {
+ enum Pcdata_state { pcdsno, pcdsin, pcdsfinished };
+ std::istream& stream;
+ Fonttbl fonttbl;
+ StateStack stack;
+ unsigned int codepage; // defined by \ansi, \mac, \pc, \pca, and \ansicpgN
+ int deff;
+ std::stringstream pcdata_a;
+ unsigned int pcdata_a_codepage;
+ Pcdata_state pcdata_a_state;
+
+ explicit GlobalState(std::istream& s)
+ : stream(s), codepage(CP_ACP), deff(-1), pcdata_a_state(pcdsno) {
+ LocalState st;
+ st.fonttbl = false;
+ st.f = -1;
+ st.uc = 1;
+ st.codepage = 0xFFFFFFFF;
+ stack.push(st);
+ }
+ unsigned int GetCurrentCP() {
+ if (stack.top().codepage != 0xFFFFFFFF) // \cpg in use
+ return stack.top().codepage;
+ // \cpg not used; use font settings
+ int f = (stack.top().f != -1) ? stack.top().f : deff;
+ if (f != -1) {
+ Fonttbl::iterator iter = fonttbl.find(f);
+ if (iter != fonttbl.end()) {
+ unsigned int cp = iter->second.Codepage();
+ if (cp != 0xFFFFFFFF) return cp;
+ }
+ }
+ return codepage; // No overrides; use the top-level legacy setting
+ }
+};
+
+struct Keyword {
+ char name[33];
+ bool hasVal;
+ int val;
+};
+
+class Lexem {
+ public:
+ enum Type {
+ ltGroupBegin,
+ ltGroupEnd,
+ ltKeyword,
+ ltPCDATA_A,
+ ltPCDATA_W,
+ ltBDATA,
+ ltEOF,
+ ltError
+ };
+ explicit Lexem(Type t = ltError) : m_type(t) {}
+ Lexem(Lexem& from) {
+ switch (m_type = from.m_type) {
+ case ltKeyword:
+ m_keyword = from.m_keyword;
+ break;
+ case ltPCDATA_A:
+ m_pcdata_a = from.m_pcdata_a;
+ break;
+ case ltPCDATA_W:
+ m_pcdata_w = from.m_pcdata_w;
+ break;
+ case ltBDATA:
+ m_bdata = from.m_bdata; // Move pointers when copying.
+ from.m_type = ltError; // Invalidate the original. Not nice.
+ break;
+ }
+ }
+ ~Lexem() { Clear(); }
+ Lexem& operator=(Lexem& from) {
+ if (&from != this) {
+ Clear();
+ switch (m_type = from.m_type) {
+ case ltKeyword:
+ m_keyword = from.m_keyword;
+ break;
+ case ltPCDATA_A:
+ m_pcdata_a = from.m_pcdata_a;
+ break;
+ case ltPCDATA_W:
+ m_pcdata_w = from.m_pcdata_w;
+ break;
+ case ltBDATA:
+ m_bdata = from.m_bdata; // Move pointers when copying.
+ from.m_type = ltError; // Invalidate the original. Not nice.
+ break;
+ }
+ }
+ return *this;
+ }
+ Type type() const { return m_type; }
+ void SetPCDATA_A(char chdata) {
+ Clear();
+ m_pcdata_a = chdata;
+ m_type = ltPCDATA_A;
+ }
+ void SetPCDATA_W(wchar_t chdata) {
+ Clear();
+ m_pcdata_w = chdata;
+ m_type = ltPCDATA_W;
+ }
+ void SetBDATA(const char* data, int sz) {
+ char* tmp = new char[sz]; // to allow getting the data from itself
+ if (tmp) {
+ memcpy(tmp, data, sz);
+ Clear();
+ m_bdata.data = tmp;
+ m_bdata.sz = sz;
+ m_type = ltBDATA;
+ } else
+ m_type = ltError;
+ }
+ void SetKeyword(const Keyword& src) {
+ Clear();
+ m_type = ltKeyword;
+ m_keyword = src;
+ }
+ void SetKeyword(const char* name, bool hasVal = false, int val = 0) {
+ char tmp[SIZEOF(m_keyword.name)];
+ strncpy(tmp, name,
+ SIZEOF(m_keyword.name) - 1); // to allow copy drom itself
+ tmp[SIZEOF(m_keyword.name) - 1] = 0;
+ Clear();
+ m_type = ltKeyword;
+ memcpy(m_keyword.name, tmp, SIZEOF(m_keyword.name));
+ m_keyword.hasVal = hasVal;
+ m_keyword.val = val;
+ }
+ const char* KeywordName() const {
+ return (m_type == ltKeyword) ? m_keyword.name : 0;
+ }
+ const int* KeywordVal() const {
+ return ((m_type == ltKeyword) && m_keyword.hasVal) ? &m_keyword.val : 0;
+ }
+ char pcdata_a() const { return (m_type == ltPCDATA_A) ? m_pcdata_a : 0; }
+ wchar_t pcdata_w() const { return (m_type == ltPCDATA_W) ? m_pcdata_w : 0; }
+ const char* bdata() const { return (m_type == ltBDATA) ? m_bdata.data : 0; }
+ int bdata_sz() const { return (m_type == ltBDATA) ? m_bdata.sz : 0; }
+ static Lexem eof;
+ static Lexem groupBegin;
+ static Lexem groupEnd;
+ static Lexem error;
+
+ private:
+ struct BDATA {
+ size_t sz;
+ char* data;
+ };
+
+ Type m_type;
+ union {
+ Keyword m_keyword;
+ char m_pcdata_a;
+ wchar_t m_pcdata_w;
+ BDATA m_bdata;
+ };
+ // This function leaves the object in the broken state. Must be followed
+ // by a correct initialization.
+ void Clear() {
+ switch (m_type) {
+ case ltBDATA:
+ delete[] m_bdata.data;
+ break;
+ }
+ // m_type = ltError;
+ }
+};
+
+Lexem Lexem::eof(ltEOF);
+Lexem Lexem::groupBegin(ltGroupBegin);
+Lexem Lexem::groupEnd(ltGroupEnd);
+Lexem Lexem::error(ltError);
+
+// This function moves pos. When calling the function, pos must be next to the
+// backslash; pos must be in the same sequence and before end!
+Keyword GetKeyword(std::istream& stream) {
+ Keyword keyword = {"", false, 0};
+ char ch;
+ if (stream.get(ch).eof()) return keyword;
+ // Control word; maybe delimiter and value
+ if (IS_ALPHA(ch)) {
+ int i = 0;
+ do {
+ // We take up to 32 characters into account, skipping over extra
+ // characters (allowing for some non-conformant implementation).
+ if (i < 32) keyword.name[i++] = ch;
+ } while (!stream.get(ch).eof() && IS_ALPHA(ch));
+ keyword.name[i] = 0; // NULL-terminating
+ if (!stream.eof() && (IS_DIGIT(ch) || (ch == '-'))) { // Value begin
+ keyword.hasVal = true;
+ bool negative = (ch == '-');
+ if (negative) stream.get(ch);
+ i = 0;
+ while (!stream.eof() && IS_DIGIT(ch)) {
+ // We take into account only 10 digits, skip other. Older specs stated
+ // that we must be ready for an arbitrary number of digits.
+ if (i++ < 10) keyword.val = keyword.val * 10 + (ch - '0');
+ stream.get(ch);
+ }
+ if (negative) keyword.val = -keyword.val;
+ }
+ // End of control word; the space is just a delimiter - skip it
+ if (!stream.eof() && !(ch == ' ')) stream.unget();
+ } else { // Control symbol
+ keyword.name[0] = ch;
+ keyword.name[1] = 0;
+ }
+ return keyword;
+}
+
+void GetLexem(std::istream& stream, Lexem& result) {
+ // We always stay at the beginning of the next lexem or a crlf
+ // If it's a brace then it's group begin/end
+ // If it's a backslash -> Preprocess
+ // - if it's a \u or \' -> make UTF16 character
+ // - else it's a keyword -> Process (e.g., remember the codepage)
+ // - (if the keyword is \bin then the following is #BDATA)
+ // If it's some other character -> Preprocess
+ // - if it's 0x09 -> it's the keyword \tab
+ // - else it's a PCDATA
+ char ch;
+ while (!stream.get(ch).eof() && ((ch == '\n') || (ch == '\r')))
+ ; // Skip crlf
+ if (stream.eof())
+ result = Lexem::eof;
+ else {
+ switch (ch) {
+ case '{': // Group begin
+ case '}': // Group end
+ result = (ch == '{') ? Lexem::groupBegin : Lexem::groupEnd;
+ break;
+ case '\\': // Keyword
+ result.SetKeyword(GetKeyword(stream));
+ break;
+ case '\t': // tab
+ result.SetKeyword("tab");
+ break;
+ default: // PSDATA?
+ result.SetPCDATA_A(ch);
+ break;
+ }
+ }
+}
+
+void PreprocessLexem(/*inout*/ Lexem& lexem, std::istream& stream, int uc) {
+ if (lexem.type() == Lexem::ltKeyword) {
+ if (lexem.KeywordName()[0] == 0) // Empty keyword - maybe eof?
+ lexem = Lexem::error;
+ else if (eq(lexem.KeywordName(), "u")) {
+ // Unicode character - get the UTF16 and skip the uc characters
+ if (const int* val = lexem.KeywordVal()) {
+ lexem.SetPCDATA_W(*val);
+ stream.ignore(uc);
+ } else
+ lexem = Lexem::error;
+ } else if (eq(lexem.KeywordName(), "'")) {
+ // 8-bit character (\'hh) -> use current codepage
+ char ch = 0, ch1 = 0;
+ if (!stream.get(ch).eof()) ch1 = HexToInt(ch);
+ if (!stream.get(ch).eof()) (ch1 <<= 4) += HexToInt(ch);
+ lexem.SetPCDATA_A(ch1);
+ } else if (eq(lexem.KeywordName(), "\\") || eq(lexem.KeywordName(), "{") ||
+ eq(lexem.KeywordName(), "}")) // escaped characters
+ lexem.SetPCDATA_A(lexem.KeywordName()[0]);
+ else if (eq(lexem.KeywordName(), "bin")) {
+ if (const int* i = lexem.KeywordVal()) {
+ char* data = new char[*i];
+ if (data) {
+ stream.read(data, *i);
+ if (stream.fail())
+ lexem = Lexem::error;
+ else
+ lexem.SetBDATA(data, *i);
+ delete[] data;
+ } else
+ lexem = Lexem::error;
+ } else
+ lexem = Lexem::error;
+ } else if (eq(lexem.KeywordName(), "\n") || eq(lexem.KeywordName(), "\r")) {
+ // escaped cr or lf
+ lexem.SetKeyword("par");
+ }
+ }
+}
+
+void UpdateState(const Lexem& lexem, /*inout*/ GlobalState& globalState) {
+ switch (globalState.pcdata_a_state) {
+ case GlobalState::pcdsfinished: // Last time we finished the pcdata
+ globalState.pcdata_a_state = GlobalState::pcdsno;
+ break;
+ case GlobalState::pcdsin:
+ // to be reset later if still in the pcdata
+ globalState.pcdata_a_state = GlobalState::pcdsfinished;
+ break;
+ }
+
+ switch (lexem.type()) {
+ case Lexem::ltGroupBegin:
+ globalState.stack.push(globalState.stack.top());
+ break;
+ case Lexem::ltGroupEnd:
+ globalState.stack.pop();
+ break;
+ case Lexem::ltKeyword: {
+ const int* val = lexem.KeywordVal();
+ if (eq(lexem.KeywordName(), "ansi"))
+ globalState.codepage = CP_ACP;
+ else if (eq(lexem.KeywordName(), "mac"))
+ globalState.codepage = CP_MACCP;
+ else if (eq(lexem.KeywordName(), "pc"))
+ globalState.codepage = 437;
+ else if (eq(lexem.KeywordName(), "pca"))
+ globalState.codepage = 850;
+ else if (eq(lexem.KeywordName(), "ansicpg") && val)
+ globalState.codepage = static_cast<unsigned int>(*val);
+ else if (eq(lexem.KeywordName(), "deff") && val)
+ globalState.deff = *val;
+ else if (eq(lexem.KeywordName(), "fonttbl"))
+ globalState.stack.top().fonttbl = true;
+ else if (eq(lexem.KeywordName(), "f") && val) {
+ globalState.stack.top().f = *val;
+ } else if (eq(lexem.KeywordName(), "fcharset") &&
+ globalState.stack.top().fonttbl &&
+ (globalState.stack.top().f != -1) && val) {
+ FontInfo& f = globalState.fonttbl[globalState.stack.top().f];
+ f.options |= FontInfo::has_fcharset;
+ f.fcharset = *val;
+ } else if (eq(lexem.KeywordName(), "cpg") && val) {
+ if (globalState.stack.top().fonttbl &&
+ (globalState.stack.top().f != -1)) { // Defining a font
+ FontInfo& f = globalState.fonttbl[globalState.stack.top().f];
+ f.options |= FontInfo::has_cpg;
+ f.cpg = *val;
+ } else { // Overriding the codepage for the block - may be in filenames
+ globalState.stack.top().codepage = *val;
+ }
+ } else if (eq(lexem.KeywordName(), "plain"))
+ globalState.stack.top().f = -1;
+ else if (eq(lexem.KeywordName(), "uc") && val)
+ globalState.stack.top().uc = *val;
+ } break;
+ case Lexem::ltPCDATA_A:
+ if (globalState.pcdata_a_state ==
+ GlobalState::pcdsno) // Beginning of the pcdata
+ globalState.pcdata_a_codepage =
+ globalState.GetCurrentCP(); // to use later to convert to utf16
+ globalState.pcdata_a_state = GlobalState::pcdsin;
+ globalState.pcdata_a << lexem.pcdata_a();
+ break;
+ }
+}
+
+void DecodeRTF(std::istream& rtf, CRTFDecoder& decoder) {
+ // Check if this is the rtf
+ Lexem lexem;
+ GetLexem(rtf, lexem);
+ if (lexem.type() != Lexem::ltGroupBegin) return;
+ decoder.BeginGroup();
+ GetLexem(rtf, lexem);
+ if ((lexem.type() != Lexem::ltKeyword) || !eq(lexem.KeywordName(), "rtf") ||
+ !lexem.KeywordVal() || (*lexem.KeywordVal() != 1))
+ return;
+ decoder.Keyword(lexem.KeywordName(), lexem.KeywordVal());
+
+ GlobalState state(rtf);
+ // Level is the count of elements in the stack
+
+ while (!state.stream.eof() &&
+ (state.stack.size() > 0)) { // Don't go past the global group
+ GetLexem(state.stream, lexem);
+ PreprocessLexem(lexem, state.stream, state.stack.top().uc);
+ UpdateState(lexem, state);
+
+ if (state.pcdata_a_state == GlobalState::pcdsfinished) {
+ std::string s = state.pcdata_a.str();
+ int sz = ::MultiByteToWideChar(state.pcdata_a_codepage, 0, s.c_str(),
+ s.size(), 0, 0);
+ if (sz) {
+ wchar_t* data = new wchar_t[sz];
+ ::MultiByteToWideChar(state.pcdata_a_codepage, 0, s.c_str(), s.size(),
+ data, sz);
+ decoder.PCDATA(data, sz);
+ delete[] data;
+ }
+ state.pcdata_a.str(""); // reset
+ }
+
+ switch (lexem.type()) {
+ case Lexem::ltGroupBegin:
+ decoder.BeginGroup();
+ break;
+ case Lexem::ltGroupEnd:
+ decoder.EndGroup();
+ break;
+ case Lexem::ltKeyword:
+ decoder.Keyword(lexem.KeywordName(), lexem.KeywordVal());
+ break;
+ case Lexem::ltPCDATA_W: {
+ wchar_t ch = lexem.pcdata_w();
+ decoder.PCDATA(&ch, 1);
+ } break;
+ case Lexem::ltBDATA:
+ decoder.BDATA(lexem.bdata(), lexem.bdata_sz());
+ break;
+ case Lexem::ltError:
+ break; // Just silently skip the erroneous data - basic error recovery
+ }
+ } // while
+} // DecodeRTF
diff --git a/comm/mailnews/import/src/rtfDecoder.h b/comm/mailnews/import/src/rtfDecoder.h
new file mode 100644
index 0000000000..1b547c77b8
--- /dev/null
+++ b/comm/mailnews/import/src/rtfDecoder.h
@@ -0,0 +1,21 @@
+/* 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/. */
+
+#include <istream>
+
+template <size_t len>
+inline bool eq(const char* str1, const char (&str2)[len]) {
+ return ::strncmp(str1, str2, len) == 0;
+};
+
+class CRTFDecoder {
+ public:
+ virtual void BeginGroup() = 0;
+ virtual void EndGroup() = 0;
+ virtual void Keyword(const char* name, const int* Val) = 0;
+ virtual void PCDATA(const wchar_t* data, size_t cch) = 0;
+ virtual void BDATA(const char* data, size_t sz) = 0;
+};
+
+void DecodeRTF(std::istream& rtf, CRTFDecoder& decoder);
diff --git a/comm/mailnews/import/src/rtfMailDecoder.cpp b/comm/mailnews/import/src/rtfMailDecoder.cpp
new file mode 100644
index 0000000000..c5a234320e
--- /dev/null
+++ b/comm/mailnews/import/src/rtfMailDecoder.cpp
@@ -0,0 +1,71 @@
+/* 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/. */
+
+#include "rtfMailDecoder.h"
+
+void CRTFMailDecoder::BeginGroup() {
+ ClearState(sAsterisk);
+ SetState(sBeginGroup);
+ if (m_skipLevel) ++m_skipLevel;
+}
+
+void CRTFMailDecoder::EndGroup() {
+ ClearState(sAsterisk | sBeginGroup);
+ if (m_skipLevel) --m_skipLevel;
+}
+
+void CRTFMailDecoder::AddText(const wchar_t* txt, size_t cch) {
+ if (!IsHtmlRtf()) {
+ if (cch == static_cast<size_t>(-1))
+ m_text += txt;
+ else
+ m_text.append(txt, cch);
+ }
+}
+
+void CRTFMailDecoder::Keyword(const char* name, const int* Val) {
+ bool asterisk = IsAsterisk();
+ ClearState(sAsterisk); // for inside use only
+ bool beginGroup = IsBeginGroup();
+ ClearState(sBeginGroup); // for inside use only
+ if (!m_skipLevel) {
+ if (eq(name, "*") && beginGroup)
+ SetState(sAsterisk);
+ else if (asterisk) {
+ if (eq(name, "htmltag") &&
+ (m_mode ==
+ mHTML)) { // \*\htmltag -> don't ignore; include the following text
+ } else
+ ++m_skipLevel;
+ } else if (eq(name, "htmlrtf")) {
+ if (Val && (*Val == 0))
+ ClearState(sHtmlRtf);
+ else
+ SetState(sHtmlRtf);
+ } else if (eq(name, "par") || eq(name, "line")) {
+ AddText(L"\r\n");
+ } else if (eq(name, "tab")) {
+ AddText(L"\t");
+ } else if (eq(name, "rquote")) {
+ AddText(L"\x2019"); // Unicode right single quotation mark
+ } else if (eq(name, "fromtext") &&
+ (m_mode == mNone)) { // avoid double "fromX"
+ m_mode = mText;
+ } else if (eq(name, "fromhtml") &&
+ (m_mode == mNone)) { // avoid double "fromX"
+ m_mode = mHTML;
+ } else if (eq(name, "fonttbl") || eq(name, "colortbl") ||
+ eq(name, "stylesheet") || eq(name, "pntext"))
+ ++m_skipLevel;
+ }
+}
+
+void CRTFMailDecoder::PCDATA(const wchar_t* data, size_t cch) {
+ ClearState(sAsterisk | sBeginGroup);
+ if (!m_skipLevel) AddText(data, cch);
+}
+
+void CRTFMailDecoder::BDATA(const char* data, size_t sz) {
+ ClearState(sAsterisk | sBeginGroup);
+}
diff --git a/comm/mailnews/import/src/rtfMailDecoder.h b/comm/mailnews/import/src/rtfMailDecoder.h
new file mode 100644
index 0000000000..7f4063c5ae
--- /dev/null
+++ b/comm/mailnews/import/src/rtfMailDecoder.h
@@ -0,0 +1,44 @@
+/* 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/. */
+
+#include "mozilla/Attributes.h"
+#include <string>
+#include "rtfDecoder.h"
+
+class CRTFMailDecoder : public CRTFDecoder {
+ public:
+ enum Mode { mNone, mText, mHTML };
+ CRTFMailDecoder() : m_mode(mNone), m_state(sNormal), m_skipLevel(0) {}
+ void BeginGroup() override;
+ void EndGroup() override;
+ void Keyword(const char* name, const int* Val) override;
+ void PCDATA(const wchar_t* data, size_t cch) override;
+ void BDATA(const char* data, size_t sz) override;
+ const wchar_t* text() { return m_text.c_str(); }
+ std::wstring::size_type textSize() { return m_text.size(); }
+ Mode mode() { return m_mode; }
+
+ private:
+ enum State {
+ sNormal = 0x0000,
+ sBeginGroup = 0x0001,
+ sAsterisk = 0x0002,
+ sHtmlRtf = 0x0004
+ };
+
+ std::wstring m_text;
+ Mode m_mode;
+ unsigned int m_state; // bitmask of State
+ // bool m_beginGroup; // true just after the {
+ // bool m_asterisk; // true just after the {\*
+ int m_skipLevel; // if >0 then we ignore everything
+ // bool m_htmlrtf;
+ inline void SetState(unsigned int s) { m_state |= s; }
+ inline void ClearState(unsigned int s) { m_state &= ~s; }
+ inline bool CheckState(State s) { return (m_state & s) != 0; }
+ inline bool IsAsterisk() { return CheckState(sAsterisk); }
+ inline bool IsBeginGroup() { return CheckState(sBeginGroup); }
+ inline bool IsHtmlRtf() { return CheckState(sHtmlRtf); }
+ void AddText(const wchar_t* txt, size_t cch = static_cast<size_t>(-1));
+};
diff --git a/comm/mailnews/import/test/moz.build b/comm/mailnews/import/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/import/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/import/test/unit/head_import.js b/comm/mailnews/import/test/unit/head_import.js
new file mode 100644
index 0000000000..19be6a2662
--- /dev/null
+++ b/comm/mailnews/import/test/unit/head_import.js
@@ -0,0 +1,23 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+// Import the required setup scripts.
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+// Import the script with basic import functions
+/* import-globals-from resources/import_helper.js */
+load("resources/import_helper.js");
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/import/test/unit/resources/AB_README b/comm/mailnews/import/test/unit/resources/AB_README
new file mode 100644
index 0000000000..3cbeb09508
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/AB_README
@@ -0,0 +1,39 @@
+To test importing an address book, make a new file in the
+/import/test/unit directory with the prefix test_ in the filename
+(ex. test_ldif_import.js).
+
+It should have a function named run_test with no parameters. If you are using
+import_helper.js, which is already imported, you must at least get the file
+to import and make a new AbImportHelper object with at least the file and type
+of import. Call the beginImport method on the object when you are ready to
+start the import.
+
+If you would like the results of the import checked, make sure to update
+addressbook.json. This file is read by import_helper.js to compare the address
+book cards imported to an array of "cards" in in this file. When making a new
+import, first chose a name for the array (like basic_addressbook) to store the
+cards that should be in the newly-imported address book. The properties and
+values of each object in the array should identical to the properties and values
+of the newly-imported card(s) and the cards themselves need to be in the
+expected order. If a card to be imported does not have a property, do not
+include it in the JSON card. Multiple types of imports can be tested with one
+array, as only the supported attributes are checked.
+
+You will also need to give the AbImportHelper constructor two additional
+parameters: the name the imported address book will have (the filename without
+the extension) and the name you chose for the JSON object.
+
+Here is a sample LDIF unit test that doesn't check the results:
+function run_test()
+{
+ var file = do_get_file("resources/basic_ldif_addressbook.ldif");
+ new AbImportHelper(file, "Text file").beginImport();
+}
+
+Here is a sample CSV unit test that checks the results:
+function run_test()
+{
+ var file = do_get_file("resources/basic_csv_addressbook.csv");
+ new AbImportHelper(file, "Text file", "basic_csv_addressbook",
+ "basic_addressbook").beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/resources/WindowsLiveMail/MicrosoftCommunities/account{2E23}.oeaccount b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/MicrosoftCommunities/account{2E23}.oeaccount
new file mode 100644
index 0000000000..b1b278658a
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/MicrosoftCommunities/account{2E23}.oeaccount
Binary files differ
diff --git a/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallimap/donhallimap{testimap}.oeaccount b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallimap/donhallimap{testimap}.oeaccount
new file mode 100644
index 0000000000..c23cf25d17
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallimap/donhallimap{testimap}.oeaccount
Binary files differ
diff --git a/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallnntp/donhallnntp{testnntp}.oeaccount b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallnntp/donhallnntp{testnntp}.oeaccount
new file mode 100644
index 0000000000..65872dcdbe
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/donhallnntp/donhallnntp{testnntp}.oeaccount
Binary files differ
diff --git a/comm/mailnews/import/test/unit/resources/WindowsLiveMail/news.mozilla.org/account{B3B3}.oeaccount b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/news.mozilla.org/account{B3B3}.oeaccount
new file mode 100644
index 0000000000..f17de718ae
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/news.mozilla.org/account{B3B3}.oeaccount
Binary files differ
diff --git a/comm/mailnews/import/test/unit/resources/WindowsLiveMail/pop3.test.test/account{D244}.oeaccount b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/pop3.test.test/account{D244}.oeaccount
new file mode 100644
index 0000000000..5ec08c4945
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/WindowsLiveMail/pop3.test.test/account{D244}.oeaccount
Binary files differ
diff --git a/comm/mailnews/import/test/unit/resources/addressbook.json b/comm/mailnews/import/test/unit/resources/addressbook.json
new file mode 100644
index 0000000000..c9545d3027
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/addressbook.json
@@ -0,0 +1,170 @@
+{
+ "basic_addressbook": [
+ {
+ "DisplayName": "Display Name",
+ "PrimaryEmail": "primaryemail@host.invalid",
+ "FirstName": "First",
+ "LastName": "Last",
+ "NickName": "Nickname",
+ "SecondEmail": "secondemail@host.invalid",
+ "_AimScreenName": "screenname",
+ "LastModifiedDate": 1213818826,
+ "_vCard": [
+ "VERSION:4.0",
+ "FN:Display Name",
+ "EMAIL;PREF=1:primaryemail@host.invalid",
+ "EMAIL:secondemail@host.invalid",
+ "NICKNAME:Nickname",
+ "NOTE:Notes line 1\\nNotes line 2\\nNotes line 3\\nNotes line 4",
+ "ORG:Organization Name;Department",
+ "TITLE:Job Title",
+ "BDAY;VALUE=DATE:19000102",
+ "N:Last;First;;;",
+ "ADR;TYPE=home:;Home Address Line 2;Home Address Line 1;Home City;Home State",
+ " ;Home Zip;Home Country",
+ "ADR;TYPE=work:;Work Address Line 2;Work Address Line 1;Work City;Work State",
+ " ;Work Zip;Work Country",
+ "TEL;TYPE=home;VALUE=TEXT:234-567-8901",
+ "TEL;TYPE=work;VALUE=TEXT:123-456-7890",
+ "TEL;TYPE=fax;VALUE=TEXT:345-678-9012",
+ "TEL;TYPE=pager;VALUE=TEXT:456-789-0123",
+ "TEL;TYPE=cell;VALUE=TEXT:567-890-1234",
+ "URL;TYPE=work;VALUE=URL:http://127.0.0.1",
+ "URL;TYPE=home;VALUE=URL:http://localhost",
+ "X-CUSTOM1;VALUE=TEXT:Custom Field 1",
+ "X-CUSTOM2;VALUE=TEXT:Custom Field 2",
+ "X-CUSTOM3;VALUE=TEXT:Custom Field 3",
+ "X-CUSTOM4;VALUE=TEXT:Custom Field 4",
+ "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ ]
+ }
+ ],
+ "bug_263304": [
+ {
+ "DisplayName": "Display Name",
+ "PrimaryEmail": "primaryemail@host.invalid",
+ "_vCard": [
+ "VERSION:4.0",
+ "FN:Display Name",
+ "EMAIL;PREF=1:primaryemail@host.invalid",
+ "URL;TYPE=work;VALUE=URL:http://127.0.0.1",
+ "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ ]
+ }
+ ],
+ "utf16_csv": [
+ {
+ "DisplayName": "John Doe",
+ "PrimaryEmail": "johndoe@host.invalid",
+ "FirstName": "John",
+ "LastName": "Doe"
+ }
+ ],
+ "shiftjis_csv": [
+ {
+ "DisplayName": "åç„¡ã—ã®æ¨©å…µè¡›",
+ "PrimaryEmail": "åç„¡ã—ã®æ¨©å…µè¡›@host.invalid"
+ }
+ ],
+ "quote_csv": [
+ {
+ "DisplayName": "Acer America",
+ "_vCard": [
+ "VERSION:4.0",
+ "FN:Acer America",
+ "ORG:Acer America;",
+ "TEL;TYPE=work;VALUE=TEXT:(800) 000-0000",
+ "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ ]
+ }
+ ],
+ "vcard_import": [
+ {
+ "DisplayName": "John Doe",
+ "FirstName": "John",
+ "LastName": "Doe",
+ "PrimaryEmail": "john.doe@genericemail.invalid"
+ },
+ {
+ "DisplayName": "Jane Doe",
+ "FirstName": "Jane",
+ "LastName": "Doe",
+ "PrimaryEmail": "jane.doe@genericemail.invalid"
+ }
+ ],
+ "dos_vcard_import": [
+ {
+ "DisplayName": "Name Surname",
+ "FirstName": "Name",
+ "LastName": "Surname",
+ "PrimaryEmail": "example@gmail.com"
+ }
+ ],
+ "csv_import": [
+ {
+ "DisplayName": "John Doe",
+ "FirstName": "John",
+ "LastName": "Doe",
+ "PrimaryEmail": "john@doe.invalid"
+ },
+ {
+ "DisplayName": "Jane Doe",
+ "FirstName": "Jane",
+ "LastName": "Doe",
+ "PrimaryEmail": "jane@doe.invalid"
+ }
+ ],
+ "becky_addressbook": [
+ {
+ "DisplayName": "The first man",
+ "PrimaryEmail": "first@host.invalid",
+ "_vCard": [
+ "VERSION:3.0",
+ "FN:The first man",
+ "ORG:Organization;Post;",
+ "X-BECKY-IMAGE:0",
+ "N:The nick name of the first man",
+ "TEL;TYPE=HOME:11-1111-1111",
+ "TEL;TYPE=WORK:22-2222-2222",
+ "TEL;TYPE=CELL:333-3333-3333",
+ "EMAIL;TYPE=INTERNET:first@host.invalid",
+ "NOTE:This is a note.",
+ "UID:4E4D17E8.0043655C"
+ ]
+ },
+ {
+ "DisplayName": "The second man",
+ "PrimaryEmail": "second@host.invalid",
+ "_vCard": [
+ "VERSION:3.0",
+ "FN:The second man",
+ "ORG:Organization;post;",
+ "X-BECKY-IMAGE:0",
+ "N:The nick name of the second man",
+ "TEL;TYPE=HOME:44-4444-4444",
+ "TEL;TYPE=WORK:55-5555-5555",
+ "TEL;TYPE=CELL:666-6666-6666",
+ "EMAIL;TYPE=INTERNET:second@host.invalid",
+ "NOTE:This is a note.",
+ "UID:4EBBF6FE.00AC632B"
+ ]
+ },
+ {
+ "DisplayName": "The third man",
+ "PrimaryEmail": "third@host.invalid",
+ "_vCard": [
+ "VERSION:3.0",
+ "FN:The third man",
+ "ORG:Organization;post;",
+ "X-BECKY-IMAGE:0",
+ "N:The third man",
+ "TEL;TYPE=HOME:77-7777-7777",
+ "TEL;TYPE=WORK:88-8888-8888",
+ "TEL;TYPE=CELL:999-9999-9999",
+ "EMAIL;TYPE=INTERNET:third@host.invalid",
+ "NOTE:This is a note.",
+ "UID:4E57AB44.0001D53E"
+ ]
+ }
+ ]
+}
diff --git a/comm/mailnews/import/test/unit/resources/basic_addressbook.csv b/comm/mailnews/import/test/unit/resources/basic_addressbook.csv
new file mode 100644
index 0000000000..5d97d36f17
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/basic_addressbook.csv
@@ -0,0 +1,2 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes,Screen Name,
+John,Doe,John Doe,,johndoe@host.invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
diff --git a/comm/mailnews/import/test/unit/resources/basic_csv_addressbook.csv b/comm/mailnews/import/test/unit/resources/basic_csv_addressbook.csv
new file mode 100644
index 0000000000..d4ecd1a308
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/basic_csv_addressbook.csv
@@ -0,0 +1,3 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes,Screen Name
+John,Doe,John Doe,,john@doe.invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+Jane,Doe,Jane Doe,,jane@doe.invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
diff --git a/comm/mailnews/import/test/unit/resources/basic_ldif_addressbook.ldif b/comm/mailnews/import/test/unit/resources/basic_ldif_addressbook.ldif
new file mode 100644
index 0000000000..cf8ebab355
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/basic_ldif_addressbook.ldif
@@ -0,0 +1,45 @@
+dn: cn=Display Name,mail=primaryemail@host.invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: First
+sn: Last
+cn: Display Name
+mozillaNickname: Nickname
+mail: primaryemail@host.invalid
+mozillaSecondEmail: secondemail@host.invalid
+nsAIMid: screenname
+mozillaUseHtmlMail: true
+modifytimestamp: 1213818826
+telephoneNumber: 123-456-7890
+homePhone: 234-567-8901
+fax: 345-678-9012
+pager: 456-789-0123
+mobile: 567-890-1234
+mozillaHomeStreet: Home Address Line 1
+mozillaHomeStreet2: Home Address Line 2
+mozillaHomeLocalityName: Home City
+mozillaHomeState: Home State
+mozillaHomePostalCode: Home Zip
+mozillaHomeCountryName: Home Country
+street: Work Address Line 1
+mozillaWorkStreet2: Work Address Line 2
+l: Work City
+st: Work State
+postalCode: Work Zip
+c: Work Country
+title: Job Title
+ou: Department
+o: Organization Name
+mozillaWorkUrl: http://127.0.0.1
+mozillaHomeUrl: http://localhost
+birthyear: 1900
+birthmonth: 1
+birthday: 2
+mozillaCustom1: Custom Field 1
+mozillaCustom2: Custom Field 2
+mozillaCustom3: Custom Field 3
+mozillaCustom4: Custom Field 4
+description:: Tm90ZXMgbGluZSAxCk5vdGVzIGxpbmUgMgpOb3RlcyBsaW5lIDMKTm90ZXMgbGluZSA0
diff --git a/comm/mailnews/import/test/unit/resources/basic_vcard_addressbook.vcf b/comm/mailnews/import/test/unit/resources/basic_vcard_addressbook.vcf
new file mode 100644
index 0000000000..7232bd6f23
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/basic_vcard_addressbook.vcf
@@ -0,0 +1,12 @@
+BEGIN:VCARD
+VERSION:2.1
+FN:John Doe
+N:Doe;John;;;
+EMAIL;TYPE=INTERNET:john.doe@genericemail.invalid
+END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+FN:Jane Doe
+N:Doe;Jane;;;
+EMAIL;TYPE=INTERNET:jane.doe@genericemail.invalid
+END:VCARD
diff --git a/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186e.bab b/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186e.bab
new file mode 100644
index 0000000000..2b7e4de7e9
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186e.bab
@@ -0,0 +1,26 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:4E4D17E8.0043655C
+FN:The first man
+ORG:Organization;Post;
+X-BECKY-IMAGE:0
+N:The nick name of the first man
+TEL;TYPE=HOME:11-1111-1111
+TEL;TYPE=WORK:22-2222-2222
+TEL;TYPE=CELL:333-3333-3333
+EMAIL;TYPE=INTERNET;PREF:first@host.invalid
+NOTE;ENCODING=QUOTED-PRINTABLE:This is a note.
+END:VCARD
+BEGIN:VCARD
+VERSION:3.0
+UID:4EBBF6FE.00AC632B
+FN:The second man
+ORG:Organization;post;
+X-BECKY-IMAGE:0
+N:The nick name of the second man
+TEL;TYPE=HOME:44-4444-4444
+TEL;TYPE=WORK:55-5555-5555
+TEL;TYPE=CELL:666-6666-6666
+EMAIL;TYPE=INTERNET;PREF:second@host.invalid
+NOTE;ENCODING=QUOTED-PRINTABLE:This is a note.
+END:VCARD
diff --git a/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186f.bab b/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186f.bab
new file mode 100644
index 0000000000..13df134ef8
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/becky/addressbooks/4e4d186f.bab
@@ -0,0 +1,13 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:4E57AB44.0001D53E
+FN:The third man
+ORG:Organization;post;
+X-BECKY-IMAGE:0
+N:The third man
+TEL;TYPE=HOME:77-7777-7777
+TEL;TYPE=WORK:88-8888-8888
+TEL;TYPE=CELL:999-9999-9999
+EMAIL;TYPE=INTERNET;PREF:third@host.invalid
+NOTE;ENCODING=QUOTED-PRINTABLE:This is a note.
+END:VCARD
diff --git a/comm/mailnews/import/test/unit/resources/becky/addressbooks/do_not_import_this.nobab b/comm/mailnews/import/test/unit/resources/becky/addressbooks/do_not_import_this.nobab
new file mode 100644
index 0000000000..3a49cbfbf7
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/becky/addressbooks/do_not_import_this.nobab
@@ -0,0 +1,12 @@
+BEGIN:VCARD
+UID:4E4D17E8.0043655C
+FN:Nobody
+ORG:Organization;post;
+X-BECKY-IMAGE:0
+N:Nobody
+TEL;HOME:00-0000-0000
+TEL;WORK:11-1111-1111
+TEL;CELL:090-0000-0000
+EMAIL;PREF:nobody@example.com
+NOTE;ENCODING=QUOTED-PRINTABLE:This is a note.
+END:VCARD
diff --git a/comm/mailnews/import/test/unit/resources/becky/filters/IFilter.def b/comm/mailnews/import/test/unit/resources/becky/filters/IFilter.def
new file mode 100644
index 0000000000..0777e163c1
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/becky/filters/IFilter.def
@@ -0,0 +1,23 @@
+Version=1
+AutoSorting=1
+OnlyRead=0
+OnlyOneFolder=1
+:Begin ""
+!M:11111111.mb\!!!!Inbox\Sub11_OR_12\
+@0:Subject:Subject11 O I
+@0:Subject:Subject12 O I
+$O:Sort=1
+:End ""
+:Begin ""
+!M:11111111.mb\!!!!Inbox\From11_OR_12\
+@0:From:From11@example.com O I
+@0:From:From12@example.com O I
+$O:Sort=1
+:End ""
+:Begin ""
+!M:11111111.mb\!!!!Inbox\Sub12+From11\
+@0:Subject:Subject12 O I
+@1:From:From11@example.com O I
+$O:Sort=1
+$X:disabled
+:End ""
diff --git a/comm/mailnews/import/test/unit/resources/becky/filters/OFilter.def b/comm/mailnews/import/test/unit/resources/becky/filters/OFilter.def
new file mode 100644
index 0000000000..bf7d184ff5
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/becky/filters/OFilter.def
@@ -0,0 +1,22 @@
+Version=1
+AutoSorting=1
+OnlyRead=0
+OnlyOneFolder=1
+:Begin ""
+!M:11111111.mb\!!!!Outbox\!!!Sent\Sub21_OR_22\
+@0:Subject:Subject21 O IT
+@0:Subject:Subject22 O IT
+$O:Sort=1
+:End ""
+:Begin ""
+!M:11111111.mb\!!!!Outbox\!!!Sent\To21_OR_22\
+@0:To:To21@example.com O I
+@0:To:To22@example.com O I
+$O:Sort=1
+:End ""
+:Begin ""
+!M:11111111.mb\!!!!Outbox\!!!Sent\To21\
+@0:To:To21@example.com O I
+$O:Sort=1
+$X:disabled
+:End ""
diff --git a/comm/mailnews/import/test/unit/resources/bug_263304.ldif b/comm/mailnews/import/test/unit/resources/bug_263304.ldif
new file mode 100644
index 0000000000..3e4e34ee0f
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/bug_263304.ldif
@@ -0,0 +1,7 @@
+dn: cn=Display Name,mail=primaryemail@host.invalid
+objectclass: top
+objectclass: person
+objectclass: inetOrgPerson
+cn: Display Name
+mail: primaryemail@host.invalid
+labeledURI: http://127.0.0.1 label
diff --git a/comm/mailnews/import/test/unit/resources/csv_no_header.csv b/comm/mailnews/import/test/unit/resources/csv_no_header.csv
new file mode 100644
index 0000000000..f017ec3f7d
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/csv_no_header.csv
@@ -0,0 +1,2 @@
+John Doe,John,Doe,john@doe.invalid
+Jane Doe,Jane,Doe,jane@doe.invalid
diff --git a/comm/mailnews/import/test/unit/resources/csv_semicolon.csv b/comm/mailnews/import/test/unit/resources/csv_semicolon.csv
new file mode 100644
index 0000000000..6323d439ee
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/csv_semicolon.csv
@@ -0,0 +1,3 @@
+Display Name;First Name;Last Name;Primary Email
+John Doe;John;Doe;john@doe.invalid
+Jane Doe;Jane;Doe;jane@doe.invalid
diff --git a/comm/mailnews/import/test/unit/resources/dos_vcard_addressbook.vcf b/comm/mailnews/import/test/unit/resources/dos_vcard_addressbook.vcf
new file mode 100644
index 0000000000..722bebc137
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/dos_vcard_addressbook.vcf
@@ -0,0 +1,6 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Surname;Name;;;
+FN:Name Surname
+EMAIL;type=INTERNET;type=HOME:example@gmail.com
+END:VCARD
diff --git a/comm/mailnews/import/test/unit/resources/emptylines_vcard_addressbook.vcf b/comm/mailnews/import/test/unit/resources/emptylines_vcard_addressbook.vcf
new file mode 100644
index 0000000000..2f67c61dfe
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/emptylines_vcard_addressbook.vcf
@@ -0,0 +1,18 @@
+
+
+BEGIN:VCARD
+VERSION:2.1
+FN:John Doe
+N:Doe;John;;;
+EMAIL;TYPE=INTERNET:john.doe@genericemail.invalid
+END:VCARD
+
+
+BEGIN:VCARD
+VERSION:2.1
+FN:Jane Doe
+N:Doe;Jane;;;
+EMAIL;TYPE=INTERNET:jane.doe@genericemail.invalid
+END:VCARD
+
+
diff --git a/comm/mailnews/import/test/unit/resources/import_helper.js b/comm/mailnews/import/test/unit/resources/import_helper.js
new file mode 100644
index 0000000000..e689547377
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/import_helper.js
@@ -0,0 +1,665 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// used by checkProgress to periodically check the progress of the import
+var gGenericImportHelper;
+/**
+ * GenericImportHelper
+ * The parent class of AbImportHelper, MailImportHelper, SettingsImportHelper
+ * and FiltersImportHelper.
+ *
+ * @param aModuleType The type of import module. Should be addressbook or mail.
+ * @param aModuleSearchString
+ * The string to search the module names for, such as
+ * "Text file" to find the import module for comma-separated
+ * value, LDIF, and tab-delimited files.
+ * @param aFile An instance of nsIFile to import.
+ *
+ * @class
+ * @class
+ */
+function GenericImportHelper(aModuleType, aModuleSearchString, aFile) {
+ gGenericImportHelper = null;
+ if (!["addressbook", "mail", "settings", "filters"].includes(aModuleType)) {
+ do_throw("Unexpected type passed to the GenericImportHelper constructor");
+ }
+ this.mModuleType = aModuleType;
+ this.mModuleSearchString = aModuleSearchString;
+ this.mInterface = this._findInterface();
+ Assert.ok(this.mInterface !== null);
+
+ this.mFile = aFile; // checked in the beginImport method
+}
+
+GenericImportHelper.prototype = {
+ interfaceType: Ci.nsIImportGeneric,
+ /**
+ * GenericImportHelper.beginImport
+ * Imports the given address book export or mail data and invoke
+ * checkProgress of child class to check the data,
+ */
+ beginImport() {
+ Assert.ok(this.mFile instanceof Ci.nsIFile && this.mFile.exists());
+
+ if (this.mModuleType == "addressbook") {
+ this.mInterface.SetData("addressLocation", this.mFile);
+ } else if (this.mModuleType == "mail") {
+ this.mInterface.SetData("mailLocation", this.mFile);
+ }
+
+ Assert.ok(this.mInterface.WantsProgress());
+ const error = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ Assert.ok(this.mInterface.BeginImport(null, error));
+ Assert.equal(error.data, "");
+ do_test_pending();
+ this.checkProgress();
+ },
+ /**
+ * GenericImportHelper.getInterface
+ *
+ * @returns An nsIImportGeneric import interface.
+ */
+ getInterface() {
+ return this.mInterface;
+ },
+
+ _findInterface() {
+ var importService = Cc["@mozilla.org/import/import-service;1"].getService(
+ Ci.nsIImportService
+ );
+ var count = importService.GetModuleCount(this.mModuleType);
+
+ // Iterate through each import module until the one being searched for is
+ // found and then return the ImportInterface of that module
+ for (var i = 0; i < count; i++) {
+ // Check if the current module fits the search string gets the interface
+ if (
+ importService
+ .GetModuleName(this.mModuleType, i)
+ .includes(this.mModuleSearchString)
+ ) {
+ return importService
+ .GetModule(this.mModuleType, i)
+ .GetImportInterface(this.mModuleType)
+ .QueryInterface(this.interfaceType);
+ }
+ }
+ return null; // it wasn't found
+ },
+ /**
+ * GenericImportHelper.checkProgress
+ * Checks the progress of an import every 200 milliseconds until it is
+ * complete. Checks the test results if there is an original address book,
+ * otherwise evaluates the optional command, or calls do_test_finished().
+ */
+ checkProgress() {
+ Assert.ok(
+ this.mInterface && this.mInterface instanceof Ci.nsIImportGeneric
+ );
+ Assert.ok(this.mInterface.ContinueImport());
+ // if the import isn't done, check again in 200 milliseconds.
+ if (this.mInterface.GetProgress() != 100) {
+ // use the helper object to check the progress of the import after 200 ms
+ gGenericImportHelper = this;
+ do_timeout(200, function () {
+ gGenericImportHelper.checkProgress();
+ });
+ } else {
+ // if it is done, check the results or finish the test.
+ this.checkResults();
+ do_test_finished();
+ }
+ },
+
+ /**
+ * GenericImportHelper.checkResults
+ * Checks the results of the import.
+ * Child class should implement this method.
+ */
+ checkResults() {},
+};
+
+/**
+ * AbImportHelper
+ * A helper for Address Book imports. To use, supply at least the file and type.
+ * If you would like the results checked, add a new array in the addressbook
+ * JSON file in the resources folder and supply aAbName and aJsonName.
+ * See AB_README for more information.
+ *
+ * @param aFile An instance of nsIAbFile to import.
+ * @param aModuleSearchString
+ * The string to search the module names for, such as
+ * "Text file" to find the import module for comma-separated
+ * value, LDIF, and tab-delimited files.
+ * Optional parameters: Include if you would like the import checked.
+ * @param aAbName The name the address book will have (the filename without
+ * the extension).
+ * @param aJsonName The name of the array in addressbook.json with the cards
+ * to compare with the imported cards.
+ * @class
+ * @class
+ */
+function AbImportHelper(aFile, aModuleSearchString, aAbName, aJsonName) {
+ GenericImportHelper.call(this, "addressbook", aModuleSearchString, aFile);
+
+ this.mAbName = aAbName;
+ /* Attribute notes: The attributes listed in the declaration below are
+ * supported by all three text export/import types.
+ * The following are not supported: anniversaryYear, anniversaryMonth,
+ * anniversaryDay, popularityIndex, isMailList, mailListURI, lastModifiedDate.
+ */
+ var supportedAttributes = [
+ "FirstName",
+ "LastName",
+ "DisplayName",
+ "NickName",
+ "PrimaryEmail",
+ "SecondEmail",
+ "WorkPhone",
+ "HomePhone",
+ "FaxNumber",
+ "PagerNumber",
+ "CellularNumber",
+ "HomeAddress",
+ "HomeAddress2",
+ "HomeCity",
+ "HomeState",
+ "HomeZipCode",
+ "HomeCountry",
+ "WorkAddress",
+ "WorkAddress2",
+ "WorkCity",
+ "WorkState",
+ "WorkZipCode",
+ "WorkCountry",
+ "JobTitle",
+ "Department",
+ "Company",
+ "BirthYear",
+ "BirthMonth",
+ "BirthDay",
+ "WebPage1",
+ "WebPage2",
+ "Custom1",
+ "Custom2",
+ "Custom3",
+ "Custom4",
+ "Notes",
+ "_AimScreenName",
+ "_vCard",
+ ];
+
+ // get the extra attributes supported for the given type of import
+ if (this.mFile.leafName.toLowerCase().endsWith(".ldif")) {
+ this.mSupportedAttributes = supportedAttributes;
+ } else if (this.mFile.leafName.toLowerCase().endsWith(".csv")) {
+ this.mSupportedAttributes = supportedAttributes;
+ this.setFieldMap(this.getDefaultFieldMap(true));
+ } else if (this.mFile.leafName.toLowerCase().endsWith(".vcf")) {
+ this.mSupportedAttributes = supportedAttributes;
+ }
+
+ // get the "cards" from the JSON file, if necessary
+ if (aJsonName) {
+ this.mJsonCards = this.getJsonCards(aJsonName);
+ }
+}
+
+AbImportHelper.prototype = {
+ __proto__: GenericImportHelper.prototype,
+ /**
+ * AbImportHelper.getDefaultFieldMap
+ * Returns the default field map.
+ *
+ * @param aSkipFirstRecord True if the first record of the text file should
+ * be skipped.
+ * @returns A default field map.
+ */
+ getDefaultFieldMap(aSkipFirstRecord) {
+ var importService = Cc["@mozilla.org/import/import-service;1"].getService(
+ Ci.nsIImportService
+ );
+ var fieldMap = importService.CreateNewFieldMap();
+
+ fieldMap.DefaultFieldMap(fieldMap.numMozFields);
+ this.mInterface
+ .GetData("addressInterface")
+ .QueryInterface(Ci.nsIImportAddressBooks)
+ .InitFieldMap(fieldMap);
+ fieldMap.skipFirstRecord = aSkipFirstRecord;
+
+ return fieldMap;
+ },
+
+ /**
+ * AbImportHelper.setFieldMap
+ * Set the field map.
+ *
+ * @param aFieldMap The field map used for address book import.
+ */
+ setFieldMap(aFieldMap) {
+ this.mInterface.SetData("fieldMap", aFieldMap);
+ },
+
+ /**
+ * AbImportHelper.setAddressLocation
+ * Set the the location of the address book.
+ *
+ * @param aLocation The location of the source address book.
+ */
+ setAddressBookLocation(aLocation) {
+ this.mInterface.SetData("addressLocation", aLocation);
+ },
+
+ /**
+ * AbImportHelper.setAddressDestination
+ * Set the the destination of the address book.
+ *
+ * @param aDestination URI of destination address book or null if
+ * new address books will be created.
+ */
+ setAddressDestination(aDestination) {
+ this.mInterface.SetData("addressDestination", aDestination);
+ },
+
+ /**
+ * AbImportHelper.checkResults
+ * Checks the results of the import.
+ * Ensures the an address book was created, then compares the supported
+ * attributes of each card with the card(s) in the JSON array.
+ * Calls do_test_finished() when done
+ */
+ checkResults() {
+ if (!this.mJsonCards) {
+ do_throw("The address book must be setup before checking results");
+ }
+ // When do_test_pending() was called and there is an error the test hangs.
+ // This try/catch block will catch any errors and call do_throw() with the
+ // error to throw the error and avoid the hang.
+ try {
+ // make sure an address book was created
+ var newAb = this.getAbByName(this.mAbName);
+ Assert.ok(newAb !== null);
+ Assert.ok(newAb.QueryInterface(Ci.nsIAbDirectory));
+ // get the imported card(s) and check each one
+ var count = 0;
+ for (let importedCard of newAb.childCards) {
+ this.compareCards(this.mJsonCards[count], importedCard);
+ count++;
+ }
+ // make sure there are the same number of cards in the address book and
+ // the JSON array
+ Assert.equal(count, this.mJsonCards.length);
+ do_test_finished();
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+ /**
+ * AbImportHelper.getAbByName
+ * Returns the Address Book (if any) with the given name.
+ *
+ * @param aName The name of the Address Book to find.
+ * @returns An nsIAbDirectory, if found.
+ * null if the requested Address Book could not be found.
+ */
+ getAbByName(aName) {
+ Assert.ok(aName && aName.length > 0);
+
+ for (let data of MailServices.ab.directories) {
+ if (data.dirName == aName) {
+ return data;
+ }
+ }
+ return null;
+ },
+ /**
+ * AbImportHelper.compareCards
+ * Compares a JSON "card" with an imported card and throws an error if the
+ * values of a supported attribute are different.
+ *
+ * @param aJsonCard The object decoded from addressbook.json.
+ * @param aCard The imported card to compare with.
+ */
+ compareCards(aJsonCard, aCard) {
+ for (let [key, value] of Object.entries(aJsonCard)) {
+ if (!this.mSupportedAttributes.includes(key)) {
+ continue;
+ }
+ if (key == "_vCard") {
+ equal(
+ aCard
+ .getProperty(key, "")
+ .replace(
+ /UID:[a-f0-9-]{36}/i,
+ "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ ),
+ `BEGIN:VCARD\r\n${value.join("\r\n")}\r\nEND:VCARD\r\n`,
+ "_vCard should be correct"
+ );
+ } else {
+ equal(aCard.getProperty(key, ""), value, `${key} should be correct`);
+ }
+ }
+ },
+ /**
+ * AbImportHelper.getJsonCards
+ * Gets an array of "cards" from the JSON file addressbook.json located in the
+ * mailnews/import/test/resources folder. The array should contain objects
+ * with the expected properties and values of the cards in the imported
+ * address book.
+ * See addressbook.json for an example and AB_README for more details.
+ *
+ * @param aName The name of the array in addressbook.json.
+ * @returns An array of "cards".
+ */
+ getJsonCards(aName) {
+ if (!aName) {
+ do_throw("Error - getJSONAb requires an address book name");
+ }
+ var file = do_get_file("resources/addressbook.json");
+ if (!file || !file.exists() || !file.isFile()) {
+ do_throw("Unable to get JSON file");
+ }
+
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fis.init(file, 0x01, 0o444, 0);
+ var istream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ var replacementChar =
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
+ istream.init(fis, "UTF-8", 1024, replacementChar);
+ var json = "";
+ var str = {};
+ // get the entire file into the json string
+ while (istream.readString(4096, str) != 0) {
+ json += str.value;
+ }
+ // close the input streams
+ istream.close();
+ fis.close();
+ // decode the JSON and get the array of cards
+ var arr = JSON.parse(json)[aName];
+ Assert.ok(arr && arr.length > 0);
+ return arr;
+ },
+
+ setSupportedAttributes(attributes) {
+ this.mSupportedAttributes = attributes;
+ },
+};
+
+/**
+ * MailImportHelper
+ * A helper for mail imports.
+ *
+ * @param aFile An instance of nsIFile to import.
+ * @param aModuleSearchString
+ * The string to search the module names for, such as
+ * "Outlook Express", etc.
+ * @param aExpected An instance of nsIFile to compare with the imported
+ * folders.
+ *
+ * @class
+ * @class
+ */
+function MailImportHelper(aFile, aModuleSearchString, aExpected) {
+ GenericImportHelper.call(this, "mail", aModuleSearchString, aFile);
+ this.mExpected = aExpected;
+}
+
+MailImportHelper.prototype = {
+ __proto__: GenericImportHelper.prototype,
+ interfaceType: Ci.nsIImportGeneric,
+ _checkEqualFolder(expectedFolder, actualFolder) {
+ Assert.equal(expectedFolder.leafName, actualFolder.name);
+
+ let expectedSubFolders = [];
+ for (let entry of expectedFolder.directoryEntries) {
+ if (entry.isDirectory()) {
+ expectedSubFolders.push(entry);
+ }
+ }
+ let actualSubFolders = actualFolder.subFolders;
+ Assert.equal(expectedSubFolders.length, actualSubFolders.length);
+ for (let i = 0; i < expectedSubFolders.length; i++) {
+ this._checkEqualFolder(expectedSubFolders[i], actualSubFolders[i]);
+ }
+ },
+
+ checkResults() {
+ let rootFolder = MailServices.accounts.localFoldersServer.rootFolder;
+ Assert.ok(rootFolder.containsChildNamed(this.mFile.leafName));
+ let importedFolder = rootFolder.getChildNamed(this.mFile.leafName);
+ Assert.notEqual(importedFolder, null);
+
+ this._checkEqualFolder(this.mExpected, importedFolder);
+ },
+};
+
+/**
+ * SettingsImportHelper
+ * A helper for settings imports.
+ *
+ * @param aFile An instance of nsIFile to import, can be null.
+ * @param aModuleSearchString
+ * The string to search the module names for, such as
+ * "Outlook Express", etc.
+ * @param aExpected An array of object which has incomingServer, identity
+ * and smtpSever to compare with imported nsIMsgAccount.
+ *
+ * @class
+ * @class
+ */
+function SettingsImportHelper(aFile, aModuleSearchString, aExpected) {
+ GenericImportHelper.call(this, "settings", aModuleSearchString, aFile);
+ this.mExpected = aExpected;
+}
+
+SettingsImportHelper.prototype = {
+ __proto__: GenericImportHelper.prototype,
+ interfaceType: Ci.nsIImportSettings,
+ /**
+ * SettingsImportHelper.beginImport
+ * Imports settings from a specific file or auto-located if the file is null,
+ * and compare the import results with the expected array.
+ */
+ beginImport() {
+ this._ensureNoAccounts();
+ if (this.mFile) {
+ this.mInterface.SetLocation(this.mFile);
+ } else {
+ Assert.equal(true, this.mInterface.AutoLocate({}, {}));
+ }
+ Assert.equal(true, this.mInterface.Import({}));
+ this.checkResults();
+ },
+
+ _ensureNoAccounts() {
+ for (let account of MailServices.accounts.accounts) {
+ MailServices.accounts.removeAccount(account);
+ }
+ },
+
+ _checkSmtpServer(expected, actual) {
+ Assert.equal(expected.port, actual.port);
+ Assert.equal(expected.username, actual.username);
+ Assert.equal(expected.authMethod, actual.authMethod);
+ Assert.equal(expected.socketType, actual.socketType);
+ },
+
+ _checkIdentity(expected, actual) {
+ Assert.equal(expected.fullName, actual.fullName);
+ Assert.equal(expected.email, actual.email);
+ Assert.equal(expected.replyTo, actual.replyTo);
+ Assert.equal(expected.organization, actual.organization);
+ },
+
+ _checkPop3IncomingServer(expected, actual) {
+ Assert.equal(expected.leaveMessagesOnServer, actual.leaveMessagesOnServer);
+ Assert.equal(
+ expected.deleteMailLeftOnServer,
+ actual.deleteMailLeftOnServer
+ );
+ Assert.equal(expected.deleteByAgeFromServer, actual.deleteByAgeFromServer);
+ Assert.equal(
+ expected.numDaysToLeaveOnServer,
+ actual.numDaysToLeaveOnServer
+ );
+ },
+
+ _checkIncomingServer(expected, actual) {
+ Assert.equal(expected.type, actual.type);
+ Assert.equal(expected.port, actual.port);
+ Assert.equal(expected.username, actual.username);
+ Assert.equal(expected.isSecure, actual.isSecure);
+ Assert.equal(expected.hostName, actual.hostName);
+ Assert.equal(expected.prettyName, actual.prettyName);
+ Assert.equal(expected.authMethod, actual.authMethod);
+ Assert.equal(expected.socketType, actual.socketType);
+ Assert.equal(expected.doBiff, actual.doBiff);
+ Assert.equal(expected.biffMinutes, actual.biffMinutes);
+
+ if (expected.type == "pop3") {
+ this._checkPop3IncomingServer(
+ expected,
+ actual.QueryInterface(Ci.nsIPop3IncomingServer)
+ );
+ }
+ },
+
+ _checkAccount(expected, actual) {
+ this._checkIncomingServer(expected.incomingServer, actual.incomingServer);
+
+ Assert.equal(1, actual.identities.length);
+ let actualIdentity = actual.identities[0];
+ this._checkIdentity(expected.identity, actualIdentity);
+
+ if (expected.incomingServer.type != "nntp") {
+ let actualSmtpServer = MailServices.smtp.getServerByKey(
+ actualIdentity.smtpServerKey
+ );
+ this._checkSmtpServer(expected.smtpServer, actualSmtpServer);
+ }
+ },
+
+ _isLocalMailAccount(account) {
+ return (
+ account.incomingServer.type == "none" &&
+ account.incomingServer.username == "nobody" &&
+ account.incomingServer.hostName == "Local Folders"
+ );
+ },
+
+ _findExpectedAccount(account) {
+ return this.mExpected.filter(function (expectedAccount) {
+ return (
+ expectedAccount.incomingServer.type == account.incomingServer.type &&
+ expectedAccount.incomingServer.username ==
+ account.incomingServer.username &&
+ expectedAccount.incomingServer.hostName ==
+ account.incomingServer.hostName
+ );
+ });
+ },
+
+ checkResults() {
+ for (let actualAccount of MailServices.accounts.accounts) {
+ if (this._isLocalMailAccount(actualAccount)) {
+ continue;
+ }
+ let expectedAccounts = this._findExpectedAccount(actualAccount);
+ Assert.notEqual(null, expectedAccounts);
+ Assert.equal(1, expectedAccounts.length);
+ this._checkAccount(expectedAccounts[0], actualAccount);
+ }
+ },
+};
+
+/**
+ * FiltersImportHelper
+ * A helper for filter imports.
+ *
+ * @param aFile An instance of nsIFile to import.
+ * @param aModuleSearchString
+ * The string to search the module names for, such as
+ * "Outlook Express", etc.
+ * @param aExpected The number of filters that should exist after import.
+ *
+ * @class
+ * @class
+ */
+function FiltersImportHelper(aFile, aModuleSearchString, aExpected) {
+ GenericImportHelper.call(this, "filters", aModuleSearchString, aFile);
+ this.mExpected = aExpected;
+}
+
+FiltersImportHelper.prototype = {
+ __proto__: GenericImportHelper.prototype,
+ interfaceType: Ci.nsIImportFilters,
+
+ /**
+ * FiltersImportHelper.beginImport
+ * Imports filters from a specific file/folder or auto-located if the file is null,
+ * and compare the import results with the expected array.
+ */
+ beginImport() {
+ if (this.mFile) {
+ this.mInterface.SetLocation(this.mFile);
+ } else {
+ Assert.equal(true, this.mInterface.AutoLocate({}, {}));
+ }
+ Assert.equal(true, this.mInterface.Import({}));
+ this.checkResults();
+ },
+
+ _loopOverFilters(aFilterList, aCondition) {
+ let result = 0;
+ for (let i = 0; i < aFilterList.filterCount; i++) {
+ let filter = aFilterList.getFilterAt(i);
+ if (aCondition(filter)) {
+ result++;
+ }
+ }
+ return result;
+ },
+
+ checkResults() {
+ let expected = this.mExpected;
+ let server = MailServices.accounts.localFoldersServer;
+ let filterList = server.getFilterList(null);
+ if ("count" in expected) {
+ Assert.equal(filterList.filterCount, expected.count);
+ }
+ if ("enabled" in expected) {
+ Assert.equal(
+ this._loopOverFilters(filterList, f => f.enabled),
+ expected.enabled
+ );
+ }
+ if ("incoming" in expected) {
+ Assert.equal(
+ this._loopOverFilters(
+ filterList,
+ f => f.filterType & Ci.nsMsgFilterType.InboxRule
+ ),
+ expected.incoming
+ );
+ }
+ if ("outgoing" in expected) {
+ Assert.equal(
+ this._loopOverFilters(
+ filterList,
+ f => f.filterType & Ci.nsMsgFilterType.PostOutgoing
+ ),
+ expected.outgoing
+ );
+ }
+ },
+};
diff --git a/comm/mailnews/import/test/unit/resources/mock_windows_reg_factory.js b/comm/mailnews/import/test/unit/resources/mock_windows_reg_factory.js
new file mode 100644
index 0000000000..19583d5007
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/mock_windows_reg_factory.js
@@ -0,0 +1,84 @@
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var gCid;
+
+function MockWindowsRegKey(registryData) {
+ this._registryData = registryData;
+}
+
+MockWindowsRegKey.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]),
+
+ open(aRootKey, aRelPath, aMode) {
+ if (!this._registryData[aRelPath]) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ this._keyPath = aRelPath;
+ },
+
+ close() {},
+
+ openChild(aRelPath, aMode) {
+ if (
+ !this._registryData[this._keyPath] ||
+ !this._registryData[this._keyPath][aRelPath]
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ let child = new MockWindowsRegKey({});
+ let newKeyPath = this._keyPath + "\\" + aRelPath;
+ child._keyPath = newKeyPath;
+ child._registryData[newKeyPath] =
+ this._registryData[this._keyPath][aRelPath];
+ return child;
+ },
+
+ get childCount() {
+ return Object.keys(this._registryData[this._keyPath]).length;
+ },
+
+ getChildName(aIndex) {
+ let keys = Object.keys(this._registryData[this._keyPath]);
+ let keyAtIndex = keys[aIndex];
+ if (!keyAtIndex) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ return keyAtIndex;
+ },
+
+ _readValue(aName) {
+ if (
+ !this._registryData[this._keyPath] ||
+ !this._registryData[this._keyPath][aName]
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ return this._registryData[this._keyPath][aName];
+ },
+
+ readIntValue(aName) {
+ return this._readValue(aName);
+ },
+
+ readStringValue(aName) {
+ return this._readValue(aName);
+ },
+};
+
+/* exported setup_mock_registry, teardown_mock_registry */
+function setup_mock_registry(mockRegistry) {
+ gCid = MockRegistrar.register(
+ "@mozilla.org/windows-registry-key;1",
+ MockWindowsRegKey,
+ [mockRegistry]
+ );
+}
+
+function teardown_mock_registry() {
+ MockRegistrar.unregister(gCid);
+}
diff --git a/comm/mailnews/import/test/unit/resources/quote.csv b/comm/mailnews/import/test/unit/resources/quote.csv
new file mode 100644
index 0000000000..d508eac33c
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/quote.csv
@@ -0,0 +1,2 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Screen Name,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes
+,,"Acer America",,,,,"(800) 000-0000","","","","","","","","","","","","","","","","",,,"Acer America",,,,,,,,,,
diff --git a/comm/mailnews/import/test/unit/resources/shiftjis_addressbook.csv b/comm/mailnews/import/test/unit/resources/shiftjis_addressbook.csv
new file mode 100644
index 0000000000..31467dd42f
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/shiftjis_addressbook.csv
@@ -0,0 +1,2 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes,Screen Name,
+Œ •º‰q,–¼–³‚µ,–¼–³‚µ‚ÌŒ •º‰q,,–¼–³‚µ‚ÌŒ •º‰q@host.invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
diff --git a/comm/mailnews/import/test/unit/resources/tab_comma_mixed.csv b/comm/mailnews/import/test/unit/resources/tab_comma_mixed.csv
new file mode 100644
index 0000000000..804a330e22
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/tab_comma_mixed.csv
@@ -0,0 +1,4 @@
+1,1,1,1,1@host.invalid,,,,,,,,,,,,,,,,,,,,,
+2 2 2 2 2@host.invalid
+3 3 3 3 3@host.invalid
+4 4 4 4 4@host.invalid
diff --git a/comm/mailnews/import/test/unit/resources/utf16_addressbook.csv b/comm/mailnews/import/test/unit/resources/utf16_addressbook.csv
new file mode 100644
index 0000000000..e22a64b187
--- /dev/null
+++ b/comm/mailnews/import/test/unit/resources/utf16_addressbook.csv
Binary files differ
diff --git a/comm/mailnews/import/test/unit/test_AddrBookFileImporter.js b/comm/mailnews/import/test/unit/test_AddrBookFileImporter.js
new file mode 100644
index 0000000000..2ba696d1ed
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_AddrBookFileImporter.js
@@ -0,0 +1,130 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+var { AddrBookFileImporter } = ChromeUtils.import(
+ "resource:///modules/AddrBookFileImporter.jsm"
+);
+
+/**
+ * Create a temporary address book, import a source file into it, then test the
+ * cards are correct.
+ *
+ * @param {string} type - A source file type supported by AddrBookFileImporter.
+ * @param {string} filePath - The path of a source file.
+ * @param {string} refDataKey - The key of an object in addressbook.json.
+ * @param {string[]} [csvFieldMap] - Map of CSV fields to address book fields.
+ */
+async function test_importAbFile(type, filePath, refDataKey, csvFieldMap) {
+ // Create an address book and init the importer.
+ let dirId = MailServices.ab.newAddressBook(
+ `tmp-${type}`,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let targetDir = MailServices.ab.getDirectoryFromId(dirId);
+ let importer = new AddrBookFileImporter(type);
+
+ // Start importing.
+ let sourceFile = do_get_file(filePath);
+ if (type == "csv") {
+ let unmatched = await importer.parseCsvFile(sourceFile);
+ if (unmatched.length) {
+ importer.setCsvFields(csvFieldMap);
+ }
+ }
+ await importer.startImport(sourceFile, targetDir);
+
+ // Read in the reference data.
+ let refFile = do_get_file("resources/addressbook.json");
+ let refData = JSON.parse(await IOUtils.readUTF8(refFile.path))[refDataKey];
+
+ // Compare with the reference data.
+ for (let i = 0; i < refData.length; i++) {
+ let card = targetDir.childCards[i];
+ for (let [key, value] of Object.entries(refData[i])) {
+ if (key == "LastModifiedDate") {
+ continue;
+ }
+ if (key == "_vCard") {
+ equal(
+ card
+ .getProperty(key, "")
+ .replace(
+ /UID:[a-f0-9-]{36}/i,
+ "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ ),
+ `BEGIN:VCARD\r\n${value.join("\r\n")}\r\nEND:VCARD\r\n`,
+ "_vCard should be correct"
+ );
+ } else {
+ equal(card.getProperty(key, ""), value, `${key} should be correct`);
+ }
+ }
+ }
+}
+
+/** Test importing .csv file works. */
+add_task(async function test_importCsvFile() {
+ // A comma separated file.
+ await test_importAbFile(
+ "csv",
+ "resources/basic_csv_addressbook.csv",
+ "csv_import"
+ );
+
+ // A semicolon separated file.
+ await test_importAbFile("csv", "resources/csv_semicolon.csv", "csv_import");
+
+ // A comma separated file without header row.
+ Services.prefs.setBoolPref("mail.import.csv.skipfirstrow", false);
+ await test_importAbFile("csv", "resources/csv_no_header.csv", "csv_import", [
+ 2, // DisplayName
+ 0, // FirstName
+ 1, // LastName
+ 4, // PrimaryEmail
+ ]);
+ Services.prefs.clearUserPref("mail.import.csv.skipfirstrow");
+
+ // A comma separated file with some fields containing quotes.
+ await test_importAbFile("csv", "resources/quote.csv", "quote_csv");
+
+ // Non-UTF8 csv file.
+ await test_importAbFile(
+ "csv",
+ "resources/shiftjis_addressbook.csv",
+ "shiftjis_csv"
+ );
+ await test_importAbFile(
+ "csv",
+ "resources/utf16_addressbook.csv",
+ "utf16_csv"
+ );
+});
+
+/** Test importing .vcf file works. */
+add_task(async function test_importVCardFile() {
+ return test_importAbFile(
+ "vcard",
+ "resources/basic_vcard_addressbook.vcf",
+ "vcard_import"
+ );
+});
+
+/** Test importing .vcf file with \r\r\n as line breaks works. */
+add_task(async function test_importDosVCardFile() {
+ return test_importAbFile(
+ "vcard",
+ "resources/dos_vcard_addressbook.vcf",
+ "dos_vcard_import"
+ );
+});
+
+/** Test importing .ldif file works. */
+add_task(async function test_importLdifFile() {
+ return test_importAbFile(
+ "ldif",
+ "resources/basic_ldif_addressbook.ldif",
+ "basic_addressbook"
+ );
+});
diff --git a/comm/mailnews/import/test/unit/test_ThunderbirdProfileImporter.js b/comm/mailnews/import/test/unit/test_ThunderbirdProfileImporter.js
new file mode 100644
index 0000000000..f89a8ca2a3
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_ThunderbirdProfileImporter.js
@@ -0,0 +1,288 @@
+/*
+ * 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 https://mozilla.org/MPL/2.0/.
+ */
+
+// It is necessary to manually disable `xpc::IsInAutomation` since
+// `resetPrefs` will flip the preference to re-enable `once`-synced
+// preference change assertions, and also change the value of those
+// preferences.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { ThunderbirdProfileImporter } = ChromeUtils.import(
+ "resource:///modules/ThunderbirdProfileImporter.jsm"
+);
+
+let tmpProfileDir;
+registerCleanupFunction(() => {
+ tmpProfileDir?.remove(true);
+});
+
+/**
+ * Create a temporary dir to use as the source profile dir. Write a prefs.js
+ * into it.
+ *
+ * @param {Array<[string, string]>} prefs - An array of tuples, each tuple is
+ * a pref represented as [prefName, prefValue].
+ */
+async function createTmpProfileWithPrefs(prefs) {
+ tmpProfileDir?.remove(true);
+
+ // Create a temporary dir.
+ tmpProfileDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tmpProfileDir.append("profile-tmp");
+ tmpProfileDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ info(`Created a temporary profile at ${tmpProfileDir.path}`);
+
+ // Write prefs to prefs.js.
+ let prefsFile = tmpProfileDir.clone();
+ prefsFile.append("prefs.js");
+ let prefsContent = prefs
+ .map(([name, value]) => {
+ let prefValue = typeof value == "string" ? `"${value}"` : value;
+ return `user_pref("${name}", ${prefValue});`;
+ })
+ .join("\n");
+ return IOUtils.writeUTF8(prefsFile.path, prefsContent);
+}
+
+/**
+ * Construct a temporary profile dir with prefs, import into the current
+ * profile, then check the values of prefs related to mail accounts.
+ */
+add_task(async function test_importAccountsIntoEmptyProfile() {
+ equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "",
+ "Should have no accounts at first"
+ );
+ let charPrefs = [
+ ["mail.smtpserver.smtp1.username", "smtp-user-1"],
+ ["mail.smtpserver.smtp3.username", "smtp-user-2"],
+ ["mail.smtpservers", "smtp1,smtp3"],
+ ["mail.identity.id1.smtpServer", "smtp3"],
+ ["mail.identity.id3.fullName", "id-name-2"],
+ ["mail.identity.id4.stmpServer", "smtp1"],
+ ["mail.server.server2.type", "none"],
+ ["mail.server.server6.type", "imap"],
+ ["mail.server.server7.type", "pop3"],
+ ["mail.account.account2.server", "server2"],
+ ["mail.account.account3.server", "server6"],
+ ["mail.account.account3.identities", "id1,id3"],
+ ["mail.account.account4.server", "server7"],
+ ["mail.accountmanager.accounts", "account3,account4,account2"],
+ ];
+ await createTmpProfileWithPrefs(charPrefs);
+
+ let importer = new ThunderbirdProfileImporter();
+
+ await importer.startImport(tmpProfileDir, importer.SUPPORTED_ITEMS);
+ // Server/identity/account keys should be changed and remapped correctly after
+ // import.
+ let expectedCharPrefs = [
+ ["mail.smtpserver.smtp1.username", "smtp-user-1"],
+ ["mail.smtpserver.smtp2.username", "smtp-user-2"],
+ ["mail.smtpservers", "smtp1,smtp2"],
+ ["mail.identity.id1.smtpServer", "smtp2"],
+ ["mail.identity.id2.fullName", "id-name-2"],
+ ["mail.identity.id3.stmpServer", "smtp1"],
+ ["mail.server.server1.type", "none"],
+ ["mail.server.server2.type", "imap"],
+ ["mail.server.server3.type", "pop3"],
+ ["mail.account.account1.server", "server1"],
+ ["mail.account.account2.server", "server2"],
+ ["mail.account.account2.identities", "id1,id2"],
+ ["mail.account.account3.server", "server3"],
+ ["mail.accountmanager.accounts", "account2,account3,account1"],
+ ];
+ for (let [name, value] of expectedCharPrefs) {
+ equal(
+ Services.prefs.getCharPref(name, ""),
+ value,
+ `${name} should be correct`
+ );
+ }
+
+ // Remove all the prefs to do the next test.
+ Services.prefs.resetPrefs();
+
+ equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "",
+ "Should have no accounts after resetPrefs"
+ );
+ await importer.startImport(tmpProfileDir, {
+ ...importer.SUPPORTED_ITEMS,
+ accounts: false,
+ mailMessages: false, // If true, Local Folders is created
+ });
+ equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "",
+ "Should still have no accounts without importing accounts"
+ );
+
+ Services.prefs.resetPrefs();
+});
+
+/**
+ * Test that importing a server without directory works. A server without
+ * directory can happen after clicking a news url.
+ */
+add_task(async function test_serverWithoutDirectory() {
+ let prefs = [
+ ["mail.server.server1.type", "nntp"],
+ ["mail.server.server1.hostname", "news.invalid"],
+ ];
+ await createTmpProfileWithPrefs(prefs);
+
+ let importer = new ThunderbirdProfileImporter();
+ await importer.startImport(tmpProfileDir, importer.SUPPORTED_ITEMS);
+ for (let [name, value] of prefs) {
+ equal(
+ Services.prefs.getCharPref(name, ""),
+ value,
+ `${name} should be correct`
+ );
+ }
+});
+
+/**
+ * Test that when the source profile and current profile each has Local Folders,
+ * the source Local Folders will be merged into the current Local Folders.
+ */
+add_task(async function test_mergeLocalFolders() {
+ let prefs = [
+ ["mail.smtpserver.smtp1.username", "smtp-user-1"],
+ ["mail.smtpservers", "smtp1"],
+ ["mail.identity.id1.smtpServer", "smtp1"],
+ ["mail.server.server2.type", "none"],
+ ["mail.server.server2.directory-rel", "[ProfD]Mail/Local Folders"],
+ ["mail.server.server2.hostname", "Local Folders"],
+ ["mail.server.server3.type", "imap"],
+ ["mail.account.account2.server", "server2"],
+ ["mail.account.account3.server", "server3"],
+ ["mail.account.account3.identities", "id1"],
+ ["mail.accountmanager.accounts", "account3,account2"],
+ ["mail.accountmanager.localfoldersserver", "server2"],
+ ];
+ await createTmpProfileWithPrefs(prefs);
+
+ // Create a physical file in tmpProfileDir.
+ let sourceLocalFolder = tmpProfileDir.clone();
+ sourceLocalFolder.append("Mail");
+ sourceLocalFolder.append("Local Folders");
+ sourceLocalFolder.append("folder-xpcshell");
+ sourceLocalFolder.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ // Create Local Folders in the current profile.
+ MailServices.accounts.createLocalMailAccount();
+
+ let importer = new ThunderbirdProfileImporter();
+ await importer.startImport(tmpProfileDir, importer.SUPPORTED_ITEMS);
+
+ // Test that sub msg folders are created in the current Local Folders.
+ let localFolders = MailServices.accounts.localFoldersServer.rootMsgFolder;
+ ok(localFolders.containsChildNamed("Local Folders"));
+ let msgFolder = localFolders.getChildNamed("Local Folders");
+ ok(msgFolder.containsChildNamed("folder-xpcshell"));
+
+ // Test that folder-xpcshell is copied into current Local Folders.
+ let importedFolder = localFolders.filePath;
+ importedFolder.append("Local Folders.sbd");
+ importedFolder.append("folder-xpcshell");
+ ok(importedFolder.exists(), "Source Local Folders should be merged in.");
+});
+
+/**
+ * Test that calendars can be correctly imported.
+ */
+add_task(async function test_importCalendars() {
+ // Set sortOrder to contain a fake calendar id.
+ Services.prefs.setCharPref("calendar.list.sortOrder", "uuid-x");
+
+ let prefs = [
+ ["calendar.registry.uuid-1.name", "Home"],
+ ["calendar.registry.uuid-1.type", "Storage"],
+ ["calendar.registry.uuid-3.name", "cal1"],
+ ["calendar.registry.uuid-3.type", "caldav"],
+ ["calendar.list.sortOrder", "uuid-1 uuid-3"],
+ ];
+
+ await createTmpProfileWithPrefs(prefs);
+
+ let importer = new ThunderbirdProfileImporter();
+
+ await importer.startImport(tmpProfileDir, { calendars: true });
+
+ // Test calendar.registry.* are imported correctly.
+ for (let [name, value] of prefs.slice(0, -1)) {
+ equal(
+ Services.prefs.getCharPref(name, ""),
+ value,
+ `${name} should be correct`
+ );
+ }
+
+ // Test calendar.list.sortOrder has merged ids.
+ equal(
+ Services.prefs.getCharPref("calendar.list.sortOrder"),
+ "uuid-x uuid-1 uuid-3",
+ "calendar.list.sortOrder should be correct"
+ );
+
+ Services.prefs.resetPrefs();
+});
+
+/**
+ * Test that tags can be correctly imported.
+ */
+add_task(async function test_importTags() {
+ let prefs = [
+ ["mailnews.tags.$label1.color", "#CC0011"],
+ ["mailnews.tags.$label1.tag", "tag1"],
+ ["mailnews.tags.$label2.color", "#CC0022"],
+ ["mailnews.tags.$label2.tag", "tag2"],
+ ];
+ await createTmpProfileWithPrefs(prefs);
+
+ let importer = new ThunderbirdProfileImporter();
+ await importer.startImport(tmpProfileDir, importer.SUPPORTED_ITEMS);
+
+ // Test mailnews.tags.* are imported because existing tags are in default state.
+ for (let [name, value] of prefs) {
+ equal(
+ Services.prefs.getCharPref(name, ""),
+ value,
+ `${name} should be correct`
+ );
+ }
+
+ let prefs2 = [
+ ["mailnews.tags.$label1.color", "#DD0011"],
+ ["mailnews.tags.$label1.tag", "tag11"],
+ ["mailnews.tags.$label2.color", "#DD0022"],
+ ["mailnews.tags.$label2.tag", "tag22"],
+ ["mailnews.tags.$tag3.color", "#DD0033"],
+ ["mailnews.tags.$tag3.tag", "tag3"],
+ ];
+ await createTmpProfileWithPrefs(prefs2);
+
+ await importer.startImport(tmpProfileDir, importer.SUPPORTED_ITEMS);
+
+ // $label1 and $label2 should not be imported, only $tag3 should be imported.
+ for (let [name, value] of [...prefs, ...prefs2.slice(4)]) {
+ equal(
+ Services.prefs.getCharPref(name, ""),
+ value,
+ `${name} should be correct`
+ );
+ }
+});
diff --git a/comm/mailnews/import/test/unit/test_becky_addressbook.js b/comm/mailnews/import/test/unit/test_becky_addressbook.js
new file mode 100644
index 0000000000..c4d4528e32
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_becky_addressbook.js
@@ -0,0 +1,59 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ let file = do_get_file("resources/becky/addressbooks");
+ let helper = new AbImportHelper(
+ file,
+ "Becky!",
+ "addressbooks",
+ "becky_addressbook"
+ );
+ let vcfSupportedAttributes = [
+ "FirstName",
+ "LastName",
+ "DisplayName",
+ "NickName",
+ "PrimaryEmail",
+ "SecondEmail",
+ "WorkPhone",
+ "HomePhone",
+ "FaxNumber",
+ "PagerNumber",
+ "CellularNumber",
+ "HomeAddress",
+ "HomeAddress2",
+ "HomeCity",
+ "HomeState",
+ "HomeZipCode",
+ "HomeCountry",
+ "WorkAddress",
+ "WorkAddress2",
+ "WorkCity",
+ "WorkState",
+ "WorkZipCode",
+ "WorkCountry",
+ "JobTitle",
+ "Department",
+ "Company",
+ "BirthYear",
+ "BirthMonth",
+ "BirthDay",
+ "WebPage1",
+ "WebPage2",
+ "Custom1",
+ "Custom2",
+ "Custom3",
+ "Custom4",
+ "Notes",
+ "_AimScreenName",
+ "_vCard",
+ ];
+ helper.setSupportedAttributes(vcfSupportedAttributes);
+ helper.beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_becky_filters.js b/comm/mailnews/import/test/unit/test_becky_filters.js
new file mode 100644
index 0000000000..5af6483071
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_becky_filters.js
@@ -0,0 +1,41 @@
+const { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ // Import incoming filters.
+ let file = do_get_file("resources/becky/filters/IFilter.def");
+ let helper1 = new FiltersImportHelper(file, "Becky!", {
+ count: 3,
+ enabled: 2,
+ incoming: 3,
+ outgoing: 0,
+ });
+ helper1.beginImport();
+
+ // Import outgoing filters.
+ file = do_get_file("resources/becky/filters/OFilter.def");
+ let helper2 = new FiltersImportHelper(file, "Becky!", {
+ count: 6,
+ enabled: 4,
+ incoming: 3,
+ outgoing: 3,
+ });
+ helper2.beginImport();
+
+ // Import both filter types automatically.
+ file = do_get_file("resources/becky/filters");
+ let helper3 = new FiltersImportHelper(file, "Becky!", {
+ count: 12,
+ enabled: 8,
+ incoming: 6,
+ outgoing: 6,
+ });
+ helper3.beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_bug_263304.js b/comm/mailnews/import/test/unit/test_bug_263304.js
new file mode 100644
index 0000000000..7089c0a4c2
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_bug_263304.js
@@ -0,0 +1,25 @@
+/**
+ * Tests importing an address book export in the LDAP data interchange format
+ * (LDIF) with a labeledURI and checks the accuracy of the imported contact.
+ *
+ * This test checks for the following bug:
+ * - Bug 264405: The Address Book doesn't show the LDAP-field "labeledURI"
+ * as Website
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ var file = do_get_file("resources/bug_263304.ldif");
+ new AbImportHelper(
+ file,
+ "Text file",
+ "bug_263304",
+ "bug_263304"
+ ).beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_bug_437556.js b/comm/mailnews/import/test/unit/test_bug_437556.js
new file mode 100644
index 0000000000..284309a2dc
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_bug_437556.js
@@ -0,0 +1,23 @@
+/**
+ * Test for a regression of Bug 437556: mailnews crashes while importing an
+ * address book if a field map is required but not set.
+ */
+function run_test() {
+ var file = do_get_file("resources/basic_addressbook.csv");
+ var errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ // get the Address Book text import interface and make sure it succeeded
+ var helper = new AbImportHelper(file, "Text file");
+ helper.setFieldMap(null);
+ helper.setAddressBookLocation(file);
+
+ var abInterface = helper.getInterface();
+ Assert.notEqual(abInterface, null);
+ // prepare to start the import
+ Assert.ok(abInterface.WantsProgress());
+ // start the import
+ // BeginImport should return false and log an error if the fieldMap isn't set
+ Assert.ok(!abInterface.BeginImport(null, errorStr));
+ Assert.notEqual(errorStr, "");
+}
diff --git a/comm/mailnews/import/test/unit/test_csv_GetSample.js b/comm/mailnews/import/test/unit/test_csv_GetSample.js
new file mode 100644
index 0000000000..3c401391e0
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_csv_GetSample.js
@@ -0,0 +1,15 @@
+function run_test() {
+ var file = do_get_file("resources/tab_comma_mixed.csv");
+ var helper = new AbImportHelper(file, "Text file");
+ var genericInterface = helper.getInterface();
+ Assert.notEqual(genericInterface, null);
+ let abInterface = genericInterface
+ .GetData("addressInterface")
+ .QueryInterface(Ci.nsIImportAddressBooks);
+ abInterface.SetSampleLocation(file);
+ let recordExists = {};
+
+ let sampleData = abInterface.GetSampleData(3, recordExists);
+ Assert.ok(recordExists.value);
+ Assert.equal(sampleData, "4\n4\n4\n4\n4@host.invalid");
+}
diff --git a/comm/mailnews/import/test/unit/test_csv_import.js b/comm/mailnews/import/test/unit/test_csv_import.js
new file mode 100644
index 0000000000..df53525de9
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_csv_import.js
@@ -0,0 +1,23 @@
+/**
+ * Tests importing an address book export in the LDAP data interchange (LDIF)
+ * format and checks the accuracy of the imported address book's cards.
+ * The current export contains only one card with most fields full.
+ *
+ * This test also checks for the following bugs:
+ * -Bug 439819: LDIF import does not include mozillahomestreet.
+ * -Bug 182128: Edit Card, Notes on several lines appear on one after
+ * export/import in text format *(only tests the import).
+ */
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ let file = do_get_file("resources/basic_csv_addressbook.csv");
+ new AbImportHelper(
+ file,
+ "csv",
+ "basic_csv_addressbook",
+ "csv_import"
+ ).beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_csv_import_quote.js b/comm/mailnews/import/test/unit/test_csv_import_quote.js
new file mode 100644
index 0000000000..67b469bac5
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_csv_import_quote.js
@@ -0,0 +1,11 @@
+/**
+ * Tests importing quoted csv address books.
+ */
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ let file = do_get_file("resources/quote.csv");
+ new AbImportHelper(file, "csv", "quote", "quote_csv").beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_ldif_import.js b/comm/mailnews/import/test/unit/test_ldif_import.js
new file mode 100644
index 0000000000..48be83c27b
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_ldif_import.js
@@ -0,0 +1,27 @@
+/**
+ * Tests importing an address book export in the LDAP data interchange (LDIF)
+ * format and checks the accuracy of the imported address book's cards.
+ * The current export contains only one card with most fields full.
+ *
+ * This test also checks for the following bugs:
+ * -Bug 439819: LDIF import does not include mozillahomestreet.
+ * -Bug 182128: Edit Card, Notes on several lines appear on one after
+ * export/import in text format *(only tests the import).
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ var file = do_get_file("resources/basic_ldif_addressbook.ldif");
+ new AbImportHelper(
+ file,
+ "Text file",
+ "basic_ldif_addressbook",
+ "basic_addressbook"
+ ).beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_outlook_settings.js b/comm/mailnews/import/test/unit/test_outlook_settings.js
new file mode 100644
index 0000000000..0f0313dcad
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_outlook_settings.js
@@ -0,0 +1,158 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from resources/mock_windows_reg_factory.js */
+load("resources/mock_windows_reg_factory.js");
+
+function POP3Account() {}
+
+POP3Account.prototype = {
+ "Account Name": "POP3 Account Name",
+ "POP3 Server": "pop.host.invalid",
+ "POP3 User Name": "pop3user",
+ "Leave Mail On Server": 1,
+ "SMTP Server": "smtp.host.invalid",
+ "SMTP Display Name": "SMTP Display Name",
+ "SMTP Email Address": "pop3user@host.invalid",
+ "SMTP Reply To Email Address": "pop3user@host.invalid",
+ "SMTP Organization Name": "SMTP Organization Name",
+ "SMTP User Name": "smtpuser",
+};
+
+function IMAPAccount() {}
+
+IMAPAccount.prototype = {
+ "Account Name": "IMAP Account Name",
+ "IMAP Server": "imap.host.invalid",
+ "IMAP User Name": "imapuser",
+ "SMTP Server": "smtp.host.invalid",
+ "SMTP Display Name": "SMTP Display Name",
+ "SMTP Email Address": "imapuser@host.invalid",
+ "SMTP Reply To Email Address": "imapuser@host.invalid",
+ "SMTP Organization Name": "SMTP Organization Name",
+ "SMTP User Name": "smtpuser",
+};
+
+/* Outlook 98 */
+function Outlook98Registry(defaultAccount) {
+ this._defaultAccount = defaultAccount;
+}
+
+Outlook98Registry.prototype = {
+ get "Software\\Microsoft\\Office\\8.0\\Outlook\\OMI Account Manager"() {
+ return {
+ "Default Mail Account": "00000001",
+ "00000001": this._defaultAccount,
+ };
+ },
+};
+
+/* Outlook 2003 - */
+function Outlook2003Registry(defaultAccount) {
+ this._defaultAccount = defaultAccount;
+}
+
+Outlook2003Registry.prototype = {
+ get "Software\\Microsoft\\Office\\Outlook\\OMI Account Manager"() {
+ return {
+ "Default Mail Account": "00000001",
+ "00000001": this._defaultAccount,
+ };
+ },
+};
+
+var expectedPop3Account = {
+ incomingServer: {
+ prettyName: "POP3 Account Name",
+ type: "pop3",
+ hostName: "pop.host.invalid",
+ username: "pop3user",
+ leaveMessagesOnServer: true,
+
+ // These are account default values, not imported from Outlook.
+ // They should probably be omitted, but the check functions in
+ // import_helper.js expect to find them.
+ deleteMailLeftOnServer: false,
+ deleteByAgeFromServer: false,
+ numDaysToLeaveOnServer: 7,
+ port: 110,
+ isSecure: false,
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ socketType: Ci.nsMsgSocketType.plain,
+ doBiff: true,
+ biffMinutes: 10,
+ },
+ identity: {
+ fullName: "SMTP Display Name",
+ email: "pop3user@host.invalid",
+ replyTo: "pop3user@host.invalid",
+ organization: "SMTP Organization Name",
+ },
+ smtpServer: {
+ hostname: "smtp.host.invalid",
+ username: "smtpuser",
+ port: 0, // default port
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ socketType: Ci.nsMsgSocketType.plain,
+ },
+};
+
+var expectedImapAccount = {
+ incomingServer: {
+ prettyName: "IMAP Account Name",
+ type: "imap",
+ hostName: "imap.host.invalid",
+ username: "imapuser",
+
+ // These are account default values, not imported from Outlook.
+ // They should probably be omitted, but the check functions in
+ // import_helper.js expect to find them.
+ port: 143,
+ isSecure: false,
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ socketType: Ci.nsMsgSocketType.plain,
+ doBiff: true,
+ biffMinutes: 10,
+ },
+ identity: {
+ fullName: "SMTP Display Name",
+ email: "imapuser@host.invalid",
+ replyTo: "imapuser@host.invalid",
+ organization: "SMTP Organization Name",
+ },
+ smtpServer: {
+ hostname: "smtp.host.invalid",
+ username: "smtpuser",
+ port: 0, // default port
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ socketType: Ci.nsMsgSocketType.plain,
+ },
+};
+
+function teardown() {
+ for (let server of MailServices.smtp.servers) {
+ MailServices.smtp.deleteServer(server);
+ }
+
+ teardown_mock_registry();
+}
+
+function _test(registry, expectedAccount) {
+ try {
+ setup_mock_registry(registry);
+ new SettingsImportHelper(null, "Outlook", [expectedAccount]).beginImport();
+ } catch (e) {
+ teardown();
+ do_throw(e);
+ }
+ teardown();
+}
+
+function run_test() {
+ _test(new Outlook2003Registry(new POP3Account()), expectedPop3Account);
+ _test(new Outlook2003Registry(new IMAPAccount()), expectedImapAccount);
+
+ _test(new Outlook98Registry(new POP3Account()), expectedPop3Account);
+ _test(new Outlook98Registry(new IMAPAccount()), expectedImapAccount);
+}
diff --git a/comm/mailnews/import/test/unit/test_shiftjis_csv.js b/comm/mailnews/import/test/unit/test_shiftjis_csv.js
new file mode 100644
index 0000000000..bd4416a09a
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_shiftjis_csv.js
@@ -0,0 +1,20 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ let file = do_get_file("resources/shiftjis_addressbook.csv");
+ let helper = new AbImportHelper(
+ file,
+ "csv",
+ "shiftjis_addressbook",
+ "shiftjis_csv"
+ );
+
+ helper.setFieldMap(helper.getDefaultFieldMap(true));
+ helper.beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_utf16_csv.js b/comm/mailnews/import/test/unit/test_utf16_csv.js
new file mode 100644
index 0000000000..cf64e72c0a
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_utf16_csv.js
@@ -0,0 +1,20 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ let file = do_get_file("resources/utf16_addressbook.csv");
+ let helper = new AbImportHelper(
+ file,
+ "csv",
+ "utf16_addressbook",
+ "utf16_csv"
+ );
+
+ helper.setFieldMap(helper.getDefaultFieldMap(true));
+ helper.beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_vcard_import.js b/comm/mailnews/import/test/unit/test_vcard_import.js
new file mode 100644
index 0000000000..9ba0c5511a
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_vcard_import.js
@@ -0,0 +1,34 @@
+/**
+ * Tests importing an address book export in the LDAP data interchange (LDIF)
+ * format and checks the accuracy of the imported address book's cards.
+ * The current export contains only one card with most fields full.
+ *
+ * This test also checks for the following bugs:
+ * -Bug 439819: LDIF import does not include mozillahomestreet.
+ * -Bug 182128: Edit Card, Notes on several lines appear on one after
+ * export/import in text format *(only tests the import).
+ */
+function run_test() {
+ // Due to the import code using nsIAbManager off the main thread, we need
+ // to ensure that it is initialized before we start the main test.
+ MailServices.ab;
+
+ // test regular import (e.g. from file exported by another mail client)
+ let file = do_get_file("resources/basic_vcard_addressbook.vcf");
+ new AbImportHelper(
+ file,
+ "vcf",
+ "basic_vcard_addressbook",
+ "vcard_import"
+ ).beginImport();
+
+ // test import against file with extra newlines (e.g. as copy-pasted by
+ // hand, a relatively unlikely but still reasonable use case to cover)
+ file = do_get_file("resources/emptylines_vcard_addressbook.vcf");
+ new AbImportHelper(
+ file,
+ "vcf",
+ "emptylines_vcard_addressbook",
+ "vcard_import"
+ ).beginImport();
+}
diff --git a/comm/mailnews/import/test/unit/test_winmail.js b/comm/mailnews/import/test/unit/test_winmail.js
new file mode 100644
index 0000000000..73de9a1451
--- /dev/null
+++ b/comm/mailnews/import/test/unit/test_winmail.js
@@ -0,0 +1,178 @@
+/**
+ * Basic tests for importing accounts of Windows Live Mail.
+ */
+
+/* import-globals-from resources/mock_windows_reg_factory.js */
+load("resources/mock_windows_reg_factory.js");
+
+var expectedPop3TestTestAccount = {
+ incomingServer: {
+ type: "pop3",
+ hostName: "pop3.test.test",
+ prettyName: "testpopaccountname",
+ port: 110,
+ socketType: 0,
+ doBiff: true,
+ biffMinutes: 2,
+ isSecure: false,
+ username: "testpopusername",
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ leaveMessagesOnServer: false,
+ deleteMailLeftOnServer: false,
+ deleteByAgeFromServer: false,
+ numDaysToLeaveOnServer: 7,
+ },
+ identity: {
+ fullName: "popdisplayname",
+ organization: "",
+ email: "testpop@invalid.invalid",
+ },
+ smtpServer: {
+ hostname: "smtp.pop.test",
+ port: 0, // default port
+ username: "",
+ authMethod: Ci.nsMsgAuthMethod.none,
+ socketType: 0,
+ },
+};
+
+var expectedNewsMozillaOrgAccount = {
+ incomingServer: {
+ type: "nntp",
+ hostName: "testnews.mozilla.org",
+ prettyName: "accountnamemozillanews",
+ port: 119,
+ username: "",
+ socketType: 0,
+ isSecure: false,
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ doBiff: false,
+ biffMinutes: 10, // default value
+ },
+ identity: {
+ fullName: "test",
+ organization: "",
+ email: "mozillanews@invalid.invalid",
+ },
+};
+
+var expectedMicrosoftCommunitiesAccount = {
+ incomingServer: {
+ type: "nntp",
+ hostName: "testmsnews.microsoft.invalid",
+ prettyName: "Microsoft Communities Test",
+ port: 119,
+ username: "",
+ socketType: 0,
+ isSecure: false,
+ authMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ doBiff: false,
+ biffMinutes: 10, // default value
+ },
+ identity: {
+ fullName: "",
+ organization: "",
+ },
+};
+
+var expectedDonHallNntpAccount = {
+ incomingServer: {
+ type: "nntp",
+ hostName: "news.wingtiptoys.invalid",
+ prettyName: "donhallnntp",
+ port: 563,
+ username: "don",
+ isSecure: false,
+ authMethod: Ci.nsMsgAuthMethod.secure,
+ socketType: 0,
+ doBiff: false,
+ biffMinutes: 10, // default value
+ },
+ identity: {
+ fullName: "Don Hall",
+ organization: "Wingtip Toys",
+ email: "don@wingtiptoys.invalid",
+ replyTo: "don@wingtiptoys.invalid",
+ },
+};
+
+var expectedDonHallImapAccount = {
+ incomingServer: {
+ type: "imap",
+ hostName: "mail.wingtiptoys.invalid",
+ prettyName: "donhallimap",
+ port: 993,
+ isSecure: true,
+ doBiff: true,
+ biffMinutes: 2,
+ username: "don",
+ authMethod: Ci.nsMsgAuthMethod.secure,
+ socketType: Ci.nsMsgSocketType.SSL,
+ },
+ identity: {
+ fullName: "Don Hall",
+ organization: "Wingtip Toys",
+ email: "don@wingtiptoys.invalid",
+ replyTo: "don@wingtiptoys.invalid",
+ },
+ smtpServer: {
+ hostname: "smtp.wingtiptoys.invalid",
+ username: "don",
+ port: 25,
+ socketType: Ci.nsMsgSocketType.SSL,
+ authMethod: Ci.nsMsgAuthMethod.secure,
+ },
+};
+
+var expectedAccounts = [
+ expectedPop3TestTestAccount,
+ expectedNewsMozillaOrgAccount,
+ expectedMicrosoftCommunitiesAccount,
+ expectedDonHallNntpAccount,
+ expectedDonHallImapAccount,
+];
+
+function WinLiveMailRegistry(rootPath) {
+ this._rootPath = rootPath;
+}
+
+WinLiveMailRegistry.prototype = {
+ get "Software\\Microsoft\\Windows Live Mail"() {
+ return {
+ "Default Mail Account": "fill in mail account",
+ "Default News Account": "fill in news account",
+ "Store Root": this._rootPath,
+ mail: {
+ "Poll For Mail": 120000,
+ },
+ };
+ },
+};
+
+function _test(registry) {
+ try {
+ setup_mock_registry(registry);
+ new SettingsImportHelper(
+ null,
+ "Windows Live Mail",
+ expectedAccounts
+ ).beginImport();
+ } catch (e) {
+ teardown();
+ do_throw(e);
+ }
+ teardown();
+}
+
+function teardown() {
+ for (let server of MailServices.smtp.servers) {
+ MailServices.smtp.deleteServer(server);
+ }
+
+ teardown_mock_registry();
+}
+
+function run_test() {
+ let root = do_get_file("resources/WindowsLiveMail");
+ _test(new WinLiveMailRegistry(root.path));
+}
diff --git a/comm/mailnews/import/test/unit/xpcshell.ini b/comm/mailnews/import/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..891ba63d96
--- /dev/null
+++ b/comm/mailnews/import/test/unit/xpcshell.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+head = head_import.js
+tail =
+support-files = resources/*
+
+[test_AddrBookFileImporter.js]
+[test_bug_263304.js]
+[test_bug_437556.js]
+[test_csv_GetSample.js]
+[test_becky_addressbook.js]
+run-if = os == 'win'
+tags = vcard
+[test_becky_filters.js]
+run-if = os == 'win'
+[test_csv_import.js]
+[test_csv_import_quote.js]
+[test_ldif_import.js]
+[test_outlook_settings.js]
+run-if = os == 'win'
+[test_shiftjis_csv.js]
+[test_ThunderbirdProfileImporter.js]
+[test_utf16_csv.js]
+[test_vcard_import.js]
+tags = vcard
+[test_winmail.js]
+run-if = os == 'win'
diff --git a/comm/mailnews/intl/charsetData.properties b/comm/mailnews/intl/charsetData.properties
new file mode 100644
index 0000000000..d4a4147cda
--- /dev/null
+++ b/comm/mailnews/intl/charsetData.properties
@@ -0,0 +1,104 @@
+# 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/.
+
+## Rule of this file:
+## 1. key should always be in lower case ascii so we can do case insensitive
+## comparison in the code faster.
+
+## Format of this file:
+##
+## charset_name.isInternal = anything - specifies that this charset should
+## not be exposed to web content because of the vulnerability to XSS attacks
+## or some other reasons
+##
+## charset_name.LangGroup =
+##
+## charset_name.isMultibyte = multi byte charsets
+
+replacement.isInternal = true
+
+# XXX : todo: move to something based on BCP 47 (RFC 5646);
+# these should primarily specify script (and sometimes region),
+# but NOT language.
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=756022
+# e.g. x-western -> *-Latn-155 (Western Europe),
+# *-Latn-151 (Eastern Europe),
+# *-Latn-154 (Northern Europe),
+# *-Latn-TR
+# x-cyrillic -> *-Cyrl
+# zh-TW -> *-Hant-TW
+# zh-HK -> *-Hant-HK
+# zh-CN -> *-Hans
+# ja -> *-Jpan
+# ko -> *-Hang
+# he -> *-Hebr
+# ar -> *-Arab
+# etc
+
+big5.LangGroup = zh-TW
+big5-hkscs.LangGroup = zh-HK
+euc-jp.LangGroup = ja
+euc-kr.LangGroup = ko
+gb2312.LangGroup = zh-CN
+gb18030.LangGroup = zh-CN
+gb18030.2000-0.LangGroup = zh-CN
+gb18030.2000-1.LangGroup = zh-CN
+hkscs-1.LangGroup = zh-HK
+ibm866.LangGroup = x-cyrillic
+ibm1125.LangGroup = x-cyrillic
+ibm1131.LangGroup = x-cyrillic
+iso-2022-jp.LangGroup = ja
+iso-8859-1.LangGroup = x-western
+iso-8859-10.LangGroup = x-western
+iso-8859-14.LangGroup = x-western
+iso-8859-15.LangGroup = x-western
+iso-8859-2.LangGroup = x-western
+iso-8859-16.LangGroup = x-western
+iso-8859-3.LangGroup = x-western
+iso-8859-4.LangGroup = x-western
+iso-8859-13.LangGroup = x-western
+iso-8859-5.LangGroup = x-cyrillic
+iso-8859-6.LangGroup = ar
+iso-8859-7.LangGroup = el
+iso-8859-8.LangGroup = he
+iso-8859-8-i.LangGroup = he
+jis_0208-1983.LangGroup = ja
+koi8-r.LangGroup = x-cyrillic
+koi8-u.LangGroup = x-cyrillic
+shift_jis.LangGroup = ja
+windows-874.LangGroup = th
+utf-8.LangGroup = x-unicode
+utf-16.LangGroup = x-unicode
+utf-16be.LangGroup = x-unicode
+utf-16le.LangGroup = x-unicode
+utf-7.LangGroup = x-unicode
+replacement.LangGroup = x-unicode
+windows-1250.LangGroup = x-western
+windows-1251.LangGroup = x-cyrillic
+windows-1252.LangGroup = x-western
+windows-1253.LangGroup = el
+windows-1254.LangGroup = x-western
+windows-1255.LangGroup = he
+windows-1256.LangGroup = ar
+windows-1257.LangGroup = x-western
+windows-1258.LangGroup = x-western
+gbk.LangGroup = zh-CN
+
+# The following two are in the Encoding Standard (https://encoding.spec.whatwg.org/),
+# x-mac-ukrainian is a label.
+x-mac-cyrillic.LangGroup = x-cyrillic
+macintosh.LangGroup = x-western
+
+x-user-defined.LangGroup = x-unicode
+
+iso-2022-jp.isMultibyte = true
+shift_jis.isMultibyte = true
+euc-jp.isMultibyte = true
+big5.isMultibyte = true
+big5-hkscs.isMultibyte = true
+gb2312.isMultibyte = true
+euc-kr.isMultibyte = true
+utf-7.isMultibyte = true
+utf-8.isMultibyte = true
+replacement.isMultibyte = true
diff --git a/comm/mailnews/intl/charsetalias.properties b/comm/mailnews/intl/charsetalias.properties
new file mode 100644
index 0000000000..1ef5e52ee2
--- /dev/null
+++ b/comm/mailnews/intl/charsetalias.properties
@@ -0,0 +1,151 @@
+# 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/.
+
+# Rule of this file:
+# 1. key should always be in lower case ascii so we can do case insensitive
+# comparison in the code faster.
+# 2. value should be the _name_ used in the WHATWG Encoding Standard
+# https://encoding.spec.whatwg.org/ (of "UTF-7" for UTF-7).
+#
+# This file contains email-specific labels. Web-relevant labels for
+# encodings are in the Encoding Standard / encoding_rs.
+
+# Added for Solaris ns_langinfo. Unlikely relevant to email.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=77300#c9
+646=windows-1252
+
+# Aliases for ISO-8859-8-I
+# From the original IBM bidi patch.
+iso-8859-8i=ISO-8859-8-I
+
+# ISO 8859 series with underscore for JavaMail
+# compat.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=820767
+iso8859_1=windows-1252
+iso8859_2=ISO-8859-2
+iso8859_3=ISO-8859-3
+iso8859_4=ISO-8859-4
+iso8859_5=ISO-8859-5
+iso8859_6=ISO-8859-6
+iso8859_7=ISO-8859-7
+# Unclear if 8 with underscore was visual or not
+iso8859_9=windows-1254
+# No evidence of 10 occurring with underscore
+# 11 is tis620
+# 12 does not exist
+iso8859_13=ISO-8859-13
+# No evidence of 14 occurring with underscore
+iso8859_15=ISO-8859-15
+# No evidence of 16 occurring with underscore
+
+koi8r=KOI8-R
+
+# Code pages shared by DOS and Windows with ms prefix.
+# Evidence of this pattern in the wild:
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1120813
+# Plausible cause: JavaMail
+# The ms prefix as used by Sun is not relevant to windows-125x series
+ms874=windows-874
+# ms932 was added to the Encoding Standard as one-off Thunderbird request
+# MS936 shows up at https://www.iana.org/assignments/character-sets/character-sets.xhtml
+ms936=GBK
+ms949=EUC-KR
+ms950=Big5
+ms950_hkscs=Big5
+
+# Underscore versions of Unix CJK encodings.
+# No evidence of these in the wild, but these could plausibly
+# occur for the same reason as the above two groups.
+euc_cn=GBK
+euc_kr=EUC-KR
+euc_jp=EUC-JP
+big5_hkscs=Big5
+
+# Code pages shared by DOS and Windows with cp prefix
+# cp125x series are in the Encoding Standard
+# Evidence of the pattern in the wild:
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1511950
+# https://bugzilla.mozilla.org/show_bug.cgi?id=542823
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1217161
+cp874=windows-874
+cp932=Shift_JIS
+# CP936 shows up at https://www.iana.org/assignments/character-sets/character-sets.xhtml
+cp936=GBK
+cp949=EUC-KR
+cp950=Big5
+
+# Aliases for ISO-2022-JP
+# The following are really not aliases ISO-2022-JP, but sharing the same decoder
+# Kept mainly for compat with old Apple Mail.
+iso-2022-jp-2=ISO-2022-JP
+csiso2022jp2=ISO-2022-JP
+# A Google search suggests the variant without hyphens has been used with
+# JavaMail.
+iso2022jp=ISO-2022-JP
+
+# Aliases for Big5
+# Added in patch that generally meant to support emails sent by
+# dtmail on Sun Solaris
+# https://bugzilla.mozilla.org/show_bug.cgi?id=146287
+zh_tw-big5=Big5
+
+# Aliases for EUC-KR
+# Added for Solaris ns_langinfo. Unlikely relevant to email.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=82075
+5601=EUC-KR
+# https://bugzilla.mozilla.org/show_bug.cgi?id=234958
+x-windows-949=EUC-KR
+
+# Aliases for windows-874
+# Added originally for nl_langinfo reasons but could plausibly be sent
+# by JavaMail.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=101295
+tis620=windows-874
+
+# Aliases for IBM866
+# This alias may have been made up by accident and may
+# not be relevant to real-world email.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=77588
+cp-866=IBM866
+
+# Aliases for UTF-7
+utf-7=UTF-7
+# The below 4 aliases were not in Thunderbird 60, and there were
+# no complaints.
+# This alias appears to have been generated by the email part
+# of the Netscape 4.0 suite per http://jkorpela.fi/chars.html
+x-unicode-2-0-utf-7=UTF-7
+# This appears to be just a made-up non-x version of the above
+# (checked in without bug number).
+unicode-2-0-utf-7=UTF-7
+# The two aliases below show up at
+# https://www.iana.org/assignments/character-sets/character-sets.xhtml
+unicode-1-1-utf-7=UTF-7
+csunicode11utf7=UTF-7
+
+# The below aliases were not in Thunderbird 60, and there were
+# no complaints.
+# These aliases show up at
+# https://www.iana.org/assignments/character-sets/character-sets.xhtml
+csunicode=UTF-16BE
+csunicode11=UTF-16BE
+iso-10646-ucs-basic=UTF-16BE
+csunicodeascii=UTF-16BE
+iso-10646-unicode-latin1=UTF-16BE
+csunicodelatin1=UTF-16BE
+iso-10646=UTF-16BE
+iso-10646-j-1=UTF-16BE
+iso-10646-ucs-2=UTF-16BE
+# Netscape aliases checked in without bug number.
+# Possibly meant to be Netscape-private.
+x-iso-10646-ucs-2-be=UTF-16BE
+x-iso-10646-ucs-2-le=UTF-16LE
+
+# Shows up at https://www.iana.org/assignments/character-sets/character-sets.xhtml
+# https://bugzilla.mozilla.org/show_bug.cgi?id=651113
+windows-936=GBK
+
+# Added for Solaris ns_langinfo(). Unlikely to be relevant to email.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=82075
+ansi-1251=windows-1251
diff --git a/comm/mailnews/intl/components.conf b/comm/mailnews/intl/components.conf
new file mode 100644
index 0000000000..6c1f128442
--- /dev/null
+++ b/comm/mailnews/intl/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{3c1c0163-9bd0-11d3-9d09-0050040007b2}",
+ "contract_ids": ["@mozilla.org/charset-converter-manager;1"],
+ "type": "nsCharsetConverterManager",
+ "headers": ["/comm/mailnews/intl/nsCharsetConverterManager.h"],
+ },
+]
diff --git a/comm/mailnews/intl/jar.mn b/comm/mailnews/intl/jar.mn
new file mode 100644
index 0000000000..ab02275d97
--- /dev/null
+++ b/comm/mailnews/intl/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+toolkit.jar:
+ res/charsetData.properties (charsetData.properties)
diff --git a/comm/mailnews/intl/moz.build b/comm/mailnews/intl/moz.build
new file mode 100644
index 0000000000..03b8a13a46
--- /dev/null
+++ b/comm/mailnews/intl/moz.build
@@ -0,0 +1,42 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsICharsetConverterManager.idl",
+]
+
+UNIFIED_SOURCES += [
+ "nsCharsetAlias.cpp",
+ "nsCharsetConverterManager.cpp",
+ "nsMUTF7ToUnicode.cpp",
+ "nsUnicodeToMUTF7.cpp",
+ "nsUnicodeToUTF7.cpp",
+ "nsUTF7ToUnicode.cpp",
+]
+
+XPIDL_MODULE = "commuconv"
+
+LOCAL_INCLUDES += [
+ "/intl/locale",
+]
+
+GENERATED_FILES += [
+ "charsetalias.properties.h",
+]
+charsetalias = GENERATED_FILES["charsetalias.properties.h"]
+charsetalias.script = "/intl/locale/props2arrays.py"
+charsetalias.inputs = ["charsetalias.properties"]
+
+FINAL_LIBRARY = "mail"
+
+# Tests need more attention before they can be enabled.
+TEST_DIRS += ["test"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/intl/nsCharsetAlias.cpp b/comm/mailnews/intl/nsCharsetAlias.cpp
new file mode 100644
index 0000000000..a92cf193c9
--- /dev/null
+++ b/comm/mailnews/intl/nsCharsetAlias.cpp
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Encoding.h"
+
+#include "nsCharsetAlias.h"
+
+// for NS_ERROR_UCONV_NOCONV
+#include "nsCharsetConverterManager.h"
+
+#include "nsUConvPropertySearch.h"
+
+using namespace mozilla;
+
+//
+static const nsUConvProp kAliases[] = {
+#include "charsetalias.properties.h"
+};
+
+//--------------------------------------------------------------
+// static
+nsresult nsCharsetAlias::GetPreferredInternal(const nsACString& aAlias,
+ nsACString& oResult) {
+ // First check charsetalias.properties and if there is no match, continue to
+ // call Encoding::ForLabel.
+ nsAutoCString key(aAlias);
+ ToLowerCase(key);
+
+ nsresult rv = nsUConvPropertySearch::SearchPropertyValue(
+ kAliases, ArrayLength(kAliases), key, oResult);
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+
+ const Encoding* encoding = Encoding::ForLabel(key);
+ if (!encoding) return NS_ERROR_NOT_AVAILABLE;
+ encoding->Name(oResult);
+ return NS_OK;
+}
+
+//--------------------------------------------------------------
+// static
+nsresult nsCharsetAlias::GetPreferred(const nsACString& aAlias,
+ nsACString& oResult) {
+ if (aAlias.IsEmpty()) return NS_ERROR_NULL_POINTER;
+
+ nsresult res = GetPreferredInternal(aAlias, oResult);
+ if (NS_FAILED(res)) return res;
+
+ if (nsCharsetConverterManager::IsInternal(oResult))
+ return NS_ERROR_UCONV_NOCONV;
+
+ return res;
+}
+
+//--------------------------------------------------------------
+// static
+nsresult nsCharsetAlias::Equals(const nsACString& aCharset1,
+ const nsACString& aCharset2, bool* oResult) {
+ nsresult res = NS_OK;
+
+ if (aCharset1.Equals(aCharset2, nsCaseInsensitiveCStringComparator)) {
+ *oResult = true;
+ return res;
+ }
+
+ if (aCharset1.IsEmpty() || aCharset2.IsEmpty()) {
+ *oResult = false;
+ return res;
+ }
+
+ *oResult = false;
+ nsAutoCString name1;
+ res = GetPreferredInternal(aCharset1, name1);
+ if (NS_FAILED(res)) return res;
+
+ nsAutoCString name2;
+ res = GetPreferredInternal(aCharset2, name2);
+ if (NS_FAILED(res)) return res;
+
+ *oResult = name1.Equals(name2);
+ return NS_OK;
+}
diff --git a/comm/mailnews/intl/nsCharsetAlias.h b/comm/mailnews/intl/nsCharsetAlias.h
new file mode 100644
index 0000000000..6b24f33567
--- /dev/null
+++ b/comm/mailnews/intl/nsCharsetAlias.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsCharsetAlias_h___
+#define nsCharsetAlias_h___
+
+#include "nscore.h"
+#include "nsString.h"
+
+class nsCharsetConverterManager;
+class nsScriptableUnicodeConverter;
+
+class nsCharsetAlias {
+ friend class nsCharsetConverterManager;
+ friend class nsScriptableUnicodeConverter;
+ static nsresult GetPreferredInternal(const nsACString& aAlias,
+ nsACString& aResult);
+
+ public:
+ static nsresult GetPreferred(const nsACString& aAlias, nsACString& aResult);
+ static nsresult Equals(const nsACString& aCharset1,
+ const nsACString& aCharset2, bool* aResult);
+};
+
+#endif /* nsCharsetAlias_h___ */
diff --git a/comm/mailnews/intl/nsCharsetConverterManager.cpp b/comm/mailnews/intl/nsCharsetConverterManager.cpp
new file mode 100644
index 0000000000..a15917206e
--- /dev/null
+++ b/comm/mailnews/intl/nsCharsetConverterManager.cpp
@@ -0,0 +1,184 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsCharsetAlias.h"
+#include "nsICharsetConverterManager.h"
+#include "nsIStringBundle.h"
+#include "nsTArray.h"
+#include "mozilla/Components.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "../base/src/nsMsgI18N.h"
+
+// just for CONTRACTIDs
+#include "nsCharsetConverterManager.h"
+
+static nsCOMPtr<nsIStringBundle> sDataBundle;
+static nsCOMPtr<nsIStringBundle> sTitleBundle;
+
+// Class nsCharsetConverterManager [implementation]
+
+NS_IMPL_ISUPPORTS(nsCharsetConverterManager, nsICharsetConverterManager)
+
+nsCharsetConverterManager::nsCharsetConverterManager() {}
+
+nsCharsetConverterManager::~nsCharsetConverterManager() {
+ sDataBundle = nullptr;
+ sTitleBundle = nullptr;
+}
+
+static nsresult LoadBundle(const char* aBundleURLSpec,
+ nsIStringBundle** aResult) {
+ nsCOMPtr<nsIStringBundleService> sbServ =
+ mozilla::components::StringBundle::Service();
+ if (!sbServ) return NS_ERROR_FAILURE;
+
+ return sbServ->CreateBundle(aBundleURLSpec, aResult);
+}
+
+static nsresult GetBundleValueInner(nsIStringBundle* aBundle, const char* aName,
+ const nsString& aProp, nsAString& aResult) {
+ nsAutoString key;
+
+ CopyASCIItoUTF16(mozilla::MakeStringSpan(aName), key);
+ ToLowerCase(key); // we lowercase the main comparison key
+ key.Append(aProp);
+
+ return aBundle->GetStringFromName(NS_ConvertUTF16toUTF8(key).get(), aResult);
+}
+
+static nsresult GetBundleValue(nsIStringBundle* aBundle, const char* aName,
+ const nsString& aProp, nsAString& aResult) {
+ nsresult rv = NS_OK;
+
+ nsAutoString value;
+ rv = GetBundleValueInner(aBundle, aName, aProp, value);
+ if (NS_FAILED(rv)) return rv;
+
+ aResult = value;
+
+ return NS_OK;
+}
+
+static nsresult GetCharsetDataImpl(const char* aCharset, const char16_t* aProp,
+ nsAString& aResult) {
+ NS_ENSURE_ARG_POINTER(aCharset);
+ // aProp can be nullptr
+
+ if (!sDataBundle) {
+ nsresult rv = LoadBundle("resource://gre-resources/charsetData.properties",
+ getter_AddRefs(sDataBundle));
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ return GetBundleValue(sDataBundle, aCharset, nsDependentString(aProp),
+ aResult);
+}
+
+// static
+bool nsCharsetConverterManager::IsInternal(const nsACString& aCharset) {
+ nsAutoString str;
+ // fully qualify to possibly avoid vtable call
+ nsresult rv = GetCharsetDataImpl(PromiseFlatCString(aCharset).get(),
+ u".isInternal", str);
+
+ return NS_SUCCEEDED(rv);
+}
+
+//----------------------------------------------------------------------------//----------------------------------------------------------------------------
+// Interface nsICharsetConverterManager [implementation]
+
+// XXX Improve the implementation of this method. Right now, it is build on
+// top of the nsCharsetAlias service. We can make the nsCharsetAlias
+// better, with its own hash table (not the StringBundle anymore) and
+// a nicer file format.
+NS_IMETHODIMP
+nsCharsetConverterManager::GetCharsetAlias(const char* aCharset,
+ nsACString& aResult) {
+ NS_ENSURE_ARG_POINTER(aCharset);
+
+ // We try to obtain the preferred name for this charset from the charset
+ // aliases.
+ nsresult rv;
+
+ rv = nsCharsetAlias::GetPreferred(nsDependentCString(aCharset), aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::GetCharsetTitle(const char* aCharset,
+ nsAString& aResult) {
+ NS_ENSURE_ARG_POINTER(aCharset);
+
+ if (!sTitleBundle) {
+ nsresult rv =
+ LoadBundle("chrome://messenger/locale/charsetTitles.properties",
+ getter_AddRefs(sTitleBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return GetBundleValue(sTitleBundle, aCharset, u".title"_ns, aResult);
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::GetCharsetData(const char* aCharset,
+ const char16_t* aProp,
+ nsAString& aResult) {
+ return GetCharsetDataImpl(aCharset, aProp, aResult);
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::GetCharsetLangGroup(const char* aCharset,
+ nsACString& aResult) {
+ // resolve the charset first
+ nsAutoCString charset;
+
+ nsresult rv = GetCharsetAlias(aCharset, charset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // fully qualify to possibly avoid vtable call
+ return nsCharsetConverterManager::GetCharsetLangGroupRaw(charset.get(),
+ aResult);
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::GetCharsetLangGroupRaw(const char* aCharset,
+ nsACString& aResult) {
+ nsAutoString langGroup;
+ // fully qualify to possibly avoid vtable call
+ nsresult rv = nsCharsetConverterManager::GetCharsetData(
+ aCharset, u".LangGroup", langGroup);
+
+ if (NS_SUCCEEDED(rv)) {
+ ToLowerCase(langGroup); // use lowercase for all language groups
+ aResult = NS_ConvertUTF16toUTF8(langGroup);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::Utf7ToUnicode(const nsACString& aSrc,
+ nsAString& aDest) {
+ return CopyUTF7toUTF16(aSrc, aDest);
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::Mutf7ToUnicode(const nsACString& aSrc,
+ nsAString& aDest) {
+ return CopyMUTF7toUTF16(aSrc, aDest);
+}
+
+NS_IMETHODIMP
+nsCharsetConverterManager::UnicodeToMutf7(const nsAString& aSrc,
+ nsACString& aDest) {
+ return CopyUTF16toMUTF7(aSrc, aDest);
+}
diff --git a/comm/mailnews/intl/nsCharsetConverterManager.h b/comm/mailnews/intl/nsCharsetConverterManager.h
new file mode 100644
index 0000000000..9a217bf0e6
--- /dev/null
+++ b/comm/mailnews/intl/nsCharsetConverterManager.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsCharsetConverterManager_h__
+#define nsCharsetConverterManager_h__
+
+#include "nsICharsetConverterManager.h"
+
+class nsCharsetAlias;
+
+class nsCharsetConverterManager : public nsICharsetConverterManager {
+ friend class nsCharsetAlias;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICHARSETCONVERTERMANAGER
+
+ public:
+ nsCharsetConverterManager();
+
+ private:
+ virtual ~nsCharsetConverterManager();
+
+ static bool IsInternal(const nsACString& aCharset);
+};
+
+#endif // nsCharsetConverterManager_h__
diff --git a/comm/mailnews/intl/nsICharsetConverterManager.idl b/comm/mailnews/intl/nsICharsetConverterManager.idl
new file mode 100644
index 0000000000..fe77ed6843
--- /dev/null
+++ b/comm/mailnews/intl/nsICharsetConverterManager.idl
@@ -0,0 +1,71 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#include "mozilla/Encoding.h"
+
+// XXX change to NS_CHARSETCONVERTERMANAGER_CID
+#define NS_ICHARSETCONVERTERMANAGER_CID \
+ {0x3c1c0163, 0x9bd0, 0x11d3, { 0x9d, 0x9, 0x0, 0x50, 0x4, 0x0, 0x7, 0xb2}}
+
+#define NS_CHARSETCONVERTERMANAGER_CONTRACTID "@mozilla.org/charset-converter-manager;1"
+%}
+
+[scriptable, uuid(a0550d46-8d9c-47dd-acc7-c083620dff12)]
+interface nsICharsetConverterManager : nsISupports
+{
+ /**
+ * A shortcut to calling nsICharsetAlias to do alias resolution
+ * @throws if aCharset is an unknown charset.
+ */
+ ACString getCharsetAlias(in string aCharset);
+
+ /**
+ * Get the human-readable name for the given charset.
+ * @throws if aCharset is an unknown charset.
+ */
+ AString getCharsetTitle(in string aCharset);
+
+ /**
+ * Get some data about the given charset. This includes whether the
+ * character encoding may be used for certain purposes, if it is
+ * multi-byte, and the language code for it. See charsetData.properties
+ * for the source of this data. Some known property names:
+ * LangGroup - language code for charset, e.g. 'he' and 'zh-CN'.
+ * isMultibyte - is this a multi-byte charset?
+ * isInternal - not to be used in untrusted web content.
+ *
+ * @param aCharset name of the character encoding, e.g. 'iso-8859-15'.
+ * @param aProp property desired for the character encoding.
+ * @throws if aCharset is an unknown charset.
+ * @return the value of the property, for the character encoding.
+ */
+ AString getCharsetData(in string aCharset,
+ in wstring aProp);
+
+ /**
+ * Get the language group for the given charset. This is similar to
+ * calling <tt>getCharsetData</tt> with the <tt>prop</tt> "LangGroup".
+ *
+ * @param aCharset name of the character encoding, e.g. 'iso-8859-15'.
+ * @throws if aCharset is an unknown charset.
+ * @return the language code for the character encoding.
+ */
+ AUTF8String getCharsetLangGroup(in string aCharset);
+ AUTF8String getCharsetLangGroupRaw(in string aCharset);
+
+ /**
+ * Decoding of UTF-7 in message headers and bodies.
+ */
+ AString utf7ToUnicode(in ACString aMutf7);
+
+ /**
+ * Support for Modified UTF-7 (MUTF-7) used by IMAP.
+ */
+ AString mutf7ToUnicode(in ACString aMutf7);
+ ACString unicodeToMutf7(in AString aUnicode);
+};
diff --git a/comm/mailnews/intl/nsMUTF7ToUnicode.cpp b/comm/mailnews/intl/nsMUTF7ToUnicode.cpp
new file mode 100644
index 0000000000..bd49d647fa
--- /dev/null
+++ b/comm/mailnews/intl/nsMUTF7ToUnicode.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMUTF7ToUnicode.h"
+
+//----------------------------------------------------------------------
+// Class nsMUTF7ToUnicode [implementation]
+
+nsMUTF7ToUnicode::nsMUTF7ToUnicode() : nsBasicUTF7Decoder(',', '&') {}
diff --git a/comm/mailnews/intl/nsMUTF7ToUnicode.h b/comm/mailnews/intl/nsMUTF7ToUnicode.h
new file mode 100644
index 0000000000..ff26e8b6ab
--- /dev/null
+++ b/comm/mailnews/intl/nsMUTF7ToUnicode.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMUTF7ToUnicode_h___
+#define nsMUTF7ToUnicode_h___
+
+#include "nsUTF7ToUnicode.h"
+
+//----------------------------------------------------------------------
+// Class nsMUTF7ToUnicode [declaration]
+
+/**
+ * A character set converter from Modified UTF7 to Unicode.
+ *
+ * @created 18/May/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsMUTF7ToUnicode : public nsBasicUTF7Decoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsMUTF7ToUnicode();
+};
+
+#endif /* nsMUTF7ToUnicode_h___ */
diff --git a/comm/mailnews/intl/nsUTF7ToUnicode.cpp b/comm/mailnews/intl/nsUTF7ToUnicode.cpp
new file mode 100644
index 0000000000..2257affa51
--- /dev/null
+++ b/comm/mailnews/intl/nsUTF7ToUnicode.cpp
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUTF7ToUnicode.h"
+
+#define ENC_DIRECT 0
+#define ENC_BASE64 1
+
+//----------------------------------------------------------------------
+// Class nsBasicUTF7Decoder [implementation]
+
+nsBasicUTF7Decoder::nsBasicUTF7Decoder(char aLastChar, char aEscChar) {
+ mLastChar = aLastChar;
+ mEscChar = aEscChar;
+ mFreshBase64 = false;
+ Reset();
+}
+
+nsresult nsBasicUTF7Decoder::DecodeDirect(const char* aSrc, int32_t* aSrcLength,
+ char16_t* aDest,
+ int32_t* aDestLength) {
+ const char* srcEnd = aSrc + *aSrcLength;
+ const char* src = aSrc;
+ char16_t* destEnd = aDest + *aDestLength;
+ char16_t* dest = aDest;
+ nsresult res = NS_OK;
+ char ch;
+
+ while (src < srcEnd) {
+ ch = *src;
+
+ // stop when we meet other chars or end of direct encoded seq.
+ // if (!(DirectEncodable(ch)) || (ch == mEscChar)) {
+ // but we are decoding; so we should be lax; pass everything until escchar
+ if (ch == mEscChar) {
+ res = NS_ERROR_UDEC_ILLEGALINPUT;
+ break;
+ }
+
+ if (dest >= destEnd) {
+ res = NS_OK_UDEC_MOREOUTPUT;
+ break;
+ } else {
+ *dest++ = ch;
+ src++;
+ }
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+nsresult nsBasicUTF7Decoder::DecodeBase64(const char* aSrc, int32_t* aSrcLength,
+ char16_t* aDest,
+ int32_t* aDestLength) {
+ const char* srcEnd = aSrc + *aSrcLength;
+ const char* src = aSrc;
+ char16_t* destEnd = aDest + *aDestLength;
+ char16_t* dest = aDest;
+ nsresult res = NS_OK;
+ char ch;
+ uint32_t value;
+
+ while (src < srcEnd) {
+ ch = *src;
+
+ // stop when we meet other chars or end of direct encoded seq.
+ value = CharToValue(ch);
+ if (value > 0xff) {
+ res = NS_ERROR_UDEC_ILLEGALINPUT;
+ break;
+ }
+
+ switch (mEncStep) {
+ case 0:
+ mEncBits = value << 10;
+ break;
+ case 1:
+ mEncBits += value << 4;
+ break;
+ case 2:
+ if (dest >= destEnd) {
+ res = NS_OK_UDEC_MOREOUTPUT;
+ break;
+ }
+ mEncBits += value >> 2;
+ *(dest++) = (char16_t)mEncBits;
+ mEncBits = (value & 0x03) << 14;
+ break;
+ case 3:
+ mEncBits += value << 8;
+ break;
+ case 4:
+ mEncBits += value << 2;
+ break;
+ case 5:
+ if (dest >= destEnd) {
+ res = NS_OK_UDEC_MOREOUTPUT;
+ break;
+ }
+ mEncBits += value >> 4;
+ *(dest++) = (char16_t)mEncBits;
+ mEncBits = (value & 0x0f) << 12;
+ break;
+ case 6:
+ mEncBits += value << 6;
+ break;
+ case 7:
+ if (dest >= destEnd) {
+ res = NS_OK_UDEC_MOREOUTPUT;
+ break;
+ }
+ mEncBits += value;
+ *(dest++) = (char16_t)mEncBits;
+ mEncBits = 0;
+ break;
+ }
+
+ if (res != NS_OK) break;
+
+ src++;
+ (++mEncStep) %= 8;
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+uint32_t nsBasicUTF7Decoder::CharToValue(char aChar) {
+ if ((aChar >= 'A') && (aChar <= 'Z'))
+ return (uint8_t)(aChar - 'A');
+ else if ((aChar >= 'a') && (aChar <= 'z'))
+ return (uint8_t)(26 + aChar - 'a');
+ else if ((aChar >= '0') && (aChar <= '9'))
+ return (uint8_t)(26 + 26 + aChar - '0');
+ else if (aChar == '+')
+ return (uint8_t)(26 + 26 + 10);
+ else if (aChar == mLastChar)
+ return (uint8_t)(26 + 26 + 10 + 1);
+ else
+ return 0xffff;
+}
+
+//----------------------------------------------------------------------
+// Subclassing of nsBufferDecoderSupport class [implementation]
+
+NS_IMETHODIMP nsBasicUTF7Decoder::ConvertNoBuff(const char* aSrc,
+ int32_t* aSrcLength,
+ char16_t* aDest,
+ int32_t* aDestLength) {
+ const char* srcEnd = aSrc + *aSrcLength;
+ const char* src = aSrc;
+ char16_t* destEnd = aDest + *aDestLength;
+ char16_t* dest = aDest;
+ int32_t bcr, bcw;
+ nsresult res = NS_OK;
+
+ while (src < srcEnd) {
+ // first, attempt to decode in the current mode
+ bcr = srcEnd - src;
+ bcw = destEnd - dest;
+ if (mEncoding == ENC_DIRECT)
+ res = DecodeDirect(src, &bcr, dest, &bcw);
+ else if ((mFreshBase64) && (*src == '-')) {
+ *dest = mEscChar;
+ bcr = 0;
+ bcw = 1;
+ res = NS_ERROR_UDEC_ILLEGALINPUT;
+ } else {
+ mFreshBase64 = false;
+ res = DecodeBase64(src, &bcr, dest, &bcw);
+ }
+ src += bcr;
+ dest += bcw;
+
+ // if an illegal char was encountered, test if it is an escape seq.
+ if (res == NS_ERROR_UDEC_ILLEGALINPUT) {
+ if (mEncoding == ENC_DIRECT) {
+ if (*src == mEscChar) {
+ mEncoding = ENC_BASE64;
+ mFreshBase64 = true;
+ mEncBits = 0;
+ mEncStep = 0;
+ src++;
+ res = NS_OK;
+ } else
+ break;
+ } else {
+ mEncoding = ENC_DIRECT;
+ res = NS_OK;
+ // absorbe end of escape sequence
+ if (*src == '-') src++;
+ }
+ } else if (res != NS_OK)
+ break;
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+NS_IMETHODIMP nsBasicUTF7Decoder::Reset() {
+ mEncoding = ENC_DIRECT;
+ mEncBits = 0;
+ mEncStep = 0;
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+// Class nsUTF7ToUnicode [implementation]
+
+nsUTF7ToUnicode::nsUTF7ToUnicode() : nsBasicUTF7Decoder('/', '+') {}
diff --git a/comm/mailnews/intl/nsUTF7ToUnicode.h b/comm/mailnews/intl/nsUTF7ToUnicode.h
new file mode 100644
index 0000000000..b7b5be4522
--- /dev/null
+++ b/comm/mailnews/intl/nsUTF7ToUnicode.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsUTF7ToUnicode_h___
+#define nsUTF7ToUnicode_h___
+
+//----------------------------------------------------------------------
+// Class nsBasicUTF7Decoder [declaration]
+
+/**
+ * Basic class for a character set converter from UTF-7 to Unicode.
+ *
+ * @created 03/Jun/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsBasicUTF7Decoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsBasicUTF7Decoder(char aLastChar, char aEscChar);
+ NS_IMETHOD ConvertNoBuff(const char* aSrc, int32_t* aSrcLength,
+ char16_t* aDest, int32_t* aDestLength);
+
+ protected:
+ int32_t mEncoding; // current encoding
+ uint32_t mEncBits;
+ int32_t mEncStep;
+ char mLastChar;
+ char mEscChar;
+ bool mFreshBase64;
+
+ nsresult DecodeDirect(const char* aSrc, int32_t* aSrcLength, char16_t* aDest,
+ int32_t* aDestLength);
+ nsresult DecodeBase64(const char* aSrc, int32_t* aSrcLength, char16_t* aDest,
+ int32_t* aDestLength);
+ uint32_t CharToValue(char aChar);
+
+ //--------------------------------------------------------------------
+ // Subclassing of nsBufferDecoderSupport class [declaration]
+
+ NS_IMETHOD Reset();
+};
+
+//----------------------------------------------------------------------
+// Class nsUTF7ToUnicode [declaration]
+
+/**
+ * A character set converter from Modified UTF7 to Unicode.
+ *
+ * @created 18/May/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsUTF7ToUnicode : public nsBasicUTF7Decoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsUTF7ToUnicode();
+};
+
+#endif /* nsUTF7ToUnicode_h___ */
diff --git a/comm/mailnews/intl/nsUnicodeToMUTF7.cpp b/comm/mailnews/intl/nsUnicodeToMUTF7.cpp
new file mode 100644
index 0000000000..56433a5421
--- /dev/null
+++ b/comm/mailnews/intl/nsUnicodeToMUTF7.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUnicodeToMUTF7.h"
+
+//----------------------------------------------------------------------
+// Class nsUnicodeToMUTF7 [implementation]
+
+nsUnicodeToMUTF7::nsUnicodeToMUTF7() : nsBasicUTF7Encoder(',', '&') {}
diff --git a/comm/mailnews/intl/nsUnicodeToMUTF7.h b/comm/mailnews/intl/nsUnicodeToMUTF7.h
new file mode 100644
index 0000000000..fafb3b6a84
--- /dev/null
+++ b/comm/mailnews/intl/nsUnicodeToMUTF7.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsUnicodeToMUTF7_h___
+#define nsUnicodeToMUTF7_h___
+
+#include "nsUnicodeToUTF7.h"
+
+//----------------------------------------------------------------------
+// Class nsUnicodeToMUTF7 [declaration]
+
+/**
+ * A character set converter from Unicode to Modified UTF-7.
+ *
+ * @created 18/May/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsUnicodeToMUTF7 : public nsBasicUTF7Encoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsUnicodeToMUTF7();
+};
+
+#endif /* nsUnicodeToMUTF7_h___ */
diff --git a/comm/mailnews/intl/nsUnicodeToUTF7.cpp b/comm/mailnews/intl/nsUnicodeToUTF7.cpp
new file mode 100644
index 0000000000..ab9d1cf895
--- /dev/null
+++ b/comm/mailnews/intl/nsUnicodeToUTF7.cpp
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsUnicodeToUTF7.h"
+#include <string.h>
+
+//----------------------------------------------------------------------
+// Global functions and data [declaration]
+
+#define ENC_DIRECT 0
+#define ENC_BASE64 1
+
+//----------------------------------------------------------------------
+// Class nsBasicUTF7Encoder [implementation]
+
+nsBasicUTF7Encoder::nsBasicUTF7Encoder(char aLastChar, char aEscChar) {
+ mLastChar = aLastChar;
+ mEscChar = aEscChar;
+ Reset();
+}
+
+nsresult nsBasicUTF7Encoder::ShiftEncoding(int32_t aEncoding, char* aDest,
+ int32_t* aDestLength) {
+ if (aEncoding == mEncoding) {
+ *aDestLength = 0;
+ return NS_OK;
+ }
+
+ nsresult res = NS_OK;
+ char* dest = aDest;
+ char* destEnd = aDest + *aDestLength;
+
+ if (mEncStep != 0) {
+ if (dest >= destEnd) return NS_OK_UENC_MOREOUTPUT;
+ *(dest++) = ValueToChar(mEncBits);
+ mEncStep = 0;
+ mEncBits = 0;
+ }
+
+ if (dest >= destEnd) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ } else {
+ switch (aEncoding) {
+ case 0:
+ *(dest++) = '-';
+ mEncStep = 0;
+ mEncBits = 0;
+ break;
+ case 1:
+ *(dest++) = mEscChar;
+ break;
+ }
+ mEncoding = aEncoding;
+ }
+
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+nsresult nsBasicUTF7Encoder::EncodeDirect(const char16_t* aSrc,
+ int32_t* aSrcLength, char* aDest,
+ int32_t* aDestLength) {
+ nsresult res = NS_OK;
+ const char16_t* src = aSrc;
+ const char16_t* srcEnd = aSrc + *aSrcLength;
+ char* dest = aDest;
+ char* destEnd = aDest + *aDestLength;
+ char16_t ch;
+
+ while (src < srcEnd) {
+ ch = *src;
+
+ // stop when we reach Unicode chars
+ if (!DirectEncodable(ch)) break;
+
+ if (ch == mEscChar) {
+ // special case for the escape char
+ if (destEnd - dest < 1) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ break;
+ } else {
+ *dest++ = (char)ch;
+ *dest++ = (char)'-';
+ src++;
+ }
+ } else {
+ // classic direct encoding
+ if (dest >= destEnd) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ break;
+ } else {
+ *dest++ = (char)ch;
+ src++;
+ }
+ }
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+nsresult nsBasicUTF7Encoder::EncodeBase64(const char16_t* aSrc,
+ int32_t* aSrcLength, char* aDest,
+ int32_t* aDestLength) {
+ nsresult res = NS_OK;
+ const char16_t* src = aSrc;
+ const char16_t* srcEnd = aSrc + *aSrcLength;
+ char* dest = aDest;
+ char* destEnd = aDest + *aDestLength;
+ char16_t ch;
+ uint32_t value;
+
+ while (src < srcEnd) {
+ ch = *src;
+
+ // stop when we reach printable US-ASCII chars
+ if (DirectEncodable(ch)) break;
+
+ switch (mEncStep) {
+ case 0:
+ if (destEnd - dest < 2) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ break;
+ }
+ value = ch >> 10;
+ *(dest++) = ValueToChar(value);
+ value = (ch >> 4) & 0x3f;
+ *(dest++) = ValueToChar(value);
+ mEncBits = (ch & 0x0f) << 2;
+ break;
+ case 1:
+ if (destEnd - dest < 3) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ break;
+ }
+ value = mEncBits + (ch >> 14);
+ *(dest++) = ValueToChar(value);
+ value = (ch >> 8) & 0x3f;
+ *(dest++) = ValueToChar(value);
+ value = (ch >> 2) & 0x3f;
+ *(dest++) = ValueToChar(value);
+ mEncBits = (ch & 0x03) << 4;
+ break;
+ case 2:
+ if (destEnd - dest < 3) {
+ res = NS_OK_UENC_MOREOUTPUT;
+ break;
+ }
+ value = mEncBits + (ch >> 12);
+ *(dest++) = ValueToChar(value);
+ value = (ch >> 6) & 0x3f;
+ *(dest++) = ValueToChar(value);
+ value = ch & 0x3f;
+ *(dest++) = ValueToChar(value);
+ mEncBits = 0;
+ break;
+ }
+
+ if (res != NS_OK) break;
+
+ src++;
+ (++mEncStep) %= 3;
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+char nsBasicUTF7Encoder::ValueToChar(uint32_t aValue) {
+ if (aValue < 26)
+ return (char)('A' + aValue);
+ else if (aValue < 26 + 26)
+ return (char)('a' + aValue - 26);
+ else if (aValue < 26 + 26 + 10)
+ return (char)('0' + aValue - 26 - 26);
+ else if (aValue == 26 + 26 + 10)
+ return '+';
+ else if (aValue == 26 + 26 + 10 + 1)
+ return mLastChar;
+ else
+ return -1;
+}
+
+bool nsBasicUTF7Encoder::DirectEncodable(char16_t aChar) {
+ // spec says: printable US-ASCII chars
+ if ((aChar >= 0x20) && (aChar <= 0x7e))
+ return true;
+ else
+ return false;
+}
+
+//----------------------------------------------------------------------
+// Subclassing of nsEncoderSupport class [implementation]
+
+NS_IMETHODIMP nsBasicUTF7Encoder::ConvertNoBuffNoErr(const char16_t* aSrc,
+ int32_t* aSrcLength,
+ char* aDest,
+ int32_t* aDestLength) {
+ nsresult res = NS_OK;
+ const char16_t* src = aSrc;
+ const char16_t* srcEnd = aSrc + *aSrcLength;
+ char* dest = aDest;
+ char* destEnd = aDest + *aDestLength;
+ int32_t bcr, bcw;
+ char16_t ch;
+ int32_t enc;
+
+ while (src < srcEnd) {
+ // find the encoding for the next char
+ ch = *src;
+ if (DirectEncodable(ch))
+ enc = ENC_DIRECT;
+ else
+ enc = ENC_BASE64;
+
+ // if necessary, shift into the required encoding
+ bcw = destEnd - dest;
+ res = ShiftEncoding(enc, dest, &bcw);
+ dest += bcw;
+ if (res != NS_OK) break;
+
+ // now encode (as much as you can)
+ bcr = srcEnd - src;
+ bcw = destEnd - dest;
+ if (enc == ENC_DIRECT)
+ res = EncodeDirect(src, &bcr, dest, &bcw);
+ else
+ res = EncodeBase64(src, &bcr, dest, &bcw);
+ src += bcr;
+ dest += bcw;
+
+ if (res != NS_OK) break;
+ }
+
+ *aSrcLength = src - aSrc;
+ *aDestLength = dest - aDest;
+ return res;
+}
+
+NS_IMETHODIMP nsBasicUTF7Encoder::FinishNoBuff(char* aDest,
+ int32_t* aDestLength) {
+ return ShiftEncoding(ENC_DIRECT, aDest, aDestLength);
+}
+
+NS_IMETHODIMP nsBasicUTF7Encoder::Reset() {
+ mEncoding = ENC_DIRECT;
+ mEncBits = 0;
+ mEncStep = 0;
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+// Class nsUnicodeToUTF7 [implementation]
+
+nsUnicodeToUTF7::nsUnicodeToUTF7() : nsBasicUTF7Encoder('/', '+') {}
+
+bool nsUnicodeToUTF7::DirectEncodable(char16_t aChar) {
+ if ((aChar >= 'A') && (aChar <= 'Z'))
+ return true;
+ else if ((aChar >= 'a') && (aChar <= 'z'))
+ return true;
+ else if ((aChar >= '0') && (aChar <= '9'))
+ return true;
+ else if ((aChar >= 39) && (aChar <= 41))
+ return true;
+ else if ((aChar >= 44) && (aChar <= 47))
+ return true;
+ else if (aChar == 58)
+ return true;
+ else if (aChar == 63)
+ return true;
+ else if (aChar == ' ')
+ return true;
+ else if (aChar == 9)
+ return true;
+ else if (aChar == 13)
+ return true;
+ else if (aChar == 10)
+ return true;
+ else if (aChar == 60)
+ return true; // '<'
+ else if (aChar == 33)
+ return true; // '!'
+ else if (aChar == 34)
+ return true; // '"'
+ else if (aChar == 62)
+ return true; // '>'
+ else if (aChar == 61)
+ return true; // '='
+ else if (aChar == 59)
+ return true; // ';'
+ else if (aChar == 91)
+ return true; // '['
+ else if (aChar == 93)
+ return true; // ']'
+ else
+ return false;
+}
diff --git a/comm/mailnews/intl/nsUnicodeToUTF7.h b/comm/mailnews/intl/nsUnicodeToUTF7.h
new file mode 100644
index 0000000000..423bfa8198
--- /dev/null
+++ b/comm/mailnews/intl/nsUnicodeToUTF7.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsUnicodeToUTF7_h___
+#define nsUnicodeToUTF7_h___
+
+//----------------------------------------------------------------------
+// Class nsBasicUTF7Encoder [declaration]
+
+/**
+ * Basic class for a character set converter from Unicode to UTF-7.
+ *
+ * @created 03/Jun/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsBasicUTF7Encoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsBasicUTF7Encoder(char aLastChar, char aEscChar);
+ NS_IMETHOD ConvertNoBuffNoErr(const char16_t* aSrc, int32_t* aSrcLength,
+ char* aDest, int32_t* aDestLength);
+ NS_IMETHOD FinishNoBuff(char* aDest, int32_t* aDestLength);
+
+ protected:
+ int32_t mEncoding; // current encoding
+ uint32_t mEncBits;
+ int32_t mEncStep;
+ char mLastChar;
+ char mEscChar;
+
+ nsresult ShiftEncoding(int32_t aEncoding, char* aDest, int32_t* aDestLength);
+ nsresult EncodeDirect(const char16_t* aSrc, int32_t* aSrcLength, char* aDest,
+ int32_t* aDestLength);
+ nsresult EncodeBase64(const char16_t* aSrc, int32_t* aSrcLength, char* aDest,
+ int32_t* aDestLength);
+ char ValueToChar(uint32_t aValue);
+ virtual bool DirectEncodable(char16_t aChar);
+
+ //--------------------------------------------------------------------
+ // Subclassing of nsEncoderSupport class [declaration]
+
+ NS_IMETHOD Reset();
+};
+
+//----------------------------------------------------------------------
+// Class nsUnicodeToUTF7 [declaration]
+
+/**
+ * A character set converter from Unicode to UTF-7.
+ *
+ * @created 03/Jun/1999
+ * @author Catalin Rotaru [CATA]
+ */
+class nsUnicodeToUTF7 : public nsBasicUTF7Encoder {
+ public:
+ /**
+ * Class constructor.
+ */
+ nsUnicodeToUTF7();
+
+ protected:
+ virtual bool DirectEncodable(char16_t aChar);
+};
+
+#endif /* nsUnicodeToUTF7_h___ */
diff --git a/comm/mailnews/intl/test/moz.build b/comm/mailnews/intl/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/intl/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/intl/test/unit/head_CharsetConversionTests.js b/comm/mailnews/intl/test/unit/head_CharsetConversionTests.js
new file mode 100644
index 0000000000..f1ae6c7155
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/head_CharsetConversionTests.js
@@ -0,0 +1,46 @@
+var CC = Components.Constructor;
+
+function CreateScriptableConverter() {
+ var ScriptableUnicodeConverter = CC(
+ "@mozilla.org/intl/scriptableunicodeconverter",
+ "nsIScriptableUnicodeConverter"
+ );
+
+ return new ScriptableUnicodeConverter();
+}
+
+function checkDecode(converter, charset, inText, expectedText) {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+
+ try {
+ converter.charset = manager.getCharsetAlias(charset);
+ } catch (e) {
+ converter.charset = "iso-8859-1";
+ }
+
+ dump("testing decoding from " + charset + " to Unicode.\n");
+ try {
+ var outText = converter.ConvertToUnicode(inText) + converter.Finish();
+ } catch (e) {
+ outText = "\ufffd";
+ }
+ Assert.equal(outText, expectedText);
+}
+
+function checkEncode(converter, charset, inText, expectedText) {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+
+ try {
+ converter.charset = manager.getCharsetAlias(charset);
+ } catch (e) {
+ converter.charset = "iso-8859-1";
+ }
+
+ dump("testing encoding from Unicode to " + charset + "\n");
+ var outText = converter.ConvertFromUnicode(inText) + converter.Finish();
+ Assert.equal(outText, expectedText);
+}
diff --git a/comm/mailnews/intl/test/unit/test_decode_utf-7.js b/comm/mailnews/intl/test/unit/test_decode_utf-7.js
new file mode 100644
index 0000000000..e81dd137e6
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/test_decode_utf-7.js
@@ -0,0 +1,23 @@
+// Tests conversion from UTF-7 to Unicode. The conversion should fail!
+
+var inString =
+ "+LGI--+ITIipSIp- +AocCeQ-oddns +Ad0CjQ- s+ATECZQKH- p+AlAB3QJ5- u+AlACVA- no+Ao4- +Al8-I";
+
+var expectedString =
+ "+LGI--+ITIipSIp- +AocCeQ-oddns +Ad0CjQ- s+ATECZQKH- p+AlAB3QJ5- u+AlACVA- no+Ao4- +Al8-I";
+
+var aliases = [
+ "UTF-7",
+ "utf-7",
+ "x-unicode-2-0-utf-7",
+ "unicode-2-0-utf-7",
+ "unicode-1-1-utf-7",
+ "csunicode11utf7",
+];
+
+function run_test() {
+ let converter = CreateScriptableConverter();
+ for (let i = 0; i < aliases.length; ++i) {
+ checkDecode(converter, aliases[i], inString, expectedString);
+ }
+}
diff --git a/comm/mailnews/intl/test/unit/test_decode_utf-7_internal.js b/comm/mailnews/intl/test/unit/test_decode_utf-7_internal.js
new file mode 100644
index 0000000000..e31f0f8840
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/test_decode_utf-7_internal.js
@@ -0,0 +1,30 @@
+// Tests conversion from UTF-7 to Unicode.
+
+var inString =
+ "+LGI--+ITIipSIp- +AocCeQ-oddns +Ad0CjQ- s+ATECZQKH- p+AlAB3QJ5- u+AlACVA- no+Ao4- +Al8-I";
+
+var expectedString =
+ "\u2C62-\u2132\u22A5\u2229 \u0287\u0279oddns \u01DD\u028D s\u0131\u0265\u0287 p\u0250\u01DD\u0279 u\u0250\u0254 no\u028E \u025FI";
+
+var aliases = [
+ "UTF-7",
+ "utf-7",
+ "x-unicode-2-0-utf-7",
+ "unicode-2-0-utf-7",
+ "unicode-1-1-utf-7",
+ "csunicode11utf7",
+];
+function run_test() {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ let converter = CreateScriptableConverter();
+ converter.isInternal = true;
+ for (let i = 0; i < aliases.length; ++i) {
+ if (manager.getCharsetAlias(aliases[i]).toLowerCase() == "utf-7") {
+ Assert.equal(manager.utf7ToUnicode(inString), expectedString);
+ } else {
+ checkDecode(converter, aliases[i], inString, expectedString);
+ }
+ }
+}
diff --git a/comm/mailnews/intl/test/unit/test_encode_utf-7.js b/comm/mailnews/intl/test/unit/test_encode_utf-7.js
new file mode 100644
index 0000000000..1acc8957bd
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/test_encode_utf-7.js
@@ -0,0 +1,22 @@
+// Tests conversion from Unicode to UTF-7. The conversion should fail!
+
+var inString =
+ "\u2C62-\u2132\u22A5\u2229 \u0287\u0279oddns \u01DD\u028D s\u0131\u0265\u0287 p\u0250\u01DD\u0279 u\u0250\u0254 no\u028E \u025FI";
+
+var expectedString = "?-??? ??oddns ?? s??? p??? u?? no? ?I";
+
+var aliases = [
+ "UTF-7",
+ "utf-7",
+ "x-unicode-2-0-utf-7",
+ "unicode-2-0-utf-7",
+ "unicode-1-1-utf-7",
+ "csunicode11utf7",
+];
+
+function run_test() {
+ let converter = CreateScriptableConverter();
+ for (let i = 0; i < aliases.length; ++i) {
+ checkEncode(converter, aliases[i], inString, expectedString);
+ }
+}
diff --git a/comm/mailnews/intl/test/unit/test_encode_utf-7_internal.js b/comm/mailnews/intl/test/unit/test_encode_utf-7_internal.js
new file mode 100644
index 0000000000..31af29c30b
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/test_encode_utf-7_internal.js
@@ -0,0 +1,24 @@
+// Tests conversion from Unicode to UTF-7.
+
+var inString =
+ "\u2C62-\u2132\u22A5\u2229 \u0287\u0279oddns \u01DD\u028D s\u0131\u0265\u0287 p\u0250\u01DD\u0279 u\u0250\u0254 no\u028E \u025FI";
+
+var expectedString =
+ "+LGI--+ITIipSIp- +AocCeQ-oddns +Ad0CjQ- s+ATECZQKH- p+AlAB3QJ5- u+AlACVA- no+Ao4- +Al8-I";
+
+var aliases = [
+ "UTF-7",
+ "utf-7",
+ "x-unicode-2-0-utf-7",
+ "unicode-2-0-utf-7",
+ "unicode-1-1-utf-7",
+ "csunicode11utf7",
+];
+
+function run_test() {
+ let converter = CreateScriptableConverter();
+ converter.isInternal = true;
+ for (let i = 0; i < aliases.length; ++i) {
+ checkEncode(converter, aliases[i], inString, expectedString);
+ }
+}
diff --git a/comm/mailnews/intl/test/unit/xpcshell.ini b/comm/mailnews/intl/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..cbc671669c
--- /dev/null
+++ b/comm/mailnews/intl/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head = head_CharsetConversionTests.js
+tail =
+
+[test_decode_utf-7.js]
+[test_decode_utf-7_internal.js]
+[test_encode_utf-7.js]
+[test_encode_utf-7_internal.js]
+# Disabled per bug 1363281: No scriptable converter for UTF-7 exists any more.
+skip-if = true
diff --git a/comm/mailnews/jar.mn b/comm/mailnews/jar.mn
new file mode 100644
index 0000000000..22ab9be229
--- /dev/null
+++ b/comm/mailnews/jar.mn
@@ -0,0 +1,120 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/addressbook/pref-directory-add.js (addrbook/prefs/content/pref-directory-add.js)
+ content/messenger/addressbook/pref-directory-add.xhtml (addrbook/prefs/content/pref-directory-add.xhtml)
+ content/messenger/addressbook/pref-editdirectories.js (addrbook/prefs/content/pref-editdirectories.js)
+ content/messenger/addressbook/pref-editdirectories.xhtml (addrbook/prefs/content/pref-editdirectories.xhtml)
+ content/messenger/addressbook/abAddressBookNameDialog.js (addrbook/content/abAddressBookNameDialog.js)
+ content/messenger/addressbook/abAddressBookNameDialog.xhtml (addrbook/content/abAddressBookNameDialog.xhtml)
+ content/messenger/addressbook/abCardDAVDialog.js (addrbook/content/abCardDAVDialog.js)
+ content/messenger/addressbook/abCardDAVDialog.xhtml (addrbook/content/abCardDAVDialog.xhtml)
+ content/messenger/addressbook/abCardDAVProperties.js (addrbook/content/abCardDAVProperties.js)
+ content/messenger/addressbook/abCardDAVProperties.xhtml (addrbook/content/abCardDAVProperties.xhtml)
+ content/messenger/addressbook/abResultsPane.js (addrbook/content/abResultsPane.js)
+ content/messenger/addressbook/abDragDrop.js (addrbook/content/abDragDrop.js)
+ content/messenger/addressbook/abMailListDialog.js (addrbook/content/abMailListDialog.js)
+ content/messenger/addressbook/abView.js (addrbook/content/abView.js)
+#ifdef MOZ_SUITE
+ content/messenger/addressbook/map-list.js (addrbook/content/map-list.js)
+#endif
+* content/messenger/AccountManager.xhtml (base/prefs/content/AccountManager.xhtml)
+ content/messenger/AccountManager.js (base/prefs/content/AccountManager.js)
+ content/messenger/am-main.xhtml (base/prefs/content/am-main.xhtml)
+ content/messenger/am-main.js (base/prefs/content/am-main.js)
+ content/messenger/am-server.xhtml (base/prefs/content/am-server.xhtml)
+ content/messenger/am-serverwithnoidentities.xhtml (base/prefs/content/am-serverwithnoidentities.xhtml)
+ content/messenger/am-serverwithnoidentities.js (base/prefs/content/am-serverwithnoidentities.js)
+ content/messenger/am-server.js (base/prefs/content/am-server.js)
+* content/messenger/am-copies.xhtml (base/prefs/content/am-copies.xhtml)
+ content/messenger/am-copies.js (base/prefs/content/am-copies.js)
+ content/messenger/am-junk.xhtml (base/prefs/content/am-junk.xhtml)
+ content/messenger/am-junk.js (base/prefs/content/am-junk.js)
+ content/messenger/am-offline.xhtml (base/prefs/content/am-offline.xhtml)
+ content/messenger/am-offline.js (base/prefs/content/am-offline.js)
+* content/messenger/am-addressing.xhtml (base/prefs/content/am-addressing.xhtml)
+ content/messenger/am-addressing.js (base/prefs/content/am-addressing.js)
+ content/messenger/am-server-advanced.xhtml (base/prefs/content/am-server-advanced.xhtml)
+ content/messenger/am-server-advanced.js (base/prefs/content/am-server-advanced.js)
+ content/messenger/am-smtp.xhtml (base/prefs/content/am-smtp.xhtml)
+ content/messenger/am-smtp.js (base/prefs/content/am-smtp.js)
+ content/messenger/am-prefs.js (base/prefs/content/am-prefs.js)
+ content/messenger/am-identities-list.js (base/prefs/content/am-identities-list.js)
+ content/messenger/am-identities-list.xhtml (base/prefs/content/am-identities-list.xhtml)
+ content/messenger/am-identity-edit.js (base/prefs/content/am-identity-edit.js)
+* content/messenger/am-identity-edit.xhtml (base/prefs/content/am-identity-edit.xhtml)
+ content/messenger/am-archiveoptions.xhtml (base/prefs/content/am-archiveoptions.xhtml)
+ content/messenger/am-archiveoptions.js (base/prefs/content/am-archiveoptions.js)
+* content/messenger/AccountWizard.xhtml (base/prefs/content/AccountWizard.xhtml)
+ content/messenger/AccountWizard.js (base/prefs/content/AccountWizard.js)
+ content/messenger/aw-identity.js (base/prefs/content/aw-identity.js)
+ content/messenger/aw-incoming.js (base/prefs/content/aw-incoming.js)
+ content/messenger/aw-accname.js (base/prefs/content/aw-accname.js)
+ content/messenger/aw-done.js (base/prefs/content/aw-done.js)
+ content/messenger/accountUtils.js (base/prefs/content/accountUtils.js)
+ content/messenger/amUtils.js (base/prefs/content/amUtils.js)
+ content/messenger/SmtpServerEdit.xhtml (base/prefs/content/SmtpServerEdit.xhtml)
+ content/messenger/SmtpServerEdit.js (base/prefs/content/SmtpServerEdit.js)
+ content/messenger/converterDialog.xhtml (base/prefs/content/converterDialog.xhtml)
+ content/messenger/converterDialog.js (base/prefs/content/converterDialog.js)
+ content/messenger/removeAccount.xhtml (base/prefs/content/removeAccount.xhtml)
+ content/messenger/removeAccount.js (base/prefs/content/removeAccount.js)
+ content/messenger/msgSelectOfflineFolders.xhtml (base/content/msgSelectOfflineFolders.xhtml)
+ content/messenger/msgSelectOfflineFolders.js (base/content/msgSelectOfflineFolders.js)
+ content/messenger/msgSynchronize.xhtml (base/content/msgSynchronize.xhtml)
+ content/messenger/msgSynchronize.js (base/content/msgSynchronize.js)
+ content/messenger/folderProps.xhtml (base/content/folderProps.xhtml)
+ content/messenger/menulist-charsetpicker.js (base/content/menulist-charsetpicker.js)
+ content/messenger/folderProps.js (base/content/folderProps.js)
+ content/messenger/folder-menupopup.js (base/content/folder-menupopup.js)
+ content/messenger/newFolderDialog.xhtml (base/content/newFolderDialog.xhtml)
+ content/messenger/newFolderDialog.js (base/content/newFolderDialog.js)
+ content/messenger/msgAccountCentral.xhtml (base/content/msgAccountCentral.xhtml)
+ content/messenger/msgAccountCentral.js (base/content/msgAccountCentral.js)
+ content/messenger/renameFolderDialog.xhtml (base/content/renameFolderDialog.xhtml)
+ content/messenger/renameFolderDialog.js (base/content/renameFolderDialog.js)
+ content/messenger/retention.js (base/content/retention.js)
+ content/messenger/subscribe.js (base/content/subscribe.js)
+ content/messenger/subscribe.xhtml (base/content/subscribe.xhtml)
+ content/messenger/virtualFolderListEdit.xhtml (base/content/virtualFolderListEdit.xhtml)
+ content/messenger/virtualFolderListEdit.js (base/content/virtualFolderListEdit.js)
+* content/messenger/virtualFolderProperties.xhtml (base/content/virtualFolderProperties.xhtml)
+ content/messenger/virtualFolderProperties.js (base/content/virtualFolderProperties.js)
+ content/messenger/junkCommands.js (base/content/junkCommands.js)
+ content/messenger/junkLog.xhtml (base/content/junkLog.xhtml)
+ content/messenger/junkLog.js (base/content/junkLog.js)
+ content/messenger/jsTreeView.js (base/content/jsTreeView.js)
+ content/messenger/searchTerm.js (search/content/searchTerm.js)
+* content/messenger/CustomHeaders.xhtml (search/content/CustomHeaders.xhtml)
+ content/messenger/CustomHeaders.js (search/content/CustomHeaders.js)
+* content/messenger/FilterEditor.xhtml (search/content/FilterEditor.xhtml)
+ content/messenger/FilterEditor.js (search/content/FilterEditor.js)
+ content/messenger/searchWidgets.js (search/content/searchWidgets.js)
+ content/messenger/viewLog.xhtml (search/content/viewLog.xhtml)
+ content/messenger/viewLog.js (search/content/viewLog.js)
+ content/messenger/messengercompose/sendProgress.xhtml (compose/content/sendProgress.xhtml)
+ content/messenger/messengercompose/sendProgress.js (compose/content/sendProgress.js)
+ content/messenger/exportDialog.js (export/content/exportDialog.js)
+ content/messenger/exportDialog.xhtml (export/content/exportDialog.xhtml)
+ content/messenger/aboutImport.js (import/content/aboutImport.js)
+* content/messenger/aboutImport.xhtml (import/content/aboutImport.xhtml)
+ content/messenger/csv-field-map.js (import/content/csv-field-map.js)
+ content/messenger/importDialog.js (import/content/importDialog.js)
+ content/messenger/importDialog.xhtml (import/content/importDialog.xhtml)
+ content/messenger/fieldMapImport.xhtml (import/content/fieldMapImport.xhtml)
+ content/messenger/fieldMapImport.js (import/content/fieldMapImport.js)
+ content/messenger/downloadheaders.js (news/content/downloadheaders.js)
+ content/messenger/downloadheaders.xhtml (news/content/downloadheaders.xhtml)
+ content/messenger/markByDate.js (base/content/markByDate.js)
+ content/messenger/markByDate.xhtml (base/content/markByDate.xhtml)
+ content/messenger/dateFormat.js (base/content/dateFormat.js)
+ content/messenger/shutdownWindow.xhtml (base/content/shutdownWindow.xhtml)
+ content/messenger/shutdownWindow.js (base/content/shutdownWindow.js)
+ content/messenger/newsError.xhtml (base/content/newsError.xhtml)
+ content/messenger/newsError.js (base/content/newsError.js)
+#ifndef XP_MACOSX
+ content/messenger/newmailalert.js (base/content/newmailalert.js)
+ content/messenger/newmailalert.xhtml (base/content/newmailalert.xhtml)
+#endif
diff --git a/comm/mailnews/jsaccount/modules/JSAccountUtils.jsm b/comm/mailnews/jsaccount/modules/JSAccountUtils.jsm
new file mode 100644
index 0000000000..adef4710e0
--- /dev/null
+++ b/comm/mailnews/jsaccount/modules/JSAccountUtils.jsm
@@ -0,0 +1,264 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * This file implements helper methods to make the transition of base mailnews
+ * objects from JS to C++ easier, and also to allow creating specialized
+ * versions of those accounts using only JS XPCOM implementations.
+ *
+ * In C++ land, the XPCOM component is a generic C++ class that does nothing
+ * but delegate any calls to interfaces known in C++ to either the generic
+ * C++ implementation (such as nsMsgIncomingServer.cpp) or a JavaScript
+ * implementation of those methods. Those delegations could be used for either
+ * method-by-method replacement of the generic C++ methods with JavaScript
+ * versions, or for specialization of the generic class using JavaScript to
+ * implement a particular class type. We use a C++ class as the main XPCOM
+ * version for two related reasons: First, we do not want to go through a
+ * C++->js->C++ XPCOM transition just to execute a C++ method. Second, C++
+ * inheritance is different from JS inheritance, and sometimes the C++ code
+ * will ignore the XPCOM parts of the JS, and just execute using C++
+ * inheritance.
+ *
+ * In JavaScript land, the implementation currently uses the XPCOM object for
+ * JavaScript calls, with the last object in the prototype chain defaulting
+ * to calling using the CPP object, specified in an instance-specific
+ * this.cppBase object.
+ *
+ * Examples of use can be found in the test files for jsaccount stuff.
+ */
+
+const EXPORTED_SYMBOLS = ["JSAccountUtils"];
+var JSAccountUtils = {};
+
+// Logging usage: set mailnews.jsaccount.loglevel to the word "Debug" to
+// increase logging level.
+var log = console.createInstance({
+ prefix: "mail.jsaccount",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.jsaccount.loglevel",
+});
+
+/**
+ *
+ * Generic factory to create XPCOM components under JsAccount.
+ *
+ * @param aProperties This a a const JS object that describes the specific
+ * details of a particular JsAccount XPCOM object:
+ * {
+ * baseContractID: string contractID used to create the base generic C++
+ * object. This object must implement the interfaces in
+ * baseInterfaces, plus msgIOverride.
+ *
+ * baseInterfaces: JS array of interfaces implemented by the base, generic
+ * C++ object.
+ *
+ * extraInterfaces: JS array of additional interfaces implemented by the
+ * component (accessed using getInterface())
+ *
+ * contractID: string contract ID for the JS object that will be
+ * created by the factory.
+ *
+ * classID: Components.ID(CID) for the JS object that will be
+ * created by the factory, where CID is a string uuid.
+ * }
+ *
+ * @param aJsDelegateConstructor: a JS constructor class, called using new,
+ * that will create the JS object to which
+ * XPCOM methods calls will be delegated.
+ */
+
+JSAccountUtils.jaFactory = function (aProperties, aJsDelegateConstructor) {
+ let factory = {};
+ factory.QueryInterface = ChromeUtils.generateQI([Ci.nsIFactory]);
+
+ factory.createInstance = function (iid) {
+ // C++ delegator class.
+ let delegator = Cc[aProperties.baseContractID].createInstance(
+ Ci.msgIOverride
+ );
+
+ // Make sure the delegator JS wrapper knows its interfaces.
+ aProperties.baseInterfaces.forEach(iface => delegator instanceof iface);
+
+ // JavaScript overrides of base class functions.
+ let jsDelegate = new aJsDelegateConstructor(
+ delegator,
+ aProperties.baseInterfaces
+ );
+ delegator.jsDelegate = jsDelegate;
+
+ // Get the delegate list for this current class. Use OwnProperty in case it
+ // inherits from another JsAccount class.
+
+ let delegateList = null;
+ if (Object.getPrototypeOf(jsDelegate).hasOwnProperty("delegateList")) {
+ delegateList = Object.getPrototypeOf(jsDelegate).delegateList;
+ }
+ if (delegateList instanceof Ci.msgIDelegateList) {
+ delegator.methodsToDelegate = delegateList;
+ } else {
+ // Lazily create and populate the list of methods to delegate.
+ log.info(
+ "creating delegate list for contractID " + aProperties.contractID
+ );
+ let delegateList = delegator.methodsToDelegate;
+ Object.keys(delegator).forEach(name => {
+ log.debug("delegator has key " + name);
+ });
+
+ // jsMethods contains the methods that may be targets of the C++ delegation to JS.
+ let jsMethods = Object.getPrototypeOf(jsDelegate);
+ for (let name in jsMethods) {
+ log.debug("processing jsDelegate method: " + name);
+ if (name[0] == "_") {
+ // don't bother with methods explicitly marked as internal.
+ log.debug("skipping " + name);
+ continue;
+ }
+ // Other methods to skip.
+ if (
+ [
+ "QueryInterface", // nsISupports
+ "methodsToDelegate",
+ "jsDelegate",
+ "cppBase", // msgIOverride
+ "delegateList",
+ "wrappedJSObject", // non-XPCOM methods to skip
+ ].includes(name)
+ ) {
+ log.debug("skipping " + name);
+ continue;
+ }
+
+ let jsDescriptor = getPropertyDescriptor(jsMethods, name);
+ if (!jsDescriptor) {
+ log.debug("no jsDescriptor for " + name);
+ continue;
+ }
+ let cppDescriptor = Object.getOwnPropertyDescriptor(delegator, name);
+ if (!cppDescriptor) {
+ log.debug("no cppDescriptor found for " + name);
+ // It is OK for jsMethods to have methods that are not used in override of C++.
+ continue;
+ }
+
+ let upperCaseName = name[0].toUpperCase() + name.substr(1);
+ if ("value" in jsDescriptor) {
+ log.info("delegating " + upperCaseName);
+ delegateList.add(upperCaseName);
+ } else {
+ if (jsDescriptor.set) {
+ log.info("delegating Set" + upperCaseName);
+ delegateList.add("Set" + upperCaseName);
+ }
+ if (jsDescriptor.get) {
+ log.info("delegating Get" + upperCaseName);
+ delegateList.add("Get" + upperCaseName);
+ }
+ }
+ }
+
+ // Save the delegate list for reuse, statically for all instances.
+ Object.getPrototypeOf(jsDelegate).delegateList = delegateList;
+ }
+
+ for (let iface of aProperties.baseInterfaces) {
+ if (iid.equals(iface)) {
+ log.debug("Successfully returning delegator " + delegator);
+ return delegator;
+ }
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ };
+
+ return factory;
+};
+
+/**
+ * Create a JS object that contains calls to each of the methods in a CPP
+ * base class, that will reference the cpp object defined on a particular
+ * instance of the object. This is intended to be the last item in the
+ * prototype chain for a JsAccount implementation.
+ *
+ * @param aProperties see definition in jsFactory above
+ *
+ * @returns a JS object suitable as the prototype of a JsAccount implementation.
+ */
+JSAccountUtils.makeCppDelegator = function (aProperties) {
+ log.info("Making cppDelegator for contractID " + aProperties.contractID);
+ let cppDelegator = {};
+ let cppDummy = Cc[aProperties.baseContractID].createInstance(Ci.nsISupports);
+ // Add methods from all interfaces.
+ for (let iface of aProperties.baseInterfaces) {
+ cppDummy instanceof Ci[iface];
+ }
+
+ for (let method in cppDummy) {
+ // skip nsISupports and msgIOverride methods
+ if (
+ [
+ "QueryInterface",
+ "methodsToDelegate",
+ "jsDelegate",
+ "cppBase",
+ "getInterface",
+ ].includes(method)
+ ) {
+ log.debug("Skipping " + method);
+ continue;
+ }
+ log.debug("Processing " + method);
+ let descriptor = Object.getOwnPropertyDescriptor(cppDummy, method);
+ let property = { enumerable: true };
+ // We must use Immediately Invoked Function Expressions to pass method, otherwise it is
+ // a closure containing just the last value it was set to.
+ if ("value" in descriptor) {
+ log.debug("Adding value for " + method);
+ property.value = (function (aMethod) {
+ return function (...args) {
+ return Reflect.apply(this.cppBase[aMethod], undefined, args);
+ };
+ })(method);
+ }
+ if (descriptor.set) {
+ log.debug("Adding setter for " + method);
+ property.set = (function (aMethod) {
+ return function (aVal) {
+ this.cppBase[aMethod] = aVal;
+ };
+ })(method);
+ }
+ if (descriptor.get) {
+ log.debug("Adding getter for " + method);
+ property.get = (function (aMethod) {
+ return function () {
+ return this.cppBase[aMethod];
+ };
+ })(method);
+ }
+ Object.defineProperty(cppDelegator, method, property);
+ }
+ return cppDelegator;
+};
+
+// Utility functions.
+
+// Iterate over an object and its prototypes to get a property descriptor.
+function getPropertyDescriptor(obj, name) {
+ let descriptor = null;
+
+ // Eventually we will hit an object that will delegate JS calls to a CPP
+ // object, which are not JS overrides of CPP methods. Locate this item, and
+ // skip, because it will not have _JsPrototypeToDelegate defined.
+ while (obj && "_JsPrototypeToDelegate" in obj) {
+ descriptor = Object.getOwnPropertyDescriptor(obj, name);
+ if (descriptor) {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ return descriptor;
+}
diff --git a/comm/mailnews/jsaccount/modules/JaBaseUrl.jsm b/comm/mailnews/jsaccount/modules/JaBaseUrl.jsm
new file mode 100644
index 0000000000..03f2a851d3
--- /dev/null
+++ b/comm/mailnews/jsaccount/modules/JaBaseUrl.jsm
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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 = ["JaBaseUrlProperties", "JaBaseUrl"];
+
+const { JSAccountUtils } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JSAccountUtils.jsm"
+);
+
+// A partial JavaScript implementation of the base server methods.
+
+const JaBaseUrlProperties = {
+ // The CPP object that delegates to CPP or JS.
+ baseContractID: "@mozilla.org/jacppurldelegator;1",
+
+ // Interfaces implemented by the base CPP version of this object.
+ baseInterfaces: [
+ Ci.nsIURI,
+ Ci.nsIURL,
+ Ci.nsIMsgMailNewsUrl,
+ Ci.nsIMsgMessageUrl,
+ Ci.msgIOverride,
+ Ci.nsISupports,
+ Ci.nsIInterfaceRequestor,
+ ],
+ // Don't pass Ci.nsISupports to generateQI().
+ baseInterfacesQI: [
+ Ci.nsIURI,
+ Ci.nsIURL,
+ Ci.nsIMsgMailNewsUrl,
+ Ci.nsIMsgMessageUrl,
+ Ci.msgIOverride,
+ Ci.nsIInterfaceRequestor,
+ ],
+
+ // We don't typically define this as a creatable component, but if we do use
+ // these. Subclasses for particular account types require these defined for
+ // that type.
+ contractID: "@mozilla.org/jsaccount/jaurl;1",
+ classID: Components.ID("{1E7B42CA-E6D9-408F-A4E4-8D2F82AECBBD}"),
+};
+
+// Typical boilerplate to include in all implementations.
+function JaBaseUrl(aDelegator, aBaseInterfaces) {
+ // Object delegating method calls to the appropriate XPCOM object.
+ // Weak because it owns us.
+ this._delegatorWeak = Cu.getWeakReference(aDelegator);
+
+ // Base implementation of methods with no overrides.
+ this.cppBase = aDelegator.cppBase;
+
+ // cppBase class sees all interfaces
+ aBaseInterfaces.forEach(iface => this.cppBase instanceof iface);
+}
+
+// Typical boilerplate to include in all implementations.
+JaBaseUrl.prototype = {
+ __proto__: JSAccountUtils.makeCppDelegator(JaBaseUrlProperties),
+
+ // Flag this item as CPP needs to delegate to JS.
+ _JsPrototypeToDelegate: true,
+
+ // QI to the interfaces.
+ QueryInterface: ChromeUtils.generateQI(JaBaseUrlProperties.baseInterfacesQI),
+
+ // Used to access an instance as JS, bypassing XPCOM.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Accessor to the weak cpp delegator.
+ get delegator() {
+ return this._delegatorWeak.get();
+ },
+
+ // Dynamically-generated list of delegate methods.
+ delegateList: null,
+
+ // Implementation in JS (if any) of methods in XPCOM interfaces.
+};
diff --git a/comm/mailnews/jsaccount/moz.build b/comm/mailnews/jsaccount/moz.build
new file mode 100644
index 0000000000..57cdd9d119
--- /dev/null
+++ b/comm/mailnews/jsaccount/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += ["public", "src"]
+
+EXTRA_JS_MODULES.jsaccount += [
+ "modules/JaBaseUrl.jsm",
+ "modules/JSAccountUtils.jsm",
+ "test/unit/resources/TestJaMsgProtocolInfoComponent.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "test/components.conf",
+]
+
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/jsaccount/public/moz.build b/comm/mailnews/jsaccount/public/moz.build
new file mode 100644
index 0000000000..41715534ff
--- /dev/null
+++ b/comm/mailnews/jsaccount/public/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "msgIDelegateList.idl",
+ "msgIJaUrl.idl",
+ "msgIOverride.idl",
+]
+
+EXPORTS += []
+
+XPIDL_MODULE = "msgjsaccount"
diff --git a/comm/mailnews/jsaccount/public/msgIDelegateList.idl b/comm/mailnews/jsaccount/public/msgIDelegateList.idl
new file mode 100644
index 0000000000..0a65596206
--- /dev/null
+++ b/comm/mailnews/jsaccount/public/msgIDelegateList.idl
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This interface provides a list of methods that should be delegated to
+ * a JsObject rather than a C++ XPCOM base object in JsAccount classes.
+ */
+
+[scriptable, builtinclass, uuid(627D3A34-F8A3-40eb-91FE-E413D6638D27)]
+interface msgIDelegateList : nsISupports
+{
+ /// Method name to delegate to JavaScript.
+ void add(in ACString aMethod);
+};
diff --git a/comm/mailnews/jsaccount/public/msgIJaUrl.idl b/comm/mailnews/jsaccount/public/msgIJaUrl.idl
new file mode 100644
index 0000000000..41379ab239
--- /dev/null
+++ b/comm/mailnews/jsaccount/public/msgIJaUrl.idl
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Additional interface supported by JsBaseUrl to allow setting variables that
+ * are typically done in C++ by overriding classes, which is not allowed in JS.
+ */
+
+[scriptable, uuid(f9148c17-9b67-43bb-b741-4fdfe1aef935)]
+interface msgIJaUrl : nsISupports
+{
+ /**
+ * Set the URL type, which will be checked by nsIMsgMailNewsUrl::IsUrlType. See
+ * IsUrlType for possible values.
+ */
+ void setUrlType(in unsigned long type);
+
+ /**
+ * Set the spec on a URL.
+ */
+ void setSpec(in AUTF8String spec);
+};
diff --git a/comm/mailnews/jsaccount/public/msgIOverride.idl b/comm/mailnews/jsaccount/public/msgIOverride.idl
new file mode 100644
index 0000000000..cad1bf4157
--- /dev/null
+++ b/comm/mailnews/jsaccount/public/msgIOverride.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "msgIDelegateList.idl"
+
+/**
+ * Mailnews code typically has a C++ base class for objects, which is then
+ * specialized for each account type with a C++ subclass of the base class.
+ *
+ * This interface provides the ability of JavaScript-based account
+ * implementations to use the same C++ base classes as core objects, but
+ * use JavaScript to override methods instead of C++.
+ */
+
+[scriptable, uuid(68075269-8BBD-4a09-AC04-3241BF44F633)]
+interface msgIOverride : nsISupports
+{
+ /**
+ *
+ * A list of methods in the C++ base class that will be delegated to the JS
+ * delegate. This is calculated once, and then a fixed value is set to
+ * all subsequent instances so that it does not need to be recalculated each
+ * time. If the value has not yet been set, this will return a new instance.
+ */
+ attribute msgIDelegateList methodsToDelegate;
+
+ /**
+ * JavaScript-based xpcom object that overrides C++ methods.
+ */
+ attribute nsISupports jsDelegate;
+
+ /**
+ * C++ class used to implement default functionality. This is used when
+ * JavaScript methods want to call the base class default action, bypassing a
+ * possible JS override.
+ */
+ readonly attribute nsISupports cppBase;
+};
diff --git a/comm/mailnews/jsaccount/readme.html b/comm/mailnews/jsaccount/readme.html
new file mode 100644
index 0000000000..cb1b875dbe
--- /dev/null
+++ b/comm/mailnews/jsaccount/readme.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>JsAccount Usage and Architecture</title>
+ </head>
+ <body>
+ <h1>Overview</h1>
+ <p>
+ JsAccount is a technology that allows message account types to be created
+ in Mozilla Mailnews code using JavaScript. Although this is primarily
+ targeted at allowing extensions to create new accounts, it might also be
+ useful as a bridge to convert existing account types from being C++ based
+ to JavaScript based.
+ </p>
+ <h2>Existing C++-based architecture of mailnews accounts</h2>
+ <p>
+ In mailnews code, an account type is a set of classes that allow
+ implementation of a messaging particular protocol. The account type is
+ given a short string identifier ("imap", "news", "pop3") and is then used
+ to create objects of the appropriate type by appending that string to the
+ end of a base XPCOM contractID. So, for example, to create an imap server,
+ you generate a contractID using a base ID,
+ "@mozilla.org/messenger/server;1?type=", then append "imap" to get:
+ </p>
+ <p>@mozilla.org/messenger/server;1?type=imap</p>
+ <p>
+ In the C++ code, there is a base object implementing shared functionality.
+ An account-specific class inherits that base functionality, then extends
+ it to represent the account-specific behavior that is needed. This same
+ basic concept is used to represent a whole series of classes that are
+ necessary to implement a specific mailnews account type.
+ </p>
+ <p>
+ For the server example, there is a base class named
+ nsMsgIncomingServer.cpp that implements that base interface
+ nsIMsgIncomingServer.idl. For imap, there is a specific class
+ nsImapIncomingServer.cpp that inherits from nsMsgIncomingServer.cpp,
+ overrides some of the methods in nsIMsgIncomingServer.idl, and also
+ implements an imap-specific interface nsIImapIncomingServer.idl. All of
+ this works fine using C++ inheritance and polymorphism.
+ </p>
+ <p>
+ Although JsAccount is intended mostly for mailnews accounts, the same
+ basic method of using a base class extended for specific types is also
+ used in other ways in mailnews code, including for addressbook types and
+ views. The technology may also be applied to those other object types as
+ well.
+ </p>
+ <h2>Role of JsAccount</h2>
+ <p>
+ The JavaScript class system works very differently than the C++ system,
+ and you cannot use normal language constructs to override a C++ class with
+ a JavaScript class. What JsAccount allows you to do is to create XPCOM
+ objects in JavaScript, and use those objects to override or extend the
+ methods from the C++ base class in a way that will function correctly
+ whether those objects are executed from within C++ code or JavaScript
+ code. This allows you to create a new account using JavaScript code, while
+ using the same base class functionality that is used by the core C++
+ account types. Thus a new account type may be created in JavaScript-based
+ extension. The technology may also be used to create JavaScript versions
+ of existing account types in an incremental manner, slowly converting
+ methods from C++ to JavaScript.
+ </p>
+ <p><br /></p>
+ </body>
+</html>
diff --git a/comm/mailnews/jsaccount/src/DelegateList.cpp b/comm/mailnews/jsaccount/src/DelegateList.cpp
new file mode 100644
index 0000000000..cd146fe3dc
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/DelegateList.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "DelegateList.h"
+
+// This class is used within JsAccount to allow static storage of a list
+// of methods to be overridden by JS implementations, in a way that can
+// be stored and manipulated in JS, but used efficiently in C++.
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS(DelegateList, msgIDelegateList)
+
+NS_IMETHODIMP DelegateList::Add(const nsACString& aMethodName) {
+ mMethods.InsertOrUpdate(aMethodName, true);
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/DelegateList.h b/comm/mailnews/jsaccount/src/DelegateList.h
new file mode 100644
index 0000000000..f59e178f0c
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/DelegateList.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _DelegateList_H_
+#define _DelegateList_H_
+
+#include "nsISupports.h"
+#include "msgIDelegateList.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace mailnews {
+
+// This class provides a list of method names to delegate to another object.
+class DelegateList : public msgIDelegateList {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MSGIDELEGATELIST
+ DelegateList() {}
+ nsTHashMap<nsCStringHashKey, bool> mMethods;
+
+ protected:
+ virtual ~DelegateList() {}
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+/*
+ * This macro is used in forwarding functions.
+ * _jsdelegate: the name of the JS pointer that implements a particular
+ * interface.
+ * _jsmethods: the DelegateList object
+ * _cppbase: the C++ base instance (used when call not delegated to js)
+ *
+ **/
+
+#define DELEGATE_JS(_jsdelegate, _jsmethods, _cppbase) \
+ (_jsdelegate && _jsmethods && \
+ _jsmethods->Contains(nsLiteralCString(__func__)) \
+ ? _jsdelegate \
+ : (_cppbase))
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/JaAbDirectory.cpp b/comm/mailnews/jsaccount/src/JaAbDirectory.cpp
new file mode 100644
index 0000000000..11b29e99ab
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaAbDirectory.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "JaAbDirectory.h"
+#include "nsComponentManagerUtils.h"
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS_INHERITED(JaBaseCppAbDirectory, nsAbDirProperty,
+ nsIInterfaceRequestor)
+
+// nsIInterfaceRequestor implementation
+NS_IMETHODIMP JaBaseCppAbDirectory::GetInterface(const nsIID& aIID,
+ void** aSink) {
+ return QueryInterface(aIID, aSink);
+}
+
+// Delegator
+NS_IMPL_ISUPPORTS_INHERITED(JaCppAbDirectoryDelegator, JaBaseCppAbDirectory,
+ msgIOverride)
+
+// Delegator object to bypass JS method override.
+NS_IMPL_ISUPPORTS(JaCppAbDirectoryDelegator::Super, nsIAbDirectory,
+ nsIInterfaceRequestor)
+
+JaCppAbDirectoryDelegator::JaCppAbDirectoryDelegator()
+ : mCppBase(new Super(this)), mMethods(nullptr) {}
+
+NS_IMETHODIMP JaCppAbDirectoryDelegator::SetMethodsToDelegate(
+ msgIDelegateList* aDelegateList) {
+ if (!aDelegateList) {
+ NS_WARNING("Null delegate list");
+ return NS_ERROR_NULL_POINTER;
+ }
+ // We static_cast since we want to use the hash object directly.
+ mDelegateList = static_cast<DelegateList*>(aDelegateList);
+ mMethods = &(mDelegateList->mMethods);
+ return NS_OK;
+}
+NS_IMETHODIMP JaCppAbDirectoryDelegator::GetMethodsToDelegate(
+ msgIDelegateList** aDelegateList) {
+ if (!mDelegateList) mDelegateList = new DelegateList();
+ mMethods = &(mDelegateList->mMethods);
+ NS_ADDREF(*aDelegateList = mDelegateList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaCppAbDirectoryDelegator::SetJsDelegate(
+ nsISupports* aJsDelegate) {
+ // If these QIs fail, then overrides are not provided for methods in that
+ // interface, which is OK.
+ mJsISupports = aJsDelegate;
+ mJsIAbDirectory = do_QueryInterface(aJsDelegate);
+ mJsIInterfaceRequestor = do_QueryInterface(aJsDelegate);
+ return NS_OK;
+}
+NS_IMETHODIMP JaCppAbDirectoryDelegator::GetJsDelegate(
+ nsISupports** aJsDelegate) {
+ NS_ENSURE_ARG_POINTER(aJsDelegate);
+ if (mJsISupports) {
+ NS_ADDREF(*aJsDelegate = mJsISupports);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP JaCppAbDirectoryDelegator::GetCppBase(nsISupports** aCppBase) {
+ nsCOMPtr<nsISupports> cppBaseSupports;
+ cppBaseSupports = NS_ISUPPORTS_CAST(nsIAbDirectory*, mCppBase);
+ NS_ENSURE_STATE(cppBaseSupports);
+ cppBaseSupports.forget(aCppBase);
+
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/JaAbDirectory.h b/comm/mailnews/jsaccount/src/JaAbDirectory.h
new file mode 100644
index 0000000000..7a2903d3d4
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaAbDirectory.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _JaAbDirectory_H_
+#define _JaAbDirectory_H_
+
+#include "nsISupports.h"
+#include "DelegateList.h"
+#include "msgIOverride.h"
+#include "nsIAbDirectory.h"
+#include "nsAbDirProperty.h"
+#include "nsTHashMap.h"
+#include "nsIInterfaceRequestor.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/* Header file */
+
+// This class is an XPCOM component, usable in JS, that calls the methods
+// in the C++ base class (bypassing any JS override).
+class JaBaseCppAbDirectory : public nsAbDirProperty,
+ public nsIInterfaceRequestor {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIINTERFACEREQUESTOR
+ JaBaseCppAbDirectory() {}
+
+ protected:
+ virtual ~JaBaseCppAbDirectory() {}
+};
+
+class JaCppAbDirectoryDelegator : public JaBaseCppAbDirectory,
+ public msgIOverride {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MSGIOVERRIDE
+
+ // use mCppBase as a raw pointer where possible
+ NS_FORWARD_NSIABDIRECTORY(DELEGATE_JS(mJsIAbDirectory, mMethods, mCppBase)->)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(
+ DELEGATE_JS(
+ mJsIInterfaceRequestor, mMethods,
+ (nsCOMPtr<nsIInterfaceRequestor>(do_QueryInterface(mCppBase))))
+ ->)
+
+ JaCppAbDirectoryDelegator();
+
+ private:
+ virtual ~JaCppAbDirectoryDelegator() {}
+
+ class Super : public nsIAbDirectory, public nsIInterfaceRequestor {
+ public:
+ explicit Super(JaCppAbDirectoryDelegator* aFakeThis) {
+ mFakeThis = aFakeThis;
+ }
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSIABDIRECTORY(mFakeThis->JaBaseCppAbDirectory::)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(mFakeThis->JaBaseCppAbDirectory::)
+ private:
+ virtual ~Super() {}
+ JaCppAbDirectoryDelegator* mFakeThis;
+ };
+
+ // Interfaces that may be overridden by JS.
+ nsCOMPtr<nsIAbDirectory> mJsIAbDirectory;
+ nsCOMPtr<nsIInterfaceRequestor> mJsIInterfaceRequestor;
+
+ nsCOMPtr<nsISupports> mJsISupports;
+
+ // Class to bypass JS delegates. nsCOMPtr for when we do cycle collection.
+ nsCOMPtr<nsIAbDirectory> mCppBase;
+
+ RefPtr<DelegateList> mDelegateList;
+ nsTHashMap<nsCStringHashKey, bool>* mMethods;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/JaCompose.cpp b/comm/mailnews/jsaccount/src/JaCompose.cpp
new file mode 100644
index 0000000000..a44dac2c01
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaCompose.cpp
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "JaCompose.h"
+#include "nsComponentManagerUtils.h"
+
+// This file specifies the implementation of nsIMsgCompose.idl objects
+// in the JsAccount system.
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS_INHERITED(JaBaseCppCompose, nsMsgCompose,
+ nsIInterfaceRequestor)
+
+// nsIInterfaceRequestor implementation
+NS_IMETHODIMP
+JaBaseCppCompose::GetInterface(const nsIID& aIID, void** aSink) {
+ return QueryInterface(aIID, aSink);
+}
+
+// Delegator object to bypass JS method override.
+
+JaCppComposeDelegator::JaCppComposeDelegator() {
+ mCppBase =
+ do_QueryInterface(NS_ISUPPORTS_CAST(nsIMsgCompose*, new Super(this)));
+ mMethods = nullptr;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(JaCppComposeDelegator, JaBaseCppCompose,
+ msgIOverride)
+
+NS_IMPL_ISUPPORTS(JaCppComposeDelegator::Super, nsIMsgCompose,
+ nsIMsgSendListener, nsIInterfaceRequestor)
+
+NS_IMETHODIMP
+JaCppComposeDelegator::SetMethodsToDelegate(msgIDelegateList* aDelegateList) {
+ if (!aDelegateList) {
+ NS_WARNING("Null delegate list");
+ return NS_ERROR_NULL_POINTER;
+ }
+ // We static_cast since we want to use the hash object directly.
+ mDelegateList = static_cast<DelegateList*>(aDelegateList);
+ mMethods = &(mDelegateList->mMethods);
+ return NS_OK;
+}
+NS_IMETHODIMP
+JaCppComposeDelegator::GetMethodsToDelegate(msgIDelegateList** aDelegateList) {
+ if (!mDelegateList) mDelegateList = new DelegateList();
+ mMethods = &(mDelegateList->mMethods);
+ NS_ADDREF(*aDelegateList = mDelegateList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JaCppComposeDelegator::SetJsDelegate(nsISupports* aJsDelegate) {
+ // If these QIs fail, then overrides are not provided for methods in that
+ // interface, which is OK.
+ mJsISupports = aJsDelegate;
+ mJsIMsgCompose = do_QueryInterface(aJsDelegate);
+ mJsIMsgSendListener = do_QueryInterface(aJsDelegate);
+ mJsIInterfaceRequestor = do_QueryInterface(aJsDelegate);
+ return NS_OK;
+}
+NS_IMETHODIMP
+JaCppComposeDelegator::GetJsDelegate(nsISupports** aJsDelegate) {
+ NS_ENSURE_ARG_POINTER(aJsDelegate);
+ if (mJsISupports) {
+ NS_ADDREF(*aJsDelegate = mJsISupports);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP
+JaCppComposeDelegator::GetCppBase(nsISupports** aCppBase) {
+ nsCOMPtr<nsISupports> cppBaseSupports;
+ cppBaseSupports = NS_ISUPPORTS_CAST(nsIMsgCompose*, mCppBase);
+ NS_ENSURE_STATE(cppBaseSupports);
+ cppBaseSupports.forget(aCppBase);
+
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/JaCompose.h b/comm/mailnews/jsaccount/src/JaCompose.h
new file mode 100644
index 0000000000..587e91738a
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaCompose.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _JaCompose_H_
+#define _JaCompose_H_
+
+#include "nsISupports.h"
+#include "DelegateList.h"
+#include "msgIOverride.h"
+#include "nsMsgCompose.h"
+#include "nsIMsgCompose.h"
+#include "nsTHashMap.h"
+#include "nsIInterfaceRequestor.h"
+
+// This file specifies the definition of nsIMsgCompose.idl objects
+// in the JsAccount system.
+
+namespace mozilla {
+namespace mailnews {
+
+/* Header file */
+
+// This class is an XPCOM component, usable in JS, that calls the methods
+// in the C++ base class (bypassing any JS override).
+class JaBaseCppCompose : public nsMsgCompose, public nsIInterfaceRequestor {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIINTERFACEREQUESTOR
+ JaBaseCppCompose() {}
+
+ protected:
+ virtual ~JaBaseCppCompose() {}
+};
+
+class JaCppComposeDelegator : public JaBaseCppCompose, public msgIOverride {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MSGIOVERRIDE
+
+ NS_FORWARD_NSIMSGCOMPOSE(DELEGATE_JS(mJsIMsgCompose, mMethods, mCppBase)->)
+ NS_FORWARD_NSIMSGSENDLISTENER(
+ DELEGATE_JS(mJsIMsgSendListener, mMethods, mCppBase.get())->)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(
+ DELEGATE_JS(
+ mJsIInterfaceRequestor, mMethods,
+ (nsCOMPtr<nsIInterfaceRequestor>(do_QueryInterface(mCppBase))))
+ ->)
+
+ JaCppComposeDelegator();
+
+ private:
+ virtual ~JaCppComposeDelegator() {}
+
+ // This class will call a method on the delegator, but force the use of the
+ // C++ cppBase class, bypassing any JS Delegate.
+ class Super : public nsIMsgCompose, public nsIInterfaceRequestor {
+ public:
+ explicit Super(JaCppComposeDelegator* aFakeThis) { mFakeThis = aFakeThis; }
+ NS_DECL_ISUPPORTS
+ // Forward all overridable methods, bypassing JS override.
+ NS_FORWARD_NSIMSGCOMPOSE(mFakeThis->JaBaseCppCompose::)
+ NS_FORWARD_NSIMSGSENDLISTENER(mFakeThis->JaBaseCppCompose::)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(mFakeThis->JaBaseCppCompose::)
+ private:
+ virtual ~Super(){};
+ JaCppComposeDelegator* mFakeThis;
+ };
+
+ // Interfaces that may be overridden by JS.
+ nsCOMPtr<nsIMsgCompose> mJsIMsgCompose;
+ nsCOMPtr<nsIMsgSendListener> mJsIMsgSendListener;
+ nsCOMPtr<nsIInterfaceRequestor> mJsIInterfaceRequestor;
+
+ nsCOMPtr<nsISupports> mJsISupports;
+
+ // Class to bypass JS delegates.
+ nsCOMPtr<nsIMsgCompose> mCppBase;
+
+ RefPtr<DelegateList> mDelegateList;
+ nsTHashMap<nsCStringHashKey, bool>* mMethods;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/JaIncomingServer.cpp b/comm/mailnews/jsaccount/src/JaIncomingServer.cpp
new file mode 100644
index 0000000000..8acdf9c655
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaIncomingServer.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "JaIncomingServer.h"
+#include "nsComponentManagerUtils.h"
+
+// This file specifies the implementation of nsIMsgIncomingServer.idl objects
+// in the JsAccount system.
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS_INHERITED(JaBaseCppIncomingServer, nsMsgIncomingServer,
+ nsIInterfaceRequestor)
+
+// nsMsgIncomingServer overrides
+nsresult JaBaseCppIncomingServer::CreateRootFolderFromUri(
+ const nsACString& serverUri, nsIMsgFolder** rootFolder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// nsIInterfaceRequestor implementation
+NS_IMETHODIMP
+JaBaseCppIncomingServer::GetInterface(const nsIID& aIID, void** aSink) {
+ return QueryInterface(aIID, aSink);
+}
+
+// Delegator object to bypass JS method override.
+
+JaCppIncomingServerDelegator::JaCppIncomingServerDelegator() {
+ mCppBase = do_QueryInterface(
+ NS_ISUPPORTS_CAST(nsIMsgIncomingServer*, new Super(this)));
+ mMethods = nullptr;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(JaCppIncomingServerDelegator,
+ JaBaseCppIncomingServer, msgIOverride)
+
+NS_IMPL_ISUPPORTS(JaCppIncomingServerDelegator::Super, nsIMsgIncomingServer,
+ nsIInterfaceRequestor)
+
+NS_IMETHODIMP
+JaCppIncomingServerDelegator::SetMethodsToDelegate(
+ msgIDelegateList* aDelegateList) {
+ if (!aDelegateList) {
+ NS_WARNING("Null delegate list");
+ return NS_ERROR_NULL_POINTER;
+ }
+ // We static_cast since we want to use the hash object directly.
+ mDelegateList = static_cast<DelegateList*>(aDelegateList);
+ mMethods = &(mDelegateList->mMethods);
+ return NS_OK;
+}
+NS_IMETHODIMP
+JaCppIncomingServerDelegator::GetMethodsToDelegate(
+ msgIDelegateList** aDelegateList) {
+ if (!mDelegateList) mDelegateList = new DelegateList();
+ mMethods = &(mDelegateList->mMethods);
+ NS_ADDREF(*aDelegateList = mDelegateList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JaCppIncomingServerDelegator::SetJsDelegate(nsISupports* aJsDelegate) {
+ // If these QIs fail, then overrides are not provided for methods in that
+ // interface, which is OK.
+ mJsISupports = aJsDelegate;
+ mJsIMsgIncomingServer = do_QueryInterface(aJsDelegate);
+ mJsIInterfaceRequestor = do_QueryInterface(aJsDelegate);
+ return NS_OK;
+}
+NS_IMETHODIMP
+JaCppIncomingServerDelegator::GetJsDelegate(nsISupports** aJsDelegate) {
+ NS_ENSURE_ARG_POINTER(aJsDelegate);
+ if (mJsISupports) {
+ NS_ADDREF(*aJsDelegate = mJsISupports);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP
+JaCppIncomingServerDelegator::GetCppBase(nsISupports** aCppBase) {
+ nsCOMPtr<nsISupports> cppBaseSupports;
+ cppBaseSupports = NS_ISUPPORTS_CAST(nsIMsgIncomingServer*, mCppBase);
+ NS_ENSURE_STATE(cppBaseSupports);
+ cppBaseSupports.forget(aCppBase);
+
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/JaIncomingServer.h b/comm/mailnews/jsaccount/src/JaIncomingServer.h
new file mode 100644
index 0000000000..67c7fd48f6
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaIncomingServer.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _JaIncomingServer_H_
+#define _JaIncomingServer_H_
+
+#include "nsISupports.h"
+#include "DelegateList.h"
+#include "msgIOverride.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsMsgIncomingServer.h"
+#include "nsTHashMap.h"
+#include "nsIInterfaceRequestor.h"
+
+// This file specifies the definition of nsIMsgIncomingServer.idl objects
+// in the JsAccount system.
+
+namespace mozilla {
+namespace mailnews {
+
+/* Header file */
+
+// This class is an XPCOM component, usable in JS, that calls the methods
+// in the C++ base class (bypassing any JS override).
+class JaBaseCppIncomingServer : public nsMsgIncomingServer,
+ public nsIInterfaceRequestor {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIINTERFACEREQUESTOR
+ JaBaseCppIncomingServer() {}
+
+ // nsMsgIncomingServer overrides
+ nsresult CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) override;
+
+ protected:
+ virtual ~JaBaseCppIncomingServer() {}
+};
+
+class JaCppIncomingServerDelegator : public JaBaseCppIncomingServer,
+ public msgIOverride {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MSGIOVERRIDE
+
+ // use mCppBase as a raw pointer where possible
+ NS_FORWARD_NSIMSGINCOMINGSERVER(
+ DELEGATE_JS(mJsIMsgIncomingServer, mMethods, mCppBase)->)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(
+ DELEGATE_JS(
+ mJsIInterfaceRequestor, mMethods,
+ (nsCOMPtr<nsIInterfaceRequestor>(do_QueryInterface(mCppBase))))
+ ->)
+
+ JaCppIncomingServerDelegator();
+
+ private:
+ virtual ~JaCppIncomingServerDelegator() {}
+
+ // This class will call a method on the delegator, but force the use of the
+ // C++ parent class, bypassing any JS Delegate.
+ class Super : public nsIMsgIncomingServer, public nsIInterfaceRequestor {
+ public:
+ explicit Super(JaCppIncomingServerDelegator* aFakeThis) {
+ mFakeThis = aFakeThis;
+ }
+ NS_DECL_ISUPPORTS
+ // Forward all overridable methods, bypassing JS override.
+ NS_FORWARD_NSIMSGINCOMINGSERVER(mFakeThis->JaBaseCppIncomingServer::)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(mFakeThis->JaBaseCppIncomingServer::)
+ private:
+ virtual ~Super(){};
+ JaCppIncomingServerDelegator* mFakeThis;
+ };
+
+ // Interfaces that may be overridden by JS.
+ nsCOMPtr<nsIMsgIncomingServer> mJsIMsgIncomingServer;
+ nsCOMPtr<nsIInterfaceRequestor> mJsIInterfaceRequestor;
+
+ nsCOMPtr<nsISupports> mJsISupports;
+
+ // Class to bypass JS delegates.
+ nsCOMPtr<nsIMsgIncomingServer> mCppBase;
+
+ RefPtr<DelegateList> mDelegateList;
+ nsTHashMap<nsCStringHashKey, bool>* mMethods;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/JaMsgFolder.cpp b/comm/mailnews/jsaccount/src/JaMsgFolder.cpp
new file mode 100644
index 0000000000..e10f9f1704
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaMsgFolder.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "JaMsgFolder.h"
+#include "nsComponentManagerUtils.h"
+
+#define MAILDATABASE_CONTRACTID_BASE "@mozilla.org/nsMsgDatabase/msgDB-"
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS_INHERITED(JaBaseCppMsgFolder, nsMsgDBFolder,
+ nsIInterfaceRequestor)
+
+// nsIInterfaceRequestor implementation
+NS_IMETHODIMP
+JaBaseCppMsgFolder::GetInterface(const nsIID& aIID, void** aSink) {
+ return QueryInterface(aIID, aSink);
+}
+
+// Definition of abstract nsMsgDBFolder methods.
+nsresult JaBaseCppMsgFolder::GetDatabase() {
+ nsresult rv = NS_OK;
+ if (!mDatabase) {
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the database, keeping it if it is "out of date"
+ rv = msgDBService->OpenFolderDB(this, true, getter_AddRefs(mDatabase));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) {
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ NS_ENSURE_STATE(mDatabase);
+ // not sure about this ... the issue is that if the summary is not valid,
+ // then the db does not get added to the cache in the future, and
+ // reindexes do not show all of the messages.
+ // mDatabase->SetSummaryValid(true);
+ mDatabase->SetSummaryValid(false);
+ CreateDummyFile(this);
+ }
+
+ if (rv != NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ NS_ENSURE_SUCCESS(rv, rv);
+ else if (mDatabase) {
+ // Not going to warn here, because on initialization we set all
+ // databases as invalid.
+ // NS_WARNING("Mail Summary database is out of date");
+ // Grrr, the only way to get this into the cache is to set the db as
+ // valid,
+ // close, reopen, then set as invalid.
+ mDatabase->SetSummaryValid(true);
+ msgDBService->ForceFolderDBClosed(this);
+ rv = msgDBService->OpenFolderDB(this, true, getter_AddRefs(mDatabase));
+ if (mDatabase) mDatabase->SetSummaryValid(false);
+ }
+
+ if (mDatabase) {
+ //
+ // When I inadvertently deleted the out-of-date database, I hit this code
+ // with the db's m_dbFolderInfo as null from the delete, yet the local
+ // mDatabase reference kept the database alive. So I hit an assert when I
+ // tried to open the database. Be careful if you try to fix the
+ // out-of-date issues!
+ //
+ // UpdateNewMessages();
+ if (mAddListener) mDatabase->AddListener(this);
+ // UpdateSummaryTotals can null mDatabase during initialization, so we
+ // save a local copy
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ UpdateSummaryTotals(true);
+ mDatabase = database;
+ }
+ }
+
+ return rv;
+}
+
+/*
+ * The utility function GetSummaryFileLocation takes a folder file,
+ * then appends .msf to come up with the name of the database file. So
+ * we need a placeholder file with simply the folder name. This method
+ * creates an appropriate file as a placeholder, or you may use the file if
+ * appropriate.
+ */
+nsresult JaBaseCppMsgFolder::CreateDummyFile(nsIMsgFolder* aMailFolder) {
+ nsresult rv;
+ if (!aMailFolder) return NS_OK;
+ nsCOMPtr<nsIFile> path;
+ // need to make sure folder exists...
+ aMailFolder->GetFilePath(getter_AddRefs(path));
+ if (path) {
+ bool exists;
+ rv = path->Exists(&exists);
+ if (!exists) {
+ rv = path->Create(nsIFile::NORMAL_FILE_TYPE, 0644);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+// AFAICT this is unused in mailnews code.
+nsresult JaBaseCppMsgFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// Delegator object to bypass JS method override.
+
+JaCppMsgFolderDelegator::JaCppMsgFolderDelegator()
+ : mCppBase(new Super(this)), mMethods(nullptr) {}
+
+NS_IMPL_ISUPPORTS_INHERITED(JaCppMsgFolderDelegator, JaBaseCppMsgFolder,
+ msgIOverride)
+
+NS_IMPL_ISUPPORTS(JaCppMsgFolderDelegator::Super, nsIMsgFolder,
+ nsIDBChangeListener, nsIUrlListener,
+ nsIJunkMailClassificationListener,
+ nsIMsgTraitClassificationListener, nsIInterfaceRequestor)
+
+NS_IMETHODIMP
+JaCppMsgFolderDelegator::SetMethodsToDelegate(msgIDelegateList* aDelegateList) {
+ if (!aDelegateList) {
+ NS_WARNING("Null delegate list");
+ return NS_ERROR_NULL_POINTER;
+ }
+ // We static_cast since we want to use the hash object directly.
+ mDelegateList = static_cast<DelegateList*>(aDelegateList);
+ mMethods = &(mDelegateList->mMethods);
+ return NS_OK;
+}
+NS_IMETHODIMP
+JaCppMsgFolderDelegator::GetMethodsToDelegate(
+ msgIDelegateList** aDelegateList) {
+ if (!mDelegateList) mDelegateList = new DelegateList();
+ mMethods = &(mDelegateList->mMethods);
+ NS_ADDREF(*aDelegateList = mDelegateList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaCppMsgFolderDelegator::SetJsDelegate(nsISupports* aJsDelegate) {
+ // If these QIs fail, then overrides are not provided for methods in that
+ // interface, which is OK.
+ mJsISupports = aJsDelegate;
+ mJsIMsgFolder = do_QueryInterface(aJsDelegate);
+ mJsIDBChangeListener = do_QueryInterface(aJsDelegate);
+ mJsIUrlListener = do_QueryInterface(aJsDelegate);
+ mJsIJunkMailClassificationListener = do_QueryInterface(aJsDelegate);
+ mJsIMsgTraitClassificationListener = do_QueryInterface(aJsDelegate);
+ mJsIInterfaceRequestor = do_QueryInterface(aJsDelegate);
+ return NS_OK;
+}
+NS_IMETHODIMP JaCppMsgFolderDelegator::GetJsDelegate(
+ nsISupports** aJsDelegate) {
+ NS_ENSURE_ARG_POINTER(aJsDelegate);
+ if (mJsISupports) {
+ NS_ADDREF(*aJsDelegate = mJsISupports);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP JaCppMsgFolderDelegator::GetCppBase(nsISupports** aCppBase) {
+ nsCOMPtr<nsISupports> cppBaseSupports;
+ cppBaseSupports = NS_ISUPPORTS_CAST(nsIMsgFolder*, mCppBase);
+ NS_ENSURE_STATE(cppBaseSupports);
+ cppBaseSupports.forget(aCppBase);
+
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/JaMsgFolder.h b/comm/mailnews/jsaccount/src/JaMsgFolder.h
new file mode 100644
index 0000000000..97cdfce5e6
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaMsgFolder.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _JaMsgFolder_H_
+#define _JaMsgFolder_H_
+
+#include "nsISupports.h"
+#include "DelegateList.h"
+#include "msgIOverride.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgDBFolder.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashMap.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsNetUtil.h"
+#include "nsIDBChangeListener.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIInterfaceRequestor.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/* Header file */
+
+// This class is an XPCOM component, usable in JS, that calls the methods
+// in the C++ base class (bypassing any JS override).
+class JaBaseCppMsgFolder : public nsMsgDBFolder,
+ public nsIInterfaceRequestor
+
+{
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIINTERFACEREQUESTOR
+ JaBaseCppMsgFolder() {}
+
+ // nsMsgDBFolder overrides
+
+ nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+ nsresult GetDatabase() override;
+
+ // Local Utility Functions
+
+ // Create a placeholder file to represent a folder.
+ nsresult CreateDummyFile(nsIMsgFolder* aMailFolder);
+
+ protected:
+ virtual ~JaBaseCppMsgFolder() {}
+};
+
+class JaCppMsgFolderDelegator : public JaBaseCppMsgFolder, public msgIOverride {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MSGIOVERRIDE
+
+ NS_FORWARD_NSIMSGFOLDER(DELEGATE_JS(mJsIMsgFolder, mMethods, mCppBase)->)
+ NS_FORWARD_NSIDBCHANGELISTENER(
+ DELEGATE_JS(mJsIDBChangeListener, mMethods,
+ (nsCOMPtr<nsIDBChangeListener>(do_QueryInterface(mCppBase))))
+ ->)
+ NS_FORWARD_NSIURLLISTENER(
+ DELEGATE_JS(mJsIUrlListener, mMethods,
+ (nsCOMPtr<nsIUrlListener>(do_QueryInterface(mCppBase))))
+ ->)
+ NS_FORWARD_NSIJUNKMAILCLASSIFICATIONLISTENER(
+ DELEGATE_JS(mJsIJunkMailClassificationListener, mMethods,
+ (nsCOMPtr<nsIJunkMailClassificationListener>(
+ do_QueryInterface(mCppBase))))
+ ->)
+ NS_FORWARD_NSIMSGTRAITCLASSIFICATIONLISTENER(
+ DELEGATE_JS(mJsIMsgTraitClassificationListener, mMethods,
+ (nsCOMPtr<nsIMsgTraitClassificationListener>(
+ do_QueryInterface(mCppBase))))
+ ->)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(
+ DELEGATE_JS(
+ mJsIInterfaceRequestor, mMethods,
+ (nsCOMPtr<nsIInterfaceRequestor>(do_QueryInterface(mCppBase))))
+ ->)
+
+ JaCppMsgFolderDelegator();
+
+ private:
+ virtual ~JaCppMsgFolderDelegator() {}
+
+ class Super : public nsIMsgFolder,
+ public nsIDBChangeListener,
+ public nsIUrlListener,
+ public nsIJunkMailClassificationListener,
+ public nsIMsgTraitClassificationListener,
+ public nsIInterfaceRequestor {
+ public:
+ // Why fake this? Because this method is fully owned by
+ // JaCppMsgFolderDelegator, and this reference is to the "this" of the
+ // main method. But it is not really the local "this".
+ explicit Super(JaCppMsgFolderDelegator* aFakeThis) {
+ mFakeThis = aFakeThis;
+ }
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSIMSGFOLDER(mFakeThis->JaBaseCppMsgFolder::)
+ NS_FORWARD_NSIDBCHANGELISTENER(mFakeThis->JaBaseCppMsgFolder::)
+ NS_FORWARD_NSIURLLISTENER(mFakeThis->JaBaseCppMsgFolder::)
+ NS_FORWARD_NSIJUNKMAILCLASSIFICATIONLISTENER(
+ mFakeThis->JaBaseCppMsgFolder::)
+ NS_FORWARD_NSIMSGTRAITCLASSIFICATIONLISTENER(
+ mFakeThis->JaBaseCppMsgFolder::)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(mFakeThis->JaBaseCppMsgFolder::)
+ private:
+ virtual ~Super() {}
+ JaCppMsgFolderDelegator* mFakeThis;
+ };
+
+ // Interfaces that may be overridden by JS.
+ nsCOMPtr<nsIMsgFolder> mJsIMsgFolder;
+ nsCOMPtr<nsIDBChangeListener> mJsIDBChangeListener;
+ nsCOMPtr<nsIUrlListener> mJsIUrlListener;
+ nsCOMPtr<nsIJunkMailClassificationListener>
+ mJsIJunkMailClassificationListener;
+ nsCOMPtr<nsIMsgTraitClassificationListener>
+ mJsIMsgTraitClassificationListener;
+ nsCOMPtr<nsIInterfaceRequestor> mJsIInterfaceRequestor;
+
+ nsCOMPtr<nsISupports> mJsISupports;
+
+ nsCOMPtr<nsIMsgFolder> mCppBase;
+ RefPtr<DelegateList> mDelegateList;
+ nsTHashMap<nsCStringHashKey, bool>* mMethods;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/JaUrl.cpp b/comm/mailnews/jsaccount/src/JaUrl.cpp
new file mode 100644
index 0000000000..249c45c9df
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaUrl.cpp
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "JaUrl.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIFile.h"
+#include "nsIMessenger.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgUtils.h"
+
+// This file contains an implementation of mailnews URLs in JsAccount.
+
+namespace mozilla {
+namespace mailnews {
+
+NS_IMPL_ISUPPORTS_INHERITED(JaBaseCppUrl, nsMsgMailNewsUrl, nsIMsgMessageUrl,
+ msgIJaUrl, nsIInterfaceRequestor,
+ nsISupportsWeakReference)
+
+// nsIMsgMailNewsUrl overrides
+NS_IMETHODIMP JaBaseCppUrl::GetFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_IF_ADDREF(*aFolder = mFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::SetFolder(nsIMsgFolder* aFolder) {
+ mFolder = aFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetServer(nsIMsgIncomingServer** aIncomingServer) {
+ if (mFolder) {
+ return mFolder->GetServer(aIncomingServer);
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+ *isType = (m_urlType == type);
+ return NS_OK;
+}
+
+// nsIMsgMessageUrl implementation
+NS_IMETHODIMP JaBaseCppUrl::GetUri(nsACString& aUri) {
+ if (!mUri.IsEmpty())
+ aUri = mUri;
+ else
+ return NS_ERROR_NOT_INITIALIZED;
+ return NS_OK;
+}
+NS_IMETHODIMP JaBaseCppUrl::SetUri(const nsACString& aUri) {
+ mUri = aUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetMessageFile(nsIFile** aMessageFile) {
+ NS_ENSURE_ARG_POINTER(aMessageFile);
+ NS_IF_ADDREF(*aMessageFile = mMessageFile);
+ return NS_OK;
+}
+NS_IMETHODIMP JaBaseCppUrl::SetMessageFile(nsIFile* aMessageFile) {
+ mMessageFile = aMessageFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetAddDummyEnvelope(bool* aAddDummyEnvelope) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP JaBaseCppUrl::SetAddDummyEnvelope(bool aAddDummyEnvelope) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetCanonicalLineEnding(bool* aCanonicalLineEnding) {
+ NS_ENSURE_ARG_POINTER(aCanonicalLineEnding);
+ *aCanonicalLineEnding = mCanonicalLineEnding;
+ return NS_OK;
+}
+NS_IMETHODIMP JaBaseCppUrl::SetCanonicalLineEnding(bool aCanonicalLineEnding) {
+ mCanonicalLineEnding = aCanonicalLineEnding;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetOriginalSpec(nsACString& aOriginalSpec) {
+ if (mOriginalSpec.IsEmpty()) return NS_ERROR_NULL_POINTER;
+ aOriginalSpec = mOriginalSpec;
+ return NS_OK;
+}
+NS_IMETHODIMP JaBaseCppUrl::SetOriginalSpec(const nsACString& aOriginalSpec) {
+ mOriginalSpec = aOriginalSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ // URLs contain a lot of query parts. We want need a normalized form:
+ // scheme://server/folder?number=123
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsURL;
+ QueryInterface(NS_GET_IID(nsIMsgMailNewsUrl), getter_AddRefs(mailnewsURL));
+
+ nsAutoCString spec;
+ mailnewsURL->GetSpecIgnoringRef(spec);
+
+ nsCString queryPart = MsgExtractQueryPart(spec, "number=");
+
+ // Strip any query part beginning with ? or /;
+ MsgRemoveQueryPart(spec);
+
+ if (!queryPart.IsEmpty()) spec += "?"_ns + queryPart;
+
+ aPrincipalSpec.Assign(spec);
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::GetMessageHeader(nsIMsgDBHdr** aMessageHeader) {
+ // This routine does a lookup using messenger, assuming that the message URI
+ // has been set in mUri.
+ NS_ENSURE_TRUE(!mUri.IsEmpty(), NS_ERROR_NOT_INITIALIZED);
+ nsresult rv;
+ nsCOMPtr<nsIMessenger> messenger(
+ do_CreateInstance("@mozilla.org/messenger;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = messenger->MsgHdrFromURI(mUri, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgHdr.forget(aMessageHeader);
+ return NS_OK;
+}
+
+// msgIJaUrl implementation
+NS_IMETHODIMP JaBaseCppUrl::SetUrlType(unsigned int type) {
+ m_urlType = type;
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaBaseCppUrl::SetSpec(const nsACString& aSpec) {
+ return SetSpecInternal(aSpec);
+}
+
+// nsIInterfaceRequestor implementation
+NS_IMETHODIMP JaBaseCppUrl::GetInterface(const nsIID& aIID, void** aSink) {
+ return QueryInterface(aIID, aSink);
+}
+
+// Delegator
+NS_IMPL_ISUPPORTS_INHERITED(JaCppUrlDelegator, JaBaseCppUrl, msgIOverride)
+
+// Delegator object to bypass JS method override.
+NS_IMPL_ISUPPORTS(JaCppUrlDelegator::Super, nsIMsgMessageUrl, nsIURI, nsIURL,
+ nsIURIWithSpecialOrigin, nsIMsgMailNewsUrl, msgIJaUrl,
+ nsIInterfaceRequestor, nsISupportsWeakReference)
+
+JaCppUrlDelegator::JaCppUrlDelegator()
+ : mCppBase(new Super(this)), mMethods(nullptr) {}
+
+NS_IMETHODIMP JaCppUrlDelegator::SetMethodsToDelegate(
+ msgIDelegateList* aDelegateList) {
+ if (!aDelegateList) {
+ NS_WARNING("Null delegate list");
+ return NS_ERROR_NULL_POINTER;
+ }
+ // We static_cast since we want to use the hash object directly.
+ mDelegateList = static_cast<DelegateList*>(aDelegateList);
+ mMethods = &(mDelegateList->mMethods);
+ return NS_OK;
+}
+NS_IMETHODIMP JaCppUrlDelegator::GetMethodsToDelegate(
+ msgIDelegateList** aDelegateList) {
+ if (!mDelegateList) mDelegateList = new DelegateList();
+ mMethods = &(mDelegateList->mMethods);
+ NS_ADDREF(*aDelegateList = mDelegateList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP JaCppUrlDelegator::SetJsDelegate(nsISupports* aJsDelegate) {
+ // If these QIs fail, then overrides are not provided for methods in that
+ // interface, which is OK.
+ mJsISupports = aJsDelegate;
+ mJsIMsgMessageUrl = do_QueryInterface(aJsDelegate);
+ mJsIInterfaceRequestor = do_QueryInterface(aJsDelegate);
+ return NS_OK;
+}
+NS_IMETHODIMP JaCppUrlDelegator::GetJsDelegate(nsISupports** aJsDelegate) {
+ NS_ENSURE_ARG_POINTER(aJsDelegate);
+ if (mJsISupports) {
+ NS_ADDREF(*aJsDelegate = mJsISupports);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+}
+
+NS_IMETHODIMP JaCppUrlDelegator::GetCppBase(nsISupports** aCppBase) {
+ nsCOMPtr<nsISupports> cppBaseSupports;
+ cppBaseSupports = NS_ISUPPORTS_CAST(nsIMsgMailNewsUrl*, mCppBase);
+ NS_ENSURE_STATE(cppBaseSupports);
+ cppBaseSupports.forget(aCppBase);
+
+ return NS_OK;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/jsaccount/src/JaUrl.h b/comm/mailnews/jsaccount/src/JaUrl.h
new file mode 100644
index 0000000000..6de4feba60
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/JaUrl.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef _JaUrl_H_
+#define _JaUrl_H_
+
+#include "DelegateList.h"
+#include "msgCore.h"
+#include "msgIOverride.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashMap.h"
+#include "nsIFile.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIMsgFolder.h"
+#include "nsISupports.h"
+#include "nsIURI.h"
+#include "nsIURL.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsWeakReference.h"
+#include "msgIJaUrl.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/* Header file */
+
+// This class is an XPCOM component, usable in JS, that calls the methods
+// in the C++ base class (bypassing any JS override).
+class JaBaseCppUrl : public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public msgIJaUrl,
+ public nsIInterfaceRequestor,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_MSGIJAURL
+ NS_DECL_NSIINTERFACEREQUESTOR
+ JaBaseCppUrl() {}
+
+ // nsIMsgMailNewsUrl overrides
+ NS_IMETHOD GetFolder(nsIMsgFolder** aFolder) override;
+ NS_IMETHOD SetFolder(nsIMsgFolder* aFolder) override;
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+ NS_IMETHOD GetServer(nsIMsgIncomingServer** aIncomingServer) override;
+
+ protected:
+ virtual ~JaBaseCppUrl() {}
+
+ // nsIMsgMailUrl variables.
+
+ nsCOMPtr<nsIMsgFolder> mFolder;
+
+ // nsIMsgMessageUrl variables.
+
+ // the uri for the original message, like ews-message://server/folder#123
+ nsCString mUri;
+ nsCOMPtr<nsIFile> mMessageFile;
+ bool mCanonicalLineEnding = false;
+ nsCString mOriginalSpec;
+
+ // msgIJaUrl variables
+ unsigned int m_urlType{0};
+};
+
+class JaCppUrlDelegator : public JaBaseCppUrl, public msgIOverride {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MSGIOVERRIDE
+
+ NS_FORWARD_NSIMSGMESSAGEURL(
+ DELEGATE_JS(mJsIMsgMessageUrl, mMethods,
+ (nsCOMPtr<nsIMsgMessageUrl>(do_QueryInterface(mCppBase))))
+ ->)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(
+ DELEGATE_JS(
+ mJsIInterfaceRequestor, mMethods,
+ (nsCOMPtr<nsIInterfaceRequestor>(do_QueryInterface(mCppBase))))
+ ->)
+
+ JaCppUrlDelegator();
+
+ class Super : public nsIMsgMailNewsUrl,
+ public nsIURIWithSpecialOrigin,
+ public nsIMsgMessageUrl,
+ public msgIJaUrl,
+ public nsIInterfaceRequestor,
+ public nsISupportsWeakReference {
+ public:
+ explicit Super(JaCppUrlDelegator* aFakeThis) { mFakeThis = aFakeThis; }
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSIMSGMAILNEWSURL(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSIURI(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSIURL(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSIURIWITHSPECIALORIGIN(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSIMSGMESSAGEURL(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_MSGIJAURL(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSIINTERFACEREQUESTOR(mFakeThis->JaBaseCppUrl::)
+ NS_FORWARD_NSISUPPORTSWEAKREFERENCE(mFakeThis->JaBaseCppUrl::)
+ private:
+ virtual ~Super() {}
+ JaCppUrlDelegator* mFakeThis;
+ };
+
+ private:
+ virtual ~JaCppUrlDelegator() {
+ NS_ReleaseOnMainThread("JaCppUrlDelegator::mJsIMsgMessageUrl",
+ mJsIMsgMessageUrl.forget());
+ NS_ReleaseOnMainThread("JaCppUrlDelegator::mJsIInterfaceRequestor",
+ mJsIInterfaceRequestor.forget());
+ NS_ReleaseOnMainThread("JaCppUrlDelegator::mJsISupports",
+ mJsISupports.forget());
+ NS_ReleaseOnMainThread("JaCppUrlDelegator::mDelegateList",
+ mDelegateList.forget());
+ }
+
+ // Interfaces that may be overridden by JS.
+ nsCOMPtr<nsIMsgMessageUrl> mJsIMsgMessageUrl;
+ nsCOMPtr<nsIInterfaceRequestor> mJsIInterfaceRequestor;
+
+ // Owning reference to the JS override.
+ nsCOMPtr<nsISupports> mJsISupports;
+
+ // Class to bypass JS delegates. nsCOMPtr for when we do cycle collection.
+ nsCOMPtr<nsIMsgMailNewsUrl> mCppBase;
+
+ RefPtr<DelegateList> mDelegateList;
+ nsTHashMap<nsCStringHashKey, bool>* mMethods;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/jsaccount/src/components.conf b/comm/mailnews/jsaccount/src/components.conf
new file mode 100644
index 0000000000..89b4543316
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/components.conf
@@ -0,0 +1,37 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{77b5592c-5018-436d-a466-c4e5443a1669}",
+ "contract_ids": ["@mozilla.org/jacppabdirectorydelegator;1"],
+ "type": "mailnews::JaCppAbDirectoryDelegator",
+ "headers": ["/comm/mailnews/jsaccount/src/JaAbDirectory.h"],
+ },
+ {
+ "cid": "{cfcd1caa-00d9-40d0-831e-673820e04fc6}",
+ "contract_ids": ["@mozilla.org/jacppcomposedelegator;1"],
+ "type": "mailnews::JaCppComposeDelegator",
+ "headers": ["/comm/mailnews/jsaccount/src/JaCompose.h"],
+ },
+ {
+ "cid": "{7aa11dd3-5590-4e01-bd87-91f60272d01a}",
+ "contract_ids": ["@mozilla.org/jacppincomingserverdelegator;1"],
+ "type": "mailnews::JaCppIncomingServerDelegator",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/jsaccount/src/JaIncomingServer.h"],
+ },
+ {
+ "cid": "{d6bd81fa-b1d4-424a-88ea-bb3ea8381d50}",
+ "contract_ids": ["@mozilla.org/jacppmsgfolderdelegator;1"],
+ "type": "mailnews::JaCppMsgFolderDelegator",
+ "headers": ["/comm/mailnews/jsaccount/src/JaMsgFolder.h"],
+ },
+ {
+ "cid": "{1a0b778c-2fe6-4012-b4f3-e81c0c116409}",
+ "contract_ids": ["@mozilla.org/jacppurldelegator;1"],
+ "type": "mailnews::JaCppUrlDelegator",
+ "headers": ["/comm/mailnews/jsaccount/src/JaUrl.h"],
+ },
+]
diff --git a/comm/mailnews/jsaccount/src/moz.build b/comm/mailnews/jsaccount/src/moz.build
new file mode 100644
index 0000000000..a78cb5c1f7
--- /dev/null
+++ b/comm/mailnews/jsaccount/src/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "DelegateList.cpp",
+ "JaAbDirectory.cpp",
+ "JaCompose.cpp",
+ "JaIncomingServer.cpp",
+ "JaMsgFolder.cpp",
+ "JaUrl.cpp",
+]
+
+EXPORTS += [
+ "DelegateList.h",
+ "JaAbDirectory.h",
+ "JaCompose.h",
+ "JaIncomingServer.h",
+ "JaMsgFolder.h",
+ "JaUrl.h",
+]
+
+Library("JsAccount")
+FINAL_LIBRARY = "mail"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/jsaccount/test/components.conf b/comm/mailnews/jsaccount/test/components.conf
new file mode 100644
index 0000000000..63e26224b3
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{74b9b9c3-9594-41c4-b9f0-326e5daac2e0}",
+ "contract_ids": ["@mozilla.org/messenger/protocol/info;1?type=testja"],
+ "jsm": "resource:///modules/jsaccount/TestJaMsgProtocolInfoComponent.jsm",
+ "constructor": "TestJaMsgProtocolInfo",
+ },
+]
diff --git a/comm/mailnews/jsaccount/test/idl/moz.build b/comm/mailnews/jsaccount/test/idl/moz.build
new file mode 100644
index 0000000000..9975e6125b
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/idl/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+XPIDL_SOURCES += [
+ "msgIFooUrl.idl",
+]
+
+XPIDL_MODULE = "testJsAccount"
+
+if "comm" in CONFIG["MOZ_BUILD_APP"]:
+ test_harness_base = TEST_HARNESS_FILES.xpcshell.comm
+else:
+ test_harness_base = TEST_HARNESS_FILES.xpcshell
diff --git a/comm/mailnews/jsaccount/test/idl/msgIFooUrl.idl b/comm/mailnews/jsaccount/test/idl/msgIFooUrl.idl
new file mode 100644
index 0000000000..9652a27cc5
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/idl/msgIFooUrl.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a sample test interface implemented by the URL object.
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(12CAD9FC-57FC-4AEE-A800-895A289237DD)]
+interface msgIFooUrl : nsISupports
+{
+ /// Foo id for item.
+ attribute AString itemId;
+ /// Does this url refer to an attachment?
+ readonly attribute boolean isAttachment;
+};
diff --git a/comm/mailnews/jsaccount/test/moz.build b/comm/mailnews/jsaccount/test/moz.build
new file mode 100644
index 0000000000..a3af38d313
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+TEST_DIRS += [
+ "idl",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
+
+TESTING_JS_MODULES.mailnews += [
+ "unit/resources/testJaBaseIncomingServer.jsm",
+ "unit/resources/testJaBaseMsgFolder.jsm",
+]
diff --git a/comm/mailnews/jsaccount/test/unit/head_jsaccount.js b/comm/mailnews/jsaccount/test/unit/head_jsaccount.js
new file mode 100644
index 0000000000..a3c37dea1f
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/head_jsaccount.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var CC = Components.Constructor;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+// Load the test components.
+let contracts = [
+ {
+ contractID: "@mozilla.org/jsaccount/testjafoourl;1",
+ classID: "{73F98539-A59F-4F6F-9A72-D83A08646C23}",
+ source: "resources/testJaFooUrlComponent.js",
+ },
+ {
+ contractID: "@mozilla.org/mail/folder-factory;1?name=testja",
+ classID: "{8508ddeb-3eab-4877-a420-297518f62371}",
+ source: "resources/testJaBaseMsgFolderComponent.js",
+ },
+ {
+ contractID: "@mozilla.org/messenger/server;1?type=testja",
+ classID: "{0eec03cd-da67-4949-ab2d-5fa4bdc68135}",
+ source: "resources/testJaBaseIncomingServerComponent.js",
+ },
+];
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+for (let { contractID, classID, source } of contracts) {
+ let scope = {};
+ Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file(source)).spec,
+ scope
+ );
+ registrar.registerFactory(
+ Components.ID(classID),
+ "",
+ contractID,
+ scope.xpcomFactory
+ );
+}
+
+// Ensure the profile directory is set up.
+do_get_profile();
+
+registerCleanupFunction(function () {
+ load("../../../../mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/jsaccount/test/unit/resources/TestJaMsgProtocolInfoComponent.jsm b/comm/mailnews/jsaccount/test/unit/resources/TestJaMsgProtocolInfoComponent.jsm
new file mode 100644
index 0000000000..555f395220
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/TestJaMsgProtocolInfoComponent.jsm
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is the component definition for a demo base implementation of a
+// javascript nsIMsgProtocolInfo implementation.
+
+var EXPORTED_SYMBOLS = ["TestJaMsgProtocolInfo"];
+
+function TestJaMsgProtocolInfo() {
+ dump("testJaMsgProtocolInfo");
+ // nsIFile object to be used for the default local path.
+ this._defaultLocalPath = null;
+}
+
+TestJaMsgProtocolInfo.prototype = {
+ // Flag this item as CPP needs to delegate to JS.
+ _JsPrototypeToDelegate: true,
+
+ get defaultLocalPath() {
+ if (this._defaultLocalPath) {
+ return this._defaultLocalPath;
+ }
+ // Setup a default location, "TestFoo" directory in profile.
+ const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+ let typedir = Services.dirsvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
+ typedir.append("TestFoo");
+ if (!typedir.exists()) {
+ typedir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
+ }
+ this._defaultLocalPath = typedir;
+ return typedir;
+ },
+ set defaultLocalPath(defaultLocalPath) {
+ this._defaultLocalPath = defaultLocalPath;
+ },
+ // serverIID is used in AccountWizard.js, if missing will just report an error.
+ get serverIID() {
+ return null;
+ },
+ get requiresUsername() {
+ return false;
+ },
+ get preflightPrettyNameWithEmailAddress() {
+ return false;
+ },
+ get canDelete() {
+ return true;
+ },
+ get canLoginAtStartUp() {
+ return false;
+ },
+ get canDuplicate() {
+ return false;
+ },
+ getDefaultServerPort: isSecure => 0,
+ get canGetMessages() {
+ return false;
+ },
+ get canGetIncomingMessages() {
+ return false;
+ },
+ get defaultDoBiff() {
+ return false;
+ },
+ get showComposeMsgLink() {
+ return false;
+ },
+ get foldersCreatedAsync() {
+ return false;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgProtocolInfo"]),
+};
diff --git a/comm/mailnews/jsaccount/test/unit/resources/readme.html b/comm/mailnews/jsaccount/test/unit/resources/readme.html
new file mode 100644
index 0000000000..e5490e7d41
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/readme.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>JsAccount Usage and Architecture</title>
+ </head>
+ <body>
+ <h2>Overview of Testing Objects </h2>
+ This directory contains sample JS components to test the basic JsAccount
+ concepts, which can also serve as templates for new implementations.<br>
+ <h3>Component Naming</h3>
+ Because there are many different components involved with different roles,
+ it will be helpful to keep things straight by using a specific naming
+ convention for objects.
+ For testing, we consider that we are creating a new account type "foo". For
+ the specific case of the JA implementation of an object that implements the
+ nsIMsgMailNewsUrl interface, we'll use the following naming convention.
+ Typically C++ classes and JS constructors are capitalized, class instances
+ are not.<br>
+ <br>
+ Names are constructed with the following subparts:<br>
+ <br>
+ (<strong>Ja)</strong>(<strong>AccountType</strong>)(<strong>Language</strong>)(<strong>ComponentType</strong>)(<strong>Role</strong>)
+ <br>
+ <h4>Common parts of names</h4>
+ <ul>
+ <li><strong>Ja</strong>: All objects or classes begin with Ja</li>
+ <li><strong>AccountType</strong>: the type of account being created (here
+ <strong>Foo</strong>), or <strong>Base</strong> for the generic
+ implementation that is the base class of all types. May be *blank* if
+ the object is used for both base types and specific types.</li>
+ <li>
+ <strong>Language</strong>: Use <strong>Cpp</strong> with objects and
+ classes implemented using C++, leave blank for JS versions.
+ </li>
+ <li>
+ <strong>ComponentType</strong>: the standard MailNews term for objects
+ that implement a particular interface. The legacy .cpp base components
+ are typically named:<br>
+ <strong>(nsMsg)(ComponentType</strong>).cpp<br>
+ for example <strong>nsMsgIncomingServer</strong>.cpp. This may be
+ shortened where appropriate, for example we use <strong>Url</strong>
+ instead of <strong>MailNewsUrl</strong> as the <strong>Ja</strong>
+ prefix implies that this is an implementation of MailNews objects.</li>
+ <li><strong>Role</strong>: the function of the object within the JA
+ architecture.</li>
+ <ul>
+ <li><strong>Constructor</strong>: Creates the object only. </li>
+ <li><strong>Delegator</strong>: Calls the appropriate object, either a
+ JS or C++ variant, that implements a particular XPCOM method.
+ </li>
+ <li> <strong>Properties</strong>: JavaScript object that establishes
+ properties of a JA implementation class. This is used for automatic
+ generation of a list of methods to delegate to the JavaScript classes.</li>
+ <li>(blank): Actual implementation.</li>
+ </ul>
+ </ul>
+ Example: The C++ class that delegates the implementation of
+ nsIMsgMailNewsUrl (abbreviated as Url) to either a C++ or JS method is
+ called <strong>JaCppUrlDelegator</strong>.
+ </body>
+</html>
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testComponents.manifest b/comm/mailnews/jsaccount/test/unit/resources/testComponents.manifest
new file mode 100644
index 0000000000..22bacb7262
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testComponents.manifest
@@ -0,0 +1,16 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+# Definitions of components used in testing of JsAccount
+
+# nsIMsgMailNewsUrl implementation of a demo base URL
+component {73F98539-A59F-4F6F-9A72-D83A08646C23} testJaFooUrlComponent.js
+contract @mozilla.org/jsaccount/testjafoourl;1 {73F98539-A59F-4F6F-9A72-D83A08646C23}
+
+# nsIMsgFolder implementation
+component {8508ddeb-3eab-4877-a420-297518f62371} testJaBaseMsgFolderComponent.js
+contract @mozilla.org/mail/folder-factory;1?name=testja {8508ddeb-3eab-4877-a420-297518f62371}
+
+# nsIMsgIncomingServer implementation
+component {0eec03cd-da67-4949-ab2d-5fa4bdc68135} testJaBaseIncomingServerComponent.js
+contract @mozilla.org/messenger/server;1?type=testja {0eec03cd-da67-4949-ab2d-5fa4bdc68135}
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServer.jsm b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServer.jsm
new file mode 100644
index 0000000000..602df26c30
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServer.jsm
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ This file creates a JS-based override of the JaIncomingServer implementation. It
+ demos a minimal JS class, and is also used in testing the additional methods
+ added to JaIncomingServer.cpp that are not in nsMsgDBFolder.cpp
+ */
+
+const EXPORTED_SYMBOLS = [
+ "JaBaseIncomingServerProperties",
+ "JaBaseIncomingServer",
+];
+
+// A partial JavaScript implementation of the base server methods.
+
+const JaBaseIncomingServerProperties = {
+ baseContractID: "@mozilla.org/jacppincomingserverdelegator;1",
+ baseInterfaces: [
+ Ci.nsISupports,
+ Ci.nsIMsgIncomingServer,
+ Ci.nsIInterfaceRequestor,
+ Ci.msgIOverride,
+ Ci.nsISupportsWeakReference,
+ ],
+ delegateInterfaces: ["nsIMsgIncomingServer"],
+ contractID: "@mozilla.org/messenger/server;1?type=testja",
+ classID: Components.ID("{0eec03cd-da67-4949-ab2d-5fa4bdc68135}"),
+};
+
+function JaBaseIncomingServer(aDelegator, aBaseInterfaces) {
+ dump("JaBaseIncomingServer\n");
+ // Typical boilerplate to include in all implementations.
+
+ // Object delegating method calls to the appropriate XPCOM object.
+ // Weak because it owns us.
+ this.delegator = Cu.getWeakReference(aDelegator);
+
+ // Base implementation of methods with no overrides.
+ this.cppBase = aDelegator.cppBase;
+
+ // cppBase class sees all interfaces
+ aBaseInterfaces.forEach(iface => this.cppBase instanceof iface);
+}
+
+JaBaseIncomingServer.prototype = {
+ // Typical boilerplate to include in all implementations.
+
+ // Flag this item as CPP needs to delegate to JS.
+ _JsPrototypeToDelegate: true,
+
+ // QI to the (partially implemented only) interfaces.
+ QueryInterface: ChromeUtils.generateQI(
+ JaBaseIncomingServerProperties.delegateInterfaces
+ ),
+
+ // Used to access an instance as JS, bypassing XPCOM.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Dynamically-generated list of delegate methods.
+ delegateList: null,
+
+ // nsIMsgIncomingServer overrides.
+ get localStoreType() {
+ return "testja";
+ },
+ get localDatabaseType() {
+ return "mailbox";
+ },
+};
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServerComponent.js b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServerComponent.js
new file mode 100644
index 0000000000..dc2e6682bb
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseIncomingServerComponent.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is the component definition for a demo base implementation of a
+// javascript IncomingServer.
+
+const { JSAccountUtils } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JSAccountUtils.jsm"
+);
+var { JaBaseIncomingServerProperties, JaBaseIncomingServer } =
+ ChromeUtils.import(
+ "resource://testing-common/mailnews/testJaBaseIncomingServer.jsm"
+ );
+
+var xpcomFactory = JSAccountUtils.jaFactory(
+ JaBaseIncomingServerProperties,
+ JaBaseIncomingServer
+);
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolder.jsm b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolder.jsm
new file mode 100644
index 0000000000..7f56bbdaef
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolder.jsm
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ This file creates a JS-based override of the JaMsgFolder implementation. It
+ demos a minimal JS class, and is also used in testing the additional methods
+ added to JaMsgFolder.cpp that are not in nsMsgDBFolder.cpp
+ */
+
+const EXPORTED_SYMBOLS = ["JaBaseMsgFolderProperties", "JaBaseMsgFolder"];
+
+// A partial JavaScript implementation of the base server methods.
+
+const JaBaseMsgFolderProperties = {
+ baseContractID: "@mozilla.org/jacppmsgfolderdelegator;1",
+ baseInterfaces: [
+ Ci.nsISupports,
+ Ci.nsIMsgFolder,
+ Ci.nsIDBChangeListener,
+ Ci.nsIUrlListener,
+ Ci.nsIJunkMailClassificationListener,
+ Ci.nsIMsgTraitClassificationListener,
+ Ci.nsIInterfaceRequestor,
+ Ci.msgIOverride,
+ ],
+ delegateInterfaces: ["nsIMsgFolder"],
+ contractID: "@mozilla.org/mail/folder-factory;1?name=testja",
+ classID: Components.ID("{8508ddeb-3eab-4877-a420-297518f62371}"),
+};
+
+function JaBaseMsgFolder(aDelegator, aBaseInterfaces) {
+ // Typical boilerplate to include in all implementations.
+
+ // Object delegating method calls to the appropriate XPCOM object.
+ // Weak because it owns us.
+ this.delegator = Cu.getWeakReference(aDelegator);
+
+ // Base implementation of methods with no overrides.
+ this.cppBase = aDelegator.cppBase;
+
+ // cppBase class sees all interfaces
+ aBaseInterfaces.forEach(iface => this.cppBase instanceof iface);
+}
+
+JaBaseMsgFolder.prototype = {
+ // Typical boilerplate to include in all implementations.
+
+ // Flag this item as CPP needs to delegate to JS.
+ _JsPrototypeToDelegate: true,
+
+ // QI to the (partially implemented only) interfaces.
+ QueryInterface: ChromeUtils.generateQI(
+ JaBaseMsgFolderProperties.delegateInterfaces
+ ),
+
+ // Used to access an instance as JS, bypassing XPCOM.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Dynamically-generated list of delegate methods.
+ delegateList: null,
+
+ // nsIMsgFolder overrides.
+ get incomingServerType() {
+ return "testja";
+ },
+};
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolderComponent.js b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolderComponent.js
new file mode 100644
index 0000000000..8f71f71b6c
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testJaBaseMsgFolderComponent.js
@@ -0,0 +1,19 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is the component definition for a demo base implementation of a
+// javascript msgFolder.
+
+const { JSAccountUtils } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JSAccountUtils.jsm"
+);
+var { JaBaseMsgFolderProperties, JaBaseMsgFolder } = ChromeUtils.import(
+ "resource://testing-common/mailnews/testJaBaseMsgFolder.jsm"
+);
+
+var xpcomFactory = JSAccountUtils.jaFactory(
+ JaBaseMsgFolderProperties,
+ JaBaseMsgFolder
+);
diff --git a/comm/mailnews/jsaccount/test/unit/resources/testJaFooUrlComponent.js b/comm/mailnews/jsaccount/test/unit/resources/testJaFooUrlComponent.js
new file mode 100644
index 0000000000..9ed4d4c15b
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/resources/testJaFooUrlComponent.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ One of the goals of JsAccount is to be able to incrementally extend a base
+ implementation, possibly adding a new interface. This code demonstrates
+ a mailnews URL extended for a hypthetical account type "foo".
+*/
+
+const { JSAccountUtils } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JSAccountUtils.jsm"
+);
+const { JaBaseUrl, JaBaseUrlProperties } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JaBaseUrl.jsm"
+);
+
+const ATTACHMENT_QUERY = "part=1.";
+
+var FooUrlProperties = {
+ // Extend the base properties.
+ __proto__: JaBaseUrlProperties,
+
+ contractID: "@mozilla.org/jsaccount/testjafoourl;1",
+ classID: Components.ID("{73F98539-A59F-4F6F-9A72-D83A08646C23}"),
+
+ // Add an additional interface only needed by this custom class.
+ extraInterfaces: [Ci.msgIFooUrl],
+};
+
+// Constructor
+var xpcomFactory = JSAccountUtils.jaFactory(FooUrlProperties, FooUrl);
+
+// Main class.
+function FooUrl(aDelegator, aBaseInterfaces) {
+ // Superclass constructor
+ JaBaseUrl.call(this, aDelegator, aBaseInterfaces);
+
+ // I'm not sure why I have to call this again, as it is called in the
+ // base constructor, but without it this method will not find the
+ // interfaces beyond nsISupports.
+ aBaseInterfaces.forEach(iface => this.cppBase instanceof iface);
+
+ // instance variables
+ this._urlType = -1; // unknown;
+ this._itemId = null;
+ this._hidden = "IAmHidden";
+}
+
+// Extend the base class methods.
+FooUrl.prototype = {
+ // Typical boilerplate to include in all implementations.
+
+ // Extended the JS URL object.
+ __proto__: JaBaseUrl.prototype,
+
+ // Delegate these methods to CPP.
+ _JsPrototypeToDelegate: true,
+
+ // InterfaceRequestor override, needed if extraInterfaces.
+
+ getInterface(iid) {
+ for (let iface of FooUrlProperties.extraInterfaces) {
+ if (iid.equals(iface)) {
+ return this;
+ }
+ }
+ return this.delegator.QueryInterface(iid);
+ },
+
+ // msgIFooUrl implementation
+
+ // Foo id for item.
+ // attribute AString itemId;
+ get itemId() {
+ return this._itemId;
+ },
+ set itemId(aVal) {
+ this._itemId = aVal;
+ },
+
+ // Does this url refer to an attachment?
+ // readonly attribute boolean isAttachment;
+ get isAttachment() {
+ // We look to see if the URL has an attachment query
+ let query = this.QueryInterface(Ci.nsIURL).query;
+ return query && query.includes(ATTACHMENT_QUERY);
+ },
+};
diff --git a/comm/mailnews/jsaccount/test/unit/test_componentsExist.js b/comm/mailnews/jsaccount/test/unit/test_componentsExist.js
new file mode 100644
index 0000000000..726281bccc
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/test_componentsExist.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the components made available by JaAccount can be created with
+// each supported interface.
+
+let tests = [
+ // JaUrl
+ ["@mozilla.org/jacppurldelegator;1", "nsISupports"],
+ ["@mozilla.org/jacppurldelegator;1", "nsIMsgMailNewsUrl"],
+ ["@mozilla.org/jacppurldelegator;1", "nsIMsgMessageUrl"],
+ ["@mozilla.org/jacppurldelegator;1", "nsIURL"],
+ ["@mozilla.org/jacppurldelegator;1", "nsIURI"],
+ ["@mozilla.org/jacppurldelegator;1", "msgIOverride"],
+ ["@mozilla.org/jacppurldelegator;1", "nsIInterfaceRequestor"],
+ // (probably a url bug) ["@mozilla.org/jacppurldelegator;1", "nsISupportsWeakReference"],
+
+ // FooJaUrl
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsISupports"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsIMsgMailNewsUrl"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsIMsgMessageUrl"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsIURL"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsIURI"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "msgIOverride"],
+ ["@mozilla.org/jsaccount/testjafoourl;1", "nsIInterfaceRequestor"],
+ // JaAbDirectory
+ ["@mozilla.org/jacppabdirectorydelegator;1", "nsISupports"],
+ ["@mozilla.org/jacppabdirectorydelegator;1", "nsIAbDirectory"],
+ ["@mozilla.org/jacppabdirectorydelegator;1", "msgIOverride"],
+ ["@mozilla.org/jacppabdirectorydelegator;1", "nsIInterfaceRequestor"],
+ ["@mozilla.org/jacppabdirectorydelegator;1", "nsISupportsWeakReference"],
+ // JaCompose
+ ["@mozilla.org/jacppcomposedelegator;1", "nsISupports"],
+ ["@mozilla.org/jacppcomposedelegator;1", "nsIMsgCompose"],
+ ["@mozilla.org/jacppcomposedelegator;1", "nsIMsgSendListener"],
+ ["@mozilla.org/jacppcomposedelegator;1", "msgIOverride"],
+ ["@mozilla.org/jacppcomposedelegator;1", "nsIInterfaceRequestor"],
+ ["@mozilla.org/jacppcomposedelegator;1", "nsISupportsWeakReference"],
+ // JaIncomingServer
+ ["@mozilla.org/jacppincomingserverdelegator;1", "nsISupports"],
+ ["@mozilla.org/jacppincomingserverdelegator;1", "nsIMsgIncomingServer"],
+ ["@mozilla.org/jacppincomingserverdelegator;1", "msgIOverride"],
+ ["@mozilla.org/jacppincomingserverdelegator;1", "nsIInterfaceRequestor"],
+ ["@mozilla.org/jacppincomingserverdelegator;1", "nsISupportsWeakReference"],
+ // JaMsgFolder
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "nsISupports"],
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "nsIMsgFolder"],
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "nsIDBChangeListener"],
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "nsIUrlListener"],
+ [
+ "@mozilla.org/jacppmsgfolderdelegator;1",
+ "nsIJunkMailClassificationListener",
+ ],
+ [
+ "@mozilla.org/jacppmsgfolderdelegator;1",
+ "nsIMsgTraitClassificationListener",
+ ],
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "msgIOverride"],
+ ["@mozilla.org/jacppmsgfolderdelegator;1", "nsISupportsWeakReference"],
+ // TestJaIncomingServer
+ ["@mozilla.org/messenger/server;1?type=testja", "nsISupports"],
+ ["@mozilla.org/messenger/server;1?type=testja", "nsIMsgIncomingServer"],
+ ["@mozilla.org/messenger/server;1?type=testja", "msgIOverride"],
+ ["@mozilla.org/messenger/server;1?type=testja", "nsISupportsWeakReference"],
+ // TestJaMsgProtocolInfo
+ ["@mozilla.org/messenger/protocol/info;1?type=testja", "nsISupports"],
+ ["@mozilla.org/messenger/protocol/info;1?type=testja", "nsIMsgProtocolInfo"],
+];
+
+function run_test() {
+ for (let [contractID, iface] of tests) {
+ dump(
+ "trying to create component " +
+ contractID +
+ " with interface " +
+ iface +
+ "\n"
+ );
+ try {
+ dump(Cc[contractID] + " " + Ci[iface] + "\n");
+ } catch (e) {
+ dump(e + "\n");
+ }
+
+ let comp = Cc[contractID].createInstance(Ci[iface]);
+ Assert.ok(comp instanceof Ci[iface]);
+ }
+}
diff --git a/comm/mailnews/jsaccount/test/unit/test_fooUrl.js b/comm/mailnews/jsaccount/test/unit/test_fooUrl.js
new file mode 100644
index 0000000000..8526ef2093
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/test_fooUrl.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests of override functionality using a demo "foo" type url.
+
+var { JaBaseUrlProperties } = ChromeUtils.import(
+ "resource:///modules/jsaccount/JaBaseUrl.jsm"
+);
+
+var extraInterfaces = [Ci.msgIFooUrl];
+
+function newURL() {
+ return Cc["@mozilla.org/jsaccount/testjafoourl;1"].createInstance(
+ Ci.nsISupports
+ );
+}
+
+var tests = [
+ function testExists() {
+ // test the existence of components and their interfaces.
+ let url = newURL();
+ for (let iface of JaBaseUrlProperties.baseInterfaces) {
+ Assert.ok(url instanceof iface);
+ let urlQI = url.QueryInterface(iface);
+ // Since the URL wasn't properly initialised, that is, it has no spec
+ // the following will crash. The underlying nsMsgMailNewsUrl
+ // has no m_baseURL yet and hence GetSpec() triggered by the
+ // Assert.uk(urlQI) will crash. So use this instead:
+ Assert.ok(urlQI != null);
+ }
+ for (let iface of extraInterfaces) {
+ let fooUrl = url.getInterface(iface);
+ Assert.ok(fooUrl instanceof iface);
+ Assert.ok(fooUrl.QueryInterface(iface) != null);
+ }
+ },
+ function test_msgIOverride() {
+ let url = newURL().QueryInterface(Ci.msgIOverride);
+
+ // test of access to wrapped JS object.
+
+ // Access the ._hidden attribute using the XPCOM interface,
+ // where it is not defined.
+ Assert.equal(typeof url.jsDelegate._hidden, "undefined");
+
+ // Get the JS object, where _hidden IS defined.
+ Assert.equal(url.jsDelegate.wrappedJSObject._hidden, "IAmHidden");
+ },
+
+ // We used to test nsIURI, nsIURL, and nsIMsgMailNewsUrl overrides, but those
+ // can no longer be overridden.
+ function test_nsIMsgMessageUrl() {
+ let url = newURL().QueryInterface(Ci.nsIMsgMessageUrl);
+ Assert.ok("originalSpec" in url);
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ Assert.ok(appDir.path);
+ // test attributes
+ url.messageFile = appDir;
+ Assert.equal(url.messageFile.path, appDir.path);
+ },
+ function test_msgIJaUrl() {
+ let url = newURL().QueryInterface(Ci.msgIJaUrl);
+ url.setUrlType(Ci.nsIMsgMailNewsUrl.eMove);
+ Assert.ok(
+ url
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .IsUrlType(Ci.nsIMsgMailNewsUrl.eMove)
+ );
+ },
+ function test_msgIFooUrl() {
+ let url = newURL().QueryInterface(Ci.nsIInterfaceRequestor);
+ let fooUrl = url.getInterface(Ci.msgIFooUrl);
+ Assert.ok(fooUrl instanceof Ci.msgIFooUrl);
+
+ fooUrl.itemId = "theItemId";
+ Assert.equal(fooUrl.itemId, "theItemId");
+
+ url.QueryInterface(Ci.msgIJaUrl).setSpec("https://foo.invalid/bar/");
+ Assert.ok(!fooUrl.isAttachment);
+ url
+ .QueryInterface(Ci.msgIJaUrl)
+ .setSpec("https://foo.invalid/bar?part=1.4&dummy=stuff");
+ Assert.ok(fooUrl.isAttachment);
+ },
+];
+
+function run_test() {
+ for (var test of tests) {
+ test();
+ }
+}
diff --git a/comm/mailnews/jsaccount/test/unit/test_jaMsgFolder.js b/comm/mailnews/jsaccount/test/unit/test_jaMsgFolder.js
new file mode 100644
index 0000000000..30a8557b01
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/test_jaMsgFolder.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests the additional methods added to JaMsgFolder.cpp that are not
+// in nsMsgDBFolder.cpp Although this code have been done creating the
+// delegator class directly, instead we use a JS component as a demo of
+// JS override classes.
+
+var { JaBaseMsgFolderProperties } = ChromeUtils.import(
+ "resource://testing-common/mailnews/testJaBaseMsgFolder.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ let server = MailServices.accounts.createIncomingServer(
+ "foouser",
+ "foohost",
+ "testja"
+ );
+ Assert.ok(server instanceof Ci.msgIOverride);
+
+ // If you create a folder object directly, it will complain about not being registered.
+ // Use folder-lookup-service instead.
+ let testJaMsgFolder = MailUtils.getOrCreateFolder(
+ "testja://foouser@foohost/somefolder"
+ );
+ // let testJaMsgFolder = Cc[JaBaseMsgFolderProperties.contractID]
+ // .createInstance(Ci.msgIOverride);
+ Assert.ok(testJaMsgFolder instanceof Ci.nsIMsgFolder);
+
+ JaBaseMsgFolderProperties.baseInterfaces.forEach(iface => {
+ dump("testing interface " + iface + "(" + Ci[iface] + ")\n");
+ testJaMsgFolder.QueryInterface(Ci[iface]);
+ });
+
+ let db = testJaMsgFolder.msgDatabase;
+ Assert.ok(db instanceof Ci.nsIMsgDatabase);
+
+ // Make sure the DB actually works.
+ let dbFolder = db.folder;
+ Assert.ok(dbFolder instanceof Ci.nsIMsgFolder);
+ Assert.equal(dbFolder.URI, "testja://foouser@foohost/somefolder");
+ let fi = db.dBFolderInfo;
+ Assert.ok(fi instanceof Ci.nsIDBFolderInfo);
+ fi.setCharProperty("testProperty", "foobar");
+ Assert.equal(fi.getCharProperty("testProperty"), "foobar");
+ db.forceClosed();
+ db = null;
+
+ // Confirm that we can access XPCOM properties.
+}
diff --git a/comm/mailnews/jsaccount/test/unit/xpcshell.ini b/comm/mailnews/jsaccount/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..69676b5d6f
--- /dev/null
+++ b/comm/mailnews/jsaccount/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+[DEFAULT]
+head = head_jsaccount.js
+tail =
+support-files = resources/*
+[test_componentsExist.js]
+[test_fooUrl.js]
+[test_jaMsgFolder.js]
diff --git a/comm/mailnews/local/public/moz.build b/comm/mailnews/local/public/moz.build
new file mode 100644
index 0000000000..962078a7af
--- /dev/null
+++ b/comm/mailnews/local/public/moz.build
@@ -0,0 +1,26 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsILocalMailIncomingServer.idl",
+ "nsIMailboxService.idl",
+ "nsIMailboxUrl.idl",
+ "nsIMsgLocalMailFolder.idl",
+ "nsIMsgParseMailMsgState.idl",
+ "nsINewsBlogFeedDownloader.idl",
+ "nsINoIncomingServer.idl",
+ "nsINoneService.idl",
+ "nsIPop3IncomingServer.idl",
+ "nsIPop3Protocol.idl",
+ "nsIPop3Service.idl",
+ "nsIPop3Sink.idl",
+ "nsIPop3URL.idl",
+ "nsIRssIncomingServer.idl",
+ "nsIRssService.idl",
+]
+
+XPIDL_MODULE = "msglocal"
+
+EXPORTS += []
diff --git a/comm/mailnews/local/public/nsILocalMailIncomingServer.idl b/comm/mailnews/local/public/nsILocalMailIncomingServer.idl
new file mode 100644
index 0000000000..11bd5543b6
--- /dev/null
+++ b/comm/mailnews/local/public/nsILocalMailIncomingServer.idl
@@ -0,0 +1,23 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIMsgWindow;
+interface nsIUrlListener;
+interface nsIMsgFolder;
+
+[scriptable, uuid(f465a3ee-5b29-4da6-8b2e-d764bcba468e)]
+interface nsILocalMailIncomingServer : nsISupports
+{
+ /// Create the necessary default folders that must always exist in an account (e.g. Inbox/Trash).
+ void createDefaultMailboxes();
+
+ /// Set special folder flags on the default folders.
+ void setFlagsOnDefaultMailboxes();
+
+ nsIURI getNewMail(in nsIMsgWindow aMsgWindow, in nsIUrlListener aUrlListener, in nsIMsgFolder aInbox);
+};
diff --git a/comm/mailnews/local/public/nsIMailboxService.idl b/comm/mailnews/local/public/nsIMailboxService.idl
new file mode 100644
index 0000000000..13d998844d
--- /dev/null
+++ b/comm/mailnews/local/public/nsIMailboxService.idl
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIUrlListener.idl"
+
+interface nsIURI;
+interface nsIStreamListener;
+interface nsIMsgWindow;
+interface nsIFile;
+
+[scriptable, uuid(809FCD02-B9EA-4DC0-84F0-3FBC55AE11F1)]
+interface nsIMailboxService : nsISupports {
+
+ /*
+ * All of these functions build mailbox urls and run them. If you want a
+ * handle on the running task, pass in a valid nsIURI ptr. You can later
+ * interrupt this action by asking the netlib service manager to interrupt
+ * the url you are given back. Remember to release aURL when you are done
+ * with it. Pass nullptr in for aURL if you don't care about the returned URL.
+ */
+
+ /*
+ * Pass in a file path for the mailbox you wish to parse. You also need to
+ * pass in a mailbox parser (the consumer). The url listener can be null
+ * if you have no interest in tracking the url.
+ */
+ nsIURI ParseMailbox(in nsIMsgWindow aMsgWindow, in nsIFile aMailboxPath,
+ in nsIStreamListener aMailboxParser,
+ in nsIUrlListener aUrlListener);
+
+};
diff --git a/comm/mailnews/local/public/nsIMailboxUrl.idl b/comm/mailnews/local/public/nsIMailboxUrl.idl
new file mode 100644
index 0000000000..85aefd7343
--- /dev/null
+++ b/comm/mailnews/local/public/nsIMailboxUrl.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIStreamListener;
+interface nsIMsgDBHdr;
+
+typedef long nsMailboxAction;
+
+[scriptable, uuid(2ac72280-90f4-4d80-8af1-5e7a1997e2a8)]
+interface nsIMailboxUrl : nsISupports {
+
+ // Mailbox urls which parse a mailbox folder require a consumer of the
+ // stream which will represent the mailbox. This consumer is the mailbox
+ // parser. As data from the mailbox folder is read in, the data will be
+ // written to a stream and the consumer will be notified through
+ // nsIStreamListenter::OnDataAvailable that the stream has data
+ // available...
+ // mscott: I wonder if the caller should be allowed to create and set
+ // the stream they want the data written to as well? Hmm....
+
+ attribute nsIStreamListener mailboxParser;
+
+ /////////////////////////////////////////////////////////////////////////
+ // Copy/Move mailbox urls require a mailbox copy handler which actually
+ // performs the copy.
+ /////////////////////////////////////////////////////////////////////////
+ attribute nsIStreamListener mailboxCopyHandler;
+
+ // Some mailbox urls include a message key for the message in question.
+ readonly attribute nsMsgKey messageKey;
+
+ // this is to support multiple msg move/copy in one url
+ void setMoveCopyMsgKeys(in Array<nsMsgKey> keysToFlag);
+ void getMoveCopyMsgHdrForIndex(in unsigned long msgIndex, out nsIMsgDBHdr msgHdr);
+ readonly attribute unsigned long numMoveCopyMsgs;
+ attribute unsigned long curMoveCopyMsgIndex;
+ // mailbox urls to fetch a mail message can specify the size of
+ // the message...
+ // this saves us the trouble of having to open up the msg db and ask
+ // ourselves...
+ attribute unsigned long messageSize;
+
+ attribute nsMailboxAction mailboxAction;
+
+ /* these are nsMailboxActions */
+ const long ActionParseMailbox = 0;
+ const long ActionFetchMessage = 1;
+ const long ActionCopyMessage = 2;
+ const long ActionMoveMessage = 3;
+ const long ActionSaveMessageToDisk = 4;
+ const long ActionAppendMessageToDisk = 5;
+ const long ActionFetchPart = 6;
+};
diff --git a/comm/mailnews/local/public/nsIMsgLocalMailFolder.idl b/comm/mailnews/local/public/nsIMsgLocalMailFolder.idl
new file mode 100644
index 0000000000..80c7da02f7
--- /dev/null
+++ b/comm/mailnews/local/public/nsIMsgLocalMailFolder.idl
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+interface nsIMsgWindow;
+interface nsIUrlListener;
+interface nsIMsgDatabase;
+interface nsIMsgDBHdr;
+interface nsIMsgFolder;
+interface nsIMsgCopyServiceListener;
+
+[ptr] native nsLocalFolderScanState(nsLocalFolderScanState);
+
+%{C++
+/* flags for markMsgsOnPop3Server */
+#define POP3_NONE 0
+#define POP3_DELETE 1
+#define POP3_FETCH_BODY 2
+#define POP3_FORCE_DEL 3
+
+struct nsLocalFolderScanState;
+%}
+
+[scriptable, uuid(ebf7576c-e15f-4aba-b021-cc6e9266e90c)]
+interface nsIMsgLocalMailFolder : nsISupports {
+ /**
+ * Set the default flags on the subfolders of this folder, such as
+ * Drafts, Templates, etc.
+ * @param flags bitwise OR matching the type of mailboxes you want to flag.
+ * This function will be smart and find the right names.
+ * E.g. nsMsgFolderFlags::Inbox | nsMsgFolderFlags::Drafts
+ */
+ void setFlagsOnDefaultMailboxes(in unsigned long flags);
+
+ /*
+ * This will return null if the db is out of date
+ */
+ nsIMsgDatabase getDatabaseWOReparse();
+
+ /*
+ * If the db is out of date, this will return NS_ERROR_NOT_INITIALIZED
+ * and kick off an async url to reparse the messages.
+ * If aReparseUrlListener is null, folder will use itself as the listener.
+ */
+ nsIMsgDatabase getDatabaseWithReparse(in nsIUrlListener aReparseUrlListener, in nsIMsgWindow aMsgWindow);
+ void parseFolder(in nsIMsgWindow aMsgWindow, in nsIUrlListener listener);
+ void copyFolderLocal(in nsIMsgFolder srcFolder, in boolean isMove, in nsIMsgWindow msgWindow, in nsIMsgCopyServiceListener listener);
+
+ /**
+ * Does copy of same level subfolders of the srcFolder to the destination
+ * (this) local folder. If isMove is true, the messages in the subfolders are
+ * deleted (or marked deleted if source is imap) after the copy completes; so
+ * effectively, the folders are copied and only the messages are moved.
+ *
+ * @param srcFolder The folder one level above subfolders being copied
+ * @param msgWindow Window for notification callbacks, can be null
+ * @param listener Listener which receive operation notifications, can be null
+ * @param isMove If true, after copy completes, delete the source messages
+ */
+ void copyAllSubFolders(in nsIMsgFolder srcFolder, in nsIMsgWindow msgWindow,
+ in nsIMsgCopyServiceListener listener, in boolean isMove);
+
+ void onCopyCompleted(in nsISupports aSrcSupport, in boolean aMoveCopySucceeded);
+ attribute boolean checkForNewMessagesAfterParsing;
+ void markMsgsOnPop3Server(in Array<nsIMsgDBHdr> aMessages, in int32_t aMark);
+
+ /**
+ * File size on disk has possibly changed - update and notify.
+ */
+ void refreshSizeOnDisk();
+
+ /**
+ * Creates a subfolder to the current folder with the passed in folder name.
+ * @param aFolderName name of the folder to create.
+ * @return newly created folder.
+ */
+ nsIMsgFolder createLocalSubfolder(in AString aFolderName);
+
+ /**
+ * Adds a message to the end of the folder, parsing it as it goes, and
+ * applying filters, if applicable.
+ * @param aMessage string containing the entire body of the message to add
+ * @return the nsIMsgDBHdr of the added message
+ */
+ nsIMsgDBHdr addMessage(in string aMessage);
+
+ /**
+ * Add one or more messages to the end of the folder in a single batch. Each
+ * batch requires an fsync() on the mailbox file so it is a good idea to
+ * try and minimize the number of calls you make to this method or addMessage.
+ *
+ * Filters are applied, if applicable.
+ *
+ * @param aMessageCount The number of messages.
+ * @param aMessages An array of pointers to strings containing entire message
+ * bodies.
+ * @return an array of nsIMsgDBHdr of the added messages
+ */
+ Array<nsIMsgDBHdr> addMessageBatch(in Array<ACString> aMessages);
+
+ /**
+ * Functions for updating the UI while running downloadMessagesForOffline:
+ * delete the old message before adding its newly downloaded body, and
+ * select the new message after it has replaced the old one
+ */
+ void deleteDownloadMsg(in nsIMsgDBHdr aMsgHdr);
+ void notifyDelete();
+
+ /**
+ * Functions for grubbing through a folder to find the Uidl for a
+ * given msgDBHdr.
+ */
+ [noscript] void getFolderScanState(in nsLocalFolderScanState aState);
+ [noscript] void getUidlFromFolder(in nsLocalFolderScanState aState, in nsIMsgDBHdr aMsgHdr);
+
+
+ /**
+ * Shows warning if there is not enough space in the message store
+ * for a message of the given size.
+ */
+ boolean warnIfLocalFileTooBig(in nsIMsgWindow aWindow,
+ [optional] in long long aSpaceRequested);
+
+ /**
+ * Update properties on a new header from an old header, for cases where
+ * a partial message will be replaced with a full message.
+ *
+ * @param aOldHdr message header used as properties source
+ * @param aNewHdr message header used as properties destination
+ */
+ void updateNewMsgHdr(in nsIMsgDBHdr aOldHdr, in nsIMsgDBHdr aNewHdr);
+};
diff --git a/comm/mailnews/local/public/nsIMsgParseMailMsgState.idl b/comm/mailnews/local/public/nsIMsgParseMailMsgState.idl
new file mode 100644
index 0000000000..77c92b72df
--- /dev/null
+++ b/comm/mailnews/local/public/nsIMsgParseMailMsgState.idl
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl" // for nsMsgKey typedef
+
+interface nsIMsgDatabase;
+interface nsIMsgDBHdr;
+
+typedef long nsMailboxParseState;
+
+[scriptable, uuid(0d44646c-0759-43a2-954d-dc2a9a9660ec)]
+interface nsIMsgParseMailMsgState : nsISupports {
+ void SetMailDB(in nsIMsgDatabase aDatabase);
+ /*
+ * Set a backup mail database, whose data will be read during parsing to
+ * attempt to recover message metadata
+ *
+ * @param aDatabase the backup database
+ */
+ void SetBackupMailDB(in nsIMsgDatabase aDatabase);
+ void Clear();
+
+ void ParseAFolderLine(in string line, in unsigned long lineLength);
+ /// db header for message we're currently parsing
+ attribute nsIMsgDBHdr newMsgHdr;
+ void FinishHeader();
+
+ long GetAllHeaders(out string headers);
+ readonly attribute string headers;
+ attribute nsMailboxParseState state;
+ /* these are nsMailboxParseState */
+ const long ParseEnvelopeState=0;
+ const long ParseHeadersState=1;
+ const long ParseBodyState=2;
+
+ /**
+ * Set the key to be used for the new message header.
+ *
+ * @param aNewKey the new db key
+ *
+ */
+ void setNewKey(in nsMsgKey aKey);
+};
diff --git a/comm/mailnews/local/public/nsINewsBlogFeedDownloader.idl b/comm/mailnews/local/public/nsINewsBlogFeedDownloader.idl
new file mode 100644
index 0000000000..c87320fb47
--- /dev/null
+++ b/comm/mailnews/local/public/nsINewsBlogFeedDownloader.idl
@@ -0,0 +1,32 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIUrlListener;
+interface nsIMsgWindow;
+
+[scriptable, uuid(86e5bd0e-c324-11e3-923a-00269e4fddc1)]
+interface nsINewsBlogFeedDownloader : nsISupports
+{
+ void downloadFeed(in nsIMsgFolder aFolder,
+ in nsIUrlListener aUrlListener,
+ in bool aIsBiff,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Called when the RSS Incoming Server detects a change to an RSS folder name,
+ * such as delete (move to trash), move/copy, or rename. We then need to update
+ * the feeds.rdf subscriptions data source.
+ *
+ * @param nsIMsgFolder aFolder - the folder, new if rename or target of
+ * move/copy folder (new parent)
+ * @param nsIMsgFolder aOrigFolder - original folder
+ * @param string aAction - "move" or "copy" or "rename"
+ */
+ void updateSubscriptionsDS(in nsIMsgFolder aFolder,
+ in nsIMsgFolder aOrigFolder,
+ in string aAction);
+};
diff --git a/comm/mailnews/local/public/nsINoIncomingServer.idl b/comm/mailnews/local/public/nsINoIncomingServer.idl
new file mode 100644
index 0000000000..717689f58b
--- /dev/null
+++ b/comm/mailnews/local/public/nsINoIncomingServer.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(dacd4917-ddbe-47bb-8a49-6a8d91c49a86)]
+interface nsINoIncomingServer : nsISupports {
+ /**
+ * Copy the default messages (e.g. Templates) from
+ * bin/defaults/messenger/<folderNameOnDisk> to <rootFolder>/<folderNameOnDisk>.
+ * This is useful when first creating the standard folders (like Templates).
+ */
+ void copyDefaultMessages(in string folderNameOnDisk);
+};
diff --git a/comm/mailnews/local/public/nsINoneService.idl b/comm/mailnews/local/public/nsINoneService.idl
new file mode 100644
index 0000000000..045a018fff
--- /dev/null
+++ b/comm/mailnews/local/public/nsINoneService.idl
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(14714890-1dd2-11b2-87de-d265839520d6)]
+interface nsINoneService : nsISupports {
+ /* nothing yet, but soon. */
+};
diff --git a/comm/mailnews/local/public/nsIPop3IncomingServer.idl b/comm/mailnews/local/public/nsIPop3IncomingServer.idl
new file mode 100644
index 0000000000..b1433f5676
--- /dev/null
+++ b/comm/mailnews/local/public/nsIPop3IncomingServer.idl
@@ -0,0 +1,33 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIPop3Protocol;
+interface nsIMsgFolder;
+interface nsIUrlListener;
+interface nsIMsgWindow;
+
+[scriptable, uuid(8494584a-49b7-49df-9001-80ccdd0b50aa)]
+interface nsIPop3IncomingServer : nsISupports {
+ attribute boolean leaveMessagesOnServer;
+ attribute boolean headersOnly;
+ attribute boolean deleteMailLeftOnServer;
+ attribute unsigned long pop3CapabilityFlags;
+ attribute boolean deleteByAgeFromServer;
+ attribute long numDaysToLeaveOnServer;
+ // client adds uidls to mark one by one, then calls markMessages
+ void addUidlToMark(in string aUidl, in int32_t newStatus);
+ // TODO: make this async.
+ void markMessages();
+ /* account to which this server defers storage, for global inbox */
+ attribute ACString deferredToAccount;
+ // whether get new mail in deferredToAccount gets
+ // new mail with this server.
+ attribute boolean deferGetNewMail;
+ void downloadMailFromServers(
+ in Array<nsIPop3IncomingServer> aServers, in nsIMsgWindow aMsgWindow,
+ in nsIMsgFolder aFolder, in nsIUrlListener aListener);
+};
diff --git a/comm/mailnews/local/public/nsIPop3Protocol.idl b/comm/mailnews/local/public/nsIPop3Protocol.idl
new file mode 100644
index 0000000000..8263f94a55
--- /dev/null
+++ b/comm/mailnews/local/public/nsIPop3Protocol.idl
@@ -0,0 +1,23 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[ptr] native Pop3UidlEntryArrayRef(nsTArray<Pop3UidlEntry*>);
+
+%{C++
+#include "nsTArray.h"
+struct Pop3UidlEntry;
+%}
+
+[scriptable, uuid(3aff0550-87de-4337-9bc1-c84eb5462afe)]
+interface nsIPop3Protocol : nsISupports {
+ /* aUidl is an array of pointers to Pop3UidlEntry's. That structure is
+ * currently defined in nsPop3Protocol.h, perhaps it should be here
+ * instead...
+ */
+ [noscript] void markMessages(in Pop3UidlEntryArrayRef aUidl);
+ boolean checkMessage(in string aUidl);
+};
diff --git a/comm/mailnews/local/public/nsIPop3Service.idl b/comm/mailnews/local/public/nsIPop3Service.idl
new file mode 100644
index 0000000000..4c3e9a28ee
--- /dev/null
+++ b/comm/mailnews/local/public/nsIPop3Service.idl
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIUrlListener.idl"
+#include "nsIPop3IncomingServer.idl"
+#include "nsIMsgFolder.idl"
+
+interface nsIURI;
+interface nsIMsgWindow;
+interface nsIMsgFolder;
+
+[scriptable, uuid(7302fd8e-946f-4ae3-9468-0fb3a7706c51)]
+interface nsIPop3ServiceListener : nsISupports {
+ /**
+ * Notification that a pop3 download has started.
+ *
+ * @param aFolder folder in which the download is started.
+ */
+ void onDownloadStarted(in nsIMsgFolder aFolder);
+
+ /**
+ * Notification about download progress.
+ *
+ * @param aFolder folder in which the download is happening.
+ * @param aNumDownloaded number of the messages that have been downloaded.
+ * @param aTotalToDownload total number of messages to download.
+ */
+ void onDownloadProgress(in nsIMsgFolder aFolder,
+ in unsigned long aNumDownloaded,
+ in unsigned long aTotalToDownload);
+
+ /**
+ * Notification that a download has completed.
+ *
+ * @param aFolder folder to which the download has completed.
+ * @param aNumberOfMessages number of the messages that were downloaded.
+ */
+ void onDownloadCompleted(in nsIMsgFolder aFolder,
+ in unsigned long aNumberOfMessages);
+};
+
+/*
+ * The Pop3 Service is an interface designed to make building and running
+ * pop3 urls easier.
+ */
+[scriptable, uuid(96d3cc14-a842-4cdf-98f8-a4cc695f8b3b)]
+interface nsIPop3Service : nsISupports {
+ /*
+ * All of these functions build pop3 urls and run them. If you want
+ * a handle on the running task, pass in a valid nsIURI ptr. You can later
+ * interrupt this action by asking the netlib service manager to interrupt
+ * the url you are given back. Remember to release aURL when you are
+ * done with it. Pass nullptr in for aURL if you
+ * don't care about the returned URL.
+ */
+
+ /*
+ * right now getting new mail doesn't require any user specific data.
+ * We use the default current identity for this information. I suspect that
+ * we'll eventually pass in an identity to this call so you can get
+ * mail on different pop3 accounts....
+ */
+
+ nsIURI GetNewMail(in nsIMsgWindow aMsgWindow, in nsIUrlListener aUrlListener,
+ in nsIMsgFolder aInbox, in nsIPop3IncomingServer popServer);
+
+ nsIURI CheckForNewMail(in nsIMsgWindow aMsgWindow, in nsIUrlListener aUrlListener,
+ in nsIMsgFolder inbox, in nsIPop3IncomingServer popServer);
+
+ /**
+ * Verify that we can logon
+ *
+ * @param aServer - pop3 server we're logging on to.
+ * @param aUrlListener - gets called back with success or failure.
+ * @param aMsgWindow - nsIMsgWindow to use for notification callbacks.
+ * @return - the url that we run.
+ *
+ */
+ nsIURI verifyLogon(in nsIMsgIncomingServer aServer,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Add a listener for pop3 events like message download. This is
+ * used by the activity manager.
+ *
+ * @param aListener listener that gets notified of pop3 events.
+ */
+ void addListener(in nsIPop3ServiceListener aListener);
+
+ /**
+ * Remove a listener for pop3 events like message download.
+ *
+ * @param aListener listener to remove.
+ */
+ void removeListener(in nsIPop3ServiceListener aListener);
+
+ /**
+ * Send the notification that a pop3 download has started.
+ * This is called from the nsIPop3Sink code.
+ *
+ * @param aFolder folder in which the download is started.
+ */
+ void notifyDownloadStarted(in nsIMsgFolder aFolder);
+
+ /**
+ * Send notification about download progress.
+ *
+ * @param aFolder folder in which the download is happening.
+ * @param aNumDownloaded number of the messages that have been downloaded.
+ * @param aTotalToDownload total number of messages to download.
+ */
+ void notifyDownloadProgress(in nsIMsgFolder aFolder,
+ in unsigned long aNumDownloaded,
+ in unsigned long aTotalToDownload);
+ /**
+ * Send the notification that a download has completed.
+ * This is called from the nsIPop3Sink code.
+ *
+ * @param aFolder folder to which the download has completed.
+ * @param aNumberOfMessages number of the messages that were downloaded.
+ */
+ void notifyDownloadCompleted(in nsIMsgFolder aFolder,
+ in unsigned long aNumberOfMessages);
+};
diff --git a/comm/mailnews/local/public/nsIPop3Sink.idl b/comm/mailnews/local/public/nsIPop3Sink.idl
new file mode 100644
index 0000000000..0291329a2c
--- /dev/null
+++ b/comm/mailnews/local/public/nsIPop3Sink.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIPop3IncomingServer.idl"
+#include "nsIMsgFolder.idl"
+
+interface nsIURI;
+
+[scriptable, uuid(ceabfc6b-f139-4c25-890f-efb7c3069d40)]
+interface nsIPop3Sink : nsISupports {
+
+ attribute boolean buildMessageUri;
+ attribute AUTF8String messageUri;
+ attribute AUTF8String baseMessageUri;
+
+ /// message uri for header-only message version
+ attribute AUTF8String origMessageUri;
+
+ boolean beginMailDelivery(in boolean uidlDownload, in nsIMsgWindow msgWindow);
+ void endMailDelivery(in nsIPop3Protocol protocol);
+ void abortMailDelivery(in nsIPop3Protocol protocol);
+
+ void incorporateBegin(in string uidlString, in unsigned long flags);
+ void incorporateWrite(in string block, in long length);
+ void incorporateComplete(in nsIMsgWindow aMsgWindow, in int32_t aSize);
+ void incorporateAbort(in boolean uidlDownload);
+
+ /**
+ * Tell the pop3sink how many messages we're going to download.
+ *
+ * @param aNumMessages how many messages we're going to download.
+ */
+ void setMsgsToDownload(in unsigned long aNumMessages);
+
+ void setBiffStateAndUpdateFE(in unsigned long biffState, in long numNewMessages, in boolean notify);
+
+ attribute nsIPop3IncomingServer popServer;
+ attribute nsIMsgFolder folder;
+};
diff --git a/comm/mailnews/local/public/nsIPop3URL.idl b/comm/mailnews/local/public/nsIPop3URL.idl
new file mode 100644
index 0000000000..3fcd66e1a3
--- /dev/null
+++ b/comm/mailnews/local/public/nsIPop3URL.idl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIPop3Sink.idl"
+
+[scriptable, uuid(5fb87ae7-a3a0-440a-8b49-6bca42fb7ff2)]
+interface nsIPop3URL : nsISupports {
+ attribute nsIPop3Sink pop3Sink;
+ attribute AUTF8String messageUri;
+
+ /// Constant for the default POP3 port number
+ const int32_t DEFAULT_POP3_PORT = 110;
+
+ /// Constant for the default POP3 over ssl port number
+ const int32_t DEFAULT_POP3S_PORT = 995;
+};
diff --git a/comm/mailnews/local/public/nsIRssIncomingServer.idl b/comm/mailnews/local/public/nsIRssIncomingServer.idl
new file mode 100644
index 0000000000..aa5373126b
--- /dev/null
+++ b/comm/mailnews/local/public/nsIRssIncomingServer.idl
@@ -0,0 +1,16 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(6d744e7f-2218-45c6-8734-998a56cb3c6d)]
+interface nsIRssIncomingServer : nsISupports {
+ // Path to the subscriptions file for this RSS server.
+ readonly attribute nsIFile subscriptionsPath;
+
+ // Path to the feed items file for this RSS server.
+ readonly attribute nsIFile feedItemsPath;
+};
diff --git a/comm/mailnews/local/public/nsIRssService.idl b/comm/mailnews/local/public/nsIRssService.idl
new file mode 100644
index 0000000000..8f5e94af55
--- /dev/null
+++ b/comm/mailnews/local/public/nsIRssService.idl
@@ -0,0 +1,10 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(4b31bdd9-6f4c-46d4-a766-a94f11b599bc)]
+interface nsIRssService : nsISupports
+{
+};
diff --git a/comm/mailnews/local/src/Pop3Channel.jsm b/comm/mailnews/local/src/Pop3Channel.jsm
new file mode 100644
index 0000000000..4fae72894b
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3Channel.jsm
@@ -0,0 +1,105 @@
+/* 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 = ["Pop3Channel"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ Pop3Client: "resource:///modules/Pop3Client.jsm",
+});
+
+/**
+ * A channel to interact with POP3 server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ */
+class Pop3Channel {
+ QueryInterface = ChromeUtils.generateQI(["nsIChannel", "nsIRequest"]);
+
+ _logger = console.createInstance({
+ prefix: "mailnews.pop3",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.pop3.loglevel",
+ });
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} loadInfo - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ this._server = MailServices.accounts
+ .findServerByURI(uri)
+ .QueryInterface(Ci.nsIPop3IncomingServer);
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.URI = uri;
+ this.loadInfo = loadInfo;
+ this.contentLength = 0;
+ }
+
+ /**
+ * @see nsIRequest
+ */
+ get status() {
+ return Cr.NS_OK;
+ }
+
+ /**
+ * @see nsIChannel
+ */
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ "Pop3Channel.open() not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug(`asyncOpen ${this.URI.spec}`);
+ let match = this.URI.spec.match(/pop3?:\/\/.+\/(?:\?|&)uidl=([^&]+)/);
+ let uidl = decodeURIComponent(match?.[1] || "");
+ if (!uidl) {
+ throw Components.Exception(
+ `Unrecognized url=${this.URI.spec}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ let client = new lazy.Pop3Client(this._server);
+ client.runningUri = this.URI;
+ client.connect();
+ client.onOpen = () => {
+ listener.onStartRequest(this);
+ client.fetchBodyForUidl(
+ this.URI.QueryInterface(Ci.nsIPop3URL).pop3Sink,
+ uidl
+ );
+ };
+ client.onDone = status => {
+ listener.onStopRequest(this, status);
+ };
+ }
+}
diff --git a/comm/mailnews/local/src/Pop3Client.jsm b/comm/mailnews/local/src/Pop3Client.jsm
new file mode 100644
index 0000000000..59e21a66f7
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3Client.jsm
@@ -0,0 +1,1570 @@
+/* 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 = ["Pop3Client"];
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+var { LineReader } = ChromeUtils.import("resource:///modules/LineReader.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+var { Pop3Authenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+);
+
+/**
+ * A structure to represent a response received from the server. A response can
+ * be a single status line of a multi-line data block.
+ *
+ * @typedef {object} Pop3Response
+ * @property {boolean} success - True for a positive status indicator ("+OK","+").
+ * @property {string} status - The status indicator, can be "+OK", "-ERR" or "+".
+ * @property {string} statusText - The status line of the response excluding the
+ * status indicator.
+ * @property {string} data - The part of a multi-line data block excluding the
+ * status line.
+ *
+ * A single char to represent a uidl status, possible values are:
+ * - 'k'=KEEP,
+ * - 'd'=DELETE
+ * - 'b'=TOO_BIG
+ * - 'f'=FETCH_BODY
+ * @typedef {string} UidlStatus
+ */
+
+const UIDL_KEEP = "k";
+const UIDL_DELETE = "d";
+const UIDL_TOO_BIG = "b";
+const UIDL_FETCH_BODY = "f";
+
+// There can be multiple Pop3Client running concurrently, assign each logger a
+// unique prefix.
+let loggerId = 0;
+
+function getLoggerId() {
+ return loggerId++ % 1000;
+}
+
+/**
+ * A class to interact with POP3 server.
+ */
+class Pop3Client {
+ /**
+ * @param {nsIPop3IncomingServer} server - The associated server instance.
+ */
+ constructor(server) {
+ this._server = server.QueryInterface(Ci.nsIMsgIncomingServer);
+ this._server.wrappedJSObject.runningClient = this;
+ this._authenticator = new Pop3Authenticator(server);
+ this._lineReader = new LineReader();
+
+ // Somehow, Services.io.newURI("pop3://localhost") doesn't work, what we
+ // need is just a valid nsIMsgMailNewsUrl to propagate OnStopRunningUrl and
+ // secInfo.
+ this.runningUri = Services.io
+ .newURI(`smtp://${this._server.hostName}:${this._server.port}`)
+ .mutate()
+ .setScheme("pop3")
+ .finalize()
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+
+ // A list of auth methods detected from the EHLO response.
+ this._supportedAuthMethods = [];
+ // A list of auth methods that worth a try.
+ this._possibleAuthMethods = [];
+ // Auth method set by user preference.
+ this._preferredAuthMethods =
+ {
+ [Ci.nsMsgAuthMethod.passwordCleartext]: ["USERPASS", "PLAIN", "LOGIN"],
+ [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"],
+ [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"],
+ [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"],
+ [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"],
+ [Ci.nsMsgAuthMethod.secure]: ["CRAM-MD5", "GSSAPI"],
+ }[server.authMethod] || [];
+ // The next auth method to try if the current failed.
+ this._nextAuthMethod = null;
+
+ this._sink = Cc["@mozilla.org/messenger/pop3-sink;1"].createInstance(
+ Ci.nsIPop3Sink
+ );
+ this._sink.popServer = server;
+
+ this._logger = console.createInstance({
+ prefix: `mailnews.pop3.${getLoggerId()}`,
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.pop3.loglevel",
+ });
+
+ this.onReady = () => {};
+
+ this._cutOffTimestamp = -1;
+ if (
+ this._server.deleteByAgeFromServer &&
+ this._server.numDaysToLeaveOnServer
+ ) {
+ // We will send DELE request for messages received before this timestamp.
+ this._cutOffTimestamp =
+ Date.now() / 1000 - this._server.numDaysToLeaveOnServer * 24 * 60 * 60;
+ }
+
+ this._maxMessageSize = Infinity;
+ if (this._server.limitOfflineMessageSize) {
+ this._maxMessageSize = this._server.maxMessageSize
+ ? this._server.maxMessageSize * 1024
+ : 50 * 1024;
+ }
+
+ this._messagesToHandle = [];
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ let hostname = this._server.hostName.toLowerCase();
+ this._logger.debug(`Connecting to pop://${hostname}:${this._server.port}`);
+ this.runningUri
+ .QueryInterface(Ci.nsIMsgMailNewsUrl)
+ .SetUrlState(true, Cr.NS_OK);
+ this._server.serverBusy = true;
+ this._secureTransport = this._server.socketType == Ci.nsMsgSocketType.SSL;
+ this._socket = new TCPSocket(hostname, this._server.port, {
+ binaryType: "arraybuffer",
+ useSecureTransport: this._secureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+
+ this._authenticating = false;
+ // Indicates if the connection has been closed and can't be used anymore.
+ this._destroyed = false;
+ // Save the incomplete server payload, start parsing after seeing \r\n.
+ this._pendingPayload = "";
+ }
+
+ /**
+ * Check and fetch new mails.
+ *
+ * @param {boolean} downloadMail - Whether to download mails using TOP/RETR.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIMsgFolder} folder - The folder to save the messages to.
+ */
+ async getMail(downloadMail, msgWindow, folder) {
+ this._downloadMail = downloadMail;
+ this._msgWindow = msgWindow;
+ this._sink.folder = folder;
+ this._actionAfterAuth = this._actionStat;
+ this.urlListener.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+
+ await this._loadUidlState();
+ this._actionCapa();
+ }
+
+ /**
+ * Verify that we can logon to the server. Exit after auth success/failure.
+ *
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ */
+ verifyLogon(msgWindow) {
+ this._msgWindow = msgWindow;
+ this._verifyLogon = true;
+ this._actionAfterAuth = this._actionDone;
+ this._actionCapa();
+ }
+
+ /**
+ * Fetch the full message of a uidl.
+ *
+ * @param {nsIPop3Sink} sink - The sink to use for this request.
+ * @param {string} uidl - The uidl of the message to fetch.
+ */
+ async fetchBodyForUidl(sink, uidl) {
+ this._logger.debug(`Fetching body for uidl=${uidl}`);
+
+ this._downloadMail = true;
+ this._sink = sink;
+ this._sink.buildMessageUri = true;
+ this.urlListener = sink.folder.QueryInterface(Ci.nsIUrlListener);
+ this.urlListener.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+
+ await this._loadUidlState();
+
+ let uidlState = this._uidlMap.get(uidl);
+ if (!uidlState) {
+ // This uidl is no longer on the server, use this._sink to delete the
+ // msgHdr.
+ try {
+ this._sink.beginMailDelivery(true, null);
+ this._folderLocked = true;
+ this._logger.debug(
+ `Folder lock acquired uri=${this._sink.folder.URI}.`
+ );
+ this._sink.incorporateBegin(uidl, 0);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ }
+ return;
+ }
+ if (uidlState.status != UIDL_TOO_BIG) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+
+ this._singleUidlToDownload = uidl;
+ this._uidlMap.set(uidl, {
+ ...uidlState,
+ status: UIDL_FETCH_BODY,
+ });
+ this._actionAfterAuth = this._actionStat;
+ this._actionCapa();
+ }
+
+ /**
+ * Mark uidl status by a passed in Map, then write to popstate.dat.
+ *
+ * @param {Map<string, UidlStatus>} uidlsToMark - A Map from uidl to status.
+ */
+ async markMessages(uidlsToMark) {
+ this._logger.debug("markMessages", uidlsToMark);
+ if (!this._uidlMap) {
+ this._loadUidlState();
+ }
+ // Callers of nsIPop3IncomingServer.markMessages (e.g. filters) expect it to
+ // act as a sync function, otherwise, the flags set by filters may not take
+ // effect.
+ Services.tm.spinEventLoopUntil(
+ "nsIPop3IncomingServer.markMessages is a synchronous function",
+ () => {
+ return this._uidlMap;
+ }
+ );
+ for (let [uidl, status] of uidlsToMark) {
+ let uidlState = this._uidlMap.get(uidl);
+ this._uidlMap.set(uidl, {
+ ...uidlState,
+ status,
+ });
+ this._uidlMapChanged = true;
+ }
+ await this._writeUidlState(true);
+ }
+
+ /**
+ * Send `QUIT` request to the server.
+ * @param {Function} nextAction - Callback function after QUIT response.
+ */
+ quit(nextAction) {
+ this._onData = () => {};
+ this._onError = () => {};
+ if (this._socket?.readyState == "open") {
+ this._send("QUIT");
+ this._nextAction = nextAction || this.close;
+ } else if (nextAction) {
+ nextAction();
+ }
+ }
+
+ /**
+ * Close the socket.
+ */
+ close = () => {
+ this._socket.close();
+ };
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this._nextAction = res => {
+ // See if there is an APOP timestamp.
+ // eslint-disable-next-line no-control-regex
+ let matches = res.statusText.match(/<[\x00-\x7F]+@[\x00-\x7F]+>/);
+ if (matches?.[0]) {
+ this._apopTimestamp = matches[0];
+ }
+ this.onOpen();
+ };
+ this._socket.transport.setTimeout(
+ Ci.nsISocketTransport.TIMEOUT_READ_WRITE,
+ Services.prefs.getIntPref("mailnews.tcptimeout")
+ );
+ };
+
+ /**
+ * Parse the server response.
+ *
+ * @param {string} str - Response received from the server.
+ * @returns {Pop3Response}
+ */
+ _parse(str) {
+ if (this._lineReader.processingMultiLineResponse) {
+ // When processing multi-line response, no parsing should happen. If
+ // `+something` is treated as status line, _actionRetrResponse will treat
+ // it as a new message.
+ return { data: str };
+ }
+ let matches = /^(\+OK|-ERR|\+) ?(.*)\r\n([^]*)/.exec(str);
+ if (matches) {
+ let [, status, statusText, data] = matches;
+ return { success: status != "-ERR", status, statusText, data };
+ }
+ return { data: str };
+ }
+
+ /**
+ * The data event handler.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = async event => {
+ // Some servers close the socket on invalid username/password, this line
+ // guarantees onclose is handled before we try another AUTH method. See the
+ // same handling in SmtpClient.jsm.
+ await new Promise(resolve => setTimeout(resolve));
+
+ let stringPayload = CommonUtils.arrayBufferToByteString(
+ new Uint8Array(event.data)
+ );
+ this._logger.debug(`S: ${stringPayload}`);
+ if (this._pendingPayload) {
+ stringPayload = this._pendingPayload + stringPayload;
+ }
+ if (stringPayload.includes("\r\n")) {
+ // Start parsing if the payload contains at least one line break.
+ this._pendingPayload = "";
+ let res = this._parse(stringPayload);
+ this._nextAction?.(res);
+ } else {
+ // Save the incomplete payload for the next ondata event.
+ this._pendingPayload = stringPayload;
+ }
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = async event => {
+ this._logger.error(`${event.name}: a ${event.message} error occurred`);
+ this._server.serverBusy = false;
+ this.quit();
+ let secInfo =
+ await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo();
+ if (secInfo) {
+ this._logger.error(`SecurityError info: ${secInfo.errorCodeString}`);
+ if (secInfo.failedCertChain.length) {
+ let chain = secInfo.failedCertChain.map(c => {
+ return c.commonName + "; serial# " + c.serialNumber;
+ });
+ this._logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`);
+ }
+ this.runningUri.failedSecInfo = secInfo;
+ // Notify about the error directly. Due to the await above, the _onClose
+ // event is likely to complete before we get here, which means _actionDone
+ // ran and won't run again.
+ this.urlListener.OnStopRunningUrl(this.runningUri, event.errorCode);
+ }
+ this._actionDone(event.errorCode);
+ };
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed.");
+ this._server.serverBusy = false;
+ this._destroyed = true;
+ if (this._authenticating) {
+ // In some cases, socket is closed for invalid username/password.
+ this._actionAuthResponse({ success: false });
+ } else {
+ this._actionDone();
+ }
+ };
+
+ _lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ /**
+ * Read popstate.dat into this._uidlMap.
+ */
+ async _loadUidlState() {
+ let stateFile = this._server.localPath;
+ stateFile.append("popstate.dat");
+ if (!(await IOUtils.exists(stateFile.path))) {
+ this._uidlMap = new Map();
+ return;
+ }
+
+ let content = await IOUtils.readUTF8(stateFile.path);
+ this._uidlMap = new Map();
+ let uidlLine = false;
+ for (let line of content.split(this._lineSeparator)) {
+ if (!line) {
+ continue;
+ }
+ if (uidlLine) {
+ let [status, uidl, receivedAt] = line.split(" ");
+ this._uidlMap.set(uidl, {
+ status, // @type {UidlStatus}
+ uidl,
+ receivedAt,
+ });
+ }
+ if (line.startsWith("#")) {
+ // A comment line.
+ continue;
+ }
+ if (line.startsWith("*")) {
+ // The host & user line.
+ uidlLine = true;
+ }
+ }
+ }
+
+ /**
+ * Write this._uidlMap into popstate.dat.
+ *
+ * @param {boolean} [resetFlag] - If true, reset _uidlMapChanged to false.
+ */
+ async _writeUidlState(resetFlag) {
+ if (!this._uidlMapChanged) {
+ return;
+ }
+
+ let stateFile = this._server.localPath;
+ stateFile.append("popstate.dat");
+ let content = [
+ "# POP3 State File",
+ "# This is a generated file! Do not edit.",
+ "",
+ `*${this._server.hostName} ${this._server.username}`,
+ ];
+ for (let msg of this._messagesToHandle) {
+ // _messagesToHandle is not empty means an error happened, put them back
+ // to _uidlMap to prevent loss of popstate.
+ this._uidlMap.set(msg.uidl, msg);
+ }
+ for (let { status, uidl, receivedAt } of this._uidlMap.values()) {
+ if (receivedAt) {
+ content.push(`${status} ${uidl} ${receivedAt}`);
+ }
+ }
+ this._writeUidlPromise = IOUtils.writeUTF8(
+ stateFile.path,
+ content.join(this._lineSeparator)
+ );
+ await this._writeUidlPromise;
+ this._writeUidlPromise = null;
+
+ if (resetFlag) {
+ this._uidlMapChanged = false;
+ }
+ }
+
+ /**
+ * Send a command to the server.
+ *
+ * @param {string} str - The command string to send.
+ * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str.
+ */
+ _send(str, suppressLogging) {
+ if (this._socket?.readyState != "open") {
+ if (str != "QUIT") {
+ this._logger.warn(
+ `Socket state is ${this._socket?.readyState} - won't send command.`
+ );
+ }
+ return;
+ }
+
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this._logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ // Do not suppress for non-release builds, so that debugging auth problems
+ // is easier.
+ this._logger.debug(`C: ${str}`);
+ }
+
+ this._socket.send(CommonUtils.byteStringToArrayBuffer(str + "\r\n").buffer);
+ }
+
+ /**
+ * Send `CAPA` request to the server.
+ */
+ _actionCapa = () => {
+ this._nextAction = this._actionCapaResponse;
+ this._capabilities = [];
+ this._newMessageDownloaded = 0;
+ this._newMessageTotal = 0;
+ this._send("CAPA");
+ };
+
+ /**
+ * Handle `CAPA` response.
+ *
+ * @param {Pop3Response} res - CAPA response received from the server.
+ */
+ _actionCapaResponse = res => {
+ if (res.status && !res.success) {
+ this._actionChooseFirstAuthMethod();
+ return;
+ }
+ this._lineReader.read(
+ res.data,
+ line => {
+ line = line.trim().toUpperCase();
+ if (line == "USER") {
+ this._supportedAuthMethods.push("USERPASS");
+ } else if (line.startsWith("SASL ")) {
+ this._supportedAuthMethods.push(...line.slice(5).split(" "));
+ } else {
+ this._capabilities.push(line.split(" ")[0]);
+ }
+ },
+ () => this._actionChooseFirstAuthMethod()
+ );
+ };
+
+ /**
+ * Decide the first auth method to try.
+ */
+ _actionChooseFirstAuthMethod = () => {
+ if (
+ [
+ Ci.nsMsgSocketType.trySTARTTLS,
+ Ci.nsMsgSocketType.alwaysSTARTTLS,
+ ].includes(this._server.socketType) &&
+ !this._secureTransport
+ ) {
+ if (this._capabilities.includes("STLS")) {
+ // Init STARTTLS negotiation if required by user pref and supported.
+ this._nextAction = this._actionStlsResponse;
+ // STLS is the POP3 command to init STARTTLS.
+ this._send("STLS");
+ } else {
+ // Abort if not supported.
+ this._logger.error("Server doesn't support STLS. Aborting.");
+ this._actionError("nsErrorCouldNotConnectViaTls");
+ }
+ return;
+ }
+
+ // If a preferred method is not supported by the server, no need to try it.
+ this._possibleAuthMethods = this._preferredAuthMethods.filter(x =>
+ this._supportedAuthMethods.includes(x)
+ );
+ if (!this._possibleAuthMethods.length) {
+ if (this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext) {
+ this._possibleAuthMethods.unshift("USERPASS");
+ } else if (
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordEncrypted
+ ) {
+ this._possibleAuthMethods.unshift(
+ this._apopTimestamp ? "APOP" : "CRAM-MD5"
+ );
+ } else if (this._server.authMethod == Ci.nsMsgAuthMethod.GSSAPI) {
+ this._possibleAuthMethods.unshift("GSSAPI");
+ } else if (this._server.authMethod == Ci.nsMsgAuthMethod.NTLM) {
+ this._possibleAuthMethods.unshift("NTLM");
+ } else if (this._server.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ // Some servers don't return XOAUTH2 in CAPA correctly.
+ this._possibleAuthMethods.unshift("XOAUTH2");
+ }
+ }
+ this._logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`);
+ this._nextAuthMethod = this._nextAuthMethod || this._possibleAuthMethods[0];
+
+ if (this._nextAuthMethod) {
+ this._updateStatus("hostContact");
+ this._actionAuth();
+ return;
+ }
+
+ // Preferred auth methods don't match any supported auth methods. Give user
+ // some hints to change the config.
+ if (
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext &&
+ this._supportedAuthMethods.includes("CRAM-MD5")
+ ) {
+ // Suggest changing from plain password to encrypted password.
+ this._actionError("pop3AuthChangePlainToEncrypt");
+ } else if (
+ this._server.authMethod == Ci.nsMsgAuthMethod.passwordEncrypted &&
+ (this._supportedAuthMethods.includes("PLAIN") ||
+ this._supportedAuthMethods.includes("LOGIN"))
+ ) {
+ // Suggest changing from encrypted password to plain password.
+ this._actionError(
+ this._secureTransport
+ ? "pop3AuthChangeEncryptToPlainSSL"
+ : "pop3AuthChangeEncryptToPlainNoSSL"
+ );
+ } else {
+ // General suggestion about changing auth method.
+ this._actionError("pop3AuthMechNotSupported");
+ }
+ };
+
+ /**
+ * Handle STLS response. STLS is the POP3 command to init STARTTLS.
+ *
+ * @param {Pop3Response} res - STLS response received from the server.
+ */
+ _actionStlsResponse = res => {
+ if (!res.success) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._socket.upgradeToSecure();
+ this._secureTransport = true;
+ this._actionCapa();
+ };
+
+ /**
+ * Init authentication depending on server capabilities and user prefs.
+ */
+ _actionAuth = async () => {
+ if (!this._nextAuthMethod) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (this._destroyed) {
+ // If connection is lost, reconnect.
+ this.connect();
+ return;
+ }
+
+ this._authenticating = true;
+
+ this._currentAuthMethod = this._nextAuthMethod;
+ this._nextAuthMethod =
+ this._possibleAuthMethods[
+ this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1
+ ];
+ this._logger.debug(`Current auth method: ${this._currentAuthMethod}`);
+ this._nextAction = this._actionAuthResponse;
+
+ switch (this._currentAuthMethod) {
+ case "USERPASS":
+ this._nextAction = this._actionAuthUserPass;
+ this._send(`USER ${this._authenticator.username}`);
+ break;
+ case "PLAIN":
+ this._nextAction = this._actionAuthPlain;
+ this._send("AUTH PLAIN");
+ break;
+ case "LOGIN":
+ this._nextAction = this._actionAuthLoginUser;
+ this._send("AUTH LOGIN");
+ break;
+ case "CRAM-MD5":
+ this._nextAction = this._actionAuthCramMd5;
+ this._send("AUTH CRAM-MD5");
+ break;
+ case "APOP": {
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.MD5);
+ let data =
+ this._apopTimestamp +
+ (await this._authenticator.getByteStringPassword());
+ let digest = CommonUtils.bytesAsHex(
+ CryptoUtils.digestBytes(data, hasher)
+ );
+ this._send(`APOP ${this._authenticator.username} ${digest}`, true);
+ break;
+ }
+ case "GSSAPI": {
+ this._authenticator.initGssapiAuth("pop");
+ try {
+ let token = this._authenticator.getNextGssapiToken("");
+ this._nextAction = res => this._actionAuthGssapi(res, token);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionError("pop3GssapiFailure");
+ return;
+ }
+ this._send("AUTH GSSAPI");
+ break;
+ }
+ case "NTLM": {
+ this._authenticator.initNtlmAuth("pop");
+ try {
+ let token = this._authenticator.getNextNtlmToken("");
+ this._nextAction = res => this._actionAuthNtlm(res, token);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._send("AUTH NTLM");
+ break;
+ }
+ case "XOAUTH2":
+ this._nextAction = this._actionAuthXoauth;
+ this._send("AUTH XOAUTH2");
+ break;
+ default:
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle authentication response.
+ *
+ * @param {Pop3Response} res - Authentication response received from the server.
+ */
+ _actionAuthResponse = res => {
+ this._authenticating = false;
+ if (res.success) {
+ this._actionAfterAuth();
+ return;
+ }
+
+ if (this._nextAuthMethod) {
+ // Try the next auth method.
+ this._actionAuth();
+ return;
+ }
+
+ if (this._verifyLogon) {
+ this.runningUri.errorCode = "pop3PasswordFailed";
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (
+ ["USERPASS", "PLAIN", "LOGIN", "CRAM-MD5"].includes(
+ this._currentAuthMethod
+ )
+ ) {
+ this._actionError(
+ "pop3PasswordFailed",
+ [this._server.username],
+ res.statusText
+ );
+
+ // Ask user what to do.
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ if (action == 2) {
+ // 'New password' button pressed.
+ this._authenticator.forgetPassword();
+ }
+
+ // Retry.
+ this._nextAuthMethod = this._possibleAuthMethods[0];
+ this._actionAuth();
+ } else if (this._currentAuthMethod == "GSSAPI") {
+ this._actionError("pop3GssapiFailure", [], res.statusText);
+ }
+ };
+
+ /**
+ * The second step of USER/PASS auth, send the password to the server.
+ */
+ _actionAuthUserPass = async res => {
+ if (!res.success) {
+ this._actionError("pop3UsernameFailure", [], res.statusText);
+ return;
+ }
+ this._nextAction = this._actionAuthResponse;
+ this._send(
+ `PASS ${await this._authenticator.getByteStringPassword()}`,
+ true
+ );
+ };
+
+ /**
+ * The second step of PLAIN auth, send the auth token to the server.
+ */
+ _actionAuthPlain = async res => {
+ if (!res.success) {
+ this._actionError("pop3UsernameFailure", [], res.statusText);
+ return;
+ }
+ this._nextAction = this._actionAuthResponse;
+ this._send(await this._authenticator.getPlainToken(), true);
+ };
+
+ /**
+ * The second step of LOGIN auth, send the username to the server.
+ */
+ _actionAuthLoginUser = () => {
+ this._nextAction = this._actionAuthLoginPass;
+ this._logger.debug("AUTH LOGIN USER");
+ this._send(btoa(this._authenticator.username), true);
+ };
+
+ /**
+ * The third step of LOGIN auth, send the password to the server.
+ */
+ _actionAuthLoginPass = async res => {
+ if (!res.success) {
+ this._actionError("pop3UsernameFailure", [], res.statusText);
+ return;
+ }
+ this._nextAction = this._actionAuthResponse;
+ this._logger.debug("AUTH LOGIN PASS");
+ let password = await this._authenticator.getPassword();
+ if (
+ !Services.prefs.getBoolPref(
+ "mail.smtp_login_pop3_user_pass_auth_is_latin1",
+ true
+ ) ||
+ !/^[\x00-\xFF]+$/.test(password) // eslint-disable-line no-control-regex
+ ) {
+ // Unlike PLAIN auth, the payload of LOGIN auth is not standardized. When
+ // `mail.smtp_login_pop3_user_pass_auth_is_latin1` is true, we apply
+ // base64 encoding directly. Otherwise, we convert it to UTF-8
+ // BinaryString first, to make it work with btoa().
+ password = MailStringUtils.stringToByteString(password);
+ }
+ this._send(btoa(password), true);
+ };
+
+ /**
+ * The second step of CRAM-MD5 auth, send a HMAC-MD5 signature to the server.
+ *
+ * @param {Pop3Response} res - AUTH response received from the server.
+ */
+ _actionAuthCramMd5 = async res => {
+ if (!res.success) {
+ this._actionError("pop3UsernameFailure", [], res.statusText);
+ return;
+ }
+ this._nextAction = this._actionAuthResponse;
+ this._send(
+ this._authenticator.getCramMd5Token(
+ await this._authenticator.getPassword(),
+ res.statusText
+ ),
+ true
+ );
+ };
+
+ /**
+ * The second and next step of GSSAPI auth.
+ *
+ * @param {Pop3Response} res - AUTH response received from the server.
+ * @param {string} firstToken - The first GSSAPI token to send.
+ */
+ _actionAuthGssapi = (res, firstToken) => {
+ if (res.status != "+") {
+ this._actionAuthResponse(res);
+ return;
+ }
+
+ if (firstToken) {
+ this._nextAction = this._actionAuthGssapi;
+ this._send(firstToken, true);
+ return;
+ }
+
+ // Server returns a challenge, we send a new token. Can happen multiple times.
+ let token;
+ try {
+ token = this._authenticator.getNextGssapiToken(res.statusText);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionAuthResponse({ success: false, data: "AUTH GSSAPI" });
+ return;
+ }
+ this._send(token, true);
+ };
+
+ /**
+ * The second and next step of NTLM auth.
+ *
+ * @param {Pop3Response} res - AUTH response received from the server.
+ * @param {string} firstToken - The first NTLM token to send.
+ */
+ _actionAuthNtlm = (res, firstToken) => {
+ if (res.status != "+") {
+ this._actionAuthResponse(res);
+ return;
+ }
+
+ if (firstToken) {
+ this._nextAction = this._actionAuthNtlm;
+ this._send(firstToken, true);
+ return;
+ }
+
+ // Server returns a challenge, we send a new token. Can happen multiple times.
+ let token;
+ try {
+ token = this._authenticator.getNextNtlmToken(res.statusText);
+ } catch (e) {
+ this._logger.error(e);
+ this._actionAuthResponse({ success: false, data: "AUTH NTLM" });
+ return;
+ }
+ this._send(token, true);
+ };
+
+ /**
+ * The second step of XOAUTH2 auth.
+ *
+ * @param {Pop3Response} res - AUTH response received from the server.
+ */
+ _actionAuthXoauth = async res => {
+ if (res.status != "+") {
+ this._actionAuthResponse(res);
+ return;
+ }
+ this._nextAction = this._actionAuthResponse;
+ let token = await this._authenticator.getOAuthToken();
+ this._send(token, true);
+ };
+
+ /**
+ * Send `STAT` request to the server.
+ */
+ _actionStat = () => {
+ this._nextAction = this._actionStatResponse;
+ this._send("STAT");
+ };
+
+ /**
+ * Handle `STAT` response.
+ *
+ * @param {Pop3Response} res - STAT response received from the server.
+ */
+ _actionStatResponse = res => {
+ if (!res.success) {
+ this._actionError("pop3StatFail", [], res.statusText);
+ return;
+ }
+
+ let numberOfMessages = Number.parseInt(res.statusText);
+ if (!numberOfMessages) {
+ if (this._uidlMap.size) {
+ this._uidlMap.clear();
+ this._uidlMapChanged = true;
+ }
+ // Finish if there is no message.
+ MailServices.pop3.notifyDownloadCompleted(this._sink.folder, 0);
+ this._actionDone();
+ return;
+ }
+ if (!this._downloadMail && !this._server.leaveMessagesOnServer) {
+ // We are not downloading new mails, so finish now.
+ this._sink.setBiffStateAndUpdateFE(
+ Ci.nsIMsgFolder.nsMsgBiffState_NewMail,
+ numberOfMessages,
+ true
+ );
+ this._actionDone();
+ return;
+ }
+
+ if (this._downloadMail) {
+ try {
+ this._sink.beginMailDelivery(
+ this._singleUidlToDownload,
+ this._msgWindow
+ );
+ this._folderLocked = true;
+ this._logger.debug(
+ `Folder lock acquired uri=${this._sink.folder.URI}.`
+ );
+ } catch (e) {
+ const NS_MSG_FOLDER_BUSY = 2153054218;
+ if (e.result == NS_MSG_FOLDER_BUSY) {
+ this._actionError("pop3ServerBusy", [this._server.prettyName]);
+ } else {
+ this._actionError("pop3MessageWriteError");
+ }
+ return;
+ }
+ }
+ this._actionList();
+ };
+
+ /**
+ * Send `LIST` request to the server.
+ */
+ _actionList = () => {
+ this._messageSizeMap = new Map();
+ this._nextAction = this._actionListResponse;
+ this._send("LIST");
+ };
+
+ /**
+ * Handle `LIST` response.
+ *
+ * @param {Pop3Response} res - LIST response received from the server.
+ */
+ _actionListResponse = res => {
+ if (res.status && !res.success) {
+ this._actionError("pop3ListFailure", [], res.statusText);
+ return;
+ }
+ this._lineReader.read(
+ res.data,
+ line => {
+ let [messageNumber, messageSize] = line.split(" ");
+ this._messageSizeMap.set(messageNumber, Number(messageSize));
+ },
+ () => {
+ this._actionUidl();
+ }
+ );
+ };
+
+ /**
+ * Send `UIDL` request to the server.
+ */
+ _actionUidl = () => {
+ this._messagesToHandle = [];
+ this._newUidlMap = new Map();
+ this._nextAction = this._actionUidlResponse;
+ this._send("UIDL");
+ };
+
+ /**
+ * Handle `UIDL` response.
+ *
+ * @param {Pop3Response} res - UIDL response received from the server.
+ */
+ _actionUidlResponse = ({ status, success, data }) => {
+ if (status && !success) {
+ this._actionNoUidl();
+ return;
+ }
+ this._lineReader.read(
+ data,
+ line => {
+ let [messageNumber, uidl] = line.split(" ");
+ uidl = uidl.trim();
+ let uidlState = this._uidlMap.get(uidl);
+ if (uidlState) {
+ if (
+ uidlState.status == UIDL_KEEP &&
+ (!this._server.leaveMessagesOnServer ||
+ uidlState.receivedAt < this._cutOffTimestamp)
+ ) {
+ // Delete this message.
+ this._messagesToHandle.push({
+ ...uidlState,
+ messageNumber,
+ status: UIDL_DELETE,
+ });
+ } else if (
+ [UIDL_FETCH_BODY, UIDL_DELETE].includes(uidlState.status)
+ ) {
+ // Fetch the full message.
+ this._messagesToHandle.push({
+ ...uidlState,
+ messageNumber,
+ status: uidlState.status,
+ });
+ } else {
+ // Do nothing to this message.
+ this._newUidlMap.set(uidl, uidlState);
+ }
+ } else {
+ this._newMessageTotal++;
+ // Fetch the full message or only headers depending on server settings
+ // and message size.
+ let status =
+ this._server.headersOnly ||
+ this._messageSizeMap.get(messageNumber) > this._maxMessageSize
+ ? UIDL_TOO_BIG
+ : UIDL_FETCH_BODY;
+ this._messagesToHandle.push({
+ messageNumber,
+ uidl,
+ status,
+ });
+ }
+ },
+ () => {
+ if (!this._downloadMail) {
+ let numberOfMessages = this._messagesToHandle.filter(
+ // No receivedAt means we're seeing it for the first time.
+ msg => !msg.receivedAt
+ ).length;
+ if (numberOfMessages) {
+ this._sink.setBiffStateAndUpdateFE(
+ Ci.nsIMsgFolder.nsMsgBiffState_NewMail,
+ numberOfMessages,
+ true
+ );
+ }
+ this._actionDone();
+ return;
+ }
+
+ if (this._singleUidlToDownload) {
+ this._messagesToHandle = this._messagesToHandle.filter(
+ msg => msg.uidl == this._singleUidlToDownload
+ );
+ this._newUidlMap = this._uidlMap;
+ }
+
+ this._messagesToDownload = this._messagesToHandle.filter(msg =>
+ [UIDL_FETCH_BODY, UIDL_TOO_BIG].includes(msg.status)
+ );
+ this._totalDownloadSize = this._messagesToDownload.reduce(
+ (acc, msg) => acc + this._messageSizeMap.get(msg.messageNumber),
+ 0
+ );
+ this._totalReceivedSize = 0;
+ try {
+ let localFolder = this._sink.folder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ if (
+ localFolder.warnIfLocalFileTooBig(
+ this._msgWindow,
+ this._totalDownloadSize
+ )
+ ) {
+ throw new Error("Not enough disk space");
+ }
+ } catch (e) {
+ this._logger.error(e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+
+ this._uidlMapChanged =
+ this._uidlMap.size != this._newUidlMap.size ||
+ this._messagesToHandle.length;
+ // This discards staled uidls that are no longer on the server.
+ this._uidlMap = this._newUidlMap;
+
+ this._sink.setMsgsToDownload(this._messagesToDownload.length);
+ this._actionHandleMessage();
+ this._updateProgress();
+ }
+ );
+ };
+
+ /**
+ * If the server doesn't support UIDL, leaveMessagesOnServer and headersOnly
+ * feature can't be used.
+ */
+ _actionNoUidl = () => {
+ if (
+ this._server.leaveMessagesOnServer ||
+ this._server.headersOnly ||
+ this._server.limitOfflineMessageSize ||
+ this._singleUidlToDownload
+ ) {
+ this._actionError("pop3ServerDoesNotSupportUidlEtc", [
+ this._server.hostName,
+ ]);
+ return;
+ }
+ for (let [messageNumber] of this._messageSizeMap) {
+ // Send RETR for each message.
+ this._messagesToHandle.push({
+ status: UIDL_FETCH_BODY,
+ messageNumber,
+ });
+ }
+ this._actionHandleMessage();
+ };
+
+ /**
+ * Consume a message from this._messagesToHandle, decide to send TOP, RETR or
+ * DELE request.
+ */
+ _actionHandleMessage = () => {
+ this._currentMessage = this._messagesToHandle.shift();
+ if (
+ this._messagesToHandle.length > 0 &&
+ this._messagesToHandle.length % 20 == 0 &&
+ !this._writeUidlPromise
+ ) {
+ // Update popstate.dat every 20 messages, so that even if an error
+ // happens, no need to re-download all messages.
+ this._writeUidlState();
+ }
+ if (this._currentMessage) {
+ switch (this._currentMessage.status) {
+ case UIDL_TOO_BIG:
+ if (this._topFailed) {
+ this._actionRetr();
+ } else {
+ this._actionTop();
+ }
+ break;
+ case UIDL_FETCH_BODY:
+ this._actionRetr();
+ break;
+ case UIDL_DELETE:
+ this._actionDelete();
+ break;
+ default:
+ break;
+ }
+ } else {
+ this._sink.setBiffStateAndUpdateFE(
+ Ci.nsIMsgFolder.nsMsgBiffState_NewMail,
+ this._messagesToDownload
+ ? this._messagesToDownload.length
+ : // No UIDL support, every message is new.
+ this._messageSizeMap.size,
+ false
+ );
+ try {
+ this._sink.endMailDelivery(this);
+ this._folderLocked = false;
+ this._logger.debug("Folder lock released.");
+ } catch (e) {
+ this._logger.error("endMailDelivery failed", e);
+ this._actionDone(e.result || Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Send `TOP` request to the server.
+ */
+ _actionTop = () => {
+ this._nextAction = this._actionTopResponse;
+ let lineNumber = this._server.headersOnly ? 0 : 20;
+ this._send(`TOP ${this._currentMessage.messageNumber} ${lineNumber}`);
+ this._updateStatus("receivingMessages", [
+ ++this._newMessageDownloaded,
+ this._newMessageTotal,
+ ]);
+ };
+
+ /**
+ * Handle `TOP` response.
+ *
+ * @param {Pop3Response} res - TOP response received from the server.
+ */
+ _actionTopResponse = res => {
+ if (res.status) {
+ if (res.success) {
+ try {
+ // Call incorporateBegin only once for each message.
+ this._sink.incorporateBegin(
+ this._currentMessage.uidl,
+ Ci.nsMsgMessageFlags.Partial
+ );
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ return;
+ }
+ } else {
+ // TOP is not supported.
+ this._topFailed = true;
+ this._actionRetr();
+ return;
+ }
+ }
+ this._lineReader.read(
+ res.data,
+ line => {
+ // Remove \r\n and use the OS native line ending.
+ line = line.slice(0, -2) + this._lineSeparator;
+ try {
+ this._sink.incorporateWrite(line, line.length);
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ throw e; // Stop reading.
+ }
+ },
+ () => {
+ try {
+ this._sink.incorporateComplete(
+ this._msgWindow,
+ // Set size because it's a partial message.
+ this._messageSizeMap.get(this._currentMessage.messageNumber)
+ );
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ return;
+ }
+
+ let state = this._uidlMap.get(this._currentMessage.uidl);
+ if (state?.status == UIDL_FETCH_BODY) {
+ this._actionRetr();
+ return;
+ }
+ if (state?.status == UIDL_DELETE) {
+ this._actionDelete();
+ return;
+ }
+ this._uidlMap.set(this._currentMessage.uidl, {
+ status: UIDL_TOO_BIG,
+ uidl: this._currentMessage.uidl,
+ receivedAt: Math.floor(Date.now() / 1000),
+ });
+ this._uidlMapChanged = true;
+ this._actionHandleMessage();
+ }
+ );
+ };
+
+ /**
+ * Send `RETR` request to the server.
+ */
+ _actionRetr = () => {
+ this._nextAction = this._actionRetrResponse;
+ this._send(`RETR ${this._currentMessage.messageNumber}`);
+ this._updateStatus("receivingMessages", [
+ ++this._newMessageDownloaded,
+ this._newMessageTotal,
+ ]);
+ };
+
+ /**
+ * Handle `RETR` response.
+ *
+ * @param {Pop3Response} res - RETR response received from the server.
+ */
+ _actionRetrResponse = res => {
+ if (res.status) {
+ if (!res.success) {
+ this._actionError("pop3RetrFailure", [], res.statusText);
+ return;
+ }
+ try {
+ // Call incorporateBegin only once for each message.
+ this._sink.incorporateBegin(this._currentMessage.uidl, 0);
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ return;
+ }
+ }
+ this._lineReader.read(
+ res.data,
+ line => {
+ line = line.slice(0, -2) + this._lineSeparator;
+ try {
+ this._sink.incorporateWrite(line, line.length);
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ throw e; // Stop reading.
+ }
+ },
+ () => {
+ // Don't count the ending indicator.
+ this._totalReceivedSize -= ".\r\n".length;
+ try {
+ this._sink.incorporateComplete(
+ this._msgWindow,
+ 0 // Set size only when it's a partial message.
+ );
+ } catch (e) {
+ this._actionError("pop3MessageWriteError");
+ return;
+ }
+ if (this._server.leaveMessagesOnServer) {
+ let state = this._uidlMap.get(this._currentMessage.uidl);
+ if (state?.status == UIDL_DELETE) {
+ this._actionDelete();
+ } else {
+ this._uidlMap.set(this._currentMessage.uidl, {
+ status: UIDL_KEEP,
+ uidl: this._currentMessage.uidl,
+ receivedAt: Math.floor(Date.now() / 1000),
+ });
+ this._uidlMapChanged = true;
+ this._actionHandleMessage();
+ }
+ } else {
+ this._actionDelete();
+ }
+ }
+ );
+
+ this._totalReceivedSize += res.data.length;
+ this._updateProgress();
+ };
+
+ /**
+ * Send `DELE` request to the server.
+ */
+ _actionDelete = () => {
+ this._nextAction = this._actionDeleteResponse;
+ this._send(`DELE ${this._currentMessage.messageNumber}`);
+ };
+
+ /**
+ * Handle `DELE` response.
+ *
+ * @param {Pop3Response} res - DELE response received from the server.
+ */
+ _actionDeleteResponse = res => {
+ if (!res.success) {
+ this._actionError("pop3DeleFailure", [], res.statusText);
+ return;
+ }
+ this._actionHandleMessage();
+ };
+
+ /**
+ * Show an error prompt.
+ *
+ * @param {string} errorName - An error name corresponds to an entry of
+ * localMsgs.properties.
+ * @param {string[]} errorParams - Params to construct the error message.
+ * @param {string} serverErrorMsg - Error message returned by the server.
+ */
+ _actionError(errorName, errorParams, serverErrorMsg) {
+ this._logger.error(`Got an error name=${errorName}`);
+ if (errorName != "pop3PasswordFailed") {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+
+ if (!this._msgWindow) {
+ return;
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/localMsgs.properties"
+ );
+ let errorMsg;
+ if (errorParams) {
+ errorMsg = bundle.formatStringFromName(errorName, errorParams);
+ } else {
+ errorMsg = bundle.GetStringFromName(errorName);
+ }
+ if (serverErrorMsg) {
+ let serverSaidPrefix = bundle.formatStringFromName("pop3ServerSaid", [
+ this._server.hostName,
+ ]);
+ errorMsg += ` ${serverSaidPrefix} ${serverErrorMsg}`;
+ }
+
+ let errorTitle = bundle.formatStringFromName("pop3ErrorDialogTitle", [
+ this._server.prettyName,
+ ]);
+ Services.prompt.alert(this._msgWindow.domWindow, errorTitle, errorMsg);
+ }
+
+ /**
+ * Save popstate.dat when necessary, send QUIT.
+ * @param {nsresult} status - Indicate if the last action succeeded.
+ */
+ _actionDone = (status = Cr.NS_OK) => {
+ if (this._done) {
+ return;
+ }
+ this._done = true;
+ this._logger.debug(`Done with status=${status}`);
+ this._authenticating = false;
+ if (status == Cr.NS_OK) {
+ if (this._newMessageTotal) {
+ this._updateStatus("receivedMsgs", [
+ this._newMessageTotal,
+ this._newMessageTotal,
+ ]);
+ } else {
+ this._updateStatus("noNewMessages");
+ }
+ } else if (this._currentMessage) {
+ // Put _currentMessage back to the queue to prevent loss of popstate.
+ this._messagesToHandle.unshift(this._currentMessage);
+ }
+ this._writeUidlState(true);
+ // Normally we clean up after QUIT response.
+ this.quit(() => this._cleanUp(status));
+ // If we didn't receive QUIT response after 3 seconds, clean up anyway.
+ setTimeout(() => {
+ if (!this._cleanedUp) {
+ this._cleanUp(status);
+ }
+ }, 3000);
+ };
+
+ /**
+ * Notify listeners, close the socket and rest states.
+ * @param {nsresult} status - Indicate if the last action succeeded.
+ */
+ _cleanUp = status => {
+ this._cleanedUp = true;
+ this.close();
+ this.urlListener.OnStopRunningUrl(this.runningUri, status);
+ this.runningUri.SetUrlState(false, Cr.NS_OK);
+ this.onDone?.(status);
+ if (this._folderLocked) {
+ this._sink.abortMailDelivery(this);
+ this._folderLocked = false;
+ this._logger.debug("Folder lock released.");
+ }
+ this._server.wrappedJSObject.runningClient = null;
+ };
+
+ /**
+ * Show a status message in the status bar.
+ *
+ * @param {string} statusName - A string name in localMsgs.properties.
+ * @param {string[]} [params] - Params to format the string.
+ */
+ _updateStatus(statusName, params) {
+ if (!this._msgWindow?.statusFeedback) {
+ return;
+ }
+ if (!this._localBundle) {
+ this._localBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/localMsgs.properties"
+ );
+ this._messengerBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ }
+ let status = params
+ ? this._localBundle.formatStringFromName(statusName, params)
+ : this._localBundle.GetStringFromName(statusName);
+ this._msgWindow.statusFeedback.showStatusString(
+ this._messengerBundle.formatStringFromName("statusMessage", [
+ this._server.prettyName,
+ status,
+ ])
+ );
+ }
+
+ /**
+ * Show a progress bar in the status bar.
+ */
+ _updateProgress() {
+ this._msgWindow?.statusFeedback?.showProgress(
+ Math.floor((this._totalReceivedSize * 100) / this._totalDownloadSize)
+ );
+ }
+
+ /** @see nsIPop3Protocol */
+ checkMessage(uidl) {
+ return this._uidlMap.has(uidl);
+ }
+}
diff --git a/comm/mailnews/local/src/Pop3IncomingServer.jsm b/comm/mailnews/local/src/Pop3IncomingServer.jsm
new file mode 100644
index 0000000000..b8c147a022
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3IncomingServer.jsm
@@ -0,0 +1,308 @@
+/* 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 = ["Pop3IncomingServer"];
+
+var { MsgIncomingServer } = ChromeUtils.import(
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ Pop3Client: "resource:///modules/Pop3Client.jsm",
+});
+
+/**
+ * @implements {nsIPop3IncomingServer}
+ * @implements {nsILocalMailIncomingServer}
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ */
+class Pop3IncomingServer extends MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIPop3IncomingServer",
+ "nsILocalMailIncomingServer",
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ ]);
+
+ constructor() {
+ super();
+
+ // nsIMsgIncomingServer attributes.
+ this.localStoreType = "mailbox";
+ this.localDatabaseType = "mailbox";
+ this.downloadMessagesAtStartup = true;
+ this.canBeDefaultServer = true;
+
+ Object.defineProperty(this, "canCreateFoldersOnServer", {
+ get: () => !this.deferredToAccount,
+ });
+ Object.defineProperty(this, "canFileMessagesOnServer", {
+ get: () => !this.deferredToAccount,
+ });
+
+ // nsIPop3IncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Bool", "leaveMessagesOnServer", "leave_on_server"],
+ ["Bool", "headersOnly", "headers_only"],
+ ["Bool", "deleteMailLeftOnServer", "delete_mail_left_on_server"],
+ ["Bool", "deleteByAgeFromServer", "delete_by_age_from_server"],
+ ["Bool", "deferGetNewMail", "defer_get_new_mail"],
+ ["Int", "numDaysToLeaveOnServer", "num_days_to_leave_on_server"],
+ ]);
+
+ // @type {Map<string,string>} - A map from uidl to status.
+ this._uidlsToMark = new Map();
+ }
+
+ /** @see nsIMsgIncomingServer */
+ get rootMsgFolder() {
+ if (this._rootMsgFolder) {
+ return this._rootMsgFolder;
+ }
+
+ if (!this.deferredToAccount) {
+ this._rootMsgFolder = this.rootFolder;
+ return this._rootMsgFolder;
+ }
+
+ let incomingServer = MailServices.accounts.getAccount(
+ this.deferredToAccount
+ ).incomingServer;
+ if (incomingServer.equals(this)) {
+ // Make sure we're not deferred to ourself.
+ throw Components.Exception(
+ `${incomingServer.prettyName} cannot be deferred to itself`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ this._rootMsgFolder = incomingServer.rootMsgFolder;
+ return this._rootMsgFolder;
+ }
+
+ get canSearchMessages() {
+ return this.canFileMessagesOnServer;
+ }
+
+ getNewMessages(folder, msgWindow, urlListener) {
+ let inbox = this.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ if (!this.deferredToAccount) {
+ let deferredServers = this._getDeferedServers(folder.server);
+ if (deferredServers.length) {
+ // If other servers are deferred to this server, get new mail for them
+ // as well.
+ return this.downloadMailFromServers(
+ [...deferredServers, this],
+ msgWindow,
+ inbox,
+ urlListener
+ );
+ }
+ }
+ return MailServices.pop3.GetNewMail(msgWindow, urlListener, inbox, this);
+ }
+
+ verifyLogon(urlListener, msgWindow) {
+ return MailServices.pop3.verifyLogon(this, urlListener, msgWindow);
+ }
+
+ performBiff(msgWindow) {
+ this.performingBiff = true;
+ let inbox = this.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ let urlListener = inbox.QueryInterface(Ci.nsIUrlListener);
+ if (this.downloadOnBiff) {
+ MailServices.pop3.GetNewMail(msgWindow, urlListener, inbox, this);
+ } else {
+ MailServices.pop3.CheckForNewMail(msgWindow, urlListener, inbox, this);
+ }
+ }
+
+ /** @see nsILocalMailIncomingServer */
+ createDefaultMailboxes() {
+ for (let name of ["Inbox", "Trash"]) {
+ let folderUri = this.rootFolder.URI + "/" + name;
+ // Check by URI instead of by name, because folder name can be localized.
+ if (!this.rootFolder.getChildWithURI(folderUri, false, false)) {
+ this.msgStore.createFolder(this.rootFolder, name);
+ }
+ }
+ }
+
+ setFlagsOnDefaultMailboxes() {
+ this.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ // POP3 account gets an inbox, but no queue (unsent messages).
+ .setFlagsOnDefaultMailboxes(
+ Ci.nsMsgFolderFlags.SpecialUse & ~Ci.nsMsgFolderFlags.Queue
+ );
+ }
+
+ getNewMail(msgWindow, urlListener, inbox) {
+ return MailServices.pop3.GetNewMail(msgWindow, urlListener, inbox, this);
+ }
+
+ /** @see nsIPop3IncomingServer */
+ get deferredToAccount() {
+ let accountKey = this.getCharValue("deferred_to_account");
+ if (!accountKey) {
+ return "";
+ }
+
+ let account = MailServices.accounts.getAccount(accountKey);
+ // If currently deferred to an invalid or hidden server, change to defer to
+ // the local folders inbox.
+ if (!account || !account.incomingServer || account.incomingServer.hidden) {
+ let localAccount;
+ try {
+ localAccount = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+ } catch (e) {
+ // MailServices.accounts.localFoldersServer throws if no Local Folders.
+ return "";
+ }
+ accountKey = localAccount.key;
+ this.setCharValue("deferred_to_account", accountKey);
+ }
+
+ return accountKey;
+ }
+
+ set deferredToAccount(accountKey) {
+ this._rootMsgFolder = null;
+
+ let wasDeferred = Boolean(this.deferredToAccount);
+ this.setCharValue("deferred_to_account", accountKey);
+
+ // If isDeferred state has changed, send notification.
+ if (Boolean(accountKey) != wasDeferred) {
+ let folderListenerManager = MailServices.mailSession.QueryInterface(
+ Ci.nsIFolderListener
+ );
+ folderListenerManager.onFolderBoolPropertyChanged(
+ this.rootFolder,
+ "isDeferred",
+ wasDeferred,
+ !wasDeferred
+ );
+ folderListenerManager.onFolderBoolPropertyChanged(
+ this.rootFolder,
+ "CanFileMessages",
+ !wasDeferred,
+ wasDeferred
+ );
+ }
+
+ if (!accountKey) {
+ return;
+ }
+ // Check if we are deferred to the local folders, and create INBOX if needed.
+ let server = MailServices.accounts.getAccount(accountKey).incomingServer;
+ if (server instanceof Ci.nsILocalMailIncomingServer) {
+ // Check by URI instead of by name, because folder name can be localized.
+ if (
+ !this.rootFolder.getChildWithURI(
+ `${this.rootFolder.URI}/Inbox`,
+ false,
+ false
+ )
+ ) {
+ server.rootFolder.createSubfolder("Inbox", null);
+ }
+ }
+ }
+
+ addUidlToMark(uidl, mark) {
+ // @see nsIMsgLocalMailFolder
+ const POP3_DELETE = 1;
+ const POP3_FETCH_BODY = 2;
+ let status = "k";
+ if (mark == POP3_DELETE) {
+ status = "d";
+ } else if (mark == POP3_FETCH_BODY) {
+ status = "f";
+ }
+ this._uidlsToMark.set(uidl, status);
+ }
+
+ markMessages() {
+ if (!this._uidlsToMark.size) {
+ return;
+ }
+
+ let client = this.runningClient || new lazy.Pop3Client(this);
+ // Pass a clone of this._uidlsToMark to client.markMessages, because
+ // this._uidlsToMark may be changed before markMessages finishes.
+ client.markMessages(new Map(this._uidlsToMark));
+ this._uidlsToMark = new Map();
+ }
+
+ downloadMailFromServers(servers, msgWindow, folder, urlListener) {
+ let server = servers.shift();
+ if (!server) {
+ urlListener?.OnStopRunningUrl(null, Cr.NS_OK);
+ return;
+ }
+
+ // If server != folder.server, it means server is deferred to folder.server,
+ // so if server.deferGetNewMail is false, no need to call GetNewMail.
+ if (server == folder.server || server.deferGetNewMail) {
+ MailServices.pop3.GetNewMail(
+ msgWindow,
+ {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl: () => {
+ // Call GetNewMail for the next server only after it is finished for
+ // the current server.
+ this.downloadMailFromServers(
+ servers,
+ msgWindow,
+ folder,
+ urlListener
+ );
+ },
+ },
+ folder,
+ server
+ );
+ return;
+ }
+ this.downloadMailFromServers(servers, msgWindow, folder, urlListener);
+ }
+
+ /**
+ * Get all the servers that defer to the passed in server.
+ *
+ * @param {nsIMsgIncomingServer} dstServer - The server that others servers
+ * are deferred to.
+ */
+ _getDeferedServers(dstServer) {
+ let dstAccount = MailServices.accounts.FindAccountForServer(dstServer);
+ if (!dstAccount) {
+ return [];
+ }
+ return MailServices.accounts.allServers.filter(
+ server =>
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount == dstAccount.key
+ );
+ }
+}
+
+Pop3IncomingServer.prototype.classID = Components.ID(
+ "{f99fdbf7-2e79-4ce3-9d94-7af3763b82fc}"
+);
diff --git a/comm/mailnews/local/src/Pop3ProtocolHandler.jsm b/comm/mailnews/local/src/Pop3ProtocolHandler.jsm
new file mode 100644
index 0000000000..51b1d5bf22
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3ProtocolHandler.jsm
@@ -0,0 +1,40 @@
+/* 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 EXPORTED_SYMBOLS = ["Pop3ProtocolHandler"];
+
+var { Pop3Channel } = ChromeUtils.import("resource:///modules/Pop3Channel.jsm");
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class Pop3ProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "pop3";
+
+ newChannel(uri, loadInfo) {
+ let channel = new Pop3Channel(uri, loadInfo);
+ let spec = uri.spec;
+ if (
+ spec.includes("part=") &&
+ !spec.includes("type=message/rfc822") &&
+ !spec.includes("type=application/x-message-display") &&
+ !spec.includes("type=application/pdf")
+ ) {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ } else {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE;
+ }
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return true;
+ }
+}
+
+Pop3ProtocolHandler.prototype.classID = Components.ID(
+ "{eed38573-d01b-4c13-9f9d-f69963095a4d}"
+);
diff --git a/comm/mailnews/local/src/Pop3ProtocolInfo.jsm b/comm/mailnews/local/src/Pop3ProtocolInfo.jsm
new file mode 100644
index 0000000000..57fc9b07d9
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3ProtocolInfo.jsm
@@ -0,0 +1,44 @@
+/* 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 = ["Pop3ProtocolInfo"];
+
+var { MsgProtocolInfo } = ChromeUtils.importESModule(
+ "resource:///modules/MsgProtocolInfo.sys.mjs"
+);
+
+/**
+ * @implements {nsIMsgProtocolInfo}
+ */
+class Pop3ProtocolInfo extends MsgProtocolInfo {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]);
+
+ serverIID = Components.ID("{f99fdbf7-2e79-4ce3-9d94-7af3763b82fc}");
+
+ requiresUsername = true;
+ preflightPrettyNameWithEmailAddress = true;
+ canDelete = true;
+ canLoginAtStartUp = true;
+ canDuplicate = true;
+ canGetMessages = true;
+ canGetIncomingMessages = true;
+ defaultDoBiff = true;
+ showComposeMsgLink = true;
+ foldersCreatedAsync = false;
+
+ getDefaultServerPort(isSecure) {
+ return isSecure
+ ? Ci.nsIPop3URL.DEFAULT_POP3S_PORT
+ : Ci.nsIPop3URL.DEFAULT_POP3_PORT;
+ }
+
+ // @see MsgProtocolInfo.sys.mjs
+ RELATIVE_PREF = "mail.root.pop3-rel";
+ ABSOLUTE_PREF = "mail.root.pop3";
+ DIR_SERVICE_PROP = "MailD";
+}
+
+Pop3ProtocolInfo.prototype.classID = Components.ID(
+ "{7689942f-cbd1-42ad-87b9-44128354f55d}"
+);
diff --git a/comm/mailnews/local/src/Pop3Service.jsm b/comm/mailnews/local/src/Pop3Service.jsm
new file mode 100644
index 0000000000..0270489fcd
--- /dev/null
+++ b/comm/mailnews/local/src/Pop3Service.jsm
@@ -0,0 +1,77 @@
+/* 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 = ["Pop3Service"];
+
+var { Pop3Client } = ChromeUtils.import("resource:///modules/Pop3Client.jsm");
+
+/**
+ * @implements {nsIPop3Service}
+ */
+class Pop3Service {
+ QueryInterface = ChromeUtils.generateQI(["nsIPop3Service"]);
+
+ constructor() {
+ this._listeners = new Set();
+ }
+
+ GetNewMail(msgWindow, urlListener, inbox, server) {
+ return this._getMail(true, msgWindow, urlListener, inbox, server);
+ }
+
+ CheckForNewMail(msgWindow, urlListener, inbox, server) {
+ return this._getMail(false, msgWindow, urlListener, inbox, server);
+ }
+
+ verifyLogon(server, urlListener, msgWindow) {
+ let client = new Pop3Client(server);
+ client.urlListener = urlListener;
+ client.connect();
+ client.onOpen = () => {
+ client.verifyLogon(msgWindow);
+ };
+ return client.runningUri;
+ }
+
+ addListener(listener) {
+ this._listeners.add(listener);
+ }
+
+ removeListener(listener) {
+ this._listeners.remove(listener);
+ }
+
+ notifyDownloadStarted(folder) {
+ for (let listener of this._listeners) {
+ listener.onDownloadStarted(folder);
+ }
+ }
+
+ notifyDownloadProgress(folder, numMessages, numTotalMessages) {
+ for (let listener of this._listeners) {
+ listener.onDownloadProgress(folder, numMessages, numTotalMessages);
+ }
+ }
+
+ notifyDownloadCompleted(folder, numMessages) {
+ for (let listener of this._listeners) {
+ listener.onDownloadCompleted(folder, numMessages);
+ }
+ }
+
+ _getMail(downloadNewMail, msgWindow, urlListener, inbox, server) {
+ let client = new Pop3Client(server);
+ client.runningUri.msgWindow = msgWindow;
+ client.urlListener = urlListener;
+ client.connect();
+ client.onOpen = () => {
+ client.getMail(downloadNewMail, msgWindow, inbox);
+ };
+ return client.runningUri;
+ }
+}
+
+Pop3Service.prototype.classID = Components.ID(
+ "{1e8f21c3-32c3-4114-9ea4-3d74006fb351}"
+);
diff --git a/comm/mailnews/local/src/components.conf b/comm/mailnews/local/src/components.conf
new file mode 100644
index 0000000000..00f814f5df
--- /dev/null
+++ b/comm/mailnews/local/src/components.conf
@@ -0,0 +1,150 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{060e684a-0acd-41f1-b36b-12de686f201e}",
+ "contract_ids": ["@mozilla.org/messenger/pop3-sink;1"],
+ "type": "nsPop3Sink",
+ "headers": ["/comm/mailnews/local/src/nsPop3Sink.h"],
+ },
+ {
+ "cid": "{f99fdbf7-2e79-4ce3-9d94-7af3763b82fc}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=pop3"],
+ "jsm": "resource:///modules/Pop3IncomingServer.jsm",
+ "constructor": "Pop3IncomingServer",
+ },
+ {
+ "cid": "{1e8f21c3-32c3-4114-9ea4-3d74006fb351}",
+ "contract_ids": ["@mozilla.org/messenger/popservice;1"],
+ "jsm": "resource:///modules/Pop3Service.jsm",
+ "constructor": "Pop3Service",
+ },
+ {
+ "cid": "{7689942f-cbd1-42ad-87b9-44128354f55d}",
+ "contract_ids": ["@mozilla.org/messenger/protocol/info;1?type=pop3"],
+ "jsm": "resource:///modules/Pop3ProtocolInfo.jsm",
+ "constructor": "Pop3ProtocolInfo",
+ },
+ {
+ "cid": "{eed38573-d01b-4c13-9f9d-f69963095a4d}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=pop"],
+ "jsm": "resource:///modules/Pop3ProtocolHandler.jsm",
+ "constructor": "Pop3ProtocolHandler",
+ "protocol_config": {
+ "scheme": "pop3",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_DANGEROUS_TO_LOAD",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ ],
+ "default_port": 110,
+ },
+ },
+ {
+ "cid": "{46efcb10-cb6d-11d2-8065-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/mailboxurl;1"],
+ "type": "nsMailboxUrl",
+ "headers": ["/comm/mailnews/local/src/nsMailboxUrl.h"],
+ },
+ {
+ "cid": "{3fdae3ab-4ac1-4ad4-b28a-28d0fa363929}",
+ "contract_ids": ["@mozilla.org/messenger/msgmailnewsurl;1"],
+ "type": "nsMsgMailNewsUrl",
+ "headers": ["/comm/mailnews/base/src/nsMsgMailNewsUrl.h"],
+ },
+ {
+ "cid": "{eef82462-cb69-11d2-8065-006008128c4e}",
+ "contract_ids": [
+ "@mozilla.org/messenger/mailboxservice;1",
+ "@mozilla.org/messenger/messageservice;1?type=mailbox",
+ "@mozilla.org/messenger/messageservice;1?type=mailbox-message",
+ "@mozilla.org/network/protocol;1?name=mailbox",
+ ],
+ "type": "nsMailboxService",
+ "headers": ["/comm/mailnews/local/src/nsMailboxService.h"],
+ "protocol_config": {
+ "scheme": "mailbox",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_DANGEROUS_TO_LOAD",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ },
+ },
+ {
+ "cid": "{d17e0e22-285b-4239-863c-2eebb008b5cd}",
+ "contract_ids": ["@mozilla.org/messenger/mailboxparser;1"],
+ "type": "nsMsgMailboxParser",
+ "headers": ["/comm/mailnews/local/src/nsParseMailbox.h"],
+ },
+ {
+ "cid": "{ea1b0a11-e6f4-11d2-8070-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/popurl;1"],
+ "type": "nsPop3URL",
+ "headers": ["/comm/mailnews/local/src/nsPop3URL.h"],
+ },
+ {
+ "cid": "{75b63b46-1dd2-11b2-9873-bb375e1550fa}",
+ "contract_ids": [
+ "@mozilla.org/messenger/noneservice;1",
+ "@mozilla.org/messenger/protocol/info;1?type=none",
+ ],
+ "type": "nsNoneService",
+ "headers": ["/comm/mailnews/local/src/nsNoneService.h"],
+ },
+ {
+ "cid": "{e490d22c-cd67-11d2-8cca-0060b0fc14a3}",
+ "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=mailbox"],
+ "type": "nsMsgLocalMailFolder",
+ "headers": ["/comm/mailnews/local/src/nsLocalMailFolder.h"],
+ },
+ {
+ "cid": "{36358199-a0e4-4b68-929f-77c01de34c67}",
+ "contract_ids": ["@mozilla.org/msgstore/berkeleystore;1"],
+ "type": "nsMsgBrkMBoxStore",
+ "headers": ["/comm/mailnews/local/src/nsMsgBrkMBoxStore.h"],
+ },
+ {
+ "cid": "{1f993eda-7dd9-11df-819a-6257dfd72085}",
+ "contract_ids": ["@mozilla.org/msgstore/maildirstore;1"],
+ "type": "nsMsgMaildirStore",
+ "headers": ["/comm/mailnews/local/src/nsMsgMaildirStore.h"],
+ },
+ {
+ "cid": "{ca5ffe7e-5f47-11d3-9a51-004005263078}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=none"],
+ "type": "nsNoIncomingServer",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/local/src/nsNoIncomingServer.h"],
+ },
+ {
+ "cid": "{2b79ac51-1459-11d3-8097-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/messagestateparser;1"],
+ "type": "nsParseMailMessageState",
+ "headers": ["/comm/mailnews/local/src/nsParseMailbox.h"],
+ },
+ {
+ "cid": "{44aef4ce-475b-42e3-bc42-7730d5ce7365}",
+ "contract_ids": [
+ "@mozilla.org/messenger/rssservice;1",
+ "@mozilla.org/messenger/protocol/info;1?type=rss",
+ ],
+ "type": "nsRssService",
+ "headers": ["/comm/mailnews/local/src/nsRssService.h"],
+ },
+ {
+ "cid": "{3a874285-5520-41a0-bcda-a3dee3dbf4f3}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=rss"],
+ "type": "nsRssIncomingServer",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/local/src/nsRssIncomingServer.h"],
+ },
+]
diff --git a/comm/mailnews/local/src/moz.build b/comm/mailnews/local/src/moz.build
new file mode 100644
index 0000000000..f530740b80
--- /dev/null
+++ b/comm/mailnews/local/src/moz.build
@@ -0,0 +1,40 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsLocalMailFolder.cpp",
+ "nsLocalUndoTxn.cpp",
+ "nsLocalUtils.cpp",
+ "nsMailboxProtocol.cpp",
+ "nsMailboxServer.cpp",
+ "nsMailboxService.cpp",
+ "nsMailboxUrl.cpp",
+ "nsMsgBrkMBoxStore.cpp",
+ "nsMsgFileHdr.cpp",
+ "nsMsgLocalStoreUtils.cpp",
+ "nsMsgMaildirStore.cpp",
+ "nsNoIncomingServer.cpp",
+ "nsNoneService.cpp",
+ "nsParseMailbox.cpp",
+ "nsPop3Sink.cpp",
+ "nsPop3URL.cpp",
+ "nsRssIncomingServer.cpp",
+ "nsRssService.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+EXTRA_JS_MODULES += [
+ "Pop3Channel.jsm",
+ "Pop3Client.jsm",
+ "Pop3IncomingServer.jsm",
+ "Pop3ProtocolHandler.jsm",
+ "Pop3ProtocolInfo.jsm",
+ "Pop3Service.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/local/src/nsLocalMailFolder.cpp b/comm/mailnews/local/src/nsLocalMailFolder.cpp
new file mode 100644
index 0000000000..2602c40046
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalMailFolder.cpp
@@ -0,0 +1,3468 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "prlog.h"
+
+#include "msgCore.h" // precompiled header...
+#include "nsLocalMailFolder.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsITransactionManager.h"
+#include "nsParseMailbox.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgWindow.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsLocalUtils.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsILocalMailIncomingServer.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsString.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsUnicharUtils.h"
+#include "nsICopyMessageStreamListener.h"
+#include "nsIMsgCopyService.h"
+#include "nsMsgTxn.h"
+#include "nsIMessenger.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIDocShell.h"
+#include "nsIPrompt.h"
+#include "nsIPop3URL.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgFolderCompactor.h"
+#include "nsNetCID.h"
+#include "nsISpamSettings.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsMailHeaders.h"
+#include "nsCOMArray.h"
+#include "nsIRssIncomingServer.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsReadLine.h"
+#include "nsIStringEnumerator.h"
+#include "nsIURIMutator.h"
+#include "mozilla/Components.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/SlicedInputStream.h"
+
+//////////////////////////////////////////////////////////////////////////////
+// nsLocal
+/////////////////////////////////////////////////////////////////////////////
+
+nsLocalMailCopyState::nsLocalMailCopyState()
+ : m_flags(0),
+ m_lastProgressTime(PR_IntervalToMilliseconds(PR_IntervalNow())),
+ m_curDstKey(nsMsgKey_None),
+ m_curCopyIndex(0),
+ m_totalMsgCount(0),
+ m_dataBufferSize(0),
+ m_leftOver(0),
+ m_isMove(false),
+ m_dummyEnvelopeNeeded(false),
+ m_fromLineSeen(false),
+ m_writeFailed(false),
+ m_notifyFolderLoaded(false) {}
+
+nsLocalMailCopyState::~nsLocalMailCopyState() {
+ PR_Free(m_dataBuffer);
+ if (m_fileStream) m_fileStream->Close();
+ if (m_messageService) {
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryInterface(m_srcSupport);
+ if (srcFolder && m_message) {
+ nsCString uri;
+ srcFolder->GetUriForMsg(m_message, uri);
+ }
+ }
+}
+
+nsLocalFolderScanState::nsLocalFolderScanState() : m_uidl(nullptr) {}
+
+nsLocalFolderScanState::~nsLocalFolderScanState() {}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsMsgLocalMailFolder interface
+///////////////////////////////////////////////////////////////////////////////
+
+nsMsgLocalMailFolder::nsMsgLocalMailFolder(void)
+ : mCopyState(nullptr),
+ mHaveReadNameFromDB(false),
+ mInitialized(false),
+ mCheckForNewMessagesAfterParsing(false),
+ m_parsingFolder(false),
+ mDownloadState(DOWNLOAD_STATE_NONE) {}
+
+nsMsgLocalMailFolder::~nsMsgLocalMailFolder(void) {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgLocalMailFolder, nsMsgDBFolder,
+ nsICopyMessageListener, nsIMsgLocalMailFolder)
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgLocalMailFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ nsMsgLocalMailFolder* newFolder = new nsMsgLocalMailFolder;
+ if (!newFolder) return NS_ERROR_OUT_OF_MEMORY;
+
+ NS_ADDREF(*folder = newFolder);
+ newFolder->Init(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::CreateLocalSubfolder(
+ const nsAString& aFolderName, nsIMsgFolder** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+ nsresult rv = CreateSubfolderInternal(aFolderName, nullptr, aChild);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(*aChild);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetManyHeadersToDownload(bool* retval) {
+ bool isLocked;
+ // if the folder is locked, we're probably reparsing - let's build the
+ // view when we've finished reparsing.
+ GetLocked(&isLocked);
+ if (isLocked) {
+ *retval = true;
+ return NS_OK;
+ }
+
+ return nsMsgDBFolder::GetManyHeadersToDownload(retval);
+}
+
+// run the url to parse the mailbox
+NS_IMETHODIMP nsMsgLocalMailFolder::ParseFolder(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aListener != this) mReparseListener = aListener;
+ // if parsing is synchronous, we need to set m_parsingFolder to
+ // true before starting. And we need to open the db before
+ // setting m_parsingFolder to true.
+ // OpenDatabase();
+ rv = msgStore->RebuildIndex(this, mDatabase, aMsgWindow, this);
+ if (NS_SUCCEEDED(rv)) m_parsingFolder = true;
+
+ return rv;
+}
+
+// this won't force a reparse of the folder if the db is invalid.
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetMsgDatabase(nsIMsgDatabase** aMsgDatabase) {
+ return GetDatabaseWOReparse(aMsgDatabase);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ if (!mInitialized) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ // need to set this flag here to avoid infinite recursion
+ mInitialized = true;
+ rv = server->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // This should add all existing folders as sub-folders of this folder.
+ rv = msgStore->DiscoverSubFolders(this, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> path;
+ rv = GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ bool directory;
+ path->IsDirectory(&directory);
+ if (directory) {
+ SetFlag(nsMsgFolderFlags::Mail | nsMsgFolderFlags::Elided |
+ nsMsgFolderFlags::Directory);
+
+ bool isServer;
+ GetIsServer(&isServer);
+ if (isServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsILocalMailIncomingServer> localMailServer;
+ localMailServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ // first create the folders on disk (as empty files)
+ rv = localMailServer->CreateDefaultMailboxes();
+ if (NS_FAILED(rv) && rv != NS_MSG_FOLDER_EXISTS) return rv;
+
+ // must happen after CreateSubFolders, or the folders won't exist.
+ rv = localMailServer->SetFlagsOnDefaultMailboxes();
+ if (NS_FAILED(rv)) return rv;
+ }
+ }
+ UpdateSummaryTotals(false);
+ }
+
+ return nsMsgDBFolder::GetSubFolders(folders);
+}
+
+nsresult nsMsgLocalMailFolder::GetDatabase() {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ return GetDatabaseWOReparse(getter_AddRefs(msgDB));
+}
+
+// we treat failure as null db returned
+NS_IMETHODIMP nsMsgLocalMailFolder::GetDatabaseWOReparse(
+ nsIMsgDatabase** aDatabase) {
+ NS_ENSURE_ARG(aDatabase);
+ if (m_parsingFolder) return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+
+ nsresult rv = NS_OK;
+ if (!mDatabase) {
+ rv = OpenDatabase();
+ if (mDatabase) {
+ mDatabase->AddListener(this);
+ UpdateNewMessages();
+ }
+ }
+ NS_IF_ADDREF(*aDatabase = mDatabase);
+ if (mDatabase) mDatabase->SetLastUseTime(PR_Now());
+ return rv;
+}
+
+// Makes sure the database is open and exists. If the database is out of date,
+// then this call will return NS_ERROR_NOT_INITIALIZED and run an async url
+// to reparse the folder. The passed in url listener will get called when the
+// url is done.
+NS_IMETHODIMP nsMsgLocalMailFolder::GetDatabaseWithReparse(
+ nsIUrlListener* aReparseUrlListener, nsIMsgWindow* aMsgWindow,
+ nsIMsgDatabase** aMsgDatabase) {
+ nsresult rv = NS_OK;
+ // if we're already reparsing, just remember the listener so we can notify it
+ // when we've finished.
+ if (m_parsingFolder) {
+ NS_ASSERTION(!mReparseListener, "can't have an existing listener");
+ mReparseListener = aReparseUrlListener;
+ return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+
+ if (!mDatabase) {
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ rv = pathFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists)
+ return NS_ERROR_NULL_POINTER; // mDatabase will be null at this point.
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsresult folderOpen =
+ msgDBService->OpenFolderDB(this, true, getter_AddRefs(mDatabase));
+ if (folderOpen == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ if (mDatabase) {
+ mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ dbFolderInfo->SetNumMessages(0);
+ dbFolderInfo->SetNumUnreadMessages(0);
+ dbFolderInfo->GetTransferInfo(getter_AddRefs(transferInfo));
+ }
+ dbFolderInfo = nullptr;
+
+ // A backup message database might have been created earlier, for
+ // example if the user requested a reindex. We'll use the earlier one if
+ // we can, otherwise we'll try to backup at this point.
+ if (NS_FAILED(OpenBackupMsgDatabase())) {
+ CloseAndBackupFolderDB(EmptyCString());
+ if (NS_FAILED(OpenBackupMsgDatabase()) && mBackupDatabase) {
+ mBackupDatabase->RemoveListener(this);
+ mBackupDatabase = nullptr;
+ }
+ } else
+ mDatabase->ForceClosed();
+
+ mDatabase = nullptr;
+ }
+ nsCOMPtr<nsIFile> summaryFile;
+ rv = GetSummaryFileLocation(pathFile, getter_AddRefs(summaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Remove summary file.
+ summaryFile->Remove(false);
+
+ // if it's out of date then reopen with upgrade.
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (transferInfo && mDatabase) {
+ SetDBTransferInfo(transferInfo);
+ mDatabase->SetSummaryValid(false);
+ }
+ } else if (folderOpen == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) {
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ }
+
+ if (mDatabase) {
+ if (mAddListener) mDatabase->AddListener(this);
+
+ // if we have to regenerate the folder, run the parser url.
+ if (folderOpen == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ folderOpen == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ if (NS_FAILED(rv = ParseFolder(aMsgWindow, aReparseUrlListener))) {
+ if (rv == NS_MSG_FOLDER_BUSY) {
+ // we need to null out the db so that parsing gets kicked off again.
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ ThrowAlertMsg("parsingFolderFailed", aMsgWindow);
+ }
+ return rv;
+ }
+
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // We have a valid database so lets extract necessary info.
+ UpdateSummaryTotals(true);
+ }
+ }
+ NS_IF_ADDREF(*aMsgDatabase = mDatabase);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::UpdateFolder(nsIMsgWindow* aWindow) {
+ (void)RefreshSizeOnDisk();
+ nsresult rv;
+
+ if (!PromptForMasterPasswordIfNecessary()) return NS_ERROR_FAILURE;
+
+ // If we don't currently have a database, get it. Otherwise, the folder has
+ // been updated (presumably this changes when we download headers when opening
+ // inbox). If it's updated, send NotifyFolderLoaded.
+ if (!mDatabase) {
+ // return of NS_ERROR_NOT_INITIALIZED means running parsing URL
+ // We don't need the return value, and assigning it to mDatabase which
+ // is already set internally leaks.
+ nsCOMPtr<nsIMsgDatabase> returnedDb;
+ rv = GetDatabaseWithReparse(this, aWindow, getter_AddRefs(returnedDb));
+ if (NS_SUCCEEDED(rv)) NotifyFolderEvent(kFolderLoaded);
+ } else {
+ bool valid;
+ rv = mDatabase->GetSummaryValid(&valid);
+ // don't notify folder loaded or try compaction if db isn't valid
+ // (we're probably reparsing or copying msgs to it)
+ if (NS_SUCCEEDED(rv) && valid)
+ NotifyFolderEvent(kFolderLoaded);
+ else if (mCopyState)
+ mCopyState->m_notifyFolderLoaded =
+ true; // defer folder loaded notification
+ else if (!m_parsingFolder) // if the db was already open, it's probably OK
+ // to load it if not parsing
+ NotifyFolderEvent(kFolderLoaded);
+ }
+ bool filtersRun;
+ bool hasNewMessages;
+ GetHasNewMessages(&hasNewMessages);
+ if (mDatabase) ApplyRetentionSettings();
+ // if we have new messages, try the filter plugins.
+ if (NS_SUCCEEDED(rv) && hasNewMessages)
+ (void)CallFilterPlugins(aWindow, &filtersRun);
+ // Callers should rely on folder loaded event to ensure completion of loading.
+ // So we'll return NS_OK even if parsing is still in progress
+ if (rv == NS_ERROR_NOT_INITIALIZED) rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetFolderURL(nsACString& aUrl) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> path;
+ rv = GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = NS_GetURLSpecFromFile(path, aUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aUrl.Replace(0, strlen("file:"), "mailbox:");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::CreateStorageIfMissing(
+ nsIUrlListener* aUrlListener) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ GetParent(getter_AddRefs(msgParent));
+
+ // parent is probably not set because *this* was probably created by rdf
+ // and not by folder discovery. So, we have to compute the parent.
+ if (!msgParent) {
+ nsAutoCString folderName(mURI);
+ nsAutoCString uri;
+ int32_t leafPos = folderName.RFindChar('/');
+ nsAutoCString parentName(folderName);
+ if (leafPos > 0) {
+ // If there is a hierarchy, there is a parent.
+ // Don't strip off slash if it's the first character
+ parentName.SetLength(leafPos);
+ rv = GetOrCreateFolder(parentName, getter_AddRefs(msgParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ if (msgParent) {
+ nsString folderName;
+ GetName(folderName);
+ rv = msgParent->CreateSubfolder(folderName, nullptr);
+ // by definition, this is OK.
+ if (rv == NS_MSG_FOLDER_EXISTS) return NS_OK;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ nsresult rv =
+ CreateSubfolderInternal(folderName, msgWindow, getter_AddRefs(newFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(newFolder);
+
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::CreateSubfolderInternal(
+ const nsAString& folderName, nsIMsgWindow* msgWindow,
+ nsIMsgFolder** aNewFolder) {
+ nsresult rv = CheckIfFolderExists(folderName, this, msgWindow);
+ // No need for an assertion: we already throw an alert.
+ if (NS_FAILED(rv)) return rv;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgStore->CreateFolder(this, folderName, aNewFolder);
+ if (rv == NS_MSG_ERROR_INVALID_FOLDER_NAME) {
+ ThrowAlertMsg("folderCreationFailed", msgWindow);
+ } else if (rv == NS_MSG_FOLDER_EXISTS) {
+ ThrowAlertMsg("folderExists", msgWindow);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ // we need to notify explicitly the flag change because it failed when we
+ // did AddSubfolder
+ (*aNewFolder)->OnFlagChange(mFlags);
+ (*aNewFolder)
+ ->SetPrettyName(
+ folderName); // because empty trash will create a new trash folder
+ NotifyFolderAdded(*aNewFolder);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool storeSupportsCompaction;
+ msgStore->GetSupportsCompaction(&storeSupportsCompaction);
+ nsTArray<RefPtr<nsIMsgFolder>> folderArray;
+ if (storeSupportsCompaction) {
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = rootFolder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t expungedBytes = 0;
+ for (auto folder : allDescendants) {
+ expungedBytes = 0;
+ if (folder) rv = folder->GetExpungedBytes(&expungedBytes);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (expungedBytes > 0) folderArray.AppendElement(folder);
+ }
+ }
+
+ if (folderArray.IsEmpty()) {
+ // Nothing to do - early out.
+ if (aListener) {
+ aListener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor =
+ do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv);
+ return folderCompactor->CompactFolders(folderArray, aListener, aMsgWindow);
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t expungedBytes = 0;
+ GetExpungedBytes(&expungedBytes);
+ bool supportsCompaction;
+ msgStore->GetSupportsCompaction(&supportsCompaction);
+ if (!supportsCompaction || expungedBytes == 0) {
+ // Nothing to do. Early out.
+ if (aListener) {
+ aListener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor =
+ do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderCompactor->CompactFolders({this}, aListener, aMsgWindow);
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::EmptyTrash(nsIUrlListener* aListener) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ rv = GetTrashFolder(getter_AddRefs(trashFolder));
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t flags;
+ trashFolder->GetFlags(&flags);
+ int32_t totalMessages = 0;
+ rv = trashFolder->GetTotalMessages(true, &totalMessages);
+ if (totalMessages <= 0) {
+ // Any folders to deal with?
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = trashFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (subFolders.IsEmpty()) {
+ return NS_OK;
+ }
+ }
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ rv = trashFolder->GetParent(getter_AddRefs(parentFolder));
+ if (NS_SUCCEEDED(rv) && parentFolder) {
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ trashFolder->GetDBTransferInfo(getter_AddRefs(transferInfo));
+ trashFolder->SetParent(nullptr);
+ parentFolder->PropagateDelete(trashFolder, true);
+ parentFolder->CreateSubfolder(u"Trash"_ns, nullptr);
+ nsCOMPtr<nsIMsgFolder> newTrashFolder;
+ rv = GetTrashFolder(getter_AddRefs(newTrashFolder));
+ if (NS_SUCCEEDED(rv) && newTrashFolder) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localTrash =
+ do_QueryInterface(newTrashFolder);
+ if (transferInfo) newTrashFolder->SetDBTransferInfo(transferInfo);
+ if (localTrash) localTrash->RefreshSizeOnDisk();
+ // update the summary totals so the front end will
+ // show the right thing for the new trash folder
+ // see bug #161999
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ newTrashFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ dbFolderInfo->SetNumUnreadMessages(0);
+ dbFolderInfo->SetNumMessages(0);
+ }
+ newTrashFolder->UpdateSummaryTotals(true);
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::IsChildOfTrash(bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ uint32_t parentFlags = 0;
+ *result = false;
+ bool isServer;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_FAILED(rv) || isServer) return NS_OK;
+
+ rv = GetFlags(&parentFlags); // this is the parent folder
+ if (parentFlags & nsMsgFolderFlags::Trash) {
+ *result = true;
+ return rv;
+ }
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ nsCOMPtr<nsIMsgFolder> thisFolder;
+ rv = QueryInterface(NS_GET_IID(nsIMsgFolder), getter_AddRefs(thisFolder));
+
+ while (!isServer) {
+ thisFolder->GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_OK;
+
+ rv = parentFolder->GetIsServer(&isServer);
+ if (NS_FAILED(rv) || isServer) return NS_OK;
+
+ rv = parentFolder->GetFlags(&parentFlags);
+ if (NS_FAILED(rv)) return NS_OK;
+
+ if (parentFlags & nsMsgFolderFlags::Trash) {
+ *result = true;
+ return rv;
+ }
+
+ thisFolder = parentFolder;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::DeleteSelf(nsIMsgWindow* msgWindow) {
+ nsresult rv;
+ bool isChildOfTrash;
+ IsChildOfTrash(&isChildOfTrash);
+
+ uint32_t folderFlags = 0;
+ GetFlags(&folderFlags);
+ // when deleting from trash, or virtual folder, just delete it.
+ if (isChildOfTrash || folderFlags & nsMsgFolderFlags::Virtual)
+ return nsMsgDBFolder::DeleteSelf(msgWindow);
+
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ rv = GetTrashFolder(getter_AddRefs(trashFolder));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgCopyService> copyService(
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyService->CopyFolder(this, trashFolder, true, nullptr, msgWindow);
+ }
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::ConfirmFolderDeletion(nsIMsgWindow* aMsgWindow,
+ nsIMsgFolder* aFolder,
+ bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ NS_ENSURE_ARG(aMsgWindow);
+ NS_ENSURE_ARG(aFolder);
+ nsCOMPtr<nsIDocShell> docShell;
+ aMsgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ bool confirmDeletion = true;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pPrefBranch->GetBoolPref("mailnews.confirm.moveFoldersToTrash",
+ &confirmDeletion);
+ if (confirmDeletion) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/localMsgs.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = aFolder->GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AutoTArray<nsString, 1> formatStrings = {folderName};
+
+ nsAutoString deleteFolderDialogTitle;
+ rv = bundle->GetStringFromName("pop3DeleteFolderDialogTitle",
+ deleteFolderDialogTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString deleteFolderButtonLabel;
+ rv = bundle->GetStringFromName("pop3DeleteFolderButtonLabel",
+ deleteFolderButtonLabel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString confirmationStr;
+ rv = bundle->FormatStringFromName("pop3MoveFolderToTrash", formatStrings,
+ confirmationStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog) {
+ int32_t buttonPressed = 0;
+ // Default the dialog to "cancel".
+ const uint32_t buttonFlags =
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1);
+ bool dummyValue = false;
+ rv = dialog->ConfirmEx(deleteFolderDialogTitle.get(),
+ confirmationStr.get(), buttonFlags,
+ deleteFolderButtonLabel.get(), nullptr, nullptr,
+ nullptr, &dummyValue, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aResult = !buttonPressed; // "ok" is in position 0
+ }
+ } else
+ *aResult = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::Rename(const nsAString& aNewName,
+ nsIMsgWindow* msgWindow) {
+ // Renaming to the same name is easy
+ if (mName.Equals(aNewName)) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ nsresult rv = GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_ERROR_NULL_POINTER;
+
+ rv = CheckIfFolderExists(aNewName, parentFolder, msgWindow);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgStore->RenameFolder(this, aNewName, getter_AddRefs(newFolder));
+ if (NS_FAILED(rv)) {
+ if (msgWindow)
+ (void)ThrowAlertMsg(
+ (rv == NS_MSG_FOLDER_EXISTS) ? "folderExists" : "folderRenameFailed",
+ msgWindow);
+ return rv;
+ }
+
+ int32_t count = mSubFolders.Count();
+ if (newFolder) {
+ // Because we just renamed the db, w/o setting the pretty name in it,
+ // we need to force the pretty name to be correct.
+ // SetPrettyName won't write the name to the db if it doesn't think the
+ // name has changed. This hack forces the pretty name to get set in the db.
+ // We could set the new pretty name on the db before renaming the .msf file,
+ // but if the rename failed, it would be out of sync.
+ newFolder->SetPrettyName(EmptyString());
+ newFolder->SetPrettyName(aNewName);
+ bool changed = false;
+ MatchOrChangeFilterDestination(newFolder, true /*case-insensitive*/,
+ &changed);
+ if (changed) AlertFilterChanged(msgWindow);
+
+ if (count > 0) newFolder->RenameSubFolders(msgWindow, this);
+
+ // Discover the subfolders inside this folder (this is recursive)
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ newFolder->GetSubFolders(dummy);
+
+ // the newFolder should have the same flags
+ newFolder->SetFlags(mFlags);
+ if (parentFolder) {
+ SetParent(nullptr);
+ parentFolder->PropagateDelete(this, false);
+ parentFolder->NotifyFolderAdded(newFolder);
+ }
+ // Forget our path, since this folder object renamed itself.
+ SetFilePath(nullptr);
+ newFolder->NotifyFolderEvent(kRenameCompleted);
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderRenamed(this, newFolder);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) {
+ nsresult rv = NS_OK;
+ mInitialized = true;
+
+ uint32_t flags;
+ oldFolder->GetFlags(&flags);
+ SetFlags(flags);
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = oldFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ nsString folderName;
+ rv = msgFolder->GetName(folderName);
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ AddSubfolder(folderName, getter_AddRefs(newFolder));
+ if (newFolder) {
+ newFolder->SetPrettyName(folderName);
+ bool changed = false;
+ msgFolder->MatchOrChangeFilterDestination(
+ newFolder, true /*case-insensitive*/, &changed);
+ if (changed) msgFolder->AlertFilterChanged(msgWindow);
+ newFolder->RenameSubFolders(msgWindow, msgFolder);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetPrettyName(nsAString& prettyName) {
+ return nsMsgDBFolder::GetPrettyName(prettyName);
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::SetPrettyName(const nsAString& aName) {
+ nsresult rv = nsMsgDBFolder::SetPrettyName(aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString folderName;
+ rv = GetStringProperty("folderName", folderName);
+ NS_ConvertUTF16toUTF8 utf8FolderName(mName);
+ return NS_FAILED(rv) || !folderName.Equals(utf8FolderName)
+ ? SetStringProperty("folderName", utf8FolderName)
+ : rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetName(nsAString& aName) {
+ ReadDBFolderInfo(false);
+ return nsMsgDBFolder::GetName(aName);
+}
+
+nsresult nsMsgLocalMailFolder::OpenDatabase() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> file;
+ rv = GetFilePath(getter_AddRefs(file));
+
+ rv = msgDBService->OpenFolderDB(this, true, getter_AddRefs(mDatabase));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) {
+ // check if we're a real folder by looking at the parent folder.
+ nsCOMPtr<nsIMsgFolder> parent;
+ GetParent(getter_AddRefs(parent));
+ if (parent) {
+ // This little dance creates an empty .msf file and then checks
+ // if the db is valid - this works if the folder is empty, which
+ // we don't have a direct way of checking.
+ nsCOMPtr<nsIMsgDatabase> db;
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(db));
+ if (db) {
+ UpdateSummaryTotals(true);
+ db->Close(true);
+ mDatabase = nullptr;
+ db = nullptr;
+ rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase));
+ if (NS_FAILED(rv)) mDatabase = nullptr;
+ }
+ }
+ } else if (NS_FAILED(rv))
+ mDatabase = nullptr;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) {
+ if (!db || !folderInfo || !mPath || mIsServer)
+ return NS_ERROR_NULL_POINTER; // ducarroz: should we use
+ // NS_ERROR_INVALID_ARG?
+
+ nsresult rv;
+ if (mDatabase)
+ rv = NS_OK;
+ else {
+ rv = OpenDatabase();
+
+ if (mAddListener && mDatabase) mDatabase->AddListener(this);
+ }
+
+ NS_IF_ADDREF(*db = mDatabase);
+ if (NS_SUCCEEDED(rv) && *db) rv = (*db)->GetDBFolderInfo(folderInfo);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ NS_ENSURE_ARG_POINTER(element);
+ nsresult rv = nsMsgDBFolder::ReadFromFolderCacheElem(element);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString utf8Name;
+ rv = element->GetCachedString("folderName", utf8Name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(utf8Name, mName);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::WriteToFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ NS_ENSURE_ARG_POINTER(element);
+ nsMsgDBFolder::WriteToFolderCacheElem(element);
+ return element->SetCachedString("folderName", NS_ConvertUTF16toUTF8(mName));
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+
+ bool isServer;
+ GetIsServer(&isServer);
+ *deletable = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::RefreshSizeOnDisk() {
+ int64_t oldFolderSize = mFolderSize;
+ // we set this to unknown to force it to get recalculated from disk
+ mFolderSize = kSizeUnknown;
+ if (NS_SUCCEEDED(GetSizeOnDisk(&mFolderSize)))
+ NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetSizeOnDisk(int64_t* aSize) {
+ NS_ENSURE_ARG_POINTER(aSize);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ // If this is the rootFolder, return 0 as a safe value.
+ if (NS_FAILED(rv) || isServer) mFolderSize = 0;
+
+ if (mFolderSize == kSizeUnknown) {
+ nsCOMPtr<nsIFile> file;
+ rv = GetFilePath(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Use a temporary variable so that we keep mFolderSize on kSizeUnknown
+ // if GetFileSize() fails.
+ int64_t folderSize;
+ rv = file->GetFileSize(&folderSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFolderSize = folderSize;
+ }
+ *aSize = mFolderSize;
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::GetTrashFolder(nsIMsgFolder** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv)) {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, result);
+ if (!*result) rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::DeleteMessages(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHeaders, nsIMsgWindow* msgWindow,
+ bool deleteStorage, bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ nsresult rv;
+
+ // shift delete case - (delete to trash is handled in EndMove)
+ // this is also the case when applying retention settings.
+ if (deleteStorage && !isMove) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete;
+ for (auto msgHdr : msgHeaders) {
+ uint32_t attachmentDetached = 0;
+ msgHdr->GetUint32Property("attachmentDetached", &attachmentDetached);
+ if (!attachmentDetached) {
+ hdrsToDelete.AppendElement(msgHdr);
+ }
+ }
+ MarkMsgsOnPop3Server(hdrsToDelete, POP3_DELETE);
+ }
+
+ bool isTrashFolder = mFlags & nsMsgFolderFlags::Trash;
+
+ // notify on delete from trash and shift-delete
+ if (!isMove && (deleteStorage || isTrashFolder)) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+ notifier->NotifyMsgsDeleted(msgHeaders);
+ }
+ }
+
+ if (!deleteStorage && !isTrashFolder) {
+ // We're moving the messages to trash folder. Start by kicking off a copy.
+ nsCOMPtr<nsIMsgFolder> trashFolder;
+ rv = GetTrashFolder(getter_AddRefs(trashFolder));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // When the copy completes, DeleteMessages() will be called again to
+ // perform the actual delete.
+ return copyService->CopyMessages(this, msgHeaders, trashFolder, true,
+ listener, msgWindow, allowUndo);
+ }
+ } else {
+ // Performing an _actual_ delete. There are two ways we got here:
+ // 1) We're deleting messages without moving to trash.
+ // 2) We're in the second phase of a Move (to trash or elsewhere). The
+ // copy succeeded, and now we need to delete the source messages.
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ rv = GetDatabaseWOReparse(getter_AddRefs(msgDB));
+ if (NS_SUCCEEDED(rv)) {
+ if (deleteStorage && isMove && GetDeleteFromServerOnMove())
+ MarkMsgsOnPop3Server(msgHeaders, POP3_DELETE);
+
+ nsCOMPtr<nsISupports> msgSupport;
+ rv = EnableNotifications(allMessageCountNotifications, false);
+ if (NS_SUCCEEDED(rv)) {
+ // First, delete the actual messages in the store.
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_SUCCEEDED(rv)) {
+ // Second, remove the message entries from the DB.
+ rv = msgStore->DeleteMessages(msgHeaders);
+ for (auto hdr : msgHeaders) {
+ rv = msgDB->DeleteHeader(hdr, nullptr, false, true);
+ }
+ }
+ } else if (rv == NS_MSG_FOLDER_BUSY) {
+ ThrowAlertMsg("deletingMsgsFailed", msgWindow);
+ }
+
+ // Let everyone know the operation has finished.
+ NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+ // NOTE: This reenabling also forces immediate recount + notification.
+ EnableNotifications(allMessageCountNotifications, true);
+ if (msgWindow) {
+ AutoCompact(msgWindow);
+ }
+ }
+ }
+
+ if (msgWindow && !isMove && (deleteStorage || isTrashFolder)) {
+ // Clear undo and redo stack.
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ txnMgr->Clear();
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) {
+ nsMsgMessageFlagType msgFlag = 0;
+ switch (aDispositionFlag) {
+ case nsIMsgFolder::nsMsgDispositionState_Replied:
+ msgFlag = nsMsgMessageFlags::Replied;
+ break;
+ case nsIMsgFolder::nsMsgDispositionState_Forwarded:
+ msgFlag = nsMsgMessageFlags::Forwarded;
+ break;
+ case nsIMsgFolder::nsMsgDispositionState_Redirected:
+ msgFlag = nsMsgMessageFlags::Redirected;
+ break;
+ default:
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv =
+ nsMsgDBFolder::AddMessageDispositionState(aMessage, aDispositionFlag);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->ChangeFlags({aMessage}, msgFlag, true);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::MarkMessagesRead(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, bool aMarkRead) {
+ nsresult rv = nsMsgDBFolder::MarkMessagesRead(aMessages, aMarkRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->ChangeFlags(aMessages, nsMsgMessageFlags::Read, aMarkRead);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::MarkMessagesFlagged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, bool aMarkFlagged) {
+ nsresult rv = nsMsgDBFolder::MarkMessagesFlagged(aMessages, aMarkFlagged);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->ChangeFlags(aMessages, nsMsgMessageFlags::Marked,
+ aMarkFlagged);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsMsgKey> thoseMarked;
+ EnableNotifications(allMessageCountNotifications, false);
+ rv = mDatabase->MarkAllRead(thoseMarked);
+ EnableNotifications(allMessageCountNotifications, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (thoseMarked.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ rv = MsgGetHeadersFromKeys(mDatabase, thoseMarked, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = msgStore->ChangeFlags(messages, nsMsgMessageFlags::Read, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+
+ // Setup a undo-state
+ if (aMsgWindow)
+ rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(),
+ thoseMarked.Length());
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::MarkThreadRead(nsIMsgThread* thread) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsMsgKey> thoseMarked;
+ rv = mDatabase->MarkThreadRead(thread, nullptr, thoseMarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (thoseMarked.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ rv = MsgGetHeadersFromKeys(mDatabase, thoseMarked, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = msgStore->ChangeFlags(messages, nsMsgMessageFlags::Read, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::InitCopyState(
+ nsISupports* aSupport, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgCopyServiceListener* listener, nsIMsgWindow* msgWindow,
+ bool isFolder, bool allowUndo) {
+ nsCOMPtr<nsIFile> path;
+
+ NS_ASSERTION(!mCopyState, "already copying a msg into this folder");
+ if (mCopyState) return NS_ERROR_FAILURE; // already has a copy in progress
+
+ // get mDatabase set, so we can use it to add new hdrs to this db.
+ // calling GetDatabase will set mDatabase - we use the comptr
+ // here to avoid doubling the refcnt on mDatabase. We don't care if this
+ // fails - we just want to give it a chance. It will definitely fail in
+ // nsLocalMailFolder::EndCopy because we will have written data to the folder
+ // and changed its size.
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ GetDatabaseWOReparse(getter_AddRefs(msgDB));
+ bool isLocked;
+
+ GetLocked(&isLocked);
+ if (isLocked) return NS_MSG_FOLDER_BUSY;
+
+ AcquireSemaphore(static_cast<nsIMsgLocalMailFolder*>(this));
+
+ mCopyState = new nsLocalMailCopyState();
+ NS_ENSURE_TRUE(mCopyState, NS_ERROR_OUT_OF_MEMORY);
+
+ mCopyState->m_dataBuffer = (char*)PR_CALLOC(COPY_BUFFER_SIZE + 1);
+ NS_ENSURE_TRUE(mCopyState->m_dataBuffer, NS_ERROR_OUT_OF_MEMORY);
+
+ mCopyState->m_dataBufferSize = COPY_BUFFER_SIZE;
+ mCopyState->m_destDB = msgDB;
+
+ mCopyState->m_srcSupport = aSupport;
+ mCopyState->m_messages = messages.Clone();
+ mCopyState->m_curCopyIndex = 0;
+ mCopyState->m_isMove = isMove;
+ mCopyState->m_isFolder = isFolder;
+ mCopyState->m_allowUndo = allowUndo;
+ mCopyState->m_msgWindow = msgWindow;
+ mCopyState->m_totalMsgCount = messages.Length();
+ if (listener) mCopyState->m_listener = listener;
+ mCopyState->m_copyingMultipleMessages = false;
+ mCopyState->m_wholeMsgInStream = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ if (mCopyState) mCopyState->m_destDB = nullptr;
+ return nsMsgDBFolder::OnAnnouncerGoingAway(instigator);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::OnCopyCompleted(nsISupports* srcSupport,
+ bool moveCopySucceeded) {
+ if (mCopyState && mCopyState->m_notifyFolderLoaded)
+ NotifyFolderEvent(kFolderLoaded);
+
+ (void)RefreshSizeOnDisk();
+ // we are the destination folder for a move/copy
+ bool haveSemaphore;
+ nsresult rv =
+ TestSemaphore(static_cast<nsIMsgLocalMailFolder*>(this), &haveSemaphore);
+ if (NS_SUCCEEDED(rv) && haveSemaphore)
+ ReleaseSemaphore(static_cast<nsIMsgLocalMailFolder*>(this));
+
+ if (mCopyState && !mCopyState->m_newMsgKeywords.IsEmpty() &&
+ mCopyState->m_newHdr) {
+ AddKeywordsToMessages({&*mCopyState->m_newHdr},
+ mCopyState->m_newMsgKeywords);
+ }
+ if (moveCopySucceeded && mDatabase) {
+ mDatabase->SetSummaryValid(true);
+ (void)CloseDBIfFolderNotOpen(false);
+ }
+
+ delete mCopyState;
+ mCopyState = nullptr;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return copyService->NotifyCompletion(
+ srcSupport, this, moveCopySucceeded ? NS_OK : NS_ERROR_FAILURE);
+}
+
+bool nsMsgLocalMailFolder::CheckIfSpaceForCopy(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* srcFolder,
+ nsISupports* srcSupports,
+ bool isMove,
+ int64_t totalMsgSize) {
+ bool spaceNotAvailable = true;
+ nsresult rv =
+ WarnIfLocalFileTooBig(msgWindow, totalMsgSize, &spaceNotAvailable);
+ if (NS_FAILED(rv) || spaceNotAvailable) {
+ if (isMove && srcFolder)
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ OnCopyCompleted(srcSupports, false);
+ return false;
+ }
+ return true;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::CopyMessages(nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& srcHdrs,
+ bool isMove, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener,
+ bool isFolder, bool allowUndo) {
+ nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder);
+ bool isServer;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_SUCCEEDED(rv) && isServer) {
+ NS_ERROR("Destination is the root folder. Cannot move/copy here");
+ if (isMove) srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ return OnCopyCompleted(srcSupport, false);
+ }
+
+ UpdateTimestamps(allowUndo);
+ nsCString protocolType;
+ rv = srcFolder->GetURI(protocolType);
+ protocolType.SetLength(protocolType.FindChar(':'));
+
+ // If we're offline and the source folder is imap or news, to do the
+ // copy the message bodies MUST reside in offline storage.
+ bool needOfflineBodies =
+ (WeAreOffline() && (protocolType.LowerCaseEqualsLiteral("imap") ||
+ protocolType.LowerCaseEqualsLiteral("news")));
+ int64_t totalMsgSize = 0;
+ bool allMsgsHaveOfflineStore = true;
+ for (auto message : srcHdrs) {
+ nsMsgKey key;
+ uint32_t msgSize;
+ message->GetMessageSize(&msgSize);
+
+ /* 200 is a per-message overhead to account for any extra data added
+ to the message.
+ */
+ totalMsgSize += msgSize + 200;
+
+ // Check if each source folder message has offline storage regardless
+ // of whether we're online or offline.
+ message->GetMessageKey(&key);
+ bool hasMsgOffline = false;
+ srcFolder->HasMsgOffline(key, &hasMsgOffline);
+ allMsgsHaveOfflineStore = allMsgsHaveOfflineStore && hasMsgOffline;
+
+ // If we're offline and not all messages are in offline storage, the copy
+ // or move can't occur and a notification for the user to download the
+ // messages is posted.
+ if (needOfflineBodies && !hasMsgOffline) {
+ if (isMove) srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ ThrowAlertMsg("cantMoveMsgWOBodyOffline", msgWindow);
+ return OnCopyCompleted(srcSupport, false);
+ }
+ }
+
+ if (!CheckIfSpaceForCopy(msgWindow, srcFolder, srcSupport, isMove,
+ totalMsgSize))
+ return NS_OK;
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool storeDidCopy = false;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsITransaction> undoTxn;
+ nsTArray<RefPtr<nsIMsgDBHdr>> dstHdrs;
+ rv = msgStore->CopyMessages(isMove, srcHdrs, this, dstHdrs,
+ getter_AddRefs(undoTxn), &storeDidCopy);
+ if (storeDidCopy) {
+ NS_ASSERTION(undoTxn, "if store does copy, it needs to add undo action");
+ if (msgWindow && undoTxn) {
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) txnMgr->DoTransaction(undoTxn);
+ }
+ if (isMove) {
+ srcFolder->NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ // If the store did the copy, like maildir, we need to mark messages on
+ // the server. Otherwise that's done in EndMove().
+ nsCOMPtr<nsIMsgLocalMailFolder> localDstFolder;
+ QueryInterface(NS_GET_IID(nsIMsgLocalMailFolder),
+ getter_AddRefs(localDstFolder));
+ if (localDstFolder) {
+ // If we are the trash and a local msg is being moved to us, mark the
+ // source for delete from server, if so configured.
+ if (mFlags & nsMsgFolderFlags::Trash) {
+ // If we're deleting on all moves, we'll mark this message for
+ // deletion when we call DeleteMessages on the source folder. So don't
+ // mark it for deletion here, in that case.
+ if (!GetDeleteFromServerOnMove()) {
+ localDstFolder->MarkMsgsOnPop3Server(dstHdrs, POP3_DELETE);
+ }
+ }
+ }
+ }
+
+ OnCopyCompleted(srcSupport, NS_SUCCEEDED(rv));
+ return rv;
+ }
+ // If the store doesn't do the copy, we'll stream the source messages into
+ // the target folder, using getMsgInputStream and getNewMsgOutputStream.
+
+ // don't update the counts in the dest folder until it is all over
+ EnableNotifications(allMessageCountNotifications, false);
+
+ // sort the message array by key
+ nsTArray<nsMsgKey> keyArray(srcHdrs.Length());
+ nsTArray<RefPtr<nsIMsgDBHdr>> sortedMsgs(srcHdrs.Length());
+ for (nsIMsgDBHdr* aMessage : srcHdrs) {
+ nsMsgKey key;
+ aMessage->GetMessageKey(&key);
+ keyArray.AppendElement(key);
+ }
+ keyArray.Sort();
+ rv = MessagesInKeyOrder(keyArray, srcFolder, sortedMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InitCopyState(srcSupport, sortedMsgs, isMove, listener, msgWindow,
+ isFolder, allowUndo);
+
+ if (NS_FAILED(rv)) {
+ ThrowAlertMsg("operationFailedFolderBusy", msgWindow);
+ (void)OnCopyCompleted(srcSupport, false);
+ return rv;
+ }
+
+ if (!protocolType.LowerCaseEqualsLiteral("mailbox")) {
+ // Copying from a non-mbox source, so we will be synthesising
+ // a "From " line and "X-Mozilla-*" headers before copying the message
+ // proper.
+ mCopyState->m_dummyEnvelopeNeeded = true;
+ nsParseMailMessageState* parseMsgState = new nsParseMailMessageState();
+ if (parseMsgState) {
+ nsCOMPtr<nsIMsgDatabase> msgDb;
+ mCopyState->m_parseMsgState = parseMsgState;
+ GetDatabaseWOReparse(getter_AddRefs(msgDb));
+ if (msgDb) parseMsgState->SetMailDB(msgDb);
+ }
+ }
+
+ // undo stuff
+ if (allowUndo) // no undo for folder move/copy or or move/copy from search
+ // window
+ {
+ RefPtr<nsLocalMoveCopyMsgTxn> msgTxn = new nsLocalMoveCopyMsgTxn;
+ if (msgTxn && NS_SUCCEEDED(msgTxn->Init(srcFolder, this, isMove))) {
+ msgTxn->SetMsgWindow(msgWindow);
+ if (isMove) {
+ if (mFlags & nsMsgFolderFlags::Trash)
+ msgTxn->SetTransactionType(nsIMessenger::eDeleteMsg);
+ else
+ msgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ } else
+ msgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ msgTxn.swap(mCopyState->m_undoMsgTxn);
+ }
+ }
+
+ if (srcHdrs.Length() > 1 &&
+ ((protocolType.LowerCaseEqualsLiteral("imap") &&
+ !allMsgsHaveOfflineStore) ||
+ protocolType.LowerCaseEqualsLiteral("mailbox"))) {
+ // For an imap source folder with more than one message to be copied that
+ // are not all in offline storage, this fetches all the messages from the
+ // imap server to do the copy. When source folder is "mailbox", this is not
+ // a concern since source messages are in local storage.
+ mCopyState->m_copyingMultipleMessages = true;
+ rv = CopyMessagesTo(keyArray, msgWindow, isMove);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("copy message failed");
+ (void)OnCopyCompleted(srcSupport, false);
+ }
+ } else {
+ // This obtains the source messages from local/offline storage to do the
+ // copy. Note: CopyMessageTo() actually handles one or more messages.
+ nsIMsgDBHdr* msgSupport = mCopyState->m_messages[0];
+ if (msgSupport) {
+ rv = CopyMessageTo(msgSupport, msgWindow, isMove);
+ if (NS_FAILED(rv)) {
+ NS_ASSERTION(false, "copy message failed");
+ (void)OnCopyCompleted(srcSupport, false);
+ }
+ }
+ }
+ // if this failed immediately, need to turn back on notifications and inform
+ // FE.
+ if (NS_FAILED(rv)) {
+ if (isMove) srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+ return rv;
+}
+
+// for srcFolder that are on different server than the dstFolder.
+// "this" is the parent of the new dest folder.
+nsresult nsMsgLocalMailFolder::CopyFolderAcrossServer(
+ nsIMsgFolder* srcFolder, nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener, bool moveMsgs) {
+ mInitialized = true;
+
+ nsString folderName;
+ srcFolder->GetName(folderName);
+
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ nsresult rv = CreateSubfolderInternal(folderName, msgWindow,
+ getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgEnumerator> messages;
+ rv = srcFolder->GetMessages(getter_AddRefs(messages));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgArray;
+ bool hasMoreElements = false;
+
+ if (messages) rv = messages->HasMoreElements(&hasMoreElements);
+
+ while (NS_SUCCEEDED(rv) && hasMoreElements) {
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ rv = messages->GetNext(getter_AddRefs(msg));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgArray.AppendElement(msg);
+ rv = messages->HasMoreElements(&hasMoreElements);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (msgArray.Length() > 0) // if only srcFolder has messages..
+ // Allow move of copied messages but keep source folder in place.
+ newMsgFolder->CopyMessages(srcFolder, msgArray, moveMsgs, msgWindow,
+ listener, true /* is folder*/,
+ false /* allowUndo */);
+ else {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(newMsgFolder);
+ if (localFolder) {
+ // normally these would get called from ::EndCopy when the last message
+ // was finished copying. But since there are no messages, we have to call
+ // them explicitly.
+ nsCOMPtr<nsISupports> srcSupports = do_QueryInterface(srcFolder);
+ localFolder->CopyAllSubFolders(srcFolder, msgWindow, listener, moveMsgs);
+ return localFolder->OnCopyCompleted(srcSupports, true);
+ }
+ }
+ return NS_OK; // otherwise the front-end will say Exception::CopyFolder
+}
+
+nsresult // copy the sub folders
+nsMsgLocalMailFolder::CopyAllSubFolders(nsIMsgFolder* srcFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener,
+ bool isMove) {
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = srcFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* folder : subFolders) {
+ CopyFolderAcrossServer(folder, msgWindow, listener, isMove);
+ }
+ return NS_OK;
+}
+
+// "this" is the destination (parent) folder that srcFolder is copied to.
+NS_IMETHODIMP
+nsMsgLocalMailFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ nsresult rv;
+ bool sameServer;
+ rv = IsOnSameServer(this, srcFolder, &sameServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (sameServer && isMoveFolder) {
+ // Do a pure folder move within the same Local Folder account/server. where
+ // "pure" means the folder AND messages are copied to the Local Folders
+ // destination and then both are removed from source account.
+ rv = CopyFolderLocal(srcFolder, isMoveFolder, msgWindow, listener);
+ } else {
+ // !sameServer OR it's a copy. Unit tests expect a successful folder
+ // copy within Local Folders account/server even though the UI forbids copy
+ // and only allows moves inside the same account. CopyFolderAcrossServer(),
+ // called below, handles the folder copy within Local Folders (needed by
+ // unit tests) and it handles the folder move or copy from another account
+ // or server into Local Folders. The move from another account is "impure"
+ // since just the messages are moved but the source folder remains in place.
+ rv = CopyFolderAcrossServer(srcFolder, msgWindow, listener, isMoveFolder);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::CopyFolderLocal(nsIMsgFolder* srcFolder,
+ bool isMoveFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* aListener) {
+ mInitialized = true;
+ bool isChildOfTrash;
+ nsresult rv = IsChildOfTrash(&isChildOfTrash);
+ if (NS_SUCCEEDED(rv) && isChildOfTrash) {
+ // do it just for the parent folder (isMoveFolder is true for parent only)
+ // if we are deleting/moving a folder tree don't confirm for rss folders.
+ if (isMoveFolder) {
+ // if there's a msgWindow, confirm the deletion
+ if (msgWindow) {
+ bool okToDelete = false;
+ ConfirmFolderDeletion(msgWindow, srcFolder, &okToDelete);
+ if (!okToDelete) return NS_MSG_ERROR_COPY_FOLDER_ABORTED;
+ }
+ // if we are moving a favorite folder to trash, we should clear the
+ // favorites flag so it gets removed from the view.
+ srcFolder->ClearFlag(nsMsgFolderFlags::Favorite);
+ }
+
+ bool match = false;
+ srcFolder->MatchOrChangeFilterDestination(nullptr, false, &match);
+ if (match && msgWindow) {
+ bool confirmed = false;
+ srcFolder->ConfirmFolderDeletionForFilter(msgWindow, &confirmed);
+ if (!confirmed) return NS_MSG_ERROR_COPY_FOLDER_ABORTED;
+ }
+ }
+
+ nsAutoString newFolderName;
+ nsAutoString folderName;
+ rv = srcFolder->GetName(folderName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isMoveFolder) {
+ rv = CheckIfFolderExists(folderName, this, msgWindow);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ // If folder name already exists in destination, generate a new unique name.
+ bool containsChild = true;
+ uint32_t i;
+ for (i = 1; containsChild; i++) {
+ newFolderName.Assign(folderName);
+ if (i > 1) {
+ // This could be localizable but Toolkit is fine without it, see
+ // mozilla/toolkit/content/contentAreaUtils.js::uniqueFile()
+ newFolderName.Append('(');
+ newFolderName.AppendInt(i);
+ newFolderName.Append(')');
+ }
+ rv = ContainsChildNamed(newFolderName, &containsChild);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // 'i' is one more than the number of iterations done
+ // and the number tacked onto the name of the folder.
+ if (i > 2 && !isChildOfTrash) {
+ // Folder name already exists, ask if rename is OK.
+ // If moving to Trash, don't ask and do it.
+ if (!ConfirmAutoFolderRename(msgWindow, folderName, newFolderName))
+ return NS_MSG_ERROR_COPY_FOLDER_ABORTED;
+ }
+ }
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return msgStore->CopyFolder(srcFolder, this, isMoveFolder, msgWindow,
+ aListener, newFolderName);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* msgToReplace,
+ bool isDraftOrTemplate,
+ uint32_t newMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ nsParseMailMessageState* parseMsgState = nullptr;
+ int64_t fileSize = 0;
+
+ nsCOMPtr<nsISupports> fileSupport(aFile);
+
+ aFile->GetFileSize(&fileSize);
+ if (!CheckIfSpaceForCopy(msgWindow, nullptr, fileSupport, false, fileSize))
+ return NS_OK;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ if (msgToReplace) messages.AppendElement(msgToReplace);
+
+ rv = InitCopyState(fileSupport, messages, msgToReplace ? true : false,
+ listener, msgWindow, false, false);
+ if (NS_SUCCEEDED(rv)) {
+ if (mCopyState) {
+ mCopyState->m_newMsgKeywords = aNewMsgKeywords;
+ mCopyState->m_flags = newMsgFlags;
+ }
+
+ parseMsgState = new nsParseMailMessageState();
+ NS_ENSURE_TRUE(parseMsgState, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIMsgDatabase> msgDb;
+ mCopyState->m_parseMsgState = parseMsgState;
+ GetDatabaseWOReparse(getter_AddRefs(msgDb));
+ if (msgDb) parseMsgState->SetMailDB(msgDb);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+
+ // All or none for adding a message file to the store
+ if (NS_SUCCEEDED(rv) && fileSize > PR_INT32_MAX)
+ rv = NS_ERROR_ILLEGAL_VALUE; // may need error code for max msg size
+
+ if (NS_SUCCEEDED(rv) && inputStream) {
+ char buffer[5];
+ uint32_t readCount;
+ rv = inputStream->Read(buffer, 5, &readCount);
+ if (NS_SUCCEEDED(rv)) {
+ if (strncmp(buffer, "From ", 5))
+ mCopyState->m_dummyEnvelopeNeeded = true;
+ nsCOMPtr<nsISeekableStream> seekableStream =
+ do_QueryInterface(inputStream, &rv);
+ if (NS_SUCCEEDED(rv))
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ }
+ }
+
+ mCopyState->m_wholeMsgInStream = true;
+ if (NS_SUCCEEDED(rv)) rv = BeginCopy();
+
+ if (NS_SUCCEEDED(rv)) rv = CopyData(inputStream, (int32_t)fileSize);
+
+ if (NS_SUCCEEDED(rv)) rv = EndCopy(true);
+
+ // mDatabase should have been initialized above - if we got msgDb
+ // If we were going to delete, here is where we would do it. But because
+ // existing code already supports doing those deletes, we are just going
+ // to end the copy.
+ if (NS_SUCCEEDED(rv) && msgToReplace && mDatabase)
+ rv = OnCopyCompleted(fileSupport, true);
+
+ if (inputStream) inputStream->Close();
+ }
+
+ if (NS_FAILED(rv)) (void)OnCopyCompleted(fileSupport, false);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsILocalMailIncomingServer> localMailServer =
+ do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ // XXX todo, move all this into nsILocalMailIncomingServer's GetNewMail
+ // so that we don't have to have RSS foo here.
+ nsCOMPtr<nsIRssIncomingServer> rssServer = do_QueryInterface(server, &rv);
+ mozilla::Unused << rssServer;
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> resultURI;
+ return localMailServer->GetNewMail(aWindow, aListener, this,
+ getter_AddRefs(resultURI));
+ }
+
+ nsCOMPtr<nsIMsgFolder> inbox;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox,
+ getter_AddRefs(inbox));
+ }
+ nsCOMPtr<nsIMsgLocalMailFolder> localInbox = do_QueryInterface(inbox, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ bool valid = false;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // this will kick off a reparse if the db is out of date.
+ rv = localInbox->GetDatabaseWithReparse(nullptr, aWindow,
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> resultURI;
+ db->GetSummaryValid(&valid);
+ rv = valid ? localMailServer->GetNewMail(aWindow, aListener, inbox,
+ getter_AddRefs(resultURI))
+ : localInbox->SetCheckForNewMessagesAfterParsing(true);
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::WriteStartOfNewMessage() {
+ // If moving, delete the message in source folder that was just copied.
+ // It will have index one less than the current index.
+ // But only do this if source folder is imap.
+ // Could be optimized (DeleteMessages() operate on non-array)?
+ nsresult rv;
+ uint32_t idx = mCopyState->m_curCopyIndex;
+ if (mCopyState->m_isMove && idx) {
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(mCopyState->m_srcSupport, &rv);
+ if (NS_SUCCEEDED(rv) && srcFolder) {
+ // Delete source messages as we go only if they come from
+ // an imap folder.
+ nsCString protocolType;
+ if (NS_SUCCEEDED(srcFolder->GetURI(protocolType))) {
+ if (StringHead(protocolType, 5).LowerCaseEqualsLiteral("imap:")) {
+ // Create "array" of one message header to delete
+ idx--;
+ if (idx < mCopyState->m_messages.Length()) {
+ // Above check avoids a possible MOZ_CRASH after error recovery.
+ RefPtr<nsIMsgDBHdr> msg = mCopyState->m_messages[idx];
+ srcFolder->DeleteMessages({msg}, mCopyState->m_msgWindow, true,
+ true, nullptr, mCopyState->m_allowUndo);
+ }
+ }
+ }
+ }
+ }
+
+ // CopyFileMessage() and CopyMessages() from servers other than pop3
+ if (mCopyState->m_parseMsgState) {
+ // Make sure the parser knows where the "From " separator is.
+ // A hack for Bug 1734847.
+ // If we were using nsMsgMailboxParser, that would handle it automatically.
+ // But we're using the base class (nsParseMailMessageState) which doesn't.
+ mCopyState->m_parseMsgState->m_envelope_pos =
+ mCopyState->m_parseMsgState->m_position;
+
+ if (mCopyState->m_parseMsgState->m_newMsgHdr) {
+ mCopyState->m_parseMsgState->m_newMsgHdr->GetMessageKey(
+ &mCopyState->m_curDstKey);
+ }
+ mCopyState->m_parseMsgState->SetState(
+ nsIMsgParseMailMsgState::ParseHeadersState);
+ }
+ if (mCopyState->m_dummyEnvelopeNeeded) {
+ nsCString result;
+ nsAutoCString nowStr;
+ MsgGenerateNowStr(nowStr);
+ result.AppendLiteral("From - ");
+ result.Append(nowStr);
+ result.Append(MSG_LINEBREAK);
+
+ uint32_t bytesWritten;
+ mCopyState->m_fileStream->Write(result.get(), result.Length(),
+ &bytesWritten);
+ if (mCopyState->m_parseMsgState) {
+ mCopyState->m_parseMsgState->ParseAFolderLine(result.get(),
+ result.Length());
+ // Make sure the parser knows where the header block begins.
+ // Another hack for Bug 1734847.
+ mCopyState->m_parseMsgState->m_headerstartpos =
+ mCopyState->m_parseMsgState->m_position;
+ }
+
+ // *** jt - hard code status line for now; come back later
+ char statusStrBuf[50];
+ if (mCopyState->m_curCopyIndex < mCopyState->m_messages.Length()) {
+ uint32_t dbFlags = 0;
+ mCopyState->m_messages[mCopyState->m_curCopyIndex]->GetFlags(&dbFlags);
+
+ // write out x-mozilla-status, but make sure we don't write out
+ // nsMsgMessageFlags::Offline
+ PR_snprintf(
+ statusStrBuf, sizeof(statusStrBuf),
+ X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK,
+ dbFlags &
+ ~(nsMsgMessageFlags::RuntimeOnly | nsMsgMessageFlags::Offline) &
+ 0x0000FFFF);
+ } else
+ strcpy(statusStrBuf, "X-Mozilla-Status: 0001" MSG_LINEBREAK);
+ mCopyState->m_fileStream->Write(statusStrBuf, strlen(statusStrBuf),
+ &bytesWritten);
+ if (mCopyState->m_parseMsgState)
+ mCopyState->m_parseMsgState->ParseAFolderLine(statusStrBuf,
+ strlen(statusStrBuf));
+ result = "X-Mozilla-Status2: 00000000" MSG_LINEBREAK;
+ mCopyState->m_fileStream->Write(result.get(), result.Length(),
+ &bytesWritten);
+ if (mCopyState->m_parseMsgState)
+ mCopyState->m_parseMsgState->ParseAFolderLine(result.get(),
+ result.Length());
+ result = X_MOZILLA_KEYWORDS;
+ mCopyState->m_fileStream->Write(result.get(), result.Length(),
+ &bytesWritten);
+ if (mCopyState->m_parseMsgState)
+ mCopyState->m_parseMsgState->ParseAFolderLine(result.get(),
+ result.Length());
+ mCopyState->m_fromLineSeen = true;
+ } else
+ mCopyState->m_fromLineSeen = false;
+
+ mCopyState->m_curCopyIndex++;
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::InitCopyMsgHdrAndFileStream() {
+ nsresult rv = GetMsgStore(getter_AddRefs(mCopyState->m_msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mCopyState->m_msgStore->GetNewMsgOutputStream(
+ this, getter_AddRefs(mCopyState->m_newHdr),
+ getter_AddRefs(mCopyState->m_fileStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mCopyState->m_parseMsgState)
+ mCopyState->m_parseMsgState->SetNewMsgHdr(mCopyState->m_newHdr);
+ return rv;
+}
+
+// nsICopyMessageListener
+NS_IMETHODIMP nsMsgLocalMailFolder::BeginCopy() {
+ if (!mCopyState) return NS_ERROR_NULL_POINTER;
+
+ if (!mCopyState->m_copyingMultipleMessages) {
+ nsresult rv = InitCopyMsgHdrAndFileStream();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // The output stream may or may not be set already, depending upon all kinds
+ // of inscrutable conditions. This needs cleaning up (see Bug 1731177).
+ if (!mCopyState->m_fileStream) {
+ return NS_OK;
+ }
+
+ int32_t messageIndex = (mCopyState->m_copyingMultipleMessages)
+ ? mCopyState->m_curCopyIndex - 1
+ : mCopyState->m_curCopyIndex;
+ NS_ASSERTION(!mCopyState->m_copyingMultipleMessages || messageIndex >= 0,
+ "messageIndex invalid");
+ // by the time we get here, m_curCopyIndex is 1 relative because
+ // WriteStartOfNewMessage increments it
+ if (messageIndex < (int32_t)mCopyState->m_messages.Length()) {
+ mCopyState->m_message = mCopyState->m_messages[messageIndex];
+ } else {
+ mCopyState->m_message = nullptr;
+ }
+ // The flags of the source message can get changed when it is deleted, so
+ // save them here.
+ if (mCopyState->m_message)
+ mCopyState->m_message->GetFlags(&(mCopyState->m_flags));
+ DisplayMoveCopyStatusMsg();
+ if (mCopyState->m_listener)
+ mCopyState->m_listener->OnProgress(mCopyState->m_curCopyIndex,
+ mCopyState->m_totalMsgCount);
+ // if we're copying more than one message, StartMessage will handle this.
+ return !mCopyState->m_copyingMultipleMessages ? WriteStartOfNewMessage()
+ : NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::CopyData(nsIInputStream* aIStream,
+ int32_t aLength) {
+ // check to make sure we have control of the write.
+ bool haveSemaphore;
+ nsresult rv = NS_OK;
+
+ rv = TestSemaphore(static_cast<nsIMsgLocalMailFolder*>(this), &haveSemaphore);
+ if (NS_FAILED(rv)) return rv;
+ if (!haveSemaphore) return NS_MSG_FOLDER_BUSY;
+
+ if (!mCopyState) return NS_ERROR_OUT_OF_MEMORY;
+
+ uint32_t readCount;
+ // allocate one extra byte for '\0' at the end and another extra byte at the
+ // front to insert a '>' if we have a "From" line
+ // allocate 2 more for crlf that may be needed for those without crlf at end
+ // of file
+ if (aLength + mCopyState->m_leftOver + 4 > mCopyState->m_dataBufferSize) {
+ char* newBuffer = (char*)PR_REALLOC(mCopyState->m_dataBuffer,
+ aLength + mCopyState->m_leftOver + 4);
+ if (!newBuffer) return NS_ERROR_OUT_OF_MEMORY;
+
+ mCopyState->m_dataBuffer = newBuffer;
+ mCopyState->m_dataBufferSize = aLength + mCopyState->m_leftOver + 3;
+ }
+
+ rv = aIStream->Read(mCopyState->m_dataBuffer + mCopyState->m_leftOver + 1,
+ aLength, &readCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mCopyState->m_leftOver += readCount;
+ mCopyState->m_dataBuffer[mCopyState->m_leftOver + 1] = '\0';
+ char* start = mCopyState->m_dataBuffer + 1;
+ char* endBuffer = mCopyState->m_dataBuffer + mCopyState->m_leftOver + 1;
+
+ uint32_t lineLength;
+ uint32_t bytesWritten;
+
+ while (1) {
+ char* end = PL_strnpbrk(start, "\r\n", endBuffer - start);
+ if (!end) {
+ mCopyState->m_leftOver -= (start - mCopyState->m_dataBuffer - 1);
+ // In CopyFileMessage, a complete message is being copied in a single
+ // call to CopyData, and if it does not have a LINEBREAK at the EOF,
+ // then end will be null after reading the last line, and we need
+ // to append the LINEBREAK to the buffer to enable transfer of the last
+ // line.
+ if (mCopyState->m_wholeMsgInStream) {
+ end = start + mCopyState->m_leftOver;
+ memcpy(end, MSG_LINEBREAK "\0", MSG_LINEBREAK_LEN + 1);
+ } else {
+ memmove(mCopyState->m_dataBuffer + 1, start, mCopyState->m_leftOver);
+ break;
+ }
+ }
+
+ // need to set the linebreak_len each time
+ uint32_t linebreak_len = 1; // assume CR or LF
+ if (*end == '\r' && *(end + 1) == '\n') linebreak_len = 2; // CRLF
+
+ if (!mCopyState->m_fromLineSeen) {
+ mCopyState->m_fromLineSeen = true;
+ NS_ASSERTION(strncmp(start, "From ", 5) == 0,
+ "Fatal ... bad message format\n");
+ } else if (strncmp(start, "From ", 5) == 0) {
+ // if we're at the beginning of the buffer, we've reserved a byte to
+ // insert a '>'. If we're in the middle, we're overwriting the previous
+ // line ending, but we've already written it to m_fileStream, so it's OK.
+ *--start = '>';
+ }
+
+ if (!mCopyState->m_fileStream) {
+ ThrowAlertMsg("copyMsgWriteFailed", mCopyState->m_msgWindow);
+ mCopyState->m_writeFailed = true;
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ lineLength = end - start + linebreak_len;
+ rv = mCopyState->m_fileStream->Write(start, lineLength, &bytesWritten);
+ if (bytesWritten != lineLength || NS_FAILED(rv)) {
+ ThrowAlertMsg("copyMsgWriteFailed", mCopyState->m_msgWindow);
+ mCopyState->m_writeFailed = true;
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+
+ if (mCopyState->m_parseMsgState)
+ mCopyState->m_parseMsgState->ParseAFolderLine(start, lineLength);
+
+ start = end + linebreak_len;
+ if (start >= endBuffer) {
+ mCopyState->m_leftOver = 0;
+ break;
+ }
+ }
+ return rv;
+}
+
+void nsMsgLocalMailFolder::CopyPropertiesToMsgHdr(nsIMsgDBHdr* destHdr,
+ nsIMsgDBHdr* srcHdr,
+ bool aIsMove) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCString dontPreserve;
+
+ // These preferences exist so that extensions can control which properties
+ // are preserved in the database when a message is moved or copied. All
+ // properties are preserved except those listed in these preferences
+ if (aIsMove)
+ prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnMove",
+ dontPreserve);
+ else
+ prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnCopy",
+ dontPreserve);
+
+ CopyHdrPropertiesWithSkipList(destHdr, srcHdr, dontPreserve);
+}
+
+void nsMsgLocalMailFolder::CopyHdrPropertiesWithSkipList(
+ nsIMsgDBHdr* destHdr, nsIMsgDBHdr* srcHdr, const nsCString& skipList) {
+ nsTArray<nsCString> properties;
+ nsresult rv = srcHdr->GetProperties(properties);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // We'll add spaces at beginning and end so we can search for space-name-space
+ nsCString dontPreserveEx(" "_ns);
+ dontPreserveEx.Append(skipList);
+ dontPreserveEx.Append(' ');
+
+ nsCString sourceString;
+ for (auto property : properties) {
+ nsAutoCString propertyEx(" "_ns);
+ propertyEx.Append(property);
+ propertyEx.Append(' ');
+ if (dontPreserveEx.Find(propertyEx) != -1) // -1 is not found
+ continue;
+
+ srcHdr->GetStringProperty(property.get(), sourceString);
+ destHdr->SetStringProperty(property.get(), sourceString);
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMsgLocalMailFolder::EndCopy(bool aCopySucceeded) {
+ if (!mCopyState) return NS_OK;
+
+ // we are the destination folder for a move/copy
+ nsresult rv = aCopySucceeded ? NS_OK : NS_ERROR_FAILURE;
+
+ if (!aCopySucceeded || mCopyState->m_writeFailed) {
+ if (mCopyState->m_fileStream) {
+ if (mCopyState->m_curDstKey != nsMsgKey_None)
+ mCopyState->m_msgStore->DiscardNewMessage(mCopyState->m_fileStream,
+ mCopyState->m_newHdr);
+ mCopyState->m_fileStream->Close();
+ }
+
+ if (!mCopyState->m_isMove) {
+ // passing true because the messages that have been successfully
+ // copied have their corresponding hdrs in place. The message that has
+ // failed has been truncated so the msf file and berkeley mailbox
+ // are in sync.
+ (void)OnCopyCompleted(mCopyState->m_srcSupport, true);
+ // enable the dest folder
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+ return NS_OK;
+ }
+
+ bool multipleCopiesFinished =
+ (mCopyState->m_curCopyIndex >= mCopyState->m_totalMsgCount);
+
+ RefPtr<nsLocalMoveCopyMsgTxn> localUndoTxn = mCopyState->m_undoMsgTxn;
+
+ NS_ASSERTION(mCopyState->m_leftOver == 0,
+ "whoops, something wrong with previous copy");
+ mCopyState->m_leftOver = 0; // reset to 0.
+ // need to reset this in case we're move/copying multiple msgs.
+ mCopyState->m_fromLineSeen = false;
+
+ // flush the copied message. We need a close at the end to get the
+ // file size and time updated correctly.
+ //
+ // These filestream closes are handled inconsistently in the code. In some
+ // cases, this is done in EndMessage, while in others it is done here in
+ // EndCopy. When we do the close in EndMessage, we'll set
+ // mCopyState->m_fileStream to null since it is no longer needed, and detect
+ // here the null stream so we know that we don't have to close it here.
+ //
+ // Similarly, m_parseMsgState->GetNewMsgHdr() returns a null hdr if the hdr
+ // has already been processed by EndMessage so it is not doubly added here.
+
+ if (mCopyState->m_fileStream) {
+ rv = FinishNewLocalMessage(mCopyState->m_fileStream, mCopyState->m_newHdr,
+ mCopyState->m_msgStore,
+ mCopyState->m_parseMsgState);
+ if (NS_SUCCEEDED(rv) && mCopyState->m_newHdr)
+ mCopyState->m_newHdr->GetMessageKey(&mCopyState->m_curDstKey);
+ if (multipleCopiesFinished)
+ mCopyState->m_fileStream->Close();
+ else
+ mCopyState->m_fileStream->Flush();
+ }
+ // Copy the header to the new database
+ if (mCopyState->m_message) {
+ // CopyMessages() goes here, and CopyFileMessages() with metadata to save;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ if (!mCopyState->m_parseMsgState) {
+ if (mCopyState->m_destDB) {
+ if (mCopyState->m_newHdr) {
+ newHdr = mCopyState->m_newHdr;
+ CopyHdrPropertiesWithSkipList(newHdr, mCopyState->m_message,
+ "storeToken msgOffset"_ns);
+ // We need to copy more than just what UpdateNewMsgHdr does. In fact,
+ // I think we want to copy almost every property other than
+ // storeToken and msgOffset.
+ mCopyState->m_destDB->AddNewHdrToDB(newHdr, true);
+ } else {
+ rv = mCopyState->m_destDB->CopyHdrFromExistingHdr(
+ mCopyState->m_curDstKey, mCopyState->m_message, true,
+ getter_AddRefs(newHdr));
+ }
+ uint32_t newHdrFlags;
+ if (newHdr) {
+ // turn off offline flag - it's not valid for local mail folders.
+ newHdr->AndFlags(~nsMsgMessageFlags::Offline, &newHdrFlags);
+ mCopyState->m_destMessages.AppendElement(newHdr);
+ }
+ }
+ // we can do undo with the dest folder db, see bug #198909
+ // else
+ // mCopyState->m_undoMsgTxn = nullptr; // null out the transaction
+ // // because we can't undo w/o
+ // // the msg db
+ }
+
+ // if we plan on allowing undo, (if we have a mCopyState->m_parseMsgState or
+ // not) we need to save the source and dest keys on the undo txn. see bug
+ // #179856 for details
+ bool isImap;
+ if (NS_SUCCEEDED(rv) && localUndoTxn) {
+ localUndoTxn->GetSrcIsImap(&isImap);
+ if (!isImap || !mCopyState->m_copyingMultipleMessages) {
+ nsMsgKey aKey;
+ mCopyState->m_message->GetMessageKey(&aKey);
+ localUndoTxn->AddSrcKey(aKey);
+ localUndoTxn->AddDstKey(mCopyState->m_curDstKey);
+ }
+ }
+ }
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ // CopyFileMessage() and CopyMessages() from servers other than mailbox
+ if (mCopyState->m_parseMsgState) {
+ nsCOMPtr<nsIMsgDatabase> msgDb;
+ mCopyState->m_parseMsgState->FinishHeader();
+ GetDatabaseWOReparse(getter_AddRefs(msgDb));
+ if (msgDb) {
+ nsresult result =
+ mCopyState->m_parseMsgState->GetNewMsgHdr(getter_AddRefs(newHdr));
+ // we need to copy newHdr because mCopyState will get cleared
+ // in OnCopyCompleted, but we need OnCopyCompleted to know about
+ // the newHdr, via mCopyState. And we send a notification about newHdr
+ // after OnCopyCompleted.
+ mCopyState->m_newHdr = newHdr;
+ if (NS_SUCCEEDED(result) && newHdr) {
+ // Copy message metadata.
+ uint32_t newFlags;
+ newHdr->GetFlags(&newFlags);
+ if (mCopyState->m_message) {
+ // Propagate the new flag on an imap to local folder filter action
+ // Flags may get changed when deleting the original source message in
+ // IMAP. We have a copy of the original flags, but parseMsgState has
+ // already tried to decide what those flags should be. Who to believe?
+ // Let's deal here with the flags that might get changed, Read and
+ // New, and trust upstream code for everything else. However,
+ // we need to carry over HasRe since the subject is copied over
+ // from the original.
+ uint32_t carryOver = nsMsgMessageFlags::New |
+ nsMsgMessageFlags::Read |
+ nsMsgMessageFlags::HasRe;
+ newHdr->SetFlags((newFlags & ~carryOver) |
+ ((mCopyState->m_flags) & carryOver));
+
+ // Copy other message properties.
+ CopyPropertiesToMsgHdr(newHdr, mCopyState->m_message,
+ mCopyState->m_isMove);
+ } else {
+ // Carry over some of the enforced flags, but do not clear any of the
+ // already set flags (for example nsMsgMessageFlags::Queued or
+ // nsMsgMessageFlags::MDNReportSent).
+ uint32_t carryOver = nsMsgMessageFlags::New |
+ nsMsgMessageFlags::Read |
+ nsMsgMessageFlags::Marked;
+ newHdr->SetFlags((newFlags & ~carryOver) |
+ ((mCopyState->m_flags) & carryOver));
+ }
+ msgDb->AddNewHdrToDB(newHdr, true);
+ if (localUndoTxn) {
+ // ** jt - recording the message size for possible undo use; the
+ // message size is different for pop3 and imap4 messages
+ uint32_t msgSize;
+ newHdr->GetMessageSize(&msgSize);
+ localUndoTxn->AddDstMsgSize(msgSize);
+ }
+
+ mCopyState->m_destMessages.AppendElement(newHdr);
+ }
+ // msgDb->SetSummaryValid(true);
+ // msgDb->Commit(nsMsgDBCommitType::kLargeCommit);
+ } else
+ mCopyState->m_undoMsgTxn = nullptr; // null out the transaction because
+ // we can't undo w/o the msg db
+
+ mCopyState->m_parseMsgState->Clear();
+ if (mCopyState->m_listener) // CopyFileMessage() only
+ mCopyState->m_listener->SetMessageKey(mCopyState->m_curDstKey);
+ }
+
+ if (!multipleCopiesFinished && !mCopyState->m_copyingMultipleMessages) {
+ // CopyMessages() goes here; CopyFileMessage() never gets in here because
+ // curCopyIndex will always be less than the mCopyState->m_totalMsgCount
+ nsIMsgDBHdr* aSupport = mCopyState->m_messages[mCopyState->m_curCopyIndex];
+ rv = CopyMessageTo(aSupport, mCopyState->m_msgWindow, mCopyState->m_isMove);
+ } else {
+ // If we have some headers, then there is a source, so notify
+ // itemMoveCopyCompleted. If we don't have any headers already, (eg save as
+ // draft, send) then notify itemAdded. This notification is done after the
+ // messages are deleted, so that saving a new draft of a message works
+ // correctly -- first an itemDeleted is sent for the old draft, then an
+ // itemAdded for the new draft.
+ uint32_t numHdrs = mCopyState->m_messages.Length();
+
+ if (multipleCopiesFinished && numHdrs && !mCopyState->m_isFolder) {
+ // we need to send this notification before we delete the source messages,
+ // because deleting the source messages clears out the src msg db hdr.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgsMoveCopyCompleted(mCopyState->m_isMove,
+ mCopyState->m_messages, this,
+ mCopyState->m_destMessages);
+ }
+ }
+
+ // Now allow folder or nested folders move of their msgs from Local Folders.
+ // The original source folder(s) remain, just the msgs are moved (after
+ // copy they are deleted).
+ if (multipleCopiesFinished) {
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ srcFolder = do_QueryInterface(mCopyState->m_srcSupport);
+ if (mCopyState->m_isFolder) {
+ // Copy or move all subfolders then notify completion
+ CopyAllSubFolders(srcFolder, nullptr, nullptr, mCopyState->m_isMove);
+ }
+
+ // If this is done on move of selected messages between "mailbox" folders,
+ // the source messages are never deleted. So do this only on msg copy.
+ if (!mCopyState->m_isMove) {
+ if (mCopyState->m_msgWindow && mCopyState->m_undoMsgTxn) {
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ mCopyState->m_msgWindow->GetTransactionManager(
+ getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ RefPtr<nsLocalMoveCopyMsgTxn> txn = mCopyState->m_undoMsgTxn;
+ txnMgr->DoTransaction(txn);
+ }
+ }
+
+ // enable the dest folder
+ EnableNotifications(allMessageCountNotifications, true);
+ if (srcFolder && !mCopyState->m_isFolder) {
+ // I'm not too sure of the proper location of this event. It seems to
+ // need to be after the EnableNotifications, or the folder counts can
+ // be incorrect during the kDeleteOrMoveMsgCompleted call.
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ }
+ (void)OnCopyCompleted(mCopyState->m_srcSupport, true);
+ }
+ }
+ // Send the itemAdded notification in case we didn't send the
+ // itemMoveCopyCompleted notification earlier. Posting news messages
+ // involves this, yet doesn't have the newHdr initialized, so don't send any
+ // notifications in that case.
+ if (!numHdrs && newHdr) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgAdded(newHdr);
+ // We do not appear to trigger classification in this case, so let's
+ // paper over the abyss by just sending the classification notification.
+ notifier->NotifyMsgsClassified({&*newHdr}, false, false);
+ // (We do not add the NotReportedClassified processing flag since we
+ // just reported it!)
+ }
+ }
+ }
+ return rv;
+}
+
+static bool gGotGlobalPrefs;
+static bool gDeleteFromServerOnMove;
+
+bool nsMsgLocalMailFolder::GetDeleteFromServerOnMove() {
+ if (!gGotGlobalPrefs) {
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (pPrefBranch) {
+ pPrefBranch->GetBoolPref("mail.pop3.deleteFromServerOnMove",
+ &gDeleteFromServerOnMove);
+ gGotGlobalPrefs = true;
+ }
+ }
+ return gDeleteFromServerOnMove;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMsgLocalMailFolder::EndMove(bool moveSucceeded) {
+ nsresult rv;
+ if (!mCopyState) return NS_OK;
+
+ if (!moveSucceeded || mCopyState->m_writeFailed) {
+ // Notify that a completion finished.
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(mCopyState->m_srcSupport, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+
+ /* passing true because the messages that have been successfully copied have
+ their corresponding hdrs in place. The message that has failed has been
+ truncated so the msf file and berkeley mailbox are in sync*/
+
+ (void)OnCopyCompleted(mCopyState->m_srcSupport, true);
+ // enable the dest folder
+ EnableNotifications(allMessageCountNotifications, true);
+ return NS_OK;
+ }
+
+ if (mCopyState && mCopyState->m_curCopyIndex >= mCopyState->m_totalMsgCount) {
+ // Notify that a completion finished.
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(mCopyState->m_srcSupport, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgLocalMailFolder> localSrcFolder =
+ do_QueryInterface(srcFolder);
+ if (localSrcFolder) {
+ // if we are the trash and a local msg is being moved to us, mark the
+ // source for delete from server, if so configured.
+ if (mFlags & nsMsgFolderFlags::Trash) {
+ // if we're deleting on all moves, we'll mark this message for deletion
+ // when we call DeleteMessages on the source folder. So don't mark it
+ // for deletion here, in that case.
+ if (!GetDeleteFromServerOnMove()) {
+ localSrcFolder->MarkMsgsOnPop3Server(mCopyState->m_messages,
+ POP3_DELETE);
+ }
+ }
+ }
+ // lets delete these all at once - much faster that way
+ rv = srcFolder->DeleteMessages(mCopyState->m_messages,
+ mCopyState->m_msgWindow, true, true, nullptr,
+ mCopyState->m_allowUndo);
+ AutoCompact(mCopyState->m_msgWindow);
+
+ // enable the dest folder
+ EnableNotifications(allMessageCountNotifications, true);
+ // I'm not too sure of the proper location of this event. It seems to need
+ // to be after the EnableNotifications, or the folder counts can be
+ // incorrect during the kDeleteOrMoveMsgCompleted call.
+ srcFolder->NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+
+ if (NS_SUCCEEDED(rv) && mCopyState->m_msgWindow &&
+ mCopyState->m_undoMsgTxn) {
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ mCopyState->m_msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ if (txnMgr) {
+ RefPtr<nsLocalMoveCopyMsgTxn> txn = mCopyState->m_undoMsgTxn;
+ txnMgr->DoTransaction(txn);
+ }
+ }
+ (void)OnCopyCompleted(
+ mCopyState->m_srcSupport,
+ NS_SUCCEEDED(rv)
+ ? true
+ : false); // clear the copy state so that the next message from a
+ // different folder can be move
+ }
+
+ return NS_OK;
+}
+
+// this is the beginning of the next message copied
+NS_IMETHODIMP nsMsgLocalMailFolder::StartMessage() {
+ // We get crashes that we don't understand (bug 284876), so stupidly prevent
+ // that.
+ NS_ENSURE_ARG_POINTER(mCopyState);
+ nsresult rv = InitCopyMsgHdrAndFileStream();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return WriteStartOfNewMessage();
+}
+
+// just finished the current message.
+NS_IMETHODIMP nsMsgLocalMailFolder::EndMessage(nsMsgKey key) {
+ NS_ENSURE_ARG_POINTER(mCopyState);
+
+ RefPtr<nsLocalMoveCopyMsgTxn> localUndoTxn = mCopyState->m_undoMsgTxn;
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsresult rv;
+
+ if (localUndoTxn) {
+ localUndoTxn->GetMsgWindow(getter_AddRefs(msgWindow));
+ localUndoTxn->AddSrcKey(key);
+ localUndoTxn->AddDstKey(mCopyState->m_curDstKey);
+ }
+
+ // I think this is always true for online to offline copy
+ mCopyState->m_dummyEnvelopeNeeded = true;
+ if (mCopyState->m_fileStream) {
+ rv = FinishNewLocalMessage(mCopyState->m_fileStream, mCopyState->m_newHdr,
+ mCopyState->m_msgStore,
+ mCopyState->m_parseMsgState);
+ mCopyState->m_fileStream->Close();
+ mCopyState->m_fileStream = nullptr; // all done with the file stream
+ }
+
+ // CopyFileMessage() and CopyMessages() from servers other than mailbox
+ if (mCopyState->m_parseMsgState) {
+ nsCOMPtr<nsIMsgDatabase> msgDb;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+
+ mCopyState->m_parseMsgState->FinishHeader();
+
+ rv = mCopyState->m_parseMsgState->GetNewMsgHdr(getter_AddRefs(newHdr));
+ if (NS_SUCCEEDED(rv) && newHdr) {
+ nsCOMPtr<nsIMsgFolder> srcFolder =
+ do_QueryInterface(mCopyState->m_srcSupport, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (srcDB) {
+ nsCOMPtr<nsIMsgDBHdr> srcMsgHdr;
+ srcDB->GetMsgHdrForKey(key, getter_AddRefs(srcMsgHdr));
+ if (srcMsgHdr)
+ CopyPropertiesToMsgHdr(newHdr, srcMsgHdr, mCopyState->m_isMove);
+ }
+ rv = GetDatabaseWOReparse(getter_AddRefs(msgDb));
+ if (NS_SUCCEEDED(rv) && msgDb) {
+ msgDb->AddNewHdrToDB(newHdr, true);
+ if (localUndoTxn) {
+ // ** jt - recording the message size for possible undo use; the
+ // message size is different for pop3 and imap4 messages
+ uint32_t msgSize;
+ newHdr->GetMessageSize(&msgSize);
+ localUndoTxn->AddDstMsgSize(msgSize);
+ }
+ } else
+ mCopyState->m_undoMsgTxn = nullptr; // null out the transaction because
+ // we can't undo w/o the msg db
+ }
+ mCopyState->m_parseMsgState->Clear();
+
+ if (mCopyState->m_listener) // CopyFileMessage() only
+ mCopyState->m_listener->SetMessageKey(mCopyState->m_curDstKey);
+ }
+
+ if (mCopyState->m_fileStream) mCopyState->m_fileStream->Flush();
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::CopyMessagesTo(nsTArray<nsMsgKey>& keyArray,
+ nsIMsgWindow* aMsgWindow,
+ bool isMove) {
+ if (!mCopyState) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv;
+
+ nsCOMPtr<nsICopyMessageStreamListener> copyStreamListener = do_CreateInstance(
+ "@mozilla.org/messenger/copymessagestreamlistener;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> srcFolder(
+ do_QueryInterface(mCopyState->m_srcSupport, &rv));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE);
+
+ rv = copyStreamListener->Init(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mCopyState->m_messageService) {
+ nsCString uri;
+ srcFolder->GetURI(uri);
+ rv = GetMessageServiceFromURI(uri,
+ getter_AddRefs(mCopyState->m_messageService));
+ }
+
+ if (NS_SUCCEEDED(rv) && mCopyState->m_messageService) {
+ nsCOMPtr<nsIStreamListener> streamListener(
+ do_QueryInterface(copyStreamListener, &rv));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE);
+
+ mCopyState->m_curCopyIndex = 0;
+ // we need to kick off the first message - subsequent messages
+ // are kicked off by nsMailboxProtocol when it finishes a message
+ // before starting the next message. Only do this if the source folder
+ // is a local folder, however. IMAP will handle calling StartMessage for
+ // each message that gets downloaded, and news doesn't go through here
+ // because news only downloads one message at a time, and this routine
+ // is for multiple message copy.
+ nsCOMPtr<nsIMsgLocalMailFolder> srcLocalFolder =
+ do_QueryInterface(srcFolder);
+ if (srcLocalFolder) {
+ StartMessage();
+ }
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = mCopyState->m_messageService->CopyMessages(
+ keyArray, srcFolder, streamListener, isMove, nullptr, aMsgWindow,
+ getter_AddRefs(dummyNull));
+ }
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::CopyMessageTo(nsISupports* message,
+ nsIMsgWindow* aMsgWindow,
+ bool isMove) {
+ if (!mCopyState) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr(do_QueryInterface(message, &rv));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE);
+
+ mCopyState->m_message = msgHdr;
+
+ nsCOMPtr<nsIMsgFolder> srcFolder(
+ do_QueryInterface(mCopyState->m_srcSupport, &rv));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE);
+ nsCString uri;
+ srcFolder->GetUriForMsg(msgHdr, uri);
+
+ nsCOMPtr<nsICopyMessageStreamListener> copyStreamListener = do_CreateInstance(
+ "@mozilla.org/messenger/copymessagestreamlistener;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyStreamListener->Init(this);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mCopyState->m_messageService)
+ rv = GetMessageServiceFromURI(uri,
+ getter_AddRefs(mCopyState->m_messageService));
+
+ if (NS_SUCCEEDED(rv) && mCopyState->m_messageService) {
+ nsCOMPtr<nsIStreamListener> streamListener(
+ do_QueryInterface(copyStreamListener, &rv));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE);
+ rv = mCopyState->m_messageService->CopyMessage(uri, streamListener, isMove,
+ nullptr, aMsgWindow);
+ }
+ return rv;
+}
+
+// A message is being deleted from a POP3 mail file, so check and see if we have
+// the message being deleted in the server. If so, then we need to remove the
+// message from the server as well. We have saved the UIDL of the message in the
+// popstate.dat file and we must match this uidl, so read the message headers
+// and see if we have it, then mark the message for deletion from the server.
+// The next time we look at mail the message will be deleted from the server.
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::MarkMsgsOnPop3Server(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, int32_t aMark) {
+ nsLocalFolderScanState folderScanState;
+ nsCOMPtr<nsIPop3IncomingServer> curFolderPop3MailServer;
+ nsCOMArray<nsIPop3IncomingServer>
+ pop3Servers; // servers with msgs deleted...
+
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ nsresult rv = GetServer(getter_AddRefs(incomingServer));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // I wonder if we should run through the pop3 accounts and see if any of them
+ // have leave on server set. If not, we could short-circuit some of this.
+
+ curFolderPop3MailServer = do_QueryInterface(incomingServer, &rv);
+ rv = GetFolderScanState(&folderScanState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Filter delete requests are always honored, others are subject
+ // to the deleteMailLeftOnServer preference.
+ int32_t mark;
+ mark = (aMark == POP3_FORCE_DEL) ? POP3_DELETE : aMark;
+
+ for (auto msgDBHdr : aMessages) {
+ uint32_t flags = 0;
+ if (msgDBHdr) {
+ msgDBHdr->GetFlags(&flags);
+ nsCOMPtr<nsIPop3IncomingServer> msgPop3Server = curFolderPop3MailServer;
+ bool leaveOnServer = false;
+ bool deleteMailLeftOnServer = false;
+ // set up defaults, in case there's no x-mozilla-account header
+ if (curFolderPop3MailServer) {
+ curFolderPop3MailServer->GetDeleteMailLeftOnServer(
+ &deleteMailLeftOnServer);
+ curFolderPop3MailServer->GetLeaveMessagesOnServer(&leaveOnServer);
+ }
+
+ rv = GetUidlFromFolder(&folderScanState, msgDBHdr);
+ if (!NS_SUCCEEDED(rv)) continue;
+
+ if (folderScanState.m_uidl) {
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->GetAccount(folderScanState.m_accountKey,
+ getter_AddRefs(account));
+ if (NS_SUCCEEDED(rv) && account) {
+ account->GetIncomingServer(getter_AddRefs(incomingServer));
+ nsCOMPtr<nsIPop3IncomingServer> curMsgPop3MailServer =
+ do_QueryInterface(incomingServer);
+ if (curMsgPop3MailServer) {
+ msgPop3Server = curMsgPop3MailServer;
+ msgPop3Server->GetDeleteMailLeftOnServer(&deleteMailLeftOnServer);
+ msgPop3Server->GetLeaveMessagesOnServer(&leaveOnServer);
+ }
+ }
+ }
+ // ignore this header if not partial and leaveOnServer not set...
+ // or if we can't find the pop3 server.
+ if (!msgPop3Server ||
+ (!(flags & nsMsgMessageFlags::Partial) && !leaveOnServer))
+ continue;
+ // if marking deleted, ignore header if we're not deleting from
+ // server when deleting locally.
+ if (aMark == POP3_DELETE && leaveOnServer && !deleteMailLeftOnServer)
+ continue;
+ if (folderScanState.m_uidl) {
+ msgPop3Server->AddUidlToMark(folderScanState.m_uidl, mark);
+ // remember this pop server in list of servers with msgs deleted
+ if (pop3Servers.IndexOfObject(msgPop3Server) == -1)
+ pop3Servers.AppendObject(msgPop3Server);
+ }
+ }
+ }
+ if (folderScanState.m_inputStream) folderScanState.m_inputStream->Close();
+ // need to do this for all pop3 mail servers that had messages deleted.
+ uint32_t serverCount = pop3Servers.Count();
+ for (uint32_t index = 0; index < serverCount; index++)
+ pop3Servers[index]->MarkMessages();
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::DeleteDownloadMsg(nsIMsgDBHdr* aMsgHdr) {
+ uint32_t numMsgs;
+ char* newMsgId;
+
+ // This method is only invoked through DownloadMessagesForOffline()
+ if (mDownloadState != DOWNLOAD_STATE_NONE) {
+ // We only remember the first key, no matter how many
+ // messages were originally selected.
+ if (mDownloadState == DOWNLOAD_STATE_INITED) {
+ aMsgHdr->GetMessageKey(&mDownloadSelectKey);
+ mDownloadState = DOWNLOAD_STATE_GOTMSG;
+ }
+
+ aMsgHdr->GetMessageId(&newMsgId);
+
+ // Walk through all the selected headers, looking for a matching
+ // Message-ID.
+ numMsgs = mDownloadMessages.Length();
+ for (uint32_t i = 0; i < numMsgs; i++) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgDBHdr = mDownloadMessages[i];
+ char* oldMsgId = nullptr;
+ msgDBHdr->GetMessageId(&oldMsgId);
+
+ // Delete the first match and remove it from the array
+ if (!PL_strcmp(newMsgId, oldMsgId)) {
+ rv = GetDatabase();
+ if (!mDatabase) return rv;
+
+ UpdateNewMsgHdr(msgDBHdr, aMsgHdr);
+
+ mDatabase->DeleteHeader(msgDBHdr, nullptr, false, true);
+ mDownloadMessages.RemoveElementAt(i);
+ break;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& aMessages, nsIMsgWindow* aWindow) {
+ if (mDownloadState != DOWNLOAD_STATE_NONE)
+ return NS_ERROR_FAILURE; // already has a download in progress
+
+ // We're starting a download...
+ mDownloadState = DOWNLOAD_STATE_INITED;
+
+ MarkMsgsOnPop3Server(aMessages, POP3_FETCH_BODY);
+
+ // Pull out all the PARTIAL messages into a new array
+ nsresult rv;
+ for (nsIMsgDBHdr* hdr : aMessages) {
+ uint32_t flags = 0;
+ hdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) {
+ mDownloadMessages.AppendElement(hdr);
+ }
+ }
+ mDownloadWindow = aWindow;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsILocalMailIncomingServer> localMailServer =
+ do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+ nsCOMPtr<nsIURI> resultURI;
+ return localMailServer->GetNewMail(aWindow, this, this,
+ getter_AddRefs(resultURI));
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::HasMsgOffline(nsMsgKey msgKey,
+ bool* result) {
+ NS_ENSURE_ARG(result);
+ *result = false;
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t flags = 0;
+ hdr->GetFlags(&flags);
+ // Would be nice to check nsMsgMessageFlags::Offline... but local
+ // folders don't set it.
+ // Don't want partial messages.
+ if (!(flags & nsMsgMessageFlags::Partial)) {
+ *result = true;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ uint64_t offset = 0;
+ hdr->GetMessageOffset(&offset);
+ // It's a local folder - .messageSize holds the size.
+ uint32_t size = 0;
+ hdr->GetMessageSize(&size);
+ nsCOMPtr<nsIInputStream> fileStream;
+ nsresult rv = GetMsgInputStream(hdr, getter_AddRefs(fileStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(fileStream.forget(), offset,
+ uint64_t(size));
+ slicedStream.forget(stream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::NotifyDelete() {
+ NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ return NS_OK;
+}
+
+// TODO: once we move certain code into the IncomingServer (search for TODO)
+// this method will go away.
+// sometimes this gets called when we don't have the server yet, so
+// that's why we're not calling GetServer()
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetIncomingServerType(nsACString& aServerType) {
+ nsresult rv;
+ if (mType.IsEmpty()) {
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // try "none" first
+ rv = NS_MutateURI(url).SetScheme("none"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server)
+ mType.AssignLiteral("none");
+ else {
+ // next try "pop3"
+ rv = NS_MutateURI(url).SetScheme("pop3"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server)
+ mType.AssignLiteral("pop3");
+ else {
+ // next try "rss"
+ rv = NS_MutateURI(url).SetScheme("rss"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server)
+ mType.AssignLiteral("rss");
+ else {
+ }
+ }
+ }
+ }
+ aServerType = mType;
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ return nsCreateLocalBaseMessageURI(aURI, mBaseMessageURI);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::OnStartRunningUrl(nsIURI* aUrl) {
+ nsresult rv;
+ nsCOMPtr<nsIPop3URL> popurl = do_QueryInterface(aUrl, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString aSpec;
+ rv = aUrl->GetSpec(aSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (strstr(aSpec.get(), "uidl=")) {
+ nsCOMPtr<nsIPop3Sink> popsink;
+ rv = popurl->GetPop3Sink(getter_AddRefs(popsink));
+ if (NS_SUCCEEDED(rv)) {
+ popsink->SetBaseMessageUri(mBaseMessageURI);
+ nsCString messageuri;
+ popurl->GetMessageUri(messageuri);
+ popsink->SetOrigMessageUri(messageuri);
+ }
+ }
+ }
+ return nsMsgDBFolder::OnStartRunningUrl(aUrl);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ // If we just finished a DownloadMessages call, reset...
+ if (mDownloadState != DOWNLOAD_STATE_NONE) {
+ mDownloadState = DOWNLOAD_STATE_NONE;
+ mDownloadMessages.Clear();
+ mDownloadWindow = nullptr;
+ return nsMsgDBFolder::OnStopRunningUrl(aUrl, aExitCode);
+ }
+
+ nsresult rv;
+ if (NS_SUCCEEDED(aExitCode)) {
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ nsAutoCString aSpec;
+ if (aUrl) {
+ rv = aUrl->GetSpec(aSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mFlags & nsMsgFolderFlags::Inbox) {
+ if (mDatabase && mCheckForNewMessagesAfterParsing) {
+ bool valid =
+ false; // GetSummaryValid may return without setting valid.
+ mDatabase->GetSummaryValid(&valid);
+ if (valid && msgWindow) rv = GetNewMessages(msgWindow, nullptr);
+ mCheckForNewMessagesAfterParsing = false;
+ }
+ }
+ }
+
+ if (m_parsingFolder) {
+ // Clear this before calling OnStopRunningUrl, in case the url listener
+ // tries to get the database.
+ m_parsingFolder = false;
+
+ // TODO: Updating the size should be pushed down into the msg store backend
+ // so that the size is recalculated as part of parsing the folder data
+ // (important for maildir), once GetSizeOnDisk is pushed into the msgStores
+ // (bug 1032360).
+ (void)RefreshSizeOnDisk();
+
+ // Update the summary totals so the front end will
+ // show the right thing.
+ UpdateSummaryTotals(true);
+
+ if (mReparseListener) {
+ nsCOMPtr<nsIUrlListener> saveReparseListener = mReparseListener;
+ mReparseListener = nullptr;
+ saveReparseListener->OnStopRunningUrl(aUrl, aExitCode);
+ }
+ }
+ if (mFlags & nsMsgFolderFlags::Inbox) {
+ // if we are the inbox and running pop url
+ nsCOMPtr<nsIPop3URL> popurl = do_QueryInterface(aUrl, &rv);
+ mozilla::Unused << popurl;
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ GetServer(getter_AddRefs(server));
+ // this is the deferred to account, in the global inbox case
+ if (server) server->SetPerformingBiff(false); // biff is over
+ }
+ }
+ return nsMsgDBFolder::OnStopRunningUrl(aUrl, aExitCode);
+}
+
+nsresult nsMsgLocalMailFolder::DisplayMoveCopyStatusMsg() {
+ nsresult rv = NS_OK;
+ if (mCopyState) {
+ if (!mCopyState->m_statusFeedback) {
+ // get msgWindow from undo txn
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ if (mCopyState->m_undoMsgTxn)
+ mCopyState->m_undoMsgTxn->GetMsgWindow(getter_AddRefs(msgWindow));
+ if (!msgWindow) return NS_OK; // not a fatal error.
+
+ msgWindow->GetStatusFeedback(
+ getter_AddRefs(mCopyState->m_statusFeedback));
+ }
+
+ if (!mCopyState->m_stringBundle) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/localMsgs.properties",
+ getter_AddRefs(mCopyState->m_stringBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (mCopyState->m_statusFeedback && mCopyState->m_stringBundle) {
+ nsString folderName;
+ GetName(folderName);
+ nsAutoString numMsgSoFarString;
+ numMsgSoFarString.AppendInt((mCopyState->m_copyingMultipleMessages)
+ ? mCopyState->m_curCopyIndex
+ : 1);
+
+ nsAutoString totalMessagesString;
+ totalMessagesString.AppendInt(mCopyState->m_totalMsgCount);
+ nsString finalString;
+ AutoTArray<nsString, 3> stringArray = {numMsgSoFarString,
+ totalMessagesString, folderName};
+ rv = mCopyState->m_stringBundle->FormatStringFromName(
+ (mCopyState->m_isMove) ? "movingMessagesStatus"
+ : "copyingMessagesStatus",
+ stringArray, finalString);
+ int64_t nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+
+ // only update status/progress every half second
+ if (nowMS - mCopyState->m_lastProgressTime < 500 &&
+ mCopyState->m_curCopyIndex < mCopyState->m_totalMsgCount)
+ return NS_OK;
+
+ mCopyState->m_lastProgressTime = nowMS;
+ mCopyState->m_statusFeedback->ShowStatusString(finalString);
+ mCopyState->m_statusFeedback->ShowProgress(
+ mCopyState->m_curCopyIndex * 100 / mCopyState->m_totalMsgCount);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::SetFlagsOnDefaultMailboxes(uint32_t flags) {
+ if (flags & nsMsgFolderFlags::Inbox)
+ setSubfolderFlag(u"Inbox"_ns, nsMsgFolderFlags::Inbox);
+
+ if (flags & nsMsgFolderFlags::SentMail)
+ setSubfolderFlag(u"Sent"_ns, nsMsgFolderFlags::SentMail);
+
+ if (flags & nsMsgFolderFlags::Drafts)
+ setSubfolderFlag(u"Drafts"_ns, nsMsgFolderFlags::Drafts);
+
+ if (flags & nsMsgFolderFlags::Templates)
+ setSubfolderFlag(u"Templates"_ns, nsMsgFolderFlags::Templates);
+
+ if (flags & nsMsgFolderFlags::Trash)
+ setSubfolderFlag(u"Trash"_ns, nsMsgFolderFlags::Trash);
+
+ if (flags & nsMsgFolderFlags::Queue)
+ setSubfolderFlag(u"Unsent Messages"_ns, nsMsgFolderFlags::Queue);
+
+ if (flags & nsMsgFolderFlags::Junk)
+ setSubfolderFlag(u"Junk"_ns, nsMsgFolderFlags::Junk);
+
+ if (flags & nsMsgFolderFlags::Archive)
+ setSubfolderFlag(u"Archives"_ns, nsMsgFolderFlags::Archive);
+
+ return NS_OK;
+}
+
+nsresult nsMsgLocalMailFolder::setSubfolderFlag(const nsAString& aFolderName,
+ uint32_t flags) {
+ // FindSubFolder() expects the folder name to be escaped
+ // see bug #192043
+ nsAutoCString escapedFolderName;
+ nsresult rv = NS_MsgEscapeEncodeURLPath(aFolderName, escapedFolderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = FindSubFolder(escapedFolderName, getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we only want to do this if the folder *really* exists,
+ // so check if it has a parent. Otherwise, we'll create the
+ // .msf file when we don't want to.
+ nsCOMPtr<nsIMsgFolder> parent;
+ msgFolder->GetParent(getter_AddRefs(parent));
+ if (!parent) return NS_ERROR_FAILURE;
+
+ rv = msgFolder->SetFlag(flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgFolder->SetPrettyName(aFolderName);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetCheckForNewMessagesAfterParsing(
+ bool* aCheckForNewMessagesAfterParsing) {
+ NS_ENSURE_ARG_POINTER(aCheckForNewMessagesAfterParsing);
+ *aCheckForNewMessagesAfterParsing = mCheckForNewMessagesAfterParsing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::SetCheckForNewMessagesAfterParsing(
+ bool aCheckForNewMessagesAfterParsing) {
+ mCheckForNewMessagesAfterParsing = aCheckForNewMessagesAfterParsing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::NotifyCompactCompleted() {
+ mExpungedBytes = 0;
+ m_newMsgs.Clear(); // if compacted, m_newMsgs probably aren't valid.
+ // if compacted, processing flags probably also aren't valid.
+ ClearProcessingFlags();
+ (void)RefreshSizeOnDisk();
+ (void)CloseDBIfFolderNotOpen(false);
+ NotifyFolderEvent(kCompactCompleted);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::Shutdown(bool shutdownChildren) {
+ mInitialized = false;
+ return nsMsgDBFolder::Shutdown(shutdownChildren);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent)
+
+{
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!aMsgURI.IsEmpty()) // not end of batch
+ {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if this message needs junk classification
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) {
+ nsMsgDBFolder::OnMessageClassified(aMsgURI, aClassification,
+ aJunkPercent);
+
+ if (aClassification == nsIJunkMailPlugin::JUNK) {
+ bool willMoveMessage = false;
+
+ // don't do the move when we are opening up
+ // the junk mail folder or the trash folder
+ // or when manually classifying messages in those folders
+ if (!(mFlags & nsMsgFolderFlags::Junk ||
+ mFlags & nsMsgFolderFlags::Trash)) {
+ bool moveOnSpam = false;
+ rv = spamSettings->GetMoveOnSpam(&moveOnSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (moveOnSpam) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = FindFolder(spamFolderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (folder) {
+ rv = folder->SetFlag(nsMsgFolderFlags::Junk);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamKeysToMove.AppendElement(msgKey);
+ willMoveMessage = true;
+ } else {
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // the listener should do
+ // rv = folder->SetFlag(nsMsgFolderFlags::Junk);
+ // NS_ENSURE_SUCCESS(rv,rv);
+ // mSpamKeysToMove.AppendElement(msgKey);
+ // willMoveMessage = true;
+ rv =
+ GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed");
+ }
+ }
+ }
+ rv = spamSettings->LogJunkHit(msgHdr, willMoveMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ else // end of batch
+ {
+ // Parent will apply post bayes filters.
+ nsMsgDBFolder::OnMessageClassified(EmptyCString(),
+ nsIJunkMailPlugin::UNCLASSIFIED, 0);
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ if (!mSpamKeysToMove.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ if (!spamFolderURI.IsEmpty()) {
+ rv = FindFolder(spamFolderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ for (uint32_t keyIndex = 0; keyIndex < mSpamKeysToMove.Length();
+ keyIndex++) {
+ // If an upstream filter moved this message, don't move it here.
+ nsMsgKey msgKey = mSpamKeysToMove.ElementAt(keyIndex);
+ nsMsgProcessingFlagType processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+ if (folder && !(processingFlags & nsMsgProcessingFlags::FilterToMove)) {
+ nsCOMPtr<nsIMsgDBHdr> mailHdr;
+ rv = GetMessageHeader(msgKey, getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) messages.AppendElement(mailHdr);
+ } else {
+ // We don't need the processing flag any more.
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::FilterToMove);
+ }
+ }
+
+ if (folder) {
+ nsCOMPtr<nsIMsgCopyService> copySvc =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copySvc->CopyMessages(
+ this, messages, folder, true,
+ /*nsIMsgCopyServiceListener* listener*/ nullptr, nullptr,
+ false /*allowUndo*/);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "CopyMessages failed");
+ if (NS_FAILED(rv)) {
+ nsAutoCString logMsg(
+ "failed to copy junk messages to junk folder rv = ");
+ logMsg.AppendInt(static_cast<uint32_t>(rv), 16);
+ spamSettings->LogJunkString(logMsg.get());
+ }
+ }
+ }
+ int32_t numNewMessages;
+ GetNumNewMessages(false, &numNewMessages);
+ SetNumNewMessages(numNewMessages - messages.Length());
+ mSpamKeysToMove.Clear();
+ // check if this is the inbox first...
+ if (mFlags & nsMsgFolderFlags::Inbox) PerformBiffNotifications();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetFolderScanState(nsLocalFolderScanState* aState) {
+ NS_ENSURE_ARG_POINTER(aState);
+
+ nsresult rv = GetMsgStore(getter_AddRefs(aState->m_msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ aState->m_uidl = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::GetUidlFromFolder(nsLocalFolderScanState* aState,
+ nsIMsgDBHdr* aMsgDBHdr) {
+ bool more = false;
+ uint32_t size = 0, len = 0;
+ const char* accountKey = nullptr;
+ nsresult rv =
+ GetMsgInputStream(aMsgDBHdr, getter_AddRefs(aState->m_inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozilla::UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>);
+
+ aState->m_uidl = nullptr;
+
+ aMsgDBHdr->GetMessageSize(&len);
+ while (len > 0) {
+ rv = NS_ReadLine(aState->m_inputStream.get(), lineBuffer.get(),
+ aState->m_header, &more);
+ if (NS_SUCCEEDED(rv)) {
+ size = aState->m_header.Length();
+ if (!size) break;
+ // this isn't quite right - need to account for line endings
+ len -= size;
+ // account key header will always be before X_UIDL header
+ if (!accountKey) {
+ accountKey =
+ strstr(aState->m_header.get(), HEADER_X_MOZILLA_ACCOUNT_KEY);
+ if (accountKey) {
+ accountKey += strlen(HEADER_X_MOZILLA_ACCOUNT_KEY) + 2;
+ aState->m_accountKey = accountKey;
+ }
+ } else {
+ aState->m_uidl = strstr(aState->m_header.get(), X_UIDL);
+ if (aState->m_uidl) {
+ aState->m_uidl += X_UIDL_LEN + 2; // skip UIDL: header
+ break;
+ }
+ }
+ }
+ }
+ aState->m_inputStream->Close();
+ aState->m_inputStream = nullptr;
+ return rv;
+}
+
+/**
+ * Adds a message to the end of the folder, parsing it as it goes, and
+ * applying filters, if applicable.
+ */
+NS_IMETHODIMP
+nsMsgLocalMailFolder::AddMessage(const char* aMessage, nsIMsgDBHdr** aHdr) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ AutoTArray<nsCString, 1> aMessages = {nsDependentCString(aMessage)};
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrs;
+ nsresult rv = AddMessageBatch(aMessages, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ADDREF(*aHdr = hdrs[0]);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::AddMessageBatch(
+ const nsTArray<nsCString>& aMessages,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray) {
+ aHdrArray.ClearAndRetainStorage();
+ aHdrArray.SetCapacity(aMessages.Length());
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsCOMPtr<nsIOutputStream> outFileStream;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+
+ rv = server->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isLocked;
+
+ GetLocked(&isLocked);
+ if (isLocked) return NS_MSG_FOLDER_BUSY;
+
+ AcquireSemaphore(static_cast<nsIMsgLocalMailFolder*>(this));
+
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < aMessages.Length(); i++) {
+ RefPtr<nsParseNewMailState> newMailParser = new nsParseNewMailState;
+ NS_ENSURE_TRUE(newMailParser, NS_ERROR_OUT_OF_MEMORY);
+ if (!mGettingNewMessages) newMailParser->DisableFilters();
+ rv = msgStore->GetNewMsgOutputStream(this, getter_AddRefs(newHdr),
+ getter_AddRefs(outFileStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get a msgWindow. Proceed without one, but filter actions to imap
+ // folders will silently fail if not signed in and no window for a prompt.
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+
+ rv = newMailParser->Init(rootFolder, this, msgWindow, newHdr,
+ outFileStream);
+
+ uint32_t bytesWritten;
+ uint32_t messageLen = aMessages[i].Length();
+ outFileStream->Write(aMessages[i].get(), messageLen, &bytesWritten);
+ rv = newMailParser->BufferInput(aMessages[i].get(), messageLen);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = newMailParser->Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ FinishNewLocalMessage(outFileStream, newHdr, msgStore, newMailParser);
+ outFileStream->Close();
+ outFileStream = nullptr;
+ newMailParser->OnStopRequest(nullptr, NS_OK);
+ newMailParser->EndMsgDownload();
+ aHdrArray.AppendElement(newHdr);
+ }
+ }
+ ReleaseSemaphore(static_cast<nsIMsgLocalMailFolder*>(this));
+ return rv;
+}
+
+nsresult nsMsgLocalMailFolder::FinishNewLocalMessage(
+ nsIOutputStream* aOutputStream, nsIMsgDBHdr* aNewHdr,
+ nsIMsgPluggableStore* aMsgStore, nsParseMailMessageState* aParseMsgState) {
+ uint32_t bytesWritten;
+ aOutputStream->Write(MSG_LINEBREAK, MSG_LINEBREAK_LEN, &bytesWritten);
+ if (aParseMsgState)
+ aParseMsgState->ParseAFolderLine(MSG_LINEBREAK, MSG_LINEBREAK_LEN);
+ return aMsgStore->FinishNewMessage(aOutputStream, aNewHdr);
+}
+
+NS_IMETHODIMP
+nsMsgLocalMailFolder::WarnIfLocalFileTooBig(nsIMsgWindow* aWindow,
+ int64_t aSpaceRequested,
+ bool* aTooBig) {
+ NS_ENSURE_ARG_POINTER(aTooBig);
+
+ *aTooBig = true;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool spaceAvailable = false;
+ // check if we have a reasonable amount of space left
+ rv = msgStore->HasSpaceAvailable(this, aSpaceRequested, &spaceAvailable);
+ if (NS_SUCCEEDED(rv) && spaceAvailable) {
+ *aTooBig = false;
+ } else if (rv == NS_ERROR_FILE_TOO_BIG) {
+ ThrowAlertMsg("mailboxTooLarge", aWindow);
+ } else {
+ ThrowAlertMsg("outOfDiskSpace", aWindow);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::FetchMsgPreviewText(
+ nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) {
+ NS_ENSURE_ARG_POINTER(aAsyncResults);
+
+ *aAsyncResults = false;
+ nsCOMPtr<nsIInputStream> inputStream;
+
+ for (uint32_t i = 0; i < aKeysToFetch.Length(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCString prevBody;
+ nsresult rv = GetMessageHeader(aKeysToFetch[i], getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // ignore messages that already have a preview body.
+ msgHdr->GetStringProperty("preview", prevBody);
+ if (!prevBody.IsEmpty()) continue;
+
+ rv = GetMsgInputStream(msgHdr, getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetMsgPreviewTextFromStream(msgHdr, inputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = nsMsgDBFolder::AddKeywordsToMessages(aMessages, aKeywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->ChangeKeywords(aMessages, aKeywords, true /* add */);
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = nsMsgDBFolder::RemoveKeywordsFromMessages(aMessages, aKeywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->ChangeKeywords(aMessages, aKeywords, false /* remove */);
+}
+
+NS_IMETHODIMP nsMsgLocalMailFolder::UpdateNewMsgHdr(nsIMsgDBHdr* aOldHdr,
+ nsIMsgDBHdr* aNewHdr) {
+ NS_ENSURE_ARG_POINTER(aOldHdr);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+ // Preserve any properties set on the message.
+ CopyPropertiesToMsgHdr(aNewHdr, aOldHdr, true);
+
+ // Preserve keywords manually, since they are set as don't preserve.
+ nsCString keywordString;
+ aOldHdr->GetStringProperty("keywords", keywordString);
+ aNewHdr->SetStringProperty("keywords", keywordString);
+
+ // If the junk score was set by the plugin, remove junkscore to force a new
+ // junk analysis, this time using the body.
+ nsCString junkScoreOrigin;
+ aOldHdr->GetStringProperty("junkscoreorigin", junkScoreOrigin);
+ if (junkScoreOrigin.EqualsLiteral("plugin"))
+ aNewHdr->SetStringProperty("junkscore", ""_ns);
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsLocalMailFolder.h b/comm/mailnews/local/src/nsLocalMailFolder.h
new file mode 100644
index 0000000000..247a7d459c
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalMailFolder.h
@@ -0,0 +1,285 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Interface for representing Local Mail folders.
+*/
+
+#ifndef nsMsgLocalMailFolder_h__
+#define nsMsgLocalMailFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBFolder.h" /* include the interface we are going to support */
+#include "nsICopyMessageListener.h"
+#include "nsMsgTxn.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISeekableStream.h"
+#include "nsIStringBundle.h"
+#include "nsLocalUndoTxn.h"
+
+#define COPY_BUFFER_SIZE 16384
+
+class nsParseMailMessageState;
+
+struct nsLocalMailCopyState {
+ nsLocalMailCopyState();
+ virtual ~nsLocalMailCopyState();
+
+ nsCOMPtr<nsIOutputStream> m_fileStream;
+ nsCOMPtr<nsIMsgPluggableStore> m_msgStore;
+ nsCOMPtr<nsISupports> m_srcSupport;
+ /// Source nsIMsgDBHdr instances.
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_messages;
+ /// Destination nsIMsgDBHdr instances.
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_destMessages;
+ RefPtr<nsLocalMoveCopyMsgTxn> m_undoMsgTxn;
+ nsCOMPtr<nsIMsgDBHdr> m_message; // current copy message
+ nsMsgMessageFlagType m_flags; // current copy message flags
+ RefPtr<nsParseMailMessageState> m_parseMsgState;
+ nsCOMPtr<nsIMsgCopyServiceListener> m_listener;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgDatabase> m_destDB;
+
+ // for displaying status;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIStringBundle> m_stringBundle;
+ int64_t m_lastProgressTime;
+
+ nsMsgKey m_curDstKey;
+ uint32_t m_curCopyIndex;
+ nsCOMPtr<nsIMsgMessageService> m_messageService;
+ /// The number of messages in m_messages.
+ uint32_t m_totalMsgCount;
+ char* m_dataBuffer;
+ uint32_t m_dataBufferSize;
+ uint32_t m_leftOver;
+ bool m_isMove;
+ bool m_isFolder; // isFolder move/copy
+ bool m_dummyEnvelopeNeeded;
+ bool m_copyingMultipleMessages;
+ bool m_fromLineSeen;
+ bool m_allowUndo;
+ bool m_writeFailed;
+ bool m_notifyFolderLoaded;
+ bool m_wholeMsgInStream;
+ nsCString m_newMsgKeywords;
+ nsCOMPtr<nsIMsgDBHdr> m_newHdr;
+};
+
+struct nsLocalFolderScanState {
+ nsLocalFolderScanState();
+ ~nsLocalFolderScanState();
+
+ nsCOMPtr<nsIInputStream> m_inputStream;
+ nsCOMPtr<nsIMsgPluggableStore> m_msgStore;
+ nsCString m_header;
+ nsCString m_accountKey;
+ const char* m_uidl; // memory is owned by m_header
+};
+
+class nsMsgLocalMailFolder : public nsMsgDBFolder,
+ public nsIMsgLocalMailFolder,
+ public nsICopyMessageListener {
+ public:
+ nsMsgLocalMailFolder(void);
+ NS_DECL_NSICOPYMESSAGELISTENER
+ NS_DECL_NSIMSGLOCALMAILFOLDER
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIUrlListener methods
+ NS_IMETHOD OnStartRunningUrl(nsIURI* aUrl) override;
+ NS_IMETHOD OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) override;
+
+ // nsIMsgFolder methods:
+ NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override;
+ NS_IMETHOD GetMsgDatabase(nsIMsgDatabase** aMsgDatabase) override;
+
+ NS_IMETHOD OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) override;
+ NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override;
+
+ NS_IMETHOD CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD EmptyTrash(nsIUrlListener* aListener) override;
+ NS_IMETHOD DeleteSelf(nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD CreateStorageIfMissing(nsIUrlListener* urlListener) override;
+ NS_IMETHOD Rename(const nsAString& aNewName,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) override;
+
+ NS_IMETHOD GetPrettyName(nsAString& prettyName)
+ override; // Override of the base, for top-level mail folder
+ NS_IMETHOD SetPrettyName(const nsAString& aName) override;
+
+ NS_IMETHOD GetFolderURL(nsACString& url) override;
+
+ NS_IMETHOD GetManyHeadersToDownload(bool* retval) override;
+
+ NS_IMETHOD GetDeletable(bool* deletable) override;
+ NS_IMETHOD GetSizeOnDisk(int64_t* size) override;
+
+ NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) override;
+
+ NS_IMETHOD DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD CopyMessages(
+ nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener,
+ bool isFolder, bool allowUndo) override;
+ NS_IMETHOD CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) override;
+ NS_IMETHOD CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* msgToReplace,
+ bool isDraftOrTemplate, uint32_t newMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener) override;
+
+ NS_IMETHOD AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) override;
+ NS_IMETHOD MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ bool aMarkRead) override;
+ NS_IMETHOD MarkMessagesFlagged(const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ bool aMarkFlagged) override;
+ NS_IMETHOD MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD MarkThreadRead(nsIMsgThread* thread) override;
+ NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) override;
+ NS_IMETHOD NotifyCompactCompleted() override;
+ NS_IMETHOD Shutdown(bool shutdownChildren) override;
+
+ NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element) override;
+ NS_IMETHOD ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) override;
+
+ NS_IMETHOD GetName(nsAString& aName) override;
+
+ // Used when headers_only is TRUE
+ NS_IMETHOD DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& aMessages,
+ nsIMsgWindow* aWindow) override;
+ NS_IMETHOD HasMsgOffline(nsMsgKey msgKey, bool* result) override;
+ NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) override;
+ NS_IMETHOD FetchMsgPreviewText(nsTArray<nsMsgKey> const& aKeysToFetch,
+ nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) override;
+ NS_IMETHOD AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) override;
+ NS_IMETHOD RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) override;
+ NS_IMETHOD GetIncomingServerType(nsACString& serverType) override;
+
+ protected:
+ virtual ~nsMsgLocalMailFolder();
+ nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+ nsresult CopyFolderAcrossServer(nsIMsgFolder* srcFolder,
+ nsIMsgWindow* msgWindow,
+ nsIMsgCopyServiceListener* listener,
+ bool moveMsgs);
+
+ nsresult CreateSubFolders(nsIFile* path);
+ nsresult GetTrashFolder(nsIMsgFolder** trashFolder);
+ nsresult WriteStartOfNewMessage();
+
+ // CreateSubfolder, but without the nsIMsgFolderListener notification
+ nsresult CreateSubfolderInternal(const nsAString& folderName,
+ nsIMsgWindow* msgWindow,
+ nsIMsgFolder** aNewFolder);
+
+ nsresult IsChildOfTrash(bool* result);
+ nsresult RecursiveSetDeleteIsMoveTrash(bool bVal);
+ nsresult ConfirmFolderDeletion(nsIMsgWindow* aMsgWindow,
+ nsIMsgFolder* aFolder, bool* aResult);
+
+ nsresult GetDatabase() override;
+ // this will set mDatabase, if successful. It will also create a .msf file
+ // for an empty local mail folder. It will leave invalid DBs in place, and
+ // return an error.
+ nsresult OpenDatabase();
+
+ // copy message helper
+ nsresult DisplayMoveCopyStatusMsg();
+
+ nsresult CopyMessageTo(nsISupports* message, nsIMsgWindow* msgWindow,
+ bool isMove);
+
+ /**
+ * Checks if there's room in the target folder to copy message(s) into.
+ * If not, handles alerting the user, and sending the copy notifications.
+ */
+ bool CheckIfSpaceForCopy(nsIMsgWindow* msgWindow, nsIMsgFolder* srcFolder,
+ nsISupports* srcSupports, bool isMove,
+ int64_t totalMsgSize);
+
+ // copy multiple messages at a time from this folder
+ nsresult CopyMessagesTo(nsTArray<nsMsgKey>& keyArray,
+ nsIMsgWindow* aMsgWindow, bool isMove);
+ nsresult InitCopyState(nsISupports* aSupport,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* msgWindow, bool isMoveFolder,
+ bool allowUndo);
+ nsresult InitCopyMsgHdrAndFileStream();
+ // preserve message metadata when moving or copying messages
+ void CopyPropertiesToMsgHdr(nsIMsgDBHdr* destHdr, nsIMsgDBHdr* srcHdr,
+ bool isMove);
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override;
+ nsresult ChangeKeywordForMessages(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& aMessages,
+ const nsACString& aKeyword, bool add);
+ bool GetDeleteFromServerOnMove();
+ void CopyHdrPropertiesWithSkipList(nsIMsgDBHdr* destHdr, nsIMsgDBHdr* srcHdr,
+ const nsCString& skipList);
+ nsresult FinishNewLocalMessage(nsIOutputStream* outputStream,
+ nsIMsgDBHdr* newHdr,
+ nsIMsgPluggableStore* msgStore,
+ nsParseMailMessageState* parseMsgState);
+
+ protected:
+ nsLocalMailCopyState* mCopyState; // We only allow one of these at a time
+ nsCString mType;
+ bool mHaveReadNameFromDB;
+ bool mInitialized;
+ bool mCheckForNewMessagesAfterParsing;
+ bool m_parsingFolder;
+ nsCOMPtr<nsIUrlListener> mReparseListener;
+ nsTArray<nsMsgKey> mSpamKeysToMove;
+ nsresult setSubfolderFlag(const nsAString& aFolderName, uint32_t flags);
+
+ // state variables for DownloadMessagesForOffline
+
+ nsCOMArray<nsIMsgDBHdr> mDownloadMessages;
+ nsCOMPtr<nsIMsgWindow> mDownloadWindow;
+ nsMsgKey mDownloadSelectKey;
+ uint32_t mDownloadState;
+#define DOWNLOAD_STATE_NONE 0
+#define DOWNLOAD_STATE_INITED 1
+#define DOWNLOAD_STATE_GOTMSG 2
+#define DOWNLOAD_STATE_DIDSEL 3
+};
+
+#endif // nsMsgLocalMailFolder_h__
diff --git a/comm/mailnews/local/src/nsLocalUndoTxn.cpp b/comm/mailnews/local/src/nsLocalUndoTxn.cpp
new file mode 100644
index 0000000000..73bda9dcec
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalUndoTxn.cpp
@@ -0,0 +1,495 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIMsgHdr.h"
+#include "nsLocalUndoTxn.h"
+#include "nsImapCore.h"
+#include "nsIImapService.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsThreadUtils.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsMsgDBFolder.h"
+
+NS_IMPL_ISUPPORTS_INHERITED(nsLocalMoveCopyMsgTxn, nsMsgTxn, nsIFolderListener)
+
+nsLocalMoveCopyMsgTxn::nsLocalMoveCopyMsgTxn()
+ : m_srcIsImap4(false), m_canUndelete(false) {}
+
+nsLocalMoveCopyMsgTxn::~nsLocalMoveCopyMsgTxn() {}
+
+nsresult nsLocalMoveCopyMsgTxn::Init(nsIMsgFolder* srcFolder,
+ nsIMsgFolder* dstFolder, bool isMove) {
+ nsresult rv;
+ rv = SetSrcFolder(srcFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetDstFolder(dstFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_isMove = isMove;
+
+ mUndoFolderListener = nullptr;
+
+ nsCString protocolType;
+ rv = srcFolder->GetURI(protocolType);
+ protocolType.SetLength(protocolType.FindChar(':'));
+ if (protocolType.LowerCaseEqualsLiteral("imap")) m_srcIsImap4 = true;
+ return nsMsgTxn::Init();
+}
+nsresult nsLocalMoveCopyMsgTxn::GetSrcIsImap(bool* isImap) {
+ *isImap = m_srcIsImap4;
+ return NS_OK;
+}
+nsresult nsLocalMoveCopyMsgTxn::SetSrcFolder(nsIMsgFolder* srcFolder) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ if (srcFolder) m_srcFolder = do_GetWeakReference(srcFolder, &rv);
+ return rv;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::SetDstFolder(nsIMsgFolder* dstFolder) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ if (dstFolder) m_dstFolder = do_GetWeakReference(dstFolder, &rv);
+ return rv;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::AddSrcKey(nsMsgKey aKey) {
+ m_srcKeyArray.AppendElement(aKey);
+ return NS_OK;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::AddDstKey(nsMsgKey aKey) {
+ m_dstKeyArray.AppendElement(aKey);
+ return NS_OK;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::AddDstMsgSize(uint32_t msgSize) {
+ m_dstSizeArray.AppendElement(msgSize);
+ return NS_OK;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::UndoImapDeleteFlag(nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>& keyArray,
+ bool deleteFlag) {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (m_srcIsImap4) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> urlListener;
+ nsCString msgIds;
+ uint32_t i, count = keyArray.Length();
+ urlListener = do_QueryInterface(folder, &rv);
+ for (i = 0; i < count; i++) {
+ if (!msgIds.IsEmpty()) msgIds.Append(',');
+ msgIds.AppendInt((int32_t)keyArray[i]);
+ }
+ // This is to make sure that we are in the selected state
+ // when executing the imap url; we don't want to load the
+ // folder so use lite select to do the trick
+ rv = imapService->LiteSelectFolder(folder, urlListener, nullptr, nullptr);
+ if (!deleteFlag)
+ rv = imapService->AddMessageFlags(folder, urlListener, msgIds,
+ kImapMsgDeletedFlag, true);
+ else
+ rv = imapService->SubtractMessageFlags(folder, urlListener, msgIds,
+ kImapMsgDeletedFlag, true);
+ if (NS_SUCCEEDED(rv) && m_msgWindow) folder->UpdateFolder(m_msgWindow);
+ rv = NS_OK; // always return NS_OK to indicate that the src is imap
+ } else
+ rv = NS_ERROR_FAILURE;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsLocalMoveCopyMsgTxn::UndoTransaction() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDatabase> dstDB;
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgLocalMailFolder> dstlocalMailFolder =
+ do_QueryReferent(m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dstlocalMailFolder->GetDatabaseWOReparse(getter_AddRefs(dstDB));
+
+ if (!dstDB) {
+ // This will listen for the db reparse finishing, and the corresponding
+ // FolderLoadedNotification. When it gets that, it will then call
+ // UndoTransactionInternal.
+ mUndoFolderListener = new nsLocalUndoFolderListener(this, dstFolder);
+ if (!mUndoFolderListener) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(mUndoFolderListener);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mailSession->AddFolderListener(mUndoFolderListener,
+ nsIFolderListener::event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else
+ rv = UndoTransactionInternal();
+ return rv;
+}
+
+nsresult nsLocalMoveCopyMsgTxn::UndoTransactionInternal() {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ if (mUndoFolderListener) {
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mailSession->RemoveFolderListener(mUndoFolderListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_RELEASE(mUndoFolderListener);
+ mUndoFolderListener = nullptr;
+ }
+
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgDatabase> dstDB;
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
+ if (NS_FAILED(rv)) return rv;
+
+ uint32_t count = m_srcKeyArray.Length();
+ uint32_t i;
+
+ // protect against a bogus undo txn without any source keys
+ // see bug #179856 for details
+ NS_ASSERTION(count, "no source keys");
+ if (!count) return NS_ERROR_UNEXPECTED;
+
+ if (m_isMove) {
+ if (m_srcIsImap4) {
+ bool deleteFlag =
+ true; // message has been deleted -we are trying to undo it
+ CheckForToggleDelete(srcFolder, m_srcKeyArray[0],
+ &deleteFlag); // there could have been a toggle.
+ rv = UndoImapDeleteFlag(srcFolder, m_srcKeyArray, deleteFlag);
+ } else if (m_canUndelete) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> srcMessages(count);
+ nsTArray<RefPtr<nsIMsgDBHdr>> destMessages(count);
+
+ for (i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgDBHdr> oldHdr;
+ rv = dstDB->GetMsgHdrForKey(m_dstKeyArray[i], getter_AddRefs(oldHdr));
+ NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header");
+ if (NS_SUCCEEDED(rv) && oldHdr) {
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ rv = srcDB->CopyHdrFromExistingHdr(m_srcKeyArray[i], oldHdr, true,
+ getter_AddRefs(newHdr));
+ NS_ASSERTION(newHdr, "fatal ... cannot create new msg header");
+ if (NS_SUCCEEDED(rv) && newHdr) {
+ srcDB->UndoDelete(newHdr);
+ srcMessages.AppendElement(newHdr);
+ // (we want to keep these two lists in sync)
+ destMessages.AppendElement(oldHdr);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ // Remember that we're actually moving things back from the destination
+ // to the source!
+ notifier->NotifyMsgsMoveCopyCompleted(true, destMessages, srcFolder,
+ srcMessages);
+ }
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(srcFolder);
+ if (localFolder) {
+ localFolder->MarkMsgsOnPop3Server(srcMessages,
+ POP3_NONE /*deleteMsgs*/);
+ }
+ } else // undoing a move means moving the messages back.
+ {
+ nsTArray<RefPtr<nsIMsgDBHdr>> dstMessages(m_dstKeyArray.Length());
+ m_numHdrsCopied = 0;
+ m_srcKeyArray.Clear();
+ for (i = 0; i < count; i++) {
+ // GetMsgHdrForKey is not a test for whether the key exists, so check.
+ bool hasKey = false;
+ dstDB->ContainsKey(m_dstKeyArray[i], &hasKey);
+ nsCOMPtr<nsIMsgDBHdr> dstHdr;
+ if (hasKey)
+ dstDB->GetMsgHdrForKey(m_dstKeyArray[i], getter_AddRefs(dstHdr));
+ if (dstHdr) {
+ nsCString messageId;
+ dstHdr->GetMessageId(getter_Copies(messageId));
+ dstMessages.AppendElement(dstHdr);
+ m_copiedMsgIds.AppendElement(messageId);
+ } else {
+ NS_WARNING("Cannot get old msg header");
+ }
+ }
+ if (m_copiedMsgIds.Length()) {
+ srcFolder->AddFolderListener(this);
+ m_undoing = true;
+ return srcFolder->CopyMessages(dstFolder, dstMessages, true, nullptr,
+ nullptr, false, false);
+ } else {
+ // Nothing to do, probably because original messages were deleted.
+ NS_WARNING("Undo did not find any messages to move");
+ }
+ }
+ srcDB->SetSummaryValid(true);
+ }
+
+ dstDB->DeleteMessages(m_dstKeyArray, nullptr);
+ dstDB->SetSummaryValid(true);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsLocalMoveCopyMsgTxn::RedoTransaction() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ nsCOMPtr<nsIMsgDatabase> dstDB;
+
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ if (NS_FAILED(rv)) return rv;
+ rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
+ if (NS_FAILED(rv)) return rv;
+
+ uint32_t count = m_srcKeyArray.Length();
+ uint32_t i;
+ nsCOMPtr<nsIMsgDBHdr> oldHdr;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> srcMessages(m_srcKeyArray.Length());
+ for (i = 0; i < count; i++) {
+ rv = srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(oldHdr));
+ NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header");
+
+ if (NS_SUCCEEDED(rv) && oldHdr) {
+ srcMessages.AppendElement(oldHdr);
+
+ if (m_canUndelete) {
+ rv = dstDB->CopyHdrFromExistingHdr(m_dstKeyArray[i], oldHdr, true,
+ getter_AddRefs(newHdr));
+ NS_ASSERTION(newHdr, "fatal ... cannot get new msg header");
+ if (NS_SUCCEEDED(rv) && newHdr) {
+ if (i < m_dstSizeArray.Length())
+ rv = newHdr->SetMessageSize(m_dstSizeArray[i]);
+ dstDB->UndoDelete(newHdr);
+ }
+ }
+ }
+ }
+ dstDB->SetSummaryValid(true);
+
+ if (m_isMove) {
+ if (m_srcIsImap4) {
+ // protect against a bogus undo txn without any source keys
+ // see bug #179856 for details
+ NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys");
+ if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED;
+
+ bool deleteFlag = false; // message is un-deleted- we are trying to redo
+ CheckForToggleDelete(srcFolder, m_srcKeyArray[0],
+ &deleteFlag); // there could have been a toggle
+ rv = UndoImapDeleteFlag(srcFolder, m_srcKeyArray, deleteFlag);
+ } else if (m_canUndelete) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(srcFolder);
+ if (localFolder) {
+ localFolder->MarkMsgsOnPop3Server(srcMessages,
+ POP3_DELETE /*deleteMsgs*/);
+ }
+
+ rv = srcDB->DeleteMessages(m_srcKeyArray, nullptr);
+ srcDB->SetSummaryValid(true);
+ } else {
+ nsCOMPtr<nsIMsgDBHdr> srcHdr;
+ m_numHdrsCopied = 0;
+ m_dstKeyArray.Clear();
+ for (i = 0; i < count; i++) {
+ srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(srcHdr));
+ NS_ASSERTION(srcHdr, "fatal ... cannot get old msg header");
+ if (srcHdr) {
+ nsCString messageId;
+ srcHdr->GetMessageId(getter_Copies(messageId));
+ m_copiedMsgIds.AppendElement(messageId);
+ }
+ }
+ dstFolder->AddFolderListener(this);
+ m_undoing = false;
+ return dstFolder->CopyMessages(srcFolder, srcMessages, true, nullptr,
+ nullptr, false, false);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msgHdr) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder =
+ do_QueryReferent(m_undoing ? m_srcFolder : m_dstFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ if (m_copiedMsgIds.Contains(messageId)) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (m_undoing)
+ m_srcKeyArray.AppendElement(msgKey);
+ else
+ m_dstKeyArray.AppendElement(msgKey);
+ if (++m_numHdrsCopied == m_copiedMsgIds.Length()) {
+ folder->RemoveFolderListener(this);
+ m_copiedMsgIds.Clear();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, const nsACString& oldValue,
+ const nsACString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderIntPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, int64_t oldValue,
+ int64_t newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderBoolPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, bool oldValue,
+ bool newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderPropertyFlagChanged(
+ nsIMsgDBHdr* item, const nsACString& property, uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderEvent(nsIMsgFolder* aItem,
+ const nsACString& aEvent) {
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsLocalUndoFolderListener, nsIFolderListener)
+
+nsLocalUndoFolderListener::nsLocalUndoFolderListener(
+ nsLocalMoveCopyMsgTxn* aTxn, nsIMsgFolder* aFolder) {
+ mTxn = aTxn;
+ mFolder = aFolder;
+}
+
+nsLocalUndoFolderListener::~nsLocalUndoFolderListener() {}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, const nsACString& oldValue,
+ const nsACString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderIntPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, int64_t oldValue,
+ int64_t newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderBoolPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, bool oldValue,
+ bool newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* item, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderPropertyFlagChanged(
+ nsIMsgDBHdr* item, const nsACString& property, uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderEvent(
+ nsIMsgFolder* aItem, const nsACString& aEvent) {
+ if (mTxn && mFolder && aItem == mFolder) {
+ if (aEvent.Equals(kFolderLoaded)) return mTxn->UndoTransactionInternal();
+ }
+
+ return NS_ERROR_FAILURE;
+}
diff --git a/comm/mailnews/local/src/nsLocalUndoTxn.h b/comm/mailnews/local/src/nsLocalUndoTxn.h
new file mode 100644
index 0000000000..f9f8244193
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalUndoTxn.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsLocalUndoTxn_h__
+#define nsLocalUndoTxn_h__
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsIMsgFolder.h"
+#include "nsMailboxService.h"
+#include "nsMsgTxn.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsIFolderListener.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsLocalUndoFolderListener;
+
+class nsLocalMoveCopyMsgTxn : public nsIFolderListener, public nsMsgTxn {
+ public:
+ nsLocalMoveCopyMsgTxn();
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIFOLDERLISTENER
+
+ // overloading nsITransaction methods
+ NS_IMETHOD UndoTransaction(void) override;
+ NS_IMETHOD RedoTransaction(void) override;
+
+ // helper
+ nsresult AddSrcKey(nsMsgKey aKey);
+ nsresult AddDstKey(nsMsgKey aKey);
+ nsresult AddDstMsgSize(uint32_t msgSize);
+ nsresult SetSrcFolder(nsIMsgFolder* srcFolder);
+ nsresult GetSrcIsImap(bool* isImap);
+ nsresult SetDstFolder(nsIMsgFolder* dstFolder);
+ nsresult Init(nsIMsgFolder* srcFolder, nsIMsgFolder* dstFolder, bool isMove);
+ nsresult UndoImapDeleteFlag(nsIMsgFolder* aFolder,
+ nsTArray<nsMsgKey>& aKeyArray, bool deleteFlag);
+ nsresult UndoTransactionInternal();
+ // If the store using this undo transaction can "undelete" a message,
+ // it will call this function on the transaction; This makes undo/redo
+ // easy because message keys don't change after undo/redo. Otherwise,
+ // we need to adjust the src or dst keys after every undo/redo action
+ // to note the new keys.
+ void SetCanUndelete(bool canUndelete) { m_canUndelete = canUndelete; }
+
+ private:
+ virtual ~nsLocalMoveCopyMsgTxn();
+ nsWeakPtr m_srcFolder;
+ nsTArray<nsMsgKey> m_srcKeyArray; // used when src is local or imap
+ nsWeakPtr m_dstFolder;
+ nsTArray<nsMsgKey> m_dstKeyArray;
+ bool m_isMove;
+ bool m_srcIsImap4;
+ bool m_canUndelete;
+ nsTArray<uint32_t> m_dstSizeArray;
+ bool m_undoing; // if false, re-doing
+ uint32_t m_numHdrsCopied;
+ nsTArray<nsCString> m_copiedMsgIds;
+ nsLocalUndoFolderListener* mUndoFolderListener;
+};
+
+class nsLocalUndoFolderListener : public nsIFolderListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFOLDERLISTENER
+
+ nsLocalUndoFolderListener(nsLocalMoveCopyMsgTxn* aTxn, nsIMsgFolder* aFolder);
+
+ private:
+ virtual ~nsLocalUndoFolderListener();
+ nsLocalMoveCopyMsgTxn* mTxn;
+ nsIMsgFolder* mFolder;
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsLocalUtils.cpp b/comm/mailnews/local/src/nsLocalUtils.cpp
new file mode 100644
index 0000000000..2dc96635c0
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalUtils.cpp
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsLocalUtils.h"
+#include "prsystem.h"
+#include "nsCOMPtr.h"
+#include "prmem.h"
+// stuff for temporary root folder hack
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsNativeCharsetUtils.h"
+
+#include "nsMsgUtils.h"
+#include "nsNetCID.h"
+#include "nsIURIMutator.h"
+
+// it would be really cool to:
+// - cache the last hostname->path match
+// - if no such server exists, behave like an old-style mailbox URL
+// (i.e. return the mail.directory preference or something)
+static nsresult nsGetMailboxServer(const char* uriStr,
+ nsIMsgIncomingServer** aResult) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(nsDependentCString(uriStr))
+ .Finalize(url);
+ if (NS_FAILED(rv)) return rv;
+
+ // retrieve the AccountManager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // find all local mail "no servers" matching the given hostname
+ nsCOMPtr<nsIMsgIncomingServer> none_server;
+ rv = NS_MutateURI(url).SetScheme("none"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // No unescaping of username or hostname done here.
+ // The unescaping is done inside of FindServerByURI
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(none_server));
+ if (NS_SUCCEEDED(rv)) {
+ none_server.forget(aResult);
+ return rv;
+ }
+
+ // if that fails, look for the rss hosts matching the given hostname
+ nsCOMPtr<nsIMsgIncomingServer> rss_server;
+ rv = NS_MutateURI(url).SetScheme("rss"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(rss_server));
+ if (NS_SUCCEEDED(rv)) {
+ rss_server.forget(aResult);
+ return rv;
+ }
+
+ // if that fails, look for the pop hosts matching the given hostname
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_FAILED(rv)) {
+ rv = NS_MutateURI(url).SetScheme("pop3"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+
+ // if we can't find a pop server, maybe it's a local message
+ // in an imap hierarchy. look for an imap server.
+ if (NS_FAILED(rv)) {
+ rv = NS_MutateURI(url).SetScheme("imap"_ns).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ }
+ }
+ if (NS_SUCCEEDED(rv)) {
+ server.forget(aResult);
+ return rv;
+ }
+
+ // If you fail after looking at all "pop3", "none" servers, you fail.
+ return rv;
+}
+
+static nsresult nsLocalURI2Server(const char* uriStr,
+ nsIMsgIncomingServer** aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = nsGetMailboxServer(uriStr, getter_AddRefs(server));
+ server.forget(aResult);
+ return rv;
+}
+
+// given rootURI and rootURI##folder, return on-disk path of folder
+nsresult nsLocalURI2Path(const char* rootURI, const char* uriStr,
+ nsCString& pathResult) {
+ nsresult rv;
+
+ // verify that rootURI starts with "mailbox:/" or "mailbox-message:/"
+ if ((PL_strcmp(rootURI, kMailboxRootURI) != 0) &&
+ (PL_strcmp(rootURI, kMailboxMessageRootURI) != 0)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // verify that uristr starts with rooturi
+ nsAutoCString uri(uriStr);
+ if (uri.Find(rootURI) != 0) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = nsLocalURI2Server(uriStr, getter_AddRefs(server));
+
+ if (NS_FAILED(rv)) return rv;
+
+ // now ask the server what it's root is
+ // and begin pathResult with the mailbox root
+ nsCOMPtr<nsIFile> localPath;
+ rv = server->GetLocalPath(getter_AddRefs(localPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef XP_WIN
+ nsString path = localPath->NativePath();
+ nsCString localNativePath;
+ NS_CopyUnicodeToNative(path, localNativePath);
+#else
+ nsCString localNativePath = localPath->NativePath();
+#endif
+ nsEscapeNativePath(localNativePath);
+ pathResult = localNativePath.get();
+ const char* curPos = uriStr + PL_strlen(rootURI);
+ if (curPos) {
+ // advance past hostname
+ while ((*curPos) == '/') curPos++;
+ while (*curPos && (*curPos) != '/') curPos++;
+
+ nsAutoCString newPath("");
+
+ // Unescape folder name
+ nsCString unescapedStr;
+ MsgUnescapeString(nsDependentCString(curPos), 0, unescapedStr);
+ NS_MsgCreatePathStringFromFolderURI(unescapedStr.get(), newPath, "none"_ns);
+
+ pathResult.Append('/');
+ pathResult.Append(newPath);
+ }
+
+ return NS_OK;
+}
+
+/* parses LocalMessageURI
+ * mailbox-message://folder1/folder2#123?header=none or
+ * mailbox-message://folder1/folder2#1234&part=1.2
+ *
+ * puts folder URI in folderURI (mailbox://folder1/folder2)
+ * message key number in key
+ */
+nsresult nsParseLocalMessageURI(const nsACString& uri, nsCString& folderURI,
+ nsMsgKey* key) {
+ if (!key) return NS_ERROR_NULL_POINTER;
+
+ const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri);
+ int32_t keySeparator = uriStr.FindChar('#');
+ if (keySeparator != -1) {
+ int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "?&", keySeparator);
+ folderURI = StringHead(uriStr, keySeparator);
+ folderURI.Cut(7, 8); // cut out the -message part of mailbox-message:
+
+ nsAutoCString keyStr;
+ if (keyEndSeparator != -1)
+ keyStr = Substring(uriStr, keySeparator + 1,
+ keyEndSeparator - (keySeparator + 1));
+ else
+ keyStr = StringTail(uriStr, uriStr.Length() - (keySeparator + 1));
+
+ *key = msgKeyFromInt(ParseUint64Str(keyStr.get()));
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsBuildLocalMessageURI(const nsACString& baseURI, nsMsgKey key,
+ nsACString& uri) {
+ // need to convert mailbox://hostname/.. to mailbox-message://hostname/..
+ uri.Append(baseURI);
+ uri.Append('#');
+ uri.AppendInt(key);
+ return NS_OK;
+}
+
+nsresult nsCreateLocalBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI) {
+ nsAutoCString tailURI(baseURI);
+
+ // chop off mailbox:/
+ if (tailURI.Find(kMailboxRootURI) == 0)
+ tailURI.Cut(0, PL_strlen(kMailboxRootURI));
+
+ baseMessageURI = kMailboxMessageRootURI;
+ baseMessageURI += tailURI;
+
+ return NS_OK;
+}
+
+void nsEscapeNativePath(nsCString& nativePath) {
+#if defined(XP_WIN)
+ nativePath.Insert('/', 0);
+ nativePath.ReplaceChar('\\', '/');
+#endif
+}
diff --git a/comm/mailnews/local/src/nsLocalUtils.h b/comm/mailnews/local/src/nsLocalUtils.h
new file mode 100644
index 0000000000..0208be8495
--- /dev/null
+++ b/comm/mailnews/local/src/nsLocalUtils.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NS_LOCALUTILS_H
+#define NS_LOCALUTILS_H
+
+#include "nsString.h"
+#include "MailNewsTypes2.h"
+
+static const char kMailboxRootURI[] = "mailbox:/";
+static const char kMailboxMessageRootURI[] = "mailbox-message:/";
+
+nsresult nsLocalURI2Path(const char* rootURI, const char* uriStr,
+ nsCString& pathResult);
+
+nsresult nsParseLocalMessageURI(const nsACString& uri, nsCString& folderURI,
+ nsMsgKey* key);
+
+nsresult nsBuildLocalMessageURI(const nsACString& baseURI, nsMsgKey key,
+ nsACString& uri);
+
+nsresult nsCreateLocalBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI);
+
+void nsEscapeNativePath(nsCString& nativePath);
+
+#endif // NS_LOCALUTILS_H
diff --git a/comm/mailnews/local/src/nsMailboxProtocol.cpp b/comm/mailnews/local/src/nsMailboxProtocol.cpp
new file mode 100644
index 0000000000..389de0a93e
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxProtocol.cpp
@@ -0,0 +1,657 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+
+#include "nsMailboxProtocol.h"
+#include "nscore.h"
+#include "nsIInputStreamPump.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgLineBuffer.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgFolder.h"
+#include "nsICopyMessageStreamListener.h"
+#include "prtime.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SlicedInputStream.h"
+#include "prerror.h"
+#include "prprf.h"
+#include "nspr.h"
+#include "nsIStreamTransportService.h"
+#include "nsIStreamConverterService.h"
+#include "nsNetUtil.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgWindow.h"
+#include "nsISeekableStream.h"
+#include "nsStreamUtils.h"
+
+using namespace mozilla;
+
+static LazyLogModule MAILBOX("Mailbox");
+
+/* the output_buffer_size must be larger than the largest possible line
+ * 2000 seems good for news
+ *
+ * jwz: I increased this to 4k since it must be big enough to hold the
+ * entire button-bar HTML, and with the new "mailto" format, that can
+ * contain arbitrarily long header fields like "references".
+ *
+ * fortezza: proxy auth is huge, buffer increased to 8k (sigh).
+ */
+#define OUTPUT_BUFFER_SIZE (4096 * 2)
+
+nsMailboxProtocol::nsMailboxProtocol(nsIURI* aURI)
+ : nsMsgProtocol(aURI),
+ m_mailboxAction(nsIMailboxUrl::ActionParseMailbox),
+ m_nextState(MAILBOX_UNINITIALIZED),
+ m_initialState(MAILBOX_UNINITIALIZED),
+ mCurrentProgress(0) {}
+
+nsMailboxProtocol::~nsMailboxProtocol() {}
+
+nsresult nsMailboxProtocol::OpenMultipleMsgTransport(uint64_t offset,
+ int64_t size) {
+ nsresult rv;
+
+ nsCOMPtr<nsIStreamTransportService> serv =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> clonedStream;
+ nsCOMPtr<nsIInputStream> replacementStream;
+ rv = NS_CloneInputStream(m_multipleMsgMoveCopyStream,
+ getter_AddRefs(clonedStream),
+ getter_AddRefs(replacementStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (replacementStream) {
+ // If m_multipleMsgMoveCopyStream is not clonable, NS_CloneInputStream
+ // will clone it using a pipe. In order to keep the copy alive and working,
+ // we have to replace the original stream with the replacement.
+ m_multipleMsgMoveCopyStream = replacementStream.forget();
+ }
+ // XXX 64-bit
+ // This can be called with size == -1 which means "read as much as we can".
+ // We pass this on as UINT64_MAX, which is in fact uint64_t(-1).
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ clonedStream.forget(), offset, size == -1 ? UINT64_MAX : uint64_t(size));
+ // Always close the sliced stream when done, we still have the original.
+ rv = serv->CreateInputTransport(slicedStream, true,
+ getter_AddRefs(m_transport));
+
+ return rv;
+}
+
+nsresult nsMailboxProtocol::Initialize(nsIURI* aURL) {
+ NS_ASSERTION(aURL, "invalid URL passed into MAILBOX Protocol");
+ nsresult rv = NS_OK;
+ if (aURL) {
+ m_runningUrl = do_QueryInterface(aURL, &rv);
+ if (NS_SUCCEEDED(rv) && m_runningUrl) {
+ nsCOMPtr<nsIMsgWindow> window;
+ rv = m_runningUrl->GetMailboxAction(&m_mailboxAction);
+ // clear stopped flag on msg window, because we care.
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ if (mailnewsUrl) {
+ mailnewsUrl->GetMsgWindow(getter_AddRefs(window));
+ if (window) window->SetStopped(false);
+ }
+
+ if (m_mailboxAction == nsIMailboxUrl::ActionParseMailbox) {
+ // Set the length of the file equal to the max progress
+ nsCOMPtr<nsIFile> file;
+ GetFileFromURL(aURL, getter_AddRefs(file));
+ if (file) {
+ int64_t fileSize = 0;
+ file->GetFileSize(&fileSize);
+ mailnewsUrl->SetMaxProgress(fileSize);
+ }
+
+ rv =
+ OpenFileSocket(aURL, 0, -1 /* read in all the bytes in the file */);
+ } else {
+ if (RunningMultipleMsgUrl()) {
+ // if we're running multiple msg url, we clear the event sink because
+ // the multiple msg urls will handle setting the progress.
+ mProgressEventSink = nullptr;
+ }
+
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl =
+ do_QueryInterface(m_runningUrl, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ uint32_t msgSize = 0;
+ msgHdr->GetMessageSize(&msgSize);
+ m_runningUrl->SetMessageSize(msgSize);
+
+ SetContentLength(msgSize);
+ mailnewsUrl->SetMaxProgress(msgSize);
+
+ rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) {
+ nsCOMPtr<nsIInputStream> stream;
+ int64_t offset = 0;
+ rv = folder->GetMsgInputStream(msgHdr, getter_AddRefs(stream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsISeekableStream> seekableStream(
+ do_QueryInterface(stream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ seekableStream->Tell(&offset);
+ // create input stream transport
+ nsCOMPtr<nsIStreamTransportService> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+ m_readCount = msgSize;
+
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ stream.forget(), offset, uint64_t(msgSize));
+ // Always close the sliced stream when done, we still have the
+ // original.
+ rv = sts->CreateInputTransport(slicedStream, true,
+ getter_AddRefs(m_transport));
+
+ m_socketIsOpen = false;
+ }
+ }
+ if (!folder) { // must be a .eml file
+ rv = OpenFileSocket(aURL, 0, -1);
+ }
+ }
+ NS_ASSERTION(NS_SUCCEEDED(rv), "oops....i messed something up");
+ }
+ }
+ }
+
+ m_lineStreamBuffer = new nsMsgLineStreamBuffer(OUTPUT_BUFFER_SIZE, true);
+
+ m_nextState = MAILBOX_READ_FOLDER;
+ m_initialState = MAILBOX_READ_FOLDER;
+ mCurrentProgress = 0;
+
+ // do we really need both?
+ m_tempMessageFile = m_tempMsgFile;
+ return rv;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// we support the nsIStreamListener interface
+////////////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMailboxProtocol::OnStartRequest(nsIRequest* request) {
+ // extract the appropriate event sinks from the url and initialize them in our
+ // protocol data the URL should be queried for a nsINewsURL. If it doesn't
+ // support a news URL interface then we have an error.
+ if (m_nextState == MAILBOX_READ_FOLDER && m_mailboxParser) {
+ // we need to inform our mailbox parser that it's time to start...
+ // NOTE: `request` here will be an nsInputStreamPump, but our callbacks
+ // are expecting to be able to QI to a `nsIChannel` to get the URI.
+ // So we pass `this`. See Bug 1528662.
+ m_mailboxParser->OnStartRequest(this);
+ }
+ return nsMsgProtocol::OnStartRequest(request);
+}
+
+bool nsMailboxProtocol::RunningMultipleMsgUrl() {
+ if (m_mailboxAction == nsIMailboxUrl::ActionCopyMessage ||
+ m_mailboxAction == nsIMailboxUrl::ActionMoveMessage) {
+ uint32_t numMoveCopyMsgs;
+ nsresult rv = m_runningUrl->GetNumMoveCopyMsgs(&numMoveCopyMsgs);
+ if (NS_SUCCEEDED(rv) && numMoveCopyMsgs > 1) return true;
+ }
+ return false;
+}
+
+// stop binding is a "notification" informing us that the stream associated with
+// aURL is going away.
+NS_IMETHODIMP nsMailboxProtocol::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsresult rv;
+ if (m_nextState == MAILBOX_READ_FOLDER && m_mailboxParser) {
+ // we need to inform our mailbox parser that there is no more incoming
+ // data... NOTE: `request` here will be an nsInputStreamPump, but our
+ // callbacks are expecting to be able to QI to a `nsIChannel` to get the
+ // URI. So we pass `this`. See Bug 1528662.
+ m_mailboxParser->OnStopRequest(this, aStatus);
+ } else if (m_nextState == MAILBOX_READ_MESSAGE) {
+ DoneReadingMessage();
+ }
+ // I'm not getting cancel status - maybe the load group still has the status.
+ bool stopped = false;
+ if (m_runningUrl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsIMsgWindow> window;
+ mailnewsUrl->GetMsgWindow(getter_AddRefs(window));
+ if (window) window->GetStopped(&stopped);
+ }
+
+ if (!stopped && NS_SUCCEEDED(aStatus) &&
+ (m_mailboxAction == nsIMailboxUrl::ActionCopyMessage ||
+ m_mailboxAction == nsIMailboxUrl::ActionMoveMessage)) {
+ uint32_t numMoveCopyMsgs;
+ uint32_t curMoveCopyMsgIndex;
+ rv = m_runningUrl->GetNumMoveCopyMsgs(&numMoveCopyMsgs);
+ if (NS_SUCCEEDED(rv) && numMoveCopyMsgs > 0) {
+ m_runningUrl->GetCurMoveCopyMsgIndex(&curMoveCopyMsgIndex);
+ if (++curMoveCopyMsgIndex < numMoveCopyMsgs) {
+ if (!mSuppressListenerNotifications && m_channelListener) {
+ nsCOMPtr<nsICopyMessageStreamListener> listener =
+ do_QueryInterface(m_channelListener, &rv);
+ if (listener) {
+ listener->EndCopy(mailnewsUrl, aStatus);
+ listener->StartMessage(); // start next message.
+ }
+ }
+ m_runningUrl->SetCurMoveCopyMsgIndex(curMoveCopyMsgIndex);
+ nsCOMPtr<nsIMsgDBHdr> nextMsg;
+ rv = m_runningUrl->GetMoveCopyMsgHdrForIndex(curMoveCopyMsgIndex,
+ getter_AddRefs(nextMsg));
+ if (NS_SUCCEEDED(rv) && nextMsg) {
+ uint32_t msgSize = 0;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nextMsg->GetFolder(getter_AddRefs(msgFolder));
+ NS_ASSERTION(
+ msgFolder,
+ "couldn't get folder for next msg in multiple msg local copy");
+ if (msgFolder) {
+ nsCString uri;
+ msgFolder->GetUriForMsg(nextMsg, uri);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl =
+ do_QueryInterface(m_runningUrl);
+ if (msgUrl) {
+ msgUrl->SetOriginalSpec(uri);
+ msgUrl->SetUri(uri);
+
+ uint64_t msgOffset;
+ nextMsg->GetMessageOffset(&msgOffset);
+ nextMsg->GetMessageSize(&msgSize);
+ // now we have to seek to the right position in the file and
+ // basically re-initialize the transport with the correct
+ // message size. then, we have to make sure the url keeps
+ // running somehow.
+ //
+ // put us in a state where we are always notified of incoming
+ // data
+ //
+ m_transport = nullptr; // open new stream transport
+ m_outputStream = nullptr;
+
+ if (m_multipleMsgMoveCopyStream) {
+ rv = OpenMultipleMsgTransport(msgOffset, msgSize);
+ } else {
+ nsCOMPtr<nsIInputStream> stream;
+ rv = msgFolder->GetMsgInputStream(nextMsg,
+ getter_AddRefs(stream));
+ if (NS_SUCCEEDED(rv)) {
+ // create input stream transport
+ nsCOMPtr<nsIStreamTransportService> sts = do_GetService(
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ m_readCount = msgSize;
+ RefPtr<SlicedInputStream> slicedStream =
+ new SlicedInputStream(stream.forget(), msgOffset,
+ uint64_t(msgSize));
+ rv = sts->CreateInputTransport(
+ slicedStream, true, getter_AddRefs(m_transport));
+ }
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIInputStream> stream;
+ rv = m_transport->OpenInputStream(0, 0, 0,
+ getter_AddRefs(stream));
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump),
+ stream.forget());
+ if (NS_SUCCEEDED(rv)) {
+ rv = pump->AsyncRead(this);
+ if (NS_SUCCEEDED(rv)) m_request = pump;
+ }
+ }
+ }
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncRead failed");
+ if (m_loadGroup)
+ m_loadGroup->RemoveRequest(static_cast<nsIRequest*>(this),
+ nullptr, aStatus);
+ m_socketIsOpen = true; // mark the channel as open
+ return aStatus;
+ }
+ }
+ }
+ } else {
+ }
+ }
+ }
+ }
+ // and we want to mark ourselves for deletion or some how inform our protocol
+ // manager that we are available for another url if there is one.
+
+ // mscott --> maybe we should set our state to done because we don't run
+ // multiple urls in a mailbox protocol connection....
+ m_nextState = MAILBOX_DONE;
+
+ // the following is for smoke test purposes. QA is looking at this "Mailbox
+ // Done" string which is printed out to the console and determining if the
+ // mail app loaded up correctly...obviously this solution is not very good so
+ // we should look at something better, but don't remove this line before
+ // talking to me (mscott) and mailnews QA....
+
+ MOZ_LOG(MAILBOX, LogLevel::Info, ("Mailbox Done"));
+
+ // when on stop binding is called, we as the protocol are done...let's close
+ // down the connection releasing all of our interfaces. It's important to
+ // remember that this on stop binding call is coming from netlib so they are
+ // never going to ping us again with on data available. This means we'll never
+ // be going through the Process loop...
+
+ if (m_multipleMsgMoveCopyStream) {
+ m_multipleMsgMoveCopyStream->Close();
+ m_multipleMsgMoveCopyStream = nullptr;
+ }
+ nsMsgProtocol::OnStopRequest(request, aStatus);
+ return CloseSocket();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// End of nsIStreamListenerSupport
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMailboxProtocol::DoneReadingMessage() {
+ nsresult rv = NS_OK;
+ // and close the article file if it was open....
+
+ if (m_mailboxAction == nsIMailboxUrl::ActionSaveMessageToDisk &&
+ m_msgFileOutputStream)
+ rv = m_msgFileOutputStream->Close();
+
+ return rv;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// Begin protocol state machine functions...
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMailboxProtocol::LoadUrl(nsIURI* aURL, nsISupports* aConsumer) {
+ nsresult rv = NS_OK;
+ // if we were already initialized with a consumer, use it...
+ nsCOMPtr<nsIStreamListener> consumer = do_QueryInterface(aConsumer);
+ if (consumer) m_channelListener = consumer;
+
+ if (aURL) {
+ m_runningUrl = do_QueryInterface(aURL);
+ if (m_runningUrl) {
+ // find out from the url what action we are supposed to perform...
+ rv = m_runningUrl->GetMailboxAction(&m_mailboxAction);
+
+ bool convertData = false;
+
+ // need to check if we're fetching an rfc822 part in order to
+ // quote a message.
+ if (m_mailboxAction == nsIMailboxUrl::ActionFetchMessage) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl =
+ do_QueryInterface(m_runningUrl, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString queryStr;
+ rv = msgUrl->GetQuery(queryStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if this is a filter plugin requesting the message.
+ // in that case, set up a text converter
+ convertData = (queryStr.Find("header=filter") != -1 ||
+ queryStr.Find("header=attach") != -1);
+ } else if (m_mailboxAction == nsIMailboxUrl::ActionFetchPart) {
+ // when fetching a part, we need to insert a converter into the listener
+ // chain order to force just the part out of the message. Our channel
+ // listener is the consumer we'll pass in to AsyncConvertData.
+ convertData = true;
+ consumer = m_channelListener;
+ }
+ if (convertData) {
+ nsCOMPtr<nsIStreamConverterService> streamConverter =
+ do_GetService("@mozilla.org/streamConverters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIStreamListener> conversionListener;
+ nsCOMPtr<nsIChannel> channel;
+ QueryInterface(NS_GET_IID(nsIChannel), getter_AddRefs(channel));
+
+ rv = streamConverter->AsyncConvertData(
+ "message/rfc822", "*/*", consumer, channel,
+ getter_AddRefs(m_channelListener));
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ switch (m_mailboxAction) {
+ case nsIMailboxUrl::ActionParseMailbox:
+ // extract the mailbox parser..
+ rv =
+ m_runningUrl->GetMailboxParser(getter_AddRefs(m_mailboxParser));
+ m_nextState = MAILBOX_READ_FOLDER;
+ break;
+ case nsIMailboxUrl::ActionSaveMessageToDisk:
+ // ohhh, display message already writes a msg to disk (as part of a
+ // hack) so we can piggy back off of that!! We just need to change
+ // m_tempMessageFile to be the name of our save message to disk
+ // file. Since save message to disk urls are run without a docshell
+ // to display the msg into, we won't be trying to display the
+ // message after we write it to disk...
+ {
+ nsCOMPtr<nsIMsgMessageUrl> messageUrl =
+ do_QueryInterface(m_runningUrl, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ messageUrl->GetMessageFile(getter_AddRefs(m_tempMessageFile));
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(m_msgFileOutputStream), m_tempMessageFile,
+ -1, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool addDummyEnvelope = false;
+ messageUrl->GetAddDummyEnvelope(&addDummyEnvelope);
+ if (addDummyEnvelope)
+ SetFlag(MAILBOX_MSG_PARSE_FIRST_LINE);
+ else
+ ClearFlag(MAILBOX_MSG_PARSE_FIRST_LINE);
+ }
+ }
+ m_nextState = MAILBOX_READ_MESSAGE;
+ break;
+ case nsIMailboxUrl::ActionCopyMessage:
+ case nsIMailboxUrl::ActionMoveMessage:
+ case nsIMailboxUrl::ActionFetchMessage:
+ ClearFlag(MAILBOX_MSG_PARSE_FIRST_LINE);
+ m_nextState = MAILBOX_READ_MESSAGE;
+ break;
+ case nsIMailboxUrl::ActionFetchPart:
+ m_nextState = MAILBOX_READ_MESSAGE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ rv = nsMsgProtocol::LoadUrl(aURL, m_channelListener);
+
+ } // if we received an MAILBOX url...
+ } // if we received a url!
+
+ return rv;
+}
+
+int32_t nsMailboxProtocol::ReadFolderResponse(nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) {
+ // okay we are doing a folder read in 8K chunks of a mail folder....
+ // this is almost too easy....we can just forward the data in this stream on
+ // to our folder parser object!!!
+
+ nsresult rv = NS_OK;
+ mCurrentProgress += length;
+
+ if (m_mailboxParser) {
+ rv = m_mailboxParser->OnDataAvailable(
+ nullptr, inputStream, sourceOffset,
+ length); // let the parser deal with it...
+ }
+ if (NS_FAILED(rv)) {
+ m_nextState = MAILBOX_ERROR_DONE; // drop out of the loop....
+ return -1;
+ }
+
+ // now wait for the next 8K chunk to come in.....
+ SetFlag(MAILBOX_PAUSE_FOR_READ);
+
+ // leave our state alone so when the next chunk of the mailbox comes in we
+ // jump to this state and repeat....how does this process end? Well when the
+ // file is done being read in, core net lib will issue an ::OnStopRequest to
+ // us...we'll use that as our sign to drop out of this state and to close the
+ // protocol instance...
+
+ return 0;
+}
+
+int32_t nsMailboxProtocol::ReadMessageResponse(nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) {
+ char* line = nullptr;
+ uint32_t status = 0;
+ nsresult rv = NS_OK;
+ mCurrentProgress += length;
+
+ // if we are doing a move or a copy, forward the data onto the copy handler...
+ // if we want to display the message then parse the incoming data...
+
+ if (m_channelListener) {
+ // just forward the data we read in to the listener...
+ rv = m_channelListener->OnDataAvailable(this, inputStream, sourceOffset,
+ length);
+ } else {
+ bool pauseForMoreData = false;
+ bool canonicalLineEnding = false;
+ nsCOMPtr<nsIMsgMessageUrl> msgurl = do_QueryInterface(m_runningUrl);
+
+ if (msgurl) msgurl->GetCanonicalLineEnding(&canonicalLineEnding);
+
+ while ((line = m_lineStreamBuffer->ReadNextLine(inputStream, status,
+ pauseForMoreData)) &&
+ !pauseForMoreData) {
+ /* When we're sending this line to a converter (ie,
+ it's a message/rfc822) use the local line termination
+ convention, not CRLF. This makes text articles get
+ saved with the local line terminators. Since SMTP
+ and NNTP mandate the use of CRLF, it is expected that
+ the local system will convert that to the local line
+ terminator as it is read.
+ */
+ // mscott - the firstline hack is aimed at making sure we don't write
+ // out the dummy header when we are trying to display the message.
+ // The dummy header is the From line with the date tag on it.
+ if (m_msgFileOutputStream && TestFlag(MAILBOX_MSG_PARSE_FIRST_LINE)) {
+ uint32_t count = 0;
+ rv = m_msgFileOutputStream->Write(line, PL_strlen(line), &count);
+ if (NS_FAILED(rv)) break;
+
+ if (canonicalLineEnding)
+ rv = m_msgFileOutputStream->Write(CRLF, 2, &count);
+ else
+ rv = m_msgFileOutputStream->Write(MSG_LINEBREAK, MSG_LINEBREAK_LEN,
+ &count);
+
+ if (NS_FAILED(rv)) break;
+ } else
+ SetFlag(MAILBOX_MSG_PARSE_FIRST_LINE);
+ PR_Free(line);
+ }
+ PR_Free(line);
+ }
+
+ SetFlag(MAILBOX_PAUSE_FOR_READ); // wait for more data to become available...
+ if (mProgressEventSink && m_runningUrl) {
+ int64_t maxProgress;
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(m_runningUrl));
+ mailnewsUrl->GetMaxProgress(&maxProgress);
+ mProgressEventSink->OnProgress(this, mCurrentProgress, maxProgress);
+ }
+
+ if (NS_FAILED(rv)) return -1;
+
+ return 0;
+}
+
+/*
+ * returns negative if the transfer is finished or error'd out
+ *
+ * returns zero or more if the transfer needs to be continued.
+ */
+nsresult nsMailboxProtocol::ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t offset,
+ uint32_t length) {
+ nsresult rv = NS_OK;
+ int32_t status = 0;
+ ClearFlag(MAILBOX_PAUSE_FOR_READ); /* already paused; reset */
+
+ while (!TestFlag(MAILBOX_PAUSE_FOR_READ)) {
+ switch (m_nextState) {
+ case MAILBOX_READ_MESSAGE:
+ if (inputStream == nullptr)
+ SetFlag(MAILBOX_PAUSE_FOR_READ);
+ else
+ status = ReadMessageResponse(inputStream, offset, length);
+ break;
+ case MAILBOX_READ_FOLDER:
+ if (inputStream == nullptr)
+ SetFlag(MAILBOX_PAUSE_FOR_READ); // wait for file socket to read in
+ // the next chunk...
+ else
+ status = ReadFolderResponse(inputStream, offset, length);
+ break;
+ case MAILBOX_DONE:
+ case MAILBOX_ERROR_DONE: {
+ nsCOMPtr<nsIMsgMailNewsUrl> anotherUrl =
+ do_QueryInterface(m_runningUrl);
+ rv = m_nextState == MAILBOX_DONE ? NS_OK : NS_ERROR_FAILURE;
+ anotherUrl->SetUrlState(false, rv);
+ m_nextState = MAILBOX_FREE;
+ } break;
+
+ case MAILBOX_FREE:
+ // MAILBOX is a one time use connection so kill it if we get here...
+ CloseSocket();
+ return rv; /* final end */
+
+ default: /* should never happen !!! */
+ m_nextState = MAILBOX_ERROR_DONE;
+ break;
+ }
+
+ /* check for errors during load and call error
+ * state if found
+ */
+ if (status < 0 && m_nextState != MAILBOX_FREE) {
+ m_nextState = MAILBOX_ERROR_DONE;
+ /* don't exit! loop around again and do the free case */
+ ClearFlag(MAILBOX_PAUSE_FOR_READ);
+ }
+ } /* while(!MAILBOX_PAUSE_FOR_READ) */
+
+ return rv;
+}
+
+nsresult nsMailboxProtocol::CloseSocket() {
+ // how do you force a release when closing the connection??
+ nsMsgProtocol::CloseSocket();
+ m_runningUrl = nullptr;
+ m_mailboxParser = nullptr;
+ return NS_OK;
+}
+
+// vim: ts=2 sw=2
diff --git a/comm/mailnews/local/src/nsMailboxProtocol.h b/comm/mailnews/local/src/nsMailboxProtocol.h
new file mode 100644
index 0000000000..287729cb44
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxProtocol.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsMailboxProtocol_h___
+#define nsMailboxProtocol_h___
+
+#include "mozilla/Attributes.h"
+#include "nsMsgProtocol.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+#include "nsIMailboxUrl.h"
+// State Flags (Note, I use the word state in terms of storing
+// state information about the connection (authentication, have we sent
+// commands, etc. I do not intend it to refer to protocol state)
+
+#define MAILBOX_PAUSE_FOR_READ \
+ 0x00000001 /* should we pause for the next read */
+#define MAILBOX_MSG_PARSE_FIRST_LINE \
+ 0x00000002 /* have we read in the first line of the msg */
+
+/* states of the machine
+ */
+typedef enum _MailboxStatesEnum {
+ MAILBOX_UNINITIALIZED,
+ MAILBOX_READ_FOLDER,
+ MAILBOX_READ_MESSAGE,
+ MAILBOX_DONE,
+ MAILBOX_ERROR_DONE,
+ MAILBOX_FREE,
+} MailboxStatesEnum;
+
+class nsMsgLineStreamBuffer;
+
+class nsMailboxProtocol : public nsMsgProtocol {
+ public:
+ // Creating a protocol instance requires the URL which needs to be run AND it
+ // requires a transport layer.
+ explicit nsMailboxProtocol(nsIURI* aURL);
+ virtual ~nsMailboxProtocol();
+
+ // initialization function given a new url and transport layer
+ nsresult Initialize(nsIURI* aURL);
+
+ // the consumer of the url might be something like an nsIDocShell....
+ virtual nsresult LoadUrl(nsIURI* aURL, nsISupports* aConsumer) override;
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIStreamListener interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ NS_IMETHOD OnStartRequest(nsIRequest* request) override;
+ NS_IMETHOD OnStopRequest(nsIRequest* request, nsresult aStatus) override;
+
+ private:
+ nsCOMPtr<nsIMailboxUrl>
+ m_runningUrl; // the nsIMailboxURL that is currently running
+ nsMailboxAction m_mailboxAction; // current mailbox action associated with
+ // this connection...
+ // Event sink handles
+ nsCOMPtr<nsIStreamListener> m_mailboxParser;
+
+ // Local state for the current operation
+ RefPtr<nsMsgLineStreamBuffer>
+ m_lineStreamBuffer; // used to efficiently extract lines from the
+ // incoming data stream
+
+ // Generic state information -- What state are we in? What state do we want to
+ // go to after the next response? What was the last response code? etc.
+ MailboxStatesEnum m_nextState;
+ MailboxStatesEnum m_initialState;
+
+ int64_t mCurrentProgress;
+
+ // can we just use the base class m_tempMsgFile?
+ nsCOMPtr<nsIFile> m_tempMessageFile;
+ nsCOMPtr<nsIOutputStream> m_msgFileOutputStream;
+
+ // this is used to hold the source mailbox file open when move/copying
+ // multiple messages.
+ nsCOMPtr<nsIInputStream> m_multipleMsgMoveCopyStream;
+
+ virtual nsresult ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) override;
+ virtual nsresult CloseSocket() override;
+
+ nsresult OpenMultipleMsgTransport(uint64_t offset, int64_t size);
+ bool RunningMultipleMsgUrl();
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // Protocol Methods --> This protocol is state driven so each protocol method
+ // is designed to re-act to the current "state". I've attempted to
+ // group them together based on functionality.
+ ////////////////////////////////////////////////////////////////////////////////////////
+
+ // When parsing a mailbox folder in chunks, this protocol state reads in the
+ // current chunk and forwards it to the mailbox parser.
+ int32_t ReadFolderResponse(nsIInputStream* inputStream, uint64_t sourceOffset,
+ uint32_t length);
+ int32_t ReadMessageResponse(nsIInputStream* inputStream,
+ uint64_t sourceOffset, uint32_t length);
+ nsresult DoneReadingMessage();
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // End of Protocol Methods
+ ////////////////////////////////////////////////////////////////////////////////////////
+};
+
+#endif // nsMailboxProtocol_h___
diff --git a/comm/mailnews/local/src/nsMailboxServer.cpp b/comm/mailnews/local/src/nsMailboxServer.cpp
new file mode 100644
index 0000000000..e0ddc26388
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxServer.cpp
@@ -0,0 +1,28 @@
+/**
+ * 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/. */
+
+#include "nsMailboxServer.h"
+#include "nsLocalMailFolder.h"
+
+NS_IMETHODIMP
+nsMailboxServer::GetLocalStoreType(nsACString& type) {
+ type.AssignLiteral("mailbox");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailboxServer::GetLocalDatabaseType(nsACString& type) {
+ type.AssignLiteral("mailbox");
+ return NS_OK;
+}
+
+nsresult nsMailboxServer::CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) {
+ nsMsgLocalMailFolder* newRootFolder = new nsMsgLocalMailFolder;
+ if (!newRootFolder) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*rootFolder = newRootFolder);
+ newRootFolder->Init(serverUri);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMailboxServer.h b/comm/mailnews/local/src/nsMailboxServer.h
new file mode 100644
index 0000000000..07bf628aae
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxServer.h
@@ -0,0 +1,22 @@
+/**
+ * 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/. */
+
+#ifndef nsMailboxServer_h__
+#define nsMailboxServer_h__
+
+#include "mozilla/Attributes.h"
+#include "nsMsgIncomingServer.h"
+
+class nsMailboxServer : public nsMsgIncomingServer {
+ public:
+ NS_IMETHOD GetLocalStoreType(nsACString& type) override;
+ NS_IMETHOD GetLocalDatabaseType(nsACString& type) override;
+
+ protected:
+ virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) override;
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsMailboxService.cpp b/comm/mailnews/local/src/nsMailboxService.cpp
new file mode 100644
index 0000000000..8a6a9766a8
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxService.cpp
@@ -0,0 +1,559 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsCOMPtr.h"
+
+#include "nsMailboxService.h"
+#include "nsMailboxUrl.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsMailboxProtocol.h"
+#include "nsIMsgDatabase.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsLocalUtils.h"
+#include "nsIDocShell.h"
+#include "nsMsgUtils.h"
+#include "nsPop3URL.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsNetUtil.h"
+#include "nsIWebNavigation.h"
+#include "prprf.h"
+#include "nsIMsgHdr.h"
+#include "nsIFileURL.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/LoadInfo.h"
+#include "nsDocShellLoadState.h"
+#include "nsContentUtils.h"
+#include "nsMsgFileHdr.h"
+
+nsMailboxService::nsMailboxService() {}
+
+nsMailboxService::~nsMailboxService() {}
+
+NS_IMPL_ISUPPORTS(nsMailboxService, nsIMailboxService, nsIMsgMessageService,
+ nsIProtocolHandler, nsIMsgMessageFetchPartService)
+
+nsresult nsMailboxService::ParseMailbox(nsIMsgWindow* aMsgWindow,
+ nsIFile* aMailboxPath,
+ nsIStreamListener* aMailboxParser,
+ nsIUrlListener* aUrlListener,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aMailboxPath);
+
+ nsresult rv;
+ nsCOMPtr<nsIMailboxUrl> mailboxurl =
+ do_CreateInstance("@mozilla.org/messenger/mailboxurl;1", &rv);
+ if (NS_SUCCEEDED(rv) && mailboxurl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> url = do_QueryInterface(mailboxurl);
+ // okay now generate the url string
+#ifdef XP_WIN
+ nsString path = aMailboxPath->NativePath();
+ nsCString mailboxPath;
+ NS_CopyUnicodeToNative(path, mailboxPath);
+#else
+ nsCString mailboxPath = aMailboxPath->NativePath();
+#endif
+ nsAutoCString buf;
+ MsgEscapeURL(mailboxPath,
+ nsINetUtil::ESCAPE_URL_MINIMAL | nsINetUtil::ESCAPE_URL_FORCED,
+ buf);
+ nsEscapeNativePath(buf);
+ url->SetUpdatingFolder(true);
+ url->SetMsgWindow(aMsgWindow);
+ nsAutoCString uriSpec("mailbox://");
+ uriSpec.Append(buf);
+ rv = url->SetSpecInternal(uriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mailboxurl->SetMailboxParser(aMailboxParser);
+ if (aUrlListener) url->RegisterListener(aUrlListener);
+
+ rv = RunMailboxUrl(url, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aURL) {
+ url.forget(aURL);
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMailboxService::CopyMessage(const nsACString& aSrcMailboxURI,
+ nsIStreamListener* aMailboxCopyHandler,
+ bool moveMessage,
+ nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsMailboxAction mailboxAction = nsIMailboxUrl::ActionMoveMessage;
+ nsCOMPtr<nsIURI> aURL; // unused...
+ if (!moveMessage) mailboxAction = nsIMailboxUrl::ActionCopyMessage;
+ return FetchMessage(aSrcMailboxURI, aMailboxCopyHandler, aMsgWindow,
+ aUrlListener, nullptr, mailboxAction, false,
+ getter_AddRefs(aURL));
+}
+
+nsresult nsMailboxService::CopyMessages(
+ const nsTArray<nsMsgKey>& aMsgKeys, nsIMsgFolder* srcFolder,
+ nsIStreamListener* aMailboxCopyHandler, bool moveMessage,
+ nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG(srcFolder);
+ NS_ENSURE_TRUE(!aMsgKeys.IsEmpty(), NS_ERROR_INVALID_ARG);
+ nsCOMPtr<nsIMailboxUrl> mailboxurl;
+
+ nsMailboxAction actionToUse = nsIMailboxUrl::ActionMoveMessage;
+ if (!moveMessage) actionToUse = nsIMailboxUrl::ActionCopyMessage;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgDatabase> db;
+ srcFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ db->GetMsgHdrForKey(aMsgKeys[0], getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsCString uri;
+ srcFolder->GetUriForMsg(msgHdr, uri);
+ rv = PrepareMessageUrl(uri, aUrlListener, actionToUse,
+ getter_AddRefs(mailboxurl), aMsgWindow);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> url = do_QueryInterface(mailboxurl);
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(url));
+ nsCOMPtr<nsIMailboxUrl> mailboxUrl(do_QueryInterface(url));
+ msgUrl->SetMsgWindow(aMsgWindow);
+
+ mailboxUrl->SetMoveCopyMsgKeys(aMsgKeys);
+ rv = RunMailboxUrl(url, aMailboxCopyHandler);
+ }
+ }
+ }
+ if (aURL && mailboxurl) CallQueryInterface(mailboxurl, aURL);
+
+ return rv;
+}
+
+nsresult nsMailboxService::FetchMessage(
+ const nsACString& aMessageURI, nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener,
+ const char* aFileName, /* only used by open attachment... */
+ nsMailboxAction mailboxAction, bool aAutodetectCharset, nsIURI** aURL) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMailboxUrl> mailboxurl;
+ nsMailboxAction actionToUse = mailboxAction;
+ nsCOMPtr<nsIURI> url;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl;
+ nsAutoCString uriString(aMessageURI);
+
+ if (StringBeginsWith(aMessageURI, "file:"_ns)) {
+ int64_t fileSize;
+ nsCOMPtr<nsIURI> fileUri;
+ rv = NS_NewURI(getter_AddRefs(fileUri), aMessageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(fileUri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> file;
+ rv = fileUrl->GetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ file->GetFileSize(&fileSize);
+ uriString.Replace(0, 5, "mailbox:"_ns);
+ uriString.AppendLiteral("&number=0");
+ rv = NS_NewURI(getter_AddRefs(url), uriString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgUrl = do_QueryInterface(url);
+ if (msgUrl) {
+ msgUrl->SetMsgWindow(aMsgWindow);
+ nsCOMPtr<nsIMailboxUrl> mailboxUrl = do_QueryInterface(msgUrl, &rv);
+ mailboxUrl->SetMessageSize((uint32_t)fileSize);
+ }
+ } else {
+ // this happens with forward inline of message/rfc822 attachment
+ // opened in a stand-alone msg window.
+ int32_t typeIndex = uriString.Find("&type=application/x-message-display");
+ if (typeIndex != -1) {
+ uriString.Cut(typeIndex,
+ sizeof("&type=application/x-message-display") - 1);
+ rv = NS_NewURI(getter_AddRefs(url), uriString.get());
+ mailboxurl = do_QueryInterface(url);
+ } else
+ rv = PrepareMessageUrl(aMessageURI, aUrlListener, actionToUse,
+ getter_AddRefs(mailboxurl), aMsgWindow);
+
+ if (NS_SUCCEEDED(rv)) {
+ url = do_QueryInterface(mailboxurl);
+ msgUrl = do_QueryInterface(url);
+ msgUrl->SetMsgWindow(aMsgWindow);
+ if (aFileName) msgUrl->SetFileNameInternal(nsDependentCString(aFileName));
+ }
+ }
+
+ nsCOMPtr<nsIMsgI18NUrl> i18nurl(do_QueryInterface(msgUrl));
+ if (i18nurl) i18nurl->SetAutodetectCharset(aAutodetectCharset);
+
+ // instead of running the mailbox url like we used to, let's try to run the
+ // url in the docshell...
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aDisplayConsumer, &rv));
+ // if we were given a docShell, run the url in the docshell..otherwise just
+ // run it normally.
+ if (NS_SUCCEEDED(rv) && docShell && url) {
+ // DIRTY LITTLE HACK --> if we are opening an attachment we want the
+ // docshell to treat this load as if it were a user click event. Then the
+ // dispatching stuff will be much happier.
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url);
+ loadState->SetLoadFlags(mailboxAction == nsIMailboxUrl::ActionFetchPart
+ ? nsIWebNavigation::LOAD_FLAGS_IS_LINK
+ : nsIWebNavigation::LOAD_FLAGS_NONE);
+ if (mailboxAction == nsIMailboxUrl::ActionFetchPart)
+ loadState->SetLoadType(LOAD_LINK);
+ loadState->SetFirstParty(false);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ rv = docShell->LoadURI(loadState, false);
+ } else
+ rv = RunMailboxUrl(url, aDisplayConsumer);
+
+ if (aURL && mailboxurl) CallQueryInterface(mailboxurl, aURL);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxService::FetchMimePart(
+ nsIURI* aURI, const nsACString& aMessageURI, nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener, nsIURI** aURL) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(aURI, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgUrl->SetMsgWindow(aMsgWindow);
+
+ // set up the url listener
+ if (aUrlListener) msgUrl->RegisterListener(aUrlListener);
+
+ return RunMailboxUrl(msgUrl, aDisplayConsumer);
+}
+
+NS_IMETHODIMP nsMailboxService::LoadMessage(const nsACString& aMessageURI,
+ nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ bool aOverideCharset) {
+ nsCOMPtr<nsIURI> aURL; // unused...
+ return FetchMessage(aMessageURI, aDisplayConsumer, aMsgWindow, aUrlListener,
+ nullptr, nsIMailboxUrl::ActionFetchMessage,
+ aOverideCharset, getter_AddRefs(aURL));
+}
+
+NS_IMETHODIMP
+nsMailboxService::StreamMessage(const nsACString& aMessageURI,
+ nsISupports* aConsumer,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ bool /* aConvertData */,
+ const nsACString& aAdditionalHeader,
+ bool aLocalOnly, nsIURI** aURL) {
+ // The mailbox protocol object will look for "header=filter" or
+ // "header=attach" to decide if it wants to convert the data instead of
+ // using aConvertData. It turns out to be way too hard to pass aConvertData
+ // all the way over to the mailbox protocol object.
+ nsAutoCString aURIString(aMessageURI);
+ if (!aAdditionalHeader.IsEmpty()) {
+ aURIString.FindChar('?') == -1 ? aURIString += "?" : aURIString += "&";
+ aURIString += "header=";
+ aURIString += aAdditionalHeader;
+ }
+
+ return FetchMessage(aURIString, aConsumer, aMsgWindow, aUrlListener, nullptr,
+ nsIMailboxUrl::ActionFetchMessage, false, aURL);
+}
+
+NS_IMETHODIMP nsMailboxService::StreamHeaders(const nsACString& aMessageURI,
+ nsIStreamListener* aConsumer,
+ nsIUrlListener* aUrlListener,
+ bool aLocalOnly, nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aConsumer);
+ nsAutoCString folderURI;
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv =
+ DecomposeMailboxURI(aMessageURI, getter_AddRefs(folder), &msgKey);
+ if (msgKey == nsMsgKey_None) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = db->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = folder->GetLocalMsgStream(msgHdr, getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return MsgStreamMsgHeaders(inputStream, aConsumer);
+}
+
+NS_IMETHODIMP nsMailboxService::IsMsgInMemCache(nsIURI* aUrl,
+ nsIMsgFolder* aFolder,
+ bool* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMailboxService::SaveMessageToDisk(const nsACString& aMessageURI,
+ nsIFile* aFile, bool aAddDummyEnvelope,
+ nsIUrlListener* aUrlListener, nsIURI** aURL,
+ bool canonicalLineEnding,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMailboxUrl> mailboxurl;
+
+ rv = PrepareMessageUrl(aMessageURI, aUrlListener,
+ nsIMailboxUrl::ActionSaveMessageToDisk,
+ getter_AddRefs(mailboxurl), aMsgWindow);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(mailboxurl);
+ if (msgUrl) {
+ msgUrl->SetMessageFile(aFile);
+ msgUrl->SetAddDummyEnvelope(aAddDummyEnvelope);
+ msgUrl->SetCanonicalLineEnding(canonicalLineEnding);
+ }
+
+ nsCOMPtr<nsIURI> url = do_QueryInterface(mailboxurl);
+ rv = RunMailboxUrl(url);
+ }
+
+ if (aURL && mailboxurl) CallQueryInterface(mailboxurl, aURL);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxService::GetUrlForUri(const nsACString& aMessageURI,
+ nsIMsgWindow* aMsgWindow,
+ nsIURI** aURL) {
+ NS_ENSURE_ARG_POINTER(aURL);
+ if (StringBeginsWith(aMessageURI, "file:"_ns) ||
+ PL_strstr(PromiseFlatCString(aMessageURI).get(),
+ "type=application/x-message-display") ||
+ StringBeginsWith(aMessageURI, "mailbox:"_ns))
+ return NS_NewURI(aURL, aMessageURI);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMailboxUrl> mailboxurl;
+ rv =
+ PrepareMessageUrl(aMessageURI, nullptr, nsIMailboxUrl::ActionFetchMessage,
+ getter_AddRefs(mailboxurl), aMsgWindow);
+ if (NS_SUCCEEDED(rv) && mailboxurl) rv = CallQueryInterface(mailboxurl, aURL);
+ return rv;
+}
+
+// Takes a mailbox url, this method creates a protocol instance and loads the
+// url into the protocol instance.
+nsresult nsMailboxService::RunMailboxUrl(nsIURI* aMailboxUrl,
+ nsISupports* aDisplayConsumer) {
+ // create a protocol instance to run the url..
+ RefPtr<nsMailboxProtocol> protocol = new nsMailboxProtocol(aMailboxUrl);
+ // It implements nsIChannel, and all channels require loadInfo.
+ protocol->SetLoadInfo(new mozilla::net::LoadInfo(
+ nsContentUtils::GetSystemPrincipal(), nullptr, nullptr,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER));
+ nsresult rv = protocol->Initialize(aMailboxUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return protocol->LoadUrl(aMailboxUrl, aDisplayConsumer);
+}
+
+// This function takes a message uri, converts it into a file path & msgKey
+// pair. It then turns that into a mailbox url object. It also registers a url
+// listener if appropriate. AND it can take in a mailbox action and set that
+// field on the returned url as well.
+nsresult nsMailboxService::PrepareMessageUrl(
+ const nsACString& aSrcMsgMailboxURI, nsIUrlListener* aUrlListener,
+ nsMailboxAction aMailboxAction, nsIMailboxUrl** aMailboxUrl,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv =
+ CallCreateInstance("@mozilla.org/messenger/mailboxurl;1", aMailboxUrl);
+ if (NS_SUCCEEDED(rv) && aMailboxUrl && *aMailboxUrl) {
+ // okay now generate the url string
+ char* urlSpec;
+ nsAutoCString folderURI;
+ nsMsgKey msgKey;
+ nsCString folderPath;
+ const nsPromiseFlatCString& flat = PromiseFlatCString(aSrcMsgMailboxURI);
+ const char* part = PL_strstr(flat.get(), "part=");
+ const char* header = PL_strstr(flat.get(), "header=");
+ rv = nsParseLocalMessageURI(aSrcMsgMailboxURI, folderURI, &msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsLocalURI2Path(kMailboxRootURI, folderURI.get(), folderPath);
+
+ if (NS_SUCCEEDED(rv)) {
+ // set up the url spec and initialize the url with it.
+ nsAutoCString buf;
+ MsgEscapeURL(
+ folderPath,
+ nsINetUtil::ESCAPE_URL_DIRECTORY | nsINetUtil::ESCAPE_URL_FORCED,
+ buf);
+ if (part)
+ urlSpec =
+ PR_smprintf("mailbox://%s?number=%lu&%s", buf.get(), msgKey, part);
+ else if (header)
+ urlSpec = PR_smprintf("mailbox://%s?number=%lu&%s", buf.get(), msgKey,
+ header);
+ else
+ urlSpec = PR_smprintf("mailbox://%s?number=%lu", buf.get(), msgKey);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> url = do_QueryInterface(*aMailboxUrl);
+ rv = url->SetSpecInternal(nsDependentCString(urlSpec));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_smprintf_free(urlSpec);
+
+ (*aMailboxUrl)->SetMailboxAction(aMailboxAction);
+
+ // set up the url listener
+ if (aUrlListener) rv = url->RegisterListener(aUrlListener);
+
+ url->SetMsgWindow(msgWindow);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(url);
+ if (msgUrl) {
+ msgUrl->SetOriginalSpec(aSrcMsgMailboxURI);
+ msgUrl->SetUri(aSrcMsgMailboxURI);
+ }
+
+ } // if we got a url
+ } // if we got a url
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxService::GetScheme(nsACString& aScheme) {
+ aScheme = "mailbox";
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxService::AllowPort(int32_t port, const char* scheme,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ // don't override anything.
+ *_retval = false;
+ return NS_OK;
+}
+
+nsresult nsMailboxService::NewURI(const nsACString& aSpec,
+ const char* aOriginCharset, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = 0;
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> aMsgUri =
+ do_CreateInstance("@mozilla.org/messenger/mailboxurl;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // SetSpecInternal must not fail, or else the URL won't have a base URL and
+ // we'll crash later.
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ rv = aBaseURI->Resolve(aSpec, newSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aMsgUri->SetSpecInternal(newSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = aMsgUri->SetSpecInternal(aSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ aMsgUri.forget(_retval);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxService::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** _retval) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+ MOZ_ASSERT(aLoadInfo);
+ nsresult rv = NS_OK;
+ nsAutoCString spec;
+ rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (spec.Find("?uidl=") >= 0 || spec.Find("&uidl=") >= 0) {
+ nsCOMPtr<nsIProtocolHandler> handler =
+ do_GetService("@mozilla.org/network/protocol;1?name=pop", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIURI> pop3Uri;
+
+ rv = nsPop3URL::NewURI(spec, aURI, getter_AddRefs(pop3Uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return handler->NewChannel(pop3Uri, aLoadInfo, _retval);
+ }
+ }
+
+ RefPtr<nsMailboxProtocol> protocol = new nsMailboxProtocol(aURI);
+ if (!protocol) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ rv = protocol->Initialize(aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = protocol->SetLoadInfo(aLoadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the attachment disposition. This forces docShell to open the
+ // attachment instead of displaying it. Content types we have special
+ // handlers for are white-listed. This white list also exists in
+ // nsImapService::NewChannel and nsNntpService::NewChannel, so if you're
+ // changing this, update those too.
+ if (spec.Find("part=") >= 0 && spec.Find("type=message/rfc822") < 0 &&
+ spec.Find("type=application/x-message-display") < 0 &&
+ spec.Find("type=application/pdf") < 0) {
+ rv = protocol->SetContentDisposition(nsIChannel::DISPOSITION_ATTACHMENT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ protocol.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxService::Search(nsIMsgSearchSession* aSearchSession,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgFolder* aMsgFolder,
+ const nsACString& aMessageUri) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMailboxService::DecomposeMailboxURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder,
+ nsMsgKey* aMsgKey) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aMsgKey);
+
+ nsresult rv = NS_OK;
+ nsAutoCString folderURI;
+ rv = nsParseLocalMessageURI(aMessageURI, folderURI, aMsgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetOrCreateFolder(folderURI, aFolder);
+}
+
+NS_IMETHODIMP
+nsMailboxService::MessageURIToMsgHdr(const nsACString& uri,
+ nsIMsgDBHdr** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ if (StringBeginsWith(uri, "file:"_ns)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr = new nsMsgFileHdr(uri);
+ msgHdr.forget(_retval);
+ return NS_OK;
+ }
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsMsgKey msgKey;
+
+ rv = DecomposeMailboxURI(uri, getter_AddRefs(folder), &msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->GetMessageHeader(msgKey, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMailboxService.h b/comm/mailnews/local/src/nsMailboxService.h
new file mode 100644
index 0000000000..1ab9be1b67
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxService.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMailboxService_h___
+#define nsMailboxService_h___
+
+#include "nscore.h"
+#include "nsISupports.h"
+
+#include "nsIMailboxService.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgWindow.h"
+#include "nsIMailboxUrl.h"
+#include "nsIURI.h"
+#include "nsIUrlListener.h"
+#include "nsIProtocolHandler.h"
+
+class nsMailboxService : public nsIMailboxService,
+ public nsIMsgMessageService,
+ public nsIMsgMessageFetchPartService,
+ public nsIProtocolHandler {
+ public:
+ nsMailboxService();
+ static nsresult NewURI(const nsACString& aSpec, const char* aOriginCharset,
+ nsIURI* aBaseURI, nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMAILBOXSERVICE
+ NS_DECL_NSIMSGMESSAGESERVICE
+ NS_DECL_NSIMSGMESSAGEFETCHPARTSERVICE
+ NS_DECL_NSIPROTOCOLHANDLER
+
+ protected:
+ virtual ~nsMailboxService();
+
+ // helper functions used by the service
+ nsresult PrepareMessageUrl(const nsACString& aSrcMsgMailboxURI,
+ nsIUrlListener* aUrlListener,
+ nsMailboxAction aMailboxAction,
+ nsIMailboxUrl** aMailboxUrl,
+ nsIMsgWindow* msgWindow);
+
+ nsresult RunMailboxUrl(nsIURI* aMailboxUrl,
+ nsISupports* aDisplayConsumer = nullptr);
+
+ nsresult FetchMessage(
+ const nsACString& aMessageURI, nsISupports* aDisplayConsumer,
+ nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener,
+ const char* aFileName, /* only used by open attachment */
+ nsMailboxAction mailboxAction, bool aAutodetectCharset, nsIURI** aURL);
+
+ nsresult DecomposeMailboxURI(const nsACString& aMessageURI,
+ nsIMsgFolder** aFolder, nsMsgKey* aMsgKey);
+};
+
+#endif /* nsMailboxService_h___ */
diff --git a/comm/mailnews/local/src/nsMailboxUrl.cpp b/comm/mailnews/local/src/nsMailboxUrl.cpp
new file mode 100644
index 0000000000..841b6d8922
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxUrl.cpp
@@ -0,0 +1,474 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsIURI.h"
+#include "nsIMailboxUrl.h"
+#include "nsMailboxUrl.h"
+
+#include "nsString.h"
+#include "nsLocalUtils.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+
+#include "nsIMsgFolder.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsIMsgMailSession.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIStandardURL.h"
+
+// this is totally lame and MUST be removed by M6
+// the real fix is to attach the URI to the URL as it runs through netlib
+// then grab it and use it on the other side
+#include "nsCOMPtr.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+
+// helper function for parsing the search field of a url
+char* extractAttributeValue(const char* searchString,
+ const char* attributeName);
+
+nsMailboxUrl::nsMailboxUrl() {
+ m_mailboxAction = nsIMailboxUrl::ActionParseMailbox;
+ m_filePath = nullptr;
+ m_messageID = nullptr;
+ m_messageKey = nsMsgKey_None;
+ m_messageSize = 0;
+ m_messageFile = nullptr;
+ m_addDummyEnvelope = false;
+ m_canonicalLineEnding = false;
+ m_curMsgIndex = 0;
+ mAutodetectCharset = false;
+}
+
+nsMailboxUrl::~nsMailboxUrl() { PR_Free(m_messageID); }
+
+NS_IMPL_ADDREF_INHERITED(nsMailboxUrl, nsMsgMailNewsUrl)
+NS_IMPL_RELEASE_INHERITED(nsMailboxUrl, nsMsgMailNewsUrl)
+
+NS_INTERFACE_MAP_BEGIN(nsMailboxUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMailboxUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMailboxUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl)
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIMailboxUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+nsresult nsMailboxUrl::SetMailboxParser(nsIStreamListener* aMailboxParser) {
+ if (aMailboxParser) m_mailboxParser = aMailboxParser;
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::GetMailboxParser(nsIStreamListener** aConsumer) {
+ NS_ENSURE_ARG_POINTER(aConsumer);
+
+ NS_IF_ADDREF(*aConsumer = m_mailboxParser);
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::SetMailboxCopyHandler(
+ nsIStreamListener* aMailboxCopyHandler) {
+ if (aMailboxCopyHandler) m_mailboxCopyHandler = aMailboxCopyHandler;
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::GetMailboxCopyHandler(
+ nsIStreamListener** aMailboxCopyHandler) {
+ NS_ENSURE_ARG_POINTER(aMailboxCopyHandler);
+
+ if (aMailboxCopyHandler) {
+ NS_IF_ADDREF(*aMailboxCopyHandler = m_mailboxCopyHandler);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::GetMessageKey(nsMsgKey* aMessageKey) {
+ *aMessageKey = m_messageKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetMessageSize(uint32_t* aMessageSize) {
+ if (aMessageSize) {
+ *aMessageSize = m_messageSize;
+ return NS_OK;
+ } else
+ return NS_ERROR_NULL_POINTER;
+}
+
+nsresult nsMailboxUrl::SetMessageSize(uint32_t aMessageSize) {
+ m_messageSize = aMessageSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsURL;
+ QueryInterface(NS_GET_IID(nsIMsgMailNewsUrl), getter_AddRefs(mailnewsURL));
+
+ nsAutoCString spec;
+ mailnewsURL->GetSpecIgnoringRef(spec);
+
+ // mailbox: URLs contain a lot of query parts. We want need a normalized form:
+ // mailbox:///path/to/folder?number=nn.
+ // We also need to translate the second form
+ // mailbox://user@domain@server/folder?number=nn.
+
+ char* messageKey = extractAttributeValue(spec.get(), "number=");
+
+ // Strip any query part beginning with ? or /;
+ MsgRemoveQueryPart(spec);
+
+ // Check for format lacking absolute path.
+ if (spec.Find("///") == kNotFound) {
+ nsCString folderPath;
+ nsresult rv = nsLocalURI2Path(kMailboxRootURI, spec.get(), folderPath);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString buf;
+ MsgEscapeURL(
+ folderPath,
+ nsINetUtil::ESCAPE_URL_DIRECTORY | nsINetUtil::ESCAPE_URL_FORCED,
+ buf);
+ spec = "mailbox://"_ns + buf;
+ }
+ }
+
+ if (messageKey) {
+ spec += "?number="_ns;
+ spec.Append(messageKey);
+ PR_Free(messageKey);
+ }
+
+ aPrincipalSpec.Assign(spec);
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::CreateURL(const nsACString& aSpec, nsIURL** aURL) {
+ nsresult rv;
+ nsCOMPtr<nsIURL> url;
+ // Check whether the URL is of the form
+ // mailbox://user@domain@server/folder?number=nn and contains a hostname.
+ // Check for format lacking absolute path.
+ if (PromiseFlatCString(aSpec).Find("///") == kNotFound) {
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(aSpec)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // The URL is more like a file URL without a hostname.
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .Apply(&nsIStandardURLMutator::Init,
+ nsIStandardURL::URLTYPE_NO_AUTHORITY, -1,
+ PromiseFlatCString(aSpec), nullptr, nullptr, nullptr)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ url.forget(aURL);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::SetUri(const nsACString& aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::Clone(nsIURI** _retval) {
+ nsresult rv = nsMsgMailNewsUrl::Clone(_retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // also clone the mURI member, because GetUri below won't work if
+ // mURI isn't set due to nsIFile fun.
+ nsCOMPtr<nsIMsgMessageUrl> clonedUrl = do_QueryInterface(*_retval);
+ if (clonedUrl) clonedUrl->SetUri(mURI);
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetUri(nsACString& aURI) {
+ // if we have been given a uri to associate with this url, then use it
+ // otherwise try to reconstruct a URI on the fly....
+
+ if (!mURI.IsEmpty())
+ aURI = mURI;
+ else {
+ if (m_filePath) {
+ nsAutoCString baseUri;
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we blow off errors here so that we can open attachments
+ // in .eml files.
+ (void)accountManager->FolderUriForPath(m_filePath, baseUri);
+ if (baseUri.IsEmpty()) {
+ rv = m_baseURL->GetSpec(baseUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCString baseMessageURI;
+ nsCreateLocalBaseMessageURI(baseUri, baseMessageURI);
+ nsBuildLocalMessageURI(baseMessageURI, m_messageKey, aURI);
+ } else
+ aURI = "";
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::GetMsgHdrForKey(nsMsgKey msgKey, nsIMsgDBHdr** aMsgHdr) {
+ nsresult rv = NS_OK;
+ if (aMsgHdr && m_filePath) {
+ nsCOMPtr<nsIMsgDatabase> mailDBFactory;
+ nsCOMPtr<nsIMsgDatabase> mailDB;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+
+ if (msgDBService) {
+ rv = msgDBService->OpenMailDBFromFile(m_filePath, nullptr, false, false,
+ getter_AddRefs(mailDB));
+ }
+ if (NS_SUCCEEDED(rv) && mailDB) {
+ // Did we get a db back?
+ rv = mailDB->GetMsgHdrForKey(msgKey, aMsgHdr);
+ } else {
+ rv = NS_OK;
+ }
+ } else {
+ rv = NS_ERROR_NULL_POINTER;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) {
+ return GetMsgHdrForKey(m_messageKey, aMsgHdr);
+}
+
+NS_IMPL_GETSET(nsMailboxUrl, AddDummyEnvelope, bool, m_addDummyEnvelope)
+NS_IMPL_GETSET(nsMailboxUrl, CanonicalLineEnding, bool, m_canonicalLineEnding)
+
+NS_IMETHODIMP
+nsMailboxUrl::GetOriginalSpec(nsACString& aSpec) {
+ if (m_originalSpec.IsEmpty()) return NS_ERROR_NULL_POINTER;
+ aSpec = m_originalSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailboxUrl::SetOriginalSpec(const nsACString& aSpec) {
+ m_originalSpec = aSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::SetMessageFile(nsIFile* aFile) {
+ m_messageFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetMessageFile(nsIFile** aFile) {
+ // why don't we return an error for null aFile?
+ if (aFile) NS_IF_ADDREF(*aFile = m_messageFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+
+ switch (type) {
+ case nsIMsgMailNewsUrl::eCopy:
+ *isType = (m_mailboxAction == nsIMailboxUrl::ActionCopyMessage);
+ break;
+ case nsIMsgMailNewsUrl::eMove:
+ *isType = (m_mailboxAction == nsIMailboxUrl::ActionMoveMessage);
+ break;
+ case nsIMsgMailNewsUrl::eDisplay:
+ *isType = (m_mailboxAction == nsIMailboxUrl::ActionFetchMessage ||
+ m_mailboxAction == nsIMailboxUrl::ActionFetchPart);
+ break;
+ default:
+ *isType = false;
+ };
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// End nsIMailboxUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// possible search part phrases include: MessageID=id&number=MessageKey
+
+nsresult nsMailboxUrl::ParseSearchPart() {
+ nsAutoCString searchPart;
+ nsresult rv = GetQuery(searchPart);
+ // add code to this function to decompose everything past the '?'.....
+ if (NS_SUCCEEDED(rv) && !searchPart.IsEmpty()) {
+ // the action for this mailbox must be a display message...
+ char* msgPart = extractAttributeValue(searchPart.get(), "part=");
+ if (msgPart) // if we have a part in the url then we must be fetching just
+ // the part.
+ m_mailboxAction = nsIMailboxUrl::ActionFetchPart;
+ else
+ m_mailboxAction = nsIMailboxUrl::ActionFetchMessage;
+
+ char* messageKey = extractAttributeValue(searchPart.get(), "number=");
+ m_messageID = extractAttributeValue(searchPart.get(), "messageid=");
+ if (messageKey)
+ m_messageKey =
+ (nsMsgKey)ParseUint64Str(messageKey); // convert to a uint32_t...
+
+ PR_Free(msgPart);
+ PR_Free(messageKey);
+ } else
+ m_mailboxAction = nsIMailboxUrl::ActionParseMailbox;
+
+ return rv;
+}
+
+// warning: don't assume when parsing the url that the protocol part is
+// "news"...
+nsresult nsMailboxUrl::ParseUrl() {
+ GetFilePath(m_file);
+
+ ParseSearchPart();
+ // ### fix me.
+ // this hack is to avoid asserting on every local message loaded because the
+ // security manager is creating an empty "mailbox://" uri for every message.
+ if (m_file.Length() < 2)
+ m_filePath = nullptr;
+ else {
+ nsCString fileUri("file://");
+ fileUri.Append(m_file);
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIURI> uri;
+ rv = ioService->NewURI(fileUri, nullptr, nullptr, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> fileURLFile;
+ fileURL->GetFile(getter_AddRefs(fileURLFile));
+ NS_ENSURE_TRUE(fileURLFile, NS_ERROR_NULL_POINTER);
+ m_filePath = fileURLFile;
+ }
+
+ GetPathQueryRef(m_file);
+ return NS_OK;
+}
+
+nsresult nsMailboxUrl::SetSpecInternal(const nsACString& aSpec) {
+ nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(aSpec);
+ if (NS_SUCCEEDED(rv)) {
+ // Do not try to parse URLs of the form
+ // mailbox://user@domain@server/folder?number=nn since this will fail.
+ // Check for format lacking absolute path.
+ if (PromiseFlatCString(aSpec).Find("///") != kNotFound) {
+ rv = ParseUrl();
+ } else {
+ // We still need to parse the search part to populate
+ // m_messageKey etc.
+ ParseSearchPart();
+ }
+ }
+ return rv;
+}
+
+nsresult nsMailboxUrl::SetQuery(const nsACString& aQuery) {
+ nsresult rv = nsMsgMailNewsUrl::SetQuery(aQuery);
+ if (NS_SUCCEEDED(rv)) rv = ParseUrl();
+ return rv;
+}
+
+// takes a string like ?messageID=fooo&number=MsgKey and returns a new string
+// containing just the attribute value. i.e you could pass in this string with
+// an attribute name of messageID and I'll return fooo. Use PR_Free to delete
+// this string...
+
+// Assumption: attribute pairs in the string are separated by '&'.
+char* extractAttributeValue(const char* searchString,
+ const char* attributeName) {
+ char* attributeValue = nullptr;
+
+ if (searchString && attributeName) {
+ // search the string for attributeName
+ uint32_t attributeNameSize = PL_strlen(attributeName);
+ char* startOfAttribute = PL_strcasestr(searchString, attributeName);
+ if (startOfAttribute) {
+ startOfAttribute += attributeNameSize; // skip over the attributeName
+ if (startOfAttribute) // is there something after the attribute name
+ {
+ char* endOfAttribute = PL_strchr(startOfAttribute, '&');
+ nsDependentCString attributeValueStr;
+ if (startOfAttribute &&
+ endOfAttribute) // is there text after attribute value
+ attributeValueStr.Assign(startOfAttribute,
+ endOfAttribute - startOfAttribute);
+ else // there is nothing left so eat up rest of line.
+ attributeValueStr.Assign(startOfAttribute);
+
+ // now unescape the string...
+ nsCString unescapedValue;
+ MsgUnescapeString(attributeValueStr, 0, unescapedValue);
+ attributeValue = PL_strdup(unescapedValue.get());
+ } // if we have a attribute value
+
+ } // if we have a attribute name
+ } // if we got non-null search string and attribute name values
+
+ return attributeValue;
+}
+
+// nsIMsgI18NUrl support
+
+nsresult nsMailboxUrl::GetFolder(nsIMsgFolder** msgFolder) {
+ // if we have a RDF URI, then try to get the folder for that URI and then ask
+ // the folder for it's charset....
+ nsCString uri;
+ GetUri(uri);
+ NS_ENSURE_TRUE(!uri.IsEmpty(), NS_ERROR_FAILURE);
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ GetMsgDBHdrFromURI(uri, getter_AddRefs(msg));
+ if (!msg) return NS_ERROR_FAILURE;
+ return msg->GetFolder(msgFolder);
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetAutodetectCharset(bool* aAutodetectCharset) {
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::SetAutodetectCharset(bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::SetMoveCopyMsgKeys(
+ const nsTArray<nsMsgKey>& keysToFlag) {
+ m_keys = keysToFlag.Clone();
+ if (!m_keys.IsEmpty() && m_messageKey == nsMsgKey_None)
+ m_messageKey = m_keys[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetMoveCopyMsgHdrForIndex(uint32_t msgIndex,
+ nsIMsgDBHdr** msgHdr) {
+ NS_ENSURE_ARG(msgHdr);
+ if (msgIndex < m_keys.Length()) {
+ nsMsgKey nextKey = m_keys[msgIndex];
+ return GetMsgHdrForKey(nextKey, msgHdr);
+ }
+ return NS_MSG_MESSAGE_NOT_FOUND;
+}
+
+NS_IMETHODIMP nsMailboxUrl::GetNumMoveCopyMsgs(uint32_t* numMsgs) {
+ NS_ENSURE_ARG(numMsgs);
+ *numMsgs = m_keys.Length();
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMailboxUrl.h b/comm/mailnews/local/src/nsMailboxUrl.h
new file mode 100644
index 0000000000..29b932e38c
--- /dev/null
+++ b/comm/mailnews/local/src/nsMailboxUrl.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsMailboxUrl_h__
+#define nsMailboxUrl_h__
+
+#include "mozilla/Attributes.h"
+#include "nsIMailboxUrl.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsIStreamListener.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+
+class nsMailboxUrl : public nsIMailboxUrl,
+ public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public nsIMsgI18NUrl {
+ public:
+ // nsIMsgMailNewsUrl override
+ nsresult SetSpecInternal(const nsACString& aSpec) override;
+ nsresult SetQuery(const nsACString& aQuery) override;
+ nsresult CreateURL(const nsACString& aSpec, nsIURL** aURL) override;
+
+ // from nsIMailboxUrl:
+ NS_IMETHOD SetMailboxParser(nsIStreamListener* aConsumer) override;
+ NS_IMETHOD GetMailboxParser(nsIStreamListener** aConsumer) override;
+ NS_IMETHOD SetMailboxCopyHandler(nsIStreamListener* aConsumer) override;
+ NS_IMETHOD GetMailboxCopyHandler(nsIStreamListener** aConsumer) override;
+
+ NS_IMETHOD GetMessageKey(nsMsgKey* aMessageKey) override;
+ NS_IMETHOD GetMessageSize(uint32_t* aMessageSize) override;
+ NS_IMETHOD SetMessageSize(uint32_t aMessageSize) override;
+ NS_IMETHOD GetMailboxAction(nsMailboxAction* result) override {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_mailboxAction;
+ return NS_OK;
+ }
+ NS_IMETHOD SetMailboxAction(nsMailboxAction aAction) override {
+ m_mailboxAction = aAction;
+ return NS_OK;
+ }
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+ NS_IMETHOD SetMoveCopyMsgKeys(const nsTArray<nsMsgKey>& keysToFlag) override;
+ NS_IMETHOD GetMoveCopyMsgHdrForIndex(uint32_t msgIndex,
+ nsIMsgDBHdr** msgHdr) override;
+ NS_IMETHOD GetNumMoveCopyMsgs(uint32_t* numMsgs) override;
+ NS_IMETHOD GetCurMoveCopyMsgIndex(uint32_t* result) override {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = m_curMsgIndex;
+ return NS_OK;
+ }
+ NS_IMETHOD SetCurMoveCopyMsgIndex(uint32_t aIndex) override {
+ m_curMsgIndex = aIndex;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetFolder(nsIMsgFolder** msgFolder) override;
+
+ // nsMsgMailNewsUrl override
+ nsresult Clone(nsIURI** _retval) override;
+
+ // nsMailboxUrl
+ nsMailboxUrl();
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGI18NURL
+
+ protected:
+ virtual ~nsMailboxUrl();
+ // protocol specific code to parse a url...
+ virtual nsresult ParseUrl();
+ nsresult GetMsgHdrForKey(nsMsgKey msgKey, nsIMsgDBHdr** aMsgHdr);
+
+ // mailboxurl specific state
+ nsCOMPtr<nsIStreamListener> m_mailboxParser;
+ nsCOMPtr<nsIStreamListener> m_mailboxCopyHandler;
+
+ nsMailboxAction m_mailboxAction; // the action this url represents...parse
+ // mailbox, display messages, etc.
+ nsCOMPtr<nsIFile> m_filePath;
+ char* m_messageID;
+ uint32_t m_messageSize;
+ nsMsgKey m_messageKey;
+ nsCString m_file;
+
+ // used by save message to disk
+ nsCOMPtr<nsIFile> m_messageFile;
+ bool m_addDummyEnvelope;
+ bool m_canonicalLineEnding;
+ nsresult ParseSearchPart();
+
+ // for multiple msg move/copy
+ nsTArray<nsMsgKey> m_keys;
+ int32_t m_curMsgIndex;
+
+ // truncated message support
+ nsCString m_originalSpec;
+ nsCString mURI; // the RDF URI associated with this url.
+ bool mAutodetectCharset; // used by nsIMsgI18NUrl...
+};
+
+#endif // nsMailboxUrl_h__
diff --git a/comm/mailnews/local/src/nsMsgBrkMBoxStore.cpp b/comm/mailnews/local/src/nsMsgBrkMBoxStore.cpp
new file mode 100644
index 0000000000..1f48b7d278
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgBrkMBoxStore.cpp
@@ -0,0 +1,1033 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Class for handling Berkeley Mailbox stores.
+*/
+
+#include "prlog.h"
+#include "msgCore.h"
+#include "nsMsgBrkMBoxStore.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIInputStream.h"
+#include "nsCOMArray.h"
+#include "nsIFile.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIMsgHdr.h"
+#include "nsNetUtil.h"
+#include "nsIMsgDatabase.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsMsgUtils.h"
+#include "nsIDBFolderInfo.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsMailHeaders.h"
+#include "nsParseMailbox.h"
+#include "nsIMailboxService.h"
+#include "nsIMsgFolderCompactor.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsPrintfCString.h"
+#include "nsQuarantinedOutputStream.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SlicedInputStream.h"
+#include "prprf.h"
+#include <cstdlib> // for std::abs(int/long)
+#include <cmath> // for std::abs(float/double)
+
+nsMsgBrkMBoxStore::nsMsgBrkMBoxStore() {}
+
+nsMsgBrkMBoxStore::~nsMsgBrkMBoxStore() {}
+
+NS_IMPL_ISUPPORTS(nsMsgBrkMBoxStore, nsIMsgPluggableStore)
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::DiscoverSubFolders(nsIMsgFolder* aParentFolder,
+ bool aDeep) {
+ NS_ENSURE_ARG_POINTER(aParentFolder);
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aParentFolder->GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ path->Exists(&exists);
+ if (!exists) {
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return AddSubFolders(aParentFolder, path, aDeep);
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::CreateFolder(nsIMsgFolder* aParent,
+ const nsAString& aFolderName,
+ nsIMsgFolder** aResult) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (aFolderName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsCOMPtr<nsIFile> path;
+ nsCOMPtr<nsIMsgFolder> child;
+ nsresult rv = aParent->GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+ // Get a directory based on our current path.
+ rv = CreateDirectoryForFolder(path);
+ if (NS_FAILED(rv)) return rv;
+
+ // Now we have a valid directory or we have returned.
+ // Make sure the new folder name is valid
+ nsAutoString safeFolderName(aFolderName);
+ NS_MsgHashIfNecessary(safeFolderName);
+
+ path->Append(safeFolderName);
+ bool exists;
+ path->Exists(&exists);
+ if (exists) // check this because localized names are different from disk
+ // names
+ return NS_MSG_FOLDER_EXISTS;
+
+ rv = path->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // GetFlags and SetFlags in AddSubfolder will fail because we have no db at
+ // this point but mFlags is set.
+ rv = aParent->AddSubfolder(safeFolderName, getter_AddRefs(child));
+ if (!child || NS_FAILED(rv)) {
+ path->Remove(false);
+ return rv;
+ }
+ // Create an empty database for this mail folder, set its name from the user
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ if (msgDBService) {
+ nsCOMPtr<nsIMsgDatabase> unusedDB;
+ rv = msgDBService->OpenFolderDB(child, true, getter_AddRefs(unusedDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ rv = msgDBService->CreateNewDB(child, getter_AddRefs(unusedDB));
+
+ if ((NS_SUCCEEDED(rv) || rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) &&
+ unusedDB) {
+ // need to set the folder name
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (NS_SUCCEEDED(rv)) folderInfo->SetMailboxName(safeFolderName);
+
+ unusedDB->SetSummaryValid(true);
+ unusedDB->Close(true);
+ aParent->UpdateSummaryTotals(true);
+ } else {
+ path->Remove(false);
+ rv = NS_MSG_CANT_CREATE_FOLDER;
+ }
+ }
+ child.forget(aResult);
+ return rv;
+}
+
+// Get the current attributes of the mbox file, corrected for caching
+void nsMsgBrkMBoxStore::GetMailboxModProperties(nsIMsgFolder* aFolder,
+ int64_t* aSize,
+ uint32_t* aDate) {
+ // We'll simply return 0 on errors.
+ *aDate = 0;
+ *aSize = 0;
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = pathFile->GetFileSize(aSize);
+ if (NS_FAILED(rv)) return; // expected result for virtual folders
+
+ PRTime lastModTime;
+ rv = pathFile->GetLastModifiedTime(&lastModTime);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ *aDate = (uint32_t)(lastModTime / PR_MSEC_PER_SEC);
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::HasSpaceAvailable(nsIMsgFolder* aFolder,
+ int64_t aSpaceRequested,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool allow4GBfolders =
+ mozilla::Preferences::GetBool("mailnews.allowMboxOver4GB", true);
+
+ if (!allow4GBfolders) {
+ // Allow the mbox to only reach 0xFFC00000 = 4 GiB - 4 MiB.
+ int64_t fileSize;
+ rv = pathFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = ((fileSize + aSpaceRequested) < 0xFFC00000LL);
+ if (!*aResult) return NS_ERROR_FILE_TOO_BIG;
+ }
+
+ *aResult = DiskSpaceAvailableInStore(pathFile, aSpaceRequested);
+ if (!*aResult) return NS_ERROR_FILE_NO_DEVICE_SPACE;
+
+ return NS_OK;
+}
+
+static bool gGotGlobalPrefs = false;
+static int32_t gTimeStampLeeway = 60;
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::IsSummaryFileValid(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDB,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aDB);
+ NS_ENSURE_ARG_POINTER(aResult);
+ // We only check local folders for db validity.
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder(do_QueryInterface(aFolder));
+ if (!localFolder) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = aDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t folderSize;
+ uint32_t folderDate;
+ int32_t numUnreadMessages;
+
+ *aResult = false;
+
+ folderInfo->GetNumUnreadMessages(&numUnreadMessages);
+ folderInfo->GetFolderSize(&folderSize);
+ folderInfo->GetFolderDate(&folderDate);
+
+ int64_t fileSize = 0;
+ uint32_t actualFolderTimeStamp = 0;
+ GetMailboxModProperties(aFolder, &fileSize, &actualFolderTimeStamp);
+
+ if (folderSize == fileSize && numUnreadMessages >= 0) {
+ if (!folderSize) {
+ *aResult = true;
+ return NS_OK;
+ }
+ if (!gGotGlobalPrefs) {
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (pPrefBranch) {
+ rv = pPrefBranch->GetIntPref("mail.db_timestamp_leeway",
+ &gTimeStampLeeway);
+ gGotGlobalPrefs = true;
+ }
+ }
+ // if those values are ok, check time stamp
+ if (gTimeStampLeeway == 0)
+ *aResult = folderDate == actualFolderTimeStamp;
+ else
+ *aResult = std::abs((int32_t)(actualFolderTimeStamp - folderDate)) <=
+ gTimeStampLeeway;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::SetSummaryFileValid(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDB,
+ bool aValid) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aDB);
+ // We only need to do this for local folders.
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder(do_QueryInterface(aFolder));
+ if (!localFolder) return NS_OK;
+
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = aDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool exists;
+ pathFile->Exists(&exists);
+ if (!exists) return NS_MSG_ERROR_FOLDER_MISSING;
+
+ if (aValid) {
+ uint32_t actualFolderTimeStamp;
+ int64_t fileSize;
+ GetMailboxModProperties(aFolder, &fileSize, &actualFolderTimeStamp);
+ folderInfo->SetFolderSize(fileSize);
+ folderInfo->SetFolderDate(actualFolderTimeStamp);
+ } else {
+ folderInfo->SetVersion(0); // that ought to do the trick.
+ }
+ aDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::DeleteFolder(nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ bool exists;
+
+ // Delete mbox file.
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ exists = false;
+ pathFile->Exists(&exists);
+ if (exists) {
+ rv = pathFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Delete any subfolders (.sbd-suffixed directories).
+ AddDirectorySeparator(pathFile);
+ exists = false;
+ pathFile->Exists(&exists);
+ if (exists) {
+ rv = pathFile->Remove(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::RenameFolder(nsIMsgFolder* aFolder,
+ const nsAString& aNewName,
+ nsIMsgFolder** aNewFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aNewFolder);
+
+ uint32_t numChildren;
+ aFolder->GetNumSubFolders(&numChildren);
+ nsString existingName;
+ aFolder->GetName(existingName);
+
+ nsCOMPtr<nsIFile> oldPathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(oldPathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ rv = aFolder->GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = aFolder->GetSummaryFile(getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> dirFile;
+ oldPathFile->Clone(getter_AddRefs(dirFile));
+
+ if (numChildren > 0) {
+ rv = CreateDirectoryForFolder(dirFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoString safeName(aNewName);
+ NS_MsgHashIfNecessary(safeName);
+
+ nsAutoCString oldLeafName;
+ oldPathFile->GetNativeLeafName(oldLeafName);
+
+ nsCOMPtr<nsIFile> parentPathFile;
+ parentFolder->GetFilePath(getter_AddRefs(parentPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isDirectory = false;
+ parentPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) {
+ nsAutoString leafName;
+ parentPathFile->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ rv = parentPathFile->SetLeafName(leafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ aFolder->ForceDBClosed();
+ // save off dir name before appending .msf
+ rv = oldPathFile->MoveTo(nullptr, safeName);
+ if (NS_FAILED(rv)) return rv;
+
+ nsString dbName(safeName);
+ dbName.AppendLiteral(SUMMARY_SUFFIX);
+ oldSummaryFile->MoveTo(nullptr, dbName);
+
+ if (numChildren > 0) {
+ // rename "*.sbd" directory
+ nsAutoString newNameDirStr(safeName);
+ newNameDirStr.AppendLiteral(FOLDER_SUFFIX);
+ dirFile->MoveTo(nullptr, newNameDirStr);
+ }
+
+ return parentFolder->AddSubfolder(safeName, aNewFolder);
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::CopyFolder(
+ nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDstFolder, bool aIsMoveFolder,
+ nsIMsgWindow* aMsgWindow, nsIMsgCopyServiceListener* aListener,
+ const nsAString& aNewName) {
+ NS_ENSURE_ARG_POINTER(aSrcFolder);
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+
+ nsAutoString folderName;
+ if (aNewName.IsEmpty())
+ aSrcFolder->GetName(folderName);
+ else
+ folderName.Assign(aNewName);
+
+ nsAutoString safeFolderName(folderName);
+ NS_MsgHashIfNecessary(safeFolderName);
+ nsCOMPtr<nsIMsgLocalMailFolder> localSrcFolder(do_QueryInterface(aSrcFolder));
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ if (localSrcFolder)
+ localSrcFolder->GetDatabaseWOReparse(getter_AddRefs(srcDB));
+ bool summaryValid = !!srcDB;
+ srcDB = nullptr;
+ aSrcFolder->ForceDBClosed();
+
+ nsCOMPtr<nsIFile> oldPath;
+ nsresult rv = aSrcFolder->GetFilePath(getter_AddRefs(oldPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> summaryFile;
+ GetSummaryFileLocation(oldPath, getter_AddRefs(summaryFile));
+
+ nsCOMPtr<nsIFile> newPath;
+ rv = aDstFolder->GetFilePath(getter_AddRefs(newPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool newPathIsDirectory = false;
+ newPath->IsDirectory(&newPathIsDirectory);
+ if (!newPathIsDirectory) {
+ AddDirectorySeparator(newPath);
+ rv = newPath->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIFile> origPath;
+ oldPath->Clone(getter_AddRefs(origPath));
+
+ // copying necessary for aborting.... if failure return
+ rv = oldPath->CopyTo(newPath, safeFolderName);
+ NS_ENSURE_SUCCESS(rv, rv); // Will fail if a file by that name exists
+
+ // Copy to dir can fail if filespec does not exist. If copy fails, we test
+ // if the filespec exist or not, if it does not that's ok, we continue
+ // without copying it. If it fails and filespec exist and is not zero sized
+ // there is real problem
+ // Copy the file to the new dir
+ nsAutoString dbName(safeFolderName);
+ dbName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = summaryFile->CopyTo(newPath, dbName);
+ if (NS_FAILED(rv)) // Test if the copy is successful
+ {
+ // Test if the filespec has data
+ bool exists;
+ int64_t fileSize;
+ summaryFile->Exists(&exists);
+ summaryFile->GetFileSize(&fileSize);
+ if (exists && fileSize > 0)
+ NS_ENSURE_SUCCESS(rv, rv); // Yes, it should have worked !
+ // else case is filespec is zero sized, no need to copy it,
+ // not an error
+ }
+
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ rv = aDstFolder->AddSubfolder(safeFolderName, getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // linux and mac are not good about maintaining the file stamp when copying
+ // folders around. So if the source folder db is good, set the dest db as
+ // good too.
+ nsCOMPtr<nsIMsgDatabase> destDB;
+ if (summaryValid) {
+ nsAutoString folderLeafName;
+ origPath->GetLeafName(folderLeafName);
+ newPath->Append(folderLeafName);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(newPath, newMsgFolder, false, true,
+ getter_AddRefs(destDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE && destDB)
+ destDB->SetSummaryValid(true);
+ }
+ newMsgFolder->SetPrettyName(folderName);
+ uint32_t flags;
+ aSrcFolder->GetFlags(&flags);
+ newMsgFolder->SetFlags(flags);
+ bool changed = false;
+ rv = aSrcFolder->MatchOrChangeFilterDestination(newMsgFolder, true, &changed);
+ if (changed) aSrcFolder->AlertFilterChanged(aMsgWindow);
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = aSrcFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copy subfolders to the new location
+ nsresult copyStatus = NS_OK;
+ nsCOMPtr<nsIMsgLocalMailFolder> localNewFolder(
+ do_QueryInterface(newMsgFolder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* folder : subFolders) {
+ copyStatus =
+ localNewFolder->CopyFolderLocal(folder, false, aMsgWindow, aListener);
+ // Test if the call succeeded, if not we have to stop recursive call
+ if (NS_FAILED(copyStatus)) {
+ // Copy failed we have to notify caller to handle the error and stop
+ // moving the folders. In case this happens to the topmost level of
+ // recursive call, then we just need to break from the while loop and
+ // go to error handling code.
+ if (!aIsMoveFolder) return copyStatus;
+ break;
+ }
+ }
+ }
+
+ if (aIsMoveFolder && NS_SUCCEEDED(copyStatus)) {
+ if (localNewFolder) {
+ nsCOMPtr<nsISupports> srcSupport(do_QueryInterface(aSrcFolder));
+ localNewFolder->OnCopyCompleted(srcSupport, true);
+ }
+
+ // Notify the "folder" that was dragged and dropped has been created. No
+ // need to do this for its subfolders. isMoveFolder will be true for folder.
+ aDstFolder->NotifyFolderAdded(newMsgFolder);
+
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ aSrcFolder->GetParent(getter_AddRefs(msgParent));
+ aSrcFolder->SetParent(nullptr);
+ if (msgParent) {
+ // The files have already been moved, so delete storage false
+ msgParent->PropagateDelete(aSrcFolder, false);
+ oldPath->Remove(false); // berkeley mailbox
+ aSrcFolder->DeleteStorage();
+
+ nsCOMPtr<nsIFile> parentPath;
+ rv = msgParent->GetFilePath(getter_AddRefs(parentPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDirectorySeparator(parentPath);
+ nsCOMPtr<nsIDirectoryEnumerator> children;
+ parentPath->GetDirectoryEntries(getter_AddRefs(children));
+ bool more;
+ // checks if the directory is empty or not
+ if (children && NS_SUCCEEDED(children->HasMoreElements(&more)) && !more)
+ parentPath->Remove(true);
+ }
+ } else {
+ // This is the case where the copy of a subfolder failed.
+ // We have to delete the newDirectory tree to make a "rollback".
+ // Someone should add a popup to warn the user that the move was not
+ // possible.
+ if (aIsMoveFolder && NS_FAILED(copyStatus)) {
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ newMsgFolder->ForceDBClosed();
+ newMsgFolder->GetParent(getter_AddRefs(msgParent));
+ newMsgFolder->SetParent(nullptr);
+ if (msgParent) {
+ msgParent->PropagateDelete(newMsgFolder, false);
+ newMsgFolder->DeleteStorage();
+ AddDirectorySeparator(newPath);
+ newPath->Remove(true); // berkeley mailbox
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::GetNewMsgOutputStream(nsIMsgFolder* aFolder,
+ nsIMsgDBHdr** aNewMsgHdr,
+ nsIOutputStream** aResult) {
+ bool quarantining = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefBranch) {
+ prefBranch->GetBoolPref("mailnews.downloadToTempFile", &quarantining);
+ }
+
+ if (!quarantining) {
+ // Caller will write directly to mbox.
+ return InternalGetNewMsgOutputStream(aFolder, aNewMsgHdr, aResult);
+ }
+
+ // Quarantining is on, so we want to write the new message to a temp file
+ // and let the virus checker have at it before we append it to the mbox.
+ // We'll wrap the mboxStream with an nsQuarantinedOutputStream and return
+ // that.
+ nsCOMPtr<nsIOutputStream> mboxStream;
+ nsresult rv = InternalGetNewMsgOutputStream(aFolder, aNewMsgHdr,
+ getter_AddRefs(mboxStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsQuarantinedOutputStream> qStream =
+ new nsQuarantinedOutputStream(mboxStream);
+ qStream.forget(aResult);
+ return NS_OK;
+}
+
+nsresult nsMsgBrkMBoxStore::InternalGetNewMsgOutputStream(
+ nsIMsgFolder* aFolder, nsIMsgDBHdr** aNewMsgHdr,
+ nsIOutputStream** aResult) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aNewMsgHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+#ifdef _DEBUG
+ NS_ASSERTION(m_streamOutstandingFolder != aFolder, "didn't finish prev msg");
+ m_streamOutstandingFolder = aFolder;
+#endif
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> mboxFile;
+ rv = aFolder->GetFilePath(getter_AddRefs(mboxFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> db;
+ aFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (!db && !*aNewMsgHdr) NS_WARNING("no db, and no message header");
+ bool exists = false;
+ mboxFile->Exists(&exists);
+ if (!exists) {
+ rv = mboxFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString URI;
+ aFolder->GetURI(URI);
+ nsCOMPtr<nsISeekableStream> seekable;
+ if (m_outputStreams.Get(URI, aResult)) {
+ seekable = do_QueryInterface(*aResult, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = seekable->Seek(nsISeekableStream::NS_SEEK_END, 0);
+ if (NS_FAILED(rv)) {
+ m_outputStreams.Remove(URI);
+ NS_RELEASE(*aResult);
+ }
+ }
+ if (!*aResult) {
+ rv = MsgGetFileStream(mboxFile, aResult);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed opening offline store for output");
+ if (NS_FAILED(rv))
+ printf("failed opening offline store for %s\n", URI.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ seekable = do_QueryInterface(*aResult, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = seekable->Seek(nsISeekableStream::NS_SEEK_END, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_outputStreams.InsertOrUpdate(URI, *aResult);
+ }
+ int64_t filePos;
+ seekable->Tell(&filePos);
+
+ if (db && !*aNewMsgHdr) {
+ db->CreateNewHdr(nsMsgKey_None, aNewMsgHdr);
+ }
+
+ if (*aNewMsgHdr) {
+ nsCString storeToken = nsPrintfCString("%" PRId64, filePos);
+ (*aNewMsgHdr)->SetStringProperty("storeToken", storeToken);
+ (*aNewMsgHdr)->SetMessageOffset(filePos);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::DiscardNewMessage(nsIOutputStream* aOutputStream,
+ nsIMsgDBHdr* aNewHdr) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+#ifdef _DEBUG
+ m_streamOutstandingFolder = nullptr;
+#endif
+ nsCOMPtr<nsISafeOutputStream> safe = do_QueryInterface(aOutputStream);
+ if (safe) {
+ // nsISafeOutputStream only writes upon finish(), so no cleanup required.
+ return aOutputStream->Close();
+ }
+ // Truncate the mbox back to where we started writing.
+ uint64_t hdrOffset;
+ aNewHdr->GetMessageOffset(&hdrOffset);
+ aOutputStream->Close();
+ nsCOMPtr<nsIFile> mboxFile;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aNewHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetFilePath(getter_AddRefs(mboxFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mboxFile->SetFileSize(hdrOffset);
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::FinishNewMessage(nsIOutputStream* aOutputStream,
+ nsIMsgDBHdr* aNewHdr) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+#ifdef _DEBUG
+ m_streamOutstandingFolder = nullptr;
+#endif
+ // Quarantining is implemented using a nsISafeOutputStream.
+ // It requires an explicit commit, or the data will be discarded.
+ nsCOMPtr<nsISafeOutputStream> safe = do_QueryInterface(aOutputStream);
+ if (safe) {
+ return safe->Finish();
+ }
+ aOutputStream->Close();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::MoveNewlyDownloadedMessage(nsIMsgDBHdr* aNewHdr,
+ nsIMsgFolder* aDestFolder,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::GetMsgInputStream(nsIMsgFolder* aMsgFolder,
+ const nsACString& aMsgToken,
+ nsIInputStream** aResult) {
+ MOZ_ASSERT(aMsgFolder);
+ MOZ_ASSERT(aResult);
+ MOZ_ASSERT(!aMsgToken.IsEmpty());
+
+ uint64_t offset = ParseUint64Str(PromiseFlatCString(aMsgToken).get());
+ nsCOMPtr<nsIFile> mboxFile;
+ nsresult rv = aMsgFolder->GetFilePath(getter_AddRefs(mboxFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIInputStream> msgStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(msgStream), mboxFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(msgStream));
+ rv = seekable->Seek(PR_SEEK_SET, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgStream.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::DeleteMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray) {
+ return ChangeFlags(aHdrArray, nsMsgMessageFlags::Expunged, true);
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::CopyMessages(bool isMove,
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray,
+ nsIMsgFolder* aDstFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aDstHdrs,
+ nsITransaction** aUndoAction, bool* aCopyDone) {
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+ NS_ENSURE_ARG_POINTER(aCopyDone);
+ aDstHdrs.Clear();
+ *aUndoAction = nullptr;
+ // We return false to indicate there's no shortcut. The calling code will
+ // just have to perform the copy the hard way.
+ *aCopyDone = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::GetSupportsCompaction(bool* aSupportsCompaction) {
+ NS_ENSURE_ARG_POINTER(aSupportsCompaction);
+ *aSupportsCompaction = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::CompactFolder(nsIMsgFolder* aFolder,
+ nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ // Eventually, folder compaction should be managed by nsMsgBrkMBoxStore, but
+ // for now it's separate (see nsMsgFolderCompactor) and invoked via the
+ // folder methods Compact() and CompactAll().
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::RebuildIndex(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aMsgDB,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ if (NS_FAILED(rv)) return rv;
+
+ bool isLocked;
+ aFolder->GetLocked(&isLocked);
+ if (isLocked) {
+ NS_ASSERTION(false, "Could not get folder lock");
+ return NS_MSG_FOLDER_BUSY;
+ }
+
+ nsCOMPtr<nsIMailboxService> mailboxService =
+ do_GetService("@mozilla.org/messenger/mailboxservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsMsgMailboxParser> parser = new nsMsgMailboxParser(aFolder);
+ NS_ENSURE_TRUE(parser, NS_ERROR_OUT_OF_MEMORY);
+ rv = parser->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mailboxService->ParseMailbox(aMsgWindow, pathFile, parser, aListener,
+ nullptr);
+ if (NS_SUCCEEDED(rv)) ResetForceReparse(aMsgDB);
+ return rv;
+}
+
+nsresult nsMsgBrkMBoxStore::GetOutputStream(
+ nsIMsgDBHdr* aHdr, nsCOMPtr<nsIOutputStream>& outputStream,
+ nsCOMPtr<nsISeekableStream>& seekableStream, int64_t& restorePos) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString URI;
+ folder->GetURI(URI);
+ restorePos = -1;
+ if (m_outputStreams.Get(URI, getter_AddRefs(outputStream))) {
+ seekableStream = do_QueryInterface(outputStream);
+ rv = seekableStream->Tell(&restorePos);
+ if (NS_FAILED(rv)) {
+ outputStream = nullptr;
+ m_outputStreams.Remove(URI);
+ }
+ }
+ if (!outputStream) {
+ nsCOMPtr<nsIFile> mboxFile;
+ rv = folder->GetFilePath(getter_AddRefs(mboxFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = MsgGetFileStream(mboxFile, getter_AddRefs(outputStream));
+ seekableStream = do_QueryInterface(outputStream);
+ if (NS_SUCCEEDED(rv)) m_outputStreams.InsertOrUpdate(URI, outputStream);
+ }
+ return rv;
+}
+
+void nsMsgBrkMBoxStore::SetDBValid(nsIMsgDBHdr* aHdr) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ aHdr->GetFolder(getter_AddRefs(folder));
+ if (folder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ folder->GetMsgDatabase(getter_AddRefs(db));
+ if (db) SetSummaryFileValid(folder, db, true);
+ }
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::ChangeFlags(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray, uint32_t aFlags,
+ bool aSet) {
+ if (aHdrArray.IsEmpty()) return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsCOMPtr<nsISeekableStream> seekableStream;
+ int64_t restoreStreamPos;
+ nsresult rv = GetOutputStream(aHdrArray[0], outputStream, seekableStream,
+ restoreStreamPos);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ for (auto msgHdr : aHdrArray) {
+ // Work out the flags we want to write.
+ uint32_t flags = 0;
+ (void)msgHdr->GetFlags(&flags);
+ flags &= ~(nsMsgMessageFlags::RuntimeOnly | nsMsgMessageFlags::Offline);
+ if (aSet) {
+ flags |= aFlags;
+ } else {
+ flags &= ~aFlags;
+ }
+
+ // Rewrite flags into X-Mozilla-Status headers.
+ nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(outputStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint64_t msgOffset;
+ rv = msgHdr->GetMessageOffset(&msgOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ seekable->Seek(nsISeekableStream::NS_SEEK_SET, msgOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RewriteMsgFlags(seekable, flags);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ if (restoreStreamPos != -1)
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, restoreStreamPos);
+ else if (outputStream)
+ outputStream->Close();
+ SetDBValid(aHdrArray[0]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::ChangeKeywords(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray, const nsACString& aKeywords,
+ bool aAdd) {
+ if (aHdrArray.IsEmpty()) return NS_ERROR_INVALID_ARG;
+
+ nsTArray<nsCString> keywordsToAdd;
+ nsTArray<nsCString> keywordsToRemove;
+ if (aAdd) {
+ ParseString(aKeywords, ' ', keywordsToAdd);
+ } else {
+ ParseString(aKeywords, ' ', keywordsToRemove);
+ }
+
+ // Get the (possibly-cached) seekable & writable stream for this mbox.
+ nsCOMPtr<nsIOutputStream> output;
+ nsCOMPtr<nsISeekableStream> seekable;
+ int64_t restoreStreamPos;
+ nsresult rv =
+ GetOutputStream(aHdrArray[0], output, seekable, restoreStreamPos);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto msgHdr : aHdrArray) {
+ uint64_t msgStart;
+ msgHdr->GetMessageOffset(&msgStart);
+ seekable->Seek(nsISeekableStream::NS_SEEK_SET, msgStart);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool notEnoughRoom;
+ rv = ChangeKeywordsHelper(seekable, keywordsToAdd, keywordsToRemove,
+ notEnoughRoom);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (notEnoughRoom) {
+ // The growKeywords property indicates that the X-Mozilla-Keys header
+ // doesn't have enough space, and should be rebuilt during the next
+ // folder compaction.
+ msgHdr->SetUint32Property("growKeywords", 1);
+ }
+ }
+
+ if (restoreStreamPos != -1) {
+ seekable->Seek(nsISeekableStream::NS_SEEK_SET, restoreStreamPos);
+ } else if (output) {
+ output->Close();
+ }
+ SetDBValid(aHdrArray[0]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBrkMBoxStore::GetStoreType(nsACString& aType) {
+ aType.AssignLiteral("mbox");
+ return NS_OK;
+}
+
+// Iterates over the files in the "path" directory, and adds subfolders to
+// parent for each mailbox file found.
+nsresult nsMsgBrkMBoxStore::AddSubFolders(nsIMsgFolder* parent,
+ nsCOMPtr<nsIFile>& path, bool deep) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> tmp; // at top level so we can safely assign to path
+ bool isDirectory;
+ path->IsDirectory(&isDirectory);
+ if (!isDirectory) {
+ rv = path->Clone(getter_AddRefs(tmp));
+ path = tmp;
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ path->SetLeafName(leafName);
+ path->IsDirectory(&isDirectory);
+ }
+ if (!isDirectory) return NS_OK;
+ // first find out all the current subfolders and files, before using them
+ // while creating new subfolders; we don't want to modify and iterate the same
+ // directory at once.
+ nsCOMArray<nsIFile> currentDirEntries;
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ rv = path->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIFile> currentFile;
+ rv = directoryEnumerator->GetNextFile(getter_AddRefs(currentFile));
+ if (NS_SUCCEEDED(rv) && currentFile) {
+ currentDirEntries.AppendObject(currentFile);
+ }
+ }
+
+ // add the folders
+ int32_t count = currentDirEntries.Count();
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsIFile> currentFile(currentDirEntries[i]);
+
+ nsAutoString leafName;
+ currentFile->GetLeafName(leafName);
+ // here we should handle the case where the current file is a .sbd directory
+ // w/o a matching folder file, or a directory w/o the name .sbd
+ if (nsShouldIgnoreFile(leafName, currentFile)) continue;
+
+ nsCOMPtr<nsIMsgFolder> child;
+ rv = parent->AddSubfolder(leafName, getter_AddRefs(child));
+ if (NS_FAILED(rv) && rv != NS_MSG_FOLDER_EXISTS) {
+ return rv;
+ }
+ if (child) {
+ nsString folderName;
+ child->GetName(folderName); // try to get it from cache/db
+ if (folderName.IsEmpty()) child->SetPrettyName(leafName);
+ if (deep) {
+ nsCOMPtr<nsIFile> path;
+ rv = child->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = AddSubFolders(child, path, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ return rv == NS_MSG_FOLDER_EXISTS ? NS_OK : rv;
+}
+
+/* Finds the directory associated with this folder. That is if the path is
+ c:\Inbox, it will return c:\Inbox.sbd if it succeeds. If that path doesn't
+ currently exist then it will create it. Path is strictly an out parameter.
+ */
+nsresult nsMsgBrkMBoxStore::CreateDirectoryForFolder(nsIFile* path) {
+ nsresult rv = NS_OK;
+
+ bool pathIsDirectory = false;
+ path->IsDirectory(&pathIsDirectory);
+ if (!pathIsDirectory) {
+ // If the current path isn't a directory, add directory separator
+ // and test it out.
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ rv = path->SetLeafName(leafName);
+ if (NS_FAILED(rv)) return rv;
+
+ // If that doesn't exist, then we have to create this directory
+ pathIsDirectory = false;
+ path->IsDirectory(&pathIsDirectory);
+ if (!pathIsDirectory) {
+ bool pathExists;
+ path->Exists(&pathExists);
+ // If for some reason there's a file with the directory separator
+ // then we are going to fail.
+ rv = pathExists ? NS_MSG_COULD_NOT_CREATE_DIRECTORY
+ : path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgBrkMBoxStore::SliceStream(nsIInputStream* inStream, uint64_t start,
+ uint32_t length, nsIInputStream** result) {
+ nsCOMPtr<nsIInputStream> in(inStream);
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(in.forget(), start, uint64_t(length));
+ slicedStream.forget(result);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMsgBrkMBoxStore.h b/comm/mailnews/local/src/nsMsgBrkMBoxStore.h
new file mode 100644
index 0000000000..a943cafb60
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgBrkMBoxStore.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Class for handling Berkeley Mailbox stores.
+*/
+
+#ifndef nsMsgBrkMboxStore_h__
+#define nsMsgBrkMboxStore_h__
+
+#include "nsMsgLocalStoreUtils.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIFile.h"
+#include "nsInterfaceHashtable.h"
+#include "nsISeekableStream.h"
+#include "nsIOutputStream.h"
+
+class nsMsgBrkMBoxStore final : public nsMsgLocalStoreUtils,
+ nsIMsgPluggableStore {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPLUGGABLESTORE
+
+ nsMsgBrkMBoxStore();
+
+ private:
+ ~nsMsgBrkMBoxStore();
+
+ protected:
+ nsresult InternalGetNewMsgOutputStream(nsIMsgFolder* aFolder,
+ nsIMsgDBHdr** aNewMsgHdr,
+ nsIOutputStream** aResult);
+ nsresult AddSubFolders(nsIMsgFolder* parent, nsCOMPtr<nsIFile>& path,
+ bool deep);
+ nsresult CreateDirectoryForFolder(nsIFile* path);
+ nsresult GetOutputStream(nsIMsgDBHdr* aHdr,
+ nsCOMPtr<nsIOutputStream>& outputStream,
+ nsCOMPtr<nsISeekableStream>& seekableStream,
+ int64_t& restorePos);
+ void GetMailboxModProperties(nsIMsgFolder* aFolder, int64_t* aSize,
+ uint32_t* aDate);
+ void SetDBValid(nsIMsgDBHdr* aHdr);
+
+ // We don't want to keep re-opening an output stream when downloading
+ // multiple pop3 messages, or adjusting x-mozilla-status headers, so
+ // we cache output streams based on folder uri's. If the caller has closed
+ // the stream, we'll get a new one.
+ nsInterfaceHashtable<nsCStringHashKey, nsIOutputStream> m_outputStreams;
+
+#ifdef _DEBUG
+ nsCOMPtr<nsIMsgFolder> m_streamOutstandingFolder;
+#endif
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsMsgFileHdr.cpp b/comm/mailnews/local/src/nsMsgFileHdr.cpp
new file mode 100644
index 0000000000..57cac43886
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgFileHdr.cpp
@@ -0,0 +1,389 @@
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "nsMsgFileHdr.h"
+#include "nsMsgMessageFlags.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "HeaderReader.h"
+#include "nsIFileStreams.h"
+#include "nsIMimeConverter.h"
+
+static inline uint32_t PRTimeToSeconds(PRTime aTimeUsec) {
+ return uint32_t(aTimeUsec / PR_USEC_PER_SEC);
+}
+
+NS_IMPL_ISUPPORTS(nsMsgFileHdr, nsIMsgDBHdr)
+
+nsMsgFileHdr::nsMsgFileHdr(const nsACString& aUri) {
+ mUri = nsCString(aUri);
+ mDate = 0;
+ mFlags = 0;
+}
+
+nsMsgFileHdr::~nsMsgFileHdr() {}
+
+nsresult nsMsgFileHdr::ReadFile() {
+ if (mFile) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), mUri);
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri);
+ rv = fileUrl->GetFile(getter_AddRefs(mFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID);
+ rv = fileStream->Init(mFile, PR_RDONLY, 0664, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t count;
+ char buffer[8192];
+ rv = fileStream->Read(&buffer[0], 8192, &count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto cb = [&](HeaderReader::Hdr const& hdr) {
+ auto name = hdr.Name(buffer);
+ if (name.EqualsLiteral("Subject") && mSubject.IsEmpty()) {
+ mSubject = hdr.Value(buffer);
+ }
+ if (name.EqualsLiteral("From") && mAuthor.IsEmpty()) {
+ mAuthor = hdr.Value(buffer);
+ }
+ if (name.EqualsLiteral("To") && mRecipients.IsEmpty()) {
+ mRecipients = hdr.Value(buffer);
+ }
+ if (name.EqualsLiteral("Cc") && mCcList.IsEmpty()) {
+ mCcList = hdr.Value(buffer);
+ }
+ if (name.EqualsLiteral("Bcc") && mBccList.IsEmpty()) {
+ mBccList = hdr.Value(buffer);
+ }
+ if (name.EqualsLiteral("Date") && mDate == 0) {
+ PR_ParseTimeString(hdr.Value(buffer).get(), false, &mDate);
+ }
+ if (name.EqualsLiteral("Message-ID") && mMessageID.IsEmpty()) {
+ mMessageID = hdr.Value(buffer);
+ mMessageID.Trim("<>");
+ }
+ return true;
+ };
+ HeaderReader rdr;
+ rdr.Parse(buffer, cb);
+
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1");
+ mimeConverter->DecodeMimeHeader(mSubject.get(), "UTF-8", false, true,
+ mDecodedSubject);
+ mimeConverter->DecodeMimeHeader(mAuthor.get(), "UTF-8", false, true,
+ mDecodedAuthor);
+ mimeConverter->DecodeMimeHeader(mRecipients.get(), "UTF-8", false, true,
+ mDecodedRecipients);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetStringProperty(const char* propertyName,
+ const nsACString& propertyValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetStringProperty(const char* propertyName,
+ nsACString& _retval) {
+ if (!strcmp(propertyName, "dummyMsgUrl")) {
+ _retval = mUri;
+ return NS_OK;
+ }
+ _retval.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetUint32Property(const char* propertyName,
+ uint32_t* _retval) {
+ if (!strcmp(propertyName, "dummyMsgLastModifiedTime")) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PRTime modifiedTime;
+ mFile->GetLastModifiedTime(&modifiedTime);
+ *_retval = PRTimeToSeconds(modifiedTime);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetUint32Property(const char* propertyName,
+ uint32_t propertyVal) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetIsRead(bool* aIsRead) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetIsFlagged(bool* aIsFlagged) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetIsKilled(bool* aIsKilled) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::MarkRead(bool read) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::MarkFlagged(bool flagged) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::MarkHasAttachments(bool hasAttachments) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetPriority(nsMsgPriorityValue* aPriority) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetPriority(nsMsgPriorityValue aPriority) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetFlags(uint32_t* aFlags) {
+ *aFlags = mFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetFlags(uint32_t aFlags) {
+ mFlags = aFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::OrFlags(uint32_t flags, uint32_t* _retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::AndFlags(uint32_t flags, uint32_t* _retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetThreadId(nsMsgKey* aThreadId) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::SetThreadId(nsMsgKey aThreadId) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetMessageKey(nsMsgKey* aMessageKey) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetMessageKey(nsMsgKey aMessageKey) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetThreadParent(nsMsgKey* aThreadParent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetThreadParent(nsMsgKey aThreadParent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetMessageSize(uint32_t* aMessageSize) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t fileSize;
+ mFile->GetFileSize(&fileSize);
+
+ *aMessageSize = uint32_t(fileSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetMessageSize(uint32_t aMessageSize) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetLineCount(uint32_t* aLineCount) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::SetLineCount(uint32_t aLineCount) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetMessageOffset(uint64_t* aMessageOffset) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetMessageOffset(uint64_t aMessageOffset) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetOfflineMessageSize(
+ uint32_t* aOfflineMessageSize) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetOfflineMessageSize(
+ uint32_t aOfflineMessageSize) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetDate(PRTime* aDate) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aDate = mDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetDate(PRTime aDate) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetDateInSeconds(uint32_t* aDateInSeconds) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetMessageId(char** aMessageId) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aMessageId = strdup(mMessageID.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetMessageId(const char* aMessageId) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetCcList(char** aCcList) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aCcList = strdup(mCcList.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetCcList(const char* aCcList) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetBccList(char** aBccList) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aBccList = strdup(mBccList.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetBccList(const char* aBccList) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetAuthor(char** aAuthor) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aAuthor = strdup(mAuthor.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetAuthor(const char* aAuthor) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetSubject(nsACString& aSubject) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aSubject = mSubject;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetSubject(const nsACString& aSubject) {
+ mSubject = aSubject;
+ bool strippedRE = NS_MsgStripRE(mSubject, mSubject);
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1");
+ mimeConverter->DecodeMimeHeader(mSubject.get(), "UTF-8", false, true,
+ mDecodedSubject);
+ if (strippedRE) {
+ mFlags |= nsMsgMessageFlags::HasRe;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetRecipients(char** aRecipients) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aRecipients = strdup(mRecipients.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetRecipients(const char* aRecipients) {
+ // FIXME: should do assignment (maybe not used but if used, a trap!)
+ // Same for all the other unimplemented setters here.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::SetReferences(const nsACString& references) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetNumReferences(uint16_t* aNumReferences) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetStringReference(int32_t refNum,
+ nsACString& _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetMime2DecodedAuthor(
+ nsAString& aMime2DecodedAuthor) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aMime2DecodedAuthor.Truncate();
+ aMime2DecodedAuthor.Assign(mDecodedAuthor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetMime2DecodedSubject(
+ nsAString& aMime2DecodedSubject) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aMime2DecodedSubject.Truncate();
+ aMime2DecodedSubject.Assign(mDecodedSubject);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetMime2DecodedRecipients(
+ nsAString& aMime2DecodedRecipients) {
+ nsresult rv = ReadFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aMime2DecodedRecipients.Truncate();
+ aMime2DecodedRecipients.Assign(mDecodedRecipients);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetAuthorCollationKey(nsTArray<uint8_t>& _retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetSubjectCollationKey(nsTArray<uint8_t>& _retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetRecipientsCollationKey(
+ nsTArray<uint8_t>& _retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetCharset(char** aCharset) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::SetCharset(const char* aCharset) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetEffectiveCharset(nsACString& aEffectiveCharset) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetAccountKey(char** aAccountKey) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::SetAccountKey(const char* aAccountKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgFileHdr::GetFolder(nsIMsgFolder** aFolder) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgFileHdr::GetProperties(nsTArray<nsCString>& headers) {
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMsgFileHdr.h b/comm/mailnews/local/src/nsMsgFileHdr.h
new file mode 100644
index 0000000000..d63b099e04
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgFileHdr.h
@@ -0,0 +1,41 @@
+/* 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/. */
+
+#ifndef _nsMsgFileHdr_H
+#define _nsMsgFileHdr_H
+
+#include "nsIMsgHdr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+/* This mail-related class is a stub. You can help mailnews by expanding it. */
+
+class nsMsgFileHdr : public nsIMsgDBHdr {
+ public:
+ explicit nsMsgFileHdr(const nsACString& aUri);
+
+ NS_DECL_NSIMSGDBHDR
+ NS_DECL_ISUPPORTS
+
+ private:
+ virtual ~nsMsgFileHdr();
+
+ nsresult ReadFile();
+
+ nsCString mUri;
+ nsCOMPtr<nsIFile> mFile;
+ nsCString mAuthor;
+ nsString mDecodedAuthor;
+ nsCString mSubject;
+ nsString mDecodedSubject;
+ nsCString mRecipients;
+ nsString mDecodedRecipients;
+ nsCString mCcList;
+ nsCString mBccList;
+ PRTime mDate;
+ nsCString mMessageID;
+ uint32_t mFlags;
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsMsgLocalStoreUtils.cpp b/comm/mailnews/local/src/nsMsgLocalStoreUtils.cpp
new file mode 100644
index 0000000000..eef6694a67
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgLocalStoreUtils.cpp
@@ -0,0 +1,380 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsMsgLocalStoreUtils.h"
+#include "nsIFile.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "HeaderReader.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "mozilla/Buffer.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "prprf.h"
+
+#define EXTRA_SAFETY_SPACE 0x400000 // (4MiB)
+
+nsMsgLocalStoreUtils::nsMsgLocalStoreUtils() {}
+
+nsresult nsMsgLocalStoreUtils::AddDirectorySeparator(nsIFile* path) {
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ return path->SetLeafName(leafName);
+}
+
+bool nsMsgLocalStoreUtils::nsShouldIgnoreFile(nsAString& name, nsIFile* path) {
+ if (name.IsEmpty()) return true;
+
+ char16_t firstChar = name.First();
+ if (firstChar == '.' || firstChar == '#' ||
+ name.CharAt(name.Length() - 1) == '~')
+ return true;
+
+ if (name.LowerCaseEqualsLiteral("msgfilterrules.dat") ||
+ name.LowerCaseEqualsLiteral("rules.dat") ||
+ name.LowerCaseEqualsLiteral("filterlog.html") ||
+ name.LowerCaseEqualsLiteral("junklog.html") ||
+ name.LowerCaseEqualsLiteral("rulesbackup.dat"))
+ return true;
+
+ // don't add summary files to the list of folders;
+ // don't add popstate files to the list either, or rules (sort.dat).
+ if (StringEndsWith(name, u".snm"_ns) ||
+ name.LowerCaseEqualsLiteral("popstate.dat") ||
+ name.LowerCaseEqualsLiteral("sort.dat") ||
+ name.LowerCaseEqualsLiteral("mailfilt.log") ||
+ name.LowerCaseEqualsLiteral("filters.js") ||
+ StringEndsWith(name, u".toc"_ns))
+ return true;
+
+ // ignore RSS data source files (see FeedUtils.jsm)
+ if (name.LowerCaseEqualsLiteral("feeds.json") ||
+ name.LowerCaseEqualsLiteral("feeds.json.tmp") ||
+ name.LowerCaseEqualsLiteral("feeds.json.backup") ||
+ name.LowerCaseEqualsLiteral("feeds.json.corrupt") ||
+ name.LowerCaseEqualsLiteral("feeditems.json") ||
+ name.LowerCaseEqualsLiteral("feeditems.json.tmp") ||
+ name.LowerCaseEqualsLiteral("feeditems.json.backup") ||
+ name.LowerCaseEqualsLiteral("feeditems.json.corrupt") ||
+ name.LowerCaseEqualsLiteral("feeds.rdf") ||
+ name.LowerCaseEqualsLiteral("feeditems.rdf") ||
+ StringBeginsWith(name, u"feeditems_error"_ns))
+ return true;
+
+ // Ignore hidden and other special system files.
+ bool specialFile = false;
+ path->IsHidden(&specialFile);
+ if (specialFile) return true;
+ specialFile = false;
+ path->IsSpecial(&specialFile);
+ if (specialFile) return true;
+
+ // The .mozmsgs dir is for spotlight support
+ return (StringEndsWith(name, u".mozmsgs"_ns) ||
+ StringEndsWith(name, NS_LITERAL_STRING_FROM_CSTRING(FOLDER_SUFFIX)) ||
+ StringEndsWith(name, NS_LITERAL_STRING_FROM_CSTRING(SUMMARY_SUFFIX)));
+}
+
+// Attempts to fill a buffer. Returns a span holding the data read.
+// Might be less than buffer size, if EOF was encountered.
+// Upon error, an empty span is returned.
+static mozilla::Span<char> readBuf(nsIInputStream* readable,
+ mozilla::Buffer<char>& buf) {
+ uint32_t total = 0;
+ while (total < buf.Length()) {
+ uint32_t n;
+ nsresult rv =
+ readable->Read(buf.Elements() + total, buf.Length() - total, &n);
+ if (NS_FAILED(rv)) {
+ total = 0;
+ break;
+ }
+ if (n == 0) {
+ break; // EOF
+ }
+ total += n;
+ }
+ return mozilla::Span<char>(buf.Elements(), total);
+}
+
+// Write data to outputstream, until complete or error.
+static nsresult writeBuf(nsIOutputStream* writeable, const char* data,
+ size_t dataSize) {
+ uint32_t written = 0;
+ while (written < dataSize) {
+ uint32_t n;
+ nsresult rv = writeable->Write(data + written, dataSize - written, &n);
+ NS_ENSURE_SUCCESS(rv, rv);
+ written += n;
+ }
+ return NS_OK;
+}
+
+/**
+ * Attempt to update X-Mozilla-Status and X-Mozilla-Status2 headers with
+ * new message flags by rewriting them in place.
+ */
+nsresult nsMsgLocalStoreUtils::RewriteMsgFlags(nsISeekableStream* seekable,
+ uint32_t msgFlags) {
+ nsresult rv;
+
+ // Remember where we started.
+ int64_t msgStart;
+ rv = seekable->Tell(&msgStart);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We edit the file in-place, so need to be able to read and write too.
+ nsCOMPtr<nsIInputStream> readable(do_QueryInterface(seekable, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIOutputStream> writable = do_QueryInterface(seekable, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read in the first chunk of the header and search for the X-Mozilla-Status
+ // headers. We know that those headers always appear at the beginning, so
+ // don't need to look too far in.
+ mozilla::Buffer<char> buf(512);
+ mozilla::Span<const char> data = readBuf(readable, buf);
+
+ // If there's a "From " line, consume it.
+ mozilla::Span<const char> fromLine;
+ if (data.Length() >= 5 &&
+ nsDependentCSubstring(data.First(5)).EqualsLiteral("From ")) {
+ fromLine = FirstLine(data);
+ data = data.From(fromLine.Length());
+ }
+
+ HeaderReader::Hdr statusHdr;
+ HeaderReader::Hdr status2Hdr;
+ auto findHeadersFn = [&](auto const& hdr) {
+ if (hdr.Name(data).EqualsLiteral(X_MOZILLA_STATUS)) {
+ statusHdr = hdr;
+ } else if (hdr.Name(data).EqualsLiteral(X_MOZILLA_STATUS2)) {
+ status2Hdr = hdr;
+ } else {
+ return true; // Keep looking.
+ }
+ // Keep looking until we find both.
+ return statusHdr.IsEmpty() || status2Hdr.IsEmpty();
+ };
+ HeaderReader rdr;
+ rdr.Parse(data, findHeadersFn);
+
+ // Update X-Mozilla-Status (holds the lower 16bits worth of flags).
+ if (!statusHdr.IsEmpty()) {
+ uint32_t oldFlags = statusHdr.Value(data).ToInteger(&rv, 16);
+ if (NS_SUCCEEDED(rv)) {
+ // Preserve the Queued flag from existing X-Mozilla-Status header.
+ // (Note: not sure why we do this, but keeping it in for now. - BenC)
+ msgFlags |= oldFlags & nsMsgMessageFlags::Queued;
+
+ if ((msgFlags & 0xFFFF) != oldFlags) {
+ auto out = nsPrintfCString("%4.4x", msgFlags & 0xFFFF);
+ if (out.Length() <= statusHdr.rawValLen) {
+ rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
+ msgStart + fromLine.Length() + statusHdr.pos +
+ statusHdr.rawValOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Should be an exact fit already, but just in case...
+ while (out.Length() < statusHdr.rawValLen) {
+ out.Append(' ');
+ }
+ rv = writeBuf(writable, out.BeginReading(), out.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+
+ // Update X-Mozilla-Status2 (holds the upper 16bit flags only(!)).
+ if (!status2Hdr.IsEmpty()) {
+ uint32_t oldFlags = status2Hdr.Value(data).ToInteger(&rv, 16);
+ if (NS_SUCCEEDED(rv)) {
+ if ((msgFlags & 0xFFFF0000) != oldFlags) {
+ auto out = nsPrintfCString("%8.8x", msgFlags & 0xFFFF0000);
+ if (out.Length() <= status2Hdr.rawValLen) {
+ rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
+ msgStart + fromLine.Length() + status2Hdr.pos +
+ status2Hdr.rawValOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (out.Length() < status2Hdr.rawValLen) {
+ out.Append(' ');
+ }
+ rv = writeBuf(writable, out.BeginReading(), out.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Returns true if there is enough space on disk.
+ *
+ * @param aFile Any file in the message store that is on a logical
+ * disk volume so that it can be queried for disk space.
+ * @param aSpaceRequested The size of free space there must be on the disk
+ * to return true.
+ */
+bool nsMsgLocalStoreUtils::DiskSpaceAvailableInStore(nsIFile* aFile,
+ uint64_t aSpaceRequested) {
+ int64_t diskFree;
+ nsresult rv = aFile->GetDiskSpaceAvailable(&diskFree);
+ if (NS_SUCCEEDED(rv)) {
+#ifdef DEBUG
+ printf("GetDiskSpaceAvailable returned: %lld bytes\n", (long long)diskFree);
+#endif
+ // When checking for disk space available, take into consideration
+ // possible database changes, therefore ask for a little more
+ // (EXTRA_SAFETY_SPACE) than what the requested size is. Also, due to disk
+ // sector sizes, allocation blocks, etc. The space "available" may be
+ // greater than the actual space usable.
+ return ((aSpaceRequested + EXTRA_SAFETY_SPACE) < (uint64_t)diskFree);
+ } else if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ // The call to GetDiskSpaceAvailable is not implemented!
+ // This will happen on certain platforms where GetDiskSpaceAvailable
+ // is not implemented. Since people on those platforms still need
+ // to download mail, we will simply bypass the disk-space check.
+ //
+ // We'll leave a debug message to warn people.
+#ifdef DEBUG
+ printf(
+ "Call to GetDiskSpaceAvailable FAILED because it is not "
+ "implemented!\n");
+#endif
+ return true;
+ } else {
+ printf("Call to GetDiskSpaceAvailable FAILED!\n");
+ return false;
+ }
+}
+
+/**
+ * Resets forceReparse in the database.
+ *
+ * @param aMsgDb The database to reset.
+ */
+void nsMsgLocalStoreUtils::ResetForceReparse(nsIMsgDatabase* aMsgDB) {
+ if (aMsgDB) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ aMsgDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (folderInfo) folderInfo->SetBooleanProperty("forceReparse", false);
+ }
+}
+
+/**
+ * Update the value of an X-Mozilla-Keys header in place.
+ *
+ * @param seekable The stream containing the message, positioned at the
+ * beginning of the message (must also be readable and
+ * writable).
+ * @param keywordsToAdd The list of keywords to add.
+ * @param keywordsToRemove The list of keywords to remove.
+ * @param notEnoughRoom Upon return, this will be set if the header is missing
+ * or too small to contain the new keywords.
+ *
+ */
+nsresult nsMsgLocalStoreUtils::ChangeKeywordsHelper(
+ nsISeekableStream* seekable, nsTArray<nsCString> const& keywordsToAdd,
+ nsTArray<nsCString> const& keywordsToRemove, bool& notEnoughRoom) {
+ notEnoughRoom = false;
+ nsresult rv;
+
+ // Remember where we started.
+ int64_t msgStart;
+ rv = seekable->Tell(&msgStart);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We edit the file in-place, so need to be able to read and write too.
+ nsCOMPtr<nsIInputStream> readable(do_QueryInterface(seekable, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIOutputStream> writable = do_QueryInterface(seekable, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read in the first chunk of the header and search for X-Mozilla-Keys.
+ // We know that it always appears near the beginning, so don't need to look
+ // too far in.
+ mozilla::Buffer<char> buf(512);
+ mozilla::Span<const char> data = readBuf(readable, buf);
+
+ // If there's a "From " line, consume it.
+ mozilla::Span<const char> fromLine;
+ if (data.Length() >= 5 &&
+ nsDependentCSubstring(data.First(5)).EqualsLiteral("From ")) {
+ fromLine = FirstLine(data);
+ data = data.From(fromLine.Length());
+ }
+
+ HeaderReader::Hdr kwHdr;
+ auto findHeaderFn = [&](auto const& hdr) {
+ if (hdr.Name(data).EqualsLiteral(HEADER_X_MOZILLA_KEYWORDS)) {
+ kwHdr = hdr;
+ return false;
+ }
+ return true; // Keep looking.
+ };
+ HeaderReader rdr;
+ rdr.Parse(data, findHeaderFn);
+
+ if (kwHdr.IsEmpty()) {
+ NS_WARNING("X-Mozilla-Keys header not found.");
+ notEnoughRoom = true;
+ return NS_OK;
+ }
+
+ // Get existing keywords.
+ nsTArray<nsCString> keywords;
+ nsAutoCString old(kwHdr.Value(data));
+ old.CompressWhitespace();
+ for (nsACString const& kw : old.Split(' ')) {
+ keywords.AppendElement(kw);
+ }
+
+ bool altered = false;
+ // Add missing keywords.
+ for (auto const& add : keywordsToAdd) {
+ if (!keywords.Contains(add)) {
+ keywords.AppendElement(add);
+ altered = true;
+ }
+ }
+
+ // Remove any keywords we want gone.
+ for (auto const& remove : keywordsToRemove) {
+ auto idx = keywords.IndexOf(remove);
+ if (idx != keywords.NoIndex) {
+ keywords.RemoveElementAt(idx);
+ altered = true;
+ }
+ }
+
+ if (!altered) {
+ return NS_OK;
+ }
+
+ // Write updated keywords over existing value.
+ auto out = StringJoin(" "_ns, keywords);
+ if (out.Length() > kwHdr.rawValLen) {
+ NS_WARNING("X-Mozilla-Keys too small for new value.");
+ notEnoughRoom = true;
+ return NS_OK;
+ }
+ while (out.Length() < kwHdr.rawValLen) {
+ out.Append(' ');
+ }
+
+ rv = seekable->Seek(
+ nsISeekableStream::NS_SEEK_SET,
+ msgStart + fromLine.Length() + kwHdr.pos + kwHdr.rawValOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeBuf(writable, out.BeginReading(), out.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMsgLocalStoreUtils.h b/comm/mailnews/local/src/nsMsgLocalStoreUtils.h
new file mode 100644
index 0000000000..968ee8d436
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgLocalStoreUtils.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsMsgLocalStoreUtils_h__
+#define nsMsgLocalStoreUtils_h__
+
+#include "msgCore.h"
+#include "nsString.h"
+#include "nsISeekableStream.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsMailHeaders.h"
+#include "nsMsgUtils.h"
+#include "nsMsgMessageFlags.h"
+#include "nsArray.h"
+
+/**
+ * Utility Class for handling local mail stores. Berkeley Mailbox
+ * and MailDir stores inherit from this class to share some code.
+ */
+
+class nsMsgLocalStoreUtils {
+ public:
+ nsMsgLocalStoreUtils();
+
+ static nsresult AddDirectorySeparator(nsIFile* path);
+ static bool nsShouldIgnoreFile(nsAString& name, nsIFile* path);
+ static nsresult ChangeKeywordsHelper(
+ nsISeekableStream* seekable, nsTArray<nsCString> const& keywordsToAdd,
+ nsTArray<nsCString> const& keywordsToRemove, bool& notEnoughSpace);
+
+ static void ResetForceReparse(nsIMsgDatabase* aMsgDB);
+
+ nsresult RewriteMsgFlags(nsISeekableStream* seekable, uint32_t flags);
+ bool DiskSpaceAvailableInStore(nsIFile* aFile, uint64_t aSpaceRequested);
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsMsgMaildirStore.cpp b/comm/mailnews/local/src/nsMsgMaildirStore.cpp
new file mode 100644
index 0000000000..2f597cd464
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgMaildirStore.cpp
@@ -0,0 +1,1380 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Class for handling Maildir stores.
+*/
+
+#include "prprf.h"
+#include "msgCore.h"
+#include "nsMsgMaildirStore.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIInputStream.h"
+#include "nsMsgFolderFlags.h"
+#include "nsCOMArray.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsIMsgDatabase.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsMsgUtils.h"
+#include "nsIDBFolderInfo.h"
+#include "nsMailHeaders.h"
+#include "nsParseMailbox.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsITimer.h"
+#include "nsIMailboxUrl.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsLocalUndoTxn.h"
+#include "nsIMessenger.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/UniquePtr.h"
+
+static mozilla::LazyLogModule MailDirLog("MailDirStore");
+
+// Helper function to produce a safe filename from a Message-ID value.
+// We'll percent-encode anything not in this set: [-+.%=@_0-9a-zA-Z]
+// This is an overly-picky set, but should:
+// - leave most sane Message-IDs unchanged
+// - be safe on windows (the pickiest case)
+// - avoid chars that can trip up shell scripts (spaces, semicolons etc)
+// If input contains malicious binary (or multibyte chars) it'll be
+// safely encoded as individual bytes.
+static void percentEncode(nsACString const& in, nsACString& out) {
+ const char* end = in.EndReading();
+ const char* cur;
+ // We know the output will be at least as long as the input.
+ out.SetLength(0);
+ out.SetCapacity(in.Length());
+ for (cur = in.BeginReading(); cur < end; ++cur) {
+ const char c = *cur;
+ bool whitelisted = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z') || c == '-' || c == '+' ||
+ c == '.' || c == '%' || c == '=' || c == '@' || c == '_';
+ if (whitelisted) {
+ out.Append(c);
+ } else {
+ out.AppendPrintf("%%%02x", (unsigned char)c);
+ }
+ }
+}
+
+nsMsgMaildirStore::nsMsgMaildirStore() {}
+
+nsMsgMaildirStore::~nsMsgMaildirStore() {}
+
+NS_IMPL_ISUPPORTS(nsMsgMaildirStore, nsIMsgPluggableStore)
+
+// Iterates over the folders in the "path" directory, and adds subfolders to
+// parent for each Maildir folder found.
+nsresult nsMsgMaildirStore::AddSubFolders(nsIMsgFolder* parent, nsIFile* path,
+ bool deep) {
+ nsCOMArray<nsIFile> currentDirEntries;
+
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ nsresult rv = path->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIFile> currentFile;
+ rv = directoryEnumerator->GetNextFile(getter_AddRefs(currentFile));
+ if (NS_SUCCEEDED(rv) && currentFile) {
+ nsAutoString leafName;
+ currentFile->GetLeafName(leafName);
+ bool isDirectory = false;
+ currentFile->IsDirectory(&isDirectory);
+ // Make sure this really is a mail folder dir (i.e., a directory that
+ // contains cur and tmp sub-dirs, and not a .sbd or .mozmsgs dir).
+ if (isDirectory && !nsShouldIgnoreFile(leafName, currentFile))
+ currentDirEntries.AppendObject(currentFile);
+ }
+ }
+
+ // add the folders
+ int32_t count = currentDirEntries.Count();
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsIFile> currentFile(currentDirEntries[i]);
+
+ nsAutoString leafName;
+ currentFile->GetLeafName(leafName);
+
+ nsCOMPtr<nsIMsgFolder> child;
+ rv = parent->AddSubfolder(leafName, getter_AddRefs(child));
+ if (child) {
+ nsString folderName;
+ child->GetName(folderName); // try to get it from cache/db
+ if (folderName.IsEmpty()) child->SetPrettyName(leafName);
+ if (deep) {
+ nsCOMPtr<nsIFile> path;
+ rv = child->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Construct the .sbd directory path for the possible children of the
+ // folder.
+ GetDirectoryForFolder(path);
+ bool directory = false;
+ // Check that <folder>.sbd really is a directory.
+ path->IsDirectory(&directory);
+ if (directory) AddSubFolders(child, path, true);
+ }
+ }
+ }
+ return rv == NS_MSG_FOLDER_EXISTS ? NS_OK : rv;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::DiscoverSubFolders(nsIMsgFolder* aParentFolder,
+ bool aDeep) {
+ NS_ENSURE_ARG_POINTER(aParentFolder);
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aParentFolder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isServer, directory = false;
+ aParentFolder->GetIsServer(&isServer);
+ if (!isServer) GetDirectoryForFolder(path);
+
+ path->IsDirectory(&directory);
+ if (directory) rv = AddSubFolders(aParentFolder, path, aDeep);
+
+ return (rv == NS_MSG_FOLDER_EXISTS) ? NS_OK : rv;
+}
+
+/**
+ * Create if missing a Maildir-style folder with "tmp" and "cur" subfolders
+ * but no "new" subfolder, because it doesn't make sense in the mail client
+ * context. ("new" directory is for messages on the server that haven't been
+ * seen by a mail client).
+ * aFolderName is already "safe" - it has been through NS_MsgHashIfNecessary.
+ */
+nsresult nsMsgMaildirStore::CreateMaildir(nsIFile* path) {
+ nsresult rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ NS_WARNING("Could not create root directory for message folder");
+ return rv;
+ }
+
+ // Create tmp, cur leaves
+ nsCOMPtr<nsIFile> leaf(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ leaf->InitWithFile(path);
+
+ leaf->AppendNative("tmp"_ns);
+ rv = leaf->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ NS_WARNING("Could not create tmp directory for message folder");
+ return rv;
+ }
+
+ leaf->SetNativeLeafName("cur"_ns);
+ rv = leaf->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ NS_WARNING("Could not create cur directory for message folder");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::CreateFolder(nsIMsgFolder* aParent,
+ const nsAString& aFolderName,
+ nsIMsgFolder** aResult) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (aFolderName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aParent->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get a directory based on our current path
+ bool isServer;
+ aParent->GetIsServer(&isServer);
+ rv = CreateDirectoryForFolder(path, isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make sure the new folder name is valid
+ nsAutoString safeFolderName(aFolderName);
+ NS_MsgHashIfNecessary(safeFolderName);
+
+ path->Append(safeFolderName);
+ bool exists;
+ path->Exists(&exists);
+ if (exists) // check this because localized names are different from disk
+ // names
+ return NS_MSG_FOLDER_EXISTS;
+
+ rv = CreateMaildir(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> child;
+ // GetFlags and SetFlags in AddSubfolder will fail because we have no db at
+ // this point but mFlags is set.
+ rv = aParent->AddSubfolder(safeFolderName, getter_AddRefs(child));
+ if (!child || NS_FAILED(rv)) {
+ path->Remove(true); // recursive
+ return rv;
+ }
+
+ // Create an empty database for this mail folder, set its name from the user
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ if (msgDBService) {
+ nsCOMPtr<nsIMsgDatabase> unusedDB;
+ rv = msgDBService->OpenFolderDB(child, true, getter_AddRefs(unusedDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ rv = msgDBService->CreateNewDB(child, getter_AddRefs(unusedDB));
+
+ if ((NS_SUCCEEDED(rv) || rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) &&
+ unusedDB) {
+ // need to set the folder name
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (NS_SUCCEEDED(rv)) folderInfo->SetMailboxName(safeFolderName);
+
+ unusedDB->SetSummaryValid(true);
+ unusedDB->Close(true);
+ aParent->UpdateSummaryTotals(true);
+ } else {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("CreateFolder - failed creating db for new folder"));
+ path->Remove(true); // recursive
+ rv = NS_MSG_CANT_CREATE_FOLDER;
+ }
+ }
+ child.forget(aResult);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::HasSpaceAvailable(nsIMsgFolder* aFolder,
+ int64_t aSpaceRequested,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = DiskSpaceAvailableInStore(pathFile, aSpaceRequested);
+ if (!*aResult) return NS_ERROR_FILE_NO_DEVICE_SPACE;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::IsSummaryFileValid(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDB,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aDB);
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ aDB->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ nsresult rv =
+ dbFolderInfo->GetBooleanProperty("maildirValid", false, aResult);
+ if (!*aResult) {
+ nsCOMPtr<nsIFile> newFile;
+ rv = aFolder->GetFilePath(getter_AddRefs(newFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ newFile->Append(u"cur"_ns);
+
+ // If the "cur" sub-dir doesn't exist, and there are no messages
+ // in the db, then the folder is probably new and the db is valid.
+ bool exists;
+ newFile->Exists(&exists);
+ if (!exists) {
+ int32_t numMessages;
+ dbFolderInfo->GetNumMessages(&numMessages);
+ if (!numMessages) *aResult = true;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::SetSummaryFileValid(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aDB,
+ bool aValid) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aDB);
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ aDB->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_STATE(dbFolderInfo);
+ return dbFolderInfo->SetBooleanProperty("maildirValid", aValid);
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::DeleteFolder(nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ bool exists;
+
+ // Delete the Maildir itself.
+ nsCOMPtr<nsIFile> pathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ exists = false;
+ pathFile->Exists(&exists);
+ if (exists) {
+ rv = pathFile->Remove(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Delete any subfolders (.sbd-suffixed directories).
+ AddDirectorySeparator(pathFile);
+ exists = false;
+ pathFile->Exists(&exists);
+ if (exists) {
+ rv = pathFile->Remove(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::RenameFolder(nsIMsgFolder* aFolder,
+ const nsAString& aNewName,
+ nsIMsgFolder** aNewFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aNewFolder);
+
+ // old path
+ nsCOMPtr<nsIFile> oldPathFile;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(oldPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // old sbd directory
+ nsCOMPtr<nsIFile> sbdPathFile;
+ uint32_t numChildren;
+ aFolder->GetNumSubFolders(&numChildren);
+ if (numChildren > 0) {
+ sbdPathFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = sbdPathFile->InitWithFile(oldPathFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GetDirectoryForFolder(sbdPathFile);
+ }
+
+ // old summary
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = aFolder->GetSummaryFile(getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Validate new name
+ nsAutoString safeName(aNewName);
+ NS_MsgHashIfNecessary(safeName);
+
+ aFolder->ForceDBClosed();
+
+ // rename folder
+ rv = oldPathFile->MoveTo(nullptr, safeName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (numChildren > 0) {
+ // rename "*.sbd" directory
+ nsAutoString sbdName = safeName;
+ sbdName.AppendLiteral(FOLDER_SUFFIX);
+ sbdPathFile->MoveTo(nullptr, sbdName);
+ }
+
+ // rename summary
+ nsAutoString summaryName(safeName);
+ summaryName.AppendLiteral(SUMMARY_SUFFIX);
+ oldSummaryFile->MoveTo(nullptr, summaryName);
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ rv = aFolder->GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_ERROR_NULL_POINTER;
+
+ return parentFolder->AddSubfolder(safeName, aNewFolder);
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::CopyFolder(
+ nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDstFolder, bool aIsMoveFolder,
+ nsIMsgWindow* aMsgWindow, nsIMsgCopyServiceListener* aListener,
+ const nsAString& aNewName) {
+ NS_ENSURE_ARG_POINTER(aSrcFolder);
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+
+ nsAutoString folderName;
+ if (aNewName.IsEmpty())
+ aSrcFolder->GetName(folderName);
+ else
+ folderName.Assign(aNewName);
+
+ nsAutoString safeFolderName(folderName);
+ NS_MsgHashIfNecessary(safeFolderName);
+ aSrcFolder->ForceDBClosed();
+
+ nsCOMPtr<nsIFile> oldPath;
+ nsresult rv = aSrcFolder->GetFilePath(getter_AddRefs(oldPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> summaryFile;
+ GetSummaryFileLocation(oldPath, getter_AddRefs(summaryFile));
+
+ nsCOMPtr<nsIFile> newPath;
+ rv = aDstFolder->GetFilePath(getter_AddRefs(newPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // create target directory based on our current path
+ bool isServer;
+ aDstFolder->GetIsServer(&isServer);
+ rv = CreateDirectoryForFolder(newPath, isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> origPath;
+ oldPath->Clone(getter_AddRefs(origPath));
+
+ rv = oldPath->CopyTo(newPath, safeFolderName);
+ NS_ENSURE_SUCCESS(rv, rv); // will fail if a file by that name exists
+
+ // Copy to dir can fail if file does not exist. If copy fails, we test
+ // if the file exists or not, if it does not that's ok, we continue
+ // without copying it. If it fails and file exist and is not zero sized
+ // there is real problem.
+ nsAutoString dbName(safeFolderName);
+ dbName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = summaryFile->CopyTo(newPath, dbName);
+ if (!NS_SUCCEEDED(rv)) {
+ // Test if the file is not empty
+ bool exists;
+ int64_t fileSize;
+ summaryFile->Exists(&exists);
+ summaryFile->GetFileSize(&fileSize);
+ if (exists && fileSize > 0)
+ NS_ENSURE_SUCCESS(rv, rv); // Yes, it should have worked!
+ // else case is file is zero sized, no need to copy it,
+ // not an error
+ // else case is file does not exist - not an error
+ }
+
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ rv = aDstFolder->AddSubfolder(safeFolderName, getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newMsgFolder->SetPrettyName(folderName);
+ uint32_t flags;
+ aSrcFolder->GetFlags(&flags);
+ newMsgFolder->SetFlags(flags);
+ bool changed = false;
+ rv = aSrcFolder->MatchOrChangeFilterDestination(newMsgFolder, true, &changed);
+ if (changed) aSrcFolder->AlertFilterChanged(aMsgWindow);
+
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = aSrcFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copy subfolders to the new location
+ nsresult copyStatus = NS_OK;
+ nsCOMPtr<nsIMsgLocalMailFolder> localNewFolder(
+ do_QueryInterface(newMsgFolder, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* folder : subFolders) {
+ copyStatus =
+ localNewFolder->CopyFolderLocal(folder, false, aMsgWindow, aListener);
+ // Test if the call succeeded, if not we have to stop recursive call
+ if (NS_FAILED(copyStatus)) {
+ // Copy failed we have to notify caller to handle the error and stop
+ // moving the folders. In case this happens to the topmost level of
+ // recursive call, then we just need to break from the while loop and
+ // go to error handling code.
+ if (!aIsMoveFolder) return copyStatus;
+ break;
+ }
+ }
+ }
+
+ if (aIsMoveFolder && NS_SUCCEEDED(copyStatus)) {
+ if (localNewFolder) {
+ nsCOMPtr<nsISupports> srcSupport(do_QueryInterface(aSrcFolder));
+ localNewFolder->OnCopyCompleted(srcSupport, true);
+ }
+
+ // Notify that the folder that was dragged and dropped has been created.
+ // No need to do this for its subfolders - isMoveFolder will be true for
+ // folder.
+ aDstFolder->NotifyFolderAdded(newMsgFolder);
+
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ aSrcFolder->GetParent(getter_AddRefs(msgParent));
+ aSrcFolder->SetParent(nullptr);
+ if (msgParent) {
+ // The files have already been moved, so delete storage false
+ msgParent->PropagateDelete(aSrcFolder, false);
+ oldPath->Remove(true);
+ aSrcFolder->DeleteStorage();
+
+ nsCOMPtr<nsIFile> parentPath;
+ rv = msgParent->GetFilePath(getter_AddRefs(parentPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDirectorySeparator(parentPath);
+ nsCOMPtr<nsIDirectoryEnumerator> children;
+ parentPath->GetDirectoryEntries(getter_AddRefs(children));
+ bool more;
+ // checks if the directory is empty or not
+ if (children && NS_SUCCEEDED(children->HasMoreElements(&more)) && !more)
+ parentPath->Remove(true);
+ }
+ } else {
+ // This is the case where the copy of a subfolder failed.
+ // We have to delete the newDirectory tree to make a "rollback".
+ // Someone should add a popup to warn the user that the move was not
+ // possible.
+ if (aIsMoveFolder && NS_FAILED(copyStatus)) {
+ nsCOMPtr<nsIMsgFolder> msgParent;
+ newMsgFolder->ForceDBClosed();
+ newMsgFolder->GetParent(getter_AddRefs(msgParent));
+ newMsgFolder->SetParent(nullptr);
+ if (msgParent) {
+ msgParent->PropagateDelete(newMsgFolder, false);
+ newMsgFolder->DeleteStorage();
+ AddDirectorySeparator(newPath);
+ newPath->Remove(true); // berkeley mailbox
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::GetNewMsgOutputStream(nsIMsgFolder* aFolder,
+ nsIMsgDBHdr** aNewMsgHdr,
+ nsIOutputStream** aResult) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_ENSURE_ARG_POINTER(aNewMsgHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = aFolder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!*aNewMsgHdr) {
+ rv = db->CreateNewHdr(nsMsgKey_None, aNewMsgHdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // With maildir, messages have whole file to themselves.
+ (*aNewMsgHdr)->SetMessageOffset(0);
+
+ // We're going to save the new message into the maildir 'tmp' folder.
+ // When the message is completed, it can be moved to 'cur'.
+ nsCOMPtr<nsIFile> newFile;
+ rv = aFolder->GetFilePath(getter_AddRefs(newFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ newFile->Append(u"tmp"_ns);
+
+ // let's check if the folder exists
+ // XXX TODO: kill this and make sure maildir creation includes cur/tmp
+ bool exists;
+ newFile->Exists(&exists);
+ if (!exists) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("GetNewMsgOutputStream - tmp subfolder does not exist!!"));
+ rv = newFile->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Generate the 'tmp' file name based on timestamp.
+ // (We'll use the Message-ID as the basis for the final filename,
+ // but we don't have headers at this point).
+ nsAutoCString newName;
+ newName.AppendInt(static_cast<int64_t>(PR_Now()));
+ newFile->AppendNative(newName);
+
+ // CreateUnique, in case we get more than one message per millisecond :-)
+ rv = newFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newFile->GetNativeLeafName(newName);
+ // save the file name in the message header - otherwise no way to retrieve it
+ (*aNewMsgHdr)->SetStringProperty("storeToken", newName);
+ return MsgNewBufferedFileOutputStream(aResult, newFile,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::DiscardNewMessage(nsIOutputStream* aOutputStream,
+ nsIMsgDBHdr* aNewHdr) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+
+ aOutputStream->Close();
+ // file path is stored in message header property "storeToken"
+ nsAutoCString fileName;
+ aNewHdr->GetStringProperty("storeToken", fileName);
+ if (fileName.IsEmpty()) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIFile> path;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aNewHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // path to the message download folder
+ path->Append(u"tmp"_ns);
+ path->AppendNative(fileName);
+
+ return path->Remove(false);
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::FinishNewMessage(nsIOutputStream* aOutputStream,
+ nsIMsgDBHdr* aNewHdr) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+
+ aOutputStream->Close();
+
+ nsCOMPtr<nsIFile> folderPath;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aNewHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // tmp filename is stored in "storeToken".
+ // By now we'll have the Message-ID, which we'll base the final filename on.
+ nsAutoCString tmpName;
+ aNewHdr->GetStringProperty("storeToken", tmpName);
+ if (tmpName.IsEmpty()) {
+ NS_ERROR("FinishNewMessage - no storeToken in msg hdr!!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // path to the new destination
+ nsCOMPtr<nsIFile> curPath;
+ folderPath->Clone(getter_AddRefs(curPath));
+ curPath->Append(u"cur"_ns);
+
+ // let's check if the folder exists
+ // XXX TODO: kill this and make sure maildir creation includes cur/tmp
+ bool exists;
+ curPath->Exists(&exists);
+ if (!exists) {
+ rv = curPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // path to the downloaded message
+ nsCOMPtr<nsIFile> fromPath;
+ folderPath->Clone(getter_AddRefs(fromPath));
+ fromPath->Append(u"tmp"_ns);
+ fromPath->AppendNative(tmpName);
+
+ // Check that the message is still in tmp.
+ // XXX TODO: revisit this. I think it's needed because the
+ // pairing rules for:
+ // GetNewMsgOutputStream(), FinishNewMessage(),
+ // MoveNewlyDownloadedMessage() and DiscardNewMessage()
+ // are not well defined.
+ // If they are sorted out, this code can be removed.
+ fromPath->Exists(&exists);
+ if (!exists) {
+ // Perhaps the message has already moved. See bug 1028372 to fix this.
+ nsCOMPtr<nsIFile> existingPath;
+ curPath->Clone(getter_AddRefs(existingPath));
+ existingPath->AppendNative(tmpName);
+ existingPath->Exists(&exists);
+ if (exists) // then there is nothing to do
+ return NS_OK;
+
+ NS_ERROR("FinishNewMessage - oops! file does not exist!");
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ nsCString msgID;
+ aNewHdr->GetMessageId(getter_Copies(msgID));
+
+ nsCString baseName;
+ // For missing or suspiciously-short Message-IDs, use a timestamp
+ // instead.
+ // This also avoids some special filenames we can't use in windows (CON,
+ // AUX, NUL, LPT1 etc...). With an extension (eg "LPT4.txt") they're all
+ // below 9 chars.
+ if (msgID.Length() < 9) {
+ baseName.AppendInt(static_cast<int64_t>(PR_Now()));
+ } else {
+ percentEncode(msgID, baseName);
+ // No length limit on Message-Id header, but lets clip our filenames
+ // well below any MAX_PATH limits.
+ if (baseName.Length() > (128 - 4)) {
+ baseName.SetLength(128 - 4); // (4 for ".eml")
+ }
+ }
+
+ nsCOMPtr<nsIFile> toPath;
+ curPath->Clone(getter_AddRefs(toPath));
+ nsCString toName(baseName);
+ toName.Append(".eml");
+ toPath->AppendNative(toName);
+
+ // Using CreateUnique in case we have duplicate Message-Ids
+ rv = toPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ if (NS_FAILED(rv)) {
+ // NS_ERROR_FILE_TOO_BIG means CreateUnique() bailed out at 10000 attempts.
+ if (rv != NS_ERROR_FILE_TOO_BIG) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // As a last resort, fall back to using timestamp as filename.
+ toName.SetLength(0);
+ toName.AppendInt(static_cast<int64_t>(PR_Now()));
+ toName.Append(".eml");
+ toPath->SetNativeLeafName(toName);
+ rv = toPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Move into place (using whatever name CreateUnique() settled upon).
+ toPath->GetNativeLeafName(toName);
+ rv = fromPath->MoveToNative(curPath, toName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Update the db to reflect the final filename.
+ aNewHdr->SetStringProperty("storeToken", toName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::MoveNewlyDownloadedMessage(nsIMsgDBHdr* aHdr,
+ nsIMsgFolder* aDestFolder,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCOMPtr<nsIFile> folderPath;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // file path is stored in message header property
+ nsAutoCString fileName;
+ aHdr->GetStringProperty("storeToken", fileName);
+ if (fileName.IsEmpty()) {
+ NS_ERROR("FinishNewMessage - no storeToken in msg hdr!!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // path to the downloaded message
+ nsCOMPtr<nsIFile> fromPath;
+ folderPath->Clone(getter_AddRefs(fromPath));
+ fromPath->Append(u"cur"_ns);
+ fromPath->AppendNative(fileName);
+
+ // let's check if the tmp file exists
+ bool exists;
+ fromPath->Exists(&exists);
+ if (!exists) {
+ NS_ERROR("FinishNewMessage - oops! file does not exist!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // move to the "cur" subfolder
+ nsCOMPtr<nsIFile> toPath;
+ aDestFolder->GetFilePath(getter_AddRefs(folderPath));
+ folderPath->Clone(getter_AddRefs(toPath));
+ toPath->Append(u"cur"_ns);
+
+ // let's check if the folder exists
+ // XXX TODO: kill this and make sure maildir creation includes cur/tmp
+ toPath->Exists(&exists);
+ if (!exists) {
+ rv = toPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgDatabase> destMailDB;
+ rv = aDestFolder->GetMsgDatabase(getter_AddRefs(destMailDB));
+ NS_WARNING_ASSERTION(destMailDB && NS_SUCCEEDED(rv),
+ "failed to open mail db moving message");
+
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+ if (destMailDB)
+ rv = destMailDB->CopyHdrFromExistingHdr(nsMsgKey_None, aHdr, true,
+ getter_AddRefs(newHdr));
+ if (NS_SUCCEEDED(rv) && !newHdr) rv = NS_ERROR_UNEXPECTED;
+
+ if (NS_FAILED(rv)) {
+ aDestFolder->ThrowAlertMsg("filterFolderHdrAddFailed", nullptr);
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> existingPath;
+ toPath->Clone(getter_AddRefs(existingPath));
+ existingPath->AppendNative(fileName);
+ existingPath->Exists(&exists);
+
+ if (exists) {
+ rv = existingPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ existingPath->GetNativeLeafName(fileName);
+ newHdr->SetStringProperty("storeToken", fileName);
+ }
+
+ rv = fromPath->MoveToNative(toPath, fileName);
+ *aResult = NS_SUCCEEDED(rv);
+ if (NS_FAILED(rv))
+ aDestFolder->ThrowAlertMsg("filterFolderWriteFailed", nullptr);
+
+ if (NS_FAILED(rv)) {
+ if (destMailDB) destMailDB->Close(true);
+
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+
+ bool movedMsgIsNew = false;
+ // if we have made it this far then the message has successfully been
+ // written to the new folder now add the header to the destMailDB.
+
+ uint32_t newFlags;
+ newHdr->GetFlags(&newFlags);
+ nsMsgKey msgKey;
+ newHdr->GetMessageKey(&msgKey);
+ if (!(newFlags & nsMsgMessageFlags::Read)) {
+ nsCString junkScoreStr;
+ (void)newHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (atoi(junkScoreStr.get()) != nsIJunkMailPlugin::IS_SPAM_SCORE) {
+ newHdr->OrFlags(nsMsgMessageFlags::New, &newFlags);
+ destMailDB->AddToNewList(msgKey);
+ movedMsgIsNew = true;
+ }
+ }
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgAdded(newHdr);
+
+ if (movedMsgIsNew) {
+ aDestFolder->SetHasNewMessages(true);
+
+ // Notify the message was moved.
+ if (notifier) {
+ notifier->NotifyMsgUnincorporatedMoved(folder, newHdr);
+ }
+ }
+
+ nsCOMPtr<nsIMsgDatabase> sourceDB;
+ rv = folder->GetMsgDatabase(getter_AddRefs(sourceDB));
+
+ if (NS_SUCCEEDED(rv) && sourceDB) sourceDB->RemoveHeaderMdbRow(aHdr);
+
+ destMailDB->SetSummaryValid(true);
+ aDestFolder->UpdateSummaryTotals(true);
+ destMailDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::GetMsgInputStream(nsIMsgFolder* aMsgFolder,
+ const nsACString& aMsgToken,
+ nsIInputStream** aResult) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // construct path to file
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aMsgFolder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aMsgToken.IsEmpty()) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("GetMsgInputStream - empty storeToken!!"));
+ return NS_ERROR_FAILURE;
+ }
+
+ path->Append(u"cur"_ns);
+
+ // let's check if the folder exists
+ // XXX TODO: kill this and make sure maildir creation includes cur/tmp
+ bool exists;
+ path->Exists(&exists);
+ if (!exists) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("GetMsgInputStream - oops! cur subfolder does not exist!"));
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ path->AppendNative(aMsgToken);
+ return NS_NewLocalFileInputStream(aResult, path);
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::DeleteMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray) {
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ for (auto msgHdr : aHdrArray) {
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = folder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString fileName;
+ msgHdr->GetStringProperty("storeToken", fileName);
+
+ if (fileName.IsEmpty()) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("DeleteMessages - empty storeToken!!"));
+ // Perhaps an offline store has not downloaded this particular message.
+ continue;
+ }
+
+ path->Append(u"cur"_ns);
+ path->AppendNative(fileName);
+
+ // Let's check if the message exists.
+ bool exists;
+ path->Exists(&exists);
+ if (!exists) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("DeleteMessages - file does not exist !!"));
+ // Perhaps an offline store has not downloaded this particular message.
+ continue;
+ }
+ path->Remove(false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::CopyMessages(bool aIsMove,
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray,
+ nsIMsgFolder* aDstFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aDstHdrs,
+ nsITransaction** aUndoAction, bool* aCopyDone) {
+ NS_ENSURE_ARG_POINTER(aDstFolder);
+ NS_ENSURE_ARG_POINTER(aCopyDone);
+ NS_ENSURE_ARG_POINTER(aUndoAction);
+
+ *aCopyDone = false;
+ if (aHdrArray.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ nsresult rv;
+ nsIMsgDBHdr* msgHdr = aHdrArray[0];
+ rv = msgHdr->GetFolder(getter_AddRefs(srcFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Both source and destination folders must use maildir type store.
+ nsCOMPtr<nsIMsgPluggableStore> srcStore;
+ nsAutoCString srcType;
+ srcFolder->GetMsgStore(getter_AddRefs(srcStore));
+ if (srcStore) srcStore->GetStoreType(srcType);
+ nsCOMPtr<nsIMsgPluggableStore> dstStore;
+ nsAutoCString dstType;
+ aDstFolder->GetMsgStore(getter_AddRefs(dstStore));
+ if (dstStore) dstStore->GetStoreType(dstType);
+ if (!srcType.EqualsLiteral("maildir") || !dstType.EqualsLiteral("maildir"))
+ return NS_OK;
+
+ // Both source and destination must be local folders. In theory we could
+ // do efficient copies of the offline store of IMAP, but this is not
+ // supported yet. For that, we need to deal with both correct handling
+ // of deletes from the src server, and msgKey = UIDL in the dst folder.
+ nsCOMPtr<nsIMsgLocalMailFolder> destLocalFolder(
+ do_QueryInterface(aDstFolder));
+ if (!destLocalFolder) return NS_OK;
+ nsCOMPtr<nsIMsgLocalMailFolder> srcLocalFolder(do_QueryInterface(srcFolder));
+ if (!srcLocalFolder) return NS_OK;
+
+ // We should be able to use a file move for an efficient copy.
+
+ nsCOMPtr<nsIFile> destFolderPath;
+ nsCOMPtr<nsIMsgDatabase> destDB;
+ aDstFolder->GetMsgDatabase(getter_AddRefs(destDB));
+ rv = aDstFolder->GetFilePath(getter_AddRefs(destFolderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ destFolderPath->Append(u"cur"_ns);
+
+ nsCOMPtr<nsIFile> srcFolderPath;
+ rv = srcFolder->GetFilePath(getter_AddRefs(srcFolderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ srcFolderPath->Append(u"cur"_ns);
+
+ nsCOMPtr<nsIMsgDatabase> srcDB;
+ srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
+ RefPtr<nsLocalMoveCopyMsgTxn> msgTxn = new nsLocalMoveCopyMsgTxn;
+ NS_ENSURE_TRUE(msgTxn, NS_ERROR_OUT_OF_MEMORY);
+ if (NS_SUCCEEDED(msgTxn->Init(srcFolder, aDstFolder, aIsMove))) {
+ if (aIsMove)
+ msgTxn->SetTransactionType(nsIMessenger::eMoveMsg);
+ else
+ msgTxn->SetTransactionType(nsIMessenger::eCopyMsg);
+ }
+
+ aDstHdrs.Clear();
+ aDstHdrs.SetCapacity(aHdrArray.Length());
+
+ for (auto srcHdr : aHdrArray) {
+ nsMsgKey srcKey;
+ srcHdr->GetMessageKey(&srcKey);
+ msgTxn->AddSrcKey(srcKey);
+ nsAutoCString fileName;
+ srcHdr->GetStringProperty("storeToken", fileName);
+ if (fileName.IsEmpty()) {
+ MOZ_LOG(MailDirLog, mozilla::LogLevel::Info,
+ ("GetMsgInputStream - empty storeToken!!"));
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> srcFile;
+ rv = srcFolderPath->Clone(getter_AddRefs(srcFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ srcFile->AppendNative(fileName);
+
+ nsCOMPtr<nsIFile> destFile;
+ destFolderPath->Clone(getter_AddRefs(destFile));
+ destFile->AppendNative(fileName);
+ bool exists;
+ destFile->Exists(&exists);
+ if (exists) {
+ rv = destFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ destFile->GetNativeLeafName(fileName);
+ }
+ if (aIsMove)
+ rv = srcFile->MoveToNative(destFolderPath, fileName);
+ else
+ rv = srcFile->CopyToNative(destFolderPath, fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> destHdr;
+ if (destDB) {
+ rv = destDB->CopyHdrFromExistingHdr(nsMsgKey_None, srcHdr, true,
+ getter_AddRefs(destHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ destHdr->SetStringProperty("storeToken", fileName);
+ aDstHdrs.AppendElement(destHdr);
+ nsMsgKey dstKey;
+ destHdr->GetMessageKey(&dstKey);
+ msgTxn->AddDstKey(dstKey);
+ }
+ }
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgsMoveCopyCompleted(aIsMove, aHdrArray, aDstFolder,
+ aDstHdrs);
+ }
+
+ // For now, we only support local dest folders, and for those we are done and
+ // can delete the messages. Perhaps this should be moved into the folder
+ // when we try to support other folder types.
+ if (aIsMove) {
+ for (auto msgDBHdr : aHdrArray) {
+ srcDB->DeleteHeader(msgDBHdr, nullptr, false, true);
+ }
+ }
+
+ *aCopyDone = true;
+ msgTxn.forget(aUndoAction);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::GetSupportsCompaction(bool* aSupportsCompaction) {
+ NS_ENSURE_ARG_POINTER(aSupportsCompaction);
+ *aSupportsCompaction = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::CompactFolder(nsIMsgFolder* aFolder,
+ nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ return NS_OK;
+}
+
+class MaildirStoreParser {
+ public:
+ MaildirStoreParser(nsIMsgFolder* aFolder, nsIMsgDatabase* aMsgDB,
+ nsIDirectoryEnumerator* aDirectoryEnumerator,
+ nsIUrlListener* aUrlListener);
+ virtual ~MaildirStoreParser();
+
+ nsresult ParseNextMessage(nsIFile* aFile);
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+ nsresult StartTimer();
+
+ nsCOMPtr<nsIDirectoryEnumerator> m_directoryEnumerator;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgDatabase> m_db;
+ nsCOMPtr<nsITimer> m_timer;
+ nsCOMPtr<nsIUrlListener> m_listener;
+};
+
+MaildirStoreParser::MaildirStoreParser(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aMsgDB,
+ nsIDirectoryEnumerator* aDirEnum,
+ nsIUrlListener* aUrlListener) {
+ m_folder = aFolder;
+ m_db = aMsgDB;
+ m_directoryEnumerator = aDirEnum;
+ m_listener = aUrlListener;
+}
+
+MaildirStoreParser::~MaildirStoreParser() {}
+
+nsresult MaildirStoreParser::ParseNextMessage(nsIFile* aFile) {
+ nsresult rv;
+ NS_ENSURE_TRUE(m_db, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIMsgParseMailMsgState> msgParser =
+ do_CreateInstance("@mozilla.org/messenger/messagestateparser;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgParser->SetMailDB(m_db);
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+ rv = m_db->CreateNewHdr(nsMsgKey_None, getter_AddRefs(newMsgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newMsgHdr->SetMessageOffset(0);
+
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+ if (NS_SUCCEEDED(rv) && inputStream) {
+ RefPtr<nsMsgLineStreamBuffer> inputStreamBuffer =
+ new nsMsgLineStreamBuffer(FILE_IO_BUFFER_SIZE, true, false);
+ int64_t fileSize;
+ aFile->GetFileSize(&fileSize);
+ msgParser->SetNewMsgHdr(newMsgHdr);
+ msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState);
+ bool needMoreData = false;
+ char* newLine = nullptr;
+ uint32_t numBytesInLine = 0;
+ // we only have to read the headers, because we know the message size
+ // from the file size. So we can do this in one time slice.
+ do {
+ newLine = inputStreamBuffer->ReadNextLine(inputStream, numBytesInLine,
+ needMoreData);
+ if (newLine) {
+ msgParser->ParseAFolderLine(newLine, numBytesInLine);
+ free(newLine);
+ }
+ } while (newLine && numBytesInLine > 0);
+
+ msgParser->FinishHeader();
+ // A single message needs to be less than 4GB
+ newMsgHdr->SetMessageSize((uint32_t)fileSize);
+ m_db->AddNewHdrToDB(newMsgHdr, true);
+ nsAutoCString storeToken;
+ aFile->GetNativeLeafName(storeToken);
+ newMsgHdr->SetStringProperty("storeToken", storeToken);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+void MaildirStoreParser::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ MaildirStoreParser* parser = (MaildirStoreParser*)aClosure;
+ bool hasMore;
+ parser->m_directoryEnumerator->HasMoreElements(&hasMore);
+ if (!hasMore) {
+ nsCOMPtr<nsIMsgPluggableStore> store;
+ parser->m_folder->GetMsgStore(getter_AddRefs(store));
+ parser->m_timer->Cancel();
+ if (parser->m_db) parser->m_db->SetSummaryValid(true);
+ // store->SetSummaryFileValid(parser->m_folder, parser->m_db, true);
+ if (parser->m_listener) {
+ nsresult rv;
+ nsCOMPtr<nsIMailboxUrl> mailboxurl =
+ do_CreateInstance("@mozilla.org/messenger/mailboxurl;1", &rv);
+ if (NS_SUCCEEDED(rv) && mailboxurl) {
+ nsCOMPtr<nsIMsgMailNewsUrl> url = do_QueryInterface(mailboxurl);
+ url->SetUpdatingFolder(true);
+ nsAutoCString uriSpec("mailbox://");
+ // ### TODO - what if SetSpec fails?
+ (void)url->SetSpecInternal(uriSpec);
+ parser->m_listener->OnStopRunningUrl(url, NS_OK);
+ }
+ }
+ // Parsing complete and timer cancelled, so we release the parser object.
+ delete parser;
+ return;
+ }
+ nsCOMPtr<nsIFile> currentFile;
+ nsresult rv =
+ parser->m_directoryEnumerator->GetNextFile(getter_AddRefs(currentFile));
+ if (NS_SUCCEEDED(rv)) rv = parser->ParseNextMessage(currentFile);
+ if (NS_FAILED(rv) && parser->m_listener)
+ parser->m_listener->OnStopRunningUrl(nullptr, NS_ERROR_FAILURE);
+}
+
+nsresult MaildirStoreParser::StartTimer() {
+ nsresult rv;
+ m_timer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_timer->InitWithNamedFuncCallback(TimerCallback, (void*)this, 0,
+ nsITimer::TYPE_REPEATING_SLACK,
+ "MaildirStoreParser::TimerCallback");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::RebuildIndex(nsIMsgFolder* aFolder,
+ nsIMsgDatabase* aMsgDB,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ // This code needs to iterate over the maildir files, and parse each
+ // file and add a msg hdr to the db for the file.
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aFolder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+ path->Append(u"cur"_ns);
+
+ nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator;
+ rv = path->GetDirectoryEntries(getter_AddRefs(directoryEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MaildirStoreParser* fileParser =
+ new MaildirStoreParser(aFolder, aMsgDB, directoryEnumerator, aListener);
+ NS_ENSURE_TRUE(fileParser, NS_ERROR_OUT_OF_MEMORY);
+ fileParser->StartTimer();
+ ResetForceReparse(aMsgDB);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::ChangeFlags(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray, uint32_t aFlags,
+ bool aSet) {
+ for (auto msgHdr : aHdrArray) {
+ // get output stream for header
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsresult rv = GetOutputStream(msgHdr, outputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Work out the flags we want to write.
+ uint32_t flags = 0;
+ (void)msgHdr->GetFlags(&flags);
+ flags &= ~(nsMsgMessageFlags::RuntimeOnly | nsMsgMessageFlags::Offline);
+ if (aSet) {
+ flags |= aFlags;
+ } else {
+ flags &= ~aFlags;
+ }
+
+ // Rewrite X-Mozilla-Status headers.
+ nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(outputStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RewriteMsgFlags(seekable, flags);
+ if (NS_FAILED(rv)) NS_WARNING("updateFolderFlag failed");
+ }
+ return NS_OK;
+}
+
+// get output stream from header
+nsresult nsMsgMaildirStore::GetOutputStream(
+ nsIMsgDBHdr* aHdr, nsCOMPtr<nsIOutputStream>& aOutputStream) {
+ // file name is stored in message header property "storeToken"
+ nsAutoCString fileName;
+ aHdr->GetStringProperty("storeToken", fileName);
+ if (fileName.IsEmpty()) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = aHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> folderPath;
+ rv = folder->GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> maildirFile;
+ folderPath->Clone(getter_AddRefs(maildirFile));
+ maildirFile->Append(u"cur"_ns);
+ maildirFile->AppendNative(fileName);
+
+ return MsgGetFileStream(maildirFile, getter_AddRefs(aOutputStream));
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::ChangeKeywords(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aHdrArray, const nsACString& aKeywords,
+ bool aAdd) {
+ if (aHdrArray.IsEmpty()) return NS_ERROR_INVALID_ARG;
+
+ nsTArray<nsCString> keywordsToAdd;
+ nsTArray<nsCString> keywordsToRemove;
+ if (aAdd) {
+ ParseString(aKeywords, ' ', keywordsToAdd);
+ } else {
+ ParseString(aKeywords, ' ', keywordsToRemove);
+ }
+
+ for (auto msgHdr : aHdrArray) {
+ // Open the message file.
+ nsCOMPtr<nsIOutputStream> output;
+ nsresult rv = GetOutputStream(msgHdr, output);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(output, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool notEnoughRoom;
+ rv = ChangeKeywordsHelper(seekable, keywordsToAdd, keywordsToRemove,
+ notEnoughRoom);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (notEnoughRoom) {
+ // The growKeywords property indicates that the X-Mozilla-Keys header
+ // doesn't have enough space, and should be rebuilt during the next
+ // folder compaction.
+ // TODO: For maildir there is no compaction, so this'll have no effect!
+ msgHdr->SetUint32Property("growKeywords", 1);
+ }
+ output->Close();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMaildirStore::GetStoreType(nsACString& aType) {
+ aType.AssignLiteral("maildir");
+ return NS_OK;
+}
+
+/**
+ * Finds the directory associated with this folder. That is if the path is
+ * c:\Inbox, it will return c:\Inbox.sbd if it succeeds. Path is strictly
+ * an out parameter.
+ */
+nsresult nsMsgMaildirStore::GetDirectoryForFolder(nsIFile* path) {
+ // add directory separator to the path
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ return path->SetLeafName(leafName);
+}
+
+nsresult nsMsgMaildirStore::CreateDirectoryForFolder(nsIFile* path,
+ bool aIsServer) {
+ nsresult rv = NS_OK;
+ if (!aIsServer) {
+ rv = GetDirectoryForFolder(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ bool pathIsDirectory = false;
+ path->IsDirectory(&pathIsDirectory);
+ if (!pathIsDirectory) {
+ bool pathExists;
+ path->Exists(&pathExists);
+ // If for some reason there's a file with the directory separator
+ // then we are going to fail.
+ rv = pathExists ? NS_MSG_COULD_NOT_CREATE_DIRECTORY
+ : path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgMaildirStore::SliceStream(nsIInputStream* inStream, uint64_t start,
+ uint32_t length, nsIInputStream** result) {
+ nsCOMPtr<nsIInputStream> in(inStream);
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(in.forget(), start, uint64_t(length));
+ slicedStream.forget(result);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsMsgMaildirStore.h b/comm/mailnews/local/src/nsMsgMaildirStore.h
new file mode 100644
index 0000000000..0ab95be6c0
--- /dev/null
+++ b/comm/mailnews/local/src/nsMsgMaildirStore.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Class for handling Maildir stores.
+*/
+
+#ifndef nsMsgMaildirStore_h__
+#define nsMsgMaildirStore_h__
+
+#include "nsMsgLocalStoreUtils.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIFile.h"
+#include "nsMsgMessageFlags.h"
+
+class nsMsgMaildirStore final : public nsMsgLocalStoreUtils,
+ nsIMsgPluggableStore {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPLUGGABLESTORE
+
+ nsMsgMaildirStore();
+
+ private:
+ ~nsMsgMaildirStore();
+
+ protected:
+ nsresult GetDirectoryForFolder(nsIFile* path);
+ nsresult CreateDirectoryForFolder(nsIFile* path, bool aIsServer);
+
+ nsresult CreateMaildir(nsIFile* path);
+ nsresult AddSubFolders(nsIMsgFolder* parent, nsIFile* path, bool deep);
+ nsresult GetOutputStream(nsIMsgDBHdr* aHdr,
+ nsCOMPtr<nsIOutputStream>& aOutputStream);
+};
+#endif
diff --git a/comm/mailnews/local/src/nsNoIncomingServer.cpp b/comm/mailnews/local/src/nsNoIncomingServer.cpp
new file mode 100644
index 0000000000..b3a351eda6
--- /dev/null
+++ b/comm/mailnews/local/src/nsNoIncomingServer.cpp
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // pre-compiled headers
+
+#include "prmem.h"
+#include "plstr.h"
+#include "prprf.h"
+#include "nsNoIncomingServer.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgMailSession.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+
+NS_IMPL_ISUPPORTS_INHERITED(nsNoIncomingServer, nsMsgIncomingServer,
+ nsINoIncomingServer, nsILocalMailIncomingServer)
+
+nsNoIncomingServer::nsNoIncomingServer() {}
+
+nsNoIncomingServer::~nsNoIncomingServer() {}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetLocalStoreType(nsACString& type) {
+ type.AssignLiteral("mailbox");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetLocalDatabaseType(nsACString& type) {
+ type.AssignLiteral("mailbox");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetAccountManagerChrome(nsAString& aResult) {
+ aResult.AssignLiteral("am-serverwithnoidentities.xhtml");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::SetFlagsOnDefaultMailboxes() {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(rootFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // None server may have an inbox if it's deferred to,
+ // or if it's the smart mailboxes account.
+ localFolder->SetFlagsOnDefaultMailboxes(nsMsgFolderFlags::SpecialUse);
+
+ return NS_OK;
+}
+
+// TODO: make this work with maildir message store, bug 890742.
+NS_IMETHODIMP nsNoIncomingServer::CopyDefaultMessages(
+ const char* folderNameOnDisk) {
+ NS_ENSURE_ARG(folderNameOnDisk);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get defaults directory for messenger files. MailSession service appends
+ // 'messenger' to the the app defaults folder and returns it. Locale will be
+ // added to the path, if there is one.
+ nsCOMPtr<nsIFile> defaultMessagesFile;
+ rv = mailSession->GetDataFilesDir("messenger",
+ getter_AddRefs(defaultMessagesFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if bin/defaults/messenger/<folderNameOnDisk>
+ // (or bin/defaults/messenger/<locale>/<folderNameOnDisk> if we had a locale
+ // provide) exists. it doesn't have to exist. if it doesn't, return
+ rv = defaultMessagesFile->AppendNative(nsDependentCString(folderNameOnDisk));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists;
+ rv = defaultMessagesFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) return NS_OK;
+
+ nsCOMPtr<nsIFile> parentDir;
+ rv = GetLocalPath(getter_AddRefs(parentDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if parentDir/<folderNameOnDisk> exists
+ {
+ nsCOMPtr<nsIFile> testDir;
+ rv = parentDir->Clone(getter_AddRefs(testDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = testDir->AppendNative(nsDependentCString(folderNameOnDisk));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = testDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // if it exists add to the end, else copy
+ if (exists) {
+#ifdef DEBUG
+ printf("append default %s (unimplemented)\n", folderNameOnDisk);
+#endif
+ // todo for bug #1181 (the bug ID seems wrong...)
+ // open folderFile, seek to end
+ // read defaultMessagesFile, write to folderFile
+ } else {
+#ifdef DEBUG
+ printf("copy default %s\n", folderNameOnDisk);
+#endif
+ rv = defaultMessagesFile->CopyTo(parentDir, EmptyString());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNoIncomingServer::CreateDefaultMailboxes() {
+ nsresult rv;
+ bool isHidden = false;
+ GetHidden(&isHidden);
+ if (isHidden) return NS_OK;
+
+ // notice, no Inbox, unless we're deferred to...
+ bool isDeferredTo;
+ if (NS_SUCCEEDED(GetIsDeferredTo(&isDeferredTo)) && isDeferredTo) {
+ rv = CreateLocalFolder(u"Inbox"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = CreateLocalFolder(u"Trash"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // copy the default templates into the Templates folder
+ rv = CopyDefaultMessages("Templates");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CreateLocalFolder(u"Unsent Messages"_ns);
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetNewMail(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ nsIMsgFolder* aInbox, nsIURI** aResult) {
+ if (aResult) {
+ *aResult = nullptr;
+ }
+ nsTArray<RefPtr<nsIPop3IncomingServer>> deferredServers;
+ nsresult rv = GetDeferredServers(this, deferredServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!deferredServers.IsEmpty()) {
+ rv = deferredServers[0]->DownloadMailFromServers(
+ deferredServers, aMsgWindow, aInbox, aUrlListener);
+ }
+ // listener might be counting on us to send a notification.
+ else if (aUrlListener)
+ aUrlListener->OnStopRunningUrl(nullptr, NS_OK);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetCanSearchMessages(bool* canSearchMessages) {
+ NS_ENSURE_ARG_POINTER(canSearchMessages);
+ *canSearchMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) {
+ NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff);
+ *aServerRequiresPasswordForBiff =
+ false; // for local folders, we don't require a password
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoIncomingServer::GetSortOrder(int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = 200000000;
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsNoIncomingServer.h b/comm/mailnews/local/src/nsNoIncomingServer.h
new file mode 100644
index 0000000000..79d09baa3a
--- /dev/null
+++ b/comm/mailnews/local/src/nsNoIncomingServer.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef __nsNoIncomingServer_h
+#define __nsNoIncomingServer_h
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsINoIncomingServer.h"
+#include "nsILocalMailIncomingServer.h"
+#include "nsMsgIncomingServer.h"
+#include "nsMailboxServer.h"
+
+/* get some implementation from nsMsgIncomingServer */
+class nsNoIncomingServer : public nsMailboxServer,
+ public nsINoIncomingServer,
+ public nsILocalMailIncomingServer
+
+{
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSINOINCOMINGSERVER
+ NS_DECL_NSILOCALMAILINCOMINGSERVER
+
+ nsNoIncomingServer();
+
+ NS_IMETHOD GetLocalStoreType(nsACString& type) override;
+ NS_IMETHOD GetLocalDatabaseType(nsACString& type) override;
+ NS_IMETHOD GetCanSearchMessages(bool* canSearchMessages) override;
+ NS_IMETHOD GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) override;
+ NS_IMETHOD GetAccountManagerChrome(nsAString& aResult) override;
+ NS_IMETHOD GetSortOrder(int32_t* aSortOrder) override;
+
+ private:
+ virtual ~nsNoIncomingServer();
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsNoneService.cpp b/comm/mailnews/local/src/nsNoneService.cpp
new file mode 100644
index 0000000000..74b03dc93e
--- /dev/null
+++ b/comm/mailnews/local/src/nsNoneService.cpp
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsNoneService.h"
+#include "nsINoIncomingServer.h"
+#include "nsINoneService.h"
+#include "nsIMsgProtocolInfo.h"
+
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+
+#include "nsMailDirServiceDefs.h"
+
+#define PREF_MAIL_ROOT_NONE_REL "mail.root.none-rel"
+// old - for backward compatibility only
+#define PREF_MAIL_ROOT_NONE "mail.root.none"
+
+nsNoneService::nsNoneService() {}
+
+nsNoneService::~nsNoneService() {}
+
+NS_IMPL_ISUPPORTS(nsNoneService, nsINoneService, nsIMsgProtocolInfo)
+
+NS_IMETHODIMP
+nsNoneService::SetDefaultLocalPath(nsIFile* aPath) {
+ NS_ENSURE_ARG(aPath);
+ return NS_SetPersistentFile(PREF_MAIL_ROOT_NONE_REL, PREF_MAIL_ROOT_NONE,
+ aPath);
+}
+
+NS_IMETHODIMP
+nsNoneService::GetDefaultLocalPath(nsIFile** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+
+ bool havePref;
+ nsCOMPtr<nsIFile> localFile;
+ nsresult rv = NS_GetPersistentFile(PREF_MAIL_ROOT_NONE_REL,
+ PREF_MAIL_ROOT_NONE, NS_APP_MAIL_50_DIR,
+ havePref, getter_AddRefs(localFile));
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ rv = localFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = localFile->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!havePref || !exists) {
+ rv = NS_SetPersistentFile(PREF_MAIL_ROOT_NONE_REL, PREF_MAIL_ROOT_NONE,
+ localFile);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to set root dir pref.");
+ }
+
+ localFile.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetServerIID(nsIID** aServerIID) {
+ *aServerIID = new nsIID(NS_GET_IID(nsINoIncomingServer));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetRequiresUsername(bool* aRequiresUsername) {
+ NS_ENSURE_ARG_POINTER(aRequiresUsername);
+ *aRequiresUsername = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetPreflightPrettyNameWithEmailAddress(
+ bool* aPreflightPrettyNameWithEmailAddress) {
+ NS_ENSURE_ARG_POINTER(aPreflightPrettyNameWithEmailAddress);
+ *aPreflightPrettyNameWithEmailAddress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetCanLoginAtStartUp(bool* aCanLoginAtStartUp) {
+ NS_ENSURE_ARG_POINTER(aCanLoginAtStartUp);
+ *aCanLoginAtStartUp = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetCanDelete(bool* aCanDelete) {
+ NS_ENSURE_ARG_POINTER(aCanDelete);
+ *aCanDelete = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetCanDuplicate(bool* aCanDuplicate) {
+ NS_ENSURE_ARG_POINTER(aCanDuplicate);
+ *aCanDuplicate = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetCanGetMessages(bool* aCanGetMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetMessages);
+ *aCanGetMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetCanGetIncomingMessages(bool* aCanGetIncomingMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetIncomingMessages);
+ *aCanGetIncomingMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetDefaultDoBiff(bool* aDoBiff) {
+ NS_ENSURE_ARG_POINTER(aDoBiff);
+ // by default, don't do biff for "none" servers
+ *aDoBiff = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetDefaultServerPort(bool isSecure, int32_t* aDefaultPort) {
+ NS_ENSURE_ARG_POINTER(aDefaultPort);
+ *aDefaultPort = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetShowComposeMsgLink(bool* showComposeMsgLink) {
+ NS_ENSURE_ARG_POINTER(showComposeMsgLink);
+ *showComposeMsgLink = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNoneService::GetFoldersCreatedAsync(bool* aAsyncCreation) {
+ NS_ENSURE_ARG_POINTER(aAsyncCreation);
+ *aAsyncCreation = false;
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsNoneService.h b/comm/mailnews/local/src/nsNoneService.h
new file mode 100644
index 0000000000..f87b25b846
--- /dev/null
+++ b/comm/mailnews/local/src/nsNoneService.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsNoneService_h___
+#define nsNoneService_h___
+
+#include "nscore.h"
+
+#include "nsIMsgProtocolInfo.h"
+#include "nsINoneService.h"
+
+class nsNoneService : public nsIMsgProtocolInfo, public nsINoneService {
+ public:
+ nsNoneService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPROTOCOLINFO
+ NS_DECL_NSINONESERVICE
+
+ private:
+ virtual ~nsNoneService();
+};
+
+#endif /* nsNoneService_h___ */
diff --git a/comm/mailnews/local/src/nsParseMailbox.cpp b/comm/mailnews/local/src/nsParseMailbox.cpp
new file mode 100644
index 0000000000..7dc3c4d5b7
--- /dev/null
+++ b/comm/mailnews/local/src/nsParseMailbox.cpp
@@ -0,0 +1,2354 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsIURI.h"
+#include "nsIChannel.h"
+#include "nsParseMailbox.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIInputStream.h"
+#include "nsIFile.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsIMailboxUrl.h"
+#include "nsNetUtil.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIMsgFilterHitNotify.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsMsgI18N.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsMsgUtils.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsMsgSearchCore.h"
+#include "nsMailHeaders.h"
+#include "nsIMsgMailSession.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIMsgComposeService.h"
+#include "nsIMsgCopyService.h"
+#include "nsICryptoHash.h"
+#include "nsIStringBundle.h"
+#include "nsPrintfCString.h"
+#include "nsIMsgFilterCustomAction.h"
+#include <ctype.h>
+#include "nsIMsgPluggableStore.h"
+#include "mozilla/Components.h"
+#include "nsQueryObject.h"
+#include "nsIOutputStream.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+
+extern LazyLogModule FILTERLOGMODULE;
+
+/* the following macros actually implement addref, release and query interface
+ * for our component. */
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgMailboxParser, nsParseMailMessageState,
+ nsIStreamListener, nsIRequestObserver)
+
+// Whenever data arrives from the connection, core netlib notifices the protocol
+// by calling OnDataAvailable. We then read and process the incoming data from
+// the input stream.
+NS_IMETHODIMP nsMsgMailboxParser::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* aIStream,
+ uint64_t sourceOffset,
+ uint32_t aLength) {
+ return ProcessMailboxInputStream(aIStream, aLength);
+}
+
+NS_IMETHODIMP nsMsgMailboxParser::OnStartRequest(nsIRequest* request) {
+ // extract the appropriate event sinks from the url and initialize them in our
+ // protocol data the URL should be queried for a nsIMailboxURL. If it doesn't
+ // support a mailbox URL interface then we have an error.
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIIOService> ioServ = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioServ, NS_ERROR_UNEXPECTED);
+
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMailboxUrl> runningUrl = do_QueryInterface(uri, &rv);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> url = do_QueryInterface(uri);
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_folder);
+
+ if (NS_SUCCEEDED(rv) && runningUrl && folder) {
+ url->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+
+ // okay, now fill in our event sinks...Note that each getter ref counts
+ // before it returns the interface to us...we'll release when we are done
+
+ folder->GetName(m_folderName);
+
+ nsCOMPtr<nsIFile> path;
+ folder->GetFilePath(getter_AddRefs(path));
+
+ if (path) {
+ int64_t fileSize;
+ path->GetFileSize(&fileSize);
+ // the size of the mailbox file is our total base line for measuring
+ // progress
+ m_graph_progress_total = fileSize;
+ UpdateStatusText("buildingSummary");
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ if (msgDBService) {
+ // Use OpenFolderDB to always open the db so that db's m_folder
+ // is set correctly.
+ rv = msgDBService->OpenFolderDB(folder, true, getter_AddRefs(m_mailDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ rv = msgDBService->CreateNewDB(folder, getter_AddRefs(m_mailDB));
+
+ if (m_mailDB) m_mailDB->AddListener(this);
+ }
+ NS_ASSERTION(m_mailDB, "failed to open mail db parsing folder");
+
+ // try to get a backup message database
+ nsresult rvignore =
+ folder->GetBackupMsgDatabase(getter_AddRefs(m_backupMailDB));
+
+ // We'll accept failures and move on, as we're dealing with some
+ // sort of unknown problem to begin with.
+ if (NS_FAILED(rvignore)) {
+ if (m_backupMailDB) m_backupMailDB->RemoveListener(this);
+ m_backupMailDB = nullptr;
+ } else if (m_backupMailDB) {
+ m_backupMailDB->AddListener(this);
+ }
+ }
+ }
+
+ // need to get the mailbox name out of the url and call SetMailboxName with
+ // it. then, we need to open the mail db for this parser.
+ return rv;
+}
+
+// stop binding is a "notification" informing us that the stream associated with
+// aURL is going away.
+NS_IMETHODIMP nsMsgMailboxParser::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ DoneParsingFolder(aStatus);
+ // what can we do? we can close the stream?
+
+ if (m_mailDB) m_mailDB->RemoveListener(this);
+ // and we want to mark ourselves for deletion or some how inform our protocol
+ // manager that we are available for another url if there is one....
+
+ ReleaseFolderLock();
+ // be sure to clear any status text and progress info..
+ m_graph_progress_received = 0;
+ UpdateProgressPercent();
+ UpdateStatusText("localStatusDocumentDone");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParseMailMessageState::OnHdrPropertyChanged(
+ nsIMsgDBHdr* aHdrToChange, const nsACString& property, bool aPreChange,
+ uint32_t* aStatus, nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParseMailMessageState::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged,
+ uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParseMailMessageState::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParseMailMessageState::OnHdrAdded(nsIMsgDBHdr* aHdrAdded, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+/* void OnParentChanged (in nsMsgKey aKeyChanged, in nsMsgKey oldParent, in
+ * nsMsgKey newParent, in nsIDBChangeListener aInstigator); */
+NS_IMETHODIMP
+nsParseMailMessageState::OnParentChanged(nsMsgKey aKeyChanged,
+ nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+/* void OnAnnouncerGoingAway (in nsIDBChangeAnnouncer instigator); */
+NS_IMETHODIMP
+nsParseMailMessageState::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ if (m_backupMailDB && m_backupMailDB == instigator) {
+ m_backupMailDB->RemoveListener(this);
+ m_backupMailDB = nullptr;
+ } else if (m_mailDB) {
+ m_mailDB->RemoveListener(this);
+ m_mailDB = nullptr;
+ m_newMsgHdr = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::OnEvent(nsIMsgDatabase* aDB,
+ const char* aEvent) {
+ return NS_OK;
+}
+
+/* void OnReadChanged (in nsIDBChangeListener instigator); */
+NS_IMETHODIMP
+nsParseMailMessageState::OnReadChanged(nsIDBChangeListener* instigator) {
+ return NS_OK;
+}
+
+/* void OnJunkScoreChanged (in nsIDBChangeListener instigator); */
+NS_IMETHODIMP
+nsParseMailMessageState::OnJunkScoreChanged(nsIDBChangeListener* instigator) {
+ return NS_OK;
+}
+
+nsMsgMailboxParser::nsMsgMailboxParser() : nsMsgLineBuffer() { Init(); }
+
+nsMsgMailboxParser::nsMsgMailboxParser(nsIMsgFolder* aFolder)
+ : nsMsgLineBuffer() {
+ m_folder = do_GetWeakReference(aFolder);
+}
+
+nsMsgMailboxParser::~nsMsgMailboxParser() { ReleaseFolderLock(); }
+
+nsresult nsMsgMailboxParser::Init() {
+ m_graph_progress_total = 0;
+ m_graph_progress_received = 0;
+ return AcquireFolderLock();
+}
+
+void nsMsgMailboxParser::UpdateStatusText(const char* stringName) {
+ if (m_statusFeedback) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (!bundleService) return;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/localMsgs.properties",
+ getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return;
+ nsString finalString;
+ AutoTArray<nsString, 1> stringArray = {m_folderName};
+ rv = bundle->FormatStringFromName(stringName, stringArray, finalString);
+ m_statusFeedback->ShowStatusString(finalString);
+ }
+}
+
+void nsMsgMailboxParser::UpdateProgressPercent() {
+ if (m_statusFeedback && m_graph_progress_total != 0) {
+ // prevent overflow by dividing both by 100
+ int64_t progressTotal = m_graph_progress_total / 100;
+ int64_t progressReceived = m_graph_progress_received / 100;
+ if (progressTotal > 0)
+ m_statusFeedback->ShowProgress((100 * (progressReceived)) /
+ progressTotal);
+ }
+}
+
+nsresult nsMsgMailboxParser::ProcessMailboxInputStream(nsIInputStream* aIStream,
+ uint32_t aLength) {
+ nsresult ret = NS_OK;
+
+ uint32_t bytesRead = 0;
+
+ if (NS_SUCCEEDED(m_inputStream.GrowBuffer(aLength))) {
+ // OK, this sucks, but we're going to have to copy into our
+ // own byte buffer, and then pass that to the line buffering code,
+ // which means a couple buffer copies.
+ ret = aIStream->Read(m_inputStream.GetBuffer(), aLength, &bytesRead);
+ if (NS_SUCCEEDED(ret))
+ ret = BufferInput(m_inputStream.GetBuffer(), bytesRead);
+ }
+ if (m_graph_progress_total > 0) {
+ if (NS_SUCCEEDED(ret)) m_graph_progress_received += bytesRead;
+ }
+ return (ret);
+}
+
+void nsMsgMailboxParser::DoneParsingFolder(nsresult status) {
+ // End of file. Flush out any data remaining in the buffer.
+ Flush();
+ PublishMsgHeader(nullptr);
+
+ // only mark the db valid if we've succeeded.
+ if (NS_SUCCEEDED(status) &&
+ m_mailDB) // finished parsing, so flush db folder info
+ UpdateDBFolderInfo();
+ else if (m_mailDB)
+ m_mailDB->SetSummaryValid(false);
+
+ // remove the backup database
+ if (m_backupMailDB) {
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_folder);
+ if (folder) folder->RemoveBackupMsgDatabase();
+ m_backupMailDB = nullptr;
+ }
+}
+
+void nsMsgMailboxParser::UpdateDBFolderInfo() { UpdateDBFolderInfo(m_mailDB); }
+
+// update folder info in db so we know not to reparse.
+void nsMsgMailboxParser::UpdateDBFolderInfo(nsIMsgDatabase* mailDB) {
+ mailDB->SetSummaryValid(true);
+}
+
+// Tell the world about the message header (add to db, and view, if any)
+int32_t nsMsgMailboxParser::PublishMsgHeader(nsIMsgWindow* msgWindow) {
+ FinishHeader();
+ if (m_newMsgHdr) {
+ nsCString storeToken = nsPrintfCString("%" PRIu64, m_envelope_pos);
+ m_newMsgHdr->SetStringProperty("storeToken", storeToken);
+ m_newMsgHdr->SetMessageOffset(m_envelope_pos);
+
+ uint32_t flags;
+ (void)m_newMsgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Expunged) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ m_mailDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ uint32_t size;
+ (void)m_newMsgHdr->GetMessageSize(&size);
+ folderInfo->ChangeExpungedBytes(size);
+ m_newMsgHdr = nullptr;
+ } else if (m_mailDB) {
+ // add hdr but don't notify - shouldn't be requiring notifications
+ // during summary file rebuilding
+ m_mailDB->AddNewHdrToDB(m_newMsgHdr, false);
+ m_newMsgHdr = nullptr;
+ } else
+ NS_ASSERTION(
+ false,
+ "no database while parsing local folder"); // should have a DB, no?
+ } else if (m_mailDB) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ m_mailDB->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (folderInfo)
+ folderInfo->ChangeExpungedBytes(m_position - m_envelope_pos);
+ }
+ return 0;
+}
+
+void nsMsgMailboxParser::AbortNewHeader() {
+ if (m_newMsgHdr && m_mailDB) m_newMsgHdr = nullptr;
+}
+
+void nsMsgMailboxParser::OnNewMessage(nsIMsgWindow* msgWindow) {
+ PublishMsgHeader(msgWindow);
+ Clear();
+}
+
+nsresult nsMsgMailboxParser::HandleLine(const char* line, uint32_t lineLength) {
+ /* If this is the very first line of a non-empty folder, make sure it's an
+ * envelope */
+ if (m_graph_progress_received == 0) {
+ /* This is the first block from the file. Check to see if this
+ looks like a mail file. */
+ const char* s = line;
+ const char* end = s + lineLength;
+ while (s < end && IS_SPACE(*s)) s++;
+ if ((end - s) < 20 || !IsEnvelopeLine(s, end - s)) {
+ // char buf[500];
+ // PR_snprintf (buf, sizeof(buf),
+ // XP_GetString(MK_MSG_NON_MAIL_FILE_READ_QUESTION),
+ // folder_name);
+ // else if (!FE_Confirm (m_context, buf))
+ // return NS_MSG_NOT_A_MAIL_FOLDER; /* #### NOT_A_MAIL_FILE */
+ }
+ }
+ // m_graph_progress_received += lineLength;
+
+ // mailbox parser needs to do special stuff when it finds an envelope
+ // after parsing a message body. So do that.
+ if (line[0] == 'F' && IsEnvelopeLine(line, lineLength)) {
+ // **** This used to be
+ // PR_ASSERT (m_parseMsgState->m_state == nsMailboxParseBodyState);
+ // **** I am not sure this is a right thing to do. This happens when
+ // going online, downloading a message while playing back append
+ // draft/template offline operation. We are mixing
+ // nsMailboxParseBodyState &&
+ // nsMailboxParseHeadersState. David I need your help here too. **** jt
+
+ NS_ASSERTION(m_state == nsIMsgParseMailMsgState::ParseBodyState ||
+ m_state == nsIMsgParseMailMsgState::ParseHeadersState,
+ "invalid parse state"); /* else folder corrupted */
+ OnNewMessage(nullptr);
+ nsresult rv = StartNewEnvelope(line, lineLength);
+ NS_ASSERTION(NS_SUCCEEDED(rv), " error starting envelope parsing mailbox");
+ // at the start of each new message, update the progress bar
+ UpdateProgressPercent();
+ return rv;
+ }
+
+ // otherwise, the message parser can handle it completely.
+ if (m_mailDB != nullptr) // if no DB, do we need to parse at all?
+ return ParseFolderLine(line, lineLength);
+
+ return NS_ERROR_NULL_POINTER; // need to error out if we don't have a db.
+}
+
+void nsMsgMailboxParser::ReleaseFolderLock() {
+ nsresult result;
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_folder);
+ if (!folder) return;
+ bool haveSemaphore;
+ nsCOMPtr<nsISupports> supports =
+ do_QueryInterface(static_cast<nsIMsgParseMailMsgState*>(this));
+ result = folder->TestSemaphore(supports, &haveSemaphore);
+ if (NS_SUCCEEDED(result) && haveSemaphore)
+ (void)folder->ReleaseSemaphore(supports);
+}
+
+nsresult nsMsgMailboxParser::AcquireFolderLock() {
+ nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_folder);
+ if (!folder) return NS_ERROR_NULL_POINTER;
+ nsCOMPtr<nsISupports> supports = do_QueryObject(this);
+ return folder->AcquireSemaphore(supports);
+}
+
+NS_IMPL_ISUPPORTS(nsParseMailMessageState, nsIMsgParseMailMsgState,
+ nsIDBChangeListener)
+
+nsParseMailMessageState::nsParseMailMessageState() {
+ m_position = 0;
+ m_new_key = nsMsgKey_None;
+ m_state = nsIMsgParseMailMsgState::ParseBodyState;
+
+ // setup handling of custom db headers, headers that are added to .msf files
+ // as properties of the nsMsgHdr objects, controlled by the
+ // pref mailnews.customDBHeaders, a space-delimited list of headers.
+ // E.g., if mailnews.customDBHeaders is "X-Spam-Score", and we're parsing
+ // a mail message with the X-Spam-Score header, we'll set the
+ // "x-spam-score" property of nsMsgHdr to the value of the header.
+ m_customDBHeaderValues = nullptr;
+ nsCString customDBHeaders; // not shown in search UI
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!pPrefBranch) {
+ return;
+ }
+ pPrefBranch->GetCharPref("mailnews.customDBHeaders", customDBHeaders);
+ ToLowerCase(customDBHeaders);
+ if (customDBHeaders.Find("content-base") == -1)
+ customDBHeaders.InsertLiteral("content-base ", 0);
+ ParseString(customDBHeaders, ' ', m_customDBHeaders);
+
+ // now add customHeaders
+ nsCString customHeadersString; // shown in search UI
+ nsTArray<nsCString> customHeadersArray;
+ pPrefBranch->GetCharPref("mailnews.customHeaders", customHeadersString);
+ ToLowerCase(customHeadersString);
+ customHeadersString.StripWhitespace();
+ ParseString(customHeadersString, ':', customHeadersArray);
+ for (uint32_t i = 0; i < customHeadersArray.Length(); i++) {
+ if (!m_customDBHeaders.Contains(customHeadersArray[i]))
+ m_customDBHeaders.AppendElement(customHeadersArray[i]);
+ }
+
+ if (m_customDBHeaders.Length()) {
+ m_customDBHeaderValues =
+ new struct message_header[m_customDBHeaders.Length()];
+ }
+ Clear();
+}
+
+nsParseMailMessageState::~nsParseMailMessageState() {
+ ClearAggregateHeader(m_toList);
+ ClearAggregateHeader(m_ccList);
+ delete[] m_customDBHeaderValues;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::Clear() {
+ m_message_id.length = 0;
+ m_references.length = 0;
+ m_date.length = 0;
+ m_delivery_date.length = 0;
+ m_from.length = 0;
+ m_sender.length = 0;
+ m_newsgroups.length = 0;
+ m_subject.length = 0;
+ m_status.length = 0;
+ m_mozstatus.length = 0;
+ m_mozstatus2.length = 0;
+ m_envelope_from.length = 0;
+ m_envelope_date.length = 0;
+ m_priority.length = 0;
+ m_keywords.length = 0;
+ m_mdn_dnt.length = 0;
+ m_return_path.length = 0;
+ m_account_key.length = 0;
+ m_in_reply_to.length = 0;
+ m_replyTo.length = 0;
+ m_content_type.length = 0;
+ m_mdn_original_recipient.length = 0;
+ m_bccList.length = 0;
+ m_body_lines = 0;
+ m_lastLineBlank = 0;
+ m_newMsgHdr = nullptr;
+ m_envelope_pos = 0;
+ m_new_key = nsMsgKey_None;
+ ClearAggregateHeader(m_toList);
+ ClearAggregateHeader(m_ccList);
+ m_headers.ResetWritePos();
+ m_envelope.ResetWritePos();
+ m_receivedTime = 0;
+ m_receivedValue.Truncate();
+ for (uint32_t i = 0; i < m_customDBHeaders.Length(); i++) {
+ m_customDBHeaderValues[i].length = 0;
+ }
+ m_headerstartpos = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::SetState(nsMailboxParseState aState) {
+ m_state = aState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::GetState(nsMailboxParseState* aState) {
+ if (!aState) return NS_ERROR_NULL_POINTER;
+
+ *aState = m_state;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::GetNewMsgHdr(nsIMsgDBHdr** aMsgHeader) {
+ NS_ENSURE_ARG_POINTER(aMsgHeader);
+ NS_IF_ADDREF(*aMsgHeader = m_newMsgHdr);
+ return m_newMsgHdr ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::SetNewMsgHdr(nsIMsgDBHdr* aMsgHeader) {
+ m_newMsgHdr = aMsgHeader;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::ParseAFolderLine(const char* line,
+ uint32_t lineLength) {
+ ParseFolderLine(line, lineLength);
+ return NS_OK;
+}
+
+nsresult nsParseMailMessageState::ParseFolderLine(const char* line,
+ uint32_t lineLength) {
+ nsresult rv;
+
+ if (m_state == nsIMsgParseMailMsgState::ParseHeadersState) {
+ if (EMPTY_MESSAGE_LINE(line)) {
+ /* End of headers. Now parse them. */
+ rv = ParseHeaders();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "error parsing headers parsing mailbox");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = FinalizeHeaders();
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "error finalizing headers parsing mailbox");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_state = nsIMsgParseMailMsgState::ParseBodyState;
+ } else {
+ /* Otherwise, this line belongs to a header. So append it to the
+ header data, and stay in MBOX `MIME_PARSE_HEADERS' state.
+ */
+ m_headers.AppendBuffer(line, lineLength);
+ }
+ } else if (m_state == nsIMsgParseMailMsgState::ParseBodyState) {
+ m_body_lines++;
+ // See comment in msgCore.h for why we use `IS_MSG_LINEBREAK` rather than
+ // just comparing `line` to `MSG_LINEBREAK`.
+ m_lastLineBlank = IS_MSG_LINEBREAK(line);
+ }
+
+ m_position += lineLength;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::SetMailDB(nsIMsgDatabase* mailDB) {
+ m_mailDB = mailDB;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::SetBackupMailDB(
+ nsIMsgDatabase* aBackupMailDB) {
+ m_backupMailDB = aBackupMailDB;
+ if (m_backupMailDB) m_backupMailDB->AddListener(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::SetNewKey(nsMsgKey aKey) {
+ m_new_key = aKey;
+ return NS_OK;
+}
+
+/* #define STRICT_ENVELOPE */
+
+bool nsParseMailMessageState::IsEnvelopeLine(const char* buf,
+ int32_t buf_size) {
+#ifdef STRICT_ENVELOPE
+ /* The required format is
+ From jwz Fri Jul 1 09:13:09 1994
+ But we should also allow at least:
+ From jwz Fri, Jul 01 09:13:09 1994
+ From jwz Fri Jul 1 09:13:09 1994 PST
+ From jwz Fri Jul 1 09:13:09 1994 (+0700)
+
+ We can't easily call XP_ParseTimeString() because the string is not
+ null terminated (ok, we could copy it after a quick check...) but
+ XP_ParseTimeString() may be too lenient for our purposes.
+
+ DANGER!! The released version of 2.0b1 was (on some systems,
+ some Unix, some NT, possibly others) writing out envelope lines
+ like "From - 10/13/95 11:22:33" which STRICT_ENVELOPE will reject!
+ */
+ const char *date, *end;
+
+ if (buf_size < 29) return false;
+ if (*buf != 'F') return false;
+ if (strncmp(buf, "From ", 5)) return false;
+
+ end = buf + buf_size;
+ date = buf + 5;
+
+ /* Skip horizontal whitespace between "From " and user name. */
+ while ((*date == ' ' || *date == '\t') && date < end) date++;
+
+ /* If at the end, it doesn't match. */
+ if (IS_SPACE(*date) || date == end) return false;
+
+ /* Skip over user name. */
+ while (!IS_SPACE(*date) && date < end) date++;
+
+ /* Skip horizontal whitespace between user name and date. */
+ while ((*date == ' ' || *date == '\t') && date < end) date++;
+
+ /* Don't want this to be localized. */
+# define TMP_ISALPHA(x) \
+ (((x) >= 'A' && (x) <= 'Z') || ((x) >= 'a' && (x) <= 'z'))
+
+ /* take off day-of-the-week. */
+ if (date >= end - 3) return false;
+ if (!TMP_ISALPHA(date[0]) || !TMP_ISALPHA(date[1]) || !TMP_ISALPHA(date[2]))
+ return false;
+ date += 3;
+ /* Skip horizontal whitespace (and commas) between dotw and month. */
+ if (*date != ' ' && *date != '\t' && *date != ',') return false;
+ while ((*date == ' ' || *date == '\t' || *date == ',') && date < end) date++;
+
+ /* take off month. */
+ if (date >= end - 3) return false;
+ if (!TMP_ISALPHA(date[0]) || !TMP_ISALPHA(date[1]) || !TMP_ISALPHA(date[2]))
+ return false;
+ date += 3;
+ /* Skip horizontal whitespace between month and dotm. */
+ if (date == end || (*date != ' ' && *date != '\t')) return false;
+ while ((*date == ' ' || *date == '\t') && date < end) date++;
+
+ /* Skip over digits and whitespace. */
+ while (((*date >= '0' && *date <= '9') || *date == ' ' || *date == '\t') &&
+ date < end)
+ date++;
+ /* Next character should be a colon. */
+ if (date >= end || *date != ':') return false;
+
+ /* Ok, that ought to be enough... */
+
+# undef TMP_ISALPHA
+
+#else /* !STRICT_ENVELOPE */
+
+ if (buf_size < 5) return false;
+ if (*buf != 'F') return false;
+ if (strncmp(buf, "From ", 5)) return false;
+
+#endif /* !STRICT_ENVELOPE */
+
+ return true;
+}
+
+// We've found the start of the next message, so finish this one off.
+NS_IMETHODIMP nsParseMailMessageState::FinishHeader() {
+ if (m_newMsgHdr) {
+ if (m_lastLineBlank) m_body_lines--;
+ m_newMsgHdr->SetMessageSize(m_position - m_envelope_pos - m_lastLineBlank);
+ m_newMsgHdr->SetLineCount(m_body_lines);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsParseMailMessageState::GetAllHeaders(char** pHeaders,
+ int32_t* pHeadersSize) {
+ if (!pHeaders || !pHeadersSize) return NS_ERROR_NULL_POINTER;
+ *pHeaders = m_headers.GetBuffer();
+ *pHeadersSize = m_headers.GetBufferPos();
+ return NS_OK;
+}
+
+// generate headers as a string, with CRLF between the headers
+NS_IMETHODIMP nsParseMailMessageState::GetHeaders(char** pHeaders) {
+ NS_ENSURE_ARG_POINTER(pHeaders);
+ nsCString crlfHeaders;
+ char* curHeader = m_headers.GetBuffer();
+ for (uint32_t headerPos = 0; headerPos < m_headers.GetBufferPos();) {
+ crlfHeaders.Append(curHeader);
+ crlfHeaders.Append(CRLF);
+ int32_t headerLen = strlen(curHeader);
+ curHeader += headerLen + 1;
+ headerPos += headerLen + 1;
+ }
+ *pHeaders = ToNewCString(crlfHeaders);
+ return NS_OK;
+}
+
+struct message_header* nsParseMailMessageState::GetNextHeaderInAggregate(
+ nsTArray<struct message_header*>& list) {
+ // When parsing a message with multiple To or CC header lines, we're storing
+ // each line in a list, where the list represents the "aggregate" total of all
+ // the header. Here we get a new line for the list
+
+ struct message_header* header =
+ (struct message_header*)PR_Calloc(1, sizeof(struct message_header));
+ list.AppendElement(header);
+ return header;
+}
+
+void nsParseMailMessageState::GetAggregateHeader(
+ nsTArray<struct message_header*>& list, struct message_header* outHeader) {
+ // When parsing a message with multiple To or CC header lines, we're storing
+ // each line in a list, where the list represents the "aggregate" total of all
+ // the header. Here we combine all the lines together, as though they were
+ // really all found on the same line
+
+ struct message_header* header = nullptr;
+ int length = 0;
+ size_t i;
+
+ // Count up the bytes required to allocate the aggregated header
+ for (i = 0; i < list.Length(); i++) {
+ header = list.ElementAt(i);
+ length += (header->length + 1); //+ for ","
+ }
+
+ if (length > 0) {
+ char* value = (char*)PR_CALLOC(length + 1); //+1 for null term
+ if (value) {
+ // Catenate all the To lines together, separated by commas
+ value[0] = '\0';
+ size_t size = list.Length();
+ for (i = 0; i < size; i++) {
+ header = list.ElementAt(i);
+ PL_strncat(value, header->value, header->length);
+ if (i + 1 < size) PL_strcat(value, ",");
+ }
+ outHeader->length = length;
+ outHeader->value = value;
+ }
+ } else {
+ outHeader->length = 0;
+ outHeader->value = nullptr;
+ }
+}
+
+void nsParseMailMessageState::ClearAggregateHeader(
+ nsTArray<struct message_header*>& list) {
+ // Reset the aggregate headers. Free only the message_header struct since
+ // we don't own the value pointer
+
+ for (size_t i = 0; i < list.Length(); i++) PR_Free(list.ElementAt(i));
+ list.Clear();
+}
+
+// We've found a new envelope to parse.
+nsresult nsParseMailMessageState::StartNewEnvelope(const char* line,
+ uint32_t lineLength) {
+ m_envelope_pos = m_position;
+ m_state = nsIMsgParseMailMsgState::ParseHeadersState;
+ m_position += lineLength;
+ m_headerstartpos = m_position;
+ return ParseEnvelope(line, lineLength);
+}
+
+/* largely lifted from mimehtml.c, which does similar parsing, sigh...
+ */
+nsresult nsParseMailMessageState::ParseHeaders() {
+ char* buf = m_headers.GetBuffer();
+ uint32_t buf_length = m_headers.GetBufferPos();
+ if (buf_length == 0) {
+ // No header of an expected type is present. Consider this a successful
+ // parse so email still shows on summary and can be accessed and deleted.
+ return NS_OK;
+ }
+ char* buf_end = buf + buf_length;
+ if (!(buf_length > 1 &&
+ (buf[buf_length - 1] == '\r' || buf[buf_length - 1] == '\n'))) {
+ NS_WARNING("Header text should always end in a newline");
+ return NS_ERROR_UNEXPECTED;
+ }
+ while (buf < buf_end) {
+ char* colon = PL_strnchr(buf, ':', buf_end - buf);
+ char* value = 0;
+ struct message_header* header = 0;
+ struct message_header receivedBy;
+
+ if (!colon) break;
+
+ nsDependentCSubstring headerStr(buf, colon);
+ ToLowerCase(headerStr);
+
+ // Obtain firstChar in headerStr. But if headerStr is empty, just set it to
+ // the colon. This is needed because First() asserts on an empty string.
+ char firstChar = !headerStr.IsEmpty() ? headerStr.First() : *colon;
+
+ // See RFC 5322 section 3.6 for min-max number for given header.
+ // If multiple headers exist we need to make sure to use the first one.
+
+ switch (firstChar) {
+ case 'b':
+ if (headerStr.EqualsLiteral("bcc") && !m_bccList.length)
+ header = &m_bccList;
+ break;
+ case 'c':
+ if (headerStr.EqualsLiteral("cc")) // XXX: RFC 5322 says it's 0 or 1.
+ header = GetNextHeaderInAggregate(m_ccList);
+ else if (headerStr.EqualsLiteral("content-type"))
+ header = &m_content_type;
+ break;
+ case 'd':
+ if (headerStr.EqualsLiteral("date") && !m_date.length)
+ header = &m_date;
+ else if (headerStr.EqualsLiteral("disposition-notification-to"))
+ header = &m_mdn_dnt;
+ else if (headerStr.EqualsLiteral("delivery-date"))
+ header = &m_delivery_date;
+ break;
+ case 'f':
+ if (headerStr.EqualsLiteral("from") && !m_from.length) {
+ header = &m_from;
+ }
+ break;
+ case 'i':
+ if (headerStr.EqualsLiteral("in-reply-to") && !m_in_reply_to.length)
+ header = &m_in_reply_to;
+ break;
+ case 'm':
+ if (headerStr.EqualsLiteral("message-id") && !m_message_id.length)
+ header = &m_message_id;
+ break;
+ case 'n':
+ if (headerStr.EqualsLiteral("newsgroups")) header = &m_newsgroups;
+ break;
+ case 'o':
+ if (headerStr.EqualsLiteral("original-recipient"))
+ header = &m_mdn_original_recipient;
+ break;
+ case 'p':
+ // we could very well care what the priority header was when we
+ // remember its value. If so, need to remember it here. Also,
+ // different priority headers can appear in the same message,
+ // but we only remember the last one that we see. Applies also to
+ // x-priority checked below.
+ if (headerStr.EqualsLiteral("priority")) header = &m_priority;
+ break;
+ case 'r':
+ if (headerStr.EqualsLiteral("references") && !m_references.length)
+ header = &m_references;
+ else if (headerStr.EqualsLiteral("return-path"))
+ header = &m_return_path;
+ // treat conventional Return-Receipt-To as MDN
+ // Disposition-Notification-To
+ else if (headerStr.EqualsLiteral("return-receipt-to"))
+ header = &m_mdn_dnt;
+ else if (headerStr.EqualsLiteral("reply-to") && !m_replyTo.length)
+ header = &m_replyTo;
+ else if (headerStr.EqualsLiteral("received")) {
+ header = &receivedBy;
+ header->length = 0;
+ }
+ break;
+ case 's':
+ if (headerStr.EqualsLiteral("subject") && !m_subject.length)
+ header = &m_subject;
+ else if (headerStr.EqualsLiteral("sender") && !m_sender.length)
+ header = &m_sender;
+ else if (headerStr.EqualsLiteral("status"))
+ header = &m_status;
+ break;
+ case 't':
+ if (headerStr.EqualsLiteral("to")) // XXX: RFC 5322 says it's 0 or 1.
+ header = GetNextHeaderInAggregate(m_toList);
+ break;
+ case 'x':
+ if (headerStr.EqualsIgnoreCase(X_MOZILLA_STATUS2) &&
+ !m_mozstatus2.length)
+ header = &m_mozstatus2;
+ else if (headerStr.EqualsIgnoreCase(X_MOZILLA_STATUS) &&
+ !m_mozstatus.length)
+ header = &m_mozstatus;
+ else if (headerStr.EqualsIgnoreCase(HEADER_X_MOZILLA_ACCOUNT_KEY) &&
+ !m_account_key.length)
+ header = &m_account_key;
+ else if (headerStr.EqualsLiteral("x-priority")) // See case 'p' above.
+ header = &m_priority;
+ else if (headerStr.EqualsIgnoreCase(HEADER_X_MOZILLA_KEYWORDS) &&
+ !m_keywords.length)
+ header = &m_keywords;
+ break;
+ }
+
+ if (!header && m_customDBHeaders.Length()) {
+ size_t customHeaderIndex = m_customDBHeaders.IndexOf(headerStr);
+ if (customHeaderIndex != m_customDBHeaders.NoIndex)
+ header = &m_customDBHeaderValues[customHeaderIndex];
+ }
+
+ buf = colon + 1;
+ // We will be shuffling downwards, so this is our insertion point.
+ char* bufWrite = buf;
+
+ SEARCH_NEWLINE:
+ // move past any non terminating characters, rewriting them if folding white
+ // space exists
+ while (buf < buf_end && *buf != '\r' && *buf != '\n') {
+ if (buf != bufWrite) *bufWrite = *buf;
+ buf++;
+ bufWrite++;
+ }
+
+ // Look for folding, so CRLF, CR or LF followed by space or tab.
+ if ((buf + 2 < buf_end && (buf[0] == '\r' && buf[1] == '\n') &&
+ (buf[2] == ' ' || buf[2] == '\t')) ||
+ (buf + 1 < buf_end && (buf[0] == '\r' || buf[0] == '\n') &&
+ (buf[1] == ' ' || buf[1] == '\t'))) {
+ // Remove trailing spaces at the "write position" and add a single
+ // folding space.
+ while (*(bufWrite - 1) == ' ' || *(bufWrite - 1) == '\t') bufWrite--;
+ *(bufWrite++) = ' ';
+
+ // Skip CRLF, CR+space or LF+space ...
+ buf += 2;
+
+ // ... and skip leading spaces in that line.
+ while (buf < buf_end && (*buf == ' ' || *buf == '\t')) buf++;
+
+ // If we get here, the message headers ended in an empty line, like:
+ // To: blah blah blah<CR><LF> <CR><LF>[end of buffer]. The code below
+ // requires buf to land on a newline to properly null-terminate the
+ // string, so back up a tad so that it is pointing to one.
+ if (buf == buf_end) {
+ --buf;
+ MOZ_ASSERT(*buf == '\n' || *buf == '\r',
+ "Header text should always end in a newline.");
+ }
+ goto SEARCH_NEWLINE;
+ }
+
+ if (header) {
+ value = colon + 1;
+ // eliminate trailing blanks after the colon
+ while (value < bufWrite && (*value == ' ' || *value == '\t')) value++;
+
+ header->value = value;
+ header->length = bufWrite - value;
+ if (header->length < 0) header->length = 0;
+ }
+ if (*buf == '\r' || *buf == '\n') {
+ char* last = bufWrite;
+ char* saveBuf = buf;
+ if (*buf == '\r' && buf + 1 < buf_end && buf[1] == '\n') buf++;
+ buf++;
+ // null terminate the left-over slop so we don't confuse msg filters.
+ *saveBuf = 0;
+ *last = 0; /* short-circuit const, and null-terminate header. */
+ }
+
+ if (header) {
+ /* More const short-circuitry... */
+ /* strip trailing whitespace */
+ while (header->length > 0 && IS_SPACE(header->value[header->length - 1]))
+ ((char*)header->value)[--header->length] = 0;
+ if (header == &receivedBy) {
+ if (m_receivedTime == 0) {
+ // parse Received: header for date.
+ // We trust the first header as that is closest to recipient,
+ // and less likely to be spoofed.
+ nsAutoCString receivedHdr(header->value, header->length);
+ int32_t lastSemicolon = receivedHdr.RFindChar(';');
+ if (lastSemicolon != -1) {
+ nsAutoCString receivedDate;
+ receivedDate = Substring(receivedHdr, lastSemicolon + 1);
+ receivedDate.Trim(" \t\b\r\n");
+ PRTime resultTime;
+ if (PR_ParseTimeString(receivedDate.get(), false, &resultTime) ==
+ PR_SUCCESS)
+ m_receivedTime = resultTime;
+ else
+ NS_WARNING("PR_ParseTimeString failed in ParseHeaders().");
+ }
+ }
+ // Someone might want the received header saved.
+ if (m_customDBHeaders.Length()) {
+ if (m_customDBHeaders.Contains("received"_ns)) {
+ if (!m_receivedValue.IsEmpty()) m_receivedValue.Append(' ');
+ m_receivedValue.Append(header->value, header->length);
+ }
+ }
+ }
+
+ MOZ_ASSERT(header->value[header->length] == 0,
+ "Non-null-terminated strings cause very, very bad problems");
+ }
+ }
+ return NS_OK;
+}
+
+// Try and glean a sender and/or timestamp from the "From " line, to use
+// as last-ditch fallbacks if the message is missing "From"/"Sender" or
+// "Date" headers.
+nsresult nsParseMailMessageState::ParseEnvelope(const char* line,
+ uint32_t line_size) {
+ const char* end;
+ char* s;
+
+ m_envelope.AppendBuffer(line, line_size);
+ end = m_envelope.GetBuffer() + line_size;
+ s = m_envelope.GetBuffer() + 5;
+
+ while (s < end && IS_SPACE(*s)) s++;
+ m_envelope_from.value = s;
+ while (s < end && !IS_SPACE(*s)) s++;
+ m_envelope_from.length = s - m_envelope_from.value;
+
+ while (s < end && IS_SPACE(*s)) s++;
+ m_envelope_date.value = s;
+ m_envelope_date.length = (uint16_t)(line_size - (s - m_envelope.GetBuffer()));
+
+ while (m_envelope_date.length > 0 &&
+ IS_SPACE(m_envelope_date.value[m_envelope_date.length - 1]))
+ m_envelope_date.length--;
+
+ /* #### short-circuit const */
+ ((char*)m_envelope_from.value)[m_envelope_from.length] = 0;
+ ((char*)m_envelope_date.value)[m_envelope_date.length] = 0;
+
+ return NS_OK;
+}
+
+nsresult nsParseMailMessageState::InternSubject(struct message_header* header) {
+ if (!header || header->length == 0) {
+ m_newMsgHdr->SetSubject(""_ns);
+ return NS_OK;
+ }
+
+ nsDependentCString key(header->value);
+
+ uint32_t flags;
+ (void)m_newMsgHdr->GetFlags(&flags);
+ /* strip "Re: " */
+ /**
+ We trust the X-Mozilla-Status line to be the smartest in almost
+ all things. One exception, however, is the HAS_RE flag. Since
+ we just parsed the subject header anyway, we expect that parsing
+ to be smartest. (After all, what if someone just went in and
+ edited the subject line by hand?)
+ */
+ nsCString modifiedSubject;
+ bool strippedRE = NS_MsgStripRE(key, modifiedSubject);
+ if (strippedRE)
+ flags |= nsMsgMessageFlags::HasRe;
+ else
+ flags &= ~nsMsgMessageFlags::HasRe;
+ m_newMsgHdr->SetFlags(flags); // this *does not* update the mozilla-status
+ // header in the local folder
+
+ m_newMsgHdr->SetSubject(strippedRE ? modifiedSubject : key);
+
+ return NS_OK;
+}
+
+// we've reached the end of the envelope, and need to turn all our accumulated
+// message_headers into a single nsIMsgDBHdr to store in a database.
+nsresult nsParseMailMessageState::FinalizeHeaders() {
+ nsresult rv;
+ struct message_header* sender;
+ struct message_header* recipient;
+ struct message_header* subject;
+ struct message_header* id;
+ struct message_header* inReplyTo;
+ struct message_header* replyTo;
+ struct message_header* references;
+ struct message_header* date;
+ struct message_header* deliveryDate;
+ struct message_header* statush;
+ struct message_header* mozstatus;
+ struct message_header* mozstatus2;
+ struct message_header* priority;
+ struct message_header* keywords;
+ struct message_header* account_key;
+ struct message_header* ccList;
+ struct message_header* bccList;
+ struct message_header* mdn_dnt;
+ struct message_header md5_header;
+ struct message_header* content_type;
+ char md5_data[50];
+
+ uint32_t flags = 0;
+ nsMsgPriorityValue priorityFlags = nsMsgPriority::notSet;
+
+ if (!m_mailDB) // if we don't have a valid db, skip the header.
+ return NS_OK;
+
+ struct message_header to;
+ GetAggregateHeader(m_toList, &to);
+ struct message_header cc;
+ GetAggregateHeader(m_ccList, &cc);
+ // we don't aggregate bcc, as we only generate it locally,
+ // and we don't use multiple lines
+
+ // clang-format off
+ sender = (m_from.length ? &m_from :
+ m_sender.length ? &m_sender :
+ m_envelope_from.length ? &m_envelope_from : 0);
+ recipient = (to.length ? &to :
+ cc.length ? &cc :
+ m_newsgroups.length ? &m_newsgroups : 0);
+ ccList = (cc.length ? &cc : 0);
+ bccList = (m_bccList.length ? &m_bccList : 0);
+ subject = (m_subject.length ? &m_subject : 0);
+ id = (m_message_id.length ? &m_message_id : 0);
+ references = (m_references.length ? &m_references : 0);
+ statush = (m_status.length ? &m_status : 0);
+ mozstatus = (m_mozstatus.length ? &m_mozstatus : 0);
+ mozstatus2 = (m_mozstatus2.length ? &m_mozstatus2 : 0);
+ date = (m_date.length ? &m_date :
+ m_envelope_date.length ? &m_envelope_date : 0);
+ deliveryDate = (m_delivery_date.length ? &m_delivery_date : 0);
+ priority = (m_priority.length ? &m_priority : 0);
+ keywords = (m_keywords.length ? &m_keywords : 0);
+ mdn_dnt = (m_mdn_dnt.length ? &m_mdn_dnt : 0);
+ inReplyTo = (m_in_reply_to.length ? &m_in_reply_to : 0);
+ replyTo = (m_replyTo.length ? &m_replyTo : 0);
+ content_type = (m_content_type.length ? &m_content_type : 0);
+ account_key = (m_account_key.length ? &m_account_key : 0);
+ // clang-format on
+
+ if (mozstatus) {
+ if (mozstatus->length == 4) {
+ NS_ASSERTION(MsgIsHex(mozstatus->value, 4),
+ "Expected 4 hex digits for flags.");
+ flags = MsgUnhex(mozstatus->value, 4);
+ // strip off and remember priority bits.
+ flags &= ~nsMsgMessageFlags::RuntimeOnly;
+ priorityFlags =
+ (nsMsgPriorityValue)((flags & nsMsgMessageFlags::Priorities) >> 13);
+ flags &= ~nsMsgMessageFlags::Priorities;
+ }
+ }
+
+ if (mozstatus2) {
+ uint32_t flags2 = 0;
+ sscanf(mozstatus2->value, " %x ", &flags2);
+ flags |= flags2;
+ }
+
+ if (!(flags & nsMsgMessageFlags::Expunged)) // message was deleted, don't
+ // bother creating a hdr.
+ {
+ // We'll need the message id first to recover data from the backup database
+ nsAutoCString rawMsgId;
+ /* Take off <> around message ID. */
+ if (id) {
+ if (id->length > 0 && id->value[0] == '<') {
+ id->length--;
+ id->value++;
+ }
+
+ NS_WARNING_ASSERTION(id->length > 0,
+ "id->length failure in FinalizeHeaders().");
+
+ if (id->length > 0 && id->value[id->length - 1] == '>')
+ /* generate a new null-terminated string without the final > */
+ rawMsgId.Assign(id->value, id->length - 1);
+ else
+ rawMsgId.Assign(id->value);
+ }
+
+ /*
+ * Try to copy the data from the backup database, referencing the MessageID
+ * If that fails, just create a new header
+ */
+ nsCOMPtr<nsIMsgDBHdr> oldHeader;
+ nsresult ret = NS_OK;
+
+ if (m_backupMailDB && !rawMsgId.IsEmpty())
+ ret = m_backupMailDB->GetMsgHdrForMessageID(rawMsgId.get(),
+ getter_AddRefs(oldHeader));
+
+ // m_new_key is set in nsImapMailFolder::ParseAdoptedHeaderLine to be
+ // the UID of the message, so that the key can get created as UID. That of
+ // course is extremely confusing, and we really need to clean that up. We
+ // really should not conflate the meaning of envelope position, key, and
+ // UID.
+ if (NS_SUCCEEDED(ret) && oldHeader)
+ ret = m_mailDB->CopyHdrFromExistingHdr(m_new_key, oldHeader, false,
+ getter_AddRefs(m_newMsgHdr));
+ else if (!m_newMsgHdr) {
+ // Should assert that this is not a local message
+ ret = m_mailDB->CreateNewHdr(m_new_key, getter_AddRefs(m_newMsgHdr));
+ }
+
+ if (NS_SUCCEEDED(ret) && m_newMsgHdr) {
+ uint32_t origFlags;
+ (void)m_newMsgHdr->GetFlags(&origFlags);
+ if (origFlags & nsMsgMessageFlags::HasRe)
+ flags |= nsMsgMessageFlags::HasRe;
+ else
+ flags &= ~nsMsgMessageFlags::HasRe;
+
+ flags &=
+ ~nsMsgMessageFlags::Offline; // don't keep nsMsgMessageFlags::Offline
+ // for local msgs
+ if (mdn_dnt && !(origFlags & nsMsgMessageFlags::Read) &&
+ !(origFlags & nsMsgMessageFlags::MDNReportSent) &&
+ !(flags & nsMsgMessageFlags::MDNReportSent))
+ flags |= nsMsgMessageFlags::MDNReportNeeded;
+
+ m_newMsgHdr->SetFlags(flags);
+ if (priorityFlags != nsMsgPriority::notSet)
+ m_newMsgHdr->SetPriority(priorityFlags);
+
+ // if we have a reply to header, and it's different from the from: header,
+ // set the "replyTo" attribute on the msg hdr.
+ if (replyTo && (!sender || replyTo->length != sender->length ||
+ strncmp(replyTo->value, sender->value, sender->length)))
+ m_newMsgHdr->SetStringProperty("replyTo",
+ nsDependentCString(replyTo->value));
+ if (sender) m_newMsgHdr->SetAuthor(sender->value);
+ if (recipient == &m_newsgroups) {
+ /* In the case where the recipient is a newsgroup, truncate the string
+ at the first comma. This is used only for presenting the thread
+ list, and newsgroup lines tend to be long and non-shared, and tend to
+ bloat the string table. So, by only showing the first newsgroup, we
+ can reduce memory and file usage at the expense of only showing the
+ one group in the summary list, and only being able to sort on the
+ first group rather than the whole list. It's worth it. */
+ char* ch;
+ ch = PL_strchr(recipient->value, ',');
+ if (ch) {
+ /* generate a new string that terminates before the , */
+ nsAutoCString firstGroup;
+ firstGroup.Assign(recipient->value, ch - recipient->value);
+ m_newMsgHdr->SetRecipients(firstGroup.get());
+ }
+ m_newMsgHdr->SetRecipients(recipient->value);
+ } else if (recipient) {
+ m_newMsgHdr->SetRecipients(recipient->value);
+ }
+ if (ccList) {
+ m_newMsgHdr->SetCcList(ccList->value);
+ }
+
+ if (bccList) {
+ m_newMsgHdr->SetBccList(bccList->value);
+ }
+
+ rv = InternSubject(subject);
+ if (NS_SUCCEEDED(rv)) {
+ if (!id) {
+ // what to do about this? we used to do a hash of all the headers...
+ nsAutoCString hash;
+ const char* md5_b64 = "dummy.message.id";
+ nsresult rv;
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ if (NS_SUCCEEDED(hasher->Init(nsICryptoHash::MD5)) &&
+ NS_SUCCEEDED(
+ hasher->Update((const uint8_t*)m_headers.GetBuffer(),
+ m_headers.GetBufferPos())) &&
+ NS_SUCCEEDED(hasher->Finish(true, hash)))
+ md5_b64 = hash.get();
+ }
+ PR_snprintf(md5_data, sizeof(md5_data), "<md5:%s>", md5_b64);
+ md5_header.value = md5_data;
+ md5_header.length = strlen(md5_data);
+ id = &md5_header;
+ }
+
+ if (!rawMsgId.IsEmpty())
+ m_newMsgHdr->SetMessageId(rawMsgId.get());
+ else
+ m_newMsgHdr->SetMessageId(id->value);
+ m_mailDB->UpdatePendingAttributes(m_newMsgHdr);
+
+ if (!mozstatus && statush) {
+ /* Parse a little bit of the Berkeley Mail status header. */
+ for (const char* s = statush->value; *s; s++) {
+ uint32_t msgFlags = 0;
+ (void)m_newMsgHdr->GetFlags(&msgFlags);
+ switch (*s) {
+ case 'R':
+ case 'r':
+ m_newMsgHdr->SetFlags(msgFlags | nsMsgMessageFlags::Read);
+ break;
+ case 'D':
+ case 'd':
+ /* msg->flags |= nsMsgMessageFlags::Expunged; ### Is this
+ * reasonable? */
+ break;
+ case 'N':
+ case 'n':
+ case 'U':
+ case 'u':
+ m_newMsgHdr->SetFlags(msgFlags & ~nsMsgMessageFlags::Read);
+ break;
+ default: // Should check for corrupt file.
+ NS_ERROR("Corrupt file. Should not happen.");
+ break;
+ }
+ }
+ }
+
+ if (account_key != nullptr)
+ m_newMsgHdr->SetAccountKey(account_key->value);
+ // use in-reply-to header as references, if there's no references header
+ if (references != nullptr) {
+ m_newMsgHdr->SetReferences(nsDependentCString(references->value));
+ } else if (inReplyTo != nullptr)
+ m_newMsgHdr->SetReferences(nsDependentCString(inReplyTo->value));
+
+ // 'Received' should be as reliable an indicator of the receipt
+ // date+time as possible, whilst always giving something *from
+ // the message*. It won't use PR_Now() under any circumstance.
+ // Therefore, the fall-thru order for 'Received' is:
+ // Received: -> Delivery-date: -> date
+ // 'Date' uses:
+ // date -> 'Received' -> PR_Now()
+ //
+ // date is:
+ // Date: -> m_envelope_date
+
+ uint32_t rcvTimeSecs = 0;
+ PRTime datePRTime = 0;
+ if (date) {
+ // Date:
+ if (PR_ParseTimeString(date->value, false, &datePRTime) ==
+ PR_SUCCESS) {
+ // Convert to seconds as default value for 'Received'.
+ PRTime2Seconds(datePRTime, &rcvTimeSecs);
+ } else {
+ NS_WARNING(
+ "PR_ParseTimeString of date failed in FinalizeHeader().");
+ }
+ }
+ if (m_receivedTime) {
+ // Upgrade 'Received' to Received: ?
+ PRTime2Seconds(m_receivedTime, &rcvTimeSecs);
+ if (datePRTime == 0) datePRTime = m_receivedTime;
+ } else if (deliveryDate) {
+ // Upgrade 'Received' to Delivery-date: ?
+ PRTime resultTime;
+ if (PR_ParseTimeString(deliveryDate->value, false, &resultTime) ==
+ PR_SUCCESS) {
+ PRTime2Seconds(resultTime, &rcvTimeSecs);
+ if (datePRTime == 0) datePRTime = resultTime;
+ } else {
+ // TODO/FIXME: We need to figure out what to do in this case!
+ NS_WARNING(
+ "PR_ParseTimeString of delivery date failed in "
+ "FinalizeHeader().");
+ }
+ }
+ m_newMsgHdr->SetUint32Property("dateReceived", rcvTimeSecs);
+
+ if (datePRTime == 0) {
+ // If there was some problem parsing the Date header *AND* we
+ // couldn't get a valid envelope date *AND* we couldn't get a valid
+ // Received: header date, use now as the time.
+ // This doesn't affect local (POP3) messages, because we use the
+ // envelope date if there's no Date: header, but it will affect IMAP
+ // msgs w/o a Date: header or Received: headers.
+ datePRTime = PR_Now();
+ }
+ m_newMsgHdr->SetDate(datePRTime);
+
+ if (priority) {
+ nsMsgPriorityValue priorityVal = nsMsgPriority::Default;
+
+ // We can ignore |NS_MsgGetPriorityFromString()| return value,
+ // since we set a default value for |priorityVal|.
+ NS_MsgGetPriorityFromString(priority->value, priorityVal);
+ m_newMsgHdr->SetPriority(priorityVal);
+ } else if (priorityFlags == nsMsgPriority::notSet)
+ m_newMsgHdr->SetPriority(nsMsgPriority::none);
+ if (keywords) {
+ // When there are many keywords, some may not have been written
+ // to the message file, so add extra keywords from the backup
+ nsAutoCString oldKeywords;
+ m_newMsgHdr->GetStringProperty("keywords", oldKeywords);
+ nsTArray<nsCString> newKeywordArray, oldKeywordArray;
+ ParseString(
+ Substring(keywords->value, keywords->value + keywords->length),
+ ' ', newKeywordArray);
+ ParseString(oldKeywords, ' ', oldKeywordArray);
+ for (uint32_t i = 0; i < oldKeywordArray.Length(); i++)
+ if (!newKeywordArray.Contains(oldKeywordArray[i]))
+ newKeywordArray.AppendElement(oldKeywordArray[i]);
+ nsAutoCString newKeywords;
+ for (uint32_t i = 0; i < newKeywordArray.Length(); i++) {
+ if (i) newKeywords.Append(' ');
+ newKeywords.Append(newKeywordArray[i]);
+ }
+ m_newMsgHdr->SetStringProperty("keywords", newKeywords);
+ }
+ for (uint32_t i = 0; i < m_customDBHeaders.Length(); i++) {
+ if (m_customDBHeaderValues[i].length)
+ m_newMsgHdr->SetStringProperty(
+ m_customDBHeaders[i].get(),
+ nsDependentCString(m_customDBHeaderValues[i].value));
+ // The received header is accumulated separately
+ if (m_customDBHeaders[i].EqualsLiteral("received") &&
+ !m_receivedValue.IsEmpty())
+ m_newMsgHdr->SetStringProperty("received", m_receivedValue);
+ }
+ if (content_type) {
+ char* substring = PL_strstr(content_type->value, "charset");
+ if (substring) {
+ char* charset = PL_strchr(substring, '=');
+ if (charset) {
+ charset++;
+ /* strip leading whitespace and double-quote */
+ while (*charset && (IS_SPACE(*charset) || '\"' == *charset))
+ charset++;
+ /* strip trailing whitespace and double-quote */
+ char* end = charset;
+ while (*end && !IS_SPACE(*end) && '\"' != *end && ';' != *end)
+ end++;
+ if (*charset) {
+ if (*end != '\0') {
+ // if we're not at the very end of the line, we need
+ // to generate a new string without the trailing crud
+ nsAutoCString rawCharSet;
+ rawCharSet.Assign(charset, end - charset);
+ m_newMsgHdr->SetCharset(rawCharSet.get());
+ } else {
+ m_newMsgHdr->SetCharset(charset);
+ }
+ }
+ }
+ }
+ substring = PL_strcasestr(content_type->value, "multipart/mixed");
+ if (substring) {
+ uint32_t newFlags;
+ m_newMsgHdr->OrFlags(nsMsgMessageFlags::Attachment, &newFlags);
+ }
+ }
+ }
+ } else {
+ NS_ASSERTION(false, "error creating message header");
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+ } else
+ rv = NS_OK;
+
+ // ### why is this stuff const?
+ char* tmp = (char*)to.value;
+ PR_Free(tmp);
+ tmp = (char*)cc.value;
+ PR_Free(tmp);
+
+ return rv;
+}
+
+nsParseNewMailState::nsParseNewMailState() : m_disableFilters(false) {
+ m_numNotNewMessages = 0;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsParseNewMailState, nsMsgMailboxParser,
+ nsIMsgFilterHitNotify)
+
+nsresult nsParseNewMailState::Init(nsIMsgFolder* serverFolder,
+ nsIMsgFolder* downloadFolder,
+ nsIMsgWindow* aMsgWindow, nsIMsgDBHdr* aHdr,
+ nsIOutputStream* aOutputStream) {
+ NS_ENSURE_ARG_POINTER(serverFolder);
+ nsresult rv;
+ Clear();
+ m_rootFolder = serverFolder;
+ m_msgWindow = aMsgWindow;
+ m_downloadFolder = downloadFolder;
+
+ m_newMsgHdr = aHdr;
+ m_outputStream = aOutputStream;
+ // the new mail parser isn't going to get the stream input, it seems, so we
+ // can't use the OnStartRequest mechanism the mailbox parser uses. So, let's
+ // open the db right now.
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ if (msgDBService && !m_mailDB)
+ rv = msgDBService->OpenFolderDB(downloadFolder, false,
+ getter_AddRefs(m_mailDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = serverFolder->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv)) {
+ nsString serverName;
+ server->GetPrettyName(serverName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Detected new local messages on account '%s'",
+ NS_ConvertUTF16toUTF8(serverName).get()));
+ rv = server->GetFilterList(aMsgWindow, getter_AddRefs(m_filterList));
+
+ if (m_filterList) rv = server->ConfigureTemporaryFilters(m_filterList);
+ // check if this server defers to another server, in which case
+ // we'll use that server's filters as well.
+ nsCOMPtr<nsIMsgFolder> deferredToRootFolder;
+ server->GetRootMsgFolder(getter_AddRefs(deferredToRootFolder));
+ if (serverFolder != deferredToRootFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> deferredToServer;
+ deferredToRootFolder->GetServer(getter_AddRefs(deferredToServer));
+ if (deferredToServer)
+ deferredToServer->GetFilterList(
+ aMsgWindow, getter_AddRefs(m_deferredToServerFilterList));
+ }
+ }
+ m_disableFilters = false;
+ return NS_OK;
+}
+
+nsParseNewMailState::~nsParseNewMailState() {
+ if (m_mailDB) m_mailDB->Close(true);
+ if (m_backupMailDB) m_backupMailDB->ForceClosed();
+#ifdef DOING_JSFILTERS
+ JSFilter_cleanup();
+#endif
+}
+
+// not an IMETHOD so we don't need to do error checking or return an error.
+// We only have one caller.
+void nsParseNewMailState::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_IF_ADDREF(*aMsgWindow = m_msgWindow);
+}
+
+// This gets called for every message because libnet calls IncorporateBegin,
+// IncorporateWrite (once or more), and IncorporateComplete for every message.
+void nsParseNewMailState::DoneParsingFolder(nsresult status) {
+ PublishMsgHeader(nullptr);
+ if (m_mailDB) // finished parsing, so flush db folder info
+ UpdateDBFolderInfo();
+}
+
+void nsParseNewMailState::OnNewMessage(nsIMsgWindow* msgWindow) {}
+
+int32_t nsParseNewMailState::PublishMsgHeader(nsIMsgWindow* msgWindow) {
+ bool moved = false;
+ FinishHeader();
+
+ if (m_newMsgHdr) {
+ uint32_t newFlags, oldFlags;
+ m_newMsgHdr->GetFlags(&oldFlags);
+ if (!(oldFlags &
+ nsMsgMessageFlags::Read)) // don't mark read messages as new.
+ m_newMsgHdr->OrFlags(nsMsgMessageFlags::New, &newFlags);
+
+ if (!m_disableFilters) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_rootFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, 0);
+ int32_t duplicateAction;
+ server->GetIncomingDuplicateAction(&duplicateAction);
+ if (duplicateAction != nsIMsgIncomingServer::keepDups) {
+ bool isDup;
+ server->IsNewHdrDuplicate(m_newMsgHdr, &isDup);
+ if (isDup) {
+ // we want to do something similar to applying filter hits.
+ // if a dup is marked read, it shouldn't trigger biff.
+ // Same for deleting it or moving it to trash.
+ switch (duplicateAction) {
+ case nsIMsgIncomingServer::deleteDups: {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv =
+ m_downloadFolder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_SUCCEEDED(rv)) {
+ rv = msgStore->DiscardNewMessage(m_outputStream, m_newMsgHdr);
+ if (NS_FAILED(rv))
+ m_rootFolder->ThrowAlertMsg("dupDeleteFolderTruncateFailed",
+ msgWindow);
+ }
+ m_mailDB->RemoveHeaderMdbRow(m_newMsgHdr);
+ } break;
+
+ case nsIMsgIncomingServer::moveDupsToTrash: {
+ nsCOMPtr<nsIMsgFolder> trash;
+ GetTrashFolder(getter_AddRefs(trash));
+ if (trash) {
+ uint32_t newFlags;
+ bool msgMoved;
+ m_newMsgHdr->AndFlags(~nsMsgMessageFlags::New, &newFlags);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = m_downloadFolder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_SUCCEEDED(rv))
+ rv = msgStore->MoveNewlyDownloadedMessage(m_newMsgHdr, trash,
+ &msgMoved);
+ if (NS_SUCCEEDED(rv) && !msgMoved) {
+ rv = MoveIncorporatedMessage(m_newMsgHdr, m_mailDB, trash,
+ nullptr, msgWindow);
+ if (NS_SUCCEEDED(rv))
+ rv = m_mailDB->RemoveHeaderMdbRow(m_newMsgHdr);
+ }
+ if (NS_FAILED(rv))
+ NS_WARNING("moveDupsToTrash failed for some reason.");
+ }
+ } break;
+ case nsIMsgIncomingServer::markDupsRead:
+ MarkFilteredMessageRead(m_newMsgHdr);
+ break;
+ }
+ int32_t numNewMessages;
+ m_downloadFolder->GetNumNewMessages(false, &numNewMessages);
+ m_downloadFolder->SetNumNewMessages(numNewMessages - 1);
+
+ m_newMsgHdr = nullptr;
+ return 0;
+ }
+ }
+
+ ApplyFilters(&moved, msgWindow);
+ }
+ if (!moved) {
+ if (m_mailDB) {
+ m_mailDB->AddNewHdrToDB(m_newMsgHdr, true);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgAdded(m_newMsgHdr);
+ // mark the header as not yet reported classified
+ nsMsgKey msgKey;
+ m_newMsgHdr->GetMessageKey(&msgKey);
+ m_downloadFolder->OrProcessingFlags(
+ msgKey, nsMsgProcessingFlags::NotReportedClassified);
+ }
+ } // if it was moved by imap filter, m_parseMsgState->m_newMsgHdr ==
+ // nullptr
+ m_newMsgHdr = nullptr;
+ }
+ return 0;
+}
+
+nsresult nsParseNewMailState::GetTrashFolder(nsIMsgFolder** pTrashFolder) {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ if (!pTrashFolder) return NS_ERROR_NULL_POINTER;
+
+ if (m_downloadFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ m_downloadFolder->GetServer(getter_AddRefs(incomingServer));
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ incomingServer->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ if (rootMsgFolder) {
+ rv = rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash,
+ pTrashFolder);
+ if (!*pTrashFolder) rv = NS_ERROR_FAILURE;
+ }
+ }
+ return rv;
+}
+
+void nsParseNewMailState::ApplyFilters(bool* pMoved, nsIMsgWindow* msgWindow) {
+ m_msgMovedByFilter = m_msgCopiedByFilter = false;
+
+ if (!m_disableFilters) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr = m_newMsgHdr;
+ nsCOMPtr<nsIMsgFolder> downloadFolder = m_downloadFolder;
+ if (m_rootFolder) {
+ if (!downloadFolder)
+ m_rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox,
+ getter_AddRefs(downloadFolder));
+ if (downloadFolder) downloadFolder->GetURI(m_inboxUri);
+ char* headers = m_headers.GetBuffer();
+ uint32_t headersSize = m_headers.GetBufferPos();
+ nsAutoCString tok;
+ msgHdr->GetStringProperty("storeToken", tok);
+ if (m_filterList) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Running filters on 1 message (%s)", tok.get()));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Using filters from the original account"));
+ (void)m_filterList->ApplyFiltersToHdr(
+ nsMsgFilterType::InboxRule, msgHdr, downloadFolder, m_mailDB,
+ nsDependentCSubstring(headers, headersSize), this, msgWindow);
+ }
+ if (!m_msgMovedByFilter && m_deferredToServerFilterList) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Running filters on 1 message (%s)", tok.get()));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Using filters from the deferred to account"));
+ (void)m_deferredToServerFilterList->ApplyFiltersToHdr(
+ nsMsgFilterType::InboxRule, msgHdr, downloadFolder, m_mailDB,
+ nsDependentCSubstring(headers, headersSize), this, msgWindow);
+ }
+ }
+ }
+ if (pMoved) *pMoved = m_msgMovedByFilter;
+}
+
+NS_IMETHODIMP nsParseNewMailState::ApplyFilterHit(nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow,
+ bool* applyMore) {
+ NS_ENSURE_ARG_POINTER(filter);
+ NS_ENSURE_ARG_POINTER(applyMore);
+
+ uint32_t newFlags;
+ nsresult rv = NS_OK;
+
+ *applyMore = true;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr = m_newMsgHdr;
+
+ nsTArray<RefPtr<nsIMsgRuleAction>> filterActionList;
+ rv = filter->GetSortedActionList(filterActionList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t numActions = filterActionList.Length();
+
+ nsCString msgId;
+ msgHdr->GetMessageId(getter_Copies(msgId));
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Applying %" PRIu32
+ " filter actions on message with key %" PRIu32,
+ numActions, msgKeyToInt(msgKey)));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Local) Message ID: %s", msgId.get()));
+
+ bool loggingEnabled = false;
+ if (m_filterList && numActions)
+ m_filterList->GetLoggingEnabled(&loggingEnabled);
+
+ bool msgIsNew = true;
+ nsresult finalResult = NS_OK; // result of all actions
+ for (uint32_t actionIndex = 0; actionIndex < numActions && *applyMore;
+ actionIndex++) {
+ nsCOMPtr<nsIMsgRuleAction> filterAction(filterActionList[actionIndex]);
+ if (!filterAction) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
+ ("(Local) Filter action at index %" PRIu32 " invalid, skipping",
+ actionIndex));
+ continue;
+ }
+
+ nsMsgRuleActionType actionType;
+ if (NS_SUCCEEDED(filterAction->GetType(&actionType))) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Running filter action at index %" PRIu32
+ ", action type = %i",
+ actionIndex, actionType));
+ if (loggingEnabled) (void)filter->LogRuleHit(filterAction, msgHdr);
+
+ nsCString actionTargetFolderUri;
+ if (actionType == nsMsgFilterAction::MoveToFolder ||
+ actionType == nsMsgFilterAction::CopyToFolder) {
+ rv = filterAction->GetTargetFolderUri(actionTargetFolderUri);
+ if (NS_FAILED(rv) || actionTargetFolderUri.IsEmpty()) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
+ ("(Local) Target URI for Copy/Move action is empty, skipping"));
+ // clang-format on
+ NS_ASSERTION(false, "actionTargetFolderUri is empty");
+ continue;
+ }
+ }
+
+ rv = NS_OK; // result of the current action
+ switch (actionType) {
+ case nsMsgFilterAction::Delete: {
+ nsCOMPtr<nsIMsgFolder> trash;
+ // set value to trash folder
+ rv = GetTrashFolder(getter_AddRefs(trash));
+ if (NS_SUCCEEDED(rv) && trash) {
+ rv = trash->GetURI(actionTargetFolderUri);
+ if (NS_FAILED(rv)) break;
+ }
+
+ rv = msgHdr->OrFlags(nsMsgMessageFlags::Read,
+ &newFlags); // mark read in trash.
+ msgIsNew = false;
+ }
+ // FALLTHROUGH
+ [[fallthrough]];
+ case nsMsgFilterAction::MoveToFolder: {
+ // if moving to a different file, do it.
+ if (!actionTargetFolderUri.IsEmpty() &&
+ !m_inboxUri.Equals(actionTargetFolderUri,
+ nsCaseInsensitiveCStringComparator)) {
+ nsCOMPtr<nsIMsgFolder> destIFolder;
+ // XXX TODO: why do we create the folder here, while we do not in
+ // the Copy action?
+ rv = GetOrCreateFolder(actionTargetFolderUri,
+ getter_AddRefs(destIFolder));
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Target Folder for Move action does not exist"));
+ break;
+ }
+ bool msgMoved = false;
+ // If we're moving to an imap folder, or this message has already
+ // has a pending copy action, use the imap coalescer so that
+ // we won't truncate the inbox before the copy fires.
+ if (m_msgCopiedByFilter ||
+ StringBeginsWith(actionTargetFolderUri, "imap:"_ns)) {
+ if (!m_moveCoalescer)
+ m_moveCoalescer =
+ new nsImapMoveCoalescer(m_downloadFolder, m_msgWindow);
+ NS_ENSURE_TRUE(m_moveCoalescer, NS_ERROR_OUT_OF_MEMORY);
+ rv = m_moveCoalescer->AddMove(destIFolder, msgKey);
+ msgIsNew = false;
+ if (NS_FAILED(rv)) break;
+ } else {
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = m_downloadFolder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_SUCCEEDED(rv))
+ rv = msgStore->MoveNewlyDownloadedMessage(msgHdr, destIFolder,
+ &msgMoved);
+ if (NS_SUCCEEDED(rv) && !msgMoved)
+ rv = MoveIncorporatedMessage(msgHdr, m_mailDB, destIFolder,
+ filter, msgWindow);
+ m_msgMovedByFilter = NS_SUCCEEDED(rv);
+ if (!m_msgMovedByFilter /* == NS_FAILED(err) */) {
+ // XXX: Invoke MSG_LOG_TO_CONSOLE once bug 1135265 lands.
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureMoveFailed"_ns);
+ }
+ }
+ }
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Target folder is the same as source folder, "
+ "skipping"));
+ rv = NS_OK;
+ }
+ *applyMore = false;
+ } break;
+ case nsMsgFilterAction::CopyToFolder: {
+ nsCString uri;
+ rv = m_rootFolder->GetURI(uri);
+
+ if (!actionTargetFolderUri.IsEmpty() &&
+ !actionTargetFolderUri.Equals(uri)) {
+ nsCOMPtr<nsIMsgFolder> dstFolder;
+ nsCOMPtr<nsIMsgCopyService> copyService;
+ rv = GetExistingFolder(actionTargetFolderUri,
+ getter_AddRefs(dstFolder));
+ if (NS_FAILED(rv)) {
+ // Let's show a more specific warning.
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Target Folder for Copy action does not exist"));
+ NS_WARNING("Target Folder does not exist.");
+ break;
+ }
+
+ copyService = do_GetService(
+ "@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = copyService->CopyMessages(m_downloadFolder, {&*msgHdr},
+ dstFolder, false, nullptr,
+ msgWindow, false);
+
+ if (NS_FAILED(rv)) {
+ // XXX: Invoke MSG_LOG_TO_CONSOLE once bug 1135265 lands.
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureCopyFailed"_ns);
+ }
+ } else
+ m_msgCopiedByFilter = true;
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Target folder is the same as source folder, "
+ "skipping"));
+ break;
+ }
+ } break;
+ case nsMsgFilterAction::MarkRead:
+ msgIsNew = false;
+ MarkFilteredMessageRead(msgHdr);
+ rv = NS_OK;
+ break;
+ case nsMsgFilterAction::MarkUnread:
+ msgIsNew = true;
+ MarkFilteredMessageUnread(msgHdr);
+ rv = NS_OK;
+ break;
+ case nsMsgFilterAction::KillThread:
+ rv = msgHdr->SetUint32Property("ProtoThreadFlags",
+ nsMsgMessageFlags::Ignored);
+ break;
+ case nsMsgFilterAction::KillSubthread:
+ rv = msgHdr->OrFlags(nsMsgMessageFlags::Ignored, &newFlags);
+ break;
+ case nsMsgFilterAction::WatchThread:
+ rv = msgHdr->OrFlags(nsMsgMessageFlags::Watched, &newFlags);
+ break;
+ case nsMsgFilterAction::MarkFlagged: {
+ rv = m_downloadFolder->MarkMessagesFlagged({&*msgHdr}, true);
+ } break;
+ case nsMsgFilterAction::ChangePriority: {
+ nsMsgPriorityValue filterPriority;
+ filterAction->GetPriority(&filterPriority);
+ rv = msgHdr->SetPriority(filterPriority);
+ } break;
+ case nsMsgFilterAction::AddTag: {
+ nsCString keyword;
+ filterAction->GetStrValue(keyword);
+ rv = m_downloadFolder->AddKeywordsToMessages({&*msgHdr}, keyword);
+ break;
+ }
+ case nsMsgFilterAction::JunkScore: {
+ nsAutoCString junkScoreStr;
+ int32_t junkScore;
+ filterAction->GetJunkScore(&junkScore);
+ junkScoreStr.AppendInt(junkScore);
+ if (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) msgIsNew = false;
+ rv = msgHdr->SetStringProperty("junkscore", junkScoreStr);
+ msgHdr->SetStringProperty("junkscoreorigin", "filter"_ns);
+ } break;
+ case nsMsgFilterAction::Forward: {
+ nsCString forwardTo;
+ filterAction->GetStrValue(forwardTo);
+ m_forwardTo.AppendElement(forwardTo);
+ m_msgToForwardOrReply = msgHdr;
+ rv = NS_OK;
+ } break;
+ case nsMsgFilterAction::Reply: {
+ nsCString replyTemplateUri;
+ filterAction->GetStrValue(replyTemplateUri);
+ m_replyTemplateUri.AppendElement(replyTemplateUri);
+ m_msgToForwardOrReply = msgHdr;
+ m_ruleAction = filterAction;
+ m_filter = filter;
+ rv = NS_OK;
+ } break;
+ case nsMsgFilterAction::DeleteFromPop3Server: {
+ nsCOMPtr<nsIMsgFolder> downloadFolder;
+ msgHdr->GetFolder(getter_AddRefs(downloadFolder));
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(downloadFolder, &rv);
+ if (NS_FAILED(rv) || !localFolder) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Couldn't find local mail folder"));
+ break;
+ }
+ // This action ignores the deleteMailLeftOnServer preference
+ rv = localFolder->MarkMsgsOnPop3Server({&*msgHdr}, POP3_FORCE_DEL);
+
+ // If this is just a header, throw it away. It's useless now
+ // that the server copy is being deleted.
+ uint32_t flags = 0;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) {
+ m_msgMovedByFilter = true;
+ msgIsNew = false;
+ }
+ } break;
+ case nsMsgFilterAction::FetchBodyFromPop3Server: {
+ nsCOMPtr<nsIMsgFolder> downloadFolder;
+ msgHdr->GetFolder(getter_AddRefs(downloadFolder));
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(downloadFolder, &rv);
+ if (NS_FAILED(rv) || !localFolder) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Couldn't find local mail folder"));
+ break;
+ }
+ uint32_t flags = 0;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) {
+ rv = localFolder->MarkMsgsOnPop3Server({&*msgHdr}, POP3_FETCH_BODY);
+ // Don't add this header to the DB, we're going to replace it
+ // with the full message.
+ m_msgMovedByFilter = true;
+ msgIsNew = false;
+ // Don't do anything else in this filter, wait until we
+ // have the full message.
+ *applyMore = false;
+ }
+ } break;
+
+ case nsMsgFilterAction::StopExecution: {
+ // don't apply any more filters
+ *applyMore = false;
+ rv = NS_OK;
+ } break;
+
+ case nsMsgFilterAction::Custom: {
+ nsCOMPtr<nsIMsgFilterCustomAction> customAction;
+ rv = filterAction->GetCustomAction(getter_AddRefs(customAction));
+ if (NS_FAILED(rv)) break;
+
+ nsAutoCString value;
+ rv = filterAction->GetStrValue(value);
+ if (NS_FAILED(rv)) break;
+
+ rv = customAction->ApplyAction({&*msgHdr}, value, nullptr,
+ nsMsgFilterType::InboxRule, msgWindow);
+ } break;
+
+ default:
+ // XXX should not be reached. Check in debug build.
+ NS_ERROR("unexpected filter action");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+ }
+ if (NS_FAILED(rv)) {
+ finalResult = rv;
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Action execution failed with error: %" PRIx32,
+ static_cast<uint32_t>(rv)));
+ if (loggingEnabled) {
+ (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
+ "filterFailureAction"_ns);
+ }
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Action execution succeeded"));
+ }
+ }
+ if (!msgIsNew) {
+ int32_t numNewMessages;
+ m_downloadFolder->GetNumNewMessages(false, &numNewMessages);
+ if (numNewMessages > 0)
+ m_downloadFolder->SetNumNewMessages(numNewMessages - 1);
+ m_numNotNewMessages++;
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Message will not be marked new"));
+ }
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Finished executing actions"));
+ return finalResult;
+}
+
+// this gets run in a second pass, after apply filters to a header.
+nsresult nsParseNewMailState::ApplyForwardAndReplyFilter(
+ nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ uint32_t i;
+ uint32_t count = m_forwardTo.Length();
+ nsMsgKey msgKey;
+ if (count > 0 && m_msgToForwardOrReply) {
+ m_msgToForwardOrReply->GetMessageKey(&msgKey);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Forwarding message with key %" PRIu32 " to %" PRIu32
+ " addresses",
+ msgKeyToInt(msgKey), count));
+ }
+
+ for (i = 0; i < count; i++) {
+ if (!m_forwardTo[i].IsEmpty()) {
+ nsAutoString forwardStr;
+ CopyASCIItoUTF16(m_forwardTo[i], forwardStr);
+ rv = m_rootFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ {
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = compService->ForwardMessage(
+ forwardStr, m_msgToForwardOrReply, msgWindow, server,
+ nsIMsgComposeService::kForwardAsDefault);
+ if (NS_FAILED(rv))
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Forwarding failed"));
+ }
+ }
+ }
+ m_forwardTo.Clear();
+
+ count = m_replyTemplateUri.Length();
+ if (count > 0 && m_msgToForwardOrReply) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Local) Replying message with key %" PRIu32 " to %" PRIu32
+ " addresses",
+ msgKeyToInt(msgKey), count));
+ }
+
+ for (i = 0; i < count; i++) {
+ if (!m_replyTemplateUri[i].IsEmpty()) {
+ // copy this and truncate the original, so we don't accidentally re-use it
+ // on the next hdr.
+ rv = m_rootFolder->GetServer(getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1");
+ if (compService) {
+ rv = compService->ReplyWithTemplate(
+ m_msgToForwardOrReply, m_replyTemplateUri[i], msgWindow, server);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ReplyWithTemplate failed");
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Local) Replying failed"));
+ if (rv == NS_ERROR_ABORT) {
+ (void)m_filter->LogRuleHitFail(
+ m_ruleAction, m_msgToForwardOrReply, rv,
+ "filterFailureSendingReplyAborted"_ns);
+ } else {
+ (void)m_filter->LogRuleHitFail(
+ m_ruleAction, m_msgToForwardOrReply, rv,
+ "filterFailureSendingReplyError"_ns);
+ }
+ }
+ }
+ }
+ }
+ }
+ m_replyTemplateUri.Clear();
+ m_msgToForwardOrReply = nullptr;
+ return rv;
+}
+
+void nsParseNewMailState::MarkFilteredMessageRead(nsIMsgDBHdr* msgHdr) {
+ m_downloadFolder->MarkMessagesRead({msgHdr}, true);
+}
+
+void nsParseNewMailState::MarkFilteredMessageUnread(nsIMsgDBHdr* msgHdr) {
+ uint32_t newFlags;
+ if (m_mailDB) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ m_mailDB->AddToNewList(msgKey);
+ } else {
+ msgHdr->OrFlags(nsMsgMessageFlags::New, &newFlags);
+ }
+ m_downloadFolder->MarkMessagesRead({msgHdr}, false);
+}
+
+nsresult nsParseNewMailState::EndMsgDownload() {
+ if (m_moveCoalescer) m_moveCoalescer->PlaybackMoves();
+
+ // need to do this for all folders that had messages filtered into them
+ uint32_t serverCount = m_filterTargetFolders.Count();
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) // don't use NS_ENSURE_SUCCESS here - we
+ // need to release semaphore below
+ {
+ for (uint32_t index = 0; index < serverCount; index++) {
+ bool folderOpen;
+ session->IsFolderOpenInWindow(m_filterTargetFolders[index], &folderOpen);
+ if (!folderOpen) {
+ uint32_t folderFlags;
+ m_filterTargetFolders[index]->GetFlags(&folderFlags);
+ if (!(folderFlags &
+ (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox))) {
+ bool filtersRun;
+ m_filterTargetFolders[index]->CallFilterPlugins(nullptr, &filtersRun);
+ if (!filtersRun)
+ m_filterTargetFolders[index]->SetMsgDatabase(nullptr);
+ }
+ }
+ }
+ }
+ m_filterTargetFolders.Clear();
+ return rv;
+}
+
+nsresult nsParseNewMailState::AppendMsgFromStream(nsIInputStream* fileStream,
+ nsIMsgDBHdr* aHdr,
+ nsIMsgFolder* destFolder) {
+ nsCOMPtr<nsIMsgPluggableStore> store;
+ nsCOMPtr<nsIOutputStream> destOutputStream;
+ nsresult rv = destFolder->GetMsgStore(getter_AddRefs(store));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = store->GetNewMsgOutputStream(destFolder, &aHdr,
+ getter_AddRefs(destOutputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint64_t bytesCopied;
+ rv = SyncCopyStream(fileStream, destOutputStream, bytesCopied);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = store->FinishNewMessage(destOutputStream, aHdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+/*
+ * Moves message pointed to by mailHdr into folder destIFolder.
+ * After successful move mailHdr is no longer usable by the caller.
+ */
+nsresult nsParseNewMailState::MoveIncorporatedMessage(nsIMsgDBHdr* mailHdr,
+ nsIMsgDatabase* sourceDB,
+ nsIMsgFolder* destIFolder,
+ nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(destIFolder);
+ nsresult rv = NS_OK;
+
+ // check if the destination is a real folder (by checking for null parent)
+ // and if it can file messages (e.g., servers or news folders can't file
+ // messages). Or read only imap folders...
+ bool canFileMessages = true;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ destIFolder->GetParent(getter_AddRefs(parentFolder));
+ if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages);
+ if (!parentFolder || !canFileMessages) {
+ if (filter) {
+ filter->SetEnabled(false);
+ // we need to explicitly save the filter file.
+ if (m_filterList) m_filterList->SaveToDefaultFile();
+ destIFolder->ThrowAlertMsg("filterDisabled", msgWindow);
+ }
+ return NS_MSG_NOT_A_MAIL_FOLDER;
+ }
+
+ uint32_t messageLength;
+ mailHdr->GetMessageSize(&messageLength);
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(destIFolder);
+ if (localFolder) {
+ bool destFolderTooBig = true;
+ rv = localFolder->WarnIfLocalFileTooBig(msgWindow, messageLength,
+ &destFolderTooBig);
+ if (NS_FAILED(rv) || destFolderTooBig)
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+
+ nsCOMPtr<nsISupports> myISupports =
+ do_QueryInterface(static_cast<nsIMsgParseMailMsgState*>(this));
+
+ // Make sure no one else is writing into this folder
+ if (NS_FAILED(rv = destIFolder->AcquireSemaphore(myISupports))) {
+ destIFolder->ThrowAlertMsg("filterFolderDeniedLocked", msgWindow);
+ return rv;
+ }
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv =
+ m_downloadFolder->GetLocalMsgStream(mailHdr, getter_AddRefs(inputStream));
+ if (NS_FAILED(rv)) {
+ NS_ERROR("couldn't get source msg input stream in move filter");
+ destIFolder->ReleaseSemaphore(myISupports);
+ return NS_MSG_FOLDER_UNREADABLE; // ### dmb
+ }
+
+ nsCOMPtr<nsIMsgDatabase> destMailDB;
+
+ if (!localFolder) return NS_MSG_POP_FILTER_TARGET_ERROR;
+
+ // don't force upgrade in place - open the db here before we start writing to
+ // the destination file because XP_Stat can return file size including bytes
+ // written...
+ rv = localFolder->GetDatabaseWOReparse(getter_AddRefs(destMailDB));
+ NS_WARNING_ASSERTION(destMailDB && NS_SUCCEEDED(rv),
+ "failed to open mail db parsing folder");
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+
+ if (destMailDB)
+ rv = destMailDB->CopyHdrFromExistingHdr(m_new_key, mailHdr, true,
+ getter_AddRefs(newHdr));
+ if (NS_SUCCEEDED(rv) && !newHdr) rv = NS_ERROR_UNEXPECTED;
+
+ if (NS_FAILED(rv)) {
+ destIFolder->ThrowAlertMsg("filterFolderHdrAddFailed", msgWindow);
+ } else {
+ rv = AppendMsgFromStream(inputStream, newHdr, destIFolder);
+ if (NS_FAILED(rv))
+ destIFolder->ThrowAlertMsg("filterFolderWriteFailed", msgWindow);
+ }
+
+ if (NS_FAILED(rv)) {
+ if (destMailDB) destMailDB->Close(true);
+
+ destIFolder->ReleaseSemaphore(myISupports);
+
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+
+ bool movedMsgIsNew = false;
+ // if we have made it this far then the message has successfully been written
+ // to the new folder now add the header to the destMailDB.
+
+ uint32_t newFlags;
+ newHdr->GetFlags(&newFlags);
+ nsMsgKey msgKey;
+ newHdr->GetMessageKey(&msgKey);
+ if (!(newFlags & nsMsgMessageFlags::Read)) {
+ nsCString junkScoreStr;
+ (void)newHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_HAM_SCORE) {
+ newHdr->OrFlags(nsMsgMessageFlags::New, &newFlags);
+ destMailDB->AddToNewList(msgKey);
+ movedMsgIsNew = true;
+ }
+ }
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgAdded(newHdr);
+ // mark the header as not yet reported classified
+ destIFolder->OrProcessingFlags(msgKey,
+ nsMsgProcessingFlags::NotReportedClassified);
+ m_msgToForwardOrReply = newHdr;
+
+ if (movedMsgIsNew) destIFolder->SetHasNewMessages(true);
+ if (!m_filterTargetFolders.Contains(destIFolder))
+ m_filterTargetFolders.AppendObject(destIFolder);
+
+ destIFolder->ReleaseSemaphore(myISupports);
+
+ (void)localFolder->RefreshSizeOnDisk();
+
+ // Notify the message was moved.
+ if (notifier) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = mailHdr->GetFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) {
+ notifier->NotifyMsgUnincorporatedMoved(folder, newHdr);
+ } else {
+ NS_WARNING("Can't get folder for message that was moved.");
+ }
+ }
+
+ nsCOMPtr<nsIMsgPluggableStore> store;
+ rv = m_downloadFolder->GetMsgStore(getter_AddRefs(store));
+ if (store) store->DiscardNewMessage(m_outputStream, mailHdr);
+ if (sourceDB) sourceDB->RemoveHeaderMdbRow(mailHdr);
+
+ // update the folder size so we won't reparse.
+ UpdateDBFolderInfo(destMailDB);
+ destIFolder->UpdateSummaryTotals(true);
+
+ destMailDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ return rv;
+}
diff --git a/comm/mailnews/local/src/nsParseMailbox.h b/comm/mailnews/local/src/nsParseMailbox.h
new file mode 100644
index 0000000000..1e272cd8c0
--- /dev/null
+++ b/comm/mailnews/local/src/nsParseMailbox.h
@@ -0,0 +1,256 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsParseMailbox_H
+#define nsParseMailbox_H
+
+#include "mozilla/Attributes.h"
+#include "nsIURI.h"
+#include "nsIMsgParseMailMsgState.h"
+#include "nsIStreamListener.h"
+#include "nsMsgLineBuffer.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIDBChangeListener.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIMsgWindow.h"
+#include "nsImapMoveCoalescer.h"
+#include "nsString.h"
+#include "nsIMsgFilterList.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgFilterHitNotify.h"
+#include "nsTArray.h"
+
+class nsOutputFileStream;
+class nsIMsgFolder;
+
+/* Used for the various things that parse RFC822 headers...
+ */
+typedef struct message_header {
+ const char* value; /* The contents of a header (after ": ") */
+ int32_t length; /* The length of the data (it is not NULL-terminated.) */
+} message_header;
+
+// This object maintains the parse state for a single mail message.
+class nsParseMailMessageState : public nsIMsgParseMailMsgState,
+ public nsIDBChangeListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPARSEMAILMSGSTATE
+ NS_DECL_NSIDBCHANGELISTENER
+
+ nsParseMailMessageState();
+
+ nsresult ParseFolderLine(const char* line, uint32_t lineLength);
+ nsresult StartNewEnvelope(const char* line, uint32_t lineLength);
+ nsresult ParseHeaders();
+ nsresult FinalizeHeaders();
+ nsresult ParseEnvelope(const char* line, uint32_t line_size);
+ nsresult InternSubject(struct message_header* header);
+
+ // Returns true if line looks like an mbox "From " line.
+ static bool IsEnvelopeLine(const char* buf, int32_t buf_size);
+
+ // Helpers for dealing with multi-value headers.
+ struct message_header* GetNextHeaderInAggregate(
+ nsTArray<struct message_header*>& list);
+ void GetAggregateHeader(nsTArray<struct message_header*>& list,
+ struct message_header*);
+ void ClearAggregateHeader(nsTArray<struct message_header*>& list);
+
+ nsCOMPtr<nsIMsgDBHdr> m_newMsgHdr; /* current message header we're building */
+ nsCOMPtr<nsIMsgDatabase> m_mailDB;
+ nsCOMPtr<nsIMsgDatabase> m_backupMailDB;
+
+ nsMailboxParseState m_state;
+ int64_t m_position;
+ // The start of the "From " line (the line before the start of the message).
+ uint64_t m_envelope_pos;
+ // The start of the message headers (immediately follows "From " line).
+ uint64_t m_headerstartpos;
+ nsMsgKey m_new_key; // DB key for the new header.
+
+ // The "From " line, if any.
+ ::nsByteArray m_envelope;
+
+ // These two point into the m_envelope buffer.
+ struct message_header m_envelope_from;
+ struct message_header m_envelope_date;
+
+ // The raw header data.
+ ::nsByteArray m_headers;
+
+ // These all point into the m_headers buffer.
+ struct message_header m_message_id;
+ struct message_header m_references;
+ struct message_header m_date;
+ struct message_header m_delivery_date;
+ struct message_header m_from;
+ struct message_header m_sender;
+ struct message_header m_newsgroups;
+ struct message_header m_subject;
+ struct message_header m_status;
+ struct message_header m_mozstatus;
+ struct message_header m_mozstatus2;
+ struct message_header m_in_reply_to;
+ struct message_header m_replyTo;
+ struct message_header m_content_type;
+ struct message_header m_bccList;
+
+ // Support for having multiple To or Cc header lines in a message
+ nsTArray<struct message_header*> m_toList;
+ nsTArray<struct message_header*> m_ccList;
+
+ struct message_header m_priority;
+ struct message_header m_account_key;
+ struct message_header m_keywords;
+
+ // Mdn support
+ struct message_header m_mdn_original_recipient;
+ struct message_header m_return_path;
+ struct message_header m_mdn_dnt; /* MDN Disposition-Notification-To: header */
+
+ PRTime m_receivedTime;
+ uint16_t m_body_lines;
+ uint16_t m_lastLineBlank;
+
+ // this enables extensions to add the values of particular headers to
+ // the .msf file as properties of nsIMsgHdr. It is initialized from a
+ // pref, mailnews.customDBHeaders
+ nsTArray<nsCString> m_customDBHeaders;
+ struct message_header* m_customDBHeaderValues;
+ nsCString m_receivedValue; // accumulated received header
+ protected:
+ virtual ~nsParseMailMessageState();
+};
+
+// This class is part of the mailbox parsing state machine
+class nsMsgMailboxParser : public nsIStreamListener,
+ public nsParseMailMessageState,
+ public nsMsgLineBuffer {
+ public:
+ explicit nsMsgMailboxParser(nsIMsgFolder*);
+ nsMsgMailboxParser();
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // we support the nsIStreamListener interface
+ ////////////////////////////////////////////////////////////////////////////////////////
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ void SetDB(nsIMsgDatabase* mailDB) { m_mailDB = mailDB; }
+
+ // message socket libnet callbacks, which come through folder pane
+ nsresult ProcessMailboxInputStream(nsIInputStream* aIStream,
+ uint32_t aLength);
+
+ virtual void DoneParsingFolder(nsresult status);
+ virtual void AbortNewHeader();
+
+ // for nsMsgLineBuffer
+ virtual nsresult HandleLine(const char* line, uint32_t line_length) override;
+
+ void UpdateDBFolderInfo();
+ void UpdateDBFolderInfo(nsIMsgDatabase* mailDB);
+ void UpdateStatusText(const char* stringName);
+
+ // Update the progress bar based on what we know.
+ virtual void UpdateProgressPercent();
+ virtual void OnNewMessage(nsIMsgWindow* msgWindow);
+
+ protected:
+ virtual ~nsMsgMailboxParser();
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+
+ virtual int32_t PublishMsgHeader(nsIMsgWindow* msgWindow);
+
+ // data
+ nsString m_folderName;
+ nsCString m_inboxUri;
+ ::nsByteArray m_inputStream;
+ uint64_t m_graph_progress_total;
+ uint64_t m_graph_progress_received;
+
+ private:
+ nsWeakPtr m_folder;
+ void ReleaseFolderLock();
+ nsresult AcquireFolderLock();
+};
+
+class nsParseNewMailState : public nsMsgMailboxParser,
+ public nsIMsgFilterHitNotify {
+ public:
+ nsParseNewMailState();
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsresult Init(nsIMsgFolder* rootFolder, nsIMsgFolder* downloadFolder,
+ nsIMsgWindow* aMsgWindow, nsIMsgDBHdr* aHdr,
+ nsIOutputStream* aOutputStream);
+
+ virtual void DoneParsingFolder(nsresult status) override;
+
+ void DisableFilters() { m_disableFilters = true; }
+
+ NS_DECL_NSIMSGFILTERHITNOTIFY
+
+ nsOutputFileStream* GetLogFile();
+ virtual int32_t PublishMsgHeader(nsIMsgWindow* msgWindow) override;
+ void GetMsgWindow(nsIMsgWindow** aMsgWindow);
+ nsresult EndMsgDownload();
+
+ nsresult AppendMsgFromStream(nsIInputStream* fileStream, nsIMsgDBHdr* aHdr,
+ nsIMsgFolder* destFolder);
+
+ void ApplyFilters(bool* pMoved, nsIMsgWindow* msgWindow);
+ nsresult ApplyForwardAndReplyFilter(nsIMsgWindow* msgWindow);
+ virtual void OnNewMessage(nsIMsgWindow* msgWindow) override;
+
+ // this keeps track of how many messages we downloaded that
+ // aren't new - e.g., marked read, or moved to an other server.
+ int32_t m_numNotNewMessages;
+
+ protected:
+ virtual ~nsParseNewMailState();
+ virtual nsresult GetTrashFolder(nsIMsgFolder** pTrashFolder);
+ virtual nsresult MoveIncorporatedMessage(nsIMsgDBHdr* mailHdr,
+ nsIMsgDatabase* sourceDB,
+ nsIMsgFolder* destIFolder,
+ nsIMsgFilter* filter,
+ nsIMsgWindow* msgWindow);
+ virtual void MarkFilteredMessageRead(nsIMsgDBHdr* msgHdr);
+ virtual void MarkFilteredMessageUnread(nsIMsgDBHdr* msgHdr);
+
+ nsCOMPtr<nsIMsgFilterList> m_filterList;
+ nsCOMPtr<nsIMsgFilterList> m_deferredToServerFilterList;
+ nsCOMPtr<nsIMsgFolder> m_rootFolder;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgFolder> m_downloadFolder;
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ nsCOMArray<nsIMsgFolder> m_filterTargetFolders;
+
+ RefPtr<nsImapMoveCoalescer> m_moveCoalescer;
+
+ bool m_msgMovedByFilter;
+ bool m_msgCopiedByFilter;
+ bool m_disableFilters;
+
+ // we have to apply the reply/forward filters in a second pass, after
+ // msg quarantining and moving to other local folders, so we remember the
+ // info we'll need to apply them with these vars.
+ // these need to be arrays in case we have multiple reply/forward filters.
+ nsTArray<nsCString> m_forwardTo;
+ nsTArray<nsCString> m_replyTemplateUri;
+ nsCOMPtr<nsIMsgDBHdr> m_msgToForwardOrReply;
+ nsCOMPtr<nsIMsgFilter> m_filter;
+ nsCOMPtr<nsIMsgRuleAction> m_ruleAction;
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsPop3Sink.cpp b/comm/mailnews/local/src/nsPop3Sink.cpp
new file mode 100644
index 0000000000..2d4b7472c9
--- /dev/null
+++ b/comm/mailnews/local/src/nsPop3Sink.cpp
@@ -0,0 +1,740 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsPop3Sink.h"
+#include "prprf.h"
+#include "prlog.h"
+#include "nscore.h"
+#include <stdio.h>
+#include <time.h>
+#include "nsParseMailbox.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsLocalUtils.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsMsgMessageFlags.h"
+#include "nsMailHeaders.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIPop3Protocol.h"
+#include "nsLocalMailFolder.h"
+#include "nsIInputStream.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIPromptService.h"
+#include "nsIDocShell.h"
+#include "mozIDOMWindow.h"
+#include "nsEmbedCID.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIObserverService.h"
+#include "nsIPop3Service.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+
+/* for logging to Error Console */
+#include "nsIScriptError.h"
+
+mozilla::LazyLogModule POP3LOGMODULE("POP3");
+#define POP3LOG(str) "sink: [this=%p] " str, this
+
+NS_IMPL_ISUPPORTS(nsPop3Sink, nsIPop3Sink)
+
+nsPop3Sink::nsPop3Sink() {
+ m_biffState = 0;
+ m_numNewMessages = 0;
+ m_numNewMessagesInFolder = 0;
+ m_numMsgsDownloaded = 0;
+ m_senderAuthed = false;
+ m_outFileStream = nullptr;
+ m_uidlDownload = false;
+ m_buildMessageUri = false;
+}
+
+nsPop3Sink::~nsPop3Sink() {
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("Calling ReleaseFolderLock from ~nsPop3Sink")));
+ ReleaseFolderLock();
+}
+
+partialRecord::partialRecord() : m_msgDBHdr(nullptr) {}
+
+partialRecord::~partialRecord() {}
+
+// Walk through all the messages in this folder and look for any
+// PARTIAL messages. For each of those, dig through the mailbox and
+// find the Account that the message belongs to. If that Account
+// matches the current Account, then look for the Uidl and save
+// this message for later processing.
+nsresult nsPop3Sink::FindPartialMessages() {
+ nsCOMPtr<nsIMsgEnumerator> messages;
+ bool hasMore = false;
+ bool isOpen = false;
+ nsLocalFolderScanState folderScanState;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+ m_folder->GetMsgDatabase(getter_AddRefs(db));
+ if (!localFolder || !db)
+ return NS_ERROR_FAILURE; // we need it to grub through the folder
+
+ nsresult rv = db->EnumerateMessages(getter_AddRefs(messages));
+ if (messages) messages->HasMoreElements(&hasMore);
+ while (hasMore && NS_SUCCEEDED(rv)) {
+ uint32_t flags = 0;
+ nsCOMPtr<nsIMsgDBHdr> msgDBHdr;
+ rv = messages->GetNext(getter_AddRefs(msgDBHdr));
+ if (!NS_SUCCEEDED(rv)) break;
+ msgDBHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) {
+ // Open the various streams we need to seek and read from the mailbox
+ if (!isOpen) {
+ rv = localFolder->GetFolderScanState(&folderScanState);
+ if (NS_SUCCEEDED(rv))
+ isOpen = true;
+ else
+ break;
+ }
+ rv = localFolder->GetUidlFromFolder(&folderScanState, msgDBHdr);
+ if (!NS_SUCCEEDED(rv)) break;
+
+ // If we got the uidl, see if this partial message belongs to this
+ // account. Add it to the array if so...
+ if (folderScanState.m_uidl &&
+ m_accountKey.Equals(folderScanState.m_accountKey,
+ nsCaseInsensitiveCStringComparator)) {
+ partialRecord* partialMsg = new partialRecord();
+ if (partialMsg) {
+ partialMsg->m_uidl = folderScanState.m_uidl;
+ partialMsg->m_msgDBHdr = msgDBHdr;
+ m_partialMsgsArray.AppendElement(partialMsg);
+ }
+ }
+ }
+ messages->HasMoreElements(&hasMore);
+ }
+ if (isOpen && folderScanState.m_inputStream)
+ folderScanState.m_inputStream->Close();
+ return rv;
+}
+
+// For all the partial messages saved by FindPartialMessages,
+// ask the protocol handler if they still exist on the server.
+// Any messages that don't exist any more are deleted from the
+// msgDB.
+void nsPop3Sink::CheckPartialMessages(nsIPop3Protocol* protocol) {
+ uint32_t count = m_partialMsgsArray.Length();
+ bool deleted = false;
+
+ for (uint32_t i = 0; i < count; i++) {
+ partialRecord* partialMsg;
+ bool found = true;
+ partialMsg = m_partialMsgsArray.ElementAt(i);
+ protocol->CheckMessage(partialMsg->m_uidl.get(), &found);
+ if (!found && partialMsg->m_msgDBHdr) {
+ if (m_newMailParser)
+ m_newMailParser->m_mailDB->DeleteHeader(partialMsg->m_msgDBHdr, nullptr,
+ false, true);
+ deleted = true;
+ }
+ delete partialMsg;
+ }
+ m_partialMsgsArray.Clear();
+ if (deleted) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+ if (localFolder) localFolder->NotifyDelete();
+ }
+}
+
+nsresult nsPop3Sink::BeginMailDelivery(bool uidlDownload,
+ nsIMsgWindow* aMsgWindow, bool* aBool) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(m_popServer);
+ if (!server) return NS_ERROR_UNEXPECTED;
+
+ m_window = aMsgWindow;
+
+ nsCOMPtr<nsIMsgAccountManager> acctMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ nsCOMPtr<nsIMsgAccount> account;
+ NS_ENSURE_SUCCESS(rv, rv);
+ acctMgr->FindAccountForServer(server, getter_AddRefs(account));
+ if (account) account->GetKey(m_accountKey);
+
+ bool isLocked;
+ nsCOMPtr<nsISupports> supports =
+ do_QueryInterface(static_cast<nsIPop3Sink*>(this));
+
+ NS_ENSURE_STATE(m_folder);
+ m_folder->GetLocked(&isLocked);
+ if (!isLocked) {
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("BeginMailDelivery acquiring semaphore")));
+ m_folder->AcquireSemaphore(supports);
+ } else {
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("BeginMailDelivery folder locked")));
+ return NS_MSG_FOLDER_BUSY;
+ }
+ m_uidlDownload = uidlDownload;
+ if (!uidlDownload) FindPartialMessages();
+
+ m_folder->GetNumNewMessages(false, &m_numNewMessagesInFolder);
+
+#ifdef DEBUG
+ printf("Begin mail message delivery.\n");
+#endif
+ nsCOMPtr<nsIPop3Service> pop3Service(
+ do_GetService("@mozilla.org/messenger/popservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pop3Service->NotifyDownloadStarted(m_folder);
+ if (aBool) *aBool = true;
+ return NS_OK;
+}
+
+nsresult nsPop3Sink::EndMailDelivery(nsIPop3Protocol* protocol) {
+ CheckPartialMessages(protocol);
+
+ if (m_newMailParser) {
+ if (m_outFileStream) m_outFileStream->Flush(); // try this.
+ m_newMailParser->OnStopRequest(nullptr, NS_OK);
+ m_newMailParser->EndMsgDownload();
+ }
+ if (m_outFileStream) {
+ m_outFileStream->Close();
+ m_outFileStream = nullptr;
+ }
+
+ // tell the parser to mark the db valid *after* closing the mailbox.
+ if (m_newMailParser) m_newMailParser->UpdateDBFolderInfo();
+
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("Calling ReleaseFolderLock from EndMailDelivery")));
+ nsresult rv = ReleaseFolderLock();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "folder lock not released successfully");
+
+ bool filtersRun;
+ m_folder->CallFilterPlugins(nullptr,
+ &filtersRun); // ??? do we need msgWindow?
+ int32_t numNewMessagesInFolder;
+ // if filters have marked msgs read or deleted, the num new messages count
+ // will go negative by the number of messages marked read or deleted,
+ // so if we add that number to the number of msgs downloaded, that will give
+ // us the number of actual new messages.
+ m_folder->GetNumNewMessages(false, &numNewMessagesInFolder);
+ m_numNewMessages -= (m_numNewMessagesInFolder - numNewMessagesInFolder);
+ m_folder->SetNumNewMessages(
+ m_numNewMessages); // we'll adjust this for spam later
+ if (!filtersRun && m_numNewMessages > 0) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ m_folder->GetServer(getter_AddRefs(server));
+ if (server) {
+ server->SetPerformingBiff(true);
+ m_folder->SetBiffState(m_biffState);
+ server->SetPerformingBiff(false);
+ }
+ }
+ // note that size on disk has possibly changed.
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+ if (localFolder) (void)localFolder->RefreshSizeOnDisk();
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(m_popServer);
+ if (server) {
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ rv = server->GetFilterList(nullptr, getter_AddRefs(filterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (filterList) (void)filterList->FlushLogIfNecessary();
+ }
+
+ // fix for bug #161999
+ // we should update the summary totals for the folder (inbox)
+ // in case it's not the open folder
+ m_folder->UpdateSummaryTotals(true);
+
+ // check if the folder open in this window is not the current folder, and if
+ // it has new message, in which case we need to try to run the filter plugin.
+ if (m_newMailParser) {
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ m_newMailParser->GetMsgWindow(getter_AddRefs(msgWindow));
+ // this breaks down if it's biff downloading new mail because
+ // there's no msgWindow...
+ if (msgWindow) {
+ nsCOMPtr<nsIMsgFolder> openFolder;
+ (void)msgWindow->GetOpenFolder(getter_AddRefs(openFolder));
+ if (openFolder && openFolder != m_folder) {
+ // only call filter plugins if folder is a local folder, because only
+ // local folders get messages filtered into them synchronously by pop3.
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(openFolder);
+ if (localFolder) {
+ bool hasNew, isLocked;
+ (void)openFolder->GetHasNewMessages(&hasNew);
+ if (hasNew) {
+ // if the open folder is locked, we shouldn't run the spam filters
+ // on it because someone is using the folder. see 218433.
+ // Ideally, the filter plugin code would try to grab the folder lock
+ // and hold onto it until done, but that's more difficult and I
+ // think this will actually fix the problem.
+ openFolder->GetLocked(&isLocked);
+ if (!isLocked) openFolder->CallFilterPlugins(nullptr, &filtersRun);
+ }
+ }
+ }
+ }
+ }
+#ifdef DEBUG
+ printf("End mail message delivery.\n");
+#endif
+ nsCOMPtr<nsIPop3Service> pop3Service(
+ do_GetService("@mozilla.org/messenger/popservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pop3Service->NotifyDownloadCompleted(m_folder, m_numNewMessages);
+ return NS_OK;
+}
+
+nsresult nsPop3Sink::ReleaseFolderLock() {
+ nsresult result = NS_OK;
+ if (!m_folder) return result;
+ bool haveSemaphore;
+ nsCOMPtr<nsISupports> supports =
+ do_QueryInterface(static_cast<nsIPop3Sink*>(this));
+ result = m_folder->TestSemaphore(supports, &haveSemaphore);
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("ReleaseFolderLock haveSemaphore = %s"),
+ haveSemaphore ? "TRUE" : "FALSE"));
+
+ if (NS_SUCCEEDED(result) && haveSemaphore)
+ result = m_folder->ReleaseSemaphore(supports);
+ return result;
+}
+
+nsresult nsPop3Sink::AbortMailDelivery(nsIPop3Protocol* protocol) {
+ CheckPartialMessages(protocol);
+
+ // ### PS TODO - discard any new message?
+
+ if (m_outFileStream) {
+ m_outFileStream->Close();
+ m_outFileStream = nullptr;
+ }
+
+ /* tell the parser to mark the db valid *after* closing the mailbox.
+ we have truncated the inbox, so berkeley mailbox and msf file are in sync*/
+ if (m_newMailParser) m_newMailParser->UpdateDBFolderInfo();
+ MOZ_LOG(POP3LOGMODULE, mozilla::LogLevel::Debug,
+ (POP3LOG("Calling ReleaseFolderLock from AbortMailDelivery")));
+
+ nsresult rv = ReleaseFolderLock();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "folder lock not released successfully");
+
+#ifdef DEBUG
+ printf("Abort mail message delivery.\n");
+#endif
+ nsCOMPtr<nsIPop3Service> pop3Service(
+ do_GetService("@mozilla.org/messenger/popservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pop3Service->NotifyDownloadCompleted(m_folder, 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::IncorporateBegin(const char* uidlString, uint32_t flags) {
+#ifdef DEBUG
+ printf("Incorporate message begin:\n");
+ if (uidlString) printf("uidl string: %s\n", uidlString);
+#endif
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> newHdr;
+
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(m_popServer);
+ if (!server) return NS_ERROR_UNEXPECTED;
+
+ rv = server->GetMsgStore(getter_AddRefs(m_msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = m_msgStore->GetNewMsgOutputStream(m_folder, getter_AddRefs(newHdr),
+ getter_AddRefs(m_outFileStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // create a new mail parser
+ if (!m_newMailParser) m_newMailParser = new nsParseNewMailState;
+ NS_ENSURE_TRUE(m_newMailParser, NS_ERROR_OUT_OF_MEMORY);
+ if (m_uidlDownload) m_newMailParser->DisableFilters();
+
+ nsCOMPtr<nsIMsgFolder> serverFolder;
+ rv = GetServerFolder(getter_AddRefs(serverFolder));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = m_newMailParser->Init(serverFolder, m_folder, m_window, newHdr,
+ m_outFileStream);
+ // If we failed to initialize the parser, then just don't use it!!!
+ // We can still continue without one.
+
+ if (NS_FAILED(rv)) {
+ m_newMailParser = nullptr;
+ rv = NS_OK;
+ }
+
+ nsCString outputString(GetDummyEnvelope());
+ rv = WriteLineToMailbox(outputString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Write out account-key before UIDL so the code that looks for
+ // UIDL will find the account first and know it can stop looking
+ // once it finds the UIDL line.
+ if (!m_accountKey.IsEmpty()) {
+ outputString.AssignLiteral(HEADER_X_MOZILLA_ACCOUNT_KEY ": ");
+ outputString.Append(m_accountKey);
+ outputString.AppendLiteral(MSG_LINEBREAK);
+ rv = WriteLineToMailbox(outputString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (uidlString) {
+ outputString.AssignLiteral("X-UIDL: ");
+ outputString.Append(uidlString);
+ outputString.AppendLiteral(MSG_LINEBREAK);
+ rv = WriteLineToMailbox(outputString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // WriteLineToMailbox("X-Mozilla-Status: 8000" MSG_LINEBREAK);
+ char* statusLine = PR_smprintf(X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK, flags);
+ outputString.Assign(statusLine);
+ rv = WriteLineToMailbox(outputString);
+ PR_smprintf_free(statusLine);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = WriteLineToMailbox("X-Mozilla-Status2: 00000000"_ns MSG_LINEBREAK);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // leave space for 60 bytes worth of keys/tags
+ rv = WriteLineToMailbox(nsLiteralCString(X_MOZILLA_KEYWORDS));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::SetPopServer(nsIPop3IncomingServer* server) {
+ m_popServer = server;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::GetPopServer(nsIPop3IncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ NS_IF_ADDREF(*aServer = m_popServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPop3Sink::GetFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPop3Sink::SetFolder(nsIMsgFolder* aFolder) {
+ m_folder = aFolder;
+ return NS_OK;
+}
+
+nsresult nsPop3Sink::GetServerFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ if (m_popServer) {
+ // not sure what this is used for - might be wrong if we have a deferred
+ // account.
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer =
+ do_QueryInterface(m_popServer);
+ if (incomingServer) return incomingServer->GetRootFolder(aFolder);
+ }
+ *aFolder = nullptr;
+ return NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsPop3Sink::SetMsgsToDownload(uint32_t aNumMessages) {
+ m_numNewMessages = aNumMessages;
+ return NS_OK;
+}
+
+char* nsPop3Sink::GetDummyEnvelope(void) {
+ static char result[75];
+ char* ct;
+ time_t now = time((time_t*)0);
+#if defined(XP_WIN)
+ if (now < 0 || now > 0x7FFFFFFF) now = 0x7FFFFFFF;
+#endif
+ ct = ctime(&now);
+ PR_ASSERT(ct[24] == '\r' || ct[24] == '\n');
+ ct[24] = 0;
+ /* This value must be in ctime() format, with English abbreviations.
+ strftime("... %c ...") is no good, because it is localized. */
+ PL_strcpy(result, "From - ");
+ PL_strcpy(result + 7, ct);
+ PL_strcpy(result + 7 + 24, MSG_LINEBREAK);
+ return result;
+}
+
+nsresult nsPop3Sink::IncorporateWrite(const char* block, int32_t length) {
+ m_outputBuffer.Truncate();
+ if (!strncmp(block, "From ", 5)) m_outputBuffer.Assign('>');
+
+ m_outputBuffer.Append(block);
+
+ return WriteLineToMailbox(m_outputBuffer);
+}
+
+nsresult nsPop3Sink::WriteLineToMailbox(const nsACString& buffer) {
+ if (!buffer.IsEmpty()) {
+ uint32_t bufferLen = buffer.Length();
+ if (m_newMailParser)
+ m_newMailParser->HandleLine(buffer.BeginReading(), bufferLen);
+ // The following (!m_outFileStream etc) was added to make sure that we don't
+ // write somewhere where for some reason or another we can't write to and
+ // lose the messages See bug 62480
+ NS_ENSURE_TRUE(m_outFileStream, NS_ERROR_OUT_OF_MEMORY);
+
+ // To remove seeking to the end for each line to be written, remove the
+ // following line. See bug 1116055 for details.
+#define SEEK_TO_END
+#ifdef SEEK_TO_END
+ // seek to the end in case someone else has sought elsewhere in our stream.
+ nsCOMPtr<nsISeekableStream> seekableOutStream =
+ do_QueryInterface(m_outFileStream);
+
+ if (seekableOutStream) {
+ int64_t before_seek_pos;
+ nsresult rv2 = seekableOutStream->Tell(&before_seek_pos);
+ MOZ_ASSERT(NS_SUCCEEDED(rv2),
+ "seekableOutStream->Tell(&before_seek_pos) failed");
+
+ // XXX Handle error such as network error for remote file system.
+ seekableOutStream->Seek(nsISeekableStream::NS_SEEK_END, 0);
+
+ int64_t after_seek_pos;
+ nsresult rv3 = seekableOutStream->Tell(&after_seek_pos);
+ MOZ_ASSERT(NS_SUCCEEDED(rv3),
+ "seekableOutStream->Tell(&after_seek_pos) failed");
+
+ if (NS_SUCCEEDED(rv2) && NS_SUCCEEDED(rv3)) {
+ if (before_seek_pos != after_seek_pos) {
+ nsString folderName;
+ if (m_folder) m_folder->GetPrettyName(folderName);
+ // This merits a console message, it's poor man's telemetry.
+ MsgLogToConsole4(
+ u"Unexpected file position change detected"_ns +
+ (folderName.IsEmpty() ? EmptyString() : u" in folder "_ns) +
+ (folderName.IsEmpty() ? EmptyString() : folderName) +
+ u". "
+ "If you can reliably reproduce this, please report the "
+ "steps you used to dev-apps-thunderbird@lists.mozilla.org "
+ "or to bug 1308335 at bugzilla.mozilla.org. "
+ "Resolving this problem will allow speeding up message "
+ "downloads."_ns,
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ nsIScriptError::errorFlag);
+# ifdef DEBUG
+ // Debugging, see bug 1116055.
+ if (!folderName.IsEmpty()) {
+ fprintf(stderr,
+ "(seekdebug) WriteLineToMailbox() detected an unexpected "
+ "file position change in folder %s.\n",
+ NS_ConvertUTF16toUTF8(folderName).get());
+ } else {
+ fprintf(stderr,
+ "(seekdebug) WriteLineToMailbox() detected an unexpected "
+ "file position change.\n");
+ }
+ fprintf(stderr,
+ "(seekdebug) before_seek_pos=0x%016llx, "
+ "after_seek_pos=0x%016llx\n",
+ (long long unsigned int)before_seek_pos,
+ (long long unsigned int)after_seek_pos);
+# endif
+ }
+ }
+ }
+#endif
+
+ uint32_t bytesWritten;
+ nsresult rv =
+ m_outFileStream->Write(buffer.BeginReading(), bufferLen, &bytesWritten);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(bytesWritten == bufferLen, NS_ERROR_FAILURE);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::IncorporateComplete(nsIMsgWindow* aMsgWindow, int32_t aSize) {
+ if (m_buildMessageUri && !m_baseMessageUri.IsEmpty() && m_newMailParser &&
+ m_newMailParser->m_newMsgHdr) {
+ nsMsgKey msgKey;
+ m_newMailParser->m_newMsgHdr->GetMessageKey(&msgKey);
+ m_messageUri.Truncate();
+ nsBuildLocalMessageURI(m_baseMessageUri, msgKey, m_messageUri);
+ }
+
+ nsresult rv = WriteLineToMailbox(nsLiteralCString(MSG_LINEBREAK));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool leaveOnServer = false;
+ m_popServer->GetLeaveMessagesOnServer(&leaveOnServer);
+ // We need to flush the output stream, in case mail filters move
+ // the new message, which relies on all the data being flushed.
+ rv =
+ m_outFileStream->Flush(); // Make sure the message is written to the disk
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(m_newMailParser, "could not get m_newMailParser");
+ if (m_newMailParser) {
+ // PublishMsgHdr clears m_newMsgHdr, so we need a comptr to
+ // hold onto it.
+ nsCOMPtr<nsIMsgDBHdr> hdr = m_newMailParser->m_newMsgHdr;
+ NS_ASSERTION(hdr, "m_newMailParser->m_newMsgHdr wasn't set");
+ if (!hdr) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+
+ // If a header already exists for this message (for example, when
+ // getting a complete message when a partial exists), then update the new
+ // header from the old.
+ nsCOMPtr<nsIMsgDBHdr> oldMsgHdr;
+ if (!m_origMessageUri.IsEmpty() && localFolder) {
+ rv = GetMsgDBHdrFromURI(m_origMessageUri, getter_AddRefs(oldMsgHdr));
+ if (NS_SUCCEEDED(rv) && oldMsgHdr) {
+ localFolder->UpdateNewMsgHdr(oldMsgHdr, hdr);
+ }
+ }
+ m_msgStore->FinishNewMessage(m_outFileStream, hdr);
+ m_newMailParser->PublishMsgHeader(aMsgWindow);
+ m_newMailParser->ApplyForwardAndReplyFilter(aMsgWindow);
+ if (aSize) hdr->SetUint32Property("onlineSize", aSize);
+
+ if (oldMsgHdr) {
+ // We had the partial message, but got the full now.
+ nsCOMPtr<nsIMsgFolder> oldMsgFolder;
+ rv = oldMsgHdr->GetFolder(getter_AddRefs(oldMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString oldURI;
+ rv = oldMsgFolder->GetUriForMsg(oldMsgHdr, oldURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> newMsgFolder;
+ rv = hdr->GetFolder(getter_AddRefs(newMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString newURI;
+ rv = newMsgFolder->GetUriForMsg(hdr, newURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Delete old header before notifying.
+ nsCOMPtr<nsIMsgDatabase> db;
+ rv = m_folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = db->DeleteHeader(oldMsgHdr, nullptr, false, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ nsCOMPtr<nsISupportsString> origUri =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ origUri->SetData(NS_ConvertUTF8toUTF16(oldURI));
+ obsServ->NotifyObservers(origUri, "message-content-updated",
+ NS_ConvertUTF8toUTF16(newURI).get());
+ }
+ }
+ }
+
+#ifdef DEBUG
+ printf("Incorporate message complete.\n");
+#endif
+ nsCOMPtr<nsIPop3Service> pop3Service(
+ do_GetService("@mozilla.org/messenger/popservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pop3Service->NotifyDownloadProgress(m_folder, ++m_numMsgsDownloaded,
+ m_numNewMessages);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::IncorporateAbort(bool uidlDownload) {
+ NS_ENSURE_STATE(m_outFileStream);
+ nsresult rv = m_outFileStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_msgStore && m_newMailParser && m_newMailParser->m_newMsgHdr) {
+ m_msgStore->DiscardNewMessage(m_outFileStream,
+ m_newMailParser->m_newMsgHdr);
+ }
+#ifdef DEBUG
+ printf("Incorporate message abort.\n");
+#endif
+ return rv;
+}
+
+nsresult nsPop3Sink::SetBiffStateAndUpdateFE(uint32_t aBiffState,
+ int32_t numNewMessages,
+ bool notify) {
+ m_biffState = aBiffState;
+ if (m_newMailParser) numNewMessages -= m_newMailParser->m_numNotNewMessages;
+
+ if (notify && m_folder && numNewMessages > 0 &&
+ numNewMessages != m_numNewMessages &&
+ aBiffState == nsIMsgFolder::nsMsgBiffState_NewMail) {
+ m_folder->SetNumNewMessages(numNewMessages);
+ m_folder->SetBiffState(aBiffState);
+ }
+ m_numNewMessages = numNewMessages;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::GetBuildMessageUri(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_buildMessageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::SetBuildMessageUri(bool bVal) {
+ m_buildMessageUri = bVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::GetMessageUri(nsACString& messageUri) {
+ NS_ENSURE_TRUE(!m_messageUri.IsEmpty(), NS_ERROR_FAILURE);
+ messageUri = m_messageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::SetMessageUri(const nsACString& messageUri) {
+ m_messageUri = messageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::GetBaseMessageUri(nsACString& baseMessageUri) {
+ NS_ENSURE_TRUE(!m_baseMessageUri.IsEmpty(), NS_ERROR_FAILURE);
+ baseMessageUri = m_baseMessageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::SetBaseMessageUri(const nsACString& baseMessageUri) {
+ m_baseMessageUri = baseMessageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::GetOrigMessageUri(nsACString& aOrigMessageUri) {
+ aOrigMessageUri.Assign(m_origMessageUri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3Sink::SetOrigMessageUri(const nsACString& aOrigMessageUri) {
+ m_origMessageUri.Assign(aOrigMessageUri);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsPop3Sink.h b/comm/mailnews/local/src/nsPop3Sink.h
new file mode 100644
index 0000000000..9ea4e6b2ce
--- /dev/null
+++ b/comm/mailnews/local/src/nsPop3Sink.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsPop3Sink_h__
+#define nsPop3Sink_h__
+
+#include "nscore.h"
+#include "nsIPop3Sink.h"
+#include "nsIOutputStream.h"
+#include "prmem.h"
+#include "prio.h"
+#include "plstr.h"
+#include "prenv.h"
+#include "nsIMsgFolder.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+class nsParseNewMailState;
+class nsIMsgFolder;
+
+struct partialRecord {
+ partialRecord();
+ ~partialRecord();
+
+ nsCOMPtr<nsIMsgDBHdr> m_msgDBHdr;
+ nsCString m_uidl;
+};
+
+class nsPop3Sink : public nsIPop3Sink {
+ public:
+ nsPop3Sink();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPOP3SINK
+ nsresult GetServerFolder(nsIMsgFolder** aFolder);
+ nsresult FindPartialMessages();
+ void CheckPartialMessages(nsIPop3Protocol* protocol);
+
+ static char* GetDummyEnvelope(void);
+
+ protected:
+ virtual ~nsPop3Sink();
+ nsresult WriteLineToMailbox(const nsACString& buffer);
+ nsresult ReleaseFolderLock();
+
+ uint32_t m_biffState;
+ int32_t m_numNewMessages;
+ int32_t m_numNewMessagesInFolder;
+ int32_t m_numMsgsDownloaded;
+ bool m_senderAuthed;
+ nsCString m_outputBuffer;
+ nsCOMPtr<nsIPop3IncomingServer> m_popServer;
+ // Currently the folder we want to update about biff info
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ RefPtr<nsParseNewMailState> m_newMailParser;
+ nsCOMPtr<nsIOutputStream>
+ m_outFileStream; // the file we write to, which may be temporary
+ nsCOMPtr<nsIMsgPluggableStore> m_msgStore;
+ bool m_uidlDownload;
+ bool m_buildMessageUri;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCString m_messageUri;
+ nsCString m_baseMessageUri;
+ nsCString m_origMessageUri;
+ nsCString m_accountKey;
+ nsTArray<partialRecord*> m_partialMsgsArray;
+};
+
+#endif
diff --git a/comm/mailnews/local/src/nsPop3URL.cpp b/comm/mailnews/local/src/nsPop3URL.cpp
new file mode 100644
index 0000000000..138a4d9930
--- /dev/null
+++ b/comm/mailnews/local/src/nsPop3URL.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsPop3URL.h"
+#include "nsString.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prprf.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgAccountManager.h"
+#include "nsLocalMailFolder.h"
+#include "nsPop3Sink.h"
+
+#define NS_POP3URL_CID \
+ { \
+ 0xea1b0a11, 0xe6f4, 0x11d2, { \
+ 0x80, 0x70, 0x0, 0x60, 0x8, 0x12, 0x8c, 0x4e \
+ } \
+ }
+static NS_DEFINE_CID(kPop3UrlCID, NS_POP3URL_CID);
+
+nsPop3URL::nsPop3URL() : nsMsgMailNewsUrl() {}
+
+nsPop3URL::~nsPop3URL() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsPop3URL, nsMsgMailNewsUrl, nsIPop3URL)
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIPop3URL specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsPop3URL::SetPop3Sink(nsIPop3Sink* aPop3Sink) {
+ if (aPop3Sink) m_pop3Sink = aPop3Sink;
+ return NS_OK;
+}
+
+nsresult nsPop3URL::GetPop3Sink(nsIPop3Sink** aPop3Sink) {
+ if (aPop3Sink) {
+ *aPop3Sink = m_pop3Sink;
+ NS_IF_ADDREF(*aPop3Sink);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3URL::GetMessageUri(nsACString& aMessageUri) {
+ if (m_messageUri.IsEmpty()) return NS_ERROR_NULL_POINTER;
+ aMessageUri = m_messageUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPop3URL::SetMessageUri(const nsACString& aMessageUri) {
+ m_messageUri = aMessageUri;
+ return NS_OK;
+}
+
+nsresult nsPop3URL::BuildPop3Url(const char* urlSpec, nsIMsgFolder* inbox,
+ nsIPop3IncomingServer* server,
+ nsIUrlListener* aUrlListener, nsIURI** aUrl,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ nsPop3Sink* pop3Sink = new nsPop3Sink();
+
+ pop3Sink->SetPopServer(server);
+ pop3Sink->SetFolder(inbox);
+
+ // now create a pop3 url and a protocol instance to run the url....
+ nsCOMPtr<nsIPop3URL> pop3Url = do_CreateInstance(kPop3UrlCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ pop3Url->SetPop3Sink(pop3Sink);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl;
+ rv = pop3Url->QueryInterface(NS_GET_IID(nsIMsgMailNewsUrl),
+ getter_AddRefs(mailnewsurl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mailnewsurl->SetSpecInternal(nsDependentCString(urlSpec));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aUrlListener) mailnewsurl->RegisterListener(aUrlListener);
+ if (aMsgWindow) mailnewsurl->SetMsgWindow(aMsgWindow);
+
+ mailnewsurl.forget(aUrl);
+
+ return rv;
+}
+
+nsresult nsPop3URL::NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsAutoCString folderUri(aSpec);
+ int32_t offset = folderUri.FindChar('?');
+ if (offset != kNotFound) folderUri.SetLength(offset);
+
+ // Hold onto the string until it goes out of scope.
+ const nsPromiseFlatCString& flat = PromiseFlatCString(aSpec);
+ const char* uidl = PL_strstr(flat.get(), "uidl=");
+ NS_ENSURE_TRUE(uidl, NS_ERROR_FAILURE);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderUri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ nsLocalFolderScanState folderScanState;
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder);
+ nsCOMPtr<nsIMailboxUrl> mailboxUrl = do_QueryInterface(aBaseURI);
+
+ if (mailboxUrl && localFolder) {
+ rv = localFolder->GetFolderScanState(&folderScanState);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsMsgKey msgKey;
+ mailboxUrl->GetMessageKey(&msgKey);
+ folder->GetMessageHeader(msgKey, getter_AddRefs(msgHdr));
+ // we do this to get the account key
+ if (msgHdr) localFolder->GetUidlFromFolder(&folderScanState, msgHdr);
+ if (!folderScanState.m_accountKey.IsEmpty()) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (accountManager) {
+ nsCOMPtr<nsIMsgAccount> account;
+ accountManager->GetAccount(folderScanState.m_accountKey,
+ getter_AddRefs(account));
+ if (account) account->GetIncomingServer(getter_AddRefs(server));
+ }
+ }
+ }
+
+ if (!server) rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPop3IncomingServer> popServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ nsCString username;
+ server->GetHostName(hostname);
+ server->GetUsername(username);
+
+ int32_t port;
+ server->GetPort(&port);
+ if (port == -1) port = nsIPop3URL::DEFAULT_POP3_PORT;
+
+ // We need to escape the username before calling SetUsername() because it may
+ // contain characters like / % or @. GetUsername() will unescape the username.
+ nsCString escapedUsername;
+ MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+
+ nsAutoCString popSpec("pop://");
+ popSpec += escapedUsername;
+ popSpec += "@";
+ popSpec += hostname;
+ popSpec += ":";
+ popSpec.AppendInt(port);
+ popSpec += "?";
+ popSpec += uidl;
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(folder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> newUri;
+ rv = BuildPop3Url(popSpec.get(), folder, popServer, urlListener,
+ getter_AddRefs(newUri), nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(newUri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mailnewsurl->SetUsernameInternal(escapedUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPop3URL> popurl = do_QueryInterface(newUri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString messageUri(aSpec);
+ if (!strncmp(messageUri.get(), "mailbox:", 8))
+ messageUri.Replace(0, 8, "mailbox-message:");
+ offset = messageUri.Find("?number=");
+ if (offset != kNotFound) messageUri.Replace(offset, 8, "#");
+ offset = messageUri.FindChar('&');
+ if (offset != kNotFound) messageUri.SetLength(offset);
+ popurl->SetMessageUri(messageUri);
+ nsCOMPtr<nsIPop3Sink> pop3Sink;
+ rv = popurl->GetPop3Sink(getter_AddRefs(pop3Sink));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ pop3Sink->SetBuildMessageUri(true);
+
+ newUri.forget(_retval);
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsPop3URL.h b/comm/mailnews/local/src/nsPop3URL.h
new file mode 100644
index 0000000000..44e2ce6dc9
--- /dev/null
+++ b/comm/mailnews/local/src/nsPop3URL.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsPop3URL_h__
+#define nsPop3URL_h__
+
+#include "nsIPop3URL.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsCOMPtr.h"
+
+class nsPop3URL : public nsIPop3URL, public nsMsgMailNewsUrl {
+ public:
+ NS_DECL_NSIPOP3URL
+ nsPop3URL();
+ static nsresult NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+ NS_DECL_ISUPPORTS_INHERITED
+
+ protected:
+ virtual ~nsPop3URL();
+
+ nsCString m_messageUri;
+
+ /* Pop3 specific event sinks */
+ nsCOMPtr<nsIPop3Sink> m_pop3Sink;
+
+ // convenience function to make constructing of the pop3 url easier...
+ static nsresult BuildPop3Url(const char* urlSpec, nsIMsgFolder* inbox,
+ nsIPop3IncomingServer*,
+ nsIUrlListener* aUrlListener, nsIURI** aUrl,
+ nsIMsgWindow* aMsgWindow);
+};
+
+#endif // nsPop3URL_h__
diff --git a/comm/mailnews/local/src/nsRssIncomingServer.cpp b/comm/mailnews/local/src/nsRssIncomingServer.cpp
new file mode 100644
index 0000000000..005e202c14
--- /dev/null
+++ b/comm/mailnews/local/src/nsRssIncomingServer.cpp
@@ -0,0 +1,248 @@
+/* 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/. */
+
+#include "nsRssIncomingServer.h"
+#include "nsMsgFolderFlags.h"
+#include "nsINewsBlogFeedDownloader.h"
+#include "nsIFile.h"
+#include "nsIMsgFolderNotificationService.h"
+
+#include "nsIMsgLocalMailFolder.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+
+nsrefcnt nsRssIncomingServer::gInstanceCount = 0;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsRssIncomingServer, nsMsgIncomingServer,
+ nsIRssIncomingServer, nsIMsgFolderListener,
+ nsILocalMailIncomingServer)
+
+nsRssIncomingServer::nsRssIncomingServer() {
+ m_canHaveFilters = true;
+
+ if (gInstanceCount == 0) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolderNotificationService> notifyService =
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ notifyService->AddListener(
+ this, nsIMsgFolderNotificationService::folderAdded |
+ nsIMsgFolderNotificationService::folderDeleted |
+ nsIMsgFolderNotificationService::folderMoveCopyCompleted |
+ nsIMsgFolderNotificationService::folderRenamed);
+ }
+
+ gInstanceCount++;
+}
+
+nsRssIncomingServer::~nsRssIncomingServer() {
+ gInstanceCount--;
+
+ if (gInstanceCount == 0) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolderNotificationService> notifyService =
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1", &rv);
+ if (NS_SUCCEEDED(rv)) notifyService->RemoveListener(this);
+ }
+}
+
+nsresult nsRssIncomingServer::FillInDataSourcePath(
+ const nsAString& aDataSourceName, nsIFile** aLocation) {
+ nsresult rv;
+ // Get the local path for this server.
+ nsCOMPtr<nsIFile> localFile;
+ rv = GetLocalPath(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Append the name of the subscriptions data source.
+ rv = localFile->Append(aDataSourceName);
+ localFile.forget(aLocation);
+ return rv;
+}
+
+// nsIRSSIncomingServer methods
+NS_IMETHODIMP nsRssIncomingServer::GetSubscriptionsPath(nsIFile** aLocation) {
+ return FillInDataSourcePath(u"feeds.json"_ns, aLocation);
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetFeedItemsPath(nsIFile** aLocation) {
+ return FillInDataSourcePath(u"feeditems.json"_ns, aLocation);
+}
+
+NS_IMETHODIMP nsRssIncomingServer::CreateDefaultMailboxes() {
+ // For Feeds, all we have is Trash.
+ return CreateLocalFolder(u"Trash"_ns);
+}
+
+NS_IMETHODIMP nsRssIncomingServer::SetFlagsOnDefaultMailboxes() {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(rootFolder, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ localFolder->SetFlagsOnDefaultMailboxes(nsMsgFolderFlags::Trash);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) {
+ // Get the account root (server) folder and pass it on.
+ nsCOMPtr<nsIMsgFolder> rootRSSFolder;
+ GetRootMsgFolder(getter_AddRefs(rootRSSFolder));
+ nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(rootRSSFolder);
+ nsresult rv;
+ bool isBiff = true;
+ nsCOMPtr<nsINewsBlogFeedDownloader> rssDownloader =
+ do_GetService("@mozilla.org/newsblog-feed-downloader;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rssDownloader->DownloadFeed(rootRSSFolder, urlListener, isBiff, aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetNewMail(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener,
+ nsIMsgFolder* aFolder,
+ nsIURI** _retval) {
+ // Pass the selected folder on to the downloader.
+ if (_retval) {
+ *_retval = nullptr;
+ }
+ NS_ENSURE_ARG_POINTER(aFolder);
+ nsresult rv;
+ bool isBiff = false;
+ nsCOMPtr<nsINewsBlogFeedDownloader> rssDownloader =
+ do_GetService("@mozilla.org/newsblog-feed-downloader;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rssDownloader->DownloadFeed(aFolder, aUrlListener, isBiff, aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetAccountManagerChrome(nsAString& aResult) {
+ aResult.AssignLiteral("am-newsblog.xhtml");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetOfflineSupportLevel(
+ int32_t* aSupportLevel) {
+ NS_ENSURE_ARG_POINTER(aSupportLevel);
+ *aSupportLevel = OFFLINE_SUPPORT_LEVEL_NONE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetSupportsDiskSpace(
+ bool* aSupportsDiskSpace) {
+ NS_ENSURE_ARG_POINTER(aSupportsDiskSpace);
+ *aSupportsDiskSpace = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) {
+ NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff);
+ // For Feed folders, we don't require a password.
+ *aServerRequiresPasswordForBiff = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::GetCanSearchMessages(
+ bool* canSearchMessages) {
+ NS_ENSURE_ARG_POINTER(canSearchMessages);
+ *canSearchMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgAdded(nsIMsgDBHdr* aMsg) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgsClassified(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs, bool aJunkProcessed,
+ bool aTraitProcessed) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgsJunkStatusChanged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgsDeleted(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgsMoveCopyCompleted(
+ bool aMove, const nsTArray<RefPtr<nsIMsgDBHdr>>& aSrcMsgs,
+ nsIMsgFolder* aDestFolder, const nsTArray<RefPtr<nsIMsgDBHdr>>& aDestMsgs) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgKeyChanged(nsMsgKey aOldKey,
+ nsIMsgDBHdr* aNewHdr) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderAdded(nsIMsgFolder* aFolder) {
+ // Nothing to do. Not necessary for new folder adds, as a new folder never
+ // has a subscription.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderDeleted(nsIMsgFolder* aFolder) {
+ // Not necessary for folder deletes, which are move to Trash and handled by
+ // movecopy. Virtual folder or trash folder deletes send a folderdeleted,
+ // but these should have no subscriptions already.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderMoveCopyCompleted(
+ bool aMove, nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDestFolder) {
+ return FolderChanged(aDestFolder, aSrcFolder, (aMove ? "move" : "copy"));
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderRenamed(nsIMsgFolder* aOrigFolder,
+ nsIMsgFolder* aNewFolder) {
+ return FolderChanged(aNewFolder, aOrigFolder, "rename");
+}
+
+nsresult nsRssIncomingServer::FolderChanged(nsIMsgFolder* aFolder,
+ nsIMsgFolder* aOrigFolder,
+ const char* aAction) {
+ if (!aFolder) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsINewsBlogFeedDownloader> rssDownloader =
+ do_GetService("@mozilla.org/newsblog-feed-downloader;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rssDownloader->UpdateSubscriptionsDS(aFolder, aOrigFolder, aAction);
+ return rv;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::MsgUnincorporatedMoved(
+ nsIMsgFolder* srcFolder, nsIMsgDBHdr* msg) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderCompactStart(nsIMsgFolder* folder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderCompactFinish(nsIMsgFolder* folder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssIncomingServer::FolderReindexTriggered(
+ nsIMsgFolder* folder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsRssIncomingServer::GetSortOrder(int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = 400000000;
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsRssIncomingServer.h b/comm/mailnews/local/src/nsRssIncomingServer.h
new file mode 100644
index 0000000000..337f1ce95d
--- /dev/null
+++ b/comm/mailnews/local/src/nsRssIncomingServer.h
@@ -0,0 +1,47 @@
+/* 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/. */
+
+#ifndef __nsRssIncomingServer_h
+#define __nsRssIncomingServer_h
+
+#include "mozilla/Attributes.h"
+#include "nsIRssIncomingServer.h"
+#include "nsILocalMailIncomingServer.h"
+#include "nsMsgIncomingServer.h"
+#include "nsIMsgFolderListener.h"
+#include "nsMailboxServer.h"
+
+class nsRssIncomingServer : public nsMailboxServer,
+ public nsIRssIncomingServer,
+ public nsILocalMailIncomingServer,
+ public nsIMsgFolderListener
+
+{
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIRSSINCOMINGSERVER
+ NS_DECL_NSILOCALMAILINCOMINGSERVER
+ NS_DECL_NSIMSGFOLDERLISTENER
+
+ NS_IMETHOD GetOfflineSupportLevel(int32_t* aSupportLevel) override;
+ NS_IMETHOD GetSupportsDiskSpace(bool* aSupportsDiskSpace) override;
+ NS_IMETHOD GetAccountManagerChrome(nsAString& aResult) override;
+ NS_IMETHOD PerformBiff(nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) override;
+ NS_IMETHOD GetCanSearchMessages(bool* canSearchMessages) override;
+ NS_IMETHOD GetSortOrder(int32_t* aSortOrder) override;
+
+ nsRssIncomingServer();
+
+ protected:
+ virtual ~nsRssIncomingServer();
+ nsresult FolderChanged(nsIMsgFolder* aFolder, nsIMsgFolder* aOrigFolder,
+ const char* aAction);
+ nsresult FillInDataSourcePath(const nsAString& aDataSourceName,
+ nsIFile** aLocation);
+ static nsrefcnt gInstanceCount;
+};
+
+#endif /* __nsRssIncomingServer_h */
diff --git a/comm/mailnews/local/src/nsRssService.cpp b/comm/mailnews/local/src/nsRssService.cpp
new file mode 100644
index 0000000000..77cf04365d
--- /dev/null
+++ b/comm/mailnews/local/src/nsRssService.cpp
@@ -0,0 +1,113 @@
+/* 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/. */
+
+#include "nsRssService.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsIProperties.h"
+#include "nsServiceManagerUtils.h"
+
+nsRssService::nsRssService() {}
+
+nsRssService::~nsRssService() {}
+
+NS_IMPL_ISUPPORTS(nsRssService, nsIRssService, nsIMsgProtocolInfo)
+
+NS_IMETHODIMP nsRssService::GetDefaultLocalPath(nsIFile** aDefaultLocalPath) {
+ NS_ENSURE_ARG_POINTER(aDefaultLocalPath);
+ *aDefaultLocalPath = nullptr;
+
+ nsCOMPtr<nsIFile> localFile;
+ nsCOMPtr<nsIProperties> dirService(
+ do_GetService("@mozilla.org/file/directory_service;1"));
+ if (!dirService) return NS_ERROR_FAILURE;
+ dirService->Get(NS_APP_MAIL_50_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+ if (!localFile) return NS_ERROR_FAILURE;
+
+ bool exists;
+ nsresult rv = localFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = localFile->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ if (NS_FAILED(rv)) return rv;
+
+ localFile.forget(aDefaultLocalPath);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::SetDefaultLocalPath(nsIFile* aDefaultLocalPath) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssService::GetServerIID(nsIID** aServerIID) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssService::GetRequiresUsername(bool* aRequiresUsername) {
+ NS_ENSURE_ARG_POINTER(aRequiresUsername);
+ *aRequiresUsername = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetPreflightPrettyNameWithEmailAddress(
+ bool* aPreflightPrettyNameWithEmailAddress) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsRssService::GetCanDelete(bool* aCanDelete) {
+ NS_ENSURE_ARG_POINTER(aCanDelete);
+ *aCanDelete = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetCanLoginAtStartUp(bool* aCanLoginAtStartUp) {
+ NS_ENSURE_ARG_POINTER(aCanLoginAtStartUp);
+ *aCanLoginAtStartUp = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetCanDuplicate(bool* aCanDuplicate) {
+ NS_ENSURE_ARG_POINTER(aCanDuplicate);
+ *aCanDuplicate = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetDefaultServerPort(bool isSecure,
+ int32_t* _retval) {
+ *_retval = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetCanGetMessages(bool* aCanGetMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetMessages);
+ *aCanGetMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetCanGetIncomingMessages(
+ bool* aCanGetIncomingMessages) {
+ NS_ENSURE_ARG_POINTER(aCanGetIncomingMessages);
+ *aCanGetIncomingMessages = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetDefaultDoBiff(bool* aDefaultDoBiff) {
+ NS_ENSURE_ARG_POINTER(aDefaultDoBiff);
+ // by default, do biff for RSS feeds
+ *aDefaultDoBiff = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetShowComposeMsgLink(bool* aShowComposeMsgLink) {
+ NS_ENSURE_ARG_POINTER(aShowComposeMsgLink);
+ *aShowComposeMsgLink = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsRssService::GetFoldersCreatedAsync(bool* aAsyncCreation) {
+ NS_ENSURE_ARG_POINTER(aAsyncCreation);
+ *aAsyncCreation = false;
+ return NS_OK;
+}
diff --git a/comm/mailnews/local/src/nsRssService.h b/comm/mailnews/local/src/nsRssService.h
new file mode 100644
index 0000000000..490351d61e
--- /dev/null
+++ b/comm/mailnews/local/src/nsRssService.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsRssService_h___
+#define nsRssService_h___
+
+#include "nsIRssService.h"
+#include "nsIMsgProtocolInfo.h"
+
+class nsRssService : public nsIMsgProtocolInfo, public nsIRssService {
+ public:
+ nsRssService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIRSSSERVICE
+ NS_DECL_NSIMSGPROTOCOLINFO
+
+ private:
+ virtual ~nsRssService();
+};
+
+#endif /* nsRssService_h___ */
diff --git a/comm/mailnews/local/test/moz.build b/comm/mailnews/local/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/local/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/local/test/unit/data/dot b/comm/mailnews/local/test/unit/data/dot
new file mode 100644
index 0000000000..adab8ff515
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/dot
@@ -0,0 +1,10 @@
+From - Tue Jan 31 11:44:20 2012
+Subject: Dot at the line head
+Date: Tue, 21 Jan 2012 11:44:20 +0000
+Mime-Version: 1.0
+Content-Type: text/html; charset=iso-8859-1; format=flowed
+
+.
+. This is a line starting with a dot.
+.
+
diff --git a/comm/mailnews/local/test/unit/data/invalid_mozilla_keys.eml b/comm/mailnews/local/test/unit/data/invalid_mozilla_keys.eml
new file mode 100644
index 0000000000..611029bb7d
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/invalid_mozilla_keys.eml
@@ -0,0 +1,5 @@
+Date: Tue, 21 Jan 2012 11:44:20 +0000
+X-Mozilla-Keys:
+
+
+This mail has invalid X-Mozilla-Keys header.
diff --git a/comm/mailnews/local/test/unit/data/mailformed_recipients.eml b/comm/mailnews/local/test/unit/data/mailformed_recipients.eml
new file mode 100644
index 0000000000..3a3b9a874a
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/mailformed_recipients.eml
@@ -0,0 +1,66 @@
+Return-Path: <notifier.mars@krw.rzd>
+Received: from [10.95.185.198] (HELO mars)
+ by cgbe1.sf.icc.krsk.krw.rzd (CommuniGate Pro SMTP 5.4.2)
+ with ESMTP id 197561; Sat, 03 Dec 2011 17:07:41 +0400
+Subject: CS-MARS Incident Notification (red, Rule Name: System Rule: DoS: Network - Success Likely)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: 7bit
+Date: Sat, 3 Dec 2011 17:07:41 +0400
+From: notifier.mars@krw.rzd
+Message-Id: <1322917661.14@mars>
+To: LyukshinRA@krw.rzd,
+ biakus@krw.rzd,
+
+
+The following incident occurred on "mars"
+
+Start time: Sat Dec 3 16:52:33 2011
+End time: Sat Dec 3 17:07:35 2011
+Fired Rule Id: 3354883
+Fired Rule: System Rule: DoS: Network - Success Likely
+Incident Id: 24500896822
+Incident Severity:red
+
+Top 3 src-dest address pairs sorted by severity and count (showing 3 of 319):
+1. N/A -> 10.88.21.45 Severity: red Count: 16
+2. 10.89.234.223 -> N/A Severity: red Count: 16
+3. 10.144.58.124 -> 10.92.23.37 Severity: green Count: 1
+
+Top 3 src ip's address sorted by severity and count (showing 3 of 10):
+1. N/A -> Severity: red Count: 16
+2. 10.89.234.223 -> Severity: red Count: 16
+3. 10.132.51.53 -> Severity: green Count: 48
+
+Top 3 dest ip's address sorted by severity and count (showing 3 of 319):
+1. 10.88.21.45 -> Severity: red Count: 16
+2. N/A -> Severity: red Count: 16
+3. 10.92.23.37 -> Severity: green Count: 1
+
+Top 3 dest TCP/UDP ports sorted by severity and count (showing 0 of 0):
+
+Top 3 event types sorted by severity and count (showing 2 of 2):
+1. Sudden increase of traffic to a port Severity: red Count: 32
+2. Deny packet due to security policy Severity: green Count: 317
+
+Top 3 reporting devices sorted by count (showing 3 of 11):
+1. KRW-EXP3 Count: 152
+2. kzi-spd-asa.secadm.m.krw.rzd Count: 151
+3. mars Count: 32
+
+
+
+For more details about this incident please go to:
+ https://mars/Incidents/IncidentDetails.jsp?Incident_Id=24500896822
+ https://mars.secadm.m.krw.rzd/Incidents/IncidentDetails.jsp?Incident_Id=24500896822
+ https://mars.krw.rzd/Incidents/IncidentDetails.jsp?Incident_Id=24500896822
+ https://10.95.185.198/Incidents/IncidentDetails.jsp?Incident_Id=24500896822
+ https://10.95.185.130/Incidents/IncidentDetails.jsp?Incident_Id=24500896822
+
+For all incidents occurred recently please go to:
+ https://mars/Incidents/
+ https://mars.secadm.m.krw.rzd/Incidents/
+ https://mars.krw.rzd/Incidents/
+ https://10.95.185.198/Incidents/
+ https://10.95.185.130/Incidents/
+
diff --git a/comm/mailnews/local/test/unit/data/mailformed_subject.eml b/comm/mailnews/local/test/unit/data/mailformed_subject.eml
new file mode 100644
index 0000000000..b4cae27827
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/mailformed_subject.eml
@@ -0,0 +1,1934 @@
+From - Wed Nov 09 20:53:04 2011
+X-Account-Key: account6
+X-UIDL: AK9oUtQAAQncTrq1IQTAI2gZvkw
+X-Mozilla-Status: 1003
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+X-Apparently-To: milonguero66@yahoo.de via 212.82.104.175; Wed, 09 Nov 2011 17:15:13 +0000
+Received-SPF: none (domain of yahoo.de does not designate permitted sender hosts)
+X-YMailISG: ki6Cgm0WLDt5_yRciWKCGRrm_vnjeHjNnC0KO465U1eaOZyt
+ pivGwlOFQRjp2s2Ygsm5JQ9e3mXBbof5vgzmR3tyuYMCf7EIK7zbRrnlOVCi
+ KfWabfqU2cTV2h7Ic5RTSuENb5nJQIXwVJKAsZspB63KFOJ4tESpGHVHOXLl
+ DcjkJJPSCgA23jDgVh_2XoL__I9xRfVC93IJkNBx_k1iWhz7faHweH.AS26L
+ E2Z0o97aS7U.UR3JJPtTZYjhPyw5ncRZefyDpP6XXTHGjbXp7TkaopFfZ1t5
+ MO0QFlOSCAXq30VBg.ViDBIz6nfSetzflBHtgCRA2k6ovl.KLOPN4ZOvV3Io
+ YikmLnc3EMQKKwuGytzvr0qvB2W_dARwmdVAkWdpAFqoGQIRsuuK.vEPbQgt
+ o.4_7krO5H.E5yTSKs3AxRpSgu7Tpqo1USzeN6VfSp4XSsqoEXf6jPwo9COl
+ cRiDNC1ofVzcPDp1FP2A6ihTDsi64ZKhrtfRXBPSAWs0DbNsjrbO2qAG8iZi
+ cmO1Hxg_8F5BxOolKIkzc3ykR7Ou.M0ebP9OusURSxfgLLmUo4Mw2asmWCTZ
+ umthtzDW1Zf76E.flIaVKlP0btDZqJpvTVyg7KN0cWRMtFdC4ybvQLyVxSK2
+ KWQ7f232DAlH.JUbIuJKVnyuTVAlZBPoZue8AQi3P53J2ZwC7XB6VXahgqgn
+ xKlZiD.zUREwUeUCJQBMSZk7re7TrCoPwXfE.n3tc8NjtmIzeQRPxAPdl1Ka
+ 8fwMfibbBAAOS9SmeqayScRVPjvNyidH1t5BNk0YWc6EzyxgLPvru65hvXnZ
+ W3CayiMl7XKxeIDctxiRKlbUhJ_QzgVGMunN_jynWts_1vIIBvGUbaP9EFjZ
+ PAcNoNwSiTnSlQ3cc4aLCaGBUG.Gq8e5u3zUu_L0HRvAhIaZcBvi_xiWjDjp
+ kRXylog9074fulAzY_7Zlbq2.xa6AdUR7PrOzlS3QMblu5yc4_hlpq8KPYbM
+ voqzmi6JIZ8dAKTp4BAVFr7Q63UlHJWUjsdlJ1uIds.dl1OxJiTgGfEh.O1g
+ tyh3zY_hE3z7zRW7LFzEqoNclh8WYNie0j7kDHYfXbR1klg49y2mK_8jJRFA
+ nAOeUm71bWXPP65HaHm2r8ti.czc2g_CxzuReSyWyhdJNvLe5YTwpVZqCcbS
+ rhx7A7jtQiNf
+X-Originating-IP: [217.146.183.238]
+Authentication-Results: mta1087.mail.ird.yahoo.com from=yahoo.de; domainkeys=neutral (no sig); from=yahoo.de; dkim=pass (ok)
+Received: from 127.0.0.1 (HELO nm8-vm0.bullet.mail.ukl.yahoo.com) (217.146.183.238)
+ by mta1087.mail.ird.yahoo.com with SMTP; Wed, 09 Nov 2011 17:15:13 +0000
+Received: from [217.146.183.212] by nm8.bullet.mail.ukl.yahoo.com with NNFMP; 09 Nov 2011 17:15:12 -0000
+Received: from [77.238.184.80] by tm5.bullet.mail.ukl.yahoo.com with NNFMP; 09 Nov 2011 17:15:12 -0000
+Received: from [127.0.0.1] by smtp149.mail.ukl.yahoo.com with NNFMP; 09 Nov 2011 17:15:12 -0000
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.de; s=s1024; t=1320858912; bh=HOGFslJAcWp5sxJ2b+wkpi2WeYP/qYljF1212byJva8=; h=X-Yahoo-Newman-Id:X-Yahoo-Newman-Property:X-YMail-OSG:X-Yahoo-SMTP:Received:MIME-Version:Message-Id:Date:Content-Type:X-Mailer:From:X-FID:X-Priority:To:Subject; b=lb3j0vTYp/7M5Mgxy6xy1mn0rkgwxme0XA3iGfZrWEiQ3i3VNFsspvEXyWjerEKVcggZZyvH3Wjgc5U8HxxG73FrdEHjunpYEMy7Vp0sA4L+s+pndQdqI710P3SaDP21+VSA8HhKO03X3k4CVy5od2iSUGqdDa/hcJwYPgPBhZM=
+X-Yahoo-Newman-Id: 289474.41256.bm@smtp149.mail.ukl.yahoo.com
+X-YMail-OSG: QnILw7kVM1ljfDVZBNFZdSZEHkFwvGT1RdUyd.LgwTYAaBt
+ YMNKhbRWIEnXuXKkEHGuj_Sfc8hluz9X9kIlgz9EsdAdrB_gcA85hCzCU3_v
+ gpoxkri1I9D_2rR79yzbCR7Pi6ltYwTnticz1f40ZsonLoDAsGloPD26p_sp
+ xjH2379iUBy5m2tdBG4LP7C64twSNJVSivUYEQakBGKdvA7lNOsTZl.Gseh.
+ Qr8bS65cSwyKFKsC8jQGsowP7r_flK.gD.YJf7BKKCxal.SIOjAjIwZiDF2g
+ TufXZcJKOguH.kUeolYYe1DfS1qJ1WHYCkUKXZ8eloDp5N4id8XsNsl.IYdp
+ dXjQ71ZvRf6x0vMHEO1ykyWT45MpmKClZzsa_up1GmYM.mJ0fYPawsVDKLPj
+ vLa67ErmwS71fflMajhNBi9KZaCxLLBrXNvL0dprlyLG6rIpm4h9EvihcZOq
+ dARgemVEvLAtwAM50Et4NRKktpDhYBUr97kp.7dN0xTOzvqKqU3.2rfTFJ42
+ Kil6kccIv3e3njoOaQ1y.GQt6e_WjvtaDSVHRB3wJDN_8b7SE3nKkhkfMx.R
+ .ZEmZ4ywN8zixGYBOJrzH
+X-Yahoo-SMTP: 6uAm6CCswBCasj3_KVt4BWWJsLuOj9UNkP4D
+Received: from milkyway (pink_amaryllis@89.204.137.94 with login)
+ by smtp149.mail.ukl.yahoo.com with SMTP; 09 Nov 2011 17:15:06 +0000 GMT
+MIME-Version: 1.0
+Message-Id: <4EBAB50C.00000E.03032@MILKYWAY>
+Date: Wed, 9 Nov 2011 18:14:52 +0100 (Westeuropäische Normalzeit)
+Content-Type: Multipart/related;
+ charset="iso-8859-1";
+ type="multipart/alternative";
+ boundary="------------Boundary-00=_S8LEXFP0000000000000"
+X-Mailer: IncrediMail (6274918)
+From: "Eva C. Hammel" <pink_amaryllis@yahoo.de>
+X-FID: 1D3F6781-6356-4D2B-9E2A-C2FC357A0AB3
+X-Priority: 3
+To: <milonguero66@yahoo.de>
+Subject: =?iso-8859-1?B?UmVjaG51bmcgQW535Gx0aW4=?=
+
+
+
+--------------Boundary-00=_S8LEXFP0000000000000
+Content-Type: Multipart/Alternative;
+ boundary="------------Boundary-00=_S8LESPT1VA4000000000"
+
+
+--------------Boundary-00=_S8LESPT1VA4000000000
+Content-Type: Text/Plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+ist bearbeitet;=0D
+Ku=DF!
+--------------Boundary-00=_S8LESPT1VA4000000000
+Content-Type: Text/HTML;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<HTML><HEAD>
+<META content=3D"text/html; charset=3Diso-8859-1" http-equiv=3DContent-Ty=
+pe>
+<META name=3DGENERATOR content=3D"IncrediMail 1.0">
+<STYLE>=0Av\:* {behavior:url (#default#vml);}=0A</STYLE>
+
+<!--IncrdiXMLRemarkStart>
+<IncrdiX-Info>
+<X-FID>1D3F6781-6356-4D2B-9E2A-C2FC357A0AB3</X-FID>
+<X-FVER>4,000000</X-FVER>
+<X-FIT>Letter</X-FIT>
+<X-FILE>Letter\jack-o-lantern.imf</X-FILE>
+<X-FCOL>Halloween</X-FCOL>
+<X-FCAT>Holidays</X-FCAT>
+<X-FDIS>Jack-O-Lantern</X-FDIS>
+<X-Extensions>SU1CTDEsNDYsgUmBSYUolTiZncGFTZmVjZlNiSiRME3FLJEkTTSRODSVjZ0=
+kgSQwlUmBSYFJgSxJTUJMMiwwLCxJTUJMMywwLCw=3D</X-Extensions>
+<X-BG>cid:05946161-B6A4-43DD-9BA5-19BC9B252161</X-BG>
+<X-BGT>no-repeat</X-BGT>
+<X-BGC>#f35901</X-BGC>
+<X-BGPX>right</X-BGPX>
+<X-BGPY>bottom</X-BGPY>
+<X-ASN>BCEB29C0-42D3-11D4-BA3E-0050DAC68030</X-ASN>
+<X-ASNF>0</X-ASNF>
+<X-ASH>BCEB29C0-42D3-11D4-BA3E-0050DAC68030</X-ASH>
+<X-ASHF>1</X-ASHF>
+<X-AN>BFF138F0-3EFC-11D4-BA3D-0050DAC68030</X-AN>
+<X-ANF>0</X-ANF>
+<X-AP>BFF138F0-3EFC-11D4-BA3D-0050DAC68030</X-AP>
+<X-APF>1</X-APF>
+<X-AD>E3F15280-2BF7-11D4-BA28-0050DAC68030</X-AD>
+<X-ADF>0</X-ADF>
+<X-AUTO>X-ASN,X-ASH,X-AN,X-AP,X-AD</X-AUTO>
+<X-CNT>;</X-CNT>
+</IncrdiX-Info>
+<IncrdiXMLRemarkEnd-->
+</HEAD>
+<BODY style=3D"MARGIN: 0px 200px 0px 10px; BACKGROUND-REPEAT: no-repeat; =
+FONT-FAMILY: Verdana; BACKGROUND-POSITION: right bottom; COLOR: #f8fdb5; =
+FONT-SIZE: 12pt" background=3Dcid:05946161-B6A4-43DD-9BA5-19BC9B252161 aL=
+ink=3D#00ff00 scroll=3Dyes link=3D#00ff00 bgProperties=3Dfixed bgColor=3D=
+#f35901 text=3D#f8fdb5 vLink=3D#00ff00 INCREDIFIXEDFORIMOL=3D"true" SIGCO=
+LOR=3D"16777215">
+<TABLE id=3DINCREDIMAINTABLE border=3D0 cellSpacing=3D0 cellPadding=3D2 w=
+idth=3D"100%">
+<TBODY>
+<TR>
+<TD style=3D"POSITION: relative; DIRECTION: ltr; FONT-SIZE: 12pt" id=3DIN=
+CREDITEXTREGION vAlign=3Dtop width=3D"100%">
+<DIV style=3D"PADDING-LEFT: 2px; FONT-FAMILY: DejaVu Serif; FONT-SIZE: 12=
+pt" id=3DINCREDI_TEXT_AREA>
+<DIV>ist bearbeitet;</DIV>
+<DIV>Ku=DF!</DIV><BR><BR><BR></DIV></TD></TR>
+<TR>
+<TD id=3DINCREDIFOOTER width=3D"100%">
+<TABLE cellSpacing=3D0 cellPadding=3D0 width=3D"100%">
+<TBODY>
+<TR>
+<TD width=3D"100%"></TD>
+<TD id=3DINCREDISOUND vAlign=3Dbottom align=3Dmiddle></TD>
+<TD id=3DINCREDIANIM vAlign=3Dbottom align=3Dmiddle></TD></TR></TBODY></T=
+ABLE></TD></TR></TBODY></TABLE><SPAN id=3DIncrediStamp><A href=3D"http://=
+www.incredimail.com/?id=3D619278&amp;did=3D10500&amp;ppd=3D2690,201107041=
+701,7,[TypeID],[IM_UPN2]&amp;rui=3D116597769&amp;sd=3D20111109"><SPAN nam=
+e=3D"imgCache" border=3D"0"><IMG border=3D0 alt=3D"Tierisch gut! KOSTENLO=
+SE E-Mail-Animationen =96 von IncrediMail! Hier Klicken!" src=3D"cid:F0E3=
+0D5E-C01C-4CC4-BCED-F05ADA8739AC"></SPAN></A></SPAN></BODY></HTML>
+--------------Boundary-00=_S8LESPT1VA4000000000--
+
+--------------Boundary-00=_S8LEXFP0000000000000
+Content-Type: image/jpeg;
+ name="pum_final.jpg"
+Content-Transfer-Encoding: base64
+Content-ID: <05946161-B6A4-43DD-9BA5-19BC9B252161>
+
+/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAARgAA/+4ADkFkb2JlAGTAAAAAAf/b
+AIQABAMDAwMDBAMDBAYEAwQGBwUEBAUHCAYGBwYGCAoICQkJCQgKCgwMDAwMCgwMDQ0MDBERERER
+FBQUFBQUFBQUFAEEBQUIBwgPCgoPFA4ODhQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU
+FBQUFBQUFBQUFBQUFBQUFBQU/8AAEQgCeAXcAwERAAIRAQMRAf/EALoAAQEAAQUBAQAAAAAAAAAA
+AAABBgIDBAUHCAkBAQEAAgMBAQAAAAAAAAAAAAABAgQDBQYHCBAAAgIBAgQEBAQDBQgBAQkAABEB
+EgIDBCExQQVRYZEGcYETB6EiMhTwsfHhQiMVCMHRUmJyMyQWgpKiwkNTY3ODNCURAQEAAgECBAIH
+BgQFBAEEAwARAQIDIQQxQRIFUWHwcYGRoSITscHRMkIG4VIjFGJygpIV8aLSM0OywuIWU9Mk/9oA
+DAMBAAIRAxEAPwDSz84P1QMAwJ/IAwDAMAwABgGAfEAAAAGAYBwAYBgGAYCwgMAwFhAYB8QDAjKK
+yA+IgPxAjKKyCMoTMALCBYQVkEfAoMBMoAwDAMAwg48Qo+IB+ABgGAt4iAwgwowgwDClhBGEWwgW
+EUsIJYQLCBYQWMhBGAfHwCD6QBWBH4ALCKTlIiDgKWEQiQKwI5+YBgGFHwEQYBvqVUaCLYRRqQiW
+EVWEH5EEsWKTIFmZCDAjlAH1ANhRhBsKPxkIW8eYijYQfqFGELeMCA5kAwI/UKrCDAOAD6cwDEEY
+FbAjkKrmQg+oEfAC2EEsIDjqAYBgHIB9ADATIB+ZQZAbAMoPwIDKDID/ALQD6AGUHJAZQiQDQKMA
+wDkBE+oB+MgGBGAYCZkCvgBGgDARIFCVGFWJAWkREYBhRhB+YUYB+AKMAAAMAwDBR+AQ/iQo0AYS
+jAfiFoEGFqMChBgRhRgVhEYVWEGBrfmYqMoMA0QGII559CqMIrkgjKKwI+gFYBoCOPEA5AWEBgH5
+BR9AgwD4gLCA/kAYCJAjCqwgwJM9OgVWwhZiKj4hFYCJQEfgAYBgGBWBGAYgMAwD8ygyA4ATIBlB
+x1ID8SgwDYCMuogWJAZRGBfgCjAj8QKwDIDRQsIDYBgGwI1yAMA/QA+gB+YCZ+AFchEcoKNhBz4h
+RsACgFYEbAPxkAwDfxCD4AQKrkB/IIRIBvmAYUfiEGBGFVhEABRgVhBgRgGBWBGAYBgGAcfMorIV
+GUGQGUGAcAo0AYBgGAYKMFGAYBgGAYCwiDCjAMA+gBgGBGAYFYBgHwAjgA4AMAwDAMAwDAWECwgM
+AwDAMAwFhBGBbCAwDAMIMRUYBlBgGBWQRgGUGAYBgGEGFGELCAwowD8wFhEH5gGAYUYRGAYBgGBZ
+kKMIjkDWYslBRhEYFYKjArCowDCK+AEYUYQYBgGUo+BFVhEZQZAfoUGAZAZQYBkBlBkBlBgGAYWj
+CESAsIDAMA/UAwD9QDAWEEYFfgBGBbCCMAwDAWEBiBMgWwiIwowDAMAwDAMAwDYBgH4gGAfgBLCC
+sA2EGAYUYgMIlixSwiDCjIg/QAygwDClhAYQmQFhFLcBELCKMIMKWEEsIisCWEVWIiMKMIrAjCjC
+FhAYBgGAYB+IUsIgwDEBgGIFhAYCwgMAwDBRgGAYB+QCxYDIDBRlBkBgRlFfQgMAwIyhYQVgRgGE
+LCKMAwDBRgo+oSj4MLRgRgVgGBGCrEgqMCsCMAwDAMFGAYBgGAYBgGEGFGAYBhBgGVRkQZQZCjKF
+hAYB+RAcAGULeRIEyUGiAyiOOoBgowK/IA/7QIxAf9ADAr+AGpmKjCrYREYCwgWEB9QDAWECwgW6
+CAwDAMA+gBgGVRkQtIgMAwDAMAyg/wCpAfiUHJAYCxYDAMgMpRz1IgyqcQVGgACZAMA/UAwDANAG
+AaANf7gESAYRGFV+gBwAYEYKvkEowIwUYUYBxAFmQD8wlIkKj8wDgIr8OIEYBhRhBwAcBR+oQYBg
+GAcdCgwDATlHQkBx8QDgoTJDAwUcfEA/mUR+IFa5gHIBgRgWwgj8ADAWECwgMAwUfEA/MBMgpZiI
+MKMA+IBgo+IBhKRKBR9QowD9QD9QlH1C1GEGFVyEo/kFqMCsIMCMKMJS3qIFhAYByAYBgGCjkoMg
+WLAsIFhFGAYQsIFhBGAYFYBgRiA+IFYgMCMAxAYgNgowDAMAwD8wDAMAwDAjArAMA/ECMCsFGEH5
+haMFLQIIyg4IDArgIjCjKlLCKPzArIiMKMpSZkFGEGFowD8wDCFhFLCIMK3GYqMBYQLCBYQLCAwD
+AjAMA5AP1AMAwUfAAwUYCZ8ADiQFp8OAgWEByAc9QUsIDn4Ao55dAiNBaOQUYKOQhbgIo/mELdRA
+fmFLCIPxAMFH5lWkzM9Qg/mQGUpbgiQLQWKMIWEUYSj8QtRgWZgIMCWEVX1CJYQVgHIEfgAiQD8w
+KwI4AP0ArgCNgH1BRgGAfqAYC3gIESAsIEz1ATPiAYgMA/AIWEEYVWwDCUYEc8wowLYRB9IC0jIR
+EcIKOfkAYSkT4lUYBvqRBlFZBLFgNhR8P9oBhB+YB+oKPr+ABgqMLRhBgVgS3qIowFhEImADClhA
+sIDCDAOGFLCINcuQCwgWEB8ShYkC3zEEZVW3QkQcgGBGUW3VCCOApYRBgW3oIJYQGIFpEC3QQGAt
+IgMAwDkAwFhAaAW9RAt6iBaRAYBzyAOQFhAYBgRgIniAmfEA2AsIFhAYBgLCINhRyCjAN/EBafEs
+BgGQGEH5hR+ZUR+YVbCIMCMKrCD8wIwKwIwKwDEEYFYEYWjBW4+piowFhAfkEGFGAtAiD8Ao2AYg
+MIWEEsIq2ERLCKtoEQjIRUYQsIpYQLCIW8hAsIowFhELCBYQLCKWEQsIpYQLCBYRCwgWECwgMolu
+IgtkSBYQSzLAsILYkEsWBYRSwiFhAYgWEC0iAwFpEBgowFhAfHjxANAGBGCq/H0AWECJAjYCMvmI
+DAMFIkA5ANgG/iAYByAYKMAwUYBgo56AJkCOCisA/AgjKKyJRoKjKVWERhaMBMgGAYKrCIwK+AEs
+IDC0cBB+IB+AWlhEJkAwUiQUfqAfgCjYEYFfQA1wAMQLCCPqCjAMCsCOQUfp4AHxArAjkA/EAwKw
+IygyAwUYBoAwDBRhBhRlB+gKMFH1AWECwgPgQGUGwgwowlRwBWCowUYFfECPqFGEGAYBgLCAwFhA
+YBgLCAwDAMFLCAwDEBgRgWwgMCWEBgGAsIDKDAMgWECxYFhAsIFhAsIFhAsIFhAsIFhBrZiowUsI
+DAMAwDAMAwFhAYKP1AMAwDC0YQYBgH4AGCj4gGCowKwJMgowDBVfEFGERhaMFGBWEH/EhUYCwgMA
+2ULeZIhYRR+YBlCyEBkBv4FBhEYUfiBXAQYilmIDARJERlWj6gJmOQRZlgRgGCj8+IUsIgwpaBEG
+AYC0QIDgAwFoEB+YBgHAEcAV+ABgIyEBgGAbAWEEZRWQSwgWLAZAsWBbwEC3QQGAt6iBORIDKFhA
+YCwglhBbCBYQLCAxBLCC2EEsILYQS3gIFhAt5iBYQLCBYQLCBYQGIFhAsIJYsFt1JBLFgtiQSxYF
+hAsIFhAsIFhAsIFhAsIFhAsIFhAsIhYRSwgWEQsIFhAYVLCIWECwijECwiDEUsIhYQLCBYRRgLCI
+WECwgWLAsSBYQLCBYQSxYowhYQWwgWEEt5iKWEQsIFhAsIFhAYBgGIDAMQGAsIDAMAwDEEYFYBgG
+CowDBSwgMBaBBu2MIo+JQYCwgPwAMQGCjAMAwDAlhBbCBYQLCCWEC3oIFhAsIFhAsIFhAsIDEC3m
+IFhAsIFhAfmAbEEsWC2JAsIFhBLFgWEFsSCWLBWQRlCwgWECwgWECwgMBYQGELCKPgELCKMAxELC
+KWEKWEQt8hBGFWwiVLCAwFpEUsIDAMA5CFhAYBgH6BSxYhYkCxYoyIWksEsIDClhEGAsIDkA2IFh
+AsIFhAYB/wAQIDAWEBgGClhAYBgGIDAlhBWCjAjAMCsFSwgWEByAYKMAwDAMA/QFGAYSjCjCUYBl
+BkWkyUowUYQYWjCJYRVYSowUb6hRgowlGAsIDBR+YBgGCjjxBRyAsIowgwUYWlhEGAfmCjBRwCow
+UYFYKjAMAyhYkCwgMFJkoWEBkKMAylLCAwUYKMBYRBgH5AGCjBRhaMFLCCOQVbCIlhAsIDCjCUYW
+jCDBR+YWj8whEgGAYCJBRgbljGMhgGAYBgGBGAsIKwJMgGCjAMAwDBVYEYKMIPqFowgwowhYQGVa
+MA5AMJRhUYFYQYKMAwDBRsCMBYQGAYKMFGCjAMAwUsIDAMAwtH4hBgowDgCWEBgWwglhAsIDAMQG
+CjECwgMoMQGSBYRBharAjKDIUsWFGEpYRRhCwgMLRyEowDBSwgjEFYEYWqwgwDYKjArBUYKMFGgU
+YBgowDBS3mIDAPoAYEYFsIUt0EBgRgqsFRwUo/MQo4AMFH4iAwUcApYQGIDAMFGAYiUtAijAWQhR
+gLCIWgRSwgRkIFoERLQIFhAt4iBYQLQIpboIhYQLCBbgIFuoilhELCBYQLCBYQLFgWJAsIJYsCwg
+WECwgWECwgWECwgWECwgWECwgWECwglhBbCBYQLCBYQLCBYQLCCWECwgWECwgOQUsIFhAsIUYCwh
+SwgWECwgWECxYFiQGULCIWJFRlKtpEG4zBRgGClvMQLCAwDgFLCA/MAwUYKj4lKWEKWJAYhSxYDB
+S39BAYKMAwESAtAgWEBgoxClvURC0CBaBFLCAwFoESlhAsIqWERbCA/EBYQSwilhBWESwgWECwil
+hELCBYQpYQLCBbzEUtAiFhBLeJYE5eYgthBLMQLCBbwECwgWECwgWEC3yECwgWECwgWECwglhBbC
+BYQLCBYQLCCWkQLCBYQLSIFhAt4iBYQLCAwDECxYFiQLCBYsBgLEgjKLbxEEsIlLCKWEByBbCCMA
+wFhEowDAWEUsIFhAYBgowFhEGAsIDAMQpYQowtH/AEERGCjAMQGAsIDAWEBlKMgMoWEBgGAYBgGA
+YCwgWECwgWECwglhBbCAwiWEWjAWEBgpYQpYQLCAwDAWEQsIowFhELCBYRSwiFhBLCC2EEsIpYRC
+wgthBLCBYsCwgWJAsWBYQpYQLCKWECwiFhAsIFhAYCwgW8xBGIFhBbCCWECwgWECwgWEG6zBS3oW
+KWJAfEoWJAZRGAYQYUYRbCCMAwDAMKMIPwAMAwtGEGAYCwgMBYQGgDAMAwDAjCqxEGBGAfmCj6AG
+AYBgLIQLCA2WAwFkSAygwDAjEFbAMBYQHAEsIDAMCsIjClhAYRbQIqP0EC3mIFhEGFLCAwhYRRoI
+MRUsIKwiMC2EEsIFhFGAsIKwiWLFGRCxYDAMBYQLCBYQGAYCwgj9ALYQSwgW6iKMIthAYEfqIDhC
+BYQGAsxAshAsID4gLCA4AW4CAxAYCwgliwGBbEgjKD9RAfqAYBgqv1AlhAYCwgMBYQLCBYRCwilh
+AcgGBH5gVyAYCwiIwowDAWEFsIhYQRgLCBYRSwiFhAYKWECwgWEBssBgGQGUSwgMAwFhBWBGAsID
+AMAwUYWjCDAMQH5gLCBYQGAsIFhAcgRgWwgjBSwgWECwhRgLCBbzLAZCjEBlCwgMI3bGEZFhBLeY
+gthBLCC2EEsIFhBbeAglhAsIDEKMQLCBYQGgFhAYCwgWECwgWEBiBYsCwglhAYCwgWEBgLCAwDAW
+kQLCBYRCwijAW6CBYQLCIWEKWEEsIo/MBYQWwiJZCBYQLCBYQHxAWEB+AKMBYQLCBYQLeAilhELC
+AylGFGEqMFLCBYQWwgjAWEUf9REHACwgWgQLCAwFhCjAWEC0CBYQSwgWEFsIIxBbCCMQLCAwFhAs
+IFhAsIFhAsWAwDIUsWBYkCxYFiQLFgWEEsILYREsxFH6BFnIQSwgWEWlhEGAsIpYRC3QQGIFhFLS
+IhYQpYQLdeggMCWEWqwg2ClhAsIDBUsIDClhELCAygxClhAYBiBYQLCBYQLCBYQSwgthBLCC2EKl
+hBWIFhAsIJYQWwglhAsIFhAsIFhAYgMBb8BAsIhYQLCBYQLCFH48AJb5FgWEFsSCWLFLCBYRCchA
+twECwilhELMQLCLSwiFhAsIpYRCwgMBYQLeIgWkQqWEG7YwjIsIFhAsWBYQGAsSIWLFLCBYRCwgW
+EUsIFhBLCAxAsILYREsIFhAYijAWEBhBgLCAwDBRgpYQLCBYQLCCWECwgrBUsILYQS3mIFhAsIDK
+FiQLFgWEBiBYQLeAgRl5iBYQowFvEQLMQS0CC26CCWEC0CBYQLCBbzEC3QQLCA2AtAgWECMhAsIh
+YRSwgWECwgMIjKqsiE5CBYRUYSlpLAsIq2JESxYtLeAiFvmSAylLCBYQLIQSwirb5CIWXGRAfmAt
+4CA2CpYQW0iCTM/EFLR4iBYQLCC26MQqW6CBOQgTkIFvEQLSIFuJYFiQH4cgD8ylLEhR+fAJSxYo
+wlLIRS3UQRwClhEqsLUt/QRKWEUcgqv+gSpYQo/UFLCFLeMiFLCA/QFGClvDmIUsIDBSwgW6sQqN
+yFLCJSZBSwhRlhSxIUfEFHH9oKW4iC2EEt5iBYQHBYUsIFhAsyQLFgW4+ZIFiwSwgMC2EEshAsIE
+5CIWEUt5iBYQLCBZCIWEWlhClhAYQsIFpEVLCItlyEUsIhZiAxBLCFW0iCWECwgWLCliQGUpYQLS
+SBYsKWJAsWBaRBu2MIzLepIhaSwLfIQoxCluAgW/oII+DAMFWJ4gRgpbyEFsID/qCowUfhzBRgow
+UfiCjCDC0YKWEBlKPr0IlGFowlJmCqPwIgwUt6CBaBAtAgjKtLcBELf0ECwgPj/tEWk5RMCBMxIQ
+tx4chFLCITPmCpbgvxEUBVjLixAtxERGgqviEqNQFHMQgi24CCMFLCFLeogTIMLb0EEiYbARlECA
+2IUZSlhAsSAyhYQJy/oSIWEEZVpYQq28eQiJaBFWwiIwpYRCwgWECwilv4kRC3gIFhAsIEZIQH4c
+AUt5cBAsIEZIQIy8hFRgVwIhYQLCBYQSwgWECwgWYhSzkQLeQgWLAsIFvQkBlCwgMFLEgWLBGCrY
+QSwgrAWEEYKMBYQJyEBgGAYCwgRkIFhAYBgLCBYQowIwLYREsItGUGCjAMAwUsIgwDC0sIhYQLCA
+wFhAsIDAWECwgWEEsIFhAsIFhClpECwgMAwFhAsIFhAsIFhAYCwgMBYQLCBYQLCCWLBbEgliwLCA
+wFhAYCwgWECwgWgQLCAxAsIDCUYBgpYRUsIDArCN2xhGaWERbCFSwhSwhSwgWkQLFgMFLCBYQpYQ
+LCFLCBYQLCCW8xAYKthClvMQRgpOXmIFhAsIUsIFvMQLeYgWEKWEBgpYQpYQpYQLCBYQLCBboIFh
+AsIiWEVbCIWEVLFiFhFLCIWEKWgRS0CIWECwilhELCBYQLCFGIDEKWEEsIFhAsIVbCFSwgthBLCB
+YQpYQLCBYQLCBYQLCBYQLCFLCBYQLCBYsCxIJZlgMFLCBYQLCBYQLCFLCBYQLCBYQLCBYQLCBYQL
+CBYREjIRVsIiWEWluoiFhAsIpYQLCBYQLCJSxYpYkQsIpYsCyJEpYsCxIUsIJYsFYCwhSxIJaSwp
+YQWwglhAsIVbCCWECwgWEKW8xAtwEC3qIFhAsIFhClhAsIFhAYKlhClhAsIFhCjBRiFLIQLSIFiw
+GCliRKWLFLCFLCFGCjBUYFYhUsIFhAsIi2EEYgthBLCBYRRhKMFLCBYQLCFGClhAYKWECwgW8xCl
+hAsIUsIJYQq2EKloECxYFhAsSBYQLFhSwgWECwgWEK3bGEZFhELCFLCKlhAt0EQsIFhFLCBYRCwi
+lhAsIhOXmIFkIDAWLFLCBbgSIWLAsIpYREsILYQRgWwglhAsIFxAsIFhAsIFhAsIFhAsIUYCwgWE
+CwglhAtIgWEBiFLCBaRClhAsIUYhSwgWEKWECxYUsIDQCJEEsILYkCxYgwqWERbCKlhAsIFvMQpb
+xEBsFGEJnrIgWECwgW8xAsIUsIpYRCwilvMRKWEC3jIgWYgWEEsIUYCwgr8+AEsIFiwLCBYkCxYF
+hAsIUsIFvQQLCBboIFhAsIFhAsIUsIUt1EKWECwgWEKWhiCW8BAYKWEQsIDC0YCwhSwiFhFLCBYR
+CwilhELCCW+RYLYkEsWC2JFSxYhYRSwiFkIFhClvUQLCBYQJyEC3UQLiBYQLCBYQLT8hAsIFhBLC
+BYQWzEKlkIFhClhAsIlLeAi0sIUsIFhAsIUYhSwhS3UQLCFLFgWEKWn4iBYREsIpb0ECwgrCJaRF
+paRClhEpYQpaRClhAsIFhFGEo/MFLCFGAsIDBSwgMQpbzEKlhCq/MFLCFLCCMFLeYg3beBhGRYQp
+YsCwgOAUbAMA/UA/UAwFhAYB/wBQD6gqP1AWEKr9AVLCC2EKlhAYCwgWECwgMQLCIWEUtAiFhFpY
+QLCJSwgWEUsIhYQSxYpYRFsSAwVLeBYqsiVLFgMKWESj8QFhAsIFokQLMQLeQhS0CA/UBYRS0dRE
+SwgXn+0QH48AVWBLCBaeSECz+IgWEC0iBYQLCBYsBgLSSBbwLAt/QkBlgWmRBLfxIgrBUYBgq2/i
+SQHJSlhES0iA0IDBRgHx4haWESjBRgpGUyIFkID4go/UFHw4cwtGEpYQHAKj8wVbCCWEFtx8hBH6
+gowUsIUfHjJQb5yQpYsEfmCqwDBUcgGClhCjBVtw8xAfgClhBLdeogPiAYSlhFLegiD9QUYWlvQR
+C3oIFoECwgWgQGAtAgWECZgQRgowDgKthESwgWECwgtvUQSxYFiQLFgWJAt/UsCwgWECwgWEC0CF
+SwgthBLCC2EEYCchAsIFhAsIFhAsIFhClhELCBYQLCKMIjAthBLSIpYsQZAsWC2JBLCBYsC3nyEC
+wgWEWlvEQLQIhYQLCCWEFsIDEEsIFhAsIVvWMIyLCBYQpbzEKlpEC3jIgW8+AhVsIVH0BRrqWFWx
+IIylHxBSxIUsIFhCjKUYKW4CFLQIUsIUfmCpYQq2ESpYRSwhRgWzEKWgRKjgLSwhSwhS0CJSMhFL
+R1EQsIpaBEH4gpYQLeYgWEKW+QgWEKTkIFxFLMREsIFhCliwpORIFiwLEgWLClhAsIFhAsIFhBLC
+FWwgWEKW8RClhAsIJGQgWEKWEKWECwgWECwgW8xAsIFhAsIUsIFhAsIFhELeIipYQLFiFhBbEgli
+wJyEKWECxIFvAsCwgWECwgWEUsIiWEVbeYiFhBLCFLCC2gQqW8xAsIFhAsIFhAsIFhAsIFhAsIFv
+EQLCBYQLCCWLAsILYQSwgWECwgWEC3oIFhAsIFhAsIFhAsIFhAsIFhAsIJYQLCIthFSwiFhFpYQL
+CBbzEQsItLCBYRKW8xFLFiFiQGULEgWLBLCCsCWECwgthBLCFLCBYQpYQowtLCIWECwgWEKWEKMA
+wDBSwgWECwglhAsIUsIFhAsIUsIFhAsIFiwGClhAsIFvMQLCBYRC39oilhAsIVu2MIpYQLCCWEFs
+IJYQLCBYQLCBYQLCBYQLCBYQLCFLCBYQLFgWECxIFiwSwgWQgWECwgWECwgWECwgWECwhSwgWECw
+glhBbCCWECwgthAsIJYQWwiJYRS3iIFhELCBYQLCKWEQsIpbwECwiFiwGClpEEsIFhClhAsILYQS
+chAsIDAWEKWEKWEC3gIUsIFhAsIFhAYKWEEsIVbCFLCFSwhSwgWEFYKliwWxIVH8wFiwpYQLCBYQ
+LeYgWECwiFhFLCBYQSwgthBLCBYQLCFLCIs5CCTkIpYQLCBYRKWEUsIFhELCBYQLCFLCBYQSwhVs
+IJYQLCBYsCxIFhAsIFiwLCBYQLeggWECwgWEKWEC3qIFhClhAsIFmIJaRClhAsIFhAt1EC3iIFhA
+sIFhELCKWECwiFhFGClhEpYRRgowhYQLCBYQSxYtLCBYRFmQUt4iCMAwDBRgGCjBRgowUYKPqClh
+ClhAsIJYQq2EBgowVLCAwFhAsIUsIFhClhAtAhSwhS3iIFhAt5iI3bepjGdH5gGEGFGClvQREt1E
+VbCIlhFpYRFYEsIEZQIpaPEQpaBAsIFhAtwEQsIpYRC3AQLCBYQpaBAYCwgWgQSwgthBLCBYQLQW
+FLCBbyEC39RAsIFhAsxAYhSxIFkWBMgqWEFt6iBbxEEsIKxBGClhAt5iAwUbArCVH5/MLVsIVLSI
+UYKWECJ+YBgLCAwlLCKMJUZVqvzBUfjyAtiREZSjC0YKTkImBgowDBRhaW+YiDBRgpb0EBoFGCjB
+SwgMFGBGCjARIKMFVgSwgrBUsIKwJYQowUYB+BSjIUZSjAWJELCAwUcFUYSo/QLVsIiP+oCwiqxE
+SwgMFIyEBgGAnL1EKRPAFIyECwgWEBgLCBb5iFLdBAsIFhClvQQpYRCwijAlvQQInqClhClhAsIF
+hELFilhAYhSwiFiQLFilhAsIJYQWwiJYQLMRSwgWEKWECwgWEQsIpYQHAKWEQsIDEBgLCBYQSwi1
+bCIWECwglhAsIUsIFhAsIFiwLCBYQLCBYQLCBYQSwgtvD5iFLeQiFhFqWECwhSwgWEG9YwjKlhCl
+hCpYQLCFLCFW3mIlSwilhAsIFhClvUsCxIFvAsSliRS3h6lhS0CFLegiVJyEVbCJSxIVLFgWEKrB
+UsIFhAsItLCJSwgWEKWECwgWEKWEC3oIFhClhAsIFhClhBLCC2EEsILYQqWECwhSxYFvMkCwgW8y
+wLEgWEKWLAt4iBYQpYQpYQLCIWEWpYQW3DmIUsIiWEVbCIlhClhFLCBYQpYQpaBELQIUsIFhAsIp
+YRCwglhAsIUsIUt6lhRgpYQLEhSxYUsSBYsCwgWEKW8xClhAsIUsIFhClhBLCC2EKlhClhAsIUsI
+FhAuIFhAsIhYRSwgW8xAsIFhAsIJYQWwhUsWBYRCwi0sIFhELeAilhELCBYQLCBYQLf1ECwhSwhS
+wglhAsIFhAsIFhAsIFhAsIFhClhAsIFhAsIFhAsIFhAsIFiwLEgWLBLCBYQLCBYQLCBYQLCBYRCw
+ilhEpYRSwgWEQsIpYQLSIiWEUsIi2EEuIpYRCwilpESlhAsIUsIUsIDBSwhSxYFhCliQSxYLYQGC
+pYQWwhUsIFhClhAsIN6xhGRYQLCCMBYQLCFLCBYQWwglhAsIFhAsIUsIFhAsIJYsFsSIlixatiQS
+xYhYRSwgWEQsIFhAsIFhAsIUsIUsIFhClhAsIFhBLQIVbCFLCCWECwgWEKWECwgWECwhSwgWECwg
+WECwgWEKWLBLCC2EQsIpYQSwgthEqWEUsIFhAsIFxAsIFhAsIFhELCLSwiFhFpOXiIiW9BAYKWEK
+WECwilhEpYRSwiUcgpYsKWEKWEUsIhYRR8QlLCFLCFLCFLCFGIUsIDBSwglhClkIDAW4iAwUYKWE
+CwgOQUYKMFLCAwDkQLCBYRCwglhFLSIUsIE5CIW4li0sSIWLAYCwgWECwgWEKMFLSIDBSwglhFq2
+EQYgMFLcBBGClhBbCCMFLCBYQLCBYQLCBYQpZCAwVLCC2EKPzEEsIKwVLCBYQq26CCWECxYFhAsI
+FhAuIFhELCKWEC39ogWECwgWECwiFoEVLCBYQWwiJYQLCKWEQsIFhAsIFhAsIFhBLFilhBbEiFhB
+LFhSwgthAsSBYsG5YwjJbCBYQLCBYQSwhRgLCFGAsIDCD9QpYQHxBRgGAsID6sFLdBCjheIKMAwI
+wUtAgOAhYsUsSIW8xAZVLCFLEiDKpYQLCBaRAYSlvERR+oRLCA2BbfxyEC3iIIxAt5CFLCBYQLeQ
+gP1AWgQGCjAMBYQLCA/ABYQLMQLCCW8xBbCCWLBWQRgW0dRCowlLeZYowFhAt58RELCKMFGAsIlL
+CKWECchELCCWEVbQIDgJUsIpYQLCIWECwi0sIhaBAsIFhAsIDAWkQLCBYQLCBGQgWLCpYQWwglhA
+sILYkCwgliwLeIgWEC3iIFhAsIFhAYKW4CBbgIVLCC2EEYgtvMQS0iBbxEBgo+oKWECwgMAwFhAs
+WA+IKlhCqyJkZQsIpMiIP0CjAj6ALCFHASjBR+QWlhEo/AAwowhYQLCBYQLCBYQLCCWEFsIJYQVg
+LCFSwgMFLCAwFhClhAsIUsIFhAZQYEYKWECwgrAjAMBYRCwirbxEEsIgwpYQWwiJYRRgGAaBSwgW
+ECwiFhAsIN2xhGRYQLCBYQLCBYQpYQLeIgWkQLFglhAsIVbEgliwWwglhClhAsIFhAsIFhAsIFvM
+QLCBYQLCBYQLSIFhBLCBYQJyECyECwiFhFLCFLCIWEUsIhafERRlhSxIhYsWlhELMRRgqWEKthEL
+CKWEQsIFvEQSwgWEC3mIpYRCwilhELCBaBFLCIMQLCBOQgWECwgloEFsIVLFgW8SQLCBYQLCBYsC
+xIUsWBYQpYQLCBbzECwiFhFLCFSwgthAsIhYQSwilvURKWEUnIQpYRKWEUsIhYQLCFLCLSwiFhAs
+IFhAsIFhAsIFhAsIJYQLCBYQpORYFhAsSFLFgWECwgWECwhS3mIFhAt5iBYQSwhSwhS3mIFvPiIF
+vMQLCBYQLCFLCBYQLCFLCBZCBYQLCBYQpYQpYRCwhSwhSwhUsWLSwhSwiUsIUsIFhFLCJSwhSwgW
+EWlhELCBYQpYQpYQLCFLCFLCFSwgWECwgrBUsIUsIFhAsIFhAsIFhAsIUsIUt5iJSwi1GUq2ECwi
+UsIUsIJOQi0sIUsIUsIhYRaWECwiUt5iFbtjCMywgWEQsIFhAsIFhAsIUsIFhAsIJYsCxIFiwLCF
+LEgWLClhAsIUsIFhClhAsIUsIFhAYKlhClhAsIFhCrYQpYQqWECwgWECwgW6CBYQpYRKWEUsIUsI
+FuIgMFLCIlixarJEqWLFLQIlLCKWYiUsIpYQLCJSwgWEKWEUsIlLCFLCFLCBYQpYQLCBYQLSIVLC
+C2EKlhClvEQLCBYQoxAsIUshAsIUsIFhAtIhSwgWECwhRlKWJELCLSxYFiRCxYVLCAwtLCJSwhSw
+hSwhRhaMRKWEKMFLCFLCFLCFGClhCluIhUfEFWwhRgpYQqMFGCq5BUsIUsIUsIFiwpYQGClhClhC
+lhAsIUZIUsWBYRCwgWEVLCBYQLCBYRCwi0sIFhELCBYQLCBYQLCBaRAsIFhBLCC2EBgqWEFsIJYQ
+LCBYQWwglhAsWBYQLEgWLAsIFhAsSFLFgWEEsILYQLCCWEKWECwgWEKWECwgWECwgWECwgWEQsIp
+YRCwilhAsIhYQSwi0sIlWwglixSwiUsIFiQLFg3bGEZ0YQsIUsIUsxAYhR9QUYCwhSwgMAxCjAMF
+GCowUYKthAYKMCOPkCjBS3gIUYgWECZ9BCjBRlKWJAtBYhYRaWEKWEQt6iKMIWjxECzEC0dRFIyE
+EnIQGIUsIFhELeYgWECyEUsIUtAiFoEKWEKWEBsQLQIFvEQpYQSZBVsIVLCBYsCxID6ALFgWJAZS
+liQLFgP5kCxYFhAsIVGBZy8BClhClhBH5gpYQWwiFhFSwgW6iIPzBSJ8QFhFpOQiFhFLCIW8xFHx
+5gpaRES0cyxSyJEJyEKthBJkpVtxJAfmCpb1LBbEglvUQpb+pYDEB+oKMBaBCloEC39RAmQUsIDB
+R+gKPqClhBLeohRiAxAsIFhAYFfDh6gqP1BRgpbiID8PQBYQowUYKMBYQH5lKPxIiP0KtWwgMkQY
+UZULeYgjC0t4CIMBbzECwgMAwtGAYSlvCRAYgMFGCjBRwCjBRgRgq2EEYKMC2EKjBRgowUsIFhAY
+BgLCIMKMAwDBRlCxIUZQsIVLCCsBYQLeYgWEEYSlhFLCC2ERLCKWEQsIFhAtAg3bGEZlhClhELCB
+YQLCBYRUsWBYkSlixSwiFhFLCJS3QQLCFLCBYQpYQLCBYQpYQLCBYQLCCWEFsIUYhRgqOeYBgq2E
+EYKWEBgq2ERLCKWEKWECwgWECwhSwgWECwgW8xELCKlvMsKthERgoxFLCItkIJYQpYRaWESlhCjB
+R+IKWkQpbwEKMKMJSwgPzBSwgWECwgMFGAsIIwUsIUsIFhCrYQRgpbxEC3zECwhRgpYsCxIE5CFL
+CBYsCwgWEC3QQGAsIFhBLCFLeogWECwiFhFLCFLCAwUsIhYRSwiFhFLCIWECwgWEEsIUsIFhAsIF
+hAsIFhAsIFhAsIFiwLCBYkCxYUsIFhAsIFhAsIFhBLCBYQLiBYQLCBYQLCBYQLCBYQLCBYQpYQLC
+BYQLCBYQSwgWECwgWLAsIFhELCKWECwiFhAsIFhFLCIWECwgWEEsItWwiUsIJYQLCBYQLCBYRSwi
+FhAsIUtIhSwhSwgMQpYQLCBYQSxYFhAsIFhClhAsIUsIFhClhAsIN2xhFLCBZCBYQLCKWESlhAsI
+FhFpYRCwgWECwgWECwgWEEsILYQSwgWLAsSBYQLCBYQLFgWECwgWECwgWECwgWECwglhBbCCWECw
+gWECwhSwgWECwgWECwgWECwgWECwiFhFLCCWEC0iC2EEjMRCxYFhFLCFLEgWLAsIFhAsIhYQLCBa
+BFLCITkItLCIloEWlhELCBbqIFhClhAsIUsIUsIUsIUsIFhCliwJyJBLFgWEC3oIUsIFhAsIFhAs
+IFhAsIFhAsIFhAsIFhAsIFhAsIJYQLCBYQpYQLCBYQLCFLCIWEUsIFhELCKWECwiFhAsIFhAsIFh
+CpYsUt4iC2ERGItWwiUsIJYQLCBYQpYQpYQpafkIFhAsIUsIFhAsIFhAsIFhBLCC2EEsIFhAsIFh
+AsWBYkCxYUsSBYsKWEBkSlixRiFLCJRhaWgRKMQpYQowUtIglhFqsJSwgjBR/gClhBWCowDBRgow
+UsIUsIUYhRgoxAbBRhKMLSwhSwgj4gowVbCFLCFRgLCDdsYxkWEKMBYQpYQLCBbzECwgWECwgWEC
+wgPzAW8xAsIJYQLeggWgQWwiVLMRSwiE5eYilhAsIFhELFilmSIWLFLCCWEKRkIlLCKrIiMq0sIl
+WwhUsIUsIFhClhAsItLCIW8BClhAsIFhClhClhBGBbeYhUsIVWCj8wJYQpYQo/GQDBR+AhRgo/MF
+HPiEo/Qq0YKPzIlLLrxLFo/AJRkKWEBlKMFLCFRhar9AhaRCj9AVGhCj9QUfyBVsIJEgo4BRgo/D
+kCjBR/1BRwAfmCjBRwCj8wUfQFGCowVX5gowUYKjBRgqvzCVH4haMpRkSjAMFH/UFGUoyA/PgUoy
+FHxKUfiQLehYUYKPzBR+ABgowtH5/IJRgo31BRgowD9AJbgIKwUYKW8BCoxAYBhBhRgowDCUfmFo
+whYQpb+ogMFGCjBUZVqsiDBUfgUqt/EhRgGCk5CCMpRgowUYKMFGClhAsIFvMQLeYhRgGEGFowFv
+MQLCAwFvMQLCCWEFsIDjxCJYRS0CBYQVhEtAi0sIg4EUsIFvEQpYQLCFLCIWEEsWKthELCBYRW4+
+hgyo/AFGCowVbeIhSwhSwhUfUFWwgPzBUsIVXwBUYKMFLCJRlWliRBgoyrRgowlGQoylGFGEpYQp
+EgowUYKMFLCAwDBRgqMFVgRgqsFGBGAYBgGIDAMAwUYKMFGCjBRhKMA0CjBRgGItLCIMFGAsIVIk
+oWEFsSCWLCkyBbEgliwGClhAsIFhAYKWEKWECwgW8RAsIFhClhAsIFhAsIUsIFoEEsIFhClhClhA
+sIUsIFhAsIFhAsIFhAsIhYQLFipYQWwgWERLCKthEqWECwgWECwilhELCBYQLCBYQLCBYQLCBYQL
+CCW9RAsILYQSwgWECwgWECwgWECwgWEKWECxYFhCliQLCBYsCwgWECwgWEEsILYQSwgWECwgWECw
+gWECwgWEKWECwgWECwgWEEsIFhCrYQLCIlhFWwiJYQLCBYRSwiFhFLFiFiQLFgWECwgWEKWECwgW
+ECwgWECwglhAsIFhAsIFhAsIFhAt5iBYQLCBYQowN2xhGRYQLCIWECxYpYkRLFgtiRUsWIthBLCB
+YRSwiFhAsIFhClhAsIE5CBYQLCBYQLCBYQSwgthBLCBYQLCC2EEjIQLCBYQLFgWJAsIFiwLCBYQL
+CBYQLCCWEFsIJYQGAsIFhAsIFhClhAsIFhAsIUYKWECwgMFLCFLCBb0EEYFsxBLCFWwhSwgMJUsI
+owhYQLCBYsCwgWECwilhEpYQpYQLCBYQHMALCBYQSwilhELCBYQLCBYQJyECwgWEKWECwgWECwgW
+ECwgWgQSxYFiQLFgtiQSxYFhAsSBYsCwgWECwgWEC3AQLCBYQpYQLCBYQSwgWEFsIDCJYRSwgWEK
+WEB+oiUsIFhAsIpYRKWECwhSwilhClhELCBYQLCBYsCwglhAsIFvMQLCBYQLCBYQLQIFhAsIFhAs
+IJYQWwhUsILYQLCBYQSwhSwhVsIVLCBYQLCBYQLCFLCFLFgWJAsIhYRSxYFhAsSAylGCjCUsIDBS
+wilhEqMFHICZEKrBUYWlhEowtb1vMwipboIFhAsIowhbzECwgW4iBYQLeYgWEEsWFW0Egj9CwLEh
+SxYFhCq4IJYsC0CBbwECwgW8xAsIFvEQLCBbwEBgLfIQLCCWkQGELCKWECwgWECwiFpEUsIhYQGA
+sIDkFLCBYRRoRKWEUchB+JQckKWLAmQVLCCuQDAlhAsuogMKW8xEGClvMQLCBYRS3mIiW6CC2fUQ
+H5gLCAwDAWECwglhClhAsIFhAYKMAwFhAsWBYkBgLCAwFiwLCIOCLRlCwgWkRBiKWEQsIqMQGCjB
+RyEGAYKMFHIKMAwUsIUsIUfmAfmClvMQHIKWXUQR+YFfACMFV+YKj8wDgBbzEB+ZSj8yJRhRlB+Y
+KPzAMFGAs+pIlHBVpYRKMFLCA/UQowUYBhaMJRwCjBRwAYKjAMFGCjEBgowUYBgowUcgo5BSJBRy
+CjBRhKMFHIWjBRhKMLRgo5CUcgo5C0YSjBUYKMFGUoyFH5gpbzEBlKPzIUYB+YKMoPzBRgo48eYK
+MFH5go/QFLQIDgFGAcCFS0eIhW6zFnSwhSwiUYKWEEsIowlWwglpEB+ohRgpYQo5ECwhRgpYQJyE
+BgGCjEKWEKOQFhClvMQS3mIVbCBbzEEYC3EQLcRAsWBYkKMFLCBYsKWEKWEKMFGClhAsSFGUowUs
+IUsIIwDArAlhCjBVmQlRwCjBSwgMAwUYKMBYQGCjBRgGCjBRgo5BRyCjBRgo5AOQUYSjC0YSjCjB
+RgowlGCowUYKMFGUo/MFLEhRlKMFGCjAPzAMFGCjBRgowVHAKrgFHEgGAbBRgqWgQLQIDgBaBAcA
+paBCjAOAUtAgWgRC0CKWEKWEC0CBaBClhAtAiFhBLFirORIhYQpYQLCCWLBbCKlhClhEpaRAsIpY
+RCwgWECwgWECwgWECwgWECwgWECwgWECwgWEEYhSwgWECwgWECwhS3QQLCBYQLCAxAsIDRQsSBYs
+CwgMQRgqsCWECwgMCsFLCCMRKMKWECwgWEQsIpYRBhRhKMKMJRgGFpYRCwijCFhCjAMFbjMWVH5g
+owUYKMAwUYKMFHAKjBVYBwCpaBBXAKloEC3QQLQIFoYgWEBwEpYRSwgWgQLcRELCKWECwgliwWxI
+hYQLQIqWLELCKtiREt4FilhELCBaBClhAsIFhAsIFhAsIFhAsIFhAsIJYQWwgWEKlhBbCFSwgRkI
+Fp8RAsIFhAsIFhAsIDBSwgMBYQLeZYUt/QkCwiFhFJyLBGBbeIhUYhRiAwFhAYCwhSwgWEQsIFhF
+LCIMKMJSwijBRhBgowUsIDBRgpYQowIwFhFWwiJYQGCjBS3QQGAYBgGIDBRlBkBlCwgMAwDECwgM
+BYQGCpYQGCqwgwtLCIjC0YKMAwDAMAwgwDC0YQYKMFGAYBgGAYBgowUYEYKMFGCjBSwgWECxYDBR
+kCxYDIUYgWLAsIFhAYCwhSwgWECwgWEBgLCAwDAjAWEQsID8xFLCBYRCwilhEGFLCBYQLCJSwi0s
+IFhCpbiWC2JAtAhS0CBaBEbjMWYwDCFhAYBgGAYEYBiAwDAMFGAYBgGAYBgGUowUYBkBlBgGAYEf
+mAYFYKWXUQGCowDAMAwUYBgLCBbwEBgHIBhBgH4hRgLCIMAwtGEGFowiMAwowlGAsIDAWEBgGVRk
+QcgGULCAwDBR+YBzABgGAsIFhAYBgIkAxBLCCsAwDAlhAYKrAj8wFhAYQYUt0ECwgWEQjIRS3mID
+6zIQsIoxELdBAcASxYq2JELMRUZULCKWEQsIpaORIhE8CqWEC3ERC3qIFhFLCIWECwgWEUsIhYQS
+wgWEFsIJYRSwiFhAsIFhAsIFhAsIFhAtIgWECwgWECwhS0iAwDAWkQLeIgWkQHJRGAYFYEsIDAWk
+QGAYQYUYBgGAYhS3mIFhAfmEH5hRgRgGEGFGAYSjCjCDBRgGFGUGRKMAwDKDCjCDBRkBlBkBlBgH
+4AGAYEYFYEYG7YwjKlhClhCjECwgWEQsIFoECwgWECwhUsIq2ECwiFhAsIFhBLFgTkIFhAsIFhAt
+1ECwgWECwgWECwgWECwilhELMQLCBYQLCBYQSwgWECwgWECwgWECwgWECwgWEC0iBYQLCBYQLFgW
+ECxIFiwLCCWECwgWEBgLCBYQowhYRRgLCFGAYCwiDCjBRgGCjCDAMAwJYRRhKrAlhAYKMAwDBRgG
+CjAMFGUGQGULCAyAwDKFiQGUGBGCrYQRgVgGIDAjAMBYQGAsIgwtLCAwUsIDBRiFGClhEowDBRha
+WESjBRgowD4ARgVgqMFVgqMFGBWCowUYKWEBgowDBRgGCjKUYBgowUZCjKUYKMJRgowtGAYKMIMF
+RgowqsJUYWjCKwIwUYKMFGAYBgowUYKMFGCjAMAwUYKMAwUYBgowUYKMAwUYKMFGCjAMFGErWzFm
+MpRgRgVgRgqsQowDBUYKthBGAYBgGEGFowDBRgpYRBgGFowgwUYgMFGAYBgpYQowUYBgoxAYBgRg
+qsQRgGAYBgGCj8wDBRgowUZSjAMFLEhRlKMAwlGFowgwUYEYVWEGClhFowgwtRhBgqsFRgowUYBg
+owDAMAwUYBgowUYKMFGAYBgowlGFowDCUYUYKMFLCFRgqsFLCJRhajBRgowlGCjAMFGUowUYKMFG
+AYKMFGCjAMAwUYKMAwDAMAwVGBbCAwVLCAwFhBWCowDAMQGCjAMBYQGCjEKWEC3gIFhAsIDAMBYQ
+LCIlixSwiDClhELCKWEKWECwgWEQsIpYRCwgWEUYQsIFhAsIDAWECwgWECwgWECwgWEC3iIVLCCs
+CWEFsIJYQLCBYQGAsIDBSwgMBYQGAYCwgMAyhYQGQbjMVowtGCjAMJUYKMFVgqMFGUowUZCjArAj
+KUYKMFGCjBRgGAYKMAwDAMAwDAMAwIwKwJYQGAYCwhSwgWEBgpYQLeYgMAxAYBgLCA/EIWEWjCFh
+FLCIliwWxIqWLEWxIDBUsWBYRVYEsIg/UBYRVYRGAYBgLCAwDAWEBgGClhAtIgWEBgLCAwI5APwA
+rAlhBWBGAfmIDAMBaRAYByIDCDKo+HMgMAwD8wUZQYCwgMAwIwDkBYQVgqMAwDAMAwDBRhCwgMKM
+IPgFGELCKMIMAwpYRBhUYFsIgwUYBgRgGBWBGIDAMAwDAOQDAMAygwDBRgowDBRgowDBR+gKMAwD
+AjBVYBgRgowDkFGCrYRCJCpYQGCjAMAwgwowUYKMAwUYBhKMKMAwlGIIwqsIjCjCKwDEEYUYFYRH
+IFYGuxjGRYQLCBYQGAsIUsIFhAsIDAMolhBWAsIDAWEEsIDAWEBgGILYQRgGEowDCjAMAwlLCKMF
+LCCMQVgGEGFGELCCMKrERGBWBGAYBhaMIMFGAYBgHJQZAYBgLCAylLEgWLCjEBgGAYBgowFhBLCF
+VgowI2ClhBWBGAsIDAMFGCjAMAwFhAYBgGAsIDAMAwgwDCjCIwtVgGEGAYCwgMFGAYKjArBUYBlU
+YQZAYBlKMAwDAMA+pCjKFhAYBgGCjCDCowK+IEsIDArAMCPxBRgVgqMAwDBRgGAYBgGAfmAYKMA/
+GQDAPzAMJRgowDBRgowUmQtGCjCDAMFGCjBRgGCoylJkCshUZSjBRgHABgowKyCMorIJYsCwgMA/
+MFLCA4AWEQYUcAGCjCD+YVGAYFYBgLQIiWEVX0CNbMWVGFSwiKwUYEYBgqsFRgLepYFhBWQqMoP0
+IUclB+gKMAwUYKMA/wCoKWEBgGCjAMA5AMFJyEBgGEowowlGFGuYKOQlRhRgowlGFo3yAWEBgowD
+8wUYSjCj8AUsIg/AFGAYKNhaMIMFGFo0EH4haNBCZBRoFHADh1BRgqMFHBRWQqMorIVHHQoMQHwA
+rggjKDBRgo4fkAcAGAYBgpYQGAt4CAwUsIFhAsIUsIFhAYEsIK/UBb1EC3iIJYQLCIrCo4AOQFhA
+sIFhAtIgMIWEUsIhYsKWJAYCwgMQLeghRlKP0BRkC0lgWEC3AkBqCiMFVgRgVgHIKWEEYKMFLSID
+BRgGAYByCjBR8QFvMQH1BRgo/MFGAYBgowUYBhBhRhKMLUYKMFVhKjC0cAowKwUYEYQYWjCUf9gW
+jCUYUYKMIMFGCjAMpR+BCjAMFHABlKMAwDAMAwUYKMBYQGCjgBEgSZBVYKMDWzFkjAthBGBbCIjC
+lhAsIEyAsIFhAcAHxAWEBwAtwEC09BAsIlGAYUcFBsiD8wFixUYQsIKyFGURgW3qIFhBGBXIEYFf
+iAYEYByCjAMA5AMAwDAMFH4gGAYBgGEowowUYSjCjCUiQowD8wgwowDCDCjAOegQfyCjCIwoCq/k
+EAVPiCjAoACMFGAcwCjgByBRgGAYBlKMgMA/MIOAoylGAYBgqMCsAwIwK/GQUYEYBgHIBgGAfmAY
+BgGAfzAMFGEGFGEGAYUYQYUYQYCwgWEBgowDAMBYQHIBgRgGAYKMAwDAMAylGQowDKDIDKUfAAwl
+GFowUYKMAwDAMFGABQFGCjCUYBgGFGEowUYKMCMFVgowUYEYFBUYAFGAYAFVgqMFGAYKMAwDAMFG
+CjAMFGCjBRgGCjBRgrWyMhwAYKjgFIkCsAwUYBgHAEYKMFV8AUYEmfAA4AMIMKMIMAwDAMKWEQYU
+sIgwDAMFGCjAMAwDAMFGAYhRgGCjAlhBWCjBUfoBWCowDArEEYBlBkKMpRkCzEBlBgGAYBgowlGg
+owUYBgGAYKMFGEGBGAYVX5hB+AVGEVgqMLRhBgowDBRgowUYBgowUYKMFGCjAMFGCjBRgGCjBRgo
+wUYKMAwg0FGEGFowUYKMAwIyishUYRWBGVVZCoypVZFqMqDBRgGQoygwDAMAwDAMAwDAMAwDBRgG
+AYBgGAYKMAwDBRgGCjAMAwgwqMCsIjC0YBgGAYBgGAYBhBgGFGEGFGEowDCjCUYUYQZQZFGEGFGE
+RlUYCwiKwDAjAr8QIwKwIwDAMAwDgAwDA1sxZDBRgowUYBgGCjBRgGCjBRgGCjBRgowDCUYKMLUY
+KMorIlGBGAZSjBRgGCjBRgowtGCjCUYBgowUYBgGAmQUYBgowDAMAwDAMAwVGAYFYKMFRgVgRgVg
+qMFGAYKWEBgGAYSjAMKMFGAYSjBRhRgowDAMIMKMIMLRhCxYIwDArC0YRGAYFfmCjBUYBgGAYKMQ
+LCAwUYBgGAsIDBRgH5gLCA/MAwDEBgHACwgP1BSwgWgQRgq2ERGFGAYBgLCAwDAMAwDLAsSBYQLF
+gMAwFhAZAZQYEcgWwgWEQYVLCIrAjEUYBhBhRgowgwowhYQGAYBgGCjAMAwDAMAwowgwDBR+oBgG
+CjBRyBGCqwIwVWBGAYKMAwUYBlKMgMAwUYQYWjBRlKMFGQGUowDIVrZFGAYKMAwDAMA+oBgGAYBg
+GAYKMAwDBRgo+gBgowDAMCPwAMAwUYBgJkBYQGAsIFhAYB8AD8QE5f1LAZAZQYCchAsSCWLBX1Ig
+yqlpEFiQIwhYRRgWwgjYBhCwirYREYCwijAMINAH0AMKMIMAwUYKMA5AMCMCsAwIwKwDAjCqwiMF
+GAfgAYKMAwUYKMFGCjAMoMgMoMgMpRgGQGUowDCUYWjAMAwlGCjBRgowowlGFGEowowUYQYKAowI
+Cj4AGAYKMCsFRgGAYKAGCjAMFGCjBQFGCgKAoCgKMFAlAUYKBaMAEoFoEowAUCUBQFAUBQFAUBRg
+GCjBQFAUYAFAUYKAoEoCjC0BUAoSkSFGEowUYWoygQoyikSowKCjQEf9pQcEDgUoQoyg4IDjmVWp
+mLIYBgowDKDIUZQYKMFGCjBRhKMAwowlGFowgwDAMAwVGFqsJRgGCjBUfqCnwBRgVgqMFAVQVGAY
+BgowUBT4AowUYKMFGCjBQJRhQFAlAtGEoCgWnEJRhaBKPwC0CHxBQFAAKAoABQFAUYAFAUBQFJkF
+AUBQFGCjAPxBR+PMFAUYBgoCjCUYKNBaBKjCqwlGCgKMFRgowKwUBUcFBkKMA4AMq0YSjIUcSUo4
+BRgGQpMlBgo4ANgGCjBRwAcAGCjgFHAKP1BRhBhaPgCowVX6AowDCI+IWq/QA4Aj9AUcBBhaMIMA
+4+YBhRhBhRwEowowg+oUYQYBgGCjAMBM+gWjKlGQo+oB+IKPxAMCMorIJElFZBGUqsgjKUYKMFGA
+YBgOAKMAwDAMA/AFGCjAMFGAcdQlH4BaMFHAKP8AsBUBVfgCjCUYWjCAK1sxZVJkFVgowIyg4IDB
+Rgo4KDBQhRlKMAwDAMAwDBRgGAYBgo4APh/sBR9OoKMA4gFGAYEfHgCqwUYQYUYQYUYRHxCjCDgK
+MCvxCIwowlGFGwhYQHw4gHIBgGIFpEBgGIUYKMAwE5CAwUYBgHIKMFGAsID8AESBLFgrIIylWxII
+ylHIKMBb1EBgVkEZQsIDBRgowUYQYUYQYWjAPiAYSj8QtGEowtGEGFowlH5gowDCjCVGFqvpAQYB
+gR+gKrBUYKMFGCjAMFGAYKMFGCjBRgowDBR8fMFGCjBRgowUYKMFGCjCUYWjCDKDC0ZEoyrRkKMJ
+UZVqsJRgowVGCqwowlRgGBWCowDBRgGAYBgowUYhRgGAYKMAwDBRgGCjBRgowUYBgGAYBgGAYgMF
+GBGEGFGAYBgGAYBgGEGFGEGAZQYUZAZRrZitGFGEGAYBgowDAjBRgowVWCjAjAMAwUYBgowUYKMF
+GAYKMFGCjAMAwUYQYWjAMAygyIMFGCjKowDIDKlGQoyqMJRgGQGUGCowDBVYEYKPgAYKMAwDBRgo
+wDAMAwDBRgowUaBRgowDBRsAwUYKMJSJYUYKWEBgH8wDCUfgFH4gRgGEGAfqFGCq5/qERgowUYUf
+EBYsQZCjAMAwDBRgH6Aoyg/AA0AYBgGCjAMFGAYKMAwVGFGEVyBGCjBVYEYBsFGEowo5BRgGAYBg
+GEGFowDAMAwUYKMAwDAMCMJVYKMAwowIwUYQYBgGAYBlKMAwDCjAMIMAwDBRgowUYKMAwDAMAwDA
+MAwDAMFRgowKwDAMCMAwKwIwDAMAwUYSjCjBRgGAYSjC1uMxZVGAfAAwgwoygwDIgwowgygyAwDK
+DBRhRhCwgMCMCsAwDAMA+IBgRgIkAwD4gVgqMAwDAPxAMAwDAMC2EEYBgH5gGEGFowD9Ag5Cj9QD
+APxAMIjCqwiMKMAwKwiMqjIgwDAMqjIgwUYKWLFGRBlBgGAYUYQYBgGAYBgH48wDBRgGAsIDAMCM
+CsAwDAMFRgGAYFYKjAMCsCMFGEo/MAwowgAcgGFowUYBhBhRhKMLRhBgGCjBR+oBgGCjBRgGCjAM
+FGAYAFGCjAMAwDAjKABkFYKjKUIDAMoMAwUYBgoCjBRgowgwDBRhaBKMLRgowUYQYBgowUYUYSjB
+QFGAYBgGCjAMAwDAMFGCjAMAwIwKwDAMFIkFSZBRgqsCMAwDAMJVYGpkZDCjAMCMJRhaMAwUYKMA
+wlGFowgwDCjCDAMLRhKMKMIMFGFowgwowgwUYKMFGAYKMFGAYKAowVGUVkBgowIyishRgGBGUowD
+BRgGAZAKUYKMFGEowUC0YKAowUYQYKMFGCjBRgGFGEowUYAFGABRgowUYBgGCjBRgowDCIwtVgqM
+FVgo2AYBgqMCsIMKjArAjAMFGCjAMAwDAMAwDAMIMLRgGAsIDEQcAowDCjAWEQcFgWEBgGClhAYE
+sIDArAjAWEFYEYBgGAYBgLCAwDAMAwDAP0BRgGAYBgowDCVHIWqwDkAwlHIUchBgGCjCjkIMCMLR
+yEqsFGCowVWCjAjBQFUFRgoCgKcQUBQFGCjAApIKAoCgKAoCgSjC0KUYKMFAUIUCVqZGdGAYKMFV
+gqMIMKP0AMAwDBRgGEGAYBhRhBhRgGUGRBgoyqMiDAMAyhYQRgVgGAYEYBiAwDAMAwD9QDBRgGAY
+BgGAcgGAYBgowDBRgHIBhBhRyEGCjC0cgqMA5ArAkzIKMJRsA5BSZC05hKMFAtVhKjmADBRgowUY
+KAowUBRlKEKAowDBRgowUBRlKEKAoCjKUABKBaAoRKFUBQJRgoCgKAo4BRgoCgKAoCgKMFGAYKAG
+AYEYKMFVgqAowUYFYKAoEowAWoCqCowUYQYFBUYBgAUBTiCqCowUYBgGABRgowUYBgowUYBgGCjA
+OADAMAwUYKMFGCjAMAwgwowUYKMIMAwDAMKj8wDAMIMCsFRgGAZQZBWBGUGQGUGAYBkGojOjBQFA
+UBQAUo+hChSgKAoCgKESgKMLRhKFKBaBBgAUC0CUBQFAtAlGCgKAowVAUBVBQFIBUYKoKBKMLQFQ
+FUFGCoEo4BRhRgqhKjAAowDBRgAUYAKTIRQIwDBRgGAaBRgAUYBgowUYKMFGCjAMFGAYKMAwDgFG
+EHABgGFowiPzCjBVYRGFVoBMhBgqPzAMCsCMoMgMA/UoMKMiDKDAWEB+gKMAwDBUYFYEYFYCwgjA
+rAWEBgGAsIIwDAWEFYBgSwgMAwDAMAwDAMAwDAWEBgGAsIDBRgGAYBgGAYQsIIygyKMJVYKj9SlG
+Qq2EEfmBWCjBUfUpRgowUYKMFGCjBRgowUYKMFGCjBRgowDBRgowUYKMFGCjBRgowUYKMFGEGFqB
+KrC0YBgrUzFVYEYUYBgGVBgGQGUGAYBgH1IDKDAMAwDAWECwgMBYQGAsIIwKwJYQWwgjAMCsAwIw
+DAMAwDQBgGAYKMAwDAMAwDAOQDAMAwlGFowIwKwlGAYKjCjCDKUfzBRgGQowDBRgoylGCjANAowU
+iQUYKMFGCjBRgowUYKMFGCjBRgowDBRgqMFVhKMKMIMFRhaMJVYKMFRhaMJRgowVWCowUYKMFGCj
+BRgowUiQUcgowUBRgowUYKMFGCjBRgowUBRgoCjBQFAUYSjBQFGCjAMAwtCpRkKMAwVGUowUYKrI
+UYKMFGilR/iCqwUZCjKUYKjBRvgCqCowUYKMACjBRgqsCMFAUYKMFGEowUYKMFGFowlGFAgwDBR+
+YKMFGAYKMA2BHAKrAMAwUYKNARgVgowDAOOYK1MjKowDBRgoCjBRsFGCgKMFGCjAAowUYKMFGCjB
+QFHIBgoCjAOQUcAowUYKMFAlGCjC0fqCjABKMAwtGEowUYEZSgKrBQFGQqFKpAZSjBRkKhSqyFRl
+KMFVkEZSgKrBUYAFAgwU8gtVkEKUYFYSowtGCjCDBQLRhBgowU/mCjAfiAYBgGA4Ap8OIKOAUYBg
+owUYAFGAYBgH0AMCOAD8wKwI4QFAj4oAwEyCjAPzAMCthKjCjAdQiv5gGBHxCjAMAwg4ATPEAwDK
+oyIMoPoRRlQYBwQLFgPqQGUGBGAYBgVzABgRgGAYBgGAYB/MAwDBRyAYB/MAwUYBgH5gGBGBWgDE
+BgH6gR+YKrAMFH5go/MFQJRhVYQYUYSjBRgoCowUBQFAUcgowUYApRkBgpxBR+AKOQUKUYBgAUIU
+KUYK1mLIYBgGAYKMAwDAOAD/AI6gGgDAMCOADAPwArAMFRgowKwiMKMAwDAP0CDBRwCj8Qo2EJkA
+wDKtGRBlUYByRBuPMA+BQfEgPzAjKqzIQfiBGAYFcgRgHIByBbCCOQFpECJAOQFpEBgH1APgAYBg
+GClpEByCo2BWAYBgGAYBgo+oBgHPwBRgRgqsIMFHIBgJkLRgLCIRMgGAYEaArAjAMFWZAjAPoCjA
+FKMhQpRkAFGUGQpEgowUYBvmChSj8wUYAhRlKPwCUYWjBR9AlAtHxBQJQFGCgKMFAUBRgoCgKgKf
+zBVBUBVBUYKAoCr8AUYKAqdQVQUBU6gqhKjCnzBRgoCqEqAoCjBRgoCgKAowUfHgCgKAoCkgoCj8
+wU4Ap8QUBQFAUBQFAU/EFAUBTgCgAFGCgKAGCjAMFAg/AFGAYWgQ+YFZGdGCjBRsFAUcFKMhRgoC
+jAMFH6AGUoCgKMFH1IDKUBQFH6EKFShCjKtGAYKMFGCjCUBUYKvkCgKAo+IKfAFGCgKMFAUBQFGC
+gKgKrBUBRgqgqAowlAtVhKgKoWgSoCnHqCqwVAUBQFAUBQFWQVAUYKsgqMFOoKApM/MFJ/EFGCjj
+xBSJAMFIkFOXAFR/MCzwBQFG+IKSCjCHILSJ4go/EJRhRwAcApAKN8ggwtSclzESq/TxBUcAqsFG
+Cj8AIwVWBGCgFfkgVH0Av8wVGCq/D+wCMoPzAP1ArII+pQYB9SAyg+IB+gBgH6AGQGUH0AP5AGAY
+B9QDAMAwDCFhFH0CDAMKMIjANgGFH/YEGBWBH/UCvx+QEYBgHIFaAjAoKjj5gUFRgVgR9ADAMFGA
+YKMA/wAQDBSwgOAD9ADBRhKjRSjCUYWjBViSFGFOAKMAwUYBgH/EBEKtVyRB/wASCkyBGUowK+r4
+kK1OCMj+GA5AqdQKCgKjAvUAwVHxBVcAH0AjAMAyisgMFGwVJlc+QKPxKK/H1II38CivoQqP0KUj
+kCjAMBEgIkBM+IBgpEkB+BQb5ApMgVv+wiVIkqkTPUFADfIA3PAIjCq5An8wLM+oQb5BR/IAwDCD
+9fUA/gBH6gWeQKMCceoKMBMrkAYBgo55AInoAYFc+AEYCZ8AK4YE4yCjkAwD+QBgOL8wD48wUfAB
+058AD+YB9QLYQT+YB8QET8wgwo1wAAGAYKMFR/IoWEFZEGAieABsKMITPoAYKMCP1KUjxBV4RzII
+ylGAfiCjAPgCjAAoAYByCn8wD4gowU5AowUYKMFOQKMFGAYBgowUYKMFAUBRgowlGCjBQFGCjBRg
+oCowVX/UFRgqgowVAUYKMFGCgKrAjgFHAKMFGCjBRgAUYKOAUYKMFGCjBRgoCjANgo0EowtAUYSo
+ypSZCVpy1McIecxjDTnhxkuMZyx23xr45WMn5iLRkWrEharItGUowUBRgowUYKjBVBRgoCowVWQo
+wVqfAjMYKPqESJKtV9CAwg+LAkTxKqv5kRH0KpMsA5CK+vUgOQDYVH/QqK5iOBBOfxKUYKrIIyg1
+5AGAiQUc/EBEyAgFHK5gJkAwUYBgo+kAo/EBEriAAP8AqCjAAGCjBUYKv4AqBKr6AowtGCjAMFGE
+QLVfXqEpE+ABgowJIKAowLEgqPxAr4IFGCowUYKMAwUYKAowUYKMFGCjBRgpzBRgowUYKMFGCjBR
+go4CUYBhaNApEsJRgowUZQmSFGCjBRlBgowUBQFCAylGwUfQhUZSjBVmQDBRgoCgKjArBRgqRIBg
+GAfQAwDBRgowABgGCj48wgCjgA/AA+IWjCDC0ceIQYBgPMAwDBRgHAKMFGAYBgGAYKMAwDAfABE+
+YBgGBGAAvyAjhlByQAAKN8I6FB/gRB+AWjCUf9pVGRKOQDfyAMA/PiChUrSysa7Hs+12U5avc+7a
+M6+x0tPVjbbWf06+po1y1YyUTljhjhPHLHiavcb79NOPM2znFz8MZ8Plb8eja7Xh029XLy4umNdp
+j/NnH83zmMeeOrV7h2W22Pc9Sdjjjj23eRG72Wng5ww0tb88YYzPOMWo8idpybcnHj1fza/lz9eP
+P7WXe9vrw8n5P5Nvza/CZ6zHy+DrIyNuNKj8CMqr8QtVkWjAcADKE9ADXkCj8OMgQABX5oA/EAwD
+48/kBqZiyoylGQowUBUZSjBVYAFGCjBUYKMFVoFQFUFAUBRgqAqtAqNgGAYKMFGBQVGCjCUYKTKB
+RgowDAAowtGEGCgKOAUaBRgIn+oKMFJkFAUYKMFRgV+AACMA/mCq/ECOQVZ8gVJmAKwIwKwJEgqs
+B5AGBH/Qor8SFSQlOYWqwiMLR8UBWESZAAo/MA/kFo+JUGQo/QBIKN+QKMFPxgpR9QHIBMkCZBRw
+AfQA/mCkz8ygyFH6lAhSZKEzHwAMCOFwBV+IB9SCPpHIoAV8QVGBZkCN8egBgo1/uAr9AESAYKgK
+Ao+gBgGAfFgowlP5BavwCI+nUKMIPqAYBgGAYBgHxBRoA5APr1AP+oBgGCjBSeIKMAwDAMCAVgGC
+o/4YKoKMFRgVgGBPMFH4gVgqSCgFYQC1AUkpRkSjArC1AUchKMpRgpMgqMJUmSsc5SZKxzlsaG/3
+O21NljqRjhGjqampwcxlGssc7fL8DPbi12xtPPGPw8HBw93yce3HjbGMenO2fr9XTa/TwZH3DWyy
+7VnsNXCMf8pzxnQywmcsZ2WtM0ltzXJ48uEI6vh1nJ68f/k8f+bHj+HV6XuenDtxZxjH6OcZxOv+
+nt4Z+zNx8sR0LOxdBWqJIyqsMq1NkKMLRgowtHJCgKFKAOfMFGQoCgKrkFUMh9HxAPxBUfgBYkFI
+l9QHn0BUYF8wVH4AP4QFfqBH1Ar4AHwAkz0kA+IFfoBI8PwBSZ/qCkyAYCJ6gHDAP4MAwgwo4CU4
++HAA2AfUA56AJkA/AKMJT+YKMFHxAOQDAP0AMBMz4gowDQBgp/HgAmWBAVWCnMAAYBgqfMFVgGCp
+xAr8QIAfiBWEo0BH5haMFVhBgqMFAUYWgKMAVKMinxCUCgSjBQpVIICgKMCgqeRSkEKFKfAhQpTz
+BQhQFClAUIUKBCnxKUBTmQp/DKUCUBQLTzBQJQFAtAUCU4kKFKAoQoUoCgKfMFAHAFAUBRgqRIKo
+KfAFAU5ApwBRgqAqv1BQFAU8gASgKMKMFOQSgKMACpMgqgo45dQUYEBTqBQUBQFGCnwBRgAVGCqw
+DBUYB+PAFH4AGCkyEqMJRlSj4AqWEStM5FiVGVjnLjbqLYRlHGcMolR4Twk5dPFq8+LjGfhlkXbd
+PZbvW2WO+1Yw0dXT1dr9fUxnKNN4xlpzlGMxlGMTCZ1nNnfTG3px1xnGZ8fi9Z2+OPk34vXnptrt
+pc+XT1a3zmM4l+x0eMzDxlTWZhw1K6wzsMvN9cdGqMjGLWqMiRlVt+Ai1bfMRarItGRar+YWjQKP
+xCj+YQBTn8gUYEYDj5ArUGYyB5lKMhTmUPgCgKACFClCFClCFAUBT4FKfIAEoCgKEApQLT+REoUp
+5goCgKfwgBCnxKUBQFPiCgKEKFKeYKAoCj/iAUBQFAUAAoA5goCj8QUAAoEoFP5go38QgFp/DCVI
+8OgFAAo+gD4AqTK5goCr1AfgCjBUBVfzAMFR+ABgpC5AAK/45gqPwAOOQKP+IAMCgqMCvivxAj/i
+AVWCo+IB8fMA+LBT8ZATPkAYSkTAUfAA+kAH0AfDkEPwgLTzCUfQA+iATKBRgowHLkCnEFPOYBTg
+BP5FBogv8MFGAmQUmf6gIBQA/UFGBHJRX0nqQOHgCo1/uKHPiCqyAwDAMCfIFH0APoUoAZBX1Ajk
+pVfyIiPw5lVWQTlx/EorfAgnl0KE/gCnAAwK38SIhQYD4AH6gHABgGFQJVsIFhAcAGuXIA18wIBW
+AYCwgjj5gGCjCVpmepUqTkWMaWESpOUiCRMlTqjXQsY5uEsIxrTl+bGcZ6wpLjox2xcR2vbNXTx2
+23y19D9zGlrTnr7CdTLRjWwia5ac6mKywiZ5zjJq82mc7ZmfTcT1S+n5zz+p2WnPjHaYzt/Rtttt
+r6vT6sYx11uOuOvidyw/zDumlp9t2OG0w3UV2nbtvbLHHrEY5ZzM5tzxZOG8fHnO+2dvT47Z/f8A
+BefH+45ePHDrjGOTXHo11+/p558Xax9vfdP0vrZbSuM8FlnEZPwq3Pyg08+7dtZ6noNP7R9w2xZj
+/uw42Xsz3LjnOH7DPLKOcYrKY+K4x8zlx7j2+cX1YcG39r+46/8A4/xx/Ft5e1fcOEOdjnx5cY/3
+mWO+4M/1YYZ/tr3HH/4s/h/F1u42m72eU47rRz0smvz4zEOPCTa05Nd/5c103cdpz9vmcum2n14j
+ZjM5I1a1WZIyqxMfAi0YZVWRaOPAAwEfAAwIyisg1RPUjIAP0CDYUfEACkzwAP8AoEOQABzAeQKB
+U+PPxCDAv4ApH4gpM+gD48gD+QKn8fwylV/MhUsWC9SFRsorXkQGCo4KK+JBGUXkQSZnoUX4EAB1
+BU/kUP4YKPqCj9QDBR+IBz4kKP5lFkghSj8ADRA4FKMFGugFaIiOCqOP6AGEGAYCZAMFH4gqP+JB
+VfzgAwD48AEzIEmYkFVxABxACZAMA4AjgFVwAmfmBGBWp5gR+YKMCzk/gII+ABgOAKPzARK+AKMA
+wUYAAwDQBgGAYBgRlFZAbAOQBQfiQGAYBlBkRGVarIDCDAjKUYFYEcgHIFfBAqMA/AAwUYBgowDA
+MFH4Ao5AMFGAYKAGCjBRgAUYB9QDBRgowUBRgoCgKOfkCjBQFAUYBgGCgSjBQFGBAtVhKAQFVgow
+UYKjBQFJkFGVK0zIY1JksY12nZ/b3de9Z4ztNvnltvqY6epuIxmcMbTx+KjioNTuO74+HH5s9Z4O
+69u9m7rvevHrn03rt5Y+/wAZ8n0J7e+yvtva6OlG60J3e71Ih6mvN2/DCIS8Op807v8AuTuNs59O
+fTj5fxfTO29n7HtcfyY5M48c7dfwz0/Bm21+zfY8oiNPt2lhE87aeGnHzhT+J0O/9xc2PHfP35c+
+e77Xj8OPT7Ncfwao+0/Y9GYx0Zwx1ukaWhGpmv8Ah4RMMn/nubbxs+eWxx+4aa9ccWuPuw4u4+zv
+bM8c51djqamFuWrjpacTM/CYmZ8Tl0/uHkx4bYx9Vyu3f9vydNtdM/Zf3Oj1fs1sHlp7XseGtqy4
+mMMbwp5OJnh8sjsdf7i38duSYXGntsuePjxj564/gxzf/bafamWp3jW9r6+psNFau40dvOnvMscN
+OJmVhhNpjL8OZ2fF7z/u5x45sY2z0xnN18fnno4dOD2zgxtycemM5lmNbnpj+nXPj9WL1eW7jseW
+Gx7z3HU2WfbZ7h/5fYuzYaerut3nob3KYx46cTGnTh/3Jh+Hj7Xj7rX9Xi0xtjf0dOTe411uvj4/
+zX5X6/h8027Xkz23c7/o50zy3PFrM7bdfC6630Z/ZZnwufSPZ32U90912ntXvfYe4RG519plvNfd
+7nZZ6Gj2re7bLGm21I1Zxz1frfm08stLH8q8JPK+5/3L2/Hvz8fLr09Umu1zya7X82J019PTbGNs
+9frwvZ9ty9txcfT0768evpztjF12xMzOuc+qZxnfXbwkzPHD1ra+2fc+/wBrp7nu/tvLYb/Jxutl
+t93o6mlhETwjHWxeWGE/3lpzqeDPEcnedvx7Z14+X1a48Ns65xn7dfPPw6+n4vo3be64zxfn2113
++3bF+uY9X4Yc/S9qbnTxnHDa4TGLjLTwWEYuIiVgpleeUzlPWTV27/XPjn6fX/CY+GGz/wCR089v
+p9Ph0+Dh7v2rOvhMfSzy4TfDT0cJr6y+Bz8ffenPjj78trj9wxr54+3LEu8/b/b73DOM9HVyUTxx
+x0558FOPOfNHddt7ttpnEzj8W9nueLmx6eTGucZ+LyX3L9qtbRnPW7bnjePzfTmPpzMTy/LPKfge
+z7L33Gem/wDF5D3L+z+HuMZ37bPo2/y/0/T7nmW82e77duMtrvNLLR1sOeOcL5wer4+TXk19Wubh
+8q7vtObtOTPHy6512x8f3NmJZm1cZamRlQLVYWjIUC0KBEGCtTDMZAYKMA/EAwDAMAygyIMKRKCD
+6hUZUGBWBH0Cj8wgwDAMAwDAP0BVYEYBgH4ApEyCj6AoCjBQFAUBQFADAjBVYKAoCgKAoCgBgoCg
+KMACj9AUYKBKMLQFRhFYWgACMJVAjBVCowlUFRgowUYKrAjBSZBRgAUYKAqgqMCsFQFAUBRgoCgA
+ACgKAoCnIJRhaMFGA/AFADBToEowUYKBaBBgoAKUIUBSZBQFClGCjIUkFJKUBQhQpQFGCgKMFCFC
+lCFCgAYKMACjAAoCgSoAYWqwUBRhKBRhEfqBWCoAYBgGAcgo4AP0AMA4AAAUfQB8QDAMBEgH6gH/
+AGgqMJUmSsa0zkWMa9B+3v243HufX0t7v8Mse2zNsNKPy5asY85f93Dz6nm/dveNe1xnXT+b9n+L
+2/sX9vfr645+46cfljz2/wAP2vrD2l7M2vb9rpaelhjpaeMUjS08Fhhj/wAOMTwc9fHqfG+/9x25
+Ns5zmva913uumPRpjGNdfB6B27sG10McdPb6WOnjDvpYTGGM265Ln8IPOcvdbbZ65v08nmufvd9u
+u2b8/wCDusOwbTLSmNxM6vCLznqZxhOOPHplCRw45dvHHR1me93xn8vT7MOZtvbmE7jT2+00tSNz
+qzGU6GGrqY4Y6c/38sbT/wDTwOTX175njnPyw1+Tv8+nO22cTHnMePwx0/F2ev7L2eloZZ5xnqxh
+wy3Wec4Z6mU84xiWog5duLbXF8vurR0923ztMTF8p4fW4efYNKmOGGX0NHDlhGGEOPHnz9Dg9OfH
+LZx3ubc9c/Xlxf8A1fQWOthjjjrN6ec4ZRDnqcmPVPFz/wDkdvDPh9bl6Xt3LRwmcdWcctTjqTGE
+ROXXxx4eUFzpnLg2771Z8PD5/wDq5Wj2DDVyxnU1NTP+9wmszHTlHBGWOJr7d7nHhjBuPamOrllu
+NDHKN1pxPHGfzTE9J4cfnBnni2mcYXT3KY9O38uXC1ux6S09TVjLT1lW+MxE25zjKiImTi9PRsa9
+5nrjHXDgbnsGlrzjhra2X5oWnlEYY4zqebjh8iYz6c9G5x97nXrjH7fBj/dPZu6xwyjT3utlqY5f
+4mOvpbfV1IwTro5xhjMf/wAljb07rXXx1x9+2Mfbi/sjtu2910znrrifLO2Mf9WL/wDpjz/3D7X3
+G50Ms9HuGknGEaupssJyh/3c4xyiuT68ju+077XXaZ1z/wB37HrOy9w11zM6Z/78/h8cPF/fnsDc
+b7bakZY6M46GpGE7nSjPTzwymIV8NScoxiZ6xkp6Hu/a/ddePbHj1x4Zmfuzj6Ydh7l2XB7twY4t
+8+nbx1z0z92fP54eEd27Rvey7rLbbzBKVjn0y+B9D4O405tfVq+H+6e08/t/L6OXHTy28suFEnO6
+nGVZGVUjJWwowhw+IBgGFVkZUAAGCgKMFAlAtGCgAJTqFPMJQAChQIUBR+gKFKEKApzKDIHQAwUA
+AowUYKMFPh6goCjKUaAMCAqgGA5AowDBQFAABkQ4fIojCqwVGgUCDC0BVCICqABU+AFBRgqAoAmQ
+UYB8AUfgBXHMFRgowUYAAwDBRgH1ASAkAwUYBrmA5AJAPxBVYEjwAPxAoEYD+GBGIKwD+QBgOAQ4
+gI8gowD/AKBB8Apz6BEfiFWJYB/0CDUARhR/0CKwD6sCMA/46gV+vmBHADjzYBgGAYCZkoP0IDKD
+IDKD4AGuAB8f9gB8SAwDf+8oP18SA+v4lB8QDATP9sAHw8AD9AD/AIgAwET5cQD4/wC0AwHw+YB+
+PEAwI/EBYQVgGAfqBHxAOQDAr8eIB9QDlfEA1AQcgH5hUYBhBz0+YKfxIKkyVjWmZKxzllHsX2vn
+7k7rjGrjM7HQmMtWIj9c9MI+J1HunfY7bi6fzZ+leo/t72n/AHvN6t//AK9PH5/L+PyfYPsr25ob
+LbY5RpcIjHGKxyr/AMPl5HxL3LvM77eL6V33czGNdemHp3b9rEYRj+n8vFOIjF+PieT5d+ryPPyd
+ayLa6Gjo0mcYnLT/ADac5REzjOULhK5zHU0/Vmuo5N9tr82/nrZaepTCI1NW0fSXHTcxwymOq8PE
+z12cWNLi56Y8/i7ztm4/Z7SmnNdzqzbUzz45TlZTNon1Nrj5fTr08cur7jT9Te58Mfw+Dn73u2lu
+dbDRw/NtNrjEqYV9RcMlJt9x3mOT04/p0x+PxanD22dNc5z/ADbfhhtaGE77V09vhnGGepNdTVzU
+44zlxlRHGZnp0MOH/U3xrZcy5Z75/TxnbPl5Ow/yvS1u55bfQcbbRxiM9XUm055RD4ceB2H+0127
+jPHx5/Lr/VnzmGp/uc68Xq28c+Xwa8u0auOU/Tn6iUYwlMPlz4GOe22sx1/xY47rGcdejsNDtOnp
+xnlq6jyiFTHrMc5X+7gdnx9hrrjbO+3XHljxz/HGGnv3Oc+GG3raujtoxnGY/JGWnWYjLLKYhQ/F
+mpvya6eGfDp8Wemud/2ug7hjhoZau2+lbVrfDTamcJnhM+Mw0dTyZ9Gc4jtuDOdsY2vRju4ynDHU
+wmYmMJq+EyvhymYNHbd3PHi5xlxdxudLT0scbNVrlH6qxwb8IMPVfBz6cec5YP7jwjT1895pY/4m
+f5NTFwtTGecLjD6wdr2m3qx6c/8Ao9R2GbrjXPhj8GBb+NLV1MZmMIjLTyw09SIiL6fXCYX5o/5f
+Q9DxXGPt/H4vU8Oc64+38fi8j9+e0NlvtHPTxwqomcYjjOD5THlB7X2v3DfTNb/d9pxe48GeLl+z
+Pwy+f+47DX7bu89puIWWE/ly6THSYPpHDy45NcbYfAvcew5Oy588XJ44/Fxok5WhjLUyMqpFo5+Q
+UAAowVqc/wBhGVGAfUAwhP4hRgHAQfoFHARGAYFfUKNSEH4gRgIkKrgIj9QDAP1AWEBlBkB+oBgH
+/UA/UAwDKD8eQBgGQGUGQH48ig/MAyAygBGBWAYCJAR58gI/EABXIB+ABgG/94EfoBWAf8cgD/hA
+GAYEcgVsCMBMgowHwAfAA/UABXPiEQLRhD4hRgGEGuXMCsCRPiAfUABXIEBRgoA844AAU/kCnwBT
+4gAUBRgJmeoKeQKMFOgKcf7QAAFAUBQFAUBQFGCgKfgEAtAUCUBSQUAAoCnwBQFP5AoCoCqCgKeQ
+KAp/MFAUBToCgKfzBQFAUBTgCgKAoCgKBKcAtAUCUBT+QKAoCjC0CUBSQUBT+QWgSgKAoCgKAoCn
+xBQFAVCpUDHOUwwz1tTHR04tnnMY44x1meEFznGMXJprnfbGuPHL6e+1XtfHtnb9vGWP+LnETlPJ
+5Z8pmeh8m99779Xkz8H3Lse117LtdeLHj45+vzfQvadtGho4tZZx+THKEpmOsT0k+ac+/qy6HueT
+1bMj2WOOGEZzCjGFnwlqOc4xHE6vkzcum5s5zmN7W7lobeJ1dT/t48MaxbJ5cI9TDXhzt0w49e32
+26Y8XmHuL7id13Hf9P297LjDW7js9bHU7xvc8Yy0sacY2uKiXlklnlH6fjy9Z2ftPHrw55u56a7Y
+/Jjz/wCf6seWPN6vsPauLPHnk7jppJjHn/zfvx+L1Dt3fct7stHd5bWdnra2K3Ox1nGWjq9ceURO
+PhlHODy/Pw+jfOuM+rHlnHhnH08nk+fs8cfJnX1erGPDbH9WP4/J2e13+E45RqZf4kT1XGPj/I1s
+4zho8nBm9PBv6Hef2GrG6wWUYRL4xH5p5S5mJn5F49t8bYzjxce/afq49OfNq7X7qy22etnpzjlr
+6kzlhp5ysVOVlj15m1xc3LwZ9Wvn/Gse49t9eMYz4YZBtPeMVnHV0sNLUmZynKMvyvLjM445OZ8+
+Jt6+68mPDE65+rr49M3rn658nUcvtXXpm4cLW91aurqamMOsTOOGrhlXFZdFMuUau/ecm2bnPX9z
+a09txrjH7GBe6/uxsOybvDsfZNHL3B7r10tht85ww0dKVGWe51YifpwuURFpO27L2/k5tM8u+f0+
+PH9WfP5a46X8MY/B6H2/2LbuPzcmf0uPHjnP7sebk7D3/tO86WURhnsO47XHHHfdt1f+7pzM/qiZ
+/Xi/05R+Bo912nJx5xOuufDbHhn+Gfk5Ob2Tft8+ON9Nv5d8eGfl8s/HDVr932+eeU6WeU4zMVnF
+RlOS4x5eZp44dvNjp2u2MdXV7ruOM4LUynDTji+E58PPw+Bs6cPXo3uPg69PFj+v3TR1Ms8Y1MYy
+nllKlyuMceUrgdlrwZxjwdrpwZwwvveGGdsNOY0J1ZvpzMqfq9Jw5Qp/vHfdtnOOues/Z83dcW+c
+Y+LF9xh/mmymJrG70LYuIUTlPOPhPgdtpn9Lf5Zb/Hy5xl4r9wux6erp5bnRwrqaLT5qOeM/A957
+T3WcZ9OfN57+5+x17rt/Xj+fTr9nweWxPieufGa1wYs8ZULVIoAAgKoZjfMFAHEFAUBQFAUCUBQL
+TqCn8wUAAAlPiCjYWnIFPMJQFAUBQFAU6MFOQKAoCgKAp+AKAoCgKAoCgKgKoKAoCgKAoCgKAoCg
+KcQUCUBTiCnAFOQKAoCgKApzBTqCgKAAU5goCnmCoCqCgKAoCjBQFAUBT4AqBKoWn8cAVP5BFC05
+gowlQpV/kQqAUFAUBUYFBRgQpVIUYAFOoKjBVYKAQFAUBVAgKPh5gClAUIUBRlAhVYEZSjBQAyAU
+oChEoyqMFGCgSjCnUiH8wBQIBSjRCjKDAMACjBRkBlD4kBlAAAfqABQFGAZChQYBgowU48wAAACj
+AMAwVJkMa0zJWOcsl9hdonvHuHb6Ux/h4TbKfD+IOr907j9Hgzl6n+2e0/W7vG2fDTq+w/aHa89H
+PTw0ojLV1VhERx5+vSOh8Q9w58ZxnOfDD6Z3nNj05znww9G2ejjjqTqy509Gf22GopX1I45Lo58/
+A8xybXE+PV5rl3uJ55/N9jsNLcfTcV4Y4xMYW/Vn0nlx+BrbaVp7cdeZ/cr3zve26O37N2PKI773
+TLLS0G1o6cRN9aInrjH6fM9X7N7ZpyZzycv8mnXPz+Gv2+b0Pt/Y42zdsdE+1mOz9vdt0N5s8NDW
+ymdSMsd1GV89fHUU631MZctcl1Mve9tufkzrtfLw8JP5Y3PeuD15/RucYxOuPhnHhM/tey+w/e3t
+r7p6Hdexamnpdv7zsd5r7Pa6sTGX1MtHHHKM4iZcxlGUvF8azyNHT2bHHvrx7/l/V1xtrt5erPT0
+7fX8fnjz8fm3uHZdx7bnXn0znfjn5vl18P2TPlcOn7hp73tW51dnu8PpbrQzy0s9CJmZvzT4OJiY
+ywnrB1PJ2+3HvnTfGcba+Nd5wbcfNpjfXN1zi35fTpn5ul3ndNbRziMY/JisJwy04nLKOvGH4nLx
+8GM4/wAXYcfBrnH+LTodx0dPCMscpjVyymmWOS448Hk+Jlvw5zn5Mt+LOc/JzdTu+OFJznUyzw5Y
+uZmf+ZzETx8INfHb34NfXtr4Ri3f/dnfd73bZez/AGfoYz7v7rE115U6ez2/LPXzx4w1+j1+Pc9n
+7dxa8e3Pz5/0tPLz2z5a4/e2tO14+PTPLy5/09PH5/L/AAese0vbH26+zu20tv3bdTq+4d1hG93+
+/wBbR1NxuNfPVnKJ1JnDHLrGShm3tv8A7jk05Oe41mPTprj8uuvljFzjDxfd9x7h7vf0df8ATxn0
+4x6sa46fXnHyafc2h7Q+42nv8/Z2vp5e8Oy6GG60sstLPb5V3F4w0tWcsYmcNSdPKJjpwnwOu5Nt
+eDa49WOLfOfGeUucTbOLrcT4/e2vbuXvPattde6xn9Dlz6c49WNvCfmxM+OL0z5+Dxba+443OnGW
+f1NCMJz0txs8/wAmpobnTlTjlPXKJhecG/v2fpz5Z88Z+Or6Fy9vjTOcdM/DPx1z4Zx8stGv3v62
+ll+43FnPDLBzWsqWoiOvFsy17b056YcGNca56YdXrd6w28fVnPPPX04nOVjjnjET+mYXGeHE29e2
+zt08mdvk6bu/do1Yx085x1YnGXq3icXn1hNfP0N7t+CdfByabR0M77H6UauhFc4bzdVnjC4xHBSd
+jji6zLlxyMQ9xa2OtKyxv9XF5xPWYjjlH+07vtNc6/YyzyYz08svEu57edpvtbR5RGUr4TxPe8O/
+r0xl8Z9z7f8AQ7jbXyrjRkc0ddhqZGVGRQAwDA1NkZAEBVBQFAAKAP4YAFQC/wAwHMCApz+AF5cQ
+HAACj/qABU6go4APoUUhUYAFV+AEiQUYKFFf9SAwUBR+AKfAFT8CoMLRhALV5kRGVT4BAhQoEKfE
+oQAYBgGAAcAAAFPiAAAGAAAowDAfxxAAowAKMAABRgAH4AowD4AGCgBgPjwBQIMKj8AK/UCP+JAv
+EJRhQFAgwD/qAAjgKdQgCgBwBQAE/hAGBWBAAFYEYD5AVgT5AHxAMAAmUA/hAOYB9ADAMAAf8QAf
+8dQHAAwABgGAiegBgJkAwDARz8wACwgPr0APxAPw4gIkAwI/HkUV8CB5xzKD9SCFD+QBwBWuXyII
+/EoMIMKrAP8AAiI5KquJIifyKHMA/IKBB+HoAYBgRhjlpmTJhl6/9lO05Z7jV32WMKZrbhPCPzTE
+vxR4n+5Oea41fUv7W7f9PttuTPjtl9m/bfY6G1z1+87yI+l2rZZ6881+41uUxz4xEfpPiPunJ6vy
+Y/qz+GPD8c37HH71y7ba68WvjybYx9mGT9y2OHavZfasdxlGr3DV3H1c8sInCM9XWwyynOMZmZ8Z
+U8jrOPbHJzbZ16eOP+nHpnV1Pb82eXvd501xrPsxnHR5tve85Y6WernqTGhjOWUrLjTHr5Oeh6Dj
+7frMY6vWaceMeTzieybDvW+/znvOpr7juO8xy0dLa6OWO30dHQiXjOGUWztwczY9R/ud+HT9Pjxj
+GuvW565zn5+Tt9e4zxdNfL7ervttu9tsdDS2O0zw/bbeZ+nOf+NMTPGYl15ydfvx7b7Z228c/Y1+
+bl25ds77eOV9qbzZe19/Pde3aOWfcdfd5dwy1MtfLHGdbKrrhjjiseHKZky7vHJz4xrnaY1xjXEx
+1xPDr8fucPPrjm4s8W2fy5xPD+LNPeH3S3XujcbLcZ7TT2O72uGelq7nb5TqTuNPKHjGcTjFfpz+
+bGbTznxOHk4t+fGP1ZtnGPH0zbP/ADZufVmdPDHTxsw6T272jj7PXbXG2dtds4zM/wBPxn1+f1YY
+Vre5tDVziNbWmMsMsqZ/UnjM9Iq2zk17LbGOmPwdx6MYTDvs6Ov9bPVjPUxjhiphwnGUTPOH0Ge1
+9WsxhlnW4bGt7n7nnhr47WI+pNpidWcZwnNPGfGYiecI5Ney48Zx6k1118219r/de49i993ffO57
+KO9dz3k33O51teNPLBclljjMRhj0xjE2Pde117rTTTTPo108NZmZ/HGb9rj9y7bXvOD9HG+ePH1X
++Dv/AHv9z9x707vp941NphsNGNrhscdppbidxp4/T1c841vqfTw45RnOMxOPAme1sxMdJ4YnTGJJ
+nbb6/Fr+2e38fYcH6Wu2d/zZ29WcTxxjElz0xPi1/br7l9n9ke6e69375huNbt/ddno7T6O1wxy1
+NLPb6mpnGc4zMOJjUmFzNbu/bdufh101xc6+r+qfzY1+WfCf49Gv7x7dv3vDrrptjXOu162eHXri
+/Jjfv73V7W7p7n3Pe/bO5+ts97hjlq9v19LU225y1o4fUweMYzljHqbHtvY8/FwY4+XE9Oem1xti
+fDLtewztp2mvDzZ/1NLM464zr8GI59/0nTCY1a8JzrGGXzrPQ7jHa5+ozs4WPf5x3GOf5cdK05Wz
+crqo8n/M589r+X5sM7uJuu9ZakWwwx0tKZnjh+WJjxyiWjn4+2nj1ywzyTq6ye55aeUY55Xwy4zl
+jMuMekTBtfoXwX9Xq6/vO40s9N45/picsc48Z4rh5Gz22mcZY7crzP3VhE7rS3GMKNTGYmfGcZ/t
+PV9jn8ucfB4X+4tMevTfGPHE+n4uhiTsXlcNTIyamuRFRvmFOAFcIDURUfoAYB/OQKwJ8AD6AGAY
+BgGAYBgGAYBgGAYBoA4APiAYBwAfoAfoAiQDAMAAiQDAP5SAmeKAPzATPyAMA+ICJAjKLMkBgGBJ
+nwKLEkEifkUV/wAQQHwAPxKg+oUZAfgBGUGBXHyIDAPiERlFcEEZQYB+IUc8+gB+PLwCDAMKRPiE
+GAcgH4AH/QAwHEA2AiQDAMCOQKwDAMB8wDAjAszIBgInwAjArAAH/EgGAmQD8/kAcgT+YAAwEAAK
+/MCAADAAIlAAUfUAwK+gEAoD+QEAMAwUAMIBaAAgAAAGAAMACgAFAD6FKMhRyAAFB+pAAAowDkFA
+UAMFGChSgKAoAAMhQFABQAgKAUAyFQqVJkrHOWnnMRHOeBWL6Q+0O00dDsWnERGGpr/l1L8q8H64
+zJ8t/uDkztzZ+T7Z7Zx44+z49cY8v8X0j7f7rjs+27vY4a30sO4brR09LOMLY546ccIfTj5M+Xd3
+wZ33xtL6dc5y6ru+D18mu+cX0Yz+LIPfXuGM+3zqbSInDR0sNvt89T+5lqTOm48Z6mn7f2v58Y28
+fHP2dXTe29tnTbr45zcvGvem7nZfU7NoZzGpH09tmqxMTwieU8Jn4nsvbeP1zkz88vVcO3qx6suj
+7l3THZ4x9GcsNDQwjSwmZi0yuULj8ZOx4eD1+PjnqudmLanfc8Yy1fqY4TjLicpmax48Op2+va48
+IwzvPNuaPuLKMIbiIiMonHpPPjE+PMw27Tqv6jlT37Pcx9HCcctDJTqTk18HjxZw/wC1xr1z4md6
+4257pttvrTuN3qrb6eM21csYmccYjhjEREc+RzacG22Jrjrlnr8cu8jsfvTa+z495b7t049k19TC
+Yxzictba6GvK0dSf+TOeEvlPxND/AHHa79znt9Nvz64+zbOPHH14beebtv8A6vV/rYxZ8Z46/wDN
+rjr9/wAGObnuOtnjjuM9OMc8/wAmWWEY1icevDjGR2WnDjHStXbPxcPPX188omM8p08pdn4f8XwN
+jGuuGNbun3GdOJxvjGMxN9SFFZnqYZ4amN3Gy7rqafH6t8MeWMp18YficmODGfJP1Jmurz7lE62W
+aytPHGZmXEwbmOHpHDnlznNaNTuGWWUTEPKP1TKcz/sMscUceeSuHrbiIiYmbTPGMH1ObXRx7bxw
+I7hlln9OcpxxiZtipyUx0Nn9LpWvjk6zybervbvLGeGEvGHxnxmDLXijL9TGfDycfX3meeCy4TMK
+0RyienzOTXjxjLDbkzhinfFntccv/wArU4T/ANX5V+J3Pa9Nvrw837vj1dvjP+Xb/B0cSdg8ljLU
+Ys8ZVhlQAwAFIyOfGQDAAVgQFALwAP5ARgHxAAGAYF+QE5AGBfgBGABTiAc/MIAAtGEoFGEGFAgw
+HQFJkAACnQINAClORCgAocyB1AOOoD8ABShAANAoCgAoAoQCgQABSn8iFCgyACnBFAAwBCoUqkKc
+SlAUCIFqgRoJVC0+QSoBQU6gQCwBPgCqBAKCowVQVGBXwBUgFGAAoKgKAAUAMACgKAXkCowAACgq
+AAHUAACUCgAAEoCnQAAAAoABRgAUAP1AAOgAB0BQACgAACgBgoAAAoCgAFGCjAAoAYKAoEoFAlOg
+WoBQgwtAUAAqSESSsc5XS462ET4jbwZcXXfH1vo77f6v0cNlp4TfQxxwzzxacVUxHmfLvdtbnbPn
+1feOHWcWMfLH7Hsu37ro7TPabfDXppZ6sa2ppw5yjDGHGUuZXhMdTw2/BnfG2Z1kaHJxZ2znp5OB
+373fqbj6OyyiNbZxuo19TCXExOE8MMo5pQja7XsMa3bwz6Z/i4eLtMa3PnHmneu96277/huIzjCb
+57jLH9OOM8eERzUPgz1PbdtjThn1Yck9Mwxbc953GvrZ/mytm+Pi+fwO307bXXDU25Orh7jcxOeG
+OL+tExxy5efI59NOnycOdurfy3Vsp0tXKOHHKJ6+cyceOPpcMs7fEw3upjMzH+HE8Ov4TAzxYyy1
+2nyZ59qPaGXvP3Dh3LueF/bva9SMsNHOPy7rd4w8cXPCcMJ45HnfffcP9nwejT/7N8eP+XX+OXPv
+vni4/X558P8A5fZ5fP6n3n2TsHb957Z/y7uehjudnvtDU2+70NXGKZ6Os8csUo/Kp4Hh/aOz1/8A
+uz45+nqx88eOP+Lr5Pkfe95vr3Pr0zM65xnH14+nV+fn3C9o732N7v7t7U3Oeplqdt3ExtdxqT+X
+W2ef59vq5cFacJjHJf3j6d23L+pp6sy+Gf8Amx0zPlnx1+WcPrPBzY7nt+Pm1/rxftx0zj/pz0+f
+TLE9TexjnMSlHKY4epuY47hhttjHi2dXfzn+WYWCVY6mevFGHqy6/cbycJUfrX5cvDwNnTjrhzu4
+OW8WU4ZxMZt/Uf6fhBsY4/g4c82ftaNbfZRlbDPjlEPPHnJlrxdOrj35c483Gy32u8ohTjMLKf73
+kcuOLDg25dsZ6Nv6mrlNcZiaw8o/T/WTOYLnOZjyaJyyxn8szC/TPJTJYlcfcamWNfzTEQ/XxOXT
+FcPJnOM9HT7z6m6050NLHLPW1NTDHHHm8pyiIXnJu8c1zc+EdR32/r4dsY8bj9qd79s969t56Gn3
+vbfs9fcY/U0tDPLCdSnTKccZmYiejOTg7rj58Zzx59WMPNbaba+PR1cGwYUiqFa/o6n0vrVWk1ee
+ETPl4hW2EanAZICq/UBID5AQCsABAUAeQACgqAAUAAoCnnIKdQAAFAKERhT4gVgT4AOAAIPwAfIB
+8goEOgAAwowlGAAMAwUfgAAMFAABgADBQAwUAMAA4AGAgAwHIFAAAAAYACAXgEIC0YKAOYQYU4AA
+ESEQKoQAAQFUAAAMBwAAOgBgoBOAFYAFAE8wJzArAnxArAgB8QKwUYEmQDAMAAYB9QDAAAKBAABg
+GA/mEAowDAOABQZAfgUGAIgyqMIeYUcACIRPQoTMAGAfkAcAGAYUCH8iA+BQfiBGAcAVgHABwAYE
+fiAYCJAMAwDArAjAMAwiTJWOXLjYa3+XR3bTmMtHDW+hq4w7YZKJiZ8pieficX6mvr9GfGVy449s
+a45MeFn2vbfZO6mdltNXCZxn6eMxlE/3seHoeA9y0/PtjPxfdey39fBrt/w4/Yzrb91j97Fc3raW
+hk+nHKeUT4Hnt+D8nyzly7YrsvbPZO1+9fcW39v9y7jl2Pvuro5z2DumlhGW33WtjM5Z6W80/wC9
+lX9MwnEeJw9zz79pw55NdfXpcevXPjrjyzrnyx8fGOu73mz2/Fnkxr65n83WZ9P/AA+XTPjjPl1x
+OrrfuF9p/uD7Bz3Hdvc/bcdz2ScZwjv/AGqctzs/zcp1MYj6mjHFfnxT6ncdl3nBz6a448y56YzO
+v1bY/LnPnOm0/pw6vtfdO27vpx7fmn8uem33fj+XO083l+MxlEauExqacxFcsZjKJjymDu8/DLPO
+Li48HW62tl+6xmP04S5xlxM/GTa11/K19v5sNydXDLOcsomM4U1iXEmPpzjC565bm1nU3+5w7foz
+Wdafz6kNaelH6sp8+kGO849fXny/HLb7Ht9u65deLXz8flr5/wAMPsL7K6PYtnttrs88tPabDQxj
+HGdSYicvGY5zMzPM+M++fqcnLds+OevyT+5uPl01nHjOZ5Y8n0vsu7dp1dPHS2270MqQsccM8Z4f
+Bmx2fe8OnHjXOcYzq+Nc3bc2M3bXP3PB/wDVV9vtPvntfT+4Ha8Yy7p7fwp3OMFM6/a8pczKbnRy
+m8f8tj0vtneceN8a+rGfXnGPHGev9G32/wAmf+h7H+1/cc6bbdrv/Lv+bX5b4x1x/wBWMffjV8N7
+7Uwj8sS5jKziOCXST3fFjL12+Y4GWtkpmG/HnM/GTYxq1s5beWtH04xUzH4wZ416sM7OFqajni7R
+ynqc+MNTbf722/XrBk4sJlMvhznhE9RhjnGb0cjR2+pnMcJlenzOPbfGG3x8O2fFr3ehlpxMZ8JT
+cE49qz5+OYmXT689Iji1PxN3V1PJjOOmHE2+f/8ApbLDGeMbjSmJ6frieJz7Y/09vqz+x1fcbY6Y
+x5Zw5nfdbd+4O+967hvs8tTU0stTHSmekaczjhjEeEYwXtOLXh4dddfh/wCrqOTGeTfbb4MaNxrY
+czR7fuNbSx1tPGJxymsRPBeZjnbGGxpw7bYuHa7bsenp56eWvqfUnnlpxH5Tjzv8G/x9nM3Zw+96
++We5jbxFdLQhY48oc8WZaY6NXutvzzyw6z5GbVrUyKAGAAAQB5AGCn8wAACsAwIwDAcADAMpQgMA
+wDKDAMA38CIMKRIB+ZQfzIDCDgKFQYUCDCjAMiDKoRBlBgGAYBgGAYEYFAMAwDAMAwowiMCuQJM+
+AFYEfACvgAcfMCMAwD9ADAMAwDAMAwD6AGAYBgIkA5AMAAfgAYBgGAcwAAMAAcgADkIOQoAcgAlG
+FHIAAwH8gAQATxCgKcQASgAKBKgKoAFAUBQFQFOfEFUFAUAAoCowVQVAUBQFAUAoKgKAqgqAoCqC
+p8CgQoCgAFAUBQoEQ4FWgShFoUCIAOAKAoAC0KgQoBQIACOVte273faG43G10vqae0xjPWUxaMZf
+GI5ylxRhty665xjOZfBy6cO++udtcXGvj8mW+yd5t9Tt267VOEZ7jLPLVy088Yzxz088cMeMTEtT
+j+J03uWm2N9eTy8Pqem9h24uTXfh3xb1nxZR7W3P7PSnZZRMfQyywxjlMY45SvwR03faevPq+L6D
+7dn0cWNf8vT7MZZPtNzXuGX96MtKa8HMzCngdTyafk+12G29c/X3Oe4+jlpa87bdaGWOttN1hKz0
+tfRmMsM8eUvHKDX10xrbi4z0zj44z44S3Ez9Pp5vs37JfdvZ/cPsn7DuOWnoe7u3YRpd42MTCzX5
+Y1tOJ/Vp58/+XlPQ+ed32e3tnNOueHk/l2+OP8u3xzjzxnxx1+r5J797Nt2m/wCppj/T2z0/4c/5
+c/P4Z88dfi637g/6X/YHvGdXuXtzCPafuDU4zuO36WM7HVnh/wB7aflw/wDlp0nrLPVdp7pya4x1
+9evwzc4+zbrtp/7tZiY1w4uy/uPn4czm/wBXHz/n/wC7+r/qufLGcPlj31/p4+6XsvUz3W87PPeO
+2YWnHunZMc97pxjETLz0oj62nwjjM4Tj5npuD3Tg21659H/NJ4z+fH5evljOddvk9d23una9z/Lt
+jGfht+XP1fPOf+HOXk+ntruNLOM8o4ThGUWifCcecfODuc7zxdr+nnGY0YaG50dWdTS1NXb6kwpy
+0spwa8UXO2ucTOMZ+thrnfTN1znX/lzHf9p95+7uxZ463a++7vQ1dOfyXzjX04jrFdSJhHXc/t3a
+8+Jvx65x937HJtyb7azbb1X/ADdf8WU6f33+6MY1jvWhMRCeWw0J6x8DqM/2v7d//jz/AN+zW/Q4
+5/Lj/wB3/wAm/qffv7m5dt3Hbt13jT1Npq6eelloaWz0MMc9PUhZYTPGVlBhr/a/t+N8ba6Zxn4+
+rZnxcPBpvjl249dtsZxn+ry/6nlOpq6WrL09LLDQj/taeXGcYXL5cj2GNc48c9Tl2xvnMxNb0x9P
+g0fSzzhxD+PJl9WMOH0ZymW21M54Yyo680XG+MJtx5y2s9jnlPCH/wAxljlww27etc9vz08L6kxj
+p8nn+WHPxRP1cZz0P9tnGOrJ/Z/2195e+d5jsvaXYd33PUmVOvjp5aW20/8A9zW1K4Yx/wBUmnz9
+9xceZtti/wCXHXb7seXz8MebPm04+21vNnHHj47dPux/Nn7MZeke6PsvtPtv2unvHvehufee7xjP
+t/t3tVs9HQ08VOWvvNfJcMeMRjjjxnlM8V5nj9537nm9PDr/AKeuZvvnOM5v+XWflv1Zz08Z0vZe
+z83H3O2d9ePO3Dr47bflxnb4aa258s+rM9OPHWy+I9414mZjhMYwpmOq6nse30dd3vJi/Fju4ymk
+/hxO00x1ef5tvyupy19Xb7nR1tDD6mtp6kZ4YzDicsItDjryN3XXGcZxnwec7vfbEnXN/c7ft+W4
+z0so1NOZz1Jyz19Wf72ecuZlmPJvrr5uftOHfOszrnr4tWHtzPS1M9xuNKY2kTjjpzE2xtMTPOOH
+FSYa9xrv0xnq5tvat+La8mM41z4fNzVGOPCFX8PAramMYatLOuMTP6spcz5GK48GMdyx1Y3mrlqY
+zjbKZxt4dEbOPB53mvqzXN/ySP8A1n/2D63+L+6/b/t1/wDh1d3/ANXAnq/NGE6V1DkyAA5AP0AA
+owAECKAYUCUBQCAWQVAVegDyBUAoBoFAUBRyCoCqCoBQVAUBVBQFQFAVQVAVQVAAKAqgqApwKBAB
+QFAVQVClCAEoVQhVBQIgKAGUCFCqEQf9AUYApR+ID8QUYCQUIAKAClPhxIAAB1KUAcgUYKAH/QgF
+KP1AMAAYAAwABgGAYKMAwDAMBH4gOoBoIAGACkzAQaATPiAYAAAYBgGAAnH+0CgoAAMAwAKAAUAA
+GABQAAYEBVfQACnzAgKvzAAAIAfQFAVQVAUBVgCSEIAMLVfAIgWgKAoEOHiCqABUAMAwAHM1u177
+R1dHRz0Mpy3GGGpoTjFsc8NSInGcZjhP+zqcf6mszm+Dm14eTbbGuMZznPh9r0v2h2//ACXZzpZx
+jnr7jJ7jhEqFMRETPgeU9w5v1trjwx4Pq/svtH+14M43/m3/AJvl8mQ9m9r+3NjqbrebTTz0u47j
+TnCa5PRxicoy/Jjz45RDx4nX8/fc/JjGu2fy4+/7Wrx+y8Xbc/6nHfq+H72O77DPtne89KHhOrEa
+2D4/mVcojnwVZN/jzjk4b8Oje7bkm+dfj1x+/wDc7jY9wnPc6c5LHJTjEcjR5eKa5dj6+rnfvHMV
+U1iYU+Zr/puL9Reze5+7e2/cG2772Hc5bPvOyi+jqxPDPCZ/Np5x1xnwkvcdnx9xw54+XF02/DPx
+x82OdtN7pyYxtrtibYz54/dnHll90fZ772dl+5XbsdvqZY7H3Tt8Yjf9qzlZWjnnpv8AVjPPgfLO
+87TuPauSZz6uLbPTbyz9fw2/a+Ze8+x7dpn16Xbiz4Z88fLb4Z/a9Zw1InjynyNvt+6xt1x0z9PP
+9zyudWLe5/tj9vPeOepr+5/bPbu5bvUxrlu9Xb4Y7qcfD62EY6nD/qOw4+bk4eumc6zrNc518fjr
+j8u2fr1dh2/ufc8GMY03zjGPDGfza/dm4ee9y/0r/ZvdRfZ9v3nZ+kfsN5mpn/p3H1o9IOXHvnLx
+a525N9tsZ8L6PuxNdc5+93fB/c3eadM403+vWf8A6M6sd1v9HH261bTo+4+/aWU8pnU2GUR5L9nD
+Obj/ALk32kzxz567f/7V2/uTuc5znOuv2X99/F1Gr/oq7BGX/je8t/jjHLHW2m3z9ZwjA2//AOw8
+mOmcafdyY/fs2dP7mn83FnP/AFY/+Dzr7w/6Z8Ptt7N3PvDb+5P81w2mto6M9unZRtpyx184wtGr
+9bPjjMtUOy9v97xz8mvHnEztnOL16TXO3njHwjuvb/eNe95M8evFnXONc7fz42x08p6ceXnfsfO9
+MY1fp5ccWpiPI9VeldzLmPb/ALNfYPdfdvsm775te+6PadDZ73U2Gtt9XbTuM5y0sccrRlGWMKYy
+5HmO/wDd8dt3GOHOtznXXa3OMfmvwxnwjR7/AN44ey9ONtM752xfHE8Z9fk9h2H+jDt8Y07n7w19
+SOsbPY6GhC+OWWpPzOv/APMcu235ddMfbtv/APF0vJ/d+v8ARwYxj57f/wAWT7L/AEffa7RxjDdb
+3vO9ziOOee60tOOHlhpQXj9x7jfaZzj/AKdMa4/922+fwaO3939x5cXF92+f/wBzOfb3+n/7Re29
+X9zsPa21190o/wAfuE59wzivWP3OWcY//GIODfHPy4m3JtnH1z7/AEY0xnGPnh1PN/cnf8n/AOT0
+f8mMafjrM/i6H7x/ersf217dPYOx/S1/dGtova7HCIx2+3wnhGrr1UY4RzjHnlyg6bXts+4Yxx8O
+ZwX/AFNtcT1Z/wAmuP6s/Pww7f2H2Dk77f8AX585xxXrnOfzbfV+/Pl9fR8C+7feXce67zd7vdbv
+U3m93uc6m93ur/3dfP8A+7hHLHCOEQfSew9u04tddddca66+GPLH+Pxy+hd33uNdMcXFj06a9MYx
+9PD9vjnqwTebnPUy5xOPKWeh49MYeS7jlznLh7qZjFLz9Tn0anNno2u1baNxvcspa0sJmZ62z4Rz
+8mZ82/p0+to9vpjPLf8ALj9rvcdvrKmnh+WOMxwSj58Tr87Y83fa9GXdr2f7js+OnuNPGIyn6eeP
+GVxcTMzKjhyOp5eXPHy5zrl67tOLj7rt8acmLjH0/Y6PvnbNp23OI0da86v5sdCf1Yx1mZ6w+R3n
+a8+3Lrc4eN9x7LXtuT042xn5eePrdPDmJif1Rxn4QbuMV1WczFdNudPfdyziaxpbbB0y1ssdPHzl
+5TDk29OPPhh5zl5v1NnM/cbH/wBc/wAs/fY/W/cXrXKn6XZqy/u/pNr/AG/5berX9fTx6Md+RqOU
+AcYAAADAP5BCWFADCAUCH8wDkCR8Aq/H8ADCHEB8AtAhxAgVWAkIfwgDAfwwABgQC9OIKPoA6cgH
+EB+ADl8QJ+ACQAFAPw9AESBOYKoDkBAUAoE/kBQIBeQACcgEgOAQCr8AJ8glP4QUYD+QKAoA5+YR
+QIFOARQJIFAn8wHmAAcGCgAFJAAAHMFPgABT8QHwAcQHABP4AoCnEBy+AD4AAE+oAAwAAAwIUUgB
+AKeXQCPoBWgDAPoACAU/mEH/AEATIDoA8mAfqAAcpAP5AAJHh08SivxIJ8ClVkACP5lD4gX+GQPi
+AAdAD9QE8mBOJRSCPwKDAPoBeJBCg5AceZAZQkA5AAAAAAAj8AHiBYII5KDnlyAcuoB8eYB8/AD2
+77S96x2n+Ua+ppbPcYzrY7bc6e+jTnTyw0s1Efm4xFMvPlyk8p7nxberaWZx5Xo+h+y93w8nafp8
+m+NdtbjXr1+P+D6j7z9rvtx3/Zzv+17fHte/zWeGtsc1pufzRNMrYTjPP8sQfMv/ADHdcO+deSbf
+X/g3u37zuuHbGM59WPni/wCLybvf2z7/ANl189LYTj3DazD4xOnnXhwXi+uOTO47f3fg5sfm/Ll6
+DHdacmtzh5h757F3zseO23Hd9nlo62MfU09TKYynLTn8ufGJl9Mkep9u5+LluNNsZx+91vcba4xj
+k1/p8fq8/wCLoNvvJ/LrTPDHhzfM3d+PyZZ5fPLtdPfTq5RlbpEcY6Gnniiev1ZcDU3OWn3HC36M
+8JjibGNLxtXbmz+rjHlnGXZdo71vNhutv3PYbrPZd12mcZ7be6MzjqaeUTw4wnHkavcdtrya5031
+9Wm3jjPm3uHnxM42mcZxM4z4Zx8M4fYP2e/1RbDveW29ufcPLT7Z3zKfpbbu8TGOx3WUcrZctPOf
+CeHwPmfuX9vc3Z3l7a78eOvp/r1/+WPn9/xeN9y/t/G137fH/R4/9uf6vq/mx8/F9Lae5x1cMc8J
+jPDJLLGXExPnB0WnuPqx5dcz5vDbcecZmXxh9w/9VXvPs33A792ntPbtpu/b/a95nstrGeWenq/+
+OsdSYyxlPLJ84PZcP9q8Hfdvpy8nJtrtti4xienGM+H4eOfN9J7b2ftuPh0xvptnfOuM7Z9WOuds
+Xwzrnws8Wc/aP/UJ3H7jdz3fatXtefb91s9v+6nOdaNbTzxnKNNJRMS5Z5L33+2v/GcevJryevG2
+Z4TycPeez9trx+vTOfHGJnGMZ634Zz8H0Znu8dNXy4wrQefz3O1xc56PB44r4PH/APVNljl9nu4y
+4nTjd7Oc8pyiIiPrYxxnpzPU/wBvcvq7/jxjrfVn/wBuXqf7VxO8zen5N/2PgFaMa0z9bCXPLHKM
+p/8Ass+y9Z4Pe68OfXH2N/o/3mew9id6jcRlho6vetfLb5ZQoyx+ho8Ya6nyD+9N532mceP6et/7
+tnj/AO4e22zya4nhjP3ep9J6PctLVU4S/hJ4zXut9c+Lxu3b518XO09yojOZWMcZmf8Aadx2num+
+k2znphqbcfk8G++X+o3Ze0NvuvbntDW0937qifp7iaznpbKJj9etl+mz/Rp//V4HrO17fn93mdsZ
+4u1+7blz+7X6dfL2fsv9va5zjl7nH5Zdcf5v341+Oel/p+L4T7/7j33d95ud/wBx3epvN/vNT6m6
+3WtlOWpqZzHPKfL+7HKIPpvadnpxaY001xrrrjpjHhh7HuO51xr6demPDE6dGM6+rOplOUzMxyjr
+xO111mHScnLcuKpylz1lY4+JzNHGLnr5tO/w1Ntnloa+M6erpTOGphlHGMolTHEz4+vgc2Zh3Htz
+YzGz/dZQs9zN8Yn/AIP7vTwUmn3XJ+b0/Bq9r119X+br9nl+DuZ0cp/w6pzbj4RzRpY3drr4dGc+
+1+0aOptNtG81ccHM5/Ryc5TEyonKJ4Rio5nTdzyZ23z6fvep4+4/23B6cY/Nt18fD5sD92e2PcHZ
+t1r907p9PPR1tafo6saunM6mMzKrp2tERHDlwPXdrz8XJrjXTyw+e836mN8779c5z4/F0e2zicM5
+nhGpjlhGcfqxmesfA2/DN+Dk036X5Oi3PYt3q7jKNGcc8IwtfKYxc9Y/6p6eR2mnPrnDy3c9pyeu
+56/UyD/1La/+n/5pXT/ztfV+jbh9F/R8f1P8/ga3+6/1vR/T4faz/wBj/oev+rx+z4fvYUYsCZAA
+AAAAAAMAwAKMAAAAowgFAlAp5gADCDAMKBBhSZ8QgFGEAIwLxAMCMovUgMAAYEfEopBH5FBgGQWZ
+AMBHgA+AEfEoRM+AFcEEAOSi8yBAEfiUX5kDiBGupRXBBGUH6eIFZBOJRWQSJnoUUgkz8ygwDATP
+UAAYD5kDzKD8QDAPxARICJAMA/MIPx5hT5hD+YVAKwgAfHyCj4ASAKEJlARhVYQbgA/kA4AImeYC
+ZAfyAkckBeoBgHEAHwAPjxAnwAr4AHw8gI+IFYDp5ARgOgBv/cAYAAwEz4AVgGBGBZkCPiAc9QD6
+AVgRgP5gHIAAwEzwAMAwDkAwD6FAgMoOWQHxAOQDAnmUX4EBgGAZQZA5lAgFKMgky0UWZIhEgIiZ
+lYw5nlAVzsu1952m6x0MtnuNLduJww+nnGc+E4qOPyMMb6bYtxHJtx767TOM4y+oPsl9we9732nr
+dt7pnlnuO3av0NHPVhZZaXD8uUzEzM4zKPmH9ye16a82OTTw28fr+L6F/b3NnuNPRydc6+GXrG13
+nc88f3GplEzg5+nOUZwso5Qjxm3b6Z6PT8nDpjo3O8+3Nn7r7Dnh3X/yNHS/72jGMRq4uOeL4xMM
+vbc2/ac116fsaO2Zv6JPV4Z8nhmP2U32v7a18dtEaXedhra37eYc6e72traUan/BqRxiJjgk/E9z
+v/cfFjn1xn+XbGLn/Lt5/Xhp69tyceM4z8emL5fTo8sw1M9LKdPWxnT1tPJamnk4yxyxmYmJjyk9
+LnW9ceDDHJXH3Gt/5enqOVMzz8JOTTX8ucNLm5Jy6bfTq0Y7rLTzrM/lj1UGWdMZwy155tMuRhvm
+9LNZ6Ocfnwnjy5fGYOPPF5+bb17jGczyeo/br/UH9wvtzp6Gw2O+/wAz9u4ZKO3b6J1p0Yng9POZ
+vGMf8LPMe5f212nebZ3n6fJ/m16X68eF+Zydv2vdbY/3GPVjPjtj+fGPj/xT/i6zwywPuvc/827l
+3DueeeOpnvt3rbrPKIrFtabTw6cZO74OD9Lj10x/TrjH3Oz7nbjzy7ejN0s1+rGI+h/9Iuxx1+6+
+6e45w8YnYbLTmePGM51co+Cx4nzr++d5pwa/8237nRe6bzinzz+Gv/8AJ9X7/uEYxqThnOepMzEY
+8ofLnJ8uteU4eCy4mHAy0Nr3jtmt2vuu20u5baco1MtrvNLHW074S8ZrnExKnkZa8m/HnG2mc65+
+OMzP4NnO23DyY5OPOdM+F1zOn2Oq0vbXtvQ0dTU0e07Hbdyc/T1Y2WjMcPGKo5s91zZ6Z32zj/my
+3v8Ae9xcY/U3zp8PXl1+tobnaaGO5wUbe9c8NLSx08dHLLksdOEp8V8S4zjfPz/a3tN9d9vTnxnn
+nOb97lbH3Z27sO21e9993ulseyaUTff7nOMdNx004555eWLMf9pyc2+OPj1ztvnyx4/4fa1O67Hb
+l/0+PF3+GPLHx2z/AE4+t4v73+/3uv7kb3P2x9rNtuO2e3dWctHW9w6mGWnuNZ8/oOJ+nC/vKcuv
+A+hdl/bPB2XHjn77OM7Y644/HXH/ADf5s/Lwdh7b7Vwdv+fk/wBTfH/Zr9/8315/L8MbdMvIvuj2
+Lbex/YnbO0aWvG43+932W97jrRwjLVjTyxxxhzOU1ieeXWZ8T1vs3dbd73O2/hprrNcfb45+v4eW
+G7zdxtnbO232fx+14XnuJ1cpfxnyk9zjSYdLvz52ykZPlPCW5LDGfV1ZR7M2fbMt5h3PuOWWUbXU
+j6GljGMxfGYmMpcw48vn5Gh3fLtrj04x4ux7Pts8v5+nT8Wv7hdu2XeO/wCx3fbpy+hvtJ9wymOE
+ZaNYzytE8ZziYjlzcnJ2PcenizfHXw/d9zpvc+Df9XGkmNvh8PPH2/vad33Ptu0iMccotCiMU54Q
+lE8IODj4d9srr+THVyNDS3O408dzjXSz1IjLCZxicoiOLUwmoOHfbXXMen7P27bfGNtszHjJ+1l3
+au+910c9LSz14jU/Lhlnhj+aOURwmJjr4HXbcWP6XdcnY8efzb58MdXm3uTue+7x3zd57rc56uP1
+s8cc9XK2U44TOMQ/hB6/t+LHHx4xjHk+Ycm/6m+c+GHH0dGcpx09OMs5hrHHjM+hy5z8XNrr5Yd9
+2P29/mGvGnus50oiPqZaWMRlqzh4y5jHF8otMeUSafN3WvHrW7w9jycu2NcYmc/f9n+PRkn/AKvp
+L9p9LH6t3W8/9rmrrw6159Edb/5TFvWfv+r/ABeh/wD67t+nb+e/y/Kf5v8AN9k8vm8IZ6l8oH0A
+dAHQKMIMABGBWAmX8AH8eYB9eoB+IB8EAYByA/hAGwJMgH/UA/ECzPQCAIn5AGAATy8QACJAr8AJ
+yAPj4AOX+4BE/wBQD/oA8ugBgPiAaAMBM8AHMAwD8OfiAYB8QDAdSgyAwHAA+LAMAAYCJkAygyA5
+KHwAAT8PIAwKQOhRJlAPiEAqsiD8wqFQ48wqhDqAAEEZRQqPwCDAoEfWQLx8QI/D8AHwAoACAAKw
+IBXwAgB+IBgAAFAkSAYAAAAAAACAUAAAD8QDAAAADzAMAA6gADABBhRgAAAAwgwABgOIUBQJQAwA
+KAAAAFAAKAAUAAAABgAAEAoAAB2nbPb/AHLu84xso0Ms8/8At4am50NLPKWlGOeeMzPyOLk5ddMX
+N+7Of2Ofh4duXb06y/POMftzh6f7T+1eloYaW79y6d95GrGWGhpajwwiOMRmomJlxyaPN977tnrr
+xeE8f4PpHs39o421xyd1cZvTXGceH/F/hl6JntNfb55aWO2znRxhTMxGeERPKcVCXwPL+rGceL6f
+dcuz7TtdfShY5LQczlp5NxbmphRxOHm/P45saWeHi1znOuuMZz8MY65+b0HsWrp7XPT09zjnlt8l
+Glm/0PHheHB03ccOc+Dqe6022x0/9WY9u2ejMzl9Wmec01M4yiZzieOPlyg67l3xMdL+50HPvnwx
+j5sq7X2btsYaeGWEY5Ljh0mInxOk5ts5zfL4uk7juuS5zXx3/qk7J2P2v9ydPLs+eOGt3Ta473e7
+TSWMaerOU4NRy+pGL+Lk+s/2pycnP2OPX4aZ9OM/L/DwaOndXpnxY79pvt7ufuZ3PexlOWl23t21
+1Z1NxGLxjd6unljt8ePP8355XTE7P3LvcdlrjPjnbP4ebn5P9WTPh+1jHdfY3vLt297lt932Te4T
+2vHPV3ur9DP6Wno6cqdSc1Wr5ZNSdnxc3HtrjONsdWhz8l3Y7OeUTjxh9Tni7cmcZx1cnS3ExLjx
+SOLbR2PD3GXN09aM8uCcS0cGdY7PXk9XV639mPu/uPtZv99p6na47j2TuWpp6u9z08qbrDhWctN8
+JmImeB47+4PYse5a65xv6N9MZxi/y5+tsc3acfc8fo2uM4zcbePj/m1+Xxx92X177P8AuX9tPeul
+Gt2bvulqbieM7Hc5Rt9zjPhlp5TxXji4PkHfez952eZy8ecY+OOuPvw813HZd3xeGvqx/m1/Nj+O
+Pq2xh6DtdHSjVy1NtlbCY/TeMtOJ6zETyOnrouTfMm3+LrO7+5fZXZZ1t33fvW12k6UPX+pq4zGK
+6cJ5+XM2uLtOXlzjGmm21+GG32/Zd5zYxjj49tsfU8F98/6pPavb8tfY/b/tup3zXxmX3HXmdvsY
+yjquGWpHl+U9t2H9m8/JNu42/S1/y+O38Mfi9P2nse+Znn3/AOnSZn17/wAuP+n1fYxf2Ho+5/fu
+lqe7vdu4x3vc93uJ0ez6epERttrt9OPzzpaaphjx5xj04HYe57dv2OccHBj064xd8+e2fK58cu15
+uXTh/wBPGJpjFzjHnnyufHbPzy9O0vb+h2jQ1NTDSx3O4mJvuXGPHlPjMR5HlN+525dpnMw63Pd5
+5c4xfTj4PBfu/wDav3t7i9ybHfbTDGPb2po4/U3GrrY12+pllllnM6drOca8MYmZPpfsnuva9r22
+cbf/AGfCePw6/wAWry537jf0a/Uwn7rew+z+3u2du3Ht7DLPHZ4/tu4ZViMs85jHKNTKYmXLy5+f
+gjuPZvdOTuOTbHL59df4NjuOwzxceNsY+v8Adn9rzLt3bNz3PPU09tXGNOIy1NTKVjjEyo5dZPT8
+vNrx4uzV4uHO+3o18/wd9tY0dCNPZ7afpaOOX5pdpyULKZ6cZNDlz6s52y9D2/8Ao8WNMZ8+p7h3
+b2v7bt+nlqauExymZxeUQ3j4wuC4F7TTrdsuq9z9e+cenrnwdBsux77can199GUTE8p5zlzS6Qb/
+ACdzpriasez9q5Ns+vlZnoaerhpYaWOUKMHlxWK838EdHtnGc5y9tpj04xfByO6bbQ2/a9bU3G61
+dPV0YvjpaEuc8s44fUq1j8fE2uzzn1eGJn4vL+97cfNiaZz6sfD+Vjntz2xufcWerGjp600l5aun
+jbFzyhLnPxg7fuO704cXOcYx83leDtM723GXpW0+1nbdnstHcbjU1PrZfrWcYuY/VMRx4HkOT37k
+33zjXGJ9T1Hbe08Wsuc+r8HZ6+WOhGGz2O2jHVyrhqa2EXnLHB1lw5nhzXCOnOTXzvtydds9MeT1
+nbcGnHtnbxzJ9Pk6v9vubfUrP1OTU1pyTX4o5fXhv+rE+f0+yfY+bT6O/OAwEgAD4AAAD8QEgAHT
+iAAcgHEACnAByAfyAMAAAfEAAAPqCgD4cwHzARyAfDkAAAAHEIBQBADlyAP5AAgwDAAJCgQABUYR
+Qp8AgAYDiBOIAC9PEB8QUAcwAB/xAD4AAUAMByBQCfgAAcQKCgBgAIAAvAFAJ+IFAAQABQHkBALI
+KgFYAAEGFPgEOCCp/IIAOIFAAoBAKwAAAwAEAoACAAAFBUAAAADqAAMAAAAOIAAACAWnxBQIrCoB
+QicAAUAFAgAAgAChUoQCgyAACgQAfzAAPLqFdh3Hsfdu0YaOfctrnt8deHp3XhEqYiZUqeU8Ti4+
+bTkznGubGzzdrzcOMZ31zrjbwriau219DT0dbW0ssNPcYzno5TCjLGJrMx84OTG2M+Hk4M67YxjO
+cePg9G9gfb3tHvj27v8Ac6m71Nh3Tt+vGGWtjGOppTpZ4WxthNZbjLjY6nve+27bfHS65d/7V7Tr
+32m029O+s8vGuk7t9vO97LfaW17bhl3HT1840tLV0sarLJK8Oax/zTKOXt/cOPlxnOfy5x8U9y9i
+7nss49WPVrt4Z1e4e3fbfuz2ts9Dae4tzpdw0cJjL99p6jz22KjGMcpyiMssImJi3T4HkO+7ng5d
+7pjOufh/m/xe5/t7u+47bj9HPn1aeXx1/wAPl9zMNPHPHOdXKLOZ06zyhRziOUnTb74w97mZx08H
+Z6G3iIwusMseMKImfN8PPoau2+c46NXfa+DmRobuXoY9y1NGNSYiIhOI5uvE4f1J1zjDT2xr451c
+zbbrdaOeOlqd23WppaeMXzmkYxHWZWLbODfkznXpjDS34Nc4vpx1Z37R7vOvvKY6+pqaOM1nU1Ms
+q5ZP+7C4nTd1n09M+P2PP+49vjGlnV8xf6mO4bre+/e4xp9u2OrqbOMNPb62voY562enhjGU4Tq8
++E5TWGoPp39uba6dvri5xjPl8PseP7nt9sa421/mn3/Kst+0Hd+6Yex9vr6+jtu2autqZ5ftdppY
+bW+EZTGnOWOPCcpjrz5HQf3FpjbuprnO2NcfG/W9j7Nw+vt8bcmkznOfHHkwP/UDl3WdLtPctLc7
+idhrY57be6UaupOjGeM46mnGWDrxeUxK4zE+B3P9scmvp348+OM3H1Om/uTizpya764mJ4vDcdzO
+URxiYPa50eWxz5zhv6WpOrlEYYzOSnLh4Yw59IhmGdY2tOe5jl7fcTjlEc5nn5nBvpXddv3E6O12
+u/yw5cZmY/hGnvxV33F3GdXYaGWhv9bDTy0r7jPKMcZ03GraeUROPGZZr5120x08Pwb2nPrnx+/w
+zj7Xe6vuLv8Aht47Xqd77hpbPR/J+zncamOnipU4qJg67HZ8Pq9f6enqz5+nFdl+vx5z6s46/Hp+
+3xy6Hdbnb6mrH1ba+cSonVzy1OfllKOx49NsY6dPq6NLue6xtnr1nxznL3b2J9k+47rV2+/9yRhO
+OeGOpodtweUThzi6SiXw6Hhvc/fMaYzpxYz/AM38Ghye4Yx5/T/B9B9v7d2329t8MZpo0wjDDRic
+aYYYRwxxg+d8n6vPtc4y6Lk5d+bMw67ee8+0aOvTUxwifzZ/SxltcImZjhznhC8zZ4/buTPk3NPb
++XbXoxf3F37De5R+yynWn9E5YccImYfC3GY/5pO84e0zjH5nb9p2v6f8/R553ntW67jobrPT2uWe
+hMzfS1JxiPD80RM844TB3PDyY4s46x32vLx9Nds4zjOPh+DzPX9ubDsuw3X09GdKdXL8ulecp+rP
+6ZhTDjHjMW4LzPUcXd7c2+L1mHXcvbcPBr+TzzfpWJ7jZ62Gp+eOM8csZTWU8ImMeENODt9d8Roc
+mPXu3u09v0v3GO61d5GG614yw0dLVmMcHqYyoymVjE5eEcmTfkzn8uMdGp+prx7fqbZ8/uv08vDD
+Itv26Yj/ABcowfFY4/mXBrlw8zQ22+GHqOPl1uMXHXw+pv7bY6erq47XKMsNHPLjWcb6kcYiItEw
+p48J5nDtyZx183Ly8GeTOMWY+n2Mt7V2TT2tu2ael9TbYZTqbzX3uX/iaWlkpmNTKcccYiH4ceUc
+VBo79xtn81nw9PjlpcnB2+vDdut8J45+Ws/D8Y7ba/5Ro7fV2+nntc8MdSMtHb7GPo7XGcZiY1JV
+cs85XC0KOMI6rl35M7Yzn1fXt1z9Xyw1tePXE9OucY+fj9Xw/i2+5fTxyjXyj68TFcM/CceE44w5
+4RxiTk4c+Xg3+3zpv9n2uHq7jDDQtr5YzpxaFETjjjWInKfLguRz4xnOejdxjr9Xj9PpGN/+8e2f
+86/a/vMv2q//AL1Mvo2X/Dzf926/D8x2f+w7j9Ozr8PP6fTx6Om/8vxX037fJ83H0Z8VqsAEAHxA
+AAAEAoKjAoKAOIKAQCgqMABWBAVQVAAKAoA+IFBUAAoAAAAUYAABQIA+ACQKBAAKMAABRgAH4AAA
+Q4BQIAoABQAFoyoMKBKeYAhQoMgMoMhQAUPiCjIADqUGQAAAAUADBQgAAHwAFAgFAgfHmAZShAZQ
+BRgCB/IqDBQihQ8wHkEH0CjAMIAAAVAL/MIjArAj9QAVWEAIwowgwKwDAAR+oF8wAEYAA0AYAA4A
+oEYBgAD6gPMAAAMAAAAGAAMAwDAMAwDgAwOZ2nc7fZ9y2u73W3/d7fR1Mc89s63jGXVqf5HFy652
+0zjGZnPm5+DfXTkxttj1Yxnw+L23sW77N7q2WtpauhlO50soy3Ww32MZauOWUuMlMRGWM+KcdYPI
+9xx8vbb4zfqzj6dH2X2v3DtPdOLPFtp4eOm3X7cfTo5fuP2Ls/d+y0tvt9bHZ73ZRP7bUjF4VlRO
+E4wlErpyOPtu/wBu22znOPVjbxbHvX9v8ff8WmvFNNuPE1/yz4Z/dlhXsrcd39m+4d97N320tO/1
+NPDca2nMzSNLDPKM8Z4ROMxm58jue949O74ccuufDrj/ABfOfae55fau9zxb63Oc+nb4/XhnPcMt
+DHKdXaaucxlzxfJxy4Hm+K+GX1rm388dcOw2fcdbV2GOWWE5a23n8mpHP6c8fzLmpMNuHHqcfFnG
+cfW3tn3/AH2G6y1d1uJ3W2yjGI0MowjHGY4tzjOUcI4qTDk7bXOOmJn7U1uucZxtmfDoy3sXvnsH
+ctx+y+tjjvtOazpZ4/TymLP9UwpnwmJOs7nsOTTHqxj8rh07jTk2zrjOb8Mu8z19tucrakYzo5TM
+znGWX1MbSo68U+Bp/p5xhuY1zjHTx/BzcNnlq6GO2tOWrln9PQz1XjOWMZRbKZx/urocXS5zj7Wl
+y5zi7Y+H2Mry0Nf27jo9zw19GNPZY5a+rfUx0cIxUyspzUQdXrxZ5MzHjnM+OXnuXl15NdsZxnq+
+WvdvdcvcneO57jvv09Pda2rlrZTnrxoxOObnDPSnLGMZwnFKGfR+34P0Ndf0+uJ8L97rtsa+n0cn
+SMw0dl3DtOy2ulhrY57KNHS09CdOXOP08YUZeahcTp99td+TOc463Ne57DXXHBprj/LhlnaN3j3L
+a57LfbfDX09bGutpauN8M8GpiYS6nSd3wZ4t8b6Zzj4Zx5Nbue3121m3XHweJ+5/sT7kw9z7nQ9s
+4aWv2TWmdfb6uWpT6OnlxplEw1jPCJiJ4Ht+z/uTgz2+M82ZvjpnE8fq+t8w7r2DnxzZxxY/0856
+XPh8vj0/F6/7L+0vtTsXZNz2KMst97m71tNbab7e6mOP+H9fCcYjRw41r+pTP5pjjPJeW7z33n5u
+XHJjHp49NsZnxmfP6/wd3wey57fi2vXOcT1fC48vpXyfuf3Hbt5r7Hd45aevt9TLS1dPOJwyjLCV
+MTE8YPqfpxti4eF07nOuZlvae+UOZ/A4c8TuuPv49L+x3t3ufu77g9s0dlozntdjqfvN7qqZww09
+L80TMrg8lEHnvfe817LtNt8+Phrj55+lbWO89WM4zmV7D/qR+2UbHQ1Pf3Z9vGjnlljHdttpxNcr
+TGP1qxHCXMWnq38fF/2l7ztzTteb+bGPy5/dn6eDZ4O89Gstx83iXYfaXcMtHR7vraGG63kZ45bT
+s+pnhhnnxicctaNSYWM/3ceeXlHP6Fy9xppjPXOMY8dp0a3J3edumP2vrjYe49eNvo6HestTY76d
+LTz1/p2jSn/DjhOWOES4nnkojyiD5P3fHjPLnOOumc9Pj9zsu37fOdbjGM5+F8PqcffavatfQz3O
+Otray/LNNfPVrM8oUyo48Ua2Mb65xjydrw/qYzjExj68Og2Pb8Y19TefUvp45cdTCZ4Yt8o6TxOw
+m86Ox5e46eiR3eWhu9xttSNpu9HW0f1ZRyyjFxCcTM8Dg25Ma561o45ddc9dc4ywX3R3zuHZpyw1
+sYyzymY22UQ7W8Zb4RLn/Ydr2Xa45sXPh5t3Xm49sTXrnP4fT4fa887h3KN7ucNbfT+qJnHLHGkT
+OUOYzrH6eKxnnxPR8PFjTE1TbfOemc/f9PBj84xutXWjSyjVnFY4xn+ucceTcLhHOfDwOxvpxh1+
+3JL6vLp1+n0+Tg6mznU3WGjr6unlDmMIiZnCOLnymY8YaOfj6YuMPP8Aed7ja665/b+H8fuy4mlv
+t3233Do7rU1sctDXnLHW3OM2j6cZTj+V9IiufDojY34sb8WcTq5vbu8zx8+u3hr5588/H8evT4PQ
+u4e4Np2TaY9v7V9PX1tfKM9bcZZYxi+MY5ROSnJTwiqjHnzk8/x9pyct22xmY+T1nN3mnrxnOfsv
+hjycbae3PcPcsP8AONWNf9rMxE73LKu3nKZURGWX5efCF4HDy91xcef08S/DzZ682vjnPX933O+2
+vatbaf4e5wicpmMbRjGOrEzHDhHN8EdXvy43z0/wbXrxvi6sd90d8n2p7q2/b+562vodj3+jp7nW
+nTiNTV0pnLLSyzxwylS404mcZ+R3XZ9r/uu39WMY9euc4+vz/e6jk9wx23LnOMY2xlhPuP3Jq99z
+1Np236mh2nDhhjqan1dTUwxm0Z6uUKPOuP5cfOeM912vZ68HXaZ2+nh9Orp++9y5e51zjbPp1x5O
+hWw+jX6mf17r6VJtR/qtyouDOw/Pfk6T/dcPz8fw/hPNhZ3LyoUABAKADyCABhTzAMIEUZQCUYAA
+CjAAAIwowKwVHARQAEAMABWBPiBX6AAJ5AOoAA/6AUCfyAdQDAMAAYAAwDANAAD6AGAAMAwDAMAw
+HIAAYBgOQBgGAcgGAYBgGEH1Cj8ADAAJkAwET8wD9QD8wiMKsz0AMAAYBgJniBGBWBAK/AA/ECPq
+Ab5lFfUiDCkyAfoER8QowgygyKOAhElAgrAgCCg+IFZFQqDAMA48AD9Ao/AIcuAAAwp8Qg5YByAI
+DKD8QDAMAAYBgGAYBgH4AGAYAAwHMB/CAMBMgJAeYAAwDAMB/MAwDAAc7tvZO694nOO2bXLczp8d
+SMJh4x4y5jh5nDyc2nH/ADZjZ4O25efM49c7Z+TNPav243uvu9Pc97wjDb4S42jvln/1VlRj8zqu
+69x1xrNPH4vZez/2xy8vJjbuMenTH9Pnn+D0Xb+xux6Wt+42+w0tHV4Rjnp5TjOM9Fx4HS/+Q5cf
+1Pd4/tnsb045n68sm2naNfCdpp6ehox9GZnPcZPLVy08+E4vjMvwOPfv9OTTOvI6zPsG/ac2nL2u
+bnXP9Wf3+ePxcvLt2thNtF5zMThjljFcsp6TljPX4HT45OnV7bG32Zbm27ZsMtzHcO6aMxvcMPpZ
+asY5/mxieFowlTMco4cibc/Jrr6OPPTPk6nue07ffk/W30u/x6uV3P2Ls+7bvLedryxx1stPGceM
+4YZRymZiIk09PcNuPE3+LHOddesjo9XtGOy2mps9fRy2+8xjJ7nKZ+lnhMconp6eh2encY264zcJ
+jk45c5x4fGMOz05xmNfRzvs5nL82HGIiOvy4naYzenm1vVjfHr0zdXR7nsm5z1P3O0ynHVnKZw1Y
+yWXGXExMTwRt682MYmzqeXsdt/zafN6D7e793Db9qw0N5paWvvdKYx19eXjGWnExGMTEf3o8fmdL
+3Hbabb3XMx8HddrzcvHx412zjOdXG91+6/eevufqdt7nn23Q+n9DUw20xFsXwyjKbZRMtflk5e07
+Pt9cTbX1efX6R1ne6cm2fy5mM+Ljdw91+5/dW02HY+6bvOe19s0cP3EW47vcYZTlGtrcnlHCMY5Q
+nzOXj7Th4M7b6467Z+7Hww4O17bb15vhrh1/c9n3Tdbzbd17Zs/3W42NNDc9tmLzrbfKYc4/8OWM
+rKJ5QbvbZ1zjPHnMxt1xn4Za3uuu+OTTl01xttrmZ1zjptr/ABwz/ZacZbbSrljltJh4Zan5cscp
+4zhMw+MHneTjzpv18Xr+Lm131uv/AKO12epo7fPWidaNKcHONFlLmIj/AGKIMOTHqxJ4mcerxdtq
+e4e4aOlOjp5xhG4xrqTEQ6pLlynyOs4/b+L1Xxjg/wBrpnNz5N3tU46enu+9aupnsNjt8fp6O8jD
+HPUjc6kq2GGbxzyxbjGYmDPk2xnfXjxj1XN2x8seX2us9y3224s6abTbb8HyJ9zew9/7F7m3ut33
+e5901t3q56mXc83OWpk+F3yyS4H1Tse44ubScfT0+T4733acvb5xnlx0364z+D13/T99q/Z/3E7J
+rx7j2WpnvIyz1Ntu8dfPRtOWUaenpqMuMPDPL9LnxPLf3B7t3HZcuP0pnwuM4+nybvB2ume2xy5z
+m52z/wBuJ+99c/b725sPZfac+0bDb7XZbfTikY7bSjDVyo/z6mUzOWeU84mT5Z3/AH2/dcmdt9s+
+Px/d5OXk4+O49GM/a7vfa2Hccdx2vXiM9tGETqy/zZTl+jGJiPDjl8og0c4zx7+rT+a/lz+9nrrO
+v0+586bn7O+zfbnd977i3upqd07l+5nc447rKKY6mplOcflxX6T2/J733XNx40xNcSdPH6Zd12ft
+/HvnGZnN/Bj+z7R717P3Tune5z1u5eytxucv22eEzuNXR1pjH/DyjH8+ETjPCZxjHkb3Pv2/c8Gm
+J6eXXE8pludrx78PdbaZ319O12men3Xpn54rJOyb7b7zU3G00NXCdxqV1NXaYfl11pzE5uJiJi0P
+p4dZOi5uLfXGM5+/ydxz8uJc9Z5ux3eh3TZ6OjtdnnjH6vo6sKM8lMy11iYJwcmNrnPROPn022zn
+OKx3uO87xExlqa+jpYzEPPQwzxeMypcwo5fqZ2fHx6bZ+Lc/T4t9M69evxz4fU6z3Dj27um2jaRv
+s9LumzWpo6O4lY5Y6uPDnVtc+h3PDvjXW+WXmccfNwcnXW4+X4PJt/37Q0tTV0dSjxyyx1YzzjDG
+MsZ8cZzyyhx/djid1x9rtnGM4+n7GHN7j6sZ1xcXx+nX72Zdg2nt/f8AYs88Pc+1ju2enluMthj2
+vffR0+HPPXxjG0+M0kw5eL05xnOM9Pnho7dvtnHXbPXw8c9fq/a6PX9h977vtdTuew7h2zuex01p
+av7DUynU0uMT/iYaqzw87Ywc+ebTix+bGcfX/g4OHh5c7ZmvT8en4uJ3v27tO390nZ/W1dxo5Yaf
+5JrE41xiMcXx4REI1eHvc8mvqxjzen7fsP1OPHq6X6fb9Pm4/ddnq6uhpaWGp+6x0Jx0NPSyi9HH
+5azEdKxyyNvt+6zi3o0u89uz6tca5uu2et8sfHHwad/7g93bv2po+0M9xlt+06OvlutPPHHGZnUy
+iYmzUqPzJ8pmTDh4e307jPNnF2ziHe9nzcvH/pbenMnl18/s+ldn233R7l7b2fT7ZhuoyxxlRvJx
+xjdac8OMZTDxj4KevM4efsO35OX9TOs+Xk0OHvO500xptv6vL4ff8vnl0HcdPW3+pnnvtxq7zXc4
+5a24znWy5zP6sp81zNjTONOmuMa4+XR3PBx+vi69c/f9Pg63T2n0NS2nGOWlaYzjPkvPylHPnkrD
+k7bXbXOM46Z8WQ/V9sftrfttP6yp9P8Abadv0urXh/ebtx5cDSnN6vHM+vP0/wAHH/t+3np/Tx/2
+4+Hx8ZOvx9XXw6PHn5nrnzhWEHHiBPOeQUcAX+YE+IBlQaIHUKFQZA+HABADoVRkQfUocOpAZVGE
+ORAclB/ABzAMijKgwD+RAfAoP+oAgFBkDnBQmfQAwDkAwDARIBgADnqA+YB/MBzAMAwHmAjkAAnE
+CsAAAATmBeYEYF+AEArAgFYEAAX4gRgX+QEYF4gGBAgFUJU+AUAoEAAAHwCEhT4gAgFAH4hD4hRg
+AgBQoEQKBAAAfkFAgAAAAH8MAA+AAACgAACgAAAAAPIAAAAAAAAAAAAAAACgAJQAFAAAIAAUCgAF
+WImZUc/ACBAACgAABu6G519rq46+21c9HWx/Tnp5TjlHzgx21xtiZxWenJtpm65mfkzb259zu7dq
+1Pp90e/2mUK35cdbDzjKYU/CTqe49s4+TH5fy5/B6723+6u67XM5P9XX4Z8fsy9W9s+6dr7j0stz
+sNxhjGCx1NHJYbjTiedseU/GDzPddntw9NsfwfR/b/7i7bu9bjOddvhnxZho62w08M8txvPyYw8c
+VNpfDl5HT7a8l6au3x3/ABZxNc9W/O+22Orjp6OcxpZYROnnKico5xPHlE8oOD07Z65bOuc5xfwb
+213V9SfqakRpL/uyomseE8uBhyZxjHTDDlmMdWn/ADXR2ncMc9j3CNjOpnMZ5amnOpoTGUcbxhjK
+fXLGCen9TWb638Muo3300xPTnPn0bG6y1t9uNfbb/CM9acZyznHK2nljEcIeEquXTKJNjXTXXXGd
+fBvY07fn0xjHXH4/b5sGjt+ns9x9DZY02ueWWpGjlxnK0xCxmfBHdY5M7a/m8XX8XYY7bkz6M59O
+3llyNbtEamWEaf8AgznlaVlaIcdIjy5F15M+fU5tMZzcdM1rnYaunjTQznDQz4T9WIrnnjMRz4Su
+POZLnfHnj7mtrx5znOMbZx9fn9PrcXue1jVwyy166VcHCl4ZVl/KY8xxbTPRhz7a63O+cYxPp9Xy
+cXZbbW2+E/XwmZy/N9SOOE24xxj/AHnNyb4znoy7e+nr5/c7DSy1NDUjcxqTpaWi9TPcTNcYxxj8
+zmOcRHM456umPFy8/Jx8emdt89MfFzPaX3K9o7Lebnt/ce47XX225/L/AORhuNHa5ZRMJ5/S1V/1
+I3+X2/n21xdbPn1eR193498zGfTnyz4Ox1e8dsnumX+W57XX0taZzxjt25y3mGnhEq2cVxnGIacw
+dX3Pab6aXP5fr8/qeg7b3TTXOPXt6r9V+vo19x9ydt7ft9SN3uccNXTxnONDC31dR/piIlQp8fma
+vD2vJyZ/Ljp8fLDY7r3ntuLTOfV1+Hnn5L7Q3Pfe4rf+4dDPDSxzwjtGlm8NHT0s4+q1jDnhFrTx
+4HH3+vDw/l4uuf6s+fw+mHn+w5+fuc7b8txjMmJjwdfvu0bPe6e73G908tbcZTnMa2OWWLyymPzR
+HKeMDt+75OLOMau5732/h7v0+rOcYxjyz93T6R2X2v3HuH2tsN5p9s3ez2+ptdzfTw1tLLKcscol
+OYj82Mzbh0lrmZ+68nF3Xpzm+HV4rPtu/bYzpvj1a58Jn9z2v2z37u/eu2Z9y77noZ5bfLKPrbbH
+LS08p01OWUxnlPKJ5uOTR8+77g49OTGvHfDz6sduLGm3pxjxbu+7pG72U57bWx1NtjOOrq447jLb
+5Z4xx4ZYY5TjyibVObtu13tz4Y+ng2ePjzjaZx+FYz7c9xdh7/3DU0+6Z222llMztp1JnLKcVEZR
+nnjjnOHCfP4co7LuOPbimcY+5t8/63Dx4xp/N8f8GZf+7e2+xaE9v7Hp7fT22OUzOEfV1pnOeczM
+Y5ekyaf6e+/XOHRbdtzc23r5LnP2MT99Z7T3r2v/ADDs+3z7b7v2GUbnZb/baWeP1Yxj82nnKxme
+EOG+MR0mYOy7Tfbi29O/5tM9M4+DPixt2+0zm658vh9Pk8v3H3A7r27t2c952Wj3PueP+HttbRjL
+bYzmph6tZnl/+nEOenU7f/x3HycmPRtnXXz8/u/xd72XqznOPCfjj6ebpfr/AHM7hp6m83G40NGc
+4xyx2Wphp2zif0xlhMZzEypTyiTYzjsuLpjGc/O/vdl69PB0PeMu7d009DV19nGy3Ns9DV08cZrE
+4Z8YxnLj18fmb/Fnj0z0zceOHNx7eu4uMZv3NOOt2T25pT3HvPtfZbjdxM5aW83GtnOnjHRaET+f
+JxwiZ4vjyN7tuXPLjOmu+c/KfvdT3vH+nyY2x6dMdc7b56yfDXzznyw7DtfuT3P7n1NTuffNDHS2
+GrttTS7Vt4jT0cMdGfyxEYxHCJXDpw84OP3HOnHrji0z1uM7fT5NX2n/AP6eX9X07Z0xn8uc+efp
+8OjiTsNTOctzljP09TGmWpjE1x0onjlEwuHHgzS/V6Y1vh+17XbXizvcYufl/N9vw+Of2tE4Zdw3
+eeWhjlutfOa44zExOOOUyusy+PNwcmmPRr16OHbFxn1/yz7vi4OOvvvb8a2236ndVynT2+eVrTMT
+GMzjjlMwp4s2fTjl648HW783Hnj6ZuceHj1+1xNllhraU3yxecTFl48kuURP9OJeTEy5dN/Vi4n0
++H+La2G2x/a1zjLLWxynCNNWeMcYyxiX0hfgcvJz7Yz08Gjt2mm+PDw+n+H4NGe0ymccsM8Zwzcx
+nlFpnnMysePUmOT4ubTPXEcXS0dWYyiP+/p5Vyzy6RHT59Dm2zjpnycevqzceeMtP7f/AAr2mv6u
+n634tMvq6sf0/m8uPVPlYwAQAAAAKAAAFAgDqAABQIAAUBTmAAAAAAAABQACgAFAUAfEFP5gAhIU
+ABAAFAAAB+AKAAhyABacwUABKoEBQLQAEoAAAAAKAAAKAAAAFAAKAoABQFAAAAAAAAAAAEAAAKAI
+BQAABQAEAAUABAKBAAFAgAAAAAAAA6gAUAAAAAAAAAAAAAABQAAAAGAAAAAAB8wAAFAAAAAiQAAA
+ABQAACAV619nvZPf9x3mO6bvaa2y7Nq7bP6e61dOYx1ZzmKUjJWhw3B573bueP8AT9GM4ztfB6v2
+DteX9fHJnGcaTPX4/TLPO4+xffG632GjOnhNv+zqbbLHDbRj0yytlbj1sef17rg11+lfRNdca4uc
+vWtD2j7e3HbNn27uenjqTttDT0s9S2T+phhEZZYzipiJycnn9u429Wdtcyssc/Jjw6/J0W69n9g2
+Grlqdt7hp6mlHGNDW1cbYzE/8czETHoTPPybdM4b3H3ONsfnw6fu+12u322ruO460bTYNTqTM46c
+zxlOIc/CDPgznO2NdMerZh3e3HxaZ33z0Yn7m7bn7H2/bPcmnvdLa6Hc8cdTYbjRytOrhnF4mkxx
+x6ZW5TzO57Lfbu99uL05znTxxnyea5u44uPGOTXbH5ulx/D4fN2G07rpe4tjHdNbRw0t1ET9es4x
+p6k2iPqYJ825xj5Ge3Dnh29N6fTo9B7f32eXXGm2PzNydXTxxwx1dfS2UamT+pramOEZpQsbT+PQ
+uNLi4xnafDDZ5ufTTabZxrceecfTLVjpb3Vz0tTT0tTX22tnOltNXDDOurlj008pxi3yNnbi30l1
+lw4eHuO239Xp2/l8a4XuLsHv7e56mj27sevtu344YYamejovPVmJtOU5S5iHCiPCPib3D22MYxnP
+XP7Hl++5v1czGcT7PJjO17X7s2mpjhOjr4V4zt9bSnDGrc/lWKj4Ge/FjPk0tP1ePGOuZ+H4Ml3O
+17Z3jssdq7rt9Tb62WcYzGm5nTfHHLGJyUw4U8DruPbl4eX1a9fr827nj4ufXONszOPDr/HyeY+4
+ex9r7Zu9PZ9l+puY0oW73uvMRjnqvjGnjEQsceXWZPT9tz7763fHp+GP4/N5nuO2zw3bHXHx8PuZ
+P7d7v27se1jbYaOWWU/m1c8VjnlnMLi4cxHhP+88533Hy9zt0z0dr2mnDwa3bbHqnX+D2DsHb+wf
+cPtun3mNDQ1e7bLGNHcaEu+OOnwwmWl+VJ80jzfPnuezz6MZzjGfxa/r7ffk6Yxvjp8en0+fR3WO
+jo7bdbPQ1dNfRjOdTHDGV+fTnTcx05Rj1XQ6z152xtnzd5pz6zOMeLhZdt0NfW/cfSzx2+EPR0Yf
+1s5cVnOJn8q/uxMx/sOb15xiefx8sNn/AHPpxjGNuvxz5Mg9qewN13fHuu8xj6Glp6eEaG3yWVcs
+pmcYmeHJS+R6T2f2zXvsbYztPROvxro/ce/xjOuM5vz+Dp/2+70+26vb9xq4zt9Sc9TV0sM5x0lG
+X+Hw5zOUxZfCOR5vuNOPXmz6OuMZmNm/2/SbbfzY858fl9Td1NrraPatTcX1cY0sPoY2n8kxqLDj
+jEcG/E1vVjGc4+1ycvLr4TrnLzbedt1J3n1+37vDHX0fzZYXmJjCeUzCa6Hc8fJj0zfHTLPPPieH
+R2+39w5bWYx1tfbTusIm8fWwmIXlMxETPqcH+328cWfU6/kmemf2Mn7D9xNljq6OO7znQ19TKMcM
+sVq6b/6sZleb9THk7XM8HVcnb4z01+P2+DGtjvuxafvnR7dudvo6mhq6+eM62eWb09LVmfozxphG
+WOUxGf5eUt8zk5uPkz22d9c5uPx+Pz+ng7LXuebXixrfTjpieGfH49M9XoueGeN/3HbP2saOeU6W
+cZ4auGenES8onCHj/wA2OSmH1PN+rG2Py73PniNfj5NtszOc2tzbdh7X3aM89bb6eUYzMZKI4zMu
+Zc8n5HDt3XJp0xlnyc/6d/ewL3z7S9j923W00N13TZaW52mpOOn27U3Gnp5xqRKnHUiHMOOP5oj4
+nofa++7zh02zrpmb/wBU/Y67k7XXud8fqY2mPh4Z+n72W9o9qezdHb6WXcNX93TFTo7WYw0MlxiH
++qcYS5xC6HH+vt6s5z+LY5fcOfXHo4sfp48MXx+rHy+9i/3R3Xs3HHT3+lu89rq4Rjhqduwx+rjn
+hpRGOP08cYmcJjGIj8/5Z8pO07PHJvn8uvX4/T9zZ9r73l7fG2OXHqxnz/q+Pn44/Z9TyPunuHu/
+dNrqbf29lhsNDL9U4TH1s8I5Y5asQ8fl6nfcPBx8e2M8v5v2Y+zzOfueTlx437fpHTbT2x3vHb59
+zy089eIWW4nSyjWywy8ZVplcGb2/Pptn04ODk9P82eqaW5x0PzROWppTMzqRjwh8PzcvLice2nqx
+83Z682NeuPD6dXZbLdaetq5auMzOnGWUYTMqazL5Lh5cjU5OOYja4uX1309cX6dGqNT6uGehEXeW
+URkuKmZSjpEiTNYy4/Bwlho62WU6UrVwnLHFTExlE9EbGOuv1ZcO2ca7X5fscn6Oz+lf62XK1aSv
+qNqvM47tfA/Vx6bM/VP3eL//2Q==
+
+--------------Boundary-00=_S8LEXFP0000000000000
+Content-Type: image/gif;
+ name="stampa_girl_line_de.gif"
+Content-Transfer-Encoding: base64
+Content-ID: <F0E30D5E-C01C-4CC4-BCED-F05ADA8739AC>
+
+R0lGODlhRAKMANUAAPb6/JCLqvaVHgEAALkZTfI6X9iIT7JpYPmxmXWIxIUROax2i85OcmYPMcjV
+5tnZ2bjI3am30AAA/9B5FWt7trNmCyxQgllll9eQfFBcj5ZWYv3Os5Sm6HVzm4aY2AJFd+qhhvS6
+o2FwqPe3TQs3W8Kzt9rk79milJCozKiirMJ9cMGUm+93jc7GyKG0929FRHhsdQAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJ
+CgAxACwAAAAARAKMAAAG/8CYcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW67
+3/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiYqLjI2Oj5CReAKUApKXmJmam4QCI5acoaKjpKVfnp9C
+lausoKavsLGyiZRDqJaUBbq7uiy+BiOfrrPExcbHbLWqwgIFBM/Q0c+6DCzBw8jZ2tvcUJ6VnwbO
+DCoqIOfoIBgqDAQK0wUgqd309fbZlMGfBdbXrazBMDB4F28DtnsIEyqMBE7frX8QG05jsWHEwosY
+Mw4CBzCix4YqCM7TSLKkSTi1WE2Y8LGlJxDPGIw8SbOmzTGUVrrcCZOAtf+DN4MKHerEE0uVR3eu
+qiAAhAJ+FolKnUp1mT8BOrEyVVpp5QgVBAqEAFq1rFmM+bpWmLB1LdecLDc8+3m2rt2ErVZiQADi
+6FauOkdgCDvzruHDxlQaCIGgMQKmWZX+lRsvKuLLmE2l1erYMYa1f12yBdeObubTqDOxqrAYgYEK
+FUC4Xum25YTX4AYXKJy6t29Eq9iGgA37gGwQTEFHDs62wuOkI54iIPu7unU+S4mz7ozg80oD4LUT
+D18BgwHoYUFQv86+fZzN47k7Rr5dPncDjWsLYMBvvfv/AJ6RFlvIyWafa7AdaN9nlRzgk38BRihh
+F0tNgABjCiJoIHfqlIP/gYG1GfDghCSWeEpOa12YYWPnLHjAixocYE5fLInIggAGmKjjjlV8s5KF
+GK5onwovDGCkkS8YhwBbExBAzgkYwJCBCFRekEEGPGappS1wVRBCkHxtmGEDRsJgJgxHNiAbbD4h
+sEIHF1CQQAIeuOCBCFhuqaeJXQngJYYgyKjCXhkeYGQEJiSaaAdI8lXBAQgEIIKcc1bqggt57qlp
+hJVAdmGgLzQg6gsaiMmdkYqmagIELxCKwAkdUFqppRxkuumt7B1gACWernAkCWYe6Sp3KqSgaqow
+dLbABbM260ICtuIqbWoyfonBAZAZOsALDqQaAAkvHLhCAIlCIMIFIiBa/4IG5YBwQgayNlunCNPW
+29sBX34Z6GvaPnBsCskeCAMEJohgwcEwyIiOmyI0O6sLtdorcWYHbNBZoBNs8EIAACgKQQQfB8zX
+h46pAIMDBmegsJgLxDtrnRRMLDNi+GJgs2wqnBDCCRwnisLBFkCQrAoy2vyibGcmvFeL6pwQp8N0
+cmDlzFTbdcC1RvO1QQggwNAxBEBbkIHSgKoDKQgeGrhhB1e6TGcCDU99xJXRVm03SSpg6CGGMLRg
+QgRAjz0o1mjLCOl8stncGJwJUCCnrBSgy8GlHsg9BLweeEBB3Xd3rhAGjGFwgsWNXQujBmZCeQLR
+x9lMqDqKN+a0y3heOf/pnB5M/mymGeTOwe8Rey78QoZvHebFaKvQWDkKdlj0u1BHX+nk0MaA+cse
+cD789tvIaDx4niUuZMnKu/u09JVmcPAHFlBQZ/YZVEoBlRRwEDP3+HdTnvEcjtxuhtdSXmNWwCz0
+zUkEJEBBAj5AAgvMiQPQ8kDj0GWlzGkvfxiMxQRUwD++GCBxLxpUOtCxjhcpyTGxMuCcMsA+C3yA
+gQ1LgNTkdIEApCAF8cteBndYjBHgi3QswlnyiKaBFxjxiGR6gQAdEz8VQosEL4xiE+nkOBHcMAUN
+0yEPtwiLr4wOTAvSwJHGuC0xrWCKzYrcBda4RhJA8YUkoBSe4EaBKzL/S4tczKMoPGGAHwqJSGQ0
+0uEcs4AYNutcAUhkIuGkvoMVkAIkyEADO3DF3uFRj5hUTTg4CEb5BCqQAxjk4twGrQ4wMgARSGUA
+pqRIaH0gARZw4w0DcAH7ZfKWmqSE9zrJHQyIcVRGFCUCUpjGDKSyA7RMQSojkAL+UEOSUSQBJTsg
+AhdcAJfYjAQzBPCi0a3oADHSgAYY8CLuFNJhm1MlMpW5zGgUgAFvZN+VLmDBbNqTEd9YRR9z9qUD
+BUoFLDhAQIeFgHMWU5k4ZGc7odECFsCTgR/Iogvodc+KAucTnzjKSkJ4AhX10oQnLJ0KNFDAZmUA
+lRFA5jJT2Y65mMCh/wc71wEtStNC5PMTW6ENJfY5ODEtrDtEO4Cf0FgpGFAyBatUaARWEA0G8GcB
+dbLSBWtK1TwYpVNdYg54XkSyi2kgKROAQUkXIKhymPUAK1AoU93JgKhW9a2AwMoEMuqX5SSFV6Xq
+DFjeQZsJTImsBJ1PCRegzBW0NCxQxRRcF7sHXnXFL1hZCo5WkFYNEGCJQHWHAjbLGhmNr3ArWCkz
+6alYxprWDkzCqmPvWoECLGAFJSjBMzDbmJYuYAEECOlnD6DQFJiSTlM9rXDX4KOkQGYV0VDBA1rg
+VPm0tAQtoO34+MLbCNDySo0brnbloFrHrsIZLDgBAWC7Vga4TiDvyP+tOaYrnxMcIAPIZFv1tktf
+OITmru14Z2yl8QxxWhYaumVvYzZwgBI4IKUZuGZ9F+wG2PTVAGldwTsfQOHDQsOpTm3BA8ghYPls
+4ATLmieDR9zg4sz2pQx4gAKY2o4SPAAsF6bw1eZj1vUKaWsaSHBwSczjMbgjRi1t6Qoe4OINN9XF
+KxhWoAJAVoVlaGvv7bGU2fCMw7VjIOOl8HJLsFYCaDnJei1kAclqY/kw5gCXoxvdpszmLxBAA19E
+QDvSm2UXy9ZJXG5BCQR4rQXMaY2VIrOpLkQ064mgTpeiHJ6okGD5KbjNUnbSBibNl6ZS2LAWJoCG
+jQPOAMwKXrMic43/D+Be61Eg0RDznTV3bIQppdp3loP0dkHazUljiAAlEPIDuiwNp6IVBre9raQM
+WSlFmnJ1QqgmxGpnO5ixmggZ+F3k2nanZ8vaolcLwaQnjYEFWHYFJ1DAnfnrVP4+7wToJhoMoOa7
+BCxArPGD2BqNbaUEuGBzT4CXnS6ATFoemqLXNm21bO2YEAgkBEjlLzSG3IIhB1nb2952CFTgZ4ch
+mgMSrOXuFGnseAOcCRe41OY4LjlrB/yWVyM4h64GgAdYttxN1TCFYaztfg54292OXu4ulYFl15Dj
+i7wjq+M90X4rsokmP7keOdjBzpiu3Cne8HIvrOFcayCwjYH4Bigu/z0JVvNORgd6nKTWhJAvO+wB
+SGHSlb5FUjedOy+ChgY0LPN2aHm5KZBuZyAeggOgr2HP4jfQOd47fCtB488SAdrhdEm2V5TpGQqU
+g6pM4T0/w8UqMOwKhOlhbns6etGGoOAHn8g4Nb4IiIcgIoEev4873p5uB+KBiAZjTV/6wgDW+94n
+DeJAB/u2Ic842jkOp1pxLsG6g2DjTir2yL0el7zQxQ956fQXMWCtFG5BNF4+4xXZ2u/OjL4u/Kx4
+0ge9cgl4tBD0fSnl/3nxk3q+HsXPCwS83T4vQnJMpJGCAhztxpN2AOHnDGHBCwwwcubHOIcmNXTj
+Pu0nQbMSOUBHTf+uJ38YxAvutAssMD4vMmS1l2UrsAAt4H8BZh+25kwKBw8MMG+kp4Awo2wYB4FQ
+A1+ENz8WyEO6kIIFqAvollY3BG5ORzSZxnAp4FDXMj5fkoM6CA3vRIODxzhyAjG940Sb02/yZW2g
+lIVauIVc2IVe+IVgGIZiOIZkWIZmeIZomIZquIZs2IZu+IZpooRLSA0tIHN1WAIp0FEIUA4vsll+
+qAA2lALexjzjI4dLOA05NnygZnqY4kRzsjnzBDcWAIY3ODEEeIjwgAF3p2UpsALd8QSc5xjodkMo
+yF9/SGdhYSWjBycFdDu14ohpFGuVODyGiIk8uImVl3eg2F43FFv/LXACtahZp7hZ09AyUhWJy5c+
+sPiItTOL+XOJzzCM/LULMreJKYBmThBSJ3BD1fgAuxANwyiNYSE/8TIlj1hSKjQ/V+KMz3iJ4eiH
+0tALuEhk2OgEApRW3fgAGGCI73iK0/B5s4InJRU5BjQ/UsWOGeSO/UiMTHiLm9gC9dgExpEC+bhc
+3xiNC/mHiBWBaxQvVIJOBrlmCLlDBJiR8NiQLICLLRAAUCAjFamPcmiS/sgAj9g2xbRGaiaSI8lF
+JSmTqKgLuJgCUrAAWhZbWnaRPqmRDCCQbmN4O2lPzpCUDFmAaXV3QhkFRcaN2ReTUvkOS0lKkPiU
+91QAXTmVrnVD/5wYBRT5AClQZBWmkF3pJNGzaGI5lmWJigxwQyygYVf5BAEQWymgZcDIlWVJAI5z
+mNO2dnWJP8IYl9BAigyAh1LQiw8wmNB4l+mVk4q5mPnTmFIZDW2ZAhqgABowBT9YgNKAmQTAmYvl
+mZ/5DKGJWwowBQGwAgRhine5mqz5Vq6ZlI+5XLJpmqOpcJj5DrsJV70pk7/ZArjlDlOAiuCYm7p5
+nFSVnMpJAIFZAgfgh89JnKpJnbxpnSb5AtlpWbP5nNBZnJsFnm+lnhpQns6JntCgnuvJnu2JmRpg
+Z+ZZBRhJn/YJV8WZn8sFA/XZnfR5nv95n2UpgssVAAUqBQf6oP8JSlV3yaAtkAISCgUHOqGmtaDQ
+xZYIip7FyaGn1ZXveUWlaQUjSqIlmpQCWgIwgAWYyaLa5ZN9w5Za0JU0Sl+jmZEBQGF9KaMyuaP0
+1ZYlsAALeUUsuQUZSaT1RZGSmQShGaROuphQSqVFEIhVepyBiKVbmqAU6aVf+p+AOaYsGppLaqYj
+GQEGEzZuCjSUhIeN9KZ0Wqd2eqd4mqd6eqcXEAFquiXMciyCekUlIKiGeqiImqiKuqiMqirp96da
+YgEmAACUWqmWaqmxdamauqmc2qme+qmgGqqdagIWAKlZIqmimqqquqqs2qqbSqqmyiOo6qq0Wqu2
+2qqwGqs6YgH/DnCrvvqrwGqpDlCqumoivBqsyJqsrTqsxWqsvaqs0BqtncqszUoix1qrRoKs2QoA
+27qp3QqtuVqtEnKtn/qt2/qtqtqt5zoAlIquluqu6cqunhquaCABEgAF9oqv9zoh+RoD/XoF/4oF
+AVsP/Tqw+ioEBlsEQROq5iqv2Cqv6uqwngqvqUqxlUqtYGCvGqux/rqvTpCwSQCyUvCvCUuyHtux
+HEsEG7uvK8uxLYuwJ+uvMIsEK1sENQuzG2sEL3sEAyuyHTuzIeuzQEsFJssIOSsGBcuyMeuzSTu0
+RBA0iVKuEruulTpGVcuu6JqtDduuDmu13AqxXXskVyu2XPup/xiLtDE7BkL7BEWrs0qrsh7btggL
+t3RrswH7tm47BHIrsz8btEyQsij7sWkLt4PbBWtrCIcLsGkLuIC7BE1rBFA7qRM7tWBbtl/LtWQ7
+thJ7uZyrrpcbsZZLtZw7rcSqtidrskWbr6gbtyl7s27bum9bsyV7r3e7tHjLs6e7uH3Ltwbbs7db
+tzRbuISrt6tLvE5rt7vruk37uDirtC7Luqubu9EbBcx7tCgLstNLvMvrvNL7uIxbvEDLvE9LMIqa
+hYmCKiaAvuo7AIZ6JMeyvufLvqkyRvMrv/CbvvYrv4YKAGcbBrUbvtxLu7+bvH3bu3jrvayLu76L
+vIGLu8Crt/8EvLvGW7eNe7wMrAQCLLMGDMB+27YILMAZzMAefMAkzMES/LclXMADPLQIDMGym8Le
++7MhTMEprLCI4gA4nMM6jMNGosM97AA/HEg8PAA7nMNjtMNBTMRArMRGfCQ+TEZDHMVLXMQ6zL+l
+i7Y0bMJafLzia8ItbMEN/MDJa71Ou7MHbMEL3LzQW8HXm7QFK8NjDLsAnLoJrMJevMa3+8V6XMPa
+u7gwzMcTbMcn/L+CHMOqG8d7fAQWcMNU/MSOPMWQ/MhF3MM/LMVJjMREXMmV3MRS3MhVDDZlQMhm
+nLvaG8hbfMdaPLtpbMo4+8C9a7wrvMqlfMIXHLKoXMjCq7r/dZzIuMzCfzzHckzLjvu9t3zBX2zK
+o+zFt8zLNmwCnszJ0HzJPNzJmNzJ0qzJkCzNQGzJSrzJRbwqV+y/pEzLhDzBabzBvSzMyFvOYMzO
+rzyzsWy7DqzOYGyzywzIfRzMvMzM+xzP9Ry0I3zKvnzK7LzM9yzQzQwBCr3QDK3QRsLQDw0BES3R
+R7LQE93QEx3RGj0AFl3RFN3RDu3RHz3SF93QBAM4oTzOJFvG8kzAXZzO1Tu44iu348zSOju8wkzT
+Wey3jpu3MF24Af3TBz3U1YvQGEzM6WzMNTzDEazHPt3PiowoJj3VVF3VVn3VWJ3VWq3VfxPOGVvT
+1ivKjXu08C/dxvlcyrOLzGRs1q58sy3dsnhst9+71m0M1D7N1mU9xkSdx2Gty31szmsM2CNru0FN
+uEsdzH991gV8126NzzGwyKuy1ZI92ZRd2ZPdLShtCokrrngA2Zb92aAd2pTd1WgL16Z92qid2qq9
+2qzd2q792rAd27I927Rd27Z927id27q92679tFIt2sAd3MJ90l7N2e1hASjwN6K13Mzd3M793NAd
+3dI93c1tAj9j3BGC3AdG3dzd3d793d7tANeN3QACLyhw3uid3uq93uzd3u793vAd3/I93yjglOTd
+HhFwAXu63/zd3/7933Tap2AQBAAh+QQJCgAxACwAAAUARAKHAAAG/8CYcEgsGo/IpHLJbDqf0Kh0
+Sq1ar9isdsvtepeCsOBLLpvP6LR6zW673/B4UTyW2+/4vH7P7/v/THRDdISCgIeIiYqLjI2ORGGD
+YkICBiwFmJmYLCwGho+goaKjpKWPkZQjYpgEra6vBJosqnWmtre4ubq7T5MCtAatDCogxcYgKioM
+BAqtm7S80dLT1NV7YaqqBQwGILSFdCMjGAfOLCGq1urr7O3uTeDi3+D0IxsqzQXete/9/v8AQ2GT
+R5CeQUIjQDjbF7Chw4cQ4RiccLBiuIUjImrcyLEjljATKFocOWHEQn4eU6pcuRGbSDovR9KpUNJZ
+OpY4c+ps94teSP+ZhEImVFBg1s6jSJPa6jmzAk0BP4GCpDhiWYGbSrNq3dqn57cKBhCAqBCGrFSo
+IkMQZYGSq9u3cM8UAougrgGyT6XmrUqAAda4gAMLpoJtat3DdyeYlamYDoZYfwdLnkyZElPFiA1g
+QEDTacyJBqL+IgqibeXTqLcSAjvWKd27AjqH/IzWKYjQYkwWNZ26t2+cMCu0potYcTEDoV27Rn63
+22IBKggY/U29usrLyw8fHjshrPbv2+2+DHbVuvnzEJmCrXAAvF3b7uNzFkk+I/r7+N31VLxZPoK7
+FcSHzAEEYgACAhgsRhRv+TXo4C1i0ITBBv79R9x3IGjwwoYvHND/30+xGEAggQ+WaOIoPUlIYYXx
+qfDCADDGeMALYoUUiwob5NhBBiL0KEIGGZwo5JBdTTUhi+5pACMMKTQZQIwN1FWBBsQgcEIHIiTg
+gZYccJBAkESGKeYbYoRkwIoIEqgCizAGYMKbcMJAwgB1IXPgCSJQkMCefHLggghjBiqoXKpQNAGF
+IMzIYZXxHTCAm3BGGsAC362QJZ+YeuACBYN26qkWv/x0KDkNwDgnjO3FB0OkDkSaAjEH1nWBnpj2
+6cIFn+aqqxMjIifqCktCAGcAc/YHHgzCmnCBBRZk4EAJySRTVwcX1IqpCx6Aueu23A5xQAgbhHDC
+AWbC+AIAkb5J/4KxlboZAbMWwHAAo4dVa+2emuLa7b7b4rgduRi8QEIJrL4Jwwp1gmDgYUy+mwGV
+Ch8Y6wn23uvCl/xmnOsJIWhHTgghwPAAnB4wi8KriaqAAQYqzFsXDAto4GGdYhVz5aXWcvCjxjx3
+SsyId1K4wMgmJABvvAus2bHCLG+2sjE113VlBhfgzKcHHmS5c89ch+nhmgiofOQKAaArArwZyHtC
+ywN+rV2sC6/AYwIUZEnB3XRfcIGm2G49BNV79qht14T/pkLHYWOAOAgwq7AADDCkPW6i7TG9MIIr
+X77CrLVSoHfVtCbgJ7b6CkGBC6ij7nfhrJ82r4E4Lv60wi2v2f+0fCvPO3PV9/a+p58e6JuBl5hy
+UHrryFNWIKLIhacwkmKpfJjcvmN6tgUfWJBllxdkQMGWdN+N9eDJlw8YWCesWEzCda7sH8sz1xu6
+7yKQQEEEH5Bgv+jZ8vkjkKIjn/kGyJXupO9tC6Oc+zAEPwJ9ZwEVq179LHCBD+QvA7+rWwLy1KQ8
+uUCABAxhUioxIcSJZWEeUlO0WnYADZXqBexCAOeqFzgSWPCGFrjani4QgCZh6YMiDGJWfvEtNL1t
+OxjQQKliFCMNxEpqGLxX3T6nNwvY0IIkiCIFvFe3JqWgA6cDoRDHqBJxaCZcLHIRE2VUqSh27gId
+CIAcA0CtDBz/jVbZI4H2vFgtIJLxjywRhwAOcMAKJWqNqHqg1fgExzpeIAURiEAK9CbHGnouf17M
+wPgAyUmPMGWQhXyfkhrQgBdoQGaKvFcGAhABOtIxkpJkQCYWUL8bkqADXxSB8TrJy5Z8Q0TpM2GL
+ZHZKTqTqMLRUJSsD8EhIwrIAztjGC2zJo2pxQIy9zOY7PhmGeXFMmBiaFwsOMM4YQlCZkeQhLCP5
+ChYggAHTzKIF9OQlbdoTINwUCXteBzL3tM1ldWKhGzEFx0i+cp0LcAUDTHCCAthRb1jD2D0nqp8R
+lAQtZfmJiDxkoCe6h4WhmQAM5renLUZykqyEZQpewYBLMCAB/38CEkVnyhOoFEqfoonNRgvkUbFo
+4CVgceOkRkTUAyzAmZKEBSZgKlGaOpUaoUpRmXL6mSl5NDrNCAlZYCCCALyup9Ez6gogmQIGLEMY
+MAXUU9cqDZBEaCox0egKxqoBAoCtLixjhgL2ShYCgRU88EMqLPf2J7YaVhdQIctN4QqTCRRgASso
+QQlacdfDnHUBCY0f9MK2ApV2oAMwPd5hRzsKz7yVsXR4hQoe0AKzguesJWiB9DZ7mAOMlZkAzBZp
+d2uKqUyVENBkwQkIENkVCCNzGGBAMwjwOtpqJwQHyEAcdxQ83lqXFLGhjRiswgDJwqIVp6yrQivr
+3HuUwAEQYP/m6q7LXkfQxFAUMcBYV7CNB9j3rCw1KwNa8ADm/hV6G8DAAnZENWy298CIUE4FKGsC
+FjDgAQow7jJK8IDoKNS+0joMcgEM3QIj+MOjYIbMznrWFTyAwg/Ab18ovAJjKcyoC2iuf8IVXRDb
+OBStSNUylEtc+7K2BMZthY9b/LYDaNCrmgUPuA7wNyA5WaZWePKNp7wFAmggmO9khitMTOHJrngF
+LWhBqga0wygOlV6HARmTATc61Hlgi1TgEdawBmcq25kKfckRolhq3xWYFRb8VVmiYEDQCMb4dStT
+wdqEoMvUccADXdqUgZHgPRcQT3RNvbOmk6BCRecIcQQoQYn/HxDk75rVQzCII588Z61JPe5g5IjB
+8FC3we4B7mKTNkIGULfFz+0p15u+MT/1fIKYEfcECvCyqVXsik4TaAEDvVpEO3CA7p1OZ9L97Ge9
+92hgC6HSOosjHaul22Cbm5Cf1o4yQpCCAHx3y6w1MX4Vp+dPkwO0tYJ06rbUxwTAcY5y3BEFvOTt
+WWcL4MzMmrfNfV10G7EuLcMAAB5Q1z+zlL8YbkX66l1vBBwgAL3T958gvUWEzxGD12xCpTfFQ4CD
+sdwMp7LD/UmgPz84xaxVKH9FTSVw1kXPHqeh8brkOZMHXARvVrmfOFB0l+9t4TEnLY4e7rFygJe/
+GF+Gj1mL/zL56FnA1atb38RtdAzCnNJ8e3PLAY5BtUbdxuj2ubqtzlz7QqsVFFaBn1dAXvDo+QAS
+ZKoIyG5yLJ1d1zC19K9NPitOvR3u4DLkvFyB8VLjN8nuAVeAQd47wm6Q8CZve/eOQFjF/xr01Frv
+49k7dRbVPMj2bcErKo55Jeeo2L7btZeabvQAtH1Lg+M26i5da4RTq86rP/CEqA7YA7BYGLBIQQH8
+yqIcBX1PmM1+H8H3795jUE+SptrAhw8+TEnX6chPPns3zqJEmdjCrTDxChbQgukfc8Y5+rgsNTHL
+Va899HQDU8bjaBL0f40Hdeo3U+xXF2PlRStwArV1AMxmYv8tkAIOlmEVomf7x3/8x3nnV3hRJAKq
+U340JAKNZIL+9gSItIIs2IIu+IIwGIMyOIM0WIM2eIM4mIM6uIM82IM++INAGIRCiEh7tVcEUEIN
+GFus1QIl0CQnQDkHUIRF2EMpEDMYiH+XwIFauAB084EAxzs9klYkVYB604UzmIC9pIWboAIpgHFb
+F3spsAJMxgS19x0nwDFZyArvpglc2GufNW5adCl5QkOdh4BoyElqqAn79YZbV4FzyAR9dwIN6EUb
++G5KhQl9aGtAgjO8429jWIJQdohrlYgc6IaMmAKPuAT3dwJNooT21VB6aImXyHnhYz0VE21hZ2ui
+aFgcuIf/mcCIPtYCAcArUtOKb1h/sSiLr4AJL2UtnhNBuOiM/7OLh8V/yhgLRQGMrDWMTeAhrGiK
+PpaF1+iLbzQ3/ueMU6SL1FiNmTCO2JiNjFgCUMCG2oiM0PQKUpiPy1gA/meO5vdkALmO1tWO7viO
+LHCKUDA0wbh14ugK+fiQRugMYNQ9i1RmAgliBFmQ7xhZW5cCUJACcAiSr5iMEAmRrlAAMDBD1jJ6
+F/lhydgKJTmLTYJxHvkEFDaTW0eSJWmSsYBv1jKNLYlgL7mTRQgLDNAkLbUA3OgEKdCEjShL+EiU
+DymReXI3dwOUQSmU9wiTUhmRruAkyqUAH9mUrJWHUdmV//pYALIWkFlpY7GIlkXpCgvQhO7GDFDQ
+Q6L2knoFl3HZlpu2lXwZlwRAfw+wUjCZkPnwXYEpha3gl5oGmIu5XIRpmHbpBAGwXO8WmUbomI/J
+lZppZTfJlU+wAJgJC5q5mZx5Z9B0ms2gAaGpV1BQmmcZmQSQmqq5l5qpASBZVof5BF7pkKdZm7Zp
+Z56pmS8AkiUgXmLpm6bJms0wnMSJm5HpmiemAUUYm8XpnNB5Z9K5mNTZAjBwnczpnPm4ncTpnMdp
+Xykgnk5AnuVpnufJmiKZAi+wV7HpnvYJn/F5mm3YhOG5nO2Jn/qpac55VP4JoL5JngO6aayZnvQ5
+Bdq5oP/BdprUmQIaQAWnKaExF5lzWZhWsJga+naL6UU1+aFdGaLJx5dNWQJLeQUliaKieEr1+ZBC
+4EUweqNG0IZNgqM82gQ62qI9GqRF0J8lKqRGWqNNWKRH6pgRcD1H86RQCi8B0IQdEKVWeqVYmqVa
+uqVaegERsKRdUy3pMqZkagIOoKOtUqZquqZs2qZu+qaRkoJg2jMWYAIAcKd4mqd6mqdM2IR7+qeA
+GqiCOqiEWqh/agIWMKd0aqeGmqc6WgKNGqmSOqmSiqiKyjN1OqmF2SSU2qme+ql3aqmXmjEW4ACT
+agKtCKqquqqE6gCJOqr8UqqUylqsWqu2mqeuCquxaqr/t9qrvtqouaqr3SKrnwojt2qsAICsf6qs
+vyqqwrotxDqozIqszBqpykqtA3Cn1Zqn22qt2SqozsoGEiABUDCu5UqunmKuMaCuV8CuWOCuLKGu
+8HquQjCvRWABEFCo0/qtxfqt18qvgtqtjSqweBqsZDCuCIuw64quTmCvSeCwUsCu9iqxDLuwCksE
+CYuuGauwG1uvFbuuHosEGVsEI+uxCWsEHXsE8AqxCxuyD8uyLksFFIsLJ2sG8qqxH8uyNxuzRICv
+byKtAIuteMpEQ5ut1Wqs+6qt/Eq0yeqvSxsjRQu1SjuoBmuzH3sGMPsEM4uyOIuxDLu19eq1Ykuy
+7tq1/1w7BGALsi37skxwsRbbsFfrtXHbBVk7CnXbrlfrtm67BDtrBD7LqIGatE07tYPbtFIbtQBb
+uEKrrYP7r4S7uAR7p1VbBmX7tXpLruZKsZqLs3srt32buSc7sZhbsStrtipLunm7tmo7r6XLs2g7
+t657upY7u5Ybu6+ruiW7tqDLszU7spv7untbsxZ7t7eLu5fbucU7vKi7szdbuRfbvMALvLWru3H7
+t2zKgm8CI9k7ANvbvWQaI2OqvSYgvuILJ0wUKeTLveOrvum7pgAwuQeLui7bvLurujE7urH7uWab
+ttJLtv77tqc7thiLu/mbs5x7tmzLty2Lv//btwG8tf/6O7oMPLYQvL8WPL+m2wT6i8G2W8H3q7zU
+y8G6u8D2K8IO3LMRYKYOsMIs3MIOACMtDMMvPAArjEg1TMMufMMyzMIy3MM4zMMxEsNrdMNEPMM5
+3MLv+6pYK78hbL8nXMJP3MQb3MGsa8DSa8VXrLHzW8D/C8Imi7wp27skvLkXPLy368FSbLoli8ZT
+3MZ5C8ZlPMVdLMe8G8f7O8a0a8dHYAEpfMQuvMNGHMiAXMQ5DMOA7MOETMQ7PMiC/MN+zMIAAAFK
+bLUUvMZMDMBQnMEmXMYl/MVdXMCX3Mm1q8mZzLWkLMAPu8mqLLIT7Mar7MCuTL29e8pKILyxPMec
+XMf/s2zCr6zHftvHj5zIiBzIL5zIQkzIw7zIjYzMRTzMfmwCkpwGlcvFySvCxVvFvdy2E1zKCFzH
+KHvG1FzN1wy7nfzNabzKprzL57zOsOzLURzFfHu56CzAdOzEb8zL7JzLe5zCENDP/vzP/Qwj/yzQ
+EEDQBR0j/mzQAG3QBN3QA5DQCH3QEB3QES3RFq3QAC0s7yLNTCyx3lzJaNvEIL3O5XzCYBvK00yy
+ckvFydvR5Jy195zPrDzO2SzTNQ3L1gy3bHzKOx3SKb26cYzAsWyvfAzNGX3USJ3USr3UTN3UTt3U
+JrDRaPDTtuzSzqvPX5zH0Su61Zy7Wf3JYdzAKevF3VecxenssDHtxfBsvDetxlXdyvJsvGhMr55L
+02Qrx+rcv2U9wrj8vFgdA0X91II92IRd2ITdKlLdD8T7rIoQ2Ib92JAd2YMd1ZPsBRt72Zid2Zq9
+2Zzd2Z792aAd2qI92qRd2qZ92qid2qq92qzd2pyNwkYt2bI927RN2YwNrSgQ1eu027zd277928Ad
+3MI93MS9TiaAApV9251iASjgAMX93NAd3dIt3Q6A3MqtK96DAtq93dzd3d793eAd3uI93uRd3uaN
+Aul33YMSAcvCpe793vAd3/INpV5KBkEAACH5BAkKADEALAAABQBEAocAAAb/wJhwSCwaj8ikcsls
+Op/QqHRKrVqv2Kx2y+16l4Lwd0wum8/otHrNbrvf8GJYHK/b7/i8fs/v+5sCIyMCQnOGh4R/iouM
+jY6PkJFyiTGBhgYsLAWbnJuZBoaSoqOkpaano3SWc5sErq+wBJ0Yc6i2t7i5urtRtasFBAwHBsTF
+xAcqDAQKrpsgg5S80tPU1dZ3gYODBSwGgoiIghjKss+D1+jp6uvsgOHf4PFhIyHKmwgj7fr7/P2n
+84ICwpNHMJCKZvj8KVzIsCGceBMmFJwYCATCfA4zatzI8UqYiBQpThjR7FzHkyhTdgxkQKIhlyEP
+VRBgkZtJlThz6kyX7aXE/wkzY/occbAAgmg7kypdWmrVnAoVfsIUCnKDKwY3mWrdynWPJWhADSCI
+KiCoUAEgR2Aoh7Sr27dwzcx7CgIBAhAzQZ41S9Jm3L+AA3uUKdYugpZAzYrkay+r4MeQIxdaFTHE
+XWJ4gWqeGNGA4pqOJYse3VUmCM9QK5yOiAG13kOJKyB4PUJBARBtSevenfNlBQypMRiOOsFusdTI
+iUFtzVcWi9y8o0vPuAp5XcOHw2Lfvl34bJgHCDyfTr48Q9+eC2+PWJy7e8N454Qfb76+/XWGoN59
+P7uCeu4gqHAAMt55FoYBskB334IM2pJfcZbxBxR/ILxg4QsaHCAcCC7JYv/AgAM2KOKIpMzBHgIR
+8mfAddtp0MAAMMLYwIB3zSQLBhvkuEAGF4jgYwYZkCjkkF59FBECG/Cn5AswdpDCkzDE+EKNGnh3
+QgcXUOBBAglw4EICQRIp5phtmBgRCEnahcGA3vGHAYwRmCCnnA5kMMCUCGCggl0riEABl4B26QIF
+ZBZqaBnzsIdjngdciOGSA0Aw56QmwLAnditkGSigHgx66KegahGIohsICCMJJMTY5nYnBDCpA5PC
+gAEILGq6KZdeXhDqrrw6AaIBKiga5QAwwGqCA8PyV+yxFjQrggMlIKOCd1jeCqgLHoTZ67bcCoFM
+jiGccEBEB8AIA6UmRID/p3sLpGCCB81aAAMyLNp1gbVcdqprt/z2esAJKa4pWwMvUAorsvBhMKtd
+J8AQAbwZaAiCd9edcC++X2rb78aHHpCiXQJu0DAAdIrQLAQwnLDmtHpqiEDDC2R4XV20vtyBCPhy
+IILGHPc8poZr0liXyDCQbMIF8cqrggohWDaxnnUpPLFd9XbAI86beuCBnxfw7PPXJBJ419IoIgBD
+CXImncG8CNAI4rTY0bwwAtVSgLWfXHbdKbZdG5HBnzt7Dfbgo2kY4b8RYmDpCgHAAGTKKoCAzF0K
+zy01ixZjHagIF3T+Z74uZEyECF6GrjPhqO9Gr54bRDjx0y0Lt/Sq3QV9/0BdN+Oru5fZCpEBB1vm
+y8G+qRcvGY1Jtkax3ErGTS+tK2Sg++bNfmCBCJ1m+3fwFPy5peDGh/9WBR4nSevM10ndvICY2zq9
+CCRQEMEHqP4JvPRcBi597+L3H9cE5cOOwtTEpnolTECXMkzuppc/EljgAh+gH/44YLcE+OlJWXIB
++PzHwaQIQAUnSJOaNoSMAS3thEs7gAZe0IAqbcd97yNBBGdoAU7lLQBPupkGO8hDrdBEBa1zD/pU
+wKQYGfF22DkB/m5lt8450QIyjCAJ8EeBv9ntSSnQ4QZ7yMWOCOJfIlSSCl5kRBgdYDuZshYFLtCB
+NgYgAFjKQNL+RAHrOf9QBFjE2Q67yEeVfCOAzZNcGc24nQVoLlBsbCOPUhCBCKSgc29s4Brph8UM
+aG2LfcykP1bxoRCE8T0Y0ICULHRG7BjSWhkIQATgCMdGOpIBnDBkFOnnpACQTgSazKVGnPIhJH3M
+PSrUgDCFUUrD9AmVqgzABZ7kyggAQxYFWMAsp8i5LmFSl9jkCTTm8CEQNo1CyGDBAcS5KhAs4GKb
+SmUjM8DIZsIiEwyQoQM1RcFrZvOe1PiKTEwIsF8SEEQsCtCAlojIDjSyjc2MADkIcAITsIABzsLe
+9/BJ0XaMahB6Yc8xWGbAu9SsbcNADAwOyaUqNvKRqnRlCt55gmgmwAX/gauoTNUxKksEJSKK6QyI
+JNYdmHRmiQvYqVAX0K6TxmITLwXTTJd6jZqG4aZA8QlsigkyBSjAJRI5ABvpJcSWFTUFCw3GS3HJ
+1LJKY1RYxWpZpFqMUBJgVcmwqlwlIrPmje0AK0hoBC6gL7P6VRdomckgyIKWtZppAk8qQQk0QIAE
+GoYcDFCGy+wKnwO005FttOZfN2sLqFgiqh8xLCtewYAWrEADDOAOOVrQgshRljsqYGQWgcQle3L2
+tot4ik8RoQwGrIAAJXhAC5bBAuyogBmNde1rWXWARVotAcTDrXRTkVPYOGcFB4GFXOVKWscu1y6l
+WkELIIAlsk73vKMg/w5aXIKBFCxgE8F9QFhdsQAVLIAAKygBAZD4XeyIbAEB4BGQ0Evg9CJHmCcA
+AAJY8AAN6NcV+Q0rAx4gX8fqaWkddU/rtDrgAnuYFKQlx3EJsIAHrIDC8w3uCto0sQMEIKiT5U+O
+mvvhGo9iGbezSDCYcWIKt3ahLaDwiuNmyCwFwIQZRtEGDuA7IDnZyVYAUo84Z+Mqa4EAVQqiMpDr
+Wwo/4METZm0LSjmxBeTtb1wKauTqVRdxxWB7oYszB6BLhXvFOXSEsrKeqRCMHJkPFhN+gH3nG2SW
+HcDMgKomoF5sQgH9i8l/k3PwshddJ1gSWz7C2dZsu2c9S+sESAoiAv/wu1oTx+IqjlYBQROwRmu9
+sY3YZfIFTMe5J1OAA8PjdAxmrbNEdm5rnQ42EsDoZxVglwEnAIECSrDQ+Z56v0I9AJg+FygP4NoD
+jevcS7fGxjfCMQOk4wCnpecCcXfA2z3yQKWF3ekzihpkvk1BAILx7BK3gAEPdgUGPOnnJAkIndW+
+89a8BN1ze/uNPZqzbWf9pQscXJm1ZXewkeFJ2HoMAIt9djB8LGhXhLBpTeu3uKh9q71hj4IOfzjC
+b73uI1x6UCn39s34J3Erf+vdlWVAJjYu3xL89ipBZvZb/cnvDSiOgdnCtMFVjqWJKgHcplvjw5ve
+8poXuHz+JOArNMD/cfkSIMg+ToF3sVN0FXRgenZTusq9vb0tQj10HpD6w6VnXqvXGIif3I6AXlFi
+4Qo9yOJdAHaVxG9xvS+pIlg60/naN5eLQM61ZbqP7F7ljytJcuTgunAhm939Jtkwfj4AyTfF1zlz
+bu1sz5/XIm06QHX74FiqIuU/jPX1JaO3Xj51Aaj6Hn6bXXe8jjzqA8zqBNCcR3vDVqDkLvMs6Xr2
+Sz1Ajuy6puAehAHJgEUKdj927vD76Ey8tOsVP3cLJrVHWir3nFGp+JvJHvoExrtdJXfin9O3BEQt
+we5p5x5+t23Rr2ZJ61dSqYR6dPdSLsBrWoN2GbB0f7Mz8Bd/08cw/yewAk+yAqBmF8iQYsIVAAgg
+DPznfeByAJHVCZsQWQGASDEHe/eCM15CUgwoYDjDaYNUgzZ4gziYgzq4gzzYgz74g0AYhEI4hERY
+hEZ4hEiYhEq4hEyYg/J3AoklZi1QAk+CAAh0alNYAixQV83DbyBggmDoUgT4euiGM62mM6PHQKwG
+OBbQgxE4U3j3JGDnZV5GhUzGBLzXexvwhWEIhojGalfTRlhyMa1mQWmohlX0fG9IUQWQWk9Ch5BI
+YSlwh0vAX0lkgVgkdn3Yh39oQbZGgFiDZmoIKIm4iNLVhycQiZA4Zk0QYxWYAiUwhw/QUq2gcc3A
+CSk4PT0CKKumO/9202GmyFmbyAkhoIpelgKt6B0WGF902AKcYItHtQm5eCs7ozlVhHa1pojBiE3D
+2AmpaIwB4AQgBIvG+IzQGI0MQI08QnKc0z3u2ERdA4zbeFvdaIIMFoktEI6tGACySIea8EznGI2b
+s46bkohPJo/zOF1hqHGccI90WAJPsADMKFxeFgLm+ArbtV26FwC/CEMEqI0JyVRgeI4NuYrI2AQp
+cIwpMIcXuQwZ+ZKwUAAaAG6HCIEhWWUmGJDQVAD5BXb56AQpSYUTiQG16AoveZTIJQuduHyNd5M1
+1gk6GZP4tgKboAH6yAQrmZJ0aJEA6ZJICZOyoAE+8o7Z6JQ4WZT/GAmWsbCS72VVQPmIs5gJaPmV
+SNkMDPBmB2mWVgaVaakADfCXgGlVsSCR8maUb7lYLemVdAmTBaCXEteSVgWYkvmXgrl1irVShomS
+VtWVRrmYddmYjilsczmZpEmZW5eSKcBYbtkEGpCUseCZdUkAoSmaAOmXpVmaSRmU97WaTOCa2gWb
+R+kKsxlstWmbt0mayJWS+ZiZvalxwBmcsjmce/ZMkXmcuGlVASCJzLkEldmXz5mR0Smdekad1lme
+MBBkKSBXTuCb3wmd4jmdRlme1nmeD5AClLme3tmeGvme8Fmd8lma9JkCLMSbSsAM+vmV4cmfNgYM
+/vmfk0mfLQAD//fZmwf6lQq6Zy7poKX5AvEloAqwnhV6lBeKoQ2qoX/5Ais5hTDwoU0Qoi85op22
+DCZKmpm4ACxKoS5KoDBqZQQwo5OpAUGpAVCQoze6o3omoz5Kmbo5pCFqpOxWohqKol8mBQfqpFZn
+nCaKok9ylU/wnVZKeXKloQoApFtKBZ75pfC3XcepnjGARWj6pkRgoXA6p0kAiydJp3iKBHaap3xq
+BJfZp8MZASaTNIRaqIXKTlTYAYa6qIzaqI76qJDKqBcQAYAKNveCLpiaqceCRZKiqZ76qaAaqqKq
+qXRWqT5jASYAAKq6qqzaqq5KMnb6qrI6q7Raq7Z6q7RqAhZgqv+nmqq4+qr1SYW/OqzEWqy4qqu8
+2jOoaqyrepklwKzQGq3EiqzJujEW4ADRikUtIK3c2q2y6gC7Wq39cq3Z+iS+6q3o2q3gKq7jiq3Q
+KofpGq/cuq7s2i3kGq0wEq/5CgD7+qr9Kq+qSq31ui33Wqv/uq//Oqz9irADoKoJy6oPq7ANm6vh
+ygYSIAFQcLEZi7GgorEx4LFXALJYILIq4bEku7FCcLJFYAEQcKsHO7H4OrELC7O0GrG/arOrSq9j
+cLE8y7Mfy7FOoLJJILRSALIqa7RA+7M+SwQ9y7FN67NPm7JJ+7FSiwRNWwRXK7U9awRRewQkS7Q/
+W7VDC7ZiSwX/SIsLW1sGJuu0Uwu2a1u2RMCycmKwNMuwq2pEd9uwCZuvL+uwMIu3/CqzfxsjeUu4
+flurOqu2U2sGZPsEZ8u1bMu0QPu4KSu5lou1Ihu5kDsElEu1YTu2TLC0Shu0iyu5pdsFjVsKqRuy
+iyu6orsEb2sEcnuustq3gXu4txu4hlu4NJu7duuwtzuzuPu7OKuqiUsGmTu5rouxGou0zsu2r2u6
+sdu8W3u0zJu0X6u5Xou9rfu5nnuy2Qu3nHu64ru9ynu+ylu+4+u9Wfu51Au3aXu1zzu+r5u2Sru6
+68u+yxu9+Xu/3Pu2a5u8SxvA9Eu/6eu+pTu7n2qDcgIjDTwA/w8cwZgaI+jiwCZgwRY8J0Y0KRgM
+wRfswR3sqQBwvDvLvWIbwO/rvWV7veU7vZrbuQaMuTI8utt7uUzLvi3cttC7uaALu2HLwjMcuzX8
+uC58vUB8uUT8wkp8wtrbBC7MxOqbxCvsvwgMxe77wypsxUIct3HiAF78xWDsxTACxmPsAGU8SGI8
+AGH8xUYUxmesxmYMx2wcI2RcRml8x3G8xmA8whWruEi8xPkbvn/Mw0+sv+bLw/3bvvCbtS+cwzNM
+xVrLv10bv1j8vIBsvylcxZosxIx8wIX8yU0cyYR8yaG8woDcv5t8yZWMvqR8BBbQxXpcx7Kcx7Q8
+y2s8xmWMx/9v7MZqnMu5PMd4HMt7DAF9jLwm7L9N7LyorMmDnMrLLMo2nMPH3MLrW8qCbLrq+8xc
+q8WtbLVHDMrcbMXOnMrxW8qwu7zhjMiFjMqTrMrjDM6yC8vCXMu0vMtiHMy8HMz27Mv1DMf83M/4
+vMYmQMxokLyOvMxbzMyB3M1KkMlZbL3RDL7VfNDanNCPPLTpvM6Y28kZ3c1S/M6ta84Njc7j/Mga
+rcLg687wbNErGycQ8NIwHdMvDSMxTdMQYNM3HSMwjdMyjdM2/dORMtM6ndM7HdRGVNRCndQyHdPp
+Uswl3MzfO83iDMQp3dEVncwH7LmLHM2VW8BZbMqcC9U17MP/QczQJu3RaA3WID3FCu3EJH3Sas3J
+KD25GT3KVh3PA73Uer3XfN3Xfv3XgB3YgN3UBT3NmHzM9nu/4iy9rFzAEL3I0avIc03Jf9y+km3Z
+ko3MVqvO5ZzNH73Sja21+vvRir3YpLvRnhzS69zZ0svYJ8zZA3zKXJzXgl3btn3buF3bsBIBTq0O
++DuwjfDKtJ3bxF3cxi3YhG3MT7vczN3czv3c0B3d0j3d1F3d1n3d2J3d2r3d3N3d3v3d4B3ezj3b
+x13e5n3eL53cwM0rFoAC6aJX8B3f8j3f9F3f9n3f9m0CKNDb620o7e0A+B3gAj7gBE7gDrDf/c0r
+f4MCDN7gJw7+4BAe4RI+4RRe4RZ+4RiOAu+X4KCyV5H64SAe4iI+4oY6qWMQBAAh+QQJCgAxACwA
+AAUARAKHAAAG/8CYcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvtepeC8HdMLpvP6LR6zW673/BiWByv
+2+/4vH7P7/ubAiMCQnOFhnR/iYqLjI2Oj5BEg0OBhQYsBZmamiwsBoWRoaKjpKWmonSVYQaZBK6v
+sJuegpOntre4ubq7T3QjtAWuKgbExcQHKq4KrpkgtLzQ0dLT1HqBgoIFs4fcvxgMywXOtdXl5ufo
+6YCFv9zu7CPMLILq9fb3+KfXv9jv/uwHFGSil6+gwYMI37ibMOGfw0AYwhFMSLGixYtRas1h+NBh
+QxAECjCYiLGkyZMWA3Es1LDjoQojImojibKmzZvVVG0Ms9LlnP8KE0YcCImAHM6jSJOS0ilgQoUK
+YYD63NgwHoF5RpVq3co1TyVaTg2AgOp0Ks+qGEKCyNq1rdu3ZcLQElABBAIEBhpKnQpVrsB5cAML
+HnzlpYG7d6E+7duxrFwGIWkSnky5cgy5Z0MgAEGM4YS8e/0xNMBYQLICki2rXs3VUF3SizEAnYD3
+aU+WixH0HCFwLevfwJXilv0Uw129iIstXk6sOOk58bSxDU69OkVVy+0md2ocsffvd7v3TMZiuvXz
+6NXhJn34O8MK4ON/HzvHwFXz6fPrj1bo6eb4Y1XQnnwgqHAAMt09J4B9BeC334MQmtIfbZrFB5t8
+d4HwwoYvaHD/gHEgtCSQASqUeECEKKY4CksUYhhid+Bp0MAANNLYwIGbQRXSCRuEsMEJMGQg5JAq
+FmnkHlTRtgGGTL5AYwcpRAlDjS9sNoEGMK5wgQgJeOABBxwkkMGRZJbpRpIgLBnegTDKhwGNEZgg
+p5wOZDBAlZt1h0EHXCbgp58euCCmmYQWSkaSGCyJwQEcdtjkABDMKakJMKjwnZYU/KlpoB6Maein
+oGKhEkOJGkgjCSTU2KZ3JwQgqQOSwoABCNqBsKWmm7pAQai89vrEgQt6hsGUA8AAqwkOEIuhscha
+4KwIDpSAjArdrZABrppy4MIFvnbrbREHIKDZogwdQCMMk5oQ/wGe8S2QggkeOGsBDMhoh1gHmWLr
+p6CefusvrwfwiJiB8DXwwqSwJosYCBjMeheQEcSbwYcMZ4jYtfomAGa//3ZsaLjfHVDBjzAAQKcI
+zkIAwwmLUouBgcYBuYCHtW6m3QkYY/sltx73XGjABZa4GY9AmmzCBfLOW2IImjH8sl0NV/wfYid0
+IKS+Xl5wAcc+d11kibM2jAHTCMBQgpxJZ0AvAjgeiKB3djF8860XZJqvn1u64IIHWxuRwZZ9ey14
+dQEj9iFiw6qwQgBBqn2CCiAgk2fU4VGOGKa4bi1k3X5qyy8R1+q9twiDlw5c4QX2mGHYDcOMANgY
+vjztXdZmbP+7tnwLkQEFHHjpJQc8my58ZW7zGAIGBnRHa55Mevfyd5zb7icFIghpAQWcjpmBB7h6
+yfXw4MNVwQE+2lyrdmIz2fJ3HVwg/Z8fWBDAB6hm2nsGInBPwZAUuPB9+ADkygQOsAE1VW5NH1rV
+6gxkKVbl7H0ksEACLEA/CWqMAnYTQQACUDcOkC6AIOzKCFQgMMSBCBkHKpEKTaSBFzRAA/a6C5/e
+9ycSfOCGNyTBn4BHPQpEiYMeDKEQtRITFagOPPYqkJNqxESQeSd6uKKe1rSGMhviUIc7TIAGf3iB
+IA7xize5BvkMyKQDzIiJNGqgdx6oqQt04I0btFoGLIAqEvT/SQQ2tMAFopQCPnkRjIA0SSUMQEAy
+EkgDaEzjd3CmLxG80Wp7jEAEOOg+TdkxAfELQJTwta1AehIjqkBGAZuHAAw4qQEG04CHFlnJzAVg
+khcIQAckGYE+MkAkC0iAFemXgShdwHufDOZ15oIMH1UIQyrw0AE0wAIcsaqVbXxlCjIwSVpG4Jaa
+WAAOK8gnEbjgg8IMZz6YsiBknEBcLkJGM5u5KhWwEW+vjEAsrXnNVzCABRqwYQQtIIL8dUqcAL3H
+V3CTwrEdE24GOlASDWSAd2pxlpOEKC0h44oVmIAFDEAVPwP1x4B69Bw8EQRH3rMKNs0qhuBJ6HNg
+cLc/7S8F/7XMQDwlmQJYlCAEBcjlN4n00Z6WQyWV6AtJqUJItzkMbgcg6gMXsIBlMrWpMIApLRcA
+i5AwIAGf86lWqRHSprTEM/05xJViqAIFKKAlYYCBBgtKq7Yy7EALWAFMVxCLq35zq3jlzwgYsxKy
+hHVBxMCABgjQJhWAw6xmbQi9FDifRR1AqitgAAMW0AGsgjOvmL2FV3/CWbTSpSlRKkEJBqtGxFBU
+sgqQHCkR91hrbrB/wcusbEvxlEr0xK+FCIYrGNCCFWiAAeChaAsesILSrnYzKpDqNP34v9k6FxKc
+jW4hIMMAupbgAS0ggAJY4J2yVnQFjF3tCShrNath8LnoLf+FY3hiiAmEhAXFrSpiEfsKDRj3uHfZ
+gAo02YJpVi+9ABaFbRqCVgykYAGZuO4DKAqLBaiAqisoAQGciF/vbAADK5gkTwPMYUgs5ymqPAEA
+EMCCB2hAwhUtAYMJwIAHLLi0bj1ujw7wt8B1+MahsCdFvbsA4rp4xQS4LngXllAEoRQ8BaRxc3HM
+ZEVo9wC02u0yVuBi7Br2FcMlbpsih8EENBVyGGLaiYYwpA1TocxLbrKapUAALKkOMstgMZVdjOIW
+t6AF0lrYAQJQvUo29XDgCcEJxuw+33lJi2lWQuhEl7s1O5oKLC7gkkBiTxc/GMjDpVaB9vwn/P0p
+AE0Vm9j/HieELoqud2DKKhRC5wHq+TPRj451DIxqnAJWiACRdcVw6VpVFjNwWC2lADT91AGmwmBl
+i4qBqQUlAs25b2+wLsIFXMABYb9xd4OStbbBRUJjKs6wJwCBAlRsz15XVZlPXUAAxIQ1L1HvAFvr
+nwfd+Ejz3u8JrHbjBjmYqWhvu8MfOuJdDlDdFASAxebucQsYgGJXYOAETBM0y+Ddp015rnfCDpSY
+9r1ve+/PCRnQ1sY57r6P/zvWARP4XQwUAgCM1twsdnGmXYEASdt8xjC4nejyV+1Ycnzfu+NAtBct
+7J/z6Z8nd3TKD2q4e7Ig5gsuAa9ZPFxyH9lHtj5AZW23/zMwFf3ncczfrpgwbWp/veO/jG3SmUxA
+puu5vjL/MQGyjN0UrABDkh70+6g3ug6AHehdSvT29NZqn3P8Wpdd+43HqL6huKLH2CX3rlsQ1/Dm
+t4Aq2HrGuMRsv/9dlr9M8+Cp7SeZGr2fim+yEQ2ZUsdrQOanTYYrKBwfrA9Ler/s/Oc3eC1hf+9v
+gSI93jyPdpOnvsOJcvt3XiZZqD+g1wy4L5JJVnFsZYD0Z/973fLVLyH1T29h+lOzjV43fx9/q+FS
+OYAOcN1kRH/FKSgAoJmU9WH/qW57G/7uMda//f1NY9TGPdZHfLIkAsZ3fuk1RsoXMlQ2dQSwACXA
+VCUgf/9HNn0boHWNJAK941Km93cY401Cpy0baDvjt2/bZ34I2FMKyCpytQIncE6vc2U2hV0BgAAM
+QHvygXWPtW6aEnTYojWf94Ha0mU0lADC5kZ08wSJtIRM2IRO+IRQGIVSOIVUWIVWeIVYmIVauIVc
+2IVe+IVgGIZi+IQriABylQJ3dmdRMnGyBwt4VgL4JH2BlmQDkG5MFQDhF0UZQIBhZ4TuEybVt3eu
+pkUWMIUpqILGdAJoWGWM2F9NxQQ4GB+Shk2boAmThS37Q2+gV0nCNj2BWIT+d4gBNkaKyIim6GIL
+0ASR+IIn4IIRVgIgUImyKBKYqDlCUnG3kgBQBIo2Jor/CXgCJZACp3iKKaCKLMhHoiVaCzaLs3hV
++kKEftJsnVaERlg9KOiLWgWBw3iKJWCMdxFaw4gJzNiMe6c1Pbh3zSYk2Mhhs1hi2/gAxQiJiiiM
+2yiOMMcMleiMjeRpHDg9GORqtriOHTaOmXAC29iNTaCN9dgK9xgL2RRF1fOJtlhmAslkBLkJBsmN
+TjBncVdlJ8CQDemQmeBSEakvvViRTXaRlZiRjBiPSxAAWRaM9OhimhCSvZZN1viJeHONKJlZ43iT
+mbACw9UCISBaTQCT/bWIVWaPsDBfTllVmcAAOjk96tiTajaLDRmVUXJL2qUATICGKaBgSwmSXemU
+ZllX/wbYUojGk1aJWZVok68AkwgWDGb1lfTYAhggjrqlDGbZl3EWEgUQA2hmZm1pkZsAl/XFR69Q
+l0sQloNVk03pl375CoFZmCd3mIj5ClHSAo7HmEoQAHG2l4spmZPJDJb5b5gZmaX5gHd2cF3JBBrw
+l/JFmpJpmqepbZAZmai0m7tpVo83XDWlDEwgm7NJm31pm7cZa2SpDLzZnL2pACcGj6O5BL7Za8ZJ
+mgSQnLKWm13pnN7ZAArwAsI4Wr7plUpQnap5ncepncopmmb1nd8pntgFA6jkmUhAnOqJndnJno4G
+ku8Jn94pnw8QAL1JnXyZn9jJn4/mngAKn/SYAi4Env/UiaDG6QoK2p+L2aDwCZM0WJ8TSqEJeqFr
+plv/qaHOaTYuBqESep4gSpoiuqDaZaLxKVpREpvD2aJ++aIwqgAy+p38RZ43iqNOqaOPxpw96pzy
+2QKgGaRCap9EqmYxeqTOuQDA6QRN6qRPCqU8KqW8CQN45pJMSqFZum1RyqUvxEdQAKJjenJleqTQ
+GZZgaqXGuaaKV6ImyphRYgVmSacpiFgNiqV8SqcuGqiEmgRwWqiIigSHmqiWGQEokzSQGqmS6ixw
+2gGTeqmYmqmauqmcegERwKiC4z7pMqqkOicOAJbvUqqquqqs2qquOqq6CKpeYwEmAAC2equ4mqu6
+iqv/cJoCu/qrwBqswjqsxLqrJmABsto1tFqswpqUvsqs0Bqt0hqsx5qsPrOs05qrJgCn2dqt3lqs
+1WqtHmMBDvCttvqGJWCu6rqutuoAyCquHUOu6vql6cqu9tqt7gqv8Vqu5iqTLXCvACut+aqv/iKv
+3kojMlmr5kojtsqwv+qwAWur4Uqw3mKwwgqxAOCwNGICdxatEKuxA9CwIfuwI5utGGus78oGEiAB
+ULCyLcuyoOKyMSCzV0CzWGCzKCGzOPuyQrCzRWABEECsGHuy0wqyt0q0u4q0zKq0tzqwY7CyUAu1
+MwuzTuCzSWC1UkCzPqu1VDu1UksEUQuzYSu1Y9uz/107s2aLBGFbBGtrtlFrBGV7BDiLtVObtldL
+t3ZLBVyLC29bBjortmdLt3+bt0QAtHJysSWbsSP7sTVytCF7sgw7tItbskwkspYrsozbuJcLrE7r
+t2drBnj7BHsLt4ALtlQ7uj1ruqrLtjZbuqQ7BKiLtnV7t0zwtV5btZ9rurnbBaFLCr1bs59ru7a7
+BINrBIarsMAquZdrtIqruY4LuZO7uY87vY67vNGruMLauWTQuqcrvCzrslwbvoA7vLpbvOD7tlv7
+vV07t64rt+sbvLMruzvLvoQLu7tbv+7bvfrbvfhrv/HbtrN7voTbt2srvvY7vH3rtb/rv//rveTL
+wP8K/L6D+7fc+7UUfMAHzL8BnLvHu6pMKCcbawIhPMIDQKo1ki4kDMIlLClMxMIrnMIi/MIrTKoA
+oL1P+752S8ECHL95q774a76uG7sZzLpEfLvuu7pg+78/HLjj+7q0S7x168NFXLxHPLpArL5SvLpW
+HMRcnMPt2wRA7MX9u8U9HMEbLMYBHMU8jMZUXLhx4gBwHMdyDMc0Isd17AB3nEh0PABzHMdMNMd5
+zMd4LMh+XCN2jEZ7nMiD3MdyXMMp67la3MUMTL+R7MRh3MD568QQDMAD3LZBvMRFbMZu+8BxS8Bq
+LL6SnMA7fMasTMWerMGXHMtfPMqWnMqz3MOSDMH/rZzKp7y/tnwEFvDGjHzIxLzIxlzMfVzHd6zI
+gQzIfLzMy1zIijzMjQwBj7y9OBzBXxy+uszKlbzL3UzLSLzE2fzD/nvLlKy7/RvOcMvGv6y2WSzL
+7ozG4LzLBHzLxOu986zJl6zLpczL9SzPxivM1HzMxtzMdDzNzjzNCA3NBy3IDv3QCt3HJmDNaMC9
+oNzNbezNk/zOSrDKa5y+4zy/55zR7LzRoXy1+9zPrPvKK/3OZBzQwYvPH63P9RzKLM3D8wvQAo3S
+PxsnEBDUQj3UQU0jQ23UEIDUSV0jQq3URK3USB3VkFLUTL3UTT3VTHTVVL3VRD3U6nLNN/zN8lvO
+//QsxTv90ie9zRosu508zqmLwWuMy7Ar1kcMxVPs0TgN03ot1zJdxhwNxjad03ztyjp9uitdy2g9
+0BXd1Yzd2I792JAd2ZI92ZL91RddzqqczQmswPRcvr6MwSLdyeTLyYVtypEMwKSN2qStzWrLz/e8
+zjHd05/ttg0c05zd2bjb0rA80/382uXr2Tns2hacy2682JR93Mid3Mp93LASAWB9DgtMsY0QzMa9
+3NZ93dhN2ZaNzWPb3d793eAd3uI93uRd3uZ93uid3uq93uzd3u793vAd3/I93+Bd3Nl93/id30G9
+3dLtKxaAAupCTwI+4ARe4AZ+4Aie4ApO4CaAAkTP3d+G8t8OsOAUXuEWfuEX7gAODuG+sjso8OEg
+HuIiPuIkXuImfuIonuIqvuIocIAc/inyxKkyPuM0XuM2LqmeOgZBAAAh+QQJFAAxACwAAAUARAKH
+AAAG/8CYcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvtepeC8HdMLpvP6LR6zW6733CjYCSO2+/4vH7P
+7/v/THN0QmGFhoeAiYqLjI2Oj5BFdTGCApQCBiwFm5ybLCwGhpGjpKWmp6ikk3OFmwSvsLEEnSx0
+lqm4ubq7vL1QhZS2Bq8MKgbHyAYHBwwECq+etr7T1NXW13thdHQFDAYgtoeHIyMgB9C1g9jr7O3u
+74Hi5OHi9dobKs8F4Lfw/v8AA5bSNq+gvYPjQEDjJ7Chw4cQ3xycgLCioRELR0TcyLGjRyxhJlC0
+SHICxlkg+n1cybJlR20jSVqsYBKaOpc4c+psx6rQSP+aMsWJLKegQK2dSJMqxdUzZIUKAiZADWpI
+5JxmBW4u3cq1Kx9W4Soco2iVasiRIYqyUOm1rdu3Z8SJRQACqtSpVIFeJcBAK9y/gANP0XYWgWED
+IhHjLbkYwyy/giNLnnzJllTDCAzQnGAgs96KnMvOKZqSsunTfw+Jrfv0qUjDIKSKjOnzLgjEhTAa
+ZYu6t2+dVSuwnrsZM92xrVsjW71YBYGjv6NLZ9nz7lzDIisY384d9uGYw7JOH08eoiGxFQ4YV9y9
+PXe94TWWn0//nc8KGLo/BeEeMwgVyxyAAX8Y4FUUb/UlqOAuhdCEwQbcSZVff3Rp8MKFLwiIHUWz
+KBP/4IIghmhKgxM8yJ1mnfWnwgsDtOjiAS/QJdIsKmxg4wkwZKDjjiL26GMftZm4HWv9adAiDCkk
+GYCLDRhWgQYq8IfACheIkIAHHnDAQQIZ/OjllxKdZQCEhmGwjAoYTNBfiwGY4OabMJAwAGxR0tWB
+lQnkmacHLnAJ5p+AlsEKWRCag+GFUnZ3wABtvuloAAtsd8IFFOhpKZ8edBnoppyCNIJVE2xgZgMt
+ytmieu3B4KgDjqYQZaJ3WnqpCxR0auutTgQ4FlkrHAnBmwHIOSF3MPxqwgUWWJCBAyWo4KwKhq2Q
+gayWcuDCBbhmq20RB4SwQQgnHJBYiy8A4KibJAy7/90KbUaQrAUwHFCncR1USm2efWq67b631uif
+uBi8QEIJq7oJwwqwgTAgZki6mwGUCvOX6AX35qkltvxm3OkJIRhnZgghqPqmB8mi4Ko5aGIAILQI
+wLCABhrSRRcIIJywAMX3cuABxhr3DGiUAfJ3AoQwPOBmAu/CuwC0HSuscn4YDEjzzJgtkMEF01K7
+5QVY++z1lwKynDKEKgRgrgjvZhDvCQD+t4zKxkm5MAId4IyzvQlQQLELfXZdxI76fi24dCp0jACa
+hoPg8gopwJAjDOGao57Tc0c9NwInxCor4CJUmiXfIgROqbUeUBD44KibJi/NNZLptNMrI/C0eyrL
+W/+n5hVTy2e+QkyrM5Zbnp768JG9/e1tKcqsMIWYmZmotLlbqncGylLAp58XbFmtCMR3LxlnQycs
+vuxR92fmvJhTGn2eFJCQAQUfkPDBlbRmnTfXImApvPf8d4VJ+P5ZmJkEpK4Axo5eeFqfCEhgpQ/E
+D2fXYp8IOtAB3+2vfxhMyggM8CDD0UVq8jrTs551AAs1QAMFVN/6ErBAB7rQAnrygJVEEIAk1csF
+F8ygDl3Cim6RaTsso4sKNEAqF70oUZizn6z0xjX8kUB+L4zh1pKUgjvhcIdY3AkrOPgt5q3IiC9a
+F86WeAEKdiAAddPRE59orwvIzwIioKIVc5jFOkb/pCcHAGB/zAHGU21nAQm0VBkpmIEORCACKQhA
+BgLZPhRc4AMZoKKVOEBHO1pSIE3J4w9pZ6QGNOAFGoDZHwOppwwEIAJoPOMhEamBTTAgAC10IAlq
+mIILeMBPl8yleeihjKF5sD0AKqEGPoEqzKyAlHkyJSo7kMhVRqATBVgAFB+IRgpcS5fYdEhTCiEv
+jv1ySPJiwQHEWcCb3UuZKYikMyMQCwYUQGDxU1aV9JfNev6jJ3SYCk3OhAGQdcdty0gUQJWopzIe
+UpXObMYrWvAABmjgiR+w3sXsSVH7fOosUdlMGDwUtam5J5iaEcD7qCWCCxxSkSlw5gJgwYAHnKAA
+//XiQJVKWtGasiMkYQDKbGIilWMESEBIpMsBGiSAA9gPUgso4QKWeoAFpHSVCoVFNPnkN5tatRpz
+wMtUZlMV2oREA0jMhwLIEgYY0PBtNEsrzaKmggUwLgIrlSoD+MS9q9rVFxiNyki4WpuNHgMDGiCA
+ulTAAAUY1rAUiVdQu1O7lK5gAQxgwAI6YK263vWyuZANUc+yGFAlqQQlCGwQMaPQyCpgdczzjwqe
+ish0WquSmI3tIwbF06iIowAsbcEKNMAA7ii0BS1AU2q3Y6anVtCWmZKtclGR06rUoxkMWAEBSvCA
+FjiDBcbJxytWINzhEneyg8yATJdL3lN8xqtRmf8FC7gri8MeNhbF9C5mNnCAlDognaYrr35J0RrZ
+jAQDKVjAJqjbUFkQYGkrXUEJCBAz+c5XBYw7Lmz3S2FFPCWUJwAAAljwAA0seLsliCpfHtDQ+Kp1
+uBA6ANd0VOEWl4KlCtXuAh6wAhKLeLoP6K4Qm3qmxW7HRkadsIuH3AdnHIBmxHhGjUkc3KgylMbq
+MgcFKAAp1LYHZAfoHeAAV4Wr6Wl6RA5zFgiAwi424xl8WfIDPtxS4K5gWOaApZcTkNQjdycEZorB
++3bHNxfojGdQWCTffhc8MRuaCnyxUaHaSeK23hi4kwOQpRappyqnjK3h0rMI+vw7LLlgZ1IQb77/
+dJS//B761Ev4KQJsZDgCrOC3NDYwX1aGARjg7X7UQuri8nyBQU9vRxS4mJCFsOmdnRGNtjQ1qpc9
+BHl5y0YQJuwJQKCAELNU1u38aVMnS9A9YSkBHVDxtPxc0mOjcZFaGnYGrneBALg7AFwDNbOZPbRv
+eZCwjAsAX2Q94xYw4MOvKJyi7W0mZCbAWny7pS3z9e6GV6nQTdg0rdrt8EoNe97L1eQmDQOgEAAg
+tNhuaXVzHHB7g2zg4crd51yQPw7oreENf981mbBuP7+84XWTN8bF3DrGMuMTI25oCaRLDIZau3Dc
+efYGUh49Stqc4jBH9i0rKfM+3Tzmedu5mH34/9FzvEIDTLYxAZ5c3QD3R9Eq6ED0KFDsCUb93e+j
+pBIEzbf7Rb1euNS6izvoHsnBYsbVtTZDV9CCBYSLQjZCwAGiZ6U+uf3tUlf23yT+6WSa292xurje
+r6qejXPHTAoFe3VL65xXNLg9il587my5pau/PWtV7d3eEl7Qy0td85u36QESTyF5RTboDzBwAeJ7
+ZSCrvWIZ8HMyIe9uStlLUzoK9qBvKUiY1y3vuddvzylkJuo6pxgiTsHwR1t8fMCgYnurPfOnNeV8
+WczP1Ccp1JGN/eyT90Ge5445akz0VyygBEtVAsNXQKgnKudHLZTSJ19WSJDnfBLHevGXOyLgdv8V
+xHa4Z3/2VG+YcQInsAIceALG4XuyQF0tEAAIwACn5x43sgABICsUoz2CNH9Yx3bWwkIrpCehE331
+pwR91IM++INAGIRCOIREWIRGeIRImIRKuIRM2IRO+IRQGIVSOIVUCIQvUG8d+FktAFpJgjCKJy8G
+toUlwAIw42PcwWoacIBShHxlBHNVkicUc0sGt0J6YypDiIE1NTQlkAIERmJ+uGYpkGlMYGcU8myZ
+1oIxlDumUyVmxDUS9Ig3+GWhg4cUdgJ7+IeYyGQpkGWDSIAcyDhUxIcqMAR4d4M5SGql1Ea3tj6/
+RokU9n+ZGIslwImpNixZCFrABVxG0wIBJgT/3SBZkSh/BbWK92KBF+iK9QRNRhGLfziLTpAfJ8CH
+fYiJALABmqCM3RCMeTNnyaSIJcUjyEhe2MgJIseMvOgEq5UCZJeJGjaOncAAdIg1t2Y6UzaBE4g1
+4BiO4uiOnbCOzfgE9cWMfniN/MgJC1Ax04NM+HiM+mhTBQlN/khiJfAEgOeH/uhOD9kJByk9WINM
+YNaQROaOstAJIZCJE+kEKcBke7iOL8UJsjaO8GiMBmc6DAmSNYWN2AYNm/BZfniSTZCSSeKPLYlb
+OamTnaABlFYxk2iTQ6aMRRkLBZCOKsACDPACT6CO01hdGOCSTylVnaAjVTJlYpmDTBmSndCV/1C5
+ACWoAgVgWFdJYFFzlmgJlZ2gZVxWlmbpCnMpC1QUVwrgBAEAgK3ElXtJl5uAl8xGmO3lXu8VC+pY
+gq/glkyQAoY1C9h2WJ6UmY1pmIiJanIZC5iZmZqJZgdGXSngDJK5BJV5mQogmq7pSavplZ15aoqJ
+mq35mqKJZhqQkiCXmkpAmgZ2m7j5mrF5mLNpaHoJC4Y1nMT5DClZeJHZBLEJmsw5nMBZAMeJnEQZ
+mcJZnaOZkg+gb84gncDJnd6Jm6SJndkZZtvpDOc5nOCZAi/wDH+pmqC5nO+JntCwnuypnPmJm/H5
+Ag3gm0ewmqH5n8T5CvzZn+6JoK4JAwxVgv8DWp9J4F4OypxotqBm2aAX6kkeRmKNM6Cq2Z0d6prP
+oKFhFpklmpngWQIBIKK/uaLDSQAomqIcWqKBSUUawAQkKqMDSqM1SmQ32qEvkCTVtQDS6aOjGaRi
+NqQXWqQgmqRKSqBM2mKvoKQwYJoBwKM96qAUWqU26qNFyocpQJ4lSqVgSmT4eaF/+Vllaqb/eVhp
+ymwH+p6pmSRRsKYYiqZzemgWuqdfagV1OqF82qfzxpiMaaiKmgRuuqiOmgSBuYePOqlGQKZbSqmI
+GQFokzSc2qmdegHqmEieOqqkWqqmeqqoSqomhalfQzHn8qqwuiqh2AKxWqu2equ4mqu6agL/CQBo
+rJoxFmACADCsxFqsxnqsw/oAodoCyNqszvqs0Bqt0tqsJmABv+ozwTqtz8qFJaCt3vqt4Bqt1Xqt
+PZOt4VqsW5gk57qu7Kqt40quwOoA7QoAvKiu83qv+DqsDmCt8MovFiCv7WoCVJSvBNuu+9qv/gqw
+7QpcwlqwDguuB4uw2/Kv7doiBWuxAICxyKqxD2su/Cqx2UKx0cqxGMux3qqxJTsAw2qyxcqyJ6uy
+z/qubCABEgAFNGuzNcspNxsDO3sFPYsFP+sSOxu0OCsERFsEFgAB0kqyMMuuKUusLnusUTutU6uv
+H/sFNJu1WcuzOesER5sEXysFPXu0Y9u1/1y7tUSgtTmrtlvLtkZrtjz7tkigtkVAt2+rtUbgtkcQ
+tGHLtXILtn37t1RQtrqAt2UwtGsLt32LuIJLBEnrJtDKtCvbtEYEtSprshYruRlLuS4yuZ47uShb
+uZ/rrBFrBoHbBafrtV1Ltombtqv7umnrurJbtz/bunkLu41ru3MLt3tbtry7u0qAtof7u7mQuj7L
+u8IrvEvAuEbwuA3rrJr7tJsLupjbuS0Ls9KbsdMbup8rvVULAKU7vLN7trB7s777uoirvK6LtoxL
+t6zrt0PAt7p7u+Mbv/Abt/cbv4o7v/obvMS7vvqLvrgbtuYrwANcs8x7t4nbtgIcwLWbvP+G+wQJ
+HMF2S78KbLaGW8AKvL7MC8EN/LcJ7LjGcqs+6CYtYsIDgMIqDKsu8qonbAIv/MJvYkSOEsMpDMM3
+bMO2Cr5XSwYPDMILrMH5e78IzL/w28G6y7ryS7t3C7z9W7dE3LhSnL4W7MTB67dFbMEhzMSEe8QL
+fLZD7MVPjMRAXMZh7L9mTMZVrMZx6762q8ZZnMVMLMZnbAERYAIOkMd6vMd53CJ77McOAMh91McD
+wMd6bER8LMiFHMiLfMgu8sdgRMiSzMiGvMc8jAY/TMeZvMSzS7RwnMS/q75DfL77y8FDC8JSPMoG
+7MCebLcZjMVEnMFvDMGxnMazzMqC+8n/tmzGHKzFt0zHc6zJpTzGv3zK5hvL7PvLSHvHlZzIjUzJ
+0AzIkNzMfizN0bzI1kzJ0pzN19zMhgwAENDDY7DJrozB6PvExOzLu5zK5BvMqay8mRy7cmvEnAzA
+Z4zOYLvObEy/XazLwpzLxTzLsszO/tvPyjy++5y/egvH+hzQR2DHeOzNkzzJinzIE+3ME13R0BzI
+3bzR2PzRF23IJhDOmGzO72zSnTzMrdzQTCDEUxzKKP3O/UvPwzzHi/u/9vvPOn275czSOm3QPp3S
+UUDBDr3GB63QyFvMPr3FzXvHEPDUUB3VT90iUU3VEGDVV+0iUI3VUo3VVv3VA7DVWp3V/2I91WNN
+1mjN1VL9K+5S0kJttLmr0qsLzOm80++L1Ogc03gNxQHMzl2Mv7zcuy2tzjsdzP582KC81HNN101A
+1Ots2Mosx7msz4Ttz8s80mud2Zq92Zzd2Z792aD92SbQ1mcQz45NzA+c0BeMy01810itvhUc16+c
+0hUc2+RrwEkdwXyN0AN9z0D906Dswch8wA5c10Xby4G9wcDc28XN2qcM2cmc3EIA0aFd3dZ93dh9
+3axC2v5gvCDbCNSd3eI93uRt3aMtzlzAtuq93uzd3u793vAd3/I93/Rd3/Z93/id3/q93/zd3/79
+3wD+3o7r1OVd4AZ+4GyN3t8dKBaAAlqjvU4QHuESPuEUXuEWfuEYLuEmgAIKvuB/0uAOkOEiPuIk
+XuIl7gAc7uG38j4o0OIu/uIwHuMyPuM0XuM2fuM4nuMoIHkqHigRgCypGuRCPuREXuSfGgFjEAQA
+IfkECQoAMQAsAAAFAEQChwAABv/AmHBILBqPyKRyyWw6n9CodEqtWq/YrHbL7XqXgrDgSy6bz+i0
+es1uu9/weFE8ltvv+Lx+z+/7/0wCIyN1dIZ0gImKi4yNjo+QR3UxgoRiBiwFmpsFDCwGg2KRo6Sl
+pqeopKKUlgKaBLCxsgSbLCCWqbm6u7y9vlGGhAYTsCoqGAbJygYHKgQKsJosuL/V1tfY2XuVg5i3
+rYdigyAH0SwbhNrq6+zt7k3B3OHz4iMHCpq3k+/8/f7/quSFCUWv4EAM0fQBXMiwoUM49CpMMEhR
+UMJ0DzNq3MjRSpgJEytWHMGA1q2OKFOqTCkopEiDIEdEw7iyps2b7ATRkSgA5Ev/QxMqWCwwDafR
+o0hz6WxVgafPn2KEjnBWgGbSq1iz9hkY1QAIoUGhRh1Iq6jWs2jTrjlUwQACBEIFNBX71B4tq2rz
+6t07hWvPCSHeDutpgOdLwwJA4AOxj6/jx5CHGKoAAgEyiSARFHZZMOjToSwaRx5NWuvkCV+bNs2M
+AMTmz2JANr0srlzo0rhzJ6XjWTUGuKzfuh6mWvWEZE3h0kFYVbfz5yp3qnb7Fuzb69izX8fwe3AY
+A3ehix/vMHbbttiJa1/P3jD45uTjy3cXewKGDdlTs89vzFhlzXGNgI8B8xVo4DVR2Ydfdsntdx0I
+GIAA4QEHVIZBSAMyQ+GBHHZo/0qC92kXnIPaYVBhaxLhowICIYSwAgwZxCijhzTWuNVHCmIn4TAk
+7vcfigTYElgKF4iQgAcecMBBAhnY6OSTEOFowIIg9HfAbz32yAyWHRiZwJdfeuACk1CWaaYZOk00
+AX4YqPDfj1lmp8ILDWjwW2UndEABmHyK6UGTZwYqKBY49oTfinHup0IDAzQ6wAvXdclnny5QMOil
+mDrRzDEUqolfYIlqB8ILjr5A4QFvrZDBpHxy4MIFmcYqaxEqbNBiCCoccNyCocrJ6KNXZqcnq2CO
+CeisyGIa4nUHUAYqiRJGqCOpwEqb3QXEfqkkrMl2OygGIUho4QG8+tgMd7ki+v/Cus1U5m6EJyyA
+LbEceMCtt/iaqcIJm/4WAq9Vuoldmyy2hu5vx4ir3QIZXLAqq0te4HC+FD95JZbGgIsAtY42gCoC
+++ZaJYUEP1iZAVzOO++eX1KArQtjTlyEjMdWbPNziIJ8woINvKDBAeuS2oCJ5KAKIXf/cYflW3l6
+ySfNIuyZJMwi1HwBBa56QEHNN3dN2sW1BhatcG3OefB+3FGIqKTZEiumsUKsWi+SS3Lt9d2OUbhz
+a8mQPXaW5LgZoapt8+lyBhZkQIGYZF6wZKsi4C35YxMc8K9wOpK9NHu5Ln3C1YW3TILiH5DwwZGV
+PpyAy0UiaffksGMliOXlcvf/lokXrwdhrh9H6nThIpBg5Aelz/tqyyJ00IHcr8fuvFGCGLC3yW81
+o3Z/VmpA5ws5vwV66AkET/z4FoDpgZEXBJBCCnq60Pzz8K8UPbnP6j6yo/g/CucJqk/KusRFIoHp
+yGe+iK2PfSJwX/wWiBOdXMlWPZpT/hrVu1TNy38XUF4HAtABh2VAgAJk2QVMZwERHLB972OgCh9C
+kANMz0HkmOAAKoiABfwOTBnsYAY6EIEIpCAAGfgdBUiAggt8IAMHxBYHUrjCJv5DJ99xYbnYgwEN
+lIpd2bEhsTIQgAhwcIM99KEGNMGAAIiPeCRQXwoyUDcnuvEhUIzifeq3ngNo/+COGmAAhbKzght+
+iYte7MAPwxgBThRgAQMsHgcp8Ko3OnIhcYwNyU5ARx01gwUHwOTmQHbBp3VxjSkgZARkwYACvMB0
+JEhc6/70yFb2oyWE8AlIQDLJSlrmVCciG4X6h0MeRgCMhCwJLFrwAAZoQIAfWNy2XMlMd4xgIrFU
+0yyjeDE4CUdC1TvVRBTHKhFcoIdADGUYFxALBjzgBAXQEweK5M1mulMdHxlLT+TCm+NoiGTWzJU0
+BXAA1QVgAbjE5QLEGQFhxuKQYpLZOxdqjXgWip5xCUNE6WDH7JQDGqsRAAySh09R8W6gESDnQRkg
+psgx9KS/+Is86cmZiBpgBf8rSIEGCNC9Nj1DAThtCwxyCcNcraCHC2AAAxbQAVeZFKVI1cVcopkg
+zoTkkCsoQQmKoR1hLoCcGhBYnMhB0PWxUYFJDatSJLoT2IRBFip4QAuEWlVYlKAFWg1VlcS5vAu4
+Tqx4RYVKYxOOAgTpBASI6gpgwQClYYAB0CDAuXp1nRAcoIPLY+O98kpZUiAmHCXphFRnAQs8yiKr
+jL3OBg7QRQcAcWuVTa1lJbKaibw0BSvoxANma9ByCpUBxFSsNUO1gXixr2FMVK1wF9GUYpiABeZU
+wGBLUoIHOKOcs93jg5S22/XY6gASi9Fwt1uKZ/xMmMJcwQOaW0xSNncFSxv/2VUX66ANjFa73I1v
+JGDxsZIgNrCzVWsJBguL/KJXRzY0UgdItp8WHWAINKOZFYLIsqPK98FXIIAGdhaYkiQWv82dKgEY
+sN8WlCBnbVqdxL4UgI5mJwQmioHiXAUzmNVrslCwqwuU5CoyQfjGU9iwe/EDAlLOdgVCnUULWmC0
+AywgADh02j+v158UB5HF9QqTC+wlhQwwDoAiOF9wccxlXO4MggggQAnC+wD+claox+Cly1gVgDYr
+D70HvoCLDycjrK1zy0OQs7002EEtc/nPR3DhrVC8gJmu4AQK0PCZa0vfgFKISW5DUgIGPOIZe3OD
+bn7yEp9g5Zi12c0iWCeg/0cdg2b8y1a3OmwIfsjZWIi3BeI1KCV37F6QwaCTYGLxjMNXYy5++tNF
+amMT5Fyp9P0aW6wkNY6lOMXWXAkAD5hpkElJzNk+91+DpjW/WEYvqiXJZb8+NiNhjIROcwDcv+5S
+spX94GbUWlQUCrI5i6nWchJzzBqo7qk3YKLQ/WnG6A43B+1q4yQEEWZaM/avV+VgdnO3Vs2+Tq5i
+oQFiVrsk+VVrCrp34h2rAMlto0ACz4dpgZ923UdoGItFLPCrNdzhwkUVmHVHoVgcYLYfdqtzgVwy
+9pyaX8BLwJiSZ3I32xW1KXccwv9YclC/HOapJVfEsZOuWFTbzAblKXt2fP8AbrPKrkvyZtHb/DCF
+5lnoS/9SBsPdQaRDXbgQh1a8+TvbFnx2wzRcz6lV0IG2ZWDXTBp7AK7GsmM17G1RBlPAje72t6f2
+PlOn+gHOS9hZpKAAzejRqTEAg2y9rJdjfxgjmSQ1F3u+6R0suOMrS+EemUi8z4WFeFewgBZgfpN6
+d2+8WOUyrBluh0VflchhBnYPgC+HZKdA41ePV9qB6gTQj74lD8DoV6cAucEi0alrCHIwZflxfBK7
+wFMv8hr7sW0eDKLqlyDD9rv//fCPv/znT//62//++M+//vfP//77//8AGIACOID19wLYFlMpIFUK
+CFsn4GwUglMQqABqVGj/WudzuvcCndcqxrdFa3dsXqJE4QM+xOIyJEB/zNdMUpeA1ZZxGrcC5NAE
+eZd7/CYEbSaCrJM8yjNiq+M05xc6VYNnJ9hKhoQBAbCCLJhfsHVgTFCBCHACCHhA66OEMfA94POD
+MeI03JQAVAg+IgdfQZhUhsQJJ3CELNgCJQaDm3MCKsiCAGACJbAAhnRbIjgpOqiFXpctXeiFX4hS
+YRiGLGCER7gCUqgE2RdTb0WGAAAAmdCHZLQANhgj3MZLhuNNM7KHYMiIfTiGZDheT7AiCbiJD5CI
+i4iJZFQ4h+N1VaN8IrCKlKiHlniJYdhqm6CJR5gCT3AAUQWKJqCIpBiG/92neA7jRwr2isLVh612
+UJpAixlni06wAORVd0c4ir3ICd23NVlIh0BIjCdlSMc4C5sAiGb4BCmAhCkAiKPYjbQQhpS4hcCY
+jdq4UNyIjrKgCYaYX+HoBOPYAp/Igosoj/O4CQxwh+Hnju/4TpzQahGIU944UCXAAg5ZAgEgjvoI
+iA/Qj/74j5pwa1GjfKroigVZWZvAWRDYACTZABA4C0UYAKWkABrwBGqUXyFwAud4kd5IRiqWYB8Z
+XyEZCzhVkj5JkgpZTgd0UQogjuNITgcpkgl5YTVZADnJZa/Akwrwk1QZlLCgggEAC0UpjhoADX6F
+kD3pkxHIWZrwlDj2lf9aSZVqCZSxUHsPkAI8+QQvgI5huZYmaZXpaJY3hpZTaZdqmVhuCZc39QRM
+OQt16ZcneVB6CWFf2Zd+WZXQoAEZNlM45QTQUJha6ZiPCZQX5pSLKV+NuZl/yZLjWAIz9QyWiZk3
+JZo/2ZmfCZqryZpUqQH5uACXmZqGeZiyGZSvCZuaKZsl+QLj+AABcJeWmZkjCZytCQu9KV/PoJxU
+OZwpQCdbyQQRCJ1rCQ3N6Zy/CZ1FOF7FWZ1LoJvYKZYEsJ3xlZbl2QAwQF4pUJlN0J3raZLniZ7p
+KZ+yKZz6+EPiqQT4WZ71aZ/pOZ8NoD7N9UOWSaDmKaAP9p+i+QIB4J7/qamgxsmgDTqfEjhkCBqf
+Diqa/Wmh8UWeskmbB0SYHWqX8AmiOCaiiEmDCciMEwqcEKiio5aciCme6yMFNoqiKUqjyraUCdkF
+QAqkPvp2KXBVLVmkSsoEEVoCMLqkUHoEUpWjUVqlRPCiT2qlORkBImABXvqlYBqmYgqFiCOmZnqm
+aJqmarqmbPql36SlFYMtJjCndFqndnqnPqSPEXCnfNqnfvqngBqoglqnWginFFM+c5gtRfhDidqo
+jvqoc4gCFmCo+VI+onSpmEpICeikmdqpnvqpoBqqohpGHjCplOotljqqmHpAqtqqrvqqnlqqp4qq
+CQCrYcSqtpqruhqq/7I6q8lSPhAQrMI6rMRarMHaQ8aarMq6rMzarM7KrAlgqr46K8D6rNZ6rdia
+rdparNE6rchSrdsaruI6rtnard4qK+C6rY1CrsS6rhDgrsYKr+xqrObaBhIgAVBwr/mKr4KirzHg
+r1cAsFggsCvhrwS7r0JwsEVQPg7QsA77sA7bKA8rsQ5AsRB7sRhrsRS7sQOAsQ1rsR4bsh/bsSJb
+r19wryiLsv/Kr06gsEngslIAsAorsyy7sipLBCnLrzmrsjubsDX7rz6LBDlbBEPrsylrBD17BAQL
+sysbtC/LtE5LBTS7C0drBgarsz/LtFcbtUTAsCI7shNLsl/7tRwbsf9iK7IgO7YXm7YXa7JlALVd
+ALcty7Izi7U4S7d4i7N3u7dEK7B2i7R5y7V/K7Q/q7Q0W7iEqwQ3a7WIqwtyG7CFu7iLuwRbawRe
+S7ZnW7Yjq7Edm7YSC7KaW7GOAraia7acO7qkG7JuSwZ+i7dTq6+H67pYO7l3e7NbO7R127RDsLSD
+C7h8q7cyy7XCW7l9q7iNW7u7G7vJK7x7G7uBC7u9W7W4K7vJO7lVa7OPu7y6i73V67LOW7t2e7Wt
+a7uzW73dq73E27UJIKiNUqftawLvG78DMKf5Y6f4Y7/zC7/5+77wq7/+S7/7G8AAPMB/uronW7OV
+K77QO7x0q7sHm8D/fzu1gMu7xWuziau9wNvA24vBTku7G+y7lNu0+PrAEXy8C9zBJTzCJ8y3EgzB
+KPzCH0y5JQzDuUvDDTy9NtzBIhzDLhzDMVA+iRjEQizE7ZeIjWLEA4DESjzESJzEQ3zEAADFUBzE
++EPETizFV5zFTDzEBuwFrZvD6Ju1Ysy8PUy8dRu5Y2y0FNy9BqvDPJzG2Cu7JCy9trvD3+vAs5u3
+LTzDeMzGUVvGfNzD4AvCgMy82yvIghvIEWzHcgzGC5sAW8zEUxzFWkzJVUzJkXzJVFzJk2zJTtzE
+jrLEWBzJQdzFcYvAeZy+h8vBfVzBhWzI18vAE/y7PLy8vSvLfXvL/xWsuGCMyMW7x72syGEczHRs
+yMYLzK28y748vuRLw8HsyOpLylY8zZhczdQsyUk8yaNszUo8xZ1czd9MyqbMBV/8xrSczDDMwsIs
+wxu8xrvczpFry2QMx+psvEywzr7Mxq+8z4PLz/NszE+LzOmLwcuMyoS8yK38ykkAxNK8xKJcyVHs
+0E8M0dvszeBM0Q8t0Vs8zltQzsELz658w7rsz7AcvYELtBxcznprvj4swSidzoZ7zwcNzb9L0iSd
+0OuM0gN9z5Kb0zXNx0Cr0vg80zhtyAzd0NpM0aGs0Zvc1Be9ydt80d7MyZ8cyRytBULd0+rMzDDt
+x2rsui190nGcy7/uHMc6G9JFO9bgK8doHMsr/dPN7MOHjM9AndY8S73pfL07zdNkPcx9nczFPMhe
+3cbKHNh7fdQNndiKvdiM3diOzcXS+g7Ze66MgNiPfdmYndmanYhXbQU7+9mgHdqiPdqkXdqmfdqo
+ndqqvdqs3dqu/dqwHduyPdu0XdukHc2bndu6vdsbHdmUPSiWzdvCPdyO3dm/XSPBTdzKvdy9fdyY
+ojjMHd3SLcRV49yXEgEX0Kbavd3c3d3ebaZv+gVBAAAh+QQJCgAxACwAAAQARAKIAAAG/8CYcEgs
+Go/IpHLJbDqf0Kh0Sq1ar9isdsvtepsCwXdMLpvP6LR6zW6733BjOByv2+/4vH7P7/udAiNif4SF
+hoeIiYqLVINCYYKPYQaUlXNzjJmam5ydnp10j4JzBaWmpwUMLCCjjp+vsLGys7RQoYGkBAQKvL28
+ugSmq6O1xcbHyMl8lyMjBgzQlM3T05QqwMEFLMTK3d7f4OFPkM0C262X6XMjIAwKwSAboeL09fb3
+oNOB1Or96iMhdKUKMQKfwYMIE8bxh4uhw3XXgoWYp7CixYsYbRGZM2HCw4+BQOhiECmjyZMoTc6p
+ALLlBJEEtrlKSbOmzW4cw3TU2bJfhf8RGBQUADHzptGjSDnhGjWhgsedPS95HHEgWMmkWLNq7dNw
+ZdMwLKPmDBSMRdGtaNOqNQNppQEEBjw6FQsWEgOrZ9fq3cvXVroJbxGAYFlhbtSvYTAEQ5C3r+PH
+j9sKaIqgMgLCIBC3NBxI6DbIoEM7TlfhrYHCTiuAgMuZYdPT60ZeFU27NtK/qlE77bhacNOOHqU2
+rXDg8qVrBWbbXs5cZcPUpVfHfWlZMIjpwAFjwFAYbnABBoQSbU6+fEWpqAP7pl69vXvBgsOSHdrY
+vP37yS4VPt3eae/3AFoGWxhl1YffgQjCwlFhG4TQn2oBVoeBCgccoEJvUFVlVoIcdhj/C0cdbeAe
+ZRFapsILKL6ggQqVxSXAXWZVeICHNNaoiFcTiNgefyVq8MIAQAbpo3QT3MUAAiGEsAIMGTTppI1Q
+RmkHjg1ath2PAaoAJAkpdJkCCUFicBkBDGDg4AoXiJCABx5wwEECGUgp55xprCRABVWCoIIK25WI
+QAMDwGDCoIR+OYCYIFhYWQBqJuCoox644EGcdFZq6Ra4ENYgBgcECeQLYr6HQaCElmpCBDC41wEF
+j7aagAscUHrprLQ+UWElmmoAKAkddAnmoQDCAEGpDhCKKgj/LXCBq61KKmut0EZLxAFmhnDCAYTB
+AGQKpkZAwgsArhDAoAlYYG4ELey5/+dqJ2TAKrNruiCCtPTSe0CVgh3Q0Y8vAGCqCQGw+B4Iwppg
+rgUZaEBteys0Ci8Hk9Yrca0q6FgZp2a+UEKpbEaQwgLWTYgBsggsCQHCMByAbG+9rQpvvPNOLLOl
+B4Sg7moqJAlDC4M6cMHBKcCQaJ8TqoyACikrCp91J6D5cpsXzCw1nQdci+yFGDSoQgD+enAwwgvU
+vLLIxYGwHckkV4bmBRnAy+YFbE8td5QjV7bnCZbBUEILERycQdjXUsuphQJXRnKfJbubQKPvOpqm
+C5LGbYSTTc5tOXMLI1CzjolaKCMMIFObuXVWbld3ZR04/CjbTV7wLgeQw0mEu5FCHP/z5bjTVmHW
+Iazcnp6rmf2fe9t5LqayLycPuwdRx+CupGxCnfv0oJV2QoPXhXqd734Kxqdl7TaePAUiNGkBBZFG
+nMGbrbL5LPXwqyVAcRYTT6HRAxcdKuqqJ/+BBQH4AAlIwCqIZUAEHnAc3CjggvfF74FZcQbvrGQA
+nO1uZRgs2gFW1J52Jc9VJLBAuQQowgRwgAKsIl8HOuAuDtwOgjBMiiAq5iDL9EZM27lfilAEqAGA
+qj2p+2CrSPCBIhaRBI/igOtEQIEuBeACLoyhFI+Ci6rhK0Ag0ICnPKWB4SHAdfAiH9zgJgILENGI
+SEzi4gLgRCi+cIpwPElDNucnDGj/cYsDKE572gavC6ywAwEIAAszYMYBNkoEA7TABbyUuijG8ZEm
+KQd4aOgnFfTQU3oEHx9dJYI/ZmCREYjAE5c1RDWFkJEMbB4kV6mQrsyPkhFKVANSpAGFdZCUrrpA
+AESpyw6E0mMdYEAqFpCAMwpwkSm4gPtYyUyEuPKVIbiiqGSkARZQ6JZ93GUKMiDKX0ZAmKZYgBFJ
+KEg3NvOc93BlcIjDpxPU8D3qOoA1h6enTbZKl6HEpzfvMhIWaOCM5oLbMtFJ0HAEIjhQ2Y0BLIQB
+d8LzZjakkAHs+SgRXCCUgvTmN4GxAhOwgAEDJAEC3fTGgpr0GGPZSXY4UiE+ca89/4ObjgBgID5H
+USADKfBYBnb5yxRgowQhKMACRKBEEVj0pEjNzwjkExbgCGcSC2VoeyiEUPBssgNhk5FWF5DTUC4A
+G8FgwKtu6sCkmlVBI4CKWu8kFZ9oYKq7IMBvZiqCsLnUPXqqEFcjsAJspGKsZw0sLSbD1JzIZzJz
+MMAKVpACDRCgcAiY0C56wZIVeXFgFFpBKFcAjQXAIFIlFaxoN1EYENnpsMER6gpKUAJdQLYy/FzA
+V0fXPT111WOCfJUqR8tbpdTFTt+ZAzZU8IAWQMM9/CzBA77Xval2NQCfXFNZe0tdRWgGseooQExO
+QIDV9pVMpsOAO0Zyoea+5wAdGP9jBjwQ2uq6FxGFUak67pIK1oJVF7V0LDAY8FrzImADW4uAA6Ar
+u/caOBO66YhiU7CCVDzgwfzEBjSg0YIHEAB//rXMBpomyk9O98AgRkRVHutRBjxAAX29i3IjMpIH
+70lCprvsexp0ALhVLsQ4ZsQuFMZPfq7gAcp9QITJpNwV7M9sB5Dt7kq0gQ0c4MY5jnIidKFHI73j
+xw9uQQm+S4AHP8DIEjqAURMQgAsCKEkzEgLlKGeF1hl1t1KOMxUIoIHrOegu79DFj5XbWiKvoAUt
+yCSFEkABtjkqAHa9bAg45TwKwA5ykEsgnJ+wLFi5yQUUkLOmp0CmJosIJi3+MjT/wFphlx4ABvfE
+ZQLsqq49MfqAj4YYpCInhfVJyqKuY96HN71prVLoevgiQAl8/OX7joQB1IIBIB9VaHgFcoVGntEF
+IHdC1jXJ0UrctRGmXe0VpikBk+Z1r63oaQAvwLErOIEC+nzfUYNVYfFMMnRruiY3LQ4GcHuVC/0Y
+SEHCOlZPWK+z+i1I9Glb3CGmVjTrF1kGhCAFATC2nov74x7jrdwbppCqHxUpSLMXdnAieL/T9KaD
+C2HamNYlwUlpcoS7l9zvtBK1APAAx7p7vxV2sS4wjnFO0RtSjx5poUU+ckeHe3K1GzrBUxcxl8u5
+YgyX+QFGbWIhFxfnQGZAnWNe/5mFbwADHRDipGDVSaI/W5kFTsIBI630lYPb6XHeHNerUyFgaKDC
+Ob+Ll4ubgv5axutb+yD5bg1Is8+76Uj45KMJrXKCu6u9cH9v1qIe5hFf+MEliMiKOQvmAC38Wh9U
+kwsWV3izp1fXiYdipB21U5GnDvKRry6wS9RSYOScyxGm7YybfICfO65Ni2u82flYvskxkNqrK/3Z
+bxr7hEdz7jCtEAO+m2Vs2DyTnm+yCsIeRm6z3vCBdB2rEP/JjkuqVW1ffsubb9Z7Ud49FSrySMCa
+ggIoLUILxwCq+/iq5IO/baIHJ0zUcezDLBmgfKnDfOxnYFDXPRbyYyzWXV+2AP8tYH9+1x6fR0yu
+UmiOhn6tN3yL8youoEzQI3gH2G/uooAL6F4NeAJN4yUMVh18MnVg9WMtkAIfpXvvsXAhcAABwEls
+wiwWZXrLQj4g1z8f1DqKs3541IRO+IRQGIVSOIVUWIVWeIVYmIVauIVc2IVe+IVgGIZiOIZkGCQN
+4AsKACobUAJdsjd7x4YrYDf3g4YKwEYfozD7k30NogEw8INJlEDw8knK90SNsixvgoRChEKFZgFV
+uIKshAqnwAApsHeUCGQpcDROgGEA4k7RdC0xgGiyFYovU2hp8kf5tjiHhIhCVD7r54iQBImwyAKV
+uHc3qAKZmIeV8YIwyIYLYIv/sFgKDOCHrcKK12ZTigMnvhdGrOiKgfWLsMgAOTeLJZBmTQBZJ9CG
+lAgAABBUzhhOQuQ4DkNRyWNRUMaMSYUKEpcNpRCNlBhoTyAw15gC7OhlAAAC3YgKGvgy5aM64shs
+YlSO5niOppCOwGAKVTeLKWArCJACbjiL9XiPkJiPHghGzJaCRvVm1haQo3UKBOlXpSCLlXiDtsJY
+s+hl3Khd6QiJwhh8x1iRT6KRBjaQHemRBXAClciGULAAtNiOMtmRqEBo5UORrtKKMCmQKDmTNFmJ
+CQkFQXaDk7h34ISUBVkKGnBAP0dWRQlipXBfdOgLHslY0biUT8Bn8kiJGLCV/8aWZ8ZmCqNYfFmp
+lUc5WV3pldjAVQHAAhiAk08QAGz4lHt3AmgJDHOpllOZgIpYaAD5lu4Vl4NJh/SXAtDAC1HQJdHY
+AizQk3LZmB5ZlWummDl2lI3ZlXXZJV8lmVDAl+mCjoIZmr3gV565aSjJmqKJXwwJmbsQBYjGC4G5
+mrL5CwLxmpoWm72JhiMxiSWgXwqQk4QJVsNJlwUAnHKmXc1JnLpgnCOWnE/gm1w5na35nNApZdLJ
+nV45iS1QmtjpBMuJDeL5C975nZ+ZmetJgQ/gU7eZnekJn9OpC+4ZZbqwnr4gn/R5nk2Qnv7Zmvsp
+ZfjJnQD6AqaJnrxZoL9woP/8maDNuQDK1QIw0KADSqHrSQASiqAQqgFBlgIaygQQKpofCqL+qQFP
+2VgCaqIn6pUpqqL+KY9e8qJLEKMyOqNRBqFcRZ5iOaA6WqI8GmIQykZZFgD2qaNFqmkFyqKWGKQ5
+eqJNymsF6iV6iZ4FWqUIx51D4CVR4J9cGnmyuQXTOabNR5oakAaNiaYraKNS6qZyqgTymKVzeqdI
+UJtdgqd8egR62qcHGgFl9DWEWqhf0ys32AGGuqiM2qiO+qiQ6qgXBahzsyz/cqmX6gBwiqmc2qme
++qmgGqql8naUOjUl9I0vw5ddgqqs2qqu+qrMggIWUKqmmgAadau46k1ekqv/vNqrvvqrwBqs3uQ1
+tCo1IiSsuNolyLqszNqsv0qsxSozx+qs1Fqt1tqs0BqtEiNCENCt3vqt4Bqu4jqu5Fqu5nqu6Jqu
+EFAu2jox3Kqu8Bqv8jqv9Mqu7Vov70qv+rqv/Cqv9nqv0pKv9Aok/QquBAsBByuuCVuw4vqvayAB
+EgAFECuxEXspExsDF3sFGYsFG5sSF9uxFCsEIFsEIuQAJnuyKHuyQIKyK+sALZuyMBuzL9uyNDsA
+MWuyL3uzOouzNruzDusFEBu0QYuxFesEI5sERysFGTuyS1u0RDu0RCC0FSu1Q0u1Iuu0GHu1SCC1
+RcC1Vyu0RmC1R9CxSUu0/1qLtGV7tlTQtLEAtmXwsVOLtWULt2pLBCW7szzLsj2Lt3hbsyq7tzub
+s3wLs4ILsz87BmnbBYlrtEXLtHEbtY0buVELuZTbtRv7uGEruXWLuVuLtWPbtJ7buUoAtW8burCw
+uBrruaRLuktAt0Zwt30LuH7LszNrs4K7sjk7uy4bJHm7u39bu7zbuzp7uF9wuZHLthMLuscbt6wL
+uVBLt1zruGY7BGTLuZlbuZO7tHW7va5ruaNrus5LvcorvttLucqruclrvW4bvcsrvqzrtk+LuuQ7
+vfHrvkd7vs77uHBrvM/LvO5rv/PbvXabAKAKJKViwCaAwAo8AIOyRabiKf8PzMAJLMEInMATfMEN
+TMEanMEc3KnEC7RO67r7m77c27jTC7IijLlsm7nV671PK7rzm70mTL8xfLbNS8PX27pmG7EorMLg
+S8I27MM8DMSVu8IpHMRIjMOt68NJLL1NbMLs+8Q2vMNKfMRKHAMipI1avMVb7ITaCCRfPABhPMZc
+HMZizMVgDABpnMZa7CldfMZrDMdyXMZc/MGKG8JMTMMCjMN7fMJ5vMKWq7pyu7yDnL8fO8VVXMhf
+e76CvL7PS8X468eLTL5G/Mec67WVLMWSvMn5m8NW3Mf0a8U1/MkqDMmErMkkmwB0XMZsrMZz7Mpu
+7MqrHMtt/MqtDMtnbMb/QULGcbzKWmzHXGC8iyzAoFvDnBzAlmzMk4y95fu+eBy2lNzMihy+V6zM
+0LzJpIy2oZzM2JzH3fzEjly+S5zJxxzDory5mPzHmpzNr6vKvlzLb8zLuQzPdAzGrdzLshzPbHzL
++czPvgzMWyDMiczM5dzH7Jy4RMzHoSvQzRzNA03QSQzDo7vO3hzI4fzNGN29B229Ec245AzKHz3N
+Dy3CFN3RR5DF70zG8qzSr8zKLY3P+9zPL73S+fzPs3oGAq29eqzI2mvQ3AzIRfzMaivU6HzNXyvO
+gCzUcwu+1OvJ3JzDGf3T3rzRQ23STAC/7OzCGJ21DP3TTl3S7ZzSKi3T4LW8z/MczzRt1vgs02ZN
+0zaNBl29ukrtzOdsv6d81E68083rtaOMyS5MtXdt13bdyDcsslCdzuK8zWCt0VhNxI091HKd2N9r
+0cjMwlPdv5Qt2Cd82BcNyigt1qAd2qI92qRN2gCNDPILsIjw2aXd2q792rD9yzeNuIBd27Z927id
+27q927zd277928Ad3MI93MRd3MZ93Mid3Mrt2wMc28793NC9yqet2lLC2tF93dhd2tNN3VBi3dn9
+3eAt3bPN3bPiLuF93ui9xW5J3rMSAT8TqfAd3/I93/RdqJP6BUEAACH5BAkKADEALAAABQBEAocA
+AAb/wJhwSCwaj8ikcslsOp/QqHRKrVqv2Kx2y+16mYKw4Esum8/otHrNbrvf8HhRMArL7/i8fs/v
++/+ATWF1Q2KGh4GJiouMjY6PkEdjQoNjYQYFmZoFLJ0GI3V2kaOkpaanqKSidGKYBK+wsQSaLJ+i
+qbi5uru8vVCGdQYGryoqIMfIIBgqBwQKr5ksob7U1dbX2H2DoAYMGKChh4cjBgfPBQygk9ns7e7v
+8Eti4HTq4vfAIyrnIITx/wADCjQ1D5/Bg3RAQOu3bqDDhxAjtlklZsIEhBgHLfQnsaPHjyCr0LGY
+seQIaBxDqlzJ8iOri4YqlLxnMcQsaQ1b6tzJM1u9/1ATKsCUOdNQ0BEgFBQAkbOn06dQS7GKSZJk
+UTEyRzCYlTKq169g+bAKVwEEAplBr2IdpJRp2Ldw47IRVwEDAgQGLlYgWtSqVgI45QoeTNiKuAkG
+7uJFa0BoUcdhMHBtWriy5cuHamJovDcoAhCQEQYNfbIAAsqXU6t+e6jsgb2wBVRQ3NgiTKOjQeSd
+dxP16t/AeRqFXSExXs+Kdecl3pmzAQx8RzSTFry69Z1The5VfHw29+/g8ZoNrYLr9fPoPb4s/pw7
+aO/h4yfnO6zAiPT48wd8OQHDBvDbycfdMsWYtZgYSt2n34IMXlOJUP6Bh5yAnzFzgAYHGHNcGEoZ
+4P9bgyCGGAlVEX6XF4V3aTDAiis2cIBZuymV4QIZXJAABQkkcEEGIvboYyJh2AbCf8kpRyEGDQxA
+AgxMvjBAki8cN4sKAVyAY44JeOCCBzz+6OWXdwRp0ZB3HZChCnYJCMKKKZjgppsBtHhXORh0cCWW
+OXLggghg9unnGmL2958KTrbYQJrhOdnmm29C8IIGimFgJZ546nnBn5hmyoWZAuRlEQYnvJAkkzCQ
+sOIB8Z0AA6OsBnCCYgvYSCmWW3ap6a24QoFqCAhgcIBFBqz4AqspOBkfCKu6KcIFIqBgggNngprB
+rFhqyWeu2GaLxAEnEImACnlhsGIJrJoQQZTxwQD/gQkoWOBuBsxggMwKIlCbJ5fa5qsvqt+9FsIB
+MADwpgMRROAADAYqI+9dK6jrgQUZaICmgXctUC+1HnBwqb4cY4sqgXYt81/AbqKQgbsowLBMhhis
+bFfDAcBwQJp2KRPrxZRmvGPHPN+a4cwIvDgkr8lC4O67MmMQAq8KowpCMS1/d8IFzMqKpwcUiIB1
+BrYWwXXPYIMIdK/F8IrAAgE84MDRFsAwcYYgmMkMxcf0mtjUd97IbNYZaO2CCxxQ0HUMfXuA9c5h
+J54et2R7y4yFGjC5AoFAK4woMrBajacInOetpQsJ2Dot4BxYqvjp55l5QAj/1V1kgd9qGF8xcp8w
+/6291OKoJb4xXMDB1RwMjvrwqU1wwAa8CmNg1Auj+NnMxzCLO5YUWEDC9STUW+vtN3YeOPHgpybA
+BCog795nd4Gspq8Gxjo9lhmQAEEEH1yfZ+ge6L1sBvkLH/7/rJEO67gjjDLBjWLJoZzsEDCp94Xu
+AxCEIAlkpTEcXSAAKUjBtHgHwA6yZgPP8Rb6erUy1ZnQTC8QFb8Yxr1ZUeCFL7wACSIIQe5hLWsZ
+TEG9OOjBHkaFFeUzm3zixqIiPkkF3HEftXbEtSZaL4IkuJII+iYCCuTQRjz0oRZ5og5uiTA+GFCR
+EQfwAgR2AGd4ukAH1tiBDAagAyfj2sUoQIL4Wf+gjRnkXxa3yEeVTEUAq/tieFSQJCOWkTt2cmEG
+UvBGqhUsAim4AAwWgLYHJsB6JHCj7yjQx06y5I/lGKCaDsCiBqRQA2bMW44EV7AOvDEAj0wBAzRB
+oxlKEIP1Cp4nd+mScFzCi0IcpAaGiaG5JRGNq8xAwd74yIKtIBbosCUJINa33/mPl9gEyFiMUrtg
+uidDLNBAJ5B4TEWmAJKLbGYKYMGAFrCAAdizAI5Ml816DqQgVQmK8YDmzeepjpzpU4EGNAc/WEbA
+lc2MQCzIxQAG1CgBF9uYPScaj5GsZXxp+SXLmncXzFXITELJgCrfiMeYnfORC4jFOwsQgIw1kaL/
+MIVHkMQUBqHcBqOdKgfcvgMCDM30ALJagNyKQVQLLeCkz4TmAlwa06a2Y6Y1HYpVaiqOCyGwPM8Y
+jQBgIIKdHosZR43ACrbyCgYEgAN7dKpad2ERyGQUozeFiQFWsIIUaIAAACWbMxTA1+KMDUXMWEFC
+NYjWa671sKhwTB1CY5uYCKAAC1hBCUpADPCQlZISQyCKnibYZa4xf4gNbS9oStpDxEIFD2hBQy37
+ihJMznnhOUAAZttED0hUtLhNhWxuM1UxFAAwJyCAZJPqjZZhgAHPIECGYBufA6wxAlwLXW6niwub
+4tQQW0HHZGXxCmLG4kXMBc8GDlACB2jwa9RN/29iiYOYuq4AHQ+IL1lj0dCGtuABDMhreBHAOgzQ
+qEaGVa+AI9EMvJrgnQ9QwDO3UoIHlIed8f1r3FSHKPmwTlVUC/CAN9wIZ2CIrGRdwQMajF/6NniB
+NktAAMzE0fBs4AQH0DCHZ6yIV/BrK8gVbnxT61pY7BhN3FFB6EQaAKECOTy8OoAQasS5JiNuCkxu
+soxpTGUoEEAD3eLVVpKr4wZTlgAMcG0LSuA0XwUgR1PEUgdY5jplhEAFS/7c3+ZsWyiL4G+lK92T
+q8znKYB5A4D+DH3jO9b5vuK+aBJoB9KITKGaEE1K5h+esVbFjNU5ChcAHAWoZiUPTLHPoGbCmf+I
+2i3zIYAAJQjxA5LK3YYeQIlY4hzuRNCBFSg5BhT4W+g4vSMK6BkKIt2SGtfIrEuH+thEyFCpAb0B
+FSzgris4gQK+3GpDw+KEZoIjxvDcSN+BLgOunO0bq5lWJGRATzoSd5UoUG5kU9mLpr5LCI4bAkZy
+FxYiboGI56uCE7CO2b3SADJzlDE6ZzpwGVC3uJllzSaMbk/hXvjW3B3qQMY7UjMDwAPu2lBZtPPH
+r/AP8pbGbOSpQJXAc0EGtkRrhYtbpLpkQqYB13J1d3rKFKeuyMFopo4zQL6pZed9Uy2xi9+l5EKe
+3gtBp0aXz/aMnmaCpFke8adrDec5x235jM7/HTPBQgP3vW+Jd5zaFIA3PgA/wPRsxPSqK/x2AZ76
+75bl8mndNuscLrWAfFVg5ca3BA8+8VhXoF/xAppKuDv375ru9Dde/ZpTB53eXG6lu+NdwIHsZ78y
+BAuxs3q+ZqLQv0+wANxlOn81d3oHbFTFu9dIzngCt8LPeK3Lb3jrKOI7A5Ia3xbEguPLpRCg/2Uv
+7a3ygo1fPUQN16WHzvl3m0O+uGlvew7jnkJPI++qyyqLFBSAxShidtIpxSzAYYnxLlc+jlyQtSz9
+LX/2kv3Cs1b9De/8LifIP3jipgIRP/gVIrYCC9AC37dCAiJ+ZxZ7IoBW0Zd8NrJ+HHBn8Kd0//Kn
+bVhXf9i0CZrgDchzAjn0gSsQZL5ibfmWAu/kK87zb4iXIyTFSLNCNapXL1WkJy30PpvWRC+EdWO0
+gzzYgz74g0AYhEI4hERYhEZ4hEiYhEq4hEzYhE74hFAYhVKoFBq4CRiQQWK3Y2OWAq8SNBbCV2Co
+ABiUAs/GDCl4eKU3S1WIDqV3IzVSdcrXPQkQPA5kL4JjKkKIgX20hmvIAmT3hw+QQd/yBAYYHvl3
+AiXwZmrIh5pwfFHWa2h2JTVYhzeoh2pVhdy1CT8HiDvmWrfWBIWIf3WVQ2OWaoyogQxAPZyGM2nm
+hnWoihdoiXy0CfcGTZqQhYC4YoSIQB6YAv+4+AAAwAIamImaOGsidXyUyESyuFaaUIuysAmcGF9m
+R4hp0ou/GF/CmAnOCA0bqEgNBFEDRz37E4vLuIfauI22CF+c2AIBAAVI5IHXGF8h0IzoyI2QBYuq
+FGVOxkTkWI6edI71CAu0wIklEAXcQmKcOI8AGZCNeIMD92n+OGAL+QphCIbEWADXmAJSkDad+If0
+GJACWQAa0DdL1I8RSVELWZEq+Yyc8IsaGQUk1gIZRHbZ+FuwoJJh+IypqEgmeZITZZMUiZMrKZBH
+hZDsGAUBMHS+SHYn8JFCWZHQtJObg14+KWBA+ZQ4SV8pUAIscAItIJNR4IslkAJ/SIBAuVf/WGmR
+0MAATTaOVclhNpmWWQkLC1ACAVAeLyAFpCiPGJCNsSCXUAkNb9lncQmYgQlmObRlUxAAk3UAtHiT
+hqmWszCYfFaYkclXseCLLZBSCkAFC8BXE4mWlzmZlFllv3WZh5k2m0mRG8ll3IWaFlkApclnogmb
+rzCAMrlXU+CafwmbmEkAs1lltWmbAxiIyNWZUuCMvvmbwUllwwmbdRmIrCkFvBmUywmczUlj1rmc
+MHBf6+QMuykLy5mT2emc48lXGuBld4WcUMBl55mT2FmeHLadsKkBZLmZfEWd9Hme8Smf83meL0CW
+gZifUfCeK+mfVfaeAloCn0mdBlqRCJqg/+OZlDmUlw76oOwZoTQ2nvbpZVSAoRmqoRvKnQL6kgVq
+oCJ6bMuZlIHYjhe6nCmKbL5pnzn0ob4Zo1kXmUOwlSb6ooCJo3g3WxqAlUVQo1bwo0AapJPVo0na
+pE2wlWPpok46pUvAo1JKpVhqBB+YpVyqpTLJpF1amhEgAmxTpmbKNh0wWQFwpmzapm76pnAap3F6
+AREQpoljI+WSp+ViXjLZAnr6p4AaqII6qITKKjpip2FjAa9ILWOYgIv6qJAaqa/YLogKNoqaUJia
+qQWTQ5raqZ76qaAaqqKKqQ9TqT1zqaOaqqq6qqzaqqVqqh2Dqq06q7Raq6v6qrCqL4oKAf+82qu+
++qvAGqzCOqzEWqzGeqzICgGXlKscs6vJ+qzQGq3SOq3Lyqz54qzTmq3auq3RWq3Wmi3YOq0rwq2/
+Oq4QYK7Biq7kGqzeygYSIAFQ8K7xCq+aIq8xYK9XgK9YoK8tYa/8Oq9C8K9FoKgOULAGe7AGuyIH
+q7AOwLAI+7AQ67AMO7EDALEF67AWm7EXW7Ea265e8K4gC7L3Sq9OILBJYLJSgK8Cq7IkO7IiSwQh
+S68xK7IzG7Ate682iwQxWwQ7a7MhawQ1ewT8irIjm7MnS7RGSwUsqws/awb+KrM3S7RPm7REQLAa
+u7ELy7FXe7UUm7Baq7EYu7UPG7YP67H/ZIC0XYC2JUuyKwu1MMu2cAuzbzu3PKuvbgu0cUu1d6uz
+Nyu0LNu3fKsEL+u0gJsLapuvfTu4g7sEU2sEVsu1X9u1GyuxFRu2CouxktuwLIK1muu1lLu5nJux
+ZvsFdgu3Syuvf2u6ULu4b/uyU7uzbVu0QzC0e4u3dCu3Kku1utu4dSu4hdu6s5u6wau7c5u6eYu6
+tdu0sKu6wbu4Teuyhzu8sgu9zWuyxtu6bvu0peu6q9u81Su9vFu1CTCoK8Io5WsC55u+A+AmRsQq
+ReS+64u+8Xu+6Cu/9su+85u/+Lu/gDq6H9uyjau9yLu7bCu7/xrAd7u0eEu7veuygSu9/7hbwNML
+wUbLuhNsu4xbtPB6wAn8uwNcwR28wR9MtwqMwCB8whfMuB2MwrHLwgW8vC5cwRqcwiacwjGgqACQ
+wzq8wzrMgzm8Ij88AEE8xDwcxELMw0AMAEmcxD3MIju8xEcMxURcxDrsv2kLwCs8weF7wVtswFms
+wHWbuFGrumOMvf4qwzRcxj5rvGKsvK47w9frxWs8vCX8xXvbs3Ucw3K8x9iLwTXcxdNbwxT8xwkM
+x2SsxwObAFRcxEysxFH8yEU0xUjsxE/8yJLsyI08RkQsxYucw1bMBaW7xuH7txTMx+Brx6U8x7dL
+vM6LxUBLx6ysxsBrw6n8yntMyEcbyP+ofMtZzMsu7MbEq8J5bMoQLMh6i8dfrMe47LiK3MlNXMmb
+fMTQTMVAnMmW7MiTjM2XrM2N7MwA8MlbEMppvMrE3MXLjLYjzMWAK86sDMvjTM4o/MCCq8y9HMbA
+7Mv4zLvnXLvxvLbDDMj/LMvvHMD03M9HgMPerM3RPMXXzMgNzclMLMQQLc3XzMmdDM5aIM65q8Vq
+nLvmvMtgTMKunLQjfcy27LPBDMYjLbW/O7t+vMsYnM8g3cv7TNIGzQTPu8wNjM84y84g/dIFzcwJ
+rdDcTNGUTNTTXNQMTckRDclGvdDOjNFZ4NOKu9KtbMzVe8go3cIczbo9O8h43MAzq9WzWZ3VbWzB
+ARvTyBzMuhzU+pzTIwzXJF3VbO279nzKC0zT3HvXZW3Aan3PgIzQQz3YhF3Yhn3Y3izV1RC938oI
+go3YkB3Zkh3Ziq20Y33ZmJ3Zmr3ZnN3Znv3ZoB3aoj3apF3apn3aqJ3aqr3arA3a4jvZsB3bsr3I
+ld3YPvLYs53bun3YtW3bIoLbux3cwk3bFuDbuSJSw53cyr3DEGncmhIBFyCn0j3d1F3d1n2mdEoG
+QQAAIfkECQoAMQAsAAAFAEQChwAABv/AmHBILBqPyKRyyWw6n9CodEqtWq/YrHbL7XqZgrDgSy6b
+z+i0es1uu9/weFEwCsvv+Lx+z+/7/4BIY0NhdUJiiImDgYyNjo+QkZKTc4t0iAYFmpuaLCwgI3V2
+lKSlpqeoqaWjl2ETBLCxsrCbn6KquLm6u7y9Uoh1BRMMDCoYIMjJyCoqBAq0DCy3vtTV1tfYfIWh
+AtLcisChIAe0LBuG2enq6+ztYKHwdN/g9PLNBSwI6O78/f7/puqJqkeQDgha+hYBXMiwoUM3CsVM
+mFCwIh2E+x5q3MixIxUxFSyKHEEro8eTKFNulJhookhFE0kSYGBSpc2bOLFtc1WBooD/ni8TVRgB
+QkEBEApzKl3K9JQ8RBUMUHQZFCQdcgVqNt3KtaseMQMrgEDgEmjVn2FCGEXqta3bt2wKgTSAAAGI
+kBXMvqQ6ggEBaUnhCh5MOArMCXXrTpxgQK9FvRgIZA1cuLLlyy3Fgmicd6IBEBNCVgytd8RaypdT
+q24rdMKBvFGlMq7bePHhvAakgpXMAvXq38BxQoUdNXFPunU3SyXemfNn0WH8SgtOvfrNS6FJJ657
+fLv37wg+I9CLdYT18+hX1gmdG4N3z+Djb78rxoBk8+nz6+93qSeGDd815p58iWHAjApjhQedZL7t
+5+CDusjlH4DeNZYggSCocMABGhyA/2B4PhllQIMQlmjiJIhM9N93GFRAYGIaDCCjjC8cMJZuRh0I
+QwYUUJBAAiJkcOKQRDLiykQgUGhXMogRiEEDA5AAw44yQvkCiEZhkMIFIvz4owcucHBBkWSWiceR
+EyRZl4bMDCgfBjLCYMKccwZQJXcaqNBBl156yYELCZgp6KBroLmiCi/M2EADbn6XaAp0RgrBCxok
+h8ECF/TZJ5gUEOrpp1xsKIBsE2BwwgtQwhAADCTIeEB8KsgZaaQBnJDYCRlo2uefQoLq669RqIBA
+CAhgcIBnNM6aQqLxgSCrCSJc4AEEc3poDAJ76uoloL0C6+23RhxwgpIaVgDnACXMav9CBFfGB0ME
+JqBgwbwZrHDAMcggkKu2Cfw5JrgAA3wAsdu9tsEBMABAJwQRROAADAmCgMExda0AAwQeWJABDB5e
+iMAJfOrqAQciBGzyt+IaeO2wG5zQgcLQzmuBBzAY6OHEGo5l8aodF2vXpVxqO/IF3Z5stKf33puh
+xBsQm/C6MmvM8QkES2ysXW02WlcHXGaqqQdAekBBBkUTQXbZR6edn7jJ3Uuwqg+UkIHMMDAzDoIb
+2p1Yvpt9fIGPQP6YgQgUBCntnySXnYG0YBOt9uNr2zoxudZyOOUKKt/788SWcm7XCn/rWrgIIScw
+sgseOB5DrmFy8OfYkMdu3YYDA5j/73x6T+zxfDZ/GDq/opsepuMXcKCpmLInH5xrTYcnXrHuUfzi
+msIigynwfVpAwvYkZOovj16SToEHHqCt/PmDTaBC83vvrfKLxgpbF67Yh08CCg988AEJPrruNZDR
+yhUHzIe+AnrlKuyziwEGtCHdKSMZE9uQx7JVPyCRQH8YtMCXEpApEQQgBVtKgAsIaMASMuUq45pP
+XSaWuQO84IUwRFUDXrUd8FWQAhfE4Ac0+KXGgTAFhBuhCYfYFToYYH0Ek884oDSjGb2gUfTTFgWI
+dra55XB/XhOBxgb3wyCSkIhgRAk3xKUkJR6giU70GOikeIEAdKADH3Tj3DT2PxJo/9ECIvhhpoQY
+xj7apBUCqN30VMDEJr7AYwsonZcy8EauXSAFDYtABzKwgEpacF77+2AKMkC+L/rxkwwBpAEE6aQY
+LYpSlEKkIgUXgAi40Y2RjAADNsEAHGKweynYkwv+BcpeqgcTZEwieDjEIQ2wYEPeWaOuMtDKLcWy
+YbIoAAMyoD8SbJFxnvSlNtkBSETcjGrN8tAxjym/xChTU21sWDpjKYsWsIAB2rNml8BUsm3aEyB0
+8AlVeuKae4HTO8aiXTmLpaF9aUoEHWgYLGPpF1iwwAQsKADROPijet7zou7Ip1UEMBHojDJrHkvG
+CjV0rAnAYJUdgMEPYZDQSKZAFv/EkKYIVYfRmnKTozw5EnQ4SpHcNHB3INCAPhkDvgAsIG8HYsYB
+FgDJSDaUFgUQoUVtSlWd4DQMZsELVHwCEg14rBnPII0AVHCBn4JHYhtiagRSsICnLoCeVY3rNbKz
+UbRwFS2jWsEKUqABAgzUQM5QgGCjornpobWpsZTWLuXKWF9UIAbY4WpI7jqRAixgBSUoASwGWpeG
+VjJPuxvkAiI5yQ70K5uNTS0lclpXRMhCBQ9oATG+09ASrICz00vMAQLAW67lCnaqDS4ueJoiRRTg
+LycgAGZXAAsGsBADDHgGASSYW/CE4ABvTEEAcoVa4XoXEma5qwD8Is3MzgIWGkj/ryxsVF3wHKwF
+DojA4nj53fqigjiM2esKpPmA/j41FsQgRgseUIz2uhdog+uufRf8CHL4FaIMeIACmOuXEjygGQDu
+b2Gx1qbQficELdsRTRlMYlM4o0MNbegK4uZfmFr4Q5babQeOqrXvtEwDCi6xjv8ACxr6JbrK7W9s
+bRsLIa9shYn8m1F7Zt3rCoFspIsy2axQxSnv+MpbIIAGxkUsv0g3yBbW7Exs24IS0NBYC6ioDY96
+5OSEwFir65cL5jxn4+WYCHuscwLujOU+G2EmGwi0XWDa3xUQYxYDRpCGTBu+0gVAoGw6wQHiTGcO
+gO10wIVCBv7kAfGBjc9+1jFS/42hgnGxjwAlUPEDmHvemTBgqTCoZCX3tMoE9LYDkhaCCObcaSou
+ToR7jgIFwhQt3/oI1KG2781AHOhAn2ABfV3BCRQg5vMeehavLvUJtq0hGAjNdR5YAAyIBqbUtZG3
+cqSApRXMSdQxs7f7QnayhXuvZnvnzQwIgXZbDYsVt2DFTw0Bs5vdtEuJzHV6Lp6d0Y3uSao700vY
+tQumyHCudXreGI/B+hI4n3sB4AF9vTaAB9xfDINY4Cgn+HWBB6YRog6hDG94rsrXhHaTDI4MD528
+My7X2glzOwE9dIQJHNuRx40BGsDAz+sycBWkGXu75iDOY+5GEZCMCYPjNcwr3v+lnfOcqh4StHw2
+FAsNDJjkfhFybFNAw/gwe+XA6xKgLjB1qsdbCYujM5Dq7sZM0ffrJa5dGVlE9h73twQYfrGh7eXh
+xDAbA4zWFieNR3eqo5tLNEdC1nmdAIrH/G9/BzyDN7507+QtFiRn9UxigcwXBRrukg+T1C3fW2kl
+2Ag8qvQiqU5r0e/4P4MHjzEOwABW97cFsgi5h6bXbBXwq0eo6/y5Lc81INl5CIP7k569VHmuT9X3
+jOWE+AsA/OlF0MKs/i8BUlCABk7v8QHQVbRkLzi+N9zv9OTS+OoMNk15/vKFA36pNX7jxwIEcwLS
+tm3eMQ4YsGIY1m+rtgAt0H7/7PUizPZsXmJUAUYM4TN9MVd9IlBuwwZuUDd9kwQkAihX4zcLnMAA
+IJRZmQVCK1Aw46B+/pYC73Q17+dsabYABKgJDBB/jGR5QeMjc2ZQFRQtdAc+CoZGTviEUBiFUjiF
+VFiFVniFWJiFWriFXNiFXviFYBiGYjiGZFiGXWgUmsBvUIUPand8JZACtoIAHrIhglWHCqBJ0KYh
+ufV4bfWDnLAAY8N3DgckRoiENyQ+HNQqVpiCfbQJahgLm3ACbXh8KTCDk+YEFfgd27ZXP3QAfjh+
+gLg4jRQkfEI4pmOIFaRmXseI6OOIjwiJmiCJk/gAlXiJTZCJ87NXmUVyH+eK/2q4CWkWJGTzO2MD
+OKhYP8LIilWVhq8YTZpAcpN4VFDQKC84i774iMAoRTY0RanYecmojFTFjM3ojEM3iRoCBfJzAilg
+YZMIUeI4jpqgK0QDOABUP1N0NuAYV8cVC3bYj1+2hrLYhisQBTayV7PYXxH1jvDIAOEzOJpiQ17S
+IwFkZfmogvzoj/7ojAUwiylAkCqQAgcZW9c4jpLBkIZDj6pYZRRZkQN4kRjZj84YkMcXAFKwAGr3
+hmqXkLPwkoLFgoODkj8CcSxpX/vIkxgZTSAEjSUwBSAZN+sIjSfwjkZZh9HEL0EylCV2XFN5lLGw
+AG9Ich0pBWHGjmrni1tJlf+0IDojhpVEGVhnaYeyAEKz1AJhGQUpQJdtGJX7CAtviZYFEGWks5Js
+uWB82ZdoCQtemQLRBQNMCULHp5d76ZZ9SQuDGWqFaZg9iZhl9lIKUAUg6YPXiJlUWQCV6WeXiZld
+OWCc2ZlMuQDPEJmnaZiSUZp9FpuyqWUW9lLOQAUB8I+yIJpoSZu1CZxUqQG5GVhU8AK+6ZKiSQDC
+OZzEKVgaAJKrSQXLaZuy+ZxYhp19+QIgWQJ9xZpScJjc+ZbOqZ3bGZ0KYJyxtQC7OQWZWZ6TiZ59
+pp7s2QLuKZ5SwJzReZ70eWX2aWF0KVjWKZnqSaD/iWXRyZ60qAH6CQUH6o//CepnxDmdh8eY1hmh
+dTihyQacd7mOdQmfGsqh8yaa05mbVjCiJIpxmGmhdFkFEbqiPIeZAbB2KUqcMgp4RrWVQqBJ2nWj
+mJmjgAeiCxADGFkEIIoFfSmkvvehNMkEIPSkSmqUTAp+HxqiVZqlT/BDP6qlXuoE61gCUvqlZJoE
+H1qmaIoESZqmExoBdxQ1cBqnFtABINQBcnqneJqnerqnfNqnFxABbAo5maIuhDordFkC1FKoirqo
+jNqojvqo6sJBgfo4PNSNlnqpmJqpmqor8jKpaqNBzxSqojqqpFqqpnqqqJqqppoxnpo2oKqqsBqr
+sjqrtMqqrWo0r0qrurqr/7wqq7Z6qyajQRAwrMRarMZ6rMiarMq6rMzarM76rBCQABYArCcjrNB6
+rdiardq6rdJKrcGaANsaruI6rtw6rd4KMNYarjJCrsa6rhDgrsgKr+yKrN3qBhIgAVBwr/mKr6Ci
+rzHgr1cAsFggsCrhrwS7r0JwsEWgQQ7QsA77sA4rIw8rsQ5AsRB7sRhrsRS7sQOAsQ1rsR4bsh/b
+sSJbr2RwryiLsv/Kr06gsEngslIAsAorsyy7sipLBCnLrzmrsjubsDX7rz6LBDlbBEPrsylrBD17
+BAQLsysbtC/LtE5LBTSrC0drBgarsz/LtFcbtUTAsCI7shNLsl/7tRwbsf9iK7IgO7YXm7YXa7JW
++7NnALVPMLVIi7U4y7J0m7B3u7dEK7B2W7dDkLdA27RPywQ3a7MtC7d3q7hdILen4LgBC7eHe7hL
+sLVG4LVke7ZlO7Ia27FpK7Egu7kVOyNgO7pm27mkW7oh67Zl4Ld4O7n4qq80O7tYS7mLa7mye7Qz
+G7s1u7R/q7S9K7mEO7gH67tcG7iMe7zA+7rM+7rKi7zDW7SEm7tcW7VDS7vIS7lVa7OQC73RC7u2
+673cG7xbe7Wue7Pmm73Z67zTq7ga5KgyEinxawLzW78DMCdoNCtNpL/3S7/9O7/0678CjL//W8AE
+fMCLyronG7xOa77UO7z/Ucu7you7fyu469u3GIy4wMu3OBu9E5y1tQu4hVu5TSvBGWy5G0y3FMy7
+Jsy3KlzBMNzAv9sEFCzDz/vCETy+7WvD01vCEMzDKNy1CQAARFzERlzEUEjEMqLEA8DETnzETNzE
+R7zEAEDFVIzEM2LEVizFW/zEUFzECvwFrgvEDPzDxvvDO5zGFmy0Iiy+0lu9RVvBH5zBOmy04Zu0
+1uvDtBvD3Au9OKzGvxvH7FvDgMzDtyvChBzEbszH4lvIaqzHzcvHCqtBXwzFV1zFXJzJTeTFU5zF
+WpzJnIzJl+yET9zFlUzEYewFYzy+Mzy7jZzGLizJ7Nu3Z/zKGgzHSOvH/3PMweqLxrwstGQczELb
+woksy4ZczDJsvTNMw7ArzHRMyI2Mx5IszMh8uUN8ylj8yaUsxdr8xUs8yqCMyZ0szqFMzpeMzQCQ
+yo1bxsVbxt57xu1Mzcnrx+58w/VcvLqMxrUcy0ogt8bsyHUryPI80AStyLCcuH9s0N9ryLh8wnLs
+yNCMBJSMzuS8zV4czpaM0aZ8xU280dwczqZ8yurMBasssw39znh70AU9y/y8yIHLy6vcwb28u27c
+0sNsuIj8z21czTwdyP+MwoyM0IMc1C4Nyy3swRCd0wS9sNeMzuB80Z5c0eNs0RW9yVWtyR8N0txc
+ySO9BTG9vSWtvei7zMV2HMnqS9NwbLtvDMF47NDSu9ZvvdasDMzPrMzPu9AQzchxTczN/L1/jLCH
+zNB2fMxjTcvki7s7bdcqLQQTTdGO/diQHdmSLdldfQ3de66Q0NiTvdmc3dmeDcbmusA7O9qkXdqm
+fdqondqqvdqs3dqu/dqwHduyPdu0Xdu2fdu4ndumLcSf3du+/dtcHdqYDSyaDdzGfdyTXdnDXSbF
+jdzO/dzBvdzewiPQXd3WbcRXKd2/EgEX0Kfe/d3gHd7iLad/SgZBAAAh+QQFFAAxACwAAAUARAKH
+AAAG/8CYcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvtepuCsOBLLpvP6LR6zW673/B4USyv2+/4vH7P
+7/uTY0NhI4FihoZ/iYqLjI2Oj5BIYYKEY2EGBZmaBSwMBgaVgZGjpKWmp6ijdAKhEwUEsLGyBJos
+GJWpubq7vL2+UoODEwYMBCoYIMnKySoqspksIJO/1NXW19h6YiOE0dyhh9sjIAewmQiE2err7O3u
+TOLfrODh9YMY5izp7/z9/v+m7AkceIhbvn0AEypcyLCOoQkEIw5yxglhw4sYM2qkIqaCxI8jyhUA
+YXGjyZMoLwoLB/FjuAojihUombKmzZvZWOkUMKEnT/+PLg15HEFLnyicSJMqDUio1YQKEH0GDdNz
+BL4CCI4u3cq1655gYSoYQIAAqgCgU80SrajVq9u3cM28HEsWhEezaSfSohm3r9+/UQ71DEGWLNSn
+aCVKZaVgZFvAkCNLjvGwAgIMBipoztzzaUuCnbfB0je5tOm+YKFi0KwZQ9meCOwuFvzUgF1Dzhjw
+Pc27d8pgiDUfAFG4c2EDiDv3hMoawWIQex/7nk69oVDWxAsjQD5Be91lychm3o52ROPd1dOrf6fT
+MwjX3jl7n0//tZjG0tbr3+9OTOcN9NlFV31kYWAgMuKhRUt+/DXoIDX+9QQgfWIRWJcKGmigwgEq
+ECf/VVHSPSjiiJB0JGF9FRIIggoDtNjiCwe4hpwAtHhyAAwZiKDjBRlkQOKPQPbREU8TxmYgcRVk
+F6CLMMBAAgkDNBDldhPUuEIGFyTggZYccECBj0GGKSYc/hFJ1obNuKYkfS8MQIIDJsRpQgpQDqAC
+eQesIAIFCfTppwsuXDDmoISiQVVUAB7Qpot2EsgiDHJG6gAML2SHwQIi+KlpAhwEWuinoGJxwKif
++DRBCFIOkEEAKQTwYn0gvABnpHKmcCdZJ2SwqaZdghnqr8A2cUAIG4RwwgE9GeDiA5LCMAB888Ew
+Kwp9ohCnrQYicEGmu/bpwpfBhituEQecUCQGyMY6/0AAANBqwgvQerdAACZAYMG9FqyQpoEnZNkt
+pxz4Ou7AwA7rHbobnAADs3FCQEEEJsBwpzLZxgZDBBHcmwGHa+r5rwffEixywSegOypZxCrcrgko
+4ItCAPqmecKGxCmcAgwaDHdZXSt0wOeuHnDA48hEg8qhaxjcuQGAC8eJ770wxBgCYe8lzcwx8V52
+wdY/a7plpkMXLbaYHBbGIWEIrABDCy1cgG8GCxyAwMmjHq0dMiDY1m+mFPDZt985dupCAoIWgWWf
+YY+teHVHJ11sYc00MyoMC5CD7sQGdvhdeCAs4K+mImy9rZ9Bu+Clr7oCCqgHhS/uum8nA/jemrFp
+fv8kgUmPCkKu//ZeOrgxXMCB1wG/bvxpFRzw+O2XqUm7iseQAwO3vfeZwb0fWEAByAn0qGnfnFJw
+/PiSCaDC44UpWXUzFiKwIXy8V289CdR+QIIFfZ6+JeE9ZrClwOQLoFtYMawibYcuo4pegHKnObJ0
+gHrVy0D2LPAB+3FLaHy6AKsCcAEPeACAAgzhUlhhAAwYMDvrq5sKR/WCNukMV7qS3/wqSMMYaqlv
+IkiBDnX1QRH6cCs6UZ4BA6SCRTGqRXIrzJX+RQHRie5JNCQBt0SgKxF0QIcp8F8Pf8hFnATRhGiz
+0AFSxagGxAtT3QpdANa4xg7w6G19ogAJMnC/K+7/0IMg7KIeMzKPMJRriPTBgAaOOAB4acdn3eof
+jzqAsQgEIEdrtN4HEmCB++mQg17aoyZNshMBGOCPYazPGF3UgAYYsjCI3NSXMNaBALSykSlgQCYY
+sAA6RpFVPiveJnepEnB8UgVTs5AgX5ChusUrld/LACtZlYJGruAVtCjAAkhQwfttq4Pd46U2FdJJ
+nviRQycI5nzIoQIWaIAFLHghWdCoygw0k4ONbKQsTtCJJz0JfJ7apj79wYqWSOUwvwxnKGOjwgYa
+SQOf09SqMKbBeEagGLA4gQk6oTEReDCb+8xoO/rZnqgc5hIJPBIKlXGhuk0gA13rkyvtGAAYNNOZ
+/7LohDRB1j+N2pQdHA3LoRLjk08kkHbQ0cChDuCvDiQwcpE7wAJW8NIVzKIADACZCG5K1ZyMYCg8
+8edTHvKZMGRoTeVQAAFYI4Dp2a0+uVtAM1cAUVosgKZVjWs1PKMTtECEpxBhqg41YAztvAcWYhXr
+J7OGO6U6NIudyqNcF5uKjkYIrwRQ6gpKwNdbaQeik9XQ89qnghU0cqV4ZKxodzGkIXW1SgQoxgoe
+8IBieAc6sChBCSzbvvkc4J3988BUR8tbXfA0HKNxKgNUsIBYMOC4xRBrapNYW/qE4ABYikCPttjb
+6p7io4n5ZFFa0NpZEGAB4E1tMdTZXO9s4AAtcP9AFkNn3faigjVQMUAKZIvOB5Sgu7Boa35ZO9zy
+1mcDl8ISFd1L4FRAlAEnOO4DFFAMFTCAtc4wLmtjlL4NjYqw80mZBkKn2AJ7uBEEUICG9EuA1XK3
+BbNYwH0p/J0DJCAAcTMofYoF3Q/bmBSwACYCWJBasTKgBNxlbQn2y9oVxIu4VORTjDGMAMIcQAg9
+0pGUe2SF/lH5xljWAgE0gICl7TjEsbjvClow5BwvgMxye49S+0RFPwXgwq8NwYZioCvBqW54HV4C
+Dz3oAeBl+c9TGO7S0Cbe1BbZuwS4b4cw1AFNjc7NcSvoseicgDtv6Xd5RoLwXLClvv0P0KBWwtH/
+TkDqY0wNPomGqIqLi2hYKJVy4J2XjnpnxUnHgAKr43CPsjS4TBsB1xwInRtj6OtQf7hsS0t2sRbA
+AA3ATAFORTRxUyuLnGGA1KS+FAy6FTQOeGABMOARyFinQTZ2AKXeLjaUx72qNhLb2MaOUbK9YywG
+bKAFKWh1iR/QgtUeOJzKHvSluC244QkPz2w0N7r9vIQLAKqJCX8g6+AdahOi77UcaldlcyOL+7KW
+1cSa2tQCPjffASoDnLZiwhWupWL7z3QqT7i/1E1x3gpxoIUxGS0M/YDVPiC/KuBuCcCLAZw3Odno
+kp8IBneBVq68jSIINhNylGunm9uiNK85Y/8I/0jtjKqtPucvAVgr9BKQl96DPkAAaF1pwln96e9O
+ApZUl4CYmztLrdO6hwvY9ZyPKhYHEDJmW8ts3VmIWABeQO/8Z/C3r3xb1DUC1VeXAIivXFd51ztj
+N8H5m1uInNHet329W4CTtS/ti+e0259+9w8mbggovbOf2h1xHWletJzPfQGIVdtjKLUExeBu6L+7
+XNr+d2kqYCKu41hu1rux7rpFXQbsPDw/NX3lD9zt7eOqe90b/WBJE/MsiqGBFJT+GLVF+tpfLEvO
+M8B6jr874aS6LYt2ytu7svzVtb/9m26i1bVwAlikQydgWxgwZvp1X0P3AAWQJrWFeCegeAvQff+z
+tDWs50pZYlGcBmz41zvbknBV1H9UpQn6FguaEGRkpkMr4HXk4F0MwDYp0AlW84BLE4HtR4GZAAPx
+x0F8U2kuYEMytC0d4EaZQnOEdIRImIRKuIRM2IRO+IRQGIVSOIVUWIVWeIVYmIVauIVc2IVZ2Bgl
++FScwFpkaF8pYDYt9AKltIYNAAMtRUw5s1kZBmA3CICakAFDGHH+sieVBoQyJGWZIgJ10oQi+EOZ
+EIZiiAFlSHYpUIBP9gRMRmp6VQLmR4IlOEtYMoTD9jN8+EEy1C1NlHWFKEKHiIjPkAlBVob4tgKP
+6ASExVSyFWQAwAKliIiZgCmK5Cdf8jN+KEP/oTiKNgVNsaAAxFiMxEh6Y7iI/PZmUEBbeqWMJ1CL
+ppgJoIhSzPeJlUdFogiMPiSMxviNxXiKBZCKZRg3UMBc86WM/GaJpmgOiudo1shmEJR/uraN3NiN
+gAWO+niKLKCOKxAFMSKA5FiG7CgL+2iC78dmObIpOfI3f7MjNXWP3JeP+giOJpgJyohvUnBbHqeM
+GFCQIVaRymUOFEBFF5BSdWdlViaRvPUKIlmRF3kCi1gCU5AnZEhmBCmNLxmO5jA9KFl59siSGUWR
+O8mT5sBUqZgCUxAAQaZDqRiNwhiSRalcBbB+mzJgQklgUjmVRilNOkSGSikFAcBaKUCOLaCT/1xJ
+lVbJfEGZlfu0lWk5kgygQwwQAg8QllFQlulYhrJkkHEpVgWQfTpyZW6plX9plATAKsy2AgEwBVgU
+ZLSIln8JCxpQmFkGl2kpCyqWAhogYlUQALKlASB5mMplmZeJmVwZC2c2dMRYBfNFjFFJlHEJC6Z5
+maQ5kmf2AAEQYq7Zma12m7RZmzeGmqn5XdyVbwpgBSM5C7cpVsKZZc0pVmfWAgvQmlWwnMPYnATw
+nFgWnQqQAvxWncl5ndgZncHJnTYWneB5l9upnMvpne2JnjfWnOuJnMqZnd4pn9BJmi9Qn+N5ncRJ
+mvr5Z4epAR6XAv9JBd75jQMKaH9poEJWmf/uuaAJ2qDdmZb9KVs3gwUUaqGhFpesQomNeQX56aHG
+BgNcWX7LyKHNaaLwVpbiuZP1mQW36aIvSmaNuZNMqZE0Gpc2SnF6iZdJMIBcMJU/WnOgqUNNMF9C
+ugXgeKS3x6RNsEZQWqVDkKQjaqVaygQiuqVeqgRY9KViagRhOqYNGgEi8DRquqYW0Epuw6ZwGqdy
+Oqd0Wqd2ei8XEAFmOjZZ4i5++qeAGqiCOqiEWqiGagKEs6digz/Y2KiO+qiQGqnd0jKKWjT441CY
+mqmauqmc2qme+qmg2qkeYAGVSjSXGqqomqqquqqsOqqlOjKnyqqyOqu0qqqu+qoEgz8QsKv/vNqr
+vvqrwBqswjqsxFqsxnqsEEBJuJqrCYCszvqs0Bqt0pqspLqs46Kr05qt2rqtyKqs1iou2DqtLcKt
+vTquEGCuwIqu5Pqr3toGEiABUPCu8QqvoCKvMWCvV4CvWKCvKWGv/DqvQvCvRYA/DlCwBnuwBtsi
+B6uwDsCwCPuwEOuwDDuxAwCxBeuwFpuxF1uxGtuuX/CuIAuy90qvTiCwSWCyUoCvAquyJDuyIksE
+IUuvMSuyMxuwLXuvNosEMVsEO2uzIWsENXsE/IqyI5uzJ0u0RksFLLsLP2sG/iqzN0u0T5u0RECw
+GruxC8uxV3u1FJuwWquxGLu1Dxu2D+ux/2WAtF2AtiVLsisLtTDLtnALs287tzyrr24LtHFLtXer
+szcrtCzbt3yrBC/rtICrC2qbr307uIO7BFNrBFbLtV/btRsrsRUbtgqLsZLbsC6CtZrrtZS7uZyb
+sWZLBnYLt0srr39rulC7uG/7slO7s21btEMwtHuLt3QrtypLtbrbuHUruIXburObusGru3ObunmL
+urXbtLCrusG7uE3rsoc7vLILvc1rssbbum77tKXruqvbvNUrvbxbtQlAqC0SKeVrAuebvgMQJ0dE
+K4zivuuLvvF7vugrv/bLvvObv/i7v4E6uh/bso2rvci7u2wru/8awHe7tHhLu73rsoErvf+4W8DT
+C8FGy7oTbLuMW7TwesAJ/LsDXMEdvMEfTLcKjMAgfMIXzLgdjMKxy8IFvLwuXMEanMImnMIxgD8A
+kMM6vMM6jIQ53CI/PABBPMQ8HMRCzMNADABJnMQ97CI7vMRHDMVEXMQ67L9eULoxDL5Ru8XEW8O8
+27aJy8U+y8DV668yTMNiDL2qy8HK67ozfL0GvLpxW8IrHMdlnLReXMc1jL0YnMfEO717rLd6nMBv
+vMZZPLAJQMVFzMRKHMWOzChTjMRO/MSOHMmNzMiERMRSrMg5bMVpC8ByHL5/S8F23MB+/MfPS8AL
+fLs0PLy1q8p1+8oNLLhZHMi9S8e1PMj/WpzLbfzHvovLpTzLtry93MvCuXzI4svJlLzMjWzJzUzF
+QIzJlfzMTUzNjKzJR6zMVVytZ4DFaMzKwYzCJKzLKjzBZDzL5py4rtzFaTzOvssE5GzLZXzK9Ly3
+9czOvny0wBy+EDzMoNzHhFzKp5wEOKzN1PzMm6zEzozN1YzQ2SzEm8zE05zQiuzJXODNuZvOpvzC
+snzPqJy8eYuzFOzNcuu9NqzAIi3OfgvPAI3Mt+vRHi3Q5CzS/AzPijvTL13HOEvS8dzSMv3HBa3N
+0jzFkLzQ1jzRDz3JEv3ISc3QymzRW8DTNz3OxKzSdzzGpnvSIa3GsXzOaiyzG92zXI29s2scxqlc
+0jldzDYMyPGs02JNs8wrzs9b0zbd1bts18Hcy3x81WYszHpN10Ft0II92IRd2IZd2FB9DdH7rYwQ
+2If92JAd2ZLdydz8vzN72Zid2Zq92Zzd2Z792aAd2qI92qRd2qZ92qid2qq92qzd2pqdzJMd27I9
+21Sc2IwdJo5N27q924Zt27cNJLnN28I93LVd2b/9KShF3Mq93DuMlccNKhHwpnc63dRd3dZt3XlK
+BkEAADs=
+
+--------------Boundary-00=_S8LEXFP0000000000000--
+
+
diff --git a/comm/mailnews/local/test/unit/data/message1.eml b/comm/mailnews/local/test/unit/data/message1.eml
new file mode 100644
index 0000000000..e82610ebc7
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/message1.eml
@@ -0,0 +1,7 @@
+From: from@invalid.com
+To: to@invalid.com
+Subject: test mail
+
+this email is in dos format because that is what the interface requires
+
+test message
diff --git a/comm/mailnews/local/test/unit/data/message2.eml b/comm/mailnews/local/test/unit/data/message2.eml
new file mode 100644
index 0000000000..9574d946aa
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/message2.eml
@@ -0,0 +1,9 @@
+From: from@invalid.com
+To: to@invalid.com
+Message-ID: <abc@def.org>
+cc: dupemail@test.com
+Subject: test mail
+
+this email is in dos format because that is what the interface requires
+
+test message
diff --git a/comm/mailnews/local/test/unit/data/message3.eml b/comm/mailnews/local/test/unit/data/message3.eml
new file mode 100644
index 0000000000..92722046f9
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/message3.eml
@@ -0,0 +1,7 @@
+From: from@invalid.com
+To: to@invalid.com
+Subject: test mail 2
+
+this email is in dos format because that is what the interface requires
+
+test message 2
diff --git a/comm/mailnews/local/test/unit/data/movemailspool b/comm/mailnews/local/test/unit/data/movemailspool
new file mode 100644
index 0000000000..1fb0587c49
--- /dev/null
+++ b/comm/mailnews/local/test/unit/data/movemailspool
@@ -0,0 +1,169 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from caspiaco by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for kent@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([63.245.208.146] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for kent@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <kent@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::63.245.208.146:host29.hostmonster.com::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
+From - Sat Jun 07 04:23:42 2008
+X-Mozilla-Status: 0000
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+FCC: mailbox://nobody@Local%20Folders/Sent
+BCC: Some User <u1@example.com>, Another Person <u2@example.com>
+X-Identity-Key: id2
+Message-ID: <4849BF7B.2030800@example.com>
+Date: Sat, 07 Jun 2008 04:23:42 +0530
+From: Some User <example@example.com>
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0
+User-Agent: Thunderbird 3.0a2pre (Windows/2008060416)
+MIME-Version: 1.0
+To: bugmail@example.org
+Subject: Hello, did you receive my bugmail?
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta http-equiv="content-type"
+ content="text/html; charset=ISO-8859-1">
+</head>
+<body text="#000000" bgcolor="#ffffff">
+This is an HTML message. Did you receive my bugmail?<br>
+<br>
+Thanks, and goodbye.<br>
+<br>
+--<br>
+Some User<br>
+<br>
+</body>
+</html>
+
+
+From - Tue Oct 02 00:26:47 2007
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 655578] list-id filter broken
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+ <4DC2493C.4060403@gmx.invalid>
+ <BANLkTin2w8LJxYGHV3_5NpFbsiBhrP96XA@mail.gmail.invalid>
+ <175221688.20110506234025@my_localhost>
+ <201105072315.25120@thufir.ingo-kloecker.invalid>
+ <BANLkTinacQCd+mZ7fL1THLK55X2+u9g5-w@mail.gmail.invalid>
+ <05433510.20110507224940@my_localhost>
+ <4DC5C015.7050800@sixdemonbag.invalid>
+ <BANLkTinv7NvPG9gE1Fha+X+6ZkHzdXdRdg@mail.gmail.invalid>
+ <BANLkTi=6zDTsYymc+bUTwPOM2AohJD2wfA@mail.gmail.invalid>
+Reply-To: FOO <bar@foo.invalid>
+List-Id: Help and discussion among users of GnuPG <gnupg-users.gnupg.org>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/local/test/unit/head_maillocal.js b/comm/mailnews/local/test/unit/head_maillocal.js
new file mode 100644
index 0000000000..dff897b36e
--- /dev/null
+++ b/comm/mailnews/local/test/unit/head_maillocal.js
@@ -0,0 +1,214 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var test = null;
+
+// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo.
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+// Import the pop3 server scripts
+/* import-globals-from ../../../test/fakeserver/Maild.jsm */
+/* import-globals-from ../../../test/fakeserver/Auth.jsm */
+/* import-globals-from ../../../test/fakeserver/Pop3d.jsm */
+var {
+ nsMailServer,
+ gThreadManager,
+ fsDebugNone,
+ fsDebugAll,
+ fsDebugRecv,
+ fsDebugRecvSend,
+} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm");
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var {
+ Pop3Daemon,
+ POP3_RFC1939_handler,
+ POP3_RFC2449_handler,
+ POP3_RFC5034_handler,
+} = ChromeUtils.import("resource://testing-common/mailnews/Pop3d.jsm");
+
+// Setup the daemon and server
+// If the debugOption is set, then it will be applied to the server.
+function setupServerDaemon(debugOption) {
+ var daemon = new Pop3Daemon();
+ var extraProps = {};
+ function createHandler(d) {
+ var handler = new POP3_RFC5034_handler(d);
+ for (var prop in extraProps) {
+ handler[prop] = extraProps[prop];
+ }
+ return handler;
+ }
+ var server = new nsMailServer(createHandler, daemon);
+ if (debugOption) {
+ server.setDebugLevel(debugOption);
+ }
+ return [daemon, server, extraProps];
+}
+
+function createPop3ServerAndLocalFolders(port, hostname = "localhost") {
+ localAccountUtils.loadLocalMailAccount();
+ let server = localAccountUtils.create_incoming_server(
+ "pop3",
+ port,
+ "fred",
+ "wilma",
+ hostname
+ );
+ return server;
+}
+
+var gCopyListener = {
+ callbackFunction: null,
+ copiedMessageHeaderKeys: [],
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ try {
+ this.copiedMessageHeaderKeys.push(aKey);
+ } catch (ex) {
+ dump(ex);
+ }
+ },
+ GetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ if (this.callbackFunction) {
+ mailTestUtils.do_timeout_function(0, this.callbackFunction, null, [
+ this.copiedMessageHeaderKeys,
+ aStatus,
+ ]);
+ }
+ },
+};
+
+/**
+ * copyFileMessageInLocalFolder
+ * A utility wrapper of nsIMsgCopyService.copyFileMessage to copy a message
+ * into local inbox folder.
+ *
+ * @param aMessageFile An instance of nsIFile to copy.
+ * @param aMessageFlags Message flags which will be set after message is
+ * copied
+ * @param aMessageKeyword Keywords which will be set for newly copied
+ * message
+ * @param aMessageWindow Window for notification callbacks, can be null
+ * @param aCallback Callback function which will be invoked after
+ * message is copied
+ */
+function copyFileMessageInLocalFolder(
+ aMessageFile,
+ aMessageFlags,
+ aMessageKeywords,
+ aMessageWindow,
+ aCallback
+) {
+ // Set up local folders
+ localAccountUtils.loadLocalMailAccount();
+
+ gCopyListener.callbackFunction = aCallback;
+ // Copy a message into the local folder
+ MailServices.copy.copyFileMessage(
+ aMessageFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ aMessageFlags,
+ aMessageKeywords,
+ gCopyListener,
+ aMessageWindow
+ );
+}
+
+function do_check_transaction(real, expected) {
+ // If we don't spin the event loop before starting the next test, the readers
+ // aren't expired. In this case, the "real" real transaction is the last one.
+ if (Array.isArray(real)) {
+ real = real[real.length - 1];
+ }
+
+ // real.them may have an extra QUIT on the end, where the stream is only
+ // closed after we have a chance to process it and not them. We therefore
+ // excise this from the list
+ if (real.them[real.them.length - 1] == "QUIT") {
+ real.them.pop();
+ }
+
+ if (expected[0] == "AUTH") {
+ // We don't send initial AUTH command now.
+ expected = expected.slice(1);
+ }
+
+ Assert.equal(real.them.join(","), expected.join(","));
+ dump("Passed test " + test + "\n");
+}
+
+function create_temporary_directory() {
+ let directory = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ directory.append("mailFolder");
+ directory.createUnique(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
+ return directory;
+}
+
+function create_sub_folders(parent, subFolders) {
+ parent.leafName = parent.leafName + ".sbd";
+ parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
+
+ for (let folder in subFolders) {
+ let subFolder = parent.clone();
+ subFolder.append(subFolders[folder].name);
+ subFolder.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
+ if (subFolders[folder].subFolders) {
+ create_sub_folders(subFolder, subFolders[folder].subFolders);
+ }
+ }
+}
+
+function create_mail_directory(subFolders) {
+ let root = create_temporary_directory();
+
+ for (let folder in subFolders) {
+ if (!subFolders[folder].subFolders) {
+ continue;
+ }
+ let directory = root.clone();
+ directory.append(subFolders[folder].name);
+ create_sub_folders(directory, subFolders[folder].subFolders);
+ }
+
+ return root;
+}
+
+function setup_mailbox(type, mailboxPath) {
+ let user = Services.uuid.generateUUID().toString();
+ let incomingServer = MailServices.accounts.createIncomingServer(
+ user,
+ "Local Folder",
+ type
+ );
+ incomingServer.localPath = mailboxPath;
+
+ return incomingServer.rootFolder;
+}
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/local/test/unit/test_Pop3Channel.js b/comm/mailnews/local/test/unit/test_Pop3Channel.js
new file mode 100644
index 0000000000..b518e5b19f
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_Pop3Channel.js
@@ -0,0 +1,75 @@
+/* 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 { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+let [daemon, server] = setupServerDaemon();
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Test Pop3Channel can download a partial message correctly.
+ */
+add_task(async function test_fetchPartialMessage() {
+ // Set up a test message.
+ daemon.setMessages(["message1.eml"]);
+
+ // Set up the incoming server to fetch headers only.
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer
+ .QueryInterface(Ci.nsILocalMailIncomingServer)
+ .createDefaultMailboxes();
+ incomingServer.headersOnly = true;
+
+ // Use GetNewMail to fetch the headers.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ incomingServer.rootFolder.getChildNamed("Inbox"),
+ incomingServer
+ );
+ await urlListener.promise;
+
+ // Check TOP is correctly sent.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "TOP 1 0",
+ ]);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ // A nsIPop3URL instance is needed to construct a Pop3Channel, but we can't
+ // create a nsIPop3URL instance in JS directly. A workaround is constructing a
+ // mailbox: url with a uidl query, then newChannel will return a Pop3Channel.
+ let channel = NetUtil.newChannel({
+ uri: `${incomingServer.serverURI}/Inbox?uidl=UIDL1`,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+ channel.asyncOpen(streamListener);
+ await streamListener.promise;
+
+ // Check RETR is correctly sent.
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ]);
+});
diff --git a/comm/mailnews/local/test/unit/test_bug457168.js b/comm/mailnews/local/test/unit/test_bug457168.js
new file mode 100644
index 0000000000..6b2ec41b88
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_bug457168.js
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Protocol tests for POP3.
+ */
+
+var server;
+var daemon;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "Get New Mail, One Message",
+ messages: ["message2.eml", "message2.eml", "message3.eml"],
+ transaction: [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ "RETR 2",
+ "DELE 2",
+ "RETR 3",
+ "DELE 3",
+ ],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ var transaction = server.playTransaction();
+
+ do_check_transaction(transaction, thisTest.transaction);
+
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 2);
+ Assert.equal(localAccountUtils.inboxFolder.getNumUnread(false), 1);
+
+ Assert.equal(result, 0);
+ } catch (e) {
+ // If we have an error, clean up nicely before we throw it.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, checkBusy);
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ // Set up the test
+ test = thisTest.title;
+
+ daemon.setMessages(thisTest.messages);
+
+ // Now get the mail
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+function run_test() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setIntPref("mail.server.default.dup_action", 2);
+
+ server = setupServerDaemon();
+ daemon = server[0];
+ server = server[1];
+ server.start();
+
+ // Set up the basic accounts and folders
+ incomingServer = createPop3ServerAndLocalFolders(server.port);
+
+ // Create a cc filter
+ var filters = incomingServer.getFilterList(null);
+
+ var filter = filters.createFilter("cc dup test");
+ filter.filterType = Ci.nsMsgFilterType.Incoming;
+ var searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.CC;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ var oldValue = searchTerm.value;
+ oldValue.attrib = Ci.nsMsgSearchAttrib.CC;
+ oldValue.str = "dupemail";
+ searchTerm.value = oldValue;
+ filter.appendTerm(searchTerm);
+ var filterAction = filter.createAction();
+ filterAction.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(filterAction);
+ filter.enabled = true;
+
+ filters.insertFilterAt(0, filter);
+
+ incomingServer.setFilterList(filters);
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ do_test_pending();
+
+ testNext();
+}
diff --git a/comm/mailnews/local/test/unit/test_duplicateKey.js b/comm/mailnews/local/test/unit/test_duplicateKey.js
new file mode 100644
index 0000000000..845a25cc1a
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_duplicateKey.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test deletes intermediate messages, then compacts, then adds more
+ * messages, testing for duplicated keys in bug 1202105.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_task(async function runPump() {
+ gPOP3Pump.files = [
+ "../../../data/bugmail1",
+ "../../../data/bugmail1",
+ "../../../data/bugmail1",
+ "../../../data/bugmail1",
+ "../../../data/bugmail1",
+ ];
+ await gPOP3Pump.run();
+
+ // get message headers for the inbox folder
+ var hdrs = showMessages(localAccountUtils.inboxFolder);
+ Assert.equal(hdrs.length, 5, "Check initial db count");
+
+ // Deletes 2 middle messages.
+ let deletes = [hdrs[1], hdrs[2]];
+
+ // Note the listener won't work because this is a sync delete,
+ // but it should!
+ localAccountUtils.inboxFolder.deleteMessages(
+ deletes,
+ null, // in nsIMsgWindow msgWindow,
+ true, // in boolean deleteStorage,
+ true, // in boolean isMove,
+ null, // in nsIMsgCopyServiceListener,
+ false
+ ); // in boolean allowUndo
+
+ dump("Messages after delete\n");
+ hdrs = showMessages(localAccountUtils.inboxFolder);
+ Assert.equal(hdrs.length, 3, "Check db length after deleting two messages");
+
+ // compact
+ var listener = new PromiseTestUtils.PromiseUrlListener();
+ localAccountUtils.inboxFolder.compact(listener, null);
+ await listener.promise;
+
+ dump("Messages after compact\n");
+ hdrs = showMessages(localAccountUtils.inboxFolder);
+ Assert.equal(hdrs.length, 3, "Check db length after compact");
+
+ // Add some more messages. This fails in nsMsgDatabase::AddNewHdrToDB with
+ // NS_ERROR("adding hdr that already exists") before bug 1202105.
+ gPOP3Pump.files = ["../../../data/draft1"];
+ await gPOP3Pump.run();
+
+ dump("Messages after new message\n");
+ hdrs = showMessages(localAccountUtils.inboxFolder);
+ Assert.equal(hdrs.length, 4, "Check db length after adding one message");
+
+ gPOP3Pump = null;
+});
+
+function showMessages(folder) {
+ var hdrs = [];
+ for (let hdr of folder.msgDatabase.enumerateMessages()) {
+ hdrs.push(hdr);
+ dump(
+ "key " +
+ (hdrs.length - 1) +
+ " is " +
+ hdrs[hdrs.length - 1].messageKey +
+ "\n"
+ );
+ }
+ return hdrs;
+}
diff --git a/comm/mailnews/local/test/unit/test_fileName.js b/comm/mailnews/local/test/unit/test_fileName.js
new file mode 100644
index 0000000000..e69b1b45a4
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_fileName.js
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Test handling of special chars in folder names
+ */
+
+function run_test() {
+ let testFolderName = "";
+ let OSname = Services.sysinfo.getProperty("name");
+ if (OSname == "Windows_NT") {
+ // On Windows test file with ' ' in the name.
+ testFolderName = "bugmail 1";
+ } else if (OSname == "Linux") {
+ // On Linux test file with '`' in the name.
+ testFolderName = "bugmail`1";
+ } else if (OSname == "Darwin") {
+ // On Mac test file with ':' in the name (generated from Mozilla 1.8 branch).
+ testFolderName = "bugmail:1";
+ } else {
+ // Not sure what this OS is so just use a safe name.
+ testFolderName = "bugmail1";
+ }
+
+ let bugmail = do_get_file("../../../data/bugmail-1");
+ let bugmailmsf = do_get_file("../../../data/bugmail-1.msf");
+ let localMailDir = do_get_profile().clone();
+ localMailDir.append("Mail");
+ localMailDir.append("Local Folders");
+ let pop3dir = do_get_profile().clone();
+ pop3dir.append("Mail");
+ pop3dir.append("poptest");
+ // Copy the file to the local mail directory
+ bugmail.copyTo(localMailDir, testFolderName);
+ bugmailmsf.copyTo(localMailDir, testFolderName + ".msf");
+
+ // Copy the file to the pop3 server mail directory
+ bugmail.copyTo(pop3dir, testFolderName);
+ bugmailmsf.copyTo(pop3dir, testFolderName + ".msf");
+
+ // These preferences set up a local folders account so we'll use the
+ // contents of the Local Folders dir we've already pre-populated.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2"
+ );
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account1");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.name", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server2.directory-rel",
+ "[ProfD]Mail/poptest"
+ );
+ Services.prefs.setCharPref("mail.server.server2.hostname", "poptest");
+ Services.prefs.setCharPref("mail.server.server2.name", "poptest");
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server2.userName", "user");
+ // This basically says to ignore the time stamp in the .msf file
+ Services.prefs.setIntPref("mail.db_timestamp_leeway", 0x7fffffff);
+
+ localAccountUtils.incomingServer = MailServices.accounts.localFoldersServer;
+ // force load of accounts.
+ MailServices.accounts.defaultAccount;
+
+ let pop3Server = MailServices.accounts.findServer("user", "poptest", "pop3");
+ let rootFolder =
+ localAccountUtils.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ let pop3Root = pop3Server.rootMsgFolder;
+
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ localAccountUtils.inboxFolder = rootFolder.createLocalSubfolder("Inbox");
+ // a local inbox should have a Mail flag!
+ localAccountUtils.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+
+ rootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ bugmail = rootFolder.getChildNamed(testFolderName);
+ Assert.equal(bugmail.getTotalMessages(false), 1);
+ bugmail = pop3Root.getChildNamed(testFolderName);
+ Assert.equal(bugmail.getTotalMessages(false), 1);
+
+ // Check if creating an empty folder returns a proper error
+ // instead of crash (bug 831190).
+ try {
+ rootFolder.createSubfolder("", null);
+ do_throw("createSubfolder() should have failed on empty folder name.");
+ } catch (e) {
+ // NS_MSG_ERROR_INVALID_FOLDER_NAME
+ Assert.equal(e.result, 2153054242);
+ }
+
+ // And try to create an existing folder again.
+ try {
+ rootFolder.createSubfolder(testFolderName, null);
+ do_throw("createSubfolder() should have failed on existing folder.");
+ } catch (e) {
+ // NS_MSG_FOLDER_EXISTS
+ Assert.equal(e.result, 2153054227);
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_folderLoaded.js b/comm/mailnews/local/test/unit/test_folderLoaded.js
new file mode 100644
index 0000000000..398aa6bff4
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_folderLoaded.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+/**
+ * The intent of this file is to show a folder loaded event after a load
+ * with a null database.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+var gMsgFile1 = do_get_file("../../../data/bugmail1");
+var gMsgFile2 = do_get_file("../../../data/draft1");
+
+var gTargetFolder = null;
+
+add_setup(async function () {
+ if (typeof localAccountUtils.inboxFolder == "undefined") {
+ localAccountUtils.loadLocalMailAccount();
+ }
+ localAccountUtils.rootFolder.createSubfolder("target", null);
+ gTargetFolder = localAccountUtils.rootFolder.getChildNamed("target");
+
+ let copyListenerFile1 = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ gMsgFile1,
+ gTargetFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListenerFile1,
+ null
+ );
+ await copyListenerFile1.promise;
+
+ let copyListenerFile2 = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ gMsgFile2,
+ gTargetFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListenerFile2,
+ null
+ );
+ await copyListenerFile2.promise;
+});
+
+add_task(async function firstUpdate() {
+ // Get message headers for the target folder.
+ var msgCount = 0;
+ for (let hdr of gTargetFolder.msgDatabase.enumerateMessages()) {
+ msgCount++;
+ Assert.equal(hdr.subject, testSubjects[msgCount - 1]);
+ }
+ Assert.equal(msgCount, 2);
+
+ let folderAddedListener = PromiseTestUtils.promiseFolderEvent(
+ gTargetFolder,
+ "FolderLoaded"
+ );
+ gTargetFolder.updateFolder(null);
+ await folderAddedListener;
+});
+
+add_task(async function secondUpdate() {
+ // If the following executes, the test hangs in bug 787557.
+ gTargetFolder.msgDatabase = null;
+ let folderAddedListener = PromiseTestUtils.promiseFolderEvent(
+ gTargetFolder,
+ "FolderLoaded"
+ );
+ gTargetFolder.updateFolder(null);
+ await folderAddedListener;
+});
diff --git a/comm/mailnews/local/test/unit/test_localFolder.js b/comm/mailnews/local/test/unit/test_localFolder.js
new file mode 100644
index 0000000000..de7bb7619f
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_localFolder.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+/**
+ * nsIMsgFolder.subFolders tests
+ * These tests intend to test pluggableStore.discoverSubFolders
+ * and nsIMsgFolder.hasSubFolders.
+ */
+
+// Currently we have two mailbox storage formats.
+var gPluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+
+/**
+ * Check whether the expected folder structure
+ * exists in the root folder "mailFolder".
+ *
+ * @param expected array of folders and subfolders
+ * we expect
+ * @param actual actual subfolders enumerator
+ */
+function check_sub_folders(expected, actual) {
+ for (let actualFolder of actual) {
+ let index;
+ for (index = 0; index < expected.length; index++) {
+ if (expected[index].name == actualFolder.name) {
+ break;
+ }
+ }
+ // If index goes out of bounds, probably we didn't find the name.
+ Assert.ok(index < expected.length);
+
+ let pluggableStore = actualFolder.msgStore;
+ pluggableStore.discoverSubFolders(actualFolder, true);
+ Assert.equal(!!expected[index].subFolders, actualFolder.hasSubFolders);
+ if (actualFolder.hasSubFolders) {
+ Assert.equal(
+ expected[index].subFolders.length,
+ actualFolder.numSubFolders
+ );
+ check_sub_folders(expected[index].subFolders, actualFolder.subFolders);
+ }
+ }
+}
+
+/**
+ * Test default mailbox without creating any subfolders.
+ */
+function test_default_mailbox(expected, type) {
+ let mailbox = setup_mailbox(type, create_temporary_directory());
+
+ check_sub_folders(expected, mailbox.subFolders);
+}
+
+/**
+ * A helper method to add the folders in aFolderArray
+ * to the aParentFolder as subfolders.
+ *
+ * @param aFolderArray array of folders and subfolders
+ * (js objects).
+ * @param aParentFolder folder (nsIMsgFolder) to which
+ * the folders and subfolders from
+ * aFolderArray are to be added.
+ */
+function add_sub_folders(aFolderArray, aParentFolder) {
+ for (let msgFolder of aFolderArray) {
+ if (!aParentFolder.containsChildNamed(msgFolder.name)) {
+ aParentFolder.createSubfolder(msgFolder.name, null);
+ }
+ if (msgFolder.subFolders) {
+ add_sub_folders(
+ msgFolder.subFolders,
+ aParentFolder.getChildNamed(msgFolder.name)
+ );
+ }
+ }
+}
+
+/**
+ * Create a server with folders and subfolders from the
+ * "expected" structure, then create a new server with
+ * the same filePath, and test that we can discover these
+ * folders based on that filePath.
+ */
+function test_mailbox(expected, type) {
+ let mailboxRootFolder = setup_mailbox(type, create_temporary_directory());
+ add_sub_folders(expected, mailboxRootFolder);
+
+ let actualFolder = setup_mailbox(type, mailboxRootFolder.filePath);
+ check_sub_folders(expected, actualFolder.subFolders);
+}
+
+function run_all_tests() {
+ test_default_mailbox([{ name: "Trash" }, { name: "Outbox" }], "none");
+ test_default_mailbox([{ name: "Inbox" }, { name: "Trash" }], "pop3");
+
+ // Assuming that the order of the folders returned from the actual folder
+ // discovery is independent and un-important for this test.
+ test_mailbox(
+ [
+ {
+ name: "Inbox",
+ subFolders: [
+ {
+ name: "sub4",
+ },
+ ],
+ },
+ {
+ name: "Trash",
+ },
+ ],
+ "pop3"
+ );
+
+ test_mailbox(
+ [
+ {
+ name: "Inbox",
+ subFolders: [
+ {
+ name: "inbox-sub1",
+ subFolders: [
+ {
+ name: "inbox-sub-sub1",
+ },
+ {
+ name: "inbox-sub-sub2",
+ },
+ ],
+ },
+ {
+ name: "inbox-sub2",
+ },
+ ],
+ },
+ {
+ name: "Trash",
+ },
+ {
+ name: "Outbox",
+ subFolders: [
+ {
+ name: "outbox-sub1",
+ },
+ ],
+ },
+ ],
+ "pop3"
+ );
+}
+
+function run_test() {
+ for (let store in gPluggableStores) {
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ gPluggableStores[store]
+ );
+ run_all_tests();
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_mailboxContentLength.js b/comm/mailnews/local/test/unit/test_mailboxContentLength.js
new file mode 100644
index 0000000000..f97a787946
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_mailboxContentLength.js
@@ -0,0 +1,62 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test content length for the mailbox protocol. This focuses on necko URLs
+ * that are run externally.
+ */
+
+// Take a multipart message as we're testing attachment URLs as well
+var gFile = do_get_file("../../../data/multipart-complex2");
+
+function run_test() {
+ do_test_pending();
+ copyFileMessageInLocalFolder(gFile, 0, "", null, verifyContentLength);
+}
+
+function verifyContentLength(aMessageHeaderKeys, aStatus) {
+ Assert.notEqual(aMessageHeaderKeys, null);
+ // First get the message URI
+ let msgHdr = localAccountUtils.inboxFolder.GetMessageHeader(
+ aMessageHeaderKeys[0]
+ );
+ let messageUri = localAccountUtils.inboxFolder.getUriForMsg(msgHdr);
+ // Convert this to a URI that necko can run
+ let messageService = MailServices.messageServiceFromURI(messageUri);
+ let neckoURL = messageService.getUrlForUri(messageUri);
+ // Don't use the necko URL directly. Instead, get the spec and create a new
+ // URL using the IO service
+ let urlToRun = Services.io.newURI(neckoURL.spec);
+
+ // Get a channel from this URI, and check its content length
+ let channel = Services.io.newChannelFromURI(
+ urlToRun,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ Assert.equal(channel.contentLength, gFile.fileSize);
+
+ // Now try an attachment. &part=1.2
+ let attachmentURL = Services.io.newURI(neckoURL.spec + "&part=1.2");
+ Services.io.newChannelFromURI(
+ attachmentURL,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ // Currently attachments have their content length set to the length of the
+ // entire message
+ Assert.equal(channel.contentLength, gFile.fileSize);
+
+ do_test_finished();
+}
diff --git a/comm/mailnews/local/test/unit/test_mailboxProtocol.js b/comm/mailnews/local/test/unit/test_mailboxProtocol.js
new file mode 100644
index 0000000000..f7662e8cfb
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_mailboxProtocol.js
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for getting mailbox urls via the protocol handler.
+ */
+
+var defaultProtocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS |
+ Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC;
+
+var protocols = [
+ {
+ protocol: "mailbox",
+ urlSpec: "mailbox://user@localhost/",
+ // mailbox protocol doesn't use a port
+ defaultPort: -1,
+ },
+];
+
+function run_test() {
+ for (var part = 0; part < protocols.length; ++part) {
+ print("protocol: " + protocols[part].protocol);
+
+ var pH = Cc[
+ "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol
+ ].createInstance(Ci.nsIProtocolHandler);
+
+ Assert.equal(pH.scheme, protocols[part].protocol);
+ Assert.equal(
+ Services.io.getDefaultPort(pH.scheme),
+ protocols[part].defaultPort
+ );
+ Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags);
+
+ // Whip through some of the ports to check we get the right results.
+ for (let i = 0; i < 1024; ++i) {
+ Assert.equal(pH.allowPort(i, ""), false);
+ }
+
+ // Check we get a URI when we ask for one
+ var uri = Services.io.newURI(protocols[part].urlSpec);
+
+ uri.QueryInterface(Ci.nsIMailboxUrl);
+
+ Assert.equal(uri.spec, protocols[part].urlSpec);
+
+ // XXX This fails on Windows
+ // do_check_neq(pH.newChannel(uri), null);
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_mailboxURL.js b/comm/mailnews/local/test/unit/test_mailboxURL.js
new file mode 100644
index 0000000000..f2330cf8ca
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_mailboxURL.js
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for mailbox: URLs.
+ */
+
+var mailboxFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+mailboxFile.append("mailFolder");
+mailboxFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+var mailboxFileName = Services.io.newFileURI(mailboxFile).pathQueryRef;
+
+var mailboxURLs = [
+ {
+ url: "mailbox://user@domain@example.com/folder?number=1",
+ spec: "mailbox://user%40domain@example.com/folder?number=1",
+ host: "example.com",
+ port: -1,
+ scheme: "mailbox",
+ pathQueryRef: "/folder?number=1",
+ prePath: "mailbox://user%40domain@example.com",
+ },
+ {
+ url: "mailbox://nobody@Local%20Folders/folder?number=2",
+ spec: "mailbox://nobody@local%20folders/folder?number=2",
+ host: "local%20folders",
+ port: -1,
+ scheme: "mailbox",
+ pathQueryRef: "/folder?number=2",
+ prePath: "mailbox://nobody@local%20folders",
+ },
+ {
+ url: "mailbox://" + mailboxFileName + "?number=3",
+ spec: "mailbox://" + mailboxFileName + "?number=3",
+ host: "",
+ port: -1,
+ scheme: "mailbox",
+ pathQueryRef: mailboxFileName + "?number=3",
+ prePath: "mailbox://",
+ },
+];
+
+function run_test() {
+ registerCleanupFunction(teardown);
+ var url;
+
+ // Test - get and check urls.
+ var part = 0;
+ for (part = 0; part < mailboxURLs.length; part++) {
+ dump(`url: ${mailboxURLs[part].url}\n`);
+ url = Services.io.newURI(mailboxURLs[part].url);
+
+ Assert.equal(url.spec, mailboxURLs[part].spec);
+ Assert.equal(url.scheme, mailboxURLs[part].scheme);
+ Assert.equal(url.host, mailboxURLs[part].host);
+ Assert.equal(url.port, mailboxURLs[part].port);
+ Assert.equal(url.pathQueryRef, mailboxURLs[part].pathQueryRef);
+ Assert.equal(url.prePath, mailboxURLs[part].prePath);
+ }
+
+ // Test - Check changing values.
+ dump("Other Tests\n");
+
+ // We can set the username on the URLs with a host.
+ url = Services.io.newURI("mailbox://user@domain@example.com/folder?number=1");
+ url.mutate().setUsername("john").finalize();
+ url = Services.io.newURI("mailbox://nobody@Local%20Folders/folder?number=2");
+ url.mutate().setUsername("jane").finalize();
+
+ // It should throw on our file-style URLs.
+ url = Services.io.newURI("mailbox://" + mailboxFileName + "?number=3");
+ try {
+ url.mutate().setUsername("noway").finalize();
+ do_throw("Should not be able to set username on file-style mailbox: URL");
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_UNEXPECTED);
+ }
+}
+
+function teardown() {
+ if (mailboxFile.exists()) {
+ mailboxFile.remove(false);
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_msgCopy.js b/comm/mailnews/local/test/unit/test_msgCopy.js
new file mode 100644
index 0000000000..c49bff465b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_msgCopy.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+// Test of setting keywords with copyFileMessage
+
+var bugmail11 = do_get_file("../../../data/bugmail11");
+
+// main test
+
+// tag used with test messages
+var tag1 = "istag";
+
+function run_test() {
+ do_test_pending();
+ copyFileMessageInLocalFolder(bugmail11, 0, tag1, null, test_keywords);
+}
+
+function test_keywords(aMessageHeaderKeys, aStatus) {
+ let headerKeys = aMessageHeaderKeys;
+ Assert.notEqual(headerKeys, null);
+ let copiedMessage = localAccountUtils.inboxFolder.GetMessageHeader(
+ headerKeys[0]
+ );
+ Assert.equal(copiedMessage.getStringProperty("keywords"), tag1);
+ do_test_finished();
+}
diff --git a/comm/mailnews/local/test/unit/test_msgIDParsing.js b/comm/mailnews/local/test/unit/test_msgIDParsing.js
new file mode 100644
index 0000000000..39d435cb1f
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_msgIDParsing.js
@@ -0,0 +1,24 @@
+/*
+ * Test bug 676916 - nsParseMailbox parses multi-line message-id header incorrectly
+ */
+
+var headers =
+ "from: alice@t1.example.com\r\n" +
+ "to: bob@t2.example.net\r\n" +
+ "message-id: \r\n <abcmessageid>\r\n";
+
+function testMsgID() {
+ localAccountUtils.inboxFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ localAccountUtils.inboxFolder.addMessage(
+ "From \r\n" + headers + "\r\nhello\r\n"
+ );
+ let msgHdr = localAccountUtils.inboxFolder.firstNewMessage;
+ Assert.equal(msgHdr.messageId, "abcmessageid");
+}
+
+function run_test() {
+ for (let storeID of localAccountUtils.pluggableStores) {
+ localAccountUtils.loadLocalMailAccount(storeID);
+ testMsgID();
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_noTop.js b/comm/mailnews/local/test/unit/test_noTop.js
new file mode 100644
index 0000000000..a30a03c060
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_noTop.js
@@ -0,0 +1,64 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/**
+ * A handler with no TOP support.
+ */
+class NoTopHandler extends POP3_RFC1939_handler {
+ TOP() {
+ return this.onError("TOP");
+ }
+}
+
+let daemon = new Pop3Daemon();
+let server = new nsMailServer(d => {
+ let handler = new NoTopHandler(d);
+ return handler;
+}, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Inject a message to the server and do a GetNewMail for the incomingServer.
+ *
+ * @param {nsIPop3IncomingServer} incomingServer
+ */
+async function getNewMail(incomingServer) {
+ daemon.setMessages(["message1.eml"]);
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ return urlListener.promise;
+}
+
+/**
+ * Test TOP is sent even if not advertised, and fallback to RETR after failed.
+ */
+add_task(async function testNoTop() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.headersOnly = true;
+ await getNewMail(incomingServer);
+ do_check_transaction(server.playTransaction(), [
+ "CAPA",
+ "USER fred",
+ "PASS wilma",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "TOP 1 0",
+ "RETR 1",
+ "DELE 1",
+ ]);
+});
diff --git a/comm/mailnews/local/test/unit/test_noUidl.js b/comm/mailnews/local/test/unit/test_noUidl.js
new file mode 100644
index 0000000000..a3f8de4725
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_noUidl.js
@@ -0,0 +1,89 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/**
+ * A handler with no UIDL support.
+ */
+class NoUidlHandler extends POP3_RFC1939_handler {
+ UIDL() {
+ return this.onError("UIDL");
+ }
+}
+
+let daemon = new Pop3Daemon();
+let server = new nsMailServer(d => {
+ let handler = new NoUidlHandler(d);
+ return handler;
+}, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Inject a message to the server and do a GetNewMail for the incomingServer.
+ *
+ * @param {nsIPop3IncomingServer} incomingServer
+ */
+async function getNewMail(incomingServer) {
+ daemon.setMessages(["message1.eml"]);
+
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ // Now get the mail.
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ return urlListener.promise;
+}
+
+/**
+ * Test that RETR and DELE are correctly sent even if UIDL is not supported.
+ */
+add_task(async function testNoUidl() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ await getNewMail(incomingServer);
+ do_check_transaction(server.playTransaction(), [
+ "CAPA",
+ "USER fred",
+ "PASS wilma",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ]);
+});
+
+/**
+ * Test that connection is aborted if trying to use headersOnly when UIDL is
+ * unsupported.
+ */
+add_task(async function testNoUidlHeadersOnly() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.headersOnly = true;
+ await Assert.rejects(
+ getNewMail(incomingServer),
+ e => e == Cr.NS_ERROR_FAILURE
+ );
+});
+
+/**
+ * Test that connection is aborted if trying to use leaveMessagesOnServer when
+ * UIDL is unsupported.
+ */
+add_task(async function testNoUidlLeaveMessagesOnServer() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.leaveMessagesOnServer = true;
+ await Assert.rejects(
+ getNewMail(incomingServer),
+ e => e == Cr.NS_ERROR_FAILURE
+ );
+});
diff --git a/comm/mailnews/local/test/unit/test_nsIMsgLocalMailFolder.js b/comm/mailnews/local/test/unit/test_nsIMsgLocalMailFolder.js
new file mode 100644
index 0000000000..b0b14ecf31
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_nsIMsgLocalMailFolder.js
@@ -0,0 +1,321 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for local folder functions.
+ */
+
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+/**
+ * Bug 66763
+ * Test deletion of a folder with a name already existing in Trash.
+ */
+function subtest_folder_deletion(root) {
+ // Now we only have <root> and some default subfolders, like Trash.
+ let trash = root.getChildNamed("Trash");
+ Assert.ok(!trash.hasSubFolders);
+
+ // Create new "folder" in root.
+ let folder = root.createLocalSubfolder("folder");
+ let path = folder.filePath;
+ Assert.ok(path.exists());
+
+ // Delete "folder" into Trash.
+ folder.deleteSelf(null);
+ Assert.ok(!path.exists());
+ Assert.equal(trash.numSubFolders, 1);
+ trash.getChildNamed("folder");
+
+ // Create another "folder" in root.
+ folder = root.createLocalSubfolder("folder");
+ // Delete "folder" into Trash again.
+ folder.deleteSelf(null);
+ Assert.equal(trash.numSubFolders, 2);
+ // The folder should be automatically renamed as the same name already is in Trash.
+ trash.getChildNamed("folder(2)");
+
+ // Create yet another "folder" in root.
+ folder = root.createLocalSubfolder("folder");
+
+ // But now with another subfolder
+ folder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("subfolder");
+
+ // Delete folder into Trash again
+ folder.deleteSelf(null);
+ Assert.equal(trash.numSubFolders, 3);
+ // The folder should be automatically renamed as the same name already is in Trash
+ // but the subfolder should be untouched.
+ let folderDeleted3 = trash.getChildNamed("folder(3)");
+ Assert.notEqual(folderDeleted3, null);
+ Assert.ok(folderDeleted3.containsChildNamed("subfolder"));
+ // Now we have <root>
+ // +--Trash
+ // +--folder
+ // +--folder(2)
+ // +--folder(3)
+ // +--subfolder
+
+ // Create another "folder(3)" in root.
+ Assert.ok(!root.containsChildNamed("folder(3)"));
+ folder = root.createLocalSubfolder("folder(3)");
+ Assert.ok(root.containsChildNamed("folder(3)"));
+ // Now try to move "folder(3)" from Trash back to root.
+ // That should fail, because the user gets a prompt about it and that does
+ // not work in xpcshell.
+ try {
+ root.copyFolderLocal(folderDeleted3, true, null, null);
+ do_throw("copyFolderLocal() should have failed here due to user prompt!");
+ } catch (e) {
+ // Catch only the expected error NS_MSG_ERROR_COPY_FOLDER_ABORTED,
+ // otherwise fail the test.
+ if (e.result != 0x8055001a) {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Test proper creation/rename/removal of folders under a Local mail account
+ */
+function subtest_folder_operations(root) {
+ // Test - num/hasSubFolders
+
+ // Get the current number of folders
+ var numSubFolders = root.numSubFolders;
+
+ var folder = root
+ .createLocalSubfolder("folder1")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ Assert.ok(root.hasSubFolders);
+ Assert.equal(root.numSubFolders, numSubFolders + 1);
+
+ Assert.ok(!folder.hasSubFolders);
+ Assert.equal(folder.numSubFolders, 0);
+
+ var folder2 = folder.createLocalSubfolder("folder2");
+
+ Assert.ok(folder.hasSubFolders);
+ Assert.equal(folder.numSubFolders, 1);
+
+ // Now we have <root>
+ // +--folder1
+ // +--folder2
+
+ // Test - getChildNamed
+
+ Assert.equal(root.getChildNamed("folder1"), folder);
+
+ // Check for non match, this should throw
+ var thrown = false;
+ try {
+ root.getChildNamed("folder2");
+ } catch (e) {
+ thrown = true;
+ }
+
+ Assert.ok(thrown);
+
+ // folder2 is a child of folder however.
+ folder2 = folder.getChildNamed("folder2");
+
+ // Test - isAncestorOf
+
+ Assert.ok(folder.isAncestorOf(folder2));
+ Assert.ok(root.isAncestorOf(folder2));
+ Assert.ok(!folder.isAncestorOf(root));
+
+ // Test - FoldersWithFlag
+
+ folder.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+ Assert.ok(folder.getFlag(Ci.nsMsgFolderFlags.CheckNew));
+ Assert.ok(!folder.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ Assert.ok(folder.getFlag(Ci.nsMsgFolderFlags.CheckNew));
+ Assert.ok(folder.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ folder.toggleFlag(Ci.nsMsgFolderFlags.CheckNew);
+ Assert.ok(!folder.getFlag(Ci.nsMsgFolderFlags.CheckNew));
+ Assert.ok(folder.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ Assert.ok(!folder.getFlag(Ci.nsMsgFolderFlags.CheckNew));
+ Assert.ok(!folder.getFlag(Ci.nsMsgFolderFlags.Offline));
+
+ folder.setFlag(Ci.nsMsgFolderFlags.Favorite);
+ folder2.setFlag(Ci.nsMsgFolderFlags.Favorite);
+ folder.setFlag(Ci.nsMsgFolderFlags.CheckNew);
+ folder2.setFlag(Ci.nsMsgFolderFlags.Offline);
+
+ Assert.equal(root.getFolderWithFlags(Ci.nsMsgFolderFlags.CheckNew), folder);
+
+ // Test - Move folders around
+
+ var folder3 = root.createLocalSubfolder("folder3");
+ var folder3Local = folder3.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ var folder1Local = folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ // put a single message in folder1.
+ let messageGenerator = new MessageGenerator();
+ let message = messageGenerator.makeMessage();
+ let hdr = folder1Local.addMessage(message.toMboxString());
+ Assert.equal(message.messageId, hdr.messageId);
+
+ folder3Local.copyFolderLocal(folder, true, null, null);
+
+ // Test - Get the new folders, make sure the old ones don't exist
+
+ var folder1Moved = folder3.getChildNamed("folder1");
+ folder1Moved.getChildNamed("folder2");
+
+ thrown = false;
+ try {
+ root.getChildNamed("folder1");
+ } catch (e) {
+ thrown = true;
+ }
+
+ Assert.ok(thrown);
+
+ if (folder.filePath.exists()) {
+ dump("shouldn't exist - folder file path " + folder.URI + "\n");
+ }
+ Assert.ok(!folder.filePath.exists());
+ if (folder2.filePath.exists()) {
+ dump("shouldn't exist - folder2 file path " + folder2.URI + "\n");
+ }
+ Assert.ok(!folder2.filePath.exists());
+
+ // make sure getting the db doesn't throw an exception
+ let db = folder1Moved.msgDatabase;
+ Assert.ok(db.summaryValid);
+
+ // Move folders back, get them
+ var rootLocal = root.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ rootLocal.copyFolderLocal(folder1Moved, true, null, null);
+ folder = root.getChildNamed("folder1");
+ folder2 = folder.getChildNamed("folder2");
+
+ // Test - Rename (test that .msf file is renamed as well)
+ folder.rename("folder1-newname", null);
+ // make sure getting the db doesn't throw an exception, and is valid
+ folder = rootLocal.getChildNamed("folder1-newname");
+ db = folder.msgDatabase;
+ Assert.ok(db.summaryValid);
+
+ folder.rename("folder1", null);
+ folder = rootLocal.getChildNamed("folder1");
+
+ // Test - propagateDelete (this tests recursiveDelete as well)
+ // The folders will be removed from disk completely, not merely to Trash.
+
+ var path1 = folder.filePath;
+ var path2 = folder2.filePath;
+ var path3 = folder3.filePath;
+
+ Assert.ok(path1.exists());
+ Assert.ok(path2.exists());
+ Assert.ok(path3.exists());
+
+ // First try deleting folder3 -- folder1 and folder2 paths should still exist
+ root.propagateDelete(folder3, true);
+
+ Assert.ok(path1.exists());
+ Assert.ok(path2.exists());
+ Assert.ok(!path3.exists());
+
+ root.propagateDelete(folder, true);
+
+ Assert.ok(!path1.exists());
+ Assert.ok(!path2.exists());
+}
+
+function test_store_rename(root) {
+ let folder1 = root
+ .createLocalSubfolder("newfolder1")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ Assert.ok(root.hasSubFolders);
+ Assert.ok(!folder1.hasSubFolders);
+ let folder2 = folder1.createLocalSubfolder("newfolder1-sub");
+ let folder3 = root
+ .createLocalSubfolder("newfolder3")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder3.createLocalSubfolder("newfolder3-sub");
+
+ Assert.ok(folder1.hasSubFolders);
+ Assert.ok(!folder2.hasSubFolders);
+ Assert.ok(folder3.hasSubFolders);
+
+ folder1.rename("folder1", null);
+ Assert.ok(root.containsChildNamed("folder1"));
+ folder1 = root.getChildNamed("folder1");
+
+ folder1.rename("newfolder1", null);
+ Assert.ok(root.containsChildNamed("newfolder1"));
+ folder1 = root.getChildNamed("newfolder1");
+ folder2 = folder1.getChildNamed("newfolder1-sub");
+
+ Assert.ok(folder1.containsChildNamed(folder2.name));
+ Assert.ok(folder2.filePath.exists());
+
+ folder3 = root.getChildNamed("newfolder3");
+ root.propagateDelete(folder3, true);
+ Assert.ok(!root.containsChildNamed("newfolder3"));
+ folder3 = root
+ .createLocalSubfolder("newfolder3")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder3.createLocalSubfolder("newfolder3-sub");
+ folder3.rename("folder3", null);
+
+ Assert.ok(root.containsChildNamed("folder3"));
+ Assert.ok(!root.containsChildNamed("newfolder3"));
+}
+
+var gPluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+
+function run_all_tests(aHostName) {
+ let server = MailServices.accounts.createIncomingServer(
+ "nobody",
+ aHostName,
+ "none"
+ );
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+
+ let root = server.rootMsgFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ subtest_folder_operations(root);
+ subtest_folder_deletion(root);
+ test_store_rename(root);
+}
+
+function run_test() {
+ let hostName = "Local Folders";
+ let index = 0;
+ while (index < gPluggableStores.length) {
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ gPluggableStores[index]
+ );
+ run_all_tests(hostName);
+ hostName += "-" + ++index;
+ }
+
+ // At this point,
+ // we should have <root>
+ // +--newfolder1
+ // +--newfolder1-subfolder
+ // +--newfolder3-anotherName
+ // +--newfolder3-sub
+ // +--folder(3)
+ // +--Trash
+ // +--folder
+ // +--folder(2)
+ // +--folder(3)
+ // +--subfolder
+}
diff --git a/comm/mailnews/local/test/unit/test_nsIMsgParseMailMsgState.js b/comm/mailnews/local/test/unit/test_nsIMsgParseMailMsgState.js
new file mode 100644
index 0000000000..a64c88f8d6
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_nsIMsgParseMailMsgState.js
@@ -0,0 +1,42 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var MSG_LINEBREAK = "\r\n";
+
+add_task(async function run_the_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ await test_parse_headers_without_crash("./data/mailformed_recipients.eml");
+ await test_parse_headers_without_crash("./data/mailformed_subject.eml");
+ await test_parse_headers_without_crash("./data/invalid_mozilla_keys.eml");
+});
+
+async function test_parse_headers_without_crash(eml) {
+ let file = do_get_file(eml);
+
+ let parser = Cc["@mozilla.org/messenger/messagestateparser;1"].createInstance(
+ Ci.nsIMsgParseMailMsgState
+ );
+
+ parser.SetMailDB(localAccountUtils.inboxFolder.getDatabaseWOReparse());
+ parser.state = Ci.nsIMsgParseMailMsgState.ParseHeadersState;
+
+ let bytes = await IOUtils.read(file.path);
+ let mailData = new TextDecoder().decode(bytes);
+ let lines = mailData.split(MSG_LINEBREAK);
+
+ for (let line = 0; line < lines.length; line++) {
+ parser.ParseAFolderLine(
+ lines[line] + MSG_LINEBREAK,
+ lines[line].length + 2
+ );
+ }
+ // Apparently getDatabaseWOReparse doesn't like being called too often
+ // in a row.
+ await PromiseTestUtils.promiseDelay(200);
+}
diff --git a/comm/mailnews/local/test/unit/test_nsIMsgPluggableStore.js b/comm/mailnews/local/test/unit/test_nsIMsgPluggableStore.js
new file mode 100644
index 0000000000..c9e7d63864
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_nsIMsgPluggableStore.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+/**
+ * nsIMsgPluggableStore interface tests
+ */
+
+function test_discoverSubFolders() {
+ let mailbox = setup_mailbox("none", create_temporary_directory());
+ mailbox.msgStore.discoverSubFolders(mailbox, true);
+}
+
+function test_sliceStream() {
+ let mailbox = setup_mailbox("none", create_temporary_directory());
+
+ let str = "Just a test string.";
+ let strStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ strStream.setData(str, str.length);
+
+ let sliced = mailbox.msgStore.sliceStream(strStream, 7, 4);
+
+ let s = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ s.init(sliced);
+
+ let chunk = s.read(1024);
+ Assert.equal(chunk, "test", "Check we got the expected subset.");
+ Assert.equal(s.available(), 0, "Check no more bytes available.");
+ Assert.equal(s.read(1024), "", "Check read() returns EOF.");
+}
+
+// Return a wrapper which sets the store type before running fn().
+function withStore(store, fn) {
+ return () => {
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", store);
+ fn();
+ };
+}
+
+const pluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+
+for (let store of pluggableStores) {
+ add_task(withStore(store, test_discoverSubFolders));
+ add_task(withStore(store, test_sliceStream));
+}
diff --git a/comm/mailnews/local/test/unit/test_over2GBMailboxes.js b/comm/mailnews/local/test/unit/test_over2GBMailboxes.js
new file mode 100644
index 0000000000..fe7976b0fe
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_over2GBMailboxes.js
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/* Test of accessing over 2 GiB local folder. */
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Find hdr for message whose offset is over 2 GiB.
+function findHugeMessageHdr(folder) {
+ //getMessageHdr() {
+ for (let header of folder.msgDatabase.enumerateMessages()) {
+ if (header.messageOffset >= 0x80000000) {
+ return header;
+ }
+ }
+
+ do_throw("Over 2 GiB msgkey was not found!");
+ return null; // This won't ever happen, but we're keeping the linter happy.
+}
+
+let gInboxFile;
+let gInbox;
+let gSmallMsgFile = do_get_file("../../../data/bugmail10");
+
+add_setup(async function () {
+ // Make sure we're using mbox.
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+ );
+
+ localAccountUtils.loadLocalMailAccount();
+
+ gInbox = localAccountUtils.inboxFolder;
+ gInboxFile = gInbox.filePath;
+
+ let neededFreeSpace = 0x100000000;
+ let freeDiskSpace = gInboxFile.diskSpaceAvailable;
+ info("Free disk space = " + mailTestUtils.toMiBString(freeDiskSpace));
+ if (freeDiskSpace < neededFreeSpace) {
+ throw new Error(
+ "This test needs " +
+ mailTestUtils.toMiBString(neededFreeSpace) +
+ " free space to run. Aborting."
+ );
+ }
+});
+
+// Extend mbox file to over 2 GiB.
+add_task(async function extendPast2GiB() {
+ let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream)
+ .QueryInterface(Ci.nsISeekableStream);
+ // Open in write-only mode, no truncate.
+ outputStream.init(gInboxFile, 0x02, -1, 0);
+ // seek past 2GB.
+ outputStream.seek(0, 0x80000010);
+ // Write a "space" character.
+ outputStream.write(" ", 1);
+ outputStream.close();
+});
+
+// Copy another (normal sized) message into the local folder.
+// This message should be past the 2GiB position.
+add_task(async function appendSmallMessage() {
+ // Remember initial mbox file size.
+ let initialInboxSize = gInbox.filePath.fileSize;
+ info(`Local inbox size (before copyFileMessage()) = ${initialInboxSize}`);
+
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ gSmallMsgFile,
+ gInbox,
+ null /* msgToReplace*/,
+ false /* isDraftOrTemplate */,
+ 0 /* message flags */,
+ "" /* keywords */,
+ copyListener,
+ null /* window */
+ );
+ await copyListener.promise;
+
+ // Make sure inbox file grew (i.e., we were not writing over data).
+ let localInboxSize = gInbox.filePath.fileSize;
+ info(
+ "Local inbox size (after copyFileMessageInLocalFolder()) = " +
+ localInboxSize
+ );
+ Assert.greater(localInboxSize, initialInboxSize);
+});
+
+// Copy the huge message into a subfolder.
+add_task(async function copyHugeMessage() {
+ let trash =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed("Trash");
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gInbox,
+ [findHugeMessageHdr(gInbox)],
+ trash /* destFolder */,
+ false,
+ copyListener,
+ null,
+ false
+ );
+ await copyListener.promise;
+});
+
+// Read out the smaller message beyond the 2 GiB offset and make sure
+// it matches what we expect.
+add_task(async function verifySmallMessage() {
+ let msghdr = findHugeMessageHdr(gInbox);
+ let msgURI = msghdr.folder.getUriForMsg(msghdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ msgServ.streamMessage(msgURI, streamListener, null, null, false, "", true);
+ let got = await streamListener.promise;
+
+ let expected = await IOUtils.readUTF8(gSmallMsgFile.path);
+ Assert.equal(got, expected);
+});
+
+add_task(async function cleanup() {
+ // Free up disk space - if you want to look at the file after running
+ // this test, comment out this line.
+ gInbox.filePath.remove(false);
+});
diff --git a/comm/mailnews/local/test/unit/test_over4GBMailboxes.js b/comm/mailnews/local/test/unit/test_over4GBMailboxes.js
new file mode 100644
index 0000000000..befd72fca9
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_over4GBMailboxes.js
@@ -0,0 +1,640 @@
+/* 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/. */
+
+/*
+ * Test to ensure that operations around the 4GiB folder size boundary work correctly.
+ * This test only works for mbox format mail folders.
+ * Some of the tests will be removed when support for over 4GiB folders is enabled by default.
+ * The test functions are executed in this order:
+ * - run_test
+ * - ParseListener_run_test
+ * - downloadUnder4GiB
+ * - downloadOver4GiB_fail
+ * - downloadOver4GiB_success
+ * - downloadOver4GiB_success_check
+ * - copyIntoOver4GiB_fail
+ * - copyIntoOver4GiB_fail_check
+ * - copyIntoOver4GiB_success
+ * - copyIntoOver4GiB_success_check1
+ * - copyIntoOver4GiB_success_check2
+ * - compactOver4GiB
+ * - CompactListener_compactOver4GiB
+ * - compactUnder4GiB
+ * - CompactListener_compactUnder4GiB
+ */
+
+// Need to do this before loading POP3Pump.js
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/POP3pump.js");
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// If we're running out of memory parsing the folder, lowering the
+// block size might help, though it will slow the test down and consume
+// more disk space.
+var kSparseBlockSize = 102400000;
+var kSizeLimit = 0x100000000; // 4GiB
+var kNearLimit = kSizeLimit - 0x1000000; // -16MiB
+
+var gInboxFile = null; // The mbox file storing the Inbox folder.
+var gInboxSize = 0; // The size of the Inbox folder.
+var gInbox; // The nsIMsgFolder object of the Inbox folder in Local Folders.
+var gExpectedNewMessages = 0; // The number of messages pushed manually into the mbox file.
+
+var alertIsPending = true;
+var alertResolve;
+var alertPromise = new Promise(resolve => {
+ alertResolve = resolve;
+}).finally(() => {
+ alertIsPending = false;
+});
+function resetAlertPromise() {
+ alertIsPending = true;
+ alertPromise = new Promise(resolve => {
+ alertResolve = resolve;
+ }).finally(() => {
+ alertIsPending = false;
+ });
+}
+
+add_setup(async function () {
+ registerAlertTestUtils();
+
+ localAccountUtils.loadLocalMailAccount();
+
+ allow4GBFolders(false);
+
+ gInbox = localAccountUtils.inboxFolder;
+ gInboxFile = gInbox.filePath;
+
+ let neededFreeSpace = kSizeLimit + 0x10000000; // +256MiB
+ // On Windows, check whether the drive is NTFS. If it is, mark the file as
+ // sparse. If it isn't, then bail out now, because in all probability it is
+ // FAT32, which doesn't support file sizes greater than 4 GiB.
+ if (
+ "@mozilla.org/windows-registry-key;1" in Cc &&
+ mailTestUtils.get_file_system(gInboxFile) != "NTFS"
+ ) {
+ throw new Error("On Windows, this test only works on NTFS volumes.\n");
+ }
+
+ let freeDiskSpace = gInboxFile.diskSpaceAvailable;
+ info("Free disk space = " + mailTestUtils.toMiBString(freeDiskSpace));
+ if (freeDiskSpace < neededFreeSpace) {
+ throw new Error(
+ "This test needs " +
+ mailTestUtils.toMiBString(neededFreeSpace) +
+ " free space to run. Aborting."
+ );
+ }
+
+ MailServices.mailSession.AddFolderListener(
+ FListener,
+ Ci.nsIFolderListener.all
+ );
+
+ // Grow inbox to a size near the max limit.
+ gExpectedNewMessages = growInbox(kNearLimit);
+
+ // Force the db closed, so that getDatabaseWithReparse will notice
+ // that it's out of date.
+ gInbox.msgDatabase.forceClosed();
+ gInbox.msgDatabase = null;
+ let parseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ try {
+ gInbox.getDatabaseWithReparse(parseUrlListener, gDummyMsgWindow);
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ await parseUrlListener.promise;
+ // Check: reparse successful.
+ Assert.notEqual(gInbox.msgDatabase, null);
+ Assert.ok(gInbox.msgDatabase.summaryValid);
+ // Bug 813459
+ // Check if the onFolderIntPropertyChanged folder listener hook can return
+ // values below 2^32 for properties which are not 64 bits long.
+ Assert.equal(FListener.msgsHistory(0), gExpectedNewMessages);
+ Assert.equal(FListener.msgsHistory(0), gInbox.getTotalMessages(false));
+ Assert.equal(FListener.sizeHistory(0), gInbox.sizeOnDisk);
+});
+
+/**
+ * Check we can download new mail when we are near 4GiB limit but do not cross it.
+ */
+add_task(async function downloadUnder4GiB() {
+ // Check fake POP3 server is ready.
+ Assert.notEqual(gPOP3Pump.fakeServer, null);
+
+ // Download a file that still fits into the limit.
+ let bigFile = do_get_file("../../../data/mime-torture");
+ Assert.ok(bigFile.fileSize >= 1024 * 1024);
+ Assert.ok(bigFile.fileSize <= 1024 * 1024 * 2);
+
+ gPOP3Pump.files = ["../../../data/mime-torture"];
+ let pop3Resolve;
+ let pop3OnDonePromise = new Promise(resolve => {
+ pop3Resolve = resolve;
+ });
+ gPOP3Pump.onDone = pop3Resolve;
+ // It must succeed.
+ gPOP3Pump.run(Cr.NS_OK);
+ await pop3OnDonePromise;
+});
+
+/**
+ * Bug 640371
+ * Check we will not cross the 4GiB limit when downloading new mail.
+ */
+add_task(async function downloadOver4GiB_fail() {
+ let localInboxSize = gInboxFile.clone().fileSize;
+ Assert.ok(localInboxSize >= kNearLimit);
+ Assert.ok(localInboxSize < kSizeLimit);
+ Assert.equal(gInbox.sizeOnDisk, localInboxSize);
+ Assert.ok(gInbox.msgDatabase.summaryValid);
+ // The big file is between 1 and 2 MiB. Append it 16 times to attempt to cross the 4GiB limit.
+ gPOP3Pump.files = [
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ ];
+ let pop3Resolve;
+ let pop3OnDonePromise = new Promise(resolve => {
+ pop3Resolve = resolve;
+ });
+ gPOP3Pump.onDone = pop3Resolve;
+ // The download must fail.
+ gPOP3Pump.run(Cr.NS_ERROR_FAILURE);
+ await pop3OnDonePromise;
+});
+
+/**
+ * Bug 789679
+ * Check we can cross the 4GiB limit when downloading new mail.
+ */
+add_task(async function downloadOver4GiB_success_check() {
+ allow4GBFolders(true);
+ // Grow inbox to size greater than the max limit (+16 MiB).
+ gExpectedNewMessages = 16;
+ // We are in the .onDone() callback of the previous run of gPOP3Pump
+ // so we need a new POP3Pump so that internal variables of the previous
+ // one don't get confused.
+ gPOP3Pump = new POP3Pump();
+ gPOP3Pump._incomingServer = gPOP3Pump._createPop3ServerAndLocalFolders();
+ gPOP3Pump.files = [
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ "../../../data/mime-torture",
+ ];
+ let pop3Resolve;
+ let pop3OnDonePromise = new Promise(resolve => {
+ pop3Resolve = resolve;
+ });
+ gPOP3Pump.onDone = pop3Resolve;
+ // The download must not fail.
+ gPOP3Pump.run(Cr.NS_OK);
+ await pop3OnDonePromise;
+
+ /**
+ * Bug 608449
+ * Check we can parse a folder if it is above 4GiB.
+ */
+ let localInboxSize = gInboxFile.clone().fileSize;
+ info(
+ "Local inbox size (after downloadOver4GiB_success) = " +
+ localInboxSize +
+ "\n"
+ );
+ Assert.ok(localInboxSize > kSizeLimit);
+ Assert.ok(gInbox.msgDatabase.summaryValid);
+
+ // Bug 789679
+ // Check if the public SizeOnDisk method can return sizes above 4GB.
+ Assert.equal(gInbox.sizeOnDisk, localInboxSize);
+
+ // Bug 813459
+ // Check if the onFolderIntPropertyChanged folder listener hook can return
+ // values above 2^32 for properties where it is relevant.
+ Assert.equal(FListener.sizeHistory(0), gInbox.sizeOnDisk);
+ Assert.ok(FListener.sizeHistory(1) < FListener.sizeHistory(0));
+ Assert.equal(
+ FListener.msgsHistory(0),
+ FListener.msgsHistory(16) + gExpectedNewMessages
+ );
+ Assert.equal(gInbox.expungedBytes, 0);
+
+ // Bug 1183490
+ // Check that the message keys are below 4GB (thus no offset),
+ // actually just incrementing by 1 for each message.
+ let key = 0;
+ for (let hdr of gInbox.messages) {
+ key++;
+ Assert.equal(hdr.messageKey, key);
+ }
+});
+
+/**
+ * Bug 598104
+ * Check that copy operation does not allow to grow a local folder above 4 GiB.
+ */
+add_task(async function copyIntoOver4GiB_fail_check() {
+ allow4GBFolders(false);
+ // Save initial file size.
+ let localInboxSize = gInboxFile.clone().fileSize;
+ info("Local inbox size (before copyFileMessage) = " + localInboxSize);
+
+ // Use copyFileMessage to (try to) append another message
+ // to local inbox.
+ let file = do_get_file("../../../data/mime-torture");
+
+ // Set up local folders
+ localAccountUtils.loadLocalMailAccount();
+
+ let copiedMessageHeaderKeys = []; // Accumulated MsgHdrKeys for listener.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ copiedMessageHeaderKeys.push(aKey);
+ },
+ });
+ // Copy a message into the local folder
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ gDummyMsgWindow
+ );
+ await Assert.rejects(
+ copyListener.promise,
+ reason => {
+ return reason === Cr.NS_ERROR_FAILURE;
+ },
+ "The local folder is not above 4GiB"
+ );
+
+ Assert.equal(copiedMessageHeaderKeys.length, 0);
+ let alertText = await alertPromise;
+ Assert.ok(
+ alertText.startsWith(
+ "The folder Inbox on Local Folders is full, and can't hold any more messages."
+ )
+ );
+
+ // Make sure inbox file did not grow (i.e., no data were appended).
+ let newLocalInboxSize = gInboxFile.clone().fileSize;
+ info("Local inbox size (after copyFileMessage()) = " + newLocalInboxSize);
+});
+
+/**
+ * Bug 789679
+ * Check that copy operation does allow to grow a local folder above 4 GiB.
+ */
+add_task(async function copyIntoOver4GiB_success_check1() {
+ allow4GBFolders(true);
+ // Append 2 new 2MB messages to the folder.
+ gExpectedNewMessages = 2;
+
+ // Reset the Promise for alertTestUtils.js.
+ // This message will be preserved in CompactUnder4GB.
+ resetAlertPromise();
+ let file = do_get_file("../../../data/mime-torture");
+ let copiedMessageHeaderKeys = []; // Accumulated MsgHdrKeys for listener.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ copiedMessageHeaderKeys.push(aKey);
+ },
+ });
+ // Copy a message into the local folder
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ gDummyMsgWindow
+ );
+
+ await copyListener.promise;
+ Assert.equal(copiedMessageHeaderKeys[0], 60);
+ // An alert shouldn't be triggered after our reset.
+ Assert.ok(alertIsPending);
+});
+
+add_task(async function copyIntoOver4GiB_success_check2() {
+ // This message will be removed in compactOver4GB.
+ let file = do_get_file("../../../data/mime-torture");
+ let copiedMessageHeaderKeys = []; // Accumulated MsgHdrKeys for listener.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener({
+ SetMessageKey(aKey) {
+ copiedMessageHeaderKeys.push(aKey);
+ },
+ });
+ // Copy a message into the local folder.
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ gDummyMsgWindow
+ );
+
+ await copyListener.promise;
+ Assert.equal(copiedMessageHeaderKeys[0], 61);
+ // An alert shouldn't be triggered so far.
+ Assert.ok(alertIsPending);
+
+ Assert.equal(
+ FListener.msgsHistory(0),
+ FListener.msgsHistory(2) + gExpectedNewMessages
+ );
+});
+
+/**
+ * Bug 794303
+ * Check we can compact a folder that stays above 4 GiB after compact.
+ */
+add_task(async function compactOver4GiB() {
+ gInboxSize = gInboxFile.clone().fileSize;
+ Assert.ok(gInboxSize > kSizeLimit);
+ Assert.equal(gInbox.expungedBytes, 0);
+ // Delete the last small message at folder end.
+ let doomed = [...gInbox.messages].slice(-1);
+ let sizeToExpunge = 0;
+ for (let header of doomed) {
+ sizeToExpunge = header.messageSize;
+ }
+ let deleteListener = new PromiseTestUtils.PromiseCopyListener();
+ gInbox.deleteMessages(doomed, null, true, false, deleteListener, false);
+ await deleteListener.promise;
+ Assert.equal(gInbox.expungedBytes, sizeToExpunge);
+
+ /* Unfortunately, the compaction now would kill the sparse markings in the file
+ * so it will really take 4GiB of space in the filesystem and may be slow. */
+ // Note: compact() will also add 'X-Mozilla-Status' and 'X-Mozilla-Status2'
+ // lines to message(s).
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ gInbox.compact(urlListener, null);
+ await urlListener.promise;
+ Assert.ok(gInbox.msgDatabase.summaryValid);
+ // Check that folder size is still above max limit ...
+ let localInboxSize = gInbox.filePath.clone().fileSize;
+ info("Local inbox size (after compact 1) = " + localInboxSize);
+ Assert.ok(localInboxSize > kSizeLimit);
+ // ... but it got smaller by removing 1 message.
+ Assert.ok(gInboxSize > localInboxSize);
+ Assert.equal(gInbox.sizeOnDisk, localInboxSize);
+});
+
+/**
+ * Bug 608449
+ * Check we can compact a folder to get it under 4 GiB.
+ */
+add_task(async function compactUnder4GiB() {
+ // The folder is still above 4GB.
+ Assert.ok(gInboxFile.clone().fileSize > kSizeLimit);
+ let folderSize = gInbox.sizeOnDisk;
+ let totalMsgs = gInbox.getTotalMessages(false);
+ // Let's close the database and re-open the folder (hopefully dumping memory caches)
+ // and re-reading the values from disk (msg database). That is to test if
+ // the values were properly serialized to the database.
+ gInbox.ForceDBClosed();
+ gInbox.msgDatabase = null;
+ gInbox.getDatabaseWOReparse();
+
+ Assert.equal(gInbox.sizeOnDisk, folderSize);
+ Assert.equal(gInbox.getTotalMessages(false), totalMsgs);
+
+ // Very last header in folder is retained,
+ // but all other preceding headers are marked as deleted.
+ let doomed = [...gInbox.messages].slice(0, -1);
+ let sizeToExpunge = gInbox.expungedBytes; // If compact in compactOver4GB was skipped, this is not 0.
+ for (let header of doomed) {
+ sizeToExpunge += header.messageSize;
+ }
+ let deleteListener = new PromiseTestUtils.PromiseCopyListener();
+ gInbox.deleteMessages(doomed, null, true, false, deleteListener, false);
+ await deleteListener.promise;
+
+ // Bug 894012: size of messages to expunge is now higher than 4GB.
+ // Only the small 1MiB message remains.
+ Assert.equal(gInbox.expungedBytes, sizeToExpunge);
+ Assert.ok(sizeToExpunge > kSizeLimit);
+
+ // Note: compact() will also add 'X-Mozilla-Status' and 'X-Mozilla-Status2'
+ // lines to message(s).
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ gInbox.compact(urlListener, null);
+ await urlListener.promise;
+ // Check: message successfully copied.
+ Assert.ok(gInbox.msgDatabase.summaryValid);
+
+ // Check that folder size isn't much bigger than our sparse block size, ...
+ let localInboxSize = gInbox.filePath.clone().fileSize;
+ info("Local inbox size (after compact 2) = " + localInboxSize);
+ Assert.equal(gInbox.sizeOnDisk, localInboxSize);
+ Assert.ok(localInboxSize < kSparseBlockSize + 1000);
+ // ... i.e., that we just have one message.
+ Assert.equal(gInbox.getTotalMessages(false), 1);
+ Assert.equal(FListener.sizeHistory(0), gInbox.sizeOnDisk);
+ Assert.equal(FListener.msgsHistory(0), 1);
+
+ // The message has its key preserved in compact.
+ Assert.equal([...gInbox.messages][0].messageKey, 60);
+});
+
+add_task(function endTest() {
+ MailServices.mailSession.RemoveFolderListener(FListener);
+ // Free up disk space - if you want to look at the file after running
+ // this test, comment out this line.
+ gInbox.filePath.remove(false);
+ Services.prefs.clearUserPref("mailnews.allowMboxOver4GB");
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+// This alert() is triggered when file size becomes close (enough) to or
+// exceeds 4 GiB.
+// See hardcoded value in nsMsgBrkMBoxStore::HasSpaceAvailable().
+function alertPS(parent, aDialogTitle, aText) {
+ // See "/*/locales/en-US/chrome/*/messenger.properties > mailboxTooLarge".
+ alertResolve(aText);
+}
+
+// A stub nsIMsgFolderListener that only listens to changes on Inbox and stores
+// the seen values for interesting folder properties so we can later test them.
+var FListener = {
+ folderSize: [-1], // an array of seen values of "FolderSize"
+ totalMsgs: [-1], // an array of seen values of "TotalMessages"
+
+ // Returns the value that is stored 'aBack' entries from the last one in the history.
+ sizeHistory(aBack) {
+ return this.folderSize[this.folderSize.length - 1 - aBack];
+ },
+ msgsHistory(aBack) {
+ return this.totalMsgs[this.totalMsgs.length - 1 - aBack];
+ },
+
+ onFolderAdded: function act_add(parentFolder, child) {},
+ onMessageAdded: function act_add(parentFolder, msg) {},
+ onFolderRemoved: function act_remove(parentFolder, child) {},
+ onMessageRemoved: function act_remove(parentFolder, msg) {},
+
+ onFolderPropertyChanged(aItem, aProperty, aOld, aNew) {},
+ onFolderIntPropertyChanged(aItem, aProperty, aOld, aNew) {
+ if (aItem === gInbox) {
+ info(
+ "Property change on folder Inbox:" +
+ aProperty +
+ "=" +
+ aOld +
+ "->" +
+ aNew +
+ "\n"
+ );
+ if (aProperty == "FolderSize") {
+ this.folderSize.push(aNew);
+ } else if (aProperty == "TotalMessages") {
+ this.totalMsgs.push(aNew);
+ }
+ }
+ },
+ onFolderBoolPropertyChanged(aItem, aProperty, aOld, aNew) {},
+ onFolderUnicharPropertyChanged(aItem, aProperty, aOld, aNew) {},
+ onFolderPropertyFlagChanged(aItem, aProperty, aOld, aNew) {},
+ onFolderEvent(aFolder, aEvent) {},
+};
+
+/**
+ * Allow folders to grow over 4GB.
+ */
+function allow4GBFolders(aOn) {
+ Services.prefs.setBoolPref("mailnews.allowMboxOver4GB", aOn);
+}
+
+/**
+ * Grow local inbox folder to the wanted size using direct appending
+ * to the underlying file. The folder is filled with copies of a dummy
+ * message with kSparseBlockSize bytes in size.
+ * The file must be reparsed (getDatabaseWithReparse) after it is artificially
+ * enlarged here.
+ * The file is marked as sparse in the filesystem so that it does not
+ * really take 4GiB and working with it is faster.
+ *
+ * @returns The number of messages created in the folder file.
+ */
+function growInbox(aWantedSize) {
+ let msgsAdded = 0;
+ // Put a single message in the Inbox.
+ let messageGenerator = new MessageGenerator();
+ let message = messageGenerator.makeMessage();
+
+ // Refresh 'gInboxFile'.
+ gInboxFile = gInbox.filePath;
+ let localSize = 0;
+
+ let mboxString = message.toMboxString();
+ let plugStore = gInbox.msgStore;
+ // Grow local inbox to our wished size that is below the max limit.
+ do {
+ let sparseStart = gInboxFile.clone().fileSize + mboxString.length;
+ let nextOffset = Math.min(sparseStart + kSparseBlockSize, aWantedSize - 2);
+ if (aWantedSize - (nextOffset + 2) < mboxString.length + 2) {
+ nextOffset = aWantedSize - 2;
+ }
+
+ // Get stream to write a new message.
+ let reusable = {};
+ let newMsgHdr = {};
+ let outputStream = plugStore
+ .getNewMsgOutputStream(gInbox, newMsgHdr, reusable)
+ .QueryInterface(Ci.nsISeekableStream);
+ // Write message header.
+ outputStream.write(mboxString, mboxString.length);
+ outputStream.close();
+
+ // "Add" a new (empty) sparse block at the end of the file.
+ if (nextOffset - sparseStart == kSparseBlockSize) {
+ mailTestUtils.mark_file_region_sparse(
+ gInboxFile,
+ sparseStart,
+ kSparseBlockSize
+ );
+ }
+
+ // Append message terminator.
+ outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream)
+ .QueryInterface(Ci.nsISeekableStream);
+ // Open in write-only mode, no truncate.
+ outputStream.init(gInboxFile, 0x02, 0o600, 0);
+
+ // Skip to the wished end of the message.
+ outputStream.seek(0, nextOffset);
+ // Add a CR+LF to terminate the message.
+ outputStream.write("\r\n", 2);
+ outputStream.close();
+ msgsAdded++;
+
+ // Refresh 'gInboxFile'.
+ gInboxFile = gInbox.filePath;
+ localSize = gInboxFile.clone().fileSize;
+ } while (localSize < aWantedSize);
+
+ Assert.equal(gInboxFile.clone().fileSize, aWantedSize);
+ info(
+ "Local inbox size = " +
+ localSize +
+ "bytes = " +
+ mailTestUtils.toMiBString(localSize)
+ );
+ Assert.equal(localSize, aWantedSize);
+ return msgsAdded;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3AuthMethods.js b/comm/mailnews/local/test/unit/test_pop3AuthMethods.js
new file mode 100644
index 0000000000..3b45287f23
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3AuthMethods.js
@@ -0,0 +1,201 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Login tests for POP3
+ *
+ * Test code <copied from="test_pop3GetNewMail.js">
+ */
+
+var server;
+var handler;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "Cleartext password, with server only supporting USER/PASS",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: [],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "USER fred", "PASS wilma", "STAT"],
+ },
+ {
+ // Just to make sure we clear the auth flags and re-issue "AUTH"
+ title:
+ "Second time Cleartext password, with server only supporting USER/PASS",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: [],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "USER fred", "PASS wilma", "STAT"],
+ },
+ {
+ title:
+ "Cleartext password, with server supporting AUTH PLAIN, LOGIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH PLAIN", "STAT"],
+ },
+ {
+ title: "Cleartext password, with server supporting only AUTH LOGIN",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+ serverAuthMethods: ["LOGIN"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH LOGIN", "STAT"],
+ },
+ {
+ title: "Encrypted password, with server supporting PLAIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH CRAM-MD5", "STAT"],
+ },
+ {
+ title: "Encrypted password, try CRAM even if if not advertised",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["PLAIN", "LOGIN"],
+ expectSuccess: false,
+ transaction: ["AUTH", "CAPA", "AUTH CRAM-MD5"],
+ },
+ {
+ title: "Any secure method, with server supporting AUTH PLAIN and CRAM",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH CRAM-MD5", "STAT"],
+ },
+ {
+ title:
+ "Any secure method, with server only supporting AUTH PLAIN and LOGIN (must fail)",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["PLAIN"],
+ expectSuccess: false,
+ transaction: ["AUTH", "CAPA"],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ if (thisTest.expectSuccess) {
+ Assert.equal(result, 0);
+ } else {
+ Assert.notEqual(result, 0);
+ }
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, thisTest.transaction);
+
+ do_timeout(0, checkBusy);
+ } catch (e) {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ test = thisTest.title;
+ dump("NEXT test: " + thisTest.title + "\n");
+
+ handler.kAuthSchemes = thisTest.serverAuthMethods;
+
+ // Mailnews caches server capabilities, so try to reset it
+ // (alternative would be .pop3CapabilityFlags = 0, but this is safer)
+ deletePop3Server();
+ incomingServer = createPop3Server();
+
+ let msgServer = incomingServer;
+ msgServer.QueryInterface(Ci.nsIMsgIncomingServer);
+ msgServer.authMethod = thisTest.clientAuthMethod;
+
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
+
+// <copied from="head_maillocal.js::createPop3ServerAndLocalFolders()">
+function createPop3Server() {
+ let incoming = MailServices.accounts.createIncomingServer(
+ "fred",
+ "localhost",
+ "pop3"
+ );
+ incoming.port = server.port;
+ incoming.password = "wilma";
+ return incoming;
+}
+// </copied>
+
+function deletePop3Server() {
+ if (!incomingServer) {
+ return;
+ }
+ MailServices.accounts.removeIncomingServer(incomingServer, true);
+ incomingServer = null;
+}
+
+function run_test() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ let ssd = setupServerDaemon();
+ server = ssd[1];
+ handler = ssd[2];
+ server.start();
+
+ // incomingServer = createPop3ServerAndLocalFolders();
+ localAccountUtils.loadLocalMailAccount();
+
+ do_test_pending();
+
+ testNext();
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3Client.js b/comm/mailnews/local/test/unit/test_pop3Client.js
new file mode 100644
index 0000000000..19e71a3e78
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Client.js
@@ -0,0 +1,145 @@
+/* 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/. */
+
+let [daemon, server, handler] = setupServerDaemon();
+handler.kCapabilities = ["uidl", "top"]; // CAPA response is case-insensitive.
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Test when alwaysSTARTTLS is set, but the server doesn't support STARTTLS,
+ * should abort after CAPA response.
+ */
+add_task(async function testSTARTTLS() {
+ server.resetTest();
+
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ // Set to always use STARTTLS.
+ incomingServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+ let urlListener = {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl(url, result) {
+ try {
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, ["AUTH", "CAPA"]);
+ Assert.equal(result, Cr.NS_ERROR_FAILURE);
+ } catch (e) {
+ } finally {
+ MailServices.accounts.removeIncomingServer(incomingServer, false);
+ do_test_finished();
+ }
+ },
+ };
+
+ // Now get the mail.
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+
+ do_test_pending();
+});
+
+/**
+ * Test that depending on user prefs and message size, TOP or RETR should be used.
+ *
+ * @param {nsIMsgIncomingServer} incomingServer - A server instance.
+ * @param {string[]} transaction - The commands sent to the server.
+ */
+async function testTopOrRetr(incomingServer, transaction) {
+ server.resetTest();
+ // Any message file larger than 50KB is good for this test.
+ daemon.setMessages(["mailformed_subject.eml"]);
+
+ let urlListener = {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl(url, result) {
+ try {
+ do_check_transaction(server.playTransaction(), transaction);
+ Assert.equal(result, 0);
+ } catch (e) {
+ } finally {
+ MailServices.accounts.removeIncomingServer(incomingServer, false);
+ do_test_finished();
+ }
+ },
+ };
+
+ // Now get the mail.
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+
+ do_test_pending();
+}
+
+/**
+ * Turn off server.limitOfflineMessageSize, test RETR is used.
+ */
+add_task(async function testNoOfflineMessageSizeLimit() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.limitOfflineMessageSize = false;
+ incomingServer.maxMessageSize = 1;
+
+ testTopOrRetr(incomingServer, [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ]);
+});
+
+/**
+ * Turn on server.limitOfflineMessageSize and set maxMessageSize to 1KB, test
+ * TOP is used.
+ */
+add_task(async function testMaxMessageSize() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.limitOfflineMessageSize = true;
+ incomingServer.maxMessageSize = 1;
+
+ testTopOrRetr(incomingServer, [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "TOP 1 20",
+ ]);
+});
+
+/**
+ * Turn on server.headersOnly, test TOP is used.
+ */
+add_task(async function testHeadersOnly() {
+ let incomingServer = createPop3ServerAndLocalFolders(server.port);
+ incomingServer.headersOnly = true;
+
+ testTopOrRetr(incomingServer, [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "TOP 1 0",
+ ]);
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3Download.js b/comm/mailnews/local/test/unit/test_pop3Download.js
new file mode 100644
index 0000000000..0268170f30
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Download.js
@@ -0,0 +1,81 @@
+/**
+ * The intent of this file is to test that pop3 download code message storage
+ * works correctly.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+ "[Bug 655578] list-id filter broken",
+];
+
+var gMsgHdrs = [];
+var gHdrIndex = 0;
+var gFiles = [
+ "../../../data/bugmail1",
+ "../../../data/draft1",
+ "../../../data/bugmail19",
+];
+
+// This combination of prefs is required to reproduce bug 713611, which
+// is what this test is about.
+Services.prefs.setBoolPref("mailnews.downloadToTempFile", false);
+Services.prefs.setBoolPref("mail.server.default.leave_on_server", true);
+
+function run_test() {
+ // add 3 messages
+ gPOP3Pump.files = gFiles;
+ gPOP3Pump.onDone = continueTest;
+ do_test_pending();
+ gPOP3Pump.run();
+}
+
+function continueTest() {
+ // get message headers for the inbox folder
+ var msgCount = 0;
+ for (let hdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ gMsgHdrs.push(hdr);
+ Assert.equal(hdr.subject, testSubjects[msgCount++]);
+ }
+ Assert.equal(msgCount, 3);
+ gPOP3Pump = null;
+ streamNextMessage();
+}
+
+function streamNextMessage() {
+ let msghdr = gMsgHdrs[gHdrIndex];
+ let msgURI = msghdr.folder.getUriForMsg(msghdr);
+ let msgServ = MailServices.messageServiceFromURI(msgURI);
+ msgServ.streamMessage(msgURI, gStreamListener, null, null, false, "", true);
+}
+
+var gStreamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ _stream: null,
+ _data: null,
+ onStartRequest(aRequest) {
+ this._stream = null;
+ this._data = "";
+ },
+ onStopRequest(aRequest, aStatusCode) {
+ // check that the streamed message starts with "From "
+ Assert.ok(this._data.startsWith("From "));
+ if (++gHdrIndex == gFiles.length) {
+ do_test_finished();
+ } else {
+ streamNextMessage();
+ }
+ },
+ onDataAvailable(aRequest, aInputStream, aOff, aCount) {
+ if (this._stream == null) {
+ this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ this._stream.init(aInputStream);
+ }
+ this._data += this._stream.read(aCount);
+ },
+};
diff --git a/comm/mailnews/local/test/unit/test_pop3DownloadTempFileHandling.js b/comm/mailnews/local/test/unit/test_pop3DownloadTempFileHandling.js
new file mode 100644
index 0000000000..2524c489df
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3DownloadTempFileHandling.js
@@ -0,0 +1,62 @@
+/**
+ * The intent of this file is to test temp file handling when
+ * downloading multiple pop3 messages with quarantining turned on.
+ *
+ * Original author: David Bienvenu <dbienvenu@mozilla.com>
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+var gExpectedFiles;
+
+function run_test() {
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", true);
+ gExpectedFiles = createExpectedTemporaryFiles(2);
+ // add 2 messages
+ gPOP3Pump.files = ["../../../data/bugmail1", "../../../data/draft1"];
+ gPOP3Pump.onDone = continueTest;
+ do_test_pending();
+ gPOP3Pump.run();
+}
+
+function continueTest() {
+ dump("temp file path = " + gExpectedFiles[0].path + "\n");
+ dump("temp file path = " + gExpectedFiles[1].path + "\n");
+ for (let expectedFile of gExpectedFiles) {
+ Assert.ok(!expectedFile.exists());
+ }
+
+ // get message headers for the inbox folder
+ var msgCount = 0;
+ for (let hdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ Assert.equal(hdr.subject, testSubjects[msgCount++]);
+ }
+ Assert.equal(msgCount, 2);
+ gPOP3Pump = null;
+ do_test_finished();
+}
+
+function createExpectedTemporaryFiles(numFiles) {
+ function createTemporaryFile() {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("newmsg");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ return file;
+ }
+
+ let expectedFiles = [];
+ for (let i = 0; i < numFiles; i++) {
+ expectedFiles.push(createTemporaryFile());
+ }
+
+ for (let expectedFile of expectedFiles) {
+ expectedFile.remove(false);
+ }
+
+ return expectedFiles;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3Duplicates.js b/comm/mailnews/local/test/unit/test_pop3Duplicates.js
new file mode 100644
index 0000000000..a863cbb1ed
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Duplicates.js
@@ -0,0 +1,40 @@
+/**
+ * The intent of this file is to test duplicate handling options
+ * in the pop3 download code.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+
+function run_test() {
+ // Set duplicate action to be delete duplicates.
+ Services.prefs.setIntPref(
+ "mail.server.default.dup_action",
+ Ci.nsIMsgIncomingServer.deleteDups
+ );
+ // add 3 messages, 2 of which are duplicates.
+ gPOP3Pump.files = [
+ "../../../data/bugmail1",
+ "../../../data/draft1",
+ "../../../data/bugmail1",
+ ];
+ gPOP3Pump.onDone = continueTest;
+ do_test_pending();
+ gPOP3Pump.run();
+}
+
+function continueTest() {
+ // get message headers for the inbox folder
+ var msgCount = 0;
+ for (let hdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ Assert.equal(hdr.subject, testSubjects[msgCount++]);
+ }
+ Assert.equal(msgCount, 2);
+ gPOP3Pump = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3FilterActions.js b/comm/mailnews/local/test/unit/test_pop3FilterActions.js
new file mode 100644
index 0000000000..4c35e46735
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3FilterActions.js
@@ -0,0 +1,143 @@
+/*
+ * This file tests that a pop3 add tag filter writes the new tag
+ * into the message keywords header. It also tests marking read,
+ * and flagging messages.
+ *
+ * Original author: David Bienvenu <dbienvenu@mozilla.com>
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFiles = ["../../../data/bugmail10", "../../../data/bugmail11"];
+
+Services.prefs.setBoolPref("mail.server.default.leave_on_server", true);
+
+// Currently we have two mailbox storage formats.
+var gPluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+
+// Map subject to previews using subject as the key.
+var previews = {
+ "[Bug 436880] IMAP itemDeleted and itemMoveCopyCompleted notifications quite broken":
+ "Do not reply to this email. You can add comments to this bug at https://bugzilla.mozilla.org/show_bug.cgi?id=436880 -- Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email ------- You are receiving this mail because: -----",
+ "Bugzilla: confirm account creation":
+ "Bugzilla has received a request to create a user account using your email address (example@example.org). To confirm that you want to create an account using that email address, visit the following link: https://bugzilla.mozilla.org/token.cgi?t=xxx",
+};
+
+var gFilter; // the test filter
+var gFilterList;
+var gTestArray = [
+ function createFilters() {
+ gFilterList = gPOP3Pump.fakeServer.getFilterList(null);
+ gFilter = gFilterList.createFilter("AddKeyword");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.matchAll = true;
+ gFilter.appendTerm(searchTerm);
+ let tagAction = gFilter.createAction();
+ tagAction.type = Ci.nsMsgFilterAction.AddTag;
+ tagAction.strValue = "TheTag";
+ gFilter.appendAction(tagAction);
+ tagAction = gFilter.createAction();
+ tagAction.type = Ci.nsMsgFilterAction.MarkRead;
+ gFilter.appendAction(tagAction);
+ tagAction = gFilter.createAction();
+ tagAction.type = Ci.nsMsgFilterAction.MarkFlagged;
+ gFilter.appendAction(tagAction);
+ gFilter.enabled = true;
+ gFilter.filterType = Ci.nsMsgFilterType.InboxRule;
+ gFilterList.insertFilterAt(0, gFilter);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages1() {
+ gPOP3Pump.files = gFiles;
+ await gPOP3Pump.run();
+ },
+ async function verifyFolders2() {
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 2);
+
+ // invalidate the inbox summary file, to be sure that we wrote the keywords
+ // into the mailbox.
+ localAccountUtils.inboxFolder.msgDatabase.summaryValid = false;
+ localAccountUtils.inboxFolder.msgDatabase = null;
+ localAccountUtils.inboxFolder.ForceDBClosed();
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ try {
+ localAccountUtils.inboxFolder.getDatabaseWithReparse(
+ promiseUrlListener,
+ null
+ );
+ } catch (ex) {
+ await promiseUrlListener.promise;
+ Assert.ok(ex.result == Cr.NS_ERROR_NOT_INITIALIZED);
+ return;
+ }
+
+ // This statement is never reached.
+ Assert.ok(false);
+ },
+ function verifyMessages() {
+ let hdrs = [];
+ let keys = [];
+ for (let hdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ keys.push(hdr.messageKey);
+ hdrs.push(hdr);
+ }
+ Assert.ok(!localAccountUtils.inboxFolder.fetchMsgPreviewText(keys, null));
+ let preview1 = hdrs[0].getStringProperty("preview");
+ let preview2 = hdrs[1].getStringProperty("preview");
+ Assert.equal(preview1, previews[hdrs[0].subject]);
+ Assert.equal(preview2, previews[hdrs[1].subject]);
+ Assert.equal(hdrs[0].getStringProperty("keywords"), "TheTag");
+ Assert.equal(hdrs[1].getStringProperty("keywords"), "TheTag");
+ Assert.equal(
+ hdrs[0].flags,
+ Ci.nsMsgMessageFlags.Read | Ci.nsMsgMessageFlags.Marked
+ );
+ Assert.equal(
+ hdrs[1].flags,
+ Ci.nsMsgMessageFlags.Read | Ci.nsMsgMessageFlags.Marked
+ );
+ },
+];
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
+
+function setup_store(storeID) {
+ return function _setup_store() {
+ // Initialize pop3Pump with correct mailbox format.
+ gPOP3Pump.resetPluggableStore(storeID);
+
+ // Set the default mailbox store.
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+
+ // Make sure we're not quarantining messages
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", false);
+
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+ };
+}
+
+function run_test() {
+ for (let store of gPluggableStores) {
+ add_task(setup_store(store));
+ gTestArray.forEach(x => add_task(x));
+ }
+
+ add_task(exitTest);
+ run_next_test();
+}
+
+function exitTest() {
+ info("Exiting mail tests\n");
+ gPOP3Pump = null;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3Filters.js b/comm/mailnews/local/test/unit/test_pop3Filters.js
new file mode 100644
index 0000000000..53ef182a6d
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Filters.js
@@ -0,0 +1,114 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+let [daemon, server, handler] = setupServerDaemon();
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+let incomingServer = createPop3ServerAndLocalFolders(server.port);
+
+/**
+ * Inject a message to the server and do a GetNewMail for the incomingServer.
+ */
+async function getNewMail() {
+ daemon.setMessages(["message1.eml", "message3.eml"]);
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ return urlListener.promise;
+}
+
+/**
+ * Test DeleteFromPop3Server filter should send DELE for matched message.
+ */
+add_task(async function testDeleteFromPop3Server() {
+ // Turn on leaveMessagesOnServer, so that DELE would not be sent normally.
+ incomingServer.leaveMessagesOnServer = true;
+
+ // Create a DeleteFromPop3Server filter.
+ let filterList = incomingServer.getFilterList(null);
+ let filter = filterList.createFilter("deleteFromServer");
+
+ let searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.Subject;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ let value = searchTerm.value;
+ value.str = "mail 2";
+ searchTerm.value = value;
+ filter.appendTerm(searchTerm);
+
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.DeleteFromPop3Server;
+ filter.appendAction(action);
+
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+
+ await getNewMail();
+ do_check_transaction(server.playTransaction(), [
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1", // message1.eml doesn't match the filter, no DELE.
+ "RETR 2",
+ "DELE 2", // message3.eml matches the filter, DELE was sent.
+ ]);
+
+ // MailServices.accounts.removeIncomingServer(incomingServer, false);
+ filterList.removeFilterAt(0);
+});
+
+/**
+ * Test FetchBodyFromPop3Server filter should send RETR for matched message.
+ */
+add_task(async function testFetchBodyFromPop3Server() {
+ incomingServer.leaveMessagesOnServer = true;
+ // Turn on leaveMessagesOnServer, so that RETR would not be sent normally.
+ incomingServer.headersOnly = true;
+
+ // Create a FetchBodyFromPop3Server filter.
+ let filterList = incomingServer.getFilterList(null);
+ let filter = filterList.createFilter("fetchBodyFromServer");
+
+ let searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.Subject;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ let value = searchTerm.value;
+ value.str = "mail 2";
+ searchTerm.value = value;
+ filter.appendTerm(searchTerm);
+
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.FetchBodyFromPop3Server;
+ filter.appendAction(action);
+
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+
+ await getNewMail();
+ do_check_transaction(server.playTransaction(), [
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "TOP 1 0", // message1.eml doesn't match the filter, no RETR.
+ "TOP 2 0",
+ "RETR 2", // message3.eml matches the filter, RETR was sent.
+ ]);
+
+ filterList.removeFilterAt(0);
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3GSSAPIFail.js b/comm/mailnews/local/test/unit/test_pop3GSSAPIFail.js
new file mode 100644
index 0000000000..2ff133a14e
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3GSSAPIFail.js
@@ -0,0 +1,222 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * A server offers GSSAPI (Kerberos), but auth fails, due to client or server.
+ *
+ * This mainly tests whether we use the correct login mode.
+ *
+ * Whether it fails due to
+ * - client not set up
+ * - client ticket expired / not logged in
+ * - server not being set up properly
+ * makes no difference to Thunderbird, as that's all hidden in the gssapi-Library
+ * from the OS. So, the server here just returning err is a good approximation
+ * of reality of the above cases.
+ *
+ * Actually, we (more precisely the OS GSSAPI lib) fail out of band
+ * in the Kerberos protocol, before the AUTH GSSAPI command is even issued.
+ *
+ * @author Ben Bucksch
+ */
+
+var server;
+var daemon;
+var authSchemes;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "GSSAPI auth, server with GSSAPI only",
+ clientAuthMethod: Ci.nsMsgAuthMethod.GSSAPI,
+ serverAuthMethods: ["GSSAPI"],
+ expectSuccess: false,
+ transaction: ["AUTH", "CAPA"],
+ },
+ {
+ // First GSSAPI step happens and fails out of band, thus no "AUTH GSSAPI"
+ title: "GSSAPI auth, server with GSSAPI and CRAM-MD5",
+ clientAuthMethod: Ci.nsMsgAuthMethod.GSSAPI,
+ serverAuthMethods: ["GSSAPI", "CRAM-MD5"],
+ expectSuccess: false,
+ transaction: ["AUTH", "CAPA"],
+ },
+ {
+ title: "Any secure auth, server with GSSAPI only",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["GSSAPI"],
+ expectSuccess: false,
+ transaction: ["AUTH", "CAPA"],
+ },
+ {
+ title: "Any secure auth, server with GSSAPI and CRAM-MD5",
+ clientAuthMethod: Ci.nsMsgAuthMethod.secure,
+ serverAuthMethods: ["GSSAPI", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH CRAM-MD5", "STAT"],
+ },
+ {
+ title: "Encrypted password, server with GSSAPI and CRAM-MD5",
+ clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted,
+ serverAuthMethods: ["GSSAPI", "CRAM-MD5"],
+ expectSuccess: true,
+ transaction: ["AUTH", "CAPA", "AUTH CRAM-MD5", "STAT"],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ if (thisTest.expectSuccess) {
+ Assert.equal(result, 0);
+ } else {
+ Assert.notEqual(result, 0);
+ }
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, thisTest.transaction);
+
+ do_timeout(0, checkBusy);
+ } catch (e) {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ test = thisTest.title;
+ dump("NEXT test is: " + thisTest.title + "\n");
+
+ authSchemes = thisTest.serverAuthMethods;
+
+ // Mailnews caches server capabilities, so try to reset it
+ deletePop3Server();
+ incomingServer = createPop3Server();
+
+ let msgServer = incomingServer;
+ msgServer.QueryInterface(Ci.nsIMsgIncomingServer);
+ msgServer.authMethod = thisTest.clientAuthMethod;
+
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
+
+// <copied from="head_maillocal.js::createPop3ServerAndLocalFolders()">
+function createPop3Server() {
+ let incoming = MailServices.accounts.createIncomingServer(
+ "fred",
+ "localhost",
+ "pop3"
+ );
+ incoming.port = server.port;
+ incoming.password = "wilma";
+ return incoming;
+}
+// </copied>
+
+function deletePop3Server() {
+ if (!incomingServer) {
+ return;
+ }
+ MailServices.accounts.removeIncomingServer(incomingServer, true);
+ incomingServer = null;
+}
+
+class GSSAPIFail_handler extends POP3_RFC5034_handler {
+ _needGSSAPI = false;
+ // kAuthSchemes will be set by test
+
+ AUTH(restLine) {
+ var scheme = restLine.split(" ")[0];
+ if (scheme == "GSSAPI") {
+ this._multiline = true;
+ this._needGSSAPI = true;
+ return "+";
+ }
+ return super.AUTH(restLine); // call parent
+ }
+ onMultiline(line) {
+ if (this._needGSSAPI) {
+ this._multiline = false;
+ this._needGSSAPI = false;
+ return "-ERR hm.... shall I allow you? hm... NO.";
+ }
+
+ if (super.onMultiline) {
+ // Call parent.
+ return super.onMultiline(line);
+ }
+ return undefined;
+ }
+}
+
+function run_test() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ var handler = new GSSAPIFail_handler(d);
+ handler.kAuthSchemes = authSchemes;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ // incomingServer = createPop3ServerAndLocalFolders();
+ localAccountUtils.loadLocalMailAccount();
+
+ do_test_pending();
+
+ testNext();
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3GetNewMail.js b/comm/mailnews/local/test/unit/test_pop3GetNewMail.js
new file mode 100644
index 0000000000..f0ecf9eb69
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3GetNewMail.js
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Protocol tests for POP3.
+ */
+var server;
+var daemon;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "Get New Mail, No Messages",
+ messages: [],
+ transaction: ["AUTH", "CAPA", "AUTH PLAIN", "STAT"],
+ },
+ {
+ title: "Get New Mail, No Messages 2",
+ messages: [],
+ transaction: ["CAPA", "AUTH PLAIN", "STAT"],
+ },
+ {
+ title: "Get New Mail, One Message",
+ messages: ["message1.eml"],
+ transaction: [
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ var transaction = server.playTransaction();
+
+ do_check_transaction(transaction, thisTest.transaction);
+
+ Assert.equal(
+ localAccountUtils.inboxFolder.getTotalMessages(false),
+ thisTest.messages.length
+ );
+
+ Assert.equal(result, 0);
+ } catch (e) {
+ // If we have an error, clean up nicely before we throw it.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, checkBusy);
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ // Set up the test
+ test = thisTest.title;
+ daemon.setMessages(thisTest.messages);
+
+ // Now get the mail
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+function run_test() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ server = setupServerDaemon();
+ daemon = server[0];
+ server = server[1];
+ server.start();
+
+ // Set up the basic accounts and folders
+ incomingServer = createPop3ServerAndLocalFolders(server.port);
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ do_test_pending();
+
+ testNext();
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3MoveFilter.js b/comm/mailnews/local/test/unit/test_pop3MoveFilter.js
new file mode 100644
index 0000000000..eec79bb63a
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3MoveFilter.js
@@ -0,0 +1,137 @@
+/*
+ * This file tests that a pop3 move filter doesn't leave the
+ * original message in the inbox.
+ *
+ * Original author: David Bienvenu <dbienvenu@mozilla.com>
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFiles = ["../../../data/bugmail10", "../../../data/bugmail11"];
+
+// make sure limiting download size doesn't causes issues with move filters.
+Services.prefs.setBoolPref(
+ "mail.server.default.limit_offline_message_size",
+ true
+);
+Services.prefs.setBoolPref("mail.server.default.leave_on_server", true);
+
+// Currently we have two mailbox storage formats.
+var gPluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+
+var previews = {
+ "[Bug 436880] IMAP itemDeleted and itemMoveCopyCompleted notifications quite broken":
+ "Do not reply to this email. You can add comments to this bug at https://bugzilla.mozilla.org/show_bug.cgi?id=436880 -- Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email ------- You are receiving this mail because: -----",
+ "Bugzilla: confirm account creation":
+ "Bugzilla has received a request to create a user account using your email address (example@example.org). To confirm that you want to create an account using that email address, visit the following link: https://bugzilla.mozilla.org/token.cgi?t=xxx",
+};
+
+var gMoveFolder;
+var gFilter; // the test filter
+var gFilterList;
+var gTestArray = [
+ function createFilters() {
+ gFilterList = gPOP3Pump.fakeServer.getFilterList(null);
+ gFilter = gFilterList.createFilter("MoveAll");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.matchAll = true;
+ gFilter.appendTerm(searchTerm);
+ let moveAction = gFilter.createAction();
+ moveAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ moveAction.targetFolderUri = gMoveFolder.URI;
+ gFilter.appendAction(moveAction);
+ gFilter.enabled = true;
+ gFilter.filterType = Ci.nsMsgFilterType.InboxRule;
+ gFilterList.insertFilterAt(0, gFilter);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages1() {
+ gPOP3Pump.files = gFiles;
+ await gPOP3Pump.run();
+ },
+ async function verifyFolders2() {
+ Assert.equal(folderCount(gMoveFolder), 2);
+ // the local inbox folder should now be empty, since we moved incoming mail.
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+
+ // invalidate the inbox summary file, to be sure that we really moved
+ // the mail.
+ localAccountUtils.inboxFolder.msgDatabase.summaryValid = false;
+ localAccountUtils.inboxFolder.msgDatabase = null;
+ localAccountUtils.inboxFolder.ForceDBClosed();
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ try {
+ localAccountUtils.inboxFolder.getDatabaseWithReparse(
+ promiseUrlListener,
+ null
+ );
+ } catch (ex) {
+ await promiseUrlListener.promise;
+ Assert.ok(ex.result == Cr.NS_ERROR_NOT_INITIALIZED);
+ return;
+ }
+ // This statement isn't reached since the error is thrown.
+ Assert.ok(false);
+ },
+ function verifyMessages() {
+ let hdrs = [];
+ let keys = [];
+ for (let hdr of gMoveFolder.msgDatabase.enumerateMessages()) {
+ keys.push(hdr.messageKey);
+ hdrs.push(hdr);
+ }
+ Assert.ok(!gMoveFolder.fetchMsgPreviewText(keys, null));
+ Assert.equal(
+ hdrs[0].getStringProperty("preview"),
+ previews[hdrs[0].subject]
+ );
+ Assert.equal(
+ hdrs[1].getStringProperty("preview"),
+ previews[hdrs[1].subject]
+ );
+ },
+];
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
+
+function setup_store(storeID) {
+ return function _setup_store() {
+ // Reset pop3Pump with correct mailbox format.
+ gPOP3Pump.resetPluggableStore(storeID);
+
+ // Make sure we're not quarantining messages
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", false);
+
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ gMoveFolder =
+ localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder");
+ };
+}
+
+function run_test() {
+ for (let store of gPluggableStores) {
+ add_task(setup_store(store));
+ gTestArray.forEach(x => add_task(x));
+ }
+
+ add_task(exitTest);
+ run_next_test();
+}
+
+function exitTest() {
+ // Cleanup and exit the test.
+ info("Exiting mail tests\n");
+ gPOP3Pump = null;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3MoveFilter2.js b/comm/mailnews/local/test/unit/test_pop3MoveFilter2.js
new file mode 100644
index 0000000000..71a7578310
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3MoveFilter2.js
@@ -0,0 +1,108 @@
+/*
+ * This file tests that a pop3 move filter doesn't reuse msg hdr
+ * info from previous moves.
+ *
+ * Original author: David Bienvenu <dbienvenu@mozilla.com>
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+var gFiles = ["../../../data/bugmail10", "../../../data/basic1"];
+
+Services.prefs.setBoolPref("mail.server.default.leave_on_server", true);
+
+// Currently we have two mailbox storage formats.
+var gPluggableStores = [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+];
+var basic1_preview = "Hello, world!";
+var bugmail10_preview =
+ "Do not reply to this email. You can add comments to this bug at https://bugzilla.mozilla.org/show_bug.cgi?id=436880 -- Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email ------- You are receiving this mail because: -----";
+
+var gMoveFolder;
+var gFilter; // the test filter
+var gFilterList;
+var gTestArray = [
+ function createFilters() {
+ gFilterList = gPOP3Pump.fakeServer.getFilterList(null);
+ // create a cc filter which will match the first message but not the second.
+ gFilter = gFilterList.createFilter("MoveCc");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.CC;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ var oldValue = searchTerm.value;
+ oldValue.attrib = Ci.nsMsgSearchAttrib.CC;
+ oldValue.str = "invalid@example.com";
+ searchTerm.value = oldValue;
+ gFilter.appendTerm(searchTerm);
+ let moveAction = gFilter.createAction();
+ moveAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ moveAction.targetFolderUri = gMoveFolder.URI;
+ gFilter.appendAction(moveAction);
+ gFilter.enabled = true;
+ gFilter.filterType = Ci.nsMsgFilterType.InboxRule;
+ gFilterList.insertFilterAt(0, gFilter);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages1() {
+ gPOP3Pump.files = gFiles;
+ await gPOP3Pump.run();
+ },
+ function verifyFolders2() {
+ Assert.equal(folderCount(gMoveFolder), 1);
+ // the local inbox folder should have one message.
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 1);
+ },
+ function verifyMessages() {
+ // check MoveFolder message
+ let hdr = [...gMoveFolder.msgDatabase.enumerateMessages()][0];
+ Assert.ok(!gMoveFolder.fetchMsgPreviewText([hdr.messageKey], null));
+ Assert.equal(hdr.getStringProperty("preview"), bugmail10_preview);
+ // check inbox message
+ hdr = [...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()][0];
+ Assert.ok(
+ !localAccountUtils.inboxFolder.fetchMsgPreviewText([hdr.messageKey], null)
+ );
+ Assert.equal(hdr.getStringProperty("preview"), basic1_preview);
+ },
+];
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
+
+function setup_store(storeID) {
+ return function _setup_store() {
+ // Initialize pop3Pump with correct mailbox format.
+ gPOP3Pump.resetPluggableStore(storeID);
+
+ // Set the default mailbox store.
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+
+ // Make sure we're not quarantining messages
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", false);
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ gMoveFolder =
+ localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder");
+ };
+}
+
+function run_test() {
+ for (let store of gPluggableStores) {
+ add_task(setup_store(store));
+ gTestArray.forEach(x => add_task(x));
+ }
+
+ add_task(exitTest);
+ run_next_test();
+}
+
+function exitTest() {
+ // Cleanup and exit the test.
+ info("Exiting mail tests\n");
+ gPOP3Pump = null;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3MultiCopy.js b/comm/mailnews/local/test/unit/test_pop3MultiCopy.js
new file mode 100644
index 0000000000..42a0f67e23
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3MultiCopy.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests that copied multiple messages in maildir are correct.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/maildirstore;1"
+);
+
+add_task(async function runPump() {
+ // Test for multiple message copy for maildir.
+ let storeID = "@mozilla.org/msgstore/maildirstore;1";
+ gPOP3Pump.resetPluggableStore(storeID);
+ // Set the default mailbox store.
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+
+ // We want to test cross-server copy, so don't defer.
+ gPOP3Pump.fakeServer.deferredToAccount = "";
+
+ gPOP3Pump.files = ["../../../data/bugmail1", "../../../data/draft1"];
+ await gPOP3Pump.run();
+
+ // get message headers for the inbox folder
+ let inbox = gPOP3Pump.fakeServer.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ dump("inbox is at " + inbox.filePath.path + "\n");
+
+ // Accumulate messages to copy.
+ let messages = [];
+ let msgCount = 0;
+ for (let hdr of inbox.msgDatabase.enumerateMessages()) {
+ msgCount++;
+ messages.push(hdr);
+ Assert.equal(hdr.subject, testSubjects[msgCount - 1]);
+ }
+ Assert.equal(messages.length, 2);
+
+ // Create a test folder on the Local Folders account.
+ let testFolder = localAccountUtils.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("test");
+ dump("testFolder is at " + testFolder.filePath.path + "\n");
+
+ // Copy messages to that folder.
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ inbox,
+ messages,
+ testFolder,
+ false,
+ promiseCopyListener,
+ null,
+ false
+ );
+ await promiseCopyListener.promise;
+
+ // Check the destination headers.
+ messages = [];
+ msgCount = 0;
+ let subjects = [];
+ for (let hdr of testFolder.msgDatabase.enumerateMessages()) {
+ msgCount++;
+ messages.push(hdr);
+ dump("Subject: " + hdr.subject + "\n");
+ subjects.push(hdr.subject);
+ }
+ Assert.equal(messages.length, 2);
+
+ // Check for subjects. maildir order for messages may not match
+ // order for creation, hence the array.includes.
+ for (let subject of testSubjects) {
+ Assert.ok(subjects.includes(subject));
+ }
+
+ // Make sure the body matches the message.
+ for (let hdr of testFolder.msgDatabase.enumerateMessages()) {
+ let body = mailTestUtils.loadMessageToString(testFolder, hdr);
+ Assert.ok(body.includes(hdr.subject));
+ }
+
+ gPOP3Pump = null;
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3MultiCopy2.js b/comm/mailnews/local/test/unit/test_pop3MultiCopy2.js
new file mode 100644
index 0000000000..007a0fd99b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3MultiCopy2.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests that moved multiple messages from maildir->mbox and
+ mbox->maildir are correct.
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/maildirstore;1"
+);
+
+var gInboxFolder, gTestFolder;
+
+gPOP3Pump.files = ["../../../data/bugmail1", "../../../data/draft1"];
+var gTestSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+
+add_setup(async function () {
+ let storeID = "@mozilla.org/msgstore/maildirstore;1";
+ resetPluggableStoreLocal(storeID);
+
+ // We want to test cross-server copy, so don't defer.
+ gPOP3Pump.fakeServer.deferredToAccount = "";
+
+ // Create a test folder on the Local Folders account.
+ gTestFolder = localAccountUtils.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("test");
+ dump("testFolder is at " + gTestFolder.filePath.path + "\n");
+ await gPOP3Pump.run();
+});
+
+add_task(async function maildirToMbox() {
+ // Test for multiple message copy for maildir->mbox.
+
+ // get message headers for the inbox folder
+ gInboxFolder = gPOP3Pump.fakeServer.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ dump("inbox is at " + gInboxFolder.filePath.path + "\n");
+
+ // Accumulate messages to copy.
+ let messages = [];
+ for (let hdr of gInboxFolder.msgDatabase.enumerateMessages()) {
+ messages.push(hdr);
+ }
+ Assert.equal(messages.length, 2);
+
+ // Move messages to mbox test folder.
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gInboxFolder,
+ messages,
+ gTestFolder,
+ true, // isMove
+ promiseCopyListener,
+ null, // window
+ false // allowUndo
+ );
+ await promiseCopyListener.promise;
+
+ // Check the destination headers.
+ messages = [];
+ let subjects = [];
+ for (let hdr of gTestFolder.msgDatabase.enumerateMessages()) {
+ messages.push(hdr);
+ dump("Subject: " + hdr.subject + "\n");
+ subjects.push(hdr.subject);
+ }
+ Assert.equal(messages.length, 2);
+
+ // messages should be missing from source
+ Assert.equal(gInboxFolder.getTotalMessages(false), 0);
+
+ // Check for subjects. maildir order for messages may not match
+ // order for creation, hence the array.includes.
+ for (let subject of gTestSubjects) {
+ Assert.ok(subjects.includes(subject));
+ }
+
+ // Make sure the body matches the message.
+ for (let hdr of gTestFolder.msgDatabase.enumerateMessages()) {
+ let body = mailTestUtils.loadMessageToString(gTestFolder, hdr);
+ Assert.ok(body.includes(hdr.subject));
+ }
+});
+
+add_task(async function mboxToMaildir() {
+ // Test for multiple message copy for mbox->maildir.
+
+ // Accumulate messages to copy.
+ let messages = [];
+ for (let hdr of gTestFolder.msgDatabase.enumerateMessages()) {
+ messages.push(hdr);
+ }
+ Assert.equal(messages.length, 2);
+
+ // Move messages to inbox folder.
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gTestFolder,
+ messages,
+ gInboxFolder,
+ true,
+ promiseCopyListener,
+ null,
+ false
+ );
+ await promiseCopyListener.promise;
+
+ // Check the destination headers.
+ messages = [];
+ let subjects = [];
+ for (let hdr of gInboxFolder.msgDatabase.enumerateMessages()) {
+ messages.push(hdr);
+ dump("Subject: " + hdr.subject + "\n");
+ subjects.push(hdr.subject);
+ }
+ Assert.equal(messages.length, 2);
+
+ // messages should be missing from source
+ Assert.equal(gTestFolder.getTotalMessages(false), 0);
+
+ // Check for subjects. maildir order for messages may not match
+ // order for creation, hence the array.includes.
+ for (let subject of gTestSubjects) {
+ Assert.ok(subjects.includes(subject));
+ }
+
+ // Make sure the body matches the message.
+ for (let hdr of gInboxFolder.msgDatabase.enumerateMessages()) {
+ let body = mailTestUtils.loadMessageToString(gInboxFolder, hdr);
+ Assert.ok(body.includes(hdr.subject));
+ }
+});
+
+add_task(function testCleanup() {
+ gPOP3Pump = null;
+});
+
+// Clone of POP3pump resetPluggableStore that does not reset local folders.
+function resetPluggableStoreLocal(aStoreContractID) {
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ aStoreContractID
+ );
+
+ // Cleanup existing files, server and account instances, if any.
+ if (gPOP3Pump._server) {
+ gPOP3Pump._server.stop();
+ }
+
+ if (gPOP3Pump.fakeServer && gPOP3Pump.fakeServer.valid) {
+ gPOP3Pump.fakeServer.closeCachedConnections();
+ MailServices.accounts.removeIncomingServer(gPOP3Pump.fakeServer, false);
+ }
+
+ gPOP3Pump.fakeServer = localAccountUtils.create_incoming_server(
+ "pop3",
+ gPOP3Pump.kPOP3_PORT,
+ "fred",
+ "wilma"
+ );
+
+ // localAccountUtils.clearAll();
+
+ gPOP3Pump._incomingServer = gPOP3Pump.fakeServer;
+ gPOP3Pump._mailboxStoreContractID = aStoreContractID;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3Password.js b/comm/mailnews/local/test/unit/test_pop3Password.js
new file mode 100644
index 0000000000..8cfd0de79d
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Password.js
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for POP3.
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var server;
+var daemon;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "Get New Mail, One Message",
+ messages: ["message1.eml"],
+ transaction: [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ var transaction = server.playTransaction();
+
+ do_check_transaction(transaction, thisTest.transaction);
+
+ Assert.equal(
+ localAccountUtils.inboxFolder.getTotalMessages(false),
+ thisTest.messages.length
+ );
+
+ Assert.equal(result, 0);
+ } catch (e) {
+ // If we have an error, clean up nicely before we throw it.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, checkBusy);
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ // Set up the test
+ test = thisTest.title;
+ daemon.setMessages(thisTest.messages);
+
+ // Now get the mail
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ // Set up the Server
+ var serverArray = setupServerDaemon();
+ daemon = serverArray[0];
+ server = serverArray[1];
+ var handler = serverArray[2];
+ server.start();
+
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = "testpop3";
+ handler.kPassword = "pop3test";
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ incomingServer = MailServices.accounts.createIncomingServer(
+ "testpop3",
+ "localhost",
+ "pop3"
+ );
+
+ incomingServer.port = server.port;
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ do_test_pending();
+
+ testNext();
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3Password2.js b/comm/mailnews/local/test/unit/test_pop3Password2.js
new file mode 100644
index 0000000000..9bc32e471b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Password2.js
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for POP3 - checks for servers whose details have
+ * changed (e.g. realusername and realhostname are different from username and
+ * hostname).
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var server;
+var daemon;
+var incomingServer;
+var thisTest;
+
+var tests = [
+ {
+ title: "Get New Mail, One Message",
+ messages: ["message1.eml"],
+ transaction: [
+ "AUTH",
+ "CAPA",
+ "AUTH PLAIN",
+ "STAT",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "DELE 1",
+ ],
+ },
+];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ var transaction = server.playTransaction();
+
+ do_check_transaction(transaction, thisTest.transaction);
+
+ Assert.equal(
+ localAccountUtils.inboxFolder.getTotalMessages(false),
+ thisTest.messages.length
+ );
+
+ Assert.equal(result, 0);
+ } catch (e) {
+ // If we have an error, clean up nicely before we throw it.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, checkBusy);
+ },
+};
+
+function checkBusy() {
+ if (tests.length == 0) {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ testNext();
+}
+
+function testNext() {
+ thisTest = tests.shift();
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ server.resetTest();
+
+ // Set up the test
+ test = thisTest.title;
+ daemon.setMessages(thisTest.messages);
+
+ // Now get the mail
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+add_task(async function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ // These preferences set up a local pop server that has had its hostname
+ // and username changed from the original settings. We can't do this by
+ // function calls for this test as they would cause the password to be
+ // forgotten when changing the hostname/username and this breaks the test.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id1");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2"
+ );
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account2");
+ Services.prefs.setCharPref("mail.identity.id1.fullName", "testpop3");
+ Services.prefs.setCharPref(
+ "mail.identity.id1.useremail",
+ "testpop3@localhost"
+ );
+ Services.prefs.setBoolPref("mail.identity.id1.valid", true);
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.name", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server2.directory-rel",
+ "[ProfD]Mail/invalid"
+ );
+ Services.prefs.setCharPref("mail.server.server2.hostname", "invalid");
+ Services.prefs.setCharPref(
+ "mail.server.server2.name",
+ "testpop3 on localhost"
+ );
+ Services.prefs.setCharPref("mail.server.server2.realhostname", "localhost");
+ Services.prefs.setCharPref("mail.server.server2.realuserName", "testpop3");
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server2.userName", "othername");
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-alt.json");
+
+ // Set up the Server
+ var serverArray = setupServerDaemon();
+ daemon = serverArray[0];
+ server = serverArray[1];
+ var handler = serverArray[2];
+ server.start();
+ Services.prefs.setIntPref("mail.server.server2.port", server.port);
+
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = "testpop3";
+ handler.kPassword = "pop3test";
+
+ MailServices.accounts.loadAccounts();
+
+ localAccountUtils.incomingServer = MailServices.accounts.localFoldersServer;
+
+ var rootFolder =
+ localAccountUtils.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ localAccountUtils.inboxFolder = rootFolder.createLocalSubfolder("Inbox");
+ // a local inbox should have a Mail flag!
+ localAccountUtils.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+
+ // Create the incoming server with "original" details.
+ incomingServer = MailServices.accounts.getIncomingServer("server2");
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ do_test_pending();
+
+ testNext();
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3Password3.js b/comm/mailnews/local/test/unit/test_pop3Password3.js
new file mode 100644
index 0000000000..b12181e746
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Password3.js
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Extra tests for POP3 passwords (forgetPassword)
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var kUser1 = "testpop3";
+var kUser2 = "testpop3a";
+var kProtocol = "pop3";
+var kHostname = "localhost";
+var kServerUrl = "mailbox://" + kHostname;
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-multiple.json");
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ let incomingServer1 = MailServices.accounts.createIncomingServer(
+ kUser1,
+ kHostname,
+ kProtocol
+ );
+
+ let incomingServer2 = MailServices.accounts.createIncomingServer(
+ kUser2,
+ kHostname,
+ kProtocol
+ );
+
+ // Test - Check there are two logins to begin with.
+ var logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ Assert.equal(logins.length, 2);
+
+ // These will either be one way around or the other.
+ if (logins[0].username == kUser1) {
+ Assert.equal(logins[1].username, kUser2);
+ } else {
+ Assert.equal(logins[0].username, kUser2);
+ Assert.equal(logins[1].username, kUser1);
+ }
+
+ // Test - Remove a login via the incoming server
+ incomingServer1.forgetPassword();
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // should be one login left for kUser2
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUser2);
+
+ // Bug 561056 - Expand username to also contain domain (i.e. full email).
+ incomingServer2.username = kUser2 + "@local.host";
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // There should still be the one login left for kUser2
+ Assert.equal(logins.length, 1);
+ // LoginInfo should be migrated in MsgIncomingServer.jsm.
+ Assert.equal(logins[0].username, incomingServer2.username);
+
+ // Change username to another one.
+ incomingServer2.username = "testpop";
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // LoginInfo should be migrated in MsgIncomingServer.jsm.
+ Assert.equal(logins.length, 1);
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc1939.js b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc1939.js
new file mode 100644
index 0000000000..5a582649fa
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc1939.js
@@ -0,0 +1,216 @@
+/* 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/. */
+
+/**
+ * Tests password failure with RFC1939.
+ *
+ * This test checks to see if the pop3 password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+var daemon;
+var incomingServer;
+var attempt = 0;
+
+var kUserName = "testpop3";
+var kInvalidPassword = "pop3test";
+var kValidPassword = "testpop3";
+
+add_setup(async function () {
+ // Enable debug for the sign on.
+ Services.prefs.setBoolPref("signon.debug", true);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Set up the Server
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ var handler = new POP3_RFC1939_handler(d);
+ handler.dropOnAuthFailure = true;
+
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = kUserName;
+ handler.kPassword = kValidPassword;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ incomingServer = MailServices.accounts.createIncomingServer(
+ kUserName,
+ "localhost",
+ "pop3"
+ );
+
+ incomingServer.port = server.port;
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ daemon.setMessages(["message1.eml"]);
+});
+
+add_task(async function getMail1() {
+ // Now get the mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await Assert.rejects(
+ urlListener.promise,
+ reason => {
+ return reason === Cr.NS_ERROR_FAILURE;
+ },
+ "Check that wrong password is entered and thrown"
+ );
+ // We shouldn't have emails as the auth failed.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+ // Sanity check that we are at attempt 2.
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+});
+
+add_task(async function getMail2() {
+ // Now get the mail
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await urlListener.promise;
+ Assert.equal(attempt, 4);
+ // On the last attempt (4th), we should have successfully got one mail.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 1);
+
+ // Now check the new one has been saved.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+});
+
+add_task(function endTest() {
+ // Cleanup for potential Sockets/Ports leakage.
+ server.stop();
+ server = null;
+ daemon = null;
+ incomingServer = null;
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+/* exported alert, confirmEx, promptPasswordPS */
+function alertPS(parent, aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ info("Alert Title: " + aDialogText + "\nAlert Text: " + aText);
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ info("Attempting retry");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ info("Cancelling login attempt");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ info("Attempting Retry");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ info("Enter new password");
+ return 2;
+ default:
+ throw new Error("unexpected attempt number " + attempt);
+ }
+}
+
+/**
+ * Extension for alertTestUtils.
+ * Make sure that at the 4th attempt the correct password is used.
+ */
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc2449.js b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc2449.js
new file mode 100644
index 0000000000..1701f999c9
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc2449.js
@@ -0,0 +1,215 @@
+/* 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/. */
+
+/**
+ * Tests password failure with RFC2449 auth.
+ *
+ * This test checks to see if the pop3 password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+var daemon;
+var incomingServer;
+var attempt = 0;
+
+var kUserName = "testpop3";
+var kInvalidPassword = "pop3test";
+var kValidPassword = "testpop3";
+
+add_setup(async function () {
+ // Enable debug for the sign on.
+ Services.prefs.setBoolPref("signon.debug", true);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Set up the Server
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ var handler = new POP3_RFC2449_handler(d);
+ handler.dropOnAuthFailure = true;
+
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = kUserName;
+ handler.kPassword = kValidPassword;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ incomingServer = MailServices.accounts.createIncomingServer(
+ kUserName,
+ "localhost",
+ "pop3"
+ );
+
+ incomingServer.port = server.port;
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ daemon.setMessages(["message1.eml"]);
+});
+
+add_task(async function getMail1() {
+ // Now get the mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await Assert.rejects(
+ urlListener.promise,
+ reason => {
+ return reason === Cr.NS_ERROR_FAILURE;
+ },
+ "Check that wrong password is entered and thrown"
+ );
+ // We shouldn't have emails as the auth failed.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+ // Sanity check that we are at attempt 2.
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+});
+
+add_task(async function getMail2() {
+ // Now get the mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await urlListener.promise;
+ Assert.equal(attempt, 4);
+ // On the last attempt (4th), we should have successfully got one mail.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 1);
+
+ // Now check the new one has been saved.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+});
+
+add_task(function endTest() {
+ // Cleanup for potential Sockets/Ports leakage.
+ server.stop();
+ server = null;
+ daemon = null;
+ incomingServer = null;
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+function alertPS(parent, aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ info("Alert Title: " + aDialogText + "\nAlert Text: " + aText);
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ info("Attempting retry");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ info("Cancelling login attempt");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ info("Attempting Retry");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ info("Enter new password");
+ return 2;
+ default:
+ throw new Error("unexpected attempt number " + attempt);
+ }
+}
+
+/**
+ * Extension for alertTestUtils.
+ * Make sure that at the 4th attempt the correct password is used.
+ */
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc5034.js b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc5034.js
new file mode 100644
index 0000000000..20e63d1358
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3PasswordFailure_rfc5034.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+/**
+ * Tests password failure with RFC5034 auth.
+ *
+ * This test checks to see if the pop3 password failure is handled correctly
+ * in the case of the server dropping the connection during auth login.
+ * We use POP3_RFC5034_handler so auth=login will be supported.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var server;
+var daemon;
+var incomingServer;
+var attempt = 0;
+
+var kUserName = "testpop3";
+var kInvalidPassword = "pop3test";
+var kValidPassword = "testpop3";
+
+add_setup(async function () {
+ // Enable debug for the sign on.
+ Services.prefs.setBoolPref("signon.debug", true);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Set up the Server
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ var handler = new POP3_RFC5034_handler(d);
+ handler.dropOnAuthFailure = true;
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = kUserName;
+ handler.kPassword = kValidPassword;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ incomingServer = MailServices.accounts.createIncomingServer(
+ kUserName,
+ "localhost",
+ "pop3"
+ );
+
+ incomingServer.port = server.port;
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ daemon.setMessages(["message1.eml"]);
+});
+
+add_task(async function getMail1() {
+ // Now get the mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await Assert.rejects(
+ urlListener.promise,
+ reason => {
+ return reason === Cr.NS_ERROR_FAILURE;
+ },
+ "Check that wrong password is entered and thrown"
+ );
+ // We shouldn't have emails as the auth failed.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+ // Sanity check that we are at attempt 2.
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+});
+
+add_task(async function getMail2() {
+ // Now get the mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ gDummyMsgWindow,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ await urlListener.promise;
+ Assert.equal(attempt, 4);
+ // On the last attempt (4th), we should have successfully got one mail.
+ Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 1);
+
+ // Now check the new one has been saved.
+ let logins = Services.logins.findLogins(
+ "mailbox://localhost",
+ null,
+ "mailbox://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+});
+
+add_task(function endTest() {
+ // Cleanup for potential Sockets/Ports leakage.
+ server.stop();
+ server = null;
+ daemon = null;
+ incomingServer = null;
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+/* exported alert, confirmEx, promptPasswordPS */
+function alertPS(parent, aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ info("Alert Title: " + aDialogText + "\nAlert Text: " + aText);
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ info("Attempting retry");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ info("Cancelling login attempt");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ info("Attempting Retry");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ info("Enter new password");
+ return 2;
+ default:
+ throw new Error("unexpected attempt number " + attempt);
+ }
+}
+
+/**
+ * Extension for alertTestUtils.
+ * Make sure that at the 4th attempt the correct password is used.
+ */
+function promptPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3Proxy.js b/comm/mailnews/local/test/unit/test_pop3Proxy.js
new file mode 100644
index 0000000000..248bbaf92b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Proxy.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Test that POP3 over a proxy works.
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const PORT = 110;
+
+var server, daemon, incomingServer;
+
+add_setup(async function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ [daemon, server] = setupServerDaemon();
+ server.start();
+ NetworkTestUtils.configureProxy("pop.tinderbox.invalid", PORT, server.port);
+
+ // Set up the basic accounts and folders
+ incomingServer = createPop3ServerAndLocalFolders(
+ PORT,
+ "pop.tinderbox.invalid"
+ );
+
+ // Add a message to download
+ daemon.setMessages(["message1.eml"]);
+});
+
+add_task(async function downloadEmail() {
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0);
+
+ // Now get the mail
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ await urlListener.promise;
+
+ // We downloaded a message, so it works!
+ equal(localAccountUtils.inboxFolder.getTotalMessages(false), 1);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ incomingServer.closeCachedConnections();
+ server.stop();
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3Pump.js b/comm/mailnews/local/test/unit/test_pop3Pump.js
new file mode 100644
index 0000000000..239d284abb
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3Pump.js
@@ -0,0 +1,32 @@
+/**
+ * The intent of this file is to demonstrate a minimal
+ * POP3 unit test using the testing file POP3Pump.js
+ */
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var testSubjects = [
+ "[Bug 397009] A filter will let me tag, but not untag",
+ "Hello, did you receive my bugmail?",
+];
+
+add_task(async function runPump() {
+ // demonstration of access to the local inbox folder
+ dump(
+ "local inbox folder " + localAccountUtils.inboxFolder.URI + " is loaded\n"
+ );
+ // demonstration of access to the fake server
+ dump("Server " + gPOP3Pump.fakeServer.prettyName + " is loaded\n");
+
+ gPOP3Pump.files = ["../../../data/bugmail1", "../../../data/draft1"];
+ await gPOP3Pump.run();
+
+ // get message headers for the inbox folder
+ var msgCount = 0;
+ for (let hdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ msgCount++;
+ Assert.equal(hdr.subject, testSubjects[msgCount - 1]);
+ }
+ Assert.equal(msgCount, 2);
+ gPOP3Pump = null;
+});
diff --git a/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMDisconnect.js b/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMDisconnect.js
new file mode 100644
index 0000000000..ff13870352
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMDisconnect.js
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Server which advertises CRAM-MD5, but is impolite enough to just
+ * disconnect (close the TCP connection) when we try it.
+ *
+ * This is a tough one, because we may lose state on which auth schemes
+ * are allowed and which ones failed and may restart from scratch, and
+ * retry, never skipping the failed scheme.
+ * Dear server implementors, NEVER DO THAT! Be polite, give an error
+ * with explanation and by all means keep the connection open.
+ *
+ * I don't know if real servers do that, but bienvenu says they exist.
+ *
+ * TODO:
+ * This test shows that the current situation is not good.
+ * Problems:
+ * - We should reopen the connection, remember which auth scheme failed
+ * and start with the next in list, not trying the broken one again.
+ * We currently neither retry nor remember.
+ * - incomingServer thinks it is still running/busy although the connection is
+ * clearly done and over.
+ *
+ * @author Ben Bucksch
+ */
+
+var server;
+var daemon;
+var incomingServer;
+test =
+ "Server which advertises CRAM-MD5, but closes the connection when it's tried";
+// that's how it currently looks like (we fail to log in):
+var expectedTransaction = ["AUTH", "CAPA", "AUTH CRAM-MD5"];
+// TODO that's how it should look like (we start a new connection and try another scheme):
+// const expectedTransaction = ["AUTH", "CAPA", "AUTH CRAM-MD5", "CAPA", "AUTH PLAIN", "STAT"];
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ // We should be getting an error here, because we couldn't log in.
+ Assert.equal(result, Cr.NS_ERROR_FAILURE);
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, expectedTransaction);
+
+ do_timeout(0, endTest);
+ } catch (e) {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+ },
+};
+
+function endTest() {
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function CRAMFail_handler(daemon_) {
+ POP3_RFC5034_handler.call(this, daemon_);
+
+ this._kAuthSchemeStartFunction["CRAM-MD5"] = this.killConn;
+}
+CRAMFail_handler.prototype = {
+ __proto__: POP3_RFC5034_handler.prototype, // inherit
+
+ killConn() {
+ this.closing = true;
+ return "-ERR I don't feel like it";
+ },
+};
+
+function run_test() {
+ try {
+ do_test_pending();
+
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ return new CRAMFail_handler(d);
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ incomingServer = createPop3ServerAndLocalFolders(server.port);
+ let msgServer = incomingServer;
+ msgServer.QueryInterface(Ci.nsIMsgIncomingServer);
+ // Need to allow any auth here, although that's not use in TB really,
+ // because we need to fall back to something after CRAM-MD5 and
+ // check that login works after we fell back.
+ msgServer.authMethod = Ci.nsMsgAuthMethod.anything;
+
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMFail.js b/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMFail.js
new file mode 100644
index 0000000000..105f3fb0a0
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_pop3ServerBrokenCRAMFail.js
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Server which advertises CRAM-MD5, but fails when it's tried.
+ * This reportedly happens for some misconfigured servers.
+ */
+
+var server;
+var daemon;
+var incomingServer;
+test = "Server which advertises CRAM-MD5, but fails when it's tried";
+var expectedTransaction = [
+ "AUTH",
+ "CAPA",
+ "AUTH CRAM-MD5",
+ "AUTH PLAIN",
+ "STAT",
+];
+
+const kStateAuthNeeded = 1; // the same value as in Pop3d.jsm
+
+var urlListener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, result) {
+ try {
+ Assert.equal(result, 0);
+
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, expectedTransaction);
+
+ do_timeout(0, checkBusy);
+ } catch (e) {
+ server.stop();
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_throw(e);
+ }
+ },
+};
+
+function checkBusy() {
+ // If the server hasn't quite finished, just delay a little longer.
+ if (incomingServer.serverBusy) {
+ do_timeout(20, checkBusy);
+ return;
+ }
+
+ endTest();
+}
+
+function endTest() {
+ incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function CRAMFail_handler(daemon_) {
+ POP3_RFC5034_handler.call(this, daemon_);
+
+ this._kAuthSchemeStartFunction["CRAM-MD5"] = this.killConn;
+}
+CRAMFail_handler.prototype = {
+ __proto__: POP3_RFC5034_handler.prototype, // inherit
+
+ killConn() {
+ this._multiline = false;
+ this._state = kStateAuthNeeded;
+ return "-ERR I just pretended to implement CRAM-MD5";
+ },
+};
+
+function run_test() {
+ try {
+ do_test_pending();
+
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ return new CRAMFail_handler(d);
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ incomingServer = createPop3ServerAndLocalFolders(server.port);
+ let msgServer = incomingServer;
+ msgServer.QueryInterface(Ci.nsIMsgIncomingServer);
+ // Need to allow any auth here, although that's not use in TB really,
+ // because we need to fall back to something after CRAM-MD5 and
+ // check that login works after we fell back.
+ msgServer.authMethod = Ci.nsMsgAuthMethod.anything;
+
+ MailServices.pop3.GetNewMail(
+ null,
+ urlListener,
+ localAccountUtils.inboxFolder,
+ incomingServer
+ );
+ server.performTest();
+ } catch (e) {
+ server.stop();
+
+ do_throw(e);
+ } finally {
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_preview.js b/comm/mailnews/local/test/unit/test_preview.js
new file mode 100644
index 0000000000..83cfce256b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_preview.js
@@ -0,0 +1,40 @@
+var bugmail10 = do_get_file("../../../data/bugmail10");
+var bugmail11 = do_get_file("../../../data/bugmail11");
+var bugmail10_preview =
+ "Do not reply to this email. You can add comments to this bug at https://bugzilla.mozilla.org/show_bug.cgi?id=436880 -- Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email ------- You are receiving this mail because: -----";
+var bugmail11_preview =
+ "Bugzilla has received a request to create a user account using your email address (example@example.org). To confirm that you want to create an account using that email address, visit the following link: https://bugzilla.mozilla.org/token.cgi?t=xxx";
+
+function run_test() {
+ do_test_pending();
+ copyFileMessageInLocalFolder(bugmail10, 0, "", null, copy_next_message);
+}
+
+function copy_next_message(aMessageHeaderKeys, aStatus) {
+ copyFileMessageInLocalFolder(bugmail11, 0, "", null, test_preview);
+}
+
+function test_preview(aMessageHeaderKeys, aStatus) {
+ let headerKeys = aMessageHeaderKeys;
+ Assert.notEqual(headerKeys, null);
+ Assert.equal(headerKeys.length, 2);
+ try {
+ localAccountUtils.inboxFolder.fetchMsgPreviewText(headerKeys, null);
+ Assert.equal(
+ localAccountUtils.inboxFolder
+ .GetMessageHeader(headerKeys[0])
+ .getStringProperty("preview"),
+ bugmail10_preview
+ );
+ Assert.equal(
+ localAccountUtils.inboxFolder
+ .GetMessageHeader(headerKeys[1])
+ .getStringProperty("preview"),
+ bugmail11_preview
+ );
+ } catch (ex) {
+ dump(ex);
+ do_throw(ex);
+ }
+ do_test_finished();
+}
diff --git a/comm/mailnews/local/test/unit/test_saveMessage.js b/comm/mailnews/local/test/unit/test_saveMessage.js
new file mode 100644
index 0000000000..e488a0ed5b
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_saveMessage.js
@@ -0,0 +1,69 @@
+/**
+ * Test bug 460636 - Saving message in local folder as .EML removes starting dot in all lines, and ignores line if single dot only line.
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var MSG_LINEBREAK = "\r\n";
+var dot = do_get_file("data/dot");
+var saveFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+saveFile.append(dot.leafName + ".eml");
+saveFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+function run_test() {
+ registerCleanupFunction(teardown);
+ do_test_pending();
+ do_timeout(10000, function () {
+ do_throw(
+ "SaveMessageToDisk did not complete within 10 seconds" +
+ "(incorrect messageURI?). ABORTING."
+ );
+ });
+ copyFileMessageInLocalFolder(dot, 0, "", null, save_message);
+}
+
+async function save_message(aMessageHeaderKeys, aStatus) {
+ let headerKeys = aMessageHeaderKeys;
+ Assert.notEqual(headerKeys, null);
+
+ let message = localAccountUtils.inboxFolder.GetMessageHeader(headerKeys[0]);
+ let msgURI = localAccountUtils.inboxFolder.getUriForMsg(message);
+ let messageService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=mailbox-message"
+ ].getService(Ci.nsIMsgMessageService);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ messageService.SaveMessageToDisk(
+ msgURI,
+ saveFile,
+ false,
+ promiseUrlListener,
+ {},
+ true,
+ null
+ );
+ await promiseUrlListener.promise;
+ check_each_line(
+ await IOUtils.readUTF8(dot.path),
+ await IOUtils.readUTF8(saveFile.path)
+ );
+ do_test_finished();
+}
+
+function check_each_line(aExpectedLines, aActualLines) {
+ let expectedStrings = aExpectedLines.split(MSG_LINEBREAK);
+ let actualStrings = aActualLines.split(MSG_LINEBREAK);
+
+ expectedStrings.shift();
+ Assert.equal(expectedStrings.length, actualStrings.length);
+ for (let line = 0; line < expectedStrings.length; line++) {
+ Assert.equal(expectedStrings[line], actualStrings[line]);
+ }
+}
+
+function teardown() {
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+}
diff --git a/comm/mailnews/local/test/unit/test_streamHeaders.js b/comm/mailnews/local/test/unit/test_streamHeaders.js
new file mode 100644
index 0000000000..a1e7ef1640
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_streamHeaders.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/**
+ * This mainly tests that streamHeaders does not result in the crash
+ * of bug 752768
+ *
+ * adapted from test_pop3Pump.js by Kent James <kent@caspia.com>
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var testSubjects = ["Hello, did you receive my bugmail?"];
+
+var gHdr;
+
+add_task(async function loadMessages() {
+ let pop3Resolve;
+ let pop3Promise = new Promise(resolve => {
+ pop3Resolve = resolve;
+ });
+ gPOP3Pump.files = ["../../../data/draft1"];
+ gPOP3Pump.onDone = pop3Resolve;
+ gPOP3Pump.run();
+ await pop3Promise;
+
+ // Get message headers for the inbox folder.
+ var msgCount = 0;
+ for (gHdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ msgCount++;
+ Assert.equal(gHdr.subject, testSubjects[msgCount - 1]);
+ }
+ Assert.equal(msgCount, 1);
+ gPOP3Pump = null;
+});
+
+add_task(async function goodStreaming() {
+ // Try to stream the headers of the last message.
+ let uri = gHdr.folder.getUriForMsg(gHdr);
+ let messageService = MailServices.messageServiceFromURI(uri);
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ messageService.streamHeaders(uri, streamListener, null, true);
+ // The message contains this header.
+ let streamData = await streamListener.promise;
+ Assert.ok(
+ streamData.includes(
+ "X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0"
+ )
+ );
+});
+
+/**
+ * Crash from bug 752768.
+ */
+add_task(async function badStreaming() {
+ // Try to stream the headers of the last message.
+ let folder = gHdr.folder;
+ let uri = folder.getUriForMsg(gHdr);
+
+ let dbFile = folder.summaryFile;
+ // Force an invalid database.
+ folder.msgDatabase.forceClosed();
+ dbFile.remove(false);
+ folder.msgDatabase = null;
+
+ let messageService = MailServices.messageServiceFromURI(uri);
+ let haveError = false;
+ try {
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ messageService.streamHeaders(uri, streamListener, null, true);
+ await streamListener.promise;
+ } catch (e) {
+ // Should throw NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE (0x80550005).
+ haveError = true;
+ } finally {
+ Assert.ok(
+ haveError,
+ "Ensure that the stream crashes with NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE"
+ );
+ }
+});
diff --git a/comm/mailnews/local/test/unit/test_undoDelete.js b/comm/mailnews/local/test/unit/test_undoDelete.js
new file mode 100644
index 0000000000..a3a52e2fd5
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_undoDelete.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+// This file tests undoing of an local folder message deleted to the trash.
+//
+// Original Author: David Bienvenu <dbienvenu@mozilla.com>
+
+// Globals
+let gMsg1;
+let gMessages = [];
+let gMsgWindow;
+let gCurTestNum;
+let gMsgId1;
+let gTestFolder;
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var messageInjection = new MessageInjection({ mode: "local" });
+
+add_setup(async function () {
+ gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+ );
+
+ var messageGenerator = new MessageGenerator();
+ gMsg1 = messageGenerator.makeMessage();
+ let msg2 = messageGenerator.makeMessage({ inReplyTo: gMsg1 });
+
+ let messages = [];
+ messages = messages.concat([gMsg1, msg2]);
+ let msgSet = new SyntheticMessageSet(messages);
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+});
+
+add_task(async function deleteMessage() {
+ let msgToDelete = mailTestUtils.firstMsgHdr(gTestFolder);
+ gMsgId1 = msgToDelete.messageId;
+ gMessages.push(msgToDelete);
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ gTestFolder.deleteMessages(
+ gMessages,
+ gMsgWindow,
+ false,
+ true,
+ copyListener,
+ true
+ );
+ await copyListener.promise;
+});
+
+add_task(async function undoDelete() {
+ gMsgWindow.transactionManager.undoTransaction();
+ // There's no listener for this, so we'll just have to wait a little.
+ await PromiseTestUtils.promiseDelay(1500);
+});
+
+add_task(function verifyFolders() {
+ let msgRestored = gTestFolder.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+ let msg = mailTestUtils.loadMessageToString(gTestFolder, msgRestored);
+ Assert.equal(msg, gMsg1.toMboxString());
+});
+
+add_task(function endTest() {
+ // Cleanup, null out everything.
+ gMessages = [];
+ gMsgWindow.closeWindow();
+ gMsgWindow = null;
+ localAccountUtils.inboxFolder = null;
+ localAccountUtils.incomingServer = null;
+});
diff --git a/comm/mailnews/local/test/unit/test_verifyLogon.js b/comm/mailnews/local/test/unit/test_verifyLogon.js
new file mode 100644
index 0000000000..6993be7203
--- /dev/null
+++ b/comm/mailnews/local/test/unit/test_verifyLogon.js
@@ -0,0 +1,93 @@
+/**
+ * This test checks to see if the pop3 verify logon handles password failure correctly.
+ * The steps are:
+ * - Set an invalid password on the server object.
+ * - Check that verifyLogon fails
+ */
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var server;
+var daemon;
+var incomingServer;
+
+var kUserName = "testpop3";
+var kInvalidPassword = "pop3test";
+var kValidPassword = "testpop3";
+
+function verifyPop3Logon(validPassword) {
+ incomingServer.password = validPassword ? kValidPassword : kInvalidPassword;
+ urlListener.expectSuccess = validPassword;
+ let uri = incomingServer.verifyLogon(urlListener, gDummyMsgWindow);
+ // clear msgWindow so url won't prompt for passwords.
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null;
+
+ server.performTest();
+ return false;
+}
+
+var urlListener = {
+ expectSucess: false,
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, aResult) {
+ Assert.equal(Components.isSuccessCode(aResult), this.expectSuccess);
+ },
+};
+
+function actually_run_test() {
+ daemon.setMessages(["message1.eml"]);
+
+ // check that verifyLogon fails with bad password
+ verifyPop3Logon(false);
+
+ dump("\nverify logon false 1\n");
+ do_timeout(1000, verifyGoodLogon);
+}
+
+function verifyGoodLogon() {
+ server.resetTest();
+
+ // check that verifyLogon succeeds with good password
+ verifyPop3Logon(true);
+
+ dump("\nverify logon true 1\n");
+ do_test_finished();
+}
+
+function run_test() {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ // Set up the Server
+ daemon = new Pop3Daemon();
+ function createHandler(d) {
+ var handler = new POP3_RFC1939_handler(d);
+ // Login information needs to match the one stored in the signons json file.
+ handler.kUsername = kUserName;
+ handler.kPassword = kValidPassword;
+ handler.dropOnAuthFailure = true;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+
+ // Set up the basic accounts and folders.
+ // We would use createPop3ServerAndLocalFolders() however we want to have
+ // a different username and NO password for this test (as we expect to load
+ // it from the signons json file in which the login information is stored).
+ localAccountUtils.loadLocalMailAccount();
+
+ incomingServer = MailServices.accounts.createIncomingServer(
+ kUserName,
+ "localhost",
+ "pop3"
+ );
+ incomingServer.port = server.port;
+
+ do_test_pending();
+
+ actually_run_test();
+}
diff --git a/comm/mailnews/local/test/unit/xpcshell.ini b/comm/mailnews/local/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..43fd637fe3
--- /dev/null
+++ b/comm/mailnews/local/test/unit/xpcshell.ini
@@ -0,0 +1,57 @@
+[DEFAULT]
+head = head_maillocal.js
+tail =
+support-files = data/*
+prefs =
+ mail.biff.play_sound=false
+ mail.biff.show_alert=false
+ mail.biff.show_tray_icon=false
+ mail.biff.animate_dock_icon=false
+
+[test_bug457168.js]
+[test_duplicateKey.js]
+[test_fileName.js]
+[test_folderLoaded.js]
+[test_localFolder.js]
+[test_mailboxContentLength.js]
+[test_mailboxProtocol.js]
+[test_mailboxURL.js]
+[test_msgCopy.js]
+[test_msgIDParsing.js]
+[test_noTop.js]
+[test_noUidl.js]
+[test_nsIMsgLocalMailFolder.js]
+[test_nsIMsgParseMailMsgState.js]
+[test_nsIMsgPluggableStore.js]
+[test_over2GBMailboxes.js]
+[test_over4GBMailboxes.js]
+# This one needs a longer timeout for working with a 4GiB file.
+requesttimeoutfactor = 2
+[test_Pop3Channel.js]
+[test_pop3AuthMethods.js]
+[test_pop3Client.js]
+[test_pop3Download.js]
+[test_pop3DownloadTempFileHandling.js]
+[test_pop3Duplicates.js]
+[test_pop3FilterActions.js]
+[test_pop3Filters.js]
+[test_pop3GetNewMail.js]
+[test_pop3GSSAPIFail.js]
+[test_pop3MoveFilter.js]
+[test_pop3MoveFilter2.js]
+[test_pop3MultiCopy.js]
+[test_pop3MultiCopy2.js]
+[test_pop3Password.js]
+[test_pop3Password2.js]
+skip-if = true # realhostname and realuserName don't exist anymore
+[test_pop3Password3.js]
+[test_pop3PasswordFailure_rfc1939.js]
+[test_pop3PasswordFailure_rfc2449.js]
+[test_pop3PasswordFailure_rfc5034.js]
+[test_pop3Proxy.js]
+[test_pop3Pump.js]
+[test_preview.js]
+[test_saveMessage.js]
+[test_streamHeaders.js]
+[test_undoDelete.js]
+[test_verifyLogon.js]
diff --git a/comm/mailnews/mailnews.js b/comm/mailnews/mailnews.js
new file mode 100644
index 0000000000..badd0f5b85
--- /dev/null
+++ b/comm/mailnews/mailnews.js
@@ -0,0 +1,1150 @@
+#filter dumbComments emptyLines substitution
+
+// 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/.
+
+// SpaceHit() function: whether spacebar advances to next unread message.
+pref("mail.advance_on_spacebar", true);
+
+pref("mailnews.logComposePerformance", false);
+
+pref("mail.wrap_long_lines", true);
+
+// Show attachments of supported types rendered directly in the message body view.
+pref("mail.inline_attachments", true);
+// When rendering attachments inline, show also text attachments (e.g. CSV, HTML,
+// plain text) which are potentially very long.
+pref("mail.inline_attachments.text", false);
+pref("mail.reply_quote_inline", false);
+// When in a message the List-Post header contains the content of the Reply-To
+// (which is called "Reply-To Munging") we override the Reply-To header with
+// the From header.
+pref("mail.override_list_reply_to", true);
+// hidden pref for controlling if the Content-Language header
+// should be set.
+pref("mail.suppress_content_language", false);
+// Pref for controlling if the Date header is sanitized, by:
+// 1. Converting the date to UTC, to prevent leaking the local time zone.
+// 2. Rounding the date down to the most recent whole minute, to prevent
+// fingerprinting of small clock offsets.
+pref("mail.sanitize_date_header", false);
+
+// This determines the date/time format in the thread pane.
+// 0: (Short) Time only
+// 1: Long date and (short) time
+// 2: Short date and (short) time
+// 3: Unused, used to be Year/month and time, no very useful.
+// 4: Weekday and (short) time. Some people prefer this for "thisweek".
+pref("mail.ui.display.dateformat.default", 2);
+pref("mail.ui.display.dateformat.thisweek", 2);
+pref("mail.ui.display.dateformat.today", 0);
+
+// Is a user agent header sent in outgoing email messages?
+pref("mailnews.headers.sendUserAgent", true);
+
+// If sending the user agent header is enabled,
+// should only a minimal header be sent?
+pref("mailnews.headers.useMinimalUserAgent", true);
+
+// hidden pref for controlling if the user agent string
+// is displayed in the message pane or not...
+pref("mailnews.headers.showUserAgent", false);
+
+// hidden pref for controlling if the organization string
+// is displayed in the message pane or not...
+pref("mailnews.headers.showOrganization", false);
+
+// hidden pref for controlling if the references header
+// is displayed in the message pane or not...
+pref("mailnews.headers.showReferences", false);
+
+// hidden pref for controlling if the message-id header
+// is displayed in the message pane or not...
+pref("mailnews.headers.showMessageId", false);
+
+// hidden pref for controlling if the message to a message-id
+// is opened in a new window or in the same window
+pref("mailnews.messageid.openInNewWindow", false);
+
+// hidden pref for url which will be used to open message-ids
+// in browser (%mid ist replaced with the message-id)
+pref("mailnews.messageid_browser.url", "chrome://messenger-region/locale/region.properties");
+
+
+// hidden pref for whether or not to warn when deleting filters. Default YES
+pref("mailnews.filters.confirm_delete", true);
+
+// space-delimited list of extra headers to show in msg header display area.
+pref("mailnews.headers.extraExpandedHeaders", "");
+
+// Space-delimited list of extra headers that will be pushed to
+// currentHeaderData for processing in add-ons (without being displayed).
+// Use a value of "*" to get all headers (other wildcards not supported).
+pref("mailnews.headers.extraAddonHeaders", "");
+
+// default sort order settings (when creating new folder views)
+// sort_order is an int value reflecting nsMsgViewSortOrder values
+// as defined in nsIMsgDBView.idl (ascending = 1, descending = 2)
+// sort_type is an int value reflecting nsMsgViewSortType values
+// as defined in nsIMsgDBView.idl (byDate = 18, byId = 21 etc.)
+
+// for Mail/RSS/... (nsMsgDatabase)
+pref("mailnews.default_sort_order", 1);
+pref("mailnews.default_sort_type", 18);
+// for News (nsNewsDatabase)
+pref("mailnews.default_news_sort_order", 1);
+pref("mailnews.default_news_sort_type", 21);
+
+// hidden pref for whether "sort by date" and "sort by received date" in
+// threaded mode should be based on the newest message in the thread, or on
+// the thread root
+pref("mailnews.sort_threads_by_root", false);
+
+// default view flags for new folders
+// both flags are int values reflecting nsMsgViewFlagsType values
+// as defined in nsIMsgDBView.idl (kNone = 0, kThreadedDisplay = 1 etc.)
+
+// for Mail/RSS/... (nsMsgDatabase)
+pref("mailnews.default_view_flags", 1);
+// for News (nsNewsDatabase)
+pref("mailnews.default_news_view_flags", 1);
+
+// If true, delete will use the direction of the sort order
+// in determining the next message to select.
+pref("mail.delete_matches_sort_order", false);
+
+// mailnews tcp read+write timeout in seconds.
+pref("mailnews.tcptimeout", 100);
+
+pref("mailnews.headers.showSender", false);
+
+// set to 0 if you don't want to ignore timestamp differences between
+// local mail folders and the value stored in the corresponding .msf file.
+// 0 was the default up to and including 1.5. I've made the default
+// be greater than one hour so daylight savings time changes don't affect us.
+// We will still always regenerate .msf files if the file size changes.
+pref("mail.db_timestamp_leeway", 4000);
+// How long should we leave idle db's open, in milliseconds.
+pref("mail.db.idle_limit", 300000);
+// How many db's should we leave open? LRU db's will be closed first
+pref("mail.db.max_open", 30);
+
+// Should we allow folders over 4GB in size?
+pref("mailnews.allowMboxOver4GB", true);
+
+// For IMAP caching lift the limits since they are designed for HTML pages.
+// Note that the maximum size of a cache entry is limited by
+// max_entry_size and (capacity >> 3), so divided by 8.
+// Larger messages or attachments won't be cached.
+
+// 25 MB
+pref("browser.cache.memory.max_entry_size", 25000);
+// 200 MB = 8*25 MB
+pref("browser.cache.memory.capacity", 200000);
+
+pref("mail.imap.chunk_size", 65536);
+pref("mail.imap.min_chunk_size_threshold", 98304);
+pref("mail.imap.chunk_fast", 2);
+pref("mail.imap.chunk_ideal", 4);
+pref("mail.imap.chunk_add", 8192);
+pref("mail.imap.hide_other_users", false);
+pref("mail.imap.hide_unused_namespaces", true);
+pref("mail.imap.use_literal_plus", true);
+pref("mail.imap.expunge_after_delete", false);
+pref("mail.imap.check_deleted_before_expunge", false);
+pref("mail.imap.expunge_option", 0);
+pref("mail.imap.expunge_threshold_number", 20);
+pref("mail.imap.hdr_chunk_size", 200);
+// Should we filter imap messages based on new messages since the previous
+// highest UUID seen instead of unread?
+pref("mail.imap.filter_on_new", true);
+
+pref("mail.imap.tcp_keepalive.enabled", true);
+// For both items below if set less than 0 it means "use network.tcp.keepalive.*"
+// values. Or if set to 0, the value will be changed to 1, both in units of seconds.
+// Note: idle_time is the TCP keepalive idle time and not related to IMAP IDLE.
+pref("mail.imap.tcp_keepalive.idle_time", 100);
+pref("mail.imap.tcp_keepalive.retry_interval", 5);
+
+// if true, we assume that a user access a folder in the other users namespace
+// is acting as a delegate for that folder, and wishes to use the other users
+// identity when acting on messages in other users folders.
+pref("mail.imap.delegateOtherUsersFolders", false);
+// if false, only thread by subject if Re:
+pref("mail.thread_without_re", false);
+// if true, don't thread by subject at all
+pref("mail.strict_threading", true);
+// if true, makes sure threading works correctly always (see bug 181446)
+pref("mail.correct_threading", true);
+pref("mail.pop3.deleteFromServerOnMove", false);
+pref("mail.fixed_width_messages", true);
+#ifdef MOZ_SUITE
+// quoted color
+pref("mail.citation_color", "#000000");
+#else
+// quoted color
+pref("mail.citation_color", "#007cff");
+#endif
+// If true, remove the everything after the "-- \n" signature delimiter when replying.
+pref("mail.strip_sig_on_reply", true);
+// 0=plain, 1=bold, 2=italic, 3=bolditalic
+pref("mail.quoted_style", 0);
+// 0=normal, 1=big, 2=small
+pref("mail.quoted_size", 0);
+// use HTML-style quoting for displaying plain text
+pref("mail.quoted_graphical", true);
+// use HTML-style quoting for quoting plain text
+pref("mail.quoteasblock", true);
+pref("mail.strictly_mime", false);
+pref("mail.strictly_mime_headers", true);
+// The maximum number of entries in the "Recent" menu of the folder picker.
+pref("mail.folder_widget.max_recent", 25);
+// 0/1 (name param is encoded in a legacy way), 2(RFC 2231 only)
+// 0 the name param is never separated to multiple lines.
+pref("mail.strictly_mime.parm_folding", 1);
+pref("mail.label_ascii_only_mail_as_us_ascii", false);
+pref("mail.file_attach_binary", false);
+pref("mail.show_headers", 1);
+// some S/MIME parts are not external (but inline decrypted).
+pref("mailnews.p7m_external", false);
+pref("mail.pane_config.dynamic", 0);
+#ifdef MOZ_SUITE
+pref("mail.addr_book.mapit_url.format", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.1.name", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.1.format", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.2.name", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.2.format", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.3.name", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.3.format", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.4.name", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.4.format", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.5.name", "chrome://messenger-region/locale/region.properties");
+pref("mail.addr_book.mapit_url.5.format", "chrome://messenger-region/locale/region.properties");
+pref("mailnews.start_page.url", "chrome://messenger-region/locale/region.properties");
+pref("mail.accountwizard.deferstorage", false);
+// |false|: Show both name and address, even for people in my addressbook.
+pref("mail.showCondensedAddresses", false);
+#endif
+
+pref("mail.addr_book.loglevel", "Warn");
+pref("mail.addr_book.view.startupURI", "moz-abdirectory://?");
+pref("mail.addr_book.view.startupURIisDefault", true);
+
+pref("carddav.setup.loglevel", "Warn");
+pref("carddav.sync.loglevel", "Warn");
+
+// mail.addr_book.quicksearchquery.format is the model query used for:
+// * TB: AB Quick Search and composition's Contact Side Bar
+// * SM: AB Quick Search and composition's Select Addresses dialogue
+//
+// The format for "mail.addr_book.quicksearchquery.format" is:
+// @V == the escaped value typed in the quick search bar in the address book
+// c == contains | bw == beginsWith | ...
+//
+// Note, changing the fields searched might require changing labels:
+// SearchNameOrEmail.label in messenger.dtd,
+// searchNameAndEmail.emptytext in abMainWindow.dtd, etc.
+//
+// mail.addr_book.quicksearchquery.format will be used if mail.addr_book.show_phonetic_fields is "false"
+pref("mail.addr_book.quicksearchquery.format", "(or(DisplayName,c,@V)(FirstName,c,@V)(LastName,c,@V)(NickName,c,@V)(PrimaryEmail,c,@V)(SecondEmail,c,@V)(and(IsMailList,=,TRUE)(Notes,c,@V))(Company,c,@V)(Department,c,@V)(JobTitle,c,@V)(WebPage1,c,@V)(WebPage2,c,@V))");
+// mail.addr_book.quicksearchquery.format.phonetic will be used if mail.addr_book.show_phonetic_fields is "true"
+pref("mail.addr_book.quicksearchquery.format.phonetic", "(or(DisplayName,c,@V)(FirstName,c,@V)(LastName,c,@V)(NickName,c,@V)(PrimaryEmail,c,@V)(SecondEmail,c,@V)(and(IsMailList,=,TRUE)(Notes,c,@V))(Company,c,@V)(Department,c,@V)(JobTitle,c,@V)(WebPage1,c,@V)(WebPage2,c,@V)(PhoneticFirstName,c,@V)(PhoneticLastName,c,@V))");
+
+// mail.addr_book.autocompletequery.format is the model query used for:
+// * TB: Recipient Autocomplete (composition, mailing list properties dialogue)
+// * SM: Recipient Autocomplete (composition, mailing list properties dialogue)
+//
+// mail.addr_book.autocompletequery.format will be used if mail.addr_book.show_phonetic_fields is "false"
+pref("mail.addr_book.autocompletequery.format", "(or(DisplayName,c,@V)(FirstName,c,@V)(LastName,c,@V)(NickName,c,@V)(PrimaryEmail,c,@V)(SecondEmail,c,@V)(and(IsMailList,=,TRUE)(Notes,c,@V)))");
+// mail.addr_book.autocompletequery.format.phonetic will be used if mail.addr_book.show_phonetic_fields is "true"
+pref("mail.addr_book.autocompletequery.format.phonetic", "(or(DisplayName,c,@V)(FirstName,c,@V)(LastName,c,@V)(NickName,c,@V)(PrimaryEmail,c,@V)(SecondEmail,c,@V)(and(IsMailList,=,TRUE)(Notes,c,@V))(PhoneticFirstName,c,@V)(PhoneticLastName,c,@V))");
+
+// values for "mail.addr_book.lastnamefirst" are:
+//0=displayname, 1=lastname first, 2=firstname first
+pref("mail.addr_book.lastnamefirst", 0);
+pref("mail.addr_book.displayName.autoGeneration", true);
+pref("mail.addr_book.show_phonetic_fields", "chrome://messenger/locale/messenger.properties");
+pref("mail.html_compose", true);
+// you can specify multiple, option headers
+// this will show up in the address picker in the compose window
+// examples: "X-Face" or "Approved,X-No-Archive"
+pref("mail.compose.other.header", "");
+pref("mail.compose.autosave", true);
+// interval in minutes
+pref("mail.compose.autosaveinterval", 5);
+pref("mail.compose.default_to_paragraph", false);
+
+// 0=auto, 1=plain, 2=html, 3=both
+pref("mail.default_send_format", 0);
+// 0: Never 1: Always 2: Ask me
+pref("mail.mdn.report.not_in_to_cc", 2);
+// 0: Never 1: Always 2: Ask me
+pref("mail.mdn.report.outside_domain", 2);
+// 0: Never 1: Always 2: Ask me 3: Denial
+pref("mail.mdn.report.other", 2);
+// 0: Inbox/filter 1: Sent folder
+pref("mail.incorporate.return_receipt", 0);
+// 1: DSN 2: MDN 3: Both
+pref("mail.request.return_receipt", 2);
+// 0: MDN-DNT header 1: RRT header 2: Both (MC)
+pref("mail.receipt.request_header_type", 0);
+pref("mail.receipt.request_return_receipt_on", false);
+// false: Never send true: Send sometimes
+pref("mail.mdn.report.enabled", true);
+
+pref("mail.dsn.always_request_on", false);
+// DSN request is sent with SUCCESS option
+pref("mail.dsn.request_on_success_on", true);
+// DSN request is sent with FAILURE option
+pref("mail.dsn.request_on_failure_on", true);
+// DSN request is sent with DELAY option
+pref("mail.dsn.request_on_delay_on", true);
+// DSN request is not sent with NEVER option
+pref("mail.dsn.request_never_on", false);
+// DSN request is sent with RET FULL option
+pref("mail.dsn.ret_full_on", true);
+
+// false: Use global true: Use custom
+pref("mail.identity.default.dsn_use_custom_prefs", false);
+pref("mail.identity.default.dsn_always_request_on", false);
+
+pref("news.show_size_in_lines", true);
+pref("news.update_unread_on_expand", true);
+pref("news.get_messages_on_select", true);
+
+// list new groups created in the last number of days
+pref("news.newgroups_for_num_days", 180);
+
+pref("mailnews.wraplength", 72);
+
+// 0=no header, 1="<author> wrote:", 2="On <date> <author> wrote:", 3="<author> wrote On <date>:", 4=user specified
+pref("mailnews.reply_header_type", 1);
+pref("mailnews.reply_header_authorwrotesingle", "chrome://messenger/locale/messengercompose/composeMsgs.properties");
+pref("mailnews.reply_header_ondateauthorwrote", "chrome://messenger/locale/messengercompose/composeMsgs.properties");
+pref("mailnews.reply_header_authorwroteondate", "chrome://messenger/locale/messengercompose/composeMsgs.properties");
+pref("mailnews.reply_header_originalmessage", "chrome://messenger/locale/messengercompose/composeMsgs.properties");
+pref("mailnews.forward_header_originalmessage", "chrome://messenger/locale/messengercompose/composeMsgs.properties");
+
+pref("mailnews.reply_to_self_check_all_ident", true);
+
+pref("mailnews.reply_quoting_selection", true);
+pref("mailnews.reply_quoting_selection.only_if_chars", "");
+pref("mailnews.reply_quoting_selection.multi_word", true);
+
+pref("mailnews.smtp.loglevel", "Warn");
+
+pref("mailnews.nntp.loglevel", "Warn");
+
+pref("mailnews.pop3.loglevel", "Warn");
+
+// If true, ImapService.jsm is used. Otherwise, nsImapService.cpp is used.
+pref("mailnews.imap.jsmodule", false);
+pref("mailnews.imap.loglevel", "Warn");
+
+pref("mail.operate_on_msgs_in_collapsed_threads", false);
+pref("mail.warn_on_collapsed_thread_operation", true);
+pref("mail.warn_on_shift_delete", true);
+pref("news.warn_on_delete", true);
+pref("mail.warn_on_delete_from_trash", true);
+pref("mail.purge_threshhold_mb", 200);
+pref("mail.prompt_purge_threshhold", true);
+pref("mail.purge.ask", true);
+
+pref("mailnews.offline_sync_mail", false);
+pref("mailnews.offline_sync_news", false);
+pref("mailnews.offline_sync_send_unsent", true);
+pref("mailnews.offline_sync_work_offline", false);
+pref("mailnews.force_ascii_search", false);
+
+// AppleDouble is causing problems with some webmail clients and Microsoft mail servers
+// rejecting a MIME part of multipart/appledouble. Mac uses resource forks less and less
+// so we only use AppleDouble if the file has no extension or its extension is whitelisted below.
+// "" (default) - AppleDouble won't be used if the file has an extension
+// "*" - AppleDouble will always be used
+// Comma-separated list of extensions for which to use AppleDouble, for example "doc,xls" (not-case sensitive).
+pref("mailnews.extensions_using_appledouble", "");
+pref("mailnews.localizedRe", "chrome://messenger-region/locale/region.properties");
+
+pref("mailnews.search_date_format", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.search_date_separator", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.search_date_leading_zeros", "chrome://messenger/locale/messenger.properties");
+// used to decide whether to migrate global quoting prefs
+pref("mailnews.quotingPrefs.version", 0);
+
+// the first time, we'll warn the user about the blind send, and they can disable the warning if they want.
+pref("mapi.blind-send.enabled", true);
+// automatically move the user offline or online based on the network connection
+pref("offline.autoDetect", false);
+
+pref("ldap_2.autoComplete.useDirectory", false);
+pref("ldap_2.autoComplete.directoryServer", "");
+
+pref("ldap_2.servers.pab.position", 1);
+pref("ldap_2.servers.pab.description", "chrome://messenger/locale/addressbook/addressBook.properties");
+pref("ldap_2.servers.pab.dirType", 101);
+pref("ldap_2.servers.pab.filename", "abook.sqlite");
+pref("ldap_2.servers.pab.isOffline", false);
+
+pref("ldap_2.servers.history.position", 2);
+pref("ldap_2.servers.history.description", "chrome://messenger/locale/addressbook/addressBook.properties");
+pref("ldap_2.servers.history.dirType", 101);
+pref("ldap_2.servers.history.filename", "history.sqlite");
+pref("ldap_2.servers.history.isOffline", false);
+
+// default mapping of addressbook properties to ldap attributes
+pref("ldap_2.servers.default.attrmap.FirstName", "givenName");
+pref("ldap_2.servers.default.attrmap.LastName", "sn,surname");
+pref("ldap_2.servers.default.attrmap.DisplayName", "cn,commonname");
+pref("ldap_2.servers.default.attrmap.NickName", "mozillaNickname,xmozillanickname");
+pref("ldap_2.servers.default.attrmap.PrimaryEmail", "mail");
+pref("ldap_2.servers.default.attrmap.SecondEmail", "mozillaSecondEmail,xmozillasecondemail");
+pref("ldap_2.servers.default.attrmap.WorkPhone", "telephoneNumber");
+pref("ldap_2.servers.default.attrmap.HomePhone", "homePhone");
+pref("ldap_2.servers.default.attrmap.FaxNumber", "facsimiletelephonenumber,fax");
+pref("ldap_2.servers.default.attrmap.PagerNumber", "pager,pagerphone");
+pref("ldap_2.servers.default.attrmap.CellularNumber", "mobile,cellphone,carphone");
+pref("ldap_2.servers.default.attrmap.WorkAddress", "street,streetaddress,postOfficeBox");
+pref("ldap_2.servers.default.attrmap.HomeAddress", "mozillaHomeStreet");
+pref("ldap_2.servers.default.attrmap.WorkAddress2", "mozillaWorkStreet2");
+pref("ldap_2.servers.default.attrmap.HomeAddress2", "mozillaHomeStreet2");
+pref("ldap_2.servers.default.attrmap.WorkCity", "l,locality");
+pref("ldap_2.servers.default.attrmap.HomeCity", "mozillaHomeLocalityName");
+pref("ldap_2.servers.default.attrmap.WorkState", "st,region");
+pref("ldap_2.servers.default.attrmap.HomeState", "mozillaHomeState");
+pref("ldap_2.servers.default.attrmap.WorkZipCode", "postalCode,zip");
+pref("ldap_2.servers.default.attrmap.HomeZipCode", "mozillaHomePostalCode");
+pref("ldap_2.servers.default.attrmap.WorkCountry", "c,countryname");
+pref("ldap_2.servers.default.attrmap.HomeCountry", "mozillaHomeCountryName");
+pref("ldap_2.servers.default.attrmap.JobTitle", "title");
+pref("ldap_2.servers.default.attrmap.Department", "ou,department,departmentnumber,orgunit");
+pref("ldap_2.servers.default.attrmap.Company", "o,company");
+pref("ldap_2.servers.default.attrmap._AimScreenName", "nsAIMid,nscpaimscreenname");
+pref("ldap_2.servers.default.attrmap.WebPage1", "mozillaWorkUrl,workurl,labeledURI");
+pref("ldap_2.servers.default.attrmap.WebPage2", "mozillaHomeUrl,homeurl");
+pref("ldap_2.servers.default.attrmap.BirthYear", "birthyear");
+pref("ldap_2.servers.default.attrmap.BirthMonth", "birthmonth");
+pref("ldap_2.servers.default.attrmap.BirthDay", "birthday");
+pref("ldap_2.servers.default.attrmap.Custom1", "mozillaCustom1,custom1");
+pref("ldap_2.servers.default.attrmap.Custom2", "mozillaCustom2,custom2");
+pref("ldap_2.servers.default.attrmap.Custom3", "mozillaCustom3,custom3");
+pref("ldap_2.servers.default.attrmap.Custom4", "mozillaCustom4,custom4");
+pref("ldap_2.servers.default.attrmap.Notes", "description,notes");
+pref("ldap_2.servers.default.attrmap.LastModifiedDate", "modifytimestamp");
+
+pref("ldap_2.user_id", 0);
+// Update kCurrentListVersion in include/dirprefs.h if you change this
+pref("ldap_2.version", 3);
+
+pref("mailnews.ldap.loglevel", "Warn");
+
+pref("mailnews.confirm.moveFoldersToTrash", true);
+
+// space-delimited list of extra headers to add to .msf file
+pref("mailnews.customDBHeaders", "");
+
+// close standalone message window when deleting the displayed message
+pref("mail.close_message_window.on_delete", false);
+
+#ifdef MOZ_SUITE
+pref("mailnews.reuse_message_window", true);
+#endif
+// warn user if they attempt to open more than this many messages at once
+pref("mailnews.open_window_warning", 10);
+// warn user if they attempt to open more than this many messages at once
+pref("mailnews.open_tab_warning", 20);
+
+pref("mailnews.start_page.enabled", true);
+
+pref("mailnews.remember_selected_message", true);
+pref("mailnews.scroll_to_new_message", true);
+
+// if true, any click on a column header other than the thread column will unthread the view
+pref("mailnews.thread_pane_column_unthreads", false);
+
+/* default prefs for Mozilla 5.0 */
+pref("mail.identity.default.compose_html", true);
+pref("mail.identity.default.valid", true);
+pref("mail.identity.default.fcc", true);
+pref("mail.identity.default.fcc_folder", "mailbox://nobody@Local%20Folders/Sent");
+pref("mail.identity.default.fcc_reply_follows_parent", false);
+pref("mail.identity.default.autocompleteToMyDomain", false);
+
+pref("mail.identity.default.archive_enabled", true);
+// archive into 0: single folder, 1: yearly folder, 2: year/year-month folder
+pref("mail.identity.default.archive_granularity", 1);
+pref("mail.identity.default.archive_keep_folder_structure", false);
+
+// keep these defaults for backwards compatibility and migration
+
+// but .doBcc and .doBccList are the right ones from now on.
+pref("mail.identity.default.bcc_self", false);
+pref("mail.identity.default.bcc_others", false);
+pref("mail.identity.default.bcc_list", "");
+
+pref("mail.identity.default.draft_folder", "mailbox://nobody@Local%20Folders/Drafts");
+pref("mail.identity.default.stationery_folder", "mailbox://nobody@Local%20Folders/Templates");
+pref("mail.identity.default.directoryServer", "");
+pref("mail.identity.default.overrideGlobal_Pref", false);
+pref("mail.identity.default.auto_quote", true);
+// 0=bottom 1=top 2=select
+pref("mail.identity.default.reply_on_top", 0);
+// true=below quoted false=above quoted
+pref("mail.identity.default.sig_bottom", true);
+// Include signature on fwd?
+pref("mail.identity.default.sig_on_fwd", false);
+// Include signature on re?
+pref("mail.identity.default.sig_on_reply", true);
+
+// Suppress double-dash signature separator
+pref("mail.identity.default.suppress_signature_separator", false);
+
+// default to archives folder on same server.
+pref("mail.identity.default.archives_folder_picker_mode", "0");
+
+// Headers to always add to outgoing mail
+// examples: "header1,header2"
+// pref("mail.identity.id1.headers", "header1");
+// user_pref("mail.identity.id1.header.header1", "X-Mozilla-Rocks: True")
+pref("mail.identity.default.headers", "");
+
+// by default, only collect addresses the user sends to (outgoing)
+// incoming is all spam anyways
+#ifdef MOZ_SUITE
+pref("mail.collect_email_address_incoming", false);
+pref("mail.collect_email_address_newsgroup", false);
+#endif
+pref("mail.collect_email_address_outgoing", true);
+// by default, use the Collected Addressbook for collection
+pref("mail.collect_addressbook", "jsaddrbook://history.sqlite");
+
+pref("mail.default_sendlater_uri", "mailbox://nobody@Local%20Folders/Unsent%20Messages");
+
+pref("mail.server.default.clientid", "");
+pref("mail.smtpserver.default.clientid", "");
+
+// This is not to be enabled by default until the prerequisite
+// changes are completed. See here for details:
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1565379
+pref("mail.server.default.clientidEnabled", false);
+pref("mail.smtpserver.default.clientidEnabled", false);
+pref("mail.smtpserver.default.max_cached_connections", 3);
+
+pref("mail.smtpservers", "");
+pref("mail.accountmanager.accounts", "");
+
+// Last used account key value
+pref("mail.account.lastKey", 0);
+
+pref("mail.server.default.port", -1);
+pref("mail.server.default.offline_support_level", -1);
+pref("mail.server.default.leave_on_server", false);
+pref("mail.server.default.download_on_biff", false);
+pref("mail.server.default.check_time", 10);
+pref("mail.server.default.delete_by_age_from_server", false);
+pref("mail.server.default.num_days_to_leave_on_server", 7);
+pref("mail.server.default.limit_offline_message_size", false);
+pref("mail.server.default.max_size", 50);
+pref("mail.server.default.delete_mail_left_on_server", false);
+pref("mail.server.default.valid", true);
+pref("mail.server.default.abbreviate", true);
+pref("mail.server.default.isSecure", false);
+// cleartext password. @see nsIMsgIncomingServer.authMethod.
+pref("mail.server.default.authMethod", 3);
+// @see nsIMsgIncomingServer.socketType
+pref("mail.server.default.socketType", 0);
+pref("mail.server.default.override_namespaces", true);
+pref("mail.server.default.deferred_to_account", "");
+
+pref("mail.server.default.delete_model", 1);
+pref("mail.server.default.fetch_by_chunks", true);
+// Send IMAP RFC 2971 ID Info to server
+pref("mail.server.default.send_client_info", true);
+pref("mail.server.default.always_authenticate", false);
+pref("mail.server.default.singleSignon", true);
+pref("mail.server.default.max_articles", 500);
+pref("mail.server.default.notify.on", true);
+pref("mail.server.default.mark_old_read", false);
+pref("mail.server.default.empty_trash_on_exit", false);
+// 0 = Keep Dupes, leave them alone
+// 1 = delete dupes
+// 2 = Move Dupes to trash
+// 3 = Mark Dupes as Read
+pref("mail.server.default.dup_action", 0);
+pref("mail.server.default.hidden", false);
+
+pref("mail.server.default.using_subscription", true);
+pref("mail.server.default.dual_use_folders", true);
+pref("mail.server.default.canDelete", false);
+pref("mail.server.default.login_at_startup", false);
+pref("mail.server.default.allows_specialfolders_usage", true);
+pref("mail.server.default.canCreateFolders", true);
+pref("mail.server.default.canFileMessages", true);
+
+// special enhancements for IMAP servers
+pref("mail.server.default.is_gmail", false);
+pref("mail.server.default.use_idle", true);
+// in case client or server has bugs in condstore implementation
+pref("mail.server.default.use_condstore", false);
+// in case client or server has bugs in compress implementation
+pref("mail.server.default.use_compress_deflate", true);
+// for spam
+// 0 off, 100 on. not doing bool since we might have real levels one day.
+pref("mail.server.default.spamLevel", 100);
+pref("mail.server.default.moveOnSpam", false);
+// 0 == "Junk" on server, 1 == specific folder
+pref("mail.server.default.moveTargetMode", 0);
+pref("mail.server.default.spamActionTargetAccount", "");
+pref("mail.server.default.spamActionTargetFolder", "");
+pref("mail.server.default.useWhiteList", true);
+// the Personal addressbook.
+pref("mail.server.default.whiteListAbURI", "jsaddrbook://abook.sqlite");
+pref("mail.server.default.useServerFilter", false);
+pref("mail.server.default.serverFilterName", "SpamAssassin");
+// 1 == trust positives, 2 == trust negatives, 3 == trust both
+pref("mail.server.default.serverFilterTrustFlags", 1);
+pref("mail.server.default.purgeSpam", false);
+// 14 days
+pref("mail.server.default.purgeSpamInterval", 14);
+pref("mail.server.default.check_all_folders_for_new", false);
+// should we inhibit whitelisting of the email addresses for a server's identities?
+pref("mail.server.default.inhibitWhiteListingIdentityUser", true);
+// should we inhibit whitelisting of the domain for a server's identities?
+pref("mail.server.default.inhibitWhiteListingIdentityDomain", false);
+
+// For sending imap SELECT when checking for new mail. Has been needed by some
+// servers that don't properly support imap NOOP for new mail detection.
+pref("mail.server.default.force_select_imap", false);
+
+// to activate auto-sync feature (preemptive message download for imap) by default
+pref("mail.server.default.autosync_offline_stores",true);
+pref("mail.server.default.offline_download",true);
+
+// -1 means no limit, no purging of offline stores.
+pref("mail.server.default.autosync_max_age_days", -1);
+
+// Can we change the store type without conversion? (=has the store been used)
+pref("mail.server.default.canChangeStoreType", false);
+
+// Enable use of imap capability UTF8=ACCEPT described in RFC 6855.
+pref("mail.server.default.allow_utf8_accept", true);
+
+// Store conversion (mbox <-> maildir)
+#ifndef RELEASE_OR_BETA
+pref("mail.store_conversion_enabled", true);
+#else
+pref("mail.store_conversion_enabled", false);
+#endif
+
+// Time between applications of periodic filters
+pref("mail.server.default.periodicFilterRateMinutes", 10);
+
+pref("mail.periodicfilters.loglevel", "Warn");
+
+// This is the default store contractID for newly created servers.
+// We don't use mail.server.default because we want to ensure that the
+// store contract id is always written out to prefs.js
+pref("mail.serverDefaultStoreContractID", "@mozilla.org/msgstore/berkeleystore;1");
+// the probablilty threshold over which messages are classified as junk
+// this number is divided by 100 before it is used. The classifier can be fine tuned
+// by changing this pref. Typical values are .99, .95, .90, .5, etc.
+pref("mail.adaptivefilters.junk_threshold", 90);
+// used to determine when to migrate global spam settings
+pref("mail.spam.version", 0);
+pref("mail.spam.logging.enabled", false);
+pref("mail.spam.manualMark", false);
+pref("mail.spam.markAsReadOnSpam", false);
+// 0 == "move to junk folder", 1 == "delete"
+pref("mail.spam.manualMarkMode", 0);
+pref("mail.spam.markAsNotJunkMarksUnRead", true);
+// display simple html for html junk messages
+pref("mail.spam.display.sanitize", true);
+// the number of allowed bayes tokens before the database is shrunk
+pref("mailnews.bayesian_spam_filter.junk_maxtokens", 100000);
+
+// pref to warn the users of exceeding the size of the message being composed. (Default 20MB).
+pref("mailnews.message_warning_size", 20971520);
+
+// set default traits for junk and good. Index should match the values in nsIJunkMailPlugin
+pref("mailnews.traits.id.1", "mailnews@mozilla.org#good");
+pref("mailnews.traits.name.1", "Good");
+pref("mailnews.traits.enabled.1", false);
+pref("mailnews.traits.id.2", "mailnews@mozilla.org#junk");
+pref("mailnews.traits.name.2", "Junk");
+pref("mailnews.traits.enabled.2", true);
+pref("mailnews.traits.antiId.2", "mailnews@mozilla.org#good");
+// traits 3 - 1000 are reserved for use by mailnews@mozilla.org
+// the first externally defined trait will have index 1001
+pref("mailnews.traits.lastIndex", 1000);
+
+// Show extra column in address entry. This is numeric for
+// historical reasons: 0 = no extra column, 1 = show extra column.
+pref("mail.autoComplete.commentColumn", 0);
+
+// if true, we'll use the password from an incoming server with
+// matching username and domain
+pref("mail.smtp.useMatchingDomainServer", false);
+
+// if true, we'll use the password from an incoming server with
+// matching username and host name
+pref("mail.smtp.useMatchingHostNameServer", false);
+
+// if true, we'll use the email sender's address for the smtp
+// MAIL FROM, which might become the return-path. If false
+// we use the identity email address, which is the old behaviour
+pref("mail.smtp.useSenderForSmtpMailFrom", true);
+// cleartext password. @see nsIMsgIncomingServer.authMethod.
+pref("mail.smtpserver.default.authMethod", 3);
+// @see nsISmtpServer.socketType
+pref("mail.smtpserver.default.try_ssl", 0);
+
+// If true, SMTP LOGIN auth and POP3 USER/PASS auth, the last of the methods to try, will use Latin1.
+pref("mail.smtp_login_pop3_user_pass_auth_is_latin1", true);
+
+// Strip CSS conditional rules in received and sent mail
+pref("mail.html_sanitize.drop_conditional_css", true);
+
+// For the next 3 prefs, see <http://www.bucksch.org/1/projects/mozilla/16507>
+// TXT->HTML :-) etc. in viewer
+pref("mail.display_glyph", true);
+// TXT->HTML *bold* etc. in viewer; ditto
+pref("mail.display_struct", true);
+// HTML->HTML *bold* etc. during Send; ditto
+pref("mail.send_struct", false);
+// display time and date using senders timezone in message pane and when printing
+pref("mailnews.display.date_senders_timezone", false);
+// For the next 4 prefs, see <http://www.bucksch.org/1/projects/mozilla/108153>
+// Ignore HTML parts in multipart/alternative
+pref("mailnews.display.prefer_plaintext", false);
+// How to display HTML/MIME parts.
+// 0 = Render the sender's HTML;
+// 1 = HTML->TXT->HTML;
+// 2 = Show HTML source;
+// 3 = Sanitize HTML;
+// 4 = Show all body parts
+pref("mailnews.display.html_as", 0);
+// Whether the View > Message body as > All body parts menu item is available
+pref("mailnews.display.show_all_body_parts_menu", false);
+// whether to drop <font>, <center>, align='...', etc.
+pref("mailnews.display.html_sanitizer.drop_non_css_presentation", true);
+// whether to drop <img>, <video> and <audio>
+pref("mailnews.display.html_sanitizer.drop_media", false);
+// Let only a few classes process incoming data. This protects from bugs (e.g. buffer overflows) and from security loopholes
+// (e.g. allowing unchecked HTML in some obscure classes, although the user has html_as > 0).
+pref("mailnews.display.disallow_mime_handlers", 0);
+// This option is mainly for the UI of html_as.
+// 0 = allow all available classes
+// 1 = Use hardcoded blacklist to avoid rendering (incoming) HTML
+// 2 = ... and inline images
+// 3 = ... and some other uncommon content types
+// 100 = Use hardcoded whitelist to avoid even more bugs(buffer overflows).
+// This mode will limit the features available (e.g. uncommon
+// attachment types and inline images) and is for paranoid users.
+
+// RSS rendering options, see prior 4 prefs above.
+pref("rss.display.prefer_plaintext", false);
+pref("rss.display.html_as", 0);
+pref("rss.display.disallow_mime_handlers", 0);
+
+// Feed message display (summary or web page), on select.
+// 0 - global override, load web page
+// 1 - global override, load summary
+// 2 - use default feed folder setting from Subscribe dialog; if no setting default to 1
+pref("rss.show.summary", 1);
+
+// Feed message display (summary or web page), on open.
+// Action on double click or enter in threadpane for a feed message.
+// 0 - open content-base url in new window
+// 1 - open summary in new window
+// 2 - toggle load summary and content-base url in message pane
+// 3 - load content-base url in browser
+pref("rss.show.content-base", 0);
+
+// Feed message additional web page display.
+// 0 - no action
+// 1 - load web page in default browser, on select
+pref("rss.message.loadWebPageOnSelect", 0);
+
+// Feed auto updates / "Pause Updates"
+// true = If updating a feed results in an error code, disable the feed until next manual check or application restart.
+// false = Keep feed automatic updates scheduled, even if the feed source responds with an error code.
+pref("rss.disable_feeds_on_update_failure", true);
+
+pref("feeds.loglevel", "Warn");
+
+// 0=default as attachment
+// 1=forward as quoted (mapped to 2 in mozilla)
+// 2=forward as inline with attachments, (obsolete 4.x value)
+pref("mail.forward_message_mode", 0);
+// add .eml extension when forwarding as attachment
+pref("mail.forward_add_extension", true);
+// Prefix of for mail forwards. E.g. "Fwd" -> subject will be Fwd: <subject>
+pref("mail.forward_subject_prefix", "Fwd");
+
+pref("mail.startup.enabledMailCheckOnce", false);
+// RFC 2646=======
+pref("mailnews.send_plaintext_flowed", true);
+pref("mailnews.display.disable_format_flowed_support", false);
+// prompt user when crossing folders
+pref("mailnews.nav_crosses_folders", 1);
+
+// these two news.cancel.* prefs are for use by QA for automated testing. see bug #31057
+pref("news.cancel.confirm", true);
+pref("news.cancel.alert_on_success", true);
+pref("mail.SpellCheckBeforeSend", false);
+pref("mail.spellcheck.inline", true);
+// enable / disable phishing detection for link clicks
+pref("mail.phishing.detection.enabled", true);
+pref("mail.warn_on_send_accel_key", true);
+pref("mail.enable_autocomplete", true);
+pref("mailnews.global_html_domains.version", 1);
+
+/////////////////////////////////////////////////////////////////
+// Privacy Controls for Handling Remote Content
+/////////////////////////////////////////////////////////////////
+pref("mailnews.message_display.disable_remote_image", true);
+
+/////////////////////////////////////////////////////////////////
+// Trusted Mail Domains
+//
+// Specific domains can be white listed to bypass various privacy controls in Thunderbird
+// such as blocking remote images, the phishing detector, etc. This is particularly
+// useful for business deployments where images or links reference servers inside a
+// corporate intranet. For multiple domains, separate them with a comma. i.e.
+// pref("mail.trusteddomains", "mozilla.org,mozillafoundation.org");
+/////////////////////////////////////////////////////////////////
+pref("mail.trusteddomains", "");
+
+pref("mail.imap.use_status_for_biff", true);
+// in percent. when the quota meter starts showing up at all. decrease this for it to be more than a warning.
+pref("mail.quota.mainwindow_threshold.show", 75);
+// when it gets yellow
+pref("mail.quota.mainwindow_threshold.warning", 80);
+// when it gets red
+pref("mail.quota.mainwindow_threshold.critical", 95);
+
+// Pref controlling the updates on the pre-configured accounts.
+// In order to add new pre-configured accounts (after a version),
+// increase the following version number besides updating the
+// pref mail.accountmanager.appendaccounts
+pref("mailnews.append_preconfig_accounts.version", 1);
+
+// Pref controlling the updates on the pre-configured smtp servers.
+// In order to add new pre-configured smtp servers (after a version),
+// increase the following version number besides updating the
+// pref mail.smtpservers.appendsmtpservers
+pref("mail.append_preconfig_smtpservers.version", 1);
+
+pref("mail.biff.alert.show_preview", true);
+pref("mail.biff.alert.show_subject", true);
+pref("mail.biff.alert.show_sender", true);
+pref("mail.biff.alert.preview_length", 40);
+
+#ifdef XP_MACOSX
+pref("mail.biff.play_sound", false);
+#else
+pref("mail.biff.play_sound", true);
+#endif
+// 0 == default system sound, 1 == user specified wav
+pref("mail.biff.play_sound.type", 0);
+// _moz_mailbeep is a magic key, for the default sound.
+// otherwise, this needs to be a file url
+pref("mail.biff.play_sound.url", "");
+pref("mail.biff.show_alert", true);
+#ifdef XP_WIN
+pref("mail.biff.show_badge", true);
+pref("mail.biff.show_tray_icon", true);
+pref("mail.biff.show_tray_icon_always", false);
+pref("mail.biff.use_system_alert", false);
+#elifdef XP_MACOSX
+pref("mail.biff.animate_dock_icon", false);
+#elifdef XP_UNIX
+pref("mail.biff.use_system_alert", true);
+#endif
+
+// add jitter to biff interval
+pref("mail.biff.add_interval_jitter", true);
+
+#ifdef MOZ_SUITE
+// if true, check for new mail even when opening non-mail windows
+pref("mail.biff.on_new_window", true);
+#endif
+
+#ifdef XP_MACOSX
+// If true, the number shown in the badge will be the the number of "new"
+// messages, as per the classic Thunderbird definition. Defaults to false, which
+// notifies about the number of unread messages.
+pref("mail.biff.use_new_count_in_badge", false);
+#endif
+#ifdef XP_WIN
+pref("mail.biff.use_new_count_in_badge", true);
+#endif
+
+// For feed account serverType=rss sound on biff; if true, mail.biff.play_sound.* settings are used.
+pref("mail.feed.play_sound", false);
+
+// Content disposition for attachments (except binary files and vcards).
+// 0= Content-Disposition: inline
+// 1= Content-Disposition: attachment
+pref("mail.content_disposition_type", 1);
+
+// Experimental option to send message in the background - don't wait to close window.
+pref("mailnews.sendInBackground", false);
+// Will show a progress dialog when saving or sending a message
+pref("mailnews.show_send_progress", true);
+pref("mail.server.default.retainBy", 1);
+
+pref("mailnews.ui.junk.manualMarkAsJunkMarksRead", true);
+
+// for manual upgrades of certain UI features.
+// 1 -> 2 is for the folder pane tree landing, to hide the
+// unread and total columns, see messenger.js
+pref("mail.ui.folderpane.version", 1);
+
+// for manual upgrades of certain UI features.
+// 1 -> 2 is for the ab results pane tree landing
+// to hide the non default columns in the addressbook dialog
+// see abCommon.js and addressbook.js
+pref("mailnews.ui.addressbook_results.version", 1);
+// for manual upgrades of certain UI features.
+// 1 -> 2 is for the ab results pane tree landing
+// to hide the non default columns in the addressbook sidebar panel
+// see abCommon.js and addressbook-panel.js
+pref("mailnews.ui.addressbook_panel_results.version", 1);
+// for manual upgrades of certain UI features.
+// 1 -> 2 is for the ab results pane tree landing
+// to hide the non default columns in the select addresses dialog
+// see abCommon.js and abSelectAddressesDialog.js
+pref("mailnews.ui.select_addresses_results.version", 1);
+// for manual upgrades of certain UI features.
+// 1 -> 2 is for the ab results pane
+// to hide the non default columns in the advanced directory search dialog
+// see abCommon.js and abSearchDialog.js
+pref("mailnews.ui.advanced_directory_search_results.version", 1);
+
+// default description and color prefs for tags
+// (we keep the .labels. names for backwards compatibility)
+pref("mailnews.labels.description.1", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.labels.description.2", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.labels.description.3", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.labels.description.4", "chrome://messenger/locale/messenger.properties");
+pref("mailnews.labels.description.5", "chrome://messenger/locale/messenger.properties");
+// default: red
+pref("mailnews.labels.color.1", "#FF0000");
+// default: orange
+pref("mailnews.labels.color.2", "#FF9900");
+// default: green
+pref("mailnews.labels.color.3", "#009900");
+// default: blue
+pref("mailnews.labels.color.4", "#3333FF");
+// default: purple
+pref("mailnews.labels.color.5", "#993399");
+
+// Whether the colors from tags should be applied only to the message(s)
+// actually tagged, or also to any collapsed threads which contain tagged
+// messages.
+pref("mailnews.display_reply_tag_colors_for_collapsed_threads", true);
+
+//default null headers
+//example "X-Warn: XReply", list of hdrs separated by ": "
+pref("mailnews.customHeaders", "");
+
+// default msg compose font prefs
+pref("msgcompose.font_face", "");
+pref("msgcompose.font_size", "3");
+// If true, let the user agent use default colors (don't set text_color and
+// background_color on the message body).
+pref("msgcompose.default_colors", true);
+pref("msgcompose.text_color", "#000000");
+pref("msgcompose.background_color", "#FFFFFF");
+
+// When there is no disclosed recipients (only bcc), we should address the message to empty group
+// to prevent some mail server to disclose the bcc recipients
+pref("mail.compose.add_undisclosed_recipients", true);
+
+pref("mail.compose.dontWarnMail2Newsgroup", false);
+
+// Attach http image resources to composed messages.
+pref("mail.compose.attach_http_images", false);
+
+// Headers to check to find the right from identity to use when catchAll is active.
+pref("mail.compose.catchAllHeaders", "delivered-to, envelope-to, x-original-to, to, cc");
+
+// these prefs (in minutes) are here to help QA test this feature
+// "mail.purge.min_delay", never purge a junk folder more than once every 480 minutes (60 mins/hour * 8 hours)
+// "mail.purge.timer_interval", fire the purge timer every 5 minutes, starting 5 minutes after we load accounts
+pref("mail.purge.min_delay", 480);
+pref("mail.purge.timer_interval", 5);
+
+// Set to false if opening a message in the standalone message window or viewing
+// it in the message pane should never mark it as read.
+pref("mailnews.mark_message_read.auto", true);
+
+// Set to true if viewing a message should mark it as read after the msg is
+// viewed in the message pane for a specified time interval in seconds.
+pref("mailnews.mark_message_read.delay", false);
+// measured in seconds
+pref("mailnews.mark_message_read.delay.interval", 5);
+
+// delay after which messages are showed when moving through them with cursors
+// during thread pane navigation
+// measured in milliseconds
+pref("mailnews.threadpane_select_delay", 250);
+
+// require a password before showing imap or local headers in thread pane
+pref("mail.password_protect_local_cache", false);
+
+// import option to skip the first record, recorded so that we can save
+// the users last used preference.
+pref("mailnews.import.text.skipfirstrecord", true);
+
+#ifdef MOZ_SUITE
+// automatically scale attached images that are displayed inline
+pref("mail.enable_automatic_image_resizing", true);
+#endif
+
+#ifdef XP_WIN
+pref("ldap_2.servers.outlook.uri", "moz-aboutlookdirectory:///");
+pref("ldap_2.servers.outlook.description", "chrome://messenger/locale/addressbook/addressBook.properties");
+// Set to 3 to enable.
+pref("ldap_2.servers.outlook.dirType", -1);
+#endif
+#ifdef XP_MACOSX
+pref("ldap_2.servers.osx.uri", "moz-abosxdirectory:///");
+pref("ldap_2.servers.osx.description", "chrome://messenger/locale/addressbook/addressBook.properties");
+pref("ldap_2.servers.osx.dirType", 3);
+pref("mail.notification.sound", "");
+#endif
+pref("mail.notification.count.inbox_only", true);
+pref("mail.notification.loglevel", "Warn");
+
+// For the Empty Junk/Trash confirmation dialogs.
+pref("mailnews.emptyJunk.dontAskAgain", false);
+pref("mailnews.emptyTrash.dontAskAgain", false);
+
+// where to fetch auto config information from.
+pref("mailnews.auto_config_url", "https://live.thunderbird.net/autoconfig/v1.1/");
+// The list of addons which can handle certain account types
+pref("mailnews.auto_config.addons_url", "https://live.thunderbird.net/autoconfig/addons.json");
+// Allow to contact the ISP (email address domain).
+// This may happen via insecure means (HTTP) susceptible to eavesdropping
+// and MitM (see mailnews.auto_config.fetchFromISP.sslOnly below).
+pref("mailnews.auto_config.fetchFromISP.enabled", true);
+// Allow the username to be sent to the ISP when fetching.
+// Note that the username will leak in plaintext if a non-SSL fetch is
+// performed.
+pref("mailnews.auto_config.fetchFromISP.sendEmailAddress", true);
+// Allow only SSL channels when fetching config from ISP.
+// If false, an active attacker can block SSL fetches and then
+// MITM the HTTP fetch, determining the config that is shown to the user.
+// However:
+// 1. The user still needs to explicitly approve the false config.
+// 2. Most hosters that offer this ISP config do so on HTTP and not on HTTPS.
+// That's because they direct customer domains (HTTP) to their provider
+// config (HTTPS). If you set this to true, you simply break this mechanism.
+// You will simply not get most configs.
+// 3. There are guess config and AutoDiscover config mechanisms which
+// have the exact same problem. In order to mitigate those additional
+// vectors, set the following prefs accordingly:
+// * mailnews.auto_config.guess.sslOnly = true
+// * mailnews.auto_config.fetchFromExchange.enabled = false
+// Not all mail servers support SSL so enabling this option might lock
+// you out from your ISP. This especially affect internal mail servers.
+pref("mailnews.auto_config.fetchFromISP.sslOnly", false);
+// Allow the Microsoft Exchange AutoDiscover protocol.
+// This also sends the email address and password to the server,
+// which the protocol unfortunately requires in practice.
+pref("mailnews.auto_config.fetchFromExchange.enabled", true);
+// Whether we will attempt to guess the account configuration based on
+// protocol default ports and common domain practices
+// (e.g. {mail,pop,imap,smtp}.<email-domain>).
+pref("mailnews.auto_config.guess.enabled", true);
+// Allow only SSL configs when guessing.
+// An attacker could block SSL to force plaintext and thus be able to
+// eavesdrop. Compared to mailnews.auto_config.fetchFromISP.sslOnly
+// the attacker cannot determine the config, just pick which one it
+// likes best among those Thunderbird generates for the user based on
+// the email address.
+// Not all mail servers support SSL so enabling this option might lock
+// you out from your ISP. This especially affect internal mail servers.
+pref("mailnews.auto_config.guess.sslOnly", false);
+// When connecting to a server for guessing, either require a good
+// certificate, or allow connecting anyway.
+pref("mailnews.auto_config.guess.requireGoodCert", true);
+// The timeout (in seconds) for each guess
+pref("mailnews.auto_config.guess.timeout", 10);
+// Work around bug 1454325 by disabling mimetype mungling in XmlHttpRequest
+pref("dom.xhr.standard_content_type_normalization", false);
+
+// -- Summary Database options
+// dontPreserveOnCopy: a space separated list of properties that are not
+// copied to the new nsIMsgHdr when a message is copied.
+// Allows extensions to control preservation of properties.
+pref("mailnews.database.summary.dontPreserveOnCopy",
+ "account msgOffset threadParent msgThreadId statusOfset flags size numLines ProtoThreadFlags label gloda-id gloda-dirty storeToken");
+
+// dontPreserveOnMove: a space separated list of properties that are not
+// copied to the new nsIMsgHdr when a message is moved.
+// Allows extensions to control preservation of properties.
+pref("mailnews.database.summary.dontPreserveOnMove",
+ "account msgOffset threadParent msgThreadId statusOfset flags size numLines ProtoThreadFlags label storeToken");
+// Should we output dbcache log? Set to "Debug" to show.
+pref("mailnews.database.dbcache.loglevel", "Warn");
+
+// -- Global Database (gloda) options
+// Should the indexer be enabled?
+pref("mailnews.database.global.indexer.enabled", false);
+pref("gloda.loglevel", "Warn");
+pref("gloda.test.loglevel", "Warn");
+// Rate of growth of the gloda cache, whose maximum value is 8 MiB and max is 64 MiB.
+// See more: https://developer.mozilla.org/en/Thunderbird/gloda#Cache_Size"
+pref("mailnews.database.global.datastore.cache_to_memory_permillage", 10);
+
+// default field order in the fieldmap
+pref("mailnews.import.text.fieldmap", "+0,+1,+2,+3,+4,+5,+36,+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");
+
+// On networks deploying QoS, it is recommended that these be lockpref()'d,
+// since inappropriate marking can easily overwhelm bandwidth reservations
+// for certain services (i.e. EF for VoIP, AF4x for interactive video,
+// AF3x for broadcast/streaming video, etc)
+
+// default value for SMTP and POP3.
+// in a DSCP environment this should be 48 (0x30, or AF12) per RFC-4594,
+// Section 4.8 "High-Throughput Data Service Class"
+pref("mail.pop3.qos", 0);
+pref("mail.smtp.qos", 0);
+pref("mail.nntp.qos", 0);
+
+// default value for IMAP4
+// in a DSCP environment this should be 56 (0x38, or AF13), ibid.
+pref("mail.imap.qos", 0);
+
+// PgpMime Addon
+pref("mail.pgpmime.addon_url", "https://addons.mozilla.org/addon/enigmail/");
+
+pref("mail.asyncprompter.loglevel", "Warn");
+
+pref("mail.mailstoreconverter.loglevel", "Warn");
+
+pref("mail.jsaccount.loglevel", "Warn");
+
+pref("mailnews.oauth.loglevel", "Warn");
+
+// Using a private browser for OAuth sign-in causes issues when the provider is
+// expecting a device identifier from the browser. However, not all providers
+// have been tested with non-private browsers and there is potential for
+// existing session information to cause interference when signing into multiple
+// accounts.
+pref("mailnews.oauth.usePrivateBrowser", false);
+
+pref("test.loghelper.loglevel", "Warn");
+
+// Use importDialog.xhtml by default, set to true to use aboutImport.xhtml.
+pref("mail.import.in_new_tab", false);
+pref("mail.import.loglevel", "Warn");
+
+pref("mail.export.loglevel", "Warn");
+
+// When true, disk cache is used for messages not in offline store. If false,
+// memory cache is used instead. Both use the cache2 implementation.
+pref("mail.imap.use_disk_cache2", true);
diff --git a/comm/mailnews/mapi/include/README.md b/comm/mailnews/mapi/include/README.md
new file mode 100644
index 0000000000..fe9d707388
--- /dev/null
+++ b/comm/mailnews/mapi/include/README.md
@@ -0,0 +1,33 @@
+The contents of this directory are from the MAPI Stub Library, formerly
+Outlook MAPI Headers.
+
+The files were downloaded from https://github.com/stephenegriffin/MAPIStubLibrary
+at revision 01f9bf8cd2ac39078aabfc4662c873159efd75d6.
+
+The files have been renamed to lower-cased filenames for cross-compiling
+compatibility.
+
+The content of the imported files was validated by comparing them to the
+ones found in the Outlook2010MAPIHeaders.exe that was previously available
+from MSDN. No differences were found that affect the code (mostly whitespace).
+
+MIT License
+
+Copyright (c) 2018 Microsoft
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/comm/mailnews/mapi/include/mapiaux.h b/comm/mailnews/mapi/include/mapiaux.h
new file mode 100644
index 0000000000..4be02ab7da
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiaux.h
@@ -0,0 +1,235 @@
+/*
+ * M A P I A U X . H
+ *
+ * Messaging Applications Programming Interface.
+ *
+ * Copyright (c) 2010 Microsoft Corporation. All Rights Reserved.
+ *
+ * Purpose:
+ *
+ * This file defines additional interfaces, structures, and constants
+ * used by the Messaging Applications Programming Interface
+ */
+
+
+#ifndef MAPIAUXGUID_H
+#ifdef INITGUID
+#include <mapiguid.h>
+#define MAPIAUXGUID_H
+#endif /* INITGUID */
+
+#if !defined(INITGUID) || defined(USES_IID_IMsgServiceAdmin2)
+DEFINE_OLEGUID(IID_IMsgServiceAdmin2,0x00020387, 0, 0);
+#endif
+
+#if !defined(INITGUID) || defined(USES_IID_IMessageRaw)
+DEFINE_OLEGUID(IID_IMessageRaw, 0x0002038A, 0, 0);
+#endif
+
+#endif /* MAPIAUXGUID_H */
+
+
+
+#ifndef MAPIAUX_H
+#define MAPIAUX_H
+
+/* Include common MAPI header files if they haven't been already. */
+
+#ifndef MAPIDEFS_H
+#include <mapidefs.h>
+#endif
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+/* Forward interface declarations */
+
+DECLARE_MAPI_INTERFACE_PTR(IMsgServiceAdmin2, LPSERVICEADMIN2);
+
+// Property tags
+
+#define PR_ATTACH_CONTENT_ID PROP_TAG( PT_TSTRING, 0x3712)
+#define PR_ATTACH_CONTENT_ID_W PROP_TAG( PT_UNICODE, 0x3712)
+#define PR_ATTACH_CONTENT_ID_A PROP_TAG( PT_STRING8, 0x3712)
+
+// Additional display attributes, to supplement PR_DISPLAY_TYPE.
+#define PR_DISPLAY_TYPE_EX PROP_TAG( PT_LONG, 0x3905)
+
+#define PR_MSG_EDITOR_FORMAT PROP_TAG( PT_LONG, 0x5909 )
+
+#define PR_ROH_FLAGS PROP_TAG( PT_LONG, 0x6623)
+
+#define PR_ROH_PROXY_AUTH_SCHEME PROP_TAG( PT_LONG, 0x6627)
+
+
+// Constants
+
+/* MAPILogonEx() flags. */
+#define MAPI_BG_SESSION 0x00200000 /* Used for async profile access */
+
+/* Flags for MAPIINIT_0 structure ulFlags value passed to MAPIInitialize() */
+#define MAPI_NO_COINIT 0x00000008
+
+/* SaveChanges flags */
+#define SPAMFILTER_ONSAVE ((ULONG) 0x00000080)
+#define ITEMPROC_FORCE ((ULONG) 0x00000800)
+#define NON_EMS_XP_SAVE ((ULONG) 0x00001000)
+
+/* Flags for OpenMessageStore() */
+#define MDB_ONLINE ((ULONG) 0x00000100)
+
+
+/* IMsgStore Interface ----------------------------------------------------- */
+/* PR_STORE_SUPPORT_MASK bits */
+#define STORE_UNICODE_OK ((ULONG) 0x00040000)
+#define STORE_ITEMPROC ((ULONG) 0x00200000)
+
+/* Miscellaneous flags */
+#define MAPI_NO_CACHE ((ULONG) 0x00000200)
+#define MAPI_CACHE_ONLY ((ULONG) 0x00004000)
+
+
+/* Values for PR_AGING_GRANULARITY (Determines aging is measured by months, weeks, or days) */
+#define AG_MONTHS 0
+#define AG_WEEKS 1
+#define AG_DAYS 2
+#define NUM_AG_TYPES 3
+
+
+// PR_DISPLAY_TYPE_EX has the following format
+//
+// 33222222222211111111110000000000
+// 10987654321098765432109876543210
+//
+// FAxxxxxxxxxxxxxxRRRRRRRRLLLLLLLL
+//
+// F = 1 if remote is valid, 0 if it is not
+// A = 1 if the user is ACL-able, 0 if the user is not
+// x - unused at this time, do not interpret as this may be used in the future
+// R = display type from
+
+#define DTE_FLAG_REMOTE_VALID 0x80000000
+#define DTE_FLAG_ACL_CAPABLE 0x40000000
+#define DTE_MASK_REMOTE 0x0000ff00
+#define DTE_MASK_LOCAL 0x000000ff
+
+#define DTE_IS_REMOTE_VALID(v) (!!((v) & DTE_FLAG_REMOTE_VALID))
+#define DTE_IS_ACL_CAPABLE(v) (!!((v) & DTE_FLAG_ACL_CAPABLE))
+#define DTE_REMOTE(v) (((v) & DTE_MASK_REMOTE) >> 8)
+#define DTE_LOCAL(v) ((v) & DTE_MASK_LOCAL)
+
+#define DT_ROOM ((ULONG) 0x00000007)
+#define DT_EQUIPMENT ((ULONG) 0x00000008)
+#define DT_SEC_DISTLIST ((ULONG) 0x00000009)
+
+// Sender's editor format (PR_MSG_EDITOR_FORMAT)
+#define EDITOR_FORMAT_DONTKNOW ((ULONG)0)
+#define EDITOR_FORMAT_PLAINTEXT ((ULONG)1)
+#define EDITOR_FORMAT_HTML ((ULONG)2)
+#define EDITOR_FORMAT_RTF ((ULONG)3)
+
+
+// Flags used in PR_ROH_FLAGS - http://support.microsoft.com/kb/898835
+// Connect to my Exchange mailbox using HTTP
+#define ROHFLAGS_USE_ROH 0x1
+// Connect using SSL only
+#define ROHFLAGS_SSL_ONLY 0x2
+// Mutually authenticate the session when connecting with SSL
+#define ROHFLAGS_MUTUAL_AUTH 0x4
+// On fast networks, connect using HTTP first, then connect using TCP/IP
+#define ROHFLAGS_HTTP_FIRST_ON_FAST 0x8
+// On slow networks, connect using HTTP first, then connect using TCP/IP
+#define ROHFLAGS_HTTP_FIRST_ON_SLOW 0x20
+
+// Flags used in PR_ROH_PROXY_AUTH_SCHEME
+// Basic Authentication
+#define ROHAUTH_BASIC 0x1
+// NTLM Authentication
+#define ROHAUTH_NTLM 0x2
+
+
+// Interface declarations
+
+#define MAPI_IMSGSERVICEADMIN_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetMsgServiceTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(CreateMsgService) \
+ (THIS_ LPTSTR lpszService, \
+ LPTSTR lpszDisplayName, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteMsgService) \
+ (THIS_ LPMAPIUID lpUID) IPURE; \
+ MAPIMETHOD(CopyMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPTSTR lpszDisplayName, \
+ LPCIID lpInterfaceToCopy, \
+ LPCIID lpInterfaceDst, \
+ LPVOID lpObjectDst, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(RenameMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags, \
+ LPTSTR lpszDisplayName) IPURE; \
+ MAPIMETHOD(ConfigureMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ ULONG cValues, \
+ LPSPropValue lpProps) IPURE; \
+ MAPIMETHOD(OpenProfileSection) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPPROFSECT FAR * lppProfSect) IPURE; \
+ MAPIMETHOD(MsgServiceTransportOrder) \
+ (THIS_ ULONG cUID, \
+ LPMAPIUID lpUIDList, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(AdminProviders) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags, \
+ LPPROVIDERADMIN FAR * lppProviderAdmin) IPURE; \
+ MAPIMETHOD(SetPrimaryIdentity) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetProviderTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+
+#define MAPI_IMSGSERVICEADMIN_METHODS2(IPURE) \
+ MAPIMETHOD(CreateMsgServiceEx) \
+ (THIS_ LPTSTR lpszService, \
+ LPTSTR lpszDisplayName, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPMAPIUID lpuidService) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMsgServiceAdmin2
+DECLARE_MAPI_INTERFACE_(IMsgServiceAdmin2, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMSGSERVICEADMIN_METHODS(PURE)
+ MAPI_IMSGSERVICEADMIN_METHODS2(PURE)
+};
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MAPIAUX_H */
diff --git a/comm/mailnews/mapi/include/mapicode.h b/comm/mailnews/mapi/include/mapicode.h
new file mode 100644
index 0000000000..1c448c3003
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapicode.h
@@ -0,0 +1,218 @@
+/*
+ * M A P I C O D E . H
+ *
+ * Status Codes returned by MAPI routines
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPICODE_H
+#define MAPICODE_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN64) && !defined (_WIN64)
+#define _WIN64
+#endif
+
+/*
+ * Under Win64 systems Win32 is also defined for backwards compatibility.
+ */
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+/* Define S_OK and ITF_* */
+
+#if defined(_WIN64) || defined(_WIN32)
+#include <winerror.h>
+#endif
+
+
+/*
+ * On Windows, scodes are 32-bit values laid out as follows:
+ *
+ * 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
+ * 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
+ * +-+-+-+-+-+---------------------+-------------------------------+
+ * |S|R|C|N|r| Facility | Code |
+ * +-+-+-+-+-+---------------------+-------------------------------+
+ *
+ * where
+ *
+ * S - Severity - indicates success/fail
+ *
+ * 0 - Success
+ * 1 - Fail (COERROR)
+ *
+ * R - reserved portion of the facility code, corresponds to Windows
+ * second severity bit.
+ *
+ * C - reserved portion of the facility code, corresponds to Windows
+ * C field.
+ *
+ * N - reserved portion of the facility code. Used to indicate a
+ * mapped Windows status value.
+ *
+ * r - reserved portion of the facility code. Reserved for internal
+ * use. Used to indicate HRESULT values that are not status
+ * values, but are instead message ids for display strings.
+ *
+ * Facility - is the facility code
+ * FACILITY_NULL 0x0
+ * FACILITY_RPC 0x1
+ * FACILITY_DISPATCH 0x2
+ * FACILITY_STORAGE 0x3
+ * FACILITY_ITF 0x4
+ * FACILITY_WIN32 0x7
+ * FACILITY_WINDOWS 0x8
+ *
+ * Code - is the facility's status code
+ *
+ */
+
+
+
+
+/*
+ * We can't use OLE 2.0 macros to build sCodes because the definition has
+ * changed and we wish to conform to the new definition.
+ */
+#define MAKE_MAPI_SCODE(sev,fac,code) \
+ ((SCODE) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
+
+/* The following two macros are used to build OLE 2.0 style sCodes */
+
+#define MAKE_MAPI_E( err ) (MAKE_MAPI_SCODE( 1, FACILITY_ITF, err ))
+#define MAKE_MAPI_S( warn ) (MAKE_MAPI_SCODE( 0, FACILITY_ITF, warn ))
+
+#ifdef SUCCESS_SUCCESS
+#undef SUCCESS_SUCCESS
+#endif
+#define SUCCESS_SUCCESS 0L
+
+/* General errors (used by more than one MAPI object) */
+
+#define MAPI_E_CALL_FAILED E_FAIL
+#define MAPI_E_NOT_ENOUGH_MEMORY E_OUTOFMEMORY
+#define MAPI_E_INVALID_PARAMETER E_INVALIDARG
+#define MAPI_E_INTERFACE_NOT_SUPPORTED E_NOINTERFACE
+#define MAPI_E_NO_ACCESS E_ACCESSDENIED
+
+#define MAPI_E_NO_SUPPORT MAKE_MAPI_E( 0x102 )
+#define MAPI_E_BAD_CHARWIDTH MAKE_MAPI_E( 0x103 )
+#define MAPI_E_STRING_TOO_LONG MAKE_MAPI_E( 0x105 )
+#define MAPI_E_UNKNOWN_FLAGS MAKE_MAPI_E( 0x106 )
+#define MAPI_E_INVALID_ENTRYID MAKE_MAPI_E( 0x107 )
+#define MAPI_E_INVALID_OBJECT MAKE_MAPI_E( 0x108 )
+#define MAPI_E_OBJECT_CHANGED MAKE_MAPI_E( 0x109 )
+#define MAPI_E_OBJECT_DELETED MAKE_MAPI_E( 0x10A )
+#define MAPI_E_BUSY MAKE_MAPI_E( 0x10B )
+#define MAPI_E_NOT_ENOUGH_DISK MAKE_MAPI_E( 0x10D )
+#define MAPI_E_NOT_ENOUGH_RESOURCES MAKE_MAPI_E( 0x10E )
+#define MAPI_E_NOT_FOUND MAKE_MAPI_E( 0x10F )
+#define MAPI_E_VERSION MAKE_MAPI_E( 0x110 )
+#define MAPI_E_LOGON_FAILED MAKE_MAPI_E( 0x111 )
+#define MAPI_E_SESSION_LIMIT MAKE_MAPI_E( 0x112 )
+#define MAPI_E_USER_CANCEL MAKE_MAPI_E( 0x113 )
+#define MAPI_E_UNABLE_TO_ABORT MAKE_MAPI_E( 0x114 )
+#define MAPI_E_NETWORK_ERROR MAKE_MAPI_E( 0x115 )
+#define MAPI_E_DISK_ERROR MAKE_MAPI_E( 0x116 )
+#define MAPI_E_TOO_COMPLEX MAKE_MAPI_E( 0x117 )
+#define MAPI_E_BAD_COLUMN MAKE_MAPI_E( 0x118 )
+#define MAPI_E_EXTENDED_ERROR MAKE_MAPI_E( 0x119 )
+#define MAPI_E_COMPUTED MAKE_MAPI_E( 0x11A )
+#define MAPI_E_CORRUPT_DATA MAKE_MAPI_E( 0x11B )
+#define MAPI_E_UNCONFIGURED MAKE_MAPI_E( 0x11C )
+#define MAPI_E_FAILONEPROVIDER MAKE_MAPI_E( 0x11D )
+#define MAPI_E_UNKNOWN_CPID MAKE_MAPI_E( 0x11E )
+#define MAPI_E_UNKNOWN_LCID MAKE_MAPI_E( 0x11F )
+
+/* Flavors of E_ACCESSDENIED, used at logon */
+
+#define MAPI_E_PASSWORD_CHANGE_REQUIRED MAKE_MAPI_E( 0x120 )
+#define MAPI_E_PASSWORD_EXPIRED MAKE_MAPI_E( 0x121 )
+#define MAPI_E_INVALID_WORKSTATION_ACCOUNT MAKE_MAPI_E( 0x122 )
+#define MAPI_E_INVALID_ACCESS_TIME MAKE_MAPI_E( 0x123 )
+#define MAPI_E_ACCOUNT_DISABLED MAKE_MAPI_E( 0x124 )
+
+/* MAPI base function and status object specific errors and warnings */
+
+#define MAPI_E_END_OF_SESSION MAKE_MAPI_E( 0x200 )
+#define MAPI_E_UNKNOWN_ENTRYID MAKE_MAPI_E( 0x201 )
+#define MAPI_E_MISSING_REQUIRED_COLUMN MAKE_MAPI_E( 0x202 )
+#define MAPI_W_NO_SERVICE MAKE_MAPI_S( 0x203 )
+
+/* Property specific errors and warnings */
+
+#define MAPI_E_BAD_VALUE MAKE_MAPI_E( 0x301 )
+#define MAPI_E_INVALID_TYPE MAKE_MAPI_E( 0x302 )
+#define MAPI_E_TYPE_NO_SUPPORT MAKE_MAPI_E( 0x303 )
+#define MAPI_E_UNEXPECTED_TYPE MAKE_MAPI_E( 0x304 )
+#define MAPI_E_TOO_BIG MAKE_MAPI_E( 0x305 )
+#define MAPI_E_DECLINE_COPY MAKE_MAPI_E( 0x306 )
+#define MAPI_E_UNEXPECTED_ID MAKE_MAPI_E( 0x307 )
+
+#define MAPI_W_ERRORS_RETURNED MAKE_MAPI_S( 0x380 )
+
+/* Table specific errors and warnings */
+
+#define MAPI_E_UNABLE_TO_COMPLETE MAKE_MAPI_E( 0x400 )
+#define MAPI_E_TIMEOUT MAKE_MAPI_E( 0x401 )
+#define MAPI_E_TABLE_EMPTY MAKE_MAPI_E( 0x402 )
+#define MAPI_E_TABLE_TOO_BIG MAKE_MAPI_E( 0x403 )
+
+#define MAPI_E_INVALID_BOOKMARK MAKE_MAPI_E( 0x405 )
+
+#define MAPI_W_POSITION_CHANGED MAKE_MAPI_S( 0x481 )
+#define MAPI_W_APPROX_COUNT MAKE_MAPI_S( 0x482 )
+
+/* Transport specific errors and warnings */
+
+#define MAPI_E_WAIT MAKE_MAPI_E( 0x500 )
+#define MAPI_E_CANCEL MAKE_MAPI_E( 0x501 )
+#define MAPI_E_NOT_ME MAKE_MAPI_E( 0x502 )
+
+#define MAPI_W_CANCEL_MESSAGE MAKE_MAPI_S( 0x580 )
+
+/* Message Store, Folder, and Message specific errors and warnings */
+
+#define MAPI_E_CORRUPT_STORE MAKE_MAPI_E( 0x600 )
+#define MAPI_E_NOT_IN_QUEUE MAKE_MAPI_E( 0x601 )
+#define MAPI_E_NO_SUPPRESS MAKE_MAPI_E( 0x602 )
+#define MAPI_E_COLLISION MAKE_MAPI_E( 0x604 )
+#define MAPI_E_NOT_INITIALIZED MAKE_MAPI_E( 0x605 )
+#define MAPI_E_NON_STANDARD MAKE_MAPI_E( 0x606 )
+#define MAPI_E_NO_RECIPIENTS MAKE_MAPI_E( 0x607 )
+#define MAPI_E_SUBMITTED MAKE_MAPI_E( 0x608 )
+#define MAPI_E_HAS_FOLDERS MAKE_MAPI_E( 0x609 )
+#define MAPI_E_HAS_MESSAGES MAKE_MAPI_E( 0x60A )
+#define MAPI_E_FOLDER_CYCLE MAKE_MAPI_E( 0x60B )
+
+#define MAPI_W_PARTIAL_COMPLETION MAKE_MAPI_S( 0x680 )
+
+/* Address Book specific errors and warnings */
+
+#define MAPI_E_AMBIGUOUS_RECIP MAKE_MAPI_E( 0x700 )
+
+/* The range 0x0800 to 0x08FF is reserved */
+
+/* Obsolete typing shortcut that will go away eventually. */
+#ifndef MakeResult
+#define MakeResult(_s) ResultFromScode(_s)
+#endif
+
+/* We expect these to eventually be defined by OLE, but for now,
+ * here they are. When OLE defines them they can be much more
+ * efficient than these, but these are "proper" and don't make
+ * use of any hidden tricks.
+ */
+#ifndef HR_SUCCEEDED
+#define HR_SUCCEEDED(_hr) SUCCEEDED((SCODE)(_hr))
+#define HR_FAILED(_hr) FAILED((SCODE)(_hr))
+#endif
+
+#endif /* MAPICODE_H */
diff --git a/comm/mailnews/mapi/include/mapidbg.h b/comm/mailnews/mapi/include/mapidbg.h
new file mode 100644
index 0000000000..e22c5fce57
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapidbg.h
@@ -0,0 +1,492 @@
+/*
+ * M A P I D B G . H
+ *
+ * Debugging support for MAPI service providers.
+ * Support functions are implemented in MAPIDBG.C.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef __MAPIDBG_H_
+#define __MAPIDBG_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN64) && !defined (_WIN64)
+#define _WIN64
+#endif
+
+/*
+ * Under Win64 systems Win32 is also defined for backwards compatibility.
+ */
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+/*
+ * Debugging Macros -------------------------------------------------------
+ *
+ * IFDBG(x) Results in the expression x if DEBUG is defined, or
+ * to nothing if DEBUG is not defined
+ *
+ * IFNDBG(x) Results in the expression x if DEBUG is not defined,
+ * or to nothing if DEBUG is defined
+ *
+ * Unreferenced(a) Causes a to be referenced so that the compiler
+ * doesn't issue warnings about unused local variables
+ * which exist but are reserved for future use (eg
+ * ulFlags in many cases)
+ */
+
+#if defined(DEBUG)
+#define IFDBG(x) x
+#define IFNDBG(x)
+#else
+#define IFDBG(x)
+#define IFNDBG(x) x
+#endif
+
+#ifdef __cplusplus
+#define EXTERN_C_BEGIN extern "C" {
+#define EXTERN_C_END }
+#else
+#define EXTERN_C_BEGIN
+#define EXTERN_C_END
+#endif
+
+#define dimensionof(a) (sizeof(a)/sizeof(*(a)))
+
+#define Unreferenced(a) ((void)(a))
+
+typedef long SCODE;
+typedef unsigned long ULONG;
+typedef unsigned long DWORD;
+
+/*
+ * Assert Macros ---------------------------------------------------------
+ *
+ * Assert(a) Displays a message indicating the file and line number
+ * of this Assert() if a == 0. OK'ing an assert traps
+ * into the debugger.
+ *
+ * AssertSz(a,sz) Works like an Assert(), but displays the string sz
+ * along with the file and line number.
+ *
+ * Side asserts A side assert works like an Assert(), but evaluates
+ * 'a' even when asserts are not enabled.
+ *
+ * NF asserts A NF (Non-Fatal) assert works like an Assert(), but
+ * continues instead of trapping into the debugger when
+ * OK'ed.
+ */
+
+#if defined(DEBUG) || defined(ASSERTS_ENABLED)
+#define IFTRAP(x) x
+#else
+#define IFTRAP(x) 0
+#endif
+
+#define Trap() IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,"Trap"))
+#define TrapSz(psz) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz))
+#define TrapSz1(psz,a1) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1))
+#define TrapSz2(psz,a1,a2) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2))
+#define TrapSz3(psz,a1,a2,a3) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3))
+#define TrapSz4(psz,a1,a2,a3,a4) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4))
+#define TrapSz5(psz,a1,a2,a3,a4,a5) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5))
+#define TrapSz6(psz,a1,a2,a3,a4,a5,a6) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6))
+#define TrapSz7(psz,a1,a2,a3,a4,a5,a6,a7) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7))
+#define TrapSz8(psz,a1,a2,a3,a4,a5,a6,a7,a8) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8))
+#define TrapSz9(psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9))
+
+#define Assert(t) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,"Assertion Failure: " #t),0))
+#define AssertSz(t,psz) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz),0))
+#define AssertSz1(t,psz,a1) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1),0))
+#define AssertSz2(t,psz,a1,a2) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2),0))
+#define AssertSz3(t,psz,a1,a2,a3) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3),0))
+#define AssertSz4(t,psz,a1,a2,a3,a4) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4),0))
+#define AssertSz5(t,psz,a1,a2,a3,a4,a5) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5),0))
+#define AssertSz6(t,psz,a1,a2,a3,a4,a5,a6) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6),0))
+#define AssertSz7(t,psz,a1,a2,a3,a4,a5,a6,a7) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7),0))
+#define AssertSz8(t,psz,a1,a2,a3,a4,a5,a6,a7,a8) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8),0))
+#define AssertSz9(t,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) IFTRAP(((t) ? 0 : DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9),0))
+
+#define SideAssert(t) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,"Assertion Failure: " #t)),0)
+#define SideAssertSz(t,psz) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz)),0)
+#define SideAssertSz1(t,psz,a1) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1)),0)
+#define SideAssertSz2(t,psz,a1,a2) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2)),0)
+#define SideAssertSz3(t,psz,a1,a2,a3) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3)),0)
+#define SideAssertSz4(t,psz,a1,a2,a3,a4) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4)),0)
+#define SideAssertSz5(t,psz,a1,a2,a3,a4,a5) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5)),0)
+#define SideAssertSz6(t,psz,a1,a2,a3,a4,a5,a6) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6)),0)
+#define SideAssertSz7(t,psz,a1,a2,a3,a4,a5,a6,a7) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7)),0)
+#define SideAssertSz8(t,psz,a1,a2,a3,a4,a5,a6,a7,a8) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8)),0)
+#define SideAssertSz9(t,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) ((t) ? 0 : IFTRAP(DebugTrapFn(1,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9)),0)
+
+#define NFAssert(t) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,"Assertion Failure: " #t),0))
+#define NFAssertSz(t,psz) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz),0))
+#define NFAssertSz1(t,psz,a1) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1),0))
+#define NFAssertSz2(t,psz,a1,a2) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2),0))
+#define NFAssertSz3(t,psz,a1,a2,a3) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3),0))
+#define NFAssertSz4(t,psz,a1,a2,a3,a4) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4),0))
+#define NFAssertSz5(t,psz,a1,a2,a3,a4,a5) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5),0))
+#define NFAssertSz6(t,psz,a1,a2,a3,a4,a5,a6) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6),0))
+#define NFAssertSz7(t,psz,a1,a2,a3,a4,a5,a6,a7) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7),0))
+#define NFAssertSz8(t,psz,a1,a2,a3,a4,a5,a6,a7,a8) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8),0))
+#define NFAssertSz9(t,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) IFTRAP(((t) ? 0 : DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9),0))
+
+#define NFSideAssert(t) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,"Assertion Failure: " #t)),0)
+#define NFSideAssertSz(t,psz) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz)),0)
+#define NFSideAssertSz1(t,psz,a1) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1)),0)
+#define NFSideAssertSz2(t,psz,a1,a2) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2)),0)
+#define NFSideAssertSz3(t,psz,a1,a2,a3) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3)),0)
+#define NFSideAssertSz4(t,psz,a1,a2,a3,a4) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4)),0)
+#define NFSideAssertSz5(t,psz,a1,a2,a3,a4,a5) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5)),0)
+#define NFSideAssertSz6(t,psz,a1,a2,a3,a4,a5,a6) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6)),0)
+#define NFSideAssertSz7(t,psz,a1,a2,a3,a4,a5,a6,a7) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7)),0)
+#define NFSideAssertSz8(t,psz,a1,a2,a3,a4,a5,a6,a7,a8) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8)),0)
+#define NFSideAssertSz9(t,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) ((t) ? 0 : IFTRAP(DebugTrapFn(0,__FILE__,__LINE__,psz,a1,a2,a3,a4,a5,a6,a7,a8,a9)),0)
+
+/*
+ * Trace Macros ------------------------------------------------------------
+ *
+ * DebugTrace Use for arbitrary formatted output. It
+ * takes exactly the same arguments as the
+ * Windows wsprintf() function.
+ * DebugTraceResult Shorthand for error tracing with an
+ * HRESULT. Arguments are the name of the
+ * function (not quoted) and the HRESULT.
+ * DebugTraceSc Shorthand for error tracing with an
+ * SCODE. Arguments are the name of the
+ * function (not quoted) and the SCODE.
+ * DebugTraceArg Shorthand for invalid parameter
+ * tracing. Arguments are the name of the
+ * function (not quoted) and a quoted
+ * string describing the bad parameter.
+ */
+
+#if defined(DEBUG) || defined(TRACES_ENABLED)
+#define IFTRACE(x) x
+#define DebugTrace DebugTraceFn
+#else
+#define IFTRACE(x) 0
+#define DebugTrace 1?0:DebugTraceFn
+#endif
+
+#define DebugTraceResult(f,hr) IFTRACE(((hr) ? DebugTraceFn(#f " returns 0x%08lX %s\n", GetScode(hr), SzDecodeScode(GetScode(hr))) : 0))
+#define DebugTraceSc(f,sc) IFTRACE(((sc) ? DebugTraceFn(#f " returns 0x%08lX %s\n", sc, SzDecodeScode(sc)) : 0))
+#define DebugTraceArg(f,s) IFTRACE(DebugTraceFn(#f ": bad parameter: " s "\n"))
+#define DebugTraceLine() IFTRACE(DebugTraceFn("File %s, Line %i \n",__FILE__,__LINE__))
+#define DebugTraceProblems(sz, rgprob) IFTRACE(DebugTraceProblemsFn(sz, rgprob))
+
+#define TraceSz(psz) IFTRACE(DebugTraceFn("~" psz))
+#define TraceSz1(psz,a1) IFTRACE(DebugTraceFn("~" psz,a1))
+#define TraceSz2(psz,a1,a2) IFTRACE(DebugTraceFn("~" psz,a1,a2))
+#define TraceSz3(psz,a1,a2,a3) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3))
+#define TraceSz4(psz,a1,a2,a3,a4) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4))
+#define TraceSz5(psz,a1,a2,a3,a4,a5) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4,a5))
+#define TraceSz6(psz,a1,a2,a3,a4,a5,a6) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4,a5,a6))
+#define TraceSz7(psz,a1,a2,a3,a4,a5,a6,a7) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4,a5,a6,a7))
+#define TraceSz8(psz,a1,a2,a3,a4,a5,a6,a7,a8) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4,a5,a6,a7,a8))
+#define TraceSz9(psz,a1,a2,a3,a4,a5,a6,a7,a8,a9) IFTRACE(DebugTraceFn("~" psz,a1,a2,a3,a4,a5,a6,a7,a8,a9))
+
+/* Debugging Functions ---------------------------------------------------- */
+
+EXTERN_C_BEGIN
+
+#if defined(_WIN64) || defined (_WIN32)
+#define EXPORTDBG
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+int EXPORTDBG __cdecl DebugTrapFn(int fFatal, char *pszFile, int iLine, char *pszFormat, ...);
+int EXPORTDBG __cdecl DebugTraceFn(char *pszFormat, ...);
+void EXPORTDBG __cdecl DebugTraceProblemsFn(char *sz, void *rgprob);
+char * EXPORTDBG __cdecl SzDecodeScodeFn(SCODE sc);
+char * EXPORTDBG __cdecl SzDecodeUlPropTypeFn(unsigned long ulPropType);
+char * EXPORTDBG __cdecl SzDecodeUlPropTagFn(unsigned long ulPropTag);
+unsigned long EXPORTDBG __cdecl UlPropTagFromSzFn(char *psz);
+SCODE EXPORTDBG __cdecl ScodeFromSzFn(char *psz);
+void * EXPORTDBG __cdecl DBGMEM_EncapsulateFn(void * pmalloc, char *pszSubsys, int fCheckOften);
+void EXPORTDBG __cdecl DBGMEM_ShutdownFn(void * pmalloc);
+void EXPORTDBG __cdecl DBGMEM_CheckMemFn(void * pmalloc, int fReportOrphans);
+#if defined(_WIN64) || defined(_WIN32)
+void EXPORTDBG __cdecl DBGMEM_LeakHook(FARPROC pfn);
+void EXPORTDBG __cdecl GetCallStack(DWORD_PTR *, int, int);
+#endif
+void EXPORTDBG __cdecl DBGMEM_NoLeakDetectFn(void * pmalloc, void *pv);
+void EXPORTDBG __cdecl DBGMEM_SetFailureAtFn(void * pmalloc, ULONG ulFailureAt);
+SCODE EXPORTDBG __cdecl ScCheckScFn(SCODE, SCODE *, char *, char *, int);
+void * EXPORTDBG __cdecl VMAlloc(ULONG);
+void * EXPORTDBG __cdecl VMAllocEx(ULONG, ULONG);
+void * EXPORTDBG __cdecl VMRealloc(void *, ULONG);
+void * EXPORTDBG __cdecl VMReallocEx(void *, ULONG, ULONG);
+ULONG EXPORTDBG __cdecl VMGetSize(void *);
+ULONG EXPORTDBG __cdecl VMGetSizeEx(void *, ULONG);
+void EXPORTDBG __cdecl VMFree(void *);
+void EXPORTDBG __cdecl VMFreeEx(void *, ULONG);
+
+EXTERN_C_END
+
+/*
+ * Debugging Macros -------------------------------------------------------
+ *
+ * SzDecodeScode Returns the string name of an SCODE
+ * SzDecodeUlPropTag Returns the string name of a property
+ * tag
+ * UlPropTagFromSz Given a property tag's name, returns
+ * its value
+ * ScodeFromSz Given an SCODE's name, returns its
+ * value
+ *
+ * DBGMEM_Encapsulate Given an IMalloc interface, adds heap-
+ * checking functionality and returns a
+ * wrapped interface
+ * DBGMEM_Shutdown Undoes DBGMEM_Encapsulate, and prints
+ * out information on any allocations made
+ * since the interface was encapsulated
+ * that have not yet been released.
+ * DBGMEM_CheckMem Checks all memory allocated on the heap,
+ * and optionally reports leaked blocks.
+ * DBGMEM_NoLeakDetect Prevents a block from appearing on the leak
+ * report. Pass NULL for pv to inhibit leak
+ * reports at all from this heap.
+ */
+
+#ifdef DEBUG
+
+#define SzDecodeScode(_sc) SzDecodeScodeFn(_sc)
+#define SzDecodeUlPropType(_ulPropType) SzDecodeUlPropTypeFn(_ulPropType)
+#define SzDecodeUlPropTag(_ulPropTag) SzDecodeUlPropTagFn(_ulPropTag)
+#define UlPropTagFromSz(_sz) UlPropTagFromSzFn(_sz)
+#define ScodeFromSz(_sz) ScodeFromSzFn(_sz)
+#define DBGMEM_Encapsulate(pm, psz, f) DBGMEM_EncapsulateFn(pm, psz, f)
+#define DBGMEM_Shutdown(pm) DBGMEM_ShutdownFn(pm)
+#define DBGMEM_CheckMem(pm, f) DBGMEM_CheckMemFn(pm, f)
+#define DBGMEM_NoLeakDetect(pm, pv) DBGMEM_NoLeakDetectFn(pm, pv)
+#define DBGMEM_SetFailureAt(pm, ul) DBGMEM_SetFailureAtFn(pm, ul)
+
+#else
+
+#define SzDecodeScode(_sc) (0)
+#define SzDecodeUlPropType(_ulPropType) (0)
+#define SzDecodeUlPropTag(_ulPropTag) (0)
+#define UlPropTagFromSz(_sz) (0)
+#define ScodeFromSz(_sz) (0)
+
+#if defined(__cplusplus) && !defined(CINTERFACE)
+#define DBGMEM_Encapsulate(pmalloc, pszSubsys, fCheckOften) \
+ ((pmalloc)->AddRef(), (pmalloc))
+#define DBGMEM_Shutdown(pmalloc) \
+ ((pmalloc)->Release())
+#else
+#define DBGMEM_Encapsulate(pmalloc, pszSubsys, fCheckOften) \
+ ((pmalloc)->lpVtbl->AddRef(pmalloc), (pmalloc))
+#define DBGMEM_Shutdown(pmalloc) \
+ ((pmalloc)->lpVtbl->Release(pmalloc))
+#endif
+#define DBGMEM_CheckMem(pm, f)
+#define DBGMEM_NoLeakDetect(pm, pv)
+#define DBGMEM_SetFailureAt(pm, ul)
+
+#endif
+
+/*
+ * SCODE maps -------------------------------------------------------------
+ *
+ * ScCheckSc Given an SCODE and method name, verifies
+ * that the SCODE can legally be returned from
+ * thet method. Prints out a debug string if
+ * it cannot.
+ * HrCheckHr As ScCheckSc, for functions that return
+ * HRESULT.
+ */
+
+#if defined(DEBUG) && !defined(DOS)
+#define ScCheckSc(sc,fn) ScCheckScFn(sc,fn##_Scodes,#fn,__FILE__, __LINE__)
+#define HrCheckHr(hr,fn) HrCheckSc(GetScode(hr),fn)
+#else
+#define ScCheckSc(sc,fn) (sc)
+#define HrCheckHr(hr,fn) (hr)
+#endif
+
+#define HrCheckSc(sc,fn) ResultFromScode(ScCheckSc(sc,fn))
+
+#if defined(DEBUG) && !defined(DOS)
+extern SCODE Common_Scodes[];
+extern SCODE MAPILogon_Scodes[];
+extern SCODE MAPIAllocateBuffer_Scodes[];
+extern SCODE MAPIAllocateMore_Scodes[];
+extern SCODE MAPIFreeBuffer_Scodes[];
+
+extern SCODE IUnknown_QueryInterface_Scodes[];
+extern SCODE IUnknown_AddRef_Scodes[];
+extern SCODE IUnknown_Release_Scodes[];
+extern SCODE IUnknown_GetLastError_Scodes[];
+
+extern SCODE IMAPIProp_CopyTo_Scodes[];
+extern SCODE IMAPIProp_CopyProps_Scodes[];
+extern SCODE IMAPIProp_DeleteProps_Scodes[];
+extern SCODE IMAPIProp_GetIDsFromNames_Scodes[];
+extern SCODE IMAPIProp_GetLastError_Scodes[];
+extern SCODE IMAPIProp_GetNamesFromIDs_Scodes[];
+extern SCODE IMAPIProp_GetPropList_Scodes[];
+extern SCODE IMAPIProp_GetProps_Scodes[];
+extern SCODE IMAPIProp_OpenProperty_Scodes[];
+extern SCODE IMAPIProp_SetProps_Scodes[];
+extern SCODE IMAPIProp_SaveChanges_Scodes[];
+
+extern SCODE IStream_Read_Scodes[];
+extern SCODE IStream_Write_Scodes[];
+extern SCODE IStream_Seek_Scodes[];
+extern SCODE IStream_SetSize_Scodes[];
+extern SCODE IStream_Tell_Scodes[];
+extern SCODE IStream_LockRegion_Scodes[];
+extern SCODE IStream_UnlockRegion_Scodes[];
+extern SCODE IStream_Clone_Scodes[];
+extern SCODE IStream_CopyTo_Scodes[];
+extern SCODE IStream_Revert_Scodes[];
+extern SCODE IStream_Stat_Scodes[];
+extern SCODE IStream_Commit_Scodes[];
+
+extern SCODE IMAPITable_GetLastError_Scodes[];
+extern SCODE IMAPITable_Advise_Scodes[];
+extern SCODE IMAPITable_Unadvise_Scodes[];
+extern SCODE IMAPITable_GetStatus_Scodes[];
+extern SCODE IMAPITable_SetColumns_Scodes[];
+extern SCODE IMAPITable_QueryColumns_Scodes[];
+extern SCODE IMAPITable_GetRowCount_Scodes[];
+extern SCODE IMAPITable_SeekRow_Scodes[];
+extern SCODE IMAPITable_SeekRowApprox_Scodes[];
+extern SCODE IMAPITable_QueryPosition_Scodes[];
+extern SCODE IMAPITable_FindRow_Scodes[];
+extern SCODE IMAPITable_Restrict_Scodes[];
+extern SCODE IMAPITable_CreateBookmark_Scodes[];
+extern SCODE IMAPITable_FreeBookmark_Scodes[];
+extern SCODE IMAPITable_SortTable_Scodes[];
+extern SCODE IMAPITable_QuerySortOrder_Scodes[];
+extern SCODE IMAPITable_QueryRows_Scodes[];
+extern SCODE IMAPITable_Abort_Scodes[];
+extern SCODE IMAPITable_ExpandRow_Scodes[];
+extern SCODE IMAPITable_CollapseRow_Scodes[];
+extern SCODE IMAPITable_WaitForCompletion_Scodes[];
+extern SCODE IMAPITable_GetCollapseState_Scodes[];
+extern SCODE IMAPITable_SetCollapseState_Scodes[];
+
+extern SCODE IMAPISession_LogOff_Scodes[];
+extern SCODE IMAPISession_Release_Scodes[];
+extern SCODE IMAPISession_GetLastError_Scodes[];
+extern SCODE IMAPISession_GetMsgStoresTable_Scodes[];
+extern SCODE IMAPISession_GetStatusTable_Scodes[];
+extern SCODE IMAPISession_OpenMsgStore_Scodes[];
+extern SCODE IMAPISession_OpenAddressBook_Scodes[];
+extern SCODE IMAPISession_OpenEntry_Scodes[];
+extern SCODE IMAPISession_OpenProfileSection_Scodes[];
+extern SCODE IMAPISession_Advise_Scodes[];
+extern SCODE IMAPISession_Unadvise_Scodes[];
+extern SCODE IMAPISession_CompareEntryIDs_Scodes[];
+extern SCODE IMAPISession_MessageOptions_Scodes[];
+extern SCODE IMAPISession_QueryDefaultMessageOpt_Scodes[];
+extern SCODE IMAPISession_EnumAdrTypes_Scodes[];
+extern SCODE IMAPISession_QueryIdentity_Scodes[];
+extern SCODE IMAPISession_OpenProfileSection_Scodes[];
+extern SCODE IMAPISession_GetStatusTable_Scodes[];
+
+extern SCODE IMsgStore_Advise_Scodes[] ;
+extern SCODE IMsgStore_Unadvise_Scodes[] ;
+extern SCODE IMsgStore_CompareEntryIDs_Scodes[] ;
+extern SCODE IMsgStore_OpenEntry_Scodes[] ;
+extern SCODE IMsgStore_SetReceiveFolder_Scodes[];
+extern SCODE IMsgStore_GetReceiveFolder_Scodes[];
+extern SCODE IMsgStore_GetReceiveFolderTable_Scodes[];
+extern SCODE IMsgStore_StoreLogoff_Scodes[];
+extern SCODE IMsgStore_AbortSubmit_Scodes[];
+extern SCODE IMsgStore_GetOutgoingQueue_Scodes[] ;
+extern SCODE IMsgStore_SetLockState_Scodes[] ;
+extern SCODE IMsgStore_FinishedMsg_Scodes[] ;
+extern SCODE IMsgStore_NotifyNewMail_Scodes[];
+
+extern SCODE IMAPIFolder_GetContentsTable_Scodes[];
+extern SCODE IMAPIFolder_GetHierarchyTable_Scodes[];
+extern SCODE IMAPIFolder_SaveContentsSort_Scodes[];
+extern SCODE IMAPIFolder_OpenEntry_Scodes[];
+extern SCODE IMAPIFolder_CreateMessage_Scodes[];
+extern SCODE IMAPIFolder_CopyMessages_Scodes[];
+extern SCODE IMAPIFolder_DeleteMessages_Scodes[];
+extern SCODE IMAPIFolder_CreateFolder_Scodes[];
+extern SCODE IMAPIFolder_CopyFolder_Scodes[];
+extern SCODE IMAPIFolder_DeleteFolder_Scodes[];
+extern SCODE IMAPIFolder_SetSearchCriteria_Scodes[];
+extern SCODE IMAPIFolder_GetSearchCriteria_Scodes[];
+extern SCODE IMAPIFolder_SetReadFlags_Scodes[];
+extern SCODE IMAPIFolder_GetMessageStatus_Scodes[];
+extern SCODE IMAPIFolder_SetMessageStatus_Scodes[];
+extern SCODE IMAPIFolder_EmptyFolder_Scodes[];
+
+extern SCODE IMessage_SaveChanges_Scodes[];
+extern SCODE IMessage_GetAttachmentTable_Scodes[];
+extern SCODE IMessage_OpenAttach_Scodes[];
+extern SCODE IMessage_CreateAttach_Scodes[];
+extern SCODE IMessage_DeleteAttach_Scodes[];
+extern SCODE IMessage_GetRecipientTable_Scodes[];
+extern SCODE IMessage_ModifyRecipients_Scodes[];
+extern SCODE IMessage_SubmitMessage_Scodes[];
+extern SCODE IMessage_SetReadFlag_Scodes[];
+
+extern SCODE IAttach_SaveChanges_Scodes[];
+
+extern SCODE IAddrBook_OpenEntry_Scodes[];
+extern SCODE IAddrBook_CompareEntryIDs_Scodes[];
+extern SCODE IAddrBook_CreateOneOff_Scodes[];
+extern SCODE IAddrBook_ResolveName_Scodes[];
+extern SCODE IAddrBook_Address_Scodes[];
+extern SCODE IAddrBook_Details_Scodes[];
+extern SCODE IAddrBook_RecipOptions_Scodes[];
+extern SCODE IAddrBook_QueryDefaultRecipOpt_Scodes[];
+extern SCODE IAddrBook_Address_Scodes[];
+extern SCODE IAddrBook_ButtonPress_Scodes[];
+
+extern SCODE IABContainer_GetContentsTable_Scodes[];
+extern SCODE IABContainer_GetHierarchyTable_Scodes[];
+
+extern SCODE INotifObj_ChangeEvMask_Scodes[];
+
+extern SCODE IMAPIStatus_ChangePassword_Scodes[];
+extern SCODE IMAPIStatus_FlushQueues_Scodes[];
+extern SCODE IMAPIStatus_SettingsDialog_Scodes[];
+extern SCODE IMAPIStatus_ValidateState_Scodes[];
+
+extern SCODE IMSProvider_Logon_Scodes[];
+extern SCODE IMSProvider_Deinit_Scodes[];
+extern SCODE IMSProvider_Init_Scodes[];
+extern SCODE IMSProvider_SpoolerLogon_Scodes[];
+extern SCODE IMSProvider_Shutdown_Scodes[];
+
+extern SCODE SMAPI_MAPILogon_Scodes[];
+extern SCODE SMAPI_MAPILogoff_Scodes[];
+extern SCODE SMAPI_MAPIFreeBuffer_Scodes[];
+extern SCODE SMAPI_MAPISendMail_Scodes[];
+extern SCODE SMAPI_MAPISendDocuments_Scodes[];
+extern SCODE SMAPI_MAPIFindNext_Scodes[];
+extern SCODE SMAPI_MAPIReadMail_Scodes[];
+extern SCODE SMAPI_MAPISaveMail_Scodes[];
+extern SCODE SMAPI_MAPIDeleteMail_Scodes[];
+extern SCODE SMAPI_MAPIAddress_Scodes[];
+extern SCODE SMAPI_MAPIResolveName_Scodes[];
+extern SCODE SMAPI_MAPIDetails_Scodes[];
+
+extern SCODE IMSLogon_OpenEntry_Scodes[];
+extern SCODE IMSLogon_OpenStatusEntry_Scodes[];
+extern SCODE IMSLogon_CompareEntryIDs_Scodes[];
+extern SCODE IMSLogon_Advise_Scodes[];
+extern SCODE IMSLogon_Unadvise_Scodes[];
+extern SCODE IMSLogon_Logoff_Scodes[];
+#endif
+
+/* ------------------------------------------------------------------------ */
+
+#endif
+
diff --git a/comm/mailnews/mapi/include/mapidefs.h b/comm/mailnews/mapi/include/mapidefs.h
new file mode 100644
index 0000000000..83597279e3
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapidefs.h
@@ -0,0 +1,2736 @@
+/*
+ * M A P I D E F S . H
+ *
+ * Definitions used by MAPI clients and service providers.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPIDEFS_H
+#define MAPIDEFS_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN64) && !defined (_WIN64)
+#define _WIN64
+#endif
+
+/*
+ * Under Win64 systems Win32 is also defined for backwards compatibility.
+ */
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+#if defined (_WIN64) || defined(_WIN32) /* Must include WINDOWS.H on Win32/Win64 */
+#ifndef _WINDOWS_
+#define INC_OLE2 /* Get the OLE2 stuff */
+#define INC_RPC /* harmless on Windows NT; Windows 95 needs it */
+#define _INC_OLE /* Windows 95 will include OLE1 without this */
+#include <windows.h>
+#endif
+
+#ifndef _OLEERROR_H_
+#include <winerror.h>
+#endif
+#ifndef _OBJBASE_H_
+#include <objbase.h>
+#endif
+#endif
+
+#if defined (DOS)
+#ifndef _COMPOBJ_H_
+#include <compobj.h>
+#endif
+#endif
+
+#ifndef _INC_STDDEF
+#include <stddef.h>
+#endif
+
+/* Array dimension for structures with variable-sized arrays at the end. */
+
+#ifndef MAPI_DIM
+#define MAPI_DIM 1
+#endif
+
+/* Provider init type. Force to cdecl always */
+
+#ifndef STDMAPIINITCALLTYPE
+#if !defined (_MAC) && (defined (_WIN64) || defined(_WIN32))
+#define STDMAPIINITCALLTYPE __cdecl
+#else
+#define STDMAPIINITCALLTYPE STDMETHODCALLTYPE
+#endif
+#define STDINITMETHODIMP HRESULT STDMAPIINITCALLTYPE
+#define STDINITMETHODIMP_(type) type STDMAPIINITCALLTYPE
+#endif
+
+
+#define MAPI_NT_SERVICE ((ULONG) 0x00010000) /* Provider is being loaded in an NT service */
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Simple data types */
+
+#if !defined (MIDL_PASS) /* MIDL doesn't want to see these */
+
+#ifndef _MAC
+typedef wchar_t WCHAR; // wc, 16-bit UNICODE character
+#else
+// some Macintosh compilers don't define wchar_t in a convenient location, or define it as a char
+typedef unsigned short WCHAR; // wc, 16-bit UNICODE character
+#endif
+
+#ifdef UNICODE
+typedef WCHAR TCHAR;
+#else
+typedef char TCHAR;
+#endif
+
+
+#if _SAL_VERSION >= 20
+typedef _Null_terminated_ WCHAR FAR * LPWSTR;
+typedef _Null_terminated_ const WCHAR FAR * LPCWSTR;
+#else
+typedef WCHAR FAR * LPWSTR;
+typedef const WCHAR FAR * LPCWSTR;
+#endif
+typedef TCHAR FAR * LPTSTR;
+typedef const TCHAR FAR * LPCTSTR;
+typedef BYTE FAR * LPBYTE;
+#endif /* defined MIDL_PASS */
+
+typedef ULONG FAR * LPULONG;
+
+#ifndef __LHANDLE
+#define __LHANDLE
+typedef ULONG_PTR LHANDLE, FAR * LPLHANDLE;
+#endif
+
+#if !defined(_WINBASE_) && !defined(_FILETIME_)
+#define _FILETIME_
+typedef struct _FILETIME
+{
+ DWORD dwLowDateTime;
+ DWORD dwHighDateTime;
+} FILETIME, FAR * LPFILETIME;
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+/*
+ * This flag is used in many different MAPI calls to signify that
+ * the object opened by the call should be modifiable (MAPI_MODIFY).
+ * If the flag MAPI_MAX_ACCESS is set, the object returned should be
+ * returned at the maximum access level allowed. An additional
+ * property available on the object (PR_ACCESS_LEVEL) uses the same
+ * MAPI_MODIFY flag to say just what this new access level is.
+ */
+
+#define MAPI_MODIFY ((ULONG) 0x00000001)
+
+/*
+ * The following flags are used to indicate to the client what access
+ * level is permissible in the object. They appear in PR_ACCESS in
+ * message and folder objects as well as in contents and associated
+ * contents tables
+ */
+
+#define MAPI_ACCESS_MODIFY ((ULONG) 0x00000001)
+#define MAPI_ACCESS_READ ((ULONG) 0x00000002)
+#define MAPI_ACCESS_DELETE ((ULONG) 0x00000004)
+#define MAPI_ACCESS_CREATE_HIERARCHY ((ULONG) 0x00000008)
+#define MAPI_ACCESS_CREATE_CONTENTS ((ULONG) 0x00000010)
+#define MAPI_ACCESS_CREATE_ASSOCIATED ((ULONG) 0x00000020)
+
+/*
+ * The MAPI_UNICODE flag is used in many different MAPI calls to signify
+ * that strings passed through the interface are in Unicode (a 16-bit
+ * character set). The default is an 8-bit character set.
+ *
+ * The value fMapiUnicode can be used as the 'normal' value for
+ * that bit, given the application's default character set.
+ */
+
+#define MAPI_UNICODE ((ULONG) 0x80000000)
+
+#ifdef UNICODE
+#define fMapiUnicode MAPI_UNICODE
+#else
+#define fMapiUnicode 0
+#endif
+
+/* successful HRESULT */
+#define hrSuccess 0
+
+
+
+/* Recipient types */
+#ifndef MAPI_ORIG /* also defined in mapi.h */
+#define MAPI_ORIG 0 /* Recipient is message originator */
+#define MAPI_TO 1 /* Recipient is a primary recipient */
+#define MAPI_CC 2 /* Recipient is a copy recipient */
+#define MAPI_BCC 3 /* Recipient is blind copy recipient */
+#define MAPI_P1 0x10000000 /* Recipient is a P1 resend recipient */
+#define MAPI_SUBMITTED 0x80000000 /* Recipient is already processed */
+/* #define MAPI_AUTHORIZE 4 recipient is a CMC authorizing user */
+/*#define MAPI_DISCRETE 0x10000000 Recipient is a P1 resend recipient */
+#endif
+
+/* Bit definitions for abFlags[0] of ENTRYID */
+#define MAPI_SHORTTERM 0x80
+#define MAPI_NOTRECIP 0x40
+#define MAPI_THISSESSION 0x20
+#define MAPI_NOW 0x10
+#define MAPI_NOTRESERVED 0x08
+
+/* Bit definitions for abFlags[1] of ENTRYID */
+#define MAPI_COMPOUND 0x80
+
+/* ENTRYID */
+typedef struct
+{
+ BYTE abFlags[4];
+ BYTE ab[MAPI_DIM];
+} ENTRYID, FAR *LPENTRYID;
+
+#define CbNewENTRYID(_cb) (offsetof(ENTRYID,ab) + (_cb))
+#define CbENTRYID(_cb) (offsetof(ENTRYID,ab) + (_cb))
+#define SizedENTRYID(_cb, _name) \
+ struct _ENTRYID_ ## _name \
+{ \
+ BYTE abFlags[4]; \
+ BYTE ab[_cb]; \
+} _name
+
+/* Byte-order-independent version of GUID (world-unique identifier) */
+typedef struct _MAPIUID
+{
+ BYTE ab[16];
+} MAPIUID, FAR * LPMAPIUID;
+
+/* Note: need to include C run-times (memory.h) to use this macro */
+
+#define IsEqualMAPIUID(lpuid1, lpuid2) (!memcmp(lpuid1, lpuid2, sizeof(MAPIUID)))
+
+/*
+ * Constants for one-off entry ID:
+ * The MAPIUID that identifies the one-off provider;
+ * the flag that defines whether the embedded strings are Unicode;
+ * the flag that specifies whether the recipient gets TNEF or not.
+ */
+
+#define MAPI_ONE_OFF_UID { 0x81, 0x2b, 0x1f, 0xa4, 0xbe, 0xa3, 0x10, 0x19, \
+ 0x9d, 0x6e, 0x00, 0xdd, 0x01, 0x0f, 0x54, 0x02 }
+#define MAPI_ONE_OFF_UNICODE 0x8000
+#define MAPI_ONE_OFF_NO_RICH_INFO 0x0001
+
+/* Object type */
+
+#define MAPI_STORE ((ULONG) 0x00000001) /* Message Store */
+#define MAPI_ADDRBOOK ((ULONG) 0x00000002) /* Address Book */
+#define MAPI_FOLDER ((ULONG) 0x00000003) /* Folder */
+#define MAPI_ABCONT ((ULONG) 0x00000004) /* Address Book Container */
+#define MAPI_MESSAGE ((ULONG) 0x00000005) /* Message */
+#define MAPI_MAILUSER ((ULONG) 0x00000006) /* Individual Recipient */
+#define MAPI_ATTACH ((ULONG) 0x00000007) /* Attachment */
+#define MAPI_DISTLIST ((ULONG) 0x00000008) /* Distribution List Recipient */
+#define MAPI_PROFSECT ((ULONG) 0x00000009) /* Profile Section */
+#define MAPI_STATUS ((ULONG) 0x0000000A) /* Status Object */
+#define MAPI_SESSION ((ULONG) 0x0000000B) /* Session */
+#define MAPI_FORMINFO ((ULONG) 0x0000000C) /* Form Information */
+
+
+/*
+ * Maximum length of profile names and passwords, not including
+ * the null termination character.
+ */
+#ifndef cchProfileNameMax
+#define cchProfileNameMax 64
+#define cchProfilePassMax 64
+#endif
+
+
+/* Property Types */
+
+#define MV_FLAG 0x1000 /* Multi-value flag */
+
+#define PT_UNSPECIFIED ((ULONG) 0) /* (Reserved for interface use) type doesn't matter to caller */
+#define PT_NULL ((ULONG) 1) /* NULL property value */
+#define PT_I2 ((ULONG) 2) /* Signed 16-bit value */
+#define PT_LONG ((ULONG) 3) /* Signed 32-bit value */
+#define PT_R4 ((ULONG) 4) /* 4-byte floating point */
+#define PT_DOUBLE ((ULONG) 5) /* Floating point double */
+#define PT_CURRENCY ((ULONG) 6) /* Signed 64-bit int (decimal w/ 4 digits right of decimal pt) */
+#define PT_APPTIME ((ULONG) 7) /* Application time */
+#define PT_ERROR ((ULONG) 10) /* 32-bit error value */
+#define PT_BOOLEAN ((ULONG) 11) /* 16-bit boolean (non-zero true) */
+#define PT_OBJECT ((ULONG) 13) /* Embedded object in a property */
+#define PT_I8 ((ULONG) 20) /* 8-byte signed integer */
+#define PT_STRING8 ((ULONG) 30) /* Null terminated 8-bit character string */
+#define PT_UNICODE ((ULONG) 31) /* Null terminated Unicode string */
+#define PT_SYSTIME ((ULONG) 64) /* FILETIME 64-bit int w/ number of 100ns periods since Jan 1,1601 */
+#define PT_CLSID ((ULONG) 72) /* OLE GUID */
+#define PT_BINARY ((ULONG) 258) /* Uninterpreted (counted byte array) */
+#define PT_PTR ((ULONG) 259) /* Pointer Variable, scales to the platform */
+/* Changes are likely to these numbers, and to their structures. */
+
+/* Alternate property type names for ease of use */
+#define PT_SHORT PT_I2
+#define PT_I4 PT_LONG
+#define PT_FLOAT PT_R4
+#define PT_R8 PT_DOUBLE
+#define PT_LONGLONG PT_I8
+
+/*
+ * The type of a MAPI-defined string property is indirected, so
+ * that it defaults to Unicode string on a Unicode platform and to
+ * String8 on an ANSI or DBCS platform.
+ *
+ * Macros are defined here both for the property type, and for the
+ * field of the property value structure which should be
+ * dereferenced to obtain the string pointer.
+ */
+
+#ifdef UNICODE
+#define PT_TSTRING PT_UNICODE
+#define PT_MV_TSTRING (MV_FLAG|PT_UNICODE)
+#define LPSZ lpszW
+#define LPPSZ lppszW
+#define MVSZ MVszW
+#else
+#define PT_TSTRING PT_STRING8
+#define PT_MV_TSTRING (MV_FLAG|PT_STRING8)
+#define LPSZ lpszA
+#define LPPSZ lppszA
+#define MVSZ MVszA
+#endif
+
+
+/* Property Tags
+ *
+ * By convention, MAPI never uses 0 or FFFF as a property ID.
+ * Use as null values, initializers, sentinels, or what have you.
+ */
+
+#define PROP_TYPE_MASK ((ULONG)0x0000FFFF) /* Mask for Property type */
+#define PROP_TYPE(ulPropTag) (((ULONG)(ulPropTag))&PROP_TYPE_MASK)
+#define PROP_ID(ulPropTag) (((ULONG)(ulPropTag))>>16)
+#define PROP_TAG(ulPropType,ulPropID) ((((ULONG)(ulPropID))<<16)|((ULONG)(ulPropType)))
+#define PROP_ID_NULL 0
+#define PROP_ID_INVALID 0xFFFF
+#define PR_NULL PROP_TAG( PT_NULL, PROP_ID_NULL)
+#define CHANGE_PROP_TYPE(ulPropTag, ulPropType) \
+ (((ULONG)0xFFFF0000 & (ulPropTag)) | (ulPropType))
+
+
+/* Multi-valued Property Types */
+
+#define PT_MV_I2 (MV_FLAG|PT_I2)
+#define PT_MV_LONG (MV_FLAG|PT_LONG)
+#define PT_MV_R4 (MV_FLAG|PT_R4)
+#define PT_MV_DOUBLE (MV_FLAG|PT_DOUBLE)
+#define PT_MV_CURRENCY (MV_FLAG|PT_CURRENCY)
+#define PT_MV_APPTIME (MV_FLAG|PT_APPTIME)
+#define PT_MV_SYSTIME (MV_FLAG|PT_SYSTIME)
+#define PT_MV_STRING8 (MV_FLAG|PT_STRING8)
+#define PT_MV_BINARY (MV_FLAG|PT_BINARY)
+#define PT_MV_UNICODE (MV_FLAG|PT_UNICODE)
+#define PT_MV_CLSID (MV_FLAG|PT_CLSID)
+#define PT_MV_I8 (MV_FLAG|PT_I8)
+
+/* Alternate property type names for ease of use */
+#define PT_MV_SHORT PT_MV_I2
+#define PT_MV_I4 PT_MV_LONG
+#define PT_MV_FLOAT PT_MV_R4
+#define PT_MV_R8 PT_MV_DOUBLE
+#define PT_MV_LONGLONG PT_MV_I8
+
+/*
+ * Property type reserved bits
+ *
+ * MV_INSTANCE is used as a flag in table operations to request
+ * that a multi-valued property be presented as a single-valued
+ * property appearing in multiple rows.
+ */
+
+#define MV_INSTANCE 0x2000
+#define MVI_FLAG (MV_FLAG | MV_INSTANCE)
+#define MVI_PROP(tag) ((tag) | MVI_FLAG)
+
+/* --------------- */
+/* Data Structures */
+/* --------------- */
+
+/* Property Tag Array */
+
+typedef struct _SPropTagArray
+{
+ ULONG cValues;
+ ULONG aulPropTag[MAPI_DIM];
+} SPropTagArray, FAR * LPSPropTagArray;
+
+#define CbNewSPropTagArray(_ctag) \
+ (offsetof(SPropTagArray,aulPropTag) + (_ctag)*sizeof(ULONG))
+#define CbSPropTagArray(_lparray) \
+ (offsetof(SPropTagArray,aulPropTag) + \
+ (UINT)((_lparray)->cValues)*sizeof(ULONG))
+/* SPropTagArray */
+#define SizedSPropTagArray(_ctag, _name) \
+struct _SPropTagArray_ ## _name \
+{ \
+ ULONG cValues; \
+ ULONG aulPropTag[_ctag]; \
+} _name
+
+/* -------------- */
+/* Property Value */
+/* -------------- */
+
+typedef struct _SPropValue SPropValue;
+
+
+/* 32-bit CURRENCY definition stolen from oaidl.h */
+/* 16-bit CURRENCY definition stolen from variant.h */
+
+#ifndef _tagCY_DEFINED
+#define _tagCY_DEFINED
+#define _CY_DEFINED
+#if defined (DOS) && !defined (_VARIANT_H_)
+typedef struct FARSTRUCT tagCY {
+#ifdef _MAC
+ long Hi;
+ long Lo;
+#else
+ unsigned long Lo;
+ long Hi;
+#endif
+} CY;
+#elif defined (_WIN64) || defined(_WIN32)
+/* real definition that makes the C++ compiler happy */
+typedef union tagCY {
+ struct {
+#ifdef _MAC
+ long Hi;
+ long Lo;
+#else
+ unsigned long Lo;
+ long Hi;
+#endif
+ };
+ LONGLONG int64;
+} CY;
+#endif
+#endif
+ /* size is 8 */
+typedef CY CURRENCY;
+
+typedef struct _SBinary
+{
+ ULONG cb;
+ LPBYTE lpb;
+} SBinary, FAR *LPSBinary;
+
+typedef struct _SShortArray
+{
+ ULONG cValues;
+ short int FAR *lpi;
+} SShortArray;
+
+typedef struct _SGuidArray
+{
+ ULONG cValues;
+ GUID FAR *lpguid;
+} SGuidArray;
+
+typedef struct _SRealArray
+{
+ ULONG cValues;
+ float FAR *lpflt;
+} SRealArray;
+
+typedef struct _SLongArray
+{
+ ULONG cValues;
+ LONG FAR *lpl;
+} SLongArray;
+
+typedef struct _SLargeIntegerArray
+{
+ ULONG cValues;
+ LARGE_INTEGER FAR *lpli;
+} SLargeIntegerArray;
+
+typedef struct _SDateTimeArray
+{
+ ULONG cValues;
+ FILETIME FAR *lpft;
+} SDateTimeArray;
+
+typedef struct _SAppTimeArray
+{
+ ULONG cValues;
+ double FAR *lpat;
+} SAppTimeArray;
+
+typedef struct _SCurrencyArray
+{
+ ULONG cValues;
+ CURRENCY FAR *lpcur;
+} SCurrencyArray;
+
+typedef struct _SBinaryArray
+{
+ ULONG cValues;
+ SBinary FAR *lpbin;
+} SBinaryArray;
+
+typedef struct _SDoubleArray
+{
+ ULONG cValues;
+ double FAR *lpdbl;
+} SDoubleArray;
+
+typedef struct _SWStringArray
+{
+ ULONG cValues;
+ LPWSTR FAR *lppszW;
+} SWStringArray;
+
+typedef struct _SLPSTRArray
+{
+ ULONG cValues;
+ LPSTR FAR *lppszA;
+} SLPSTRArray;
+
+typedef union _PV
+{
+ short int i; /* case PT_I2 */
+ LONG l; /* case PT_LONG */
+ ULONG ul; /* alias for PT_LONG */
+ LPVOID lpv; /* alias for PT_PTR */
+ float flt; /* case PT_R4 */
+ double dbl; /* case PT_DOUBLE */
+ unsigned short int b; /* case PT_BOOLEAN */
+ CURRENCY cur; /* case PT_CURRENCY */
+ double at; /* case PT_APPTIME */
+ FILETIME ft; /* case PT_SYSTIME */
+ LPSTR lpszA; /* case PT_STRING8 */
+ SBinary bin; /* case PT_BINARY */
+ LPWSTR lpszW; /* case PT_UNICODE */
+ LPGUID lpguid; /* case PT_CLSID */
+ LARGE_INTEGER li; /* case PT_I8 */
+ SShortArray MVi; /* case PT_MV_I2 */
+ SLongArray MVl; /* case PT_MV_LONG */
+ SRealArray MVflt; /* case PT_MV_R4 */
+ SDoubleArray MVdbl; /* case PT_MV_DOUBLE */
+ SCurrencyArray MVcur; /* case PT_MV_CURRENCY */
+ SAppTimeArray MVat; /* case PT_MV_APPTIME */
+ SDateTimeArray MVft; /* case PT_MV_SYSTIME */
+ SBinaryArray MVbin; /* case PT_MV_BINARY */
+ SLPSTRArray MVszA; /* case PT_MV_STRING8 */
+ SWStringArray MVszW; /* case PT_MV_UNICODE */
+ SGuidArray MVguid; /* case PT_MV_CLSID */
+ SLargeIntegerArray MVli; /* case PT_MV_I8 */
+ SCODE err; /* case PT_ERROR */
+ LONG x; /* case PT_NULL, PT_OBJECT (no usable value) */
+} __UPV;
+
+typedef struct _SPropValue
+{
+ ULONG ulPropTag;
+ ULONG dwAlignPad;
+ union _PV Value;
+} SPropValue, FAR * LPSPropValue;
+
+
+/* --------------------------------------------- */
+/* Property Problem and Property Problem Arrays */
+/* --------------------------------------------- */
+
+typedef struct _SPropProblem
+{
+ ULONG ulIndex;
+ ULONG ulPropTag;
+ SCODE scode;
+} SPropProblem, FAR * LPSPropProblem;
+
+typedef struct _SPropProblemArray
+{
+ ULONG cProblem;
+ SPropProblem aProblem[MAPI_DIM];
+} SPropProblemArray, FAR * LPSPropProblemArray;
+
+#define CbNewSPropProblemArray(_cprob) \
+ (offsetof(SPropProblemArray,aProblem) + (_cprob)*sizeof(SPropProblem))
+#define CbSPropProblemArray(_lparray) \
+ (offsetof(SPropProblemArray,aProblem) + \
+ (UINT) ((_lparray)->cProblem*sizeof(SPropProblem)))
+#define SizedSPropProblemArray(_cprob, _name) \
+struct _SPropProblemArray_ ## _name \
+{ \
+ ULONG cProblem; \
+ SPropProblem aProblem[_cprob]; \
+} _name
+
+/*
+ * ENTRYLIST
+ */
+
+typedef SBinaryArray ENTRYLIST, FAR *LPENTRYLIST;
+
+/*
+ * FLATENTRYLIST
+ * MTSID
+ * FLATMTSIDLIST
+ */
+
+typedef struct {
+ ULONG cb;
+ BYTE abEntry[MAPI_DIM];
+} FLATENTRY, FAR *LPFLATENTRY;
+
+typedef struct {
+ ULONG cEntries;
+ ULONG cbEntries;
+ BYTE abEntries[MAPI_DIM];
+} FLATENTRYLIST, FAR *LPFLATENTRYLIST;
+
+typedef struct {
+ ULONG cb;
+ BYTE ab[MAPI_DIM];
+} MTSID, FAR *LPMTSID;
+
+typedef struct {
+ ULONG cMTSIDs;
+ ULONG cbMTSIDs;
+ BYTE abMTSIDs[MAPI_DIM];
+} FLATMTSIDLIST, FAR *LPFLATMTSIDLIST;
+
+#define CbNewFLATENTRY(_cb) (offsetof(FLATENTRY,abEntry) + (_cb))
+#define CbFLATENTRY(_lpentry) (offsetof(FLATENTRY,abEntry) + (_lpentry)->cb)
+#define CbNewFLATENTRYLIST(_cb) (offsetof(FLATENTRYLIST,abEntries) + (_cb))
+#define CbFLATENTRYLIST(_lplist) (offsetof(FLATENTRYLIST,abEntries) + (_lplist)->cbEntries)
+#define CbNewMTSID(_cb) (offsetof(MTSID,ab) + (_cb))
+#define CbMTSID(_lpentry) (offsetof(MTSID,ab) + (_lpentry)->cb)
+#define CbNewFLATMTSIDLIST(_cb) (offsetof(FLATMTSIDLIST,abMTSIDs) + (_cb))
+#define CbFLATMTSIDLIST(_lplist) (offsetof(FLATMTSIDLIST,abMTSIDs) + (_lplist)->cbMTSIDs)
+/* No SizedXXX macros for these types. */
+
+/* ------------------------------ */
+/* ADRENTRY, ADRLIST */
+
+typedef struct _ADRENTRY
+{
+ ULONG ulReserved1; /* Never used */
+ ULONG cValues;
+ LPSPropValue rgPropVals;
+} ADRENTRY, FAR * LPADRENTRY;
+
+typedef struct _ADRLIST
+{
+ ULONG cEntries;
+ ADRENTRY aEntries[MAPI_DIM];
+} ADRLIST, FAR * LPADRLIST;
+
+#define CbNewADRLIST(_centries) \
+ (offsetof(ADRLIST,aEntries) + (_centries)*sizeof(ADRENTRY))
+#define CbADRLIST(_lpadrlist) \
+ (offsetof(ADRLIST,aEntries) + (UINT)(_lpadrlist)->cEntries*sizeof(ADRENTRY))
+#define SizedADRLIST(_centries, _name) \
+struct _ADRLIST_ ## _name \
+{ \
+ ULONG cEntries; \
+ ADRENTRY aEntries[_centries]; \
+} _name
+
+/* ------------------------------ */
+/* SRow, SRowSet */
+
+typedef struct _SRow
+{
+ ULONG ulAdrEntryPad; /* Pad so SRow's can map to ADRENTRY's */
+ ULONG cValues; /* Count of property values */
+ LPSPropValue lpProps; /* Property value array */
+} SRow, FAR * LPSRow;
+
+typedef struct _SRowSet
+{
+ ULONG cRows; /* Count of rows */
+ SRow aRow[MAPI_DIM]; /* Array of rows */
+} SRowSet, FAR * LPSRowSet;
+
+#define CbNewSRowSet(_crow) (offsetof(SRowSet,aRow) + (_crow)*sizeof(SRow))
+#define CbSRowSet(_lprowset) (offsetof(SRowSet,aRow) + \
+ (UINT)((_lprowset)->cRows*sizeof(SRow)))
+#define SizedSRowSet(_crow, _name) \
+struct _SRowSet_ ## _name \
+{ \
+ ULONG cRows; \
+ SRow aRow[_crow]; \
+} _name
+
+/* MAPI Allocation Routines ------------------------------------------------ */
+
+typedef SCODE (STDMETHODCALLTYPE ALLOCATEBUFFER)(
+ ULONG cbSize,
+ LPVOID FAR * lppBuffer
+);
+
+typedef SCODE (STDMETHODCALLTYPE ALLOCATEMORE)(
+ ULONG cbSize,
+ LPVOID lpObject,
+ LPVOID FAR * lppBuffer
+);
+
+typedef ULONG (STDAPICALLTYPE FREEBUFFER)(
+ LPVOID lpBuffer
+);
+
+typedef ALLOCATEBUFFER FAR *LPALLOCATEBUFFER;
+typedef ALLOCATEMORE FAR * LPALLOCATEMORE;
+typedef FREEBUFFER FAR * LPFREEBUFFER;
+
+/* MAPI Component Object Model Macros -------------------------------------- */
+
+#if defined(MAPI_IF) && (!defined(__cplusplus) || defined(CINTERFACE))
+#define DECLARE_MAPI_INTERFACE(iface) \
+ typedef struct iface##Vtbl iface##Vtbl, FAR * iface; \
+ struct iface##Vtbl
+#define DECLARE_MAPI_INTERFACE_(iface, baseiface) \
+ DECLARE_MAPI_INTERFACE(iface)
+#define DECLARE_MAPI_INTERFACE_PTR(iface, piface) \
+ typedef struct iface##Vtbl iface##Vtbl, FAR * iface, FAR * FAR * piface;
+#else
+#define DECLARE_MAPI_INTERFACE(iface) \
+ DECLARE_INTERFACE(iface)
+#define DECLARE_MAPI_INTERFACE_(iface, baseiface) \
+ DECLARE_INTERFACE_(iface, baseiface)
+#ifdef __cplusplus
+#define DECLARE_MAPI_INTERFACE_PTR(iface, piface) \
+ interface iface; typedef iface FAR * piface
+#else
+#define DECLARE_MAPI_INTERFACE_PTR(iface, piface) \
+ typedef interface iface iface, FAR * piface
+#endif
+#endif
+
+#define MAPIMETHOD(method) MAPIMETHOD_(HRESULT, method)
+#define MAPIMETHOD_(type, method) STDMETHOD_(type, method)
+#define MAPIMETHOD_DECLARE(type, method, prefix) \
+ STDMETHODIMP_(type) prefix##method
+#define MAPIMETHOD_TYPEDEF(type, method, prefix) \
+ typedef type (STDMETHODCALLTYPE prefix##method##_METHOD)
+
+#define MAPI_IUNKNOWN_METHODS(IPURE) \
+ MAPIMETHOD(QueryInterface) \
+ (THIS_ REFIID riid, LPVOID FAR * ppvObj) IPURE; \
+ MAPIMETHOD_(ULONG,AddRef) (THIS) IPURE; \
+ MAPIMETHOD_(ULONG,Release) (THIS) IPURE; \
+
+#undef IMPL
+#define IMPL
+
+/* Pointers to MAPI Interfaces --------------------------------------------- */
+
+typedef const IID FAR * LPCIID;
+
+DECLARE_MAPI_INTERFACE_PTR(IMsgStore, LPMDB);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFolder, LPMAPIFOLDER);
+DECLARE_MAPI_INTERFACE_PTR(IMessage, LPMESSAGE);
+DECLARE_MAPI_INTERFACE_PTR(IAttach, LPATTACH);
+DECLARE_MAPI_INTERFACE_PTR(IAddrBook, LPADRBOOK);
+DECLARE_MAPI_INTERFACE_PTR(IABContainer, LPABCONT);
+DECLARE_MAPI_INTERFACE_PTR(IMailUser, LPMAILUSER);
+DECLARE_MAPI_INTERFACE_PTR(IDistList, LPDISTLIST);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIStatus, LPMAPISTATUS);
+DECLARE_MAPI_INTERFACE_PTR(IMAPITable, LPMAPITABLE);
+DECLARE_MAPI_INTERFACE_PTR(IProfSect, LPPROFSECT);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIProp, LPMAPIPROP);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIContainer, LPMAPICONTAINER);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIAdviseSink, LPMAPIADVISESINK);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIProgress, LPMAPIPROGRESS);
+DECLARE_MAPI_INTERFACE_PTR(IProviderAdmin, LPPROVIDERADMIN);
+
+/* Extended MAPI Error Information ----------------------------------------- */
+
+typedef struct _MAPIERROR
+{
+ ULONG ulVersion;
+ LPTSTR lpszError;
+ LPTSTR lpszComponent;
+ ULONG ulLowLevelError;
+ ULONG ulContext;
+
+} MAPIERROR, FAR * LPMAPIERROR;
+
+
+/* IMAPIAdviseSink Interface ----------------------------------------------- */
+
+/*
+ * Notification event types. The event types can be combined in a bitmask
+ * for filtering. Each one has a parameter structure associated with it:
+ *
+ * fnevCriticalError ERROR_NOTIFICATION
+ * fnevNewMail NEWMAIL_NOTIFICATION
+ * fnevObjectCreated OBJECT_NOTIFICATION
+ * fnevObjectDeleted OBJECT_NOTIFICATION
+ * fnevObjectModified OBJECT_NOTIFICATION
+ * fnevObjectCopied OBJECT_NOTIFICATION
+ * fnevSearchComplete OBJECT_NOTIFICATION
+ * fnevTableModified TABLE_NOTIFICATION
+ * fnevStatusObjectModified ?
+ *
+ * fnevExtended EXTENDED_NOTIFICATION
+ */
+
+#define fnevCriticalError ((ULONG) 0x00000001)
+#define fnevNewMail ((ULONG) 0x00000002)
+#define fnevObjectCreated ((ULONG) 0x00000004)
+#define fnevObjectDeleted ((ULONG) 0x00000008)
+#define fnevObjectModified ((ULONG) 0x00000010)
+#define fnevObjectMoved ((ULONG) 0x00000020)
+#define fnevObjectCopied ((ULONG) 0x00000040)
+#define fnevSearchComplete ((ULONG) 0x00000080)
+#define fnevTableModified ((ULONG) 0x00000100)
+#define fnevStatusObjectModified ((ULONG) 0x00000200)
+#define fnevReservedForMapi ((ULONG) 0x40000000)
+#define fnevExtended ((ULONG) 0x80000000)
+
+/* TABLE_NOTIFICATION event types passed in ulTableEvent */
+
+#define TABLE_CHANGED 1
+#define TABLE_ERROR 2
+#define TABLE_ROW_ADDED 3
+#define TABLE_ROW_DELETED 4
+#define TABLE_ROW_MODIFIED 5
+#define TABLE_SORT_DONE 6
+#define TABLE_RESTRICT_DONE 7
+#define TABLE_SETCOL_DONE 8
+#define TABLE_RELOAD 9
+
+/* Event Structures */
+
+typedef struct _ERROR_NOTIFICATION
+{
+ ULONG cbEntryID;
+ LPENTRYID lpEntryID;
+ SCODE scode;
+ ULONG ulFlags; /* 0 or MAPI_UNICODE */
+ LPMAPIERROR lpMAPIError; /* Detailed error information */
+} ERROR_NOTIFICATION;
+
+typedef struct _NEWMAIL_NOTIFICATION
+{
+ ULONG cbEntryID;
+ LPENTRYID lpEntryID; /* identifies the new message */
+ ULONG cbParentID;
+ LPENTRYID lpParentID; /* identifies the folder it lives in */
+ ULONG ulFlags; /* 0 or MAPI_UNICODE */
+ LPTSTR lpszMessageClass; /* message class (UNICODE or string8) */
+ ULONG ulMessageFlags; /* copy of PR_MESSAGE_FLAGS */
+} NEWMAIL_NOTIFICATION;
+
+typedef struct _OBJECT_NOTIFICATION
+{
+ ULONG cbEntryID;
+ LPENTRYID lpEntryID; /* EntryID of object */
+ ULONG ulObjType; /* Type of object */
+ ULONG cbParentID;
+ LPENTRYID lpParentID; /* EntryID of parent object */
+ ULONG cbOldID;
+ LPENTRYID lpOldID; /* EntryID of old object */
+ ULONG cbOldParentID;
+ LPENTRYID lpOldParentID; /* EntryID of old parent */
+ LPSPropTagArray lpPropTagArray;
+} OBJECT_NOTIFICATION;
+
+typedef struct _TABLE_NOTIFICATION
+{
+ ULONG ulTableEvent; /* Identifies WHICH table event */
+ HRESULT hResult; /* Value for TABLE_ERROR */
+ SPropValue propIndex; /* This row's "index property" */
+ SPropValue propPrior; /* Preceding row's "index property" */
+ SRow row; /* New data of added/modified row */
+ ULONG ulPad; /* Force to 8-byte boundary */
+} TABLE_NOTIFICATION;
+
+typedef struct _EXTENDED_NOTIFICATION
+{
+ ULONG ulEvent; /* extended event code */
+ ULONG cb; /* size of event parameters */
+ LPBYTE pbEventParameters; /* event parameters */
+} EXTENDED_NOTIFICATION;
+
+typedef struct
+{
+ ULONG cbEntryID;
+ LPENTRYID lpEntryID;
+ ULONG cValues;
+ LPSPropValue lpPropVals;
+} STATUS_OBJECT_NOTIFICATION;
+
+typedef struct _NOTIFICATION
+{
+ ULONG ulEventType; /* notification type, i.e. fnevSomething */
+ ULONG ulAlignPad; /* Force to 8-byte boundary */
+ union
+ {
+ ERROR_NOTIFICATION err;
+ NEWMAIL_NOTIFICATION newmail;
+ OBJECT_NOTIFICATION obj;
+ TABLE_NOTIFICATION tab;
+ EXTENDED_NOTIFICATION ext;
+ STATUS_OBJECT_NOTIFICATION statobj;
+ } info;
+} NOTIFICATION, FAR * LPNOTIFICATION;
+
+
+/* Interface used for registering and issuing notification callbacks. */
+
+#define MAPI_IMAPIADVISESINK_METHODS(IPURE) \
+ MAPIMETHOD_(ULONG, OnNotify) \
+ (THIS_ ULONG cNotif, \
+ LPNOTIFICATION lpNotifications) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIAdviseSink
+DECLARE_MAPI_INTERFACE_(IMAPIAdviseSink, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIADVISESINK_METHODS(PURE)
+};
+
+/* Callback function type for MAPIAllocAdviseSink */
+
+typedef long (STDAPICALLTYPE NOTIFCALLBACK) (
+ LPVOID lpvContext,
+ ULONG cNotification,
+ LPNOTIFICATION lpNotifications);
+typedef NOTIFCALLBACK FAR * LPNOTIFCALLBACK;
+
+/*
+ * Message name for the 16-bit MAPI notififcation engine.
+ * This can be used in 16-bit applications to force processing
+ * of notification callbacks.
+ */
+
+#define szMAPINotificationMsg "MAPI Notify window message"
+
+
+/* IMAPIProgress Interface ------------------------------------------------- */
+
+/* Flag values for the progress indicator */
+
+#define MAPI_TOP_LEVEL ((ULONG) 0x00000001)
+
+#define MAPI_IMAPIPROGRESS_METHODS(IPURE) \
+ MAPIMETHOD(Progress) \
+ (THIS_ ULONG ulValue, \
+ ULONG ulCount, \
+ ULONG ulTotal) IPURE; \
+ MAPIMETHOD(GetFlags) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(GetMax) \
+ (THIS_ ULONG FAR * lpulMax) IPURE; \
+ MAPIMETHOD(GetMin) \
+ (THIS_ ULONG FAR * lpulMin) IPURE; \
+ MAPIMETHOD(SetLimits) \
+ (THIS_ LPULONG lpulMin, \
+ LPULONG lpulMax, \
+ LPULONG lpulFlags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIProgress
+DECLARE_MAPI_INTERFACE_(IMAPIProgress, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROGRESS_METHODS(PURE)
+};
+
+
+/* IMAPIProp Interface ----------------------------------------------------- */
+
+/* GetLastError */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/*
+ * Version:
+ */
+#define MAPI_ERROR_VERSION 0x00000000L
+
+/* GetPropList */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* GetProps */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* SaveChanges */
+
+#define KEEP_OPEN_READONLY ((ULONG) 0x00000001)
+#define KEEP_OPEN_READWRITE ((ULONG) 0x00000002)
+#define FORCE_SAVE ((ULONG) 0x00000004)
+/* define MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+
+/* OpenProperty - ulFlags */
+/****** MAPI_MODIFY ((ULONG) 0x00000001) above */
+#define MAPI_CREATE ((ULONG) 0x00000002)
+#define STREAM_APPEND ((ULONG) 0x00000004)
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+
+/* OpenProperty - ulInterfaceOptions, IID_IMAPITable */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* CopyTo, CopyProps */
+
+#define MAPI_MOVE ((ULONG) 0x00000001)
+#define MAPI_NOREPLACE ((ULONG) 0x00000002)
+#define MAPI_DECLINE_OK ((ULONG) 0x00000004)
+
+#ifndef MAPI_DIALOG /* also defined in mapi.h */
+#define MAPI_DIALOG ((ULONG) 0x00000008)
+#endif
+
+#ifndef MAPI_USE_DEFAULT /* also defined in mapi.h */
+#define MAPI_USE_DEFAULT 0x00000040 /* Use default profile in logon */
+#endif
+
+/* Flags used in GetIDsFromNames */
+/****** MAPI_CREATE ((ULONG) 0x00000002) above */
+
+/* Flags used in GetNamesFromIDs (bit fields) */
+#define MAPI_NO_STRINGS ((ULONG) 0x00000001)
+#define MAPI_NO_IDS ((ULONG) 0x00000002)
+
+/* Union discriminator */
+#define MNID_ID 0
+#define MNID_STRING 1
+typedef struct _MAPINAMEID
+{
+ LPGUID lpguid;
+ ULONG ulKind;
+ union {
+ LONG_PTR lID_Reserved;
+ LONG lID;
+ LPWSTR lpwstrName;
+ } Kind;
+
+} MAPINAMEID, FAR * LPMAPINAMEID;
+
+#define MAPI_IMAPIPROP_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(SaveChanges) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetProps) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcValues, \
+ LPSPropValue FAR * lppPropArray) IPURE; \
+ MAPIMETHOD(GetPropList) \
+ (THIS_ ULONG ulFlags, \
+ LPSPropTagArray FAR * lppPropTagArray) IPURE; \
+ MAPIMETHOD(OpenProperty) \
+ (THIS_ ULONG ulPropTag, \
+ LPCIID lpiid, \
+ ULONG ulInterfaceOptions, \
+ ULONG ulFlags, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(SetProps) \
+ (THIS_ ULONG cValues, \
+ LPSPropValue lpPropArray, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(DeleteProps) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(CopyTo) \
+ (THIS_ ULONG ciidExclude, \
+ LPCIID rgiidExclude, \
+ LPSPropTagArray lpExcludeProps, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ LPCIID lpInterface, \
+ LPVOID lpDestObj, \
+ ULONG ulFlags, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(CopyProps) \
+ (THIS_ LPSPropTagArray lpIncludeProps, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ LPCIID lpInterface, \
+ LPVOID lpDestObj, \
+ ULONG ulFlags, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(GetNamesFromIDs) \
+ (THIS_ LPSPropTagArray FAR * lppPropTags, \
+ LPGUID lpPropSetGuid, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcPropNames, \
+ LPMAPINAMEID FAR * FAR * lpppPropNames) IPURE; \
+ MAPIMETHOD(GetIDsFromNames) \
+ (THIS_ ULONG cPropNames, \
+ LPMAPINAMEID FAR * lppPropNames, \
+ ULONG ulFlags, \
+ LPSPropTagArray FAR * lppPropTags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIProp
+DECLARE_MAPI_INTERFACE_(IMAPIProp, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+};
+
+/* IMAPITable Interface ---------------------------------------------------- */
+
+/* Table status */
+
+#define TBLSTAT_COMPLETE ((ULONG) 0)
+#define TBLSTAT_QCHANGED ((ULONG) 7)
+#define TBLSTAT_SORTING ((ULONG) 9)
+#define TBLSTAT_SORT_ERROR ((ULONG) 10)
+#define TBLSTAT_SETTING_COLS ((ULONG) 11)
+#define TBLSTAT_SETCOL_ERROR ((ULONG) 13)
+#define TBLSTAT_RESTRICTING ((ULONG) 14)
+#define TBLSTAT_RESTRICT_ERROR ((ULONG) 15)
+
+
+/* Table Type */
+
+#define TBLTYPE_SNAPSHOT ((ULONG) 0)
+#define TBLTYPE_KEYSET ((ULONG) 1)
+#define TBLTYPE_DYNAMIC ((ULONG) 2)
+
+
+/* Sort order */
+
+/* bit 0: set if descending, clear if ascending */
+
+#define TABLE_SORT_ASCEND ((ULONG) 0x00000000)
+#define TABLE_SORT_DESCEND ((ULONG) 0x00000001)
+#define TABLE_SORT_COMBINE ((ULONG) 0x00000002)
+
+
+/* Data structures */
+
+typedef struct _SSortOrder
+{
+ ULONG ulPropTag; /* Column to sort on */
+ ULONG ulOrder; /* Ascending, descending, combine to left */
+} SSortOrder, FAR * LPSSortOrder;
+
+typedef struct _SSortOrderSet
+{
+ ULONG cSorts; /* Number of sort columns in aSort below*/
+ ULONG cCategories; /* 0 for non-categorized, up to cSorts */
+ ULONG cExpanded; /* 0 if no categories start expanded, */
+ /* up to cExpanded */
+ SSortOrder aSort[MAPI_DIM]; /* The sort orders */
+} SSortOrderSet, FAR * LPSSortOrderSet;
+
+#define CbNewSSortOrderSet(_csort) \
+ (offsetof(SSortOrderSet,aSort) + (_csort)*sizeof(SSortOrder))
+#define CbSSortOrderSet(_lpset) \
+ (offsetof(SSortOrderSet,aSort) + \
+ (UINT)((_lpset)->cSorts*sizeof(SSortOrder)))
+#define SizedSSortOrderSet(_csort, _name) \
+struct _SSortOrderSet_ ## _name \
+{ \
+ ULONG cSorts; \
+ ULONG cCategories; \
+ ULONG cExpanded; \
+ SSortOrder aSort[_csort]; \
+} _name
+
+typedef ULONG_PTR BOOKMARK;
+
+#define BOOKMARK_BEGINNING ((BOOKMARK) 0) /* Before first row */
+#define BOOKMARK_CURRENT ((BOOKMARK) 1) /* Before current row */
+#define BOOKMARK_END ((BOOKMARK) 2) /* After last row */
+
+/* Fuzzy Level */
+
+#define FL_FULLSTRING ((ULONG) 0x00000000)
+#define FL_SUBSTRING ((ULONG) 0x00000001)
+#define FL_PREFIX ((ULONG) 0x00000002)
+
+#define FL_IGNORECASE ((ULONG) 0x00010000)
+#define FL_IGNORENONSPACE ((ULONG) 0x00020000)
+#define FL_LOOSE ((ULONG) 0x00040000)
+
+/* Restrictions */
+
+typedef struct _SRestriction FAR * LPSRestriction;
+
+/* Restriction types */
+
+#define RES_AND ((ULONG) 0x00000000)
+#define RES_OR ((ULONG) 0x00000001)
+#define RES_NOT ((ULONG) 0x00000002)
+#define RES_CONTENT ((ULONG) 0x00000003)
+#define RES_PROPERTY ((ULONG) 0x00000004)
+#define RES_COMPAREPROPS ((ULONG) 0x00000005)
+#define RES_BITMASK ((ULONG) 0x00000006)
+#define RES_SIZE ((ULONG) 0x00000007)
+#define RES_EXIST ((ULONG) 0x00000008)
+#define RES_SUBRESTRICTION ((ULONG) 0x00000009)
+#define RES_COMMENT ((ULONG) 0x0000000A)
+#define RES_COUNT ((ULONG) 0x0000000B) // OFFICEDEV: Count restriction to CAP results
+#define RES_ANNOTATION ((ULONG) 0x0000000C) // OFFICEDEV: Annotation restriction to pass information like LCID, etc.
+
+/* Relational operators. These apply to all property comparison restrictions. */
+
+#define RELOP_LT ((ULONG) 0) /* < */
+#define RELOP_LE ((ULONG) 1) /* <= */
+#define RELOP_GT ((ULONG) 2) /* > */
+#define RELOP_GE ((ULONG) 3) /* >= */
+#define RELOP_EQ ((ULONG) 4) /* == */
+#define RELOP_NE ((ULONG) 5) /* != */
+#define RELOP_RE ((ULONG) 6) /* LIKE (Regular expression) */
+
+/* Bitmask operators, for RES_BITMASK only. */
+
+#define BMR_EQZ ((ULONG) 0) /* ==0 */
+#define BMR_NEZ ((ULONG) 1) /* !=0 */
+
+/* Subobject identifiers for RES_SUBRESTRICTION only. See MAPITAGS.H. */
+
+/* #define PR_MESSAGE_RECIPIENTS PROP_TAG(PT_OBJECT,0x0E12) */
+/* #define PR_MESSAGE_ATTACHMENTS PROP_TAG(PT_OBJECT,0x0E13) */
+
+typedef struct _SAndRestriction
+{
+ ULONG cRes;
+ LPSRestriction lpRes;
+} SAndRestriction;
+
+typedef struct _SOrRestriction
+{
+ ULONG cRes;
+ LPSRestriction lpRes;
+} SOrRestriction;
+
+typedef struct _SNotRestriction
+{
+ ULONG ulReserved;
+ LPSRestriction lpRes;
+} SNotRestriction;
+
+typedef struct _SContentRestriction
+{
+ ULONG ulFuzzyLevel;
+ ULONG ulPropTag;
+ LPSPropValue lpProp;
+} SContentRestriction;
+
+typedef struct _SBitMaskRestriction
+{
+ ULONG relBMR;
+ ULONG ulPropTag;
+ ULONG ulMask;
+} SBitMaskRestriction;
+
+typedef struct _SPropertyRestriction
+{
+ ULONG relop;
+ ULONG ulPropTag;
+ LPSPropValue lpProp;
+} SPropertyRestriction;
+
+typedef struct _SComparePropsRestriction
+{
+ ULONG relop;
+ ULONG ulPropTag1;
+ ULONG ulPropTag2;
+} SComparePropsRestriction;
+
+typedef struct _SSizeRestriction
+{
+ ULONG relop;
+ ULONG ulPropTag;
+ ULONG cb;
+} SSizeRestriction;
+
+typedef struct _SExistRestriction
+{
+ ULONG ulReserved1;
+ ULONG ulPropTag;
+ ULONG ulReserved2;
+} SExistRestriction;
+
+typedef struct _SSubRestriction
+{
+ ULONG ulSubObject;
+ LPSRestriction lpRes;
+} SSubRestriction;
+
+typedef struct _SCommentRestriction
+{
+ ULONG cValues; /* # of properties in lpProp */
+ LPSRestriction lpRes;
+ LPSPropValue lpProp;
+} SCommentRestriction;
+
+// OFFICEDEV: The following two restrictions are new to Office 12 and are not
+// backwards compatible with older clients.
+typedef struct _SAnnotationRestriction
+{
+ ULONG cValues; /* # of properties in lpProp */
+ LPSRestriction lpRes;
+ LPSPropValue lpProp;
+} SAnnotationRestriction;
+
+typedef struct _SCountRestriction
+{
+ ULONG ulCount;
+ LPSRestriction lpRes;
+} SCountRestriction;
+
+typedef struct _SRestriction
+{
+ ULONG rt; /* Restriction type */
+ union
+ {
+ SComparePropsRestriction resCompareProps; /* first */
+ SAndRestriction resAnd;
+ SOrRestriction resOr;
+ SNotRestriction resNot;
+ SContentRestriction resContent;
+ SPropertyRestriction resProperty;
+ SBitMaskRestriction resBitMask;
+ SSizeRestriction resSize;
+ SExistRestriction resExist;
+ SSubRestriction resSub;
+ SCommentRestriction resComment;
+ SAnnotationRestriction resAnnotation; // OFFICEDEV: not backwards compatible with Office 11 and older
+ SCountRestriction resCount; // OFFICEDEV: not backwards compatible with Office 11 and older
+ } res;
+} SRestriction;
+
+/* SComparePropsRestriction is first in the union so that */
+/* static initializations of 3-value restriction work. */
+
+/* Flags of the methods of IMAPITable */
+
+/* QueryColumn */
+
+#define TBL_ALL_COLUMNS ((ULONG) 0x00000001)
+
+/* QueryRows */
+/* Possible values for PR_ROW_TYPE (for categorization) */
+
+#define TBL_LEAF_ROW ((ULONG) 1)
+#define TBL_EMPTY_CATEGORY ((ULONG) 2)
+#define TBL_EXPANDED_CATEGORY ((ULONG) 3)
+#define TBL_COLLAPSED_CATEGORY ((ULONG) 4)
+
+/* Table wait flag */
+
+#define TBL_NOWAIT ((ULONG) 0x00000001)
+/* alternative name for TBL_NOWAIT */
+#define TBL_ASYNC ((ULONG) 0x00000001)
+#define TBL_BATCH ((ULONG) 0x00000002)
+
+/* FindRow */
+
+#define DIR_BACKWARD ((ULONG) 0x00000001)
+
+/* Table cursor states */
+
+#define TBL_NOADVANCE ((ULONG) 0x00000001)
+
+#define MAPI_IMAPITABLE_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(GetStatus) \
+ (THIS_ ULONG FAR * lpulTableStatus, \
+ ULONG FAR * lpulTableType) IPURE; \
+ MAPIMETHOD(SetColumns) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(QueryColumns) \
+ (THIS_ ULONG ulFlags, \
+ LPSPropTagArray FAR * lpPropTagArray) IPURE; \
+ MAPIMETHOD(GetRowCount) \
+ (THIS_ ULONG ulFlags, \
+ ULONG FAR * lpulCount) IPURE; \
+ MAPIMETHOD(SeekRow) \
+ (THIS_ BOOKMARK bkOrigin, \
+ LONG lRowCount, \
+ LONG FAR * lplRowsSought) IPURE; \
+ MAPIMETHOD(SeekRowApprox) \
+ (THIS_ ULONG ulNumerator, \
+ ULONG ulDenominator) IPURE; \
+ MAPIMETHOD(QueryPosition) \
+ (THIS_ ULONG FAR * lpulRow, \
+ ULONG FAR * lpulNumerator, \
+ ULONG FAR * lpulDenominator) IPURE; \
+ MAPIMETHOD(FindRow) \
+ (THIS_ LPSRestriction lpRestriction, \
+ BOOKMARK bkOrigin, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(Restrict) \
+ (THIS_ LPSRestriction lpRestriction, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(CreateBookmark) \
+ (THIS_ BOOKMARK FAR * lpbkPosition) IPURE; \
+ MAPIMETHOD(FreeBookmark) \
+ (THIS_ BOOKMARK bkPosition) IPURE; \
+ MAPIMETHOD(SortTable) \
+ (THIS_ LPSSortOrderSet lpSortCriteria, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(QuerySortOrder) \
+ (THIS_ LPSSortOrderSet FAR * lppSortCriteria) IPURE; \
+ MAPIMETHOD(QueryRows) \
+ (THIS_ LONG lRowCount, \
+ ULONG ulFlags, \
+ LPSRowSet FAR * lppRows) IPURE; \
+ MAPIMETHOD(Abort) (THIS) IPURE; \
+ MAPIMETHOD(ExpandRow) \
+ (THIS_ ULONG cbInstanceKey, \
+ LPBYTE pbInstanceKey, \
+ ULONG ulRowCount, \
+ ULONG ulFlags, \
+ LPSRowSet FAR * lppRows, \
+ ULONG FAR * lpulMoreRows) IPURE; \
+ MAPIMETHOD(CollapseRow) \
+ (THIS_ ULONG cbInstanceKey, \
+ LPBYTE pbInstanceKey, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulRowCount) IPURE; \
+ MAPIMETHOD(WaitForCompletion) \
+ (THIS_ ULONG ulFlags, \
+ ULONG ulTimeout, \
+ ULONG FAR * lpulTableStatus) IPURE; \
+ MAPIMETHOD(GetCollapseState) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbInstanceKey, \
+ LPBYTE lpbInstanceKey, \
+ ULONG FAR * lpcbCollapseState, \
+ LPBYTE FAR * lppbCollapseState) IPURE; \
+ MAPIMETHOD(SetCollapseState) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbCollapseState, \
+ LPBYTE pbCollapseState, \
+ BOOKMARK FAR * lpbkLocation) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPITable
+DECLARE_MAPI_INTERFACE_(IMAPITable, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPITABLE_METHODS(PURE)
+};
+
+/* IProfSect Interface ----------------------------------------------------- */
+
+/* Standard section for public profile properties */
+
+#define PS_PROFILE_PROPERTIES_INIT \
+{ 0x98, 0x15, 0xAC, 0x08, 0xAA, 0xB0, 0x10, 0x1A, \
+ 0x8C, 0x93, 0x08, 0x00, 0x2B, 0x2A, 0x56, 0xC2 }
+
+
+#define MAPI_IPROFSECT_METHODS(IPURE)
+
+#undef INTERFACE
+#define INTERFACE IProfSect
+DECLARE_MAPI_INTERFACE_(IProfSect, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IPROFSECT_METHODS(PURE)
+};
+
+/* IMAPIStatus Interface --------------------------------------------------- */
+
+/* Values for PR_RESOURCE_TYPE, _METHODS, _FLAGS */
+
+#define MAPI_STORE_PROVIDER ((ULONG) 33) /* Message Store */
+#define MAPI_AB ((ULONG) 34) /* Address Book */
+#define MAPI_AB_PROVIDER ((ULONG) 35) /* Address Book Provider */
+#define MAPI_TRANSPORT_PROVIDER ((ULONG) 36) /* Transport Provider */
+#define MAPI_SPOOLER ((ULONG) 37) /* Message Spooler */
+#define MAPI_PROFILE_PROVIDER ((ULONG) 38) /* Profile Provider */
+#define MAPI_SUBSYSTEM ((ULONG) 39) /* Overall Subsystem Status */
+#define MAPI_HOOK_PROVIDER ((ULONG) 40) /* Spooler Hook */
+
+#define STATUS_VALIDATE_STATE ((ULONG) 0x00000001)
+#define STATUS_SETTINGS_DIALOG ((ULONG) 0x00000002)
+#define STATUS_CHANGE_PASSWORD ((ULONG) 0x00000004)
+#define STATUS_FLUSH_QUEUES ((ULONG) 0x00000008)
+
+#define STATUS_DEFAULT_OUTBOUND ((ULONG) 0x00000001)
+#define STATUS_DEFAULT_STORE ((ULONG) 0x00000002)
+#define STATUS_PRIMARY_IDENTITY ((ULONG) 0x00000004)
+#define STATUS_SIMPLE_STORE ((ULONG) 0x00000008)
+#define STATUS_XP_PREFER_LAST ((ULONG) 0x00000010)
+#define STATUS_NO_PRIMARY_IDENTITY ((ULONG) 0x00000020)
+#define STATUS_NO_DEFAULT_STORE ((ULONG) 0x00000040)
+#define STATUS_TEMP_SECTION ((ULONG) 0x00000080)
+#define STATUS_OWN_STORE ((ULONG) 0x00000100)
+/****** HOOK_INBOUND ((ULONG) 0x00000200) Defined in MAPIHOOK.H */
+/****** HOOK_OUTBOUND ((ULONG) 0x00000400) Defined in MAPIHOOK.H */
+#define STATUS_NEED_IPM_TREE ((ULONG) 0x00000800)
+#define STATUS_PRIMARY_STORE ((ULONG) 0x00001000)
+#define STATUS_SECONDARY_STORE ((ULONG) 0x00002000)
+
+
+/*
+ * PR_STATUS_CODE bit. Low 16 bits for common values; High 16 bits
+ * for provider type-specific values. (DCR 304)
+ */
+
+#define STATUS_AVAILABLE ((ULONG) 0x00000001)
+#define STATUS_OFFLINE ((ULONG) 0x00000002)
+#define STATUS_FAILURE ((ULONG) 0x00000004)
+
+/* Transport values of PR_STATUS_CODE */
+
+#define STATUS_INBOUND_ENABLED ((ULONG) 0x00010000)
+#define STATUS_INBOUND_ACTIVE ((ULONG) 0x00020000)
+#define STATUS_INBOUND_FLUSH ((ULONG) 0x00040000)
+#define STATUS_OUTBOUND_ENABLED ((ULONG) 0x00100000)
+#define STATUS_OUTBOUND_ACTIVE ((ULONG) 0x00200000)
+#define STATUS_OUTBOUND_FLUSH ((ULONG) 0x00400000)
+#define STATUS_REMOTE_ACCESS ((ULONG) 0x00800000)
+
+/* ValidateState flags */
+
+#define SUPPRESS_UI ((ULONG) 0x00000001)
+#define REFRESH_XP_HEADER_CACHE ((ULONG) 0x00010000)
+#define PROCESS_XP_HEADER_CACHE ((ULONG) 0x00020000)
+#define FORCE_XP_CONNECT ((ULONG) 0x00040000)
+#define FORCE_XP_DISCONNECT ((ULONG) 0x00080000)
+#define CONFIG_CHANGED ((ULONG) 0x00100000)
+#define ABORT_XP_HEADER_OPERATION ((ULONG) 0x00200000)
+#define SHOW_XP_SESSION_UI ((ULONG) 0x00400000)
+
+/* SettingsDialog flags */
+
+#define UI_READONLY ((ULONG) 0x00000001)
+
+/* FlushQueues flags */
+
+#define FLUSH_UPLOAD ((ULONG) 0x00000002)
+#define FLUSH_DOWNLOAD ((ULONG) 0x00000004)
+#define FLUSH_FORCE ((ULONG) 0x00000008)
+#define FLUSH_NO_UI ((ULONG) 0x00000010)
+#define FLUSH_ASYNC_OK ((ULONG) 0x00000020)
+
+#define MAPI_IMAPISTATUS_METHODS(IPURE) \
+ MAPIMETHOD(ValidateState) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SettingsDialog) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(ChangePassword) \
+ (THIS_ LPTSTR lpOldPass, \
+ LPTSTR lpNewPass, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(FlushQueues) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG cbTargetTransport, \
+ LPENTRYID lpTargetTransport, \
+ ULONG ulFlags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIStatus
+DECLARE_MAPI_INTERFACE_(IMAPIStatus, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAPISTATUS_METHODS(PURE)
+};
+
+/* IMAPIContainer Interface ------------------------------------------------ */
+
+/* Flags for OpenEntry() */
+
+/****** MAPI_MODIFY ((ULONG) 0x00000001) above */
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+#define MAPI_BEST_ACCESS ((ULONG) 0x00000010)
+
+/* GetContentsTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+/****** MAPI_ASSOCIATED ((ULONG) 0x00000040) below */
+
+/* GetHierarchyTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+#define CONVENIENT_DEPTH ((ULONG) 0x00000001)
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+
+/* GetSearchCriteria */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+#define SEARCH_RUNNING ((ULONG) 0x00000001)
+#define SEARCH_REBUILD ((ULONG) 0x00000002)
+#define SEARCH_RECURSIVE ((ULONG) 0x00000004)
+#define SEARCH_FOREGROUND ((ULONG) 0x00000008)
+
+/* SetSearchCriteria */
+#define STOP_SEARCH ((ULONG) 0x00000001)
+#define RESTART_SEARCH ((ULONG) 0x00000002)
+#define RECURSIVE_SEARCH ((ULONG) 0x00000004)
+#define SHALLOW_SEARCH ((ULONG) 0x00000008)
+#define FOREGROUND_SEARCH ((ULONG) 0x00000010)
+#define BACKGROUND_SEARCH ((ULONG) 0x00000020)
+
+#define MAPI_IMAPICONTAINER_METHODS(IPURE) \
+ MAPIMETHOD(GetContentsTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(GetHierarchyTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(SetSearchCriteria) \
+ (THIS_ LPSRestriction lpRestriction, \
+ LPENTRYLIST lpContainerList, \
+ ULONG ulSearchFlags) IPURE; \
+ MAPIMETHOD(GetSearchCriteria) \
+ (THIS_ ULONG ulFlags, \
+ LPSRestriction FAR * lppRestriction, \
+ LPENTRYLIST FAR * lppContainerList, \
+ ULONG FAR * lpulSearchState)IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIContainer
+DECLARE_MAPI_INTERFACE_(IMAPIContainer, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAPICONTAINER_METHODS(PURE)
+};
+
+/* IABContainer Interface -------------------------------------------------- */
+
+/*
+ * IABContainer PR_CONTAINER_FLAGS values
+ * If AB_UNMODIFIABLE and AB_MODIFIABLE are both set, it means the container
+ * doesn't know if it's modifiable or not, and the client should
+ * try to modify the contents but we won't expect it to work.
+ * If the AB_RECIPIENTS flag is set and neither AB_MODIFIABLE or AB_UNMODIFIABLE
+ * bits are set, it is an error.
+ */
+
+typedef struct _flaglist
+{
+ ULONG cFlags;
+ ULONG ulFlag[MAPI_DIM];
+} FlagList, FAR * LPFlagList;
+
+
+/*
+ * Container flags
+ */
+#define AB_RECIPIENTS ((ULONG) 0x00000001)
+#define AB_SUBCONTAINERS ((ULONG) 0x00000002)
+#define AB_MODIFIABLE ((ULONG) 0x00000004)
+#define AB_UNMODIFIABLE ((ULONG) 0x00000008)
+#define AB_FIND_ON_OPEN ((ULONG) 0x00000010)
+#define AB_NOT_DEFAULT ((ULONG) 0x00000020)
+
+/* CreateEntry() */
+
+#define CREATE_CHECK_DUP_STRICT ((ULONG) 0x00000001)
+#define CREATE_CHECK_DUP_LOOSE ((ULONG) 0x00000002)
+#define CREATE_REPLACE ((ULONG) 0x00000004)
+
+/* ResolveNames() - ulFlags */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* ResolveNames() - rgulFlags */
+#define MAPI_UNRESOLVED ((ULONG) 0x00000000)
+#define MAPI_AMBIGUOUS ((ULONG) 0x00000001)
+#define MAPI_RESOLVED ((ULONG) 0x00000002)
+
+
+#define MAPI_IABCONTAINER_METHODS(IPURE) \
+ MAPIMETHOD(CreateEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulCreateFlags, \
+ LPMAPIPROP FAR * lppMAPIPropEntry) IPURE; \
+ MAPIMETHOD(CopyEntries) \
+ (THIS_ LPENTRYLIST lpEntries, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteEntries) \
+ (THIS_ LPENTRYLIST lpEntries, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(ResolveNames) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ ULONG ulFlags, \
+ LPADRLIST lpAdrList, \
+ LPFlagList lpFlagList) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IABContainer
+DECLARE_MAPI_INTERFACE_(IABContainer, IMAPIContainer)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAPICONTAINER_METHODS(PURE)
+ MAPI_IABCONTAINER_METHODS(PURE)
+};
+
+/* IMailUser Interface ----------------------------------------------------- */
+
+/* Any call which can create a one-off entryID (i.e. MAPISupport::CreateOneOff
+ or IAdrBook::CreateOneOff) can encode the value for PR_SEND_RICH_INFO by
+ passing in the following flag in the ulFlags parameter. Setting this flag
+ indicates that PR_SEND_RICH_INFO will be FALSE.
+*/
+#define MAPI_SEND_NO_RICH_INFO ((ULONG) 0x00010000)
+
+
+
+
+/* Values of PR_NDR_DIAG_CODE */
+
+#define MAPI_DIAG(_code) ((LONG) _code)
+
+#define MAPI_DIAG_NO_DIAGNOSTIC MAPI_DIAG( -1 )
+#define MAPI_DIAG_OR_NAME_UNRECOGNIZED MAPI_DIAG( 0 )
+#define MAPI_DIAG_OR_NAME_AMBIGUOUS MAPI_DIAG( 1 )
+#define MAPI_DIAG_MTS_CONGESTED MAPI_DIAG( 2 )
+#define MAPI_DIAG_LOOP_DETECTED MAPI_DIAG( 3 )
+#define MAPI_DIAG_RECIPIENT_UNAVAILABLE MAPI_DIAG( 4 )
+#define MAPI_DIAG_MAXIMUM_TIME_EXPIRED MAPI_DIAG( 5 )
+#define MAPI_DIAG_EITS_UNSUPPORTED MAPI_DIAG( 6 )
+#define MAPI_DIAG_CONTENT_TOO_LONG MAPI_DIAG( 7 )
+#define MAPI_DIAG_IMPRACTICAL_TO_CONVERT MAPI_DIAG( 8 )
+#define MAPI_DIAG_PROHIBITED_TO_CONVERT MAPI_DIAG( 9 )
+#define MAPI_DIAG_CONVERSION_UNSUBSCRIBED MAPI_DIAG( 10 )
+#define MAPI_DIAG_PARAMETERS_INVALID MAPI_DIAG( 11 )
+#define MAPI_DIAG_CONTENT_SYNTAX_IN_ERROR MAPI_DIAG( 12 )
+#define MAPI_DIAG_LENGTH_CONSTRAINT_VIOLATD MAPI_DIAG( 13 )
+#define MAPI_DIAG_NUMBER_CONSTRAINT_VIOLATD MAPI_DIAG( 14 )
+#define MAPI_DIAG_CONTENT_TYPE_UNSUPPORTED MAPI_DIAG( 15 )
+#define MAPI_DIAG_TOO_MANY_RECIPIENTS MAPI_DIAG( 16 )
+#define MAPI_DIAG_NO_BILATERAL_AGREEMENT MAPI_DIAG( 17 )
+#define MAPI_DIAG_CRITICAL_FUNC_UNSUPPORTED MAPI_DIAG( 18 )
+#define MAPI_DIAG_CONVERSION_LOSS_PROHIB MAPI_DIAG( 19 )
+#define MAPI_DIAG_LINE_TOO_LONG MAPI_DIAG( 20 )
+#define MAPI_DIAG_PAGE_TOO_LONG MAPI_DIAG( 21 )
+#define MAPI_DIAG_PICTORIAL_SYMBOL_LOST MAPI_DIAG( 22 )
+#define MAPI_DIAG_PUNCTUATION_SYMBOL_LOST MAPI_DIAG( 23 )
+#define MAPI_DIAG_ALPHABETIC_CHARACTER_LOST MAPI_DIAG( 24 )
+#define MAPI_DIAG_MULTIPLE_INFO_LOSSES MAPI_DIAG( 25 )
+#define MAPI_DIAG_REASSIGNMENT_PROHIBITED MAPI_DIAG( 26 )
+#define MAPI_DIAG_REDIRECTION_LOOP_DETECTED MAPI_DIAG( 27 )
+#define MAPI_DIAG_EXPANSION_PROHIBITED MAPI_DIAG( 28 )
+#define MAPI_DIAG_SUBMISSION_PROHIBITED MAPI_DIAG( 29 )
+#define MAPI_DIAG_EXPANSION_FAILED MAPI_DIAG( 30 )
+#define MAPI_DIAG_RENDITION_UNSUPPORTED MAPI_DIAG( 31 )
+#define MAPI_DIAG_MAIL_ADDRESS_INCORRECT MAPI_DIAG( 32 )
+#define MAPI_DIAG_MAIL_OFFICE_INCOR_OR_INVD MAPI_DIAG( 33 )
+#define MAPI_DIAG_MAIL_ADDRESS_INCOMPLETE MAPI_DIAG( 34 )
+#define MAPI_DIAG_MAIL_RECIPIENT_UNKNOWN MAPI_DIAG( 35 )
+#define MAPI_DIAG_MAIL_RECIPIENT_DECEASED MAPI_DIAG( 36 )
+#define MAPI_DIAG_MAIL_ORGANIZATION_EXPIRED MAPI_DIAG( 37 )
+#define MAPI_DIAG_MAIL_REFUSED MAPI_DIAG( 38 )
+#define MAPI_DIAG_MAIL_UNCLAIMED MAPI_DIAG( 39 )
+#define MAPI_DIAG_MAIL_RECIPIENT_MOVED MAPI_DIAG( 40 )
+#define MAPI_DIAG_MAIL_RECIPIENT_TRAVELLING MAPI_DIAG( 41 )
+#define MAPI_DIAG_MAIL_RECIPIENT_DEPARTED MAPI_DIAG( 42 )
+#define MAPI_DIAG_MAIL_NEW_ADDRESS_UNKNOWN MAPI_DIAG( 43 )
+#define MAPI_DIAG_MAIL_FORWARDING_UNWANTED MAPI_DIAG( 44 )
+#define MAPI_DIAG_MAIL_FORWARDING_PROHIB MAPI_DIAG( 45 )
+#define MAPI_DIAG_SECURE_MESSAGING_ERROR MAPI_DIAG( 46 )
+#define MAPI_DIAG_DOWNGRADING_IMPOSSIBLE MAPI_DIAG( 47 )
+
+/* Values of PR_DELIVERY_POINT (MH_T_DELIVERY_POINT) */
+
+#define MAPI_MH_DP_PUBLIC_UA ((ULONG) 0)
+#define MAPI_MH_DP_PRIVATE_UA ((ULONG) 1)
+#define MAPI_MH_DP_MS ((ULONG) 2)
+#define MAPI_MH_DP_ML ((ULONG) 3)
+#define MAPI_MH_DP_PDAU ((ULONG) 4)
+#define MAPI_MH_DP_PDS_PATRON ((ULONG) 5)
+#define MAPI_MH_DP_OTHER_AU ((ULONG) 6)
+
+
+#define MAPI_IMAILUSER_METHODS(IPURE)
+
+#undef INTERFACE
+#define INTERFACE IMailUser
+DECLARE_MAPI_INTERFACE_(IMailUser, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAILUSER_METHODS(PURE)
+};
+
+/* IDistList Interface ----------------------------------------------------- */
+
+#define MAPI_IDISTLIST_METHODS(IPURE) \
+ MAPIMETHOD(CreateEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulCreateFlags, \
+ LPMAPIPROP FAR * lppMAPIPropEntry) IPURE; \
+ MAPIMETHOD(CopyEntries) \
+ (THIS_ LPENTRYLIST lpEntries, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteEntries) \
+ (THIS_ LPENTRYLIST lpEntries, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(ResolveNames) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ ULONG ulFlags, \
+ LPADRLIST lpAdrList, \
+ LPFlagList lpFlagList) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IDistList
+DECLARE_MAPI_INTERFACE_(IDistList, IMAPIContainer)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAPICONTAINER_METHODS(PURE)
+ MAPI_IDISTLIST_METHODS(PURE)
+};
+
+/* IMAPIFolder Interface --------------------------------------------------- */
+
+/* IMAPIFolder folder type (enum) */
+
+#define FOLDER_ROOT ((ULONG) 0x00000000)
+#define FOLDER_GENERIC ((ULONG) 0x00000001)
+#define FOLDER_SEARCH ((ULONG) 0x00000002)
+
+/* CreateMessage */
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+/****** MAPI_ASSOCIATED ((ULONG) 0x00000040) below */
+
+/* CopyMessages */
+
+#define MESSAGE_MOVE ((ULONG) 0x00000001)
+#define MESSAGE_DIALOG ((ULONG) 0x00000002)
+/****** MAPI_DECLINE_OK ((ULONG) 0x00000004) above */
+
+/* CreateFolder */
+
+#define OPEN_IF_EXISTS ((ULONG) 0x00000001)
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* DeleteFolder */
+
+#define DEL_MESSAGES ((ULONG) 0x00000001)
+#define FOLDER_DIALOG ((ULONG) 0x00000002)
+#define DEL_FOLDERS ((ULONG) 0x00000004)
+
+/* EmptyFolder */
+#define DEL_ASSOCIATED ((ULONG) 0x00000008)
+
+/* CopyFolder */
+
+#define FOLDER_MOVE ((ULONG) 0x00000001)
+/****** FOLDER_DIALOG ((ULONG) 0x00000002) above */
+/****** MAPI_DECLINE_OK ((ULONG) 0x00000004) above */
+#define COPY_SUBFOLDERS ((ULONG) 0x00000010)
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+
+/* SetReadFlags */
+
+/****** SUPPRESS_RECEIPT ((ULONG) 0x00000001) below */
+/****** FOLDER_DIALOG ((ULONG) 0x00000002) above */
+/****** CLEAR_READ_FLAG ((ULONG) 0x00000004) below */
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+/****** GENERATE_RECEIPT_ONLY ((ULONG) 0x00000010) below */
+/****** CLEAR_RN_PENDING ((ULONG) 0x00000020) below */
+/****** CLEAR_NRN_PENDING ((ULONG) 0x00000040) below */
+
+
+/* GetMessageStatus */
+
+#define MSGSTATUS_HIGHLIGHTED ((ULONG) 0x00000001)
+#define MSGSTATUS_TAGGED ((ULONG) 0x00000002)
+#define MSGSTATUS_HIDDEN ((ULONG) 0x00000004)
+#define MSGSTATUS_DELMARKED ((ULONG) 0x00000008)
+
+/* Bits for remote message status */
+
+#define MSGSTATUS_REMOTE_DOWNLOAD ((ULONG) 0x00001000)
+#define MSGSTATUS_REMOTE_DELETE ((ULONG) 0x00002000)
+
+/* SaveContentsSort */
+
+#define RECURSIVE_SORT ((ULONG) 0x00000002)
+
+/* PR_STATUS property */
+
+#define FLDSTATUS_HIGHLIGHTED ((ULONG) 0x00000001)
+#define FLDSTATUS_TAGGED ((ULONG) 0x00000002)
+#define FLDSTATUS_HIDDEN ((ULONG) 0x00000004)
+#define FLDSTATUS_DELMARKED ((ULONG) 0x00000008)
+
+#define MAPI_IMAPIFOLDER_METHODS(IPURE) \
+ MAPIMETHOD(CreateMessage) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPMESSAGE FAR * lppMessage) IPURE; \
+ MAPIMETHOD(CopyMessages) \
+ (THIS_ LPENTRYLIST lpMsgList, \
+ LPCIID lpInterface, \
+ LPVOID lpDestFolder, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteMessages) \
+ (THIS_ LPENTRYLIST lpMsgList, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(CreateFolder) \
+ (THIS_ ULONG ulFolderType, \
+ LPTSTR lpszFolderName, \
+ LPTSTR lpszFolderComment, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPMAPIFOLDER FAR * lppFolder) IPURE; \
+ MAPIMETHOD(CopyFolder) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ LPVOID lpDestFolder, \
+ LPTSTR lpszNewFolderName, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteFolder) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SetReadFlags) \
+ (THIS_ LPENTRYLIST lpMsgList, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetMessageStatus) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulMessageStatus) IPURE; \
+ MAPIMETHOD(SetMessageStatus) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulNewStatus, \
+ ULONG ulNewStatusMask, \
+ ULONG FAR * lpulOldStatus) IPURE; \
+ MAPIMETHOD(SaveContentsSort) \
+ (THIS_ LPSSortOrderSet lpSortCriteria, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(EmptyFolder) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFolder
+DECLARE_MAPI_INTERFACE_(IMAPIFolder, IMAPIContainer)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMAPICONTAINER_METHODS(PURE)
+ MAPI_IMAPIFOLDER_METHODS(PURE)
+};
+
+/* IMsgStore Interface ----------------------------------------------------- */
+
+/* PR_STORE_SUPPORT_MASK bits */
+#define STORE_ENTRYID_UNIQUE ((ULONG) 0x00000001)
+#define STORE_READONLY ((ULONG) 0x00000002)
+#define STORE_SEARCH_OK ((ULONG) 0x00000004)
+#define STORE_MODIFY_OK ((ULONG) 0x00000008)
+#define STORE_CREATE_OK ((ULONG) 0x00000010)
+#define STORE_ATTACH_OK ((ULONG) 0x00000020)
+#define STORE_OLE_OK ((ULONG) 0x00000040)
+#define STORE_SUBMIT_OK ((ULONG) 0x00000080)
+#define STORE_NOTIFY_OK ((ULONG) 0x00000100)
+#define STORE_MV_PROPS_OK ((ULONG) 0x00000200)
+#define STORE_CATEGORIZE_OK ((ULONG) 0x00000400)
+#define STORE_RTF_OK ((ULONG) 0x00000800)
+#define STORE_RESTRICTION_OK ((ULONG) 0x00001000)
+#define STORE_SORT_OK ((ULONG) 0x00002000)
+#define STORE_PUBLIC_FOLDERS ((ULONG) 0x00004000)
+#define STORE_UNCOMPRESSED_RTF ((ULONG) 0x00008000)
+
+/* PR_STORE_STATE bits, try not to collide with PR_STORE_SUPPORT_MASK */
+
+#define STORE_HAS_SEARCHES ((ULONG) 0x01000000)
+
+
+/* OpenEntry() */
+
+/****** MAPI_MODIFY ((ULONG) 0x00000001) above */
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+/****** MAPI_BEST_ACCESS ((ULONG) 0x00000010) above */
+
+/* SetReceiveFolder() */
+
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* GetReceiveFolder() */
+
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* GetReceiveFolderTable() */
+
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+
+/* StoreLogoff() */
+
+#define LOGOFF_NO_WAIT ((ULONG) 0x00000001)
+#define LOGOFF_ORDERLY ((ULONG) 0x00000002)
+#define LOGOFF_PURGE ((ULONG) 0x00000004)
+#define LOGOFF_ABORT ((ULONG) 0x00000008)
+#define LOGOFF_QUIET ((ULONG) 0x00000010)
+
+#define LOGOFF_COMPLETE ((ULONG) 0x00010000)
+#define LOGOFF_INBOUND ((ULONG) 0x00020000)
+#define LOGOFF_OUTBOUND ((ULONG) 0x00040000)
+#define LOGOFF_OUTBOUND_QUEUE ((ULONG) 0x00080000)
+
+/* SetLockState() */
+
+#define MSG_LOCKED ((ULONG) 0x00000001)
+#define MSG_UNLOCKED ((ULONG) 0x00000000)
+
+/* Flag bits for PR_VALID_FOLDER_MASK */
+
+#define FOLDER_IPM_SUBTREE_VALID ((ULONG) 0x00000001)
+#define FOLDER_IPM_INBOX_VALID ((ULONG) 0x00000002)
+#define FOLDER_IPM_OUTBOX_VALID ((ULONG) 0x00000004)
+#define FOLDER_IPM_WASTEBASKET_VALID ((ULONG) 0x00000008)
+#define FOLDER_IPM_SENTMAIL_VALID ((ULONG) 0x00000010)
+#define FOLDER_VIEWS_VALID ((ULONG) 0x00000020)
+#define FOLDER_COMMON_VIEWS_VALID ((ULONG) 0x00000040)
+#define FOLDER_FINDER_VALID ((ULONG) 0x00000080)
+
+#define MAPI_IMSGSTORE_METHODS(IPURE) \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(SetReceiveFolder) \
+ (THIS_ LPTSTR lpszMessageClass, \
+ ULONG ulFlags, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(GetReceiveFolder) \
+ (THIS_ LPTSTR lpszMessageClass, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID, \
+ LPTSTR FAR * lppszExplicitClass) IPURE; \
+ MAPIMETHOD(GetReceiveFolderTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(StoreLogoff) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(AbortSubmit) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetOutgoingQueue) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(SetLockState) \
+ (THIS_ LPMESSAGE lpMessage, \
+ ULONG ulLockState) IPURE; \
+ MAPIMETHOD(FinishedMsg) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(NotifyNewMail) \
+ (THIS_ LPNOTIFICATION lpNotification) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMsgStore
+DECLARE_MAPI_INTERFACE_(IMsgStore, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMSGSTORE_METHODS(PURE)
+};
+
+/* IMessage Interface ------------------------------------------------------ */
+
+/* SubmitMessage */
+
+#define FORCE_SUBMIT ((ULONG) 0x00000001)
+
+/* Flags defined in PR_MESSAGE_FLAGS */
+
+#define MSGFLAG_READ ((ULONG) 0x00000001)
+#define MSGFLAG_UNMODIFIED ((ULONG) 0x00000002)
+#define MSGFLAG_SUBMIT ((ULONG) 0x00000004)
+#define MSGFLAG_UNSENT ((ULONG) 0x00000008)
+#define MSGFLAG_HASATTACH ((ULONG) 0x00000010)
+#define MSGFLAG_FROMME ((ULONG) 0x00000020)
+#define MSGFLAG_ASSOCIATED ((ULONG) 0x00000040)
+#define MSGFLAG_RESEND ((ULONG) 0x00000080)
+#define MSGFLAG_RN_PENDING ((ULONG) 0x00000100)
+#define MSGFLAG_NRN_PENDING ((ULONG) 0x00000200)
+
+/* Flags defined in PR_SUBMIT_FLAGS */
+
+#define SUBMITFLAG_LOCKED ((ULONG) 0x00000001)
+#define SUBMITFLAG_PREPROCESS ((ULONG) 0x00000002)
+
+/* GetAttachmentTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* GetRecipientTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* ModifyRecipients */
+
+/* ((ULONG) 0x00000001 is not a valid flag on ModifyRecipients. */
+#define MODRECIP_ADD ((ULONG) 0x00000002)
+#define MODRECIP_MODIFY ((ULONG) 0x00000004)
+#define MODRECIP_REMOVE ((ULONG) 0x00000008)
+
+/* SetReadFlag */
+
+#define SUPPRESS_RECEIPT ((ULONG) 0x00000001)
+#define CLEAR_READ_FLAG ((ULONG) 0x00000004)
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) below */
+#define GENERATE_RECEIPT_ONLY ((ULONG) 0x00000010)
+#define CLEAR_RN_PENDING ((ULONG) 0x00000020)
+#define CLEAR_NRN_PENDING ((ULONG) 0x00000040)
+
+/* DeleteAttach */
+
+#define ATTACH_DIALOG ((ULONG) 0x00000001)
+
+/* PR_SECURITY values */
+#define SECURITY_SIGNED ((ULONG) 0x00000001)
+#define SECURITY_ENCRYPTED ((ULONG) 0x00000002)
+
+/* PR_PRIORITY values */
+#define PRIO_URGENT ((long) 1)
+#define PRIO_NORMAL ((long) 0)
+#define PRIO_NONURGENT ((long) -1)
+
+/* PR_SENSITIVITY values */
+#define SENSITIVITY_NONE ((ULONG) 0x00000000)
+#define SENSITIVITY_PERSONAL ((ULONG) 0x00000001)
+#define SENSITIVITY_PRIVATE ((ULONG) 0x00000002)
+#define SENSITIVITY_COMPANY_CONFIDENTIAL ((ULONG) 0x00000003)
+
+/* PR_IMPORTANCE values */
+#define IMPORTANCE_LOW ((long) 0)
+#define IMPORTANCE_NORMAL ((long) 1)
+#define IMPORTANCE_HIGH ((long) 2)
+
+#define MAPI_IMESSAGE_METHODS(IPURE) \
+ MAPIMETHOD(GetAttachmentTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(OpenAttach) \
+ (THIS_ ULONG ulAttachmentNum, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPATTACH FAR * lppAttach) IPURE; \
+ MAPIMETHOD(CreateAttach) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulAttachmentNum, \
+ LPATTACH FAR * lppAttach) IPURE; \
+ MAPIMETHOD(DeleteAttach) \
+ (THIS_ ULONG ulAttachmentNum, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetRecipientTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(ModifyRecipients) \
+ (THIS_ ULONG ulFlags, \
+ LPADRLIST lpMods) IPURE; \
+ MAPIMETHOD(SubmitMessage) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SetReadFlag) \
+ (THIS_ ULONG ulFlags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMessage
+DECLARE_MAPI_INTERFACE_(IMessage, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IMESSAGE_METHODS(PURE)
+};
+
+/* IAttach Interface ------------------------------------------------------- */
+
+/* IAttach attachment methods: PR_ATTACH_METHOD values */
+
+#define NO_ATTACHMENT ((ULONG) 0x00000000)
+#define ATTACH_BY_VALUE ((ULONG) 0x00000001)
+#define ATTACH_BY_REFERENCE ((ULONG) 0x00000002)
+#define ATTACH_BY_REF_RESOLVE ((ULONG) 0x00000003)
+#define ATTACH_BY_REF_ONLY ((ULONG) 0x00000004)
+#define ATTACH_EMBEDDED_MSG ((ULONG) 0x00000005)
+#define ATTACH_OLE ((ULONG) 0x00000006)
+
+#define MAPI_IATTACH_METHODS(IPURE)
+
+#undef INTERFACE
+#define INTERFACE IAttach
+DECLARE_MAPI_INTERFACE_(IAttach, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IATTACH_METHODS(PURE)
+};
+
+/* --------------------------------- */
+/* Address Book interface definition */
+
+/* ADRPARM ulFlags - top 4 bits used for versioning */
+
+#define GET_ADRPARM_VERSION(ulFlags) (((ULONG)ulFlags) & 0xF0000000)
+#define SET_ADRPARM_VERSION(ulFlags, ulVersion) (((ULONG)ulVersion) | (((ULONG)ulFlags) & 0x0FFFFFFF))
+
+/* Current versions of ADRPARM */
+#define ADRPARM_HELP_CTX ((ULONG) 0x00000000)
+
+
+/* ulFlags - bit fields */
+#define DIALOG_MODAL ((ULONG) 0x00000001)
+#define DIALOG_SDI ((ULONG) 0x00000002)
+#define DIALOG_OPTIONS ((ULONG) 0x00000004)
+#define ADDRESS_ONE ((ULONG) 0x00000008)
+#define AB_SELECTONLY ((ULONG) 0x00000010)
+#define AB_RESOLVE ((ULONG) 0x00000020)
+
+/* --------------------------------- */
+/* PR_DISPLAY_TYPEs */
+/*
+ * These standard display types are
+ * by default handled by MAPI.
+ * They have default icons associated
+ * with them.
+ */
+
+/* For address book contents tables */
+#define DT_MAILUSER ((ULONG) 0x00000000)
+#define DT_DISTLIST ((ULONG) 0x00000001)
+#define DT_FORUM ((ULONG) 0x00000002)
+#define DT_AGENT ((ULONG) 0x00000003)
+#define DT_ORGANIZATION ((ULONG) 0x00000004)
+#define DT_PRIVATE_DISTLIST ((ULONG) 0x00000005)
+#define DT_REMOTE_MAILUSER ((ULONG) 0x00000006)
+
+/* For address book hierarchy tables */
+#define DT_MODIFIABLE ((ULONG) 0x00010000)
+#define DT_GLOBAL ((ULONG) 0x00020000)
+#define DT_LOCAL ((ULONG) 0x00030000)
+#define DT_WAN ((ULONG) 0x00040000)
+#define DT_NOT_SPECIFIC ((ULONG) 0x00050000)
+
+/* For folder hierarchy tables */
+#define DT_FOLDER ((ULONG) 0x01000000)
+#define DT_FOLDER_LINK ((ULONG) 0x02000000)
+#define DT_FOLDER_SPECIAL ((ULONG) 0x04000000)
+
+/* Accelerator callback for DIALOG_SDI form of AB UI */
+typedef BOOL (STDMETHODCALLTYPE ACCELERATEABSDI)(ULONG_PTR ulUIParam,
+ LPVOID lpvmsg);
+typedef ACCELERATEABSDI FAR * LPFNABSDI;
+
+/* Callback to application telling it that the DIALOG_SDI form of the */
+/* AB UI has been dismissed. This is so that the above LPFNABSDI */
+/* function doesn't keep being called. */
+typedef void (STDMETHODCALLTYPE DISMISSMODELESS)(ULONG_PTR ulUIParam,
+ LPVOID lpvContext);
+typedef DISMISSMODELESS FAR * LPFNDISMISS;
+
+/*
+ * Prototype for the client function hooked to an optional button on
+ * the address book dialog
+ */
+
+typedef SCODE (STDMETHODCALLTYPE FAR * LPFNBUTTON)(
+ ULONG_PTR ulUIParam,
+ LPVOID lpvContext,
+ ULONG cbEntryID,
+ LPENTRYID lpSelection,
+ ULONG ulFlags
+);
+
+
+/* Parameters for the address book dialog */
+typedef struct _ADRPARM
+{
+ ULONG cbABContEntryID;
+ LPENTRYID lpABContEntryID;
+ ULONG ulFlags;
+
+ LPVOID lpReserved;
+ ULONG ulHelpContext;
+ LPTSTR lpszHelpFileName;
+
+ LPFNABSDI lpfnABSDI;
+ LPFNDISMISS lpfnDismiss;
+ LPVOID lpvDismissContext;
+ LPTSTR lpszCaption;
+ LPTSTR lpszNewEntryTitle;
+ LPTSTR lpszDestWellsTitle;
+ ULONG cDestFields;
+ ULONG nDestFieldFocus;
+ LPTSTR FAR * lppszDestTitles;
+ ULONG FAR * lpulDestComps;
+ LPSRestriction lpContRestriction;
+ LPSRestriction lpHierRestriction;
+} ADRPARM, FAR * LPADRPARM;
+
+
+/* ------------ */
+/* Random flags */
+
+/* Flag for deferred error */
+#define MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008)
+
+/* Flag for creating and using Folder Associated Information Messages */
+#define MAPI_ASSOCIATED ((ULONG) 0x00000040)
+
+/* Flags for OpenMessageStore() */
+
+#define MDB_NO_DIALOG ((ULONG) 0x00000001)
+#define MDB_WRITE ((ULONG) 0x00000004)
+/****** MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) above */
+/****** MAPI_BEST_ACCESS ((ULONG) 0x00000010) above */
+#define MDB_TEMPORARY ((ULONG) 0x00000020)
+#define MDB_NO_MAIL ((ULONG) 0x00000080)
+
+/* Flags for OpenAddressBook */
+
+#define AB_NO_DIALOG ((ULONG) 0x00000001)
+
+/* IMAPIControl Interface -------------------------------------------------- */
+
+/* Interface used in controls (particularly the button) defined by */
+/* Display Tables. */
+
+/* Flags for GetState */
+
+#define MAPI_ENABLED ((ULONG) 0x00000000)
+#define MAPI_DISABLED ((ULONG) 0x00000001)
+
+#define MAPI_IMAPICONTROL_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(Activate) \
+ (THIS_ ULONG ulFlags, \
+ ULONG_PTR ulUIParam) IPURE; \
+ MAPIMETHOD(GetState) \
+ (THIS_ ULONG ulFlags, \
+ ULONG FAR * lpulState) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIControl
+DECLARE_MAPI_INTERFACE_(IMAPIControl, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPICONTROL_METHODS(PURE)
+};
+
+DECLARE_MAPI_INTERFACE_PTR(IMAPIControl, LPMAPICONTROL);
+
+/* Display Tables ---------------------------------------------------------- */
+
+/* Flags used in display tables - that is, PR_CONTROL_FLAGS */
+
+#define DT_MULTILINE ((ULONG) 0x00000001)
+#define DT_EDITABLE ((ULONG) 0x00000002)
+#define DT_REQUIRED ((ULONG) 0x00000004)
+#define DT_SET_IMMEDIATE ((ULONG) 0x00000008)
+#define DT_PASSWORD_EDIT ((ULONG) 0x00000010)
+#define DT_ACCEPT_DBCS ((ULONG) 0x00000020)
+#define DT_SET_SELECTION ((ULONG) 0x00000040)
+
+/* Display Table structures */
+
+#define DTCT_LABEL ((ULONG) 0x00000000)
+#define DTCT_EDIT ((ULONG) 0x00000001)
+#define DTCT_LBX ((ULONG) 0x00000002)
+#define DTCT_COMBOBOX ((ULONG) 0x00000003)
+#define DTCT_DDLBX ((ULONG) 0x00000004)
+#define DTCT_CHECKBOX ((ULONG) 0x00000005)
+#define DTCT_GROUPBOX ((ULONG) 0x00000006)
+#define DTCT_BUTTON ((ULONG) 0x00000007)
+#define DTCT_PAGE ((ULONG) 0x00000008)
+#define DTCT_RADIOBUTTON ((ULONG) 0x00000009)
+#define DTCT_MVLISTBOX ((ULONG) 0x0000000B)
+#define DTCT_MVDDLBX ((ULONG) 0x0000000C)
+
+/* Labels */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLLABEL
+{
+ ULONG ulbLpszLabelName;
+ ULONG ulFlags;
+} DTBLLABEL, FAR * LPDTBLLABEL;
+#define SizedDtblLabel(n,u) \
+struct _DTBLLABEL_ ## u \
+{ \
+ DTBLLABEL dtbllabel; \
+ TCHAR lpszLabelName[n]; \
+} u
+
+
+/* Simple Text Edits */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLEDIT
+{
+ ULONG ulbLpszCharsAllowed;
+ ULONG ulFlags;
+ ULONG ulNumCharsAllowed;
+ ULONG ulPropTag;
+} DTBLEDIT, FAR * LPDTBLEDIT;
+#define SizedDtblEdit(n,u) \
+struct _DTBLEDIT_ ## u \
+{ \
+ DTBLEDIT dtbledit; \
+ TCHAR lpszCharsAllowed[n]; \
+} u
+
+/* List Box */
+/* Valid ulFlags:
+ */
+#define MAPI_NO_HBAR ((ULONG) 0x00000001)
+#define MAPI_NO_VBAR ((ULONG) 0x00000002)
+
+typedef struct _DTBLLBX
+{
+ ULONG ulFlags;
+ ULONG ulPRSetProperty;
+ ULONG ulPRTableName;
+} DTBLLBX, FAR * LPDTBLLBX;
+
+
+/* Combo Box */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLCOMBOBOX
+{
+ ULONG ulbLpszCharsAllowed;
+ ULONG ulFlags;
+ ULONG ulNumCharsAllowed;
+ ULONG ulPRPropertyName;
+ ULONG ulPRTableName;
+} DTBLCOMBOBOX, FAR * LPDTBLCOMBOBOX;
+#define SizedDtblComboBox(n,u) \
+struct _DTBLCOMBOBOX_ ## u \
+{ \
+ DTBLCOMBOBOX dtblcombobox; \
+ TCHAR lpszCharsAllowed[n]; \
+} u
+
+
+/* Drop Down */
+/* Valid ulFlags:
+ * none
+ */
+typedef struct _DTBLDDLBX
+{
+ ULONG ulFlags;
+ ULONG ulPRDisplayProperty;
+ ULONG ulPRSetProperty;
+ ULONG ulPRTableName;
+} DTBLDDLBX, FAR * LPDTBLDDLBX;
+
+
+/* Check Box */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLCHECKBOX
+{
+ ULONG ulbLpszLabel;
+ ULONG ulFlags;
+ ULONG ulPRPropertyName;
+} DTBLCHECKBOX, FAR * LPDTBLCHECKBOX;
+#define SizedDtblCheckBox(n,u) \
+struct _DTBLCHECKBOX_ ## u \
+{ \
+ DTBLCHECKBOX dtblcheckbox; \
+ TCHAR lpszLabel[n]; \
+} u
+
+
+
+/* Group Box */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLGROUPBOX
+{
+ ULONG ulbLpszLabel;
+ ULONG ulFlags;
+} DTBLGROUPBOX, FAR * LPDTBLGROUPBOX;
+#define SizedDtblGroupBox(n,u) \
+struct _DTBLGROUPBOX_ ## u \
+{ \
+ DTBLGROUPBOX dtblgroupbox; \
+ TCHAR lpszLabel[n]; \
+} u
+
+/* Button control */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLBUTTON
+{
+ ULONG ulbLpszLabel;
+ ULONG ulFlags;
+ ULONG ulPRControl;
+} DTBLBUTTON, FAR * LPDTBLBUTTON;
+#define SizedDtblButton(n,u) \
+struct _DTBLBUTTON_ ## u \
+{ \
+ DTBLBUTTON dtblbutton; \
+ TCHAR lpszLabel[n]; \
+} u
+
+/* Pages */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLPAGE
+{
+ ULONG ulbLpszLabel;
+ ULONG ulFlags;
+ ULONG ulbLpszComponent;
+ ULONG ulContext;
+} DTBLPAGE, FAR * LPDTBLPAGE;
+#define SizedDtblPage(n,n1,u) \
+struct _DTBLPAGE_ ## u \
+{ \
+ DTBLPAGE dtblpage; \
+ TCHAR lpszLabel[n]; \
+ TCHAR lpszComponent[n1]; \
+} u
+
+/* Radio button */
+/* Valid ulFlags:
+ * MAPI_UNICODE
+ */
+typedef struct _DTBLRADIOBUTTON
+{
+ ULONG ulbLpszLabel;
+ ULONG ulFlags;
+ ULONG ulcButtons;
+ ULONG ulPropTag;
+ long lReturnValue;
+} DTBLRADIOBUTTON, FAR * LPDTBLRADIOBUTTON;
+#define SizedDtblRadioButton(n,u) \
+struct _DTBLRADIOBUTTON_ ## u \
+{ \
+ DTBLRADIOBUTTON dtblradiobutton; \
+ TCHAR lpszLabel[n]; \
+} u
+
+
+/* MultiValued listbox */
+/* Valid ulFlags:
+ * none
+ */
+typedef struct _DTBLMVLISTBOX
+{
+ ULONG ulFlags;
+ ULONG ulMVPropTag;
+} DTBLMVLISTBOX, FAR * LPDTBLMVLISTBOX;
+
+
+/* MultiValued dropdown */
+/* Valid ulFlags:
+ * none
+ */
+typedef struct _DTBLMVDDLBX
+{
+ ULONG ulFlags;
+ ULONG ulMVPropTag;
+} DTBLMVDDLBX, FAR * LPDTBLMVDDLBX;
+
+
+
+
+
+/* IProviderAdmin Interface ---------------------------------------------- */
+
+/* Flags for ConfigureMsgService */
+
+#define UI_SERVICE 0x00000002
+#define SERVICE_UI_ALWAYS 0x00000002 /* Duplicate UI_SERVICE for consistency and compatibility */
+#define SERVICE_UI_ALLOWED 0x00000010
+#define UI_CURRENT_PROVIDER_FIRST 0x00000004
+/* MSG_SERVICE_UI_READ_ONLY 0x00000008 - in MAPISPI.H */
+
+/* GetProviderTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) above */
+
+/* Values for PR_RESOURCE_FLAGS in message service table */
+
+#define MAPI_IPROVIDERADMIN_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetProviderTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(CreateProvider) \
+ (THIS_ LPTSTR lpszProvider, \
+ ULONG cValues, \
+ LPSPropValue lpProps, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ MAPIUID FAR * lpUID) IPURE; \
+ MAPIMETHOD(DeleteProvider) \
+ (THIS_ LPMAPIUID lpUID) IPURE; \
+ MAPIMETHOD(OpenProfileSection) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPPROFSECT FAR * lppProfSect) IPURE; \
+
+
+#undef INTERFACE
+#define INTERFACE IProviderAdmin
+DECLARE_MAPI_INTERFACE_(IProviderAdmin, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IPROVIDERADMIN_METHODS(PURE)
+};
+
+
+
+/* IMAPIClientShutdown Interface ----------------------------------------- */
+DECLARE_MAPI_INTERFACE_PTR(IMAPIClientShutdown, LPMAPICLIENTSHUTDOWN);
+
+#define MAPI_IMAPICLIENTSHUTDOWN_METHODS(IPURE) \
+ MAPIMETHOD(QueryFastShutdown) \
+ (THIS) IPURE; \
+ MAPIMETHOD(NotifyProcessShutdown) \
+ (THIS) IPURE; \
+ MAPIMETHOD(DoFastShutdown) \
+ (THIS) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIClientShutdown
+DECLARE_MAPI_INTERFACE_(IMAPIClientShutdown, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPICLIENTSHUTDOWN_METHODS(PURE)
+};
+
+
+/* IMAPIProviderShutdown Interface --------------------------------------- */
+DECLARE_MAPI_INTERFACE_PTR(IMAPIProviderShutdown, LPMAPIPROVIDERSHUTDOWN);
+
+#define MAPI_IMAPIPROVIDERSHUTDOWN_METHODS(IPURE) \
+ MAPIMETHOD(QueryFastShutdown) \
+ (THIS) IPURE; \
+ MAPIMETHOD(NotifyProcessShutdown) \
+ (THIS) IPURE; \
+ MAPIMETHOD(DoFastShutdown) \
+ (THIS) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIProviderShutdown
+DECLARE_MAPI_INTERFACE_(IMAPIProviderShutdown, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROVIDERSHUTDOWN_METHODS(PURE)
+};
+
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MAPIDEFS_H */
diff --git a/comm/mailnews/mapi/include/mapiform.h b/comm/mailnews/mapi/include/mapiform.h
new file mode 100644
index 0000000000..e6ab3970ee
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiform.h
@@ -0,0 +1,632 @@
+/*
+ * M A P I F O R M . H
+ *
+ * Declarations of interfaces for clients and providers of MAPI
+ * forms and form registries.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPIFORM_H
+#define MAPIFORM_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+/* Include common MAPI header files if they haven't been already. */
+
+#ifndef MAPIDEFS_H
+#include <mapidefs.h>
+#include <mapicode.h>
+#include <mapiguid.h>
+#include <mapitags.h>
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+#ifndef _MAC
+typedef const RECT FAR *LPCRECT;
+#endif
+
+/* HFRMREG is an enumeration which represents a registry container.
+ * Microsoft reserves the values from 0 to 0x3FFF for its own use.
+ */
+
+typedef ULONG HFRMREG;
+
+#define HFRMREG_DEFAULT 0
+#define HFRMREG_LOCAL 1
+#define HFRMREG_PERSONAL 2
+#define HFRMREG_FOLDER 3
+
+DECLARE_MAPI_INTERFACE_PTR(IPersistMessage, LPPERSISTMESSAGE);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIMessageSite, LPMAPIMESSAGESITE);
+DECLARE_MAPI_INTERFACE_PTR(IMAPISession, LPMAPISESSION);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIViewContext, LPMAPIVIEWCONTEXT);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIViewAdviseSink, LPMAPIVIEWADVISESINK);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFormAdviseSink, LPMAPIFORMADVISESINK);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFormInfo, LPMAPIFORMINFO);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFormMgr, LPMAPIFORMMGR);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFormContainer, LPMAPIFORMCONTAINER);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIForm, LPMAPIFORM);
+DECLARE_MAPI_INTERFACE_PTR(IMAPIFormFactory, LPMAPIFORMFACTORY);
+
+typedef const char FAR *FAR * LPPCSTR;
+typedef LPMAPIFORMINFO FAR *LPPMAPIFORMINFO;
+
+STDAPI MAPIOpenFormMgr(LPMAPISESSION pSession, LPMAPIFORMMGR FAR * ppmgr);
+STDAPI MAPIOpenLocalFormContainer(LPMAPIFORMCONTAINER FAR * ppfcnt);
+
+
+/*-- GetLastError ----------------------------------------------------------*/
+/* This defines the GetLastError method held in common by most mapiform
+ * interfaces. It is defined separately so that an implementor may include
+ * more than one mapiform interface in a class.
+ */
+
+#define MAPI_GETLASTERROR_METHOD(IPURE) \
+ MAPIMETHOD(GetLastError) (THIS_ \
+ /*in*/ HRESULT hResult, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+
+
+/*-- IPersistMessage -------------------------------------------------------*/
+/* This interface is implemented by forms and is used to save,
+ * initialize and load forms to and from messages.
+ */
+
+#define MAPI_IPERSISTMESSAGE_METHODS(IPURE) \
+ MAPIMETHOD(GetClassID) (THIS_ LPCLSID lpClassID) IPURE; \
+ MAPIMETHOD(IsDirty)(THIS) IPURE; \
+ MAPIMETHOD(InitNew)(THIS_ \
+ /*in*/ LPMAPIMESSAGESITE pMessageSite, \
+ /*in*/ LPMESSAGE pMessage) IPURE; \
+ MAPIMETHOD(Load)(THIS_ \
+ /*in*/ LPMAPIMESSAGESITE pMessageSite, \
+ /*in*/ LPMESSAGE pMessage, \
+ /*in*/ ULONG ulMessageStatus, \
+ /*in*/ ULONG ulMessageFlags) IPURE; \
+ MAPIMETHOD(Save)(THIS_ \
+ /*in*/ LPMESSAGE pMessage, \
+ /*in*/ ULONG fSameAsLoad) IPURE; \
+ MAPIMETHOD(SaveCompleted)(THIS_ \
+ /*in*/ LPMESSAGE pMessage) IPURE; \
+ MAPIMETHOD(HandsOffMessage)(THIS) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IPersistMessage
+DECLARE_MAPI_INTERFACE_(IPersistMessage, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IPERSISTMESSAGE_METHODS(PURE)
+};
+
+
+/*-- IMAPIMessageSite ------------------------------------------------------*/
+
+#define MAPI_IMAPIMESSAGESITE_METHODS(IPURE) \
+ MAPIMETHOD(GetSession) (THIS_ \
+ /*out*/ LPMAPISESSION FAR * ppSession) IPURE; \
+ MAPIMETHOD(GetStore) (THIS_ \
+ /*out*/ LPMDB FAR * ppStore) IPURE; \
+ MAPIMETHOD(GetFolder) (THIS_ \
+ /*out*/ LPMAPIFOLDER FAR * ppFolder) IPURE; \
+ MAPIMETHOD(GetMessage) (THIS_ \
+ /*out*/ LPMESSAGE FAR * ppmsg) IPURE; \
+ MAPIMETHOD(GetFormManager) (THIS_ \
+ /*out*/ LPMAPIFORMMGR FAR * ppFormMgr) IPURE; \
+ MAPIMETHOD(NewMessage) (THIS_ \
+ /*in*/ ULONG fComposeInFolder, \
+ /*in*/ LPMAPIFOLDER pFolderFocus, \
+ /*in*/ LPPERSISTMESSAGE pPersistMessage, \
+ /*out*/ LPMESSAGE FAR * ppMessage, \
+ /*out*/ LPMAPIMESSAGESITE FAR * ppMessageSite, \
+ /*out*/ LPMAPIVIEWCONTEXT FAR * ppViewContext) IPURE; \
+ MAPIMETHOD(CopyMessage) (THIS_ \
+ /*in*/ LPMAPIFOLDER pFolderDestination) IPURE; \
+ MAPIMETHOD(MoveMessage) (THIS_ \
+ /*in*/ LPMAPIFOLDER pFolderDestination, \
+ /*in*/ LPMAPIVIEWCONTEXT pViewContext, \
+ /*in*/ LPCRECT prcPosRect) IPURE; \
+ MAPIMETHOD(DeleteMessage) (THIS_ \
+ /*in*/ LPMAPIVIEWCONTEXT pViewContext, \
+ /*in*/ LPCRECT prcPosRect) IPURE; \
+ MAPIMETHOD(SaveMessage) (THIS) IPURE; \
+ MAPIMETHOD(SubmitMessage) (THIS_ \
+ /*in*/ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetSiteStatus) (THIS_ \
+ /*out*/ LPULONG lpulStatus) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIMessageSite
+DECLARE_MAPI_INTERFACE_(IMAPIMessageSite, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIMESSAGESITE_METHODS(PURE)
+};
+
+
+/*-- IMAPIForm -------------------------------------------------------------*/
+/* This interface is implemented by forms for the benefit of viewers.
+ * One method (ShutdownForm) is provided such that simple forms implementing
+ * only IMAPIForm and IPersistMessage have reasonable embedding behavior.
+ */
+
+#define MAPI_IMAPIFORM_METHODS(IPURE) \
+ MAPIMETHOD(SetViewContext) (THIS_ \
+ /*in*/ LPMAPIVIEWCONTEXT pViewContext) IPURE; \
+ MAPIMETHOD(GetViewContext) (THIS_ \
+ /*out*/ LPMAPIVIEWCONTEXT FAR * ppViewContext) IPURE; \
+ MAPIMETHOD(ShutdownForm)(THIS_ \
+ /*in*/ ULONG ulSaveOptions) IPURE; \
+ MAPIMETHOD(DoVerb) (THIS_ \
+ /*in*/ LONG iVerb, \
+ /*in*/ LPMAPIVIEWCONTEXT lpViewContext, /* can be null */ \
+ /*in*/ ULONG_PTR hwndParent, \
+ /*in*/ LPCRECT lprcPosRect) IPURE; \
+ MAPIMETHOD(Advise)(THIS_ \
+ /*in*/ LPMAPIVIEWADVISESINK pAdvise, \
+ /*out*/ ULONG_PTR FAR * pdwStatus) IPURE; \
+ MAPIMETHOD(Unadvise) (THIS_ \
+ /*in*/ ULONG_PTR ulConnection) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIForm
+DECLARE_MAPI_INTERFACE_(IMAPIForm, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIFORM_METHODS(PURE)
+};
+
+typedef enum tagSAVEOPTS
+{
+ SAVEOPTS_SAVEIFDIRTY = 0,
+ SAVEOPTS_NOSAVE = 1,
+ SAVEOPTS_PROMPTSAVE = 2
+} SAVEOPTS;
+
+
+/*-- IMAPIViewContext ------------------------------------------------------*/
+/* Implemented by viewers to support next/previous in forms.
+ */
+
+/* Structure passed in GetPrintSetup */
+
+typedef struct {
+ ULONG ulFlags; /* MAPI_UNICODE */
+ HGLOBAL hDevMode;
+ HGLOBAL hDevNames;
+ ULONG ulFirstPageNumber;
+ ULONG fPrintAttachments;
+} FORMPRINTSETUP, FAR * LPFORMPRINTSETUP;
+
+/* Values for pulFormat in GetSaveStream */
+
+#define SAVE_FORMAT_TEXT 1
+#define SAVE_FORMAT_RICHTEXT 2
+
+/* Values from 0 to 0x3fff are reserved for future definition by Microsoft */
+
+#define MAPI_IMAPIVIEWCONTEXT_METHODS(IPURE) \
+ MAPIMETHOD(SetAdviseSink)(THIS_ \
+ /*in*/ LPMAPIFORMADVISESINK pmvns) IPURE; \
+ MAPIMETHOD(ActivateNext)(THIS_ \
+ /*in*/ ULONG ulDir, \
+ /*in*/ LPCRECT prcPosRect) IPURE; \
+ MAPIMETHOD(GetPrintSetup)(THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPFORMPRINTSETUP FAR * lppFormPrintSetup) IPURE; \
+ MAPIMETHOD(GetSaveStream)(THIS_ \
+ /*out*/ ULONG FAR * pulFlags, \
+ /*out*/ ULONG FAR * pulFormat, \
+ /*out*/ LPSTREAM FAR * ppstm) IPURE; \
+ MAPIMETHOD(GetViewStatus) (THIS_ \
+ /*out*/ LPULONG lpulStatus) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIViewContext
+DECLARE_MAPI_INTERFACE_(IMAPIViewContext, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIVIEWCONTEXT_METHODS(PURE)
+};
+
+#define VCSTATUS_NEXT 0x00000001
+#define VCSTATUS_PREV 0x00000002
+#define VCSTATUS_MODAL 0x00000004
+#define VCSTATUS_INTERACTIVE 0x00000008
+#define VCSTATUS_READONLY 0x00000010
+#define VCSTATUS_DELETE 0x00010000
+#define VCSTATUS_COPY 0x00020000
+#define VCSTATUS_MOVE 0x00040000
+#define VCSTATUS_SUBMIT 0x00080000
+#define VCSTATUS_DELETE_IS_MOVE 0x00100000
+#define VCSTATUS_SAVE 0x00200000
+#define VCSTATUS_NEW_MESSAGE 0x00400000
+
+#define VCDIR_NEXT VCSTATUS_NEXT
+#define VCDIR_PREV VCSTATUS_PREV
+#define VCDIR_DELETE VCSTATUS_DELETE
+#define VCDIR_MOVE VCSTATUS_MOVE
+
+
+/*-- IMAPIFormAdviseSink ---------------------------------------------------*/
+/* Part of form server, held by view; receives notifications from the view.
+ *
+ * This part of the form server, but is not an interface on the form
+ * object. This means that clients should not expect to QueryInterface
+ * from an IMAPIForm* or IOleObject* to this interface, or vice versa.
+ */
+
+#define MAPI_IMAPIFORMADVISESINK_METHODS(IPURE) \
+ STDMETHOD(OnChange)(THIS_ ULONG ulDir) IPURE; \
+ STDMETHOD(OnActivateNext)(THIS_ \
+ /*in*/ LPCSTR lpszMessageClass, \
+ /*in*/ ULONG ulMessageStatus, \
+ /*in*/ ULONG ulMessageFlags, \
+ /*out*/ LPPERSISTMESSAGE FAR * ppPersistMessage) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFormAdviseSink
+DECLARE_MAPI_INTERFACE_(IMAPIFormAdviseSink, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIFORMADVISESINK_METHODS(PURE)
+};
+
+
+/*-- IMAPIViewAdviseSink ---------------------------------------------------*/
+/* Part of view context, held by form; receives notifications from the form.
+ */
+
+#define MAPI_IMAPIVIEWADVISESINK_METHODS(IPURE) \
+ MAPIMETHOD(OnShutdown)(THIS) IPURE; \
+ MAPIMETHOD(OnNewMessage)(THIS) IPURE; \
+ MAPIMETHOD(OnPrint)(THIS_ \
+ /*in*/ ULONG dwPageNumber, \
+ /*in*/ HRESULT hrStatus) IPURE; \
+ MAPIMETHOD(OnSubmitted) (THIS) IPURE; \
+ MAPIMETHOD(OnSaved) (THIS) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIViewAdviseSink
+DECLARE_MAPI_INTERFACE_(IMAPIViewAdviseSink, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIVIEWADVISESINK_METHODS(PURE)
+};
+
+
+/*-- IMAPIFormInfo ---------------------------------------------------------*/
+/* Is implemented by registries. Describes the form.
+ */
+
+/* Single enum value */
+
+typedef struct
+{ /* fpev */
+ LPTSTR pszDisplayName; /* carries the display string */
+ ULONG nVal; /* the value for the above enumeration */
+} SMAPIFormPropEnumVal, FAR * LPMAPIFORMPROPENUMVAL;
+
+/* MAPI Form property descriptor */
+
+/*
+ * Values for the tag in the SMAPIFormProp structure
+ *
+ * Microsoft reserves the range from 0 to 0x3FFF for future use in its other
+ * forms registry implementations.
+ */
+
+typedef ULONG FORMPROPSPECIALTYPE;
+
+#define FPST_VANILLA 0
+#define FPST_ENUM_PROP 1
+
+typedef struct
+{
+ ULONG ulFlags; /* Contains MAPI_UNICODE if strings are UNICODE */
+ ULONG nPropType; /* type of the property, hiword is 0 */
+ MAPINAMEID nmid; /* id of the property */
+ LPTSTR pszDisplayName;
+ FORMPROPSPECIALTYPE nSpecialType; /* tag for the following union */
+ union
+ {
+ struct
+ {
+ MAPINAMEID nmidIdx;
+ ULONG cfpevAvailable; /* # of enums */
+ LPMAPIFORMPROPENUMVAL pfpevAvailable;
+ } s1; /* Property String/Number association Enumeration */
+ } u;
+} SMAPIFormProp, FAR * LPMAPIFORMPROP;
+
+/* Array of form properties */
+
+typedef struct
+{
+ ULONG cProps;
+ ULONG ulPad; /* Pad to 8-byte alignment for insurance */
+ SMAPIFormProp aFormProp[MAPI_DIM];
+} SMAPIFormPropArray, FAR * LPMAPIFORMPROPARRAY;
+
+#define CbMAPIFormPropArray(_c) \
+ (offsetof(SMAPIFormPropArray, aFormProp) + \
+ (_c)*sizeof(SMAPIFormProp))
+
+/* Structure defining the layout of an mapi verb description */
+
+typedef struct
+{
+ LONG lVerb;
+ LPTSTR szVerbname;
+ DWORD fuFlags;
+ DWORD grfAttribs;
+ ULONG ulFlags; /* Either 0 or MAPI_UNICODE */
+} SMAPIVerb, FAR * LPMAPIVERB;
+
+/* Structure used for returning arrays of mapi verbs */
+
+typedef struct
+{
+ ULONG cMAPIVerb; /* Number of verbs in the structure */
+ SMAPIVerb aMAPIVerb[MAPI_DIM];
+} SMAPIVerbArray, FAR * LPMAPIVERBARRAY;
+
+#define CbMAPIVerbArray(_c) \
+ (offsetof(SMAPIVerbArray, aMAPIVerb) + \
+ (_c)*sizeof(SMAPIVerb))
+
+#define MAPI_IMAPIFORMINFO_METHODS(IPURE) \
+ MAPIMETHOD(CalcFormPropSet)(THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIFORMPROPARRAY FAR * ppFormPropArray) IPURE; \
+ MAPIMETHOD(CalcVerbSet)(THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIVERBARRAY FAR * ppMAPIVerbArray) IPURE; \
+ MAPIMETHOD(MakeIconFromBinary)(THIS_ \
+ /*in*/ ULONG nPropID, \
+ /*out*/ HICON FAR* phicon) IPURE; \
+ MAPIMETHOD(SaveForm)(THIS_ \
+ /*in*/ LPCTSTR szFileName) IPURE; \
+ MAPIMETHOD(OpenFormContainer)(THIS_ \
+ /*out*/ LPMAPIFORMCONTAINER FAR * ppformcontainer) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFormInfo
+DECLARE_MAPI_INTERFACE_(IMAPIFormInfo, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE) /* note: subsumes getlasterror */
+ MAPI_IMAPIFORMINFO_METHODS(PURE)
+};
+
+
+/* Enumeration of permissible values for PR_FORM_MESSAGE_BEHAVIOR */
+
+#define MAPI_MESSAGE_BEHAVIOR_IPM 0
+#define MAPI_MESSAGE_BEHAVIOR_FOLDER 1
+
+
+/*-- IMAPIFormMgr ----------------------------------------------------------*/
+/* The client-visible interface for form resolution and dispatch.
+ */
+
+/* Structure containing an array of message class strings */
+
+typedef struct
+{
+ ULONG cValues;
+ LPCSTR aMessageClass[MAPI_DIM];
+} SMessageClassArray, FAR * LPSMESSAGECLASSARRAY;
+
+#define CbMessageClassArray(_c) \
+ (offsetof(SMessageClassArray, aMessageClass) + (_c)*sizeof(LPCSTR))
+
+/* Structure containing an array of IMAPIFormInfo interfaces */
+
+typedef struct
+{
+ ULONG cForms;
+ LPMAPIFORMINFO aFormInfo[MAPI_DIM];
+} SMAPIFormInfoArray, FAR * LPSMAPIFORMINFOARRAY;
+
+#define CbMAPIFormInfoArray(_c) \
+ (offsetof(SMAPIFormInfoArray, aFormInfo) + \
+ (_c)*sizeof(LPMAPIFORMINFO))
+
+/* Flags for IMAPIFormMgr::SelectFormContainer */
+
+#define MAPIFORM_SELECT_ALL_REGISTRIES 0
+#define MAPIFORM_SELECT_FOLDER_REGISTRY_ONLY 1
+#define MAPIFORM_SELECT_NON_FOLDER_REGISTRY_ONLY 2
+
+/* Flags for IMAPIFormMgr::CalcFormPropSet */
+
+#define FORMPROPSET_UNION 0
+#define FORMPROPSET_INTERSECTION 1
+
+/* Flags for IMAPIFormMgr::ResolveMessageClass and
+ IMAPIFormMgr::ResolveMultipleMessageClasses */
+
+#define MAPIFORM_EXACTMATCH 0x0020
+
+#define MAPI_IMAPIFORMMGR_METHODS(IPURE) \
+ MAPIMETHOD(LoadForm)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPCSTR lpszMessageClass, \
+ /*in*/ ULONG ulMessageStatus, \
+ /*in*/ ULONG ulMessageFlags, \
+ /*in*/ LPMAPIFOLDER pFolderFocus, \
+ /*in*/ LPMAPIMESSAGESITE pMessageSite, \
+ /*in*/ LPMESSAGE pmsg, \
+ /*in*/ LPMAPIVIEWCONTEXT pViewContext, \
+ /*in*/ REFIID riid, \
+ /*out*/ LPVOID FAR *ppvObj) IPURE; \
+ MAPIMETHOD(ResolveMessageClass)(THIS_ \
+ /*in*/ LPCSTR szMsgClass, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPMAPIFOLDER pFolderFocus, /* can be null */ \
+ /*out*/ LPMAPIFORMINFO FAR* ppResult) IPURE; \
+ MAPIMETHOD(ResolveMultipleMessageClasses)(THIS_ \
+ /*in*/ LPSMESSAGECLASSARRAY pMsgClasses, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPMAPIFOLDER pFolderFocus, /* can be null */ \
+ /*out*/ LPSMAPIFORMINFOARRAY FAR * pfrminfoarray) IPURE; \
+ MAPIMETHOD(CalcFormPropSet)(THIS_ \
+ /*in*/ LPSMAPIFORMINFOARRAY pfrminfoarray, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIFORMPROPARRAY FAR* ppResults) IPURE; \
+ MAPIMETHOD(CreateForm)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPMAPIFORMINFO pfrminfoToActivate, \
+ /*in*/ REFIID refiidToAsk, \
+ /*out*/ LPVOID FAR* ppvObj) IPURE; \
+ MAPIMETHOD(SelectForm)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPCTSTR pszTitle, \
+ /*in*/ LPMAPIFOLDER pfld, \
+ /*out*/ LPMAPIFORMINFO FAR * ppfrminfoReturned) IPURE; \
+ MAPIMETHOD(SelectMultipleForms)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPCTSTR pszTitle, \
+ /*in*/ LPMAPIFOLDER pfld, \
+ /*in*/ LPSMAPIFORMINFOARRAY pfrminfoarray, \
+ /*out*/ LPSMAPIFORMINFOARRAY FAR * ppfrminfoarray) IPURE; \
+ MAPIMETHOD(SelectFormContainer)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIFORMCONTAINER FAR * lppfcnt) IPURE; \
+ MAPIMETHOD(OpenFormContainer)(THIS_ \
+ /*in*/ HFRMREG hfrmreg, \
+ /*in*/ LPUNKNOWN lpunk, \
+ /*out*/ LPMAPIFORMCONTAINER FAR * lppfcnt) IPURE; \
+ MAPIMETHOD(PrepareForm)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPMAPIFORMINFO pfrminfo) IPURE; \
+ MAPIMETHOD(IsInConflict)(THIS_ \
+ /*in*/ ULONG ulMessageFlags, \
+ /*in*/ ULONG ulMessageStatus, \
+ /*in*/ LPCSTR szMessageClass, \
+ /*in*/ LPMAPIFOLDER pFolderFocus) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFormMgr
+DECLARE_MAPI_INTERFACE_(IMAPIFormMgr, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIFORMMGR_METHODS(PURE)
+};
+
+/* Platform numbers (used in .CFG files for forms) */
+
+#define MAPIFORM_CPU_X86 1
+#define MAPIFORM_CPU_MIP 2
+#define MAPIFORM_CPU_AXP 3
+#define MAPIFORM_CPU_PPC 4
+#define MAPIFORM_CPU_M68 5
+#define MAPIFORM_CPU_X64 6
+
+#define MAPIFORM_OS_WIN_31 1
+#define MAPIFORM_OS_WINNT_35 2
+#define MAPIFORM_OS_WIN_95 3
+#define MAPIFORM_OS_MAC_7x 4
+#define MAPIFORM_OS_WINNT_40 5
+#define MAPIFORM_OS_WINNT_50 6
+#define MAPIFORM_OS_WINNT_60 7
+
+#define MAPIFORM_PLATFORM(CPU, OS) ((ULONG) ((((ULONG) CPU) << 16) | OS))
+
+
+/*-- IMAPIFormContainer -------------------------------------------------*/
+
+/* Flags for IMAPIFormMgr::CalcFormPropSet */
+
+/* #define FORMPROPSET_UNION 0 */
+/* #define FORMPROPSET_INTERSECTION 1 */
+
+/* Flags for IMAPIFormMgr::InstallForm */
+
+#define MAPIFORM_INSTALL_DIALOG MAPI_DIALOG
+#define MAPIFORM_INSTALL_OVERWRITEONCONFLICT 0x0010
+
+/* Flags for IMAPIFormContainer::ResolveMessageClass and
+ IMAPIFormContainer::ResolveMultipleMessageClasses */
+/* #define MAPIFORM_EXACTIMATCH 0x0020 */
+
+#define MAPI_IMAPIFORMCONTAINER_METHODS(IPURE) \
+ MAPIMETHOD(InstallForm)(THIS_ \
+ /*in*/ ULONG_PTR ulUIParam, \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ LPCTSTR szCfgPathName) IPURE; \
+ MAPIMETHOD(RemoveForm)(THIS_ \
+ /*in*/ LPCSTR szMessageClass) IPURE; \
+ MAPIMETHOD(ResolveMessageClass) (THIS_ \
+ /*in*/ LPCSTR szMessageClass, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIFORMINFO FAR * pforminfo) IPURE; \
+ MAPIMETHOD(ResolveMultipleMessageClasses) (THIS_ \
+ /*in*/ LPSMESSAGECLASSARRAY pMsgClassArray, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPSMAPIFORMINFOARRAY FAR * ppfrminfoarray) IPURE; \
+ MAPIMETHOD(CalcFormPropSet)(THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPMAPIFORMPROPARRAY FAR * ppResults) IPURE; \
+ MAPIMETHOD(GetDisplay)(THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPTSTR FAR * pszDisplayName) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFormContainer
+DECLARE_MAPI_INTERFACE_(IMAPIFormContainer, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIFORMCONTAINER_METHODS(PURE)
+};
+
+/*-- IMAPIFormFactory ------------------------------------------------------*/
+
+#define MAPI_IMAPIFORMFACTORY_METHODS(IPURE) \
+ MAPIMETHOD(CreateClassFactory) (THIS_ \
+ /*in*/ REFCLSID clsidForm, \
+ /*in*/ ULONG ulFlags, \
+ /*out*/ LPCLASSFACTORY FAR * lppClassFactory) IPURE; \
+ MAPIMETHOD(LockServer) (THIS_ \
+ /*in*/ ULONG ulFlags, \
+ /*in*/ ULONG fLockServer) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPIFormFactory
+DECLARE_MAPI_INTERFACE_(IMAPIFormFactory, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_GETLASTERROR_METHOD(PURE)
+ MAPI_IMAPIFORMFACTORY_METHODS(PURE)
+};
+
+#endif /* MAPIFORM_H */
diff --git a/comm/mailnews/mapi/include/mapiguid.h b/comm/mailnews/mapi/include/mapiguid.h
new file mode 100644
index 0000000000..f7b9dc71fb
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiguid.h
@@ -0,0 +1,353 @@
+/*
+ * M A P I G U I D . H
+ *
+ * Master definitions of all GUID's for MAPI.
+ *
+ * When included without INITGUID defined, this header file
+ * defines symbols that reference IIDs elsewhere.
+ *
+ * When included with INITGUID defined and a "USES_IID_I..."
+ * statement for each IID used by the subsystem, it generates the
+ * bytes for those actual IIDs into the associated object file.
+ *
+ * This range of 256 GUIDs reserved by OLE for MAPI use October 5, 1992.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+/*
+ * List of GUIDS allocated by MAPI
+ *
+ * 0x00020300 IID_IMAPISession
+ * 0x00020301 IID_IMAPITable
+ * 0x00020302 IID_IMAPIAdviseSink
+ * 0x00020303 IID_IMAPIProp
+ * 0x00020304 IID_IProfSect
+ * 0x00020305 IID_IMAPIStatus
+ * 0x00020306 IID_IMsgStore
+ * 0x00020307 IID_IMessage
+ * 0x00020308 IID_IAttachment
+ * 0x00020309 IID_IAddrBook
+ * 0x0002030A IID_IMailUser
+ * 0x0002030B IID_IMAPIContainer
+ * 0x0002030C IID_IMAPIFolder
+ * 0x0002030D IID_IABContainer
+ * 0x0002030E IID_IDistList
+ * 0x0002030F IID_IMAPISup
+ * 0x00020310 IID_IMSProvider
+ * 0x00020311 IID_IABProvider
+ * 0x00020312 IID_IXPProvider
+ * 0x00020313 IID_IMSLogon
+ * 0x00020314 IID_IABLogon
+ * 0x00020315 IID_IXPLogon
+ * 0x00020316 IID_IMAPITableData
+ * 0x00020317 IID_IMAPISpoolerInit
+ * 0x00020318 IID_IMAPISpoolerSession
+ * 0x00020319 IID_ITNEF
+ * 0x0002031A IID_IMAPIPropData
+ * 0x0002031B IID_IMAPIControl
+ * 0x0002031C IID_IProfAdmin
+ * 0x0002031D IID_IMsgServiceAdmin
+ * 0x0002031E IID_IMAPISpoolerService
+ * 0x0002031F IID_IMAPIProgress
+ * 0x00020320 IID_ISpoolerHook
+ * 0x00020321 IID_IMAPIViewContext
+ * 0x00020322 IID_IMAPIFormMgr
+ * 0x00020323 IID_IEnumMAPIFormProp
+ * 0x00020324 IID_IMAPIFormInfo
+ * 0x00020325 IID_IProviderAdmin
+ * 0x00020327 IID_IMAPIForm
+ * 0x00020328 PS_MAPI
+ * 0x00020329 PS_PUBLIC_STRINGS
+ * 0x0002032A IID_IPersistMessage
+ * 0x0002032B IID_IMAPIViewAdviseSink
+ * 0x0002032C IID_IStreamDocfile
+ * 0x0002032D IID_IMAPIFormProp
+ * 0x0002032E IID_IMAPIFormContainer
+ * 0x0002032F IID_IMAPIFormAdviseSink
+ * 0x00020330 IID_IStreamTnef
+ * 0x00020350 IID_IMAPIFormFactory
+ * 0x00020370 IID_IMAPIMessageSite
+ * 0x00020380 PS_ROUTING_EMAIL_ADDRESSES
+ * 0x00020381 PS_ROUTING_ADDRTYPE
+ * 0x00020382 PS_ROUTING_DISPLAY_NAME
+ * 0x00020383 PS_ROUTING_ENTRYID
+ * 0x00020384 PS_ROUTING_SEARCH_KEY
+ * 0x00020385 MUID_PROFILE_INSTANCE
+ * 0x00020397 IID_IMAPIClientShutdown
+ * 0x00020398 IID_IMAPIProviderShutdown
+ *
+ * The remaining GUIDs from 0x00020300 to 0x000203FF are reserved by
+ * MAPI for future use. The current maximum used by MAPI is 0x00020398
+ *
+ */
+
+#ifndef MAPIGUID_H
+#ifdef INITGUID
+#define MAPIGUID_H
+#if _MSC_VER > 1000
+#pragma once
+#endif
+#endif
+
+/* Derive from IUnknown */
+#if !defined(INITGUID) || defined(USES_IID_IMAPISession)
+DEFINE_OLEGUID(IID_IMAPISession, 0x00020300, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPITable)
+DEFINE_OLEGUID(IID_IMAPITable, 0x00020301, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIAdviseSink)
+DEFINE_OLEGUID(IID_IMAPIAdviseSink, 0x00020302, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIControl)
+DEFINE_OLEGUID(IID_IMAPIControl, 0x0002031B, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IProfAdmin)
+DEFINE_OLEGUID(IID_IProfAdmin, 0x0002031C, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMsgServiceAdmin)
+DEFINE_OLEGUID(IID_IMsgServiceAdmin,0x0002031D, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IProviderAdmin)
+DEFINE_OLEGUID(IID_IProviderAdmin, 0x00020325, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIProgress)
+DEFINE_OLEGUID(IID_IMAPIProgress, 0x0002031F, 0, 0);
+#endif
+
+/* MAPIProp or derive from MAPIProp */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIProp)
+DEFINE_OLEGUID(IID_IMAPIProp, 0x00020303, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IProfSect)
+DEFINE_OLEGUID(IID_IProfSect, 0x00020304, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIStatus)
+DEFINE_OLEGUID(IID_IMAPIStatus, 0x00020305, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMsgStore)
+DEFINE_OLEGUID(IID_IMsgStore, 0x00020306, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMessage)
+DEFINE_OLEGUID(IID_IMessage, 0x00020307, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IAttachment)
+DEFINE_OLEGUID(IID_IAttachment, 0x00020308, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IAddrBook)
+DEFINE_OLEGUID(IID_IAddrBook, 0x00020309, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMailUser)
+DEFINE_OLEGUID(IID_IMailUser, 0x0002030A, 0, 0);
+#endif
+
+/* MAPIContainer or derive from MAPIContainer */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIContainer)
+DEFINE_OLEGUID(IID_IMAPIContainer, 0x0002030B, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFolder)
+DEFINE_OLEGUID(IID_IMAPIFolder, 0x0002030C, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IABContainer)
+DEFINE_OLEGUID(IID_IABContainer, 0x0002030D, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IDistList)
+DEFINE_OLEGUID(IID_IDistList, 0x0002030E, 0, 0);
+#endif
+
+/* MAPI Support Object */
+#if !defined(INITGUID) || defined(USES_IID_IMAPISup)
+DEFINE_OLEGUID(IID_IMAPISup, 0x0002030F, 0, 0);
+#endif
+
+/* Provider INIT objects */
+#if !defined(INITGUID) || defined(USES_IID_IMSProvider)
+DEFINE_OLEGUID(IID_IMSProvider, 0x00020310, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IABProvider)
+DEFINE_OLEGUID(IID_IABProvider, 0x00020311, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IXPProvider)
+DEFINE_OLEGUID(IID_IXPProvider, 0x00020312, 0, 0);
+#endif
+
+/* Provider LOGON Objects */
+#if !defined(INITGUID) || defined(USES_IID_IMSLogon)
+DEFINE_OLEGUID(IID_IMSLogon, 0x00020313, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IABLogon)
+DEFINE_OLEGUID(IID_IABLogon, 0x00020314, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IXPLogon)
+DEFINE_OLEGUID(IID_IXPLogon, 0x00020315, 0, 0);
+#endif
+
+/* IMAPITable-in-memory Table Data Object */
+#if !defined(INITGUID) || defined(USES_IID_IMAPITableData)
+DEFINE_OLEGUID(IID_IMAPITableData, 0x00020316, 0, 0);
+#endif
+
+/* MAPI Spooler Init Object (internal) */
+#if !defined(INITGUID) || defined(USES_IID_IMAPISpoolerInit)
+DEFINE_OLEGUID(IID_IMAPISpoolerInit, 0x00020317, 0, 0);
+#endif
+
+/* MAPI Spooler Session Object (internal) */
+#if !defined(INITGUID) || defined(USES_IID_IMAPISpoolerSession)
+DEFINE_OLEGUID(IID_IMAPISpoolerSession, 0x00020318, 0, 0);
+#endif
+
+/* MAPI TNEF Object Interface */
+#if !defined(INITGUID) || defined(USES_IID_ITNEF)
+DEFINE_OLEGUID(IID_ITNEF, 0x00020319, 0, 0);
+#endif
+
+/* IMAPIProp-in-memory Property Data Object */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIPropData)
+DEFINE_OLEGUID(IID_IMAPIPropData, 0x0002031A, 0, 0);
+#endif
+
+/* MAPI Spooler Hook Object */
+#if !defined(INITGUID) || defined(USES_IID_ISpoolerHook)
+DEFINE_OLEGUID(IID_ISpoolerHook, 0x00020320, 0, 0);
+#endif
+
+/* MAPI Spooler Service Object */
+#if !defined(INITGUID) || defined(USES_IID_IMAPISpoolerService)
+DEFINE_OLEGUID(IID_IMAPISpoolerService, 0x0002031E, 0, 0);
+#endif
+
+/* MAPI forms, form manager, etc. */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIViewContext)
+DEFINE_OLEGUID(IID_IMAPIViewContext, 0x00020321, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormMgr)
+DEFINE_OLEGUID(IID_IMAPIFormMgr, 0x00020322, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IEnumMAPIFormProp)
+DEFINE_OLEGUID(IID_IEnumMAPIFormProp, 0x00020323, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormInfo)
+DEFINE_OLEGUID(IID_IMAPIFormInfo, 0x00020324, 0, 0);
+#endif
+#if !defined(INITGUID) || defined(USES_IID_IMAPIForm)
+DEFINE_OLEGUID(IID_IMAPIForm, 0x00020327, 0, 0);
+#endif
+
+
+/* Well known guids for name<->id mappings */
+
+/* The name of MAPI's property set */
+#if !defined(INITGUID) || defined(USES_PS_MAPI)
+DEFINE_OLEGUID(PS_MAPI, 0x00020328, 0, 0);
+#endif
+
+/* The name of the set of public strings */
+#if !defined(INITGUID) || defined(USES_PS_PUBLIC_STRINGS)
+DEFINE_OLEGUID(PS_PUBLIC_STRINGS, 0x00020329, 0, 0);
+#endif
+
+
+
+
+/* MAPI forms, form manager, (cont) */
+#if !defined(INITGUID) || defined(USES_IID_IPersistMessage)
+DEFINE_OLEGUID(IID_IPersistMessage, 0x0002032A, 0, 0);
+#endif
+
+/* IMAPIViewAdviseSink */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIViewAdviseSink)
+DEFINE_OLEGUID(IID_IMAPIViewAdviseSink, 0x0002032B, 0, 0);
+#endif
+
+/* Message Store OpenProperty */
+#if !defined(INITGUID) || defined(USES_IID_IStreamDocfile)
+DEFINE_OLEGUID(IID_IStreamDocfile, 0x0002032C, 0, 0);
+#endif
+
+/* IMAPIFormProp */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormProp)
+DEFINE_OLEGUID(IID_IMAPIFormProp, 0x0002032D, 0, 0);
+#endif
+
+/* IMAPIFormContainer */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormContainer)
+DEFINE_OLEGUID(IID_IMAPIFormContainer, 0x0002032E, 0, 0);
+#endif
+
+/* IMAPIFormAdviseSink */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormAdviseSink)
+DEFINE_OLEGUID(IID_IMAPIFormAdviseSink, 0x0002032F, 0, 0);
+#endif
+
+/* TNEF OpenProperty */
+#if !defined(INITGUID) || defined(USES_IID_IStreamTnef)
+DEFINE_OLEGUID(IID_IStreamTnef, 0x00020330, 0, 0);
+#endif
+
+/* IMAPIFormFactory */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIFormFactory)
+DEFINE_OLEGUID(IID_IMAPIFormFactory, 0x00020350, 0, 0);
+#endif
+
+/* IMAPIMessageSite */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIMessageSite)
+DEFINE_OLEGUID(IID_IMAPIMessageSite, 0x00020370, 0, 0);
+#endif
+
+
+
+/* Well known guids routing property sets.
+ Useful when writing applications that route documents
+ (i.e. Workflow) across gateways. Gateways that speak MAPI
+ should convert the properties found in the follow property
+ sets appropriately. */
+
+/* PS_ROUTING_EMAIL_ADDRESSES: Addresses that need converting at gateways, etc. */
+#if !defined(INITGUID) || defined(USES_PS_ROUTING_EMAIL_ADDRESSES)
+DEFINE_OLEGUID(PS_ROUTING_EMAIL_ADDRESSES, 0x00020380, 0, 0);
+#endif
+
+/* PS_ROUTING_ADDRTYPE: Address types that need converting at gateways, etc. */
+#if !defined(INITGUID) || defined(USES_PS_ROUTING_ADDRTYPE)
+DEFINE_OLEGUID(PS_ROUTING_ADDRTYPE, 0x00020381, 0, 0);
+#endif
+
+/* PS_ROUTING_DISPLAY_NAME: Display Name that corresponds to the other props */
+#if !defined(INITGUID) || defined(USES_PS_ROUTING_DISPLAY_NAME)
+DEFINE_OLEGUID(PS_ROUTING_DISPLAY_NAME, 0x00020382, 0, 0);
+#endif
+
+/* PS_ROUTING_ENTRYID: (optional) EntryIDs that need converting at gateways, etc. */
+#if !defined(INITGUID) || defined(USES_PS_ROUTING_ENTRYID)
+DEFINE_OLEGUID(PS_ROUTING_ENTRYID, 0x00020383, 0, 0);
+#endif
+
+/* PS_ROUTING_SEARCH_KEY: (optional) search keys that need converting at gateways, etc. */
+#if !defined(INITGUID) || defined(USES_PS_ROUTING_SEARCH_KEY)
+DEFINE_OLEGUID(PS_ROUTING_SEARCH_KEY, 0x00020384, 0, 0);
+#endif
+
+/* MUID_PROFILE_INSTANCE
+ Well known section in a profile which contains a property (PR_SEARCH_KEY) which is unique
+ for any given profile. Applications and providers can depend on this value as being
+ different for each unique profile. */
+#if !defined(INITGUID) || defined(USES_MUID_PROFILE_INSTANCE)
+DEFINE_OLEGUID(MUID_PROFILE_INSTANCE, 0x00020385, 0, 0);
+#endif
+
+
+/* Interface GUIDs for Fast Shutdown support */
+
+/* IMAPIClientShutdown */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIClientShutdown)
+DEFINE_OLEGUID(IID_IMAPIClientShutdown, 0x00020397, 0, 0);
+#endif
+
+/* IMAPIProviderShutdown */
+#if !defined(INITGUID) || defined(USES_IID_IMAPIProviderShutdown)
+DEFINE_OLEGUID(IID_IMAPIProviderShutdown, 0x00020398, 0, 0);
+#endif
+
+#endif /* MAPIGUID_H */
diff --git a/comm/mailnews/mapi/include/mapihook.h b/comm/mailnews/mapi/include/mapihook.h
new file mode 100644
index 0000000000..a547dcfaad
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapihook.h
@@ -0,0 +1,83 @@
+/*
+ * M A P I H O O K . H
+ *
+ * Defines the SpoolerMsgHook provider interface.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPIHOOK_H
+#define MAPIHOOK_H
+
+#ifndef MAPIDEFS_H
+#include <mapidefs.h>
+#include <mapicode.h>
+#include <mapiguid.h>
+#include <mapitags.h>
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ISpoolerHook Interface ------------------------------------------------ */
+
+/* MsgHooks */
+
+#define HOOK_DELETE ((ULONG) 0x00000001)
+#define HOOK_CANCEL ((ULONG) 0x00000002)
+
+#define MAPI_ISPOOLERHOOK_METHODS(IPURE) \
+ MAPIMETHOD(InboundMsgHook) \
+ (THIS_ LPMESSAGE lpMessage, \
+ LPMAPIFOLDER lpFolder, \
+ LPMDB lpMDB, \
+ ULONG FAR * lpulFlags, \
+ ULONG FAR * lpcbEntryID, \
+ LPBYTE FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(OutboundMsgHook) \
+ (THIS_ LPMESSAGE lpMessage, \
+ LPMAPIFOLDER lpFolder, \
+ LPMDB lpMDB, \
+ ULONG FAR * lpulFlags, \
+ ULONG FAR * lpcbEntryID, \
+ LPBYTE FAR * lppEntryID) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE ISpoolerHook
+DECLARE_MAPI_INTERFACE_(ISpoolerHook, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_ISPOOLERHOOK_METHODS(PURE)
+};
+
+DECLARE_MAPI_INTERFACE_PTR(ISpoolerHook, LPSPOOLERHOOK);
+
+/* Hook Provider Entry Point */
+
+#define HOOK_INBOUND ((ULONG) 0x00000200)
+#define HOOK_OUTBOUND ((ULONG) 0x00000400)
+
+typedef HRESULT (STDMAPIINITCALLTYPE HPPROVIDERINIT)(
+ LPMAPISESSION lpSession,
+ HINSTANCE hInstance,
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPALLOCATEMORE lpAllocateMore,
+ LPFREEBUFFER lpFreeBuffer,
+ LPMAPIUID lpSectionUID,
+ ULONG ulFlags,
+ LPSPOOLERHOOK FAR * lppSpoolerHook
+);
+
+HPPROVIDERINIT HPProviderInit;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* MAPIHOOK_H */
diff --git a/comm/mailnews/mapi/include/mapinls.h b/comm/mailnews/mapi/include/mapinls.h
new file mode 100644
index 0000000000..cc04096a52
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapinls.h
@@ -0,0 +1,196 @@
+/*
+ * M A P I N L S . H
+ *
+ * Internationalization Support Utilities
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _MAPINLS_H_
+#define _MAPINLS_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN64) && !defined (_WIN64)
+#define _WIN64
+#endif
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* We don't want to include windows.h in case that conflicts with an */
+/* earlier inclusion of compobj.h */
+
+#if !defined(WINAPI)
+ #if defined(_WIN64) && (_MSC_VER >= 800)
+ #define WINAPI
+ #elif defined(_WIN32) && (_MSC_VER >= 800)
+ #define WINAPI __stdcall
+ #else
+ #error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+ #endif
+#endif
+
+#if defined(DOS) || defined(_MAC)
+#include <string.h>
+#endif
+
+#ifndef FAR
+#define FAR
+#endif
+
+typedef unsigned char BYTE;
+typedef unsigned short WORD;
+typedef unsigned long DWORD;
+typedef unsigned int UINT;
+typedef int BOOL;
+
+#ifndef __CHAR_DEFINED__
+typedef char CHAR;
+#endif
+
+#ifdef UNICODE
+typedef WCHAR TCHAR;
+#else
+typedef char TCHAR;
+#endif
+
+#if !defined(_NATIVE_WCHAR_T_DEFINED)
+typedef unsigned short WCHAR;
+#endif
+
+typedef WCHAR FAR * LPWSTR;
+typedef const WCHAR FAR * LPCWSTR;
+typedef CHAR FAR * LPSTR;
+typedef const CHAR FAR * LPCSTR;
+typedef TCHAR FAR * LPTSTR;
+typedef const TCHAR FAR * LPCTSTR;
+typedef DWORD LCID;
+typedef const void FAR * LPCVOID;
+
+#ifndef _MAC
+#ifndef LPOLESTR
+#if defined(_WIN64) || defined(_WIN32)
+#define LPOLESTR LPWSTR
+#define LPCOLESTR LPCWSTR
+#define OLECHAR WCHAR
+#define OLESTR(str) L##str
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif /* _WIN64 || _WIN32*/
+#endif /* LPOLESTR */
+#endif /* _MAC */
+
+#define NORM_IGNORECASE 0x00000001 /* ignore case */
+#define NORM_IGNORENONSPACE 0x00000002 /* ignore diacritics */
+#define NORM_IGNORESYMBOLS 0x00000004 /* ignore symbols */
+
+#if defined(_WIN64) || defined (_WIN32) /* from winnls.h */
+#define NORM_IGNOREKANATYPE 0x00010000 /* ignore kanatype */
+#define NORM_IGNOREWIDTH 0x00020000 /* ignore width */
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+#if defined(DOS) || defined(_MAC)
+
+#define IsBadReadPtr(lp, cb) (FALSE)
+#define IsBadWritePtr(lp, cb) (FALSE)
+#define IsBadHugeReadPtr(lp, cb) (FALSE)
+#define IsBadHugeWritePtr(lp, cb) (FALSE)
+#define IsBadCodePtr(lpfn) (FALSE)
+#ifdef _MAC
+#undef IsBadStringPtr
+#endif
+#define IsBadStringPtr(lpsz, cchMax) (FALSE)
+#define IsBadStringPtrA(lpsz, cchMax) (FALSE)
+
+#if defined(DOS)
+
+#define lstrcpyA strcpy
+#define lstrlenA strlen
+#define lstrcmpA strcmp
+#define lstrcmp strcmp
+#define lstrcmpi strcmpi
+#define lstrcpy strcpy
+#define lstrcat strcat
+#define lstrlen strlen
+#define wsprintf sprintf
+
+#endif
+#endif
+
+#if defined(DOS) || defined(WIN16)
+/* Simulate effect of afx header */
+#define __T(x) x
+#define _T(x) __T(x)
+#define TEXT _T
+#endif
+
+#define CP_ACP 0 /* default to ANSI code page */
+#define CP_OEMCP 1 /* default to OEM code page */
+
+LCID WINAPI MNLS_GetUserDefaultLCID(void);
+UINT WINAPI MNLS_GetACP(void);
+int WINAPI MNLS_CompareStringA(LCID Locale, DWORD dwCmpFlags,
+ LPCSTR lpString1, int cchCount1, LPCSTR lpString2,
+ int cchCount2);
+int WINAPI MNLS_CompareStringW(LCID Locale, DWORD dwCmpFlags,
+ LPCWSTR lpString1, int cchCount1, LPCWSTR lpString2,
+ int cchCount2);
+int WINAPI MNLS_MultiByteToWideChar(UINT uCodePage, DWORD dwFlags,
+ LPCSTR lpMultiByteStr, int cchMultiByte,
+ LPWSTR lpWideCharStr, int cchWideChar);
+int WINAPI MNLS_WideCharToMultiByte(UINT uCodePage, DWORD dwFlags,
+ LPCWSTR lpWideCharStr, int cchWideChar,
+ LPSTR lpMultiByteStr, int cchMultiByte,
+ LPCSTR lpDefaultChar, BOOL FAR *lpfUsedDefaultChar);
+int WINAPI MNLS_lstrlenW(LPCWSTR lpString);
+int WINAPI MNLS_lstrcmpW(LPCWSTR lpString1, LPCWSTR lpString2);
+LPWSTR WINAPI MNLS_lstrcpyW(LPWSTR lpString1, LPCWSTR lpString2);
+BOOL WINAPI MNLS_IsBadStringPtrW(LPCWSTR lpsz, UINT ucchMax);
+
+#if (defined (_WIN64) || defined(_WIN32)) && !defined(_WINNT) && !defined(_WIN95) && !defined(_MAC)
+#define _WINNT
+#endif
+
+#if !defined(_WINNT) && !defined(_WIN95)
+#define GetUserDefaultLCID MNLS_GetUserDefaultLCID
+#define GetACP MNLS_GetACP
+#define MultiByteToWideChar MNLS_MultiByteToWideChar
+#define WideCharToMultiByte MNLS_WideCharToMultiByte
+#define CompareStringA MNLS_CompareStringA
+#endif
+
+#if !defined(MAPI_NOWIDECHAR)
+
+#define lstrlenW MNLS_lstrlenW
+#define lstrcmpW MNLS_lstrcmpW
+#define lstrcpyW MNLS_lstrcpyW
+#define CompareStringW MNLS_CompareStringW
+
+#if defined(_WINNT) || defined(_WIN95)
+#define IsBadStringPtrW MNLS_IsBadStringPtrW
+#elif defined(_MAC)
+#define IsBadStringPtrW(lpsz, cchMax) (FALSE)
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#define IsBadStringPtrW (FALSE)
+#endif
+
+#endif /* ! MAPI_NOWIDECHAR */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _MAPINLS_H_ */
+
+
diff --git a/comm/mailnews/mapi/include/mapioid.h b/comm/mailnews/mapi/include/mapioid.h
new file mode 100644
index 0000000000..8d0536aa75
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapioid.h
@@ -0,0 +1,106 @@
+/*
+ * M A P I O I D . H
+ *
+ * MAPI OID definition header file
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _MAPIOID_
+#define _MAPIOID_
+
+/*
+ * MAPI 1.0 Object Identifiers (OID's)
+ *
+ * All MAPI 1.0 OIDs are prefixed by the segment
+ *
+ * {iso(1) ansi(2) usa(840) microsoft(113556) mapi(3)}
+ *
+ * All MAPI 1.0 tags are also include the addistion segment
+ *
+ * {tags(10)}
+ *
+ * All MAPI 1.0 encodings are also include the addistion segment
+ *
+ * {encodeings(11)}
+ *
+ * The set of defined tags are as follows
+ *
+ * {{mapiprefix} {tags} {tnef(1)}} MAPI 1.0 TNEF encapsulation tag
+ *
+ * {{mapiprefix} {tags} {ole(3)}} MAPI 1.0 OLE prefix
+ * {{mapiprefix} {tags} {ole(3)} {v1(1)}} MAPI 1.0 OLE 1.0 prefix
+ * {{mapiprefix} {tags} {ole(3)} {v1(1)} {storage(1)}} MAPI 1.0 OLE 1.0 OLESTREAM
+ * {{mapiprefix} {tags} {ole(3)} {v2(2)}} MAPI 1.0 OLE 2.0 prefix
+ * {{mapiprefix} {tags} {ole(3)} {v2(2)} {storage(1)}} MAPI 1.0 OLE 2.0 IStorage
+ *
+ * The set of defined encodings are as follows
+ *
+ * {{mapiprefix} {encodings} {MacBinary(1)}} MAPI 1.0 MacBinary
+ */
+
+#define OID_TAG 0x0A
+#define OID_ENCODING 0x0B
+
+#define DEFINE_OID_1(name, b0, b1) \
+ EXTERN_C const BYTE name[]
+
+#define DEFINE_OID_2(name, b0, b1, b2) \
+ EXTERN_C const BYTE name[]
+
+#define DEFINE_OID_3(name, b0, b1, b2, b3) \
+ EXTERN_C const BYTE name[]
+
+#define DEFINE_OID_4(name, b0, b1, b2, b3, b4) \
+ EXTERN_C const BYTE name[]
+
+#define CB_OID_1 9
+#define CB_OID_2 10
+#define CB_OID_3 11
+#define CB_OID_4 12
+
+#ifdef INITOID
+#include <initoid.h>
+#endif
+
+#ifdef USES_OID_TNEF
+DEFINE_OID_1(OID_TNEF, OID_TAG, 0x01);
+#define CB_OID_TNEF CB_OID_1
+#endif
+
+#ifdef USES_OID_OLE
+DEFINE_OID_1(OID_OLE, OID_TAG, 0x03);
+#define CB_OID_OLE CB_OID_1
+#endif
+
+#ifdef USES_OID_OLE1
+DEFINE_OID_2(OID_OLE1, OID_TAG, 0x03, 0x01);
+#define CB_OID_OLE1 CB_OID_2
+#endif
+
+#ifdef USES_OID_OLE1_STORAGE
+DEFINE_OID_3(OID_OLE1_STORAGE, OID_TAG, 0x03, 0x01, 0x01);
+#define CB_OID_OLE1_STORAGE CB_OID_3
+#endif
+
+#ifdef USES_OID_OLE2
+DEFINE_OID_2(OID_OLE2, OID_TAG, 0x03, 0x02);
+#define CB_OID_OLE2 CB_OID_2
+#endif
+
+#ifdef USES_OID_OLE2_STORAGE
+DEFINE_OID_3(OID_OLE2_STORAGE, OID_TAG, 0x03, 0x02, 0x01);
+#define CB_OID_OLE2_STORAGE CB_OID_3
+#endif
+
+#ifdef USES_OID_MAC_BINARY
+DEFINE_OID_1(OID_MAC_BINARY, OID_ENCODING, 0x01);
+#define CB_OID_MAC_BINARY CB_OID_1
+#endif
+
+#ifdef USES_OID_MIMETAG
+DEFINE_OID_1(OID_MIMETAG, OID_TAG, 0x04);
+#define CB_OID_MIMETAG CB_OID_1
+#endif
+
+#endif
diff --git a/comm/mailnews/mapi/include/mapispi.h b/comm/mailnews/mapi/include/mapispi.h
new file mode 100644
index 0000000000..cc326b840a
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapispi.h
@@ -0,0 +1,929 @@
+/*
+ * M A P I S P I . H
+ *
+ * Defines the calls and structures exchanged between MAPI or the spooler
+ * and the MAPI service providers
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPISPI_H
+#define MAPISPI_H
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+/* Include common MAPI header files if they haven't been already. */
+#ifndef MAPIDEFS_H
+#include <mapidefs.h>
+#endif
+#ifndef MAPICODE_H
+#include <mapicode.h>
+#endif
+#ifndef MAPIGUID_H
+#include <mapiguid.h>
+#endif
+#ifndef MAPITAGS_H
+#include <mapitags.h>
+#endif
+
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* The MAPI SPI has a version number. MAPIX.DLL knows and supports
+ * one or more versions of the SPI. Each provider supports one or
+ * more versions of the SPI. Checks are performed in both MAPIX.DLL
+ * and in the provider to ensure that they agree to use exactly one
+ * version of the MAPI SPI.
+ *
+ * The SPI version number is composed of a major (8-bit) version,
+ * minor (8-bit) version, and micro (16-bit) version. The first
+ * retail ship of MAPI 1.0 is expected to be version 1.0.0.
+ * The major version number changes rarely.
+ * The minor version number changes opon each retail ship of
+ * MAPI if the SPI has been modified.
+ * The micro version number changes internally at Microsoft
+ * during development of MAPI.
+ *
+ * The version of the SPI documented by this set of header files
+ * is ALWAYS known as "CURRENT_SPI_VERSION". If you write a
+ * service provider, and get a new set of header files, and update
+ * your code to the new interface, you'll be at the "current" version.
+ */
+#define CURRENT_SPI_VERSION 0x00010010L
+
+/* Here are some well-known SPI version numbers:
+ * (These will eventually be useful for provider-writers who
+ * might choose to make provider DLLs that support more than
+ * one version of the MAPI SPI.
+ */
+#define PDK1_SPI_VERSION 0x00010000L /* 0.1.0 MAPI PDK1 Spring 1993 */
+
+#define PDK2_SPI_VERSION 0x00010008L /* 0.1.8 MAPI PDK2 Spring 1994 */
+
+#define PDK3_SPI_VERSION 0x00010010L /* 0.1.16 MAPI PDK3 Fall 1994 */
+
+/*
+ * Forward declaration of interface pointers specific to the service
+ * provider interface.
+ */
+DECLARE_MAPI_INTERFACE_PTR(IMAPISupport, LPMAPISUP);
+
+/* IMAPISupport Interface -------------------------------------------------- */
+
+/* Notification key structure for the MAPI notification engine */
+
+typedef struct
+{
+ ULONG cb; /* How big the key is */
+ BYTE ab[MAPI_DIM]; /* Key contents */
+} NOTIFKEY, FAR * LPNOTIFKEY;
+
+#define CbNewNOTIFKEY(_cb) (offsetof(NOTIFKEY,ab) + (_cb))
+#define CbNOTIFKEY(_lpkey) (offsetof(NOTIFKEY,ab) + (_lpkey)->cb)
+#define SizedNOTIFKEY(_cb, _name) \
+ struct _NOTIFKEY_ ## _name \
+{ \
+ ULONG cb; \
+ BYTE ab[_cb]; \
+} _name
+
+
+/* For Subscribe() */
+
+#define NOTIFY_SYNC ((ULONG) 0x40000000)
+
+/* For Notify() */
+
+#define NOTIFY_CANCELED ((ULONG) 0x80000000)
+
+
+/* From the Notification Callback function (well, this is really a ulResult) */
+
+#define CALLBACK_DISCONTINUE ((ULONG) 0x80000000)
+
+/* For Transport's SpoolerNotify() */
+
+#define NOTIFY_NEWMAIL ((ULONG) 0x00000001)
+#define NOTIFY_READYTOSEND ((ULONG) 0x00000002)
+#define NOTIFY_SENTDEFERRED ((ULONG) 0x00000004)
+#define NOTIFY_CRITSEC ((ULONG) 0x00001000)
+#define NOTIFY_NONCRIT ((ULONG) 0x00002000)
+#define NOTIFY_CONFIG_CHANGE ((ULONG) 0x00004000)
+#define NOTIFY_CRITICAL_ERROR ((ULONG) 0x10000000)
+
+/* For Message Store's SpoolerNotify() */
+
+#define NOTIFY_NEWMAIL_RECEIVED ((ULONG) 0x20000000)
+
+/* For ModifyStatusRow() */
+
+#define STATUSROW_UPDATE ((ULONG) 0x10000000)
+
+/* For IStorageFromStream() */
+
+#define STGSTRM_RESET ((ULONG) 0x00000000)
+#define STGSTRM_CURRENT ((ULONG) 0x10000000)
+#define STGSTRM_MODIFY ((ULONG) 0x00000002)
+#define STGSTRM_CREATE ((ULONG) 0x00001000)
+
+/* For GetOneOffTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* For CreateOneOff() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+/****** MAPI_SEND_NO_RICH_INFO ((ULONG) 0x00010000) */
+
+/* For ReadReceipt() */
+#define MAPI_NON_READ ((ULONG) 0x00000001)
+
+/* For DoConfigPropSheet() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* Preprocessor calls: */
+
+/* PreprocessMessage, first ordinal in RegisterPreprocessor(). */
+
+typedef HRESULT (STDMETHODCALLTYPE PREPROCESSMESSAGE)(
+ LPVOID lpvSession,
+ LPMESSAGE lpMessage,
+ LPADRBOOK lpAdrBook,
+ LPMAPIFOLDER lpFolder,
+ LPALLOCATEBUFFER AllocateBuffer,
+ LPALLOCATEMORE AllocateMore,
+ LPFREEBUFFER FreeBuffer,
+ ULONG FAR *lpcOutbound,
+ LPMESSAGE FAR * FAR *lpppMessage,
+ LPADRLIST FAR *lppRecipList);
+
+/* RemovePreprocessInfo, second ordinal in RegisterPreprocessor(). */
+
+typedef HRESULT (STDMETHODCALLTYPE REMOVEPREPROCESSINFO)(LPMESSAGE lpMessage);
+
+/* Function pointer for GetReleaseInfo */
+
+#define MAPI_IMAPISUPPORT_METHODS1(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetMemAllocRoutines) \
+ (THIS_ LPALLOCATEBUFFER FAR * lpAllocateBuffer, \
+ LPALLOCATEMORE FAR * lpAllocateMore, \
+ LPFREEBUFFER FAR * lpFreeBuffer) IPURE; \
+ MAPIMETHOD(Subscribe) \
+ (THIS_ LPNOTIFKEY lpKey, \
+ ULONG ulEventMask, \
+ ULONG ulFlags, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unsubscribe) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(Notify) \
+ (THIS_ LPNOTIFKEY lpKey, \
+ ULONG cNotification, \
+ LPNOTIFICATION lpNotifications, \
+ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(ModifyStatusRow) \
+ (THIS_ ULONG cValues, \
+ LPSPropValue lpColumnVals, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(OpenProfileSection) \
+ (THIS_ LPMAPIUID lpUid, \
+ ULONG ulFlags, \
+ LPPROFSECT FAR * lppProfileObj) IPURE; \
+ MAPIMETHOD(RegisterPreprocessor) \
+ (THIS_ LPMAPIUID lpMuid, \
+ LPTSTR lpszAdrType, \
+ LPTSTR lpszDLLName, \
+ LPSTR /* String8! */ lpszPreprocess, \
+ LPSTR /* String8! */ lpszRemovePreprocessInfo, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(NewUID) \
+ (THIS_ LPMAPIUID lpMuid) IPURE; \
+ MAPIMETHOD(MakeInvalid) \
+ (THIS_ ULONG ulFlags, \
+ LPVOID lpObject, \
+ ULONG ulRefCount, \
+ ULONG cMethods) IPURE; \
+
+#define MAPI_IMAPISUPPORT_METHODS2(IPURE) \
+ MAPIMETHOD(SpoolerYield) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SpoolerNotify) \
+ (THIS_ ULONG ulFlags, \
+ LPVOID lpvData) IPURE; \
+ MAPIMETHOD(CreateOneOff) \
+ (THIS_ LPTSTR lpszName, \
+ LPTSTR lpszAdrType, \
+ LPTSTR lpszAddress, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(SetProviderUID) \
+ (THIS_ LPMAPIUID lpProviderID, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntry1, \
+ LPENTRYID lpEntry1, \
+ ULONG cbEntry2, \
+ LPENTRYID lpEntry2, \
+ ULONG ulCompareFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(OpenTemplateID) \
+ (THIS_ ULONG cbTemplateID, \
+ LPENTRYID lpTemplateID, \
+ ULONG ulTemplateFlags, \
+ LPMAPIPROP lpMAPIPropData, \
+ LPCIID lpInterface, \
+ LPMAPIPROP FAR * lppMAPIPropNew, \
+ LPMAPIPROP lpMAPIPropSibling) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulOpenFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(GetOneOffTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(Address) \
+ (THIS_ ULONG_PTR FAR * lpulUIParam, \
+ LPADRPARM lpAdrParms, \
+ LPADRLIST FAR * lppAdrList) IPURE; \
+ MAPIMETHOD(Details) \
+ (THIS_ ULONG_PTR FAR * lpulUIParam, \
+ LPFNDISMISS lpfnDismiss, \
+ LPVOID lpvDismissContext, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPFNBUTTON lpfButtonCallback, \
+ LPVOID lpvButtonContext, \
+ LPTSTR lpszButtonText, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(NewEntry) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ ULONG cbEIDContainer, \
+ LPENTRYID lpEIDContainer, \
+ ULONG cbEIDNewEntryTpl, \
+ LPENTRYID lpEIDNewEntryTpl, \
+ ULONG FAR * lpcbEIDNewEntry, \
+ LPENTRYID FAR * lppEIDNewEntry) IPURE; \
+ MAPIMETHOD(DoConfigPropsheet) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPTSTR lpszTitle, \
+ LPMAPITABLE lpDisplayTable, \
+ LPMAPIPROP lpCOnfigData, \
+ ULONG ulTopPage) IPURE; \
+ MAPIMETHOD(CopyMessages) \
+ (THIS_ LPCIID lpSrcInterface, \
+ LPVOID lpSrcFolder, \
+ LPENTRYLIST lpMsgList, \
+ LPCIID lpDestInterface, \
+ LPVOID lpDestFolder, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(CopyFolder) \
+ (THIS_ LPCIID lpSrcInterface, \
+ LPVOID lpSrcFolder, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpDestInterface, \
+ LPVOID lpDestFolder, \
+ LPTSTR lszNewFolderName, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ ULONG ulFlags) IPURE; \
+
+#define MAPI_IMAPISUPPORT_METHODS3(IPURE) \
+ MAPIMETHOD(DoCopyTo) \
+ (THIS_ LPCIID lpSrcInterface, \
+ LPVOID lpSrcObj, \
+ ULONG ciidExclude, \
+ LPCIID rgiidExclude, \
+ LPSPropTagArray lpExcludeProps, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ LPCIID lpDestInterface, \
+ LPVOID lpDestObj, \
+ ULONG ulFlags, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(DoCopyProps) \
+ (THIS_ LPCIID lpSrcInterface, \
+ LPVOID lpSrcObj, \
+ LPSPropTagArray lpIncludeProps, \
+ ULONG_PTR ulUIParam, \
+ LPMAPIPROGRESS lpProgress, \
+ LPCIID lpDestInterface, \
+ LPVOID lpDestObj, \
+ ULONG ulFlags, \
+ LPSPropProblemArray FAR * lppProblems) IPURE; \
+ MAPIMETHOD(DoProgressDialog) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPMAPIPROGRESS FAR * lppProgress) IPURE; \
+ MAPIMETHOD(ReadReceipt) \
+ (THIS_ ULONG ulFlags, \
+ LPMESSAGE lpReadMessage, \
+ LPMESSAGE FAR * lppEmptyMessage) IPURE; \
+ MAPIMETHOD(PrepareSubmit) \
+ (THIS_ LPMESSAGE lpMessage, \
+ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(ExpandRecips) \
+ (THIS_ LPMESSAGE lpMessage, \
+ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(UpdatePAB) \
+ (THIS_ ULONG ulFlags, \
+ LPMESSAGE lpMessage) IPURE; \
+ MAPIMETHOD(DoSentMail) \
+ (THIS_ ULONG ulFlags, \
+ LPMESSAGE lpMessage) IPURE; \
+ MAPIMETHOD(OpenAddressBook) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPADRBOOK FAR * lppAdrBook) IPURE; \
+ MAPIMETHOD(Preprocess) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(CompleteMsg) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(StoreLogoffTransports) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(StatusRecips) \
+ (THIS_ LPMESSAGE lpMessage, \
+ LPADRLIST lpRecipList) IPURE; \
+ MAPIMETHOD(WrapStoreEntryID) \
+ (THIS_ ULONG cbOrigEntry, \
+ LPENTRYID lpOrigEntry, \
+ ULONG FAR * lpcbWrappedEntry, \
+ LPENTRYID FAR * lppWrappedEntry) IPURE; \
+ MAPIMETHOD(ModifyProfile) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(IStorageFromStream) \
+ (THIS_ LPUNKNOWN lpUnkIn, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPSTORAGE FAR * lppStorageOut) IPURE; \
+ MAPIMETHOD(GetSvcConfigSupportObj) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPISUP FAR * lppSvcSupport) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMAPISupport
+DECLARE_MAPI_INTERFACE_(IMAPISupport, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPISUPPORT_METHODS1(PURE)
+ MAPI_IMAPISUPPORT_METHODS2(PURE)
+ MAPI_IMAPISUPPORT_METHODS3(PURE)
+};
+
+
+/********************************************************************/
+/* */
+/* ADDRESS BOOK SPI */
+/* */
+/********************************************************************/
+
+/* Address Book Provider ------------------------------------------------- */
+
+/* OpenTemplateID() */
+#define FILL_ENTRY ((ULONG) 0x00000001)
+
+/* For Logon() */
+
+/*#define AB_NO_DIALOG ((ULONG) 0x00000001) in mapidefs.h */
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+
+
+
+DECLARE_MAPI_INTERFACE_PTR(IABProvider, LPABPROVIDER);
+DECLARE_MAPI_INTERFACE_PTR(IABLogon, LPABLOGON);
+
+#define MAPI_IABPROVIDER_METHODS(IPURE) \
+ MAPIMETHOD(Shutdown) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(Logon) \
+ (THIS_ LPMAPISUP lpMAPISup, \
+ ULONG_PTR ulUIParam, \
+ LPTSTR lpszProfileName, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulpcbSecurity, \
+ LPBYTE FAR * lppbSecurity, \
+ LPMAPIERROR FAR * lppMAPIError, \
+ LPABLOGON FAR * lppABLogon) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IABProvider
+DECLARE_MAPI_INTERFACE_(IABProvider, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IABPROVIDER_METHODS(PURE)
+};
+
+/* For GetOneOffTable() */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+#define MAPI_IABLOGON_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(Logoff) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(OpenStatusEntry) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPMAPISTATUS FAR * lppEntry) IPURE; \
+ MAPIMETHOD(OpenTemplateID) \
+ (THIS_ ULONG cbTemplateID, \
+ LPENTRYID lpTemplateID, \
+ ULONG ulTemplateFlags, \
+ LPMAPIPROP lpMAPIPropData, \
+ LPCIID lpInterface, \
+ LPMAPIPROP FAR * lppMAPIPropNew, \
+ LPMAPIPROP lpMAPIPropSibling) IPURE; \
+ MAPIMETHOD(GetOneOffTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(PrepareRecips) \
+ (THIS_ ULONG ulFlags, \
+ LPSPropTagArray lpPropTagArray, \
+ LPADRLIST lpRecipList) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IABLogon
+DECLARE_MAPI_INTERFACE_(IABLogon, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IABLOGON_METHODS(PURE)
+};
+
+typedef HRESULT (STDMAPIINITCALLTYPE ABPROVIDERINIT)(
+ HINSTANCE hInstance,
+ LPMALLOC lpMalloc,
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPALLOCATEMORE lpAllocateMore,
+ LPFREEBUFFER lpFreeBuffer,
+ ULONG ulFlags,
+ ULONG ulMAPIVer,
+ ULONG FAR * lpulProviderVer,
+ LPABPROVIDER FAR * lppABProvider
+);
+
+ABPROVIDERINIT ABProviderInit;
+
+
+
+/********************************************************************/
+/* */
+/* TRANSPORT SPI */
+/* */
+/********************************************************************/
+
+/* For DeinitTransport */
+
+#define DEINIT_NORMAL ((ULONG) 0x00000001)
+#define DEINIT_HURRY ((ULONG) 0x80000000)
+
+/* For TransportLogon */
+
+/* Flags that the Spooler may pass to the transport: */
+
+#define LOGON_NO_DIALOG ((ULONG) 0x00000001)
+#define LOGON_NO_CONNECT ((ULONG) 0x00000004)
+#define LOGON_NO_INBOUND ((ULONG) 0x00000008)
+#define LOGON_NO_OUTBOUND ((ULONG) 0x00000010)
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+
+/* Flags that the transport may pass to the Spooler: */
+
+#define LOGON_SP_IDLE ((ULONG) 0x00010000)
+#define LOGON_SP_POLL ((ULONG) 0x00020000)
+#define LOGON_SP_RESOLVE ((ULONG) 0x00040000)
+
+
+DECLARE_MAPI_INTERFACE_PTR(IXPProvider, LPXPPROVIDER);
+DECLARE_MAPI_INTERFACE_PTR(IXPLogon, LPXPLOGON);
+
+#define MAPI_IXPPROVIDER_METHODS(IPURE) \
+ MAPIMETHOD(Shutdown) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(TransportLogon) \
+ (THIS_ LPMAPISUP lpMAPISup, \
+ ULONG_PTR ulUIParam, \
+ __in LPTSTR lpszProfileName, \
+ ULONG FAR * lpulFlags, \
+ LPMAPIERROR FAR * lppMAPIError, \
+ LPXPLOGON FAR * lppXPLogon) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IXPProvider
+DECLARE_MAPI_INTERFACE_(IXPProvider, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IXPPROVIDER_METHODS(PURE)
+};
+
+/* OptionData returned from call to RegisterOptions */
+
+#define OPTION_TYPE_RECIPIENT ((ULONG) 0x00000001)
+#define OPTION_TYPE_MESSAGE ((ULONG) 0x00000002)
+
+typedef struct _OPTIONDATA
+{
+ ULONG ulFlags; /* MAPI_RECIPIENT, MAPI_MESSAGE */
+ LPGUID lpRecipGUID; /* Same as returned by AddressTypes() */
+ LPTSTR lpszAdrType; /* Same as returned by AddressTypes() */
+ LPTSTR lpszDLLName; /* Options DLL */
+ ULONG ulOrdinal; /* Ordinal in that DLL */
+ ULONG cbOptionsData; /* Count of bytes in lpbOptionsData */
+ LPBYTE lpbOptionsData; /* Providers per [recip|message] option data */
+ ULONG cOptionsProps; /* Count of Options default prop values */
+ LPSPropValue lpOptionsProps; /* Default Options property values */
+} OPTIONDATA, FAR *LPOPTIONDATA;
+
+typedef SCODE (STDMAPIINITCALLTYPE OPTIONCALLBACK)(
+ HINSTANCE hInst,
+ LPMALLOC lpMalloc,
+ ULONG ulFlags,
+ ULONG cbOptionData,
+ LPBYTE lpbOptionData,
+ LPMAPISUP lpMAPISup,
+ LPMAPIPROP lpDataSource,
+ LPMAPIPROP FAR * lppWrappedSource,
+ LPMAPIERROR FAR * lppMAPIError);
+
+/* For XP_AddressTypes */
+
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+
+/* For XP_RegisterRecipOptions */
+
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+
+/* For XP_RegisterMessageOptions */
+
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+
+/* For TransportNotify */
+
+#define NOTIFY_ABORT_DEFERRED ((ULONG) 0x40000000)
+#define NOTIFY_CANCEL_MESSAGE ((ULONG) 0x80000000)
+#define NOTIFY_BEGIN_INBOUND ((ULONG) 0x00000001)
+#define NOTIFY_END_INBOUND ((ULONG) 0x00010000)
+#define NOTIFY_BEGIN_OUTBOUND ((ULONG) 0x00000002)
+#define NOTIFY_END_OUTBOUND ((ULONG) 0x00020000)
+#define NOTIFY_BEGIN_INBOUND_FLUSH ((ULONG) 0x00000004)
+#define NOTIFY_END_INBOUND_FLUSH ((ULONG) 0x00040000)
+#define NOTIFY_BEGIN_OUTBOUND_FLUSH ((ULONG) 0x00000008)
+#define NOTIFY_END_OUTBOUND_FLUSH ((ULONG) 0x00080000)
+
+/* For TransportLogoff */
+
+#define LOGOFF_NORMAL ((ULONG) 0x00000001)
+#define LOGOFF_HURRY ((ULONG) 0x80000000)
+
+/* For SubmitMessage */
+
+#define BEGIN_DEFERRED ((ULONG) 0x00000001)
+
+/* For EndMessage */
+
+/* Flags that the Spooler may pass to the Transport: */
+
+/* Flags that the transport may pass to the Spooler: */
+
+#define END_RESEND_NOW ((ULONG) 0x00010000)
+#define END_RESEND_LATER ((ULONG) 0x00020000)
+#define END_DONT_RESEND ((ULONG) 0x00040000)
+
+#define MAPI_IXPLOGON_METHODS(IPURE) \
+ MAPIMETHOD(AddressTypes) \
+ (THIS_ ULONG FAR * lpulFlags, \
+ ULONG FAR * lpcAdrType, \
+ __deref_out_ecount_full(*lpcAdrType) LPTSTR FAR * FAR * lpppAdrTypeArray, \
+ ULONG FAR * lpcMAPIUID, \
+ LPMAPIUID FAR * FAR * lpppUIDArray) IPURE; \
+ MAPIMETHOD(RegisterOptions) \
+ (THIS_ ULONG FAR * lpulFlags, \
+ ULONG FAR * lpcOptions, \
+ LPOPTIONDATA FAR * lppOptions) IPURE; \
+ MAPIMETHOD(TransportNotify) \
+ (THIS_ ULONG FAR * lpulFlags, \
+ LPVOID FAR * lppvData) IPURE; \
+ MAPIMETHOD(Idle) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(TransportLogoff) \
+ (THIS_ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SubmitMessage) \
+ (THIS_ ULONG ulFlags, \
+ LPMESSAGE lpMessage, \
+ ULONG_PTR FAR * lpulMsgRef, \
+ ULONG_PTR FAR * lpulReturnParm) IPURE; \
+ MAPIMETHOD(EndMessage) \
+ (THIS_ ULONG_PTR ulMsgRef, \
+ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(Poll) \
+ (THIS_ ULONG FAR * lpulIncoming) IPURE; \
+ MAPIMETHOD(StartMessage) \
+ (THIS_ ULONG ulFlags, \
+ LPMESSAGE lpMessage, \
+ ULONG_PTR FAR * lpulMsgRef) IPURE; \
+ MAPIMETHOD(OpenStatusEntry) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPMAPISTATUS FAR * lppEntry) IPURE; \
+ MAPIMETHOD(ValidateState) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(FlushQueues) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG cbTargetTransport, \
+ LPENTRYID lpTargetTransport, \
+ ULONG ulFlags) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IXPLogon
+DECLARE_MAPI_INTERFACE_(IXPLogon, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IXPLOGON_METHODS(PURE)
+};
+
+
+/* Transport Provider Entry Point */
+
+typedef HRESULT (STDMAPIINITCALLTYPE XPPROVIDERINIT)(
+ HINSTANCE hInstance,
+ LPMALLOC lpMalloc,
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPALLOCATEMORE lpAllocateMore,
+ LPFREEBUFFER lpFreeBuffer,
+ ULONG ulFlags,
+ ULONG ulMAPIVer,
+ ULONG FAR * lpulProviderVer,
+ LPXPPROVIDER FAR * lppXPProvider);
+
+XPPROVIDERINIT XPProviderInit;
+
+/********************************************************************/
+/* */
+/* MESSAGE STORE SPI */
+/* */
+/********************************************************************/
+
+/* Flags and enums */
+
+/* For Logon() */
+
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+/*#define MDB_NO_DIALOG ((ULONG) 0x00000001) in mapidefs.h */
+/*#define MDB_WRITE ((ULONG) 0x00000004) in mapidefs.h */
+/*#define MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) in mapidefs.h */
+/*#define MDB_TEMPORARY ((ULONG) 0x00000020) in mapidefs.h */
+/*#define MDB_NO_MAIL ((ULONG) 0x00000080) in mapidefs.h */
+
+/* For SpoolerLogon() */
+
+/*#define MAPI_UNICODE ((ULONG) 0x80000000) in mapidefs.h */
+/*#define MDB_NO_DIALOG ((ULONG) 0x00000001) in mapidefs.h */
+/*#define MDB_WRITE ((ULONG) 0x00000004) in mapidefs.h */
+/*#define MAPI_DEFERRED_ERRORS ((ULONG) 0x00000008) in mapidefs.h */
+
+/* GetCredentials, SetCredentials */
+
+#define LOGON_SP_TRANSPORT ((ULONG) 0x00000001)
+#define LOGON_SP_PROMPT ((ULONG) 0x00000002)
+#define LOGON_SP_NEWPW ((ULONG) 0x00000004)
+#define LOGON_CHANGED ((ULONG) 0x00000008)
+
+/* DoMCDialog */
+
+#define DIALOG_FOLDER ((ULONG) 0x00000001)
+#define DIALOG_MESSAGE ((ULONG) 0x00000002)
+#define DIALOG_PROP ((ULONG) 0x00000004)
+#define DIALOG_ATTACH ((ULONG) 0x00000008)
+
+#define DIALOG_MOVE ((ULONG) 0x00000010)
+#define DIALOG_COPY ((ULONG) 0x00000020)
+#define DIALOG_DELETE ((ULONG) 0x00000040)
+
+#define DIALOG_ALLOW_CANCEL ((ULONG) 0x00000080)
+#define DIALOG_CONFIRM_CANCEL ((ULONG) 0x00000100)
+
+/* ExpandRecips */
+
+#define NEEDS_PREPROCESSING ((ULONG) 0x00000001)
+#define NEEDS_SPOOLER ((ULONG) 0x00000002)
+
+/* PrepareSubmit */
+
+#define CHECK_SENDER ((ULONG) 0x00000001)
+#define NON_STANDARD ((ULONG) 0x00010000)
+
+
+DECLARE_MAPI_INTERFACE_PTR(IMSLogon, LPMSLOGON);
+DECLARE_MAPI_INTERFACE_PTR(IMSProvider, LPMSPROVIDER);
+
+/* Message Store Provider Interface (IMSPROVIDER) */
+
+#define MAPI_IMSPROVIDER_METHODS(IPURE) \
+ MAPIMETHOD(Shutdown) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(Logon) \
+ (THIS_ LPMAPISUP lpMAPISup, \
+ ULONG_PTR ulUIParam, \
+ LPTSTR lpszProfileName, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulFlags, \
+ LPCIID lpInterface, \
+ ULONG FAR * lpcbSpoolSecurity, \
+ LPBYTE FAR * lppbSpoolSecurity, \
+ LPMAPIERROR FAR * lppMAPIError, \
+ LPMSLOGON FAR * lppMSLogon, \
+ LPMDB FAR * lppMDB) IPURE; \
+ MAPIMETHOD(SpoolerLogon) \
+ (THIS_ LPMAPISUP lpMAPISup, \
+ ULONG_PTR ulUIParam, \
+ LPTSTR lpszProfileName, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulFlags, \
+ LPCIID lpInterface, \
+ ULONG cbSpoolSecurity, \
+ LPBYTE lpbSpoolSecurity, \
+ LPMAPIERROR FAR * lppMAPIError, \
+ LPMSLOGON FAR * lppMSLogon, \
+ LPMDB FAR * lppMDB) IPURE; \
+ MAPIMETHOD(CompareStoreIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMSProvider
+DECLARE_MAPI_INTERFACE_(IMSProvider, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMSPROVIDER_METHODS(PURE)
+};
+
+/* The MSLOGON object is returned by the Logon() method of the
+ * MSPROVIDER interface. This object is for use by MAPIX.DLL.
+ */
+#define MAPI_IMSLOGON_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(Logoff) \
+ (THIS_ ULONG FAR * lpulFlags) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(OpenStatusEntry) \
+ (THIS_ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPVOID FAR * lppEntry) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IMSLogon
+DECLARE_MAPI_INTERFACE_(IMSLogon, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMSLOGON_METHODS(PURE)
+};
+
+/* Message Store Provider Entry Point */
+
+typedef HRESULT (STDMAPIINITCALLTYPE MSPROVIDERINIT)(
+ HINSTANCE hInstance,
+ LPMALLOC lpMalloc, /* AddRef() if you keep it */
+ LPALLOCATEBUFFER lpAllocateBuffer, /* -> AllocateBuffer */
+ LPALLOCATEMORE lpAllocateMore, /* -> AllocateMore */
+ LPFREEBUFFER lpFreeBuffer, /* -> FreeBuffer */
+ ULONG ulFlags,
+ ULONG ulMAPIVer,
+ ULONG FAR * lpulProviderVer,
+ LPMSPROVIDER FAR * lppMSProvider
+);
+
+MSPROVIDERINIT MSProviderInit;
+
+
+/********************************************************************/
+/* */
+/* MESSAGE SERVICE CONFIGURATION */
+/* */
+/********************************************************************/
+
+/* Flags for service configuration entry point */
+
+/* #define MAPI_UNICODE 0x80000000 */
+/* #define SERVICE_UI_ALWAYS 0x00000002 */
+/* #define SERVICE_UI_ALLOWED 0x00000010 */
+#define MSG_SERVICE_UI_READ_ONLY 0x00000008 /* display parameters only */
+#define SERVICE_LOGON_FAILED 0x00000020 /* reconfigure provider */
+
+/* Contexts for service configuration entry point */
+
+#define MSG_SERVICE_INSTALL 0x00000001
+#define MSG_SERVICE_CREATE 0x00000002
+#define MSG_SERVICE_CONFIGURE 0x00000003
+#define MSG_SERVICE_DELETE 0x00000004
+#define MSG_SERVICE_UNINSTALL 0x00000005
+#define MSG_SERVICE_PROVIDER_CREATE 0x00000006
+#define MSG_SERVICE_PROVIDER_DELETE 0x00000007
+
+/* Prototype for service configuration entry point */
+
+typedef HRESULT (STDAPICALLTYPE MSGSERVICEENTRY)(
+ HINSTANCE hInstance,
+ LPMALLOC lpMalloc,
+ LPMAPISUP lpMAPISup,
+ ULONG_PTR ulUIParam,
+ ULONG ulFlags,
+ ULONG ulContext,
+ ULONG cValues,
+ LPSPropValue lpProps,
+ LPPROVIDERADMIN lpProviderAdmin,
+ LPMAPIERROR FAR *lppMapiError
+);
+typedef MSGSERVICEENTRY FAR *LPMSGSERVICEENTRY;
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MAPISPI_H */
+
diff --git a/comm/mailnews/mapi/include/mapitags.h b/comm/mailnews/mapi/include/mapitags.h
new file mode 100644
index 0000000000..a259cfccd9
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapitags.h
@@ -0,0 +1,1036 @@
+/*
+ * M A P I T A G S . H
+ *
+ * Property tag definitions for standard properties of MAPI
+ * objects.
+ *
+ * The following ranges should be used for all property IDs. Note that
+ * property IDs for objects other than messages and recipients should
+ * all fall in the range 0x3000 to 0x3FFF:
+ *
+ * From To Kind of property
+ * --------------------------------
+ * 0001 0BFF MAPI_defined envelope property
+ * 0C00 0DFF MAPI_defined per-recipient property
+ * 0E00 0FFF MAPI_defined non-transmittable property
+ * 1000 2FFF MAPI_defined message content property
+ *
+ * 3000 3FFF MAPI_defined property (usually not message or recipient)
+ *
+ * 4000 57FF Transport-defined envelope property
+ * 5800 5FFF Transport-defined per-recipient property
+ * 6000 65FF User-defined non-transmittable property
+ * 6600 67FF Provider-defined internal non-transmittable property
+ * 6800 7BFF Message class-defined content property
+ * 7C00 7FFF Message class-defined non-transmittable
+ * property
+ *
+ * 8000 FFFE User-defined Name-to-id mapped property
+ *
+ * The 3000-3FFF range is further subdivided as follows:
+ *
+ * From To Kind of property
+ * --------------------------------
+ * 3000 33FF Common property such as display name, entry ID
+ * 3400 35FF Message store object
+ * 3600 36FF Folder or AB container
+ * 3700 38FF Attachment
+ * 3900 39FF Address book object
+ * 3A00 3BFF Mail user
+ * 3C00 3CFF Distribution list
+ * 3D00 3DFF Profile section
+ * 3E00 3FFF Status object
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPITAGS_H
+#define MAPITAGS_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+/* Determine if a property is transmittable. */
+
+#define FIsTransmittable(ulPropTag) \
+ ((PROP_ID (ulPropTag) < (ULONG)0x0E00) || \
+ (PROP_ID (ulPropTag) >= (ULONG)0x8000) || \
+ ((PROP_ID (ulPropTag) >= (ULONG)0x1000) && (PROP_ID (ulPropTag) < (ULONG)0x6000)) || \
+ ((PROP_ID (ulPropTag) >= (ULONG)0x6800) && (PROP_ID (ulPropTag) < (ULONG)0x7C00)))
+
+/*
+ * Message envelope properties
+ */
+
+#define PR_ACKNOWLEDGEMENT_MODE PROP_TAG( PT_LONG, 0x0001)
+#define PR_ALTERNATE_RECIPIENT_ALLOWED PROP_TAG( PT_BOOLEAN, 0x0002)
+#define PR_AUTHORIZING_USERS PROP_TAG( PT_BINARY, 0x0003)
+#define PR_AUTO_FORWARD_COMMENT PROP_TAG( PT_TSTRING, 0x0004)
+#define PR_AUTO_FORWARD_COMMENT_W PROP_TAG( PT_UNICODE, 0x0004)
+#define PR_AUTO_FORWARD_COMMENT_A PROP_TAG( PT_STRING8, 0x0004)
+#define PR_AUTO_FORWARDED PROP_TAG( PT_BOOLEAN, 0x0005)
+#define PR_CONTENT_CONFIDENTIALITY_ALGORITHM_ID PROP_TAG( PT_BINARY, 0x0006)
+#define PR_CONTENT_CORRELATOR PROP_TAG( PT_BINARY, 0x0007)
+#define PR_CONTENT_IDENTIFIER PROP_TAG( PT_TSTRING, 0x0008)
+#define PR_CONTENT_IDENTIFIER_W PROP_TAG( PT_UNICODE, 0x0008)
+#define PR_CONTENT_IDENTIFIER_A PROP_TAG( PT_STRING8, 0x0008)
+#define PR_CONTENT_LENGTH PROP_TAG( PT_LONG, 0x0009)
+#define PR_CONTENT_RETURN_REQUESTED PROP_TAG( PT_BOOLEAN, 0x000A)
+
+
+
+#define PR_CONVERSATION_KEY PROP_TAG( PT_BINARY, 0x000B)
+
+#define PR_CONVERSION_EITS PROP_TAG( PT_BINARY, 0x000C)
+#define PR_CONVERSION_WITH_LOSS_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x000D)
+#define PR_CONVERTED_EITS PROP_TAG( PT_BINARY, 0x000E)
+#define PR_DEFERRED_DELIVERY_TIME PROP_TAG( PT_SYSTIME, 0x000F)
+#define PR_DELIVER_TIME PROP_TAG( PT_SYSTIME, 0x0010)
+#define PR_DISCARD_REASON PROP_TAG( PT_LONG, 0x0011)
+#define PR_DISCLOSURE_OF_RECIPIENTS PROP_TAG( PT_BOOLEAN, 0x0012)
+#define PR_DL_EXPANSION_HISTORY PROP_TAG( PT_BINARY, 0x0013)
+#define PR_DL_EXPANSION_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x0014)
+#define PR_EXPIRY_TIME PROP_TAG( PT_SYSTIME, 0x0015)
+#define PR_IMPLICIT_CONVERSION_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x0016)
+#define PR_IMPORTANCE PROP_TAG( PT_LONG, 0x0017)
+#define PR_IPM_ID PROP_TAG( PT_BINARY, 0x0018)
+#define PR_LATEST_DELIVERY_TIME PROP_TAG( PT_SYSTIME, 0x0019)
+#define PR_MESSAGE_CLASS PROP_TAG( PT_TSTRING, 0x001A)
+#define PR_MESSAGE_CLASS_W PROP_TAG( PT_UNICODE, 0x001A)
+#define PR_MESSAGE_CLASS_A PROP_TAG( PT_STRING8, 0x001A)
+#define PR_MESSAGE_DELIVERY_ID PROP_TAG( PT_BINARY, 0x001B)
+
+
+
+
+
+#define PR_MESSAGE_SECURITY_LABEL PROP_TAG( PT_BINARY, 0x001E)
+#define PR_OBSOLETED_IPMS PROP_TAG( PT_BINARY, 0x001F)
+#define PR_ORIGINALLY_INTENDED_RECIPIENT_NAME PROP_TAG( PT_BINARY, 0x0020)
+#define PR_ORIGINAL_EITS PROP_TAG( PT_BINARY, 0x0021)
+#define PR_ORIGINATOR_CERTIFICATE PROP_TAG( PT_BINARY, 0x0022)
+#define PR_ORIGINATOR_DELIVERY_REPORT_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0023)
+#define PR_ORIGINATOR_RETURN_ADDRESS PROP_TAG( PT_BINARY, 0x0024)
+
+
+
+#define PR_PARENT_KEY PROP_TAG( PT_BINARY, 0x0025)
+#define PR_PRIORITY PROP_TAG( PT_LONG, 0x0026)
+
+
+
+#define PR_ORIGIN_CHECK PROP_TAG( PT_BINARY, 0x0027)
+#define PR_PROOF_OF_SUBMISSION_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0028)
+#define PR_READ_RECEIPT_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0029)
+#define PR_RECEIPT_TIME PROP_TAG( PT_SYSTIME, 0x002A)
+#define PR_RECIPIENT_REASSIGNMENT_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x002B)
+#define PR_REDIRECTION_HISTORY PROP_TAG( PT_BINARY, 0x002C)
+#define PR_RELATED_IPMS PROP_TAG( PT_BINARY, 0x002D)
+#define PR_ORIGINAL_SENSITIVITY PROP_TAG( PT_LONG, 0x002E)
+#define PR_LANGUAGES PROP_TAG( PT_TSTRING, 0x002F)
+#define PR_LANGUAGES_W PROP_TAG( PT_UNICODE, 0x002F)
+#define PR_LANGUAGES_A PROP_TAG( PT_STRING8, 0x002F)
+#define PR_REPLY_TIME PROP_TAG( PT_SYSTIME, 0x0030)
+#define PR_REPORT_TAG PROP_TAG( PT_BINARY, 0x0031)
+#define PR_REPORT_TIME PROP_TAG( PT_SYSTIME, 0x0032)
+#define PR_RETURNED_IPM PROP_TAG( PT_BOOLEAN, 0x0033)
+#define PR_SECURITY PROP_TAG( PT_LONG, 0x0034)
+#define PR_INCOMPLETE_COPY PROP_TAG( PT_BOOLEAN, 0x0035)
+#define PR_SENSITIVITY PROP_TAG( PT_LONG, 0x0036)
+#define PR_SUBJECT PROP_TAG( PT_TSTRING, 0x0037)
+#define PR_SUBJECT_W PROP_TAG( PT_UNICODE, 0x0037)
+#define PR_SUBJECT_A PROP_TAG( PT_STRING8, 0x0037)
+#define PR_SUBJECT_IPM PROP_TAG( PT_BINARY, 0x0038)
+#define PR_CLIENT_SUBMIT_TIME PROP_TAG( PT_SYSTIME, 0x0039)
+#define PR_REPORT_NAME PROP_TAG( PT_TSTRING, 0x003A)
+#define PR_REPORT_NAME_W PROP_TAG( PT_UNICODE, 0x003A)
+#define PR_REPORT_NAME_A PROP_TAG( PT_STRING8, 0x003A)
+#define PR_SENT_REPRESENTING_SEARCH_KEY PROP_TAG( PT_BINARY, 0x003B)
+#define PR_X400_CONTENT_TYPE PROP_TAG( PT_BINARY, 0x003C)
+#define PR_SUBJECT_PREFIX PROP_TAG( PT_TSTRING, 0x003D)
+#define PR_SUBJECT_PREFIX_W PROP_TAG( PT_UNICODE, 0x003D)
+#define PR_SUBJECT_PREFIX_A PROP_TAG( PT_STRING8, 0x003D)
+#define PR_NON_RECEIPT_REASON PROP_TAG( PT_LONG, 0x003E)
+#define PR_RECEIVED_BY_ENTRYID PROP_TAG( PT_BINARY, 0x003F)
+#define PR_RECEIVED_BY_NAME PROP_TAG( PT_TSTRING, 0x0040)
+#define PR_RECEIVED_BY_NAME_W PROP_TAG( PT_UNICODE, 0x0040)
+#define PR_RECEIVED_BY_NAME_A PROP_TAG( PT_STRING8, 0x0040)
+#define PR_SENT_REPRESENTING_ENTRYID PROP_TAG( PT_BINARY, 0x0041)
+#define PR_SENT_REPRESENTING_NAME PROP_TAG( PT_TSTRING, 0x0042)
+#define PR_SENT_REPRESENTING_NAME_W PROP_TAG( PT_UNICODE, 0x0042)
+#define PR_SENT_REPRESENTING_NAME_A PROP_TAG( PT_STRING8, 0x0042)
+#define PR_RCVD_REPRESENTING_ENTRYID PROP_TAG( PT_BINARY, 0x0043)
+#define PR_RCVD_REPRESENTING_NAME PROP_TAG( PT_TSTRING, 0x0044)
+#define PR_RCVD_REPRESENTING_NAME_W PROP_TAG( PT_UNICODE, 0x0044)
+#define PR_RCVD_REPRESENTING_NAME_A PROP_TAG( PT_STRING8, 0x0044)
+#define PR_REPORT_ENTRYID PROP_TAG( PT_BINARY, 0x0045)
+#define PR_READ_RECEIPT_ENTRYID PROP_TAG( PT_BINARY, 0x0046)
+#define PR_MESSAGE_SUBMISSION_ID PROP_TAG( PT_BINARY, 0x0047)
+#define PR_PROVIDER_SUBMIT_TIME PROP_TAG( PT_SYSTIME, 0x0048)
+#define PR_ORIGINAL_SUBJECT PROP_TAG( PT_TSTRING, 0x0049)
+#define PR_ORIGINAL_SUBJECT_W PROP_TAG( PT_UNICODE, 0x0049)
+#define PR_ORIGINAL_SUBJECT_A PROP_TAG( PT_STRING8, 0x0049)
+#define PR_DISC_VAL PROP_TAG( PT_BOOLEAN, 0x004A)
+#define PR_ORIG_MESSAGE_CLASS PROP_TAG( PT_TSTRING, 0x004B)
+#define PR_ORIG_MESSAGE_CLASS_W PROP_TAG( PT_UNICODE, 0x004B)
+#define PR_ORIG_MESSAGE_CLASS_A PROP_TAG( PT_STRING8, 0x004B)
+#define PR_ORIGINAL_AUTHOR_ENTRYID PROP_TAG( PT_BINARY, 0x004C)
+#define PR_ORIGINAL_AUTHOR_NAME PROP_TAG( PT_TSTRING, 0x004D)
+#define PR_ORIGINAL_AUTHOR_NAME_W PROP_TAG( PT_UNICODE, 0x004D)
+#define PR_ORIGINAL_AUTHOR_NAME_A PROP_TAG( PT_STRING8, 0x004D)
+#define PR_ORIGINAL_SUBMIT_TIME PROP_TAG( PT_SYSTIME, 0x004E)
+#define PR_REPLY_RECIPIENT_ENTRIES PROP_TAG( PT_BINARY, 0x004F)
+#define PR_REPLY_RECIPIENT_NAMES PROP_TAG( PT_TSTRING, 0x0050)
+#define PR_REPLY_RECIPIENT_NAMES_W PROP_TAG( PT_UNICODE, 0x0050)
+#define PR_REPLY_RECIPIENT_NAMES_A PROP_TAG( PT_STRING8, 0x0050)
+
+#define PR_RECEIVED_BY_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0051)
+#define PR_RCVD_REPRESENTING_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0052)
+#define PR_READ_RECEIPT_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0053)
+#define PR_REPORT_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0054)
+#define PR_ORIGINAL_DELIVERY_TIME PROP_TAG( PT_SYSTIME, 0x0055)
+#define PR_ORIGINAL_AUTHOR_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0056)
+
+#define PR_MESSAGE_TO_ME PROP_TAG( PT_BOOLEAN, 0x0057)
+#define PR_MESSAGE_CC_ME PROP_TAG( PT_BOOLEAN, 0x0058)
+#define PR_MESSAGE_RECIP_ME PROP_TAG( PT_BOOLEAN, 0x0059)
+
+#define PR_ORIGINAL_SENDER_NAME PROP_TAG( PT_TSTRING, 0x005A)
+#define PR_ORIGINAL_SENDER_NAME_W PROP_TAG( PT_UNICODE, 0x005A)
+#define PR_ORIGINAL_SENDER_NAME_A PROP_TAG( PT_STRING8, 0x005A)
+#define PR_ORIGINAL_SENDER_ENTRYID PROP_TAG( PT_BINARY, 0x005B)
+#define PR_ORIGINAL_SENDER_SEARCH_KEY PROP_TAG( PT_BINARY, 0x005C)
+#define PR_ORIGINAL_SENT_REPRESENTING_NAME PROP_TAG( PT_TSTRING, 0x005D)
+#define PR_ORIGINAL_SENT_REPRESENTING_NAME_W PROP_TAG( PT_UNICODE, 0x005D)
+#define PR_ORIGINAL_SENT_REPRESENTING_NAME_A PROP_TAG( PT_STRING8, 0x005D)
+#define PR_ORIGINAL_SENT_REPRESENTING_ENTRYID PROP_TAG( PT_BINARY, 0x005E)
+#define PR_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY PROP_TAG( PT_BINARY, 0x005F)
+
+#define PR_START_DATE PROP_TAG( PT_SYSTIME, 0x0060)
+#define PR_END_DATE PROP_TAG( PT_SYSTIME, 0x0061)
+#define PR_OWNER_APPT_ID PROP_TAG( PT_LONG, 0x0062)
+#define PR_RESPONSE_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0063)
+
+#define PR_SENT_REPRESENTING_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0064)
+#define PR_SENT_REPRESENTING_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0064)
+#define PR_SENT_REPRESENTING_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0064)
+#define PR_SENT_REPRESENTING_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0065)
+#define PR_SENT_REPRESENTING_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0065)
+#define PR_SENT_REPRESENTING_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0065)
+
+#define PR_ORIGINAL_SENDER_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0066)
+#define PR_ORIGINAL_SENDER_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0066)
+#define PR_ORIGINAL_SENDER_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0066)
+#define PR_ORIGINAL_SENDER_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0067)
+#define PR_ORIGINAL_SENDER_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0067)
+#define PR_ORIGINAL_SENDER_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0067)
+
+#define PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0068)
+#define PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0068)
+#define PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0068)
+#define PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0069)
+#define PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0069)
+#define PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0069)
+
+#define PR_CONVERSATION_TOPIC PROP_TAG( PT_TSTRING, 0x0070)
+#define PR_CONVERSATION_TOPIC_W PROP_TAG( PT_UNICODE, 0x0070)
+#define PR_CONVERSATION_TOPIC_A PROP_TAG( PT_STRING8, 0x0070)
+#define PR_CONVERSATION_INDEX PROP_TAG( PT_BINARY, 0x0071)
+
+#define PR_ORIGINAL_DISPLAY_BCC PROP_TAG( PT_TSTRING, 0x0072)
+#define PR_ORIGINAL_DISPLAY_BCC_W PROP_TAG( PT_UNICODE, 0x0072)
+#define PR_ORIGINAL_DISPLAY_BCC_A PROP_TAG( PT_STRING8, 0x0072)
+#define PR_ORIGINAL_DISPLAY_CC PROP_TAG( PT_TSTRING, 0x0073)
+#define PR_ORIGINAL_DISPLAY_CC_W PROP_TAG( PT_UNICODE, 0x0073)
+#define PR_ORIGINAL_DISPLAY_CC_A PROP_TAG( PT_STRING8, 0x0073)
+#define PR_ORIGINAL_DISPLAY_TO PROP_TAG( PT_TSTRING, 0x0074)
+#define PR_ORIGINAL_DISPLAY_TO_W PROP_TAG( PT_UNICODE, 0x0074)
+#define PR_ORIGINAL_DISPLAY_TO_A PROP_TAG( PT_STRING8, 0x0074)
+
+#define PR_RECEIVED_BY_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0075)
+#define PR_RECEIVED_BY_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0075)
+#define PR_RECEIVED_BY_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0075)
+#define PR_RECEIVED_BY_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0076)
+#define PR_RECEIVED_BY_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0076)
+#define PR_RECEIVED_BY_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0076)
+
+#define PR_RCVD_REPRESENTING_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0077)
+#define PR_RCVD_REPRESENTING_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0077)
+#define PR_RCVD_REPRESENTING_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0077)
+#define PR_RCVD_REPRESENTING_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0078)
+#define PR_RCVD_REPRESENTING_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0078)
+#define PR_RCVD_REPRESENTING_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0078)
+
+#define PR_ORIGINAL_AUTHOR_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0079)
+#define PR_ORIGINAL_AUTHOR_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0079)
+#define PR_ORIGINAL_AUTHOR_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0079)
+#define PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x007A)
+#define PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x007A)
+#define PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x007A)
+
+#define PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE PROP_TAG( PT_TSTRING, 0x007B)
+#define PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x007B)
+#define PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x007B)
+#define PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x007C)
+#define PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x007C)
+#define PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x007C)
+
+#define PR_TRANSPORT_MESSAGE_HEADERS PROP_TAG(PT_TSTRING, 0x007D)
+#define PR_TRANSPORT_MESSAGE_HEADERS_W PROP_TAG(PT_UNICODE, 0x007D)
+#define PR_TRANSPORT_MESSAGE_HEADERS_A PROP_TAG(PT_STRING8, 0x007D)
+
+#define PR_DELEGATION PROP_TAG(PT_BINARY, 0x007E)
+
+#define PR_TNEF_CORRELATION_KEY PROP_TAG(PT_BINARY, 0x007F)
+
+
+
+/*
+ * Message content properties
+ */
+
+#define PR_BODY PROP_TAG( PT_TSTRING, 0x1000)
+#define PR_BODY_W PROP_TAG( PT_UNICODE, 0x1000)
+#define PR_BODY_A PROP_TAG( PT_STRING8, 0x1000)
+#define PR_REPORT_TEXT PROP_TAG( PT_TSTRING, 0x1001)
+#define PR_REPORT_TEXT_W PROP_TAG( PT_UNICODE, 0x1001)
+#define PR_REPORT_TEXT_A PROP_TAG( PT_STRING8, 0x1001)
+#define PR_ORIGINATOR_AND_DL_EXPANSION_HISTORY PROP_TAG( PT_BINARY, 0x1002)
+#define PR_REPORTING_DL_NAME PROP_TAG( PT_BINARY, 0x1003)
+#define PR_REPORTING_MTA_CERTIFICATE PROP_TAG( PT_BINARY, 0x1004)
+
+/* Removed PR_REPORT_ORIGIN_AUTHENTICATION_CHECK with DCR 3865, use PR_ORIGIN_CHECK */
+
+#define PR_RTF_SYNC_BODY_CRC PROP_TAG( PT_LONG, 0x1006)
+#define PR_RTF_SYNC_BODY_COUNT PROP_TAG( PT_LONG, 0x1007)
+#define PR_RTF_SYNC_BODY_TAG PROP_TAG( PT_TSTRING, 0x1008)
+#define PR_RTF_SYNC_BODY_TAG_W PROP_TAG( PT_UNICODE, 0x1008)
+#define PR_RTF_SYNC_BODY_TAG_A PROP_TAG( PT_STRING8, 0x1008)
+#define PR_RTF_COMPRESSED PROP_TAG( PT_BINARY, 0x1009)
+#define PR_RTF_SYNC_PREFIX_COUNT PROP_TAG( PT_LONG, 0x1010)
+#define PR_RTF_SYNC_TRAILING_COUNT PROP_TAG( PT_LONG, 0x1011)
+#define PR_ORIGINALLY_INTENDED_RECIP_ENTRYID PROP_TAG( PT_BINARY, 0x1012)
+
+/*
+ * Reserved 0x1100-0x1200
+ */
+
+
+/*
+ * Message recipient properties
+ */
+
+#define PR_CONTENT_INTEGRITY_CHECK PROP_TAG( PT_BINARY, 0x0C00)
+#define PR_EXPLICIT_CONVERSION PROP_TAG( PT_LONG, 0x0C01)
+#define PR_IPM_RETURN_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C02)
+#define PR_MESSAGE_TOKEN PROP_TAG( PT_BINARY, 0x0C03)
+#define PR_NDR_REASON_CODE PROP_TAG( PT_LONG, 0x0C04)
+#define PR_NDR_DIAG_CODE PROP_TAG( PT_LONG, 0x0C05)
+#define PR_NON_RECEIPT_NOTIFICATION_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C06)
+#define PR_DELIVERY_POINT PROP_TAG( PT_LONG, 0x0C07)
+
+#define PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C08)
+#define PR_ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT PROP_TAG( PT_BINARY, 0x0C09)
+#define PR_PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY PROP_TAG( PT_BOOLEAN, 0x0C0A)
+#define PR_PHYSICAL_DELIVERY_MODE PROP_TAG( PT_LONG, 0x0C0B)
+#define PR_PHYSICAL_DELIVERY_REPORT_REQUEST PROP_TAG( PT_LONG, 0x0C0C)
+#define PR_PHYSICAL_FORWARDING_ADDRESS PROP_TAG( PT_BINARY, 0x0C0D)
+#define PR_PHYSICAL_FORWARDING_ADDRESS_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C0E)
+#define PR_PHYSICAL_FORWARDING_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x0C0F)
+#define PR_PHYSICAL_RENDITION_ATTRIBUTES PROP_TAG( PT_BINARY, 0x0C10)
+#define PR_PROOF_OF_DELIVERY PROP_TAG( PT_BINARY, 0x0C11)
+#define PR_PROOF_OF_DELIVERY_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C12)
+#define PR_RECIPIENT_CERTIFICATE PROP_TAG( PT_BINARY, 0x0C13)
+#define PR_RECIPIENT_NUMBER_FOR_ADVICE PROP_TAG( PT_TSTRING, 0x0C14)
+#define PR_RECIPIENT_NUMBER_FOR_ADVICE_W PROP_TAG( PT_UNICODE, 0x0C14)
+#define PR_RECIPIENT_NUMBER_FOR_ADVICE_A PROP_TAG( PT_STRING8, 0x0C14)
+#define PR_RECIPIENT_TYPE PROP_TAG( PT_LONG, 0x0C15)
+#define PR_REGISTERED_MAIL_TYPE PROP_TAG( PT_LONG, 0x0C16)
+#define PR_REPLY_REQUESTED PROP_TAG( PT_BOOLEAN, 0x0C17)
+#define PR_REQUESTED_DELIVERY_METHOD PROP_TAG( PT_LONG, 0x0C18)
+#define PR_SENDER_ENTRYID PROP_TAG( PT_BINARY, 0x0C19)
+#define PR_SENDER_NAME PROP_TAG( PT_TSTRING, 0x0C1A)
+#define PR_SENDER_NAME_W PROP_TAG( PT_UNICODE, 0x0C1A)
+#define PR_SENDER_NAME_A PROP_TAG( PT_STRING8, 0x0C1A)
+#define PR_SUPPLEMENTARY_INFO PROP_TAG( PT_TSTRING, 0x0C1B)
+#define PR_SUPPLEMENTARY_INFO_W PROP_TAG( PT_UNICODE, 0x0C1B)
+#define PR_SUPPLEMENTARY_INFO_A PROP_TAG( PT_STRING8, 0x0C1B)
+#define PR_TYPE_OF_MTS_USER PROP_TAG( PT_LONG, 0x0C1C)
+#define PR_SENDER_SEARCH_KEY PROP_TAG( PT_BINARY, 0x0C1D)
+#define PR_SENDER_ADDRTYPE PROP_TAG( PT_TSTRING, 0x0C1E)
+#define PR_SENDER_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x0C1E)
+#define PR_SENDER_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x0C1E)
+#define PR_SENDER_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x0C1F)
+#define PR_SENDER_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x0C1F)
+#define PR_SENDER_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x0C1F)
+
+/*
+ * Message non-transmittable properties
+ */
+
+/*
+ * The two tags, PR_MESSAGE_RECIPIENTS and PR_MESSAGE_ATTACHMENTS,
+ * are to be used in the exclude list passed to
+ * IMessage::CopyTo when the caller wants either the recipients or attachments
+ * of the message to not get copied. It is also used in the ProblemArray
+ * return from IMessage::CopyTo when an error is encountered copying them
+ */
+
+#define PR_CURRENT_VERSION PROP_TAG( PT_I8, 0x0E00)
+#define PR_DELETE_AFTER_SUBMIT PROP_TAG( PT_BOOLEAN, 0x0E01)
+#define PR_DISPLAY_BCC PROP_TAG( PT_TSTRING, 0x0E02)
+#define PR_DISPLAY_BCC_W PROP_TAG( PT_UNICODE, 0x0E02)
+#define PR_DISPLAY_BCC_A PROP_TAG( PT_STRING8, 0x0E02)
+#define PR_DISPLAY_CC PROP_TAG( PT_TSTRING, 0x0E03)
+#define PR_DISPLAY_CC_W PROP_TAG( PT_UNICODE, 0x0E03)
+#define PR_DISPLAY_CC_A PROP_TAG( PT_STRING8, 0x0E03)
+#define PR_DISPLAY_TO PROP_TAG( PT_TSTRING, 0x0E04)
+#define PR_DISPLAY_TO_W PROP_TAG( PT_UNICODE, 0x0E04)
+#define PR_DISPLAY_TO_A PROP_TAG( PT_STRING8, 0x0E04)
+#define PR_PARENT_DISPLAY PROP_TAG( PT_TSTRING, 0x0E05)
+#define PR_PARENT_DISPLAY_W PROP_TAG( PT_UNICODE, 0x0E05)
+#define PR_PARENT_DISPLAY_A PROP_TAG( PT_STRING8, 0x0E05)
+#define PR_MESSAGE_DELIVERY_TIME PROP_TAG( PT_SYSTIME, 0x0E06)
+#define PR_MESSAGE_FLAGS PROP_TAG( PT_LONG, 0x0E07)
+#define PR_MESSAGE_SIZE PROP_TAG( PT_LONG, 0x0E08)
+#define PR_PARENT_ENTRYID PROP_TAG( PT_BINARY, 0x0E09)
+#define PR_SENTMAIL_ENTRYID PROP_TAG( PT_BINARY, 0x0E0A)
+#define PR_CORRELATE PROP_TAG( PT_BOOLEAN, 0x0E0C)
+#define PR_CORRELATE_MTSID PROP_TAG( PT_BINARY, 0x0E0D)
+#define PR_DISCRETE_VALUES PROP_TAG( PT_BOOLEAN, 0x0E0E)
+#define PR_RESPONSIBILITY PROP_TAG( PT_BOOLEAN, 0x0E0F)
+#define PR_SPOOLER_STATUS PROP_TAG( PT_LONG, 0x0E10)
+#define PR_TRANSPORT_STATUS PROP_TAG( PT_LONG, 0x0E11)
+#define PR_MESSAGE_RECIPIENTS PROP_TAG( PT_OBJECT, 0x0E12)
+#define PR_MESSAGE_ATTACHMENTS PROP_TAG( PT_OBJECT, 0x0E13)
+#define PR_SUBMIT_FLAGS PROP_TAG( PT_LONG, 0x0E14)
+#define PR_RECIPIENT_STATUS PROP_TAG( PT_LONG, 0x0E15)
+#define PR_TRANSPORT_KEY PROP_TAG( PT_LONG, 0x0E16)
+#define PR_MSG_STATUS PROP_TAG( PT_LONG, 0x0E17)
+#define PR_MESSAGE_DOWNLOAD_TIME PROP_TAG( PT_LONG, 0x0E18)
+#define PR_CREATION_VERSION PROP_TAG( PT_I8, 0x0E19)
+#define PR_MODIFY_VERSION PROP_TAG( PT_I8, 0x0E1A)
+#define PR_HASATTACH PROP_TAG( PT_BOOLEAN, 0x0E1B)
+#define PR_BODY_CRC PROP_TAG( PT_LONG, 0x0E1C)
+#define PR_NORMALIZED_SUBJECT PROP_TAG( PT_TSTRING, 0x0E1D)
+#define PR_NORMALIZED_SUBJECT_W PROP_TAG( PT_UNICODE, 0x0E1D)
+#define PR_NORMALIZED_SUBJECT_A PROP_TAG( PT_STRING8, 0x0E1D)
+#define PR_RTF_IN_SYNC PROP_TAG( PT_BOOLEAN, 0x0E1F)
+#define PR_ATTACH_SIZE PROP_TAG( PT_LONG, 0x0E20)
+#define PR_ATTACH_NUM PROP_TAG( PT_LONG, 0x0E21)
+#define PR_PREPROCESS PROP_TAG( PT_BOOLEAN, 0x0E22)
+
+/* PR_ORIGINAL_DISPLAY_TO, _CC, and _BCC moved to transmittible range 03/09/95 */
+
+#define PR_ORIGINATING_MTA_CERTIFICATE PROP_TAG( PT_BINARY, 0x0E25)
+#define PR_PROOF_OF_SUBMISSION PROP_TAG( PT_BINARY, 0x0E26)
+
+
+/*
+ * The range of non-message and non-recipient property IDs (0x3000 - 0x3FFF) is
+ * further broken down into ranges to make assigning new property IDs easier.
+ *
+ * From To Kind of property
+ * --------------------------------
+ * 3000 32FF MAPI_defined common property
+ * 3200 33FF MAPI_defined form property
+ * 3400 35FF MAPI_defined message store property
+ * 3600 36FF MAPI_defined Folder or AB Container property
+ * 3700 38FF MAPI_defined attachment property
+ * 3900 39FF MAPI_defined address book property
+ * 3A00 3BFF MAPI_defined mailuser property
+ * 3C00 3CFF MAPI_defined DistList property
+ * 3D00 3DFF MAPI_defined Profile Section property
+ * 3E00 3EFF MAPI_defined Status property
+ * 3F00 3FFF MAPI_defined display table property
+ */
+
+/*
+ * Properties common to numerous MAPI objects.
+ *
+ * Those properties that can appear on messages are in the
+ * non-transmittable range for messages. They start at the high
+ * end of that range and work down.
+ *
+ * Properties that never appear on messages are defined in the common
+ * property range (see above).
+ */
+
+/*
+ * properties that are common to multiple objects (including message objects)
+ * -- these ids are in the non-transmittable range
+ */
+
+#define PR_ENTRYID PROP_TAG( PT_BINARY, 0x0FFF)
+#define PR_OBJECT_TYPE PROP_TAG( PT_LONG, 0x0FFE)
+#define PR_ICON PROP_TAG( PT_BINARY, 0x0FFD)
+#define PR_MINI_ICON PROP_TAG( PT_BINARY, 0x0FFC)
+#define PR_STORE_ENTRYID PROP_TAG( PT_BINARY, 0x0FFB)
+#define PR_STORE_RECORD_KEY PROP_TAG( PT_BINARY, 0x0FFA)
+#define PR_RECORD_KEY PROP_TAG( PT_BINARY, 0x0FF9)
+#define PR_MAPPING_SIGNATURE PROP_TAG( PT_BINARY, 0x0FF8)
+#define PR_ACCESS_LEVEL PROP_TAG( PT_LONG, 0x0FF7)
+#define PR_INSTANCE_KEY PROP_TAG( PT_BINARY, 0x0FF6)
+#define PR_ROW_TYPE PROP_TAG( PT_LONG, 0x0FF5)
+#define PR_ACCESS PROP_TAG( PT_LONG, 0x0FF4)
+
+/*
+ * properties that are common to multiple objects (usually not including message objects)
+ * -- these ids are in the transmittable range
+ */
+
+#define PR_ROWID PROP_TAG( PT_LONG, 0x3000)
+#define PR_DISPLAY_NAME PROP_TAG( PT_TSTRING, 0x3001)
+#define PR_DISPLAY_NAME_W PROP_TAG( PT_UNICODE, 0x3001)
+#define PR_DISPLAY_NAME_A PROP_TAG( PT_STRING8, 0x3001)
+#define PR_ADDRTYPE PROP_TAG( PT_TSTRING, 0x3002)
+#define PR_ADDRTYPE_W PROP_TAG( PT_UNICODE, 0x3002)
+#define PR_ADDRTYPE_A PROP_TAG( PT_STRING8, 0x3002)
+#define PR_EMAIL_ADDRESS PROP_TAG( PT_TSTRING, 0x3003)
+#define PR_EMAIL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x3003)
+#define PR_EMAIL_ADDRESS_A PROP_TAG( PT_STRING8, 0x3003)
+#define PR_COMMENT PROP_TAG( PT_TSTRING, 0x3004)
+#define PR_COMMENT_W PROP_TAG( PT_UNICODE, 0x3004)
+#define PR_COMMENT_A PROP_TAG( PT_STRING8, 0x3004)
+#define PR_DEPTH PROP_TAG( PT_LONG, 0x3005)
+#define PR_PROVIDER_DISPLAY PROP_TAG( PT_TSTRING, 0x3006)
+#define PR_PROVIDER_DISPLAY_W PROP_TAG( PT_UNICODE, 0x3006)
+#define PR_PROVIDER_DISPLAY_A PROP_TAG( PT_STRING8, 0x3006)
+#define PR_CREATION_TIME PROP_TAG( PT_SYSTIME, 0x3007)
+#define PR_LAST_MODIFICATION_TIME PROP_TAG( PT_SYSTIME, 0x3008)
+#define PR_RESOURCE_FLAGS PROP_TAG( PT_LONG, 0x3009)
+#define PR_PROVIDER_DLL_NAME PROP_TAG( PT_TSTRING, 0x300A)
+#define PR_PROVIDER_DLL_NAME_W PROP_TAG( PT_UNICODE, 0x300A)
+#define PR_PROVIDER_DLL_NAME_A PROP_TAG( PT_STRING8, 0x300A)
+#define PR_SEARCH_KEY PROP_TAG( PT_BINARY, 0x300B)
+#define PR_PROVIDER_UID PROP_TAG( PT_BINARY, 0x300C)
+#define PR_PROVIDER_ORDINAL PROP_TAG( PT_LONG, 0x300D)
+
+/*
+ * MAPI Form properties
+ */
+#define PR_FORM_VERSION PROP_TAG(PT_TSTRING, 0x3301)
+#define PR_FORM_VERSION_W PROP_TAG(PT_UNICODE, 0x3301)
+#define PR_FORM_VERSION_A PROP_TAG(PT_STRING8, 0x3301)
+#define PR_FORM_CLSID PROP_TAG(PT_CLSID, 0x3302)
+#define PR_FORM_CONTACT_NAME PROP_TAG(PT_TSTRING, 0x3303)
+#define PR_FORM_CONTACT_NAME_W PROP_TAG(PT_UNICODE, 0x3303)
+#define PR_FORM_CONTACT_NAME_A PROP_TAG(PT_STRING8, 0x3303)
+#define PR_FORM_CATEGORY PROP_TAG(PT_TSTRING, 0x3304)
+#define PR_FORM_CATEGORY_W PROP_TAG(PT_UNICODE, 0x3304)
+#define PR_FORM_CATEGORY_A PROP_TAG(PT_STRING8, 0x3304)
+#define PR_FORM_CATEGORY_SUB PROP_TAG(PT_TSTRING, 0x3305)
+#define PR_FORM_CATEGORY_SUB_W PROP_TAG(PT_UNICODE, 0x3305)
+#define PR_FORM_CATEGORY_SUB_A PROP_TAG(PT_STRING8, 0x3305)
+#define PR_FORM_HOST_MAP PROP_TAG(PT_MV_LONG, 0x3306)
+#define PR_FORM_HIDDEN PROP_TAG(PT_BOOLEAN, 0x3307)
+#define PR_FORM_DESIGNER_NAME PROP_TAG(PT_TSTRING, 0x3308)
+#define PR_FORM_DESIGNER_NAME_W PROP_TAG(PT_UNICODE, 0x3308)
+#define PR_FORM_DESIGNER_NAME_A PROP_TAG(PT_STRING8, 0x3308)
+#define PR_FORM_DESIGNER_GUID PROP_TAG(PT_CLSID, 0x3309)
+#define PR_FORM_MESSAGE_BEHAVIOR PROP_TAG(PT_LONG, 0x330A)
+
+/*
+ * Message store properties
+ */
+
+#define PR_DEFAULT_STORE PROP_TAG( PT_BOOLEAN, 0x3400)
+#define PR_STORE_SUPPORT_MASK PROP_TAG( PT_LONG, 0x340D)
+#define PR_STORE_STATE PROP_TAG( PT_LONG, 0x340E)
+
+#define PR_IPM_SUBTREE_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3410)
+#define PR_IPM_OUTBOX_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3411)
+#define PR_IPM_WASTEBASKET_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3412)
+#define PR_IPM_SENTMAIL_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3413)
+#define PR_MDB_PROVIDER PROP_TAG( PT_BINARY, 0x3414)
+#define PR_RECEIVE_FOLDER_SETTINGS PROP_TAG( PT_OBJECT, 0x3415)
+
+#define PR_VALID_FOLDER_MASK PROP_TAG( PT_LONG, 0x35DF)
+#define PR_IPM_SUBTREE_ENTRYID PROP_TAG( PT_BINARY, 0x35E0)
+
+#define PR_IPM_OUTBOX_ENTRYID PROP_TAG( PT_BINARY, 0x35E2)
+#define PR_IPM_WASTEBASKET_ENTRYID PROP_TAG( PT_BINARY, 0x35E3)
+#define PR_IPM_SENTMAIL_ENTRYID PROP_TAG( PT_BINARY, 0x35E4)
+#define PR_VIEWS_ENTRYID PROP_TAG( PT_BINARY, 0x35E5)
+#define PR_COMMON_VIEWS_ENTRYID PROP_TAG( PT_BINARY, 0x35E6)
+#define PR_FINDER_ENTRYID PROP_TAG( PT_BINARY, 0x35E7)
+
+/* Proptags 0x35E8-0x35FF reserved for folders "guaranteed" by PR_VALID_FOLDER_MASK */
+
+
+/*
+ * Folder and AB Container properties
+ */
+
+#define PR_CONTAINER_FLAGS PROP_TAG( PT_LONG, 0x3600)
+#define PR_FOLDER_TYPE PROP_TAG( PT_LONG, 0x3601)
+#define PR_CONTENT_COUNT PROP_TAG( PT_LONG, 0x3602)
+#define PR_CONTENT_UNREAD PROP_TAG( PT_LONG, 0x3603)
+#define PR_CREATE_TEMPLATES PROP_TAG( PT_OBJECT, 0x3604)
+#define PR_DETAILS_TABLE PROP_TAG( PT_OBJECT, 0x3605)
+#define PR_SEARCH PROP_TAG( PT_OBJECT, 0x3607)
+#define PR_SELECTABLE PROP_TAG( PT_BOOLEAN, 0x3609)
+#define PR_SUBFOLDERS PROP_TAG( PT_BOOLEAN, 0x360A)
+#define PR_STATUS PROP_TAG( PT_LONG, 0x360B)
+#define PR_ANR PROP_TAG( PT_TSTRING, 0x360C)
+#define PR_ANR_W PROP_TAG( PT_UNICODE, 0x360C)
+#define PR_ANR_A PROP_TAG( PT_STRING8, 0x360C)
+#define PR_CONTENTS_SORT_ORDER PROP_TAG( PT_MV_LONG, 0x360D)
+#define PR_CONTAINER_HIERARCHY PROP_TAG( PT_OBJECT, 0x360E)
+#define PR_CONTAINER_CONTENTS PROP_TAG( PT_OBJECT, 0x360F)
+#define PR_FOLDER_ASSOCIATED_CONTENTS PROP_TAG( PT_OBJECT, 0x3610)
+#define PR_DEF_CREATE_DL PROP_TAG( PT_BINARY, 0x3611)
+#define PR_DEF_CREATE_MAILUSER PROP_TAG( PT_BINARY, 0x3612)
+#define PR_CONTAINER_CLASS PROP_TAG( PT_TSTRING, 0x3613)
+#define PR_CONTAINER_CLASS_W PROP_TAG( PT_UNICODE, 0x3613)
+#define PR_CONTAINER_CLASS_A PROP_TAG( PT_STRING8, 0x3613)
+#define PR_CONTAINER_MODIFY_VERSION PROP_TAG( PT_I8, 0x3614)
+#define PR_AB_PROVIDER_ID PROP_TAG( PT_BINARY, 0x3615)
+#define PR_DEFAULT_VIEW_ENTRYID PROP_TAG( PT_BINARY, 0x3616)
+#define PR_ASSOC_CONTENT_COUNT PROP_TAG( PT_LONG, 0x3617)
+
+/* Reserved 0x36C0-0x36FF */
+
+/*
+ * Attachment properties
+ */
+
+#define PR_ATTACHMENT_X400_PARAMETERS PROP_TAG( PT_BINARY, 0x3700)
+#define PR_ATTACH_DATA_OBJ PROP_TAG( PT_OBJECT, 0x3701)
+#define PR_ATTACH_DATA_BIN PROP_TAG( PT_BINARY, 0x3701)
+#define PR_ATTACH_ENCODING PROP_TAG( PT_BINARY, 0x3702)
+#define PR_ATTACH_EXTENSION PROP_TAG( PT_TSTRING, 0x3703)
+#define PR_ATTACH_EXTENSION_W PROP_TAG( PT_UNICODE, 0x3703)
+#define PR_ATTACH_EXTENSION_A PROP_TAG( PT_STRING8, 0x3703)
+#define PR_ATTACH_FILENAME PROP_TAG( PT_TSTRING, 0x3704)
+#define PR_ATTACH_FILENAME_W PROP_TAG( PT_UNICODE, 0x3704)
+#define PR_ATTACH_FILENAME_A PROP_TAG( PT_STRING8, 0x3704)
+#define PR_ATTACH_METHOD PROP_TAG( PT_LONG, 0x3705)
+#define PR_ATTACH_LONG_FILENAME PROP_TAG( PT_TSTRING, 0x3707)
+#define PR_ATTACH_LONG_FILENAME_W PROP_TAG( PT_UNICODE, 0x3707)
+#define PR_ATTACH_LONG_FILENAME_A PROP_TAG( PT_STRING8, 0x3707)
+#define PR_ATTACH_PATHNAME PROP_TAG( PT_TSTRING, 0x3708)
+#define PR_ATTACH_PATHNAME_W PROP_TAG( PT_UNICODE, 0x3708)
+#define PR_ATTACH_PATHNAME_A PROP_TAG( PT_STRING8, 0x3708)
+#define PR_ATTACH_RENDERING PROP_TAG( PT_BINARY, 0x3709)
+#define PR_ATTACH_TAG PROP_TAG( PT_BINARY, 0x370A)
+#define PR_RENDERING_POSITION PROP_TAG( PT_LONG, 0x370B)
+#define PR_ATTACH_TRANSPORT_NAME PROP_TAG( PT_TSTRING, 0x370C)
+#define PR_ATTACH_TRANSPORT_NAME_W PROP_TAG( PT_UNICODE, 0x370C)
+#define PR_ATTACH_TRANSPORT_NAME_A PROP_TAG( PT_STRING8, 0x370C)
+#define PR_ATTACH_LONG_PATHNAME PROP_TAG( PT_TSTRING, 0x370D)
+#define PR_ATTACH_LONG_PATHNAME_W PROP_TAG( PT_UNICODE, 0x370D)
+#define PR_ATTACH_LONG_PATHNAME_A PROP_TAG( PT_STRING8, 0x370D)
+#define PR_ATTACH_MIME_TAG PROP_TAG( PT_TSTRING, 0x370E)
+#define PR_ATTACH_MIME_TAG_W PROP_TAG( PT_UNICODE, 0x370E)
+#define PR_ATTACH_MIME_TAG_A PROP_TAG( PT_STRING8, 0x370E)
+#define PR_ATTACH_ADDITIONAL_INFO PROP_TAG( PT_BINARY, 0x370F)
+
+/*
+ * AB Object properties
+ */
+
+#define PR_DISPLAY_TYPE PROP_TAG( PT_LONG, 0x3900)
+#define PR_TEMPLATEID PROP_TAG( PT_BINARY, 0x3902)
+#define PR_PRIMARY_CAPABILITY PROP_TAG( PT_BINARY, 0x3904)
+
+
+/*
+ * Mail user properties
+ */
+#define PR_7BIT_DISPLAY_NAME PROP_TAG( PT_STRING8, 0x39FF)
+#define PR_ACCOUNT PROP_TAG( PT_TSTRING, 0x3A00)
+#define PR_ACCOUNT_W PROP_TAG( PT_UNICODE, 0x3A00)
+#define PR_ACCOUNT_A PROP_TAG( PT_STRING8, 0x3A00)
+#define PR_ALTERNATE_RECIPIENT PROP_TAG( PT_BINARY, 0x3A01)
+#define PR_CALLBACK_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A02)
+#define PR_CALLBACK_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A02)
+#define PR_CALLBACK_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A02)
+#define PR_CONVERSION_PROHIBITED PROP_TAG( PT_BOOLEAN, 0x3A03)
+#define PR_DISCLOSE_RECIPIENTS PROP_TAG( PT_BOOLEAN, 0x3A04)
+#define PR_GENERATION PROP_TAG( PT_TSTRING, 0x3A05)
+#define PR_GENERATION_W PROP_TAG( PT_UNICODE, 0x3A05)
+#define PR_GENERATION_A PROP_TAG( PT_STRING8, 0x3A05)
+#define PR_GIVEN_NAME PROP_TAG( PT_TSTRING, 0x3A06)
+#define PR_GIVEN_NAME_W PROP_TAG( PT_UNICODE, 0x3A06)
+#define PR_GIVEN_NAME_A PROP_TAG( PT_STRING8, 0x3A06)
+#define PR_GOVERNMENT_ID_NUMBER PROP_TAG( PT_TSTRING, 0x3A07)
+#define PR_GOVERNMENT_ID_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A07)
+#define PR_GOVERNMENT_ID_NUMBER_A PROP_TAG( PT_STRING8, 0x3A07)
+#define PR_BUSINESS_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A08)
+#define PR_BUSINESS_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A08)
+#define PR_BUSINESS_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A08)
+#define PR_OFFICE_TELEPHONE_NUMBER PR_BUSINESS_TELEPHONE_NUMBER
+#define PR_OFFICE_TELEPHONE_NUMBER_W PR_BUSINESS_TELEPHONE_NUMBER_W
+#define PR_OFFICE_TELEPHONE_NUMBER_A PR_BUSINESS_TELEPHONE_NUMBER_A
+#define PR_HOME_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A09)
+#define PR_HOME_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A09)
+#define PR_HOME_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A09)
+#define PR_INITIALS PROP_TAG( PT_TSTRING, 0x3A0A)
+#define PR_INITIALS_W PROP_TAG( PT_UNICODE, 0x3A0A)
+#define PR_INITIALS_A PROP_TAG( PT_STRING8, 0x3A0A)
+#define PR_KEYWORD PROP_TAG( PT_TSTRING, 0x3A0B)
+#define PR_KEYWORD_W PROP_TAG( PT_UNICODE, 0x3A0B)
+#define PR_KEYWORD_A PROP_TAG( PT_STRING8, 0x3A0B)
+#define PR_LANGUAGE PROP_TAG( PT_TSTRING, 0x3A0C)
+#define PR_LANGUAGE_W PROP_TAG( PT_UNICODE, 0x3A0C)
+#define PR_LANGUAGE_A PROP_TAG( PT_STRING8, 0x3A0C)
+#define PR_LOCATION PROP_TAG( PT_TSTRING, 0x3A0D)
+#define PR_LOCATION_W PROP_TAG( PT_UNICODE, 0x3A0D)
+#define PR_LOCATION_A PROP_TAG( PT_STRING8, 0x3A0D)
+#define PR_MAIL_PERMISSION PROP_TAG( PT_BOOLEAN, 0x3A0E)
+#define PR_MHS_COMMON_NAME PROP_TAG( PT_TSTRING, 0x3A0F)
+#define PR_MHS_COMMON_NAME_W PROP_TAG( PT_UNICODE, 0x3A0F)
+#define PR_MHS_COMMON_NAME_A PROP_TAG( PT_STRING8, 0x3A0F)
+#define PR_ORGANIZATIONAL_ID_NUMBER PROP_TAG( PT_TSTRING, 0x3A10)
+#define PR_ORGANIZATIONAL_ID_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A10)
+#define PR_ORGANIZATIONAL_ID_NUMBER_A PROP_TAG( PT_STRING8, 0x3A10)
+#define PR_SURNAME PROP_TAG( PT_TSTRING, 0x3A11)
+#define PR_SURNAME_W PROP_TAG( PT_UNICODE, 0x3A11)
+#define PR_SURNAME_A PROP_TAG( PT_STRING8, 0x3A11)
+#define PR_ORIGINAL_ENTRYID PROP_TAG( PT_BINARY, 0x3A12)
+#define PR_ORIGINAL_DISPLAY_NAME PROP_TAG( PT_TSTRING, 0x3A13)
+#define PR_ORIGINAL_DISPLAY_NAME_W PROP_TAG( PT_UNICODE, 0x3A13)
+#define PR_ORIGINAL_DISPLAY_NAME_A PROP_TAG( PT_STRING8, 0x3A13)
+#define PR_ORIGINAL_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3A14)
+#define PR_POSTAL_ADDRESS PROP_TAG( PT_TSTRING, 0x3A15)
+#define PR_POSTAL_ADDRESS_W PROP_TAG( PT_UNICODE, 0x3A15)
+#define PR_POSTAL_ADDRESS_A PROP_TAG( PT_STRING8, 0x3A15)
+#define PR_COMPANY_NAME PROP_TAG( PT_TSTRING, 0x3A16)
+#define PR_COMPANY_NAME_W PROP_TAG( PT_UNICODE, 0x3A16)
+#define PR_COMPANY_NAME_A PROP_TAG( PT_STRING8, 0x3A16)
+#define PR_TITLE PROP_TAG( PT_TSTRING, 0x3A17)
+#define PR_TITLE_W PROP_TAG( PT_UNICODE, 0x3A17)
+#define PR_TITLE_A PROP_TAG( PT_STRING8, 0x3A17)
+#define PR_DEPARTMENT_NAME PROP_TAG( PT_TSTRING, 0x3A18)
+#define PR_DEPARTMENT_NAME_W PROP_TAG( PT_UNICODE, 0x3A18)
+#define PR_DEPARTMENT_NAME_A PROP_TAG( PT_STRING8, 0x3A18)
+#define PR_OFFICE_LOCATION PROP_TAG( PT_TSTRING, 0x3A19)
+#define PR_OFFICE_LOCATION_W PROP_TAG( PT_UNICODE, 0x3A19)
+#define PR_OFFICE_LOCATION_A PROP_TAG( PT_STRING8, 0x3A19)
+#define PR_PRIMARY_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1A)
+#define PR_PRIMARY_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1A)
+#define PR_PRIMARY_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1A)
+#define PR_BUSINESS2_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1B)
+#define PR_BUSINESS2_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1B)
+#define PR_BUSINESS2_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1B)
+#define PR_OFFICE2_TELEPHONE_NUMBER PR_BUSINESS2_TELEPHONE_NUMBER
+#define PR_OFFICE2_TELEPHONE_NUMBER_W PR_BUSINESS2_TELEPHONE_NUMBER_W
+#define PR_OFFICE2_TELEPHONE_NUMBER_A PR_BUSINESS2_TELEPHONE_NUMBER_A
+#define PR_MOBILE_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1C)
+#define PR_MOBILE_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1C)
+#define PR_MOBILE_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1C)
+#define PR_CELLULAR_TELEPHONE_NUMBER PR_MOBILE_TELEPHONE_NUMBER
+#define PR_CELLULAR_TELEPHONE_NUMBER_W PR_MOBILE_TELEPHONE_NUMBER_W
+#define PR_CELLULAR_TELEPHONE_NUMBER_A PR_MOBILE_TELEPHONE_NUMBER_A
+#define PR_RADIO_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1D)
+#define PR_RADIO_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1D)
+#define PR_RADIO_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1D)
+#define PR_CAR_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1E)
+#define PR_CAR_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1E)
+#define PR_CAR_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1E)
+#define PR_OTHER_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A1F)
+#define PR_OTHER_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A1F)
+#define PR_OTHER_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A1F)
+#define PR_TRANSMITABLE_DISPLAY_NAME PROP_TAG( PT_TSTRING, 0x3A20)
+#define PR_TRANSMITABLE_DISPLAY_NAME_W PROP_TAG( PT_UNICODE, 0x3A20)
+#define PR_TRANSMITABLE_DISPLAY_NAME_A PROP_TAG( PT_STRING8, 0x3A20)
+#define PR_PAGER_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A21)
+#define PR_PAGER_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A21)
+#define PR_PAGER_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A21)
+#define PR_BEEPER_TELEPHONE_NUMBER PR_PAGER_TELEPHONE_NUMBER
+#define PR_BEEPER_TELEPHONE_NUMBER_W PR_PAGER_TELEPHONE_NUMBER_W
+#define PR_BEEPER_TELEPHONE_NUMBER_A PR_PAGER_TELEPHONE_NUMBER_A
+#define PR_USER_CERTIFICATE PROP_TAG( PT_BINARY, 0x3A22)
+#define PR_PRIMARY_FAX_NUMBER PROP_TAG( PT_TSTRING, 0x3A23)
+#define PR_PRIMARY_FAX_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A23)
+#define PR_PRIMARY_FAX_NUMBER_A PROP_TAG( PT_STRING8, 0x3A23)
+#define PR_BUSINESS_FAX_NUMBER PROP_TAG( PT_TSTRING, 0x3A24)
+#define PR_BUSINESS_FAX_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A24)
+#define PR_BUSINESS_FAX_NUMBER_A PROP_TAG( PT_STRING8, 0x3A24)
+#define PR_HOME_FAX_NUMBER PROP_TAG( PT_TSTRING, 0x3A25)
+#define PR_HOME_FAX_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A25)
+#define PR_HOME_FAX_NUMBER_A PROP_TAG( PT_STRING8, 0x3A25)
+#define PR_COUNTRY PROP_TAG( PT_TSTRING, 0x3A26)
+#define PR_COUNTRY_W PROP_TAG( PT_UNICODE, 0x3A26)
+#define PR_COUNTRY_A PROP_TAG( PT_STRING8, 0x3A26)
+#define PR_BUSINESS_ADDRESS_COUNTRY PR_COUNTRY
+#define PR_BUSINESS_ADDRESS_COUNTRY_W PR_COUNTRY_W
+#define PR_BUSINESS_ADDRESS_COUNTRY_A PR_COUNTRY_A
+
+#define PR_LOCALITY PROP_TAG( PT_TSTRING, 0x3A27)
+#define PR_LOCALITY_W PROP_TAG( PT_UNICODE, 0x3A27)
+#define PR_LOCALITY_A PROP_TAG( PT_STRING8, 0x3A27)
+#define PR_BUSINESS_ADDRESS_CITY PR_LOCALITY
+#define PR_BUSINESS_ADDRESS_CITY_W PR_LOCALITY_W
+#define PR_BUSINESS_ADDRESS_CITY_A PR_LOCALITY_A
+
+#define PR_STATE_OR_PROVINCE PROP_TAG( PT_TSTRING, 0x3A28)
+#define PR_STATE_OR_PROVINCE_W PROP_TAG( PT_UNICODE, 0x3A28)
+#define PR_STATE_OR_PROVINCE_A PROP_TAG( PT_STRING8, 0x3A28)
+#define PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE PR_STATE_OR_PROVINCE
+#define PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_W PR_STATE_OR_PROVINCE_W
+#define PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_A PR_STATE_OR_PROVINCE_A
+
+#define PR_STREET_ADDRESS PROP_TAG( PT_TSTRING, 0x3A29)
+#define PR_STREET_ADDRESS_W PROP_TAG( PT_UNICODE, 0x3A29)
+#define PR_STREET_ADDRESS_A PROP_TAG( PT_STRING8, 0x3A29)
+#define PR_BUSINESS_ADDRESS_STREET PR_STREET_ADDRESS
+#define PR_BUSINESS_ADDRESS_STREET_W PR_STREET_ADDRESS_W
+#define PR_BUSINESS_ADDRESS_STREET_A PR_STREET_ADDRESS_A
+
+#define PR_POSTAL_CODE PROP_TAG( PT_TSTRING, 0x3A2A)
+#define PR_POSTAL_CODE_W PROP_TAG( PT_UNICODE, 0x3A2A)
+#define PR_POSTAL_CODE_A PROP_TAG( PT_STRING8, 0x3A2A)
+#define PR_BUSINESS_ADDRESS_POSTAL_CODE PR_POSTAL_CODE
+#define PR_BUSINESS_ADDRESS_POSTAL_CODE_W PR_POSTAL_CODE_W
+#define PR_BUSINESS_ADDRESS_POSTAL_CODE_A PR_POSTAL_CODE_A
+
+
+#define PR_POST_OFFICE_BOX PROP_TAG( PT_TSTRING, 0x3A2B)
+#define PR_POST_OFFICE_BOX_W PROP_TAG( PT_UNICODE, 0x3A2B)
+#define PR_POST_OFFICE_BOX_A PROP_TAG( PT_STRING8, 0x3A2B)
+#define PR_BUSINESS_ADDRESS_POST_OFFICE_BOX PR_POST_OFFICE_BOX
+#define PR_BUSINESS_ADDRESS_POST_OFFICE_BOX_W PR_POST_OFFICE_BOX_W
+#define PR_BUSINESS_ADDRESS_POST_OFFICE_BOX_A PR_POST_OFFICE_BOX_A
+
+
+#define PR_TELEX_NUMBER PROP_TAG( PT_TSTRING, 0x3A2C)
+#define PR_TELEX_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A2C)
+#define PR_TELEX_NUMBER_A PROP_TAG( PT_STRING8, 0x3A2C)
+#define PR_ISDN_NUMBER PROP_TAG( PT_TSTRING, 0x3A2D)
+#define PR_ISDN_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A2D)
+#define PR_ISDN_NUMBER_A PROP_TAG( PT_STRING8, 0x3A2D)
+#define PR_ASSISTANT_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A2E)
+#define PR_ASSISTANT_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A2E)
+#define PR_ASSISTANT_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A2E)
+#define PR_HOME2_TELEPHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A2F)
+#define PR_HOME2_TELEPHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A2F)
+#define PR_HOME2_TELEPHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A2F)
+#define PR_ASSISTANT PROP_TAG( PT_TSTRING, 0x3A30)
+#define PR_ASSISTANT_W PROP_TAG( PT_UNICODE, 0x3A30)
+#define PR_ASSISTANT_A PROP_TAG( PT_STRING8, 0x3A30)
+#define PR_SEND_RICH_INFO PROP_TAG( PT_BOOLEAN, 0x3A40)
+
+#define PR_WEDDING_ANNIVERSARY PROP_TAG( PT_SYSTIME, 0x3A41)
+#define PR_BIRTHDAY PROP_TAG( PT_SYSTIME, 0x3A42)
+
+
+#define PR_HOBBIES PROP_TAG( PT_TSTRING, 0x3A43)
+#define PR_HOBBIES_W PROP_TAG( PT_UNICODE, 0x3A43)
+#define PR_HOBBIES_A PROP_TAG( PT_STRING8, 0x3A43)
+
+#define PR_MIDDLE_NAME PROP_TAG( PT_TSTRING, 0x3A44)
+#define PR_MIDDLE_NAME_W PROP_TAG( PT_UNICODE, 0x3A44)
+#define PR_MIDDLE_NAME_A PROP_TAG( PT_STRING8, 0x3A44)
+
+#define PR_DISPLAY_NAME_PREFIX PROP_TAG( PT_TSTRING, 0x3A45)
+#define PR_DISPLAY_NAME_PREFIX_W PROP_TAG( PT_UNICODE, 0x3A45)
+#define PR_DISPLAY_NAME_PREFIX_A PROP_TAG( PT_STRING8, 0x3A45)
+
+#define PR_PROFESSION PROP_TAG( PT_TSTRING, 0x3A46)
+#define PR_PROFESSION_W PROP_TAG( PT_UNICODE, 0x3A46)
+#define PR_PROFESSION_A PROP_TAG( PT_STRING8, 0x3A46)
+
+#define PR_PREFERRED_BY_NAME PROP_TAG( PT_TSTRING, 0x3A47)
+#define PR_PREFERRED_BY_NAME_W PROP_TAG( PT_UNICODE, 0x3A47)
+#define PR_PREFERRED_BY_NAME_A PROP_TAG( PT_STRING8, 0x3A47)
+
+#define PR_SPOUSE_NAME PROP_TAG( PT_TSTRING, 0x3A48)
+#define PR_SPOUSE_NAME_W PROP_TAG( PT_UNICODE, 0x3A48)
+#define PR_SPOUSE_NAME_A PROP_TAG( PT_STRING8, 0x3A48)
+
+#define PR_COMPUTER_NETWORK_NAME PROP_TAG( PT_TSTRING, 0x3A49)
+#define PR_COMPUTER_NETWORK_NAME_W PROP_TAG( PT_UNICODE, 0x3A49)
+#define PR_COMPUTER_NETWORK_NAME_A PROP_TAG( PT_STRING8, 0x3A49)
+
+#define PR_CUSTOMER_ID PROP_TAG( PT_TSTRING, 0x3A4A)
+#define PR_CUSTOMER_ID_W PROP_TAG( PT_UNICODE, 0x3A4A)
+#define PR_CUSTOMER_ID_A PROP_TAG( PT_STRING8, 0x3A4A)
+
+#define PR_TTYTDD_PHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A4B)
+#define PR_TTYTDD_PHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A4B)
+#define PR_TTYTDD_PHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A4B)
+
+#define PR_FTP_SITE PROP_TAG( PT_TSTRING, 0x3A4C)
+#define PR_FTP_SITE_W PROP_TAG( PT_UNICODE, 0x3A4C)
+#define PR_FTP_SITE_A PROP_TAG( PT_STRING8, 0x3A4C)
+
+#define PR_GENDER PROP_TAG( PT_SHORT, 0x3A4D)
+
+#define PR_MANAGER_NAME PROP_TAG( PT_TSTRING, 0x3A4E)
+#define PR_MANAGER_NAME_W PROP_TAG( PT_UNICODE, 0x3A4E)
+#define PR_MANAGER_NAME_A PROP_TAG( PT_STRING8, 0x3A4E)
+
+#define PR_NICKNAME PROP_TAG( PT_TSTRING, 0x3A4F)
+#define PR_NICKNAME_W PROP_TAG( PT_UNICODE, 0x3A4F)
+#define PR_NICKNAME_A PROP_TAG( PT_STRING8, 0x3A4F)
+
+#define PR_PERSONAL_HOME_PAGE PROP_TAG( PT_TSTRING, 0x3A50)
+#define PR_PERSONAL_HOME_PAGE_W PROP_TAG( PT_UNICODE, 0x3A50)
+#define PR_PERSONAL_HOME_PAGE_A PROP_TAG( PT_STRING8, 0x3A50)
+
+
+#define PR_BUSINESS_HOME_PAGE PROP_TAG( PT_TSTRING, 0x3A51)
+#define PR_BUSINESS_HOME_PAGE_W PROP_TAG( PT_UNICODE, 0x3A51)
+#define PR_BUSINESS_HOME_PAGE_A PROP_TAG( PT_STRING8, 0x3A51)
+
+#define PR_CONTACT_VERSION PROP_TAG( PT_CLSID, 0x3A52)
+#define PR_CONTACT_ENTRYIDS PROP_TAG( PT_MV_BINARY, 0x3A53)
+
+#define PR_CONTACT_ADDRTYPES PROP_TAG( PT_MV_TSTRING, 0x3A54)
+#define PR_CONTACT_ADDRTYPES_W PROP_TAG( PT_MV_UNICODE, 0x3A54)
+#define PR_CONTACT_ADDRTYPES_A PROP_TAG( PT_MV_STRING8, 0x3A54)
+
+#define PR_CONTACT_DEFAULT_ADDRESS_INDEX PROP_TAG( PT_LONG, 0x3A55)
+
+#define PR_CONTACT_EMAIL_ADDRESSES PROP_TAG( PT_MV_TSTRING, 0x3A56)
+#define PR_CONTACT_EMAIL_ADDRESSES_W PROP_TAG( PT_MV_UNICODE, 0x3A56)
+#define PR_CONTACT_EMAIL_ADDRESSES_A PROP_TAG( PT_MV_STRING8, 0x3A56)
+
+
+#define PR_COMPANY_MAIN_PHONE_NUMBER PROP_TAG( PT_TSTRING, 0x3A57)
+#define PR_COMPANY_MAIN_PHONE_NUMBER_W PROP_TAG( PT_UNICODE, 0x3A57)
+#define PR_COMPANY_MAIN_PHONE_NUMBER_A PROP_TAG( PT_STRING8, 0x3A57)
+
+#define PR_CHILDRENS_NAMES PROP_TAG( PT_MV_TSTRING, 0x3A58)
+#define PR_CHILDRENS_NAMES_W PROP_TAG( PT_MV_UNICODE, 0x3A58)
+#define PR_CHILDRENS_NAMES_A PROP_TAG( PT_MV_STRING8, 0x3A58)
+
+
+
+#define PR_HOME_ADDRESS_CITY PROP_TAG( PT_TSTRING, 0x3A59)
+#define PR_HOME_ADDRESS_CITY_W PROP_TAG( PT_UNICODE, 0x3A59)
+#define PR_HOME_ADDRESS_CITY_A PROP_TAG( PT_STRING8, 0x3A59)
+
+#define PR_HOME_ADDRESS_COUNTRY PROP_TAG( PT_TSTRING, 0x3A5A)
+#define PR_HOME_ADDRESS_COUNTRY_W PROP_TAG( PT_UNICODE, 0x3A5A)
+#define PR_HOME_ADDRESS_COUNTRY_A PROP_TAG( PT_STRING8, 0x3A5A)
+
+#define PR_HOME_ADDRESS_POSTAL_CODE PROP_TAG( PT_TSTRING, 0x3A5B)
+#define PR_HOME_ADDRESS_POSTAL_CODE_W PROP_TAG( PT_UNICODE, 0x3A5B)
+#define PR_HOME_ADDRESS_POSTAL_CODE_A PROP_TAG( PT_STRING8, 0x3A5B)
+
+#define PR_HOME_ADDRESS_STATE_OR_PROVINCE PROP_TAG( PT_TSTRING, 0x3A5C)
+#define PR_HOME_ADDRESS_STATE_OR_PROVINCE_W PROP_TAG( PT_UNICODE, 0x3A5C)
+#define PR_HOME_ADDRESS_STATE_OR_PROVINCE_A PROP_TAG( PT_STRING8, 0x3A5C)
+
+#define PR_HOME_ADDRESS_STREET PROP_TAG( PT_TSTRING, 0x3A5D)
+#define PR_HOME_ADDRESS_STREET_W PROP_TAG( PT_UNICODE, 0x3A5D)
+#define PR_HOME_ADDRESS_STREET_A PROP_TAG( PT_STRING8, 0x3A5D)
+
+#define PR_HOME_ADDRESS_POST_OFFICE_BOX PROP_TAG( PT_TSTRING, 0x3A5E)
+#define PR_HOME_ADDRESS_POST_OFFICE_BOX_W PROP_TAG( PT_UNICODE, 0x3A5E)
+#define PR_HOME_ADDRESS_POST_OFFICE_BOX_A PROP_TAG( PT_STRING8, 0x3A5E)
+
+#define PR_OTHER_ADDRESS_CITY PROP_TAG( PT_TSTRING, 0x3A5F)
+#define PR_OTHER_ADDRESS_CITY_W PROP_TAG( PT_UNICODE, 0x3A5F)
+#define PR_OTHER_ADDRESS_CITY_A PROP_TAG( PT_STRING8, 0x3A5F)
+
+#define PR_OTHER_ADDRESS_COUNTRY PROP_TAG( PT_TSTRING, 0x3A60)
+#define PR_OTHER_ADDRESS_COUNTRY_W PROP_TAG( PT_UNICODE, 0x3A60)
+#define PR_OTHER_ADDRESS_COUNTRY_A PROP_TAG( PT_STRING8, 0x3A60)
+
+#define PR_OTHER_ADDRESS_POSTAL_CODE PROP_TAG( PT_TSTRING, 0x3A61)
+#define PR_OTHER_ADDRESS_POSTAL_CODE_W PROP_TAG( PT_UNICODE, 0x3A61)
+#define PR_OTHER_ADDRESS_POSTAL_CODE_A PROP_TAG( PT_STRING8, 0x3A61)
+
+#define PR_OTHER_ADDRESS_STATE_OR_PROVINCE PROP_TAG( PT_TSTRING, 0x3A62)
+#define PR_OTHER_ADDRESS_STATE_OR_PROVINCE_W PROP_TAG( PT_UNICODE, 0x3A62)
+#define PR_OTHER_ADDRESS_STATE_OR_PROVINCE_A PROP_TAG( PT_STRING8, 0x3A62)
+
+#define PR_OTHER_ADDRESS_STREET PROP_TAG( PT_TSTRING, 0x3A63)
+#define PR_OTHER_ADDRESS_STREET_W PROP_TAG( PT_UNICODE, 0x3A63)
+#define PR_OTHER_ADDRESS_STREET_A PROP_TAG( PT_STRING8, 0x3A63)
+
+#define PR_OTHER_ADDRESS_POST_OFFICE_BOX PROP_TAG( PT_TSTRING, 0x3A64)
+#define PR_OTHER_ADDRESS_POST_OFFICE_BOX_W PROP_TAG( PT_UNICODE, 0x3A64)
+#define PR_OTHER_ADDRESS_POST_OFFICE_BOX_A PROP_TAG( PT_STRING8, 0x3A64)
+
+
+/*
+ * Profile section properties
+ */
+
+#define PR_STORE_PROVIDERS PROP_TAG( PT_BINARY, 0x3D00)
+#define PR_AB_PROVIDERS PROP_TAG( PT_BINARY, 0x3D01)
+#define PR_TRANSPORT_PROVIDERS PROP_TAG( PT_BINARY, 0x3D02)
+
+#define PR_DEFAULT_PROFILE PROP_TAG( PT_BOOLEAN, 0x3D04)
+#define PR_AB_SEARCH_PATH PROP_TAG( PT_MV_BINARY, 0x3D05)
+#define PR_AB_DEFAULT_DIR PROP_TAG( PT_BINARY, 0x3D06)
+#define PR_AB_DEFAULT_PAB PROP_TAG( PT_BINARY, 0x3D07)
+
+#define PR_FILTERING_HOOKS PROP_TAG( PT_BINARY, 0x3D08)
+#define PR_SERVICE_NAME PROP_TAG( PT_TSTRING, 0x3D09)
+#define PR_SERVICE_NAME_W PROP_TAG( PT_UNICODE, 0x3D09)
+#define PR_SERVICE_NAME_A PROP_TAG( PT_STRING8, 0x3D09)
+#define PR_SERVICE_DLL_NAME PROP_TAG( PT_TSTRING, 0x3D0A)
+#define PR_SERVICE_DLL_NAME_W PROP_TAG( PT_UNICODE, 0x3D0A)
+#define PR_SERVICE_DLL_NAME_A PROP_TAG( PT_STRING8, 0x3D0A)
+#define PR_SERVICE_ENTRY_NAME PROP_TAG( PT_STRING8, 0x3D0B)
+#define PR_SERVICE_UID PROP_TAG( PT_BINARY, 0x3D0C)
+#define PR_SERVICE_EXTRA_UIDS PROP_TAG( PT_BINARY, 0x3D0D)
+#define PR_SERVICES PROP_TAG( PT_BINARY, 0x3D0E)
+#define PR_SERVICE_SUPPORT_FILES PROP_TAG( PT_MV_TSTRING, 0x3D0F)
+#define PR_SERVICE_SUPPORT_FILES_W PROP_TAG( PT_MV_UNICODE, 0x3D0F)
+#define PR_SERVICE_SUPPORT_FILES_A PROP_TAG( PT_MV_STRING8, 0x3D0F)
+#define PR_SERVICE_DELETE_FILES PROP_TAG( PT_MV_TSTRING, 0x3D10)
+#define PR_SERVICE_DELETE_FILES_W PROP_TAG( PT_MV_UNICODE, 0x3D10)
+#define PR_SERVICE_DELETE_FILES_A PROP_TAG( PT_MV_STRING8, 0x3D10)
+#define PR_AB_SEARCH_PATH_UPDATE PROP_TAG( PT_BINARY, 0x3D11)
+#define PR_PROFILE_NAME PROP_TAG( PT_TSTRING, 0x3D12)
+#define PR_PROFILE_NAME_A PROP_TAG( PT_STRING8, 0x3D12)
+#define PR_PROFILE_NAME_W PROP_TAG( PT_UNICODE, 0x3D12)
+
+/*
+ * Status object properties
+ */
+
+#define PR_IDENTITY_DISPLAY PROP_TAG( PT_TSTRING, 0x3E00)
+#define PR_IDENTITY_DISPLAY_W PROP_TAG( PT_UNICODE, 0x3E00)
+#define PR_IDENTITY_DISPLAY_A PROP_TAG( PT_STRING8, 0x3E00)
+#define PR_IDENTITY_ENTRYID PROP_TAG( PT_BINARY, 0x3E01)
+#define PR_RESOURCE_METHODS PROP_TAG( PT_LONG, 0x3E02)
+#define PR_RESOURCE_TYPE PROP_TAG( PT_LONG, 0x3E03)
+#define PR_STATUS_CODE PROP_TAG( PT_LONG, 0x3E04)
+#define PR_IDENTITY_SEARCH_KEY PROP_TAG( PT_BINARY, 0x3E05)
+#define PR_OWN_STORE_ENTRYID PROP_TAG( PT_BINARY, 0x3E06)
+#define PR_RESOURCE_PATH PROP_TAG( PT_TSTRING, 0x3E07)
+#define PR_RESOURCE_PATH_W PROP_TAG( PT_UNICODE, 0x3E07)
+#define PR_RESOURCE_PATH_A PROP_TAG( PT_STRING8, 0x3E07)
+#define PR_STATUS_STRING PROP_TAG( PT_TSTRING, 0x3E08)
+#define PR_STATUS_STRING_W PROP_TAG( PT_UNICODE, 0x3E08)
+#define PR_STATUS_STRING_A PROP_TAG( PT_STRING8, 0x3E08)
+#define PR_X400_DEFERRED_DELIVERY_CANCEL PROP_TAG( PT_BOOLEAN, 0x3E09)
+#define PR_HEADER_FOLDER_ENTRYID PROP_TAG( PT_BINARY, 0x3E0A)
+#define PR_REMOTE_PROGRESS PROP_TAG( PT_LONG, 0x3E0B)
+#define PR_REMOTE_PROGRESS_TEXT PROP_TAG( PT_TSTRING, 0x3E0C)
+#define PR_REMOTE_PROGRESS_TEXT_W PROP_TAG( PT_UNICODE, 0x3E0C)
+#define PR_REMOTE_PROGRESS_TEXT_A PROP_TAG( PT_STRING8, 0x3E0C)
+#define PR_REMOTE_VALIDATE_OK PROP_TAG( PT_BOOLEAN, 0x3E0D)
+
+/*
+ * Display table properties
+ */
+
+#define PR_CONTROL_FLAGS PROP_TAG( PT_LONG, 0x3F00)
+#define PR_CONTROL_STRUCTURE PROP_TAG( PT_BINARY, 0x3F01)
+#define PR_CONTROL_TYPE PROP_TAG( PT_LONG, 0x3F02)
+#define PR_DELTAX PROP_TAG( PT_LONG, 0x3F03)
+#define PR_DELTAY PROP_TAG( PT_LONG, 0x3F04)
+#define PR_XPOS PROP_TAG( PT_LONG, 0x3F05)
+#define PR_YPOS PROP_TAG( PT_LONG, 0x3F06)
+#define PR_CONTROL_ID PROP_TAG( PT_BINARY, 0x3F07)
+#define PR_INITIAL_DETAILS_PANE PROP_TAG( PT_LONG, 0x3F08)
+
+/*
+ * Secure property id range
+ */
+
+#define PROP_ID_SECURE_MIN 0x67F0
+#define PROP_ID_SECURE_MAX 0x67FF
+
+
+#endif /* MAPITAGS_H */
diff --git a/comm/mailnews/mapi/include/mapiutil.h b/comm/mailnews/mapi/include/mapiutil.h
new file mode 100644
index 0000000000..a08634cbff
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiutil.h
@@ -0,0 +1,889 @@
+/*
+ * M A P I U T I L . H
+ *
+ * Definitions and prototypes for utility functions provided by MAPI
+ * in MAPI[xx].DLL.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _MAPIUTIL_H_
+#define _MAPIUTIL_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN64) && !defined (_WIN64)
+#define _WIN64
+#endif
+
+/*
+ * Under Win64 systems Win32 is also defined for backwards compatibility.
+ */
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef MAPIX_H
+#include <mapix.h>
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+
+/* IMAPITable in memory */
+
+/* ITableData Interface ---------------------------------------------------- */
+
+DECLARE_MAPI_INTERFACE_PTR(ITableData, LPTABLEDATA);
+
+typedef void (STDAPICALLTYPE CALLERRELEASE)(
+ ULONG_PTR ulCallerData,
+ LPTABLEDATA lpTblData,
+ LPMAPITABLE lpVue
+);
+
+#define MAPI_ITABLEDATA_METHODS(IPURE) \
+ MAPIMETHOD(HrGetView) \
+ (THIS_ LPSSortOrderSet lpSSortOrderSet, \
+ CALLERRELEASE FAR * lpfCallerRelease, \
+ ULONG_PTR ulCallerData, \
+ LPMAPITABLE FAR * lppMAPITable) IPURE; \
+ MAPIMETHOD(HrModifyRow) \
+ (THIS_ LPSRow) IPURE; \
+ MAPIMETHOD(HrDeleteRow) \
+ (THIS_ LPSPropValue lpSPropValue) IPURE; \
+ MAPIMETHOD(HrQueryRow) \
+ (THIS_ LPSPropValue lpsPropValue, \
+ LPSRow FAR * lppSRow, \
+ ULONG FAR * lpuliRow) IPURE; \
+ MAPIMETHOD(HrEnumRow) \
+ (THIS_ ULONG ulRowNumber, \
+ LPSRow FAR * lppSRow) IPURE; \
+ MAPIMETHOD(HrNotify) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cValues, \
+ LPSPropValue lpSPropValue) IPURE; \
+ MAPIMETHOD(HrInsertRow) \
+ (THIS_ ULONG uliRow, \
+ LPSRow lpSRow) IPURE; \
+ MAPIMETHOD(HrModifyRows) \
+ (THIS_ ULONG ulFlags, \
+ LPSRowSet lpSRowSet) IPURE; \
+ MAPIMETHOD(HrDeleteRows) \
+ (THIS_ ULONG ulFlags, \
+ LPSRowSet lprowsetToDelete, \
+ ULONG FAR * cRowsDeleted) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE ITableData
+DECLARE_MAPI_INTERFACE_(ITableData, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_ITABLEDATA_METHODS(PURE)
+};
+
+
+/* Entry Point for in memory ITable */
+
+
+/* CreateTable()
+ * Creates the internal memory structures and object handle
+ * to bring a new table into existence.
+ *
+ * lpInterface
+ * Interface ID of the TableData object (IID_IMAPITableData)
+ *
+ * lpAllocateBuffer, lpAllocateMore, and lpFreeBuffer
+ * Function addresses are provided by the caller so that
+ * this DLL allocates/frees memory appropriately.
+ * lpvReserved
+ * Reserved. Should be NULL.
+ * ulTableType
+ * TBLTYPE_DYNAMIC, etc. Visible to the calling application
+ * as part of the GetStatus return data on its views
+ * ulPropTagIndexColumn
+ * Index column for use when changing the data
+ * lpSPropTagArrayColumns
+ * Column proptags for the minimum set of columns in the table
+ * lppTableData
+ * Address of the pointer which will receive the TableData object
+ */
+
+STDAPI_(SCODE)
+CreateTable( LPCIID lpInterface,
+ ALLOCATEBUFFER FAR * lpAllocateBuffer,
+ ALLOCATEMORE FAR * lpAllocateMore,
+ FREEBUFFER FAR * lpFreeBuffer,
+ LPVOID lpvReserved,
+ ULONG ulTableType,
+ ULONG ulPropTagIndexColumn,
+ LPSPropTagArray lpSPropTagArrayColumns,
+ LPTABLEDATA FAR * lppTableData );
+
+/* HrGetView()
+ * This function obtains a new view on the underlying data
+ * which supports the IMAPITable interface. All rows and columns
+ * of the underlying table data are initially visible
+ * lpSSortOrderSet
+ * if specified, results in the view being sorted
+ * lpfCallerRelease
+ * pointer to a routine to be called when the view is released, or
+ * NULL.
+ * ulCallerData
+ * arbitrary data the caller wants saved with this view and returned in
+ * the Release callback.
+ */
+
+/* HrModifyRows()
+ * Add or modify a set of rows in the table data
+ * ulFlags
+ * Must be zero
+ * lpSRowSet
+ * Each row in the row set contains all the properties for one row
+ * in the table. One of the properties must be the index column. Any
+ * row in the table with the same value for its index column is
+ * replaced, or if there is no current row with that value the
+ * row is added.
+ * Each row in LPSRowSet MUST have a unique Index column!
+ * If any views are open, the view is updated as well.
+ * The properties do not have to be in the same order as the
+ * columns in the current table
+ */
+
+/* HrModifyRow()
+ * Add or modify one row in the table
+ * lpSRow
+ * This row contains all the properties for one row in the table.
+ * One of the properties must be the index column. Any row in
+ * the table with the same value for its index column is
+ * replaced, or if there is no current row with that value the
+ * row is added
+ * If any views are open, the view is updated as well.
+ * The properties do not have to be in the same order as the
+ * columns in the current table
+ */
+
+/* HrDeleteRows()
+ * Delete a row in the table.
+ * ulFlags
+ * TAD_ALL_ROWS - Causes all rows in the table to be deleted
+ * lpSRowSet is ignored in this case.
+ * lpSRowSet
+ * Each row in the row set contains all the properties for one row
+ * in the table. One of the properties must be the index column. Any
+ * row in the table with the same value for its index column is
+ * deleted.
+ * Each row in LPSRowSet MUST have a unique Index column!
+ * If any views are open, the view is updated as well.
+ * The properties do not have to be in the same order as the
+ * columns in the current table
+ */
+#define TAD_ALL_ROWS 1
+
+/* HrDeleteRow()
+ * Delete a row in the table.
+ * lpSPropValue
+ * This property value specifies the row which has this value
+ * for its index column
+ */
+
+/* HrQueryRow()
+ * Returns the values of a specified row in the table
+ * lpSPropValue
+ * This property value specifies the row which has this value
+ * for its index column
+ * lppSRow
+ * Address of where to return a pointer to an SRow
+ * lpuliRow
+ * Address of where to return the row number. This can be NULL
+ * if the row number is not required.
+ *
+ */
+
+/* HrEnumRow()
+ * Returns the values of a specific (numbered) row in the table
+ * ulRowNumber
+ * Indicates row number 0 to n-1
+ * lppSRow
+ * Address of where to return a pointer to a SRow
+ */
+
+/* HrInsertRow()
+ * Inserts a row into the table.
+ * uliRow
+ * The row number before which this row will be inserted into the table.
+ * Row numbers can be from 0 to n where o to n-1 result in row insertion
+ * a row number of n results in the row being appended to the table.
+ * lpSRow
+ * This row contains all the properties for one row in the table.
+ * One of the properties must be the index column. Any row in
+ * the table with the same value for its index column is
+ * replaced, or if there is no current row with that value the
+ * row is added
+ * If any views are open, the view is updated as well.
+ * The properties do not have to be in the same order as the
+ * columns in the current table
+ */
+
+
+/* IMAPIProp in memory */
+
+/* IPropData Interface ---------------------------------------------------- */
+
+
+#define MAPI_IPROPDATA_METHODS(IPURE) \
+ MAPIMETHOD(HrSetObjAccess) \
+ (THIS_ ULONG ulAccess) IPURE; \
+ MAPIMETHOD(HrSetPropAccess) \
+ (THIS_ LPSPropTagArray lpPropTagArray, \
+ ULONG FAR * rgulAccess) IPURE; \
+ MAPIMETHOD(HrGetPropAccess) \
+ (THIS_ LPSPropTagArray FAR * lppPropTagArray, \
+ ULONG FAR * FAR * lprgulAccess) IPURE; \
+ MAPIMETHOD(HrAddObjProps) \
+ (THIS_ LPSPropTagArray lppPropTagArray, \
+ LPSPropProblemArray FAR * lprgulAccess) IPURE;
+
+
+#undef INTERFACE
+#define INTERFACE IPropData
+DECLARE_MAPI_INTERFACE_(IPropData, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IPROPDATA_METHODS(PURE)
+};
+
+DECLARE_MAPI_INTERFACE_PTR(IPropData, LPPROPDATA);
+
+
+/* Entry Point for in memory IMAPIProp */
+
+
+/* CreateIProp()
+ * Creates the internal memory structures and object handle
+ * to bring a new property interface into existence.
+ *
+ * lpInterface
+ * Interface ID of the TableData object (IID_IMAPIPropData)
+ *
+ * lpAllocateBuffer, lpAllocateMore, and lpFreeBuffer
+ * Function addresses are provided by the caller so that
+ * this DLL allocates/frees memory appropriately.
+ * lppPropData
+ * Address of the pointer which will receive the IPropData object
+ * lpvReserved
+ * Reserved. Should be NULL.
+ */
+
+STDAPI_(SCODE)
+CreateIProp( LPCIID lpInterface,
+ ALLOCATEBUFFER FAR * lpAllocateBuffer,
+ ALLOCATEMORE FAR * lpAllocateMore,
+ FREEBUFFER FAR * lpFreeBuffer,
+ LPVOID lpvReserved,
+ LPPROPDATA FAR * lppPropData );
+
+/*
+ * Defines for prop/obj access
+ */
+#define IPROP_READONLY ((ULONG) 0x00000001)
+#define IPROP_READWRITE ((ULONG) 0x00000002)
+#define IPROP_CLEAN ((ULONG) 0x00010000)
+#define IPROP_DIRTY ((ULONG) 0x00020000)
+
+/*
+ - HrSetPropAccess
+ -
+ * Sets access right attributes on a per-property basis. By default,
+ * all properties are read/write.
+ *
+ */
+
+/*
+ - HrSetObjAccess
+ -
+ * Sets access rights for the object itself. By default, the object has
+ * read/write access.
+ *
+ */
+
+#ifndef NOIDLEENGINE
+
+/* Idle time scheduler */
+
+/*
+ * PRI
+ *
+ * Priority of an idle task.
+ * The idle engine sorts tasks by priority, and the one with the higher
+ * value runs first. Within a priority level, the functions are called
+ * round-robin.
+ */
+
+#define PRILOWEST -32768
+#define PRIHIGHEST 32767
+#define PRIUSER 0
+
+/*
+ * IRO
+ *
+ * Idle routine options. This is a combined bit mask consisting of
+ * individual firo's. Listed below are the possible bit flags.
+ *
+ * FIROWAIT and FIROINTERVAL are mutually exclusive.
+ * If neither of the flags are specified, the default action
+ * is to ignore the time parameter of the idle function and
+ * call it as often as possible if firoPerBlock is not set;
+ * otherwise call it one time only during the idle block
+ * once the time constraint has been set. FIROINTERVAL
+ * is also incompatible with FIROPERBLOCK.
+ *
+ * FIROWAIT - time given is minimum idle time before calling
+ * for the first time in the block of idle time,
+ * afterwhich call as often as possible.
+ * FIROINTERVAL - time given is minimum interval between each
+ * successive call
+ * FIROPERBLOCK - called only once per contiguous block of idle
+ * time
+ * FIRODISABLED - initially disabled when registered, the
+ * default is to enable the function when registered.
+ * FIROONCEONLY - called only one time by the scheduler and then
+ * deregistered automatically.
+ */
+
+#define IRONULL ((USHORT) 0x0000)
+#define FIROWAIT ((USHORT) 0x0001)
+#define FIROINTERVAL ((USHORT) 0x0002)
+#define FIROPERBLOCK ((USHORT) 0x0004)
+#define FIRODISABLED ((USHORT) 0x0020)
+#define FIROONCEONLY ((USHORT) 0x0040)
+
+/*
+ * IRC
+ *
+ * Idle routine change options. This is a combined bit mask consisting
+ * of individual firc's; each one identifies an aspect of the idle task
+ * that can be changed.
+ *
+ */
+
+#define IRCNULL ((USHORT) 0x0000)
+#define FIRCPFN ((USHORT) 0x0001) /* change function pointer */
+#define FIRCPV ((USHORT) 0x0002) /* change parameter block */
+#define FIRCPRI ((USHORT) 0x0004) /* change priority */
+#define FIRCCSEC ((USHORT) 0x0008) /* change time */
+#define FIRCIRO ((USHORT) 0x0010) /* change routine options */
+
+/*
+ * Type definition for idle functions. An idle function takes one
+ * parameter, an PV, and returns a BOOL value.
+ */
+
+typedef BOOL (STDAPICALLTYPE FNIDLE) (LPVOID);
+typedef FNIDLE FAR *PFNIDLE;
+
+/*
+ * FTG
+ *
+ * Function Tag. Used to identify a registered idle function.
+ *
+ */
+
+typedef void FAR *FTG;
+typedef FTG FAR *PFTG;
+#define FTGNULL ((FTG) NULL)
+
+/*
+ - MAPIInitIdle/MAPIDeinitIdle
+ -
+ * Purpose:
+ * Initialises the idle engine
+ * If the initialisation succeeded, returns 0, else returns -1
+ *
+ * Arguments:
+ * lpvReserved Reserved, must be NULL.
+ */
+
+STDAPI_(LONG)
+MAPIInitIdle (LPVOID lpvReserved);
+
+STDAPI_(VOID)
+MAPIDeinitIdle (VOID);
+
+
+/*
+ * FtgRegisterIdleRoutine
+ *
+ * Registers the function pfn of type PFNIDLE, i.e., (BOOL (*)(LPVOID))
+ * as an idle function.
+ *
+ * The idle function will be called with the parameter pv by the
+ * idle engine. The function has initial priority priIdle,
+ * associated time csecIdle, and options iroIdle.
+ */
+
+STDAPI_(FTG)
+FtgRegisterIdleRoutine (PFNIDLE lpfnIdle, LPVOID lpvIdleParam,
+ short priIdle, ULONG csecIdle, USHORT iroIdle);
+
+/*
+ * DeregisterIdleRoutine
+ *
+ * Removes the given routine from the list of idle routines.
+ * The routine will not be called again. It is the responsibility
+ * of the caller to clean up any data structures pointed to by the
+ * pvIdleParam parameter; this routine does not free the block.
+ */
+
+STDAPI_(void)
+DeregisterIdleRoutine (FTG ftg);
+
+/*
+ * EnableIdleRoutine
+ *
+ * Enables or disables an idle routine.
+ */
+
+STDAPI_(void)
+EnableIdleRoutine (FTG ftg, BOOL fEnable);
+
+/*
+ * ChangeIdleRoutine
+ *
+ * Changes some or all of the characteristics of the given idle
+ * function. The changes to make are indicated with flags in the
+ * ircIdle parameter.
+ */
+
+STDAPI_(void)
+ChangeIdleRoutine (FTG ftg, PFNIDLE lpfnIdle, LPVOID lpvIdleParam,
+ short priIdle, ULONG csecIdle, USHORT iroIdle, USHORT ircIdle);
+
+
+#endif /* ! NOIDLEENGINE */
+
+
+/* IMalloc Utilities */
+
+STDAPI_(LPMALLOC) MAPIGetDefaultMalloc(VOID);
+
+
+/* StreamOnFile (SOF) */
+
+/*
+ * Methods and #define's for implementing an OLE 2.0 storage stream
+ * (as defined in the OLE 2.0 specs) on top of a system file.
+ */
+
+#define SOF_UNIQUEFILENAME ((ULONG) 0x80000000)
+
+STDMETHODIMP OpenStreamOnFile(
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer,
+ ULONG ulFlags,
+ __in LPCTSTR lpszFileName,
+ __in_opt LPCTSTR lpszPrefix,
+ LPSTREAM FAR * lppStream);
+
+typedef HRESULT (STDMETHODCALLTYPE FAR * LPOPENSTREAMONFILE) (
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer,
+ ULONG ulFlags,
+ __in LPCTSTR lpszFileName,
+ __in_opt LPCTSTR lpszPrefix,
+ LPSTREAM FAR * lppStream);
+
+#if defined(_WIN64) || defined(_WIN32)
+#define OPENSTREAMONFILE "OpenStreamOnFile"
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+
+/* Property interface utilities */
+
+/*
+ * Copies a single SPropValue from Src to Dest. Handles all the various
+ * types of properties and will link its allocations given the master
+ * allocation object and an allocate more function.
+ */
+STDAPI_(SCODE)
+PropCopyMore( LPSPropValue lpSPropValueDest,
+ LPSPropValue lpSPropValueSrc,
+ ALLOCATEMORE * lpfAllocMore,
+ LPVOID lpvObject );
+
+/*
+ * Returns the size in bytes of structure at lpSPropValue, including the
+ * Value.
+ */
+STDAPI_(ULONG)
+UlPropSize( LPSPropValue lpSPropValue );
+
+
+STDAPI_(BOOL)
+FEqualNames( LPMAPINAMEID lpName1, LPMAPINAMEID lpName2 );
+
+#if (defined(_WIN64) || defined(_WIN32)) && !defined(_WINNT) && !defined(_WIN95) && !defined(_MAC)
+#define _WINNT
+#endif
+
+STDAPI_(void)
+GetInstance(LPSPropValue lpPropMv, LPSPropValue lpPropSv, ULONG uliInst);
+
+extern char rgchCsds[];
+extern char rgchCids[];
+extern char rgchCsdi[];
+extern char rgchCidi[];
+
+STDAPI_(BOOL)
+FPropContainsProp( LPSPropValue lpSPropValueDst,
+ LPSPropValue lpSPropValueSrc,
+ ULONG ulFuzzyLevel );
+
+STDAPI_(BOOL)
+FPropCompareProp( LPSPropValue lpSPropValue1,
+ ULONG ulRelOp,
+ LPSPropValue lpSPropValue2 );
+
+STDAPI_(LONG)
+LPropCompareProp( LPSPropValue lpSPropValueA,
+ LPSPropValue lpSPropValueB );
+
+STDAPI_(HRESULT)
+HrAddColumns( LPMAPITABLE lptbl,
+ LPSPropTagArray lpproptagColumnsNew,
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer);
+
+STDAPI_(HRESULT)
+HrAddColumnsEx( LPMAPITABLE lptbl,
+ LPSPropTagArray lpproptagColumnsNew,
+ LPALLOCATEBUFFER lpAllocateBuffer,
+ LPFREEBUFFER lpFreeBuffer,
+ void (FAR *lpfnFilterColumns)(LPSPropTagArray ptaga));
+
+
+/* Notification utilities */
+
+/*
+ * Function that creates an advise sink object given a notification
+ * callback function and context.
+ */
+
+STDAPI
+HrAllocAdviseSink( LPNOTIFCALLBACK lpfnCallback,
+ LPVOID lpvContext,
+ LPMAPIADVISESINK FAR *lppAdviseSink );
+
+
+/*
+ * Wraps an existing advise sink with another one which guarantees
+ * that the original advise sink will be called in the thread on
+ * which it was created.
+ */
+
+STDAPI
+HrThisThreadAdviseSink( LPMAPIADVISESINK lpAdviseSink,
+ LPMAPIADVISESINK FAR *lppAdviseSink);
+
+
+
+/*
+ * Allows a client and/or provider to force notifications
+ * which are currently queued in the MAPI notification engine
+ * to be dispatched without doing a message dispatch.
+ */
+
+STDAPI HrDispatchNotifications (ULONG ulFlags);
+
+
+/* Service Provider Utilities */
+
+/*
+ * Structures and utility function for building a display table
+ * from resources.
+ */
+
+typedef struct {
+ ULONG ulCtlType; /* DTCT_LABEL, etc. */
+ ULONG ulCtlFlags; /* DT_REQUIRED, etc. */
+ LPBYTE lpbNotif; /* pointer to notification data */
+ ULONG cbNotif; /* count of bytes of notification data */
+ LPTSTR lpszFilter; /* character filter for edit/combobox */
+ ULONG ulItemID; /* to validate parallel dlg template entry */
+ union { /* ulCtlType discriminates */
+ LPVOID lpv; /* Initialize this to avoid warnings */
+ LPDTBLLABEL lplabel;
+ LPDTBLEDIT lpedit;
+ LPDTBLLBX lplbx;
+ LPDTBLCOMBOBOX lpcombobox;
+ LPDTBLDDLBX lpddlbx;
+ LPDTBLCHECKBOX lpcheckbox;
+ LPDTBLGROUPBOX lpgroupbox;
+ LPDTBLBUTTON lpbutton;
+ LPDTBLRADIOBUTTON lpradiobutton;
+ LPDTBLMVLISTBOX lpmvlbx;
+ LPDTBLMVDDLBX lpmvddlbx;
+ LPDTBLPAGE lppage;
+ } ctl;
+} DTCTL, FAR *LPDTCTL;
+
+typedef struct {
+ ULONG cctl;
+ LPTSTR lpszResourceName; /* as usual, may be an integer ID */
+ union { /* as usual, may be an integer ID */
+ LPTSTR lpszComponent;
+ ULONG ulItemID;
+ };
+ LPDTCTL lpctl;
+} DTPAGE, FAR *LPDTPAGE;
+
+
+
+STDAPI
+BuildDisplayTable( LPALLOCATEBUFFER lpAllocateBuffer,
+ LPALLOCATEMORE lpAllocateMore,
+ LPFREEBUFFER lpFreeBuffer,
+ LPMALLOC lpMalloc,
+ HINSTANCE hInstance,
+ UINT cPages,
+ LPDTPAGE lpPage,
+ ULONG ulFlags,
+ LPMAPITABLE * lppTable,
+ LPTABLEDATA * lppTblData );
+
+
+/* MAPI structure validation/copy utilities */
+
+/*
+ * Validate, copy, and adjust pointers in MAPI structures:
+ * notification
+ * property value array
+ * option data
+ */
+
+STDAPI_(SCODE)
+ScCountNotifications(int cNotifications, LPNOTIFICATION lpNotifications,
+ ULONG FAR *lpcb);
+
+STDAPI_(SCODE)
+ScCopyNotifications(int cNotification, LPNOTIFICATION lpNotifications,
+ LPVOID lpvDst, ULONG FAR *lpcb);
+
+STDAPI_(SCODE)
+ScRelocNotifications(int cNotification, LPNOTIFICATION lpNotifications,
+ LPVOID lpvBaseOld, LPVOID lpvBaseNew, ULONG FAR *lpcb);
+
+
+STDAPI_(SCODE)
+ScCountProps(int cValues, LPSPropValue lpPropArray, ULONG FAR *lpcb);
+
+STDAPI_(LPSPropValue)
+LpValFindProp(ULONG ulPropTag, ULONG cValues, LPSPropValue lpPropArray);
+
+STDAPI_(SCODE)
+ScCopyProps(int cValues, LPSPropValue lpPropArray, LPVOID lpvDst,
+ ULONG FAR *lpcb);
+
+STDAPI_(SCODE)
+ScRelocProps(int cValues, LPSPropValue lpPropArray,
+ LPVOID lpvBaseOld, LPVOID lpvBaseNew, ULONG FAR *lpcb);
+
+STDAPI_(SCODE)
+ScDupPropset(int cValues, LPSPropValue lpPropArray,
+ LPALLOCATEBUFFER lpAllocateBuffer, LPSPropValue FAR *lppPropArray);
+
+
+/* General utility functions */
+
+/* Related to the OLE Component object model */
+
+STDAPI_(ULONG) UlAddRef(LPVOID lpunk);
+STDAPI_(ULONG) UlRelease(LPVOID lpunk);
+
+/* Related to the MAPI interface */
+
+STDAPI HrGetOneProp(LPMAPIPROP lpMapiProp, ULONG ulPropTag,
+ LPSPropValue FAR *lppProp);
+STDAPI HrSetOneProp(LPMAPIPROP lpMapiProp,
+ LPSPropValue lpProp);
+STDAPI_(BOOL) FPropExists(LPMAPIPROP lpMapiProp, ULONG ulPropTag);
+STDAPI_(LPSPropValue) PpropFindProp(LPSPropValue lpPropArray, ULONG cValues,
+ ULONG ulPropTag);
+STDAPI_(void) FreePadrlist(LPADRLIST lpAdrlist);
+STDAPI_(void) FreeProws(LPSRowSet lpRows);
+STDAPI HrQueryAllRows(LPMAPITABLE lpTable,
+ LPSPropTagArray lpPropTags,
+ LPSRestriction lpRestriction,
+ LPSSortOrderSet lpSortOrderSet,
+ LONG crowsMax,
+ LPSRowSet FAR *lppRows);
+
+/* Create or validate the IPM folder tree in a message store */
+
+#define MAPI_FORCE_CREATE 1
+#define MAPI_FULL_IPM_TREE 2
+
+STDAPI HrValidateIPMSubtree(LPMDB lpMDB, ULONG ulFlags,
+ ULONG FAR *lpcValues, LPSPropValue FAR *lppValues,
+ LPMAPIERROR FAR *lpperr);
+
+/* Encoding and decoding strings */
+
+STDAPI_(BOOL) FBinFromHex(__in LPTSTR lpsz, LPBYTE lpb);
+STDAPI_(SCODE) ScBinFromHexBounded(__in LPTSTR lpsz, LPBYTE lpb, ULONG cb);
+STDAPI_(void) HexFromBin(LPBYTE lpb, int cb, __in LPTSTR lpsz);
+STDAPI_(ULONG) UlFromSzHex(LPCTSTR lpsz);
+
+/* Encoding and decoding entry IDs */
+STDAPI HrEntryIDFromSz(__in LPTSTR lpsz, ULONG FAR *lpcb,
+ LPENTRYID FAR *lppEntryID);
+STDAPI HrSzFromEntryID(ULONG cb, LPENTRYID lpEntryID,
+ __in LPTSTR FAR *lpsz);
+STDAPI HrComposeEID(LPMAPISESSION lpSession,
+ ULONG cbStoreRecordKey, LPBYTE lpStoreRecordKey,
+ ULONG cbMsgEntryID, LPENTRYID lpMsgEntryID,
+ ULONG FAR *lpcbEID, LPENTRYID FAR *lppEntryID);
+STDAPI HrDecomposeEID(LPMAPISESSION lpSession,
+ ULONG cbEntryID, LPENTRYID lpEntryID,
+ ULONG FAR *lpcbStoreEntryID,
+ LPENTRYID FAR *lppStoreEntryID,
+ ULONG FAR *lpcbMsgEntryID,
+ LPENTRYID FAR *lppMsgEntryID);
+STDAPI HrComposeMsgID(LPMAPISESSION lpSession,
+ ULONG cbStoreSearchKey, LPBYTE pStoreSearchKey,
+ ULONG cbMsgEntryID, LPENTRYID lpMsgEntryID,
+ __in LPTSTR FAR *lpszMsgID);
+STDAPI HrDecomposeMsgID(LPMAPISESSION lpSession,
+ __in LPTSTR lpszMsgID,
+ ULONG FAR *lpcbStoreEntryID,
+ LPENTRYID FAR *lppStoreEntryID,
+ ULONG FAR *lppcbMsgEntryID,
+ LPENTRYID FAR *lppMsgEntryID);
+
+/* C runtime substitutes */
+
+
+STDAPI_(LPTSTR) SzFindCh(LPCTSTR lpsz, USHORT ch); /* strchr */
+STDAPI_(LPTSTR) SzFindLastCh(LPCTSTR lpsz, USHORT ch); /* strrchr */
+STDAPI_(LPTSTR) SzFindSz(LPCTSTR lpsz, LPCTSTR lpszKey); /*strstr */
+STDAPI_(unsigned int) UFromSz(LPCTSTR lpsz); /* atoi */
+
+STDAPI_(SCODE) ScUNCFromLocalPath(__in LPSTR lpszLocal, __in LPSTR lpszUNC,
+ UINT cchUNC);
+STDAPI_(SCODE) ScLocalPathFromUNC(__in LPSTR lpszUNC, __in LPSTR lpszLocal,
+ UINT cchLocal);
+
+/* 64-bit arithmetic with times */
+
+STDAPI_(FILETIME) FtAddFt(FILETIME ftAddend1, FILETIME ftAddend2);
+STDAPI_(FILETIME) FtMulDwDw(DWORD ftMultiplicand, DWORD ftMultiplier);
+STDAPI_(FILETIME) FtMulDw(DWORD ftMultiplier, FILETIME ftMultiplicand);
+STDAPI_(FILETIME) FtSubFt(FILETIME ftMinuend, FILETIME ftSubtrahend);
+STDAPI_(FILETIME) FtNegFt(FILETIME ft);
+
+/* Message composition */
+
+STDAPI_(SCODE) ScCreateConversationIndex (ULONG cbParent,
+ LPBYTE lpbParent,
+ ULONG FAR * lpcbConvIndex,
+ LPBYTE FAR * lppbConvIndex);
+
+/* Store support */
+
+STDAPI WrapStoreEntryID (ULONG ulFlags, __in LPTSTR lpszDLLName, ULONG cbOrigEntry,
+ LPENTRYID lpOrigEntry, ULONG *lpcbWrappedEntry, LPENTRYID *lppWrappedEntry);
+
+/* RTF Sync Utilities */
+
+#define RTF_SYNC_RTF_CHANGED ((ULONG) 0x00000001)
+#define RTF_SYNC_BODY_CHANGED ((ULONG) 0x00000002)
+
+STDAPI_(HRESULT)
+RTFSync (LPMESSAGE lpMessage, ULONG ulFlags, __out BOOL FAR * lpfMessageUpdated);
+
+
+/* Flags for WrapCompressedRTFStream() */
+
+/****** MAPI_MODIFY ((ULONG) 0x00000001) mapidefs.h */
+/****** STORE_UNCOMPRESSED_RTF ((ULONG) 0x00008000) mapidefs.h */
+
+STDAPI_(HRESULT)
+WrapCompressedRTFStream (__in LPSTREAM lpCompressedRTFStream,
+ ULONG ulFlags, __out LPSTREAM FAR * lpUncompressedRTFStream);
+
+/* Storage on Stream */
+
+#if defined(_WIN64) || defined(_WIN32) || defined(WIN16)
+STDAPI_(HRESULT)
+HrIStorageFromStream (LPUNKNOWN lpUnkIn,
+ LPCIID lpInterface, ULONG ulFlags, LPSTORAGE FAR * lppStorageOut);
+#endif
+
+
+/*
+ * Setup and cleanup.
+ *
+ * Providers never need to make these calls.
+ *
+ * Test applications and the like which do not call MAPIInitialize
+ * may want to call them, so that the few utility functions which
+ * need MAPI allocators (and do not ask for them explicitly)
+ * will work.
+ */
+
+/* All flags are reserved for ScInitMapiUtil. */
+
+STDAPI_(SCODE) ScInitMapiUtil(ULONG ulFlags);
+STDAPI_(VOID) DeinitMapiUtil(VOID);
+
+
+/*
+ * Entry point names.
+ *
+ * These are for new entry points defined since MAPI first shipped
+ * in Windows 95. Using these names in a GetProcAddress call makes
+ * it easier to write code which uses them optionally.
+ */
+
+#if defined (_WIN64) && defined(_AMD64_)
+#define szHrDispatchNotifications "HrDispatchNotifications"
+#elif defined (_WIN32) && defined (_X86_)
+#define szHrDispatchNotifications "_HrDispatchNotifications@4"
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32/X86 and Win64/AMD64"
+#endif
+
+typedef HRESULT (STDAPICALLTYPE DISPATCHNOTIFICATIONS)(ULONG ulFlags);
+typedef DISPATCHNOTIFICATIONS FAR * LPDISPATCHNOTIFICATIONS;
+
+#if defined (_WIN64) && defined (_AMD64_)
+#define szScCreateConversationIndex "ScCreateConversationIndex"
+#elif defined (_WIN32) && defined (_X86_)
+#define szScCreateConversationIndex "_ScCreateConversationIndex@16"
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32/X86 and Win64/AMD64"
+#endif
+
+typedef SCODE (STDAPICALLTYPE CREATECONVERSATIONINDEX)(ULONG cbParent,
+ LPBYTE lpbParent, ULONG FAR *lpcbConvIndex, LPBYTE FAR *lppbConvIndex);
+typedef CREATECONVERSATIONINDEX FAR *LPCREATECONVERSATIONINDEX;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _MAPIUTIL_H_ */
diff --git a/comm/mailnews/mapi/include/mapival.h b/comm/mailnews/mapi/include/mapival.h
new file mode 100644
index 0000000000..0e034100d5
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapival.h
@@ -0,0 +1,1975 @@
+/*
+ * M A P I V A L . H
+ *
+ * Macros used to validate parameters on standard MAPI object methods.
+ * Used in conjunction with routines found in MAPIU.DLL.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _INC_VALIDATE
+#define _INC_VALIDATE
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef MAPIUTIL_H
+#include <mapiutil.h>
+#endif
+#include <stddef.h>
+#include <stdarg.h>
+
+
+#define MAKE_ENUM(Method, Interface) Interface##_##Method
+
+typedef enum _tagMethods
+{
+/* IUnknown */
+ MAKE_ENUM(QueryInterface, IUnknown) = 0,
+ MAKE_ENUM(AddRef, IUnknown), /* For completness */
+ MAKE_ENUM(Release, IUnknown), /* For completness */
+
+/* IMAPIProps */
+ MAKE_ENUM(GetLastError, IMAPIProp),
+ MAKE_ENUM(SaveChanges, IMAPIProp),
+ MAKE_ENUM(GetProps, IMAPIProp),
+ MAKE_ENUM(GetPropList, IMAPIProp),
+ MAKE_ENUM(OpenProperty, IMAPIProp),
+ MAKE_ENUM(SetProps, IMAPIProp),
+ MAKE_ENUM(DeleteProps, IMAPIProp),
+ MAKE_ENUM(CopyTo, IMAPIProp),
+ MAKE_ENUM(CopyProps, IMAPIProp),
+ MAKE_ENUM(GetNamesFromIDs, IMAPIProp),
+ MAKE_ENUM(GetIDsFromNames, IMAPIProp),
+
+/* IMAPITable */
+ MAKE_ENUM(GetLastError, IMAPITable),
+ MAKE_ENUM(Advise, IMAPITable),
+ MAKE_ENUM(Unadvise, IMAPITable),
+ MAKE_ENUM(GetStatus, IMAPITable),
+ MAKE_ENUM(SetColumns, IMAPITable),
+ MAKE_ENUM(QueryColumns, IMAPITable),
+ MAKE_ENUM(GetRowCount, IMAPITable),
+ MAKE_ENUM(SeekRow, IMAPITable),
+ MAKE_ENUM(SeekRowApprox, IMAPITable),
+ MAKE_ENUM(QueryPosition, IMAPITable),
+ MAKE_ENUM(FindRow, IMAPITable),
+ MAKE_ENUM(Restrict, IMAPITable),
+ MAKE_ENUM(CreateBookmark, IMAPITable),
+ MAKE_ENUM(FreeBookmark, IMAPITable),
+ MAKE_ENUM(SortTable, IMAPITable),
+ MAKE_ENUM(QuerySortOrder, IMAPITable),
+ MAKE_ENUM(QueryRows, IMAPITable),
+ MAKE_ENUM(Abort, IMAPITable),
+ MAKE_ENUM(ExpandRow, IMAPITable),
+ MAKE_ENUM(CollapseRow, IMAPITable),
+ MAKE_ENUM(WaitForCompletion, IMAPITable),
+ MAKE_ENUM(GetCollapseState, IMAPITable),
+ MAKE_ENUM(SetCollapseState, IMAPITable),
+
+/* IMAPIContainer */
+ MAKE_ENUM(GetContentsTable, IMAPIContainer),
+ MAKE_ENUM(GetHierarchyTable, IMAPIContainer),
+ MAKE_ENUM(OpenEntry, IMAPIContainer),
+ MAKE_ENUM(SetSearchCriteria, IMAPIContainer),
+ MAKE_ENUM(GetSearchCriteria, IMAPIContainer),
+
+/* IABContainer */
+ MAKE_ENUM(CreateEntry, IABContainer),
+ MAKE_ENUM(CopyEntries, IABContainer),
+ MAKE_ENUM(DeleteEntries, IABContainer),
+ MAKE_ENUM(ResolveNames, IABContainer),
+
+/* IDistList */
+ MAKE_ENUM(CreateEntry, IDistList),
+ MAKE_ENUM(CopyEntries, IDistList),
+ MAKE_ENUM(DeleteEntries, IDistList),
+ MAKE_ENUM(ResolveNames, IDistList),
+
+/* IMAPIFolder */
+ MAKE_ENUM(CreateMessage, IMAPIFolder),
+ MAKE_ENUM(CopyMessages, IMAPIFolder),
+ MAKE_ENUM(DeleteMessages, IMAPIFolder),
+ MAKE_ENUM(CreateFolder, IMAPIFolder),
+ MAKE_ENUM(CopyFolder, IMAPIFolder),
+ MAKE_ENUM(DeleteFolder, IMAPIFolder),
+ MAKE_ENUM(SetReadFlags, IMAPIFolder),
+ MAKE_ENUM(GetMessageStatus, IMAPIFolder),
+ MAKE_ENUM(SetMessageStatus, IMAPIFolder),
+ MAKE_ENUM(SaveContentsSort, IMAPIFolder),
+ MAKE_ENUM(EmptyFolder, IMAPIFolder),
+
+/* IMsgStore */
+ MAKE_ENUM(Advise, IMsgStore),
+ MAKE_ENUM(Unadvise, IMsgStore),
+ MAKE_ENUM(CompareEntryIDs, IMsgStore),
+ MAKE_ENUM(OpenEntry, IMsgStore),
+ MAKE_ENUM(SetReceiveFolder, IMsgStore),
+ MAKE_ENUM(GetReceiveFolder, IMsgStore),
+ MAKE_ENUM(GetReceiveFolderTable, IMsgStore),
+ MAKE_ENUM(StoreLogoff, IMsgStore),
+ MAKE_ENUM(AbortSubmit, IMsgStore),
+ MAKE_ENUM(GetOutgoingQueue, IMsgStore),
+ MAKE_ENUM(SetLockState, IMsgStore),
+ MAKE_ENUM(FinishedMsg, IMsgStore),
+ MAKE_ENUM(NotifyNewMail, IMsgStore),
+
+/* IMessage */
+ MAKE_ENUM(GetAttachmentTable, IMessage),
+ MAKE_ENUM(OpenAttach, IMessage),
+ MAKE_ENUM(CreateAttach, IMessage),
+ MAKE_ENUM(DeleteAttach, IMessage),
+ MAKE_ENUM(GetRecipientTable, IMessage),
+ MAKE_ENUM(ModifyRecipients, IMessage),
+ MAKE_ENUM(SubmitMessage, IMessage),
+ MAKE_ENUM(SetReadFlag, IMessage),
+
+
+/* IABProvider */
+ MAKE_ENUM(Shutdown, IABProvider),
+ MAKE_ENUM(Logon, IABProvider),
+
+/* IABLogon */
+ MAKE_ENUM(GetLastError, IABLogon),
+ MAKE_ENUM(Logoff, IABLogon),
+ MAKE_ENUM(OpenEntry, IABLogon),
+ MAKE_ENUM(CompareEntryIDs, IABLogon),
+ MAKE_ENUM(Advise, IABLogon),
+ MAKE_ENUM(Unadvise, IABLogon),
+ MAKE_ENUM(OpenStatusEntry, IABLogon),
+ MAKE_ENUM(OpenTemplateID, IABLogon),
+ MAKE_ENUM(GetOneOffTable, IABLogon),
+ MAKE_ENUM(PrepareRecips, IABLogon),
+
+/* IXPProvider */
+ MAKE_ENUM(Shutdown, IXPProvider),
+ MAKE_ENUM(TransportLogon, IXPProvider),
+
+/* IXPLogon */
+ MAKE_ENUM(AddressTypes, IXPLogon),
+ MAKE_ENUM(RegisterOptions, IXPLogon),
+ MAKE_ENUM(TransportNotify, IXPLogon),
+ MAKE_ENUM(Idle, IXPLogon),
+ MAKE_ENUM(TransportLogoff, IXPLogon),
+ MAKE_ENUM(SubmitMessage, IXPLogon),
+ MAKE_ENUM(EndMessage, IXPLogon),
+ MAKE_ENUM(Poll, IXPLogon),
+ MAKE_ENUM(StartMessage, IXPLogon),
+ MAKE_ENUM(OpenStatusEntry, IXPLogon),
+ MAKE_ENUM(ValidateState, IXPLogon),
+ MAKE_ENUM(FlushQueues, IXPLogon),
+
+/* IMSProvider */
+ MAKE_ENUM(Shutdown, IMSProvider),
+ MAKE_ENUM(Logon, IMSProvider),
+ MAKE_ENUM(SpoolerLogon, IMSProvider),
+ MAKE_ENUM(CompareStoreIDs, IMSProvider),
+
+/* IMSLogon */
+ MAKE_ENUM(GetLastError, IMSLogon),
+ MAKE_ENUM(Logoff, IMSLogon),
+ MAKE_ENUM(OpenEntry, IMSLogon),
+ MAKE_ENUM(CompareEntryIDs, IMSLogon),
+ MAKE_ENUM(Advise, IMSLogon),
+ MAKE_ENUM(Unadvise, IMSLogon),
+ MAKE_ENUM(OpenStatusEntry, IMSLogon),
+
+/* IMAPIControl */
+ MAKE_ENUM(GetLastError, IMAPIControl),
+ MAKE_ENUM(Activate, IMAPIControl),
+ MAKE_ENUM(GetState, IMAPIControl),
+
+/* IMAPIStatus */
+ MAKE_ENUM(ValidateState, IMAPIStatus),
+ MAKE_ENUM(SettingsDialog, IMAPIStatus),
+ MAKE_ENUM(ChangePassword, IMAPIStatus),
+ MAKE_ENUM(FlushQueues, IMAPIStatus),
+
+/* IStream */
+ MAKE_ENUM(_Read, IStream),
+ MAKE_ENUM(_Write, IStream),
+ MAKE_ENUM(Seek, IStream),
+ MAKE_ENUM(SetSize, IStream),
+ MAKE_ENUM(CopyTo, IStream),
+ MAKE_ENUM(Commit, IStream),
+ MAKE_ENUM(Revert, IStream),
+ MAKE_ENUM(LockRegion, IStream),
+ MAKE_ENUM(UnlockRegion, IStream),
+ MAKE_ENUM(Stat, IStream),
+ MAKE_ENUM(Clone, IStream),
+
+/* IMAPIAdviseSink */
+ MAKE_ENUM(OnNotify, IMAPIAdviseSink),
+
+} METHODS;
+
+
+/* Macro wrappers to hide the Validate function return handling */
+#if defined(_AMD64_) || defined(_X86_)
+#ifdef __cplusplus
+
+/* C++ methods can't take the address of the This pointer, so we must
+ use the first parameter instead */
+
+#define ValidateParameters(eMethod, First) \
+ { HRESULT _hr_; \
+ _hr_ = __CPPValidateParameters(eMethod, (LPVOID) &First); \
+ if (HR_FAILED(_hr_)) return (_hr_); }
+
+#define UlValidateParameters(eMethod, First) \
+ { HRESULT _hr_; \
+ _hr_ = __CPPValidateParameters(eMethod, &First); \
+ if (HR_FAILED(_hr_)) return (ULONG) (_hr_); }
+
+/* Methods called by MAPI should have correct parameters
+ - just assert in Debug to check */
+#define CheckParameters(eMethod, First) \
+ AssertSz(HR_SUCCEEDED(__CPPValidateParameters(eMethod, &First)), "Parameter validation failed for method called by MAPI!")
+
+
+#else /* __cplusplus */
+
+/* For methods that will be called by clients
+ - validate always */
+
+#define ValidateParameters(eMethod, ppThis) \
+ { HRESULT _hr_; \
+ _hr_ = __ValidateParameters(eMethod, ppThis); \
+ if (HR_FAILED(_hr_)) return (_hr_); }
+
+#define UlValidateParameters(eMethod, ppThis) \
+ { HRESULT _hr_; \
+ _hr_ = __ValidateParameters(eMethod, ppThis); \
+ if (HR_FAILED(_hr_)) return (ULONG) (_hr_); }
+
+/* Methods called by MAPI should have correct parameters
+ - just assert in Debug to check */
+#define CheckParameters(eMethod, ppThis) \
+ AssertSz(HR_SUCCEEDED(__ValidateParameters(eMethod, ppThis)), "Parameter validation failed for method called by MAPI!")
+
+#endif /* __cplusplus */
+#endif /* _X86_ || _AMD64_ */
+
+/* Prototypes for functions used to validate complex parameters.
+ */
+#define FBadPropVal( lpPropVal) (FAILED(ScCountProps( 1, lpPropVal, NULL)))
+
+#define FBadRgPropVal( lpPropVal, cValues) \
+ (FAILED(ScCountProps( cValues, lpPropVal, NULL)))
+
+#define FBadAdrList( lpAdrList) \
+ ( AssertSz( ( offsetof( ADRLIST, cEntries) \
+ == offsetof( SRowSet, cRows)) \
+ && ( offsetof( ADRLIST, aEntries) \
+ == offsetof( SRowSet, aRow)) \
+ && ( offsetof( ADRENTRY, cValues) \
+ == offsetof( SRow, cValues)) \
+ && ( offsetof( ADRENTRY, rgPropVals) \
+ == offsetof( SRow, lpProps)) \
+ , "ADRLIST doesn't match SRowSet") \
+ || FBadRowSet( (LPSRowSet) lpAdrList))
+
+STDAPI_(BOOL)
+FBadRglpszW(__in LPWSTR FAR *lppszW,
+ ULONG cStrings);
+
+STDAPI_(BOOL)
+FBadRowSet( LPSRowSet lpRowSet);
+
+STDAPI_(BOOL)
+FBadRglpNameID( LPMAPINAMEID FAR * lppNameId,
+ ULONG cNames);
+
+STDAPI_(BOOL)
+FBadEntryList( LPENTRYLIST lpEntryList);
+
+
+/* BAD_STANDARD_OBJ
+ *
+ * This macro insures that the object is a writable object of the correct size
+ * and that this method belongs to the object.
+ *
+ * NOTES ON USE!
+ * This depends upon using the standard method of declaring the object
+ * interface.
+ *
+ * prefix is the method prefix you chose when declaring the object interface.
+ * method is the standard method name of the calling method.
+ * lpVtbl is the name of the lpVtbl element of your object.
+ */
+#define BAD_STANDARD_OBJ( lpObj, prefix, method, lpVtbl) \
+ ( IsBadWritePtr( (lpObj), sizeof(*lpObj)) \
+ || IsBadReadPtr( (void *) &(lpObj->lpVtbl->method), sizeof(LPVOID)) \
+ ||( ( LPVOID) (lpObj->lpVtbl->method) != (LPVOID) (prefix##method)))
+
+
+#define FBadUnknown( lpObj ) \
+ ( IsBadReadPtr( (lpObj), sizeof(LPVOID) ) \
+ || IsBadReadPtr( (lpObj)->lpVtbl, 3 * sizeof(LPUNKNOWN) ) \
+ || IsBadCodePtr( (FARPROC)(lpObj)->lpVtbl->QueryInterface ))
+
+/*
+ * IUnknown
+ */
+
+
+/*
+ * QueryInterface
+ */
+#define FBadQueryInterface( lpObj, riid, ppvObj) \
+ ( IsBadReadPtr( riid, sizeof(IID)) \
+ || IsBadWritePtr( ppvObj, sizeof(LPVOID)))
+
+
+/*
+ * AddRef
+ * No parameter validation required.
+ */
+#define FBadAddRef( lpObj) FALSE
+
+
+/*
+ * Release
+ * No parameter validation required.
+ */
+#define FBadRelease( lpObj) FALSE
+
+
+/*
+ * GetLastError
+ */
+#define FBadGetLastError( lpObj, hResult, ulFlags, lppMAPIError )\
+ (IsBadWritePtr( lppMAPIError, sizeof(LPMAPIERROR)))
+
+/*
+ * IMAPIProp
+ */
+
+
+/*
+ * SaveChanges
+ * No parameter validation required.
+ */
+#define FBadSaveChanges( lpObj, ulFlags) FALSE
+
+
+/*
+ * GetProps
+ */
+#define FBadGetProps( lpObj, lpPTagA, lpcValues, lppPropArray) \
+ ( ( lpPTagA \
+ && ( IsBadReadPtr( lpPTagA, sizeof(ULONG)) \
+ || IsBadReadPtr( lpPTagA, (UINT)( (lpPTagA->cValues + 1) \
+ * sizeof(ULONG))))) \
+ || IsBadWritePtr( lpcValues, sizeof(ULONG)) \
+ || IsBadWritePtr( lppPropArray, sizeof(LPSPropValue)))
+
+
+/*
+ * GetPropList
+ */
+#define FBadGetPropList( lpObj, lppPTagA) \
+ (IsBadWritePtr( lppPTagA, sizeof(LPSPropTagArray FAR *)))
+
+
+/*
+ * OpenProperty
+ */
+#define FBadOpenProperty( lpObj, ulPropTag, lpiid, ulInterfaceOptions, ulFlags \
+ , lppUnk) \
+ ( IsBadReadPtr( lpiid, sizeof(IID)) \
+ || IsBadWritePtr( lppUnk, sizeof (LPUNKNOWN FAR *)))
+
+
+/*
+ * SetProps
+ */
+#define FBadSetProps( lpObj, cValues, lpPropArray, lppProblems) \
+ ( FBadRgPropVal( lpPropArray, (UINT) cValues) \
+ || ( lppProblems \
+ && IsBadWritePtr( lppProblems, sizeof(LPSPropProblemArray))))
+
+
+/*
+ * DeleteProps
+ */
+#define FBadDeleteProps( lpObj, lpPTagA, lppProblems) \
+ ( ( !lpPTagA \
+ || ( IsBadReadPtr( lpPTagA, sizeof(ULONG)) \
+ || IsBadReadPtr( lpPTagA, (UINT)( (lpPTagA->cValues + 1) \
+ * sizeof(ULONG))))) \
+ || ( lppProblems \
+ && IsBadWritePtr( lppProblems, sizeof(LPSPropProblemArray))))
+
+
+/*
+ * CopyTo
+ */
+#define FBadCopyTo( lpIPDAT, ciidExclude, rgiidExclude, lpExcludeProps \
+ , ulUIParam, lpProgress, lpInterface, lpDestObj \
+ , ulFlags, lppProblems) \
+ ( ( ciidExclude \
+ && ( IsBadReadPtr( rgiidExclude, (UINT)(ciidExclude * sizeof(IID))))) \
+ || ( lpExcludeProps \
+ && ( IsBadReadPtr( lpExcludeProps, sizeof(ULONG)) \
+ || IsBadReadPtr( lpExcludeProps \
+ , (UINT)( (lpExcludeProps->cValues + 1) \
+ * sizeof(ULONG))))) \
+ || (lpProgress && FBadUnknown( lpProgress )) \
+ || (lpInterface && IsBadReadPtr( lpInterface, sizeof(IID))) \
+ || IsBadReadPtr( lpDestObj, sizeof(LPVOID)) \
+ || ( lppProblems \
+ && IsBadWritePtr( lppProblems, sizeof(LPSPropProblemArray))))
+
+
+/*
+ * CopyProps
+ */
+#define FBadCopyProps( lpIPDAT, lpPropTagArray \
+ , ulUIParam, lpProgress, lpInterface, lpDestObj \
+ , ulFlags, lppProblems) \
+ ( ( lpPropTagArray \
+ && ( IsBadReadPtr( lpPropTagArray, sizeof(ULONG)) \
+ || IsBadReadPtr( lpPropTagArray \
+ , (UINT)( (lpPropTagArray->cValues + 1) \
+ * sizeof(ULONG))))) \
+ || (lpProgress && FBadUnknown( lpProgress )) \
+ || (lpInterface && IsBadReadPtr( lpInterface, sizeof(IID))) \
+ || IsBadReadPtr( lpDestObj, sizeof(LPVOID)) \
+ || ( lppProblems \
+ && IsBadWritePtr( lppProblems, sizeof(LPSPropProblemArray))))
+
+
+
+/*
+ * GetNamesFromIDs
+ */
+#define FBadGetNamesFromIDs( lpIPDAT, lppPropTags, lpPropSetGuid, ulFlags, \
+ lpcPropNames, lpppPropNames) \
+ ( IsBadReadPtr( lppPropTags, sizeof(LPSPropTagArray)) \
+ || ( lpPropSetGuid && IsBadReadPtr( lpPropSetGuid, sizeof(GUID))) \
+ || ( *lppPropTags \
+ && ( IsBadReadPtr( *lppPropTags, sizeof(ULONG)) \
+ || IsBadReadPtr( *lppPropTags \
+ , (UINT)( ( ( *lppPropTags)->cValues + 1) \
+ * sizeof(ULONG))))) \
+ || IsBadWritePtr( lpcPropNames, sizeof (ULONG)) \
+ || IsBadWritePtr( lpppPropNames, sizeof (LPVOID FAR *)))
+
+
+
+/*
+ * GetNamesFromIDs
+ */
+#define FBadGetIDsFromNames( lpIPDAT, cPropNames, lppPropNames, ulFlags \
+ , lppPropTags) \
+ ( (cPropNames && FBadRglpNameID( lppPropNames, cPropNames)) \
+ || IsBadWritePtr( lppPropTags, sizeof(LPULONG FAR *)))
+
+
+STDAPI_(ULONG)
+FBadRestriction( LPSRestriction lpres );
+
+STDAPI_(ULONG)
+FBadPropTag( ULONG ulPropTag );
+
+STDAPI_(ULONG)
+FBadRow( LPSRow lprow );
+
+STDAPI_(ULONG)
+FBadProp( LPSPropValue lpprop );
+
+STDAPI_(ULONG)
+FBadSortOrderSet( LPSSortOrderSet lpsos );
+
+STDAPI_(ULONG)
+FBadColumnSet( LPSPropTagArray lpptaCols );
+
+/* Validation function
+
+ The eMethod parameter tells us which internal validation to perform.
+
+ The ppThis parameter tells us where the stack is, so we can access the other
+ parameters.
+
+ Becuase of this *magic* we MUST obtain the pointer to the This pointer in
+ the method function.
+
+*/
+
+#if defined(_WIN64) || defined(_WIN32)
+#define BASED_STACK
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+#if defined(_WIN64) || defined(_WIN32)
+HRESULT STDAPICALLTYPE
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+__CPPValidateParameters(METHODS eMethod, const LPVOID ppFirst);
+
+#if defined(_WIN64) || defined(_WIN32)
+HRESULT STDAPICALLTYPE
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+__ValidateParameters(METHODS eMethod, LPVOID ppThis);
+
+#ifdef _MAC
+#define STDAPIVCALLTYPE __cdecl
+#define STDAPIV EXTERN_C HRESULT STDAPIVCALLTYPE
+#define STDAPIV_(type) EXTERN_C type STDAPIVCALLTYPE
+#endif /* _MAC */
+
+/* Macro wrappers for platform independent validation */
+
+#if defined(_AMD64_) || defined(_X86_)
+
+#define ArgShiftN (1 + (sizeof(DWORD_PTR)>>2))
+#define ArgSize(T) ((sizeof(T)+sizeof(DWORD_PTR)-1)>>ArgShiftN)
+
+#define MakeArg1(idx, a1) memcpy(__rgArgs+idx, &a1, ArgSize(a1)*sizeof(DWORD_PTR))
+#define MakeArg2(idx, a1, a2) MakeArg1(idx, a1); MakeArg1(idx+ArgSize(a1), a2)
+#define MakeArg3(idx, a1, a2, a3) MakeArg1(idx, a1); MakeArg2(idx+ArgSize(a1), a2, a3)
+#define MakeArg4(idx, a1, a2, a3, a4) MakeArg1(idx, a1); MakeArg3(idx+ArgSize(a1), a2, a3, a4)
+#define MakeArg5(idx, a1, a2, a3, a4, a5) MakeArg1(idx, a1); MakeArg4(idx+ArgSize(a1), a2, a3, a4, a5)
+#define MakeArg6(idx, a1, a2, a3, a4, a5, a6) MakeArg1(idx, a1); MakeArg5(idx+ArgSize(a1), a2, a3, a4, a5, a6)
+#define MakeArg7(idx, a1, a2, a3, a4, a5, a6, a7) MakeArg1(idx, a1); MakeArg6(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7)
+#define MakeArg8(idx, a1, a2, a3, a4, a5, a6, a7, a8) MakeArg1(idx, a1); MakeArg7(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8)
+#define MakeArg9(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9) MakeArg1(idx, a1); MakeArg8(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9)
+#define MakeArg10(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) MakeArg1(idx, a1); MakeArg9(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10)
+#define MakeArg11(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) MakeArg1(idx, a1); MakeArg10(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)
+#define MakeArg12(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) MakeArg1(idx, a1); MakeArg11(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)
+#define MakeArg13(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) MakeArg1(idx, a1); MakeArg12(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)
+#define MakeArg14(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) MakeArg1(idx, a1); MakeArg13(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)
+#define MakeArg15(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) MakeArg1(idx, a1); MakeArg14(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
+#define MakeArg16(idx, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) MakeArg1(idx, a1); MakeArg15(idx+ArgSize(a1), a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)
+
+#define MakeArray1(a1) \
+ DWORD_PTR __rgArgs[ArgSize(a1)]; \
+ MakeArg1(0, a1)
+
+#define MakeArray2(a1, a2) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2)]; \
+ MakeArg2(0, a1, a2)
+
+#define MakeArray3(a1, a2, a3) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3)]; \
+ MakeArg3(0, a1, a2, a3)
+
+#define MakeArray4(a1, a2, a3, a4) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4)]; \
+ MakeArg4(0, a1, a2, a3, a4)
+
+#define MakeArray5(a1, a2, a3, a4, a5) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5)]; \
+ MakeArg5(0, a1, a2, a3, a4, a5)
+
+#define MakeArray6(a1, a2, a3, a4, a5, a6) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6)]; \
+ MakeArg6(0, a1, a2, a3, a4, a5, a6)
+
+#define MakeArray7(a1, a2, a3, a4, a5, a6, a7) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7)]; \
+ MakeArg7(0, a1, a2, a3, a4, a5, a6, a7)
+
+#define MakeArray8(a1, a2, a3, a4, a5, a6, a7, a8) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8)]; \
+ MakeArg8(0, a1, a2, a3, a4, a5, a6, a7, a8)
+
+#define MakeArray9(a1, a2, a3, a4, a5, a6, a7, a8, a9) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9)]; \
+ MakeArg9(0, a1, a2, a3, a4, a5, a6, a7, a8, a9)
+
+#define MakeArray10(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10)]; \
+ MakeArg10(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
+
+#define MakeArray11(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11)]; \
+ MakeArg11(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)
+
+#define MakeArray12(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11) + ArgSize(a12)]; \
+ MakeArg12(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)
+
+#define MakeArray13(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11) + ArgSize(a12) + ArgSize(a13)]; \
+ MakeArg13(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)
+
+#define MakeArray14(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11) + ArgSize(a12) + ArgSize(a13) + ArgSize(a14)]; \
+ MakeArg14(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)
+
+#define MakeArray15(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11) + ArgSize(a12) + ArgSize(a13) + ArgSize(a14) + ArgSize(a15)]; \
+ MakeArg15(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
+
+#define MakeArray16(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) \
+ DWORD_PTR __rgArgs[ArgSize(a1) + ArgSize(a2) + ArgSize(a3) + ArgSize(a4) + ArgSize(a5) + ArgSize(a6) + ArgSize(a7) + ArgSize(a8) + ArgSize(a9) + ArgSize(a10) + ArgSize(a11) + ArgSize(a12) + ArgSize(a13) + ArgSize(a14) + ArgSize(a15) + ArgSize(a16)]; \
+ MakeArg16(0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)
+
+
+
+#define ValidateParameters1( m, a1 )
+#define ValidateParameters2( m, a1, a2 ) \
+ { HRESULT _hr_; \
+ MakeArray1(a2); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters3( m, a1, a2, a3 ) \
+ { HRESULT _hr_; \
+ MakeArray2(a2, a3); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters4( m, a1, a2, a3, a4 ) \
+ { HRESULT _hr_; \
+ MakeArray3(a2, a3, a4); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters5( m, a1, a2, a3, a4, a5 ) \
+ { HRESULT _hr_; \
+ MakeArray4(a2, a3, a4, a5); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ { HRESULT _hr_; \
+ MakeArray5(a2, a3, a4, a5, a6); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ { HRESULT _hr_; \
+ MakeArray6(a2, a3, a4, a5, a6, a7); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ { HRESULT _hr_; \
+ MakeArray7(a2, a3, a4, a5, a6, a7, a8); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ { HRESULT _hr_; \
+ MakeArray8(a2, a3, a4, a5, a6, a7, a8, a9); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ { HRESULT _hr_; \
+ MakeArray9(a2, a3, a4, a5, a6, a7, a8, a9, a10); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ { HRESULT _hr_; \
+ MakeArray10(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ { HRESULT _hr_; \
+ MakeArray11(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ { HRESULT _hr_; \
+ MakeArray12(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ { HRESULT _hr_; \
+ MakeArray13(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ { HRESULT _hr_; \
+ MakeArray14(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+#define ValidateParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ { HRESULT _hr_; \
+ MakeArray15(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return (_hr_); }
+
+#define UlValidateParameters1( m, a1 )
+#define UlValidateParameters2( m, a1, a2 ) \
+ { HRESULT _hr_; \
+ MakeArray1(a2); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters3( m, a1, a2, a3 ) \
+ { HRESULT _hr_; \
+ MakeArray2(a2, a3); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters4( m, a1, a2, a3, a4 ) \
+ { HRESULT _hr_; \
+ MakeArray3(a2, a3, a4); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters5( m, a1, a2, a3, a4, a5 ) \
+ { HRESULT _hr_; \
+ MakeArray4(a2, a3, a4, a5); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ { HRESULT _hr_; \
+ MakeArray5(a2, a3, a4, a5, a6); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ { HRESULT _hr_; \
+ MakeArray6(a2, a3, a4, a5, a6, a7); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ { HRESULT _hr_; \
+ MakeArray7(a2, a3, a4, a5, a6, a7, a8); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ { HRESULT _hr_; \
+ MakeArray8(a2, a3, a4, a5, a6, a7, a8, a9); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ { HRESULT _hr_; \
+ MakeArray9(a2, a3, a4, a5, a6, a7, a8, a9, a10); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ { HRESULT _hr_; \
+ MakeArray10(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ { HRESULT _hr_; \
+ MakeArray11(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ { HRESULT _hr_; \
+ MakeArray12(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ { HRESULT _hr_; \
+ MakeArray13(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ { HRESULT _hr_; \
+ MakeArray14(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+#define UlValidateParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ { HRESULT _hr_; \
+ MakeArray15(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16); \
+ _hr_ = HrValidateParameters(m, (void **)__rgArgs); if (HR_FAILED(_hr_)) return ((ULONG)_hr_); }
+
+#ifdef DEBUG
+#define CheckParameters1( m, a1 )
+#define CheckParameters2( m, a1, a2 ) \
+ { \
+ MakeArray1(a2); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters3( m, a1, a2, a3 ) \
+ { \
+ MakeArray2(a2, a3); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters4( m, a1, a2, a3, a4 ) \
+ { \
+ MakeArray3(a2, a3, a4); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters5( m, a1, a2, a3, a4, a5 ) \
+ { \
+ MakeArray4(a2, a3, a4, a5); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ { \
+ MakeArray5(a2, a3, a4, a5, a6); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ { \
+ MakeArray6(a2, a3, a4, a5, a6, a7); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ { \
+ MakeArray7(a2, a3, a4, a5, a6, a7, a8); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ { \
+ MakeArray8(a2, a3, a4, a5, a6, a7, a8, a9); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ { \
+ MakeArray9(a2, a3, a4, a5, a6, a7, a8, a9, a10); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ { \
+ MakeArray10(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ { \
+ MakeArray11(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ { \
+ MakeArray12(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ { \
+ MakeArray13(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ { \
+ MakeArray14(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#define CheckParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ { \
+ MakeArray15(a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16); \
+ AssertSz(HR_SUCCEEDED( HrValidateParameters(m, (void **)__rgArgs)), "Parameter validation failed for method called by MAPI!"); }
+#else /* DEBUG */
+#define CheckParameters1( m, a1 )
+#define CheckParameters2( m, a1, a2 )
+#define CheckParameters3( m, a1, a2, a3 )
+#define CheckParameters4( m, a1, a2, a3, a4 )
+#define CheckParameters5( m, a1, a2, a3, a4, a5 )
+#define CheckParameters6( m, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters7( m, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 )
+#define CheckParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+#define CheckParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 )
+#define CheckParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 )
+#define CheckParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 )
+#define CheckParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+#define CheckParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 )
+#define CheckParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 )
+#define CheckParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 )
+#endif /* DEBUG */
+#else /* !_AMD64_ && !_X86_ */
+#define ValidateParms(x) { HRESULT _hr_ = HrValidateParametersV x; if (HR_FAILED(_hr_)) return (_hr_); }
+#define UlValidateParms(x) { HRESULT _hr_ = HrValidateParametersV x; if (HR_FAILED(_hr_)) return (ULONG)(_hr_); }
+#define CheckParms(x) AssertSz(HR_SUCCEEDED( HrValidateParametersV x ), "Parameter validation failed for method called by MAPI!")
+
+#define ValidateParameters1( m, a1 ) \
+ ValidateParms( ( m, a1 ) )
+#define ValidateParameters2( m, a1, a2 ) \
+ ValidateParms( ( m, a1, a2 ))
+#define ValidateParameters3( m, a1, a2, a3 ) \
+ ValidateParms( ( m, a1, a2, a3 ))
+#define ValidateParameters4( m, a1, a2, a3, a4 ) \
+ ValidateParms( ( m, a1, a2, a3, a4 ))
+#define ValidateParameters5( m, a1, a2, a3, a4, a5 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5 ))
+#define ValidateParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6 ))
+#define ValidateParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7 ))
+#define ValidateParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8 ))
+#define ValidateParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ))
+#define ValidateParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ))
+#define ValidateParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ))
+#define ValidateParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ))
+#define ValidateParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ))
+#define ValidateParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ))
+#define ValidateParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ))
+#define ValidateParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ ValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ))
+
+#define UlValidateParameters1( m, a1 ) \
+ UlValidateParms( ( m, a1 ) )
+#define UlValidateParameters2( m, a1, a2 ) \
+ UlValidateParms( ( m, a1, a2 ))
+#define UlValidateParameters3( m, a1, a2, a3 ) \
+ UlValidateParms( ( m, a1, a2, a3 ))
+#define UlValidateParameters4( m, a1, a2, a3, a4 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4 ))
+#define UlValidateParameters5( m, a1, a2, a3, a4, a5 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5 ))
+#define UlValidateParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6 ))
+#define UlValidateParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7 ))
+#define UlValidateParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8 ))
+#define UlValidateParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ))
+#define UlValidateParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ))
+#define UlValidateParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ))
+#define UlValidateParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ))
+#define UlValidateParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ))
+#define UlValidateParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ))
+#define UlValidateParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ))
+#define UlValidateParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ UlValidateParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ))
+
+#define CheckParameters1( m, a1 ) \
+ CheckParms( ( m, a1 ) )
+#define CheckParameters2( m, a1, a2 ) \
+ CheckParms( ( m, a1, a2 ))
+#define CheckParameters3( m, a1, a2, a3 ) \
+ CheckParms( ( m, a1, a2, a3 ))
+#define CheckParameters4( m, a1, a2, a3, a4 ) \
+ CheckParms( ( m, a1, a2, a3, a4 ))
+#define CheckParameters5( m, a1, a2, a3, a4, a5 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5 ))
+#define CheckParameters6( m, a1, a2, a3, a4, a5, a6 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6 ))
+#define CheckParameters7( m, a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7 ))
+#define CheckParameters8( m, a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8 ))
+#define CheckParameters9( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9 ))
+#define CheckParameters10( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ))
+#define CheckParameters11( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 ))
+#define CheckParameters12( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 ))
+#define CheckParameters13( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ))
+#define CheckParameters14( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 ))
+#define CheckParameters15( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 ))
+#define CheckParameters16( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ) \
+ CheckParms( ( m, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16 ))
+
+#endif /* _AMD64_ || _X86_ */
+
+
+/*
+ * M A P I P A R A M E T E R V A L I D A T I O N M A C R O S
+ */
+
+
+/* IUnknown */
+
+#define Validate_IUnknown_QueryInterface( a1, a2, a3 ) \
+ ValidateParameters3( IUnknown_QueryInterface, a1, a2, a3 )
+#define UlValidate_IUnknown_QueryInterface( a1, a2, a3 ) \
+ UlValidateParameters3( IUnknown_QueryInterface, a1, a2, a3 )
+#define CheckParameters_IUnknown_QueryInterface( a1, a2, a3 ) \
+ CheckParameters3( IUnknown_QueryInterface, a1, a2, a3 )
+
+#define Validate_IUnknown_AddRef( a1 ) \
+ ValidateParameters1( IUnknown_AddRef, a1 )
+#define UlValidate_IUnknown_AddRef( a1 ) \
+ UlValidateParameters1( IUnknown_AddRef, a1 )
+#define CheckParameters_IUnknown_AddRef( a1 ) \
+ CheckParameters1( IUnknown_AddRef, a1 )
+
+#define Validate_IUnknown_Release( a1 ) \
+ ValidateParameters1( IUnknown_Release, a1 )
+#define UlValidate_IUnknown_Release( a1 ) \
+ UlValidateParameters1( IUnknown_Release, a1 )
+#define CheckParameters_IUnknown_Release( a1 ) \
+ CheckParameters1( IUnknown_Release, a1 )
+
+
+/* IMAPIProp */
+
+#define Validate_IMAPIProp_GetLastError( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIProp_GetLastError, a1, a2, a3, a4 )
+#define UlValidate_IMAPIProp_GetLastError( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIProp_GetLastError, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIProp_GetLastError( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIProp_GetLastError, a1, a2, a3, a4 )
+
+#define Validate_IMAPIProp_SaveChanges( a1, a2 ) \
+ ValidateParameters2( IMAPIProp_SaveChanges, a1, a2 )
+#define UlValidate_IMAPIProp_SaveChanges( a1, a2 ) \
+ UlValidateParameters2( IMAPIProp_SaveChanges, a1, a2 )
+#define CheckParameters_IMAPIProp_SaveChanges( a1, a2 ) \
+ CheckParameters2( IMAPIProp_SaveChanges, a1, a2 )
+
+#define Validate_IMAPIProp_GetProps( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIProp_GetProps, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIProp_GetProps( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIProp_GetProps, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIProp_GetProps( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIProp_GetProps, a1, a2, a3, a4, a5 )
+
+#define Validate_IMAPIProp_GetPropList( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIProp_GetPropList, a1, a2, a3 )
+#define UlValidate_IMAPIProp_GetPropList( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIProp_GetPropList, a1, a2, a3 )
+#define CheckParameters_IMAPIProp_GetPropList( a1, a2, a3 ) \
+ CheckParameters3( IMAPIProp_GetPropList, a1, a2, a3 )
+
+#define Validate_IMAPIProp_OpenProperty( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMAPIProp_OpenProperty, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMAPIProp_OpenProperty( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMAPIProp_OpenProperty, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMAPIProp_OpenProperty( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMAPIProp_OpenProperty, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMAPIProp_SetProps( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIProp_SetProps, a1, a2, a3, a4 )
+#define UlValidate_IMAPIProp_SetProps( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIProp_SetProps, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIProp_SetProps( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIProp_SetProps, a1, a2, a3, a4 )
+
+#define Validate_IMAPIProp_DeleteProps( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIProp_DeleteProps, a1, a2, a3 )
+#define UlValidate_IMAPIProp_DeleteProps( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIProp_DeleteProps, a1, a2, a3 )
+#define CheckParameters_IMAPIProp_DeleteProps( a1, a2, a3 ) \
+ CheckParameters3( IMAPIProp_DeleteProps, a1, a2, a3 )
+
+#define Validate_IMAPIProp_CopyTo( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ ValidateParameters10( IMAPIProp_CopyTo, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 )
+#define UlValidate_IMAPIProp_CopyTo( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ UlValidateParameters10( IMAPIProp_CopyTo, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 )
+#define CheckParameters_IMAPIProp_CopyTo( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 ) \
+ CheckParameters10( IMAPIProp_CopyTo, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 )
+
+#define Validate_IMAPIProp_CopyProps( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ ValidateParameters8( IMAPIProp_CopyProps, a1, a2, a3, a4, a5, a6, a7, a8 )
+#define UlValidate_IMAPIProp_CopyProps( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ UlValidateParameters8( IMAPIProp_CopyProps, a1, a2, a3, a4, a5, a6, a7, a8 )
+#define CheckParameters_IMAPIProp_CopyProps( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ CheckParameters8( IMAPIProp_CopyProps, a1, a2, a3, a4, a5, a6, a7, a8 )
+
+#define Validate_IMAPIProp_GetNamesFromIDs( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMAPIProp_GetNamesFromIDs, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMAPIProp_GetNamesFromIDs( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMAPIProp_GetNamesFromIDs, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMAPIProp_GetNamesFromIDs( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMAPIProp_GetNamesFromIDs, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMAPIProp_GetIDsFromNames( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIProp_GetIDsFromNames, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIProp_GetIDsFromNames( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIProp_GetIDsFromNames, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIProp_GetIDsFromNames( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIProp_GetIDsFromNames, a1, a2, a3, a4, a5 )
+
+
+/* IMAPITable */
+
+#define Validate_IMAPITable_GetLastError( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_GetLastError, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_GetLastError( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_GetLastError, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_GetLastError( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_GetLastError, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_Advise( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_Advise, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_Advise( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_Advise, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_Advise( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_Advise, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_Unadvise( a1, a2 ) \
+ ValidateParameters2( IMAPITable_Unadvise, a1, a2 )
+#define UlValidate_IMAPITable_Unadvise( a1, a2 ) \
+ UlValidateParameters2( IMAPITable_Unadvise, a1, a2 )
+#define CheckParameters_IMAPITable_Unadvise( a1, a2 ) \
+ CheckParameters2( IMAPITable_Unadvise, a1, a2 )
+
+#define Validate_IMAPITable_GetStatus( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_GetStatus, a1, a2, a3 )
+#define UlValidate_IMAPITable_GetStatus( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_GetStatus, a1, a2, a3 )
+#define CheckParameters_IMAPITable_GetStatus( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_GetStatus, a1, a2, a3 )
+
+#define Validate_IMAPITable_SetColumns( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_SetColumns, a1, a2, a3 )
+#define UlValidate_IMAPITable_SetColumns( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_SetColumns, a1, a2, a3 )
+#define CheckParameters_IMAPITable_SetColumns( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_SetColumns, a1, a2, a3 )
+
+#define Validate_IMAPITable_QueryColumns( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_QueryColumns, a1, a2, a3 )
+#define UlValidate_IMAPITable_QueryColumns( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_QueryColumns, a1, a2, a3 )
+#define CheckParameters_IMAPITable_QueryColumns( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_QueryColumns, a1, a2, a3 )
+
+#define Validate_IMAPITable_GetRowCount( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_GetRowCount, a1, a2, a3 )
+#define UlValidate_IMAPITable_GetRowCount( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_GetRowCount, a1, a2, a3 )
+#define CheckParameters_IMAPITable_GetRowCount( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_GetRowCount, a1, a2, a3 )
+
+#define Validate_IMAPITable_SeekRow( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_SeekRow, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_SeekRow( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_SeekRow, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_SeekRow( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_SeekRow, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_SeekRowApprox( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_SeekRowApprox, a1, a2, a3 )
+#define UlValidate_IMAPITable_SeekRowApprox( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_SeekRowApprox, a1, a2, a3 )
+#define CheckParameters_IMAPITable_SeekRowApprox( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_SeekRowApprox, a1, a2, a3 )
+
+#define Validate_IMAPITable_QueryPosition( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_QueryPosition, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_QueryPosition( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_QueryPosition, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_QueryPosition( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_QueryPosition, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_FindRow( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_FindRow, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_FindRow( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_FindRow, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_FindRow( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_FindRow, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_Restrict( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_Restrict, a1, a2, a3 )
+#define UlValidate_IMAPITable_Restrict( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_Restrict, a1, a2, a3 )
+#define CheckParameters_IMAPITable_Restrict( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_Restrict, a1, a2, a3 )
+
+#define Validate_IMAPITable_CreateBookmark( a1, a2 ) \
+ ValidateParameters2( IMAPITable_CreateBookmark, a1, a2 )
+#define UlValidate_IMAPITable_CreateBookmark( a1, a2 ) \
+ UlValidateParameters2( IMAPITable_CreateBookmark, a1, a2 )
+#define CheckParameters_IMAPITable_CreateBookmark( a1, a2 ) \
+ CheckParameters2( IMAPITable_CreateBookmark, a1, a2 )
+
+#define Validate_IMAPITable_FreeBookmark( a1, a2 ) \
+ ValidateParameters2( IMAPITable_FreeBookmark, a1, a2 )
+#define UlValidate_IMAPITable_FreeBookmark( a1, a2 ) \
+ UlValidateParameters2( IMAPITable_FreeBookmark, a1, a2 )
+#define CheckParameters_IMAPITable_FreeBookmark( a1, a2 ) \
+ CheckParameters2( IMAPITable_FreeBookmark, a1, a2 )
+
+#define Validate_IMAPITable_SortTable( a1, a2, a3 ) \
+ ValidateParameters3( IMAPITable_SortTable, a1, a2, a3 )
+#define UlValidate_IMAPITable_SortTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPITable_SortTable, a1, a2, a3 )
+#define CheckParameters_IMAPITable_SortTable( a1, a2, a3 ) \
+ CheckParameters3( IMAPITable_SortTable, a1, a2, a3 )
+
+#define Validate_IMAPITable_QuerySortOrder( a1, a2 ) \
+ ValidateParameters2( IMAPITable_QuerySortOrder, a1, a2 )
+#define UlValidate_IMAPITable_QuerySortOrder( a1, a2 ) \
+ UlValidateParameters2( IMAPITable_QuerySortOrder, a1, a2 )
+#define CheckParameters_IMAPITable_QuerySortOrder( a1, a2 ) \
+ CheckParameters2( IMAPITable_QuerySortOrder, a1, a2 )
+
+#define Validate_IMAPITable_QueryRows( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_QueryRows, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_QueryRows( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_QueryRows, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_QueryRows( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_QueryRows, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_Abort( a1 ) \
+ ValidateParameters1( IMAPITable_Abort, a1 )
+#define UlValidate_IMAPITable_Abort( a1 ) \
+ UlValidateParameters1( IMAPITable_Abort, a1 )
+#define CheckParameters_IMAPITable_Abort( a1 ) \
+ CheckParameters1( IMAPITable_Abort, a1 )
+
+#define Validate_IMAPITable_ExpandRow( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMAPITable_ExpandRow, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMAPITable_ExpandRow( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMAPITable_ExpandRow, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMAPITable_ExpandRow( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMAPITable_ExpandRow, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMAPITable_CollapseRow( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPITable_CollapseRow, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPITable_CollapseRow( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPITable_CollapseRow, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPITable_CollapseRow( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPITable_CollapseRow, a1, a2, a3, a4, a5 )
+
+#define Validate_IMAPITable_WaitForCompletion( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPITable_WaitForCompletion, a1, a2, a3, a4 )
+#define UlValidate_IMAPITable_WaitForCompletion( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPITable_WaitForCompletion, a1, a2, a3, a4 )
+#define CheckParameters_IMAPITable_WaitForCompletion( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPITable_WaitForCompletion, a1, a2, a3, a4 )
+
+#define Validate_IMAPITable_GetCollapseState( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMAPITable_GetCollapseState, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMAPITable_GetCollapseState( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMAPITable_GetCollapseState, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMAPITable_GetCollapseState( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMAPITable_GetCollapseState, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMAPITable_SetCollapseState( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPITable_SetCollapseState, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPITable_SetCollapseState( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPITable_SetCollapseState, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPITable_SetCollapseState( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPITable_SetCollapseState, a1, a2, a3, a4, a5 )
+
+
+/* IMAPIContainer */
+
+#define Validate_IMAPIContainer_GetContentsTable( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIContainer_GetContentsTable, a1, a2, a3 )
+#define UlValidate_IMAPIContainer_GetContentsTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIContainer_GetContentsTable, a1, a2, a3 )
+#define CheckParameters_IMAPIContainer_GetContentsTable( a1, a2, a3 ) \
+ CheckParameters3( IMAPIContainer_GetContentsTable, a1, a2, a3 )
+
+#define Validate_IMAPIContainer_GetHierarchyTable( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIContainer_GetHierarchyTable, a1, a2, a3 )
+#define UlValidate_IMAPIContainer_GetHierarchyTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIContainer_GetHierarchyTable, a1, a2, a3 )
+#define CheckParameters_IMAPIContainer_GetHierarchyTable( a1, a2, a3 ) \
+ CheckParameters3( IMAPIContainer_GetHierarchyTable, a1, a2, a3 )
+
+#define Validate_IMAPIContainer_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMAPIContainer_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMAPIContainer_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMAPIContainer_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMAPIContainer_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMAPIContainer_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMAPIContainer_SetSearchCriteria( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIContainer_SetSearchCriteria, a1, a2, a3, a4 )
+#define UlValidate_IMAPIContainer_SetSearchCriteria( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIContainer_SetSearchCriteria, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIContainer_SetSearchCriteria( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIContainer_SetSearchCriteria, a1, a2, a3, a4 )
+
+#define Validate_IMAPIContainer_GetSearchCriteria( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIContainer_GetSearchCriteria, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIContainer_GetSearchCriteria( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIContainer_GetSearchCriteria, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIContainer_GetSearchCriteria( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIContainer_GetSearchCriteria, a1, a2, a3, a4, a5 )
+
+
+/* IABContainer */
+
+#define Validate_IABContainer_CreateEntry( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IABContainer_CreateEntry, a1, a2, a3, a4, a5 )
+#define UlValidate_IABContainer_CreateEntry( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IABContainer_CreateEntry, a1, a2, a3, a4, a5 )
+#define CheckParameters_IABContainer_CreateEntry( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IABContainer_CreateEntry, a1, a2, a3, a4, a5 )
+
+#define Validate_IABContainer_CopyEntries( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IABContainer_CopyEntries, a1, a2, a3, a4, a5 )
+#define UlValidate_IABContainer_CopyEntries( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IABContainer_CopyEntries, a1, a2, a3, a4, a5 )
+#define CheckParameters_IABContainer_CopyEntries( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IABContainer_CopyEntries, a1, a2, a3, a4, a5 )
+
+#define Validate_IABContainer_DeleteEntries( a1, a2, a3 ) \
+ ValidateParameters3( IABContainer_DeleteEntries, a1, a2, a3 )
+#define UlValidate_IABContainer_DeleteEntries( a1, a2, a3 ) \
+ UlValidateParameters3( IABContainer_DeleteEntries, a1, a2, a3 )
+#define CheckParameters_IABContainer_DeleteEntries( a1, a2, a3 ) \
+ CheckParameters3( IABContainer_DeleteEntries, a1, a2, a3 )
+
+#define Validate_IABContainer_ResolveNames( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IABContainer_ResolveNames, a1, a2, a3, a4, a5 )
+#define UlValidate_IABContainer_ResolveNames( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IABContainer_ResolveNames, a1, a2, a3, a4, a5 )
+#define CheckParameters_IABContainer_ResolveNames( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IABContainer_ResolveNames, a1, a2, a3, a4, a5 )
+
+
+/* IDistList */
+
+#define Validate_IDistList_CreateEntry( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IDistList_CreateEntry, a1, a2, a3, a4, a5 )
+#define UlValidate_IDistList_CreateEntry( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IDistList_CreateEntry, a1, a2, a3, a4, a5 )
+#define CheckParameters_IDistList_CreateEntry( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IDistList_CreateEntry, a1, a2, a3, a4, a5 )
+
+#define Validate_IDistList_CopyEntries( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IDistList_CopyEntries, a1, a2, a3, a4, a5 )
+#define UlValidate_IDistList_CopyEntries( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IDistList_CopyEntries, a1, a2, a3, a4, a5 )
+#define CheckParameters_IDistList_CopyEntries( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IDistList_CopyEntries, a1, a2, a3, a4, a5 )
+
+#define Validate_IDistList_DeleteEntries( a1, a2, a3 ) \
+ ValidateParameters3( IDistList_DeleteEntries, a1, a2, a3 )
+#define UlValidate_IDistList_DeleteEntries( a1, a2, a3 ) \
+ UlValidateParameters3( IDistList_DeleteEntries, a1, a2, a3 )
+#define CheckParameters_IDistList_DeleteEntries( a1, a2, a3 ) \
+ CheckParameters3( IDistList_DeleteEntries, a1, a2, a3 )
+
+#define Validate_IDistList_ResolveNames( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IDistList_ResolveNames, a1, a2, a3, a4, a5 )
+#define UlValidate_IDistList_ResolveNames( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IDistList_ResolveNames, a1, a2, a3, a4, a5 )
+#define CheckParameters_IDistList_ResolveNames( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IDistList_ResolveNames, a1, a2, a3, a4, a5 )
+
+
+/* IMAPIFolder */
+
+#define Validate_IMAPIFolder_CreateMessage( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIFolder_CreateMessage, a1, a2, a3, a4 )
+#define UlValidate_IMAPIFolder_CreateMessage( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIFolder_CreateMessage, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIFolder_CreateMessage( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIFolder_CreateMessage, a1, a2, a3, a4 )
+
+#define Validate_IMAPIFolder_CopyMessages( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMAPIFolder_CopyMessages, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMAPIFolder_CopyMessages( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMAPIFolder_CopyMessages, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMAPIFolder_CopyMessages( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMAPIFolder_CopyMessages, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMAPIFolder_DeleteMessages( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIFolder_DeleteMessages, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIFolder_DeleteMessages( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIFolder_DeleteMessages, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIFolder_DeleteMessages( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIFolder_DeleteMessages, a1, a2, a3, a4, a5 )
+
+#define Validate_IMAPIFolder_CreateFolder( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMAPIFolder_CreateFolder, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMAPIFolder_CreateFolder( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMAPIFolder_CreateFolder, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMAPIFolder_CreateFolder( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMAPIFolder_CreateFolder, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMAPIFolder_CopyFolder( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ ValidateParameters9( IMAPIFolder_CopyFolder, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+#define UlValidate_IMAPIFolder_CopyFolder( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ UlValidateParameters9( IMAPIFolder_CopyFolder, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+#define CheckParameters_IMAPIFolder_CopyFolder( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ CheckParameters9( IMAPIFolder_CopyFolder, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+
+#define Validate_IMAPIFolder_DeleteFolder( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMAPIFolder_DeleteFolder, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMAPIFolder_DeleteFolder( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMAPIFolder_DeleteFolder, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMAPIFolder_DeleteFolder( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMAPIFolder_DeleteFolder, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMAPIFolder_SetReadFlags( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIFolder_SetReadFlags, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIFolder_SetReadFlags( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIFolder_SetReadFlags, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIFolder_SetReadFlags( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIFolder_SetReadFlags, a1, a2, a3, a4, a5 )
+
+#define Validate_IMAPIFolder_GetMessageStatus( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIFolder_GetMessageStatus, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIFolder_GetMessageStatus( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIFolder_GetMessageStatus, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIFolder_GetMessageStatus( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIFolder_GetMessageStatus, a1, a2, a3, a4, a5 )
+
+#define Validate_IMAPIFolder_SetMessageStatus( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMAPIFolder_SetMessageStatus, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMAPIFolder_SetMessageStatus( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMAPIFolder_SetMessageStatus, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMAPIFolder_SetMessageStatus( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMAPIFolder_SetMessageStatus, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMAPIFolder_SaveContentsSort( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIFolder_SaveContentsSort, a1, a2, a3 )
+#define UlValidate_IMAPIFolder_SaveContentsSort( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIFolder_SaveContentsSort, a1, a2, a3 )
+#define CheckParameters_IMAPIFolder_SaveContentsSort( a1, a2, a3 ) \
+ CheckParameters3( IMAPIFolder_SaveContentsSort, a1, a2, a3 )
+
+#define Validate_IMAPIFolder_EmptyFolder( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIFolder_EmptyFolder, a1, a2, a3, a4 )
+#define UlValidate_IMAPIFolder_EmptyFolder( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIFolder_EmptyFolder, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIFolder_EmptyFolder( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIFolder_EmptyFolder, a1, a2, a3, a4 )
+
+
+/* IMsgStore */
+
+#define Validate_IMsgStore_Advise( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMsgStore_Advise, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMsgStore_Advise( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMsgStore_Advise, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMsgStore_Advise( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMsgStore_Advise, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMsgStore_Unadvise( a1, a2 ) \
+ ValidateParameters2( IMsgStore_Unadvise, a1, a2 )
+#define UlValidate_IMsgStore_Unadvise( a1, a2 ) \
+ UlValidateParameters2( IMsgStore_Unadvise, a1, a2 )
+#define CheckParameters_IMsgStore_Unadvise( a1, a2 ) \
+ CheckParameters2( IMsgStore_Unadvise, a1, a2 )
+
+#define Validate_IMsgStore_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMsgStore_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMsgStore_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMsgStore_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMsgStore_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMsgStore_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMsgStore_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMsgStore_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMsgStore_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMsgStore_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMsgStore_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMsgStore_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMsgStore_SetReceiveFolder( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMsgStore_SetReceiveFolder, a1, a2, a3, a4, a5 )
+#define UlValidate_IMsgStore_SetReceiveFolder( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMsgStore_SetReceiveFolder, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMsgStore_SetReceiveFolder( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMsgStore_SetReceiveFolder, a1, a2, a3, a4, a5 )
+
+#define Validate_IMsgStore_GetReceiveFolder( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMsgStore_GetReceiveFolder, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMsgStore_GetReceiveFolder( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMsgStore_GetReceiveFolder, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMsgStore_GetReceiveFolder( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMsgStore_GetReceiveFolder, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMsgStore_GetReceiveFolderTable( a1, a2, a3 ) \
+ ValidateParameters3( IMsgStore_GetReceiveFolderTable, a1, a2, a3 )
+#define UlValidate_IMsgStore_GetReceiveFolderTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMsgStore_GetReceiveFolderTable, a1, a2, a3 )
+#define CheckParameters_IMsgStore_GetReceiveFolderTable( a1, a2, a3 ) \
+ CheckParameters3( IMsgStore_GetReceiveFolderTable, a1, a2, a3 )
+
+#define Validate_IMsgStore_StoreLogoff( a1, a2 ) \
+ ValidateParameters2( IMsgStore_StoreLogoff, a1, a2 )
+#define UlValidate_IMsgStore_StoreLogoff( a1, a2 ) \
+ UlValidateParameters2( IMsgStore_StoreLogoff, a1, a2 )
+#define CheckParameters_IMsgStore_StoreLogoff( a1, a2 ) \
+ CheckParameters2( IMsgStore_StoreLogoff, a1, a2 )
+
+#define Validate_IMsgStore_AbortSubmit( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMsgStore_AbortSubmit, a1, a2, a3, a4 )
+#define UlValidate_IMsgStore_AbortSubmit( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMsgStore_AbortSubmit, a1, a2, a3, a4 )
+#define CheckParameters_IMsgStore_AbortSubmit( a1, a2, a3, a4 ) \
+ CheckParameters4( IMsgStore_AbortSubmit, a1, a2, a3, a4 )
+
+#define Validate_IMsgStore_GetOutgoingQueue( a1, a2, a3 ) \
+ ValidateParameters3( IMsgStore_GetOutgoingQueue, a1, a2, a3 )
+#define UlValidate_IMsgStore_GetOutgoingQueue( a1, a2, a3 ) \
+ UlValidateParameters3( IMsgStore_GetOutgoingQueue, a1, a2, a3 )
+#define CheckParameters_IMsgStore_GetOutgoingQueue( a1, a2, a3 ) \
+ CheckParameters3( IMsgStore_GetOutgoingQueue, a1, a2, a3 )
+
+#define Validate_IMsgStore_SetLockState( a1, a2, a3 ) \
+ ValidateParameters3( IMsgStore_SetLockState, a1, a2, a3 )
+#define UlValidate_IMsgStore_SetLockState( a1, a2, a3 ) \
+ UlValidateParameters3( IMsgStore_SetLockState, a1, a2, a3 )
+#define CheckParameters_IMsgStore_SetLockState( a1, a2, a3 ) \
+ CheckParameters3( IMsgStore_SetLockState, a1, a2, a3 )
+
+#define Validate_IMsgStore_FinishedMsg( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMsgStore_FinishedMsg, a1, a2, a3, a4 )
+#define UlValidate_IMsgStore_FinishedMsg( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMsgStore_FinishedMsg, a1, a2, a3, a4 )
+#define CheckParameters_IMsgStore_FinishedMsg( a1, a2, a3, a4 ) \
+ CheckParameters4( IMsgStore_FinishedMsg, a1, a2, a3, a4 )
+
+#define Validate_IMsgStore_NotifyNewMail( a1, a2 ) \
+ ValidateParameters2( IMsgStore_NotifyNewMail, a1, a2 )
+#define UlValidate_IMsgStore_NotifyNewMail( a1, a2 ) \
+ UlValidateParameters2( IMsgStore_NotifyNewMail, a1, a2 )
+#define CheckParameters_IMsgStore_NotifyNewMail( a1, a2 ) \
+ CheckParameters2( IMsgStore_NotifyNewMail, a1, a2 )
+
+
+/* IMessage */
+
+#define Validate_IMessage_GetAttachmentTable( a1, a2, a3 ) \
+ ValidateParameters3( IMessage_GetAttachmentTable, a1, a2, a3 )
+#define UlValidate_IMessage_GetAttachmentTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMessage_GetAttachmentTable, a1, a2, a3 )
+#define CheckParameters_IMessage_GetAttachmentTable( a1, a2, a3 ) \
+ CheckParameters3( IMessage_GetAttachmentTable, a1, a2, a3 )
+
+#define Validate_IMessage_OpenAttach( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMessage_OpenAttach, a1, a2, a3, a4, a5 )
+#define UlValidate_IMessage_OpenAttach( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMessage_OpenAttach, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMessage_OpenAttach( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMessage_OpenAttach, a1, a2, a3, a4, a5 )
+
+#define Validate_IMessage_CreateAttach( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMessage_CreateAttach, a1, a2, a3, a4, a5 )
+#define UlValidate_IMessage_CreateAttach( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMessage_CreateAttach, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMessage_CreateAttach( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMessage_CreateAttach, a1, a2, a3, a4, a5 )
+
+#define Validate_IMessage_DeleteAttach( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMessage_DeleteAttach, a1, a2, a3, a4, a5 )
+#define UlValidate_IMessage_DeleteAttach( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMessage_DeleteAttach, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMessage_DeleteAttach( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMessage_DeleteAttach, a1, a2, a3, a4, a5 )
+
+#define Validate_IMessage_GetRecipientTable( a1, a2, a3 ) \
+ ValidateParameters3( IMessage_GetRecipientTable, a1, a2, a3 )
+#define UlValidate_IMessage_GetRecipientTable( a1, a2, a3 ) \
+ UlValidateParameters3( IMessage_GetRecipientTable, a1, a2, a3 )
+#define CheckParameters_IMessage_GetRecipientTable( a1, a2, a3 ) \
+ CheckParameters3( IMessage_GetRecipientTable, a1, a2, a3 )
+
+#define Validate_IMessage_ModifyRecipients( a1, a2, a3 ) \
+ ValidateParameters3( IMessage_ModifyRecipients, a1, a2, a3 )
+#define UlValidate_IMessage_ModifyRecipients( a1, a2, a3 ) \
+ UlValidateParameters3( IMessage_ModifyRecipients, a1, a2, a3 )
+#define CheckParameters_IMessage_ModifyRecipients( a1, a2, a3 ) \
+ CheckParameters3( IMessage_ModifyRecipients, a1, a2, a3 )
+
+#define Validate_IMessage_SubmitMessage( a1, a2 ) \
+ ValidateParameters2( IMessage_SubmitMessage, a1, a2 )
+#define UlValidate_IMessage_SubmitMessage( a1, a2 ) \
+ UlValidateParameters2( IMessage_SubmitMessage, a1, a2 )
+#define CheckParameters_IMessage_SubmitMessage( a1, a2 ) \
+ CheckParameters2( IMessage_SubmitMessage, a1, a2 )
+
+#define Validate_IMessage_SetReadFlag( a1, a2 ) \
+ ValidateParameters2( IMessage_SetReadFlag, a1, a2 )
+#define UlValidate_IMessage_SetReadFlag( a1, a2 ) \
+ UlValidateParameters2( IMessage_SetReadFlag, a1, a2 )
+#define CheckParameters_IMessage_SetReadFlag( a1, a2 ) \
+ CheckParameters2( IMessage_SetReadFlag, a1, a2 )
+
+
+/* IABProvider */
+
+#define Validate_IABProvider_Shutdown( a1, a2 ) \
+ ValidateParameters2( IABProvider_Shutdown, a1, a2 )
+#define UlValidate_IABProvider_Shutdown( a1, a2 ) \
+ UlValidateParameters2( IABProvider_Shutdown, a1, a2 )
+#define CheckParameters_IABProvider_Shutdown( a1, a2 ) \
+ CheckParameters2( IABProvider_Shutdown, a1, a2 )
+
+#define Validate_IABProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ ValidateParameters9( IABProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+#define UlValidate_IABProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ UlValidateParameters9( IABProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+#define CheckParameters_IABProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9 ) \
+ CheckParameters9( IABProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9 )
+
+
+/* IABLogon */
+
+#define Validate_IABLogon_GetLastError( a1, a2, a3, a4 ) \
+ ValidateParameters4( IABLogon_GetLastError, a1, a2, a3, a4 )
+#define UlValidate_IABLogon_GetLastError( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IABLogon_GetLastError, a1, a2, a3, a4 )
+#define CheckParameters_IABLogon_GetLastError( a1, a2, a3, a4 ) \
+ CheckParameters4( IABLogon_GetLastError, a1, a2, a3, a4 )
+
+#define Validate_IABLogon_Logoff( a1, a2 ) \
+ ValidateParameters2( IABLogon_Logoff, a1, a2 )
+#define UlValidate_IABLogon_Logoff( a1, a2 ) \
+ UlValidateParameters2( IABLogon_Logoff, a1, a2 )
+#define CheckParameters_IABLogon_Logoff( a1, a2 ) \
+ CheckParameters2( IABLogon_Logoff, a1, a2 )
+
+#define Validate_IABLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IABLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IABLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IABLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IABLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IABLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IABLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IABLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IABLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IABLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IABLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IABLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IABLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IABLogon_Advise, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IABLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IABLogon_Advise, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IABLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IABLogon_Advise, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IABLogon_Unadvise( a1, a2 ) \
+ ValidateParameters2( IABLogon_Unadvise, a1, a2 )
+#define UlValidate_IABLogon_Unadvise( a1, a2 ) \
+ UlValidateParameters2( IABLogon_Unadvise, a1, a2 )
+#define CheckParameters_IABLogon_Unadvise( a1, a2 ) \
+ CheckParameters2( IABLogon_Unadvise, a1, a2 )
+
+#define Validate_IABLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IABLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define UlValidate_IABLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IABLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define CheckParameters_IABLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IABLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+
+#define Validate_IABLogon_OpenTemplateID( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ ValidateParameters8( IABLogon_OpenTemplateID, a1, a2, a3, a4, a5, a6, a7, a8 )
+#define UlValidate_IABLogon_OpenTemplateID( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ UlValidateParameters8( IABLogon_OpenTemplateID, a1, a2, a3, a4, a5, a6, a7, a8 )
+#define CheckParameters_IABLogon_OpenTemplateID( a1, a2, a3, a4, a5, a6, a7, a8 ) \
+ CheckParameters8( IABLogon_OpenTemplateID, a1, a2, a3, a4, a5, a6, a7, a8 )
+
+#define Validate_IABLogon_GetOneOffTable( a1, a2, a3 ) \
+ ValidateParameters3( IABLogon_GetOneOffTable, a1, a2, a3 )
+#define UlValidate_IABLogon_GetOneOffTable( a1, a2, a3 ) \
+ UlValidateParameters3( IABLogon_GetOneOffTable, a1, a2, a3 )
+#define CheckParameters_IABLogon_GetOneOffTable( a1, a2, a3 ) \
+ CheckParameters3( IABLogon_GetOneOffTable, a1, a2, a3 )
+
+#define Validate_IABLogon_PrepareRecips( a1, a2, a3, a4 ) \
+ ValidateParameters4( IABLogon_PrepareRecips, a1, a2, a3, a4 )
+#define UlValidate_IABLogon_PrepareRecips( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IABLogon_PrepareRecips, a1, a2, a3, a4 )
+#define CheckParameters_IABLogon_PrepareRecips( a1, a2, a3, a4 ) \
+ CheckParameters4( IABLogon_PrepareRecips, a1, a2, a3, a4 )
+
+
+/* IXPProvider */
+
+#define Validate_IXPProvider_Shutdown( a1, a2 ) \
+ ValidateParameters2( IXPProvider_Shutdown, a1, a2 )
+#define UlValidate_IXPProvider_Shutdown( a1, a2 ) \
+ UlValidateParameters2( IXPProvider_Shutdown, a1, a2 )
+#define CheckParameters_IXPProvider_Shutdown( a1, a2 ) \
+ CheckParameters2( IXPProvider_Shutdown, a1, a2 )
+
+#define Validate_IXPProvider_TransportLogon( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IXPProvider_TransportLogon, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IXPProvider_TransportLogon( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IXPProvider_TransportLogon, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IXPProvider_TransportLogon( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IXPProvider_TransportLogon, a1, a2, a3, a4, a5, a6, a7 )
+
+
+/* IXPLogon */
+
+#define Validate_IXPLogon_AddressTypes( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IXPLogon_AddressTypes, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IXPLogon_AddressTypes( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IXPLogon_AddressTypes, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IXPLogon_AddressTypes( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IXPLogon_AddressTypes, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IXPLogon_RegisterOptions( a1, a2, a3, a4 ) \
+ ValidateParameters4( IXPLogon_RegisterOptions, a1, a2, a3, a4 )
+#define UlValidate_IXPLogon_RegisterOptions( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IXPLogon_RegisterOptions, a1, a2, a3, a4 )
+#define CheckParameters_IXPLogon_RegisterOptions( a1, a2, a3, a4 ) \
+ CheckParameters4( IXPLogon_RegisterOptions, a1, a2, a3, a4 )
+
+#define Validate_IXPLogon_TransportNotify( a1, a2, a3 ) \
+ ValidateParameters3( IXPLogon_TransportNotify, a1, a2, a3 )
+#define UlValidate_IXPLogon_TransportNotify( a1, a2, a3 ) \
+ UlValidateParameters3( IXPLogon_TransportNotify, a1, a2, a3 )
+#define CheckParameters_IXPLogon_TransportNotify( a1, a2, a3 ) \
+ CheckParameters3( IXPLogon_TransportNotify, a1, a2, a3 )
+
+#define Validate_IXPLogon_Idle( a1, a2 ) \
+ ValidateParameters2( IXPLogon_Idle, a1, a2 )
+#define UlValidate_IXPLogon_Idle( a1, a2 ) \
+ UlValidateParameters2( IXPLogon_Idle, a1, a2 )
+#define CheckParameters_IXPLogon_Idle( a1, a2 ) \
+ CheckParameters2( IXPLogon_Idle, a1, a2 )
+
+#define Validate_IXPLogon_TransportLogoff( a1, a2 ) \
+ ValidateParameters2( IXPLogon_TransportLogoff, a1, a2 )
+#define UlValidate_IXPLogon_TransportLogoff( a1, a2 ) \
+ UlValidateParameters2( IXPLogon_TransportLogoff, a1, a2 )
+#define CheckParameters_IXPLogon_TransportLogoff( a1, a2 ) \
+ CheckParameters2( IXPLogon_TransportLogoff, a1, a2 )
+
+#define Validate_IXPLogon_SubmitMessage( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IXPLogon_SubmitMessage, a1, a2, a3, a4, a5 )
+#define UlValidate_IXPLogon_SubmitMessage( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IXPLogon_SubmitMessage, a1, a2, a3, a4, a5 )
+#define CheckParameters_IXPLogon_SubmitMessage( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IXPLogon_SubmitMessage, a1, a2, a3, a4, a5 )
+
+#define Validate_IXPLogon_EndMessage( a1, a2, a3 ) \
+ ValidateParameters3( IXPLogon_EndMessage, a1, a2, a3 )
+#define UlValidate_IXPLogon_EndMessage( a1, a2, a3 ) \
+ UlValidateParameters3( IXPLogon_EndMessage, a1, a2, a3 )
+#define CheckParameters_IXPLogon_EndMessage( a1, a2, a3 ) \
+ CheckParameters3( IXPLogon_EndMessage, a1, a2, a3 )
+
+#define Validate_IXPLogon_Poll( a1, a2 ) \
+ ValidateParameters2( IXPLogon_Poll, a1, a2 )
+#define UlValidate_IXPLogon_Poll( a1, a2 ) \
+ UlValidateParameters2( IXPLogon_Poll, a1, a2 )
+#define CheckParameters_IXPLogon_Poll( a1, a2 ) \
+ CheckParameters2( IXPLogon_Poll, a1, a2 )
+
+#define Validate_IXPLogon_StartMessage( a1, a2, a3, a4 ) \
+ ValidateParameters4( IXPLogon_StartMessage, a1, a2, a3, a4 )
+#define UlValidate_IXPLogon_StartMessage( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IXPLogon_StartMessage, a1, a2, a3, a4 )
+#define CheckParameters_IXPLogon_StartMessage( a1, a2, a3, a4 ) \
+ CheckParameters4( IXPLogon_StartMessage, a1, a2, a3, a4 )
+
+#define Validate_IXPLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IXPLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define UlValidate_IXPLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IXPLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define CheckParameters_IXPLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IXPLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+
+#define Validate_IXPLogon_ValidateState( a1, a2, a3 ) \
+ ValidateParameters3( IXPLogon_ValidateState, a1, a2, a3 )
+#define UlValidate_IXPLogon_ValidateState( a1, a2, a3 ) \
+ UlValidateParameters3( IXPLogon_ValidateState, a1, a2, a3 )
+#define CheckParameters_IXPLogon_ValidateState( a1, a2, a3 ) \
+ CheckParameters3( IXPLogon_ValidateState, a1, a2, a3 )
+
+#define Validate_IXPLogon_FlushQueues( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IXPLogon_FlushQueues, a1, a2, a3, a4, a5 )
+#define UlValidate_IXPLogon_FlushQueues( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IXPLogon_FlushQueues, a1, a2, a3, a4, a5 )
+#define CheckParameters_IXPLogon_FlushQueues( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IXPLogon_FlushQueues, a1, a2, a3, a4, a5 )
+
+
+/* IMSProvider */
+
+#define Validate_IMSProvider_Shutdown( a1, a2 ) \
+ ValidateParameters2( IMSProvider_Shutdown, a1, a2 )
+#define UlValidate_IMSProvider_Shutdown( a1, a2 ) \
+ UlValidateParameters2( IMSProvider_Shutdown, a1, a2 )
+#define CheckParameters_IMSProvider_Shutdown( a1, a2 ) \
+ CheckParameters2( IMSProvider_Shutdown, a1, a2 )
+
+#define Validate_IMSProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ ValidateParameters13( IMSProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+#define UlValidate_IMSProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ UlValidateParameters13( IMSProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+#define CheckParameters_IMSProvider_Logon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ CheckParameters13( IMSProvider_Logon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+
+#define Validate_IMSProvider_SpoolerLogon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ ValidateParameters13( IMSProvider_SpoolerLogon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+#define UlValidate_IMSProvider_SpoolerLogon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ UlValidateParameters13( IMSProvider_SpoolerLogon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+#define CheckParameters_IMSProvider_SpoolerLogon( a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 ) \
+ CheckParameters13( IMSProvider_SpoolerLogon, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 )
+
+#define Validate_IMSProvider_CompareStoreIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMSProvider_CompareStoreIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMSProvider_CompareStoreIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMSProvider_CompareStoreIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMSProvider_CompareStoreIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMSProvider_CompareStoreIDs, a1, a2, a3, a4, a5, a6, a7 )
+
+
+/* IMSLogon */
+
+#define Validate_IMSLogon_GetLastError( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMSLogon_GetLastError, a1, a2, a3, a4 )
+#define UlValidate_IMSLogon_GetLastError( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMSLogon_GetLastError, a1, a2, a3, a4 )
+#define CheckParameters_IMSLogon_GetLastError( a1, a2, a3, a4 ) \
+ CheckParameters4( IMSLogon_GetLastError, a1, a2, a3, a4 )
+
+#define Validate_IMSLogon_Logoff( a1, a2 ) \
+ ValidateParameters2( IMSLogon_Logoff, a1, a2 )
+#define UlValidate_IMSLogon_Logoff( a1, a2 ) \
+ UlValidateParameters2( IMSLogon_Logoff, a1, a2 )
+#define CheckParameters_IMSLogon_Logoff( a1, a2 ) \
+ CheckParameters2( IMSLogon_Logoff, a1, a2 )
+
+#define Validate_IMSLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMSLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMSLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMSLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMSLogon_OpenEntry( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMSLogon_OpenEntry, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMSLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ ValidateParameters7( IMSLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define UlValidate_IMSLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ UlValidateParameters7( IMSLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+#define CheckParameters_IMSLogon_CompareEntryIDs( a1, a2, a3, a4, a5, a6, a7 ) \
+ CheckParameters7( IMSLogon_CompareEntryIDs, a1, a2, a3, a4, a5, a6, a7 )
+
+#define Validate_IMSLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ ValidateParameters6( IMSLogon_Advise, a1, a2, a3, a4, a5, a6 )
+#define UlValidate_IMSLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ UlValidateParameters6( IMSLogon_Advise, a1, a2, a3, a4, a5, a6 )
+#define CheckParameters_IMSLogon_Advise( a1, a2, a3, a4, a5, a6 ) \
+ CheckParameters6( IMSLogon_Advise, a1, a2, a3, a4, a5, a6 )
+
+#define Validate_IMSLogon_Unadvise( a1, a2 ) \
+ ValidateParameters2( IMSLogon_Unadvise, a1, a2 )
+#define UlValidate_IMSLogon_Unadvise( a1, a2 ) \
+ UlValidateParameters2( IMSLogon_Unadvise, a1, a2 )
+#define CheckParameters_IMSLogon_Unadvise( a1, a2 ) \
+ CheckParameters2( IMSLogon_Unadvise, a1, a2 )
+
+#define Validate_IMSLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMSLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define UlValidate_IMSLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMSLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMSLogon_OpenStatusEntry( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMSLogon_OpenStatusEntry, a1, a2, a3, a4, a5 )
+
+
+/* IMAPIControl */
+
+#define Validate_IMAPIControl_GetLastError( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIControl_GetLastError, a1, a2, a3, a4 )
+#define UlValidate_IMAPIControl_GetLastError( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIControl_GetLastError, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIControl_GetLastError( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIControl_GetLastError, a1, a2, a3, a4 )
+
+#define Validate_IMAPIControl_Activate( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIControl_Activate, a1, a2, a3 )
+#define UlValidate_IMAPIControl_Activate( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIControl_Activate, a1, a2, a3 )
+#define CheckParameters_IMAPIControl_Activate( a1, a2, a3 ) \
+ CheckParameters3( IMAPIControl_Activate, a1, a2, a3 )
+
+#define Validate_IMAPIControl_GetState( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIControl_GetState, a1, a2, a3 )
+#define UlValidate_IMAPIControl_GetState( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIControl_GetState, a1, a2, a3 )
+#define CheckParameters_IMAPIControl_GetState( a1, a2, a3 ) \
+ CheckParameters3( IMAPIControl_GetState, a1, a2, a3 )
+
+
+/* IMAPIStatus */
+
+#define Validate_IMAPIStatus_ValidateState( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIStatus_ValidateState, a1, a2, a3 )
+#define UlValidate_IMAPIStatus_ValidateState( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIStatus_ValidateState, a1, a2, a3 )
+#define CheckParameters_IMAPIStatus_ValidateState( a1, a2, a3 ) \
+ CheckParameters3( IMAPIStatus_ValidateState, a1, a2, a3 )
+
+#define Validate_IMAPIStatus_SettingsDialog( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIStatus_SettingsDialog, a1, a2, a3 )
+#define UlValidate_IMAPIStatus_SettingsDialog( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIStatus_SettingsDialog, a1, a2, a3 )
+#define CheckParameters_IMAPIStatus_SettingsDialog( a1, a2, a3 ) \
+ CheckParameters3( IMAPIStatus_SettingsDialog, a1, a2, a3 )
+
+#define Validate_IMAPIStatus_ChangePassword( a1, a2, a3, a4 ) \
+ ValidateParameters4( IMAPIStatus_ChangePassword, a1, a2, a3, a4 )
+#define UlValidate_IMAPIStatus_ChangePassword( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IMAPIStatus_ChangePassword, a1, a2, a3, a4 )
+#define CheckParameters_IMAPIStatus_ChangePassword( a1, a2, a3, a4 ) \
+ CheckParameters4( IMAPIStatus_ChangePassword, a1, a2, a3, a4 )
+
+#define Validate_IMAPIStatus_FlushQueues( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IMAPIStatus_FlushQueues, a1, a2, a3, a4, a5 )
+#define UlValidate_IMAPIStatus_FlushQueues( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IMAPIStatus_FlushQueues, a1, a2, a3, a4, a5 )
+#define CheckParameters_IMAPIStatus_FlushQueues( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IMAPIStatus_FlushQueues, a1, a2, a3, a4, a5 )
+
+
+/* IStream */
+
+#define Validate_IStream_Read( a1, a2, a3, a4 ) \
+ ValidateParameters4( IStream__Read, a1, a2, a3, a4 )
+#define UlValidate_IStream_Read( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IStream__Read, a1, a2, a3, a4 )
+#define CheckParameters_IStream_Read( a1, a2, a3, a4 ) \
+ CheckParameters4( IStream__Read, a1, a2, a3, a4 )
+
+#define Validate_IStream_Write( a1, a2, a3, a4 ) \
+ ValidateParameters4( IStream__Write, a1, a2, a3, a4 )
+#define UlValidate_IStream_Write( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IStream__Write, a1, a2, a3, a4 )
+#define CheckParameters_IStream_Write( a1, a2, a3, a4 ) \
+ CheckParameters4( IStream__Write, a1, a2, a3, a4 )
+
+#define Validate_IStream_Seek( a1, a2, a3, a4 ) \
+ ValidateParameters4( IStream_Seek, a1, a2, a3, a4 )
+#define UlValidate_IStream_Seek( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IStream_Seek, a1, a2, a3, a4 )
+#define CheckParameters_IStream_Seek( a1, a2, a3, a4 ) \
+ CheckParameters4( IStream_Seek, a1, a2, a3, a4 )
+
+#define Validate_IStream_SetSize( a1, a2 ) \
+ ValidateParameters2( IStream_SetSize, a1, a2 )
+#define UlValidate_IStream_SetSize( a1, a2 ) \
+ UlValidateParameters2( IStream_SetSize, a1, a2 )
+#define CheckParameters_IStream_SetSize( a1, a2 ) \
+ CheckParameters2( IStream_SetSize, a1, a2 )
+
+#define Validate_IStream_CopyTo( a1, a2, a3, a4, a5 ) \
+ ValidateParameters5( IStream_CopyTo, a1, a2, a3, a4, a5 )
+#define UlValidate_IStream_CopyTo( a1, a2, a3, a4, a5 ) \
+ UlValidateParameters5( IStream_CopyTo, a1, a2, a3, a4, a5 )
+#define CheckParameters_IStream_CopyTo( a1, a2, a3, a4, a5 ) \
+ CheckParameters5( IStream_CopyTo, a1, a2, a3, a4, a5 )
+
+#define Validate_IStream_Commit( a1, a2 ) \
+ ValidateParameters2( IStream_Commit, a1, a2 )
+#define UlValidate_IStream_Commit( a1, a2 ) \
+ UlValidateParameters2( IStream_Commit, a1, a2 )
+#define CheckParameters_IStream_Commit( a1, a2 ) \
+ CheckParameters2( IStream_Commit, a1, a2 )
+
+#define Validate_IStream_Revert( a1 ) \
+ ValidateParameters1( IStream_Revert, a1 )
+#define UlValidate_IStream_Revert( a1 ) \
+ UlValidateParameters1( IStream_Revert, a1 )
+#define CheckParameters_IStream_Revert( a1 ) \
+ CheckParameters1( IStream_Revert, a1 )
+
+#define Validate_IStream_LockRegion( a1, a2, a3, a4 ) \
+ ValidateParameters4( IStream_LockRegion, a1, a2, a3, a4 )
+#define UlValidate_IStream_LockRegion( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IStream_LockRegion, a1, a2, a3, a4 )
+#define CheckParameters_IStream_LockRegion( a1, a2, a3, a4 ) \
+ CheckParameters4( IStream_LockRegion, a1, a2, a3, a4 )
+
+#define Validate_IStream_UnlockRegion( a1, a2, a3, a4 ) \
+ ValidateParameters4( IStream_UnlockRegion, a1, a2, a3, a4 )
+#define UlValidate_IStream_UnlockRegion( a1, a2, a3, a4 ) \
+ UlValidateParameters4( IStream_UnlockRegion, a1, a2, a3, a4 )
+#define CheckParameters_IStream_UnlockRegion( a1, a2, a3, a4 ) \
+ CheckParameters4( IStream_UnlockRegion, a1, a2, a3, a4 )
+
+#define Validate_IStream_Stat( a1, a2, a3 ) \
+ ValidateParameters3( IStream_Stat, a1, a2, a3 )
+#define UlValidate_IStream_Stat( a1, a2, a3 ) \
+ UlValidateParameters3( IStream_Stat, a1, a2, a3 )
+#define CheckParameters_IStream_Stat( a1, a2, a3 ) \
+ CheckParameters3( IStream_Stat, a1, a2, a3 )
+
+#define Validate_IStream_Clone( a1, a2 ) \
+ ValidateParameters2( IStream_Clone, a1, a2 )
+#define UlValidate_IStream_Clone( a1, a2 ) \
+ UlValidateParameters2( IStream_Clone, a1, a2 )
+#define CheckParameters_IStream_Clone( a1, a2 ) \
+ CheckParameters2( IStream_Clone, a1, a2 )
+
+
+/* IMAPIAdviseSink */
+
+#define Validate_IMAPIAdviseSink_OnNotify( a1, a2, a3 ) \
+ ValidateParameters3( IMAPIAdviseSink_OnNotify, a1, a2, a3 )
+#define UlValidate_IMAPIAdviseSink_OnNotify( a1, a2, a3 ) \
+ UlValidateParameters3( IMAPIAdviseSink_OnNotify, a1, a2, a3 )
+#define CheckParameters_IMAPIAdviseSink_OnNotify( a1, a2, a3 ) \
+ CheckParameters3( IMAPIAdviseSink_OnNotify, a1, a2, a3 )
+
+#if defined(_AMD64_) || defined(_X86_)
+STDAPI HrValidateParameters( METHODS eMethod, LPVOID FAR *ppFirstArg );
+#elif defined(DOS) || defined(_MAC)
+STDAPIV HrValidateParametersV( METHODS eMethod, ... );
+STDAPIV HrValidateParametersValist( METHODS eMethod, va_list arglist );
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _INC_VALIDATE */
+
+
diff --git a/comm/mailnews/mapi/include/mapiwin.h b/comm/mailnews/mapi/include/mapiwin.h
new file mode 100644
index 0000000000..6dc00c45ee
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiwin.h
@@ -0,0 +1,274 @@
+/*
+ * M A P I W I N . H
+ *
+ * Definitions used by the MAPI Development Team to aid in
+ * developing single-source service providers that run on
+ * both WIN32 and WIN16 platforms.
+ * There are three sections.
+ *
+ * The first section defines how to call something that
+ * is available by different methods in WIN16 vs. WIN32.
+ * As such, they are totally new mechanisms.
+ *
+ * The second section establishes things that are available
+ * AS-IS in one environment but we have to define for the
+ * other environment.
+ *
+ * The third section simply defines a few conventions
+ * (simplifications) for common operations.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+/*
+ * Routines are included in the first section to manage per-instance
+ * global variables for DLLs. They assume that all of the DLL's
+ * per-instance global variables live in a single block of memory.
+ * Functions are provided to install and retrieve the correct block of
+ * memory for the current instance.
+ *
+ * There are only two functions:
+ *
+ * PvGetInstanceGlobals Call this to get the address of the
+ * per-instance globals structure.
+ * ScSetinstanceGlobals Call this to install the
+ * per-instance globals structure. It
+ * may fail if the number of instances
+ * exceeds a certain limit.
+ *
+ * The caller is free to choose the name, size, and allocation
+ * method of the per-instance global variables structure.
+ *
+ * The WIN32 implementation uses a pointer in the DLL's data
+ * segment. This assumes that the DLL gets a separate instance
+ * of the default data segment per calling process.
+ *
+ * The WIN16 implementation uses a fixed array of pointers and a
+ * matching fixed array of keys unique to the calling process.
+ */
+
+/*
+ * The second section consists largely of Win32 file I/O functions
+ * that are not supported under Win16. These functions are
+ * implemented in mapiwin.c, using DOS calls. Most have limitations
+ * relative to their Win32 counterparts, which are spelled out in
+ * the comments to the source code.
+ */
+
+#ifndef __MAPIWIN_H__
+#define __MAPIWIN_H__
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if defined (WIN32) && !defined (_WIN32)
+#define _WIN32
+#endif
+
+#include "mapinls.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#if defined(_MAC)
+
+#define MULDIV(x,y,z) MulDiv(x,y,z)
+
+LPVOID FAR PASCAL PvGetInstanceGlobals(WORD wDataSet);
+LONG FAR PASCAL ScSetInstanceGlobals(LPVOID pv, WORD wDataSet);
+LONG FAR PASCAL ScSetVerifyInstanceGlobals(LPVOID pv, DWORD dwPid,
+ WORD wDataSet);
+LPVOID FAR PASCAL PvGetVerifyInstanceGlobals(DWORD dwPid, DWORD wDataSet);
+LPVOID FAR PASCAL PvSlowGetInstanceGlobals(DWORD dwPid, DWORD wDataSet);
+BOOL FAR PASCAL FCleanupInstanceGlobals(WORD, DWORD);
+
+#elif defined(_WIN64) || defined(_WIN32)
+
+#define MULDIV(x,y,z) MulDiv(x,y,z)
+
+extern LPVOID pinstX;
+#define PvGetInstanceGlobals() pinstX
+#define ScSetInstanceGlobals(_pv) (pinstX = _pv, 0)
+#define PvGetVerifyInstanceGlobals(_pid) pinstX
+#define ScSetVerifyInstanceGlobals(_pv,_pid) (pinstX = _pv, 0)
+#define PvSlowGetInstanceGlobals(_pid) pinstX
+
+#else
+#error "Unknown Platform: MAPI is currently supported on Win32 and Win64"
+#endif
+
+#if (defined(_WIN64) || defined(_WIN32)) && !defined(_MAC)
+#define szMAPIDLLSuffix "32"
+#elif defined(DOS)
+#define szMAPIDLLSuffix ""
+#elif defined(_MAC)
+#define szMAPIDLLSuffix "M"
+#else
+#error "Don't know the suffix for DLLs on this platform"
+#endif
+
+/********************************/
+/* Things missing from one */
+/* system-provided environment */
+/* or the other. */
+/********************************/
+
+#if !defined(_WIN64) && !defined(_WIN32)
+#define ZeroMemory(pb,cb) memset((pb),0,(cb))
+#define FillMemory(pb,cb,b) memset((pb),(b),(cb))
+#define CopyMemory(pbDst,pbSrc,cb) do \
+ { \
+ size_t _cb = (size_t)(cb); \
+ if (_cb) \
+ memcpy(pbDst,pbSrc,_cb);\
+ } while (FALSE)
+#define MoveMemory(pbDst,pbSrc,cb) memmove((pbDst),(pbSrc),(cb))
+
+#define UNALIGNED
+
+#endif
+
+#if defined(_MAC)
+
+typedef int INT;
+typedef unsigned long ULONG;
+typedef short SHORT;
+typedef unsigned short USHORT;
+typedef double LONGLONG;
+typedef double DWORDLONG;
+typedef unsigned char UCHAR;
+typedef unsigned char FAR* PUCHAR;
+typedef int BOOL;
+
+
+/* Synchronization */
+#define InterlockedIncrement(plong) (++(*(plong)))
+#define InterlockedDecrement(plong) (--(*(plong)))
+
+#ifndef CreateMutex
+#define CreateMutexA CreateMutex
+#define CreateMutexW CreateMutex
+#define CreateMutex(pv, bool, sz) (INVALID_HANDLE_VALUE)
+#endif
+
+#define WaitForSingleObject(hObj, dw) ((void)0)
+#define ReleaseMutex(hObj) ((BOOL)1)
+#define CloseMutexHandle(hObj) TRUE
+
+#define CRITICAL_SECTION ULONG
+#define InitializeCriticalSection(_pcs) ((void)0)
+#define DeleteCriticalSection(_pcs) ((void)0)
+#define EnterCriticalSection(_pcs) ((void)0)
+#define LeaveCriticalSection(_pcs) ((void)0)
+
+#define MAX_PATH 260
+
+#define FILE_FLAG_SEQUENTIAL_SCAN 0x08000000
+
+#define CREATE_NEW 1
+#define CREATE_ALWAYS 2
+#define OPEN_EXISTING 3
+#define OPEN_ALWAYS 4
+#define TRUNCATE_EXISTING 5
+
+#define FILE_ATTRIBUTE_READONLY 0x00000001
+#define FILE_ATTRIBUTE_HIDDEN 0x00000002
+#define FILE_ATTRIBUTE_SYSTEM 0x00000004
+#define FILE_ATTRIBUTE_DIRECTORY 0x00000010
+#define FILE_ATTRIBUTE_ARCHIVE 0x00000020
+#define FILE_ATTRIBUTE_NORMAL 0x00000080
+#define FILE_ATTRIBUTE_TEMPORARY 0x00000100
+
+#define FILE_FLAG_WRITE_THROUGH 0x80000000
+#define FILE_FLAG_RANDOM_ACCESS 0x10000000
+
+#define TIME_ZONE_ID_UNKNOWN 0
+#define TIME_ZONE_ID_STANDARD 1
+#define TIME_ZONE_ID_DAYLIGHT 2
+
+
+
+DWORD WINAPI GetLastError(void);
+DWORD WINAPI GetFileAttributes(LPCSTR lpFileName);
+DWORD WINAPI GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh);
+BOOL WINAPI GetFileTime(HANDLE hFile, FILETIME FAR *lpftCreation,
+ FILETIME FAR *lpftLastAccess, FILETIME FAR *lpftLastWrite);
+BOOL WINAPI SetFileTime(HANDLE hFile, const FILETIME FAR *lpftCreation,
+ const FILETIME FAR *lpftLastAccess,
+ const FILETIME FAR *lpftLastWrite);
+DWORD WINAPI SetFilePointer(HANDLE hFile, LONG lDistanceToMove,
+ LONG FAR *lpDistanceToMoveHigh, DWORD dwMoveMethod);
+BOOL WINAPI SetEndOfFile(HANDLE hFile);
+BOOL WINAPI CloseHandle(HANDLE hObject);
+DWORD WINAPI GetTempPath(DWORD nBufferLength, LPSTR lpBuffer);
+UINT WINAPI GetTempFileName32 (LPCSTR lpPathName, LPCSTR lpPrefixString,
+ UINT uUnique, LPSTR lpTempFileName);
+BOOL WINAPI DeleteFile(LPCSTR lpFileName);
+BOOL WINAPI RemoveDirectory(LPCSTR lpPathName);
+BOOL WINAPI CopyFile(LPCSTR szSrc, LPCSTR szDst, BOOL fFailIfExists);
+BOOL WINAPI MoveFile(LPCSTR lpExistingFileName, LPCSTR lpNewFileName);
+HANDLE WINAPI FindFirstFile(LPCSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData);
+BOOL WINAPI FindNextFile(HANDLE hFindFile, LPWIN32_FIND_DATA lpFindFileData);
+BOOL WINAPI FindClose(HANDLE hFindFile);
+DWORD WINAPI GetFullPathName(LPCSTR lpFileName, DWORD nBufferLength,
+ LPSTR lpBuffer, LPSTR *lpFilePart);
+void WINAPI Sleep(DWORD dwMilliseconds);
+LONG WINAPI CompareFileTime(const FILETIME FAR *, const FILETIME FAR *);
+BOOL WINAPI LocalFileTimeToFileTime(const FILETIME FAR *, FILETIME FAR *);
+BOOL WINAPI FileTimeToLocalFileTime(const FILETIME FAR *, FILETIME FAR *);
+BOOL WINAPI FileTimeToSystemTime(const FILETIME FAR *, SYSTEMTIME FAR *);
+BOOL WINAPI SystemTimeToFileTime(const SYSTEMTIME FAR *, FILETIME FAR *);
+void WINAPI GetSystemTime(SYSTEMTIME FAR *);
+void WINAPI GetLocalTime(SYSTEMTIME FAR *);
+BOOL WINAPI FileTimeToDosDateTime(const FILETIME FAR * lpFileTime,
+ WORD FAR *lpFatDate, WORD FAR *lpFatTime);
+BOOL WINAPI DosDateTimeToFileTime(WORD wFatDate, WORD wFatTime,
+ FILETIME FAR * lpFileTime);
+DWORD WINAPI GetTimeZoneInformation(
+ LPTIME_ZONE_INFORMATION lpTimeZoneInformation);
+BOOL WINAPI SetTimeZoneInformation(
+ const TIME_ZONE_INFORMATION FAR *lpTimeZoneInformation);
+
+DWORD WINAPI GetCurrentProcessId(void);
+long WINAPI MulDiv32(long, long, long);
+
+#else /* _MAC */
+
+/* Remaps GetTempFileName32() to the real 32bit version */
+
+#define GetTempFileName32(_szPath,_szPfx,_n,_lpbuf) GetTempFileName(_szPath,_szPfx,_n,_lpbuf)
+
+#define CloseMutexHandle CloseHandle
+
+#endif /* _MAC */
+
+
+#ifdef _MAC
+#define CRITICAL_SECTION ULONG
+#define InitializeCriticalSection(_pcs) ((void)0)
+#define DeleteCriticalSection(_pcs) ((void)0)
+#define EnterCriticalSection(_pcs) ((void)0)
+#define LeaveCriticalSection(_pcs) ((void)0)
+#endif
+
+/********************************/
+/* Our private conventions */
+/* (common to WIN32/WIN64) */
+/********************************/
+
+#define Cbtszsize(_a) ((lstrlen(_a)+1)*sizeof(TCHAR))
+#define CbtszsizeA(_a) ((lstrlenA(_a) + 1))
+#define CbtszsizeW(_a) ((lstrlenW(_a) + 1) * sizeof(WCHAR))
+#define HexCchOf(_s) (sizeof(_s)*2+1)
+#define HexSizeOf(_s) (HexCchOf(_s)*sizeof(TCHAR))
+
+BOOL WINAPI IsBadBoundedStringPtr(const void FAR* lpsz, UINT cchMax);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __MAPIWIN_H__ */
diff --git a/comm/mailnews/mapi/include/mapiwz.h b/comm/mailnews/mapi/include/mapiwz.h
new file mode 100644
index 0000000000..0ebe69fcbf
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapiwz.h
@@ -0,0 +1,73 @@
+/*
+ * M A P I W Z . H
+ *
+ * Definitions for the Profile Wizard. Includes all prototypes
+ * and constants required by the provider-wizard code consumers.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _MAPIWZ_H
+#define _MAPIWZ_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#define WIZ_QUERYNUMPAGES (WM_USER +10)
+#define WIZ_NEXT (WM_USER +11)
+#define WIZ_PREV (WM_USER +12)
+/*
+ * NOTE: Provider-Wizards should not use ids ranging between
+ * (WM_USER + 1) and (WM_USER +20) as these have been reserved for
+ * future releases.
+ */
+
+/* Flags for LaunchWizard API */
+
+#define MAPI_PW_FIRST_PROFILE 0x00000001
+#define MAPI_PW_LAUNCHED_BY_CONFIG 0x00000002
+#define MAPI_PW_ADD_SERVICE_ONLY 0x00000004
+#define MAPI_PW_PROVIDER_UI_ONLY 0x00000008
+#define MAPI_PW_HIDE_SERVICES_LIST 0x00000010
+
+/*
+ * Provider should set this property to TRUE if it does not
+ * want the Profile Wizard to display the PST setup page.
+ */
+#define PR_WIZARD_NO_PST_PAGE PROP_TAG(PT_BOOLEAN, 0x6700)
+#define PR_WIZARD_NO_PAB_PAGE PROP_TAG(PT_BOOLEAN, 0x6701)
+
+typedef HRESULT (STDAPICALLTYPE LAUNCHWIZARDENTRY)
+(
+ HWND hParentWnd,
+ ULONG ulFlags,
+ LPCTSTR FAR * lppszServiceNameToAdd,
+ ULONG cbBufferMax,
+ LPTSTR lpszNewProfileName
+);
+typedef LAUNCHWIZARDENTRY FAR * LPLAUNCHWIZARDENTRY;
+
+typedef BOOL (STDAPICALLTYPE SERVICEWIZARDDLGPROC)
+(
+ HWND hDlg,
+ UINT wMsgID,
+ WPARAM wParam,
+ LPARAM lParam
+);
+typedef SERVICEWIZARDDLGPROC FAR * LPSERVICEWIZARDDLGPROC;
+
+typedef ULONG (STDAPICALLTYPE WIZARDENTRY)
+(
+ HINSTANCE hProviderDLLInstance,
+ LPTSTR FAR * lppcsResourceName,
+ DLGPROC FAR * lppDlgProc,
+ LPMAPIPROP lpMapiProp,
+ LPVOID lpMapiSupportObject
+);
+typedef WIZARDENTRY FAR * LPWIZARDENTRY;
+
+#define LAUNCHWIZARDENTRYNAME "LAUNCHWIZARD"
+
+#endif /* _MAPIWZ_H */
+
diff --git a/comm/mailnews/mapi/include/mapix.h b/comm/mailnews/mapi/include/mapix.h
new file mode 100644
index 0000000000..d9694bd565
--- /dev/null
+++ b/comm/mailnews/mapi/include/mapix.h
@@ -0,0 +1,545 @@
+/*
+ * M A P I X . H
+ *
+ * Definitions of objects/flags, etc used by Extended MAPI.
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef MAPIX_H
+#define MAPIX_H
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+/* Include common MAPI header files if they haven't been already. */
+#ifndef MAPIDEFS_H
+#include "mapidefs.h"
+#endif
+#ifndef MAPICODE_H
+#include "mapicode.h"
+#endif
+#ifndef MAPIGUID_H
+#include "mapiguid.h"
+#endif
+#ifndef MAPITAGS_H
+#include "mapitags.h"
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef BEGIN_INTERFACE
+#define BEGIN_INTERFACE
+#endif
+
+/* Forward interface declarations */
+
+DECLARE_MAPI_INTERFACE_PTR(IProfAdmin, LPPROFADMIN);
+DECLARE_MAPI_INTERFACE_PTR(IMsgServiceAdmin, LPSERVICEADMIN);
+DECLARE_MAPI_INTERFACE_PTR(IMAPISession, LPMAPISESSION);
+
+/* ------------------------------------------------------ */
+/* shared with simple mapi */
+
+typedef ULONG FLAGS;
+
+/* MAPILogon() flags. */
+
+#define MAPI_LOGON_UI 0x00000001 /* Display logon UI */
+#define MAPI_NEW_SESSION 0x00000002 /* Don't use shared session */
+#define MAPI_ALLOW_OTHERS 0x00000008 /* Make this a shared session */
+#define MAPI_EXPLICIT_PROFILE 0x00000010 /* Don't use default profile */
+#define MAPI_EXTENDED 0x00000020 /* Extended MAPI Logon */
+#define MAPI_FORCE_DOWNLOAD 0x00001000 /* Get new mail before return */
+#define MAPI_SERVICE_UI_ALWAYS 0x00002000 /* Do logon UI in all providers */
+#define MAPI_NO_MAIL 0x00008000 /* Do not activate transports */
+/* #define MAPI_NT_SERVICE 0x00010000 Allow logon from an NT service */
+#ifndef MAPI_PASSWORD_UI
+#define MAPI_PASSWORD_UI 0x00020000 /* Display password UI only */
+#endif
+#define MAPI_TIMEOUT_SHORT 0x00100000 /* Minimal wait for logon resources */
+
+#define MAPI_SIMPLE_DEFAULT (MAPI_LOGON_UI | MAPI_FORCE_DOWNLOAD | MAPI_ALLOW_OTHERS)
+#define MAPI_SIMPLE_EXPLICIT (MAPI_NEW_SESSION | MAPI_FORCE_DOWNLOAD | MAPI_EXPLICIT_PROFILE)
+
+/* Structure passed to MAPIInitialize(), and its ulFlags values */
+
+typedef struct
+{
+ ULONG ulVersion;
+ ULONG ulFlags;
+} MAPIINIT_0, FAR *LPMAPIINIT_0;
+
+typedef MAPIINIT_0 MAPIINIT;
+typedef MAPIINIT FAR *LPMAPIINIT;
+
+#define MAPI_INIT_VERSION 0
+
+#define MAPI_MULTITHREAD_NOTIFICATIONS 0x00000001
+/* Reserved for MAPI 0x40000000 */
+/* #define MAPI_NT_SERVICE 0x00010000 Use from NT service */
+
+/* MAPI base functions */
+
+typedef HRESULT (STDAPICALLTYPE MAPIINITIALIZE)(
+ LPVOID lpMapiInit
+);
+typedef MAPIINITIALIZE FAR *LPMAPIINITIALIZE;
+
+typedef void (STDAPICALLTYPE MAPIUNINITIALIZE)(void);
+typedef MAPIUNINITIALIZE FAR *LPMAPIUNINITIALIZE;
+
+MAPIINITIALIZE MAPIInitialize;
+MAPIUNINITIALIZE MAPIUninitialize;
+
+
+/* Extended MAPI Logon function */
+
+
+typedef HRESULT (STDMETHODCALLTYPE MAPILOGONEX)(
+ ULONG_PTR ulUIParam,
+ /*OFFICEDEV add _opt*/ __in_opt LPTSTR lpszProfileName,
+ /*OFFICEDEV add _opt*/ __in_opt LPTSTR lpszPassword,
+ ULONG ulFlags, /* ulFlags takes all that SimpleMAPI does + MAPI_UNICODE */
+ LPMAPISESSION FAR * lppSession
+);
+typedef MAPILOGONEX FAR *LPMAPILOGONEX;
+
+MAPILOGONEX MAPILogonEx;
+
+
+typedef SCODE (STDMETHODCALLTYPE MAPIALLOCATEBUFFER)(
+ ULONG cbSize,
+ LPVOID FAR * lppBuffer
+);
+
+typedef SCODE (STDMETHODCALLTYPE MAPIALLOCATEMORE)(
+ ULONG cbSize,
+ LPVOID lpObject,
+ LPVOID FAR * lppBuffer
+);
+
+typedef ULONG (STDAPICALLTYPE MAPIFREEBUFFER)(
+ LPVOID lpBuffer
+);
+
+typedef MAPIALLOCATEBUFFER FAR *LPMAPIALLOCATEBUFFER;
+typedef MAPIALLOCATEMORE FAR *LPMAPIALLOCATEMORE;
+typedef MAPIFREEBUFFER FAR *LPMAPIFREEBUFFER;
+
+MAPIALLOCATEBUFFER MAPIAllocateBuffer;
+MAPIALLOCATEMORE MAPIAllocateMore;
+MAPIFREEBUFFER MAPIFreeBuffer;
+
+typedef HRESULT (STDMETHODCALLTYPE MAPIADMINPROFILES)(
+ ULONG ulFlags,
+ LPPROFADMIN FAR *lppProfAdmin
+);
+
+typedef MAPIADMINPROFILES FAR *LPMAPIADMINPROFILES;
+
+MAPIADMINPROFILES MAPIAdminProfiles;
+
+/* IMAPISession Interface -------------------------------------------------- */
+
+/* Flags for OpenEntry and others */
+
+/*#define MAPI_MODIFY ((ULONG) 0x00000001) */
+
+/* Flags for Logoff */
+
+#define MAPI_LOGOFF_SHARED 0x00000001 /* Close all shared sessions */
+#define MAPI_LOGOFF_UI 0x00000002 /* It's OK to present UI */
+
+/* Flags for SetDefaultStore. They are mutually exclusive. */
+
+#define MAPI_DEFAULT_STORE 0x00000001 /* for incoming messages */
+#define MAPI_SIMPLE_STORE_TEMPORARY 0x00000002 /* for simple MAPI and CMC */
+#define MAPI_SIMPLE_STORE_PERMANENT 0x00000003 /* for simple MAPI and CMC */
+#define MAPI_PRIMARY_STORE 0x00000004 /* Used by some clients */
+#define MAPI_SECONDARY_STORE 0x00000005 /* Used by some clients */
+
+/* Flags for ShowForm. */
+
+#define MAPI_POST_MESSAGE 0x00000001 /* Selects post/send semantics */
+#define MAPI_NEW_MESSAGE 0x00000002 /* Governs copying during submission */
+
+/* MessageOptions */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* QueryDefaultMessageOpt */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+#define MAPI_IMAPISESSION_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetMsgStoresTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(OpenMsgStore) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPMDB FAR * lppMDB) IPURE; \
+ MAPIMETHOD(OpenAddressBook) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPADRBOOK FAR * lppAdrBook) IPURE; \
+ MAPIMETHOD(OpenProfileSection) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPPROFSECT FAR * lppProfSect) IPURE; \
+ MAPIMETHOD(GetStatusTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(MessageOptions) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPTSTR lpszAdrType, \
+ LPMESSAGE lpMessage) IPURE; \
+ MAPIMETHOD(QueryDefaultMessageOpt) \
+ (THIS_ LPTSTR lpszAdrType, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcValues, \
+ LPSPropValue FAR * lppOptions) IPURE; \
+ MAPIMETHOD(EnumAdrTypes) \
+ (THIS_ ULONG ulFlags, \
+ ULONG FAR * lpcAdrTypes, \
+ LPTSTR FAR * FAR * lpppszAdrTypes) IPURE; \
+ MAPIMETHOD(QueryIdentity) \
+ (THIS_ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(Logoff) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ ULONG ulReserved) IPURE; \
+ MAPIMETHOD(SetDefaultStore) \
+ (THIS_ ULONG ulFlags, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(AdminServices) \
+ (THIS_ ULONG ulFlags, \
+ LPSERVICEADMIN FAR * lppServiceAdmin) IPURE; \
+ MAPIMETHOD(ShowForm) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ LPMDB lpMsgStore, \
+ LPMAPIFOLDER lpParentFolder, \
+ LPCIID lpInterface, \
+ ULONG_PTR ulMessageToken, \
+ LPMESSAGE lpMessageSent, \
+ ULONG ulFlags, \
+ ULONG ulMessageStatus, \
+ ULONG ulMessageFlags, \
+ ULONG ulAccess, \
+ LPSTR lpszMessageClass) IPURE; \
+ MAPIMETHOD(PrepareForm) \
+ (THIS_ LPCIID lpInterface, \
+ LPMESSAGE lpMessage, \
+ ULONG_PTR FAR * lpulMessageToken) IPURE; \
+
+
+#undef INTERFACE
+#define INTERFACE IMAPISession
+DECLARE_MAPI_INTERFACE_(IMAPISession, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPISESSION_METHODS(PURE)
+};
+
+/*DECLARE_MAPI_INTERFACE_PTR(IMAPISession, LPMAPISESSION);*/
+
+/* IAddrBook Interface ----------------------------------------------------- */
+
+/* CreateOneOff */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+/****** MAPI_SEND_NO_RICH_INFO ((ULONG) 0x00010000) */
+
+/* RecipOptions */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* QueryDefaultRecipOpt */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* GetSearchPath */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+
+#define MAPI_IADDRBOOK_METHODS(IPURE) \
+ MAPIMETHOD(OpenEntry) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulObjType, \
+ LPUNKNOWN FAR * lppUnk) IPURE; \
+ MAPIMETHOD(CompareEntryIDs) \
+ (THIS_ ULONG cbEntryID1, \
+ LPENTRYID lpEntryID1, \
+ ULONG cbEntryID2, \
+ LPENTRYID lpEntryID2, \
+ ULONG ulFlags, \
+ ULONG FAR * lpulResult) IPURE; \
+ MAPIMETHOD(Advise) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ ULONG ulEventMask, \
+ LPMAPIADVISESINK lpAdviseSink, \
+ ULONG_PTR FAR * lpulConnection) IPURE; \
+ MAPIMETHOD(Unadvise) \
+ (THIS_ ULONG_PTR ulConnection) IPURE; \
+ MAPIMETHOD(CreateOneOff) \
+ (THIS_ LPTSTR lpszName, \
+ LPTSTR lpszAdrType, \
+ LPTSTR lpszAddress, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(NewEntry) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ ULONG cbEIDContainer, \
+ LPENTRYID lpEIDContainer, \
+ ULONG cbEIDNewEntryTpl, \
+ LPENTRYID lpEIDNewEntryTpl, \
+ ULONG FAR * lpcbEIDNewEntry, \
+ LPENTRYID FAR * lppEIDNewEntry) IPURE; \
+ MAPIMETHOD(ResolveName) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPTSTR lpszNewEntryTitle, \
+ LPADRLIST lpAdrList) IPURE; \
+ MAPIMETHOD(Address) \
+ (THIS_ ULONG_PTR FAR * lpulUIParam, \
+ LPADRPARM lpAdrParms, \
+ LPADRLIST FAR * lppAdrList) IPURE; \
+ MAPIMETHOD(Details) \
+ (THIS_ ULONG_PTR FAR * lpulUIParam, \
+ LPFNDISMISS lpfnDismiss, \
+ LPVOID lpvDismissContext, \
+ ULONG cbEntryID, \
+ LPENTRYID lpEntryID, \
+ LPFNBUTTON lpfButtonCallback, \
+ LPVOID lpvButtonContext, \
+ LPTSTR lpszButtonText, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(RecipOptions) \
+ (THIS_ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPADRENTRY lpRecip) IPURE; \
+ MAPIMETHOD(QueryDefaultRecipOpt) \
+ (THIS_ LPTSTR lpszAdrType, \
+ ULONG ulFlags, \
+ ULONG FAR * lpcValues, \
+ LPSPropValue FAR * lppOptions) IPURE; \
+ MAPIMETHOD(GetPAB) \
+ (THIS_ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(SetPAB) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(GetDefaultDir) \
+ (THIS_ ULONG FAR * lpcbEntryID, \
+ LPENTRYID FAR * lppEntryID) IPURE; \
+ MAPIMETHOD(SetDefaultDir) \
+ (THIS_ ULONG cbEntryID, \
+ LPENTRYID lpEntryID) IPURE; \
+ MAPIMETHOD(GetSearchPath) \
+ (THIS_ ULONG ulFlags, \
+ LPSRowSet FAR * lppSearchPath) IPURE; \
+ MAPIMETHOD(SetSearchPath) \
+ (THIS_ ULONG ulFlags, \
+ LPSRowSet lpSearchPath) IPURE; \
+ MAPIMETHOD(PrepareRecips) \
+ (THIS_ ULONG ulFlags, \
+ LPSPropTagArray lpPropTagArray, \
+ LPADRLIST lpRecipList) IPURE; \
+
+#undef INTERFACE
+#define INTERFACE IAddrBook
+DECLARE_MAPI_INTERFACE_(IAddrBook, IMAPIProp)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMAPIPROP_METHODS(PURE)
+ MAPI_IADDRBOOK_METHODS(PURE)
+};
+
+DECLARE_MAPI_INTERFACE_PTR(IAddrBook, LPADRBOOK);
+
+/* IProfAdmin Interface ---------------------------------------------------- */
+
+/* Flags for CreateProfile */
+#define MAPI_DEFAULT_SERVICES 0x00000001
+
+/* GetProfileTable */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+#define MAPI_IPROFADMIN_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetProfileTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(CreateProfile) \
+ (THIS_ LPTSTR lpszProfileName, \
+ LPTSTR lpszPassword, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteProfile) \
+ (THIS_ LPTSTR lpszProfileName, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(ChangeProfilePassword) \
+ (THIS_ LPTSTR lpszProfileName, \
+ LPTSTR lpszOldPassword, \
+ LPTSTR lpszNewPassword, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(CopyProfile) \
+ (THIS_ LPTSTR lpszOldProfileName, \
+ LPTSTR lpszOldPassword, \
+ LPTSTR lpszNewProfileName, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(RenameProfile) \
+ (THIS_ LPTSTR lpszOldProfileName, \
+ LPTSTR lpszOldPassword, \
+ LPTSTR lpszNewProfileName, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(SetDefaultProfile) \
+ (THIS_ LPTSTR lpszProfileName, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(AdminServices) \
+ (THIS_ LPTSTR lpszProfileName, \
+ LPTSTR lpszPassword, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ LPSERVICEADMIN FAR * lppServiceAdmin) IPURE; \
+
+
+#undef INTERFACE
+#define INTERFACE IProfAdmin
+DECLARE_MAPI_INTERFACE_(IProfAdmin, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IPROFADMIN_METHODS(PURE)
+};
+
+/* IMsgServiceAdmin Interface ---------------------------------------------- */
+
+/* Values for PR_RESOURCE_FLAGS in message service table */
+
+#define SERVICE_DEFAULT_STORE 0x00000001
+#define SERVICE_SINGLE_COPY 0x00000002
+#define SERVICE_CREATE_WITH_STORE 0x00000004
+#define SERVICE_PRIMARY_IDENTITY 0x00000008
+#define SERVICE_NO_PRIMARY_IDENTITY 0x00000020
+
+/* GetMsgServiceTable */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+/* GetProviderTable */
+/****** MAPI_UNICODE ((ULONG) 0x80000000) */
+
+#define MAPI_IMSGSERVICEADMIN_METHODS(IPURE) \
+ MAPIMETHOD(GetLastError) \
+ (THIS_ HRESULT hResult, \
+ ULONG ulFlags, \
+ LPMAPIERROR FAR * lppMAPIError) IPURE; \
+ MAPIMETHOD(GetMsgServiceTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+ MAPIMETHOD(CreateMsgService) \
+ (THIS_ LPTSTR lpszService, \
+ LPTSTR lpszDisplayName, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(DeleteMsgService) \
+ (THIS_ LPMAPIUID lpUID) IPURE; \
+ MAPIMETHOD(CopyMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPTSTR lpszDisplayName, \
+ LPCIID lpInterfaceToCopy, \
+ LPCIID lpInterfaceDst, \
+ LPVOID lpObjectDst, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(RenameMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags, \
+ LPTSTR lpszDisplayName) IPURE; \
+ MAPIMETHOD(ConfigureMsgService) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG_PTR ulUIParam, \
+ ULONG ulFlags, \
+ ULONG cValues, \
+ LPSPropValue lpProps) IPURE; \
+ MAPIMETHOD(OpenProfileSection) \
+ (THIS_ LPMAPIUID lpUID, \
+ LPCIID lpInterface, \
+ ULONG ulFlags, \
+ LPPROFSECT FAR * lppProfSect) IPURE; \
+ MAPIMETHOD(MsgServiceTransportOrder) \
+ (THIS_ ULONG cUID, \
+ LPMAPIUID lpUIDList, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(AdminProviders) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags, \
+ LPPROVIDERADMIN FAR * lppProviderAdmin) IPURE; \
+ MAPIMETHOD(SetPrimaryIdentity) \
+ (THIS_ LPMAPIUID lpUID, \
+ ULONG ulFlags) IPURE; \
+ MAPIMETHOD(GetProviderTable) \
+ (THIS_ ULONG ulFlags, \
+ LPMAPITABLE FAR * lppTable) IPURE; \
+
+
+#undef INTERFACE
+#define INTERFACE IMsgServiceAdmin
+DECLARE_MAPI_INTERFACE_(IMsgServiceAdmin, IUnknown)
+{
+ BEGIN_INTERFACE
+ MAPI_IUNKNOWN_METHODS(PURE)
+ MAPI_IMSGSERVICEADMIN_METHODS(PURE)
+};
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MAPIX_H */
diff --git a/comm/mailnews/mapi/include/mspst.h b/comm/mailnews/mapi/include/mspst.h
new file mode 100644
index 0000000000..d063e25c58
--- /dev/null
+++ b/comm/mailnews/mapi/include/mspst.h
@@ -0,0 +1,99 @@
+/*
+ * M S P S T . H
+ *
+ * This file lists internal properties of the Microsoft Personal
+ * Information Store
+ *
+ * Copyright (c) 2009 Microsoft Corporation. All Rights Reserved.
+ */
+
+#ifndef _MSPST_H_
+#define _MSPST_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+
+/* The following is a list of properties that may be passed in
+ as the properties in the array of SPropValue structure on the
+ MsgServiceConfigure function.
+
+ Creating a PST profile section through CreateMsgService.
+ The creation of the actual PST file is a two step process. First the
+ client should call CreateMsgService to setup the profile section and then
+ ConfigureMsgService to create the PST file.
+ The CreateMsgService call will setup the PR_DISPLAY_NAME property in the
+ profile section to be used on the PST when it is created.
+
+ Configuring an PST file through ConfigureMsgService.
+ The configuration of an PST can take two forms, either configuring an
+ existing PST or creating a new PST. The Microsoft Personal Information
+ Store provider will try to find the necessary properties by first looking
+ in the array of SPropValue structures provided by the client and then in the
+ profile section, except for PR_PST_PW_SZ_OLD for which it will only look
+ in the array of properties.
+
+ The Microsoft PST provider will try to open the file specified by the
+ PR_PST_PATH property, using the password given in the PR_PST_PW_SZ_OLD
+ property. If it finds a file and it recognizes it as a PST
+ file, it will start the configuration routine. Otherwise it will start the
+ creation routine.
+
+ The configuration routine will look for the PR_DISPLAY_NAME_A and
+ PR_COMMENT_A properties and set them in the message store object. Then it
+ will look for the PR_PST_REMEMBER_PW property to decide if it should
+ remember the password in the profile. (If not found then it will defaut to
+ the current status of the profile password.) Then if it is supposed to
+ use UI, it will display the configuration property sheet to the user. After
+ all has succeeded, it will update the profile.
+
+ The creation routine will follow one of two paths to get the PR_PST_PATH
+ property. If it is supposed to use UI it will always display the file open
+ dialog to confirm the path passed in or allow the user to change it. If
+ the user chooses an existing file and it recognizes it as an PST it will
+ drop back to the configuration routine. If the user chooses an existing
+ file and it is not recognized as an PST file, the user will be given the
+ option of choosing another file or creating a new PST in its place, in
+ which case is will continue with the create routine. If the user chooses
+ a new file it will continue with the create routine. If the routine is not
+ allowed to use UI, then the routine will create a file at the given path
+ even if another file exists there.
+
+ Once it decides to continue with the creation process it will get the
+ PR_DISPLAY_NAME, PR_COMMENT, PR_PST_ENCRYPTION, and PR_PST_SZ_PW_NEW
+ properties. If it is supposed to use UI, it will use these to initialize
+ the creation dialog and get any changes the user want. Then it will create
+ a new file and update the profile.
+
+ PR_DISPLAY_NAME_A display name for the PST service
+ PR_COMMENT_A comment to the place on the PST store object
+ PR_PST_PATH location the store to create or configure
+ PR_PST_REMEMBER_PW whether or not the remember the password in the profile
+ PR_PST_ENCRYPTION encryption level at which to create the file
+ PR_PST_PW_SZ_OLD password of the PST being configured
+ PR_PST_PW_SZ_NEW password to use for future access to the PST
+*/
+
+#define PST_EXTERN_PROPID_BASE (0x6700)
+#define PR_PST_PATH PROP_TAG(PT_STRING8, PST_EXTERN_PROPID_BASE + 0)
+#define PR_PST_REMEMBER_PW PROP_TAG(PT_BOOLEAN, PST_EXTERN_PROPID_BASE + 1)
+#define PR_PST_ENCRYPTION PROP_TAG(PT_LONG, PST_EXTERN_PROPID_BASE + 2)
+#define PR_PST_PW_SZ_OLD PROP_TAG(PT_STRING8, PST_EXTERN_PROPID_BASE + 3)
+#define PR_PST_PW_SZ_NEW PROP_TAG(PT_STRING8, PST_EXTERN_PROPID_BASE + 4)
+
+#define PSTF_NO_ENCRYPTION ((DWORD)0x80000000)
+#define PSTF_COMPRESSABLE_ENCRYPTION ((DWORD)0x40000000)
+#define PSTF_BEST_ENCRYPTION ((DWORD)0x20000000)
+
+/*
+ * PR_MDB_PROVIDER is the GUID that represent the Microsoft Personal
+ * Information Store. This guid is available as a property in the stores
+ * table and on the message store and status objects.
+ */
+#define MSPST_UID_PROVIDER { 0x4e, 0x49, 0x54, 0x41, \
+ 0xf9, 0xbf, 0xb8, 0x01, \
+ 0x00, 0xaa, 0x00, 0x37, \
+ 0xd9, 0x6e, 0x00, 0x00 }
+
+#endif /* _MSPST_H_ */
diff --git a/comm/mailnews/mapi/mapiDll/Makefile.in b/comm/mailnews/mapi/mapiDll/Makefile.in
new file mode 100644
index 0000000000..b6769e3144
--- /dev/null
+++ b/comm/mailnews/mapi/mapiDll/Makefile.in
@@ -0,0 +1,6 @@
+#
+# 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/.
+
+EMBED_MANIFEST_AT = 2
diff --git a/comm/mailnews/mapi/mapiDll/Mapi32.def b/comm/mailnews/mapi/mapiDll/Mapi32.def
new file mode 100644
index 0000000000..cc8dcef449
--- /dev/null
+++ b/comm/mailnews/mapi/mapiDll/Mapi32.def
@@ -0,0 +1,22 @@
+; 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/.
+
+LIBRARY mozMapi32.dll
+
+EXPORTS
+ MAPILogon
+ MAPILogoff
+ MAPISendMail
+ MAPISendDocuments
+ MAPIFindNext
+ MAPIReadMail
+ MAPISaveMail
+ MAPIDeleteMail
+ MAPIAddress
+ MAPIDetails
+ MAPIResolveName
+ MAPIFreeBuffer
+ MAPISendMailW
+ GetMapiDllVersion
+
diff --git a/comm/mailnews/mapi/mapiDll/MapiDll.cpp b/comm/mailnews/mapi/mapiDll/MapiDll.cpp
new file mode 100644
index 0000000000..027257a111
--- /dev/null
+++ b/comm/mailnews/mapi/mapiDll/MapiDll.cpp
@@ -0,0 +1,577 @@
+/* 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/. */
+
+#pragma warning(disable : 4996) // MAPILogoff is deprecated
+
+#include <windows.h>
+#include <mapidefs.h>
+#include <mapi.h>
+#include "msgMapi.h"
+
+// Ensure that our COM structs match MS MAPI structs - thoroughly check each
+// struct layout This needs that MAPI.h is not used from
+// https://www.microsoft.com/en-us/download/details.aspx?id=12905 because the
+// newer MAPI.h is the part of Windows SDK, and it includes MapiFileDescW and
+// friends.
+
+static_assert(sizeof(nsMapiFileDesc) == sizeof(MapiFileDesc), "Size mismatch!");
+static_assert(offsetof(nsMapiFileDesc, ulReserved) ==
+ offsetof(MapiFileDesc, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDesc, flFlags) ==
+ offsetof(MapiFileDesc, flFlags),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDesc, nPosition_NotUsed) ==
+ offsetof(MapiFileDesc, nPosition),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDesc, lpszPathName) ==
+ offsetof(MapiFileDesc, lpszPathName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDesc, lpszFileName) ==
+ offsetof(MapiFileDesc, lpszFileName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDesc, lpFileType_NotUsed) ==
+ offsetof(MapiFileDesc, lpFileType),
+ "Member offset mismatch!");
+
+static_assert(sizeof(nsMapiRecipDesc) == sizeof(MapiRecipDesc),
+ "Size mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, ulReserved) ==
+ offsetof(MapiRecipDesc, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, ulRecipClass) ==
+ offsetof(MapiRecipDesc, ulRecipClass),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, lpszName) ==
+ offsetof(MapiRecipDesc, lpszName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, lpszAddress) ==
+ offsetof(MapiRecipDesc, lpszAddress),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, ulEIDSize_NotUsed) ==
+ offsetof(MapiRecipDesc, ulEIDSize),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDesc, lpEntryID_NotUsed) ==
+ offsetof(MapiRecipDesc, lpEntryID),
+ "Member offset mismatch!");
+
+static_assert(sizeof(nsMapiMessage) == sizeof(MapiMessage), "Size mismatch!");
+static_assert(offsetof(nsMapiMessage, ulReserved) ==
+ offsetof(MapiMessage, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpszSubject) ==
+ offsetof(MapiMessage, lpszSubject),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpszNoteText) ==
+ offsetof(MapiMessage, lpszNoteText),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpszMessageType) ==
+ offsetof(MapiMessage, lpszMessageType),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpszDateReceived) ==
+ offsetof(MapiMessage, lpszDateReceived),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpszConversationID_NotUsed) ==
+ offsetof(MapiMessage, lpszConversationID),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, flFlags) ==
+ offsetof(MapiMessage, flFlags),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpOriginator) ==
+ offsetof(MapiMessage, lpOriginator),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, nRecipCount) ==
+ offsetof(MapiMessage, nRecipCount),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpRecips) ==
+ offsetof(MapiMessage, lpRecips),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, nFileCount) ==
+ offsetof(MapiMessage, nFileCount),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessage, lpFiles) ==
+ offsetof(MapiMessage, lpFiles),
+ "Member offset mismatch!");
+
+static_assert(sizeof(nsMapiFileDescW) == sizeof(MapiFileDescW),
+ "Size mismatch!");
+static_assert(offsetof(nsMapiFileDescW, ulReserved) ==
+ offsetof(MapiFileDescW, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDescW, flFlags) ==
+ offsetof(MapiFileDescW, flFlags),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDescW, nPosition_NotUsed) ==
+ offsetof(MapiFileDescW, nPosition),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDescW, lpszPathName) ==
+ offsetof(MapiFileDescW, lpszPathName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDescW, lpszFileName) ==
+ offsetof(MapiFileDescW, lpszFileName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiFileDescW, lpFileType_NotUsed) ==
+ offsetof(MapiFileDescW, lpFileType),
+ "Member offset mismatch!");
+
+static_assert(sizeof(nsMapiRecipDescW) == sizeof(MapiRecipDescW),
+ "Size mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, ulReserved) ==
+ offsetof(MapiRecipDescW, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, ulRecipClass) ==
+ offsetof(MapiRecipDescW, ulRecipClass),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, lpszName) ==
+ offsetof(MapiRecipDescW, lpszName),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, lpszAddress) ==
+ offsetof(MapiRecipDescW, lpszAddress),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, ulEIDSize_NotUsed) ==
+ offsetof(MapiRecipDescW, ulEIDSize),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiRecipDescW, lpEntryID_NotUsed) ==
+ offsetof(MapiRecipDescW, lpEntryID),
+ "Member offset mismatch!");
+
+static_assert(sizeof(nsMapiMessageW) == sizeof(MapiMessageW), "Size mismatch!");
+static_assert(offsetof(nsMapiMessageW, ulReserved) ==
+ offsetof(MapiMessageW, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszSubject) ==
+ offsetof(MapiMessageW, lpszSubject),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, ulReserved) ==
+ offsetof(MapiMessageW, ulReserved),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszSubject) ==
+ offsetof(MapiMessageW, lpszSubject),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszNoteText) ==
+ offsetof(MapiMessageW, lpszNoteText),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszMessageType) ==
+ offsetof(MapiMessageW, lpszMessageType),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszDateReceived) ==
+ offsetof(MapiMessageW, lpszDateReceived),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpszConversationID_NotUsed) ==
+ offsetof(MapiMessageW, lpszConversationID),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, flFlags) ==
+ offsetof(MapiMessageW, flFlags),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpOriginator) ==
+ offsetof(MapiMessageW, lpOriginator),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, nRecipCount) ==
+ offsetof(MapiMessageW, nRecipCount),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpRecips) ==
+ offsetof(MapiMessageW, lpRecips),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, nFileCount) ==
+ offsetof(MapiMessageW, nFileCount),
+ "Member offset mismatch!");
+static_assert(offsetof(nsMapiMessageW, lpFiles) ==
+ offsetof(MapiMessageW, lpFiles),
+ "Member offset mismatch!");
+
+#define MAX_RECIPS 2000
+#define MAX_FILES 100
+
+#define MAX_NAME_LEN 256
+#define MAX_PW_LEN 256
+#define MAX_MSGINFO_LEN 512
+#define MAX_POINTERS 32
+
+const CLSID CLSID_CMapiImp = {0x29f458be,
+ 0x8866,
+ 0x11d5,
+ {0xa3, 0xdd, 0x0, 0xb0, 0xd0, 0xf3, 0xba, 0xa7}};
+const IID IID_nsIMapi = {0x6EDCD38E,
+ 0x8861,
+ 0x11d5,
+ {0xA3, 0xDD, 0x00, 0xB0, 0xD0, 0xF3, 0xBA, 0xA7}};
+
+DWORD tId = 0;
+
+#define MAPI_MESSAGE_TYPE 0
+#define MAPI_RECIPIENT_TYPE 1
+
+typedef struct {
+ LPVOID lpMem;
+ UCHAR memType;
+} memTrackerType;
+
+// this can't be right.
+memTrackerType memArray[MAX_POINTERS];
+
+//
+// For remembering memory...how ironic.
+//
+void SetPointerArray(LPVOID ptr, BYTE type) {
+ int i;
+
+ for (i = 0; i < MAX_POINTERS; i++) {
+ if (memArray[i].lpMem == NULL) {
+ memArray[i].lpMem = ptr;
+ memArray[i].memType = type;
+ break;
+ }
+ }
+}
+
+BOOL WINAPI DllMain(HINSTANCE aInstance, DWORD aReason, LPVOID aReserved) {
+ switch (aReason) {
+ case DLL_PROCESS_ATTACH:
+ tId = TlsAlloc();
+ if (tId == 0xFFFFFFFF) return FALSE;
+ break;
+
+ case DLL_PROCESS_DETACH:
+ TlsFree(tId);
+ break;
+ }
+ return TRUE;
+}
+
+BOOL InitMozillaReference(nsIMapi** aRetValue) {
+ // Check whether this thread has a valid Interface
+ // by looking into thread-specific-data variable
+
+ *aRetValue = (nsIMapi*)TlsGetValue(tId);
+
+ // Check whether the pointer actually resolves to
+ // a valid method call; otherwise mozilla is not running
+
+ if ((*aRetValue) && (*aRetValue)->IsValid() == S_OK) return TRUE;
+
+ HRESULT hRes = ::CoInitialize(NULL);
+
+ hRes = ::CoCreateInstance(CLSID_CMapiImp, NULL, CLSCTX_LOCAL_SERVER,
+ IID_nsIMapi, (LPVOID*)aRetValue);
+
+ if (hRes == S_OK && (*aRetValue)->Initialize() == S_OK)
+ if (TlsSetValue(tId, (LPVOID)(*aRetValue))) return TRUE;
+
+ // Either CoCreate or TlsSetValue failed; so return FALSE
+
+ if ((*aRetValue)) (*aRetValue)->Release();
+
+ ::CoUninitialize();
+ return FALSE;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// The MAPILogon function begins a Simple MAPI session, loading the default
+// message store and address book providers
+/////////////////////////////////////////////////////////////////////////////
+
+ULONG FAR PASCAL MAPILogon(ULONG aUIParam, LPSTR aProfileName, LPSTR aPassword,
+ FLAGS aFlags, ULONG aReserved, LPLHANDLE aSession) {
+ HRESULT hr = 0;
+ ULONG nSessionId = 0;
+ nsIMapi* pNsMapi = NULL;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ hr = pNsMapi->Login(aUIParam, aProfileName, aPassword, aFlags, &nSessionId);
+ if (hr == S_OK)
+ (*aSession) = (LHANDLE)nSessionId;
+ else
+ return nSessionId;
+
+ return SUCCESS_SUCCESS;
+}
+
+ULONG FAR PASCAL MAPILogoff(LHANDLE aSession, ULONG aUIParam, FLAGS aFlags,
+ ULONG aReserved) {
+ nsIMapi* pNsMapi = (nsIMapi*)TlsGetValue(tId);
+ if (pNsMapi != NULL) {
+ if (pNsMapi->Logoff((ULONG)aSession) == S_OK) pNsMapi->Release();
+ pNsMapi = NULL;
+ }
+
+ TlsSetValue(tId, NULL);
+
+ ::CoUninitialize();
+
+ return SUCCESS_SUCCESS;
+}
+
+ULONG FAR PASCAL MAPISendMail(LHANDLE lhSession, ULONG ulUIParam,
+ nsMapiMessage* lpMessage, FLAGS flFlags,
+ ULONG ulReserved) {
+ HRESULT hr = 0;
+ BOOL bTempSession = FALSE;
+ nsIMapi* pNsMapi = NULL;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ if (lpMessage->nRecipCount > MAX_RECIPS) return MAPI_E_TOO_MANY_RECIPIENTS;
+
+ if (lpMessage->nFileCount > MAX_FILES) return MAPI_E_TOO_MANY_FILES;
+
+ if ((!(flFlags & MAPI_DIALOG)) && (lpMessage->lpRecips == NULL))
+ return MAPI_E_UNKNOWN_RECIPIENT;
+
+ if (!lhSession || pNsMapi->IsValidSession(lhSession) != S_OK) {
+ FLAGS LoginFlag = flFlags & (MAPI_LOGON_UI | MAPI_NEW_SESSION);
+ hr = MAPILogon(ulUIParam, nullptr, nullptr, LoginFlag, 0, &lhSession);
+ if (hr != SUCCESS_SUCCESS) return MAPI_E_LOGIN_FAILURE;
+ bTempSession = TRUE;
+ }
+
+ hr = pNsMapi->SendMail(lhSession, lpMessage, flFlags, ulReserved);
+
+ // we are seeing a problem when using Word, although we return success from
+ // the MAPI support MS COM interface in mozilla, we are getting this error
+ // here. This is a temporary hack !!
+ if (hr == (HRESULT)0x800703e6) hr = SUCCESS_SUCCESS;
+
+ if (bTempSession) MAPILogoff(lhSession, ulUIParam, 0, 0);
+
+ return hr;
+}
+
+ULONG FAR PASCAL MAPISendMailW(LHANDLE lhSession, ULONG ulUIParam,
+ nsMapiMessageW* lpMessage, FLAGS flFlags,
+ ULONG ulReserved) {
+ HRESULT hr = 0;
+ BOOL bTempSession = FALSE;
+ nsIMapi* pNsMapi = nullptr;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ if (lpMessage->nRecipCount > MAX_RECIPS) return MAPI_E_TOO_MANY_RECIPIENTS;
+
+ if (lpMessage->nFileCount > MAX_FILES) return MAPI_E_TOO_MANY_FILES;
+
+ if ((!(flFlags & MAPI_DIALOG)) && (lpMessage->lpRecips == nullptr))
+ return MAPI_E_UNKNOWN_RECIPIENT;
+
+ if (!lhSession || pNsMapi->IsValidSession(lhSession) != S_OK) {
+ FLAGS LoginFlag = flFlags & (MAPI_LOGON_UI | MAPI_NEW_SESSION);
+ hr = MAPILogon(ulUIParam, nullptr, nullptr, LoginFlag, 0, &lhSession);
+ if (hr != SUCCESS_SUCCESS) return MAPI_E_LOGIN_FAILURE;
+ bTempSession = TRUE;
+ }
+
+ hr = pNsMapi->SendMailW(lhSession, lpMessage, flFlags, ulReserved);
+
+ // we are seeing a problem when using Word, although we return success from
+ // the MAPI support MS COM interface in mozilla, we are getting this error
+ // here. This is a temporary hack !!
+ if (hr == (HRESULT)0x800703e6) hr = SUCCESS_SUCCESS;
+
+ if (bTempSession) MAPILogoff(lhSession, ulUIParam, 0, 0);
+
+ return hr;
+}
+
+ULONG FAR PASCAL MAPISendDocuments(ULONG ulUIParam, LPSTR lpszDelimChar,
+ LPSTR lpszFilePaths, LPSTR lpszFileNames,
+ ULONG ulReserved) {
+ LHANDLE lhSession;
+ nsIMapi* pNsMapi = NULL;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ unsigned long result =
+ MAPILogon(ulUIParam, nullptr, nullptr, MAPI_LOGON_UI, 0, &lhSession);
+ if (result != SUCCESS_SUCCESS) return MAPI_E_LOGIN_FAILURE;
+
+ HRESULT hr;
+
+ hr = pNsMapi->SendDocuments(lhSession, lpszDelimChar, lpszFilePaths,
+ lpszFileNames, ulReserved);
+
+ MAPILogoff(lhSession, ulUIParam, 0, 0);
+
+ return hr;
+}
+
+ULONG FAR PASCAL MAPIFindNext(LHANDLE lhSession, ULONG ulUIParam,
+ const LPSTR lpszMessageType,
+ const LPSTR lpszSeedMessageID, FLAGS flFlags,
+ ULONG ulReserved,
+ unsigned char lpszMessageID[64]) {
+ nsIMapi* pNsMapi = NULL;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ if (lhSession == 0) return MAPI_E_INVALID_SESSION;
+
+ return pNsMapi->FindNext(lhSession, ulUIParam, lpszMessageType,
+ lpszSeedMessageID, flFlags, ulReserved,
+ lpszMessageID);
+}
+
+ULONG FAR PASCAL MAPIReadMail(LHANDLE lhSession, ULONG ulUIParam,
+ LPSTR lpszMessageID, FLAGS flFlags,
+ ULONG ulReserved, nsMapiMessage** lppMessage) {
+ nsIMapi* pNsMapi = NULL;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ if (lhSession == 0) return MAPI_E_INVALID_SESSION;
+
+ return pNsMapi->ReadMail(lhSession, ulUIParam, lpszMessageID, flFlags,
+ ulReserved, lppMessage);
+}
+
+ULONG FAR PASCAL MAPISaveMail(LHANDLE lhSession, ULONG ulUIParam,
+ lpnsMapiMessage lpMessage, FLAGS flFlags,
+ ULONG ulReserved, LPSTR lpszMessageID) {
+ nsIMapi* pNsMapi = NULL;
+
+ if (lhSession == 0) return MAPI_E_INVALID_SESSION;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ return MAPI_E_FAILURE;
+}
+
+ULONG FAR PASCAL MAPIDeleteMail(LHANDLE lhSession, ULONG ulUIParam,
+ LPSTR lpszMessageID, FLAGS flFlags,
+ ULONG ulReserved) {
+ nsIMapi* pNsMapi = NULL;
+
+ if (lhSession == 0) return MAPI_E_INVALID_SESSION;
+
+ if (!InitMozillaReference(&pNsMapi)) return MAPI_E_FAILURE;
+
+ return pNsMapi->DeleteMail(lhSession, ulUIParam, lpszMessageID, flFlags,
+ ulReserved);
+}
+
+ULONG FAR PASCAL MAPIAddress(LHANDLE lhSession, ULONG ulUIParam,
+ LPSTR lpszCaption, ULONG nEditFields,
+ LPSTR lpszLabels, ULONG nRecips,
+ lpMapiRecipDesc lpRecips, FLAGS flFlags,
+ ULONG ulReserved, LPULONG lpnNewRecips,
+ lpMapiRecipDesc FAR* lppNewRecips) {
+ return MAPI_E_NOT_SUPPORTED;
+}
+
+ULONG FAR PASCAL MAPIDetails(LHANDLE lhSession, ULONG ulUIParam,
+ lpMapiRecipDesc lpRecip, FLAGS flFlags,
+ ULONG ulReserved) {
+ return MAPI_E_NOT_SUPPORTED;
+}
+
+ULONG FAR PASCAL MAPIResolveName(LHANDLE lhSession, ULONG ulUIParam,
+ LPSTR lpszName, FLAGS flFlags,
+ ULONG ulReserved,
+ lpMapiRecipDesc FAR* lppRecip) {
+ char* lpszRecipName = new char[(strlen((const char*)lpszName) + 1)];
+ if (lpszRecipName == NULL) return MAPI_E_INSUFFICIENT_MEMORY;
+ char* lpszRecipAddress = new char[(strlen((const char*)lpszName) + 6)];
+ if (!lpszRecipAddress) {
+ delete[] lpszRecipName;
+ return MAPI_E_INSUFFICIENT_MEMORY;
+ }
+ strcpy(lpszRecipName, (const char*)lpszName);
+ strcpy(lpszRecipAddress, (const char*)lpszName);
+ (*lppRecip) = (lpMapiRecipDesc FAR)malloc(sizeof(MapiRecipDesc));
+ if (!(*lppRecip)) {
+ delete[] lpszRecipName;
+ delete[] lpszRecipAddress;
+ return MAPI_E_INSUFFICIENT_MEMORY;
+ }
+ (*lppRecip)->ulRecipClass = 1;
+ (*lppRecip)->lpszName = lpszRecipName;
+ (*lppRecip)->lpszAddress = lpszRecipAddress;
+ (*lppRecip)->ulEIDSize = 0;
+ (*lppRecip)->lpEntryID = 0;
+ return SUCCESS_SUCCESS;
+}
+
+void FreeMAPIRecipient(lpMapiRecipDesc pv);
+void FreeMAPIMessage(lpMapiMessage pv);
+
+ULONG FAR PASCAL MAPIFreeBuffer(LPVOID pv) {
+ int i;
+
+ if (!pv) return S_OK;
+
+ for (i = 0; i < MAX_POINTERS; i++) {
+ if (pv == memArray[i].lpMem) {
+ if (memArray[i].memType == MAPI_MESSAGE_TYPE) {
+ FreeMAPIMessage((MapiMessage*)pv);
+ memArray[i].lpMem = NULL;
+ } else if (memArray[i].memType == MAPI_RECIPIENT_TYPE) {
+ FreeMAPIRecipient((MapiRecipDesc*)pv);
+ memArray[i].lpMem = NULL;
+ }
+ }
+ }
+
+ pv = NULL;
+ return S_OK;
+}
+
+ULONG FAR PASCAL GetMapiDllVersion() { return 94; }
+
+void FreeMAPIFile(lpMapiFileDesc pv) {
+ if (!pv) return;
+
+ if (pv->lpszPathName != NULL) free(pv->lpszPathName);
+
+ if (pv->lpszFileName != NULL) free(pv->lpszFileName);
+}
+
+void FreeMAPIMessage(lpMapiMessage pv) {
+ ULONG i;
+
+ if (!pv) return;
+
+ if (pv->lpszSubject != NULL) free(pv->lpszSubject);
+
+ if (pv->lpszNoteText) free(pv->lpszNoteText);
+
+ if (pv->lpszMessageType) free(pv->lpszMessageType);
+
+ if (pv->lpszDateReceived) free(pv->lpszDateReceived);
+
+ if (pv->lpszConversationID) free(pv->lpszConversationID);
+
+ if (pv->lpOriginator) FreeMAPIRecipient(pv->lpOriginator);
+
+ for (i = 0; i < pv->nRecipCount; i++) {
+ if (&(pv->lpRecips[i]) != NULL) {
+ FreeMAPIRecipient(&(pv->lpRecips[i]));
+ }
+ }
+
+ if (pv->lpRecips != NULL) {
+ free(pv->lpRecips);
+ }
+
+ for (i = 0; i < pv->nFileCount; i++) {
+ if (&(pv->lpFiles[i]) != NULL) {
+ FreeMAPIFile(&(pv->lpFiles[i]));
+ }
+ }
+
+ if (pv->lpFiles != NULL) {
+ free(pv->lpFiles);
+ }
+
+ free(pv);
+ pv = NULL;
+}
+
+void FreeMAPIRecipient(lpMapiRecipDesc pv) {
+ if (!pv) return;
+
+ if (pv->lpszName != NULL) free(pv->lpszName);
+
+ if (pv->lpszAddress != NULL) free(pv->lpszAddress);
+
+ if (pv->lpEntryID != NULL) free(pv->lpEntryID);
+}
diff --git a/comm/mailnews/mapi/mapiDll/module.ver b/comm/mailnews/mapi/mapiDll/module.ver
new file mode 100644
index 0000000000..d62b9e99cb
--- /dev/null
+++ b/comm/mailnews/mapi/mapiDll/module.ver
@@ -0,0 +1,7 @@
+WIN32_MODULE_FILEVERSION=0,8,0,0
+WIN32_MODULE_FILEVERSION_STRING=0.8
+WIN32_MODULE_COPYRIGHT=©Thunderbird and Mozilla Developers, according to the MPL 1.1/GPL 2.0/LGPL 2.1 licenses, as applicable.
+WIN32_MODULE_COMPANYNAME=Mozilla.org
+WIN32_MODULE_TRADEMARKS=Mozilla
+WIN32_MODULE_COMMENT=Mozilla Thunderbird MAPI Dll
+
diff --git a/comm/mailnews/mapi/mapiDll/moz.build b/comm/mailnews/mapi/mapiDll/moz.build
new file mode 100644
index 0000000000..a013feffec
--- /dev/null
+++ b/comm/mailnews/mapi/mapiDll/moz.build
@@ -0,0 +1,24 @@
+# vim: set filetype=python:
+# 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/.
+
+# Statically link against the CRT, so that we don't go hunting around for it
+# and not find it when we're loaded into explorer.exe or similar
+SharedLibrary("mozMapi32")
+USE_STATIC_LIBS = True
+
+LOCAL_INCLUDES += ["../include"]
+
+SOURCES += [
+ "MapiDll.cpp",
+]
+
+OS_LIBS += [
+ "ole32",
+]
+
+DEFINES["UNICODE"] = True
+DEFINES["_UNICODE"] = True
+
+DEFFILE = "Mapi32.def"
diff --git a/comm/mailnews/mapi/mapihook/build/MapiProxy.def b/comm/mailnews/mapi/mapihook/build/MapiProxy.def
new file mode 100644
index 0000000000..4da08eb7d3
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/build/MapiProxy.def
@@ -0,0 +1,13 @@
+; 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/.
+
+LIBRARY MapiProxy.dll
+
+EXPORTS
+ DllGetClassObject PRIVATE
+ DllCanUnloadNow PRIVATE
+ GetProxyDllInfo PRIVATE
+ DllRegisterServer PRIVATE
+ DllUnregisterServer PRIVATE
+
diff --git a/comm/mailnews/mapi/mapihook/build/module.ver b/comm/mailnews/mapi/mapihook/build/module.ver
new file mode 100644
index 0000000000..7691020c26
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/build/module.ver
@@ -0,0 +1,6 @@
+WIN32_MODULE_FILEVERSION=0,8,0,0
+WIN32_MODULE_FILEVERSION_STRING=0.8
+WIN32_MODULE_COPYRIGHT=©Thunderbird and Mozilla Developers, according to the MPL 1.1/GPL 2.0/LGPL 2.1 licenses, as applicable.
+WIN32_MODULE_COMPANYNAME=Mozilla.org
+WIN32_MODULE_TRADEMARKS=Mozilla
+WIN32_MODULE_COMMENT=Mozilla Thunderbird Thunderbird MAPI Proxy Dll
diff --git a/comm/mailnews/mapi/mapihook/build/moz.build b/comm/mailnews/mapi/mapihook/build/moz.build
new file mode 100644
index 0000000000..9832a5cb05
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/build/moz.build
@@ -0,0 +1,56 @@
+# vim: set filetype=python:
+# 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/.
+
+SharedLibrary("MapiProxy")
+
+OS_LIBS += [
+ "rpcrt4",
+]
+
+LOCAL_INCLUDES += ["/comm/mailnews/mapi/include"]
+
+SOURCES += ["!dlldata.c", "!msgMapi_i.c", "!msgMapi_p.c"]
+
+GeneratedFile(
+ "dlldata.c",
+ "msgMapi.h",
+ "msgMapi_i.c",
+ "msgMapi_p.c",
+ inputs=["msgMapi.idl"],
+ script="/build/midl.py",
+ entry_point="midl",
+ flags=[
+ "-I",
+ SRCDIR,
+ "-I",
+ TOPSRCDIR + "/comm/mailnews/mapi/include",
+ ],
+)
+
+EXPORTS += [
+ "!msgMapi.h",
+ "!msgMapi_i.c",
+]
+
+for var in ("REGISTER_PROXY_DLL", "UNICODE", "_UNICODE"):
+ DEFINES[var] = True
+
+DEFFILE = "MapiProxy.def"
+
+# The Windows MIDL code generator creates things like:
+#
+# #endif !_MIDL_USE_GUIDDEF_
+#
+# which clang-cl complains about. MSVC doesn't, so turn this warning off.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CFLAGS += ["-Wno-extra-tokens"]
+
+# clang-cl complains about these in generated code:
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CFLAGS += [
+ "-Wno-extern-initializer",
+ "-Wno-missing-braces",
+ "-Wno-unused-const-variable",
+ ]
diff --git a/comm/mailnews/mapi/mapihook/build/msgMapi.idl b/comm/mailnews/mapi/mapihook/build/msgMapi.idl
new file mode 100644
index 0000000000..e2738ae8e1
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/build/msgMapi.idl
@@ -0,0 +1,125 @@
+/* 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/. */
+
+// This idl will be compiled by MIDL. MS-COM is used
+// as bridge between MAPI clients and the Mozilla.
+
+import "unknwn.idl";
+
+typedef struct
+{
+ unsigned long ulReserved;
+ unsigned long flFlags; /* Flags */
+ unsigned long nPosition_NotUsed; /* character in text to be replaced by attachment */
+ LPSTR lpszPathName; /* Full path name including file name */
+ LPSTR lpszFileName; /* Real (original) file name */
+ unsigned char * lpFileType_NotUsed ;
+} nsMapiFileDesc, * lpnsMapiFileDesc;
+
+
+typedef struct
+{
+ unsigned long ulReserved;
+ unsigned long ulRecipClass; /* MAPI_TO, MAPI_CC, MAPI_BCC, MAPI_ORIG */
+ LPSTR lpszName; /* Recipient name to display */
+ LPSTR lpszAddress; /* Recipient email address */
+ unsigned long ulEIDSize_NotUsed;
+ unsigned char * lpEntryID_NotUsed ;
+} nsMapiRecipDesc, * lpnsMapiRecipDesc;
+
+typedef struct
+{
+ unsigned long ulReserved;
+ LPSTR lpszSubject; /* Message Subject */
+ LPSTR lpszNoteText; /* Message Text */
+ LPSTR lpszMessageType;
+ LPSTR lpszDateReceived; /* in YYYY/MM/DD HH:MM format */
+ LPSTR lpszConversationID_NotUsed; /* conversation thread ID */
+ unsigned long flFlags; /* unread,return receipt */
+ lpnsMapiRecipDesc lpOriginator; /* Originator descriptor */
+ unsigned long nRecipCount; /* Number of recipients */
+ [size_is (nRecipCount)] lpnsMapiRecipDesc lpRecips; /* Recipient descriptors */
+ unsigned long nFileCount; /* # of file attachments */
+ [size_is (nFileCount)] lpnsMapiFileDesc lpFiles; /* Attachment descriptors */
+} nsMapiMessage, * lpnsMapiMessage;
+
+typedef struct
+{
+ unsigned long ulReserved;
+ unsigned long flFlags; /* Flags */
+ unsigned long nPosition_NotUsed; /* character in text to be replaced by attachment */
+ LPWSTR lpszPathName; /* Full path name including file name */
+ LPWSTR lpszFileName; /* Real (original) file name */
+ unsigned char* lpFileType_NotUsed;
+} nsMapiFileDescW, *lpnsMapiFileDescW;
+
+typedef struct
+{
+ unsigned long ulReserved;
+ unsigned long ulRecipClass; /* MAPI_TO, MAPI_CC, MAPI_BCC, MAPI_ORIG */
+ LPWSTR lpszName; /* Recipient name to display */
+ LPWSTR lpszAddress; /* Recipient email address */
+ unsigned long ulEIDSize_NotUsed;
+ unsigned char* lpEntryID_NotUsed;
+} nsMapiRecipDescW, *lpnsMapiRecipDescW;
+
+typedef struct
+{
+ unsigned long ulReserved;
+ LPWSTR lpszSubject; /* Message Subject */
+ LPWSTR lpszNoteText; /* Message Text */
+ LPWSTR lpszMessageType;
+ LPWSTR lpszDateReceived; /* in YYYY/MM/DD HH:MM format */
+ LPWSTR lpszConversationID_NotUsed; /* conversation thread ID */
+ unsigned long flFlags; /* unread,return receipt */
+ lpnsMapiRecipDescW lpOriginator; /* Originator descriptor */
+ unsigned long nRecipCount; /* Number of recipients */
+ [size_is (nRecipCount)] lpnsMapiRecipDescW lpRecips; /* Recipient descriptors */
+ unsigned long nFileCount; /* # of file attachments */
+ [size_is (nFileCount)] lpnsMapiFileDescW lpFiles; /* Attachment descriptors */
+} nsMapiMessageW, *lpnsMapiMessageW;
+
+[
+ object,
+ uuid(6EDCD38E-8861-11d5-A3DD-00B0D0F3BAA7),
+ helpstring("nsIMapi Interface"),
+ pointer_default(unique)
+]
+
+interface nsIMapi : IUnknown
+{
+ HRESULT Login([in] unsigned long aUIArg, [in, unique, max_is(256)] LPSTR aLogin,
+ [in, unique, max_is(256)] LPSTR aPassWord, [in] unsigned long aFlags,
+ [out] unsigned long *aSessionId);
+
+ HRESULT Initialize();
+ HRESULT IsValid();
+ HRESULT IsValidSession([in] unsigned long aSession);
+
+ HRESULT SendMail([in] unsigned long aSession, [in, unique] lpnsMapiMessage aMessage,
+ [in] unsigned long aFlags, [in] unsigned long aReserved) ;
+
+ HRESULT SendDocuments([in] unsigned long aSession,
+ [in, unique] LPSTR aDelimChar, [in, unique] LPSTR aFilePaths,
+ [in, unique] LPSTR aFileNames, [in] ULONG aFlags);
+
+ HRESULT FindNext([in] unsigned long aSession, [in] ULONG ulUIParam, [in, unique] const LPSTR lpszMessageType,
+ [in, unique] const LPSTR lpszSeedMessageID, [in] ULONG flFlags, [in] ULONG ulReserved,
+ [in] [out] char lpszMessageID[64] ) ;
+
+ HRESULT ReadMail([in] unsigned long lhSession, [in] ULONG ulUIParam, [in, unique] LPSTR lpszMessageID,
+ [in] ULONG flFlags, [in] ULONG ulReserved, [out] lpnsMapiMessage *lppMessage);
+
+ HRESULT DeleteMail([in] unsigned long lhSession, [in] ULONG ulUIParam, [in, unique] LPSTR lpszMessageID,
+ [in] ULONG flFlags, [in] ULONG ulReserved);
+
+ HRESULT SaveMail([in] unsigned long lhSession, [in] ULONG ulUIParam, [in, unique] lpnsMapiMessage lppMessage,
+ [in] ULONG flFlags, [in] ULONG ulReserved, [in, unique] LPSTR lpszMessageID);
+
+ HRESULT SendMailW([in] unsigned long aSession, [in, unique] lpnsMapiMessageW aMessage,
+ [in] unsigned long aFlags, [in] unsigned long aReserved);
+
+ HRESULT Logoff(unsigned long aSession);
+ HRESULT CleanUp();
+};
diff --git a/comm/mailnews/mapi/mapihook/moz.build b/comm/mailnews/mapi/mapihook/moz.build
new file mode 100644
index 0000000000..2160ce3345
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "build",
+ "src",
+]
diff --git a/comm/mailnews/mapi/mapihook/public/moz.build b/comm/mailnews/mapi/mapihook/public/moz.build
new file mode 100644
index 0000000000..12f8019242
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/public/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIMapiSupport.idl",
+]
+
+XPIDL_MODULE = "mapihook"
diff --git a/comm/mailnews/mapi/mapihook/public/nsIMapiSupport.idl b/comm/mailnews/mapi/mapihook/public/nsIMapiSupport.idl
new file mode 100644
index 0000000000..39a947ae7a
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/public/nsIMapiSupport.idl
@@ -0,0 +1,42 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This interface provides support for registering Mozilla as a COM component
+ * for extending the use of Mail/News through Simple MAPI.
+ *
+ */
+
+[uuid(2907B676-C4BD-49af-880A-E27A0616291E)]
+interface nsIMapiSupport : nsISupports {
+
+ /** Initiates MAPI support
+ */
+
+ void initializeMAPISupport();
+
+ /** Shuts down the MAPI support
+ */
+
+ void shutdownMAPISupport();
+
+ /** registerServer - register the mapi DLL with the desktop
+ * Typically called by the window shell service when we are
+ * made the default mail app
+ */
+ void registerServer();
+
+ /** unRegisterServer - unregister the mapi DLL with the desktop
+ * Typically called by the window shell service when we are
+ * removed as the default mail app.
+ */
+ void unRegisterServer();
+};
+
+%{C++
+#define NS_IMAPISUPPORT_CONTRACTID "@mozilla.org/mapisupport;1"
+#define NS_IMAPISUPPORT_CLASSNAME "Mozilla MAPI Support"
+%}
diff --git a/comm/mailnews/mapi/mapihook/src/Registry.cpp b/comm/mailnews/mapi/mapihook/src/Registry.cpp
new file mode 100644
index 0000000000..bf57e31e57
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/Registry.cpp
@@ -0,0 +1,249 @@
+/* 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/. */
+
+#undef _UNICODE
+#undef UNICODE
+
+#include <objbase.h>
+#include "nsString.h"
+#include "Registry.h"
+
+#define MAPI_PROXY_DLL_NAME u"MapiProxy.dll"
+#define MAPI_STARTUP_ARG L" /MAPIStartUp"
+#define MAX_SIZE 2048
+
+// Size of a CLSID as a string
+const int CLSID_STRING_SIZE = 39;
+
+// Proxy/Stub Dll Routines
+
+typedef HRESULT(__stdcall ProxyServer)();
+
+// Convert a CLSID to a WCHAR string.
+
+BOOL CLSIDtoWchar(const CLSID& clsid, WCHAR* szCLSID) {
+ // Get CLSID
+ HRESULT hr = StringFromCLSID(clsid, &szCLSID);
+ if (FAILED(hr)) return FALSE;
+ return TRUE;
+}
+
+// Create a key and set its value.
+
+BOOL setKeyAndValue(nsAutoString keyName, const WCHAR* subKey,
+ const WCHAR* theValue) {
+ HKEY hKey;
+ BOOL retValue = TRUE;
+
+ nsAutoString theKey(keyName);
+ if (subKey != NULL) {
+ theKey += L"\\";
+ theKey += subKey;
+ }
+
+ // Create and open key and subkey.
+ long lResult = RegCreateKeyExW(HKEY_CLASSES_ROOT, theKey.get(), 0, NULL,
+ REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL,
+ &hKey, NULL);
+ if (lResult != ERROR_SUCCESS) return FALSE;
+
+ // Set the Value.
+ if (theValue != NULL) {
+ lResult = RegSetValueExW(hKey, NULL, 0, REG_SZ, (BYTE*)theValue,
+ wcslen(theValue) + 1);
+ if (lResult != ERROR_SUCCESS) retValue = FALSE;
+ }
+
+ RegCloseKey(hKey);
+ return retValue;
+}
+
+// Delete a key and all of its descendents.
+
+LONG recursiveDeleteKey(HKEY hKeyParent, // Parent of key to delete
+ const WCHAR* lpszKeyChild) // Key to delete
+{
+ // Open the child.
+ HKEY hKeyChild;
+ LONG lRes =
+ RegOpenKeyExW(hKeyParent, lpszKeyChild, 0, KEY_ALL_ACCESS, &hKeyChild);
+ if (lRes != ERROR_SUCCESS) {
+ return lRes;
+ }
+
+ // Enumerate all of the descendants of this child.
+ FILETIME time;
+ WCHAR szBuffer[MAX_SIZE];
+ DWORD dwSize = MAX_SIZE;
+ while (RegEnumKeyExW(hKeyChild, 0, szBuffer, &dwSize, NULL, NULL, NULL,
+ &time) == S_OK) {
+ // Delete the descendants of this child.
+ lRes = recursiveDeleteKey(hKeyChild, szBuffer);
+ if (lRes != ERROR_SUCCESS) {
+ // Cleanup before exiting.
+ RegCloseKey(hKeyChild);
+ return lRes;
+ }
+ dwSize = MAX_SIZE;
+ }
+
+ // Close the child.
+ RegCloseKey(hKeyChild);
+
+ // Delete this child.
+ return RegDeleteKeyW(hKeyParent, lpszKeyChild);
+}
+
+void RegisterProxy() {
+ HINSTANCE h = NULL;
+ ProxyServer* RegisterFunc = NULL;
+
+ WCHAR szModule[MAX_SIZE];
+ WCHAR* pTemp = NULL;
+
+ HMODULE hModule = GetModuleHandleW(NULL);
+ DWORD dwResult =
+ ::GetModuleFileNameW(hModule, szModule, sizeof(szModule) / sizeof(WCHAR));
+ if (dwResult == 0) return;
+
+ pTemp = wcsrchr(szModule, L'\\');
+ if (pTemp == NULL) return;
+
+ *pTemp = '\0';
+ nsAutoString proxyPath(szModule);
+
+ proxyPath += u"\\";
+ proxyPath += MAPI_PROXY_DLL_NAME;
+
+ h = LoadLibraryW(proxyPath.get());
+ if (h == NULL) return;
+
+ RegisterFunc = (ProxyServer*)GetProcAddress(h, "DllRegisterServer");
+ if (RegisterFunc) RegisterFunc();
+
+ FreeLibrary(h);
+}
+
+void UnRegisterProxy() {
+ HINSTANCE h = NULL;
+ ProxyServer* UnRegisterFunc = NULL;
+
+ WCHAR szModule[MAX_SIZE];
+ WCHAR* pTemp = NULL;
+
+ HMODULE hModule = GetModuleHandleW(NULL);
+ DWORD dwResult =
+ ::GetModuleFileNameW(hModule, szModule, sizeof(szModule) / sizeof(WCHAR));
+ if (dwResult == 0) return;
+
+ pTemp = wcsrchr(szModule, L'\\');
+ if (pTemp == NULL) return;
+
+ *pTemp = '\0';
+ nsAutoString proxyPath(szModule);
+
+ proxyPath += u"\\";
+ proxyPath += MAPI_PROXY_DLL_NAME;
+
+ h = LoadLibraryW(proxyPath.get());
+ if (h == NULL) return;
+
+ UnRegisterFunc = (ProxyServer*)GetProcAddress(h, "DllUnregisterServer");
+ if (UnRegisterFunc) UnRegisterFunc();
+
+ FreeLibrary(h);
+}
+
+// Register the component in the registry.
+
+HRESULT RegisterServer(const CLSID& clsid, // Class ID
+ const WCHAR* szFriendlyName, // Friendly Name
+ const WCHAR* szVerIndProgID, // Programmatic
+ const WCHAR* szProgID) // IDs
+{
+ HMODULE hModule = GetModuleHandleW(NULL);
+ WCHAR szModuleName[MAX_SIZE];
+ WCHAR szCLSID[CLSID_STRING_SIZE];
+
+ nsAutoString independentProgId(szVerIndProgID);
+ nsAutoString progId(szProgID);
+
+ DWORD dwResult = ::GetModuleFileNameW(hModule, szModuleName,
+ sizeof(szModuleName) / sizeof(WCHAR));
+
+ if (dwResult == 0) return S_FALSE;
+
+ nsAutoString moduleName(szModuleName);
+ nsAutoString registryKey(L"CLSID\\");
+
+ moduleName += MAPI_STARTUP_ARG;
+
+ // Convert the CLSID into a WCHAR.
+ if (!CLSIDtoWchar(clsid, szCLSID)) return S_FALSE;
+ registryKey += szCLSID;
+
+ // Add the CLSID to the registry.
+ if (!setKeyAndValue(registryKey, NULL, szFriendlyName)) return S_FALSE;
+
+ if (!setKeyAndValue(registryKey, L"LocalServer32", moduleName.get()))
+ return S_FALSE;
+
+ // Add the ProgID subkey under the CLSID key.
+ if (!setKeyAndValue(registryKey, L"ProgID", szProgID)) return S_FALSE;
+
+ // Add the version-independent ProgID subkey under CLSID key.
+ if (!setKeyAndValue(registryKey, L"VersionIndependentProgID", szVerIndProgID))
+ return S_FALSE;
+
+ // Add the version-independent ProgID subkey under HKEY_CLASSES_ROOT.
+ if (!setKeyAndValue(independentProgId, NULL, szFriendlyName)) return S_FALSE;
+ if (!setKeyAndValue(independentProgId, L"CLSID", szCLSID)) return S_FALSE;
+ if (!setKeyAndValue(independentProgId, L"CurVer", szProgID)) return S_FALSE;
+
+ // Add the versioned ProgID subkey under HKEY_CLASSES_ROOT.
+ if (!setKeyAndValue(progId, NULL, szFriendlyName)) return S_FALSE;
+ if (!setKeyAndValue(progId, L"CLSID", szCLSID)) return S_FALSE;
+
+ RegisterProxy();
+
+ return S_OK;
+}
+
+LONG UnregisterServer(const CLSID& clsid, // Class ID
+ const WCHAR* szVerIndProgID, // Programmatic
+ const WCHAR* szProgID) // IDs
+{
+ LONG lResult = S_OK;
+
+ // Convert the CLSID into a char.
+
+ WCHAR szCLSID[CLSID_STRING_SIZE];
+ if (!CLSIDtoWchar(clsid, szCLSID)) return S_FALSE;
+
+ UnRegisterProxy();
+
+ nsAutoString registryKey(L"CLSID\\");
+ registryKey += szCLSID;
+
+ lResult = recursiveDeleteKey(HKEY_CLASSES_ROOT, registryKey.get());
+ if (lResult == ERROR_SUCCESS || lResult == ERROR_FILE_NOT_FOUND)
+ return lResult;
+
+ registryKey += L"\\LocalServer32";
+
+ // Delete only the path for this server.
+
+ lResult = recursiveDeleteKey(HKEY_CLASSES_ROOT, registryKey.get());
+ if (lResult != ERROR_SUCCESS && lResult != ERROR_FILE_NOT_FOUND)
+ return lResult;
+
+ // Delete the version-independent ProgID Key.
+ lResult = recursiveDeleteKey(HKEY_CLASSES_ROOT, szVerIndProgID);
+ if (lResult != ERROR_SUCCESS && lResult != ERROR_FILE_NOT_FOUND)
+ return lResult;
+
+ lResult = recursiveDeleteKey(HKEY_CLASSES_ROOT, szProgID);
+
+ return lResult;
+}
diff --git a/comm/mailnews/mapi/mapihook/src/Registry.h b/comm/mailnews/mapi/mapihook/src/Registry.h
new file mode 100644
index 0000000000..7207694e55
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/Registry.h
@@ -0,0 +1,20 @@
+/* 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/. */
+
+#ifndef _REGISTRY_H_
+#define _REGISTRY_H_
+
+#include <objbase.h>
+
+// This function will register a component in the Registry.
+
+HRESULT RegisterServer(const CLSID& clsid, const WCHAR* szFriendlyName,
+ const WCHAR* szVerIndProgID, const WCHAR* szProgID);
+
+// This function will unregister a component.
+
+HRESULT UnregisterServer(const CLSID& clsid, const WCHAR* szVerIndProgID,
+ const WCHAR* szProgID);
+
+#endif
diff --git a/comm/mailnews/mapi/mapihook/src/components.conf b/comm/mailnews/mapi/mapihook/src/components.conf
new file mode 100644
index 0000000000..d69a1df0d5
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/components.conf
@@ -0,0 +1,18 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{8967fed2-c8bb-11d5-a3e9-00b0d0f3baa7}",
+ "contract_ids": ["@mozilla.org/mapisupport;1"],
+ "type": "nsMapiSupport",
+ "headers": ["/comm/mailnews/mapi/mapihook/src/msgMapiSupport.h"],
+ }
+]
+
+Categories = {
+ "app-startup": {
+ "Mapi Support": "@mozilla.org/mapisupport;1",
+ }
+}
diff --git a/comm/mailnews/mapi/mapihook/src/moz.build b/comm/mailnews/mapi/mapihook/src/moz.build
new file mode 100644
index 0000000000..f2ef529008
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/moz.build
@@ -0,0 +1,33 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "!../build/msgMapi_i.c",
+ "msgMapiFactory.cpp",
+ "msgMapiHook.cpp",
+ "msgMapiImp.cpp",
+ "msgMapiMain.cpp",
+ "msgMapiSupport.cpp",
+ "Registry.cpp",
+]
+
+LOCAL_INCLUDES += ["/comm/mailnews/mapi/include"]
+
+FINAL_LIBRARY = "xul"
+
+OS_LIBS += [
+ "ole32",
+]
+
+DEFINES["UNICODE"] = True
+DEFINES["_UNICODE"] = True
+
+# clang-cl rightly complains about switch on nsresult.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-switch"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiFactory.cpp b/comm/mailnews/mapi/mapihook/src/msgMapiFactory.cpp
new file mode 100644
index 0000000000..cb99fc04fd
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiFactory.cpp
@@ -0,0 +1,64 @@
+/* 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/. */
+
+#undef UNICODE
+#undef _UNICODE
+
+#include "msgMapiFactory.h"
+#include "msgMapiImp.h"
+#include "msgMapi.h"
+
+CMapiFactory ::CMapiFactory() : m_cRef(1) {}
+
+CMapiFactory::~CMapiFactory() {}
+
+STDMETHODIMP CMapiFactory::QueryInterface(const IID& aIid, void** aPpv) {
+ if ((aIid == IID_IUnknown) || (aIid == IID_IClassFactory)) {
+ *aPpv = static_cast<IClassFactory*>(this);
+ } else {
+ *aPpv = nullptr;
+ return E_NOINTERFACE;
+ }
+ reinterpret_cast<IUnknown*>(*aPpv)->AddRef();
+ return S_OK;
+}
+
+STDMETHODIMP_(ULONG) CMapiFactory::AddRef() { return ++m_cRef; }
+
+STDMETHODIMP_(ULONG) CMapiFactory::Release() {
+ int32_t temp = --m_cRef;
+ if (m_cRef == 0) {
+ delete this;
+ return 0;
+ }
+
+ return temp;
+}
+
+STDMETHODIMP CMapiFactory::CreateInstance(IUnknown* aUnknownOuter,
+ const IID& aIid, void** aPpv) {
+ // Cannot aggregate.
+
+ if (aUnknownOuter != nullptr) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ // Create component.
+
+ CMapiImp* pImp = new CMapiImp();
+ if (pImp == nullptr) {
+ return E_OUTOFMEMORY;
+ }
+
+ // Get the requested interface.
+ HRESULT hr = pImp->QueryInterface(aIid, aPpv);
+
+ // Release the IUnknown pointer.
+ // (If QueryInterface failed, component will delete itself.)
+
+ pImp->Release();
+ return hr;
+}
+
+STDMETHODIMP CMapiFactory::LockServer(BOOL aLock) { return S_OK; }
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiFactory.h b/comm/mailnews/mapi/mapihook/src/msgMapiFactory.h
new file mode 100644
index 0000000000..3970b3fa99
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiFactory.h
@@ -0,0 +1,35 @@
+/* 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/. */
+
+#ifndef MSG_MAPI_FACTORY_H
+#define MSG_MAPI_FACTORY_H
+
+#include <windows.h>
+#include <objbase.h>
+#include "nspr.h"
+#include "nsISupportsImpl.h" // ThreadSafeAutoRefCnt
+#include <stdint.h>
+
+class CMapiFactory : public IClassFactory {
+ public:
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aIid, void** aPpv);
+ STDMETHODIMP_(ULONG) AddRef(void);
+ STDMETHODIMP_(ULONG) Release(void);
+
+ // IClassFactory
+
+ STDMETHODIMP CreateInstance(LPUNKNOWN aUnkOuter, REFIID aIid, void** aPpv);
+ STDMETHODIMP LockServer(BOOL aLock);
+
+ CMapiFactory();
+
+ private:
+ mozilla::ThreadSafeAutoRefCnt m_cRef;
+
+ virtual ~CMapiFactory();
+};
+
+#endif // MSG_MAPI_FACTORY_H
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiHook.cpp b/comm/mailnews/mapi/mapihook/src/msgMapiHook.cpp
new file mode 100644
index 0000000000..eb8c9b9fe8
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiHook.cpp
@@ -0,0 +1,934 @@
+/* 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/. */
+
+#define MAPI_STARTUP_ARG "/MAPIStartUp"
+
+#include <mapidefs.h>
+#include <mapi.h>
+#include <direct.h>
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsIPromptService.h"
+#include "nsIAppShellService.h"
+#include "mozIDOMWindow.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIStringBundle.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIMsgAttachment.h"
+#include "nsIMsgCompFields.h"
+#include "nsIMsgComposeParams.h"
+#include "nsIMsgCompose.h"
+#include "nsIMsgSend.h"
+#include "nsIMsgComposeService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "msgMapi.h"
+#include "msgMapiHook.h"
+#include "msgMapiSupport.h"
+#include "msgMapiMain.h"
+#include "nsThreadUtils.h"
+#include "nsMsgUtils.h"
+#include "nsNetUtil.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/Components.h"
+#include "nsEmbedCID.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SpinEventLoopUntil.h"
+
+using namespace mozilla::dom;
+
+extern mozilla::LazyLogModule MAPI; // defined in msgMapiImp.cpp
+
+class MAPISendListener : public nsIMsgSendListener,
+ public mozilla::ReentrantMonitor {
+ public:
+ MAPISendListener()
+ : ReentrantMonitor("MAPISendListener monitor"), m_done(false) {}
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* void OnStartSending (in string aMsgID, in uint32_t aMsgSize); */
+ NS_IMETHOD OnStartSending(const char* aMsgID, uint32_t aMsgSize) {
+ return NS_OK;
+ }
+
+ /* void OnProgress (in string aMsgID, in uint32_t aProgress, in uint32_t
+ * aProgressMax); */
+ NS_IMETHOD OnProgress(const char* aMsgID, uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_OK;
+ }
+
+ /* void OnStatus (in string aMsgID, in wstring aMsg); */
+ NS_IMETHOD OnStatus(const char* aMsgID, const char16_t* aMsg) {
+ return NS_OK;
+ }
+
+ /* void OnStopSending (in string aMsgID, in nsresult aStatus, in wstring aMsg,
+ * in nsIFile returnFile); */
+ NS_IMETHOD OnStopSending(const char* aMsgID, nsresult aStatus,
+ const char16_t* aMsg, nsIFile* returnFile) {
+ mozilla::ReentrantMonitorAutoEnter mon(*this);
+ m_done = true;
+ NotifyAll();
+ return NS_OK;
+ }
+
+ /* void OnTransportSecurityError( in string msgID, in nsresult status, in
+ * nsITransportSecurityInfo secInfo, in ACString location); */
+ NS_IMETHOD OnTransportSecurityError(const char* msgID, nsresult status,
+ nsITransportSecurityInfo* secInfo,
+ nsACString const& location) {
+ return NS_OK;
+ }
+
+ /* void OnSendNotPerformed */
+ NS_IMETHOD OnSendNotPerformed(const char* aMsgID, nsresult aStatus) {
+ return OnStopSending(aMsgID, aStatus, nullptr, nullptr);
+ }
+
+ /* void OnGetDraftFolderURI (); */
+ NS_IMETHOD OnGetDraftFolderURI(const char* aMsgID,
+ const nsACString& aFolderURI) {
+ return NS_OK;
+ }
+
+ bool IsDone() { return m_done; }
+
+ private:
+ bool m_done;
+ virtual ~MAPISendListener() {}
+};
+
+/// Helper for setting up the hidden window for blind MAPI.
+class MOZ_STACK_CLASS AutoHiddenWindow {
+ public:
+ explicit AutoHiddenWindow(nsresult& rv)
+ : mAppService(do_GetService("@mozilla.org/appshell/appShellService;1")) {
+ mCreatedHiddenWindow = false;
+ rv = mAppService->GetHiddenDOMWindow(getter_AddRefs(mHiddenWindow));
+ if (rv == NS_ERROR_FAILURE) {
+ // Try to get a hidden window. If it doesn't exist, create a hidden
+ // window for us to use.
+ rv = mAppService->CreateHiddenWindow();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ mCreatedHiddenWindow = true;
+ rv = mAppService->GetHiddenDOMWindow(getter_AddRefs(mHiddenWindow));
+ }
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ ~AutoHiddenWindow() {
+ if (mCreatedHiddenWindow) mAppService->DestroyHiddenWindow();
+ }
+ mozIDOMWindowProxy* operator->() { return mHiddenWindow; }
+ operator mozIDOMWindowProxy*() { return mHiddenWindow; }
+
+ private:
+ nsCOMPtr<nsIAppShellService> mAppService;
+ nsCOMPtr<mozIDOMWindowProxy> mHiddenWindow;
+ bool mCreatedHiddenWindow;
+};
+
+NS_IMPL_ISUPPORTS(MAPISendListener, nsIMsgSendListener)
+
+bool nsMapiHook::isMapiService = false;
+
+void nsMapiHook::CleanUp() {
+ // This routine will be fully implemented in future
+ // to cleanup mapi related stuff inside mozilla code.
+}
+
+bool nsMapiHook::DisplayLoginDialog(bool aLogin, char16_t** aUsername,
+ char16_t** aPassword) {
+ nsresult rv;
+ bool btnResult = false;
+
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv) && dlgService) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (!bundleService) return false;
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(MAPI_PROPERTIES_CHROME,
+ getter_AddRefs(bundle));
+ if (NS_FAILED(rv) || !bundle) return false;
+
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv =
+ bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(brandBundle));
+ if (NS_FAILED(rv)) return false;
+
+ nsString brandName;
+ rv = brandBundle->GetStringFromName("brandFullName", brandName);
+ if (NS_FAILED(rv)) return false;
+
+ nsString loginTitle;
+ AutoTArray<nsString, 1> brandStrings = {brandName};
+ rv = bundle->FormatStringFromName("loginTitle", brandStrings, loginTitle);
+ if (NS_FAILED(rv)) return false;
+
+ if (aLogin) {
+ nsString loginText;
+ rv = bundle->GetStringFromName("loginTextwithName", loginText);
+ if (NS_FAILED(rv) || loginText.IsEmpty()) return false;
+
+ rv = dlgService->PromptUsernameAndPassword(nullptr, loginTitle.get(),
+ loginText.get(), aUsername,
+ aPassword, &btnResult);
+ } else {
+ // nsString loginString;
+ nsString loginText;
+ AutoTArray<nsString, 1> userNameStrings = {nsDependentString(*aUsername)};
+ rv =
+ bundle->FormatStringFromName("loginText", userNameStrings, loginText);
+ if (NS_FAILED(rv)) return false;
+
+ rv = dlgService->PromptPassword(nullptr, loginTitle.get(),
+ loginText.get(), aPassword, &btnResult);
+ }
+ }
+
+ return btnResult;
+}
+
+bool nsMapiHook::VerifyUserName(const nsCString& aUsername, nsCString& aIdKey) {
+ nsresult rv;
+
+ if (aUsername.IsEmpty()) return false;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager(
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv));
+ if (NS_FAILED(rv)) return false;
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = accountManager->GetAllIdentities(identities);
+ if (NS_FAILED(rv)) return false;
+
+ for (auto thisIdentity : identities) {
+ if (thisIdentity) {
+ nsCString email;
+ rv = thisIdentity->GetEmail(email);
+ if (NS_FAILED(rv)) continue;
+
+ // get the username from the email and compare with the username
+ int32_t index = email.FindChar('@');
+ if (index != -1) email.SetLength(index);
+
+ if (aUsername.Equals(email))
+ return NS_SUCCEEDED(thisIdentity->GetKey(aIdKey));
+ }
+ }
+
+ return false;
+}
+
+bool nsMapiHook::IsBlindSendAllowed() {
+ bool enabled = false;
+ bool warn = true;
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefBranch) {
+ prefBranch->GetBoolPref(PREF_MAPI_WARN_PRIOR_TO_BLIND_SEND, &warn);
+ prefBranch->GetBoolPref(PREF_MAPI_BLIND_SEND_ENABLED, &enabled);
+ }
+ if (!enabled) return false;
+
+ if (!warn) return true; // Everything is okay.
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (!bundleService) return false;
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(MAPI_PROPERTIES_CHROME,
+ getter_AddRefs(bundle));
+ if (NS_FAILED(rv) || !bundle) return false;
+
+ nsString warningMsg;
+ rv = bundle->GetStringFromName("mapiBlindSendWarning", warningMsg);
+ if (NS_FAILED(rv)) return false;
+
+ nsString dontShowAgainMessage;
+ rv = bundle->GetStringFromName("mapiBlindSendDontShowAgain",
+ dontShowAgainMessage);
+ if (NS_FAILED(rv)) return false;
+
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv) || !dlgService) return false;
+
+ bool continueToWarn = true;
+ bool okayToContinue = false;
+ dlgService->ConfirmCheck(nullptr, nullptr, warningMsg.get(),
+ dontShowAgainMessage.get(), &continueToWarn,
+ &okayToContinue);
+
+ if (!continueToWarn && okayToContinue && prefBranch)
+ prefBranch->SetBoolPref(PREF_MAPI_WARN_PRIOR_TO_BLIND_SEND, false);
+
+ return okayToContinue;
+}
+
+// this is used for Send without UI
+nsresult nsMapiHook::BlindSendMail(unsigned long aSession,
+ nsIMsgCompFields* aCompFields) {
+ nsresult rv = NS_OK;
+
+ if (!IsBlindSendAllowed()) return NS_ERROR_FAILURE;
+
+ // Get a hidden window to use for compose.
+ AutoHiddenWindow hiddenWindow(rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // smtp password and Logged in used IdKey from MapiConfig (session obj)
+ nsMAPIConfiguration* pMapiConfig =
+ nsMAPIConfiguration::GetMAPIConfiguration();
+ if (!pMapiConfig) return NS_ERROR_FAILURE; // get the singleton obj
+ char16_t* password = pMapiConfig->GetPassword(aSession);
+
+ // Id key
+ nsCString MsgIdKey;
+ pMapiConfig->GetIdKey(aSession, MsgIdKey);
+
+ // get the MsgIdentity for the above key using AccountManager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1");
+ if (NS_FAILED(rv) || (!accountManager)) return rv;
+
+ nsCOMPtr<nsIMsgIdentity> pMsgId;
+ rv = accountManager->GetIdentity(MsgIdKey, getter_AddRefs(pMsgId));
+ if (NS_FAILED(rv)) return rv;
+
+ // create a send listener to get back the send status
+ RefPtr<MAPISendListener> sendListener = new MAPISendListener;
+
+ // create the compose params object
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv));
+ if (NS_FAILED(rv) || (!pMsgComposeParams)) return rv;
+
+ // populate the compose params
+ bool forcePlainText;
+ aCompFields->GetForcePlainText(&forcePlainText);
+ pMsgComposeParams->SetType(nsIMsgCompType::New);
+ pMsgComposeParams->SetFormat(forcePlainText ? nsIMsgCompFormat::PlainText
+ : nsIMsgCompFormat::HTML);
+ pMsgComposeParams->SetIdentity(pMsgId);
+ pMsgComposeParams->SetComposeFields(aCompFields);
+ pMsgComposeParams->SetSendListener(sendListener);
+ if (password) pMsgComposeParams->SetSmtpPassword(nsDependentString(password));
+
+ // create the nsIMsgCompose object to send the object
+ nsCOMPtr<nsIMsgCompose> pMsgCompose(
+ do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = pMsgCompose->Initialize(pMsgComposeParams, hiddenWindow, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we're in offline mode, we'll need to queue it for later.
+ RefPtr<Promise> promise;
+ rv = pMsgCompose->SendMsg(WeAreOffline() ? nsIMsgSend::nsMsgQueueForLater
+ : nsIMsgSend::nsMsgDeliverNow,
+ pMsgId, nullptr, nullptr, nullptr,
+ getter_AddRefs(promise));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // When offline, the message is saved to Outbox and OnStopSending won't be
+ // called.
+ if (WeAreOffline()) return NS_OK;
+
+ // Wait for OnStopSending to be called.
+ mozilla::SpinEventLoopUntil("nsIMsgCompose::SendMsg is async"_ns,
+ [=]() { return sendListener->IsDone(); });
+
+ return rv;
+}
+
+nsresult nsMapiHook::HandleAttachments(nsIMsgCompFields* aCompFields,
+ int32_t aFileCount,
+ lpnsMapiFileDesc aFiles, bool aIsUTF8) {
+ nsresult rv = NS_OK;
+ // Do nothing if there are no files to process.
+ if (!aFiles || aFileCount <= 0) return NS_OK;
+
+ nsAutoCString Attachments;
+ nsAutoCString TempFiles;
+
+ nsCOMPtr<nsIFile> pFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || (!pFile)) return rv;
+ nsCOMPtr<nsIFile> pTempDir = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || (!pTempDir)) return rv;
+
+ for (int i = 0; i < aFileCount; i++) {
+ if (aFiles[i].lpszPathName) {
+ // check if attachment exists
+ if (!aIsUTF8)
+ pFile->InitWithNativePath(nsDependentCString(aFiles[i].lpszPathName));
+ else
+ pFile->InitWithPath(NS_ConvertUTF8toUTF16(aFiles[i].lpszPathName));
+
+ bool bExist;
+ rv = pFile->Exists(&bExist);
+ MOZ_LOG(
+ MAPI, mozilla::LogLevel::Debug,
+ ("nsMapiHook::HandleAttachments: filename: %s path: %s exists = %s\n",
+ (const char*)aFiles[i].lpszFileName,
+ (const char*)aFiles[i].lpszPathName, bExist ? "true" : "false"));
+ if (NS_FAILED(rv) || (!bExist)) return NS_ERROR_FILE_NOT_FOUND;
+
+ // Temp Directory
+ nsCOMPtr<nsIFile> pTempDir;
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(pTempDir));
+
+ // create a new sub directory called moz_mapi underneath the temp
+ // directory
+ pTempDir->AppendRelativePath(u"moz_mapi"_ns);
+ pTempDir->Exists(&bExist);
+ if (!bExist) {
+ rv = pTempDir->Create(nsIFile::DIRECTORY_TYPE, 777);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // rename or copy the existing temp file with the real file name
+
+ nsAutoString leafName;
+ // convert to Unicode using Platform charset
+ // leafName already contains a unicode leafName from lpszPathName. If we
+ // were given a value for lpszFileName, use it. Otherwise stick with
+ // leafName
+ if (aFiles[i].lpszFileName) {
+ nsAutoString wholeFileName;
+ if (!aIsUTF8)
+ NS_CopyNativeToUnicode(nsDependentCString(aFiles[i].lpszFileName),
+ wholeFileName);
+ else
+ wholeFileName.Append(NS_ConvertUTF8toUTF16(aFiles[i].lpszFileName));
+ // need to find the last '\' and find the leafname from that.
+ int32_t lastSlash = wholeFileName.RFindChar(char16_t('\\'));
+ if (lastSlash != kNotFound)
+ leafName.Assign(Substring(wholeFileName, lastSlash + 1));
+ else
+ leafName.Assign(wholeFileName);
+ } else
+ pFile->GetLeafName(leafName);
+
+ nsCOMPtr<nsIMsgAttachment> attachment =
+ do_CreateInstance("@mozilla.org/messengercompose/attachment;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ attachment->SetName(leafName);
+
+ nsCOMPtr<nsIFile> pTempFile;
+ rv = pTempDir->Clone(getter_AddRefs(pTempFile));
+ if (NS_FAILED(rv) || !pTempFile) return rv;
+
+ pTempFile->Append(leafName);
+ pTempFile->Exists(&bExist);
+ if (bExist) {
+ rv = pTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0777);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pTempFile->Remove(false); // remove so we can copy over it.
+ pTempFile->GetLeafName(leafName);
+ }
+ // copy the file to its new location and file name
+ pFile->CopyTo(pTempDir, leafName);
+ // point pFile to the new location of the attachment
+ pFile->InitWithFile(pTempDir);
+ pFile->Append(leafName);
+
+ // create MsgCompose attachment object
+ attachment->SetTemporary(
+ true); // this one is a temp file so set the flag for MsgCompose
+
+ // now set the attachment object
+ nsAutoCString pURL;
+ NS_GetURLSpecFromFile(pFile, pURL);
+ attachment->SetUrl(pURL);
+
+ // set the file size
+ int64_t fileSize;
+ pFile->GetFileSize(&fileSize);
+ attachment->SetSize(fileSize);
+
+ // add the attachment
+ rv = aCompFields->AddAttachment(attachment);
+ if (NS_FAILED(rv))
+ MOZ_LOG(
+ MAPI, mozilla::LogLevel::Debug,
+ ("nsMapiHook::HandleAttachments: AddAttachment rv = %x\n", rv));
+ }
+ }
+ return rv;
+}
+
+nsresult nsMapiHook::HandleAttachmentsW(nsIMsgCompFields* aCompFields,
+ int32_t aFileCount,
+ lpnsMapiFileDescW aFiles) {
+ nsresult rv = NS_OK;
+ // Do nothing if there are no files to process.
+ if (!aFiles || aFileCount <= 0) return NS_OK;
+
+ nsAutoCString Attachments;
+ nsAutoCString TempFiles;
+
+ nsCOMPtr<nsIFile> pFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || (!pFile)) return rv;
+ nsCOMPtr<nsIFile> pTempDir = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || (!pTempDir)) return rv;
+
+ for (int i = 0; i < aFileCount; i++) {
+ if (aFiles[i].lpszPathName) {
+ // Check if attachment exists.
+ pFile->InitWithPath(nsDependentString(aFiles[i].lpszPathName));
+
+ bool bExist;
+ rv = pFile->Exists(&bExist);
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("nsMapiHook::HandleAttachmentsW: filename: %s path: %s exists = "
+ "%s \n",
+ NS_ConvertUTF16toUTF8(aFiles[i].lpszFileName).get(),
+ NS_ConvertUTF16toUTF8(aFiles[i].lpszPathName).get(),
+ bExist ? "true" : "false"));
+ if (NS_FAILED(rv) || (!bExist)) return NS_ERROR_FILE_NOT_FOUND;
+
+ // Temp Directory.
+ nsCOMPtr<nsIFile> pTempDir;
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(pTempDir));
+
+ // Create a new sub directory called moz_mapi underneath the temp
+ // directory.
+ pTempDir->AppendRelativePath(u"moz_mapi"_ns);
+ pTempDir->Exists(&bExist);
+ if (!bExist) {
+ rv = pTempDir->Create(nsIFile::DIRECTORY_TYPE, 777);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Rename or copy the existing temp file with the real file name.
+
+ nsAutoString leafName;
+ // leafName already contains a unicode leafName from lpszPathName. If we
+ // were given a value for lpszFileName, use it. Otherwise stick with
+ // leafName.
+ if (aFiles[i].lpszFileName) {
+ nsAutoString wholeFileName(aFiles[i].lpszFileName);
+ // Need to find the last '\' and find the leafname from that.
+ int32_t lastSlash = wholeFileName.RFindChar(char16_t('\\'));
+ if (lastSlash != kNotFound)
+ leafName.Assign(Substring(wholeFileName, lastSlash + 1));
+ else
+ leafName.Assign(wholeFileName);
+ } else
+ pFile->GetLeafName(leafName);
+
+ nsCOMPtr<nsIMsgAttachment> attachment =
+ do_CreateInstance("@mozilla.org/messengercompose/attachment;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ attachment->SetName(leafName);
+
+ nsCOMPtr<nsIFile> pTempFile;
+ rv = pTempDir->Clone(getter_AddRefs(pTempFile));
+ if (NS_FAILED(rv) || !pTempFile) return rv;
+
+ pTempFile->Append(leafName);
+ pTempFile->Exists(&bExist);
+ if (bExist) {
+ rv = pTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0777);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pTempFile->Remove(false); // remove so we can copy over it.
+ pTempFile->GetLeafName(leafName);
+ }
+ // Copy the file to its new location and file name.
+ pFile->CopyTo(pTempDir, leafName);
+ // Point pFile to the new location of the attachment.
+ pFile->InitWithFile(pTempDir);
+ pFile->Append(leafName);
+
+ // Create MsgCompose attachment object.
+ attachment->SetTemporary(
+ true); // this one is a temp file so set the flag for MsgCompose
+
+ // Now set the attachment object.
+ nsAutoCString pURL;
+ NS_GetURLSpecFromFile(pFile, pURL);
+ attachment->SetUrl(pURL);
+
+ // Set the file size.
+ int64_t fileSize;
+ pFile->GetFileSize(&fileSize);
+ attachment->SetSize(fileSize);
+
+ // Add the attachment.
+ rv = aCompFields->AddAttachment(attachment);
+ if (NS_FAILED(rv))
+ MOZ_LOG(
+ MAPI, mozilla::LogLevel::Debug,
+ ("nsMapiHook::HandleAttachmentsW: AddAttachment rv = %x\n", rv));
+ }
+ }
+ return rv;
+}
+
+// this is used to convert non Unicode data and then populate comp fields
+nsresult nsMapiHook::PopulateCompFieldsWithConversion(
+ lpnsMapiMessage aMessage, nsIMsgCompFields* aCompFields) {
+ bool isUTF8 = aMessage->ulReserved == CP_UTF8;
+
+ if (aMessage->lpOriginator && aMessage->lpOriginator->lpszAddress) {
+ nsAutoString From;
+ if (!isUTF8)
+ From.Append(NS_ConvertASCIItoUTF16(aMessage->lpOriginator->lpszAddress));
+ else
+ From.Append(NS_ConvertUTF8toUTF16(aMessage->lpOriginator->lpszAddress));
+ aCompFields->SetFrom(From);
+ }
+
+ nsAutoString To;
+ nsAutoString Cc;
+ nsAutoString Bcc;
+ constexpr auto Comma = u","_ns;
+ if (aMessage->lpRecips) {
+ for (int i = 0; i < (int)aMessage->nRecipCount; i++) {
+ if (aMessage->lpRecips[i].lpszAddress || aMessage->lpRecips[i].lpszName) {
+ const char* addressWithoutType = (aMessage->lpRecips[i].lpszAddress)
+ ? aMessage->lpRecips[i].lpszAddress
+ : aMessage->lpRecips[i].lpszName;
+ if (!PL_strncasecmp(addressWithoutType, "SMTP:", 5))
+ addressWithoutType += 5;
+
+ switch (aMessage->lpRecips[i].ulRecipClass) {
+ case MAPI_TO:
+ if (!To.IsEmpty()) To += Comma;
+ if (!isUTF8)
+ To.Append(NS_ConvertASCIItoUTF16(addressWithoutType));
+ else
+ To.Append(NS_ConvertUTF8toUTF16(addressWithoutType));
+ break;
+
+ case MAPI_CC:
+ if (!Cc.IsEmpty()) Cc += Comma;
+ if (!isUTF8)
+ Cc.Append(NS_ConvertASCIItoUTF16(addressWithoutType));
+ else
+ Cc.Append(NS_ConvertUTF8toUTF16(addressWithoutType));
+ break;
+
+ case MAPI_BCC:
+ if (!Bcc.IsEmpty()) Bcc += Comma;
+ if (!isUTF8)
+ Bcc.Append(NS_ConvertASCIItoUTF16(addressWithoutType));
+ else
+ Bcc.Append(NS_ConvertUTF8toUTF16(addressWithoutType));
+ break;
+ }
+ }
+ }
+ }
+
+ // set To, Cc, Bcc
+ aCompFields->SetTo(To);
+ aCompFields->SetCc(Cc);
+ aCompFields->SetBcc(Bcc);
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("to: %s cc: %s bcc: %s \n", NS_ConvertUTF16toUTF8(To).get(),
+ NS_ConvertUTF16toUTF8(Cc).get(), NS_ConvertUTF16toUTF8(Bcc).get()));
+
+ // set subject
+ nsresult rv = NS_OK;
+ if (aMessage->lpszSubject) {
+ nsAutoString Subject;
+ if (!isUTF8)
+ rv = NS_CopyNativeToUnicode(nsDependentCString(aMessage->lpszSubject),
+ Subject);
+ else
+ Subject.Append(NS_ConvertUTF8toUTF16(aMessage->lpszSubject));
+ if (NS_FAILED(rv)) return rv;
+ aCompFields->SetSubject(Subject);
+ }
+
+ // handle attachments as File URL
+ rv = HandleAttachments(aCompFields, aMessage->nFileCount, aMessage->lpFiles,
+ isUTF8);
+ if (NS_FAILED(rv)) return rv;
+
+ // set body
+ if (aMessage->lpszNoteText) {
+ nsAutoString Body;
+ if (!isUTF8)
+ rv = NS_CopyNativeToUnicode(nsDependentCString(aMessage->lpszNoteText),
+ Body);
+ else
+ Body.Append(NS_ConvertUTF8toUTF16(aMessage->lpszNoteText));
+ if (NS_FAILED(rv)) return rv;
+ if (Body.IsEmpty() || Body.Last() != '\n') Body.AppendLiteral(CRLF);
+
+ // This is needed when Simple MAPI is used without a compose window.
+ // See bug 1366196.
+ if (Body.Find(u"<html>") == kNotFound) aCompFields->SetForcePlainText(true);
+
+ rv = aCompFields->SetBody(Body);
+ } else {
+ // No body: Assume that we can do plaintext. This will trigger the default
+ // compose format in ShowComposerWindow().
+ aCompFields->SetForcePlainText(true);
+ }
+
+#ifdef RAJIV_DEBUG
+ // testing what all was set in CompFields
+ printf("To : %S \n", To.get());
+ printf("CC : %S \n", Cc.get());
+ printf("BCC : %S \n", Bcc.get());
+#endif
+
+ return rv;
+}
+
+// This is used to populate comp fields with UTF-16 data from MAPISendMailW
+// function.
+nsresult nsMapiHook::PopulateCompFieldsW(lpnsMapiMessageW aMessage,
+ nsIMsgCompFields* aCompFields) {
+ if (aMessage->lpOriginator && aMessage->lpOriginator->lpszAddress)
+ aCompFields->SetFrom(
+ nsDependentString(aMessage->lpOriginator->lpszAddress));
+
+ nsAutoString To;
+ nsAutoString Cc;
+ nsAutoString Bcc;
+
+ constexpr auto Comma = u","_ns;
+
+ if (aMessage->lpRecips) {
+ for (int i = 0; i < (int)aMessage->nRecipCount; i++) {
+ if (aMessage->lpRecips[i].lpszAddress || aMessage->lpRecips[i].lpszName) {
+ const wchar_t* addressWithoutType =
+ (aMessage->lpRecips[i].lpszAddress)
+ ? aMessage->lpRecips[i].lpszAddress
+ : aMessage->lpRecips[i].lpszName;
+ if (_wcsnicmp(addressWithoutType, L"SMTP:", 5) == 0)
+ addressWithoutType += 5;
+ switch (aMessage->lpRecips[i].ulRecipClass) {
+ case MAPI_TO:
+ if (!To.IsEmpty()) To += Comma;
+ To.Append(nsDependentString(addressWithoutType));
+ break;
+
+ case MAPI_CC:
+ if (!Cc.IsEmpty()) Cc += Comma;
+ Cc.Append(nsDependentString(addressWithoutType));
+ break;
+
+ case MAPI_BCC:
+ if (!Bcc.IsEmpty()) Bcc += Comma;
+ Bcc.Append(nsDependentString(addressWithoutType));
+ break;
+ }
+ }
+ }
+ }
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("to: %s cc: %s bcc: %s \n", NS_ConvertUTF16toUTF8(To).get(),
+ NS_ConvertUTF16toUTF8(Cc).get(), NS_ConvertUTF16toUTF8(Bcc).get()));
+ // set To, Cc, Bcc
+ aCompFields->SetTo(To);
+ aCompFields->SetCc(Cc);
+ aCompFields->SetBcc(Bcc);
+
+ // Set subject.
+ if (aMessage->lpszSubject)
+ aCompFields->SetSubject(nsDependentString(aMessage->lpszSubject));
+
+ // handle attachments as File URL
+ nsresult rv =
+ HandleAttachmentsW(aCompFields, aMessage->nFileCount, aMessage->lpFiles);
+ if (NS_FAILED(rv)) return rv;
+
+ // Set body.
+ if (aMessage->lpszNoteText) {
+ nsString Body(aMessage->lpszNoteText);
+ if (Body.IsEmpty() || Body.Last() != '\n') Body.AppendLiteral(CRLF);
+
+ // This is needed when Simple MAPI is used without a compose window.
+ // See bug 1366196.
+ if (Body.Find(u"<html>") == kNotFound) aCompFields->SetForcePlainText(true);
+
+ rv = aCompFields->SetBody(Body);
+ } else {
+ // No body: Assume that we can do plaintext. This will trigger the default
+ // compose format in ShowComposerWindow().
+ aCompFields->SetForcePlainText(true);
+ }
+ return rv;
+}
+
+// this is used to populate the docs as attachments in the Comp fields for Send
+// Documents
+nsresult nsMapiHook::PopulateCompFieldsForSendDocs(
+ nsIMsgCompFields* aCompFields, ULONG aFlags, LPSTR aDelimChar,
+ LPSTR aFilePaths) {
+ nsAutoCString strDelimChars;
+ nsAutoCString strFilePaths;
+ nsresult rv = NS_OK;
+ bool bExist;
+
+ if (aDelimChar) strDelimChars.Assign(aDelimChar);
+ if (aFilePaths) strFilePaths.Assign(aFilePaths);
+
+ // check for comma in filename
+ if (strDelimChars.FindChar(',') ==
+ kNotFound) // if comma is not in the delimiter specified by user
+ {
+ if (strFilePaths.FindChar(',') !=
+ kNotFound) // if comma found in filenames return error
+ return NS_ERROR_FILE_INVALID_PATH;
+ }
+
+ nsCString Attachments;
+
+ // only 1 file is to be sent, no delim specified
+ if (strDelimChars.IsEmpty()) strDelimChars.Assign(';');
+
+ int32_t offset = 0;
+ int32_t FilePathsLen = strFilePaths.Length();
+ if (FilePathsLen) {
+ nsAutoString Subject;
+
+ // multiple files to be sent, delim specified
+ nsCOMPtr<nsIFile> pFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ if (NS_FAILED(rv) || (!pFile)) return rv;
+
+ char* newFilePaths = (char*)strFilePaths.get();
+ while (offset != kNotFound) {
+ // Temp Directory
+ nsCOMPtr<nsIFile> pTempDir;
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(pTempDir));
+
+ // if not already existing, create another temp dir for mapi within Win
+ // temp dir this is windows only so we can do "\\"
+ pTempDir->AppendRelativePath(u"moz_mapi"_ns);
+ pTempDir->Exists(&bExist);
+ if (!bExist) {
+ rv = pTempDir->Create(nsIFile::DIRECTORY_TYPE, 777);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ nsAutoCString RemainingPaths;
+ RemainingPaths.Assign(newFilePaths);
+ offset = RemainingPaths.Find(strDelimChars);
+ if (offset != kNotFound) {
+ RemainingPaths.SetLength(offset);
+ if ((offset + (int32_t)strDelimChars.Length()) < FilePathsLen)
+ newFilePaths += offset + strDelimChars.Length();
+ else
+ offset = kNotFound;
+ FilePathsLen -= offset + strDelimChars.Length();
+ }
+
+ if (RemainingPaths[1] != ':' && RemainingPaths[1] != '\\') {
+ char cwd[MAX_PATH];
+ if (_getdcwd(_getdrive(), cwd, MAX_PATH)) {
+ nsAutoCString cwdStr;
+ cwdStr.Assign(cwd);
+ cwdStr.Append('\\');
+ RemainingPaths.Insert(cwdStr, 0);
+ }
+ }
+
+ pFile->InitWithNativePath(RemainingPaths);
+
+ rv = pFile->Exists(&bExist);
+ if (NS_FAILED(rv) || (!bExist)) return NS_ERROR_FILE_NOT_FOUND;
+
+ // filename of the file attachment
+ nsAutoString leafName;
+ pFile->GetLeafName(leafName);
+ if (NS_FAILED(rv) || leafName.IsEmpty()) return rv;
+
+ if (!Subject.IsEmpty()) Subject.AppendLiteral(", ");
+ Subject += leafName;
+
+ // create MsgCompose attachment object
+ nsCOMPtr<nsIMsgAttachment> attachment =
+ do_CreateInstance("@mozilla.org/messengercompose/attachment;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsDependentString fileNameNative(leafName.get());
+ rv = pFile->CopyTo(pTempDir, fileNameNative);
+ if (NS_FAILED(rv)) return rv;
+
+ // now turn pTempDir into a full file path to the temp file
+ pTempDir->Append(fileNameNative);
+
+ // this one is a temp file so set the flag for MsgCompose
+ attachment->SetTemporary(true);
+
+ // now set the attachment object
+ nsAutoCString pURL;
+ NS_GetURLSpecFromFile(pTempDir, pURL);
+ attachment->SetUrl(pURL);
+
+ // set the file size
+ int64_t fileSize;
+ pFile->GetFileSize(&fileSize);
+ attachment->SetSize(fileSize);
+
+ // add the attachment
+ rv = aCompFields->AddAttachment(attachment);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ rv = aCompFields->SetBody(Subject);
+ }
+
+ return rv;
+}
+
+// this used for Send with UI
+nsresult nsMapiHook::ShowComposerWindow(unsigned long aSession,
+ nsIMsgCompFields* aCompFields) {
+ nsresult rv = NS_OK;
+
+ // create a send listener to get back the send status
+ RefPtr<MAPISendListener> sendListener = new MAPISendListener;
+
+ // create the compose params object
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv));
+ if (NS_FAILED(rv) || (!pMsgComposeParams)) return rv;
+
+ // If we found HTML, compose in HTML.
+ bool forcePlainText;
+ aCompFields->GetForcePlainText(&forcePlainText);
+ pMsgComposeParams->SetFormat(forcePlainText ? nsIMsgCompFormat::Default
+ : nsIMsgCompFormat::HTML);
+
+ // populate the compose params
+ pMsgComposeParams->SetType(nsIMsgCompType::New);
+
+ // Never force to plain text, the default format will take care of that.
+ // Undo the forcing that happened in
+ // PopulateCompFields/PopulateCompFieldsWithConversion. See bug 1095629 and
+ // bug 1366196.
+ aCompFields->SetForcePlainText(false);
+ pMsgComposeParams->SetComposeFields(aCompFields);
+ pMsgComposeParams->SetSendListener(sendListener);
+
+ /** get the nsIMsgComposeService object to open the compose window **/
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1");
+ if (NS_FAILED(rv) || (!compService)) return rv;
+
+ rv = compService->OpenComposeWindowWithParams(nullptr, pMsgComposeParams);
+ if (NS_FAILED(rv)) return rv;
+
+ return rv;
+}
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiHook.h b/comm/mailnews/mapi/mapihook/src/msgMapiHook.h
new file mode 100644
index 0000000000..aa6ad1d66f
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiHook.h
@@ -0,0 +1,39 @@
+/* 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/. */
+
+#ifndef MSG_MAPI_HOOK_H_
+#define MSG_MAPI_HOOK_H_
+
+#include "prtypes.h"
+
+class nsMapiHook {
+ public:
+ static bool DisplayLoginDialog(bool aLogin, char16_t** aUsername,
+ char16_t** aPassword);
+ static bool VerifyUserName(const nsCString& aUsername, nsCString& aIdKey);
+
+ static bool IsBlindSendAllowed();
+ static nsresult BlindSendMail(unsigned long aSession,
+ nsIMsgCompFields* aCompFields);
+ static nsresult ShowComposerWindow(unsigned long aSession,
+ nsIMsgCompFields* aCompFields);
+ static nsresult PopulateCompFieldsWithConversion(
+ lpnsMapiMessage aMessage, nsIMsgCompFields* aCompFields);
+ static nsresult PopulateCompFieldsW(lpnsMapiMessageW aMessage,
+ nsIMsgCompFields* aCompFields);
+ static nsresult PopulateCompFieldsForSendDocs(nsIMsgCompFields* aCompFields,
+ ULONG aFlags, LPSTR aDelimChar,
+ LPSTR aFilePaths);
+ static nsresult HandleAttachments(nsIMsgCompFields* aCompFields,
+ int32_t aFileCount, lpnsMapiFileDesc aFiles,
+ bool aIsUTF8);
+ static nsresult HandleAttachmentsW(nsIMsgCompFields* aCompFields,
+ int32_t aFileCount,
+ lpnsMapiFileDescW aFiles);
+ static void CleanUp();
+
+ static bool isMapiService;
+};
+
+#endif // MSG_MAPI_HOOK_H_
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiImp.cpp b/comm/mailnews/mapi/mapihook/src/msgMapiImp.cpp
new file mode 100644
index 0000000000..62abed1f7b
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiImp.cpp
@@ -0,0 +1,801 @@
+/* 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/. */
+
+#include <mapidefs.h>
+#include <mapi.h>
+#include <winstring.h>
+#include "msgMapiImp.h"
+#include "msgMapiFactory.h"
+#include "msgMapiMain.h"
+
+#include "nsIMsgCompFields.h"
+#include "msgMapiHook.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgHdr.h"
+#include "MailNewsTypes.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include <time.h>
+#include "nsIInputStream.h"
+#include "nsILineInputStream.h"
+#include "nsISeekableStream.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsNetCID.h"
+#include "nsMsgMessageFlags.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla::mailnews;
+
+mozilla::LazyLogModule MAPI("MAPI");
+
+CMapiImp::CMapiImp() : m_cRef(1) { m_Lock = PR_NewLock(); }
+
+CMapiImp::~CMapiImp() {
+ if (m_Lock) PR_DestroyLock(m_Lock);
+}
+
+STDMETHODIMP CMapiImp::QueryInterface(const IID& aIid, void** aPpv) {
+ if (aIid == IID_IUnknown) {
+ *aPpv = static_cast<nsIMapi*>(this);
+ } else if (aIid == IID_nsIMapi) {
+ *aPpv = static_cast<nsIMapi*>(this);
+ } else {
+ *aPpv = nullptr;
+ return E_NOINTERFACE;
+ }
+
+ reinterpret_cast<IUnknown*>(*aPpv)->AddRef();
+ return S_OK;
+}
+
+STDMETHODIMP_(ULONG) CMapiImp::AddRef() { return ++m_cRef; }
+
+STDMETHODIMP_(ULONG) CMapiImp::Release() {
+ int32_t temp = --m_cRef;
+ if (m_cRef == 0) {
+ delete this;
+ return 0;
+ }
+
+ return temp;
+}
+
+STDMETHODIMP CMapiImp::IsValid() { return S_OK; }
+
+STDMETHODIMP CMapiImp::IsValidSession(unsigned long aSession) {
+ nsMAPIConfiguration* pConfig = nsMAPIConfiguration::GetMAPIConfiguration();
+ if (pConfig && pConfig->IsSessionValid(aSession)) return S_OK;
+
+ return E_FAIL;
+}
+
+STDMETHODIMP CMapiImp::Initialize() {
+ HRESULT hr = E_FAIL;
+
+ if (!m_Lock) return E_FAIL;
+
+ PR_Lock(m_Lock);
+
+ // Initialize MAPI Configuration
+
+ nsMAPIConfiguration* pConfig = nsMAPIConfiguration::GetMAPIConfiguration();
+ if (pConfig != nullptr) hr = S_OK;
+
+ PR_Unlock(m_Lock);
+
+ return hr;
+}
+
+STDMETHODIMP CMapiImp::Login(unsigned long aUIArg, LPSTR aLogin,
+ LPSTR aPassWord, unsigned long aFlags,
+ unsigned long* aSessionId) {
+ HRESULT hr = E_FAIL;
+ bool bNewSession = false;
+ nsCString id_key;
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::Login using flags %lu", aFlags));
+ if (aFlags & MAPI_NEW_SESSION) bNewSession = true;
+
+ // Check For Profile Name
+ if (aLogin != nullptr && aLogin[0] != '\0') {
+ if (!nsMapiHook::VerifyUserName(nsDependentCString(aLogin), id_key)) {
+ *aSessionId = MAPI_E_LOGIN_FAILURE;
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::Login failed for username %s", aLogin));
+ NS_ASSERTION(false, "failed verifying user name");
+ return hr;
+ }
+ } else {
+ // get default account
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, MAPI_E_LOGIN_FAILURE);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->GetDefaultAccount(getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, MAPI_E_LOGIN_FAILURE);
+ if (!account) return MAPI_E_LOGIN_FAILURE;
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = account->GetDefaultIdentity(getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, MAPI_E_LOGIN_FAILURE);
+ if (!identity) return MAPI_E_LOGIN_FAILURE;
+ identity->GetKey(id_key);
+ }
+
+ // finally register(create) the session.
+ uint32_t nSession_Id;
+ int16_t nResult = 0;
+
+ nsMAPIConfiguration* pConfig = nsMAPIConfiguration::GetMAPIConfiguration();
+ if (pConfig != nullptr)
+ nResult = pConfig->RegisterSession(
+ aUIArg, aLogin ? nsDependentCString(aLogin) : EmptyCString(),
+ aPassWord ? nsDependentCString(aPassWord) : EmptyCString(),
+ (aFlags & MAPI_FORCE_DOWNLOAD), bNewSession, &nSession_Id,
+ id_key.get());
+ switch (nResult) {
+ case -1: {
+ *aSessionId = MAPI_E_TOO_MANY_SESSIONS;
+ return hr;
+ }
+ case 0: {
+ *aSessionId = MAPI_E_INSUFFICIENT_MEMORY;
+ return hr;
+ }
+ default: {
+ *aSessionId = nSession_Id;
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug, ("CMapiImp::Login succeeded"));
+ break;
+ }
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP CMapiImp::SendMail(unsigned long aSession,
+ lpnsMapiMessage aMessage, unsigned long aFlags,
+ unsigned long aReserved) {
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::SendMail flags=%lx subject: %s sender: %s", aFlags,
+ (aMessage && aMessage->lpszSubject) ? aMessage->lpszSubject
+ : "(no subject)",
+ (aMessage && aMessage->lpOriginator &&
+ aMessage->lpOriginator->lpszAddress)
+ ? aMessage->lpOriginator->lpszAddress
+ : "(no sender)"));
+
+ /** create nsIMsgCompFields obj and populate it **/
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompFields> pCompFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+ if (NS_FAILED(rv) || (!pCompFields)) return MAPI_E_INSUFFICIENT_MEMORY;
+
+ if (aMessage)
+ rv = nsMapiHook::PopulateCompFieldsWithConversion(aMessage, pCompFields);
+
+ if (NS_SUCCEEDED(rv)) {
+ // see flag to see if UI needs to be brought up
+ if (!(aFlags & MAPI_DIALOG)) {
+ rv = nsMapiHook::BlindSendMail(aSession, pCompFields);
+ } else {
+ rv = nsMapiHook::ShowComposerWindow(aSession, pCompFields);
+ }
+ }
+
+ return nsMAPIConfiguration::GetMAPIErrorFromNSError(rv);
+}
+
+STDMETHODIMP CMapiImp::SendMailW(unsigned long aSession,
+ lpnsMapiMessageW aMessage,
+ unsigned long aFlags,
+ unsigned long aReserved) {
+ MOZ_LOG(
+ MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::SendMailW flags=%lx subject: %s sender: %s", aFlags,
+ (aMessage && aMessage->lpszSubject)
+ ? NS_ConvertUTF16toUTF8(aMessage->lpszSubject).get()
+ : "(no subject)",
+ (aMessage && aMessage->lpOriginator &&
+ aMessage->lpOriginator->lpszAddress)
+ ? NS_ConvertUTF16toUTF8(aMessage->lpOriginator->lpszAddress).get()
+ : "(no sender)"));
+
+ // Create nsIMsgCompFields obj and populate it.
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgCompFields> pCompFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+ if (NS_FAILED(rv) || !pCompFields) return MAPI_E_INSUFFICIENT_MEMORY;
+
+ if (aMessage) rv = nsMapiHook::PopulateCompFieldsW(aMessage, pCompFields);
+
+ if (NS_SUCCEEDED(rv)) {
+ // Check flag to see if UI needs to be brought up.
+ if (!(aFlags & MAPI_DIALOG)) {
+ rv = nsMapiHook::BlindSendMail(aSession, pCompFields);
+ } else {
+ rv = nsMapiHook::ShowComposerWindow(aSession, pCompFields);
+ }
+ }
+
+ return nsMAPIConfiguration::GetMAPIErrorFromNSError(rv);
+}
+
+STDMETHODIMP CMapiImp::SendDocuments(unsigned long aSession, LPSTR aDelimChar,
+ LPSTR aFilePaths, LPSTR aFileNames,
+ ULONG aFlags) {
+ nsresult rv = NS_OK;
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::SendDocument using flags %lu", aFlags));
+ /** create nsIMsgCompFields obj and populate it **/
+ nsCOMPtr<nsIMsgCompFields> pCompFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+ if (NS_FAILED(rv) || (!pCompFields)) return MAPI_E_INSUFFICIENT_MEMORY;
+
+ if (aFilePaths) {
+ rv = nsMapiHook::PopulateCompFieldsForSendDocs(pCompFields, aFlags,
+ aDelimChar, aFilePaths);
+ }
+
+ if (NS_SUCCEEDED(rv))
+ rv = nsMapiHook::ShowComposerWindow(aSession, pCompFields);
+ else
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::SendDocument error rv = %x, paths = %s names = %s", rv,
+ aFilePaths, aFileNames));
+
+ return nsMAPIConfiguration::GetMAPIErrorFromNSError(rv);
+}
+
+nsresult CMapiImp::GetDefaultInbox(nsIMsgFolder** inboxFolder) {
+ // get default account
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->GetDefaultAccount(getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!account) return NS_ERROR_FAILURE;
+
+ // get incoming server
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = account->GetIncomingServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString type;
+ rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we only care about imap and pop3
+ if (type.EqualsLiteral("imap") || type.EqualsLiteral("pop3")) {
+ // imap and pop3 account should have an Inbox
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!rootMsgFolder) return NS_ERROR_FAILURE;
+
+ rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, inboxFolder);
+ if (!*inboxFolder) return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+//*****************************************************************************
+// Encapsulate the XP DB stuff required to enumerate messages
+
+class MsgMapiListContext {
+ public:
+ MsgMapiListContext() {}
+ ~MsgMapiListContext();
+
+ nsresult OpenDatabase(nsIMsgFolder* folder);
+
+ nsMsgKey GetNext();
+ nsresult MarkRead(nsMsgKey key, bool read);
+
+ lpnsMapiMessage GetMessage(nsMsgKey, unsigned long flFlags);
+ bool IsIMAPHost(void);
+ bool DeleteMessage(nsMsgKey key);
+
+ protected:
+ char* ConvertDateToMapiFormat(time_t);
+ char* ConvertBodyToMapiFormat(nsIMsgDBHdr* hdr);
+ void ConvertRecipientsToMapiFormat(
+ const nsCOMArray<msgIAddressObject>& ourRecips,
+ lpnsMapiRecipDesc mapiRecips, int mapiRecipClass);
+
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgDatabase> m_db;
+ nsCOMPtr<nsIMsgEnumerator> m_msgEnumerator;
+};
+
+LONG CMapiImp::InitContext(unsigned long session,
+ MsgMapiListContext** listContext) {
+ nsMAPIConfiguration* pMapiConfig =
+ nsMAPIConfiguration::GetMAPIConfiguration();
+ if (!pMapiConfig) return MAPI_E_FAILURE; // get the singleton obj
+ *listContext = (MsgMapiListContext*)pMapiConfig->GetMapiListContext(session);
+ // This is the first message
+ if (!*listContext) {
+ nsCOMPtr<nsIMsgFolder> inboxFolder;
+ nsresult rv = GetDefaultInbox(getter_AddRefs(inboxFolder));
+ if (NS_FAILED(rv)) {
+ NS_ASSERTION(false, "in init context, no inbox");
+ return (MAPI_E_NO_MESSAGES);
+ }
+
+ *listContext = new MsgMapiListContext;
+ if (!*listContext) return MAPI_E_INSUFFICIENT_MEMORY;
+
+ rv = (*listContext)->OpenDatabase(inboxFolder);
+ if (NS_FAILED(rv)) {
+ pMapiConfig->SetMapiListContext(session, NULL);
+ delete *listContext;
+ NS_ASSERTION(false, "in init context, unable to open db");
+ return MAPI_E_NO_MESSAGES;
+ } else
+ pMapiConfig->SetMapiListContext(session, *listContext);
+ }
+ return SUCCESS_SUCCESS;
+}
+
+STDMETHODIMP CMapiImp::FindNext(unsigned long aSession, unsigned long ulUIParam,
+ LPSTR lpszMessageType, LPSTR lpszSeedMessageID,
+ unsigned long flFlags, unsigned long ulReserved,
+ unsigned char lpszMessageID[64])
+
+{
+ //
+ // If this is true, then this is the first call to this FindNext function
+ // and we should start the enumeration operation.
+ //
+
+ *lpszMessageID = '\0';
+ nsMAPIConfiguration* pMapiConfig =
+ nsMAPIConfiguration::GetMAPIConfiguration();
+ if (!pMapiConfig) {
+ NS_ASSERTION(false, "failed to get config in findnext");
+ return MAPI_E_FAILURE; // get the singleton obj
+ }
+ MsgMapiListContext* listContext;
+ LONG ret = InitContext(aSession, &listContext);
+ if (ret != SUCCESS_SUCCESS) {
+ NS_ASSERTION(false, "init context failed");
+ return ret;
+ }
+ NS_ASSERTION(listContext, "initContext returned null context");
+ if (listContext) {
+ // NS_ASSERTION(false, "find next init context succeeded");
+ nsMsgKey nextKey = listContext->GetNext();
+ if (nextKey == nsMsgKey_None) {
+ pMapiConfig->SetMapiListContext(aSession, NULL);
+ delete listContext;
+ return (MAPI_E_NO_MESSAGES);
+ }
+
+ // TRACE("MAPI: ProcessMAPIFindNext() Found message id = %d\n", nextKey);
+
+ sprintf((char*)lpszMessageID, "%d", nextKey);
+ }
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::FindNext returning key %s", (char*)lpszMessageID));
+ return (SUCCESS_SUCCESS);
+}
+
+STDMETHODIMP CMapiImp::ReadMail(unsigned long aSession, unsigned long ulUIParam,
+ LPSTR lpszMessageID, unsigned long flFlags,
+ unsigned long ulReserved,
+ lpnsMapiMessage* lppMessage) {
+ nsresult irv;
+ nsAutoCString keyString((char*)lpszMessageID);
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("CMapiImp::ReadMail asking for key %s", (char*)lpszMessageID));
+ nsMsgKey msgKey = keyString.ToInteger(&irv);
+ if (NS_FAILED(irv)) {
+ NS_ASSERTION(false, "invalid lpszMessageID");
+ return MAPI_E_INVALID_MESSAGE;
+ }
+ MsgMapiListContext* listContext;
+ LONG ret = InitContext(aSession, &listContext);
+ if (ret != SUCCESS_SUCCESS) {
+ NS_ASSERTION(false, "init context failed in ReadMail");
+ return ret;
+ }
+ *lppMessage = listContext->GetMessage(msgKey, flFlags);
+ NS_ASSERTION(*lppMessage, "get message failed");
+
+ return (*lppMessage) ? SUCCESS_SUCCESS : E_FAIL;
+}
+
+STDMETHODIMP CMapiImp::DeleteMail(unsigned long aSession,
+ unsigned long ulUIParam, LPSTR lpszMessageID,
+ unsigned long flFlags,
+ unsigned long ulReserved) {
+ nsresult irv;
+ nsAutoCString keyString((char*)lpszMessageID);
+ nsMsgKey msgKey = keyString.ToInteger(&irv);
+ // XXX Why do we return success on failure?
+ if (NS_FAILED(irv)) return SUCCESS_SUCCESS;
+ MsgMapiListContext* listContext;
+ LONG ret = InitContext(aSession, &listContext);
+ if (ret != SUCCESS_SUCCESS) return ret;
+ return (listContext->DeleteMessage(msgKey)) ? SUCCESS_SUCCESS
+ : MAPI_E_INVALID_MESSAGE;
+}
+
+STDMETHODIMP CMapiImp::SaveMail(unsigned long aSession, unsigned long ulUIParam,
+ lpnsMapiMessage lppMessage,
+ unsigned long flFlags, unsigned long ulReserved,
+ LPSTR lpszMessageID) {
+ MsgMapiListContext* listContext;
+ LONG ret = InitContext(aSession, &listContext);
+ if (ret != SUCCESS_SUCCESS) return ret;
+ return S_OK;
+}
+
+STDMETHODIMP CMapiImp::Logoff(unsigned long aSession) {
+ nsMAPIConfiguration* pConfig = nsMAPIConfiguration::GetMAPIConfiguration();
+
+ if (pConfig->UnRegisterSession((uint32_t)aSession)) return S_OK;
+
+ return E_FAIL;
+}
+
+STDMETHODIMP CMapiImp::CleanUp() {
+ nsMapiHook::CleanUp();
+ return S_OK;
+}
+
+#define MAX_NAME_LEN 256
+
+MsgMapiListContext::~MsgMapiListContext() {
+ if (m_db) m_db->Close(false);
+}
+
+nsresult MsgMapiListContext::OpenDatabase(nsIMsgFolder* folder) {
+ nsresult dbErr = NS_ERROR_FAILURE;
+ if (folder) {
+ m_folder = folder;
+ dbErr = folder->GetMsgDatabase(getter_AddRefs(m_db));
+ if (m_db) dbErr = m_db->EnumerateMessages(getter_AddRefs(m_msgEnumerator));
+ }
+ return dbErr;
+}
+
+bool MsgMapiListContext::IsIMAPHost(void) {
+ if (!m_folder) return FALSE;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder);
+
+ return imapFolder != nullptr;
+}
+
+nsMsgKey MsgMapiListContext::GetNext() {
+ nsMsgKey key = nsMsgKey_None;
+ bool keepTrying = TRUE;
+
+ // NS_ASSERTION (m_msgEnumerator && m_db, "need enumerator and db");
+ if (m_msgEnumerator && m_db) {
+ do {
+ keepTrying = FALSE;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (NS_SUCCEEDED(m_msgEnumerator->GetNext(getter_AddRefs(msgHdr))) &&
+ msgHdr) {
+ msgHdr->GetMessageKey(&key);
+
+ // Check here for IMAP message...if not, just return...
+ if (!IsIMAPHost()) return key;
+
+ // If this is an IMAP message, we have to make sure we have a valid
+ // body to work with.
+ uint32_t flags = 0;
+
+ (void)msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Offline) return key;
+
+ // Ok, if we get here, we have an IMAP message without a body!
+ // We need to keep trying by calling the GetNext member recursively...
+ keepTrying = TRUE;
+ }
+ } while (keepTrying);
+ }
+
+ return key;
+}
+
+nsresult MsgMapiListContext::MarkRead(nsMsgKey key, bool read) {
+ nsresult err = NS_ERROR_FAILURE;
+ NS_ASSERTION(m_db, "no db");
+ if (m_db) err = m_db->MarkRead(key, read, nullptr);
+ return err;
+}
+
+lpnsMapiMessage MsgMapiListContext::GetMessage(nsMsgKey key,
+ unsigned long flFlags) {
+ lpnsMapiMessage message =
+ (lpnsMapiMessage)CoTaskMemAlloc(sizeof(nsMapiMessage));
+ memset(message, 0, sizeof(nsMapiMessage));
+ if (message) {
+ nsCString subject;
+ nsCString author;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+
+ m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ msgHdr->GetSubject(subject);
+ message->lpszSubject = (char*)CoTaskMemAlloc(subject.Length() + 1);
+ strcpy((char*)message->lpszSubject, subject.get());
+ uint32_t date;
+ (void)msgHdr->GetDateInSeconds(&date);
+ message->lpszDateReceived = ConvertDateToMapiFormat(date);
+
+ // Pull out the flags info
+ // anything to do with MAPI_SENT? Since we're only reading the Inbox, I
+ // guess not
+ uint32_t ourFlags;
+ (void)msgHdr->GetFlags(&ourFlags);
+ if (!(ourFlags & nsMsgMessageFlags::Read))
+ message->flFlags |= MAPI_UNREAD;
+ if (ourFlags & (nsMsgMessageFlags::MDNReportNeeded |
+ nsMsgMessageFlags::MDNReportSent))
+ message->flFlags |= MAPI_RECEIPT_REQUESTED;
+
+ // Pull out the author/originator info
+ message->lpOriginator =
+ (lpnsMapiRecipDesc)CoTaskMemAlloc(sizeof(nsMapiRecipDesc));
+ memset(message->lpOriginator, 0, sizeof(nsMapiRecipDesc));
+ if (message->lpOriginator) {
+ msgHdr->GetAuthor(getter_Copies(author));
+ ConvertRecipientsToMapiFormat(EncodedHeader(author),
+ message->lpOriginator, MAPI_ORIG);
+ }
+ // Pull out the To/CC info
+ nsCString recipients, ccList;
+ msgHdr->GetRecipients(getter_Copies(recipients));
+ msgHdr->GetCcList(getter_Copies(ccList));
+
+ nsCOMArray<msgIAddressObject> parsedToRecips = EncodedHeader(recipients);
+ nsCOMArray<msgIAddressObject> parsedCCRecips = EncodedHeader(ccList);
+ uint32_t numToRecips = parsedToRecips.Length();
+ uint32_t numCCRecips = parsedCCRecips.Length();
+
+ message->lpRecips = (lpnsMapiRecipDesc)CoTaskMemAlloc(
+ (numToRecips + numCCRecips) * sizeof(MapiRecipDesc));
+ memset(message->lpRecips, 0,
+ (numToRecips + numCCRecips) * sizeof(MapiRecipDesc));
+ if (message->lpRecips) {
+ ConvertRecipientsToMapiFormat(parsedToRecips, message->lpRecips,
+ MAPI_TO);
+ ConvertRecipientsToMapiFormat(parsedCCRecips,
+ &message->lpRecips[numToRecips], MAPI_CC);
+ }
+
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("MsgMapiListContext::GetMessage flags=%lu subject %s date %s "
+ "sender %s",
+ flFlags, (char*)message->lpszSubject,
+ (char*)message->lpszDateReceived, author.get()));
+
+ // Convert any body text that we have locally
+ if (!(flFlags & MAPI_ENVELOPE_ONLY))
+ message->lpszNoteText = (char*)ConvertBodyToMapiFormat(msgHdr);
+ }
+ if (!(flFlags & (MAPI_PEEK | MAPI_ENVELOPE_ONLY)))
+ m_db->MarkRead(key, true, nullptr);
+ }
+ return message;
+}
+
+char* MsgMapiListContext::ConvertDateToMapiFormat(time_t ourTime) {
+ char* date = (char*)CoTaskMemAlloc(32);
+ if (date) {
+ // MAPI time format is YYYY/MM/DD HH:MM
+ // Note that we're not using XP_StrfTime because that localizes the time
+ // format, and the way I read the MAPI spec is that their format is
+ // canonical, not localized.
+ struct tm* local = localtime(&ourTime);
+ if (local)
+ strftime(date, 32, "%Y/%m/%d %I:%M",
+ local); // use %H if hours should be 24 hour format
+ }
+ return date;
+}
+
+void MsgMapiListContext::ConvertRecipientsToMapiFormat(
+ const nsCOMArray<msgIAddressObject>& recipients,
+ lpnsMapiRecipDesc mapiRecips, int mapiRecipClass) {
+ nsTArray<nsCString> names, addresses;
+ ExtractAllAddresses(recipients, UTF16ArrayAdapter<>(names),
+ UTF16ArrayAdapter<>(addresses));
+
+ size_t numAddresses = names.Length();
+ for (size_t i = 0; i < numAddresses; i++) {
+ if (!names[i].IsEmpty()) {
+ mapiRecips[i].lpszName = (char*)CoTaskMemAlloc(names[i].Length() + 1);
+ if (mapiRecips[i].lpszName)
+ strcpy((char*)mapiRecips[i].lpszName, names[i].get());
+ }
+ if (!addresses[i].IsEmpty()) {
+ mapiRecips[i].lpszName = (char*)CoTaskMemAlloc(addresses[i].Length() + 1);
+ if (mapiRecips[i].lpszName)
+ strcpy((char*)mapiRecips[i].lpszName, addresses[i].get());
+ }
+ mapiRecips[i].ulRecipClass = mapiRecipClass;
+ }
+}
+
+char* MsgMapiListContext::ConvertBodyToMapiFormat(nsIMsgDBHdr* hdr) {
+ const int kBufLen =
+ 64000; // I guess we only return the first 64K of a message.
+#define EMPTY_MESSAGE_LINE(buf) \
+ (buf[0] == '\r' || buf[0] == '\n' || buf[0] == '\0')
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ if (!folder) return nullptr;
+
+ nsCOMPtr<nsIFile> localFile;
+ folder->GetFilePath(getter_AddRefs(localFile));
+
+ nsresult rv;
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ rv = fileStream->Init(localFile, PR_RDONLY, 0664,
+ false); // just have to read the messages
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsILineInputStream> fileLineStream = do_QueryInterface(fileStream);
+ if (!fileLineStream) return nullptr;
+
+ // ### really want to skip past headers...
+ uint64_t messageOffset;
+ uint32_t lineCount;
+ hdr->GetMessageOffset(&messageOffset);
+ hdr->GetLineCount(&lineCount);
+ nsCOMPtr<nsISeekableStream> seekableStream = do_QueryInterface(fileStream);
+ seekableStream->Seek(PR_SEEK_SET, messageOffset);
+ bool hasMore = true;
+ nsAutoCString curLine;
+
+ while (hasMore) // advance past message headers
+ {
+ nsresult rv = fileLineStream->ReadLine(curLine, &hasMore);
+ if (NS_FAILED(rv) || EMPTY_MESSAGE_LINE(curLine)) break;
+ }
+ uint32_t msgSize;
+ hdr->GetMessageSize(&msgSize);
+ if (msgSize > kBufLen) msgSize = kBufLen - 1;
+ // this is too big, since it includes the msg hdr size...oh well
+ char* body = (char*)CoTaskMemAlloc(msgSize + 1);
+
+ if (!body) return nullptr;
+ int32_t bytesCopied = 0;
+ for (hasMore = TRUE; lineCount > 0 && hasMore && NS_SUCCEEDED(rv);
+ lineCount--) {
+ rv = fileLineStream->ReadLine(curLine, &hasMore);
+ if (NS_FAILED(rv)) break;
+ curLine.Append(CRLF);
+ // make sure we have room left
+ if (bytesCopied + curLine.Length() < msgSize) {
+ strcpy(body + bytesCopied, curLine.get());
+ bytesCopied += curLine.Length();
+ }
+ }
+ MOZ_LOG(MAPI, mozilla::LogLevel::Debug,
+ ("ConvertBodyToMapiFormat size=%x allocated size %x body = %100.100s",
+ bytesCopied, msgSize + 1, (char*)body));
+ body[bytesCopied] = '\0'; // rhp - fix last line garbage...
+ return body;
+}
+
+//*****************************************************************************
+// MSGMAPI API implementation
+
+static void msg_FreeMAPIFile(lpMapiFileDesc f) {
+ if (f) {
+ CoTaskMemFree(f->lpszPathName);
+ CoTaskMemFree(f->lpszFileName);
+ }
+}
+
+static void msg_FreeMAPIRecipient(lpMapiRecipDesc rd) {
+ if (rd) {
+ if (rd->lpszName) CoTaskMemFree(rd->lpszName);
+ if (rd->lpszAddress) CoTaskMemFree(rd->lpszAddress);
+ // CoTaskMemFree(rd->lpEntryID);
+ }
+}
+
+extern "C" void MSG_FreeMapiMessage(lpMapiMessage msg) {
+ ULONG i;
+
+ if (msg) {
+ CoTaskMemFree(msg->lpszSubject);
+ CoTaskMemFree(msg->lpszNoteText);
+ CoTaskMemFree(msg->lpszMessageType);
+ CoTaskMemFree(msg->lpszDateReceived);
+ CoTaskMemFree(msg->lpszConversationID);
+
+ if (msg->lpOriginator) msg_FreeMAPIRecipient(msg->lpOriginator);
+
+ for (i = 0; i < msg->nRecipCount; i++)
+ if (&(msg->lpRecips[i]) != nullptr)
+ msg_FreeMAPIRecipient(&(msg->lpRecips[i]));
+
+ CoTaskMemFree(msg->lpRecips);
+
+ for (i = 0; i < msg->nFileCount; i++)
+ if (&(msg->lpFiles[i]) != nullptr) msg_FreeMAPIFile(&(msg->lpFiles[i]));
+
+ CoTaskMemFree(msg->lpFiles);
+
+ CoTaskMemFree(msg);
+ }
+}
+
+extern "C" bool MsgMarkMapiMessageRead(nsIMsgFolder* folder, nsMsgKey key,
+ bool read) {
+ bool success = FALSE;
+ MsgMapiListContext* context = new MsgMapiListContext();
+ if (context) {
+ if (NS_SUCCEEDED(context->OpenDatabase(folder))) {
+ if (NS_SUCCEEDED(context->MarkRead(key, read))) success = TRUE;
+ }
+ delete context;
+ }
+ return success;
+}
+
+bool MsgMapiListContext::DeleteMessage(nsMsgKey key) {
+ if (!m_db) return FALSE;
+
+ if (!IsIMAPHost()) {
+ nsTArray<nsMsgKey> doomed({key});
+ return NS_SUCCEEDED((m_db->DeleteMessages(doomed, nullptr)));
+ }
+#if 0
+ else if ( m_folder->GetIMAPFolderInfoMail() )
+ {
+ AutoTArray<nsMsgKey, 1> messageKeys;
+ messageKeys.AppendElement(key);
+
+ (m_folder->GetIMAPFolderInfoMail())->DeleteSpecifiedMessages(pane, messageKeys, nsMsgKey_None);
+ m_db->DeleteMessage(key, nullptr, FALSE);
+ return TRUE;
+ }
+#endif
+ else {
+ return FALSE;
+ }
+}
+
+/* Return TRUE on success, FALSE on failure */
+extern "C" bool MSG_DeleteMapiMessage(nsIMsgFolder* folder, nsMsgKey key) {
+ bool success = FALSE;
+ MsgMapiListContext* context = new MsgMapiListContext();
+ if (context) {
+ if (NS_SUCCEEDED(context->OpenDatabase(folder))) {
+ success = context->DeleteMessage(key);
+ }
+
+ delete context;
+ }
+
+ return success;
+}
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiImp.h b/comm/mailnews/mapi/mapihook/src/msgMapiImp.h
new file mode 100644
index 0000000000..b83f0566a0
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiImp.h
@@ -0,0 +1,79 @@
+/* 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/. */
+
+#ifndef MSG_MAPI_IMP_H
+#define MSG_MAPI_IMP_H
+
+#include "msgMapi.h"
+#include "nspr.h"
+#include "nscore.h"
+#include "nsISupportsImpl.h" // ThreadSafeAutoRefCnt
+
+class nsIMsgFolder;
+class MsgMapiListContext;
+
+const CLSID CLSID_CMapiImp = {0x29f458be,
+ 0x8866,
+ 0x11d5,
+ {0xa3, 0xdd, 0x0, 0xb0, 0xd0, 0xf3, 0xba, 0xa7}};
+
+// this class implements the MS COM interface nsIMapi that provides the methods
+// called by mapi32.dll to perform the mail operations as specified by MAPI.
+// These class methods in turn use the Mozilla Mail XPCOM interfaces to do so.
+class CMapiImp : public nsIMapi {
+ public:
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(const IID& aIid, void** aPpv);
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP_(ULONG) Release();
+
+ // Interface INsMapi
+
+ STDMETHODIMP Login(unsigned long aUIArg, LPSTR aLogin, LPSTR aPassWord,
+ unsigned long aFlags, unsigned long* aSessionId);
+
+ STDMETHODIMP SendMail(unsigned long aSession, lpnsMapiMessage aMessage,
+ unsigned long aFlags, unsigned long aReserved);
+
+ STDMETHODIMP SendDocuments(unsigned long aSession, LPSTR aDelimChar,
+ LPSTR aFilePaths, LPSTR aFileNames, ULONG aFlags);
+
+ STDMETHODIMP FindNext(unsigned long aSession, unsigned long ulUIParam,
+ LPSTR lpszMessageType, LPSTR lpszSeedMessageID,
+ unsigned long flFlags, unsigned long ulReserved,
+ unsigned char lpszMessageID[64]);
+
+ STDMETHODIMP ReadMail(unsigned long lhSession, unsigned long ulUIParam,
+ LPSTR lpszMessageID, unsigned long flFlags,
+ unsigned long ulReserved, lpnsMapiMessage* lppMessage);
+ STDMETHODIMP DeleteMail(unsigned long lhSession, unsigned long ulUIParam,
+ LPSTR lpszMessageID, unsigned long flFlags,
+ unsigned long ulReserved);
+ STDMETHODIMP SaveMail(unsigned long lhSession, unsigned long ulUIParam,
+ lpnsMapiMessage lppMessage, unsigned long flFlags,
+ unsigned long ulReserved, LPSTR lpszMessageID);
+
+ STDMETHODIMP Initialize();
+ STDMETHODIMP IsValid();
+ STDMETHODIMP IsValidSession(unsigned long aSession);
+
+ STDMETHODIMP SendMailW(unsigned long aSession, lpnsMapiMessageW aMessage,
+ unsigned long aFlags, unsigned long aReserved);
+
+ STDMETHODIMP Logoff(unsigned long aSession);
+ STDMETHODIMP CleanUp();
+
+ CMapiImp();
+ virtual ~CMapiImp();
+
+ LONG InitContext(unsigned long session, MsgMapiListContext** listContext);
+ nsresult GetDefaultInbox(nsIMsgFolder** inboxFolder);
+
+ private:
+ PRLock* m_Lock;
+ mozilla::ThreadSafeAutoRefCnt m_cRef;
+};
+
+#endif // MSG_MAPI_IMP_H
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiMain.cpp b/comm/mailnews/mapi/mapihook/src/msgMapiMain.cpp
new file mode 100644
index 0000000000..0e8d4cacac
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiMain.cpp
@@ -0,0 +1,248 @@
+/* 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/. */
+
+#include <mapidefs.h>
+#include <mapi.h>
+
+#include "msgCore.h"
+#include "nsComposeStrings.h"
+#include "msgMapiMain.h"
+#include "nsCOMPtr.h"
+
+nsMAPIConfiguration* nsMAPIConfiguration::m_pSelfRef = nullptr;
+uint32_t nsMAPIConfiguration::session_generator = 0;
+uint32_t nsMAPIConfiguration::sessionCount = 0;
+
+nsMAPIConfiguration* nsMAPIConfiguration::GetMAPIConfiguration() {
+ if (m_pSelfRef == nullptr) m_pSelfRef = new nsMAPIConfiguration();
+
+ return m_pSelfRef;
+}
+
+nsMAPIConfiguration::nsMAPIConfiguration() : m_nMaxSessions(MAX_SESSIONS) {
+ m_Lock = PR_NewLock();
+}
+
+nsMAPIConfiguration::~nsMAPIConfiguration() {
+ if (m_Lock) PR_DestroyLock(m_Lock);
+}
+
+void nsMAPIConfiguration::OpenConfiguration() {
+ // No. of max. sessions is set to MAX_SESSIONS. In future
+ // if it is decided to have configuration (registry)
+ // parameter, this function can be used to set the
+ // max sessions;
+
+ return;
+}
+
+int16_t nsMAPIConfiguration::RegisterSession(
+ uint32_t aHwnd, const nsCString& aUserName, const nsCString& aPassword,
+ bool aForceDownLoad, bool aNewSession, uint32_t* aSession,
+ const char* aIdKey) {
+ int16_t nResult = 0;
+ uint32_t n_SessionId = 0;
+
+ PR_Lock(m_Lock);
+
+ // Check whether max sessions is exceeded
+
+ if (sessionCount >= m_nMaxSessions) {
+ PR_Unlock(m_Lock);
+ return -1;
+ }
+
+ if (!aUserName.IsEmpty()) n_SessionId = m_ProfileMap.Get(aUserName);
+
+ // try to share a session; if not create a session
+ if (n_SessionId > 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(n_SessionId, &pTemp);
+ if (pTemp != nullptr) {
+ pTemp->IncrementSession();
+ *aSession = n_SessionId;
+ nResult = 1;
+ }
+ } else if (aNewSession ||
+ n_SessionId == 0) // checking for n_SessionId is a concession
+ {
+ // create a new session; if new session is specified OR there is no session
+ session_generator++;
+
+ // I don't think there will be (2 power 32) sessions alive
+ // in a cycle. This is an assumption.
+ if (session_generator == 0) session_generator++;
+ m_SessionMap.InsertOrUpdate(
+ session_generator,
+ mozilla::MakeUnique<nsMAPISession>(aHwnd, aUserName, aPassword,
+ aForceDownLoad, aIdKey));
+ if (!aUserName.IsEmpty())
+ m_ProfileMap.InsertOrUpdate(aUserName, session_generator);
+ *aSession = session_generator;
+ sessionCount++;
+ nResult = 1;
+ }
+
+ PR_Unlock(m_Lock);
+ return nResult;
+}
+
+bool nsMAPIConfiguration::UnRegisterSession(uint32_t aSessionID) {
+ bool bResult = false;
+
+ PR_Lock(m_Lock);
+
+ if (aSessionID != 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(aSessionID, &pTemp);
+
+ if (pTemp != nullptr) {
+ if (pTemp->DecrementSession() == 0) {
+ if (pTemp->m_pProfileName.get() != nullptr)
+ m_ProfileMap.Remove(pTemp->m_pProfileName);
+ m_SessionMap.Remove(aSessionID);
+ sessionCount--;
+ bResult = true;
+ }
+ }
+ }
+
+ PR_Unlock(m_Lock);
+ return bResult;
+}
+
+bool nsMAPIConfiguration::IsSessionValid(uint32_t aSessionID) {
+ if (aSessionID == 0) return false;
+ bool retValue = false;
+ PR_Lock(m_Lock);
+ retValue = m_SessionMap.Get(aSessionID, NULL);
+ PR_Unlock(m_Lock);
+ return retValue;
+}
+
+char16_t* nsMAPIConfiguration::GetPassword(uint32_t aSessionID) {
+ char16_t* pResult = nullptr;
+
+ PR_Lock(m_Lock);
+
+ if (aSessionID != 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(aSessionID, &pTemp);
+
+ if (pTemp) pResult = pTemp->GetPassword();
+ }
+ PR_Unlock(m_Lock);
+ return pResult;
+}
+
+void* nsMAPIConfiguration::GetMapiListContext(uint32_t aSessionID) {
+ void* pResult = nullptr;
+
+ PR_Lock(m_Lock);
+
+ if (aSessionID != 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(aSessionID, &pTemp);
+ if (pTemp) pResult = pTemp->GetMapiListContext();
+ }
+
+ PR_Unlock(m_Lock);
+ return pResult;
+}
+
+void nsMAPIConfiguration::SetMapiListContext(uint32_t aSessionID,
+ void* mapiListContext) {
+ PR_Lock(m_Lock);
+
+ if (aSessionID != 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(aSessionID, &pTemp);
+ if (pTemp) pTemp->SetMapiListContext(mapiListContext);
+ }
+
+ PR_Unlock(m_Lock);
+}
+
+void nsMAPIConfiguration::GetIdKey(uint32_t aSessionID, nsCString& aKey) {
+ PR_Lock(m_Lock);
+ if (aSessionID != 0) {
+ nsMAPISession* pTemp = nullptr;
+ m_SessionMap.Get(aSessionID, &pTemp);
+ if (pTemp) pTemp->GetIdKey(aKey);
+ }
+ PR_Unlock(m_Lock);
+ return;
+}
+
+// util func
+HRESULT nsMAPIConfiguration::GetMAPIErrorFromNSError(nsresult res) {
+ HRESULT hr = SUCCESS_SUCCESS;
+
+ if (NS_SUCCEEDED(res)) return hr;
+
+ // if failure return the related MAPI failure code
+ switch (res) {
+ case NS_MSG_NO_RECIPIENTS:
+ hr = MAPI_E_BAD_RECIPTYPE;
+ break;
+ case NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS:
+ case NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY:
+ // Something went wrong with the sender. There's no error we can map to
+ // so we use a general error, see:
+ // https://msdn.microsoft.com/en-us/library/hh802867(v=vs.85).aspx
+ hr = MAPI_E_FAILURE;
+ break;
+ case NS_ERROR_SMTP_AUTH_FAILURE:
+ case NS_ERROR_SMTP_AUTH_GSSAPI:
+ case NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED:
+ case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL:
+ case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL:
+ case NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT:
+ hr = MAPI_E_LOGIN_FAILURE;
+ break;
+ case NS_MSG_UNABLE_TO_OPEN_FILE:
+ case NS_MSG_UNABLE_TO_OPEN_TMP_FILE:
+ case NS_MSG_COULDNT_OPEN_FCC_FOLDER:
+ case NS_ERROR_FILE_INVALID_PATH:
+ hr = MAPI_E_ATTACHMENT_OPEN_FAILURE;
+ break;
+ case NS_ERROR_FILE_NOT_FOUND:
+ hr = MAPI_E_ATTACHMENT_NOT_FOUND;
+ break;
+ case NS_MSG_ERROR_WRITING_FILE:
+ case NS_MSG_UNABLE_TO_SAVE_TEMPLATE:
+ case NS_MSG_UNABLE_TO_SAVE_DRAFT:
+ hr = MAPI_E_ATTACHMENT_WRITE_FAILURE;
+ break;
+ default:
+ hr = MAPI_E_FAILURE;
+ break;
+ }
+
+ return hr;
+}
+
+nsMAPISession::nsMAPISession(uint32_t aHwnd, const nsCString& aUserName,
+ const nsCString& aPassword, bool aForceDownLoad,
+ const char* aKey)
+ : m_nShared(1), m_pIdKey(aKey) {
+ m_listContext = NULL;
+ m_pProfileName = aUserName;
+ m_pPassword = aPassword;
+}
+
+nsMAPISession::~nsMAPISession() {}
+
+uint32_t nsMAPISession::IncrementSession() { return ++m_nShared; }
+
+uint32_t nsMAPISession::DecrementSession() { return --m_nShared; }
+
+uint32_t nsMAPISession::GetSessionCount() { return m_nShared; }
+
+char16_t* nsMAPISession::GetPassword() { return (char16_t*)m_pPassword.get(); }
+
+void nsMAPISession::GetIdKey(nsCString& aKey) {
+ aKey = m_pIdKey;
+ return;
+}
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiMain.h b/comm/mailnews/mapi/mapihook/src/msgMapiMain.h
new file mode 100644
index 0000000000..fcca6ca541
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiMain.h
@@ -0,0 +1,80 @@
+/* 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/. */
+
+#ifndef MSG_MAPI_MAIN_H_
+#define MSG_MAPI_MAIN_H_
+
+#define MAX_NAME_LEN 256
+#define MAX_PW_LEN 256
+#define MAX_SESSIONS 50
+#define MAPI_SENDCOMPLETE_EVENT "SendCompletionEvent"
+
+#define MAPI_PROPERTIES_CHROME "chrome://messenger-mapi/locale/mapi.properties"
+#define PREF_MAPI_WARN_PRIOR_TO_BLIND_SEND "mapi.blind-send.warn"
+#define PREF_MAPI_BLIND_SEND_ENABLED "mapi.blind-send.enabled"
+
+#include "nspr.h"
+#include "nsTHashMap.h"
+#include "nsClassHashtable.h"
+#include "nsString.h"
+
+class nsMAPISession;
+
+class nsMAPIConfiguration {
+ private:
+ static uint32_t session_generator;
+ static uint32_t sessionCount;
+ static nsMAPIConfiguration* m_pSelfRef;
+ PRLock* m_Lock;
+ uint32_t m_nMaxSessions;
+
+ nsTHashMap<nsCStringHashKey, uint32_t> m_ProfileMap;
+ nsClassHashtable<nsUint32HashKey, nsMAPISession> m_SessionMap;
+ nsMAPIConfiguration();
+ ~nsMAPIConfiguration();
+
+ public:
+ static nsMAPIConfiguration* GetMAPIConfiguration();
+ void OpenConfiguration();
+ int16_t RegisterSession(uint32_t aHwnd, const nsCString& aUserName,
+ const nsCString& aPassword, bool aForceDownLoad,
+ bool aNewSession, uint32_t* aSession,
+ const char* aIdKey);
+ bool IsSessionValid(uint32_t aSessionID);
+ bool UnRegisterSession(uint32_t aSessionID);
+ char16_t* GetPassword(uint32_t aSessionID);
+ void GetIdKey(uint32_t aSessionID, nsCString& aKey);
+ void* GetMapiListContext(uint32_t aSessionID);
+ void SetMapiListContext(uint32_t aSessionID, void* mapiListContext);
+
+ // a util func
+ static HRESULT GetMAPIErrorFromNSError(nsresult res);
+};
+
+class nsMAPISession {
+ friend class nsMAPIConfiguration;
+
+ private:
+ uint32_t m_nShared;
+ nsCString m_pIdKey;
+ nsCString m_pProfileName;
+ nsCString m_pPassword;
+ void* m_listContext; // used by findNext
+
+ public:
+ nsMAPISession(uint32_t aHwnd, const nsCString& aUserName,
+ const nsCString& aPassword, bool aForceDownLoad,
+ const char* aKey);
+ uint32_t IncrementSession();
+ uint32_t DecrementSession();
+ uint32_t GetSessionCount();
+ char16_t* GetPassword();
+ void GetIdKey(nsCString& aKey);
+ ~nsMAPISession();
+ // For enumerating Messages...
+ void SetMapiListContext(void* listContext) { m_listContext = listContext; }
+ void* GetMapiListContext() { return m_listContext; }
+};
+
+#endif // MSG_MAPI_MAIN_H_
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiSupport.cpp b/comm/mailnews/mapi/mapihook/src/msgMapiSupport.cpp
new file mode 100644
index 0000000000..9fd1655526
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiSupport.cpp
@@ -0,0 +1,101 @@
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "objbase.h"
+#include "nsISupports.h"
+
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "Registry.h"
+#include "msgMapiSupport.h"
+
+#include "msgMapiImp.h"
+
+/** Implementation of the nsIMapiSupport interface.
+ * Use standard implementation of nsISupports stuff.
+ */
+
+NS_IMPL_ISUPPORTS(nsMapiSupport, nsIMapiSupport, nsIObserver)
+
+NS_IMETHODIMP
+nsMapiSupport::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsresult rv = NS_OK;
+
+ if (!strcmp(aTopic, "profile-after-change")) return InitializeMAPISupport();
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID))
+ return ShutdownMAPISupport();
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ rv = observerService->AddObserver(this, "profile-after-change", false);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ if (NS_FAILED(rv)) return rv;
+
+ return rv;
+}
+
+nsMapiSupport::nsMapiSupport() : m_dwRegister(0), m_nsMapiFactory(nullptr) {}
+
+nsMapiSupport::~nsMapiSupport() {}
+
+NS_IMETHODIMP
+nsMapiSupport::InitializeMAPISupport() {
+ ::OleInitialize(nullptr);
+
+ if (m_nsMapiFactory ==
+ nullptr) // No Registering if already done. Sanity Check!!
+ {
+ m_nsMapiFactory = new CMapiFactory();
+
+ if (m_nsMapiFactory != nullptr) {
+ HRESULT hr = ::CoRegisterClassObject(CLSID_CMapiImp, m_nsMapiFactory,
+ CLSCTX_LOCAL_SERVER,
+ REGCLS_MULTIPLEUSE, &m_dwRegister);
+
+ if (FAILED(hr)) {
+ m_nsMapiFactory->Release();
+ m_nsMapiFactory = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMapiSupport::ShutdownMAPISupport() {
+ if (m_dwRegister != 0) ::CoRevokeClassObject(m_dwRegister);
+
+ if (m_nsMapiFactory != nullptr) {
+ m_nsMapiFactory->Release();
+ m_nsMapiFactory = nullptr;
+ }
+
+ ::OleUninitialize();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMapiSupport::RegisterServer() {
+ // TODO: Figure out what kind of error propagation to pass back
+ ::RegisterServer(CLSID_CMapiImp, L"Mozilla MAPI", L"MozillaMapi",
+ L"MozillaMapi.1");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMapiSupport::UnRegisterServer() {
+ // TODO: Figure out what kind of error propagation to pass back
+ ::UnregisterServer(CLSID_CMapiImp, L"MozillaMapi", L"MozillaMapi.1");
+ return NS_OK;
+}
diff --git a/comm/mailnews/mapi/mapihook/src/msgMapiSupport.h b/comm/mailnews/mapi/mapihook/src/msgMapiSupport.h
new file mode 100644
index 0000000000..af17ea637c
--- /dev/null
+++ b/comm/mailnews/mapi/mapihook/src/msgMapiSupport.h
@@ -0,0 +1,35 @@
+/* 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/. */
+
+#ifndef MSG_MAPI_SUPPORT_H_
+#define MSG_MAPI_SUPPORT_H_
+
+#include "nsIObserver.h"
+#include "nsIMapiSupport.h"
+#include "msgMapiFactory.h"
+
+#define NS_IMAPISUPPORT_CID \
+ { \
+ 0x8967fed2, 0xc8bb, 0x11d5, { \
+ 0xa3, 0xe9, 0x00, 0xb0, 0xd0, 0xf3, 0xba, 0xa7 \
+ } \
+ }
+
+class nsMapiSupport : public nsIMapiSupport, public nsIObserver {
+ public:
+ nsMapiSupport();
+
+ // Declare all interface methods we must implement.
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMAPISUPPORT
+
+ private:
+ virtual ~nsMapiSupport();
+
+ DWORD m_dwRegister;
+ CMapiFactory* m_nsMapiFactory;
+};
+
+#endif // MSG_MAPI_SUPPORT_H_
diff --git a/comm/mailnews/mapi/test/moz.build b/comm/mailnews/mapi/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/mapi/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/mapi/test/unit/head_mapi.js b/comm/mailnews/mapi/test/unit/head_mapi.js
new file mode 100644
index 0000000000..d80b7da173
--- /dev/null
+++ b/comm/mailnews/mapi/test/unit/head_mapi.js
@@ -0,0 +1,243 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+// Ensure the profile directory is set up.
+do_get_profile();
+
+// Import fakeserver
+var { nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Smtpd.jsm"
+);
+
+var SMTP_PORT = 1024 + 120;
+var POP3_PORT = 1024 + 121;
+
+// Setup the daemon and server
+function setupServerDaemon(handler) {
+ if (!handler) {
+ handler = function (d) {
+ return new SMTP_RFC2821_handler(d);
+ };
+ }
+ let daemon = new SmtpDaemon();
+ let server = new nsMailServer(handler, daemon);
+ return [daemon, server];
+}
+
+function getBasicSmtpServer() {
+ // We need to have a default account for MAPI.
+ localAccountUtils.loadLocalMailAccount();
+ let incoming = localAccountUtils.create_incoming_server(
+ "pop3",
+ POP3_PORT,
+ "user",
+ "password"
+ );
+ let server = localAccountUtils.create_outgoing_server(
+ SMTP_PORT,
+ "user",
+ "password"
+ );
+ // We also need to have a working identity, including an email address.
+ let account = MailServices.accounts.FindAccountForServer(incoming);
+ localAccountUtils.associate_servers(account, server, true);
+ let identity = account.defaultIdentity;
+ identity.email = "tinderbox@tinderbox.invalid";
+ MailServices.accounts.defaultAccount = account;
+
+ return server;
+}
+
+/**
+ * Returns a structure allowing access to all of the Simple MAPI functions.
+ * The functions do not have the MAPI prefix on the variables. Also added are
+ * the three structures needed for MAPI.
+ */
+function loadMAPILibrary() {
+ // This is a hack to load the MAPI support in the current environment, as the
+ // profile-after-change event is never sent out.
+ var gMapiSupport = Cc["@mozilla.org/mapisupport;1"].getService(
+ Ci.nsIObserver
+ );
+ gMapiSupport.observe(null, "profile-after-change", null);
+ // Set some preferences to make MAPI (particularly blind MAPI, aka work
+ // without a dialog box) work properly.
+ Services.prefs.setBoolPref("mapi.blind-send.enabled", true);
+ Services.prefs.setBoolPref("mapi.blind-send.warn", false);
+
+ // The macros that are used in the definitions
+ let WINAPI = ctypes.winapi_abi;
+ let ULONG = ctypes.unsigned_long;
+ let LHANDLE = ULONG.ptr;
+ let LPSTR = ctypes.char.ptr;
+ let LPVOID = ctypes.voidptr_t;
+ let FLAGS = ctypes.unsigned_long;
+
+ // Define all of the MAPI structs we need to use.
+ let functionData = {};
+ functionData.MapiRecipDesc = new ctypes.StructType("gMapi.MapiRecipDesc", [
+ { ulReserved: ULONG },
+ { ulRecipClass: ULONG },
+ { lpszName: LPSTR },
+ { lpszAddress: LPSTR },
+ { ulEIDSize: ULONG },
+ { lpEntryID: LPVOID },
+ ]);
+ let lpMapiRecipDesc = functionData.MapiRecipDesc.ptr;
+
+ functionData.MapiFileDesc = new ctypes.StructType("gMapi.MapiFileDesc", [
+ { ulReserved: ULONG },
+ { flFlags: ULONG },
+ { nPosition: ULONG },
+ { lpszPathName: LPSTR },
+ { lpszFileName: LPSTR },
+ { lpFileType: LPVOID },
+ ]);
+ let lpMapiFileDesc = functionData.MapiFileDesc.ptr;
+
+ functionData.MapiMessage = new ctypes.StructType("gMapi.MapiMessage", [
+ { ulReserved: ULONG },
+ { lpszSubject: LPSTR },
+ { lpszNoteText: LPSTR },
+ { lpszMessageType: LPSTR },
+ { lpszDateReceived: LPSTR },
+ { lpszConversationID: LPSTR },
+ { flFlags: FLAGS },
+ { lpOriginator: lpMapiRecipDesc },
+ { nRecipCount: ULONG },
+ { lpRecips: lpMapiRecipDesc },
+ { nFileCount: ULONG },
+ { lpFiles: lpMapiFileDesc },
+ ]);
+ let lpMapiMessage = functionData.MapiMessage.ptr;
+
+ // Load the MAPI library. We're using our definition instead of the global
+ // MAPI definition.
+ let mapi = ctypes.open("mozMapi32.dll");
+
+ // Load the MAPI functions,
+ // see https://developer.mozilla.org/en-US/docs/Mozilla/js-ctypes/Using_js-ctypes/Declaring_types
+ // for details. The first three parameters of the declaration are name, API flag and output value.
+ // This is followed by input parameters.
+
+ // MAPIAddress is not supported.
+
+ functionData.DeleteMail = mapi.declare(
+ "MAPIDeleteMail",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszMessageID
+ FLAGS, // flFlags
+ ULONG
+ ); // ulReserved
+
+ // MAPIDetails is not supported.
+
+ functionData.FindNext = mapi.declare(
+ "MAPIFindNext",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszMessageType
+ LPSTR, // lpszSeedMessageID
+ FLAGS, // flFlags
+ ULONG, // ulReserved
+ LPSTR
+ ); // lpszMessageID
+
+ functionData.FreeBuffer = mapi.declare(
+ "MAPIFreeBuffer",
+ WINAPI,
+ ULONG,
+ LPVOID
+ ); // pv
+
+ functionData.Logoff = mapi.declare(
+ "MAPILogoff",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ FLAGS, // flFlags
+ ULONG
+ ); // ulReserved
+
+ functionData.Logon = mapi.declare(
+ "MAPILogon",
+ WINAPI,
+ ULONG,
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszProfileName
+ LPSTR, // lpszPassword
+ FLAGS, // flFlags
+ ULONG, // ulReserved
+ LHANDLE.ptr
+ ); // lplhSession
+
+ functionData.ReadMail = mapi.declare(
+ "MAPIReadMail",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszMessageID
+ FLAGS, // flFlags
+ ULONG, // ulReserved
+ lpMapiMessage.ptr
+ ); // *lppMessage
+
+ functionData.ResolveName = mapi.declare(
+ "MAPIResolveName",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszName
+ FLAGS, // flFlags
+ ULONG, // ulReserved
+ lpMapiRecipDesc.ptr
+ ); // *lppRecip
+
+ // MAPISaveMail is not supported.
+
+ functionData.SendDocuments = mapi.declare(
+ "MAPISendDocuments",
+ WINAPI,
+ ULONG,
+ ULONG.ptr, // ulUIParam
+ LPSTR, // lpszDelimChar
+ LPSTR, // lpszFilePaths
+ LPSTR, // lpszFileNames
+ ULONG
+ ); // ulReserved
+
+ functionData.SendMail = mapi.declare(
+ "MAPISendMail",
+ WINAPI,
+ ULONG,
+ LHANDLE, // lhSession
+ ULONG.ptr, // ulUIParam
+ lpMapiMessage, // lpMessage
+ FLAGS, // flFlags
+ ULONG
+ ); // ulReserved
+
+ return functionData;
+}
diff --git a/comm/mailnews/mapi/test/unit/tail_mapi.js b/comm/mailnews/mapi/test/unit/tail_mapi.js
new file mode 100644
index 0000000000..f64ae61317
--- /dev/null
+++ b/comm/mailnews/mapi/test/unit/tail_mapi.js
@@ -0,0 +1 @@
+load("../../../resources/mailShutdown.js");
diff --git a/comm/mailnews/mapi/test/unit/test_mapisendmail.js b/comm/mailnews/mapi/test/unit/test_mapisendmail.js
new file mode 100644
index 0000000000..5dc480af9e
--- /dev/null
+++ b/comm/mailnews/mapi/test/unit/test_mapisendmail.js
@@ -0,0 +1,97 @@
+/* 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 { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+// Set up an SMTP server and the MAPI daemon.
+getBasicSmtpServer();
+let [daemon, server] = setupServerDaemon();
+server.start(SMTP_PORT);
+let mapi = loadMAPILibrary();
+
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Construct a MapiMessage, then send through MAPI.
+ *
+ * @param {boolean} offline - Switch to offline mode if true.
+ */
+function testMapiSendMail(offline) {
+ Services.io.offline = offline;
+
+ // Build a message using the MAPI interface.
+ let message = new mapi.MapiMessage();
+ message.lpszSubject = ctypes.char.array()(`Hello, MAPI offline=${offline}!`);
+ message.lpszNoteText = ctypes.char.array()("I successfully sent a message!");
+ message.lpszMessageType = ctypes.char.array()("");
+
+ let file = do_get_file("../../../compose/test/unit/data/message1.eml");
+ let attachment = new mapi.MapiFileDesc();
+ attachment.lpszFileName = ctypes.char.array()(file.leafName);
+ attachment.lpszPathName = ctypes.char.array()(file.path);
+ message.nFileCount = 1;
+ message.lpFiles = attachment.address();
+
+ let recipient = new mapi.MapiRecipDesc();
+ recipient.ulRecipClass = 1; /* MAPI_TO */
+ recipient.lpszName = ctypes.char.array()("John Doe");
+ recipient.lpszAddress = ctypes.char.array()("SMTP:john.doe@example.com");
+ message.nRecipCount = 1;
+ message.lpRecips = recipient.address();
+
+ // Use MAPISendMail to send this message.
+ mapi.SendMail(
+ null /* No session */,
+ null /* No HWND */,
+ message.address(),
+ 0x2 /* MAPI_NEW_SESSION */,
+ 0
+ );
+}
+
+/**
+ * Test that when we're online, the message can be sent correctly to the SMTP
+ * server.
+ */
+add_task(function mapiSendMailOnline() {
+ server.resetTest();
+ testMapiSendMail(false);
+
+ // Check that the post has the correct information.
+ let [headers, body] = MimeParser.extractHeadersAndBody(daemon.post);
+ Assert.equal(headers.get("from")[0].email, "tinderbox@tinderbox.invalid");
+ Assert.equal(headers.get("to")[0].email, "john.doe@example.com");
+ Assert.equal(headers.get("subject"), "Hello, MAPI offline=false!");
+ Assert.ok(body.includes("I successfully sent a message!"));
+ Assert.ok(body.includes("this email is in dos format"));
+});
+
+/**
+ * Test that when we're offline, the message can be saved correctly to the Outbox.
+ */
+add_task(function mapiSendMailOffline() {
+ server.resetTest();
+ testMapiSendMail(true);
+
+ let outbox = localAccountUtils.rootFolder.getChildNamed("Outbox");
+ let msgData = mailTestUtils.loadMessageToString(
+ outbox,
+ mailTestUtils.firstMsgHdr(outbox)
+ );
+ // Check that the post has the correct information.
+ let [headers, body] = MimeParser.extractHeadersAndBody(msgData);
+ Assert.equal(headers.get("from")[0].email, "tinderbox@tinderbox.invalid");
+ Assert.equal(headers.get("to")[0].email, "john.doe@example.com");
+ Assert.equal(headers.get("subject"), "Hello, MAPI offline=true!");
+ Assert.ok(body.includes("I successfully sent a message!"));
+ Assert.ok(body.includes("this email is in dos format"));
+});
diff --git a/comm/mailnews/mapi/test/unit/xpcshell.ini b/comm/mailnews/mapi/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..8d4fe504ab
--- /dev/null
+++ b/comm/mailnews/mapi/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = head_mapi.js
+tail = tail_mapi.js
+run-sequentially = Need to use well-known port for SMTP.
+
+[test_mapisendmail.js]
+skip-if = true # Not yet enabled, see bug 1526807.
diff --git a/comm/mailnews/mime/cthandlers/glue/mimexpcom.cpp b/comm/mailnews/mime/cthandlers/glue/mimexpcom.cpp
new file mode 100644
index 0000000000..626dc043a4
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/glue/mimexpcom.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIMimeObjectClassAccess.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+
+// {403B0540-B7C3-11d2-B35E-525400E2D63A}
+#define NS_MIME_OBJECT_CLASS_ACCESS_CID \
+ { \
+ 0x403b0540, 0xb7c3, 0x11d2, { \
+ 0xb3, 0x5e, 0x52, 0x54, 0x0, 0xe2, 0xd6, 0x3a \
+ } \
+ }
+static NS_DEFINE_CID(kMimeObjectClassAccessCID,
+ NS_MIME_OBJECT_CLASS_ACCESS_CID);
+
+/*
+ * These calls are necessary to expose the object class hierarchy
+ * to externally developed content type handlers.
+ */
+extern "C" void* COM_GetmimeInlineTextClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) objAccess->GetmimeInlineTextClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" void* COM_GetmimeLeafClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) objAccess->GetmimeLeafClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" void* COM_GetmimeObjectClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) objAccess->GetmimeObjectClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" void* COM_GetmimeContainerClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) objAccess->GetmimeContainerClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" void* COM_GetmimeMultipartClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) objAccess->GetmimeMultipartClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" void* COM_GetmimeMultipartSignedClass(void) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess)
+ objAccess->GetmimeMultipartSignedClass(&ptr);
+
+ return ptr;
+}
+
+extern "C" int COM_MimeObject_write(void* mimeObject, char* data,
+ int32_t length, bool user_visible_p) {
+ int32_t rc = -1;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess) {
+ if (NS_SUCCEEDED(objAccess->MimeObjectWrite(mimeObject, data, length,
+ user_visible_p)))
+ rc = length;
+ else
+ rc = -1;
+ }
+
+ return rc;
+}
+
+extern "C" void* COM_MimeCreate(char* content_type, void* hdrs, void* opts) {
+ void* ptr = NULL;
+
+ nsresult res;
+ nsCOMPtr<nsIMimeObjectClassAccess> objAccess =
+ do_CreateInstance(kMimeObjectClassAccessCID, &res);
+ if (NS_SUCCEEDED(res) && objAccess)
+ objAccess->MimeCreate(content_type, hdrs, opts, &ptr);
+
+ return ptr;
+}
diff --git a/comm/mailnews/mime/cthandlers/glue/mimexpcom.h b/comm/mailnews/mime/cthandlers/glue/mimexpcom.h
new file mode 100644
index 0000000000..343c7d0dc7
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/glue/mimexpcom.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This is the definitions for the Content Type Handler plugins to
+ * access internals of libmime via XP-COM calls
+ */
+#ifndef _MIMEXPCOM_H_
+#define _MIMEXPCOM_H_
+
+/*
+ This header exposes functions that are necessary to access the
+ object hierarchy for the mime chart. The class hierarchy is:
+
+ MimeObject (abstract)
+ |
+ |--- MimeContainer (abstract)
+ | |
+ | |--- MimeMultipart (abstract)
+ | | |
+ | | |--- MimeMultipartMixed
+ | | |
+ | | |--- MimeMultipartDigest
+ | | |
+ | | |--- MimeMultipartParallel
+ | | |
+ | | |--- MimeMultipartAlternative
+ | | |
+ | | |--- MimeMultipartRelated
+ | | |
+ | | |--- MimeMultipartAppleDouble
+ | | |
+ | | |--- MimeSunAttachment
+ | | |
+ | | |--- MimeMultipartSigned (abstract)
+ | | |
+ | | |--- MimeMultipartSigned
+ | |
+ | |--- MimeXlateed (abstract)
+ | | |
+ | | |--- MimeXlateed
+ | |
+ | |--- MimeMessage
+ | |
+ | |--- MimeUntypedText
+ |
+ |--- MimeLeaf (abstract)
+ | |
+ | |--- MimeInlineText (abstract)
+ | | |
+ | | |--- MimeInlineTextPlain
+ | | |
+ | | |--- MimeInlineTextHTML
+ | | |
+ | | |--- MimeInlineTextRichtext
+ | | | |
+ | | | |--- MimeInlineTextEnriched
+ | | |
+ | | |--- MimeInlineTextVCard
+ | |
+ | |--- MimeInlineImage
+ | |
+ | |--- MimeExternalObject
+ |
+ |--- MimeExternalBody
+ */
+
+/*
+ * These functions are exposed by libmime to be used by content type
+ * handler plugins for processing stream data.
+ */
+/*
+ * This is the write call for outputting processed stream data.
+ */
+extern "C" int COM_MimeObject_write(void* mimeObject, const char* data,
+ int32_t length, bool user_visible_p);
+/*
+ * The following group of calls expose the pointers for the object
+ * system within libmime.
+ */
+extern "C" void* COM_GetmimeInlineTextClass(void);
+extern "C" void* COM_GetmimeLeafClass(void);
+extern "C" void* COM_GetmimeObjectClass(void);
+extern "C" void* COM_GetmimeContainerClass(void);
+extern "C" void* COM_GetmimeMultipartClass(void);
+extern "C" void* COM_GetmimeMultipartSignedClass(void);
+
+extern "C" void* COM_MimeCreate(char* content_type, void* hdrs, void* opts);
+
+#endif /* _MIMEXPCOM_H_ */
diff --git a/comm/mailnews/mime/cthandlers/glue/moz.build b/comm/mailnews/mime/cthandlers/glue/moz.build
new file mode 100644
index 0000000000..4955981f3d
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/glue/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "nsMimeContentTypeHandler.h",
+]
+
+SOURCES += [
+ "mimexpcom.cpp",
+ "nsMimeContentTypeHandler.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+Library("mimecthglue_s")
diff --git a/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.cpp b/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.cpp
new file mode 100644
index 0000000000..ace9c64d8c
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <stdio.h>
+#include "nscore.h"
+#include "plstr.h"
+#include "nsMimeContentTypeHandler.h"
+
+/*
+ * The following macros actually implement addref, release and
+ * query interface for our component.
+ */
+NS_IMPL_ISUPPORTS(nsMimeContentTypeHandler, nsIMimeContentTypeHandler)
+
+/*
+ * nsIMimeEmitter definitions....
+ */
+nsMimeContentTypeHandler::nsMimeContentTypeHandler(
+ const char* aMimeType, MCTHCreateCTHClass callback) {
+ NS_ASSERTION(
+ aMimeType,
+ "nsMimeContentTypeHandler should be initialized with non-null mime type");
+ NS_ASSERTION(
+ callback,
+ "nsMimeContentTypeHandler should be initialized with non-null callback");
+ mimeType = PL_strdup(aMimeType);
+ realCreateContentTypeHandlerClass = callback;
+}
+
+nsMimeContentTypeHandler::~nsMimeContentTypeHandler(void) {
+ if (mimeType) {
+ free(mimeType);
+ mimeType = 0;
+ }
+ realCreateContentTypeHandlerClass = 0;
+}
+
+// Get the content type if necessary
+nsresult nsMimeContentTypeHandler::GetContentType(char** contentType) {
+ *contentType = PL_strdup(mimeType);
+ return NS_OK;
+}
+
+// Set the output stream for processed data.
+nsresult nsMimeContentTypeHandler::CreateContentTypeHandlerClass(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct,
+ MimeObjectClass** objClass) {
+ *objClass = realCreateContentTypeHandlerClass(content_type, initStruct);
+ if (!*objClass)
+ return NS_ERROR_OUT_OF_MEMORY; /* we couldn't allocate the object */
+ else
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.h b/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.h
new file mode 100644
index 0000000000..2974e98f17
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/glue/nsMimeContentTypeHandler.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This interface is implemented by content type handlers that will be
+ * called upon by libmime to process various attachments types. The primary
+ * purpose of these handlers will be to represent the attached data in a
+ * viewable HTML format that is useful for the user
+ *
+ * Note: These will all register by their content type prefixed by the
+ * following: mimecth:text/vcard
+ *
+ * libmime will then use the XPCOM Component Manager to
+ * locate the appropriate Content Type handler
+ */
+#ifndef nsMimeContentTypeHandler_h_
+#define nsMimeContentTypeHandler_h_
+
+#include "mozilla/Attributes.h"
+#include "nsIMimeContentTypeHandler.h"
+
+typedef MimeObjectClass* (*MCTHCreateCTHClass)(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct);
+
+class nsMimeContentTypeHandler : public nsIMimeContentTypeHandler {
+ public:
+ nsMimeContentTypeHandler(const char* aMimeType, MCTHCreateCTHClass callback);
+
+ /* this macro defines QueryInterface, AddRef and Release for this class */
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD GetContentType(char** contentType) override;
+
+ NS_IMETHOD CreateContentTypeHandlerClass(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct,
+ MimeObjectClass** objClass) override;
+
+ private:
+ virtual ~nsMimeContentTypeHandler();
+ char* mimeType;
+ MCTHCreateCTHClass realCreateContentTypeHandlerClass;
+};
+
+#endif /* nsMimeContentTypeHandler_h_ */
diff --git a/comm/mailnews/mime/cthandlers/moz.build b/comm/mailnews/mime/cthandlers/moz.build
new file mode 100644
index 0000000000..7d61b9ebf5
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+# pgpmime depends on glue.
+DIRS += [
+ "glue",
+ "pgpmime",
+]
diff --git a/comm/mailnews/mime/cthandlers/pgpmime/components.conf b/comm/mailnews/mime/cthandlers/pgpmime/components.conf
new file mode 100644
index 0000000000..a1098a43f8
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/pgpmime/components.conf
@@ -0,0 +1,21 @@
+# 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/.
+
+Classes = [
+ {
+ "cid": "{212f415f-f8b5-11d2-ffe0-00af19a7d144}",
+ "contract_ids": [
+ "@mozilla.org/mimecth;1?type=multipart/encrypted",
+ ],
+ "legacy_constructor": "nsPgpMimeMimeContentTypeHandlerConstructor",
+ "headers": ["/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeMimeContentTypeHandler.h"],
+ },
+ {
+ "cid": "{815c4fbe-0e7c-45b6-8324-f7044c7252ac}",
+ "contract_ids": ["@mozilla.org/mime/pgp-mime-decrypt;1"],
+ "type": "nsPgpMimeProxy",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.h"],
+ },
+]
diff --git a/comm/mailnews/mime/cthandlers/pgpmime/moz.build b/comm/mailnews/mime/cthandlers/pgpmime/moz.build
new file mode 100644
index 0000000000..7259395c5a
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/pgpmime/moz.build
@@ -0,0 +1,24 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "nsPgpMimeProxy.h",
+]
+
+SOURCES += [
+ "nsPgpMimeProxy.cpp",
+]
+
+FINAL_LIBRARY = "mail"
+
+Library("pgpmime_s")
+
+LOCAL_INCLUDES += [
+ "../glue",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeMimeContentTypeHandler.h b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeMimeContentTypeHandler.h
new file mode 100644
index 0000000000..bc8a432b08
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeMimeContentTypeHandler.h
@@ -0,0 +1,22 @@
+/* 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/. */
+
+#include "nsMimeContentTypeHandler.h"
+#include "nsPgpMimeProxy.h"
+
+extern "C" MimeObjectClass* MIME_PgpMimeCreateContentTypeHandlerClass(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct);
+
+static nsresult nsPgpMimeMimeContentTypeHandlerConstructor(REFNSIID aIID,
+ void** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = nullptr;
+
+ RefPtr<nsMimeContentTypeHandler> inst(new nsMimeContentTypeHandler(
+ "multipart/encrypted", &MIME_PgpMimeCreateContentTypeHandlerClass));
+
+ NS_ENSURE_TRUE(inst, NS_ERROR_OUT_OF_MEMORY);
+
+ return inst->QueryInterface(aIID, aResult);
+}
diff --git a/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.cpp b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.cpp
new file mode 100644
index 0000000000..4382ed0c1a
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.cpp
@@ -0,0 +1,672 @@
+/* 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/. */
+
+#include "nsPgpMimeProxy.h"
+#include "nspr.h"
+#include "plstr.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/Components.h"
+#include "nsIRequest.h"
+#include "nsIStringBundle.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIURI.h"
+#include "mimexpcom.h"
+#include "nsMsgUtils.h"
+#include "nsNetUtil.h"
+
+#include "mimecth.h"
+#include "mimemoz2.h"
+#include "nspr.h"
+#include "plstr.h"
+#include "nsIPgpMimeProxy.h"
+#include "nsComponentManagerUtils.h"
+
+/**
+ * Overall description
+ * ===================
+ *
+ * There are three components involved here: MIME, a proxy object
+ * (nsPgpMimeProxy) and Enigmail (or any other add-on that registered a
+ * decryption object with
+ * "@mozilla.org/mime/pgp-mime-js-decrypt;1").
+ *
+ * MIME creates and initialises the proxy object in nsPgpMimeProxy::Init(). This
+ * creates a decryption object, for example EnigmailMimeDecrypt. When MIME wants
+ * to decode something, it calls the Write() method of the proxy, which in turn
+ * calls OnDataAvailable() on the decryptor. The decryptor optains the encrypted
+ * data form the proxy via the proxy's Read() method. The decryptor decrypts the
+ * data and passes the result back to the proxy, using the OutputDecryptedData()
+ * method or by passing a stream to the proxy's OnDataAvailable() method, in
+ * which the proxy will read from that stream. The proxy knows how to interface
+ * with MIME and passes the data on using some function pointers it got given
+ * via nsPgpMimeProxy::SetMimeCallback().
+ */
+
+#define MIME_SUPERCLASS mimeEncryptedClass
+MimeDefClass(MimeEncryptedPgp, MimeEncryptedPgpClass, mimeEncryptedPgpClass,
+ &MIME_SUPERCLASS);
+
+#define kCharMax 1024
+
+extern "C" MimeObjectClass* MIME_PgpMimeCreateContentTypeHandlerClass(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct) {
+ MimeObjectClass* objClass = (MimeObjectClass*)&mimeEncryptedPgpClass;
+
+ initStruct->force_inline_display = false;
+
+ return objClass;
+}
+
+static void* MimePgpe_init(MimeObject*,
+ int (*output_fn)(const char*, int32_t, void*),
+ void*);
+static int MimePgpe_write(const char*, int32_t, void*);
+static int MimePgpe_eof(void*, bool);
+static char* MimePgpe_generate(void*);
+static void MimePgpe_free(void*);
+
+/* Returns a string describing the location of the part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ */
+static nsCString determineMimePart(MimeObject* obj);
+
+#define PGPMIME_PROPERTIES_URL "chrome://messenger/locale/pgpmime.properties"
+#define PGPMIME_STR_NOT_SUPPORTED_ID "pgpNotAvailable"
+
+static void PgpMimeGetNeedsAddonString(nsCString& aResult) {
+ aResult.AssignLiteral("???");
+
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ nsresult rv = stringBundleService->CreateBundle(PGPMIME_PROPERTIES_URL,
+ getter_AddRefs(stringBundle));
+ if (NS_FAILED(rv)) return;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return;
+
+ nsString result;
+ rv = stringBundle->GetStringFromName(PGPMIME_STR_NOT_SUPPORTED_ID, result);
+ if (NS_FAILED(rv)) return;
+ aResult = NS_ConvertUTF16toUTF8(result);
+}
+
+static int MimeEncryptedPgpClassInitialize(MimeEncryptedPgpClass* clazz) {
+ mozilla::DebugOnly<MimeObjectClass*> oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "oclass is not initialized");
+
+ MimeEncryptedClass* eclass = (MimeEncryptedClass*)clazz;
+
+ eclass->crypto_init = MimePgpe_init;
+ eclass->crypto_write = MimePgpe_write;
+ eclass->crypto_eof = MimePgpe_eof;
+ eclass->crypto_generate_html = MimePgpe_generate;
+ eclass->crypto_free = MimePgpe_free;
+
+ return 0;
+}
+
+class MimePgpeData : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ int (*output_fn)(const char* buf, int32_t buf_size, void* output_closure);
+ void* output_closure;
+ MimeObject* self;
+
+ nsCOMPtr<nsIPgpMimeProxy> mimeDecrypt;
+
+ MimePgpeData() : output_fn(nullptr), output_closure(nullptr) {}
+
+ private:
+ virtual ~MimePgpeData() {}
+};
+
+NS_IMPL_ISUPPORTS0(MimePgpeData)
+
+static void* MimePgpe_init(MimeObject* obj,
+ int (*output_fn)(const char* buf, int32_t buf_size,
+ void* output_closure),
+ void* output_closure) {
+ if (!(obj && obj->options && output_fn)) return nullptr;
+
+ MimePgpeData* data = new MimePgpeData();
+ NS_ENSURE_TRUE(data, nullptr);
+
+ data->self = obj;
+ data->output_fn = output_fn;
+ data->output_closure = output_closure;
+ data->mimeDecrypt = nullptr;
+
+ // Create proxy object.
+ nsresult rv;
+ data->mimeDecrypt = do_CreateInstance(NS_PGPMIMEPROXY_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return data;
+
+ char* ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+
+ rv = (ct ? data->mimeDecrypt->SetContentType(nsDependentCString(ct))
+ : data->mimeDecrypt->SetContentType(EmptyCString()));
+
+ PR_Free(ct);
+
+ if (NS_FAILED(rv)) return nullptr;
+
+ nsCString mimePart = determineMimePart(obj);
+
+ rv = data->mimeDecrypt->SetMimePart(mimePart);
+ if (NS_FAILED(rv)) return nullptr;
+
+ if (mimePart.EqualsLiteral("1.1") && obj->parent &&
+ obj->parent->content_type &&
+ !strcmp(obj->parent->content_type, "multipart/signed") &&
+ ((MimeContainer*)obj->parent)->nchildren == 1) {
+ // Don't show status for the outer signature, it could be misleading,
+ // the signature could have been created by someone not knowing
+ // the contents of the inner encryption layer.
+ // Another reason is, we usually skip decrypting nested encrypted
+ // parts. However, we make an exception: If the outermost layer
+ // is a signature, and the signature wraps only a single encrypted
+ // message (no sibling MIME parts next to the encrypted part),
+ // then we allow decryption. (bug 1594253)
+ data->mimeDecrypt->SetAllowNestedDecrypt(true);
+ }
+
+ mime_stream_data* msd =
+ (mime_stream_data*)(data->self->options->stream_closure);
+ nsIChannel* channel = msd->channel;
+
+ nsCOMPtr<nsIURI> uri;
+ if (channel) channel->GetURI(getter_AddRefs(uri));
+
+ if (!uri && obj && obj->options && obj->options->url) {
+ // Allow the PGP mime decrypt code to know what message we're
+ // working with, necessary for storing into the message DB
+ // (e.g. decrypted subject). Bug 1666005.
+ NS_NewURI(getter_AddRefs(uri), obj->options->url);
+ }
+
+ // Initialise proxy object with MIME's output function, object and URI.
+ if (NS_FAILED(
+ data->mimeDecrypt->SetMimeCallback(output_fn, output_closure, uri)))
+ return nullptr;
+
+ return data;
+}
+
+static int MimePgpe_write(const char* buf, int32_t buf_size,
+ void* output_closure) {
+ MimePgpeData* data = (MimePgpeData*)output_closure;
+
+ if (!data || !data->output_fn) return -1;
+
+ if (!data->mimeDecrypt) return 0;
+
+ return (NS_SUCCEEDED(data->mimeDecrypt->Write(buf, buf_size)) ? 0 : -1);
+}
+
+static int MimePgpe_eof(void* output_closure, bool abort_p) {
+ MimePgpeData* data = (MimePgpeData*)output_closure;
+
+ if (!data || !data->output_fn) return -1;
+
+ if (NS_FAILED(data->mimeDecrypt->Finish())) return -1;
+
+ data->mimeDecrypt->RemoveMimeCallback();
+ data->mimeDecrypt = nullptr;
+ return 0;
+}
+
+static char* MimePgpe_generate(void* output_closure) {
+ const char htmlMsg[] = "<html><body><b>GEN MSG<b></body></html>";
+ char* msg = (char*)PR_MALLOC(strlen(htmlMsg) + 1);
+ if (msg) PL_strcpy(msg, htmlMsg);
+
+ return msg;
+}
+
+static void MimePgpe_free(void* output_closure) {
+ MimePgpeData* data = (MimePgpeData*)output_closure;
+ if (data->mimeDecrypt) {
+ data->mimeDecrypt->RemoveMimeCallback();
+ data->mimeDecrypt = nullptr;
+ }
+}
+
+/* Returns a string describing the location of the part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ */
+static nsCString determineMimePart(MimeObject* obj) {
+ char mimePartNum[20];
+ MimeObject* kid;
+ MimeContainer* cont;
+ int32_t i;
+
+ nsCString mimePart;
+
+ while (obj->parent) {
+ cont = (MimeContainer*)obj->parent;
+ for (i = 0; i < cont->nchildren; i++) {
+ kid = cont->children[i];
+ if (kid == obj) {
+ sprintf(mimePartNum, ".%d", i + 1);
+ mimePart.Insert(mimePartNum, 0);
+ }
+ }
+ obj = obj->parent;
+ }
+
+ // remove leading "."
+ if (mimePart.Length() > 0) mimePart.Cut(0, 1);
+
+ return mimePart;
+}
+
+////////////////////////////////////////////////////////////////////////////
+NS_IMPL_ISUPPORTS(nsPgpMimeProxy, nsIPgpMimeProxy, nsIRequestObserver,
+ nsIStreamListener, nsIRequest, nsIInputStream)
+
+// nsPgpMimeProxy implementation
+nsPgpMimeProxy::nsPgpMimeProxy()
+ : mInitialized(false),
+#ifdef DEBUG
+ mOutputWasRemoved(false),
+#endif
+ mOutputFun(nullptr),
+ mOutputClosure(nullptr),
+ mLoadFlags(LOAD_NORMAL),
+ mCancelStatus(NS_OK),
+ mAllowNestedDecrypt(false) {
+}
+
+nsPgpMimeProxy::~nsPgpMimeProxy() { Finalize(); }
+
+nsresult nsPgpMimeProxy::Finalize() { return NS_OK; }
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetMimeCallback(MimeDecodeCallbackFun outputFun,
+ void* outputClosure, nsIURI* myUri) {
+ if (!outputFun || !outputClosure) return NS_ERROR_NULL_POINTER;
+
+ mOutputFun = outputFun;
+ mOutputClosure = outputClosure;
+ mInitialized = true;
+ mMessageURI = myUri;
+
+ mStreamOffset = 0;
+ mByteBuf.Truncate();
+
+ if (mDecryptor) return mDecryptor->OnStartRequest((nsIRequest*)this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::RemoveMimeCallback() {
+ mOutputFun = nullptr;
+ mOutputClosure = nullptr;
+#ifdef DEBUG
+ mOutputWasRemoved = true;
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Init() {
+ mByteBuf.Truncate();
+
+ // Create add-on supplied decryption object.
+ nsresult rv;
+ mDecryptor = do_CreateInstance(PGPMIME_JS_DECRYPTOR_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) mDecryptor = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Write(const char* buf, uint32_t buf_size) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ mByteBuf.Assign(buf, buf_size);
+ mStreamOffset = 0;
+
+ // Pass data to the decryption object for decryption.
+ // The result is returned via OutputDecryptedData().
+ if (mDecryptor)
+ return mDecryptor->OnDataAvailable((nsIRequest*)this, (nsIInputStream*)this,
+ 0, buf_size);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Finish() {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ if (mDecryptor) {
+ return mDecryptor->OnStopRequest((nsIRequest*)this, NS_OK);
+ } else {
+ if (!mOutputFun) return NS_ERROR_FAILURE;
+
+ nsCString temp;
+ temp.AppendLiteral(
+ "Content-Type: text/html; Charset=utf-8\r\n\r\n<html><body>");
+ temp.AppendLiteral(
+ "<BR><text=\"#000000\" bgcolor=\"#FFFFFF\" link=\"#FF0000\" "
+ "vlink=\"#800080\" alink=\"#0000FF\">");
+ temp.AppendLiteral("<center><table BORDER=1 ><tr><td><CENTER>");
+
+ nsCString tString;
+ PgpMimeGetNeedsAddonString(tString);
+ temp.Append(tString);
+ temp.AppendLiteral(
+ "</CENTER></td></tr></table></center><BR></body></html>\r\n");
+
+ PR_SetError(0, 0);
+ int status = mOutputFun(temp.get(), temp.Length(), mOutputClosure);
+ if (status < 0) {
+ PR_SetError(status, 0);
+ mOutputFun = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetDecryptor(nsIStreamListener** aDecryptor) {
+ NS_IF_ADDREF(*aDecryptor = mDecryptor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetDecryptor(nsIStreamListener* aDecryptor) {
+ mDecryptor = aDecryptor;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetContentType(nsACString& aContentType) {
+ aContentType = mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetContentType(const nsACString& aContentType) {
+ mContentType = aContentType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetMessageURI(nsIURI** aMessageURI) {
+ NS_IF_ADDREF(*aMessageURI = mMessageURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetMimePart(nsACString& aMimePart) {
+ aMimePart = mMimePart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetMimePart(const nsACString& aMimePart) {
+ mMimePart = aMimePart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetAllowNestedDecrypt(bool aAllowNestedDecrypt) {
+ mAllowNestedDecrypt = aAllowNestedDecrypt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetAllowNestedDecrypt(bool* aAllowNestedDecrypt) {
+ *aAllowNestedDecrypt = mAllowNestedDecrypt;
+ return NS_OK;
+}
+
+/**
+ * This method is called by the add-on-supplied decryption object.
+ * It passes the decrypted data back to the proxy which calls the
+ * output function is was initialised with.
+ */
+NS_IMETHODIMP
+nsPgpMimeProxy::OutputDecryptedData(const char* buf, uint32_t buf_size) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ NS_ENSURE_ARG(buf);
+
+#ifdef DEBUG
+ // If this assertion is hit, there might be a bug related to object
+ // lifetime, e.g. a JS MIME handler might live longer than the
+ // corresponding MIME data, e.g. bug 1665475.
+ NS_ASSERTION(!mOutputWasRemoved, "MIME data already destroyed");
+#endif
+
+ if (!mOutputFun) return NS_ERROR_FAILURE;
+
+ int status = mOutputFun(buf, buf_size, mOutputClosure);
+ if (status < 0) {
+ PR_SetError(status, 0);
+ mOutputFun = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIRequest methods
+///////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetName(nsACString& result) {
+ result = "pgpmimeproxy";
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::IsPending(bool* result) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ *result = NS_SUCCEEDED(mCancelStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetStatus(nsresult* status) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ *status = mCancelStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPgpMimeProxy::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsPgpMimeProxy::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsPgpMimeProxy::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+// NOTE: We assume that OnStopRequest should not be called if
+// request is canceled. This may be wrong!
+NS_IMETHODIMP
+nsPgpMimeProxy::Cancel(nsresult status) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ // Need a non-zero status code to cancel
+ if (NS_SUCCEEDED(status)) return NS_ERROR_FAILURE;
+
+ if (NS_SUCCEEDED(mCancelStatus)) mCancelStatus = status;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Suspend(void) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Resume(void) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ NS_IF_ADDREF(*aLoadGroup = mLoadGroup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ mLoadGroup = aLoadGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIInputStream methods
+///////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Available(uint64_t* _retval) {
+ NS_ENSURE_ARG(_retval);
+
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ *_retval = (mByteBuf.Length() > mStreamOffset)
+ ? mByteBuf.Length() - mStreamOffset
+ : 0;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Read(char* buf, uint32_t count, uint32_t* readCount) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ if (!buf || !readCount) return NS_ERROR_NULL_POINTER;
+
+ int32_t avail = (mByteBuf.Length() > mStreamOffset)
+ ? mByteBuf.Length() - mStreamOffset
+ : 0;
+
+ uint32_t readyCount = ((uint32_t)avail > count) ? count : avail;
+
+ if (readyCount) {
+ memcpy(buf, mByteBuf.get() + mStreamOffset, readyCount);
+ *readCount = readyCount;
+ }
+
+ mStreamOffset += *readCount;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::ReadSegments(nsWriteSegmentFun writer, void* aClosure,
+ uint32_t count, uint32_t* readCount) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::IsNonBlocking(bool* aNonBlocking) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ *aNonBlocking = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPgpMimeProxy::Close() {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+
+ mStreamOffset = 0;
+ mByteBuf.Truncate();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPgpMimeProxy::StreamStatus() { return NS_OK; }
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIStreamListener methods
+///////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsPgpMimeProxy::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+nsPgpMimeProxy::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIStreamListener method
+///////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsPgpMimeProxy::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aSourceOffset, uint32_t aLength) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
+ NS_ENSURE_ARG(aInputStream);
+
+ if (!mOutputFun) return NS_ERROR_FAILURE;
+
+ char buf[kCharMax];
+ uint32_t readCount, readMax;
+
+ while (aLength > 0) {
+ readMax = (aLength < kCharMax) ? aLength : kCharMax;
+
+ nsresult rv;
+ rv = aInputStream->Read((char*)buf, readMax, &readCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int status = mOutputFun(buf, readCount, mOutputClosure);
+ if (status < 0) {
+ PR_SetError(status, 0);
+ mOutputFun = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ aLength -= readCount;
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.h b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.h
new file mode 100644
index 0000000000..e115be2ddd
--- /dev/null
+++ b/comm/mailnews/mime/cthandlers/pgpmime/nsPgpMimeProxy.h
@@ -0,0 +1,73 @@
+/* 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/. */
+
+#ifndef _nsPgpmimeDecrypt_h_
+#define _nsPgpmimeDecrypt_h_
+
+#include "mimecth.h"
+#include "nsIPgpMimeProxy.h"
+#include "nsCOMPtr.h"
+#include "nsIStreamListener.h"
+#include "nsIInputStream.h"
+#include "nsILoadGroup.h"
+
+#define PGPMIME_JS_DECRYPTOR_CONTRACTID \
+ "@mozilla.org/mime/pgp-mime-js-decrypt;1"
+
+typedef struct MimeEncryptedPgpClass MimeEncryptedPgpClass;
+typedef struct MimeEncryptedPgp MimeEncryptedPgp;
+
+struct MimeEncryptedPgpClass {
+ MimeEncryptedClass encrypted;
+};
+
+struct MimeEncryptedPgp {
+ MimeEncrypted encrypted;
+};
+
+class nsPgpMimeProxy : public nsIPgpMimeProxy,
+ public nsIRequest,
+ public nsIInputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPGPMIMEPROXY
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUEST
+ NS_DECL_NSIINPUTSTREAM
+
+ nsPgpMimeProxy();
+
+ // Define a Create method to be used with a factory:
+ static nsresult Create(nsISupports* aOuter, REFNSIID aIID, void** aResult);
+
+ protected:
+ virtual ~nsPgpMimeProxy();
+ bool mInitialized;
+ nsCOMPtr<nsIStreamListener> mDecryptor;
+
+#ifdef DEBUG
+ PRBool mOutputWasRemoved;
+#endif
+ MimeDecodeCallbackFun mOutputFun;
+ void* mOutputClosure;
+
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+ nsLoadFlags mLoadFlags;
+ nsresult mCancelStatus;
+
+ uint32_t mStreamOffset;
+ nsCString mByteBuf;
+ nsCString mContentType;
+ nsCString mMimePart;
+ bool mAllowNestedDecrypt;
+
+ nsCOMPtr<nsIURI> mMessageURI;
+ nsresult Finalize();
+};
+
+#define MimeEncryptedPgpClassInitializer(ITYPE, CSUPER) \
+ { MimeEncryptedClassInitializer(ITYPE, CSUPER) }
+
+#endif
diff --git a/comm/mailnews/mime/emitters/moz.build b/comm/mailnews/mime/emitters/moz.build
new file mode 100644
index 0000000000..7e17ac0b9f
--- /dev/null
+++ b/comm/mailnews/mime/emitters/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += []
+
+SOURCES += [
+ "nsEmitterUtils.cpp",
+ "nsMimeBaseEmitter.cpp",
+ "nsMimeHtmlEmitter.cpp",
+ "nsMimePlainEmitter.cpp",
+ "nsMimeRawEmitter.cpp",
+ "nsMimeRebuffer.cpp",
+ "nsMimeXmlEmitter.cpp",
+]
+
+FINAL_LIBRARY = "mail"
diff --git a/comm/mailnews/mime/emitters/nsEmitterUtils.cpp b/comm/mailnews/mime/emitters/nsEmitterUtils.cpp
new file mode 100644
index 0000000000..d2e29026ba
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsEmitterUtils.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsMailHeaders.h"
+#include "nsIMimeEmitter.h"
+#include "prprf.h"
+
+extern "C" bool EmitThisHeaderForPrefSetting(int32_t dispType,
+ const char* header) {
+ if (nsMimeHeaderDisplayTypes::AllHeaders == dispType) return true;
+
+ if ((!header) || (!*header)) return false;
+
+ if (nsMimeHeaderDisplayTypes::MicroHeaders == dispType) {
+ if ((!strcmp(header, HEADER_SUBJECT)) || (!strcmp(header, HEADER_FROM)) ||
+ (!strcmp(header, HEADER_DATE)))
+ return true;
+ else
+ return false;
+ }
+
+ if (nsMimeHeaderDisplayTypes::NormalHeaders == dispType) {
+ if (!strcmp(header, HEADER_DATE) || !strcmp(header, HEADER_TO) ||
+ !strcmp(header, HEADER_SUBJECT) || !strcmp(header, HEADER_SENDER) ||
+ !strcmp(header, HEADER_RESENT_TO) ||
+ !strcmp(header, HEADER_RESENT_SENDER) ||
+ !strcmp(header, HEADER_RESENT_FROM) ||
+ !strcmp(header, HEADER_RESENT_CC) || !strcmp(header, HEADER_REPLY_TO) ||
+ !strcmp(header, HEADER_REFERENCES) ||
+ !strcmp(header, HEADER_NEWSGROUPS) ||
+ !strcmp(header, HEADER_MESSAGE_ID) || !strcmp(header, HEADER_FROM) ||
+ !strcmp(header, HEADER_FOLLOWUP_TO) || !strcmp(header, HEADER_CC) ||
+ !strcmp(header, HEADER_ORGANIZATION) || !strcmp(header, HEADER_BCC))
+ return true;
+ else
+ return false;
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/mime/emitters/nsEmitterUtils.h b/comm/mailnews/mime/emitters/nsEmitterUtils.h
new file mode 100644
index 0000000000..30499cc84f
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsEmitterUtils.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsEmitterUtils_h_
+#define _nsEmitterUtils_h_
+
+#include "prmem.h"
+#include "plstr.h"
+
+extern "C" bool EmitThisHeaderForPrefSetting(int32_t dispType,
+ const char* header);
+
+#endif // _nsEmitterUtils_h_
diff --git a/comm/mailnews/mime/emitters/nsMimeBaseEmitter.cpp b/comm/mailnews/mime/emitters/nsMimeBaseEmitter.cpp
new file mode 100644
index 0000000000..5d60f050d5
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeBaseEmitter.cpp
@@ -0,0 +1,978 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include <stdio.h>
+#include "nsMimeBaseEmitter.h"
+#include "nsMailHeaders.h"
+#include "nscore.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "prmem.h"
+#include "nsEmitterUtils.h"
+#include "nsMimeStringResources.h"
+#include "msgCore.h"
+#include "nsEmitterUtils.h"
+#include "nsIMimeStreamConverter.h"
+#include "mozilla/Logging.h"
+#include "prprf.h"
+#include "nsIMimeHeaders.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsTextFormatter.h"
+#include "mozilla/Components.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+static mozilla::LazyLogModule gMimeEmitterLogModule("MIME");
+
+#define MIME_HEADER_URL "chrome://messenger/locale/mimeheader.properties"
+#define MIME_URL "chrome://messenger/locale/mime.properties"
+
+NS_IMPL_ISUPPORTS(nsMimeBaseEmitter, nsIMimeEmitter, nsIInterfaceRequestor)
+
+nsMimeBaseEmitter::nsMimeBaseEmitter() {
+ // Initialize data output vars...
+ mFirstHeaders = true;
+
+ mBufferMgr = nullptr;
+ mTotalWritten = 0;
+ mTotalRead = 0;
+ mInputStream = nullptr;
+ mOutStream = nullptr;
+ mOutListener = nullptr;
+
+ // Display output control vars...
+ mDocHeader = false;
+ m_stringBundle = nullptr;
+ mURL = nullptr;
+ mHeaderDisplayType = nsMimeHeaderDisplayTypes::NormalHeaders;
+
+ // Setup array for attachments
+ mAttachCount = 0;
+ mAttachArray = new nsTArray<attachmentInfoType*>();
+ mCurrentAttachment = nullptr;
+
+ // Header cache...
+ mHeaderArray = new nsTArray<headerInfoType*>();
+
+ // Embedded Header Cache...
+ mEmbeddedHeaderArray = nullptr;
+
+ // HTML Header Data...
+ // mHTMLHeaders = "";
+ // mCharset = "";
+
+ // Init the body...
+ mBodyStarted = false;
+ // mBody = "";
+
+ // This is needed for conversion of I18N Strings...
+ mUnicodeConverter = do_GetService("@mozilla.org/messenger/mimeconverter;1");
+
+ // Do prefs last since we can live without this if it fails...
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (pPrefBranch)
+ pPrefBranch->GetIntPref("mail.show_headers", &mHeaderDisplayType);
+}
+
+nsMimeBaseEmitter::~nsMimeBaseEmitter(void) {
+ // Delete the buffer manager...
+ if (mBufferMgr) {
+ delete mBufferMgr;
+ mBufferMgr = nullptr;
+ }
+
+ // Clean up the attachment array structures...
+ if (mAttachArray) {
+ for (size_t i = 0; i < mAttachArray->Length(); i++) {
+ attachmentInfoType* attachInfo = mAttachArray->ElementAt(i);
+ if (!attachInfo) continue;
+
+ PR_FREEIF(attachInfo->contentType);
+ if (attachInfo->displayName) free(attachInfo->displayName);
+ PR_FREEIF(attachInfo->urlSpec);
+ PR_FREEIF(attachInfo);
+ }
+ delete mAttachArray;
+ }
+
+ // Cleanup allocated header arrays...
+ CleanupHeaderArray(mHeaderArray);
+ mHeaderArray = nullptr;
+
+ CleanupHeaderArray(mEmbeddedHeaderArray);
+ mEmbeddedHeaderArray = nullptr;
+}
+
+NS_IMETHODIMP nsMimeBaseEmitter::GetInterface(const nsIID& aIID,
+ void** aInstancePtr) {
+ NS_ENSURE_ARG_POINTER(aInstancePtr);
+ return QueryInterface(aIID, aInstancePtr);
+}
+
+void nsMimeBaseEmitter::CleanupHeaderArray(nsTArray<headerInfoType*>* aArray) {
+ if (!aArray) return;
+
+ for (size_t i = 0; i < aArray->Length(); i++) {
+ headerInfoType* headerInfo = aArray->ElementAt(i);
+ if (!headerInfo) continue;
+
+ PR_FREEIF(headerInfo->name);
+ PR_FREEIF(headerInfo->value);
+ PR_FREEIF(headerInfo);
+ }
+
+ delete aArray;
+}
+
+static int32_t MapHeaderNameToID(const char* header) {
+ // emitter passes UPPERCASE for header names
+ if (!strcmp(header, "DATE"))
+ return MIME_MHTML_DATE;
+ else if (!strcmp(header, "FROM"))
+ return MIME_MHTML_FROM;
+ else if (!strcmp(header, "SUBJECT"))
+ return MIME_MHTML_SUBJECT;
+ else if (!strcmp(header, "TO"))
+ return MIME_MHTML_TO;
+ else if (!strcmp(header, "SENDER"))
+ return MIME_MHTML_SENDER;
+ else if (!strcmp(header, "RESENT-TO"))
+ return MIME_MHTML_RESENT_TO;
+ else if (!strcmp(header, "RESENT-SENDER"))
+ return MIME_MHTML_RESENT_SENDER;
+ else if (!strcmp(header, "RESENT-FROM"))
+ return MIME_MHTML_RESENT_FROM;
+ else if (!strcmp(header, "RESENT-CC"))
+ return MIME_MHTML_RESENT_CC;
+ else if (!strcmp(header, "REPLY-TO"))
+ return MIME_MHTML_REPLY_TO;
+ else if (!strcmp(header, "REFERENCES"))
+ return MIME_MHTML_REFERENCES;
+ else if (!strcmp(header, "NEWSGROUPS"))
+ return MIME_MHTML_NEWSGROUPS;
+ else if (!strcmp(header, "MESSAGE-ID"))
+ return MIME_MHTML_MESSAGE_ID;
+ else if (!strcmp(header, "FOLLOWUP-TO"))
+ return MIME_MHTML_FOLLOWUP_TO;
+ else if (!strcmp(header, "CC"))
+ return MIME_MHTML_CC;
+ else if (!strcmp(header, "ORGANIZATION"))
+ return MIME_MHTML_ORGANIZATION;
+ else if (!strcmp(header, "BCC"))
+ return MIME_MHTML_BCC;
+
+ return 0;
+}
+
+char* nsMimeBaseEmitter::MimeGetStringByName(const char* aHeaderName) {
+ nsresult res = NS_OK;
+
+ if (!m_headerStringBundle) {
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ if (sBundleService) {
+ res = sBundleService->CreateBundle(MIME_HEADER_URL,
+ getter_AddRefs(m_headerStringBundle));
+ if (NS_FAILED(res)) return nullptr;
+ }
+ }
+
+ if (m_headerStringBundle) {
+ nsString val;
+
+ res = m_headerStringBundle->GetStringFromName(aHeaderName, val);
+
+ if (NS_FAILED(res)) return nullptr;
+
+ // Here we need to return a new copy of the string
+ // This returns a UTF-8 string so the caller needs to perform a conversion
+ // if this is used as UCS-2 (e.g. cannot do nsString(utfStr);
+ //
+ return ToNewUTF8String(val);
+ }
+ return nullptr;
+}
+
+char* nsMimeBaseEmitter::MimeGetStringByID(int32_t aID) {
+ nsresult res = NS_OK;
+
+ if (!m_stringBundle) {
+ static const char propertyURL[] = MIME_URL;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ if (sBundleService)
+ res = sBundleService->CreateBundle(propertyURL,
+ getter_AddRefs(m_stringBundle));
+ }
+
+ if (m_stringBundle) {
+ nsString val;
+ res = m_stringBundle->GetStringFromID(aID, val);
+
+ if (NS_FAILED(res)) return nullptr;
+
+ return ToNewUTF8String(val);
+ }
+ return nullptr;
+}
+
+//
+// This will search a string bundle (eventually) to find a descriptive header
+// name to match what was found in the mail message. aHeaderName is passed in
+// in all caps and a dropback default name is provided. The caller needs to free
+// the memory returned by this function.
+//
+char* nsMimeBaseEmitter::LocalizeHeaderName(const char* aHeaderName,
+ const char* aDefaultName) {
+ char* retVal = nullptr;
+
+ // prefer to use translated strings if not for quoting
+ if (mFormat != nsMimeOutput::nsMimeMessageQuoting &&
+ mFormat != nsMimeOutput::nsMimeMessageBodyQuoting) {
+ // map name to id and get the translated string
+ int32_t id = MapHeaderNameToID(aHeaderName);
+ if (id > 0) retVal = MimeGetStringByID(id);
+ }
+
+ // get the string from the other bundle (usually not translated)
+ if (!retVal) retVal = MimeGetStringByName(aHeaderName);
+
+ if (retVal)
+ return retVal;
+ else
+ return strdup(aDefaultName);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// nsMimeBaseEmitter Interface
+///////////////////////////////////////////////////////////////////////////
+NS_IMETHODIMP
+nsMimeBaseEmitter::SetPipe(nsIInputStream* aInputStream,
+ nsIOutputStream* outStream) {
+ mInputStream = aInputStream;
+ mOutStream = outStream;
+ return NS_OK;
+}
+
+// Note - these is setup only...you should not write
+// anything to the stream since these may be image data
+// output streams, etc...
+NS_IMETHODIMP
+nsMimeBaseEmitter::Initialize(nsIURI* url, nsIChannel* aChannel,
+ int32_t aFormat) {
+ // set the url
+ mURL = url;
+ mChannel = aChannel;
+
+ // Create rebuffering object
+ delete mBufferMgr;
+ mBufferMgr = new MimeRebuffer();
+
+ // Counters for output stream
+ mTotalWritten = 0;
+ mTotalRead = 0;
+ mFormat = aFormat;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::SetOutputListener(nsIStreamListener* listener) {
+ mOutListener = listener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::GetOutputListener(nsIStreamListener** listener) {
+ NS_ENSURE_ARG_POINTER(listener);
+
+ NS_IF_ADDREF(*listener = mOutListener);
+ return NS_OK;
+}
+
+// Attachment handling routines
+nsresult nsMimeBaseEmitter::StartAttachment(const nsACString& name,
+ const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) {
+ // Ok, now we will setup the attachment info
+ mCurrentAttachment = (attachmentInfoType*)PR_NEWZAP(attachmentInfoType);
+ if ((mCurrentAttachment) && mAttachArray) {
+ ++mAttachCount;
+
+ mCurrentAttachment->displayName = ToNewCString(name);
+ mCurrentAttachment->urlSpec = strdup(url);
+ mCurrentAttachment->contentType = strdup(contentType);
+ mCurrentAttachment->isExternalAttachment = aIsExternalAttachment;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::EndAttachment() {
+ // Ok, add the attachment info to the attachment array...
+ if ((mCurrentAttachment) && (mAttachArray)) {
+ mAttachArray->AppendElement(mCurrentAttachment);
+ mCurrentAttachment = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::EndAllAttachments() { return NS_OK; }
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::AddAttachmentField(const char* field, const char* value) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::UtilityWrite(const char* buf) {
+ NS_ENSURE_ARG_POINTER(buf);
+
+ uint32_t written;
+ Write(nsDependentCString(buf), &written);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::UtilityWrite(const nsACString& buf) {
+ uint32_t written;
+ Write(buf, &written);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::UtilityWriteCRLF(const char* buf) {
+ NS_ENSURE_ARG_POINTER(buf);
+
+ uint32_t written;
+ Write(nsDependentCString(buf), &written);
+ Write(nsLiteralCString(CRLF), &written);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::Write(const nsACString& buf, uint32_t* amountWritten) {
+ unsigned int written = 0;
+ nsresult rv = NS_OK;
+ uint32_t needToWrite;
+
+#ifdef DEBUG_BenB
+ // If you want to see libmime output...
+ printf("%s", buf);
+#endif
+
+ MOZ_LOG(gMimeEmitterLogModule, mozilla::LogLevel::Info,
+ ("%s", PromiseFlatCString(buf).get()));
+ //
+ // Make sure that the buffer we are "pushing" into has enough room
+ // for the write operation. If not, we have to buffer, return, and get
+ // it on the next time through
+ //
+ *amountWritten = 0;
+
+ needToWrite = mBufferMgr->GetSize();
+ // First, handle any old buffer data...
+ if (needToWrite > 0) {
+ rv = WriteHelper(mBufferMgr->GetBuffer(), &written);
+
+ mTotalWritten += written;
+ mBufferMgr->ReduceBuffer(written);
+ *amountWritten = written;
+
+ // if we couldn't write all the old data, buffer the new data
+ // and return
+ if (mBufferMgr->GetSize() > 0) {
+ mBufferMgr->IncreaseBuffer(buf);
+ return rv;
+ }
+ }
+
+ // if we get here, we are dealing with new data...try to write
+ // and then do the right thing...
+ rv = WriteHelper(buf, &written);
+ *amountWritten = written;
+ mTotalWritten += written;
+
+ if (written < buf.Length()) {
+ const nsACString& remainder = Substring(buf, written);
+ mBufferMgr->IncreaseBuffer(remainder);
+ }
+
+ return rv;
+}
+
+nsresult nsMimeBaseEmitter::WriteHelper(const nsACString& buf,
+ uint32_t* countWritten) {
+ NS_ENSURE_TRUE(mOutStream, NS_ERROR_NOT_INITIALIZED);
+
+ nsresult rv =
+ mOutStream->Write(buf.BeginReading(), buf.Length(), countWritten);
+ if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
+ // pipe is full, push contents of pipe to listener...
+ uint64_t avail;
+ rv = mInputStream->Available(&avail);
+ if (NS_SUCCEEDED(rv) && avail) {
+ mOutListener->OnDataAvailable(mChannel, mInputStream, 0,
+ std::min(avail, uint64_t(PR_UINT32_MAX)));
+
+ // try writing again...
+ rv = mOutStream->Write(buf.BeginReading(), buf.Length(), countWritten);
+ }
+ }
+ return rv;
+}
+
+//
+// Find a cached header! Note: Do NOT free this value!
+//
+const char* nsMimeBaseEmitter::GetHeaderValue(const char* aHeaderName) {
+ char* retVal = nullptr;
+ nsTArray<headerInfoType*>* array =
+ mDocHeader ? mHeaderArray : mEmbeddedHeaderArray;
+
+ if (!array) return nullptr;
+
+ for (size_t i = 0; i < array->Length(); i++) {
+ headerInfoType* headerInfo = array->ElementAt(i);
+ if ((!headerInfo) || (!headerInfo->name) || (!(*headerInfo->name)))
+ continue;
+
+ if (!PL_strcasecmp(aHeaderName, headerInfo->name)) {
+ retVal = headerInfo->value;
+ break;
+ }
+ }
+
+ return retVal;
+}
+
+//
+// This is called at the start of the header block for all header information in
+// ANY AND ALL MESSAGES (yes, quoted, attached, etc...)
+//
+// NOTE: This will be called even when headers are will not follow. This is
+// to allow us to be notified of the charset of the original message. This is
+// important for forward and reply operations
+//
+NS_IMETHODIMP
+nsMimeBaseEmitter::StartHeader(bool rootMailHeader, bool headerOnly,
+ const char* msgID, const char* outCharset) {
+ NS_ENSURE_ARG_POINTER(outCharset);
+
+ mDocHeader = rootMailHeader;
+
+ // If this is not the mail messages header, then we need to create
+ // the mEmbeddedHeaderArray structure for use with this internal header
+ // structure.
+ if (!mDocHeader) {
+ if (mEmbeddedHeaderArray) CleanupHeaderArray(mEmbeddedHeaderArray);
+
+ mEmbeddedHeaderArray = new nsTArray<headerInfoType*>();
+ NS_ENSURE_TRUE(mEmbeddedHeaderArray, NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // If the main doc, check on updated character set
+ if (mDocHeader) UpdateCharacterSet(outCharset);
+ CopyASCIItoUTF16(nsDependentCString(outCharset), mCharset);
+ return NS_OK;
+}
+
+// Ok, if we are here, and we have a aCharset passed in that is not
+// UTF-8 or US-ASCII, then we should tag the mChannel member with this
+// charset. This is because replying to messages with specified charset's
+// need to be tagged as that charset by default.
+//
+NS_IMETHODIMP
+nsMimeBaseEmitter::UpdateCharacterSet(const char* aCharset) {
+ if (aCharset) {
+ nsAutoCString contentType;
+
+ if (NS_SUCCEEDED(mChannel->GetContentType(contentType)) &&
+ !contentType.IsEmpty()) {
+ char* cBegin = contentType.BeginWriting();
+
+ const char* cPtr = PL_strcasestr(cBegin, "charset=");
+
+ if (cPtr) {
+ char* ptr = cBegin;
+ while (*ptr) {
+ if ((*ptr == ' ') || (*ptr == ';')) {
+ if ((ptr + 1) >= cPtr) {
+ *ptr = '\0';
+ break;
+ }
+ }
+
+ ++ptr;
+ }
+ }
+
+ // have to set content-type since it could have an embedded null byte
+ mChannel->SetContentType(nsDependentCString(cBegin));
+ if (PL_strcasecmp(aCharset, "US-ASCII") == 0) {
+ mChannel->SetContentCharset("ISO-8859-1"_ns);
+ } else {
+ mChannel->SetContentCharset(nsDependentCString(aCharset));
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+//
+// This will be called for every header field regardless if it is in an
+// internal body or the outer message.
+//
+NS_IMETHODIMP
+nsMimeBaseEmitter::AddHeaderField(const char* field, const char* value) {
+ if ((!field) || (!value)) return NS_OK;
+
+ nsTArray<headerInfoType*>* tPtr;
+ if (mDocHeader)
+ tPtr = mHeaderArray;
+ else
+ tPtr = mEmbeddedHeaderArray;
+
+ // This is a header so we need to cache and output later.
+ // Ok, now we will setup the header info for the header array!
+ headerInfoType* ptr = (headerInfoType*)PR_NEWZAP(headerInfoType);
+ if ((ptr) && tPtr) {
+ ptr->name = strdup(field);
+ ptr->value = strdup(value);
+ tPtr->AppendElement(ptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::AddAllHeaders(const nsACString& allheaders) {
+ if (mDocHeader) // We want to set only the main headers of a message, not the
+ // potentially embedded one
+ {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(mURL));
+ if (msgurl) {
+ nsCOMPtr<nsIMimeHeaders> mimeHeaders =
+ do_CreateInstance(NS_IMIMEHEADERS_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mimeHeaders->Initialize(allheaders);
+ msgurl->SetMimeHeaders(mimeHeaders);
+ }
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// The following code is responsible for formatting headers in a manner that is
+// identical to the normal XUL output.
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMimeBaseEmitter::GenerateDateString(const char* dateString,
+ nsACString& formattedDate,
+ bool showDateForToday) {
+ nsresult rv = NS_OK;
+
+ /**
+ * See if the user wants to have the date displayed in the senders
+ * timezone (including the timezone offset).
+ */
+ bool displaySenderTimezone = false;
+
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> dateFormatPrefs;
+ rv = prefs->GetBranch("mailnews.display.", getter_AddRefs(dateFormatPrefs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dateFormatPrefs->GetBoolPref("date_senders_timezone", &displaySenderTimezone);
+
+ PRExplodedTime explodedMsgTime;
+
+ // Bogus date string may leave some fields uninitialized, so take precaution.
+ memset(&explodedMsgTime, 0, sizeof(PRExplodedTime));
+
+ if (PR_ParseTimeStringToExplodedTime(dateString, false, &explodedMsgTime) !=
+ PR_SUCCESS)
+ return NS_ERROR_FAILURE;
+
+ /**
+ * To determine the date format to use, comparison of current and message
+ * time has to be made. If displaying in local time, both timestamps have
+ * to be in local time. If displaying in senders time zone, leave the compare
+ * time in that time zone.
+ * Otherwise in TZ+0100 on 2009-03-12 a message from 2009-03-11T20:49-0700
+ * would be displayed as "20:49 -0700" though it in fact is not from the
+ * same day.
+ */
+ PRExplodedTime explodedCompTime;
+ if (displaySenderTimezone)
+ explodedCompTime = explodedMsgTime;
+ else
+ PR_ExplodeTime(PR_ImplodeTime(&explodedMsgTime), PR_LocalTimeParameters,
+ &explodedCompTime);
+
+ PRExplodedTime explodedCurrentTime;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &explodedCurrentTime);
+
+ // If we want short dates, check if the message is from today, and if so
+ // only show the time (e.g. 3:15 pm).
+ nsDateFormatSelectorComm dateFormat = kDateFormatShort;
+ if (!showDateForToday &&
+ explodedCurrentTime.tm_year == explodedCompTime.tm_year &&
+ explodedCurrentTime.tm_month == explodedCompTime.tm_month &&
+ explodedCurrentTime.tm_mday == explodedCompTime.tm_mday) {
+ // same day...
+ dateFormat = kDateFormatNone;
+ }
+
+ nsAutoString formattedDateString;
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ if (dateFormat == kDateFormatShort) {
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ }
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, &explodedCompTime,
+ formattedDateString);
+
+ if (NS_SUCCEEDED(rv)) {
+ if (displaySenderTimezone) {
+ // offset of local time from UTC in minutes
+ int32_t senderoffset = (explodedMsgTime.tm_params.tp_gmt_offset +
+ explodedMsgTime.tm_params.tp_dst_offset) /
+ 60;
+ // append offset to date string
+ nsString tzstring;
+ nsTextFormatter::ssprintf(
+ tzstring, u" %+05d", (senderoffset / 60 * 100) + (senderoffset % 60));
+ formattedDateString.Append(tzstring);
+ }
+
+ CopyUTF16toUTF8(formattedDateString, formattedDate);
+ }
+
+ return rv;
+}
+
+char* nsMimeBaseEmitter::GetLocalizedDateString(const char* dateString) {
+ char* i18nValue = nullptr;
+
+ bool displayOriginalDate = false;
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ if (prefBranch)
+ prefBranch->GetBoolPref("mailnews.display.date_senders_timezone",
+ &displayOriginalDate);
+
+ if (!displayOriginalDate) {
+ nsAutoCString convertedDateString;
+ nsresult rv = GenerateDateString(dateString, convertedDateString, true);
+ if (NS_SUCCEEDED(rv))
+ i18nValue = strdup(convertedDateString.get());
+ else
+ i18nValue = strdup(dateString);
+ } else
+ i18nValue = strdup(dateString);
+
+ return i18nValue;
+}
+
+nsresult nsMimeBaseEmitter::WriteHeaderFieldHTML(const char* field,
+ const char* value) {
+ nsCString newValue;
+ char* i18nValue = nullptr;
+
+ if ((!field) || (!value)) return NS_OK;
+
+ //
+ // This is a check to see what the pref is for header display. If
+ // We should only output stuff that corresponds with that setting.
+ //
+ if (!EmitThisHeaderForPrefSetting(mHeaderDisplayType, field)) return NS_OK;
+
+ //
+ // If we encounter the 'Date' header we try to convert it's value
+ // into localized format.
+ //
+ if (strcmp(field, "Date") == 0)
+ i18nValue = GetLocalizedDateString(value);
+ else
+ i18nValue = strdup(value);
+
+ if ((mUnicodeConverter) && (mFormat != nsMimeOutput::nsMimeMessageSaveAs)) {
+ nsCString tValue;
+
+ // we're going to need a converter to convert
+ nsresult rv = mUnicodeConverter->DecodeMimeHeaderToUTF8(
+ nsDependentCString(i18nValue), nullptr, false, true, tValue);
+ if (NS_SUCCEEDED(rv) && !tValue.IsEmpty())
+ nsAppendEscapedHTML(tValue, newValue);
+ else
+ nsAppendEscapedHTML(nsDependentCString(i18nValue), newValue);
+ } else {
+ nsAppendEscapedHTML(nsDependentCString(i18nValue), newValue);
+ }
+
+ free(i18nValue);
+
+ if (newValue.IsEmpty()) return NS_OK;
+
+ mHTMLHeaders.AppendLiteral("<tr>");
+ mHTMLHeaders.AppendLiteral("<td>");
+
+ if (mFormat == nsMimeOutput::nsMimeMessageSaveAs)
+ mHTMLHeaders.AppendLiteral("<b>");
+ else
+ mHTMLHeaders.AppendLiteral(
+ "<div class=\"moz-header-display-name\" style=\"display:inline;\">");
+
+ // Here is where we are going to try to L10N the tagName so we will always
+ // get a field name next to an emitted header value. Note: Default will always
+ // be the name of the header itself.
+ //
+ nsCString newTagName(field);
+ newTagName.StripWhitespace();
+ ToUpperCase(newTagName);
+
+ char* l10nTagName = LocalizeHeaderName(newTagName.get(), field);
+ if ((!l10nTagName) || (!*l10nTagName))
+ mHTMLHeaders.Append(field);
+ else {
+ mHTMLHeaders.Append(l10nTagName);
+ }
+ PR_FREEIF(l10nTagName);
+
+ mHTMLHeaders.AppendLiteral(": ");
+ if (mFormat == nsMimeOutput::nsMimeMessageSaveAs)
+ mHTMLHeaders.AppendLiteral("</b>");
+ else
+ mHTMLHeaders.AppendLiteral("</div>");
+
+ // Now write out the actual value itself and move on!
+ //
+ mHTMLHeaders.Append(newValue);
+ mHTMLHeaders.AppendLiteral("</td>");
+
+ mHTMLHeaders.AppendLiteral("</tr>");
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::WriteHeaderFieldHTMLPrefix(const nsACString& name) {
+ if (((mFormat == nsMimeOutput::nsMimeMessageSaveAs) && (mFirstHeaders)) ||
+ ((mFormat == nsMimeOutput::nsMimeMessagePrintOutput) &&
+ (mFirstHeaders)) ||
+ ((mFormat == nsMimeOutput::nsMimeMessageBodyDisplay) && (mFirstHeaders)))
+ /* DO NOTHING */; // rhp: Do nothing...leaving the conditional like this so
+ // its easier to see the logic of what is going on.
+ else {
+ mHTMLHeaders.AppendLiteral(
+ "<br><fieldset class=\"moz-mime-attachment-header\">");
+ if (!name.IsEmpty()) {
+ mHTMLHeaders.AppendLiteral(
+ "<legend class=\"moz-mime-attachment-header-name\">");
+ nsAppendEscapedHTML(name, mHTMLHeaders);
+ mHTMLHeaders.AppendLiteral("</legend>");
+ }
+ mHTMLHeaders.AppendLiteral("</fieldset>");
+ }
+
+ mFirstHeaders = false;
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::WriteHeaderFieldHTMLPostfix() {
+ mHTMLHeaders.AppendLiteral("<br>");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::WriteHTMLHeaders(const nsACString& name) {
+ WriteHeaderFieldHTMLPrefix(name);
+
+ // Start with the subject, from date info!
+ DumpSubjectFromDate();
+
+ // Continue with the to and cc headers
+ DumpToCC();
+
+ // Do the rest of the headers, but these will only be written if
+ // the user has the "show all headers" pref set
+ if (mHeaderDisplayType == nsMimeHeaderDisplayTypes::AllHeaders)
+ DumpRestOfHeaders();
+
+ WriteHeaderFieldHTMLPostfix();
+
+ // Now, we need to either append the headers we built up to the
+ // overall body or output to the stream.
+ UtilityWriteCRLF(mHTMLHeaders.get());
+
+ mHTMLHeaders = "";
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::DumpSubjectFromDate() {
+ mHTMLHeaders.AppendLiteral(
+ "<table border=0 cellspacing=0 cellpadding=0 width=\"100%\" "
+ "class=\"moz-header-part1");
+ if (mDocHeader) {
+ mHTMLHeaders.AppendLiteral(" moz-main-header");
+ }
+ mHTMLHeaders.AppendLiteral("\">");
+
+ // This is the envelope information
+ OutputGenericHeader(HEADER_SUBJECT);
+ OutputGenericHeader(HEADER_FROM);
+ OutputGenericHeader(HEADER_DATE);
+
+ // If we are Quoting a message, then we should dump the To: also
+ if ((mFormat == nsMimeOutput::nsMimeMessageQuoting) ||
+ (mFormat == nsMimeOutput::nsMimeMessageBodyQuoting))
+ OutputGenericHeader(HEADER_TO);
+
+ mHTMLHeaders.AppendLiteral("</table>");
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::DumpToCC() {
+ const char* toField = GetHeaderValue(HEADER_TO);
+ const char* ccField = GetHeaderValue(HEADER_CC);
+ const char* bccField = GetHeaderValue(HEADER_BCC);
+ const char* newsgroupField = GetHeaderValue(HEADER_NEWSGROUPS);
+
+ // only dump these fields if we have at least one of them! When displaying
+ // news messages that didn't have a To or Cc field, we'd always get an empty
+ // box which looked weird.
+ if (toField || ccField || bccField || newsgroupField) {
+ mHTMLHeaders.AppendLiteral(
+ "<table border=0 cellspacing=0 cellpadding=0 width=\"100%\" "
+ "class=\"moz-header-part2");
+ if (mDocHeader) {
+ mHTMLHeaders.AppendLiteral(" moz-main-header");
+ }
+ mHTMLHeaders.AppendLiteral("\">");
+
+ if (toField) WriteHeaderFieldHTML(HEADER_TO, toField);
+ if (ccField) WriteHeaderFieldHTML(HEADER_CC, ccField);
+ if (bccField) WriteHeaderFieldHTML(HEADER_BCC, bccField);
+ if (newsgroupField) WriteHeaderFieldHTML(HEADER_NEWSGROUPS, newsgroupField);
+
+ mHTMLHeaders.AppendLiteral("</table>");
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::DumpRestOfHeaders() {
+ nsTArray<headerInfoType*>* array =
+ mDocHeader ? mHeaderArray : mEmbeddedHeaderArray;
+
+ mHTMLHeaders.AppendLiteral(
+ "<table border=0 cellspacing=0 cellpadding=0 width=\"100%\" "
+ "class=\"moz-header-part3");
+ if (mDocHeader) {
+ mHTMLHeaders.AppendLiteral(" moz-main-header");
+ }
+ mHTMLHeaders.AppendLiteral("\">");
+
+ for (size_t i = 0; i < array->Length(); i++) {
+ headerInfoType* headerInfo = array->ElementAt(i);
+ if ((!headerInfo) || (!headerInfo->name) || (!(*headerInfo->name)) ||
+ (!headerInfo->value) || (!(*headerInfo->value)))
+ continue;
+
+ if ((!PL_strcasecmp(HEADER_SUBJECT, headerInfo->name)) ||
+ (!PL_strcasecmp(HEADER_DATE, headerInfo->name)) ||
+ (!PL_strcasecmp(HEADER_FROM, headerInfo->name)) ||
+ (!PL_strcasecmp(HEADER_TO, headerInfo->name)) ||
+ (!PL_strcasecmp(HEADER_CC, headerInfo->name)))
+ continue;
+
+ WriteHeaderFieldHTML(headerInfo->name, headerInfo->value);
+ }
+
+ mHTMLHeaders.AppendLiteral("</table>");
+ return NS_OK;
+}
+
+nsresult nsMimeBaseEmitter::OutputGenericHeader(const char* aHeaderVal) {
+ const char* val = GetHeaderValue(aHeaderVal);
+
+ if (val) return WriteHeaderFieldHTML(aHeaderVal, val);
+
+ return NS_ERROR_FAILURE;
+}
+
+//////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////
+// These are the methods that should be implemented by the child class!
+//////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////
+
+//
+// This should be implemented by the child class if special processing
+// needs to be done when the entire message is read.
+//
+NS_IMETHODIMP
+nsMimeBaseEmitter::Complete() {
+ // If we are here and still have data to write, we should try
+ // to flush it...if we try and fail, we should probably return
+ // an error!
+ uint32_t written;
+
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv) && (mBufferMgr) && (mBufferMgr->GetSize() > 0))
+ rv = Write(EmptyCString(), &written);
+
+ if (mOutListener) {
+ uint64_t bytesInStream = 0;
+ mozilla::DebugOnly<nsresult> rv2 = mInputStream->Available(&bytesInStream);
+ NS_ASSERTION(NS_SUCCEEDED(rv2), "Available failed");
+ if (bytesInStream) {
+ mOutListener->OnDataAvailable(
+ mChannel, mInputStream, 0,
+ std::min(bytesInStream, uint64_t(PR_UINT32_MAX)));
+ }
+ }
+
+ return NS_OK;
+}
+
+//
+// This needs to do the right thing with the stored information. It only
+// has to do the output functions, this base class will take care of the
+// memory cleanup
+//
+NS_IMETHODIMP
+nsMimeBaseEmitter::EndHeader(const nsACString& name) { return NS_OK; }
+
+// body handling routines
+NS_IMETHODIMP
+nsMimeBaseEmitter::StartBody(bool bodyOnly, const char* msgID,
+ const char* outCharset) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::WriteBody(const nsACString& buf, uint32_t* amountWritten) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeBaseEmitter::EndBody() { return NS_OK; }
diff --git a/comm/mailnews/mime/emitters/nsMimeBaseEmitter.h b/comm/mailnews/mime/emitters/nsMimeBaseEmitter.h
new file mode 100644
index 0000000000..96e7fd7b7f
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeBaseEmitter.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMimeBaseEmitter_h_
+#define _nsMimeBaseEmitter_h_
+
+#include "prio.h"
+#include "nsIMimeEmitter.h"
+#include "nsMimeRebuffer.h"
+#include "nsIStreamListener.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIURI.h"
+#include "nsIChannel.h"
+#include "nsIStringBundle.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIMimeConverter.h"
+#include "nsIInterfaceRequestor.h"
+
+//
+// The base emitter will serve as the place to do all of the caching,
+// sorting, etc... of mail headers and bodies for this internally developed
+// emitter library. The other emitter classes in this file (nsMimeHTMLEmitter,
+// etc.) will only be concerned with doing output processing ONLY.
+//
+
+//
+// Used for keeping track of the attachment information...
+//
+typedef struct {
+ char* displayName;
+ char* urlSpec;
+ char* contentType;
+ bool isExternalAttachment;
+} attachmentInfoType;
+
+//
+// For header info...
+//
+typedef struct {
+ char* name;
+ char* value;
+} headerInfoType;
+
+class nsMimeBaseEmitter : public nsIMimeEmitter, public nsIInterfaceRequestor {
+ public:
+ nsMimeBaseEmitter();
+
+ // nsISupports interface
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIMIMEEMITTER
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ // Utility output functions...
+ NS_IMETHOD UtilityWrite(const nsACString& buf);
+ NS_IMETHOD UtilityWriteCRLF(const char* buf);
+
+ // For string bundle usage...
+ char* MimeGetStringByName(const char* aHeaderName);
+ char* MimeGetStringByID(int32_t aID);
+ char* LocalizeHeaderName(const char* aHeaderName, const char* aDefaultName);
+
+ // For header processing...
+ const char* GetHeaderValue(const char* aHeaderName);
+
+ // To write out a stored header array as HTML
+ virtual nsresult WriteHeaderFieldHTMLPrefix(const nsACString& name);
+ virtual nsresult WriteHeaderFieldHTML(const char* field, const char* value);
+ virtual nsresult WriteHeaderFieldHTMLPostfix();
+
+ protected:
+ virtual ~nsMimeBaseEmitter();
+ // Internal methods...
+ void CleanupHeaderArray(nsTArray<headerInfoType*>* aArray);
+
+ // For header output...
+ nsresult DumpSubjectFromDate();
+ nsresult DumpToCC();
+ nsresult DumpRestOfHeaders();
+ nsresult OutputGenericHeader(const char* aHeaderVal);
+
+ nsresult WriteHelper(const nsACString& buf, uint32_t* countWritten);
+
+ // For string bundle usage...
+ nsCOMPtr<nsIStringBundle> m_stringBundle; // for translated strings
+ nsCOMPtr<nsIStringBundle>
+ m_headerStringBundle; // for non-translated header strings
+
+ // For buffer management on output
+ MimeRebuffer* mBufferMgr;
+
+ // mscott
+ // don't ref count the streams....the emitter is owned by the converter
+ // which owns these streams...
+ //
+ nsIOutputStream* mOutStream;
+ nsIInputStream* mInputStream;
+ nsIStreamListener* mOutListener;
+ nsCOMPtr<nsIChannel> mChannel;
+
+ // For gathering statistics on processing...
+ uint32_t mTotalWritten;
+ uint32_t mTotalRead;
+
+ // Output control and info...
+ bool mDocHeader; // For header determination...
+ nsIURI* mURL; // the url for the data being processed...
+ int32_t mHeaderDisplayType; // The setting for header output...
+ nsCString mHTMLHeaders; // HTML Header Data...
+
+ // For attachment processing...
+ int32_t mAttachCount;
+ nsTArray<attachmentInfoType*>* mAttachArray;
+ attachmentInfoType* mCurrentAttachment;
+
+ // For header caching...
+ nsTArray<headerInfoType*>* mHeaderArray;
+ nsTArray<headerInfoType*>* mEmbeddedHeaderArray;
+
+ // For body caching...
+ bool mBodyStarted;
+ nsCString mBody;
+ bool mFirstHeaders;
+
+ // For the format being used...
+ int32_t mFormat;
+
+ // For I18N Conversion...
+ nsCOMPtr<nsIMimeConverter> mUnicodeConverter;
+ nsString mCharset;
+ nsresult GenerateDateString(const char* dateString, nsACString& formattedDate,
+ bool showDateForToday);
+ // The caller is expected to free the result of GetLocalizedDateString
+ char* GetLocalizedDateString(const char* dateString);
+};
+
+#endif /* _nsMimeBaseEmitter_h_ */
diff --git a/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.cpp b/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.cpp
new file mode 100644
index 0000000000..81537c6446
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.cpp
@@ -0,0 +1,492 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include <stdio.h>
+#include "nsMimeHtmlEmitter.h"
+#include "plstr.h"
+#include "nsMailHeaders.h"
+#include "nscore.h"
+#include "nsEmitterUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIMimeStreamConverter.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsMimeTypes.h"
+#include "prtime.h"
+#include "prprf.h"
+#include "nsStringEnumerator.h"
+#include "nsServiceManagerUtils.h"
+// hack: include this to fix opening news attachments.
+#include "nsINntpUrl.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsMemory.h"
+#include "mozilla/Components.h"
+#include "nsIMailChannel.h"
+#include "nsIProgressEventSink.h"
+
+#define VIEW_ALL_HEADERS 2
+
+/**
+ * A helper class to implement nsIUTF8StringEnumerator
+ */
+
+class nsMimeStringEnumerator final : public nsStringEnumeratorBase {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUTF8STRINGENUMERATOR
+
+ nsMimeStringEnumerator() : mCurrentIndex(0) {}
+
+ template <class T>
+ nsCString* Append(T value) {
+ return mValues.AppendElement(value);
+ }
+
+ using nsStringEnumeratorBase::GetNext;
+
+ protected:
+ ~nsMimeStringEnumerator() {}
+ nsTArray<nsCString> mValues;
+ uint32_t mCurrentIndex; // consumers expect first-in first-out enumeration
+};
+
+NS_IMPL_ISUPPORTS(nsMimeStringEnumerator, nsIUTF8StringEnumerator,
+ nsIStringEnumerator)
+
+NS_IMETHODIMP
+nsMimeStringEnumerator::HasMore(bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = mCurrentIndex < mValues.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimeStringEnumerator::GetNext(nsACString& result) {
+ if (mCurrentIndex >= mValues.Length()) return NS_ERROR_UNEXPECTED;
+
+ result = mValues[mCurrentIndex++];
+ return NS_OK;
+}
+
+/*
+ * nsMimeHtmlEmitter definitions....
+ */
+nsMimeHtmlDisplayEmitter::nsMimeHtmlDisplayEmitter() : nsMimeBaseEmitter() {
+ mFirst = true;
+ mSkipAttachment = false;
+}
+
+nsMimeHtmlDisplayEmitter::~nsMimeHtmlDisplayEmitter(void) {}
+
+nsresult nsMimeHtmlDisplayEmitter::Init() { return NS_OK; }
+
+nsresult nsMimeHtmlDisplayEmitter::WriteHeaderFieldHTMLPrefix(
+ const nsACString& name) {
+ if ((mFormat == nsMimeOutput::nsMimeMessageSaveAs) ||
+ (mFormat == nsMimeOutput::nsMimeMessagePrintOutput) ||
+ (mFormat == nsMimeOutput::nsMimeMessageBodyDisplay))
+ return nsMimeBaseEmitter::WriteHeaderFieldHTMLPrefix(name);
+ else
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::WriteHeaderFieldHTML(const char* field,
+ const char* value) {
+ if ((mFormat == nsMimeOutput::nsMimeMessageSaveAs) ||
+ (mFormat == nsMimeOutput::nsMimeMessagePrintOutput) ||
+ (mFormat == nsMimeOutput::nsMimeMessageBodyDisplay))
+ return nsMimeBaseEmitter::WriteHeaderFieldHTML(field, value);
+ else
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::WriteHeaderFieldHTMLPostfix() {
+ if ((mFormat == nsMimeOutput::nsMimeMessageSaveAs) ||
+ (mFormat == nsMimeOutput::nsMimeMessagePrintOutput) ||
+ (mFormat == nsMimeOutput::nsMimeMessageBodyDisplay))
+ return nsMimeBaseEmitter::WriteHeaderFieldHTMLPostfix();
+ else
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::BroadcastHeaders(int32_t aHeaderMode) {
+ nsresult rv;
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString extraExpandedHeaders;
+ nsTArray<nsCString> extraExpandedHeadersArray;
+ nsCString extraAddonHeaders;
+ nsTArray<nsCString> extraAddonHeadersArray;
+ nsAutoCString convertedDateString;
+ bool pushAllHeaders = false;
+ bool checkExtraHeaders = false;
+ bool checkAddonHeaders = false;
+ nsCString otherHeaders;
+ nsTArray<nsCString> otherHeadersArray;
+ bool checkOtherHeaders = false;
+
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (pPrefBranch) {
+ pPrefBranch->GetCharPref("mailnews.headers.extraExpandedHeaders",
+ extraExpandedHeaders);
+ if (!extraExpandedHeaders.IsEmpty()) {
+ ToLowerCase(extraExpandedHeaders);
+ ParseString(extraExpandedHeaders, ' ', extraExpandedHeadersArray);
+ checkExtraHeaders = true;
+ }
+
+ pPrefBranch->GetCharPref("mailnews.headers.extraAddonHeaders",
+ extraAddonHeaders);
+ if (!extraAddonHeaders.IsEmpty()) {
+ // Push all headers if extraAddonHeaders is "*".
+ if (extraAddonHeaders.EqualsLiteral("*")) {
+ pushAllHeaders = true;
+ } else {
+ ToLowerCase(extraAddonHeaders);
+ ParseString(extraAddonHeaders, ' ', extraAddonHeadersArray);
+ checkAddonHeaders = true;
+ }
+ }
+
+ pPrefBranch->GetCharPref("mail.compose.other.header", otherHeaders);
+ if (!otherHeaders.IsEmpty()) {
+ ToLowerCase(otherHeaders);
+ ParseString(otherHeaders, ',', otherHeadersArray);
+ for (uint32_t i = 0; i < otherHeadersArray.Length(); i++) {
+ otherHeadersArray[i].Trim(" ");
+ }
+
+ checkOtherHeaders = true;
+ }
+ }
+
+ for (size_t i = 0; i < mHeaderArray->Length(); i++) {
+ headerInfoType* headerInfo = mHeaderArray->ElementAt(i);
+ if ((!headerInfo) || (!headerInfo->name) || (!(*headerInfo->name)) ||
+ (!headerInfo->value) || (!(*headerInfo->value)))
+ continue;
+
+ // optimization: if we aren't in view all header view mode, we only show a
+ // small set of the total # of headers. don't waste time sending those out
+ // to the UI since the UI is going to ignore them anyway.
+ if (aHeaderMode != VIEW_ALL_HEADERS &&
+ (mFormat != nsMimeOutput::nsMimeMessageFilterSniffer)) {
+ bool skip = true;
+ const char* headerName = headerInfo->name;
+ if (pushAllHeaders) {
+ skip = false;
+
+ // Accept the following:
+ } else if (!PL_strcasecmp("to", headerName) ||
+ !PL_strcasecmp("from", headerName) ||
+ !PL_strcasecmp("cc", headerName) ||
+ !PL_strcasecmp("newsgroups", headerName) ||
+ !PL_strcasecmp("bcc", headerName) ||
+ !PL_strcasecmp("followup-to", headerName) ||
+ !PL_strcasecmp("reply-to", headerName) ||
+ !PL_strcasecmp("subject", headerName) ||
+ !PL_strcasecmp("organization", headerName) ||
+ !PL_strcasecmp("user-agent", headerName) ||
+ !PL_strcasecmp("content-base", headerName) ||
+ !PL_strcasecmp("sender", headerName) ||
+ !PL_strcasecmp("date", headerName) ||
+ !PL_strcasecmp("x-mailer", headerName) ||
+ !PL_strcasecmp("content-type", headerName) ||
+ !PL_strcasecmp("message-id", headerName) ||
+ !PL_strcasecmp("x-newsreader", headerName) ||
+ !PL_strcasecmp("x-mimeole", headerName) ||
+ !PL_strcasecmp("references", headerName) ||
+ !PL_strcasecmp("in-reply-to", headerName) ||
+ !PL_strcasecmp("list-post", headerName) ||
+ !PL_strcasecmp("delivered-to", headerName)) {
+ skip = false;
+
+ } else if (checkExtraHeaders || checkAddonHeaders || checkOtherHeaders) {
+ // Make headerStr lowercase because
+ // extraExpandedHeaders/extraAddonHeadersArray was made lowercase above.
+ nsDependentCString headerStr(headerInfo->name);
+ ToLowerCase(headerStr);
+ // Accept if it's an "extra" header.
+ if (checkExtraHeaders && extraExpandedHeadersArray.Contains(headerStr))
+ skip = false;
+ if (checkAddonHeaders && extraAddonHeadersArray.Contains(headerStr))
+ skip = false;
+ if (checkOtherHeaders && otherHeadersArray.Contains(headerStr))
+ skip = false;
+ }
+
+ if (skip) continue;
+ }
+
+ const char* headerValue = headerInfo->value;
+ mailChannel->AddHeaderFromMIME(nsCString(headerInfo->name),
+ nsCString(headerValue));
+
+ // Add a localized version of the date header if we encounter it.
+ if (!PL_strcasecmp("Date", headerInfo->name)) {
+ GenerateDateString(headerValue, convertedDateString, false);
+ mailChannel->AddHeaderFromMIME("X-Mozilla-LocalizedDate"_ns,
+ convertedDateString);
+ }
+ }
+
+ // Notify the front end that the headers are ready on `mailChannel`.
+ nsCOMPtr<nsIMailProgressListener> listener;
+ mailChannel->GetListener(getter_AddRefs(listener));
+ if (listener) {
+ listener->OnHeadersComplete(mailChannel);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMimeHtmlDisplayEmitter::WriteHTMLHeaders(
+ const nsACString& name) {
+ if ((mFormat == nsMimeOutput::nsMimeMessageSaveAs) ||
+ (mFormat == nsMimeOutput::nsMimeMessagePrintOutput) ||
+ (mFormat == nsMimeOutput::nsMimeMessageBodyDisplay)) {
+ nsMimeBaseEmitter::WriteHTMLHeaders(name);
+ }
+
+ if (!mDocHeader) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ int32_t viewMode = 0;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv) && pPrefBranch) {
+ pPrefBranch->GetIntPref("mail.show_headers", &viewMode);
+ }
+
+ return BroadcastHeaders(viewMode);
+}
+
+nsresult nsMimeHtmlDisplayEmitter::EndHeader(const nsACString& name) {
+ if (mDocHeader && (mFormat != nsMimeOutput::nsMimeMessageFilterSniffer)) {
+ // Start with a UTF-8 BOM so this can't be mistaken for another charset.
+ UtilityWriteCRLF("\xEF\xBB\xBF<!DOCTYPE html>");
+ UtilityWriteCRLF("<html>");
+ UtilityWriteCRLF("<head>");
+
+ const char* val = GetHeaderValue(HEADER_SUBJECT); // do not free this value
+ if (val) {
+ nsCString subject("<title>");
+ nsAppendEscapedHTML(nsDependentCString(val), subject);
+ subject.AppendLiteral("</title>");
+ UtilityWriteCRLF(subject.get());
+ }
+
+ // Stylesheet info!
+ UtilityWriteCRLF(
+ "<link rel=\"important stylesheet\" "
+ "href=\"chrome://messagebody/skin/messageBody.css\">");
+
+ UtilityWriteCRLF("</head>");
+ UtilityWriteCRLF("<body>");
+ }
+
+ WriteHTMLHeaders(name);
+
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::StartAttachment(const nsACString& name,
+ const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) {
+ nsresult rv = NS_OK;
+
+ nsCString uriString;
+
+ nsCOMPtr<nsIMsgMessageUrl> msgurl(do_QueryInterface(mURL, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ // HACK: news urls require us to use the originalSpec. Everyone
+ // else uses GetURI to get the RDF resource which describes the message.
+ nsCOMPtr<nsINntpUrl> nntpUrl(do_QueryInterface(mURL, &rv));
+ if (NS_SUCCEEDED(rv) && nntpUrl)
+ rv = msgurl->GetOriginalSpec(uriString);
+ else
+ rv = msgurl->GetUri(uriString);
+ }
+
+ // The attachment name has already been RFC2047 processed
+ // upstream of us. (Namely, mime_decode_filename has been called, deferring
+ // to nsIMimeHeaderParam.decodeParameter.)
+ // But we'l send it through decoding ourselves as well, since we do some
+ // more adjustments, such as removing spoofy chars.
+
+ nsCString decodedName(name);
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ mimeConverter->DecodeMimeHeaderToUTF8(name, nullptr, false, true,
+ decodedName);
+ }
+
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(mChannel);
+ if (mailChannel) {
+ mailChannel->HandleAttachmentFromMIME(nsDependentCString(contentType),
+ nsDependentCString(url), decodedName,
+ uriString, aIsExternalAttachment);
+ }
+
+ // List the attachments for printing.
+ rv = StartAttachmentInBody(decodedName, contentType, url);
+
+ return rv;
+}
+
+// Attachment handling routines
+
+nsresult nsMimeHtmlDisplayEmitter::StartAttachmentInBody(
+ const nsACString& name, const char* contentType, const char* url) {
+ mSkipAttachment = false;
+ bool p7mExternal = false;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) prefs->GetBoolPref("mailnews.p7m_external", &p7mExternal);
+
+ if ((contentType) &&
+ ((!p7mExternal && !strcmp(contentType, APPLICATION_XPKCS7_MIME)) ||
+ (!p7mExternal && !strcmp(contentType, APPLICATION_PKCS7_MIME)) ||
+ (!strcmp(contentType, APPLICATION_XPKCS7_SIGNATURE)) ||
+ (!strcmp(contentType, APPLICATION_PKCS7_SIGNATURE)))) {
+ mSkipAttachment = true;
+ return NS_OK;
+ }
+
+ // Add the list of attachments. This is only visible when printing.
+
+ if (mFirst) {
+ UtilityWrite(
+ "<fieldset class=\"moz-mime-attachment-header moz-print-only\">");
+ if (!name.IsEmpty()) {
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleSvc->CreateBundle(
+ "chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString attachmentsHeader;
+ bundle->GetStringFromName("attachmentsPrintHeader", attachmentsHeader);
+
+ UtilityWrite(
+ "<legend class=\"moz-mime-attachment-headerName moz-print-only\">");
+ nsCString escapedName;
+ nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(attachmentsHeader),
+ escapedName);
+ UtilityWrite(escapedName.get());
+ UtilityWrite("</legend>");
+ }
+ UtilityWrite("</fieldset>");
+ UtilityWrite("<div class=\"moz-mime-attachment-wrap moz-print-only\">");
+ UtilityWrite("<table class=\"moz-mime-attachment-table\">");
+ }
+
+ UtilityWrite("<tr>");
+
+ UtilityWrite("<td class=\"moz-mime-attachment-file\">");
+ nsCString escapedName;
+ nsAppendEscapedHTML(name, escapedName);
+ UtilityWrite(escapedName.get());
+ UtilityWrite("</td>");
+
+ mFirst = false;
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::AddAttachmentField(const char* field,
+ const char* value) {
+ if (mSkipAttachment) return NS_OK;
+
+ // Don't let bad things happen
+ if (!value || !*value) return NS_OK;
+
+ // Don't output this ugly header...
+ if (!strcmp(field, HEADER_X_MOZILLA_PART_URL)) return NS_OK;
+
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(mChannel);
+ if (mailChannel) {
+ mailChannel->AddAttachmentFieldFromMIME(nsDependentCString(field),
+ nsDependentCString(value));
+ }
+
+ // Currently, we only care about the part size.
+ if (strcmp(field, HEADER_X_MOZILLA_PART_SIZE)) return NS_OK;
+
+ uint64_t size = atoi(value);
+ nsAutoString sizeString;
+ FormatFileSize(size, false, sizeString);
+ UtilityWrite("<td class=\"moz-mime-attachment-size\">");
+ UtilityWrite(NS_ConvertUTF16toUTF8(sizeString).get());
+ UtilityWrite("</td>");
+
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::EndAttachment() {
+ if (!mSkipAttachment) {
+ UtilityWrite("</tr>");
+ }
+
+ mSkipAttachment = false; // reset it for next attachment round
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::EndAllAttachments() {
+ UtilityWrite("</table>");
+ UtilityWrite("</div>");
+
+ // Notify the front end that we've finished reading the body.
+ nsresult rv;
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMailProgressListener> listener;
+ mailChannel->GetListener(getter_AddRefs(listener));
+ if (listener) {
+ listener->OnAttachmentsComplete(mailChannel);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::WriteBody(const nsACString& buf,
+ uint32_t* amountWritten) {
+ Write(buf, amountWritten);
+ return NS_OK;
+}
+
+nsresult nsMimeHtmlDisplayEmitter::EndBody() {
+ if (mFormat != nsMimeOutput::nsMimeMessageFilterSniffer) {
+ UtilityWriteCRLF("</body>");
+ UtilityWriteCRLF("</html>");
+ }
+
+ // Notify the front end that we've finished reading the body.
+ nsresult rv;
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMailProgressListener> listener;
+ mailChannel->GetListener(getter_AddRefs(listener));
+ if (listener) {
+ listener->OnBodyComplete(mailChannel);
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.h b/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.h
new file mode 100644
index 0000000000..818a5179dd
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMimeHtmlEmitter_h_
+#define _nsMimeHtmlEmitter_h_
+
+#include "mozilla/Attributes.h"
+#include "prio.h"
+#include "nsMimeBaseEmitter.h"
+
+class nsMimeHtmlDisplayEmitter : public nsMimeBaseEmitter {
+ public:
+ nsMimeHtmlDisplayEmitter();
+ nsresult Init();
+
+ virtual ~nsMimeHtmlDisplayEmitter(void);
+
+ // Header handling routines.
+ NS_IMETHOD EndHeader(const nsACString& name) override;
+
+ // Attachment handling routines
+ NS_IMETHOD StartAttachment(const nsACString& name, const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) override;
+ NS_IMETHOD AddAttachmentField(const char* field, const char* value) override;
+ NS_IMETHOD EndAttachment() override;
+ NS_IMETHOD EndAllAttachments() override;
+
+ // Body handling routines
+ NS_IMETHOD WriteBody(const nsACString& buf, uint32_t* amountWritten) override;
+ NS_IMETHOD EndBody() override;
+ NS_IMETHOD WriteHTMLHeaders(const nsACString& name) override;
+
+ virtual nsresult WriteHeaderFieldHTMLPrefix(const nsACString& name) override;
+ virtual nsresult WriteHeaderFieldHTML(const char* field,
+ const char* value) override;
+ virtual nsresult WriteHeaderFieldHTMLPostfix() override;
+
+ protected:
+ bool mFirst; // Attachment flag...
+ bool mSkipAttachment; // attachments we shouldn't show...
+
+ nsresult StartAttachmentInBody(const nsACString& name,
+ const char* contentType, const char* url);
+
+ nsresult BroadcastHeaders(int32_t aHeaderMode);
+};
+
+#endif /* _nsMimeHtmlEmitter_h_ */
diff --git a/comm/mailnews/mime/emitters/nsMimePlainEmitter.cpp b/comm/mailnews/mime/emitters/nsMimePlainEmitter.cpp
new file mode 100644
index 0000000000..1a74e10417
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimePlainEmitter.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <stdio.h>
+#include "nsMimePlainEmitter.h"
+#include "plstr.h"
+#include "nsMailHeaders.h"
+#include "nscore.h"
+#include "prmem.h"
+#include "nsEmitterUtils.h"
+#include "nsCOMPtr.h"
+#include "nsUnicharUtils.h"
+
+/*
+ * nsMimePlainEmitter definitions....
+ */
+nsMimePlainEmitter::nsMimePlainEmitter() {}
+
+nsMimePlainEmitter::~nsMimePlainEmitter(void) {}
+
+// Header handling routines.
+nsresult nsMimePlainEmitter::StartHeader(bool rootMailHeader, bool headerOnly,
+ const char* msgID,
+ const char* outCharset) {
+ mDocHeader = rootMailHeader;
+ return NS_OK;
+}
+
+nsresult nsMimePlainEmitter::AddHeaderField(const char* field,
+ const char* value) {
+ if ((!field) || (!value)) return NS_OK;
+
+ UtilityWrite(field);
+ UtilityWrite(":\t");
+ UtilityWriteCRLF(value);
+ return NS_OK;
+}
+
+nsresult nsMimePlainEmitter::EndHeader(const nsACString& name) {
+ UtilityWriteCRLF("");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMimePlainEmitter::WriteBody(const nsACString& buf, uint32_t* amountWritten) {
+ Write(buf, amountWritten);
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/emitters/nsMimePlainEmitter.h b/comm/mailnews/mime/emitters/nsMimePlainEmitter.h
new file mode 100644
index 0000000000..e3447f29ba
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimePlainEmitter.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMimePlainEmitter_h_
+#define _nsMimePlainEmitter_h_
+
+#include "mozilla/Attributes.h"
+#include "prio.h"
+#include "nsMimeBaseEmitter.h"
+
+class nsMimePlainEmitter : public nsMimeBaseEmitter {
+ public:
+ nsMimePlainEmitter();
+ virtual ~nsMimePlainEmitter(void);
+
+ // Header handling routines.
+ NS_IMETHOD StartHeader(bool rootMailHeader, bool headerOnly,
+ const char* msgID, const char* outCharset) override;
+ NS_IMETHOD AddHeaderField(const char* field, const char* value) override;
+ NS_IMETHOD EndHeader(const nsACString& buf) override;
+
+ NS_IMETHOD WriteBody(const nsACString& buf, uint32_t* amountWritten) override;
+};
+
+#endif /* _nsMimePlainEmitter_h_ */
diff --git a/comm/mailnews/mime/emitters/nsMimeRawEmitter.cpp b/comm/mailnews/mime/emitters/nsMimeRawEmitter.cpp
new file mode 100644
index 0000000000..7f7115017d
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeRawEmitter.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include <stdio.h>
+#include "nsMimeRawEmitter.h"
+#include "plstr.h"
+#include "nscore.h"
+#include "prmem.h"
+
+/*
+ * nsMimeRawEmitter definitions....
+ */
+nsMimeRawEmitter::nsMimeRawEmitter() {}
+
+nsMimeRawEmitter::~nsMimeRawEmitter(void) {}
+
+NS_IMETHODIMP
+nsMimeRawEmitter::WriteBody(const nsACString& buf, uint32_t* amountWritten) {
+ Write(buf, amountWritten);
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/emitters/nsMimeRawEmitter.h b/comm/mailnews/mime/emitters/nsMimeRawEmitter.h
new file mode 100644
index 0000000000..3b6fdf2276
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeRawEmitter.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMimeRawEmitter_h_
+#define _nsMimeRawEmitter_h_
+
+#include "mozilla/Attributes.h"
+#include "prio.h"
+#include "nsMimeBaseEmitter.h"
+
+class nsMimeRawEmitter : public nsMimeBaseEmitter {
+ public:
+ nsMimeRawEmitter();
+ virtual ~nsMimeRawEmitter(void);
+
+ NS_IMETHOD WriteBody(const nsACString& buf, uint32_t* amountWritten) override;
+
+ protected:
+};
+
+#endif /* _nsMimeRawEmitter_h_ */
diff --git a/comm/mailnews/mime/emitters/nsMimeRebuffer.cpp b/comm/mailnews/mime/emitters/nsMimeRebuffer.cpp
new file mode 100644
index 0000000000..136ad7553d
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeRebuffer.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <string.h>
+#include "nsMimeRebuffer.h"
+#include "prmem.h"
+
+MimeRebuffer::MimeRebuffer(void) {}
+
+MimeRebuffer::~MimeRebuffer(void) {}
+
+uint32_t MimeRebuffer::GetSize() { return mBuf.Length(); }
+
+uint32_t MimeRebuffer::IncreaseBuffer(const nsACString& addBuf) {
+ mBuf.Append(addBuf);
+ return mBuf.Length();
+}
+
+uint32_t MimeRebuffer::ReduceBuffer(uint32_t numBytes) {
+ if (numBytes == 0) return mBuf.Length();
+
+ if (numBytes >= mBuf.Length()) {
+ mBuf.Truncate();
+ return 0;
+ }
+
+ mBuf.Cut(0, numBytes);
+ return mBuf.Length();
+}
+
+nsACString& MimeRebuffer::GetBuffer() { return mBuf; }
diff --git a/comm/mailnews/mime/emitters/nsMimeRebuffer.h b/comm/mailnews/mime/emitters/nsMimeRebuffer.h
new file mode 100644
index 0000000000..8f22c35961
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeRebuffer.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _rebuffer_h_
+#define _rebuffer_h_
+
+#include <stdint.h>
+#include "nsString.h"
+
+//////////////////////////////////////////////////////////////
+// A rebuffering class necessary for stream output buffering
+//////////////////////////////////////////////////////////////
+
+class MimeRebuffer {
+ public:
+ MimeRebuffer(void);
+ virtual ~MimeRebuffer(void);
+
+ uint32_t GetSize();
+ uint32_t IncreaseBuffer(const nsACString& addBuf);
+ uint32_t ReduceBuffer(uint32_t numBytes);
+ nsACString& GetBuffer();
+
+ protected:
+ nsCString mBuf;
+};
+
+#endif /* _rebuffer_h_ */
diff --git a/comm/mailnews/mime/emitters/nsMimeXmlEmitter.cpp b/comm/mailnews/mime/emitters/nsMimeXmlEmitter.cpp
new file mode 100644
index 0000000000..30c2b5eac3
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeXmlEmitter.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <stdio.h>
+#include "nsMimeXmlEmitter.h"
+#include "plstr.h"
+#include "nsMailHeaders.h"
+#include "nscore.h"
+#include "prmem.h"
+#include "nsEmitterUtils.h"
+#include "nsCOMPtr.h"
+#include "nsUnicharUtils.h"
+#include "nsMsgUtils.h"
+
+/*
+ * nsMimeXmlEmitter definitions....
+ */
+nsMimeXmlEmitter::nsMimeXmlEmitter() {}
+
+nsMimeXmlEmitter::~nsMimeXmlEmitter(void) {}
+
+// Note - this is teardown only...you should not write
+// anything to the stream since these may be image data
+// output streams, etc...
+nsresult nsMimeXmlEmitter::Complete() {
+ char buf[16];
+
+ // Now write out the total count of attachments for this message
+ UtilityWrite("<mailattachcount>");
+ sprintf(buf, "%d", mAttachCount);
+ UtilityWrite(buf);
+ UtilityWrite("</mailattachcount>");
+
+ UtilityWrite("</message>");
+
+ return nsMimeBaseEmitter::Complete();
+}
+
+nsresult nsMimeXmlEmitter::WriteXMLHeader(const char* msgID) {
+ if ((!msgID) || (!*msgID)) msgID = "none";
+
+ nsCString newValue;
+ nsAppendEscapedHTML(nsDependentCString(msgID), newValue);
+
+ UtilityWrite("<?xml version=\"1.0\"?>");
+
+ UtilityWriteCRLF(
+ "<?xml-stylesheet href=\"chrome://messagebody/skin/messageBody.css\" "
+ "type=\"text/css\"?>");
+
+ UtilityWrite("<message id=\"");
+ UtilityWrite(newValue.get());
+ UtilityWrite("\">");
+
+ mXMLHeaderStarted = true;
+ return NS_OK;
+}
+
+nsresult nsMimeXmlEmitter::WriteXMLTag(const char* tagName, const char* value) {
+ if ((!value) || (!*value)) return NS_OK;
+
+ char* upCaseTag = NULL;
+ nsCString newValue;
+ nsAppendEscapedHTML(nsDependentCString(value), newValue);
+
+ nsCString newTagName(tagName);
+ newTagName.StripWhitespace();
+ ToUpperCase(newTagName);
+ upCaseTag = ToNewCString(newTagName);
+
+ UtilityWrite("<header field=\"");
+ UtilityWrite(upCaseTag);
+ UtilityWrite("\">");
+
+ // Here is where we are going to try to L10N the tagName so we will always
+ // get a field name next to an emitted header value. Note: Default will always
+ // be the name of the header itself.
+ //
+ UtilityWrite("<headerdisplayname>");
+ char* l10nTagName = LocalizeHeaderName(upCaseTag, tagName);
+ if ((!l10nTagName) || (!*l10nTagName))
+ UtilityWrite(tagName);
+ else {
+ UtilityWrite(l10nTagName);
+ }
+ PR_FREEIF(l10nTagName);
+
+ UtilityWrite(": ");
+ UtilityWrite("</headerdisplayname>");
+
+ // Now write out the actual value itself and move on!
+ //
+ UtilityWrite(newValue.get());
+ UtilityWrite("</header>");
+
+ free(upCaseTag);
+
+ return NS_OK;
+}
+
+// Header handling routines.
+nsresult nsMimeXmlEmitter::StartHeader(bool rootMailHeader, bool headerOnly,
+ const char* msgID,
+ const char* outCharset) {
+ mDocHeader = rootMailHeader;
+ WriteXMLHeader(msgID);
+ UtilityWrite("<mailheader>");
+
+ return NS_OK;
+}
+
+nsresult nsMimeXmlEmitter::AddHeaderField(const char* field,
+ const char* value) {
+ if ((!field) || (!value)) return NS_OK;
+
+ WriteXMLTag(field, value);
+ return NS_OK;
+}
+
+nsresult nsMimeXmlEmitter::EndHeader(const nsACString& name) {
+ UtilityWrite("</mailheader>");
+ return NS_OK;
+}
+
+// Attachment handling routines
+nsresult nsMimeXmlEmitter::StartAttachment(const nsACString& name,
+ const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) {
+ char buf[128];
+
+ ++mAttachCount;
+
+ sprintf(buf, "<mailattachment id=\"%d\">", mAttachCount);
+ UtilityWrite(buf);
+
+ AddAttachmentField(HEADER_PARM_FILENAME, PromiseFlatCString(name).get());
+ return NS_OK;
+}
+
+nsresult nsMimeXmlEmitter::AddAttachmentField(const char* field,
+ const char* value) {
+ WriteXMLTag(field, value);
+ return NS_OK;
+}
+
+nsresult nsMimeXmlEmitter::EndAttachment() {
+ UtilityWrite("</mailattachment>");
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/emitters/nsMimeXmlEmitter.h b/comm/mailnews/mime/emitters/nsMimeXmlEmitter.h
new file mode 100644
index 0000000000..8c6f2d29ba
--- /dev/null
+++ b/comm/mailnews/mime/emitters/nsMimeXmlEmitter.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _nsMimeXmlEmitter_h_
+#define _nsMimeXmlEmitter_h_
+
+#include "mozilla/Attributes.h"
+#include "prio.h"
+#include "nsMimeBaseEmitter.h"
+
+class nsMimeXmlEmitter : public nsMimeBaseEmitter {
+ public:
+ nsMimeXmlEmitter();
+ virtual ~nsMimeXmlEmitter(void);
+
+ NS_IMETHOD Complete() override;
+
+ // Header handling routines.
+ NS_IMETHOD StartHeader(bool rootMailHeader, bool headerOnly,
+ const char* msgID, const char* outCharset) override;
+ NS_IMETHOD AddHeaderField(const char* field, const char* value) override;
+ NS_IMETHOD EndHeader(const nsACString& buf) override;
+
+ // Attachment handling routines
+ NS_IMETHOD StartAttachment(const nsACString& name, const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) override;
+ NS_IMETHOD AddAttachmentField(const char* field, const char* value) override;
+ NS_IMETHOD EndAttachment() override;
+
+ NS_IMETHOD WriteXMLHeader(const char* msgID);
+ NS_IMETHOD WriteXMLTag(const char* tagName, const char* value);
+
+ protected:
+ // For header determination...
+ bool mXMLHeaderStarted;
+ int32_t mAttachCount;
+};
+
+#endif /* _nsMimeXmlEmitter_h_ */
diff --git a/comm/mailnews/mime/jsmime/LICENSE b/comm/mailnews/mime/jsmime/LICENSE
new file mode 100644
index 0000000000..9ddc547d9c
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 Joshua Cranmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/comm/mailnews/mime/jsmime/README.md b/comm/mailnews/mime/jsmime/README.md
new file mode 100644
index 0000000000..a418516ea2
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/README.md
@@ -0,0 +1,59 @@
+Code Layout
+===========
+
+JSMime is a MIME parsing and composition library that is written completely in
+JavaScript using ES6 functionality and WebAPIs (where such APIs exist). There
+are a few features for which a standardized WebAPI does not exist; for these,
+external JavaScript libraries are used.
+
+The MIME parser consists of three logical phases of translation:
+
+1. Build the MIME (and pseudo-MIME) tree.
+2. Convert the MIME tree into a list of body parts and attachments.
+3. Use the result to drive a displayed version of the message.
+
+The first stage is located in `mimeparser.js`. The latter stages have yet to be
+implemented.
+
+Dependencies
+============
+
+This code depends on the following ES6 features and Web APIs:
+* ES6 generators
+* ES6 Map and Set
+* ES6 @@iterator support (especially for Map and Set)
+* ES6 let
+* ES6 let-destructuring
+* ES6 const
+* Typed arrays (predominantly Uint8Array)
+* btoa, atob (found on global Windows or WorkerScopes)
+* TextDecoder
+
+Versions and API stability
+==========================
+
+As APIs require some use and experimentation to get a feel for what works best,
+the APIs may change between successive version updates as uses indicate
+substandard or error-prone APIs. Therefore, there will be no guarantee of API
+stability until version 1.0 is released.
+
+This code is being initially developed as an effort to replace the MIME library
+within Thunderbird. New versions will be released as needed to bring new support
+into the Thunderbird codebase; version 1.0 will correspond to the version where
+feature-parity with the old MIME library is reached. The set of features which
+will be added before 1.0 are the following:
+* S/MIME encryption and decryption
+* PGP encryption and decryption
+* IMAP parts-on-demand support
+* Support for text/plain to HTML conversion for display
+* Support for HTML downgrading and sanitization for display
+* Support for all major multipart types
+* Ability to convert HTML documents to text/plain and multipart/related
+* Support for building outgoing messages
+* Support for IDN and EAI
+* yEnc and uuencode decoding support
+* Support for date and Message-ID/References-like headers
+
+Other features than these may be added before version 1.0 is released (most
+notably TNEF decoding support), but they are not considered necessary to release
+a version 1.0.
diff --git a/comm/mailnews/mime/jsmime/jsmime.js b/comm/mailnews/mime/jsmime/jsmime.js
new file mode 100644
index 0000000000..28308daf4f
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/jsmime.js
@@ -0,0 +1,3682 @@
+/* import-globals-from ../src/jsmime.jsm */
+/* globals define, module */
+
+(function (root, fn) {
+ if (typeof define === "function" && define.amd) {
+ define(fn);
+ } else if (typeof module !== "undefined" && module.exports) {
+ module.exports = fn();
+ } else {
+ root.jsmime = fn();
+ }
+})(this, function () {
+ var mods = {};
+ function req(id) {
+ return mods[id.replace(/^\.\//, "")];
+ }
+
+ function def(id, fn) {
+ mods[id] = fn(req);
+ }
+ def("mimeutils", function () {
+ "use strict";
+
+ /**
+ * Decode a quoted-printable buffer into a binary string.
+ *
+ * @param buffer {BinaryString} The string to decode.
+ * @param more {Boolean} This argument is ignored.
+ * @returns {Array(BinaryString, BinaryString)} The first element of the array
+ * is the decoded string. The second element is always the empty
+ * string.
+ */
+ function decode_qp(buffer, more) {
+ // Unlike base64, quoted-printable isn't stateful across multiple lines, so
+ // there is no need to buffer input, so we can always ignore more.
+ let decoded = buffer.replace(
+ // Replace either =<hex><hex> or =<wsp>CRLF
+ /=([0-9A-F][0-9A-F]|[ \t]*(\r\n|[\r\n]|$))/gi,
+ function (match, param) {
+ // If trailing text matches [ \t]*CRLF, drop everything, since it's a
+ // soft line break.
+ if (param.trim().length == 0) {
+ return "";
+ }
+ return String.fromCharCode(parseInt(param, 16));
+ }
+ );
+ return [decoded, ""];
+ }
+
+ /**
+ * Decode a base64 buffer into a binary string. Unlike window.atob, the buffer
+ * may contain non-base64 characters that will be ignored.
+ *
+ * @param buffer {BinaryString} The string to decode.
+ * @param more {Boolean} If true, we expect that this function could be
+ * called again and should retain extra data. If
+ * false, we should flush all pending output.
+ * @returns {Array(BinaryString, BinaryString)} The first element of the array
+ * is the decoded string. The second element contains the data that
+ * could not be decoded and needs to be retained for the next call.
+ */
+ function decode_base64(buffer, more) {
+ // Drop all non-base64 characters
+ let sanitize = buffer.replace(/[^A-Za-z0-9+\/=]/g, "");
+ // Remove harmful `=' chars in the middle.
+ sanitize = sanitize.replace(/=+([A-Za-z0-9+\/])/g, "$1");
+ // We need to encode in groups of 4 chars. If we don't have enough, leave the
+ // excess for later. If there aren't any more, drop enough to make it 4.
+ let excess = sanitize.length % 4;
+ if (excess != 0 && more) {
+ buffer = sanitize.slice(-excess);
+ } else {
+ buffer = "";
+ }
+ sanitize = sanitize.substring(0, sanitize.length - excess);
+ // Delete all unnecessary '====' in padding.
+ sanitize = sanitize.replace(/(====)+$/g, "");
+ // Use the atob function we (ought to) have in global scope.
+ return [atob(sanitize), buffer];
+ }
+
+ /**
+ * Converts a binary string into a Uint8Array buffer.
+ *
+ * @param buffer {BinaryString} The string to convert.
+ * @returns {Uint8Array} The converted data.
+ */
+ function stringToTypedArray(buffer) {
+ var typedarray = new Uint8Array(buffer.length);
+ for (var i = 0; i < buffer.length; i++) {
+ typedarray[i] = buffer.charCodeAt(i);
+ }
+ return typedarray;
+ }
+
+ /**
+ * Converts a Uint8Array buffer to a binary string.
+ *
+ * @param buffer {Uint8Array} The Uint8Array to convert.
+ * @returns {string} The converted string.
+ */
+ function typedArrayToString(buffer) {
+ var string = "";
+ for (let i = 0; i < buffer.length; i += 100) {
+ string += String.fromCharCode.apply(
+ undefined,
+ buffer.subarray(i, i + 100)
+ );
+ }
+ return string;
+ }
+
+ /** A list of month names for Date parsing. */
+ var kMonthNames = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+
+ return {
+ decode_base64,
+ decode_qp,
+ kMonthNames,
+ stringToTypedArray,
+ typedArrayToString,
+ };
+ });
+ /**
+ * This file implements knowledge of how to encode or decode structured headers
+ * for several key headers. It is not meant to be used externally to jsmime.
+ */
+
+ def("structuredHeaders", function (require) {
+ "use strict";
+
+ var structuredDecoders = new Map();
+ var structuredEncoders = new Map();
+ var preferredSpellings = new Map();
+
+ function addHeader(name, decoder, encoder) {
+ var lowerName = name.toLowerCase();
+ structuredDecoders.set(lowerName, decoder);
+ structuredEncoders.set(lowerName, encoder);
+ preferredSpellings.set(lowerName, name);
+ }
+
+ // Addressing headers: We assume that they can be specified in 1* form (this is
+ // false for From, but it's close enough to the truth that it shouldn't matter).
+ // There is no need to specialize the results for the header, so just pun it
+ // back to parseAddressingHeader.
+ function parseAddress(value) {
+ let headerparser = this;
+ return value.reduce(function (results, header) {
+ return results.concat(headerparser.parseAddressingHeader(header, true));
+ }, []);
+ }
+ function writeAddress(value) {
+ // Make sure the input is an array (accept a single entry)
+ if (!Array.isArray(value)) {
+ value = [value];
+ }
+ this.addAddresses(value);
+ }
+
+ // Addressing headers from RFC 5322:
+ addHeader("Bcc", parseAddress, writeAddress);
+ addHeader("Cc", parseAddress, writeAddress);
+ addHeader("From", parseAddress, writeAddress);
+ addHeader("Reply-To", parseAddress, writeAddress);
+ addHeader("Resent-Bcc", parseAddress, writeAddress);
+ addHeader("Resent-Cc", parseAddress, writeAddress);
+ addHeader("Resent-From", parseAddress, writeAddress);
+ addHeader("Resent-Reply-To", parseAddress, writeAddress);
+ addHeader("Resent-Sender", parseAddress, writeAddress);
+ addHeader("Resent-To", parseAddress, writeAddress);
+ addHeader("Sender", parseAddress, writeAddress);
+ addHeader("To", parseAddress, writeAddress);
+ // From RFC 5536:
+ addHeader("Approved", parseAddress, writeAddress);
+ // From RFC 3798:
+ addHeader("Disposition-Notification-To", parseAddress, writeAddress);
+ // Non-standard headers:
+ addHeader("Delivered-To", parseAddress, writeAddress);
+ addHeader("Return-Receipt-To", parseAddress, writeAddress);
+
+ // http://cr.yp.to/proto/replyto.html
+ addHeader("Mail-Reply-To", parseAddress, writeAddress);
+ addHeader("Mail-Followup-To", parseAddress, writeAddress);
+
+ // Parameter-based headers. Note that all parameters are slightly different, so
+ // we use slightly different variants here.
+ function parseParameterHeader(value, do2231, do2047) {
+ // Only use the first header for parameters; ignore subsequent redefinitions.
+ return this.parseParameterHeader(value[0], do2231, do2047);
+ }
+
+ // RFC 2045
+ function parseContentType(value) {
+ let params = parseParameterHeader.call(this, value, false, false);
+ let origtype = params.preSemi;
+ let parts = origtype.split("/");
+ if (parts.length != 2) {
+ // Malformed. Return to text/plain. Evil, ain't it?
+ params = new Map();
+ parts = ["text", "plain"];
+ }
+ let mediatype = parts[0].toLowerCase();
+ let subtype = parts[1].toLowerCase();
+ let type = mediatype + "/" + subtype;
+ let structure = new Map();
+ structure.mediatype = mediatype;
+ structure.subtype = subtype;
+ structure.type = type;
+ params.forEach(function (value, name) {
+ structure.set(name.toLowerCase(), value);
+ });
+ return structure;
+ }
+ structuredDecoders.set("Content-Type", parseContentType);
+
+ // Unstructured headers (just decode RFC 2047 for the first header value)
+ function parseUnstructured(values) {
+ return this.decodeRFC2047Words(values[0]);
+ }
+ function writeUnstructured(value) {
+ this.addUnstructured(value);
+ }
+
+ // Message-ID headers.
+ function parseMessageID(values) {
+ // TODO: Proper parsing support for these headers is currently unsupported).
+ return this.decodeRFC2047Words(values[0]);
+ }
+ function writeMessageID(value) {
+ // TODO: Proper parsing support for these headers is currently unsupported).
+ this.addUnstructured(value);
+ }
+
+ // RFC 5322
+ addHeader("Comments", parseUnstructured, writeUnstructured);
+ addHeader("Keywords", parseUnstructured, writeUnstructured);
+ addHeader("Subject", parseUnstructured, writeUnstructured);
+
+ // RFC 2045
+ addHeader("MIME-Version", parseUnstructured, writeUnstructured);
+ addHeader("Content-Description", parseUnstructured, writeUnstructured);
+
+ // RFC 7231
+ addHeader("User-Agent", parseUnstructured, writeUnstructured);
+
+ // Date headers
+ function parseDate(values) {
+ return this.parseDateHeader(values[0]);
+ }
+ function writeDate(value) {
+ this.addDate(value);
+ }
+
+ // RFC 5322
+ addHeader("Date", parseDate, writeDate);
+ addHeader("Resent-Date", parseDate, writeDate);
+ // RFC 5536
+ addHeader("Expires", parseDate, writeDate);
+ addHeader("Injection-Date", parseDate, writeDate);
+ addHeader("NNTP-Posting-Date", parseDate, writeDate);
+
+ // RFC 5322
+ addHeader("Message-ID", parseMessageID, writeMessageID);
+ addHeader("Resent-Message-ID", parseMessageID, writeMessageID);
+
+ // Miscellaneous headers (those that don't fall under the above schemes):
+
+ // RFC 2047
+ structuredDecoders.set("Content-Transfer-Encoding", function (values) {
+ return values[0].toLowerCase();
+ });
+ structuredEncoders.set("Content-Transfer-Encoding", writeUnstructured);
+
+ // Some clients like outlook.com send non-compliant References headers that
+ // separate values using commas. Also, some clients don't separate References
+ // with spaces, since these are optional according to RFC2822. So here we
+ // preprocess these headers (see bug 1154521 and bug 1197686).
+ function preprocessMessageIDs(values) {
+ let msgId = /<[^>]*>/g;
+ let match,
+ ids = [];
+ while ((match = msgId.exec(values)) !== null) {
+ ids.push(match[0]);
+ }
+ return ids.join(" ");
+ }
+ structuredDecoders.set("References", preprocessMessageIDs);
+ structuredDecoders.set("In-Reply-To", preprocessMessageIDs);
+
+ return Object.freeze({
+ decoders: structuredDecoders,
+ encoders: structuredEncoders,
+ spellings: preferredSpellings,
+ });
+ });
+ def("headerparser", function (require) {
+ /**
+ * This file implements the structured decoding of message header fields. It is
+ * part of the same system as found in mimemimeutils.js, and occasionally makes
+ * references to globals defined in that file or other dependencies thereof. See
+ * documentation in that file for more information about external dependencies.
+ */
+
+ "use strict";
+ var mimeutils = require("./mimeutils");
+
+ /**
+ * This is the API that we ultimately return.
+ *
+ * We define it as a global here, because we need to pass it as a |this|
+ * argument to a few functions.
+ */
+ var headerparser = {};
+
+ /**
+ * Clean up characters that could cause display problems since they
+ * are not displayed.
+ *
+ * @param {string} token - The string to be cleaned.
+ * @returns {string} The cleaned string.
+ */
+ function cleanToken(token) {
+ // Replace problematic characters so we don't get unexpected behavior
+ // down the line. These fall into a few categories:
+ // A) "Separator, space" (Zs),
+ // B) "Mark, Nonspacing" (Mn)
+ // C) "Other, Control" (Cc)
+ // D) "Other, Format" (Cf)
+ // E) "Symbol, Other"
+ // Unfortunately, no support for the needed regexp Unicode property escapes
+ // in our engine. So we need to hand-roll it. Used the regexpu tool for
+ // that: https://mothereff.in/regexpu.
+ // This should be updated regularly, to take into account new additions
+ // to the unicode standard. Last updated July 2019.
+ // For a full list of categories, see http://unicode.org/Public//5.0.0/ucd/UCD.html.
+
+ // -- case A: /\p{Zs}/u
+ // https://www.fileformat.info/info/unicode/category/Zs/list.htm
+ // https://mothereff.in/regexpu#input=/\p{Zs}/u&unicodePropertyEscape=1
+ token = token.replace(
+ /[\xA0\u1680\u2000-\u200A\u202F\u205F\u3000]/g,
+ " "
+ );
+
+ // -- case B: /\p{Mn}/u
+ // https://www.fileformat.info/info/unicode/category/Mn/list.htm
+ // https://mothereff.in/regexpu#input=/\p{Mn}/u&unicodePropertyEscape=1
+ // This is a bit more complicated as some of them could be "real", so we'll
+ // only remove the ones that are known to show as blank.
+ token = token.replace(
+ /[\u034F\u17B4\u17B5\u180B-\u180D\uFE00-\uFE0F]/g,
+ ""
+ );
+ // \uE0100-\uE01EF need to be written using their surrogate code point pairs
+ // until extended Unicode escapes are supported in regexps.
+ // https://www.fileformat.info/info/unicode/char/e0100/index.htm says \uDB40\uDD00.
+ // https://www.fileformat.info/info/unicode/char/e01ef/index.htm says \uDB40\uDDEF.
+ token = token.replace(/\uDB40[\uDD00-\uDDEF]/g, "");
+
+ // -- case C: /\p{Cc}/u, except Tab/LF/CR
+ // https://www.fileformat.info/info/unicode/category/Cc/list.htm
+ // https://mothereff.in/regexpu#input=/\p{Cc}/u&unicodePropertyEscape=1
+ // eslint-disable-next-line no-control-regex
+ token = token.replace(/(?![\t\n\r])[\0-\x1F\x7F-\x9F]/g, "");
+
+ // -- case D: /\p{Cf}/u
+ // https://www.fileformat.info/info/unicode/category/Cf/list.htm
+ // https://mothereff.in/regexpu#input=/\p{Cf}/u&unicodePropertyEscape=1
+ // Remove all of these except for \u0600-\u0605.
+ // XXX: We replace these with spaces (" "), not empty strings ("").
+ // Notably, for zero width space (\u200B) replacing with empty space
+ // would later drop real spaces surrounding it. Dunno why.
+ token = token.replace(
+ /(?:[\xAD\u061C\u06DD\u070F\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC38]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F])/g,
+ " "
+ );
+
+ // -- case E: problematic symbols
+ // https://www.fileformat.info/info/unicode/category/So/list.htm
+ // Replace U+2800 BRAILLE PATTERN BLANK with space.
+ token = token.replace(/\u2800/g, " ");
+
+ return token;
+ }
+
+ /**
+ * Tokenizes a message header into a stream of tokens as a generator.
+ *
+ * The low-level tokens are meant to be loosely correspond to the tokens as
+ * defined in RFC 5322. For reasons of saner error handling, however, the two
+ * definitions are not exactly equivalent. The tokens we emit are the following:
+ * 1. Special delimiters: Any char in the delimiters string is emitted as a
+ * string by itself. Parsing parameter headers, for example, would use ";="
+ * for the delimiter string.
+ * 2. Quoted-strings (if opt.qstring is true): A string which is surrounded by
+ * double quotes. Escapes in the string are omitted when returning.
+ * 3. Domain Literals (if opt.dliteral is true): A string which matches the
+ * dliteral construct in RFC 5322. Escapes here are NOT omitted.
+ * 4. Comments (if opt.comments is true): Comments are handled specially. In
+ * practice, decoding the comments in To headers appears to be necessary, so
+ * comments are not stripped in the output value. Instead, they are emitted
+ * as if they are a special delimiter. However, all delimiters found within a
+ * comment are returned as if they were a quoted string, so that consumers
+ * ignore delimiters within comments. If ignoring comment text completely is
+ * desired, upon seeing a "(" token, consumers should ignore all tokens until
+ * a matching ")" is found (note that comments can be nested).
+ * 5. RFC 2047 encoded-words (if opts.rfc2047 is true): These are strings which
+ * are the decoded contents of RFC 2047's =?UTF-8?Q?blah?=-style words.
+ * 6. Atoms: Atoms are defined not in the RFC 5322 sense, but rather as the
+ * longest sequence of characters that is neither whitespace nor any of the
+ * special characters above.
+ *
+ * The intended interpretation of the stream of output tokens is that they are
+ * the portions of text which can be safely wrapped in whitespace with no ill
+ * effect. The output tokens are either strings (which represent individual
+ * delimiter tokens) or instances of a class that has a customized .toString()
+ * for output (for quoted strings, atoms, domain literals, and encoded-words).
+ * Checking for a delimiter MUST use the strictly equals operator (===). For
+ * example, the proper way to call this method is as follows:
+ *
+ * for (let token of getHeaderTokens(rest, ";=", opts)) {
+ * if (token === ';') {
+ * // This represents a literal ';' in the string
+ * } else if (token === '=') {
+ * // This represents a literal '=' in the string
+ * } else {
+ * // If a ";" qstring was parsed, we fall through to here!
+ * token = token.toString();
+ * }
+ * }
+ *
+ * This method does not properly tokenize 5322 in all corner cases; however,
+ * this is equivalent in those corner cases to an older header parsing
+ * algorithm, so the algorithm should be correct for all real-world cases. The
+ * corner cases are as follows:
+ * 1. Quoted-strings and domain literals are parsed even if they are within a
+ * comment block (we effectively treat ctext as containing qstring).
+ * 2. WSP need not be between a qstring and an atom (a"b" produces two tokens,
+ * a and b). This is an error case, though.
+ * 3. Legacy comments as display names: We recognize address fields with
+ * comments, and (a) either drop them if inside addr-spec or (b) preserve
+ * them as part of the display-name if not. If the display-name is empty
+ * while the last comment is not, we assume it's the legacy form above and
+ * take the comment content as the display-name.
+ *
+ * @param {string} value - The header value, post charset conversion but
+ * before RFC 2047 decoding, to be parsed.
+ * @param {string} delimiters A set of delimiters to include as individual
+ * tokens.
+ * @param {object} opts - A set of options selecting what to parse.
+ * @param {boolean} [opts.qstring] - If true, recognize quoted strings.
+ * @param {boolean} [opts.dliteral] If true, recognize domain literals.
+ * @param {boolean} [opts.comments] If true, recognize comments.
+ * @param {boolean} [opts.rfc2047] - If true, parse and decode RFC 2047
+ * encoded-words.
+ * @returns {(Token | string)[]} An array of Token objects (which have a toString
+ * method returning their value) or String objects
+ * (representing delimiters).
+ */
+ /* eslint-disable complexity */
+ function getHeaderTokens(value, delimiters, opts) {
+ // The array of parsed tokens. This method used to be a generator, but it
+ // appears that generators are poorly optimized in current engines, so it was
+ // converted to not be one.
+ let tokenList = [];
+
+ // Represents a non-delimiter token.
+ function Token(token) {
+ // Unescape all quoted pairs. Any trailing \ is deleted.
+ this.token = token.replace(/\\(.?)/g, "$1");
+ }
+ Token.prototype.toString = function () {
+ return this.token;
+ };
+
+ // The start of the current token (e.g., atoms, strings)
+ let tokenStart = undefined;
+ // The set of whitespace characters, as defined by RFC 5322
+ let wsp = " \t\r\n";
+ // If we are a domain literal ([]) or a quoted string ("), this is set to the
+ // character to look for at the end.
+ let endQuote = undefined;
+ // The current depth of comments, since they can be nested. A value 0 means we
+ // are not in a comment.
+ let commentDepth = 0;
+
+ // Iterate over every character one character at a time.
+ let length = value.length;
+ for (let i = 0; i < length; i++) {
+ let ch = value[i];
+ // If we see a \, no matter what context we are in, ignore the next
+ // character.
+ if (ch == "\\") {
+ i++;
+ continue;
+ }
+
+ // If we are in a qstring or a dliteral, process the character only if it is
+ // what we are looking for to end the quote.
+ if (endQuote !== undefined) {
+ if (ch == endQuote && ch == '"') {
+ // Quoted strings don't include their delimiters.
+ let text = value.slice(tokenStart + 1, i);
+
+ // If RFC 2047 is enabled, always decode the qstring.
+ if (opts.rfc2047) {
+ text = decodeRFC2047Words(text);
+ }
+
+ tokenList.push(new Token(text));
+ endQuote = undefined;
+ tokenStart = undefined;
+ } else if (ch == endQuote && ch == "]") {
+ // Domain literals include their delimiters.
+ tokenList.push(new Token(value.slice(tokenStart, i + 1)));
+ endQuote = undefined;
+ tokenStart = undefined;
+ }
+ // Avoid any further processing.
+ continue;
+ }
+
+ // If we can match the RFC 2047 encoded-word pattern, we need to decode the
+ // entire word or set of words.
+ if (
+ opts.rfc2047 &&
+ ch == "=" &&
+ i + 1 < value.length &&
+ value[i + 1] == "?"
+ ) {
+ // RFC 2047 tokens separated only by whitespace are conceptually part of
+ // the same output token, so we need to decode them all at once.
+ let encodedWordsRE = /([ \t\r\n]*=\?[^?]*\?[BbQq]\?[^?]*\?=)+/;
+ let result = encodedWordsRE.exec(value.slice(i));
+ if (result !== null) {
+ // If we were in the middle of a prior token (i.e., something like
+ // foobar=?UTF-8?Q?blah?=), yield the previous segment as a token.
+ if (tokenStart !== undefined) {
+ tokenList.push(new Token(value.slice(tokenStart, i)));
+ tokenStart = undefined;
+ }
+
+ // Find out how much we need to decode...
+ let encWordsLen = result[0].length;
+ let string = decodeRFC2047Words(
+ value.slice(i, i + encWordsLen),
+ "UTF-8"
+ );
+ // Don't make a new Token variable, since we do not want to unescape the
+ // decoded string.
+ tokenList.push({
+ toString() {
+ return string;
+ },
+ });
+
+ // Skip everything we decoded. The -1 is because we don't want to
+ // include the starting character.
+ i += encWordsLen - 1;
+ continue;
+ }
+
+ // If we are here, then we failed to match the simple 2047 encoded-word
+ // regular expression, despite the fact that it matched the =? at the
+ // beginning. Fall through and treat the text as if we aren't trying to
+ // decode RFC 2047.
+ }
+
+ // If we reach this point, we're not inside of quoted strings, domain
+ // literals, or RFC 2047 encoded-words. This means that the characters we
+ // parse are potential delimiters (unless we're in comments, where
+ // everything starts to go really wonky). Several things could happen,
+ // depending on the kind of character we read and whether or not we were in
+ // the middle of a token. The three values here tell us what we could need
+ // to do at this point:
+ // tokenIsEnding: The current character is not able to be accumulated to an
+ // atom, so we need to flush the atom if there is one.
+ // tokenIsStarting: The current character could begin an atom (or
+ // anything that requires us to mark the starting point), so we need to save
+ // the location.
+ // isSpecial: The current character is a delimiter that needs to be output.
+ let tokenIsEnding = false,
+ tokenIsStarting = false,
+ isSpecial = false;
+ if (wsp.includes(ch)) {
+ // Whitespace ends current tokens, doesn't emit anything.
+ tokenIsEnding = true;
+ } else if (commentDepth == 0 && delimiters.includes(ch)) {
+ // Delimiters end the current token, and need to be output. They do not
+ // apply within comments.
+ tokenIsEnding = true;
+ isSpecial = true;
+ } else if (opts.qstring && ch == '"') {
+ // Quoted strings end the last token and start a new one.
+ tokenIsEnding = true;
+ tokenIsStarting = true;
+ endQuote = ch;
+ } else if (opts.dliteral && ch == "[") {
+ // Domain literals end the last token and start a new one.
+ tokenIsEnding = true;
+ tokenIsStarting = true;
+ endQuote = "]";
+ } else if (opts.comments && ch == "(") {
+ // Comments are nested (oh joy). We only really care for the outer
+ // delimiter, though, which also ends the prior token and needs to be
+ // output if the consumer requests it.
+ commentDepth++;
+ if (commentDepth == 1) {
+ tokenIsEnding = true;
+ isSpecial = true;
+ } else {
+ tokenIsStarting = true;
+ }
+ } else if (opts.comments && ch == ")") {
+ // Comments are nested (oh joy). We only really care for the outer
+ // delimiter, though, which also ends the prior token and needs to be
+ // output if the consumer requests it.
+ if (commentDepth > 0) {
+ commentDepth--;
+ }
+ if (commentDepth == 0) {
+ tokenIsEnding = true;
+ isSpecial = true;
+ } else {
+ tokenIsStarting = true;
+ }
+ } else {
+ // Not a delimiter, whitespace, comment, domain literal, or quoted string.
+ // Must be part of an atom then!
+ tokenIsStarting = true;
+ }
+
+ // If our analysis concluded that we closed an open token, and there is an
+ // open token, then yield that token.
+ if (tokenIsEnding && tokenStart !== undefined) {
+ tokenList.push(new Token(value.slice(tokenStart, i)));
+ tokenStart = undefined;
+ }
+ // If we need to output a delimiter, do so.
+ if (isSpecial) {
+ tokenList.push(ch);
+ }
+ // If our analysis concluded that we could open a token, and no token is
+ // opened yet, then start the token.
+ if (tokenIsStarting && tokenStart === undefined) {
+ tokenStart = i;
+ }
+ }
+
+ // That concludes the loop! If there is a currently open token, close that
+ // token now.
+ if (tokenStart !== undefined) {
+ // Error case: a partially-open quoted string is assumed to have a trailing
+ // " character.
+ if (endQuote == '"') {
+ tokenList.push(new Token(value.slice(tokenStart + 1)));
+ } else {
+ tokenList.push(new Token(value.slice(tokenStart)));
+ }
+ }
+ return tokenList;
+ }
+ /* eslint-enable complexity */
+
+ /**
+ * Convert a header value into UTF-16 strings by attempting to decode as UTF-8
+ * or another legacy charset. If the header is valid UTF-8, it will be decoded
+ * as UTF-8; if it is not, the fallbackCharset will be attempted instead.
+ *
+ * @param {string} headerValue - The header (as a binary string) to attempt
+ * to convert to UTF-16.
+ * @param {string} [fallbackCharset] The optional charset to try if UTF-8
+ * doesn't work.
+ * @returns {string} The UTF-16 representation of the string above.
+ */
+ function convert8BitHeader(headerValue, fallbackCharset) {
+ // Only attempt to convert the headerValue if it contains non-ASCII
+ // characters.
+ if (/[\x80-\xff]/.exec(headerValue)) {
+ // First convert the value to a typed-array for MimeTextDecoder.
+ let typedarray = mimeutils.stringToTypedArray(headerValue);
+
+ // Don't try UTF-8 as fallback (redundant), and don't try UTF-16 or UTF-32
+ // either, since they radically change header interpretation.
+ // If we have a fallback charset, we want to know if decoding will fail;
+ // otherwise, we want to replace with substitution chars.
+ let hasFallback =
+ fallbackCharset && !fallbackCharset.toLowerCase().startsWith("utf");
+ let utf8Decoder = new MimeTextDecoder("utf-8", { fatal: hasFallback });
+ try {
+ headerValue = utf8Decoder.decode(typedarray);
+ } catch (e) {
+ // Failed, try the fallback
+ try {
+ let decoder = new MimeTextDecoder(fallbackCharset, {
+ fatal: false,
+ });
+ headerValue = decoder.decode(typedarray);
+ } catch (ex) {}
+ }
+ }
+ return cleanToken(headerValue);
+ }
+
+ /**
+ * Decodes all RFC 2047 encoded-words in the input string. The string does not
+ * necessarily have to contain any such words. This is useful, for example, for
+ * parsing unstructured headers.
+ *
+ * @param {string} headerValue The header which may contain RFC 2047 encoded-
+ * words.
+ * @returns {string} A full UTF-16 string with all encoded words expanded.
+ */
+ function decodeRFC2047Words(headerValue) {
+ // Unfortunately, many implementations of RFC 2047 encoding are actually wrong
+ // in that they split over-long encoded words without regard for whether or
+ // not the split point is in the middle of a multibyte character. Therefore,
+ // we need to be able to handle these situations gracefully. This is done by
+ // using the decoder in streaming mode so long as the next token is another
+ // 2047 token with the same charset.
+ let lastCharset = "",
+ currentDecoder = undefined;
+
+ /**
+ * Decode a single RFC 2047 token. This function is inline so that we can
+ * easily close over the lastCharset/currentDecoder variables, needed for
+ * handling bad RFC 2047 productions properly.
+ * E.g. =?iso-8859-1?q?this=20is=20some=20text?=
+ */
+ function decode2047Token(token, isLastToken) {
+ let tokenParts = token.split("?");
+
+ // If it's obviously not a valid token, return false immediately.
+ if (tokenParts.length != 5 || tokenParts[4] != "=") {
+ return false;
+ }
+
+ // The charset parameter is defined in RFC 2231 to be charset or
+ // charset*language. We only care about the charset here, so ignore any
+ // language parameter that gets passed in.
+ let charset = tokenParts[1].split("*", 1)[0];
+ let encoding = tokenParts[2],
+ text = tokenParts[3];
+
+ let buffer;
+ if (encoding == "B" || encoding == "b") {
+ // Decode base64. If there's any non-base64 data, treat the string as
+ // an illegal token.
+ if (/[^ A-Za-z0-9+\/=]/.exec(text)) {
+ return false;
+ }
+
+ // Decode the string
+ buffer = mimeutils.decode_base64(text, false)[0];
+ } else if (encoding == "Q" || encoding == "q") {
+ // Q encoding here looks a lot like quoted-printable text. The differences
+ // between quoted-printable and this are that quoted-printable allows you
+ // to quote newlines (this doesn't), while this replaces spaces with _.
+ // We can reuse the decode_qp code here, since newlines are already
+ // stripped from the header. There is one edge case that could trigger a
+ // false positive, namely when you have a single = or an = followed by
+ // whitespace at the end of the string. Such an input string is already
+ // malformed to begin with, so stripping the = and following input in that
+ // case should not be an important loss.
+ buffer = mimeutils.decode_qp(text.replace(/_/g, " "), false)[0];
+ } else {
+ return false;
+ }
+
+ // Make the buffer be a typed array for what follows
+ let stringBuffer = buffer;
+ buffer = mimeutils.stringToTypedArray(buffer);
+
+ // If we cannot reuse the last decoder, flush out whatever remains.
+ var output = "";
+ if (charset != lastCharset && currentDecoder) {
+ output += currentDecoder.decode();
+ currentDecoder = null;
+ }
+
+ // Initialize the decoder for this token.
+ lastCharset = charset;
+ if (!currentDecoder) {
+ try {
+ currentDecoder = new MimeTextDecoder(charset, { fatal: false });
+ } catch (e) {
+ // We don't recognize the charset, so give up.
+ return false;
+ }
+ }
+
+ // Convert this token with the buffer. Note the stream parameter--although
+ // RFC 2047 tokens aren't supposed to break in the middle of a multibyte
+ // character, a lot of software messes up and does so because it's hard not
+ // to (see headeremitter.js for exactly how hard!).
+ // We must not stream ISO-2022-JP if the buffer switches back to
+ // the ASCII state, that is, ends in "ESC(B".
+ // Also, we shouldn't do streaming on the last token.
+ let doStreaming;
+ if (
+ isLastToken ||
+ (charset.toUpperCase() == "ISO-2022-JP" &&
+ stringBuffer.endsWith("\x1B(B"))
+ ) {
+ doStreaming = { stream: false };
+ } else {
+ doStreaming = { stream: true };
+ }
+ return output + currentDecoder.decode(buffer, doStreaming);
+ }
+
+ // The first step of decoding is to split the string into RFC 2047 and
+ // non-RFC 2047 tokens. RFC 2047 tokens look like the following:
+ // =?charset?c?text?=, where c is one of B, b, Q, and q. The split regex does
+ // some amount of semantic checking, so that malformed RFC 2047 tokens will
+ // get ignored earlier.
+ let components = headerValue.split(/(=\?[^?]*\?[BQbq]\?[^?]*\?=)/);
+
+ // Find last RFC 2047 token.
+ let lastRFC2047Index = -1;
+ for (let i = 0; i < components.length; i++) {
+ if (components[i].substring(0, 2) == "=?") {
+ lastRFC2047Index = i;
+ }
+ }
+ for (let i = 0; i < components.length; i++) {
+ if (components[i].substring(0, 2) == "=?") {
+ let decoded = decode2047Token(components[i], i == lastRFC2047Index);
+ if (decoded !== false) {
+ // If 2047 decoding succeeded for this bit, rewrite the original value
+ // with the proper decoding.
+ components[i] = decoded;
+
+ // We're done processing, so continue to the next link.
+ continue;
+ }
+ } else if (/^[ \t\r\n]*$/.exec(components[i])) {
+ // Whitespace-only tokens get squashed into nothing, so 2047 tokens will
+ // be concatenated together.
+ components[i] = "";
+ continue;
+ }
+
+ // If there was stuff left over from decoding the last 2047 token, flush it
+ // out.
+ lastCharset = "";
+ if (currentDecoder) {
+ components[i] = currentDecoder.decode() + components[i];
+ currentDecoder = null;
+ }
+ }
+
+ // After the for loop, we'll have a set of decoded strings. Concatenate them
+ // together to make the return value.
+ return cleanToken(components.join(""));
+ }
+
+ // Structured field decoders
+ // -------------------------
+
+ /**
+ * Extract a list of addresses from a header which matches the RFC 5322
+ * address-list production, possibly doing RFC 2047 decoding along the way.
+ *
+ * The output of this method is an array of elements corresponding to the
+ * addresses and the groups in the input header. An address is represented by
+ * an object of the form:
+ * {
+ * name: The display name of the address
+ * email: The address of the object
+ * }
+ * while a group is represented by an object of the form:
+ * {
+ * name: The display name of the group
+ * group: An array of address object for members in the group.
+ * }
+ *
+ * @param {string} header - The MIME header text to be parsed
+ * @param {boolean} doRFC2047 If true, decode RFC 2047 parameters found in the
+ * header.
+ * @returns {(Address|Group)[]} An array of the addresses found in the header,
+ * where each element is of the form mentioned
+ * above.
+ */
+ function parseAddressingHeader(header, doRFC2047) {
+ // Default to true
+ if (doRFC2047 === undefined) {
+ doRFC2047 = true;
+ }
+
+ // The final (top-level) results list to append to.
+ let results = [];
+ // Temporary results
+ let addrlist = [];
+
+ // Build up all of the values
+ let name = "",
+ groupName = "",
+ localPart = "",
+ address = "",
+ comment = "";
+ // Indicators of current state
+ let inAngle = false,
+ inComment = false,
+ needsSpace = false,
+ afterAddress = false;
+ let preserveSpace = false;
+ let commentClosed = false;
+
+ // RFC 5322 §3.4 notes that legacy implementations exist which use a simple
+ // recipient form where the addr-spec appears without the angle brackets,
+ // but includes the name of the recipient in parentheses as a comment
+ // following the addr-spec. While we do not create this format, we still
+ // want to recognize it, though.
+ // Furthermore, despite allowing comments in addresses, RFC 5322 §3.4 notes
+ // that legacy implementations may interpret the comment, and thus it
+ // recommends not to use them. (Also, they may be illegal as per RFC 5321.)
+ // While we do not create address fields with comments, we recognize such
+ // comments during parsing and (a) either drop them if inside addr-spec or
+ // (b) preserve them as part of the display-name if not.
+ // If the display-name is empty while the last comment is not, we assume it's
+ // the legacy form above and take the comment content as the display-name.
+ //
+ // When parsing the address field, we at first do not know whether any
+ // strings belong to the display-name (which may include comments) or to the
+ // local-part of an addr-spec (where we ignore comments) until we find an
+ // '@' or an '<' token. Thus, we collect both variants until the fog lifts,
+ // plus the last comment seen.
+ let lastComment = "";
+
+ /**
+ * Add the parsed mailbox object to the address list.
+ * If it's in the legacy form above, correct the display-name.
+ * Also reset any faked flags.
+ *
+ * @param {string} displayName - display-name as per RFC 5322
+ * @param {string} addrSpec - addr-spec as per RFC 5322
+ */
+ function addToAddrList(displayName, addrSpec) {
+ // Keep the local-part quoted if it needs to be.
+ let lp = addrSpec.substring(0, addrSpec.lastIndexOf("@"));
+ if (/[ !()<>\[\]:;@\\,"]/.exec(lp) !== null) {
+ addrSpec =
+ '"' +
+ lp.replace(/([\\"])/g, "\\$1") +
+ '"' +
+ addrSpec.substring(addrSpec.lastIndexOf("@"));
+ }
+
+ // Replace all whitespace characters with a single whitespace,
+ // to avoid consecutive whitespace and also to normalize tabs and newlines.
+ displayName = displayName.replace(/\s+/g, " ").trim();
+
+ if (displayName === "" && lastComment !== "") {
+ // Take last comment content as the display-name.
+ let offset = lastComment[0] === " " ? 2 : 1;
+ displayName = lastComment.substr(
+ offset,
+ lastComment.length - offset - 1
+ );
+ }
+ if (displayName !== "" || addrSpec !== "") {
+ addrlist.push({ name: displayName, email: addrSpec });
+ }
+ // Clear pending flags and variables.
+ name = localPart = address = lastComment = "";
+ inAngle = inComment = needsSpace = afterAddress = false;
+ }
+
+ // Main parsing loop
+ for (let token of getHeaderTokens(header, ":,;<>@", {
+ qstring: true,
+ comments: true,
+ dliteral: true,
+ rfc2047: doRFC2047,
+ })) {
+ if (token === ":") {
+ groupName = name;
+ name = "";
+ localPart = "";
+ // If we had prior email address results, commit them to the top-level.
+ if (addrlist.length > 0) {
+ results = results.concat(addrlist);
+ }
+ addrlist = [];
+ } else if (token === "<" && !afterAddress) {
+ if (inAngle) {
+ // Interpret the address we were parsing as a name.
+ if (address.length > 0) {
+ name = address;
+ }
+ localPart = address = "";
+ } else {
+ inAngle = true;
+ }
+ } else if (token === ">" && !afterAddress) {
+ inAngle = false;
+ // Forget addr-spec comments.
+ lastComment = "";
+ afterAddress = true;
+ } else if (token === "(") {
+ inComment = true;
+ // The needsSpace flag may not always be set even if it should be,
+ // e.g. for a comment behind an angle-addr.
+ // Also, we need to restore the needsSpace flag if we ignore the comment.
+ preserveSpace = needsSpace;
+ if (!needsSpace) {
+ needsSpace = name !== "" && name.substr(-1) !== " ";
+ }
+ comment = needsSpace ? " (" : "(";
+ commentClosed = false;
+ } else if (token === ")") {
+ inComment = false;
+ comment += ")";
+ lastComment = comment;
+ // The comment may be part of the name, but not of the local-part.
+ // Enforce a space behind the comment only when not ignoring it.
+ if (inAngle) {
+ needsSpace = preserveSpace;
+ } else {
+ name += comment;
+ needsSpace = true;
+ }
+ commentClosed = true;
+ continue;
+ } else if (token === "@") {
+ if (afterAddress) {
+ continue;
+ }
+ // An @ means we see an email address. If we're not within <> brackets,
+ // then we just parsed an email address instead of a display name. Empty
+ // out the display name for the current production.
+ if (!inAngle) {
+ address = localPart;
+ name = "";
+ localPart = "";
+ // The remainder of this mailbox is part of an addr-spec.
+ inAngle = true;
+ }
+ address += "@";
+ } else if (token === ",") {
+ // A comma ends the current name. If we have something that's kind of a
+ // name, add it to the result list. If we don't, then our input looks like
+ // To: , , -> don't bother adding an empty entry.
+ addToAddrList(name, address);
+ afterAddress = false;
+ } else if (token === ";") {
+ // Add pending name to the list
+ addToAddrList(name, address);
+
+ // If no group name was found, treat the ';' as a ','. In any case, we
+ // need to copy the results of addrlist into either a new group object or
+ // the main list.
+ if (groupName === "") {
+ results = results.concat(addrlist);
+ } else {
+ results.push({
+ name: groupName,
+ group: addrlist,
+ });
+ }
+ // ... and reset every other variable.
+ addrlist = [];
+ groupName = "";
+ } else {
+ // This is either comment content, a quoted-string, or some span of
+ // dots and atoms.
+ token = cleanToken(token.toString());
+
+ // Ignore the needs space if we're a "close" delimiter token.
+ let spacedToken = token;
+ if (needsSpace && token && token[0] != ".") {
+ spacedToken = " " + spacedToken;
+ }
+
+ // Which field do we add this data to?
+ if (inComment) {
+ comment += spacedToken;
+ } else if (inAngle) {
+ address += spacedToken;
+ } else {
+ if (!afterAddress) {
+ name += spacedToken;
+ }
+ // Never add a space to the local-part, if we just ignored a comment.
+ if (commentClosed) {
+ localPart += token;
+ commentClosed = false;
+ } else {
+ localPart += spacedToken;
+ }
+ }
+
+ // We need space for the next token if we aren't some kind of comment or
+ // . delimiter.
+ needsSpace = token && token[0] != ".";
+ // The fall-through case after this resets needsSpace to false, and we
+ // don't want that!
+ continue;
+ }
+
+ // If we just parsed a delimiter, we don't need any space for the next
+ // token.
+ needsSpace = false;
+ }
+
+ // If we're missing the final ';' of a group, assume it was present. Also, add
+ // in the details of any email/address that we previously saw.
+ addToAddrList(name, address);
+ if (groupName !== "") {
+ results.push({ name: groupName, group: addrlist });
+ addrlist = [];
+ }
+
+ // Add the current address list build-up to the list of addresses, and return
+ // the whole array to the caller.
+ return results.concat(addrlist);
+ }
+
+ /**
+ * Extract parameters from a header which is a series of ;-separated
+ * attribute=value tokens.
+ *
+ * @param {string} headerValue The MIME header value to parse.
+ * @param {boolean} doRFC2047 - If true, decode RFC 2047 encoded-words.
+ * @param {boolean} doRFC2231 - If true, decode RFC 2231 encoded parameters.
+ * @returns {Map(String -> String)} A map of parameter names to parameter values.
+ * The property preSemi is set to the token that
+ * precedes the first semicolon.
+ */
+ /* eslint-disable complexity */
+ function parseParameterHeader(headerValue, doRFC2047, doRFC2231) {
+ // The basic syntax of headerValue is token [; token = token-or-qstring]*
+ // Copying more or less liberally from nsMIMEHeaderParamImpl:
+ // The first token is the text to the first whitespace or semicolon.
+ var semi = headerValue.indexOf(";");
+ let start, rest;
+ if (semi < 0) {
+ start = headerValue;
+ rest = "";
+ } else {
+ start = headerValue.substring(0, semi);
+ rest = headerValue.substring(semi); // Include the semicolon
+ }
+ // Strip start to be <WSP><nowsp><WSP>.
+ start = start.trim().split(/[ \t\r\n]/)[0];
+
+ // Decode the the parameter tokens.
+ let opts = { qstring: true, rfc2047: doRFC2047 };
+ // Name is the name of the parameter, inName is true iff we don't have a name
+ // yet.
+ let name = "",
+ inName = true;
+ // Matches is a list of [name, value] pairs, where we found something that
+ // looks like name=value in the input string.
+ let matches = [];
+ for (let token of getHeaderTokens(rest, ";=", opts)) {
+ if (token === ";") {
+ // If we didn't find a name yet (we have ... tokenA; tokenB), push the
+ // name with an empty token instead.
+ if (name != "" && !inName) {
+ matches.push([name, ""]);
+ }
+ name = "";
+ inName = true;
+ } else if (token === "=") {
+ inName = false;
+ } else if (inName && name == "") {
+ name = token.toString();
+ } else if (!inName && name != "") {
+ token = token.toString();
+ // RFC 2231 doesn't make it clear if %-encoding is supposed to happen
+ // within a quoted string, but this is very much required in practice. If
+ // it ends with a '*', then the string is an extended-value, which means
+ // that its value may be %-encoded.
+ if (doRFC2231 && name.endsWith("*")) {
+ token = token.replace(
+ /%([0-9A-Fa-f]{2})/g,
+ function (match, hexchars) {
+ return String.fromCharCode(parseInt(hexchars, 16));
+ }
+ );
+ }
+ matches.push([name, token]);
+ // Clear the name, so we ignore anything afterwards.
+ name = "";
+ } else if (inName) {
+ // We have ...; tokenA tokenB ... -> ignore both tokens
+ name = ""; // Error recovery, ignore this one
+ }
+ }
+ // If we have a leftover ...; tokenA, push the tokenA
+ if (name != "" && !inName) {
+ matches.push([name, ""]);
+ }
+
+ // Now matches holds the parameters, so clean up for RFC 2231. There are three
+ // cases: param=val, param*=us-ascii'en-US'blah, and param*n= variants. The
+ // order of preference is to pick the middle, then the last, then the first.
+ // Note that we already unpacked %-encoded values.
+
+ // simpleValues is just a straight parameter -> value map.
+ // charsetValues is the parameter -> value map, although values are stored
+ // before charset decoding happens.
+ // continuationValues maps parameter -> array of values, with extra properties
+ // valid (if we decided we couldn't do anything anymore) and hasCharset (which
+ // records if we need to decode the charset parameter or not).
+ var simpleValues = new Map(),
+ charsetValues = new Map(),
+ continuationValues = new Map();
+ for (let pair of matches) {
+ let name = pair[0];
+ let value = pair[1];
+ // Get first index, not last index, so we match param*0*= like param*0=.
+ let star = name.indexOf("*");
+ if (star == -1) {
+ // This is the case of param=val. Select the first value here, if there
+ // are multiple ones.
+ if (!simpleValues.has(name)) {
+ simpleValues.set(name, value);
+ }
+ } else if (star == name.length - 1) {
+ // This is the case of param*=us-ascii'en-US'blah.
+ name = name.substring(0, star);
+ // Again, select only the first value here.
+ if (!charsetValues.has(name)) {
+ charsetValues.set(name, value);
+ }
+ } else {
+ // This is the case of param*0= or param*0*=.
+ let param = name.substring(0, star);
+ let entry = continuationValues.get(param);
+ // Did we previously find this one to be bungled? Then ignore it.
+ if (continuationValues.has(param) && !entry.valid) {
+ continue;
+ }
+
+ // If we haven't seen it yet, set up entry already. Note that entries are
+ // not straight string values but rather [valid, hasCharset, param0, ... ]
+ if (!continuationValues.has(param)) {
+ entry = [];
+ entry.valid = true;
+ entry.hasCharset = undefined;
+ continuationValues.set(param, entry);
+ }
+
+ // When the string ends in *, we need to charset decoding.
+ // Note that the star is only meaningful for the *0*= case.
+ let lastStar = name[name.length - 1] == "*";
+ let number = name.substring(
+ star + 1,
+ name.length - (lastStar ? 1 : 0)
+ );
+ if (number == "0") {
+ entry.hasCharset = lastStar;
+ } else if (
+ number.length == 0 ||
+ (number[0] == "0" && number != "0") ||
+ !/^[0-9]+$/.test(number)
+ ) {
+ // Is the continuation number illegal?
+ entry.valid = false;
+ continue;
+ }
+ // Normalize to an integer
+ number = parseInt(number, 10);
+
+ // Is this a repeat? If so, bail.
+ if (entry[number] !== undefined) {
+ entry.valid = false;
+ continue;
+ }
+
+ // Set the value for this continuation index. JS's magic array setter will
+ // expand the array if necessary.
+ entry[number] = value;
+ }
+ }
+
+ // Build the actual parameter array from the parsed values
+ var values = new Map();
+ // Simple values have lowest priority, so just add everything into the result
+ // now.
+ for (let pair of simpleValues) {
+ values.set(pair[0], pair[1]);
+ }
+
+ if (doRFC2231) {
+ // Continuation values come next
+ for (let pair of continuationValues) {
+ let name = pair[0];
+ let entry = pair[1];
+ // If we never saw a param*0= or param*0*= value, then we can't do any
+ // reasoning about what it looks like, so bail out now.
+ if (entry.hasCharset === undefined) {
+ continue;
+ }
+
+ // Use as many entries in the array as are valid--if we are missing an
+ // entry, stop there.
+ let valid = true;
+ for (var i = 0; valid && i < entry.length; i++) {
+ if (entry[i] === undefined) {
+ valid = false;
+ }
+ }
+
+ // Concatenate as many parameters as are valid. If we need to decode thec
+ // charset, do so now.
+ let value = entry.slice(0, i).join("");
+ if (entry.hasCharset) {
+ try {
+ value = decode2231Value(value);
+ } catch (e) {
+ // Bad charset, don't add anything.
+ continue;
+ }
+ }
+ // Finally, add this to the output array.
+ values.set(name, value);
+ }
+
+ // Highest priority is the charset conversion.
+ for (let pair of charsetValues) {
+ try {
+ values.set(pair[0], decode2231Value(pair[1]));
+ } catch (e) {
+ // Bad charset, don't add anything.
+ }
+ }
+ }
+
+ for (let [key, value] of values.entries()) {
+ values.set(key, cleanToken(value));
+ }
+
+ // Finally, return the values computed above.
+ values.preSemi = start;
+ return values;
+ }
+ /* eslint-enable complexity */
+
+ /**
+ * Convert a RFC 2231-encoded string parameter into a Unicode version of the
+ * string. This assumes that percent-decoding has already been applied.
+ *
+ * @param {string} value The RFC 2231-encoded string to decode.
+ * @returns The Unicode version of the string.
+ */
+ function decode2231Value(value) {
+ let quote1 = value.indexOf("'");
+ let quote2 = quote1 >= 0 ? value.indexOf("'", quote1 + 1) : -1;
+
+ let charset = quote1 >= 0 ? value.substring(0, quote1) : "";
+ // It turns out that the language isn't useful anywhere in our codebase for
+ // the present time, so we will safely ignore it.
+ // var language = (quote2 >= 0 ? value.substring(quote1 + 2, quote2) : "");
+ value = value.substring(Math.max(quote1, quote2) + 1);
+
+ // Convert the value into a typed array for decoding
+ let typedarray = mimeutils.stringToTypedArray(value);
+
+ // Decode the charset. If the charset isn't found, we throw an error. Try to
+ // fallback in that case.
+ return new MimeTextDecoder(charset, { fatal: true }).decode(typedarray, {
+ stream: false,
+ });
+ }
+
+ // This is a map of known timezone abbreviations, for fallback in obsolete Date
+ // productions.
+ var kKnownTZs = {
+ // The following timezones are explicitly listed in RFC 5322.
+ UT: "+0000",
+ GMT: "+0000",
+ EST: "-0500",
+ EDT: "-0400",
+ CST: "-0600",
+ CDT: "-0500",
+ MST: "-0700",
+ MDT: "-0600",
+ PST: "-0800",
+ PDT: "-0700",
+ // The following are time zones copied from NSPR's prtime.c
+ AST: "-0400", // Atlantic Standard Time
+ NST: "-0330", // Newfoundland Standard Time
+ BST: "+0100", // British Summer Time
+ MET: "+0100", // Middle Europe Time
+ EET: "+0200", // Eastern Europe Time
+ JST: "+0900", // Japan Standard Time
+ };
+
+ /**
+ * Parse a header that contains a date-time definition according to RFC 5322.
+ * The result is a JS date object with the same timestamp as the header.
+ *
+ * The dates returned by this parser cannot be reliably converted back into the
+ * original header for two reasons. First, JS date objects cannot retain the
+ * timezone information they were initialized with, so reserializing a date
+ * header would necessarily produce a date in either the current timezone or in
+ * UTC. Second, JS dates measure time as seconds elapsed from the POSIX epoch
+ * excluding leap seconds. Any timestamp containing a leap second is instead
+ * converted into one that represents the next second.
+ *
+ * Dates that do not match the RFC 5322 production are instead attempted to
+ * parse using the Date.parse function. The strings that are accepted by
+ * Date.parse are not fully defined by the standard, but most implementations
+ * should accept strings that look rather close to RFC 5322 strings. Truly
+ * invalid dates produce a formulation that results in an invalid date,
+ * detectable by having its .getTime() method return NaN.
+ *
+ * @param {string} header The MIME header value to parse.
+ * @returns {Date} The date contained within the header, as described
+ * above.
+ */
+ function parseDateHeader(header) {
+ let tokens = getHeaderTokens(header, ",:", {}).map(x => x.toString());
+ // What does a Date header look like? In practice, most date headers devolve
+ // into Date: [dow ,] dom mon year hh:mm:ss tzoff [(abbrev)], with the day of
+ // week mostly present and the timezone abbreviation mostly absent.
+
+ // First, ignore the day-of-the-week if present. This would be the first two
+ // tokens.
+ if (tokens.length > 1 && tokens[1] === ",") {
+ tokens = tokens.slice(2);
+ }
+
+ // If there are too few tokens, the date is obviously invalid.
+ if (tokens.length < 8) {
+ return new Date(NaN);
+ }
+
+ // Save off the numeric tokens
+ let day = parseInt(tokens[0]);
+ // month is tokens[1]
+ let year = parseInt(tokens[2]);
+ let hours = parseInt(tokens[3]);
+ // tokens[4] === ':'
+ let minutes = parseInt(tokens[5]);
+ // tokens[6] === ':'
+ let seconds = parseInt(tokens[7]);
+
+ // Compute the month. Check only the first three digits for equality; this
+ // allows us to accept, e.g., "January" in lieu of "Jan."
+ let month = mimeutils.kMonthNames.indexOf(tokens[1].slice(0, 3));
+ // If the month name is not recognized, make the result illegal.
+ if (month < 0) {
+ month = NaN;
+ }
+
+ // Compute the full year if it's only 2 digits. RFC 5322 states that the
+ // cutoff is 50 instead of 70.
+ if (year < 100) {
+ year += year < 50 ? 2000 : 1900;
+ }
+
+ // Compute the timezone offset. If it's not in the form ±hhmm, convert it to
+ // that form.
+ let tzoffset = tokens[8];
+ if (tzoffset in kKnownTZs) {
+ tzoffset = kKnownTZs[tzoffset];
+ }
+ let decompose = /^([+-])(\d\d)(\d\d)$/.exec(tzoffset);
+ // Unknown? Make it +0000
+ if (decompose === null) {
+ decompose = ["+0000", "+", "00", "00"];
+ }
+ let tzOffsetInMin = parseInt(decompose[2]) * 60 + parseInt(decompose[3]);
+ if (decompose[1] == "-") {
+ tzOffsetInMin = -tzOffsetInMin;
+ }
+
+ // How do we make the date at this point? Well, the JS date's constructor
+ // builds the time in terms of the local timezone. To account for the offset
+ // properly, we need to build in UTC.
+ let finalDate = new Date(
+ Date.UTC(year, month, day, hours, minutes, seconds) -
+ tzOffsetInMin * 60 * 1000
+ );
+
+ // Suppose our header was mangled and we couldn't read it--some of the fields
+ // became undefined. In that case, the date would become invalid, and the
+ // indication that it is so is that the underlying number is a NaN. In that
+ // scenario, we could build attempt to use JS Date parsing as a last-ditch
+ // attempt. But it's not clear that such messages really exist in practice,
+ // and the valid formats for Date in ES6 are unspecified.
+ return finalDate;
+ }
+
+ // Structured header decoding support
+ // ----------------------------------
+
+ // Load the default structured decoders
+ var structuredDecoders = new Map();
+ var structuredHeaders = require("./structuredHeaders");
+ var preferredSpellings = structuredHeaders.spellings;
+ var forbiddenHeaders = new Set();
+ for (let pair of structuredHeaders.decoders) {
+ addStructuredDecoder(pair[0], pair[1]);
+ forbiddenHeaders.add(pair[0].toLowerCase());
+ }
+
+ /**
+ * Use an already-registered structured decoder to parse the value of the header
+ * into a structured representation.
+ *
+ * As this method is designed to be used for the internal MIME Parser to convert
+ * the raw header values to well-structured values, value is intended to be an
+ * array consisting of all occurrences of the header in order. However, for ease
+ * of use by other callers, it can also be treated as a string.
+ *
+ * If the decoder for the header is not found, an exception will be thrown.
+ *
+ * A large set of headers have pre-defined structured decoders; these decoders
+ * cannot be overridden with addStructuredDecoder, as doing so could prevent the
+ * MIME or message parsers from working properly. The pre-defined structured
+ * headers break down into five clases of results, plus some ad-hoc
+ * representations. They are:
+ *
+ * Addressing headers (results are the same as parseAddressingHeader):
+ * - Approved
+ * - Bcc
+ * - Cc
+ * - Delivered-To
+ * - Disposition-Notification-To
+ * - From
+ * - Mail-Reply-To
+ * - Mail-Followup-To
+ * - Reply-To
+ * - Resent-Bcc
+ * - Resent-Cc
+ * - Resent-From
+ * - Resent-Reply-To
+ * - Resent-Sender
+ * - Resent-To
+ * - Return-Receipt-To
+ * - Sender
+ * - To
+ *
+ * Date headers (results are the same as parseDateHeader):
+ * - Date
+ * - Expires
+ * - Injection-Date
+ * - NNTP-Posting-Date
+ * - Resent-Date
+ *
+ * References headers (results are the same as parseReferencesHeader):
+ * - (TODO: Parsing support for these headers is currently unsupported)
+ *
+ * Message-ID headers (results are the first entry of the result of
+ * parseReferencesHeader):
+ * - (TODO: Parsing support for these headers is currently unsupported)
+ *
+ * Unstructured headers (results are merely decoded according to RFC 2047):
+ * - Comments
+ * - Content-Description
+ * - Keywords
+ * - Subject
+ *
+ * The ad-hoc headers and their resulting formats are as follows:
+ * Content-Type: returns a JS Map of parameter names (in lower case) to their
+ * values, along with the following extra properties defined on the map:
+ * - mediatype: the type to the left of '/' (e.g., 'text', 'message')
+ * - subtype: the type to the right of '/' (e.g., 'plain', 'rfc822')
+ * - type: the full typename (e.g., 'text/plain')
+ * RFC 2047 and RFC 2231 decoding is applied where appropriate. The values of
+ * the type, mediatype, and subtype attributes are all normalized to lower-case,
+ * as are the names of all parameters.
+ *
+ * Content-Transfer-Encoding: the first value is converted to lower-case.
+ *
+ * @param {string} header The name of the header of the values.
+ * @param {string | Array} value The value(s) of the headers, after charset
+ * conversion (if any) has been applied. If it is
+ * an array, the headers are listed in the order
+ * they appear in the message.
+ * @returns {object} A structured representation of the header values.
+ */
+ function parseStructuredHeader(header, value) {
+ // Enforce that the parameter is an array. If it's a string, make it a
+ // 1-element array.
+ if (typeof value === "string" || value instanceof String) {
+ value = [value];
+ }
+ if (!Array.isArray(value)) {
+ throw new TypeError("Header value is not an array: " + value);
+ }
+
+ // Lookup the header in our decoders; if present, use that to decode the
+ // header.
+ let lowerHeader = header.toLowerCase();
+ if (structuredDecoders.has(lowerHeader)) {
+ return structuredDecoders.get(lowerHeader).call(headerparser, value);
+ }
+
+ // If not present, throw an exception.
+ throw new Error("Unknown structured header: " + header);
+ }
+
+ /**
+ * Add a custom structured MIME decoder to the set of known decoders. These
+ * decoders are used for {@link parseStructuredHeader} and similar functions to
+ * encode richer, more structured values instead of relying on string
+ * representations everywhere.
+ *
+ * Structured decoders are functions which take in a single parameter consisting
+ * of an array of the string values of the header, in order that they appear in
+ * the message. These headers have had the charset conversion (if necessary)
+ * applied to them already. The this parameter of the function is set to be the
+ * jsmime.headerparser module.
+ *
+ * There is a large set of structured decoders built-in to the jsmime library
+ * already. As these headers are fundamental to the workings of jsmime,
+ * attempting to replace them with a custom version will instead produce an
+ * exception.
+ *
+ * @param {string} header The header name (in any case)
+ * for which the decoder will be
+ * used.
+ * @param {Function(String[] -> Object)} decoder The structured decoder
+ * function.
+ */
+ function addStructuredDecoder(header, decoder) {
+ let lowerHeader = header.toLowerCase();
+ if (forbiddenHeaders.has(lowerHeader)) {
+ throw new Error("Cannot override header: " + header);
+ }
+ structuredDecoders.set(lowerHeader, decoder);
+ if (!preferredSpellings.has(lowerHeader)) {
+ preferredSpellings.set(lowerHeader, header);
+ }
+ }
+
+ headerparser.addStructuredDecoder = addStructuredDecoder;
+ headerparser.convert8BitHeader = convert8BitHeader;
+ headerparser.decodeRFC2047Words = decodeRFC2047Words;
+ headerparser.getHeaderTokens = getHeaderTokens;
+ headerparser.parseAddressingHeader = parseAddressingHeader;
+ headerparser.parseDateHeader = parseDateHeader;
+ headerparser.parseParameterHeader = parseParameterHeader;
+ headerparser.parseStructuredHeader = parseStructuredHeader;
+ return Object.freeze(headerparser);
+ });
+
+ // JavaScript Raw MIME Parser
+ // --------------------------
+
+ /**
+ * The parser implemented in this file produces a MIME part tree for a given
+ * input message via a streaming callback interface. It does not, by itself,
+ * understand concepts like attachments (hence the term 'Raw'); the consumer
+ * must translate output into such a format.
+ *
+ * Charsets:
+ * The MIME specifications permit a single message to contain multiple charsets
+ * (or perhaps none) as raw octets. As JavaScript strings are implicitly
+ * implemented in UTF-16, it is possible that some engines will attempt to
+ * convert these strings using an incorrect charset or simply fail to convert
+ * them at all. This parser assumes that its input is in the form of a "binary
+ * string", a string that uses only the first 256 characters of Unicode to
+ * represent the individual octets. To verify that charsets are not getting
+ * mangled elsewhere in the pipeline, the auxiliary test file test/data/charsets
+ * can be used.
+ *
+ * This parser attempts to hide the charset details from clients as much as
+ * possible. The resulting values of structured headers are always converted
+ * into proper Unicode strings before being exposed to clients; getting at the
+ * raw binary string data can only be done via getRawHeader. The .charset
+ * parameter on header objects, if changed, changes the fallback charset used
+ * for headers. It is initialized to the presumed charset of the corresponding
+ * part, taking into account the charset and force-charset options of the
+ * parser. Body parts are only converted into Unicode strings if the strformat
+ * option is set to Unicode. Even then, only the bodies of parts with a media
+ * type of text are converted to Unicode strings using available charset data;
+ * other parts are retained as Uint8Array objects.
+ *
+ * Part numbering:
+ * Since the output is a streaming format, individual parts are identified by a
+ * numbering scheme. The intent of the numbering scheme for parts is to comply
+ * with the part numbers as dictated by RFC 3501 as much possible; however,
+ * that scheme does have several edge cases which would, if strictly followed,
+ * make it impossible to refer to certain parts of the message. In addition, we
+ * wish to make it possible to refer to parts which are not discoverable in the
+ * original MIME tree but are still viewable as parts. The part numbering
+ * scheme is as follows:
+ * - Individual sections of a multipart/* body are numbered in increasing order
+ * sequentially, starting from 1. Note that the prologue and the epilogue of
+ * a multipart/* body are not considered entities and are therefore not
+ * included in the part numbering scheme (there is no way to refer to them).
+ * - The numbers of multipart/* parts are separated by `.' characters.
+ * - The outermost message is referred to by use of the empty string.
+ * --> The following segments are not accounted for by IMAP part numbering. <--
+ * - The body of any message/rfc822 or similar part is distinguished from the
+ * message part as a whole by appending a `$' character. This does not apply
+ * to the outermost message/rfc822 envelope.
+ */
+
+ def("mimeparser", function (require) {
+ "use strict";
+
+ var mimeutils = require("./mimeutils");
+ var headerparser = require("./headerparser");
+ var spellings = require("./structuredHeaders").spellings;
+
+ /**
+ * An object that represents the structured MIME headers for a message.
+ *
+ * This class is primarily used as the 'headers' parameter in the startPart
+ * callback on handlers for MimeParser. As such, it is designed to do the right
+ * thing in common cases as much as possible, with some advanced customization
+ * possible for clients that need such flexibility.
+ *
+ * In a nutshell, this class stores the raw headers as an internal Map. The
+ * structured headers are not computed until they are actually used, which means
+ * that potentially expensive structuring (e.g., doing manual DKIM validation)
+ * can be performed as a structured decoder without impeding performance for
+ * those who just want a few common headers.
+ *
+ * The outer API of this class is intended to be similar to a read-only Map
+ * object (complete with iterability support), with a few extra properties to
+ * represent things that are hard to determine properly from headers. The keys
+ * used are "preferred spellings" of the headers, although the get and has
+ * methods will accept header parameters of any case. Preferred spellings are
+ * derived from the name passed to addStructuredDecoder/addStructuredEncoder; if
+ * no structured decoder has been registered, then the name capitalizes the
+ * first letter of every word in the header name.
+ *
+ * Extra properties compared to a Map object are:
+ * - charset: This field represents the assumed charset of the associated MIME
+ * body. It is prefilled using a combination of the charset and force-charset
+ * options on the associated MimeParser instance as well as attempting to find
+ * a charset parameter in the Content-Type header.
+ *
+ * If the force-charset option is false, the charset is guessed first using
+ * the Content-Type header's charset parameter, falling back to the charset
+ * option if it is present. If the force-charset option is true, the charset
+ * is initially set to the charset option. This initial guessed value can be
+ * overridden at any time by simply setting the field on this object.
+ *
+ * The charset is better reflected as a parameter of the body rather than the
+ * headers; this is ultimately the charset parameter that will be used if a
+ * body part is being converted to a Unicode strformat. Headers are converted
+ * using headerparser.convert8BitHeader, and this field is used as the
+ * fallbackCharset parameter, which will always to attempt to decode as UTF-8
+ * first (in accordance with RFC 6532) and will refuse to decode as UTF-16 or
+ * UTF-32, as ASCII is not a subset of those charsets.
+ *
+ * - rawHeaderText: This read-only field contains the original header text from
+ * which headers were parsed, preserving case and whitespace (including
+ * alternate line endings instead of CRLF) exactly. If the header text begins
+ * with the mbox delimiter (i.e., a line that begins with "From "), then that
+ * is excluded from the rawHeaderText value and is not reflected anywhere in
+ * this object.
+ *
+ * - contentType: This field contains the structured representation of the
+ * Content-Type header, if it is present. If it is not present, it is set to
+ * the structured representation of the default Content-Type for a part (as
+ * this data is not easily guessed given only MIME tree events).
+ *
+ * The constructor for these objects is not externally exported, and thus they
+ * can only be created via MimeParser.
+ *
+ * @param rawHeaderText {BinaryString} The contents of the MIME headers to be
+ * parsed.
+ * @param options {Object} Options for the header parser.
+ * @param options.stripcontinuations {Boolean} If true, elide CRLFs from the
+ * raw header output.
+ */
+ function StructuredHeaders(rawHeaderText, options) {
+ // An individual header is terminated by a CRLF, except if the CRLF is
+ // followed by a SP or TAB. Use negative lookahead to capture the latter case,
+ // and don't capture the strings or else split results get nasty.
+ let values = rawHeaderText.split(/(?:\r\n|\n)(?![ \t])|\r(?![ \t\n])/);
+
+ // Ignore the first "header" if it begins with an mbox delimiter
+ if (values.length > 0 && values[0].substring(0, 5) == "From ") {
+ values.shift();
+ // Elide the mbox delimiter from this._headerData
+ if (values.length == 0) {
+ rawHeaderText = "";
+ } else {
+ rawHeaderText = rawHeaderText.substring(
+ rawHeaderText.indexOf(values[0])
+ );
+ }
+ }
+
+ let headers = new Map();
+ for (let i = 0; i < values.length; i++) {
+ // Look for a colon. If it's not present, this header line is malformed,
+ // perhaps by premature EOF or similar.
+ let colon = values[i].indexOf(":");
+ let header, val;
+ if (colon >= 0) {
+ header = values[i].substring(0, colon);
+ val = values[i].substring(colon + 1).trim();
+ if (options.stripcontinuations) {
+ val = val.replace(/[\r\n]/g, "");
+ }
+ } else {
+ header = values[i];
+ val = "";
+ }
+
+ // Canonicalize the header in lower-case form.
+ header = header.trim().toLowerCase();
+ // Omit "empty" headers
+ if (header == "") {
+ continue;
+ }
+
+ // We keep an array of values for each header, since a given header may be
+ // repeated multiple times.
+ if (headers.has(header)) {
+ headers.get(header).push(val);
+ } else {
+ headers.set(header, [val]);
+ }
+ }
+
+ /**
+ * A map of header names to arrays of raw values found in this header block.
+ *
+ * @private
+ */
+ this._rawHeaders = headers;
+ /**
+ * Cached results of structured header parsing.
+ *
+ * @private
+ */
+ this._cachedHeaders = new Map();
+ Object.defineProperty(this, "rawHeaderText", {
+ get() {
+ return rawHeaderText;
+ },
+ });
+ Object.defineProperty(this, "size", {
+ get() {
+ return this._rawHeaders.size;
+ },
+ });
+ Object.defineProperty(this, "charset", {
+ get() {
+ return this._charset;
+ },
+ set(value) {
+ this._charset = value;
+ // Clear the cached headers, since this could change their values
+ this._cachedHeaders.clear();
+ },
+ });
+
+ // Default to the charset, until the message parser overrides us.
+ if ("charset" in options) {
+ this._charset = options.charset;
+ } else {
+ this._charset = null;
+ }
+
+ // If we have a Content-Type header, set contentType to return the structured
+ // representation. We don't set the value off the bat, since we want to let
+ // someone who changes the charset affect the values of 8-bit parameters.
+ Object.defineProperty(this, "contentType", {
+ configurable: true,
+ get() {
+ return this.get("Content-Type");
+ },
+ });
+ }
+
+ /**
+ * Get a raw header.
+ *
+ * Raw headers are an array of the header values, listed in order that they were
+ * specified in the header block, and without any attempt to convert charsets or
+ * apply RFC 2047 decoding. For example, in the following message (where the
+ * <XX> is meant to represent binary-octets):
+ *
+ * X-Header: Value A
+ * X-Header: V<C3><A5>lue B
+ * Header2: Q
+ *
+ * the result of calling getRawHeader('X-Header') or getRawHeader('x-header')
+ * would be ['Value A', 'V\xC3\xA5lue B'] and the result of
+ * getRawHeader('Header2') would be ['Q'].
+ *
+ * @param headerName {String} The header name for which to get header values.
+ * @returns {BinaryString[]} The raw header values (with no charset conversion
+ * applied).
+ */
+ StructuredHeaders.prototype.getRawHeader = function (headerName) {
+ return this._rawHeaders.get(headerName.toLowerCase());
+ };
+
+ /**
+ * Retrieve a structured version of the header.
+ *
+ * If there is a registered structured decoder (registration happens via
+ * headerparser.addStructuredDecoder), then the result of calling that decoder
+ * on the charset-corrected version of the header is returned. Otherwise, the
+ * values are charset-corrected and RFC 2047 decoding is applied as if the
+ * header were an unstructured header.
+ *
+ * A substantial set of headers have pre-registed structured decoders, which, in
+ * some cases, are unable to be overridden due to their importance in the
+ * functioning of the parser code itself.
+ *
+ * @param headerName {String} The header name for which to get the header value.
+ * @returns The structured header value of the output.
+ */
+ StructuredHeaders.prototype.get = function (headerName) {
+ // Normalize the header name to lower case
+ headerName = headerName.toLowerCase();
+
+ // First, check the cache for the header value
+ if (this._cachedHeaders.has(headerName)) {
+ return this._cachedHeaders.get(headerName);
+ }
+
+ // Not cached? Grab it [propagating lack of header to caller]
+ let headerValue = this._rawHeaders.get(headerName);
+ if (headerValue === undefined) {
+ return headerValue;
+ }
+
+ // Convert the header to Unicode
+ let charset = this.charset;
+ headerValue = headerValue.map(function (value) {
+ return headerparser.convert8BitHeader(value, charset);
+ });
+
+ // If there is a structured decoder, use that; otherwise, assume that the
+ // header is unstructured and only do RFC 2047 conversion
+ let structured;
+ try {
+ structured = headerparser.parseStructuredHeader(
+ headerName,
+ headerValue
+ );
+ } catch (e) {
+ structured = headerValue.map(function (value) {
+ return headerparser.decodeRFC2047Words(value);
+ });
+ }
+
+ // Cache the result and return it
+ this._cachedHeaders.set(headerName, structured);
+ return structured;
+ };
+
+ /**
+ * Check if the message has the given header.
+ *
+ * @param headerName {String} The header name for which to get the header value.
+ * @returns {boolean} True if the header is present in this header block.
+ */
+ StructuredHeaders.prototype.has = function (headerName) {
+ // Check for presence in the raw headers instead of cached headers.
+ return this._rawHeaders.has(headerName.toLowerCase());
+ };
+
+ // Make a custom iterator. Presently, support for Symbol isn't yet present in
+ // SpiderMonkey (or V8 for that matter), so type-pun the name for now.
+ var JS_HAS_SYMBOLS = typeof Symbol === "function";
+ var ITERATOR_SYMBOL = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
+
+ /**
+ * An equivalent of Map.@@iterator, applied to the structured header
+ * representations. This is the function that makes
+ * for (let [header, value] of headers) work properly.
+ */
+ StructuredHeaders.prototype[ITERATOR_SYMBOL] = function* () {
+ // Iterate over all the raw headers, and use the cached headers to retrieve
+ // them.
+ for (let headerName of this.keys()) {
+ yield [headerName, this.get(headerName)];
+ }
+ };
+
+ /**
+ * An equivalent of Map.forEach, applied to the structured header
+ * representations.
+ *
+ * @param callback {Function(value, name, headers)} The callback to call for
+ * each header/value combo.
+ * @param thisarg {Object} The parameter that will be
+ * the |this| of the callback.
+ */
+ StructuredHeaders.prototype.forEach = function (callback, thisarg) {
+ for (let [header, value] of this) {
+ callback.call(thisarg, value, header, this);
+ }
+ };
+
+ /**
+ * An equivalent of Map.entries, applied to the structured header
+ * representations.
+ */
+ StructuredHeaders.prototype.entries =
+ StructuredHeaders.prototype[Symbol.iterator];
+
+ // This function maps lower case names to a pseudo-preferred spelling.
+ function capitalize(headerName) {
+ return headerName.replace(/\b[a-z]/g, function (match) {
+ return match.toUpperCase();
+ });
+ }
+
+ /**
+ * An equivalent of Map.keys, applied to the structured header representations.
+ */
+ StructuredHeaders.prototype.keys = function* () {
+ for (let name of this._rawHeaders.keys()) {
+ yield spellings.get(name) || capitalize(name);
+ }
+ };
+
+ /**
+ * An equivalent of Map.values, applied to the structured header
+ * representations.
+ */
+ StructuredHeaders.prototype.values = function* () {
+ for (let [, value] of this) {
+ yield value;
+ }
+ };
+
+ /**
+ * A MIME parser.
+ *
+ * The inputs to the constructor consist of a callback object which receives
+ * information about the output data and an optional object containing the
+ * settings for the parser.
+ *
+ * The first parameter, emitter, is an object which contains several callbacks.
+ * Note that any and all of these methods are optional; the parser will not
+ * crash if one is missing. The callbacks are as follows:
+ * startMessage()
+ * Called when the stream to be parsed has started delivering data. This
+ * will be called exactly once, before any other call.
+ * endMessage()
+ * Called after all data has been delivered and the message parsing has
+ * been completed. This will be called exactly once, after any other call.
+ * startPart(string partNum, object headers)
+ * Called after the headers for a body part (including the top-level
+ * message) have been parsed. The first parameter is the part number (see
+ * the discussion on part numbering). The second parameter is an instance
+ * of StructuredHeaders that represents all of the headers for the part.
+ * endPart(string partNum)
+ * Called after all of the data for a body part (including sub-parts) has
+ * been parsed. The first parameter is the part number.
+ * deliverPartData(string partNum, {string,typedarray} data)
+ * Called when some data for a body part has been delivered. The first
+ * parameter is the part number. The second parameter is the data which is
+ * being delivered; the exact type of this data depends on the options
+ * used. Note that data is only delivered for leaf body parts.
+ *
+ * The second parameter, options, is an optional object containing the options
+ * for the parser. The following are the options that the parser may use:
+ * pruneat: <string> [default=""]
+ * Treat the message as starting at the given part number, so that no parts
+ * above <string> are returned.
+ * bodyformat: one of {none, raw, nodecode, decode} [default=nodecode]
+ * How to return the bodies of parts:
+ * none: no part data is returned
+ * raw: the body of the part is passed through raw
+ * nodecode: the body is passed through without decoding QP/Base64
+ * decode: quoted-printable and base64 are fully decoded
+ * strformat: one of {binarystring, unicode, typedarray} [default=binarystring]
+ * How to treat output strings:
+ * binarystring: Data is a JS string with chars in the range [\x00-\xff]
+ * unicode: Data for text parts is converted to UTF-16; data for other
+ * parts is a typed array buffer, akin to typedarray.
+ * typedarray: Data is a JS typed array buffer
+ * charset: <string> [default=""]
+ * What charset to assume if no charset information is explicitly provided.
+ * This only matters if strformat is unicode. See above note on charsets
+ * for more details.
+ * force-charset: <boolean> [default=false]
+ * If true, this coerces all types to use the charset option, even if the
+ * message specifies a different content-type.
+ * stripcontinuations: <boolean> [default=true]
+ * If true, then the newlines in headers are removed in the returned
+ * header objects.
+ * onerror: <function(thrown error)> [default = nop-function]
+ * An error function that is called if an emitter callback throws an error.
+ * By default, such errors are swallowed by the parser. If you want the
+ * parser itself to throw an error, rethrow it via the onerror function.
+ * decodeSubMessages: <boolean> [default=true]
+ * Parse attached messages (message/rfc822, message/global & message/news)
+ * and return all of their mime data instead of returning their content
+ * as regular attachments.
+ */
+ function MimeParser(emitter, options) {
+ // The actual emitter
+ this._emitter = emitter;
+ // Options for the parser (those listed here are defaults)
+ this._options = {
+ decodeSubMessages: true,
+ pruneat: "",
+ bodyformat: "nodecode",
+ strformat: "binarystring",
+ stripcontinuations: true,
+ charset: "",
+ "force-charset": false,
+ onerror(error) {},
+ };
+ // Load the options as a copy here (prevents people from changing on the fly).
+ if (options) {
+ for (var opt in options) {
+ this._options[opt] = options[opt];
+ }
+ }
+
+ // Ensure that the error function is in fact a function
+ if (typeof this._options.onerror != "function") {
+ throw new Error("onerror callback must be a function");
+ }
+
+ // Reset the parser
+ this.resetParser();
+ }
+
+ /**
+ * Resets the parser to read a new message. This method need not be called
+ * immediately after construction.
+ */
+ MimeParser.prototype.resetParser = function () {
+ // Current parser state
+ this._state = PARSING_HEADERS;
+ // Input data that needs to be held for buffer conditioning
+ this._holdData = "";
+ // Complete collection of headers (also used to accumulate _headerData)
+ this._headerData = "";
+ // Whether or not emitter.startMessage has been called
+ this._triggeredCall = false;
+
+ // Splitting input
+ this._splitRegex = this._handleSplit = undefined;
+ // Subparsing
+ this._subparser = this._subPartNum = undefined;
+ // Data that has yet to be consumed by _convertData
+ this._savedBuffer = "";
+ // Convert data
+ this._convertData = undefined;
+ // String decoder
+ this._decoder = undefined;
+ };
+
+ /**
+ * Deliver a buffer of data to the parser.
+ *
+ * @param buffer {BinaryString} The raw data to add to the message.
+ */
+ MimeParser.prototype.deliverData = function (buffer) {
+ // In ideal circumstances, we'd like to parse the message all at once. In
+ // reality, though, data will be coming to us in packets. To keep the amount
+ // of saved state low, we want to make basic guarantees about how packets get
+ // delivered. Our basic model is a twist on line-buffering, as the format of
+ // MIME and messages make it hard to not do so: we can handle multiple lines
+ // at once. To ensure this, we start by conditioning the packet by
+ // withholding data to make sure that the internal deliveries have the
+ // guarantees. This implies that we need to do the following steps:
+ // 1. We don't know if a `\r' comes from `\r\n' or the old mac line ending
+ // until we see the next character. So withhold the last `\r'.
+ // 2. Ensure that every packet ends on a newline. So scan for the end of the
+ // line and withhold until the \r\n comes through.
+ // [Note that this means that an input message that uses \r line endings and
+ // is being passed to us via a line-buffered input is going to have most of
+ // its data being withhold until the next buffer. Since \r is so uncommon of
+ // a line ending in modern times, this is acceptable lossage.]
+ // 3. Eliminate empty packets.
+
+ // Add in previously saved data
+ if (this._holdData) {
+ buffer = this._holdData + buffer;
+ this._holdData = "";
+ }
+
+ // Condition the input, so that we get the multiline-buffering mentioned in
+ // the above comment.
+ if (buffer.length > 0) {
+ [buffer, this._holdData] = conditionToEndOnCRLF(buffer);
+ }
+
+ // Ignore 0-length buffers.
+ if (buffer.length == 0) {
+ return;
+ }
+
+ // Signal the beginning, if we haven't done so.
+ if (!this._triggeredCall) {
+ this._callEmitter("startMessage");
+ this._triggeredCall = true;
+ }
+
+ // Finally, send it the internal parser.
+ this._dispatchData("", buffer, true);
+ };
+
+ /**
+ * Ensure that a set of data always ends in an end-of-line character.
+ *
+ * @param buffer {BinaryString} The data with no guarantees about where it ends.
+ * @returns {BinaryString[]} An array of 2 binary strings where the first string
+ * ends in a newline and the last string contains the
+ * text in buffer following the first string.
+ */
+ function conditionToEndOnCRLF(buffer) {
+ // Find the last occurrence of '\r' or '\n' to split the string. However, we
+ // don't want to consider '\r' if it is the very last character, as we need
+ // the next packet to tell if the '\r' is the beginning of a CRLF or a line
+ // ending by itself.
+ let lastCR = buffer.lastIndexOf("\r", buffer.length - 2);
+ let lastLF = buffer.lastIndexOf("\n");
+ let end = lastLF > lastCR ? lastLF : lastCR;
+ return [buffer.substring(0, end + 1), buffer.substring(end + 1)];
+ }
+
+ /**
+ * Tell the parser that all of the data has been delivered.
+ *
+ * This will flush all of the internal state of the parser.
+ */
+ MimeParser.prototype.deliverEOF = function () {
+ // Start of input buffered too long? Call start message now.
+ if (!this._triggeredCall) {
+ this._triggeredCall = true;
+ this._callEmitter("startMessage");
+ }
+ // Force a flush of all of the data.
+ if (this._holdData) {
+ this._dispatchData("", this._holdData, true);
+ }
+ this._dispatchEOF("");
+ // Signal to the emitter that we're done.
+ this._callEmitter("endMessage");
+ };
+
+ /**
+ * Calls a method on the emitter safely.
+ *
+ * This method ensures that errors in the emitter call won't cause the parser
+ * to exit with an error, unless the user wants it to.
+ *
+ * @param funcname {String} The function name to call on the emitter.
+ * @param args... Extra arguments to pass into the emitter callback.
+ */
+ MimeParser.prototype._callEmitter = function (funcname, ...args) {
+ if (this._emitter && funcname in this._emitter) {
+ if (args.length > 0 && this._willIgnorePart(args[0])) {
+ // partNum is always the first argument, so check to make sure that it
+ // satisfies our emitter's pruneat requirement.
+ return;
+ }
+ try {
+ this._emitter[funcname].apply(this._emitter, args);
+ } catch (e) {
+ // We ensure that the onerror attribute in options is a function, so this
+ // is always safe.
+ this._options.onerror(e);
+ }
+ }
+ };
+
+ /**
+ * Helper function to decide if a part's output will never be seen.
+ *
+ * @param part {String} The number of the part.
+ * @returns {boolean} True if the emitter is not interested in this part.
+ */
+ MimeParser.prototype._willIgnorePart = function (part) {
+ if (this._options.pruneat) {
+ let match = this._options.pruneat;
+ let start = part.substr(0, match.length);
+ // It needs to start with and follow with a new part indicator
+ // (i.e., don't let 10 match with 1, but let 1.1 or 1$ do so)
+ if (
+ start != match ||
+ (match.length < part.length && !"$.".includes(part[match.length]))
+ ) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // MIME parser core
+ // ----------------
+
+ // This MIME parser is a stateful parser; handling of the MIME tree is mostly
+ // done by creating new parsers and feeding data to them manually. In parallel
+ // to the externally-visible deliverData and deliverEOF, the two methods
+ // _dispatchData and _dispatchEOF are the internal counterparts that do the
+ // main work of moving data to where it needs to go; helper functions are used
+ // to handle translation.
+ //
+ // The overall flow of the parser is this. First, it buffers all of the data
+ // until the dual-CRLF pattern is noticed. Once that is found, it parses the
+ // entire header chunk at once. As a result of header parsing, the parser enters
+ // one of three modes for handling data, and uses a special regex to change
+ // modes and handle state changes. Specific details about the states the parser
+ // can be in are as follows:
+ // PARSING_HEADERS: The input buffer is concatenated to the currently-received
+ // text, which is then searched for the CRLFCRLF pattern. If found, the data
+ // is split at this boundary; the first chunk is parsed using _parseHeaders,
+ // and the second chunk will fall through to buffer processing. After
+ // splitting, the headers are deliverd via the emitter, and _startBody is
+ // called to set up state for the parser.
+ // SEND_TO_BLACK_HOLE: All data in the input is ignored.
+ // SEND_TO_EMITTER: All data is passed into the emitter, if it is desired.
+ // Data can be optionally converted with this._convertData.
+ // SEND_TO_SUBPARSER: All data is passed into the subparser's _dispatchData
+ // method, using _subPartNum as the part number and _subparser as the object
+ // to call. Data can be optionally converted first with this._convertData.
+ //
+ // Additional state modifications can be done using a regex in _splitRegex and
+ // the callback method this._handleSplit(partNum, regexResult). The _handleSplit
+ // callback is free to do any modification to the current parser, including
+ // modifying the _splitRegex value. Packet conditioning guarantees that every
+ // buffer string passed into _dispatchData will have started immediately after a
+ // newline character in the fully assembled message.
+ //
+ // The this._convertData method, if present, is expected to return an array of
+ // two values, [{typedarray, string} decoded_buffer, string unused_buffer], and
+ // has as its arguments (string buffer, bool moreToCome).
+ //
+ // The header parsing by itself does very little parsing, only parsing as if all
+ // headers were unstructured fields. Values are munged so that embedded newlines
+ // are stripped and the result is also trimmed. Headers themselves are
+ // canonicalized into lower-case.
+
+ // Parser states. See the large comment above.
+ var PARSING_HEADERS = 1;
+ var SEND_TO_BLACK_HOLE = 2;
+ var SEND_TO_EMITTER = 3;
+ var SEND_TO_SUBPARSER = 4;
+
+ /**
+ * Main dispatch for incoming packet data.
+ *
+ * The incoming data needs to have been sanitized so that each packet begins on
+ * a newline boundary. The part number for the current parser also needs to be
+ * passed in. The checkSplit parameter controls whether or not the data in
+ * buffer needs to be checked against _splitRegex; this is used internally for
+ * the mechanics of splitting and should otherwise always be true.
+ *
+ * @param partNum {String} The part number being currently parsed.
+ * @param buffer {BinaryString} The text (conditioned as mentioned above) to
+ * pass to the parser.
+ * @param checkSplit {Boolean} - If true, split the text using _splitRegex.
+ * This is set to false internally to handle
+ * low-level splitting details.
+ */
+ MimeParser.prototype._dispatchData = function (
+ partNum,
+ buffer,
+ checkSplit
+ ) {
+ // Are we parsing headers?
+ if (this._state == PARSING_HEADERS) {
+ this._headerData += buffer;
+ // Find the end of the headers--either it's a CRLF at the beginning (in
+ // which case we have no headers), or it's a pair of CRLFs.
+ let result = /(?:^(?:\r\n|[\r\n]))|(\r\n|[\r\n])\1/.exec(
+ this._headerData
+ );
+ if (result != null) {
+ // If we found the end of headers, split the data at this point and send
+ // the stuff after the double-CRLF into the later body parsing.
+ let headers = this._headerData.substr(0, result.index);
+ buffer = this._headerData.substring(result.index + result[0].length);
+ this._headerData = headers;
+ this._headers = this._parseHeaders();
+ this._callEmitter("startPart", partNum, this._headers);
+ this._startBody(partNum);
+ } else {
+ return;
+ }
+ }
+
+ // We're in the middle of the body. Start by testing the split regex, to see
+ // if there are many things that need to be done.
+ if (checkSplit && this._splitRegex) {
+ let splitResult = this._splitRegex.exec(buffer);
+ if (splitResult) {
+ // Pass the text before the split through the current state.
+ let start = splitResult.index,
+ len = splitResult[0].length;
+ if (start > 0) {
+ this._dispatchData(partNum, buffer.substr(0, start), false);
+ }
+
+ // Tell the handler that we've seen the split. Note that this can change
+ // any method on `this'.
+ this._handleSplit(partNum, splitResult);
+
+ // Send the rest of the data to where it needs to go. There could be more
+ // splits in the data, so watch out!
+ buffer = buffer.substring(start + len);
+ if (buffer.length > 0) {
+ this._dispatchData(partNum, buffer, true);
+ }
+ return;
+ }
+ }
+
+ // Where does the data go?
+ if (this._state == SEND_TO_BLACK_HOLE) {
+ // Don't send any data when going to the black hole.
+ } else if (this._state == SEND_TO_EMITTER) {
+ // Don't pass body data if the format is to be none
+ let passData = this._options.bodyformat != "none";
+ if (!passData || this._willIgnorePart(partNum)) {
+ return;
+ }
+ buffer = this._applyDataConversion(buffer, this._options.strformat);
+ if (buffer.length > 0) {
+ this._callEmitter("deliverPartData", partNum, buffer);
+ }
+ } else if (this._state == SEND_TO_SUBPARSER) {
+ buffer = this._applyDataConversion(buffer, "binarystring");
+ if (buffer.length > 0) {
+ this._subparser._dispatchData(this._subPartNum, buffer, true);
+ }
+ }
+ };
+
+ /**
+ * Output data using the desired output format, saving data if data conversion
+ * needs extra data to be saved.
+ *
+ * @param buf {BinaryString} The data to be sent to the output.
+ * @param type {String} - The type of the data to output. Valid values are
+ * the same as the strformat option.
+ * @returns Coerced and converted data that can be sent to the emitter or
+ * subparser.
+ */
+ MimeParser.prototype._applyDataConversion = function (buf, type) {
+ // If we need to convert data, do so.
+ if (this._convertData) {
+ // Prepend leftover data from the last conversion.
+ buf = this._savedBuffer + buf;
+ [buf, this._savedBuffer] = this._convertData(buf, true);
+ }
+ return this._coerceData(buf, type, false);
+ };
+
+ /**
+ * Coerce the input buffer into the given output type.
+ *
+ * @param buffer {BinaryString|Uint8Array} The data to be converted.
+ * @param type {String} The type to convert the data to.
+ * @param more {boolean} If true, this function will never be
+ * called again.
+ * @returns {BinaryString | string | Uint8Array} The desired output format.
+ */
+ // Coerces the buffer (a string or typedarray) into a given type
+ MimeParser.prototype._coerceData = function (buffer, type, more) {
+ if (typeof buffer == "string") {
+ // string -> binarystring is a nop
+ if (type == "binarystring") {
+ return buffer;
+ }
+ // Either we're going to array or unicode. Both people need the array
+ var typedarray = mimeutils.stringToTypedArray(buffer);
+ // If it's unicode, do the coercion from the array
+ // If its typedarray, just return the synthesized one
+ return type == "unicode"
+ ? this._coerceData(typedarray, "unicode", more)
+ : typedarray;
+ } else if (type == "binarystring") {
+ // Doing array -> binarystring
+ return mimeutils.typedArrayToString(buffer);
+ } else if (type == "unicode") {
+ // Doing array-> unicode: Use the decoder set up earlier to convert
+ if (this._decoder) {
+ return this._decoder.decode(buffer, { stream: more });
+ }
+ // If there is no charset, just return the typed array instead.
+ return buffer;
+ }
+ throw new Error("Invalid type: " + type);
+ };
+
+ /**
+ * Signal that no more data will be dispatched to this parser.
+ *
+ * @param partNum {String} The part number being currently parsed.
+ */
+ MimeParser.prototype._dispatchEOF = function (partNum) {
+ if (this._state == PARSING_HEADERS) {
+ // Unexpected EOF in headers. Parse them now and call startPart/endPart
+ this._headers = this._parseHeaders();
+ this._callEmitter("startPart", partNum, this._headers);
+ } else if (this._state == SEND_TO_SUBPARSER) {
+ // Pass in any lingering data
+ if (this._convertData && this._savedBuffer) {
+ this._subparser._dispatchData(
+ this._subPartNum,
+ this._convertData(this._savedBuffer, false)[0],
+ true
+ );
+ }
+ this._subparser._dispatchEOF(this._subPartNum);
+ // Clean up after ourselves
+ this._subparser = null;
+ } else if (this._convertData && this._savedBuffer) {
+ // Convert lingering data
+ let [buffer] = this._convertData(this._savedBuffer, false);
+ buffer = this._coerceData(buffer, this._options.strformat, false);
+ if (buffer.length > 0) {
+ this._callEmitter("deliverPartData", partNum, buffer);
+ }
+ }
+
+ // We've reached EOF for this part; tell the emitter
+ this._callEmitter("endPart", partNum);
+ };
+
+ /**
+ * Produce a dictionary of all headers as if they were unstructured fields.
+ *
+ * @returns {StructuredHeaders} The structured header objects for the header
+ * block.
+ */
+ MimeParser.prototype._parseHeaders = function () {
+ let headers = new StructuredHeaders(this._headerData, this._options);
+
+ // Fill the headers.contentType parameter of headers.
+ let contentType = headers.get("Content-Type");
+ if (typeof contentType === "undefined") {
+ contentType = headerparser.parseStructuredHeader(
+ "Content-Type",
+ this._defaultContentType || "text/plain"
+ );
+ Object.defineProperty(headers, "contentType", {
+ get() {
+ return contentType;
+ },
+ });
+ } else {
+ Object.defineProperty(headers, "contentType", { configurable: false });
+ }
+
+ // Find the charset for the current part. If the user requested a forced
+ // conversion, use that first. Otherwise, check the content-type for one and
+ // fallback to a default if it is not present.
+ let charset = "";
+ if (this._options["force-charset"]) {
+ charset = this._options.charset;
+ } else if (contentType.has("charset")) {
+ charset = contentType.get("charset");
+ } else {
+ charset = this._options.charset;
+ }
+ headers.charset = charset;
+
+ // Retain a copy of the charset so that users don't override our decision for
+ // decoding body parts.
+ this._charset = charset;
+ return headers;
+ };
+
+ /**
+ * Initialize the parser state for the body of this message.
+ *
+ * @param partNum {String} The part number being currently parsed.
+ */
+ MimeParser.prototype._startBody = function (partNum) {
+ let contentType = this._headers.contentType;
+
+ // Should the bodyformat be raw, we just want to pass through all data without
+ // trying to interpret it.
+ if (
+ this._options.bodyformat == "raw" &&
+ partNum == this._options.pruneat
+ ) {
+ this._state = SEND_TO_EMITTER;
+ return;
+ }
+
+ // The output depents on the content-type. Basic rule of thumb:
+ // 1. Discrete media types (text, video, audio, image, application) are passed
+ // through with no alterations beyond Content-Transfer-Encoding unpacking.
+ // 2. Everything with a media type of multipart is treated the same.
+ // 3. Any message/* type that acts like a mail message (rfc822, news, global)
+ // is parsed as a header/body pair again. Most of the other message/* types
+ // have similar structures, but they don't have cascading child subparts,
+ // so it's better to pass their entire contents to the emitter and let the
+ // consumer deal with them.
+ // 4. For untyped data, there needs to be no Content-Type header. This helps
+ // avoid false positives.
+ if (contentType.mediatype == "multipart") {
+ // If there's no boundary type, everything will be part of the prologue of
+ // the multipart message, so just feed everything into a black hole.
+ if (!contentType.has("boundary")) {
+ this._state = SEND_TO_BLACK_HOLE;
+ return;
+ }
+ // The boundary of a multipart message needs to start with -- and be at the
+ // beginning of the line. If -- is after the boundary, it represents the
+ // terminator of the multipart. After the line, there may be only whitespace
+ // and then the CRLF at the end. Since the CRLFs in here are necessary for
+ // distinguishing the parts, they are not included in the subparts, so we
+ // need to capture them in the regex as well to prevent them leaking out.
+ this._splitRegex = new RegExp(
+ "(\r\n|[\r\n]|^)--" +
+ contentType.get("boundary").replace(/[\\^$*+?.()|{}[\]]/g, "\\$&") +
+ "(--)?[ \t]*(?:\r\n|[\r\n]|$)"
+ );
+ this._handleSplit = this._whenMultipart;
+ this._subparser = new MimeParser(this._emitter, this._options);
+ // multipart/digest defaults to message/rfc822 instead of text/plain
+ if (contentType.subtype == "digest") {
+ this._subparser._defaultContentType = "message/rfc822";
+ }
+
+ // All text before the first boundary and after the closing boundary are
+ // supposed to be ignored ("must be ignored", according to RFC 2046 §5.1.1);
+ // in accordance with these wishes, ensure they don't get passed to any
+ // deliverPartData.
+ this._state = SEND_TO_BLACK_HOLE;
+
+ // Multipart MIME messages stipulate that the final CRLF before the boundary
+ // delimiter is not matched. When the packet ends on a CRLF, we don't know
+ // if the next text could be the boundary. Therefore, we need to withhold
+ // the last line of text to be sure of what's going on. The _convertData is
+ // how we do this, even though we're not really converting any data.
+ this._convertData = function (buffer, more) {
+ let splitPoint = buffer.length;
+ if (more) {
+ if (buffer.charAt(splitPoint - 1) == "\n") {
+ splitPoint--;
+ }
+ if (splitPoint >= 0 && buffer.charAt(splitPoint - 1) == "\r") {
+ splitPoint--;
+ }
+ }
+ let res = conditionToEndOnCRLF(buffer.substring(0, splitPoint));
+ let preLF = res[0];
+ let rest = res[1];
+ return [preLF, rest + buffer.substring(splitPoint)];
+ };
+ } else if (
+ (this._options.decodeSubMessages || this._willIgnorePart(partNum)) &&
+ (contentType.type == "message/rfc822" ||
+ contentType.type == "message/global" ||
+ contentType.type == "message/news")
+ ) {
+ // The subpart is just another header/body pair that goes to EOF, so just
+ // return the parse from that blob
+ this._state = SEND_TO_SUBPARSER;
+ this._subPartNum = partNum + "$";
+ this._subparser = new MimeParser(this._emitter, this._options);
+
+ // So, RFC 6532 happily allows message/global types to have CTE applied.
+ // This means that subparts would need to be decoded to determine their
+ // contents properly. There seems to be some evidence that message/rfc822
+ // that is illegally-encoded exists in the wild, so be lenient and decode
+ // for any message/* type that gets here.
+ let cte = this._extractHeader("content-transfer-encoding", "");
+ if (cte in ContentDecoders) {
+ this._convertData = ContentDecoders[cte];
+ }
+ } else {
+ // Okay, we just have to feed the data into the output
+ this._state = SEND_TO_EMITTER;
+ if (this._options.bodyformat == "decode") {
+ // If we wish to decode, look it up in one of our decoders.
+ let cte = this._extractHeader("content-transfer-encoding", "");
+ if (cte in ContentDecoders) {
+ this._convertData = ContentDecoders[cte];
+ }
+ }
+ }
+
+ // Set up the encoder for charset conversions; only do this for text parts.
+ // Other parts are almost certainly binary, so no translation should be
+ // applied to them.
+ if (
+ this._options.strformat == "unicode" &&
+ contentType.mediatype == "text"
+ ) {
+ // If the charset is nonempty, initialize the decoder
+ this._decoder = null;
+ if (this._charset !== "") {
+ try {
+ this._decoder = new MimeTextDecoder(this._charset);
+ } catch (e) {}
+ }
+ if (!this._decoder) {
+ // There's no charset we can use for decoding, so pass through as an
+ // identity encoder or otherwise this._coerceData will complain.
+ this._decoder = {
+ decode(buffer) {
+ return MimeParser.prototype._coerceData(
+ buffer,
+ "binarystring",
+ true
+ );
+ },
+ };
+ }
+ } else {
+ this._decoder = null;
+ }
+ };
+
+ // Internal split handling for multipart messages.
+ /**
+ * When a multipary boundary is found, handle the process of managing the
+ * subparser state. This is meant to be used as a value for this._handleSplit.
+ *
+ * @param partNum {String} The part number being currently parsed.
+ * @param lastResult {Array} - The result of the regular expression match.
+ */
+ MimeParser.prototype._whenMultipart = function (partNum, lastResult) {
+ // Fix up the part number (don't do '' -> '.4' and don't do '1' -> '14')
+ if (partNum != "") {
+ partNum += ".";
+ }
+ if (!this._subPartNum) {
+ // No count? This means that this is the first time we've seen the boundary,
+ // so do some initialization for later here.
+ this._count = 1;
+ } else {
+ // If we did not match a CRLF at the beginning of the line, strip CRLF from
+ // the saved buffer. We do this in the else block because it is not
+ // necessary for the prologue, since that gets ignored anyways.
+ if (this._savedBuffer != "" && lastResult[1] === "") {
+ let useEnd = this._savedBuffer.length - 1;
+ if (this._savedBuffer[useEnd] == "\n") {
+ useEnd--;
+ }
+ if (useEnd >= 0 && this._savedBuffer[useEnd] == "\r") {
+ useEnd--;
+ }
+ this._savedBuffer = this._savedBuffer.substring(0, useEnd + 1);
+ }
+ // If we have saved data and we matched a CRLF, pass the saved data in.
+ if (this._savedBuffer != "") {
+ this._subparser._dispatchData(
+ this._subPartNum,
+ this._savedBuffer,
+ true
+ );
+ }
+ // We've seen the boundary at least once before, so this must end a subpart.
+ // Tell that subpart that it has reached EOF.
+ this._subparser._dispatchEOF(this._subPartNum);
+ }
+ this._savedBuffer = "";
+
+ // The regex feeder has a capture on the (--)?, so if its result is present,
+ // then we have seen the terminator. Alternatively, the message may have been
+ // mangled to exclude the terminator, so also check if EOF has occurred.
+ if (lastResult[2] == undefined) {
+ this._subparser.resetParser();
+ this._state = SEND_TO_SUBPARSER;
+ this._subPartNum = partNum + this._count;
+ this._count += 1;
+ } else {
+ // Ignore the epilogue
+ this._splitRegex = null;
+ this._state = SEND_TO_BLACK_HOLE;
+ }
+ };
+
+ /**
+ * Return the structured header from the current header block, or a default if
+ * it is not present.
+ *
+ * @param name {String} The header name to get.
+ * @param dflt {String} The default MIME value of the header.
+ * @returns The structured representation of the header.
+ */
+ MimeParser.prototype._extractHeader = function (name, dflt) {
+ name = name.toLowerCase(); // Normalize name
+ return this._headers.has(name)
+ ? this._headers.get(name)
+ : headerparser.parseStructuredHeader(name, [dflt]);
+ };
+
+ var ContentDecoders = {};
+ ContentDecoders["quoted-printable"] = mimeutils.decode_qp;
+ ContentDecoders.base64 = mimeutils.decode_base64;
+
+ return MimeParser;
+ });
+ def("headeremitter", function (require) {
+ /**
+ * This module implements the code for emitting structured representations of
+ * MIME headers into their encoded forms. The code here is a companion to,
+ * but completely independent of, jsmime.headerparser: the structured
+ * representations that are used as input to the functions in this file are the
+ * same forms that would be parsed.
+ */
+
+ "use strict";
+
+ var mimeutils = require("./mimeutils");
+
+ // Get the default structured encoders and add them to the map
+ var structuredHeaders = require("./structuredHeaders");
+ var encoders = new Map();
+ var preferredSpellings = structuredHeaders.spellings;
+ for (let [header, encoder] of structuredHeaders.encoders) {
+ addStructuredEncoder(header, encoder);
+ }
+
+ // Clamp a value in the range [min, max], defaulting to def
+ // if the object[property] does not contain the value.
+ function clamp(object, property, min, max, def) {
+ if (!(property in object)) {
+ return def;
+ }
+ let value = object[property];
+ if (value < min) {
+ return min;
+ }
+ if (value > max) {
+ return max;
+ }
+ return value;
+ }
+
+ /**
+ * An object that can assemble structured header representations into their MIME
+ * representation.
+ *
+ * The character-counting portion of this class operates using individual JS
+ * characters as its representation of logical character, which is not the same
+ * as the number of octets used as UTF-8. If non-ASCII characters are to be
+ * included in headers without some form of encoding, then care should be taken
+ * to set the maximum line length to account for the mismatch between character
+ * counts and octet counts: the maximum line is 998 octets, which could be as
+ * few as 332 JS characters (non-BMP characters, although they take up 4 octets
+ * in UTF-8, count as 2 in JS strings).
+ *
+ * This code takes care to only insert line breaks at the higher-level breaking
+ * points in a header (as recommended by RFC 5322), but it may need to resort to
+ * including them more aggressively if this is not possible. If even aggressive
+ * line-breaking cannot allow a header to be emitted without violating line
+ * length restrictions, the methods will throw an exception to indicate this
+ * situation.
+ *
+ * In general, this code does not attempt to modify its input; for example, it
+ * does not attempt to change the case of any input characters, apply any
+ * Unicode normalization algorithms, or convert email addresses to ACE where
+ * applicable. The biggest exception to this rule is that most whitespace is
+ * collapsed to a single space, even in unstructured headers, while most leading
+ * and trailing whitespace is trimmed from inputs.
+ *
+ * @param {StreamHandler} handler The handler to which all output is sent.
+ * @param {Function(string)} handler.deliverData Receives encoded data.
+ * @param {Function()} handler.deliverEOF Sent when all text is sent.
+ * @param {object} options Options for the emitter.
+ * @param [options.softMargin=78] {30 <= Integer <= 900}
+ * The ideal maximum number of logical characters to include in a line, not
+ * including the final CRLF pair. Lines may exceed this margin if parameters
+ * are excessively long.
+ * @param [options.hardMargin=332] {softMargin <= Integer <= 998}
+ * The maximum number of logical characters that can be included in a line,
+ * not including the final CRLF pair. If this count would be exceeded, then
+ * an error will be thrown and encoding will not be possible.
+ * @param [options.useASCII=true] {Boolean}
+ * If true, then RFC 2047 and RFC 2231 encoding of headers will be performed
+ * as needed to retain headers as ASCII.
+ */
+ function HeaderEmitter(handler, options) {
+ // The inferred value of options.useASCII
+ this._useASCII = options.useASCII === undefined ? true : options.useASCII;
+ this._sanitizeDate =
+ options.sanitizeDate === undefined ? false : options.sanitizeDate;
+ // The handler to use.
+ this._handler = handler;
+ /**
+ * The current line being built; note that we may insert a line break in the
+ * middle to keep under the maximum line length.
+ *
+ * @type String
+ * @private
+ */
+ this._currentLine = "";
+
+ // Our bounds for soft and margins are not completely arbitrary. The minimum
+ // amount we need to encode is 20 characters, which can encode a single
+ // non-BMP character with RFC 2047. The value of 30 is chosen to give some
+ // breathing room for delimiters or other unbreakable characters. The maximum
+ // length is 998 octets, per RFC 5322; soft margins are slightly lower to
+ // allow for breathing room as well. The default of 78 for the soft margin is
+ // recommended by RFC 5322.
+ this._softMargin = clamp(options, "softMargin", 30, 900, 78);
+ this._hardMargin = clamp(
+ options,
+ "hardMargin",
+ this._softMargin,
+ 998,
+ 998
+ );
+
+ /**
+ * The index of the last preferred breakable position in the current line.
+ *
+ * @type Integer
+ * @private
+ */
+ this._preferredBreakpoint = 0;
+ }
+
+ // Low-level methods
+ // -----------------
+
+ // Explanation of the emitter internals:
+ // RFC 5322 requires that we wrap our lines, ideally at 78 characters and at
+ // least by 998 octets. We can't wrap in arbitrary places, but wherever CFWS is
+ // valid... and ideally wherever clients are likely to expect it. In theory, we
+ // can break between every token (this is how RFC 822 operates), but, in RFC
+ // 5322, many of those breaks are relegated to obsolete productions, mostly
+ // because it is common to not properly handle breaks in those locations.
+ //
+ // So how do we do line breaking? The algorithm we implement is greedy, to
+ // simplify implementation. There are two margins: the soft margin, which we
+ // want to keep within, and the hard margin, which we absolutely have to keep
+ // within. There are also two kinds of break points: preferred and emergency.
+ // As long as we keep the line within the hard margin, we will only break at
+ // preferred breakpoints; emergency breakpoints are only used if we would
+ // otherwise exceed the hard margin.
+ //
+ // For illustration, here is an example header and where these break points are
+ // located:
+ //
+ // To: John "The Rock" Smith <jsmith@a.long.domain.invalid>
+ // Preferred: ^ ^ ^
+ // Emergency: ^ ^ ^ ^^ ^ ^ ^ ^ ^
+ //
+ // Preferred breakpoints are indicated by setting the mayBreakAfter parameter of
+ // addText to true, while emergency breakpoints are set after every token passed
+ // into addText. This is handled implicitly by only adding text to _currentLine
+ // if it ends in an emergency breakpoint.
+ //
+ // Internally, the code keeps track of margins by use of two variables. The
+ // _softMargin and _hardMargin variables encode the positions at which code must
+ // absolutely break, and are set up from the initial options parameter. Breaking
+ // happens when _currentLine.length approaches these values, as mentioned above.
+
+ /**
+ * Send a header line consisting of the first N characters to the handler.
+ *
+ * If the count parameter is missing, then we presume that the current header
+ * value being emitted is done and therefore we should not send a continuation
+ * space. Otherwise, we presume that we're still working, so we will send the
+ * continuation space.
+ *
+ * @private
+ * @param [count] {Integer} The number of characters in the current line to
+ * include before wrapping.
+ */
+ HeaderEmitter.prototype._commitLine = function (count) {
+ let isContinuing = typeof count !== "undefined";
+
+ // Split at the point, and lop off whitespace immediately before and after.
+ let firstN, lastN;
+ if (isContinuing) {
+ firstN = this._currentLine.slice(0, count).trimRight();
+ lastN = this._currentLine.slice(count).trimLeft();
+ } else {
+ firstN = this._currentLine.trimRight();
+ lastN = "";
+ }
+
+ // Send the line plus the final CRLF.
+ this._handler.deliverData(firstN + "\r\n");
+
+ // Fill the start of the line with the new data.
+ this._currentLine = lastN;
+
+ // If this is a continuation, add an extra space at the beginning of the line.
+ // Adjust the breakpoint shift amount as well.
+ if (isContinuing) {
+ this._currentLine = " " + this._currentLine;
+ }
+
+ // We will always break at a point at or after the _preferredBreakpoint, if it
+ // exists, so this always gets reset to 0.
+ this._preferredBreakpoint = 0;
+ };
+
+ /**
+ * Reserve at least length characters in the current line. If there aren't
+ * enough characters, insert a line break.
+ *
+ * @private
+ * @param length {Integer} The number of characters to reserve space for.
+ * @returns {boolean} Whether or not there is enough space for length characters.
+ */
+ HeaderEmitter.prototype._reserveTokenSpace = function (length) {
+ // We are not going to do a sanity check that length is within the wrap
+ // margins. The rationale is that this lets code simply call this function to
+ // force a higher-level line break than normal preferred line breaks (see
+ // addAddress for an example use). The text that would be added may need to be
+ // itself broken up, so it might not need all the length anyways, but it
+ // starts the break already.
+
+ // If we have enough space, we don't need to do anything.
+ if (this._currentLine.length + length <= this._softMargin) {
+ return true;
+ }
+
+ // If we have a preferred breakpoint, commit the line at that point, and see
+ // if that is sufficient line-breaking.
+ if (this._preferredBreakpoint > 0) {
+ this._commitLine(this._preferredBreakpoint);
+ if (this._currentLine.length + length <= this._softMargin) {
+ return true;
+ }
+ }
+
+ // At this point, we can no longer keep within the soft margin. Let us see if
+ // we can fit within the hard margin.
+ if (this._currentLine.length + length <= this._hardMargin) {
+ return true;
+ }
+
+ // Adding the text to length would violate the hard margin as well. Break at
+ // the last emergency breakpoint.
+ if (this._currentLine.length > 0) {
+ this._commitLine(this._currentLine.length);
+ }
+
+ // At this point, if there is still insufficient room in the hard margin, we
+ // can no longer do anything to encode this word. Bail.
+ return this._currentLine.length + length <= this._hardMargin;
+ };
+
+ /**
+ * Adds a block of text to the current header, inserting a break if necessary.
+ * If mayBreakAfter is true and text does not end in whitespace, a single space
+ * character may be added to the output. If the text could not be added without
+ * violating line length restrictions, an error is thrown instead.
+ *
+ * @protected
+ * @param {string} text The text to add to the output.
+ * @param {boolean} mayBreakAfter If true, the end of this text is a preferred
+ * breakpoint.
+ */
+ HeaderEmitter.prototype.addText = function (text, mayBreakAfter) {
+ // Try to reserve space for the tokens. If we can't, give up.
+ if (!this._reserveTokenSpace(text.length)) {
+ throw new Error("Cannot encode " + text + " due to length.");
+ }
+
+ this._currentLine += text;
+ if (mayBreakAfter) {
+ // Make sure that there is an extra space if text could break afterwards.
+ this._preferredBreakpoint = this._currentLine.length;
+ if (text[text.length - 1] != " ") {
+ this._currentLine += " ";
+ }
+ }
+ };
+
+ /**
+ * Adds a block of text that may need quoting if it contains some character in
+ * qchars. If it is already quoted, no quoting will be applied. If the text
+ * cannot be added without violating maximum line length, an error is thrown
+ * instead.
+ *
+ * @protected
+ * @param {string} text The text to add to the output.
+ * @param {string} qchars The set of characters that cannot appear
+ * outside of a quoted string.
+ * @param {boolean} mayBreakAfter If true, the end of this text is a preferred
+ * breakpoint.
+ */
+ HeaderEmitter.prototype.addQuotable = function (
+ text,
+ qchars,
+ mayBreakAfter
+ ) {
+ // No text -> no need to be quoted (prevents strict warning errors).
+ if (text.length == 0) {
+ return;
+ }
+
+ // Figure out if we need to quote the string. Don't quote a string which
+ // already appears to be quoted.
+ let needsQuote = false;
+
+ if (!(text[0] == '"' && text[text.length - 1] == '"') && qchars != "") {
+ for (let i = 0; i < text.length; i++) {
+ if (qchars.includes(text[i])) {
+ needsQuote = true;
+ break;
+ }
+ }
+ }
+
+ if (needsQuote) {
+ text = '"' + text.replace(/["\\]/g, "\\$&") + '"';
+ }
+ this.addText(text, mayBreakAfter);
+ };
+
+ /**
+ * Adds a block of text that corresponds to the phrase production in RFC 5322.
+ * Such text is a sequence of atoms, quoted-strings, or RFC-2047 encoded-words.
+ * This method will preprocess input to normalize all space sequences to a
+ * single space. If the text cannot be added without violating maximum line
+ * length, an error is thrown instead.
+ *
+ * @protected
+ * @param {string} text The text to add to the output.
+ * @param {string} qchars The set of characters that cannot appear
+ * outside of a quoted string.
+ * @param {boolean} mayBreakAfter If true, the end of this text is a preferred
+ * breakpoint.
+ */
+ HeaderEmitter.prototype.addPhrase = function (text, qchars, mayBreakAfter) {
+ // Collapse all whitespace spans into a single whitespace node.
+ text = text.replace(/[ \t\r\n]+/g, " ");
+
+ // If we have non-ASCII text, encode it using RFC 2047.
+ if (this._useASCII && nonAsciiRe.test(text)) {
+ this.encodeRFC2047Phrase(text, mayBreakAfter);
+ return;
+ }
+
+ // If quoting the entire string at once could fit in the line length, then do
+ // so. The check here is very loose, but this will inform is if we are going
+ // to definitely overrun the soft margin.
+ if (this._currentLine.length + text.length < this._softMargin) {
+ try {
+ this.addQuotable(text, qchars, mayBreakAfter);
+ // If we don't have a breakpoint, and the text is encoded as a sequence of
+ // atoms (and not a quoted-string), then make the last space we added a
+ // breakpoint, regardless of the mayBreakAfter setting.
+ if (this._preferredBreakpoint == 0 && text.includes(" ")) {
+ if (this._currentLine[this._currentLine.length - 1] != '"') {
+ this._preferredBreakpoint = this._currentLine.lastIndexOf(" ");
+ }
+ }
+ return;
+ } catch (e) {
+ // If we get an error at this point, we failed to add the quoted string
+ // because the string was too long. Fall through to the case where we know
+ // that the input was too long to begin with.
+ }
+ }
+
+ // If the text is too long, split the quotable string at space boundaries and
+ // add each word individually. If we still can't add all those words, there is
+ // nothing that we can do.
+ let words = text.split(" ");
+ for (let i = 0; i < words.length; i++) {
+ this.addQuotable(
+ words[i],
+ qchars,
+ i == words.length - 1 ? mayBreakAfter : true
+ );
+ }
+ };
+
+ // A regular expression for characters that need to be encoded.
+ var nonAsciiRe = /[^\x20-\x7e]/;
+
+ // The beginnings of RFC 2047 encoded-word
+ var b64Prelude = "=?UTF-8?B?",
+ qpPrelude = "=?UTF-8?Q?";
+
+ // A list of ASCII characters forbidden in RFC 2047 encoded-words
+ var qpForbidden = "\"#$%&'(),.:;<=>?@[\\]^_`{|}~";
+
+ var hexString = "0123456789ABCDEF";
+
+ /**
+ * Add a block of text as a single RFC 2047 encoded word. This does not try to
+ * split words if they are too long.
+ *
+ * @private
+ * @param {Uint8Array} encodedText - The octets to encode.
+ * @param {boolean} useQP If true, use quoted-printable; if false,
+ * use base64.
+ * @param {boolean} mayBreakAfter If true, the end of this text is a
+ * preferred breakpoint.
+ */
+ HeaderEmitter.prototype._addRFC2047Word = function (
+ encodedText,
+ useQP,
+ mayBreakAfter
+ ) {
+ let binaryString = mimeutils.typedArrayToString(encodedText);
+ let token;
+ if (useQP) {
+ token = qpPrelude;
+ for (let i = 0; i < encodedText.length; i++) {
+ if (
+ encodedText[i] < 0x20 ||
+ encodedText[i] >= 0x7f ||
+ qpForbidden.includes(binaryString[i])
+ ) {
+ let ch = encodedText[i];
+ token += "=" + hexString[(ch & 0xf0) >> 4] + hexString[ch & 0x0f];
+ } else if (binaryString[i] == " ") {
+ token += "_";
+ } else {
+ token += binaryString[i];
+ }
+ }
+ token += "?=";
+ } else {
+ token = b64Prelude + btoa(binaryString) + "?=";
+ }
+ this.addText(token, mayBreakAfter);
+ };
+
+ /**
+ * Add a block of text as potentially several RFC 2047 encoded-word tokens.
+ *
+ * @protected
+ * @param {string} text The text to add to the output.
+ * @param {boolean} mayBreakAfter If true, the end of this text is a preferred
+ * breakpoint.
+ */
+ HeaderEmitter.prototype.encodeRFC2047Phrase = function (
+ text,
+ mayBreakAfter
+ ) {
+ // Start by encoding the text into UTF-8 directly.
+ let encodedText = new TextEncoder("UTF-8").encode(text);
+
+ // Make sure there's enough room for a single token.
+ let minLineLen = b64Prelude.length + 10; // Eight base64 characters plus ?=
+ if (!this._reserveTokenSpace(minLineLen)) {
+ this._commitLine(this._currentLine.length);
+ }
+
+ // Try to encode as much UTF-8 text as possible in each go.
+ let b64Len = 0,
+ qpLen = 0,
+ start = 0;
+ let maxChars =
+ this._softMargin - this._currentLine.length - (b64Prelude.length + 2);
+ for (let i = 0; i < encodedText.length; i++) {
+ let b64Inc = 0,
+ qpInc = 0;
+ // The length we need for base64 is ceil(length / 3) * 4...
+ if ((i - start) % 3 == 0) {
+ b64Inc += 4;
+ }
+
+ // The length for quoted-printable is 3 chars only if encoded
+ if (
+ encodedText[i] < 0x20 ||
+ encodedText[i] >= 0x7f ||
+ qpForbidden.includes(String.fromCharCode(encodedText[i]))
+ ) {
+ qpInc = 3;
+ } else {
+ qpInc = 1;
+ }
+
+ if (b64Len + b64Inc > maxChars && qpLen + qpInc > maxChars) {
+ // Oops, we have too many characters! We need to encode everything through
+ // the current character. However, we can't split in the middle of a
+ // multibyte character. In UTF-8, characters that start with 10xx xxxx are
+ // the middle of multibyte characters, so backtrack until the start
+ // character is legal.
+ while ((encodedText[i] & 0xc0) == 0x80) {
+ --i;
+ }
+
+ // Add this part of the word and then make a continuation.
+ this._addRFC2047Word(
+ encodedText.subarray(start, i),
+ b64Len >= qpLen,
+ true
+ );
+
+ // Reset the array for parsing.
+ start = i;
+ --i; // Reparse this character as well
+ b64Len = qpLen = 0;
+ maxChars = this._softMargin - b64Prelude.length - 3;
+ } else {
+ // Add the counts for the current variable to the count to encode.
+ b64Len += b64Inc;
+ qpLen += qpInc;
+ }
+ }
+
+ // Add the entire array at this point.
+ this._addRFC2047Word(
+ encodedText.subarray(start),
+ b64Len >= qpLen,
+ mayBreakAfter
+ );
+ };
+
+ // High-level methods
+ // ------------------
+
+ /**
+ * Add the header name, with the colon and trailing space, to the output.
+ *
+ * @public
+ * @param {string} name The name of the header.
+ */
+ HeaderEmitter.prototype.addHeaderName = function (name) {
+ this._currentLine = this._currentLine.trimRight();
+ if (this._currentLine.length > 0) {
+ this._commitLine();
+ }
+ this.addText(name + ": ", false);
+ };
+
+ /**
+ * Add a header and its structured value to the output.
+ *
+ * The name can be any case-insensitive variant of a known structured header;
+ * the output will include the preferred name of the structure instead of the
+ * case put into the name. If no structured encoder can be found, and the input
+ * value is a string, then the header is assumed to be unstructured and the
+ * value is added as if {@link addUnstructured} were called.
+ *
+ * @public
+ * @param {string} name - The name of the header.
+ * @param value The structured value of the header.
+ */
+ HeaderEmitter.prototype.addStructuredHeader = function (name, value) {
+ let lowerName = name.toLowerCase();
+ if (encoders.has(lowerName)) {
+ this.addHeaderName(preferredSpellings.get(lowerName));
+ encoders.get(lowerName).call(this, value);
+ } else if (typeof value === "string") {
+ // Assume it's an unstructured header.
+ // All-lower-case-names are ugly, so capitalize first letters.
+ name = name.replace(/(^|-)[a-z]/g, function (match) {
+ return match.toUpperCase();
+ });
+ this.addHeaderName(name);
+ this.addUnstructured(value);
+ } else {
+ throw new Error("Unknown header " + name);
+ }
+ };
+
+ /**
+ * Add a single address to the header. The address is an object consisting of a
+ * possibly-empty display name and an email address.
+ *
+ * @public
+ * @param Address addr The address to be added.
+ * @param {string} addr.name - The (possibly-empty) name of the address to add.
+ * @param {string} addr.email The email of the address to add.
+ * @see headerparser.parseAddressingHeader
+ */
+ HeaderEmitter.prototype.addAddress = function (addr) {
+ // If we have a display name, add that first.
+ if (addr.name) {
+ // This is a simple estimate that keeps names on one line if possible.
+ this._reserveTokenSpace(addr.name.length + addr.email.length + 3);
+ this.addPhrase(addr.name, ',()<>[]:;@."', true);
+
+ // If we don't have an email address, don't write out the angle brackets for
+ // the address. It's already an abnormal situation should this appear, and
+ // this has better round-tripping properties.
+ if (!addr.email) {
+ return;
+ }
+
+ this.addText("<", false);
+ }
+
+ // Find the local-part and domain of the address, since the local-part may
+ // need to be quoted separately. Note that the @ goes to the domain, so that
+ // the local-part may be quoted if it needs to be.
+ let at = addr.email.lastIndexOf("@");
+ let localpart = "",
+ domain = "";
+ if (at == -1) {
+ localpart = addr.email;
+ } else {
+ localpart = addr.email.slice(0, at);
+ domain = addr.email.slice(at);
+ }
+
+ this.addQuotable(localpart, '()<>[]:;@\\," !', false);
+ this.addText(domain + (addr.name ? ">" : ""), false);
+ };
+
+ /**
+ * Add an array of addresses and groups to the output. Such an array may be
+ * found as the output of {@link headerparser.parseAddressingHeader}. Each
+ * element is either an address (an object with properties name and email), or a
+ * group (an object with properties name and group).
+ *
+ * @public
+ * @param {(Address|Group)[]} addrs A collection of addresses to add.
+ * @param {string} addrs[i].name The (possibly-empty) name of the
+ * address or the group to add.
+ * @param {string} [addrs[i].email] The email of the address to add.
+ * @param {Address[]} [addrs[i].group] A list of email addresses in the group.
+ * @see HeaderEmitter.addAddress
+ * @see headerparser.parseAddressingHeader
+ */
+ HeaderEmitter.prototype.addAddresses = function (addresses) {
+ let needsComma = false;
+ for (let addr of addresses) {
+ // Add a comma if this is not the first element.
+ if (needsComma) {
+ this.addText(", ", true);
+ }
+ needsComma = true;
+
+ if ("email" in addr) {
+ this.addAddress(addr);
+ } else {
+ // A group has format name: member, member;
+ // Note that we still add a comma after the group is completed.
+ this.addPhrase(addr.name, ',()<>[]:;@."', false);
+ this.addText(":", true);
+
+ this.addAddresses(addr.group);
+ this.addText(";", true);
+ }
+ }
+ };
+
+ /**
+ * Add an unstructured header value to the output. This effectively means only
+ * inserting line breaks were necessary, and using RFC 2047 encoding where
+ * necessary.
+ *
+ * @public
+ * @param {string} text The text to add to the output.
+ */
+ HeaderEmitter.prototype.addUnstructured = function (text) {
+ if (text.length == 0) {
+ return;
+ }
+
+ // Unstructured text is basically a phrase that can't be quoted. So, if we
+ // have nothing in qchars, nothing should be quoted.
+ this.addPhrase(text, "", false);
+ };
+
+ /** RFC 822 labels for days of the week. */
+ var kDaysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ /**
+ * Formatting helper to output numbers between 0-9 as 00-09 instead.
+ */
+ function padTo2Digits(num) {
+ return num < 10 ? "0" + num : num.toString();
+ }
+
+ /**
+ * Add a date/time field to the output, using the JS date object as the time
+ * representation. The value will be output using the timezone offset of the
+ * date object, which is usually the timezone of the user (modulo timezone and
+ * DST changes).
+ *
+ * Note that if the date is an invalid date (its internal date parameter is a
+ * NaN value), this method throws an error instead of generating an invalid
+ * string.
+ *
+ * @public
+ * @param {Date} date The date to be added to the output string.
+ */
+ HeaderEmitter.prototype.addDate = function (date) {
+ // Rather than make a header plastered with NaN values, throw an error on
+ // specific invalid dates.
+ if (isNaN(date.getTime())) {
+ throw new Error("Cannot encode an invalid date");
+ }
+
+ let fullYear,
+ month,
+ dayOfMonth,
+ dayOfWeek,
+ hours,
+ minutes,
+ seconds,
+ tzOffset;
+
+ if (this._sanitizeDate) {
+ fullYear = date.getUTCFullYear();
+ month = date.getUTCMonth();
+ dayOfMonth = date.getUTCDate();
+ dayOfWeek = date.getUTCDay();
+ hours = date.getUTCHours();
+ minutes = date.getUTCMinutes();
+ // To reduce the chance of fingerprinting the clock offset,
+ // round the time down to the nearest minute.
+ seconds = 0;
+ tzOffset = 0;
+ } else {
+ fullYear = date.getFullYear();
+ month = date.getMonth();
+ dayOfMonth = date.getDate();
+ dayOfWeek = date.getDay();
+ hours = date.getHours();
+ minutes = date.getMinutes();
+ seconds = date.getSeconds();
+ tzOffset = date.getTimezoneOffset();
+ }
+
+ // RFC 5322 says years can't be before 1900. The after 9999 is a bit that
+ // derives from the specification saying that years have 4 digits.
+ if (fullYear < 1900 || fullYear > 9999) {
+ throw new Error("Date year is out of encodable range");
+ }
+
+ // Start by computing the timezone offset for a day. We lack a good format, so
+ // the the 0-padding is done by hand. Note that the tzoffset we output is in
+ // the form ±hhmm, so we need to separate the offset (in minutes) into an hour
+ // and minute pair.
+ let tzOffHours = Math.abs(Math.trunc(tzOffset / 60));
+ let tzOffMinutes = Math.abs(tzOffset) % 60;
+ let tzOffsetStr =
+ (tzOffset > 0 ? "-" : "+") +
+ padTo2Digits(tzOffHours) +
+ padTo2Digits(tzOffMinutes);
+
+ // Convert the day-time figure into a single value to avoid unwanted line
+ // breaks in the middle.
+ let dayTime = [
+ kDaysOfWeek[dayOfWeek] + ",",
+ dayOfMonth,
+ mimeutils.kMonthNames[month],
+ fullYear,
+ padTo2Digits(hours) +
+ ":" +
+ padTo2Digits(minutes) +
+ ":" +
+ padTo2Digits(seconds),
+ tzOffsetStr,
+ ].join(" ");
+ this.addText(dayTime, false);
+ };
+
+ /**
+ * Signal that the current header has been finished encoding.
+ *
+ * @public
+ * @param {boolean} deliverEOF If true, signal to the handler that no more text
+ * will be arriving.
+ */
+ HeaderEmitter.prototype.finish = function (deliverEOF) {
+ this._commitLine();
+ if (deliverEOF) {
+ this._handler.deliverEOF();
+ }
+ };
+
+ /**
+ * Make a streaming header emitter that outputs on the given handler.
+ *
+ * @param {StreamHandler} handler The handler to consume output
+ * @param options Options to pass into the HeaderEmitter
+ * constructor.
+ * @returns {HeaderEmitter} A header emitter constructed with the given options.
+ */
+ function makeStreamingEmitter(handler, options) {
+ return new HeaderEmitter(handler, options);
+ }
+
+ function StringHandler() {
+ this.value = "";
+ this.deliverData = function (str) {
+ this.value += str;
+ };
+ this.deliverEOF = function () {};
+ }
+
+ /**
+ * Given a header name and its structured value, output a string containing its
+ * MIME-encoded value. The trailing CRLF for the header is included.
+ *
+ * @param {string} name - The name of the structured header.
+ * @param value The value of the structured header.
+ * @param options Options for the HeaderEmitter constructor.
+ * @returns {string} A MIME-encoded representation of the structured header.
+ * @see HeaderEmitter.addStructuredHeader
+ */
+ function emitStructuredHeader(name, value, options) {
+ let handler = new StringHandler();
+ let emitter = new HeaderEmitter(handler, options);
+ emitter.addStructuredHeader(name, value);
+ emitter.finish(true);
+ return handler.value;
+ }
+
+ /**
+ * Given a map of header names and their structured values, output a string
+ * containing all of their headers and their MIME-encoded values.
+ *
+ * This method is designed to be able to emit header values given the headerData
+ * values produced by MIME parsing. Thus, the values of the map are arrays
+ * corresponding to header multiplicity.
+ *
+ * @param {Map(String->Object[])} headerValues A map of header names to arrays
+ * of their structured values.
+ * @param options Options for the HeaderEmitter
+ * constructor.
+ * @returns {string} A MIME-encoded representation of the structured header.
+ * @see HeaderEmitter.addStructuredHeader
+ */
+ function emitStructuredHeaders(headerValues, options) {
+ let handler = new StringHandler();
+ let emitter = new HeaderEmitter(handler, options);
+ for (let instance of headerValues) {
+ instance[1].forEach(function (e) {
+ emitter.addStructuredHeader(instance[0], e);
+ });
+ }
+ emitter.finish(true);
+ return handler.value;
+ }
+
+ /**
+ * Add a custom structured MIME encoder to the set of known encoders. These
+ * encoders are used for {@link emitStructuredHeader} and similar functions to
+ * encode richer, more structured values instead of relying on string
+ * representations everywhere.
+ *
+ * Structured encoders are functions which take in a single parameter
+ * representing their structured value. The this parameter is set to be an
+ * instance of {@link HeaderEmitter}, and it is intended that the several public
+ * or protected methods on that class are useful for encoding values.
+ *
+ * There is a large set of structured encoders built-in to the jsmime library
+ * already.
+ *
+ * @param {string} header The header name (in its preferred case) for
+ * which the encoder will be used.
+ * @param {Function(Value)} encoder The structured encoder function.
+ */
+ function addStructuredEncoder(header, encoder) {
+ let lowerName = header.toLowerCase();
+ encoders.set(lowerName, encoder);
+ if (!preferredSpellings.has(lowerName)) {
+ preferredSpellings.set(lowerName, header);
+ }
+ }
+
+ return Object.freeze({
+ addStructuredEncoder,
+ emitStructuredHeader,
+ emitStructuredHeaders,
+ makeStreamingEmitter,
+ });
+ });
+
+ def("jsmime", function (require) {
+ return {
+ mimeutils: require("./mimeutils"),
+ MimeParser: require("./mimeparser"),
+ headerparser: require("./headerparser"),
+ headeremitter: require("./headeremitter"),
+ };
+ });
+ return mods.jsmime;
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/base64-1 b/comm/mailnews/mime/jsmime/test/unit/data/base64-1
new file mode 100644
index 0000000000..18e089a5b4
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/base64-1
@@ -0,0 +1,7 @@
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: base64
+
+DQpIZWxsbywgd29ybGQhIChBZ2Fpbi4uLikNCg0KTGV0J3Mgc2VlIGhvdyB3ZWxsIGJhc2U2NCB0ZXh
+0IGlzIGhhbmRsZWQuICAgICAgICAgICAgICAgICAgICAgICAgICAgIFlheSwgbG90cyBvZiBzcGFjZX
+MhIFRoZXJlJ3MgZXZlbiBhIENSTEYgYXQgdGhlIGVuZCBhbmQgb25lIGF0IHRoZSBiZWdpbm5pbmcsI
+GJ1dCB0aGUgb3V0cHV0IHNob3VsZG4ndCBoYXZlIGl0Lg0K
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/base64-2 b/comm/mailnews/mime/jsmime/test/unit/data/base64-2
new file mode 100644
index 0000000000..bb469edcde
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/base64-2
@@ -0,0 +1,6 @@
+Content-Type: text/html; encoding=iso-8859-1
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGJvZHk+VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBIVE1MIHRleHQsIGFuZCB0aGUgdGFncyB
+zaG91bGRuJ3QgYmUgc3RyaXBwZWQuDQo8Yj5Cb2xkIHRleHQgaXMgYm9sZCE8L2I+PC9ib2R5PjwvaH
+RtbD4NCg==
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/basic1 b/comm/mailnews/mime/jsmime/test/unit/data/basic1
new file mode 100644
index 0000000000..f23da3ea93
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/basic1
@@ -0,0 +1,3 @@
+Content-Type: text/plain; charset=iso-8859-1
+
+Hello, world!
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/bug505221 b/comm/mailnews/mime/jsmime/test/unit/data/bug505221
new file mode 100644
index 0000000000..c2e292156a
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/bug505221
@@ -0,0 +1,75 @@
+From - Mon Jan 1 00:00:00 1965
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 10000000
+From: <aaa bbb>
+To: <aaa@bb.invalid>
+Subject: xxx
+Date: Tue, 9 Dec 2008 16:49:02 +0200
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_36B5_01C9DB8C.9514C300"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_36B5_01C9DB8C.9514C300
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; =
+charset=3Dus-ascii">
+
+
+<META content=3D"MSHTML 6.00.6000.16735" name=3DGENERATOR></HEAD>
+<BODY> bbb
+</BODY></HTML>
+------=_NextPart_000_36B5_01C9DB8C.9514C300
+Content-Type: message/rfc822
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+
+From: <sys admin>
+To: <dd@ee.invalid>
+Subject: yyy
+Date: Sun, 7 Dec 2008 17:53:47 +0200
+MIME-Version: 1.0
+Content-Type: message/rfc822
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+From: <aa@b>
+To: <aa@b>
+Subject: ccc
+Date: Sat, 23 May 2009 09:55:19 +0200
+MIME-Version: 1.0
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
+<HTML>
+<HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; =
+charset=3Diso-8859-1">
+<META NAME=3D"Generator" CONTENT=3D"MS Exchange Server version =
+08.00.0681.000">
+<TITLE>ccc</TITLE>
+</HEAD>
+<BODY>
+<!-- Converted from text/plain format -->
+
+</BODY>
+</HTML>
+------=_NextPart_000_36B5_01C9DB8C.9514C300--
+
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/bugmail11 b/comm/mailnews/mime/jsmime/test/unit/data/bugmail11
new file mode 100644
index 0000000000..cfc03cc70e
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/bugmail11
@@ -0,0 +1,47 @@
+From - Mon Jun 02 19:00:00 2008
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Return-path: <example@example.com>
+Delivered-To: bugmail@example.org
+Received: by 10.114.166.12 with SMTP id o12cs163262wae;
+ Fri, 11 Apr 2008 07:17:31 -0700 (PDT)
+Received: by 10.115.60.1 with SMTP id n1mr214763wak.181.1207923450166;
+ Fri, 11 Apr 2008 07:17:30 -0700 (PDT)
+Return-Path: <bugzilla-daemon@mozilla.org>
+Received: from webapp-out.mozilla.org (webapp01.sj.mozilla.com [63.245.208.146])
+ by mx.google.com with ESMTP id n38si6807242wag.2.2008.04.11.07.17.29;
+ Fri, 11 Apr 2008 07:17:30 -0700 (PDT)
+Received-SPF: neutral (google.com: 63.245.208.146 is neither permitted nor denied by best guess record for domain of bugzilla-daemon@mozilla.org) client-ip=63.245.208.146;
+Authentication-Results: mx.google.com; spf=neutral (google.com: 63.245.208.146 is neither permitted nor denied by best guess record for domain of bugzilla-daemon@mozilla.org) smtp.mail=bugzilla-daemon@mozilla.org
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m3BEHTGU030132
+ for <bugmail@example.org>; Fri, 11 Apr 2008 07:17:29 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m3BEHTk4030129;
+ Fri, 11 Apr 2008 07:17:29 -0700
+Date: Fri, 11 Apr 2008 07:17:29 -0700
+Message-Id: <200804111417.m3BEHTk4030129@mrapp51.mozilla.org>
+From: bugzilla-daemon@mozilla.org
+To: bugmail@example.org
+Subject: Bugzilla: confirm account creation
+X-Bugzilla-Type: admin
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Bugzilla has received a request to create a user account
+using your email address (example@example.org).
+
+To confirm that you want to create an account using that email address,
+visit the following link:
+
+https://bugzilla.mozilla.org/token.cgi?t=xxxxxxxxxx&a=request_new_account
+
+If you are not the person who made this request, or you wish to cancel
+this request, visit the following link:
+
+https://bugzilla.mozilla.org/token.cgi?t=xxxxxxxxxx&a=cancel_new_account
+
+If you do nothing, the request will lapse after 3 days
+(on April 14th, 2008 at 07:17 PDT).
+
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/charsets b/comm/mailnews/mime/jsmime/test/unit/data/charsets
new file mode 100644
index 0000000000..d33046820d
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/charsets
Binary files differ
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/message-encoded b/comm/mailnews/mime/jsmime/test/unit/data/message-encoded
new file mode 100644
index 0000000000..44c4a8e2fc
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/message-encoded
@@ -0,0 +1,24 @@
+Content-Type: multipart/mixed; boundary="iamaboundary"
+
+This is a text message in MIME format.
+This part shouldn't appear in the output.
+
+--iamaboundary
+Content-Type: message/rfc822
+
+Subject: I am a subject
+
+This is a plain-text message.
+--iamaboundary
+Content-Type: message/global
+Content-Transfer-Encoding: base64
+
+U3ViamVjdDog56eB44Gv44CB5Lu25ZCN5Y2I5YmNDQoNCkkgYW0gYSBwbGFpbi10ZXh0IG1lc3NhZ2Uu
+--iamaboundary
+Content-Type: message/news
+Content-Transfer-Encoding: quoted-printable
+
+Subject: =e7=a7=81=e3=81=af=e3=80=81=e4=bb=b6=e5=90=8d=e5=8d=88=e5=89=8d
+
+I am an encoded plain-text message.
+--iamaboundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/mime-torture b/comm/mailnews/mime/jsmime/test/unit/data/mime-torture
new file mode 100644
index 0000000000..ad540ae0e8
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/mime-torture
@@ -0,0 +1,25875 @@
+Date: Mon, 6 Feb 1993 02:53:47 -0800 (PST)
+From: Mark Crispin <mrc@CAC.Washington.EDU>
+Subject: Multi-media mail demonstration
+To: MRC@CAC.Washington.EDU
+Mime-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=owatagusiam
+Message-Id: <CMM.0.88.687168092.mrc@akbar.cac.washington.edu>
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+--owatagusiam
+Content-Type: TEXT/PLAIN
+Content-Description: Explanation
+
+This is a demonstration of multi-part mail with encapsulated messages. This
+is a very complex message whose purpose it is to exercise software using the
+new multi-part message standard.
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Rich Text demo
+
+Received: from dimacs.rutgers.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA21889; Tue, 24 Dec 91 05:52:04 -0800
+Received: by dimacs.rutgers.edu (5.59/SMI4.0/RU1.4/3.08)
+ id AA09350; Tue, 24 Dec 91 08:14:38 EST
+Received: from thumper.bellcore.com by dimacs.rutgers.edu (5.59/SMI4.0/RU1.4/3.08)
+ id AA09346; Tue, 24 Dec 91 08:14:33 EST
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA28328> for ietf-822@dimacs.rutgers.edu; Tue, 24 Dec 91 08:14:30 EST
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA22222> for ietf-822@dimacs.rutgers.edu; Tue, 24 Dec 91 08:14:29 EST
+Received: from Messages.8.0.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.41
+ via MS.5.6.greenbush.galaxy.sun4_41;
+ Tue, 24 Dec 1991 08:14:27 -0500 (EST)
+Message-Id: <sdJn_nq0M2YtNKaFtO@thumper.bellcore.com>
+Date: Tue, 24 Dec 1991 08:14:27 -0500 (EST)
+From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+X-Andrew-Message-Size: 747+0
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk="
+To: ietf-822@dimacs.rutgers.edu
+Subject: Re: a MIME-Version misfeature
+
+This message has been composed in the 'multipart' format for extended
+mail bodies. If you are reading this prefix, your mail reader probably has
+not yet been modified to understand the new format, and some of what follows
+may look strange. You may wish to look into upgrading your mail reader.
+
+However, the first part of this message (immediately after the funny-looking
+boundary line) is the text-only version of the message. If you read that part
+and skip the rest, you will probably understand the gist of the message.
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following comment:
+
+Excerpts from internet.ietf-822: 23-Dec-91 a MIME-Version misfeature
+Mark Crispin@cac.washing (429*)
+
+> All in all, it's starting to look like MIME-Version is not really all that
+> well thought out and is in danger of being superfluous baggage.
+
+Some kind of version number is, in my opinion, totally vital -- getting
+rid of MIME-Version without some similar versioning mechanism to replace
+it would be, for me, a real show-stopper.
+
+By the way, this message is being sent by an experimental version of
+Andrew I'm playing with. I'd be interested in any comments you have on
+its formatting, etc. -- Nathaniel
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+Content-Type: multipart/mixed;
+ boundary="Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA"
+
+--Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA
+Content-type: text/richtext
+Content-Transfer-Encoding: quoted-printable
+
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following
+comment:<nl><nl><excerptedcaption>Excerpts from internet.ietf-822: 23-Dec-91 a
+MIME-Version misfeature Mark Crispin@cac.washing
+(429*)</excerptedcaption><nl><nl><quotation>All in all, it's starting to look
+like MIME-Version is not really all that<nl></quotation><quotation>well
+thought out and is in danger of being superfluous
+baggage.<nl></quotation><nl>Some kind of version number is, in my opinion,
+totally vital -- getting rid of MIME-Version without some similar versioning
+mechanism to replace it would be, for me, a real show-stopper.<nl><nl>By the
+way, this message is being sent by an experimental version of Andrew I'm
+playing with. I'd be interested in any comments you have on its formatting,
+etc. -- Nathaniel<nl>\
+
+--Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA--
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+Content-type: application/andrew-inset
+
+\begindata{text,1672608}
+\textdsversion{12}
+\template{messages}
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following comment:
+
+
+\excerptedcaption{Excerpts from internet.ietf-822: 23-Dec-91 a MIME-Version
+misfeature Mark Crispin@cac.washing (429*)}
+
+
+\quotation{All in all, it's starting to look like MIME-Version is not really
+all that
+
+}\quotation{well thought out and is in danger of being superfluous baggage.
+
+}
+Some kind of version number is, in my opinion, totally vital -- getting rid of
+MIME-Version without some similar versioning mechanism to replace it would be,
+for me, a real show-stopper.
+
+
+By the way, this message is being sent by an experimental version of Andrew
+I'm playing with. I'd be interested in any comments you have on its
+formatting, etc. -- Nathaniel
+
+\enddata{text,1672608}
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=--
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Voice Mail demo
+
+Received: from Tomobiki-Cho.CAC.Washington. (Tomobiki-Cho.CAC.Washington.EDU) by Ikkoku-Kan.Panda.COM
+ (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 2.22 ) id AA12299; Tue, 8 Oct 91 07:29:39 PDT
+Return-Path: <nsb@thumper.bellcore.com>
+Received: from thumper.bellcore.com by Tomobiki-Cho.CAC.Washington.EDU
+ (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 1.60.MRC ) id AA27545; Tue, 8 Oct 91 07:28:25 PDT
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA08355> for mrc@panda.com; Tue, 8 Oct 91 10:25:41 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA00616> for mrc@panda.com; Tue, 8 Oct 91 10:25:36 EDT
+Date: Tue, 8 Oct 91 10:25:36 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110081425.AA00616@greenbush.bellcore.com>
+To: mrc@panda.com
+Subject: Re: multipart mail
+MIME-Version: 1.0
+Content-Type: audio/basic
+Content-Transfer-Encoding: base64
+Content-Description: Hi Mark
+
++Pv/d3RydWlnbGtnYWJiaWxqaWxucHrx7erk7X709Pfx6eXo5uXe3Ojw+Px1cXZra2dsbGdr
+Z2t5bG1wbWxtf+/v8vPq7O3ue3B19efk8vz3fXl1c3RqcHFsb3x6dnZ5anf4+vb+8nt2+vL4
+8ej5+vN7/vH2dPzweG3++23y6vn6eGlneP37fHB+dm1yevt7ev39d2518evs+Pzw/Hr67np1
+fn7+evp8fP3ycmxucXxtaW58/m94/HNtfOji6e/y8vD67+Xm7P/x+G5naXBwcnl4dHx/c3Bx
+eXt3bmJfY2/29v3v6+rj4e3t5eno6/b+8vBxc3JubnNzeWxpaGRnZGZz9vbt6e/8+fr4+vfr
+6/bw7PXw+fj3/Pl3a2x5cXFwfnhra3r5em98/Pv0/Xt4fHn/7env7+vt9/748Ozq7ntpZmln
+ZWl9fvn7enVu+fH4/3R2fHZv9+v5dnB8//76/fr+fn/4+n3u6u59fHlxem5t/vl3dvl89+/q
+5u7n7Ph1aGFjZ25wam13/nt++374e35/+/Lt73lsZnB+/fP38OHj8PL0+nX17vr4b2tnX2dx
+dW5x9PB7c/z9fPTvf/9+dP39ePXn9nN6fHV4e3hydv799f9veXtxdOrk8O/i6/vr6u/49nvx
+92l1925lZWdoaW5sb3FtcXX9/XF1fXd3eHh47+Xt7ejp9fHu7uvx8vH2+vjv63l98/D4/HRl
+YWFhX29+/Xhvb3Bu/+7s8Pn3+Xhs9+bl6vJ+7Ob++Ozv9PDk7G5veWNla2lxcmtqd25ueXx8
+8/J79O7s5u38enN8/HpsbXz87fDr4+jr7u/6+Hj27Hl6fv52cG5ueGxrfff+cf/3eHFvffZ6
+e/Tv8/v7/Pnx+3X/8ft+9ejs7uvx8+/z/Hl3dW14/HVzffv5/v56/PH8b3Fxa2prcXl5/fx2
+8+zs7uzt8fPt93BtcXlwd/708n7q4vLx7Phwcn/y6/Z5efh9efj8+nVzam10dP7+/vjv7fhv
+a2xxfPL9eHJ58vlucO7p8vz9+v379/T5/fn3d2hw/fTz7fP6/fr6enny7O3s+P3/dnXu9n50
+cndtb3l8fP/09PHu8X1sbGpvdnx6/fHv7uzyfPz2d29++X5vaWJv8Ojn4+92/O3yfPr7eHx9
+dm1vefn4ePPu7O3x/Gxtffr+a3H++XFq//X6/n54eff3dW94/vv9/Xb66+599/T57Or6eHv2
+7PR2df9ubG5wd/f+cX31+np5evh9dXRvbnB6ffr39O7s7/Xr5N7h7/5ue/n9c29w++97bWtn
+Z2tqbmxoZmduaWr76eTn5uXx/ffy+nZ29vP57/119u7v7/r15+nr8Hd4e3p7cGlta2tycG95
+fHb4+3hxa2dlbP72fv15anN9bm/68fXt7/Xs5Onx7+jr7eTw+/X+ffz+b25ua290dW5qZWJn
+cHz3/fv++O3u+//0/Pv6fW5oaXX78u7r5+/s6uv8b3ZufH95dXX++O3ufG5ubGty/nh2bW1v
+bWpqZ2187uPo7/Lq4OLq7v10eH57fvt+fnx79O3wd3h4a3H5/XlyamNibXVvcm50+Pf5fH/6
++vz//PTq6+/6d/3u6/Lw6e/48+r1cn16aGx4b29rbX74/H31/2xueHRucnl1cnt4df198uzq
+6ufk6O3u7vp6d/98bGZxefrz7X1oZW37dm1oZ2t5fvz3f/vt6uXn5eno8fd+d/r3/HFzdm9x
++3ptanN2bm97fnZ+efjz+n5zb29vePPs/33r5uXe3uj09/l9cXFzb3Bxc3Z0a3F1cHV2en15
+dnZtbG91+nx5fX7v6+ji4ent7/bs73x0fXl4e3lsb/90cvfyfHt+bGZv93tpbHv18nz9fH/0
+8/P48u328/b07+73fHVzdXv+bmZqe/x+8vL0+HtmZG5we/z8dnv08XZ4+/v99/Dv7vDv7u/4
+9fP6+/r6cG7+/W9namlyfHJ+fnduevP+c335fvn6+u/x8vj5/vn1+P57++3t93lyamtsam52
+9fj6fHzy9Hd5eXpubnpxePx/c3j37evo8/7x7fP2+fPy7+/8cHN//3h3d2tmYWZrbPd9f3r5
+8Pv5fPvv7fz87vDr5+78dnx9cmlmam1jZnV67+vr9Pz37u37/v91fPr4+/Xw7vv49+7q+3Fw
+/nxwb2hsbHX3d29z/nh1/Xx8//f+b3r19X396e7v9nhvcX72/fh7e3d1bmhr9Pb38vx9/vf+
++/fu7X13fvfxdm968uzx9X93/37+cm1ufPl3cm9xa3R6fn58+vj29P379Pj79PH5eG1tb3h+
+bn/9/efs7OvyeH79em9ycmpx9/H29P//+O3z9/Jzb317dX78d3Vxd/rt+2/68u3v8O/6+/f7
+e3d5eXFvcX56cnJrZGt9//b47urydG92+unr7O718fT3fG5+8vPw+Pz9dXVxcmZlcGtiaHZ+
++vLk5urp5uPr6uzv9Pl2dHt0eXhybG5rZ21wfXx9eHx8eW9yfPv68er0dHf2+fPl5ers+fX0
+fHvu7Pj2/HRsb/v9cnBtaWpqdfXt7HZrdX3u7X56fvT5dGr77vD5eG5tanTz8Pbx7fbyfv3y
+/nh3+fvz7vl3eOru/Xl6+3x7amRqb3l0cnZ5/+/1+fr89P14e/h9eOzo9v/49XF5fP9xbHx3
+cHT89+zx6vdy/vH0fX768+/u7PZ+/nlwbWpve/R+bnZ5+PX2eX7z+npxeX12cXB/9e339u33
++fru7Ozo6/D+bW1sbm9tcnZwdWtt9/Hw8/L7/O/w7+35/v7u9/j6c3J5b254fP/+8vH6+vZ2
+cvTq6/1ta2VpbWx1/fvt6vb47/P6+PL3eHd4bH7p6efn6vh/dWtz+/10c3L48P91bHT5//x0
+cvfu+m1tbG1wdfj+8Ojyemp29PLx8/vy7+3xf/fu7XtveHV4bm55+uzs6/xpc3P+7Pvv6Ov2
++v1ybm30831+e2xjbnhzXmRrZ3F49vh19Obl6+vk6OHk4uzx8e/vev55d3Nwa2hsb21zdv/7
+e/1wdn54eW92bGZpdPT39Pj39PLv7Pdx8ufq+f18f3r58fjv+Hl1b/rq/nd5+/r6+3J29/n/
+/HVlZnFua2pwbG1ra3L88eru+evg3eTt7Onq8nlsbHv67vj6/37+cnFncH3+fHR6b3JpbX56
+evz2/fLq7Pv0/HZ8dWlseXv0ff3v5u339PN2evL+c3J+dG5sdHT/8fj07ejr6+7z7/v5fvnv
+/XJzbmt+eG1tZWpvc2pjd/vy7+3r+vr2fPn78urx8e/t73Fvffx1b3lw+uhva3V0bu7s+XN5
+/Pn0ant8//bu7nr79Pn3635oZF9ZbvTm6vXr7ez/3djy73pyWFZeaWZbafj07Ovn8evc8Hll
+bv54fert397k7+ro+3xxeW36c/VxZWds72Jia2tgZG/7/fvv3+Dh5+br7/L37+7vc3d4YWZn
+eXprdndjZXz//nl1+ur2ffvl7vXk7eXx3t3h83ptbPnxce/yXWJma2Zq9P//a/98aHdt6vJ6
+++jndOvtav/q6vv48ulvae/67/NybXb7ev5oc3Xt6/l0c/R5dmBj+e91+vR0eu/s8n5q7+97
+dOnc6e7t/Xbq7W5obHL4f/Z4efLq82JkYWBlaHr8fX177Xp98e3u7+v55exz5fLtbGd0d/fy
+5/12+f9yamJlZmtfe/b9+291b3V67fHh2Nz529/k4G9oYWFqbe/xX29qYmRiaGhsau3ffeXh
+8Xdx7//6aPfv7+Pk39/vfHp0Y11tZF9le/pt+nT3eXfi+G5sd+Xt7t/h3+p2bn5wbe7xZXFt
+c2pcd3xuevz56+v06vRqcvDpaWFl/f/02OL/dvFsWVxoeP9sfurx8+zw4+/5/mVia3rucm3v
+3+f+cnno7H3ofGtja/f4dXPv9HFzY3F2Z3b2/mp57vRveX3w/3h0bGRedN7g9Xfo5/jt8Pp7
+9PX4+Xd+7uz+aGru83Fob2xgb/7zeG58aWRjbfdxb3Ti4e755+Pq9u3n+Xl2cWRhdfjn83l7
+cvbu9e90d2l4b2Np/elpcOrofWv+fvxubvDy39/x7+59fHVraGz46vly9PNzbvX/b3Z3/nr4
+c2tva2757v396+d4bX7v9W98ev9ya+/q7/Lm5vp6fHZzfWxrenByc/h2bG956fP59H77bPvu
+c2JfeHV33+bn6ezn6PX86vl8f3l1bnNvbmdld3Zw7vtveP53fPx/c2l+8vj6bGps+37v3uTt
+c/Po6+/x+vl+a/J5Y3P2eHP+7e1wX2zzdnbk+Gpm/fRod+/v7fTxf/btfHX08mn6emto/Ob5
+ZvXm7/P17fnwfPPueG1u6PBu+nRxanp5bWtp7uxgavPue3Dr9/By/+5/emjv73Pw3ulv7t3g
+7vh/83Jf9+P8anT4dm9zfft1aXJrbmhfa/59eu/p73j25e3n6PHqemlq8HZu+Org+X7tdnBq
+be77bnXs6XJv5/dr5+X5ZmzraF198Oz4/eLw8e92/Wxp9X1sbH3mcl1sd3RyZ3bsfO7n+Hrw
+6vfu6/Zn/fdq8PHr4+n6cnn7c/j0fXZ+/Hb99fRzc2Zj/vZmau3md2h5dGb2/OTl/ff482v+
+7ejx/+Htb294emt5c2pudPt5cm9t7O5zbvzq9Xh85u537ufh7ufa7GZ39/dvau7x/n1pZ15o
++f1x+fdqbHdyZ29la+/n/Gvy5/p829h89/zu8H3z9uX5efD69G1p/m9pa+v1cPj+8Xvw6ft5
+bfTnd2lo+vhleHN1/nHs5Pr+7+17dvtpZnb+a3Hs7PHq/Or5fOtz/nTu82NxY2vucHxxe35p
+b3ns4ujv5+Dz+fJ+fvv1+2tr8vxvdOrufXFxb2r7dWzwb29wdHhnb/Rz/u7w6uzu+ez9Z3z1
+7vrw4OX5aGv8a2n75WtreGhseHzv9W7m2979f+7zfnzr9GljeX1dYnL5aWnu8nB8+nNtff3o
++F7+4fJv6OPl8Pfd82x3/e90a3ri4nVvc/lsX196fFps8f5tbezl+3bi3PD44O/1835sam/6
+9G5rfG9ob3H6dWv99ert6+bj/Gt9fmxoeft46/Ts53tw7OloaftuZ3H59f3q/G364uL6++J4
+/e70/2t8fnd58H5tanN2Z3JtbHp+7P1v8u7ke/3l6vx4e/t5aXZ9bW939e/17uvm5/X3/fj7
+eXt+bHR5a/59Z/Tq9m1w731y9/X+8fP49n929+b/a/Dv93p8e2d67Xl4f/x2fvB2+/bz9Px5
+fWtxe/b2dnnu9WJpfG14fHx+cujh5vZ18Xl58fH37Ozt+n1+a2dq+/n77vdq/fDr/Wd7+XBq
+amVnbWt39/r0dmt4d2n36PDr6d7q9+bs8uvuePjxfWNmam12bGhxdG9xbWdqcGx+dP3s6/T8
+4uLp4N7j7357b29oendjafz6+HhuZWFrbWxyfm7293v1+eHl/+rj6/lv+3p2aGv6+/xucO7w
+eeXx9exybHv+cvHrfvH2bnJ8fm52cXF9fez6b2/t73FrYnl4ZnTt9Xp7/HNq7+Tu//n+391v
+dOTq+Xju7m149XpoZnz29WNn8npnZujrbXb/9nZndOpzXmn983555+p2++fqffD27OL2/+n7
+dvdx8vpsY2j4b2f67XpobnV0am/q/m76fXL8fW9ye/jvff3h6nz15fb37Onpfnzm6Plnc/dn
+YW1uZGvl6GlrbflyXGz0bGl+7W/v3Pnx5+zd4+vx5u1rc/T8fG52729ycnr1X1txbXd6eHJ0
+9e/s3+t0fe78c3r35Xpp8+jxZml8b3Fw+nhrcfji6fbn4eZ69O52+nV2/XN4fWps/m5sc3T4
++mZs/W5pZ25mff326O/q2Nbn7Onq++/3cm9mZ3n7eGBt5/drbeXoYWT0eWBifntqZ+7m83T7
+6W9obW70fvzr7PHq6errcW7p6n129u7/+vHz7ebyb2hgbvz7al5xdHJtb31uc/59/3lvenBo
+d/Dydv7r6/p7/ebffX7h5np14eB7aWvj6mdnY29zanT5dWpqaGzu5Ot99+ry/HTz8XN5bWr7
+93B6/nL8f3L15vBy7+V4am/3+XB3fPft//fzZX3q9O16/nttd2lmb2lt++/1ZG/3b3D/5uLp
+7erh8vn+7Onq6X5oYmVnbP95eP92b35oa3xtfPZ7cu/m/n/d2/N1++vr+W3+7HtiY3l6aHt9
+Y2Nx+nVrdG599u/e5e37efDv5u978ez8enNudW9uaHh1d3n77n5vd+z1ZG/v8Ht07v18b3ng
+7W5rfP5ubvXn7/TpfGtv9O1lXWdy+3x97vXi5fLu7O35dWxvdWtvc27t7n3w4t/rd2/z/HBt
+e+9tZnrveXLr6ezvbmhqZGJoenZs/Or3/u3x9Ojv/PL8/f3y8+ni7H579X54eW9x/mxl/en7
+cnL88W17+3l1a3jufGNo9ez58PHq7Orq7OXt6vlwdG9peP14fX/y93pu+Xtobm1udvbybGty
+b3hzbnB17Or//O/u6u3t4ebp7e3q9/rt/Xd1fHFrZmN3/m9tbPv3Zmf+93Vu/uv5bPbo92t1
+5uPp/u/r6Of4+PlyY2Bscmtvb/bybPXld2lz/fV8/3vt6Xp3/fD08u3q+P745+t5a3Xq+29r
+/vF6cW91ePf3cWxy9uztd3Hw925kaXp5b2x57PR++Ovz9vTu+vz+dHn96e/39O3o/m779P9z
+cnBr9Hpuc255dXN8fXxxbHN4//7t6nX87vf1+/ry7+7t9PZ0bm1scfj2e33u6e76eHv4fnB3
+bmZw9PLx7+7ucmRnb3FycGdv7e/v7uvl93fxfHBzef3x+fjy8PRz+PP0eGdpaWdqeO74cHrv
+eHZ2c3327nx5e3Tx6Ozs5d/n8353bXP6fnl1bm59+fr47vb4+XRwbHJuZGdfXmZrdHrx9H7z
+83x7/PDp5/L46/D68+3s8vb48e33fvPp7n389f14fv12d21pbXFqXl9qbndtcPft8/5+fHx6
+fvj28P/z63pmdfr9dXjq6vf27ufr9fXt6/H4fv/5fXvw9Hl6fPR7aW5+9nh5fX5waWtrZmlv
+cXRvbvPl6e/07vZ9/v707nb/93d7fP38+XZ19+98bvro5er0+Pr8dGv473J3+vt99flvdH//
+/3h99vr4e3h2cnV2bGp0fXV6+nx1+ers8/T07eXo+nV8fnd3e3hucXhta294ff338O/q5Ot4
+aGlucGt59e/s+Pby9+7u6Ofq6fV3c/zz//r0dmpqa2tsam52dGZpeHB2efjr7Pf++Ozy8/Dz
+6ur0eff+cmVkem1qcu3o/v7u8f9tZG/v9H76+HP06vPw9u3qfXFycXFrdP7+/3trZW357+x+
+evv9dnP79/r78PB9dn7y7fPs9O7yenN5/X13bG51+HpscHd7cmxxcWtr/PL07Ovv+Pf//Pv4
++/339/t///jp7PT9fnt6eW568/v49Xt6/vTw+H/19nxvaWhua25zcvnu8Hz37fD07e92enn+
+7uno73p6d/rz/nZsaGZscmZt//18fHtveu72fenq9/jz9n/v4+bm6vZ5dGhjbnF7+3Fqb31x
+anBvd/Lt73t0fu/y+vX3e3v48vPt6e16d/x/cHp5df707PTx8fp+dGtsfflubnxzbm91+Pt4
+d/798+3q5/xvc/x+cHVta3X87+3s6Or8/vDzcGRqcXfy9Hxx+/P++/jv7/T5//Tz/X/9fHX5
+/Hx9bXj9fHb7fm9saWtucfj8amtxffR/+/Pt9e3k5efi5vn+9fhzam1ycXJ6fXx//P17a2d0
+cnH//vb58fp9+vX6dHFpaXzu+frs/fr1+nxvdPXv/nvy7/r67+rv9+17bHJ5b2Zrd/19eHJs
+Z2Nren95+u74+unl6/T++fP28ezy+vDv9e7v9u/+dnf/eG9tdnpxe3JsYl9kaW5rcHdrb/19
+++rm6O3r5+3w6+z7eGxqd/r16+js7e9+/f90evv/a2hraWpsbGlteXx4cnF88Pf88e3t6+v5
+/e/v8ff17fDxemtucXt6en359vf3d3JzcGxoa3D7fHP+/Hjw6evq6+739/Lz7/D08+rwfXFr
+bW92e21pamt0++33+e7r5ejs+Pj3+v5wamlsd/v6cG93d21udWhs/O/x7e72+/729Ors7eft
+9fb07/75+f10c3t9cmlrb3NpbHV4amz5+v59+X577/T7cnN6d313efrq6+nm7/Do6vD27+n8
+c3Jydm9teHZvdnJoYWlvbGlpdnt1//57/e/p7PZ97Of+bm92+uji6Ovm5Oz39vLw8nt5ffR5
+amdfanFsc3FxdGxrbnZ6cG//dmhtfXv+8Obk5Onr7/3+/vf7+Ovn8Pf2/O/weGVueXp2cG5v
+aWx4+Ovr7vh9bWhqb3dvcP59eHh7ff92b/nu9Ojo7/L59PR+fnr/8+/2eX1w+u19/vv38u56
+cXBvamFrcGtmZW1t+/h89uvu9PJ5fe/w8Ors9Ht4+fPu8/X9ffr+8vtvcfR8bW1ndPt+dHP+
+++/t+vn8+/f+/Hh5enttbHf79HtobXh5+/Hu7+bq9Xlscmx58vh/8Hx16ufr9Ozq6OLqfmt0
++m5eXGRtc2VZX3L253xmcPrk2edZVF753Ojt3NDM0N7g29zo7l9Yd/H8XFRdbVxVRUFV+eDu
+aGDe1t57VV/l2dnb2MrCxMjR9Wtq/u5kSUlobuNNOD5R5tfvVHfb3t1mUVf51tpz98/CvsDL
+1t7g5GNe+PxOS01MRDk+T+DP0t/u3+vq82ZRSVD22MnJy8jBvL7WaGpw1tLdaENAQTowN0jV
+wMvj9eXd3GNYTklU793FwsjHycO8xXlnaNrEyutMPjYyLDFUx7vA2evX4vxUS05LWtx8z8PD
+w8zOwMDo8urMvcfnVTkxLSgz/ry3vt3jzuZfRj5ER0/e18e/wcfNzL/B6djHu7XCVz00Ligk
+Ns61s8Dw3MzpWz04P0BK1sy6t7/Gz9DEyfXHvLW12D8+NigeJFG0q7nXb8vI7kIzOUlDSuHO
+sbS+zuvavsPnv7Wrr9dDMioeHCvYrq6618rB2lI5Mz5URj9cwq+vv+pf+MjIyruuqKvEPS4o
+HBcp26umtc/OxeZYNS4/b1hOTuayr7bIVE3bysKzraipvT4xKB0UHVGrn6u+z7rNZDQpL1Nm
+bWNMurOyv1Y+XtHDr6ukpK9QOCofExU1tJ+ntcq1vmw2JStH2t/VVb20uMFXQVHWz7Kqo6Op
+0TosIRcQI9Cjn6q+vLhfOyUjMmrmzNG8sLK9YEFCcN+8qqGhpbVDLSEXDxxbqJ2nr7Ouy0Am
+ICpI8d7fwq+uuf5DP1FgyK2in6GrbS8kGQ8UMrOeoaqvrr9KKh8mOVXf0sWurLLNSjtKWNmv
+o56fpsoyJxwQECbAn5+qr663cC8fIS9L5PXHsK2wylQ8P0Z8tqWdnJ+2OSweEg4bYqOdpaqu
+sM0zIB4qStlu2ruxq6/TPy81VLmooJ+iqso2IhQNFTiqnaOqq6m4PyIdJT394N3Bsq60wGQ5
+NTzPq5+fo6qzzS8bDQweuJ2dp6+pqWAtHh0uS+zOwLavrbbXOi85a6+in6OvtqmyIxALDkOn
+oaiusaKpPCMaHz7C6Eu7q6Ssczo4OETBrKGhqayqp7wbCQkYuJyfsrmtqLMtGiIwSkc5wqSf
+rMs8OjwzUq+fnqrLvaqjpDoKAA7Ik5S6Rr2pr/0gGyQxT9bHqp+tw0A1SFLZr6Wor7+3pJ6o
+WQ8CDCyglqfoxLTH1S0jKy04UsinprK+0nFNOkG5pqWzy7ypoKW6NBAEDz6ima5sy8RERy0s
+NTFBuLu4sb6srus6OU6wp7C0tK6oq8JSOi4PCia6m6M/L3BYOzwmL23FveBXtayst1lHVOG3
+pqertsu6tstJQ+7cGg4ix6ewLipCNy47OsyqwlZMVLKsusHW9sO7r6WotdtN6tt04c28uE4a
+FCh5sUYfHzNhP1nApKZlKy/OrLDDuq2tt7ivrLxGOUpazLi/tbtbZDgbHSEmPDEvTu9P2r2s
+rVsuL0+xp6ikpa7aSlHLw+tZYcq9xd/Jw8tMMyUZHixS3U1Abtc5U83IwU1Ku6yjpbTFa0dK
+z7+ys8vYaFnVx8DcOi4uNEE4Kyw7XeZONjQvObenqLHFxcjP5cfAyNLPta+0yU5ubVdRPjdM
+anPc8+BOLiowOUU8PkTxv6+wvdZFQ1rWvLu+uLi5wcvebk9JSkVf2NPfVf7Szko1NTI5R09e
+Xc3IxF87U8rLf3rHtrzOeMvBy19JZu7O39bjb05GTEzMwt5JXVNVRkbZ4zUsP9K82O3Du8pH
+P2zEzOrb1dHZ5s65vlw8QGjO2N3X2/LX0dJdPTo7Pzk8ZtLN0+PYbU5IX8zK3W/8yMnP1si/
+wmBS09912cnL01Bc/Eg6PU9qQDRF+9TZVEdWV2vVxrzA4HHxZ9LUyL3Dz+LJy8/5bnnf7FY6
+R0k/STgwPWRnz+Rj7X7uz93l282+vutuybi3w9vVy9p659HrX1w3LDk/QT4vL0lZ4sDEyNpx
+z9Tr0b65uNTbuLjCz+PazF/v0svLNCs1Ojk1MDtIQE7OwLzPWt/jzr++ub3Uybe6zfvVwcXR
+wsY9KSsyPTgvPPlFPGHawMdVUdPLxb64r7vbzL26vcTQ1NG/wDYiJzjkVCsnR/7y3d++s+I7
+SdCxr7fG1eXLv8G8ub7Q39MvHCA5xMYzKUZsT+u7ub1SOGa6r6yrt89Zb7uyvcvlarxGHBwo
+UsNGKjno2M7hu7jsPD3frqmtrbnU7dC/t7m7v8A2FBYwusw1LT5oOzzNqqvXPEnGr6utrLTG
+Xt64sbCwsj4NDC61sFMkPas6Jl2ypLUpK7yppKmsqrZSTb6vp6q5HQYRs6PLKyTIqSwiSqug
+ySArrp6graulr0RFt6WdpiEEDN+isjMp260qHTS2pcAnK7ukoqmnoarvS7qpn6IlBQ2+o7g2
+LrCiJRotUbK2LC65qaesrKSpVkC9p5yeHQMSsKjEOEqfqRwbOmLRVTHqra2trKShsT1Ot6yf
+xAsIVaDHLC6mlU8UJr3uOTFEraq6r6KgqtRGvayqtxMHIaC1OTezmbsXHcdrKS5jq6nOv56c
+p7rbvq2tsiEIEqerMzmwmqYdF0rBIRteoKfoXKaZqV7FrLHJuEwPDFefzzTeoJw7GzLNKRkr
+pZ687queqXHerKvAvu0VCiijyTTNoJxZHTPAJxUlp53CdqGYqUZorKzHycweDR6vyDbPoZ7O
+JjzDJREfrZ+37KaZpFltra/GwGsdDh+x2zDRop/wJ96vKREbvabEu5yZp9zhr79LuL0eDh2y
+0i7Lnp59J+urLRMc3KvI152XpL7Fsr5BXGEfECG9PTGvnaRUL8WuJhUk5L/ouZmXqravr9o/
+5NcfDh+0PCa+nKBMLL6oKBIjz8dNvJqXpbCqq+I85XcdDiOyLyOymaNDMbmsJRUn4nM7t5ia
+qKmjq+w8WVgcDiW2LCKymaRFNrawIBYs0VA2vJmZqamepdVC7esjDhyzPB7hm6FSMMOpLRQj
+02wvXp2Zp62fn7lU2MwpEBbH6B84npzGNGip5RkaPck7M6+ana+uoajNesxJGxAovzImxJ+o
+00DJrzkbIUbyTu2unp2psKyttLppKhgaOj4oMbqrsL1vz8o1JiYwWbu1va2hoqy8ua2tXSAY
+IEFALiw/vauqv1pOVUc1MDzNtba0sbCxsa6vttc5JyUzPzQpKjVqtrbCyf5Z4s/O0u9r6t7u
+6MvCwr60rrfIytlINy0oJiguP1FIVcq7trW6wMvdfNXIxsm/sa2wus9TPjgvKiYlLUdVTmPh
+0sW/v835aOjRz9DHua6ppqq+VktPQTIqJiYrNU1uWV9r28jJzt3qbV3ty7u1rqqssLe/b1F4
+VDwuKSouNjs9P0hRZNPIz9vaz8vHvrS3x8C0sLS5wtv0YU8/LyooLDE0Nj1NTlTcv7m8vbm8
+0eHMx8q9sa2vu8PL129URzIpJCYsLy8zQvTNvrSxu8rL0u75ybi6wL2zrrK8xs/Y/U00JyIh
+Ji45P13NysrCvb/Ix7/A0drIyczGurKvs7rDaElHPzApKSkrMDtCTGTgzL22tbW5w9jq5tfR
+08i9u7q3u8fiW19lSDUtKScnLjpFWN/Lwbu5uLa4v87mY1/95OPZxrextLzG3/7jVDcsKScp
+Lzk/T+/Qxbu3t7q/ze5eXXnj9OfMwbq0tbq/zODhfEMxLSsqLTY+QE7jzMO+u7q8y/tdVE1W
+aGzfx7u0r6+2vMji6+JLMy0rKSw2P0dn2dXMvby/wMruUlNeX2Jv4MW6tbK0uL3L3NvfTzcu
+KygrND4/TeLT08K4t77L5FxfcF9SWc3Cva+qq7/2z3E8PEQ+MywnKz3dura/4VrnyMHPTj06
+RWzU1OzJr6ehn6OxWycYFh4sOUVPXr6knJ6tTysnLz5FQztDy6+suNpOdraopqipr9I1Gw8U
+Ij3fvLS0qZ2dq0wnHyM1U1RSe8Kuq7dUPlDEraWjpam6PRsODx5Gv7Ctrqqem6o8HxsgN9PM
+YX6/r62+Py46yquioJ+ksGYgDgsWO7mtqquso5ugTx0ZHzbGuutFfretukEtNcyqoqCfn6rD
+LxIJDCmtp6moqaqgnbkfFRsvzrjHRUDGqqx5Ly5OsaKen6CjsVQeDAgR55+hqaioqqOlRRgS
+H+u0vl9Iabamr0ApMcuso5+enqKsYBkIBhW7naCop6mqpa8sExAlu63FVVzNrqWyNyU0uqah
+npydoq5MFwYEFa2Zn6mko6qtuioRDyeqpts2Tr2uqrU8JC63oZ+fnZuiujoWBwQTqpSdq6Sf
+qbrRKxMPJqSezC5BvbOxuksoLbygn6WhnJ2tPBoKBQ3Uk5arr6Kkt1IuGhAbtpmmNy3SrrbX
+WDwzTqyeoaafnKY/HA4IDCuak6W/qp2pTCkeGBg5n53ILEWsqc8+Q0dIyKWeoqagocMjEAoL
+HKmWnrq1oJ+8LR8aGiiynao+McGotj8zQlfUq52do6elsTEVCwsW35ycq7GknqpKIhkWHt6f
+oX0vbKmqXjA878OvoJ2gqamtWBsLCBFHnp2tsKGbo34mGRQaP6KexjBTp6XqLTJlw6+fm5+q
+rKvHHgsHDz6fnayuoZuhyCsZEhYypZ68NVyno88sLU/Er6KcnaSqrfMaCQcUxJ2ktqmbmaZl
+JhcQF0Oio9s6vqGnSycuW8Oun5qbo6mtVBUGBhmtna24oJaZqPwmFA4XbqGsSFSnna45LDx8
+ya+fmpygpbEoCwMLOJ+m0rWakpq0NxwQDx+6qdo9s5ue1Ss02cvRsp6Zm56iyhQDBRqrpWJc
+n4+Pn2AiFhEZOL9fOLqambcsM8G9TUytnJucm58uCAENYas5Ka6Oi5a5Lh8XFh8zNS7Jmpas
+MTuwrFUzyKOgo5yXrxEDCSXBMR5PkomOplczJhoYHiMmVZ6Xp0t+qKlKMMGirMWkkpgkBwcb
+TigVIp2KjJ27vccrGRYYHTOonKrVuKCoQC+6n7Yz2paNpBYIDiMvGxUvmIuSq7mlpy4UEx83
+TOnBr6ensltXtqe3OTysmJquKw8MGF/jHBe9jI3BJLuYuxQQP5+/Hyijlq4oLKycsTEztJ+k
+sLlHGRAcTDsaHa+SnT06pJ1HGyawqzglXKKnUC/dqq30PWi2rrK1vEkgHC3aOx0ivJ6zNUym
+pEMkN7CxPy7/rro+N+O5ykVIx7K0vr7DXzg5X+Q/M0u/vmpWxbteLixB4FtCUsnB313ev9lE
+SdzCxMG5tsdSSHLXVz1F387m9Me3wT8tMk9pQTpRyMtZTte/zko+ar66wsrEwc94Ym1ZTU5l
+fl19w7nNPjM+W1FAQWrQ23Tdxcb5SErpw8DGysnM3XNcUkxNUlxSTu6/utFJQEhMREBKavn4
+08G+zmBPVvXazcO/wszV5XRYSkdGTFJh4MzHzu1USklKSEdMWuPLxcveXlBacu3ezL++x9LV
+1vJOQkNOV1Zd68vG2FVMUFZOR0tY7c7Kzd9dVGJ//nfbxr/H0dLO31JESV90WlN5zMvuUFZu
+Yk1IU2/k2dHS5F1XeexvWGbYyczW2tPgVUZKX21dW37X1/JiaHplWFRf7t3V09Xle+3d8FpV
+Z9/T0Nje2uBnUFBeb2ZaXnjtdltZX2xlXmPw4uPpfW/44t/pe2Vu5dPO1ePj3e9cU2Hn6FtU
+bN7mXU9Zev5fXvvp8G9qd+fsc33k3+fi19HW6fro4WlWYfLwXlRn5eZkVVtoZVxm69/+YWN8
+8m5pc+vj4t/a2N7q7eXh7nBz6up1Y2h3eWpeX2Rma/vp7/9wbv9+Z2Fpc3/06+7y7Ovs5eTy
+bnDv4upvZW93al5cYfTf6Pv06+v3d33+e/v5fH9+8+zz9fry73tqeOTa4G1ievRmV1dj/vNr
+XnXk5HtnbnPv5+zyf/bj3+3+6tbZ9mBfeuftbWv7fmtcVWH54u93+O3zfW9v8v/37v71bm1+
+fm9sbltoeuje7vLt7+Xs9vL+6+bua2vk1tr6bHz8/ltPVGX7b3R07O1/Z196eHRqYmlrbnfw
+7e3c2t/j7eLd6PJibOzh6OLven76Y1taXmZy+Pd3Yl5uc2Ry//ntfW3y6e/e3evraV/56uf0
+5eLv62peZvbr+PbvcXJ7efpmUklW0MTWY2Pn2e1sZ1tOR0/e0N778tjS2uHh/ndsWmB06+/1
+9e/v8PDp92jo09X4WlBd5P5MR1/Rze9XZN/sb2hfbGpsdPp2bPLs6+nm7fni2tnk6vtxb2Vk
+YV9t9d7W2t/d5ndpdft+f+Xk7ntzbmBXUVJVVFRVVlRYWltdY33r2c/KxMjKw7/BytDg1NDr
+6N3halJAPUU9MzQ/QUFKXN/IwMXIvrzH3drX9ldLVO3pY3nIvcHFurG1xMzPSDI0NConLC8u
+OlfYwbSusK+vtMLP6UU5OTk3PE1wxbSvsKuoq7a8wUIoJy0gGh8sLjZnvKyko6iloK3dXk4u
+JCYpKzJE5rWop6iinqOstr9uLhwaIh8WGSxEQtWqnZucoqajrkYrLCgeHiYuPc62qp+doKCe
+pK+70zooHhgbHxkXJ/7ox6idmpufqquyPyMhIB8iJjDdrqupnpudpamrscFbNywnGxYdIxsd
+P8PAraOem5ypt7TRLh8dISorLFyrpaupn52ksbq2vlpDRjclGR0wIBckzN92vaudnai7q6jQ
+LCcrLS0oNMKssbWooKSytbKy1URN3H41JxweLCQaIFXZ3sesnpyotqyt1C4pND8yLkq6sb24
+qqSsvLy3vlY/SONuRjEeHzQsGyBF287Uv6efrfq+qbc8NE3PXjpIua69ybWpr9xkysloPThY
+1mj4Mxww0SYbNO7gzzhVqajg7biutGpA5rnPXeq5sLvXyLjITD9s3G0/RcfB604vK1RAJytD
+SFw+L2W0w1/JuK204Oi1s8vNwrq83Ejz201ATmN97l7f0cz451A8QkQ6MTY1NjU2SO3Kv7u4
+sLK6v7y/wcr36+NtSEpJ+PhWa9XPzu1y29nY6GxPVUY5MjU8PD5BaMm/08y6vs7OxMTG2Wro
+7VNSWFb/6Vluz9Luy8jb0NbpcFReTjw8UktNWFL51Ob+3ex95eNb6ODr39luY9njamtp+Njn
+8dnPz+Tez191dUxVUlBMYGjw8d3c2c/fWnHeXnN2+WpxZlpbak5S6ubsd+XT0uPW2PLw9lBT
+VmlUTWHW2t7M2szDd1zP205R+3JYUlVaXFNNXupiWerW2dLP1czI3G30X0xYXlNmW2Tl32bb
+1NvX++vxYufqalpeXfxPSWZ7VVtq7cnM29zCzf9l7l9OUFVgc2Rp7e3xaPnf4ebp6ezf1Ofx
+3XJeWlpMVGxfY3fyb+fT1u7f1G1dclxa+/hb/Nfxct1veOjde9rT3N3ffmBmWV9dV1tdZ1pv
+7Gxk/tPm/+Pj8OrcamzU22By9G5v6nNhb+RsYunW9O/p9F9dflxdbmVxbG1q9u5r7dHPc2rX
+3XfiatfqcHLz+WZXXm5x62tjYNnwX2v94PRUaPxubFzp7Xty+efs5+Xr53zl9X3W8drcdfXe
+al9o63NmempvbF9k+etZX25+aGx+cvp7eWjz9X1eb+7x53nZz+h41tnb5e1fdt37ZO3weOx8
+Z1x6YF1ga1xm7mTd+uxv7ltd5uheXOzi2Httc9Xu5lnk0Ol+6dzY3WBn/XprUktn2FdSYt3k
+9+Pi7NZyUOnVXE3ubuzY4mnh5d53at7p5G3o8u5oZfFqVfFjWmRS2GjecuDP7HxKz93mU+rl
+7mVNeXvtWlrm0Vx8dMzZbm/a0nxgbd3pV15t72pV3eD3Xfbx33Ve5O3ucV9reHBjWVnX0WRX
+5MzbbnHa0dNtX+nka2RbXWn+W1NcaPpST17gXV5v/n7p2eLd3s3LzsvIzNbT7c/J39zN0Eo8
+OTs7NzEyP0xYYt7LvLzCx8a9vcr+49XW7+3ezsbCxMe9v9hEMzI5MSopLTpKXu/GuK+vusfK
+ztP/T1Nm79bt0r+9t7m8vbe8ejstLS8sJyUrPHDnz72wqquvvMruWE0+PENKWerdxriysrK1
+tLjLUzUrLC4pJiguP/nTxLeuqau1wct2U0Q7PEBJWmvVvbWzr7GztLrHUzgsKysqKCgrOm7P
+wLyzrKy0vctcTkU+Pj9FW9vOvbaxrq+ztLjE7j8uKiwqKSkqMEjuz8G7sayutsHcY19NPz4/
+S3zazLy1sa6ytrm/y2s/LystLCspKy9BbN/JvbKusLnBzu90VkhCQ07nzMbBuLKxtry/wMv4
+QjIvMTIuLS0zQVVe5Mq8s7W+w8fN0mpLSE5f6dzWxb65ucDBw8TL505AOjo6NC8uMDdDRlHs
+y765u768vcPO52/7dG1+e9jGwsbOys3GzdPreE8+Ojk5NTMzNz5GW//dy7++u7u9wMnPzNz+
+eWFr+Ojr1OjV2u/Y1srf3VFGRUhAOTg3PEJNVWf1zsG+vr/AwsfT0upwb11UXl30ZN/44c/W
+yM3K3uxcTU5IQD08PUBGSVPrzb+7vcDLytLyWEtPTk9GT13j0s3KzcTHxcvKyc3WcVRNTEM9
+Ojs+Rk5bbt/MxsbO3fF2X01KTlJgZ2nczMfCwsjCxMPJysrO3XBXTEpDPTg6PUZNVmj31M3O
+3fVhaGRWUFRZb+rf183GxMbJxMXDxsjKy9p1T01NRj88PUBITU5SX+vZ3XpmX2pdWVVaaunj
+4dXLwb69vr7AwMTGy9psVE5JPzo5Oj1ARUhOXevd5vZ1ZWdiX1hf/eLWz8nCvbu6u7y7vb3B
+yu5MQ0FAODExNjxCREdOb9bP1ON7bmhrY1tb+dTNyse9ube4uLu7vb7E4kw7ODg1Li0wOEBM
+UVzpysPFzNn4eWZZT09d38/Kxr+6trW2uLu5vMHXTjgzNjMuLC41QU1ZaN/Hvr/I2mlbV1ZR
+SlXyy8TBvre1s7K4ubm6v9FNMy4uLywqKzFDY9/Yz7+4ucDdUU1MSkVGT9/Evr28s66ysLO6
+usDCfT4sKi0uLCksNU7Xzc7HubO3xG1HR0VHPz9P2MC9vLmvrKywubu6vcxzMyYkLC8rJys+
+3cXFyb+yr7jbRD1GRT05P3XBvL69tKmorbS0s7jK2lIuISAoLCkmLUnIvb6+t6+xwk44OkNE
+P0Fqwbu+wrmtqKqxt7e4wONtZTEfHSQrKycsUr23u726sbXMPTJAVlJES8y2tr7Etquqr7y6
+tLnPYGLVaSYbHysxKSMyxbO8z8eyrr4/Mkvd3kVA0La3wsu5rKuywcG5u9FqcM/GzjAcIy4w
+KB8txLjeW9+vr89DTce76z9My7e/xsC1rrK/y8C7xfR12tLjbNk7HyUsLi0mL9vC0NLSvL/N
+eHjQyudgfc+6v7+/ubS6v8HGz9p06Pbo7lz94TckJy46Miw407/Lc1HMvsdaTNe7xO1oxLO0
+vsq/ur7kbvLU111b82VkU0xf8UQxNjtGPTc+T11MQEPWyuD+0L25wsvFuLW+y9nIxNxXY+bi
+a1Nx3tvqYF/k2/5NSEdAOjY0NDxGSU1d4s3O0crCv8PEv72+yuHU0ehrVmr929Lc5s/M3u7/
+/3taS0I/S0o+PEhZYFJKUmZkVGDt0s7LzczKydDq3dbQ2dTNzNTg283jYXvl62RNTl9vXFRS
+XF9QR0hOT11o9Obt9tz1Z29rduT+atzT1NHU1tXW3OHl3N/3WGP7ZlhZ9Ovu+Ozc7V9eW19e
+Z19dbF1UVlpZWW/Y2eDh29rZ19rZ2eZtb2JWZ2Vabul4cvzo5+Plenzv3+z07nF2aFhYXVlb
+YV1i/9zj4t/b5vbwb1da+HJ0697i4uju7P1vfevd5vDs3u1qX11ZWV1mYGBpbvf07eju+N/u
+Zv7zZ2Lf32lp3uT67/J9+ub/bXPr7e3t6XViYl1bX21gb+3j7f7z6u7xeWt1a2lv+fTq393o
+6OZpXml3aXB+bXzi3/x97OZyaHzx++z3e/nzY2RpZGRtb2xzcP30ce3n937v6Ots89jtZfn7
+/OPf9/js529ifm1ka2pm/HJdb3L9bWJsd+fm7+vg7Wj63Ol39+d+b+3rfvf7TlzSXVDaz/R+
+eXvxflln9+NsXHvib2/f5fJz+3nsbmJobuz6ae/db3bd5u/rbXjmbGZ1dPD8dP3ud29qbnts
+Z3dveff/3+dvZ+nv9fD36d/r7+ba2XBj5t1fW2JuWk5PYv1dV17u6Wpg8el/aXTl5+976dne
+49PKxs7dz8rN1OPUydJkT1xrPi0vOTYxNkXr1+TOv8HL0szDzOTWycjJyL65vcO+u7u9v7q9
+KhozOBwcLmq4RD+moc1GYL3MKy7Dv1BNx6uwWcipsMnJta65yrixOxgfPB8YKGu3y0iqnbJP
+9OJCLS1H91Hmu7a7ycCws8O8r7K9xLWuyiUYKDkaGj+5vO/ToJ7ORsLJNR4ozEwtda2uvd6u
+pr3Kra21x8msrk8lGiYzGhpduNDqyaSi4Ei81i8nKTxNOlOvrLa8tKy1xLKtt7m9ta/EMBwd
+MSQYLL3CXWmtnrc817s+KCkyUz46u6axvbCqrs2/q7LSv7OzvT8fHS0lGCbTxWLrsKCvS+XB
+PyouMztFR8OqsrusrrrFvq2y07+utsJyJxwlKBwiScbM17qkqOZnzlIvLTI/P0TOtLa6s7K5
+xr6vr7+3rbi+yiscHycfHi/Hve/Dp6TFTOviNSsxSV1Lf7iyuru1tr2/tK65v7Cvu78+Hhwj
+Ix0mfLjM1q+ntlJP7EAuNktRS+C+t7m7tbfHxrewtbuyr7q85SYbHicfHjy0uuW5p6riPnP4
+NC9AX1BWz727vru7xse7s7K4ta6yubw5HRojKR4nw67O1K2mtkJEz00vN1VZSFXMt7zLu7zM
+x7evs7qyrbe81yQYHywfH1Kvu/+/pqleOt3ZNjBFXExW7r2ywMS6xcy9trG0uLCyuMYtGhso
+KB4wtbDh2a2mwDlPzUMvOlBYXmrDsbvGu7/Hvbiusbuzr7a+Rx0XISsfItKsvFW+pa1FOtLZ
+NS5Cb1tdzrOxyMm9w8u9rq65t6+1udoiFh0tJB9IrK785amm/TFlwkQuMVO/VkW3rMD1yL26
+zrmosb62r7PHKhUaLSYePayr0EuxosUvSMRfMS9AwctDy6yy/f/Dt7zBrKu2ubOuvy8WFiks
+Hiyvpr5Hw6OxMTjM3TcqMLq6QMerr89R3rK5x6qqtbi2r7kxFhQoKxwrsaS9Q8qjrjE5zdM4
+KTS5ukrLrLDOUdOyvL+rq7O3uLO1NxQULCwcKrCkujjhoa8vNM/TNSU7rL9Eu6qy4UrGr83D
+p6u0ubSuvC4UFS0pGy2rpsQ9y6C3LjzJ5y0kTKnLP7WouFNJua7pv6aptbyuq8YjExsuHxtI
+p6v4Sa+i9y5fyD8oKeWsWVCrrNRY2rW4XbOkrriyq61tHRUhLhwezqayUW+pqzs3z9EzIy24
+tj++p6/fVcex1neqp7C7saizORoYKSgZJLSrwU3BpbozTb9UKyc9udNOr6i8/NW7uN6/qay3
+t66tvjQbGikjGSm7r8pWtaS/OuzERiooRrRbWaqpwvDHs7xTuamyvLmtr9QvHR0qHxovw7/b
+7q6n1EfFzDssLWPCSNaqrcnYubDD26+rtry4rrnxMx4eKR8aL9jK7OKtqdJevtI+Li983T7J
+rrjIw7Oxx8Csrru9tbC+ZD0kHykiHSxW7Fdes6vI17zMSzU29Vg22rXC3sexsci6qq68vLG0
+zeluLB8pKB4lPFxLP8Wuwt+7u9ZMQ+t0OErK3W/Qt7G8uKuruLyzuM737j8pJSwpIyw/RDlC
+x7rOyrq5xd3TyVw7TGFOV823tr2xq7O8urzPYFnwSi8sNTQsLz09MzZUzebou7G1u7u90kVA
+U05IdczJxr23uL7AvsTO0dPoTz9DQzYxMzEtLTZKXeDGuLK2vL/OTUJKTUxO98nGwrm4ur6+
+vL7Ky8jWe1RDOi8sLSwrMD9k6s+6try+wNFoTU1RSklt083Fvru7v725vMDBxMbL5k47MS0t
+LC0xOkZkz8fFwMHI093lXE5OU13s2tXMyMS+vb2+v7+9vL7Kf089NC8uMzY2O05lbO3Y0dnW
+ycn1UlNXWFdo393fz8jFwL67uLi5u77E1ltFPjw2Mjg4NzpBTlRa89XT19TW4nRWV11UXvDc
+zcbCvbu8u7i8vL/ExMjTcEM7Ny8tLi8xOEFb7t/LwsXEyNbjT0hRR0pYatDEvba4ubW6u7zB
+vsLL1l5NPTMzLiwxMjQ8RV/Z0cW/yMjL3eRVUVlJUnx+2cjCvLy6sbe5tb2/ws3SU0E6MC4s
+KS0wLzlJa87GurS6vLzS3lVFVkNAWVh5zcG3s7Susbq4vsTCzeFPPzUsKyolKC8vOE7Ru7ey
+rrC4vtLqSTRBRThNedvExLavubavubq7xMDE51hMMyopJyUlLDY9VMGxr6+usLfNVU9GLzRO
+QEfdxbm+uq2vurW1u73LxLvUSE06JiYkIyQmME1vxK2pq660t8o/NT05LTxsX8vCt622ua+1
+vb/FvLvLw7TPPDwpIyMeHyguP9C4q6aoq7LB7T4uLjIyOGzIvba0r7G6vr7FzMvCuri5tr1Z
+Ri8eHh8gISg+vrCtpqKmsOxGPzIqKDFN0MzAr6yyv8/JxNRv076zsbKvrrxMMiIbHB0fJjfO
+rqakpKesxTsxLSwtMD/eu66vt7m7z1ZBRN/GxLyuqKipr79KLiEYFxohLELEq56eoau8bzUt
+JycuQN66tbaus81hQzxJUPTBs62oqKipr75KKSAbFhshLli7rKOfpKzAQTIrLC80TNe4rq++
+z3dFRTw6U9O6rquqqayvssJkPiomIBseJTPiua+qqa2wxEs2LC02Q/jNx766vcnrRD5GTtjC
+urCtrKyvub7HdU86LSUdHiMvUs+7s66rrrj1PC8sMjxcy8S+vLq9y+9NSkxe08m7sq+tr7a8
+xtLrUDwyJR4fIi9Jzrm1s7GyuN1ALSsvOubFu7e6vcDF2mdNQkxvxbKura61uL3M2edLPC4j
+Hh4iLUvJtq6vr7G82EMvLC4737qxsrfB0OhOSEJBTuG+r6qqrLO/0Op16FE7LCAcHSU17byz
+sLGxs7vURjItMkLVubO0vtRuTk1MSlVwy7eurK2yu83lfdrQekQrHhscJDXhtq2tr6+yu89H
+MiwtOX3BtLO8ye9VWF792s7BubSwsbi9xM3S2l9KMCAbGh0pSrqrqKussLvKVDkuKi0967mv
+sLjE2/5kZP7cyr+4tbO1ub7Fztb7SDUkHBocJDnJraanq6+8z1c6Mi0uOV2+r62xutBlWFJd
+9tnHu7Swsrm+yM/Q1mo9Kh4bGx8ubLSpqKywusnsSzszLjRJz7eusLjFe1tVUWH40L61sK+2
+v83e6NvaaD4rIB0dIjBuuKyrrrW9yNdbRTo2O0zcvre2usbfZlhaaOLLvbe0uMDP7HR739HN
++zYlHRseKUq6rKqtsrvAyM/ySz03OEfiwLq8xtl7bvbfzsjBvLu8wczmfnLt0svNVzAkHh0i
+Lm61rq6zvsnJyszmTUI9RfrNv73DzN7649XMx8rIxcnLz9vs9X3q3u5dRDktKCgpM0nZvbq8
+wcbHxs7e7F9eafze3NXU2dHNysjN1drg5ODc1tDU3nJcU0tEPzw6PT9ETFNeZvzh3dLKyc7Z
+/WZkcurc1NHT2tfUz8vT3XtgZffYy8bJ1m1ORUJCREhMTldw/3F3enFlYml85ujxbFtefdvO
+zM7V293Wz87Zb1VNUXjSyMbN7lNJR0ZLVFpgbn3i2d/g9W90ee/u/3h4aWv559/c2+Dr8PDw
+9WxnZV1cZP3f29/lfWhZUlVVYH3g29nb4Op4ev768f12a23z7Ovp5eDe3N3j7m9hYWBhZmlv
+cW1ufP1rZV9XU1hfb/Tn3Nja4u97a3bs39zf4OPt/u3f39zd4fVdVVFSV1hbZHvw5d/vaWdv
+//Z+fHl88+fi39/h7GxgZXD+8e/1b3L27d/d2drneWddWlVPVl1u7d7c6XpnX2Bn/ertb3Lu
+6uXi5vL27eno73pwen51f+Xb4+fi5+7r6f1nWE5OUFdq7N/g7HJfWFtleO7y9O7v6+nm5/rv
+4t3d2+Dv/P39+vLl3t/sfXdua2VdV1RSVlxod+rk8HdtdvP48ejl5uno9nx7+v5ubXnr6+vt
+7vd99uri4+Dn8XFobGllZGZsbnJ++3Ftd/13/Ofn7v52amhpZmt2fvf37+nk3d/o7u/z7urt
+739vbWhoYF9rcHFueXZ3fn9+eXN68erk3t/q6ur0+n17d3ZyZ2hyd3Z0d/7w6uv+/npram5t
+bvLxeH3x/v3q5PL7+G9ybWpreP11c21uaGh4fHv2++/w7N3b3d/b3uzx6+51dW9tYV9lYG1s
+aW/9e3t1amdlbHBkZXn2dW386+jn5uPpfu/s8Ovk5Ozt+HpwaGVmbP15fvX6efr9cmx37vP1
+6vVqbntudWpocXD3/O/ve2ttcnp6efLzfe/u+P159+x4bH9+fW9rb3xtaGlzd/fl5u7t6Ofj
+5n78+np1dXFnau/+ePzz83t3f/xsX2V1b21scnR79e7u7vttbXlqdHV18+7r+m/25enx8On0
+8OTudm5tbmp0eX3yenP36ur+//726/j1e29veXtmaXFybWtvb21+fW9sdfv2d+3t+e3s5ev0
+fHb0fXFwcfn8cO/v7evs8fn0fHh1evl7aG98bGtqbnn6/P7q7fnu6W9oanRsbHhubfn+bX5+
+/O/r5X799e/t7vDx8/d47vxtfXZt//H1+npwdHdyenZ8bv1rYmx/7+3u9vDv7+339/nw7O71
+e2/+8XFvcHBtZWz1bWlrc2948+7s+uzp6/Lu8O/s8vPz7nV8fHFvbft8dG1ydu7y/ejueXJ3
+cWVqbWBif/7n6uzn+P7o6ursbPl4b3tufW9zb/71b2px+fv67+/+bv76e3J+7fr/5e5+9/Lt
+e/f79/x2++77aW9tbW5sb2hebXd1d359/vPz7uDm3+v+9u3z7uz09210+f387vxtb2t2enBy
+bmpuanB2Zmr4ffzz7/B/eH33+OHk7/L9+vTp+3Z3dnhvb3zxdP/79nb35/H+9eb8bfP6ZmBp
+Z19x+Xz8fvHm4uTw+Pn2/nF6+29r7/d08/N0cnZvcW9vcmxkevz28Xv+8/H+8fbw6+339Pru
+4u/s5O3y+Hlxb2tibHFyf3VvbXZ0b3F6fvvt/3h88fr46vd77vl+6/h88nf6eP7n8Xzyf/7z
+fnRpanZqcHZ5dvb5f+3n/Hb58m/76vb+dHXv+np5bXjz5eXq7/T1eXZucPV9f3n4/f3+/Gxi
+bG9oe/P7+Or+8eLx9err83d8+XdmaX12cXb3d3p6cW92+e7wbvb07Oju6/fy/Xvv7vx27npm
+b/VvZnR5bmtvfvZxbnf08Oz27Px37fnr7fnvevlvZ21rYGvw8/7n5vXw7vHr7e75d2dkb292
+bfR+ZnZz8O/tdmt/cnl5/nz/4ubp5+Xj7fRua3prd/1wbnj6+vl+/Wz7/XF2+3Fr9fxsaXdv
+a3rw/vn36e93fHb4fPz16evw6+z1cG56c3b78vp8e/70c2129nJlb3NveHfv7Hn85+nq6en7
+eHZ+9nxwcmlgbXJsdnD7eX3s7e7y8vr4+3p6/W16+vbt5u/s7fl4eX19ff9tbfv2/HB+fPx0
+c3lybGlucPH57/Dv/fjt8Pt58+n5fvf99/N0bm1zcHJ9/vT9en729O/o8/f76/N2b3lnb/v2
+92xkZXb+dHX28/r48Hx+9vJ+efzw+HH39Pf2+u/ufPjpemz2fXtxamlx/vj+a2pndv54bfjr
+9vzz9fbr+33q9/bm6fv+9e7u6+77dWhrbGpuaXR6aGNrdm1zfv10dfvt6+/p6e/s5ubsfPzv
+/fH++PHw82p6enRqa2ppfvz0dHj47e3ky8hYTmtmSlRu5tzhaezQ8VFRXVXl/1Ba2tbv3sTH
+38nUeHH7SU1bVGne7WPl1ujp4nZuZWhpYl1qaWpmanvf51lT7+rs99HO82h97W5MVNbO1m/U
+xtVJVm5dU1tUadTacvvZ3W9ya3pcZmth4O9h89t0dN7o7txg8OdZS1zt9mB46ttpbeLzbGvt
+19h+29HYY2J852FXVGbtYmbh6337bm1kW3N7YGfi9Hp97eXm6ubjanba5Gtw7edja+HfZ2p7
++H1zZWp2a2/s3HJleWZgaGd6/Gx46vT24uv2d/l7ZGt+b+/g4+7m5/3u5Xxob+zjdGz95H5f
+de5qYWFoXWF24+L/bPn9aGRu5Xn+59zi9efte19de3JeZ+Lrd3fp5Pth7t3ubHbj73h09/5s
+Y2/d1vNt7uhyalxed296bO5sYXrwbm/1ceje5u3s+Ojq6nlnbH9hX2tw9Of9Zv7u7n1q8vL+
+/+fr9WtueHFoaW9ndGvr+3T17er27N7k//jv8e77dvj2+v5ianJlX2Rv8Xdt9+r4Zmn9/mb9
+7uNwduji+mJ7+Xpeaufrbmjp8u3s7ePkeWxy8Ol0euryfGRf+/NcaOjwaWb0fG1lc+z9a/fu
+8XX/8u18bvPv7n3j3eVnYG79Zl76+mhf9OTu+PHm919k+/J0bnfv6vh66uvz9/HsdGn4fml+
++PDrb2J76nlbWWx2Yl9z/HH78Obn+Xrh4WRb79x8Y/be6HF83+pnb/f5cmrv6Ph7e+xybV9q
+aGhucn75/350cer09/xrdvPy+X117eRtcnpkYmdv7e5+/vb06+/9+uj4aXL0cnv47+fm5uXv
+bWx/7v1saWxvZWR16vthbvjtd3f9dm1sbebrZXn9/X1sZvH4a2r53e396uP3++/zbHvj8/p/
+cPLd7WZ69e58Z2hpbV9fbfj7bXXo9mPv3vF2ffNyannve2997Ov0++ft7vF26fh0e3Lv62li
+a2V8/nH8dGt08/x2b/zlevXr7PTs7Pzr+nP25up3b3Ztce72cG9xdvfw9vp6aGt1b2tt/nny
+7e3vdf75fHJv9/T+bnJ+bXNs/unw7OXr93t66ep27ePwe/Ll/m598+3wem1zbmdeZ3pya295
+amx36/R3bWzu5vX98eXp7+Xl5ntv+3VocfrveGtvcXf4dm/u73V3fH349PP5en7k4vZ2/fP7
+cm10dXV4dG99+Ht7b3X29/jwfv7v+Pnr5/z27e3zdnp9/W1kcfb5bWhsfe3xfHFpaf7q+Pfr
+4uN1cunvffX56+Tz8+bsfmtoeH5sbG93bWFp+HZsdmh47+7vcHP8fG917+Tl7Oro6eLl5e15
+e3x9bWhw9f1ra3H/dmVjb3B3fXXr4fp5fnj67unh4+r29/hvb/f0fWls/XttcP/8dmtraG12
++ubj7/787+v9b+/s9m/8/Gr99+/jf+7o9353/P9xbn5waG9vbHv8ePvu/Pnv8/Lt8/jo4OPv
+8Pp/9nFqbG5vbXV0c3zxemtxcmhobH7v8/54fX7u6+ns+e7n6/Tr5u70+3v2fXb57e32/HJp
+YGJubGtzfvX+evf05uvufHb17O39e3VzaGppcu9+6uP3fvn0+Wts/fr79fn+8uzu+Pfs8ndu
+aGhrcW9v9/D7e35+fO/l6PBv++v7efz1+/Lq7fh7eXh9/3J1cW5vb/ns7urzd/HvfGlncf/9
+cWtt+3Nzcnb6+/Ds8vLu+Pv4fPDm7/zu6fX/8+ro+3h2cG1pb3dwZnn1eHx7e3d2bWxzeHB0
+fPN4cury6+nx9fPzc3p5d/pwdPDn5evr7vh8/vl6dX75bG10e/V6dHf99Pb6eXR7dnhxanj9
+fnj76ens6+f2fnX6+XF0dXhtamptem50/fn9ffp0bvrt5+jw6N/t8vX38fx0aWxvd/x1dHv/
+enZubXz+dnB3+PXy6fL17+3t8fHu7ftvZmBfam9paWt57u36e3359/d3bm939vDv9PTw+v7y
+5+bxe3j1+vnt+ndy+vtvaWhta2pxcG1oZm5ub3v/dXjz5ujs8enh6/Lz7fv//vx0cPrufHP+
+dnF1cWt99vv/cXZ7ff15bnbt8nt2+PR0dO3t9O3vfm9393ttb/338fB9bXr99+3vbmZxfnVm
+anN9+Pb6fXr28Pj+/vV8cf/s7O/s8v728/Pw8fT//3Rsev10aWZlam58/29udvz6dGp07O/1
+7Ovr5t3j+f51cG1ranl8bG9+9/Px9vf3+v52cW9vcf19fffv9fbs8npzfXFxeXZyfPTv8/1+
+8ez2/X9+bmlub3Jsbv/19Hh/7vF8/Hdy9vZ5d3R78vZ+fPf7/Xt6/fv39vt9c37r8fvq4ep9
+b2tvb2x1a2JocnBv+u/w9Pby9HtpZmp0/fv4dXN67ePm6Ovm6vhvcXB19vB6cn17b3NxbG9t
+ZWd77+/u929u9Ozx+Pjs6On/cnpvenhtc3z9c2hnZ2hpa3F0ff/57e338u3r5+rq4eHt6+3z
+8vj48HpucWxkZmhhYmhpa3JuaXzx7uru7u/89/d/9+/y/nx3+3ht//Lt6+fp+HT3fmZnaWx3
+/3x4dPz6e3Fz/evw/nz76+/8/PXy+P5/e252/G9mb3/9dn12bHJ57Obl7fZ8bG568+fm5un+
+a2ZsbW/77XpeW1xdY333eX58/uzm6ent7fTx3uHs7eTf2t3m7nRoamFdaHP5d11WUlBWX3/g
+2trg821pbXv9/uvo8nJmbHv77d3p+PTq4+v4e3bz6uDY7Gb382nq4378aWhyXkxHS1jl2Nvi
+eHFtbG9rbeff721fVl3jz87T3d7f5uHk7Orb0dHrWU1SamdXTURCTGXi2tjU1NvzW1FXbN3X
+3epsYFha8+HUzNXZ33zo1s3L097kfGVeVEpHRUBDTWzTyMvV5mJbYG78+/Ps9v9sVFjy4ODl
+fnzh1Mi/vb3F2mlPTE5LSEI/PkBLYuLRyMvQ3XFqb3vf19z4ZmBkXWjt39Tb6dzWyL2+xdJx
+ZWRPTEg7NjM2SuLFvcXS9lVTY33bz87P4V9RT1N0zsrM12Vp2sK6vcXO6PlpTEQ/NjEvNlDQ
+vbrC1GtMSlJn1MrM0O1YU1tWds3Ky+xTbNC9tLa+0GljaExEPzYxLjNM0b68w89oR0VPYdfO
+xsbsc1JKX9nBv811W2vNuLK0u9ViZVFEPTkyLCozUsS3ub/OZkxKS13by8bH6ldOR1fdycPP
+bFhk2byyr7G+2npYRD06MisoLkfGtra8zGVMSUxi3MvAw21IQz5R3s3H0HJiatu8tK+uus50
+Wk8/PTkrKC09yra2u8dfR0A+THHPwL7UUD44S9nBustaTlHuv7SsqrC+6E5EPTw3LScrON63
+t7vE7FBIRUlV7r+8zWE+OUbgv7nFeVJMaMe2rKquu9VVRDo1MysmKjfkt7O5w/5NSUdW9dG8
+vMhpPTY/5r63xmNHQVnHtKyqrbvaWD42MS8tKSs55rq1usLcU0pKW+HNwr/OYkU5P+rCuL9a
+QkFWwK+qqK2821NCNzEvKSUrPM20tr3KbE9KRE79yrq6yWU/N0PcvrfFWUI/V7+vp6auvt5N
+Oi4uLiknL0fHtbm/z2RTTUZS5Mi7vcxmPzlPzb64z1dKRXi7rqmqtMHaTzouKygmKDVqvra8
+y91rUlFOYM7Cu77mTj07asi9u9pVTErdt62oqrbKWEY2LS0oJCo37ba3vcL0a2xLT2PmwLvC
+1Us9Ql/IvMPkUkdN57usp6u2y39INispJiMtSMu3uMHI3WdiTVbgzb28y/5KPkvjx73NXEpE
+Uc+4q6est9FXRDErKyUkLknBs73EzXVtXlBh6su8v8l8RT9Szry93k0/P1vEsKmnrbzUej4v
+KygmJjBOxrq/x8/hcVtWfNXIwcbPZEVBWcu9v99MP0L7va6oqrC+4F49LionIyYzYrq2vsXX
+83dZT2bcxb3Bz2dFRu3Fu8JiQjs/57itqKu1wNZfOSspKCYrOfu7ub7G3nRnW2h9483Mz9pX
+Q03fw7vHYkU8SNG3q6ittsbfazwvLSgmKjJYwr67wtvjZUxUV3vNysfPX0dL3b+7yGBAO0jT
+tauorLTC51Y4LS0sKystQc+8t7vM315SZFxs6N3Lzl5DSuK/usdlPztG07asqauyw9fgQzMv
+KykoKTZqxbW3vstgT1pRW97Sw8hwS0vfvbjGXT03Q9y3rKmssL3dVkE3MC4qJiYvU7+ysrfE
+81hRSk1o28TE3k1P3MC5w2lAOD70u66qq664zPJOOzMtKSYlKTzdurG1vdJWT1JNcNrOws5f
+XtnGvMVzSTo9Y7+vq6yutcfeX0U4LywmIiQwWr2wsbjJa05OTFXr1sjJ3l94z8S/0FE9PE7O
+tq6srrC5x+RMPTErJiEhKULBrq2xvdthU1NUX3/WzM/eYuLLx8xgRT1E9byxrq2vsrzQe0My
+LSgjIiQy4LiurrfG4FZiY1hgXvLSz9rd2M3I31JEQVfLurOvr6+zvMtYOy8qJB8gKkfEs6+y
+ucPN1vhWS0dKVvHm/dzMxcfcXkxP7cW7uLSxsLW+0U86LykjISUvTsu8t7i5uru/y+VSRkVI
+TlFVaNzLys7o+dvNw8HCvry8v8ncVkE8Ni4sLzlEUl5qcODIvru7vcXO3fBrU0tMU1pWTk1Y
+99fPzcnDvbu9xc7a4/BiTEJCS0tEPzw7P0pZfOrc0c3Kyc/d7fHq8WtZU1vu2dfZ1MrGw8PG
+ysvN1udaSkpKQj07OjtAS1JWZ+LOyMbHzNXe29nqYVNYaO/e3N3b0s3Nz9LOy8rO23VfXlZH
+PTo5PEVNUVl83M3HxszkcvTzeHRdVWri083P0dDR0tPY2dLT0tr0aGhmWktBPz4+REpMU2Xc
+zMvQ2unu3dzl/GVf8dfUz8/Pzs7P0dne2tvlcl5ZXWJVS0Q/QkdIR0tX7tHNzdrz8uXd4/lx
+c3Ds2NXPzMvJx8vP1dve7HleUk9XVUlEQkFDRklMU23YzcvP2tzZ293sbF9jeOTY29bOy8nG
+xMnO0dnuX1BNUExGQ0A/QERGTFdo4dTT09TW0dHS2ex2d/7v5N3b1tXPyMbIyszT4m1OSkxJ
+RUVCP0NGR0xYbOHU19LS1NLP1t3e7P9uafvm3tjRzcbCyMvP2+VnT0tKQ0FDREdJSk1ZbOjW
+1dHP1t7g5+rk82xsbHLz5t3e287GxMXHzNXqYVRNQT5AQkVKSUlSbunc2dLOz9bk/W/07fJ7
+e/nj2dXOzs3Jys3Oz+FnWE9JQ0NDQkZMTk9aanjk2NfRztTd4uHc3uj79O307N/U0M/S0dLZ
+3e54Xk9KRkRDR0tNUl1oce3i3drV09PX2tzi8Hhv/+7o5N3U0dfb2N7n9HRlXFdOSUdJTU5O
+WWh+6eXi2tjY193f4uDe5P555+js6+Hd3Nzg3t3o9m5dWFdSTUpJTVFWXGvv5eDd3t7d2tjd
+5+bn3+Xu9PDy9+jm6ODZ2+f5bGReWFhVUk5PUVVYYXf+9uPd3t7j59/Z3d7b2t7m6vpyduzs
+/fDm5ubuf21mZF5cWVZTUFFWWmBsbXXx4d7e4N/c2tnd6u7l5uz19ejm6eTt8enn8XRpY15b
+XlxaVlZaWl9sbGl6fnjq39/l39rb4eTtevvw8ujh4+Lj7/v1fnBqY15dXFhcZWlna2xpa3rn
+4ufp4+Ho7PD7+376/Pf0+PPy8erk7Ovr/25rb2pkZGRlaF9ebPr/dvvu9vru5+jt5ubveXZx
+bHFyfv727fTz7Orn5Ov9e3ZsY2RrbGBjb21rc/r5+unp7u3y7evw+3d0cG9zc3J2eezp6efk
+3+Xu7vJ9d/5xZGdqYV1iZmltbWt3+fXy7/n+9Ovp7/Py6Ojs5t/f5ejs9Pb5eW1dW11cWlpc
+ZGNka3d9/uri5Ofl5+fj6Onq8P797vH77ufl3t/n8XBqdX58Zl9dWVlaWlpdX2RlaHfw5ODe
+3t/j6+Xk6evm4ufs6ubo7vLu8XRqY2JoYF9dWFlcX11fa+/y6uPe29ze4+j5+PP+e+3k6+Pg
+4d3d4/dqYmNfXFhVVlhZXmhx+nZtbf748Ojr8vLk4ubq5OXn5ubf3N3h5O3v6e12ZF1hXl9e
+XWBoaGVmYV5lfHhy+e3v+/v88ens7Onk3uPf3d7Z2+X6eG9tZV1aW11dXF5gaHH39Prz7ezz
++nV+ff3s6eTj3drX09PY4uv9aVxYU1RaWlhZW1tedfH08uTl8f5+cHN59Orl4d/b3NvX1tje
+6393aF5bWVlYWl9gXmB2fW5xfX797fF0eunf397e3NnY2Nrc4ux7amdjX1tYUVFXW15eZG55
+/e3r9O/n7u7o39jZ3Nzb2dfa3Onu73liWlJPUU9PVFxganf97u3w8X10+Ozo6ebi3NjX1dHQ
+09PW4vLzbVpXVE1NTkxRWl1eZG54eH3v+Xh78uzr3NbV0s/O0dLP0Nbc7n5tWlRXTkhNT1NW
+XGVmYWxta3n0/e7a4uri2tnU0M7R0tDT3Nzg++xjV1hYTExLS05QVmNcXGxo+e3o4tjn5d7c
+2tnZ09XRysrOz9Pf23pYT09LSEtISklTUVRXbF1v6/B3393f2tnT0s3Ly83Fys3Ozd7f9FlN
+SklBR0BFP01JUk5nZ2jv5OTez9TRz8rMy8jGycPDycvL3u1uZU1DSD0+PEA9QUpLVFP9Wvjs
+29zOzMvLx8bHx8HGwcHBzsXO+NlYTD5FOT03Ojs9RkNMT3JX1erW0cPJxMjAyMLCwcS+wL/F
+xNR490s/O0cwPTM9NEA/REdYc27P1srGv8DCw7+/wLy/vr3Dv8jMYOVJRDk8MTQyNDQ5RD5Y
+T9dnxs2/w7y8v8G/wsXAv8G7vcLB2mxSQjo4Ni4zLzYwPT5HU+LN1L2/uL21ur2+v8nGwcHE
+vcXKaG9OOjk1Nyo3LTcwSUBO9czAx7S6t7azvMG8ytnfxtXRx8zlRdw/OzU/LC41MDU38z/z
+y7jcs6+4uK+3wry96/fN2mjK2Fc//0EvND0sKUIvODzLP867tMirqriwrbnMvspXXNteSO5n
+PztZMi4yPikzOzs37NP+v664uamsta+uvsbG0kp46VhN10EyPzkqLjMtLDg6QUnF276wrrep
+qq+vsrnBytBgT2pWV+1DM04xKC0vJSo1MzxmvNa4qa6ypKexqq26xcPjR1BsSUzaPC9QLiUo
+MSIpLjc2b77ItKerrqOprqyut77FzVtP8FlcXzA6PSUkLiQgLCwxPs7Luamlq6OjrKustr3G
+zdxo5uR35zgwSyggLCkdKisvNfPYvq6lqqWhq62vuMHL5c7s3MnJzEE0UiggKyocKy0uOebc
+vK6mqaahqq+vvtXX62ni2MXIzTc8RCUgLyceLy8xQMjOuaukq6SjrLW1y+j/6PNyzbzE7TJO
+NB4fMh4iNTY4a8C2sqqiqqmmsL+8zOxZ28/cyLLAPTRnIxsnLRopPDk/urWuraKkrKqsytHT
+b1llzL7Iua3FLz1JGxkrIhkuRUFnsKmqqp6lsKyyXmrva1vMu7i4rK88K2gfFCArGiZH3FS1
+qKeuo6S3vLlYPlFgY86yrq2rqLYsKTsXEiMmHDPIt8Kpn6ezpKvQ4tk8N0nRzbmqpqeqrE0d
+LB8PFiwhLd+oqa+knbO9rss/PkM6POy0sKijn6aswB8ZJREOIC8txamcpKifp17M3T41N0BV
+ZbSop6KhpbHPRBcTHhIOK2FqsZ6YormosDE4VT82Psi1xKqepKekq8w8KRITHBMXUMGvqJ6b
+qPbG0i8sRNZTWrGpt6uhpq+xuFg1LBQTJhkXfrG1q6WeqTtRwjIsUr/F+a+muK6jqrG7zl8/
+ORsPJSQVLa+6rq+hoVk1vdUvOL67X7uqsK6qqqq8cmBlSSoRGTUZGNq1tbCrnKo9PtJuLjC6
+uNa3q6epsaiq6kVn604mEB43FRrDsK6vqJurMDXNOyYwrqvJs5+muburrkY198ZDHw8iOxUd
+r6elsamYryMmVEYjM5+ivq6kqcBer64/NH3DviwPG1EYF9Kjn7Sul6koIi9bMCqrnqyvr6yy
+a76u2T0616q9GA0zLg8hrJ2esaWaxyQdIMtAJaSYqLPSs6lHU67JPzrEo7UdDic2FBq3mZe1
+t5quHxUguEQlppWhvFOvqjk2uL1ANr+cph0NIjYUGLyVlLW8nKsjFBrLvyyzlZy9SMquTTLW
+wdJexJyZRwwNLR0QL5qOmcisoTMYEx+11U6fmqC3QMG0PTlg1ru7p5mbPAkKJRgVVZeKkc7X
+ryoQDx+1rbqdlp7FNDtPNjRdvKahpZ2fzRMDDCIhXJyNiZ8pMycXFRrpm5yen6m+KRwt6s28
+rJ6aqczFvk4SAg/Dz8+llIykGRwvKiIksJScvt9tNyEhyKCjrKmmsT4rXrO+NxkRITssX6Oa
+m7MyLywsPWK0q7VbMi442q2moqS6OyctWcuwn5meLgkHHDI7q5SLkzwcJCEfJmafo+ZKPFXO
+b7CkukgxLD/BqJqZnaU/FAYFIqmelZGSnh4MEiBDt6SVlvIeGRs12LidmaVnJSy9qqWkqqy3
+OhsLDG2gp6atrbEjFiherKu/srIvGRYivp2Xl5usLhkcP6ufnp+eoas8EQYJKK2kpKOcoS8U
+FybLtL2soK8tGhvmnJiYnrouGBYss5uRkpipSywXBQMTqpSZqKSe5RcOG+umpqefosEkGB/B
+nJyhqtMtHiA/rJqUmKpBJi4sDwgTsZWcvcuosSoVGDulnKOrq7A7GhYmppSbsUQ0LCIm052S
+lapZOTYyGQwPK56YpLi2vDMZFC+lmZqhqr4tGRUd3JuXna1EJyEkNrKcl526SljLRxoNDiui
+qbu8q6OyJxwvu56hts9kLx4bI76alpyxPCQdHCfDm5OYo7XgPSARDg8kn5mbnKKvWRgPHEqf
+mqGsr1olHBxDpJufuz4rIB0rupmSm6z6ODU0IxsZHq+bpK7DYUMlGi67n5ulv2szHhwhe52X
+nro4IR0dJtWelJSfx0YyLi4sLCMhxKissVAzTTstXsmuprVQNSorOk+yn5+tQyEbHSnlp5mT
+l6ZMJSIv7ra5SB4dPkhGSV2un7w5MS1N90VbvrisuGBPSE9LMzBgvLK1vrOstuBOarSoqr8u
+Gw8TLm6voqSnrDkkJSY/vbKsqbW78zo5P1DR4FL63trN18exsbi4wr2+XDYmHRYcZLClo7C7
+vTInLDLEqq+vscfXPSw2U2rK02tuVlRo+82zr66uuM5WOzEyLx4f4LSqrXVkumo9OTHYr7nF
+2lfdUDg+WF/Yztzd1fhVS2C8sLC2wc3P7VU+MSAfRMqyrsLFs+I7NS9fucfMdFLT9UBET2XL
+0crKdEpGRdS7x8jIzcDCwK68NiEbJ9S9uLGvqrI8Kio06txf1r+8v1U4Q2XSyebyydBROTRO
+zMnBtq6w0js73LC7LB8t07jYP+6urMk2KzZSSjxNuqeryj47bs9MNjzMs8BJO1e8t95LTGfg
+ZFDqv7Wvt8HNRCIfKzrFtLqytPZAODRTz8m9u7vITjk5PE3u58/Au79nOz1MWWVbzLGxvdpI
+Pz41O/m4q67HXEMrJzRIxLS6tLDHTzoyQEpCXM69t8haT0pXUkBI3by0vOtZX1JIRlLVxNF8
+Wk9hcFz0zca/zWZeUkZIS1F+dmB65tTO5WFVSkxl383HycfI2G1OTFpeUE5m0cTHz+xrTTw+
+Sm7QzM3Fx9llS0JDTGnQxcHG2WRPSUhV79bQy8rO511PSUpKSFfez9Pf+PjqZlxd9s7PfVxd
+89fb187O22FJQklbdNzIvsHVTj4+RFJj39DKx9LxXk9QWVtq6djMzuJoWVRYY2nez83M3Wh0
++G1ZVG7c4mRST1lkX2fh0MzUb1pVVllXcNzb29PS0dtdVFldeevv2c/Y3npcXFJScez35ePp
+3ex9cGBdW2Dq2uH+am71dlpRXeLT3vd9+vZrWVly5N3Z19jqWE5TYe3X0dPW3OdrV1hjcuTa
+4d/ldV1PTVRh8OXh3N7ofWBYXF9w4dba3/F0dG1pX1xp9uHc3uDi4/NoaGhucvPf4/BoYmBm
+bX3+9+Xd3vBtY2Fka3j46/xnYGVu/uvv8Ofe3uvz6+71937+9/z+6ebxf2tdYXL3/Hh+/nFs
+aWV2dmhv/nX87fp+/v7u7evj4NfX5/p2aW1xaXHs5uZ9aXBtYmVpZW/+/vF2cO7q8/j27+js
+9nprfXlvamv85t7sdm11fG518/Ht4u5sa29wbWZk+t/c4/p/+fz+b2Fea3f+fX7p53lv+vj9
+/v39//Xv9+73/uvn6ujzamp1fHpwa2praGVkann17e95ePt2dXZ67+7p4N/o8nx2dnx+cGlr
+//b0cWt8/H5ybHjr7Pp89/Z7+/d5fPl7+uje3eDrb2JcV1dcZnzq6fl89vj8/XNy+e7y+PHv
+7urg4+7z9P59enH+9fxxbW1vef14b3RuZ21ud/n7ff17/f317vDp5eDrc21qbnFvZWp8ePnp
+6Onr7npu/vb1fW9sfvH27e/5/Ozt/n14a2ludXt6dXd+eXR4enV4/u3t+Xr97vDv73739/Xv
+f3V9/X54bnD97/Du6u75enNxbG58dW1x+v/9enNyfvXz8vLz7Or2dXBzb2p0/H7+/3j9eGz5
+6Ojt6+5vb/vz9O7t9fN9dHZ0cW91fnBu8/J3bXF6a2ZnZ258+u77cfXq8/Tr5uXm6u73em19
+/n389+r5b3b8fX73fXZvb2plbGx4e/z0+fL2+vT/dW9se3Rwfn13a2f++fLj4eXp6ejt9+/v
+9fLv5efufm1pZmFhYmdkX2lxanf7eX1ubX739vfv+H3u5+rt4N7g4Oft8PL9eHx2bWxvbGxi
+XmVnbnn2dmdpb3Bv++rm6uvq6+vn6e35/X56cXB7//T59Hxzdv30+Pt+/fn6e3v8fHpwampy
+fXt9eXv07fJ7f/d3dnl3eXX9d2Rld/z4+Ovj4OPp6fD37Obs+v5zam5udfT2/Xh5fHVtbWZk
+aWRkZGFu9/Xz7uvt9ube6+3d3vxub3r+eXF8fn12fm508/bo7f14bWls+uvo7fX6fW9sb21r
+b35ua2397Pj37/Pu8vN+cX3s7Xxwevl87/N1fHx6cnt+d371f337fHhtbfz5//n0+3739Hxr
+bHj39vPu7ubq+ff/e31+8vfx8X1sb/ru9nv8eW5rdW9naWhtdnHw7Pl0dPnt7n77/P757+ns
+8fPv8Hl4fv///HZlZnD9d2xv++/16On1/Pbu9PX/cXr7eHn7f318+e36eP3+dG9qZ2Vne35z
+cHR9/P/48PHy5+bt6uXk4+bs5en5bWFfYGBfZWpxb2xsa33y7u75efbp9Hn8+PLq6uzt9u/u
+eW789nx3cnFyfu3wd/v09fd0anB6cWpvfHl1dHRu+unh4/Lz/Xd6d/p+cHZ5d37x7fdub/75
+//18+PH+ffTw/X59+Xt1+/R+dPvn5+zs+XdvcH92a298fHJ98u57b3V4cf3n5/V/+XdnaXf+
+dG5ueXZ1f/Tu9fHt8P569PHz/vbi5vX78ff1/fb+amlsaWdodndrbXP6+3J+8/h79+v4cXvt
+7e3r7Ofr8f14f3R8/29tbmZpbXjq6fN2fu7v7/b1c2729Hl1+/Z7dXD+dG16e25p8ujs8PX7
+8uLt+/j7cWtzaF9teP58/O/6e3n49/jz7u78+e3u8Ovj6Pdvb3t6eHl1Z2Nkb3Z0d/Ls+Hh3
+f3t+/HVz9+7v9P777vj69v51bW59/vl4/fp6++vj7vb39/1vfP1rbG9wcPrueW5zfe7p7fp6
+cGxue/x8++/2+np48/F7fu7q8/328nlsbmllav3s9n51+vt3cmluf/Du6u749/v18/Lo5+32
+89/n8Ovr+Xp5cGlfXV1gX1xicGtoefX06d/ue/fy8fHx8evl6ufe3N7e4fd6d3NtZV1lbWJe
+XWZsaXN+/vXj3u17+/D1d/zzeG1v/+3u7ujv/vfv7vn9d3hvaWVpb299+vb9fPX6eG/85d7n
+9P12b/7k5unl6O94b3J7d29nZ2RhamZgbfp6dnl2+vHz8+7q6/L3f/jr6uXj8u7h6ffv5uvu
+/3ZsaGlpZGJpbnVpanD2/Hr1dHR2bGtna3bv6evt6N/i4+Hc2d3kf2doa2hjX15nb3327+zv
+fP90Zmhsdv397+h9d+vl6+Pe5/5tcWpqbW78fnRv7+vr9vr19vV2cGhgY2x3bmx68u7o393c
+2dvd6PT9bGFeX11bWVxmb/79/PXw9/x0a21z9+/s6+Hb2trc3NnZ3/luX15bWVtaXF1bXmlx
+fvV4bW58/3Fzce3k6OXg3NbRz9PR1PD+cGZdXGBeW1pcXFdqeHD8fmV+Y19vYV1jfOze2dDO
+zcvMztTlfGtaT0xNTk9TWF5q/ezp7ff98f1rc3x97uTa1dPRzs7Pz9Pa6P1vXE9LSkxOT1JX
+Xm77+3JkZWpramhv9+Db2tfUz87Nzs3Nz9Tc6vxgUUxNTExOUVFRVl5eXmBoaml0d+jZ2Nva
+0c/OzcvLycbIztbm/mJVTkhGRkZHSk5TVllaWlpbXV5k/N3b39fU1c/My8rHxMTGyc/Y4O5v
+XFRQTEtJRkdKSkxNTlVVUFFZWlTW3dzN4F3Kxce+vL2+vsLKxdPp9FRVSD8+Oj48PD8/RUxO
+V2T969/f2c/JysrFx8bEw8LJxMbJx8/a5PR4XEhLQzs8PDo7Pj9BSFZdYt/a5tTN0NDLyMzL
+xsfMxb/Hw8LKxdPY7FppUD5JQDY8PTc7PkNITGTsaNvM4M3Czs3HzsvJxcfKx8C/w8K/08/X
+WH5POEE/MTY5MTU8PENUXtnc3cTG0cG+zcS/zM3Kx8PGvL7Gv7/NyFZL4D0uRTopMzosMUE9
+QejY18vCubnEvLXEzrzI38i/yci+wr7A1dBlQFU6LDU3KSo4LzBDSlbLx7+2trezu767xNXW
+3+rSy8u/u729ucFtRlc/KSswKCQpLjI7QGS6ur+0q62ztrW4xf7y3OV10sG7v762uMlPPVg1
+JCYqJiQnLDxXReiurLGyqaetvca/yEY6S87Y4se0rq61tbdROj0rIyIeHyUkKT/90rOqqqim
+qaywv95fRTc5QV/Ovbetqaeprto6PC4gHh0bICEjN+y/raWlpKKjqa/RS0U3Li81SMe5saqk
+oqKnujoxKSAdGBYaISc3aLynnZ2fn6Korso8MCspKzA817Onop+enqGuRywiGxoXExUbIzrQ
+sKScmpucn6m3bTMrJCElLUHGrKGdnJmanKhNKR4YFxQSExcfOr+qn5uZmpqeqLpHKiQgICQs
+PsaqnpqZmZqcp1UoGhUVExMUGB86u6OcmJmanJ+pukInHxweJzRPvaqemZaWmZ6yPCQZFRQS
+ExUZJE2yn5qZmZudoazCPScfHB4nOd+2qJ+bmJmbosM0IBgXFRYYGhwnSbKgm5ucn6Opr8NG
+LSIfIitDv62no6CdnJ6oyzcjHBwcHB0eHyg7w6mhn6OorbK4w2w5KygqNVzCtK2qp6Wkp6y+
+WzYqJyUkJSYmKjNHzriwr7K6v8bQ+Ew9Ojk+UN7FubGtqqqrrK+3x2FANjEtLSwtLzM5QFHz
+0svU9V9VTUtLS09j48+/t6+tq6ytrrO5wt5XRDo1MjM1ODk5PUJNWVxUTUxLS0hHS1jw1ce9
+t7Ovr7CxtLm+y+RkT0pFQkBBQUFGS1BOS0U+PT0/QUNKTlhv1sS8trS1tri5u8DP9F9TTkpL
+TVZfb+fd1NbhaE9EPTs4Nzk7P0hb3se8urm5ubu9ws/4WE5KSk1SYPHZzcrIx8fL22tPRD48
+PDw8PkZPbtrLxMPDxMbL1u1jVE5MTlZeaubWz8rFwb/BxMvfa1ZNSURDQ0VKUFtu8OTc4fR1
+bmdaU05NUldi7NvWzsbBwcPHys/V2+X4b2ldYGRv6vptZ2JYUUtJSUdGSU1OT1dp+9/W0c3O
+09ja3NfQ0tTPzc7KxsrMz9rzaFtMRkNAPj89P0lMSlb+cvXV1tPO3e/g7//T023JsbnKvrCv
+v1Vw0Uo2Ly4yMSwtQFRMX9jLycPDxr/D1dLM6HzLw7+4s6+vr7XNRlVHKCcqJSMmKC1BP0m5
+r8e1oq64qrK/wHdLwdQ9vavDyaqvTErGPiIpKh8eISUtNDTqu8Cxp6eqpaetsbzK2vNi28O+
+vb2wyDLdyR4jSx0XLiocNlw05664raCmoqCvrKvfaL5FPbvKX7u06DnYyiEpVh0ZLykbM1Yu
+SbOys6SgoKSloLPEuNQ7Ut5J5cPPvvkusDkYwywTOzMXNNAmQbG2vqqcpqyeosi4tTo8Tjg5
+XV7bvrKsVFGhNh2sMBJHLxQvOh479ma0r6GeqZ6durOrWk7UPj5NR+PZvatFO54uG6csFXQr
+FjIzIjdTv8Kxm6avmqPWqLVA3l03QEZFZeC6tbCtS8CuIzHNGx41GxwtKCk+vLzDnp2wnp27
+rKlf4ds9RkM98XC/tL+pr3K+4DEvLh8gIB0fIyszPr+1r6GjqKCnsa242tVUTExJ7ufJsre8
+rLXG0n87Ky4kHiAfISAsOzbdrryuoaqspqqzu7W/T97RTtzFwcC8ucDDwW1jQCwzKh0qJR4t
+LS1Xac23u6+rsayts6+5xMLO287SwsLPvsDk0V9SWzk/PSwvLysuLzE5N0NiYMi8wLezuLay
+tLu3tr++zd7HePDY6/ZuaVdGSD02PzQvPz45Qj5aWz/f1GjNwMzNwsK8wLS0vbi928nbYP1Q
+SlJFPkxKUFFydFrbdkxNQDgxOks1SM1rY763w7art7+0u19nz0Q6YUg97ujSz8e90W7I7URf
+SzE0PTc1Mn5KQr2+67q4x8nMzd3gz93/yGJQ7cvR4sW84c/mWPXYX+xxSDg8UC8yQ0c/PGzL
+2Mq/xsPd1b9VeMRTWMrm3tTIu8q8tsrOz3XvY05cQTZBOy8wNEdMPVy/cWzFy8Xe2MTtWsHP
+3cfVvcDIuL7CyuvJ5kvwaVtFMU9EKkJnMC9LWD4+xsZHwbflzsTKysPMy8m+xMO7zdK+u9nT
+xNxeSEw/LTI9Lys+RDZES05PxcVyvr3k0L7Jvbi+xLS5/c2509fEysbP4MpWLy8vLC0sQUAz
+R2ZH/PhOy8rDwMa3u8S/uLm7wba/RM+4dvm92dDHPzM7NCMrMik0RT9I+tfNzbnG3Lu7zryw
+usC1sLbPVsG7dkh2yEpNukIvQi0nKSY3OTRORta73sC1vOPVu7TBxK+ts8fFvMtozszRTmLM
+SkU/MSwlJC88PW3mUU9nVePEvLy7xsm5tLK6rKzFQNi2SD7R2szTQVw7KykqKig4Rj++5Te9
+uF2/t8+/6NGwv7ymqrbK9r7ZSNDhVG1HNC87NycjLj1HSN7T29hOaLS6c7q/0MPBuK6qrMJN
+vcc6W7/Czt1CLyQcKTpFOS7fxjo8vrW+y/TS+eu4r66tt7Wu2D9fxM7OvdrOxS4lKSYsLjZP
+elY+Vb68R1G4y1vetq2zvK6vx9F83tZe0bbOZsZGJSoqJEBEMspGLtLL6r/N2vlD2bO6tayr
+q8M96LrLW82vttM3JSc/Oh8qRjhb0mO82FFnQXXG37uzzLirsLbD4c1sS8a3vskyLzw1OC0t
+QmhDPFTGw2FP3tFx6MiwwGC1qrfk08ffQ9mtsUIkKUE+Nzk1SdA7Sbe+3lw/dsTZybGzx8u7
+s8710GBE97+56yUoYTwlJD+sszVCv7nISuK/3Vfot6y7wLK8911HfbS7Ty4mLTg4LC1Rxbvi
+SNe8ws/i2MTDxbmttcK7wtrea/2/vzobHjQuTjsp+8dI2si9tMpYz7a4urWvsL7UtKzGYsK+
+1jIZFyg9Tkg3Oj0yT8G8tstHYb6zrKmpsb7Dv62sz82/vbsgDBParMwsKUk6KDuro8IsMs2w
+rqqlprtQ0a+jq9+3pLQfBw26obkyJUc0HjmintwfJsutraukpMM+1qigruO+pKApBAq3lqAm
+GUlVHiqplq4aHGynpa+qoLczNaybqW1JqZksAAenkJ4nGHP1Giqhla8ZGsmjpbGtnrYuOa6c
+otPZoqAPABOVkJ8fGF8qGcaVljkQIKyeoLSsoGMnR6OYplW/nEEAAqWOk00RMckXJJ2RqhYU
+ZJyaq8mprTcuvpudxbyd0QAAzo6OthMhyBscopCgHA8tm5SjwLO2TTzFoJ6usp1XAAC7j421
+DyDLGx+nk50bDSqckZ7Lt7dDPMCenKqpnhEADZyKjSYNLTkeX5+XuBEQSJWPplfB1DhCrJmf
+rqAwAAPLkYekDhcuJMCen6ofDyKokJS0TmdeUMGln6KceAAAN5eJlxUXKRpVm5ukJxAhr5eV
+p77RSUfAoZujoEIAADuVipsTFiwcSp2dpSYPIaiUla7xyGdLvKObnKIaAAe7jomsERgdHqua
+ob4ZEzakkpOuST881Kabm5/WBgAbnImNLBIZFzmbm6MuDxnKmo+Zx003M8CimZWqDAAMrYuK
+txobFR6nl5nXExMxo4+Uqb0xKFasmZGhDQAKyouJrR8cFR63nperGxIeto+Qo7Y9Kka6nI+m
+CQANvIuLtScdEx60nJq9HRkltpORm7QuKTq7l5AzAgMbnoyZTzsjGSbPm5jCHRYnppSUma81
+KDCyl5wQAAxYj4+8LCslKDe+oaK/Ixo0ppaRmrdKKjGqmcwHAhSrjJPlJx8dOL22rKy/MiUt
+tJiQlqVOLT6woCIGCRqpjpi7Lx0eM1/MuKmwbjgt7p+VlJ3EN0G4sh0NDhZYoZ6gtjYsLi03
+WLaqrcxBWLSfnKW/T1HEuD8gGxkfTa6otUszNUBZxLi+0U5B77uvrb1IODtXvre6y0Y1QMiu
+rc8vKDRYyNNDNDNAzbCvu+VCRV9q3b26xl8+SNDBzFQ2OU3nvMFUNTFNva+yyVNDVe/1V+/T
+42pf3s7VW0E8RFDpwMNqPTdYuq632ExF+8/yWldXb/l1083qXFBb3dXW0edNOzlF0Li830c+
+UNnhYFZW8crK0NpfTlT2zMXL1OhfTUZEX8rC0VRKXfbxbmBVUenLz2hLSVTmzcrJz+lnXF5m
+79nT4mtrenZVTlJPTmPdz95WTE50z8nK0d9mWWbu3+Dj8NzOz+1WTlhdX/vq6XdeUlFs1srQ
+fmNbWV1ia2ZbXdzIyexMSVVp6ODn7fb6fOnV0NHZ6nVeX3vva1BNYNjGxtxbS0hX5t9wXV52
++vnr3+xsd/bo3eDh5GRVaNnJyudRSU5bYmhmZGpqfNrV42tdbPXl3t/d3OxmZ+zV1+5lVFhn
+amhfXVpn59zb4nRkZW/q6Ht839Xa8Xbu3u5lZXzm5W5dYmdveX71cFtVW/XhdVdb+9/b3d/f
+3eJ8aXvf2exOa9BW8+the1tF58xw3etp7+ju1d7wflxNUk1pv25V2PBXWWTdz2ZfVUlq1evc
+4tfI32Dv2dvh7vrd4PZZYG9ZX3T7b2NQZPz2ffR5fV9WaGl+6eTv3ejj2Ovv6vtp5+x8dGFm
+YF9u5t3xZnbu8Ojl2tf4au3s9GxfaVlVau3y8m5w2t3i2u3+5fpu+m5+719ZX11cYl5cWlZc
+7t3b1NTOw725ucjMvr/mS0Q+Ni4sKywtLjhMY9u8tLCtrKuop6mrq66wrLgxGygpEBYgHyMk
+NKu64qGepqSknqC5rKe8vK+yyh4ZTxsOHyYfJibXrD65n6uqpaOdrbWgs8Wrr7G0KRs9Ig4d
+JB0kJj60Ucmiq6umpZ+ruqOu0q+vtq/aIyJCGhMlIyAmKvHJPrOmr6qmo6O2rKC5wKyvsLPt
+LCMzHBUgHx8nK07ZSLGosamnp6e0rKW7tau1sLPQMCE7JRQgJyQmKlnOO8OptK+sqai2taWv
+vK+vsrTBQCctMBkdLCMlLTLcTz20r7ivrqaosqims7Wyuru+5TolKzQeIDIpKi8vfVY2wLvH
+r6+tqLW0rr67wPjA7WrMQkh4RVlPOkY/Ojw5Ozs5R1dj29vQwMXEwsW/xdDOycvIw8bEx87V
+3HFPQjo6NjA2OTdGX2Z8X/LPdtrCysO8u7i7vrjAyLzE8XxYRz07Pjs5OD0+OTxMTVXta9TD
+3sy7zb2817e0v729vMHe8Gp4Rzg5NTQzLS89PztE1s3OxLyvuNa0sci+18S62srN185ORmpD
+MzEuNzktOltMTEzTtsrIsrW2wta4wFnS6MvFaNPA1lRLReNFMjs1NzwxQO5VU1XEtczDt7i2
+z2+4v0baeNfHS8fJ6V8/flo7ODY4ODc8XPlYTHu4xd+4t7S3a8i2UuTLT8TY28Pe10tNbD43
+NzMyNC89al5lXMKz3sixvLvG2brGYtLD6MPCy7ptZ0tRRS0xNzIyMj3sblpi4LrIVbq1wb3h
+wbdf3MTMxcHBvMvqRlBbMy4vNDMwM0blaGHezLjIc7e2xsPfyrxx88rLvLvJu79bT1I9My0t
+LS8yOFX+8tLfzLbK4bi8wL/mxb33zNO9tMO4vsrsSj84LisqLTAyOk/m2szY2L/G3se+vr/a
+zb/VzcG9srK7xMFqREEwLC4qKjI3P/fhzb/MzsPDxs7Nv8hyduPSzta/tLO4wMfOTEI3LC0u
+LS80PnLt0L/Czsa8ydDJxMbzU+fjetPBuLGyuMTJckQ4LS0sLDAzOlBp3MbFzs28vM/OxMnb
+WF3Z+ObIvbexs7vA1nFINS4uLS8xNj5MdtTOysfKxsbS2tbmYlZZ+9nPwrmyr7G3v9lmTzwy
+MTIzNDY9SVhw3tDU1svM5uvee25bWvbayb64sq+xtr/eaU06MjExMzU4PkdPf8/MztDLyuBn
+entwaVzy08q9trKvsLW7z3JPPTQxLy80Nzo9Rlrj1dbZzcbb/u3/4/Fk79TIvLaxra6yusx/
+TjwyMS8uMjQ5P0de3trX2d/O1F1dXGPzYXrQybyzrqursLjJ7l49MjEvLjE0OkFGWuXa1dbp
+0MxuXlpe/Vdc1cq+sq6rq6+4zVhOPy8tLjAyMzlIVP7Pz8/Q8OvL1GpOT+5rUeTGurGvq6iv
+u91CPjctLC0wPD1DY93GvcPK2UY8TVdLRUnPymHfvq+npqOkr79KKCYpJSMmMVf1zLm1srC7
+0Fc0Ky4+SUxM2LrGxLSpn56foq/2NR8bHyAiJzDqvLisrK2tue43JSErPEx737uwvr6xpp2c
+nqe6UCoZFx0fJSs5yrWtp6uvr8BVLx8eL1/Qxt22rry6tKacm56s9jghFhkdHikxWLm0qqar
+r73mQCcdITTaur/Hubu+u6+inJqermcsGxYcHyQsN86wrqyqrrTGTjAjHSNBwrC6xLu+v7qt
+oZyan7JKJhcVHB8oMD/Crq2qq7K6zUouHxwoXbStvci/vri0qqGbmqK6OR4UFh0hKy9Lt6yq
+qa23vdk/KR4dK+WwrLrIyb+3r6egm5ulujEaEhcfJzEwT7Spp6iyx8v5PiccHS7Bqaq609W/
+squln5ydpsQrFhEZIS43NFu0qKOnuuHoWjomHB40uKamuuTwyK+nop+doavaJRQQGCM6QTz3
+tqegpLppSUE2Ix0gPLCipbjnVc2vpZ+en6awTSASEhknRUlL2baln6W7Vjw5LyUfJEqwo6e7
+cE3RrqKenaCrvDkdExQbKkZT3cWwpqGovk43LighIC3jq6OtvmhmvauhnZ2irtosGBMWHS9C
+VMu6q6Ghq8xAOC0lISE2wqmksMta67Wmn5yeqLdDIRUSGCI1S37Dr6Sfoq9nOzIqIx8nTLSl
+qLfaXsetpJ+epK3IMxwSExkpPFDVu6qfnqW8PzUtJyMgLd2spKm53d64p6Cen6m2XScYEhQd
+LUBvxrKlnp6p1zsvJyMfJTjDqqetwdvKrqSfnqOsvj8fFRIWIDFI5b6toJyerHc5LSciHyc+
+uaqpr8PXvaqinp6nstsyHBQTGCIwQ+C5qp6cn65jOS0nICEqT7errLfHwrGmoJ6fp6/iLxsV
+FBkjLDZOv6udnJ+u2Ek1KiMhKEHFsLC8ysKvo5+eoqqxzjgeGBcaIyouOGK1oZ6fqrzWTzsu
+Ky08X9PO9WLRuq2oqa61u8XcTDQvMTc4My8wOU3ZzMnQzsnGyNDU0t1cSD06P1jNvbm6urm5
+vMDFwsDF1VI9Nzk5OjY1Nz1DSlBTf9TK0HdJTF3lzcnKz8i+u7q5trGusbvRWk1NST0yLi8y
+Nzk8Pklk3ORlT01e3c/V6m3y2MrEv7qxraywub/Fy9J0TkA5NjIwMjU6Pj9BQkhUc9ve92Ne
+buzb1M3BuLGxtbq8vLu9xtH6Y15aT0M9Ozg2NTQyNjxHVmVjXmHq2NTOycO+vb/Bw7+9vcLI
+zszLy87Y6n5pVEY7NjU3Ozw4Nzc8R1f42M/Evb3Ax8vNzczO2d3XzcS/vr6/wcfTd1dTUUtB
+OjQxMTQ3Oj1MetXIx8nJv8DH0+lpXWT31s3JxMC/vsHCw8PI1GNJQT8/PDg1Nzk8PUJKXdjE
+v8PLz8jP3uryavng29DQzcrHx8nP2ODe4OVtW1NWUEY/PT4+RkxPUF/ay8/Y2Nza4N/rZEnG
+w/PV3mhpYOO8ydPGyMfI7HFVTk9IPjk4O0ZNUWDr0cjFz87Yz8ruX11XWGFQSD3rwdBiya+3
+wLezt7tSOjs7My4nKTNH3cvHua6xtLzaX1NGOzkzPlJMZdTKvrq5srG1s7nAvtBIMSssLS0q
+LDvywbq4tq+vuMxuUUE4MS4yOkJT6su5sLCvrquqr7O7v8FKJx8kJigoKDnFsa6sraytv1s5
+MDItKSs5TvfFvbGtrrCxtq+qsbu8xe1EJR0jJSUrMEK2qqyqqrG1zzcwMi8tLjBHzMG8t7Gu
+r7q+ubOvusTA02w9JR8pKSYtMUy2r7Osqq+21DYxODAtMjRLvsbAsK+urrvKwL+8usbKy9tJ
+KyUsKyksL0S/tLWyr7G0xUc7Ozc0MzZF2sfIvbi3s7nDxb++uLvP8+lQMi0yLi00NTrsxsa6
+trq4uM9fYks9Pj47RF1r5sq/wMC+v8TCwcW/wOJVbVo+Oz02MzpBSFVq68zGzc/P2N/zaWBt
+6djX6+HY4V5QVF9lX23dyMC/wcC9v9JsVUpGRD46PUNHSEhOaPPt39fPyMXM1dje6mJNTVda
+Vl7608K+wMC+vcHVbFxRR0I9PD9CQURLWfXh3tDIx8XK2+fpa1FKS09VVFBi49LKw7+9vL7C
+ztzibktBQj89PT0/Slde7drUycjO09be62dWXGVaU1RWbdrUzMK9u7u+wcbS911JPz89OTk8
+PkdPWfjYz8zM09LO0/BkWldYVlNXet3Uy8G9ubi6vcHK31hIPzs5ODc5P0VNW2ru1s3Ozs3N
+ztduW1tQTk9PYdrQy8K/vLi5ur3CzG5JRUE7OTc1NztASVFd7NHLyMfKy9Dmb2NYVVdZXnXe
+ycC/vLm6vLy9xNhVSkc9ODUyMjU6QkhPaOnXycLAv8PM2/ddV1pgZWp839LJw7+9vby8vsbU
+91JLRDw2MjI0NztFT2Hp1MrEwcHFy9Xrb2JgY2xqa2ry0srHxL66ury9wMx5TUY+ODUyMTI1
+PkpRb9bMysjIyMnMz+JtZmJmaGBpbubMxcC9u7m5urzF1GVEPjw4MzIyMzQ6R1Bs18/LxsDC
+yMjK1u1vY11qfmNg5dDKw7y4uLi6vMLO/kc7Ozk1MjAwMDU/R0z5z8rFv77Ew8DK2upnVFNc
+Y1/11s7EvLm2tba5ur7LXD07OjUwLi4tLzc+QVXazMbCvsHCwcrV3P5ZWGFha+LPy8S7uLe0
+tLi8v8f8QTo7OjIuLiwsMDk9SO7Lxb+8v8TCxsvP3GxVWFpm3dHOx7+7trO0t7q8xe1EOTo4
+Mi8uLCsuNz1H8s3Iv7y+w8TDxcrdZFlbXnjdzsjCu7aysLG2u7/RTDg2ODYwLiwqKzA3PlbZ
+ysC8vMHEv8DK1v9kXGPf0s7Hwby4tbKzubzA1kc3OTo3MzAsKSoxOURu08jAvsDHycPDxs9v
+WF1y3NHNv7iysK6srbG30jUkICEkKTA4OD9vx7qwrq+5y1E1LSw3SvfKxL22sq+vrq2tqqms
+tNE/JRURFR03u6inr7i5t7W9fjYjICUvS72tra2vuL/Gzsm9sqyqqKuywEwiEQ4SHlKnn6a3
+z8WxrLpEJhwdKD7Er6ytrqustL3KxbWrqKquueBIJxENER1Qpp6ov9zFsKeyQCUcHixMyr62
+r6uprLvc2LipoqKor8D6PR0ODRMkwaGgr+54vKmoxS4fHitI0MrXxLCoqLPaV8uqnpygq8Fb
+SSkRDBAdTqafrlJNvqqjsjAdGypdv7nQ07aopK3SQku1oJ2eqLtqSCsSDBEh1qSfsD49vqej
+ti4bGytZz9d9za2fn69jPlawnpyjr77LZiIOCxQurZ2hzy4+rp6j5SAXHTnZ3WFTxqecn7xK
+TsGnnqCtwcnLMxMMESW9oKG8LzK7oqPALB0eMEtEQE2/qJ+iuezCrKGgqbre0r45EgsULbGg
+ossrMbWjqmcpIio9QDMvRrSjoa3XzKqcmp+t3jxEOhoNECa8o6CsSzF8sLk+KSw/ZU84LjnA
+qae19OSunJaapLdcQCoTDBEpvaqorL/MsKv0IBonT+1USVDPsamyTzrVp5uZnKOyvPMeDQwa
+Rbq0tLm7qp+oNhobLT4yLj+8qKOr2Dc+t6CdoKWrr7gyEw4aP8VoV8a0qZ+jXh8gPEMmHy/M
+rKSnvkBVrqGkqauuvFMjFBUpvLVfQdiupqe6MCApVV4tKEG7r7W+0lzRq5+fpaqyaiUUEh4/
+xs3WuamhpLU/Jyk2MyUmXqqjrcpuY9i9ta6nn6G2LhQNFzzBTDLRoJicrEkmJ0n6Kx0ovKCh
+r8lo7bisr7mvoqRqGg0PIU9YMjyumZaeySojNNxEJyrDoqW41sjDv7awr66npLkmDw0cODQh
+J7KZl565OSs87j8tOrKgqMlQ1Le6vr+4rKakqkwWCxIsTCoaLKWWmrk9UcG8Sy48v6emuH5u
+vrnVW82ro6myxysTEh4uMyQmX6+su8q4tbvMWVjuvayuxfbMvcjdxLWtqamuyy8XEB00MR4c
+RKeq7UG7qK/IZ+K4rq2+RlC4rsdF56ymr8R2RTUqHx4pMC4qLmS9x9vLtK++zsa3rbLLV1jf
+yc9p4r+5t8FbODVKWUI3LzVNWj4uM+23u29Yx7Ozzz9qtbh2WF1NzLfJaElAx75AM/K1w1JD
+SU9LT1FDRem7zUbnv/5H7VtGxMRLYMPpSeZZPcXBPdutwT9usb06Ms26Uzk7xbw/Pk9XVk/u
+WtDKxVhPv2s7R/ba09LP67W/P1S5vV8+NL6xPzRLfMdJP1p2uMs3TMfoRDvg0OdPT8Wzzzjs
+t97n2tbazdJcQ/69OkjyUtdJPsD9RXpN8kPe20rY3c3J/VTf1MO5Sk+0XEm/TUfcfd88aN1a
+eHJETsBBP+5c69E+17fFaVjYxORs1tjNzHdPvkh52D9R0OpPQmDUXDpNwlZE/3PS2G7TY7xx
+PPHPzPre2Ly/PcTXVt8+789LWEJN2klM73d90FVOwM/MQFq6X1pO0udbwOZfytbHZGDAXF74
+Tcw+Q09lZz/M+kTL89lpTr9vS9jPTkzN4XPK6WDPvehd491w+19jUm3VSGF1WnpKyeI/w8xB
+ys9CVMnhR+3LSODHddJj9XvO60Lt5VDM+0DOaVPbW/3QRN3EaO1L+8tjXOTaWXvJ6091zFLf
+a2tWP7u8QUG1X1jOSOph3M1XeGlRV/vMVT1sv3NJXM9o3MxU3Flhydxa39Hk//TdalfTW9zb
+TWRpeWnVSFTha2lg7EzX8XXxR83gd8DcYd/J2tpTbMjRWWRpdfBDalVO2VJLZ+JmSt7OR1zc
+V8vPa+/C69i+X0XUuF5Wannv5Fhf+ERrzUlg/Evv90rR/D7R1VbZXdzCUvLF6f3f7c/oRNTN
+Vm5iUXnu3edGWOJtY2NmSdTVTMxqUMnUy89HXfPfwE1V2vvo2tBbTGvt1+9OUVlibF1s0llN
+2sfsXGvY11x22nZVfs3abGPi7mPcde73TGnN5WlbTujvc9llXGR42tbvYl5e595kZWrt0NVs
+Yul+3dR/dFZm7n14an5fc/D2bVlrWlvt7vLtbe/a8PHsbuxset9+f+b2annu2utp8e7b8mrn
+7u7mfW1kUGt9XnJ9eW5dfedkZnDrcG3u5t3z/XV36/n62fVk4uv8dm7l39vd1/pz6XN1/Hdt
+YldYUE5dXl1kZ299fu7hfOro/e3v5dra1MvMycjMzszZ28vV2vFfSjMsKysvOUrn0t/Lvrq2
+uL3Dy8nFy9Tt7tjSzMXFyMO+v/UzJyMhJCw5TvLZw7m2s7O6xcvO1tHU52RNX9fIvru7u769
+ub71MyMfHh4mNUrWvrewrq6vtMV7XlJPau1qWFV4y7qwrq+1ur7D2kYtIB0dHiY1Usa1sa2r
+rKyvvfg+MjI5Q1j85NLEt6ypq663xc3WfkUtIB0dHiU3Zr+zrqurrKyvve4/LyorM0btwbez
+sa6rq66zusPQ+UcxJR0dHiErQOq/ta+rqqussL9dOy8rKzBC3byxr66urq2usba8yf9EMCYe
+HR4hKDJG3ruwqqeoq6+5zUo0KygqMkrHs62rq6ysrK6wucXnRzkuJx8dHR4kLkvFsqqmp6ms
+r7fIVDgtKSw0SNG5sK2tra2tr7O7yelWQTYrIR0cHyUuRc66s66sq6usrrfNSjYvLzZCZtDE
+vbiyrqytsLe/ydZtRjMnHhwdICk1TM+/ubKtqqmqrK+73kk4MC8xOkzqxbq0r66vs7m+xNBl
+PzEoIh8hJiw2RP3LvbOtqaamp6y30kUzLS0wNz9Mdc2+t7Gvsbi/yM7fWkQ1KyYkJis0QV3Y
+w7myraqpqq22ylU7MS4wNz5LcNTJvrm3uLq8wcjS6VVBNi4rKiwyOkNQ+8q9t7Gvr7K6xuZW
+SERFSU9VWlxu3szDw8TFxcfHydd+UUE6NDEzNjo/RlJv59PKw727uru+xc7pXlJKSEVFSlnp
+zsbCv7+/v8LGytNrRDgyLy80OT5FTFZx2ci9uLa2ub/O7llMRkE/QUlZ5cm/vby8vL2+wsTJ
+4FI9NC8uLzQ5Oz0/Q1Dgxru1srS5v8znXlZTUFBOTld+1cjBvry6uru7v8rtSTcuLCwuMTU3
+Oj1Ha8u7s7Cvs7nAy99rXlNMRUBBTHHMv7u6ubi6vb/H3VY9MSwqKy40ODo8QlTZvbGura6w
+uMTS9VtMQTw6OkFX07+4tra4uLvAwM5bPjErKSotMDU6PkZezbuxrausr7fAz3RMPzs5Nzc+
+Utu/t7S1tbW2u8LI5Ec0KiYnKi80NjpAW8u5r6upq660vM5pSTw0MDE2PlPVvraysLCxs7a6
+xPdGMygjIiUqLzM4QWjDsquop6eqr7vPVz40LiwtMj1b0L22r6urrbC2vMPUVDcqIx8gJCov
+Nz9nwa+npKOlqrC93Uw5LiknKjFCfs7At66ppqersbrDz2k/LSEdHB8pMTpDY8OupaGhpKmx
+wmlANCwmIiUtQ9S/ubOspqOlqrC7xNBgPy4iHBodJjI+R1XGrKKen6Oqs8tLNy4pJCAkM+K2
+sLa2raajqK+6w83Xb0YxIhsZHSk6REVbvqmfnZ+nsMNiQDcuJR8fK1S2rbC4tKulpau1x9rU
+ztxJKx0YGyMyPTw++7KinJyhrsteTUEzJx4eJ0K2qaqwuLGppaq64l3dvrzcMx8YGB8xQjs1
+SLagm5qfrvA/QEY1Jh0dKuiqoqevubSrp6zAT0L9vbnVMB0WGCQ5TD86WrKfmZmivzkxOT0v
+Ix4gNrOenaW2v7OpqbHfOzhZvrlcJRgWHS9PWkxYxaqcmJ2yPSstMzApISAtwp+anqu6u7Ct
+sMZALzj5v9otGxUaKlbP1tK9qp2Ym61DKSUoKykhHyy+nZebpbO6ta6vyz0vNk72TSoZFBor
+bcO/vLGkmpedtjgmISImJiIiM7acl5mgrrq1rbDQPjQ4PkY3HxUUHS5zurCuqp6XmKHELyAd
+HyIhHyj7ppuYm6SvsKqpteM/Nzk+OiIVEhgjOcewramfmJidrUklHh8iHx4lQ66dmp2mr7Cr
+qKy8Xj48PjklFxIVHjPJsKynnpmYnaxPKB8gISIiJDe2n5uep7K5raaptdZJQD80IBYTFh0y
+w7Cqo52am56vOyQfHyAjKDDtq5+eoau3ua6qrbjWSj06KxwWFxkhPcKupp6bm52n2i4iHRwf
+Ji93raCen6e1vLWvr7bGZEI5LB4aGhsgMnW4qJ+dnJ2mwzonHhwdISparKGdnaOts7KztrvT
+S0E4KR4bGxwkOWG/q6OenJykvEMqHx0dHidNsqGbnKKqrrCwsL1bOTItJB0bGx0oP9Cxp6Kf
+nZ6nuUsoHhwcHy3drqCcnJ+kqq6zutVFMywkHRscHSMyTcatpJ+enaKuyDskHRoaITu+pp2b
+nZ+jp6qvxE8zKiQeGxsdIS1B2LaqpaGfoKew1DAfGhcbKFGxop2cm5ydn6a08ToqIRsXGBwf
+K0Pfu6ukoJ+hqbLARyoeGBcdLs6qn52dnJqZm6O6QSkeGRUUFxslPs+wpqKgn5+nr71KLCAb
+Gh4pSbemn52cm5qanafEMh4WERAUGCAwVLajnJubnqm0wk4wJRwaHShFuamloZ6cmZiaobc+
+IhYQDxAVHitIt6ScmJebpLPpOS0iHBsdJT66qKCenp2amJmer0IfFA8PERYdKD+4opqVlZqi
+tk0vJR4bGh0nQrilnZybm5uam6K4MhsRDw8UGyEtWbCelpOUmqfIOikfHBsbHyxcr5+bmpqa
+m5yhsD0eFA8PFBogLUa+pZqVlJifs1ovJB4cGx4nPrymnZqZmpueqL80HxgTExYaIzRzsaKc
+mZmco7HtMyYgHh4iLES+qJ+cm5yfqL4/KB4aGRkcISxIvKifnJyfp6/CWjgqIh8hKj/Jr6ei
+n5+hqsQ9KiEeHx8hJi5PuKihn6GnrrzXTjkvKykqMUTRs6mmpKevykkzKiYlIyQpL0fGr6ag
+oKWsuNJOPjYwLi82RuK8rqmnqK7JPy0lISIkJy03ScqxqaSho6ivv2Q9MS0sLjVH1LmtqKem
+qrpQMCUeHR4fJS5Bxqyjn56fpauyyU81KSUmKzlzv7CppqSlrck9Kh8dHR0eIys+u6aem5yf
+pau2zE4zKSQmKzzjuKyopaSot1o0JR8fHh4fJC1QtqaenJ2hpau5z0YvKSgqNEfnu62oo6Wv
+2DssJSMhHh4iKDzBrKShoaOkp66+ZjctLS84RV7Lt62npKzMPS0nJiYiHyEnNs2vqqakpqam
+rLxvOi8vNDlCUt25q6alrMRCMy0oJiEeHyc37LevrKikoqOqvGxBOjk2MzQ/6reqpqeruWU9
+MyojHx0eJzNTv7SuqaOhpKq31U49ODIvMTxmwa+pqKmtvkw4LiciHx0gKjratq6ppaKjp6/B
+bEM5MS4uNUzMt62qqqqx0kEzKyUhHh4kL0vDtK2noqGkq7jOXD41LSwvPfrAs6ypqay5WTov
+KSQfHR8qNmDBtqujn6Gnr7zLZj0wLCwzRt6/sqqoqa7HSToxKSIfHiYvPF/JuKqjo6issrrJ
+VjozMDI5SPHBsayrrrXIVj8yKSMhJCsyOljIt6ynqKuss7vHVj47ODc8SujEt7GxtrrIVj82
+LSknJyw2PFHVwLStrrCwtbzD4lFNSERHTvzIvb27vsHFb0E9NzAvLi43PUNh28q7t7u4uL2+
+xNrua05PXmDcz9XGxL/HzWxTUEU8Nzc0PD09Q0xk0srOxL29u73HzdHjf1JVZGz36tvIw8vL
+18/k309GPTw5Ojo4P0pUXufSwLy+wMbExtd/U1dZXVrkdsPDvri/v8jR9WVGPDczNDQ1OT9L
+YunPwby8w8zI6thPWlhZaejRxb+9u7y5w7/tYUdIOjg1NTo7PT9LVerV1dnLz8rL+Odn2u/V
+18+9w7zKv8rD1tvtXllESEBDQ0FAQ0NERklLVWZjb+nPy8rJysDGv8bCysve29543/Ltaext
+22FmV1NKQz06PTw+RElPZOLbzsK8u7u/wcjc6+1y7Ofl5Nnn1NXa9edm+FZHQD9APz88QEhc
+Xmfq3MjIz8/Nzc/O4dXTy9TQ2dPO09r2aFlfSlVNV2ZvXFpgZfxdTk1TUlJPTlB5393Xysa/
+xc7SzNn5WmBeZVhZXVzf38/gy97J73FJUktMQ0FDSl5e/G7RzMbR39XW1O1sa2V47/5y2czG
+1M/i2fbzWlpmbmRMSVBaYVZWT3z8dlxeaufv/Gty293b9tjWztbo6eXX3+9me/dp5v3l2c/n
+cVJtW1tIRkhTWldYXObX2N7n1s7T7GZ9fG9mVHrs2t/k4dnP2M7Xy87NZ1tRV0xHPT5JWGBW
+XO3Uz9d3bG/X6O5Z5+3nX+jmzc3Tx9jEysDKycnJ9EdAPkM4NzM8Sl9OXHTSw8bUeG/i3d9j
+ftrP237U2rrHv86+v8DKyMXUVj83OTYwLS01RGvr28/CvL3I3FZfV2BWY/zb183Jxb26uLy4
+urq+vsr7PS8sKyooKS05WNPGvbi1s7zT7ks/P0RO7tfMv728ubu6tbm/vb2/w9dHMSoqKyom
+KC9I0r+6t7CwtMTvSz44OjxO6c7Dvbu4tLe1tri5t7q7xV40JyQmKCcmLUHLuLKzsrG0vGNH
+OjkvLjh9wry+vLKvsbm8vLa4uby+zkwuIyImKCcpMFS9srG0trW2xlE8ODc1OT/evbi6urmz
+srm/wLy5t7u+zU0vJSMlKCcqL0zDtbG2uLe3v/VEOTk6PUVezL26uri3tLe9w8G+vr7DyvFB
+LScnKSwsLjhev7e1u726usFtQzs+QEJFVNjAu7u6urq8ws7Sz87R1c3X9UM0MC8yMjM3QvvK
+w8nIwLu7yHBLSUpLS01f383Gx8TFytPd1c7P2tPLxb/G3Eg8PT49Nzc6QU1acOXYzcnHzNvu
+dGVdX2NoX2v6+vzt3tPLxL/Aw7+8u73D205ERUU8NC4vMzxKWmnlyr+9xNDs6u9rUkpKUFdg
+d+HOyMK/vLu5vLu4trrKbUI4NTMvLCwwO0ho0cvFwL/Byt9bTElJTE1KSlvPw8HCv7y6u7y8
+vr23uMDfRzQsLC0uLzM6UM68ur3FzM3O3k89OT9OY3ZgYN7Lwr/CwL28u7u6vMC9usJnOisl
+JiswNjxM1LqvsLzPdlxVSDw1OE3PwcTN39vIw8bO1cm+uru9vbq2t8VHLCEfJC05QlfQt62t
+ts5XTEtIPTY4Scy3t8bmbuXU2d/r28S6ury9vbi2vG4xJB8iKz9Y+Ni/s6+zw2lJSUdFPjtE
+5r61u+9GQ0965unhz8C6ury6trO1vlUvJB8lM1nb09LGuLO4ylhBP0A+OTlF27u1v2A/P1DZ
+ycfNz8S7tba4tre5wlQzKCAjLlnPy+Tlwrm2xlo6Nz1HRkVcyrexvGM+OkrYys7b3c29tbO2
+ubvAy3o+LychJz7XydR4z7m0vO06MDhJVlli2b20t9BANjxmx8PM29O/tbC0vMLGx9JROi4l
+Ii1Wy8Xdbc+8uslOMzI+ZOXg7dO9uL9eOzhI18XK3+rNvLS0ur7Aw8XOVTosISU8zb7KUE7V
+vr3QQDM6TufyZ2fLurrQRDY7XsrAyNjRw7mztbq+wsPF4kYwIx8rXMPEW0FSyb2/Zzg1RP/j
+4fbavbS8eDw1RNfDw9L/1ryysbS5vr6/0kwwIR0oYLq6XDxKyri7cTQvPF1yZlfpu6+21jwy
+PVHOvcre07+xq6yyur/Izlw0IRwjUbS28zU5eru51zUtOGvb8V1tv7Gyzj0wOtu4uMtcbryt
+qq23vsTH3z8sHRon4bKzWTVJyLS2WS8vRdXVbE//tay0/y0qPcu3vHxQ1LOqq7C4vr/A3UAq
+Gxsuw666QDROwbO9Qy0yT9nnVEnbsauyTikrWLavwFFUxrGtsru/v7/F3EAkGR08tK3JODrZ
+s7DLNy074M9hPj/IrKrFNikx1rS4005fva+usbm8vL3JeTQdGCBWrrDiOkrCsbhVLzFQ1elG
+Ply1qq3lLykv6La2xG3/vK+trrO5vsvk8zweFx5NrrFdMkO8rbRNLC9dyO09O/GxqrlGLzBS
+w77BzdbIv7eurK++yttoWysZGizBrMQ+QMiurtsyLEPQ9T84S7ers+A8NztP28a4t8bQybOq
+q7jO3OneSyMXGzazr2c6YbGqu0AvO9LPQzQ8y66wxU4/Pzw+Xr2utOVOy6ulrMpvyL3cOh8X
+H025xkRAvaiueTc3Td5MNDt6vLbH29vnRDQ71K+uzU/hsqmuwMvAub5NLyQdIjnsbk/quKy2
+Zz0/SFRGPEnQyMvGwMJ6Ni9HwbbC79m4rbC8v7q2u8xMOjMkHiY+1XBlx66tzDo5SH1hQD9Y
+z8HFw8hhQTtF18fe+M63rq+5vrazvvNKQkEwIiAsTfxM1bGruUw4SfNWOzlMzb7Iysn0S0xi
+5uxRTM24t7q7ubCwv+ZfV1VFMyklKjxWT8izscFbR27xPjM5Y7671XJu3MzNak5PS1rNvrm1
+s7OussdaSufjRjInIiw9PkTHtrO/dfnUSC4xT8a+1lnhxcLO3GdNSUdazby2r66trbvpXfnW
++DwvJyMqNjU4zbKvuMnY2D8sNfLOaWTcw7W+2s/oSkdGS868ubGsrbTG3dbOc0xUOCghJS4u
+LlyurLe/vslHLTJPTzdptbe7vb283DxG5Vtov7Svsri3tcpSUHxdac85JCkpIik3cbSzxLSz
+bDs3NFXLP0yvseDJvcDNQT3KxFPetK+1uru4ykRKVzZxrNFZvC0WKjseKOa9rb43vaw9LtO+
+wchOy7JaP7m2UlTQw7/W4re06G2+v9PTybu+SD3DzjEYG9o3Gy+stts/3qp7K86saUDAscJU
+W7ixTEC5tevvvbjBY8mww1HJt+Q4N1r9OzdBIiBaNCM9vtrMTFeuzTfPrsHSxL29+0/BsM7+
+u7jT89HBvuLfv8XdbkxHVDgzTl1DKidBPicvy8rrTc2zyUbXuszdzrzLUnu7utTKtrnqatPH
+yNfCvNH/11VDTT85SlpbPC0wNDEvPVzLXX7By2tTzMfE2sW/x+ryv7/EvbjAyNHUw8va09pp
+12I/UutRRmhbMy0vKis7QUVbcc7MeNC/ydbGvcTe7cnDw8W9ucPXxsbY1tbQyuBd09xJPnXi
+RkNFOi4qLTI4QUlpxs1iz8nQzsa9u8PPysXN18rBxMjKzMfS/dTE5Utt50xP9Xb1+Us4Lysu
+Liw749xwys/N0vfXusbZvr7J2tLKwdjjw7/P3cfD0W7qyuBMWNt9VWdrXj8sKzAsKzdP+uzf
+xMjU2srExMu/usnWy8bQ3NfN09jIy8/Oz874V+XuSVzuceTbTzYsLCwrLzznzXbSusxmz7/A
+xs7JvdZezr7L2tbOzubny8PK1d7My1hY0NROTNjwMSMsOiwpOc+/Yku/teJSzLi96ti4u/71
+vrzaZdjEz1p6wMHW28/ObUZQ5HZc0/QvJy8vKio37b/8Xriz3lbJuLrGzLy78lvMvtV05MjG
+9lzLv8zl1L/UREjg3G/pOSIuPSMhOGXLyli5q8pMyrzEz+PEuc5bxbjLdPbQyuJX38PF0NPI
+xORj2+xb+zkeKUAlIDlsyLj5v6e9PtzI7c7v/7a3b8e2y9zXX+nKYFnRz8/Gy8rH+XLc+Ghe
+Jh02NB4lQ9O0vOSspMU/8cjO2VHlsrphz8HP1W5M08hTVdjMxsrh0czvaXTvz1UlITYxJSo6
+6bS2ybKqt+VXWc3IUVLGv83fbNTB6U3jydXq5tjK1GBc/Pxda9LFxOA5LC81Li0xP9a8yti+
+trvCydTGv99cdGty4lRI7M/Uz9fayMx7XltgdFxX787R8mb7dEY7Pk5cTkdDS2Dx/HDcyMPE
+yc/U3PP1ZlVVXfDe39XJzNxzX23o4ejc1eFqYlpPVGN79H1jY21dTUhFS2vi9Hfy69/f4+Xv
+9G5t6N/o7H/y3N/6/dvNys/S1eF1XlVUXWNk7+De4XJeZnVubWBbV09LTFFPTFRr+Ozr3s/N
+2urp+vbf3d7c19PX63J8e2tt8Obg39ze3tva2uLwdmtoWk5KTUxKS0tOVVZWZevp69/u8+bk
+4NfU1tXW2Nrj8evg4+/w6NnX3+7s4OHq9nd4alxbXlxYVlFSWlVQUVNVUVVo6+n58+zq5OHc
+19XS1dfY2tjU1t7j5OLk6u18YF1iZGlkX2Bmam50YlpecXNiXl5fXVhUV2JsZ2dw8eXh39rb
+3dXZ3trV1dfV2Nbb7Pt5aG7x8XFjbX1kX1tXWmBubWJeXF1dWVlZWl5n//Lw7evf3+Dh6enf
+2NbV2Nvc3uDi5uzv/25ycl9gYF5eX2JobWtxff19bmJdZW5oZHZ+fvb9/f/9+v717evn6Pfx
+7+vn4t/e5eri5/BtanBoX2VoYmZscn306ejg6u/sfGllZWdmZF9mcHl79ezv6+/7a234+378
+/v3y9XJ0fHr67PJ9+PDw8u7o5uvq4url5Ont/nh2fnhra19bXV5cWmJgXWhvcXv0+//78+nl
+5Ofl3t/g4+zv6unu/ndueff07e71dm1ua2xscnx0dGtrY19ta2x0e/f3/XVvb3Tv7Ovu8e/u
+6e/q4N3f7/v7fPF1aG/7fW5tZmd+8HttbXl+8X1taG1sV1d65P9n7tzW6Wnk1uD2cXZsc2Bc
+bmz65uLu69zq7+vm8+zsd/zvdnPxdmv783Jtb2pjYlxQTFBWUE9TXH7o597QzMvLzc3P2N3e
+3dPP1dnPycnN093zVTsvLi4uLjA6V8u/ubGtrrG6yd5YQDYzNz5HTvzIvbi4uru8ws/f6t/i
+9+jOz2tRRDIsLS0sLzdIzb26sayutL3WXEo7NDY5Qlj21MG4tba6vL3B0Obe3eLd0M3Q5VdB
+MisqKystN0zPvLexra20wutYSD43NjtFW/TVwrq3ub2/v8TM1Nba2NTS0dPdZEYzKyosLS0y
+RNG8uLOvrrG81mJVQzg1OUFPXHXUv7m5u76+v8bQ09bZ1trW0dbuVT8xLCwtLS43TtXCu7aw
+r7W/0ehdSDw5PEJITl/bxLu5urq6ur/L1eZyam5w9e71Z08/ODc4NzU4Pk5t5dPMxcLBwsXI
+0eRlUkxKR0pX/drMycbDwcLHys/P0Nfg3t3o+2ddTUM+PT49PT5ARlBhe9zPycTFytDa6m9a
+UE1TXWrk1NDMycnJx8jJyMvMztbe4XtbVVFLRkRFREE/Q0ZLT1JcYvnb1tXa3eDb3/P37uDg
+3Nre3NjPzcvIxcbKzdHW3Oj5YVpUT0xHRUJBPj5AR0tPXW7i0M7P09TPz9bj9vh0bG5scuvY
+09LRzsnIy9HU1trb3+Di7m5aTUVDQD09PkFFS1Ff7tnT0M/OztHZ8V5ZXmZpYmvn2M/Kx8jF
+wsPGy8vMz9fe63ZhVUxCPjw6OTk9Q0ZMWPfWzMnLzM7Y3eV8ZmNtamJdf9jOysjFwcDCxcvR
+09HU2uhpVU5MSUQ/PDs6PD9DS1d039LMzM3O0NPc9WpmbmddZO/XzsvIyMbAvsHIzNHY5fNz
+bm9eTUZGQz88PDw/RkpNU2nq2tLOzdDR0t71cW5ka+/j3trPyMXEw8LGys7S1t/6cGtfXWBd
+TUVIS0Q+PkFGSk5QVV332tnb1dLV2eh8a2z/6NzX1NLNyMbFxMfJyc3Z4fRvb2hcWF5maVZH
+SFBNQT4/Q0hJSUpRZe7j39PNzM3Q2dzX19XRz83Mzc7P0c7P1dnb4+5xYV5cW1pbXWRpWUxM
+V1lOSUdJSktLS05c+OTd1s/MzMzMztDOz9PT1dTV2/BtzMjwfNHZ6/xiZ2RWTlFYWVRQUE9a
+XVdTU1lbXl5cX3b78uTa1tzf3djS0trf3Nnb5uHb49/e7O/q7/P4dXJo/fZpbGxhXVtWVltd
+ZGBZWmNqXfvR5n7s3ezx+HpsZPfu5+bl9d7b6dzl4OPq6eT+XHTwaWBv7u58d29sc3VubXVq
+ZF5fffD15/L/8+x+fnZofP7t/ffvemh05O31+Gv67m/u5ntrbnf18Ozc+Wt+anzx+ObqZnrX
+XV/laX5yZV1vYXXhZ+7y2dDb9N3eamplb1Ra9n90fuzg091mZmlWTlHfaExpytPs9ODc6Gxa
+7W9hZWb82+3+3d7Z39/u3+r4X1hiemxhY2rr42lq1ttq4uR5cmhqbGFfYmZdcelqZG717HZp
+aevu8vPk3+763tLf3nV33vJ3d+v16nx0X2Z+aWhpeG9ZYO33a2r3b/5jcOX87H3o5u3gffju
+/+r2aGd5dnH533ns+2Dsb2vq3Xhq7PT96n1y4eftdWv8d3xacGn5bmLqcut89u11+3x3YWDr
++2vx497z7+Xj7nXx5eT6bffmbF5nd2Fhbu77amj03vvr+3rp5vJmb/Do7P305n52X2Z6Y2p7
+7nbw/Gf47+5safr08/945uT48+jk8+7i/PpxeGlp/V7+e2V7dmRh6u548t3ueHpvdOjpb93d
+cG917+7ra2D23/lmZuvodV5deHZcX2nvfXB17u7u3t/t3tv15t/o5G9vZPNtXlpq7W/7+25s
+5d5j6+DvZmR88OF8bm/xf3RiaXdtf/byc2rz9HD5433w8mp45Ovw633q6Xbt9Phzbf7g9vDj
+b2v263tub+1uY2JmZmbg6XVteWVp82xtdvry9Xpk8t/jeuzs7uf46998aeTj92lf4tZtbfHk
+929gdvhva2ltYXplZPj2+Wp9a2xg+996/Hrl4O57697m6O7o+2zs3N5yaON6a2L763xybXlq
+bF1lXGNt6f9cXl50fm1nau/Z3ezn09zb3ejo9OTc5u995fV0d2JnYGVlX2h0ZGXxZnbodP73
+alpqd3V5dujteG/g6ezx79vm+n7u+ndwaPb+e+jy+3Vq/3ltbHZrcOby9/N4cn3e6fDl3fFv
+dG18YmBm+WBfaXt99/Fw++Do7+v1/W966Ozo7XP54vv8+/Z27PZ7/WjtaV9q5Hxu5+re9Wxo
+cvlxbH15bWRvbvPsafjm4vh2afvxd3Tn3uja9f336+1s/vLrZ21kXGNubXfyfW985e723ul0
+Z/7/++Hh6e3o8WFgdXn2/WVffnBk3fX8fuP+dvF77X5mb3Noc/72b/ru5+j4eH507PLocH70
++et26+7k82n49v1fZPpzaV9fa3R+8Hh4fWfr5u923d7u+1/ZXmjU6ORueGH4SFbL7W3m3evd
+fWnt6G1x6lBQ6934atzczdhq9W5fY3tOTlvl22585Obf1Wfo6Vt4cldPb/Py2fJg2dT9ZO/+
+5eNdZubwYfT6U2vsZ+HP6d3N2nxsZldpY1BWa2xy8mdm7ntiXWhvX2Jf5M/W1svJxMrWzMfE
+zszKzd88Kio3OTQzMTzcubW1trm5tbzyRjk4P0hQYtrKv73Av73AxMHGzsnIzF8zJyo2Ny8t
+MDvft7C1uLe3tbrXPzIzPUdJWd7Jv729wsLCxcjJycjHx8dXLikzPzguLC481raws7y8tbO8
+9DwxM0FUU1Fmz728vcHP1c3HxMjKxb/C6jcqLjw/MSosNlTEsrG5vrevschHNjU9SVNVUWPO
+vbe8zM/GwMHL19THv8tNMCswPzwtKC0/4L+2srS5tK603UA6PkI+PERa38i9uLvEw76+x9HP
+yMPJ3z4rKDM+NSooMlDMua+tsbeyrbTfOi8zNzlAU1plyrWvtsbMvrq/zdDJyNNVNSorNDov
+KSs5872xrKyvtLGvvE0vKy0xOUhi/dO5ra24xsa8uLzFyc7nTDQrKy8xLCgrOW+9rqqprK+x
+tMo9KygoLDhczMfDs6mps8TEvLzDxsLMXToqJCcuMComLFO6railpquwtsRGKR8fJzh7x763
+r6mnrLvX18G7vsnU4E0uIyIoLCkmLE25qqOfoamzvt06Jh0dJTjNtLCwq6ensMlsYt3LxcLG
+x9g6Ix0gKisnKUO0pJ+eoKq83E0zJR0dKV6xrKyrqKeuwmhMSVbVxL67trZPIBgcKy8lI0Oq
+nZucoK7dSj0pHBkhQrqtqaWjpq/GZFBNQz5Rv6yqrbZGHBMaLi4dH8KZlpyforZGMyocFh09
+urStop2grs5MR1ZPODThqqOprrM9Fg4aNCkaJ6SSlp2fqe0wKx8XGjPHy7yhmqCwvcxNQ04+
+LTmyoKSqp6w6FQ4WJScgMqaTkpqlu0ovJBsZJEfQwq2fnaWwyk0/SEIvLE6upKSho7M5HBIS
+GB8rT6mYlJeiu0ApIB4fLFPY1bOgnqrEy8dyPDIvMkq8qaGeoa/ZLhcOER4tQLeclZeesjwl
+IiMjJz+/vb+toaW61sfJUTMqLD/ErqWenabDPh8QDRcoR76jmJidrUkpJScoKDjHs73PtaWp
+w+LByD0pKTjasKafnZ+2RiwYDQ4eVLisnpibp+opISUqKzPOrazB8r6urrzPz+k7Kys/t6Wf
+np+r/zYjEgwSMLarqJ6an7kwICItMzRPs6m1SEO3qbbZzspJLCcxyaifnp6kyTksGg8OHb2f
+o6ehn6w9Ih8qP1FevauvTzFEvK+xu85TOS0rSK2fnZ+ltvo+Hg4LGM+mp6ihnqhLIh8rQ0pN
+vqmtTykvu6auv8DIRisnP6yenp+ltPE7IREMFEmmpKmmoKlPIx8tUXBiwqqo1yciRKefrtPY
+azEkLrScm5+osr9AIxMKDz2lpKqnoqflIx0t+9tvyKynzyclbqWjsct3SC4lM7CdnJ+mtvM7
+JRILEkemo6eopqn0Ix8x+9vlv6uo5h8f9qSistDpTC0jN6mbnZ+mt3g5IxAKFcWlpqeopqpP
+Hx8732Vfya2pSxwmq5+qsMRWPysl+p6cn5+oy0gvGg0NHL2jpaqop7U5IiZC1m9hv6yxNB0v
+pZ6su9lHNCgttZydoJ+p00UkEQwPJ66jpqejp84tJC5T90/+sqtdHyuvpairvWI3JCXcop6e
+nqW76TIXDQwY7KuopaChsz4qLUJTQkvFscssMbisq6WtyD4kIkSvpp+bnqzUQywPBw8uvbOr
+o52fyysuRlA8NU67aTFcu7CnpamxViUiM8+soJycosxM4B0HCB3JvMCtnJenNyxI4D4sMDFe
+s1k+rpudqsJiPigfNaueoamqpanvGwgFE86r2cucj503KVXEPyQcLKSnODunmJ+/z7HGJBss
+sqWxr6Ohp+UoGgsMJUM90aGZn8BZzMFIHx7GrU883qujrK6gn8gnICs6VMOxo5ujUjctEw8a
+ITHVs6ekruPWvUUy38f7fmBZybWvo52js1MnHytC/bamqbzHxz0fFRkuJiHerLbTzrmtrMHm
+v80+QU9Ox62qpZ+m5i0qKStCv7zOt6vNLSQtQTYhJca8OTl0tqu+z7Gt1TpK22pFbbGorbO1
+yDwsL0J+WV6xtUEzPWpSLy9g/D9LRVLEzMe6wsfKfU9SRE7MzMKxrbfFWTY1Rlrc0dreTllG
+N0FNbsDndsnWRDbpyHXOvMbkXkNAPz5iyLa+w7r1PDxNcca79Li+Pj5OOjfNa3qz2EvFSTzD
+vtLRzddCSEI/YdfH1MC5QjBKRi5dus3ZtsRAeX8yQsrJXsXByrpIdbvnvb0+wUREPjRw5b5d
+1rZNOHJBOFxQxNvEYcx3Nbs+TsrKTkurZ0m1cdl4vVPCRE/ONDm7OdW1Sty8SjLcQkXNa8DK
+3U7Uy0HN80S8yDrHeMlfT8PE3WDHRUc4UTnZyD212utlUjPESDux+sS/S0yvUV/gW8FaT75D
+a9s/wcPVR+bJMzpoQljlUte42EjYa1DdScvK1f3nw+9e+M9x9tVOX0DDL8fBQMLPyy+8OEpW
+fEjHy3a9TcFbX2DF6lXFU+Vn00fXxV1tbMxC6D9OzkntwHzn1kD45VQ8v1lwxUrYzMY7wepN
+4VTXUMlOyFfJWdVc73NCwi61RerWwkvpujlNwDhNvVBezM5XwV1gzUjPRtljXthOzF3aacVh
+Zt3xQlTLTcRDv2pc3k5lV9tJyk3c02DQ6dFFxEbAQuHPVr9FyEnK6nVfSMpUWF3YScRKXdze
+V0/MRsVO1WPJX+nUTMhgSsdf6tZd5NT/Rr9aUupnS71AUMXfXXH4195NWclB29pAzcxTdcll
+UtFI4/5a0U/ETclOy+dA4M5V2l9ezF5rZMRhd3p5Ze9SzlDRZM5I5ODtRNvJOs/fXUnV3199
+7/PNTcNYd8lTzNz+W8ZWT+fwPsTVNr16SVq2OFvBQN9QdmDa4F1s29rhR8/RUcFOabpWSOrC
+WlXtftLjPMNLvT5i0WLwSebs1kTa1lpZx0bP4D7MdcdAUsN+60nOyU7OR9zFSdVNwW1JuURc
+4VtkSL03wXVEykXDTs5t2ljc6FLQ1k/L2kzFVH73WfHf+kLM1kpV+sY7zFZayVNV/79Ecm/a
+y0zt0r9cT9HT60Te1FjUPfG3PmdbwetG5N29QevkdL9Hb9DuR1DnRW48PshGQWZ41lXjz8rG
+Y8vMxNXfvMHNy7zSy8PzwLx2KSpyMh8oSeNFNMqowTjsrso6TLa4R022ruJZs6/G2MS3w2C8
+stcoJuQtGSBYXC85vKTEPsC5zjlAuMNEfrW83cm5tcHKsrPFy7q0xz4mLzkeHTVONTrSr7Zq
+x7HkMUi9SDPYt9pXybO64Lmqsb+yrsLMvMAzITQ3HR9BSy48xrfHZb62Vzlg0z4628Xg8rex
+wbevsLe4srnHxLzNNi86LiAlODErOuju1cG9yOHuak9CTllM0rq/uLKztLa1s7a+wcPCw009
+QzAoKSwqLC41SvjNvL/Ew8hjX1I6UuHtvrK4uLS7uLvGu7jGzc3L0UlAPy8nLC0oLDI+Y8/I
+vL3Gvb/0XGFKWNrbv7vAvrjAwr7FxsLK0M/U3fVWQDMtLCwrLjA6W9TFvLq8vLm+3mNRRk1Z
+YNfM08W+ys/FwcPHysPL6tjN6kM5ODEuLjQ3Ok3ZxMHCvbzDys9jR0VLWFtr2cvKzMjKzcnH
+xMXLzM/U2OZVQDs2NDY3OUBV7M3Mxr/IzNHdalRQU1FPX+ze2c7L1NfOysXEwcDH0NHR6VtG
+QUM7PD9ARkxVbOj77tfY4eLyWE1KS01VcNzRzc7KxMXEwL+/w8bHy8TAzm5LPjk0MjI2PUpZ
+deDc0MzP3HlXTk1PT1r+4NPKycrKycLBwLy8vcDExMXQWkE6NTIyMTI6Q1Ju+9/VzMvO0+xe
+T09PU1z/2cvEwsTDv7+9u7q7vsnN0tp8STw3My8vLzE3RE9m3dPNy83Oz9t3YGBdYfbe0s3I
+xsjIxr+9vLy+wsrS5fVrTz43NjMwMjY5P09139bOycbJysnWdWBeVVBb+N7QzMrJycW/v8LE
+xszW6Pzu9VZBPDs3NDU6PUpr5tnOycfGyM3XfFNMST9G3NP36Ma/x9PHvL3Jz8jC0X3bydlL
+Qko+NDI2PT9AUtLN1s7Jz97uX0xHR0xa+d/Vdv+5tczqu7S/4s28vtx9xrnmNzpJOSstN0BA
+PVHCvtjayc3vX01IR0ZKYdrQy8bAwcTGx8TDw8PAvr/DycTDVjQzOy0mKzc/R070u7fHzL/S
+WE9HPD5PVmbNxMO/xMjAwc/IvL29ury9v8jG2TcrLjEnJS47TWn7yLG1zNnQZ09MPjlJ6uzd
+xb68vszXycnSx76+u7e4uLu+xEorJiopISY0VtPFxLivuNliT0JDRDk4Xb+/wr25ub/rUPzQ
+zsi9uLKvtL3Bv8k3Hx8pKCMqO924s7u5tLziTTw3Q1Y+Ona9uLe7x8fD21pj3ci6tLWyr7O5
+vcdfLx8dJCUkLD/huK2tsbjD7k8+MjM8QEfXu7i2tLzO3nVZbNjJvbWxsK+zvMrT6z0nHyAi
+Ji48R2TEsqyts8PpUUY/PDs6QWzIurW3vsvOy8XEx8vHvr7G1WpUVl1fW04/Ozo8PTw7Ojo/
+TVz+3NrPyMTGysfIztvlc1xWUE9g3szDvru6u7/N2dbZ3d/Z0s/lUUI7ODY1NDEyOD5GVPnZ
+0snCwMLDxcvQ1+V7feLZ1NHOx8PGztfWzs3MzM7Z4eRxTkE9Ozk3OTo7Oz5KWvHb1tLOy83U
+1tbjfXjj08vJysvKyszO0tTRzsvN19rfbV9iXlVLRUJHSUNAPz5ASVNXXGd0ePTd19DPzcnI
+ysvNztXa2tfX1dLT1tfS1uZpX11TUllebXJmYV9dWVZSS0dEQUNIU2Ft797TzszKzdfg4NzX
+z8/PztLUz9HfbmRcVldcYGN/5t7U0tjg8m5dVUxEQUFBREtVYXf77+Tf29fU1NXY2djW0c/W
+3ef5+vn/+3169+be397b5PL48n52X1RTV1ldYFZQT09SVFhaWmFvfPHm3drd3d/g3uTr4drX
+09LW2dre6urk5uLi8Hj37O7q6vN7bWRlXFJOSkhFSEtMTVVfaPfk3t3e4d/e29fT0dTY2dPV
+2tzi7PHt5+nu6uDf5Ojl5uXn8W1dVE5JRUVERkpNU1llbG/z4Nvb297e2M/S1NTV19nY1dna
+3Obr6uvu/WtpcPrs5+9zZ11aWFVQS0hFREdOWVpfafzi2NHP0dPRz9PW1tXZ3t7k5eLl6vDq
+4N7j5ubq5ur9bWhgWlZPTUtLS0lJSk9VW2Rt9+vh2tjW1djX087NztTa3eXo5d/f3d3l7vLu
+8vx5fnJqaXJoYV1ZVE5MSkZER0hKX3lu6ODa0sjIzc/P1eZXW93T3OfOysHGzMrCvr28vLzK
+OR4aHSUtPvrUuqWeoKrBOiotMS0rLkyyoqGtyl5ZdeJ7VG+/sK2vt769tbk1Fw8SHC/Gs7Kp
+nJqgtzMeHCo9SXfKvaujq8dIOz/n0VA/S9O5rKurqam3+FY0HBMVGyNNraeknp6qv0EkHCAw
+Tb6rq66vs89HPzg4S+9747yxrKenrLfOPjQ4KhkTGilarKSprKanuE8qHh86wrizr6+1vHI1
+MUTm2OhPQ2O4q6ioqKy4VTA2QCgWFB4yv6elrq6lrc4vHxsnv6moqq+72lQ3LTRNzMjbVk/C
+raamqa278jc3bUMgFxklPrWutrSoprVmKB0iTbGura+6vNIzKjtr18HKQj7cuq6npqyvukgs
+N+w0HBkfLXetrrmwqa7GPyQeLMStra+50tTnPDFPxsPG8zo487etp6esttA8LkTNNRwZIjBY
+tba9sqitzDwlHy/FsK6ts8TCxzwsO97RzuQ9Os2vqqSkrbvSOytBxDweGh8pQbu1uK+orcJB
+JB0p466npq7EzNRMNzo+QF76VVTEraWfpLLcQjEtUNEzHx0oMVK9v76zqa7OOCIfL8etqqmw
+t7jWOCwwOETg3/vSuaukoKm9XzkuNsvGMR4dJS1Yv766rqmwzjMfIDa7qKWrvMveRDg2Nz3u
+wtPd1cewo6CsvV00MVe/SygdHSY428q8r6ursv4qHyQ3w6mlqrTAfT41NDhC7cfKzcq7rKKj
+rcRGMjNtyDwlHR8mPM/Ku6+qrLRaJx8mPb2opay0wVM8PTgyPf7WysDGuqmhpa3GPjA46HYy
+IBwgLGq7tbKwrbC/QSUfJUC2pKGqtc1OOjMyLzvgvba7wb2upaeuzTsuOt1fNSEdIC1yvbOy
+s6ysuEgmHiE6uqWhp7biTkA5MSwzXrmts7m8sampsNw6LzzacTQgHB4q+7etrK2trbhCJh0e
+LsSknqGsyVBDNy0oLUe5qauvt7Wtq67LOy02WU0vIR0cKuexqaesr668PycdHSvGpZ2erNHo
+YzgtKCg4u6upqbC3sK642UMwMjg1KyQgITDfsaalqa631jwqISEsbqyfnaW0zUozKScpNset
+pqWprrG1wWs5LSsrKyorKSs90rOopqyzwFE3LCYmL+qwop6lsclFLiopKz7HsKahpKmuueBL
+OisjISElLTlK2bywqaitu182KyorL0TPsqeipK3FQC0pKjRQw66noqKlq7piOSsfHBweJjfx
+v7aurKusstU9LScoLjpXwa6ln6Gt0DgpKC082LWrpqGhpq7IPikcGBkdJTfUu6+oqaqst/48
+LygoLDJGzbSnn6CpuU8vKiouRMexp6Gho6m44jMbExQZID6/tqukpKSltU0yKSMmLC857rKl
+np6ovGA6MTIyN2O4qaKhpq2xvT0cEREVHkS8taujoqChsUovKSMlLCsufK6lnp6qvMlaOjg1
+MUm4q6emqrCytVsdEBQYHUO+2Lain6CgrEkwNCskKScoTq2jpaasu72/TjIxOVa6rK22tbCy
+tLwyFxEZIjLfxMm0pJ6jr848MDk3KiQpRL+tqKu1trO67T04PGG8srm/wb28vsT4OyUbHSkw
+OUjevLWsqLHNe2pMOTxURj9rxry2sbTH9vt2a3HtysLGxMTcTktbV01n3kApKTtKPDQ9YuHN
+vLjKUHa2sc1OWtPIwr3JTjs/7sreaNy6r7jKz1M6PkxMRUhoztROPUJGPDU2P1dv9N7QyMnJ
+w726u7+/xmQ/O0Fs5lrav7zAv8VnSlVXTkxKSU9aaf1fTktKSk9VVVrz0cva9NfGw8fWflpS
+TUdGR0/uxr7Bx8nM1dnedmZnZ+7c/FpUVVJLREhOVnHe297h59vMxsneXVRRUFJMRUdT2r++
+x9DY2+Pr8WZYY+nb1tXqWUxJS0ZCSFvXyMnZ9evXzM3Y8mhmXFFVVVFae9vOz9DY5vh8em5t
+/u3z6eDi5V9RSz8+Uupy/tbOz87N0+z/3df5W15gX2fj+GXq9Hjc1fNbZv3r7V5benlpb87Z
+SUF921Jd537g2uDg3e956WJv8nxg8tzg7OzxcXFu4NhpS9nBVUjKyU1fzlpIblhFYnlZ7dTv
+7d7x79bT8+vm7vTsZm1dStbDZFnKzFFs215XYlFN1dVXX+RhYudfVXZoUd/UYeTS9v3S2/Te
+8HR52uly3N1e3PVYduJadu1Xc+ViXOliXvVwaGJlXG1pU2Pp3V3n0fLg3N774/n32vnb1fD4
+4G9fcd/0ZXNsYWjd6OjtZv5oVlJeXmRsZXHodG7v7uvo5fHudWBi53Lq3uXc1vVy5Ontbvj5
++2Pv83F48PBfYWBgXVtxZF7k62vk1vff2/z83n5r8uJpaOj1ZV/tcPpyaXFme3f63dp8e+L8
+/WZSWW1hfPPp7uPv2+t6fu5qeN1rZndnY+t27enwfvf3a15jb2r42Nnj3tXbdHBrWmL4YFp0
+8/T64eNxdntmX2tjamBs+vP65eDg5On6aGv1+Wxx9Ozn5+Xn/X5tYV1eZ2Vmanvq7t/f5+zn
++nrq6/r8/W13+Pr77+92/nFlZF1eYmz24+Hf6HhsY1xcY2hw+ubm5ODp93d88etyYfvg4OT2
+3+D4a3JtZGp1/G5raHnt4uDi6XpfWF9dWGL95O583Nxz+PpqbmtmcnJv6+3v4tra6nnr7mtm
+9XJlbft8/OLz8uh1amxpZWlqZ/707+vr7v55a3J2cmxr7+/n3N/o8nZtd3/ud3BzaWNxdnX2
+fXz8+HNtfnD45e7z7ff0eGR88evq7O/7e/tvaHpuaW1z//7wf33w7ers6+x7bv/1c21sb291
+/3Vv++p8dX5rdO/ta3P2dHd9f3n3f33p7uzr+P9te/jy6+vn9f92+255e21senVoenh2fff0
+6uz4/mxreHl9/X/8emdzbmppbvv77e/y8e3t6uzq6Oz4fnlsevP5eHJ0ePH1+vZuZGxzbfHq
++Pbz+3l2d3FudXl7fPn6++/s8+/xfXd0cXh0fPn9/fN7a296env3/nf57Ofy/vP59+jk7vlv
+YWR3/e30ePjufHfw+Hr9+X5uY2FwdW1qbnn3/Hv9fHb47u7x7urq6enl9vv1e2xseHh4dv71
+8/j78PD9fP97/vV0bm5tanl5bmZeXWRrZ2p6++3u7u/x7Ovr6+Tr7Ofq6erl4uff3uHj3+Pd
+3N/odltOTU5JRENFRkxRWXbj29XOzc3S1Nzu4uDm3uPu5+Lf3dbMys7Oz9nZ3OTd411LQj88
+ODU3PENIVO7OyMbAvb3CzvNobl5NTlhdbHb01MrGw8G+vsHGx83e2tFoRT45NDEuLjM6Pkt1
+yby6t7Oyt73K6VhEPz89P0ZPa9nNwLm4trS0usHI0NjiVTw2MiwrLCsuNjxS0b+3sK+trbG4
+wNxOPTg4NDQ6Q1Lyzb21srCurrK5vsbUflQ+MS0sKSgoKzA3Qu/BurKtq6qssrbBakU+ODEu
+MjtBSHTEurWvrKuus7O5x95kRDYtKigmJSctMjxX0r2zrqupqq2vuc5WRz0yLi81NjlK3cS8
+s6yrrKytsrzEymY9MywoJiQkJyovPlHYvbStqqmpq663xeNUPDQyMC8yOUZe2L6zr6yqq62x
+t7zOUD8zKicmIyMmKi86TM68ta2qqamqrLK9yvFGOjYzLy81PUVxxbu0rqytrrCzus9eSTQs
+KycjJCgrLzhL4Ma1rayqqaqts7zK9Uw+ODIwMjc9R/zJv7evrq2trrO/yug9MC4pJCQmKCow
+P1LmvbGurKmqq66yus9uTzw1NDQ0OD9SdM+8trOura+zuL3tPzoxKSYmJiYqMTtC6ry2r6qo
+qqqrsLrA1E4+OjYxMjk8P1nVyL21sLCwsbe+4U5BNSspKSYnKzA3QurFu7Crq6yprLS5vdtT
+S0A4NDY4OUNd7M++t7Oxr7O4vtlbRjctKykoKSwwNj9e2MC3sK6tq62ytLrO9FxHPTs8OztG
+VmPWwr66trK1ubzPYk49MS4sKyosLzQ9UfHKvLaxrq2usrW5x9ftT0NAPzw+SE5W4MnFvLe1
+uLq9z2tQPjMvLiwsLTA0PEph1L+4tLCvr7O0t8PN2F9OSkU+P0lOVOfNy766u7u8v9JjWEQ0
+MS8rKy0wMztJWOG/urexr6+ysrW/xcpzTk1LQ0NNUVD60tPIvb2/vcDXZ1NANTEwLSwvMTQ7
+R1Pxxbu5tbCxsrG0vcfL6ldVTUJDTE5OaNfRzL6+v7y7xexfSzk0NC4sLjAwNT9KWNG/vbix
+sbOzs7zExNdfXlVHRUtOTmXf3c+/vb+8vcjjbU09NzQvLC4vMDc/S1jawb25sbG0sbS7vsnc
+f11RSEdLSkxcZejPyMHBvr7ByuNfTD43MC4tLjE1OUFPbM7Bu7e0srK0uLvC1O9oT0tJRkhL
+WmZ+1srEvLq7vb/G71hLOjQzLy0uLzI2P01X1L+9uLS0tLa3vcfL2G9fV0xJTFRcbN/PysS+
+vby+w8nrVkc6NTEuLS8wMjlDT2XPv724tLO0tbm+yNPpYFROS0lMTlZr6NLKwry8vLzDyNdR
+ST41My8uLy80OTxIXeHDvbq1tba2ubvBzNb9V1BOTkxQX2/u1svIwL7AwMDL0X1KQDczMS4u
+MDA1O0JV+su8uraxsrK2u77K1+hjU01ISUtRXHvbz8vGwsC/vsXN11ZKQTY1My4vMDM5Pk1q
+48a8ura1t7a4vMPQ4GtVTUtLTVNdcOzazsbDv7/Cw8jT2l1JQjg1MS4wMTM6PUn/2sO6ure0
+tbe8wcjZ6v1eVVJMS05VZ+fUysfEvr/Dw8nP3lFEPjc1MS4vMDU8QlHu0L+5uLOytbi9wMrd
+7l5NSkZER0tWc+bVysW/vLy/v8fO2lFBPDMwLy4vMDU8QE7pzLy3trKwsrW5wM3fa1ZJQUJC
+RkxQaN/Qxb+/u7q7ucHR2Ew7PTItLiwsLTE6QEzVxLyxr6+ur7K3v8vdVkhFOzo8PUVPZ93O
+wrq7ubS4u7vJ2Gw9OjgrKiwqKy82PUnsv7iwqqusrK62v8xsQz86MTI1NTtIXNfEvLOys7Cy
+uLu81FtVPDAyKyYqKSowN0ndybatrammqqyvus9WQDcuLS0rLjg/YMi9tK6sqqyvsbvF0G1J
+PDkvKCopJyktMztPz7mxrKemp6mss8Z1PzAtKicqLS8+X8y2rqunp6mqr7nD21lGODE0LyUp
+LicrOD9N172vrKympauvtspUOy4qJiYoKTJHcbyvrKakpqaqs73PXEY+OjMwNzAoLzYtMkNa
+4Mm5rKyuq6uzwNtNOi8pKSopLTxL3rmvqqampqiwuL/tTUI8Ojg5PkM2Ljk4LzU/WPPTvK2t
+sK2tuMxpPzUtJyktLDZl07qtq6emq6ytvtl6Rzs6O0NPSErnTzQ6Oy8xOEXuzMGvq6+ws7zX
+RjEtKycpLzhFzbSuqKanp6y4wtRLPjs6PkRM3cDN7NdKMS8sKy41P9C4sKqnqa+97j8vJyQl
+Ki4927mqpqelpq+80k9DPDQ2Q1Nnzbu3tLrLXjMnIyQmKjrVsaqmo6Oqvk80KSMgIio7/byt
+o6Clqq+/Xj41Mjk/RVrMu7ezr7G4xkwzKiAeIyoxT7ipoqOnrLpXLyYiIyYuSMKup6amqK/E
+XEA5Nzc6Us3Cvru3t7zBxM/xUTwvKiUlLDM/2LatrKyyu9tCMi0tMTxRzrWtra+7ztRYPD9I
+T33f7cm5vMnIwsPDz9/Q3Uk7LyclKSwyTcezq6qttMBfOS8uMDtS172vrbC7zltYRjo+Ve3W
+zs29trzHy8bGyczVz+RFNSwjHyctNFu8rqinr7rJSjEsLTFH1r+0rKy2ylxGSUJASunJwsTK
+wLq7xtHRys3u++XzWDcoIyInLTZTvKunqK66z0w1LS44T9G9s66uuN1FQUU/RFPvxLzFyr+6
+vMXT3dvqfGz6ztg+LighIywxRryrp6asudNFLyorMErLurKtrbLPPDU7Q0pi0762t8POwr3E
+5lhPb+xm3L262DwrIx8kKjL0r6ejprDCYTgrKCw59r61r6yvv1E5OERbXejJv72+zM6/vsz5
+VFTp5P7IuLzZOicgHyMqON6upaSottNFMSoqMErIs62srLPOPjI4Q1/g0MG5vMfN0MTC2VtO
+Tlhe9MW1tPAvJyEeIyo1y6umpam1zU40LC46abyxra2xw1k9MTdQcdLEw7+8ydnNx8LVTkFL
+XG/Kta+9Si0jHBsjLUy0p6KgqLvsOSwqLj3VtquprLjYTTQvN0vOvbu/vcPMfmzf0OFYS0h+
+z8O2r8JCKh0ZHCUv0ayhnJ6qxUgtKSktPsSuqKeuvd5CLiw2Tsi6v8K8xdzqZ+TL2k9HTt7D
+u7Kvwj0oHhocJjXMqqCen6rLPyslJy5GvauoqK/FTzkwMz1M0rq9wMTT2M3j3sroUk9U8MO4
+rq7KPCYeGhwmOcSonp6grd85KSUnMVS5qqapss0/Mi4tNEvDtbW5v8fL1H3cz9huTUznv7Wu
+rcgyIx8bGyg4wqSen6OwVDopJCo74bGoqau0djguLDI/3bu1t7/N7Gpp3c/Hz19QVv7Gtq+v
+xTUlIB0dKTzDpp6hprVONionKz3es6qrrrxVOTIuM0vZv7K2vsTdbtvW2MTPZG1s5sG2sa/H
+OCIcGhspPb+lnZ6iskQvJiUqPdWupqisu1E1Li0zSsq4srrGz+P049PIvsdnTE/qw7extdMy
+JB4aHStJuKSfoaa6Qy8nJi5Ey66prK/DTzsyMDhF27azucHR29Ha4snD0mpMS9O4tbW48jEj
+GhYeL2+soZ+eo8U8KSInM0zAq6iortQ8MCwtQ9y/srXBzN5f7M/IubvdT0lL47+5sbRnLiMc
+GSAuVK6joKCozkEuJys0TMKtq6yy1Ek6MTNN1r+2vcvP6l3a0Ma8zV9JQ0zVvrOvvV8wIx0b
+Hy/qraKgo6vFRC0oKzVWvq+trrXdQTMvOV7NvbvBxtH5edrMv73J51RGTfPOuq+3zVgsHxwb
+JD3ErKKiprBrNSspLj/4va+us77yPjY7TPrLw8PDy9zSx767w9xoTkVJXMy3tbi/fUo9KBkY
+Ijy/rq6sp67dOy0uQ//Ovbi3ucxNREhKb+Pdxst/eezXvbq+wNJYRTs9ZcC1sbrqWUs1MTs5
+JyU3XL287N29xNf+SGLJy9b4VGfbb+zDvb3C5FZYTlHvy7y4vcjXXEQ/RmnHw8/5Y0pAPjpJ
+1b23bCUlKS5LWd2upq63+DxDPjtKzLmvtcbIy99tXF7Y0WxUUnTLxczS23VdUE5ZU0dNZtzO
+3tTK4+rtPy4sLTlPYOG/tr9jRURQ1Nb11MbAwNLjxLq4u89wXk9LV+DGvsbhX09HRUpPTVRe
+WmJn6+L3WVBqZFdPTUlLSVLb2djZfGfubV5uevng1tPIvr2/zNx7ZG1taXfr3tTfcGVofe39
+Y1NNXHJdTU9q3d91cufZ51dTWFZbTUtc69nX5ejoZlJIT/vb0MjHytPxYm1uXV1v18bH1+Db
+2d5tXWhpa3n/5t3zXVZaYvzyZ1ZNSkpNTVf50cbJ1/prX1NGRlPozs3P2/NoY27p2tzZ0c/Y
++W9w9Ov569va5v1tdPl7bvfi3vZdU1ZcVk9RXv/u6uHgfV5aU1hgW1ht697V2tnZ3+fm5enw
+dHD87N/d2trpdGlvbWxu/vDo5e34dHZtYWh67+9zamZ27uvu6vD2bVdUVVNabXF95+Ho9H1s
+aHnx9+zj3tbW3u3t7fT+fn10fO/4dPDg4en0b2ZhW1ldZXvo6PH+/vjtc15bYm387+nm7vT7
+a2RpZF5aXnfp3NfV2d7l7G5iX2Nx7ujr6Obm6XtyaV5eZnPu5ufn5el+bXJ+/ndqZWNvcnzz
++Pbq8WtiX2Jocfbs5uDi8nFoaG1xc/rq5+ru6e7r5u53b2xmbvzu5uLo6Ofr+Hp0d3V6/355
+b2Rja2plZ2pqefHu+Pbs9/zy+ff09fn6ffHu7fVucP76+fP9c3Jxdfrq7O3s9vHx/25ueG9w
++/n2+H5rbv//+/j09vV5dnN4fXv77+rl6f5ybnBnZ3J8e/Xj3t3f5/TyfmhgZ21sbXRub3Ju
++/h+cW54+Ors6+no73L8fHBubm187+35fvn++Pjy8/NubHBz/ezn4d7m+XJvbWtqb3vs6O7t
+8nx8a2huZWJdXmp3+fH8/3n+8PJ8+ujq8evk6PT7fnN9/vzl5fL17fD26fVzcmZeXmhscv16
+efXx8fXr6/X5env77+3+evx7aF5jYWFkZ2/79Ovt+ubl9vZz+OTp5uPj5/Du9Xl0b3Z2eHBv
+fn11aWd1+v1++P3+9/t8/P96a2Nmbfbr9/X07e3v8/P9dmtpbn//dPPs8ezp7+9+d/L2fG98
+eP55dvh/dXxtbWx76/V7/HF48/z38Pn27fH7+fb8dHZraX32++fm8nVycWdocWpeZm177P50
+9vDq7P/08Pnn7eni6Oru+H34+ezo8nxnZ3f2dXJsZF5YXWlvePfn4+Tr8O7v7+z1fHVucf7w
+fvx9++91bnn9cW135uLo7np5/XVsefl5en12fPhubvbx8O757en7e3V+9WxrbXZ5ZmtqbO3k
+6ebq7vl++Ht7+35yZml3fvN0a/Xn6vTx9nn1/Wz8+2x97H7r4X1vcG1vaW7s7fl2d3psaWVo
+ZmRqcXv68PHt4uHk6nN88e36fOvo/WdgcPj68e7s6XtvdHBud/lzZXf8e+rrffnx9unq+3J4
+c3tqaXFqbmxpcO7t+/99fX93dvjwcnXv9fL86+Hn4/l2+X16bWn26fr79+rtamdqcv1tav16
+cG1oc/T8++39cerofO/i7Pl+c29ybvvreX5xcnpobnF8+PXv7+98fXZu7+v7ffbq6/Nvcez8
+f357dWZ5+fr2cfTg7WFg/XlvZGxrdPZvbu7u8ud08eL37t/t6Xtn7PVu+2Vn7GhaXmR87O7l
+9Pvm7/L39+t8Y3L5cvh7bu3m9XZldvlxcV5daG9s9vbj23Fu9u/z+fvo4+V7bOXi8u778etw
+Y2xoYWJhdv7/d23082h4a2jr7/v78ent6/14735k6et0fGlu7/hv+fHw92l47PJ1bvLqdHF3
+9f18/Hd7/PNpfN/4avt8evFhW3R2a3fz3OXsfOrc7HJseezuY2H7+Phxeebtb2b76mpmafjn
+cGFv+fp+afvxcGxu/eHfb2fo7GRs9Xvl4f/55uv3cvtt9edreO35YGvh19t7a+rofGxhd21b
+XF9db/x8+G1kde99cnf/6v1kb+Dk5uXu3tn3d+/08+/wcfrp7/nodWNucm/x/Vhi/G1ncm5p
+7epnfm1s5O5ud3b249748+L5fv1dZO59be9x/+X4dfBwb+TrbG11bXZiY+zo9npoXFxs//Tn
+5ebc9Gh+4djV7W716f333OLj6ldWYF1fXWTz6m5ha/tnYlxh+/7r3N99821gb2p98erc1dbX
+zMnX9fvs5Nrg5dnia1ZJTVdXVmvh7WJXVVNXV2R96eZrWvPtan3r6djX5tfL13ze2NTbX2Ln
+719aaOjm9/t3YmNtaXHm4ePW2ONzcW54e2lsXFpdW191b3hxeXNYYP51fGZj6P5lbvfvXE5b
+7d3c2Nff7Oz14NfX4G9s73t2+fXX3l5dX3NeVGhubWZfa+zn+V9hamJt8vzv5vZ77f9z+nl1
++HFs+XBn9uXt7fBufPb29Wl96+t1ft7n5O7u29/pd3Lw92VdfHFaWFto83dq/P7vdmB54OHr
+7ene5PXp/2tvbfbj5e3v395vbfFtdWthbmRmbv3r5+v5fmlpe/nv9f/zfvLu5uPr5u7t+O/q
+7+jpc2lucnv1dmN/+m9qX2ZfW1tlam588+jq7vHf3eDg49zS2eTd6vjvenz6ZF9uY1xgX2B5
+b1tmfn/88eXj6/Ty9G1eVltgYmhsend7dfjq8H3y3tfV2t3p8edxa+z0eX1v++br6+P5/XFj
+a3vr5u73fvtzW1VZVFRhZmzu5vl0cW5wa11e/+7v6ebc3unse+/e3d/d5PTu6+fo5eXq7/9j
+X2ptZV5daWpdXmh1fPbs7Ox8cPnk5vFrbnZ0/vHg3Nzf5OTi7Hp3bGJga/30fGVhaHby5+3p
+5fl0bXj0/3BxfHX85ufq6Orr5fX/7PNzb29sffhqaWlscG1sb3J3dWxpduzs7fz87ePl6+fq
+9H9zfPtobXl9/3JqbGVdWFhiXlxfanlubPvh3t/d3t3Z3/Lq6n5vdOz2bGZuevZybu3xdnr+
+bWhyam90c/vtfGNp/vBnYXZ8dndu/OXo/Pnr7u30/Xl18/1td3r44+//+f7s92pjZvz0dWFu
+c2xpb3r95OPw9ubq8Xxy7n199PHl59zd5uTyeG52fmtpfuv5fPT5b2pqZXJuW15nY2798PX8
+d2VgaXByfvvy7fT+dv368+fo5ep8cvhv8Ojp3OLy9+Dl9Ozh3uv1+Obtem9qb2hfYWxpZWts
+ZnJkaXppZWvx6vdsb3bs5nHv2d7ufnDs6nVtbvD9Z2Ry/XD//n389n7y7WJt3+h8bHHl2eTo
+5enf+2d24+d7evf7/n9odv5nWmRqW15ecftuXXfvdGpfY2/+avvs5eb28+zh3OLi5ur8e/H7
+/mNo+fn8/+rn5/N49+5tbf7w7vDk5fNxbv7t+29maGxlbG5z/31zb2lf+vducmdu+XZpbHJz
+dGtwfHDu7ujf6Ozk4ux+fPDvb2pqbnzy/vn9enf/7fl7/Op4bvHt5uR7b3t0aF5eZ25vdnv5
+7e9uZ+33a3Fta2xbVnP5Yn3xdXRlZO/tY2z8dvp6a3h3aWx9dvnm8/rr6vX18fLl6evj3t3s
+5ubw9Oz5+e13+Pz0+nhwcnduc19ebm9kaGJr7vj75+bu5vbw7HRoZF1hZV9eanhvdX3p4vp3
+7d7c7O/f3uLh3eLo4+Tj4u7+5ed6amRrbnNtbXJvcnB38vHu6ejm6fd4e3Nu/vt2dG5fYWNi
+YHjx/Xtyb3L07e3m5uv0e399bnl2aW5tfHzy8/nk4uPe5ern+P3v6erwcv7w7+z/bm/3/3l5
+e/H4ZGZvbnV1a2duff736u72b21zcHpyb3dxZGVufn50ev7y59/f5OXs/f/xd21sZm17fvX3
+eXr38e/27OLm4O34/v70/PDr+/j3cmtqbnBqZm1+/W9wbmlqYWh1b3t1bnF5b2ludXf77e7r
+6fXy6unv8fD8eG94/f5y+3lxbGp98/H9+H3/7uvt+PH4ev9ybnv+8PT3/HZ7cn779O/6ef15
+eHFlX19fY25vb2xxdfn3+u7v8XZpcXd6+fn+d/l5dPbo5unf39/e3+jt7/x2f3lqcnVzeXN3
+b2ZfYmzx4fdx/XRqbWhibfv9+PHr7vjz+e7yeXz8/fzp7XhycnZ2c25tcu/ye/Ly9frze3j3
++Ozr6uno6u/u9f3w7vd0bmxkZmxvb2ppbnV59vd4ev72fH74/3xwc3N69vL/b252/Xh6fP75
+7u97fHz48Ozp7vL9cH3u7OXj6vvz9vLt+H1ud3l3b2RqbWhqbGxvbWhwb331f3VxeXt99Pd6
+e/T5+PXz4uLt6+ft7+32cXR8/f11ePV4am1vbn705+L0d+7q7Orz+/Zyam988PX28vx3evpy
+cG5vd3n3eGt+9PD4+Hh+dWp0eGlja236+v3v9vz9bfzr9O/yfPrt9nl8dnjs6ers7e/z9e7w
+ffX7c21tbnF0bnn+cWt5cWxxcXv5/vvr8fH0/3307/f39Pr4cXN4c294/3h7cv36/fd8fH12
+bGlnbHN88O/u8Pl+/Pb7+fV1cPvr5Ofm6Ofo8npxZ2VraG31/XZ0fXb46O77/e/o6e/8fH73
+9H7z7n58e2xpa2tqa3NvdG5neO3r7Orx9PX09nR69Pbz9PHr6urv9+ztfvz4em10fXJubW9+
+fnz38/54dn368+bl6er4+/5ufX12b2hsb21vbWxvf+33/vfz9/P7/vl4bnt/dnr18/Ho6u3x
+9ntxcG91dHb7fWtobHr89PDv6/j8929pbXl5fPbx8evv/nr38fbz9vP19Hp193hsZ2lnYWVn
+a21ydXJ2d/nv9vf17vb+7ur9enxy++3y9vX0/PP2e3F5eG5qbXd4b2VoaWpz9Ozy59/o8ff6
+fX36++3r/nzw9Pv1b2ZhZ2lwa2x4ffb7fu/x/v5vZXX19Gxq7+/59Pr+8evu+vl4e3x2bW71
+7vP17O3s6ujr8e/u6/Vw/u59cm9raHn3e3V0cnV++vf7eXlvanF57uzy/fHw7PhvfO3q7u74
+/nX+9Xd1+/H6eXp2f37y/Pt8dfX8cWxpbfz59e7r5ODh4eLr7ut8bHB0cWdeWl5na2138/L0
+6Ov48O7z+PZ2enl4/fT1cHBucWtrbnJ7d3Fy/PTw6uzr6ubs+u7u7O/v9XZ8d3Bvanp2eG5k
+aGxsfe18aW5vdHBlaGRt8+78cHV99urm3tfV19ba4etvZGdlX1pXW15hX1haXWJlZG/z4Nzc
+7f3u+P/78+ja29/j5t7Y0tLa9mtwd/xtYV1cWE9JQTw/U3LVyc7My9J/Z1pXbfnl3Nzb2+tn
+XmpwcHts/vHt5vD38+zo7OXe4eXV3ODf+PP4aVRVTUM/Pk5z3dHe39jeb1xPTV7263hu+dPN
+z9fc2NTY7PTq1srIyMzY4fRZUlhOOy8rMEjRvsG/v7/KbEc9SGPd09LLx8bYUEVFSlj+2MzG
+yM7U3t/e3+fu7Ozb1tfMy9t9X0g4KyUuQue8u7y6vdBdRDxN/tzMysvL3FlPSk/jysfJzt3g
+6fzk08nGytb9XftcUtbP1vE5JyQqONC5u7W0usVdOjc+Vc7CxcC/0llBOT1gycDAxtHP2HVj
+7s/JzNz/e+DS19DFzthRLSMfKka/srW1urzaQjY0QfDBvb2+xuBOPThG38a/wsrN1Hxke9HF
+xMvZ6OrV4XPQ2dp/NiofHi51tq6vtba9YD40NU3LwL6/x8z3Rjo9Wcu9v8zW4vRzfN3HwMPN
+9l9m9NfPxL7MTy8kHR8verCrrrC1zUs7MTdN0r23vMbZSzw7QFfJurzA2V9dZ+vKvLm9yehn
+ZFlv1MrG1UQtIxwfNs+vq6+2udBLPTM6fcS7usTbdkg/QE/SvLW9zPFTUVjvy7y6v877XFVW
+d9jHw85RMSUcHi/ur6uvtbrMVkE5PXbEu7zM5l9GQD5K0L24vM/8ZFtj1MO6ucr+WktV99nH
+vcDIfDQkHB0t6LKtsri7yFQ/Nztqwru+y+RvSkI+P+C/ubvN8nByfs++t7XB2VtMUmZ94snJ
+ydg3JBwcLGm0ra+1u8hWPTU5WsS5usHT2etMPkFcxr3E0HJeXu/Vx7y7wM34WVV43szByc7X
+RiwhGx412q6rr7e6wGlIOTxsx8XG0tbI01hERWDKxc7f83393tfKwcLJ4VlOZdHEwL2/z9pC
+Kx8ZHjLLrKuyuLjE+kI1O/nFwsfZzsPPUDw+YsjEyNDUzM/Oy8fCxNRbSk3nysG6vsnYTjMk
+HRoiPMisrLCyutFPPTdK3ci/vr67v/JKPT1g1srFy83V2tvYz8rJ31BCSHbNvrm5v9JaNyYc
+GSI8vKeprrG5zVk7MT5ty768wb/HdkU2NkvRwr7J1Nbe3NnPzc/bYFV+zLy5vsHJ2vs1IhoY
+JUqxp6msrrTSRTAsOWrKvry+u8daOjA5cb+4vcfL3Gtga9vKzOVgVufCure3vMTNXjQjGhYd
+NMWnpqmrsslTOjA7bcq7ur/C0kg1LzVuvre2vcna7Obg2tDN3FlRZ+LOwLu4uMB8NyEXFSBA
+r6Omqq+8/T0tLD/Ks66zu8TuPi8uOt27ucDR2u7l2M/BvcTZVExq2se9u7vA+kQtHhcYJ1ur
+oqeqscFmPC0vR8q3sra6vn46MDFEybq5wtbd7f/pz8K+x/lOTGLY09G/vMTWPioeFx4xyqak
+p6u13j8wKzRXx7ezubq/eT8wNFq/t7vTYFxcWWfbx7/G2eTazb+9vre2w1gwHxUVHzyuo6ep
+rbvqQi8uPuC8s7e8vdBTNi44csC/1GFm7NzTyL67vcz8X+/Jv7u3u8HLQykfFxcmQrWmqauu
+vXpGNjE/37+1ucTF4kg3NU3KuLnL+u/o2c3KwL/G7FJNUPPNxb68v89CKx0XHS7GpqSorLXP
+SzYtMUfSvLrBw8d7RzI32Lqxtsv03dzo1dTKwtpMQUJYzL28ubzLVy8iGxsoRremp6isvndA
+MS85TdfEwcbHz15CO0PUt7W82mby5N3SysK+yO9XWt7Jx769y9tFLB8WGCZDrqWoqq662E04
+NkfYvrW4vb3YSjsyLkbIwbzPUXLV3NDO18zK4W/jzLuztbe71UYsHhUYJ0iso6eqsMZYOy0s
+PHbCtbi+wuBNPjc87r+5vczd3s/GxcTFzNldSk/fwLi5v8TWPiwhGRklPLmlp6quvnhGNDJA
+dcW2tbvDeUU9Oz5Xyr68xORgad3QxsLGzeRhfc3JwLq7vs4+Kh4WGilTqaGmqbLLVjksL0De
+ua+zvMpeQjw0PdC+uL1pVVxu4dDLxb/LXEtL9L+1s7e/XzQlGhcfMr2io6aruN5GMCsxUMCv
+rra920Q5MjBBxbOwudpdX/vZ1MrDvspcRErhx726v9JELB4XGSdSrKGkqK+/ajouLjvUta2u
+u8lkPzcxOVDLuLnG4V9j3s7Kx8TB1WVYVt69tLS20zkoHRccLfiooKaqttpJNCssO+u4rrO9
+zlVAOTY/+sG1usnsWPTOycnIxcXtUUhLz7qwrr5OLyUeGx4v0aefpK29a0I4LzE+1rausLzP
+XUU8OTtK5r65wM1pXfDYzMnKy+Brb3HVwri0uu4vIhsXHzPQp6Omp7DMSjMsMEXLta2vucNR
+OTU0QuLLv73L3G9efdvMycrP3/zv1cm9t7W5dC8mHhkcKUysn6OotNJNPC8uOFy9ra24x1o9
+Ojg/5L20tcDkU1Zp2crNztTfc2Bd6Lu0trxgNCogGx0pVa2ipKq43FI8MTE937atrrvSUzw3
+Nz95w7u2vdb9UGHj08/T2dXlbPXevrW1u903KCAbHi1hraOmqrbpRjUuMEHQtq6uudNKNTQ4
+RXzHubq/02NVZefPzNPb39fZ3dXDuLe6xkMrIhscKUm1pKass9BNOS4uO+25rq661lQ7NTY3
+S8m4srnO9/p94c/KxcbN42v96c68uLzLSC8nHhshNsWopKmwxlc9MzA6Yb2vrbPJXD02Nz1U
+0cLCvcDY6lte18zLz+Po4+Hb1b22ur9gOS0kHx4nRbampKy86UI6NTVA37qwr7ztSDs5PUn3
+zMa6usfPcGXZzMnL4uHV3N/n3cfDxNRNOS0oIiAoSbqmpa+8+UVAPTtJ7b2xsr12Qj4/SW5u
+6b+7vsZgU3fjx8fU3eDd3u3d0MjDy/NQOC0sJyYnO8qtqK++9FNNW1Ba78a4t8H2SEFHT2np
+xry+yPVSS1Fi0sjEwcnaYl/21cnJ0+ZNMzAtKy8qOMmzqa3IaU5LaltY/9G7usbjR0FOVWH8
+fM69w8rhTFdz5MnExsXQ5+Lu3tPMxvRKODEwLi0oL+a3qqzA4XpfZVBETeW+vMbbU09ZVlRW
+bdnGw8bL4/394cvJzNDY7d7d3tfb293+SjouKi0qLUrRta60vcbdfllNTljTyMfE1vL3WVFP
+T3HMys3Q/2Rja9rMx8vN0tvmW2Tnzr7G4kcvKykkKT3lsKuusbvQbkc7PUfvzMLDzdnvWEtI
+TVVNXGvJvcrLzsjBx+l349TP1NXSxb7JUzgtKiwqJTD6tqquu77H21o+OkR2zMrJzszJ71BK
+PkTq0MvHzMzH0OLV0M/R1tXS0tvZ2dbPUjwxKCglKDzZt62usrXC7kg7PERh38vAwcHKa1JR
+TU1RedbKyMzT09TY6Gxv6NLR29rQzMLB10s3LyghJTBet6+vra60yU02MjlHWd3IvLrAzOpe
+ST08Q17OxMC/wcHK3GVWXHndz8rFv8THxHc9MCcfHis+zLSvq6mtvmg7Njc5P1jOvLe6wcra
+aUs9PlPdzs3Qyb6/zmZVXWhfatvYyMDJxsl4TDEoIiAtO9W5saqoq7XORzs0NzhD9MK6ubzE
+1WxJOjs+Tn7Vx765vMXealZTUl7fzr+7vLnKWU40KiAdKknJuLSup6m1+DkzNDc2O2y+s7K7
+wcXWTzgzOEju2sa4sbTA425TSkZHeMS8vLu+wMhMLSYeHSw6aLqvqKOot9BGNy8tLTvsvrOw
+tLW85kc3MDdETty9s66zwdhpSkVCR27HvL26uL/LRywjHR4rO+67rqeiqLLKSTUtLC08fsK0
+sbCwuM9ENC8uOUr+urCusLnL/Eg/PERkzb67s7a70kAuJB0eKjjpva+ooqaxyUc2LisrN0/L
+ta+urbPJWzkvMTU9asm3rq60vc9WRT09VNrSvre6vtVLOCkeHSk0T8q6qaCjrLjvSDUrJyw6
+Wsi7sKqttcdIOzczMzthw7Szs7S7y19DRk1MWdjIwL3Lb0MwJiQpLTpNzbGopqesuMtONSwr
+LTVDZL2uq621vdFOOzIyPUv8zb+6ubzH1+Hh+frt2djbeE48LigsMTM6TNG3rKysrrK84z82
+MTEzNT7lvrmzs7S5zF1IRENHSVXoz8jCyMjEzNbS1+9jV0o+NSsqMTY5P07cubCwra2vtMRp
+SDw3NDM4RVniyL68uLvGzdHc4+5oamdZXFlSUk1Pae3u3tz39fpdVFdbWFRTU1txf3f76d7b
+43NcW19lferg1cvMz9nc4/Zybfzp5t/d3uZpXFlVV2j25dnW1tLV2d/rZVNPS0ZHTVRZYG9p
+ZWZhX2FiYWny3c/JyszT5Onz7ufm4N7b2tbU2+l0XVtfaHrt397f5/D1b1xQTEZFSkxNVmvy
+7Pl4bWdnXlxi8djNyMnJy87W3ux4c/nx7ufh2934YF5cWVlj8erp2dTW2upfVU1FQ0RHS09Y
+c+be3OPj6fr+efPb0dTVz9DU3OZ/enx3fvPn393e3efq6GVVV1tcXWJhanb/eWtfV1NPTVJZ
+X2jv3t/e4OPh3uHf29vd29jY3OT9Z19hX2Vy++XZ1Nja3ef6dHJfWVdTUlZcXl9eX2RmbGpp
+bnz34Nra1tfd7nd0aGlrbezh4N/h4OHq+mxhY2Ntdf15c/H39PH0eWtsbnj+/O3u6ufre2Nh
+Xl5hbXl5d3V1ePf8d3d2+fbr3t3n8vZ3bnN2bWptfO7n3tzf6e3t7vZxZ2RdYmNgbPn1/vf+
+9Xp38uvk73Z6e3h1cHj+8uzs93hvdnhsdP1ya29udPjs63z+8vHo4+9sa2hkZ3H/+fl8+PPz
+6ut7e/Ln4+Df5fdscHr9/fP0d2pramZocG5ubGt+/fP8cG9//mhjZWdu/fj17Ofr+PDl5u/1
+fvPq7urq5+nv9Xj+e3Zwa2pseXn47vd3ZW1xcXV1bWRpb313/3x4dW599ufm5unr7+7zcnv/
+c3759/fs5+707er2efx2bW949fpwaGhobHZ+eGxka21ufu7t9/z77d/c4OPn7+3m4ujt+W9j
+Y2plX2Bqbm/76+vze3Zvb3R6b3NzcHvx83t6dG5x/PH39/r79ujg5uXm6flwcnd9fHn+8fD2
+fHNwa2Rna3R1dvz9/fnu8fl3dnBz+Pt99+73/O/w7X1ramppaW14+2xwb235729vc/Pd3+Dn
+8trY9+nqeWxnbWf983FzZl5XW/fv9O7s397h4+Lc4nR2eHH89vt3bnV5eWxeZGpkbXhyaG15
+Xl5vdG16/fz06+jp6fHq3+Xv7OHg4uPh6nz/7O718/LxcmtnbG94/PN4bG1oaG59enNuY2h5
+bmxnZl9hevL7ee76fHZ+5+vm3+Pk4+Lp+G5oamdxbWxzcXz57e307+r8bX13aWRlYVpfbm5z
+8Ofq7ejf3Nzf5evt7Onq6vx4dHBzaGdqaGJpdXRw+H59+nBsePp6bGhkX2tubG5ucfn9/u7p
+6Ojp5OTu5+Ld29vf8nl6/fnz829ma2pu+/1zeG9maf7u5+Po7Or4//f3fnR3e3FvcGlsc25y
+fvv4/HhsaW93/n9yfP5udv7z9n75+37s39/p9ez1cPvs9f7/eGt2evvu8/Pr7fjv6u18b3p/
+eW1ramxy+O70/fLy9P93/PX/9vZ5fX5vaW1vbm1udW5mampx+nt7/np1/fTu8Ozq7/f6fX7+
+9vLn5OPh4OXp6/Z47+fwdmloc/vv/WxlX2BfXF9qZ2Fobm947u79bWx2+PDy++7t7eXl6O7t
+6uvo5+jveGBdXV5obWtsaXbz9fLs6efk4+Tk7fr5+3hvdXZlXmRoam9++Pb/b3r+cXJ0a238
+5ujx/vl5/O7r7fJ5df91fvf9eW1qam9xdm9yb2dt/ejo6+55anbv9fry8u7y8PR4/fP89/Hv
+6+n0dXNybnBze29oZGdjZG5tbGxwcWtsffb47ev4/PX3dnv6fHBvb2p1eHj66+/s4eXi4ez5
+9O7v9/vy7/bz93n6+Hh1d29xemtmbnV1c29xeXNuY2ZscP74+3z57Pn38PDyffbu9P59+XN2
+fn17cnV4+fPt7evh6O3s7u7n7nlwaGxsbP75a2RqbXL7/3z9/Pv97e7zdWxtbf399/50bnj5
+8+vp6Ovw8P5vdnFycGhlb3NyfOrn7e39e314d3h1dXx6cHj7/ff39/jv+HR4+vP5+3v+d250
+en1+7vV8/fR2X19laGlkeHFrb3jg5OXi3d3r+33+b2tgXmJ1fvH269vh5ur9fXl+dWhpdfv1
+83P97/1xaWxsaXh2aWz7+vnx+fH3fnhsevXr4d3g39vkd11ZYmptfPzq2dXZ4nNpZltUUVRb
+bX/v4Nnc/F5UVF1rcv7g1c/S2+j+fG9pY11f8NnX19fc6vJwWlNZYl5ke+/f0s/X4v5tYV5e
+XWBy6+Xo72taVFBPUFJd8eDb1dPW3uj8bnhvd+/j2NLS1dne4X9dXFxdaW/53NPY7WJVT1Zk
+dn196t/e6FRFQ0z+z83Y3uHd33dXTE5d6ODtbmzj2d/d1tfPytHpbV1ifPN99evk3NbZfkg6
+PUFP99/k3NLTzddqT05Td97t/Xzm2NbzXl1i5dXb5/R839LeZlxe8czFxtJdSkE6PD9N7cjB
+wMf2bFhMTVBXftLMytDvXlVY+djOztv0cm/0293sb1tWX3Ju9trX29PiQzAwNlDDu7rD12Xv
+/lxNS17cx8fQaFBY3crGzW9WVmTf0drxb9zJvru+zUUqICQt5K+qrb/2TVNQST4/X8Oys8hK
+OTpK3szR2OXWztLhaWvaxsTLaERJ4rqvte8qHh8q2a6ssMTuY/RPPzI1Tr+sq7hZNTI+e8LG
+1N/Xx8LH4Ftc2svGxMjM21A6Jh8jL72npKzNPjpHXOhaVn3Gt7jKSDY3TsW6vutDP0/RvrzA
+yMXBwsvN20MxIhsmO7KfprJMMTNK+9TrUt/Ft7jZQzY6/L+4vmdAP1bKtrK0ub/R5eLLz0Yr
+GRckSqCbo8AtJy9iw75xWNi/sL1cOTFDwbGyzDguMla2qqestb/hTFDyTjkeFR0yqpabqjQe
+IDbGr7tabOq4sto8LDHZraiyTSsrPbmmpKi2zllS8MzOPh8QEyS/lJSd9B8dKfe3tmFezLas
+wjwrKlmtpqp4LCgyzaiipKzCaUVdwdI9GwwTJ6uNkZxKGhkr4K+yRPbPsKvuLSIrup+fry4f
+IUKpnJymu01DTr68OBsLDyytjJCfPRoZLruvqz9UyrmudSYjL7yanq4uHB88q5uZoK1MMTV2
+wPAtDwwdWpSOnM8eGSW8r6fLOmlow2EsLFysmJ7IJxkeOqublpymzDEvNz4rIBUWLrCXlqU+
+IB4uvq6rt83SSzQoIjutmpaoNx0aJ/SpnZueqNc9SD02KBoSGkKnlZqvNyMmN//LuLmstU8r
+HB9Un5OWtCkaGy/JqJyanafPSDkwLB4PECevkJGkOx4eLtHWyMa0qbJCHhsnrpWToT0eHSpS
+sqWdm6O6XTUtJBsUDyaslo+gRyclLVJPWLmtqa87IiMss5ybpVkoJTJWtqyooqqvuT0gHRYN
+GU6hkJiySy4qOzk3zbWqptgsKCVIqJ+eqUcoKC+9pqSgq7zCSCAcGQ8WRKmXl6vKTS4sKyrc
+p6Wr6SstNTy8qaOgszgrK0qrop6esd82HBoWDhdXp5iar7i+Oy4kIE+pp6vIND86NM6vpqK1
+Qjk88rKqoJ2nzi4dGhQNEzaklZemr7ZJLCEeNLq2u77Hw9g5P7+uq7VvT+LTvauhnaJmJyId
+EAwUK62anJ2bpdYrHB40UE3auLCuxF/Atb/RV1vGv8m5ppydsDsqIxUMDxo5raOdlZek0ywf
+JCknLny3r6+6tquwyN5VR0pT26+fnJyoTSYZDgwTGi29p5aOk5urRigfGxkeLD+6paGdnqiu
+vE06NDAzT66enaS6LRgUExIZJD2wnZqWlZynvzwsJyEdIC9fuayvqqCosb88LSooN+LCsq61
+wOlJPTw+QEZOVF3q1tTybWtaTD01OEBO8ci3rauwxFw/OjpC57ywrK+5yGhLQT5AQ0FAPTo9
+SGDU0e1YSERFQT5H/sCzr7K6wdlUSE7hxsC9v8viT0NEUdrHyuhLP0U/M0XWXFBHPEtOSExH
+wLHHvLG8U0dAXLO8yKmqt7fEXjsuKS8vISIpLDlbwKWdoqWjrshqPC0rJyMqMDZP0L+upp+b
+nKClrcVEKx8aGRoZHCUvTLupnJeZnJ+v4UkrHh4eHicyWLKrqZ+dnp6fo6zLRjcpHBgcHRgc
+MVFdv6WcnaCeo7xcMyglHiAsLzvBsKqhnp2dn6GluGJKLBsZHRoWGSg4Nuaon6Gdmp+ut9k3
+KCImJSIvXd68qaOfnpyeo6q0zTolHh0ZExceHidUu6+jm5mcnZ6u1E8vJSMgJCctUci4p6Cf
+nZ+ipbPN8S4jIBoZFxgeIylfua2enJqYnaKp3EQ1JyYlJy44XL24q6Sno6OssL1LOysfHhsa
+HB0kMES/q6aenJ2dpK66ZTwxKywtMkt8zrOsq6mqrLtvcTkmIiEeHR8mLDX4vLSooaKfoKat
+udZEMi4uLTdR98Oxr6qqq6/KZkAsJCEfHiAlKy9Dz76yqaWlpKKor7PHUj00LiwyO0nZuLGs
+qaqtuNN0OComIB8eISYrNlPYu66qp6SipKmsss5dQjItLTI4Q9y/t6ypra65y2E1LSYhHx8i
+Ji09a8q2raelpaWlrK645lI9My8vNT1G1by1rKqusL/wTjIrJSAfHyEnLT32xLGqpaOio6Wr
+s7vnSzsxLS40O0rpwLivqq2vss5fRywnIh8dHyMpMk7FuKmhoJ+eoqevvNY9NC4pKiwxPV7N
+t66qqKmrsMRPPishHx4cHiQqOHG6rKSenZ2eo6y02UM0KCcmJSszROW9r6qnoqOpq7ZbOC4h
+HBscGx4oNUy/qaGem5udoai47josJSAgIyYtRNS8rKOhoJ6fp6+9UCwlHxkYGhsdKDv4sqSc
+m5mZnaKqwz0tJRwcHR4lMk3BrKSenJycnqWqu0M1JhoaGBUZHSMveLumnZuXmJyfqr5LKhwe
+GxYeKSxJr6mim5qbnJ+jr8brOCckHBkcFxwlJTHFr66dmpyanaW44TkhGxwcGyQ6Xbuqn52e
+nZ2hq667UkQ6LCUfHx8dGyU4LUatpaefm5+krcRPMyIcIiMfL9i7raShoKKpq6y+zb/4R2E/
+LykiIyAcHiszOcapo6CfnqKuzkk4Jx4gLC0y0a+qp6emp7DBuLv628TfWN38NSIkKRwZIzI2
+TrimnZ6loKG5PDkzIx8nMj5pwa+mqrOvr77QwsjDuL/Qyr9JIx0jHxgaJjvSt6qblpujpq15
+LyUgIiMjMsu9uKuoqqy74s7H2tvCtq+5x7vPLRwbHRoZHi7HqKKdl5adqb9DLSQcGiAnMGu8
+rqWkqauvxdrT1czBt7G0vcfoMhwYHBsbIC/XpZycmpedqcM0IyAeGhwoNdCtqaahpKuyx09Y
+7OfOv7evr7zIXy0cGRsbHiU1y6Sbm5uco6zZLB8dHBwjK0K0pJ+fo6mttutBPkNuzMG9trW9
+vu0uHRscHSAoNtCmnZubn6iy3DMiHBwfKjRls6Wfn6SsuM9UPDk8TOK/sa2wuLvjMiAbGhwg
+KDjVraCcm56ouF4yJh8eHyk53a+moqSorrnZRDg5QF7KuK6rq620wEsmGhgXGR8qPMCnnZqZ
+nqi4UC4mIh8hKTJqsKekpKevuslpRz09UNO8s7C0t7jATyodGRgaICs9yaugm5mcoKzPOSok
+ISAgJzber6aipqyxt7zG3WZd6szBvLq7wNVILB4bGBkeJzfPraOdm5udpbVXMCcfHh4jLUu+
+q6SipKirsLrH2fRWSlBs0cC7xG00JB4bGhwjLUO/rKCbmZufqLtOMCQeGxwgLVS4qaKgn6Ko
+rbK82E46MjQ8WMi8u8NaLyIeHR4jKjRTwK6jnZucn6nBPiceHBwiLk6/raaioKOmq7PDcT8z
+Ly82Q2HRwb/Gx8DDTCwhGxoeJzzOtK2opaWjpq28TjApJiozT9e/uLm1sK2ttL/Vb1xaVEpD
+Pjo7P0xaU09Zb+nUy8bCzl5EOC8vMTU/Tlvq0s2/uLW2ubzD0uvq3NHLy8nGy9DiVklGRURH
+SUpPa9zNyc3e2dza32NSRD5ATFROVmVaaHt3ZlpffON5dvdsbeLS0tzm2urrzcTDzdnl39XQ
+y8TF221qTUpZX01GTlFZZV5fYXXzdm9damtga+NqZGpXVk1MYfxfYuz04uTYz9HR08HI2tr6
+7mFYWtvx7tbc1dRaXObebE9k7uFVTV/cfklgcUxSZlVNTl/t1szNx87Y4fHoX1NGTXdsdFd5
+1s/O1srI4fdoX1pbUU1iXm101tV80M306mlbYOhXTvjh52bY3n5udXzsWVfs43ln2+be8+Zm
++OpOUe/xVlNe+P7v8Hn3/u3q9u75Zt/c6tzb/dzb3F/7zs7x+t5cVV1579/b71pcSUFKVlFQ
+ZHr35e3e1OPf6u1WYetv+/fW1dfVz9vZ0vhbb2Vldmzo197g1tjZYVNWWE5HTUpITvfS2NPY
+0eNbc+BlT11bWWrs3tzOzdPR5Xj9c2vubm/vYOrX4GpvXFx5dW9aXl5UXnx94s/T+Ord/P39
+7+9rYlxfWmrudu3199jX6OPf5vrocm31bV9fX1NPX/Dp7ODe9fXp5/leZ21sZWpkafTz7eHf
+4ePi5ujp6/z83uB5duvxbWRsX19eZGZsa2N55/Lx4uDnb/59Z2Vp9WhkfXNz7uLf4Nflfvt8
+9W/05fZrZ2Nvamr3/Pvs2ef3fvZ8aWxtZV1odX/59/798efpdHh+eGtn+9/n7ODi7O9vdf1f
+Z3F6dmlfbHp66evy+HFy/HRoYWdhY/fo8O/p7vF74d17/Ofk6+78eW1vcmtsaXn8d/f26elt
+anJ0dGt6cHJp+u118PPzfe/x++ttYm3w+3n8+vr1+f5yeXtqcPTycXn8bffm+ejkfm94+/tq
+afD4e93d933y/n97ZP3f9GFr7u9tZ+rrfnVtaW5oa2r6e3t3fvz7dHX0afnq+23y7lxr43//
+3d/e5dzm4O5s8uR+ZO5qbvJe+25fXfJmXWtlXVzr3+x5cnPmZ+LSaWdvd+za4PPt6upueeHj
+Z3t4a+j4dexoWuTga11YV2/feGVw6F1X291mVO7P7FnSwF5L6c75W1hs7F5Z7dz7X2/Y/uxo
+4tlcWXbecHTu4njxcXd1blhRWHPbdv/j+encdvXR3GPj32pjV2xocvv95vtfY2leV2rb6O76
+9f5w4u7p/Xjw5/BZWl/o6mV55eNnW/rW7Pre0t/l+W/5Ylhg7mJQXmX44XH71dpnWuPiZ1hp
+6PteYNnYb3N5fvZt8+x8d3Jo9t3o+Nzd3/lcaffvc/f7aHX5cXNu8ejv5/NgaHxlX+92ZmBb
++WVx5e7edfzj6+Hs3djn3eLo4G34YFxtaXNYbV5VYVpnXF9zWV92bXr03tLWz9XW09bHy8jK
+2dDb29HOyNNdOiwpJSYtPM+5raqutctfRDw+QE/uy76+u73GyM/Ny8S8u7u9ye08KyIeHyQv
+VbysqKitvXM+NjAxOEZ7x7q1tbm/yc3LwLi1sbK2vu48KR8cHSIuV7ippaivwFQ7NjU4OkBP
+6Me+uLe6vcPGvrm0sbG0ushTMyYeHR8nN9yzqqmtvPBDOjg5OTk+TG/KvLSxtrvBw723sa+v
+srfCWzIlHhweJznataytts5ZQj0+REc9P1Hsxbmxsba7wcC+ubSys7S5v/I4Jx4bHCM15LWv
+r7vZXk9YYFVJOzhBbcSzrKywusTNy8G6s7OytLrNPScdGhsiM/i6srfGf1pt1s3qSjgyO+y7
+rquttMHP08u/urWxs7S0vF0sHhkaHixXxLu/zdvRwbu//DwwLjrPtK2tsbvHzsvJyL+6tbKx
+r7DENh8ZGR0oPe/U3dPBuLO0vlY0LjNEzbOxtri5u7/Hz9/XvrWvrrCxt2sqHRkbHyk5V+DK
+ua+vtcVgPDM6T+HDury+uLa5xdrn+sy3rqyusbO8SiYcGhsfKTtrz7+zrrG7y11BPEdv/OLM
+y8G0r7XKb/nYv7Gtr7W2tLtLKBwZGx8oOVzQva+rrbrOfEw/QUlDQvK9sKyut9RQbMy+t7O0
+urasrNMnGRcbHyc3WtK7q6SputlkSz08Pzw6Z7WrrLbGZUhlw7y+vrmysayqvysZFxwiJi5L
+zLmso6W4fmJjRzk6OzhPuKqtwuX6af/JvsPKva+trq21QB8YGh8lKTzRuq+opa3HbWJLOTY6
+PEHbsq660NHN08/EwMnCtq2ur7PSLBwaHiQmLUzHua6oqLPR5+RNOTU4NTjqta+4vr7G0MzE
+w8e+s7G3t7TcKx4cHyQnM1/NwbSpqLG/yuBFNTEyMTjqsqyyv8zOz8m9v87Nu7Kxr7DZLB8e
+IyYnLTtP0rOmpa25xudFNTEvLTFZuK60vcHHxLu3wt3Wv7i2trjOPCsoJygnKS47Z7utq660
+u8ppQTgxLzJA0r28v8K/ura1vdLd1snHxb++z0s3Li0sLjA2Pk3uyL27vMDO7ltQT1dhaO/x
+bGjxzsPCx87c08jBvbu7v85kS0VEQTs4NjY3OT9KUlVd9dLIwb/Ez9zpbl9j79bW3NvUysa/
+u73AwsDCzd7oXkY6NTEuLjI3Oj1EW93GvLu+ydXe4e/y5tza4t7XzsnIx8fGwbu3t8DQ3mdN
+QTgwLSssLjE5RF7XycHAv8PJz9PS1Nnm6trW0czT1M7Hvru3trm+zellUkQ4LisoKSswOkNQ
+6crBvbm5vsbMz9Tk/Wr66uDV0M/MxL68u7m5ub7a/1Q+Ny4sKSgrLzlFVd/Ivrm5ur/M3O7p
+5On2ZW7l3dXW1MzGv7y5tra2uM5LQTsvKissLC42QlnnzL+8vL/Fydbtbmjz+Hft6+7239zd
+zL+6t7a0tLS3y0QyKyoqKSsuN0/QxMbHw768w+NTSkxXU1bz3Nzc0c3OysK8t7OytbW3vLvF
+OSMhJigkIy1OvbW4t7GvueRFPURKQT5Py8C+w9PIvtJRacG3ubq6s661xsLMMCIkIiAkKDXI
+srm6ra/F5FdIRkE3Pt3HyMS4tMdhTlftZ2jGsa2ys66uuL3OLiAgHR4lKji9rbe2rbjcXkVC
+Tkc6UsDGw7y6uM9BP1VPQ/24rq+ysa+wvM/KSyUgIx8mLDPbrrPGtbTVWEZHZl5BRca908fA
+ycpMMD51Ul2+r6uqsrevtszQyTwmLSQbKDUxZbS5vLLH5chSPWfrREnf2cvFzdrP5zo/b23e
+v7GrqrS6tLrL3eJlLycnHyc1M0O9sL3Cwc/USjpRfElFXNW+xNbCyGpCO1Td6NO1q6ywvLy7
+yOx6wMIvKjUkIzEuNb6267+8bvNTOk7gRD/dx8C/vbjcT1E9P2XWxLGtrqu4zb29296/vzoo
+KB8hKyk5vrbKw8Pf108/bt1fZNa+ucXMvtFBNTdFddbArKeqsL3ExdnhzM7PPykpHx0uNy37
+rb7Jv8zCzDxEv85GYcW+xnXV0DouSWhRxrGqqbC4uslc5768xN5JPi4bGy40Lke/rq7XTLy7
+Qjxa39RPR8K27FjIeDk7T9q7trOsrbbDz9DMyMS9v+I8LCQaHCw0Pti2ra/XWsXjQUBZzMTy
+aMK50Gja50s1Nce0w7aoqrfdVsjI5MW3u10vJx8XHjI6bLWsrLVpV89YQVnezMfracm+03Ld
++0g7QXe+sbGvrrfRc+3Xx767vVYwLyATGjpMT7+poqxCOcfLOzn2tLLvRsq5+Uxsbmc6M9+5
+urGsr7bNVNfL1bqwvnI6LSMUFjRiR8Wno6pZLlzHPzfTr67HQ1+9cDlO3XVJO1K2t7utrLXK
+VE7+dcyur9dHOC0fFBk7weC/pZ+tQS1G1kE31quqykBQ1k87Q9fA7T9Mu7LAuK2uwEs9WNxs
+u6u20TssIhQULcnBs6SfqE8nMVVERMisqLhFOFVZPULcu8VPS8a4vLu2s75XPEFOZb2qrL5P
+LiseERlisLuuo6KsNCA41UZFu6inwDc6d0cxPM68zlD/vLbCyrmywUg+Ze15v62suzwrKxsT
+JOzOsaSjpLYuLUY5MlG7ra7OTGdLLy473b7LyrKwwMzOv7jTSuHaTFnFsa2/e9ktEhQnP0XK
+ppmcyzVFPSckNryrtsi8vGw0LkPbWlu8sLnDv7Wz0FHZ2EE517S4zN2/vjsaESC7NSepmJ+2
+TErELxssvsTU1bmovi890U47S8GssMu4rb9Z/MzRQzjXulo52L1o6PknHTk2IzHLs629zayx
+PzpvYz4uOcLEZNOyrbrhzbvMWW7T09jPxsxfSVPpUDpUzE078cXLyk0zTlApL0lKb+pXy7xq
+Sv/QxM3XvsJ+cXtkaX7LvMfZxL3F2mnx4VQ/SfLiX1jx1N5PQ0lRR0BR5dXV0tDsTT8/RERS
+3szH1eJuTUdEX9LExMq+v9vle1re1GdVxsLK3cu/4U1TUUdDP1zR3tm8vdxQSD02Mjx0Z+i/
+xe5LNzY/P1nFu7W40NP1TOvL0sjGx8dt9cHE1MvG3URDTj5CYMrHf1f0VjlDTFN2fG3C3T9R
+Ois+2FZiuq6/UtvIaklbvLfNyrzLbWzdzt3e12VERERa5GrFyU5PPzhIXX/M5WFdWTw5RfZi
+c8DCcOzHd0/fxtDQw73G3dLH3UdQWkVET+rOc+rA1k9x2XFVT+FgQE5LREdDSebbcuvZyNVD
+679hYsPI49jLv8v74stiRmTOV1DP0ubw/vxUUONaTVFNWEg6U8vzW83ZPDx/Qz/IxsO45fDE
+XEzPwsPCycPSTVH1WH7W0spiSF5uU3zh08tOTVxANz1NQj3ev9tHa8JLOd3BaPG7xtjb08rU
+1L/J2MnhV1h71OhkzcpMP1tYP0/GxvhUe2A/Rkw+Rz063M3x7M7I+D9I4dPuybe3ysvD1Wfz
+z83Xd/LL2Gbi/URc7UpBf85UUFvnTT5zfDk/WGZ2QDtaXkncwcK7xs/P8GHlzL7Axb7D9G/d
+3+HZ3kZA2ts/arXaNkrSWTIv/Mk5S7vOOjBBSzdEtK/KybtvPT7ZwdC3rLrh0sLMYl/gXT49
+TUlatbLJxH08NS4350xJyVYyO09FRtC3xOjL1VNV2L+9wLm7ytrSztLfWeXQQT9oS03Ry8nh
+PDU7LjNM8n5NWt1CRszJxra+xtNW+8vHt7S4v9vyW0LRuVJFzOM7MUe7SSlG1DQrPHY+OeDO
+4sK6u8HRzdhY3ru6vbS0vszX7PlYQlTe1P5CQlQ5Jyo5PTk/bMrNT++6v9W7s7zf3r/A1May
+tdHx1dtdRVu7utM6JR4lKSIwta/v17nKPTvStcDFrKu+dcOxu8u4udZb6PZCabe9LBYlORoZ
++aarzcKpxSQsvrzovqWl3E61sM3QuLbZQWjB0M+x2hkZLB8ZLqqeq8KtuyojNOm/xq6fr1bM
+tsZk6Li3ZkHGr7/FuSoXHR4cJ9qhm6q1uTcdJDjwuK2kpb7oxNJa7b23xuPFr6+0uR4UJh8W
+Ka+dm62yrC4VHUJq76+bnctGvb5ANtitvErJpKetvh4YJBkVMa2dmaesqTEVFiVkzsadmKxZ
+YexPMDa0rM7Nq6GhvxwTICEYJqqVlqavqkEVEBxEw76jl5/ZQV9NLCraq7DKr52dsB8NFywa
+Ga+Pj566rboZDRU3u8qtlpm8R1pOKx82q6vArJybpiULECsdFtKPjJi8sKsfDBAqt7rFmpKv
+OUxNMiIosKC6tp6aoDgODiUfFC6XjJOrtKkuDg4d3LHIo5KeUDxBNSYh35+quKOanV4UDhsh
+GCKijY+frqlvFQ0YNL+6rZiXtzw+MiUiM62ir6mcnLIeDxUeGh3PlY2Yp6a1IxASIUvDsZ+X
+nsk5MSgjK/Cqp6ignag3GBMXGx4sqpKSnKCpTx0RFClfwqqcmJ/MLysoJi/YqaKjoqGwLhgT
+FxsgNaeUk5mfr0ofExQkTtCvnJaezS4rLCcp5qSfo6OfrDsaEBUcHyqylpGWn6vCKBUTGytK
+wqCVmbFLNzAoIjaso6qknKDMJRUTGRocQZ+VlJidp3MgGBYXIEW4oJqdqLhKLywtP76uqJ+f
+r9ctFxQZHCVFsZ2WmqOouDwiGBkqRj3NpaGovGbJyjg6uKemrLjB1y4ZFyIuMTjgq6GnsLW6
+2jwuLjlo8kZVytNlX+nFvsCxpaaxx1Q3LisnKTlt6lFTyrvSQjxiw8XQwLm+0D8uOVA/Qdm6
+rq+5sa/PSVhcXe19T0Y/PDQtLTI5RWnKsqmrtb9hPz4zLkTJysW4ubvB28ayt72ys81XNCIe
+ISIhKku3qqipqKe1SjQvKSw7R2y0rLfBvL3Cw7+vp6y8v888KR0YHikrLlytpquxt7e6Vy8z
+PzY4W+nfv7vDuq+ztrOzsbPB9WlENTAlHyw8Mis6xbi/vbOtq7lUQ0cvJCo7T8eup6Kkr73B
+2Ug9SW3Sx8e93SshKCYdHS7MtK+ooJ6lwTouLykfK8mvrqmhorBsQkJEQkXQrqmxxb3WJhoe
+Hx0eK+6xqaWjpKWv+zo5Py4lNb61vrqtrMBl68vLeHvBuLvEzMToKBseIx4bIk6zq6Wempui
+tl05MyoeIES5wsyrn6rbWNXVRTlZvrq9vrm+Nx0bICAcHzy0qqaempqgrss/LCYjHyQ/v8PO
+rqGp122/xEw/2Lm9zcS7w0YmHSEpJR8qxaamq6Odn6/kSjsuKiclLErfXO6yp63Dvq+3fEzY
+yl1DWca+y1o6NDk0Jh8pWcHOxamdn66/w+wyJCAiJjFYyLSnoqm5w8ZvNzd0xdTPr6OltNpM
+LR8dHh0eKlfCvKyem6GutLd3LSMlKSkrO+i+sq2srq+yv+Pnysp77Lqus8PcYTchGx4hHx8t
+x6yrqJ6anq69yFAtIB8jKTBF1Lmsp6etsrG67kFCX/JSUsmysMLs9UcnGx0lJyIr06moq6Kc
+navC0GYzJSMoLCw0XMC2r6yrrLS+zv9WRz9CUenEuLO2vc1QLyIfIiQjJ0K1qqqnn5+mt8bV
+SC4oKSoqLDlS4MOzrKurqq++3v13Sjs+UPTRwrm2vuRBLSMfICAjK024qqSem5yirsZKLiEe
+HiAlLk3EsaegoKWrrrjkQjo4NzpJbtnEubnLYEk6LCMjKS8yOX61q6qqqKisuNlNPDIsKSku
+O1bYva2mpKirrbjqPTMuLS0xPFfGsq6ztLLHOSgnKicgJDnPvbmsop+kq7G/azwwKiUlKzlL
+ZsSup6esr7C55jwzMzMyNkTZuq+sq62zw0wtIyEjIiAnP8W4tKmenqWvucJULygmJyktN1XF
+s6upqqyvtclNOTY3ODlDbMm5sq+usLvYPislJCQiIipEzb2zp5+fpKuvvF0yKSYkJCk0Sdq4
+q6eoqamtv1g+OjYvM0Fgzbuwr7O2vOk3KSYoJSAiLlvKwbKln6GprK67WDMrKScmKjJA+MCx
+q6ioqa6802RGOTY8SFP6ybm2vcjN8jsrKSsqJSUyYdLOuaihpq2tr71QNjAtKSktNkBsu66r
+rKqprr/a61lAODpASWjVzcq/vdc+MTMyKSImM0ZKZbqppqmqqay1zk46LysqKy84S824r6up
+qay0v9ZUQjw5NzxO7tXS1dLWSzMvMzEqKTh12tm7qqarr66uvGtDOzMtLC0zP3XHvLKrqay0
+usDnRz4+PT9HYu7w0MbiRT08Ni0qLThASu68r62trKyvucxeQjgxLS01Pk7lv7Kurq+ytr3O
+/lZKQ0RRaGFe39hNODU5Ni4rMUFMTfm8r6+wrqyvvNdbSDkvLjM4PlDYvLOvrq6vtMDWeFJB
+Oz9OXl5gfNjzPDI6OywpNktBRcu1tLSurK+4wtdlPzMyMzM5R3PIu7SvrayuuMLSYUQ6NThA
+Pz1NzMTySUXw2DcqOUw7Nz/qv8PMu66vvcq+wV49Ok5iOznXucnfva6zysm+yk89SFc9OEtu
+U2/Iw8viSEBSPCwuPzwvN2/M2c67r666wbm601Vv2GJETu57YPLHvMjZvb1fX81bSVxEOkvw
+QD7ZzlXawltD7lE1Oz82OkQ+S8/Rz7mwt7u1tb3U1M1PTk5LSO9cV8zMccq76ujR3Ul9S0VK
+UkRNbPTp8Orf/VNPVkg/RUJLT2Ff1svDx8S+x8jJ3OV7SnNrZl9izNLS3Lve0ddfb19TP9xD
+Xj/YTt5O8exd6UjaRtpJWnXubVa/9szdztXV527lTvlLalB152Pa39DT2tVn2tlWYON4Wmnm
+d2neUdBd51Zs4EbeRs9J6Fnd31vJbsVd0+/zamBs6lb0Ud9Mz1DZV9xw5PVWyUfJRb5DvUy+
+T8pO5XBX4D7CPcU4u0LCWtPZ7sdJv0zFQs5PX1Rd2UnTUc9W3Fjn9VbjV/JZ5GPp6vrf2vPl
+/Pps5W18W3tfeGL43O7V8MhmymjSWOZmWOFO90zZWv5a6Pz8YO9tfG1t43DkX+b98X5l/l5z
++Gxd4W9zZO162vbf0NrV69Jz31TqXW1O8VbgXfpi5uls5nPr8WR8etxT0F3SZv/tZtlS3FPW
+U19pW2Rj8Gjid9r00/vX6+P1fHtdZl5kZuPp6efw2Ovo4ujna/tgc11mam7xauhy3lTrXWpf
+XWBrbWD77uzn19fQ5d3641xtX2N4Zvl58mHyZdl+6Ofo92j3XHZrbnXtb9710e7abdlf615g
+W15fVuBV3m/VXthn7WzsdGb3XOVW5mfcZ9zu2e7j6OjfdOxvcGb2YX7qZeNm5VjoVORRZXFf
+72Dcbdl35N/03Ojd/e5mXXtT3Vntc9xi6PbaeOBq32TqaHLvdW7u+/l293j9XHpl62HyZeh7
+duxu317uaud2Zf5q6W55dnp88v777e/83/bf9//u5/75/+dvbPT1aHtma2Vuant8e2d0+9ze
+5uf15HRp4HNuXPlsfWx+aOld/1/9efrr+Hf6Zu5833z37nTf8dd75Hh3dmvxYOhi4V/mbPf5
+c3Rs+FziXepg7XRt3fDke+X+33PmeO9cfmZudfJg21/kW9dU11DTTNJI00zSUttjz3jZ5uTd
+/d5h1mDbTtVM3VLcVuRbbett6mDcWe1nd13edHF24/Pp3OPf8OlwZvdo6lpzZut5/9jy7fdv
+f2n4WWdmX2Jpc+3jY+je4X7r9e3fbelv4Wp3YvhvX21u22T4ZNLp3vvd53Z5buZwaFv9cHVs
+6+3i9fhp3u9oVFtcW1RUWnZtX37d1Nfa1c7Z5+7ibWpaX3FuXmT76ebd2tPV3tvm6vxlUVJK
+SUdHTVp2e9fUy8/R1tHi6GliXFtRV1led+nb19TQ19zY2up9eGdfV1ddaWFhZnRu+P3pfmtj
+aWt+bXLzf3Zu+Wxwb/D27PLd4Orn29fP0djp5/FkUVFUVE5NWPrs2dXP1d36d3puUk5YWVZa
+fuLZ3tjY2uzo+n1eXV5jY27x+OXd1dfQ2NrscmNdXWdqaHhvc35qbWpqY15iXFpcV1d+3+je
+0tja3ejf0+b1+PXv/Gz79Gtr7+jta2Rsd2RebvXvfm/96/5oYnX9al1offH28/jr83D029t+
+cPHm7nx1+HV9e+z1+WxqdfDy9/Xp83N38PV2b3lxbW5qZW/3e3fx9/zt4+DofXj483Zva2Fh
+YHDu6/bu5+nu8Pn8+XP//m9se/n6/uvg7e7t7vTy9nd3eG1mY2Bobm5senB28O/2+3p7+Pnz
+6+jh/Gf/7fj87/V7dHvw7n1md+/vfPj2/WRfanVvbfPnfG/9fXxvenx8dX7s/vrv8PPt6u/r
+8ff3fe7l6fD5/G9mYm9mYWFpdu3t+vh+enR3al9eZWdv6tva3+bm8Xp5+PZ2bu7o5vHr5PB4
+a21ra2FjbG5t79/i9XR6cGFs7Wxh7OV6e3j99Wdh7ON58OXv+2946/BsaXZ7bmt89XVrd/N+
+9unk7f5rb/H3/vT4d33x+Xz19XZubG95eHdtcXlybnNvb2ps7t/h5t7ob217e2prePHv/PPh
+4v7y+mtgX2t8cGX03XZd5dh5Yv7o5X504+VoXnFtVU9ldG733dHZ7urg62RhaGZbXOfa+X7V
+3Wnn3Px0c2huYlzu2+pye+X3aH7k/15oe2xaVXDqZF7s43r93934a/jjfmN36O/15t7fdG7u
+4+/z4N/w7uZ4TkptW0FQ2Of+08zP52rm21hMaGhNT/nfff3Y0ed629l5buLd9eXR1ezr09Zz
+5+JGQFA/OlJ8XuDNy8XN9eVvSElVVldw283N08zN6mJva1Nb5evuz8zU2d/qb1dWXFpcbOr9
+++H6e1NGX1c8TNtgXs3IzNLe1d9VUHVeTGrb5NrR1NLaddrhUF7gbGLe2eTb59zOcGLR7VZT
+SEA/Pz5MbP7Yx8PJztPkbVhOVF1gbNzQ1M7Q3upvXF9mY+bT3NvP1uXi8G5pYfzc5uLaVkdB
+O0BBP1vQ3tG/xs7V6GtXTlNcW2bl19vY0Nni7G37+W7b0d/Z2uvkflzi0V/myN1lVkw8Nj1A
+QlLUy8nCw8nTd11VSktXZl/o1NbS3dnbXW7hbG3W0dfP0czaZdLMY9/I9FtNPzg0NzxFU9nD
+v8LGy9txW1FJSVr259fJyNbs9XFaT/HQ4tvCwtLMxt5t6urxYlxbSD02NDs8PF3Gysa5u8nU
+8lVJQUZPVnbPysvIz9fcXF/b7eHFy8/Dxc7O3OjSZE9wRzgyLzY3Nk/Kz8S2ucTI12lOQERL
+SlHYztnIx9rX7Vby23HRwMXEw8bEzeXVz2VIRkIyKjI5Mz/PwcO6trvMeV5QPz5MTE7vy8rO
+zdbd/Vz84dXJw76+xMTD0ObPzmpQTEc2LCw0ODNNvsDGtrTF3XZURTxEV01ZzMfbzsfX6nTo
+3+7OwMLAv8jHy/HZ1WZ4X0A+OisqNzc1Z7q9vbK0wt9kWkM7Q09IVczL2dLO12Fb2+L9xb3D
+vrzBxM7j2e3oakFESC4oNTkvPsW8v7evts30d0k4O0ZFSOfGztPIxOxS7d5d7b/AyL64vc7M
+ze9qbURESDMsLjo3Nme5v8Kwssjd7FBBPEBIR1POx9zSvs1LcM9gU8y8w8a5tr/Lzs/6VkxD
+QDotKzk4Mk+8wMO1tb/Q6WVKPUBMSEjsy9HUy8bZW+bQe+zDwcO9vr7D2NzPX0dKQTgtLTMz
+NUzMw7qxsrnF22ZKP0NFQ0lk4trY2s7WWGLa+/zHw8G9u7zAzdzW6WBSR0c5LS04NzhfxsC9
+t7W+3O5mQjxAQ0JN7s/W3c/L31ni0OnUv73AvLu8xt3Mz1RLSTs1LSgwOjJGvLy/sa+6yOBh
+Sjs9RUJH+M/U1czK6V565Pjjyr/Av7q6xNDM2WZTRUdCLyowNzM6zrm/u6+zyNfrTj07QUZE
+T9vQ4NbI0mJc3dxq2Ly9wbm3vs7a1uxVT0Y/Oy8rLzs4Q8i4u7izucpzWUw/PEFIR1nX1eva
+x9lW38zu4MLAxL67ucLa0dFXTFFBPDIrLjY2PtO+u7a0uL7XaE8/PEBDRljf1ejhxdNMdc5y
+7cO/v726uMDYztFRTVJCOTgvKjI9PUrAtbi2s7nSXVBNQT1HT01lzc5+3slfS/7s8NnIu7m+
+u7XG6NBvTVZKPjw1LS42O0nnv7e4ubnA311NREJARU9Tec/c7tHPWlTf3d3Jvbu8vLm9493I
+blFPOz88Kio0OTlevrS1ubS52VhXST8/RE9PWs/Mbe3Te0xizs7Txra4v7u6zXvX6VZPQD0+
+LiovOz5Gy7O0vbi3y1hOTEY+QFhiVuTL4vXN401f49rNxbq1vL62wHLe111TQjg/NigpOEI9
+X7euuLy4vftGSU9DO0zta2rOzfxg5vVKX83Exb23t7u8vtZ++V9bTTw/PCwoLkVAQ8Cttr+5
+us1MP05WPkH77FrlzeFbWvFRT+DJwr+6uLi+xszc4OtcYVQ7OzwrKzo8PtG7ubO8wMF7QEpO
+RElOZej12s5pW+JrT3rLwsHCurXAysHVZX5nf2pEPDsvJys8RE/Esq+1vsLIWT5FTUpKWdze
+79bqXepYRGXS0MO5uLm9v7/YVePdVWr9Rzs4LCguOUjXu7CutcXM20xAQ0pMS1Xi29bSblpd
+WklP1cG+vri0u8XGz2df8e3ba0E9OSwkLktN7LqtrrrZ09ZANkBVU01c1MrV7PftZElJ2sK/
+vrm0ucnNwdtSZ+rvakw/Oy8mJTNLVM+yq7C/2tPvQDxMa15Z4sXE0XRaX1lFSdnAv723tr7U
+1s9lTWvPzk9DTjgoJC4/RmS8q626ycrWSDlBXFxRYs/Hzdf+VVJKQVLGvLy7uLnH7NrRaVz6
+18zkTD02LCQpP13Wua+uttlbfks9Q1N45e7QwsngXkxKSkhbyrq0trq9yPxr+lln0s3N6kpB
+NykkKj/x1bysqrjbW2JROzlO9mhc3cTH31VFSlFCWb6zsrW5vMhiatj+Zd7Ov8dUPTcrIiM0
+9tTBr6qw2UZPWTw2RuLO2+DEvMpjSEFET1PSsq+0u8TO/E9a/33VyMjR4Us0LSUiLlPbvq6r
+r8NSTVA9OkNi0s/TwLzOcUM5RUlI2revsbrCxm1IW/t+0MLBvsVONi4nIShA7L6xra25XkFG
+Pzo8S9fL28u+xdxPOTtJTei4rayywszZT0pg7NjHwr7BXDUvKSAlO2fGtq6rtO9HST85O0nX
+xs3FvMTaUjg7SUj7vbKutMbK1U5NeufQx8O7wO48MiohIjFsyLatqrHZRUJBOjlF38jNzb7A
+6009PEFNcb6vrrG7yO5SSlrm3ci/ur5pOTMuIx8u4tDCsqmqxT5BWD02Ptu7xNnCus1NODdN
+SkjLsayyyNDNWT9M2MrKy7y3yUMyMCYfKUzKuLCsq7tNQEZAPDxWw8PSyMPQZj02PkNM47uu
+rbnK0nxLTejPxry7v8xaNi0nISo8Wr+vray4cElEOjk+T8/N0MO/z2xJPT8/UMq5sa+1vspp
+Tljr39rCvMHNajktKSAkOFLRta2rseVLSz85PEzRxtnIvcntUkBBQ0jevbKvtLzMfFJQaOHP
+w7/BxN8+NCsiJC8++76wq67GXk5EOzpE48vPyb7C1FtERUpETcW1s7W6v89cUHzs6M3Fvslf
+RTouJSUuP2LItaytvO9fTz05PmbP19vKx9JlREpRQ07Qu7W2vL7I5ffl7d/OxMfQ1kUwLSUm
+MjxYvbKursLe80U8Oz1WfnDNwsjOdU1TSERg1by2t7W3wtLg4+1w3MzO1VM6NSwkJjRFZcq2
+rK7A0tlfQTo/XHFf5c7GyllWXU5MWtW8uLu5u77E3uvU093j6+lINTIrKi4zPuPEuLC2u8Tx
+UUU+R0pP++vfzONr5lZVcvnMwr+6uby8wtXR19vVdl9KOTcuKS0yOVLZvrGzubnG6FdBQUhG
+TVZV3dlj39tk63XvxMPDvLu7vczPzeLg62pPRDwxLi8vNz9M0L27t7i8v9hVTkhFSEJEV2Nj
+9N/Uy8vJwL6/wMLK321US0xPWHfi3dna5G1PRUFCREhPZfb36+bt9X1tc/L7ePvt8WteXF5w
+++fTzcfDw8TJ0NnueuTa2Nfj62pPSUVAP0FCTFJRXfft73lt9XFma3nt5PDy5nhpe/Pg183E
+wL/ByczN1d7n4dfed3NaSkM8Ozw9P0dNXH324N3k4e94fnZ/7Pj/9fTx6eje1c7KxsPCxMrO
+1tvd4N3g+l5STEdFPz0/Pj9DSlt46drZ19TV3uXj3uZxamNfZmZv7d7TzMfBv8HHys7Z3+rt
++2phVExIRT8+QURJSk9cdOng2NXZ3N3h6fJyZl1bWl9ufOnd1s7Iwr/Dx8jO1tnd3+lvZVxR
+S0VAQUA+Q0lOWW979uTd19bZ2uju9mFYVlVXX2zr29PMxsTAv8DCxMjN1uPsbVpQTEU/Pj5A
+QUNJUFtz5tzc2tjY2NvudGpeWFJXXl9s6drPysbBwsTFyMnL0tvmd2xcTktIRURDREZITVVa
+XX7u8+Db2dTW3OT0fmlVUlddaXTk19XPycTFxsfKzMzR2+T+X1dQSUZHR0VER0tNUl1sev3s
+5tjY2Nfd5PN3XVZVXWZq897Wz8/KxsXGyMrKztnj+2tdVE9MSkdGRERJTk9VY3Z7fOnb3Nvd
+6fH193hnaHnu6vh469za09HOz8/Nzc3O1+Xm6nloW1JPTUpISktMTE5WXG/25Nre5+z3fX1t
+Z2Vree/o4tjT09ne29fX29zb2tLX4O7+/3ZrZmJdWE9LTE9OTlRaX2h78PXs5+/s6Oj3fX56
+6+zz7ebm6Ozm3d3b29jS1tXV29/n6XdpZ2RcVk9NT1JVWVlaW1xaW11jYGr58+/o4ebi3dzb
+1tba3uPd3drV1tjd4Ofk6ebl/Pn7aWhkX1pST09QUlRSUlddXmJseH78+PHt7d/b2uDp3tzf
+4eTo39/b2d7f4N/l5Orn5evue31+dm5gWFNQT09NTlFVV1pOR0/03+TPwsPLysjQ3t34ZFhS
+T1lhb3bby87PxsXP2dfxVktFQEFDQ0VLVGHg08/PzczP1tzo+2ZbXWFXVV977OPSzMrIysrM
+093k+FtTTEdFRUZFR0tXa+Xa1dLSzcrM1eD//mteWVZVWGBqbu7bz8vOysfJzdPd91tPTUZD
+RERFR0xVbebZz8nGyMnM1+P4bF5WVVZTT09UW3Tg0czKyMTEyMzY/15SSUZGQ0NESE5SbeHZ
+z83KyMjMztbg92leVFBVWFpibfzh2dHMysvQ1+JsWlFMSEdHRUVITFht8tzUzcvJyMrLztPc
+73RkWllZWmNw9eHW0M/Q0dnpfV9TTktJS0lISktNVmd85dfSzs3Nzc7P1eF3dnxtbXF5c+7c
+2NLOzdLa4flkVEtJRUNGSUtNVFphb+7e1M7O09ba5e59+vr47OTd2dTQzc3LyszU6W9aTUdF
+QkNERktQVl998+zf39rb2dXZ2t3k7Hdoav/v6uTd2dbNyMnLztj0XE1HQ0FBREhJTVdibHfh
+2tbS0NHV297m6+7m73Rqdejc1tPU08/Pzs7T3vtjUUY/PT0+RUtLTlhpe+/h29fPysnKzM7U
+3OZ+al9fYmz34djQysjJys3S3nlaTEI9Ojk7PUFLVmjq2NPPysjIysrJztzkfmJaVVRZW2H+
+5NfQzMjGxsjN1uduWks/Ozk3Nzs/SVjv1czIxcHBw8TKztbi/mFaU09PUlhh59fPzcjCwsPH
+y9X6XUxBPDk2NTY5Pkpc6s/Fv76+vr/Eyc/X7WldVlFPUVRaX/fZzcfEwMDDx87daVFEPTk1
+MzQ2OT9NbdrJv7y5uby+wsbK1OtdUExJR0hKUWLiz8fCwL++v8bM2mRIPDYxLi4wNDxKdtHF
+vbm1tbe5vMPM2HtXSkM/P0JIVHLYy8S+vby6ur3H2Fo/Ni8sKysuMztIYdPDurOvrq6vtrzK
+c01BPDs8PT9IWuDLv7m0s7W3usPQdkY3LSglJScsNUR4yLu2sq6trK2xuspeQDk4OTtAR1Fq
+2cW7s6+vsLS5v89kQTIqJCAhJCkyQ3vHuLCsqKioq7LEZ0E3MzM1Oj9GUPfJurCrq62vtbvG
+1mJALyUfHR4hKjdWy7y1rqmlo6SquN8/Mi8vNDg8P0VV1bquqqmqrrW8wMjSZjkpHxwbHiUv
+PmzJvLCnoZ+fprTxPDIwMzU2NTc9TdC2rKmqrLC1trW4x2M8LCEeHh8iJy44VcOwp6Cfoqix
+yF1FOzUwLy82RHDNvbWysa+vsbGyt8HqTTstJR8eHiAnL0HZt6ynoqCkqrLGYUA6NDExNT5T
+387Eu7i2s7Cxtbe4vttRPS4mIR8fISgyRtO3rKmmpaest8pZPjk2ODs/SFV03Mm8tbKytbq8
+uba5x3g/LSQhISIlLDVDcMCvqqenqbDD9E1APkFGRkZMXfjTwry8vbu8vLq1s7nF5kcyKSQi
+IyUqMT1Uz7murKqrsL3SbldNSU1QT1Bm5tPJyMfJyMnHwb+8vcLM7k08Mi4tLi4wNj1IXNjD
+vLq6vcbNz9HX4PtmW1haXGZ76+TVy8O9uLW2u77J5ltLPjc0Mi8wMzg7QElWaurUzMvJxsPE
+xMbL09fY3d7a1dPNxsO/v8HEyc7X61ZDPDk0Ly8yNTg8Q0pTdtXLysnGw8XHx8jN09HR0tTP
+y8rGxMHDxsfM1eR3WUhAPDczMzU3ODtASFJv4tbPy8fJzMvKzdHQzMzMycXEw8DAwcXJ0OLv
+clZIQDs1MDA1ODo+SE9Xd9bNzMnGyc/QzMzSzcjKycO/wMC+v8HHzNhxVUo/Ozo3Mi8xNTg+
+S1pw4M/LysrFxszR19fW1c3LzMnEwb+9u7u9wszfXUk9ODQwLi8xNTo/TmXm1snExsjIytLX
+0dDU1tDR18/IxcC+vLy+wsjTbU0/ODMuLC0vNDk/TF/qy8C/wcHDydDTztHTzcvLy8fFwb69
+vb/HztXuU0E6My4rKy0vNT1LW/fPwLy8vL7FzM/O0tXOzM3LxcPBvry6vMXM2GRKPjk0Liws
+LS80OkRY7s7Dv768vL/ExMbJy8zQ1c/Jx8bAvLu+xcnUa0s9NzAsKyssLTI6Q1Tjxr++u7m7
+v8LCxMrKyMzNy8rKwrq4ur/Bx+BRQjoyLSoqKisuNT1IZtnLx7+7vb6/w8fHxsjDvbu4trOw
+r7G7x2s6LSglIB4jKTBE5MCzqqivvMfbSDUyNT9PUOe5q6mpop+foKjHTVgjEBMcGRQfT7qp
+pqGZmaxP7XQjGiIvOj/XrJ+mr6aeprOto6xNOUkjFBkcGh8pP7SnrqabpM5XVC8hISpCVmWv
+oqeuqaSstLCoqds69ToWFiQdHClDuayyq5uk0dthMiEfKjg+Ta2iq6ukpKuuq6eryURsORcV
+IRsZJEO4s7Kimaa/vN4yISAoMDBIramtp6Klqquop6y3Xz47GhIcGxgdOsO3raKao6+z3jkp
+JCkvLUqws7Klpqmnp6SmrKy1PC8jEhYZFhkqVNCtpZufqq3KTzglKTMwP8O1taqnpaWmoKOr
+rrXkMh8UFxsQFi9CO7SdnqSoqbFOKkFDJi7OzE6/rK2vp56ipKKtuW5IORIPKBwKH6srNp+a
+r7ujpT41uFYsRcdXO9qwxL+ioKefnqy6ukooGxgeFRAqMib7p7O5qau00rq1XN++ZFHiWlnI
+ta+rpaClraarXCcYKxkIHi0XJqrN/J6ht7WitlqyujxGz0o3V7rFu6akpqejqLrHLhoyGAwl
+HxYuwUS5pK2rqqi4vK/JQ89fN0hQ78q4r6akqaSor8JIICUiDxwmFiTPMU6lubaoq7q6rbhQ
+wL05TOJf6s69raysqKistsxlQB8gLBgcLiMi+E0zt7XivKy5vbG0vrzD7MzSZNi3v7upr7Sw
+vdhVPjMuKiopKDEvLDxMNjzfWmLEwb2ysrSyub29xcO8vLe2sb7Sy04+Ojo2MDlGOz1OOjw/
+OjVASDtXzdfKu7u9ur6/uLi5tLG3w7rMTUdCNjc6OEFLe1lKX1M/PkI5RUVO98rT2LrG2cK7
+x7mxuLa5wNpkSjs6QDQ8WT1i01Fj3ERGU0c6TV9W1dPM0MjQ5LvDx7ayvbu50VlUQDc3PDpA
+S1BdZFlsZUReT0Rfb9vd0c7F5NnE4c/Eu8G8vMLL21xVQDtAOTxVTUN29ExcXktRTWPcb/TM
+zXTN1vja1srGwr2+u7/Z3e1FP0xDOUJNO0ttRV7lXnDdaNbS2u/OZnfoTuPufc7Axr26yb/G
+Svd0QD9GQjpIUz5WX1pbftZvzPHU2tfeXc5a99nvzcbDz77G2c1rc1NNSktMQ1FOPlBORlRJ
+eVD239vP2cnN1t3P1tbRytDEzNDW0F9veWFSWldJXkVGSklETklYWFX28Nr3z83S2M3S49DN
+zsvM19TM6nndUVlnTFRmSk5mQlJVTE9c/W3m2XTb7nLk3+Lh39vO19/IzePL5t98ZHtRXVhP
+TmRSUmhZUWRgXul4dtxt9efb6/Pe4d3T0s3T3dLbcNlubGtdWVlaRltSUVtYVWVbaXZ96Hnf
+6tji6NF609LayNbQ1uPb/+r37GhdfFRVW0xNVU9OW1BXWV1oaGfm8fTg3e3U0dzP0dfQ0djY
+1eXk3nLZ5G3X63jY7mlyXUNBRjM7PjY+SkZV29/MvMG9t7y7ur/ExMrOycvMy89bVU8vMDsp
+LT0uO+JHfMN5z77Zwr3Mwb/Kwr/IvrzDvLu/vc9OWDcrOCwnNy8vYk1Lwtbmw8TPw73Pwb7X
+x7/PvbrBubq+udFaWjAsNCYnNC02b1jiv9HOyeDRy9HJys3K08zHyb65ubW0uL/bXzstMCwn
+MDMzTmleyszuzdL81tTrzczWy9PfysfCubi2s7W902o+LC8vJzA5MU3oWMnKa8/bW9Le/s3k
+etnk7cvIxre1tq+xubrUVTwtMy8pNTkzVftPzc9c0+Ra3v1m4Oh36vNs18nFuLO2sLG4tchx
+ZDIuPCorQTE42FZnylht31Pp5HLh+OPhbd7b0r+8t7Czs7K6usZOTzsqNTQpNT0zSndLW+pc
+WuPV4NrO6+7Z/dzMyL25tbO1tbi+vsPMaElELi47Kyw8MzVQS0NZc1zyyc/axM3ryMnPwby9
+vLa3vbq/zcHI1M10Q0M4LTMzLDA4NjxES01Q2czawLzMwL3Fvby/vL29u8DEw87Uz9r59PFb
+SDw7MywvMC0zOzxBSlz75ce/v7m1uLi4ury+vr/EysjH0dXadmBmVlBLQz85NDMxMDM2Oj5G
+UF7l0Mi+u7m3uLq7ub3Dv8TIydLb3X5ZVlNTTkpRSz8+REFAQEdJRkZQWk9o2c3Hw76/xcPF
+yc/l6drZ7fx49XhabHNfWl5XX2tNT2FgSFP+3uZp3MjR7N3U0l1S8ehVUnfdaU9xz/loZs/Y
+VlDy501DbM5UT+nAz/vNv8Lc7sLOUFLd9UtJX9tRQFziWj5U3NlMT9nZSkfp1VxUbsfOZna/
+vt9tv77qadXJ41xd3O5LSfljSkJcaE9IVNtpUF7gelle9u1x59vO1M/Oxs7izM7d+eTa9VVg
+b11bTFJhUl1OV2BdXmlsZ2xd9Nvc+uTQz9zZ1dLS7ODa2f755X1vWVlTT01NWFdbX3X1ZGr5
+6Hro29DX3dre5Ovg4uzd3N7n293pbmN1XVVMUVhNUF1cWV1ncfdu/dzP2ezZ0dbu4dTQ+mrd
+2epgcO9uXFxp6F5bbHtzXl9/alBWZGlgc/fp3NzZztDd4uDd63h1fPxoV2ZzZl55dGhmXGZ3
+bF9i/3Zr+/Z/dunk3djd19jU29vl8Pbr9W1yYl5eYGNiXlNdamFaYWZlal969/xj5+Te5eDY
+3ffs1dfj4ubZ6HNx4eplZVtqZ2RYWmtoWFhh7mxhbO/aeffv5OB6dn7d3O358Nz4bfTn6Gls
++d7vY/zf9WdhbW9zVVhk7GVcZfZxaWr86d/s+fXy7Hx66eHg4dvq3d/f+3r//3BkeGtrXWZ8
+929fcPPsX1x29nVabPfqdfbv4uXr+PTW5P9h/P59eHDwb3hiavng/GL/49/5cHro+WVz5uPp
+8v326HNu6d9kWV1taV5eYPP0b2rr4nd8/nv75uf85t7e7+rg6nNz+fp9a3N0eWlzb25na2Vi
+Ymlsd3Nr7u54bufc6Prz6O3s7ezk3+Ld1dbZ3Njc3uTd6ObnaUo8ODs9OzxGW/HsfN3KytPW
+0cvGztzUysjHyMnCxMvNxsPEy1Q1LS8yLy40SOTW3M3Avsh8SktebGFg5sa+x9fSyMHDxcK9
+vL/FxMLPQSomKy8vLzZNy7/Cv7y9xNJWRUhOUlhi28XDztfKwLu8v7+/vsPFxMPlNigpLS8v
+LzZMz8O9vb/Ax9lfRDxATl/eyL++v8TAvr/Dw8XIytDQxb/GTy8pKy4vMDM8T9jEvr6/w8rY
+dVVLTFly1sbGyMrIw8TM1Nba2NXSzsnGydtNPDw9OTQ1OTs9RVBw18zJx8fKz9XY1+ptbvDe
+1M3Q2NbUzcrJxsTFytLW1+9LPDg3Nzk6OTtIaeDUzc3Ny83W4OLmbmF54t3d4uHc1tfa1s7K
+ys3NyMfIy+xKPz89Ozs8Oz1CTVr02M7Mzc7W397U2ux7ZWNqd+vh6OPVzMjFwL/DxMXHzNZd
+Qj08Ojc4OTk7RE9k3s/Nzc/Pz9LX4XFdYWxxd/3q3NLLw768vb7Aw8rV2uZqTj85ODo6Ojw9
+PD9IU23azs3Ny8zP2uDl6+7r4t/WzcnIycfExszS1+Du7XNdV1phWkpAP0A/Pj9DREdRaPDa
+z8zMztDT0NHX2t7i497b3d7Y197m49za29rY3+7u9W9jX15PRERKS0ZGTExKTmFxcu7e1M7O
+zs3LzM/T3nxjYGtx8t7U0dXT1tne4+Xwd2NiYFxdX15eWFhSTk9QT1JXW2BldvXu6O318OLm
+7OHd5eff2tjX1NLR1tbV2ePf4u97al9aW1ZTU1VZXV9dYWx5dW1zdm9obW1oamhqaXrs3dfb
+3uDh4N/c3N3j83l4bGdnX11eZWhhZnD9+/jr39ve3+HudXJ9f294++7s8fPwfHFzbWZgYm17
+cWxzfG5vaWJhZmxsZWj85N3c1dLZ2t3i6OjvfHttZF5fX2d+7/Z4f/X9bWhubXHx9HRsa3Vv
+YWNjaHB47uTh4Obl6f797Ojs7ufq7/73/W1tb29rdHh4fHj77/doZHh7bnF7eG798fH7eHj+
++/337vL98url6Orq9/z0+3duam52cGx0/HN09+7y9vZ+fHNz/PZ7fvf7dGpsa2hse/57dHbx
+7Ovp7vDp5e3v5+bm3+ft7/L5c3lzcHZ0dHdvcXRvZ2RjZF9fYl5fYmhsdnH77enk4uXn4OXo
+6Onq6OLf4uTj5vDv7X5wcGhhXV1jZ2RfZWdfX2RnYmFzffLo4+He4OHf4eb19fT9/nn79PLq
+7vdwcX15cGtvaWZoaW1nY2h2d3P35ODe3d/k6+fsfW59/P94a21w+/5xePrveXlpYHD48/f9
+bW1vbHR9eXv273Rw9ft88e3r5efs7/V0bHr+dXh6e3x7e/3v7efp7PN9+3lua3B1ZWBncGxs
+/v5vdXb+9+/r6efn4eXj5ubl7vl2ampoYGFpem5pc3V0/vt3bW159355eHh2evDo7fL09vb8
+7+jud3B1+/Tt7n5tbfb3b2pz/vnt7/Dr8PTf5/j49/1tbG1ta3Frb2lfZWxfWmvu929q/fdr
+Zurb6OLd4ePuW2jT1N/e3vHv6Onr/W9sb2ddW15UWX1oXHTo9WxrZWV16en8XmnYzsrPbWPn
+7Wt1a1pt6tvkXmvxd+/b8F5eXXJfceNyYX7a73bf+lxv4+1gWPrwY3Pj2dn+ft7c5GpccG9k
+ce5vfuP1XFvt1+hy3Nb6VE1Xa/fl2d7pf2x6X23g51xt2v5sY2Dp1Obo1+VyamRVW2f97uLq
+Zmds+3fw7e/tem5saGbv7Whv6OTm5O/6+/Ti4vJ86OxiWFhfbOzf6mtv+Pf5aXZmdPp7dWxs
+Y+jY2dvd5nRz8Ov6/3ZmXGXv4v1hYnnt93t7bWFhb3t+eerqdn3m3N96X1pcXm/u+3H24NvU
+1Nnd2dDU4Xtib9/V1NvvWk9IPzgwLzU/VerNv7/DxcTGz9fWz83PzcvKxsG/wcTDxMTEwME7
+GhIYJC9Tt6qtvruqrWswMj9X0ri0zU/ZtbS+xsLJ3s++wNvTvbKv2R4PDhgpbq+lprG4qKKw
+SisiJTi8rLZ/eL62tbW94FfeurvV5sm9sqmuKw8LEyNEtKKhrrSknahkKx4ZHTuyrrmwqKmw
+uL9RN0q9t9B5z7+2qJ+4GgoLFSX9pp2jraacnrQ+IxgVHkyvq6ukoKavxkIvNmnGx8rFx7up
+np7gEwgJESW2nJqfpqGepL4yGxIWKNCtpqOhoqazTSsrO9y9usHb2relnZ9PEQcJEiytmpuj
+p5+dpMQsGhUbMMuvqaSioKO1OyYoO9O7vdtfy6yfnKM6DwgKFjapnJ6mpZ2cp9wmGBUdO72u
+qqikoajJNCktSr22zEtbvKmenKgqDgoNGUGmnaCinp2hsjscFRknWrKrrK2npbBXMCov2a+4
+cEn7uaadnbEjDwwPG0uon5+enZ6nwC0bFxsre66pra6pqrlVMSs5yLTAYVjOr6Kdn7YmEQ0P
+GzusoJ2cnaOv+yobFxsq5Kylp6qtscNIMCw+wrW//WXOsKOen68tFw8RGSu8p5+cnKGwaikb
+GR0u06ykpauwvdNNNzNDyb3SW1jQrp+dpLM3GhIRFh9gqJ6ZmaC3SCgbGR4r3aien6q1z05H
+QkFP08x6VVnYrp+eprhEIBkWFhopvKGZlp2wRSkfHB8qSbCgnqa6ZUFAUXTj5ObvXm7Uu6mj
+pq7IOCAZFBQbP6ialpmnZSwjHh4oO7+lnZ+w3D82P+zO1dHMZ07pxLKppaq8YC4dFREVKLOb
+lZeg0S4jHh4kNMmlnJ2sUjAuOta+vcDF1UtGb8Oup6etwz8lGhMSGk+ilpObtDYmIB8mNX2u
+n56ozTIrMXK5ubzE3k8/RXG4p6SnsNMyHxYQEiitmJKXpj8nIyEmLUG8o5yguzYlKT67r7S5
+xnhAOD1rsaSip7VbKx0UEBc5opaTm7guJCYnLDdat6KdptYsICz1rquzuNJNNSw21qien6W2
+PSMcFA8Z7p6VlJ3SKSYpKS49c7OioK9AKCk7vaqsuMljOCsvSbafnJ+qwjYeFxERH7OZlZim
+TikqLCssOsurn6PPLCcy4a6or8DTSi0nL+Cnm5qgr1gnGxMOEi2kl5SbrjssLysoLD+7p6Gs
+SCwvYrKqq7zYVjUqKUGxnZqdpsEzHhUPDxrGm5WYosI3MS8mJCzoq6GkxTAqRbasrLPE50Uu
+Ji7NpJqan61pJhgRDhEnp5eWm6jaOzcqHx8xv6iksEs7X7yztLfG21s4KyxSrZ6bnKa+LhwU
+Dg8a0J6ZmZ+s2kQvIB4nULOrrsTdy7u3vsHK3l09MS5BvaeenqCsSB8YEA8WLqyempufrdU7
+JR4iL2LAuLW2s6+1vcfN3lVBNjM867Gknp6nyCgZEw8UH9qonZmaoLJeLB8fKDZKd7isqqqu
+tsLI0FpCOTc6UsOuoJyfsjEdFA8SGzXLpJmXmqK2PiklIyQnMc+vpaeqrrW3yN9PPTg1O0i/
+pJ2dpHQhFRITFyM5tZ6Wl52nvU8xKCEcHjPQrquoqqyprrvcVD40MjVJuqWdn7E0HhoYFxsh
+OLqfm52fp6687jMfHCMvU866tqukpaqxvvhGOS0sPMetpqa4PykkHx0fIy5Ltaqqqaenqa/a
+MiwvMjY7Rlm9rKiqrbrPVUM3Mz1ywLOvtuU/My0pJyorNELy3L6yrKuutc/fVU0/PENRy7qv
+s7rLZkQ5NjM+X8G3s7S4vtZSODAtLi8yNj1b0b27ub7CydpmT1fwyL24ub3H6009NTI1P2nI
+vLezsre9z1s+ODQyNTc7Pk1jz8nK13pjVWnjy8W9uLa3w+JMPjc1Nz9Z0L64tbm9wsjWbk5A
+PDk6PUpebHRYWVFORUVNb8u9t7e6v8TPYEM6Oj1MbtPIv7u8wcvbcWtjW1NQT1rv2OdcUUQ9
+ODc5P1Xdxbu4ubzG3lpOS0xPX+DPwsC/xs/hdl5MSkhRXd3Kw7/FzG1JOTQ0NTk+TnHNv73A
+y9Pmd1tda9zNxsHFytHW6WRMQz9ETWTVyb+/vsPPYkhFP0A+P0FIXfTjc21pZ29+3dHGwb/A
+xMnS3mNORUJARk1y2M3Hx8fKz939bG9fWFlTT05QTUVCQkZNXHrg0sjAv7/Ey9LlbVFLSEpY
+dN7TzMzNztPT19rf3trb3O5kUkg+ODc4Oz5FUGTbyL++v8DHy9DafmhuaWtqePTr6+Xc29zY
+1NLMyMTJzdxxSjw3NjY0Nzo+SGPRyMO/vLy+v8XL1uR5W1FNTk9XWmby2crDwL/AxsrO2fJf
+U0o+ODg5NjM3PklYdM/Hv7u2tru9wc58W1FIQkFFR01l1sa+uri5u73DyM/ob1pLPC8sLS4t
+KzA8VNfAtq+sqqquucXcTjs1MzQ3P0xl0b+4t7e3uLu/yNPh5+d9U0E0KykrLCosOmHLvrGr
+qKiqrr3kU0Y3LiwxPEZS5cW7t7e5uri3vcvU19bY3fJVRjUpJyosKSo3ccS7rqmpq62zyVlK
+PjMuMDg6QOrGwb2xrbK5ubvJ2+RpWnrQz+5dZ0gsIycrJyMtVMa8r6ajpaaqt/BIPC4nJy44
+PEXPtq+ura6zucDgTUVJS0teyrq8yNPyOiMfJSgjJT28rqqinZ+lq7pQLyonISEpOlrNuK2p
+qKqvt8DaSzo6P0VIWtG/uLO3zWNeNx8aICwpJkWsoaOhnqCqvFQtIyMkICRBurGvp6CjrL/e
+b0MzLzU+SU1kzr66ubm7vL34OTQ2Kx8hMUhATrSlpausr75gOywoKzQ4O32vp6qtrK27b0Ex
+Ljo7ND7QvMrewrq9w8TV1r/BUEL9YS4hIy82LjLZrqmrq6qrt2gzLjEzLzRhurOzsrC1wWg5
+MTs8Mz7Iur64sLXDztxSS15UTNa/zW11cC8dITc1KTuuoqeppqm0/i8lJywrLEe1qKipqq24
+YjEsMjYzQM65t7e4wtz8UkFCUGHtyr26tbTHSzwqGx0uMypLp56kpqivyUIpIScvMTvDqKKk
+p669YjMmJCs4QeGyqauxvsj4OS0zRVRgzLWvsLS6wcs+JBobLTEoOaicn6qqrLs7Ih4nMi83
+vqKdoamwvWArHyMvPUzUr6aptsbeSjIrL0ffzr6uq6+6yeFhQi0hHyk6PULGp6Orur3AbS4l
+JzM+Rv+ypaOqt8pkOiomLD91y7WqqbDHcUc2LjRHcs67r66zvMzk5U4tJCEoNzw9z6eiq7q9
+yVEtJic0RlfNrqSkrb3ZTjUqJy1G3sGyq6mvxF5DNy8zQFjPvLOvsrzR6V9ONiooJCs7S1i4
+p6attb/XRS4oKzlDWsSup6iwxGVDMywsOF7GurGtr75sQzgyND1Uz7mxr7O60FlMTUU3KiYq
+Pk9J6a2mrLvI0m01KCkzS/rIs6ejq791RTYrKS5D1Ly3rqywy047NjY5QHG+sbCztr3eUEdA
+Pi4gJkBPQW2zpqa01ObtOykmL0rdzbyro6m+W0M6MCsrPce2s6yqrr5YNzAyNT3zu6+ur7O7
+2U9ENiMcIzY4P8qsoqKuwshwLyUmLj5d0beopKu51k84LCctRta+r6morsJPPTcwMj30u7Kx
+rq+61FEvIR4gKS480qyjoqistNM2JyUoLjlSv62nqKy2z0g0LC41ROy+sq2uu8vbRjY4PEBY
+1L2ytLy+0UIuJiMqMzhNvq6qqq60u9s9MjAxLzNXx721sLG0xlRGQzo2O1DJvby6uLzOak1C
+PT1FXNLKyL/G2eJPPD5KT0c6StnuVX/OyM9sctbtTkZKWmdw1sO7u8LNzdxTSExb6+Hp08fK
+2OT2ZlNNWP7p5dnR2ltNU09GOjdFV01Nat7N1u3bys71WVr7YVFpz8bKycLBzOf85vlYU2vW
+0NbTztF7U1ZhXFRWaenqaVdYW09FSFpZSEhMT1JRWuzb4NrMzdXg4dvX1c3Fw8jNztHcclxY
+VVdk7vji2ut9Y1ZYXVxeavjg815bY1tVUFFYY2tmXGX88N/k393qfPt5bXVqftvc19LQ1tjm
+e2l3eevj3Obi3vtsX1dcVFNdbW7h593c1+11ZF1ZUVhf+9/c3dvX5nV0XE1NT05ZaG740tXW
+2dr8fGptd3zo3tfR1d7d7/txZmJ4/XLk2Nnr8G5XXldQTl5iZu/f29vZ1+VzX1RRTU9cfW3p
+2Nvm42xia1xi/fLs19XY2tfl8d3Y52vt6HDv337s32ZbVFlYWWRl/P74eXR9Z2VgZ19i7mz8
+4m134+jb3nDq62h64+Dma+/VenZkdF1eaPX66uTo5N7scPtsZmhi7n/n8/B67fF0aGv5cGhp
+6ex/8W53aGlkant1X2n15d7f7ObteGVfWFxm/PN729r68en/9Pnq5Ojy69/f4uDk4ndscW5r
+cGxnXmZgX2drZF9ifX1ubXLt7vD34uTh7mp5/3L59+nr8ubi6/v1dV19aW3153V37d3o4+nq
+7ffx6W5lfGb78mZoZV5saVhebl9v6m53+evp49/wbvr5furj493Z4Obv/vN+d29sb2Zkb2Je
+Z2lwZ3Fybfv46Hz3b/71/PLi397n6uzq+f389PL28n1nfmdqaHdr8/P39PN6enR5amBmZGJp
+b/nt7uXl/e58dev2bP334t549uX7fX3z7vhj7n5teufh72j16XBeX21iYWB1fup7duv47ufn
+8O1w/OV/8O599nVpbn/+/23u7ul6e2j8fl13c2J5efLu9ezv6fZtZ2RrXXvs7uPm7PTp9Ob4
+cnlrbfh+enJz8OTlfXX2dXNhcuzvd2dv6eN+e+ZyXmR1+HV67/J+X1547XF67Ozzff789ndu
+aHnk5e/54eHv7eDg7ndka+/5bXvk9nPz6nReXV9fXF5eZnZ0fv1/+P3s5OHo7urs6Hxobvzm
+3+Hj3uP78Orj9GxtZVxYW2RoZF9jbXzy8+r0eXx4d3fr93H79fPx5uPr49/e5O93cXRua2x3
+dm1ubHL6fXFuaWZre31zfPz2/3z8d/v28O7p5ert8+/393V6+X14em9vbmdoaWppaHvv7uzm
+8/d+9+7r8PD3/f1+9n5xevrx+3lqbW5vcn3v93Vva3X79O/v8P78cXh6eX/66+jn8O/r83Rw
+cnZuc31ubHN3eft/8vR8/fL4dmprbnrz5ubs9f39+f5veuzx9+34eXFt+Pl++PH4enJvc25m
+ffpxe/t7/fnt7ff37+rzfHt+fPjv8fDv7Ox8b29yfP77f3lzaGx5bmp19fPz/nl1cnH98e/y
+9vH19O7m7Ori6f90bW9xXl9se3Vz9n5yfPHy7+7s933+fvN1ef/z9XNyfH94dnFtZ2x7ffTu
+7vHy5/N0dPz9dG1vf3x2fu7k5e/s6+99eP33fGhobmZfYWVnZG/5eH368v3z5uPi8Obg7fr/
++fT5ffbw/3j6+X72dHN9b2ZnaWZfXm52aW19Z3H+9/Dv6+7x6+Pk6/h6+PZ77u749+/s8HD7
++Xrz+Hb0+WtxeWRfbnR+bmhzeHhrdunybfzn//Pt8uzpff7ua2Fq+nZ9fPnv73psfOf5c/Xq
+63NqfvL1/vTp8nr3fW99eG73eGh0dfV/YGzc63R39+h2XW/tZWf49G5sa/XwaGX03/Fkftzn
+bfva4O124+X1efzo6l9f9HdfeHBzfmJt5H1l8vhvbmhe//5l7t3gfHXz2upj++Dvb3rk6XNo
+8fP5aG12/G5x7OnvaXRtZWt2fv/q8/vr9G14+X5ke9/09O5rbHNpaPt89OHm+G375+3j4X19
+82tgdv/849/q/Gtfb/thX39sX2p8aGjt9G5t8Xzk3Xfp3e/n8Gp24u97bWb172Fu325s4v1p
+5/lr5Ptid+X6Y/58aut4Xfvda2399/rqd3Dk82pmefh0Zujp/O5tY+D6Xf7mev/t3+Pk7Gl6
+7WFX93Z/325e5+1f+91vXnZ59Xdd9939cfDj8PJ55Xli8+jq+Xv97e54ZPPuZF1nW2n3bXLy
+6+vn7Hhxdm597H556vP+e/1vZW/uZmng5PTu7Xf3b2Lv72no2/Ntef/f71nu4mNlcnP6b2Zp
+b19fbuV9Xubc+XTseWb0/HvY52Xj2uvw4fZu7nd55mpp+m1ZXnl8bGh2fP7v7fnrcvx+aXTu
+4ulrduXkblxtefZ2ZezoaWf1c15t+fR67fvv2vR13uLq9uh89d9tbuxed/5dcnFtdWhx7GBu
+3mdq52547G9k8uVrdNpsZOn0/dpvZ/DtenHy9W9obHbf5Wj33vn9/GtqdXZ87Otq7nhf7uZt
+79hmZfRtaW/v4nNe/X14b2Dm0Ohm7ODq/GdiePpqbm1oXmn2b2bu6vHuaHHe5Xvf3Xlq+v1k
+aHno4Oh57tPxZO7mcX5lXHNlXG3kcmhs8PtqdvTwdHlv+Xlv+9/g9u7k+nx35Nx0bH378mlY
++/xWW3P2fWJp1d1a7tF1YeLyaO959uBrYePbc1/r5+5zbNn1bW1662tkcfN7aPpwbPJxX232
+ZGrt9/fwbfZ2be/v/nXu6/x6+e/b32163evs/mH4enRjX+/9Y3xueuNz/uZoZP3v+mBs6fXt
+73v25/ry6ev0b/Zvavf0cPfnXmD3bejza+7iXF3pdntxYOHfaPbndmJ93/b54en19/bx5Xle
+3O1UY/tfXG3t3/lm2tZ4XGff5l5d79t1XXbt/WV8eOPnU3PdamZteerzW+fTXVba2Wv73+7p
++2H+5+t4Y+LjVmfcf2rp621gdO//aGlseXhfdeB7a/Xd5XN48uLyb+7ra3xtYfF4ceLya2fw
+22Rn3+d9bXDc5lxifm9faezxaPntbnFtdtnoW/fvavrx7OHhY+/Vb/vc5urxbP50W216a3pq
+amtgZGxpc3lkbftvd/J64tffdd/X8uXjanheXfV7bXX+7+1rfdbhc+76XPztZ/5qXOzkZGzn
+7PFqbN3rb2x+7V9e+F9ieGvi5GDo0u9o/uX6dmdg3OFfb9vq6+h4ft3jbWj4fnVgVuDdUl30
+YmJqZu/tc/nj8W56697/fdjraeh79+JpbN/uYubgYW/9ZPLkaV5acnhlcGZ13/Rd6dD9aObp
+6vNsaep5VnHb/P5t+3lie33w8GNu3/do6PBp9urseW3q4Otl8Np6/+Py9WZXc21e73lqc1tf
+5m9t6OvufPL2aW316+Hx7dnj7nl0dG53dPn4feznam/w82dv9fBsXf7tfnvj33tZZt9gVXPk
+b2by8Ov7XXPU+l/s6+3r4fdkZ2tu+Prk3dvs5/5162dt831sam5uamBr7Gdp9PltZnhybnxm
+3txs79zq/Xfr4+JsduXiafXmb/hxbeToXWttemtfbHpy+3ZpdmDu8XTl5+bo8ervZv3vb/ns
+ZWv7a215+nPs+nTr8nh5b3ziX1rX2mzs7Xje6mry4ntiZnZze25v+mJYfndq7u/v+nh3+2xu
+bvPr7uvm7uzueOfq8uTke/PuW2b3bWJqfOz//Ph1dW9f+uZfdO9obXduc+DwZe7bb2Tz8fXv
+afzf8+n+e+vp/vDf9Wv17V5u/fTv93T1fmppbnBYXezpXW3Z5ltp3/h3f37p3n114fVcXvb7
+/d7n++/6b/XoanblfWv7dml7fPpnYerxbvPpaHzx8+PlbnXzcW5s4ux67fJrcnH35XFobe57
+WnbdYV7u9O3ubPbuZm/06vRn+ud2+/j59fDx+f1udud3XvzgdGno8Xn+7e/y7f1oeeh1a+35
+aHTzZGTr9F1iYGr3+vbk82l56u5t/ejo/nLq5W12e/zudvTr6PRy/999bvHx7ndhbe9paXZx
++3dx83hz7Xtt7ultaHj78+ruc+5vZ3795OJ9eX779nht8vdpcG9iYfbo4+L77ur4bGx49vf1
+cWpw8fZpeu3q8XVub/hza3fs4vxq++t+eHHv/md56+zt7nxz6nZbaW9taGVufPHo4+ftfXjs
+7nh8+Pj8b2pscG9janPu6/3s5fT3dujgb3bj5/F4a/7zcG3w7Px3c2/1cmByfWppbHr1cmx2
+8Ppo9ed1d+bs/nl17uhv7tzl/vjl7vtxdPH6cmdxfvFsc3ltdPT98e/++3V29en9f/bu7Hf6
+7HdubGpxY2VtcHp9/25u7unx6nt763347O/w+fvv7ejq5eL6+2xmeG1iY2l7+fl49/h98u79
+ZGNkd/x98+DnbXj17fxtcu73dG365PR/7+Dlc337fvR2cnFva213dnp8fnl0c/5zdf11dG1q
+/e7r/Xt7+Pp29N7e8Pfy+v3r5nxy+XZ2eXp9/XdtbXN2cX19cGllaGxtfPj9evHu7+zj4Pdv
++vd5/Xh193ptfP53f/z5cXF1b//18fb/ev7r+nT+7ntyaHjzfvty7vD1fern+W9t/vP+c/v1
++HhjaHT18+zr7PF0bHz6aV908XFqanbwfWlv9flyd/T6/vb5fPny8O3yfm53+n7z7O3r6f55
+++7v/Xx+/G9r++77b2/6/nJv/fn9fm5obW5kbHX89+7rfv7z/nf88PF/dvvw8vPq6/H/bW1v
+aWhtcWtxd3n17Ojj5ezk5/Hv6u51ZnB/b3J+8vh6dv19bGVta2dqZ254cHp6b3Z2ePv3+fD7
+eOzf5ebm5u729f95dn/9fn/+fHx7bW57e/vt4/B4eGpjY2tsaW1tbHR78+j2efn9/f12/vH1
+/X758Pny731tdPbr6+75ffL7f/d2eXd7dG1ybnFxcHr8+u7s6PRyePX7+O7w/W5jYWhve2xu
+dHz08PLz8/t4/Pb39fbs7fV7ePXq6vz19fv6/np5efnwd3Z6cnBubG/8f3psaGtua2v98e7t
+6uf08+/9+fLv/vvu6ezp6fd2cm5nZ2dsd2xubG57bXl+/H1+9evs7uzr7vLr5+vq7Orq6v5v
+dnJzcXlxaW1tbm97+/p5evl2bXNvenhuefd+//V9/vTm5uLl6+12cG909u/s7PF4cv/99/lx
+bHFubPt+efx/cnzy7Oz5ePp7b25rb/jr6OXn+XR2bGt0emtscn7y83p08+/v8/t5bXrz+m93
+7/p8ffnw7Ozo6/B9dW9qdfr+cnn9//Ty8enn83dqZGV88PptYmh5eP/1fHX69fr7+/r3e37n
+3+736vLx73hubWxreXFnZHDn5unj6f959/52/fL6cmdrfPLo7X1ubnj8fvpvY2ltbfXq/Xp4
+a2lqeeXq/fvv6+ru7+3w/X3z8fnw7vT/aGt0c/HvfHBsaWlreX359WxnaWdqefttaG50++7v
+7+bn8Xn9+uri5fh3cWx37+ri4fdiXGV3e/Z3bXv6bXF8a3Lv3tvpbO/R095+XV94bXPyWkU9
+R3TRzM/X9F1MTmHZysvZbU5LU2/NwsnfZ1NVb9rFvsbc5HRe7OBlTz0vLTvlvLS7zl1FOj9p
+xLa3y1A/PEVry769y1s/O0Ne0sG/xNdNRWfa1s3Oz83dZFhd/9nP5dnVMx0s0L+0scfP1C4r
+Qcq1rL1dSjYvOu+4q7HeRDs+UM66r7THXkhN6czIt781IRwhX6ysq6m/PCgiMbysrK/ASzQq
+KkS4rK6+Szc3OEq9ra242E9DOj/Grqy40tjjQC4oJyQvuqy0sbb0Py0nPru1s6+61kItKTlp
+v7W3ucJTPUhg0L62sLxSP03iwbu9uu8pHhobNa2mop6rai0eIUbDtKWirfQqHyg5U8e0ra3E
+PELVyM/MvbC03E5g1c7P1+NULR0XHUK6raOdo74tHyQyR7ymoanNNSorKzHrraexv8DPRj1x
+tq69zcPOTEbvvuUoOWIhGylJvqy4qaG/LS0yOTdJqKO7ZXRJOi4wza+40LyvuXhkxLrH2sbA
+0Es+Q1XvOiIdJjMvPLGfpLTIxn8yLEDZxcrHtMI8Ps7JVEjPuMBs17ezu762ue09PT8wNG3N
+zrzPHxksKihmqp6fu8W+OSYsQM/O1K6qwFBW2ms+SMa4vMK9tLvMx7zMR0s2Lzw7TcC6vy4c
+LSodMLenorC5r1IpMPm+y8atrcxQXXZfVNm/v768vbrUWNJKQ1AvTLpITMpiMh8fMTMtTq+o
+uMKwuE1F683Wase2zvW5ssdPT95nRuS5s7fAfUw/Mz93VFVa30cvNjosLTU6Zfzjtq63t7zE
+3ktd0Nzfvbi7xNbT7VdZ+czFz9rsWk48OOXXTFFrV2Q5L1FOLj7dU01ly7m/x7m711VVdO9u
+3MrNzsTF6Fv0aFtu0MC/ytLdZT43RmNQU+DXX0FHUUg+R/R4Q1HJw9/pw790TFtrY1/SvMDS
+y8f4SVPb1uPNv77ZYWhTQD1LTVRcVWleXN7uW2V5cXle2sjR2db1WFRMSEhUadDL19rQf1lb
+U3ne3srCyc/bZV1JTGfb1dPL6mJbWl5kY3zqcFh09257c9/bV0lOVU1a8dXS5+TcaF3p32t/
+2dvbb+feVE5eX2xu39rMYOLK4efU2N9efetqa2xqXk1IUF9W7eR44XBpX2zncml87ufu5NrZ
++3xuam5fdO3Xzc3U3X13XVH+dPL/7+9fVu9SSVlSWNrf/9zYdWB4/fV7c2/r4G7t6/7meu/v
+bnnf6unk6OPm+GTr2e/mYP5yTFVTWF/2fnHqbWloa2Re7fT75+Lo+W/e7XHc5N3X2vfp2vlc
+52FRXWnj6+bf0/VYXl5cUFVkZ+/sbt3cWnv3eeddfNdxdmvs3l934+rc3d7e8d7lYW9y/H1d
+/t7s+1plYk1PXGpvbPvX3eD08+dkXWpi7v5o191x++DY2Obb4t16+mrxek5Ob25bYPHZY+7n
+3V9bYFpe4v7673thZOb17NnX0dXyb/BnWGhfU1x+9t/c5d/e7Ovd82vx9VtgZV5WaXJiaOP5
+/ebv+uLiaefpa2J99mpo7exr9fV049/e2d766OpmY2VYXnNaXvvq8t/f4OXddFpvXVVedf98
+auncdXnf3+fj6udubf1/Z+P1bN3yeGBs5ndoZn5092dp425bb3Fqem94d357697a3+zf62J+
+523j4/j2b2lnb29pdG/77+3s6ef1+F9bXmdbX23raWHl4vduf9/edurr6fNoffb+aOXc5ODz
+3dvs7PF0YlNVVl1lZ2ns4N/obezrbWVt43Vbberq//fe4HPk3OX0a279Y1t18flraGrnYmD+
++Gpt++l67vDx6ftf7OFk3vP96+N/cOzf63jy9Plse/R7/nRqYHlnV2Z89m9/5/B2dnvy8mx9
+4u796vFuZO/vfubh8PLpa2Ru/nDh53P57/77dmT26+T1799sZ25qZXJrYW5taGX87e199ePo
+715sbXtmeeDy9HDs6vvp3dvb2ePt9WRpb23+YXb5Y2ZzfnZ+/fXsd/dnW29sdml+92le/fp4
+4N/h7ejoemT54PP5597zcWry5/zz++l2dWlWb35baPt1XV9tcvT96urq2ud2cO3j/vt3evL1
+7+vi6n7v7/tkZ2hjY3JrY278/v76eP51/f749vX1/Ovz8u3t63Tp4WleZ3Bzf+Lo4+ly8+1r
+/25u/WVebWJo6/fz6vf3+n11d/3n9XfsfGRmfWxl9e5xcOvq+/Lo8PDt7e3tfmZvbmxq9+h7
+eubu+uDs3eZ9eG5rZWthaXN3+29xfm7l5F3v5/l2a/5xdXZraG11ePfm4ujrafXc7fvj3u91
+av/7fvPq7fF7c+/2/117fmZmaW99b3Lr7u51bHtoYm9ucvPt6ebj5O308eLyc3h1bWtlYmxx
+6d/p8vPo6ufr4eX+d29wY2ViamxmaWl18/J/eOnq/Xrs+nnyeXlweXx8fmhteHdtc/j+8ezs
+8evj5ebq6e3u73Ft+Hx+/nJvb3N3a2RhXV9dZmdoZmRtZm348Ork4eXc1dbTzc7Pzc7U7VxZ
+WUtERkxNSEJIV15h+tzT0dPRzcjKz9DLyszO0M/eTklPRD08P0lLSFvl/WDu2dvc1czL09PL
+y9nMxMjHwcj0P0BQNS85REZARc7KY2DKyOxgz8XS07+2t7+9ub3C3D0sMzEkJzpQVFHCrbl8
+1b7lO0HOymXIray2tq6wwd48JygpHSI1XNfLr6WvztnSRC0zXGVavqinrKuor8RYKyAnHxso
+RtnQuqSkuN3dXS0qPU9OzqujqKijqLvrNSIjIBoiNFTNuaemsMHdVi4qOUJHyqylpqWhqLjK
+NCAkIRohNVfquqSnucXQVywqPT89zqmmqqOfqLzaMiUmHh4sOEzTr6autsH8PSouODVLvaup
+pqChr75YJickGiY5PVLAp6q2tchPLyw7MzbLr66roJ6qtsQuKCgbHjA8Q+2sqrWwvX46Lzoy
+M/63sq+loaqtuTYsMx0cLzo3Ua+tvrGw0D82RDEucr++sqejrK2vPDE+Hh82MzNPtLbMr7HZ
+T0VJMzN9y8i0qamtrL89STcdKD0rL9m5zsauu17i1UI7deZjwLCzua6xSkTPKiBGOihBv9/y
+srNkbcdYPeXFeM+1ub22tGE6wTshV08oPr9RRby9TWPCXD/Iv2/Is7rKvL1JO+M9JlXqLFG5
+S067zkxczFxFyL7Su7S4vL2+TjdvOiZPci9euk5cvfFFT2tKQtXI07myt7m3wFg/TTAoU0cw
+yr5ZwLnhZ15MPT9jac63trW3trxbR1AtK0k2NNPY77q8z9jcVT9GWkxyv7q8u7O+XuJZLTJJ
+LjXfV/+8vsvMw9xXaW1YYMy+y8a3xF/mVDA6STE3VkdayMjNzsTL+tXP/ujOydfKvMvi3lI5
+Q0k2PklAVOjm6OXNzeHOytvWztDd0MTO29tQPUlIO0ZPRlb8/+X618/gzMnZzsjU2MnFytDU
+X0dKRj5BQkRQU05YU2je4tTL0MzGzc7JxMfMznFPSkJAP0RKUGJfX2V939/Syc3Oyc/QzMrD
+ytZ3SkhBPD0+SFBWamFYZe/cz8vJxcbJzc7KxMPL31hGRj88PURNTlFbVll45tDPzsXIzc7P
+z8jDw8v1UUc+PDtAS05VVlRdcurTzsvHyczX3dDLx7++xvNNPjk9P0BIU1ZPUFpi9NvNycvO
+1tva0s3HxcjEymVKR0FAQEZLSEtQXmt12tbW1tve2c/NyMbFw8TDz2BMRTw7OzxBR1d+/PPo
+3+bl3NPOysbKzs3Pz8zLz/xUS0E8PD9DTmBnffxubGzu3NXLycvJy83O0tLR1djzU0tFQUFE
+SE9bXFZVVltr3dPNy8rHydDX1dnV1NrY4PhsWk1ISUhGSU9PWGxtd97V1tHR19zf3Nzn4NvW
+2NzheF1QTEtMTldkaXPz7/D9++zi3NfW2Nrc3t3j6ufxaVxXU1Nba37u7+/zdXNfV1heZmz1
+4tjQztHQ0+NsXFlUVFJWau7n5uPtb2BiaWlmdfrf1s/O0d3v+WtbVVVUV2Nt6t/f9GxubWZc
+X2lrdOPX2d7c2trk9XZyePnu+HlybmlkZWJhZ290YG70dHjr8/rn7Pf08/j4fvzv8v94bXf5
+7vZ8bmhtcXh+7PDn3e35/HlqZW1uaW1vXVLz09/55e59/fXr397v7O5nWlVYZH3w59jX5vh+
+d3BsXGJmXWl87ezv9O/t7fX27/bv7fPu93lyffx9enRvZ2ZjX3x1c/Pt9n98e/j58efj7Ht6
+fvv7+e71+3j7+3z4en16++/q7Pp+fmtmbWprffp8+n9ucnl9fm/28vbp7vbu6evn7Hx2/vV8
+/vp7c/fu9v9vW1tnanf5735+7ufw7Obl6P5rX3H07+/p7fj18fJ2aHB4aWxsa21yamJubPDe
+5+/g3u7j3+Tp8vx6fHljYm1iYGd1bGNtbHP27ubk6ezo6O7x6ujt9HtudXBz/nJ2enb/dm9t
+bHz/dPf4dHz/8PHr5Obt7ep5cHlqamr4fHhubnn5ePPn+3BlX1tdZv3i3N7g29re5fxsamln
+YV1bYGlx7uvl39vf7fT8bGRhXWJrcPLq393f5P/w6X1uZF9canv97O34/OP8Z29zdHB3dPnr
+fnf37/T+8e306/T67v9v+e/z+H98a39xbXh5b2t3bm59a2348ff69PX76ezq4/Dw7PX7d3J+
+cGtne+/u/fl+Z3f2+3J7fvTzc2/9/331cnjr6/X5/vzq6uz68+r2aWlpZHjx6/fq7Xt0bGxr
+7O55cnd0cWlw9fZ893Nu/Xj77P1yd3zq/unu9eh+9O78b218cm7t53z37nt5/G5vbmZpc311
+8Oj0bnF1aG12eHN4b2/78PHh3+bu9v7zdW55b3FtfP9vevXv/vDq8XJvb/76d/v4f+rk3upu
+ffhvbWV693l0XmpzZ3X0d2t79/71/e3t7+Lf5/F4725kbvt5//92dXRvdvD1+P54ffL37/Z5
+8npe389fZtnhe2x282piUXzQZFfmeep+YujR5Xp+3+Jna/5vaWpr+/d2em16cG9nbG5Ybtpq
+ZtHP5v107nxiXN7dVVjb2W9g6dr7Y3zk3vFffOBkZm138GliefdmYl9fc2t43tze3OXf0tPS
+ysbM1+pfUEE3MDAyNj5Q0b65trm7vcbOz8m+ubmzrq60Sh8fLhsUIkXVv7yfmq3Wu8FBKSv0
+1kDZramxzbalrsPBeT4hGCMlGynZuqmtrJ+tSks7LyspW7S+r6SlqLO7rbPVXzYrHxsnIRs0
+xcywrqifsErYTy4uLl63ybSlqq+7w7XLUcTIPjgjHjMjGjTL2rexp52tZ9tWMyopUsDPtqiq
+sM/bt85IzbG43jggIigaGTnMwK6lnZyv7d8+Jh4kQtDGqp+muHtQWT89w6qotMs8IyAfGR4+
+zrKloJyeu1s8Jh4eKG+6r6Ohr95CQURIyqukoaq3ThwXHRgXL8Gon56amrg5MiceHCT6sbSq
+o6rFPTxOUsyspKCpu9UkExgeGh9sp5yeoZyiTSgjIh4eMbSpq6imr04vO+jNuKaen6zDRhsQ
+FhoXJbeempubmqotHyAfGx9Iq66vpKW2TDVV2W6yoJ+iqrr1Gw4XHRkhvp2Ynp+drCkcHiMf
+IUOtp6yspa1IMEB01LynnJ6orcUyFg8aHxonrJuan6OjvR8bIScmKt+lqK+trLtCLUvO1ryn
+oKKsrbBHHRMZHhodwJ2coKCksyscJC0mKUmvpbG/rrFRMTzMusK0pKSvs62xTiQZGx4bHE2m
+o6upp7E6JCcsKytDtquysausv1JSzt5U1rivr6+opLFOKRsYGBgcOrappaGkrU0rLCspNGu5
+qqysprJRPD9JR03Gra60r66tr8RYJhcYHBgcSqyioqSfpkooMDUsLD+4p6uxqKvpP0Y/NS41
+1ry5p52coLE7HhEPFRgfy56ZmZyfqTkdICcnM76gmZ6pr+cqHh0lM0S+p6CenqOrvkYuHRET
+HiUs0KObnaesskwoKDlUTm2ypq/2We08Ki1KxsLFtKuyw7uyv9pWLyMcHSctNL6jn6Cmrbo9
+JykuNkFmuquuwsjLTjg0O0dDTcm9vLavrrW90DomISMqLjm8pKSnqrTWNigsNTlJzbKss8LN
+6Uc2MzhBSmnFu77DwLizvMG81TInKi0sMvmvqa+3uMc+LjE7Pj1Jy7a7w7u2wU41NDo3N1LB
+trS2sq+2vrW7RDExLSstMlHEy8u4tr3VUFZbOzU9R/3Lx7q3xvBQQEFFQVXIvLe2ube6vrO7
+NC08LSYpKD/KXsOoqK24e29eLys0O1fPzLOqsLzJa04+MDVJWfLKvLCvvdT/RTgzMjpNd9TL
+x8DPST8/P1R90Liwt8DaXlpJPERYZvRveNPbYWRaWmlr9tTW53Bo7dzc2N5+bmllYWr41sfF
+z+hfST48P1Ds1ci/xs3fYFJIPj9HUebLycnN2vZgW2zsfmby2tLMy8nAxNpZQjs7OzxN4se8
+urzB2VVHPTg9Q01s2cnEyM3R42FZVldodvDUysjExtDlV0dCPjxDU/7YzMK+x9Teak9MTU5b
+bv7b0NXV2OhyWExNT1Nl5dLIxcbI1WlVS0JARExt29bPy8vY9WlnZFlYYHzr6OXh3eF6bndu
+b2pgcPjt3Nzg3ej28WFVWVZWXWr14OPj4PVvaVxZZvvr5d7f2+P8+f9ze3x17uvv7/Z7a1tR
+Vl5gcOzp5ujq6u7s7vdwYl1ifPTf1tPV1+l1YVRUW2fv2tLU2+lvWktGSUxUbt3OysrN0+lj
+VlBOTlh33tHMys7b6GZWUE9RWmP729PP0+H2a15bWVdcfd3Z2dja5XtkWllaX3H45+Db1dfq
+al1ZVFhfa3L15uXl5el2Yl5l9uPh3OH09X1gYGZnbXfu5+Lc2Nrh73RfXWFeaHF79+/r9XBj
+a/9zfOrm39zb5HtqZmJia3ns39/t/m9mZ2VhYmtu+e99fPhzZ2dpbnf27Obe3d/e3+Tq8f18
+fG5iYWJgaHn77O93dXVudnt9+nj38Pr5em92em1sdP3y6+nf4enp7vprZGFlZXXxfG98+vXv
++vPu/XNnbX36+u7j5e3u/mVaWmJu9O/m6/Hq5+n1/XVqZnLr6+3r921rZmZyff/27/L8d3h7
+b2Nha3ZtbX3t6N/c3+Xt+n1vZWl59H737O/08P1vcf35d3l9dG12+vL1+Pl6eHZ0e/Xw+X38
+/XVubnt1aG50b3L783BxfP7y//j0fv726d7f7PF8ePjr6uzw+3RvcW9rZGdsdnZ2dm1lYGNt
+8+rr7fPz6OPd3u9vZ2BgbXv58erm6OHj8WxfYmVkbP/t7e7zffj+dm5vd3n5fnlvdfv9dWz/
+ffz5+Pbu4/NsYmd37OTt+3BpZ37r4d7j92ZdX2589ezxff73+HRpaGp/7ero7X17dm1wevfz
++nZvbWptdPrs5tzZ3uxuXFZaafXj2t7s/XFrampvd/Tv7uz7dXFye//p6HJWVFJRZtrHytXi
+XE9SX+/W1OT5bGD529jc5HRbW2T739rb6GRPS09WXvre2tbV4H51XFBXXWfey8rR51xOTVBc
+7t3l6uni18zO7VNISFrUw8DH5E5GSE9+0MvQ3G5XWFpgXlZPTlVo4dDN1fxaVVdbbff45uTg
+1tHX63leUVzjzsG9xeBPPzxHcc/FxdJrVE9Y99rb5WNIPD1BSXLJv77C1lhJRUhZ2ci/wNFg
+SUNIaNDCv8fXW0pKV+vRzM3T/llb9M7IyedCLikrOc+up6m1XDEsLjzmurS4vt5KPjs7QV/J
+u7a5yl1EP0/Tvbm8z1xGRW7Cur52MiQgJju4paGnvjspJy1EwK+tsb7mSz46ODtI3LuxsLrd
+Rz0/YcW3tr7aV0z7xsXZQCceHylwqJ2eq34qISUy2K+prLXQSj88Nzc6RN63rq610kM7P1vF
+uLe7y/T03drgRysfHR80tZ6anbA4IB0iO7emoqi7UTgzNTs/SufAta+wvm9FPkbkwLm6xN9x
++uH8SC4fGx4wuJyWmqw7HxshObmjoKi8SzEuMTY9SvvDtrG1wepVWvDOw7/ExcXL1OlPLR4a
+HSy/nJWYpkIeGh8zvKajqrlYODQzMTZH78C1uMHPePXGubi8z1NN48bDxE8iFxcePqKVlZy+
+JRseLcamo6u8VTY1Ojk3Qf7Ju7zVYWHbuaurscVMPk/lyLraJBYVGjOllpadyiEaHjC6n5+o
+ukgvNDg2ScvCvMNGNz1VuqShqLRZNDlQ6sTcKRkYHTeomZmgzyUdITC/pKGntkotKyw9t6Wk
+r0IgHCfanpSWotgsJCo4STYhHCE+q5ucrjceHSzDpJ+nwkY1MDhEb7yspqm/MiEgL7edmJup
+5S4kJSYeHCE1tZyYnbkqGRklaaacnq5LJx8nRK6dnalUJR4qy6Ocn67Kakk6LR4VFSO6mJGa
+xx8VGTarnJ2uTS0oKjNIx6uho7c4IyE8qJiXn7VALS0rIxkVHVCdkZWqKRUTIMmemJyvPSUc
+Hza1m5ae4x4VG1uckpSfwTwvKyYYDhElp4yKlfMVCw8qrJaSnLY3HBggP6OTlqsuFxYvpZSR
+m7swIB8lHBceOauVladCHBUfVK6enaa2PR0YHkWdj5SnLRUXMqmXkperLxoUDw8d1JyPk7Ql
+FhUotKGepb7fUicfKlWhlZqvMhocO6uZkpWkQB0TDAsX4JiMjqkmEw8beKSYlqC2MBYVIsiZ
+kZy7Lx0myaSalZzDKRkPDBU+oJGRpi4XDxQst5uQlKI8Ew4cxZaOmL0sHiFqo5iSmbslFQ0J
+Ek2ZjIudLxUNDx9cpJWTmrYdERczo5SVotAqJkKwn5mezisZDQwWTJuNjZxHGQ8SHj+qm5qe
+wSYgJjizn56lwzI/sZ+an2kjGA8OFzumko6YtikWExwtxKSfoLU9Oj89RvrDrKm7w7Wnmp3D
+LRsPDBEns5aOkqFKHBMXIk6rnp64PT9ETtXOy7zL77OknZqm2S8ZDg4ZNaybmJupTB8aHSpU
+t6apydb7RWDPzLu+bMSon52kv0clFBAYKFqpmpibrisZGR4v06umrbLIPzk/RNu8vratrqed
+nqwsEg4PHDK5l4+UpU0kGxgXIcOfm5yp40MwJzBZuaamr6ukrcQsFxcdHB9dnpGPm7xWKRQP
+FSm4nJmamqwrGxwn462on5uht/ohEBQcIC+/n5WUm6avOxkSFB06xqeXkpu2OCIeHy+wnp2e
+pb8iExYcHiVJqJqZnJ2guikaHCAfKsWinZ+nseAqITB32Lumn6s7HR4pIR86r5+hrK6ruz4v
+OUU+NTpqwrKuvU04LywvP9isoaavuuE4Kik0aL+9uq+uvks0Ly8uLjVN18jExsO9w/JKQk7R
+xMa7sLPFVTw7PDxIzre3w+VfZk86MTE3PUlkzry4vs/y+epzYu7Hu73Dy8zaTT49P1DzafDc
+aFBGOj9GPVLXy8u7rt5St9k/YcTG3cS9wsbeQDxKNCoxOzw9RcvC18K6vL/KvbS/zs7UTjY8
+b+nTt62uu18tKCYdHy04Sty4rbCup6zC0MrCc0Hx0Ovd2c/abdG+7dTNNzctHyIlJjBOtqmn
+o6WqsLm/eUVKTlfvzL3AzMTSTkcsKTInJi4xOUFYy8e2qqimpaervuZtPjc8StvBtK252F0q
+KCgcHyotNELNusi2sLOurqussK60zM9nQT9DUmhpwuE8+DAgJyUiKDbszLSjqq2psbu9xL7F
+vrfIzcvZ/ENJSiouPycrPjMuMkE+OsOxvamhqayruPNe5VRVwb7Hv8LWRS46LyAsLyguQU9J
+0rC6u6mtuq+tvr+zwFz8+Edc1O1o9z8+NCcrKiotNkZT3cC/t62tr66tsre70HvoX03q41nY
+3Tw/Ry4rLy0nMEE7XMK+ubGttLOtt76zuM3N2E1LT0tHbmU/X0swODgrLDY1OF/QyrmtsLas
+ssK4vdDWyt5Z4fFRVl8+OV4/NUE+NDNFOjdlY/a5travrLC1tcPQ3vNWV+JZZf5eST1LPS88
+PTA6Szw/635iwrrEuK61tq65xb/PYF1lTUlUZVVJW0w5OjozMzw/PGjW/Mm4vruxtr23vM7F
+yOLy411OXFdUS01uRTxCNzA0ODY/anzdure+s6+6urXDz8bTY/j3VV/v9VVSZVM9Pj0wMDc1
+Nkxv9Ma1ubevtr24vNXOymlb8l9UX99/UGNbPzo+Ni82ODVCX/fVt7K5rq68vLnN8MrkT/jk
+XnDR3lxVXU43OjstLjYzNkt36sSztbSsr7u3vejl4lZQ9d19z8HRX2tmOS84LyoyOjE+5urc
+ubG8sKu3vLTHatlxR1Pc7W3Ev3Vz1E0xMjcpKTY0NF3N1cKusrutrsG7t91b31VEVtxt+7rC
+YO3gQS4zNCcpNjE138DIuKmutquuyMO+WktpT0BQzt/Tt7r2bdxAKSwwIyU2NjTVtry1pqm2
+ravI5MZaOkdYREjFxs+ys9Nve0EpJSwmHy4/Ot6urq+no663r71PWFU5NUNRTty4ubWtu+RV
+QSwfIyYgJT1R4K+lpaejpbfCwEwyOToxNU7f0rWrrKqtw0s+Lx4bIR8eLWfIsqOdn6Whqc5b
+SzApLDA0Ove5saunpaWtyTksKBoVHSAfML+ro52ZmqOtsfAtKSgnJyxC4sywo6OkpaWnxyoi
+JRkQFiQoLsCempualpy6UkYrHBsmLy5Dtqusq6Cfp6+urtsfGCQdDxUvSEu8nJGZopydzCUg
+Jh4YIGG9xbSin6y5p6K04bu6MRkaJxsSH1+4sqqZkJywrLIvGRojIiA2taaqrains72vrLW/
+uscqGhwhGRgp4a6poZiUn7PBRiYZGB4nMd6wpqGnr7a3sKyzuLK1ZiYZGh8aGizKqZ+dm5ie
+uUQtIRsYHCtQt6ymn6Kxzsq0rba+uLHALhoZHx4cJkyrnJyfnZ+wSSYeHhscKFavpaSjo67K
+68axrbC2tLdcIRYZHh0iMMigmZ2foq3NLh8dHRwjPb+poqKkqbvf1berqq60srs7HBMYHCAp
+N72fmZyfqrxZLh8cHB4sYLmrpKChqbzVybWrq7S/v8M8HBQWHSYxPMqlm5ugr9xAMSgfHB4t
+2LCoo6CiprPpWMmvqa7IzsRkJxcVGiIwP1a7o52dqc1EPDIqJiImP7yppKirqa7Dcmy6qaqz
+wsnXMR0WFhsmNkrGrqOeoKu+Xzo0MSokJzy+rqyrq6yxucC7ramuve9OPScbFxkfLEB9xLWq
+o6KpuuhQQDUsJiYuU7ysqKioqa2ws7O3yFlCPT4xJiAfJy47P0RUyLCpp6y1xnZJOCskJC9k
+sqaio6ess7zE2FxOUOu/v1guJSEhISIkKTfNqZ6cn6evxFAzJh8gLFewpKGlrra7v8bS3sa5
+tLG9QygcGBgaHSMuUa6fmZaaoa7YPi0jHh4lP7anpaiutbu9u7e2trrCyes4JRoVFxwiLDtq
+sZ+alpifqsFMNikhHyAqSbmpqK20t7evrK62ylxXZEUsHxkYHCApM0bLrJ+ZlZierM1NMyUd
+Gx0rVbKnqKyvsq6rrrfJW1b15UssHhkYGyIrNU29pJmUlJmjt1YzJR0aGh8wy6qjqK60s62r
+r7vM2MjDz0onHBkYHSYsNl29pJuYmZ6rvOs9LSMcHCM1zrKyubuzqaSlq7jN4nZrcD8uJiEj
+IykuLDZOxKmfn6ClrbPKPSwhHiYwTMvBurGspqSps85KQUNDT2PPvOY7LyQgIyMnM0fEqqOf
+o6y2xWZINy4yP27Gws/IvLezvelSTnfKxdV21MXDxFwtJSQkLTczO1jQt7HB0dvNurW4vMTN
+zdfvcmJu/v9wbOfPwbu6wsvX5u53+ttdLikrJycoJy1BV9y9tq2oqKmutbu/zvlRREhRZPht
+8+nu4c7JxsjSyb7A2FRDOywpKykqKi45VNnDurOura2wtry/ydtXT1lq71FBSu7NzdS9tbnM
+6W57d0QxLTpLRTo4RV9eREBW09Dc0NjIwL6/ytfOvsvpZGVj7Oj31c7CddvhbzxFTUpKT0Lw
+wtPj9MFu+UXtRkhJRGrX62HdfG1RWtXW2s3Iv7zVaVZgWz88Q2971enOyL/Ov8LGv2JGTk5K
+P0NLY3dFUFBK4FNt3ta5Y719vsFKZ0FWT0BFR9901/XDvrnUytvC4kNXSlhM30bUUW9YT2Z0
+XnlP3dXAw8zD68lCXT1PSkRhRMlhvOPB5sZgTFxa/VLcctL9x0fjSmVAVE7Vb9fTysHfxkjD
+RXFCaX/V6+HX2WnY7floamdJdl7N49p33lJdT1ZS70hZ7Mb4b9JX00p4XdVv3NTN2sTbW9Na
+3lFaSu3uT9395cpfWvlbYVnmWM9h6Fn5699W2ldfUPhyz/Hk4M7h42zVeHD+Y91lXmxhw0z2
+U89U1U3dX+JudGt83EpxTdtaW1j83t3p0ufZ8G7v8N338lvZaOh6497f3vpt2OTwemvxXlpp
+WmpOZlFmW3HefHns7V1sbXrw7WjQe9Tz5NX8z2rTXsxb0VfXZfruSdJBxzzbX1DSUMhPzV36
+fUzmQ8dGymPtzUi+Prc8t0vT0mnBRLg7tjO/Ocs9ZuxAuTm0OLU/x0flek7NQsE+wT7IUe3e
+R7s8tT6+Wc/NS79Auzi9PMhD0lNZwju4PLk+vlNkzj+7NLw/0FZLyzq7OMJIz+xrvUm3QrtA
+x1FSfkbPObg7wFDKaGLCPbQ6tT2/T2/XQ8I5vDq/P9P0S789tTq4PMFL7u9EwTm9OL8+yF9f
+xUi7PbtCxGN91WfOSs1Px0fISMxT3VRi3k7JRMpEyUPMRthMz1LVW2/bWc1Ov0vBTsNJyk3O
+U9hs1PPl62z1am1Pz0LPStNP2Gtg10vJRsNAxUTJS85addRY1FrlXtxi+eXlfNFjx0/FQshF
+zUPVSf/eTtJIvjy/P8JNz+Vh0VDEP71CwkTMUN3zUMlIxUHERMhX2fVo407aTuFJ01roV91g
+eW3wfdJmy1vSeObiXM9Sz1XPUdZm417iYV/fT9VG11rlXV7ZTctN2FDbXHjrZ+T+3GfSfNl4
+1tlazVLMT9Jd/X1c203aT91L5l9ja9nnXnB71k14btXmXOzvz2P14PfXXuFw935t7WHZVtpX
+9VLYRnrvzWJj2dbfSOty2FX8Y9zoeGfw4t/hW9x201jYXGvvZVf7deZZ7GV731bNStRPy1np
+52rZ6n3f799l3+li21TeXF1r+mB2cGXv++Fa3l/hbfJY8Pt5W9xt/dj/a+Ln52nhatx9XNxT
+6V/u+XbbXdVU3GH4a2V2YfBa42Lv6+Tu8vRw5Fvq8/X3deRu6F3cXunjX2xkf1t4aF/t+1vg
+ZeXz7e392XHfaed7cX5l+m1ve+777+318uBn/Hn2+XB2/mZ4emxrYXxm4Hb6bur+/e189+Z+
+8nTn+3Xrbmrx6Wn38XHybOVi/mX78HxyXnf+altd4eBp4M7Y+/3ZY1pt4vNe+nTveXHd+eJ8
++WH7WWxdc1xu6nBuaetv5Hnqbent4nj44vz2/Nt7/3Xw+fvuaelrcXpy+mtoePVp7l5d1l/z
+1X3l4mvj8mVpbe3r/mDYV2XIY1L91WBXX+77X13scVh+YuHbV9fef+Fk6N77de3g9+rd29PZ
+2tbe3NDOzs7iUT01MDE3P0tX8dnIvb2/ws7b+G/3cHBq69jWz87LyMjJycfBw9xGMSspLC87
+Rlhy18O6tLW3wNVxUVFWUFNaXO7czsjGw7+9vb2/wMTkQC4pJycrNEJl39fCubGvsbfCd0xA
+Pz4/R1TpzsO8vL29vby/v72+xmU1JiAiJSs7YcnEv7ewrK20v186MC8yO07qwrm0s7S8xMzO
+x8C9v77JUS8jHh8mMUrRvLmzrqytssRPNSooLDdPxrSurK+6xd1m99PDvLu8vMJTLR8bHSU3
+z7axrq2sq7DJRS4oKS493r+1rrC2w15KXti/tLS3ubi3wzofFhQcL8ysp6ipqau3TyceIC3l
+sa2tsbi4xFk9OD/RuLe6wb+xqqq/LRgRFiE/w6+qpKCksUIkHSM8vqytur+9xNlOPEbWwL/P
+XWDLtaqoq7w/KBoUFx44tqCdnaS07DUnJS1Ovq6wwtHd6O1cTm/PztZaRmW7q6WlrMY6JBcQ
+EyD8o5qbn6rDQiwiIi/YsKqxzWZnTkVJWce3vu5JR+i1qqaosMc+JRgPEiW2npqdp627SCke
+Hy+9qaix+Do5Pz9ay7ivt/0+OELCqqOiq8hAKx8WEx1Vpp2do6u4TCkfHy3Oq6mxzkk3PElf
+vq+uuGIvKjThrJ+gqbnrPy4eExMjvZ+cn6mtvTkiHSNCtaitvvhLOUB0yrWuum44Kis/vaad
+n6q4z0ArHRQUJbyfnqCrr8YxHhwnW62lq7vVXz0yP3q5rrdbNy0uSrqmnp+rt8VGKyAZFB/l
+p6GhqbPJMh8eK2qtp6690GJALjPgsKmv7TYvLjfPrJ+do7PVRDEnHxkZKsClpai2xlIuIyhG
+uKustsjWVzsuQLaqrc47MDMzPcKmnJyn2TMuMS8sIR4r3bS3usTAxkwzN/6/u8TO2M3XXj5e
+r6y7TS8qLzRFs5+cn65vPTxCQjkgFhw1wbKtrqqs9y0nMErEvbu5trrKRDnSv+tANjNDW2y0
+op6ir188PkxJOigYGSnWr6qoqqnNLiEmO8Ovr62wuNU8KTBf19ffU0liUditoqOqwEY+TeBP
+Mh4WHj+zrKSlqLczHx41yK+ur7K31jspJj3Eur/JfltPRs+qo6auzT82QHn/Nx8ZIlOxrqyt
+sdEvJSplt6+ztLbCTC8mKWuvr7zJXUM7OdWpoKWzXzk7f8HlMRsYJWa2taysq8I0Jy7mu7e9
+v8PZPzItN8Grr7/gPTU0PryjoKq4cUJVzr9+KBcWI1+zsKijpcItIClnvb3Gvbq9VDUuOr+s
+sdBWODI4R7ujn6ezcD5J2clPKhkWIWCvraajpr8sHyhPwb+/tK+1UzAsNcqutdtfQzk2O82l
+nqWx3UA+YehMLx4ZI16ysKuoq78uIytixcnLurK6TTIwO82zuM7eTTczPMeln6avxlI/S+h0
+OCIZHTa8s66mqLE9Jic8eGptwrKzykU9OkzJusDNaTo2P9asoKOruWI6NVHFei4dGyVdu8Ov
+p6nGNSkuTUs9c6+oq8FHNTVG7s7QznZDQFDAq6SnrsdANkjLx08sHR0uVlzXraSnvzcuODgt
+McinoarLPzQ6REdVzsdvRUJqtainqq7DRj1Jar/KKxwcJjE5XqubnrRKMSwnIiu/oZ6ltmhC
+PTEuQdjS42fft62vr6212kM7Ozlxra0wGx4lJC5jqpueul9MLiQmN8Cnp7S5tMs7Kys8b/XX
+ta2vtbu7vc9cVU89YK+osx8OGS0oL7Gdl59JN3A0ISdAv62wuqmsXTw7OkJIRtG0tbu2r7G9
+705MPTzFs7m3VhgUJSgkP7Ojna5pvr4vJC1Rvba7r6i7REREOj1nvLK+y7e20U9txb/O49Fc
+Qj5ISi4bJDstLdawq65m17ZtMz68rL9nvbTOQjhPvshyxrvUTV7Vzci+trXSQDxCNTZOyNws
+KTo0LjpG3bzVvqu32MvL3WlJT9LJ19HEvsTP+FVLUtjEvry9wcpVPkE/OD1GRdLJTkdGOC4u
+MT/NtrG3uL3qSUZX0cTIysfDyN7z9Nzv39Rru8fH1mXbRkI6Nzh6TNrWYdpnRT9OUEbfzczj
+bF5VTFXZzcbPbcjcd1XWwHztzl7D8MHWV8NLWl1OUnB+bVReSljNaOlk41/f81HZV9BTTllk
+cHb37m1x2kze3drO19VvwEfdX1DWP8lS7NBKzD3U3vbCTMHgYOJH7mRjeUW5P8XdWMxqWXJj
+Tldl2uPa4d5dX0ddYWhP3/nqy0LdXlngatrJ29/R32nfUufT7l3DVMZP4t1R00zTRORgU8VU
+bFXYR1dMSlzfW2a0P8JhVU3QTt+8YcbKTMxcR9fbUsXXUctMzE7MR+3aRe5H3cZU7M3xRk1p
+PdJNz9zIdXTY50Dk5uDY781twk92eW//aGjc6e/Rfs1h51BeQlBO+XvfVMFPXd44uTbU4uu/
+WrxOvj3IQdbcVL9+yWhAwe85v2ftx15OxEZI1j7ITz7AXk3dxUa+Vz69U1VPvkv2zEG5R1PL
+U/js4le4aEjP1Wr1YbhO231HyENQ5u9k3kzny0TlXMpRZ1Dl2VB8adFJWdFmac9L1sQ/xH7N
+Y+5rzG7z/cvaSsZDxj/VTPvTPcRDzXxExlZmYNBOzF3iZNJN79tPvj9s1/1dfcdGxlpvxEvT
+TsVQ991S0mZM5PVi6W9Hxl9MyVTi5FjNTNruSsvXWGLNXm/dWN3c5EK5XVPJSM1V2zzF3Dm+
+PMPfTMzr5F1uWXHSTeXVdmDvblTNTV3KcW//xlR462f5Sbo/e71H62a+NMO/L7rtP86+QlO5
+Oky5P1nHRctaaOb5TNPoRs/LTNfW/V3hYMzzTMB+Yc1eXMNST+TuUl5bZ89KauNxeknZZ9rQ
+PsHURMhPY9ngP8bLVtblZezXVWff6Et1x1Jo1W3aZ3HfWdVS1krjzUDh0H5d5fBX1mFKx3lO
+39JRYMhO59B78mPPSurcWGvq7VTr7Vvs0FFtx05n3WVcdelkb9h/Wu7d7X/r+/LVbPXl9N3x
+b2XbeFhczmRPbN56R+LVW0/j6lXy+edv83Tk6nRubddhd+ns5Ojv59Z/6vjmbG/za+lealv4
+9lpqcGX/8V5oduVfZOz03m7o1eL+/utkbfVyc2/37nfr5mn57fDtdOlq93V8a2/yXO5bePln
+9nzt9Xr26m5qdvLgc+nk6+146nB59nbpZ3T8XHZ7c2lv+WH8ZnxmX2xbb3Pnfejc/dzn1Nnc
+2tzW5+bf7Of77PL+bWNWTVVLSEpGSk1QWW5q7tzk19TP0M/O287e2s/b0tPR2N3Y3t/d6ePp
+5u1XTkY/Pzw6PT5ERl5od9XazM7Nzc/Q29jc29ra2drU3NPNzdTPydHS0djo395rT0VAPjs5
+OTk6PkZQa9/Uz8nJzdDW3Ojk3ePi393Y1NPNyMnFwsTFyMzU3unk8VFEPjk2NzU0OT5IVG7d
+083KxMnJy9TY9PLx++/m5e/azs3Kx8TFyMnM0tTb3NxuTD48Ojc3Mzc8Pk1h8NvNysfFzM3S
+3eZx9nx+9fnm29bMxsO/vr6/wcfL0d7h8U8/OzczMTAyNztDUWTt1s3GycvL0NjZ3t7Z3t3c
+3NTQzcnFwb+/v8DHz9bb3uFaQjs3MzAvMTc7P09o7NnMxcXLy83e5ubo5urr4NrY0svIxsC+
+v8DCwsrS19ndY0Y8NzUyLzAyNTtJXnrezsbGxsLGy9DV2+Tu5eDn4NLNy8O/v7+/v8XM0Nrf
+9lhDOzkzLy4uMTM5SV/+2MjEwb++wsrKzNLb2Nvn7+jc3dTJw8LBwMPHyczV3OhZRT46NC8u
+MDI1PUpUadzKxsW/v8bMzdHX19bX4uPj3tTPysjEv76+v8PHzNpfRz04My8tLi8yOEBKWt3I
+wcG/v8PIys3R19jY3t7b2tbQzMfBv769vsHHy9xYRD05Mi4sLi4xOT5GVuPMyMO/v8PFyM3O
+zM3T1NHU1M7NzcnCwMC/v7/G0N9SQD86LywtLS4xNzxKatzNw77Aw8HCyM3OztDR0c/Ly8vI
+xsnHv8DEv8DR7PFLOjc0LisuLy40PENU3srCvr28v8bEyNnSztzk09LXzcjGxcK9vcC/w87p
+dU09ODQvKy4uLzQ6RU/rycXEvbzFx8LKzs3N2OPQ0NfOxMPDvb/EwsDH19dpRz03NSwsLi0w
+N0VKW9fKyMS8v8fCxM7Zztbv2M/Tz8G/xb69vr++wdThZE87NzUrLS0uMTVER1vZyMjCu8LE
+wsbU3M/e7dzS0s7DwL+7uru6vMPLfEs/NDQsLC0sLzQ7Ql3fyMPDu8PCvsjOztHe3drU1c7D
+xr+7vLm6vcLO/U49NS8rKyorLjM6RWDjw7+5try6vMTFydTf3urr29DHxb68u7q9vMtyXj85
+LysrJyosMDVJbN6+wre4ubK7wb/E29HS6tTRy8nGv7+9xcTPU29BODIuLCctLDE4QGVnxcW5
+uLmyvLu8v8vIxt7Ix8W/vbq9u8bMd0hGLy4rJyYoLSs5PlXfyry8tLqytb63v8jKxdHSwci7
+ube0uLm/5lY+LyomJCAjJyovOU34xL+1srWtr7a1uMnFwtPMxMK6tLW1tLnifVgwKyomHyMm
+JSwxPkn8zb20uaystK+vvMm6xs+8uri2tbi2w2nXRiwrLCQfKikoMjxCS3vVvb66rbS6srPD
+wbnAv7exr7a3sMpCf0UmJjMnHysvLDJATlttzba6v6+uu7y3v86/u7q4srC1sr9ZVjwqKC0l
+HyotLTVEVOXMxra0trGus7u6wc/PycG6t7q3tdJdYDssKislJCksMjc9XsjCv7Kur7GvsbrB
+ycrM1su+wL/C811cOC0wLigpLjE6PUN1x8PFt7CysrGzt7zIxsDI1MrG9EQ/RjcsLC4tLTA5
+TGr0xbeys7Kvr7K4v8PJ2v7v2d3rYkI4PjkrKiwrLC8zQt7KwrWtq6qrq6qstLy+y9jsZlRU
+TzUrMTMoJScnKi4uN2TJvK+qqKOipqalq6+yvs3P+kY/OCspLSgjJSQkKSwtO2DVva+rqKKj
+paWmqquwube6zfBUNCosJyEjIh8jKisvPUzsvLGspqOjoaOoqaqzur7K3Wg8LS8sJiYkIiYq
+KjE9P1rAt7GrqqilpqiprbCxuczhVDYwMywoKSYmKy4wO0RN2b67sKytq6irrq21t7fB02Y+
+MDEtKSknJSguMz9XW9W7s62qqqqprK+vtbu8xNX5RjEtKigqKSYnKy02S1/iwrivqKWlpair
+rK+4u8PUfUc1LSkmJiYkJCgsNURP9cW6s6unpaWnqautrq+3wNJONy8qJScnJSUnKjFBSm7R
+w7asqqmoq6ytr7Gxt7zLUjoyLCkpJyUnKi04R13Mw8C1raqoqa2usbi3uL3A0kc1LyooKyko
+KCktO1Lhwr25sa6qp6irrbS6t7q8wOc+LyslJScnKCkqLz9szLi3s66tqqipq66zu7q6ur3b
+Py4oIyIlJSgqKi89bcW0r7Cur62qq62vtbq6uLm2vmk4KyUgJCUmKCgsNkrVuq+ura6wrayv
+sbi+vry6uLfMQjAoIyQoKCorKzRJ3ryvrq+usLKusbe7xc7Ivru4vWw4LCYkKCopKywuO+q8
+sKysrq+vsbC1wM3V5828vL3ETzErKScqLisqLTA+zbWwrq6wsa6us7rG6V1p39PMy95bQzc0
+NzY4Ozk2NjxN6MzCw8zV1L2ws73DwcXKy8TCyMTA1ExBRz41Ly0sKiktOElx29jMvLKtq62u
+s7q9vbu8v8vU4mRENzAvMC4rKisuOERzx76+ureyr7C0ur/Ew76+w8nKytFyRzcyMS8tKysq
+LDNE7MG5trW1s6+ur7S5vsnW19fOysjNXj80MC4tLCooJik0Tcm4s7Cwr6yqqay0vt5gaP7d
+zczR2f5LOjQwLCknJycqNUjdvbSvraupqaqttL/ab2FXX+vY1NLZcUk4Mi0qKigoKSw2S8mz
+rKioqaqrrK+5ymdHPz9FTmPk0s/Vdkc8MSwsKikrLThPyLStqaiqqqyvsb3ZVD87Oj1KV+PF
+vrvAcEQ4LiwrKSkoKzdQv6+rp6ipqqyvtcV1QzUzMjZCXcq6tK6xvOhCMisnJCUmKTBD27qt
+qKWlp6yvuMbuRTYuLS47Wsi0rKqprK67LSIuIhokJSAwUsimnZ+dn6qy2jstJSIjJzBMv6qg
+nJiYnKK8KhYPDQ4VHjG3pZqSj5CWpGcmGBQUFBooV6abmJeeqq+6tK23xlwuIRkUFhkhPsal
+nJmXmaGyRiIbFxgeKD62p56doqm9X17fuquqq6y88ioXEA8UIVqqm5mZm5+s1yoaFRMaLOar
+n56fpK6940hK3LesqayvtNMzHQ8NDxlLqJqWmJygqu0tGxIUGzKzop2fqa+5wNlXRUntu6un
+p6iqt0oeDwwNFTaqmpWYm5+vdCoYExUeV6idmp6prsNYPC8vOmrDtbOxq6KcnapNGQsHChMx
+p5qVlZqdqlAkGRMaK8CemZmcp7hbLSIfIS7ptqqqrqqknp6sPh0TDQwSH0SlmJWTmKCxRSQd
+GxwtbKucnJylxDYmHyEqN+a9uq+zsKyrqrXcPS4vLiUbGiA4qZqXm6rMPS8oIyEiMsejmZme
+rVYqHRkZHzHCopuboK7D0srPeTwzU7+6Sx0WFx/RpZ6hsOP72k49KCIxxp+YnKxDJiAiIiUs
+Qa+empyr2T5D2bSvuLm1r7E9EQkJDTeaj46VqLzNNiUZERo6o5GSnc0pHyYqKjZLsp6anKpf
+Li43SeVb1rGkm5yzHAgEChutk5CUnKy8+ScZExY6nY6Nlr0mHBoeIyQ8sJyUl6lKKCAqPlbJ
+u66fnJ6qPhQIBg0ko4+Okp7AQC0fGhoku5iPkaI6HBcaJDdWxa+loKW5Py4vSL6xsrvEvK2l
+p7kuFQoKFzqfkZOapLj7Ox8YGB/anZmdtDUoLz9WTDo76bKmpbPc37+vq707KCc7taGepuUf
+DgsQHr6YkpOarzQiGRkhOq+bmJuo5yoiISQtSbaim52q6CsmKzhc7VhW37amoqSowzAbDQoR
+K6OOjJKkPyAfICAoOb2dlpmoPR0ZHy7rtKypqrDNOSopOcutqrdVNjvLq6OhqcMvGA0OG0md
+kZSctDQkIB4hMNSlmZmhxCkbGyI5tKSfo7dILCYrUbepp7ZrPztUt6qop7NOKBcOER/fmpCU
+n98hGRsgNb+mnJidsi8ZFRsyspyanq9AJiAlNr+qo6W0XDQsMv6uo6CktEYhEgwQI7aUjpao
+Qx4dIypJuaqgnqjKLh4dK/mtoqm8XDUvNDzfr6moskkqJSpZqJyboLRNNCMVDg8ftJGMkqsp
+FhglQr6ysKmiqLwvGxgj66KZna5LKycsOuq1qKSqzjEmKEetn6Ct1j9EUzIaEBUls5eVn9En
+Hy/WuLncV8Wxsco1JClvrKGnyjQpLUXKwb60rau0SSYeIkWpnJ6ou9vdeCsQCg8io42Ml7km
+HCg4Ozw77aOZnLQpFxo1s5+ivT83OkxoSuizp6W3Lx0cKbmblpqnz0xBLBcLChi5joiNpyoV
+FiM4UsqwoJiatCUUEySwm5qmZC8uMjxK7rWloa09HhkgW5+VlqDINC0uKRwPEiylj42aTB4Y
+H0m5t7avqaOtOx0ZH9+em6PKLyszPUFDUbqloa4+Hx0sv5+bobROMjdJYj4fFBxFqZmcvTgu
+L92yzkZP77eqxTIpLFGpo6/XODJBSTtAWb2lo7JDJB8s366mqrW6u8bR6E82HREYLrycmqrJ
+aDtOcC8oNuKroKx1OTRAy9VEP1TEr7hZQ1HArbRfMi02fr+9x8q8rq/LPzdFa0QiFR06sJue
+st/tR/JMKytMt6SkwDovMj3zXk7PuLG03zc9Y8rGcklxx8XhOjJKtqanu0Y+5rWyQxcNGT6m
+mqPEyrvJxjMeJFOtnqXeNzU4TnA9T72wrrpCMj9Ib8rFt6y9OiYjNrKipK7PWsi1vUwcDhMw
+r5yfuMCwtsg8Hx40v6mow0hk83JYNTbauLK4Wz9ZXWbRyr+zwz0sKj21p6mvv9O8ts45HREW
+Lr+op7Cvpq3+Lh8hPb2ytMHHuLpjNCovbrazu87d0/ZKP0rQt7XOPjI49rq0tLS3trjLOyIY
+FCBLuKilpaKn4i4kIjD7zMK4r6qt4C8pLULIv8W+u7zDZjs8W8i3v1E7PVLFuLe2tri9zj8o
+HBYaMsyup6ejobFFLSUoNj5ZvKuiorg/Ly82PTo+17Ksrr5NQEha2d1XSEvivbW3usG7vMxI
+KhwWHjj3ua6nnp60SC8oKy0tOsGpoqa5fEo8NTAuPsy4srK4v9NMRlRpaEU+e7iwsr3JvrrQ
+Nx8XGyk84L2rnpyowkQxLiklLFS2qamvtLxkOC0tO1Ne17murbfqTV5jTEFBacG4t7W0trpZ
+KxwYHikvVLKjnJ6ss8U/Kh8fLVrMu7KnoKfUNS0uMSwuW7Goq7W8udI7LjBGzMPFt62psek1
+Ix8fICEteqyfoaSlqLZLKCEnLDM8U72moqauwW9ALCQqOOK+vLGpqrXnPUBTTk9d07avt+Is
+HSIrJyox4aagq6qmqrVCJyotKywsQrKprKytrrNlMS4wNDtDfLqwtLe5ubnSUlhQVFRHRTgm
+JzAtNkzls6qysauus807PEE0LiwyXcXBurOuqrXeZ1tEODE2XNLXz8a3rq+8wL/TTzQrJSAj
+Kiw0arepp6ysqrC/bz48PTUuLjdU5NvHtKustb/Axu1GOTpCRUVV0Lu2uLi0tr9fMCUjJCIg
+JjTpt62moqChq7rJaD0uKSsvLzI/b7+4uLCsrbC8zNF5SD48PUlUa8/Fvr3UQzo9OS8qKjI+
+Slbcu6yprK6vtb79Rj86NDMyOERm0cbFvrq8vL6/wcjX61xMST49RU5ffGhYZl9ORD48Pj5F
+Uv3Pwry4tri9xMzqUEY+PURNZ93NxsbJzNjt6e3u3tnS2NbkZFZMSElST1BXVlNMSUxOR0ZN
+beHPyMTAv7/Ezd3mb1NJSE1TatrNycXK2WNYWF1nbtbMyc7P3upmVUxFSUlIQkhNT1FaXV1y
+2MvM09HO1NHM1N7p62lWTWJeaufZzc3P4OtxXGFZVffhzs/X29zqX0xGRENAQEdPYXnj2dDO
+z9Tf59/ybn7n3uTc1tns62hpaHD49Nje1PjseXlrZvb62ufT5Ndvd1tPSENHSU9OWWnr3dLY
+2uLtbl5kYG9+4dnNzsnP293e7GRhWnhie+/h5+bc6eFz6PHu925dVldNTEtPWF1nfObl4+7u
+7OXta2t+dWNod+PZ183QytjW9PZ5VmRaennr7djV1dns5+3ra1tSW1hYVEtRT2Jgamvt5+zp
+6N/p9F9oXlxuZOTc1NDMytnc3eJfWVNmYP3r3szQztrrdlhcVFtZXGBoYWVeYFxsZmfu8914
++3l5b3xhXltw9e7j3c/OzdfZde14aGph7HHe79Xb5N3uZVNUVVhdam19bHNlX2tedGFsf//p
+9vh2e3p+bXRt7OLZ3d/f3tjp7PV7cXd0d3vs4+Lj5/pqZFpWV1deb3x1++/u7Pl88XJvbmv8
+eP388n91/3Vvfvbh3dra4uLzaGtqYGBpbPfu5Nzm4+ToaGRcWl9eZW10e97f+314bGhiaHl+
+9ffx8/N/dXx79Pr57Ont6OLq+Pl1dnx+/m9ZYdbM12tafdzuVUtVd+P4dPvrblhr5N57WGjf
+09nkfXn6/nt4Z2N2fXz87nhnZ+rY7G3+2dDfdnD77XVgWGVuYlpk9PRgXPre5HZo9dzdbWRz
+6/h6ZHT1/mdw8+/q4d7i1tfb62l372JadHhsbl9ZVmXm6F5r5dXce3rY1XtqXmpjWE9m+2hr
+8OLzdfvg6Wp85+tsfuPS1vHq3NHX3+rc193k7eXqZlJNSEI9PD0+Q0tT9tLRzsnFwsrWzMXP
+3tza1+1kZ93O1dTSx7y9yMbG3lk6LiwsKSctN0540Lyvra+0ub7RVkQ/Oj0/RFffzMC7uba2
+t7q+v8C/wdFURDYsJyUmKS44U827sq2tr7fE304+NjQ3Pktrz8K7urq8u7u+wsDBwr67vcxt
+SzUpJCIlJys3a8C1rq2tsbvWTz08OzlAUNjDvsG+vsTM39nNysW/vriys7xrQzYoIR8jKTA+
+47qxrK2wusp9UT02Oj5FXNHDvL7Bxtp07dzZyr+5t7ayr7PHVDwtIyAgJSw1S8+4sK6zucLc
+Wkk9PEFQctfHxsDJ3PleXW/ZzMO6s6+vsLC1x005LiUgHyQsM0PaurOvsbm/021TRT5ATPro
+3dHJydTsaune08rBvLe0trq8vsLlSkU3KSUlKC80PGnDua+vt7vG4FxFOzxFUnFld8/JzNDX
+zsjIx8XAvr/J0NPZ3994ZlhEODMvMTY2O0hs2MbDxcPEx8nP1+Xr6XRcTktLRkZPaOvPxcK9
+vL2/xsnJy9PZ1drrYUo/PTo4NTY7QElYZH7XysO/wcbQ5HVZSUFETFNf7dLKxb+9vb/AxcbF
+yc7P1ub7XUs/OTg5NTQ4PUVT+9jNysTAxc3fX1pZVlFOUnPf39rY08zFw8K/v76/xMrU3+9h
+VlBMRT89Pj48OzxDUGNsbXbo187O1dfb2dXY7l9UW3FsVkvmvcXHtbK5vMDMztjk+GRQSDwu
+KisyODtEZcm6trm8xMXK7EhAPT9JVFRYds7Bw8vNxr++y9fe1M7O3tjFvcHaXUw7LCYlKjU7
+TtW5sKywusLPbk09OTxEWObp+NjEyd/t2MTBz9TIv77Dz9DFwsbhS0k+KyMjKDQ/SteyrKuw
+vsrNXz45Oj5Pefvmy7/D4Vhi3NDY5Mq5tbm/wLu7yPRPSk43JCAmLTY4Q8Ssq660ubzNRDQ6
+Qz8+UP3Nv8jb6eXd13Rpy7u2t72/vL7GzuReTkMwJSMnLTM4Vrusr6+ws73uPjpAPjo/TVzN
+xtDRztHP1PXcxLy5ur++vsXL0OBpRTkvJSQqLCw82rqvra+trr1rTkI8ODk+SHPg2dLOzcvO
+0s/Jwr+9vr7DyM/a3N97WjsvMyskLDg1Ssa+tayytLLIVFZHOjw6N0hoY93LycC+xL+6ury9
+yMrJ3l9UTU1OSktbYkU4NTI7PzdCzr/HxMK8u81o8/ddTElUet/s4NXPzNDRy8rR2drV2PdX
+Tk9KR0hNWWJnb+jd3NXQ0dPQ2e9tZFtTTUpLTExLVW/s5uXm5+bj3tXT1NHZ29La+mBVU11b
+WGT869rX29jU0dPX5/xxZ2ZkX2BdVE9NT1Zgav7o3eHo7vLs8fd3a2pmX2Fia2v04+3q2dPR
+z9Lc3eTv7GRebWthbnlqd25ZXmFdY2967uXo4OPs7epuY3JlWltcXWhsbX3r6unh3dzZ2d7s
++vx2b3r5e3f6fXFza2NqeG9xfXj8+Xd+f3lwa2hnbHRwfPnv7//w39zc3N/k5fx+e/v/eP7+
+enp4amRXUldZXHLz7e/t7n5vbGtrePTu7ern4err6e7l4uvm3uDk7/x7b3d+eXHy7XZlXl5c
+WFxfX2N48ezo5uz5fXRyb290fPjr6fV5bW5+8GZgyMxuXt7QalXW2GBRbN1vU/TR91ZSTtDU
+Tn3Kz2Fc9etmTU1n2vdZbWpT18tmWerO4l9e29V4XG/X1XFd/dvc63p8+3ZlYGFsenFq/ep7
+ZnXk6l5d+H9gXXPm+mr429rheHLj3+Te6XL68+PI0zMsy6hrJDmopkkqV6y6Mi3stWgwRbOx
+SzXossA9OmHM3VtVSWPKw8zQz8nVak5ebmRKRlLp6XRf79LY9/z/Vk1VYPLxZe/bys5pYurj
+5l5ZXuTR51VrzdVoVuPXX1ZlfnhYWG/i+k151OHf19ze2+ze+ExO7uJORPDJYEzfwMtLStbH
+bkRVeNXdX/3TzPh8eGf6Ylv53e5YVvba83Tm3dnqXGJuWE7f1lNPfczaVVffzWdSWt7TcUvj
+xN9WZNfPaUtd3NhlV+zK2F5u1NZeS1jv6FJV5dnlbmr+5F9gc+9uXFl829nj7/vg2PJUU+LS
+aWh079pzXH3jfer1Ymbbe2RXY93Ybk9Y1c9rUnfl8/1ZcdTP32Rb19tbV/NkXlJa9+3x++T2
+8d1+WPrYy8xkTV7Q4U9H79Dhcm3nzdpcVn7YaEtP/+HuYv3X8GVo6+JkXVz63+twZODO4F1a
+X3Xd5HFreWz72vZq3cvNdVlx29feXnPX/lFPWO5lT+5oSENJ/Nh1Ymdh83Nc+dfOytf65cm/
+wc/X0Nbvf+rm1MzYdczEdTImJzE+Q01o7NniXXjezMPO8NfEv7u+wby+ydLi4srKz87Xyr2/
+VB8bKDpCPTrkuMtDNT/Twdps3MK2t7q8v8XG1vHjy7q5ws3Syb/BVyAVHj753E9mrq/bRTMy
+TdDS1dTAtLjE0NrX2GlPfL+yrbHCzsa+wEMdFRwyz8Z20rKxx0Y1PF9aRkrlvLW1vcXL0NpZ
+S3O9r6yvur+6s7oyGBMaL8rA58+7tb5PNTpS6Fs+O1W6q622ws3N41FdyLOsrrm8uba6OBoV
+HC/e01Blw7q7zkg7P1bqVT4+brywrrG6yPlQU9+8r7C4u7i0tvcmGh0pNjk1Pti5sbPEYk9P
+VFZJP0BTyLOsrLHHWkZL4sK+wL64sa+z3S4hHyEkKC5GyratrLTJdE1NSkA9P07Ot66rrr30
+RkNb3c7Du7Swsra9XS4gHB0iLDxtxbevr7S6v8z9Qzo8QlHqybu1tb7aXGDWxcPJ1NfOwbi7
+cDAnKCwvNTk+RFbZvrSvsr3L0s7ZXEVDTu/S3Xxz4MzEyeBiXv7UysjIze1XTU9RTUU6MzU+
+S1ZkdtvHwcXM0NXo7N/yZWRkdeHU0Nbo38zIysvJxcbXd3jrcU9CODMzNjo+R0/51szJzs/L
+zM7V5v7o3uHl5ufv8eTVzMnJysnEw8nS4WNLPjUyNDg+REdIS1d/0MfFxczV0s/a5dzo6uXv
+9/Lp2MrDvr2+vb3MeVhWTkQ9NzQ2Ojo4OD5O9NTKysa/ws3X19ja7l5bY+LOycS/u7q+w8rJ
+w8n4U1FOQzkvKysvNTo/TPDIvr3Cx8fJ09TR3+3f2tLLys3T3NzPycPAwr+9v9FWQj03MC0r
+LDI+S1deaOjRx8LFxsHBxMTIz9Ta62taVV/p0ca/v76+wMPG1lM/PTw7OTY0NDk9QEVMXtzH
+v76/wMDAv8PVXE9PUFplbPvcy8K/v8DBxcjJ0XNUTUpDPDYxMTY8QENHT3LMvbq6vL/HzdHc
+Z1RUUlRZZH3fzsjFxsfFxMTGyNH3WFFPRz44MzM3PEFJUWvXyMfGycnJztPbdVteZWtub37t
+4djOysnHw8DBwMHJ2/9eTUY9ODc3ODk8P0dZ6tDLzc7Kyc3R1eHv8O3ve/ns5t7Yz87OzMbE
+ys7P2OPxaFdMRkM/Pj0+Pj5AR01c9tzSzMjIx8jJzNXg8W9ucX/n2NLX2tze29nW0c/Rz8/Y
+7GxZSkA8ODg5PUFIT1hs6dXV1c7LysvO19bR0dXb6fj+fOvf29jUz87MzM/Y6HtrYVxPR0RB
+QUA/QkRGS01TYuXSysjLysrN0trh6vB9fvzw6uLc2tTS0NHU1NLU09Xf/V5RTEhCQEBBQ0hO
+U1hbYHfs39rb3NjV0tnd5Obg5ujm3tfTz9HU19nd5OPm73Bpdn52Y1hPSkdFRUZJTVRgdfXi
+2+Dj5ufg3tzb2tzZ19bU2Njd6O70+vPm6+7q5+Pm8fH/ZV1aUExKRUNGSkxRV1df69TOzMrL
+zc3Q1N3y+untbHHn29jOyca+vcLCvsXvQSshIiYnKjZ2sKSiqKywvlYzKio0PUTRr6qrssjw
+XkA3PWLCtLKxrKuvvkgmGBUYHSc7uJ6XmZ+t4y8hHR4sbq+noqWqslcuLDM6S+q/r662ubGy
+t7rIRC0jGRccJz7Dq6CcoLJSNCooLDNqr6Slrb3OdD4vMUri1+7OurCxsayss8pYNysjFhId
+Os64qJ6bplsqKispLuuqn6CsvFUvLDE2Vbi0u8LmW+fNu6qmrrnFXjIlGhIbPs/Ur6Cdpkon
+LTgtL9+on6S51kosKDTuv7rH53BBNE2voZ6irbvSRisnIRgaNd/Pt6mkp+YpKjMvMtWon6e/
+fFIuIi2/r7zO1dNcNkatn6GptcluQDMyMx4YJ0NO/ryqoK44LDw8Nka9p6e8T006KzO7pq7N
+UE08LjO7np2msbzWPi8xPCcZHzvd1s21o6lHKi87PkjQq6a1ZkY+NjLLoqjPRDkzMjXPn5qj
+ss1hSzQvQjAdHjJwzufMq6jdLi88TFbQsqy17UlAOUC1o6zVOyspLTu8npqgr9hIOzQ4VjEd
+IDjay9O+qalkKiw5RVHltay5XT9EQUy2pKzlOCooME+1npuhsPA4OUVTTikbHjHTxcKsoq0+
+JiczQk/FqKa2aEE+Pj+/pq5dNS0sOtqunp2qy0o2M0DN9CQdLDxLw7mtqL8uKTU7RNSzqq58
+Oz88Pe6wp6zgNS0vOt+toaKuxlo8QO26VBwYJjzWurKjot8nJjFGXcusp7dELzA9T9OoorRP
+LykuRsOmnqa31kY2PN3ALhseK1q5u6+gqkAnKDjXzb+ssNw8MTFGzLKorMo/Lis15bSnpbDI
+9Es8SMj8Jh0hLdWzuKqjvDMpK0TEvbmzwlk9MDRauKqtwnE+LSxCuqajrrzHWTk7bdAwHh8r
+Ybi6r6WuRCsrOs67u7m++zwuMUnCray9204xLT7HqqSsuMFlOTde0TUhIi1JxsO1p6xXLiw4
+2Ly9vbrDPiwuPb+str+6XzAwPM2qp62vvkw1NlL4OCgnMFjfdr2rr944LzzNvcTAvcVJLio1
+vq22u7p6ODA6xamrr6y5RzQ8Wj8qJS9O3Wp2uq6/Rjk888LEysK/ZzIqM82yvLy41zwyOc6r
+rLKur8s8L0lHLS0uLm26Z9Wwul9MNznKu83As8dBLS1SvMfJt79KMS9brqy3rqq7RzQ8TzAk
+KULd0dzMtbVbMzdKzsTZwa24Piw44szkzrm9TDNCxrvCuaupvj4/engrICY0SGXrv6uubjQy
+OmXfXr2ksTxC5WlcV+25xjo628PLzLuustd3SjrOwyofKjE6T+awo65PNzo8Oz3Sray8zMTU
+WEhbxsxJPlbW0My8trvMeEE/T1u/sS8PHqs0IaWWo9QoK1csIrqan9VUudgtM7mqvTk09uNE
+2qyru2hEZE4sVKmqMg8buTUguZmbzyIoU0g1zJ6bvzdqzFVD0a2xRjBL2FJMvK3OQN3JPzfr
+uK5LFxxbNSPrnaDuN0v/ST2/pKnoZcXmR0fKsbx63NZIR3jh5d9ZRMa9TPC95C4eJDMvM9qo
+p8RHW85YTb+rrMlXfupRXb63vc/o1GpBQU5HRdK4ucneRSciLjIuN9O4vsW8uM1S3L29z9bE
+z1puxsbNx8fQXl7k9WJKRlRzbFZ16T8xODY3QERP2djMvb7Cvr3DztHOzu5f8NjncNLMbH7t
+Xm71cepiP0ZVSkhW5OJPSE9NSFH62/Lzzc7Rw7y/z+rf6lhQZvBtX19bWXbU1tbM11ZFRVBy
+afLU2WlYdN/wX3PebE1b8tfX1MzVbk5Vb3hnaHZmT0xs0cjK0+ZXRkVVctzLydRsWWvj2Nzg
+291pVmz4/+rX22tPSFbveFpXWldOTF7WyMrP4mVUW37i2dPP3GRWfNLR2tzdfl1jZmxvYF5j
+XFJd6dzialRWT0pT7M7Hx8/nY1Rf4+Df1dv7ZV9pbnr16er7dXttXl9odG9peuTm6+14aV9Y
+WWnf1NLQ3HJcWnPi6H3p/VtVVWBocO/b2OL2c2NkdX7t6ubn4u15dvD7cWdnb2x97eju8OTs
+/nBrYmNfWmBu7vh97+rs+u3f3/L87Ot7+ubm+mdfa21lYmhpbvvu5ebt5uP1bWtsbWhn/+vt
+6+De6fn8dHN/e3jq5/V3cv5vXl5oa2RbXmz+6d/f8Pd2aWhueHvy/XJx8OTl3NXW4f713t3f
+3tzvXlVZXFhYYWlrY1tkZmBt6+v6b2xz7+Xf3ePv7uPe3NjX2+l+b2x+5+Z8X1RQT09bY2R3
++vXub33v9/Pm3uN/c/v56+Pn6/f04d7n6ODp9nJoX2x0c2teXmFeaf5qbe/y7WdWd85pYdrm
+/fJx5ub15+t/bnBu72jx2Wrd7Vj6fV3t6ml8eHZ8eHf4cHz+9+rv+mVsa2xuW01y1mjT0v7f
+ZV3hS1LNYePP9dfYbNXq7vRG/M9G9+pc31hl02XjamToeul7aXxk8+1+eHXodPl+cfvwb3xy
+b+Zl79pJ5u1M2f1p2t53e+v37eB6a173/F3r5+7Y5e5+/WZdbWNu/ONtfm/qbndkVdteYOfz
+3d7k0HTq7Wh2aWNmZ2xiavVt73z05WznfnjzfOn9/fpo62594fj14HN4cl1qcf96Ye9taN92
+/3lz6XrW6OPN5dHX99j9XmJVTEk/REI9TU1KfuLeycXEv72+wb/Ez8vG2dLM1NpTTlA2MTgw
+LTM5Ok7d7Mm7v7+6u8TJy+Tw3/dp5t/hy7+/v7e6ysfnQjYxLCsrKy83RFvbx7u6tLe5usPL
+zd/z7Gdic+/Zvr26s7zI3E43LSwrKSsyNkb2zL+4tra5t8TV1l1QWE1KXG7gxr+2r6+ttL3X
+SDoqJiknJS82PenDvbSvsrm6v15uXkFCS0RJadrJurGtraysu8diNi0nIyYoKTY/YMW4s6+2
+tsbS7EJAQz1IYl7dx7y2sK2rra2wy9c7KywiHicrKT541Lyur6+2t9xGVjsvP0U9/cfLvrKu
+raypra+wy1M9KyckISMrLD5yyrqxsa+5xNBDPz8wOFVL5Ly8ubGvrq+trrS4vVZCMCkkIyIp
+Ljhez7y3sbS3zeFPOTg+MUHb48OytLGvrq+vsbC5vtJKOSooJCIjLzI7+8C/t7K3wfDtSTc+
+STZew82+sLK0tK+xt7SxvcLdQDUpKCUlJjEzPubGwLu5vMX66U04Qlo68Lu/u7Gytrmys7q4
+tb7EekM8KCgoJCQwOD5Zyby+uri/YPZfQDnoTlm7trq2s7W9vri8x727yNlWQi0rKCYnKjU7
+StO5vre8vc/5X2I4StlOyrS5u7m8vdHOusPLvLm+dmxIKysnJiQsM0VLybe4uba3y+dw3zpM
+y/DZtrzCzM7FdXjEwMC4tbDAbUkvKCEhIyYuRWvFsa+utLW+3E75PTnc5XDBvcPL4NHdcMm8
+ubWyra/iVzYkHx0eISk5zr6uqKess7zOSjlPOzjvyezGvMHOaN/MX9e6trWxr67NQjUgHRwe
+Ii5OuqyooaWsvMxfPC83PDo+yL3PwbzE8VV30fXct6+ytK+x/jYxJB0eIi45c6+mq6eqs9dE
+PjwvMEZPS2G4udvMvtdYUP7M29G2sbe5vbjMODgwIiQnMUNzvqmttrO/cj83PD8zP+DdXtC2
+tMzeyO9GPkrp1tm9ra60sayuUjw7IR8hJTZKVLCsvLW95005OFE8O97Ayse3r8RGTVc9OELV
+uLexqKasr6+z2TArKRwcICw6SMusrby9wstKOUnbTUPdur9jzLLKPUhoak5JvqyzuK6rsMnG
+tl8rLiodHiAqT0zTqKe5uL7Oajs56U46+L61usPJudcvNFJYSGC8qay6s6y46U/RXystMiIk
+Jir30mCuqLu8zGvKSjNo70j0xbmww0x94zctQs/K0sKtq7rOvbnYTGq80zMxLSUmJC/rzr+r
+rri9dUpRODleVujCwLS6Zn7qMy45SOHNvqqnsLO6wMlneb7cQFM7IyYrKTlARryy3cG92dVh
+Qc/JR/nCzMVdPu5gLzf+x7y7sKatwcPHy9lax7XNT0k7KB4eJC81PcKpq7a3tLlfNj39Qzpa
+w7nDYFjJ+zY8zbe9w7aqr87awL/fS3LE1kk2LCwiHCNDRF64qqarw8S+TDE6TVpWT72vvexi
+4W83NNm6wsS5rbLX4b/NUkfpyepOUT8zJR8nLj1Oy6+lrrm9wXw+MkrrRk3HvLzTU2ZLPUZF
+zbW6urGxtMdr9+xTUPPN2k9EPiogJis4TfyzqK+8u8HaSThAeUVBbsC8zlvSxVg8Ur65ycq1
+sMhf+NDyREfLvdFo3fk7KCIpMi031LCttbuus+s/P0JANDjPucjWxrfEPz7NxeVlyrS65NO8
+xl5T48zoUmZeRDouKi0uOU94wa6zu7jBz2E5PEs9QG/Kv87sw8dcTvTPyefivbzPzcvJz2Rq
+zNfnzNhQPi8pKi01QljJtLa7vL7MYUBJVkhFWO7O417YyP1g58vByNXFws/V1NPLyc7OzszI
+60k9OTAqLDZCR03dwcHKzMfE03ReX1xPTVFMT2N608W/uLa6vsXV3/FcT01TWllf9eHh6N/Z
+2+b9b2VJPTw8Ojk8SF537+Hb08zMzdXa1dbv3s7O2ORydvfh0szPx8PM0M/Q19/r5uLwXE9K
+Pzc0NTc4O0Vb49jZ0s/U4N/c5efa09Ta3dTX7Xjq3tHOzMO+vsLKysjN3OxrXE9HQT04MzM1
+NzpCWOzZz83Nz9Xf1954cdnP19nQz9ns7+fa1c/Mx8LEys7Pz8/b7PlvVElBOjc2NjY5PEZX
+7NLLy8nKz93l6mtgbv378dza293XzcnFwsLDw8bJz9jY3u91XVhQSEI8ODc3ODs9RFb34dfP
+y8zV4OTxc23o4Onk1M/Oz87Jx8XDxsrLztLU1dzj6e5kVU5FPzw5Nzc6PT9FT2nhz8vIyM3U
+3m5cYXV6dfHa0s7MyMXExMHBxMjM0NPU4O3udVpORUE+Ozs7Oz1AQ0hSY+/d1M/P1NfZ73F9
+5ed899nPzszLxsbJysrLy87W1dfi5+11XlFNSUM+Pj08PUBER05f69jSztDU1Nv9X2Z4bGL5
+2dbTzsrJxsPGycrN0dTU2d7j531fVk5JPz0+PTw+QERKVmf23dLQ0NHW5nf3+Wdmbuvb1MzI
+xsPBwsTDxsvU2dvk7/puWU9JRUA9Ojs7PD5BSldn9+Pb1M/N0dvo29jtanzk3dfT38rEyMG9
+uru/xcbO521rXkY8NzU1NDA1P0pSWeDLw8rOztXk5/9TUmF+YlJmzsrPx764u8HAvcDN3d3O
+2l5acPxTNi4uMy8sLkHm2NO/srC6yszO30o6PVRZTFzOvsfkzby7xtLGvMDW7NfJ4lFcfV1L
+Qk1ULiowMz9DSMG0vr/Dvr1+Q0ZJU0pE/MvMy9PLx9/x08rFzNHCw8/i3tDZYE5YVktGS0tN
+OTFDTUdJasjDfG/R0lxCS+HiZebLwM951MfP59zEvcvTyMLKcljt/E9GS2BfUUxOXVRDQ09d
+UFBkfHJeYO56W2H4bmt03MvP2c7JytLczsfM0Nvf42dq83JiWFZsYmVvYF5ZUV5lXF7t3flW
+WHt6W1NddGNTT23Y2d/c1NTkf+jj8frw6X1u6u3x737k3/D6f9/Y6Obb3OR6et7f/m5hXVpP
+UVhVUVFZYV1geO/vbGfs6vzu6+XscnDt5e/x5tnY5OTX0tXa3Nve8PLtfG5lW1tZUlJZWllV
+WmFfaWt7/Pjr4Ojp3+bl6vPs7vTw7+3f3+Xm6OHjfmttcXp5dHr3+XVvcWZobG1jXWJkXWVo
+evV37u397OXp6ff+9PXy+f3v6ezu9Pbs5+PnfG367uft7v9rbHNoWlteZmhtbXJ1b/18a2Rk
+ZW386uXm4+nu7fDv5d7d3+3r4uPo7/P5dnJsamZgYGBja2psdXlyefnu9mxnamZgXmBoffz2
+497l5uPj5+vd3ODo+PTt9X789/Ht+W9ja3FqZ2FhbHhsa21vdG9tcXh2a2tqZHnv93p86u71
+6OLf3dzb3d/l7erqdmv+83hlaXVxbmtgYGdrbGxvdHBx9/53fvPtenz5//14/enq8/Hs7fHy
++n55dPTf5Pz793Jrann0em159/977/5oamds+O1+cnZvZGNx7u/v7Px68vDq5e/r7urueu7q
+5ubr7e75cmVeZnVnaXNrfHRqbm1lYGz+cHVzeez/ePLq4+f0+eXl5uTj4On+dPPs+Pnv7/L/
+bGtuZFpfbHdbXHr6b2Ft9H1t/uvm6+jf7Hhodfbs7PXt9Pbn3+rs6fFqa2t1bmbx3up3Y2z2
+aF1662xgZf1zXFt863hu/ev8a//f6/Tp3eR47tvo5N7p8ef2eWtndu91ZHfucV5qb2BqdmRc
+XWl9cmlv7uXu39/49N/ucXB8+O7w7u3u7PPy8Pn8cnD5fezt/f7v9PFxbHNpY/36aXJ1bW1j
+Zm9sYmFv73966t3uePbv6HNq6uro3fvp4nd73/Nv7eXzbnj8/Gttc/H3bHD5aVtcd3xcYO9+
+Xl13fW128t3h/PDv7+996d1+7djm7/j69n9jYHZuYHPu+3P/9+b0a/XldWlse21kavT5amv4
+cWj79v789m149/N49+vl325y4ux7ferwaHh5d2xq+m1ndXnv/Gv++H50d+36anvl4/vp7Gz9
+f3X6e3n9ZV1t8ff55uR+au3oY1j05HRdbeZnXuzr8PLg4/VmauN0Wnbh+Wz57fnv/G1lXWxv
+df90d2//5vDu6Ofh83T38/NobG918H/66/34eO57ZXtv8O9v/O95a2N47XNoaXpfWmxyc3f5
+8vtp9OTf4ODv3+doeurv9n567PN0fvR0b2hte25s+HVranf5aWNp6+dv7+np72ht5Gpl7P1o
+Xujl8vb82+FfZubuZPX5b/lr5ulbX+rtZF/23u9naeDjbWre2mVo7fJ8f+7u/n5xdv1haONn
+Wnb8+HJtZ2z0dXH89+Dpf3r47u1x+Nfb9mfw7mp5ZmntZGLs4G5rfvl6Zv34cnVybnL0/O3s
+/+jobmlt7OPxbPbZ6Xb3c2xjYnb++m/963pda/RuYH/meGj27OZq+eRxZHHl9mx86eH+8d7/
+bvzx/P16au3l+nZ9bfL2XWzsanDvYXLtb2xv8e5fcH5s93L06mVc5tz76+TmeWh39H5sfNz/
+b/DqfXzg9G1r7u1jXPng/F5h+H1lauje83Fy9+9+Z27vdG/98vn8eXXsfm7p7mL132hffHF1
++/vf5+z57+v05up9X3P2Xlrt+mJ4au/pXXHmZG/qdPJtZXnk3uHt4+f44ft54/bxam9uXf/s
+bft5X/j/ZHhlcW9nbHr8dX76Zufi7edi8+vu7v7t6PJ16drq7/Te6V1o6m1dW19rWmh7anfq
+4ezscf3wZ2T9/vHi4+3/4uLj7ffvfG1bWXNsdm5o+fZga+x0Xu7dfOje7Hx37+798+vr+W7i
+6/tuc/FuZl7z7nxhZ+VrZ+r68+75e/xub2Zo+uzr5/fx6Wz29nrubWLy92B77urz9vbu91x5
+8WZpaXXx6Onu83jm7Wbu63N9dXBl9fthbvLjbn7l7PV89v5+/vbt6O13fu5pZn1jeN/t92xv
+aV9ibfz+dXBoa3N07dzmZPLm/urs5Nroe+nif2/n621oeHJoY3L3735pdvdtaHR7aXbhfWt8
+c/Lz5+r333xm+/bt+3Vq62pq6m5ydWh942dmdX/36+3p6frq+Hjs3vFq7npva2nyem9+bWxg
+WfL3X23i73Xp8fHp8vHl7Hby4OV6//Hrc2RvfH1se+1hVXfsemT24+9++ur3fO/l+mtme/Rv
+bvXtbWFx+Gdy9ebq8/Lv3HVm8en0am9+c3Vs6OX+eunjb2h9clxhdW76+vDv73dy7fL2dfTp
++Gj24+/5//vp7vP/eu5ybnZhWmxwZ2l6/HX6bW7uefjh8Hzt8/H37u396uT98urp8/788vd6
+fWJgaF9tamnq5nf+cm36+fHw+vp9/vj77fHm8XBscWxo9fFue/Nyam/s7Of5Znf7e2x4+fTu
+8PLn8u/k5nJy9vh7bvnv8G1se3Npce51ZWZ38WxhbXliY+x5aP7y9Xd8593o8uvqfe7l83jy
+6/rr8PTx+HNqePVzbmlsfHJx/v5gY/ptX15pdPL1/eTo8/ft73vu4uH7evB8ennv5/J1d/xz
+/vX9eGt0/WxpbP72b2t1eG18f3RtbHB6bXPt6/j9925y/vHt7eXi5O1y+O/8f/vy9Pbw8H5+
++fN4Z2NjaXBoYm1oZ3Nvam1/8fF9fPLr6+7x7Ozs5vN19evt9fh6+Ptud3l2eP13b3h0ev18
+bm/+cmpreXpvc29teP18e3d77vl6e3j15uLl6uvp7O/7bXP8fXF3efp2cvn5+31+fXv/dnl0
+ZWl2dm5rb/r/efj0emxvdXP98+bh8m91d3F87ezt7PF+dX368evm5ObzcWhneHVpY2/3ff53
+b25u9vltbfp/amhqc3X37PTy9O7tfP95+/j3+/7t6Or+df947+zyfXf9dmlt+O/v7fF6a2xt
+ZmJo/e7u8vH4enL6fnZ6fO/t8fLy+nFpde/0dnx7bmhscW9vdv5wa297eXf9+/X47+br7enq
+7/b39ez6/fbs6O76cWlmbW1laHX++3t39Ovx9Pp8fn76+XVreHx8dGdmZmRhbPr57+3u/H/4
+9vT07u3t6+7x7/L6ff/88/P2e29/+u/0enf99Pbv8HpxcnNmaGpscXf89fP9/nV8f3/0/nz9
+/2lncnn/+fJ6fuzs6ujq8Ovp/mhncP3u9H789np89/z293n/f3p2fHRw/3tvbHBvbPPo82tu
+6O57dfPq+G1z93Z58vp8cnD2/WtmdPv++u7s6fNoc/X77Ofn6e/7bm9+cXz4bW9sYWJv+Xv3
+4+t+a2Nv/Xn56Xxvcnvv+/7f2+5s/+xvdup6bvt//H52cGxoZml7e2Zpcnrv7O/2+frv4Ort
+7fDzc2Zq9ux1Zm1pY2ht+/rz7vJ0ZXTw7+jm7vPu8np+cW19eHJz/e7y5t37bvf8deDZ59zd
+Z2ReU1VneV9UXmZSTF3n4Pfr1NltY/7r5+re2OF7feDd5ebg4PPu19DX18/O0uJvXVBFPzo2
+NEB07M/Gx9RtVmj5/uLUztLi7+Hp5+Pu8mZdXl5i3tTV2d/jc2Be+OPf3t7Z4f5s79tv5MnP
+7k9QQyUr81Z0v7a5zD5F5FdLXc7C2l7e29bZ4dPcW09WZ9TNyL/G0eNuZ2/l0crMy8nQZiwg
+Jy5BbcCtqb5RR0hEOz/Pvc7h38THYUzz5kQ+RdW+xL62u81dTk5ESOzIv72+wtBoV158WT86
+NisnMljbzbqxttlOT2dfTl/W0ubv1sbM2NnnVkNCUO3MwL69wtltU1dj/tPLxMPEvr/WVDQu
+KyQmNVrXv7WvuND86F1GPkdj/nnVv77I2m9vUkdPddbGvru8wsjP5GtYbN/p3svNz9JTPTwv
+JSkzNjpbwLS1vru4wORdae9cSUdNaG171MjGycrK03BeZmxlafTb1dzf2Nvt7N/Y0tnu6utf
+UE1KPzo5PD9FTWHr2dDRz9DX3ejs4+Lh299+fPfw493X087Pz83R1tPS1dPPzM3T6n1uT0E9
+Pj45ODw/QkZHTmjn2czFxsvPz87R3ODh5v1fY/ne083Jw8PEx83a4OPe3+Tf5PFfT0tGQT06
+OTs9P0NJUGHi19DKw8PIy8/U4PR2c15YZfDh1s7QzMnJx8XDxcrO1eR3YFldW05FQkI+Nzg9
+Pz9BTF5y993NyMjJysvS3O7v397e29XX3OLc2NDKxcHDx8zW+G9wY1xiW0xEQT45Nzk8PEJP
+YvvYy8nL0NbV197m5uHf5OLa19bX1tDPzsvFxsjJys7X33doX1VKQj89Ozo7Pj8/RU1VYO7a
+zsnIys7O0d3q5+30697Y0s3Ix8jKzc/S2t7b29vY3OpwWUlDPzo3ODo6PkhRXe/c1c/NzM/R
+0tff7unk4+ro4t7c5vtnw7i9t7C1wMXNbEQ7NDEuLi40O0BP+8vFwcnIxMfqXFBSTEVKcPpn
+4svHycnEurq+vbi7vsG/vcRlQTYpICMpKSlBw7W3sauqu2VYakMwLTx1Y1/AsLbP6MzRTUjJ
+tLW3rquwwsbGRyomIh0fJixBu7Ctq6y0w0s8Q0s9OWPDw8S9tr1aQko/OT/Msa6rpqOptcTZ
+RiQZGBwfJja0o6WurrXVMCs6bE8/1qqx89u3uT4wS3A9Nl6xr7aro6i4x8HDWDgtIhgaHytI
+wqifp7zNUTwvMU7Nzse9r8j6xsBLOTZDSEL4sKamqKervvtZYVc0JxsYKCcsbaqmqNPWwkcu
+OV6/z0/KrchB2bfKPzVJYj9Bv6Wkqaalrt5Pbi4lKBobNTM7tK+qrFpH2zg1PVW9wEjLrN1L
+x7a5UDZRYTY+u6GiqqSkv1s5KSkjFxxMPj62rq6wPj/PPzVJcMHiScW+ybu5s7biTEA1Pd6z
+pqajn6pJKSAiIBghzcJexLWvyjE1zt5DR+/Jdjs7wa6zubW0yT867bewsqulqbxLLCMfGhw9
+y/HCs7rCSDjvylNc5e5cMi1SvrmxsLLDOjBNu6qmopyexisgHhkVH+mvsa6rr+IsKD5qZtO+
+uNoxMW2+u7u5uW4uM9ayraidmqpsLiIZEBcwzremoqOxOCkxNztM37m9ZGDNvbe7u7l+Oj5r
+xbmrn6OzRSUbFRkhNdOpoaGqzT8zLS03WtfQtrG+w8fIxs9rxLnEv7SrrL7Mvz0aDhpLJh/G
+npyrS8irSyEnM0JbVrmfn7W9uclIOV6yq7q9r7TG0VMxKBwYHCIpPbyrqK65v8dGKy9fy8++
+ramvv7+7yNa/r7LcTl/15dHHQyssKh8eJzZMZ+W6r7zfzsLJzse9ucTh3M/P1827uLm+50lH
+UU5Pd8e+az9CMigoKzJGTVu/u9XOv769w8e/wsvc9+tdZ9fOyt94e/P9U13rzs7dX0hEOjU5
+SFzW1dXYalVbb+vRxLy+xtHqT0FBTWb/2sa/xttQTk1JTFxYTk9LVu/dzMXJ22nk12hf1MjK
+xMncflpCOz9EUfnSxcHO+VpOSEFETmBjYH7h2NjVz8/M0+nZ1dfPxcHH0m9MPzw8P0dc3czJ
+zOxZS0dJTFVu3uppY+PPz87Kxs3c++7b1dfUzNXzX05GQkRMX3hmd+duVUxRWl5aauzt3dXT
+ysjMycTJ3PdiXm9xceje/lpRTktOU1xu8+jwbV1ZV1pfY19ad/Lr5NPGw8LCx9b7/Htja33p
+2+VdTkpIR0dMWPPp3d7y9G1sZGBha2tx6dfMycTKy9HpZWJYXvjq1M7YeVlKRD89R1Zv69PN
+2epxdGxkYfXa29bNzM7N2ej0YlNVXV9c/tnW1OlqVE5ISEpSat7Uzc/uaP13bGl82M7RzszT
+5eZtUE5WTVRabPzh4N3yaV5XWFRj8+rO0d7u73lkX2Lm29LP0s/b6N52ZF9dVmFZVVx2YmRi
+W25sa2bx6eTY3ud4dW5cXe7v2c/O19bZ3N7vaX39anNvaWd2WU9UWU1cXmrk6uTd7ObvaFhg
+afXr//XUy9fs2Nvg6e9iZuvlZ2vu+21fbGNRT1vt3H3v6OV1aXBgWF5xbfjc2t/j6ebq8Pxp
+aXJpYWdsdOvf5/Hw9XdudX707enf5fXu6fP+ZWL9f3r9/nh0ef10a2diX1lbaf3o5ubp8n5x
+amFsfOzf39vU1dXZ4vR1Y15qa3T07Ph4enBcVFJcX2NtdvTo5en17e78//7y6eTh393f6O/5
+b25kZGtwent/eftxYmFfWVpmde7g3tnb6/J2YmJ5fO3t7N7d5H3/emZeZGhtcnrv5+fv+3Vx
+ef9ye+7q6/t8cvvycF5n+/Pxf/XY0vtMS+DM/UlO3c/+V3jP1mhVXvB+XWLr293r+uTb7lpi
+9G1gb+rc3+3t6X7u615Ybet4XGDg6Flm5XpZZ9PXbGrY0upfYn5sTFLn4Wr408nQVEjSxUk5
+973dQU/FxVtK9snfRlPV6ldh39Pfbe3P0vtn7+9eVFph+nx8afDg725zdWBOXuTjaGDr0Nl3
+9t7ja1tr3O5x7/Zv1uNTatbdXFTkw+tMaXdxZU5a3+5ZVfXZ7GLv3ebs79jW0s/f2tHh9vRh
+UEc/PTk6Pj5Hd9jOyMS/vr7Bxb+9wcXEwL++vru/NyQtLB4gKjX+aVavp7O9xs/JZj9K5c/b
+f8y5uLu6sayutLOx/SQeJSMdHilXxvfIraqx0j9S70I8SNe1tsvFurm3vLqtrK6ur8MqGx4i
+HBwmTrS1v62mrsBSNjY8ODxWyLKzxc/Jxb25tayqq6urvisZGyMgHyM7uq+7uK+vtMw/NDlI
+UkthyLm3xvDdwrOrq6yrq6yuZSIaHSYnISQ4zbe5xsS8vMdONDM/UVtf7cGxsrvCu62oqK61
+uLq+6TIlISYvLywwPU/n5/Xe3M/Ezm5dbG1cXX3c2dDRzbuysbW9xb+9wL/D1fxhTjwvKyop
+KCowQHve7Mu9urvOce7SyMfMy8W+urzCw8G9uLi6vMp1Sjw0LikmJiguO0NKUXDKvr6/w8rJ
+wL3Azc/Fvbu8v8G9ubm+w7/JYkQ7NjEqJSQoLTc9QlHfwbi1usHCwMHFz97bzcfGyMnEvrq3
+ur6/vsXyRTgvLCgmJigsNkNLX869t7S5vr28ws3W3dfQzczS1MzBu7i4ub7ExdVJODMwLCgn
+Ki4yOkBP3ca8t7W1t7m8ytrh6v1hYPXXx727urm6vsXKz18/OjYyLysoKy41P1Hhw7qzr7O5
+wtTvVkxOVWvb0MjDwL2+v76/w8bGxNFYPzc3LSYpKy0/XHbKvruzusbN9vt3TVFj7c7a5cjD
+y8TDs6+0srO3u1IuJyMoJiIoNUbGwcGvra+vz2/WPjc7NEh4SP3IybS0ua6tra27xsvWxVst
+KCQpLSMjNk7Pws2ypquzyVzUQSstMUPeWfm5tLG0v7avtbe9vrS7v8o7KiUiIB4gLEjkv7Kq
+pqm0yWlYOCgqNU/b2M61rrW5vbuzuLm2tbKwtsJGKR8hHx0eKDnqw72tqKqwwllMPysqNUva
+zM22rK+0uLivr7e6urSyvMRjLyUhIR8fISs7Ws68rqqssb/sTjktLjlK38u9r6uqrLO0tLvA
+y9PLytPVWzouKSopJiYqMUP0zb+4tLK3yPRQTVRVY+7Ovru7vcfNz+Z4/9nCvL2+wb++10xB
+PjkvKyosLzY8QU95187IxMDCw8XJzMrKzNLX2OTy5NjKv728urq4ub7OWUA4LyspKCosMDlE
+VPfKvri5vb/BwcjO1Nnc3uF3c+XXzsrFwby6ury9vclmPzgyLSopKi0zOj9O4ca9u7y+vr/E
+ztrc5XhkX2R/3dXKwb26uLe4ury+zltBOTItKSkqLTM7Q1ffyMC9vb/DyM/W3vb+/e/j5d7R
+y8a/u7i3tri9w83aWD01MzAtLC0wNT5GTmbhzMPBxcnIxMfUdGd2eW5969jIwb26t7S1ur/L
+5H1kRzo4Ojc0MjEzOT9BSFvcycTDxMTDxtRsV2T8+2553cvCvry7uri7wtHg4t5vRj8+OjQy
+MDE1PD9FVOzJvr2/wsTHzuZdXmvw7X3v2cjAvb6+vL/Hz9nb2NtcQT48NTAuLjI7QEVS78u/
+vL3Bw8PJ1m5dY2l7++zczcfGxsfDvr7BxsjIzOZMPzw1MC8uLzU9RlBv28rDwcXMy8/V3nxy
+/Ovk4d7VzMrHw8C9vLy+xcjK2VhAOjYwLi0uMDpCTGTfzcW/vsTGyM/a+nNlZHb66+DPyMS/
+vLq6vL3Bx87mUz86NTAvLi8zOkJLXOvUyMC9wMbK0918Z2hnZWx4693Pxb+8uri4ub2/xNJj
+Qzo1MC4uLzE2PUZPZ9rJwL7Ey8/Z3vFeUWzf8e7Xyr+6urq3trW4vL/J+kg3Ly0rKSksMDhF
+Y9HBvLq6ucDT3HFWTk1OWnb71MjDvbm1tLOztLi9w9tPPTEuKCUoKSovPk3dv7+4tLe2wc7X
+Xk1WUURa73PMyMa8uLa0sbO1tr3E4FI4MC0iKCojKzk4Qr/Dx661ubjGyvJaYldJYuhL1sfj
+v7u7tLGysbK4vMt9TzAwLCEqKCQtOzdMwdK8rre0ucDGaWVuTUxdVlfQ29a+v7u3tbSzsri8
+xthMNjkoIiwkIjA0MnnB0rWuuLK2wMzg6lpOWFRQavj7zcfGurq5s7S2uL3K2VE0NykkKyQl
+MTQ45cbQtK+4sbi+xt54V05PT09ZbPnPxb+6ubSytLO2u8/dWy8zLCEnJyMsPDlcvMO3rbGz
+u73NaX1VRExeTV3X786/wbu3tLOxsbi8x+JBNDIjJCkhJjExPMrEx62vuK+4yMzgSlJYRlxo
+YNrQyMG8ubaysbK2ur/haz4uLiUiKCQlNDs9yLrCrq62sbnG3XpRS0xPV1p83t7Fwb62t7Sx
+srW5vc94PS4vIyIpIyQ2Oj3CusWurLezuMfvd1xJTE9SUWTh6Mi/vre0s7Kyt7q9x3ZKMi8q
+ISknIy5CNnW3yLqrtrq1ydJyUkxNSFpeT9TP1b26u7Ows7Gzuby/70U+LSsnIycoKDJBRs67
+u7Sutbe8y9tWTU5LSV92Z9jLyL+7t7S0tLW2vcPKVD86LSooJicrLTRKYs25t7WxtrvBzf5Z
+TUpWTk/65NzLwby4ubi1u8C+0OniTkE+My4yLy44NztSYN7Ix8q/vsjI0uzsXFNYU1VsdW3c
+0svCwLy6urm8v8XM0d5tST49ODQxMTU3PEBHU3PazsvKzMzU5Xdnb3/s6eXa0crFw7+9vLy+
+wMTHzM7XeU5EPTkzMDEyNTk/SVb12M/HycvJz9nj/njw8u/a19DKyMbDwsPCw8PFx8nHy+5S
+RD05My8uLzE3PUFRe9nLxMfGxszP1uTh2tnU1NbVz87LyMO/vr/AwcbJzNZ2T0Q9NzAtLS4v
+NDxHUG3d0MjCxsTDyczLzdLT09bZ3tzXz8rGwcDAvrzAxcTK4VhDOzUvLSwsLTM5QEtZ9szB
+v8DCwcTFxcnR3mxa9MzH0N3Nvbi8w8G+vb/Fzf9IOjAtLS0rLC42SGB438u+ubm+yc7S1dne
+82xwf+nZzczMyL+8vLy+vsDIzcvPXDwvKystLSwsMT5Z2Mi/vbu6vcTM2vJqZ2pu/vnt4NDH
+v7y7uru8vL/HzMzM029DNS4qKSkqLC84Q1/Nvbm3ubu9x9jtdGZkYl5q79zOxsC8ubm4uLzA
+xs7X2d7mX0AxKicoKSouNj9P5sa6tLOztr7O8mhiXl1bUFJg9NXFvLe1tre4u8HKz9rq6+9W
+PTEqJyYoKy86RGPSxLy3srK2vtFsU09VWFhbbPvmz7+3srGytbm9xc3X63/3bEg2LCgkJCcq
+LzpKdsy9t7Cur7K3x+pbUVNXVlJVV2rZw7izs7O0ub7G0er0fXRtTDktJyMkKCw0QlndysG6
+s6+ur7jKdU1GSk5TWlRQVPrJuK+urrK4vsjX4ubo5W9EMikkIiQoLDVCXdjFvbWura2vuMxr
+SkFDSEpLTFJe38O3r62usbe+y9fh4+XjfEs2KiMhIyYrMkBW2cK8ta6rq621w/VIPTs+QUVJ
+TmXOvrWurKyus7zL3uru+vdlQjAnIB8hJis2SvXJvLavq6mqrrjVTj05Oz9ERUhOas28sqyr
+rK+2vMbX7372/VY6KiEeHiEoLz5dzb21rqmnp6uyxVo9NjU5PD9CR1Ldv7Otq6uts7rAytLb
+5PRgQC4kHh0eIyo1S9m+sqympKSpscNcPDQzNTc7PURc0byxrKurrbK5vsfV6OrvZEUxJh4c
+HB8nL0TbvK+poqChp7DJTTkwLzI2O0JS5cC2r6yrrK+0ub7J3O/j2uNKLyQcGRoeJjJO1rus
+pJ+eoKq7XDYtLC42P0le0r+2sK+vsbS3ubzCzNjQxsPQRyweGBcaHys/3Lqtpp+cnaW6Sy4n
+Jio2SW3SvbOur7K4vsfKxcDDyszBubSyvEUkGRMUGR8vabyvpp6am6CySikgHyQvTcy4rqqp
+q7C+6U9Ob87Bv766sq2qrc4tGhEPExwsdLetpZ6bmp6vQiQbGyEvbbesp6WlqbXpPjIyP+q+
+tK+tq6mnqLE/HhENDhUjT6+mop6cm56tRCEYFh0t1qmfn5+jrL5FLikqNPq1qaWlpaeoq7Fr
+IxQNDBAeQK2fnp+go6eyRyQZFxwuv6Kbm5+ot+Q7KyUmL2axpZ+foqaqr79iLBsUDxEaLdKm
+nqCjqLC+TiwfHB8v0aecnJ6ovlg2KiYoMWC0pZ+en6auuNxNOiUaFRIXJUytn5+msMRxQTMp
+IyUy1amdnaGtykQ3LywuNla7qaKfn6astshgPCofGhUXHy++pJ+jrMdRPTIsKSoyda+in6Ks
+yE45MDI0OEzJr6eioaOpscLhTi8iHBUVHi2/op6kr985NDAtLjE70q2lpKi11V9HPTw5ND/h
+uqumo6OmrLjKUTYnHRcTGynOo5yerNgxKywsMThHxq2kpqu7Zl1OTEc8NTdZwKykoqKmrrvQ
+WUApHRcRGSfDnpqdr14tJiwtNj5RvKukqbTFUV/f3ftBMy9Cya6ioaOnr7zXd0opHhYPGCi7
+m5ectT8mICovOUr9u6mkrbzeP13QzNBGMy9CwqygoKSqt8vUz2QyIRYOFie4mJWctTMhHykx
+PVnbu6umr8xjP2e+wMtHLy0/vaefn6auv9ro2OYzIxgPFym5mJacui4gHio5TN3OvLCqs2dN
+P1+4uchMMC5BvKaen6i47lbexbxPJhcOFCG1l5SZuC4fHio9XOvYwLOsuGJCP/m4tMZILy1C
+u6Sen6m8bU7bvrb+KBQMESKpko+ZyyIaHi1pxc3p3Lu2wmdRWsWwt904KS5ZsJ+doa7KVE/L
+u7fTJhMMESSoj5CZ0R4YHS/Xsr3q3NLL00ZS1LqssnkwKCtzq56corRkQErJr63OLRcMDyC3
+k4+ZvCYYGy1wsq/IbeDY70xH2birrug0JilPrJ2cobVQPEDHray7NRwNDhxKmJCWqy4aGClW
+sqq+aVBadEtux7eqruAzJiY/rp6boLZKN0DGraq3PyISDRgso5GUnk4eFiFCuaawbD9AVk1e
+yrysqsA7JiEvuJ+Zna9LMzzTramyTSccERYnUJ2YnbI1Hx0yXq+qyU9BQj9cubisqchBKCAq
+06Wcm6njPDhvtKqvVigfFhcrQKmboLFEKB8xZb6txU49Pj5DuLKtpb8/KiAnbaiem6bMTD57
+tKms2SsdFxUkQ6ybn64/KSErWsqyvFpHREQ52bCtoq1RLSAkQ6qenKPFUkXwta2v3i4hHRce
+NNShn6nuMiUoUdC3tPBKUFNCTbyyp6bLNCMhL7egnqC5WEpjuq+wyTYmIRwcKkSuoKS4Pyok
+NW6/tc9bbFtQP1+7raWySykiKmuon56qyWhSyLSvsGgvJRwXHi/CoJ+tWy4kLVLVvsb938l1
+PTvnsqWmxzElJzq4paCjsMPm38LBv81JNSUaGiI9rKOqvUk1NklIU1xSyrnJTDlAvqqotkYs
+KzNUtqmkp6270tH37sPxPzMfGh8v16mqvM5YQEVCNjpIfLi2+z8/3bOstOo+NTc9dLaooqWt
+v9PvUf5xVkwvHhshLcasr7TE50tMNS82PsextcxfT8q0u8RmQT44OFi3pqCirLvhQT5ay9JI
+NyIcJClHv7yxr7PPZTQrMjVlv767u8bEvdrgWz4+Nz7itaiho6ux0k0/OlH7Tz0oHyUpNU7o
+v7Csr7ZbNi4qN0/gvr21rK++5EE1NzdB0rmqpaepsshsSUVXbU5CLiEjJSkxPGi+rKWmrsw/
+LSwvNkdywq6prbjlQz8+RFPgvLGrqq61v8/Pzu9QPS0lJSMjJis76bOopqSsvV83MS8xNkbh
+wbm8wMbGxMbOy8fFvL7H0d7g28vLxsfWajsuJyIgIyo2dbyuq6+1vsLDwcfTXEE8NzU6QmnL
+vLe2tba4vcfZXlNTYtXBu7zLVjsyLi0tLzhIbd5qS0RL7cK5uL3GztfuVUtOYejXzczKxsXE
+y87Pzsa9uLa5w+xMOS8tLC0wOUNGQD49RF3Yxr6+v7/Ey9nl+vv/7N3Z09HNyMrMzcrBu7i5
+u7/Qc0k6NDQ1NTU1NDM3Oz9JddTKxMPDwcLHys/W297j3tvc3OHa18/EwLy5uLq+xt9QRT89
+Ojc1MzIyMjM3Pklp0srDvr6+v8DBx87Y39/t7PBx8uLXzsjAvry+v8bN2PteT0pDPTo2MjAx
+MTM4P056z8jBvr7AwsPIzMzN1NzqaF5q7tzUy8a/vb6+v8TL0dtoU0xCPTo1MTAwLzM5P076
+0MrEv7/CxMTHycrP2uDu7+nj3dvTzMS/v7/CyM3P19zta1ZHPzkzLywtLjE3QU95z8jAvr2+
+vr7CyM3Q09bY4uz17+XbzsbCwsLAxMfO2N7wZE5EOzUwLi4tLzM6Qk9y1ce+ubi5ur3CxsrM
+z9xrVlZaaPraysK+vLy/yMrMztb3Wkg/OjQwLy4vMTU5QE5v1ca8urq7vb2/wcnT4nZhVlZV
+ZeTVyMC9vL2+wMTIy9l5UkQ+OTMvLi4vMTY7RlncysO+vby9vr7Ax8/c82hcWWVv49LJwcDB
+v72+wMTJzdd1UEE5NTAtLCwtMDU8SWLUxL27ubq6u73EzdTh8mdeZG3x3dHLxb+9vb6/wsXK
+ztRnSj44Mi0rKystMjpCUejMwb65tbq9vb/L1uXn5WRXbevr18i/vru5u7y+v8LF5ExENiwq
+KSYmKi8yPlnnxLmzrq2vsLfAydxtWUlMTUhPW/7RxLy6uLe5vL7Eyc75Tj82LSoqKCcrLTY+
+VdfEuLGurq+wt73G221OTEdCR0tbfdbDvbm2tLe5ur/D019KOzErKSclKCsvOETszb21r66v
+r7W7wMvYXVRLQ0lMT1zmysK8uLW1tre7wc3UUTk2KSsoIygnLTI/XeS+ua+ur660tb3J0Whd
+TkdMTFJk387EvbiztLO1ub7O7kg7LykoIiIlJSw0QG/NurOurKyusbO8w9BxbUpJUU1detzK
+wry4t7e1t73J6086MCwnJSUkJiswPU7TvLivrK6ur7W5v8fdZ2RPTE9XaOnJwr+4tLS0tb3R
+4U02MCslIyQjJysyP0/Vu7aura6tr7a6wM/dcmFeUVhrdNjIw7m0s7C0ucPYXTouKyUjIyIl
+Ki88U9a9uLGtrq+xtrrAz9r5WVlaXHrUyMG9t7KzsLS/xfA9MywlIiMiJCkwPE7bvbizra+y
+trrC0NPhYmR8Y2nZycG8trKvrq+5vNBAPTIkISQfHyktL0jTx7qwrq6wtLjE2NXmTVNmT2rN
+x723sa2sra+zv1pINygjIh8eIygsN1zPvK+tq6yusbfF1exXSk9QS2PYycC1rqusra66a04/
+KSAkHxwhKCs0aMe5raupqa2ztMLv+l4/QlRMWNPBurCrra6utvVLPiwiIyIeICctNU/Quq2s
+q6irs7O40m9dSkdMTXLLwbmtq7Gxr89AQDEiHyAeHyUqNmzTv6uorKmmrbK3zOZlQ0BUTVLL
+u7euqqyusMlJPy4hHh4cHSAlLkvWvq2np6Wkqq60x993RTxGTFzIu7Ssqaytsc5aQiwhHx0c
+HR8kLkRgw6+sqqanqquxvsjdUlNbW9O+urKsrq+30ldELyUjIB8hJCcwRVbVvLaxrq+2s7a/
+xsvVzsrIvLWxrq2vtsdXPzMoIiEgIicqL0Bsz725tK+xuLzAz9t/YvvRwrivrKmoqq+/aT4x
+KCEfHh8kKS89X8q3s6+urrG2vc71V09ZddO8sqynpqmtvXBCLyUfHh0fIygxRG/GuLKsq66x
+s7rG2VlITmnYvrWuqqmpr8JgPS4nIh8fISUrNUJvyb61rayurrS8w9RsU1Vrzby0rqytssH3
+RTUsKSQiJSkuOUdc0ca+uba1trvEztzzcGV8zr21r62us8L4RzUvLSknKCs0Ql3cycW+ube1
+t8DS/U9GSlJi5ce5sa6vtclfRDk0LisqLC85V9PCu7u8ube2uL/aW0g/QkZO98y/t7KzuMxN
+OTAuLSsrLTE+98G2r66xr7C0t73TVDsyMzg/X8y/t7CwtcFjPzQsKSgpKy05WsS0rKusrKyv
+s7jNX0MyLjE2Qe3GurKwsbneQzkuKSgnKCs0Rde3rampq6usr7S9e0M3Li40PE/PvLaysbS+
+YzwyLCkpKiouOVjFr6moqq6yub/OWUE9OjY5PENm0Ma/uLGwtLnKX0Q6NzIvLi0vNT5T2sS+
+vby6uLe4vcfZfV1VW2Fm/9rIvrm5wd5OPDc1MS8tLS40Q3PMwbu6urq2s7S5wthjT0xOVWnf
+yLy3srG2xWQ+Mi4sKyknKCs1SN/Buri3trSwr7K8zmdOSklPctXHvbawr7G4yVU5Ly0sKicn
+KCsxRdvDu7i1tbSwr7bC31xPTFNy2s7Fvbawr7K3xWQ8My4qKCYmJiguPGTKvbe0srKvr7S8
+yd9gVFdj99vLwbqzrq6yucdaOS4pJSQiIiMnLj17v7SvraurrK6yusx1UkdERU1a5Mi8s62s
+ra+54T0uKCMhICAiJCs4WsKzq6mpqaqsr7XC31RDPj0/SWvLvbavrKytsr5yOiskHx4eHyIl
+KzZVv66no6KkpquvtL7ZUz85OTo+SGHUwriwraytsb1wNyojHx0eHyInLj3puKujn5+foaet
+uMhkQjYuKywuNUR3wLGrqaqppqy+Yy4bFhoZGB8rM2GxqJ2Xl5iapLpTLyQfHh8lLkbJrqig
+nqWoq7zL6UdDPC8pHhcXGR49vamcnJyanaGo2C4eFhgcJ1+3qaSmp6qxvm06Ly0vPHPCta2q
+qKq14SoZFBMYKdSonJmZmZ2nsz0gGBIUHC2/pJ6epa6+2k08My4vPOy3q6Wlp6eqrrtYKhgR
+EBQlz6ebmp+krb9TLh4ZGRwvvJ+Wl5ytSS4oKTE4UMu3rKWmqrPK5PbRw8xgOiYZFBUXIkyy
+n5qbnaKuyDwnIB8mPr+poaKrw0QwLC5EzLSopqq2Uy8pKDBNw7Osrra88TsfFhQVI8Sek5GY
+o7w4KSQfHiIoV66emJymxzYoJikuOUPkvLKuscH4RkfdvLCxt7q8urrMPh4QDxIfs5qSk567
+UzEvMykiISVDqp2ZnrozIiUx1bCttMPYzL7GzUs0MzlJyb3AvMO9sK2ywkMiFBEXH86knZuk
+tsfPVVs1KCkuVauenaG7Pi4qMz9GQUA/Xr61sLvMWEhNSFNQR1DYvKuora/IRzkuHxgcHjSr
+nZaYo84+KCUsKC06T7WkoKSyPygiJDLntauorbS/Xj4yLDI/7by0trq/wb/DzOJMU2Fp2DUd
+GRohzaCamaPbNSomMz0+a9G2p6KntE4qKCs6zrWwsbrKzGBCNy4uOVnNvcfc1ce5s7jEZj4/
+6bqxwC8YFBgupJWSmrMrHx8jNj5GzralnZ2sTSQaHSviqZ+hq7lnQDYtLjE9dr+2tre9yNBc
+RDUyPOy1qaWps9QjExAUJKuWkZSmNyEdHSw8W7Won5yftjUfGR41vKKfqb9FMTM/RV/y5MrI
+1HdNSvTDt7S+7EpARFT2yby8vMMvGhoYKq6dl52zLSgoLdHVzcLItq6vyUYxKz/Ir6m5aDcx
+O32+v7/fRD46PFTbwbKvtsdILiszXa+kpK5nKBcUGii5npmaptEwKycsPmS5rKmqtGIvKSg0
+zq+psMlOOz1LftjIzP5lRz4/SXPJuLS2wWhHRFrPxMLQT0RGSn7SRSgjIC+/qaCrwzcwPGa1
+t7/WSk1kz8zK2V7bzcS8y31KPz5P7NbI2ltSR0hmy7qytclYODA74rqwvz4gGR8stp+epMc/
+LT5i0b9eWfjMur3hOC81WbGpq7pELzNSv7S2zkk7OUJQaN/Swb/E1Ug3MDl6uKusuVw0Ky06
+a7u5SC8pLfiyp6y7QjNG6Lq2xmRGTuW+vNFcQUP1xL3GYz07R1Tazs3N53tXTklR38a5ub3T
+Zl5u1VU5IRokMa+cm6LKMB8pO96vtru/zdtgRDEyRdSuqq/CSDY3TNy/vtH5T0ZEQ0ddyLix
+ueo7MDA+2LivtMNROzc4SG7K1jcxMDnMs6y3zD86X9a5uMxfR0hT1tDq2+DVwcjcV0A/V9nM
+xt1oe3FnXk5O68y+tbfA0EkwKyEeKES2oJ6owDsoKjtvvbe8u7vD2UgzLzpus6equU4uKzNf
+vbO4zWNDPz9CX+XKyc3fXl1SXHrZzc7bZVNSV93Evr3LPiIhIzW4pZ+ntT4vMTb8yr+8xMre
+eUU8P0nUvLvA11JDU1/91NvW0tbm83Rt2M3Kx9DU2U9BNyYfKDq9pKKrxTsqL0P7v7q+vsLQ
+/Es3NkPVsqmsvU0wLTldxbm6y/pVQD4+TNjBu8HiSj1CV8q7u7/cVj85P07SvLm82TAfHyVE
+rZ+fqME2LzE6Xs/IwLq8y2w7MThMwbKyvWxHPkt118nLxcnZa01S78m9u7jMRTUpHxwrTq2d
+n6rwMSIoPPOyrK60v3Q9MiwwWbysp63FQzEsNk/RvLm/0GNCOjtOzLi0u+JDPUNb0cfEx9Jt
+UEM+U8++trlQIh0eKr6knqa1RjI5PVnd4M+9t7i/ZjkxN0vFubu/0G5YVFNe6dPIyu1MQ07b
+v7u6vsxYOyscGidJp5qbqWkqHyk+2bOtr7O40z4uKS1atKekrdw3Li04Ysa5t7zXSDUvN2y4
+rK6+UzYxOlrGtrG3yF1CPEBa2FMoJis8tKeovk8uL+jAubzbZ9vAyNhONj3mwLvIUj5D/7+2
+vM50TkxLS01kyLWvtMZOOzY2LiAiLu2mnKC6OiQjPcSyscP868XKcz4uNOayqKzLOC8zR8G5
+vcfmXlVLPTxF67mvt89LOj5hzL+/yNTX2M1YIBkeK7CbmafHLR8tOlfMv7esp7ZVLCAnT7Cn
+q8BJPj9Nc3XTwba2xVU6OUbauLK0vtZfPy8eGB0xr5uYpM4uIyo7Y8e5s6ysvT4pIilqsKeq
+uvJHPjk/Uc20rrbRQDI1Qe+/ubvJ3+1rTUZKX85rLSgtNuWvqbK8WT5KTkhRW0tp29fIwcbF
+zWZSR0daz8G2sbjHVTgwNkfQvr/Cz/tMRUVNY2nayc7O2UIsJzBVu7G6w+dMPT5JXMq+w81c
+SFvLubW96FFOW/Z8Z/rOw77NTj9EU9zO23NOT0xe1s7W529IPzw4RevY09dmfOJUUVdZ287f
+3Nfw2tLWx769v9RKPj9E8L22t8VdR0I+R1JPaeDOzNFmQD49TtjKxtV9Uz88RVzfyMPLyelK
+RUNM7sjAvsHMdU5NSlvUx8DA5k1HSV7i19njeOTgZ0xDS/bSztDifmFbVFRaXmlz+3VldW18
+9uHa395hTEhNY9DAwsvbdFxWT1Bg3szHxsveX1JRV1hYZv3e0crfVUtFTWJq/trT0/JXU09Q
+Yd7SysrT8VpZYWdlXmju397n8H56++Dd82lbX+/k63dxdvTt6epiUVJg3c3V6/Lk6W9YUVlg
+b/Tq5uTb2tjfb1dNTlBSWu/Tztfq9XpgVVps6+Xl3tLQ0drs+WlWWWF06+nn4enu8nh2Z1ha
+ZPfa2uP5ZVxdZvHn+PX5aW1nWFZZa9/V2O1lXmFhct7a2trf6e7/bm91/OHg7vZycXzt3+Pu
++/xmZF1j9ev3dXNnYV5fa2VnZGBo+Ofk4On+9+np6vVsaHHy7err7u3z6+Tj92Rnbnzu5up8
+bm99cGtpbPz58vx4b2ZjXl9q7N3e3t/tfGNcZO/q8/NyaG/u7/357Ovu7/Dv9Hpz+/j2fGtn
+aHvl4Ojp8Pd7amFfX15kbnL97Obm6/Xv5ubo6O77f3Z6cGdzfGpobXvq4uX7bWtkan/3+Pf9
+dvns8/H39Ojm929nYWVy8/b3/Pjy/e3p7fX1eG13b3J3cnf6em9oa3/z7PX97u3m5PVvYmJp
+dHFyeHZ6b3R1dPfs59/i5On29url6O/1fXNvZmJiZGdrbG51dH13d+7k4eX2dn3y7fl2dHV9
+fm9yevru7efpfmlnbnV79/V2aGNoa2r57enk3eX9+/dzfu/38+vr++vj7H1oY2JkZmppZGZ1
+8fZ8f/F8e+/t7PLzfm5zdnj17vDj3uvv7vl9cnT19PN5amdobnFxcnjz83pvcH53eHRxdWxq
+9e9zdevm5uXm9HV7+/H68OTj6Pj57vV+e/b9eHtsbG9ubm1uaGZ0/XF4/Wtsc3ZpZmp17uji
+4OLr7+nh4+3r63t68P9zd/vz8HtvdnR6cHZ4dXv09nn+end6b3BxcHJuevr8dHhzaXPx7fDp
+5u728ffx8PLu/f3v7O3x+vj19np0efx8bnt+cHF3dGNodW5rbGttfPDt8evq6+vy/vPn5vR8
+f/nv9vfy/np+/X198vhqZmVnaGh0ffjx7O/09nz67urv9PttZmp1+vDy+Hh5/X/57uzl5vZ7
+eXhzef98eu7xfXJuc3z9fHX7eWlubG3/9/v+9eru7fZvc3htcPrw9vn48/f19ff++Pnz8npv
+c375fXf67ufs8e9vbHB8dGtwfX59c2hnbW1qcHx4cXlub/7s5uzp5u7o5Obn6Obw7vh9+/N2
+aW95bWpoaW96fXZ6cmpubmZod+9+c3d1c/fq5OTl3+Xu8O3s6vL18v55cWxjX2dz/nFoa33u
+/W5sdG5v/nH+6efo8vT19vT6+ft9dfrt/n7w7/1y/evo+GxufGlpdXt1cH17b3/z7vD9e/fw
+dG9sdWlu9vh4e/T08+/18fz37/HyeXzz9vLt8Hh3/Pvw9HVz/vtsZXBnYW1xbmv96e348vXw
++Ht5dPrt4OL+bvjr9m1sdPv6b219cm7z+/bq6+7q6ur2+/VucX53bv5ubHZqbWRydHhxaWtp
+aP3k6u7y6uXt6+jn6Hz26uzq5fX9+3N683tvcmtdXmNmc215fXZ3cnft9/787uXu8/fv7Ppo
+bH36dm5ve3L7/Prz73197/L3+Pf68eTr4+hxb3l5anH19XpuaHZ8dm/97m9udWhla/fu8X/+
+9/768u32fuzycWx19/B7cnR8+vzu8vFz/vH+7ezh5P337PHw7npsam1nYmFz7vdqX3Xy8/j4
+bGl1e3xwe+zq93x9+fTp5+3u6d/q+vv4cHf2bmr7+G5raGtveX5y/nb88m9mb/jv7fP89/X6
++vx359/3evB67O1ybHb9dnRoam54/nnt6vPr8HBpbHb48m9t+PprZXTu639t/evnfmd67fX1
+7OPk6e/m5X51eff5cnhuZ3lna2NteWv7dWpia/xsZnJ6e33x7ejl6eHZ4Hxx/ebubn3u9m1z
++u93Znjs82t+8XBv/vz6fGt8+PHx8fl8a2htdGxebvt3c3ly+f797n/2/fTx+Op/ce3p83lu
+++Tk9e3g7HViY29/cn7q5+zt631qanZqaWJl+XFoa33x9nF08n37+fv/ZGVv+et76enz+HZy
+7u/m7+3o7nNz7+rr7+zm7nZ9+/J4Z2h5fG9y/fN/Z2R6amBqem9ub2109Ozp6Pfn7Pbm5nxz
+cu3ybfrm6evv/Obl73VmbP1bYGRaYPpsZ3Fr/O3o5+Tv6u34fHD08+5+/+Xs5ery7fL49fhm
+W2vu9vxs6+hyY2xoa3d+eGZgYXNzdXPt6/3/9+bp/f31fP30evjo3+Pe53h0evh1dHf96PRy
+/XNsb25nbO98aW9wbnRpZvfq/mt4cunpfOjse3p+6+js9/nmbG3s4uvv8nvuamP49mpgYWxs
+aXfw9nR9//jrfv344+1q8vvr4/dtdXhocnJ39fH77eb6cv/v63pqa2dicXl0b3r07eT5+unt
+9Pfp/mNpaHLwcfbj9G948PHs9G///nRtcf5+aV9u/v/v6P37eG5v+vDx6u769+v0evDubWRq
+eHlqavfwdWtv7H3q393md3b1dmn34uz48nZ1aHvqfm9pfHBla2Zsc3129ujg7fzw/mVfafPu
+c2/97e1/7OPg5Ht48fF6aXXpf3Frc3t5+Xt7cHPr9n12+Pb7+O7ua2r68v11Z3X4enx1/H7m
+6PX0b+vvc27573h+d3h8bGx6bP9z8+Pv5/Xr8XJseP9vdH5mc+b7+OrscP36eHl79vn6bXDv
+7Xl1+/L4fe/f6vZ5dnBpeO32fXZ6729w/ez4dvP6dn38ePd+ffnseG16a2BeeXX65+7t+v10
+dPb39ebl6+3v6/707fzy8nlr+flsaWhnZm13cf3rfWVp9ujq6ff9+mdl/Xxv/e309e36dO/8
+d/Ti7nj2cPf1dWj+6PZ0b/7+Z2f683N4fu/teP76d2dkYe/xaWz75uLq5ufz9+vp/f1rcHNm
+Z3zt93p97/H99Pd1c/NsZXV7f3t++uzvd+3r9v97/P/0e358e/35fXtzaWJsem16+X7q5/Hq
+4u3t8evv7+Lw73xkZW5uZ2tze3Bqde77ZWbz8XD+7fdta/zx7/x+6Oft+fv48vf3enp67e35
+fvn1/nx59/55b3T6f/js9//u7vp0eXx1ZmBnbmpqevj+cXfx6OPf4/D5e21qcvrx6fdvfXN4
+9Ovo6fv473Jxd3z4b2p4/nF08fd8/W1vcnL97vbv6vTu7PD39P5/fnt4f3VxbXXw/G1od39y
+evTz8fl+8PLv8/X5eHL++W1//Hh0a3T7e3z3+3Rsen56+n347/bu5+ny7/l3b/ry8+3wdm52
+cXd2amp+93h77fF0eHJwbmpmYGVufPXsfHHw4Obq6urt+/3z5er4/nN79/v4/Pr9aWl9dG1x
+Zmpwa23+8PlsZvPp7PJ+/Pny8ent7vd5e3vz7/xzc3n493t5eP//fHx9dX55aWNnfO7p7vjz
+cm53+er5+O/2/unu7+l9fn54ev15fXB47/l9f2tlaGz+8/f5fPvu9PHvfnRrbXp/+Ojs7+/5
+7unv+fZ/d/vudGhz+3pyenR0fW1laHH8+n5tfPPv7/jw7+/z/HVxdv/0fHT26u11anJ59OHn
+6/Z0bmx6eXv5fG5wc3r49/nw83x/+f537+1+dPPuenBva2twdHBvc37t6eXp7fJ5ev3z+ffu
+8XFvfG9xd3318P16e2trb2lpdXv9/X716ubl+nVzampy+fn/fP5+/Ono9P/97vP6+vHu83lt
+bGptd3FwbP7v9Px+eHp7d358e/j3cWtvff71+P/v6Ojm5O5+dXh5efv6fG5qcHpxbG1vc//w
+7vn49v15eP9+9Pr99e/r7fh9/nf97O/r8PxybnFzeXZzcGxsfn78dm9wc/73+f317+/t6Ozu
+7O98fH16cWlreHn9+/x7e3d/8u/t8P1ta213+/v8+Hx3cv/t9vL0fG98fXl8cH7v8vd+//18
+/vX57fd8eWtre+79bW1xb3D88u/3/npvfO7u9e3u/vTs7v18dXR3fvV98vJ0/PHy8fhvbXtv
+bHn7aWlvbGtqc3x6dH339/Tv7vH57OTi7Pr07+/v+X53b29yb3ru7+/7dHBtcmxpd/l0cX38
+/3vu9Pny8/5vbfru7+nv/XV68npnbfZ7b3V3dnzr3+f1fnVvdXRsdXB0/vDwevrq5+zw7/N8
+ff75+3P9/mlkaW989O/4emtqfXp2dHt5c3p6/H727PFwa3p+/H766ufp5/Dv7fHt6ezs7vb9
+cP769/Z+b2dhY2pnY2hrZWRucHZ7du/p6e32fnVxf+/9+ezs7vH37Ons7vjzfnD7/Xj37fP0
+9v5zamZkZ3X+fn13bnf1+Pn6dXZ8bGpsbXj98uvm4+Xn6erw9Ph7fX16c290fH1+9ft3fP3+
+bWhtb3j09u7zdmxqbHFvcXl99PD7+3519Ozn5Ofm7P9++XRz7Ox/e/73/P1wbG92/fj8b2pt
+bnB6+vbvfm/99m5ue3X66+b4ePr2+Hz29+/s+3R8fHjx6/589Pfy9vXz+Pn28/x1ffd5b3Bu
+ZWFqZ2pta2psc/DxeXd68+rt9/787ebl4d/f5/D9fXvy7O/t4d3g3drd4ujvemxkW1dPSkhF
+QkFDSFV22czHxMXGx83X4vRqXltbXWBs+Orf2dHNysrLzNLc4ePr+H9zXEtAOjQwMTQ8Sm7L
+vbq4ubu/xc7b7GVUS0RAP0ROZ9zKv729vb/Gys7Y7nJoYmBlaWxhT0I7NTExNDtI8se9uru9
+v8TIy9HjcVlJPTs8QE7wzL65uLi6vL/Izth8WE5UWV1pZVVBNi8tLS87T9e+uLe5u77AwsPJ
+1G9IOjMyNT1S2sS8t7a2t7i7vsnhXkxDQkpY9dfP3Vs+Mi0tLjZGatPFwL69u7i1trvKXz4z
+LzA2P1XgyL66tbKxsra+0VxHQEJIVv7f0tHhYEk4LiwsLjZIb86+ubWxsLK2vdFROzIvLzU9
+SfHGurKura6wuMfzUklDQkVMXeDMxcXWVTksJygsM0Jn18S5sa2rrLPGWTwwLi4uMjxO2Lqu
+q6qrrrW+z1pDPz09SfTNv7i6yXxIMSckJigtPXHJt6ypqamtu+1GNSspKiwySc67sKuqq62x
+v95XQjs8Q0tnyry4trXETDQoHx4kKzJNwbOqo6GmrbnpPC8rJSQpM0rEr6uqqamstcVaPDg6
+P0RX2cW5sK60xU81KB8cHiQtSL6spaGgo6q37DgqJCMjKjhpva2npqirscVqST48QlBh78e7
+trKxuM1FLyYfHB0kMk7Aq6Kfn6OtxEkxKCMhJCxExbKppaiss8B8SEJEQ0zoyL65tbW3uL3f
+Pi8pIRwbIzVWxayhn6Glr+E5LichIis82Lasqaiqts9hSz8/S2Xdx7y7vby7v8vR6EUyLy8o
+Hh4teM7GrKCgp67FRDIuKSUoONW3s7KurLTfRUNLWG/kz763ub/Cw8rY4NnvSzkxLiYcHTXS
+88ymnaKuts09Ly0nJTPWvLuwqq/B7Uk7Pk5Zfb+zucK8usXe6t3NzPVLPDkwIxwfON1eyaSc
+oq++3D0tKCUpPcu5ta2qsMpNOjc/TVPWt6+5vrm5y3Zu1sHHa1lSOy8lGxsv2k1gpZqitL3L
+Py0nJCtIybyyqqq53048OEBLXL6usb+5s8FaT2zgyMTr+9g+LSofGSJd7EWzm56tucRJMCol
+Jz7Bu7WqqLb5UkI3NkL2u66yvbixyUdK5dnUx87ayngtKCofHCdK8c2qn6Oqt+Y+MSsoLkvH
+t66ssL/kTTw7P1bLt7S7ubXBX1R06OPbzcbG5DsxMSUcHy5L5r+poKOvyHM+LSksNFG+srCv
+s8hvSjw9Sue9tbi6tLfOY2np3tbayLrQRTszKx8dJS060rKoo6Wuv2w0KiorLk64r6+sr77T
+VT08T9XGvrq0sbvT39zcbVFU0r1lPD0wIh4iJitZt66moaWuwEIuLSwmLfu4tq+qrLbRSj9I
+VFrZua+zt7e90HVSTUpLXetGNjUlHSUtKz+3qqajqK20cC4pLCkrQsa0qaetsrxbOjk+VcvA
+uayrtcTMz2E/NTpKOS8uKigtLjP8tq+sqayxu180LSsoKzvct6yoqa6830tCSmLQurGvrq63
+zdZeNS85LiYqKygqLz7ewbispqmvvMlkOi4qKy89Xcm2q6uvusfL12h+y728wL24u8nW7kY3
+MyojIiQnKjFauq6qp6aosMxaRDQsKi44Rl3OuLCxtri3usLHwb7G1NnZ3eTY2mBRTzgtJx8g
+KSgtX7mtpqSlpq7CaEA2LCktNjxN1b+3uLq0sbW1tbW4xetfTEBDTElJYXlIOzYtKysrMEH4
+wLKsqqqut8TnTTs0NTo7PEdo5NrSwbm2sq+trrfIekk5MTM8QkhxzcbH1GpJQjw4ODs/Slrz
+39POzc3U0cvHyc7R3WFNSUVFUefIu7a2t7zH7VJIQj9KYvXsz8TL6fhxTENFQj9ESUtKPz9F
+SU5e2sW8u7u9xM/pXVFYaP/c0cnKyc/b9WZeW2d22s7Iz9J26WFaSUlMUE1NTktGQkNDRlD6
+18zJxMDI0dp+ZFlcbOTl3dLU5Ph8aO9p7tbIw8G/w83Y5FZMSk5KSkhISEZGSEtNT1x76+XX
+09zf7Oj3eOzw2d3X29z3/fzk9OPjzc/LzcbMztTe5GtuXF5NTENFQkxGSlBaamdfdOtwbWt0
+++Lm2t3a39rl4frj5uLf6tPZz9HS1NTT0N/a4t9sX1dRS0ZKSVFQV1VaU1FPUVlZfXfi5tDQ
+0c3U0tzm7N/u6frfft/p6ujX29jV08zb3e51TVFGSktOREtUT15WXPz1feLa3dvZ7N3j3ev2
+YPnl+uvz3und5cva09nP0dbQ3M178VJMP0Y9Pj9ARk5hYNfTyc3M0M/l8vxjWFxbVmX939XR
+z8nIy8jHyMbGyNpmYUg9NzY0Nzo8SGPUyb27ubu9x9hcR0Q+Pj9GTV7q0Ma/ure3uby8vcZc
+SEA4MC8tLjg+WtLHvbKxtrvL4GRFPTs5PUhX58zEu7e+xMva3ex+1sxaSEw7OD86PE1T2b7C
+wbrExspeSkM6P01KX8/Iv7zGy9JwdFlMVnPfv7nHZFc9NTkzMj5T3ry5ube6w8phQz05PktP
+cNLLvrvBzntQTlRSZNjIurK0zUw6LSstLTRJ2berrK+51FtFNS8wN1PJu7e3ur3KZkU6OkVb
+0r+6t7O0tsc8LigjKjI82bqwqau510QzMzY5SvnGtbCzvN5MQTs7P0vdvraztbzJ09HuOy0s
+Ki9K4MG3trW4z0w4LzJAY8y+vbq6v8ttQjg3QWLXysK/wMTM2uDPxNBPPDIsL0Viybq6vMDZ
+UD05OkR0xr28v8zXfEc6NzxM3MG8vcDIzM/PycPOck06LSs2Tc64tb7KekdAPDpG2r6zs8B/
+Sz03Oz9Jb8m6uL7SbG/Ww7i1u81XNychKDVvsamttcxJPjo0O27Asay220MyLzY/V8y7urzK
+X0xZzriur7fLQy4eGiM1w6Odp7VpLiwyMUTArqenslIwKCczV8a2sLfFdDw5UcCtp6m20T8v
+JBUYKmGhlpuuUygeKThFuqekoatYKiAfLdKvq6y7VT43N1m6p6Cks+4/NjcuGBMiOqaUmaxG
+Ih4sXcS2rKmor3UoICIwtKaqu0kvMD9cvayloqnEOzI4ZbrfGRAcKKmSmKlaISE4WtzJvaqg
+p70sHB0uvaOmyjwuL03Jtamkpa5sLy08u6irMg0NGTaYjpqtOR0lNDdVt6mcnLQwGhQe0KCa
+odguKCxCvaqgn6rRNS1Fv66r0hoMEh7GlZSfsy8fKDA5v6ehnqw2HhkbQKGZmqswHRwmZKec
+mp6zPy4yVr21uUMUDRgnqpKWpMQmHSgvT6ufn6TjIRwcKquYmaU4GRcgV6KXl52xNyYnO7iq
+pqwrDgwVK56Pk57jHhoeKHqkm52oPiAcHEOhmpy3IxgcL6+al5qnTCglL8CnqapBDwwTIaiQ
+kp28IhkeI0CnmpeeeSEYFyymmJekLxsZINyfmJaf6C0lLr+nqLkjDA4efpiQnL0uHB4tNuSm
+m5mjQBkRGE+ZjpOtJRQUH8eakJShTyIiMEu2rSoVGyNtnZ6v1C4kO/Rqv76/qq5EJR8pwZ2a
+nrQuHh8tv5+bnaO2Mx4fKSksPkZF0bixscc9Ly85zaqnsk8pIzm6qqm9RkBUzrautbi4x8rI
+USwxPhwXL96vnKg0Kyovsaa4vc9DTlEvL1DBp6CyRy0lMbWem52qwkksISQqGBdOrp+Xpjkq
+Ix4yzrifm6fPKRoeO62dnbBBJx8tuJ6bnarCZDQnKCcVFUG3pJqpTVI4IytA8aSbqOApHinu
+raShq9ovIydfpp6dorJSLiAfKRkVRbqpmaN0XzUcJU7Ln5qozS0iKC9fqp2dqTscIEqrnZ2i
+q+QrHhwqHxMncsKcmqyvyiIdJynGm5yeqy4kJyE9qqSmrkA1XOS+rqWdouYmFxslFRc/xKqd
+pKys3iklJTS8sa+sr7DDNSo6dMnA6863sq+zr6ap2zYqJSgaDhg/sKSfn6WvVCclOl9a68e9
+q61sQ82zwUsxMF65rKahn6i+NycgIB0SFzLTvKyenJ+tPisvLyosOc2trLm1qKi+PC85Xs++
+tKifprLYLicoFQ8eKSJJqqGZmKWwr9YuIyEhJzzgvqugo6mw5UNi2dvOxsLrOC0zQfXORTxU
+Oy0zPU3Iu8a9tLvMalJe2NduY+LRaUVCV87G1M2+u8hIPEvex8LJyb3ITz0+SVpNPENsUT85
+NUReWXHCt7vNTT5KeNrLu7CttdJdT1Nsd+3X1ehq/9bJzWJFSlM/NC8tMT1I+cG9wMTT/mZO
+P1evrb2utk5NTj5O6unVxcPCyc/OfVxMSVRHPDQuLjQ4RtfDwLq/yshvXtDHytHbyt5FTNnD
+u7m3rau1vcBLLSwnISQkJy4uOMu2r6ein6GwxcNELS0sLTIxPV9vvaupqaeko6uvsck/MSMc
+Hx0YICknN+68qaejnJ+nqLPNOigwLiQrOT1O3Lyxr6efo6alp67LUTYhHh0XFxweJTN9sayn
+npyeoqWry0FJMycsMi8uN1rY0rKnp6Wjoqe6ycosHiUdFhofJCo1ybSyo5yfoZ+lr77STDQv
+LisqLjc/V8Oyrqqmpamvtsk4KyoeGh4gISk1X8i6p6KkoKCqr7O+7kY+Ni0tLy81S87CuKup
+qqqutMVAMiwfHiAgIyk2Uuu+rKypoqKmqay2y/JNNC0sKywyQGXOt6yqqaqut8o9Ni4iIiMi
+JSkyRk3Br62ppaOlqaqvx+FKMi4tLS82RVzUu7GwrKqwtblaOjcpIyEjJSQqOUNhuq6ppqOg
+payrteNOOjArKy4uNlToyLWvq6qrrbW8fDo3JiIkICElKjREd7atqKKgoaSqsb9TPjErKCkt
+MD1lxriuqqenqq2ywlg3MiceIx8fJCswRte2q6efn6CjqLfNSzMsJygpLDVI1ryvq6inpqev
+tchVMC0qGx0hHR8sNk/BrqOin5yfp6m8Tz4uJiUmKi48Zsa3rKinp6apsLjNXTIlKR4ZHiAe
+KTxkua2hnJ6enqezvj4tLSIgKSswUs26raqnpampqra9yU02JyMkGxgmJx89vsSwoJ6en6Sm
+suE1LC8hHzE5MfW1urGpqKerrqm05MvcMCImKBwZJCsnMsqvrKifnKGqqbVIMycqKSAtW05r
+sK6zra2srLa3rcJQ2WUpICgiGxwjLzlFuaKjpJ6dpbPBVTcqHyYvKzLQu7qvr62rtrmttci/
+wd9PNygoKh4aJy8oOLqvqqGgn6CuytA+JycqKzI/VMGwtLevr7e4ubi0ucTadFguHiYqGhwv
+MznBsaqdnqmmpcE9OCsoKicu5MjWtq20ur6/urnFu7G7yMjpMyYoJRwcJS071LaqoJ6ip627
+3DwqJiosLTvjwrqztLi7xca+vr+2sLe8u9Y6JyEhHBwgJzjBrqeenJ6irb9hOComIyIsPlfV
+uq+trrW/w8G9u7y5tbK40k01JB4dGhwiLEa1qKGbmp6iq8s/LiQfHiEpN2a7sK2rq6+5wb65
+ur68tLW+1EgrHhwcGh0kL/mupJ2Zm56hrto6Jx0cHiEqPs6zqqipqq+7vry9wL69v8PM9Dsm
+HRwcHCApN82qn5uZm56ms/kyJB4cHCEsRcGuqaWlq7G3xNPOztPU1cfB0186JR8dGx4kLEyz
+pJyZmp2irs09Jx0aGR0qRsSspKGgo6qzv9loWE5KU+LLysrXOSYjHh0hJi5ps6admp2gprTo
+NiMcGxwlOdWwqKSho6qyw/ZTSkdIUvDFuLW+3zknIB0cICcvYrOmn52fpKq47zQkHh0hLEbD
+r6mmpaetvNZcTlFZXWnVwbi0trteKx8eHR8kKTfgs6ijpKivub9YNi8uM0XtyL27u7u9yN1p
+YO7XzcrHwLu8vsLUaFJMV0kqHh8jJi4xN/u5raqstbi3tbfG2utrbFJCQkZM/tDJv7mxrrG3
+v8/xUj42MjE4QlRodF5ZTzszNzc5QUvzxL68uri4v8nR525aSkdKTvXLurKvsbW9ydNmTzw7
+OTk9Tmpe329HRn5qPz5R8cnMYUhLaHxZTElTdtvb3svAtrS3vc3d5/FabeJeW+jU0eRjXH5X
+T0RDTU48Tv3tzM584sDdRE5T+1xmSU5cyNH60eTI1+hfa+3Ybv7PynXK3k/kQfJWXExO5+vc
+2se/V8fq5mM9fETeVk3P1stv1PNlXF1aV0rYWeXYesLJ2WXdRms+TF1CWeBd38/RxsrlycNJ
+22TazUvk31XZXd1N7Vxh0UHaYO1X3GHJ4EzMQtY92UR0V0/bVcBOvE/Dz/rSV+t6y1rMY9vZ
+XsxbbEHtX1HrWchoX/Z02/NPQcM8yDfTUU/JUL1Hvd7J1W7N+N1V3FXXUXVX1WrR1lfIS8hC
+b0x4Y0nNVctS51xYbVBeS9xjeOrSWsHken3NxWHcbdrYVlj769le5/3EWtfr82NnTvtPUuFU
+WXp9S9RL72VJ11rYX+nq2PHSW71Kzell0lrt5OLh9ezWW8x64nHm2V/dRdpFaENI2UnhTdZn
+vlTU8ObXStVN61h6Y/vJRcBOyOZy3Nvb9W/W+dNS2Wv57F9N6l9t6UvaUmNm/tpfeH7Maftf
+6fhZTl9eX2Buau7eWb5hzfnL1u/T39d+3GH+aU/4S2tV5GPtXfxe3Fr+Vtle0FZ4Zd7bUstI
+ylTjXdt85WPpeN/Lac5d0nH3alfpTPNS/lhsa97ve9pu1GTb6Fvtc/Vwd2Xu32TaUs1f4OBe
+9WPPX+NS/m/XW1rhYHRN2k/tX+H78dd029/841bqbm5Z4fBv1nnk7ubc7F3YVNZV1l/n7X3p
+fGlU+F5Rcl72W/lo4N7/y1vZd+xi4mdZz0nRT99bzfLf8lnQ7mnZY2Xu52tj7mDbTfZd3lrn
+aGfc/91Y2Gbaat9s99xX3FnnYuFg2v5q/nBl/+xlbGvfe+Ze61/ZXmHdbelr9Xbs7WDY9HRl
+dO5e5lzZW2bcX/d+7GzP/ert63Lu3lXman1o+GRpaP3tYuBu/2hd4/nk2uJ25Vnn5FhfXuhv
+fPLtdOvf83To3vl6XfvzWW3+Zmpv+F/8V+5ffHZw4+vm7Obn7uPu5G3u8eZycPn/cnbg+Oj5
+5eNxdmr8dmtycGNddWRtX29senLu7PHrad7p8Xh9/G/vc/T+6+ju5nT2bOtsdm19bfR0+Hd7
+b//q83/r7mVl7vxp+n3vZPNfbPd/ZfbybtvY9fj07Gnk+WfhZV5162D63e7d+XNqamN8X3Be
+6+Nz+upmdXFf8F1Y33db2+vYzN7Pz+7o23vt32x4XlFJRURCR1ReavP739fa09fa29nc1t7i
+393V5tDY5NzSycbKw81qPzMvLS4yOEJO4sC2sbGzvMPeW0o/Ozw/Sl/g0MvHwsHAxsvHyMvI
+yMXI70gzKicoKzE9UNa/ta2qq6661k45NDIwMz1N4MC8ubq9vsLFw8nJxsO+usH1PCohISQp
+MT9zwbStqKitt9ZHNzIzNjlDc8u7t7q+yNLQzszGxsXCu7W3x0wsHx0dICxE17esp6OmrsJH
+My0tNURQ2r60r7PGX0ZBW9vPx766s7Gxr7hOLyIYGBwlQLqpn56lrMs3LCgpOFjIr62vs8Bz
+SDg4RFDbysjFvru1sre1tLlcLSEZGB4qa66noJ+puE4sJygrRcq5q6ivv209Ozk4SGHcxL+8
+trWvra+1vMNpLyEZFR0u662mpqOqvUgqIig126+trKu5XTouMz9L78nHx8zTwLauqamvvNP2
+OyQaFBgqTrWnpaChsU8qHyQ26LCqra2xXzEqL07e1s7SyMHNxrSsp6m31VBJajskGhYeONm6
+q6Sfo8IvIiEu/7uvrK+zwzYoLEjDvc3m1czsTX2ypaOrykpLcc9KKR0ZJDxG3K6inqVuKCIm
+NuPEs6msuO4xKTfQucBqS1lZQ0PPqJ2hsWc+TF/Pz0ErHRskKTuzoJ2fuzEnJClByq6io7Nz
+QTEzWs3hTUFDT1FXwKieobNdOj1K3rq5fy0ZFBsp5KKanKTXKB4fKXKsn52kxEQ4Kio6UGXq
+7u/X4dKxpJ+oyTovOFS/q6a4KBIPGC29pZ2cp18mHB81uKOdn6rdMyskJT+9uLvFd+fj5rSk
+o65NKixKvauips0lDwsVLb+fmJugviMaHzW2n56jqu4qKCUm8Kmvw/w7P1dss5+gtT8pLeqw
+p6CqYy4VCQ4rw56Um6W0JxcdLcWdmqCozCkiHyFspai54jw9TlS7o6O1TC811bGso6HEKBkM
+Ch7BppaUorM9GBcsyqKYnaq/LB0eJFOgn7pcOS45Q9OmnqnTOjRVuKynpb0uHhILFU+qm5ef
+tWYiGSfcqJydrNUzIR8r4KagtkU0Li49w6Obo8s5NEPGsKmlyiYhGg0VTaydmanT7SodLMSm
+m5+8TTQoKDF3q6O5OiwrNey3p56kxzs0OOGvqKSrNhwbEw8ntKSbnLpTOyIm6ayjn63mRi8m
+KUC9qa1OLjM7TcaypqGxRDM8YrWop6rZIx0cERZIqJ+bp99cNiIq2K6jobVXRC8mLUu8qrNK
+Oz9CX72spKXEOjlVyrerqsMrIh8TEi2zpp2ht8JNJCNFuKeirt1qRConMcmorl89QkNLbr2m
+n65YO0Fex7Cps0QrIBUQHuisn52pt2gnITLMrKSqvMhWKSEvx6ywzlJQRTtMu6afqeE9QEZV
+vKiuNyotFw8eSrqfnausty0kM0XEqa29t8sxKzVVvrvrU19EPV69qqCozV1bPkS/rrBKLTAa
+DxxEzKaep6esNSMuOmyvrrmttT8vOkJYyuBScFFF3720qKi6zt48PMC0wkw7Nh0SGzdxsqOk
+o6lNKi83Ps+9u66v40dOP0FXRELu39+/t7OrrcDJz09dvLroPzs7JxcZLUzuuKumpbZJP0I7
+PlnXt7DI0sFcQEs6Olpe4q+surWxxNvuSGe9z1ZlNzq5ThwfOi0oO1+vnqrErq9JMzc3a8pl
+y7PCV0s/SFxNy660v72+xcx91re8YUZHOjU5TrrKHho5MiE5uaicpse3vi8qPkhZxr+6ssZY
+YO7j52Hkv77K1tLBur/Ne0VEOy0vTbu55s7fIh40Li+/t6+kvkPL7DhM73/HxurdzdPJwc/U
+2mH1zM7NwL/J515JOTs9SmLuetZ9UE1oWkvQ1k1MQjZLdFVs/OjJXzlnvLa3yse0zjxG5dS/
+u/S9/to6Pck6Vt5ibdZMZd18SmnZSltEylfi22vH/W4/P0xZQHa+0c+8SeTcRlnFcsfk/tdK
+Zt1aXM9+1M5Py9nRZ9Vtb25T+9bV3/xd3kRQQXZsYuNo715bSN9MembczdJm2mL6SeZd6Phe
+y8/721nR2mhT0+jTz8LQzdNLYkg/VU3Y3c3c1HZKR0FPTk3Zz83O0HvcW1JO10vf1trQzNBd
+01Jcat5Kzcf/w/Bb7l5BSlVO3/DL2NheXEdeSvhH1sFav87xW74+2/bm4s57zlTn7FXvXl7O
+V+NPw1L21U74Tto72kTwSL9U7dze7FneRchIzd3M38neV/1NWGLMU8Xced7kae7XT8ZK3VTc
+VMpJxkPQTFBiTFj7b2PIYOjrSu9O5kzH2+rO5c9R1WtP4990zm3eznXkdlrZXVvt10vW9Gtg
+TVtHXE9xec7m6tLY1+NY3WVQ6eP7cNf/WPb3Wdn0Z+3N6/HQWe/kVudJ0WL/5WhczUvRUPBf
+X+9wb3V25v1q3mnIV3Jy6EvfZ2vNaODf7t3ia23lXlLN/1nQ9GvPVNtV6ETUQMV13/3h71lj
+X2zYb+Hc3nDtWl/oUmB0yVjTZdNq3FvUV/BO1Vzu6n5sx1DeUdhG33ZaylnIUtNNYFToW9Vn
+x1jbcWP9+uNodvds4eXycNtMzk3jYOda0mTq/eBeWv9saGxe03XUVtH7TctKyVj87XDYcnTi
+bNBY5PRr+E984mVp+PRXy0tt0m5s1VFv31hczHj64VXZ+F121VPTdmvb4Fxp/mp7WVjpb358
+6/ri4nPYbW7mauBobfd76/PZfOz+bOPt6e5pVlhXYk5defDpb9vdz9/g4eN2+WXmdmdc7+5e
+5Xvy5GdiXWJTTVFT7fPfz8rLy9nR5epUWE5PWU1v2+/e48nd2PfX1XlvZu9vXVROSU9NUV7f
+1NfX2cnY6Ghwb1lPVHjvfvfd1dF89vB9XWFu6uj439Hj8e5s3/9gZ3R4altkbG5kbXlkYltb
+Y2r739fPzt946ulTT1v+fvz2z8jX4e3dbFdUX2VUYXl8c33SaeLa2ePr5t9rSUpwW1pl3MrY
+4tjP7FJSWWVWVOre0uTg0tBtWVxfX1xe49fk8t3maFJWcmpq+9rO2ePj3vJdWmJsZmlz+X1u
+8/f5anny7N36eN/rb2Z19/p3fubu//P06mlqa2ZdYvpkftTq9+Jy+3pee+fs/ODf4epq92xi
+XFtfam/u3t3z9238Z2RoavZ27ezk4uruenv49uz68+fn7/T8ZlpbY21oa/Xk8fhuZGhrbfjt
+6eLZ3ef49PltX2hvY3ni3e3x4fpdX3B5b2xx4upsZ/jsaWz+9v5w9N7jb3fy83VmdPN0eevs
+8m9+9nts+fLq/u7oeGtqdGx1/XJv5e9q6OBlamj+7HBv6/dxfPft+Xjq5u7q5/J0ZV9janFn
+euLd7Xt1dG1hc/Ry/NzY5nvz+mZv6n5ydl1mbV514OHh5OrhdVtfa2ZiYHre5Pfi4vXv3Nhp
+TF7kT0/QxsbObXl4QT5c+Xjez8nWXVtvWE5o3/P929nvX23deU9n1+tn7M7SZ+jP+lZ36fdv
+UExNS11k58nJ0t7uZVdWaurq5NvW6lxZW2R+6+Xm9OrgXU1rfVVi1cvM1tndXk1Yb+TQz8jQ
+RThAQTxZw7q9ydPqRztEa9vPycjOYUlMXVZR0cbVzczcWkZPZ+3JwcjIyPg9KS1OPkO6rK/F
+aXBHMDRuvry+t7xROjc8S1fLu8DeX1xQS2XPwsHN1dFtT3PMv9EyJjxCK0GtqLbNy8w5KjnT
+xMq7r7pJO0I+OEXLwNHCvtnyZ1VjXuC+w97JwcvGVDAqIS5IP7uiqLvTSjwtKki7tLCwt9k6
+MDY+P3DGxbzLb/vf72vgxbvF3cy+xtnVSy4eHUg9NqqdqbvvTzokJ2e3tK+treYuLjUxNXG6
+rrS+vM5USFjZyMfBvbi71uM+KhoXSUssqpehuepNPyIfTbKvraqlvi4qLSorSrqqq7e5xUs5
+Sce7u7awtcO/Oh8bFyc6LrOXnLC61DsjHji/xLKhoLQ/NzMmIjLbta+yrLVlTc+/wb65tbvP
+cVQwEw9KPhm9kJq0srH9HxgsxdXYo5enP0hDJRwnaMm/q6Kvz8q8v8vCuLfTQt/FHQwluhgd
+mpGstqWqPhkcTkUrvZedzdOx9yAhP0YvSaypu7alpbnBxXRLPVBGGhFbXxEvlqPboJyuRiQo
+Oycfy6Ozrp+v2V4+LigtO/HPsp+fpqWqvlstJjQhDyGyJB6jnd+zoK7GRS89OB8m4c69pqCm
+rb3uOCcoNj1jsKKen6CrvEcqKRcNIiwYPpqot5ygt7xSMjYpITRCPrmlqaeosLnfOjc0MErW
+1bOjpay94swuFxkmHxkxrrS9p6Ktusfr2kUtPk88SMK5trCysLXH7VY+NTpAXc68t7fG9l0+
+NDQuMVBHQL3CPvPsOEraaM+/y7u54OrZa2J84M3L1HlUT0Q9Scy9u6+ww9pOOTg1NlLXzb+4
+vtxNPz4+PURj2tnOyedST1ZSXdLN19HhYElETlxjyre4v77KYUc/QUtQe7+9w8LTT0VAPUNR
+WPfP2eHX5VtXb/f34ePvfF9RTk5Pbs/BwcXG0WFOS0hLWt/KxMbR3/tPRURESFZWWtDI2t3P
+0ebv29f8d+hhSklgf/zbzs7V2u5iUU9gduHW3tPW9mBbST5DSkhL/tDPyMPI0NPT3X5dYF5N
+UHzi7+DP1/fm4F9dbGVkbnr+5OHq7/5dT1JNRklf7NzMyMnP1tHdaFpdVU5We+Pw6tbV6H3v
+cVpeXl947t/W3Pjv9l5SXOZ/TV7R5VfbyfxZ9ePudmBZXGJvau/Qz9jd2dt0XFteVVJp4tfU
+1Np4YGBbUU1XdeDg7uHf6m5aXGNmdPju3N50feZu7NfY2d3c2flSW9biUE3autI1P7q/Ojbj
+vV43UsHcRErazG1N8srO73fXzuFbX+zT1e/cytdqXGlvT07f3GFPT+vZXVvnc0pPzMF+RUpv
+znpAVcHEWEjGt9JMV9DI/klZ2uhcT2V5c/nzz83iYnvtVEtX/lVV5+Zq8nDd2mlO5exaYvHX
+2mjrz85OacrJR07c2XNL89PPZVXcx3FNSc16Plxn8OVqaMDi51H7y2dK6vDX7lxr4u5i4ljY
+4VZ4dtrtS17OX+9GzsziP/nMUXVVWc3TU1LDx2hByeNlXslP5+pU3thqTc5YaG3sVufOXFbv
+ZN1tWdbjzlxd02r4XlrdYGh/62PdY1/eeWH6zGFY4VrP6FFv1dxrWPPJ6U5bxc3fSeV9zl5G
+Xu/9T1he2XFd7tLaXFTL3Wli5nvv4ORa59NQYVnW/Fxn5eHhVO7J73D901/nXG1VeGds3GvZ
+X/1X1d/oZFzmX19MzmXlXXTj/t9b4nbjaWvje9Xa2mTd1fN0VHJ99Et8fNRoUfXZ7lDoXNJO
+WmT9cPFt2Pve2mrk5dFk7Wvc83pt7Ph0fGd9bn9S7GnpX2T96etn3m7ebFxbdd7e6e3e83z7
+d2v7+2BwZXDrcVvt6Odr+9jXaGbd62pg/OV7+Vvj7/D0ZmF6YGXva/P6ZOx28nV71+zr6Xrc
+7fdyZeptdmp+4mVtXWZgZWPudPZ86t7fffj59/n79fRy3n7W7OPe7W77Z2tfaGtvbVtsaWtp
+Y2pl/n708nJw9Pr86PPa/eDj2+l16Xbl7vLy2+Rr7eX+3Pjw2Nzc49XsVk5WSkBBRUtOTExo
+4+v028/MzNLO08/a3drn2N/j4eneft5t7PLi89fz5N3Z8lZRTFM+Pz9EREJLWdrf19HMzM3b
+1tXd3W/k7Onn8ebq3O3Z0tjT3tnS2+je6t7q+mtWU1ZRQkA/QUFDSFRq7eHSycjJ2tTM6tZW
+X/th/1LieNZu187ZydnNzdjX2N7V/trvYWxOWlpdTUhMRkxESEtYY2Tm39rb2Gls72r1b3b+
+4vPZ09fIzc3LztHLx9Po8udbb2hfX1xlVmhSTUhMSExPUVlmcG53fenq7PHx7u386+Pn4dvX
+3t7b19PX09La2eHk5v186+7n9+v5fmx2WlJOTExKS0pKTFBSaG379fv3aPzs4uPg3Nra19HN
+zdjc29PV3eLr7OXh/u3i3dzh4uV5c2VbUkhHRUNAQklKT1RbZGz96tza4NfUz9LW0tXU1NjU
+19rW2djb19vg4d3keXP9a2JfW1pVTk5LSkpISUpMT1piYm7y3tvY3dzb3Nva29XRz8/W29vh
+3dTW1t7d5u3yeHRvb2tpcm1eYWljW1RiWEpNTU1NTlJaZWpu7d/m39rQz9DW1NLY3+3f3mPX
+yObf2Nl0SVTDcVjTx+Zh9+DYaVtsdV5JRkhFQ0FARk9OatrZzsPGxsXYZGReSFJZbNLIzb68
+yc3V1cjUdtvR4tXHw8LQY049LiooJiYrMj1lx7OppqinqbPFXzYvLyopNkdXzbmuqKqsqa28
+ydBrT0VKSz5ERCghLyceKzY9xLO0oJ2qqqjDSD8qIScpKT/Nvayoqaeosra7ytZzYs3M48tp
+P0wtGh8oGRozU3avpZyXnq2nsS8kJyEhIi7Cs7amnqayu8TSPjjbxWfIrq+1t8BQNyEWGx4X
+G0W6taKal5mktbtIHxsfHyMu76aiqaGfsmhGPDk0P8exr6qlpqy04y8mHxQQGyIgOamdm5qb
+m6hELC0hGx8uRce2qp6kwMHJMigtNlPKsqGfp6erweZVOSsiISQbGTBMMuCenqWmqq/eKSU6
+LyY/tre5sbe5zzI3VDk5yLOwrKuprsf1fUlFcFtGQzs5IxkjNikpsZ6nqaKhtD0qNjcmK864
+z8W6xtdINF3NVMOrrrGzvLzWSmTh7MvE30c8NycXGS0yLdeamKKppq42Hh8vLipPqqi0vb7X
+SDE50sXDs6uwu8zh4lRIV97PyL7OQjc1HxYfPEBKqpeaqry+Qx8aJUNR6ayfpLrkSTExMkC7
+rrCursZcT0NLaHf839LCsr1GTz8fFx42R96rmpquXUYyHhwrz6+spaCq5C4qJypH4LCip7S/
+Yzg4O0XMvcTHwbyysMZJOSscFR5L772gmqG8PCwrJSVLrKarq7FpLycmL0G7qKqss9s3LTRK
+5ci1rbLP3bu2uLbgMigeFho7w62em6C3Mx8gIyhGrJ+hqbpMKR8oOnK2paKy2Uc4Nz3msq+6
+x9RqTHS/r6yyzDQiGhAazLWunZqlVh4dKS46tJyYp908KR4cLK+lp6Gn4S0jKUjHsqiquks1
+PVDYs6qorMAwHhkQGMGsq5uaq0ccGCg3W6mbmac+KCQfIT2mnaSuxD8oIy7DqKeqs+MzKTTa
+sqmmpKzbJhoXDxuwp6aanrU1GBgvW76gmpyvMSEgISrbop6ms+o1Kys7tqepr8pCMi9Evayn
+qa29NR4aFA8vn6efm6fOJxQbTcSsnZqevyQcICgxwJ6dpsBCNCsrRK+kqb37SDEwXrirq7i2
+yjEgGhIXsZ+yn5yvPRoTKcbCp5mapjsdHiUpP6ibnqvuOiwlK3OrpKzDeD0uN/q2p6mvtVgo
+HhYPL56uq5ukxSgSGlzGtJ6YncEkHSIoL76fnaC8PDAnJTe9pKCwzV40Mj3zrKGrt8MwHRIM
+J6CxrJibrS8TGUBK+qSYlqosIicmJDusn6Cu0kkvJitjraSruchKMjBAuamqr8wvIxkOHK2r
+rp2dpfkaGDBKS7afmJ5RKSwpJS5LrZ2mwMVaNC0u26quuri9/Dg137O1u9s+LxcQKMjIs6Gc
+nu8fIzQ1NmWpnKbeR0k4KiM8q6i2trHCPSszzL3Itqu0+T9L18jCw8lbHhAdRDhRrJ2aqTYu
+OyomL82lp7W6wFk4KzDMwM22rrbsPEnx6826tLpxQ0xDPE7Szvn6ynVAKyJI4DNJxMXISUK2
+r8XDx9JOKy09Rf2+qqKsusBUNCwtUb++s6quy0E7Nzc8Rs3HfOTV2M/HaDU4NCcnLDrduq+k
+pbPPRD02Lz/VuK+yr7X9RUNATH3VwcTW21pIWvj4ftlQVEk9UVPMytDCyE8/Pi4+SE/Pur/e
+TD8/PVL8vLa4tLniUT9EXVXDvby/79BIQ05tV9O/QHHYQD5oztTMeNddOj5AOXu547G6TmVQ
+NT9PSbfBw713WmYzSdZBxrnayN1PZuvo12zL8kZ3SkjKzlO3y1LNSTlJOEfG+MKyw3ppQDw9
+ZFnNsuK/0us2Xj9MyvLdu2Rd5U/mRlvvbmrNU8pk6NHGWNndSmxMVGjd1Wu729FIRnE7Tutp
+wMPRX8zuOdZYVdHe6uLVXX155l81uUhHwjq9w1POwFZSZ0TJTj7Fzk6/VE7HPUXuyGruxrvM
+WWz03T9T0t3L723H10Y+zz9PWlv22WPG5s9TVW5RRnVg6ODjxdLfT89OQtJqWLlDucrs8OBb
+7Dpz0Nxb4sFg2WH5f0o+RvTNO8HJS79R0eNWPcpQT3jDVc7KWM9cakjMW1DEadjZ69lf6mxp
+Xl3RTtltXctX3WtOw0XnZGhZ28lMzfZfaXxh2lfZd13XbndN31D3anzMVcjNVuvjRspK1dff
+attZzNI/0PhZaVtf1UvSUelwVE/BTml43NvpUsjpa9FWTtdIeevV2/rNv1HY3T/KP+5vZmTO
+X9j9aGps8VNW3lNm3mXRdNtPwWZkdc5W60rHS9VgXM5mWF3Qbe/63dBd2HLh7eBd9WF09vHv
+5lfaT/1ifE/qW1pp4l1h1FzLU+LQVeTw9ctHyV/NbHrj89ZjctpgVF3z/U/wVs56UNJaYOhc
+3UzNVc5azmFh29pTxlTpbudi/u3oXORG1+BiU9JqZGru29pO4tH+X1jLVNRZ1F3UStxjdE9q
+VdR99ebdWNdWUM/zTdZh11bT8tX3b+Bu9WTi3OVfz23NWmfjXmhUX115Yl/PV9ZM4VlfVOLu
+5Ol+2d1m7mxf72jtz/Dl4ufhYvBZ3FP0WfLeX29v3u/ye+xS3lzSXNBe3uHoa+l7d19eYXpZ
+fPr7bmnyc3Js62rh7PHl6Gb2fOZ02VnYdWXs7Ot55nf4X21+al/u+27uWd587Gn25WjdW93+
++F3Vfehy6XxvZm1k+2L7XOl19/5h4nlm8+ll3WPd79tn1mn2+O5f6GzrbW3d71nzXO5zb2H0
+ZOL7Z+V8dWfnaO97den7bvLm+fZk3fHTZmfpbflt72b9al/y/nJo/f5lc+ls5eVu9Nz3dur1
+6u15bO1rYnxq62Vzdmn2dO5m7vth7ulydvv/Yfna7vft3fB97/h7bHZ3bHlh+n5993llduts
+d/Ppfv1u+/ln7fzx5/Z6cHd4eHDv6u/x/P7u+nbwbnVx+2jxavVub+p9f/JnafppZevm5fDf
+7+zqenvr+Plv5HhXcl5ecmxb8edpbvbod/nr5fXn9Wru7WBt9t/offLl53947e3jX/V3cm9z
+/V1XbeP+ePTcdf3naXRs8Proe+5o+f1XZuXja2fQ1nBsbNtvd/jp5W9tZOplYltyXXJ1e957
+6Nvp7fX79exjf3lcd/J04953+W3i3vFe1nn07vz15GtX8G9tZ2p531174n5m9lpobW52b2bg
+/2zt3/HTzd7P0M7r8HTe/O5dXE9MQT08PTxDSVvdzsa+u7u+vsbGy83KzsrFxMG5s7s2IiUt
+KCIeI0K8vriupqGnv+JYRDYtLDv1zsi/samprq+urrO8wtQ1HxobHB4eIji3pp+fn56iuEUp
+ICIjJzbPsKalqKiqs7i+vru8xNE7Jx0bGhwgJj+5o56bnZ+nwy8fGxwiLE20pqChqa6zxdXG
+vbi8vbC1OB8YFxkeIyvhp5ycnqStyzcgGx4tRc+0p6CkrsfZztTNv6+rrrCrqz8ZDxIdJCYt
+y6CZnq7L8ko0Ih0qzK+xurKlqMVIXb+7zt66q6qusau9KBUPFiItNVKxoJ+u7z5BRTYrLlG0
+rb3JuKqsv3XVvMBrVsSsqa62rapQGg0OHjM/QNCpnabIOzRGXzs3SOe4vMm6qqetzktYVkZC
+366jo6qvqa8sEwsOITZM1bmnnqzbPjRNzU08Qk66sbu5r6yzUi81RNrAtqefoaq7xrk2FAsN
+HUu7vraroqjTNi5DycxDQknYur2+s6+5ZzA1Zb61ta6mpa/PXMvUKRQNFTG7rrvMs6ev1zg2
+zbbUOC8zVcDBua6vulI0Qsu5vMm7qqq1zNe1uSwUDhYzv7fIvaefq+ozNPbTPCYmNN6wq6im
+r9k4LDjyz8m+sKeptr3Cta87GA8QJbyqq62vq7U/Kik1YPA5OVHMrqqvsspAPDE19sKzrK6t
+r7mzt7q44CgWEBtHsqm3yMHETTguOtDHXzY1Pl69tK+tve9JOU/Iv7u+uKyrq6uutcYtGxYX
+KErKsbe8xVA6Pj5VaTs2SVpd0rippLBtPzk+U+m9s6+rq6mpscfhWioZExkzx7TM6NnJ005D
+T9PSTi4vS9evqaypu0Y7NEbNy766uq6rq6ewwdE9KB0VGio2yMfTtLm8v1ZGSzYwMT/Eu7Cp
+qqvCPjk2PlNsybetpqWmp7LFQCogFxceLlG+vbe0xslqTU1BPDk+58G6sK2rsHk5NDpMX+fA
+sqimpqetu8lBJhsWGSMxTcW5ra+7xHlZTz42MDnlwrmwrqmzXzYxPEph3L6tpaOkqK222z8m
+GRUXJTvsv72vr7bKTkBBRzszN1m9sK2tr71MMjE8WMy/taumpKass7zOQyUZFhsrPEn207my
+usXpW2liQjQ0R8i1sLK4v2xCPkNW38e6sKupqauutc49LCEcHSQvPkVM3ry2tr7V4OdjSjU1
+TOG8usPEy+h2ZV3dyL+3sq+trrK2vtVXOCYeISouMS8xT8O2sbjBw8XPUzo5RVn519DMyMnM
+ycbFxcW/vbi5wcnf5flXTkQ8NDM2MzM2OkRg29Xh4M7Iwb7FzM/bcU5BQEhVcWb8z8C5uLi4
+ub7G3OvbzcPKbkQ8Pz43MjAyODo9R13Tw8XJ09ze9VdVYO/a/2/41MK8vb/Bv72/v7++vMHU
+a1BFPT4+Ozo5NC8xN0Rc/vH11cnM0dLQzM3rVk5d28zKysfFyMfCvbi3u8je7tvT61lPTEM4
+MC0tLzU6PEBOdNLFv7/Bydbr6trRz978bG/22si9uLa3vL/Dys3Qz8zWWkQ5MzEvLi0tLzY+
+SV7bxbu6vcDDxcrO1t/4Y1dUXuLIvbq6uru9v8TJycvP12hHPzs2MC0qKi0xOkpp18rEvbq6
+ur7L5GJTUlVp6NjRz8vIwb26u7y9v8TKy87V5lA+NjIvLSwtLzdBUF/kzsjAvsLGzNrobmh4
+7/R6/vXw1L+3s7O2ur/Eyc3S193xVj4yLCkpKy43Pkpd6tTHvLm6v8vX719VVl985uTh3dLG
+vry7uru9wsnNztDPztRpQzYuKikqLS82P1Lt0Me/u7m4vcfad2BcWltgZ3fk08a8trO0uLzB
+ytbf4NrRztxNNy0qKCgpLDI8SV7byL22s7K4v890U05YaXf36+jZyr+4tbW1t73G1ePo6+Pd
++Ew4LionJigsMT5X48zAu7aztLe9yuVbTEtPVl5sfeTNwby4tbO0ub/N4vDu6+LoWj8wKicl
+JisyO0/jy7+7uLWztbnH6VhJRUpOUl5659HEu7WysbK3vsvg+nfz3dlqRDQsJyUnKjA6SGjV
+xLy2srGyt8HoTUI+QEdOXHLr1Mi9ta+vsLa9x8/c3+Pk43xPOy8pJSUmKzI+VODHvbi0srG2
+vc9dRz8+RU9cc9/Rxbuzr66usrvI2/N18+ni6lk9LyomIyUoLTVCZ8++t6+srK62x2dHPz5A
+SVRp/eXPwbiwra2wucTW/Wp3593b+006LSckJCYqLztN68S3r6yrrLC94Eg8OTxBSlNv3c/C
+urOura+0vs3ncGl74tnZYEEzKyYkJScsMz5fz7yxrKqrrrbFckU7OTo+RlBo3cq9trCurrC3
+wM7rZGzo4NzcX0AvKCQhJCctN0rbwbWuqqiprbnQTTkyMTU8RlJz1sW6sq2srbC5xdxrYnPj
+1s/WXz4vKCMhJCguOE3XvbGsqKirscB+QDYwMTY8SF7fyr62r6yrrbG6yN9kWF/l1M7eUDks
+JiEhIykxQHTItq6qp6irtMVcOzAuLzQ+TWzWxLqzrqurrrS+z/9dWmzczc3mTDUqJCAhJCo0
+ReTEta2qp6msuM9MOC8uMThDVuzLvrixraurrbO/1WxYV2bk0MzfSjMoIh8gIyozRte+sq2p
+p6iststOODAuMTdBV/LOvravrKqqrbXC41pOT1384N9tRzUqJCEiJSs3TNjAtK6rqaqststX
+PDMvMTc/TnDQwrqxrausr7a/zvZbUFJcf+D1TDouKSUlJyw1QWjMvbavrayts77lRjo1MzY8
+RlL+z8C2r6yrrbO8yOReV1xfY2puWUc7Mi0rKy0xOEFU7M2/ura1trvC0WpORUE/Q0xe5MzB
+u7a0tbi8wsnW+WhkX2BjYFxLPzs3Nzk7PUBFSk9WYHXm0crJy83V3N7e4+vl5d7a083Hw8LB
+wcHDxMjO1OPscF9YTEU+PDw8PD09PDw9PkRLV3rVy8XDxsnKyszMy8zO0NXX1c3Kx8fGys7P
+1dvh63tiX1ZNSUdEQT89PTw6Ojo9RlRw18nFxs3Ny8vOzcvOz9TZ29vSzMfHxsfKzM/Z397i
+3+9kV09LRj8+PDk1NDY4PEFNZ9zMyMXGx8bGyMvLz9TZ5urb0s/LysjFw8THy9PW3ersbFZM
+R0E8OjY0NDM1OT9LZ9nKwb/CxsjL0NXX3d/f6Oro3tTMxsPCwMDDxcjMzdDZ43BURj88ODQy
+MjEzNjo/S2TZx8HBxMXLzc3R1NLZ3t7g4NTJxcPDwcHAwcPIzc/R2OlgTkQ9OTQzMS8wMjY7
+Rlrlz8S+vsDCxsjM1d7j3+Df3+HZzsnDwMHCxcfKz9PV1NTcak1FPjg0MjExMzc6P01u3c3F
+wb/Cx8zR2d7p6evq3tnX0szHxMHAwMHGy83PztLZ41lFPzw3NDIwLzI3O0NX6M/Gwb++wMjM
+09/r9nd9dfjr3s/Iw7+/v7/AwsXJzc7U32tQRz45NDEvLzE1OT5KYuTNxsO+vsHIzdfl9HFo
+YXHs2s/PyL26vL+8u7y/w8fFzGZAOTAtKykoKi41PUv3yLm1s7Kzt7zPZ1lLQUBCR1dx6Mq6
+tbSysrO1u8HCy93fcEg8Ni4pKCgnKi0zQW/YwrWzsK+zur/P6W9NQ0VJSVrr0cO7trCws7W3
+vMHEzdrZWjs4LyYmKCQlLC82T9rGtbGyra63ub/XdE5HQz8/TFv4y724sq6vr7G2ur7Ezdta
+PjctJyglICcrKjdZXMqws7Grr7e3wdlzSklIPkRRT37FwLyyr7Cvr7W5vcbM3klDOisoKSQg
+KSkpO1xewK+1r6uvtbjBzWlMUUA8TEtF3sfMuK+zr62ytrm+x8vfTEE0KSknICUqJi5GQGq3
+uLaqrbSvuMnMZk9UQEFNQE/U38m2uLStsLOyuL6+wvxVSi4rKyQgJiclMj5A67q7taqts6+5
+y8xbTU5ARVFEV87cw7O3s62xsbK5vMHLYUw9LSsqISAoJCY6O0LBt72uqrGxrr7Sy11LTkZA
+SUxf18y+t7SxsLKzt7y9w87uVj0vLSgjIyckKTY2R86/va6ssa6uvsbFUUtYQD5RS1LQyMC1
+srKvr7a5uL/Mx2JGQi4sKyQjKCUoNTVG0Ma7r62urq65wsD6TlhEPE1LRt7IzLmytrGvt7m6
+v8bB3ExRNystKR8nKSMvPjdbvMa5qq+yrLPJwdNJVlk6P1hDWcPIvq+zta60vLi+ycPLZ1M/
+LS0pIiQnJSk6OkjOwr6vrbKurr3Dw1ZMXkI+VUpSysvJtLO3r7G7ubzIx8npVVI5LS4pIyYq
+Ji08OlDHxL6urrOuscLGzE5MVz9AWEhSyszFs7S1r7K6t7zIyMn3VFQ6Li8pJScqJyw6OVLK
+wr2vrrOvssXNzEZFWT1AfU9bwcfEsLK2rrO7ub/Ozc/pT1M+LTErJCgrJSw9NUvIyr+vr7Su
+sMHLyktCVEA9ZFRNyMPHsbC3r7C5ubzKzdPsX0dBMi0uKCYrKik4PD/eyMq3r7Wzs7vLyPZH
+V1A+Uu5R2b/KvLG5t6+6vrvJ29LkaFRKPC8wLCgrLCkuPj1Ux8S+srG2s7TC0c5XRVpFPFhU
+TcvCyLWxuLO0vr2+zdPX715PRjgvMispLS0sNkI+dcXIvrO1ubW5zNrRUEhdRD9fXVjJxMS1
+s7eytL2+wszS3OdkSkc3LzItKS0vKzhHQHbHyb2zt7i4u9Te30hGWUdG72lvwsHAtbS3tLe+
+wsbO2t3dWU1LNi8zLSovLyw5Q0Bgysy/tba7ubvT499ORVpOSHzr9si+vriztri5vsfKzNbl
+62hMSD00MTAuLTAwMj9HTODGx7y0ubu7yOTxW0tPUE1acd/Lw7+6uLe3ur7Ezt3udf76aU1J
+QDc1NTEvNDQ5P0ha3s/FvLy8vMDM419UUVBXYm3w1snDv7u5urq9xszZ9HVwZXP1Z1JIPjo5
+NjQ1Nzk8RFFm3c7Jwb/Dx83leWtkYnzx7t/X0MvDv729vsHEzNTc5O/683zzblpPRz47OTYz
+MzU4PUdTa97QycPDxMfL0tXb7PXv6+HVzcnDwMDCwsbKzdHa4OPu+/P0bFtPRj46NTIzNDc7
+P0ZPaOPRzMnGxcjLztXa3d/f2Nvd0s7Lx8PCwsbJy9DY2t/j4d3ceVpORT05NjIzNTY5PUBL
+Yu7WyMLAv8HIzNDW4ezr7Ond1tHMyMbEwsPFyc/X6Pz38O/1alxbTEE+OjU0NjU3PEBKXuzU
+xr+/vr/IzdPd7nd5ffHl2dDMxsPAvr7Bx8zT3ef3fG1oX1ZMSEE9OzYzNDY3PENJWd/Nx7++
+v7/EzM/X5fJ7bXrv5djPx8HAv7/Dx8rT4O36fHF3929YUEk/PDk1NDQ0OT5FTmnhz8S/v8HF
+zNDX4/1tb/7p3NPLxL+/wcDAw8jP2u7x7O34+v5eUEpCPTo3MjQ2NztASVXt1cvBv8DAxM3Y
+2+V6d3j969zOycfFw8PDxMjL0dvj6ubp6uPvZlZLQT06NzU2Njc6PkNQdeLNwr/Bw8XM1dze
+4urk3ePf1c7Kx8XCwsTHzdfj7e/29fvwelxTTUdBPjo3ODg4O0BET/jZzMO/vr/Cyc7U2Njb
+827+5t7d29rOw8PI19LL1OH+z87c+W7iXkI/Pjg1Nzg4NTpGREJYzcS+vL27wc3M0dXr8G9k
+cHR6b+PJwL/FxsTFyszP3OP4+nZPRkU+PDo4NjY5PEFETWFz18vHxL69w8vV1NTncGttanP2
+6NzRysPAwMPIzc/X4Ot0Wk5MRT06Ojo6Ozs+Q0lQX+7e2tPJxMPEzd3e293q/Wv25dvSzcnI
+xMPDxMjMzs/X1uJ4alZIPzw7OTc4PEBESlRu5NXPzMnIy8zP1NrsaW1taHjp5N7UyMXFxsjG
+yM/S091+al1PSUQ/PTs6OzxAR09cZe3Y0M7Ix8vO09fZ4uPi5eDa2+Hd1dDLy8rJyMfKztLT
+2t/i/VpKQD07Ojg3ODo+Rk5j5tjXzsfHy83P1+DpfXnz5N7b29bOysnKys3NzM7X2Nvi621c
+UUc/PTs6ODg7PUFIUHTaz8rFw8jMzM3U2d3r8Hj2+33r2dTTz8rMy8zLztHP0NHc9G1dT0U+
+PDo4OTs9P0hPYPPZysfHxsvO0tjk7X5jbnNvf+/e2drWz87NysvQ1dfZ19nb3fdgU0lCPjw6
+Ojs9P0hSY+rPx8O/vsHGy87W4/VtYmNmZ2z+7N/d2dDOzcvLzc7P09LX5HpmWEtFPzw6ODk6
+PUFKV2vr2c3Hw8HDyMvO1+f6d21ucW3y4t7a1s/LycfJz9bY3N7l+GlcU0pCPjw6OTk5Oz9H
+T1r41crDwL+/wcXJ093l6vV/dmly7+vo3NPRz8rMz8/V3uTi5Ol+a1xPRj89Ozk5PD0/RExa
+bd3OyMTCw8jKztjf5u757+3q5t3a19PRzsrJys3O0dXR1Nvl629aTEE9Ozg2Njg7P0hVXfjS
+ysW/vr/BxMrV3t/t9vb67uvh2tTPzMjGx8rMztHY3+d7bVxORj86ODg2Njg8P0ZUcePRyMXC
+v7/CxsvT2d3o7e7s7Oje2dLPy8rJyszMzdDZ3el7X1NJPzs4NjQ0Nzo9Q01bdtvNxcG/v8TF
+xczX3tzZ3ufh19LT0s3LysvNycnLzM7S3GtNRT85My8uLi8wNj1IXtPDwLu4ubq6vcPK0O5t
+c11bb2/22c/NyMLCv76/wsbK0ORlTkY9NTIvKywuLTM6QVfhyL+7t7S3urm/ysvga2piXV9t
+c+TSycHAvru9vr/J0N/xYj86Oi4tLSkrLi43Pkzky721s7Gvtrm7xs3aaWROTlJPWunUyL+8
+u7i5urzAydbrV0E9NiwsKiYqLC42PlDexLq1srCxs7a9ws30clxOVVlj9NrOxL66tre3trvD
+xtxOPjozKCkqIyYrKzA+VufFuLKvrq2utbe5yeTccktSVE9b6dHJwrm2trOzvMO+5EJCOiko
+KSMiJiktM0Hmyryyrq6tra+6ur7a9u5ORlBfYvnNw8C5sbOzsbi+vuw/PDcoIiUjHyQrLDJJ
+3sm6r6ysrKustbi5z2puTkBGUE9Z08bEurOwrq+7uLhJN0cvHiEqHhwpLik218zIr6uqqqqp
+q7i6utdUXE4+P1NYW9TEv7ivrq+0tbdyNUk4HR8tHxolMikt78nOsaurp6isq667wsN7QEVM
+QkVi+9vEurawrrC3tLZcNEM0HR4rIRsjMi0vbsrJsK2tpqeur6uyzs3MSkBSTEZU7NXDuLay
+q7G+tL8yL0IoHCIoHh0qMy9Fz8u1qK2tpqaxta24/On3QkldSUvXydC9sLCvr7q4uzsrOC4e
+HSUkHiMvM0DUzLqop62rpai0s7bP82ZFRVhWSvy/wcOzrq6yurbINC02KB4eICAiKC01bsDH
+taalq6qnq7W2vWhcZz86SmJTccK8uq6qrLS6tcswKzIoHh4fHiIrLjBau7q1qaWnqKittLfA
+XUhKPzxBTVvdwLm0rKiprbi8xToqLCceHR4eHykxOFe6s6+opaeoqay3v8RfPj89OTtHWd/B
+ubWtp6WosbfAPyosJx0cHh0dJS85Vr2yraajpaeoq7O9y1I9OzU0Oj9P2L+6tqympKattr1F
+KCknHRocHR0jLjpaua2qo5+ipKasucXeQTU0Ly01PkvWu7SvqaWioaazvmsqIiYeGBocGx4r
+O1+4qaijnZ6io6i1ynQ/Ly4tKCs1P2e9sK2ppKKgoKq3wjohISIZFxsbGyMxRMaopKOcm6Cg
+oa3C5z4tKikkJS43RsGvrKegoqCeoq/DTykeHhwWGBwaHi5C1qqgn5yanKChqb/yQCwnJyMj
+KjJAzbKrpqGhoqGhprbtPigcGxsXGR4eIDRfx6ien52bnqWlrM5MPiwjJigmLT9S1bStraqn
+q6+sqq+6yVI5KyQkIh4fIh8mN0XlsKmrpaCmqKauvspVNTAvKiswNkLXvLOrqKimpqaosMhT
+MyYhIR4dHx4fKDRFvqupp6Gjp6Sntr/UPTIzLSsyNjhXyr2uqauppqiqqrbVYzckIiQeHiEf
+HikxPsKvrqqmp6emqa+5zEs9OzU1OTY5S+nDtK+tq6urq62yveo/LigkISAgHx8kKDJaxbWr
+qKinpqeprrbIaUc8NjUzMzpFY8a2r62sra2srrO87zktKCIgIB8fICMpO/q9rqmnpqWlpKmt
+ttVNPDQwMTE0OUbiv7Ssq6yrqqutsL4/LCokHBwfHR0hJDPXsaqkn6Cio6SqsLxONS0oKC02
+R+zaxq+opqWpqqutrLR7Mh4VExYaHiInNFOynpmWl56nuV89LCIdHB8rU7mooJ+fnp+ip7d0
+PjhI1dlPNh8XGBceLi5I1b2mnJmZn707KB4gJCQrNFWsn5uZn6288HDbW0hEPl60pJ2fsVAd
+EQ8NExsjRbahlo+PlKBaIhgUFhogOb6flpSWnrZQLy00PEjoxLGknpucsUMeDQoLDBs0vpyW
+k4+SnK4wGBAPER47rpuUkpWeuzsmICQvTL2so56dm52myioZDgkKDhpypZeRk5mer00mGRMU
+HDKvnZWVmaTINiIeHypFs6Sdmpyeoqu9QyIZFhUTFh4pvqObmJ2qyEksKyUjKjjQqZ+eo7hm
+LyopLT/Sr6WfnqGosMDzTj4uJyoyLiEcGR89sp2cobttPTk7LCoqPLqgm56vPisoLkbYva+r
+q6qvu8bQ19bb3mHdz8tYGg8NEiusl5qbufxsPD4nIiREqJuXn/stJCtB08fNu76zr7Svr7G4
+zk9ITs+urVoYCgkOLqKTlaG+P2NaPy0iK8+flpmpOR8dLVzAuczMxLi1rqmsq7dGNTRzqqao
+0hoJBwwcqJaTmqzdP1gxKCYuxZyUmKM+HhwdLGDGs6+srK+wtq+yw0ItK0SmmJerKxMFBQ0e
+ppOQl5+/PC0gJSpqopqXndQmGhgfP7epn6etuVxg/8e+00A4SbeakpauIA4FAgseqY+Mjpiz
+LB4YHCzOn5aXofMiGRkkS6uhpKvURDxDzK+qrb5OQeavnp22MxwODAwVOKSPjpOmMRoXHzG2
+n5ybo78vHxodM8yhnKe1Pi443a+kpLVvMSw7yaahq8gnGhEMEiW1k46TpUQdHSg2v66noqS1
+OSAbH0Sqn5+zPSwuTbKlpazLNywqNs+tn56rajIZDgsPK6WMjJKuJRYYIzu1pp2co80kGBsn
+waGiqdU3N1HJsqyvuV8uKStFr56Zm684Jh0SEBEftpKMkqkgFBYmy6ekqaqvwTYhICZYpqGn
+wjMsPcyxqbDCYjYwOF24pZ+gq0wqIhsVFB1BoZOVoUgfHSdJv7Gxrautzy4jHy28q6m2SjpP
+yLetuth0Qj9DS82soJ+jwC8lHBQPFyuuko+ZtiobHy8+1762pp+oeCgcHTu5q6m9VFzFvLO8
+VktDPlXfybGmo6e5OS0lGRIRG06bjo+cVB4ZHSU01q2dlpmtKxYTHEypnZ+twGhNPDo5Tb2y
+rrjYeMatpajINyQaEw8ZNaeTj5m7KxkZHy34qp2YmKc0GxMYMrOdnKK0bEAwOD/msqmotGYy
+NsWqo6tdKhoTEBkqtpmVmKlIJB4hKEO/p5ybo90nGhooWauhpKu//kVDRV++sau02T49vKaj
+rkEfFhIUHjbSo5udnrI+KiMgK1e8pp6jrustJSMpP72qpqWvvudBQlHVuLK4xse9tbLOMx8W
+EBYiO7Gen56jutg+KiUoLmOspJ+kvFgvJSctO86tpqCktuc3Lz/curK1t7i5vOIyIRYRFx40
+raGem6aut1g3KyEhLUqyn6CjrksyKSYtP8+toqKksHI/Mzx1y7WvsLS/WTMjGRUVGCjHqZqZ
+nqKyyFU1KiEfJTfEp56eordKLyYsOWW2rauts7/da09W2b60sbhdLx8XFRgeLdGuo56fo6eu
+vvw7KSMhIy5Mvamioqi04kI9P1nLwLy8y+JvWHrGtq6rr9I3HxYTExgkSrWjnJyen6arstU6
+KR8cHSEub7Kkn6Cmr8Ln5+TUy9xaRz08S+e8r6urstgwHxkWFxwnQMGspJ+fnp+iqblZMCQe
+HR4jLUrAraalp6qtrayutslPNy0qKzNHzbavssNTMiUhHyEpMUfOvbKqqKakqK642EY6MCws
+LTA6SmvWw7qwqaWlqK687T8yLS41RXvSyNFZQDMsLCssNDtM0L21r62vsre9v8rcZ0Y8Nzc5
+PD0+SHLEsKmnp6u0x21GPj09RVN83d9pSjsvKyopLDRBace6s6+ztbe6vLu+xthdSD84MzEx
+Nkbgu66qqKqvuchzSUJDSlVcYm51YUs7LyooKCwzQPXCura2t7i2tLSztbnC7UM1LSkqLTla
+xLKrqamssr3L2m5gU0xKSU5YZVlFOS4rKSovOkhy2MvFwb26tbCtqqywvWY4LCgnLDNH172y
+raysrrS6v8zzUEQ/P0JOXGJTPzQtKysuOD5PcuXPyb+5sKypqKuwvlc5LSkqLTQ/XtC9tK6r
+qquut8lzTEI/P0ZMUEtANi4sLC4yOj9JWGrgyryzramoqay2y086Ly0tLjI8UNS8sKyqqqyx
+vdFhTUtGRUNCQ0NEQTszLi0uMDpCS1z20MC0rKimqK69eEM5My8vLzU/bL+vqaiprbK4vctr
+RjcwNDgzNT9KccjMZkk8MjI7Ojk8Oz/iuK+qqrC1uMHXZUE4Oj5T1snEvru7trW7xtpoWVlN
+Qj46PEpt7+PXysT+NSooKCswMTRB9burqKyur6+vttBJPD1Pb1xZX9i9trrJ1M7IxdNOPj5D
+SkpHRVB89N/V1NjVXi8oKywuNDY78cHCt6+vrKuvt7zJ4nBPSEtNTmnm38y/u7m7ws/lX05J
+Pjg5PD5DTV/v6WZeTTw4Ni4wPD5J4NDAtLKwra2vs7vH3lRGQ0ZMVG3Rwb25u8XO0+tZRTg4
+Oz5BPz9YcUxLR0FDRENGR1Dv2trPxL26vLy4uL/M197tXU5SedjQ19fQ0c7M3HdhVUlEQT5G
+Rz9NaX3+ZGVwWUA/S0xJSE/oy87Ovrm6vb++v8nb3utqdG5v7fVhXl1aXV1aaV1OTU5QU09U
+Z+Xk3+Rsb2dncvdeVlxdYfLj39jU0tPb19bb3+dtZ2ZibWzn7O3f29zf3nVeWVZUWFtm6+jx
+9u7s8XFpYWJaU1dTXnLw49HM1Nnb397e7nxkWFVcZ291YWRrdHrv59/g6fx49f5yfvvx6+js
+6OTrfHtqaWlfX2Rqeujc3NfW5+jb3uh2Y1hVW1hUX19deOv7/Phxal1veHL28urn3tzV1djd
+7H9vb2d1b2pleHBg++LZ427o2f5z4vVfanhxYWNeWmNeZvb3aXzsd/p0burx9vb429nZ2uF+
+Z3ZoXGdiWl5eXHXf4t3W2NfeeO3hd3t0aGj/bmxybnpu+/v6b/v0c/7/bmp3bXP3c2Bi+Op+
+bG1sZ2xvffjt79zY293Z3ePc4ORvZ15eZ2Rqbmly+fLo73v99HZ2bW5nYGp38ezr6Phydmpl
+bH1xamz5+HXz6u3m6+vu6Pjtf3d3aGVscfzf7fx47vn6fGtwbfvq3OLi3N3f6nZtal5bVVJV
+VlpdYG7c2+jl6N/a3uTf3+xgXFVSWnDp4N343tj3+e5+b21fZWxma/L07e/s7v/77Od67fJ6
+dHD8bnJ+/Xr8bF9uT2DX1lxf1uBpbnx4bWZiaW9gau7yfHbn91ZK2sha4sLO29rk8eVqa2BV
+TFjw5HZI7r3XVvvS1P1OUP9eT1FJUdjyaO/b0tXvZ+h7b2ZeeOt9bd3h6Oj19Pjr9OrucP/o
++Prl3Njd7Ojd3t3e6ehuW1NDOj0/PURLUuLLysG9v8DD12RYVk1ISFFq6d/bzcO/v7++vr7A
+zM/X7Vk9NC4qKSstLzxZyrixrqyssLrJbkg7MzExN0Vg2MK5tLO3vL3BzeDxfurf4NLNzMTQ
+QzArJyYmJio6bMO0ramkpau0wnpDNS0rLDE7SurBta6srrG0u8xqTExWWV1v2cXFx8JfMiom
+IyQlJzNlw7GqqKWjqK+9dUM3LCgqLTZHecKxrKurrrK6z1xMQ0BDRlfh19DKxMXIz0ksJygn
+KSsuRca4s66sqaqxvd9OQDUtLC87W+bGs62rrbS7vcp8VUQ/QkBDTmncy8vLytp8d/PpSS4q
+LS8zNDQ/6si9ubmzsLS6zWRgWEpMUm/OysvEwMDBydXgbVJOR0lJSVFf9d/S0M7PdVpbXlhL
+R09jX0xFSEpMTEtVet/e49jOy8rR1M/T09je3NPY397c3NvV08/e1tH0XlBYXlZOTllfYlZP
+WWRfWWhx3N1scPju6VtSVFBPVlZUd3dq/XHez83Pz8zKzc7S1+zh0uH3enPt4fx8bV9rZFNZ
+6H12amBxWlxXWn9qa3fr+mlYVlp+5n505N57X2xnYXlmat3h6u/b2tjQ4NTice/sa1di/W5r
+5+bc0+nd/fL+cHFdXWNfbm36b/92dOddbub3ZntwYGpcW11qX2Zu6Pb+2tn/ee/t3PtdX/P7
+6fj25N7x+dLc4+3c9+9wZNt67+3lb/tpUl9hZl5bZvpnZndcbuhrXPpmcXhZcGjmZuj459zm
+59bP4fH+2+pq7/xoZdvm7/v639n3dGbkXVVPdVtRZlhk5tXh2+3e7WJoVFheWlRf9eTa49PP
+1958127fYHhmfWBieXvtWeF+3f/n42xzXPNk43zraOricfLr5/HkaXRqa2BbWPp9ZeXf733o
++nXwa35s/mVman9+7ev3+NLh3fnf6m9oYWB0eGrhffr26P328ebhaHNvX19pdvdd49xmY3jv
+3eX05etlXlNw/Gpa5N7x3n7X7nxeZlzlaV3qbvDv2uvn4vV+fuXte2r0bnxc89htYPH67fjs
+8mp462hlbHlrcWdp7u9+/evu/G3v+ebxefX06+3z//To6v59ZnBnbPF0Z3jn+ntlb+zt8Xvz
+7Ot9fGJteHdjdfno3ur7bHF47evcZ+PqZGT+8Gzs7XBtdV97cHNn29z27Ptva/5z73bs8HP9
+eH9o/3z6emZ7+Hl3amZt/evp5fLtfnr9ffH6+PJ8+XZn8u3zdNnp6/RsbP59Z2Vk7W1nXH7x
+ZWf34eTrb97+8Hvte/lpb297c2bv4+1t5ex3d1x94un7bHjm8nTo3tx9eHZ+emJpaWV5ZnXo
+82Nl8/tqWnRzZW9p7uHf7f7l2Obi6vNx8e94/ez9bOp+fvVn62v2am5taGV1e238YHRvc2Vg
+anhv8t7e5Obs6N3t6f7d8u954XRnc3d+c31gc2d1evjudXH3cG9t9fd85+3/Y2Tz33T4ePP2
++GZs6Hvtetvx8GvmeXf5cG5742bq3+h2cnp8al92/PNs9m5ndfT0a3j0/mjofe/+a2t27PTv
+7NhzfN7xaWP/bvf1ZWrl9f5r8u7uaH7qZ2hwfPd47Ovy7vh6Xuf9Xm5l8e/3ffj+/25y5Pf8
+ee3/dXvr5n5se27j/vpv+3nv3/V0d/9nYGXwae3+6Obi6fbteWxa8ltbVOxubnbv2NnTYu96
+1Ft98ORtaXRd1+rkatbj1/7/7mddT11Y6m1tYmFdWFdZZlpdXWVr9uLRy83KxcrQzMbIzMzK
+ycrJxdVWNykpLS0sLjRWvbKvsba1tcDyRzk1Oz5MWu7HvLq5ury8vL2+v77AwL7kOisiHyIn
+KC9By6+oqaquuchnPC4uMDtK6MO8ubq8wsrY3dnVxru2tK6tsNM2Jx8dHSQpOnS4qqaqsb9a
+SDk1MTxI586/tbO4ws12XktUdc68s66trK+0v0cqHhoaHys57b+vqKixyUo7Pj5AQk3bubi5
+t7vB3ks9PkBZy7uzrq6ytrq+xGIyKB8dICo+/MG7sbC5x1lDP0lNXV3kyri5xdLy+Uo8Nz1W
+ybixr7K2urzCv8LFXDUtIx8kL1HZxce7vMbeXUhQ+v3gZfbYwcLaWU1KPzs+Zcazr66us7rD
+yc7NxsLPRzMmHR4pQW/f27+3tb/cUUxnWE9DSt68uMJxT1ZTSUNdxLSvsra7wM//XvTJvba6
+5T8lGhwlPWZ017mvr7v2RD5NVU9IWsqzr7vfTEc+OjtUw7Ows7i5vdpi/cm4tbzG1E8wHRYb
+LOzL1dC4rq/CTz5G6NLcc+rEtrrTSDs7Oj5LzLavr7a5v9NcaNTCv8TGvrzxMh4XGyhK0drJ
+sqqsvFlFSmTpVT5F+7+5yGhaXFJJRWq9srG2u77G2OTaz87Y39vDwD8nHBsqPlBxzrOnq8BL
+OUJmY0k6PdS4sbjKYU5JPD5OxbCusbi+xN1ZTlBffuvPvK+3PikdHSs1PEXcr6euy1VR0s5T
+PTdCyLu/wszR5Uc7QG69tba2vcHPb2P45dnazsPIwds2KR8gLDRBZcavrbfM/WvuVT8+RGu/
+vcG+x9dhQj5K5b+7ubrByNXoe2Nf5s7GvsO8zD0uIx8nLT1lxrKtsr7Q6mxJPj4/T9zAu7m+
+3FtGP0dky7+8vb/DzdHX+mx31svHwL+++zovJCAoMEhp1by0tL7ZbV5XWEtIT9/HxMXWem51
+aW/50MXEyc/LxsjU5OfXzMzMxMTMXjkvKScvOEBFX8u7vMbMzczYYUlETV3i71tcaNfQ0dHJ
+wMTO1dvMyMnO2drY2dzQztLbc0xGQD09Ojg1NTxGUF3rzsrN3OHWysXL2Ovp6/f5bf7g09jo
+1svDw8/me/32eHjezs3N0NDT42JLQUI9ODg9QkdFQ0FHWerPzcrFwL/I23xfZ1xMU3TVxsLB
+wsLGy83Q0tXW19nX19naa0lCPTw7OjxBRkM9Oz9NXnns1cS9vL7DzNnf71NKVu3X1NTMx8LC
+y87Pz9Lj6djPz+D6Z1ZMSEJBSEg/PD08PkFAR09u08vGwcLDx9T2cHh2denX0tHQysfK0dXV
+0s7S08/N0OpmVk9XUEpNTk9ORj07PTs6PENW7NnQysS/xM3W5+ra3Xtu4dTU18/MycjN0s/L
+y87U1t1qWVNSX/d0XV1dUUU8Nzg6Oz0/R2Lj18/LxsXKz9rf1tLZ3d7T1d7k2s3Mzc/LxMTJ
+1uXyZVdWYWz86N9sT0xNRTo2OD4+PkNObd/m7+/XzcvR1czIy9vi39/f4ODVyMbIxcnHvtNi
+6u9pW2/r3utrc3pPPTo/ODQ2Nz9IU1tw283Ky87JytxjadXP62vWxcXQzsG8wczJxdD7XnLk
+6Gpv2c9+RlPP5Vo0JCc3LyoucLPAT8uusXFB07nDT1q7schey7fATUvGvOxM373HU1DOzFNE
+XORPO1XZS0VQ49tUVsVAJTdHOTM1ybZVSL+1vlXZsbtfX8W5zF++sr5ibMPGXU/Nx3dPa93t
+Y2htS0xOR05MTlRQSV1PNTo4Oz48QWRv+dfCtry+v8PIzt3Kv8fMzL++yM3IyO5h+fFlYnXy
+b1xaR0VNPj9GSEZPTkk+PEI/PUPz083Zy8PLzs7Cw9LHvsnLysvI52bf4O/67dja8trdXV9o
+U2tuT2JMRXtQO0pWPjxJbnx5Zs/VT1Hr1t/bw73V6dbI1lzlytV30svXbXTR3Ors7/JdWvZe
+UHVeaVlIWP9MSkZGWUhN8NTU19vsZmnY2evWysjeedzS3eHSzd5s3NNuU1z+7ldMZ25SW+rq
+WmDfZFlc5ttPVfxYUUz902j6zt5dVenXdO7IzN/Z0ehl9NXTemnV03x2z9NiYuxZTE5SV1Bg
+7Vht1d5sXfZ6SERUe29Q99Xebn7a5WHh1Nzt6cnV7tHM2X/Uyull3d9gWezmV1vl3GFGX3JM
+SmjvYVZgcVBKXdtaSu3RbWfl319p2d/j19DT1+Xj4+3j0MzS0dbxbmVndmNnePn1Zm1d9PpW
+bUpJTExKZNn2Wml+TFFv3OLu4trdaF/mz9rj1dHg3d3n3Ozaz9hrY+ztX+nN4O3r+GlOSFtv
+TU7e1VFb0XRP8FRLTk5SS1zedeft49FhWNvR4dzN0t7Sys/Z0dbf2uj03err5lpf3GhHUltP
+XujtXFJOSkxLY9f2cVlLT2xTXNzQ2+Xd2N/x0c7U7N/P1/ney83PzM/b+F9xbFDrUkviS07N
+6Wd5TlE/OmJYU3Pw4E1CWmtVZdbEz/naz+X008vO49jL1ebTycfL0tHsaGT+dOxlTH1dPU7W
+/1c/TUkxOVVXXnzh03tYZV5gzsPOz9LZ4fncx8HN1dDR3NrAxM/R7OhORVFez99a7u1FODA4
+PDZK92/7dmprXWHk3NPPysrLysbFy87P3fHRyMvIxMDOWVzoTDVavWdG2M8/Ly03OCw60uxR
+07y/Z2TBzGzhx7zJ1ry3w8vBvupT7M3Vbc2580NW9s9HQD8tKSovMTrjyt/9zcdtWdbHv8bA
+vMLRy7q4v8m/x+Jz29n22NLO0GphxtgaGl8tIS64p80vyLBJL0ewuUvNq7pu67Ou52rBvl5J
+y7bHY8S8y/3cxz8VF2ouHjmon8UxvbQyK0G0t0/HqLpX0LCt40zHzklPy7W20MW/2XnTyikQ
+G1UpIPment8+srYqHUSpzzqwn7M+dqmsQjfDuz43v6m02bavz2XISxUOJUsjKqqUnzs/rcwe
+GzuouEStna1HXK2uPS5svX1Fvaquxb+3vbw5Ew8hLyAvpJOdaU+zbR4aMLm//q6fq25ptLBJ
+NFHmZnS7qKq2uLa4Uh8UEh8uJV+dl5/BXMU7HyA0ybvIraKxd2G9uE01WdNZermpqba5r8cq
+FxIaKyw0q5eaq8zfVCYdKUvDurarqL5Z3MDLRjvowt7PtKeqt729NBUPGystMrOYmazb0E4o
+HivSubmvqqzCTVvIz01DYsPCz72vrbG5zSwZFxwkLkC1nJ6tvc9IKCAx2My7rKmxcU3MzXt9
+2L/J7L+xtby9srspGRofIyYzuaClsby77SsiMP3QzK+kq8lsy8NeP2rAvszDsa6zt7/PMhsY
+HCcsMt6joK7GyNU0JSpNxM29qam45+TF7D0/07u5uLGrrra/9yodGh0kKjTOpqSsub70NCUr
+QerSvqqps8PDxOs9OmvGv7uvqau1u9cqHRkbIyks26Sip7K6wjsiJjZY5M+soqy+wsbaPi9E
+zMW9r6qorba7MR0aGh0fKEinoKKlqLNOJSEsNkFVsqGksre2yT8rMl/V27yqpamvr/YmHRkY
+GiEvxaOenp+nvjYlIigxOHytn6Spq7njMigxR1Dltqijp6uuUyceFhUaHyy+ppyYnKW2PSYi
+ICQySrmfn6Okrc43Jyk3Pku9qaOlpai/Mh8WEhYZH0CunJWWm6K0OyMcHCMrN7efnp6hrMI/
+KCkvNEbKr6akpKSySyIXFRYWGyvBn5mYmJifujcoIiEfHi7EtrWppqeryeTIUjg5P/DI0b21
+xWIyJiotKy03TMu8vLSur7CzvcPOWEI8PUVEPlHRyMziV1NNQkNW3cnCxsnIy8rExsnbWElG
+QkJJUfjb5GFORT4/SU9i5M/Aw9HfalZdWmjX0NXS19XPyr67vb7J1/FQQ0lVUUhAPzs4ODk+
+UOvczsbCvr/S3M/Z69t+UldQSU1bTl27uc24r7u6sLi/wuE6KCImKSIkMV7Au7+0p6eutri5
+zT0uLC4xLi5Mt7a/r6aptsnWx947OVNdT/u7sLXMPyolJB4cITjDrqegnJ2mvlxDMichKDz8
+zb+xp6i65N/qPS0vRX7gx7asrLa8uMBYOisfHB8mKCtYq6Cfn5+hq9E2KiknIyY8xba0tLCv
+vldFUUw5NlPKx8e+ta+1wsW9wm1GQjQkHiApNDlMtKOfpKqstHwvJiUqLzU/0a+qsLy7uMlL
+O0Jk9FveurjNWF/qaElK8cW/ycm8x0EwLCUjKi8zUrWpqqunqrncRzcvLzAxPtK2sa+trbTZ
+RT42LzE4RfXLvrm4trzN7Hnv+O3W0dztTjMoKDEvKTHPrqyuqKWsw1I5LisqKzNeuK6vrKmu
+xE47My8uLj3LuLm1rau10OrpUEBEZcrNYF5YMSIjLi4oNLunqamioa3YPzEpJCYrNGqzqaip
+qK3CTjw4MzAxPl7GubzCu7jJUlHd5F3cubPCWU9GKh4jLy8sQ62ip6upq8FFNC4pKC89Tsis
+pamvs7jcOzEzNjxP7NC+tr7T1NJYP0R51vLnxbexut9KPCwhICouLz+5paWrqqmzWzUwLSkq
+OGfAr6ekqbC71D8uKywvOVTPvri1ucb0UElBP0hf387Du7Svsr9eNychJSsrLVCtoqappqe7
+OiopJyQoONq2qqOiqLDBTC4mJikuPtm2rKmqr7rWSzo1NDdCaMq4r66ytrzxNCQdHygsLTu+
+paGorKy0dDIsLS0wQNi7r6ilqrfJbzssKSwyPWnDt7Kvsb7kU0Y7NThFXd7DuLO0t7nJTDAl
+HyYvNDRSrqGlrq+wyDssKywsNl7Btq2np6/F7Ew1Kyw0P1XMuLOys7jJX0g/OTc9UfTbzL63
+tba71TomIiwyKSU5tKitr6motWM+OC8tNEZc1LWqqrK5u88/Ly4zOD5X1cK4s7W/2GJNPzk5
+Qlb/zr63tLe6ur8+Hx0uQCkfNK2irLGop7dfQDMoKDZLQ0+2pKezurfFSDc0NDdEft/Tuq6z
+zl5zXz42PFB03ce+vr29yHQ8KyctNzMvQL+urrCur772UUE3ND5Vbti9srS8v8pgQDs8PkRW
+3M/Kv7q/3lZSTUVES1vu1cvGw8DD02ZQSDgvNUFBPEfcwL2+vcTZ5OhbQkFX+H7gx76/yM/d
+dGBYUVNn29fm28nF0VxMUldORUhm39vf3NDM1HdPRkdHPTlDan1o28O/xs7S3vxuXE9SYuPW
+zsrKztPdbVdXXF9l7N7l3M3M319QTU5NSUpZ59bb283L129YUU5PTk5UaeTd4uDY1t78aWhu
+bmRfbeTR0NTW5vH9b2VjY2f549je69vPz+VoXFxXTk1XcuTe6N3X2uxeTUlKTU9SWe7X19/h
+1tPZ4ej6eHNsa3zd0tbc3+bwdGdhWlhkd/74+/v05OpvZmRiXFpf8d7d29vd3udpVU9SWVpa
+X3jh2d/s49ze7H1uav3t9m51597f5uv4cm5lWVRbanBvduzc19rj7PD2dWFdYnDw8vno29vf
+82tmenleVlRfbm5nY3bp6f5z9ujp6vTu6Orx7evt5+b4b3n98+7s6/Pw8vP2b2l0c2hkYV9k
+d313ffrxel9caHfy8e3k4eDo8H//fG1hYmlucn3u3tvf5+7zfvv9eHR7+/Z5bn776+Xl5ubu
+9O78a2VkaG5kZnBrZWJgY219+/zt5+Ho7+ro937/9e/n7W5panjw+PXv+HBqZ2Nq/u7u7unk
+5Ov/c2168/99fvTo5urn5vZwa2ZdXl9laW305N/k6unuf3Zva2t0fX5vcPfy/vf7/fR+bm93
+f+/q6/Hs5+bs9fp8dGtscW1te/r88+7z+W1ueHFub3BxdXd3enhvcXX19O7v6ur27/Lu7fV7
+f/Ty7/R3bm5vcG19/Hv76+317vd6bW17d29rb/x+eGxpZWxtc+3r6u3r4d/f4+54/nRubmdo
+dPp4fPPx8+vyef9va2tpdX999fV8bHr3fnpte394ffbq5uHl6/T/cW1sYWFtbm9veuzt7+3y
+6vJ1cHlucf14eP71e3bz6+zr9vn0c3r/c/78amp4c2pyfXN6eHz9+Xz87/T28O9+d29/9Pfu
+6e7s73l1eW1sc2txdHVyb397fv3z+3zw9HtzenR7/H18ffX4dXd4eX57e339+O76fvP5dvv4
+/u/s6Onp7PDx/W1rcGxmX2Blb31wdX//d/Hu/n756/T4df7u6OLf4+jm6fP8921nZmFnamF4
+7ft59Ppwa2Jlbnpu+Onv9e3t9Pj25un8e/57/Pz/+n7y8PN3b3Pz83j38/T+cnJ8b2v8+m50
+8vVyanv9dHrz7fH69u90bXj5+v777+7v7PtxfvX07/t3+/J4b3x+eWx7+3j9evl//PL8+vrz
+9HVxfG9vc3V0+vvy+3D8ffz8/W1t7u9zdnZ37+3k4ez5+PF8bG39eHLzdXz5fXlq+3Z5du3q
+am1+cW789H/7a2/9eW15/PLn7O34evTl8Ov7/e57cWt0dGt3dHd/d21qd2xsfXD58+rt8Pxt
+dnvn5vvz9fP8/u3x9/PwemptbGpr+3t3+fFydvlqbG1v8/Pt8n7u8/J0bHl3bP37bm99efDu
+5eDu9Xh1cXJsZW1udPF+9vrw/f7q/PbRylBOeHdNX9j0f/nm4drpfu3PzE5hcV9KVfpo6F7t
+wtBHT937SV7tfmb7xNVcVexeU3LV2fTUyc9qXl5fW9vKy19N+WhYUuxaS09h7+todN3P3Pbq
+729ZYfXc4N7c5Hb96+zsbHnp6Ob5ZV9fXmxxfGjo+/5tX2FWY3Vzd+7U0dfy+O/s6+FvaOne
+ZlZ4d25TZu3rfl/+8ejd2e9ta21kXX5v/vbZ8O7nbP/v6WRs7+R6Z3rneWX43O5c6/N+Zmxb
+aFxa5vfvb9re6Pvy3uxyZ/rsf2luf+Fzd/ro5nb2XWV0bnfy7nv+5uB0/uvc+F9lZ+ZjZ3t4
+b15reXn1f+1waff08e3j8u7h+mZ862lr/unvcevc7WN7/+35a+P353ju5HtqWnFxZ2hqaV9t
+Y/5rdnjk3+zxduD8bmz32efq99zsc21q6Wxi/uV9a2hyfPppYv71eG979vtu9Njc8fp+aVNq
+9fR8b+Zx6/f9d2vkbnB8+vN47P7+anPrffZs6OlnafXzeHnz3fZxcO1zX2vt2HNp+2ZramH+
+9Gpl5udr/fDc7XTq7958/vL5fWR8Zmxxaurr+XPp5mJfePN7cn/t/mVmZuPqaX/j2Gpv3eVv
+Wmbi+mxk6dj0bnne3/1fbu5iZuXsdF9pefZuX37ldW147fP96Pdt7ux2Znbr4PPv3ulwZWrt
+Zln13+JvbOPga1d7721fXfXv7mvy3nb83uZeY+3ocX7/3dvr/mne13Nm79n3TmXfelhMXuZg
+U31xadtkXt/a5ezq/ODZen/vePrg6HLb22ZfdfNiX2/m32Fa9eH7XmH55d9kWOnaeldm3eVr
+Xnz8X2t86un9aXnY5WNm69v1bXj74e9hZ3dy/PVoYunbd3P34NLhXWrX5WFederuYFbx23Nb
+a3l0X1hf5Hlc3uD88X7s4Gxf++bud37a7GTn393uZ+rt6upsbPX3ZmJ0bXxqbm1g/ebqYlj4
+62Rfbm1qeG5i4eLi2dfa29Z2duLebF5r+/piWGh+9OX8cV9h/G1lX27w+/Jv6uFv6e34emTe
+3X9r8e958W1ueGx43Od78fj26Wlf8OjZZlFu3uNcbd7tcV5eb3bs7G1ic/r1cFZr3O71Y2XZ
+0G9e9v7a1OpdfupmdPBx8fpfXvx5YO7l5Ov04OfudFlu2epmXWDu33lba+/p+2Zo+/tiZPf0
++nr62thtVn3cemVqcNvYW2fT1e9eW23e71ZWeejxZl356Xb66Pdz/drX9FVf3OljX37e7HBq
+7t7tbXDp4uhraujr8/N+efN4b2VtcHhuavTvXVZ+7/N8Xmze/W14Y+nd/H3mee7W5+NsYOv7
+7eFmau3s7vlyXWvr8l1j7fZsXWnu4mdY89jjaHP06ehqZ/fg8G3n6XD+8Op0b3b373hmXefU
+91xy5+Z2ZGJo9HFqdf7xafvncGVo93jya2fl4PRi++bk3+7p7Xt4dnxucHNd/XpeaO/aenpt
+fdnfdVxq6mxt4X1reePrduX8c+nibXBmZvF2XnLo/Ov0Xn7cc2ZwfvJteW37+WVo695oXmvZ
+1Wtg9ebm6F9m1tz1cmzvfG5oYv/y8WR932lZau/pbmXq4eL78uTuel525WZaX3fi7nxu6uPt
+/mdwZ3l0Z/T03dt5cXru3u59fv317O1bUWDi3W11+e/9b/lyaXH5737weWXy6Oj1Zv37Ymdx
+Z1/o2/lpZt/V52xw5OjpfmV87H1s7eVkXvntfXXyfFla3NpbV1/r2G9aXXDn6Hv+8nv75/h0
+8ujl8evgd/Hj+enp8XB29G5eZ/358W5navfe5l1d5e1eXfn5a2Fh7epnbPLp8P5qbvf6fufo
+7+zq6efb8Wz59/b6+vjv+3N8fHP8/e7vYl5ec/lpaG7u5uh3eHJmcXN3fWhdYvXh8G7r2Nnm
+6/by7PDwfF9ma3x+eu7xcWlubHt8YWbo6mFZYnn0/e/j6O7o3N3wb/Tq729od/Z+aWV3/fP2
++v54dWZpdnFka+/j5mti7dzkdHLu+HV0+25pcPPt6uXr+PDv9HhlXGf0fv3w9fvz7fhvanb7
+/nZrc/f68uj+eu13aW9+e3xxeebr/fT7bm55/Ofi/Pv07W9eX3bt929v6Oh6cf706/H/7evx
+9fh+dPz47Pt4eG1saWNpanH093FqaWz+4t/l7Oj3eXhpZ2l28e75d+7g73hxcvny9HhkX2r9
+5un9dn/l3uHvcWFkeHz073dlau/s9nl4d27/7X9gX27w8356cnf18HNnaWh77evv9vHt6+rf
+5/Tp/Hx+/v39b3N19nlmbX59aWl/+/1+dmVib/Tu/Pvx7e3x+W//8nxsc/Ln6vLz5eXr8fZ3
+bGhkbm9waGtwb3z59P1xbG18+HlrbnZ5+e/l4N7k5ebg3/N78u98a2ZkY2V4+XNvd//8+3dh
+Ymxve3Rzd3Rxbmh16+rt7uvk3uTt5+Pk5OXr9P766vJ5bmRma25jXl9jZGRwc3h9b3T88PT1
+7/jt4+js6ev/fX39ev/ydnl4fW5tfHJ2/PL08fb4+Xp27+3y9uvp7+3u+H5xcG1sZ2hwa2dv
+bnfw7urs7vl9cW52e/jv+fLr8O3t9vzu6O36c2lmYl9jaHjv7fl1dvbu5unx7/L2em9wc3/w
+6OXp9nFraGdmamlrZm179/H56+nv7OXi3t7k63V3+3psb3NpcPnt/mxsb2tgaGprcW5vdXV4
+ffjp5+rl4erk4et6fvt5bnZ7eP12fX5xb3ZzaWl3+Xx2cW56+Pfs9H307uzu6ub7eHRy+ezu
+9/L7ffb0e29pa/58d377fXf++21t+PT9fn/47/D+dGdnbG5vcG95cXFvc/3y7/fw6+3z7Ph0
+bGVpcXr4+nt0+eLj6+zt7uvu7u3w+X94e3Ruem9w9Pjt83h4cHd6eXn++/zz+/zv6+75dXZz
+aWpqZWNobnJ1cnV3dff1e/35+vn5/vvv7/Z6fvX39+zyentuaWluevf3fvf+eHj07v315eLg
+4ObveXR//nt9c3n+em9v+Pb3+vv29/H0fG1ueHJpcnBnbG9yfG9xent9d3x0cnf+9PHm5+v1
+/v19dXN1c3hybGxtevf+d//u6ev1fnzy+nl/9+/u8vj09fb2+3157eX1dHx7cGp0dWtzfGtq
+d/7v8f31+v55fPt4/3t3b29yfPN6aXDz7u/0fvr58ufm4NvU0dPW2udvYl5XUE1LSEZGSExU
+ZfPa0tDPzc3NzM/U2uH4fvDn2c/LzM/R2eJ4XVBFOjQwLzI6SHXSxL26uLa3ub7OY0c7OTo8
+Qk9t2s7Gv7u3tre9ydHc+3x1YVBGOzAtLi81PUhW58i8s6+wtr3RWUU+Ozs8PkROddC/ube0
+srOztLi+yvBPPjcwKygmJyovPFLOuq+qqKirs73bTDoxLi4wNj9Y1cO6tLGvrayutLvF2PxX
+PjIrJiIkJysvPFbKs6uop6issbfITTcuLC0yNzpEX825rqysq6yur7O7yPBWQC8oIyAgJCgr
+M0rPtqumpqanq7LGTDk0Ly4uLjNB7ca6tLKuqairrbG6w9VfQzQpIiIhHyEmLDvuvrOrpqSj
+pay6zOpCMy8uLzg/RFLOvLOsq6upqq+1u83uWT8uIx8iIB8gJi0/5sm4q6Wkpamtr7nkS0c+
+Nzc5PEZZ78u7sq6trbCwt8rPaU0/OCsjKCsnIScyQEdOzLKsr7GurK2wv2LMy01CelZHQ0bl
+/E5fwsPJxMDHytDvXV1gSURFQ0E/Q0tVU09f7er7693b4fn56uHc6v3/9+5iW2Tu0crMyMLC
+wMXKztHeblJLQUBBQUVIS1BVWWNve+nX19vTzczT1d9kUk9KSUpJS1VbaeHPx8bEvb7Fx8vY
+6nRcVE9MSU1MT1hZXe7e1tvv0Nrc2nXo8WJPTVNKSltXW/zwzdPQys7Mzsra9PtrW09IUE1D
+TFFTc/z+5NHNzMfKx8bM2+NtWFJPRkhZTVZZ+ex03Nbb3dnk3fV4/PZzU1RaTk9kZlP28+Ts
+2NrOy9DK1OHZ21lr6k1SXF1OXWN06mDS7ezt/9zzbVr7bHVTbmFt1kzZfH3ZX83Z39ht7d/c
+V3HgaGNV6WNR3WR52/ZT49N6WuBu4O5Mz15a2Uvr815g6NhZ/c1p8djp6t1w999Ra1fN3DzP
+z0nawFL8wkZg0OdVUNRmXlr/W0/sXl994v973XPb6WXR2uNmzfne623w8NtN0+1R2/v2Wcpy
+Qs3cRe9e6FN9XE7HQl5s+P5N3VTa1U3R297e59Ta2vvZxkzgzGpT1G1Vz1pK089DWclnTmzd
+UmteSuD0RO/KWE7fy2f69XnAYFTIzWNJ0tteX1rWzkxx29/JP+/D0zrTvj3y7lDMSkrp6V9M
+1vHaT33Ca0i61UTK9XrcUkW8Ri7HtDVPtm7ObGK/00fnYM3eOmnW91VPVr/FLN64V2w+07tf
+PsnbaMhEPbB+Nc5+3HRfTNPE4EbjucQ3cbzoU2hK3b9ANc7CQktW3Mtdb9bfy+xYV8paU9lC
+z+xKVMFL6chJacPEWljAvT/t0vFWXExd6kFV1E9O2Hzj31vV0mbtX9vvWmHe3FdN2NhdcPJJ
+vL5JSLy5QEXMyURi9UJO2XBAXM7e5m1qV8nLUm3Sz9NC3M5R6nJkV8dOSuFr+Gdw3Ndo5d/S
+XVndzWZF199ybltu1/dy4Vxa19rdX+7XVu/tTVRtdWbheE/x8e1jZefO3ktkz9BzW3fjcm5e
+9W9jbu7M5Gtv38zZ79Pg7d3e4uLpX9/QbG5PSU1KPjs5Njg9TV1d3MfEtbK5uLK3t7bGz9DP
+X13/W+/ZXjovNTMtMDEuMD1M+crHv7i5s7Gzt73CzdfO+VJcauXRwsrEur++vnw8LSclKCkn
+KC06T8q3tLCurK6srrjE4HVUSkc+SG/Vx764uLi3tLrqPS4nJiUlJSgrMEbNt7Ctq6yur7W8
+yulXRkFCSFf1x7u1r66usbS4x08tIx8fHx8jKCw3abuuqaipqq6zu8p+V0xGSEdezr+2sq+u
+rq+xtb3tOSgeHB0eHyMqLz/StaulpKaqr7zM7VdIQkJCSnnIurOvra2srK6xt9g8Kh4bGhse
+JCszR9i4q6Oho6izyl9HPz09PkVR4sS1raqpqq2vtLm9y04uIRsZGh0kLDlO276up6Ohpa7B
+Vj03ODo/TnHfzbyzraqqrK61ubzD1kctIRwaGx8nLztUz7mspqKjp67FTTkxMTQ5QU5pzryy
+raurq66vtLvD0kswJR4cHR8mLjtWzrqtqKSkp66/VjgvLS4zPUx5y7yyrauqq66zuL3F1Vo6
+KyIfHyInLjlK6MW3rqyqq6++/j80Ly81PlXhyby0r62trbG4vcLI03pKOC0qKCktLzM4P1Pc
+wrq2tbi/01tIQD5CS13lx726trS0t73DxsfEytHfWkU6NTIxMjQ1Njk+TG3dycLBxMvd7vH+
+9OfZ0svGwcLIztXY08rCvL2/w8rnV01FOzYyLi0uMTY8RFTv1szFwsLBwcfLysbEx8nP2uPd
+z8jBvbu6u77F129URDkyLSopKiwuNz9O7cu/u7m6u77Dx8fIztPb4u7t28zFvrq4uby+xddu
+VUE3MCwoKCotMTtHXtLEvbq4uby/x8zP19/g63nt3tXIv7y6uru9vsTRe049NC8sKCgsLjU/
+TnrOwLy4t7u+xszX2dba2+Hq39TOxr++vLy+vr7D0nRKOzItKygqLC82QVPrxr27ubq8v8PM
+2Nvc4uPi39/d1cnDvru6uru+x9VfQzgxLCkoKi0xPE1o08G8u7q6vcLK5uba4e7a2uDMysrC
+vbq2tre7yFxCOzUtKCUlKC43RGrNvLOvsLa6v8r/UUdFUW/z18nAurWyr62tr7jMbT4sKCcj
+HyAjKDBA/byvraurrLC2w25PRDs8RVTfxL65sK6tqqqsscDmPykjIx8cHiIlLDppu62sqaSn
+sbm//VRHPD5JR1XDubatqqqpqKy2vuovJSQeGRwgISUzS9G2r6mjpq2usthMUEA5PkVQ38S4
+rKmppaOqs7nIOSgmHxkaHh8iLkbsva+ppqeoq7TJW0g7NztBR3nDubCqpaOkpqq0yFAuJSAb
+GRsdHig5Scqtqaijo6uvtdRSQjg2OjxM2cO2rKimoaKorrPZLismGhccGxolMTnjr6qoo6Cn
+ra+9XEQ+NDM8Q1TMua+qpqKip62vyjEvKhoYHhoYJi8taK6uq5+fqKmru+VlRDY0Oz5C3723
+rqalpKaqrcY8NSobGh0ZGSMrLFO3saygoailqLrL10E2Ojk4Q+7Huq2mpaOjqqzAOjkqGhse
+FxklJipvubmroKSnpKm6xM1MOz09OELmz76tqKijpquv1D87JRodHRcdKCYvyr65p6Kop6Wv
+vLzeQkREOj5X+tO5rauopamssGU9OiEbHxwXHygkOsTHtKOjqKOlsbq9b0JGQjxEXvnLuK+r
+p6qpqr5MSjEdHR8YGCUiJVjHzqygqKifp7eyvE1LXzo5V1Vcwra1q6WurqjCPWE1Gx8kFxoo
+ISTzyNOroKionqe4sblKS2o5OVxNTsS6uKynrK+rvERMNh4eJBkaJiQkUMrerqKnqJ+ntrC3
+VlLoPTpZTUzLvLiuqqytrL1vTzUjHyAbGyAjJT3s3bGnqKehpa6ust5a7D43TkhF0cPAsqut
+rau1xmJCLiAgHxsdJSMrVfzHrKippaKpsq++WXNYN0BTPU/MzLyurK2rrbjOWDwpIiIdGyAk
+JDVk3raoqKikpa2xtd9X/D83Sj8/7M3Ita2urayzv+JQMyYkIRweJSQrR2DKr6qpp6Wpr7G+
+am1SOz9IP1DZz7+yr7Ctr7nH8kguKCUhHiIlJzJH68Cwq6qpp6y0tslgWkc7QEFEXdzLvbWy
+sK+zucVsRjcpJyciJCosMkvkybatrq2rr7y+yl1UU0RCTlJb2cXEvLO2trK6ytlTPDItKign
+LC4yRFzpvrW2tLO3vMHRcFBIREhQV2nWycW8t7i0sre7v9tRQDgvLS0sLjM6P1Tkz8O8vL6+
+ydzuVEZGSU1c7NPKwLm4trO2uLi+yt5YRDo1MC8vLzQ4P0lf3svEv8HIzudlVUxKU2D008vE
+vry6ubm5uru8ws79T0A5NDEwLzI2OkBMX+fSzcvN1uPsYVZXX2zsz8jCvry8vLu8vb28wMXM
++U5AOjYzMTEyNjk9R05h4tPPztLj4/Z37djTz8fFyMXAwb68u7q6vL/BzHVOPzYxLywtLjA1
+O0VR69DNzMTKzsvQ2dXNzszJyMrIwcO/u7y9vL7ExddsTTszLy0qLC0uMjtGU+DMyMK9wMO/
+xs3Ny8/NyMnKwb/Cvr2/v72/ys9lTDw0Li0rKiwuMjdFVPnLv7y7ub6/v8nPzs7V0dHPzsbG
+wb69vby7wsnfV0c3MS8sKSssLTM8SV/NwLy4tbu+vcnY0NXk28/X3c3JyL66vLy6vcTKbVNB
+MDAuJygtLC46Slfdvby+t7S/vbrP38/fYe/W6eXIycy+ury5tr7Cz05JPC0tLicoLi0vPlJe
+zbm1ubKvvcK61GXX7VFt3fjnyMXHvre5urm8ymNORjIrLyskKjEtMlNtdb2ztbWuscHHvuZM
+5uJJUNt3Zsm+vrqysbe5ucVRQkIvJSkrISMwMC9ZxMq7rauwsa+50dPWTkJRX0tZz8nLvLKy
+s6+wuL3LRTo5KR8lJx4hLjI1c763rqyqqa64t8NcU1RGQUxabtfEvbiyr66sr7u7vz8tMioc
+HSMgISo0TMm9sqWlq6qqtsTbUElCOTlKVFTYvLezr6yqqqy2v8FLKykoHhodHyIqMEa/r62m
+oqOmq7jE0EU5OTk3PUpmyrqzsKuqqampr7/OSSkhHxwaGxsfKzZKvaqnoZ+go6exweBBODYy
+MDlJX868sa+tq6mpqq2+1UgpHx0cGRobHyk2Wrqpo56cnp+kr8DhPC8vLCwxPEjYvrWuq6in
+p6iptdlPLh8dHBgXGxwgL1W+qZ+dm5ucn6m52EYwLCkpKi88Vsm5r6yopqSlpqu+5j0nHhsY
+FRgaHCY46q6gnJmYmZygrMFVMiknJSQnLTp7v7KrpqSjoqOlrL5nOSUcGRYUFxkcJz3Lqp2Z
+l5aZnJ+rv0suJSEfHyMqNVPGsqmjoZ+foKKntOU9JhsYFhQVGRsjN9qsnZmWlZebn6i7XDEk
+Hx0cHyYuQdS3qqOfnZ2en6StxU4vHhkVExQXGx8uTbmjm5eVlpqdprHLPyoiHhwdICg0VsKv
+pZ+dnJ2foqez2kEpHBcVFBcbHiU1VrqlnJmXmZygqLbSRy8mIR4fIyk0TsevqKSioqOkpqy5
+2j8tIx8dHR0fICYuPtO0qqSfoKKjqK65zk06MCwrLS84Q1zZwbixrq+ytrzAz9xqRzkxLi4v
+MDE1Oj9O8c2+t7W2ub/K0tz4YFpXUU9RV2X138/Ix8fFwsK/v8PGzNX8V0tDPjo3NjY4PD1C
+SU9deejg3t/b083Jw8HExsrP0tXb3t/h3trT0s7JxcDBw8fWfFFHPzs3MjAvMTY7P0RPXXza
+y8C8uLe6vcXM0N37YVRUWGHs0srGwb28u7m6v8zlXEk9NzIvLSwtLzM7RVjpz8S9uLa2t73F
+z+VoWFJQVFZe+9vOx767ubq9wcfR5XFXRT05NDMzNDc7PkZa/dbHwb69vcDI0+tlU0tKTE9W
+beDSycK/vr/Bw8bL1uJ+W09LSUtLSkxOS0lKTFJebf18dnxnYWNiYmFgYGr+59zUzMrIxsfI
+yMvO1d7te2xmYGtvb/3v73hpWlVVUk1KSEZGSk5TWFtmd+/l3/3TytvV1dHa2dzY1d3a2dPV
+3Nrf39/p6Ofq6vP1allWVlFLSUdGRUlOU1dg9O1/5+fu5/14fn3v8PHkzczk0ci9y+LEv9Ps
+z8rN1vLX1FRIRkM7NDU3ODg6Q1di+82/wsbFwcDGxsjM2ur0dm1u7t/f2s7GwsC+xcrE7UBM
+Sy8uNy4qLzg0PVpt076/vLGxt7Kyvb++z2liVElNT0pV7dXGvb3DubtUX903LDItJikuLjM7
+SN/SwbS0ta2ss6+vvL7F0elWVldNTFptb9LE2sq6S07WNi05LygtLy40Okd+Z8e0u7asrrWt
+rru5u8PWXu9aQlVlVPLLydDRvP887kEqMjQoKS4vMDRJa1DLtL64rK6zrq21vLi70N7Mfkpt
+cU1zy8/PzcHrO29BKjIyJycsLi4yS2pSwrC9t6uvta+tt7+7v+rr1VVM+vVV68C/xsC4yz5f
+XiksNSYkLC0sLkhtSceuu7uprbmvrbnDvMZTXe5IQV15UNm6vLu3sblS2s8tLTomICkqJik8
+Tz/Jrb65p623rKu6xrvNR09cPDtQWEzlubm5r6ux3cjDLiw8JB4oKCMpO0k+y629tKSrtKms
+vL+73UFLSDM4TEdH0rm3tK2psdXDvy0rPiIdJyYgKDtGQL6ru6+fqq+kqrvAvWM6Rj8uNUk+
+Qsi5u7SrqbjWv9knLTgeHiklIS9LTF+uq7Sln6yrpbDDxMk/ND40KzhEPF25tLOtqKnG8bo7
+IDIpGh8pISQ991m9pKmqnaCsp6q73uZWLi04LC1FVU/IrauxqKK5UMnfHyAxGxgnJx8wx83O
+o56qn5qorKa4/kpIMSYvMSk47W7JrKaqrKGqY0vYKhglIBQbKiEozbG6ppmdoJqdrq+0Vy8u
+LiIjLy4z3ru1qaCgpqKlyDk8LhgXHxYVIyknYqurpJiXm5ycpr/NTigiJiAeKjc+yK+ooZ2b
+n6SlvDEpKRgPGRcRHC0vcaadnJaSlpyepdE5MiMaHB8dJ0HqtqWenJuYnKeovCsfHhYOEBgT
+GDFPx6GWlpSPlJqhrmcoIR0VFx4fKuG3qp2amZiZm6e3vi4ZGBcNDRYYFzG+taCTkJKRkZuq
+uUUeGhsTEx4lL8CmoZuWlpiZm6jb7jIVEhcOCxUdGCuvq6KSjpKSkZuu0TseFRkXEh4uNL2g
+nZqVlZmcnahlSDsXEBgQCxQfGieyqaaUjpOVkpu06kUeFBkXEx4zPLqenJqVlJmcnapoRDYY
+DxgSCxMhGyaxqaeVj5SWlJy1Zj8fFhsYFSA1QLafnJqWlpmdnqpJRTYTEBsPCxghGC2srKWT
+j5aWk569yT0cGRsXGCU1TrGenJqVlpudoL1NRx8RFRYNDh4dHdmprZuPlJaVmqrKYioaGhoX
+HCs9z6ecmpiVl5ufrehLJxQVFg4OGRwcSLCsnZGTl5OXqrnJLBwdGxYbKC9cqp+cl5WYmpyq
+zGYmFhUUDQ0YGxs+sK2dkJKWk5anvckuHB0bFhsoL0+soJ2YlJaZnKrDTh8VFxILDxoUHs+2
+rJaPlJKQmam13iUcHhkVHSkscaegnJWTl5meq8UtGhcTDAwUEhcy57eckpGQjpGdp7Q0Hx4Z
+FBkfJja6pp+Yk5OXm5+uNB4dFQsMEw4RLD5en5SVkY2QmJ6m5SYiHBQXHR8pfrGkmpWTlpqc
+qzMnJxILExELFy4qaZ2amZCOk5mbo/QzKxwWGh4dKua8q5uWmJiXntw1PxsNFhULEyUfM6ij
+npSRlZeZnrNuQigdHR8dJDhXvKSdnJuZnsJV0R8QHRgMFSMbKrGtppiWl5mcnqvYXTskIici
+JjhX0ayjpJ+dpMFbzCgWHx0PFyceKsK3q52bmpifo6XBSEQuJSsrLD//yq6nqaWjqcI9STMX
+GiMXFSUqK+m1qp6dnZqeqqm1Tzs0Ky0tL0ze1bClqKinqrM8KjohFBseGR0nLc61uJ+YnqGb
+oK6300g4KyszMThuxraqqKalrLHCMSUpHRYYGRwmLDS5qKifm5yenqm5xHE6LiwtMjhH3buv
+qqenqK653TMjHxwaGhoeJzJRtaqnnpyfoKCtvsZvPjk2MThJb9LBv724ubvQWFA4KyoqKCsz
+ODxKdtLKu7C3wr2+ycO/yMa+wcTBxdh7XlRJPj9CQUxpZd/EyN3Tz2FQWEk5ODo3NztDUe3O
+xr+7uLi5ub3EzeZlVk9PTFNzcWTcys/Pw81oXVtJRUQ7Ojw+QE5818zCvcHHydH9amFPSk5e
+Z3Lh3enSw8fNys/9/tz2a+huUlleVlx96+bf3OprbFlKRUA8Oz4/QU3zzsjBu7u+v7/I2t7i
+al9iXvXZ08nAv8HDydZ3U0U7NTMwLzAzNz5OZ+DKwL27ubq9w8nU8WdhdPjw2MzHwL27vL7E
+zN5dST44MzIyMjM4PUNPetrMxcPFx8vS3ODw9uHl693OyMK/v72/xs7gaVZLQ0FCPz9CRkxY
+X2x59N/f7Hp9e2ZdXF9dXmrk2tfLxMLDxMrP2vdyaFlQTU1PT1pmbHbr3dbSz9Lc5OpxV05M
+S0tOU1RaavTl6OPg5uzv+nRtaWx87e3m3trUzs7Qz9nY1tze8HzvfF5gZW15b/1eUUtKSkpJ
+SExOUFJXb2td2tTpVmm5usrMwsDDxsrI2XjwfNza7dLQ6mhNPjo1NDg2OT5DVfHc0M7MzszG
+yMTFzNHUzsvNzcrHw7+9vL6/wMXtQS0jIB4jKi07SG+6q6aipq255lFAOTk2NkFW07q0sbCw
+sK+yuMLl69z+TTQlHhscISo2R3a/r6SenqGqu1o3MS0sLi84T9O2ra2srK6usbzC1/fq5f09
+Kh8bHB8nMDtD5ruqn52doq3AVTo2LywtLjVJ6cG2tLGur6+zvMXS1szO3EQuJB4eIictNjpU
+w62hnZ6hqrnOWEQ5LiwsLjdFas/JvrSvrq6zub2/vL7GajYoIB8iKSotMThhuaqlo6astLjF
+9VA9NTQ3O0NFTW3Uxr21tLe4t7m3uMTMXTwxLCoqLSsrLzhI6Lu1sK+wtLe8xtR/V01KQUNC
+QkdObVVOvbrFs7a3s7i9vcpuTTs1Ly0uLjI3O0NSb8vAvLq6ur3DzNHxXVNKR0lIVmBv3NS+
+ubm8vL3GxtDb7lNCOzs5Oj08PDs9Q0533c3NzcvKx8nR2/103trZ3NXb29DRz99pU93mauHk
+2/Db29PS9GhTSEE+Pj0/QENLUmPw2s3NyMbGxcXIy9Pjc11aTkxSVF7d3dfMz87Ox8nN0dpv
+U0xMTk9MSkdHRkZOVlxjfNzPyMXFyc/Z8WVPSUhIT13619HOzcfFxMbKzNzwb2BXVE9NTU5Q
+UFFTWWL77+Hb3t3i8HlkWFlQSUc9ZcJc3MXMxsnLy8vPzNjhc1tiZFxaUk5FRuX4Xl3ZytHK
+0d15Wk1KRkZLT1pRWHfz1c/N0Mrd18t70G3m4WnX9d7ee9ti0HDM5tLTbNo/SD06PDo6OkhY
+0cnIvr66uL3Fz+b4XVZKSEpPVWHn1dnT0tvZ5dXQ1tfl29bJ2NxQOTczNzY0Nj1Q3MC8trGw
+sLbBzm9XQzozMzk9TFfiz766trW6vMLGytDZ3uvcVkY1LS4uLi0vNU7Uvbayr6ysr7nM8UxC
+NjEuMztEYN/GuLGxs7S5u8bf/l5iavBTSTcvLi0uLTE0RvjDubGtq6qvtb/YXEk4MC4tNTlC
+X8y6sK+vr7O2vNTfc+12ZVVCQjIwLC0uLjQ5Ttu+ta+trayvt8DcTz40Li0vNDhJdse2rq2t
+ra+0xMdi11dbUUNMOTgsKywuMDM9SN6+ubCtq6uutr/RW0M2MC8vMjlFb8K4srCtrbG4xcr2
++1tMUEdHPTYxMDI0NTY7SGXQxb21sq6us7rD1lQ/ODU1NzxCW9XFvLa4ube/wczR619QT0dQ
+SkVLPkdETU5JS0laVnnh4dLMw8TK1t9jW1NKTlJVb+DZzszIxsXJys7P1OLnXlVMSUtMVWrm
+49/X1N3ie2FZTEhFQ0VHSkdJTlp57tvQysjGwsTGx8vM1Nfa4P9iXVpZWF1iaW356t3Z2tje
+4OX6ZFZNSURAP0BER0xVau/d1M3Ozs3MycvO0tff6/h+dHBram1w9Ofe2tvc3OHo9WphWVpS
+T05OTk9VWWNydOrk4t7a2drd4uTt9Pt1b21iYl1eY19pbG/3593d3NnW193f4+fq/mdoZmz/
++3pqbG1wcn9wb29ra19jY21uZmprb2ptc25+9Ozy7e738+zl3+Do8Hzz6+bh4+fn+H53Z2tq
+a2Jlb3R5bm9xe3l2+nv+e25qb29+fnF0++fg3+Tv+3d8eXx7/vD8cm1uev37+nRubnP9eXVy
+a3F4+e95cXX78/b2+319/Onk6uff4/Du5unvfm5haHN8+/72c2xsZmVqbmllaWxpbn54b3nt
+6O39bGp7b2/88+fk49/f4+Hj7O3r6e/6+Pj+/Pl+fnx3cWZjZGltZWdkZWpsfH99e/3x8vx4
+enFtaW738fHq4+rq5+zq8/Lr7Xtv/PH/c/v17Ovo8W9rcHBvc29qbG99+fj8ef37d25pZmJj
+bnZ69vPn4+bk3uTi5O5+fHprb2xpcHt+ef7z8u7s8nh2d3lpXmVvfv378/bz7vpxd3p68358
+d3z07/B/e+/o6/L8e250dHL86+726urp7fL6b2NiaW1qZW1zaWz7+/ns8X/5e3vw7/Pz8+zr
+7/Hy+3398/j17Ozv+Pn7+/78/vv+cHNtYV1dXVteYW52dvDp6ebk4eTm6+no8f/8/nX77+3w
+8eri5e76+XpvcXh6aWVlYU87zrw+4npRyOlfyd1L8tBMWV97wkdV68NYZmK6VDSwfElb6cLa
+5szB3kxbbPo+dvvh4Fri1uZQzNtN2EZrX+5D0ml0YETBR8tvZtTLQc7NTdbMRsD/Uenu31xx
+/8dc2e3jZtJT9WPVUU7aUFTXeN/cStDd/k7KSNBOyk2+bOt1Ztv56Erc1lD5ccxJ6m3GS8g7
+xV3fT1/HVHVO2dxmb2TLTEjQ5lfkXM3XUd7az0/KTsNK5XBi0Vx51FxeyvBJvz7j2kdsy0tj
+61vJakza1k/q+sdlSvnKVUzPyUddcsVzVubD2EJzwE472Ndkel7RvTd0v+pedVbKZT/sxVpe
+X8XsV//CXETWzUhG3MnaP3e/2UPv2cpHR9nTSFHbzdJJ9cz2Tl3O10pfzt5uUeTKfVDvzutg
+XeLmXVHb0E1c2kpY/GLP4FzG3Wz7WeJXYdHnUs3ST9rXb/lJb+5KXW3ozFhe1+5V5F7a3FZ3
+U1nZWGbK1dbWY2r2VXHk3dJebs1WVs9Y7NtW2XFJ6G5O4Vx7z0VS1vJXe83GXVvM0FtLVstn
+TPLQ3N9f9NriYdDYU1TwUknmfOr83ddx5OjfzVtc9HRYSVTk9ml6+tHbcW/dXUlUcV3y2+79
+0dDfZNjV6OnfyuRozMthUG9NOTzh2uzd0cXdO0njTkl2xN9ASdZSTMXBw2NJw9BEwLnczd3u
+9UxUzMZ46mZZTTk61s5DTNxjRjc+yWxY1k7h00ncuMDH08u7+1y8usNt78BWRtDbe1E57kwv
+PkBLUDg7ZFVPT1XBzenDz9rCzc2+v8O9vbnNfclePvHIUc/AT1o+LTEuLT0/P2Jp29ZfyL2/
+vODkxd/exr63t7evyFfKVz7tyMb/NkdEIR4sPDIvS8G51mq4rsRdwbHFaMe2ucbMtKu+UMe2
+2EJdvc8sGhkkIB0yx66rvMO630t1zri1v7ayvL24r6y1z+TExExcsq0mCRU9Gxgto5O2LbWj
+7yMtqKNJTq2osVG8nqnYU1rI1MisqboSByQlEiTUl49zK6mwMSQtpqU+W7KprVy1oLdWUFvG
+u7qpo7oVBxcpGSLLl42oLPWxPxwgtZ+0TnWqqGJks6+9UkfTrqanpLQQBRQgHDK8lYqdOz04
+MSEZSp6gr2fKqbw9SL+uvUj7qp2cokMJAhckIsuhjoieKy0nGxgZPpuWpb/AuM48LVSsteLD
+qp2XnCgCARoyN8KhjIWbKSQeFhYXMJiPnLbVcNY9Ijitqrq9qJyUnhUAAxtKx7ifioeaLBkU
+FhseNZuOmK89L+DhKS3Cq6irqJyUrA4ABRj6qaOZjYuZOxUOESFAcaWUl6dWKDbcOjRUw6ee
+oJ2Zxw8EBRJOppuSj5CbZRkODR3Zq6GcnaW+LykyO2HJw66jnpycTw0FBhF+npeSkJObzRwO
+DBnLn5uep664RCcmM8OttLSspZyZTgwDAhKwl5GSlpqexx4OCxi2lpSfwV3GyjkpK/mqqa+4
+sZ6ZzBADAQ/KlY6Smp6itS8TCxE+mo+Ztz1Sws49KjW9pqGqrqSjcxgIAgssnY2MlaGz4Tgd
+EBAhs5WSntA9THplODJtr6Sjop6jVhgJBAshqI+MkZut+TcfExEbRKCUmKjLSkE/MzJqr6em
+o52duh8LAwgZuJCLj5mnyU4sGREVI7uYlJyq3TgxKy56r6ajo5+erywOBAUPR5SKjJan1Uw2
+HxUUGzihlZedsjsrJipfrqahoaKiq0IVBwQKH52KiY6fWTExJx4ZGB/TnJOTntUqHyNFsaSf
+n6SnrNgfDAYHENKPiYmSry8nIR8hHh4uuZ6UlqdFIh0sxqacmqCsulgnFQsKDR+ljomLl74t
+HxsdHyApTK6cl5y1NiIjPbKfmZuktU8oGQ8NDxhFnY6Ljpy9LRwaGx0hLs+lmpqlzi8mLfWq
+nZqdp8IyHRIPDxQjwZyPjZCctTEdGRcZHi+4npiaqOkwKjXNqZ6cnqnXKhkQDxIYL7ackY+S
+m64yHhcTFRsytJyWmqXNNC872a2hnqCq5ikaERASGi+5nJKPkJeoPR0UDw8XLrSbk5efuDwt
+LkG7o52dptApGRAPEhouvZ6Tjo+UoWAgFA4OEya7mpCSmqtGKSYv1KWbm6C+KhcPDhAaLcSf
+k4+Pk57EKhYODBAd1JuQj5SkaisiKl+rnZqerzUYDg0OFCjJnpKPkJSeujUcEA0PGTqfko+Q
+nsg0JihHuKOam6fcHw8MCw4ea6GSjpCVnbFMIxUPDxQprZiQkZyzSSwrP8mqnZufszIXDQsM
+Ey6smY+OlJmj0TEcExIUHnykmZabqcg/NkHfsqWfnqfWKRUMCw4XN6qakZCWm6f+KhwYGR4z
+vqugn621wk7ru7mro6eqr2QoGRAOEhksvKKal5qfq+0vJyUmLkfgw7S1vr7F0ruwrqimq7TL
+QzIrIBsdHhwqWteqnaesq+47aUI2b+1I58pc+cLJwK2pq6eqx0c6LzHwvOZHNR8aHiMr67K0
+rKq+zsRyTt/Z7tfV4trKw7qvrrS9z1dKZ9zezsnqUTstKCUkKTZNzcTU7Hxx0b68u7m+w7/B
+ys3X3dLJvr2/xmtCPjk6TF3qx8PL3Uk0LSorLzlN7NTMx7+7u7e6xsPDz8S9wb3F8Eg/QT0+
+V2NuwsTNz1w8ODQxOkFN5MbAx83uX1xr0MG6tbvO4GJNSE1MS1Je58i/xMjaSj05Nzk7Pk72
+x7a1usPwYFRO0by8ubvYUUA9Oz1BQ09718G8yc7PRjw8MzI8SUvLtbm6u8puYWdkyLezu7zB
+6Ew8LzM+P13GyMrJ2WtNRTw4OUFM7M2/usLY2FxNXerUyb+6vL3Nb0M2ODtBXdDNxbrH8FpF
+Ojs+PlbFv8PEyt1OVVBKZsnFybS+181yQz5NQkFh7vLe2P9fY1ZRS0Zd4NjFxsDJ6lhQTT5M
+ZOfIvbrBx9xSRUZaVvzV0M9iR0tIPT9OYWrV0s7EzvDi1O9Wdmdk1c7bx8jb9ltXRUxP683N
+yd/ge00+OzxFUHnTxM3b3P9YUGfq3OnPy8rEy9Xd3VdNU0hKY+Pn3tHcXlpSTUdJUlrVydrb
+1lZKW19ufd7LyNLKx9N47e9XXmZPVPtgYnj4W1RQfG1ed+jW2eBr8PNUUV3j08vO0svU63Bb
+W3ReYWro6m1gXVdbW1FabV57e3Ds4/TzW2ruX1/RytbFytvR2lJt7WBebubielppXU5ZXFRW
+aedZVNvZ7WP823lY/M/s3OD03s70Yc/pVe/b6mFh629WU2htcHZS9fNnYl7Q2PNd79TgWW3M
+2VZR7NjSWmPS0FRI1N9QV2Rs6l1g4dDrVF7p5uR1aOndbVXg1fBgd93ifGpj+25ee+Ld+nbc
+Z2p85F1n12Bp5Ohi++xYa9b4X9veXG3xXlJf1NflX2rg51hs4fb95vR27FpZbOr8XNzc7fBo
+V2BoWXPj3Xr63t972NxzaOvlbmpf79fqftraaF5edWVj+Pbsb2Xq3lxWam1tX3/mbF5gXldY
+XV5SUWnk0srIyMzOy87JxcXFx8W+ubm/TCYaGR8tO0b4wbaspKCqy0E6Ozs+S1pry7Sus727
+sbG1s7C5QCAZFxsgLD5vybKln5+nuVEvJykvO0Tgs6iqsb3Fxbuvr7S4trxDIRgVGB4sU76z
+qqGen6nHOSomJis4asK1rauvvs/KwLy1sLGys78+IhgVGSM27764rqWfoq36LygoLS81SM+6
+rqqstMLDu7i3tLW4tbPENx4WFx0sRO7KvK+opKm8QCwoLDE0NkjHsauqrre+vbe3u7u6urax
+u1EpGxcbJTRJds27rqWlr941LC43PjkyPc6wqKittLu6usTZ3c+/sKqv7SwcGBsjKzA6Wbuo
+n6CrxT4vNDg0MTA+yKujpq+9x8nJ31dS27quqKe4QSUaGBsfJy9Jvquhn6Wvykk8PDcyMTZJ
+xa6prbO8yNT8U1Lrv7Gtq6y5USscGBkdJTFPxbGooqOpsstRQzw0Ly81Trysrra+xcC9v8vf
+6c6/ubS22jgoHx4hKC0wOlHFrqWipq2702tOOy8rLTnwvru5tK+us8VeR0/szcjHx8ndVzwt
+JycpLDRD6r6wq6qttsxdQjk3ODxEX9DGv7y9xNRfUVzlzMS/vr28vMTfSTczNzw/PDk5PEhg
+59rY0cnCv8DK715kaXNuX1dVWmhvamvmyby1r6+ytrzG4E8+Ni8uLi0uMThGbM/Kzs7P083H
+xcbJzdxpV1BLSU5m1sO6tbS4vL28u7/O91BAOjQuKiotNDtDTV7u0MG9vb6/xczV8GBYYu7g
+4uro2s/IxL+8uri4usHcUD83MzIxMTM3PENGSU1XctXLxsPBv72+xc79VlFXaXRv3szCvLq8
+wc3c3+98bGhmZGhZSDkxLi81Oj9IXdrDvLu9x9Tf7HX+b2dz59bPzM3P1uDs5NfPx7++wMPI
+zN1fTT83Mi8vNDk9RVBf3tHOy87NycvN0dbO2OTmeGFfW1xv6s/Ewb25uLi6vsnjUUQ/OzUy
+MTM3Oj1DR09p4NbOzcS+v8LI1OtrVlRPTlx+2Ma/vLm5uru/xcjO1epeTUI8NzMvLi8zNz1D
+UvjXy8S+vbu+zOx6ef9zeurg2NLPzsjEv76/wsTDwsbO3WFLPjo0Ly4uLzQ7QUxf5NDKxr+/
+wb/Cztna4XddXV9jb+zb08rCvr69vsDEy8/fe1tJPjs2MjMzMzg8P0la3snCvru7vb/I1/xk
+VU1MUV5w3tPNycO/v8HCwsPFyc3fb1VHPjkzLzAwNDs/S17ozsXAvb3Bw8fR2OVlWFdaXGNl
+ed/OxsG+vby9vsHIzudqWkpAPTkzMjMyNDg8RlbxzcPBvru/zLvVfvxmXmz8a+Hh2c3Lx8fG
+vr/AwMfKz+JoT0Y9OjQxLy80OkFIUWni0cjBvr6+vsPK2PJyXVVWWltr6dbKx8G+vr/BxszP
+3m5dU0c/Ozg1NTQ1OD5IWfLUxb69vL3AxsvZfGJZV1NWXWJw7eDb0crHxMPCxMnN0dz3Z1pO
+RT89Ozo6Oz9GS1p479TKycjHyczP1Oh4Y1pbVFlhZ+nZ1M3JxsXFx8jKztDY4/pnW05IQz48
+OTc5PEBKWPLVysbDxcnLzdHedWZeX1dXZW7439jWzsjFw8PGx83U2eLudllNR0I+PDo6Oz5F
+TFx53tDMyMbHyszS3vZlXVlbV15peO3d083Lx8bHxsfKzNfn9G9nVUtIQz08PD0+Q0pSYvPZ
+zsjFxcbKztbsYE9QT1BTX3N9623qvbvBvrm8w8zR0d/6XEc6NzQ1NzY6QFV80MrGwL6+xs3h
+eGFZT0dCRExXZXne0cvK2cy6ur6+vcDJ2dXX6WdYRDcwLzExNTpJ8sq9u7i4t73Jf05EPT06
+PD5GUeLKxb69u7u9xMXIz9Hf593R0NXg+2hGNy8wMjE2O03myL66ube3vct2VEc+PTo6PkdT
+5c3Cu7q5u77EyNDb3vHy8d7Z1N35bEw7MzEzNTc9TunHvru5uLm/znFSRz49PT1CSUhgzcK7
+uru7vcjM0N/f3uXi1NDMztrkVTw0MC8yMzdAV9nFvbu5ur3H31tORz8/Pz9FT3XOwru4uLq/
+x8zQ3fTz4ef30srO2tjfSTUuLi8vMDhQzr24t7Wzt8JySkM9ODY4PUlTZM+7tbO0uLy/zflu
+eerd3NHEvsTK2nlINCsoKiwuNUXbvLKysbO1v+ZEPTs3Nzg+SGnZwrm3srG0u8TY5Hhic+ja
+0MfDvsPO9UUxKScmKCswP+K8sq6srbG93VJCOjMwMztIc82/trCvsLa6wMvdYlpy/+rSxL/B
+yuJXPS0oJiQmLDZQyLmwrKutsr3gTTs0MjEyN0P2x7uzrqyutb7HzuNbUl9t7M/JxL2/1lE4
+KycmIyYsN2G9tK6pqayzxGRGNi4sLTFBdtO9sa2srbS5vMbfZ15gcGbuy8PI0XxOOSsmJicn
+LjxpwLSvqqmutb71RTkwLi4zP2LPvbKvrrG2t7rH2uZya2xy68/N2e5XPzEqJygpKzNOzLiv
+rKurr7zRUDs0Ly0vOUriwrivra6xt73C0e9wam3x4tjIydVqRTcrJSYnJyw99ruurKmmrLfD
+cEE2LywsMT1b0760rq6wtbi8xtfxcf3p39XOycfWUT8yJyMlJygzTciwq6qoqK+93kM1Lisr
+LzhO0L21r66vs7u9wc3Y3e7e3eLSyMbNbj4vJyMjJSkyS8Svqqemqa+9Zj0xKikrLzznwbiv
+rq+xusbIzNHS2djLzM3MzMPNSjcqISEiIixBfLKnpqOiqrPEQy8rJictM0bEuLOtr7S2xdnP
+1NnLy8vCxczHwuNDMyYhIyIiLkbOrqimoqOrt+U6MC0pKi83XsC8tq+zuL/e28rOzMPCvLi+
+w8BqNSwiHiElJzb0vqylpqWlr8VhNywsKy06Stm6t7u7wc/W/uzFvbq2trOzv+4/LCIfHyEo
+NlG9raqmp6y0yk07NTEyOEbsybu3ub3PXkpHSmjKv7aur7K310AvJCAkJis6/L2tq62tssdc
+PzQ0Oz1Nzry1s7m8wNtOPjo+UeXKvLCvtr3KTzAoJCYqLz7duK6tr7S/20s5MjU9RWvGurOx
+t7/F50s/PD9a1cW6tLW+y/pINyooKy84S9a6rq+0ucZvQjcwMTlDWsy6tLGzusfdU0M+PUdw
+zL24tLG30V1KNyonKi89VeS9r6+0ustxTDkyMzY/ZNbMvLO1usPZdFhHQ0hb4M/Iwr25t7/m
+V0w9MCwtND5NbMm5tri+zu9cRjs4PERSY+HLwLy8xNDT1OFlUVru497VzMbBw8PD50lGPC8u
+MTZCW33Kubm9v851WkY6Oj9IYdjKwLq8wcTP8V9NSU9dZ+3PzsnEx8nJy872Qj0/NzQ6P03i
+09bIwMfO2V9SV09IT1hr3tzfz83Y2Nfn69zj+PDw8ujg5dzS2eXf4HxgU01LSEZFRUtf7dnN
+zszGy9rzXFBNS0tPXv7j2tLNycvV2Nje4+vt1svO3XRaUUo+Ojs+Qk972cW9vr/Cy9xkSj8/
+QURNZdjHv7/AyNf3YVJMV2Bn5dTX08/W4XFXTk1JSE5YZ+nX1tfa4+70aV1eXVtlenVv+vbv
+6vLr4uLj3dvc1tbe6PdqZ3NhW3Pt6OTq+XNeT01OT1VebfDVzcvLz9nqblZQT1BWXWt859jS
+09bb3upyZ2JbV1VdaW368Ori6uvt9P1udH387+Tl5+Dg5u98c2dfXVxgYWru6+/17Obt9314
+c2tnZWleXGBpZWzo4eTg3uPn7+vq9P11efr06uPt/3VtbW5mZW958ePe5PZzaV5bYGZqbnNz
+eHZ67eni6Ovv7ebr7erq+/3ycWdkaGhve3d6/n76fnr97erx+fp3a21gYWpqc+nl6eLo+P/9
+dm1rb33n5efl6+30fW1tdnBqamhtdXZ3e/rv9W5pY1xham5sc/Dm3t3d29/k5ujxfvrw8+7v
+/f5uYlxZXWZtbG557uXi7Ht+/G9kZGNhZ25ze+vk39/g4eLteWRiaWlub3b+7+rm7fX07+57
+c/zv/vvt7O7u8v90bnh+cWtvfXZvd3VubGxxb29z9+7z9+7v+vrw7vl/8+3t7+/z/frz9Pb8
+dGlgXFxlY2hz/fPq6OXl8Pvt5fB2bnN0+u/9evn39vz7dXvx+3r07359fn17dm9sZWZrdnZo
+aHR1fOzh6fbp4uju+vjw/m1w/fD28/d+dXv27/r/9fv0eXV6bmpoZ2Zmbv/+fvry+XX+7Ort
+8fn7cnjt8n/38/l0cGxqZ2hu/e3w6+rr6vTx7/d1cWtqdnV89+7t5uju+fT1dnR1dG13fPfy
+f/z+d3F0/npua2dhYWp5+/Dq6enp6+/w9Pn6dHJ1d/Tt7+zq6er7e3lzdnNzaWJpbGtud/To
+6+vr8/ry7e34ffn5d290cnF1fm94eH32+Hz8fnJ7bmx3/X1/+vbx7+/67+/q7P/99u7q9Xp7
+dXp4/n5ye/n59e35e3l1fnp4cnNsbv55dXBx+/T9cHf7fv769+7t9/L8a25+/v5+/fH5evrs
+9PPp6Pf9fXzz6/L4fnZpZGhpcXl6dWx1d3xzfPD5/PbxeHL+9vf7ev9+bm5w7up/ffD2+vV8
+c3x5+fP38e30fHd+6/t7fX57dvv1f3FwfHxsbXBoYGFla2597OLm7vLs6+/w5e51dHz4/v78
+emxv+/T08n1ua2xreP1ucvn+dfTo8frk3t/o/n30+fL4e3JscXVtaXFzcG9tbWxvdf5+cXX7
+9frv5uTx9+3u8+/k6vX58O3v/W52eXN1fHNvdmxhaG51e3dxcX329/ru9Pnt6+3r6+/u6/d6
+/nhubWxpaf/08XxudvLq7evwdXV6e3t++H92bm//9vR7bnZ8+/n8dG95+vt9efXo6+/u6u73
+9vF4fvP0e3JzbnP9fHBvYmRsanD67+30eXL6+nd2fHt+8u32+fbx8/X7+u/w8vn08fXx/G5p
+d/X5fPrw/21ubnb9+v91bXj5enFvbnh6/vHu9P/2+fr6+fDze3328vR99/F7dvr59/r8fnZw
+ePH2ffh8enN0fHtrZ3P7+/Tt8v1xdPD1fvX1/v767+jr+3Jtb3t9b1xSWWj76ufe2djW1dzr
+7vhvY19iYWFgbH1xd/Hq7+708n5ufPL+amlxffj1/nltdPHo6/zv9nFx7+9wb3Fza3N59vZ2
+fPh8fPDx7e9+euvn7vTn9Hh783hvdnN0anby6fdyd29rbv77e2trb3X/e/vz/nl69fl5efpx
+b/x9+/l6efP7+Pr7b3jv7e7n6O73fn3w7vt++n59fH5ub21taGZsbm1sePnw9/x0bm9wavjt
+8vDw9mv4/ent6urt/fH89/fv9XR1a/52/XR3fHv5anBeY254e395+Hzu9ezs7/Ly8Xt3dex+
+end+fPR9+HtxfXN6ZnNvcnz4+Ovf5Xhr8/D56+10fGxpb+3z7urz+21xb/1u9mtuefh+cX19
+8mjw/e5xcWx0ffnyb3x9fHpveOTq+2l38fno+ez4dmrv8f975vp6/e/3fHVqdm5lYGRvbnLq
+4+Pl7vZ8/2x9/fd4eHzt8vd1Zn18dXD/e31w/Xx5be/ve3JpbP/x/fXr7/v97+576+vq/HJ9
+e/9qZ3Lwc/5ya2Jv+Xd5+err9Hn57/p75vZw+3d6cG587ff47OB7aG1uZG937/V3Zmf3/u7u
+7nXp6eF4d+9t/HT2cfdnbmdta/Fs+eft63n7+Ovv7nvy8Ph2fO/yfXv6aPL5/HR5fHJ8Zmpy
+/2787enq73dzb/Rwbf5o8/j37+/9dnn06nh3cnnufmfz5/7u9f7f+Pnqc25sbXJ09vfubnT4
+cPx89m1scH9+d35ydv/6fnb77X3z9ursePp3d2997Pn+9vTu9fXrfnVtam5sa3TnfHnwd/d4
+8/j3cGbv+nxocHFsdv/q/fn76vJ7/+z473728PLp+fzv/vp3fXhraGRmXm54/fzu/HHv7vf4
++vd8fP/49n1t/O/t/vLh7vH76PV9/u56fe/5cXJ1a3h++HhuZHBiaf/6a3B1aW7w6/Lq7eR9
+d+3w6/rx8+d95/trbHdwb2h55nd39e7t8efq6XV9+W1oa/Vzdm/9/fP5an1ram3z+2dpbHv8
+9vXn7eLr7/Tu9X1/eux3cP7++XJh9uj3fHf4e3n7bmlvf3Z/+fP24+p2bXj67Pjx+Pn68vJw
++3NzcGx1bHxm7/Lu9e3n7O3q9vb2cnVmdmtm+nT0be5zfH3+eOztbmtrb2d+/Pbp9+b18urf
+7/r2+Op6cPt4++/3fG90cXt+bG/q7nZod3btdv1wfXN4+/zw/H9ufvj+cHzo7O7td3x1fe/4
+7nL7ZWFlbPpnd+rm5un4+e75fO3q8+jx7vf8/nv97PVzanl4b2Rlb/BxZ3pq/3b89PFxcvX1
++m/u6H538evr9vfp6ep3fX5q9Hh87frz/m5ubF9wa290am/17+3p7urzff7q5fD4fuh+anXv
+9fb17Oxwbfbqd2dkc3ZkYntuZ/xz8+18a/VsY3/z8unpauZv7+7ffvHo59fl7O/obG7q7Wli
+d2hr+e9RZ2hj1+376/Nzb2d6alxuafLOzeng4XxdcF9ud+9kamp+YnLrbODz3m1j/3Ro8mvm
+3+BeY3xwUXbVzcvb4N3dWVxa+mt2b/hsc3htcXtuY25QVnJgZNHm299s//5QbN345tHX3+vl
+3mdTWGJ43uXcy7/Fw8PL0t5qRCwnKi0vNjxTwLezsra4trvAydLKxNDTxr+5t7i3u0ggGxob
+ICYtY66ln6GorsA/LignLkbXtqyqqa21ta+sq7K6uMcuGRAPFh8qQbegmZidqsY2JRwbHjK2
+npman63NPDZMvKqmqK3ZLR4WEhIYK7+impeanqtSJRgUFyBKp5qWmaPKMyct9auem5mfWB8U
+DQ4RGUGhlY+Qmq08HBQSFiHeoJeXnKvjMCgqP7KfmZeZsSoWDg0PFzOmlI6PmLAxGRITGSZu
+oZeYn7g/LS0yWrGgmZaYpTMZDgoNGSmzlo+PlK8sGxQXIDHjqp2dpcU6MDM+37OknJiYn08b
+EAsNGCe0lo+PlK4qGRIWIj+2pqOlr3gzLC9Nv66knpiXoGIeEQwPFyiumJGQmLcrGRUaKPeu
+qqSlt2IzLDFNv66lnpqdsDUcDw0UH2melZSWokggFxYfP7atrquuu9g7N1LLt62noaW1TiAS
+DxYhy5+amJqlzCwbGSA7vq6npq/HVDk7Zb6wqKSqs+omFg8RHVCkmpicpLw6JBweK1qvn5yh
+r3szMUTRtKuprLRYIRIOEyHbopybnaS3RiUcHihCsJ2Zm6O6Sjk4T761v8vdNx8VEhkutKCe
+oqy5yU0vJiAkQ6qcm52lrrXCzsn7OTUyJiAdGyVZtaimscz2TDw3MSkqTq6hnqGrrq2vrKq2
+VjMjGxgXHS/Lq6KircVPODAvKiUx1auenaSssbm5rq21yT8gFxUXIEi1p6Ops8dWOzMrJCs/
+ya2in6GkqrCzt8PfPSUcGBgeLt+uo6Clrcg+LyojJzdZvKynop+gpqistso/IxgUFRolOdav
+pZ6cn6zSMiUkKS8/6Lqonp6mr7/Gvb5vMSQeHiEkJik0ZrOloaOrtsTnVUdDT8uwqau6VTMt
+Lz1+wbq8y3ZALyspKzRI1722srK78z0yMj/SsamprsB+TUxZ2sTAws5kRTkvKyotNlLKvLm+
+1VlEOzpF772vra+1vcze73hs7NrQy9VZPTUyNTxGT1ZcaGtfTkdGTWbXyMG9u7m3uLvB0eXn
+2trnZEtDQD8/Pz49P0NOZO1xU0xPZ+DPy8e/uLKwtb3N6G1sb2RcUUlEQkE+Pj5BS1dcVVBT
+XXZxcPLYxry3tri8v8TKzdfuZlVQTUtIREBAQkVISUdKVW/tdGVk99XHxs/Z0se/vL7DzNHW
+2d3pbFxUTkhBPT0/RU1PTU5aa/z0b2lt79nNycnGwsLAvr/Fy9LX325USkdEQ0JAPj9FSUtN
+VF/15+Pe3tvX1M/LyMPBxcnMzM3P1NztaFdOSUVGRkVHR0dHSlJg+/Dv/33y4dXPysjHxsbF
+x8zY+V9XWmBx6OLlb1RIQkBDSk9bYWNeXF9n/eXYzsnFwcDH1HRaVFvt1MvJzdbsX09JRUZL
+VWd+e2FQSkhHSlFu2s3L0OxscfHZzcnFwr++v8jZcVtWWl9neHBcT0Y/P0JHS0xPUlJQT1Jc
+bubTy8O9u7y9wsrP0tXX193k7XVeVlRPTUlISUhIRkZHR0lMU2Dx2s/MzMvKx8TCwsbM0dXW
+0tno1fVRQUnuWEpMUUxKQTxETU8+RcrKdMm7v8PD2P3Kv9DiycDV4M/abnzpUUZPWEU8PkJE
+Pjs8R2BnZNrAusDKwbW52Om/t+l02v36+EdT7kxCRl9MPj9VUD9O7FdAb89mcsjI4tzKzNbN
+ysrHysvKzNXe6m5aU1FTUU5PS0pOT09WXGFld+rt6e5zc+3t6N3a1djZ19vc4eff3u72dHv9
+aWpoYlxdZV5ZVVZYV11rdvvm29nY3eHi3d3k3tzb6O3v9fHo6HZueGVYUFFRT1BTWl9t7PF0
+7OTq5+Dr8Ong2NjU0tfb1djh4u3yZ15bVlJYXFhYXltYW2Bqcfj7e3/w7Ozf3d3e3Nzd393c
+5/Xo+G1nXmBoZFpfaV5ZXWBiZ2du/ezr8vTm5e3m3N3e2tnc29zo+//5bWVjamZgYl9aV1la
+WVtlfu35f/vs3+Hq8ufn6ubk4OLg2dXZ3uDpcnF5XVVXWVpZWlldZmtsanFzbv7w5ejn63Rh
+ydRSysvo4Od45v1fanFZSM3XPX3LUFXtYWZoWmt0dm3/3ufu5tzdz3nh3Ftf3NPz5d34+Plb
+Z1paVVZCPdpo1lxdt27f2Ml84v/o6FdHdslR8FC/4UjU4vpfeF902U1pTdZPTnDPZGzYWs9r
+YeHUUU/T8rtD7sHcy0fm3MxKQVxfXzzJOezZR8i723y7YNxRRd9O20fJW9BKxfHNzUzDXV5i
+Ss9M10Pc6UTQSclf++/QdEzbPMBTWHXUU9Hkfb5n3uZjx0nXYf5sdVpfzExO0+xZ1l342FVc
+x0p45ErBWUzLbVfORM3kW89nzMpfd8dB0Vk/4eRF3vfV8mP76mlbbnnedVtmy99U5uH06E73
+xFpZzWDYUFHQ6E17ae7MSNS+ZPxXaP5eSNrv32BN089kePZu6U9b6P3W7l7jaupi9erO1N/j
++mJPS15m2t/e4thkWmJjbm3c5NNRUOZa9VDay+n3YPFu6+Rv3eRrXflrWF3463VhW+/fXHXb
+29tdXNbiXu72y8tq7eDn421j2t9rSkVKUGtl3dJ5a1lKT1Vd7Hfz0n7o1+vh0trN4e/P0M7N
+6dnOcnzme9VeVfNm/E9S2Ek8RDk5Mi08W3DEurq1wdzVeOLJxby9w7/EyMjG0NdmWFM9S+5s
+z7u7QygkHyUuMk29uL3C017gzsO9u7e6w9HPw7+9xMXEzmNXeFlg4lj5ur3AzxoOGyc4yrSk
+nK8wJy5Tv7uxpqq/RTpQwbC0uMHdT0Jezr++xtRZP1/F1xoOHy1Hubmvnas9MiwtR72rpKiv
+yUlAVsO6vM7kW0VMZMGwrrbIXD05W80jDhMs+K2stqekxDwsKT+9tbW4u7+8us7O7U5LP0Rb
+1MzAtK+vucPbTDUsQVIgFh81xaqys6euyEgpJjfYr6auv85dUD0wPdm5uO4+QnDBtbSwr71a
+QEBUuquzMhcRGi/JqKKhorFBIxwhQa2jpa22xjslIDDHrKvLPDpZ08G4rqq07Ts4TMawp6au
+TSIYERMhaqaZmqXILx8eIz2qm5qjxS8kIiYzyaqlqr1KOD3WsqutudhGODpXwK+nqr8wGRUR
+FS+wnJSZrFcjGRwn/5+Wl5/zJRwcJk6xoJ2p0zYsNFbLuLK2xFg6N1DBraquvk8oHBoXHGqo
+nZqkzT8qICk6vZ+boLYyICAmNNevpqW2Ry0rQb6rqbDPPi8wP8quqKmzzksuISImIDC4rKOj
+v0Q3KCgwP7qko6vKNCwuMkDdvq2mrs49LjjNtbK5300+MzdSwayps8DS3dg4JiMcHku9r6aw
+yM47KS872quqr7XuPzwzOP23rK7aOissRuC6q6y11DUqLT2/qqmvv/xdbs7rPT49JR4pNc6r
+ra+tx0U4Ky9kuayszUA5Nz1T6cGytcNGLDFH2rStrq7BPDEzPsWvrq+7z9H9as1cLCYdFiBP
+waegqq27NioqLm+1r6uuyF8/Lzl4wrGw0mFHNnTAzb+7yt8+Ki086rWsqqu5aT83O1FwTD8v
+ISc7RtOvqKSpXS4rLT7NwLSssb5VNDE/bsSzr7XFSygpPE6/sLGtuzouLS9Zx7WmqbnMV0ZU
+SDo0Jx8tS03Fraairz8uLi83Ql+2qauvvexINCwvRcuwrbK32TEtLzf2xb+4vnhPTVDZwb66
+vs7Iu7rISjEmGh4vOseinqCpUCwrJiAsXbKjo6uvxDcsKitOvbmzrbvZdDguOkRbyr27ub/t
+Xl9VZd3Lwbm1srLNPi8gFxsvRdSpn6Kq3y8tKyIpW7SnpKeorlcuLS81Q2fAr7bFv8tfSD46
+PkvsysG/wclzSklnz760s7rFRy4sHRg70S/hnqCssNZYViccL8vjzqmgoqzcSksvISc+W9a3
+ray0bk/9PC078dzNwb65v3F1z+Pdwr7B5z5ILRgoWCMpqqS0qKe0vzkfK0ouLLqkqqilrb9N
+LSktKipYu72vqa+92UE+QTIxUPDjvbW2tra5u8djQzwzHRpCNh1Fn6q3paOtzywoPCwgOrK4
+uKajqLV+UUgqIi06P3u1qKeutbxcNjI0NDlOzb+5rauut8xdPCgaGi8nHE+irrOfnaW7SkQ+
+JSA2Uz1qrKerrauuyDktLygmNlvJt66srb7Sy2Y5OUlMTOfDubfDy8pFJygzJR8422DFqqap
+sLe1zjQxOzIrOGHbxLmuqa67vdc7LzA0OUBjxre2urq8xdlqXG77WV3Mz0s+Mi8xKCpAYF68
+q6uvsrjKTTg3NC41W+7dvLK4vbi4v9r+3lc4ND4/PVXKvLi0sLC6ytVuPjk9OjUzNkg+Lzxv
+R0TCsbSwrK235EI7LigsNj5dva+trK61vNJPR0M/REdGTmXn39LHvLe5uLW4xls9NisgJC4o
+Kl+/yLSoqayzvMJ4OjpBNC45Pz9ZzL2zsLGsrsnZ00c4OTo6PUFY2M/Et7a4uLrBzFxAOSol
+LCkjL1le0aympqWmq7PaOjEtJCYuMTv5xryvrK6ur7nE2lI+PDg1O0hUbNfLwbm2t7jD3mAz
+KjcuIitAPTvhu7Osra6oqbjcaUYzLSsuNT1Ccr+9vLeztLrAzOtbTUU/Q0xRYOjUysnOz9Hz
+W0g+QUNAQ0td6uLd2NbX3dva4/P+al9YT01PW2/mzcG+vb3AyuFdTkhBQ0lRXnTh1M7Lyc3T
+1uNmWldPS0tJSlRbZ+HW3NvY3uj5ZFpVTU1XWltqe/Tg3dTMy8zKydPc4XpaT09SWF5839jP
+zc/R3PhjWlZOTFFbX2JhaXvt4+fo3dro+XxhWlZSUlNcavnt4tTR0M3R3Obk83dzal9ZWFhi
+b3Xq3tjT0tPV2+1sZV9WVFtbW1xeX3Ds4d7c293e9W5iV1FQUFl76d/Yz9PZ19vsbF5fX1tZ
+WmBgZXPv4OHd2Njb2tfd8P56bGdfXFxhan7m3drTz9btb2NTUFVUUVBQVmV07dzV19nY4v9l
+X2ZsaXT2dW999fZ98ODb29na2Nzh4ut3Z2lmXl5mbnz06+bseW9sbmVeYFlUWFtbZH3r5enk
+3uT17+/+/vXu/Orc3uLi6Ojp9Pf2dmt7/3NpaWZjbv78/fj9+P90b2dmaGZlbG138O7s7Onm
+8X/t5ujp6Ov2fv50X2V+fPru7vn6+fl7Y2NrbGp0c//+furl6+/r93draG1xbW38+/3v6ufn
+5/L8fHN2bm5/7+nt7+3y+Xl3Z2ViX2VnaW12+/nu9fd5aXD39fDq7PDu7e3w8vTs6e/t7e/6
+8/v+/3VoX1tZYWdkbGts7uXo6vF9evr9eHN4+3pvdvbr6eTi5eXu9X51dXj7fnN7+X1vbHP2
+8/Hw/m1rbm9jYmtwbm5ud3n65d/l6uXl5fh0+vv3enb2/216+n798eTqdW5ya2lt/Xlwe/bt
+8/b6+/319vD9amhobW139v599u7y9/Hv9Hf37vd7c/99fXtoU+vcYmnh6nz5+NzoZt7oXurp
+ZGFhXmjufG91/ubs6e56cWdz+3xnXGT54uLp5Ob44tzf9+/q7fNnXVdaYmluanL3eH316OL1
+eHz1fW52em575+fr+3/k3/f7dGVpamJfaGlncvP29/Hp5e59+evt9vn19fbt6ujufn3r6nVq
+b3X5/mxjX2h8eGlvb2/36+/9/vbt8vn7bvzq+fns7+/o6Oz5eXN1a2hmYWpvd3V1dm18f/Xw
+8v97+vHwb3Bvdvz58u36eu/w/f76fP55fvjw6+ft/nFobG9vffn07e7u/HRqbX17bGloamdr
+amx48+jt+Xv99Ph98vr7/fDl5+ji5u39+vd0e25mYGZrbvv7dnl7bmhsdnJmZvnwffDw7O7x
+5u/79Orzb291+e71eWxpc/719HJucP39fv1vcHL55Ofz+/Z+eH13a2p8+H1+evXw9Pb5d2xt
+cXZzbWx0+vj56vB4+O/3/Pnt7fry731wfHxtaW93/PltaWhrdHv7/3T24+Do7O3u7/P2e3z9
+/Xt1am96dGZocHFudfZ8cXV2dG92/P1yb/z39+vk6O/w9Pr4/Hr/fntydXVwdHJ6+XFvdXd8
+8ero7PP7d3z59Ph4b21rbWdndXv88+no7vN8dXV29frx7n1xb3d3+n1xdv/y9nz78nZ2/HFt
+cXRyd3J6+vvx8ff1+PDr6+z7/P/79Xt4cG51/vr29Ph/fm1laGx97u789f74/W929PTy6Ot9
+bnBvbnd9+f1zbm97c2508Pp66ePp7PPx8/f3+n54eX57cXX1/mxvbGZteP16fHp+8/T18fP7
+7+/t/W589Xt7dPnwfHJ6fG9sfet0Z2lu/3h3ffbz+ezr8vju73X/6eXu8/b//nl2bWVhbP19
+aHT7d3R/+//26erw7n9sc/h6e3prbndycXB1e/l0be3sd3nu8H7/+O3o7P326+73f/94e/p2
+a2prcXFsffx0eHX4d2zz73v59fn17Oz0+ezk6nRfau7pdl9t5fd4e374bnf77/F7cnRuanHv
+8nLx6u/w8nj/7vlzdGt1eGR97/J6b/j8+Wxh++t/fuz09HZi7uDv8+fv9+vy8fX1/3V9b2xt
+fHlra2xkYnFvamZu8PDd72F08d/c6vLf3nv9d//+a/Xj8XJsbvHvc11bbnNpZm178vZyd/Bw
+ZXt57eP6++t7dP9ofeV5cfn47fDv9n/r3+lw9ufq5e9v7u/z+WVpZ11cXFtz7XBgZfDje2R3
+7+z6c3Hs6f/17+ff5/V8cP18amzs9Hfs5eTn/mx4+Xhob/Nzc/lv7+N+b3H5eXJvXGtuWl54
+6eZxWnDb61xn7drb921v5dzg5e/v3PBobnBydu/zfnB18/lpXF1kbPl9aWhld97i/nrn+Wlk
+ZW3v4+nZ19zd7Wdy/XxzdXn1/GFg+e318Wlda2tnc2p76On4ePt8bm9z8fN39ebnfWj14O7z
+dGp97Ot5a2z9eWr7/n/1/W165unm5uz5d/1tdvxhaejrdXRrbOr6aXn9cn5+e3Jtdn3t7fLv
+7/5ucOfe7mJl7urqbXXtdfDmeGP23u53a250c3Rtc3b+63hp+O9tam92+uzs9uTs+e99feve
+6vT7b3P5fGdx7Pl6c33ne19w3exzbWT66mtmZ/vj9mZ093dscfv093V17eXrc3Hm43Z4///3
+eHn0aWP36vJmaPnq4OptfOr3fHBs7OJ0/nFndnp2aHr6bvP2a//n62774PtuXmft7vv+dWN4
+8Pv0fm3m3n5kbOf6eWxlanTg6Gdibuzb3Op5/P1193xpZPrz7vFgY/nt9XprafPj/m1pau74
+amFv7vv2fHdubPPn8e/z7ebm/H3t7efqcWJz/WZfduzvbGVr8vR0cW1o/+Hybvz/8ej+bW/r
+7PTwZ2Vz7Od3ZWVx8ul9bHX76OZuXHvk8Hdv8eXh7P14b29u/fn9cm757/ZyaWJfa//68XV4
+7Oz3+Pj17/Du9Hn16/T9eH729PX9cWpmanN6fnVre/B+b2tmeO58burp+PD7ffTk6ujne3H2
+73p/b2Ts53n7/mVrc2FgfvV0bnR7dnPj3/5uePv592pr+X3x7ffx8fvr5vXo4ervb2lvd3Jp
+bG5scmNea/75em9mbeXm5ObxfnjveGds9vHw8ft4enp6cXX+7+Poemr06Hl+4uh9ZmJx8vlu
+ee77dfx/aWx+bnFxZWVoZmb76Pj36vHo3+52f37u5/H9++7y8Hlucnh0fHhra2p2dXVve+n3
+9/b29//7dG76/XPn2+Pu7/Lx+XRla31sYW37cm1obHj7eWt1d3n9dHPv7nv75uHo7fHw93z2
+5vR0a2ttevJ/fP7t8HR+e/vv83BmanF1dfTz8vZvb21veGducnJsbmtq/Ozj6evy9e3h5+7r
+9u/+bm/36+76cG1rbHJ1Zl9qbWlycHvu6urs7/Px8/328/386/NyeP517O17/nZsam9hXGVz
+f/d0aXz7d2x18uno5+j09Ort7On29fDx/fv3fXRpZmxw+nlsdXb7eHBtc/11b3vv6+/v8vby
+7+jte35qY2Robm5tdX59/fP+8+989uzo6vf/d3V+dX57cXD59Pj59/rz6ur0fX1+fHFse/pt
+bnv6eGJkc3hwbmduef7zdXb18Ovg6vn09f757O3t4ODy/3h9+vn4/2Vfcv9vYWdxbXx+bv70
+dmtqb/nw/3T6f/vq5evt5O367u70dWtzanF9e3L8/HZ9dP/5f2ppe3dvePX6e3p0dPTs6/P+
+9e/v7Ort9n5yfHxqa355bWv88vp2ZWlwcmx5+W9kan718/Du4OPw7ezr9nhz8/n883RtfXll
+aH72829qe/n59PR2bvrv9H5z+e3t6un79u/zem52/nx7cW12e3hvdHxtaW947vh3dHF+/W9p
+/O12ePfp4+fu7fL49ern6/l6/vb8cXJ4+vh+dnBzdG5ucnR9d3Jzb3F79+3r8PDw9vP/7/58
+b2NibHp3fnz76/R3fvz68Onr9fp+dW9oZWd49fn+/P3x7O7u6+Xf5/n8ff7++/54cW9sa3Fy
+bG53bnNpaX5ubHh8eG9ufu7q7vD3f3Z5/vPo5uvr7fHv5+rz7fTw83VwbnNteHdqaGxva3Pz
+9Xt6b3/+b25vePD58Obp6+z1cWxu/O3p6vr29XZyc21z/Hxxev5ucHZt9ujv9vXwfHf5/Xf3
++f7zenB7bm52d292+fl9b25z/H1wcH33+vXt7evp5OXk5O7s7f9uY2h99u95YV5iZWVsb2lq
+bW53efX47OLj5eft9Ovv9/Dp6fLq6/Tt/29vb3FqaWtrb3Jta311//l7dGptfH1ydH7u7PL+
+/nZ2+fXu6+Ll6en2/PT7+/Pv/mx1e3x7+Pv9fHp9b2hoeXNpcfz2+PPq93l9eGx4fGhpeHr9
+6u/+9+fi6/t9/f1xfH1+fvPn7vPu9/xwZ2pxcXR8c3J5f/Ls8Xh0d3Z4eHd87fhy+HVy9/1z
+eW5u/3d39e7v7ers9PDxeXp5fvrt5vt2e/jv8/X7fvT8bGZnaWprbG14dm59/vz9/vh6++3t
+7+/p7f77+H5+/ffu93B2+Xt++m94+vnv9v98cWttdnR5dnJwbHR59f35+e7n6vPy8nx8/PNz
+amltbGhy9PH4+fT2fn31/Pvt7/Ly9+nu+uvwdHFydv98d3Z2/HNqbm9vcHJuZl5pcm908Ojo
+5ujm6O/56+388u7+e/v/d3p+b21vbm5wd3r+/Htwcfj7+/f38/P78u708u3xe23++nv3+vhq
+YGRvfWxs9Ofq7nZ3eHZ2fvLz6fN6+u3zenZ1bmZfZmtyfHjt6uvw8u73+fPu9m989fXx7vV1
+dHVuaWZpbXp5enxufPN8ePbu6OPv+v54fH14fPx8en54bXD8entvbHt7fn75enV3c2118fd5
+fP58e3r+6ujm6Obo8fP39vvzfWpoZWNncXN2+n10bm92dGds+fju+Xt9+/Tx+u7s7Ojo7Px4
+a2p49+/v7Ojr8nRvdXhvZ2RnZmltdXf68/Ps7ezt+nx3cWxxenV6dHh8fnd67+vn+HF4+315
++PXx6+br+P3/e3d4eHV79vd9b2tveHBze3R8d3t8cG138/Lv6+33fP39d3R2evr6/n3z8O3t
+7/j7+vh9dnp5bG/v6Ot4cXt2c3Fxbmx1//99fXd7fHn3++3m6/l7dnF+cG12+vP/eXf78efg
+4unu7Oju/377e25uZl9ganl0cnR+fXBzffv89/D4/v3v8fXv9e3s8fr5enn29+r27+54c3J7
+eG92bW1xdnt4b2x5ef35/X58dXn09vfv6evq7vHz8fH8dnp+f/ru/XFub3Fud3Nyf351b3/x
+6vJ88+7z9/Ps83t3+PxrbWpgY2dzfnx3cWt1+XpzefH39O7u8e3m5OPk4ubv8f389/z2+XRq
+bWpmZWdvb3l1c29scGttbnvq4OT09+ja231QRUhf183Lz+FtYF9f8tzT1PBhT0pNXeDYfVhh
++3lv6tjNytPleWxoWVdn7t94UUdGTm3d19nVy8nJy8vO3m5eW1NYYFxQS1Voe2lfaXH8b3Bz
+aGz64ubq6f1jXHHf2d9oTk1OXO3d3NLQ19je/F5bWllfd+fp9uXPys3W63RramprdvP2+et0
+YWlvYlhdanx9c/3u2M3J0P5bUE5QWV5eZm/99HpjY2NfXVpaXF1jbW7r2dXU2dfT1t/6ZlZS
+U1dn9ubg3dve5d7W1tba29re2+hxaGBgX1xXUFJZYnhvZGRo8uDZ1Njc6nhvbW9va2NfXllb
+Z2tpbWlu+Obh6O77/Pf5d2Ribfnu8O7zZlxifuHZ1djrbWx8e3x2aXF9++/u93R69/3v6+vz
++evc2uD+XVZaantrZ11daGt+9e3l5ez6/PV/f3373tzm+2tcWmRyaWxpZmZq8+fr/3v46efq
+5e1x++5+fW9ha/3y6+/x7+7v7OLf5v1iW2Bo9u31fXP56Oh3YlxaXGBqcm1ub37p3tvkd2Vl
+aW7+8nt68ebd3N3f5evv8PdsX19kc/zv7vl4eGtmev1+c2htcvjt+P15/XF57ezi3t/j5u30
+/2xpb3J9dWx1fvvu6u16fHprY1tcXlxdXFhZaPn3dXf47eru6N/d29zf5vT36ePi5efo7n5z
+bHF6ev77cn7wcXv9e/t/cWNdWmBt9ev3b3Z0dPLw8uvi2tnf6Or4dn73+ndrbmlfXVpYWmJ2
+6vV08u3ybWh38u3k5/f46+Ph3NfPzc/U2+pjWFtgX1hYT0tJQD9OcNHBx9HcaVtgXWrp3c3K
+0+tnV197+e3o9Wv/fX3m593W2d7p+W95/enedF1gXF92am1rXl5jSUFOW+DIyc7dblhdWVZs
+cOjb7f/z49zW3N/e/Pfj4tnNyMjM32pkXmdsSz4/OjU7SXm/ur2/1lRNS058183FytjrWU5P
+VW3d3npcVmbf0srJztd9VVBXc9XKys3X63VfV1Ja5NzuSy4sNkPGtLe7wuFkUz0+UOS8uMTa
+VkZVZFNy3dzK0WtlVmbLwMTMfU9XX+TNzc3K0O1OMSYiKUO+q6qvv2A+NTU6Xb6vrLXYRTg5
+QV3YyMbHxNVqVU1d08K/y11JUWzOwsDEzt5NRlJMSjwuKS1Cx66vucxlX1lLQkfuvrS500o7
+QmzNyNzn2dvifVtd2MW/xmBKS1TlzcjHycvWe05BRD8+NyosPdKzrLfP+ExRX0pO6sW3t8tS
+Pz1U1dLX533f0dbe3t/OxcvvTEJLdczDv8PO4GRZX008NCwkKUPJrKmyxWY+PkVCYMa6sbXM
+TDg3R+bFvsPad1xUXu7OxcXO70w+P07Rvbq8xeFidNP6PDMuJSU347GmrLXDSTg4Nz3swbay
+w20/NjpO3cK9xc3abnD+5cvIyc1uR0JO3r+3usXgVEpg5E47My0kKT/Tr6itt89EOz49Tc++
+s7XMTTozPd/Jvb7fdX5dZnxxzb/HzGw/P07dvbm+x31Q69LWVzQsJB4rY7mlpLC9aDc4ODdh
+wbSstvA/NjRKz8i8xezcfFxfYOfAvtF5QDpFY8K0tr7N+F/Rz086LispJzXetaqrt85MODo/
+SOC/t7bHVT86PGzGv7zRZ3RVVHLtzr/ByeFDO0VyvbO2vcr88d1kRS8pJyAseLqoprHCXzY1
+OzpYybuwt9lGNTQ/2by5vtXlXlJdX+vEu73MTjs/T8+0sbrJ5fDb2VYyKSYfJVW+qqOuveQ5
+Mjc0Rsi4rK7JTjcwPF/LvLzN1dxfZV5mzb28yGY+PEjaubG4wtZdZOd4Ri8rJx8sXL+op7O/
+dDg3OTddw7Wss9BLOTQ++sq7vdDP5V5dXe7Hv8PQSjk/Usu2trvH+mfd6VU8KyomJDzOs6er
+ustMNTg4POS+sq68X0E5OFXPw7nE8GxaXvnmz76+ye0+OEJfwbOzutBdUm/sUzguLCgmMF27
+qqivvlg6NDY8ZsCzrbPNUjw5Q2HMvsDfZVVQYmzix8DD3UU7PkzKtrW4wNfm/GVNPjcwLCcs
+Psytqa267D83NzhH1rqusb9ZOzo7VcfAv878aHFy89XLv7/XTTo4ROm+trO6yuBvXUo7Ly8s
+JyxFxq6orbzkQDg5O0zNurCxv1g8ODxY08a+xuldWlx508m/wtxPPztAer+1sba/4UpCSV9O
+PzUpJSw80K+sr7XLVkI3NUPtu6+0v+VNRkBLdOPR0dXT2Nzh39rP0XxMPj9P3buzsrfHbEg+
+Pjo6PDQuMEDQta+0wNlZTkhGSl/MvbvF8Ec+SV7WytDieOnT0trn39TP1f5MQEjsv7i2ucz/
+VEhdT0Q8LSYpN1q2rbGzvt5lQjg6RvG9tbvGakQ/QlT/1c/RyszR2/V67NPX3nVLSFzZwbu5
+u8xeTU1JSTovKiYvUsGvrrnB0mZSRzo/V8+5tb3VWUpUdfTm+V7/2c/O3Hp53djcX0hGTOfD
+vLu8xc/bYEZESD03LCYtQ8muqq+5zFpPQzc5P12+tLe/50hIVl92Y1Vwz8O+xNr5cfXdc05J
+SmfMvbm5v8zkaG1aPS4lICc41bCtr7S7xNZKNC4wP9G3srbA0dzddVBBPEBjw7i4v8zqaVlQ
+SkRFVdnBurq8wMx1TUhDOzQsJSs7/7Srra+5zN5ZOjIxMkTRu7KwusHI8VVDNjQ8Uci3s7S9
+z2dIPTw/S9/CubS1u8XkXFZHOzMrJScuQsewq6qtuMllPzYwLS87bbmqqKqxy088My8zPFnB
+sayttc5LOzc7RlrTvri0tbzOX0s/NS8pIyYsOdGvqaapsb3aSDozLiwvOmW1qKKhq79MMCss
+MD5oy7mvra+8djwzMjtZybm2usDM3eZcQzcpISEmL2+0qqSlq7G94046LSknKTTys6Wfn6e4
+TzItLTQ/VdjCu7e4vs1nSD9FV97Hxc3iYE9NRz0yLCkpLUHKsKilp6y3x/pIOTEuLC45WL6t
+p6eqttpGNzI2PU3+3M/Ly8vM0Nne3tnQzdHfWD82LywsLS84TdC0qaWlqbHDZ0A6NjAuLzI6
+U8q2raqqrrrdSjw4OkFNX3r03dHKxcDAyMvN2O1WPS8pJCQpM03Br6qnpqirsb/mRDUuKyos
+LzhI476xrKqqrrfIaEM6Nzc8SWXdysLAv7y/yd1SPjYvLCsrLDND57yuqaeoq6+5yHxFNy8s
+KywxOkzZvrSurKyutL7UXUQ7ODk9S3PQxL+/wsrUbkU6MS0tLjI7TO/IurKura6zvM77WEtF
+Pzs4Nzc7SGnPvreyr6+zusbkUUI7ODo/SVbu0srFxcvyTDw1MC8xNz9U2cC3sK6vsre/1G9S
+SENEREJBQkNIWuXJv7u5ubq9xM3jWUlAPT1BSlZi89zQy8vYX0M4MjEzOkNd0sG5tbKxtbrC
+029SSUNAQERJTllu3s7Fvby8vsXQ6GJTTEhCQEFIUmLm1c7P0NtgST84NDM2Pk/exLmyr7Cz
+ucLTbVBFPj0+QkdTc97TysK/vry9xtZ2V0xEQD9BRE1f69TOz9bjX0pAOzg3Oj9Mb8y9t7Gv
+sba+zXtQRT8/PkBGUWTt183KxsK/v8LI0eZiT0dEQUFJWu/Tys/qVkM7NjMzNz1M68O4sK6u
+sLnF3VpHPjs6PEBJVXjdz8W/vbq3t7q9yeNcRTw6Oj1HWHvb1d5rTD00Ly4xOk/Quq+srK6z
+vMt/T0ZAPj4/Pz9BSFfixLevra2vt8f7Rz06Oz9IWvXd2utPPTUuLC4zPWfEt7CvsbO3vL/E
+zuhVPTMtKysvPu66rKalp623yV1KSUdKUE9PV1pmbk05MCkjJSs34bGqpaOor7bF3+lZPzku
+JyYoLki+raWhpKqwv9bf/mj3WEdISVDq2mQ4Jx4cHCY+waefoqSnrq+vvek7Jh4dICpJv7Gt
+rK6tqquutddFPz5M0crKxNBsaEEoHhsYGypctqCcn56eqa62QCchGxojLkG+rKuloaerr8xN
+S0dTycLHwsbMwsJjNSEWExgfMrqsp56bm5mduDsjGhsgJSxDYsapn6KjrM5oV0NM/O/Fsa2v
+sbvRYDsqIBcQFR0qxqWfm5aYm6C9Lh8dGx0lKzvEq6GepbXWT0dMU1X5vq+ppqqyvNlKLiEd
+FxIZIyzrqaCalJefrc8wJiMdHCQwWrGpqaiuvdRbPjpAYcCspqWmq7G+60UuIRsVEBckM9+r
+oZyVlp+rxzUnIx4dJjlztaeoqqu421g9NUDvvq2mpqaruc7tPiYcGBETHSg6u6SclpSbpbNK
+KyIeHSMvO9+tpaSlrs5WQDc7W8izqaSlqq297UYrHhgSEhkfKk+tnpaTl5yjt0EpHxscHiQ5
+vqujnqGqtd9LUlhez7i0r62xur99LyEaFRcaHCQ+wKicmZmZnKa5SiogHBsfKjrIrKWjpauu
+r7jGysrT1d5fbmhCODEmHyInKCsxPt+2rauopamts8VnUkc7ODtCYc/f/+Pd29fJwb6+xMnJ
+0u1kTkZDRkZBPT9DQkVIR0VLU2Dq0MjBvL7DytXjbllJPz9HW9nLx7+5t7e5vcLO/FdUVlhk
+eFxPTUU9Ozg3Oj1ASlru08fFyc3P1eVcTEdIVvnazsW/vbm4u7/F0uzl4NnU331rW09NRz49
+PTw9QkNGTmD04t7o4dnlalhVWnLt4drTy8W/vr/Gz9rf3NTV29zX0tj6WUtCP0FESE5UVFhm
+a3B9a2FeWFhYVldfZ3jf2dTMx8jMz9fk6OPk3c/MzMzR3OdlTkdEQkdPX+7XzcvN0ehnWU9M
+SEVFRUdMVGRz59XP0tDV3urs6N3Z0s/MysrM1/xmX2FeW23s2c/M0NTa6XZWSkNBQD9AREtS
+X/jf29nY3e99d2xnbHTe0M/Nzs/U3eDl4fDP0Nl+1cXT7PDRa19PS09MSkRKSktNUVdadHZ5
+evv57vH05Gpc38/d3tDR0unYw8Lc1MbGy9PGwL3L1PFZPy4pKiwpLDE/5by4s6uprbO/ZUtE
+NC8zOkBW8c65sbGyr66xucXMxspePDo6LSIfJCksLTrasKmopqSlrLtZMiwqKCYsQdK7ubKq
+p6uzsrK4wNPXz85oOzMrHx4hIiEqPeizqKein6SuuNc9LSYjKjI4UryvrKutrqyzw77Ayc7Q
+xcXYTDwpGxwgHh4pPciqo6SenKOwwk80LCUgKDtMW76tq6uwubi2wunLvsvbx7u91k00JB0f
+Hx0hL028q6ajnp+pus5VOC4nIys9TF/Fr6ywtrm5ucDZz7y8w7+8v8bkOCgfHR8fHiQ7xLCr
+pJ+fo6zEdk43KiQjLT1GTs+xqq2ysq6wusfGvr3I08zK0GE8Kh8dICIfJDq/rqumn5+krcJX
+PDMqISMvQEhiwq6prK+vrq+2wMjExdzu2vpZUTklHh8mJiIqWLawr6mjpay30kg9OzIqKTzp
+5vbIsKuxvby5uLvDzMi+u77ObGBYMx8aHScoIilyramrqaWlq7jfRDs6NiwqNujH7ny6q668
+w7y1tbvFx721udhOVk0uHhoeJiUiL9+wqqmmpKesttxBODc0Kyk0XO1c7bmtrbO4t7Kvtb7H
+vra64kNGSjEfGh0mKCUscLCpqKalqa61z0EzNDMsJy5VzNDKt6uqsLm5trW5w87NvbnQR0BF
+NSEZHCcqJSx/raiqp6Sor7zOSTEuMi4qMmfDyMezqquyt7a2uLzF0c/AvfI8OjwsHRofKisr
+Pbysq6qnqa+6v947LjE6ODI83b7Exbmvr7i8u7m4usDHxb/F8Eo9MSYeHSEnKi9Mva6rqKeq
+rrS+dDszNDUxMT30y8nAtrCwt7q6urq9xMvGv8FuPTw+LR4bIy4uKze/q6utqqituL/NRy8t
+Njo0N1jFwMO7sK61ure1u8LFx8nP325MOi8oIiAlLC8zP8uurLGxraywwfVPQDk2NjY8Uu3b
+y7yyr7G2t7S4wtX19t72Tj88PDUqJio0OjQ1Wry2vLyyrK66y9TkTTs2Nzo/R1TZvri4u7y7
+vL7E0eDp6+5wV0tJS0E3MDU+Pzs8R2Taz8/Ivrq9xMzW3mtMR0xOSkxo18vMy8O9vcDDxsfK
+ztnwX1hVSkA8PD49OztBS0pMZODXy8fNzsjL3mhfYWllXHLXz9Xc2c3Ix8nKycfGydLe6f1c
+VVFJPz08Ozk5PUFIWfPf08zLysbJ0dXX3vZpZ3fu7Hx76tvX1c7JxsTFyMvP2/VoW1FIPzo6
+PkE9O0BR/e3k29XLw8jX3drZ6V5TW3n/YWX12dnd1snAw8fHw8PM5f7q7WtSRj02NTc7Pj4/
+TejPzdPOvrm+z+Pp7WNKQUVVYF1y3s/KxsS/vsLJyczX08/X82dhVD8wLC85OjQ4WcO6vb63
+sLPA2GhPPjY1Nzo+UtLEwL24t77FvbnBz87BvMLcVVpIMSUfICs4OEe+rKmorba+0WRLOC80
+O0jpz8m7u8TU/VddYO7EtrKytba0vO1IOi0mHhsgMUvou6yoqK253khDSD48R15v0MLDy83c
+UkNGVGvWvbKsqq24wMjZVz0vKiggHSQ7+Mu5r62us8hMQUtaWmXwz8jP5mtZW1hGQ1rVwbm1
+sq+vs8Hd5dPPeT8xLCkfHSU6ZcSxsK+vuNZVTkxX4tjaz9BvXGZRTE9PXNjAu7axsrm+wd5e
+bt3Gwc9TNSYbGB80bL6tqqusuftHRElQ89/n0dH2fPNTRVFVU9rFvLOutb3F3Xv76d3Et77j
+Ry4fFxkmQ72uqaqsscpaQjtJ3MfT73xeaeFqRkRUXfnSybmtrbfG817t0NfavrW9YTcpHhkc
+K/uyq6mtsr7rSDs8St/O6l9ccN3X+VZXZ3rdyr61rrG93FJPfdPT1cm8tLxGKB4YGSvbs6yn
+rLG7TzQ1PE/TzGpX5c7Ewd1IQEtPXNnEtKutu+JLQ0/k2dnOvLCvvj8nHhkcMcivp6iyw146
+MzpK3cbHdE5l3cjEdUI+RVbZvrWtq7PUQTg8TtLBwb21rrDAQCoiGRgn/q+kpbLFWDo4PVDc
+x8PgT1ncwrvKSDY0P+q6r6+vuN5EOTdF1bq0usO9tLTKRSseGRovvqmjq8J5PzU6P1fFvsto
+SFXNurzePzI3Uci0rq+1x1E5OD94vra0u8vKurW86C8dFRcyuKehr8jsPC4vNV61r75fPkrJ
+usFrQkVTfdDFurO5zEo6P1Hry8G8t7rFxcK7tvQnGxMcxqyoqcdu/TMqM0y2qLRaPDxrv8j3
+7tTO2kQ/esK2uMzr3exYSEvaurK40ODEt7dhKSAaGkS9va2zv79GLThew7O+aG506NFsbczC
+zFw/O07WyL25vcxcPTpQ08G5trO8yMXI5DgpHxkm1r+uq7jBbDMvPvLAvszKzt54QEXYx8rW
+ZUxj4t/Twrq8y1NAR2bhy7q0tb/V00suKCAfNb25tbTB5VQ4M0/HxcnIz9LXVkJWz83R4G3/
+81BO3b+7yOFwZu1fV9S/uLrP/M9eLyglKEbN1MCwt95cQUDf31Pyxcrc3mv+1e5YdtPccVpQ
+bdzn39TP2Otva+3d2dro18PCwPYwKy4wNkXTubO6ysrL70ZAUFJPUGfCu8Xc6dziWERJYP5r
+e8rFzszQ0MtfP0pbT1nLu7e0vD4nMTYoK1G8srG6ubHBQDtGOjM9+MG6w8i5u+lKT1lPQUFh
+y83OuK+9Z0pHQTs88Le1v/V1ejIlLT5BQ+24rrPCwbrPOjM/S0xO3r69zNLEwdFnYmVNPT1T
+79nOxcPO5+flX05PZN7a6vrxdU4/Pk5eV2L7c/jXztrr4tLL2WRXUkhCU93MxL66urzEztpW
+QDw5NjU6PkVO8MW7trO3vtFRPz4/Rk1RWm3t3dPT0srHyMzY7WFOR0dKTE9b7dHMy8vS3npe
+YG1nWFJTVlFT7tDKyc/X4FxRXnnwZVNRXnNrZWbv39zY1tfX3+v7Y1plaWJ86un+aWdzePLe
+0s7Mz+L9Wk5TVExHREJETGja0NPPzcrN19jZ5v1x/e53ZmNYVlpUX+re1tPX2+hxZ2Zw5NTP
+1dbc7/9oan9jWFdKSEpKTU1KTVtccNfP0NLMyNbi2t3e9G/z6fLo4NrV4eX8WFVbZ+XVzcfL
+0d5pWlNLR0A+P0BDTl937PXh1tTPzMnJy8/V2/P6allWU1FRWvfa087O1eD7aXrp2s/MzM3R
++GleSkRCQkE9QE1SXXbw3tza0M7Pzc/R1N3a2ubn/2liXFpfZPjZ29nZ3tzZ29bR19zf/19S
+SkZBP0A/QUhNVGvk1c7KxcHDxcfN2O13cVtVWFxeZ2Z36Ovp4eHb2NbPzs/M0eDo+11SSEFA
+Pj4/QUZLUmVteOTUzcrHwsHHys7V4vj7alxdZmFidP7q7ObX19PP0c/R2eD3bl5PR0I+Pj08
+P0VJVWV95NvRzcvJxcXGxsnO1+Lp8WxqaV5cXF9fae/m4N/a0tLWz9He4PVgWE1FREI9PT5B
+R0tQXnPt1c/Mx8XCw8fIy9Pa3+x3cXZwaWZucmdt9erh2dPPztja2OxsXVZLPz5CPDo+Q0RG
+TV545c7Ew8bAvMDJyMrY5+79aV5qfWZj/3Zga+/o593PztPNzNXb5GNOTUM7PkA7Oz8/QUhU
+ZuHQysLAwL2+xcvJzufv5Hxhb3xkYXT2cm/q3d3h187U1M7R2uhlVE5IOzc+PTY7RkRAUe/8
+5ci+v8G9u8DJxsXU8uDgY1ds815XfvBhZeTb393RztfZ1tvc9FpNTUc4Nz89NDlKSz9O09Xq
+ybq9xby4v8rFxM/i2txrWVxeV1ZbX21sb93T29/OydXh0dNcTlNNQjo3Ozo2N0NXUFTXxs3O
+vrm9v7y9wcnP0Nj+XV1mV0tOWVlVctfZ29DNztHSz9LZ52JNR0w/MjY8NzI7U09HdMbFy8K7
+ury9vbzB0N3R2VtNYWtMSV1wXFzk09rUzszJy9DS0eNeTENEPjIxOzsyNElbSlTKvsLBura5
+vL2+wMjd9fVgTUxaWExPa/Xy3s/LycvIxMfP3998T0M9PTsvLjY4MzVFWl3zx7y6ubiysri+
+vr/K8mltV0pJUlJOVnbr39bSzMfK1NvOzftaVkNDPjI3PDQwOU1JQGLKycq/u7e5vby7vs3g
+1NZdVWNoXV7t5d3c9tvP2H114u1ZVmtUSlVVSUpZ92BES+BmPUNoWUZKanpccNrTycvOwr3D
+zcW9yOfTxtZdbXBp9VRFec1QSc7SRdvLPVfBUDr/40VPX0ZXzU04074+PMvISz/ext5qesy+
+zl3TusH4YcG2UD/JyU4/Wc/eS07WwlxC57zaOk27bDBMwe06Q9DVSFp63MVEScG/Tk3TwmNF
+2eNk6lxb1/ptZG/a3NzV8HW60zzMtmE63L1YQVZs1FpESsVjTEvMxlZf+cR+TE7dwkNV4nLm
+SUvwv1I7zsJyTEjEwkp83dzQa0/RyUZmvllEzsNNS8jiTFHGW21h5V3y1F1d28pO3tp+/Nlq
+al5y4EXt2UlQ1E9U0/A5z787TbVkPbzaS8FzX9/Ke1DOztY6usc69b1ZNMDHPk68VD282jra
+rzU7q0Iuu7wsXLFEObRXM63EKtOoOzPKvmE+zFFfvT5HxcFHTc7CaFLC21DO52fSVXnGTEi/
+TU27OubKTEjY0j9wyk5V1OZCy9FLdNlwdOlJfM73RNbMbnLg0mzJ6kK52Dbfsk85xsVDTVa7
+Qz2+/VppSmKtLUKwwzRvw37TOtDMvjRMrPMuzbZLP8xv98NBPrTQLeuqOTKsaja8X0HK0zfP
+xU9LcrE8P7H/N7FVNaxlM7fSPtVPwts0xcs90k5Csvstt7oxzcw1sE84tlo9u01SvD7VvTlu
+sjpPsztuvUZFuGM8w3pLymJJ4MRYOrhsUHlr2VldvT3ovT/P6XJf3dJMYNvEM9e9RT+xOkqw
+PUXFyTzM2VDJTM1P2NVIyNw9vT7KS1rTTbw+TMC+Me/MvD9Dskhjwjrmrzg5vbg0OK/GLc26
+OM3HNtq1Rzy9uzlOvtY/4H3x31RPyuNSS8rCNtLNSWPNTtvMP+rU7FNgd8PmQ2Cz8zHEwGY7
++b1Jbc49wb01Pq7LL2W/bULXU0u6ZzfCtTlgvXptVNnPSlrHR33DO9rN8T/NwUM/sU00r0FK
+wWI4xbo3V7ROU3jLbP3q4+/fzED5ymBIzU7FSUjNzj7GekK9TltexNg42q83PK9HRrpNO7jG
+Okq6xThOtE9PzT9Zv1s3zLw/bNBYu0Vvulpdxl5my0p24HjcQdjPPGnVUONC5b8+XrxKY9NI
+zNVWWv3FVmLpwUdywlPZ/Va/R2VFat7I8kbB10D2zlDyRMzYTOB47MdPV8TbW17LUOBgVOdi
+1UPkzVJO59R0UcxLzdRCYb5gTFXfyEzcTtXHXkPJvl5QWcRb/kHLvjlNxOVAwU1Ju1Y/ztft
+SH3DTM9HzNVSXW7PWdNJ97g67749zcw7W71jO8fWQrtPMq3HKby6K8a2MUusXDS3vjLbszhA
+tto12LpS/NzJTL/8Qs/I50zs2ttnWl7XU0hCOVFANztWYk/W78y0vOW2s8LDvsDLuuvZxNbV
+47vCNCU57SUdKjs0LzJEtqvE1Kejtry7ycDLPjdS1ExtwbKzrqyuq6+6OCgpIxsYGR4mLzdV
+rKKioqOdobHKYtw8KCksNj9Dx7GkoaOfoKWtfCoiHBkXEhUfKzV+rp2ZmJqdnqXBNygkJB4d
+JjzYt6qhmpeanZ+ksT0gGxYRERIRGy9IwaKXlJKSmqOr8SobFhcaHyQys56cmpaTlJqiqrc+
+HRYUDw0PFBgo6bOhlZCRkZahtUomGhMTFx0oW7Cfl5SWlpeao7C9VCAWGRIOERMXKFbEqpiT
+lJSXobDRKhoXFxgcKUS1npmZmJaanqOwzOU7HRcaFhMVFBw47sOom5aWl52ns1grHRgaHSQw
+X66em5ubnJ6jr7/PzkciHB8XFRgXHCtUzq6dmJiXmqOrvy8fHRoaHy1AvaSfnZqdoaKpucrA
+zy4hJxwRFhgYHzNKwJ6ZmpaVm6SvSyEdGhYZJDd3rqGfm5mepKWsvMTMTSYiJBYSFhkdJTvX
+qpmXl5SVnKnJKxwaFxUZJ0PCqKCcmJqfpqixw9ldPyohIBgUGBsfJz2+ppmXl5WWnrFZJxoY
+FBQbK1i7ppyZl5mfpai4+UtHPCYfJRsUGiEhKFW9p5mYmpaWobpWJRkYFBMcLU65oJqYlZig
+pKrHUUI6NS0iIiIdHyEfLUtus6Kdm5ibpa7KKh0cFxYfKjq6pqCbmJueoq3GbUU4OjgyMioh
+JCEdJS0ydLCqopycoKOtXS8oHxkdJCg+wLGknJ2en6awus9LSmVPRj0rKCYgHh4lLzhlt6ui
+n6KlqLPPOy0nISUnLUnXu6unpqOmq6yzws3e4v1hQC0wMSQjJCUuLyw80MG4sa2nqbW7usPi
+emld3txjbFM+QUE5aVs/vbbZv6u3xrS52NDpOTI5LyovNDM/WGF2xsTHvr/Lycl3W11KSEQ/
+SGLu4Mq/u7SysK6vtLzMeUo+OTIzODw9P0pWWFhZX/xfU1VWWV9bWVplb15aZuvc3c/Aure1
+tba5v89pTklGRUtTfNjLyMvIyutRSUI9OjIvOTs6Q0VJdGxW2svKw9y7rc3DtV9J09BD7MpW
+1bm/ua23yM7FxVs82MctJkY1JCkiKFQwKNqyuq6uq52mw76/YD0qKDo8L0a5rqqrq6SowtzH
+9D0tLj45Lzjnv8k5JEJRICU7SddrPbWn4fO1t7K/6bWu3VPSzedTX7uzxcKxsr7P4XpHMTEz
+LDU1OMrGd9U7MlcsHzA5MT9G1q2vuaehqK20uL5dPkdXR0favbW4u7C44k5OQjI0PDtCRjlB
+31IyMjk2KiUvVW9btaanrK+srMNkzcTeSk7Hv33dvb3I08C+Z0ZPOj47MDpOQjlFMy8+MS5Y
+YlW7u72xuL62u8K7wsvSZ37O/uHIxcXb8NzwT0heY17tRT5BNzAtLTphSV/X187ZdsrEzsK3
+uL29vL3T1MHG61rgcEFOSVu+y0dOxfw4Lzs/MzU8ec1LPtTWQEPdvcLLvLC4zsW6vr66wMb8
+T+reTj9rvz0w59VUPzNQVCEqX0I+/cK3yTtYvMtW0a6wz9S4ssdguq/ITU7j6TY+t7xLQbvI
+NDc/My8wLT7nS1XKb1fR12Taw8XJzL+3wsy8vsnka91UWdLOwtfNustY63k5NTArPC8qUfVW
+2+xVblJK08LPur7Lu7/azbvM3szH0mh7zr3Ebsa3zUdO7UIwL0s3JC5dPzE85LjJSc6xvkxG
+yLnfY8CyutvWwMZtcc3D38+9vcnUv9A9OUEsLT09Oz5t5TsvOD5OSV21rb7szsDPX9K2sr/L
+v77QW9y7wFtvz9dN/c1cTFtCSfYsJjssIzlMX73PS7u192/Rv7fF1721x+vSxMHNz766ynXa
+1NjX7tDVU2tOb1wUDjpWJECnnp9iJs7FJSW8o6y9zLWzQDm9r89NVdzSTWqyqKu2wmxFOCxJ
+z97CuKwzDRQ4JiraqZmfLiu+/ikpXKKfzmausEs8VMO1XjzKt8bBsquux1poRy47V3m7rr0V
+DCE6LS+xm5m6Jz26Rio5va27zca4utTZ2FdPXU1c0riurqyvxlhDOj1BPr+qsrIfBxM9NDu3
+m5amJiDoxC8rvaSpvi5DrLx26sa73TY6xLGur62sv040OWBOV724vLM1CQgnVt+0npeaShsq
+8/I7/aiepkYqPnXk6My3t946Qta1rq+uvVlDPEblyrqxx87FIggJKcimpKCZnzwdJjj0zcGn
+oa1hKStF5t7gvrjIPzzBqaiwyFlQQkFgx7OvvOxTQikSDh3+rqOkqqW8LikvPOG5rqy370Qt
+LGW5tsFfYm9LbLGlp7pGOkNDQ2++rKvVRGBaKBEPIsCqp6ulptkrJCxLtauutdFNQCssxqmu
+yEg0PmDMr6mqs3gzLTNPx7qxq633Mk1KGhIbO7ajo66mui4lKjvIrrW5u089PTJRr6/LTzo9
++cy7ramwbzAsN13Fuq+stddBOsS6Hw8VL7mjpq6or00nJC1tsq+xvNToQC89zLzXTkJqvb7E
+vLbGVj45YsTHw7u7vs9KX7q4SxsOHFW6qKmnp7woHiYwwKqrr73NSDEsOM2+vsDCwM7wUHjN
+v7jC3mrWyfdvysG/vtlUVVEtExUvwKerucnKPyg0Tb6srr7T6DUsPcWvsbnCyN88NEy9rq21
+y9t3RD9E/cK88UtuV+fW1ugmGSM5/snDv7C0QDE6UMjDcVm3tPpPWMuzuNfjx9T8WE/RurfG
+2tXsXUU6PuvF0GNZduDlUCcfLztAS0XbsrJ9WdDv1M/j3MG4zuXm2bq2u83ldU1IQVfJvL3P
+8FhKRjtBT0tKQUZWzLu/yHpMPi0pNXTDvMDd5NtdSVjLvLrGefjU19fMxL/M715VUFFdaevU
+zNdfRkNJQkRi3crG21pFPz1GVfXGvsDOdlVKTEtU3Ma8v8xnR0hMaNDKztPdd09QWmb119He
+bVRHSWbdw73Ez91cPjxFS1/T0dzY2+xqWVZce//UxMXH1VxPT0xU79/Y3VVJSExX9tbZ53hf
+XHx4687Nx83fZUtHTXTNys7h6f1tbPnga1hSVuPV0djqaFRRVn7k3Ohw7e5gWuttT0tb/ODd
+68/N+F5ufeL3U2jk7OXsan7+e95/4dr+XV9r7Mrc5t9lWWFhauDgeOlsT1JOTmjaycvT721Z
+UUxP3NXe4fx+/uhr7NxmWGRhXfve7NrW9Xp44G9hefzW2vD53eRpfmVTTVJY7dXf187rUlZm
+ZXn96915VFNiaujm3+v76u3689bi43RWZWz+/X5+5n1cbWds4tnp3N1eXmJgW//ebGnwXFjs
+597r7f9q+9vra+DZaVHe0ktPwH5O7d3S637a2Uvf6Uv1xVRE18r9Pk3GSS/OvT8817h8Q87B
+4V5f1MJjQ+PGbEnpzu5b6Nfkanfc62nr5GxeXe3sXmp6Z2daV2lub/v7d21rc/Pq49Hfbf7o
+eWt9fPL48+5lWnLr6uPd2e9menNtb/BqXW90bGtnaXVn9uN7d/NvfOzi4Ors5e5z9n9reep3
+cu7u6+9ze3Vn8+9r8+5vb2hzdWNpbXBgYXFr+OXm7PLm5WZ64/Lk29ve/nP7e/Tz8nV77WRi
+aWt5fXpvbm3272ZrfW9+eH338fb79u72fH1sYWJ66u/u7/N9cO3i7u7q59/n8OtxZ3N1fnVt
+eHloanh1bHTu7nF38u9zdurscXvzdm97c2Jfbuzv5t7id2n8/nzy5vZ1b2psbn7z7fT6dWt3
+9P3z3dvh93x1dmhvfW9uam9qYGf/dmppbG979G987Ovy7OHtdXb5+fvr3+Tv7er6bnV7cW/6
+7n56/W5oaWZnZGZkZm14enX28vHq6vP59PZ+dPTf5O3t7+7s7nt2dm5ybm5uen5tb//7bm19
+fG1ybGpwcPDu/v718O7z8fDwfHr+b2dsc3r2/fjxfW/48Hx4eHlxbW5zd3N88PV/ffPr8PPq
+6Pb9+vX2dfXvdWx0em9rbXv++u3+bGlpam5xcn39fXR9fVPazErO7Wv8TFvDxVd7v9pkXlvq
+TF5r92R0aVzadu3NUV3d7V5ay1fiaFrYWdxjzdTg7vHdZWTVTvfuU+JgZmHldV7rb+ZX9V7w
+atVlbNtX2ljaXuja7dbq3eN53frrau7hXtlW6ltwflXSXFj3Zmrmal547WZS1GVmz03d/O7Y
+XdJo1Ovu6dZgyWLY4FrMRcdMW+tNclBWT1NRUFx0Wf5q/Nfm19rV5MjrzdXgzOTR2drN2dTc
+3cr+y194RDkxLzgvNzhD5crBubOysbrEz/1GPzg5PEFIWtzKu7y6vL/Gytz1XFVfXnN+09XF
+vdDeXDkvLiwsLzM7X86/tK+ura+6w/ZKPjYyMTQ4RvvPvbWxrrG5vNRtT0ZFTVtZeNfLwbu9
+wkw+MiksKikuOUfVvLiuq62us8POYD87NTAzNjxR1MS5srOwtLvF2F9OR0JKTld35dTEx8nF
+yc7VSjAwMC4uLS8/Xu3Fvbeur7S3wddzRz4/PkFJUXrNwL67vcC/ytdyWU1PSENYXGjU6uHF
+5N/ecu9bTU9nZGRFPkdHSkdJW9/m4OTYztPb5O/87dzh4tTd08/azd3M5Nbb5td0eOpw92bz
+fW7sR/FPbF1ba179Tl1YX2hd/2FfXl9ccmh8cu5t/Xhz+G/d49PZzc/wzezd017ddOtb4Oti
+cWhg33F43+zcdtxX3u5k3VnmXG5RZmRbX1jfWGv9a2L+UnFYb2li1XXN7NjY3c5s0nXvX2T7
+Xlt8X2ziedPf0urU5e9gaW9lcV1iam9n7Hnr5v3xdXL+6PpeXmNYYVxj4Fzy6eXu99xwZHp/
++3JrXen06OX83X3ieOTv3O1ye9/nbvBv+Ohpfe1sZWx3bG/t8t/rfeRsdl1scPxXXV1fXFlt
+X+tqdPze6+Ll4+fq2urgbNjf6uHl/ObY5959/3Z3X1xdXF1oc/h37Ofo8v37aGZaWlNdX2lb
+aXzo4tza2ubb5XlwavZ7eW/v8d3z5tzv/efqevlz/HJseGxsb2fqfWXi8nVt42N3bmtdanRx
+cv19/+/r4N/vbfXsZF9ranhtb/fq7uf+//N3eP30YHL5f/nu+Nrl3njz7Gr8+3xmdGzreWfr
+7+Zt423u8t7r/Wx0X2hdaGR6aGvmZFx4ZGzteH7e7m32b+jg9uDc9XXq9O7lavx4a3tkent2
+4X7429/m73Dwc/xda1pfa21lbG559X5l6ffwb+7/YOR17N/u+f7n6eF87njf/W93b/5ecGf9
+dnd05mr+ZHJv+tx07uj472Bz6m586/FufXtb5nZqfPVf9n9eed9+d+Fv6uR0ZebjbPTh2//f
+8ej3/WFqalp6XWT5V/hlY17m7Pxy+uvn7mHe9ORz6edz4ftwdfHn9Whv7n7qbXbj8Gru/O5t
+aHFibfX+bfTranf/6P9ofmpt7HVlbn3xbvx9+ufq7Pni+3399O7oZd1iZu593+ft4ejtYO9i
+c2xmcup6YGh8Zv3u9N7xaW35Y2jwYffrfF98XeP1ddzlduvX9Pj4cOB24PPp9HJd517mV938
+7vlofWbPXu5e32Fq+1tnd1lo4Ptr6ubve+vu6fn9+H3i+GPreXB87mb++/ni//zp713ha2R5
+aGvtfHxj9u5c2mXZbdj9cNpl7mLk9Wj+4llzfWnaad9efmXrZm1/W+Zt6/b3X+3vcHp+9vHf
+e2948nlpdWfkW+J37OfudW3peN3tXt7j32BPzW7ezu7O32/tc2V3SV1jSmNWXUvs4O3S5NNj
+VmRYVeNy9Nzo19vU2N3U6+7OafT4S9dX9t1qX/d8dPlmYV9eVf5cZWJcfWFj5W395Pv97+fb
+6/v55eLe2tba2M3i6cvVdO3PXHpgRj86OjpGTF5u7dnXysPBvsPN22xXT0tNT1Vdb+fb1s/M
+zMrOxc3Ozd3Q3+NnTjw0MC4xN0JR5NDGvLmzsrS6yPRIPTo2OTo/SlPlyr25uLi8v8PL0dvk
+4+Pb62ZPPDAtKiwyPlbXxr24tbSys7vGaUA4NDQ2O0FOZtzJu7SwsLW7yM3lcW9ibPzucVk/
+NC0qKS04U8+8uLWzs7Ozt8HxQzYwMTQ8SFR918i+uba4u8LM193f3+bg0MfDyeU+KyQhISg1
+cbqvrK2tr7O4wvFANC0sNET2ysbHycjEwsPIz93o7tzMycjIxsG8w+09IxsZGyZTraKeoqmw
+vMlfOSslJTBkt6inr9JANDY/VN/Kvrm1uL7P5OTXy8PCyNBSOSobGBslXaadnJ+ux145LSon
+LEy6p6KnuVAuJyowRsq0q6mtudZMRVzQxcjRzsKzrcEsFA0QH76blpuir7/MOyIaGyyznJul
+5DMwNzUxNkW5pKGpxj88asrC1ExH5Lisqa1LIBUOFSjCn5ean6TIOSUaGirRo5qgtEkwMDMv
+LT++qaOryFJKa8zJ6W9qesq4r6urfioaDQ8jxZ6Vm6Wp1TQkGho0r5yYpO0vJy08OTjxr6ur
+xUhP68K8v+dwY1nlwbewqrM0HxAMHPKjmZioqrsvIRwcOqabmqVLKiYpP1NXzrCvutk9UM23
+r7PMRTk1RMesp6qt0CgYDQ8orJuWm6mzOh4ZHS+pmJqj3ikiJDBWx7murclGPEXIsK2xxD0t
+MEe9qaWmrMI/Hw4KFWGck5ais14iGRsrsJiZpNMtJSMoSb+sqLD5NDBCva2orME8KSpJuqqm
+qKmz5ioYDAshpJWUnLdkMx0ZJmehmJ67PCkjJz/Eq6e8SDEuSbWrp6i9PiglObyuqqenq8NH
+KRcNDCudk5eizkc2HhwsxJ6ZpXAwKict6rWprFYuLTbPq6enre0uIydTrKWlqK+9fEAuHA0M
+LZqVmqhqSkAfGy6/n5mmUDQsKC/ZtqqxOSowV7OmqKy4RCgjL72lpKuwudBxSS8aDQ9fmZic
+rFVGMxscObKdmqpGNComM8irqcMrKEHEraerr8QwISdYqp+mtr3ISFTNOyobDhyrnp6hyzVP
+LB4qz6ianMUuKSYr1LK1tVEzVMvCr7C+3zwsNc2vqK7I2dBXW7jCOB8QEUOhnZ2rU0c5Hx9D
+rp2arDUmJSdLtrazzj1T1uS+srzGdjc43bqytMjYzGVSrqxIKxQLHqmenJ/XO1QnGiy7oZid
+TSMnJjS7s7ayyUlBQvOxrLnoSkFa08vIxcXI10vKpbctGw4RRZ2do6p/QDkhHT6onZyrNCMo
+L0nGtLKxwToxTryztL7a1dlORF/Kt7TJb7uqwCweFRAlrp+fpck5OyskN7iooaXPLSkuOdy0
+tLa7QjFKxrautMvW70E+WdO5sLzNu7b7Kh0XEyatnp+lzTM4MSo7xK+mpL0xKysu27Kvr8E8
+MkXpuqyuusP5OzU/4rOss7e6zjMjHRAXvqCenq8vL0EtNdnNu6KkXzAuKDS7rrS2zT0/VV/E
+rKuxvkUuNEbyvrGztMUyJly7KBchPM2lp8tj2lVS00k437W/0947LWavu8rTU27F907Btb/E
+w9plXEg/XNfX1uRSVM/Ju7gzGhwkL9evrKipwEE1LzBIxrSvtddGQ1D/z8C6t7jTSUBEYMe+
+vsHQW0k/PEBM4M3o9W5STEpGSti6veZJPDMwOEFgu6+2wNZPP0NSctXAubzI401IWvTY1exx
+2s3W2Nrn5+bf51ZGRD9CWNjFwMlkQDo9R1TlzMbH1l5OXODW33RfT1T229bW3vt3ZV5s6dbL
+ytXzWVvj09prSURT7tDIx8/vTj06QWHSwr/J61RKSlN529ngc2Vw6tvT0eZfWl9fbeTc4uz1
+8tvZfU9CPURk0cW/xtthSD0+S/rFubnC02NHQEVV9NbNyczkWU1MU2js29Xd7XdaT1Nj6tXS
+411KQkZc08G9wNZOPjs/T/DJvr/N+VVNTl16993Oy83Q61lUYGZr7OPo6XtVT1vy19LhWktG
+R1nt08rM311OSUVIZdTFwsrgXlJZ7NXW2+X9euzsePvt9W5t9+bc5HFgW1xfZWpmX11dZezd
+3OZ7bGVhXmVlcOfc2Nv8Xl5v7uDZ3Pt3cvve2tzobV5bXm546+V9fOzqeGtfWl984t7e521k
+Yl9eYW5++Pt3feni497h73FlX2j9+u/e3ebufGVudHTz7Ozr6Pjs5uxpXV5gdfHr5+r6alxY
+XGvq3+P8amZm+ufq6u3+bHPs397e3/ZpZmNkanJ4/e/s/m9qZGBue29y+e97dvr38PHx7/jv
+5ebz/Pj17OnueGJjY2j07Ovt+21oZWVtbnBwbXn6+nZv/PTv7+/u/m9sa3B+9ujj6e3w/3Js
+b3v08u/xaGNxe2568urw+Hdud/nr7P74em5uZm735+zuc2RmZV9eX2D/6/x67Of689zb3+Ln
+5OZrYHP/9fb2/Hh5+XhlZfrwcG/v7fd/cmxiY3Xsem9qfuzx9vv/bfFwcOjpfHH359/r8OXv
+6P5q9t/m9vrt4X1z/fVxX2JnX2z7cP5jXFdfWVtkeOfl5uzwfOHY19jb2eHp6+3l5HNmbF9X
+X2hgbn77+Ovx7O7+/vbq6fR+b3V6fXB7/Gr6fm549nZ6+Hv673lk6uRlZv17c+99fPh2cHX0
+8vvu7erv9/z49Pfs9Xv6f/ttdPZ/+v9zZWpqdPj76O97d3F4+n707PT7fnXz6/jr7+zvdm5r
+dnn48u3+b2xmZG33+fNvcW739W9xefl9cfZ+7+ju8/Ht//p++u7s7+7v+/T18vR593ltcmt8
+dWxsamxuanB4d2tt/35+8/B7c29re+zm4eT48/X69errd/x8dn/9+fDs+W5oZWNlbm33e25+
+e/1+8/jy8PT5fHV+fnX5e3zv8+38fnJvcm5sZG5ycHdxeuXx7+nu6unq7ejo5Ovh19vf3+xu
+X1NLREZHSU5VZvjf3NXT1tba29zf5Ovy7fLn28/JyMnLyMbIzdldRj44KycrMj1P4b2tq661
+v9BfPTMyOEZc8cq4srS8z2pQSD09SfjNxcTAvMHReu3V1/nlwr7HWzkvJSAqLC1IzrClqa2q
+tc5ILSwzNDtW0bOsrrO70ls+NDc+U9zJv7q6xddrXPTg7NzNx8LKUT40JR8rLjdnx62kqK2u
+wWU3KSsyNT/1vaysr7fE9EEzNTtH3M2/uLi9zGZlfWLp4NzMxb++0GI8LyAbKS40dr6pn6Ww
+rb9ONCYpMzI+yrOoqK+1vk82Ly46Rl/Kt7Kzu8ziV0NDTVrs1sC5urq71TstHhsnKy3ytKWf
+p66qvkUwKC0zMD7Ita2ttLjLRjcwMj9J+MK2sra+xttYSkBOXnDUwb++vsTMTkw9KyEgLjg9
+9bKnqLK+vd45LzQ/SEfuvLS3ur/L5UM8RVhnacy5t7/O0dBjRkdfc1ho0sPK8c3NXFpd4843
+HiM7KitEuqSszriu3jgyTsxbR8evt7/AvcRaRFN2UU1r0MnV0sC/3E5ERz86QebGzsnG3llH
+Wuxy3MXC/Uo2Ki4wMUhrvrS7x8/kWkpL2MPBu7i7xmxm8Vhtct/X33Xfz83IzsXKX0VKPj5F
+Q9HPTnXYbHBNTuJKOn7d38ZVSUY8NzZP32bcu8nFyNS6vMzBwtrkbOrS6uvN2dvPX39uT3Bi
+VFpTUFFZbXZ81OBabGVSRUJZXE/+zN3v6ks9MjA/WH3CubS0vcnSfF37etfQzsfTz8vJ0elZ
+SFRPRX3Wyszc2l1RTD5ATUtNVG1n9M3Lv8rV20MxNjUyM0PEury6tsRuTUxeV3DHub7Ev77J
+823e0m1adWVUWWbez9Tx73lOPjk8SlRayr/O4OZeYl1GUVg+QGhMPEvVw8zd2tJVQEfsyMK9
+uLPE1dJvS01+2NTbzcXSc3zn815TaF5OWU5KWWdpXeB7T01lUUhlTE5XVVxYZ2FrTUdJSF3i
+3Ma8u73HwMTS+8/I4tXLydPi0c/vbV9UVExLXFxU/O5dTEdESkRK1dHj4PTlU0Fc31FR2GRb
+TzxDS0Nzvb+7uMra3m/q08/Dws7Q197T1c/L1mZOUFBHUtfPb1lgVEE8UeFm6OZm30hAWlV3
+1m50YEA9RF3c5dTS3O9NYdDHwsS/wN1adPXXycS8wetfWVFdZ23OyN9+XlJYRkF4205RYUxR
+Tl/ZX9fmSVdHP09zZ1JTS1h+TWrQ0s3LycXAysvGy83X3eFp+9jUyszN0ef1XU1KTVZ5X1vg
+8kRIZltNTd52UD9L4dxfQ2NJMzpMXunky7a82dLJy8/ezb/MztHfzeZnzdHezdDg9V1ZZGFt
++lpLVfFXQ+rSTlRPS1ROTm0/N0s8N0dc5OvnxcHN0s/QyM7VwsfOycrIys/Pytbo39bM2dnO
+9GlrUUlSREVeTnlUSldnTUs9Kj89LzzowsLL3LvGT0/hytTWv7rEzMnBxdv41dv9YN3I0sXC
+xMx3V11GNU//Tk1r2drQzyofUiwiSMm2scrNrNo1YV9gUE6+utjPvbrEZezD/Eda4Mfa5bm1
+w8/X1F89SHpST9zHvjkcJzwoKE2zqa/UurBmNTx22FtfxL3fTujBz1BwvsT8XuLN6FzbwMPK
+w8PQ615t0t5PYt06IB4sMC5Iuaels8C2zTozOlFtXtm5tcNZSlxLQ1rpzsLTy7zH09Dk7Nlg
+XMnMzr3Gvb1SOi0nIiIsOma8rauttM57SjY2PlnTx727vsbaTj1KT0rmyr+7v8nK21daWmHf
+1tHGwszGvOE1LS8oJicx47u5samuv3tBPj05P9m9ubvCwc5KNz5eWWXOu7bA4/LaXEthzb/J
+5M/Axs7OQzc5KCMlKkS+v7WkqbXKRzw/NjRiwbqzusvOYTowNFLp7MCysr3fVPbwSVDavrvP
+48e9x9xbPy4nHx8xRHu2qaaosW5KOS0xQG29srm4uGFLRzU2T9jPu7q6vOJQV1xTbdvDvMnU
+xb++xEgzLyYcHS9YwrKpoqK0Rjo2Ly40X7Stt7i6zmo9MThe2NHBubG52FdYXE1N+8m+xdXF
+u8LPcDUtJxsaLUpwsKagnq1NQjwsKzFJt6y2trS7zUYwM01OTdu7sLTM5c5/RUJP0cPa3b+6
+v8DIPysnGxopOE+zpqGep9ZaPSsqLTjNs7awrbe93Tc0OTxDV9G1rrjCx9N+SkBO5djh2L+3
+ubzKOCsiGBwqLkivpZ2bp7rBQionJy1Ie9iyqqqque1SRTIuND7nwLuyra+5zldMTT07T93F
+uLa65T0pGx8nIy72uaWepaemtNtLLikrKSg0W7+pqKyoqrvrPC42OTNDzry0tsO/wl5IQj5M
+UExZXlZnVkBIW05OVUxU3+Hgxb6+vsx0UklIUVpt2MzHxc3Z52tdUk9aanPt5N7X4nP1enTn
+e2p1aVxhau7SzcvL2XxVR0hVU05dWEtKRUVbfXDh1tXLz+HW2N3R0dLP1+Tm/lpjXlVofN7U
+297vaWRrbfrXzs7LzuNmSj9JTz47QkNJW2H1y8PJzNLe2+lfZmVk6uhmYm/24urz29bbzcvb
+1c53TlDs0eLawb7Ey87M1WlCLCQxPiooQc26r7Czrq2z5z4+PDMxLzJQy8nFvLKtsMHc6vpQ
+PDlDWvTp0ruytLzDwsPRYkc7NiwhIS87Mj3Lsayrq661u8VSNS8xMzEyQsm3tbOxrq+931hI
+Pjs4OkZm1sa8tbO4wcrK0lE7NzQpIChHSzhMtqqssrCutsfcTjYvMjgzMUfDu8O/tK+4y+f0
+V0I9P0RL/M3Hv7WutMLCu8o+MDMtIB0qSEE536ukqq+sqrpoTD8vKCo4PjpNu62xuravtNBU
+UU0/Oj5KUlrcwry6uLm9wcXaSDcyKyAfK0JDPX+toaWtrKu13z82LigpM0BHab2tq7O7uLnP
+SkBGRjw8T+/8/cm4t77Ewb6+z0s3MCkhISw4OT7Rq6KkqaqsuHE6MSwmJi5GdNS7rKitt7u9
+00s8PT88PE3r4+rNurO5xsi/v8xTOS8nHyErNTg92qyfoaiqq7TeOy8rJiUsP2/Uv6+nqLG9
+v8pdPTg5OTtGYfxx2byytL7Nyr/A2k06LSQgJzM3NUDAp5+jqauuvVg5MSokJS9N2c7Cr6io
+ssjW3Fc9NzY5QVTq2tbGt6+yvcnIw8TQVzcpIiEnMTc4R8SqoaOpr7nFYzwvKCMnN3fKyb6u
+pqevxvlgTzw0NDpDVPDNw7y3tba8yuDk3ORdPi4kHyU2SEM+c66goKiyvcl9RTQqIyQwar68
+vLGopazCUkdDOjc7QUZKdMG3ub2/vb3J4/x4X11RPiwfHi1QXD5BvqOfpa66yWVAOTEpJSpI
+uq6yt6+qqrhVNjI0OUBKTVTjvLG1w9rUxMTYXlhl6+ZpQCsfHy9YVz9Lt6Kgp6+850E4OTQr
+Ji7Vraqvt7Swt9g+LywyRuTX7d7AtbS9311l7dzX2u3i08zcTTEhHSY+Y1ZYwKihpq28VzYw
+NTkwLDjNraaorrnO/nNKMCgrRcK5vsC/wcbM6kY7RNW/wszEuLfA20UnGRkpTlVKyamfoqaq
+yy8mLTgzLDXes6mioq5jOj9INCorOeK2rK2829zPbj86RfzKvLW2u727wWk3JxsXID/ZzLWn
+oqWprtcrICQrMT7auK6noaW2VzYuLjI7RVHMsKqvwOxLOzc9THLZyLuzsLa/ydhQNysiGxwu
+3rqroaStsrjnMiclJStNu6+sqamuvO09LSktOXa8s7a6u8LqTD0zN1XEuru8v8LBv8TYakQ1
+MC8nHydG1LCgpLS/zUo4MismLFS3qaGjrr/jPjMxLi463rWurbbWWVFIPj5ATtS8tre9ytvX
+0NbncVREODEoHypA5a2jq7vKTDU3NS0vR8OtoKCrv1AxLTE0OU/RvK6qr8VOOTQ6S1/73cy9
+ub3I4F5Z88zO5X1eRz0wHx8yRruiprjMSjY8QDc5W8OvpKSx1z8tKzREbMW5trK0y04/NTE/
+b+DFvsbIxsvW2XZUVWn53cvAv9ZKNiEaJzjjp6Suttc/PD06OEnZvq6or8dVNDA/asu7u83b
+5WNSTEtMTe/JycveX3bn383P4/X24tHDvcHTSTIrHhwtP7ygoqmxVTExLzJG2r6wq6y11EQ0
+MzxQ2MC+xsvO0eVWXFtNW15MTVXyxru3ucf8VEpO7s3CxN5YPS8lHSQ31qSepbFLKCguPsm1
+sq2us7v2Oi0tOPK2rq+72U1ER0NOY13d409JTmnJuLO40UQ4OELnwbq4vM5YRj0pHB4tW6ec
+n6xdKScxSsa0t7y5vc1mPzY8ZsGxsr/uPzpEWOTW4uv9bllGP0T9u6+yxEs1MDztuq+vutNT
+OS4sKCIr/rajn6/mNScrRcy0rbW/yupoT0VHWNG9ur3RT0FATvHY0GFHSU9h9W7bxMPI9EA6
+OkbOubS5ylZEQEVX+dLKy0EoKi08uKussM89OUBP08LKyMfOz/9KSFjVvrvJXj85P17OvLzL
+Sjg5PEnWv7u6v9ZgRz5L+Mu8uLvFTTUsHh431aedpr4/KSg5a8y6vcG7v8rtSD5J2cC6vexD
+Oz9qxbzA3kY3NjtM0762tcL+QTY2P2vIv7/I3V9XWldjeGnx3eTTy9VwNiUoMV2up6u130FA
+TVPpz8u+vsx2ST9HYtzHx95oUE1RYOxoTv7P29ZbP0hf2L68yNbpdu/5X2b80cDFUikeJz22
+o6i8TDI1W8jHyNnaxMvZVj5J78C6wOtCPD5P1cjFxc3jeU89ODxlvbS52UM6PlvLwMbbX1VP
+TVVj3cbBzOxLPkpv2thMLi5A7rSttsduR0pk99/P0MzTZ01MWN/GyORaSElj3tTT4GddSUBO
+WPDN2tPG0/ppUFTn3tLI1dzjc+HyPzQ0LjzJu7S521BRSkv3eta/wMLPV0ZLY9fDzXhURUpa
+b9/OyMvRXj87QWXIvcLTbktISU1r2MrFxtxYSkRKV2vZz8/M0v5bTkhV/OXP0uNiUF3o9EZI
+XPbGx9fvX2TTx9PmYExOVFl32s7IzelbTUdMW2vXzczM3VhAOD5WeMnAy9PqanHz7tbLzcjR
++efU2FI+QUA3P1Zc08nNyMTfbmRITm581dDa3dzY19tsVkxMaN7U0Nr8X19YW2pcYGFw39bZ
+eFtUW/3Zzc7V4vp2aGp69+z3enRub+3d2tThY1RLS1///e1qVWNxX21scdzO0+ZoVVlqc/p+
+b+rZz8vUfFBHTu3OysjO3n1tfOro5Ofx9+3azs5UPTYrNVtpzsXJv7vZXVlGVulkcerexsHO
+2+hmZ1hFSFbsy8fR3+TzW09NTV7v4t3Z1NnvV0tMVfvZ19rk73pjYG/h2dvuYVhceOj3fe/4
+dF1fYmdxfPD7em1obmps5Nfe7PBybW1lYmBv4dXT1t7ne2BjZ3Po4uDf5N3Q1tnW7Ug4MjhG
+Tlzy0sG9xtPrdfhuUU1SadfP0s/W295tXlxYYGhr897c3+Df3+9dWFhXYXBx+v7//nf35tna
+6XdeV1to++7m3t/p6PtoZFpXXF9bXGX1297q8G9s/Xt28uvf2Nvf3+z68O3o5u/5e3d7/+zp
+49/g8mdjY2r14dzV7VlaVkxKS0tSVVFbaPvY09fR2NrV4Ozp7+jn8nl3bXD5cmZ65NfQ1Nrn
+9Hx2cW/37vZrXl9fZGFbYHfl4erw6eDk7XNgW1hcZV1bXF5qemxpavzk3NXQ09nf6fLv6+Pn
+c3r1enNzd/js7+95ampqevLx6/Jya25oZ2tq9ud9YW71bFxXVVRaXmNoc+Lc4+Dd29rZ3+38
+9Ofl5Ort6e3o5uTe3uXs7Xl0cWFgZm5/+/37cGpzb2psdXf07XZeWmddT1RUT1FSV1teeejr
+597n5uXf3NvW0tXc3uDj6e30b3D67+nh3tze7O7z/3JtZ2pw/+/y7+72fnFrYVhbYFRRWFRP
+T09UW23w7/Tn4+bn4N7g4tzd5uPf3dvc4OPo8fh3e/Lp5ufv+vx0+/t2/3r89X5sbf13Z2Jd
+XV1fX2FkZl1cXVtbXmNqce3e5enl5OTr7eri3N7l4t7Z19/t9///cGxxfX58/nVvcHX99/p1
+dPX5/n5+bG92Z2BeYV5gY2BhYWJmYmdocvvq3+Xm39/h3dve3Nze5u7n5+vv9/z8fnx3bmNg
+ZWxqaXj6+X95dGxkaWxqbGtsbmhkcXBsc/b48O75e3P29u/r6urn6vX36N3f3Nne6Orx/nh8
+dnt9e3p4dHV4a2lu/Xh6/np3ampnY2Voa21vbGtnZ2dodHVwb2tsd+7l4Obv6ero4uLv8eno
+5efk5Ofq7fJ9b2pqa2xuaWp1bnd2dP32/m9obHh9eGxnZGZpbWxqcvXw7fVzeX718nxydXL7
+8PTp4+Pm7ex/cXp+d3358e7v5uru/v7vfG1xbmltcGpkaW169vrw7/p+dm9iW11nb2xud/51
+dH199vfz7X1zb3B49e/p4eLi39/f5d/i7Pj7+3hvbHd1amt4dmdoamBNWNlsb93t8PBtbHht
+avvy7Ot8evT38e3rd3r+9Xtv8+x1cfNuevPw/ffvfev7cXFx8P3v6nz77XT3+Xz5+3ZdcPn3
+63lwd3BveP55aW15fmdv9Gzmenb4b+Zu/PXp5Pnl+HPtfm54d2h7/m17ZnH/deH36uPu6vTj
+8On7731tfmrsZux0/etj9G1gam5ta2fybW5392xvfGzu/u91f/b/5n7472/h+fn0/vN56vdq
+41rk6Hjo+fP37nT8ZltnX2NtaWt7bv7dcPF57H9v7+zhdc9f53N96mH9X2tmbXjubN9p3/p5
+5vrjbOZm6WjrZvB+fV3tav/vdtle3HP942HvZ23uYtxp89xd53lq6VrvX3Fp92nobe5f517i
+eeRj4/Rp5/nv53nw5Wnf72ffYn3491Hu72/Uftne591aZ2tZRlpdTuB03tfZ2tfjbelkZn1e
+Vvpe+fBj3mzaYuL56eVe8WRvdvrx8W7nc+z1eOpj4F335WL/7GFk8/Jv7O9m2XJc3+Rd6mhU
+5/xn4nB55Hlz5Pdm4F9b4G9361r65V5+f+nu/mR93nDpZFjf+mDsZ+vk7Wrm3eDtXv56/m1m
+XOrm9e/p8+h4YHNlZ3VqanhnffNra+Pw8f1memxw63785n9m+Hf87frz3t7j3+715+vq5n7z
+/W93amNbW1NLT0xOYmXu/d/Y3NPe4t7r7efq4+Hd3tnZ29Xd2c/O0dPV1PFOQDgwLi4xOkzx
+yLizsbK5wtlcTUM/QUdV99HHw8XK0OJtY1ZYbnDr6NzY297d3+LX2+H67uvn1ug0JSswN1NZ
+Vr+wrqyyzXtgTEc9NTlPzb26v8jLzdlpTkdMbt3Z297f1M3Q4uzez8vQ71hDLiYpMz95xr2x
+rK61y0c3NDM3P07cvbSzuMXtUUY/PkJez720uL7PX0pBPUFV3MW8u8LHxcPKWjEfHCY2TMi+
+uamkqbVNLCkvP2bs48CxrKy5Zjw1Nzs7Q3a/srK70VhHRkNBSmfWwLm8vru4vlwvHhokOv66
+trKnoqq/OSYnL0fi3tK7r6yzbDQyOU18V0zevLi73E1UetjiWVFp0sLG1NHAt7K/QyodGidD
+br6xrKOjslstJCk0R2vPuq2qrsVEMzpGUF5c/r+1vt1VTWvZfktDSnDQysfBubKyuNc8LyUZ
+GzNiuaiqqKOsfS8iIi5rvrezra633TMqOExfzcnHtrf3RkhS289dQ0Rd4vDv17yvrrjF2V9L
+PyYUGmi4sau3r6CsOiQgJ1O5vsO2rrPLOyw0T+DWzsS7vfNESeTN13hcX/JdQEPftausutLe
+9VxEODQfFjewy7y0uaWlPyQoLEa+ztOyq7LZPTRJ4mNW4r22xko5RtLPa/bTzt1HNj7SurKv
+s77SXkJITT4/LBozrWZVuLaopjojNURHbFHXrarATEhN12U0PMe0vm9JXcfHRjtixM9KO0HL
+t8LDtbG+WEJS1r7KQCgaLMc3NL6poKVGK0M/LjJBwquvzeLD2kg3NFO9u8vKvsJ1QjpB9Nde
+XNfVz9Ns1cndaVp2zsK9sq+9RB4VIDk666idnalAKy8pIypHrZ+msrm/TS0kKmK6trazsr1I
+LzRFUW/Zz76730lLZ+DnZ3DLvsHL1MvFTiYeLFdc3bWqqrw/ND44LDTVraeuv8PJQiwrN+a7
+vb+3ufE7OUhRTk7ivbrOWFnw+FtR7sXG09rTz9jYxMg5KTRFOTNHxbC00/fWTzUyR8G1vMXD
+wuI8NkPfyc/Lu7rmRkJCTmRi3L++01JDRE1OTeDDzON0dtXWaOjHwcp+VmJdMyY20dvZwsK9
+zD45TVxqy725u9ZLTFpOSVHgv7vE2Hp+Y0I+TFh+2d/q3eRlWF3kztRxVWTXdFvWx87nXltb
+T0xk0dHq7tTJznU6KzluSU3FuLS9VUZaS0FayLu6xd/k8Uo+Q2DSysrKzN1TRktf7+ff1s/j
+VUxNUlBSa9vS32thbHh/elNK6Mt3Z9DK0WJHVdbcbt7JxNhectvPz/VlYzs3SkxRzsjLxNxR
+Uko/RmnOxMPL3O1bSktk1MnM1N95Y1hQV2/ezcnaVkVGSlB72M3FyvNVT1VeX2vUyNDqXlhd
+Xm/czsfJ32ZRSk1g5NbU419IPj9CTO/Nwb3B0mZLRUtb5Mi/wsrpU01NUGb79drb7uLtXVxc
+WXDse3f659bc8u7c2PhDRG559tbazsp2TEtNU2vx2c3M1GxVUlBNUtjIXnDF71TZ4mTT1WT1
+3v5mX2FgXl5ueGZy7evf2eP5bV1bWVZj9eDa3/R1amdve/Lo3NbX3nhZUlZi++Tl6eb7YWN6
+4uDr6t/rY1tXWWv16t/W3WlVU1RWXG/k2Nbb7nb3fGVu/fDq5u1//ez3aGx1bG14bXL06+73
+f3drZG1vZml8/Xr97ujqeWl35t/b1tjX2/VhXVxYXWRoZGRgWV1qd/3s397j4+HsfmtlaW1z
+7+bh3Nvj59/d4u79bV9UUVVca/1+cvp4Y19fXnLj2tjY3ep8YV9qdvPj3uft7vf+/vj+enlp
+ZGVoaGl4+fv19n12bGh0+e/m4ebk6flvb3N5cWt88+39c2977O/9+vZ9e3Bna2t5/Hf47ut4
+Y2RkanBubnV8e/nu5uL0+355dnd9+eXf5OLe5nFlYWRz/e/v8vpvY15gZXL28enueGJfav7u
+fPnu8/T47+rt6ePn6+72dG5sb2xnZWFjX2VxbWz+5uHg4eHl7+/z9/n4dHNxb/7+8O/3/vr7
+d3VrZ21sb25naGNnYGBx+vDv5uPm5uLg5Ofp7vH57vJycWlqaWpyamlpbu7k5Op+bGljYmVl
+andycvfp6u/1fXb26ezs5enwfnFwaG96bW98//Z8Z2V47+fo7vT+eW1wc3vr5/T7fvv7/W9z
+fHX4+f5ydnZvc3n7dGp0/XV47fJxZ19jaXX//+vk5OXf4Obtd2pnb338fHl7/v7++Pf68+zr
++f3zeWVlaGhoaW78fHR4/vP17e3o6urr6uvv/nl5dW5taWdtbmpqd/91cHZ4/n769uvq6eXs
+eHR8/fP9/u/u+vp7cv/u7n/0fG1zbXF6//n19fb1c2dqcWtncvv4ffX3ffbw9f5+fvfu6+z0
++XVy+n96fP949O7y7Orq9ndtaHV4b3RyfPbz+PPx7fl8+fv7fvduYmhtb3N2//Hp7Pj3+HRq
+amv++/v0bWh19XtvePXx9f58+One4uzu7Obw9/x2eGxpZmVt++z+/3l6en3+dXR0dm1yZmL7
+8+3p6uv0/fn3dvvueu/k7+rxbmpoaG5qZ2Rmc2x+7+/l6en4d/d68vb+9fZzb3t+8PN8enbz
+7f74+vP7eGhdYWlocOLo5+3x/m7v+vLt/vp5eXnw6Pn38HZpbF9gbmNn+P1tbfL66Oj//e3t
+6On9fPP48m9u8/90+en0+PHu7XlobnRuZ2ZobGxzb2xxamhta3Hv+33s4Ojy4Oft5+rk6O5/
+bHH29HR89m9vc2Vxcv9+emtlZGNpcHt5ffp3ceztbnP6/XJyfO7w4erz7nzu8u7vfPj5+Ovp
+9PL7cv7++nxlYGJrcmpeYmlteW9v++r67+348Pf2d2/47vHl6efm9vDx8uv1dGdudGhtbW5u
+bG51dG/8fH3v8Ozo7u7r+Pn38Hpw8vv8/37993NjX2Flb3T583747X92++vs9vxyb/br6ePo
+8Ofk/GVkcW5tfPlvaWh5dGT/8H57efnx+H9z9+3m3+fj73P//2pnbW/78251cXfx7Xpq/O9+
+b21ia3Fpbfru7Ov6fe3e3er0ff75ZWPx7nxxbXf+dHD1eHH88O1pX//n6uji+2nf3fh6XGDy
+b1lZXFxpbG1+69zaem7f297mdvji6u94bXv4cePfY17+fGRhZvZ0Xmv15Odyfvh26PVv6PBt
+dn1t+H1p8eR7cGVadenv/n599u/3fvfq3t/w9/Z1d+jo+2VecnNob29faP7t7m746O37W1vj
+7f3k79/b+0tIWubLzdnfVEvp7NrM0+3yaEtWfe/e52NZV1Rd7ene3P1sYW/z7Ofv7fV9/Oje
+297pdm5sbP7s+G9xY11gbn92bG96bmltbnRvU1bLzfbxaHPc32Xy3PTi9Vpm6+bg4Pb9fWRl
+Z2xkVOHM3XhaV2rofnLj5fpeUlv33eXua11sZ2/s+Ofe62566evb4HllZvvxfWzv3uR2YGXn
+2Plvd2J7725xempqaFhWX2pv/Xz97vLz7ezv6url6O38eXlxe97kVmD+bertbP7Y3t3ceWtp
+Y2D57fvvc2JiZV9o8ubra2lkafLf5PbrbXjzZXXc3eTf915aV1d77f3v4e5uaGX+4t7ramRh
+/e7r5eXpc2x1/e3o7P5vamdxbn3qaWloYvLm8/vteW3x9eve6ftya2Jhe+rr9X9mXmjy6Xlt
+aGF3fmpq9uv2+WVm4Obr0tP8Y1pT995obt3f8XVYWOba5PxwZm3xeWz+d3J6Z1lba2n84uVv
+6tt2Y3jueu33ct3g8ejq93Z9cP7u6e90Zl5fZGdlY29wcH7w5vXv4OxydG723WxWYXNx89/f
+39t8Xnbv9e3lbWdzcX7n7Xzv7XNpZ2ZkXl5k7exvbWdYZN3a4eLw89/a3+Lk+P3u8WpfZmNg
+Z19ebOzk3+Dk3NvleU5BSFvrz83mZWhaVv/X29vZelxaWmLl2Nrf9WFbaPnl19PY4W5WUU9i
+3tbZ715ZY29s++Hf3t/oe29dWGtvXmd6fPjq7Xb/6d/Y09/072lNRUlPdszG0m5WTlT26OTU
+0dnnb1ZRa9fNztpnUVBYbenc3eduXVVXd+na199+ZmRbXG3kz8zXblVRU2Xu7n35dnnl3uTf
+19re4PlaRzs8Se/EvcToVEpNd9rX2tXW6l5PSk540svN4FxTWHLj29rd3/BcTU1a6NHN319X
+WFxmdvfk09Dde1VMUF3739nc4ub7dHL03tjV2GZBO0JT3cG+zW9TTUxad+jVzM7fZU9LU3fX
+zdHjbV1Yaunf3d3m92RVVFr13N/l+GpeWVte+dvV0N13WUxOXvfd0dDS1+V3aXbo3Nvi/l5P
+TlFSWPnY1uR1V01QYe3f3Njc5vVyYV302tLV3/1eXF9w7+Pf4elvXFVd6uDZ2u5gVV5u+NzU
+2eLvdltSUVhs39nkdGVhZHjq6e3weHfv53lXWGP46vPy+/fx5N/vbGzr6PLuemdy6dzc5O18
+cHl1bWRi/u30cWdeZnh1bmBfbO7o6v5t/npv//v759zV1+J0X15pffLr7vb8fXd0bWz25eHo
+/nZuZ3jo+mJfb3ZzY2Fs+eXc3u1tZWZr8ujq8HdrbmZfb/Pk3t7tcWpx7+Tn7/f88fZ3bG55
++/v2/2xqaGlubW10eP7x8PVzaWZt9+73+O/p5ufk6Ozp6PJ5ZVhVZuXvYF3p1tvmc1JGSW7a
+1978X1/02tve5Ojj5OXq9vv08Gtpb1xVXGxu9N7jem75fGZjfung3+prWVtgb//48/l/fHdw
+d+vh6O/5cm/29Gdifejt9W9daOjVzdHtVkxSYfri5e/27+bh5ebp5efqb15191FJWGhm9Nfd
+e2x+dV5t5ubr+2VXWHDu5dzV3fF2Xl565N7jeVRZ2MTOT0pu39jU7Vlk3N5mTUhY08TOaUtI
+XtPJzupqavPqbFRNT3Tb+2nw4Ot8e1pQ8tDd5/BcWPDe7+fh/Gz8/GJhVkzpxsjXbVpf89ze
+7PHd2eBYPz5exr/LZ0hN8dPT09ZfOTZKYMu5wHVMS0xm19rMydpPRkxezr/D21tJS1/k6U5O
+yc176mN5y81v89n4/fxMP0JO4MbM6fzq6eza6UM3P2+/uctiTVJXcNri287cZVpQTd7J2Nn8
+UE5cWFlPaM3LyNX05+x+593v0cvc70A2RuTEwupp73pfWk02N+PExMR4T1lXTGPPz8vK41Na
+7d3p4snW2mtHR0o+WM/JvcbjW1FZ1srP1eZ1Z31YS09Sb8vK2d9KLipE2s+0u87TTjg9Zc27
+usjgaU1EWMS97tDRQ0c5ONTQ58TEystsP1HcyLvNZHNYWGH82dl36NtHLCM2wMa5vc7Bzz0x
+QGC+r7vReEtER1rCs81HSENFR0/PurjC2VxLTV/jyby6wmpLWUlLa2lqQzIsL0zYvbS5v89U
+PDpBXMm7vMTMZ0FHTV7g3s3J3nNeXdzIx8TOd2l5/OHHyXjrxc86MjorKjtL2K6uu7vQPDc6
+OE3DvLu4x15LPzpL3d3aztLRzvNu2dzYz97fzsfGy9Tex79UMDArIipC67ysrK+xzT44NTVI
+18q+tr3U/EQ6QUBDbd3Pwb/Jyczh5+rozsTFyM3Nv7rcMy0lHic4PtCuqqinuUw8Mi44R07N
+s7Czt89HPjw6QU9a1b26vMHaaP3qfuvNx8jJycbHbTssIR8oMkrBsa6qqbC/8jovMzg8Tv3a
+v7W3u8DYUUpFPkVW+Mq7ur3F2fPRwsLK035OQTo3LioqLjlbyL24tLO1trnLXEU5NDlASVvk
+zb61trvF709JR0xTWFZl2sm9tbS4vMHcSjw2LSclKC9Fzby3s7S2t7i8ws1ePDQvLjRAWdu/
+uLWytbzE1GNOST8+RE/wxrq2tLS4wNhMNCkiHyEnNmC+r6ytr7GztLS60k03LCkpLDVNzrqv
+ra6vtbzC0mBHOzU1PE/Qu7Kvs7nMTDUsJiQnLDVK28C3sa+urq6wtL7sQjIrKSksNEffvrOt
+ra6wtr3L7U4+OTk+Te7Lv77CzG1BNy8uLzM4RVzmy7+9ubOwr7O7z1E7Mi4vMjpHZtXDu7e1
+tLW3vcjjU0dDQklWa+HP1vlgSz45NjU3Oj5GU/DMvreyr6+zuspaPzg0Njo/Rk9q3szCvLi2
+trrC2V9QTE9UW2nw6OzxblJEPjk0MzM3P072yru0sK+xtrzKfUxAOzk6PD5CSVXkyb23tra3
+vMTN7VlNSkpRZOLR09p5SDw2Ly4wMjlGXc+8sq2srbG6yGtGPjw6Ozs7PUFQ6cW6tbK0t7q9
+x9pkUEtHSFB13uv3XD84NjIxNTY5RWXMuK6srK20v9NaQDs3NDQ4PEdr1sa7trOws7m+zfBb
+Uk1NU1db4t5qXUU3NDIvMDQ1Pm3DtKyrrK2vusd/PzUxLi40OkBkzL60r6+vsrnB1F1LSEdJ
+Tljt0dfyXEY5NjMvLzQ4RuXGua+trKyvu8phPTcyLS0yOEbqxrqwr6+usrvF31RNS0hNVnPS
+31plUjs3NC0uMzU/fs6+sK6vra+7wttIOzcvLjQ5Qm/NvLCtr6+yvMPcWE1JSExYcs7QbPNf
+OjIzLi0wLzde1sq2r66tr7q+yVpAOTQyNDc9WNLAtq+ura+0vMjdYE1HRUVFSlVSV1xDOz0/
+PTs8P1Bs/NnLyMXJ0M/O0NPT1NXSz8/Q1NzsfV9ZWFheZGRod+zf2t7m7PT2bmtu+Obd2uTt
+e2BVTEdFRENCQUNIUGvo3NPNycTExsbIzdXe/W5ybXzv5trSz87P1dvocmhkWVhdWlhbVk1L
+SkdFREhNT1Vieenc1tHOz9PQzs/P0tzq7nhsa3Ps39fV0M3Nzs7W6ntfV1dSTUxPUlhfXFxX
+VVdWU1RaYGdteu3b2dvb2tfU1djh9HVvcnT38PXu3NjZ2dve8X91bGpjX19fZG344u3u9Phr
+Xl1hbGhtfX1ua2dmYV9mamxpaV5ZXGFjaf3v5tzY1dPU1dTW3OPp7urq6erp7/t5al9aXl1b
+WVhZW19hZGl19+7m5ufg3+v0+/52dHhwcG5x/nVpZGRnbvbx/PPu6+Li3tzh5eb2+Ptpan35
+eH79fXl0fPft5uXvc3dwZmZlZ2Rrb2ts+vtvcXV5fH19c3F2+u7r6uTt6u55cm9ycHhtdHn0
+7O/t497c2NbZ4vtnXVxfXl5gY2x5+vl9dv57aWVpa33o4fJ/8O37cHJrbXr3fGhocG5naX3u
+6ePh5OPd2tzj7vz7/mtkaHZ4/fZwbmRpa2NrcXJoZ213d3X//f18fX/zfXfp4ux1e/tub/Dm
+5+7r4t3n7erz/3xxaWdnanX8fHv2dXF2dHd8eWlzc2/w73Zwffz/eH5+dXtxbHNpY3B7fu3n
+5OXf3uno4ebt7PZyZ2VdWmZqbHX+cv7s6+/+//37bGl97/v07e369PxzcmlmdfRwbX7t5eLo
+7/R8dv3+am19b2dvbm169u3p9HF1/nFrbv/3+/bt8fZ4ffHs6ejvd3J0Zl9u7Ojv6+Hl6O94
+dGxue3dtbWRhZ2hmbH5tc/34+fzs7+rp6t/m7+jq+Xv9+H32f318bGxnY19kb3jr7u/t+nt0
+dW52+3h5bm9vdXt+8O/t9Pb3fXn0/vfn6efp9nn+7/b+eW9nbm9la2xxe/bw8O/y9HNqaXb7
+f3N9cmlsXm9tbXBWXMbjYdrT6+rf5Nn1a3P2ePJubntsbGJqZWprcnp0e+9++/zzc2dpe3Bt
+eGFc0dB6X9LieG/nzfJXS8/bS0bCvz9YzUxI+1La11zRzO/edXfO307k3FNbaflndWvubFJZ
+ZmNUXVVgc2dhec/W2tbL2t7P083QztHLysXLztB5PzU4Ny4qMT4/Qnu+uby8s7TB4N3TUDxH
+cVpLYci/wryvrbK0rq3FRkouIB4eHB8mKEW9s6qfn6Kmrb1KMjIuJi09SlHMtKyrraalrK+w
+t9ZDMCIeHxscJi441rSsoqOnpqzOSTosIyctL0XLtq+sqaiqsK+wt76+vd1IPCkiJx8cKDEs
+Qr23rKmrqavVVlowJSw5M0HAsrGvrKmtu7awvL+1tsBIRUwjHSceGikuLeW4uKWjrqer2l5Q
+Kyo1MTnzw7ivsa2tuLu1ury0t7rESDw3IB8pHRw0NS/Isbaopa2rtGz5RiktOzI61MS9s7Cu
+sbu2tsC9s7/HvkE9ViUeNCoZLfkrQK20taisrrBmW+0tK0g5MnbD0L2xsre8t7rOx7q+yLq4
+bExxLiEvKBwrPys9uMK9rK2xtcPH7Dk4RTo5Xc/ZzLy8ysy+xcy8ury2s7S20PLdLSQ1Jhos
+NiE6t2/Mpq67rLPX5VE9Pjo7VE5Iz9BhxrvGvK+0s66ws6+14mXsKiEwJBoqOSM5s+HeparK
+r6z+W81DMz5ANz5SV3TOvbq5sq+zsq+2uri+zfREOy4mKyYjKjUzPMrHyrSuubyxv/7TXTs/
+RTk9V0xkzsy+t7azr7W2uMzc5EtFXEo8T0o0PD8wMkA7PG3je8W7xcW8xd7R6UtMUkVCWmRu
+0MXDwsHFxs/ZzdTdy8nTycXW4fhTQDw5NDQ3OTxDS1Rq8tbLy8K+wsXJ3GtaSkJEREladdXF
+vLq4tbm8vcTa4PpZVl9lVE9OPzs5NzQ1Oj1GV9/Nxb28wcTMfE1JRD4/SFt/1b+8vLe0t7m6
+vsfN4GJYT0hGRkRJT05MSUVCRklGS1JPU15jYXbi2NTRzdXg4Ox4eN/b08jCwsDCysvM1+lt
+VFBPS05UUVNnffPVz91vXVBGQkE+PkRMTFNt997OycrMysvU4+3t797c1tPX1tbZ4Nzf6+fu
+b2rv6/jj5O7q6n5ua11PSUhLS0ZHSUhMWFtYa/Hv2MzN1tLT2tnV2dze4t/m5tzW1tTV3t3d
+6H13dmxveWpsem5oZmVhaGxraV1ZWFlVU1NQUVNWWVxibHV56N3g2NTV1dDP087N09ra2Nvf
+49/f4t/nb2NhWFVaV1lha2JcX15fZWFeXl9iZ19bWVxgav3t6+3x7uXe3NrY1NXV0tnb3+9d
+W9DI4kpH3MXUXmFtZ1RWbl5LTFlcYGNjZv3k4u309/zr29fa3e3v6fD6e3n89X138ebafmL3
+eWdfYl1lW1pjaXRzZGh+9N/o/ujc2eTo3PN76fNw+2xjYlhf8eLq5+nj4Onf3fVtdWdYWVhU
+VV9cXuTjeu7X4Hne0+5e4t9aVXb/X1pm4tzd59ra6nr40tx55/hu7HRfWlVpa1tcYWJvaGTs
+7G9t3eN6aWb7b3Vu7uD1evLW3m7a4HP97Hjt2vblf2l77u1ubnFxff1zZFpdd2ZeaGdfdnd7
+6v33cnJ9e3VxbG7r5uPj19jd3OHg7m/1+F1ZZ2Jca2NcZm9hbuz0/ObrfPPt5vh1eW1v/vzv
+5+377eZ+ffD9eXfq+XJz/GNjY2Bma25iZn9yeO7q5OTt+PPw/Hp2/Hl743724ufu6+Pwdmps
+ZFl8aWhqZHl6+fx8evn5+3Z48u/3a/1xfeHdbWjmbE5Y1PFb7NHmeepv1uZcbc7eU2nqY2Ne
+WtB7at7UaFX/bW1kWlxtXFXf2+rt3dvpbml9YGRtbl9k1dd55s7N2O7m0dR758jOb/XOzX5C
+MC05PC80X8TAwb+7tbt9TFteSD5D3L/K1sO6wdjjyb3C18+8vsbJwsozHBolKB0jdqqmqaqj
+oLQ6Lzs3KCg31rGvs66quVZNV05MWuzBt7u8tbK6vbVlKSMXEh4pJz+hm5+gprLWOR8eLjo6
+7aukpae561M4LC9Abci1raussb7CxXNIT149LiEXHjQvML+fnqWuuslDJx8s78G6sKiltUoz
+LzIzNEm2q7C0tbW7z11cbF7/yLm/3EgdEB06LS6vmpmfuExIMB4ePK+oqqywuv8nHShGe860
+qaSr00pm3W5e79PAvL28u8BKHg8UNU03u5qVmq0xJSojHjCvoaCltmU5LCQkMsutqaqtts4+
+Mj3qx8jDvLq+wr++2zYmFhEnx2XJnZeerEEgIi4uNLmkp6iwSDAxKyo+zrmrqbXJaUI+WOTy
+2cnYbuPQyLu0uuM5HhIaZL6/pZyfqLwpHCMtME6yq6agrz4uLSktSceyqKm8XktAPkA+TMm7
+wczNyr+6ur29y0QkFhw6Tl22qqqork4tLi4vTry9u7C938z2PkzPz3tPPj5hy87Nw8XIwdRR
+T1JY3sm8ta2pyigZGCM33rm3squpsdg7Kyk0Ys3JznTzuay030E5Qn1PODpcwbOtsL/VWUZI
+S0hR3L+vqquz6yobGx4jO8O7s6qloqW7NiknJi5ASlu/sKymprXYRi8sLS42VsW1raqstM1G
+NTA2PmjJwLqwrLHZLyEeHyc1TMuyqqWlqrHESS0lJCgyT8u1q6mrra+/WzwwLC41OkjRubG0
+v87Sy8TNUzw4OUXewb+/y11EOy4qLDFB1LarpKOosuc2KygnLDz2vrOxs7a6vcDNUzguLC46
+UtTAure6xN1WUV5y5NPNyb+9zV4/My80OEBSYPzf2cu9tbCzvNpIODAvNj9dzsK9u7u/x83Y
+Z0k9ODpGX+ba2NnPxr+/xtDtZHXOv77NTjctKCkvP3fGvLq5t7S0u85TPDQyNz1LetTMxsC/
+vbzB1HROPz5CRUtYZHfayb64tri8wc77V0U6My4rLTM+Xs/Auraxr7G5xulKOjIuLjI7Td2/
+ta6srbPAZz84NDM3O0j7yLq0tLe9w8jQ2+B3TD01Ly4vLzE3PlbCrqeio6q56j0vKygpLThP
+y7SqqKmuu9hNPjc0MzQ4RG3Lu7a3uLi7wcnMz99aPzEpJygrMTxL2bmspqKlrLjWQzQtJyUn
+LT7TuK2pqKqss8ZQNy0rLjM7TebEt6+usLe9xtXe4Fo/MSgmKSsvOUbvu6ympKetucdfOy4o
+JCcwP+S7saunp6uzzEw9NzIxMzQ9XM+/uLW1tLe9yNd4UUM4KyQoLS86RlTEramnp6y2u8pE
+LyklKDJBVc25sauorLa/4ks+NzI0OkRUedXGu7e3ub2+xdT0UkA2KSUsLiw2RF64qaqrq660
+uM08Li0qKzU5PtG3sq2ssLS4ylpGPTg4OzxAYdnOw728t7K3wMzvVEo0JScwKyg1PUa9rbGt
+pqqusMBNPj84LzA2OkbszMq+tbW5wM7Y22xLPz1HU0xIVfHZ1s/Mz9TT0dPcbV1v+WJPSklM
+T05NTFFcX19gYmrv393e2s3Hx8zMztDP1fhhX1RPUlRdbW7x2MvGx83a531lYFtXXGFpXVVQ
+TUlISUpNUF795+Dl6vtvYW7z7NzSzc3JxsnIydTqa1xWWWVrbvTq6d7Z2uDi3+toWltkc2Rc
+VU1NUU5JS05VW2Ro+t7f8HBiWmHx3dXPy8fDwcPH0OZvXFxWT1JXYXbq5OLa1dPT2eLr83Zv
+Y1hPTEdHRENHTVNj7OHb1Nff8GlcW2Nt+t7RzcnGxsvO1eV5Y1xaVFVbaW1s9ODc2Njn7uHh
+5OLvY2VmVkxLSkhJTlVZbefc3N/mfHFmXF5dXmT74dnPzcvLzc/V3unl7vn0+Xx6fv57eX76
+8+99f+be3uf7aFdNSUZDREZFSVJhbvTp5t3d3uTj3tva2tXPzM7U3t7f4N7b3Obj4ePi4e1/
+dG1rZWFeX21vZ2ZhWE9LSkZFSkxOWnf36d/Y09HV2d3f4N/d4OHj5ubo+Pbv+O7c1dra2NjX
+2N7o5fh0c3Z4/O78YVNSTUVBRENBRkpNVm7u2tDNzczLzc/S2N3f7nhmYl9haX3o39/Z1tXR
+09PU19zf4+bv/O/6b2ZYTkpGQD4/P0JJTldp79zQzczLzdHV1t7s7f1wZmz4+n3s39vW0M7N
+0tLQ0tfa3eHub2xrYltTS0VISEVEQUJGSk9dbPnc08/NzM3Ozs/X3uj6amp3bWZu9ubj3Nvd
+29TO0NLV1tTW2uXs7/ptX1RIRUdFPjw+QkZMWGZq5tTNy8nJzc7P1eN+YlxfYFlbe97Tzc3T
+087O0+Dq5eLf39nZ39XacVdHOTQ7Pjg6R1b+zsXDwbu7x9ToXVVQRkBETEdO5+XPwsXDvr/G
+z+FsWlZSXO3YzcW/v8fO1mBHOi8lKD8uK2W7uriyq6qzf1DqSjAuOlJXWNy5tL3JxcLsRUta
+T0hL9cvS7trAxN3bxcXY1eDn+klLQC0kO1UmNry+yb6xrLJtTcNXLjNHYEVHx6273rS0z1hN
+WGI/O9bMUXnGyNLi2cnhZM7Hzvx74G1AMCovRigqxsN7vKqrssXWwU4tNUI6N0rFuLq/sK7N
+9ttgPTxKTVFb3cLL4MjG5Ove4t520sLnauDxOiUy+SQly8hIwKytrb/ItVwuOUYwLknOy8O2
+rrHBysVpPT5HPT1U79DSyrzJ0MTJ29rN5drVZeP4TDQpPTohMstJR7Ovta64ubxHPkg2MDxB
+WsvNubK8u7zT5HZJRkVET05O1s/jx73IzMnFx9Hg0dxMe1AtKz4yITnnOle3srSvsLK8alxH
+NTY7P0Jp0My9u7u+w8nlZmZPSEZOXU9b6tfNycS/vMTJv85s6GtCMSw/Lx49VS5Us8S8qrGy
+uMbJRTpCODU7T1Rfy8S9vry6xMPN2eNmVUtNS09ja/bbyMDDwb7NycpKPjM0MiAvOyk6x9DW
+rK20rbK4zn18Pj09PUVDVVXpztLIwLzMv7zX0t1lV09KTlRa6+DLw9O+wW3AUzJLRCMvVScw
+cExMwLW+ubG3x7+9euzdaV5MbFJCVlJEXttf0cLVxb/DytDN4F9uelBsfljv33nl2mNJQ2gz
+L2szNXVVRuvH8dfFxMzNucjZu8jfy9BdW3tJR1hKTnnq6t3Jy97KyePY0Hb+3O9jbHhYYmFo
+Wk9VR0tMREtLTU1SWU1dX1t66uDXycPBvry/xcXR3e33YlpuWVdpX1Rr6mR91d/p3NPgfO9+
+WVdPT1NLT15QXXxgb957a3ReVk1UT0xZXWjc1M3IyMXEycnIy87P7Wl1V11bU1RUYGNjb+Tb
+3OHb73n/bVpUV1BYY1xedfbg7fzwdW1aXFVOWFVjYmn189rf2NLPz8zP0Nja3+59bnJmcHN7
+/+nl7+fb4W3sa1ZWXFpSXG9jYvrre+3h7fh6YV9VVlNTWVZbWGhp6ebX2dbO09LV2N/h7n1r
+dGh38+3k7ODc3+Lg3eJvbn5fWmxcVVtfWldbXVleZV1eem909e58cPvr7ezd4+na4+Pg7OPr
+7vB+b2Fkb/fp8O7x5Nvk5uj7+G5iaWRnant0b2x4e31tbGt8fXD9aGlvZmVsduz0e+Xu7/fv
+92l+evj/7OTr697i7+tzb3J8fevs5OHj4eju7Pb37XNfX2NgYm9nXWNhaWFqamz9b3556+j6
+7eXu6+Xv9Hz44ex7a/j27Obo3+rr5+pu83JzaG5zeHBt9276+vDy73vxfn5ud2plXV1aX2du
+eXx7eOfq/Hnv7/D49+fu3+Lk6+rw+Pd99mlfb3b44u3+cG74/vp3b2FqZGVzbX717vDm6Xb1
+bmxpcXHy7Hz7fP5hbWr8bmltZXZu6+/o8+/x8Ojm7P7ye3xx9n17d2H5fvXw8Ofn5vLz8v9r
+Zm9tbGhqeXF7d3v+7efk2+Tx6/lvamNeVlRcUk5RVldefOjg3NbWz87OzM3U2ODq5GpiZ1xc
+XVtcX2Z97Onk3tnX3d/g4unq5VpQ9EY+UT89SUpHUWlxd9/MyMy/vMfEw83T1uFcWGZLSVVS
+S1n6WmvU1tbOytDWzdbp4t/scnZmSERWQjQ+Sjc6WlRG4sHRyLW4v7m4xc3K1GZNT0o9PkVB
+P01UWf7az8nFwsDAxcfL1evr9WV09l5id0I9bz4tPlAwMOz1PfO5xs+4tMC9ucTNzeFgX08+
+RFNDR1tXXt/V18nDx8fI1Ojf52NqcGD83OXq50o/dEcuNUU5MEN3Xd3Fw7u0ub+2ttXo0HxJ
+R0pCQ0I+TfBxfsi/xcK9w8jM33JzXU1TX1/93eHezOU/SWc4Ljg8OD1MTuO/wsK2sr3Fwc3n
+6lxGQ0dEQ0tNVtzOzsC5u76+xc7X9lpOTExNVlzwzsrNy85hPz08MC0wMzZCVGDPuLe3sLC2
+ur7RemRNPzw8PT1CS3LMxL+6t7m9wNF9YE5IRkRHVP3czcK/wMbPYkQ4MzAsLC82QmfTw7au
+rq+wt7/K50o8Ojc2ODtAVtPAu7i0sbS7ws9tS0M9Oz1FT3nPyb+5u7/I2Vk+Ni4qLCwtNUFg
+ybexrqysr7fC4U5AOjY2NzpJZNvAt7OwsLa+yudRQz06PEBHT/nNwcLDvb7I2ldGPTYwLzAx
+MzxJX9G/ubW0tbvC0PdZTURAS09f2MrDvbu8vcPP32xRTkpGRkxXW2Xr2dLP1N/v+2pbWlBL
+UVZVRj1AQj9BSFHu0s7LxsbFydTa1d1tb+Pl29fUyMTIy83S5f5dVVZST01QWWN9dvLZ0dZ+
+bvBWR0tNW2lt29TY3nlOREI9OTk8SGXt28e/v7/AwsHGzdDP09rV2uPe6vbg2NXW9VdTV09K
+XO906Hxb91RAQkhMZPBt087T0+/yXjw1NDM6Rk/bu7m6uLrAyNHa1szO08rJ0t3ua1Zf5X9j
+83Nk519PamtQSlBJREtJZN3u+tHH2O5ra+ZPNzM6PT1EacC1uL6/v8vpdefT0NDMyMnK0vDr
+0NxfWVlWW0hH69PZZk1NUkw8Qlrn2mnly8nORC8/Xjw9b8/IzHzpxdBa48jMyMPNy8Xk/MzM
+09PwYmpNR13X1/b038/dTkhNTEVBVN/o1c/pXzoqMEE8Ssq6tbnN3N9YQ0pf0cbSx73I1dDM
+zs/ncuHo8+fbz8x2UmxQSE1KbO5PYdlkTe7T5XxNOi4uNztSyrqztcXl42JLVW7OxNTl7PJv
+/97Oxs3e3+J0XmTYwb/I1PlYRDtEVFTvzs7Yz85bTl9FNDI3NjpTyru5vcPK+0dDU+jZ08zL
+1f1aV/f8W+TNz8/Y2s/N2ePZ2nNffftcZtjb2szO119NeUg7XE04OjEpbLXwzrO63VA7PG91
+Ts+6vs/Z/lv/b2nTws1g2srU1N7XyNNaUVlSV+XLw8nM22dsSkJ91kdCYUAiHMqxQsWsrrpS
+KS7UXjvNrq670kdNzVVF3Lu8y+FX8nk+WbvC2exVTEtDT76zvc3tWkc+VdDKw8heRzUYGbS3
+Va2jr8U6HyrIcEGypK29YzE69kZIvrC6xls9TnBQbMTXz8hCOlrYyb7Gy8V/RUpc0bu+SmjL
+NjpUKxcon7VYr7TNyy8ePq3Bz6ysu9E5JzvD1My2tMPWPDBP2dHHxc/rPzxOY8/AvsTiSk3e
+09fNyMjlSEpQWW1YLRkkq7pDwa62xTojOq66Wryuu8dLKjy4uszCv8DeOS9BzcTKw8TnRTg6
+Ss+5vMPJVkJb/W7Ov7/dRz9Sb0AyPzseLai35LO/T/w8KVaut7quunZROjBetra7vdBl8zwt
+UL3AxNZAPlM/OeG3s7xsQk/6XvvXzcF/OjlhwlpMcDkkJMy0ybu8wMFSLTPNu7+4ucbWWTk7
+zrm7vsnebUAxQMzAx87fVUc+PlrFusDmY1xVVF9+zsLlRujbPjxDPjYnMa+ru8DB03xELTy6
+sru+yursVDdFwre/evHsXkU2RtPIzdHjY1NJSvjGv8HbXGL372TsyONc21Q7QmpNOSYmw6y8
+wr/Az1YvLuu7urm+zsvRPTVTw7q9zXXtUDk9Te3NwcHfZE5HUGjZwLrE4t59TUdOVXXL008/
+PEs6IifOsbKwwdO/3y8uSs63rbfNxsxHOD5hwbe/1uZOPUVIQnjFxMfuR0VUT07Qu7m8zWNV
+TUpdeNjAz0o/SjgkJkfSt624v7nFPzY2OGi8u8G+uMDSaUpOXW5famhgz8bcW1VealRISmXQ
+z8/LycfO719cVFv282zp6FpKQTQsMT5T1MfFvrm6v8vxT09i+2Fl6OHMwMPM1d59Xl1kcWxP
+R01aWVVx2M3P7l1YWlNQWF/tz9Di+e3x8+Hf3dfzV1ZXUl3ve2l3aV1cUlV9397pc37d2N79
+bfv65dXRz9v/a21uX2Pw19T1aG5vZ2d9fXLg5lpOTVBc797j2t9uY2FaWm/o59zZ539eVlZc
+df/u393a3eP7b3Jv79/W2fV1aF1ZXHPf2d/s7nFbXWl1693f5/JpWFNceu7i3+r3bFtWVmTz
+4d/l4X9cV1hcY2l169fP2Nzkemp67Ojl4ufweXR49eju73xyaFtZXm3u6/JuZmddWWB069nX
+3ud8X1xkannj4eXe52ZaXWZocHd349fd4eTqd2dhYGptffH5Z2vx7e/zd37q7n1vbu/f5Ptu
+cnlpYGRs/OXg6+75cW1tZGRudHn57/R7bG1sYl9kbvHm4Nve6v5wbW12++rj6e/r+mhjZ255
+7+v5ff7z8W927OxxZ2px+evu8Xtvef50ZWZzd3717u3s73pfYWp68+ze3fbj5WVjWmN85uf0
+9O3u+WZgdHh7+f9ueejh6vbr6e35aWVqevXt9Pzv4/FlXWR6YWPc2ezx6OZ6XVVdZ27v5ezx
+7PBya256+X11dunq+/Pn6fLu/25oZWpucXDt5n3/7OLl5+Xvfnttand8bnP19urm3Nvo2tX2
+UEZERkNDSlTwz8zJxMHDydLvaFxNRkpPV19oaXTaz9zaz9PT3Pb86v9cauTm8+Paz8nKzM3l
+VTovNzctMEFV38S7ta2tur6/6EE6NjY7Pj9cv72/uLW5v+BZUEk9NztITFPqyb++wsXFz2lY
+X1xWVWrg08vX7nA6PFUvLElUQ17Bvbe0v7qxyFPt+zw4P0BHVVF8vMLzvrjdXd35UE1FTGZL
+UNDM1su/wczX2s7fUtPNQ1T6NC5IOys8VkJpycu5sr68ssVsz99FRk1HS09Q6c/dy7zE5NfM
+eVJXT0tJSlRteeXOysrGxcrK0Pjh01ZTb0Q0N0gvLEdGPmjJxL23t7S5yL/LUU1XRj5MUk5i
+/dzKyMfCxc3O1mBPV01GR05WWWPo08zKx8fK09PNXU74Ti44eCoqf0E0dsXWwLS1tLu7tM1m
+2mc/QFZDQVxwdfLLxc/OxMjq9+JYSEpMR0dXaX3czsrPysHP3cXfSdZlL0BYKjFeODfw3evA
+u7a0vLa1z9jNVj9JRzxDTkxQ+c3Vy8DFyMrV6WpXUkxGTVpXXena0MzJxsnPz9B0X1o7Pkov
+M0I2OldlZci9u7i6tbfDvsdcT15IPEVEP0xfavDQysTFycTK3d5+VVFRUk5NXPjq4dHOz8nG
+ytzpaT8+Qy8zPTQ6SE9qzcO9t7i0tb68xu/rYUQ/R0BBSlH1dtjAyMy/xNjQ2lhWVk1QVl1b
+Y/L/4dfX1dTW5eBlMT/MJzK/LjPDc0W+udG5uLnAzrbTTsxwPkpzPT93UVvoy8/RvsTY1tBn
+TW1TRFpqWF7m8u3P2OTc5WltXVFOSkpDREhLVlhc7+De0s3WycLNy83Y2tvq+P53/Gzz6vPW
+29/b8GBnb1FTXVpaX2xcYOju7N/b7njhbVZja1VWW1ZXW2dzbmze2fXk2fLg3X7n5Gz/42n+
+2/vX2ezO2NvU4uzo+F5gV09XVVJaVlr1dmfu3+7j2ON5afxqWmtuXWlmXnDs+OzZ39/c5+/v
+cmpuXlxkWVpqavTe3trY2trZ1dr4+PJhXV1bXV5odG//9P3e4nnl5313dGJZXV9fX215Znf1
+bn7i4+fv8O9nY2hmaXT1c3br6d/h4drW1dbX5unobG5pXlxcYF1dZnV5fPD3fXhuYlxmXF52
+aGJr/fXx39jc29ve4Ofr9vlybm1fXF1hZWt7ePHn7unm4uj37vhtd/tyamx57u/w7fR+dnV/
+bHD4e/R0XmNoXWN2bXT97+jv6Obu/Hb5fvp7bnp8b2xqZ21669/t+P3t4ubj4eXu8f1rZmRm
+X2Nra3dwbHV4dXX9+XluenJgY2py+Pfv5+jq6ezu6evq7Pb69uvxbm/7eGlteWxrevv5bGZt
+YGJub31+cXn98e717Ont7Oj3fPDv83n+eXV4c3h3//3+9Xd4fft7cnv/9HZudv9+fPr9b2lv
+fXpx//b3dGlrcnl9//jt5+fz/Xr76+/x7ebo9Pf29P5+8P1sbvXte212/WlhdW1hYmZqbHB6
++Wdsd3zm6ePe5+3w6u/17+nuYmZ4aXXp6OLnf+7ufHj79nlxZXNvYGJhd/nv8/T7ePTl6Pp1
+8/RtanN79OXtc3v9amp7//P0+vJzffb7/3X5f3VuZ2Zibnl97vX77evx6+zq5fRsa2hr9vv0
+4u/87X/8/PPt8Pf8eGpjaW94bW1mX2htcWxv8/J1bu/7bf3s7fXs6vf7++70+/B39O9v/u3t
+/Pj3cl9faWr35Xdz8urj7P7l3+r7endtbW5nYWhvcWliZP3ye2/0/mp2cXF6bWbv8Gn35uzm
+3+vu9Xb9+X7z5+91/OrufPLz8OP+aXB6aWRtZHFuZmxjY2R46XJv7OXzdu3t7W1x6/j49vRu
+c3t47nr+6npk6uL9eGJz6/1x6+36cGR9f3B+7Pxma256dGj07Xd17Nvi7uHzanX5+HP06vP3
+dW9r+mljfW5ma+TwbXHudV58/PTsfGl/+G/p8Gxue/DwYXnr6uj4ffTx7fX56ejv5OV8/Wz5
+8HV4eWNYZ/VpZGvy7W727337b/Tdfnz0cGr1a1/29G7p/md4cGvm3m/vb25yXfnsft7S52rd
+623c8Pvxamt+b3X2cmVdaW5oZXLu4/VgaOZ8ZWv67Hfl2e91bHDu+/Ls63Rr9fl7+uvv6+ru
+c2/t6X/7eWFdXGl3dez6++7o7mJ89v7xfXjq7Hnp6W7h911++/l8+/FuW1vxd3zzaX7q+fjr
+4vvu3uXr8nbtaWJ0/ut7Yufub2hoa3R8bfNq8t/7du/jemhzfPJ9+u/n7HHxc3XyZmlieO/m
++3R7am5gZmtuZXLj8e3sa2P8/WFg+fDf8Hr3d/j06+Dg7uDU6Ovva/PsbXn+Y2r0f2lfYWhg
+ZmZldfV6bvt0YWJfZ3F7avjl5+Ls7O/m4dza5uDd7WBebVxUa2xmfPLf3ft95d59/d5xcWlg
+bXB0dOLf5uPr6ffyaWN3XFdhYV5fam14+X7t8+n26dvp9v31dWpsX3b7ae3r7uTf7O3j5u5+
+72RoeXpvY25ne+HvZ+bR52vq7G5iaWFeZV3r6XJz/vVv9+j17O1s7/VkX2ZxdHL+8uff9PDq
+X2LveG10enZ5fXj49Obs5d3e9n3rcGv48Xb6521x/H7t7PZpZl9dZHT1/nF9aW1oZ3pr7uLi
+6uzq7PZ4b15idPNxYP3o7PT8/Pjq/fzlaHB4c2Rddnj/8Ozl3dbb7f57+mJpbGTtcWhvcnlx
+ePLvZ2Xv7mh27mteU1l0X27c5Ob47dzX3fzj3HNldW5sa2ptae/wdO9q6+V4d33/bGxwZGdu
+fG/p62Le3Onp6N/uZ2BweGtkcvT9aG/u7vLobXj5X3h9Y314b/j9bGf+6eXl5uHzffZue+1h
+ae1zbX38cvp5cnluZmfvcWPo6Xnv7Ovk5nR96OV/anJ8al70em3g83Hqa2dqYv13+PN58H32
+82zt+21s+/ZuePHm7/L98PJvdP/s82n/7/50/+Ld7/vyZ1xfX21vZWv78PDo5N/oefvi4vBz
+em92a2Rxfmx+63P06O3idW/0bnJmZW9ncHf67e/o6+1+7+v9a25sZGx+bWTv8O7n3d/g53zs
+73FeY29pbWB34e79++p4fOr073tueHJlXmxwb2hifOz07eje2+hubHdsaHBv/fFpburt9/Pl
+5ex6bXthWGv09Glu9/p5eeTe4Ont9PlmW2Bsbl1fdOjs++nl6f1u8PBsZm54amJn6+x2fu/c
+6fv67vNvaWBvdmlo9O7vfujh7unt3/Jr8HBfW1xdZv14b2/66+zu6uXc3fRqaWdiZGt07/xs
+9O/77+Xq8eb38vlcbGltdWVibO3+e/bn4Hxx5+l4fXNte2Ffef7xaO7f6N/d2+p+aPn8XmJt
+bmllavf08f578ORvef50b3ByXm38bH7f3OTj4PPseXVxdPJoXmloaHd06+b7297v8+zd/W9q
+ZX5mYHDiZWBybepwbfV3bHL6a3FwbG905e3u3eDo9HX45fP5dWp5Z2zs+ft5aW1tfO7pd/rj
+4m9eXmDs6nX27fzt5+5vY2ZsYl1t7+3r6Ontc23+/XX7efLvZXHsamBqdfp8/vnr53H37Plt
+bOHoZmn97u/0b2Xy6vJ79vXu9Wh1bWBhdO33aXLc4Xfl63N7XV/q82507etjWnHyd//97enx
+duvnfnb692x0bmr8aGr9f+rn7fDd4mVgcP5ue/ny6XZv+ejub3L+cmty9Gtm9uz1amj15uz+
+8u3x7/B1bWpnam5zafp1Z//4cm51d+7m6u/37Ozu9W9q+evy5uLu6uTl+v5uZHj6aWp+Z2vy
+fl9eX2Bwc2N45/d1dnT24+/r5d7i7eP28//86ezg6HJubG1tbXhuav99939vb3L6cWl77vZt
+8Ot4cXf5/3jy7ON9XmX17W9vffL/eO7v7ufq5uT3fHR89flx+un3fnpsZmr/b2drbHh+9fN7
+cm55ffbp7vLr7n5783119n3y/Hn66OHv9XRpb3Z9d354fvz98P5tcff09vT57X50+nVtZm5v
+a2dfbv326u59fvju6uvp3u7u6e7l73lvbGlqc3r8/PXy83VoanR4Z2ZpfHVvfmt68uzo9Hx9
+7uXleWvr7PX3dW5ydnj57fx3bmppZGlubW598vJ+8evy7nxvfPj3/Onwc/Ht9ft3e/Tx/nn8
+9O3yent4a2hsb2lrbG9ubnh4bnj4+/Ln7vv97d/h6/n49WppdXRtb3FubnH99+/v9/n+/nBw
+//r7+fN/dv56dfjs5/B8+unn5urz8vj6+v1rY2ZoYltfb2ZpfPn2e/rx9fl7c3duc/Py7+jn
+7u3r6Ort5+Pk7n9rZ2xmZmdreu/u9vjr4+14bm/16Oz2+3dobHN+e3V0a2Vqd3597enu9fp8
+b3N4+n5ubHT6dm1rbvjv6+/y9O/x6+js7O/v+H7x9Hv+en17fHh8fHZ4d2Vjb/fzeW9ue31z
+bW1ta3T7dn3+eXj/6OLh5Onn6PX97+vx+nxrX19ka2lpamz9+fr0/Xzx5+r3d/nx8+zv+3p4
+enh+fvbz/PL07fp0a2tpY2ViX19fYmp1e/Pp7/717u/j5ejs7Ojt+vPo6Oz69+jm7Pp8dGdj
+Y19dYmVjYmZse/T+ffx7/vPq8nZ1+fDt7PDu8P1/f3t9fnhtdHp2eXhrbXR4fHd/fnrt6ezv
+6unv+ft+e/748fh1cXH9/3l+amJpbHRqZmt18fV//fr2/ff49O/z8Pd6endxb3jy8PTw7vz3
+9nx6fvPn9XVqanZ0fPj4fvju/mxnbHd1b3n78/p4d/Xz+PF8bHj9dXZ5fvX3dG1x9H366PT4
++vTucWhsbWxrbf3x+/vq6Onq+Hx3b3f++fp3dXNscvbr7/rw8fT++Xttc3p2bWpjZmdkb/36
++O/r7O3o4OTr7e/0dHN9fnRubmRlb3l+eXJ7+e/x/nxydPn0/vvv7ezwfHz47vRyb/1tanN5
+enr29H1ub/f5fH56cnN2cHV4/u3q8uzl7H379v5xbF9dX2Fvev939uXf5OLf5ut/b2ttbWhq
+aG14/Xdod/7/8vX87ur2dmhs//bxdWp49n13bm9wd/Xm6Ozn6fp89fTx+XVqaWRjaW1vc35/
+8/Ds6ux3bvv9+3x2a2pydG50e+/o8v3z8O5+b3FsbX71b21yefxvcvry+nvw8fDn5+389u7s
+8nxtbP5vcPf1dm9vdHP77/p/cnp+bW1tbndtb3t7ePP5fHZt8/L/+PTs7PX5/H/05uj3/flt
+aGlp/PTwfnF4dH3wfnR7/PR6bG7w6fj67ejr6OXweXB5cmt3bWNdXF9leHhvc3b36vL2+vbm
+5OXq6u/x9f3z8/r89vf37ufs+XtzeGtod/32fm1sYl9we3FxdHV8fvv18+ro6e35+n78eX3v
+6u3ye3l3fPf07fj9/WxlaHZza291e2xrefj28u3v+Xt8+/fy5uLd3OLw+u3+fP15dHB6bmp+
+/vxtbG5teGpkYF9oa3Bva3r19/Dr6N/c3uTo6ejk4+Pq6ex5b2tqZF9fX19gbG9qbHfw7/D7
+8+97+uzn6/Lu8PT2+vx5bnrv7vn98O3u7Pz+ev12bGddXWFeXmVr+/Hj3uTvc33x7OLg5Ort
+6/B+f313+u37fHb96Ofs+XdvaFpQU11kaGJhaHLx5OHs6N7f7PX4fu/q6+zr5unt8PPq6ep3
+bHBtaWlnYWdz9/R2bmx1cXhvb/15/3dsZm/t8PXt7fH+b213bnH6+fXv6fD9f+/s9/j17ev2
+9u7q6uzm3+TzemhfX19dW1dTWV5k/erk6ejufnFudPHu69/j8Hp2b2xtcfr2/Ozi3t/f4Ojm
+5urm6fPu8XdpWUk+O0Fb387LzdPdd1JOUF/czs/X3ev27/fv5uft7/P79Pr06ez0dF1SUE9T
+YHTr4+d6Zl9hbuzd29ra3uj6ffHl5+zramJq7uN9XlRWXWh3c2NleW9hXV9y//zq5ufj3N/t
+8P3t3drc4uDe49vY3eXr8Hh98/lnTj42MzpO1r6+w8ziXk1KSlzXxsDI315bYnLj4vHv7u9/
+cGx56OLs/2xbYXb57fp+6+bxcGJmfePZ2+P58enh5O33SDAvOkrJuLrDz2FMU0lN4Mi/v85d
+SkxOat3b2OB8YGVvfdvLxsncWUxSYOXR0dnj5vNgWllvfVtfXU86LjU/3rm1u81pSU9aTFnn
+zb67xv1YWmfk8Hb6dG7y6vDf3NrU2mZNS0ti19HP0tPY62xYXuLc72dXam05LC83+7azvtRk
+Um5rSUlc1Lq2vulSTVJt+nhuZ3zZ1Nrb1dLQ21tLSVXfzcvP3et7bVtZeebV2l5NVlY4LCw1
+6bOvuNBVS19jR0NUybOvu+JNR1Xo2OPw6uXb8GJt5szF0Fg/PUh719DQ293T31pUd9bCvtVO
+SFpONSsrNdy0sLzWXVdyTTw8Vb6urrtlQkFY3dba6OjOx9dbTmXLw9VbR0ZU7d3s8OjY12dN
+SlPYv7vDYkdHVTolLDjZrKu012JDP0w5P9G6r6+/Uz09QfjK1etmatXH0N7T0NPgUUVLWujP
+z83O0dPfblxr2svTSiwgJjrLrKyyx1w7Mzk/a8O8usPsUUhR3L+7yHlNTGD/7dzMwr/E3Ew9
+O0vTvbvMXUhIVWJ94M6/wMn9SENGXz0jKDBMrKisx0c0MlFJTMy9raq3WzUyPNi6wtNfTeLG
+ydlmXdvF100+P3K9u8hiRE7cysrR3NHZTzQkHyxXsaesvlJCOTpCPVXAtK66Zjw/9sS5xFxM
+U+vL2U9O1725w1Q5NULXwMTuV13h0PFTTl7IvL7KV01NVzkgJC1Oq6WqwFg6NEk/Q866rKm2
+UzIyPtm8x9v1bsvG5VlOXs++zkk6N1XAvcdnSVTaytDR3tHG+T0oHiY/uaWlrtlFOzdAPUrJ
+ua+zzkI6RWnGxN76aF91ak9Q3MK6vnxEOj9uysTVZmHuz+FhXXzCvL3IVUY/YV8lISw0uKep
+ufdJNEVCNFPJrqSqwjovMknCw8rL2sjA30s9Q3+8udNRPULn0M9vTFvNxdXbc1/Ux811Pywh
+JDJtrqirt3s+MzQ4P9W4rKu210M8PUzPxMbN3fHo715g28nC1Us9PU9j897ZwsLPbEVKfMe6
+yOFaRUo6KSIqPsKqq7XJb0I7NzJBxaykp7pHMjI/2snDxsnByPVBOD7vwMHRVkdITlhXYd/F
+v853W2Xnyr/Ya009MyYnNv2vqq3BWUc5Ozs+5rytq7bcPTU6U83KzdDZychrSElmzMnTXU1b
+XWVYZNnKv8TWb1Re08zTaklFQi8jKTPorKmuxGw+OkA2POq3qKeveDUxOWPKxMbOycLTTTs8
+W8K6w+xMRkZMT1nfwbq+12JKU9TLzd1cRTsuIyg51a2pr8lgQDo+OUDbtquqs3I3LzdbxsDJ
+zsjDymFCRmnNxMruVk9LTE9p3MrEze1STXPaztFrU0g3JycwRrKqrbnqTT1COjVRx6ylq75E
+NDRB5M7M09bEwdROP03mzMjfV0lJTE1VctDDwM3vZVt+0snRWEhBNykqOF6yrK28d0g8Qjo6
+XMSuqq7GQzMxROHQyNLTx8XaS0FCYszFzOVyUkxQW+HLwL/P719dfubP3elfSD4pJS0+uq2s
+tstnODc0M1G8q6itxUAzMz1zzcnIw76+2Ec/RmfNwspvT0VGT2bTxsG/zOhZT2Td09Xfb0cy
+JiYvTbisrLjKXj03MTZQvKyprL9GNDM+ZMzGxsG/v9tGPD5az8HI5GRJRElQ6Me9vtFwSkhQ
+ZMzDx8x8OygiKDXLramrtdQ9MC0uP8Wup6i2YzcvNUrUxb+8vL3NTT07Q3fJxNV6TEJKVe7Q
+xsTK21ZGRlTbwbzD1l02JiQpN8Krpaiy7TQtLDFeu6ymq75NNC40S9nDu7q8x2pHPDpI4cTD
+01A/Qkt+zcTAxc3tTkdLcMa7vsxzPSgkKC/nsaeorb8/MSssQM6xqaq03z8yMTtUzry6ub3X
+Tz89RGbNx8jeUkxLU37VxsTM51ZLSV7Ow7/K00opJCUtdbWpqay6UzorKDRrtamosMxINzY7
+RF7Mu7Kxv1k7Njxb1tXW197eb05JV9e/vcZ1R0VX18bF1GpKLScpLVe8rKuuuFw+MCszT72s
+qK3BXz45PEJL98S4r7jbRDk6RXHl3tbi3f9RSkt1z8XD03FbadrKyNX8Sy4oKi9Ov6+srrbv
+QjEqL0q/ramtvfBFPDw8P1bPuq+0zks7O0FYe9zMzM3bWk1OYdnMytbu/XPi19LT7UQrJysx
+7bqwrrG53EsxKS9Gvquorb3nRT48NzhJybOrr8dUPDo9SVJp0cvI1VdKSl/h1MvP29vX1djc
+6G85KCktPsS1r7Oyu91NLSgvSLysqrDA205CODEzQsyzrLLE8khAP0BIaczFxc9tUU9ZZ+vZ
+2c7Nzs3V1eQzJSgpPMCzr7Gwu8xvLygsOMmuq6+4wONVPjAvN1a8rq64xuBMPjc1PnPDvsLP
+7u72Y1Zb+9e/vsnc7lAsJykrTMW1r7KywctZLywtOtm1ra6vus90QTIuNUfNtLK1u8ntSTYx
+N1XQxMHL0dTff1VPZ9PCv8LNPSopKi9M0byzrrG7xks0MC84U8e4sa2vuMtNNi40PVbLvLW0
+uMB/PTEwOlXRxcPHxsviYEpNYNjHy8vePywrLTRM07y1rq22xE81LzE6T827tbCwtsZdOzAy
+N0d81L23tbrOVz06PUJf3MzEv7/Hzdfp2OJNOy0oLDdPzry1trK3yH4+MjE1PlPMu7Wvr7nK
+aUI7Oz0/V9K+t7W7yW9DOjk8RVXtz8fCxM3Z7/xoXFxw2s98NSwuL0Dfxby1sbe9zUc5Oz9M
+79rdyr26ur7oQj0/Q0tVWXLNvLrC3F5abvLpbld308jCyfpLOC0uMThJ9MvBuLa6usLhWEA2
+LzM9T867tK6vtb/RWEA8OTo/TuvOwry+wcvtWktDPTxEVevOxsPBv77Dze9ZTTotLC0vQPPL
+wbmzs7O72VNHQ0ZRTkdQe9PFwszl3NHO0HRMSE5v4+Xy9NPGv8HP6X3r3PRTQzgyNz0+RUpW
+btTBvbq4vL7H80Q6NjM3QVjXv7azsbS7wtJjSkI/QEZKT2X439LT2ulzd3ru9GpnZ+3V0c7O
+1N9fRDw4Njo+R1Ft18rAu7y/ytHX19Da5vhhWVJLQkBJWOjRysbEv7/F0nJaWWV0bF1VV2Jx
+cGNXTE9bX1ZMRUFDT1/12c3Gv73D0OFnXWhsb/vl4tvT2eF+W1RRVVlk9+HXz87T3/1mVVJb
+ZWp09fLf1tDR1dnf5HlUSEM/P0BBR1BccNrM0tXT19fOy9La3uDd4exsbHj76+zv9eXb4Hhe
+WV775ePi8Xl883547urq7vX/ZllOSUdGSUlNWGfr19nk8ff7+vXx59vRycbIyc3W5/htYFhU
+X27x5OLj635kXlxZXWpze+PZ2uLu8fPxeW1mW1FNTUpJTE9SWl9h/uTd2NTS09PT1tnX2Nzf
+2tba5uzu+/j2fm5tc/f2b2NhWlhbXGZxd/Hd29zc4elvXFRNSUpLTVdib/nq5N/d4fP15uPk
+2Nbc3Nvb3t3j7f797ujm7XxzdWx/9m1jYWp58efn6fRvY2h3dXZ1bWpmX1pSUlRWWVxdZHF2
+/ene4une2NjW1dne4uLk7Wxhcvnu6ufr7eLh5e79+O7r7+zi5ev2+nhrZltOTE1NTk5NTlFY
+W2Z48ubm3Nzd293g6+ni3d3a2+Pi3d/h29vb29ze5ezpfWtnaGJgaWtsZmp3bnV9Z1pST0xK
+TUxNVFtdbOrd3tnU19jV09XY3d/a3ePc5f5/ev11bnZybHL46uDf4eTh3+Dk73Zub2pfV1BO
+TE1NTVBVWl9mZW346+jc2Nvg3tvZ2djc4ODj4uPvb3js9f3v6Obn5+Pu/nb+eWhtfHp8f/x6
+bmVdXlRLTE1MT1NYXGh979/b3tzV0dTU1Nvg3t3v+vt6en35+v/27+3h5fT7dmxufv17dXt7
+fvh+a2VhYmdkZWlua19bWVtaWlpZZXzn3t/l4NjT2Nvb4eLh5+fp7PHv/nh3e/3+5+t97u7x
+9fd+b2dlZF9rdWhofG5namFmZ11eXV9gXl9fX2V7+uve3Nrb3Nzb293d3t/f3uTxfn14ev7+
+fHJ1c3BucnP78fZ3enxubmllaF9iYVxcX2ZiZG98a2doaW5tbG786+Pg497f3dne6u7i3t7g
+5eru6ef3+/Xx8W9ta2xlYWhlZ3Fydvv7e/D9Yl9gZGNhX15ja3V1a2/1+P96+vl2/+3i4+Pm
+5uDm6uXp7O/t7e7z6+Tv9vzqf25wampgZ2JkZ2Jt9/dsaWht/O3r9/zw7v5ra3Bzb2Joamhm
+bnd0fnh2/e3r5+Xp6+jg4+vm6ffp4+p8/Xh99vf++uzz9fZ3ZGJtZl5dYWBjYmhyem1vbmZi
+aXJrbv3v8+zi4uPk397g5urs7Ovs6+jw/Prt7vl9fHR0cW9+d2x7d2NfX2BfXltofHl+fv91
+aW1ta3bt6u/p4Nzd4ufs6fF2dHt2f3/+dG9yd3ZkX2dtb/fy/v7v5fDz4uTl7Pl6/f57aF1f
+Z3V9d3Zye3x/cXFtamxvbnP18Onsevrr7Ovq8f59eH19fnf37PXt7vh9+u7r8m1qcnB4fHv7
+enR6cW7/8u/6dHV5fHByc3X6enFxa3Xv7Hl2+/P6/vd2dndzbnh4+uvvfPbs5uLn5Ojv8+32
+eHVrZmVm/f35dXju+Hhwa250c/p7aW5zdm9sfnt9+O33de3o7fp9+/X1+/n+8vZ9e21obnt+
+fnF+/mho/+z5d3p+/Hb68n/46+Pn7/Xt6/H08u97cv53aWpxcHpxaHF+enZzbm5783tsbWVi
+Y3N9+fb89+nf3+fl6v15eX5xcf77/+zp5+Xy+Pv47e38bWdv/3Vv/vbu9WtscW108HhnZ2pp
+anD56eXsenr69/jy6u7x7u91bHV99fn9+nRna3b9emx6+Pvw8P3//vn37vDu7/d6dnn87Ono
+6P9v/u3w+nlva2NiZmx2eHBoZnN/8vR3a3D8fP3+/vnq5fDq5uv1+ezn7fV3Z2lnbnZvbHLv
+5+35fnt49v91dXR9b3F8eHzw7/f2+Xxtand0dfh/dvzu7+/t9u7q7u7x+3txZ2dvcW5wc3B6
+/H188/H+fHn8f/34+nd09Pv9eXf07+z5dn/y7e/o6f15fntpZHH/eGxufXZ29/tzdndvbm1y
+d292/Org4eXo7fb37uv1end1b2ZqfPj8+H979e/r9XNwanh8dvl0al9v+3tubnh4e3f0/mhy
+d3r08enm4+Df6e/u8Pd3eGptd3f+8PD58u7s7f5va2hobXJyZ2RnaWVrff/6+f1++fTp8f9+
++evo6+7r6urn8XpqZ2lqa2x7/m1ucnl9/Xz2f/zv8v737Onu+Xp+7/L6fXVxfvtzam50bnNu
+ZmtzbGRmdPfy9v1wfPDo6ezo6OPm6Onm6e3v+W969HxqbnFvcW91amloZGdvd3FvaWv+6+Tk
+4+3u6u3s9Hxucf/17/Ls9mxmc/T+e3t7e3Jyb2198u/88u719Plyb3f+/nx7cWp0/Pj27+76
++PP6//z7/v5/c3V2dXN+fnZwfnxzdGxx/Ojg7Xz+/X54fvz9dW15fPLu8vfw9ezm6fN0evv+
+aGdpaXX8e29vfPb+bWFibW95/nz48ezi3uTr7vZ0/+/6bmx5//70/ndxdHJtdnd4em9lY297
+efv19Ovp5ujx8u3n+W9zfnFqdXBscXZ1bnR87+z7+fr8+Xl7+vh+c3Z0efr6fXBvbG1wc318
+e2/+e3j99Pj88u7s+nb88X//8+rz/H5vamVrbXBtfPf4fXF0d3z97u74+H3/c2xvd/rs7n35
++PPr6vP5e25veXv59n12fPH2fP35enp3ef79/vx0bvnx9H1+fX7/dnVsaGxybW978uzu9PTv
+7/P+eGpndfn4/fr08+rn8Ozv/nN1f3Jxdn5tbHnw8fr5fXdvc3Vuand9ffvv6ezv7PB9+/v+
++vd9cW9vbnn5/XZ0enNvcX54cHv5+XxvcX379Orq8fl4ef9/9fLxfm96fnN2931tdft8/3pv
+bf348u/zfXL08X787ez8cGdrd3t6/HFsfPT+cn/5/f79fH58cG/8e3vy+mxz7/L78Ons7fPv
+9H3z9/H5b21udHFveHt7fvz1/3349nFqbW1wa2z7+Xv39PLt7fT5+337/HVvd+/xfHl3cHJ9
+8Pl4cf/3e3zz9X178evu7O9/fe7u8vF5a2tmYV9ocW5saWhufPf2fvHo5eft7+vq6+jq8vj8
+e2hlbXj5d3b58/dxb3x4d3t1bmtsaWlmanL87/Hr5evu+fvu6Onyfnn47ev1dnn9+X99ent9
++/xzc21qZV9fZm5va2x2/nv07PXq397f6Ozp6/J+fXh7fm9sdHJ1+3tz+PD2fv/s6fpwbWZk
+a2xnZHnv+n5zcf7zem5ud/l+fnvx6OHl8evq6+/9dWtqb3BuaGl5fvry8O/9cHF4fHd2bW93
+evj0+Pnu6+zu/vXu6+39d29nZF9ham91fPft7vP4dn369ezz+HR/7fJ5b3p7dXVuc/12b3t3
+fvbtfmz58vnv6ev28O/u6+vq8PtybGtua2RmbXRubm1rZmhlZ2Fgc/p0cvDm5d/i6Ojf3ODn
+6enn5fF5/f9vdW9mZGNteG9tdnxwa3R9dnB3cGlrdvRy/fDr5/N9d/T6+u3u+m1vcnN5dO/t
+6+3v6n1+8PLz7+rs7OrvfnFtbW1uY19qZ2Nud3hqampsdn7393n66OXk7e7o4uHj6fj9dHFv
+bnhydXFsdfbvfW5pZmVrem1z7+7s8O7o4+Xs+HZsbG9wZWhwampsZ2lwb33v6eXk5/T+ffft
+6+7x/Pp8e39tamdoaW90/v14d/Hq7Ojv9HtwdHx+/fv57n79/HZ0bHNvbnBqbnN4dHf6/e7u
++urv/O/qfXd6c2pn+ft29+vx9evr+WtjbXR7+n1//fx0e355b3Fua3r87+58fvH3+fP+eHL7
+7/Z3ev5yc2xncHv48Px3/u7s9f789H57ePzs7fB+/XJ09u52a29sbHB5ent+fPz0en31f3b6
+9Xt7cXr5+fry9/vt5uzwdnl1bW9wdmtqcfrx9vr5fW90fnx0dO/t8vl7+O/v6uvw9n7//nx/
+/ntta25sbXZ5dm54fn7/bG/08fj28fp7ePrv9fv4eGlpa3r68+rp8/x++/78++719u7q9GVk
+Z29z+efw+H57dmxwd/1+fvzv7vb5ef5va25rdWxpamn47eXt/fDk5u7n6Hh5+/z+bHH07/X7
+enZzcn5zeHNqbGpvdHbz9ffs7fZ9dnr5+3x7bXrv8/l893388fh3+PF6dXd1ePn1/v7z+nhv
+evp6cHJ0fPTu6vv78efk7/P7b3t1bHlpZmx0dG11+XN3eXnv7/Pp6+/3+O/z5+X6eWxy+Pv8
+aWRmaW16+3x1+3xx+Ozvent5fvnv9/16e/x3/vvw5ePrf/d99+t3bWlse3Rwc330+Pf9fXx6
+bm1ufn5scHd96N/tcnV/evR8d3tv//708+rk7f9samhqY2j77+bt7PB8//f5b2hz9Pv1eGVo
+bv7xeGtz+Pf18+3o9f3k5/12eP56bGp3cWxxfXp1+ebvdnh99vHo7Xt4dXx1/v/49/5xc+rp
+7/lvamlu9PNvaW55c3h++3j58vh7bu7r7/h+bG/v/Pby++zy/PdzbG9sb/Dt+HF17PV4+/38
+fHt++/3r7/11cvvw7e3/dW1mb3N7cmhycW/9+v7+d/359fP/evl49+jw7+38bWlsef367+/z
+9Hj++Hz08fh3cnbw9nR98uh+ffd78Pt2/X9z7+/+d2l2b2NfanJzcmxya27u6+/4+uni7G9t
+8ubq7/Pn5+He6PdubnxzaWxvZWptcXn+/HVqbGpp/3z6+vvy6u/r83zz9v/37e3s+397dXF7
+bGhsbv98e3hy9+Xg6v3y7/BtZ2995vZ19Ptqc/5+f3Vu7/h9729xePjt7eri7v5+bnLs+nj3
+bW5qYW9uavb2bG9ue/xuZ3Tu6+ns5Ofu5ef3cXl5f/Tr5vNpZ2lx7/P6fXh7enFte+7i9XFu
+dG9qb3R9a3vq4+v39PPu7nJsb3X393Fmcft7bmp3eX33fnJp/Ovi6+5+ZGxu6ub59Xvz6en6
+cX/98vf8a3T6eW9ocHf/9Ppybnv76u9wbHTu8fl8enz9735pamFhbF9tcW94/379/3z36OHi
+5Ojq8eHd4ebq6ul6YW93b3ZoYWFs+XJhXGBtam1sfmxz+n3s/f7+/Pzn3+jvcHrs9fR1/PNs
+dPf3+Xx3+vx68Px6fHr+9+3u83V4/XR3dmlpeP18dG9rY2ZtZXnx7ubl7err+vt0ff11+/t+
+7+39+P306ntvb3t1X2JjaH7t6PTy+Onn/H5+++55fff+7/d2dHr37+9qaW9pefzv+XRtcvv4
+/2pv+fXj53T98fbv8H5v/v7q7O/8bunqd212cG5sZmxqcvLp7H1+5eTw7fVyaW725vFz+fT3
+9HhtYV9x9PljXmn+6/rv7OXm5ep5bHTw7OP0bm1p79zmcmp66/JqZW1rbvf2Z15m9/R4fG90
+9u3ufXZ09Ovp8Xno3t/5dG1s7/ZtbXB99PLv/Hl2bW56bGh0eXhwbW50d/re3uPn7fN99fp9
+dfxvcftxcf318fb37X1vbW3//nN2aGTw8Wlvdffr+e92ZvLf4O1lZ/L/8en+fPJ9++n5/nVl
+/e//7HVhcWt/emhzaGP+6n1z/O7n7v3v4uXm8n3k9Gp4dv37cWt0Xl1vb2VnaFxr+O7mePjo
+6uPj3ex7fXD79uTi73hwbm197fl7/HJeaXdnZnT1+P/59fz+cHr5b2j46fV8ZVxk7eHl+nzr
+5uzu7/B9b3bxfnp9aXj++fFyann58vj57vz+b3Xrfvvldmpxc3z76eJ0ZW1uff5obHl+8PHv
+8nXx8HBua2717vftdWlpdX3p4O7k6uv5den/duDjeWxpaGZldG1ma2r8/v/9fXdtfnl5+Xv9
+9/F/c/fm5ebr7vrp4ODuc/75/Hj3fmp1dm9t/31xbGluamFeXm/y+m9t7+3y7unq5uDo+n/y
+/e/6fP57bv3p9u7we3JzbnN3/vv7/3n8+/n9e35uYmJw/PLv9uzy8+vt8u/o3+x/aWZtc3L7
+8fX5bWVjZ3H7/vn8fHz78PDt9fZ3cP71+fv8dft+e/lsa3R69+7z7+fv/fr++fbz8PLs+f9y
+ePN8c3BtZmRrevv39e/y/ffz/W5tbWx2bnNtavz77Ojp8O/t+Oz4f+73/fF1ffNxe3p++f97
+/Xh2b29nbPzu4uzq7nd78H939v74eHz4/vl3bWx3+X1vb3j3em988vl8/Hv7/fDr9u/06+t4
+fXZyem76fWpxeHN/e3z493596+z18XZ0+nx8+/r//X19eX7y+HpsbHR5fv7xfmz8dWFmcnp7
+/Ojg5O3yfH359nxub212eHJ8eH1vaHzt7Xp8eG51evbm7Hp2/u/t9PN5cvTq/nJtdnxranVu
+bXVvfX1qanb98Oro+nTp5/Py7/14f/Dq6PL/a2NqePHw7e3t5+b7Y1xTWGJmb/rs6vpoZV9q
+euvf4Obr9HX+6drZ3drX19vk6/Ph3N3oXExISEU/OjxK3bu0t8lWPjpG37+7vtVeTExUZenc
+297c62ZWT1Jv2c7M2nlVUlx72dHW2tv3YGd73dDO1OZnWVI4Kiwy76+mqLpDKygy6LOrr81E
+NzU+eMG3ucHpSkJM27yztLvTWVzVwMo/HxQYKK+VkJrMHhMZL6qamqdUKSMrPc68tri7w2Q9
+LCswXa+ioq5UKyYu/q2kprL/OjZJvqut1yYPDhc7mo6QojAUFB/enpmetDopKzVa0MK8u7nO
+SjIuO8CooqnPMCUqR7SmqK27y+5PJQ4OEiyYiYiQ7g4ICyGfjYuUvx4VFSFEs6iprL9bNDA4
+1a2mp7s+KixEtKOhqLxaNDElEBEXKp2MipDFEAgKG6eNiY+uHxEQHmmmnqOv6kI1OlLHta2v
+wlI1PuSzqquyzzovIRAPFyefjYqOqxkMCxbGlY2PnzgaFBos16qhpa7XMisvWK2gn65IKCYz
+waiiobFKJhELDxnHj4mKlT0RDA4grJeSl6w/Jx8hJjjVrJ+jskcpKEKvn5+uQycnNLafnaRf
+GQwJDiqejIqPrCMTEx5Qqp6fqr1pPzMpIic+r5ycp00gHjGumZWe8iIbI+WinKNJEwoKEWOU
+i4uXYRsTFynBpZ+hq7ryNygfICvZopuerT0kJj2vnpymvzkrNF7Qws0nFxYTILKckpKlQyMb
+Hz3Br6mttbt2NCkmKUazpqKq1D00PcCooKKwUTAwQNbNPR4SEBg9npGQmsMmHB4vy6+tsLm8
+u8lFKSMnPLOhoKnTMCsyfKqdnaO6NissN005HBITGV+Yj4+bSB0bIjm3r7q8wbmwu1UtJCIv
+2q+jpKy46URJ2LqtqrHLWjsuKxwTExYrqJOOkaE3HRoeNMy1sK+ztrTLRzgsLTdD3byvqqan
+rLTLWEdFS1rR4EEkGBIRHEqhko+WpfIsISIkLEPJqqCepLhJKyYoMEvWt66sqqqrr7jHajwr
+IR8hJi45P0ZJ37iqpKSrv0cuKSw39bepqK25ZjkwLTE8WMWxqqinqrPK7WJJQDcwKyMhIB8o
+OeevpaOkpqy91T8tKSoxV7itqq261WdQTlNeec26uLe6wcTP+Us5LScjIyMlLTpfuqymoqKn
+rbpmPjIsLDA9XMm/v728uba4v8/p6tPFxcxoRD9DXXRPOysiICYvQ9TEvbm0rqqprrjPTkVH
+XW9ZRz07PlTKurSzuL/Fy87TaUxGRUlc6WlENSwqLDVDY9jT0M3JxcG8ubm6vcXT6VlHQj1A
+T2rMvry9v8PGyNvraFhOaNPQ3lQ+NC4sMDpEUmzr6Oje1c3Du7e2vMrpU0U6PFjbxb6+xs9y
+UGzX0snHytDa4NrU+VNHPDk6Oz5JTlVXVU9PWGjVy8fAws7mXk5MWfbOwL7AzOFx99/QysXI
+yc/oW0ZFRD9CSE9f5NXQ13tYTEdISk5QT1Rb+9zT1OpuZ+/Qw8DG0ehmYPXUycbCxc3eYU5J
+SE1TaPd7dGFfbeng3uVjUExMTlVaVVFQS0tZ/NTLxMbO3XZfa3fp0tHX2d93ZGludvfo4OLj
+3d7U0NTV1c/X6GVZT0tMSUZEREZJUl1peGxeWVtk7tnQzMrL0+huYXLu49zW0dXhatvA2+7W
+yc/LzM/N5mxZUUxFPz8+Pj9FR0pUVVxt7dzQz9ja5fr4bU1D6NN41sLHxrq7vb7Bw8bGyM7d
+6N54TjsuKikqLzU7QVrLubCvr7W9y3xKOzIuMDtYz8C9urezs7i+x8nBvLi7wcv6RTsuIx4d
+HSY66LeqpaKfoqq4TS0mIiAiKTXtsKagoaivvuRPQjxG+sSvq6+zvmozHxgUFRsoWq+hnJmX
+m6K4OSMcGxwfLEi3o5ybn6nBTDguLTFDyq2ko6asu1MpGBIQEx02vqSbmJWWnKxNIhkYGh4s
+ULmjm5qdp8g6KyUnKzfbrqKcm6GqvTofEg0NEiBcqZuVlJSYos4qGhUXHChGvaidmpuhtz4o
+IB8kLUq4o5uZmp+uzjomGQ4NEBg0rZ6YlZeZnbI5HxcVHCc+vKuknZygrGkrIR8jL1q9qJ6b
+m56nv0AsIRsUDxIcMrGem5qanqKtSiMaGBwtYbmqpaKfo7NRKx8fKTfos6minp6iqrxJMy0q
+KB4XGB4q06qnoqCkparMMCQeIC9UzretqaWnuE4uJSYwSs+3raeioKWuxlE5NT87KBwWGCA1
+xK6qpaKhoKreLiEfJjdczLmwrKitxUMsKC08eLytp6Sgo6y8fkE4ODEhFhQZIEevrKafoqGf
+rk4sHx0oPPu8tLmxq7TMRCwoMEXOsauooaClrLpZPTYrIRgTFh0vv6umn56fnqbMMSQeIS9H
+27y4s6utvOs5LC88aL2wraikpqqw0Es/LyMaExUaJV2yqJ+dnZygtj0mHh4oN03Qu7Gppau7
+Ty8sM0HpwretpaKjqLX0QC4fFhMUFyNStKObmpqan7dEJBobHyk9z7qsoZ+jrdc0KyovQt+8
+raSfnqKuzjkkGhMRExkmWqyemZeYm6O+NSEaGR0oPsivpp+fpK3VNyspLj7qvK2moZ+lr8o8
+IxkTEhUcKmOtn5qXmJujuTojHBodJjVvu6yjoKKrvk41MDZEfMCyrKinq7THTCgaFxUVHCs/
+uKKempeaoa5cKB8dHCEsOOqupaCfpbPQTT0/T/fIuLKwr7TBWzMhGxkZHCY1abOmnpuanaa6
+QikiHyAlKzbls6ehoqq2zGpdbXjdxru0sbTA+T0qHxwbHCErPMqupp+dnaGpvz4sJiMlKSoy
+Wr+tpaarsr7Jx8LGyMbFvri5yk0zJR0cHB4kLUDIrqSfnZ2irMBELiglJigrM0/Crqemqa+7
+w8O/wMHCxcbCyl89LyYfHh4fJi4/xa2noJ6fpKu4XjctJyUmKS9Gy7OrqKmtsLW6urq9v8DH
+32pINy4kHh0dHig2aLerpaGfoKWtw0YvKigmJys1TMayrKmqra+0t7e4vMLExuVQQjUqJB8d
+HSAoN922qqOgoKKnss5GMSonJSgvPfW6r6yrrbCxs7O0uL7Cw9FcRDgsJSEdHR8kLVS/r6Wg
+oKGkrLvqOywoJSYsOVjCtK6tra+wsbK1trq/v8PTWT8wJh8dHB4jKTfouKujoKGkqrbaQjAq
+Ky0yQ3jRv7m2tLKztbW1uLm6wMTG31E9LSQeHR0gKDFIyrOqo6GiqK/EVjsyLzA3Pktg4M3B
+u7i1s7KztLa4vL/F2GlKNi0nHx4fIicxROW3q6ejoqWsuF0wKikrNEvlwrm3tbGvr6+xtbnB
+7lBOQDk/S05aRy8qJyQpMTc/WPnItKupqrHOT05UX9rbdGBiadjDv8LCv720r7S7x19CQEA/
+SE5ISz0pJCclKjtBQ/nWyrOqrbCwusa8vc/WYkBAS09p0s/MvLOvrK241VlEODI1NjlNW1ha
+OionJikwPUlU+c/AsKmop6mts7nB2lU+NDM5Re2+t7Sysra5vt5NST46PDo1LigiHyEoL0LN
+vLSsqaelpamtsr3aaks7NTQ3RefCu7m7wL/BysvnRTAmHx4fJS07/cG3rqyqqqussLW8x9hZ
+Pzs8R+zFvL7L31tOWlRHOi0pKCkuO07dxrmxrauttL/TdVxSVFBOWGbexLewsLa+3VJKSEQ9
+MCciISMpOFjMubGuq6mqrbC4wMrZY0c7MjI8Wcu7tLe+y9z4fXNSRTsvKCQiJS0/8b60sK6s
+rK2trrW7wttXRj06QF3by8HH71dPVWFYSDkvLCknKi41Q/e/squoqamqrrO2vsngWUY+R1vx
+zsfQ4+h1aWBJOCwkISAjKjFA+ce5r6qmpqanqq2zvdlOPjo/Unncy8nP0NntaE06LSgkHx8i
+KC9G0LerpqWkpKaqrbK+1XJLP0dj3srAxcbC0HVPOCkgHRwdISkzS8y4rKSgn6CkqrC5xuVP
+Pjk8RlTiw7q4uLvGWDQnHxwdHiMrN1PBraWgnZ2hqK+6zl5EOzk+PkJY28C0sr5XNisjHx8g
+ISgwRsaup6Genp+fpK270kw5Nzk4OUBPft9MODQvKywvLi4yOkzJs6yppaWmpqmxvMl8VFxT
+Qz9ESDwtKCopKi81ODtK+8CtqKimp6qrrLLE/01DS25xUk1JPzMqKCYkJiouNkNpxK+moJ6c
+nZ+jqbfoRjkzNDk5Nzk3LSkoJyYmKSsuOEzZt6iinpuanJ2fqLLEb0tJSkU7NiwkIB8fHx8f
+IycvP922qqKenJubnZ+mrbnRXVhs/WxSOy8pIh8eHRwdICcyUMKwqKCdm5ucoKevv+FbTFDp
+0/BPRDkuKigkICAhIigwPmi8q6Sem5yfo6u2xNxXR0tXYVhJQjwwKyonJScpKzFAZMeyqKOh
+oKSpr7nOY0xFQUdV+9nU3d/1RzUtKCIhIyYsOGC+raOfnp+iqbC710o6MCwtMTxhw7i1sa6w
+v1UzJh8eHyIpM0jVtKiioJ+jqa+83EMzKiYlKDFLxLCpop+gpKq7RSwkHhwcHR4lMVC7qJ+d
+nZ2iqrf8NSgiHx8iJy9GxayinJmYmZuhr+UvHxsYFhYXGh4sTrqnnpubm52krss6KCIfHyMq
+NEjMsKaem5mZm56ovkUpHRgVFBQXGyEuVrinnpqZmpygqr5FLCQhISQqM0fQtKignZubm5+q
+wT4mHBYUExQXHCU20q6inZmYmZugrMY+KSAeHh8mLz/Qr6WenJubm56nu0YrHRgVFBMVGh8t
+cbCknJmYmZufq8dBKh8dHh8lL0bPr6Odm5mZmp2lu0ApHRcTEhETGB8sXLGjnJeVlpmdqLxN
+LSAcGx4jLEDXtaeenJuanJ+ovEcrHhgVFBQVGiI00aygm5eVlpqfqsNCKx8bGhwfKTlsvayj
+n52dnp+krco9Kh8aGRkZGyEuU7yqop2ampyhrMZBLiUdGhwfKDZcv66knpycnZ+iqLdjNykf
+GxsaGh4nM1K+rqihnp6jqrXYRTEmIB8kKzdP07qtpqOhoqSlpam16UAxIhwcGxocJCw72rWr
+pJ6doKWpt+1DLyUiJiktOlDbvK6ppqSkpaaprb9MNi8mHh0fHyAoMDdTvK+spqKlqq21xlg9
+NzUyMTY7Pkrfv7i1trKur7e4uL/N7EYzMzo1KyktMDExOFPPyce6srO3t7S4wtV+Wkk+NzY5
+PEVty7+5r6qqq62vuclePjIrJiQjJCgtN0J0xby2sq+urrK8yNxZSUQ9Oj1GS1vbxbuyra2t
+rbC7yvhGMywpJycoLDA5RFzWxL27trS3u8LO4G5PREFDRUtd682/ure0sbGytLvJb0M4ODQt
+LDI2MjdJVlXnxsnLv7/R2thjTVlbSk3+3dvKwb22tLi5t7q/xNBzWko1LS8xLS87Pj9s1Pje
+wsPJwMfh2N9JP0xKSWvn69LBtLO6sq2xsrS6u+BGQTEnJCEiJCQqPVrHr6qmoqOqr7rmQC4l
+JCcpLTt4uKumo6KhpKmzv8PLSzEsLCofGyAoJixD77alpKWhoKm5z0QuLCgfIS07Tc63raSh
+qa+ur7zN7ei/uv4+TlkuHR4jICUsLlyrpqmjnqOvv0UtLSsiICo4Tci4sqiiqLG0ub/BytXG
+s66+eGdbOR4WGyEeIS9qrJ6fo5+erOA6KiYoJyUrSMS2r66sp6q78H3Yz8vTzrGkp75w200g
+ExUaGx8uU62bmZ2en63iNSIeIyknLVm3q6ertK+w1z9CXsu2s7SonqCw9T4tGxAQFhslSLmj
+mJWaoKvdLyUdGyAvQuO0qKKirMR5VkA1NUrBq6Skop2fs0guIxgODxgeLcKonZaWnqzGMSAe
+HB0s9LapoqOmqr86LSwtNEbasaKen6Cip7FTJx8dEw8YISvHpqSdmJ+11zUhIyclMcKrpqCn
+ucdrLiQmLDzMs6uhnJ+mq7XAzD4oJx4REh4pNb2rp5yZp8lkMSUpKilJrqilpbhZTzYkIyw5
+zaunpZ6fqbTC9trLRTNGORoTHSIoa8jCo5qjs786KTU2LD+6rqajukZENScpLjbKqaamoqaw
+u9BZ2b3F1/Y/JBURGCIyebemm5ies04vLC0sLki3p6SszUAzKygrOPuvo6Chpa/D2WNa0r3E
+3dNNHxIRFyE53rmimJierk8sLC4tME+9raiv2zksKzI9TMuvpaCjrshtYvvcxLm1t849IBEO
+FR8zyKygmJWbrGArJCcpLDvWs6enukk2MzdDTWy7q6Wmrsbl1MzKu7nG1Wo8IxQPFSEzxayl
+mpWapswtISYtLDVvu6ijslY+OztFRD5xtKmmqbjIw7/Hyc3xQjczJBcUHCtKtKmonpqeq9Ys
+IykvMD/vwK2rvl1JPT9XXF7LtKuoqrXGztHKzPs/MS4qHRcYIT68p6Sjnp2luUAnIys1PmzE
+tKyvx086NDxMYNzAsqmnrsLh9d/MyvdLPzgtHhgYIDvAqqWinp2kuD8nIyo0PVfLtq2vwFY7
+Nj9PYePEta2qsMpncdO/vMbqW1I4IRgWHC3ysqmknpyerU4pISYvOj9av62pseo8NjpMZlx6
+wa+prLx6W9e/ub3M3+pmOiQaFhssT8CuqJ+bnatxLyYnLjQ3Q9eyqKq+Uz8+RUtAQOq4ra22
+ztzIv77Czut2/0ovIhsaIzRYvKylnpygsHoxJygrLC8/2rSqrbnMXEZEQUFU1b2zr7O8x9Tc
+ycnX5lhEOiwfHB8pO8asp6Gen6m7PygjJSgtPFvBq6asudw/PERMU/LOvrGtts9bTWjFvMPT
+2OpJLh4aGyM+uqqjnp6fpr8yIR0eJjA9/rSln6OwaTc1OUFPVe+9r66210lP0Luysb9dWGsx
+HRgaHza7q6ihoKOntTkjHyEnNkxsvKmlqK7PPz9MTmF9UmTEvcree3DFsKytsspOPSsdGBke
+L8SrpZ+gpKi0SyskIyg1R1bOtq6tsb/6Y25bVk9BQmHX0M7Gv7etqqyxxUg+OyYaGhwiPbqv
+rKaoqamyXTcwKy46OTZLzr6zrrK4t8J0Tj4yMz1K47qyr62sra61ykk6LyUfHRsgMmS5q6mq
+qKituuE1KCgqLDZGasCspaWptN5MPjUyMTRCzrOur7K1tLK4w/JCPTotIx0bHy1KxbStqaOf
+pK/cOCooKSsvOWC6q6eorbjLZUQ4MjE2RerEura0srCvs7fCbEw6KB4bGx8rSMqxp6Oio6i1
+2UAuKCgrLz1dyrWsq6uvu9BcRDs4ODpCc8i6sq+vr66xuM1DLyYeGxweJjjPsKmko6Slq7h7
+NysnKCsvOVTHs6qnqK22xW9IOjAuMz5iyLixrqyrrbPGUzksIh4cHSIuSsGxq6eko6SqulMz
+KicnKiwyTcGvpqKlqq+97EMyKigsOVjEtq+rqaqstM1POiwhHBocIi5KzLispJ6dnqW3WjQq
+JSMiJCxDxK2kn6GkqrbXPi0nJys1Tcy5r6qpqqyzwek+KR4bGhwhLDzftaiempmcpbdPMCcg
+Hh4jLk66qqKfn6Orunk5LCgoLTZH47+2r6uqq620zjomHBkaHCErO9OsnpmXmJ2qxEAsIx4c
+HSMuTrqpop+foqizz0IwLCssMT5VzrmvqqiprbtOKh4bGhseJzNqsqKbmZmep7XsOSsiHh4i
+KzhwuKukn6Ckq7npQTUuLC0xPF+/sq2srK66Zy4gHh0dISgvTLuonpubn6aux0w1KCIgJCoz
+S8+2qaOhpKq1y1w/NC4tMDpPy7mzr6+wu2MvJCAfHyQqL0m9qqCdnaGmrLxtOiokJCgqLjtg
+wK2lpKeqsL3RTzcuLTA8WNS/uLOxsLh0LyYkIiMmKC1Fv6ykoKCjpKm4+jssJicpKSszRsmu
+p6epqq61wF03LzA2P05tz721sK+67zcsKScjJCYsQMmwqqWioaGntuc+LysqKSYnL0rKtK2s
+qqiqsL91RDw5OTg9U9XBuri5vtw9LywmIyUoLT/OuKyloKCiqLbVUDsuKycmKTFCecCyrKim
+qbG901VDPTc0PVXhzca+vML1PDIuKicpKzBA372xqaSio6m2xntBNC0pJyozPEzSua6pp6uv
+tb3SYkM4OD9OVl/22szQTDg4NS0sLS42T9vHtayop6etuL3NUDszLS0xNTdC+Ma5sK+yr7C5
+wtVaRUNER09TXOfX+E1EQjw0Ly4vNj9Kc8W4r6ysr7Gyucn/TUE+OjQyNj9Nd9bKwLmzs7e9
+xtHqYFNPT1Ra9NnkVEVDPDczLy8zO0RT3cO5sq6tr7O5vsfVaEg9OTk8PT5DT/3PxL++vb7A
+xM3oXlteZ2323drhbVZPSUA6NjU2O0FKXdzJwr69vb29wMnY9GZeWVBNUV5w9dzQz87S2d/t
+eGVqYGt14NzX1Nrh3ndbTUtJQ0JCRkpTXF1259vS0Nbf6+zu8/Py7vbi3d7Z19XU2ePn7Pdq
+bPt+5tjZ3eTt73dqYldUWFlYW1tq6uHtbm1mYFtVT09PU1lfY2vk19XV0M3Oz9re6uXp+fz5
+6eHV2N3e4/78X1NST09SUlZu4tzUy8zLzdPiemBSSkRCRkdISlBedePd39jZ39/l6n/q3NfY
+0s7Tztfc1uJ6a1xXW11QYmvu6OHe1NLY1t7u+nNbV1RNS0lPU1JZXFVXYF5fYGR2buff2NDQ
+zM/N1M/T1eXb63xoY/B+6GTi7Nvo59/wb2p1XWBWWFBWY2X86+nraWNdXFBTS1FRVF/yfuLd
+29jc09na2+nnctvz39/Z1ezQ3NLX291p5WBeUVVXU29UcWnvb2/9/Od2cGN7WlxmaWNaaFVj
+Ymdebu5m8H7r79ze69Pj0OLN4NbP5t5r+W5k/VdyYHVvatxY0V3e23PYVX3wXPZ3YvRpb/D+
+WV9PUlhKS1dQVlnzftnVy9XS0NXc197w2uxz7fT94WrXXvnu2XTV4V3Wa99W21L1a2DbR89c
+XNpK8FRRUVtNTGVM+F56fO/y2d7e3O/p5ep0y+L7xtva5NDzzevoadN/9nvtWulRXv1fculH
+0Edf51N5WvRaaXVgTGtWb1nsSdFc99VQznvsxlrJcM/u8shb189c3dxlYdnmbtZv2t9b2/hW
+3HZO2XpP0fpc2lhseU5cTUlRQkxQRf/4Wcvg4sbP0NPa1N9b3t1g29xwbdnjX9rYXN7c/Wz8
+3e711+9t3c55atB8c9/uUUpHQkU8PkVBTFdg8NrHy8nF1M3L4NrtXnXbXWvY4ePa2unPyN7a
+7tjaXu52aGxsa3P319bya35YSzw7RDo6QUJPbM7Nyb/CurzMzu7qUk5NQk9OVWtt08jDxcLA
+xsjT2ONbcmFbbnjc1dTX3uNkTzguNz4wLjpKeNHLvbW2s7C5zt94UEQ7ODs+TGv75sO4uri3
+vb/Ncl1QS01cWG7f0MTJ0dlxQy0qLzUuLDlTzry1rKyurq+55Us+OTMtLjE6VNbAubOurrC3
+xORRRkJCQkNR38K4tbvGz18xIyQuLyosP8y0raimqq6vuOE2Li4sKywyPWC9r6usra2yvdtO
+PTc5P0tZ7si1raywwmRDLB0cIScpMV24q6WfnqWvutM8KiQlJik1VMy5rKWlq6+76kU5MzEy
+PFnPvLayrq2stNc9LCIaGSErL0q4pZ+fn6Ct0ks1KCAhKTE/ya6qqqmqsMhWPjIvMztBUMy3
+sbKztru7v8pPMy4oGxgmPkjrtKaio6Clxzs3MCklKjlRy6ylqrC5x3xANzEvOmDSycC8ubu/
+wdF728jFzu5XRS0cGic5PVu8rKahnqfUOzUuKyotOE63paWstMdWPjkyLTNewLq4t7m+xMxt
+TVnk08S8vsn+OiAWGy03QcSup56cn7w6LysoKi41WLKhoaq13T8zLy4uONOzrq2xvc30Tz06
+U97Pu7Gytr52LBYPHTM8z62lnZibsDQpJSIoLzX1q56fqbdqMi0pJy5JwK2pqa6+11A6ODtS
+z8K1rK2vtNwyHQ8PIUR8s6OdmZqoPyQjIiQvPeyrnp6ouf00KywpMOm5sK2sr7/vRjY5Rk/i
+xLmtqq63yTsfFA8WMcuwo56amaLoJx8hIy1A6q6fn6m7ZDcrKikv6rOvr7C0veZFNzpSX2/I
+uLCsqq/DPCYbEhEffrGknZycn7osHx8hKT/OsKCep7jzOiwoJy7+sq6ysrG5zE86OkhdX9a+
+tKyrr71MKR0WDxlasqmcm52fry8dHiMmOsKwoZykvVU5LCUmLF+vrLGzs7nEaj07S2di+sa3
+rqqutdAtHxoREyu5rJ+anKGqSx4dJCYt3q+lnZ+1XEYwJSIoPLeprbaxr7xnPDpES0hPy7Gt
+rq2uuk4oGxMQH8m3q5uZnaXWJR4kJSU9tKWenq3ZZT4oISYz3LKztqyps9hIP0g/OD/PsK+v
+q6y3dzEiGRIVLb2zp5uanq5DJSUoIydMsaahp7a8wT8nJSw/z8XJsaWouu1eWkQxLUe8tLiw
+qau1TiYcFRIbPde1npibobU/LSkgHivetKqjp6qrxjIpKCkzRmW7pqOrs7vQRjArNV7UybKp
+qq/EPCkaDxQuREKtmZebo8FBOycZHTVtxa6no5+n4TUzMyonMe21rq6rqa7RPDU7PTlIvKyw
+t7nNPh4RFywtKsKdmpyfrLXALhwfLS4z3K+noaa0w9U/KyozO0jww6+prr3DzmtKPUnPwMTM
+yslCHxYgNCQnvqChpKSlp7suIy4xIiZbt66srKutvkw7Pj02O1zRwry7t7vM3tTV29TLzldG
+SiwaHDMsIk6noqSipKSqSicuMiIfMs6/uK6pqbPQY1M/MzVHYGnPubK5xL+8ztnDvs5PR0It
+Hh0lKigvyailpqGepL9BODAlHyU4Zsq4qqWqrrjPVjsyNDtAUs+6tba0t72/wsnXV0A8Khob
+LiYcN7OtqaGdmZ2/Td42HBslKStKvKmgoqikpdE4PzcpKTRE2b21q6qusrS/Xj05Lx8YHSkf
+IlG0rqiem5mfsr7dKxwdIh8iOMmtpaKenKO40GQwIyIqMzxftqemqaent14+LSAeHBsfJy9W
+taihnJueoanBOighHxscJDL7t6ufmpyipq7fMygmKCktPsOuqKeopa9ILysgHR4cIDJDbq+g
+n5+dn6ex6DstIR0eICg3VLqln5+en6i1zEYyLCkqMj9UxLSur7rOTTMpJiUkKC85Wr2uqaSi
+paiuvmo6LSUjJiouSsG2raaio6euu8xVNjE1NDVBWNLBxMbaSTk1LCktLjE+YtC7sK6rqa20
+ucdLODcyLC46SmbRva+srayrrrvNekc8NzY7PDlg5D1xzz5ATTg0UD4x8MZf3ra3vLbAwLzi
+UmpaQEpYTe/X2cW+xcS5vcrK1WdSSz4/QkJGT3rz1tNeYN1TPD0+ODg8QVPfzsW5try9vcHH
+z+f16W1ba3pWXWxga3fv6djS3tjP2u5uVFJeVk9k4ufz5uL/W0pESUdFTF3q3+HWy9Dl5OH+
+W1ZWWXD8dPvl4N7W09HOy8fFxsvW42dWU05LTVZbYejd5fBrXU1ISExWY+jWz8zJy9joc1JP
+T0lIT1ZTZufy7Nrd39jX2dnT1uh1a2RfXXfk3NDPysfO5PZkT0tLS1VYYufW09HW1/FeT01O
+S0pOXFrz5Pb82dva3tb9dHZbTV1WZmvO3vHT/L3Q1MvV2E1ST2RRR+Lr6fjJy83b/d9iXkpF
+WmJCS/bW3+XSz8x3WOZ3UUhU8l9bXPPV5WDlzehbdenrX1VdXFtUX9TVzMjDvsvK6d7kT1FP
+ak9l3l/N0vXZ4NxSVGZJTlRTZFhGTV5KP05VU1heeNzN5N/Dw8vLwMTLx9bZz/pcZuxuW3rs
+08/S0czI2HbebVxQRzovOTwvMDtERlFl1Ly8wbu0t8LJ1v5pSz9FU1Vd38/Fvr69u73JzMne
+X1ZSVFZRUH3a6E01MT88Kys4QkRJYcCvsryxqrC/0/BbSDQvO0Y/QeW9uLe2sa21ydTO7kQ8
+PUdPRkzSv8LMXz06Oi4oKzE2PEnfta2uramqrr16Rz81LCswNz5ixrWtrK6trrvUbE0/OTQ5
+T/Dtzruzsb1aOzIuJyAjLDU9Vr+rpaapqKmy3T0yLywoKDJK3b+xqqaorra+10c4NDU4OkXc
+vbW0r6ywzDwrJiQfHCAvR9+5qqCdn6mut+80KCQkJyozZbutp6SkqK/HWj82Ly82QXHMv7Cq
+q6+zx0cyJB0eHx4mPtaxpqKfnqOxxFYvKCQhJjFE0rOqpaOosLzcRDYwLzZEVdK4r66usrm/
+4jwtKiUfHyQuQ9u7rKOhpqy0yUwxKSgtND9rw6+qrK6yu89MOTQ4Oz1M2L22tbe4t73VXUo/
+Ni8pJCcvN0PuwbKrq6+1vNdNOi8xOkNU1r2zrbG5vcfnTz88QkxOWdrCvLu9wMHRdFxNPzky
+KiUoLzxUz7uuqKmvuclrQzYuLztQ5Ma6tLG0wNpkT0U+PEZu2c7IxL+9vsTO3eruaFlXTDUl
+IiwzN0V6vamlq7G5zV4/LioyR1/Ru7Gsrr7YaVFIPDU7VdzHwr+9ur/fWV94+fhpbtnTazMg
+Ii84PEzqtKOirLfC60w4KSk4Uty+uLKts9NWSj89OzlJz768ur3CxdpSSU1Z59nRvre88i0d
+HystNUjtr56eqK/AVT0uJCUzSc61sa2qsdFVQTg4OTtUxbq2tb3M1WNDQEti3M7CuLe8+jMi
+HiEqNEHXr6KeoavATTctKCcsO9m1rayttMP/RjczOEFkx7q1tLrL900/RFrmz8jCvr/K5zwi
+HCArPHXMt6WeoazLOy8wLSktPsytqaywuc9tSjg0O0Zry8S/ur3N3FxJUnB42cTM3MzJ3T8l
+HCIxStjJu6mfo67NPTE0MSwwQtGxq661vutQSjo1O0f3w72+vL3H12dFRl7s4tvPysLAzUMl
+HSMuQubTvamhpK7NPDMzLy0zQN6zq62xvN5aTTw2OD5jw72+u77JznBEP0pV+M/JysK9wvUx
+ISAqOVLey7Smoqm4WjUyNTEvNUnDr62yu81yb0w5NztNz72+vbq+xtlMPUJWeNbO1MS7xe0+
+KSAnMkL4ybuspqiyzz80NjYyNkPguK+zu8LpV1xHOz1IdMe9wL68xtZcQEBY6NrLx8S9wc5W
+LSAhLDlR4M21qKWsu1c4ODk0MjlNx7OxuLzI5uZbPTxFWc2/w8O/xdH5SkFOcODOxsnGvcNT
+KR4iLD5ZZs+xpaSqvFE8OzgwLzRHx7WytLrDzuZXQDxAUNrGw8HCxs/mUUNGXdvHx8zDu7hg
+IhwgKz9dWOCsn6Cnu0k5OzErLTFLuq2ur7S+xfA+NzY5RXXOv7a3vcbkVExMT3Xd1Mq9trlB
+Hh0lLUBPSM+nn6KpwEhBPi0pKzBWt6+urbG6vd84LzMzPnDbwrOyur3STktLR1V8XefBvbq7
+Oh4hLDFGTULEo6CkqcpCTEUvLC4vVrSyt7W7vrjUNzAyNErf/M64trm9+URZ7WFsaW3LvLq8
+UyUgKjA7Pz9mrqKkqbhzU1M3KyssOsu3t7W1t7S6Vzc1Nz5baO3Ft7W5zU9P+PZubVte1MbG
+ykIlJS81PklF7q6lpqu7bl9kOy0tLjzMu726t7ezuVg3NztFWllT2by3usTvY+LeZU5LTurK
+zMldKyk1NjtHRFqzqKmrtd1k9kAvLi42a8XCurS1s7lzPT4+PUZJTNi6t7vG7GrZ0X1jZmXp
+y8jgOiosMTpERU7PsKmprsDtZ1M7MC4uOnjFvLm3tbS+/kc/P0VQWmnTwby/zOpt7N7md2JW
+ZtbYRCsqLjlXYlPzu6ypqrt0T09GOzUvNFDJvLm5vLq92E8/PD9NY2rnyr+9v8zo79zb5urq
+49RMKygqLj9mXF/Br6qmrcpQSUE7OC4tO968tLS8u7e810k2Mz1SaO/e1MG3u8vsW2Df2Xhe
+WVn06DstLzM//eNq2bqxra7FVE1RTEI3Lzdcyby8xse/vstvSUNPe+Lj7/rYxL6/w8bH1kw0
+KScqMT1IYseyqqirtsr+Tj0zLS00S8y9ubWztrvE90M5Nzg8RFPly7+6u8DL1nRZUUtQY/3e
+09XY31c6Njc4RVtf78q6trO51WJaUktHPzxGauDPy9DOxsPK1eJpXW397fPt28zGx8bfPDMy
+LzU8PUFazLuvrbTAyN9iVUU4MjhFb83DxcW9u77KbUhGS0tIR01d2sW/wsjO1dvwWU1GRk9t
+7fv67m1MSU1KVGpiW1/fy8K/y+fo7PX6b1NIT1tefu3gzsjKzM3a4NjT2+nn39zV7kU2MjA3
+Rk5XbdbEu7W4xNLoaVtUTEZESk9b/ePTzMjIyMzX53thWVFPT09YceLWzMrJy9PoaFRJSExU
+WV9x8t/oYVhbUE1UVVVp3dXLxMfMzdHe+G5fXFxcWmDt3tPP1tvX0M3MztTY22hDNC8uMz1M
+WurMv7ezs7vL5F1PTEU+PkJNddbP0s7LycXHz9zwalpWWFdUVFtr3M3JycrP2u1fT0lGSUlH
+QUFJS1L53dvPzMrDv8TP3PllbWdVSkxVXenQz9HMzc7P1Nnl/nFt+9/c5VxCOTU1OT5ETFzZ
+wbavsLfB0OpeTUU9OTxEVf7YzcfDw8bN3fxoXVlWV11jae3c19DNzMvO2uXueV9ENzIvMTpH
+U+zKvLawrrK7xd9SRT04NTY6QlzRxb+8u7u8vsXVe1pMSktTbeLaZkk/Ojk+Q0dPW3DUvra2
+uL3N3/BeTkc/PD5JXffVzcvKxsLFzdxyU05OTVBXaeHRysbHyMzV1eFJODIuLjU8QlLix7iu
+rK2vt8XfWz83Mi8vND5X17+5uLa2uLvG21xKQ0JFTW/SycreUkI8Ojs/QUhW9s28trS3vMvx
+WktFQD09PT9IZNPHxMfHxcTAxdT5UklGS1x5283LyMG/vr3EYDovKyotNDpEWd6+r6qprLPE
+7E8+NzQwLi84SNm8sq+vs7i5vcnwTkA8PkRT78zBwtVQPjg2Njo+PkFU3MC1r7G3vs19WUtA
+PDo3Okd+zMK/vr+9u77I21lHQUBGWuLOx8TFxcC9vsxFLysnJy42PlfeyLarpqeqtd9IOjEv
+Ly4uM0L6vK2qq662vsjZaU8/OTc9Tt6/ubi7ylI3Ly0sMDY4QGLKtqypqq63ymJHOjQzMC82
+RG7Huba1tbm+w91RRj89Pklj08K+vru7ubq/2TsqJSAiLDhF4sO7saeipKm3VzYuKysuLzA7
+Xcewp6aqr7/ZdVRHPjg0N0J3yLexs7S+/j0uKCUmKjA/77+wqaamqK+/ZD0wLCwtLjdRyriv
+ra+0usjiT0E8ODY8S3XIuri5uLu9v8zpTS8kISAjMEvgv7Stp6GgqLZ1LyknKC00OkXiu62m
+pau2zU4/Pzw9PDs+UNO9sq+ytr3JZTkrIh8iKTn2v7KrqKWlqrd9NiknKCw0P1/Jt62rrbC+
+6E5BOzs8Pkhpyru0sra6vsbQ5F5HNCcgICQuT8q7s6+rpqSquVUuJycrMj5Nasq0rKiosMZj
+Pjk9Pj5ASVjZv7q3t7q+v83qWj0sIx8iKjzZvbOtq6inq7lnNSspLDA7S+/BsqytrrrbVklA
+PT4+QVTVv7i3vsPGyszbXEtEOzApJSctQ8+7sa6tq6msu1ozKiktOERU9ca0rq2vu+VRRT49
+QEZR+NLIv7+/v76/xthbSUU6LSUiJi9Oybmxrqyqq7PMQzAsLTI7Sn7Dta+vs7vI6k9BOjg+
+TuvMxMHCvrm7xd1SSEpLPi8lIic1c8G8uLCqp6ey7DkwLy8yNTtevrGutby+wcn3Qzc4RXbV
+1dzYx7u3u8vvaGV+X0IzJyMnLjxnzb2vqKapr8ZePjYvLjI8XMe7ury7usDNbUQ+QEld6eLd
+yL69vL/N09f1ZVRANSwnKS89Ws28s62sr7jIXj83MzI4Q2bNwsC+ubm+zGpKR0pLU3ngzcTE
+x8XBw8fQeVtbRDYsJyovOUn6w7WurK+1vs1uQzMvMjlJ78/Mv7a4vcTadWNTRkZTYuPMy8a9
+u7y+zd3nVj4xKigtMTY+TN67r66vs7e/zlw7NDc7P01efcy+u7m4u8LN61JMT01OTk9acOjh
+0srR3NnkbVxSSkA9P0NFSFX/28rCxMvS3N/qaFhYbubp+Xt33s7R619ebufc3uLd1c/R297d
+3eLo6uPkaExDPj0+Pz1AS17v3NrXz9Ha5O7r39fY3efe1NHZ7mpfa+3p39XPzMnLzc/S1+V6
+aF1aVUpCPjs6Oz1BTFr92MzIxcbO2+j9c3ZybXrt5eHe3+bl5ODm6ufe08/OzdDV0s/T3OX1
+Xk5JQTw7Ozo7PkhY+NLKxcLHzdXld2xsa29/8unj29bU19zf3t/g3Nzb3NfW2+Hk5ODd6mZV
+S0U/PDk5PUFMXuvSyMPDwsbL1/JsXVhZXF1meubj3NfZ3NjV1dbZ2Nvm/Pfq2tnZ3ex2YltP
+Rj8+PT0/RUxZ79bMyMjM1Nrc4+zp7erl3Nzf4ejg5PN3amhy7+Xo7uTd29jT0NPX2+hfTUhD
+Pz9AQUVMV2jx39HNzc/X5Ov07fN5dvfh4N/d3eLi4vJ5dvjo5O5+7d7Uz8/Q0tbc3/FcTUhD
+Pz9BREZMWWru3dze3dvc3ub19Ofc3+fh3tjZ3uxua294+XVudvzm3Nva2dnY2tnd8m5gWlFO
+T01MT1JQWGNod+fd7vfq7uzr6e72/Ozm7vPu+2hgam5scPjo3tbT1tPPz83O1t/yb2trYFJL
+R0lLTlFOTFNdbtzT0tXY3NzW2+Li7GpdXV1kbG5rbPzr4eTp4N7d3ODv5+Pt9Pnt5uTi7mha
+VFBMTVBSU1laXnvh1M/O0tfY3ePse29kW1lfaWpfYWpy7N/e4tvW3OHj6/v67Pjt5+bpdlxX
+WVNUVVFRUFdebOrZ2NfR0tfc5u3v9/5pXlxfZG14dvz8/Pb17+ni4OTn5+zs6Ojk4OPm729h
+XFhQTU5OU1xiZ2t3/e3c29vZ2+fw7PV6eXxtbPfx9O/p4N3b3NfV2eDu8vTu+GZYT1BOTU1L
+S1Jeav35/Ojc1NPS0NDS1t3o+21ma2ldXV5dXV5eYmZ+6N7a3N7c2tXS0tnd3+5+d2tcV1RO
+S0tNTlVbW2N1+uvf4N3c3N3f4e39e2xt9+3s5ePm5ubf3trW19jc3NjZ3+9uYldOS0ZER0xO
+VV9sdX379O3m3NzZ1dXY2N/ze3j7fGppamJhY29xfezs6N/b1tDS19fa4OXp9W9jWE9MS0tK
+SkxPV2Bv+/Dm3NvZ1tfW2t7j6/n3dmdia/r17Ovr5N3c2uDn3drd6fHq6ObsallTTkxKSUpN
+T1dYXXrq6Ovr7u/p8Orj6Ojm7e7r93x28+Hf3NfW1dLR1NTW3eHq7vDz/HhvbWxeWFFNSEZG
+RkpPVVtke+Xb2NjX3N/i3+Hl4+bu+PXt9vZ+dn5weO3o497c293f3Nrc2tjb3uLp/mRaUE1M
+TEtJSUxVY37m3NXY29zg5enxdmlganJze3z05+nr7Orj4ujq7PPr5unt7+7+9/Ht6PD0dVxR
+S0dFREZNWGnp1szKx8fKz9nc8W5fWFZXWlteYXL35trW1dPT193j6+v4+fn98eXd3d/j8V1L
+Qj05ODk8QlfjzMG9vL3AyNDialxWTk5OT1decuLSy8XBv8DHzNbncGNeZ2x56eDe3uL5W0U6
+NC8vMTdEbMq8trS1ub7J3mdVTEVAPTw9RVXfxbq1tLe9x9j+XVVVV19+59zTzcvO3Vo/My0q
+Ky86Vse4sK6vs7rD0P5WRz03MzI3P1/Gt6+usbe+zvVaTk1OUllk8dbJwb2/ym88LCUiIys4
+a72xrq2vsrS4vtZOOC4rLDA+472xra+zuL/Kz+FhTUA9QFDewri2t7zNWj0uJiQjJi9G0rau
+rKurrrXCez0wKyotN0vOubOwsra4vcjeWkY+PUJP3MO9u7q9vcLjSTQnISEkLkzHt7Cvrq2t
+sL1vOSwpKzJI4Me8ureysbW+3UxBQEZWdeTazcK9t7a8xWw7LikiIykwS8u8tK6tra+55EE0
+LS00PVHaxbqzsLG2wNhfUlBVXVla786/t7e+ydh0Xks5LSYhJi9Ezby5trSwsLjNRzYxMz1M
+W+nNw7q2ucHR/WT08mRYTlH7zMXDwMLJys3f9ls/NS0mJy896ry6uLW0s7jGTzgyMjhGUWPS
+x765u7/Hztt6Wk9MU2Xiz83Nx8HAwsfS9VZDNy0lJCw5XsO+u7SvrbDBUjg1OD1FR0r7xry3
+uL7Jzdl/W01KU3bk2tnXyr25u8TZb2dYQzUqIiQuP/vGwbyxra+41kU7PT8+Pz1G3L65ub3G
+y8fObVFKTu3W29vczr+4uL/P53NuVj4wKCMnMD5b0cW5rqyuu+BNQ0M+Ojk6Stm/u7y+wcDF
+1GFLS1Jl6OXn0MO9ur3Hztze22hENSwoKC47S+DGvLWytbrD5VNHPjo6PUhezr+9vcHKxsni
+XU5LVXTr49vPyMPDyM/U2+fk7VtCNSwqLz5Pe93hybeztbrLbFdRSEI9PUdc2MfIy8nKyczt
+XF1h/uN6XGndzMXEy9XU1tzo93hONSosMTlNaVBfxrqzrrfK1+JsXE0+O0FPd9fY28/IwMDL
+53JoXmJmYFx83dnPzMzKyMzV7Uw4MDAyNTxGTWbOvbi0t73G0+x4XUk/PkFIUmjs2c/KxMTG
+ytLnal9eX15jafncz87O0OH78m9URT49RE1dcmZdauvi2+ZjXvDQyszhZlxmd3RzYVlabu7f
+3eHZ1dDQz9Pc29TR1d12Y3P0c1tNSU1UXF9aT0tLTE5TXGd83crEw8jS53ZsZV9TTU1Xa/Dd
+4Ovo3tjW19rd3NXR09noeXRvZmBkbH77dXJoXVpTSklLT1dg8uPf3ebw//fr7H5fYHvo5/tp
+X2F44tva3NXOy8vM1OpqY2x5b2BbVlhebWxaUlFSVFpsb2166ujf09Xb6XlpbHBnZ2NjaHF+
+fPbi2dja2NTR1NrpeGhmcndwcXBteOzk7mlWTUpMUFlcW19fdN/W0NXj/HPt2tbY4/5jZ3V5
+e3ru5d7b2NbZ2dztcHBwb29iV1FMSk9eZ2ReWVhw2s/LzdXc4OLo5uP2al5TUVdcXFtkcffm
+2tXPy87T2t/i7PZ4Z15fbXNwee/t93JcT0pGRktVaXBueO/c0cvM0dnoe2ZfYmNlbHr26trT
+0c/Q09TZ3ut5bWFbV1JPS0hJTE9YZWpveeve2tHQ0dXc6PF8cm1hW1dbY2JeZnfu4d/f4t7e
+3Nze3drY293b3+jzfW5mX1BIRENHTVBQV2V57+fe29TS1tna3unp+GpraGhscW7y39rT0c/P
+0tjd4/F+b1tPTUpHSUtNVWP95uTv6t3b2dfV3/J7cPr3+P5zZGRvcG1vdezh5eLf3tjZ3d7l
+4+Hi4u3n625eVE5OTEpNTE5UX2Vq9eDb29rW2dfV2+HofV9dW1xp+uvq5dzW09DP0dLU2uLq
+7vTzdmlaTUpHRkZITVhlbPTn4N7f4uHf4Obp5vNzZl1fYmZrfe3g2dzf3NbS0NDW2tbU2N3e
+3+Po5nZYTUdCQUNHSk5YXWb34+Lm4NvY2tzmc2ljXlVRW2hy6d7c1M/Oz8/Nzs/T197d2+Ds
+6uLnZU9HPzw9P0NJV2/15NjU0s/OzczO0N1xW1JNTE9fe+XTzMzNztTb3N3kfmxiWlpidO7h
+4uh8V0g/Ozk7P0pd5M/JxMPEw8LFxsrXeFBEPz9FTV/q2M3IxsfKzM3P2ul0XVRSV2Tu1c3N
+0eNbQzkyLzM6Q1rdysLAv769vL3D1FpDOzg4PEVa5c3FxcW/vb7ByNLrYlZUXf/Yy8fIyszP
+8kw8LyspKzND8ce8vL29u7m7wttQPTUzN0FX2cfBvry6ubq9xdleS0dLU3jXzsvGxsXByPZA
+LSQiJSw9bsi/v7y2r62uts1CMi8yPExe9eTUv7ewr7bG/kpITFRhWlFa8ci6tri+1FM9MCom
+JikvQuO+tbCurq60v99INzExNTtHZdTHvbe2uL/N5GlfWVFSV2HhzMW+vsDFz/lKNyslJSkz
+SdXGv7myrq2yw10/ODY5PT1DWNW8srK4wdLh4+9hUE1NWODLyMbFx8jHz1k8LyglKTA6TPHO
+vLGtrrK92FlJPTk5OTxP18C4trm9v8bX+1pPV3nr5dXMx8G9vsPSVjwxKiYoLTRBX9a/ta6u
+srrLZUw/ODc4O07Xw7y3tbW3vM1tXFpcamFf6c3Cv7/AxtBpPzMtKCguMj1a27+zrq2vucbx
+T0A3Njg6SH7Uw7y6uLq9xNji9lZRU2jXy8bCw8LAynFBNS0nKS4vOk//xbWvrrC1vdZeRTo5
+OjpFXObJvry5ubzDzdLca1lUTlnr1snDxcXNdEo7LyoqLjA0PlHWu7Kvr7K5ym5NQDs3OD1I
+ZNXHvrm5uby/xMrW625oX2z458/IzN5eSj82Li4wLzE5Ql3Mvbi2tba8yNlfSUJDQkRTedrI
+vry7vL7Dy9HZ+FxQUltj89fQ5GZgTTsyNjgyMjpATuLHvru2t73Byd9dUkpDRk5Zat7MxL+9
+vL6/xczR2e5sXVtiaGhub15MQTw5NzQ1NzpATvrPxby6vL/Cxc/e8mZcXF5o6NfNysnIys7X
+3OxoZ19ZWV1iYGZwd2lnZFFHR01MQkVPTUxe18/e2M/a39nb8vvh7mZ329XX0M7R0s/R3uHe
+4+3o7PxzdmdcXFZVVE9NU1lWVFNST09ZXFVbbGxhbezm3tnR0M3HxsjNz9TX2eLr5+R/Z2x8
+7+3rem5lY2RcV1daW1hdYGttZWlpYm15a2hiYGlpY2Vx7u7q4tjR1dvk8vDm3+fh4d7Z293d
+4+rt7flta21gW15wcWdoamFpb2tqbvxyYl1gYmvx8PXt4ePn5/NkW1xqaGZyevbr393b2N3p
++Pd/bm1y/vLn4d/k4uPr/mtqZV9fX1lcZmz+5N/l6u/4+O/0/m1paWlubWZt9+rr+m1qbmdg
+Ymh1+O/x/e/i3Nvc3uTm+HR0bWlqbG7v5unq7/Dw8fZ1Z2BfaWpse3xudmppevj3cHFvanT8
+7fHs6+zv7+vt9/j6e3X+9nhma31tcXj8/fv6+n357e/v+/fw8OXk6fr77/txYWRlX2FncPfu
+6+/t5+DqeHFramxtd3j99nloZWt3eXVxde3n4fB1TUC7t2FESsbHY0rvwPhXac/+S19xeVtp
+2G9ZaPnpYVZd2n523eRnXePd91pmcFRK2M/W4tzN2N5q1/BiW09MSFDcWk3fztnfy8nS9VDb
+z/5QTvBgTVb2/Utb1uzg2uPx4Hh92HhbWFBSVV5fYtzQy9fb5Mzc/Ox9eGdqU09cY0xc6uXy
+7tjT2ubv693tXlllXl9fYPvtam/r1dpx7+14ZGJv+GNXYvNsaG3v6Ovt5+Pm5ut4aGdfevFp
+/vD06uvi5OV8bPf1ZF55fm5ic9zbdWvo1Nd+YX3pdFtt6m1uYFxeb3JbV15lXF92bF999t/W
+3d7d3Nze3ubf29na4Ofl2NnYzcjR4trb719sXkc7MTA4Ozs9SWNr+t/Kv7/Cxr++xtbr39rk
+4NfIxMbLzsfEyNTd4+TuWE5UZFUwIyk1MzI3QmjLyNzItbfI2dPKxNBZVM/CzNbJvr2/zObS
+yeBab9vf4u936c/O1tZMLScrLi0vNTxWzsO/t7CzusXlZGZgTkpSYNnLysm/ubzBx8zQ0+Fc
+UWT06d/e2dnhe0U0MS8vNDc4PEd5y8G8u7q5vMDLeE1NWVZTWG3lz83Z2tTPzs/T3Nrd5dzg
+4uPl4nBZZ21SRkNHSkhGREBGUVdeaH3v4NbV0c3O0c/Q197n49zkbmP43OTs3tnRz9fp6uT6
+dHB18uHZ7lVIRktKREFCRkxRWGTh0s7PzsrN1uRtXGhuWlhp7t7a2d3e1NPY2uDc2t3U3NrO
+zcvK1Oz9Tzo1NTI0Oz1BWeHQv7i5ubvC1HpOQD4+PUVbe9rKw8G/xM3X7GNdaWzz1sq/urm9
+vcVbPTAkISYqNFPZxbWtrKywv3xMPzg4OjxL58u9uby/x9T5V01KT27k18XAwLy9xMfK23NO
+OCwkHyUxU7+2tbSwr7PBWTo0OT9JVnPWw7y8v8z0Z19YUlBd3cnBv7/GyczNycvo99r0Wz8t
+Ih8mMuy3srS2t7m93D8xLzhM283QyL66u8jzUE1WcO707NTEv7/H3v7s3d/d29rHvL3PSy4f
+HSIv+bWwtbi+x812PjQ0OlDIu7/DxcW+xf9JP0Jox7/Dx87Px8vrWEpFVdXGvrq/w7/aRi0d
+GR4uy6ijrb/V39HMVjcvMkq8rrC/bkthyshcOjM57bavtshhWt7X5VpHRvu9trW8zt7KwGsv
+HBYaLLufnqvLS1TIuss8Kig2w6urvUU1Qcu0t+80Lj3Jrau5eT88UdLL6lBO47qvsLvTYuXa
+QCkcGB07rp6dqtlCR9u+x0IrJy5psauwyVBGa8bDdDswMlC6rKy4dT9AW8q9wuFf88m1r7XF
+Uy0hHR0kPMauqKu2v8nU2XtALywuPdC1r7G6ydriY0U7ODpVw7WwuNFXVGXZyc7taF9u0sK7
+trxYLR8aHSlurKSos83nyri1zjgmIihCu6mpr77Z59veUjszMz/QtK2uu+ZKP0Vb39Ta9WD0
+w7Wvs/IrHBgbKG+rpKevwsW5sLHGOycfJDHkr6mqsMX7U0xOVlBMUmjZw7y+y2tFQU7avri5
+v87e1M1mNyUdGyAzxqukpq66w7+5vNg9KyQlLle6rauvvdJkWW3d2eVqTkdLV3TazszLycPA
+wL/Hz9rpVDssIx8iLEm+rqyutLi3tbW+bDYqJicuQtm7tLW4vMDDxcXNdk9BPD5GVe3Qx8G9
+uri3ub/L3l0+LSMeHSEuVLyurK2urKurrsFDLCMhJS5F1ry3uLe1s6+vt8pRPDQzNz5KX+HN
+v7iyr6+1vstpOiogHR0hLUnGtK6tqqakpKq8SS0lIiUtN0jxzcS5r6yrrbXHX0M6Nzg5O0JP
+7MO1rq+yucTKytdHLSEdHSMzZsK7urivpqCgp709KiUnLDI4ODtMz7KopqmvvtPj+VpCNS4u
+N1DKuLS3u7u2sLLHPicdHSAqOEZIU82voZyboLJoOTIyMCslIyg407Cqq66wr66vufY5Li0x
+Pk9YWGrPvLKusrm+y04wKCIhJy43PkvZuKienaGrvW9EOzEpJCMoMU3NvbeuqaemrLnXUj86
+ODY1OUrfxLm2tbO3xHw7KyYlJyksMTtcva2mo6KlqrPITDUtKigpLDI++r2xrKmoqq63zks8
+NzY3Oj1FV9LBu7OwtcdMNCwsLSwrLTNBdMKyq6aipay4xnU/MiomKC00PUnqu62qqamssr7m
+Sj06Ozk7Rl7cxr65ucHVRjEvMC4tLjY+Tte/tKuoqq6xt8tZOy4tLi8vMDxfyrqyrqysrrnB
+zWxIPz5AR0tSfs7DwsnMXTcxNDIvMDg7QWnOv7StrK6usr3ZUzw1NTMuLztEU8q3srGurrK0
+uc9cTkc9PktKSWba2NPOVzI0PjQvND0+Rt3JvrKwtbKtssXWe0Q7OjYwNz87RNXBwbqvsLSz
+uszR401EU1FET298dXFmSDo5OzgzNDo/TG/Pvbe0tbGus7zK7VZGPDY1OTo8S2vcy765t7S2
+u77F2GheWkxLT1lhXFJUTj04Ozs2NTxDTHLUwrq2trSxtb7K2n1OQTs7PT4/Rlfv59fHvru7
+vL/Aw87d7WlUVl5dW1NTT0A5Oz05ODw/RE9u2si+u7q1tbrAxMznZk9JSEdEQkpVVFz518nB
+wMHBwcXN0t1iWmFeWFFNTU1FP0NFPz5ARElTZuvQxcLGw7/Bx8rQ4+1+XVJWW1VTXmf93tPO
+ysbIysrN2O1fUVFRU1JVYmZdWldRUFVMRkdLTU5cXmHi0tbXy8jO0NHPz9Ph8eTg6Xz88Pfx
+7eXk2N/v7PR2W19nX1pYbfPs8unk6/ZwX1VNSklLS0xPVV9ha/Lh18/Pz83Ky9Db2N3o9W36
+8+vs8Off3t/p8np1XV1taF9kevH69e/q6u9uY2ZfWlFNTEtMTk9XXGZz8d7Wz9HOz9jb3tbc
+4t7b3Nzc393qc3piX2RkXF1kZm557OPg3d/f63h4cHpfWFpcVVJRV1VVXGF17Od++ujp7Ovq
+5N/i7evh5Ovl5uPz/ebt9PX/fe3f397s4N/n5nro/G5xXWpiYVtZXltgWl1jV1xiX15n/Gt6
+cGl2du537vfq4+bs+OPk4d/V19Tc4OTl3tbY3uJ88X56b2V5am1vY2FeX1pdX11eXl1cW2Ff
+YWdy/HF8bGh2+3lpa3b86eTg3t3f39rd3NjU197b3+Xe3Oz5+mxdYVxYW11gX2d2cHl5dnj5
+fWZoaWtqaGZqaWloaXH++vn+e3bu6OPf4+De5/fx4+Xv8n568nxnaGhma/fv7uzq7fZ+fPTx
+8vH7//n4/HFlZ3FhXmZkZWViZW91/Ht4eH9//e9zbHZ2e/PxfvTt7PPv39/k6Ovt5eTv6u3u
+7PlvaGprbXdqaGpta2RkZGhrb2doam1ubG9vbnt5dvnt4uPo7vD27+vq6e7q6n356unm7X3+
+eHBuZmpvdHRuaWdqaGRq+ezp8/9zdvHp8HpzfHd19/r0+3f5dW98dm1oa3b/fvf1/O3u/u7l
+6vP27/L58OnteXR+e3v7eGpvdHpvZnFua2tz+e3s7/t9+n1+/PH7fXJqYWJxeHt2/vzu5ubx
+fHr+/fz19vvy8vz97ebs7ev5/3Zta2l+8fX9emxqbGpqdPrw73lnb25qbG97en15/evo8P7y
+7vR+/Hhyc37y7uzu+Hd3/PTy7+/4cG94/e5+/vn8dGtsd31ub3Bpanz5eGxz9/J7dX1tcX7i
+5vvs7u7+fOzu+3Bydnl5f3plZnN5dG568u7v6err5ul5amZu//B4b3VrY23193v28vpvbvPz
+/Xv7aOPjeef35Ofs9fZ4cGZmcWpw9PlrYnrsbG549H9pX+nedHV9cHr09nfi33H88+Xw/vN9
+amxran/wem1+9XN77/j7/fb08uvyee9/aGr7fGZs+e306OT3cuz5cd/X1c/W2Nvf33hYVVFH
+Q0VDP0VJSU9i/t3OzMrEwsXIyM7Z2t18a/15ZGr78eLa2c/Oz8jIzt1aSUk+NTc6NjQ6QUNM
+b9rIw8K7t7q8urzDytPqYU5HR0M+QElKSFBw6NvNxb/Bv7q8vr6+wctlR0RCMy41MCwvNTxD
+TurHv7+6r7K5uLW7yc/c/0s+RUQ6OkVIRU/12tHMxL2+v727vcK/wsrfXko+PjItNTAtMTc7
+QFrvyr2+t7G0uLe1vMfP4HNMQkRDOzlDR0FKa+Dez8K+vb67usLGvr/O4eNZPDw+Ly8zLi8z
+Nz1QbO3FvLq0sLK2tbe8wtXnfEs+Pj47OD5CP0li4tbMwr26u726vMXExtb7bFtFOj05LzMz
+MjU4PElv69G8ubu3tLa6urzDz3xvWEM+QUM8QE1PVWrg0cvGwb2/w7/Cx83P09t5YVQ+Oj82
+MDQzMzY8Q1J6+su9vry2tLm7ur/Hz/RzWkdKS0FCS1NVXujXzczKxcTGycbJ1trZ6GldWkc8
+PTw2Nzs5OT5ETGrs2sS9vry4ur++v8/e5GhZUUlKT1NUcN3z4cvGysnGydDb3+NzW1hYTk5X
+VU5ESUk+QEVHRUpSVWrs4c7Dx8zDwcrKys3Z7u92Z2No+HRu69PV3dPS3ufwb1dSUExMT1pW
+bejt79jU3N7R9Gj8YVBPVEpJTktPWFpcf+L82s/a28/P29Da2NPf3urw3e7v82R88V1e+H5j
+cebo9/Xf3tzm7d3dbGJyX1VWW1hPTk9TU1RaYfxcYOxv4dzc5PXg3tvr59jj3/7mc+vj/drm
+3N7k4nLu3Pjd6+PifnJlW2VVVlxXWVpWWGdgbHfq92rt8vrq+ezh8ev8/m366On4a2xaaHtr
+cft6cPju9+HZ4uDV3Nzk6uPv9npkcXhaXl9dbG5z6vJo7XBt8WVseGpeWmf4+Gl6d2326P9t
+8fhw4PZp7Oju6OTs7t/d6Pvr4uTifvJxZnxjbWheY2luX2l1dP3u5+/w73ZjbmlubF5tcWpx
+ffjr4uTt6+bg9P7l7G1h6XBqc/fm7v9q6/Rmb2x3Z/h5aOvo3eTo63z47vPwbWl5629hcGJf
+X2BkX/5uYml1/fnx8eff5d/w7nf95ebs8+Bvb+Xi8evz9ex7dX5cZmTu6mlpaub+W2T5a3Nv
+bm5+aWX7fnp649r6bWrt8fTpfOt8fndrfmb+Z3v0+/Htf25zet/p6efs7uPuceb0dnn0b3V3
+avJf63Rqc/17aGhlVFlgbGL+0NTT7PPob/3s++5lZGx+bP/3e+7r3/T4b3Rf8nXma/tuXPNw
+9XXwcWjoenteYv5m7PHw2vTl8Ozh4fF/3mlnemHudfRvXehqdXnl8OP0ZPFkZ1NiW1tmYXlu
+2GXa9ePZ3dRq6XpvemTceedh9+li+nrz8nDdZPnuYvFk31t+bFZ3duJd5PpmznFrc3Z1WeBe
+6OJv7vfP7n7Zatl8fWll6E/r92tnWvpl3vdf6F9rWm3tX9327d/y6n/b7vbq8e/sfGNu/2Rn
+Z+vq+3Zg7vB+9e/0bvT1e+557ex3anBrZFxcVVlbWlxnbvvq3+zn3d7c3tnW1dba1dfi3tDY
+4+bm39neeFVWRDs+Ozk8P0NK+93Pwr+9vLvDy9HqUkQ/P0RJSVH9z8vHv768wMbJz9Lc5+rz
+3+DfZFFHOTQzMjI1OT9Od82/urS0tbi8xdlrTT87Ojs+RU9r0cO/v7y7vcDFz9/tb2FZXn7l
+2+BqXEg5NTY3Nzg8Q07zy8S9uri4usHK1PpYS0VDREdKU1v51cjCwMLGx8rQ435sW1pZYmtn
+fuvr7H5gU01KSEdGQ0RITlpdaOnZ0MrLzc7V2eP1en3z/Ord3dvc2Nnc2dzmd29mXl9cYmz+
+9nd4+97i+XZ07PZ8Z1xbVVdWWVlWWVpeX2x6dH9kYPzc6/Xe3N7g4dvY3t7W1dvc3N3rZ/vn
+8HNgX2hrXGfn7/5tfu15anPm921jZ3RpYF1qaFxcV19oXF1ne/jy8t/a7uzf3tnl7d7f9XL7
+7fBsbXt1d/vh2trf4t/k9HRxeW5nX1lbWlRVXXLy/vrh73v+9et0X11ncl5favTt79/d3/Hz
+6t/jff3p5PxvfO3j6vPu7+zr7PXxeWNsaWNdW2lwbGFre3T/d3p9cGxscPH5b/nf2+Le3Od0
+bn1+cWhhaG9pX2n38XJse/Tn6OXe4+np6/x4cXT18fR7dPXzc2z57vn+8+37cGzx7nJgXWtn
+VVRfY2dlfefj93vl3uDo5uLl+H3s6u758uPl8u3g4+v/d/lxYGFoa2VkYXJ9/Pf9+nt5fnlt
+aGpub2588+vm5Ov87+/0/m9z+fl2e/x+fnJ+8vZ+ef79bGn67fl49uvt3+Du6unl9f5va3Fi
+ZXD/d3v8bWb35+3+9PrsemZqbmlfefpvdPn07GZtcGp3aHR1b/nk7m7x5fzv/fLi4+Tg9vPi
+7PRuae99bHv65P14/nt29utgXmVoe2dt83Zt+nhvafbs5nJhdWF45HFz7uzm63/83XVt3fll
+Z2vv7OLi+3ju6fl89nnw/G9sZF9rbX3xam11bW7z9+ft7/X+dV9j++zm8W5t7N3ubnX1+H32
+7XRp8OjyeHlnXmju8WppbPvve2tqc/nf6vfj6vH+ZFpi/uXb3evq6ebf5u55bXL56+5rXl1c
+ZF1QTEpQXF9jc+3h2NfX3uXb1tzm7e/n3+Pf29nU19XOys7V2Nzd6fP+aUIqICMuRmby1Ma9
+tK6tt9JiXlhFOThAVtjBubi9wL6/z2FOTlNRV2b72srAvsLP2M7XRCsfHSAsRNLBvLKrp6mx
+wXBGPjo3NTQ6Wca5tbe5u8LSZElES3PW1tvRwru5u77Hzu5VST8yJx8eJTbhvbexr66sr71s
+Pjg3NjY5Sdm+ubm+xcXL2mNNTl/czs/X2M/JxsjP2NHMzczUZkg7LiMeIC9mwbawsK+ur7h9
+NS0uMztCZsG0r6+6zeJzY1JNUlnz0MnR6+nVz87O0M7Hwr7GdkM0KB0aIjvJs6ypqKiprtUw
+JSUsOlrLvbaurLPMSzcwNkNf3869uL3L+l1i8dvg8tzJw7+6ubvOOyMYFiBAu62rqaimpbI+
+JR8lM07SzMayqaq4ZjsyMzg6SM61ra64zVtLTEc/QmDKvru8vbq0ssguGA8WLb2oqK2spZ+m
+VCMcHy9vx9bcs6OirmIvKS4/SkBNxK6qrsVNQk5TS0hU2cC6usLPxb7BxkciGBgoerassbiv
+qazRMCYmNPbJ19+9rauy4zQtNk/vaVRpvq+vvPpLSElIPDtPyrq6v7+8uLnMakIjFhoqXK6m
+qrSvqK/vMiMiMOHCy8W2rau0SCkoMUrZzNTGs62vxkw2LzY8P23CuLOztbS0vlY2OksvHx0l
+S6+jq7++s7PNMyIiNse5v8fAtK6+OicqQc++v8vKvba/TTMvOlLl1c+/tbCzvMLMb1BHRtNh
+IRkkRLmnr+jYr63KOSYjPL2+2ubMt627Oyw2Ws7G0tvFvstpQTtL++jj1MvAur3Mz8W/yNnd
+5es9IBkfOtGytcK1qavIOSoqPP5hTPm5ra29SjpFXFtPWdu/vMTM2erfdklBTmXqysPDv7y9
+xM/oVVtBHxgiPMuvtsm1p6vMQS8tQFU/Qte4r66/TEp3ZEM/S9W6vc3IxM3eSDU3RVjovra4
+trS7xvA+PT8pHh4oVryxuLitrLTVPTI4QkFFYc25srrVW1hoX0lDWdHOz8nKys1vRD4/SffI
+vLm5ury+zFxOTTAjHyEx3r64sq2ssLxQODk4NThI3r+1t77H5F9tTj9FTm/NwMHKytV+VkE9
+S9nDvr29uLK4x87uMCMcGypH1L6wqKeptl48NS8uMTpkvLOztLnN+lpFQEA9Ss26uLnA1N/l
+Sjg7T9fCwL+4srO/zeo0JRwZJThTxbKnoaKsylc6LCotMELPvLKsrr3VYEg/OjY7Y7+6u7zA
+xc5aQkNR89TOx7y0sLG/Si4iHB4kLEbEraahpKu02zcoJyktO1TNsqmpr7vI7kg3MzdG6s/G
+ura6xupnaVxYW2vez8O8vMhNMCMiKCkvQcuwqKepqa/ASjArKy8zOE/Crqywt7u90FE9PENK
+TlHiysjO28/BvMPb5enl3GpEPTkrKjA3PmTLuayrsba4xlY8Mi83OztG+MO4uL7CwcjS5mNY
+VFNWW2l+anvezL67vMTBvsvoRDMoIykqLjlPxK2mqamqscNjOy8vLi0xPWbIvbq0tLnBytXd
+7V5NTFZjYFRU+8y9ubm7vLvJTzkvJyQlJSo2SNeyqKakpau1yUc1MC0rLTZEZs2+uLOytbi9
+xNLtXk9HRENCREhd0L66t7S1vNtFLispJiYpLTrnva+opaWnq7fLWzkuLCstMjlO3MrCuLSx
+sri9wcPSelZMSEI/PkhbdN/Tz9DsWVFGOzUzMTU5PlfPvrevra6vtsHdVkVCPz0+R1BefN7N
+x8HDxMLDxszS43xcTkA9Pj9BSFFf79/e2+VyWkpAPj9BRk1tzL66ubi6vcLO5W1cUVFVU1df
+eO/i3eDb3f3v3urv5nJiXFBLSkxKTGHl0MrFxMfO5mxVRz48PkJLV2Tt1c/Q2Njb5O51fnrp
+4NrW2tTSz9ba3Obb3OHy+nppXlNMTFFYYerc2dDKzdPlb1dKR0ZFRUhLV2vv6+bl3dzram5r
+af145djWzM3PztHY493a3uDmb2ptZVBOUU9UX3zd2dbOzMjY7GpPTEtIS1BSXnZk+dbc3Oln
+aGNfcXl593d4b33q8+/y5dbP0c3S4eDp719cU1RvZ/Xa1c/O0Nh4aFZWT0xLTk9ZVGf5e9/f
+9+Xc5Opnc21ueGdhX2lea3/619bT4NrR6OtoZWBjW251+dzm2NfU2eDld/BgZ1JaTlZTUfVs
+6nzg5+rg8fBt9WpvXGdgXmde+vHr3N/b2+Ls+mpob15eWWBke+v039za1dvY5d9ubmFca2Rr
+bnN0fWzvZnnqaXxfYVpaWmRe/3Do6vT68eDa4d7v4urzc2xnbnrzfnzt6d709nh2cm1uYGdf
+ZGl89Pbo+ubs6X73bGVlYGtmZWZve3P6fO3v+Ozv9X3+dXZ0Z2RnaHR4/+zv6eHh5eTr9f99
+dHJ1+Pj7d//+b/14eXZ8+Hl8e/n/+/H4d2VnX1ljae/u7Xdwbm79bnhv+v36a3p39/F+8v32
+8X967vTf3+zgd/Ln8+b08vH39mtlX29YX1tcbmd7ZnJ6XO1y7dXn8d1sfOxn53l88W3h337n
+7XDl82Xtd11qcGjs+vHoZe3qbe7sY/fmXmHrbWDzZ2lzbfPwbPLvevlmb27+7PPq6O/g5ffr
+/H15bGxtbHJuanrs8vXu/nP6fXJ7cH33+XZ5/vnv7u3q/Xvw+fHu/fl6anRxb3x4bGx0fnxr
+ePjx7ufr7PB9dWx4f3b4+nt0dnn4+/fv/XFzbGVtc/309/Xs6vHo5u38/Pn1+m5rbnp89+39
+c37/fvz57/b4dmlmefv/+Xd09PHt7/7r7fZ8bWtqY2h2cHP0/Xf58O7v7+vs7+zu+vjy9Ph8
+bnf6/P3xfHJ7fXN2b2tvc25oZmltb/7y8+3q7ezs9fr7+/Hx8X93/X1++PX9+3hzeH97++vy
+fPj58v5wbm547+7y+P769vV4b2dtaGBve/j5+Hp773/t7vXq6Pt4bGzy5fbr+fXi6ufv9/bl
+8mllal9fWlxmXG/o7Pl/7+n7//Zvb3t+397h4Obd3er29WRaZu9eXXt1c/t67vToeVl5fHrg
+3t/f2d3+e/JlXmNhW19kZ3D75uvm5/R9eP5oaf5yeeXh7uzl3+Lm+nbs6+vo39jc2dXc3uVp
+VEI7PD0/SE9c8cy+vLy+xc7afVNJREE/SV352MrDwsLDytnn/2pjXGngz8rIx8zlTjcrKCwx
+O0diy7WrqKqxwHVKPTItLTNF07y1sa+vtMB3PTIxNjxIXNrAt7S3vcnY6lpGQUdLYdXL4jAo
+NDY8aUxNwa2nq7O/UU1XNi0vNUjPuri+u7a9yOhEPD9GTlNf69K/u7/CwsTJz+ZbSzosJSUt
+OlzLwLWrqKu220I2NTIvM0HeurGwtLrA0E85MjQ6RvnNwrm3u8PXW0hITVZ6z8jEv8PBwM5v
+NR4eKy894sy5qKGlt9ZPNDAwKSk617q0rq2xtb9JNjU3Pkxxy761sbvJ1nllZFFNU2T379PO
+WTsuKCw3Pkf/wrKsq6++1lxCNjAxN0Lovbe0s7q/yO5JPjw7P1rTysTBxdT39W1cWlRQcczD
+xcvMx8LGQyIeKi82S1jcsaOgqrnFWj88LSUpOGbIubCwr660zVU/OztBT1FY28fBvb/M1drd
+8V9hXk1COC0qMkRc7dDJvbKus8DYVkNAPjo7RV3Twb3Aw768x+FbRD5BR0lOYOTWysfLzMzS
+2/teXmb23Onsz8jyNCswNjpUbVfduqyrsLbLT09POjAuLzdPxru+u7S2uLvWRjs+RkdIS0zy
+vLKztr3FzNPeUjktJyQpM0Bp0cm8r6mnrLnYRDs8OTQwLzdPy7m0tbOztLfGXz83Njk7QU1i
+2se9uLm7vs72b19YTkZJTz82NTM1Q+XHwcbGxb+2t8TsRjg5P0tKQUZR7cG3t7m9xMTM2XxO
+RT89R2Tcwru9vr++vctaOSsmKCktOUvjxr22sq2qrLTEVTw4Njc5ODc4QfLCt6+vs7W6v8vn
+UD83MzI1QVzSvre0tbi9xczVVTYsJyQoMD5mx7m0sq+usLK2w2k/My0sLzY7Q1fdwrWurK2v
+tb/pSjozNDlCVd/Ivru5ubzLUDgtJycrLzxZ0r64tbOzsrK2vdRMPDg2ODs9QEdY5cq9uLi3
+t7q9wdhPPzs6PUVOYd3Mx8LAws/8VTwuLCssNUrYwbu1s7Oys7i8yetdRzs2MTE1PEpx08e/
+urOys7a+02NJPTk5PUhk1srHw8PJ2l9CNzEvMDhHY9zJvbm3tre7vsfYXkQ8Ojk6PURLX9nG
+vbe0tbm8yNx4UEE+P0ZQaOXf3M/N0+hRPTQxMDM8S2XRwLq2tLO1ubzG3ldBOTU1ODxGWOPI
+v7y6ubq9wMvsXE1CQklUZXvk4dzU095YPzk0MTU7RHHOwbu4t7a4vMbZfks+OjY2O0NPedTF
+u7i3uLq+yd5bRD0+QU582MzBwMLEyNZWPTIrJyksM0vVvbCtra2tr7fF9z8vKykqMD1eybu2
+sa+usLa91048MzE2PVHVw7y4tri7vspZNigiHyEqPNy2rqyqqaiprbpfMiciISYvQ9i/urKu
+q6iqsshNNy4uND1LcNzKvbWurK+820cwJyIgISg0VcCvqaWio6etwkYtJCIjKDFF372xq6io
+q7faQTEtLzU/VPjWxru1sbG1wfFFOTg9QTovLC0zVr61sLG3uba2vnk7LCgtOk7Wyc3HurGu
+ssJNNC8yPWfm7+LYyLu1uMXgX1NVVktKTV3QZTk3Mi9G183Bv765s666bEMxLjpBRVBc272v
+rbfMYD07SUpHSUtkzby5wtTsZuXXXkY/Q1zNvbq9yUMxNDM0QEdL9L2vra600ldPPDc4NjtP
+zby5t7rDw8Z1Rzw3O03u2eTm6NPAv839TkhX4MjAv8xZPTY2OT5CQ0/Ru7CtsL/iUUA6NzU2
+PlzSvra0tba801E+NzlEU11k7M/Fv8PR+VVGQ0dNaNzX09TOy87Q6lVMSktVbOXgblpdWFZc
+WFdo6dzc3djSysXL415RUVlocXF6dHHf1dTW3/V78NvPysjkRzs5ODxESU7nv7i2ub/N3XhQ
+Pzo5PEdYfNTAuri6wNFrUEc/PT9FT2Xj0cW9vcXXZE9NTUhDRk1f08fLy8/Sz9h7U01MS1Bf
+aurT09LP1uP3ZlZUWl9aYXJ069nS09v3ZF5dWlxpaFhZZ2702tfa0s7S3ePo7ujqZFNPT09W
+Z3B56d7U0Nbc629eVldXUVFUV2Ds1c3LzNPb5HVjW05OWF1fburY0NDV2tzY1drf6vhpV1FU
+V1VUUk5UYXH87+Xl39nd+nB6+Onl3uDf2tza33JeVlBOUFlfbeve2tXU1trl821ZUVVbafLp
+6OPh4+LrcWdlbnb+9Px9+uvh3+bzdWllZ2lrbG9yfH1saXRwbP73/u7p7fv56ufvb1xYXnnp
+4t3f39za3efyfG9vdnb+f/Lh3+n6a15aWl1ZWFpj/fb39ujremdmcXvs7O7s5t3c3un1+/H1
+/P5uZmtycW9oZ2Zqb/bl4uDd2dXR193pbFtWVlRTVlthZnPv7Pl5bmdrdHr9/fTm4uTf3t7n
+9G9tdnhxbnJy/ert+fju8Xn8+ebf3eDo8PX1/HJraGZob21rZ2NhWlNQVVhdYGNz693b2trd
+4+Pt6+rs5uns5uXm5Ofs+PTt+vju6u3j3t3f6fhpXVlVT0xLTE1SWFxfYGh3/Ozd29zd29va
+2Nze5ezxcm5z+Ozn5uPe3N3j5Ofo7Pnx7PPz6e97Z15dU05NTExPUlZWV19rb3vr4d3d3trY
+19na4uvi3+T2bmx49ujj6e7w6ePi3t7l+/zx6uXm8HtuZl9VTkpISUtOVFtpdn3s5N/a2djZ
+3d/k5evx9Pp0bm989v99fn3w6ebk4ODj3trZ2NXZ3OP0/WlbVE5LSktKSkxPVl586tzZ2t7n
+4t7f4+nv7vHw8/Hx9fX48O3s5uXp6+LZ2Nfa3uLk4Ofq925kXFlTT01LSUdKT1ddZXzk3NfX
+2t7e3d7h4+jl5+3zc/z5fX95eXvz6+Ha1NTZ29nZ2NjY2ebzblhOTElEQkNGSUxPV1/83t7V
+zM7Pz87P3fV1YVlYU1BXX27q2c7HxMbIyszS2t3d5+3r49ndYUs9Njk6NjY7SGrVw724tLi/
+y+RgTUA8Oz9KTl3czMPByMjEyNV5X19cX2du4szHx8bBv8TK3VVEOC0rLi8wOU3av7azs7C1
+wt5ZRTw4NjlFXuzNv7q4ur/M1ehMQklLTVJe6sq/wsO9vszZ319KPzctLTU1N0b3yby2tbOx
+u9hfTkE6OTtDXm/uxru+wMPRfmFVSExdVlfv2M3Hw8TDwMrc5PxpWEM7NjM2NjZAXdrKwLm2
+t77O2nhKPjs9QElc6My/v8O/w83Y62RYVlRTYXx369HMysnM2n/o6GJeUEI6NTY2OENOeM7A
+ube5vsTK6UtDQT8/RFB638/Cvbu/ys7U8F5STk9OTVrt3NTKyM3MzNjh4OlaQDg0NjMyO01c
+68O3tri5u8HVV0hDPjs9R1Jd4MO9vb6+vcXdb21bSkZMU1lj3cnKzMjH0NnT3WRDNzM3NTA6
+TlNdy7m3uLW3vs9nT0lAOz1ITE3+ysnHvr3ByczP415MR0lKTV/h1tTKw8XHycfLWEFBOjQw
+NDw7P2PNwb63sre9w835SkBBPzw8R1de28S/v769wMvR3mdVSUZKTFP81tTNwb7Cxcz+TD47
+NDA3OTtDWdrMv7i4uru+yN5nVUg+Oz5ESE/81NDMwb3AxsbJ3GJeX1JPXWtt7tTNzMzZ6mJP
+SDw6PTs8QE9l/tLGw8C/vsTO2eR7WE1KSEZGTllf69DIxcTAwcnP1eJ8bm1gWVxgX2X56vdj
+XVxHQEhIQ0FMXFZf2s3Oy8HBzMzJz/Bqc1pKSlBSUFv57uDPysfHy83T2+doWFJPT09TXm/r
+3d3q/d3yTVJiTUFLfVpM+tHqbM/H3fnW0n5t3+haVXV5XWrtfN7T09rb0dbe7ntfUU9PT05X
+cPvp2dDQ1trf+WVdWVFNUVhVWGl2evvp4ujs4ur39ev5d/J7dfXo39nT09rb2eH+bWJWT09S
+U1leb+vf2trV193g7HloYVpYVVBRWF1mbfjn7+re3eDn6OxubHb9fPnr9urk4N7g5ezudGJZ
+W11YWV9odunk6uLa19jZ3/BzbWNZXF5eXmJqcOzg5+ni3+ju91xVZ3pnWGfxeGVw5N7d5vbw
+fmlmbP5zbnb28u3f3tza3+bi5ef1bGBjampdW2V2cGjx3tvkfPfudV9jXldaXF1gc+vn6PPr
+3Obw6vJ8dGhq9Obm5+Dc4Ojm3N78bmdsXVtbWWNraGx26d/59+vl5nZ4YWxrcfNbe/h8993p
+2t/w63h39Vt0ZWdpU/lr6OVb299q6N/j7ep972DgfFzpdPL6efvi9fxi79lg5WpcafZaXHd2
+bG937eva8nfgfW15/Hn25Xts+fvueGjd/n707Xxf+2r/X/VwffTs92xx597x+e/s9HL2bXL5
+Z21x9m/z8/7+6PhkdWNoaWl0d2585+d35/3n2e/9bHxrbXT67+l67m3m93Rx//p+6PNsZ31o
+bv756mxua3j99vj8cX77Z/j143by5+7feu5+fX9tdX7te/9ode79efXmb2vseeX36vRce15t
+cn79/Pbn7+lreWr4+3P7cOhf/W/v5Oji7PXx+Ofu4XjmdHFva2l8dGlmZmz7cGhtemr6//Xv
+eezv6f3m3u3uaOP7999p+19wcXdqeGvucnjrevh0aPFmZ29ycH1z7t/j3enr7H717O/p8H1t
+ZmptfW9sbWhxbnXs7/f8f3Bycf53b3x89fv49vP9fe7v6evyc3J57+n3/3d17e/3+/5yc21k
+aG93+Hp4+ubodnR59/jzcnBtdnRucHP68vZ6fu3p6PHv+/Tz9Ov+8Ox+e2hyfnhubW9oZmRu
+eXl87uft5OPi53359O/5+Xlqa3B5+Hhv8G5+fnh+dXRvc319/fp2bGhzeXj3+nX2+Ovh6/P3
+cXF9/enqcm56b3F7+fTwe378/fDufGtqcXRucf39+fv78Ozq7fl5cnd3bGllZW/+8vDt7Pxw
+b2948fF+cW98fXn+//Tz7vDt83Ju/ndxf/Hye3JydvD1+fh3c3Zwb3JubWVlanj79/R4+fB6
+9fn67e7u7uzk3+bu9fj1/v9uaF5gbm5qbG9pcXFtbHJ4bWdodnv/e2p47vX47ODd4Obo9n17
+dHZ2/ebg6vV2dHNpbWplbXVub2tpcHpybG12/nr89XFy9PTw6PR3evjp7O3s9nVsb3d9+Pf4
+fX709vTv/21qbnp9b3Jtbfj4fW5zevf9c/Xu9nRsam148erp7fR9+/H29uzufH16fvTz/vf+
+/fF+cG5zcWllbHZ2bmxscf727fdz//Pz++/w93f48XBpePbt7/5++vp6eHX88u9l59V+4uzz
+6d5yYWRob1NH7udNX+bi/s7fXNHicOnq7ndw9WZ+3XN16910Yup8ZG7semnl3v7/4/BscG5q
+ZmhfXmheXffuaHfe7nH1+m9renZ86HB46vDu4t7n6OT0dufj6ubg6tvT49rT1tzh2OJtXEpD
+RDg2PTs3PUtSatfKx8G9u7u+wL7F2N7vX1paWFhmfurWzsnCwMHEzdleRDw1MC4uLi8zOUJY
+5sW8tbOys7W2u77H3XNfTURCQUJFSU9f6M/JycC9vb28vsvN0GNAOjoyLSwvLy82RE9fzby4
+t7Gtr7S1t8PW4FdBPj08Oz1AR05r2MzGvry7vb29v8bM2l9HPjw4Ly82MjA7Qktc18XAvrmz
+t7q4vMfP3WNQRkRGP0BKTU1YdunRyMG/wL29wL/CydHdaUtAPzovLzQxMjo/Rk1+zcnHvbi6
+uri6vb/E0vxnWk5JR0RARExOV2/n3M7Hw7+/v73Bx8jS8lVEPjg1MjIyMzc6P0xr1Me+ure0
+srO3ur7M1t9tTUJCPzw9P0JHUGXj1Mm+vby4uby9wMbQc11JOjkzLy8uLzU3Pkxm5se8ubSy
+sLK3ur/M3OxbS0U+Pj4+RU1VZ9rPzMO/v7/Av8TN1OH6ZFZQQz09OzY1ODk4P0lOXN3Jwb67
+t7i5t7m+xc3gWk1LREBCQEBIT1h43dHKxsPCwsTEyNPX1/NqZExBP0A7NDk7NzpCSU1t0MrE
+vLe2ubq7v8rR1PdZVU1GQUZKSU5Za+7ezsjHxcTHyMrV4uN7WFJQTEQ+QD85Oj5CQEhjdfLV
+x7++vbu8vsHHz995XVFOSkdJTVBbcufVzcrJysvLz9fuX1FMS0lISU1OVF5nd+nf6ufh3+Xy
+8v5lYGNjXV1mamZfYnjq5t7a19fV0M/Q0tTS1tvp/mhZWFlVUU9RV1haYGv44dfX19XY1NTd
+7HJoW1hWVE5PU1RVWV1bXGh96dzZ1NPV1tTOztTX3Ojz/v9/bWBfX1tfbWllbnz17OXl6ODc
+6e/7e/xxZl1cXVpYVlVXXGx/8uno5OHg393d4unm6uTm7f11dXF1/ftydnVvbnnw6OTn6+rl
+6/Hr+Xr9fXVtbGphXF9ma3Rta2xuePLr9fXt6ezx7+vq7fp7/X5+/m9ma//69f55/vrw6+jl
+6PT++v9+fnhpZGp3dHH9+nJ9eH34/ufe5PPx5ut5a2hrZ2Vsbm987u738O7r+f1uZ19dYV5i
+bvvz6eXi393d3dva3ufs+WhnbWxvcm1re31tbGtobW5pa3BuZWFkZG/8/frq7vfs5ubj393e
+3tvd5e/6e/L2f3h3bWdtbm1wbWpoX15nbnBze3p7fvr08Orr7eji5OLr/Hb69HZtc29wdP/x
+fnrw9G9ucnV5bGZqcHh89+3l5+Xf4uXp7XlsbHV8+e7q9m5z+nhwd3trYlxLX9xq7+3p4ujq
+6vBsWHLV9/l89ujt5OPyZ19PVdjma1Fe0tjV5d7Z4nVjdWZgX2ZibXj5eWlgXO7y+/H65Ozo
+dP/r6/lubHf7fHBzef3z8Gxt8+rldmdvfvZ8cHD47O3w7Ojq4/dlXW33fGtv7+nn9HFpZ3L4
+em9ofu7+eHNxdv9zbn99d3VvbHb06+Lb19vi6vDv5O12amFma3n3fu3sfWxaTUZDRElSYd/P
+ycXHxsjO4mdWTEpNT2Tb0MrHy9LW2eF1am5ubvjdz8vK1GA9LyspKzVKyravrrC0ur/NZUI2
+Ly83Ss+5sa+1wNleRz48OTg8SGbHubOzuMXjV0hHTE9Zbu3VxL25usdEKyMeIC1LuammqKyz
+vsx+PzErKCs71K+lpKq26j42MjM3PEZmybqwr7fHY0VET3Pay8jExNH3Si4lIiErS72qpKas
+ssH+RzYtKywzXLqsp6iwwnxCOTcxMThJzrOtrrrzPzk9VNfM1PJq7Mi5sbC9VTEhHB8mPrms
+p6aqsrrWPC4mIyk52q+mqKuyxmpGMiwsLj7FrqensM9IOTlEUV1649LBvLq6wc7WYT4tHx0i
+Ld6sp6est8fZTjYuKSw82basq7K8yN9pRDQvMUPCsayxyVxLTFh9al1YXtXHx83Qz9XQ1VdE
+Pz04KSUuPbump6y+SDg+QUNDOT1dw7Gtr7vJX0E+PEJtz8jE0Prr1M3I1VxLRk/jycPM4WZR
+W3H+6t93Tkg/Pkk/OT9M9r+6vcxZPTs9RvvHu7a4wNDwWV1uaXh18NjT2ed3+ObZzt1tU09a
+Z/Dn39/u7/fc61ZGQEFGXGniyMjbTD89P0NFVnrVzs7PzsbDxtN4U09YcNLEv8PPeVNKTn7Q
+yMfL2mVUT0tLVfnS0uVtXlFVT0xp4910X1hPVk9HPjY4R+rDuLi+xd5XSkpU4Ma7t7nEe05M
+UnXZzszW2uBfXVxs5GtNTE9OTExb58nFzXxORUpaTl9lWEs8OD5c0Lm2u8ZfPzk/acW7ur2/
+xNhcTmLSw8DOZUtNXuPKxMluQzw8R1ZXas/KwshTQEFAREZHWtvaXEtPWvDkaG3SydbpeN7G
+wMHDw8rcaVRedG3p083IyeFbUFdqbl5PSk5SVVhe+tbNzdVvQzU8S1nPzfBZPzhF7c3Fx8/Z
+31tT3L+5uL3K605CTOXFvsXV519YWlZd9uJsbmBJP0Rsz8O9zGVENy0rOlvfysS+v+w/Pk57
+2MnDv7/O9+vUycfQ3Nh1TExb18G8v85gQDxGWV7nxsxhVG996NT7RS8pLTxS0L23t8VaQ01u
+5enc1t7f4t3KxNDk4ufyal9639XNxMDH3mBTTkxOXnzVzd9edtLeTzgxLzM+S9q9usbnXFFW
+XmvrysHL3/5qX1pQ+cnCxtluW2zq0MLBwc1lSEtt3szEytdkTExcUkJGPTMvND1gxsDAws5s
+WFta78vIy87U4XdXUl7u6HV+1NDb2t7Y0959Z1xOU+bGvL7H2mZc7t9USEtCOC4uOnfGwL29
+wt5IOz1P9s6/vMTZWUE/S2jWxsPK4F1aYPLPxcbUb1JPWHvZyb6/zHZZd87CyV86LCYoMlC7
+rq2yxFo5MjM+dMC3uL7iST06P2vIvbnA+EpHTevEvb2/z1A+PUrcwL2/v8HlR1HNyfxHNSso
+LTVZt62utspGNzU5Ss+8vL/NW0dCRl7IvsDI0n5NSVPcxMLF0vRVRkJM3L+6vcjbZVd9x7zM
+QC8nICk+zrGpqrrmOS0uPWTFtLfC11RAQVPbwLy/z25ORktlzL+/yutOPjxBaMe6uL3JZ0hP
+zbi72kU2KB8mPcGsp6271zstLTtuvrO7yt5QPT9S1b68zGtTTEpU5MW/w91OPjtBXcu7tLO8
+30pH9MG7w+FOOCYeJkW4qaivv+c5Kys497qzvtLeXkRBWNHAvcpeTE9Ta82/vsb1Sj8+SG3F
+u7q7y1pGTe/HvMdgQzcoHylQs6enr8juPi0sOHG8tMLf2ONMQU10ybzIXU5o8N3OycXH60Q4
+OkzTvby8usFvRk7bvb9POjkvIiM03qylqrnLWzcwMT3ftrbJ1dttTkxP+sG/21dUZN7Ny8vH
+0k89OT9twrm5u8LgSkRpxr9rOjkyJic3arSoqrnIZTs3OT1eu7S+zvtZVl9ZWNbI3F1UX9jC
+vsPTb0k9OTpMzLi3vMXbZU9W2tTpfFc9LiksO86vrK+3x1Y7MzE6c7+7vsvke2BPTFn02tDP
+0tPRz87dfmheU0hIX8u+v8PCws1sSD49SVM+Li06Tsm5ury9wOBTQjs+XtXW0tba1t5gTExZ
+3crIzMrHzdb7WldsblhWYN/KxcfIvr/WUjw4Pj81LS5A8L23vcLAweBOPTg7P/u/vLy9wd5P
+R0JLbNvMv7q8xt5TSEpNTlrk0svJzMzIxdVQPTo+REg7MzhCWd3OysO8t7nHXzw5QEVKWd7F
+u7a6yNthTUZJV+DFwcPM32lRS0tPXPXn49LMzNxeTktQWFdVVFFNS0pKT2rcz87P1t3obFNK
+T3bSycbEwsTQcE9GSVnd0NPV2+dhUE5PW2r59Obb3N96aHbs93v1+nRfUVFVXXDf1Nja3Opr
+WExISExXdNrOy83W5WRXVVzw1c7M0OFwW1VWYe3WztHZ3+trW1RVX33q5vdlX2JnZ2lra/3l
+3t7qaldPT1NdZezWzcrO7FdMSVJlfeXTzs/ZdVdPW+zV0NHU2tzmal9ibPby/2tfX2xvffV7
+fHJtcGphX2htZ15bVl/z2tjg6nxhWVxdZfLY0NDcemJaXHTi3Nva19vd7GxobO/q8mhVUF7+
+5+bn4eLtfmhXUllhY11eZW7s3Nvc5/x+c2pnanrn3+Hn9GtiY2p2fu7e2NnidmRleu73b2do
+Y2Nrcu/j3dze4eX7bGZobmpfXGt1f3X4+XVnXVxYXmvr3N3c5Ovy/P97/PLh3N/pfXJvcPjs
++nF0bmtmcfnu8f5+c2tmbv/37vDw9HNoa2x+6OHn/Wxtbmtran/r9G9nYGRye/Xy7+z1bWZo
++uzu6evx/fXo6d/a1djh6PNqXl9paGlrZ2VmZGpmXmRpbGtkX2l09e/g5PNleNTj19bU2nRb
+eOxzTkzb2fbs4uHh7Vpm6PB1aWdfUlj77Nfb1dje61ReW1ZZVFlba/h+4d/d2urt/P57ZnB6
+8OLo7fr7+u3d3d/j6/D36+/35eTo4vtjanRbST8+QT9ES1bo0MjBvbu9w83ibFxOQz9BQ0pa
+/trLwb2+w8vR3XFcUlRXVV/y2dDMx8HC0VA7MTIxKy42RWrOu7Grq7C2ucVbPTYyMzU4Q/fH
+wLuzs7a7xddsSjs5PT9CS27Owry7urzCz+ppUTotLzUuLTlT2sS7tK2rsr2/0Eg4MzAxNjpI
+18K+uLCyur7F0PtMPz9CQUdX7NDHv7u7vb/B0k0+NS0tLCovPEtywbWvrKyvtLvXSj85MS8w
+N0FR3MK4tLKytbi/z31OQTs5Oz5IWu7Lvru7u73EzvpOSj0sLTouLTxKV9q+uK6ttbOyxF5a
+SDc0NDc/R1DVwb64sbS5vsfbVUE+PTs6QlZw1MC6uLi4vM36RjQzLygrMDQ7WMy7s6+tq665
+w85UPTk0MjQ3QFb8yru2s7O1ucHdXUs/Ojg7PUFP6cu+uri4vMLK1GE+MzgzKjA5OD9c0MC4
+trCut729y1dHRz46Oz5HTFPYxcrCvL/Fxs/ie1tRTElLXW3/0MXHyMLD1mVUQjw3LzM3NTtN
+d9TAubWztbe7xd5hUUM+QD9CSE5f+tDGwr/BwsjV5HxXTU1OT09WcOPZz8vJxsvQ031RQEI+
+LjU9ODtO4tzLwLq4vr68w9r581dMSkpMS1J+d+/Ry9DRxsrZ6/5xW11+92712tng3Nbc9Xdu
+Tz8+RDo0PkVFT3HRxsXCvb/JyszlbWleXmNndvB25Nz23NHZ4+XmcmRveGFUaPlvfOnf29PS
+1Nt1+fJcUkxCRkE6RktGU+7p69LNzMvO0c/T39/fen37bPV4a+fh5OPh293d3+r2cGxlZ/fs
+9+/s7fTzcXv7WFxiUUdGS0tESFpYW+jX29PNysvQ09bfb2x3YV5y8vDo3tjX3ODf+XL+aml6
+c3Dv5eTe29vZ2eLqeFhYU0xNTU1KS1tUS156Znri3eLg3+LzdO3d3ezh2N7d09LY29fc7v56
+emxlbfv78+Hf7fHi62hhaFlQVE5MTlRdW15y6+3s5+n2/f5vYGNueHZ68Ofa19POztLQ0N7o
+7mddV1NVXWh3//nr5uTp5uPpfnFrYmRbVFpeZGtrb2178efg6u/tcl5jcGpncX1sc+jk6+nf
+2N3m6vL+eGxkXWBud//z5eXh3dzj5uPr+mxeYGJfaW5w/O7p5+rvf2tlXFhYVVVVWmFv+erk
+2tDR1tve4/F7cWhhX2VrbnH24d/k6N7e7vjzeW5vdnNnZ3BqaWl49u3s6P1lfndnZGFfXV5o
+bnfx6uzv6d/k5eHm6ev09/B2cHtxbXb3fnd8fnVtffl7d/75dW56e25z9+/z/vbt8vnv6/1u
+b3Rsbnp57+Xm8nh2e/v+9/xydHh6dW1scG1r+enq6e//ePj5fXlv++/sf298+37y6evz9vp8
+b253+PDzfHJ5cnn1d/nx+G5qa2x2bG388fDt8Xdwfff1+nZ77e3w6eDg5+fn833+/HV3fXZ0
+amppa2lvamFoaXR6dnR2fv55cPv08uzw9ezo7e3t9u7q5eX18vV8dX16bnJucXNuc3d48eTr
+9nxtdnpyeH9uZm9rZ2x0/Ozq9/Ly6+v9eXP7+Pv8ev3u6+/37fB0b2xndPH7d3z87ev2d37z
++P52a2Rla2tu/vLs6/Pu6ujs9/5xcXBzfXJ8+XlwbXD47357e3F1dPrw8u7t8nt0+OTpdW18
+eHd+cGpu+/D0/P3t7fR+c3l2c3VzcHB1/fh2cnj7fffw/XZ3fnx+ff32d3zu8O709u7u+nV9
+fH3+/nlvcHx9bXF5+/X6enny+O7u7e3w7O/9/XttZmBnbXNucv33d3z1fPjm5vJ4b3Z4cXz7
+/vt+/X16fv358u30+PLy93nw9Pz17Oz19H7+fG19bmlpa3Jx//11cHNsbv35/fX8a296/ft/
++/Dr8/b2+O7u7fDw7+96c3Z+8fJ+dHl/9fj7c25vbmlkcP37+e72bnz183598fJ7c3Nrb3h6
+bGv6/3Z5eH707uzr5ubk7XtvdXn06/x4/fr8b291dm9scW9yZmdscXz2+u7m5+Hk7vX37vN+
+cmlpY2JfYGVtdvbx+Ovt/Hl9e/16d3h7fu/v8vh79e/r8Xlv/fpva2tqbHN4df72fnpvd/X8
+cnJ2dHv283Rs/vL5++rs8PP3+W9vd/p9cXz7bmloanRwcW5qbG94cmhr/u/u73zv+fv9ffPw
+6ODo9Pvv8fx4d3Rtampsb252cG1xePT5fW5tendtcm9kb/15++/6+fT5fvR4bXX77e3t/fb7
+9u/9bXB/fXZrbHx0eOvz9+3v8vH49ft1aF9oYmRiZWxvanj8eP7v7+jr6OTw8fv07ebn7fX0
+9fh9emtpbm1uaWdobmx6e3v07n1vefns8fx2/fv/dXrx6uxzb2ptfXp5cfbo4+ju7Ptxb3Fs
+Ymh7f3R3dXl2/X/z6vDs8/n7/f94bnrv8Xxucnv/9u/99/P0+mVr/u/z9v77/HN1b3X48PX5
+/np8fXNy9u3+cv/38PT5d3n9cW9qdXl1e3z97unt9vTr7u/y7/l8fnz2+XdubnN++Px4bmlo
+Zmhse/H19vh/cP3z8uzt6/x7dHh9dHrz5uPn6vH3/Pj99u75dm9pa/V+b29tb3J3b3V6dvr2
+d3J88PR0eurn7/N3c/vu9Or3df37cXF6+PF3b290cPjp+vX08fD2en9+dnZvcWxqeHFt9PHp
+3evwbXf8c3j3d23+7/f69P9wfe/x8vP6+21qamNlZ25scv7w7efm6efq5+7v9/n+7vB9/3f7
+/X51dHf/bWVnZmlia21sfnJ4efrv7On3793d4ubqen3573ZqdmtoY2Zrdfr5e3vs9vl4d/x6
+9/X66ujm6fDs5uv++/1vbnRrY2BlaXL/e//2b3FwdHn57+/7ee/u7fnz9ffs7Hx4dW9xbXj3
+7+56am5ve/f2/e7293xrdX3z9vvv6/H36vf3fG56fmxpa2Rta/31eG1tb3z4d/p1aHFyb27+
+7O55fO/q6ur+d/369fL3+fft6vl+7u/2eW5sa2dtbXBqb/T9/3f89vDueXtwe/H8ev/3+P39
++/t8bG93aXFxanV6/nj+dW717+/26e717nh4dfby+Xr3+e7uc2hx9/T083lyeHzzeGtscW91
+/H3ye212dfX1efr7f3j793p+e3R7fWxvbXB7f+/u6uXt/GpwdnR+7Pd5+XV9fnnyeXr9fvR7
+c3j3bmh/8u7s7X319O/v+vn0c2l2b/59fG9veP14/fd3dmJpcm5qcnpodXF49uvh5ePc297k
+6/Z99fT89/xybHBtc29tZF9fZm12cHj7+/t/e25vd/T18u3s6O9ycPN2fHt+d3ft6fhwcPn4
+dvny8/dxdXf/+vx9b3J+9Pt2fPX8+39ubG92/Hdrc/Xr6/P5+/rr6+rt8/r+d2htcGtocnx2
+enx8fH/+7e5+/fL3an309O77+/x/f3l8cXF+9f3+7+3teG5z6eby93v67Pvu+XV7cW1ua2xv
+Z2xs/nZ67n3z8u348H715eXn7evw7/z9cnp+fPx1cWlsc2xsd3RqZV9kZm/u5/L76+Xv+uHd
+4+jp8PPt83pqZG5uam9tcPz8emxoZGd5/376/O/p7OXo8Xxv+f3w+/vs+nv+6uz0fvZ0Zmds
+f/r+/f1va250+fLu7fT8/nlyb33+73p09n//+PJzbGz9fG1xbvXs6O3t7/fz7+r39uv2+H7y
+8Hxvb2t7+/97enRtZWdmZnn3fv73d/n79fjx9PfqfG9y9Orq6uPm6u/7+3ZxbG5tZ2z18375
+9u3zef9/fvz4+npu8Xp3bWp3b3RxbWxocnlwc3Tp7Prt8efo6erl7ffp/HNsb3p6cXl7fHr+
+f3t9+f11amT+eG77e3d2aG7u9+7u8Pfz697j6uvt+G95eGxpZmxlYGZodG1ke/B5efl+9vzy
+++/tffr/9Xz36u716ubm7+3x925oZmhsefP+e27683ZvbW92/nVwdnr5+vz38/z+amZman7v
+7fn8dHj4d3V96eV/+Pbtcnj3fvzw6Pn7/fjxfn15e3hnbHx47/f7++Tfbm50cmL3/3doY230
+4u5/bnhuaGxmZm9+bvD33uH39+3raevr5PL41svafmtcT0pUXV5j/Nva1drh8eFxXFxUYl1h
+avTl6+7v3vL37+nocP7xeGl5eH169vXy+W9/eHRpbe32dG198nH6/Ozzdmh992hv+/t7c2v7
+c29q/9zd7np5/HZs+3D7+274dWpcZuv57G/65+Z2dHp5eWh+/3D+Yf/q9Pr13uXu3dxjZ11a
+X1x0b/bg+m/9cvzr8Ormenvl7fhgeulsXVhobWr4e/H4fnh4/Wl+7O5+9ub06/Lx4ensdXJ3
+c213ePv+dWxvZmd6eXfx9Xbf/v3va3lsaGB0aHFubvLs8OLp7etlaHt4ZGPd6fbv6u98c2xv
+Z3V8fuppb253d3P9YGZp29lq2dlr3+pt+PxaXmZ18XLm4vTo93RmbmteZmFkbG12++94ZWZs
+bXrZzXpj6uLraF/wWsWzRdrY11xZVPnyQ0xN71de4s/o8en94131YO52XfT17mPz8OV9fPjw
+7Gb5cnp8cnDl6d/b7+Pv+GxqYmz99W9f+Wzldvfy8n1w7fPyeXtrXVxk9vRzffPv8PNxee3s
+bXZ0b/rs9O76bvB59vbidmVv/Hp66Hns9O3x9fr5fG13cnZt/15ob258bXr9cnvwcm/q8Pv8
+/nHpePri523v2+Pmc+987nvn53VtW3JcZl9ob2Nsc/78ePd9eGhq4ePj5Nniefz87nxs+Ofu
+fv/nb2d5bur9Y3v9bl1aaX9zZHR072hv6u/e9+zm3Pbr4Hj66+Hu5+117npqaXxuX1t08G54
+dfNwYmn38XL7bG56eW5qbm58+m5/6enh2+d/6f79eW/9fuvi8v/yfW/9/u74b2x5cu3oc3pg
+an55a/vx+vVqYGRzeu7s/HX5+3969O5iZnV2cP/x6+3r3OPy9OzvcHRv+XJ68vV2dv516+5t
+cfH2fHz/Z2R3de38+fR++Xj293hoZ2lrdWxt8/R5/vrv497xb3v57P7t5+Xs5eP5aGJrdndm
+Z/p9X2hw8uxx+XJmZnH07vjy4udzd+r47/l48+rv9+nu9XF1e3t7e3ptYlxneXZub3j8fG5w
+fXlsd+ng4ePl6Ojv7/Xw/W1pbvZ/cHf5+/t1fu/3bXF7Z2v87fZwbnvq73hra21y9HN+8Pj9
+dP/76+XtdGRve3x8/fX86+7x6+73ffl17/Do+G9wa3Pu82t6eGtnX2JoZmpqdPf68/jz8fPu
+59/o7vF4cmRs9+fp8er+/XlvfPz8aPzt9nZ4fv778OPq/WtiYmpubnZqbG58enrt9PV7en7t
+5/LzfHL9fv1vbHJ77ezd3ejq9337eHB6dW5zfvt0bXTzaV9tcG1ucm719G769Wph/ejz7Onk
+3ePq8urud3Zqanp3eXry6+17bWhtfWlxaWZ58nxoa3zq7vrg2+t1/ezs93z99Wpobnzvb2hw
++29t7u/tbmvp4/Nvc21ubW/5fvnx7+3p5fDo7Pb+ZWFqZmry9vvu6+/76fj+/nv9bGhxb2Vt
+/Ozvd/r4+ev4eXZ09fR89O3u7+96+vT3dWt2bG1v+355c3p3cfvs3+59/n78/vf2dfvu6u93
+7eZ4bmtlZ25vb2hq+ur3enJtfu3u/nr9+fr9d/Lt7+3z5+rr5O37fnNzc3Rub3x6b/Ds/PVt
+b25t/vxvZ294eHF96Of/evt5ePj99XVy+fXr73559O54ePr1/Hv/++v3/npobnD89fz58P1u
+aW51eXv89fTy8PptbHhtb+v0+ezxbnb9ePf0c/Xt7/du+evs7vl+7vB48+9tbP52bW1mb/1t
+bnJ68PJ/aWv293NzcWx7ef3xcXzu5ebr9O/qevb2+up6b/p49uvtd251d29haH5/c3Bub25v
+7vV6f/Lu/3R66/f+8/96b3t9c/f5+fJsbe/q/Xd9en96fn78fW76eGlseO37b3fu+nv37/Hv
+6vD8//58fnhueHxxb3rv73d993lrcX9yY15r/nfy6ers+f17e/zy6fducf1vanP34+v88PT0
++Xzy8mx2dWlkbPb7/P5x8+vu+vhyfvB3d29y+/P7e/16+/pwfvDq7HRlZGtqbnT+9Px9eG5s
+eunf5uzs5ej17Ony9/x+eWxhZm1gXm96bGxxfX158uzw7/P89Pz96u3+bH/2fPLv8vv+9fV+
+aG7x+3t3fv92+/Pz7/X7dm1pcX90fvt//ftzf/txeHRudP78/3H85/F0dvry7Ptv+/f+9fFy
+bvTu7HZ36vVwamz09ntrd3VnaHX1ePz/9fFzfu7w+fnx7fL19v54/vz37vltbm1qam17+/pz
+bXL89vL+df16e/HueXzz7fd59fL7eXV+9/H9cnR0cX72evn5+npybnf9d2tvdHj8//r5+OTl
+7ezv7Hh4e297eGt0dHX07/Hz7fNva3Rydndvam969vt1enr+9urq+fz4/f71+fTv/f7x7/t1
+cm5oZmpu+vn5+Htvc3J59f11e/7/8/336evy9O3v83r97P9vcXVvaGx5/XVsb3h3df739fr0
+8/z97efm6+/49O/48vF5cXl5bGlpa19fbmppa2VuenZ19Ojq6uTq6d/c3d/z9/N8b25rYWNw
+f/v+9v17eW1la2pmdXZ7//jn8fvz8uzp6uzv/HNubGh193txc3Bsdf77fHzv7fL9ffH0fvjv
+7/H09/p9dX3/8/lwbW1rbWtpamx1+/d5b3j2fPru7urm5ufq5+Tm9W9pam5sbWxlZm5vcnN2
+/nJtffjv9X199/Xn5ufp8/Z9eHVue359cmpqeHxz+3Z+9ntua/3t7e327/P7eXh+b3Tv63xy
++/l+cHj/dG92fnF8/Pl0Z3b47/f++X7y7fbp6e3p6fH79/v/dm1pZ2dsdGtoaHL89ff2/XV5
+/Pr/cHP39Ovq7e3v7/Pq7PL1eGpqamp3d3RxbXH99PH/dHd2fH3z7Onv+/n09Xn6935w+u33
+/H7+bWtubnJ3fH1yb/18dHBscX339Ozo6u7z6env7vZza25ze337+v75+PT2+3JydHV1d/58
+b3F8fPr17vJwbXNpaW1xbWVz7e/q6/L16+jn6356/vb+/vPw+Hn99fXs6Or2/m9vdnV1dW5o
+aGVlZ2hia29++fvw9ff29PLv6ebs+/vx+PPt73V0fn35eX37+Pbr7P50aV9iZGt2dXt2ffj7
+++7u8fZ2b2929evq+Pju5+fs8PL2enVzcm5qbnVyfvr6bmt3f/h/eXlvbXr4/Pfw7e/w7fL3
++HxraGxrfe3w9vH0+379f3tycPvv+P99fnV77ez3/fD9+XVoc/j39/N8dm90b2l29vPw+nZs
+aG596+nl7PP4fnv7+np3amZtdXJ5fXBs+ers7O74ef35enL/8frx8vTq6vl2b2hteHNzb2tw
+dn//fXx4dnl6eO/t8v788e7q7/r38/z47+vv9v11a2drdXlrcf56fHJ1b2xtevjv5+r1bnB+
+7+v4+/vz8P58eW569fL+fHP5eGp2/PDo5fBxbHL++Xx++3j/e252dfj0e3r97vh1eX317+/8
+9X51fHp6bnv19v3u7n94dHBzeXhzc29zb29vam7+/X397uzr6unr9ezl6vHs8nNuc3j+eXJu
+bXFrbHV7fH57/f538+jxfv13bXZxb3n87/B9d3FzfPv8+PR9a2Zsb3f78Xp96eHg4unr6fj5
+f3R1e3ZsaXL39vpzb3R3fXR5eHr2+Xl0b3J4eXt99319+fr99vL0+fx/9Pj87ujufmxucm94
++/1vdP3/e3Zvdm9vd/rs8fnz/Hnw6/F+fvf193z37uzv+ff3bmp/dGhxeG5yffX8fHlv//L/
+dHBtbv7vfHT+/XVvbnft5uDm6fF87ev5ff79fn/5/Hx+/3p3fHd4dXlzcGpsc37v7PN7e/r6
+evr8cf79fHJu/vr+eXb++3x+8+zz7ev3/P34/3Nrbvz27+98/H5wcm50/+7v/Hp9fHNzeP/w
+9315a2p97Or/dP3+ffz4/P759X51cn33eXl7bWtxbXZ4bXV3bnZ48+zu6ebk5uvt6u/2/f59
+eP96a2pva3R8fXxzb212dm5ybW54eXF2c3F9/fny8vT//fTx6+Tl9nj+/Xj79vjz9/H3/X79
++Pfv73xxeX1tamttcXR8dnX7/v1tanB4fXd1e/p4bW9zdPXr6evu6u/s8/vv93h9+vB+d/t2
+cP3u9PtqZWxpaW38+PDn7/X57OP4cHj+9/l+dG9mcX1qaWxwdXlu+e7x9vHs7e368/x2fe7v
++Pt+fn1+7OLn7PV7al9gZl5bZ/d1bnr48P149O/u6ePh7Ojg4+Hc2Nfe7Xxzc3ZmX1hWWlRO
+V1hQbOz78ujo5/Jve/tpavPr6t3Sz87NzM/Z3e7v/V1dWkpCSk9HUfbi3dTOzdPr83JSTlFO
+TlFn4t7czs3d19Pf3trY09bb19fqZV1LPUJHQElf893NycXL3OVrTkdHSklMX+He1cjFz9DN
+2Ovp1dnSzMvJy83kSjo3NS83QE55z7+7vcLF2VVNREBERFPt2Mi8vL+/y933aHbx3tTSzMbJ
+Zj43Mi8uNEBU8cW3tbe5vs9XQz89Oz5M+tLIvLm/x8rUf1153t/Pwb6/we1dPScsMSkuTnbQ
+trGuscDG0EE1Ozk0PW7Hxruvs8XLyPpQYtvKxL26wdJrPCcpMCUrTWnkvLGtrcHKyEAwPDgu
+PtXLvravr7/Oydti2ce+vtLEwUouJi0uISx47ve2rKyuvL7POC43LyxD6s23sq6uvMa+y9DC
+w83NydxEKyQvKyA23FvXsKqrsb68ZzAyLyoxRV7HubGqr72wrrm+u755a0csJCUtJiZJ9n69
+rKurtL7ATDMuLi4wO+vDv7Grr6+pqrKyuczgNSEfJiEdL21nza6mqK66ucc7LS8vLS9MzNS/
+rq2up6erqK++2i8eHyMcHS9La8OtpKStt7XUMy4xMC0yX8nKv66oqq+spq26wkQpJSMbHSUs
+POC5pp+nqarCPzcxKiovOVPQuaypqqanrrC/PS4wKh8iKi01QOm0rrWuqbC/zd1XPjU5P0Jb
+/vvJv723uMDFxd9HOzcxLTM7P1jYx7++v7u+3f3ebFFn3d7gz8bBy/5fW01BQktSVEtJWXBX
+SE9+YF/gycDCwbi2vcLExsvhRTtFPDAvNDg7OkDNwODKsbLBxcC/zFFfw89my7u8xOlTbEMq
+LTosJzE/UGtPzLK+x6+uu7evr7jJubPTeNo+MTYqKC8pKD9LP+LN0L7d2LXF/bqvt7y0q627
+vMZJPjQoLC0mLT4/XtXOusDfvsFo2b/DzL6xsrq71UJMOCgvMSkySUzUwr2zxdi801nLvsDE
+t623v71NOEAqJDItKjxKUc/Lu7vWv79qzbe6u7Ktr7e66Dg5LiMrLyovRFTp17+5z8+/3OK/
+uby6r7C5trxEP00qKTguLD1ASG7Vx+DXu9l2trXGvrC1v7e+Qz1HKic1Liw/Y37aubHGzLbT
+Wb6+1MO0s7+8u0c5SiwjMi4oOFRNX8O0xdO1w3e4s8K7r7C8u7tINVMvITEzJjVdQUzHucnV
+trxsvbLGxLKzvbq36DhPPyIrPSgpUlE+5Li/2b6339e1vdC6sLu9sLpIReMtIzszIzNbOUHG
+vs3Ourv9wLLAw7Oywbu0xzxDYCkjPjIjOeA9S729zcu/webWwL/Iv6+xvLGufjvcPSAsPiUm
+S0k65sDDxsq+uttevrXNxa2xv7SyVz1nNiUuNykrP0xV7da9utXYvMpVzre/x7Wusba5zEA9
+OSgmLSsqNkFPzMXGuLnTz8DK1Ma8t7S3s66zwls7OjQoJiosLzQ8/MTYz7++w8fGw7+9t7m7
+s7G4u7/nOTQ4LikpKSw2OkBd6s29vL+3tru7ub2/urW6u7a5xWs8NC8tKyckKTA6SfXTybqz
+s7S4wMPBxcnMxLi1trK1yE0+NiwqKCQlKi477cfGvre0r7C9x8vb0cO/vbWxsrO1w002LCcm
+JycnJys788G2tLi5tK+0uLvG3t3Mxrqzt7y8vtZIOS0nJiYlJSkuOmLDtq+tra+ytLi/xs/1
+XezKwbqzs7rB9z0wKyclJCQmKjNKybSsq6utsbe7vsXO6VRKSlrQvLWxtLrIXTsvKyYjJCcq
+MD9sw7Kppqerr7jAyuVXRj47PUjqvrKtrK20xU8xJyIgIiUqLzlTyLWsqKeorbjG/E9KSkVD
+REJK576wqqeqrrjoOCojHyAjKi83Q33GtKqnpqiuvdxOPj4+PT09PUXru62loqSpr8JHLSQf
+Hh8kKS43SdO3qqShoqevx003MC8wLzE1PE3Ns6efnZ6jqa/EQykfGhcZHSIsPf27rKWfnZ6i
+rcBONCsmJCQmKzJMvayknpycnZ6jrLnzMR4VEA8SGSAwbbuqnpiVlZmitUQoHx0cHSAoNtSu
+op2dnqCkpaaqt2gzJx4aFhUWGCA316ygnZqYmZ2lvDkmHhsbHyQtSLunnZucn6e0xvZJRUpR
+ZvPc4EspHBwbHiwzOmW9qpuVmJ+tTy4uLCYmJig8uammp7G9uLi+2joqKzVWu66tq6mnrPcf
+ExMUHS44Srqhl4+Pm7VIKiUlHRcaJUupnaCstLi3u1UqJCk30be2r6WdmZqgujEZDQwRFB88
+z6aVj5CUn/QuJBoVFhokz6Kdm5yltc88JyUqMXOyqaakpaqqr77LVjMoHhQNFCUuz6qjm5KU
+obVVJh8eGBcnzqydmqGkorA6KiUgLWHcw66uvLu/z7m0xMK3uMPkLRMNGiUjN+WwmY+Xqa3A
+MygfGR75treln6eruC4dJiwsQ8e5p52jsrjRRFJyTunDznXoYyIOEiwuPrGlm5KVrs7RKBkY
+GBxOq6+kmp2rtPUnJCwqOLyurqapxGY+LjNxzNKypaCks2ojDg8dGhs/p5qWmamrqTsbHCEm
+PcG3p5qdsLm9NyYoKTLhydCwqb5YV079u73OsqisvFo1IQ4OKCUe4Z2YmZmipaU8Gx4rJSM9
+zqucn6+vsTgiJyknMVbAq6WvuK21+1/O39/F1NW5vj0fEx4uHSTEpaehnJ2ftDEqLiAXH0DE
+taqinp+3Ojw9KCEtTde7raimrsTEut5AcMfN3md4PBkUKyYaMbGtqZybnJ6+NToqGBgoOD+6
+op2cobS/zC0eJy8uOsuvqqirray34NvfRkBHQjooGiA9JSFsr7Sqn5+fpsdeOx8aJDIuOLWj
+oaGorrhJKiwvKi9MyLSurKmqssXNzfFOREJJMhwbMCYZNbS9tJ6bnZ6uv9ooGh8pIinMq6qm
+oaGrzkVBMSQrP0ZwvrGur7Gxt7+8v+1dRiwbFyQeFi6/xq+alpibpa7dIxkdHRklUbmooJyb
+obO75y0lKi0uPPy+trOuq66yr7jq1z0fGRoeFhc5zcqolpOXm52kVyAcGxYXIzvPrJ+ZmaGn
+q30tKykjJzJE5b2tqKmoqa6zx0wvHhcdHBMeQEfFnpeVlZqbpUMkIRoSFx8nR7Wim5ucnKS8
+bjwqJCcrMEzHt6yop6aqu2w5IRsfGRYiKi7Pp5+ZlpiaobZULB0YFxkdKj+5o56Zl5yhqc82
+LCYiJy43Xbuvq6iutOUsLi8bHSwmJ0q/tKmhnZykqavFMSooHRsfKS1LtKegnp2ep7bKRy8r
+KywuQtK+ubOvZzfdMh4uNSUsWeDFt66lp66rrMtRWDYqKCcqLC9Zv7mqoKKmpqy+ZkU6Mi42
+WltLzL0/Odw2Jjc7Ljps1L63squxu7K8Vk9mPC83OTQ3P2Hcy7itrK2sr7vMeU5CP0JEWl5R
+5GA3PUsvLT44NFbNy72zrq61uLvTVklDPDI2Pzs/XOfPvrmzsbOzucrcak9KTU1RZExMWDk2
+STsvPEo/XMrAu7m2s7a/xMxaR0A6OzY3T01U0cfGvbm8vL7Cy+N+YFxYfs7UcVpUPzU5OS8z
+PUBO3MW6t7Svsrq/y2VGPjs2MzNHTD7Dt8i4rra8vcrWW0pjTD9U3vjfw2dE2kIvPzoyPEZL
+4czFs7W6tLfDzNhPQDozNjY1Smrnv7e3tbO7wcr+V0xCQEhKT+HSzcHCV0/PPzNHPTM9UVjh
+z8S2u7+3utDh5Uc6OTk7PEPszcy8s7m7ucTab05FQj1DY+zVw9ZLy94xR1UzOk5MXc/Ou7bA
+u7fH7NpgPj09PEA9Stra2L23vLy5wddgTUY8OUJIQ1/Oz8i9vb/J4GpNPTs/ODZHTEnwyMS9
+ur29xtvuWEdKUU5e2cu7s7nBvsdEMzg0KSkzO0fouq2trq2uvv9QPjAtMDQ3Qu3Aurawr7a/
+zfZCNzc1MzhDYc2/ubOxtLzH1kw6OTk1N0BU7s7At7W+x8tuRz87Njo/QFPfzsS6uLy+xtj9
+VEdERERLXO3Xy8O/wsfH0vJvXUk3N0Y7Nk7e3Mq/vbq8zd7fUD4/Pz5FU/nPycK9v8jWXE5S
+S0lV9t/Uxb++wsfL31pSRSwqQDYyZtbFtrm/tLdfR1ZEOz1DVtDLybu6xtdnUlZNRlrcz8jD
+xb++ycPBVC8tLikrNkjaurKvrLDG/0k2MC8zQta7s62ssLzfPTEvLTFBa8SwrrS4wmpJPzk+
+V+rTvLO0srvWfCUXKC4mQc63n56vtrBWLyslKEd8YK+mrKy6Tko+Kis8ReSzsLKst/hrRjhE
+TkvJtri3yTIiICgvPfm8pqCptsxONSwqLT/LtK6vsrjjOi8tMD5YxrCtrrjRU0I9PERxvbO1
+srC0ul8pHRcYLVrFqaShn6xLLysmLDVBxqikqrPSRjkvLDNO0rqwtLm++UlJRkzlyse+ury5
+ubjJOikgGRoxbLajqquntz0vKyg7a9qyqq2xv0w3NS82X97ItLXAzGdJV/Tr19zSwsHAvLu7
+tcM6JhwSGD7WrKCrqKS6PjEnKE/byrCvtbPFPDM0LkfS+r+1wsTPSVPX8tnIefbLz8e4sa2r
+vzEfEg4iTbecpKmfqWw2JR0v2sStrLOutj8yLiY81/29t8zAxlBmzsq9vd1gZFvUvLOqprU8
+JBYNGDJunp2ro6fISi4dJlnJram3ur9JPTIoNdrEs67K0tNMYOL1yrzJ295V78C2qqi7Nycc
+ERQlQKiaoaWqzUs6JiM357KmrLrKW0U6Mi1GzbyrttjhTlfNyNXGyvnjWU/TuKumrzwnIBgW
+HS3FnZykqMFIRDQpL0Pcr6q3x9JQSE02PevnurXP6XReyrrBw8xVUGRf6MG1rKe/NScZExcn
+UKicoqWs0Uo2Jyc5a7qssbfAcFw/NjhF3bq1wM/zYtLFxcjTfl5kZ+K/ua6ouj4pHRgXHTS8
+oJ2iq75aPTQtL0LNsKyuvWhaPzlBO0jHt7e5zk5p4dzJ3Fhg9uDPxr+vrLxPLSMfHRwmTq+d
+nae16T89Oi8yRdSwqK7HST80OE44Xbi6rrHzTHde1b1+XNx42MTHv7OywFksHhsXHDzDpZ2j
+q7HcPDUrKj/Staqstc1DOTI1OUK+tLCwxGlPVV7TzPjV097OzMW0rrteLh8dGxwvbK6eoKiv
+0EM6MS45U8uxrbG9WDU5OzxT3sK2tcHXc1Lz1tnZ//TVzc29s7O3zzspHxoYIkG4n56orb1p
+SzgpLDp3sKqutsw/NUI4OG3xv6+5ydBuZ8vWZF5KY8W/urOvsb5QLSIcGR0v3amfpKq03Eo8
+LSs1ScKurbK8ajxJPzxWSH26ubm91nLT0OPgTUXzyr63tbSzv0ErIBgXITW+oqGlqLLG2z4q
+KS4+wK6trrjTTj07MjZDT761urzGzs7UU0dFROm/t6+vsrS8TSwiGhcfMMimo6enrLnHRiom
+KTL+uLGusLzqUks3Nzs+2b69ubq/wcDlU0g8S9G+ta6wtbzyOioeGBojOLqnpqWnrbC/Pism
+JzXkvrq4v8S8v99QOzhJdOXU09fCubu/0Fha5dDKycvIwtVcRS0hHiAqQsS1r66vrq6530E1
+NkJVWk5DR//Et7a9yM/T32tJPD1J88a/wsO/vLq8yNhsSj03MSsoKjNI07y5vL2+v7/H3G9r
+Z25hRjk2OUnjyL66ubm6v9JbRkZLWHjm18rAvr2+x9xeRj46MS0vN0FVb/Tt59nNyMfGwL6+
+wcvlTz47PkRPYe3Txr6/xdR5Y2h67/n/7+DZz8vT8VxPTFBXWFZadPl3W0xGRUdKVW/ezsjF
+x87c8WdZVl539Onc2Nnd43Vla/fazc3Pz9LPztXmXk9LTVRcbHd0dPh1VUlBP0FGTl596djO
+z9TY3eTk5ujg3dvd3eDg5/lvb/Ha0NLR0M/Qz9PjYlRQTU9bX19eW1NST0dDRUpUZvTm4tfS
+1Nbc39/e3+Xn+29yfu/r4+Di39jPz9ff4ePe2t7ifGNfZWZnYlhTUk5OTElIS1Jbcf17fP3o
+5N3b19DP1djg8Pj28O7t6+Hf3tza3N3m7ure4Ov6bm586udyXFRMSEhGRUhOV2fm3eLn9Wr8
+4N/h4eXe1dXc5evq4+br6uTl6OLf2dfb3+Hp8XhsaGdu7vRmWk9KSUlGRktPV2rp3d/b2tnU
+0NHe7fTp5e3vfHB/6ufs8//05t3c2djX2N/n7XtrbG1yb2ZZT01MTU9PT1BSWGBrbvLc09DR
+1dvi+3Z4dHzu597a2tjW3+bh3+Tm4d/h4OLh7fZuXVdQUExKSkpNVFdaXV5mcnR97uTc0s/X
+2dXc7PJ+a3nx5OHh3dra4OLd3eDl5Obl4uTn/mlmX1RMSklISUtPVl1keHhr+uno393a1dTY
+3uLg5+zt6ufm397n597c19jc3uPl497g6fhpWk9LR0ZFQ0ZLT1Vfc/rt5N/h4uLh4t/c29fX
+297b3NvZ2dzd2tjY2dnb4ens9v5mXVxZU0xJR0VGSElMT1lr9ebg393c29vf4N7e3t7c19fb
+3uDf3Nza19TT0dHZ3uL0aV9aU05MSEVHSUpOT09SV19t/vvq3tzc3uTh3dnW29zW19nY2tbR
+0dHS1dfa4ex3ZV9fW1BMSklISElMTlFWXWNu7Oju6vn36O/k3tza2dnZ1tnRz8/P0NHX19LT
+0tff/l5VTUtHRkZERUhKT1JSXGRsdPfz8Obf3d/n5OHb297W087OzMvMycjKz9DR1N3zaVlS
+TkpGQj8/P0NIS05WXl9jbHZ77+fh39vZ2tnW0M7MzMrJysrKyszLzdLZ5HZeVU1IRkdEQUFD
+RkpNUlVXXWZzdPzo397f39vZ19XU1tXOy8vLysnIy8/O1N3j+GVWT0lEQkJDREVHSU1QVFxx
+eXh5/vHu7+vn7HpbOT60sb+xrK+wtr2wuGA/PDcvLSclLTEuMT5MzrrFuaustre2vcR0Q0hX
+RzxP08O/wbivsb/QbkQ4LygkJygpLjhPzLy5r6ipra6zvMbsSkZBO0DswsC4raqrr8ZFOTIl
+HR0eICUoMOazr6uin6Gkrba67TkxMjA6WmvBqqanp6/fUkwpHR0dHB8kKlm7vK2foKOhqrKx
+0zc3Ni8/bmG4p6uurdBDbC4cHiIdHygrTrS5sqGfpKOqtrPGOjY+Pkjvx6+qtMZgT00sHh4j
+IiImMtOztrSlnqOrra+yykVJ0f5Mybe+yFovOEIkHCQnJS0xPbOqvq6eoqeptrWu3EbHzP+/
+y0hONyQpKx0eKSYpPkvgrausoZ6ioqavsa++0b+7101BLCEiIBscHyIsOz5durGvqKWkoaOo
+pKOsrqq0z9I9IR4fGxcZHCMwMzy3p6uooqGgpK2pp7O4rr9+1zcgJScdHCAfKjw3UbGurqiq
+p6Cnrqutrq69UllLIR0kHxwhHyZcT0G1p6ihoqWdoLK0rrW/4DxBOR8dIh4bICIpUm/OqqWl
+np+ioKeysb1gZk49TDQhJjAqIyYmOWY1R7Ksr62tpaGzw7G9XFNc4c3lLi5VQiolKT5jMjbA
+r7TBwqunvvvFvu9YQt6+REfF1kE+STs/NS9FTT9Hy86/v9e3tsXCyN7N1u3hc8b561NTX0RB
+P0M8Q0RYZU1Yctzo5tvKt9C6ur7Cw8DdzFDnXEw6Pkg4REZHWUo7SVRc+ubW3cDHvLu1usfA
+087SZ1BXUEM8Q0pBP0ZcZk1K3eLvfd7DxMrJycrV1/bgbVzpb+pvVEFOR0xJRWn/U1jP1cHM
+ybrH3/bvaFNWTmr7e01m31xYSl13V1JZ39ff68nEzM/EzOvrT1lfU0pcU01+ZG7442N7WVTd
+6Gpx3uXo3Obj421c6dlxzctbfehe4f1ITU9KWGZuXmzo3/7f3Nvv8NJy3cvn4tDe3uRm81Nz
+blNcW3dcYlRPYFhea+dqVul1/eLTz9jS4s/Y4vdoam55enHm51f8Y1JeT0vy4m9/+dLQ5eV1
+c+lqYmjvXFppbN/c3ezh6mZ37F9pcWza3Hrv3OHlYFldbV5eZX5r4+5n2t343edkfV5WbOjf
+3Wz73fbvbHZqWlX17P15aXDw2N3e09t+bGFpembo6VdjeFxqX+vsbW1aX3B499/o3txw3ePu
+69rVfVn05n5jVHFq/17/9HVbXVhm6WHp1ebg2OTrztfaXFTaZldcaHl8anXjZ1tefehnT2Bk
+Yvls5dx+2tbe0ur/2c7Z8fv8cmtkYV1XaF9oT1Nwcm1Z8m7/3+Pi0OPn3tzefV9b7V1aeXLx
+cvTh517zfX7qed/g53Xb729ganJdaF1kcmJpaOfeenXl2m5sV+TrdH7d2Gje/enj+HFXZX9b
+XnlhfmZn+vLt8HB34uHr62d/2Wjd2W/uZmrp+mZpWnFyWFd4d/h6ff7u6WD55N32bm3q5H55
+bex9aubj7O5zfXbi/OTq2+5wZWrj7PxbaFpwYFBOaFdKUVhjWVlh3Nfd1cnFyMbFxr/Dy83P
+61VLSUI8ODc7PD1ATGLr7OfGv8LKx8DH3d/OyMfVysLBxsrPYF5XQDs5NjY3NDU9R0tZ99TI
+xMS/vL/Av8HFyM7Kw8XMwcPFz1hGT0o1Ly4xNTIvOFpkXOXGu7m/yLu3xNXPzM/cac66ws/D
+u73LUUFYUDIrLjExLy03/OJX27q1tbvFurLEbd/V5mJJWL+6z865tLnOTE32PyopLy8tLC9G
+035nv6+yuL2+tbjqTe3kTUJE7bq8z72trbTEfu3gOCYoLSonKCw8fllfu62vtbm5s7rxXNbe
+TkdR5sXCxrmur7W2vvxPQi8pKCckJyotO1Fhzre1tK+yt7e8ytHXcGd0YnTTwr26uLSytb3N
+XkU7LiclJyYnKi88V+3QvbOxtLa4ur3H1NPP2e7p0cbAv723s7a8vshYPDguJyUlIycsLjld
+59O7s7Oxsri2tLzIx8fP1uNv3MfHzce/vr7G12hHOzUtKCcoKSouNUJx08O2sLGwsLS2t7vC
+xcvZ5PNjatfT2szFysvN4F5HPDo0LCkqLC4wNUTvy8W8sq+xsrO2t7rEzNLlbVtVW/Xf8O7f
+4tjS9H1yTz4/PzYwMjM0PEJI/8fFv7i4urm7xMPD3W7pb1xuamXf0+971N3j39/p6e5XS01L
+Pjs8PD9GSUpl2tfWzc7SzMvQ1NDW1dHU29fY7ejc5GReaF976tTPzc3V4fHsXE1DQD4+Ozo8
+Q0lLWHTf1s7MzMW+vsHBxtDha1ZQVldqaOHWz8bBvb28wcjic1RAOTUzMTIxMjg/Rk/x3MvC
+vr+8u73ByNLp2etaXvXa59PMxb/DwMK/ydXsWEdCPTYzMzQ1ODo+R1VfetjOycXFxL+7vcTO
+0cnL52vv2d3r39Xh2N/r6HlsXVZNSUxLRz9BRUdGRUdOXvPq69bKx8bEwr/CzNLQ3NXtX1pS
+W1laZGtff+Td2NXPz9Pf721vVEhDQUJAQ0NGUFhieN/XycTIzc3MzuLv7f13V1NhfHnw6tzN
+xcXNysjJz8/b72NTSkNBPjs8PT5BR0xXftnTzczN0dXX4tro7f5r6+HU49bQy8vNx9fJzcrL
+093k7E9IQkQ/Ojk6Oz5CRk1a8N3Qz87JzM3UzszY0Nnc1tvb1tjV3tbV2OX00ufe/mt5WVhP
+TEhLS0RCQkVITVFaZObXzc7QzcnO3Nng2+n68H7f/9n929bdz+rR5dfi+uZea2Njak5OTU9M
+R0dHTFFSX2nm4tXR19fXzNXe5vrkaXRq6+bp4OzZ2dfU8tfZ6MzS4Ofu2nFoSE9LRkI9Pz9P
+UUxXcdnYz9DVyszM3N17cn3xamfa2eLm6tzPxc7Yzs3X2OXkbO1NQkRDPztDPz1LTVNV29nS
+xMbMycXO3dPs5e52cFnqcu/j28zTwtXOz87L5tBvZ049Pzk+NTM6NkRLXW3ewMS5uby6wb3Q
+0dJpbVVWS0tcTmZ82cvNyMPCwMXT1e1bQTU6OjMzMjM4RFZW6sq+t7S0t7W3vcfT6lhNST4+
+RUpSXOLMw728urrDycrnTDwyNDQuLi4vN0RV8s6/tbGurrO2ur/J4lNFPz1AQkVKTu7KwcHB
+uri5v8vTcU46MC8vLS0vMTdGXdjGvLi0r66vsrjAzd5gST89Pj9ETVJ3z8XBvLu+vr/FznVV
+RTYvMTEuLC40Okddzr69ubGtra+0vMfTfFhJPzw9RElOVuHQx7y8v72+xcfJ2GJMRDguLjAx
+Ly81Pk1h18G7uLSwsLC3v8bUfFVLQz4/QkdPV2/Rx8XAv7+9wsrKydxdTUc8MS8yNTY2OD5K
+XN3Bubi3t7e1tb3O9VxQR0E+PkNJTlvxysTLy8C/w8HDyt9lY2hhT0I7NjM1O0BAP0dd48y/
+ube5vsPBxMzdWklFQERLTFNu7d7OxcPDyMrGyth+4d5rXllYWU9FPTw8QUhMS0pRZu7cy8TD
+yNHV1tbS23RbWl5p9d7X1M7Q1tLS2e9vXl9//VxX997Z1d3i/1hJRkVERERDQUROYOrZzsnJ
+zMzMy8zW6Pz6+Htubvzv+nNw/v576t3a0M3PysrQz8zQ3mlMQTs4Ojw9PT9ETFd32crCwMPK
+0dLP1eJ+aGFfXGN88Oje2t7c1c7NzMrMzc3R1dXcc2FVRz45Nzc6PT9DSFFv4dXKwb/Eyc3Y
+297h8mlmbW508enb2tva2tnQyMrM0dTQ1ef75+1bSj86NjY4Oz5BRkxWaeLLwb28v8jMztLW
+3fB4Xllhdu/o4N/p39HRz8vM0dbZ1dPa53pdSz86NzY5PD0/REpZ99nMxL++vsLJzdHe7O9v
+Xl1iYW/44dTV2NPNycjOzMnR2dffd1tNPzgzMzc7PD1CR09l487Cu7u9wcbJy8/b82tjYV9j
+b+zi3tTPzc7R0M3Lzc7W9GVYTUM7NDI0Nzs+Q0lVctvKwby6ubzEyczR2+xlUk9OT13r2dXS
+1c7Ev8LEy9bU2uxqWUo7My8vMzc5PD9LY97Nwru2trm9v8XO33ZfUkxIRUxg4NPOysO9vL3B
+w8bN1d98WkY5Ly0tMDU3NztDTnbMu7Sytbq7vLzDzuZaS0I/Q0xaW17sx7q1tbe6vcDEx9T9
+TDcrJygrLS4uMDhGd8a2rq2usK+tr7jMYExEPTs8Pj9ARmrCtrCvsLOztLi+z083KyYkJiYn
+JyovP//Bs66rqaemp6y3yN9bRTgxLy4uNUbux7y2r6qoqauvuctPNywkHx4fHyInLkLYvK+p
+pKGgoKKotM1eRDguKikqLDA+bcS3rqmkpKWorLXGVDQmHh0dHRwdJC9H1bWooZ6dnJ2gqbXH
+VzQpJCIiJCctOVe8q6alop+foqmyvn42JBwaGxkYGx8rPtqypZ6cmZibnqStxz4tJyEeHh8k
+Kzh2tqqkn52cnqGossVOLR8ZGRkWFRoiL0nCq5+bmJeZnJ+ltVgyKSIeHB0gKDRbwa6knp2c
+nJ+mrr1dLx8ZGBgWFBggLEXDq5+ZlZWYm56mvUsxJyAeHB0hKjxqvqqgnp2cnaGotNo9KRwY
+FxYVFhslN9WvopuWlJaanqOzUzMnHx0dHiEqO9e2qaCenZydoaez+DcnHBcXFxUXHCc5x6yh
+m5aVlpuirMU8KSEdHR8jKjrrtqifnZ2dnaGpsMRCKh0YFRYXFxoiM9ytoZyYlZaZn63LOCYf
+HBocIis9y66knZqZm56iq7nkPCcbFRQVFhgcJTu+p52YlZSWmqO7SiodGRgZHCEuWbSknZmX
+l5mco6/HTi4fFxERFBcZHytYrJ2YlpWVl5yq6C0dFxYXGR4pQbqmnpmWlpeZnqq8aTQkGREQ
+EhUaHytHtqGZlJOWmp6qyDgfFxYYGyMtP8KnnpqZm5ydoau7XjIkGxYVFhgcIi5gr6CbmJiZ
+m6Cv0zEfGxobICs3a7uroJybnJ+lrbbKSC4gGRYXGB0lLUDFrZ+ZmJqdpLDFRyoiHhwfKDJ+
+uKyln5+enqSuvGw8LiQcGRcZHSY1Yr2tpJ2bm52jr8s/KyQfHyQrNmC+rqSfnp+iqK69czoq
+IBwZGRofKDVgu62knpydnqStv0ovKCMiJSszS9G4rKSfnp6jrLvhPi0iHBkZGh4lLkq+raSf
+np6fpKq4dTksJiMlKjJA/MCwqKKfnqGptt47LCMdGxkaHSQuS8KupJ+dnZ+kq7jePS0nJCUp
+MEBvwbCooZ+foqu43jssIBwaGRseJS9Nwq2ln52dnqKqun47LCclJiowPXS+rqWfnZ6gp7Pl
+OSceGhcXGR0mNWm6qqGdm5yeoqu9UTAnIiAiKC9B17mqoZ2bnJ+mtOI1IxsXFRUZHSc45LKm
+n5uam5yhrMBILSUgHyEnLj7ftqefnJudoKax4TIgGhYUFhofKz3Vsaafm5qbnaOuyT8tJSEh
+IyguPPG2qKGenZ6gqbdqLyEaFxYZHCMuScKto56cnJ2hq7trNywmJCUnKzZSxq+no5+fpKiv
+wEsrHxsZGRwfKTdfuquinpydn6WwykcxKiYmJysvPfa6q6ShoKKlqrPMOyYcGBcZHSMtPtay
+pp6bm52hqbxZNysnJSUnKzZRxa+kn56foaart2YqGxUTFRoeJC9Mt6CZlZWZn6q39zYlHRob
+HyxGzLWqoZyamZuhq7tVKxsTDw8SGB4oSrOelI+Pk5mhr9guHRYTFRsmOdOxpJyYlpebn6ey
+2zQeEw8PEhcbHi3EopaQkZOWmp+yPiAWEhQXHSc5wKadmZiZmZufqr1MLR4VERITFhsfLr6k
+mpSTlZaZn69DJBoVFRcbIzXTrKCdmpiYmp2lsc49JRgTFRQSFRon3KqhmpWSkpWcqcU5IBgV
+FhkeJDXGqZ+bmZiXmZ+ru2kyHxYUFBMTFyA4v62kmpOSlJidqL01HhkZGBkcHzPLsqedmJaX
+mp+jrMw2IRkXFhMTGR8qQ8eqnJiXlpabpK/pMyofGRodHyc2XbWkn52amp6kqrpoLx8dHRkW
+GR8lLDzSraGfnZqbn6auvWI0KiopJCUsOVvJu6+npaqrqq/HSDk3LiMfJCYkKTdNfc+8rqqr
+q6qssbm9xmpAPkZHQUpt7c+/xsvCyWFHSUI3Mzc0Mjc+SVp53cfG0MjByse+xcO/w8bGzc7D
+xsvMyMjT32NKOzQ2Ny8uMzU1Plrr3MzEvry/w8zp283e5tHU2M/Nx8DBv7q5vsXI1mZPQDo1
+LisuLy0xP0lV38q/ubu+vMTaz9Rm89Dj7tPTzcTAu7i7v8DJ3+b2Sj87MC4vLy4wOT9JX9TG
+wsHAvsfPys70e9va39PNx7+9vcHExcfLztTyVkc/NzIxLy4yNzxO79THvr2/v8Tcc2xfWmb1
+4dHGwL++vLu/w8XJyc7gaUw/OzQuLi4uMzpEWt/Mwr2/wL7J3eB0WFZVatvRysC+v7y7vr6+
+wsXG1nNOPjkyLi0vLzE5RFX9z8G+wsLAytve6nBuZvXh2s7LysvGwsLBwsPBwsnV+05BPDUx
+MDEzNjtDS1/dzcfHx8bJzdDU1djb19nY1dfX2dnQzcnIzMvIzNn9XE1FPjs5ODk7PkJGT2Pz
+5N3X0c7NysnLzM3O09ra3uv45tvV1M7JyMbFx83afmFLPjo3NjY2OTw+RE9s5tTLxcG/wMPI
+ztHU1tXd6/Du49/c0cvGxMTIzNPrWktCOzc2NjY3Oz9LX/LbzsjIysjHx8rM0NXa3ef6fv/q
+2dHMxcO/v8TK0elYRj03MzIzNDg+Rk5j59fMx8bGx8jJzNDW2+Ho933z5dnTzcnEv7+/w8rW
+81JCPDk3NTY4Oj1GTlz84NDIx8jIyMrP0tbf5vR5d3Xv5NjNy8bExMfKztTfd1xNRT89Ojk7
+Pj9FS1Zs6drTz8/O0NHT3d/f3+np4OXj2tTPzMrKysrMzM3V3uxdTEQ+Ozk6Oz0/Rk1Yb/Lh
+19DOzc3MzdLY2t3i4eLu8Obg39rUzcnHxcXJy8/iWUdBPTk4ODg7P0hNXvDhz8jIycjJzNLb
+5O9vZmBganTu39jOyMfFwsLCxsvT7VlKQz46Nzg7PD9GTmDw4drPzM7N0dXc3uD09/Ly7OPb
+2dPNy8nHx8fEx8rQ3npWSkVBPDk5Ozw9QktSXHvo28/Nzc7Q0tPU19zg4Ovt5eLd2NHNysbG
+xMTGyc/fX0xCPzozODo4OkBKUF3r0c3LxsPBydLNz+d2bF1aYWhea9bUyL67uLm4ub7N41w+
+NzAtLCsuMTY9Te3TxLu3uLa2vL/D4F9TSENGSEtVX+rOyb65uLS1uLm7ydlePzYvLCoqKiwx
+OEdt08C5tbKzs7S9xcljTEw+PUJBSVpxzsO9tLKyr7G2usDRWj44LiopJycqLTI/WdS/u7Ow
+sLC1tb7O0l9GSz88QkBKfurMvry1srOvtLq7yOVWPDcvKigoKCouNkJO4r+5s6+vr7S1vc/Y
+YEZEPj0/QUthfM+/vbi0s7C1t7nH0+9GODAtKCcpKSwzPkzoxby2sa6vs7O3x9RvSUQ9Oj4/
+P09p5Me/ubSzsLG1trzL4VM8Ni8sKCgqKSw2QFHawLm0sK2us7O8y89fR0I6OTo6Q1Re0MO+
+tbKxrrK1tb7N3U08Mi0rJygsKi06QVTPv7ezr6yvs7K8ztNeQj86OTs7Q0xV08a/trKzsLK1
+uMDM5k0+NS8uKygsLS43Qk/cw7mzsq6utLe6yuZzTT88Ozo8QUxe6cm+vbi0tbS0usLJ2GNM
+PzgxLzAtLDM2N0NY6Mu/uLi5tLa+wcLgW15PRURLSEZW7+7Vwr/Avby/xsbI3upyUE5LQj06
+PDs0O0E+R1hm39LGv8K9vsXH183YWHpcRVJQSlVb/ODly8jPx8TLy8vJz9nU5fD2WUxEQj83
+Ojw3PEVHUm7Qycu/vsbDxNLL3G7oVVBkV1ptZmf/3NLZ2M7U1MvJy83EydvX415TSEU9NDs6
+NTw+P01c4tDKvb3DwMDLzs/e5mZeZFZUVVRWX/7u5t7TzMjExMC/wsfM0NjuXFFFPzY0OTIy
+Ojs+TGrU0su+v7+6vsLD1dHbYf9oU2JWS1ZaXX3y2NPYx8bJwsPEyMvK1eDuUEZDPTU2Ny8z
+OTpBT+3R08S9vru4vL/Fycvvb3lOT1VKS0tYaFzf0NXJwsHCxMPEzsvN+HlrTUM9PjsvNjsw
+N0NETWjOxci8uL69u77Hz8zfYnBfUkxRXE9PcH552MzO0MrI0c7J0OPn5n1meXBUTEdIRTY9
+QzU+SURWZdnN28K9ycK9wMfOzOH081x3XE58XVj0aPrm/uXj5NLY18/Y2dfe3vxz+mhjUUhP
+PThJPjlERk1WZc7NzL27wcC+w8zS0u1cdHJPUmFbVlpnY2Tt5W1/2dbe3NXieu/p/m7z3N9c
+V9lbQGFRPEVPUU9U399yzsXTzsXI29/P22Jz6ltSXWZvS1HXUE7S4XzZ0N/+19Df7NjV/enO
+3v7S1FXy3T9LXT1BSUNLVlZu3fTRydnOytTZ19nt9P55aV5qXVNdY1li6Xz409jTzNDOy87P
+z9ji1Ntmd2tNSkZBQD9BSE1MUG/k5tvO0NXSz8/W29ji+frp4Hxp/Hdqdu16XnXvX2zp+vXp
+5/F/7+bm7fTm4Obx+HR39fn+bm1dVltZV1tbW1pbYGJncXL16OTe293a19jX3+nr6uT9e/1m
+/H997vvp4/r+cGJx8fr79vj1c3NwXlxnamZwfmhlbGloaW5yb29++3Pp3+bm393i4N3f3+Tf
+4fz3/mtpaWRfYGZeYG91eHx4bGx28en6/vh++OLi7ero7XlvbWxpZGprbX7z8O7l39/f7Pnt
+63tpXl1iYnJxam11c21taP7r7vDr4eLl4O9+9nz+d2p37PL67//68312dnh/6+32//7z9XJz
++3RubmxiXmZnYWFpcXVycXT56/Ht6unj3t3e29ja4uru8/txZ2/6eXz8dnF2cGphX2FmaGVr
+b2tpa29vfu/s6vR/9/Dv6+Ph6uru9mxcavjdy/Ph3FJP9OhH591s6M7f48ZcZsjgRlR7SVJO
+Rk/sXEnk0Wr209ndzdrtzNFo/NHyXOlpW/ZlU3fdW1rocGvf62Tw52Rw4nVl/flv6fdj+ufy
+9eLm6eR1Y2prWFxsZWhycW99///m3ufm3+fh525kdnZjbvlya3/7c/H58uv1dXvw+3Ztd3B3
+enLr8Hf27vf27+7y8O77/vh2/nNvdGz//XF3/WpncHR5eHx8++7q7e7p+nFuYmhsaWdv7Ph+
+9+/07+r//O/98uDsbvXn+XFubmtueHn27eze1tvg29rs9PhoXEY8R0xBR2ruz8PLzcTMb19f
+UVhVVGDXyc3Kx8rM3VxNTUlJT1Zv3tLT2t3hc1pNSk1SXHzb0s7Lzd3i/F1TT05UcuXe0s3X
+5u1dUFVZXGn13NbT1Nze6mJaWVpdZWz35t7f3+tmYm1rWlxk/uTl497h6e/0fm9x+e198+bt
+/vdyYfnranH5dm1wbWr07/t6bfp+aGVvamv3bHTzfGro4fHg4np/9GFk7+nh2d3i3ut8fPX1
+9Pr/fnhuZmReSz9JWl3t1NXPy9pkWVZWX2Jb/dnTz9fi4N3m9fj/39HV3d3X1dbd7X5pZE4z
+Jy5N6sy/vru2yUc9QkdZ3NnQvr3J4F1MT11aWGplZtjY/vPpffLpZF9vaGBvfd/EwNNvc9vV
+4+/j3udrXDUnOd/izsHNzb75OTtITfTG0eHFyu99VUVK38fW1MjN1ttlaMa9xtXW0dPkZlFM
+RT4uITTIz8a6xNzA4jc8YGbevcjvyM1PT3tnbc3mR11kUsfB6s++zmdjTEnYxs3My9XS0vTu
+6WVjWTgmHirXyMC0r7W43jU1SmD0ysjQvb1jSmZb/czrQUbd2unSxsC9z0RBXHLx08jAu7nI
+VklDOzEoITW+v8W8uL2/3jw+YFZGWe7av7/kdtFrQmzUUFDO0Nm+v8jAx08/TlTrwr68ucH8
+Vk03KywuJyvGt8m+s7zLykY2Wc9aUsvKzrzGXnTfTUx4RzzhvczMu77Jy2RBVOtV/725vsHO
+6HY3JiUjJ1K+ycCsq7rHdkBGXj42UM/MyMHIytZPTks7P+jH0sOysLnWS0Ng3mte47+yt81Y
+QDYuKyMhPrvBx7evt7vVP0BwUzpB/9DDv83hzb7PPThS2f1a7b+xsr/v+9t+YFpsybzH7Fs/
+NzYpHCjN0FLGrbG0s9FR2MtFNUNMUtjIfWm6s+I9TtTuT01xxbq+zsvBvsV3SFPU13RvUkZD
+NSQgN1pFT76xsa+1zuXK2j04RFhp5udO8La8bXvMzOB3XFzWxMzc1MrHz35k28TG31E/OjQr
+JCo8SE7bu7WzsrfC0c/kS0JJSkRNWV3azOTqyMHM1MvLzMfP5drMy9pmU2DX0vpaT05NPC4u
+Nzg3PVXayLq1ur6+wthiaGhZX+bxY+3nXl7z/ltv0M/RycXFyNLzWlleWFZq2crP41tLS0tF
+Ozk/TFNUZ+TZ1tztcOnS1+rdzMrT3/tm69xoU17k19HV18i/wcra6+vlel9t5u1cSkJDR0I7
+O0FJUVpeZe3b2dze2c7N4fPZ0d7l3efn5eL7fdzb18/JxcrKzdzu9uRzYV9YVVJIQkJDP0FI
+SUxVaff46uPWzs7R1dja0dd0buvc4e/y++3h3u3k1c3LysjO0M3V8mBhX1NNR0RDRUI/QkhN
+Vl9kYvni39fOzM7O1Nvb3O/23t7q49jc7ODc597Y3OPVzNDSz9z3eW5fUk5HQkA/P0FHS01W
+XGBr8ubf2NPT08/T19zj39nS19jW3ezq3N3c2tjT1tTS1Nzg4vZmVU9LR0RBPz5BQ0ZMTllf
+bu3n28/Nzs/QztTW2N/j29re4+Lm8Ovn5ODc1c7NzdLW2uj+b2BVTkdGQj0+P0FCSVBSW/Dg
+2s/LzdHU3OXo6vvz4djQ0tPR0NHY3d3i3tzh3Nvf4uZ9al9bVU1MUFlWVVZPTk1LSk1UXGJx
+e/Xt7e/k3uLe19XVz8vMzczNztHY4O7x8Hx+7uzn7HxuYGReVlZWVFNZXF1eW1pbWVdPUVtc
+Wmj+d/rs7Ofh4eDc2trY09LU1dfbYkf+2dbozsnEv87d49pqUk9LTk5LSktVWWFebPzxeWpr
+Y2ZoXVRPTUxPVFtf9+nb0MnHy8fKxsvR0tTX9ejw6u99aWp4XVl2dVpYYGxlal9ra19jXlxa
+ZF1e/m5xaV9TWmZub2F44N3r5dzX2tnW2NbX2d/f3+X79enl+e9v9fBjYfPoWlheb2lbVf13
+bm3v7fDvYWpsfVpk+Gjb6efv5mlfaXXx/mxr5Wpi7uv14+d87+nk897b1uTp3eb4eOll+nJ9
+7O5q/3Zdbf1mX2hqbl1ab2Nybv1gYWZtbGt46t7k5uzt8W3p7vPv8/P06nbu7tzhfGRt52d0
+dd33but4bnboeH117vBp63b1ZF5UbmhkeF9pYn5sd2xsYHjle/H5+fvg5Nve6eP07u7f5O35
+6mRu5mxw7fFlb2Bq7vHwdnxsZXBjXu37d2xe7+3fdHzw7HBc5/FpZuzp/O/z9uXnfnzxbHt0
+ZnjiX2re7Xnq/Vjm+Wxx3eBtfnrpamFo4etocdbe5uti+Xz+bmpubPhkb2zd8Gh5ZWBmZmvo
+b+zj6vfn8nL8YXl9bnfu5ez5ft7x4Gl2d+fw7u7v3W986nXv/mxnc31uaHZXXG5cWF3tYXF2
++OL07G3l6/DubOve3vff3+B96vHq42Z1cnds6W1u7l5bXW1f8G/u5fTpfPZ0b2pob2pzb/vu
++Wjv6uJ5++fp73bvcHX8f/1x8Pp0ZW3o63194+Xubm3y9XPu4u78aGxq8XX7/3x2cGNncXts
++/NvdPj+7fnu6vL38PDf2OVvc+z4amv/9/toe3R7cvzf+nvsfnhyeHfrbmnqdXRgb+r0fnvp
+d/hq8mj7del7ZOno6PLxaO13/HV/3/HnbvV37n37bPxve3Rs+GnyY2v48Ox+/3V2a2/weHDu
++HTv9vvv5u776HT67Gls+PlrbfXxfPZzdvJyfndx/+1n8955aPDy9vl7+W5ofWtvaHx3Yl7/
++G72+Ozt2v7z/Orr+u9x5G75Z+9uaGz6emznePp73293+ed0ce7n8mtudW77fG/59mhr7W9l
+7vB//fdf++ht9Prk5+/r6XNs8XFocOLvbmfq+Hdnb/968fTv8Wtya2pp7X7883l93Pnu7PX0
+8ulRfXlpdO7x99p16+3vbfXk5X9f5Xn7VvJ4YWlgbmf8X+vf5WFx2OTf+3fz8ml76WZqfO1l
+aG5j5+ds7dt9/m1mY379Yn5tePfmbuXn+vvr9/Rx6ONq7/duYV5oe3lu3/pzdX17b/pc+2/1
+aelod9p6ed/gZvXl/3ty8Xv3b3ZuY3Tr8GJia+f1fnDk1+Tt7edx9Fpe6G5fZHxr5PLz1Nrd
+6uRqdnns2/t+X+5uXVVMV09PTVdQTmJg6+3SzsnDyMvLwsjI0MjHzcnGx8bEYDYqLC0qKSou
+PdrCt66rqKaptchVOjItKiovO1bMvLOrp6aoq6+2v9DpSTEqJB4dISQnM0vIraSin5+mrbtP
+LyklIiYsMVG/squnqKuqrre7xNHHvcdSODAnIR4cHiMtQcSvpJ2cnaOuyz8sJCMhIy1HzLSq
+qKeorbjE3m/33M+8tMB1RCseGxscHyg316yfmpmbn6q7SisfHh8iLUfLs6mlpqqwwN5wVVNh
+2r2ytsRfLB4cHB0fJzH1rJ+amZufqr5EKx8dHyQtTb6up6SnrLTJX05MTubFubK2v3cvHxsb
+HR8pOP2wn5qZmqCuzz0rIB0eISxYt6ynpqmsr73qT01W07q0tbzORSofGxsdICo8zKyfmpmb
+oa7QOikfHR4iLEy+r6mmp6qut8XNz9LHvry9yF8wHxsaHB8pNUi+ppyZmJynul0xJB4dHSEv
+Vb6tp6emp6uxsrjBwcDIz9tLMSAaGRodKDlVxKygmpeYnq3aMCIfHR0gKTZ7sqiko6Wpra2v
+s7a8y+xPOSofGxkZHCQ2+7ipoZyZmZyjt0EnHRscISw6XMK0raekpaeqrK+ztrzKVzIiGxgX
+GR4nMVS7qZ+amJico7NQKh4bGx4oNl7EubGsqKWkpKaqrrO800QsHxkVFRgdKDrhtaiemZaW
+mqG0SyofHBseJjZXybiyrqqnpKOlqa650kgwJB0ZFxgbHyxGw62jnZqYmJylu0EqIR4fJCs6
+Vs28ta+rqqqqq620xVY0Jx8dHB0fJCw6Y72tpp+dnJ2grMc+LCcnKi84R+fGvLe0srGxs7rK
+WzksJiQlKS00O0NPbtG9sqypqKmts7/eTz88PUJMad3Qzc3My8vN31A+NS8tLjQ9T+rOy9Do
+Zlxj4si+u7q7vb6/xs3X7HVoXF506d7d525YTEQ9ODc5Oj9MadvOzdf9XVlm8t/WzcbDwL69
+vb7Ez/ZdWVpedOnm9G9dUk1IQz89PD5ETmD02dLa+l5YWWrh19TLw7+8uru8wMvX729jYWFV
+TElJTFBVU1NQSUVGS1Jm6+5sYVdNSUlMVvzPxr+7uLa3t7m9xdD1U0ZAPj0+QkZMW+/e3dzb
+3uPj5/RlU0g/Ozg5O0JS6su+uri1tLW3u8PVcFFHQD49P0RNXHbs3tnU0M7Ny8vW7VxLQT09
+PkBHVGze1MrHxsPBxMrR5WtXUE9QT05UYHzg3NjX1tPPztTX3nhTSUVER0lOWWj46uDc19PO
+zc/S1NrzZ11dXF5iXV5p/e/56uXl4OTs+WxdWldVWGN56ujn7PDp4t/a1dXV2eX7b2ZjY2Ng
+XFtdWlpfZHTq5ODf6X1pY19eYHHx59/f5e3v7evp3dvf5ez58u36cl5aW1tZXWJu+e7k397h
+6fR9dXt5a2hpb2tqaGl79/L37+zk19jc3tzf+mlcVFFQVFlfae7l4uLl29vl6/Nwa29udG5p
+b338eP35cfvt6+Ti6et+cnZtbGZeX2VpZ3bx7+no6Ofl4t7f6393/vt7+X5vbGxwb2xtdnVx
+f+nuf3h0bGFeY2Nka2tvdurj397c2t7d5up2fO/v6vL+b11V4Pph+vLscP58/nhvampqX11g
+W2ZobPH44t7f3N7tU2HT0OLf3N7p8uPqdlxYW1BPZlJd09TL3mr49MjfSz5JbF5NWtHf2Vvn
+x8nJz/Dd4WDqbV5eUl59Vl95WO7nY+nj7+n5bWltXl5dYnNpd3rq6ejb5tp75fDh0vnvbXJ8
+7u/w/f93cXh4cOptX15f5l9Va1va3l/4cO7dXePxYNTxeNtbU95599jx7tBd4N5o82vq7mVp
+fFroZX97YXP+bmP4Y3Hi81/gedjq+dpkY+9aX+h+5XZz1/7o4mDv2nDt3/reXm5sXl1rbHhk
+YnVg7uDm5Obo3eb7emhoaPDmaH58ZnVoX/p+8OTi/HTuX/H0/fb77W1jdn556vjq5Ozq+H7c
+/XRnWPtgbnJdbV1Rdf3j0+PU53X+bevu9XroaW1ZZNx0ffFibuVj4ujl+fnfdP5kWV5lVnbu
+dm/27eTs2tz55ufn6Gfy6GJiX1xX+G5+6+3i3nzl7l7hb19oZvdvWt7fWNzifu3vbmzveXZr
+9OlmenV1bfN879/kfl/kfe3k53nq53Z9+f9ienJfXWNddmxw7eP65tzi6fTe3X5gdmxvaW3+
+7HZp61/s3l/9bmbsat93deHh8O/Ybu3V1uLn4fl26mVuWVFMRUhDQkZLVWvfzs/KxsLAwsPH
+ytDS1+R338/Sy8bE2Uk6LSgpLDJGV2Tf3M7EvrezubvBz9zvaV5nWXfp3c/EvsG9v77H2j8x
+KSQlKC46TW3Lxb61sa+vtL7L/FtTTEpLTk950766tbi3ub7GeEIuKiUmKCw0PUx3x7uwrKuu
+tsTpWk5LSEhITVN107+7ua6trq+0wmM1JiEeHiImLTlOzrCnoqCkrb3bSj45NDM0N0jivbCs
+qamnqauuv2AzIhwZGRsfJC1MvKedm5uepbDIPy4mISQpM0/JuKymoqCjp6uvuddFLR4ZGBkb
+HiQv7q2gm5ubnaGrxjgjHh4fKjRFxLCqpKOlpaesrrbjPjgqHBoaGh4jKD3Gr6KcnJydoq7P
+NyUiISAqNULBr62no6Wmqa+wss1JOikcGxwbHR8jNcy1qqCdm5ufq7reNSooIiQvNEDTvbKo
+p6mmpqiruN9dOSMeHRkZHB8pPF3CqZ6bmpudoarCOC4qISAkJy9DfL2spaGenqGnr73sMx8c
+GhgYGR0oNEi+p5+cmZiZnqi2azsrHx4gICIsPc+uqaSenqCkqa/GPikjHxwZGR4kKDH7s6qk
+npubnqGotts5KiklISMqL0DUvK6opqamp6y63z8zLCUfICQkJC0+Tti3q6Wjo6OnrbfI7UQ0
+MTIyNDpEW9/Bt7i3r7DGftr3NSw0LycoLjc6PXK8ur2vqrC0r7jBxelaUz4+TkpN9dDLyL+9
+w9L41nk0Lz0xKC45NzxM3r/Cw7SywLyxw9PE1V9mWFJcTVXPztC+urq6wNbe+TkuMi0lKC8w
+N0RqxL2+sq21tq+6y77KX1hLQUZIS/HQy8G6tba8v8xePDQwLCkoKS0zPlXTv7myrrGwr7jC
+vshxXVVKSExXaufQxb68urzJ2+VGMTEtJygsLTM8StbBvrKtsrWvtse+vt9tb2JiXGfm4N3L
+wcPBwMr5W0Y1Ly0oJyssLz5V7cK3sq6vsa61xcC/7VpyXlZYa+LZ0cS8vLu9yNVaPzcuKiko
+JysuMkBn1buxr62srbG3usDXcWFaTU5bZvfYzMS/vL3G1W9GOzctKispKS4zOU3bwriyrqus
+rrC2wc3T+1JJSE1TYd7RzMS+vsbR2lQ9Oi8qLC0qLTU4Q+/LvLCvr6yttbW4x9XuVlFPSE5i
+YeTJyce/xc7N+z9BPywqLywrMDZAXm/Nt7SzrauusrW5vdJdXlxIQ1BYU2Xp0M3Q0s1+PU1K
+Ki09MCs0PkFeXti2tLevqq60tLq+y2xmdEdESldVT1521tnu183gZ00/TTwrMkI5MjpQb2Zg
+zbW2vrWts8DAvsrkVWT0WURMeF9TXtzQ22ttz9dWU2JSQ0JJRUdHRk5QSE7x59bPy8i+wMjG
+xcrP325y61VQYmBnam7q8mFvcGxlfHre1N3n2eReZ1JSTk9MTlVMT13l+nDZ5NbU4NfS2HnX
+3/p1/ellfVpqz17V0MjQ2dfe/l1mTExVSGNWTdxKcNJd7+br+PVs3GD+6lv4Xfv2delt2s7W
+39fR5f1sX/NPWV1S3Uzy5H7fbNrd7dvy1fTy8W58YXdoY1/6+unx4u3lfPP3XHJPU1NOUU5Z
+/nPw1crRzdPVzt7b0uPo7u5fXmdfbltpZmX4/tnn8d7+XltWVl1XVFtjZW7f4OPa1tfa2eLS
+4Ph4bGhcWVdXX2lfbf36bWhdZv7e5uvuanx5/2p89Pfh6uHc3tTX2drb3Nfce2dlUlNVVFZO
+U1xdVU9XbFxhaunpceHc2dTU1dfX2enq8XB539rj3+DjdWxnWl5qbXpuY11fX1dZaVxXaPvu
+8nny3vPr3Njl+N7b/O3l4OHi4X5mZWly+XH+9HRzaGXl6ePvcvZ1b/78eXfp4/318/toe/F8
+cGF0cW7z9vR1cfl/93Nv/Hd69fv1+nZ1a29qafLq8nl27d/a1tzb2eLg5fX3c2x0bnJrX1tb
+XF1eXm9vdGZqdf/o4+fp4+Xl6OPrfHFsb310d3J8/fjt9PV4fGpkaGdqc3307u7x/n52bmlw
+eu/s4+fs6Ofq7O/5fvF9b2188Xd6cXNrbf7ybGlmZWZka2ttdPv67d/c3+Tn7Hd4bmVmXV9o
+a29wbHJubXPz7Ofh3drc5O3y9fv6/Xd5/XpweHN1bW5nZmt39+vtfvh+am5paWxtcHB8e/zp
+3dnc3N/h6Ovz7fpubHlscXVjYWNra2lfYnJzcnvu4Obq4+Xo7ezzdP309P5waF1t0v5f293t
+421h6G5eXF5cW2F5PFm8XFzKyWzJ2ObJeWJsa19bSVBcZUnZzffUxd/eetngT2xQamlCTv1a
+WW9pYdzR3t/FzezV4nrOb0/x+FhaY/v+805W2u52W/na52N43NrZV0phUFVT8tvL29nW1dxv
+7trV9Vtr7FdTTeJuWudm3Wb8Xmd+Xm9wf2jr/urofOrn2+jb7f185HLb5e110GntY1nu8Vts
+6Ofn8HPn7fpY5vdtdGhd5VRNVVpNSVVNb2NR+OHT7tDJzcXTxcnKzczY0tnY9c7kztDv3047
+QEU1LjU4OD1ETtC8ysG0sbq/wsXN80xSVkhGTk36z9LKvrq7v7/Dy91PPzs3MS8uMDM5Pk3j
+yby2tLO2uLvI3+VjUkpISEtSWmTl2dnNzM3Jz9XV1tzwXl1SSkRCQD8+PkJLWGHs2dLOzMjI
+ys3T3eTucGVfXVpaX2rv39/a08vKz9LP2N3uZF9WU1BMR0VJSk1PWV1e/uTc2tjW2t7i7OPt
+cGZjaXzv+Ozg3uDY29fSztPd2t3e62tvXVlTUk5LSkxNT09abXFvfu7o3t7e5ebs//j3dvvy
++evdz83Oz8/NztDT2uX8c2NcWVRQTEpJSUtNTk9QVVts+fbv3Nra19bW193f3drY2dzg5/jv
+9/vy8vT+bnF+5OPo8mxjX1xUT09TT1NZX2l779/d2dbV3uTv/Hdxb2ZqbHB6897e3d7m5e3e
+3+d2bHNraHBtcf/v9vrr7fL3d2dlYV5aVFZYVFdfZmhsZ3Ry9d7b3d7Z2NfY2N7j4+jm3dzh
+7ejq8+jg5f98/fV+bmJeWlFPTUpMUVNUWF1nfPf27N/i3drd3t7d3uDg3t3r++nn5t/d5fTs
+3+nu6ejj6Ovrd298bl9XUlBRUlBPVVxcZG57fnzt/n/t5N7h6eLd3N/g4urx5+Lu9Ozp7e/t
+3Nna2tnX2drd72paVlNMSklLTExNUVtlcP3/+vPn5fTt4N7d2NXQ0tjc3Nt9TtrC5ljky83R
+1dPS3fhmXlRFQURDQkJERUZMWG1sf9bQ2dfKyM7Pzc/W3efr7en3dfjy/efY2NHO1M/HydTW
+23tNQkc+MjM6ODQ6TVpT9MvGwby7vLy8xs3L3WBfbVdKTl9cW/DZ1s7EwcPDwcTP315HPj00
+MDY3MzdDS0plysXHvba5vru7wM3d63pVS01OS0tZ8eLZy769wL67xNbbfkk7OTIvMTAuND9D
+S+XJyr+3tre2trm+xc7zY1NIRUZGSFN34dTKwb29vLm7ytTaTj09MiwvLysuOz4/XMvEvrWx
+s7Kyub2/zO9bTEU/PT1DSU5c6M/GvLa4uLS3xczXUT86Ni4sLy4sMz1BTt3Evrq0sbOztrq/
+zNh6TkZCPj0/Q0tZ9drNwbq4t7a3usLP/Es7NzErKy0rLjg/SGnKvbexrq6vsbW8xtJ5TkE/
+PTc6QURHV9/NyL+4t7m4tbnJ0N9BOTovKSsuKiw4QUvgwLayr6ysr7K0vtZ8WkY7OTo4OD5M
+UmHXwby7s66ztbS5w95XPjQvKycnKyssN0hV1LuyrqyrrK2zusDcUEE9My40OTY8WdrLvrOv
+r6+tr7m9vtJLRjsqKisnJSkuMjxbyrqyramoq66uu9hsSjcvLy0sMDtFa8u7sa2tq6uvs7a/
+1/ZaQDUwKycpKiUsPz1GwbKyrampq665wdRDNzkuKCw1NDVZv764ramqra6vt8XT3l1APj82
+LSssLCwqMldlarusrK2rq6+8z2NJNiwuLysuP0NJxrOxrqqqrK+5vcHjXm5dTEY/NjEuKSoy
+LTFkzMq4rKysrLS8v2Q3NzMsLCwvQlRNzK6sr6uoq7G7wsHQVVBpWEU/ODMwLCkrMjU+8r+1
+rq2srK++2mtDMS4xLSw1QUrkvbavq6utrrK7xM3d+2lbUk8+MS8vKygtNDtN1LuvrKyur7XG
++0o5Mi8tLTNESFrEtrSxr66usbvAv8tuXfN6T0Y7My4sKyotOUVczLavra2us7vKX0A3MS8t
+MT5HUte+uLOvr6+vtL3DyulqZ01JRT06My8yNTU7R2TZxL27t7W6wMnoV01EPz9APUBKT1r7
+0cS9u7m4uLq8v8vfa1RMSEhJSUtLTE5RVFFQV1tbXnbu7O/v8HhvZ1xTT1VebfHf1czGwL28
+vsDCxtDkc19RSkpMTlFTVFhXVVNPUFZZWl50/fvp5vH+a1hPVl1cbOvf2M7NysfGx8fIzNHY
+6GlkX1hYXl9cXnfv739fXWlcTU1UTUpPUE1OUlRbcern08zOy8nMy8nN2NfT2efp6Ox0XFtZ
+WVpicm1ven/9697f5+x6ZF5bVFFPTk5OT05UXm7+5NbS1tTP1dvb1NPY2+Dm+W12+3ht9ubp
+4uHd2+D1/PRwZmRmW1lcW1RRUE5SV1937/f76Of08uXl5N/g6O78/Xzw8fjn3t3d19ni393l
+f3R+//P29351aF9eWVhYWFZaYV9kfPP2fHV+a1pMT8TAT2HM1dZYT83AXj/1t9A5W7bbQc/J
+ZFTb7Uffyz4+vPZA3dZCWthL9XpAeN1MRey16EK7vmxazlRPvlI6y75IYMHgdMX7Tm7PYTpo
+z/pITerAUzjA1E1+fE7ny19HWr7vPdnE5lBc4u9j2OBhx8xS38Zi09RUTdPDQEa/6z9/7lBw
+WXh9UPHfRG3EXFDS01tO6tPuWuDZfeHib2Ha127l39p4ZH7eb9p1TvXKUUnS21taVfvTWUzu
+3U9V2+BV887pUl3b3E920vHVe1nP1l1YzdtN3b9QUsbcT2bcXVZbW2ntYW75XWnSdkXfyGdU
+bt3acGza4F9v3t5c+upZX+vk2+5fc8/NWVbb0OxdVHbSWk7c71fx4+7qZnZzdGlu62NYbdrt
+UVzS3FZr1u7t2GtY1s1dSfHG505m1t9vX+/cZF7b32do7eFRVs/OV03p0ehcX/je6lxb8m1a
+/eNuaPjr4OPlb2fj7Hb3fGp74u9ZX+/f52T54eTi5GtTcN5xZGppbnV1bH7r/v/j3vVy/eXv
+YWjn7WlubWv06nD26uzs4u9fZu/2YWn35/pr7O55fXH75+Pj92R4fu3o5uX3fO7e1OtVUllr
+Wk5OS0pUa31kcOPYz87Ny8/OztLvde1wduDb6OPQ0NfPzMvJ7TosLDQ8OTM1Ss++u7a0tbi5
+ur7gQjc6RktFRVjcysK7ur/M0MnF1VhKU+3UzM9xOiwrMD09NzdG0Lm0tLO1usDBwt1FNjQ7
+SVVTUWTRvra2ur/HyMTL32dXXO7OzHg8LCcrNTw4NT3vu66srK+3vcDF2Eo0LjE6Q0ZIUt7D
+ubGwtr7IzNHca1RUbN/Ny9doPzAqKi84Ozk+Y76wrayusbe+y/9JOjEvMzs/R1Hbvrawr7G3
+wtv6ZlVMR0xu1szJ0fFINCwrMDs9Oj9+vLCtrq+xt8PnTz0yLi41P0hMXsm0rayutL3O62RW
+SD49SPDEu73H0144KygqMDg7P1+/r6qpqq22yGJDOTIuLC43SvLNvbOtrK2zv91OQ0A/PTtG
+4L60sra9ylAtIyQtODg1Psutp6eprLLHVD83LyklKTVgx724r6qqrbO/bkE8PT08O0Hru7Cu
+rrK93j0oHh4lLTM5XLWno6Olq7f3OzEuLCgoMFa9sK2rrK2xus5KNjI3PkBASHfAsq6tr7bG
+Zz8rHRofLkdt0LSmoKOor8VDLCgrLi4vPM2uqaiqr75rSElFOTI6Y8vN2Mu/vb26tbjIXk9W
+PSQaHTFt9ea7qaeusa+7PiYlNEo+Ol+3ra2urrh0OTY6OzpE88O9v8XN2t3MwL3CycjGze9D
+JhgZKF3JzMGvqqqpqLoxICQ6Xk5N07qzrair1S8rOlRKPU7Hu7y5uMxGPne/v9HXzcfAvcZA
+HBEZOMPEvK6prKuiqD0cHC7pycS4u8q6qKnzKSMtQ9y+t7/dzLOy4Tg2R9m+t7zkVsyxt20t
+GBAbXqypqquwu7Wu2SUbI06wpqayTzlgvck9LC1Avamqw0hG9cG90kk8Tca6y1VM4rqvtvYq
+FBAju6moqa/M57i2RCcnNsWpoatPLDFTx7/RRjdesq/NOzQ+4LKswj41PVDbxL29uK2rvjwf
+EREkvqegn6jPTdrgOi0xSsCrp7ZHLy8857y84UVI2cTaSDc5yKmqvUUtKTXEq6qtsbnByl42
+HA4TM66dlZizMC4+OjlFQD/EqKi8TTMqMM+tr8dNOjdB+c3FubG2zkUuJy7ZrKOiq9BBP0VI
+PiITGkaqm5adTx8jNVXFvlk/zKyqukMpJS/Cp6e+OSouVbisrbvaZEs7Ly9Bwqqipbo9MTxP
+0L8xFRY0sp+Yn0siKUPhwc09Nsiop7VJJh4rvqOivS8rPM2uqbNiQU5PQjo2QcCoo6vQMis5
+ya+zMhQTJsafl53oKCo4W8XNP0C6p6m3RSQeK8aln603KDrPtauvYDg+Qz0/S3+8qae0YTMx
+ULmxxikVFiq6npifbygmL0vHwOXTsqyuxDciHy3HpJ2m3i8tPOq8udFIP0VMTmrLvLKvvFg7
+Pte0r7wyFxIeXKaZmq45KSs46MLP176ztcFHKSEq9qecn8kpJC5Rtqq1Uj1FTVvp3urHsq63
+2lBOXWo6HhggP6+bmKdDJyUu7rG2ztrQz8bRPCkqQrein64+JSMwzquotNk/OD1Mcc+8srG6
+ytpVPTssHBwuzamdnbE6KiowWbu5ycvDzOJgPS8zUsKwrLhIMTVD17Sttc5NOTVC3ryvrbTC
+1GJENiIXHDW+opmdwi4pKjLivczavLa9xGgyKjFawbSwvV5MV2TgxsDFwsb8QTxJ27WopbBR
+LB4UFSbXqJuYoeEuJSMt+7W1urrAxsHWOigmNNCspKrKPjY9YsC2usbMzedWTFTUt6yw1ywY
+ERYlyZ2WmaXLNCYmMEvaxr29ws3+TURMbM+/u7zNXkZCUc+5sbTFX0A7RGbY13BSWP1YQ0U7
+Li46WtDAu7/L0sq/v81TPTc4RG7b2Me4r6ystO02LC06/by1usbW3OZfTD01N0brx7/C0l8/
+OjtARkZJVnzYxr/E03ZZWObKwcTVaFl91MS9xHxIQUJO7srAwcjX/FtIQEJIW9PEyf9KQURW
+eG5VSkVP2L67wdNWRElrz8nO51hLT2bez8vM219KQkRfzL24u8hiRUBBSFb62dHNzuJaSUVI
+T3TSzMzQ2OtpU0xRcdPFxNJZQD1EWtPCw8/3VkhHUu3Nx8XL22dSTU1SW3zdz8vM4VJCP0dm
+y76+xttdS0hNV3rUysjM3lpIREps08jI1mRLSE1WX/vwcG/+49/h3/5kbe7l4fdcUlBb/tLG
+xMfS+1VOUVv40MXFzvhRR0VLXvXf4O9oYmpxdm1jVVJc/d/V0tzweHJsb377f3T35d7e4ndh
+ZXvh29rb6O7s+mpbV1dZc97X2N97Xltia25rXFxp59HLy9DvW09NUFVfb/fi1tbhdlxRTlJd
+ft7QzM7R2/hpXmBkcuff3tvX3O1+aldOTU9WZenW0tXd5XxiXFpZYnzu5N3b4O1vXFlgbnvx
+5+nn5OTqeGZdXmJu9ejs7+Xh4+twWVBRWnrd09DY5fT89O71/Xdubvfs6fxpYmBiZnvr5ejv
+831teO3ya11bWl925dvd5/J3bWxqaGRk+9/c3u5oTFHPzez219ni6vHtbVhYafv6e+7i7O/3
++m9eXGJnbH30+fns7OzzbGJo+u37fnJjde3l6+7ufXJ06efz/v15bW7x625ne+Te+GtrZ2pv
+7Ofve/vxcW726v52dXXx7u//bGRqdH3v9fXt6Ofr93Zxamhqamdo/fbr6Ojj7vH5cG1sYGpw
+ev3x8nx6bnzu6/Tj5nJ05d/v+mZfXmd+b3Pw6uDl/vd0XmJr/Xvu7ejv7+Tq/W53anhnX3js
+8vjz7/Vwc2Zod2ZaZuzv49vj/Gttc378/enf3t/k5ufpfGtqcunrfG1ycW5walpbX2l5dHN6
++Ojj6v1oanR4bHXo6PHu6PNxaWZwbWv8+Xzz9nl2aGZ27ODc2tnY19XT29/d3dbR1uDf1s/Y
+Z0MyLS0yODo8RWDOvbOur7a8wcjjTz86Oz1HZNfPzcS6tba7w83Z+WpiW1dbaVxDMisqLjU3
+OkNjzbquqquvub2+yl0+NC4uNUVddOjLurCvsLW9zNzqdFlLR1Hgys5iPSwmKC86NzM85bas
+qKeqsbq7vNg6KyksMTc/TVzcuKqnrLa+xM3jXUg9PUvbxcbW+1Y9LigpLS8wO+27sKyopqqy
+vMLvOSsoKy0uNVHNvbKqpqmvt7/cS0BCQT9FXNvKv7u/7zkqJCYrMDQ5Ub2rpqWnrLW/00sw
+JiQpLzhAbL2uqKWmq7bF3FQ9NTc8QU3kwLi1tr3iPiwjHyQrLjZftKekpKWpsMRfOSkkJiwz
+OkjNsKilqK21v9dNOTM3PEVc2si+t7G0vdZMMSUeHykyN0y6pqGjpam13UEzKSIkLDlH1bOs
+qqipsL7PdT4wMj5JSl/IvLu3sbTA7lhDLSAdIS89Pm+voqCorbTGTzUrJygvRmTXuaqnrru6
+wGM7OT4/Qlbt4sy8uL7Gv7zG6FVNPi8nIig8RzlMsaOpurmxvU00LzA1RFRN5LSttb69xv1f
+Sjs+WfBWWMe91e3BtbvTzr3FbkY9PC4gIDlYLzC4oazJtaeyTzs9OTM0OT5quLG8va+txD06
+T1A6N1LKyc7Fura7wcPJ2PZlT0hNRDMnJDNBLChirK6+rqGnveRrSzYtKy9FzsvBrqitwtfP
+SjAxOj9Jbcy9t7a9xL3DaUxs3FVN2t8+JyE8TSAixqu91aidq83N5Ew3KCYyWX1MwqaosrW4
+yFg6NzxFU0tXzsHE08zAx+Z218nTfGZHOC4lKDQwLU+1r7Sto6a3zfZLOCsoLz9TXs6vqrK7
+trruPTxCOzU7U9PN2MSzsLm/wcPLYkVMRTQtJSc/Oic6srHAs6Wks8rO7Uw0KC5CPTVOt665
+t6+0vNxITlA7N0BTWVzXv7y6vMK+vdxc41YzLy4mKzguM8y0vLSopq26wMlpOi0sMTMuN+vJ
+wre1s6uuysq93kA7PT47OkzYxb+7s7C5x9RwOyklLTAkJkfT9cazqqartrGvxEs+QDswLC46
+VFZNy7CyurWyt8BnS15NPEVYWmP97dTL5fHQ3FlNS09IOz1TU0dU6N7X1djQzN1mb+Z4XWT+
+2snGyMjIy9XyZ25dTU5SUVBUXm997tzc39LLzc/P2+L2U0lMTUpHSVZoa2xo9+B9XF9tbGVf
+bOPT0tDLzM7S39zZ83vs6OxwZWxqZWdpbfTw69TS3ebd4mNQT1BKR0tRW2ZgWmn6eXb+6dzd
+7t7Nztve297d4Whx80tR2O/axM3Xz/d54VpTXGb/1+LkzdfubEc8Qz8xM0FGSGXcwbK1vbWu
+ucnM3mRHNzU5NDQ6Q+TGybuusLe4vMXbTENLQztI+X/zzsnU+j85XDkmMU0+PebEsK24s6mv
+3Hv4TTswLz1JNznJuM/CsK+3x9TBzj08Z1M+RWDKw9zPub9TPjU1NicmO046Wbatq6ytqKrL
+TFNALCotNDxDWsewr7mxrLvg8V1IOzRCc01Pxru7vcW+yE4/MioyLiU22Vpvs62urba5s9o5
+PkQvLTpFVubMvLOzvL++zFxLSkc/QE545s6+vb+8vtbsbks8MywyOisx0c1bva6ytrzCvNc7
+P084MURcW+LMwLm7xb++1nB4XUtLT1zs6tvFw8zHw913/ko9OCwvRS4q2MFI0q2yvrq7vMdL
+Rm1DLz5zTEnWv8nHu7/Dw83W21ZOZFFLbOft1cvMztPT4lZOQzEsQDknQ75IQbSuxr2wusrl
+XGRONzpuWj7yvt3Zt7zSxr/V/3pTTk1JWH7858vJ2MvI3d/qUTsxPUQpLsxZM9KtxeKvr9To
+yOJJQkFHTUZPzc5sv7fVz7nHZdbZS0ZYWk5a4tXW08rH1uvpaUk6OUQ5LUPeQ0G/t+vPsLtl
+2b9qQlpkSUlUXfPx9snD0MnAydjc23teYl5WXXRvdu3o39ro8+n5Sz5MRTQ8Z1E9Zr/S7r+7
+z9zJzW9YXWVdU17l6X3fzsvR1cjM7v7uXk5SWFhWXGdket7e4NLN3uvX9kxGTUo/P1BcT13b
+ztLQzM3X8f51X1hbW2Tx4dzTzszP2tzf/mtqY11cXGV7/+jZ1NfX2ODob1xgZFZMTk9KRklQ
+WFz539zZ29zc2t3m7u7ud/Pk9/ze3Oz27fpmX3rq8n7t5ff02tnd39vhfW9ua2BpfnFnZWNa
+UU5QUVBZZXH26Nzc3d/c3+fp8Ht38nZnbHt+a3Xe3erp3Nnd2tzl7+vm6vb+fnbu5Oz7dnN3
+92pVUVFNSkxMTldlfebc2NPQ0tbX3Ox5dWxlbHR+8+re3d/d29/t/nhxbWRqa3Hw5NrY3t7e
+3+n0blNKSElFRk5WX/fd3dfS1dPX3e1uY1xZWlphffno19Ta2tTT3Ox+/Wtcd3j95tna2NPX
+09ft92lQSkI+PkBETF963MzN0NHY2t/tZFxUUWJbXVriz+bN283P1tvh2uX57WxcY/nr39Dc
+2dzf5u9zXFJMQDs8QkVM+9bOycnO0tXf4+xlU1BUU1t+38/Nz9Ha5v1xc3p48uzve2x59ejc
+2NPW2tzf6u3m3dztXUM4Nzg8R2fWy8XGzNTd4ubybFRNTVFb8s/KyMzS3/VnWWd6/e7o6evn
+5d/b1Nfb2Nzk5ePg5vRrUT01NTk8SubKwr/Dzdrp7vxtWk5LSlNv1cfBxczgbFtVV11u8e7q
+5enq4tzb2tXZ3NjZ1tHR1/FcTTwxMDU7St3CvL3BzOJjV1dcW1NNS1J7z8O9vsjfYFJQWWzq
+3+5/++zg2tTY3Nvm8Ofb2tzZ1NbrXU89MC8xOETswr28wczdc2ZiY19ZUVBj3cq+vcPP81dM
+Tltodu7r7vLt39jc3dvb3d3Z2dze3NbU3HhUPCsqLjtpwre5wN1nXGzi3HFQRkZS2cK9vMLZ
+a11VTlNg8N3Uz9R6UlJe8M/JzNbg7+Xc2M3DxM7pTDcnJSw627avt8ddSFBaYnBaTVBr2ca+
+wcTI0t73VERATdu/u8LZWUdIUW7f2dPOzs/V6mdk38i+v+A9KB4lOdSuqrLLbEhMaVNMTk5f
+1s7e4trSv7vE70c7PVjUwLzG2uh4UkhHT+DAubzKZ0hHZsm6ucNbOi0gIzVZva2uvtVPPUVP
+T/za5N/Y8/XMxb+8ymNKRUNNbNS/u77K8EI4O0nZvLnA2FxOTWDWxLu5wls8Lh4fOtq0qa7N
++E87SE9AVNDOzcjlXM7CwL7STUdLSFT+2sS8vctkPTQ7X8O4ucHbbVRNVm7LubS9XTYpHSBD
+zrasscvVYTo9QDxywcXNz+b3w77FzO9PU2hNR1rewLi91Fc8OER2ybu5wMvsTEdNccW5u9NI
+NCIdMWfRsay7xMdCOEA6Q8vJ2s3O38q/093fYGrfXUBO4s/BwNHkX0NDU3XHurq/yOVVT2HV
+xMPNUzwqHSpcXcivuMG35jQ6PTvsw/NvxsPHu8ld8Nxz5ulBRdjXz8HH2ulaP0Nsz8G6vsrN
+3FBg0trkbkI2LSAt1+bctLK9t8w2M0RBS9rmWse2wMfG7mDja0pQeuTe4eXOxs5rTUdJaM7I
+ycPAw85z7NTa60s6NiYfRcdM16yvt6/TMTlJNTZu4nG5sMjHudJLUEU7bcZ3Xs/HyMrfT1RZ
+RkrszcW9vcjOzcjNYEY9PTklJFLKTtyvsLW00js/TzYwSvF7wLbBvrHBSERMP0JXTV3HvsPF
+wNVXUk1Ld87Mx76/yMPD5EE/TTciHjbfPkixqK2yvu52XDEpNVtNUcW5tq6xzGniUzk8VFZq
+y83Mu79pZ99RSvDd+c7CzMG//mDWXjEnKTg3Kzq2rbu5r6+78zs0PDkuON3Ex7uvr7vN7VJO
+TkRGbeNh48HAys/W2M/YWmTO10w7Q8p6MDjP1zAu6MJPTMS6wdDp3cloOUvJWzz1wdDf187C
+x2FXzs5dV3n5bV9fbN3S2tzU1NnXc0pOb1ZJV3ri2Oluee9uTUtPSkJJXk9K6dpe7srO0sjO
+18vRbGns+fvr3NbQ0t9x/N7mcH7b0tHdevl3UkpNTlJdbmlu6W5h4+Zfc3JOTFtSR01eXmL/
+/eXV1eXr2NDb4t3X1M/Kyc/X3P1iXlZafube3eL6ZWthUlFbWlxtYVVt5e3o3dvqa1tNSlBa
+V1Rn3dfd3drY4PZ293pcX/Lk69rS09jub2hgX3bb1N7k3OZ5X1xsbVtXWmZrZ2/23d7n5vpq
+ZFpVXGVkZ27v3N3r8/Rya21kXGF67O3u3dvb33ptef/9++fd2t3y+PH1c2xzdmpfbH5zY2jn
+2+xz//9oWlpgX1xj7+j5/+7h7W5183FjaHJ18+br7+nl83N6/vDf2d3f5312bGz49fbo5Op8
+am1waHDy9nlwbWZoYVtaX2VfZHF69e7q3+x7e/ru9/Ti3ebs5uPu8fL77ur/bmdjaXz+ePDp
+7O7u/HVvaWhnaWx2+H53+n5zdm5oWllfZvrf2t7d4O/1/vbo8Xfy6fJufOryaW3z+29qampq
+cu7m7Ofj8np1cmtv9+n3+ezp/2tqb2dRX+hpV/zX3+vj5Ol8ZWNydmBe+u1t9Nzc6ebg5ej2
+9+vza19neV5j6u9jaPTu8/R55t1wauftZV5obGhy7+719/Tu5v5s6u1oZ/RsZG//eXp+c3bv
+/nT/7PF++u3o/vfub/fw/n92b3rz/f3t9fTv83NxdHz6bPrtdnB0dW1manD5bmh7d2717ezo
+7fz8+XVkb/5udvR/+uzr8vj16u77+/78bXz6enNu/3t3ePn8+v1x+u99cm5xaG72+fN+6+ns
+6ujqdXn+aGlrdXFpZ15s/vr46e5wa21zc3jz7fbv6OXl6ufi5u35cWprfnp883p8+WxnaWpv
+cXdtbv35+ff993N29fv//f/19vT07ejw+Hzv7fL5+fD+dnFpaW54/Hh08vnxfPv/bXJ4bWZu
+eXJ2+O7q4OLq6Ozvd3R0aXD7+H7q+er49f/wdHn7c3t76/N7bm9oZW9sc3FnanD/9O/u9Pv4
+7PPz7+rt8e/p5fh8dGxkbX52anL56unvbmVrfn11+v9+fe7+d+/u7uzu/nBvb21iaXzu+/jv
+8nh29nR9/353bG1rdXr77uft9/n59fHt7vP9/Pv9cm1tcHJve/X8evPwc3j0fH54bW75fXF/
+9Pb67O//7ujm5+jq6+z1eXD1emtnXlhZXFVRV1lWWmnx5uDh3NTQz9LS2N3e3eh9fXN6+ejb
+3Nzf393f7G5lTEA8Ojs7Oz5NfdLEvry4t7vBytlnT0Y/QURFSlrt2M3IxsXGydLuZltVU1Na
+aP7f1M3Lzc3P1uw8MT88LjI+QVzFx7+urbi6u85tWz0zOjw5PlJ3z7u6vbi4ws7ZXU9RRkNV
+bGjdysm+vPBBQEI3Ly81QW7dzbqvr7O4w8/zRTc0Njc6P0rgwLu4tbW4vcn5VEs/Ozw+Rl/j
+1sa/wcPEzdzc4l5SWVlDMzU/OjtIS3q/vsW5tb7Dxu1UV0k9QEhJZd3b0cXHzs3W3972Wldc
+WWL879PEwMjKyMvRTzYzNzUvLzpP1sfAtq6ut7/L3W1BMzI5P0FIYMy5tri6ur7MbklCQz46
+PU1t4dvWyL/F13r78V9VVWHfz9vfzsvK1U46QUY2NT1AVtPWzrm1v8PG1Nj7Rj5HSUxbZePF
+wsvJytXV7lZcbVpZfOjVy87Py83bYkM2NDYzNDtN4cW9ubOxt7/VZk5AOjg8Qk750MG6t7e7
+wtB5Sjw6Oz0+SF3cxsDAwsfTbk9LSUlMT13ezs3Iw8bIz9x5Vk9SUE1NUlBFRFBOS1/u2sbA
+xcLBzNnoXVNTTkpPVlr26f/Zy9DRzdvc1vFu9HVp7fZy5NHOz/JNPzk3NTQ5S+vLvbezsra+
+zfJNPjs4OkFNZ9TDvby+xMrWck1DQUBDR01q28/KxMXK0PhjZlNOVV1x287NzMzW4mtPS0pE
+RExTV2f13M/Lzc7N0N5uU0VDRkhLU2zWyMS/vsDGzN1qVktISEtPWWdw3M3T393d63VhWmV4
+aWju2M7NysTAympHOTY5NDE6TffJu7eyr7bAzX1NPjY0OkFLfMzBure8wcjXa05EQkZMTVre
+0s3Gytnc81FKSURIVFdj3dPV0M3P0uJsbP5+fX1ubfP6XUdFVEtFTVt3zsjLv73Dyszee1NA
+SE5FTm9ieM/OzcXT8uRvXGVZUWnvbd/Nz8rHzs/UeV5KNjQ4MzdASG/HycW5vcK8wc7P8lBR
+TENNXFh73N/Rxc3RztjzcVVIS0xPae/o1M7RztHi4N55XVtUWGNZVWJXTFNZT1FeW2j6cnbq
+8ubZ2c/M09nZ6XJ79+zj6erZz9PY293kfWhpfH1oXl1lcnR0aWReVE5QV11hZn3r/Hl9ZGr9
+WlJaXGh3a/3U0trT1tfP0+fe1NXa3unk6W9pYW18Z2Fmauv+WWDuenB8bW5qXmRxZl1wa2pr
+cOzt3+l+bFlYXVZWX+7j3c/Qzc/a3ed4bX1nXGz1fe7u8OL17unrfnJ1aV57+vTzZ1xo4XZ4
+6eju7X1ra11jY2pna/Bvfnf8emFtfWtu5uZxYXd3f3Vd6+Pl4NPR19vk1eF7buV5Y/Tzfn32
+XGFhYFJVWVr9X2bu4njy4f77d2j+4nX86uHf6/Hp2u/k4+vs/3lbal1hYltcZnVu8n3m4ORq
+df5k9HDzfuzh3eXy4OPt6OZw7+3za2ltbW5g/GZe/vr0+P5tf3ZmY3NwafJ5eHL1eXlw8+/x
+4+p06vP73+jl6fd7/Gvo5/T953xpbGJhZnhbZvp1enFubXRhdef1/vH06tzr7en89Glpaex4
+f3j05H7+aXlh/WRz+vvv7N5zcm/9aO57Z/nj6ure23106/llaWlmWWd8++r4bv5nfm5tb/ns
+6nT6fnP4+Ont6t7i6e5+d218/mFjY/JrYXfg7Pf59W1sfmd39P1r8uX4+uHneu51+Ozv6+13
+9nhjXmV2dfjq8e7z9/1ia+/6dH7/a2j5fOtzdGzydnJ09uLv8ejofPl0am1+d2ny5e37ffRx
+cnvp7vzp3uz79nf+/nReaHxubW/wdW9i9Pd8bvXg53Vz6nnscPBubWlndV597/N+3vj8a/zp
+/e7l3PHu4OPv72j8+25pdmZi9P5ma3FyaV97/f3w6en4d+rufvX0cHfxbXjx8XF1d+9obXfs
+a/fa6u94cG/rb3Xs7PPr221tavR1e2NneGf4efD55vvy+2dqX/Z19vf7dOTqd+p633n89On0
+avHu72R0Zntvb175e27hZu39d2bo6Hv7aet9d2vo9vvt9ebx/mjq8uZz9HJ7al/ucXRo3+jq
+/fbxc+pkdnP9X/n4c/J15XLqXXn39Phk9Wz0VOrs5/v93WvoYNpv7fRt32/uWN7q32np5XX5
+WfBY+F9o6fhkbdJ0+Xx7bO5+XuN2b3vrauXraOze+2rfc2/9X/fvbVn092t+em7h53Tt3nde
+2f5q7uF5cvNZ4Gxtcn1oYuBddnV5X3X/Zd/99HzaeeHf9/HjflvYbGXs/mle6mvxbWtv8Wxo
+4mh98Oh77vB05t91aOf3aGrqZGRravXpb1r5415t3+tu4+N98uBxctz9aN18YuDsYP3pYGl6
+dnDra13d+1z37l9cdmjwdGB62e9e4td+897z9Ox1edp+aNneZfn4aWrxVurdUVzoZ1Pa/ljc
+6mrn4WXu11d712Fb33Rk1Ota4dhg5tlX/9xkX2ZtW+t9YNftZ/7nf3zv9/vg5vfl/vbr4PV0
+dGj/d1xo8l9c9HFva3R3buhuduvu7np87fXu7/b4/e3799bi79/2am/eY2Tjdv7u92fh/WTd
+dWTs6md95WZ1bldWV1RXV09ZbVpd5uLm1NLUzs3RyMrRz8/h7+Z4bHzw7efrdnxiU0VERDg4
+PT49RVhr28vDu7q9vb3BzdDdXlBPSkJFS1Zv+9nMysvMyM3a2NjpYGHo6F1t4GlDQk88Mjk/
+Pj5OdtTMyry0u8C7vtZndllEPUFOR0Zoz9XUvbrCx8HF1nVk+WdOWvN9WnPPaDs/WTktO1A+
+O2HKx8jAt7O+zsHAXz9OVD03RWlYTNy8vce+tLnOz8TTUE5oa09Q79HgYd/MTjM9XzMoOl06
+Nue9xsi8tLK/3Mu+bTpEZ0M3R9vkX9W7usbFu7zOZXPcYkRN4/JX+s3U8djfTT0/RzYvOkxD
+P/zBwcO+ube90eLWbUI+S00/RF/u7dXHyMbEyszM1On+e1xYb2th9eD47tbT3OV6Y15PREBA
+Pz9ETE5T9dbY2c/LztTX2+Lo4/lqaX71//fk1MvQ18/O1+Xt/3FxbW109+3q5PF5a19PS0pE
+Pz5ESUtTaPnp1szMycfJ0NnqaW1uY19o++7k3tnW1NfZ3ens9u/6dXNucXF7/ejh5OHr/l5P
+UFBJQkRISk5Zb/zn2tfU1NTV1N/v9398fe7r5dve4d/e3trX3uPe4vl8+3F0//Dt5u726+1r
+UE5OSUI/RklITV5octfJy87Mzc/O1Of3eGdiZWVp6N/n4NfY3mxe0M152MLOedrNzs9uTnbo
+RDU0OT88Nz5z19nKwb24u87dzdtPR0VFS05JU9rP1cjAx8TAze/t719fbv/j1MzHwsDIaU5q
+SSojMUYtK224vr60sq+xz0tdazsvND9U8d3aw7W3x8m9xvZbT09cXVJsx77LzL+/xsh6PkBG
+LiQoNT05Rca1s7S3urnEWEBAQz89RFrRwsHFwL7K6PV0W2JxX2nk39/WysTFx8fKxtNIRls7
+KyksMjs+TM23s7e6u7zLXUQ/QUNITmbOv77EyM/a7Vlc+OXc0tni3NrYzsvS29fNyd9MRko2
+JyUvPD9Oz7qwrrS/ydJsSD89Pkpt8tjEvr3F21lOUFh0487EwcfP2uXaz9Pi7N3Nx8zqTUIu
+HyIzNTX8ubWuqrDAy+1EPD46N0jdyb+5usjY3mlJSmzozb6/x8nN3tzcemre0NPR1N1iRjYi
+HCg2Ol+5tLCpqbnN/kI6Qkg8P2LTxb22vt1zT0BFXGvcvri8xNJmZtjX9PLe18bBzeRbQS4e
+HiszTrmvs62pr77PRDEzOzxFc+jQwbi0wOxLQz9CYeXSvLa6vsz57dztcO3WzcnFz2RIOSUd
+Iyo1za+urqyvu77YPjQxMDdYzMK9uri8xe9IPj1O7s29urq9yN7q1tj9Y19ked3PbUU5KR8j
+KzPhsKurq667w9FIOjQuLzxixrays7vI7U9MS01j/NPBvbq6v8/e4Hxr+V1MXe5cTD8tISQn
+LF+1q6iorbnG40pFPTQyMjpev7GsrrzlT0NGYN3W0NDPwrq4u8ThW1RccmdWSj0zLScmKzBF
+xrKrqq2zvdVbRz04ODw/T93CtrS3vdRhT09cfN/Z19DIvr2+xNhvX1xdXEo9Mi0qJy04Scq1
+raussr3SWUI+PT1ARU/vzMa/vMPM1XxiX2X+5+fr1sW+vLzG1vZaV1dSSUM3KiQlKTXWr6mm
+qbLC5lBKSUI7PD0/Vs+6sbG4zlA/Pkhf4MzLz9LPx7+8vsjfXE1PYmtaSTYoISInNs6tpaSq
+ts1jTU9PQzs5OkbevLCttMxXPjtHdM/Kzd1w/t3KvLu+xdXvbm/x8Uw5LCAeJCxMtKekp66+
+3F9HPz86Nz5S3Luwsb3sRTxEa8/DxdlyZm3Tv76+wcrPzsvZZFc+LykiICczarSopqqwxGpS
+RDw8PT1Ozry2uMdSOjhBbr+1tbzPWk5TZ+DOx8K8u7y/z1tDNi0oIiEnNNqtpaSrvGdAPz4/
+Q0peyrextcFNLCozRrysrK+75ElERENwxry0sbjD12JJPzQsJx8eLFO4pqKpt9JHNz09Ok/O
+vLKuuGQ9NDA4RmXIta6us8pFNzhE2ru1tri8wcvrWjwtKiUgIi9Vuaejq7n3OjA3QVfEtbS1
+u9tDOjc2PWnDvLe3v8jcWklESmHGuLa4v8jZ9+9TNysoJSAqW7urpqq+Yj0wNlLewrW0v9Fv
+Pzg9R05fzb66u8HSZlRMS1ravbS0vd1kYejLwM07KikmIidCxLGprL3yTkFE6NXdyMTT3dpj
+SVdYPUjPzL++1dPVa0xT3su5tL/fV1D2vra5xUwsIx8fJEHIuq+tsLrFVTtAT2jaycvPxstb
+REdYX1p+zs7NxNRs18bHxMpnUHd0fsa6trW8UiwnJR4gLUjGraaprLTrOjU2OEvMvrm3wP5L
+R0Q+QW3Gx8zCxMrEx9be709Q7uLYv7e4urvWNignHhkfPcuuoJ+nrL81Ki8wONW2trCxzkZG
+Qy4z8N3Ovbi1ub/+XW1OSlzLwsXAvri5vdg3KCMgHRw0v66ppKavu0UrLz1JVsS6vbi+aU1Y
+OyxUx13Kr7HCvM5K4t1KXr7Cfs2/xby63jIpKCIgHinJsq+so6ey1jQwP0c+RtDAvLzMzs/f
+SzhAWe3r18O8tb3Nzs/VVk9m58S+v8PPUzAnIyQhIT6/tauioqy15Dc0Ni0sP+7TubO4tK/C
+Pz5BOjxHfb+trri1s8RNPkNMcNttTHvXOikqKCUtS13Fp6CorK/ITUQ0Ki49Pj/ZubOtrbnK
+0FM2NT1DaL2ztLCvucnX/D8vMjYyMDg8Mi89SlHeuq+tq7G+x9BNODg5PVBsds64try+ws7e
+a0hBVunay766ur3YSz01MTQ1MzU8P0VKTlnfxr63srG1usXvUEQ+QEVPbdPHycfDwMDI1nVZ
+T0pKVPXazs3Y4PRYRDw5ODo8Pk5l/vPf3O/q0sTAvry/xdJpUk5PVW3m5d7Wzs7U1tvscmBU
+U2Hw5t/e0s7eWkhDPjs6Oz9Lfs7R6t3X7mNp583Ly8fFytj09+ns8OLb2tnb4PHf2/xXVFhZ
+V1RWbeLn3+NiSURMTklLUVxjd+/3ZmFfYG3v1c3Mx8DAxc7X29zp+PLr3tzh6u95aFlPT1BQ
+UVt8497k7H5tXlBMS01OU1ZcZ2tqXFpjdfbj1M3HxMXH0tzh8mdmdXH139XOzNPb43lWTUlN
+U2P3/+fb2epiV1NPSklPVm3m5uHs/W9paF1m+ezk29TZ5+ftal9se+/a0srHyc/Z4H9pX1xZ
+XnDs4+7o6nNfUkxLSk5VWGbu4/f3+Wxpa2pse+7k3N3p9XFtbW3w5N/b1NLY2t/ycPvxeHH6
+5tzd5ud1XFZQUFVXVF1x7env6/Xt6O98a2hqevjs929xbnJrY3ru9O/r8Pr1eGprcGl+39rW
+09TZ3eL4Z11fZ253++7yeG5sZF9oamZrcvj07/T7cmpdXmZt+/33/Pfzfm5maWdqePzk2tXR
+19vc4/Z7cWRlcHzu4+Hm5/p0cGdfYWtqa217cWZoZGRjb3Rsa25uc/978O15evxx+eTi5OLh
+4d7f8nBvdnP57Xn33+Xn39ze3+Hu9XZ4bGdgXV5aWlpWVVRUWFpcX2h+5uLj6O7o5enl4t3U
+09vZ2Nvm5d3v7Ojg3dvd6eHue3txYVhPTE1JR0pKTVBWXWZ5+OLb2tjRz9zY0+Xv6evg19vf
+0tLc6ePtZW357tzc49jQ2vT4W0hDPjo4Ojs9RlBf3MbCv7y8vr/Dz+H3W0lGRUVJTl7qzsfD
+vbu9wMTKztTm29heTEs6MDEuKisvMT1s2r6uq6upqa60wXJPPS4sLi4uNkV4xbqxq6irrbC5
+xN9LRENBPjc2OjIsLC8zNzxJzrm1sKuprLG7x9hFMjEvLDA5P2fDvLOsrK6vtsHNbU1JSElU
+ZtzSVU9JMissKikyN0HHtrWrpqqsscrvTTEtLyssOEFTv7SvqaqtrbLA015EQD4+S2XnysHS
+blc4LCsqKS0xPda9u6+pqq20xNNaNS4xLi83RO7AuLGrrK+xucPUWURCPz9KWurJxcXN31Y6
+Mi8sLC4yQe7TwbKusbW8ws5POzw5NDc9WdLDurGurrK4vsjfWUxISEVJW+rWzs7a9kw9OTUx
+LzI3P01qzby5uLi7vcXdYlVGPz9DV/TdzL66ubu9v8XXeGVfX1FPW21rcfpy7v5LRElDOzk6
+P0ZFSWLq2s7Jwby9xMbK1edzaHFqc3Vw9Ot+d97b49/b1tTQ0tXR0dTV2+TzaVBJQz08Ojo9
+Pj5CTFho797QycrLyczNzM3Rz9DX297e3NbU0M7OzM7Q09TX19fd9l1TRz07ODU0Njk8QEhW
+dejVzMjBwMTExMnO0Nbd7Xx+e/Pk393X0c/Pz87Pz9TV0c/S4mZTTEI6NTQ2ODo8P0hUbPPb
+y8bExMbGxszW3t7d4u7w4d/e2trTzc3O0NfWz9bc2t/s7X1eT0dAPTo6PD09P0RNWWPw2tHN
+ysrLzM/U1drb1tPb393c3+Hd2tfW0s7Nzs/V3Nzb4vJsV09LQ0A+Ozw9PT5CSExWWmXe08/K
+ycnHyc3Nzs7N1NrY2Nzc3+DZ2Nza1NbZ3OTm92NsdmVXVFFLR0RBQD88PUFDSVNce9TPzcjH
+xsTFyMnMz9fk6+76fn548+Hj3dPOz8/V4t7f7Ptwa19gXE5MR0NCPzw9Q0RETVVn3tLNycnG
+xMnQ09PZ5X3w59/g7OnYzc7NysvQ19zr/ftxXlpaWVlUUE9NSUI/Q0I9RE1MUW3u1cfHyMLC
+ycvO1Nzk4uvv6+nx7d/c29rZ2dne8f9wamhiWldYVVlcXmZwaFhTV1RKRkpQV1te9N/S1N3Z
+09Xb3+vo3uH889rO1NjRy8rP1NbY3XtlX11cV1RUWFZTV11fWVVbXVtYWF5qaGNfYW50duze
+3d/k4dzY3uvl2NLY3tzX2d3f6Oftb23973xjZ3VrZ2djaGxjXVxeW1tfYFxdaGl18frw3t3k
+7Onl8fj3+PF7am389/599ufo7erp5OHn5+zxenZyanBoY2tramJoef119enj3OPx6ud7c3Rt
+bGxoZmRgam1qam59ff30/X/29+rr9e/xfH70+Hj/e3v3e2188u/q4OHg3t7f6e95d3BvbGpp
+X2FlX19obnL/e252dm9ubnb36+r0cP33f/t1aGn4+frr4eHj4ePs8+vv9fV++fr8dmZlam1t
+cHl3eHZtbGNkb2psd3d9+Ofj6Ont7fz+/nZ3eHZ69Pd09Ovu+e7t9/Dx+H7+dnBqZ2thZGtx
+eHN0eHRx+/L//O/z+ebf7vXp5efs7+/r7vB8dm9wbmpqY2Vobvl3bXd/dnj+dn/5/Pr78vd0
+bnhxeOro7+/m5ejt7e7x/v97dHd3c3Fwb3z5/Xh1bm5raXf9/fPx/XJ2dHNvb/9vbPV4dnl3
+8vHy8e7u6+fi4+Xm8377+XR8/nR5eGdsfHhrb3VpZWxwbmxobfj4d2188Xh29e9+bnN+9PHt
+5uHo8+/0/W567O99ff7v7/nv7unu9vl3Z2RvanV4bnv9b2779e93bnluam1lX2poc+3y+fTq
+6vPx5+bo4OX1/HV66O927fF77n557Pd4bmtvcmZscXNua//w+m35+GhxePr7d27s5u7x9/r+
+eWlz731z8u377/B9+e/6dvLubHj9bXr07vz68H92dG926vF0cWlrcmtv6up8avrteHP35vNz
+d+jj72xu93pzbv94fvh4Z3LtbWBn/Pzq7/Pe5+zt7+lvZWpoYl5m6Oz96+fs7Ov9/vl2dXJv
++vtsbP/v8/Pq6v5nYG56ZF967u/8efv77vj89PX6++1zcfr3/O3sdXny/2Jx8Xp3e3T89W96
+83598Wtsb2zu73Rq9ur16O1u7+xp8OZ4cv3zb2Bke/92av7a6GJ34O1yY2jueGZz5N/7+97b
+4fx15+diWW7tb2hpbvl+aGv6al1u6/pu+uri9Xb65OtrbOrj8F5j4ehna+vm9Hl08/lveePk
+9/vvdGp7b3fj8mp9bWBpfvby/v1waPr6bmvr7HJu/vp56+7+3+D77/xr+PFpZHRn9+9pdnr1
+bm5kdfN28ur4aP7o8Oh3bOjtYG/l7W5k/+3ydefc8WVn5npkZ3n0Y2F64vxp4+T3cX/z9Gxq
+8O37aWpvaF1fe/R6+e3u8Wd253ttfvf4a3Tf6Obwbt3ea3rodmttbvDsb/rb5nr+/H5hWWVo
+XmhbXfZred/t+Nvof/ttffN+7+jl3+Dk8uzzZXdwYmJfZG9xYW37b296b/Dxam9xbm7t5urr
+59Pfduvp8/VxYfr6YOznaWz2/O/lfHztf3N1XmzvYmn4XmL+bW3x6uvxd/j6bv5/9ebq9Xzz
+395vb+z3a11i/nxcX/nf7/588Odkafbu9ffx4eR8833+5Ov1+W9v/X9taGxu+nxtfvhseW9g
+a2NX7u5i+ePd1+5v3dRtb+l6/ft4en59Z3F5XmHr7mV06u/s63z/4uv8+Xj23nxh6eVhY292
+/XJvfHZiaHf5dfnvZ3rg8XnmfHrobe/sa2/06e/r++vp/GVh7e59cm187F5c6udwbOzt9Wx2
+73pgd97eaXXq9nhlbPPvaGb893d7d+jhc+3pcXJ4a31tZunve97b4/Ry6eVlYGdne2li8+5o
+Yfrv935pc/ppaP3v5vD169/t/O7l3u/57utmYWtrXHf3afN5cerqXWF6cWdibvXvfu3c3uDa
+6Wv/6Wtt7+3m5/zs5W1heWVeZFpfX1/y9XD19Pfu/vDn6uvtbPvi73ns6ev5aW73eHj4+mj+
+fmphX3H8/P708fXo8H72+u3u8Hb76PtsdX56aGV4cnjwZm/9bmJuc//sfO7id33j7/3i5Xnv
++Wp982tx/X58am5ub/1vb+zraHPr/vb2+evu9fPqfPn3fHlkZX59dPtyb/XqfW1vffr3Zl9z
+enj3fP3k7n7p5/Tt7PL3fvPseGRn9+t0cnN+/mZg+vlmdffv9nl79/pu/2586n5rdvPz39zo
++PPyb3hrZm1ma+x+b+f9b+/tb/P0aHX1fHr2ev/r73P25P7+enJvbHd2dWl3+fxyef3x5vvq
+6fT9Zmf6+WT07WXv52995/357ffudXNpZ2x0bXvufvPy++3sdX79dvRoYmr+73Bv/n7z73jt
+4P558ftna/7v7/t57/P9efvv5u/28vrqe2f763Nvc2l0emdmavb6dW9iXnD47+rf7fvvafbf
+dmR9/Hnv+erpe/Z8aHDw9Xb0+vf4cWl68HDxdWbs/nvvdWf+5+3zduLlY2/k9Wjt4eh8ZmF+
+elpf7Oxs9e59bGf+7vj37PV4dPP/bW1+7OTp6+fp+Wt0fGtuZm18Ymt6eXX3/3jv/nv77v5z
+99/e+/r38e18a3zo9m/26O79+ejtaF9qZV1cZ3dvbGp2+mZabOvs7/Hh3uns3N377+L3ZGd+
+eHH6++jhf2/i6mpsa2ZoZ2L862976PxeZW16/H7z933v+ffsfmv23+bsfvfrfF9kX2jyePXf
+5Ov5+Ormemb49WdoaGBo+292+PTtbmZ5emF87evf7fDk5ezufXF69fXu3+D9/G1pZF9ybl9o
+dWVmfnp973ZlcH327ufu7N/t9+98aPTrePrm3+XueHD4bl5mcH5vcvNtZXJ0/u99een1d2pq
++3hv7ej6d+L1cvtv/Ov3aXXn9m7l5m9ybmt4bGnr5+zq7u/1amNx7PllcXZrbm9ufvpv/Or3
+8OT59fF57/H593Fu9/d26/F09Gxg6/12/nj+dGxsamR5ePHs9t7f8fHvcXtnae91eft6+vRt
+fu316Ox2+vhvdGttdPz4fWdxfG75cGJiffXq5url7+bm+vrz8H1wa/n+bnt0fXtoduvzb/p8
+d/JtZ35qXGb8cm5x/ODq6N3g+m/x73Fu+Pjz/W5t7u3u6fvyfG1ma3xlX3jpbvv8Z/59ce/k
+6Oz9fHv7enN45+996uLs7OpvX2JfX15eY3vs9nd6/GtmdXH+6PDv3Nrf5+TveXN18nFo+/Jo
+a+v8be33++n7cvnveWZjY2lkYX3r7nb57HZuevf4c2/w/nz96f5t7/jr6e7x6fFv9XFndG5y
+5uf39Or7amh+/Xh/+flra3n0/3br//rq/v7q+WlqZl1bZ/5+f+zd4/Xz7Pbz7Hn/6uvr8Xnx
+7X54eWxvfXV2eHl+8/d3fXJzdGllcWxzfXTx7PPo7G/+5epwbuvj73p2/3Jpfvf89HNq8/Z3
+5vZt+O9+ZWNwev3o7Pfr5+v2dGZdZWZn+Pdq8Nzi5+z87Ovy6Hp3eGZtamRmfPTr9O3mcWNv
+b2NudPPr+Pzq7/3p7+js8/D98PdnZGRm+/F8eO/p7Pn9bGVrc3hs/3X74fJtePd5be/k6+rs
++Obf8m1//WVr925pbG5oaWBdbnz+7e/q5f5x92to+O7o4uXj6d/b6fDvfX19a2l7a15lb2xh
+a/B9fe7tdmd1/W5faPHz8vH66/dvcvjvfWps//p+73pv9u3w7Ofp3uHo6ubqZWZlYGVn/PJx
+bX5vbOj3aHF3b2v9eGl5+X5sYnDv8u/s4ePr5+r+dnZz7+vqfPT/cHp0b2poYV5iaWhu9+rq
+6enq4uju9u/z/Xx8/n148/hventmYmRkZ2xqeeZzZufg6ebh9e7ydm9s//d8df3n5fX+/XJu
+dmhlZWd0cG5ufvl77+717PDy5eTp6fDv93BydW1y7+9+fnBnYF9ne3JfZ2Rpd3Z+7efq6d7h
+5+318vTr7/Bwa3FxbXT07XtmevZqZHj/evbv6e96fu/8//zz6/D5fG94bHb7a213a2ZtaGVt
+cf/v8u3k3+v99O3zdu/x+u7u9fP9cvp2dPV8d3Jqb2ldZHh7+//v4OHm4utqaW1wbG39+nb+
+6+j7ffF0b3v7cvl8a/16bHF0dXr88+ji6O/x7X968/Pr73tvcm5ubG7+b2Nxc2prdOzu/fnu
+6+3x7fJxbv99fn9qcO/1/n18+ndtef1ydvZ3cXdvfu9+eO/m7Obl4up+cHr1fG5sbnZ6+ezx
+e25qaWReX/nrdHR5+Onw7uvv7vP/+HRvcnh4fH5+8vb7e+3qdnRyZml3fPr1/39/7+nu6vPw
+7v9ucGxofvn3fXFucWlmcXt6ePTv+Pn9+Onk6uzs+XJvbnh7fPd9dX3/e/Hw9XxucXF4aG18
+fPHv/nBucHzu5/N2c3Vtcf97e/rt83R59fPt8vTx+3x6/nFtdv3x+fjy+npwefp/fnh2Z2h7
+cmRmef73ff706uv16+vx7ery7+3y+3d+e3d++/j6/X5ucW9pb3JkYm5vb3f//H/88fPw8PHm
+6fzv8Pj18f5saXjvfnJud3J8fHJ6fvX/9+707+v7bnV7c3N9+fF9b3NweP369/766e15/fT3
++PN4a3BsbXVy//jt9G1xend59/Z8+/n6/vzw7u/9fv75e/Tv/3p//mpkcWtmbXN3/f50b/nv
+8Ovr7e/x+vTu/Pb1eXdz/359+/p5/u54a3p2evzv9fbwfnt9/P7v8O91aXx7c3fz/Hp2d3Fu
+dXX3+/36a2tteHFrc/Tz+H7z7ezv5+Xt7O7q8vXz+fNzeXz6dWtua21rbPxiX2dla295+vp3
++O3f5uzk5vp47fHu9fHyeP/xfn3t73n8fm1yal5kXm5yde9+am919nRpd/j9ef3x9+vv5OP6
+feft4+n35en8/ex2dOjp+W1qdWxrZ3vyb15ibmJm7+9tav7r/nd58G1xbP14bOPn6+Pg7Nfk
+buXs7/Z6ZGZtd3n+Zf9/ZvHo8Hrs5vx2WOv/Ymfu8vhkfn/5dmr7bm79bW/vY23p9nTe3OHs
+4vT8b2z4d3Vu32hl++ls7uf3/HZ0bm1vdHpuZV/0Z2lv9vj5+fTa+v7l5udv6unjev19am9s
+e+vr62v96Gt1cPZoZGxadHVx9OXl6uJobutmdf1ybGV3Znn74eTr7/HoffHjbX53Z/VxYmp3
+5Hvhb9znavPmeXBw9e1uanhxY3P57O9sbPx2f2tscG9ofvVydfR9d+bb7HX67Xz6+fXc7XPt
+8/b37HZ2fmdj/vJoWXxzX2f9935v9+9883Xn7H589Hfr531s5HRr5X//ZXlq+Xtm4v91ZV99
+6Xd58uTpcuvz/fDua/zm9vh67/FlW3llc/Jy9fFpf21uem14/+r+8/Z76et+8fT39Xn0/nV+
+5nVkeWRk9nJmb2Nw7vj56O759uvv7+f17Xjve2Pv/mb25uxkeOz4Yn3lbW/6evT4Y2FscXb5
+/3bxa2ps79/19OXpZH59aXtv9vxv7+9u+uJn+97n73z+6/Zmd/X+b29y/ml27XPz9H17XnVu
+b3ts7fT93evv/uvtcXj07HRf7fJobW14ZWxfbHnp8Hf7aWF39m1t8uHk4eDr5/Xq5fz94+n4
+629nbW5dYHVvXHZqbm12bu599eT84OXt9mfsbW354G/65Hz+6evzbnNv+WhoWmx0bvxue3h1
+cv3t7+jo/uX7auh7fW52ambp6eVfdH52cmNg7e5uY9f27N7v2/3v9+zsaet1/W9p/Xll/mNc
+ZOp7ZfHs/HP3bOxxYXrs8Xrp3e9s/v99aO3rbf3y8XT5e2v7fm30fXBtcfrzbmvl7/tsevtt
+7uFxdl9gYnFr8+/n6dvi3u7z9XTm/fH7+PdraOnha19vaGRuXnVsY2Zi/G5w5fVmd+Nv5t5g
+8fjw9t/k6+Tl9Hx2/Gj/dGFtYujsaOrrd+/7bm5ubf1pa/R6+uD5eXR7499fW+hoXnfj19h+
+X/f9YmXo0d9obu1yUmV+aHVgamjp+vXb5vTya29laGXn6N57cebi8uLXd3T+YGxxa2jd4fLy
+429i3PtSYV1o+99eXdPfT+bP8WRxXlfke27r3e9n6/RZX3NOW9Ro3MvOzvFh22dOTk/Xz2JZ
+0spbU3vT2extcv5fYlVn3d91WPbpbGZn6tpcXuzd9lht+2pobO3p6ev678/LzMvFys3W09t4
+WUM3NTo2LzI9QUFL5MW5t7GtrK+ztbq9wcTBxMjlRzMoKyoeHiYtMThKtKSnqaSmqrxLRE43
+LjRE1sfFrJ+foqKnujseGhoSDxYdLEjYpJWWmZmcpcEtKCojICg6v7CrnJWUmKW9WxsNDg0L
+Dxgrrp6XjIqOkp/JLxkPEhUbLWapmZeTj5CVneQlGAsICQkOIDyok42KiY6Xpy4WDgwOFCDO
+n5aSkpGTmaO5XCUSDQ0ODhQovKKbko2OmKm5OBcODxcdKruZkZaampyr0dPOUiMYHR0XFx88
+wbysmpacpbO5PxcUHyMkRqaWmaikn7RXbrm2yTknKiAWFx8tQtyumpieoqq5OxkWICQlUaOY
+m6mpqMs/0ay0wcg+LB8ZGBwiL+avoJubn6u/RB8XHSgsRKqcnKevrrpkzqmtv7lAKB0WGBwf
+L8WpnZqbnKjKRB8XHCEoQbKfnaasrbrJwK2stKxHISAZFhkeNLyzo5mbn6vI4yUWHCUrOMGf
+mqezrrTM4K2kvNdSLyEWGSAjLFOwn56jnqG4WCgcHyAkNr+koKiqqrXLz6+nwsPMLB8aGh4d
+Jlm1qKCenJ+1zzgeHSAlL1+toaOqq6+90LyqttHLNSggGh0fJDncs6Sgn5+qt1okHyQiKDu8
+p6itqqy5yr2qtMizWzEkHCAdGypBx6ypnpqirbc8KSQfJy0+vK6tra+vtb+vqrSuuT4xIR0d
+GR0sPM+1qZ2dpam0Ty8oJigrOe3Au7iwra+tp6WnqrlNLh8cGRcbJDNuvKicnKClrMBMLykp
+KSw4UNzEtrKvrauqrq6w7D8xKiYhIS02PGTIta60tbfFbD42ODY4P03vyMS/uLavrK2rrbbF
+XEU4Li0tLjQ3PU5s5XxYZU4/PDlGbvzMurSzt7u0s7WxsbC0yPRmQjQuLC4vLzQ/VfpXTVpM
+Pzs/XtbMvrWztLq9ubq3s7aysLvQdFM+LyotLy4uNEJPQz5CR0hHT9m+ubq7uLe+ysO6ub25
+sK+zv8/ZUzkwLzEzMC8zNzo2Nj5KXuPJura7w8bEzXv3yL69urStrLC7w8xqQDYxMjItKywu
+MDA2TdjKwby2utHv3tx1YtbAureyraytr7jEzPVGNzEyLSclKCwvM0Phxb69vr7F7lhm6et1
+1b21srCsqqyxuL3Jb0Y6MS0qJSMoLTM5S8/CwsjKxs9qXe3P1NnIvLe3ta+tra+yt7zKWzwv
+KygjIicsMThE5s3Ozc3Mzetr48/O0Mm9urm2sK2srK6yucZnOi0qKSYkKC00O0Bc1tXq5dPQ
+3vzmzMrU0cS5t7mxrKurrrO6y1Q7LisqJyYqLzc7P1X073Z16N7j7t3OzNLUw7m0sa6pqKmr
+sMbbdjYnKSsnIiUwQDk5er7UU9+8yE9U1NNMScm3urSnn6Gmoai/RTQwIRodJiQkLta7z7yo
+rsd0z900LT4+NzvfubqxoZ2foJ2dr1U8LR8XFx8eHSpyvr6zoqa4vLz2NCw3Mys458/KsKOf
+n52bnqe7XiwhHBgaHh8oNt21s6ynrbbLXEUtKzQvM0rdv7msoZ6dnJ6eqtY7KiIZFx0eHSpL
+xbqvpam3t8ZMOS8vMS07VHXCsaqjnpydnp+u0TMpIhcYHxwfMVrIvKulsriy4Uc/MjAvNEVE
+8rmzrKKenp+dpLnlNSceGBweGyY8Scyzq62zsbxtXUcxNTo2PWrQw7Goo5+enaCpsO4wKh8Z
+HR4dJjZI37uvs7W2wd9hRjk7PTpAY+LIsqymoJ+en6iv3DgtHxwfHR0pMTdjv7u7sbbFyN5G
+PEI/OEZpW8uwrqifnp+fo6/LQy8hHR8cHCYtLk/Izr2vuca8zE9KTj85TFlMy7W1qaCgoaCi
+rLnpNiwgHyAdHyoqNPfvzre5xMDFd09ZTEBTYVzKvLitpqWjoqSqr7tINywgIiEeIyoqOFdP
+7sDA0MTEW3/STlzVbebBvLmtqKmloqmqrsVbPy0kJCMeISwqLkpJSM7A38u85efD6l7K0/W/
+t7uvqaqopamtsL1vPTEoJCUjISgsLDNDQ0/P1d7BxObLxfPZwcvEtrSwq6mrqaqwtLzmTTwv
+KyknJSUnKiwyOkBJ/c3Rx77IycDGx7u5ubGusK2ssLGyu8XH1k8+OC8rKiYjJSgpLTQ8Se/L
+xr+7vLu5urq2s7W0tLe3tLm+vb/FxMniUkI4MSwnIyIjJiovOkVtzcS+uraztLOysbGxtrm6
+vsDAxMnFwcXGyeRJOzIrJyMiIiYpLjdBYM6+uLWzsK+wsrO0uLq+xszOzMvIw8C/v8bN5Ug1
+Ly0oJCQkJiovN0Vpybuzrq6trK6ytbrAyNHl7ObVzMnHw7/Ax8zWYT8yLSkmJCQnKi46S+G/
+s66rqqytsLi/xtXta19bZefbzcTBvr3ByMzfTzoxLCglJCYqLjtQ2b+1r62rrrCzvMjR3nJo
+YVdabO7by8TBvbu+wMLWTzoxLSonJigsMT5V5Ma6tbCxtLa3vcfO1un7fmBd6dLHvry9vb3B
+x87+RTkzLispKSsvNkBNdc/Bure5u7m7w8rN2eLb3+fXzMnDwMLAwMXJydHzTT45My4rLC0v
+NT5GVN7Jv7u8vru+yczK09nT1+LYzcrCvby9vL7AwMTeTkE6MiwqKSotMTk9TurNwb2+wL3A
+zM/Nz9XNztXNxMG/u7q6ubq8vsfjUkA5MSwpKSorLjU6Q1/ZycC/wr29xMTBxMnHys7LxMC9
+uru7urvBxMv4TEA6MSwpKSkrLjU7Q1vYyMG/v729w8O+wcTEx8vIv728urm6vcPGytduRzs1
+LyspKSorLjQ6QlrcycC9vLm6vby7vr++wcbCvr/Avby+wMTJztZpRDs1LyspKSkrLjI4Qljf
+x728u7i4u7q4u72+wcTDwMDAvr2/wsXIztxdQjkzLiopKCkqLTA2P1bbv7e1tbS2urq4ur7A
+w8fHxcTBvr6/wMLH0/5NPTYxLSopKSkrLTI5R/PJvLa0tra3ubm5u77Cx83NysTBv729v8PI
+2WRJODc0JygtKigsLi81Qvu7r6ytsbS7v76/wsrU6uTOvrOtqKeoq7LcNiMZFRQVGSM6zK+n
+oqKjpamuvFkuIx8jLVK6rainqq+xr6yrrK+5yv4+KyIaFRQWGylXsKKdnZ+mq6+52zwpHx0k
+M86spKKnsL/Oxraura60v9hNOSsfGRYVGSM/sJ+dnqOstb3F004vIx8lOMWmnp+ovmFRcMGx
+ra63wsnbSjEiGBISFiFTqpyYmp+ruMfRaDkmHRwlQq+dmp2q2z08X7qsqq67z/lbRy8fFxEQ
+FifAnZWUmaO01/heRi8fGhsm8aOZl56xTzlDyq6oqrTeSktQQi0fFg8QGC+rmJGTm6rPTk1Y
+QSoeGh0su52XmKHAPzhOvKuprslLRUpdSi0dEw8RHFCgk5CVn7lPQEhLOiccGR44rJmTl6PL
+OTZYuKurt/E+PFBoSS8eFA8SHnCek5GXpMZCOkFHNiYdHCJOppeTmKbVOjpoua2vwEo4N0bl
+WzMfFREUH+uek5KYpc08NTo8MCQdHinjo5iVmqnNQz/xvLK0xV5IRFNfPyscFRMXJsyflpWa
+p79RQD0yJx4cHzK3nZaWna56RlDLtbG+UzcxPGjQXC8eFRMXJsCelpWbqcdYTkQ4KR8dITi3
+npeYn7PdVm2/trrORTg1PmFONiYaFBQcNq6blpedq73ZVDwtIh0fKuGmmpidqsD86L+wssFF
+LigqNkU/MyMZFhkl16GXlZmktdtIPTYtJyYrRbejnZ6lr7/Gvra2x0swKScuPz4yJRoWGB9H
+p5iUlp2ty0g8OjMtKyw36q+kn6Gor7i5uLrISzUtKi06Oi0jGxcZIDutm5eYnqzKTENKTkpC
+OjpI1rWrp6aprrS7y189MS4yPU1NLyAZFBcfN6+dmZqgsMvjzLm0utk9Li462rGnpamwwedO
+RUNIWmNbSywfGhUVHCn2qqCen6eqq6qlpq2/QisnKTjRr6inrcBrQT9NV0UzJxwYGBoiOM2s
+op+fnp+foaaruv06LSsrLjdH5L64t7vXRjEnIyEgJy5BybOrpqanqq+3u8nO1W3+ZFxfVV5W
+SkY6NDExNz1Lb29lZGZ92cS8trm/ydXKzcPR3XZYdF7z1MjP83NOSElEREhCOzw+QmTYzby+
+wMPZV1Jt717xx8xPaN1iVVN9997Hw7u4vMLLz0w+Pjo2Njk7Su/IwsvC20dLPkNQUnbZ4NDE
+yMO/wsPGxsPEwcrL3XdXSj02MS0xOjtR1tm/sbx2ctdSPET63NvNzcC9ytHLxs/h1cvY5d1h
+UVRHPj08ODY5PUd0y7+1tsPP3FtJTWzaz8rHx8vQ2djc7etuYltQU15eX19PRD46Oj5M7s3E
+vr7DzeN+4dvi1M7Kztnd7mpu8vhoU1FNRkFFbGpY7WJNTEdGTvTPyMC/xMrLztXQztTk82pi
+Z2Bt5eFzWE9OS0VLUFhhZWhwZk9NTlNf/9jHwsjGxsnIzM/X4u5kU23va2Rlal5YU05MT05V
+W1xy6m1kYFdaZPLe1szKztHW2NbY1tvY8O/ma2L2+l9ZSkZCQFnbWj7jydPS1cvO1uhuYWZk
+7vNyb2jh1cvU19xnYlpYTEtPWFBOWHLf2ODi1dLh9Ovf6PRjbm9oeOnY0+7r197peuxoalpU
+Tk5V/eB1cXr87O3s5ej89W1v8+zf39zvcWb2/mxsZG9u5t304XhpXFh1a/b9Zm9cYmV+a2jm
+2efs9uns3Nnbz9z2WvpmbWNj9t3g8+v1a3JuTlhfWFVoYXJ0W97c8vLW9uzact3a4+njX2jn
+Y/r3cfznd+XaZ17nYU5lZVhXXlzl7P7Z1e/b4uXv4eHj1/jv3+nzW1l2Z11bW3By/m369WVo
+WF1ZUFpu63bZ59PR19/c0drb2dja2PRx6V9YWlhSU1JWWF5ea1VS4dftbef+4+Tk79v3+/LW
+2fXX0Njc5tzuXl1YWU9VWlNdVmVoaW1443rZ9eXh6mjl2F/k7N776uf+8Wfd93ZV1t1i8vvl
+XGJff1RS8eBnb9Vr9m30bmpZ69dgZtXZX3Ll62Vk/+t6XubjenPrcGdganlxavTafHrh6Wzs
+33LubX3ic/z7a2jtfWhy9mZv6u3f7evxcnztZ17yaV9tZV9caWx+Y+/Z6t/Y7HzjbXjpc23d
+Znze9O/o53/Z7/H0dmJoaVtdX3BbXmxqWGvaZOPp6+7n6uPbddbg/d7maX3oYfvufvBgU11Y
+TFFSUGFdZuXu9dDQ1sXJx8HExcfJy8jbZldHOzQ2MS8xNjk8R1nfzr+5trSxtLe3vMLHx8/V
+0MnNzt1OWD4uMzMoKzAuMjtGVGvJuL22rrO0sre9wcbF0MvCy8nAwsnOVEw5Ky0sIyYsKy88
+SF7pv7W0rqqur662vcbM1PHaz93SwsTFyWRVOy0vLCQoLSswOkNa88W4urOssLCutr3Bytrl
+2MvMyr+/v8LTZUg2Ly4pJikqKzM8QVjUyb22srGvr7W7wcnY287NyL27uri4u8r6VDktLSkj
+JykoLjo+TNrIwbqzsra1tb7Fw8na08fDwLezt7a0vNVpRjYsKyklJCktLzdJZ+3LvLm6ubm9
+wcbJy87PycO8uLazsrW3usfmSzw3LyspKCkrLTM6QFb738/JxcPCxcTEwsC/vLq3tbO0tbe5
+ur3Cz2FEOzMtKygmJigrLjZBUXbUx8G9urq6ube3t7i4uLi4ubm6vLy+xtLqWEM6My0pJyYl
+JyotMz1P7su/u7aysrOysbO0t7q8vb/AwsXHyMjK1eFrRzw1LisoJycnKi40PlLgxLu4tLGw
+srO0trq7vcXIyc7PztDPysnN0+NYQzs1LispKSkrLjU+Ut/Jvba0sa+vsrW3u8DIzuN67O9y
+++ji2NDQ2ONtUEE7NS8tLS0tMjlBUd7Fu7Ovr6+wtbvByt34fF9ZX2pq/uHV0s3IzM/ReExB
+OTAuLi0tMzpCWdLDvLWytbe3vcrR1+xqfW5ebfLz59HMycTAxMvO7k5CPDUwMDExMzpASV/Z
+zMS8uru7vcPJzdTf5+33eu3m5NrSzMjGxMPGyttbTEI6NTMxMDI2OD1O9tbHv76+v8HIzc/V
+2dve6OXX1s/KxsO/vb2/xtDc2ng2LTMyLC0wNDpATG/XzcbAwMPHx8jK19rU19jZzcbDvru3
+s7W5u8DnST41KykoJycrLjM/W+LIubW1srO3vcPJ2O7wd19v4NjLwL67tbO3ub3NY1A7Kyoq
+IyIoKCg0RUzOtrWxq6uurrW/v8/561hRaF9i1svIuba6uLW/4HVMLyssIyEmJiYvO0TjvrWv
+rKmrrq62vcHa9m1QVVVQaOvcxr69ubq9yfNtPS4xKiImJyUrMzxPzrqxrqqprKyuubrD6m9U
+S0pMVWF02MbAu7m6vc/gUzUzLiUlKCYoLzdAesO3sayorK2stry91f5gTEtKS1dm3sjFvbi6
+vMTU6T80MiklJyYmLDI7Us27s66pqq2ssby9zXRiTkhISU9c98zBvri3ubvJ3GU6My8oJigm
+KC42RXfFtLGtqa2urrm+w+VnTkVKREZbaufGv7y2t7m+0/1LNzItJycoKCwyPFLbvbGxrauu
+rrG7v9P2XUlJS0RPcfXOwLu6ube/z9JbOzMvKSYoKCkuOUZax7SzrqqtsK+2vsvgb01KUUZL
+7+fWv726t7m8yOhePjMxKyYoKCovN0Nty7ixsK2sr7K2vc7ufFRIVllNZ9rayL68vLu+z2tb
+PzEwLigqLCwwOUj5z7yzsq+tsLW1usnT2l5NXF9OXd3j28fBxsnJ2llSRDY0NC0tLzA2Pkdg
+2Mu8trizsbe5usTM0u50b3tz+uPa2dnR2vX5bldMSj88Pjs4Ojk7QUhPa+LVx8LBvL3Avb/J
+ysjS08/U2t3h7Pl0cXdwb2NZU1BLSUdBPz49Pz9BSE9aftrQy8PBwL+/wsTEyc3O0tvb3OXh
+4+76em95a19fT0hHRD49Pjw8QEVIU3nk18vFw7+/vr7FyMfL1tfU6vTyeWVp+eXo4uLteG5l
+TktFPTs8Ozs9QEhX+tjMx8O/vb6/vsbMytXx3/Fdef5s3Nno0dDT1Nrh/FRIQzo0NTUzNjw/
+SW3QyL+6uLi5urrAzM7cZmZfUFz9bubP0M7DxcrN23BYPzg0Li0vLzI7QU/ezcG3trSyt7m6
+xMrL+2Z8W1p09ujdz8bKx7/O2uJKOzkxLCwtLS81P0pfy767tK+xs7G2vr/F5Wn+YVN15Xzb
+ycjKxcTWcVxGOTQxLCosLi42RU7wv7q5sa6xsrC1v8LJ5+/sXFd97u3XzszOys32b1M8ODUt
+LCwtLzQ8SmTRvrqzr7Gysri9wM3b5PRrYXHn38/Fx8vEx/hlWTs0NC0qKywsMTtJY827uLOt
+rrOys73Cxdb7/XJdZOnd3MrFzsvI7F5jPjMxLSkqLC0uOUdT0rq3s62trrGwtsLEzmlfYFVX
+deHgz8PExMHMfFpJODAuKygpKy0yQFjiv7a1r6yur7W5vc/c709PVlJo3tHJwbq8v7zC7V1B
+Mi0qKCcnKiw0RmDOuLCuq6uur7W9yelgUUdJTUxh28zCvLW1t7a/32M/LywoJCMlKSwzSvHF
+saysqaitsbbD2WhMQzw+Q0VY38q9trCxsLC9ze0/LiomIiEkJys0TNy9rqqopqeqsbjHWEo/
+NjY5Oj5O4ca5sK6ura61weFGNSkkIx8gJSkvQ9u8rqikpaaor7zJWD45MzI1Nz1M8cS3r6yq
+qqyvtc5MOi0jIB8eHyYsN1e/r6qkoqSnq7TIaEE0Ly0uMjtL68O2rquoqKmssLrbPy8qIB0e
+Hh8mLj/YuaumpKKiqbC/XkEzKy4uLDhJWMSxraqnpqerrbDKej8qJyAcHh8fKDRDxrKrpKSl
+pau3zEY1MSoqMjM6dsu8rqyop6mqrLS/x2Q2KigjHB4jIyg4a8KyqqKip6isv2o/Mi4rKjM+
+Qeu9t6+qqamrrq64y9B+NysqJh4eIykrNGe8s6ylo6itsLt0OjMwLSwzSFjmvbGvrq2trbG2
+ucLM2UUvKygjHyEnKzBC1LWsqaalqrG710EyLi8wMT56z8K2sK+ur7Gztru+xdluRC4oJiIg
+IicuOk7NtKupp6iss773Pjc2NTM6S3HRxLu2tLO1tbS2ub/Lzd9CLykmJSMiJi46Tc+2ramn
+qKyxu9RMOzk4NzpBTnDTyL65uLi4uLq9v8PL12M+MS0qKSkqLTM9WM+8s6+vrq+0vMnrUEVB
+OzpAQkJKX+7Uxb+6tLOzs7O3vcn2RzkxLSsqKywuNT9Z3Mi8uLWztrzByd9dUExHR0tOWO3Q
+xry3tLKytLq/zWhDODQuLCwsLjI6Qk/yzMO9uLi6vMDI2XFcT0xQV2D52s/Iv727ubq9wsjQ
+a0pAOjUyMC8wNDlAS1/hzMK+vb29wMvbfV1ZWWBo79bOx8C9u7q6vL/Izt5bRD48NjEyMzEz
+OD1AS2jdzcK9vb6+ws/g731z+ufXzsrGxb++v8HCydPe8ltKQz88OTY0MzQ3O0BKWu7QxL6+
+wMPGytDe397q5dzb1cvIx8TAwsnN097f4WVPTEY+OTY0MTM3Oj9MXurKvr29vsPFyNDd5+fw
+9+rj3dLNzMnFxsfEw8bLzt9dTUU8NTIwLi8zOD1IXuHLwL28vL7Cx83X6Pd2dPXs69zT0MvF
+wr++vb/AxdNtTkM8NC8uLS4yNjxEUubMwr28vL3ByNHf7PR3d3X47d7PzcfCv729vb/DyM7x
+VkY+ODEuLS0vMzg+Smrayr+9vr+/wsbN1dzl8uzg2c/MysbBv7++vb67vtVPRT82LioqKywu
+N0BRfcu8trO2u8HGzO5aV1ZVWmbu08bEvba3uLW2urzH+Eg4LSooJiYpLTVE7L+0rq2srbG6
+0k0+PDgxOEROadPDurKws7Cws7i7wMzZaDwtKikkHyMpLjVHyrGrq6ilqLO910IzMC4tND1A
+WMi6ta6sraytsrS3vs5rUDsnIygiHSEtMDVvt6yopqSiqbvI4zYrLSwoLkBHb7+2r6usq6ms
+trW2xd/mVi0kKyYcHissKkK/saunpKGms7vAPSouLygqPUtO0Lmwra6rqKuysrK+0NdXLyYp
+Jh0dJyopOMu2r6qkoaevtbxNLS82Kik5XVLkt62urqqorLKzsr/p69w7IiUsHRklLykwzLOu
+qaWho6+/vHQpKjkqJTrvV9a0rausrKiqtrevvVvf7CwjLSYbHiwqK0bGtKyqpaCpuri/OCkv
+LygrP2X7yK+pq66qqa+3t7rKaVhMLCIpJRwfLi0xbbqtp6elo6zBzP4vJiwuKjBP5s67sKqp
+raypr7m5vOVT4DsfJS8dGis3L0bBr6WmqqSlumHvQCclLC4vOnHCurqxq66yrq+5ubrE3/JZ
+LiMnJR4fKzhH2LeooKSoqa7KRTkuKCktM0LtyruytLSxtbi4ubq4vse/4y8kJSMdHSYzTNe6
+qJ6hqaqvzD4uKSgsMDpiwbq4srK3uru+vb29t7W+zcxGJB4iHhshL0q9rqifnaWuvWk8LiQi
+LUhO77atrrO9x7/A2trJvK+uvMi6YiAaHx0bHyxVrqWln52jrss8LiwlIStPx7uxrK2ywujp
+2uDYxbuup6vBUjwkGRkaHSc7166gnp6hrcNWNCkmKCw9xrCura2zv/VGSmnjzryyqqesvW8w
+HBcZGh4sS7qlnp+fpLToQC4nJis1Z7mxraywvtlLPkpf58e1rKilrMhEJhgWGRojONmtn52f
+oqzCSzIsKSsyRMWuq62xvc9rQztBWsu5sKqlp7VcMB4WFxkeLEq6p5+en6e46z0vLCstOuCy
+q6quucbpTD4/TW3JuK2opqvEQCsaFRgbJTv5s6Sgn6Grvk4wLCwtN0jMr6qprsPxU0dHT13p
+xLWrpKOs5TAfGBcZHSk50q2jnp6krshALywqLDdkuK2nqbO+ekxLSE1Y6cGxqaSnukQqGxcZ
+GyQ0SL6rpJ6fqLLiNSwqKzI95ritqKy3xm1cXVZmed/ItKqlp7lHJRkYGh0nM0fIrqKcnKKv
+7TIpJycsOGG8rKmqrbzP61ZUUU15ybisqa6/PCIcGRofJzjovKuhnp2frdg2JyMjKTr2vK6s
+qquyu8djRkRJ+Mi7r662zzwnHRoaHiY09LeqoZ6eoavFPCggICYyc7quqaiprbK72E0+PUrn
+vrOxvV01Jh4eHiApMUy7q6GdnZ+qv0IrIyElL0/AsaupqaywuMhiR0NHX8m8vc1MMSUfHx8l
+LDZku62jnp6gqbxKLickJiw6fL2vqqiqrrS9z31kWmHa0NjmTTUqIyAhJSozR9G0qqShoqat
+vmI8MC0uMTxUzru0tLi+zNXb3t7XzczMzuJgRTkwLSsrLjY/Ut7Hvbm4ubzAxsvQ0dPY1tre
++1JHPzw9R2vOvbe0tLe6wtVpRzw4Nzc6PkZNT09NTE1SXP/r2czKxsTEydDmWEpIS1vgyb67
+uLi5vMHNe01DP0JIT1pfW1FIPzo5Oz1BSVzlzcXBvr/J22ZNR0pc3si+u7u6u7/G03VPRUFI
+XN7Nx8vXbEpBPDo4ODo/Sl/j0s3P3WxUTUxSatjGvbq4uLm8wcvdYk1GRk5p2MvKy9X9XlFN
+SkQ/PD1CSlFZXFVNSElOWHvVycC8urm6vL/K311MR0VLXOvTysfKzNLb3uh1XFBLSUtOTkpF
+Pz4+P0ZOXfXbz8rEwcHGztz9aVxebuzZ0M3Lys/W1M/O0Nfj721pZVtUTkdBPT0/QEhOV19k
+afzh3Nrb4+js5+Ha0s7Mzs/R0dDPzs7Oz9Pc4+r5bmNbU1BPTUxLS0tJR0hJSktPWWH539XQ
+z9HW2NrZ2dbVz8vLzMzN0drj7G9pa2lrcfjq5exvXVFNR0I/P0BESlFebe/e3d7c29/d2NXT
+zsvl173N287P8mBg0NLm1svO1t73ZU9IQj8+Ojg6PUFHVmB61c/Vy8jIwr29vsPS5flmWFxe
+auvf08nBvbq4vdBgQjAqKCcoKy02T8y7rqmoqKqzy2dJOjU1MzpLbNW/uLOtrLG3ub3BzmJO
+PjAkHR0fJis4W72qoqCjqK+82jonJCo0T9C/r6eor7jDy8niXNzIxru+YEAuHxwdHSArR76o
+n5+hpbDsOSwkJCw8x66opKStvu1EQkpMWMauqqusuOk+HxIUGBon/LqlmZqgpr83LyofJDNN
+sKKkpqi6VT0uL1LbzrSqp6Souug0IBgSFR0pWa2jnpugsdI6LS4tKznSt6yqr7i+YDo6QFnK
+v7+upqq0v9FKKx4bGR0sOtirpqakrdlPPzU1My5HvLe0rrK5wko2P1V+zb+2qqizyN5CNzgl
+HB4pOFDFtqmlq8dUQ0BFPjM6vrS8vrq8veg1PF3WycLEubLH9WZMPEpcNykiLEJDTcWwq6vA
+XWPaakU3PL+3y9K8vMByOEDq0/Xl1b62yX/MxXBNS0cuHSNASE68tLGquUhJfkVCRkbCssDO
+vMTTZj0++c/pzMfHvsDPzcbPekhLLRcdPEZRt7CupLU8OEs+R2n7uKy23d5cVltJSti8xcnL
+zsrBwMfS5dVIOjcbFzD2VLqqtK2tTS5GVUN+yb+1s+pFU3x83MrQw8dkSF7Nwbu7v8K/Yz07
+GxEmfl28p6+sp2wrOk1B7sXFu7DNQFPQy8bBfWnlSz5Pzruur7u+w0s4PxwOHm3rvKSrrKTc
+KS9GPtu6w7+tvUBMz8nLvv5MYExEW8a3rq6/1cj6OTUcDRrMwsunoaWjySQlP0Z6vLqzqbhA
+T7u1xOVAOUZBQOKxrrS8xN5EStO+KgkL0KlMrpyfn7ccFjC7uq+npaOvLibLq7xwR0JKPTRj
+rqasyz5AWDQuy7AjDx/hTlSvqKasMB9VsNnEqqytsUM3uq/nZd9SWVk3Pbux22vP2FhP91Ms
+HiAtODpJuKiy5khKz7y8uLG4vr3K2MbD4HFqRTw9Xbu9YEXdxEk85s1JKiIpO0Y+97W62eJ+
+6rm0vLe62ebOxreutWw4OD81L/TBzsTNPnW2VTIyLSssO8u2vn5TVmDYvrOvs8diVEhpu7G0
+wtFRPDo9SUZXyOBexsHSSTY3MjE/XXVi+tDPbVbHuLzL7cq+119dyLa81VBYflRFR0Y7Ql1p
+2b27vM1DMz1MTkjs0NzQUUNd2+Pb0d/W5V7639TD11hYwsPc50NeUDxAPMSxvcxbXVpJT+zG
+zOjfSlNMSmRQ19xe2/FVTWDebtPUSfLKxNhFQEFOXlXEtr/S1+pGPVjJxdHVzsxfQkv1zdxV
+W09Valxq3OLh7mx9ZVxbUVRKSFvay8i+yNVnREZZ3t3Pwc9lZV1fXnB4eNTpXF9q9PxzaurV
+319ZWmdjVVvv1d/ezMzfV1JVVGbYx8xtWltoXEtZ3tx3WvjV3/753HdXZXX68V1efO1raOXj
+3t/Z2v19dO/c5+XdelZMWGxdbuvZ2/149+PnfXlzXV1u6f5tZl1wbmV84s7abl913+5ebH3m
+ZFR27nNn6tnpb+v5dfdx3M/aa1tt3tbgfW5oYWdaZN/yWF7oXFzb7F5e6910ZnVp4dT25NTb
+ZHPr6lVR/+BlYdzX8Flc/WhaaO/p9HFgWV9weO7m2tra2NvqcujjeWX25/ff4UhF0s1OTNbK
+Xj9Wz1tEeszlXXrycGxh28nYfc/M515rclxVXnJsZmR6887GclPZ105Me959Wlvd5GFn9eFy
+etzwe3Za6tz6eN/f/ltv/l1e59jiXlPoy+BDTc/xR2fjzd9a7tLoYlhu32dd9N96cuDg5/jv
+5Ox7/N5rVXzuX2ZscXni+2jf5F1x23Rc69x6XnHocVZg09xOXtDmUVzW1uR56t5uUFRfcm9f
+79Pf7+HY7Ghu83R48fv1/nBp9/Lo4nvq3d56ZuvX81Ro7GFPUVxrUEpm7m9yfm/nc1Ff2n5Z
+2MrS3tTT1dLg4M/S5tvQ2NvU2OhzYHzg295iPjI0OzQzRFtPT1BZzcbjzLu+xcXJz8rGyMnM
+z83N2tbU7GhuY0tIT0tGYeFbSkVGSkxIR0lHS1ZlYPbc2dPOy8rHy9TTzsfHz9rd29zn/uvd
+3dXW5OXf9mFm/+tTPj9MPzU4PDxATU9Z7Ojp0MnMycLGycbExcjS5N7b3NjZ3NfW2tza3/xu
+fPHz9mZHP0Q9NTY7OTtHTVX97e3PxsnFwMTGxsfKytPo5uDe2Nnh29DMzNDa3+R/aHN9ZUk+
+P0E8ODk4OT9LTVns49zPx8TAwMLCxMfFy93s4NzU097c087N0Nrr8HddW2ZfVVBCOzw9Ojg7
+Oz9MV2Lt3tjJw8G/vsDBxMnMztLb5Ofm5OLi29nX2Nje7enn82NdX2BVQzo8Pzg2PT89S2pz
+7dfPysLAwMDEx8fLztXi6+He3tPS3N/f39vW4/39cWxfWldaXk5BQEZAPUBCQk1eW17u29PN
+ysrGxsnJzdXX2+Pn3uLe1trc2M/O0tvqem9zY15bVFBPTEVERkVDQkhJTlVZYPTf2czGxsbE
+x8rL0Nvc4+7p5uDe2uHp49/h7Pp9a11bV1FOTk5SUlRVV11qY2n4/Wdu8vHx6t/k4tzb4ODj
+6ebj5u70fWtv4tzZ0tfd5uzv6f5zbVtWWVxXYGFjefNvcef7fersc2tfXWdpY2NrZ2d4f3d4
+7ebr6d3g7t3d6+Th8+vm9/Xt5uzq6ez67u1/amBhdvVnZmdXXGhfXnNoW1pebnp7a3b27+La
+3OTl6enf3u91/P5ubPn67+7o6f3s+ujq/vfo+/pnXnJpZWdpa2hja3Nrbm1qdvnm7Xz16vRm
+bnZsevru6NzmfOfp5+nl6N/d4/n6fW9pZGNcW15gWVthcnNu/mttePHr3uTs5ufr9N7c4d7i
+7vfs+OblcGX19Gd0fWtkZ2lrYl9qX2JveHRpaGJlbXf573xy7urf2ufi2uj/6dzs6+/s7v3u
+5Pr372lpfG1faGZ6bFtldGlefnlmaWdlcH9icO/m7env6eHy6Of27Ovo4+Dj6ODednV6bXr2
+bl1iYktQ0N9gZ95faGvx5Gteaed8/3nw9m/8f+br5+vy7Oz2b3199v7t9nN77vdnb+nu+urp
+bXD9cG/9e2nuaFpiXlhj4vZ46uDo5/Vw6uDl6OT4/unmferc/k3uzWpb39thdF51+2VUXnFa
+Xl9maulnX/X8/Xvr43ru2uJw39ri3nzs2uPm5X5oaOzkZ2X99uvkbGlrU1Ht8VhpeFxV9f5g
+av7re/Bo/Gnf3Nra2fH+4nLt4OZv395w4fz4anxW9+JgW+/pUVlqbWJwXG1uYPp54d/2Y+XW
+42lja3Pz8n9o83/p3dv98tngbfvsb3fzZlv4fWNn7l5X5Nxv7N9mYHFiXH/x7nxnbH717fp3
+723m09zycuTt9+v8XWpwZV1v/HB1Xmnk2PRoc+vpbWdtbVVa38/f8nd73udndN9+WV1ubWV0
+4N3q9Hvq8XVudO7o8Xp8eHH7emNlfXx47OJ4+N3pcfPqaV5//nJnZnBqZG7o5vdvd3tmXGBn
++Ovp3t7l6enr6Ozt/ffu73F1e3t9cXH34vJsbPR4ZG50ZWv6dnfw+fl77uTn6+rf5OHo/nRp
+W1JPSEM/P0ZMXOXPysK/v8HFxszR197j+ODY1czHwb29zUkyKSMgJC06WMe5trWzsK+xt8Vb
+PTUzNkFm0sK7uru5trS0trm/zGQ6LCQfHh8mMUzOurCtq6imp66/VTQqJys1StbEw8S+uLGt
+q660vs/nUTouJiAeHiMtRs22ramnpqapsNA8KyQkKzzevrq6vby2rqurrrrPYVhcTDkrIBwb
+HilBw7GsqqqppqWqukQpIB8mO8m0srnDx7uuqamuvvVMU9XKbjQiGhcZITXbvbevrKafnJ6r
+aiofHSU507zBzt3LtamlqK/DaFJpybm8SiMZFhgdKkfe582uoJqYmaDBLiIiJiw5T2b1ybix
+r62sr7e9yOBdbce8ZygaFhgeKkTxaF66oJmXmqPCMSUmKiwwPV3Ww7m0s7Csqq/BcEtIWsez
+tzobFBkhLD1YSTzWoJaXnam9Qy0sLSgmMOq9vr+6t6+mpK5lODxczr+/xmktHRwlKigoLjtb
+sp2Znaiss/s0LCspLUXe2M+6rq2trbHGYV15VkpY5si6uUEeGiMvKiIpQMmtn5+qs62v2jQw
+O0BDVHTyyrGssru7vvZGT+TffvPkz7irtSoWGioqHR86v7avp6Soqqiv4T9KZkk2NU3IubzK
+1sW2uM9NSWjVysvXzbmuvyAVHzUkGSFtu8Szp6epo6e+3r7COy4/dU1H0bu7u7a+6F3tzczL
+3FRdzL2/TyYbIC4pHiFBvb67rampqauyvL/LVT9BTVNryru+ysrK0tnV1M7PUTtF3eQ5KSkz
+MScoO97S1cOyrK+4ubKutsfW2lY4Mz1qycHIyMLLblzj1l5ESEU9PDo4ODs9PkzVxcjJyNLb
+v7e6xL20utxGPT5FR0ttz8O/x9zYvr5PMzg9MzE/Vt/lYWrPxsjT9eDbX0b6wcLNxr7G2E5B
+QkxNRlDUx8S6utJt8GRANTQ9VXFLSsm50l/LwtZdVXfVXUBnxr7F41voz0I3VFlIesnPwbne
+5MXhOD9ZOTxi0lrXxNLgvr37fdFvQ15TVF/W2OTlaVFJbnBNR//d62vn1MbG7WZe2/lifvrn
+32VV6+Tf2uRmU1dP6G1hf+7eYvT+4FlhVGlWX+vi3Ovu89TV3N/PzNlqTUpYem9fWWrZ2H75
+6WPz3VtNY+1p+dbT315UbGZaYuDnfvzs3dbk2tLZ4HJWTExSXVVa8d7T2t/f4OhuYmR1ZnXp
++Xzl2/plY2dufl5Tb9nv4tHZ5Oj/6uBeUVdRVWD52dbd6efrfG999Ortf3Zub/d+ZWn6fG1v
+dnd46/h07eb29G1ZYPrtfWRs5tnb1uLl5eV5amltb2FcZG1qcHl0bnTw6vxqX2FvamR+6ODd
+6PDn3tje6uPl5ubt7vfs+nZqYl5aX1xaZGt59PZzZnZubnZ6/X5yZGvp4+Pl6N7Y1dnZ3u7s
+6eru+nBfW1dWUlRcX3Dq8Ofd9G7z9P5sbXZ2cGFlb/R7+ujb2d3e5ODe6vVw/ux8bWFbWF9n
+ampnbnN66eXs+/j4eGhkev78cnpybP3t497e3t3e5+7reWVma2tpYF9nb/ju+G9qbPz2bHX8
+/e/s7vXt7/96bGZgYGZ09u3x7uPe5erufnJveX3++/D9fPnz7/f5+PHw8/D6amhucfbr6/pu
+amhfXV1eYmd9/PL973Fx+PXt3+Lm3uvm7+jo737v8PBu/OXv9+30eGJi9/JeXmxgWGFoZW5i
+Y/L3cPbk7+Xy6Oz2+3h46eDl3OTs4vP98+32eu5lffJkZHlmXVRr32Jb+fxu+nNr6nZq4+vd
+7Xbh2V1Na+ZV6s/n2cz89Nb4aeBmcnVeUmRUXmb5Wmv/YXT53/ba/NvudmTUeV7t63Ra+mvi
+/uNV3d952ePc5d18XlFUbXpLadJGe9Nybc5uU8jV7fNb2dxPTtbdTl5+4N/4T9fLal7x1v5v
+SvPOYUv+2WbmWm/F6Exowl9V32nP6E1fv0xT3190z1BJwP5KbdRR6HtW2M5SUczkaF9/8tH4
+a+/c9ujO1c3Y1tLF1N3Y+f1nTTg5MS4zNDRAbs6+urawsr2+v8rP0dTKv8HCvLe2umYxJx8e
+ICElO8azqqKgoai58TwtJSElMEbRta6sqaqvtLi8yM7E2EA4Ny4mIyUtPEVsuKqpq7C6yGc+
+NzY6SGbs2sa9xt/veXzaxr66t7a8yd5NNysmJS48Qna5r6+xu9BnSTUyPEdV0L67t7fF/11L
+Rktp28O4tbOzuM9LNy0nIiY0SO28srCvuN9LQTo5PEfQurWysrvKez83Oj9K5srAubGxtbrJ
+TzIqJyUlLUngv7Ctr7K/YUY/Oz1HVdi5tbvB0mNNQz5CVd7OzMXEzdDR4fzx6mxNPzcyMDZI
+7Mq5sbK3v9xYSD8+Rk9hzsLFx8jdUkc9PUVNXuHW0sjDxszLycnNzdH6TzwzLzE7UNjBube7
+xOFKPjw+Sn7Sx7u7yutRPTg8RGLUxLm3vcTO6GNRTVPszcvHyNZROC8sLj7uxry1tb3YSTg1
+O0hn1cS9ur7VU0E+SXvRxby8yuF9T0lLTl/YysTAvb3QSTguKSs3SNq6s7W4wHBHQ0dQ7MvG
+v77I31ZCPkZTXFhLXMvJ083Q2c3L3m3kz8jMYUM3LiwzQvW7r66wudFRQz08P0750sbH0O1V
+RkVPUkplzc/LwcnTysXMy8fPzsxsPDQvKSw9Ycy2r7K1u/4+Ojk7S3ThxbvByNdLPT47O1TP
+yryytrq7xNzuX1Jfdl5JQjorKz9k1ry5wcDFXz8+P0zVwcvKvsfYdEE3O0NN1768uLa8yM3Z
+8fns18/U/EMyJh8oO13EtLOzsr5eQjw9V8vI0sK9x8pwPDU4RWzVxL27u77Aw8TI53nn62U/
+NCsfIjRNzLKvsa+09z89OUrGu8fEusjmUzYtMENh1b25t7a8x8nGzNvg3dXIzEEsIB0nPVzE
+r6yrrL5CMzE4VsrJxba4wdk8LDFBQ1PMvrSts766ucPPZ07tybzKMiEdHik7T8qupqSsyzsx
+NkFOX9bCsq+8Wzo+Rz48SeK6sLO3s6+2wmBASf3NaTUlHR4qMkPBraaiqcFLOjM1PEVyvbe3
+sbnMcEAyMjpNxLCtrq6us79lPT5VZEctHh0lLjvpv7Onpa/JTDU4Q0RM7s2yqbHByms+NS4z
+5bOurq2trrHBUUE7NjEjGRwqNFq+vrGjpbPROjVXTz9DUNK5tbi4tLfMQzIzS8O4tbCsrLTI
+UzM3USoaGR0pTMbIt6ijp6/cQT08MSwuQLyqqKmqrbhzNCwzVcy8tK+qqrTdSkk5IhYTHClK
+v7OspKOptno+NTMvLTbYtq2qq62uu99OQ0NGRV7Mu62rrbbCbzAfFxQZIC1YtKWdnaCrv0k3
+LicpOG2/s6+tq663xd78cFNBPETpwLe4tq+y3CsbGBodICtItqOenqKpsLtlNSssOEhV6Me4
+srS5u77F3Ek4NDQ+WWvgwrexssNHNzk4NDI1PmPV19bc6ebxZ2Rx4tHOz9XOyszV2tTT3l5M
+TVhx0ci/ubKyusp8STs4NjQ1OT1CS01NVVlVXW1f8c/Lwr/Bv7/I0+B1ZFxdcPXt5dnEu7m6
+vcLN41VAODc7PT9BSU9RTkZBQUNHSFXvyrq1tri6wttcTU1YZG1/3cq+urm7v8nY/VhKRkVG
+RkdMTUpJR0VEQD06Oz5L7cS6trOzt73M7VdJRkdNXufNwr29w9Da4OduW1heb/91a2Nsb1dF
+PDc0NjpCWdXCvLy+vb7K/U9HRUhMVnXWzMrJzM7S2dzj+ePT1NDNzcvP6VlKPz9BPz9GTldj
+bHT/6Ozwc11ZWmV/7uTcy9DTy8/g+G554NnPyMTBv8bU62pWTEhHSk9TU0xIR0ZKS0lNXHB4
+49jV0c/W08/YZ07vxH5jxsfU0dTBvtXb2mZTcmFt4XlZUT89Pzs1OTtAWm3pyMK/ur3GxMDH
+ze5cW19QTVJZ+9PNyMLDv8HR1c/R/lpDMSwsKCgrLTl6xLaqp6Slq7LDTz86Ly40PFbZ3cm4
+tLOyuLeyu8rYTkJDLyEgIiMnKi5VubCppaShpLC/aDYvLSksO0rVvLmxq62usbq8vcva1U48
+QTAkIyQjJigtT8S3raikoaawvO86MCooMDpB37yzrKywrq+6w8bHxs7sTUU7KiQkIiIjKDVf
+wbGoo6GiqLC+ZDUrKCctNz/et6+sq6yrrbe/xsrK3FtJRTkoIiYlISMqNmfBtqujoaSpr77s
+OysoKCoxPmHAr62sqquutb3JzMnS+HNMPjEoJyglJCkwQ+W/sKejpamtt85GMSwqLC81P+K+
+uLGura2vtbu+w8TJz9xfPCwoKichIio0Qmy9rKalp6irtMxIMi0sKiotOFLTxbmtqqqur6+z
+vsPDyNFpPS4qKiYhICcvPFTDraajpKapsMBUNCwpJyktMj1dzLuwrKysrK61vcfLztDW/U8/
+MCooJyUlKjZN1bmspqOlqa68eTsuKikrLzU8T866tK+tra+1ur/L1tjQy8vVXjwvLCkkIyYu
+O1DJr6ejpKituc9MNi4rLTE3PVDTvru6t7e5v8XCwcXEvru7v+0/MCsnIyEkLDpVyLGopaWp
+r7rOUTowLi82PUVQ/MzDw8O/vb29vLq4uLm6vMlYOCsoJyMhJjNJYsmxqaeprLG80Vc8MzQ4
+PD5EUGjczs7OycG/wLy2tbe3tbvSWTgqKSgiIio8T2S/rKipqq24zGtBNDE2Oj1HYnreysXG
+ycrMy8S/v724trvJ50kxLCwnIyk1P0zJr6ysq621yWRFPDg0NT5c6fDVwb3BydLb1M/e3se8
+u7y9xnU7MTMsJSk0PT96uK+vrq+0v/VBOj04MDts1OLVu7jDys7b6PFnYNG/v724usV3OzM2
+LCQqOj087LOvsa6utstUPTg6NjNF09Hfxrm6x9nZ1PVMSmTc2dG/t7i7w/09OTQpJS45N0PE
+s7GurbG6zFI9OjczPGVwccW5v8nCzHR6ZEZL+/lzy729urW621NBMyooLDE1P965s7OvrrXL
+YUtAODY8SFN0zcPBxMfC0FZl5lxPfNbZyr++u7e7zlg5MC4mJCw4OUq+sLCtq6+4xPFKPjc2
+PEFIZtPLxcbJw83t3/dWXvTr2sa/vry7wNxFMS0rJSUwO0DPsK2sqKmvvNpPPDEuMTc6StXI
+wbq6vsXV7WJQUl1728zCvbm5u8xROSwqJyMqO0J4saiqqKatvedFNi4qLDM8TMy6t7WytsjX
+3U5CTVha6sjAvr27u8ZbPi4oKiYkN1tXvqenq6erv+tONy4tLTRAUc+4uLq1vNbS2kxKWVJc
+29DJvL29uc1WSCwnLyUiQXFEu6WrrqWsxddNNjEsLTg/Sda8v72zu8vJ2GRpYFfv1NTEu7/F
+w9ZOLygwKB0x7zpgqKiwpaS3ydg+MS8sMD4/VcG9vra4xMnaXnNcSnzM1cm5u8K+xlRDLCQv
+JR47fjnFoqqvo6i/yfU5Ly8wMDhP5tbCub3CvMLn7N5dWNfN0cC6u7zG2eA+JScwHh5NZzm4
+oayuo6vFz184Ly8wMDxRcc69u7+7vM/Y0vNdd9PJxr+4ur++z1JAJyAuIBw89Tq/oaqtoqm/
+xXU6MjAzMTlX8tvAu7++vc3b2Xhl6tfLxMK8uMHGxFA7LCApJRwv20POo6SspKe7xec7MS4u
+LjJCXOjEvLu4vMPDy+fb1eLLv8jBub/Lw+U8LB8lJxon0kZWpaGspKKxv9s+NC4sLDA9T9HA
+vbazur7A0+jZ19vNyc7AusnHvFQ/OB0fLhwe1s5Dqp2pqKOwxes4MTIpKjs8Psi7x7avv8G7
+z93N1trIx8q+ub/Jwtc/MCEdKR8bR75JtZ2jqqSsxO47Ly4rLDRDUtG9u7a2vcLG0NXRzsfD
+x8S8wMfE1GNFLx4dLB4bVrxQr5yip6aywugzLjcsJztRTMS6u7K8zb7JX87B0MW8vbq4vsPH
+6UU0KR0eJx4j177Lp56mp6zC2UwtLjQqL0lOzbrAtbPKzL/e+8LAwbm6urW8y8XvOjUnGx4m
+HiTjv76poqSlrsLIUi8xOC4zSVvKvsm4tdnfxN1vxbu8trO2tbjK3mo2Jx8cHyIfLsO+uaSg
+pqiyxM8/LzY1Lz1V4LzCzrvGUWTa+tS9trGvsK+xusniPyoiGxohICROt7OqoqOmsc7+TDEu
+Nzk9V9O4t9LJyVBNZV/Yv7mvq6ysrbfcX0QoHRsdHx8ncLOvqqKgp7bN80UwLTQ7SGPMr7LL
+w9FFPj5F9cW7raeoqay5y2YvIRsZHR8hOLyvq6Sho6/P/U8zKzE4PlbVsay6vL37RDw7TubP
+s6ioqKiuvdkzHxoaHBsfOsWwq6WeoLHP4ksvKi44S1njrqi0ur3iWT41T9LWu6upqKmwttsq
+HRoaHBwhSruyqqKforHUckYtJyoyR2XFqqetr73cWzs3S/XMs6uqp6u5vl4nGxkcHBwoW7Ws
+qqOeo7njXT4tJSczSGG9q6anr8HMZDw8TuS+sa6oqK26TjIiGBocHCM6z6+opaGfrMVpPzAp
+JSs5T8u2raWnr7jIblBITtW8ta6ssLnIQSIaHBwZHi1nr6ypnpymttRLNicgJi4/bc6vpait
+r7a91EtW0cnEubSwuOFGLR8gHhofLTzPtaqfn6artdk7KiUmKTI+Zbmsq6uqrK+81NbO19rI
+xcfMZDsrKigfHiYuPWnJrqOlqauvvE4xLSwtLzNJy7y0r6ypq7O5vcPN7GJWVVc+Li01Likq
+LDlWWN22rautsrK31Us9NjQxMj9W3sW9t7G1u77Czdnuc2757+jZ3WtURjszLzExNj1HaNDH
+vrq5u8DO+1NJQ0VNX93Lv7u6ubi7vb7IzNTtX01JQzw3MjAvMDE2P09+08S9ube7wMfP7FdH
+R1vt5dK/uLe4ubq+xc/qWkpCQD46ODo5ODY2OT5KWHjRwby7u7u9xdPuY1ZUWmbizsW/vLm3
+t7vF0f9MPTc2NDM0Njk8PUBJT2L52szCv7+9u7zB0OprWlRUX+PMwb26uLa5v835TT01Ly8u
+LjI2PEZVdtzRysfHxsjIx8jHyM7c73VfYXnt38/Fvbu7u7/J408+NS8uLS0vNTxKatvLwr+/
+v7/Cx8jMzMrMz9Ld7P7t5d/b0svIyMvQ5VpJPjcwLy8vMzg/TfzQxr++vb2+wcXJzM3Ozs/R
+09HP1NzZ2d7j6uzvblpRSUA8NzU2OTs/R1V1283IxMPAwMLFys7Q1drd3NbY2tnb3d/n3+Tp
+6PhiVk5JQj07Oz0/QklWYvXbzcnEwMLFx8rO1uByWvfGxfVmzsXlYtrPfFBe7VlFQ0pJPjw/
+RUNGTmDs1svExcfFxcnQ09PkdPHsaWTv1tbl4M7P3+Lb3OtvZ19USEA+Pj5ARUhOXX3d0M3O
+z87M1evm2+Ht7u3Z2/7u19tqb9bL2u/Ox9V1bGpXTUhDRUZDQ0hMT2Rx7dbOzdDPy83u9s3P
+XVjg2mpQ/dfnaN7J2GzZy99n29xUT19SPz9MTEZHTl1nYfrT0NPOys/U1eHc1+heaN3qW3rb
+5NXY2tHN093V/Vx2cERCVEs9RExEQkxgd3LjzczOzsvI1uva4F5fcPfw7/jhzM7M2uzLxt5X
+28h0S2dmSUVFPD1EQkBFYuHq7s+/wdDTwcbmdN3ZW1pt/vF179vU19bI0tfT2dbV5U9JUUw8
+NThAPjtEdtXb08O7u8DDwsXT6/J5W01QUl/yZF7GxNrTxMHHyevpcEY8PjsxMzk6O0BX2MzG
+u7W1u7u7wNX99l1JQ01RVlpk08zOyMPBvsLPzc1QOj09MS0vNTk5PFPTx762sa+xtrvAz3ZY
+S0VBQUJGXezdzL+7ubi7wL/BYjw7PzMoKC42Ly4/z8XNva6rrbO1srnRXFZPQTk5QUpNWN2/
+ubu5s7S6wtFaPDc2LCUoLi4sMlbLzMGwqamtrq6zvt1TRz84MjU7REtfzby4tbGur7W8x2A6
+MzIrJCUqLC0yRdnFvLCqqaqrrrK7zlxHPTYxMTY7QlnTwLiyrq2vsLS/dkA2LyslIycrKy47
+98C8tKunqKutr7XFeUg+OTQvMDc+R1vNu7SvraytsbrHXjswLigjIygqKi5D2sS7rqinqaqr
+r7nG/Ek9Ny8tMTc7Qv3Cu7Wuq6yur7XGXz40LighISYoKS0958e/sKilp6iqrbPD9kxCOjMv
+MTY5PlbOvrmzrautr7K72Uo5MSwlIiUpKSo0T9fIuq2oqaqprLG6xOVWQjkyMzc5O0JpzMS/
+tK2usbK0vNhNPTQtJyQnKiosOE7vz7uvrK6tq662vL/TYU5EPjw8PD1HXujRxbmztLa1tbzO
+W0c6LyopKyopLTlDTPHAtrazrqywuLe3wuH2d04/PkBAP0dc793Ju7m6uLa6zmtcSTguLy8s
+Ki45PD9VzsPBurKvs7ays77Ozc9nR0VIQj5BS1FZ78e+vrq1uL/O2XlENjI0LystNDc4QmfW
+zcW6tba2tbK3vsPI5E9KTEY/QEhPXHfRxcO/ubm9wsvcYEM4NjMtLC8zNDlFXuPRxbm3uLaz
+tLm+w83mal9WTEhITVJe8tzPxsO/vb/DzedsUT42NjAtLS81Oz5K6c/Lwbi3tbW3uLzEytDr
+YVhOTk9QVVxr4NjNxcPAv8LK2XRfSzo1NC8tLjE2Oj9T1MrFu7W2t7W3ub3GyNHyZ15QS01P
+Vl9r6tnVysfIxcXJ0WhQUT00NTQvLzI3PkRQ2crIvre0tre4u7/HzNNzWVZOSkpQWV5j383N
+xr6/xMbJz3xQTEQ3MTMwLi82PUZR68a+vrq1t7u5ur/I1+VvT0pOTkdKUlpn4MzHx8C9v7/A
+yt9cS0Q5MC8xLy82P0hO/8m/vbq1tLm6vMDK3fZjT0lJS0hJT15t6NDLysG/wb/BxtFjTkk8
+MC8zMC81PkVMbsm9vLu1tbq8vL/K2/B3VElLTUlIUlteetbNzcnBwcG/ws3mWEpCNjAyMS4v
+OUBHXNHCvry3sbS4ubvCz+L9WktFRENCR09cZubQy8S/v7+9vMXea1dGMy4zMCstOT4/T9PA
+vbqzrrG3tra+0d/7Tj8/QT48QEpNVOjJyMS7uLm5ub7QX1JLNiwvMSspMj08ROK/vLuyra60
+tbS6ydrlWkA7PTs4O0ZOVP7NxcK5tLe3tLnH5FZKOy0rLiwnKzs/PFbAuLu1rKuwsrC2xd/y
+XEI6Ozw4Nz1KUWHSv7y7s6+0tre93U5HPC0pLC0oKDNCQk3Lt7e1raqtsrO2v9ZqU0Q7ODk7
+OzxEWnrWwrq2tLKxtLjA1VJCPi8nKS0qJy9DRUXRtLG1rqqtsra3vtVeS0I8Nzc7Pz5EZNvP
+w7u3tLOztbnC31NEPC8pKSwqKC09SU7aubK1r6usr7S4vc5qTEQ/Ozk8QUZHV93Mx761sbW4
+t7nG+0tEPS4oKSwqJy5CSUnWtbG0r6qrr7S3vtZkS0I/Ojc5P0REUtXGxLyzsba4trrJfkxB
+Oy4pKy4qKC9FSkfft7K1r6qrsLW4vs9tTkM+OjY3PUFDT9vJx7+1sbW2tLjF5FA/OjAqKSwt
+Ki07TE/9vK+wsKyqrbO4vMbfTT8+OzY0O0E/RnfKxcO5sbK2tre9z1lDPDUrJyosKys1S2N+
+xbKvr66rrK+3vsnfUz88PDk0N0JGRFbPv766sa+xtLS4xHdJPTYtJiYqKyktPVZh27qvrq6s
+q62xusDK6kc8Pz43NDtGSU3wyMC9trKytLe6wM9bPTU1LCUlLC0pL0n17cqyrK2trKyutcDN
+021AOjs8NTU+Sk5d18K7uLWzsrW5vMXeTTkzMSokJywsKjJR4djAsKysq6urrbS/zd1POjc6
+OTU2PkdMYNHCu7aysbG0ubzJ5Uw3MS8qJCYsLSszVtXOva+rq6usrq+4ytzoTjk2Ojk1OEBM
+YN7IvbizsbCxtrq9zl1DNC4tKiUmLC0uNlXVx7mvq6qrra6vudDj7Uw5Njg3NTpEUnvTw7m0
+srGxsre+yt5WPzEtLSolJCsuLzVPyr23r6qoqq2urrbO8mdHODQ2Nzg6P07908a6tLKxsbO4
+v8jdVEU4LiwsKSYoLzQ3RdzDvLWuq6usr7G2xuloTTw4ODc4PEVOadTDvbm1srK1ub7L2mhI
+OzQvLSwrKi41O0Bazb+7trCur7K1uMDc815EPDw8PEBGTmfcy8C5t7m4t7vCytl3WUE5NzUw
+LzIzNjxCR1zPysq9tre6uru/zNrxX1JKRkZOUVJo3tHNwr++vMHAzMzlWFtTSUA+PTw5PD09
+PktNXPLj18vDycnCw8/Ny9Xmd3BfZ2pmc9zY5dDKycrNztPWcWxcX1VMTVVNSUlJR0tIRE9U
+T01XbH7l39PNysvNyczZ18/i6Oni4/ng387X1tjX7n9uamVfXm5sdWT9cHxfVlhKSkVHRUND
+Sk1PW+zb1czNzM3KzdHP1MnW19bc0XrU9d9qX+tZ3Wln4vrk/9zl2nneXF9LTEk9RD1HPkVJ
+T11j+NLOyMnHyMrJ0tDV2Nrl3vvbcu103fXq9N3d2OXj6d707m7rWlVTUU9JT0FIQUpGTUxc
+YPLf28/KyMzMytjQ597d5N/99Gt49ejb2tjY4NXt8HJ2cHhjWl9eZmZfXmFVY1hZVk9VXFxd
+aXFq+/r95+vi5d/i3t7g2PrX5NTZ3NvT19Xh5uru/Xf2dGZiWWdfWVdUaVtpWWtlamRrbmte
+V15canb7duvmf+bk29ja0trR2urm3/Lt9OP64vf3ce9tZmxlbmd+an5rZmtv72ZrXl5fcGVj
+aGRYYWJwbX7p693p2OLW3+Xufu13/XDyevh06fTddO1v3/Z74Wrva254aNtc3Wbma/HwWnFf
+amtlallsY2Zq6vtt329xaf5hbtxd7edy7F3c4+Xb29P94X7e1Fz47dhsbudk1Wn173b9TmdX
+dVZZT1tjWVZkYNpx/vnt0fb2edDy6OX/1e5w7O7ceO5q7mZ9+/F96+vz7+dq1FzV8m39W2Ze
+bFNTb0xoVVl+Wdle097p12HeXtf0XOTucFnvY/zRdtTh1dZeyN7n4tn5cc39fONjbmdjR05T
+Sz9IS01QZV1x19fVzsrYzNbv6dzyXn78YP7jX97Z5H7P0+bY3OzTadR759J+WvHzWUdKVT9H
+PkJFW0xQ6tHTz8XHyMXOeNLfZl5jZln2eVnp3t3b19nTy+Pg2NN539Ly6Pb7WFRaREBRPjk/
+R0JEZvHfysLIvrjDzcjP9mxZSkdPUkhW/n3618zPxsPKyMTV2tLc4O1meW5KPz1EPjM1PkA/
+SFPiwMXEvLa1v8bFztdiR0pORUBDTF5l7tHFvL/Cvr7Ez9HP+HBcT1BRQDc6RDwuNEVBQlFe
+zLy/wLmvtb/BwszkYUdESkA7QExQV+3PxcC8u7m3vsnJye1QUVxRPTk3NDwzKzVLRD1S07+9
+vLizr7TAxb/NXEtHQ0A7OkBNVF7cxr+9uri3tr3Dw8tpUVJNRjw2LzE8Mis1UUk9VMy7ury4
+sa61w8m/x1JHR0A9OTlCT09V38S9vru3tba9wcLH8k9bXkI4ODUwODQrME1IO1DMu7q6ta+v
+srvHw8hYQEFBOzc3PEpPU/fGvby7trO3vL6/yH5TVVpKPDs4MjQ6Lys6TT4+88K5uLixrayy
+vr++00xAPzw3MzY7P0ZR38q/urm3tre7vsHK61loWUZBPzs0Mjw3LTRIQj5gzr64t7WvrbG7
+wL7RT0E+Ozg1NDtAR1zlz762tra0s7a9xszmW1FLSkY/PTw1Mz84LTVGQUfvzr+4trSvrrK/
+w8ToTD89PTg1OEJMUl7Rvrq5t7W0tr7IzN1aS0tIREZEQUU8ND5DMDA/Q0Vf9M+7tre1r7C8
+wsLXYVJDPT49Pj9BTPbbzr+4tre4ubzH1/dXSElEPklMSERIRD5EPDZASz9FaubLvr+7tLS5
+vr/H31tKQ0JDPzxHZWNl0b+6uLq7ub3N4H5WRUdHQktMR05YTUZOVUU6PkRGVEtaycLMwrq4
+uL3Dx8vzS0hPQz1IT2Lp2M3AvL7Cx8XNa19STUs/PkhJTE1p72r1y9tL6tBSSlpNT21PSeDN
+dXPMydvd6PjT11h1z9323NPVzcDj3r3sV+pcUmBNSlBlUkly319c1O3Z5Odq4N5HUGBQTEtG
+TVpeX3Lb2Pzg3+HM3tzY1tXd0tHK3M7Y2dxoe2Z8eEx3TFZqTuPtZ9lc1ON63mfzUttIYeZH
+akzpT2RjV9x5Z1rv31/v3OfN6+rfz9hfz+TbZe1h4t1dZuZyenRk2m3a3OXg42xtZ2RdTXVd
+V+lTflrbUOhl/m74Yept1m56ylXfb/lZeHxr6Oxg01/Ueud33d/r/eHZ6+bv2WDiWF5q9FT6
+Xfls+mH5d2XaTdNP1VLUUM5V4Opb2vNqZfRjfl/2WuZZ6ubr1vTccs1o2WvYdnvSVNRVdE7V
+VeZU017NYuTs+OFLyErOWeBvbnFda/RU+kxuZHFVen5g5eji79zt5NHhW9Bi1k/ebWXoXnx1
+2/l31fzZae7vcu9rdmriXmhX3/xndvdV3E7aU9NM9k/WVvveaNxm8fnvXN1u3917Z9td5+5t
+ZNz7b9zq6mjaXOzm8k/ne0veZm5ZyEzW7nniX8xO0uhU3WhsY2h4WepTYOla33Ju8+fwZ+Nn
+7+LsX+7e/WvY41jNaVnQX1TXaV7z5GXy0Vzy215rd95Pd/Na4OtZ8d5aUdp4SdvjT+rNVFzI
+7F/b69Vr/tpPy1tKdsxJ79NSY8pnWNXJT17IaUzWcV5X2Vhf79ZsScL4UctmYcxObfXj52FY
+2NFV6mZny0p2yEjRbWLcbmX+1Vpa+dz7V9boUNveZlPf1lxoaNvv83R14+9gb+Lfa3rY7ONR
+2W1daN5bWNfdSeXTWerO81LY2E/n0VtB2NFLXeTSWerbcWbc81/p22pX3O5PyF9R089bTNry
+b+Jcd9f2W3DaZ2N6Z2t+eupZbOPrXlv/4uR54dF2a9ZobdRuWdfWUGfq51tc32dwbOHhcuht
+9WlufGZ4Zuh5Zu962e1SZNnbTPLPelbe31znfF597mFlWNn4X+zs52DW+HLn0vhl2N1h7mNi
+0d9RatVnWXxkdmN5elpWb9p2XeHf62rybWba3Fx8395q791g2//kZ+DiWGrqam3wWFnV9l39
+e25i0OZl9uHqfHdt8/Bub1Dv72zx9HjV32ps2uVfbv9nY1145F9o2+ZX9N/o3Ol8997scPR+
+7GRr3O5wZ23zYlZ95vRfYNvcXH3w3dxsY2/3VnLkbP3oeGLu1/5f4fV81e777np34/VebXV2
+dmpt9vXsZ1n34l5q3OdxXej4a1lY7N3vaeTteuXz3ev61vf27O5vWvx8b+lZ+eRtXefaeO1j
+VOzkZl5Wan3qXXjnZ+r052lk5dj27v/28W9y+9DoePDpd+XZXmzrfF5reVpkYvjiXV5q4flp
+9393bGHl7e9979nr4P128Pjv4WZs22pN9NFvWfrkXF7z82fm8Wvk6Hnh63breF/v3HNdcevi
+62tca9nhZV72cVlvb2lqcm/v+Xno6/Xw6OX6aXj27ft19t/teens9n7xcWJoZGD772Ve8Nvm
+93zr9Hpua3tjd2pedXRvePni321r7OXn7f5jeff35erd1tPc5eT07HdmWk5LTE9MS09XZWtq
+bvDb19jWz83T3djV2+zs29nc39fT2d3Y2er7aVJCPT8/PDg6RlNZXuDIv76/vr7E1OflaVJM
+TE9OUlv/2dTSzMjLzM3O0djc2d3d3+Tj7Fc9OTw9Ny4vP1ZMTNq6tr27srG80NjTbkM6PD49
+PURa3tHMvri3uby8v8jY/WhhXFhRRj02Nzs2LzE8Tk5O2Lm0t7iwrrbHz872Qzk5ODQ1Oz9L
+68jBu7Svsba3ucPZdGxfT0pNQzw4NDo5Ly43REhEZL23vLuvrLO/vr7RTkBDPjU0PUFCUtnJ
+wrq2tbe4usLO1ehYTk5SSj8/PTY6PjAvOUY/Pl/DvcC7sa6zubi5yflZVEU6OT9BPERy3d7Q
+v7m7vr68wc/d5fNeV2ZaSUVBPD07LzQ9PDc9WOXb0b2zsbazsLfGzs/sTkNBRkA7Qk9MUufO
+ysi/urzCwMHK2ePnclRLTks+OD4+MTU7Ojk9T+bc0b20tbi3tbjAxs3lX1RMQ0BFRklMU2/f
+1MvDv76+w8fGzdzwb1lPSkI/Ojw3NDo4Oj9JWHvayMC/vbq4vL/Cx8/adl5kVlRYWFtaYuze
+3NfPysvNzc/Y4PP6Y01MR0A/PDs7PD1CSU5f99nNyMK/v8DDxcrS2ODyfGdlaF1aaefg493W
+1NHQ1dDQ4ODe+2hlV01DQkA7PT49QUFNU2P23NHKxMLAw8nLytDR2t7ofn118fDq5+bn7/nz
+5eDe5uPj73Z1bl5cVU1MSUFEQENDSldXXGni29DPycfJyszV2trc4O3e4+Xj4eHs5uDi5+jq
+7+7k3N3j5Ov4Zl5TTExEP0I/QkJITVNf/+Pg0c3JysvKys7O1Nzc49nf29nc8W5vdv957njy
+5uXg4tje5HVmXlJOSkRDQkFCQklLVF1+3tfQycjKyszO0tLX3NzX1+Pg3ujs6+71+ezl63/s
+3+bh735iXl5TTExFPkNCQ0JGUVhj9trYzsrGyMfIzNDY2d7j4d/o5t7m7ezvenru4uz13+fl
+6ufufm5mW0tMRz9BP0E/RU1NV2ri2MzJxcrIxsrNzM3W2dXT5Ort+nf+9HV08unv7uro5OTj
+7nt2WktHST8/QD9AP0lLVGXd08zJx8fJxcjL083P2Nna4PXi6/L58/P+8PD6b+rf4OPh5/z9
+WllRQUM/PD09REJIVGBu29LMzMPAw8PFyNPV293n4+H2+/nx+/Ls6ujf2d3d1dnY3u9qV1RE
+QT47OTo9PkNLWmrw1szKxsLGxsXEys3L0dfZ1+Py7fD6fO3p5+708ebl2dnk/GNoUUM+Pjk5
+Oz4/Q1JgfObVzcfFwMbFwsfLzs3V2Nfa5u/n7H578vbv8d/f4trc3+5oZVtHQj87OTk8PEBM
+W2/m0M3MyMPFyMTGy87N1ejn3/Lv6Or19+Dg39zVz9DO0eRvWl9JPT47NzY4PDxCWXDp1cnH
+xsHAxsXEyMzP1ODt3+D8/uro+e7d2drV0tfQ2Nv8U1ZMPj07Nzc3Pj1CT2D108nGxsC+wsDC
+x87V1Nvr7/pkZGn75ebUzs3IyMjKz99sVEk9ODgxMTI2Oz1LXujSxL+9vLu9vb7CyNDW7n5n
+X1xXYWd77NjOy8jEw8fT3WtTRDo5MzAyMzc4P1Ft2sm/vLq3uLy7vsPL1uZoXV1SS1FbYGnj
+zsvHwb7By9LdX0Q7OzUwLzE0NjxKWefKv727tra6ubq/ytHZb1dVTkdLVmNp6MvOx729v8jT
+2ls9OzkxLi8zMjQ+TVrhx7y6uLG0t7e7v8rW51lOTUdCR1BcX9nIyMC6vMLO0uFCOjoyLS0w
+NDM8U3Tmyr26u7azt7i3u8fX4GJJSUpCRU9aYefNxb69vMXO1GtEOjgvLi0vNDU+UvbVw7m2
+tbKztbm8wM3be11NSUlIS1Vh6dLMwb29w9bfbT81NjEtLS81NzxU4djGubW1srGztru+zOBv
+U0lCREVKWGnax8K9ubq+z+zgQC8zMCspLDM3OEzOxcC0r7Cxs7K3wsbOeU9LST8+Q1RgaMy9
+vrq1t73ZdPo8LC8uKCgqMzo7U8S9vLGrrK6wrrLG1t5aPjs/PTo+XOt8xbe4tLO1ut1RVjgn
+KismJSkzPT9furGyramorLSytdVPUEk4Mjg9Oz59zci4sK2tr7K65kdEMiUlJyQkJzA+Tt61
+qquqp6Wqtrm89T05OS8rLjc9RuK/ta6sqaisr7fYPj0vHyAkIiEnM0VrzbKmpaemoqa2v8F0
+NSwvLSkqMkBM6buuqammpaiuus1BLy0iHB4hISMtQ93AsaWfoKOko6vFdFQ5KSUoKSgrOFjZ
+v6+noqWnpaawxuM+KyYiHBwfIyQuSc+3rKSenp+kpqrCSTo0KR8hJigqMl+/tqykn6Ckpaar
+wmE+KyIhHBodIiUqOsyvqqWem52hpqm0TzEuKiIeISotL0TBr6ynoJ6fpaeqtPg7LiUfHhsZ
+HSMoLEa6q6egm5qdoqevw0UsJyciHiArMjpZuaqmo5+dn6arrrlkNSslHx0cGRwjKy5Guaik
+oZyanaOqsshFLSckIiAhJjBF8L+so6Cgn56jq7W80jwqJSEeHBocISkuPMmtpqOem5ygpqu8
+XzwtJiIiIyMoN2fLvKuhn6Khn6SvvMDiOSkkIB4dGxwiKzE+y6ykop+cnJ+psbvpPCslIyIk
+JCg1Ycu8q6CfoaKgo627wc5FKyYkHhwcHB4jLDpbu6qin56dnaGqssNQNyoiHyEiIik+1r+z
+pp6eoKCfpLDAyXQ3KiYhHhwcHB4kLT16uqmgnp2cnaCqtcVPMCkiHx4fJyw4erWqpZ+cnJ+j
+p62+a0I3KyMfHB0dHR4jMEJfwaugnp+dnJ6ns8H8NSckIiAfISk6YsqwpJ6enp6fprDA2Uw0
+KyYlIBweIiMiJz3mzryqn56fn56hrL7YTzMmISIiICQuQvG/rKKfn56eo6y4xn49LywoJSAd
+HyYmIypNyMS7qJ6epKSfo7LXYk4xJCEjJCImNE/Suqujn5+foKatu9VSOC4sKCYjISMmKSow
+RtS/t6ukoqSlpqm1y15GNCklJScnKS5Fx7ivp5+foqanrL19SkE2LisrLComKS4vLzZR1c7A
+sKmoqquqrLjNbEw5LioqKystOFHNurCqpaOmq66zxV5IQjwzLzM4NiwrOD8yLD3Q1lLesKmu
+ubGqrsTn2uo9LSsvMy0tPNG/xLaopKiuray22lRiWz00PEM3LisvNy8qL0JmVVq/rq2xsqup
+ssfa2lk2LS0yMS0wRdzEv7Wqpqqur6+521lhZkc5PUI+OzIuOT8zLC9Pf0RGy7Gyvbisq7XO
+zsxfOS80OTYyPeDEx7+xq6yxs7G3y3ZtbEo+Pz9APDAyOTsvLDtJPj5ZzLy8vLOsrre9vsVs
+RD5APDU1Q2ds7MC0sLK1s7G4w87T5VRDQEE9ODM1NTQvLTM5Pj9D0Lm6vLStq6+8vbvPTEA/
+PTk2O1J278u6s7O0srK3wdPZ+E1AP0A9Nzc4LzdHLi5HTUVRXtO3vs64rra8v8TBz0xIXEs+
+QExs3d7Mube7u7u7w9l+cFNAP0RHQkZHUFlEPkRBPEE7OkdQUGTZyLy7u7WxusLIzuVUSklO
+TEhS483Nx8C8v8nIztz+UEtJR0NHU1ROavpm3tt5VmdMRD83Oj4+PEhh7NLJvre2uLi4u8fR
+4WpUSElKTVFm6dnIycTCy9D29lNWWUhGV01HU1dOZGpV8+ru997k+fdhaFpYSVFWTU9UXG7j
+8NfQ0MrMysvGzsrQ3M3g9n98bF1wZ3T1Y3Jq+lxpX29sZW1veWprYnBebVxp8Fv1ZG5fZFpa
+eVxrZHZyf+nm7Pdy9d/86N3W09/T4tPc6djv9flr/ul37ubo/erue9p27+9naVhmUl1TWXVV
+Z1tqWXV1dmBvXvtmYGf+eO7q6evj++Tf6O/s5Ofp7PHn3eLV1NrX1trc4ux7ZWJcU1JWXFxd
+Z2997H1x6XtkXmdhXGJfXmZgXWt+ffby5+Tj6/Ln+fB+7tzg4OLZ293c4drg4+fg6fXvc2ls
+a15fW1daWWNcaWp3am14anZsb29gYGVub3Tv5enf3eHa29zh3t3k3+x9am5tbWprbnDs5+/t
+9f7vbGtubW5ncn5+fPPq7ffsev32YmJiZF9kbnls9ebx5d/i7t/e5/JvZWxrW2zvcGVre3Xz
+9/365+fs5N3m7erd4+Hq53j2aGZjW1paWmN5eG54dXns4t3g6+vzb2BoX2FkeW5zZHHv9fT8
+8erp4+3x6uru9e5n/G3/9+Z15e/vbH384ujtbX1u6mn2/u5UYF1pYF9ifnhhanD1bGJsdd9/
+5PXl8er33drg7ejvdvXvbfP8+XxseHL6YvR89uH38vpqYlteXVhmWflr9e3Z2uvU5+hz63H1
+aH/seWdeeGFwYGVfX2hp8ffr8eXf3ejq7uze7X1w+m1rc/b++fF1bf13XWJfY3Xv+/To8+3q
+6+327XRxZ2FnZG1rd3z48fjn3d7f5/J6a2pqcGlvbWZoYGRdYm5sc+7m6uzt7Ori3dzf3+rt
+6uxxcWtfX2RnaG1zcPz07Ovn/XZ1fXh1fP3++3VwamlqbGZjaPd9bGdx+/vn6+Tk6eXyeHt4
+enp5ffXy+uz3+/Du8Ozv9f3+cWxsampnam9oZGh1cXl+fnv//vP7/fHm3t7g5+78cHL3d3dz
+dGtra3j7dHl8+fz/fnr9dG90+vp8d3f+eP339PX/9355d/nq6+ri6OXg4ub5d29maGtnZGZl
+YWdpd3r/8PD3+Ovq6+78e3n39H9zbXZ8+vr0+3v5+ff8+e7o8Pfu6ej+b3NraXz1enH9+Hx1
+/u/z+X59enRveHx7+u709/52eHV0evv2d3Bwb2939flxcv317uXk6eXq9vr5f/r4e3j97+vr
+WXnUa1555ml5e3rxdHJvZmRmb/H1+nVzYlRET727ztnQ29bbWE9MSFVxSz0+X9DL0M3Dw8bP
+zNH4U01PUE1JRUXVzXzj2eXczd/x8Vtbfuzpa1pg9t/p+Vpl7fb9b3Ts3+v883RnePd89+5t
+b+/z4tHLztXU1tbpU0Y/PDw7PEBHVOHJvru7vcDByNxTQ0FFR0pMVejKwL6+v76+wMnXdWn/
+cWBLPTcvKi00MTRAWcizrayrq660wGE8NDAuMTM5SHnHt7Ozsba8w9pURkA/RlBy08S8t7W0
+vv5CNCcfICYmKz37taaioqGkq7bZOSomJCUpMUHctKqnpqiss8NYOjIvLzQ/Xcq6tK6rrrna
+SjYrIx0fJyw2X7uqoZ+ipau57zkqJSQmKTFGzrGopqiprLXIUjs0MTE0PV7Ju7Wyr7G4v9pL
+OC0qJh8jLzI94b6upqeqqay73UczLi0rLTdG7b20sa6tsLfGa05FPDw+RWHYxr68u7u+xcbX
+U0tCLikkIywwLjnZuK6srKimrLnQXD8zLCstM0Zk37yvrK2wtLfC+0xGR0M/R1zhzMzGvb7N
+Z1FfWT0uKi01MSwvRd/Kxryvq622u7/I4kw+Oz9DRFTaxb64s7S5v8/E+UVcOTlHRzhGcm9a
+VcLwP7/POd7OS1PpVWzZWXnhbXJfefhseXl6bXV9/+7mzM7Uzs7L0NnX4GtobFhcaVVUc3tu
+5fbs2dze83D2f1lTUlhqX1pldOzp7v3z4OtubGFiaFxYXF52++7X1dbZ39/nefjudXP9am1w
+fXRy3trW09jd3t73aGNpY1lcWl54/m927erj9Wl1Y1lbT1NpWmN5dufY1tXd4tvk8vZraGNa
+aG525enf0tXV1tfX6n5rZm5hWVtdafxsZ2xveGdcZmdfbHD76+vn6+Xf4+5sZHBqX2pmcu3u
+5t/b2+To72xoZl1pdmtx9/Pk3O3q5+bscWlkXVdXWVxz493b0s3R1+V3XltVS0lKUVZc/Ofd
+18/R2NbefGhqZlxdYGZ09ujn39zk3trc6vl0eWxfX1tbX2dqfe/g2Nrf5/BqWlNSVFlaXXP3
+9+Hc3d3a2d7u/nJiZmZiZ2Vu9fXw6efo7Xxxbm5xbXd2d/js49/a3N7ub3NsaGhoZ2BdYGVl
+bfn17/B7eX51enl8fG1tdHJw7uHg3uDk9Xz+eXV//vvz9fx4eG9qb3FzcHZ8dHNrbnFz/3Z+
++fvr397i4+1xa2RqZmNqZGpudfXz+Hz473pqaHNvb33x8O7t6ebr5env/Pv+e3RnZ2Joe3X8
+9Pjs5ez8dXT9bmpvamVmZmNmbfTq6+rs7O/5/v7zfv7+fXZ29XdtaGx0fu/p6Pj58Xl7effv
+7uXo7/PzeXJrZ2dmamVbYGplYWhvcHvy6ert59/e4uLveXrw6vvw7O7n4/F0/nhoaG9rY2xu
+bGdnb21qaWNXUM/SP7/d+/nScvrdWehnaVX3WuxT3Xrf71x14cnudNzY1mbe0V59Vn1420db
+4U5SVN/+a1Fl2HJt9tjk8G3Z1PpaWGXOeXxN/c1v2du/9vr363NOS1NzTURc9uXl9snE7O14
+0/VTZNfyWVp18etKetx/bVfY9+JP1tTve/Hf5179e/1+ZF1tZfzxYN/p/d1j217cT2vdTmdU
+0VB3aOHnfezZzl3Z9Nr16tdu2VPTVtVtYXhp5EvWTd9SX+xq3lzh7fH6aefk5ln+cn35bOln
+zlrp72/kXuHpfGJ45OzwZeLy32TefN/6WNdf3VTUeP7dXMx8a1hrbVReW3NTV2H1eWRo2tZW
+2d7P39/l3slZ1tDX2Nnb28pd3815+el95dJEN21IMjg5OkA7OPnNYNa8uLa6uq+yxMq/yX5P
+SmpfRk3b0uPRyr+/03tHSkg1LCwyLi8vOm/v8sSxrrCxsrK1yuTVcElAR1BeXFvPzM3MzsjL
+2vXc42xsTUxUT0M/Q0JBPT1DT1Zc+dTIxsXIxMTKzNbdfe7sfPdydu/Z3dvf29vZ1dnX3t/m
+6uByYFdUSUZCPz9AQkJJVWDw287MyMfIy9DT2+VtZlpYXGV5eN/Z1dDOyczKy8zLzc7Y3/hi
+V09IRD47Pj9AQ0ZJUF1r29XS1Nbc6uXv8Ofd1tfW1szLzMjHxcjLztrf4mtnXVpVUlZOTUtL
+T0lISEpNTlBQXWz9f+nU0NPb1NHW1dXU2NjW0tLa4eng4vF79O/+b2dsdW9memplZVhdXltT
+UVFTWllZX2hqfvXt5fL8+m1v9eXj3trY2dnW09LY2+Hn7vtwamdq8PPp4t/d4OHv9nBcT0lF
+RENFRkdMVl1y6N3V19fW1NXV2dzc4t/e29vf3d3j7N7b3N3e3uTr4t3n9WRWVFJJPz9BQD9E
+SE1cX27ezczNx8jM0Nrb3Ot/4t3u9+fj6Ozg1dbPysrLxsrX1O5OR0c9MTI0MTA1PUZZ6cq6
+tri3tLi+v8PgW09JRkVFQ01de9DEvLm2s7W4ur/bRzw4LCUmKScnLjxKasq3rqysqKiutba/
+fEpAOTUxMTg+Ql/GurWuq6yrr7rNTjsxJyAjIx8jLjY/1betqaajoaWrrrTTTUQ3LSssLC83
+ROrCtq2pqKapsLfOOzcuHh4jHhwoMTJburGooqGenqWqrL9TSjcqKCkoKjA7V8y5ramopKKr
+trV1LDArGhshHRsrNTrGsaqgnp6bnqiqrupBOiwmJSUmKTBA8MKupqWkn6Kts7pCLC0kGBse
+GhwqND+/raaenZyanqaos10+MyYhIiEkKTJJ0Liqo6OfnqKqsL5AKikhFxgdGhooOD6/qKKd
+mpqanaWqu0o3LCIfHx8jKzZYwa6kn56dnaCns85GKB4gGRIZHBkfPVHKpZ6cmJiZm6SvvEss
+JyIdHSAkKj3Vt6mfnZydnZ+puOk4JRwbFxEXGxsgP9S7oZqZlpaanKS66zomHx4cHCAoMmK5
+q6Kcm5ucnaSuxEQrHxkYFhEXHR4p4barnJeXlpedoq1eMyoeGhscHSUzULqon5yZmZudoam/
+QTMiGRgXExMcHyRCtamfl5WWl5uirN0uJx8YGR0cIjprvqadnJmYm56iq7w9LCQZFRcVEhoj
+J0CxpZ6XlZWXm6Sv8SsgHhkWHCIkN8KupZyZmZibnqay0jcmHhgVFRQVHSUt3amim5SUlpid
+rMBCIR0bFRceISl+s6mdmJiXmJ2jq79CKx8aFhUUFBsiKkqtoZyVk5SXm6e+TCYaGxgTGSQp
+OrOmn5mWl5iboqq5PykiGRQWFRIaJSpJqp+clJGUl5upxkYlGhoXFRojK0mxp56YmJeYnKSs
+vTsoHxgVFRMUHSQr46eempOSlZedrdQ5HxgaFxQcKzBkqqGcmJiYmp+or9QvJB0XFRYVFh8q
+OL6inJeTk5ebpcRALBoXGxgZJDdLtqSfmpmbnJ2otL5HKh8ZGRkUFR8pLf+onpmVlZaYor5P
+Mh0XGhoaIjNctqegm5mdnp+puc5PLiAaGhwYFh0sNlKvn5qXl5iZn7s/LyEXGBwcJjh4tKOf
+nZqdoKStvt5LLyIbGx0bGB0sPOizopqXmZucorc+Kh8ZGhsdKkfHraCenJufpam01Ew6LiMb
+Gh0eHB8sR7yro52YmJufq8E9KR0YGx8jMFu+qZ6enp2hqa7CWUg9LiUeHB0fICMrPMiroZ6c
+m5ufq8w3KR8bHSAmOsy2qaCgn56lrbfOU0s/MiojHh4fICUrNFK3p6CdnJ6hp7ddMiEcHyQq
+OmXGraSioaGnrrfOW1NOOjEsJCEhICMqMDdfu62knp6fo6u9azopIyMkKjpfxK6qqaenq7C8
+4E9GPjw+PTgzLywtLzE2P05+ybuzrqurrrfD6VBHQ0RMWG73eHb56H9iVUxUX2fnz8O6tra5
+v9NZPzQvLSssLzdDac+/ura0s7a7xM7eZFZPRz8/Pz9FWevNv7u3s7Kzs7fE7kk7MSwqKisu
+Nj5R4se8ubi2uLzE1HFXTUxPUE9PUVzu1MnAvb29vLy/yt5kT0Q+PDo7Pz9DSlVv+nvu7vn1
++311cWRdYGnv3tfV0cvIw8DAwcLI1eRpVUtFR0ZLXvHa1tve625eUUhCQUFDR0dKUVxv3szI
+xcC/wMDAwMPK4WVQRj8+QUVNYeXOxcK/wMjO2O9ZS0ZCPz9BRENDSVBectrLxL++vb3DzeFd
+SkM/P0JLXubPyMK/v8HGzNv7Y1tWVldVUExJSEdGR01RWWnj1dHOzs7afV9UUlBb/uXYz8nH
+xsbJzdbn8WxdZvfh3tze4e1iU0lDPz9CREdMT1pqcW157Ofq4tTNycfGxcjP2Nvld3b47+3q
+3dzW0Nja2Nz4ZE0/PDo6OTo8PkZNXN/Iv728vr/ByNHY33dgXWFsZnHr7fzNwsjMyMjO2d7c
+bEE2MS8vMDI2OT9N2761r66wtrvBydh+XE1HR0xWa9zX3ufn0sPH82rZ0d/0bllKQD09PTw7
+Oz5FWeLNxsG/v8PIycrO4nd87+Xf7G9eYGVy6uTe3NvSzsnEw8rpVkpBOjc3NjU0OD9S8tbK
+wL7FysC4ucrb0MnM2vxeTUlR/Nvi7drKv7m5vsb+PDM1MiwpKCotOWLBubexraupq6++8E5E
+QD05NTQ6SffJvbm1sq6tr7O8yGw+LygjICEhIygwRsuxqaOhoKKlrLfaQzErKSgqLTVF3L2v
+qqakpaessrvG90MyJx4dHB0fICUvT7qnn52cnZ6iq79LLiUgICIlKzhpuqymoZ+goqatutJf
+SzsvKSEdHh4fIiUtSb2roJ6dnZ2gqbxGLygiHx8hKTdaw7CqpKCfoaatt8XqUEM7My8qIyIi
+ISMmKznUtqijoZ6eoKey9zwwKSQjIigvPFzCsqmjoqOlqKyzw+9RRj41LykiISEhIiUrO9S5
+rKahnp2fqLLRSzkrJSQjJy41Rsu2qqOjo6Kkqa/C8FhFOjItKCMhISAjJSs8a8Svp6GenqGl
+rbzZRTErJyUkKC42U8azqaSjoKKnrLjM9k0/ODEtKSckJScoKCwzS8W0rKekoqGlqrLC7EIx
+KiYlKSotPGHEr6qmoqKkp667x/RANzAuLisoJiYpLC0xOlPOuK+qpqOkp6y1v9xINSwoKSwt
+MDxfwbGsp6Snq665yfs/OjUvLzI0ODk5PT9ARUx6zsS+u7u5u8DOdlVMR0RDRUpd69rLwb25
+ub6/w8zQ6lpPTElIR0ZMT1FUT1vm6Xbq3drX3PZiVEtGQz8/RUxVXnrcz8rHxsXCv76+v8HI
+zdnoelxPTEtNUFdmeejc3t3h82paTEVAQENAP0JHT2H77OLX0s7OzMjGw8PExMfM0t/1+mZY
+VE1PWF1p9NvLx8fHztjlXk9IPz49Ozw+QEdMWGzv3NbPzc3KysrJy87T3+/o6f55dHb36uLg
+2dTSzcvN1Nzqb2FXTklGRUNAP0FFSU5VXGb+6t3W09LS193b2tXU2tzY1dLQ0tPQ0tTX4fPw
+9v9xY2ZtbmdnYV5iW09KRkVGR0ZJTlheYW177t7Y1tra08/Q0c/O0dHPz8/Q09TY3+zvdWJf
+ZG/75+15a2FcWFBOT1NPS0pLSU1UW2Ft//Ls59/a3N3d3NjZ29bV1dXV19vf4dze6eDe3+bj
+3uP6aGJZVldVV1paYGhmYF5cWllXV1FOUlZdYm/26+Pe2NTW1dLS1dLOz9LT09TY2+f6b19a
+VVRVV1xha2959ujo6ubsb2ZgWVNSU09PVlxhbPPp39nY19bW2t7q+21nZ2FfbPDn4ebr3t7c
+19jb293k+XdsamZgX11dW1teYmRlbndydnxza2tyeW5saW54bGlqeXx69/Dn39nb2dnc3uPu
+fP/o5+np7vp5cm1sWkJWuFk9wslN9c5q22ZIWltGX09F4F3j3HrbztvO3dfbW9pNbWl65Fje
+V+PFU9PL7MfLy/W/vWrOzk0+RDswMzAuNz49U9HMurWysa6xu7vI9nhLOj09ODxOTXO/072y
+s7W1usTF0EczMi4tKCMoLzc7QOW5r66tqaeorrzG0m1ANTEzNTQ2QG3Pwriyr6ytsrO4w9NU
+Ny0pJyMhISUqMz5mwK6mpaSio6aruM1lPTIuKSctLzVF78OzrKqpp6itsbnQWD8uJiMhHx4f
+ISo4TdKwpqCenZ+hpa2+Xj0vJyUkICUuOE3FtaujoqKgo6iuutpCOC0iHR0cHB4fJTNfw66l
+n5yanJ+kq7lmNCkjISEfISs5X7+vqJ+dnZ+ip6253kAyKiEcGxsbHB8kL07BrqSem5qbnaOr
+uPs3KSMgHx4hKDRXw66ln52dn6GnrrvaRDAoIBwbGxscHyYuT7+upJ2ampqdo6u3WTAoIh8e
+HiAoNlzArqSenJyeoKevvP0+LygeGhoaGhweJTF5t6qhnJmYmZ2lrrtMLiUfHh4eIis+1rWp
+op6dnZ6kq7XFVjYpHxsaGhkaHiMtTb2soZyamJmcoqy+TzAmHx0dHyMrOmy7q6KenZ2eoaiw
+vvk9LSMcGhoaGx0gKT3HraKdmpmZm6CsvU0vJyAeHR4iKjvpuaqinpycnaKqsL53PSwgGxkZ
+GRsdIS1NvKifm5mZmp2jsN08KyMfHx8gJzFOwa+oop+enqCmrrrPUjYpHxsZGhsdHycz+a+j
+nZubm5yfqsBGLychHyAiJS0+2bSppKGfn6GkqrbKaD4vJR0aGhwdISYuSrqon5ybnJ6gqLhb
+MygjICEjKCw3bruspaGhoaGkqrLC7kY0KB4bGxsdICYtQcCqn5ybnJ6jqrh9MycjICIlKS88
+6birpaGho6SorLTFc0QzKR8cHB0eIigvRMGqoJ2cnZ+lrr9YMiciICInLTdNy7WqpKOkpKer
+r7nOYUMyKCAdHR8iJyw1UbuqoZ6en6OptNVCLiQhISQqM0flvq+ppKKkpqqvt8XcVz0vJh4e
+HyInLjVBzq+ln56gpqy1zkozJyEgIik3Udi6rqmlo6WqrrbE1XZIPDUrIyIlJys2PkrOtauj
+oKSqsL3yRzgqJCMmKjl1xbatqqelqK2zvdJtXko7NzEqJyssLjlET9K5sKunqK+1wWY/OS8p
+KCouO23GuK+srKqrsLnGdE1NTU1HPjo1MjU6Ozw/Rl/KurGtrrS7w9pYQTUuLS81QGDPwLq0
+sbGwtL3PbEtEQ0tZ6+hhXFNGQEQ+OTo8P1vLvbi1ub/Cx9xhSjw2Oj9KadzVzMS/vbq9xM3d
+c2RgVUpJTV/Pv8LXa0k7Oz48Ojo6O0rgxry4ur6+v8G/yHJKQT0+SE9SV2bpz8K+vb/H1N/m
+cVxUTUhHTVr2z8bQblNDOz0+PDw/RFTQvbWvrrO4vcrlW0E2MzQ2PEpe38e+u7i4u77F0v9X
+SD07Ozo8P0FGVu7Ov7m3t7e8zvRVQDo2NDM4P0/fxLy5tbW4u8DO/k9FPz08PT9CSFj92crD
+wsLDwb69wdJhRDg1NTU3OjxDYc+9s66vs7e/z+tXRj47Ojk9Qkpb9+DPyMjIy9Pe4uPn8WhZ
+VlhbZ+7j5ujo3dbNys3Zd1xVU1RTU05NUFljZmZiW1hWV1tn9uTe4OXaz87O0Nrj5uXh3t/e
+2MzGxsTFy9R2STo0MTAxNjtFXOLLvbe1tLe9yNpqT0U/PTw+QUhWedjNx8PAv8HEyM7a6mdW
+T01LTFNcZvTc0czIytf1VEI7ODY3O0FKXd/Mv7m1tba5wM7oXE5KR0VFREVKVHXazMbCwsXL
+z9PZ4uzzb2NXRz04Njc6PkROZdzLwLq2tLS4vcfYaVBGPzw5ODk6P072zL64tbO1uLvAydT5
+VElEQD9CQT89Ojk5PEFMX97Lwr25trW3ur/M311LREA+PT5CR1Fz1cjCvr2+wcbL0d3qfmdf
+XVxeWk9MRz46OTk7P0dTfNXGvLe1tLS3vMbbXEg+OTc3ODk/SVbhx7u2tLO3ur7EzNt6XlVO
+TExLSUhBPDc1Njg+Rk5r08O6sq+ur7W/02RLPzk0MjIzOUVsybu1sbCwsra6xNdjSUE+PkFN
+Xmp+bVNDPj06OTo7PEhoz7uzsrKzt7y/yOZLPDYwMDY9R2DZx7y1sK+wtr3I4GdVTEZBQ0xl
+3dryTzkyNDMzNjY3QXjCsKqrra2vtLnGTDEtKiouNDlEc8azqqeprLS7v8rrSzw3OUFMU195
+aFVFOjc0Ly8xND/6wratq6qpqq631UE3LyorLC01SN27rqurqaqtsL3iTT45OTo5P01e+dno
+Pi82NS8zNDA86sK0q6ysqKuzvOA+NzUsLC8xP9jBt66sq6irtL7XT0Q+NjQ1NDVH7+nvVDtG
+ZEA9Pjg7Vm94xb67r7K6u77L1/5EPUA8QlNg0r+9vLq5u7zE32hOREVBPT9ISlvi7mJv1d9M
+Pj09PTw2O1BZfMnKxbe2uba7w8DKZWJfWHFcT1zf0NDP3dnN1vNrWFNZUklMT1BgZ2Ju7uvu
+2k88Qz8+QTw+T+7u0MfDt7a8vsLGzNtfU1xaZ2Zabt3Pzs7P18/O5GlcWFVPSERMSUpPVltX
+WV5senno3Njk9e7o3t7ucF1iY2BfXnjyfPX45tLPzszO19PM1drV3nxpb2BcV1dYW1ZVWVxf
+bG9udvDi8f729HdkYWJnaFxUV2R88Ozw6tfR19rc6d/l/334fX5yd3d45+Lp9ejn83JrZl5e
+YW53fvP5d3F86uff5vX8Y2hrYl5jZ3zn7eTm6v1xa2dfXVxhX2Pr6O3f3dza3+zo7vJvZmNp
+cXr09O7n2NHS1OP7bWdqV1hXVVtjZG7w/fD0+/twa2VuaWpfa/z45vr47+jv7eTmeXVvbnz9
++ufn/uTd1tvm+e35cGxlXFhdZ3xt/+bi7nJ9e29wbWx6/XV95env7Op9aF1YVFNZW2l5dn73
+6N/S0dXT1t3f5fN2bmFmZmdr+fDs5u71/WhhZGRiX19eYm98cv767+fs73t+cGhwcnBue+Xl
+4eHd3N7h6u1ybWxdXF1nbWxtc35z+O3p5vF+dGpkaG778Onn8vLu7e59dX58+Xtob/5v+vp5
+eHF4eXp6dm1qa3J4evr58v589PHr5Ono5enu7XZpbG58fHVtbGtrdHpyfPb2+nNmbffx7Obt
+fn5zbm1pbnNvcXv87uvw7uzt6ObyeXj8cG9vcm9t9Ons5+j57v1tbvXvfmZeX2ZtbW1panvn
+4/Lu7/z98vj/+/1++/T4+vb09PXv9fj8eHJsenx0enZwc29rbvbz9/Hu8vHo7vLr7vTz8XR+
+/GlpcGtpefltZGludPHr5+Xt8fDx7OXi6fn/cW39fW5wcXVxcff6b3zs6uzt/W1nZGNhXmFq
+cnP26Obx+O/p4t7e3+Tq6/l0cXB/9HJrbG52/nJoam96+ndtcvbt/nZ+8f76fnJx/Ozt9nx4
+/fd9d3V8//58dXnz9/v89/Hp6fn/+n559fB/b294e3d1dXd2e3l9/Xb793x4e/l9df7z/Hj5
+7/T2fnx4c3h6c29+fvzw7e79fnRz/fN4an779u7u/Xh2/ezt7u3v+3Jtbm9yf/R3bm93+P51
+bmxra3P9emtv+vPy6uZ3YdXZbmT36ujlZl302HNn4ttmX+VxX2ns725fV2FkUExyxs5LQ7mz
+RDq9r1o46LZ4MWO4dDVSvOU+Vr/KRGG/1EhfxflKXtZrRVzXYUhmz+lf/tjYcV7b0/1Y49Zq
+W2rT91ju2OFt7O7k/Hv5eXT7b2v+W1jrfFhs4+xmZuLhbfrtem/58+7t9+Xk8mdy6vF5c+Hl
+83ju5mtx8HtwaVlqdVVi9FxhZFdtblRk6mtb+9LgfNrM2t/KzNjY1tLa3dbX4erZ3NrT4UJd
+3C0xSC4qODkvQWXk4cawtLOqrbOvsr/M1PVRSlBHQ2Bsa9PC00XOzi00TSokMy8qN01fbL6v
+ubGnrbStsb7J1GpDSFA/ReD7ZsK8wLzBw+VJSC8qKyYkKi0vQVfKu7WrqqqprLC1vtdaTEk+
+PEZKR3XPyrqysry6vV5CQC8lJyUjJiwyOVfGvreqqaypqa2zs77qblg/PkVEQFLf2ci5usO2
+zk9UOi0pKyUkKS4wO/vPvbCrqqmmqq6utsza9Uk9Pj07Pkxj8ca8vr62xmLlRi4rLCUkKi4v
+OvnUwq+srKmnqa6ut8zaXkY9Pzo4QEVM5cXDuLi+utRkWzotLSsmKS0vNknfy7qvrayrqq60
+tb/bZlY+PUA9P0hPYNHGvre7vrzZXlA4Ly4rKSsuMTtR4cq5sa+trq+yubzI2O9aS0hDQkdL
+V3rYzMS9vcnBxFBKRjQuLiwsLzc8Ru7Nx7q0tbOvtru8ws3f6G5VVlFITllf59bNyL/C08XT
+TUw/NDIwLi4xNz1D8tjWwLy6uLa2uLu8wcrL331rVVJUT1ly5s3JwL7VxMlKUEk0Li8sKi0v
+NDtY1s69tLWzr7Cztri/zM7dXFlgSUhYW1j2y87EuczVu2o9TjkrLS4oKzA2PUvOx8KysLSu
+rbK1tr7M1t9aR01EPUhRU2XQy8e5u9zGvkY7SDQpLS8pLDg7Q+rFwravrq+vrrO7vMjleFtF
+QEI/Qk1gdtrGwru3wtnF2zs2PC4mKy8sLj1OZs28ta+urq2utLu/z35bR0BBPz5GVWnx28rB
+wcfAx9xoV0E5NzQyNDc7Q1H+zcG8ubW3ur3E1exuTkhJRkdNVV5359jR0s7Mzc3Oztbi6epu
+WVZUUFJSTEtNUVtcXF9iaWdkXllfbXzx6Obc09LQzs/P1dna3uPf2uL57Ofh6vDteXBrX2Be
+XmljVFBTTktLS0xSWF5y+/Dd19nY1NjZ2dva3OHq4tzc29ja19Tb3OH3eX5oXVdVU05OTUxN
+TE9YXGb35t/c2t3f3Nfb5ufv/u3c3dzb3+Dl6u91cnRoZWlmZml0bmJfXWJfVVlgXWT/7uvl
+39ja2tjg4t/m+H52a2d1cmptcXRtbXH06/916Or+/nduYmp4a2VmaWx88ung4ODe4N3d6Ptv
+ZGRoaWhka3BpaGlscW726O7p5Ofp7vp8c2tnZWVjaXh7fuvj7O/u7/X59H747n1zf/X28PJ7
+ePjp5/R8/PP3bGJiY2JfZmtsZW5vdfDs4uLq9f1ybG50amrz6urq39vc2dnh5+z9bGNjX2Je
+Wl5iYU9PzthLbsfcXXrT63Ne8v5kXV9zamZP58bV6tXN29ne5+p3WVtdY2ZlX2JjXWBhaHR2
+dXF8fnL55O5sa+fof+/g7vnr4dzc5ebm4Oj9bWhqYmVWSFXd1W9ba+Tb3t3rXVFWX3ztd/zi
+39/Qzs7P1NTec1JJQT5BREhPWW3f087Jx8vN0uZ2Z3r6aGVsam9/593VzMvGw8TEyMvaWD00
+LywtMTdCUf3Lvbaxr7K4wd1VRD8+Pj9ASU90zb+5uLu+xc3SzcrJy8/WdU46LisnJysuOlPU
+u6+rqaiqr7nTSDcvLi8yNjxGWtC7sKurrrK6wsvZ8HxqXlRDNCsmJCQoLzpXy7etp6Oho6my
+y0gyKykqLC4zPErovrCrqKqtsbvH1vBlY2VeU0AyKiUiIykvPXm/sKmin5+iqbTeOiwmJSYp
+LTA6S9u4raejpKitt8bZYVBPUVdOPjYrIx8jJCo9UMeyrKWfoKGkr8VHLicmJigsLjQ/acCt
+paCfo6qxv9xbRT05OkFNVko6MSwnJysuO2DTua+uqaaoqq6+30czLispKSwvPH6+rqejoaSp
+r7/1STw4NjY5P01q08xdQTcrKSwuN0lMac7AtKypqKmwwHFANC8xMjc+SfW+saqmp6qvvM5q
+Sj43Mi4uMTlK8c/CvsTlW0c2ODg0OTw7R+rQvbe6uLe8vL3G0/hSSktX8MrBwcHGycnL0+Jc
+R0I9PUBGV19y3tzQysTD2kk5ListLzM8P0Nn0b6vq6qrr7q+xdfb/0tDPz1GX+DOx8bLzMfF
+xMnabFFHREI+Pj5ATVd02dx6UUc/OTxIRkpOS1ThxbevsLW6vcDEzO1RRUFJWHDk187Iv728
+v8vkXk9JSEE7OTo9RmLZydxWRDs6PkNGRj5AVuPFt7a2tLe3trnC0WhNS0tNU1df6M/Evr2+
+xM7X81hEOzk3O0NTWUw/OjxCSlFNPzxEWc+9uru8vbiysLO8z2xTVFVXT0pNW97DvLu9ydbd
+blNHPjs9REpIPTUxNDtGTU1NXObIvbu8vb26tbK3wddeWV1WUEtKUOvGvr/Iz87P1d1dQTs8
+QEtUTj41MTE4PkRDQUdny7mxsrW3trOytLrJe1NNSkpKSlB21MjDx8rLztPmWEQ8OTs/Pzw3
+MjI0OkJKTV3eyr63tLW2trW1trrC0fheW1tPR0pZe9XJxsjLys/c+FtIQD89OC8tLC0xOT5D
+TFvQu7GurrG0tbW2u8hwTUpMTVVXVF32zsG9vr7Ey9Hc/lRJRkc+MS0qKSsvNDtFT9+/s66r
+q66xtbe8xelSQz49P0NETGfUxbu2tbe8wcvV/1tUSDctKSYmKS0wNTtM2LitqKeoq6+xs7nJ
+ZEE4NTc4Oj5DT+7HurOvsbS5vMDJ2HVNOS0oJSQlJyosMj77vK2npKSmqauvt8dvQTUvLi4x
+OEFU6Ma3sK2srK+0u8bS+E88LygiICIkJysuN1LFsKeioaKlqa2wu9BKNi0pKSotND1M2L6z
+rKinqayxt77L/0IzKiQhISEjJiovP+O7raejoqKlp6uxv+1CNC0qKiorLzdCbsGzq6moqaut
+sbnEe0M1KyYiISEiJSkvPvq9raejoKChpaqxv/VBNCwoJygqLTQ/ZcW0rKmoqKqsr7fB4Ec1
+LCgkIiEjJyowPm3BsKmloqKkp6uyv+BJNy4rKikqLTM+Wc+8sq2pqKiqrbS+2FQ/NCwnIyEi
+JSovOkzYva+ppaOkp6uwustvRzoyLSorLTI9T/bOvravrKqrrbG7yONVQTctJyUkJSktND9b
+0r2zrKmnp6mttL3PcUo8NS8tLS82P1Ply7+6s6+tra+0vMngVkM6Mi0pJygrLzc/T/DLu7Ou
+rKusr7S8yOJfSz44NDM0OD5LcdPGv7y6trOzt7zF2XZRRj86NC8uLS81PERU8dLIvbaysbC0
+u8XT7GRTSUI+PDw/SVj71czHwr68urq8vsfV72FVTEU9ODQxMTM3PEROYOvPw726ubi6vsTK
+09/6Y1RMSUhLU1xq7t7Z0szIxsPDxsvO1N57YlVJPzo4NzY5PUFHTVz83M/Jwr+/wMPFy8/R
+2PleWllXW2JvcXLu3dnUy8fIx8jM0Nvh72lRRT46Nzc4ODo9QUtaaebPycO9u7y+vsHEytTa
+429fW1RPT1RWXGzw3NPPysfFxcfKz+JtWEtBPTg2MzM1OT5ETmLoz8e/vLq4ubu+wsnN1ON8
+ZlRMTExOUV138+DTysXDwsTGy9DaeU9COzYzMTI1ODxCTV/w1Mi/vLq5u72/wsbM1eH4YVdR
+T09PUVhdbOnXzcrFw8LCxcvV7VdFPTk1NDU4Oj5DS1v72MvGw8G+vr/AxcrO2ODrfXVvYFdV
+WFtidPTf2tPOysrLy8/aeVBFPjs5ODo9P0ZNWG/g0svIxMLBwsXJzM/Z3+f2f/t4Y11fXFte
+am785uPe1tHS1drpaVhNSUdDQkNER01ZYnTk1tDMy8zMz8/S1NbZ2eH7fPd0Z2tsbG36+3Zy
+e3p1+PTq4t7c4vxnXVdVU09PUlNRUVhdY2366+Xc2Nfb2dTU1Nna3Nzc4O75dm1ydmdla25q
+cPjv4dza2+P6bWZeV1FQTk5QT1NYXF9jbXvl29jZ1dLW29rX2tjT1trh9HBsY2Nua2NjZ15p
+fXV6b2lnaW1naW54cGx4b2tkam1qbf7r6ufn6Orx8uvp6+He4+36+Xhz/H5zcXBub3Jyefx9
+em//9/Ds7/R7bWltamVpa2RlbXjy5ODl5ent7PP49/56b2xpZGlscHF++/Dp7Ozu6+Tj6err
++nhybW1xa2diX15iZGlvcn7+d/fu7OLe4eju8/r57Onk5eXq7/d5/fj+c2xqaWNdW2NmZGVp
+b2767fL06Ov07evv9/Z6efLs9vvw8Ozh3uLj5+rxfGtua2JmYlxaXGNnanj6f/Tz+vfw9frp
+5uzq7fB2cPDv9/fx8fj4+fj2+PTv/vv2fXRuf/x4dG5x8/t1d/50cHtyamRobG1qaWhs9/v6
+6ufq9u7z7d/d4evv+XVveHt4dXp5eGdob3X66eXp7fb1/fd4cXVraGhtaWFlamdla3X57u3t
+6urp4eLs+Pnz/nr++Ht4fnv26+vp7HpueHpwaGp0dG1vdXT+eG5tcnZ5eHJ4/ff8+e7s7+bf
+4ej09HtubGxsbXJvbmNhaG5xfu/t5+br7vH3+/Du9PP7+nRwcm12dnVzcm5qeevo7uno7u/3
+e2tnbGxoZmtxc29scvLm4+X3effx6OXl7fLu+29tdHNlYWRlZGdqam397+32/uzo6ubm5erl
+6vt+8/Hs6nxpZmhqcnxvZmx9bXD59vb49P37/GtjZm1zfPv7+/f79fX06d7h8fHve/jr9m5p
+fPn5+nxtbHlue/t9ffTx+3F6fXd+8u3+dW1jXFxhavPj39vc4uHb29/m925nXVhaXV5hY2Fm
+eOrr7ufr9fb16eXq7vd4b3B0/HNwe/9+dHL48Pbu5+bs7fp6dv/v+nVtbWlreHJtbm54+fb8
++/Ds+mxwf3FpbWtsd/bo5+zp6uvv8fZ7eHl9cm92eG1ucH/t6/R8+Pt3e/fs5ej2/vtvb3Np
+Y2luZ2Voa3P57eLe4Ofte25we/n8dXRraXR8/fT0/P/29/bv+XlufPp3b/r7d/nv9n50dnZu
+efz6fHD97fF88vD8enVyfHVtbnl+dfz0+n397v346+708fl1bGtvfP1zc3nx8PLt8nZu/vf+
+Z2FobHJ4/vD4fv357efp7e3u7Ovs7PPu9G5na2xscG1nZ214+31udH7y9P/6/P3n5e/y7Oru
+73dnaG9ubnVxa3ryd/jh4ebofmtnX1xaYGZqbnN7+fLl3N3d29vh7Ontfvvv/mtqeXhscn5q
+ZWVeZnL/fu3m6+bc3N/p7m1cVVFKRURIT1/u187KyMbGyMvN1OVxW09MSUhLT1hn69zWzcjF
+xsrP2epuXVdXVFFOTE1PWmhqYmNlXl1gZ3Xz7//67N/T0tfb3unxfGxlbvjy6uDg2M7KycvL
+z9jjdmBXT0tJSUdJTFRhf+Dd5nBjXFhXVE9NTU1WX2Xv2NPNycjJycrKzM3S2+Pz9u3f5enk
+6vhzamBcW1lVT09QU1dZYHPw7/d3aFxQTEtMTk9TWV9l++LWzcbCxMLDxsrLztnm8XRgW11s
+/e/s6uzs7PVyaWBcWE5LS05XXWJgVk5OT1RWYXNpaGpt/d3SzMjGx8rMztHV1tje+Xhxa27r
+2dPa4+9vY2RlWlROT1FSUlVbWlhUUU5OUVheau/q4t7c3NfPzs3LzNDV3Orw5dzb2trZ2+Hl
+3t7f7HpfUE9UUk1PUlNbaF5WUk9RUVFQUVdcY3jn29fPy8nKztLV3OTi3t7c2dXS0NPY3dvV
+1tnrZFZUT0xNTk1NUVJLRkRFRUhOVVxlbfDa08zGxsfJzNHT09fZ3N3i4+Tm3dPQ0tbX3PBn
+W1thX1VQTkpLT09LSkpIRUZGSlV15dvW0cvIxcTEyM3V3+xtbHV3fvvr39bRz8zM09re6nZt
+ZVxZXF5USkI+PDw8PUJLUmTr2s7JxMC/wMTJ0N5wY2RcXmpxfubaz8jBwMHFytHjaFdWVVFN
+SUI8OTc2Nzs/SV3m1cvEwb+9vL2/x9PoYU9IRUdMV3Td0MjCv726u7/FzN9pVE1JQz46NzQ0
+NDU4PUpn2s3Dvru5ubm7vsTO7lVIQj8/Q0hQb9PFvrq3t7i7vsXN3mJMQTs1MS8uLjE1PEZZ
+4czCu7e1tLW4vMPN6FRHQj48PkBIWt/Lv7m2tbW3vMDGz+tYRTszLy0sLC0xOkVc08G7t7Kx
+srO2vMPSbE0/Ozk3ODtBT/XLvreyr66vs7jAze1PPjUuKyopKi0xO0z3yLu1sK+vsLW6wc7y
+VUQ8OTU1OT9KZ8+/t7Gvrq+xtrzG5lBCOC8rKCcnKi43RW3JubGtrKyusLW7x+xUQzs2MzEy
+OD5O3sW6s66sra6yucDSX0U4LysoJiYoKzE9WM67sq6rq6utr7a/0mdJPDUxLzAzOUFb1L+1
+r6yrq62yu8fhUj41LikmJSYoLDRBeca4r6urqqutsbnD2lxFOjMvLS4xNz9bzbyyraurq62x
+uL7WUz80LCgnJiUoLDNA9sO4r6upqaqtsbnG51A+NjEuLS0vNT9Y0760rauqqqyvtb3RWz80
+LCckJCUpLjdK2L2zraqoqKqutL3QXkM3Ly0tLC4xOUd0ybqwrKmoqaywuMTnSDctKCUjIyYr
+MT5rxbWtqaenqKuwustnQzgxLiwsLTA4QmXKurCsqqmprK61wehHNiwnJCIjJiszQvLAs6yo
+pqWnq7C6zlw/NzAtKystMDlJcsm6sa2rqqutsbnG9UU2LSgkJCYpLjlN2r2yrKmoqKqttcHf
+Tz42Ly4uLzE2PlHgxbqzsK+ur7W7v818TDwxLCopKi0zPE/Xv7ewraysrbC3wt9RPzg0MzIz
+NTo/TfHKvre0s7O4u8LO6GBYT0Q7NjIvMDQ7SG/Pwbu4tbS1tbq/zO9PQz06OTg6PkhOX+vV
+yr+7uru8v8nZ+F9YUU1KRT8/Pz9DTVzy2c/My8rGxsjJy9TjaE5MS0tLTU5OU17+3tLLyMfH
+y83O1t3f6nZdTUM8Ojo8Qktb7tHIwby7urq9yNduT0hDPz8/Q0dPX37k0srGwcLI1etxYVtU
+T0tISEhMVWrh0MvKycrMy83W4fRtXVlVVFZXWVtgXVlZV11x7Nva2tbZ3+Tl7nZpX1pWWmJs
+amz8497b1NLR0dPc7HdjX19dXF5nX11bW15lfO3m7vZ6cXH77/V0bG5xcXH+8e/q6ez4+/rv
+6ePc19Xc4uru6ufreWFaVlJNTE9VW15pcfHp4dve3djX3ujyeWhhX2Jrbm93/fXo3djZ3uDk
++GxnYF9eW2JraGdzfPbr4+LwdWpna3Z+9e3xevvt7+jg3ef98vV1YmFfXV5nb2djZ3jy59vb
+4unrenBwamptbHNxef717uvq7+nm4Ox79/5ubW5qYV9kaGtrefP17O3z6ODn8fD1e3lvamlt
+d2xscnL3+fXy8vH17er0//t5b2ZjZGlv/vLl5OXk73f57ft7entxdvz5/nf0+/v5dX18bGhq
+dXv5+3h6+/r98+z07en5dXJ9+3dsZmJma212cXTy4uDa2d/k6en2dWpmX19mZmNmb3P77Ong
+3uHn8XtvbWtvd3JxZmxtZWluamX65+bl5urs6d/e5Oz3e3Zxa25tZmllZ29udvHr7ufj6Pp4
+cmpoZGVgXV9qanf97NzZ2d3h3+Tq8nttYmlnYnr0/XhvcX7+bGx0b3R78vDy5eLo6vt3bWNb
+W11fcune39zW0tPW2+TyfGxmZF1aW1JLTExNUVxiYm7o3NTKx8nJzNXe5/1tbW9zal5dZGhu
+eG9tb3H+6OXc19/r7/p4fGlYV1pcXmp27ej5c1pRVF1famtkbnltcvHt4tfY29vd3drb2dXV
+19na2dfU1tzo9G9eU1BPTE1OT09XXF5ra3Hh193mc1dPS0pMVV1fYmVndODPyMK+vsDFy8/S
+19nn+21fW1VbbOjh6O9qVlBOSktOVVxcWVhYXW7z59nW2+hqWVZUUFBRUVddY+/Wy8fCwsTE
+xcbL0tvm+XZmXV9z59/c3ur5b1xVT09OS0pKSUlITFhk+OLc29vb4+j0bmNdWVRTVl5t9N/Z
+1MzJxsLCwcHHzdTh7mxgY2ZeWVxhYmZwcWVdWlhOS01SVFVZXF5t+e/u6eTo+WFXVVNSVV5l
+c+vg39jRzMjGyMjIys/b4Ov99/Tt+/zl4+r0+PtpW1dZV09OTUpKTVRbXWnz7fz/a11WU1NS
+Vl1u9O3s6eTYzsnGwsHDxcnP1trn7u78dmhlbHR49+p5al1RT09RU1tjY2NeYVxWUE5OS0xO
+UVhodnfy49rUz8rGw8LGzNPc5uvr6ujc2d/k4eTg2dnc3ul1XVFMS05PUVhXT0tFPj1AREpT
+aPTm3tfTzsbAvr6/xMzS3Op+bmpkYlxh/enYzs3P1NXY4ntpX1NOT09KRUA8PD0+Q0pXet7P
+yMfIx8XGyMvO1uPzcGJfYmvz3NTQzMvJyM3Z5/JvX1lVUFNcXldMQzw5OTo8QUxg59XNx8K+
+vLu8v8fV8F9PS0lKTVp83c/Jwr69vsHIz91oW1hQUlJJPzgyLy8xNj1Ld9HDvbu5t7i3ub7F
+029OQz8/Q0ZPa+HNwr26uLe5vsbYbFZLR0Q/PDYwLSsrLjdCYsy8tbKwr66ws7jC3VpEOjUz
+NDg/Te7NwLizr66vsba/1GlPQzw1MC0pJycqLTRD9ci6sK2rq6utsbnJb0Q4Mi8vMTY9TuvH
+ubCsq6qrrrW+zmxLPjYvKyclIyQnLDhO0Lmvqqimpqmssr3XTDkuKigpLDA8V865r6unpqap
+rLO90Vw/Ni4pJSIhISMpL0HZua6opKKipKmvu9VJNSsmJCUnLDVJ1bqtqKSioaOorbjNWz4z
+LSgjHx4eHyUtO+u5rKWgnp+hpKq21UUxKSMgISMoL0PZuKyloJ+foKWsudNLOS8rKCMfHR0e
+Iis83raqo5+dnZ6hqLLLRy8mIB4eICYtO/q7rKWfnp6fpay40E89Ni4rJyMfHh4gJzNVwK6n
+op+enqClrb9QMichHx8iJyw1Tcewpp+dnJ6krbzfTT87ODQvLCYhHyAkKzpgxLOrpaCenqCn
+stw9LSUhIiMmKi45V8Cto56dnqOrudNYRT48PTs2MC8tKCcoKS47W8WwqKKfnqGnrr9RNCok
+ISEjJy05VcGtpZ+en6WtutdQQjw5OTs9P0RDOi0oJyYqNlvEsKiin56fpa7GPywkIB8gJis0
+TMmypp+en6Otwm9GOjY2NjlFbsvDxN46JyEgIis/3LytpJ6cnJ+szzclHh0fICUsOuywo52c
+naSvv/FENi8tLjhVyLavrrPFUy4fGxsdJjvGraOcmpqdpb44IxwZGh4nNeWuop6cnaSvwVY0
+LSoqLTzmvK+rrK6us8hAKh4YFx0qPsaso56amZ6uUigcGhwhJzbWr6KcnKKvxlI6My4pKTRr
+uq2qrri8vLy9yE8zKSYmJCUtPmq+rKWmqKu2ZzYtKCcsOkviua6srK2430k+NzQ7SV/axL2/
+vbW0usHGxs/cdkIvJyEgJy86Sde3q6WiqLhtPzcwLy8yPH67sbCvr7fJZ0EzLzlHUGPPvraw
+rK64wsPF12xHNC0pIR8nLTVFy7GsqaWos8XgTTUtLjAyQtG+vbatrbrO3lg/PD5BRWbFu7eu
+q664urzTTzwuJiAfISgsM0XOs6qlo6Wpr79XNi0qKSkuO1rBr6qqrq+wvVw8Ojo7QVbdvrCt
+sbWzt8tPPDAqJR8gJykrNvS2rKWgn6Opr8dFLygjHyMuPFm/raaioaartMtINC4vMjlH+Mm8
+tLCvsLXEUDctJB8fISImMW+7rqSdnZ+kqrlaNSsjICMnKjjVt6ylo6SnqrHJb003LC01ODpY
+wbq3s7G1yj4uLygdGyIsLjnFqKOjn5yeprHEUC8nIyIlKi40UrqtqqekpKmwvtRXPjYzOD4/
+TM/F7Vz9RTIyMCssMTg9Yr63tK6pqqyusb39R0A3Ly4wMy872u/ts6uys6ystsDD019TT0hA
+Pzw8PTUzPTsvNEE9PE7Zyci/ta2tsK6uuNN6bkEzMzg3MTNL+1Hrure7t6+ts7y7us1pYlpE
+Nzc5Mi84PTYzOD5BP0nYzNHAs7Cysq+xvcfF7VNIQUhCO0RFQUdMXdvMwbq4t7W2uL/N31pE
+Pj47NTg5NjY2ODs7P1dv6c+9t7e5trW4vcPFy/d75XRRTE9OR0lKUVZt1szGv7q8xczR705M
+TEZAPz49Ojs9PT1ATFxz383Hv7++vr7DyMfK2OHe7XhmXVtPUFdcburPyMO/vcDJz+BcS0dD
+PTw8PD09QUhNTV157+Hb1NPX0czU09HNydDPyMza4/NsW1RZWFxv5NLKyL+/xsrP4l1OTUY9
+PT4+PT5DSEZMX37+ft3U3eTU0NXT0MrKzMbDyM/Z2+taWmlcWmZ+8+bZz9TWztLh+2xfT0pO
+SkA9PEFCQEdPUl3z2tra0MzOz9DQy83Mys/X2Njf6vj0bWRv++/f2tLS083U29zwcV5MSkk+
+Ojw7PDxCTk9Y79XPzsrEx8rIys7b2tTl7uDg7u/f2/Jw8fZ77d3Rz9HJx83Q2N1YSUpAODU4
+NjU3PkZJXdjNycC8u7/Av8bS1dt9bnV2YmPl2+Pd197v6t3a4djLysvSz9xOT0o5NTU0LzA5
+QEJM3s3Lwry5u7y4usXGyd7+aGBXS1ZlVWPn4NzbzcfPzMPK2NTVYU9PPzc1NDIwMztAQ1fb
+0cm+u7u8urm+wsHN2dvrZ1dbWlVbbe7t5c3LzsS/yNTO50lMRjMwNTAtMDc9RUrnyc6/tbm7
+tra9wr/J3OP6Y09PY1paed/g5czHzcnBxM7P1VJGSjYuNDIrLjY1Ok/z0se+trS4trG4w73B
++G/4VEpNT09daenU2cu+wMa+vMnY0F4/QjgrLDEsKjc9Ok/PzMO6trWysra3vsjK3FZOTEZD
+SEtPW+nVz8W+vby6u8DGymtCQzssKi8sKC8+Oj7cw8O8s7GysrO3vMbP2XxNR0pFP0VVVlrc
+ysfAurm7ubrFzt1NPDoxKSotKio0PkBZysC8s6+wr7C2u73H/l9cSUFGSkdMWmjmzsfAvLq7
+u7vAzNhePzk0LCksLCouPERM38O9t7Gxr6+zur7E2GBXTkNCRkZLVltv1cfGw7y5u7y7v9Ds
+Tzw5MiopLC0rLz1IWdXAubKvsa+wt7zAyN1aSUVFQ0FIVWl37szDxL+7uru8vsjbZUs8NzMs
+KSwuLS89S2bQwbm0sbOzsra9yNLnV0hHSkpLUFz669/NxcHAv7+/xsvP3WhPPjo4MCwuMzEy
+PU9m38m+uLa5ube5wMrO3VxPTk9MS1Jl7eDZzcrGyMrCxc/a1eFoUUdFRj03Nzw7NjtJU1p0
+1cTAw8O+v8nOys7rXWl+cGBkau/o4dvMy8vOzMvQz93r829TT0tMP0JAQT0/RUdITFjv4N3T
+zMjPzcrO19/i7/Xwc+Tq29vW19XRy9TP3dD7amNvWlpMfVvwUeB0bFVZWltMTkdNS0lLTmpu
++PLd1d7r4dXU19fP1M/jzNTH1s3K1dZ63/53amH8XmdaaWdtbe1z+FpeTEhHREZFSEpNVVZ6
+/NnU1MjOzNbOz9TU3trS3uzj2ufn6PTo593o3enf6Obo7G52XWtZU09TT0tLS05NSk5OU1dr
+dunZ3dPOztPZ1Nna393k59/j2+Te3Nbc4N7c5PPwbnRcYlZYVFtYXl1kXlhfempeYHNrbG5s
+Y19qXf3p7H7i4Nvk3ufe3OTk2tjZ2NnX3N/r/nhiWltUWVdZWlxsZ2Zv+ex5637d5Or38m5v
+ZWdsdGpk6Ptw/nrvfP9v6/R7fH1s+Ph8avNx9uPh5+jk5+3r/eZ63ObhaXNeZGz7+m1rZl9f
+ZGL0dPZ/6vb3+vvx/vx6cvxsaWh8bGxt8vj/e9zteP727fL+bOvr7Prteul0debs+mvn3ers
+7OTibGN5eVxZXmVtWlprZmdge3Jvb/jn6OXt5vbj9e393nvv/eP23uPm4fjhc+lz/m1k8l5o
+V29pYmFcc+1fa17hV3hi8npzeNLy4XvZdtNt3+D+9uTm5OvnXuJrbvx93XPfWGjbVGRa81Bh
+X1NqXFJ49/Nl0ubh5N7ffORl2/Vu/dvcZ9PeeNxv42PaZHbfWvzl9mbd3FfpW1VnTk9NbUdU
+81Vp5eTnz8/4wuPs289mZtNP6W1bfd5yY9Xdac/8z97X6NvlcN97au9PWU9PPUdLPklJWmVz
+79bB18zCys3b1t70aGLyVFdqd1/30+je09HP3NfW2Nfn2eHcb/BpRklMQDc/Rjg9Sk9b587H
+vcTAusTVz9BiWF5OUEpMbHZ03MrOy8PIysnIztjd3eBodutbPz9POy87QjU1UFlP3sW/wb66
+uMLPytxTUlpIRFJOT3Pi1srFx8K/yMXH0tLS7XvrdGxYT0s/Pjs1ODg2PFVOVsjEzL+5vcK+
+xdXnY2FRSk9VT1P4293PxcPHxMTNzM3r5eFxau/vUVhTPj8/ODs9OTtQUlXLx9TDusXJvcnr
+2txaUFRWT09f7t7eysnRzMnN1c7Q4OLe+Xrd5H3tbk1ITT85Pzw2PU1JSeDZ9Mm8xMm8xOXW
+1Whj8WZZZmdh5tfa1szMzs3M1Nrc4PLz+2lpamJoYVVMSkE9Pj07PUhLUOzZ1Ma9v8TAxM7V
+1uRwe21gXWZqZ3z25dnX1NDT2tzc3+Xf3uz5/PdzbmZPSklDPTs8PT9MV2LqzsrHwcPGx8jR
+6+x+YF5nXVxzdnjg1dHOy87U0s/S3N/m73lsZW1lYmhUR0VGPjo+Pz5IXmj6zsjKwb/GysnQ
+5+nmeF1dXVxkd3Xq2djRzM/T0NLY2+Dx8fZ2/fxudm5aS0pHPjs+Pz9IV1tk2szLx8TL0szS
+7uvlZV79f2zn3N/Xz8/Qzs/V09Tk8+z6e3xwZmloWVRNRUNEPz0/RUROaX/mzsjJxsTKz8/W
+7nl5amV69vPm3+Dd2tjSz87S1tXc4e1xal5cWlpXVk5JRkNCQURGSE5c/ODVz8zJxsnO0dvl
+6e7q8vbs4ujo3NrZ1tja19fd3d3i+GpmXldXWldUV1RRT0pJSEdJSkxUZXnr2dPQzsvN0tLV
+29zb3+bp7ePd29rX19zc3d3j7vVyZFtYVlJPUVRTWV1iZmRfZWxoZmhmX2JoaWVv9Ovg2t/f
+2uTq3dvk5+Xq5drY3Nza2trc5e93cWdcVlJTVFpdXl1hcHpyeP/07fxoX2Vv/f5tZ2z05+74
+7vN7eHd6d+vf5+nj4d7c29zc4+31e2pjZmJeZGhkaHFvcv3z8u7seHr3/HFpcPh6cf/4c256
+fHRvffh+b259+v/67/f7fHhxffr06vdrZGptb/bt8evp5uXp5+Xm6vD8eW1scnFtaW95a2xs
+aG5tbmlqfHj9fW9/+f56fvPq5+30/HxtaW9pYWz98vn49fjx5uXo5OXo5e74+P5vaGZia21q
+c31vfO3r6/F8fHRyen1saXB++ndqZmVqcmpna2x96ejn5+vq5d/j7vLp6f9veGxsdX7z8Pfz
+6/P/dW5maGxrbXBsZm18bXL2fnv463xyeH13df70+fLs7ebm6ur1cGpudmz59/v4d3v97+7u
++3V5+n9yb29wb29vbnDt6Orn6ff/b2ludP5taG1vbn7v/vzx7u/07vr/7unm7flyXkzkxEh5
+1txfamhMw0xSz+jyVuDr319QY79OZf/Ibk/eYc9H8NnTSdJ7+lftzF/qW+zOU2vqzEvtTshd
+b11ya/JlWk2+THVqysxF2PvNSl3celRbXcZcTcrIZVG/e19ads5oOtvN9zvU39FKT8XYzznJ
+5cg/asBH0UXEdk/JVeJhyl9Zw03W3kPK91FK0XlR10nF811c3NVM6mDe2FTi6tlmUnrm3E9G
+3sxpT1jE1FZL3cTsUF3JzE1Q0MpXTP6/b0xpyNBHUuPPTkZ309hPSdzD+0VoztBcRGfD2UpR
+zNRyVufM011SbdxaS1Bs23pc6NbI8V/fyO9PYP71T1P3y9703tnScVVY8mxOUF/k62Bt29rv
++t7gd1pldnJibNzj8evv2djYZW1iZF5Va3VzbHzr7d3u629sfmNdXmt4evf64eLp1+Tz7Ovs
+5Gtmb216YnNn+Pb87uDocG1+/l5jcvNs/utvb+Defu7xY2pdWmNdZezT39zZ2u1++u1zWWFo
+a1x/6t/g49vb7GVYX29iX/3m6Xt46ODudnFv4/VkXVpof3j69unx6+nd9mz7bmdu63Zr/X50
+aGty4Nfr/erj7OtdXfXsa3nu/9bsXFv54HzuYWr67npu6u/nclhXXmZv6NfY6V5WYf3r/W98
+3djpaFts5N3e7vVqYm5r/9jP1NHZ8mxpZ23h5vD7cl1iY1RNVVRWT0M+QExScNvTx8PEycrR
+2dXb29XO0c/IyMrN29zd8WRldtnAPR4hKi47X09ss62xs8xDTNzf5c/r6r+8wsXL6NPIzNXc
+81762ce7uMEvGhskKDRVWdOqo6eos3tOTzgsLzhMwrOxs7fB08/O2edsWfrIwLy3r65IGhQa
+HixYc2qwoZ2boMc0LSwqKygqTa+joae2zsrGy/1FOT7bua6qqK9LHxgZGyArOFC4pJyZm6jW
+OSojIiMmN7ynoqKrub/CzOdINDZHzbGopq74KxwaHCAnLTjrq52YmJ+xcDgqIyAfJjrAqqam
+rLO3u8xcPTM6V8axqqq3UCYdHSAkKjE7zqecmJmguE41KyUjISIv3LCopquxs7jF3EY3PFLS
+uq+zwEgpIiEiJiwxPcapnpqbo7blPi4qJR8fK0TErKanqay2wtFaSkxPXc+9u75eLCMjJSox
+NTvfsaOcnaSxzkQ2LygjIyk0a7Wqp6ist72/ytRtT01ozsC/ViwlIiMpMzk/27eroZ+kqbTh
+PjArJiYpLT7Kr6mmq7a9vcbJzO5aY9zJyFAuJiIgJi40QOG8raShoaSrv00xKSYnKi06YL+v
+qamtsLa9v8LO2d3f6WE+LScjICIpLTnvuq2kn6CiqLfoPC0mJScrNE3JtK2srq+yt7q9x9PX
+2N11PywlIh8hJis1Xb2soZ2dn6WwzUcyKiYkJiw937uzsLO0sbGztr3Gy8vN3EktIx8dHiQr
+NVHDr6SenZ6hrMJUOC0oJicqNEvOu7a3t7Owrq2vuMDK0d5SMSQeHBwgKTJMyrWpn52dn6e5
+6UEyKygnJy06TtrAurmyrq2rrK+3vs7rWDYmHx0cHycuO+u7raKdnZ6ircJiPi8pJiUnLTlK
+5se9tKyqqKissrrAzndBLCAeHR0iKjFB17epoJ2dn6exwX1BMikkJCYtOEr4xbivqaalqK2z
+vMjWXjopHx0eHyUrND94uqqhnp6gp667zlY3KiYkJSoyQGPLuK2opaaprbXA02dHOS0mISIn
+Ky8zOUzXu62npaittLq+ymM+NjQ2Oj0/RE5w3nhvtKituMG9trXHSkJEOy8pKCwzNzU4QmrL
+v7ivq6uwusPL1G5JPjw9PT5DTv7Ov7m2tbe2uLzF/D80LiopKy0yODs+aL+0ra2ur6+zvcrm
+U0A5NDY7QUtV+cy8trS0tbm+yNRrRjYuKiotMDE1PErsxrmvrKutr7e/zPFMPjk5OTo+RmDT
+x7qysLG0usDL22dENCwoJSYqLjdEY8/At66op6istsTuT0E5Nzg4Oj5Jbc2/t7KvrrG5ws7k
+TzwvKygnJyouOEztybuyraqqq662wdxUPzk4NjU6P0de08C2sK+vr7a+y+dNOi8rJyYoKy85
+R2jJurGtq6ytr7W90FtFOzY1Njo+RlbdyLyzsK+vtLq+zGNDMywpJygqLDI9S+3DurKtq6us
+r7fA309CPTs5ODk+RVnbxbiwr6+wtbm+0lc8MCsnJykqLjU8Tt3GurCtrKutsbnK/E9BPTw5
+ODo9R1/Xv7ayr66vs7a9020/JyQsLCcqLi8+XtW4rrCvqqyyt8HhZUo8Ozo2O0ZO68zFubKw
+r7K0uMftQTAsKiYmKCgtO07Hsq6rqKmpqrTHbEI0Li4tLTQ6S8q7ua+srKmqr7bE3N1mNykj
+IyUlJictQM+3rqiloaClrbzsQTQsJSAlLTU/V86vqKampqiprbzZYk9PPywjIykqJiQnNGDE
+urCmn5+iqq+31z4vKSQkKCwtOF+/r6uqp6Okqq+7xc57RDkwKCQnJyEiKjVJ28KzpaCio6Wq
+ss4/NS8lJCopKjdJ3Lexrqajp6mqrrLBZU9JMyooJiYjICYwOUHRtqulpaShprC9z1k5KiYr
+KSUsPVjWwLOppainpamvucvuUzkuKCUkIx8hLTE1WL6wqaakn6Cqr7jQRzcsKislJS84PXK9
+sKqoqKSkrbK2x/dSPzMpIicnHB8vLy9QxLWqqaWeoa6srsxRPy8qKyYjLTQ5XcG4raenpKOq
+rq++6mBCMysiIykfHCouKjvIua2npZ6epqiot3BENCgnJyElLjE90LmupaOjoaasrbbdV0c1
+KyUgIyMcIDIuL/20r6iin52gqKuz/D82KyQjJicpM1HHua+ooqSnpaq3wNBnQjYwKyMjKSIf
+LDUyR8azqaWmn6Csr6/VPzguKSkoLDg8Tb+0tKypq62yubnD331lTz84LCYtKB4nNy820Lqt
+pqqkn6m5t8Y8MjItLS0uRFdKz7Cvsq6ur7bFwsDlffpv60c4MCsqJyElMTQ52Laspqamoqq+
+z949Ly8vLzEyRd7rybKur66wsbbM28vvUnRrYVxBNSwrKyQiLTo8Z7qtpqeqpae54/RFMC0s
+LzczOujHxrWtra2yuLjE7vdsVFJWVG9VPzYsLy4lJjhFSNS3q6SpraetzGBQOTAtLDQ9OkrO
+xLqxsa6tuL/D5VhQR05vX1/p/083LTUzJik5R2jJvqujrbCss8pcPTY4MSwzPkdd3cCxrbKz
+sba/4FpaWEVAUGTu5l5gQzI0NS4uNj1lysW0q66xtbzF30s6PDo1Oz1FaNzPvrWzsbW+vsd5
+VVNKSkpDTvllX1tJP0RBOj5GTXvb1MvDw8TDztrW3u50WE9RT09UWm/o2dLNy8vOz8/a7fHj
+7/ru7+7b2Of/Wk1KSEE9PkJIT1Jh6uDX1dbT19rb91xoeGvv29HOzc3MzM7Nz9zp//vu8Ozg
+2d3q7W5PRkZHPz0+P0NKT1hn7tzY1tjV1NfV2+Db3ubc2NnW1dnc4N3b4erj3eTn2dPV1dfb
+4mNPTUtDPjw9Pz9DSk1SY+rXzszIxsfJzNLW3O94df5xe+/z+fTq3NbW19XU0NLZ19je4eZ1
+ZFlKQj06Ojo8P0NJVnvc08vGxsbHx8nO1tvl+21s+PF8eOjW1djT1NXS1tve3t7sb2pvaFxR
+SUE9PD0/QUZOVlp53NHMyMXHys7V1+D4f3d97Onq3N3c1dbZ1dnk6efs/Ofu9e31/vlsVVZT
+TUpHRkZISUhLU11dduPg3NbQzM3SzszPz9DX1trf4vF7b3J2c/z9/O3s6uHo6+bucHVyZGFn
+Yl9bUktIR0dHSU1SWGX65NvRz8/T2tnT1NnZ2t/k3Nzf3drW197f3ud9/3ZubWhqXl5nYmJr
+aV9XT01LSktMUlpga/je19fa2dbX2+Lf537x5+ju7eTl6+rh393Z3fFybXL4cWh37O7s5e5+
+bW5wXVZST01NT1NVV2BxeP7o39vb3+Dl729OUcba38rMxM7c3dbvaldeYXdKTtLg8PTQ0NXb
+ZHzfWj5DRDs8PDtFaF39xb68t7vEu8DWf2NOTUo7P05LT3fbyL+/w72+xMjS2NDZ8ujuUToz
+NjMsKi02P0xew6+rrKyqq7DA7FZBNy4tLi84PlDVvreyrq2vs7zM3WhLQURDRUtOST88PTw2
+NDpDTnLdybiys7S0trzH6FdKPzs3NTtGTFzOv7q2ubq7wMvbfVxQT0lGTU9OS0c/REpBP0ZQ
+WnHe183Dv72/wcfL1vdiVFNPTlNe/uzk5d3Y29/k5PtrYGp8bP3u6tzh8P3z7v15aHRiV1BK
+SEVKT1BPW/l8/9/c19TT09LNzMzNz9LX2ub+a2VfXW5qb3z99+rp5t/o7fh5cmFebF9VTUpK
+SkhITlVaYGvl1d3h1c/U1NXX2OXi5+304uLr4tza297T0dzX1tve82RqXk9RT05PT09SUVhn
+Xl1pZ2bw7fbr8vbq92hqZWZxZGr+8eHb187LzcrJzdLW3+Tpb2hlW1tiX1teZWhdWGhuYm7z
+fnFrYmluZWpgYmBdX2doZP1+8+r04Nzg39jY19ff3+Xp39/u7+78bG5ra3FkZmheW1xdYGVv
+7+vz+Pnw5+325+n6d3doZ3j98/V+cnjx/XF47fpybmNlaG9ve+/27vTs6nz26uz18vXr5e/p
+5ubh5vp4em5wfHRnW11hX2/v7efo7u3n73dhX2NdXmRt++7y7vT17/z4/29u//T3em/v3uPj
+3N3p7/D+/XxvZGZkXF5qfPzm4ebk7fx8/mtgZmtte/Tx7ufn7fl9dmhfX2Rub2/7/Xj56+Tt
++Ovo7/Ps/PP6aXj19+3i6H98+PRuZ3B7cHTx6+/u6eb4cfDtaF5pa15ec/hqaHx7b3nw7+/v
+6ujz+/h3df35d3P68XNjd/L65+tT99bu2t7S3dp78u5iWl1xX05LRVzN51R5yMvQ6WzU11RO
+alRQVlVN79vk49DJz9Z6b93ZYFh7cWBvYVny4mhnfund72Nq3eNrXl5kXF9fcefk6nft5+z/
+/eTj72VZb+fyaH7h3OVzfe/3YFxv9ndoaW7v7vXm29fl82xi599jUmD4aFto+f766+rt3eDx
+7/VzY2JeY2pjXnvc2fJvfWdcXmVw8fRu+OHl6uPsdN7b7err7/j19mvw2+jl82536fF76PFf
+WVpUTkpGRE1OSE9o6+ni3s7FzNPN0NTR2uHSztna29zS0tvX3v3m33tub2VcXEotLDo3MzZI
+1LzGd8G2xmxV+9LdVFbIwMzSyL3C1PHWxtlmZNnJ1f390tX6al3733g4ISlGMCcy6rq33Pms
+q/VGX+55TT9buL3exbq9zW9b08HxWdPEx9/p0MbSXnzT3mFUKx0sNycoP8Kvss+3o7JPTeNy
+OjE5+cLZ0rmxvedq2sTUX+nEv9Di1cPE3eXbz85kRC4fJC4qKDbQs667uKmu109SUUI0M07J
+2uK/s7fUUeq7v3Htv7rF8G7Gw/t42MzI20wyJCEqKygvbbWvtrOoqr9ZSUo/MS4+3NnNu7Wz
+w1tcz833/NC9veJ0yL/Q69rJxutKPC8mJSsrLT3Rt6+wsKyyz11KPzgzOFXQ1se5ub/fb8/J
+7GnUycfS+c/Ez+XZzc7OdEE+LyQpLisyVsSzrbauq779U0I7OjE2Y9brzbu5vtrmwcDo7tTN
+zWxf1MvS18zGvslYTEcuIyktLTI/2rWvtq6sts9UQD88MTZU6uTjzby6yM7BvcLYYvLeXlBb
+3s3KzMi8v9TlYz8tKiwsMDVG2sC+urS0u8jXalVHPzxASkpLWdzHwsC7trnHz9TbZk1SW2B5
+4+nhz87d8mNJQUNFQT9FTlhWU1t54t/d2M/Ozc7Y63BeU1ZdaurY2dzd3M/R297a0tjh3dbZ
+497Y19zs9nZaVE1BOzw9Ozw/Sl7+7tfJx8fJyszU2t7m5ed3X1tr6+Pb08zNztDOztja2eh6
+/vHr+2RYTEdDPDk7PkBJTU5j29HSz8jEw8fMztDc82dcW2Z5Z27f1NXY1dHMy8/T09nh5uvz
+eWpcUEdBQkVBPUBFRklNWmvj09HJw8LGyc7a8WRdXWRrcXbj4l/XxtfKwL/Jzc/L0Oth/fZf
+TUhKPDg8OjM2PUZZXei9t7q9vMDB3UtGR0RARUvzzNfPu7i8vcLBw+tOXVtMTlNw0tbZwr/K
+1n0+Li4uKCguPmDayrCprbW2ucN1PTlAOzU9U83Av7q1trvG5djoTUFGTVZVTuHCxcrDvr7L
+42U+KiswKCgvRO7DvrCmq7m5u9RQOTE8PzE818jBvbywsMvlx+dLRENPWUVOzs3UyMG7vtbW
+z35EMCovNSopPXXPy8Supq6/uLfXRDc4QjswPs3HzsG4r7fa3cv3REFLZVZHZ8nQ6s7Dv8XX
+ystuRzItPDMmLUpbfNK+qqi4ua66WD45PTwvMU/e7ty9s7bAysPPaEtJVlFITvr09tfMw8HH
+xb7E9ks6NTstJzFDRE3Vu62wvbOvw11RR0A6MDpTTFXQvbe6wL+7yV1hZ1VIQk1rW1LfyMnL
+zcW/w9RKP0E9Lis4QkBAab+1ur+0r77i9n9SPTU8TkRCa8vBxcrAu8Pc3NXfXEpOXFNKU3zm
+497SxsLO5n1vUT84O0E+Pk3x1tHMxL7Cys7V321QTE9OTVZeZufa29zW2t3W2d3d6X1xaGVu
+f+3e087NysrP3X1YS0VBPjs8P0RKUHHe1s7Nzs/S3ez1cHri5ezf4t/X2NvZ1dzn7fXv6vB0
+b2xv8N/Z19rb3/ZYS0lGQD4/REpPVmnc1dDJy8/S2+p+bm7v4ujr5Obj297g397g5OTl4er2
++vB5a3P38Pfv6fZuXE9LSUNCR0pKTllt4NTSzsvMz9DT2dnX297i7nRubWdeaX58//Hq393f
+3Nvf829ud3737+fwZFlVTkhEREZJT1pedd7T1dPNz8/Q2eDg4N/h6v52/m9lb/b69O/s5N3Y
+29na4/Lu73RjaW9vaFpQT09LSUpOUlZdYW3r29bQztXd3N/q7enu7ev8+e7s7vP0/PDk3NjX
+19rd3OtqaGZbWl5eXFxfZWhqbGRYUlNWWFlfZ2ppevv36t7e6OXh5d7Y2dfY3t/b2Nvf6O/3
+/vV5eXhmZV9dXF5iXWJvcW1xfHv29/52//T8eX5tZGpuaWpud/5+fP7z+/nu6ezt6vt++Hxv
+eM7if9Pp5OHqcnBtaGdlYWdjZGhrfnZ7+nl3+/H7/n706+nk7ft8enRtbmt38/RzbGptbnf1
+7fp77u7x7vJ9dW9zc2ttb/727eDe4eTs7er+bm90aGRsbWtqdH1wefX0+/t1/vV/9ern7HVt
+aGl6/fp+/HNsbW1yenJ0ffHo5uXn4d/g4Or1ff72dGhv/25lZGZkZml2fHf+/3lzd/p8e3l4
+cWl0fPn6+OXe4Onu7+nn7e/0/H56ZF9kbnp5b3T3+vn//e9+/fB+//t3c/327unv8vn9fvv9
+dmxoaWNpbHd1fvV9en3t8nlubXB87ubk4uLo7O/t7/P3/nl+cGpnXmdve/rx6/B++3l48Obx
+93lraGRpa21yfHz/cW17eWp28ezo5/H96OPm7fZ///3/eG10b3P/bW7+7+7v+G9vdG94+vH6
+endvdHt3b3zu7vDs7fzu7H/7emNqcWxuevF7dP14c3F6WVTG2kdczs9SQfew7TVAsLEyLsGp
+VS/drc4yQrrCOz3QxEk95sVtRvfJ11xj2l9m4GVc09hdae3bcFZO+9NzUm7V1ezj0WRl8WBv
+X09QamnO5+jNyOlQTFbY505VzMjjatvM40tN5PlITt7ccWjfzth45X1gX19XSFBm+N/fYt3E
+6F7cyd9hcNfaVE3311pMbdpvVHzY3nt6z83ud+f+UFJmXFFk5/Zz4NnhdF/g6VJa53ZaZ+jp
+4u3Z221d+/NsZl5d3ude/9Xldd3obnvs5+F3YPLnZ1li591oUuvWdl9d69prW/nueXpkZejs
+Y3H/avN0V2Tq9Gnq3vPd2t/f4Pj06+1/XVxqaVxdbOn+X/fi8/bk62/s4O1maWpt6l5R6dRr
+XNvY7u9p59xfWvvlZVlq/+p7c/3e3/7+bPXsX1Ro+nJqeuLY3Gz85ORqWnvj8Gdj7d7v/Pr+
+fX1sbvz04/1oYnN9ZVlmfP53a+PkcvTd3u35fXx5bnT+729p9/107N19eOp5ePd9b/XvYmXu
+fF1u7OPndFxq3u5t5uHl3PZRUvpnWWZmbOVuUV3h3unp9PDf3+/85+Tn82/97+7u5dzWz9Pa
+4+nf2NTee+jw7m9HLig2OjQ6Ss+0sL3Cub3aWU1PXvd5ednKxMLHz8/Jy9ba5e7e0trhzcPK
+Oh4dLCwnLUC/p6Svsquvv3E3LTU8NjtaxbOtsby/xc/xTkJH/8vGxr+6tra/0D4fGR4lIic7
+wqadoamoqr1LLiMjLDU+37arpKaz01hIPjk5P3S+ubu5uLe2vd9MPS4kHh8mLjzYsKehoaWs
+u2U0KSUmKzd1uKyrrLHFXz83Nj1R58e6tLG2vMLL1N1oRTs4KB8lLTRJy7mso6SqschCMCwo
+KDFIzbKssLq7w11CPj9cy8TFwL7Av8PaWnnU9llOPC0lJSovOVHIr6Wip669Zz4yKycrOV/B
+ta+usr3M90pIV2No7NHJwsPN1M/Kys3oVF1NKx8hKi86X8WuoZ+mrb5VOzMpIyg2WL6ura2u
+uNJcQTpCWmfyysPDvsDO29XU0tRcRUg6KCIoLjhkxLmro6SqteA9NS8oKC8/3Laurq+zvuVh
+UkJHVVdm39bW0NPu4MzJycXGzt9LLCAkKi45U9i2pqKor7vtSz0tJikyQ9O6trGsrLbE1l1E
+QD08RVlm78/KxLq3ubrD5Es7LCAfKC43WsW2qaGlrLPJTDwzKikvOUvKubaxsLi/xNdNRERA
+S19cVfrNw7u3tra4xltDMSMfJCouP+a/raSjp6u330s5LCcpKzJQyby0ra2xs7vnTktCP0ZD
+QlPfzL61sbGxutRbOSUeHyMkLUXfs6OfoaKpu99KLyYmJyk3a8y5raqrrbG91FxDPDs5NztJ
+bsm5s7Cur7XBbjUkHR0gJS4+8bWmoKChqLXF9jsrKCYnLj5cy7auqqapr7rNV0A6Mi0uNUXb
+vbOtqquuuuI9Kh8dICMoMkXWsKWioaOpsLrVPS0nJCQqMj/kuKulpKeqr77mQzMsKy0zP/vA
+tK2srK+4yUYpHh8gIScsNGyyp6KenqOmq8NBLSQfHyQpNWW8raSfoqarufZDMikoKy47bsm8
+r6uqq7DKPiojIyIhIyk14bCnoJ6en6StyzwqIR8fISgzW7qqoqCipqy20kUyKyosMTtM48Cy
+q6mqrbphLyYiHx0eIitJuamhnZydnqa5Ui8kHh4fIy1Az7KopKKhpq242UM1LyspLjZI1r2w
+qqiqtOwxKCYfHR4iK1W1qqKenp6fqL5UNygiJCIkLT1mu6yopaKnrbDBTjkwKikuND7ivbKq
+qa21yjkoKiQeHyQqP72vqqCeoKCkstpNMCUlJCEnMT7vt6yooqOqrrXYSTwvKi41OUzNvbKr
+rLG43DIqLCUfIyYqQsO4rqShoZ+kschpNSkoJiIoMDhQwLGrpqWpq66870U1Li80NTpP0by1
+t73B/jw9OSknKy00T+zQtaqop6ars7vLRTIyListMTRC2MfFubKyr6+6yc7oUUlGQD9KXVdM
+UnjnelJJWPFVRVVxXFBq4Hhp2MnVbfva3eDf087b5dPP7GNgaWVaZeze3NrX1Njd6mhXVFNQ
+UFNg7drPy8jHzdjlZU9JRT89PkFDRktW7c/My8nKzNLY52tp/Hn72c7JxMLExsrP3WRRTEhG
+R0tPWXXe0MnKztLdblFHPz09PkBESVX52M/OzcrM0dvqaWb4+vDf1MzHxcXFxszT6l5YUUxJ
+R0dLVV924NLNzM3P2epuV0tFQkJFSEtSX3Dr29XW3ObueXn26eDZ0M7NzM3Ny8/f92tbWFRR
+UFZkcu/h3dnV1trh+WhfXVpZXGRnaG5tdG9oXltZT05PTk9WafXm3NLPzMzP0djf6flnXmlv
+e+bW0M3JzdDP3v5xWk1KSkE7astKXcDL1cXW5stsRExGPEBEQEhXaObd2s/EwsrQ3N/hb1le
+YWna2+3Zzs7P2enh33BaVFZebF9+3M/NzszNztHuV1A/Ojw6NTc8P0/46cu8vcC8vMHBx9/p
+7ltMSkZCS0xIVfPa1dHMxb/Ays/KzNx5bXByaVpf7N/felhIQUk9MjQ5PD5FSPK+vb63sbG1
+vcvT3FdAPT09QUBBWenYz8i+ubq+wcfO4VpMS01KT2jl0M7V2epKQUA2LzAyNj9NYMm5trGu
+r7S4w9p4VkE6OTg6P0VP+dXEvby6uLvAx9h9X1BLSU5ZYfTa0M3Q3FZDST0vLzMzOEFH7L68
+u7Ovs7S7zNbbUz9APz5GSEhd393Rw768vMDHyc3mYFdTUU9OU2fe3Pbm2WBIRj86NzY3PUVJ
+as3Dvbi3ubq+x87Y/V5US0lPT01OV/za3OHTztTa2NXV2+r9+PV+fnHc1Njf3dv4UUtLQTw5
+OTxDRk1819bNxMXEw8fLztPe5e5vdH5saf728/5sfOzn8fvu7Ojo93H97+/v8eHa5eLf3+Zy
+VE9OSEA+P0FHS01Vb93R0s3IxMXJyMnMztXe4+dwbWVhXmFaV19pZGZtbG/09e7h5PTr2d/d
+3ef37/tmZ15UVVpWTUxOT1FSUldmbnjm3NfQzs/QzczP1dPX197sdmtmaF9cXmJlZ2Zs/vD6
+/u3s/fLv9/rqfm7z/Xz4emVjalxTT05NTk1MV2Zr+t7c3dfT2djOzs/Q1NPY4+np731tX2Rm
+Z2hta/vm+H3l7/xqb1tjcWlzb/n78+9sbnJoY1xWTlRRTk5SXGd28+/d0s/T087Oz8/X3N/q
+829veuzm4uXg6fX6b2NgamVeYHdyefN0dH5xfXNtXFdbWVlWWl5cXGlnZW7/7+ji3eHc3t7d
+3N7c29/k4N7g6eXk4en85ebm7vJxaV1dZFtfaV9fXVlcXF5eZ2xrbm5laWZqZ19p+Ovg7+bX
+19ba1tze3d7h7X7s5n52bOzt9vfsfnJqbWJpamplYWxsamNfaW1nbGhvYWllbGppcPzu9fl8
+fuvp597j5tfc5dzf3+Xs7u758315eG5u+2l6dHn8dG1qbWFaXVheZHh9e/d35/Xu9HZvev5v
+ZWx8/vH78eji5Ozr5eTr4fLre/lzeu317nf7/vTw/nz+Z25uZWFjaHJq73Ru/f5vc/93/e78
+bGl0dHFnau328fz87u/v6+bs8Ojz8Pp99ezteO37eX9t+W995uvqbHj3939ka2ptZ2JydnFq
+ZmZiZmxoaW7t6Prt8uPo5+Hm4eDb4+3n7fX4ef5t73x3fe9vdfz/aG5oefZrYVtbXVxVVl5u
+7Ovk39ra5efh3Nnoeu/v+f39c2xubHJ97nzx5eno5Oft6ed8cv/tc2RqZVlPTUxNTk1UX23f
+0MvKyMnNz9vz+vxlXFtVUFNZXmP75d3Y19bb3eH093lvb/729ujb3eDg2dfnW09TTkQ+PkRL
+UFpz2MvHycnJzdTpbWFiXlNSWlxgfuPa0s3P0M7P09nm8e9rW1pdaHbw5NvZ3OVqXVVNRz8/
+QEVITVv+3tTPzsnHyMvQ1d7o+mtfX25xaXr6+/P39/Pl39/j7uro4+bl3+De3dzd2drj+mNY
+TUY/P0A/QUVKVWTt3tTOzMvN09rZ2trZ3+jn4uz07Ork5ePe393e3+bp8O/r/Pjv7+vs/Gxn
+XU5NS0A+QkJCSE9Uad/YzsrNzsvP19Xc6ung3+Dg39zm5dzc3t/d6O5+Y2BoaGdubH3u6N/f
+5envXE1MRj8/QkFGU1321tLRzMrMztTc3unx8Ptuce358uPj39fZ4+Db5vH6c2t67ff5d3X1
+6ODf5elyUExQRT0+RENIV1tk4dXQyMfLycvY3N5+anZycP3z8uji5NjV2drc3+jv+nJnZGht
+dW917uHm7+jvW05SS0BAR0dHUV1t49PPzcrM0NLZ5O/6bGJz9PD68eTg3eDe3HZe3uFr6tHN
+0tTUzMvS2dr9W0g0OUAyMzlETXbs1be7w768xND3SUtXQD1OX2Nz3NHBwM/HwcbR2/d1dU5N
+XWFq6+PSx9bqzsrmXVNKODI8NzM8RVjqzsm5tcHAvct1WExITkhEXuPj1MbBwb/JzcvuW1lT
+U09MUnh+/tjRy8nVz8XXb2BTRTIzOzMzPk5f18m+sba/vb7XU05KREdBSnPyfdLCxcTHzczS
+dV5vWk1UZvPs+NnIydPRzM3qdvNURTc2PjIvPE9PYMrAubzGv7zPV1dWRUJFT/Tv9c2+w8zG
+xsvbfXf+YU1TZGBddNTO0NTRzMva5+JeRDg4QTMuPE5LTtnCur3Eu7jKZXJtS0FDTF5ZVuPK
+0NDGyMnN2drZdVJfaFtm9NnOzszGxc3P2VtOSjUyQjQtPklDU9rNvLnFvbjPW/hsRERMTF9x
+Yt3Gycu/vsXKzdrz/VleZVVXae3/5NHS0dLc6WhYTDw7PjU0PUNCTt/OwL7BvLvC1tTVW1JS
+UVlbZO3Y1NPOz8/V2drg5m9iZV9eV1xmZPXj3N7a2OXw+mVSTExHQkFHTU1Y9uPf2NTY29Xf
+9efo+u3h49/f3N7f4ufg4uPh4vL67O7p39ve5drc5t/c4ev/al9NRERDPz0/Q0dOZXVs5NPU
+1c3P29bV3eDY1tvV097d3OTk4uLz8u349/v+e/f9/uLk+fb28e3v+3NoUEpLRz4+RkZDVu3n
+4s7L1tDM1eDd3/zr4ffx3d/n393d3t/f3+jq6O96eHd2/e/o5+Pm6errf25sXk9ISEc/PkRM
+Tlfz5N3PzNDV09nb29/g6uvq8eze5e/h29/j3N308uTqfX7w+XV2fvvq5d/l7X5iX1lMSEdE
+QURJS09o7OPUy8rLzM/d5d/l9vPxen7p9Xrn2t7k39zc3d3s/XRye21oa/nn3t3e5OjuaVpQ
+SkZDQkJDSVFfavrb0s/Pz9DU2dzl+fns7v766uXf3N/i3Nvb19rf5fF4cHV4cHNwcffl5fhu
+XkxZ+0M8RkhIVVJSdu/dzdDY09HR0972f3386fP06Ojp4tza0tLb4eLl6+z3a21va296fnzr
+6+z5/ntaUFJNQUFHR0dPYm7s3drb2M/Q2Nnb4Ofh4uzo3Nff4tvc4N3a3ebh3O53/Whdandp
+ZnFpYGRoWlldWlNTVVJRWl9cY3H7fvvq4eDh397d29rd3Nve4eTl7uji4eh67uzv83huZWBk
+X2BmZ3R8dXP5/3X9+Pj8/Hpub2tiaGZeafn9eHZ8b21sbWpu/vx9cHvr5OPl4d3c2d7g6O3c
+7W3q7WlxeHDv/WtsfnRqZWZtY2NqbWtpaG1+eHL76uPu9v92e3VzbXZ/+Ox6b/7w9vbs6ez7
++nxx9vL2eXBxeOrp7O/r9vzy/HV89/5zbHX38O74ff37fGtiYWl0eWhlbmZgYGl1ePTs6vHs
+3uL68eXq6Ol+cHV8e2p57+rj6/7v7Pp6bnF8+3Ztbmpx/n1wdPf2/Pp9dHl5bnF4bm9za2dh
+VX/P4W5j/d7gfGB23+5ub2307+307+9e5NLd4n365tjua3t7e2xmXl9qZ19ZYvT5ZV5nenfr
+53Vvae7gbU9V4NXb7W/u2trn7/bv59fXeE9V6t3sZ2V26+v1d2p39HZnYmZhX2Zja+7r7uft
+8PDtcmty7+Xs6+7x8en1d21o/vR4aG55bmtrc/vf3ev+bmh46/Rv+/L4fn5+cfbp9+bk7Phn
+aHD7aW5oY2lv+nf7a3n37XBm++fe7/P773liZPPse3du9OXk3eLi5+jc4/plW1VdXmJdWGH1
+29jW4Pj04+NjW1pya2lqa2hk/nLy+37o3+rr5eLm5epye9/c+WdZXnftc2h5+efe3eh3bmNd
+YGRdWV1kfNzX7mZvfOrhcF9m/+3a2uB1bfvs2997anh48/n7ZmdrYGB9f/Dj+G167Gxnd/Rw
+du7kcl5s7/bu9X15bn5qbnr+++30eOXzYWre3PVoXmvp4e7sd2f96fpjaGtuX1z82tjo/Xvq
+5O3k3m9XWmZpZllUcNjY5m9u6dfkbmFZW3bk9mpxePrf3NvncWv85N3ef11bYPbtcWxfWvvZ
+3/l1Z/rqbXToe1tq+vptfujzeHNy7unvcWBt8uro+W999fxtYWHw3uttbezo7uXr/n7/dWd8
+dG5obfbt5vluZWF44uJvaGtodOvd4+Tn/f72+255dHB59nh27eny7fVwbXR4a15cWlts7v1u
+bm1+6+rs4extbPDp+nV+7uni3N3d4+zr6uf0b29uZ2VwfG5ramtsaGNra1xbW1pXWGF76fd9
+493Y1tPS09Xa4OHc2tvb3M/JztbUy85oPjAqKCwxNjxXvq2qrK2usr7hRjQtLTA4QmLJurKv
+sbe/zeRURkZJT17nyr24u8DQQicfJS8uKTHHqKWopqSrvm9ALSMjLDk9R8SrqK61trnKXkg/
+PERy2t/MubG2vsTVTi0eHCUvLy9ZrKCgo6WqvFE6LCEeJjpk0rutp6mwv+tOR0A7O0zWw8XG
+v7u7ws7X3WpKOioeIDdcPjrAop6osrjAWTovJyEqVMbRyLGqr8TtW09TXlNIYsC4w9rb1dTU
+0dv13s3VVjYkHitNPzFQqp+msLa+/kI2KSQtcsHMwa+rsMDhTUhRW0lCXMW+zebc2d7b1s/O
+zcrUYjogGy3sQy5ppJ6lrrrbS0IzJCIyy73Ct66uuNBHOUBdbVRixba3yHBkfmZaad3PycHK
++FAxHh88VjM4tqOlrrvXYVc/LCg22b3EvrSvttJLPkhzfGBl0b68zlZQa/Xs7NnFv8zY1l08
+Kx4gPFg4Ra+hpKy661VSOSkpPdK/wbqyr7frPj1LUElP5cO8vsvc6OR3WmrNwM3TyNVYQS0c
+Hz9NN0mvpaarvF5qazkpLD/hwL6+ubK4Xz5MaFlX5M/Fv8vtdGpbV/vWyr/Bz87TVj8wHx43
+aDw/uailp7T2dNlEKyo1R+HMzMK1ssZ259pmUVZb3s7V5N3PzdHY0svL121d8HRINyUhOGQ3
+NOmzrKu2zsO3zDkvMjpJV0xouK60vsPAye9OP0NXWVPvy8O+wMjLz/JdT05NQ0E1KC5SSDRG
+0L6wrre7r6/LSTswLzg3M0vBt7Svr7a8xlY8Ojs8P07lwbq8vbq+0OLnWEI+MyovQjYuPlzn
+ubG6taurtcZmPDc4LyszR1fNtrKyra69zthdSEhGREpRUFNVUVlsdvrVyMzR0dd3UkY8O0FK
+S1F30MTH22vr2ONpau3WysnQ1s/P3vtqaW/t7m5vZ1pWTUtSZvTczMfJys3iWk1GQEBERkpV
+aOfW2nBSXeva1NfWzMTK7mhiYHD5am3m087W6PH2b21kYvnh2tTP0Nnkc1dQTEFASEpLTUpN
+XGVZV3bYysG+vsHFzNv4Xk1HRUhUYfnazMjKz9zf3tvY2ODl09He8O9xWFpRRDoxLzY9PURc
+17uuq62us73WSzQrKy41QljavLCtr7a9yOVRQTs9R1V53M3Bu73HztDZ9k86MCgnLjg5R8y4
+q6SmrbO+eD0vKCUpMkzcvrOsqau0x+pNPzk4O0Zt0srEvLu9wMnU6WJFNC4pJSw8Q1XEt62n
+qK+6zU45MS0rLj3txrixsbGzvt1aRT4+QUlW3se/v8HBwMzm72hPSD0zKycsPEdW0cK4rKmt
+tcPwTD84MTA1PFXPvrm1tLW7w9BcR0JCRk9g+tfKv7y/x9Hb6WBIOi8pKC42PE300LywrKyt
+sr3N71E/NzAvMjpHcsq9uLWxsLS8z15NR0JDQ0ZOaOba1tna3fFeU01KSUpJR0hMU1dm6NPO
+zs7OzMzR2vNhX2386+Hf29PO0NzteGhiaWZYV15fYn15cuzY1tzd5u7vcV5WVFhaWVtdXltS
+T1VdbPVxYm/u3dbW3vHe08/O0tjf3NnW1trd4uxvaF9ZW1teXlpibG98f29pfXhlZGlcUlBQ
+Vlxkbm1qcuzb1dTQ1dnc3eL8/3764drb1tbX2uHyfWxhW1dWWF5mXl5q/uvf2Nvf5vtmXlxd
+WVBOTE5YX3zj3NnZ2+Pp5ejt8X388+7p4NjU1NXV2uDp/2thXmReYGpv7eXh4OT7a2hjXFlY
+U09PUVVaW15dZW7t3tvg7ePj59/h3dfa29nU09jZ6HJucW9oaXF/8ezt73xtX1xbXWp2/Ht6
+9vd7cWxnXllUUlNVWV5dX2dw+erb1c/Lzc/X4e54ZF1gbfjq5trUz83O2ev/aGJjZ2ZkY2Nr
+en5vZWBeW1xbWldVUEtLTk9aaP3f1tLQzs/S1dzl6u3t7uLf39nV1drd4fN0eW5pZ2NdX2Rq
+bHR0bHr18+frb2VcWlVWU09PVFZacPjw6+jq9+3o7fjs5d3b3NzZ1tTS1Nje5O52amNhaH34
+bXr3f+ztemtsb2tvenFnXFpZXWBeX1tZVlRYXGFjav7w8Orf3t3Y0M7NzdHX3eDi6Ozq7+zv
+fPx6bWlnZF5eX2hjY2Fna2dw8eTz/PHydG1uXVRUV1teY2FibH7o3tze3t3h5ujn5+Le3Nzb
+3N/e3+Ts6PJ1bWlkXFxjYV9oaW599+zt+X50bmlob3hyampxb2VjXVdVWmNtZHbl5Obi3d/j
+5OXj3tzg397f3dvd4eXm6npnZGVpamdpa2llbG1mbX15fO3s/WhYWmJgYmJkY15cZGj992lp
+fff47Ofaz83P0dDX2+bq83Rxb21kYGdsdPfs4+nr5d7Z29zfd11cXGNyd2tWS0A5NzY4QVfd
+xb25uLq7vMDM6FlHPjs6PUVb3Me/u7i5vL/H2PhcTUdGSE1Z/djNycvN22pLOC8rKSsvO2LF
+t66sra+yusXcUj44MzAyOkjqw7iwr7C1vMncZk1CPTs7P0dZ3Mi/vLy+xM3V3V0/MywnJics
+OWi8rainqKuyvdNRPjQuLC0yPWPHtq6sra+4xNpeST47Ojs+SFzbxLu3t7u+yulOPDErJiUm
+LDdYwK+qp6eqrrfH/UU1LisrLDNH2bqtqqmrsbvNaEk+Ojg2OD5Jas29ubi4vMLL6E48Lyon
+JikvP964raimp6uvvNNZPTEsKSksNUjQtq2rq66zu8xvTD87OTg6PkvjxLu0sra6wM9sQzMr
+JiMlKjRRxbKqp6eprbS+1005LyooKi46YsKzraurrbS9zWdJPjk2NjpBY9TCu7q6u77J2U48
+MyooKiszSO2/sq+urrK4vMrsVD83MzI0PU/jxLu3tbW3u8PV/k9DPz0+RE1XbN7OysbIztfm
+b1hNS0tKS09TUU9NT1NWXm/z5N/a3NzV2tfOzszJy9DW4O7/b3B2e/7z/m5oa2NjbHzu5eXo
+5u9xXFJST05UXmRob/NxbO33bm5taG5tYmtu/d/p4tva2dvc4eTl4N/o5+p2bnpmYHv+dOzr
+8O9zdvj1+3Fvb3dx+3p1c3BiYl9bXV5mbHf87fX4/HFycfvxdXx3dP/s7eHW3OPe3ux8e29t
+Z25vevnw+u3q/ejo9fj4fmtuaWhraWZu9+no5OLo4ef3bWJaV1VWVlhfaHvn5/Xr4Of79fdt
+cnX49Ofh39jX2tjb5d/b3uvu/GhlXFpbXVtfe3t8+ubwen1pXltZVlRXU1Zp+eDa1dXU1dXZ
+6vN+fG9rbWhmanv+5ODq2t735/L+eHf6dWNoaGdpdO716OHi63poYl9ZVltdXWRrbXjl5Nzb
+7u30feztdvrx9+7z+/LpfvbmevTmeG5+aGR1b/nl6fT083z1fHlka3JuaGh8Z2bu9HTq3d/r
+9P5nbm5iaWZ873pz/+ru5O5s4uVhXmhve3T++P79+Ovx6ujf3+zr+25vdG1ndG5+7vfs6ens
+7XdjXlpfY2NiYGdub2/57Orq6erz9W9s+u7o3dzo6ePg3+ft73/zdW5pZWNlbW5qZ2lnZ216
++35+bGdhXWt3evzt7+7s5N/f3eDg5fX8+Xt4//x9/Pf6dnlucXfyeG1ybF9U3+tg/ePr9e36
+4edrb/lzbV9jb2Boanh1e+7r5e3v4uP17u98eXN6b3h+dXX/7+vi5P3r7nt5dHJqa3Tv7Wxt
+8uz78u5saWplZGZrePltcH59+Px3+O/x7u59d/v9e3T65ero5Onr6uzt+Pf2fHtucXV8b2dw
+bW7+8v5pbmRodnD87/H8/err83tw+vry7fZ8fvb5ee79/e3/em51aGp2YGBtfO7s5+ru7/np
++vzu7vjy9HTtfXl5dHr89/f/bGphYWFq/nFu+vL+8+96d+3x/X76fHbx//v39n35/Xl7cPz3
+dv/u7vP89Ov193Zudn11a3Budn7wa2RscP74f/nz7+t8aW5+d/F3Z3N28nj19Xjz+/l2enhq
+eXju4OXqdnl7fPHv6nt+83Z77fLy+Pf5bm1ye2psbmV0a3h0aWp29/f58HxvcXV5efLu597h
+6enp73tl5Nly9O13ZGtjduxybXhtZ2dlZ2lqdfvy8+jl6Ozf4e/wauXsbHn59nzfemFkXVVt
+cmD2++/b0/v65m1qXmheWmh6+NzU09PU7f3e4/1kXmFYV1NffOzj5ef+6ej3fGtlZVtdX2Rl
+Y3h8+/Te3O7q6Obi5+bu6Od17+zw/Pnr/ufk7m5u+W1neP94/Gpofe17ZWViXV1aWlxla2Rf
+aHFu8err3tzc2Nfa3tzh6uPh4u5zeP7t+3z36vJvdvDy/fd5ZW53ZmRaTkRHTktJS1Nh6drQ
+ycXGycrN0NXldmJWTU5QVVlk7NzUz9DMy9Xi4N/3amlpZV5jaWtv9/J0WUpITUxDP0NKVWXz
+3M/Jw7+/wsrQ2ehuXlxfX15fdd/X2NjX1dbe7vl5aV9eXVdWX2ly/PD5ZlhOTElGR0pOV2X8
+38/LycXDwsnP1+XseF1RTU5RV2Ny89/XzcrKzdHX5GlebW5bWWBucHL74d9mUEtKSklFQ0ZL
+VFz918zIxsTBwcfP2ul5ZltVT05OV2T95NrT1NHNz9fc5vptcmpfYWhz8eDf4uVpT0hGR0RA
+P0BHUl/mz8nEwcC/wMbR5fRtWVJQTk5SY/fj3dzXzsvNz9rv/G5eV1RXXGXz4N3a19vkZk9L
+SkdBPj9ETFr32MrExcTExMbO3e52ZldOTExLUmbr2tLOysXGyMzW53VkW1dWV1pdcufd3fVa
+SUE/QUI/PD1JYNzJwb27uru7vsndZFhPS0dEQ0RJVunSzMjHxcPFy9TiaVROTU1OUl3r1srE
+z2dCPEBGPTQzPFLmzsS8t7a3trnC5lVNS0M9Ojs/RFHhx8PAvbm3vMnfe1xNSElKSk9s08nH
+zNbqVT85OTo4NjdF4svGvreys7i9xN1VRD49Ojg8S1xx3sm8t7i6vL7K5V5PRT4+RE5YYd3G
+vr28wnw+Nzk6MyssOWXTy76yra2usrvUUEQ/OS8tMj9NUGrFtLCytra6xtxqTD47PkZOVvHL
+vru6usZzQTQvLi0rLTdTyLy3rqqqrLS+3Uc4Mi4sLTM/Yc+/tK6srLC7x9lYPjU1OTxCUOrJ
+v7q3t7i9xNc/KictLyomLWS5uLetp6aqssJSOTUxKyksPO3Kwreuq6yvu9RrWkU2LzE9UWLn
+xrq2tLS5xM/vPS0nJSkuLzBHxLCrqqqoqrDFSzQuKyssLj3bvrevq6qsssVaRDw2MDA3RFzg
+xr28u7m8w9f4X01NVVhBKyg9WzkxQci0tbSwtry5wk80MDo/Oj5Yz7mvr7a7vMZvTEE7PEVJ
+SE7dwb/BwMbHv8hZTWFHNDQ0LSw3T15sxa+srrS4wdtcQTMuMTtGWdC5sK6wtr7PXUI7NzQ7
+TF/nyb++vsLFzW5QT09NT1/u18nEzkcsLD09MThNzrSrqrC8v8dfPC4rLz5ozsS7sq+wutZU
+QDo5OkBP9ce4t77EydDeYE1OTklV7WhFQTwvMUNKSPDGvLCts7/R900+ODExP+bFvLWztbi/
+7kE7OjxCS1zgyb++xNJ6X2xlTUdQa9rNy8rGw8vbcFA/LSQqNz/nvb+6ra2zu/84MDY7P1H9
+2rqur7S/ZT8+QEBDS0xbxre6wtJubuLqZVth59TPz8/NfjwxKCYuPErWurWuqqy2yE40LS4x
+Ok/cv7Gqqqy240E4NDQ2O0jtv7e4vs15TUI/P0ZX4s/Hwb/By91wVE5QT1BZa1w/Oz08SvLa
+0MjKzMXGz9PkWVFfff3c1+/p2N7z7nBhWFJg6M/Iyc/Z39nc6mJSUFZsZWR3/+HoTTs0MTVB
+ccm8uLm9wcnXZUg9Oz5Ke8m8uLe8yOlaSkFAQ0lg1sbDx8rZZ1ROTFBj+Oze19HLxsvV315P
+T1JJODIvLjlYxrSusrm/0m1dTUE+PkRXzbu0tLrI+E9CPT1CVPfay8bGydDqaltTVVhe8dva
+2djY29nb9GVSST0yLi4xPnjAtK6vtb7VVklEPz9GU+PGvLi2uMLiTT07Pkdd3c/Ly8/b63hc
+T0xOWfvWzMnM1N/2bV5XTktOTko/Ojc2PVHOurGwtr7OcllTUlRZXF133MzDwMLP+FdJRkdQ
+ceDb2eHw93Nsdfrs5t3a3N/k7np1b2hcVFNbY1lLPTc0Nz9Zybiwr7W/1WxRTEtNVGB+4dDF
+vry+yOVWRT4/RE/329LR1Nvf7G5wbmFpZlxmfX578PF0eHpvYUc5NjAyPVfItKysrrXG9kxA
+P0BGSlj4z765ubzF7ks/PDs/SFry29XP09ja3uL8a2phanz06d7Z3t7ifF9UUUxBPDk2OUNi
+ybixsbS90G1NR0VMVmnh2c/IxcPGz2tNRD4/R1rr0szLzt75Y1lZWVpt7dzPzM3V3/dxaWRl
+aVE7NjEuN0bdvbCvsLXE4VpGPz5AQk7rzLu1tbnA2kw/OztDTnbaz83Lzdv0ZFpYVltjd+bf
+2t/i5nxpYmBfZGFTRD06NjxJesa3s7a6yN58VU5SUlRf69vLwb/Dyt1aTEZHT1x76+Ho7u3y
++2RbYGVv/unn5OP5e2RZX2hucG9eT0I7PT9O58m9u7u/w87peVlNS1Fj7s/JxcXM225bVVFZ
+WVdaX259597g6vz9dG1sb2leW1xeXWBcTk1YVWji39zb5Xf57eLR0tnZ625ubm/r3NnT0tPW
+3vBtamBdYWBmbnduamxseXtval5bWlZYY/Tp5/FfVVZYafDe2drg6O7x9H1ubnv04tra19La
+3eD7aGloZ33l4uLucHBsYltdXFxhZnnz7/F8d21nZmRmbHfw8/rs7HpvcWxx++3n6Obg5e3+
+enP88vbs7e99cXdtZWl0++vm3+Hu8fR1ZWJlXlxibHPs39/h6vlrYV1cX3nw9Oz18fFyamZn
+b37m6e3g29/t+m9ud3h1bG11+fj99eri4+7x83/z9PtuXltbX2Z09/Xu6+3o7fp1dWtxfnz7
+++/u7XVudm91bnRqdOns7397a2pvdf3v6ern7X759PPs6Onn5uru9fdtYmBeXmRve/Hp5OHr
+/H1sY2RlaW1v/u7p4+r7a2JkaGpu9eXg6O3r93x9dnFyfe7z+fh7c//v8f7+6u7y5/P19Pt0
+bHr+c/ns7vx3fGtmYWRramZrcG5wd3ZveHd99PDn5OPi5en0+nJ09vjn3d/l5Onv7npqamlr
+cHBsbGFhX19jZWlrbXL27PP++/Dx8ens5+Xo6ezs7ers7fP0fGpqbWx69H5sZmlmZmptd/f8
+cW52ef3y7+3u6/N7/O3p7/z7dHvu7e3u+PT3b2poZGv783t3+PD7dXh4fHp4bmt3/n3+/ezn
+6/d6cXP+bGp3cHF5f3x1+u9xanX59+/t6ur69PPz5+Hi397reHFtbGlnanZ9bmlmYmdnbW1n
+/vD1+PN9een6dHv+7Ovu5uTr+nJ09f1ufO3v93xsa3f+f/948Pd4+nxzdm13end5eHB8fvzu
+6u/3dnd3amd58fz28O3k7Hp9+3n79HTx6PJ3cm9wbm138fdtcm1udGVq/Obr8erv8fJ77u33
+9vVxevLz/fz+cHF2fntyc3h4d3l2b3h9/vr0fu3h7Ojq9vzn7WRof2x7+2dnb3Bzb2dpemhr
+7nvp5+zv9Oj67O/k3u90bf7v7XVu8vZ6b3L3fHxucPduYVxfd21s/vbu//n3+PHv5+Pl6ex7
+/O95aHv+8/pu+et3Y2Zic+9yc/X8/vh5bm14/O/p6/lmavbw9vTx5/hmZ213+Ov5/Pn96HBu
+e3vu6+n/d27++G/6ePF7bfVvdOrtbG95fP9z93z7/W/7cGzn5O7t7+93eWtfevt1b315/PFu
+9/p1933w6fD/9+ny+2xr8nX66O31d/Hn5/J+6ux3ZGF1bFxbbPt4bn3g5PRuZXTx7vP9+/Zw
+fPJqZnrue3J8+O3q7ezp8Xl87f109ud1c3hy+nR+8X5vbnxuZ3JpYnV7ePbvfe/j7Pv5+P93
+9n1s9nL8+2fx4XtscW5u9en7evPu+Xp1ZnPs725u++nneX3z/nhv93Vu7fVtbGto8Xdg//Vu
+8OHrfPvr4uh89ndlfelpZfrr8v15+ezf+Gt2Zf3uaWRwZ/LmbV9u6eT3amh2dm3+8e7s7+j+
+dun1bGxvZ3vvbvrz++fh5upucO/r8Xpv831q+nJcZ/v2/21t8ubo73N96OpycnF1Zl/xeXT7
+dfP9ZmVq7+RfZOnr6erw6eB9dd/1ZPtycvb+/Pdz9+ZxaefmZm38+OJ1XWr0emhqdnB+925u
+ZGRueOnw8Onr6+bx6en39XVrfnxtfnB56ehya3Z2/XB+7fDnfG79aG7yb2r27PX1b273b3h3
+ZHB+d/p5Znn5+vp4cHjn7f7i5P5+a3ru7u/rfn3tfXR0eXV58nlyamvv9mty8u72amFv+flf
+YO//dnl67O989evy9/vu4evv5+zxfm9y+u9tamtvfP1+a2Fn8eplaPJv/u516+Zra+vz7N7w
+a2xueevveHp5cOrwWmLx6er5aP7g+GViePl/dXZ6dvT3+uzd7nt9cGlv8e3+enV07/1odHR5
+bWBz5m9f+t3o9fl8+/Px7u/w/ennf3P1dv7ua19/c279/vPvc2/6a2FhamxoamL33X1w8Pfl
+3vJ45uN45N52+N3fc2x47exrXVz9/V1eaGf5a2Nt831y9mxze+nd733z797ffP/m6+98a+Xq
+eW50///7/Hn1dGFtb2Vjfn59eGRo4exvfu7j7mp83elvcPt9X2B5dHRs+dz29d/0++5ya/Vm
+X/t+bXPz9fb28Ozr73/65u3873Zs+3xtev14fnZqa213a3Dd9mj14+vo6XHz73teXnP8Z15v
++Oz6cel+aP3ubm59duvh+Pjg7P3z6Ob/++94ZvTt+efuZHT3bP3+Xmf6Z2pja+/y6vl7a3B9
+7/9o/3ptb3L9fW/35On49e3q7/Tq7u3i9P3n7P3+8fhv7Hlh7HFY/XRcXWVmaV9cdfX+Y27u
++Pru4ODj3d7t9efd2+Lv7/n0eWBtbGBpY2XvY1luZ2BmcP7s/md43eH43/hj6+l76/Fx6eht
+e/1q7N5vaG/v6vFja9vha2/0d/Loe2j99njmbV5vdWpoXFho+3pqb3j8fGlw3uh83uZt7+3+
+2t115tfi5Op87d3vW1l6eVxYY/hsZWxkXvN/cG5oaW3reGT57vXg5PL35eR6duzq8Xl/8PDk
+7PPkeGzn521cX3/rZFh+3Xtid+7ecF7/+11c+mxo7/P773t+6tvf729cc/H49P557t7kePfs
+53Jl/nxx92pm8u708m5kdun3WGfc5mJr5uL3bfDjaldt6PBqYvXc6Gr843r07W3s7m76cmt2
+cnD15nP+7Hx6b2r0fmRlbe/kdmN3dG9rae7j+3bs6+z87tzi4Orp5e5ub/H1eWdncHlmamlo
++nxsaWJq9u/27uhydO38b393dvxwb+zn82xf9+JfWe3p8ubo3dv/euXr/m529+1jWnV0b3Fo
+9d53/eVvZur7Yn10Ze57XWZ7+vxyeuDld/ru821t93hpfvPv5Wxn5O7r7+/e+WT/8XJw7u74
+fW52+2hm92dv925zbF1v63Z6693f/v/veX1xZm5rdvF7env07n37d+fmZnvq+v5mY+3sb2fw
+3O7/9/bp4ntx5PtfafhtaG1dbuZzbu9+/el2ae3vb+t8auLxaH3q7nBjZWxvfnZ57Op2eOrm
+7vx16e5nefN06uRycOJ9Xv3zZ2H+5f9lbu3d433s1+Jren1qbGBcaXdgad7rbWz273Z2ZW7v
+eG/x/ujodPXj5P15++/3cnvza29++nly8PduZPzwa3D37/P7bXze6X7k7WluaGZwaV9063xt
+9d/k+ujndfr6b/j7a3jueW/0fPryde19ZvP6aHD6fvrq7vX9+fxvbGthZfPpfnHk5vv0bGj8
+8vp1a3jt6/9zf3z6/W3z5+z58uju7fT4/GllaWZva2Z37t7e/Xrl8nXvfm52d19ke3FnbfTx
+fnNydPv7ePbw/u3z/+Tq+vDt7O3z+/fk7XJ0+vNsamtpa2txaGhsdXBt739p+Ot7c+vwbm7u
+7G/z6fb28PTz9nV5+nlr7uX77PH16+9vbfp8amVxcnx+/vtyenl4+/T/dnRx9fL5enf78Xv7
+5+/9eGpp8/lrbXn5ffrw/ObuaPnrdmjy6n7y7fd39H9+6/J1/fpjaHtva3vv7fj7cWpvc3t6
+c3H27H1sfv1w+fR+8/57fvjl6Ono9X7w/G/48Hz98GdieH1wa2tt/P9zdXxxdfX1fG5t9vdv
+9uzs6PJ97Ob8fe12aWxv+/r9fnJucm/17np99/L3eG9tb3h7+O95cvf2eWxmbXn66+zo4Ov+
+/vH1dfrp7X56b2ptd3rz82tqc3JpZGt2f21udnF9efno7/nk4u3z8v3u5+nn8f3+8fJubHB2
+bWhte/78cGV3+29x9/d8+vh3ffH19vp0bnduePtzc3zx9Xzy6urq8Pbt7PTy6e1ycv78cGRp
+a2tpbW9tenJwefn+/354/u3p9ffyfvjz/fb7/e7o6+fp+nRz/fh6bXN0aGRkaXj3/v73endz
+c3h3dXd6/vD4e/pxanr08vL19+rt//7u7PTs4fB8fXdudHN69PD2dnNtbnh8cnF1bm1vbGhk
+cHx2c3J98vd4c/Lt9u/37+zm6/zr5eXh6O/s6vT+fG5iYm1xbXFwbWhvfW5qZGFoeff2/nd0
+d3p8+PPs5+no5eTvcHH+/e3m4ej9+O/7dHr39Xh2dnJyd3VqYWJpb21pbW1wefj38PTx7PDy
+6+fr+/Lu8/bt6fr3em9++vz08nhucHt4dv18dnh9/X17/u/t7vjx+XNzdHFvfXtuZ2pwdu/r
++fr8fm53/Hd+7Pl37u/18vDq6Ovt7/JxanRxZ2lxcnh88/Dt6/h3fn93c3n48/52bm99enFy
+dm589H7/9f588PH+e3h6d2x08+7y9fl8d3vp6HVv+/d4bHLu82lkbXVqafbj6e7q7f/7b23q
+5Pd2fff8d3l8+ux5ZWVua2dqfvX9bW57dHj77u/u7O/w8PH3fXx+9OnteWpsfPH1e2lqcW50
+/f5oa/76fXbu8Ph4au3k8evsfnZ4b259fW1z+X9ye/v8/Pz76ePx+Pb3+/tvbHRxcG13fv/4
+7Ojt7eHm+3F5bGx+cmVjZWJjbXb36v587/R4bHr5f/z87ubn6vHs7392cnbr4+rp2dLY6ubl
+em9+YFteVlFOVlVHSnPsdeHR09LddOjdb2T77+jmbmvd3fZ0/+zsfHHq293k5dnV4Ojl7XJp
+cWFsZFpbVEg7OFjuTF3Gv8vXcOrRbE1q2vx3fHnazdneztTrbmfw2d7x4s/Q5/z26nxQXOxw
+695ILC9QSDlbvbi6xtnAwkg7U3xbWF7qyMne591qSURS5tLNysK/y/NfX15ZXfPa1NDPz9np
+Z1hfWD8sLEBTO0m/trnC49XA+T9N6O9lYnvHwdvh0/FkV0liz9zlycTN32NdblRMZNjPzM3L
+xsnpZkAuMC4rOVD3v7a+vrnRVVhPTF9kXtPGz83Jz95oTVLydvXPx8jL2WxiWlFTZmv41s/N
+zM/Nzf9QRTAjKEFCRcSurrHC9c7nPTlPYFVaa9C+xPlk1tdPRW7HyNnPvbvOV1bh9k5P38DG
++9PE3ElLPS4tKiw95NrHsLW8xdRVWUpBTt3m2cjCwcl+Udf4P0jezMnP28C/XUdeYlNOUvrK
+z9jIxczL0vDqUC4uMSYrXddvtq23usdTZt89OVzj/M/O076831hjSkhhWu69u8a/xndVRj1N
+++nPwb65vs/N9zw2LywpIzW+4FW0qLfFbU/jYjQ8y8r23tfFus5a6thZSVrYw8rdzMDPUk5d
+XFdW78bEzM7Ex/BaPzQ3NSQj3sRE26ytt8JN3MFHLkzcYFnqzri57HvO4EtFTPbM5urAvNRc
+V15iUUrhv8LJw8DE2E0+NzQyKCNdwUf3rq25ulllzk8uQ/VVTu7RvbnO2c3UXUtOa9P55cW7
+xtrg4+hYSW7J0OXZycxsRT4+PTQvKzbc7knMsbO8xuHFyUs7T3JTUGL60MfKysS/zvNq52FT
+Tmfd5nvXx8zX5NznalJW/HZTSlhnTzw5SFdFP03h1+nl0MfGyM3O0dd+W1Nbavnr3s/Kzt7q
+5+R+YmVv+vlsb/Hi5ePt5tbb5Ojj4+pfUUlKSkE8PERKR0lg2MzKxcPFytXyaVxYbfj+59fP
+1NjZ0NHjdu/n8fbq2NDP2/7r3ed9dvp0aFtLPDY7Qjk3RWtf+dnMwb3Ay8XL13xbTFdoV1x8
+2tna3MzFy9XX1drraHHu7P7u4N3wbuPS3V5dX0k1Ljc8NTVH383Av7ivr7zK12hHOjY3Q0tM
+b8W+wL27vsbTdl5TTEtXb/LXy8TEyM7MzPVbU0g/PDkvLj1EOUTRwLy3urSwvnhoX0I5NztI
+eG7bvre6wcfEymVFSFhOR1Pey8nLwbzH8fvT5U0/QUs9LCtLSzdByr27u7+0scVPe2VFOjo+
+W3JP4r67xcTHw8tdTmt1R0xq19DQzb28zNzV019FPkI7NzItOvhCRcK3wr27vrnLSEzsQzc8
+SFrmadC3ucbIwcHMX0/0805N8NHN0dHFws/x3XhEPD07NCwsP0c7Tr22ube3sbTOS2pdPzY7
+SWpwWM24usXHwsLdTlRqVUdOX2z04s7AwMrMys34RENGNCswOTQ6T+K/t7ezrbPG0PFNPzg1
+PkhIYsi+vr27vcTdXnhYQkRPTVVeas7F0NfDw87T1dh4QjY4PDQuN0/+58u5sLK8vr7NWkI9
+PT06Pl/X2s6+urvAzM/ZX0lITEpLTVfv1dPLwcPKzcvN60U+SjwxNj4+R1FtyLy+vba6xNXt
+XFJEPkJKS09s4czIy8rHytTj7eR1UVBfamFreu3h4OTb2u5hZHJSTE1PTUxRXO7s5tvV193c
+3vF1Z19lberk3dnb3Nze3uZ0alxXW15fZnjv39rX0dLU2+Pz/mZVTUxLSEhLVmnz5d/a0tPW
+2uj27vxmZ3X9/PLl3tzd3+Pzb2xwbG9zamrp4+/08PH19vru8XR8cGlmXmJkW1NWXV9eZX71
+eXvz5Nzh8+3l6ujs8eTc4+je2t3h397X1tva2v1WUk9LSkpKS0tNV2r88OHX1dbV1NXZ4u/3
+7WRQW21kafbo2NPU19nf9mxkZFhXXGZtdO/k2trk6OfzZ1tYU1RebnBvbnvv7fP+9nr/fnj7
+8erp8/f0/fPn5ejl5N/i6O90amNcWFlZWWBz8efj3+d9/nhvZVpgZl1eZ2t87e7u4dnU08/N
+0dTY3ODn9HNiWlVRUFFST1BSV1xcX21vePLi3dvV0dTb3eXw9P38bXF9bWpjY29+7eHf4+ff
+3d3e5uvxdWxqbWpmbnh4anl5aWNbTk1SUlFWWmBrfefe19jb293h397d3t/f4Nja3d/c2dnY
+2dvd53dZUk5JSEZGR0tPVl1u8+Pc2dTS09TY3+rq6PT/+XtqaGxmYWJv/XHy5N/c2tze2trm
+6/D57eji3d/sfnNfUE5NSEVHSElPXWT53NvY0s/Q0NHW2dfa5/huZmh09/Xv9vf47d/Y1dPR
+1dvh/WNWS0dFQ0VHSk5XYnLp2tbQzdHW1tnf4uPveG9rZmlv+XZsbHR//O3n3tjX19fa2tzf
+5OTo7m1bUk1LSkhEQ0RIUVxn89rTz87NztHS29rc5OLc4fb9enh4/HlpZWhqcfPp4t7m493e
+29zk9HBbUk5KRkZFR0xSXGlz7t3Uzs7Oz9TZ293m6e338vl2aGhsffz4+erd29rY3N/d2dXV
+2eP4dVxQTEhBPz8+P0ZMUVx94NTKycrJys/Pz9fa3efp5/R1ef5sZ218+ezm4+Tf29rd4uTi
+4eh8Y1lQTEdDQUJER0tPWm3s2tXQzsvMzdDR0trn8fb8fXNsa3zz+O3s5NrW0tTW2dnd4uDp
+7vV0XFJMRkA/P0BER0pPXvff18/OzcvJy87W19Tb4/Nyb25rZW13e3zu6efe2dna1tbX1dTX
+3eH6YVFLR0A8PD4/P0ZOWXfZz8zLysjJy83W3dnje3d7f312aWBi8OHs893OysvMzMvIx8zV
+5XJOPTczLy4vMjdCaM3BubKvrq+2v8zwSjw5NzQ1OkFY28u+s6+0t7m/215XVE9MWN/X1tfV
+1E0tLDktJCxJXnnArqinrbKuukg3OjMrLTZL997Dr6yytrW830xCQTw3P2zj1b+2tLrIz9lC
+LScqKyUpPN7JvKykpa2ztMZFMzEvKyw5XdrItKuttLS50k5FPzo5P09v1r62t7i4vtRlSTQq
+KCkoKjNK076zqqersLXBVjs1MS8vNkzbyruwr7S3uslcRUU+NzxRZXHLube7ubXA2OpJOTMp
+Jy4sKjrl2cOvq6qttLe+WDw9Ny4wO0pa3L2ytLm1t8j1ZU89O0RSUmLFvL++trjI4/ZYNysp
+KigmL0RS3LesqaqqqrDJaU45LSwwNTlF2726tq6uuL7B3khDQz47RG3g38W4vMK+xW9CNy0r
+KycsOD5Wva+tqaaprrfGZj0wLS0sLjlIbsa1rq6ur7TA2mRIOjk+QkhizL2+urrCy1s3MjMn
+JC81LkDAubWrp6mtsbbHSzs6LyouNjdDzMK/r62vr7K+zPBIPz48P0dQ78/NycvRTDpIOCov
+OjAzasvKuK6sra+wtsloUkAyMjczNEdkaMm5uLe0t73E0fZlUElMTU9g7GzfzVlATFAzL0A+
+MT3a3Pe/sri6tLO+z87iSDxDQjk+WU1L0cbOw7m+ycPCzdbW4l9ceVlNXltPVko8S0cvOWc/
+NHHGWW65u9G/s7zPxsN8Tm1lSkpWWk5V6+r83MrJyL7Ay83N2WZcVkVBQ0RDSl5ST/dhTFn0
+U0lrb1Bi19Xh0MbK0s3K2OfY2vr+/VpYb2dm6dvc08vP3+Hb7GlfVUxJSk5WYGpy5dzi39/g
+7ltWY1lLU11PVXBzdeba3dzT1drZ4+7t9P325t/c29jX19nb6W1fWlZNT1RWUV73dfvr4uDk
+293r8OjqZ19oX1xreF9p821gXF/m61Zh2/Jb5tNoW8PDXdu911Pn2lZKXWFPTmvo/njh2d7Z
+09flcGVeVVhdXGBpaGR24t7m6ftraG9tV1Rka23r3NnX1tTd5Od8bGhaVWFoZ/jk4+Dh29ja
+3uB8X11bWk1Lc9lYU9bPdHDSzfNb7t1oUl7+Zl/v2uLs4+f1/GtmYlxaW2RkafPm5eTc1dvj
+3uP7bWtvbGRdYPnp+Ora4u7i5XdqcmtdWl1n+H/07WlWXtbNa09t1+xUXeb0WVrp2+X899zd
+8nXr4fF2bm/5+Wx84+Ty6OHf5PpmY25uZm/0bF1p62xZavd1ZGj7fl5ca/drXnra2uve1tno
+3tnneu/vaFhf+GhaZHvv6mxw5+t5ff3x5fljX+Lf/vtyZXJnX/Lr7v743m1e395vc2Vt7FVh
++GPmdn7k6P9q6O9W+NZu5edPf894XGRe6uZkfc/SbWnv5t5nWWrSW07XYnHmXO5u3WVZ6OD1
+UnLdZ27seHr1/2TZbGrZ+PtyX9j0dHb+/WbpX2vz+W9izvNqd+zea2rdX356V+/YbV3U7WFv
++tFpXe7p8mZtbnz9YVXs52jgaGDn3ldt42pa6udf/NPi++xn33702Gv3c9lccOLkb2TifnFv
+Y3v3aVRh3/JX+310anRu7dxvWOfb+HLk9+B2325z1nhe3ONe8Pt473FmVePjV1zXbW3d9Xrg
+aWbseGRp3Gxkbm7rePXq6+3d5vxv6uV0WGzqdGL4d2H+cHl9du5s9HX1/nx1cenrcG/l/PR7
+9+h84fVt6etxYG/pbmBw/HV86/hfZXz9afTu+3PmcXzmffn9d/p2aOrl5u7s6/5r/G9fcnPv
+5mxq7Hpk5vN4fe5mcuphYO/67fHncfneeG1m7fBk+fJj/vprafJn7+94enD57nT3cuno8ezl
+53nn7Hj4b29vd29h+XXudWTu4WJo/GJ+b11l+vh+7u3o5/jv4/f+fvb27P18/Xdpa/zs6OP5
+bu3qYnd7Z2xp+Pv7e2Jw/fVtaHXs+G1wb3f5b3fq8vbo/Oje6Ofnc+vm9up9eG14eG5ucW37
+fHFoW2ppbnJq7/t7bvt66+tx8OTq6PRv7eLp6unpe/x4dmJtY21wcHf3eHx+6/V9+ej3+X99
+e3J3/Whje/5y8PRue/57+nx5+X/n/Hp+7u3s5+/8cnVud2l4/fHpaW73+Pd88mx3fmdtdmXz
+/X3tfefu6urt6en6bGhxe3dqZmt3c3Psdnvr7fz18fv9bvTj/njxeXF4bWdxbGx7/f31b3vx
+9fF1dOv++fJ27OXh+Onr/njv7vVv9O9wbnZpanFtc3N5bvBvePNvbGlraGlpbHf99uPh7u7n
+4ufi7Ojnf3z2fm98YG/vdWdsdHH58/r8fvXu9nl8eH7y4u9rc3r7/fF3dHxmc35haHBvc3v0
+cPX3eejz6vPt7/Hn3+rt6eP5bGdrcWx4fXP+aHN+aW9udWxodPj+bOTg+Pjf8Hj7fH54c31/
+eG56f/L17err9XFraGh3b2hoa2198ebh6OHjfu/mfW5tfnT+7vtybnFxcnr2anZmZ/t8ePfs
+dfPse330cf32fHr1cnX/du/t+u7s7W97e3Rr/XFx9m5qe3J17O736fpqcP5ycfTo+Pjr5/l8
+9/D+6PRnanBwX2R9bX7o9nJ69HJucmxucePne2757uvr5+X48fJzcHry+mFmb3Dr6+Z47u12
+fvL5c/tsbvz9/nFpfG9uc21obXdtbm128/157fXv5OPs5+zi5Ono8/j7f3b6/nhza3FybnR4
+cGhjYFtsdft2em987vvo3+P98O79ff/7/erm6+nm+2hscm328Xlobnhtcv59b/t+bf56fX10
+c3717u/29PL27+rn7f56dXVvevxyb2hvb25+9HBubGv27fLs3uHm6+zs9+z/cH50bHdkZ2pr
+bW36fHjv8Hlx+/52a2hrfX/17/Xq5uzl5/nv6m9mcP566+T+cf709nZsaGp2b2t4dmxla/Pu
+8+rr+O/u6un6f/F2dH14dPj5a235fXp9e2tqbWZsefh7fffy7u3q7Or1cf3u/3X0fG739v55
+eXBtevrz4OT09n5sbHN2cnx58fj37v5wdW9xffL1cHJzdnN0dXp0bnB+7ufj5O/u6vL063Zh
+Z25jcXR3/vju7/Lv7/d5ePLwcnVwbXJxbnpy7+308ffx7Ob5bWtue/no7PH7/3tybnJkY2xq
+Ynb7/nd7cnbr5u3v7/b77vj++f39/fv3b/n7cHp9dXN8dfr2+/bn6nt28vry7vX1cGpucGtt
++35za2/9/Xr3+W9rdHx9fXf0+e7p7PL59fp/+u/y+PZ9fPb/dnz9a29vb3n++HxtYml5+nzq
++3L87vDw+ert7e9+7erxf3t7e3twdm5ue3t5eWtvbnV9b3tufePt8PXn7Hp+/HD/8enx+v5z
+ev1vdPt5amZye3jy8Xdy5/bo4+/t8Wxtcn5kbXdzeuz57W75+/p3fPfr9/b8/H/86PBx9vv9
+9PV8835ufnRvcmRmamp+dnjvd2p+6vD87+19fvnw8ero/Ozt+/179nR2aXp5d3n893V4ZW1n
+bWltbfr9dvvn6+/e3u7b5np8fHxtfHlvcn1+/vp1//RrZXBqXmN1aXb95O578/5sfXF8c/Dl
+6/rh+f/07+r1/Xdw+3RhbXju+3vq8Ht2cfLv/PD/ZWv6fmt5cH1z9Hx3Z/7t6vb19PLw+XB8
+bXp7dm5tcfDy6+rq+/Tu6efr/vd3anz8bGRmamxdbG1qbnNvdfvv7+/5duTf4uHf5+7sffhz
+92xy7Gdu+PFsZ3dvam99fH1renNobH/o6/f05u7l+ut66vd+/f5+bmVhX2J3/Pr+/fzydevz
+6+Xp63ft9/h5c35ufX78anT78v75b/1xbm13/nL89/n6cf/rd/D2ePz1+Xzt7vP6/Xn9bGtp
+aX5rcGh3ZHnk6u7n8Pnq7vn3+P/q7+rv5+z0d29yb25nZ2xvbXNsbG37d3Jt/X558O/5+Ht3
+6evm6ujv9ufl+/f79WtwfnBddG53Zmp3ffx7cXL8evF28/vp6uX27e7te3V/+376d3ttbH72
+e3JqdX31d3H/7vrt737u7vluenf+/vH+d3Bvdmxz7/X9/Pz5+3h2fXR6d+f28ezv8PHs7ufv
++m1lYW9ub216bnx4eHj4bezv5nzlef9rfWzx/Ox38v3w9/T77u/0fXdreWpnaXJ7d2537vf6
+8u9/7OzxePt16u74b/1z+f7r+/r45Hl1a3Zoc3V1am9z8O17a/vt8evt+XZ7/W17bnhze3v6
+//5rdvnt9/Z693Dt7vF07P3mdXdq93v2/ed77u/x+O/8/W5rbHFuYGtmcm33++945ujm7Oz7
+7XB5bfB09+nv9XT+fHZwc3ZsbXBtbHdo6H7tcuXq7eTf53XsZndlfW75Zfhn+2pwZ3tu7+vs
+/u397vzyd+t78Wl+bvd97+rj7vDrdPtl+Fx8aGlpaH1m8mbuef195Xfj9Ozlc+Jd5F/naO1m
+8/b2dl/uZuT6+m9e7nZy62Bt+OHT9urfd+3521lgTFlsXH7w7vHf3N7s8HNrbG9eXmho8vfm
+7Hri59787HB6attw/O1sc3bi+X3iY/vrauNUeGZpYvFnaGbrfHrwef74/ODsc+Ff9exY3m7f
+eezW699nae1t9/pe72j/5Wpqc2D3b2r2aGz+bvj3dfrn9PV+buV67OZ2+ft+4t3v625u5XVp
+c2lpZmx5bX5qbm/1bWLvbnB5bPbv5uHv8ePu7+js7vl+bWn47vv7ePj38e59+vru7m54e3D6
+dmZeU1hWVlNWZF1tffLa2NHOz8/Z2tv6ffj1/Pji39zUzczIycfL5FY4KyksLzFCZfDGta2s
+ra+702BKPjY0Njk+TuDHv7q3ub3Dzu1RS0dHTlnz2crAvry7v9dKLyQkKS0zSPjXvK2oqKux
+wmhCOTMvMDQ7TtC8tbKxsre/z2hLQ0FDSFbozcfAvr69w/05KB8jKy457sfBr6WkqKy5XjMt
+Li4uNkdc0LqzsbK0u8jvUEpIR05VWPzOw7+8vcTHy+pELyEdIi48abq2tqujpKu5VC8pKzA2
+PEtvzrqvr7e7xexZU0tCQUxfcuHPycW+urzHzdZtWlAzIR4oNDt/t7m+raOlr8VMMSsuNjU0
+P/3Es6qsuMjQ41pRTD88RWLd0MnEw8C+wtTq3t/j1/MzHx4sOThVxNvYrJ+jrcFNNTI4OjIt
+NVS+raiuv8zJytdfPzU3SN3FwsTKzcnDyN14d3Tp3V4yISEvOzVC2tTAp56jr8JuSUI8MCon
+LEe+r660vby1tcBdOTI4T9zS723iyr/Bz29aW2jk2utMNjA2Pj08P0dcyrWxt7y7ur3NUDs2
+OD9TbGdq6Mu/urrIa1Rr2NHjWk1QZeLgaFlm/ezc1Nnj4nFNRUlTVE5KREZRaeHPy9L8W2Ht
+z8nJ0+r13NDP0dr3a3bo53NdUk5OYuzzbG366dzW2u7z3djvW1FNT2BzVkVET/vY2nZbYOHN
+yMXO7l1i4M7Rf1NLTF7f2upraf7YztTxZGRfX3/rbmjnzMbGxcx8S0VHSUU/PDxEVevNw8HF
+yMXDy9n9U0Q+P0ZNUldm38vCv7/Ey9HW4XRWTVBcbebXzcjGwr2+cTIpLDI1OD1CTdSzp6Wr
+us31UUA6NC4sM0zIt7O4u7q7wtZXPjk9Rlzo5/Pbxru5vMTQ1crBYCoeIiw2TsW/yb2so6Sx
+XTEqJyozPkZYx6+opam37EA4NTMzOEbkv7OvtsXad19o/nJcZMu1r/AoHyMpMu67v8y4qKSq
+uk8uJSUvRV/fw7ewra237zszMzY/We7Twbq5vMrwVlBX/tficd/Csa5ZIh0gJje8rLS+t6yq
+r8FDKSElNe+8sbC4v728yE43Ly44Xr+4u8XR29/pZU9JSmDUysvLxLmxtj0gHiIrWK+rs7q4
+tLS76DYnIypDwa+ttcTOy9FgPzYyOFLDtLS9zex6/XNUR0NIWtrIw8C7t7jPLyAhJzPPray0
+t7i9w9RHLyosOd61rrXG4H5tXU1BPT5O0ry4vdNeVF1093pfVFrxzsK+vsXO1GgyJyowP8iw
+sbq+zPJ8X0M4NztIz7i3wNL4XFtydF1XYeDNxsfUcV9dY3xuXlxaWGrTxsXIysrSPyooLTX/
+sa60ucXpdHVOPjk6Qfq7tLvLa0xMWWlkZm723MrGzeBuW1ln/vT9Zl1m59DLzdTb39/3Pi0s
+MDdXvbKytb7YYFNKQkBCSnrIv729xNlpVEpIT19kbt/X19DP2eb9XF1n69TFubO9QyojHyZD
+uaunqrjTb1FAOjYzNk7Cr6qrt2E2Li899cG8vcfka2ZcV1FJTHLSysbNbEtHSVrZxL/By99m
+XGJnVTwxMjhMxLKwuM9IOTlCWtzMycrKyMrbXEc+PD9S1Luwr7fKWj89Q1fMuLCvukklHR0h
+NLymoqWswllCPDY0NDhIzLKpqK7POywpLkTJt7a5wM/l8VxIQ0BEaMvBv8jtTEZPc9PIy+Zf
+YO/Mvrm8QyQfIiltqaGlr9s3MDtO8s3PeXzRwry900Y1MzpSxLW0u8xaREVOW+zTzsnEydXe
+1sfCfzIkHh8s4qqgoKi/QTAvOEhhaF1t0ruvrrfrNykoNPO1rK6/ZUdFV9jM3GJWWm/azNZx
+ZPjKtayyRSMbGR46sJ+dn63ePjY3Ojw4NDpfu6qio69RKiEhLFi3rKuyxfhaXF5VTUlMXtS/
+uLOytLnSOCIbGx8zuaKdn6rLOy4uNUFNTEhR3ryuqq7DQy4sMkzCs7bC311Y4cjJ3GNOR05t
+49rKuq6rr9QsGhUWH0eom5meq9g7MzM1NTc4Qdyzp6SntkcqJCUvXrivsbjIcGX/4edlTkpU
+7s/EvrqysLl7LBwYGiJKqJyanq3tNi4vNDtBQkveu6+rrbxOMy4vPd68uL3Le1Rb4szHzuZd
+T01PX9q9rqquxy8bFhceO6ycmZylvUIyLi0vMzY/3baqpai2XTQrKS9Ky7u3usjb2tjV2f9W
+UF7s08jCurKzvV0qGxcZIUSqnZqdp8FENTAvMTExOVq9rKWlr88+LiswQvDEu7vBx8jN1+Bl
+UE5XaOfOvra2uso7IxsZGyjkp52anae9UzYtKywsLjlcvKqjo6q6UDIsLDFF27+6uLu+wsnY
+ZFBKR0xbz766t7t+LiAbGh4vxqacm56pumo7MS0pKCo0VramoKGqvEowLC00Q3jOxL+/v8DE
+z+xeUEtS7c7IwL3PQi0jHh8nPb6on56gqLXVRjQsJyUnLkTDraalqLDHUTw1Mzc/TnzPwr26
+u77G1HFaWl5q49fySjYrJSMnMFa5qqWjp622w+hJOC0pKCs3bLyuqquvucbWdFNJPjs7P0v3
+yL68vsXN2unu7u1fRTkvKSgqLz/hu66qqaqtsrzOWz0zLiwuN0jsxby5ubu9vsHJ3GNMREBB
+R1fv2NPW2+nw4dXQ1udXRTs1NTc+S27Sx7+9u7y/xdJxT0Q/RU1e28zKzNHW3Ofz92ZfXl9c
+aO3g29rf92VdZvLXy8XHze5PQTs4OT5GTFRp6NvNx8jM2PFgVFRi7dXLxsTJztTd5fFyaF5V
+Vl5lcu/g53NfWFRafN7Tzs7U42lQSklKTU9SWFxt7+bd3u9jVk9PXH7bzcfDxMjP3OPr+3Fm
+cfjq3dnY33ZZT0tJTFv53c7LzNPkaVRNSUlKT1dfdX3q3uP5X1NPVmro08vHyMrR4nhjXlhc
+a/Lg2M7Pz9Tc7l9PS01PXPLa0M7T3Ol4XlROTUxNVFxnd/lrX1dVWWbu2s7Kx8nM0N94YVNP
+U1hp6t3VzcvLz9t2WlVTU1FZZ/3i2drY1N39Y1xYWl5eXV5aWVlSTVBXX3zd0dHR1dvd3+d8
+aV9han7o3M/LysvP2vhvZFpaWlpfbnN+9HxvbmhgYF9haG1xbl5VT05QWGT76NzW0M3Mz9jl
+eF9bXGNrfuLVz87O1+Xxdm12dWxfYPhybere8X9/b2RdWllbXFdSUE5RWFxlduLl/NHJ0uLW
+1eJsXf91Zmfhz8zP09Xe5HhoXFdZbenZz83LysvP2fxSPjczMzU2NztKbMy8t7OxsLK4xONU
+Qzw6Nzc5Pk1vz8C7t7W2u8LRfFROTUxUauXSy8bJ0l47NC8uLi8zPVfTuq+urK2vtL7bUz43
+Mi4vMzpFZ8u8s6+urrO8yvpKPz09Rk5tz8a/vcXZSTgxLiwrLTJF5b2wrKqpq7C6zVk+My0s
+LS85R3PKvLSurq+zu8fiUkM9PD5JV9/DvLu8w+c9MS4sKSotMkfXva+rqaiqr7vPTjwyLSwt
+Mj1O6MO3r66vsrjB1VpGPz09RU9p0b21uLu9yE81LSknJygrNlDIs6uopqaqrrzrQzcuLCwt
+ND9W1r2zrq+xtrzH51hGPj5ASFBxz722uL2/yUYwLSgkJigsO2/ErqinpaSprrr6PzYtKiss
+LztO6r+0r66usri8zmJKPTo+QUVX3Ma9vsC9xVY0Ky4vKyswOl69tq+rqqqpsMXwRzg0Lywu
+Nz1Q59K/s7G0tLi9w9lUSUZDRURGXN7Zz9XUxMPUYUc4NTY1Mi81SezPxLq0r6+2vMPP8VRC
+Ozo9QUZNbc/BvLq5urvA1GZORkM+Pj9EWHHr3dTMzcvU5vr3cVNKQ0BDR0VCR1Vr4NfSycTE
+ys/RzszV6/39ePL5b2313d3Z5PDu/2RQUE9TX/V8+dzY2+fzev9wbWBbXl1bWlpcduHuav/n
++2JfVl1hXGV67eTOzM7Q0NTc3f5rXWNlXmJYXGXo6NzY3tzWz+Px+GlcVVFPTFhlfvTi7eTm
+7fJuXWFrWFtfaGz66tzZ2dPY5+7+eXVxXWVqbGv9fvjX39TX29/fdWpoXGlqYF1pZmL+/vjx
+6/7vdGtebvFvfHJ+/330amVrc2pj7G/17v/v6erh3eLb2NfgfW1lVlRcXl168uf12tfg3Ozx
++mtgbF5iafLw7d7k6n1vZ1lZXVRRXVlZbP/56NjSzs/T0dvn7n1dUVhYV1x17uDd2Nbc293y
+aHppWFlcXGlv8+Df29/i5vVvX1tYT1FXXmtw/+nb3eDa3+Lh629rXl1ka2zs3d3f4OLc6vJo
+YmxrbmphYnbl4u3j3uXxblllaV9fZ2/w/e7j4d7t9e5xeGdpZWlfYmxwc2z47ujv5ev4/Ov0
+bfHt9O/w7eXp6+jt83N7dWJiaWJefmZlcf567tzl5+nudmd7+3RnZ2hsaGlr8uHq5/Dq7f9s
+9Pn8e33q5fzz7XV9c3lr9/ljaPtzbHl+dHf3ffHkfnbl7Xtof/b8cnBtbG/9eeno5+fv7e/v
+9f50bmt4d2BeZXxzdvzs6u708+/yb/7/ZXJvdHHt733o4unx/Pb7ev9nZXnveHP/aWz0c3Vt
+d/3v5OnuefLs++ru937s7vXv/mZtb3FtaGFod3F8fffv9vr3+e94cf3yfvB08Hhx8XxtY3H/
+f3x59vdtauz0a37w5+jd6Ozk3+Hm8/TufXFvaGRtc2xoaf5qZ2x0furybmhy/HNuefft7+/+
+6uLs6+v6/nB6eW1zefr7am3s9H378fN6dmZfeXl+/3L3+P3o7OLe6Obe3Ob+fWtob2h5al9g
+bm1qemZy9Hv8dm5r++758un5ffDsfvtvdXT3+nzz//z4fXzv6fX17fF++Ox1evF2aPv0cfxx
+X2b8+mp0dnJ96fx5d276bG5p+vPr7OPn6PHu7HXt5+d593Fvb2hmaG52cmtzaW18eP336/Pm
+8O/y/vh8+e/39Pl28fr3bX12Y3P/eWpva2r6bnF3eOv1duzs7/H18+7q9HHu7XB3a3T+9Prw
+8GZpYm5vcnX67fr56e7+8unw6+/6+Gz+/f1rbfx7df359ftsanH9bPPzbmxicmT88Pdt++D3
+9fzu/ubvdvTu5fDw7PHo4eN6/G5qZ2NpY29tZWpzevXqe2VveW1sf3F09G5o+2998ubn6+Tp
+5ebu5t/n+fP+bWpuXmZ293V6e21scnzk+355YmlobmVtaWp19/rr6Ont6uDh3/j86/fs7eh0
+amxpbXBpa39vZ3JuYG/s93b69Px1c33v/Pvi4Ojq7e32eHH/ff9vdnpmfvv4Zm5we/p++Hp5
+ZXN3/3xtdvxwb+jz+O/o5O3r6uv8af1vaHT+/mn1cGho/n11dmv5au/v63168/Tq7/Pr7Glx
+/e1z92xfb+7+dvV0fnj0fun6ZmduaWrt/Pf7fH5v7ubr7+/z9/f3bm1+b3B/7e/5eHR2fvj+
+7fz2fPpwb3JxcWZ86e59e3x2eu56f/R9dP709+fr/Xl9bGj56/Z87Wxpd/fv9n78+fx++fNx
+en38a3B/9nlwentp++/28vb25ef98Xx0dnb6ffZzeG5kYvh6fflr8Hr26u91bfjo+nv5/n5o
+Znzz8e7j73768vNrb255dPfvbn9//vnq6OPudn9scHJtb/94/HBybnN8//lrdvjt7vl9eH/5
+7P12ePzv9/f47nVyfvr7+O/2+Hp+fm5tfnR9+n735/Z0fnB2dG5rcWlpdHt7cvrv+3rk4e/1
+8nVp9nv5/vnv8u/07+zv+ex9amr7dm5tfXV+921793NpbnZ5fHFtcn387eHi7Hn56vP68PZ4
+enJscntxa3N88e7r+//+7/H6eXb7dXJvampyem1tbXRr/eXrcXLl4ebv7+bg6n359n58bHJu
+cvt68nh7dPx+/XlsamVhX2Z6a/r26+rp7Hv8+OPo9fD6eP359u/38e7r9H36a3tx/HRxdm58
+fHZscPR8b3pzZWdpb3Bz//Pw6ejo5ePr+/x9cHR+dG50/Hh4fOrs7ev5/nf9fnFmZW5qbGly
+e3j68/N+/3VtdG179/t+/ezz6eHf3+Dh6ftydH7+/XN8f3RwZmJoZWRpZmtycnNrbHj+/vTt
+7e3q6uzx8+zs+/rt7fP8fW9sbG9wZ2t9fHt98/D68ejo8fb6+nt56+bud29wcWpwf3lycnV2
+bWttaW559Pv68uzr5+nr7vd+fnz8ev/0+XVrdnVub3pvbXn/+nR4f/79+X11/vL1fH79+fn7
+9/bv6/l/+fPv9/P7bm1uZV9o/PL48O769fz6cXr89Pt3eW/7eXZwcnx1b3V9/Pz19nVvevn0
++35+/e35eHH47O/z+fz79/Hw7fr3/m94efxtbXp3dHF+bW12dXlxb2509vH28PD9/+7u+O7m
+8Pjt7/D7b2lmZm14dHXv9Xd4/PDu7Oj2eHhucnJ0ef39+n95c3Bta2hmZ25sce/w9v57/ezn
+6ufm6uzr7/Pv7Phuc3r8+fp5cnx1b3ZtZmv6+H94bnB7/35ubnR3cmprfe/u7+3r6vP8+Xh1
+/PLw9/n2fmly8e3x+nxubG96fnd+/nR4fn7//PP79/b17fL8dnZ9c25qa2//8/h+fn11dv72
+8PLs9O/t8u7r7vfv7v1mW15iYmdrcHF1ev739vfy7317+fz+8uPl6ebs7e/u7vP29ft2bGVp
+bGhqaWxvb3N//HX48350ePr06enx+n18fvb3fnd9cG17/PTs7Pj5/Hz1fXh8en15+e/7bWVo
+bHz4+P90//18fP788e3s9vDp7vDw+/D0fH96c252fXJteu/09vZ2b2lkZ2pr+PDx7Oz6d/v8
+8uvp/mv/6+z07/psbGpxc3b8+vT17vZ7dXd9dffs7fP9b2hven717/R5eHd8ffvz//92enFx
+c2/58ez0c3Fxbm9+/nl3evv++vnx/3l8cm91+/Tv8Hz99evs8fL68e3q/m1sdH17dnZ7em9t
+ZWFod/3+fH307+rz7/Vwcvnt+fbucG15+u/49e3+cvrx8vp8/X16df76dnNxfHx2dGxrZmT6
+eXJ9Yj58uFlUzdNUWXb6xtBj2+1SXWJfauHnWmLp3udn3c/vYVhqeGRhbeDtWv3o6+JpfOzv
+Z3HoeO/vdujya2v9/P7vcWz4fufydPV7amRvbPn5bG1kXnP26OHn637x9vHt83/+7eX5eXxn
+bV9c+Xr37fx4aX7u4vlt9O1t/fju6GR75+lle93t9Wzv8V1ie+p4eu7vbFpk6uXo+Xvdc1Vc
+9Obq9GxqV1lu/u7d295pVfzsVOrM/+TY5ulaWndkZtvb9OlaadZmeNztW05ceV5g3+NpWV1v
+927n0fBf9u/tffXQ2/re32VeaGhp8frp2mNc5ftYfudp5Hxk8+laTXPka/HM1mtjVl7+X9zN
+fmdeX11Lccp+98fhZ3pg+Vdj0tne4tH9R1TqZlrXyPlW6e9QTWrmcF3j5FFTbHJt7NbS3PL3
+bmlp5N1k2dlT7d1e7Mvr+9NyeOtXVPB9WeDdXVJcWU1Xa+D4ZON8Wlt2ZV9q69jp2crc4NXe
+187e2s7t/dj0ZvDq615s7FZQflhJ310+Q1I8M0xNRWHPz9fKv8HJv77Lz83a6d7a2+PVz9LV
+4ub5WURe40hb2nPWTC/pPyQ9Vi9Bxu7Hubu6x9HK7lZp+Xp32MfN08HG3dDO6vrg5PNj3tZc
+dMlu7Lt7LjxOHyNIMCzYucezq6yzvLvTPkRJODxQ/vDNvcHLvL3l2ctcVNfrVfDU6/rSxt7e
+5DkxOSgmNzY157y9r6utsLjFYkY8MzI4P0vrzsG9vbm5wsS/zN3Z5l1e6+/sysXO1Pc9MTYo
+JDIvLlHCybSrra+0utNHPjkvLzxATMq+vbWwtbm4vczS3FVLWE9U7eXSytHgWTw5LiUtLiw/
+3NG7ra2ur7TDbU09MzM3PEZ+zMW4tLa2t7/KzuRdUEtJT2XqzcDMzco9OD8nKDItL17MzrSt
+rrKyut9eTTcyNzk9UtHPwLa3uLa5wsrP+E1PT0hZfPjNxOPL5zQ/NCYxNi1C1Wy/r7O1sLbL
+6vRHNz48N0FYata+uri1s7i9w89jTk9GS1lU3sbN38JpNEg3Ji44LDfmadW0sbiys8Tv6E47
+P0I9RGj90764uLe0u8HF1FxUS0JLTlfWytHPxk8+TS8sNi8uRV5QxLe9u7W8y8nTS0hJPT1H
+Tlbax7+7tre6vMHZeG9PTFhZadHKzcvMTURKLi06Ly1MV0PKusi8sr7Lw8tQTls+PUtITd7N
+x7y5ubq9xNPgaE5OV1Zn1s/R0s18QEw/LDg+Ljn3R1C7xM22t8zHwWZOXkU9Q0xLYczJw7q5
+vL7AyNvvb1JOWlxr2s3X085ISVUwL0AxLU5NPc6/2by0xMe/02NjTkZHTVpl28rBvr26vcPE
+yvFnZk9OWF9p2s7i2d1LRUg3Ljk1L0JPSdzAxL65vMHE0+5eSEdNSkz86N7FwcK8vMXFyNzu
+6mNZ+fTe09XY3fhNREQ3LzY4MztHTnzVyr+9v8TFzexiZlhPVmH96NrKxcS/wMfIy93u9mJk
+/G9r6ebe3/xeTUM/PTk4PD5FUFnv08/Pys3WztDl7vhgaef699XU2MvLz87N1NbT2t3Y3OHd
+19/o5H5pWktEPjs5Ojk6PkVOaeHczsjJyMrO0tXi7u/66Nza2tXP0M7N0dXS1tva29/a2uHe
+3ut9Zk5BPTs5ODo5PENLXnjh0c3KyMjJzM/U2t3d293d2dve3dXQ0M7O0dPY293p+/vzdHtx
+WUxFPjw7Ozw/QURMXfnd0czJxsbKzc/U2OPt8P3u4+rt4N3Z0M/PztHV19re2t/o6e9+XVNM
+RD08Ozk8PkBHUVx92M7Lx8jKycrN1eP47ujv8+Xg3drSz8/Pz87U2dzd3ufw8O5+Z1tRSEE+
+Ozk7Pj5DSk5d7NrRzMnHx8jKztXY3+rx7u/p3dzY08/Nzc/P1NjZ3eLsdGtuXlhdXU5EPzs4
+ODk6PklTZd3OyMC/wcHCytbjeWVjZ3rl2tfSz83Kx8jL0dnd4u7u9G13cGBaVkpBPDg1Njk7
+PkZObdjLwr++vr/DyNHa6WxgYF5i+N/b0svIxsjLz9jd5vZwa2tmamtpXlRNRj87Ozk5PD9G
+Tl7nzsO9vL2+wsfM2HldWFRUXm/p2M/My8jIycrP2uTubWdoaW54c2lYTEU+Ojg4OTs/Rk9d
+5szAvby7vL/FzuNqV01MUl1r6tnPycPCwsPIztTfeGJgYmJoXlhXT0lDPjs4ODo8P0hWd9bG
+vru5u77Bx9DnXk5MTFFba+LSy8jEwL+/xczX63poYVxVVU9OT0lDQDs1Nzk7PkhTb9PFvrq5
+ubu9w9DhZk9LSkhOYu/XzsnEwMDCxsrS3/NzbGNhXFdTTUlFQDs1Nzs6PEVPX9vJv7y4tri8
+v8rccU9FQ0NDS1v+1snDwL6+vsHFz+hwZV5dWVNOTkxGQj86ODs6OT9MV23UxL24tre6vMXV
++ldKRERCRE5l6dHGv727u77ByNf3blpRWF9dVE1OTURBOzU5OzY5RVBY8Mq/ubS2ubi8yd1s
+U0hDQ0ZOXu7VysO/vby+wMTR6XNdVFBSVUxJUk0/QUQ3NT87NUNiV2jIvLu2s7e4uMLkfGNI
+Pj8/Qk9pdt/Jw8O+vL/Cx9Ld62pcXF9aUFRbS0NFPDQ8PzU5TFVT7ce/vbi6vLq9zuPzWklE
+RkVKXv7oz8bEv7y9v8bP3e70Xk9WXE5MV1FFSlE/OkBEOztGTVB9zsnBu7u+vb3G0tlwT09P
+TEtSWmPo0MvIxsfHyNDc2t/waV5iX15hY11UVVZKPj5DPzw/RktZ89fOx7+9v7/CyM7ZeFtb
+W1JPW2np1MzLzMvKzdHc82lhXVVTWGFhaWtnaGFcUUpFQEFCQ0hNWXHdzcbCv8DBw8rX6nte
+VlZbYnP249bPz87NztHf82dhXV9dVlRfZmh0amBfZVxRTktHRkZKT1pm79fOy8nIycnO1uD9
+/X94eHJ/9ejb2NjY19nZ4Wtkbm9hXmxxZF5eZW1wa3F0bGVaUU1LS0xOUFNfavTe3dTR0M/O
+0dna3+z37u/v4eHg29jc59zY7nru92xqXl9dW213de7p+v3x9WNUT0tIRERFR05ZbfHd0MzI
+xsfLztnm83FrZ3j48urf29fY2dbc6Ol9Wltuc2Ru3tTd4drc7/puVElCPjw8PDw/R1X708jF
+wr/AwMPL0eB8ZGNfXF5n/Ore083Q2Nrd3d3xaXHu397h393h+WZYTEE8Ozk2NjlAS1/cyr+7
+ubm6u7/K22lOSUhFREhPXPHUzMjDv7/Cw8jP2OtlYGpvZFxcXFdNQTc4PDg1OkNLW9rIwbq2
+t7q7v87ccU9FREJASFhv68zEwr69v8LDytv6ZFdTVltaVFpdT05LPjxAPzo8S09P/szGwLy6
+vL2/zt/sZU1KSEZNXn/sz8XGxMHCxcjP3vF1X1RTWmJcV1lZVFE/OUJDOjpLWk1Z1sjFv7y9
+vb/M3tzsVkxPT01TZnXfzsrHwb/FyszS4n1uY15cUk9TVlZPT0xBP0RCPT9MTVN21svHwL+/
+v8LKz9vxX1hYU1BYYWvs1M/NysjM0NDX3+txaGVjZWFmYmFiWU5KSEdBP0NHSExaa+zYzszH
+xMbKztPc7vj0dV9ibn7q3NjU0c/LzdLR1N97bWldWFpdXmpnXlxdVktKR0FAREhHTF5859XL
+ycjFyMzO0dvv/Hhrfe7u49jV1dDO0tvX2+v5eGZdXFhUWmNobG1nXlpWTUZEQ0FFTE9UcdvU
+zsjGxsfJz9jb62xsfG1p7+fk1M7U1dHX3+DieV9dXlxcX2Fu+vt2a2VbVE1GRERCQ0tRVWjd
+1M/Hw8jIyM3X3N/6ZmtoZXvl3dva2tnb3uDq93Fob25jXV9sevfj5fd3YlhPSEVGRUNGTVNc
+7dbTzsfFyszQ4uvvbWJnamJn+uLb083Oz87R2Nvi7fVyaGhzeG54fHttamNXT0lAP0FAQElP
+UV3h19DFwsfKyMvZ3uF3Y2loaf3k3tjT1djY19jc4/1qb29jZG18+Ori5Op/XlJNRkBBQz8/
+SU9UedfQycPEycrN2+55cGJnfHNy6eHh2M/Q0tLT2N3j9/x5amlveXd5/fh7bGlcUklFQ0BA
+RUhMWnzl28/KysvLztbb4/J0d29n+uXp5dnV2dfT1tvY3e94bWdgbG5sbHhvanFxamZcT0tJ
+RUBBREdPbeHb0MnHyMbFy87P2+33b15aWVtcVVnZys3c7Obd3up3XllhaGlw+Orl1s7U3vlc
+SkZFPjo8P0VT6c7Gvr3AxMrZeWRXTEpLTE9g6dTNysnKys3W3vhoXl5fX2h3ffTc1M/Q3Of5
+dFxMPTg6OztASlTlx768u7zDzNZ7TkRAQkZNWGTmzcbAvb/K0d37aWJZUVFa/tvNxsXHyMnO
+5l1MPzcwLi8zOUNb2sC4tbO0usTaWEU+PT0/SVziyb25ubzAydb+Wk5LSkxVdNvQzcvHw8bK
+z/FNQTouKi0yNkrWyb2zsbO0us5iRTo3ODxATGXeyr66ubu/ytb3WlZWU1di/dvLx8fGyMnL
+2G9VRzswLCsuN0V4yb+7t7e4u8XgV0M6ODxCT3TXzMa/vb6/xM/qX09OVFpj/d/SysbHyMnM
+z9j7UUM3LSkqLjdNz7+4srK1uL3K90k6NDM4Q2LRx8LAvr+/v8XR7VpNS1Ns7N7Z0s7Ny8fF
+ys/eWEU8NCwpKy84WMi6tLGztrnA0l5IOzU2PU7iyL+9vcDDx8zQ319MSUpQZOTQzMrKzMvJ
+zM7Q6VFFPDEqKSwwPfrBurWxs7i7xN9UQzo1Nz5Q1sK9u7u+wcLGzutUSUdITWPp3dfPy8rJ
+ysvM0XlPRDswKystM0LhwLqzsbW5vcntTz42NDhCW9LDvru7vb6/xc97VExITVx26N3Yz8vJ
+yMnQ2+9VR0A3LSkqLjdPy7u2srK1t7zJeUs8NjY8TPbOxsTAvby9wczuVUtKTFRncXbo08rH
+xsfL09jZbkpBNisnKzA7Xce9u7aysrW6y1pAOjc5QFF52crEvru6wMzgZ1RRUE5PUFhv2cfB
+w8jKzMvM1mpMQTguKiwzOkrXxb65s7K2vMh8Rj05ODxGWeTKwr67u77EzeVgUExKTFRn7t7P
+x8XFx8vS3uhpTUM6LyosMzxP1sK+urOwtLrHf0U9Ozk7RFBn1cO9u7m8xtPrXVBNSktSX33a
+ysHCw8PJ1eHl/1NGOy8rLTU9SXTWzb+1sbS7yO5SRz88Oz5JZNPCu7u8vsPL225VTEpMU119
+3c7GwsTIy8/V4vVdRzw0LSwwNzxJ882/t7GzusDK4lNEOzc7SGLlzL+9vLq8xtTpYU9NTE1T
+Xnjh0MjIycnP09vrflVCPDUtLjU7P0zkyr62srW8wsv1TEE9PD9MXu/Qw7y6vL/Hz+NpVEpL
+T1tjcd/RzcnGx83Z3fFTRj43Li83OztCa87CurW4vr/D2lRGQD0+SE9a3cO9vb2+v8LL3Wxa
+WFlYWmjt2dDNy8vR2u9dSz86NjEwNjo9RmTPv7q4uby+w9RiTEVDRkpOX9jGwL++v8HH0eth
+WFNRU1de/dzX0czMz9jpZkxBPTgyMjg6PU7nzse+uLm9w8zeXU1HREdOXHjTxr++vr/DydHf
+ZFVaYV5gavni2Nfd5u5hUEhAPDc2OTo6QFrezcS7t7i7vsXQflJKR0dMWGf108jFx8bDx87d
+92RXUldcavjn4OXp7XZXTEpDPDo8PT1BT3DfzcO/vr7Axs3b9WpYUlZhbfDazsnEwMPKztXp
+bldGQklNTFZxc2tu/OHrcmhcTUxMQTtDVkhG9s/f1cK/xcbFx8rQ43X3fGRo4tfXzsvO1Nnj
++mRYVFNQUVZaY2Jfb29eXF1VS0hOTklHS0xNWm7o2s/Mx8XDw8XJ0NXb4ens4d7Z0c/Q1tbY
+5XhdVE9NSkxNTU9VUlJYV1ZZYHh1bvzycmt9cGl6/W535eHo39nT1Nja19fe3tjZ293b2+v6
+8fptYl1ZWFpcWmZqXFxhdXlvaGReXGloY3H27/Hu6fl+7ufu+uTm7O93cWpiZnH57d/c397b
+3OLt/XBjZGVkZml9/n7p5+zs9W5ob3loaHJ3c3D48v549vD48+7t9HRqZF9kdXhlY3rz8e7t
+8fR+/Xx3cmxpe+3h39zb4+r5fXJ1+fp1c3Zsb/bu+Pjx9XB4dHFvanBybGNobXV5+ezv9vn0
++m5v/3xrZGZlYm/w6+rm5efk4uHt9vn59n1yfe7s9fPu7n989fv67+x8Y2JlZGdqa2tuefn7
++f318ndpa3ZycHhycXrr4OHn6ury9v3y7Px7enr78/X5+fbq5ezv8vx6bmdka2tsa2dsefz9
+fnh7+/Py+/50cG1qbvbv8d7e5OLf5fZ/dWxgafxueHlmVlr4W13a3Prb1fr183P8+Wt94PFu
+fO3q4d/42u9ld+f8aetscV5UXGJdVFv+bGZ35OZt+ejp5d7Z5u/o/Ori8297XnfsZWvY6W3a
+8nprcnzx9Wru71ts729fe3v6emR97V9e5N1vX+vkYWrffF5fcujndnPZ4n/j2+h339vbb27e
+eVtz415Uc21nb2Ze+vdc7+Fdft5vddvpZOZ2Yt3uXf/bYV3Z9lvb613m32D51ln/2WVj33hd
+7uJX8+Bn6uBiZNt1W/ttW2l9XXXs/uLj/3zubuzkdHzYdfd+9eztbv/p/njgcFp2bGb6fmDv
+8lz582v3dm98f39v72xl/Hxv7u/r7+zu8tps9dT0VOvlbW3sbHXuaWXqeVfv4Ghv4mxv//r/
+6d9u8+trbN1vYN7uXO34XPPnafbocnj6bW/7ZGbkcWrg63Py/2ppcejr+2Lm23xv3+xf8d3h
+Xnze5F9a8OtoYuZwbHL293ZsZm1oemJo4t9qadvcbefe+HLe42dx6ntsffZpavr3bWzc43pj
+fO52Ymz0c2Z353xy8Ph+53Zf++ro+ffs6u57e3Rv+vpsa/Xsb3nm6PDs5eTr8/Pl8HxvZmBd
+XFRYV1NYWlZZdX7/6+Lc1c7OysvOzs7W3+Hf6nh6b3Lr6urm9W5aTUhHPzs6PT9ARFH52czB
+vby7ur3Ey9Tma1ZOTEdLT1Bbb+XX0c7MzMzNzMzS2d/m/mZdUEU9PTw5ODtBRktW/tPIwLu4
+uby9wsva/19UTElLSk1UaXnv2M7LycjKzM/U19rqe2ZbVk9IQT8/PTw+QkhNXezVysTAvry9
+wMbO42BWTkhGR0pPV1/w18/MxcPDwcTJz9rn/WdbUlBQTUtHQkA/Pz9DSEtQXPrYy8fDwMDC
+w8bM09/2YldUUVJUXnV76tzX09DOztLW2uDudW5lXV9fXFtVTEdDQD8/REtOU2D02svDv7/C
+w8XKz9PceGJaUE5RWF1hfujd1NHPzszNz9fj8v54Zl1bWFNQTkpGQj8/QUZJT1p439PMyMO/
+v7/CytPd8GxbV1VTVltp+d/X08/S09DR1dzvb2NcWVZYWVhfX1RLRkI/P0FITVd449XNxsPB
+wMHCxszW43RbVFFQVlxr8fDm2tXT0NLX4O79c3NoZmZfXltaWVNMSERAQUVKVF/53tjMxsTD
+xMXIzdTe8GxkX1pXVltoe/Lh2dfV0NDU19re6XBjXlhVVllWUU5LRkNDRUpNUl772s/JxcXG
+x8jLz9DX5nxqXlpWVlhdb+/c19nUz9DV1tri9nJpYl5XVVpaVFBNR0VFRkdKTlx34NTOycbG
+xsjKztHU2+P/aVpUVFZbavfo3t3a2dfb3t7s+/jz/nBsYFxfW1VOSkVDQ0VITVRcfN/UzMfG
+xMPFyczP2eH3a1tWVFNZZXzo3dnU0NLT2Nvg7vhrYV9ZU1NWWVZPTEhFQ0RFR01WaeTQycTC
+wb+/xMjN0dzqfl1UT05PVFxq/Obc2dnc3+Hf3+3+dmVdXFxgaGxtaFtPTElGRkhKTllu5dbM
+ycfGycnLzc/Y3eh8XlZVU1pldn3o29TU2Nfa3+54aWNdXFxcX2Zybl9WTUhISEdHSlNh69jO
+yMXEw8THyc7S4PFqV05JSkxRXHjj2NLSz9HU1dHW3e5rX1tXUlhcXWVlVkxHQT8/RExXdN7Q
+y8bEwL/AwcXK1fNiUkpJSktPWnTr49zX1dHOzs3S3u9xY11dZ2tpaWNSRT48Ojs/S1tr59XM
+xr65t7m7v8ztV0tGQkBBRUhLWP7Zy8K9vb/Fzdbh825oYV9mYFpXWVJIPzw7OjxDTVv81sjA
+vrq4ubzBy+JZSUM/Pj9ARUxa5svBvLq7vMDHztrzZlhQTExRW2Rsa08/Ozw8PkFITFNq18W8
+uLa3u8PO3HBYTkc/PT5ASFflysG+vby9v8HG1G9SSkhKUmb27/9tWkg+PT07Oj1ES1Hux726
+t7a3vMPK12RMRD87OjxCTmvYx767ubm6vcXQ62JQTU9NTFNhcvpzTj06Ojo5O0FLU2rKubW1
+tbS4wcrVZ0Y8Ojo6Oj9RdNbCubW1tre8xs/tWklDRElNT1Zma1NBOjk8PDs9RlJs0r21tLe2
+tLe/0ehYQDk4Ojs9R2rUx764tbe5ur7Nc1NNR0NFSExQXH1nSDo5PT48PEBQ+9K/tbK1t7W1
+vtH5U0I6Nzg8QEpqz8fFvbe2u7/FzehdV1BJRkpQVVJUTUE6OT0/Pj9LcdjKvrays7a4vsnh
+WEk+ODY6QEtV7szDvrq4t7vCytPtXVJTW1VTVlxdSTs3Ojs4NThGX+7QwLexsbO2vcrlYE0/
+Ojg7P0da1sK9u7m5ub3G0utjU09PT09UX/D5SDExT04xLDvTy0lKvq6wvbyyt9RhbWBDOT9J
+RUNexrzBw7q4v9Ll9VpGREpNS0xa8ePf19z+4PNHPT48NDdCT1Bfy7u7wr21uMvr3NhhSUlT
+XWj+3tHLx8nP29zlb1lVWlpSVFxqdnjg2NnZ7ko/R0U5MztQVURI3cHBxr+4t73Gys3lXlRT
+Vl5hbevd18/NzcrKzuF2ZlVNS0tJS1BWWl1saFpOSElLSUNHUFxp7NPJxMLAvr7CydHm8nFc
+XGFeX3Pg1s/OzczP3/FzWU5LS0pKSUtbXlZcbfnu+3d4fWVcbXtlY3/o4d3a1dbn8Onx+HV8
+dfTt6dzY2dPMz9bS3m1zZ1hWXVNTYlxVWWRga3hqW11rZmBeaWf+7/rm4unr5vDh4fbt9m13
+d+vY5/Xm1+J+a+7fdFln6NzxbOTm+2thbWRYVV5tXl5defZt9+Xn29zn7e15ZPx6ZW72/mT6
+4ubv/fT7/vpsX15lfPr4bnPn8vrj73f99nn9b3nm7PPr3+Hn5Ofo7+3t/3R2YFRUY3pkX278
+fnRsZ3P/efp3fXF08+/t7+jo9ufi8395fn/4em3x6efn3tzv8e/ueXR8ZGdpYl50+e/2/Xxw
+ZWVtbH37dPj3fuzs8ufydG1ta21rWWDx7u32793e5+Xg6+zl4elycmxkYGdkbmxvd/t6+OHh
+7u3r73jte2x09n5q/Hh5dGxicfVmX2Rram7+eObZ4e3c4XByd25qfW1tdXLu6+Xn6Onc5nN2
+9P5pbWxlX2hoafXr8efed1Tz41tn7Xr13///+Pt53+pfY215Z39x+OXe6Oz38+zt6vlucXhi
+YWZobGb6f/nf4d/v8/j0f3VwZWtuam5sa/Lt5+zv737m7GNZV2zf72Nq8+rd3uzo7PH7dXt1
+bGJy/u7m7fz1bWt4aVpVZf/ybGBq/uPX2t/k8PDy7PF0cXd4c2tfZG9ocPft6+Xo/Hl27OT6
+cPt/eXJoa2xsb2ppbvft83dv/ufj7Ojp6ur0fm9sd3xxfXludG5rb25oa3NubnN+7e5+evbp
+5+vu9vj99PT69/Hq8fn4+fx0b3F7+/H3e3F6+3ptZWtzb3B7b15bXFpebHz1+/jz7Obf3ujg
+3eLj397i4t/b2dna397d4ePmeltMQTw4Nzk+SVnq0svHxsPCw8bM1eT6aWRfX3Xt3dDMy83O
+z9fmZEo/Ozg3OT5JWuzQxb++wMjO2/Rxamtpc3Nvcvvr3tDKyMfL0+1gUEU+Ojc4Oj5JWezO
+wb28vcLL1uJqXFlbYWl88eLXzcnHxsTHzd5yWEg+Ojc1NjpCTGPZysXCxMjM0N72bWZka3zo
+3tfPzsvIxcXIzNLlZks+OTQyNTpATWjWyMO/vsDI0OFlVFNSWF5x3tLLyMXDwcLGzNjoXkc7
+NDAvMTc/U+THvLm4ub3G1G9OSEVFSVN92s7HwL28vb/FzdtvSzowLSwtMTtS0r20sbK3vMnt
+VEY+PkFKXNzIv7y7u72+w8nZcFZCMywpKSsxR9K6r6yus7zKb0g+Ojg8R2TSv7i1tri8v8PF
+ydV2TDgrJSUnLD3Vtq6srLG/11w/NzM0OEThvrWxsbjCzMzIwb6/y249LCQiJCs9zLCrqqqv
+vNhPOS4sLjZMyLSusLjF3/rVwrq4usPuPisiICMpOc2up6WnrLvvPi8pKS076LitrLG+3Vxh
+zry0sra+3T0oHx4gJzy8qKOiprDORjMpJikzXLqrqa6+cUdHdb+zr6+2wWQvIB0dIC7Lp5+f
+o63NQDEpJSg1cbapp67FTz4+W8a3sbG1ucY8IhsbHizJo5yepbNwNiwnIyg6ya2lprLbQztC
+dsO6t7i2tLtBHxgZHzavnJufrMJJMiskISlItaajq8dDO0Zj18jEwLqvrLkuGBUbKsSemZ6r
+us1MNSogHy+9pqSrxEM7UtfR4vjqx7Coq08cExgp0aOboLG+vMlNLyIeLMGnp7HjPDz8v8b5
+XXrHsKmrZx0TGC3KqJ6jtsC4vVAvJiM3t6esv1M4OVfO3GrkyrywrbRcIRYZLcyuoqCrurzP
+OyspLVWvpqy/bD45Q15dW9W+t6+uukggFRcp6K+in6m5v+QyJygw16miq73fSD5IV05L676y
+rKy5SB8UFyrls6Whq7e82zIlJS7NqKStvdRYSkxORkVizLarq7dHHhQZLs2wpaKstrvqMSYn
+ObypqrW+xtxVSkdESVfqv6+tt0QdFBs0zrSnpq20u1wuKC1dsqyyvLy7yl9LTFpvZ33Esq/B
+LxkXI0/BtKyssLXMOCgoPb6xtry8uLvWUFXXzPNPYsGytGgkFxw3xb23rq2uu04sJzXMt73E
+v7q6y1BO08DWVFfQt7LMKxoaKte7vLOrqbNcLSUxzri/xr26vM5MS8271kRJ0LawvzgdGCRR
+yM22p6Sr0i0jM8u+zMe8ubnPR06+t+o5OVa8rbhBIhwlP2pXx6ukqcUzKT7N0V/fvLK21Ex0
+uLVuMC9Kva+7RCUgMFFGPc+uqazLOTnTxllFZsK3vOlfwa+3WC8rNeG7yUYtLUnxPzdzt7C6
+8UvPtL1PPk7Kub/Z0bevu08zLzdOZ0k2LkPH0kRE4MXIYj9fsay+XEpP6s7T0r2vscZPOzc4
+PTs1LzfVuMTz3dz4VTs51K2suuJUU2vp1b+1srvpSkhDPjkvKytBvrjEx8LbUzwvO8KvtL3N
+Y1RUUt+2rrfSYVVaZUw1KycrT7i0uri830MyLkC+s7e7xOJjS0jZs6641kxGX+hJMCgkLGC7
+uLawudxDMC5Nvbq+vcTO3FBH37ezvftGSP3ZVTYqJS1Tw7u0r7bMSjIvUcDAx8jPycHcVty9
+u8PpTEv60V45KyUrSczEurG2xV43LkPHws3Q08m8xH5+xr3E6U1L58vqQTAnKT7azL+0s7jE
+TzE3fdDk7+bPurjSYtjGx9RcSWHKyl05KiQuUdjNvLW2uc1AMz1aY3jRx7y4wefoy8rO4lZV
+1sPSXz8uKC07TNW5s7S4x2Y+MzQ+bb+0t7/J1ePq+Wht4M7Jyc3ZWDsqJCo90baus7m7yUYt
+KTJgua+zvsXN6WFPS2HMwcTKysfKWS8iISpCv62qrLDFSzQrKzvTtq2uutFlSEFLaNfGv8G/
+vcLbSzMnIyc03K6oq7XOTDo0LzVZv7KutMhhRTw/T+7FtrK4v9T1Zkk0KCUqOOy3rKuvwFg9
+Ojw/UOHGurS61Uk5O1nGube9z3hfb+LW7kEsJCQrQbyppqu47kE8Pj9AT+rHubK3yFpBQlTd
+zsvGxMTHz9d2SzgrJigvSr6rqKu37jwzMzhBYsm8uLjC8ktCRlrVxL26u8fpVElS2cbPTDIq
+Jys9ya+qrbvyS0hOUUU6OEF2v7SzucjzWFRp3c/P297RwLi3vNQ8KSAeICxdsaajqLPaQzg1
+Nzs+RVTmw7Osq6/CVz08QE/tzMLAwb++vsL5NSQeHB4pTrenoaKptchcSEA5MjE3Tsm1rq6y
+vc9fSkVDRUlPds6/ubW6xWg5KSEfICc157Gkn5+krL39RTgyLi4vN0zKta2rrrnUSjgyMjY/
+Z8e5s7O2u73GWzElHx0hLVq3p6Ggo6q1zVg+MSspKzBB2b22tLi+ytTlfW9pbGdlYWD418i+
+ubm910EuJSAgJTBUu6ulo6WrtMV4RjcvLS0wOlDZxL+/v767uLa5v9VSPjo8P01w18vEvr/K
+WzwtJyYpMEPNtq6rrK+1vMbZZUo9NjIxNDtN3L61r66usbnKWz40LzE5Su7Jvbq8v8jjSTgu
+KSgrNEjPuK+rqqutsrzhQzUuLC40PlLcwrm0sLK2v9ZXQjs4ODxJa9LEu7e5v85uRjgwLi40
+PlrQvri1tbi/znVRS0hDQEJESVNo5tTIv7u4ub3H2V9MRT8/QUhY2cG6t7e9yeNMOC4pJygs
+O+q6rqytsbjB02xLOzU0OUTrv7SvsrnH4lVIQkBBRU1f4c3GwsHEytLe825kUEc+Ny8uMjte
+w7SvsLfG51hIRENGSlfny7+8v85wTUNDSl3118vIyMrQ3OxrXmN36tbOzMnJzd1hQy8nJigw
+V7msqKy51FA/PkRISVFo28O7urzLYUpHR1nn2tLOy8fEzNxyUkhIS09ae+vYzMrGw8tqQjMr
+KCs2XL2vra+4yOxZS0ZEQ0RP6Mm9ubvG4VxQUlpp5uLm3+Xt+mdcWVdf7tTMxcXL0vtTSUQ8
+NzY3OkF4yLq1trzMaEhAQkhVbOLQy8bCwsXN1m9TSkhMU23k2djZ1t74+mtgYGVy6NPOy83c
+e1xNQz47NjU5Stu9tra7ym1OSUpNUFhq38q+u73F3FRGQkNOb9jLxsTI1e1WSEJET/fNw7+/
+wcbT6WdPQDYsJioyUbqrqa258kU9PEFIRkdS68e5tLe+0lpGP0ZW+NfOyszO2GxRTE1W987F
+v7/F0nlUTVJdcXNKOCwpLjrmt66utstZTEdGTExKUfrJubCzu9FNPjs+SmHh18/Kx8bN3V9L
+RUhY6dDLys3b7efx7tfZeEw7LyoqMUzAr6yvu9pTR0JGS0xNYNK+trS6yWVJREZMW37r4dLM
+zM7dWklDRVbjzsnK1OLr28vI0e1SPTUxLzE9Vsu2sbW92Us/Pz9JWHDcy8C8u77M71VNUF58
+4t7w/OHb2eBmVEtOYt3Qz9h+Z2nx0MzR3m5OPTczMDdAZsi6uLvE32xeVlRTTVBl28S6ubzF
+311TTUtKSkpObNXHwsjZaVNTXnPt4+1nb+TUysfL2mRKPzs5NjU5QlvVw7u6vL/G1m9USUFE
+TnXWycK/v8PJ2V9JQT9BTHDSyMXJztbs+nRjVE9RW/DZ0MvKzdDfXUU8My8wNDxO68vBvLq6
+vcHL4GhWTk1RYuzY0tDP0dff8m527d3d3eVlVU9PTlhsdXT06dvRzs7P1N9vXE1EQEFAQUhK
+S1Rm7uHY1dDPzs3Mzc7Nz9nh4uV/a2hcV1tja/fh3t7h6vTp4/F6c2FaX3jr29fT093xc19S
+SkNAQEVKUl1dXGdqa37l3dTS0c7Oy8XFyMzU3vtlXFpja2z/9P759nloamleWVVVXvjd0M3N
+ztTZ5mxfV01HRkhMTlRUTk1RWmN55NzY1tbV1NTOztDQ1djW3ufr6fRxcHFwdfnu8m1iWlVV
+XXPq2dPR2OhwZV5ZXF5aWlZQT1BOS0xNT1lidPLg2NbW087LysnKztHY5u7v9vTt8/by7vN9
+aV9YUVFYZvXf2tnb4ev9eWxhXVtWUVJTT01OS0pNUFdo897X0tHT08/MzMvLzNHX3Of7cvx1
+YmNubHB7fHNubV9eX2Z+7Ofp5Obr+np2dn53cWtbVE9JRkdKTFFYXnPs4trSzcjGxsnLzdDa
+5/V1bGRiX19qeWhld/ru9XV3fPbv7fR/+/Ts+nl4fffn3+bz7vF3W0lBQD8/Q0hMUGHx1cnB
+vby+v8bO2e1zZllWWlpcY3zh1tDP0993XVlWTk1RT1du6tvWz83Nz9PZ3+h2XlNMRkE9Ozs9
+QU1x0sO/vr+/wsfN2O9XS0tOWfXQy8rLz9Pc5fhlUktLSUpQZuXZ2dza4+rre3ZwbXzt7d7U
+1t7valZGPTk3OT5Mes/DvLm5vMDK3G1SSUVESlbw0MfCxMnO3e5pU09MSU1add7Y2Nnj/Gdf
+YWBcW19p5tHLyc3beFtRTEdAPTo7QU/hxr26u77DydPb8FlKR0dOaN3PysjL097yaltQTUxN
+UmPz4tnX3dvi+XZiYVxXYH3j1c/OzdDjemJURDo0MzY+Xsy9urq7vb/Fz3dMQj0+SGTWysXE
+xcfN129TSUBBSFFv3NTS0M/P0dr3XEtJTFRh8dvSzszKy9X4VEhDPTo4ODxJbM29uLi6vcPM
+2fBZS0RFS13gz8vKzNDd9GlXTklJT1p44djQzdDY3vxdVU9QXHPt2s/OzMnL0ulmSTYuLi82
+R+3FvLm1s7a5wOBSQz4/R05l6tHGv72/zXRUSEZISk9OU3LazMXEy9h1W1pVTk1OVnLUxcDG
+z+pXTktCODIyNUBfzb+7ubi3uLzH51BFQUVOXWr5287HwsLK5VtMRUJITE9aXevPy8nM2nxh
+WFBVXGvq2dHLx8bL21g8MS8wNj9Obc+/uLGvsrvMbExGRENCQkZY38a+vcHI0u5kVExGRUdO
+ZeTTzMbEyMzS5WhYSkNKVlxibHH083xzVklDQkNHU1587NvOxL+/wMbN0tzn+WhbW1xaZfbn
+3NHT3N/v+XxpXFdWVV1x6+Dm/P7+bGZfWU9PV19rdW9scXNmX2FlZ19bX/Hb1dLP1NLNzs/T
+3v1vaGz/9/15eu3j4d3oaV5fXVxdXGJx/+zj6+rzbGVdWVxZVVZZX2dt7e3/8/V93uD9+Gpr
+fu3s3dbb3t/a29zh4fdoYXJrY/789fn/ff1vcHtjYGVne/Z1dPt8fnNkXl1hY2trcPnv8vPn
+6Ozr72x8dHH/bG93cv7p5+Xi3/Hu7fT95exzdXR2fnxpd/vq6PPvenxzfHl8dWZcWlhiaGNt
+/X138+7r7unmdmt5aW5w9ubt7uvp6vD48vf07Ojj4eXq9HxsX1lbYGl19fx6fXd3/e7u+317
+f/399vpxa2RhZmJiaXf7++307e7ufHd6cG748vf+7+5ycOvg397b3Ovp+29kYF9ubG72bnRt
++W1ubHpqYnn89O//a/p9aHd1bG9xbnF09u/n6uv35fB87u52cfhzbWpwb37w7uXm8u7n7+zo
+931xbm5xam12/O/3+mVkZWxua299/Xx1c2x1/f7s7On0e3Nz9+vy9fvv+PPq9v9+bm1tamZq
+en17efV//3zs7O7p73FqfG948e3v8PhpdW58b+ns6PlybmJrb/T/d3X+bmd3ZW1+7/Pq735y
+bO727nZ493lteHz3dn7683fv7e7xfXh0endz+W979+p9/n7+b2/+/Op8+3pzaW958O38enJt
+e+jq8333+fx5ZWlycXrx9XP68G5mbvz7eP7v9XZ48O/39Oz0dnl5fPb5+nV99fPx7vH48Ory
+e3dvaGtze3Rua3F4bm59/fr1fXd3fX17bm96//Ty7u/s6uz2cW16c3N7ffn28vVwc3p3+urt
+7+/v7/b+9Ovuffn3fnlvbWhobmtkY256cHJ+/Pp+/fD0fPT093tycn34/fnz9X1z/vPs5unt
++fXt7vxzfXdvbHZ0bWlrbGRt9+vs8Xp3+npxeXRucX37eX/9/Pfv7Ozw8vJ5dnB7eXJ7dnzz
++HL99W9vd/7t7+35bmxudPv9++z38+rl7P75d3JqZmlobW9qa3D29Pj3/Hd0/ffw7/R+/vX0
+7+31+/h8bG5zdXRwbXN99vP4b2/6+fT26+r+8e339fL+/Xx6bmxvd3VxfHny7Phva2pobf5r
+7ddt6uDtcHdu8PZ69XdraPBiWFPd1f3j1+He91pf/1/o/ff27t7t6338dvxvaVxn7+zt7Pv6
+7HBsbmlobGpmaGNp83z14N/e3N7n6XRzeH76fPL48Onl5+Dc2tna6X9oZF9QSEA8PUNGSlrq
+y727vLy/xcvaWklDQ0dKT17p0cfCw8bMz9npZ1ddYfXl3dLO5EIxLi8xNzo/bL+zrKuusrfC
+7EU0LS4yOEd2y7uysbK3xNP7TUE/P0hd6Mq+u7e3vNs+KiIkJys0Q9+xpqKiqLPAejssJSMp
+Nk3LurSsqKyywl1GQTo3OT1Uz7+8uru6vMbgXkk1KCImLDhSz7uuqKepsMlZPTEtKyw2Ts65
+s7Oxtb3K+0xDQUVQZe/Y0M/IxcTDyMvO2VY0JCIoLTpW4byqpaWpufdPOC0pJy1A2Ly0sq+v
+tL3XUEVCRUlPXerPx8PEx8PAzNnkTTcqISYuNlHOv66np6u1e0A7MC0tMD/NuLCus7i7yf5P
+Qz5AR0522c3GxcjKzs3M7HduSz0tJiw0P3bMx7KsrK69XEY9NTM1OEjUv7e2ubu/zOBiTUVF
+SVJr49bRz83Hwb3Cyc9rTTooJCktPmfcwq+sqau86kw5MzQvNET/xLe4t7i9xtJ6TkZFR01X
+etrLwr+9vLy/y3JFMyUjJis5V9O8ramkp7HNTTgxLSwuOFLPu7Wztba8yutOQT9AR1Fq3s7I
+w768ur3BzV45JyQnKjRGYcayrKWlrbzfPzYvKiktOVbHubKvr7C1vNFbQz49PkBNZd3Hvbiz
+tbrDZTMoJCMnLTZHybSpoqOnrbxsPy8qJycsN0/QurGtrK2xusxeRT46OTxBTujIvLWztLjM
+QjIsKCosLjdJ47mtqqmrsLrNTzwxLS0wN0Z0zr63sa+wuMHN5l1LQj0/SFrt1c3Iw8TYT0A5
+NTU2Njo/Uc68tbKzuLzI3GNIPTw+Qk1adOHNxMDDx8jHyc/jZ1hSXF9cWFhf++pwVE5IREVE
+Q0VKVuTLwr6/yczP2etkV1ZZY+zh6unb2t3p+eLd3dnf4+Dq5O93bVxcWFtXUU1IRERGRkpU
+ZODMx8DDycvT3fZ6X1xu8uvt8uvd3uT4c2dq9t/d4N/n6N3q/WdnbHr3YlFHQj8/QENObdnL
+xsTGycrQ4m9kYWvx8O/9/u3u7H9uaGn04efY193b3OPu9P3p+f1tXE5KRkE/QEhSaebU0NDU
+19bc3vZz9uTb2N7n8Onp7vR+/vTh3trX3tvk9nl0a3F7ZFtYWE5NSEdESlFZafXf29TU0dLW
+0tXa3uPl7O76+3Nobm5reurq397Y2eLm8fLp5Hfw7nRkXFFMSEdJSEtRXG1+79/h4djV1dPW
+3N7n7X76/n139Ozz3+Tb4t/h39/c3/XsePP/8mNgX1hPT0pJSktOUVNbZvHj2tbPzs/S19/v
+6evw9fDu++f25unb4eDo6Ojv7evs597j6X3vb1VNRUNAQUdGSU1f7djOysnJysvO1OJzdmRp
+Z296+uXb1tfZ3ubp8ez4/XL36ujp8urt9G1PSURDQkFCRU1d7NnTzs7Mzs7P2O9vbWNqbG5z
+7dnQ0dHT1tXb3+z6dWf08er4fff8d3VbS0dGRURGRElPYuna19fPzMzQ2eXw8fh9c2547N7Z
+1dbT1NXX2d7q7n7r3t7g6nxweG1YSkdJRUBAP0FIUWTr2dbMx8jLzdbj8PLx/nJs9urf2Nre
+3tnY19rk6ePf39/o/PTt7v5uW0pGSkU+Pj5ASVdi9t3VzcjIycvV3N3e6u37anvu6ODe4+jn
+397h6+zp49/e7+3p7uDh821hVkhFSEI+Q0ZFT2du9+Da1c3Oz8/V2NTW29zm7+bl4+Tr8+ng
+5unn4+js6+btb3F8evn1/m5nYlhOTkxEQUVGSVZfXG/j2s/IyczLz9HP1+Pv9H725Ofvd3fs
+5e3s5ejm6Onr/X/+8/Hv5+3+f/h0XU5MTEQ/QUNCSVJefN3TysfGx8fLzdHe5ux9cnpwbO/p
+8fX57Ovv8O3s8n57YVj07uPPzMnKzt9uTTszODY1PUNR28bAu7q+v8bX7FlHRkxKTV9n8s3H
+xMPJz9biXlxTTFNUV27w7tXMzs7R3dzX2dLO2fZOOy8wNTI4P07VvLq2try+wd5PQz0+SFBb
+5s3EvLzAxcreelpFQkZGSlNi7dHJxsXO3eR3XF5lftfQ1NT1SDkxNjk5QEr4w7i3ubzFytlR
+Q0BBS2X24sq/vLzE2PJgTkhDPkhfX3Tb1M/KytHZ7W3s63/q2M3Fxs3dW0U9MSotNDdFbte8
+tLa4u8rX6Es/QkVS4NPLvru+wc9qZFxKSkhIXd7VzszMyc7xXk9IQ0JLVmrayb+9wMfN3F5M
+PjMuLTA6QFLXvbWys7m9xdphTUVET1tp7c/DwcLI0uJvV0tERE5aa3bo0c/P3m5ubWpudHbl
+z8vM1N/X3UQvLjM0O0FK2bu1tLa+xMXpSz89RFlqatrFvLi+z9feZ1FJP0RZX2T46djLzNje
++WhrWE1Zfvbf1NHNzdvnbj0uNzo1P0lZwba7urrBwMZYRUhDRlJPWMq/w7/M3M3Qak9KRU5g
+Ulrj0s3Iytrh62xdTUVPZ2/+7ebSy9ff2Uc1PTozP0lOzb7DurW+v8VdUV9MSVNPWc7NzsHF
+zMPPW1FNQ0lORlD+4NDIycvP9mRfTUlPVVdeberb3N7a6G1rWEE+RkZNZPvUwsHCwMXO0dxg
+UU1QXWZg/dnXzcrQ09Xnal1RTllbZPXazsvLy8vYTj86NDY4OT9X2cS6ubm5wtVdR0E+P0BK
+XdzDvby9vsLM3FxPTkxOUFr6187MzdPW5V5PRUJFRkpc9tzNys3S09j8WExHSE5OT1BVafL5
++N7X08/X19TX3unucPns7Pt3f+fa1tDMzdDU2dze61tJQjw5Ozw/THvYycC/vb/J22lNSEZC
+Q0hOZ93LxcLEy8/ia1xVT09SWG/g18/Mz9TT2+b2Z1tcZXZzbvv99erufGtqXlVQTk5RVVZY
+Wl105d3a19XX2uPv9f14b37m3drb19TS0tfd5uHqe2NeaGFGPURDRU5PXNnR0sjHzMjO+Wle
+VFhWTVJlaezX2NPQ1c3P3d3kcWNVS09cY3zl6t/c3uvv9GpgVlFTUFBYU1FfeuTZ2NfPztHZ
+5ez2bl9ibv7h2+fq4eHj7W5faGVhbm9s9e14/H/38PP+cGdkbvh6bmVhbHBpbHZubG1panX+
++XdreO7p8vTs49vd6+vi4tzW2tzd5vN2Y1xcW15iY2f+6u34bW1ybWxmZm1hX19odPLn6fB6
+9PJ+eH1wb3Fz8OPk4uTr7Obf6X1udm1rbG717v7++/7++X54eHFpa29vcG92dXry7Ovq7vz2
+7/t8a2ZfZn1+7Obq7vB3cH39+nz9/Xh6dm91d3p2/vx8+3prZ3Ty6uLe5+7r7vp4b291eHp5
+cW9uam766uXs7/N9dnp8/XhubWdnZWVrcX7v6eXs7/J9dW5sbG1873twePj6+vDu6/L07urs
+8+vv9ndycmxvcPLu9/t/d2lub3BsZGdjYml2fvHq6enz7/Do7ft+dvt8fvz/b2x4/fP7eHx8
+9v33fXT+/Px8ePz9/vDm7Ht/8Ov1b292cW1qaGZtb3l9cnp+b21ucnz47Ozx9/X5+fj9f35+
+/fzr6PF/9e74/fv/+O/6cvf+bG1nZmVzdHZybXL47+74/Pbt6vdqaXT5+vv29/Hv7/9rb25w
+c25ten587ery/vP9dXr5/Xh4cvz4/3h7/v759/Xv8fD5dHducH1vbWx0fvj57u306/h4/O/x
+/Px29/H5fnZuZWt7dXV5fnd0cnF7/vTq63Zxdmpre/Ly8u3z6ury/fvv9/fu7fT8bmtva2po
+aHZ3dGxtd/35dvnzdm92f31+9/bu7358+PDu/Hl4/fz39/z5fvz6+nl4/v13d3tudPP9cHN+
+/Xz56ef4bmtuefp4dXp29e338vf/9e/3/f17dHFvb3Z3e/t7dvzx+XF0/nN08vX6dmlna3V9
+8vnz7O/+cnT99/Xm4ez78+v2fXz19H13fGxsb3H483Vuevh2ePz+/v37dX18+/T78evn8P98
+enZubGlrbGlrbGtudX74+Prx7fHv8vL19PTu6Ofh5+77/3t3e3dzb2lpbm90eXNqbG12fnZv
+eHl58vn/9fry7O3o6+zw+PXw8/t5d397cn37d2xsZF9ia3t0ePTo8f31+v/z8354cnV/+vz7
+9evs8/l6cnTy7fP7d3p+cGhtbXR4e35+7+37evbz7v1tant3bn/9/XZ99fv/8Or2cW52/Pbu
+5+z6d3ZzfPv29fj3//z4dnpzeHNv8vj8+nNrZ2756O7/fnR/dnl2b2tqc2xteHz/+PT37O72
+6+fr6ezt7ezq7+/x9/j6b2ludnJzcm5mY2hoamlpb3NtbG1z+/Lu7Ozo7PTx7/Hp6u3m5OTt
+7+zq6fh3bGhmbft3aGVnZmpoaW5oZGZqaW199/z88ejg4+Ph5+3u8/r9efXx/f/39vx8dnJ5
+eHd7bmptcXF29/T7eXhxb/75+fX28Pv8/HRwcfnz7/lqZ2dmbn7+8erq9PXw8/r7//56d315
+ent5ffXv6+j1/vr2+X1zb29rb3B3+fx2fP7+9O/n7P78d25ud3v78e/t7e/6dmpuevx+fXx5
+fn13dPT0/3xua2hrbGFlaG3+9+3u7+ji4+nr5OHq+O7s7ff9d2x0dP9vZmVpZF9odXBz+n14
+enb47evq5uX2fHV7d3v4e/12fPh+/PTx+P56c3hydH73f3d2/+748e9+dXT5/m9ub297ffP0
+dnJxaGZxd3308vn37fP08fb3+O/t93j77fd0dv/8/vr7+/Xx9fp5bWxqamttcHV6d3358+7v
+9u/08vX+fnpvaWdu9vR4eX16fX13dnFvf/Lw8e/o6fDz7Ozx6vt1bnBwbm9ydXBvb3Z3e3Jq
+cfz57+rx8e7u8P58ffX1e3VzbXXv83h5ffn6eHR1bGpsc3n/+P11/vbs6urn6erv8fR1Y2Fo
+efn7enx+eH57/vl7cnx4fPTr/X18ffv57v999vtxd/vz8/91bGtobG9vcnR6fXr7cm537Ojp
+4uXq8/Xt9e/1+v52dW/x+X18dnN+fXVscG9rZ2t0fXJqcHR4cX39eXd39vn08vTo3+Ds+Hh6
++fd+9urr7373+Hdxfn9panN3d29pa2dnbX738/l5bm93/O30fuzq8/Pr6O338u32cG52fWts
+evpyaGttbmpx/fD47ePp6OvvfWtvbv99evr/d3h2c3zu7Pd9/n5yfG5sd/n7eH73/nZ6b2t2
+7+3t7352/fX7fnj/8P9ycHr+/P727uzu8Pb8+/V2bGtpcG1sevPz7u5+e/P7dXt+bmpwb3F1
+cvjye3f97vF8eX5+fn5zb3v89/t+ff717e/47O7+9fD8d//v8/J6/PJuc/h8Yl1ocn10eft4
+fffz7/r+/nR0bHN7eHNze/5+d3N0+/Lu6ezz6u9ua315b+7vd35+5+Txdm9vc3x49XZue3Zv
+bnx++nJ1+np9b2lobvz8dH/+cHZ4+fHr5+777er4dHn29Prw6vpucndtanN3fPn+bWVqb31/
+fHV5fnv6fvXn6Ons9/z6+XRkYWVtZGR3f3n68+/p9/Lt6+bo73P29H5183tudnt2fnhu++3q
+9293cWppanVtbH7093t79O/v+Ovo8P57//7x+X11dvL3eG1ubnN2+vtobHN87+3w8+99bmxw
+bm/z9Xr37O/9fHD883F8/nd77uDl9/37fXR5b3FzcXV0cW18+/xvc3ZudHRwdfn5/Pt89PP8
+e+7o6uz6+/pxcv17+Px59PF5ann99P1tcnFnbvz69O3q/XL27urz8u37eHhybXB1cff3eG5u
+9/bz//Z+aW1vX2Jv/+nv8ensfPzy7fvv5/P49vD/fH5z9PD5/Pz0fmt08/f/cG5tZm56b29w
+b/7p83pz/vN8e/v4/vf49v52/u/p5e779vv0/Hh1//1xbW1oYmpsbXh/8n/26PV8/Ofs8v9x
++nhxfX757fHw8+/89fxxfG9vamRmZnp7cXd+/HT27vR8cPf7b//w6+zu5fJ99v5+fvNwbnJq
+dOz4e3j06fn6/HV17+P2ZmZpcXt9emZqd3B1bmt77P9od+/z7/x89PHl3uDr4+H1d/n6em9k
+YGJgbG9sbGx7bmtz/unp9O7x9ers8+z79/p7e3L+7OXu/fx8/u7za27+dmlramJhZ3rz7O/q
+6e59eHxyanD29O34an34dv18/fzv/n/+fn9nZ3J2++fq6t7j/f95ef3v/mhwbW17enJ5/ft+
+cnZ2eOjr6vB7/3R0a2d1fHR38/D57+vr9vXu/3hwdHJhafvt83d+dvzz8ufu+nh56vR8cnB+
+bXT5emx88XX+e/3u73Fy+mp4cXFrde9+8/Lp6+/57/P58f5t/v/y7Xhqc3FvbGxtaHd0dXj4
+7evu9nh9ef7v+3ny6vn3+PJ/b+77eHxxenr5cXZ1bG5tb/x9evbt+W5nc+/s6Obn5+33+fz/
+cHPs7nl5fG1jYWv9eHhsa/t8eHJ3evz5cHrz/n18+/zx6+Pn6eju8fD99Ovu+nlqbm9pZGpt
+bn50fHBlfu39bn3l6/T09nt98Xd+8/7o7nLp+m98a2FkbWz2+HTt7fL8bfvv/nr07+71//31
+7vTse2due3FkZ3Ry9f337PP98/5vdvzk5nFrcv75+3z97e30eHB5empvf/r47fl6d3B3eP5t
+YWt8/m9udvbq8vXq5u3v5/Pt7PTz/G1pam1/a3nt+nv+cfjwb3N+b/Dg/2xobv5+eP337+t8
+b2l5f/T5b352fXBj/PX56+Hh7+/5+31sbvTofnzv+nR1cmZ8emx1e3/4fmdk9P9zfnlpb+z6
+fvvs4vtw8PHz7uns7+79+P1ze3l7c2Zrb3x7enl2enP3eG12dvnt7On0/fbu/W99eG1zcG5t
+aGz16vFscHzycW18++v4+O7u8O3j7uzu/e3x+PPu9nFmYmpoX3BucPf9/fF7bGpu/vbv+P3v
+eV9xc2l2dWtu+O7p9/Pe7e/p+X796vtsb3j/b/rs93l/9/726O7r/GppaWlnbW5w+XJtbGtw
+d3Zpb/vw7+r3+ent7Ozv7+Pf6XpqamJjfXdvd259/vR+9XptdHjw/X36f/f9+vH6+fV+evf0
+fvl6f/du7vpv/m16fmtobnZyfHZ35+bv8ntwb3L5fHZventxdXjv7Xxwd+Hc6+n3efvvc3Z/
+aXL8cHF0Zv56ZP7r8vZ1dHB8amVuaP58d2xy6u7w93X7+uzm6ezr6fh59/B6cXN2a2d2+vl2
+a3fx9W9wfXFvfPru83d49+nv+XxsbWl5+Wxvdm9veHNyfO/o7fV7f+7u7uzu9/Tu9mxjcnt6
+6u3573JpeHxra/v5dXNpdH167nzzfnj9bHxtYWr883Hx5eDf5+vm7Xpsb3Xq7fx7dXls7+9p
+ff5tZGRjZnr+8+xxavv39HJtfvf99e77+H50a/n/a+fsc/bw7PLw9vrt63pu//L7d/l5+/39
+fHl1Y372ZHFuanNx/nX+92h5+XRyfevxfPvv6+5x/ezm7PTr7vpvevNz/e339m7/7/loYWlv
+eurtcXJ1dWRmb3t3bGBoe3tw+er1+nvq7O7p7OPt9Obs7W928u/o9P3+c/htamxrbF5hY19p
+/fR9+Pnz9X/4+Hz5fO/m6OXv8u3u/nF7eG59a2RpZWlu/G1t6uLf9nr+b/tvanV3cnltbH7y
+8u/37ODte+vq/O77d/hva3Nsa3d8d3/teG1/9O/96vxs9/X/82xs9PN0a21u+H5n+u72eXn2
+7vb56/dvcHNucGp46/h2/G185+zv8O7u6nR65ndr/Ph3//ttamVpeHn69Hr5+X90cf9ybmNt
+fvX/+fbs7/7i6vL9+flt/vH893Zsdfh5dGps7+n++e5+/v1sa2p37ff9aW/y/nJ7ePjq82Vg
++PXz8/fi7PP9bG5obHP/bGj/8/Vtfuv3/vj2fnZxd3x7+Xn25evu7vL3+/PzfPH3bGVqavdu
+bPNw+u3y9mtoanJzcP3y9vn2dPnm9W5u9+n6f/B+9nxu8HJp8Hr+93f2+P51++j9fn5/7Wpl
+eXZ/e3j3e3Hs5/Tp3+337fz9dfL5bGNtZW97Z3F58XJlcvVvcvdu8vp2d3zufe7d5evnf//3
+bXhocPdtcenj+3twdP77+vfu7vdxanD8fXH68OlxcfF5bHT57nJt8H9pbnz5eG/u+fbm6O/5
+efl0aml28Xlxb/3te3x0enNobXT86ut8d3Nv4+f8/PB4aG5+fff7bvz5/fB87fDx7nXxfmVu
+93xmbXfp9Xb8duh7bfLq5nt36udoX3B2ZWb+fej2ce50aXX682l17vTr7XZz8PH0eHl2afpv
+e/h+9u/p4u998ur9dXV/fWrw/GJqe/vtd3fr8XRvbH3ufX3xfXV8dfdyd/Nud3px6W9v/vNz
+eObn7Gxv/PZtbev+/fV98vD2bm7sbHXreXr99Xbs6PVq6uZweGl3+31vZntzcnh79XB4d//q
+cnt5b25s7fpyf+3lfX/46+327/by9/VpZvFocu1peHZ2fvP8a/3/f/l7Zf7qbv7r+H7p7Xh2
+e+vo+Pf26/JyZnzxcfJtaXRobGdnYG7p8f9q5t7v7O7t/vTsenFtdf7m8m/0/Xlt9+/7dlxg
+/WVub2r9e/P99P3u3uXk7Wb26H3u4vtu7+7t835uZXdubHFjWGN4bnhxc+9qb/Zx8eL6eOTf
+73rt5+TweOnn+u3+Z3r++O5zdm9faWhq/mVte/lodfr+7HNz6nZk++jt+f1u9+52enhx/Wtw
++ePr/+Pt9fVvfnzv+/r2duz+dPd6cv7p/WZmcG9tZmNsbXJx797h+HDp4mhgaPlxW3vm7+jo
+3Nv9bXV5d3D+e/Ds7+Xz/nlycG5ma2pca2hjbv3u5Pjn3Pzz7/z+/Wth8/v7e//t6Ot5cnxv
+73Jj8fvk+mFvcfF5b/r0eHH++Ppu6utt9un1/P1/fe3y9vp8a2praW3s9m/u+fnvfO/uZmZ1
+e15abH74em/r3PVv8uXi6vT+7Ob69fPr7HF773JnZWpvb29tbvp0ZHTz6nb88/L9aXZ7Z/zk
+9O7u5+H37fBtbHhvaHb3+Htw+fb37/v59PtpZ21paWFib3f3+/nk4X185+jp/Pvy/P795t/p
+/fPr9mtrbXBmZ2ZgbnR2X2T+cX717PJ4fe7h9Pbt6+//e/Pj+Hf07+b3Y2xvZWVhbPN2cnT0
+bG5z6+h5/nvs9Ph38+r98u/j7nd4fu/0bG/v6HRdavb4b3X96Xtrb/X+bnvx7vHu+fHxf3Vv
+a3V+dG99+PhtZ3Z+dXbo6/HqeXD9+vTu9Px6aXP/8Ony6Or9/P3zcGRta211b275921sefd7
+bXV6cn71++zq9n319W778fZ6+/Hv7O/wfPrr6nhseG5dX29veXL9/XN7efvt937y7v1w/PD0
+eGxo8+z7/PLw9fvt73hydnN68e3v6uzv/fT3+3lgaWxmYmZscntxeu/teWd3+Xbr6erm6+7x
+6+vt7O7vfGltbHV8ef577e93e3xvZGt5dnF3bGpyefx++fh2+n726+7v+3lrb/707P17fvP2
+fXR77PV88Ovr6+vv/npxc3RtZGh8b3FnZGVfbHT88e3v+3j86Ony7ejo5+369np99fDt73xs
+Y15eYWlu//x5+/vr6Ofq8evw+Hx8c3Z+//j3ePp9cnj9e25udm1maG1++HZ0/nvz7+7u6urr
+7O3t7+Pl9e/s9Hh1e25pbHh+amlxc21ocXX//mxtbXV0fn52cHjs5eLq6OPh3enq7vl3aGhs
+en95cmts/394d2lka/t9b3J5dHR8//Pq6+/17u79fHd6+e3z+e73+Pv9c2h36eX2fn5zb2xz
+/nt8dHJvdX7//P708vR9bGplaHr59fv78/d+ffL18u/z/fz8/P7/cX7p63x6fG9xfXxzcHx4
+fHz99PDz+/57dnV67+32/vny+Pz0fGtyenz89vN/eXt5fvh9cm//dW3+fnh2+vH+eHJ3ffPv
+9PT17/D9fHJ48O/+dv56f/71+XpybHVveXz4dnTu6unp6/P+/fb8+PVvaWxqa3d+eW158Ov3
+dHhvbXP19vfs5Obr8O7u7fH7/HJwc2xzfG92em1pbGxqa3r1+Pj0+3R+8unh4ujr6fP78nhp
+bHFseHR1dv58ffd7d//y7O/7cXJ/+/1ua2d1fWpscn358OvyePnu8fPq6uXp+3p/f//38Ply
+bXFzb29pZGVoffD2cm96d319+/j38O/v+v3u7PD29O3v9+/w/X36eWZlffV6bGt1ff10cXFv
+d/L1f/52bnZ3cn7x8/L58vj17e3s6uLn+n3392ttb3h0ZWhxc2x3f3ZtbXVybW16/Pv07/Hv
+6eju//nv7u/8+Hx08unn7vP2+3JvbmpsZmBkaWhvfvPp6+zv/HZqcnl2/HltdPHn6+/y+PH4
+/3dxc3j57O////749u/t/nZ2eH76dXd+fn14dH57bGlrbGt9eXL8+O34e/z27uvp6fD09v9u
+aW1tefp2evL1+fv27fP49Pb69ndzb2p1+3dpcfx2bHr+c21sbXX/++/1dvfs83x6fXdzc3p4
+evry5ujt5uLm6uft/XV1aGNjZWppampzdXNtamltdnp3ePHv8erl6+7m5ezw6+3x9/308/n/
+emlpc3dxbmtmYmdtdHx5d/708Ozs7vt5/nt4fPLsfn76fPbo6fX+8PtsamNo/vPy8vv7+Pzx
+7PL07e19cnz6fXn5dm17dndxbnZ7fHhvaW1vevf3/315c3r26uDm7vbs6fb9+/H4+/1xa2lr
+YWFvb3Zze/Pz//bp6O7z7evx/H75e3r+/vl/eW9ucnt8bmhnbnt6enn+7vH1+f5++/f9fHZ/
+fXn+9f11bWZe/tH55OZmVVHb4W/k4+xsUWTZZ1hozszk69PMUkfhYE554GXe32vo3nZvcXNO
+V2xPXnbVaMzW6v7p5kvf5V7v8Vpx2mr0ddr33vFr3nZuXP5oXuxh3OzoafBned5eXtzjZnPf
+5uv9XfPf9Vtn4l9v6FLm8mti7+193XN04W9j9Oj1/29nfe7r3eZz7edl6vNZcvxgXWbr/N92
+V/NsZm5ybXfkbW3n4+Pr7Ovd293X297f7O7f32hx8ldVXllKR0M7ODc5RVzcxbq4urvAx8vQ
+19jX1tLW3dXHvr2+xsTGw8soExUdHi/bvqebn6yrsvRMOCQjLTNQuK+rpau+0GNJVF9c0r+8
+tbK0srdMIxkQEB8za6qfnZqcqsJBJh8gHiNPqqGgpbO/2D4xPWjIuLzDtqqmo6jPJxcSDxUf
+PbSgmpyfp8U0KSYiK0TqtKSjq7TQQz5CQFfZ2tfIvLWrpqWoySUcFw8RHz25nJicn6rmLykm
+Jjm/sa2rr8thVD5DeeHs0st+5cW6sKuqtHU3KB0VEho8sJ6Znai2ejIpJSlAtqipss5sVElO
+ZdzAyE88PEzGramqq7HNQTIoHRUSHsWgmJiiv1Q4KicrNdarpa7MS0VGTeXKxMlbNi45362f
+naOt2DEqLi4iGRghvp2anKxfNDIvLjBDvqqnr+I6O0NIZ8XE2FE0L0y1pZ2dqsZINC8/djke
+EhQvrJ6bobpMRDgrKzfQqqKryzsvMzhSxba2zj0vOsirn56lvzsuNVe9s8geDQ4gv6CanKm8
+Xy8iIC3aqqCktEcuLTNLvqytyDgtOdasn52n0jEqNv25seYeERIfy6Wfn6a0Vy8lIzO8qKOl
+uDcpKzBVsaWr1DMoL9Won6CrzUQ5OEvRx0gnHBkfPLWsq6qtun86MDZNybS7TUJKSFTGraet
+7CwlL9WspaesteA6OWPHbzckGRspQ8i2qaSkqcU0KCk5TFjYu7a7wMfBtrPLNCgpNtevpqSm
+sNs9LzNavlIlHRoeK9CioJ+eqecvKSUoNme5sbC0u8LGu8JcNystONq0q6SlrNc2Lj7EX0tB
+ISUoJ0fOtq6wuN1JPjw+TM+8uL/Q0MW7t7jHTDYvOFHFtK2rsc1GOkVJP1TV1DwdHEFUMEuu
+p7tGOEu4scTHubpfOz7erqewbzs7OC89vq6xutJIWOTvTEpFNC0rMTY4Vu3Yvbm/0Mi+wsnY
+3uHV2fDDs62vyz8zMzg/VuDEvb+/zHtSNSIjN0o4PMa3x1dPwa+02FnPwOBFU8G+ycm+usDO
+9GlKNjM+aFdOw7O7304/Ni8vMDpoxMXSyb/G1eDPzvZPXtHI1/3Pwsrf/tHG1lJRT0NOTUrc
+zdzR11hBPUI+P0tJZ8PEy8zEwdt1bGb5Tk3KvMb66MnTTUTZwdxbXVFQTUVO4srNzs/WbEVH
+SD07TtTP4NjV7FBHaOZ6/d3Ly9vf1tx4dNTYZPfH0f9k29xi6+vbzOtLR/ZQOFjaTWjN21lX
+TVBfTfptUmNs89HZ693h5mrPz83Kv77nUstNOMrN5NPkc2tGWFJEbNvcUlTYWEHmvjrj5TfS
+dUnG4XDFPt/ASc+60ui/RFX7ReLF2eXL4WVeVFZW5Gtq0lZf7undWs9MRt8+YV//x13v4Mxc
+wNtowkpnQmTWR2i812nSXVTNcEvhyExZ3elV7GDZbGbcRmxiVljZ1u7NYMVh3sxL1VVPcWtT
+1v/Pcs5bTs1E3Pvg8+LLS8NZW9px3VrkTU1dWt5N1fPl207T3+7tXfBj9Gjm7N/cfcRgRc9h
+U2XY3FHN8Fjf2EnY1k9Q3E9YS75LashadlreY9bW23jNZ2prbWRoy2Pq+PZq0nVT1Wxd8nph
+X9ZPXNhI7WpX3/bMTHXbZkvAYfrB5d1iz0zMa0TC7ULPxTvn2VZHx2s/w/FBycQ8c70/T8VL
+ULtRR7/pU93lZdTaY+TeV+Jfffxj31jkak3YZW5b3uVecODJ8lTI1lr9WmVXXU/O2UTc1mlg
+69fs01BnzUnjV1/haWpu1k3Y7VjP10nF7ldkuUpVu1pJ1XBHxtYzvcw0ZLg9QrlxPL9mSr9U
+Qb/FOeS8R1PHUevQVPnGUmnEW+3a63teWWtrR+fYRHPmXmT84/Zp5OXufdJo2NJW2+hgcV/u
+Yenib/7272lm49vfeFzufk1dzk5IznxW1tdh0+pf0t9P3NxPaelrXHRicu5iaNboZNjeWfXO
+Z/XVfGHaXWnlflrc6FpqeWtS7u5SWGdbTn1dU+Tfa9nY7NbN18/C3MzH3svI39bB2GdPRkgz
+LDc2LS9CRkfl0by5vLOttL21u9bT5VhNS0xa7V7cvcPoy8lUPT02Ly8tLTY9O1XIwr2xra+z
+s7W9y+BtSz4+RkVIdM3Fwbu1vc7STTozLissLSswP1DnvrSurayrrre8xHFLPTo4OTtEb9TJ
+uLKyra29x9I4LC0mHyMnJi1AXsWvq6ijpqqrtMprSjkzMTA3P0vVu7awrKqsrrXD3zoqJyIe
+HR8mKjJMu6yopJ+epauvu3Y7MS0sLCw2SG3Ht62pqqusr7e8VEA9JyAlHB4mJitFyb6qoJ+f
+n6Ost/o7NSskJywwO1HVtq2uqaerrrLC5d5ROjUpIiUhHycuNlfBtqifoKChqLLESS8tKiYp
+LjVO1cm4ra2trLC1uMr85n5HOzQrJScmJi44R8u3r6eipKerus9ONi0qKSsyOkfTwbexsbGw
+sbm+yOrk4O39UEE3LSkpKSszPVvItq6rqKqut8doPzUuLS8zPVHnyLy5uLO1tbe/xMvKz87K
+21xFMCknJSgrLTNL1LyuqqiprbK70E07NC8xOD5La9bIu7S3sqyytbvCx9dlTEA2LSclJCUo
+LjlSy7itqKWkqK63y1U9MCwtMDhGbNK+uLOvr7G3vM13bFxYUU5IPTIsKygqLzNB8MKyq6em
+p6y3yFY8MS0tLzZBac+/ure0tbe7wtDncGrp2dveeEMyLCcmKi04Tte7rKelpKqywms9NCwp
+LDA9WNnBvrizuLm7xcvP3djOy8fK1lo4LCYhIykuP9+9rqajoqavw1U4LSomKC8+47yzr7C1
+uL3Hz9r/9trLvru9wtk/LCQeHiIpNXe6rKKfoaWvyEYwKCQkKTnjuq2qq662x/hSS1j42si+
+tq6vtck8JhwZGh0jL+Suop2cnqSv1TgoIB4jLUm8q6Sip7DEWD47PEdm2MW4r6ysscY4IhsX
+FxsjNcKmnZmZnaW0SywhHR4jLlywpJ+fpq/KRTMvMT7+xrivq6eqtt8wIRkUFhsmPrikm5ia
+nqi9QCkfHB4kMO+uop6epLPkPC0sLjhewbOsp6WnsNI8JhsXFhgjNOGpnZqZnam6aSsfHh4i
+Lkm8pJ6foqzISjIqLTVCz7aup6Smq7pPLyIZFhccKD3ApZyam5+qu04qHx0fJC1MtKKeoKOs
+x0MvKy84R8myq6alp6y9SSwfGRYXHSo9waWdm5ufqrZYKh8eHiErR7Sin5+gqLtYNi4wMz3Y
+ua+ppqiuvkkrHxoYGh4lNr+qopybnaKu3jkqHx0gKjjYrqeioau6yFA3NzpH1L63rquvuMpI
+LCIeHB0kLDy/q6einqKqss8/MCkjJy40XLm1r6qwvL3cTWzk5Ma9vbe3xNPgRCsmJSAgKDJF
+0bqup6aqrK+9X0I6Ly4wM0fda9q2ucnBv8S+vb64uMDAweVHQj8vJSUqKiozTt7EuLGrqa6x
+tMJiSTszNDI2SkpE2r7Hv7Oys7G1t7fD2Nl1Rj8+MyspKyoqLjhH4Mi8ramrrKuzxtxKNjQw
+LDQ9O1XDwLuvra2usre3xO9vVUI/PjUyMysnLS8sNFXkyLOtqaaorrK+Zj84MS0sLzs/Tsa5
+tK6srK6yt73QWkpIQEBGPjc3MCkrLSstQPLLtaumpKWqr7nXQzcuKCYpMTZExrOvq6eoqq+5
+vtBKQEM/QEtHOz89KSUuLCUwe9a/raijoKerq7lMOTQpIyQpMjpOva2qqKOkrLO7zVY+OTo7
+PEVdRTY/NSgsLiw3TVrCrKuooKGqr7fyPDAoJScmKjlXyrCpp6SkqK64zFo/NjQ0NjxJT0VB
+QjQvOC8tRFhQw66tqqWprq68WEU6LCkrKS49QnKzrKympKmttMnfVDgxMzEvN0dAPk9MPkVH
+PU1oWfPCu7u0sLS4u8reekM3ODg1OEdb1ry0sK6wt8DSWkA7MzEyODxDadbKxcHCxMrS0uBm
+YFdOTFJSU1pVU1VXWl906d3WzMbDwMLIzNX/WU9HRERHSlR44dXPzsvJycrO1tjedVhUVk1M
+TUlGQ0RGSlZs6trOzM3Ozs/O0+dyaVpRTU9YWl5u59TOzMbDxMvR5d7M9urY4PHxblVOSz86
+Pj03O0lNVeDSyL68wcG/z+VuXUtBQENLSk/0zsrGv72+v8nJxdff0dZ6cdl3WVxNRz0zMzk2
+MDVIUVfWvbe0tLa0uc1pX0o5Njk6QktN4L++vbSztri8xMzdW1tZRkdPUlxZQ0tMMTE+OC47
+UU/8yb+0rbe8srfV/PRLPTg2OT9BTdnAu7ewrbC4vcbcUz88Ozk8R1d8997NUDdITS4tRUc8
+TNO9srO5rqq44M3ORzU0ODk1OV/Ox720ra+5vsDcTEdGP0FETt7V3sPJUkU8ODctLDtHPEfJ
+trOzsKytuc/b5UU0Mjk9ODt+ycvCtbCzusHEzGJKT09GRE586eHMzldNQzEzOC0tPUZF6rmx
+r66srK+60/NZOy8yODYzPv3XzLuysLO6vsPdVlJUTUhLXt7Z1svTWkk9My8tLTAzQXzKu6+u
+rKyusr3RUjw2MS8xNDpMedC8tLCvsbW5yOdrU0hGRUlk5vThztVMPUMyLDguK0R0UtW1rq+x
+ra+3v2xCQjguLjQ8PkvRvLavr7CutsTP1WBCP0VJTlVn1stvR09RMic2PSotT+PWv7asqqyy
+tLLCRj1AMy4yNThP3M69sbC0srG7y9V7T0U+PkRKU13cy15EcPM1KDVcLipfyd/Dua+qr7u7
+ts0+PUk3MTk8TNnPxrKvtri2uMbhYkxAOjg7QkVIYdTJyMjE5UM8SzImOzYx4lNKrqy+sKmu
+vcXSVUtCMDlPPD7xy8W/u7i1ucbO0GRBP0VEPkNcbfng2cjJfuXUPTRMOis0PTtKX16/sr28
+sLW9v83f2l1FT2FNTurPysXGxb/J2fB9bElARU9CP09bYHpWXOnWaHba4utWU2NOSU1SXWpc
+XtfR7dvJzdTX2tLU3d/Q0trX3tLW6XzW419TbF9FUlBOWW9WdOjy6GPj5nN5YOxkYFZX42ZX
+TXpvT/Jg5t316vHM3HrO0c/d09zf3e7q/fn6XmlhWl9a6GBp5Gbp5fvg2Ht4/W9lVGhgTmNn
+WXD7dn3m697v7nfm7lRtX15vY+/82N7W19Xr3OzuePBnXutveWj47/Dq8dnr6fV4b3JRWF5Y
+XmTs5XRv3ezp529nZF9qZ3hmZn3lZOXs5/TxdeTm+f7g+fb2cff25ebx3Nbx/n15WFlpXnBh
+bnbr4+bb5fV6ZF5dXXp07vv52/vlfPJialtYU1VZV21l+W7W19LRz9HR1efh+XFdYmRiX233
++vbc2dzh+HlkZ1hWUVhdV19y+tzZ7NzX5nBuY15aUV514+Dc2NnV4tnf6X1iWmVfUVpZZW1u
+5d7a4Nvq6d7x8P7ub29qaPBq7+zf/u1rbXBnXFZoYHBm7W9+/ufj7e7l63Jp+m9q9X/14PTb
+39777vJp8l9ta15fX2RzcOjr9vbj9e/za290dH3y4vjm7+Dz5XXucP1daGlubFxgW/l19+/q
+3/H28+TsdnDs9PT55OD08PPrbmpqb3JjdXD+aexc+XX3b/zscft4/2D+b/1m6vL37fDj5ujr
+4d7w8/LlfGduYWtrZm9me350bHpw/nN16u74/OXc7G5teHZqa3B8fmtmfPVwe/Hq/uDs7Xvv
+8//u8uludPFx63P7/OlqZ2Vi/mdubX7+buz16Pztdu1+dvBzfW/wf+fw7Pz1d/fnb2/wanBr
+cHP5bnZ9fXl7/m3/fvnw9fB4/fFs+f3r6uLh5P3rb2xzemhpamp3Y2h0b2R8/uznfe7n4vL/
+5+TvfXNxZF9rZnFrfPH1ePvk9/b87fD16vDv7f/q9Pz5bnX9bW5rdnxyZ3zyafh5fm9pc/du
+cGVoe/rvfe7/7eTt3t7g7vbo7/z87edz+vJmZW5+X3PsVG/vanHpYud9anz5a2R87+17fn3t
+9njn6ejs5fX853hpf2dc6fr+7vlia3pjcvhrcev76e3mbvnref3ybv17/npyfW3f7eP74fZ2
+aWbt7PZb619vZ2/rZ+5w6277XXJ5eOt7e3Xqbvd07ujweX3++exz93t0cOzn/d7h7u/3bvZ1
+c2N6Z1lv72JkaXzp7m563O1lcPPe7v73+fZoZ+/9aHn8am1udXj66+rx8uzjfvn19O32+3n6
+bmj17PX17vl5bGhveGxfe3Vi9Ojn7uzq9n7+9/p5/eptZ2hsbWBdXGBfWVZVW1df7+PZz8/J
+xsvKysvU5O3+bmZgYGRp8t7d9mpUSEpDOjo/PT5FTGjWzMO6ub28vL/G0+n9XExMUE1MTFFj
+eOrSy87LxsfHytrf22RIQ0Q9NzQ3PT09SXPd08i9uru+vLvDzt3saFNMTU5HS1pt6NbKw8DA
+v77BzNLSekY+OzUvLzQ2NThDZN3Rv7e2t7a0uL/IzeRYTkpGQUFIS05Z3szIxLy7v72+w8/6
+U0c+ODMyNDAyOD1HXtjHv724tLa5ur3CzN3tWEdCQ0FDSl/+69fJv76+vLzCy9ZnRzw2MjEw
+MTI2PEVQ7sm+ubWztLW5vMHL3/VdS0lHRkZITVZu28zEwL68vr/G1vtTQTs1MTIwLzM4Pk71
+zMC8uLO1tbS4vMPWelhLR0VDRUdKV2T/2M3Evr27vL/I32dYQTg0Mi8vMDY9RFbaysO9t7O1
+tre6xM7jX01FQ0RCREtRXGzbzMa+urq6vL7G41xPPjQxMC8vMDc9QVDaysG7tbO1t7a6xszd
+YE9JRkZAQUlOW+7PyMK/vr6+vcHM52JKPTUyMy8uMDg9RlLbxr+7tLS2tri7w8zUdVJKQkBB
+Q0pPVGvg1MzGv7u8vr7G1m1PRDszMjMvLzQ8RE/xyMG+uLS0t7m6vs/a4lhLSEdHQ0VRUlbv
+08vGwr6+wb/Dzt5YSUI4MDMvLjM5P0xd1sK/u7e0tbi7vMXW2ftPSUhGQ0JKUFNn3M7MycG9
+vr3Awcrpak08NzMvMjEyOT5Ha93Jvry3tbe6u7/I3O5+TkhIQkJHSlZjYN3My8S+vby9v8LJ
+33tYQTk3MS8xMTY8RFni0cK8uri4uru+yM7lXFJOSkhIRkxOVW/hz8fEwL/Bvb7GwtB0WEA6
+OS8tMjAyOT9T6ti+trm1sLS3u8PH21VTTUA/P0FFQ0xrf9/Kwr+7urq5wsbF509GOzQxLS0x
+LzM/TGTOwbmztbGvtLi8ytlhRkhBOTo9PUVPaNfKwbq5uri4ub7IzNtXQjk1LywrLzEwOkxq
+18K4sLCxr7G7v8l9T0U7OTo2OUJGVdnLwbm1trS2uLq/xdJjUEU2MDMuKy0vNDk/XMvCu6+t
+rrCwtb3M9U5BOjQ0Njc8S2jbx724tbOztrm8x9PoYEpAPDUxMS8xMjM8S05uxbq5tK+wtLe8
+xt1WQjo6OTI3REpL9MO9vLSwsre8vsbcZmhYRUE/PDUxNDk0Mj9STVPMuru6s7G2vsXL5FJB
+PD08NzhJXmTZv7i4trO0ub7I1OBiTExPSUBDQTs3ODk4OkFLT3zMw766t7m8vsjiZVhJPDxA
+P0FS6tLHvrm3trm+wMfYd2JbVE1OU05IRD88Ozs5Oz9GT2PdycC9u7y+vsTR7mVSQ0FHR0xe
+4s/Iwby6ur7GytLsal9ZVVdZVExHQjs4Nzk5O0NOXejLv7u4uLm7v8jZc1ZHQENCRU5n3c7F
+vr28vcLJztfsfG9gXF9aTkxHPzk2NzY3O0FPedDEvbq4uLm6wczdaVFDPj9DR1fz18rCvr69
+v8bLz977ZFtaXmhoU0lCOzg4ODY5Pkdb2cjAura3ury/y+RjT0U/QEBDS1/nz8O9vby7vcPM
+1uxzb2xraGxuYE49NjU2NDU5PUJa1cW8t7e4urvBzuJkS0JBQUFGU/3Yx768uru9wszZ82Vh
+WlZec/1xXEs+OTg4Njg8P0Zc0cC7t7a4urzCz/hXSEFBQEBIW/fTxL++vLy/yM/Z9mVcWl1f
+cu3wbFM/ODc2NDY6PUZizsK8tbO2ubvC1PlYRj48PT9KX+PLv7y7u7u/xs/nXVJZWVNacO3u
+9VtBODc3NTU5Pkdozb+6tLK2ubzE31lLQTw8OzxIYtzFvLm4tbW7xdD0VlNXVlVd9d/ib008
+NTQzMTA2PU7dxry2s7G0uL/QdVBGPjk4PERV38i+ubWztbnAzu5aS0dKTVZ84d/qZE9ANzMy
+MTI5Rl/bw7q1srK2vcbWYkk/Ojk7P01l07+5tLK1ur7G2llHQUNMW2r8283XaEk6NDAvLzE6
+T+XFuLOvrrC4xNheRT05Njg6QmjTwrexr7C2vMbWY0c+QEVS7t/aztb3Szc0My4uMjhH38C6
+tK+vsrnE5VVEPTg3Oz9R5M2+tK+vtLrAz/tNQT9AR01d08PBwtdTPzYvLCsuND1azryyra2v
+tb3NZkM5NTQ3PEFXybu4sa2vtbzOaVJKPjxEVGrPwb+9wtpKNzIuKSouM0LxxrmvrK6zu8z3
+Tj02Mzg8QFHUvrevrq+yt8LdW0g/P0FJXtfIwr7A1VdBMi0sKywwO07YvbWwra60v89tRz05
+Nzg9THjMvrawsLW7wMxtSUFCRklQatPDv77By25DNS4rKiwuN03TwLewrq60vs3tTz86OTk+
+TH7PxLq0tLe7v87zU0hHSEpWe9TFv8PIz11GOzEsKiwvN0Npy7yzsbK0ucHUaFBHQz8+R1n3
+18a+vLu9v8fS611TT1JaZOjPysvO1nlMPzkyLS81OD9N6M/Au7m3uLvFz95kVExGRUxebe7X
+zMPAwsXIzNbmfWtncu3m3+j5bltNRkA9Ojg5PEBIUXvaz8vFw8PBxcnP2e79dWBfbXRx5uDj
+2tTS0NLX3N/h3dvZ2+p5aF5WTkhFQUBAP0JJTk9b//Lj1c/Qz83P0dHX3N3b3+zq9Ozp5+Te
+2dvW1NfUz83Oz9ToblxTSkQ/PT09PkNKTE1edXjh1dbWzs/X0s/W1M7T3t/l9O7f4+ne3eDg
+3d3Z09PQztPe62tWTUc+Ozw9PEBHSlR09fPXzs7OztLWz9fd3N7p5d7v7+Pf4OTh3tfX2tjT
+0dPQ1dztd2RUSD48PDs7PEBFUHvp6NXLzMvLzc/Pz9TW4eTi8P/67ODZ297e4OLg3Nzg39nc
+7XNvaVhMR0I/Qj8+QUlPWXDz38/LzMzLz8/N0Nre5fLi5vXp3Nzc2Nzc2Nvm9Pn7+HxuZ2Fd
+X2JQRUFFSEZFRk5hfuzk2dDOztPY3Nvc4ujv8fHh3uLc1s/R1dbb4u/t9nRs/nhkZ2tlYVxd
+ZWRdU09PT0tKT1FRXWxqb/Tk49/Z1NPSz8/T09LU19rb3N3g5+rubW96dX327HxvbmNaU1hZ
+Xl9eYFtYWlxXVFxdXmFbWmb6e+/e2NXOyszRz8/Y3+fl4uLt/v1se+fi7O3u/nj+bV9cWE9R
+VFJZX2hqcHt29On5bGNlZ2xpa2n46u3n3dna3uTi6OTp6eXu7Ojm7OPi4+ff3+t/Y15ZVFpe
+ZGh3fn5xdHFsZGJkXVxcX195em7559vk3tzu5t7i+vvy8+nz7urn6+jl7/Vzb3BsbWd8enZ9
++P9zcH1xYmdpYl9udn5xfXru5vTg3N/v7Xp6bXduaGx7anF1eHTzdXJuc3RjeXnx+vPi29vq
+4t/f6+//a252bm36/3x/8/fw63ZubWdeXmJfZGp87uzu7Ov7a3Z8b3F+dfp6/PPt3uHveu77
+/Hl+bGj7/fl57vT0++7j73/u4n1tcX5vb311eW158XJqe3Jf+2Zr7+d26/t+eebzeG5obm5m
+Wm95YXbr4ufe6uTk3OfY6Nvx8+7pb2F+b35gVl1WVfNhbH3n5uj08u3r6nR+a375fevy82/w
+b2N1/fB1+Wx7cPRpbnjk6nfy9+vs5/vh/ux6Y3Nbc3T6e/Lhcvn76tzq+3n5eVxbaHv4avlx
+d+lzZHJ0de773ubv+eZ8fvh17231bHl/fWp57OnwceXt83ZxbHvk/HP35XXqcXT09mlb5PJ2
+ZGP/dPZpeOrjbXL1/f9xb3/r93597ez3bW7o9H95dPd6anH17ufg3+Pt8PHx9W9sbHBwZWRl
+Ymxvb3Z65uNzZmlqXVpZW19eZG3t3dzZ0tDNzs/Q0drd3t7d2NPV0tjW0+hSOS0uMS0rM0n+
+yr62ramstrq/20k7NTQ3OT1N1cC8uLW1uMDXdl5PRklRZN/Oxb/E20w2LSwqKCs3UtK8sKuo
+qa65yGg/My4uMTVAbsu9s66ur7fBz/VRRkNDS2ndzr+7w851OikmKSgpL0jIsqyqpqWsvetG
+Ni4rKzFBZtfFt6+zubq9zGxKRk9SSU7exb6/wbu7z1xDMScjKCwvP8awrKqrrK/ASTQwLi4x
+OlHHuLe3uby9zGFcaF1QVGL86+vdy8TGxsPG3FVNPi8oKS82PVa9r62xt7m+/Tw0OD4/Rl3K
+uLi+wL3E5UM/8vlJVtbN1N9l2sPN3srCzt9uTEVLOCYoPko9aLiws7rL0sZgODpLTU9v6ci3
+uc3Vz2hIR0tX2c/fz8XL2dHP3dzb3tLM1+z/fmdCLCIrQDs1bLWwt8DLvr5jPEnuZ05Secm8
+xt7Qy/VKQ1Pb2+rWxcDI3fvk3HBd7s/Jytl2dWRCLCEoPT84ZLOtr7zPy8N8PT5RV1tddci5
+u8jV5V1RW1ha08bMzcjL0XlPT2FfVWbSw8PWaWNqPigkLzw/TMqzrLHE1s/SYEM/RVr4duDI
+vLzLa1Z5alFg2MfByNne6FtIRk504dnIvL3Q9mFKOSkjKz1MXsCwr7S/0trhVERCSFfz5NnI
+vsHcV13laFH6y8PG3Gz56k9CTPjPz9HIv8DN/ExJPiohK0JOaMK1r6++4N7gWElCQlTs79nE
+vr7NYmbnXk9c6M7L4Hjk2fVdWWba0NrdzcnScllRRC4mL0FGVc67tLK9zdPdb1pHPkZga3/V
+ysLH5mjm+WByftvM0+/n2uZvZWrk3N3XzMvSeU9USjYsLjlCU2/Qvra2usbW191gRj5CUmhk
+XfTRyc3OzNHQ0dftZ2FvdWhjYWju4+7v8fLo72dr7e1hVVlcWFJOSElWX1pZedjLyM3b3trg
+92RTU3Ta1tjWzsrP81xYX3xuWl7m1dfc3d3b33lodutuT01UUEpHRUFGUl1l4s/Nx8XIz9vj
+72tWT1dy4Nva09HR1/JpeOt+a3Tk2NTW3ebo7P11fHpYS1BTSEJGRD5IVVv219PMxMHGztvr
+Z1JIR1Bx3M3Hwb/Cxc/jb2heW2VsbO/b29nX5/nqZjwwNTk3PUdMesS9u7i7vb/OUUlLPz9J
+SEhsz8a/vLy7v9RtXFBJTVJXetHKxcHCwMTYVEA8OCokLjk/9cS9sq2yu8bmVk0+NztJXN7N
+z8e8vcjP09babVVVWFxdafPb1NPQzMfJzc3+R0I9LCUtPUFixr21rrTBzepRSkY+PUpo6c7J
+zMXE09vY6evp/O3e3eHd3d/c5G5ecuZ6XF1lXk5bTzpERz5JTk750tfYz8/OyMvpXVpdd/f4
+3dPOycrT2tTZ3uD1cO7o+fH8bmxsWVVjdHhuXlZgf3p3bko4OUA/QVPcycLDyszR29rje/Dh
+4O7r3NLOzM3T1NLZ529dWV5fXl9dXV1kZmnu72pjWVdcWFdkevHl4fVkXmhlWU5MVGR5+W//
+19DX2drWzMrMzs/O0d75bV9kc3z8/HZxaVteXFtmaV1dY15hXl1iXl906+Lm9nxra3Z2dmt4
++vjx9O7n397Z1tjd7vd8enJw8t7s/OJ+YG9nXGrte3tt+d/r/+99aWthZ/x4ZmZueW9reXtu
++/Hk6Xn/fnxlbHBqbm/0eHXi4Pvz9u5naHxx4+Tn3djZ3dfcfHJrYXP2bWFsb3Pj8GBeaGRb
+YmNjZGl86X1qevHp93Xq3uv59v1vdvd3eXrt6X598+Xg6unm+29pbHF48vzn4+zq497o+PF5
+W1tbXWVs+Hr89ebmeXFfXmVYWWVq7+Xh6fnxeO/6+fPy8O7g3uLs4er8dHvx6ujt+vh9evpq
+W1xodHH17f9wfXn85X5veWdYYGhteXh+8+ftePLt+XP0+v7s9evc7XF8+e5/9fVxau92Vl7+
+fn395t7k6PhuffN0fPdt9ent633q9WR4fG9982pq+nZleXBaXWhta+zq7vjq7n54b3f08Hb9
+/u3m5XFp8el5Zl516PHz393n7/z+9fp8+HN/73Jt/Xd2fV9jdG1qcPLt/HBrb2xfYXny9/t3
+b/D2a3jv7e3l6nno9Hfq4OP07vPt6/xrfO1zafbsfm1pY2hzaW58b/73fP5zd3t4c/D1ZG3r
++XH46u3n6vDseml/6XR18/h8dG57c2dmfu7q/H7p6vXy/nNpbWpmfvx6+O/z6un/+fL7dnlo
+aXZ87fP4fXZ7fXRmef5/dnj7cP7w7n1ud3/5ef3yd3/47+35cm95c3Bvb3X56efufX3o4Ov4
+5eb3+e76ZFpUUVFTS0xbdfPo4drQzdDS1Nvg9fL3c3fz7enk4NzRzMzOztPd6002KygsMDM/
++b+wq6utrbPFWjkvLzI4P1vKt7O4vL3E2VxFPkBISU1k2svCvb6/wcXH0WtMOSskJywvPu68
+rqmqrrG530AzLi4zO0blu6+vs7e9xt1OP0FJS1BcedXIx8nFwcnV0e9PQzQmJC83OWq7r6mo
+r7i41zowLi00Q0Vluq+vsrzT9GpDOD5V4M3N1cm/x9r0+vl+WlLmzc7W90MsKzY1NErQu66t
+s7e41kY8NS80PULourSzsrnKcT43PkJFVO/IurnCxsnafFJLVvDk3cS9y1kzJywxKy9oua2q
+rbGvumA6MS8vOD1Svq+utLrC1kszMDxISnjGubO60+vcbU5MV9bL2tC7tclELCMnKSgyz66p
+p6qur7xKLysqLTdC4bWsrK+1wv5DMjU+Q1/Pvba3xvZjVElBQ17WzcS/u7fKPC0mJSgtOdit
+qKiprrfMRDArLTA8Wsu1rq+yu9BMOzg8RU9yzbu6xtzvY01CQ1nUycjHwr3D3Vw9KSEqLy9L
+uqumqLO+vmo0LS0yQF/lva6tsLnOXEM7Oj9GY8vCxtnVzVhHTU9a/OXZxcLHw8jQ1l42KiYs
+NDZHvaqprbe6u9s5LTA7Q0VWx6+stMLR4lE6Mzxe+P/Nw8/X4VBFS1Va7dTIvb7Fx8nPzdo+
+LCguMy08ya6rr7iyr803LjA1NzhGxKyrr7a5wE4wLjQ9Qk3auLC6z+TeVD47Q2jWx8O+uLnH
+71pZOyclMzk6frmtqrDBvsRDLi8zO0pU0bGqrrrF3U00LjM+Vd/HurK1ymdbTT87PVvMxcW+
+u8DK51Rf62xKKyhHPC1Dvre0t8a4smQ1PD84OUNtuLC8v7m97UA5P09HReS6uMXIxNhKOT5S
+YWDdvrvF2fbp4Nv6YuFJLSswMjVPzLmur7a4vlc5ODc2PFXJtrCzs7bDXT44NzhAWM28ub7L
+zn1DPUFJWW1w1MLI0tXg69/db1t5dTgsQ044QtjBu7zRyLvvPUdbV1Fl3MG7xcO/x2A+P0lO
+RUXowsp03cLG+01b4/dLTs/E1+7SvrzMRzAuNy0pPMm7vLq0rrLYR0ZANjM7V83AvbWvtsbf
+X0c6MzpOYmrOuLnH2tbZVj89TFtOW9PDxs/NyMbK515fWTooLkYxNeG7uLa7vbPEPj9dSjo/
+7b64wL61vPs9OT9APD/gvry+vrzHdUpFREZHT9/Px7q60GxCLC8yKjTfv76yrq+xxU9FQDEt
+NkvoxLy1rbC/1vRIODQ3Pk1hz7i2wMjD3EhCQT9CRVTaycnDvLq+zvhkXT0sLT85M1LDur3C
+vre/TEZfUEA/VtLL1c+7vM9zXFpUSkBQ2Nlp2729zdvMxdpOS3X/S0/gekg7OEI8NUDh0t7J
+ura7ys/MYjo3QERFT9u6srm8trrhSENGPTY8W9PTy725weDtdkk9PUVRXvjPwL3AwL/K+kou
+LTssKkfP0sK1sa624OvdPjE6RUlQVMWywsW0udZoVkpMQj5X7PXUv76/v8PJ3l9KOzk+PDs9
+Uc/c6MnD2llUU09KTHPX3N3Iyuf9altcdWvuzMnFw8zO1upoYGRca/ng2uHs+fZgVldSUE9W
+XWzt9NrZ6d74T0xOPj1LRkhf+tzJyci+xdXT1+hnXW33dvLMydTR1ftiZ19cYGXx4/Tl1tx/
+dWhYUFBQTFNcVGXm5NnfaXNZS05ZXE1Ye/bj2M3Izt/d4ndz49je3NLT3N3q/PZrXnfqWlJu
+82xoeebqaGH7b11ddO9t+dzc2uVlcFxLS05MTlhZ7uLdyszb3OJxWlFeX1731c7Pz87Q43fv
+5f9hdulnY/Pi5O7z5OtjXmZcUlpdXGj05+t/+n1tb2tdWGFWXH5u7ufc2Obn2+ljavHvd/Hf
+3Or3/vD3Xmzm7O/h2trb7Xx1YWlnW2/zZ2B462xk5u5mXlxbWVZgfGxw9u3v6OXd2t7j4OR8
+b/Pt9e3n73Frbn5pX3l6ZWJdeuj57djU6/72bl1ddW5t7OLp9f3r7fl0Y2RaVFdf+/Po3+/7
+6Hxdandmb/Xs8Ojc39/l49rd6ux+fnZjZ3VnbW5iYm53bf57f/xrZm5iXW/76OPx5uJ4de77
+eHv2729t6ux/fPnycG7u92Vs8Xtx/ejY3vfq5m1td2dxZ2ztdHPz+HD+8nrzfWX+cFxqeG/y
+6Pl5fnZ6/O3e4uzr731tdfT2/W1ucmps/Xxpeev3bvT2c+7t7N/p+eTo8vH7eHpzdfd4c+z4
+aXF7Yl1nY2VjY/32+e3o7O7z+unu+/Lr3/f+6/n9enZ9/2t97Pl8ffT8aWBudWn33+Pp6O3u
+d2l6fHNpdHdsbWz++3Bkb/tqbfjzbG/v8Ors8ObudvXh3Ojk2d/z/vl8Z19oc2xkb/xvY2Zs
+ZWZvfGhlbvvv6/Lv5+787d7m5+ns63Ju7O1tfHx7bWJub2Rh9Ot1cnXx+m3s6P947unzaWV+
+f2916O5/d/ft/G5+82phbPv8fevc5Wt382thau/0aXL2dGVs5ux7+vDvcnDm5O3u7PN8+3h9
+dHHq53ZvbW1jW2VraXry+XBvdPDj3uX6+nFucn72b3rv9v/5am/w7e/57nhucXRtdO/r4+z+
+eW5scXtydXn77/b9+/3w7evn82tqaWJia/7/ffh5a2pw9vL7f/18e+3h73r5+fD07et7fu/v
+/HxrYW52aGv+eHn9+/1vb/r7em97end+7Oju8vx88n5/+Hdyfev6d3J7fPzu5uHudnRzbHJ4
+9fdubXN9/vX0eGts/vZuaG76en72fXr+/v/y+e7n93p8/PDt6urv8Pf7ffb/d/fteml3c2pp
+ZmpndXH78m9tf+rk5efu7/L9fv73/f78enPy9nv5cXB4/f5vb25maX77+vN+b297ffz1/nn9
+8O31fXl0e/n+8vX++/H2fPTq6fb6+vz+eHlqbGplaWt0ePn1e3p1/fDy8vh6fvHv7/Z29vX+
++fz/c21tcnN3/G5qa3d79fd4/3368O7u7e75fHj47ez7e/d3bGRsfHpwcXp5fP75eWx37+zz
+935we3R57Ovv7PP08X19emtsenb993xybnN5eXZ9ffb4/+/s7/P1fGx+8unq+fj4dW/98Pds
+Z3FsbXn49XL8eXBxdH58fXb+7evy9vLy/Pjs8f1uaW91fu3v/nRyen55+vF8bfXw/31z/fX2
++/X6/vr46/B/fX5+c2tobnFpbHx4fO3u//zv93F1+e3v9u/s+nn+7er2+O31eXZvaGVkbXH+
+8fj5eHJv9fR29e71fH799eru+PZ2bXJ9eGx2e3Rxdm9sc3Bsbvj59u/v7urf3+l9dH767e17
+fvHw7n51/m9kZm9xcXv9eXBsbHn1/nr8fGto/fj+9PT+/Hzv6unr7/f5e29wamZw+vH39vl9
+/Pp7+3dtefL4++vk6nJpdvn38+76dnz9935ub25vam149uXs9n5/bmpubml57ezy7enycHDx
++P31e2tpc/Xs+3d/+3pz/vtwePx1bm147Of2+/jt7u3r8ntvb3B0/3d5bm1ree/z9nh4eP7x
+//98evz7ffzw+nt/cWhqbXRxbvv6//X5cnB9+Ono/nZwev746vD90+1w83fs+uz9bGJnaGpl
+bGr+7H58a3Z5bP/67vV79fb/+e/p+P3z7Ovv7vl5cHX/9nhsdfV8bXr09X38e21tbW97dHZ9
+6vH6/PT5e/nz8vL1eXB3df3z7/X69m9v//Pt6ft4/vz3939vcv18Zmh6dG1vaXf38vH5cGpw
+e3ZwbnZ+8uLl5+fm6Ofm7vD6fvj07vhvb3Nzc3VvcfhtaHZ7c3b+b2l0b2hpc/Py8vHv7e35
+evju7u/49/1/ev/9bP7u8fnw83JtcHV7b3r68uvw7+77ef17fnz+9nR0/XZueff+fXd7fv3+
+e3lwc3z6+W9x9uzv7er8dnv6dm5tb/b28+ro7fHs8Hpzem5qamVncm7+6ev0fvl8b3ztfnJ1
+e3r8//3r7H93/fzt6O9+dHVqa29rb3V4/O7r8v716fp5/n12dXx2c3p8/Xt0c3r4dW/+/vXt
+7urr+3z/+/x4e3lubvz9bGxzfnR+8u3r+vn2/m9y9/V0b359c3f9fXr68ujp+312bGpt9fL6
+6+jufvj47+/+eG9nZ3Z1dnx2fH16e3l2cm50eXZ2/fx1cezq/e3q6O/17/v16uz5e3Fv/vbt
+9fT9c3FrbGpqa//vdnz6eWdr/nB4fH11b3J7ePnr7erp6uzu9318//z7fXP7839+9Pjy5+n/
+df93eHB8b3X7fH57dW5uc2988HVrbnB1+fn28fTu7/fu+Xr+e314dfLv/XZ/+Ph8f+34/PHu
+8fLw93ZtcHNsb3Frbmpse375d/To7Pb67e99cG58/fnz/XN38+/+fHt+dXn16+78fXtza3j7
++m1y9/v56+9+fv3993V39/b9+/Xv8O/0cW99/Pf7e2lpbmR87fZzam1tcf77bGpzev53ePXt
+6OTi3+j79vP3fe/p7PX+fHNsZ3ru83J1dWlqaW5vcfx7efn8cHN/e/zy/X15dHf76+18+evp
+7O70/W1oaWt2dv7xfm5xe/v6/vHv+HVw/v3//fn3evXs8f16eXr97uvvfG96/Pt7d3FsbnF0
+dm5mYm9wbX19dXp9f3Nz//rv9nJz8+nl4ufm493e6vvw7H50ZmVrbGtoZGNu9/p3bnX/ffd+
+++33/O3v9PN4cW1qbHF5cW9tc3X56ejt8+75/Xjz73t2+/T8fnz7e29udm9xd3l7ffDy8u/1
+eHzq6uzr/XN57PB9+vB9bWpyeGxqdXd4cnJ6bmhlZGRuff7z7uvr5+fr9Pfx/nV4++7s7nJp
+bnz37vTu7P90dm5pcPfxeHr4fnBubvns7vhtZ2t3+vl+ePbp9PZ+dW1oZWdueujpfHN29O7r
+5t/k7fh8fvX3fnlzcv33fft3dXFvfHh3aWVsamz69Hpud/f09Ozl6uzr5Ofu9PZ2e3hv/vf8
+eHh0bG97d3J+/3Zsb/b2fGtpa2l48e7v8vT28/f7//58f/f18vn57vr15+z28fTr7f78eW1n
+Zmx3bW14b2xw8vD+/H798/LzfHN2dnn+eP3t+H3+/359c3d5/nH55vX0735vb3jv8X39fn73
+fv3y/mtv9/f48fb4+Xz5/Ht5e3pqce7u/v/4+n5++fN7b2pteHf6/n57ffl2bXf46+93/fnz
+9u/8bGx1/Pn3b2hmZ3JwaXj49ubf5u3r9Xl68ezx+PX0f3t19/f//3lubGxtZ2pqa3FyeXdz
+fHt3/PT49O7u9/Lm6vD88/B1c3drZmBgaXvu8+73fvn27e/s6fN+/P16efvp5/z69353b21v
+enh+eW9sbnl6d3zz/HJ6dnh3dX7v6+zw7/L57+n3eHducW1zdm91+OzwcG397f95fHl0ePfw
+93V8fPrx6uLp8vr18ezo6/FsZWhnY2n6/HdveH7/dGp3d3T/fvT3fPn8/2907/dyeft+ePzr
+7vD18vLt6eLn+fX6+/nz8Pt4fvD0bmprZWhpdn54bmlweXF9+XlsdPbu7PD5/Xd2++/08Onp
+6Ovs8Xt3fu/t+XJ2+nxpYGVud/Xo7fp7cnd1a2t0dXZxcXh89fPr7ejt9vTt+n7v/nn+/Xv3
+9vV8/vj/fnj88P9+cGxrb3F7eHR5b25veHV4+O7z9Px69e/zfn71fP3w7/T67ufvd3V6fHp6
+fP/58vl4dHNtcfx3b/r7dmhpcHj38O3xfHj49/ny+n57fXlqb356cXz99evo5efwe21ocW9+
++/n3+/Hq8/zr6vd7ePf+aGRmaGFnbHZtav7s8fz16Ofq6ex7a239+Pnr5urq8PV6/X5yfH7/
+d3Nrbm9oZW1sbHz093Z5//b47+z06e9vf+3tfXD5dG93+f51fvfz8/nw6u35fn55a291bnV/
+eHf7+uzp+G1493dtfP5yeXvx8vr57+3s7uvp9G5vb2hscG5ubGVrcvz19/V++/f7fP7z9/L8
+fX96/vz5cHFxff7/7OXi9f7/fX1yfP75f3n1+Xx2fX99eXhsZGdrdXpwc3zw9PX8eH75fm98
++3x9e37/fvD5e/z7efzz9/Dt7O18en16b3749f3x7X5yd/t5dX/+b2hoaG90dXRxa2hqfP59
+b/zq8X1y/PTy+/X19vP28fXs6Ozu7+/37/x4/Xt3aGpuZGRmcnv+9fl2dHt+eHl6efv4fXr+
+7Ovu7Pf8e3RrcPX68/Tv73Rz+fNxbG1rb2toc3N76+Pt7vDq7v5zbvvu7vft8nlyb2xkZW93
+bW9+fP/06ePr9vf4/npzdXz9dH73+e3s8/11fvT5c2ppZWFpand9evn79PLw8vPw7uz48eLo
+9/Pr7f9xeHZvb3J3/25s+H9xfvb9bW10bmxuefX8fv328PD47vD9+/50a2969/H69u3u9X39
+/3b+92tu8/T9+/Du+Hh8/v51cHtvZmhxcGn97PXv7/r07O7t6/B2bHx8evH5dW9xfXtvbWx7
+/371/Hb67e7m6+/0f3p5/nt3fXRwc2958/N5dnZ0eHxxfHltbm57/PDl5erv9P5+eXV7+Ozq
+6vZ4en13b/fxfHh8+f59bnj7bWdsdG9z+PLv//19e3dzffj4/uzt9e/o6+np7/52bW57f3t2
+cGpxcPjs9357/HVxb2ttamJob3fw5ezs6+Tk+fLx9nN2+PX08fX4fnj19f9+emxqbm5xeXlr
+a3v5fPfxeWx5e29xcn7/fHb16Ozt6enu7fp2/nt4bml38+3n5vh38/V37/dsbnx8bW1xbWRq
+ffLvf3Ntbm//f2xocf3+ePv8dH7u6+jm7OTm7+7m6fD9/PX29Pr+bmRudnFvbnBybGtuaWx3
+8/f//Xf99/Pt5+X1/3xqaHHz7/rx7vpxbm579Pf49vHv+f51evn8/X1ucvT46ertfXXu6f9q
+ZmBfan9uZGRse/p/7uHq9O3o7+vf5fL99Pf0eXz17Op+cWhpY2NmaWtscHf+eHT99Pfv9f57
+fOjq8Oz0eHN5/PV+c2xu+/z6/Hj07Ofp7uz0fv17d390bXz+fG1rf/P6fHRucm9ufm5od/17
+evr8fHj46+vn7vX08O7s5uj0+39/+X1vbWtgZW1vdnz9c2l0+vr+9PlubfTwfnX/8u7v9Ofq
+7u70/H7+fXB0cG1/d3P8+3VmbP52a250amd09+7u7erm7PLp6e/y+f59ffx1cGtq+O9vanv7
+cmxwb25z93twfP/39Xr77Ojq7/Dxevbn5Ors7vj8e3dwbGhtdWReY2lvb/ftenvt5/N8b3Z6
++vbu6vH37e/1fPj++fN5amhtanV8dPro3/F/9/v6cXr8eHZ6d3x8//dzb/bwdnBzbmhvf/zv
+7u/v+HxtbXx8dXF1cnN+dHB0evPj4vDv7vTv9PP9/PX27efq9X50eP1+aWJraF9gYmdpb337
+9/z2/n346+fx9+7q7OvvfnP+8vf69nVy+n1ve/j+e3h8dHn+fndxefX2c2lobfnx9PF/9uvv
+93lqamtsbXN++/Ly8fr77OTrfXl8c3F9eHt6/Xx9/33v7vPy8v37dGhqbm1pbfn8bnj6/3zu
++mxz//p/e3P7/nJ66+bs8351fPL3/PP19/bw7fnz+HRtdXBrbm5waWpycnX76un3fXv7+u3w
+fPn/dnv+bmlx+evs5uP9dnRwbWn/8Pj/+fL5/3JtbG9saHN5fe/x8/n27e/69+/qdW58d3J4
++/96fPX2cmxvevr27uz1+3j97/Xy7vH18/Z7bWZkcnFlaHFxbfvzcm37+HB4dXh6/fXu6/Tw
+5ujr7evzeP3+fXZzcnZ3dPvx7n358n5xb3hybWprd/t3c3RscH77a238/nZ2eXx/6+vu4+31
+6ef6/vL5cGpycm3/6ujr9Pnx+Hz/fXJmY210+vT9e3dtZWhtbHHy+Xz47uvu7+/u6/HyfXJ0
+/vhzcnh5cnJ3/Q==
+
+--owatagusiam
+Content-Type: audio/basic
+Content-Description: Flint phone
+Content-Transfer-Encoding: base64
+
+/31+/35/fv7+/v3//v///v59/f5+/X19/f5+//7/ff/8ff/+///+/Xx8//9/+Px7fPNyf/38fnZ+
+9vr7//79/Pz/ff1+/v98ff3//37/fv79fv9+/fz//nv+/P/9fX5+/v3/fvr9fH7/f379/P17/ft+
+/P99//37fX77f379f/5+fvz+fv39//1+fP3++vx//f39//39fvx++33/+379/n99f/v//P77ff3/
+//5+/fz7fn74fv39ff1+/f7++////v5+/vz7fH37fvl/ePz//vz+f3/9/v/+fX79/n///n39/3/+
+fvx8/P3++3z7fXz//n/8ff77/H7//339//3+f/p9/v5+/377/3/9/H7+f359/vz/fv349HN3fnb+
+///9//Z+fn98eXrv9/38+vz+/f7+fn79/H59/P7/fH58/n1+ff/8ff7//nr9/33+/v/+/3p+fv7+
+/H/+/H///f58/f79/Hz6fv79/n7//n79fv7+f3/7fX37/f5+/vz//3/+e/v8/v7//fx+/X7//Px9
+//39//7+fH77/f5+ffn++358/f///v/+/v7/f////X9+/vt9////f/7+fvV8ePh9/H99/n//f3/9
++33//37+/fz///78/3//fX78/v39fv/+/f9+fv/9/v58/P3+f31+/vx+/v/7/n38/3x+/P///n37
+//t9ff39f35+/fz+/f18e/v8/nz+/H77e3r+/Pt9/H5++/7+//3+/v9/fv78fn18fvt++33//X7+
+fH98+f5++3r//f1/f35+/f7//n39/X3/fH/8/33//f7+fnz//vz+/n5+/n79fX7+/v7+ff79/H39
+fXz8/P95+/1+/n9/ff79/X5/ff79fv1+fvx/fn78f///f/59+399f/7//X5+fH/7/39//vt+/v77
+c/37fv1+/Xv8f35+/P9+/v7+/33//n5+/f7//3/9//x9ff7+f379/n7+f/99/f7/fv/9fv9+/31/
++/19fv7/fv99fv78/35//P3+f/59ffp8fv79fn/+/nt/+n3+ff1///17/X38/3x+/v7//319fv1+
+/H5/+n3//nz+/f5+/n7+/X7+fX38/X5+/v3/f31/ff7+ff/9/f///Xx8+nz9/n1++/5+/33/f/79
+fn78fv7/fX/9/X1//H39/359/v1+/n3+/X78fn59+/17f/z+fv3/fn///v9+/f5//n19fv38/359
+/X/9/359/Px6/f37ef7+fP7//f18/fx+f31+fv7//P58/v3//n3+//58+vn/b//7//x+/f1+/v3+
+/n3///77ffx+ffr+//1+e/r+f//+/v7+/n19+v9+/v7/fv//ff/9/f59/v1//X3+ff79/fh8d/t5
+dPr/evt2+f179/z8fXt+8f9983z6fHd+evzzfvv++v1//Xx9/379/n39/37/fXt+/v58/f1/+nz8
+fnn7/P9+/f/+/X77ffr9fvz+/vz9fn///f3+/n77/n77ev/+/v1//vz/f/x//v7+/n78/X7+/n5+
+/Px++39+/X///37//H//ff39/f1+f/r7d379fvt9/v9+/P79/Hz9/P///v58+n79//79/v3//377
+/3/9/3/7fn7//X39+3x/fvn//X5+/H/9/v5//v7+fn38/X5//v1+/v9+//79/f58+n7///99/v3/
+/n/8/v//fn7+/n59/fn+fn5/fP5++/x8/f3+/35+ff39/v/+///+f3/+/v3//f/8/n/+/nn7/f7/
+fvv+/v/9fv77fv39/P99/X9/fv39fn78//59fnz9/P7/ff39/n3//379/398/vv+fft8evv+ff79
+fv5+fn39/X37fH7+/379/378/37+/vv//v5+fvr//v59+v9/fP18/P1+/Xv7/v3/ff9++v5/fv38
+/f18fPz+e/z+/H5//n7/ffv9/Xz9/H37fX39/v//fv79/v99/378/f19/Px7/H3+ffv+fn18+/1+
+/P54/fx+fv39/nv+ff9/fvz//P7+/nz//X75fn5//f5+/33//v77ff/9//3/fX/9//3+/v3+ff7+
+efr+fv19+/7//n97/f/8/338/v79fX79/f5+fvt9/v/+fH/8//5+fv39/f98//3++nv8//7/fX//
+/fz9fH75fPx9fn7//H7+ff77//9/fv/9ff7+/P9+fX79/P3+f/x+/f5+/37+//z6d/3+/vl4/f9/
+//5///z///59f/3///7+/X9/fn7//Px+/376/33+e////vt8ffl+/H15/P/9/X7+/vz+fH59/v38
+/3z9/P/9fX7+/f5///x/+35+/H///H7//H7+/v99/v3+/Xr7/H5+/37+/v7/fP3+/v59//79/Xz9
+/f5+//9//n79+397+339/3//+37+f//9/f56+37+/H5+/n9//H3+ff79//7+/X79///+fft9/f9+
++n79fn5++35+//z9ffx8fv79/P97/fp5+X57/f79f//9/v9+/n5++/5+//z///5+/n37/H1/fft/
+/v39ePl+//59+P9+fP59/f7//v7+//7+fX/9/vt/ff76fPt+e/t9//19/P5+/f19fvt8/f78fv/9
+e35++f9+//77/f9/fnz7/f99/v79/Hx+/vt9fv7+/Hz9fXx+//z/f/37/v7+ff/8/fx+/P7+//5+
+ff7/fn/8/nz//33+fv1+/Hz+/P/+//9///1+//1+/X99f37+f//9/3/+fX7//v39/Xr9/v/9+v12
+fXX+/n71fHd6+/j7+v39+v/8/n5+/v7+/H5+/X7/fv9//vt8/n39/n78fX7+/v59/fz9fP1/fX/9
+/Hz//v3//v59//t+/v39fX7+/X7///v+e/z///1+ff7+/n79fft//X59/H/9fX/9/vt/ff1+/v18
+/vt///58/v79ff79ffv//v5+fv3+//7+/v7+/n99/X9/+3v9/P59fv5+/fx9/vz+/H/9fvz///19
++v/9/n19/ft6/Hr8/H7+fP58/f7+fnv7fv19f3/+/v9+/vx+/n5+/v39fn5+f//+ff99/vt+f3z8
+/3799Hlvfnr2//z9/f///v/7/n78/f/+/3/+/n77/31//P///X1+/X9+fv/+//19fX/8fn7+/H5+
+/n19/v7+/37//n7+fv98/Hz9ff79//98/v78fH9/f/l+/n5+fH/8/X19+3/8fn5+fv1/ffz+/H5+
+/nz9/v59fvz9/nz+/3z6ff7///5+fv59/vx+//7+fv5+fn7+/f5+/f5///7/fH7+/vx8/vx/f37/
+ff3/fv1//P7+ff////1+ff3+//99/339f/1+fvz+f37/fP3+/n59/H////98//3+/337f3z9fn3+
+/P7/f/v9/v7+fX77//5//n3+/H39ffr8/337fv7+fX58+v19/379//1/ff7//n1+/v1///59f/79
++vn5+f3+/X18/X78/339ff9/ff7+fn5+/v3+ffx9f/78/n78/P39/35///r+//7+/f3////8/f7+
++/z9ffn9//77/f/9+/t//X79//v8/f39/P77+/79/fz7//37/f77/v37/fx++vz/+/7+//z8/P/8
+/P/8/P3+/vv8/f38/fz9f/78/P3+//r9/vx+/H76+/39/vv+/P99+fx9/P38/P39+3t/+v/9/Pr/
+/P1+f/79+vx//P79/f9+/f39+37/+X5+/f1//P39/P38+/5//n78+/t9fvz++/9+fX74fv3++/3/
+/Xx++/z+/v/9/P39fP5++v3//v37//n+/v38+//7/fv9/f1//vf+ffv8/fz///z++vz7/n74ff3+
+/vr++/39/vv9/H///fz+/P/7/P/7//79/fv9/v76/P3+/H/9//v+f/r+//t+/vz//P/9+v79/P1+
+/vv7/n/8+/7+/X3/+/z+fvr8fvz+fv75/f//+v1++v7//vv9/P/9/f37/v/9/fv8/vz8/H77fvz9
+//p/+/3+/n7+/f39+X5++vz6fv/9/f38/H/6/f/7/X38/Px+/vv9//3/ff/9+////f/+/v1+//37
+fv/+/P7+/n78//t//vr/+v9//X7++v3+f/v6fvz9//r9//39/f38/v/8/fn+/fv+/P1++/n4eXh3
+d/z+ef15e3779f32+Pr9+/z+/Pv+//36/v//fP7/fv7////8fn///n99/n///vv//nx9/v7+ff79
+/n9//33+/f3////9/n5+/X39fv5+fv1+/X59/f1//P59+33+fX5//vr+f/79/Px9/P19+H3//v1/
+/Xx/fn77ff99/P3//n7+e/v+/n7//P7+f/9+/P58/X7+/nz+ff7+/v5+/P/+fv5+f/t9/fx7//v/
+/f5+/f/+/fz7fv5/ff///n5//f/+fv1+/v7+/X1++/5//v96/f57/379/v5/f37/f/79fvv9/nz+
+fP/7//t+/vv9/f/+ffv8fv77fP7/ff9+/P7+fv7/fX//fH76fX1+fvz+/X/+ff78fH37fv7//35+
+/f7+f/v7/n5/fX79/P5/fv36/fz9fv38fP39/v78/31+fv7+fP/8fvx+fX3+/v1+ffz+ff97fn/+
+fn9+/f59/Hz+/n77/378/X7+fn7//X/+f/37/vz+/X37/n58/H38fn3+fvz9evx9/v/9e3v/fvp8
+/H7/+3z+/3x+/n5+fv1//X5+/v79f//9/n///nv++3n6fXz8/v79/v39/fx+/f78/X1+f//+/v5+
+/f3//319fvx//X7+/P//fn58f//9fv/+f/x7fv7+/Hz9//p+f35+//38/n7/+/v7f35//f/+/v79
+fP5+/n/+fn7+//v+/n5+f//8ff1+fvr//f58fv3+fP/+/v5//n39/f/9e/r//v9+fn/+/v5+//1+
++379/n76fn3++v///3z+//59//z//H7/fn3+fvt9//x9/n7+fn78fP59+35+/3t+/P7//X37/f79
+en39/fv+/v77/v79fXz9/n7++37+fX7+fv1+/n1+/v1+/3r+/f39ff/8/v/+fn3+fv9+/v7+/31/
+fv1//f/8/H38fX39fv79/339+/76/378/v78//79fn5+ff7//n5+//37e3n8fv39ff/7/P/+e/59
+//x/fv78//1/ffv9/n7++/99+39+//79/H3//ft+//37/f/7fP78//x8fXz5fX5+//5+/Xx+/n9+
++317+/z7ffh/ePp9fn39fv7+fv/9/P9+/v/+/35/fv///n5+fvr9fv79+/39/H7+/H7//nv//fp8
+fP77ff7+ff/+/f7+fvz9//5/fH37fH18/P7/fn7//P19fv78//1/ff78/P3//n78//v7efp//f9/
++3z8fP9+/v5/fH79/v57/n1++v7+fn76/v5+/v/9/X59/v5+//98f/z+/v79/37+/Xz9ffz+/378
+/P7+/v39/v59//d+fv59fv3+/nv+/H3/fP59//v9f379/37+/3r9fn7+efx/fv18/f33/nd/evr/
+e3P79/74/nz9evb4+fz9+/r7fPx/fP96fv3+f3x9/v18fn1+fPz9f/97/P///X1+/v/7e/79fvx+
+fP78/v5+/P37fn3/fv3+/n59/vz//P58+f/9/v79//79fn7+fvx8/vx/+v94/v79//t7//p8/n18
+ff79fn1++/9/fn5++/3//v78/H59/37/+X5+/fl9/Px//X39/v5+/P79fHx+/v/8fH77//19fn1+
+/f5+fvz6/v3+/n39/n9+/H7/fn1/ff/8fv38fv7/fP7+/f1+ff/7/v39/v78/f5+//t+/35+f//+
+/n/9/f79/3x//vz9/////v99/3x9/P5+f3/+/35+ff3+/v/+/P9/ff59f/r9///7/fz8fP19/f59
+fvz9/37//n79/f58/H/+/nv//v39/X19+/3+/nz//v//ff7//35+fH/6/n3++39//f//fP3+/H58
+/Pv8fv38/n1pXF9a/tjfvLjJ9Vo8Oi8xRklg67qusbGssrm/QDguJzMsLTU4w7Ovr7yxqrbvQjw7
+NywtLkq9ua+4sqmttVVBPzkvKSw0Q1W+uLqzrK25/Uw+PC4pMDQ7T+K4rq+rqq+zzkJBKiovKzc1
+R7Ctra6yrKzEQzszNiwpKyxqu7Svu6+qrsJEQTw3LCotPOLOtbi3rK2uwERCNjktKDg/Rdi+t7iw
+q623wFJCOykxNjA9Ps6vsbGvr622TT8wLTQoKys0vraxsLiqqLPpSEE9MiosLUzPurG6rqqutU09
+OjYvKC03SGHBs7GyrKqz121APjEnMzc3R1m9rrKvrrKvwj9DLi41KjAvQbe0sLK1q6y+UEE6OS4q
+LC5sw7q1va+rsLpIPjw5MiouQmTLt7q1rqytxE1HOzoqLDo7RvfEs7OzrK+4vEU9NCcyLi43Osqw
+sKystcPPX1JLRUVFRUdNUl1w9NvX0s7R09vi7u96cWttb214bnt8eH92dHp2d31zaW1qZnt2fuz3
+6dze4Ors6ff7dXV1cnZubG7+9fX+f3N1cG1lamVldH39/vP0+/L2/XX5enr8ePLp6eTp6+vu7+5+
+b29rc/vzffz9/e/0c3Vybvlzbft29uj+fXV8eHxtbXF1+ff++H7z7PR+dm10/nZ8+n338/L8+PR0
+f3VueHb97Pfr7fzt7vn+bWxybnB7+f/s7+7s+vh2cHhtfXR78Prt93X693v/bGZ4dH33/fHv+PT0
+9Hhz/nRzdHJ+8O/t7nvz93x9b3N2+f16fPjt8u33/n54eXBydHd8eO/68Xh593T+9Xz2eX387X7v
+93z59m9+fW51/f/59PLrd3BtfPJ2/fF69O777ev29PR6eHVuemlt92z1+fvz/3Zv+nlpdW1seXNs
+2Lu721VMSEU3NjZLubizt7mrsfBOOzU3KS0tOr26tre4qq3YZEA2OysvLjm5tbGxs6qs0Vk+MDor
+LS8ywri5tLasrc5QQTE7Ly0wM8i4urKwrKzIWkgvOjIuNjXItbqysK2uzkhBLjkzLTU4zLu+s66t
+rt5eRCs8MC85Pbu1ua+ur7JXSzonOC8tOD6+uLqtrK+ycnI7KTwwND1Mura2ra61uFNSNic1LjA8
+Sr22tqyttLhgVTknMzAvPlO8tLWrq7O4W0k5JTAxLz9bu7ezqau1vVtLNycxMzFD7ry5tKqtucFN
+QzglMDg0T9u9vLCprbzWVkg2KDM4OVbKuLmuqa7FakU8LyQuNTdYx7m3q6etx19QPi8oMTg/7r21
+tKupr9dKRzotKC44P3i9uLSrqrDaSkg7LSkvO0beurizq6uv5EVGOzApLjtH4bu8tausr9xFSTsx
+Ki46TN+6vrWrrq7UQEI6NCkrO1PVvMO0qq2u4khLPDAqLThdzri8s6msr/o+PzguKCo2Vc+5vrOo
+q6/0SUc6LiotN/zHtry1qay1ZUVBOS0qLDV/ybe7uaqrtHxHRTouKiwz48K2ubqqq7V7Q0A5Lior
+Mtm/tri8q6u1eUNDOi8rLDHWvri4v6yrtm5BQTszKysxzr+7usKsq7pmSEM+MS0uMsm8ubfBrau7
+WEI9PDIrLC/MvLq3v6upv2hMPD8uLS4xwbm4tr+trMdSRDg9LSwuL8a5ubS9q6rJeEw1Qi4uMi/H
+t7mzva6qyFhOMT0vKzIt2rS6srmtqb5bVjA6NCozLm20urO1rqq7VVExNjgrLzBmuby3sa6rvVJb
+LzQ4LDYy+rK5s6+urLxITC0uOiszNGG3u7Strqy9U20sLzktOjn2srazra+wwURLLCo3LDM7Ybe4
+s6yvsbxYay8sOi85P++1tbOrsLi/S08uJzUtMj1ft7Syqq23vFVSMyc1MDJCa7m2tKqsub5WSjgm
+MjUyQ2++u7WqrrvFXU04KTE3NknevLm0qq26zFBANiYtNzVM1r26rqiruuhZQzEoLjU5V8a1tKyn
+rMVVRTktJSw0OVvEu7esqa7JXFRAMSsxO0XqvLi0q6qw4UlGOi0oLTg/Zr25tKuprtVJSDsuKC03
+RuG5uLKpqq7XREQ6LygsOk3Vubuxqayv9kJEOi8pLDhS2Lm9taqtr98+QTkzKSs5Xc25wLaprK7w
+Q0s8LyotN3fJtby3qaywbj4/OC4oKjJtyLe6uKmqsO9FRzouKiwz5cK1ubqpqrVkQ0M6LSorMOPD
+trm8qqq2ZEdFOy4rLDHUv7e5vqurt2VERDwwKywx0L+4ub+rq7phR0M8LissMsq9uLjArKu7WEI/
+PTErKzHHvrq4vaqqvWBKPT4tLS0yv7q3tr2rrMVPQDc7LCstMb+6t7W5qazMbUY3Py0vMDW+ube1
+vK2t1VpDMj4uLjIzxre5tLmurM1oSi88MC00MMqzt7G2rqzNUUctOTAsMjDMs7ixsq2ryVlMLDgy
+LDYyz7S6s7CvrcNKSi01Nyw2ONu6vbOurq7KWFYsNTguPDzNtLuyrbGx1EFHKS43LTo80ra7r6uw
+sc5WTCoyNy8+R8S0ua6rtbjuRD4mLTMuO0rFtLatqbW72U5FKS02MD1WwrW3raq3welERCkrNzE9
+W8O0tqyossbrRj8qKTQyPGTAt7arqbLKc0M+Kyc5Nj/awbu1qqu051lHOysqNzlI0Lq4samrtWpJ
+PTcqJTI6SMm8uq6pqrdVUUU4LCw+TUBTvq2ztLy1rslEPC4zLygtLFK1ubCzrae05e02OjosNi9S
+s7izs7KruU1TLi41KS8tQLa2sa+uqrFtZDItOSsxMUW1tbOurqyzY1I1KzstLzZCvbu5rq6vtV1Y
+Nio6LzI8TLq2t62tsrVZRzgnNjIvPk++u7arrLK5ZFc2KDUxM0JwuLOxqau5wEpBMSQuLzBBb7y5
+saiuvMNfWDcqNjc6UNC4t7GqrsbmTEEzJy40NknWubewqKu+6lZENSctNzlRyLi3rqmsvl9OPzYn
+LDk7V8a8ua2qrcBNTj81KSw3QF/Auretq62/SEU8NSkpOEXtvr+5q6ytxUVMPjQrLTdM5rq6uKqs
+rs8/QTkwKCo0TeC7vbipq67QRko8MSstNF3Ot7m4qauw30JEOS8pKzBbzbi5u6qrseJDRjswKiwx
+fMW3t72rqrDTQUU7NCsrLmHEu7fCrqqwz0VIPjQrLC1Wv7y2wq+qsM9CQTs3LCstZb29tsGuqLLc
+S0E+MystLWi5urS+sKm07EY9OzQrLSxOur20vLKptedXPTw5LTMvS7m8tru3rLXxVzw3Oy0yMD29
+ure3t6yx5WE8MjwtMTI7u7W2s7Str+dTPS48Li4xNb+2uLGwrK3YW0EsOi4tNTbCtrqwr7Cu2EpG
+Kzk0Ljk8yry+sa6vr91bSCs4NjA9QsO2ua+tsrT3Qz4nLzUsOkPJt7mtqrK13VNDKTE2MT9Xv7W3
+rKq3vm1CPSYsNC89VMK1tquptsXiS0MqKzYzPmO/tbesqbPNe0U/Kyg0NT1qwLa2q6mwzGNKPy0o
+MTc+d8C5t6yqsNBQRT0yJy89P9vAvrSrq6/eSks8LyowPUvTurqxqqyx70A+Ny4oLTtN0729sayt
+tOtMQzUuOk9W3NfMzM7Q2Nnd3+r3eGtgW1pYWFxcYWdre/fz8ujm4ubo5O3p8nz5/vzx9uzs7vJ+
+9Htxc2txbGtvdHlyfn99/vz+e356fnl5fHv7+fv5+Pjz+vj9/vn9/Pj7/P77+v77/v1+fvp+e/t+
+fHx7fvh6cnJ28X76+Pf+8vz3fv14/Hx4/nt7c/78/3z8e3///f/9/vt3d/f9ef1++Pn5+PXz+/p8
+/Xz093z3/f39e354ef59enx//Xt8fv3+fn18dPf4/fl+fvf3+/t8/357fXp+fPX3//x7+f/7f319
+//16fPz+/Pf9/Hx+9376+H75+31+enx5eXx9/3z5/X17/vx/+nl//3b/en39fvf39fX18vz9+v38
+fXh3e///+319/fn8+f15/Xl1b3B3dH78/fX07Pb78fv5fXl3cnd6/Hv++vj1+vv4935+/XBtb2Vd
+U9G2vutkR0hDNTk3Tbq0sbi9r7HCTDk4Ni8qLDBL4byxtK6qq7HpRD46MSktOEBZyravsq6rsr/Q
+OzkuJDAvMDxDvq6wrqytrLlMRS8xNCkvLT64ta+zuaust2Q9PDczKiotUsy8tb2tqq20V0Q+Oi8q
+LjtLeL21tbKsrLnvTzk5LCc1NTlM47avsq2tsrDcSEMrMzItNzNlsbOvtressNZHPDY4LCssL+W+
+s7G5raquvUY/OzgtKSw5V9e2srKtqqy5VUI3NyslMjg9bce0srCqq7S4Xj07Ji0yLTk45K+vr6+u
+rK/fPzkwNiwqKy/Mu7ayu6ypr85JRz42KywtQt/Bs7qyq66w6Tw8NjQpKjc+Tsy4r7GsqK7D1UU/
+MyYvMjI+SMKusK+usq+8RUYvLTYqLy85urGvr7isq7htQDw4MCorLUjMu7O3r6yvuGw+ODE1NzxF
+VXzczsjFw8PFyMzR2vdmW1RRT09TV1tfZGtwf/X48/Tz9Pb68vDt7Ojm5OPh5Onv+3huamRlZmRo
+bWx1fnt+eX7+/vr8e3z+//r18fHv7O3s7eru7/X3+Pz39vz79X17eXZ8cnBxcnFtcnF2dXB2c3t3
+dX10e3d6/Pry/Pf4+fb59vr09/Ty+Pj39vb2/f/7//z9eXl9ff///fv6e/3y/vz9e3l9fHp+fHt7
+fvx8e/V4/vx5e3p9eHt6d3t6+////Pj8/Pz7/Pn3fn57e/j+9fp/+fn9fv58/f7++vz9+vr//nx9
+/P7+fHt/fPj+/Pp8fX17eXZ4dnj9f3n5fvr7+fX5//v6fn55fnz//v/5fvf5f/5//nh9enZ9/f35
++vz7/fp+fv7+f3h8enx+fvv3+fL2+fj8fXl7e3Z8+Hl8+376fHx6e/v2+OffWT9IZNzPurO20UZI
+PjctLz5a3Lu2tKqqrtFFRTkuKCoyRHC8urepqq3LREY6LygqMUvluLS0p6irw0FGOi8oKi9K8by4
+u6qrrcJARTs1KikvTta8ur+sqq7DRUo/NiwsL0/Lura/rqquv0FBOzcsKSxNx763xK+orsZIST40
+KywtS7+6s7+yqa/KREE8NSsqK0a+vbS+tKivy1JGQDgtLi1Dvbu1vLmrr85OQDw4LC0tOcC6tLe6
+q6zFW0Q7Oy0sLTPDuLW1vKyrv1hDOTwvKy0uy7i5tLqsqr5fSTY9LywvLs+2uLO5rqu6VEY1OTUr
+Li19uLy0tKypul9XMTc1KzQvcLO4s7Svq71QTC4yNisyL1i2urOvrqu7X1wuMDcsNjRms7WwrrCu
+vFBWLSw1KzM1T7W0sq2vr7lYWzErNi4yOlK6tLKtrrG6Z0o2Ky8xQ2zhyMjDxsrM0dfd5PZvYVpX
+VlRUV1tgaG189Ovp5+Tk4eLm6u3z8/j59vTt6+vv7/X+cWllYmFkZmttdv59ffb3/v15+37+9/n5
+8/Tz8vHv9PTu+Pf8+3x7/n31+v76fXt8dnt8eX12eHl2d3l4f/7/fv/8e33+fH1//n19/Pv9+Pj/
+fv79f/v7/vb8/fh8/vr8+/769/x9/P57/Px6fn38fnn7fn59+vx/e/3+//19fP9//3p9f3z+fHt1
+fn58eXz3+ff9+n789/r9en9+fv36fHL8/fn7/Ph9/P79/n3+/v79fvx++3j9+n79+/v9/Hj+fnh8
+fHt4/X19+37+/fr3+n/8+3p+fnt8d373/X74+/57e3v///9+fvv1+/X2+vn4/n79+P9+/Xp4fn5+
+fXh+/nv+fHt8f/r9f/n9+/58/3n//Xn//fr3+vj47+TdTjtP7eG8r7zW71E7LC44Qt6+vq+orLtS
+T0ExKyorW7y+t72tqsTjSS49Li42QL6+tKmtvMZbRywpMzVJ0Mi6rKyx3FRSOy8sLkjCv7i9sKq4
+41E5OjQsMTnmy7+srbfB+UkvLDIzQdq+uKyprMBQTjwvKCk9zsa8wbCotM9pOjwzLjMzzLm9rayw
+uGdNLicvLDRU1MKxqq2/cnpHMy4uPtK+tr2zqK/LXz88MysuLFq6wrSvrq/SYzsoMi4vROnAuaup
+tNdtUDcrKzVPz7vCtKmsvVlOQzUsLCxUusO2sKysxFpEKjQvLD1UyMGtqa/L9Fw8LCs2Rty+xLWp
+rbhZT0c1LCssT7zCuLisqsTtSC05Li04TMHAsKivwddTOyorMTdYxcCyqKq1a15LNiwrLGO9vbe7
+q6rC50gvOywtMDq/v7eqrbXEV0cqKjEwRNnHuauqr9VaXT4wKy5Pxb+6vq2pufFNOjwuLC40xcS+
+rK2uulFOKyk2LT5+zb2sqa3JXGg+MCwvSsy8usCtqbfpTEA8LiwsMMnDxLGvrLd7cy8sNiw4Usm8
+s6erve1xRzArLTlYyrvCr6mwy09LPi8rLC7Pu760saqu13Y6LDgrLzxrv7yrqLTM8E82KS02Q9fB
+xa+rrrpMUkY4LSsv2sDDubaqrMx8Qi48LC42Tr7Er6iwu95JPiYqNDZlysuvqau3WG5MNi0tL9m7
+ure7qavIYT8yOissLjvBxrepr7TFY0opLTIxSs2/t6mpsOlXUTktKy1KyLy4wK2ot+RPPDwuKy4v
+y77Arq2ts2tdMSg1LTVT0sCyqKu8ZXJHMiosPue/ucKvqK7ISEg9NCsqLHbAyLOtqq3YdzgpNyww
+RNy+uaqostZrTDUqKjVG2sDKsqmtvldXRTUtLS7lub60tq2rzulALDosLTdLwcCvqLLK3VQ7Kisy
+PHS/wLGoq7VfVUo4LSssUbu9trutqbztTzA5LysxOMrEvKutt8BXTi4oNTZE0869q6uuzlhlPzEt
+LkXAvbe+sKiz4Es6OTEpLDHxycWtrLC5dFkvKzQvPebBuqynrMVXWT8vKiw/1MS9x7CptttYPz8x
+LjAyyby/r6+utndrMSo1LTVM0b+2qau+8m1JMissOWDGusGwqK3DT01BNCssLXm9w7ayq67UfT0r
+Ny0vPmnAvq2psct2TT0rKTdF6MDNtqqtuFxaTTouLS1Pub+3uK2qvV9KLTUxKjQ70cm6qa2+y3hH
+LiszOVPGwrmpq7DmUE47LiorPr+/t7yvp7ncZDA5MisyNMy9vausur5vTy8oMTA788a+rqmsxF1o
+QzErLDvJv7e/taivz1k/PDcrLS5ev8uxrK+04141KDMuNV3Mv7KoqrlbW0g2Kio92cW8yLKnr81Y
+Rj8yLC4t07rBsK6ssPlfNSc1Ky9G5MK7qaq56/tONSwtOV/IucCxp63DU01BMystLGm6wbW1rKzQ
+7UEpNy0tO1a9va6mr8vfUzwqKDI9br/DtqirtGNVTTktLCxIur+3uq6pvGhRMTcyKzM82868qq27
+ympMLys0OU3Iv7mqqq3MSk08MCorPcfFu76xqLXcYzY4NCwzNNO+vqystbxlUDEnMTA59szBrqqt
+xV59RjMtLTrJvra9t6iw1287OjcqMCxLu8e1rbCx0WhAKDEvL0jav7mrqbLbZFY8LSoxVs28v76q
+q7xsTUE5LS0sPrzEua+trL5STSouNSw+WM7CrqmvzXViPi8tNUvVvLy4q6y37EtDOC8tLj3Wx722
+srTG1145Mjg5RPrVwbOvt+FQTDsvLS5Hx7y3u6+otNNmOjwzLTEx1by+r66zuO5hMSgxLjVWzcCy
+qay8Yl1INSssOtnCucG2qK7KYUU+NywvLV68ybOvr67cWT4nMjIwT/nMuqurtnVwTjcsLTnowLa+
+taeswE9GPjUqKitOvsq2r62tzu49KjYuMUbkv7urqbbiZE44Kys0S9S9xLaorcFjT0U3LS8tW7jB
+tbOtrM1+Qik1Li07VMC+rqexzuVWOioqM0HpvMO1qKu3WFJJOS0sLFG6xLe2q6rE+0orNy4rOUjE
+xLCor8PiTkArJzY9bMPQtairtlxkUDkuLi78uL61t6uqyWlDKzksKzVCx8iwqLPF1VU7KS00Peu/
+wq6nrLtWYEczLCsu0by7t7uorNPdOy46Ki4xTbzCr6i0vdhTOyctMzZlyMSvqau4W2hLNy0sMNq+
+urm6qavGakE1Oy0tLkTMz7SqsLbRXD0oLzI2XMm/r6iqs19RRzcrKDHkxb29u6isxe5DOTotMC9M
+u8S0rLG00VU+KC8xMUvgybisrLj+cE83Li006sG5urupq8LuQjo5Ky0sPbvAtq2vrsNmRCgtLy0/
+6cC5qqev0WJVOiwoL1DNu727qaq6fEtAOSwtLD28xLmurq2+V0wqLTUtPlnRwa6qr85nZD0vLDNP
+zru8uqqqtnZIQTctKis7w8a6sa6svulNLTMxLj1exL2uqbHOcFI7LSsyQu+/wbmqrLr9VEg5Li4u
+Rby+t7SwrL53UCwzMiw4Rsq/tKitw91gQC0pLzxawr+7qqqw3k9OOzArKz2+v7m1ram581ItMjAq
+NUDLwbWorbzUUkItJjA7WcfOuqqrsedcVjsvLi9Nu7y2t66rvX1ILy8tNFNQ29rTz9fa4+jw8ft4
+bm1wcHb36+Pj3t7f4enp7/Pu8/by+f53bGdjYWBiZmpuc3V1fnhyeHT+fP338+/w7O7u6urt7+76
+9Pr4fv36/P59eXh5eHh4d3l3c3l3dXx6dnh6+Pn//H//fvf7fvnyfnx+fPr/+vfv+fj4//r+en5/
+fHt1eP18fP36f3/3/vv6/fz9/n19f317fHx++f9//P7+9vz+fX/8ent4ePx9/P1+fn59e/55e/V+
+/fn3fHt7///+/Pr2+fP7/X53fHz+/f78/Pr+/fh+/vd/+v38/nx7eHl5/359e3p+fn5//f74+vv8
+e3P983f//v/9+f729v79/v59fXt5dnZ7fHv3+n399Pr6+PZ+fXt6fXz7fPz8+vf3+Pz7+vx6enp5
+dXd8ffr+9vn6+f78fX51cnN6fXb7+Pz4f//68+/29Pfr33ZEPkzeyci4trzBTkg1KTUzNEfjtK+u
+p6mvt1BBMCEtLSw9XLy4rqWos79PRi0jLi4ySs6ysqukqrrMR0AsIy8vNE7Oubmsp67G41FELik0
+Nj1sv7a2q6iw21JFOywmLzU8a762s6qnr9lRSTosJy84Qtu5tLCpqK/0SEI5LSYuO0TXurmxqaqw
+c0dGOSwnLjtJ0rm8r6mssGJBPzgvJiw/V8e5v66oq69RRUE3LCgtPW/Cs7usp6uwTD07MyslKjpq
+wrW+rKaqtEtJPzUrKSw91b2yvK2mq7dIPzswKCYoOdW8sLmupam4TEQ9MikoKTnIvLC6sqaqukxE
+PTYrKSo2yb2yubeoqrtVRj84LCorN8W+tru6qqy9U0I9OjArKzjDv7i7uamswFlHPjosLSw5vbu2
+ubiqrslLPzk4LSwtO8W+uLe0rLHKYkI3NThIT3Pq3NLV1tnZ2tve4+/4dGZfXV1cXl9la3F5+PLu
+6ujq7Orv7/x+f333+fLu7O3q7/R+cW5qaWpqbnF4enz79/v9/3r/e3n/e3n+fnj69P719Pv3/PH2
+/vrz+Xv4/Pn+evr8e312fH55/Pl7/Ht7fHt6e33+fn31/Hx+/358+/z9/f79/v7//n59/np9/v5+
+fv59/vr6/v38/P7+/nx+ffz3f3d7+vP6+3v7fv7/ev97en14/33+/n1//P7+fn79+X78fP1/fnt9
++3t+fP/9e/z9/375+/74+/t8+3//fn5+/f7//Hr7/fv6/f///P17fXp7eXl/env8/H79/X5+fX16
+evb+/P18+P35+H39/Pt+e3z+e/99fPr38vn9/Xt7eXRzdnp8/Xz++fr3/H7/+Pv5/fv6+Pp+/Pz/
+/P57fP95fHh8fnp+e312dnVoXlfovrrBX0xOPzUwM1G8ubS4sKm69FcvNjAsMzjNwLurrr7IYEsu
+KjM0RM/Buauprs5bXz4vLCw+wr+3v7OotuxTODo0Ki4w48HGrqyyuHNcMSg0MDvuxr2tqau8U1xE
+NSoqPsvEusSzp7HTXD49MywvLti8xq+tr7VeVDImMy0zUdbCs6irvnj0SzUtLjzZwbjAtKivy1ZD
+PjQsLi1au8O0sK+vzv8+KjQuL0Pvv7usqbXbaU85LCozTNW7v7iprLpgTUU3LS0sRbvBuLSuq75i
+TywyNSw7Ss/Esaqux/ZiQC8rM0J3v7+8qquw20lMOzIsKznFx764sKq142oyMDcsNj/Uv7qqq7zP
+Z0YxKC84R8/IwKyrr9BTWz4xLS07vry3uLOps+FwMjE2KjI1fr++rau6xehNMykvND/hxL2uq67B
+XldBNy8vPNrAura2vs3ZZVhPTExLTE1OUVddZnrv6eTk5efm4+Pj39ze3uHp7Pn/+Xp3+ff5fn52
+bW1nZGlnaW9tdX55fv5//Xz+fXp7f3v49/n28u728vD19f/28nr7fH76e318e/p+ef99fn57fH59
+e3h/9Pf2+Xp7e3z/ff18/v5+fnx6+H79/X19fXt8/nt8fvn+c3j8/Pr+ee/19/148P128P34/vz9
+/v16/n96fXz+e/79e//+/3x8fH19fXV9fv39ffr++/r5+f96+3t+f3t8/37/e/z7/fp/+X74fn75
+//n+/X76fv75ffb+/Pt9/n19eH5+fHt9/nz9/Xx6fX19ff/+/P36fv3+fvt8fPv69/z9+fn7/f71
+9Hx8e3d3ef13eXn/fHh9fP/7/Pn9/vj7+/78f3/6fv35+vf+/n3+fHx8f3x9/f76fvx+/P16e/58
++vfs4fNQQ0FS28u2rrfbWE44LSstWsXGta6rs9D1MC43Mk7TzbKqr8dPTzwuLCszwMK4qa+zxOY9
+KjE1Rs7Nya2sv3hKPzotLzBcvcCpqLq/0U4vKzJG5MjYvqu23185OzgsOEfOw62ntc7WWTQsLD/M
+y8G+rq3Ld0AtPS80VXfEr6u03nheNy4tMsrBwrWvq7ruYSwvNi9R89Owq7HXVls6Li4sRrvIsaqu
+s83vNSozNEnP2r+qr8ddRj4yLDA0zL65pq2+wuZBLCw2S9HH2LSrvuhPNT4vLjlWwb2pqLjP2FEz
+KixK1M7Iw6yv3eQ4Lj0sOl3NvKyost9hUzcsKDTN0ce3rqzB5k4rNzM19da+rKuz61ZRNS0sLW/A
+zK6tr7ncWCwtNTj2zNCwqbTZTkQ5Li0uP73Eraa1vcdlNCswPXvIz8arsdz9Ojc6LDQ+zsCypa/G
+zf06LSs26c7FybWswO5PLzs0MEr9yLaprsTtez0uKy1twsm6s6yz4mcyLDowSPbttq2vw1drQjIv
+Ljm+v7utra7A8EUqLzQ6+WzTrq6+ek9DOC8zMnq5vqqotbzYTy8rMjxrz9+7q7jYXTg8Mi82Sb69
+rKa0x89OMSssPe/Lycmur9PgPTM+LjdKz7+xqLDJ3Gs5LSs12c7GwrWsv+tXLTY1L03lxbOqrcNb
+aD4uKytRwdC4r6yy2/AzLDkwR9nWtquuvVRbQjAtLDTFxr6rrrHC3UQrMjU/29XOrq26eU1FOS4v
+Lk28xaypt73NVTEsMj9+yNbCrLPWbjw5Ny42Pcm+s6auxM38Oy0sOHDUys+0rMXpTjE/MTBF+MK4
+qKy/4OpAListWsrNwbmstOd2NS4+LkF32rqsrLpueUwyLiw6wMm+sq6txX5HKjQ0Nvd+yK6tuG9e
+TzYvLy/dvcesrLC64lotLTY5bdfetKy33FBGPC8vMEK9xK6ntL3KaDcsLzlZzM7Lrq7L+UM5PC4z
+OuLCuqisvs7cRi8sNFnWyc+9rLnheTU5OS09WMi9q6m23XlOMysqRM3Ww7ytrtHiPC09Lzx8zryt
+qrTiXU82Lisx0M7JtrGtwOBWLTY2NnjWx66rs9pTVjouLSxLvs6zrbG4zfgzLjc4W8vXuquyzlVG
+PTEuLzXHv7inr7zE8D0sLzlRz83QsK3C9Uk8Pi8xOW/Evqmrvc3ZSDAsMVPTyc2+rLbmZTg3PC49
+UtnErau55eJYNS8tQ8PLv7yvrcxwRS08MjVdbcmxrbjrc1k3MC4zy8DEtK+su+hgLTE4Mlvq1LCs
+t+NUUzouLy1Husexq7C1zu41LDY2TdPhv6yxy1dJPzQuMTbMwLuorrnA6EYtLjlK1dLasq3C8Es7
+PS4wOHHCvamqu8rnSi8pM1neztS6q7rnbzI6OC1BZMK7qam55/dLMiorSM/Zxr6tst/bOi8+Lj7w
+ybusqrnwb040LSsyyc7HtK+tx9tJKzYyN+3Owayqte1XTzUsLStWu8murbC31PgvLDU2XczZt6qy
+zk9KPzEtLjbFxrelrrnC6D4qLjdOzMzRrqu/cUU6PC0uOWHLvairvs/YRC8sMmbLwsi6qrXsXzQ2
+OSw8T9jCrKu9499MMi4tScDHvbutrdfePCw8Ljdf4sKuq7j2cE8zLiwvyL7AsrCsu9xjLDE1MVbY
+zLCqstVSUzotLSs9vMa0qq6wyOc5KTM1RtDdvqutv1xKPzIsLjHVv7umqre/6kosKjlK39P4tKu+
+7E04Pi8vOV++vainuM3gTDEpL07fysrN0Xi8s8plSDw+LzA5ZcK5pqi6w85BLSovUNnKzrmquutX
+LjUxKzxcwLmop7rN2kcvLCtDxMm8u66u1ts8LDsuOFzUv66qt997UTUtKy7YxMazr6y632stLzkz
+W9zSsaywz1JdPC8uLT+8yrOprrHUbTonMjlI1HXHrK/FXUpBOC4zN8++uaaru8TtRSwrNkjfz+G4
+q7/nVjU+Mi88Y7+8qam7z9tKLywtSNHLxcOtsd/aNjA8LDtdyLyrqLjY7E8yLCo0yMzAu7GsxttO
+KzkzNGfYwa+rstlcXDovLC1Zv8+0rq622/MxLTgzUdHatauwx0xUPzMtLTvLzbapsLjI9TwsMjpU
+y8/Gra7DYEU+OS0uNvrJuqeuwsbfPi4uOnDLw82zq8PtTDE+Ly8+a8S7qK3H0OI/Li0udcnIv7ys
+uO7pLzM6LUN7yLurrL/x8kcxLSs7wMy+t6+uzOJGKzs0O+Lnv62uuGdaTjUuLS/avsetrK+69Fws
+LDo5b+N6tKy221BLPjEvMUi9xK6ns7vRXzcqMT9a1ezRrrPX8z48PC85QsnBs6evx9FyPC0sOXfb
+yNO3rMnbTi8+MTJK27+1qK7H3W08LiwtZMfKvruttt/hMjE7L0Xoy7mrrb1oaUgyLSs3w8y/sa+v
+x95EKzc0O9/axa2tt3dNTjkvLS9bxsytq7O6110vLTY89cnbuKqz1k1EPjMtMT/NyrGntcTKYzgt
+MEHkyMnIra7R8z01PS01Q9vJtqizzdNzOi4tNdHJwsO3q8LqWy06NS5I68W3qa7G6f0+LiwsT8DM
+u7atsdzhNi09METi2bqsrr5aZkYxLiw0w8a+ra+uwflNKS85PN761a+uu3VPSToxMjVkvr+sq7W9
+4k4yLj5MXt7c1drg5/j3/PTy9310aWZkZ3T97OLi4eXn7fl9ev7++f58fHdzbmpqbG14dnr///j8
+ef37fHv6/fz67/j27/Tw7u73+v99d3h6eft5ev98/vt7fHt5end4dXd9ffz/e3t//Pz7/Pn6/P/8
+fvv6fv59e/f9/v/9/f38fn1++H77/n37+fl9/n5++3x+ff3+fn1+ev78fvz9/vp8/Xt8f37+e37+
+/H18fnx8ffz8ef75+vj9+/36fn59/n55/nr+//v4fvz++/f7+3t5fnl+ffz6/vz6/n73fX58/f9+
+e3z+d3x5+P1/+Pz6/ft7fHd+fXz7/f/9/f//fv17fH94fXt7/vfx+Pj7/Px9fH97/v59/n78+/38
+/Xp++3h6enx6+Pj5+/33+v79+/z8+///e3j+fHh5en94en57+////X75/Pv9e319eHBnWvy8v/T2
+RUlANjw/ybu8ra21vFlKLyc0MTzs3sGura/UUl8/Mi4vTL+9uMCvqbfrTDs+MSwuM9HKwa6vsr15
+Wy0uNzFF3sO8raqw0lZXPC8rL07Owb7Frau+31E+Py8vMDm9v7yvsrC9ZFgsLTQtO1vHvq+or8xx
+Yz4uKy9I2b28v6uqtuZPST4vLS03v8W+sq+svWRbLC82LDtPysKzqq/Fa1tDMSoySvvCx8GsrLbn
+VU8+MS8uPr3AurWvq7tmUS0vMyo3RM/GtamuxtxrQS8sNEBvwMG7qqy2+lVOOy8uLUC9wbm6squ9
+7WAvNjUtNz/Ow7qrrb/OZ0gwKjE5SM7Fvqyrr8tXXT4xLC07v764urOps95bNjU2KjA6fcu/ray5
+w2xMMCozNkTPxburqq3KTVE9MysrQMbFu72wqrfXWTo5My40O9rCu6+wuMlUQzw6PD5GT15439XP
+zMvKy8zO1trn7/ltaGFpYl9dW1tdW11iaW9xfvn6/Pv9+PXy7ufi4+Hh4eXp7vv8d3VzdG5sdHN4
+dHJ0dHZvbXJxb3Nzdnb++/rx8+/3fu/u8fD38PT2+vb8+X77/nz4fv58fn7+dnN8e3d3eHV8/nl9
+d33w+fr3fXx8f3h7fXp+/H77f337/f75+/z8+vr6f/n7fv78evx8fft8fvz9fvz7/n3+/Hz9/n36
+fn5/d338fv7+eX35f31+eH769vp/fn/6fP38/fb7dXZ7/Hj8+nV4/vv5+f33+/j7f/v7/fv/fX7+
+eXv+/f77+/3//P99fXl/enp5d3x8/P7++/n4/fv8+fh8fXp4e33/+336+fj19/t9e3l7eHt6//j7
++fXy+Pf2enp5eW90fHh8fP/9/fP3+/h9/nh3fHh+f/z7c2VfzLrUXmlYRzY0PHjJurq9ra205jw9
+Ni0oKy9Wybe1u6qorspHSz0zKywuTcm5tL2tqK7JREY7MCkqK0jHvLS/r6mvx0VGPTUrKy1WwL21
+w6+psMxKSj82LC0tU769tsOzqrLPRkI9OCwrLFW9v7bDsKiy3UxBPjUrLi1mt7uzv7Gpt29JOzw0
+Ky4sY7m8tL2wqbh9Vzo9NiwzLm+2u7S8s6u8XlU2OjcsMy5Ptrq0ubWquF9fNTY6KzMuR7O3s7W0
+q7JkUzgwPCsvLz24uLWysKuw+lw6LTwsLzI7u7a3sa+tr/5LPCw8Ly41Pb+6uq+trq/uYD0qOy8w
+OkC8tbiurbCzXEk4JzUuLThDvra2rKywtHpeOyk3MTA9T7qztKystrpZTDcmMi8vPE68s7Sqq7i8
+Y0w3JzAxMkNvu7Syq6y5xGJHOSsvNTdI7r60sbjCyuJ4XFVQTk5OT1FUV11neO3i3dra2tve3uHm
+6vPzfXl2bnFyd3Vsb/z6c21oZGNiaGtxevnv7u3s7u3u9fX6+vv79Pf69fL19PT0+f59enV1em93
+dnN8dnp6e3p9entxdfr1fn57enx4fXx+ev/9fvb8+PX7/f33+/t8fv7/+Pt+fn14/X59fPr8e3n/
++n19e33+/v9+/X59f/5+enn+ff3+//r+/3l+/P9+fX1+fnn/fX57en15eP36+/f++33+/356e/t8
+/vz9fHv+/339+P39/3h7+vv8+/x//359/Ht8/n77d3j8ePz+fv5+e/58//37+3/+d339fnt/fHx7
+dXZ8/3/7/f349vn4/Hh8e3x8eHh9fP7++/z8/fv8+f3+enn+ffn+/fj69ff8/Xx7eHVxcnp2d/5/
+/Pv1+PX6+fh5enpxcnh5eH57+fLt39ToSDxFX2/MxMjAubi/XEI7OTMrN0BDbcu0rbCurLG0yzo7
+Kyw0KzAxSLa0sbOzq67PTD41OiwsLjPJubOzvK6sss5BQTs1LCsvRPbFtLiyrKyx3UVAOzQqLTg9
+TNa8srWvrLK/ykE+MSYzMTA8P72tr66urau8S0MuMjAoLitKtrWvt7eqrrlUPTw3LygpMujJt7m6
+q6yuvkVDPDktKzNATtW6trevq6/B7UY5NycsNjE+TsSvsa6qra26RkosKjQpMC89tLGvsLarq8dM
+PjY4KykrLeO+s7G6rKmuw0RCPDgrKi0+Zs2ztrOtq67JR0E5OCkqOjlF8MKytLGqrre7R0I0JTMv
+Ljk5wa+yr7Ctq7dOPzMzNCkrKkW6u7C6tKiruVNIQTouKis0/861ubmqra7CPj44NiooMz9Ozriy
+s62orcPbRz41JjE1M0FLwq+0sa6zr78/SS0tNyoyMD22s7Cxuqusv1VFOz0vKywtecG3s72uq6+7
+Qz87OS4oLj1Y27i2tq6rrb5RRzs6Kyo5OUFmybW1ta2ut7hNPTkoNTQuODjOs7aysa2qtlpMNjc2
+KS8sQrq7sru7qq67TT49OC8pKzN+zrW2t6mqrL1FRTw5Kyo0PkrevLO0sauuxtVKPDomLTYwPT/l
+r6+wra+usU9KNSo5Ki0wML+vsK65ramz6kA8OTUpKipAyb6yvbKprbFjPz85MiksN01vvLW3says
+tOhPPDkxJTI8O1jYubW3rauzs9U+RCktNiw4NU+wsK+xs6yu00I5LzYqKCst1Lmzr7msp67JTUg/
+OCwsLj/rxrS6tKyuseU/Pjk0KSw2PkzTurGyrqqvv81EPjMmLzEwPUTArq+ura6tukhBLy81KS0t
+PLm3sbW2qqy7VkU9Oy4qKy/vxre3vaysr71BPzo5LigvRVnKt7q0rKutyU9HPDcoLjo4SXi/sLSx
+rLK3vz0/LSc2LDA1O7mwsa+0rKu/XUs0PS8rLy1euriwvLarr8FIPzw4LSotNV7Yt7W2rKqsu05F
+PTksKDE7Qvm+srOvqay6wko5NyMsMy06Q8uwsq+rrau2SkovLjcpMC49traxtbqrrblPPTs5MCkq
+Le7Euba+q6mtvUVIPTosKi8/WNG2t7Wtq67KTUE4NycrNzRDXsSwsa+qsLW7Q00vKDctMTk5vq6x
+r7Wvq7tORTM2MygtKz+8ua+3t6mstl5APzovKSsvVNy5tLmtq620Vz86NzEnLT5AeMK4t7KqrLfI
+YD4+KSw5MDxD27Gys66ur7NQPjQrOCssLjG/trOwuquptv1LPz4xKy0tWMa7tcGxq7G7RUA+Oi4q
+LjtW6bq2tq+qrL5XSz48LCo3OD9cy7Sxs6yuuLhQPzkmMC8sNzbRrrCusK6qtGNHNjQ0KS0rPb66
+sri7q621/D8/OzYrKjBc0bu4va2srrZOQz47LyoxPkn5v7e2tKytu81aOj0qKTgwOUT3tLCyra6u
+sV1KNyo3LC4zNMKys7C8sKu4Z0U7OzQqLSxBxbyxurWqrbVlREI8MystNVB+vra5s62ttW9IPTs0
+KDA8PE/evLK2r6uzuMw+QSsqNi01OUm1sLGwsK2ux0U+MjkwKy0t7Lq6sr2wqa/CS0g/Oi0sLTzd
+yLW9uKyvscw+Pzk3Kys1Q1PNubS1rauvx3BIPjIoMDQ4RE3Fr6+wrrOzvURELys2KzAyOryysLC3
+rau7YUk6PDArLi1Pw7evubKrr7dVPDs2LygrNUt4vLS0rquste5KPDMtMTtFX2HRvq+329dOVlg5
+QDpOuLe1vMqztMpDNzc1LigrLk7ZvLC3rqmssPBFQj42Ky89SWXDtrG0rqy0yOc6Ny4lLzIxPk/A
+s7Swra6uvkpGLzE1LDMySLmzsLS4rK25Wj46NzEqKS1Xxrq1va6qrbhNQz46LiouOlDnurW2r6us
+u1lGODksJzU2O1bYtK6wq6u0s+9EQScuMiw4NVyvsK+ytKyv40c7MzorKywv0bqzsbytqq/FQkE8
+NyspLDx3yrO4squsrcdBPjc3Kyg4PkjTvbW3rqquu8ZGQDYmMTMxPkHEr7Kxrq6ut0o+MS42Kiws
+N764sbK4q6q060tDPTEqLC1Ozru1va6rrrdKPjw3LikuOkt+vLSzr6qquvtVPz0rKTY1O07etbCz
+ra61s10/PCcyMCs3MPOvsq60saqv2Ek9NzgqKys0yr60t7ysrbHLPz86OSwqL0vuwba8sKytr+hF
+Qzs1Ki87PlLPurO2r6u0vM86PS0nMy4xOkW5rrCurayrxUtELTYvKjAtXbe4sLu4q6/HS0I+Oy4s
+LTTxzLe2uqysr8BERD07LiswPk3mu7S1squsu+JVOTwrJzYzOEh7ta6xrKyvr+I/PSgvLyo0L2Sy
+ta+2s6uvyUU+OTovKyszy7+3t76sq6/GREY9OS0rL0Fpy7W5ta2tr8tEPzg3Kis4PEnfv7S0sKuv
+usBIRTYoNDIxPT7Jr7OwsrKuu0xHMjE2KS8uOr64srS7rKu4a0VCPjUrLS5N0b2yu7GrrrFuPTw4
+NikrOEVTx7e0tK2qsMnuPjwyJTE2NERVva+zr6yvr70+Py4tOSsvLzy5trKys6qrvFFEOTwuKy0t
+dMC5s76wq6+9RD46OC8qLjpe1Li2tK2srr5VQjg2Nzo/SFdr49fOzMrJyszP1uHzZ1xXU1JSU1da
+X2NodHj78Pf09Pn07+/r6+ni4d/d3eHl7fb+cW9raWdqaWpubm5vd3hzdHJyc3V0dX779/j48u/0
+8/H08fLz9vr68+/5fv55enl2eHp+e316eHZ6e3t5d3t4eXd7enr/en97ffz7/vn4cfz9ffj39fn8
+/Pt9+fV8+vr//3l+fXx7fHl9fXz8/vv9//59fX59fvv+fn56/f5+fP59fHx4eHl/f/t8/fx7e3t+
+e//+eXj7/X1+fv/++/1+/f78/vz8e//6+X58/vn9/f79fn36+nx8fHt6/nn//Xv7/n5+/X53eHdy
+eXR3eXX7/fj4/Pb6/Pt9/3l5fnZ++vD3/fj893/1em18/3n8/nT+/P/5/ff3+/56eXV2b253c3j9
+9Pf67/P3+Pv8fnl3c25zeH378unf0dhJPUpOT23Pvby+t7jMz0k5OiguODI/QOqtrKytr6uu3ko5
+LjcqKiwuzri0sL2wrLPPPj04NSspLD7ZwLG4sKmsrspDPzk1Kis0QFHMuLO0rauvv9ZEODMlLDcz
+P0/ItLWxrK6uuE1ILyw2KzAyPLqxsLK4ray+Tz43OC4pKyxev7ixvK6orbxTSUE8LisuPF/Yt7e3
+r62vwklCOjgsKTU6P13NtrGzra24uvdAPSktMy04OGqvr66urqyv3EM4LzYqKiovx7iysbirqrDY
+Q0E6MyorLUnXvbS9r6uusWQ+PDg0KSw9SG++uLizq6u22WE+Py0pOTQ7Sv21sLWvr7OyZj08KDQv
+KzQv57G0rrawqbDbUD47OSsuLTzHvbO6u6yut2k+Pzs0KiwyTHK/tLawrKyy6kU/OTIpLjk9UtG7
+srOvrbO800E7LiozOkdh5M/JxsbHyMrN0tjpeGNaVFJTUVZdZWp0/vf39fb4/fz7+/f18evp5+Ph
+3uDi6evxeW9raWhoam1uc3l4e3h5cXR2b210cnF1dHL//P349/X39vT99ff99vz3+Pj3+/78+/z6
+/353/X14f356ev95/3p++nl5enZ7+n36+338fnr9//3+/n19+vt8/vp8ff56e31/fnl1e/z9/H99
+/X77/X36fHz7/f79/P74enb+ffx+eX3+fX77fH18//x+f35+fHx9dX30ff57e/58e359evt+f316
+9/76/f38+/v9f/z5+f78fHp9fX58ef37fn9/eXz+enx5eX55enh8eH5/+/z+/v1++/t4eP95fX3+
+//z49/r39vb8+f3+fHl3ff/8+v7/+Pf6/395eXh3dXFydHJ4/Pv38PH38vj7+nt7dXF9eXR4/vr3
+9+/n39tbQj9DesPGw767tsNKQDY2Oi42NUu1sq+0uqytuFU8OzgyKSovZs25t7+sq667QUA7OS0q
+MD5S2beys6+qrL1zSTc6KCg2MT1M1bCvsKuusrNRTjkoOC4uODTMsLWwubSst15FOTk3Ki0sO8C7
+r7e5qqyy9j9AOjQpKzBN/b6zua+rrbJ9Qj04NygtPj5Yzbq2t62rs7zRPUYtKDovNz5Mt6+0sLCv
+rso/PS00MSovLFi3uK+6s6itwU9HPzwtLCw12Ma0uLurrbDLPT45NysqMEFW0ri0s66qrcRiSj06
+KSw3NkBWzLKxs62vtrZNPzknMy8tODPRrrGutK+rsv9GOjU3KSwrN8G7srW8rKyy1z0/OTcrKS9U
+2ry1vK2qrbBeQD85LykvOT9RyLausa2qs7/YODgsJC8uLztKu6+vrKusrMBSRS8xLywzNmG9ubO0
+ub3I2G9VTEZDQkNESU1SZfje0s3MzMzO0NXZ3+ft9np8dG5ubm1pamhjYF1bXFpaXmBpdfvy6+bl
+5ebp7O3s7/D59fXz7+/v7+7y9/z6/np6b25tbnh0cnJ0dnh8dXV6fXh+/P38+fn17/D39Pf59ff3
++v76+/T5/P97/np4e3N4/Xl2fnp6eXd0dnt//n/8ffr/ePX2fff69/5893r9/vz8/fx8ev7//Hv+
+fnv79359/fr9//59/f37f3/9/Hx7fnnz/vbyfH59fP19en39/3h9/f37/n54/H36+v77/vn7e/1+
+fvx8e3l3fH59en7/+318/Pn+/X78+3r3+Pz8/Pr9+fj//317e358fn16//b99/x+/nl4d3l7en58
+9Pn++PP29f15/Ht+enN3/n///fv9+/T9//f+f/56e3l6/vn7fvv8fv36ffjr3+1HQk5Z1NHKtrO3
+2EFBPTUtLjdYy7e2t6mprstDRTswKSouS9a8uL6sqq/JQkU8MyorLkvNurO8rqmtvktIPjYsKy1A
+zL+2wbWrr71NRj86LystPsbBuMC5qq6+U0lCOi4tLTvAvra8vKuuv09CPTsvKyw4wL64u7uqrcdf
+Rj08LS8uOr27t7m8rK/QUkA4PS4uLje+vLi3u6yt029ENkEuMjI3vbe3tbqur+NYPy88Li8zNMC1
+t7K2rq3QWEUuOzAuNTTGtLmysq6uzU9GLTg0LTU2zbe6sa6urs5USSs1NS45Os21urGtsLDMREYq
+LjgtOkLSur2vq7Czz1FMKjA4Lz1LxrW4rqu2u+dBQCcsNi48T8i5uKyqtr/iTkMqLjg0RP68tbWq
+qbnQYkI8KCozMT99vra1qqm34WBGPCoqNjhF2bu2s6qqte9VQzwsKTY6RtG9urKqrLdcTUc4LCo0
+Pk3Ju7yuq624TUY+OSwoNEZhv729rKutukhKPzYsKzJJfb24vKyrrrxDQToyKikuQ/O/ub6sqq28
+S0xANywsL0nVvba/rqquv0ZGPTQrKy1C0b61v7Gprr9KRz81KyssQca8tL61qa2+TUU/OS0rLDzC
+vrW9uamtv1BHQDktLCw7v763vb2rrsBORD87MCwsOcG/ubq6q63CXEc9PS4uLjm/uri4uqyuyE4+
+NzouLC00wrq4tberrc1sQzM/LjAyNry2uLS3ra7eUT4tOy4uNDPBs7ews66t0V5HKzoxLjg0yrK4
+sbCxr8tOTCsyNi03ONu2uLCtr7DGVkwsLzQuOTrhsrWyrK+xv0ZHLCs3LDQ9/bq7samusr9UWS0r
+OS85RNmztrCpr7jFP0IrJTQtM0Psurquqa67xF1VLys4MztSzLW3rqmvv9ZFQSwmMjA0SNe3tq6n
+rMDZUEQvJzAzOVbHtratp67Id0tALycvNDhNybWyrKarvW5LPTAlKzM3Tsm5t62prLtjUkE3KSw4
+PFfGu7esqq28Tkk9MikqNDxZwrq2q6qsvElFPDMoKTVAbL26tKmqq8FFSTwwKSs1Rf67uraqq63J
+P0U7MCkrM0n3uru4qautyEJLPDIqLDFP2rm4uqmrrss/RjowKSsvSOK7tLuqqa29QUQ7MyopLUPY
+vbW+ramtuUdHPjksKi1D0L+1wrCprrlHRD84LCosO8vAtr+2qq22T0BAOS8qKzrGwba+uqmsuFZH
+RDsuLC01y7+3ur6rrLpUQ0A6LyssMcu+uLi/q6u5ZklCPS8sLjDNu7i1wq6qumFHPT0wKy4t4rm6
+s8GvqblwSzw+NCsvLGK3vLO+sqm180w/PTksLyxOub61vbOptPdSPDs5KzAtTLe+tbuyqrNvSjs5
+PCwvLUi4vre2rqmyZ1w4NjwrNS9Ktbu2t7Kst1ZPNTA7KzEvP7e6tbGvqrNoaDUvPCw1M0K1tbWv
+sa21VV4zKzorMTM+tre2rq+ttVdmNCs7LDI3Q7a0tq6urrNVVDgpPC4vOEC7t7iura+yXFk5Jzgu
+LzpFu7a3rKuvslxHOSUzMC48TL66tqqqr7ZeUzknNTExQV25t7WqrLa8SkI2JDAxL0Jgu7myqKu4
+v1xQNyk2NTdM2rm4sqmuws9LQzMmMDM1SNu5uLCorMHYUUU0JzA1OE7Nt7evqay/6U9ANyctNjhO
+zrm4sKmsvGtPQDYoLDc4T8q6ua+qrb5WTD03KCw7PGPCvrmsq63BSk8/NSouOUJsvrm3rKuuwEJF
+OjMoKThBbb6+uKurrcdJT0A0LC45TOq7urerra/OP0Q6MSkrNUf/vLu5qquuz0dMPTErLTVV17m7
+uqqssNVBRTwzKiwyT9u7ubyrrK7FQ0k8NCssME3WvLm+rayvwUNGPTctKzBUzr67xK+ssMVISz82
+LS0vVci8uMKvrLHIQkQ8NywrLlvDvrnGr6qy0UpNQDUtLy9rv7u3xLGrteVFQz01LC0tab6+tsex
+qbbgT0ZDNS0wLnS6vLXEtKq5c0k9PjMsLy1oubu0wLOpt+5OPj82LC8tari8tMCzqrd+TD4+Oi0v
+LVa5vra9squ3eFE7PDktMi5ct7y2vbOst19KOjo9LS8uUrm/t7iwqrhqXzY6Oi03MGG1vLa5s63A
+T08xODksNTB2t721tK+twl9bLjk4Ljs11rK5s7Ozr8xOTiw0Ni05NOa0ubOwsq7GVFcsMzkuOjnb
+tLmzr7GvxUxRLS87Ljg75re8tK2xsshOUiwvOC86P9C2u7KssrTNRkgrLTsvO0jQu7yvq7K40E1M
+Ky45MT1Ox7W5r6u2vt9DQykrNy88Ucu5uq2qtMHcT0ctLjk1QGfBtritqrbOc0Y9Kyo0Mzxew7a3
+rKq20WpJPSsrNjdB6r22taurs9ReRz0sKTY4QeW/ubWrq7PlUUk8LCo1OkTYvbq0q620fktCOy4n
+ND9Lyr29r6ustFhNSDktKzQ/VcW5uq6qrbhJQT00KigwPlbDurusqqy7TE1BNiwsM0j8vbe7rKut
+v0NIPTQqKy9DbsC3vqyqrb5DSj41KisuRt68tL2sqa26Q0Q9NispLULXvrS/rqmtukZIPjYrKi0/
+z762wbCqrrpFQj45LiksQ8W/t8SzqK29SUtBOCwsLELBvrTBt6muvkVBPjgtKis7w7+1vrqorMBX
+S0M7LS0sO769tbu9qq3FTkA6OissLDW+u7S3vamryV5FOz0sLS0zvri1tb+sq8ZYRDg9LiwuLsi4
+uLS9raq/X0s3PzAsLy7St7m0va+rvFpKNzw4LS4u77m+tritqrteVTM7NSszLuCzurK1rqu+S0Mv
+NjQqLy7utrqyr62qvl1cLTg2LDgx27G4srGvrcdISiswNSs0MWq0uLCurq2+ZFwtMDYtNzjos7ax
+rbGwxEpOKys1LDY5a7K1sauur71MViwqNyw0O3+0trGqrrG9R08tKDktNEHtt7mvqK22xUdNKyg4
+LjdI0LS2rqiuuck/RSskNS82TtC5uqynrb3TTEctKDUzO1vDs7Wrp6/I/kE8KyQvMThdxbi2qqez
+z25OQy0qNDlB4ru0tKqose9MQzkrJi41PPe8tbKpp6/fT0g8LCcuOELkureyqamu4khFOi4mLjtF
+1rq7sKmrr25DRTktJy47TM23u66oq69dPj42LSUrPl7Dub+tqKuxTEdCNywpLj78wrS9raistUZA
+PDUrJys878W2wK6nrLZKS0I4LCstPdK+sr2wp6y6R0M9NSopKjjRv7K7sqeruU1FPzcrKis4yr2y
+u7eoq7hOQkA5LSkrNMm/tbu7qau5VUVDOi4rLDLGvba5v6qruFhAPzsyKiowyb65uL+qqbteST49
+ListMMK6tra+q6q/TkI4Oy0rLC7Gubm0vKqpwWdLNj8uLTAuxrW4s7ytqsdWSjA8LysxLdmzubG5
+rqm/WlQvOTIqMy3msbiwtq+qvVFPLzk3KjItW7O6srKuqbtZWS40NiozL22zuLGur6u7Sk8tMDkr
+MjNlub20rK2svlNbLDA6LTk73LS4sq2vscZGRiwtNS01Pu66tK+3vsXb7mVZVFNTUlJUV1hfbfzq
+5d3b3Nve3uHp6/R8/mxrbWJlaGltfHdxc3dya21sbHR3+vLr5+bk5ujq7PH2+Pf9fvl+/n34/v77
+fXl0cndtcXR0dHF4d3Z5e3n9fn18fv369/5/+/r49/j28/r39/36/Pv0/X78f3t6fv78+v31/n7+
+fnx3/X3//n3/fXt9+Xv+///8fv5+/X7/fHv+/v78eX78dv95/n5/fn74/Pr6/v/3/3/9fPl5/vP2
++vz8/vv+/v79/Xx9/336/Xz/fv71/vv+fX99fn57eX94/3t/e3f49/j7/Xt9fH15fP5+ff58+f3+
++3r59/v7/n58/Hz9fv37evz/+ft8/Pr6+f50+P/9e33/eX18e+/5e/J/ffr7/3/9fHx4fn5+eHt9
+evr9/vv7+/r8fv/8/v98/f7++f7/fX98/v13+nx9/Xj/fv76fv5//Pr8ff78+vz+/nx9fX34/P36
+/Pf//P/+fnl4enp6/n36/nt7fXx8/v/6f337+vn6//x++v3+/vz7//39fP7/f/5+//z7fXx+/nt+
+fnv2+/x9/f99+n38ff/9e/38fnx++33//f/+/n3//fz7fHp7f/58fH57fv/7fv/5+/v//H3+/f3+
++vp++/38e3v6/ft+/f9+/fx9f/57enp/ef9+eX56/v97//z5//59f//++/r8fff4/f5+/vv/+n99
+/X36fn1+evz7fPz//n5+fHz//H57/Pz7//59fP78/n38+/r9ff7//Px/fH38fP5/eXr++n37f3z9
+enLy7nT9f3r5ffz9/f1+9X38+3j2/H/7e3v9/f73/XX9/f18//59ff/+fv//fn5+//3++f79/f9+
+e3x+//v+/v7+///7/fv9+/7+/n19e/96evv/ff38+X5+fX1+/vz7/f39+vr/+vp9//x+e35+d3h8
+/n1+9/z6+338/n98ev/9fXp5eX19/nx+///7/f76/Pt+/fj9+v37+f37/3t8e37+/v7//v5+e3x4
+/v54+338fv79/f56fn15fv3/ffr7e378+/19/H3//f55e/v8fnl+//v/ff3++/35+PP9+n79fvz+
+/fp8f31+/X/6ff1/fXp8/Xt4d/97enx7eXz5fvn7+/n8+fr/f/v7fv7//f99ffr+/Pl9fXx5+/1/
+//z8fnx4/v//+vv4fv76/P38f/z8ff56//53/Xx+fnr/fP/4+/78fn5/f35/fn7+fP1+eH78/Pj9
+///7+fl++31+/X37fn79fX56ffv//P18ffz9fX79fP5+/vr+/n7/fv19/n/693z9/P56f/x09fj/
++3z6ff52fv53eH3+fH94ev59/fn9+fz6+3z/9fT9937++X7+/v19ff/9ev16fHt3d3j9evt+/vd+
+/33+fX74/fj++/r99fl4d/12fn79e3r79/n8+3r8//79fPn+/v/9/n98/vl+fXv//ff+ffz9fv58
+e/t9+n/+/n/9fP59eHv/fn15evz+f/j//vv+/3p+fn5/f/t9/fr7f/v6+/79/f76+/t+ev35fX1/
+/339/H3/ffx7e3t4fX5+fPx+/Xz+//z7/vr8/v7/f/5+f/99+/l9+vT9/fp4fv5+/nj9/n7+e//+
+ffl//vz6+v78fvv9ef19/fl4fv59fXz8ff59fvt9+356/f79+////P3+fXp//n78fvx+eXz+fn39
++/37+f77+3z7fv78f/7++P7+fv//ff78/v7+fnx9eH78fXz++/1+//98+/z7+/35ff/8enx3ff56
+/f///n1+/P/4+fr6+vd/+3t6fP3++P1++/7//H94ff19fv7/fH//fX7+/f/8/v7+/f59f33++nv9
++/39fv/7fnv8fX3//n19fH19+vr++vz9f/z9fn77/v76ev39/f5//f1///59f/56eHn7/vn6/Pd/
+fvx3fH1+/H56ff5+/n18e/79/f59/f7/+/589/x++Hv7+/v6/X5+fvv6fH36f3x9e3j+/3x7/f1+
++/x+fn5+/X3+/378e/z/ffx+/nz9/H7+enz6/n1/fv78/Pz+/v3//3x89ft++Hr/ff/8ffz9/P77
+fP59fXl4/v/8/Xr+fH78/Xv++fz//n18fv39/np/+/z+ff3/f/d+/3/+/f/7fv19/f3//nz9ff58
+fvr6+v79/H38fXp9//p+fn7/fXx+fP/9/3z//f76/vt+f//8/Hx+/vj/fv9+e3/+fnx+f/99/P18
++Pt6fvn9fX7//Pn99/n7/fx+fn58fX5+fv59/X57fH19/338//j/efr+en/9fvP/fvl++fZ7ev59
+ff1+/v59/n1+/vr6/v34//x9fnx++vx7/vv+/n7+/333fP57fP57/P7+ff5+f3/++/75/n59/H56
+/3r9fv97eHx+/fn+/fz+/n/9/Pj8+/n79/58/Xz6fv79/X58/Ht8fPr+f/x9/357fnx5fv99fv77
+/Px+ff9+/Pl9+35+/Xv/d3v9/vv8/Pz4/v3+ff37/v79/Pv+/H79+v36+3/++nx9fv97fv59fP99
+fn18fHx9/v17/v3+/vz8ffz+/fv9+/37e3j9fP5+fPx7/vx+/X37f/f8/vl7eH73ffr7/Pt+/vt+
+/fx+ff5/fH76/X5+e3v+fnx8/Pr7/339ff3+//18/P/9f3x9ePt++v9+/Pz/fH16/P37+fz2/P1/
+e/99f357ff3+ff98/X3//P399/78fnt9fH5+e378/vt7e3x//fv9+vn7/v19e/x+fXt+/v19fHt8
+f/z+ff7//f5+fvnxdnj1dH/593h6/f7z//j+fPl+ff97evx//n3//Xv6fXz/fP1+fH1//v//fn3/
+/f1/ffv+/fp/+Pf5/v78+fz//n19/fv8/nl6fXl8/np+fP19e/r+/vz+/nz++378+/9+fv//fvf5
+fn54//z9/fx7dvl+/v76/vz8fX58fn78+v36/P5+/f/7+f1//n7+f35+fv3/fHv8fnx7fX15/n56
+/vx5/fv9/fr9+X7//P9/f359fPn8/Pn2+f/+/nv++Ht8/H38efp7/n59/H3/e/t7f/19ff59fn/6
+/vz7+/v9+v/+fP77/n5+/3z89f7//3t7fX5+fP9/fP99+vx+/X1+/f/8/vz8/Pt/+/78/fz8fnz9
+/379fP38e/5+/P1+/X16e///e35//X79/vr+ffz9/v78/f37ev/7//79fP1+fvz+/fz9fnv9/n7+
+eXx5e/36fn75+fz8+3/8+/98fPh6fP5+f/7+efx7//18ff5+fPt+/f5++3x9/nx8/Pr9fP59/P79
+9/33/fz++ft+/X5+fH7+fX7//v59/n59/3p9/v76//9+enz9/H3++/r7/Xx6d/79fP79/X/9/f98
+//19fn77+/5+///6+/v8//f8/f55fv///X18+/t+/n7+evv+fn9+/X3+f31+/X99fv3+fH5+fH/+
++/7+/fz5fv7/e/x///97//1+/n56+vv8/vv9+/76/337ffx8enp+f/33ffv//fp8/np8fH58/319
+//34fv5+fnz+/n/4ff76/H39eH77/f59/v57/H56+ft9/H/7/vz++n9+/fv9f/5+/v99/v33fnt7
+ePz/fvR9/v38/Xx9/nr//Xx+/vv9+/35f/1/e318/nl8/v18/vx7e39//f77+Pz8/n/9+/r6+v9+
+93r8+3z8/P1+e35/fP58env+fHz+/P19/nx9/339/35//v39fnx+/fv9fv37//79///+/P78//7/
+/f9+f3/8/v1+/P5+/f////t7ff35f/58/n1793v8ff3+f/58/n3+/f1+fvp//35+ff38fn1//X3+
+/33+/vz8fvz9fv19fn9+/P78ff7+/P18//38/Xz++n78fn5/fv3//X79/vt7e/t+/Pt8//t9f3t+
+/31++/59/P39/v99f/v9ff7+/f/+fnr9/Pz9/v38/v3//X1+/H3//f7/fv9+f/79/339/n/9fn3/
+/f7/fn/5ffx+/X58/H/+fPx+/v99/v39/37+/n7//n7+/H38/X/+/vr/fv///f79f/3+/v59/nz9
+fH7+ffr+fPx6//z//Hz9//5+fH98/f3+fP78fv59///7f378/v5+/v7+fv77+339+/7//v1+/f/+
+fPr9e/3//331/Hl5cf77/375eHd+9/j9/Pp++v/+/P/+fX37fv1+fn3+/n59/v7//nx/e/5/fvh7
+ePh//Px///79/v16+X9+/nz/fv3/e/z9/X7//v/+/fp7fvp//X59/379/nz+/X/+f35+/Pz+/335
+ff39ff79/fz7ff38//t/fn/9/n9++v/+/39+//5+/Hr8/Hz9fn7+//79fP/6fn/8fX38fn98/f5+
+/n1+//37/v7+/X5+//5+//v+/X78/P/8ff39fvl9fvr//H97fP1+fn/9/X37fP19fvv+/H79/f56
+f33/+37/fv38f35+/v78/vh5/vt9/H3+fv3+//3//P38/Xx//H3+f//8ff59/n39/n7+/v7//37+
+fP78fv59/ft9//5+/n1+/3/9ff58fnz/+/15fvv//X1/fv7++/18/vv8fv1+/ft+/H79/X7//3z/
+/n19/f38fX7+fv5//v59/P18fX9+ff3+/n7+/v7+f378+///fn/6///8fX/+/fz9ffv8/P19ff39
+/n7//f9/fn1+/fv+fXz5fX79fn3+/P9+ff77/X7//n9+/357/P79/31+ffv+fv3+/f5/fn5+/vz6
+fX33fv7+fv/9/P99//1+//9+fn7/fP3+//7//n5+/P7+f379/H1+fX3//n5/fvx+/v59fP3+//t9
++vz/ff79fPr9/v19/P77fXz9//19f/3+/n7+fXz9f////v1+/X96//v+fn5+/f7+/X3//f5/ff39
+fP9+/X/7//5+/P9+/X//ffv+/X/+/vt+/v5+/f3+//79ff76eHv8fv59/vz/+X97/f/8/X7//v9+
+/n18//7+fP7//f39/nv9/f79fvz+fv1//nv8+vz+//p8/fx9//9/f378fn77fX99/vt/fX/7//79
+fH79/H3/fn77f/78/3r+/nz9/X7+fn99/f/8fv/8f/59/n7//f78ffz7/Px9/P/9/v17/vp9fX5+
+/33+fn35fX39fP59/P3//378fv5+fX5++35+//1/+n3+/f/9fv7//P7+ff5+/fr8/nv8/v1//n5+
+/f59+/79/Xz+ff1//f9+/P1+fv98/v3//X///f3+fn3//35/ff5//v5+fX78fv19+v9//X7//n79
+/H7+/fz+/v7+/Pj+enn+/XF8ff/+fX98+Pn+/f==
+
+--owatagusiam
+Content-Type: image/pbm
+Content-Description: MTR's photo
+Content-Transfer-Encoding: base64
+
+UDQKMTA0IDEwMArve3/+3//////////w3b/b73//////////sLbt/7v3/v++//////B732/+3/+5
+/9/////w3nf7d//9/9/////78Lfc392//m++7/f///Dtt/b//7/d7//////w3bs//////v93////
+8Hbu7b/99//9/////+Du2+///9///77////wu3c7f/v9//6Nf///8O212////2+60z////Bvvd3/
+/7+ym3bP///w9s5n//+/tt253//38J3zv//+/9+3/mf///B7Pf//9v/f///r/+/g7u1v/90/////
++///8Lbvf//n7aO/6eX/3/C3O9//vu5s7/91//9wbdr//fszW2/983/98G3W///pkkN//Lv3//C2
+d7/+zEyU3f9t//+ws7n/9iAAEpf/c///4M2ef/EiIAJPbJ////Dd5//5AAAASf217/+wtnn/zBAA
+AAb29///8Gbef8CAAAAj2t////DZp/8yAAAAAfs/777Q33n+SBAAAAX83/378GbefskAAAAB9m/f
+/+B6zf4kQAAAAPs/9+9wm3d9kgQAAAL7t/2/0O20/JiAAAABbP/v//Dtn/ygIAAAAWx/+9twm2n9
+JggAAADzf/7/0HZu+UCAAAAAnb/XtvBnt/5QIAAAAP3/X/+wubX5hAIAAABXf2/b4L5t+SEAAAAC
+Xf+z//DNz/5ISEAAAGb/v7dwc3P5kgAIYAA7f3/94L51++SRvyQAP/5237Bmzf/9RPeSABf+7/bw
+W7v3///52SAf/u974Ns25//7DAEAD/n/23Dc1dv/rmcggA/o/v9wZ9tb/+SeyAAf4H+30Lsqxt/S
+PgBAW+A39vDa77f/3j/gT/vwPd/gbtWz/9oe/DuH/D95sG23j9/CHmQwBYA/7/Dbakn/4i8AIBYA
+N79w222Xf8EGyCAHAD330G23N//BEJIACUA/ffBtmsm/wIAQAAYQN+9wm26Yp4CCACAGAH3/0PNt
+pl2AACAAAAB/dvBtsyVfIEkAIAAAd9/wbbbxLkAEAAAAAP270Jtt3r6AEkKAAIDu7/DzWZ/+gAZa
+AAAA/v3wbdtgT8GBgAAAAfefcL222k/gASAAAAP98/CW7Zky6ADIAAAD73/wdtnlHuQAYAAAR/vd
+0Nt2fNuSADAAAgfe73C7bxo2wgAYAAAP/7vwbbnqbaAADAABH/v+8M227JskACQAAD3+77C22zY+
+2QAkAAAf97vgdtubD/lT8AAAHf/+8Fts+V/gAJgAAD97b7DNt2xHkgQIAAA///vgdLWeNPAQIAAA
+G+++8LeZ05ZMgCAAAD/777Crbn0TAAAIAAB/v//QamenbeIAAAAAd+/28Fu5uyUoAEAAAH3//uDd
+rtubyQAAAABf9vuwptts0nSAAAAAf9/v8DrZt2WSAAAAAM9/ftDbbt95IAAAAAC3+//wzW3ZmWAA
+AAAAt//t8Gebb+yIAAAAANv9v7C0//72gAAAAAFb//vwn///NkQAAAABbf7f0P////sgAAAAA2b/
+9vD////eiQAAAAKa/7+w/////8wAAAQDu3/t4P///73SQAAABmd//3D////v0xIEAA2Zv9vQ////
++/yAIAANtv/28P///97spABAGmb//bD////PIwEBADObP+9g////zfJIEAA9u//7cP///8fYkIAA
+Zm2/9ND////HbIQgANtN//+w////yeZhAADZ93/7YP///4DZEAAB5jd//dD///+yexIAAzu5/+3Q
+AAAAAAAAAAAAAAAAAA==
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Star Trek Party
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29543; Thu, 3 Oct 91 13:04:09 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19372; Thu, 3 Oct 91 13:03:25 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12199> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:12 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08947> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:09 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:03:08 -0400 (EDT)
+Resent-Message-Id: <IcurRw60M2Yt4Ta1h1@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:03:08 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <nsb>
+Date: Thu, 19 Sep 91 12:41:43 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9109191641.AA12840@greenbush.bellcore.com>
+To: abel@thumper.bellcore.com, bianchi@thumper.bellcore.com,
+ braun@thumper.bellcore.com, cameron@thumper.bellcore.com,
+ carmen@thumper.bellcore.com, jfp@thumper.bellcore.com,
+ jxr@thumper.bellcore.com, kraut@thumper.bellcore.com,
+ lamb@thumper.bellcore.com, lowery@thumper.bellcore.com,
+ lynn@thumper.bellcore.com, mlittman@thumper.bellcore.com,
+ nancyg@thumper.bellcore.com, sau@thumper.bellcore.com,
+ shoshi@thumper.bellcore.com, slr@thumper.bellcore.com,
+ stornett@flash.bellcore.com, tkl@thumper.bellcore.com
+Cc: nsb@thumper.bellcore.com, trina@flash.bellcore.com
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=Outermost_Trek
+
+--Outermost_Trek
+Content-type: MULTIPART/MIXED;boundary=Where_No_One_Has_Gone_Before
+
+--Where_No_One_Has_Gone_Before
+
+You are invited to a
+
+*** STAR TREK 25TH ANNIVERSARY PARTY ***
+
+When: September 28, 1991, 4:30 PM until whenever
+What: For those interested, we'll have a rerun of last year's season-ending
+ cliffhanger, and then we'll tune in the season opener and the 25th
+ anniversary TV special. Prior to that, if you bring some food
+ we'll have a pot-luck meal, and general merriment before and after.
+Who: You and your family, including kids of course.
+Where: 25 Washington Avenue, Morristown.
+ (See Nathaniel if you need directions.)
+RSVP: 993-8586
+
+What follows is some Star Trek related multimedia mail, the last of which will give you a chance to RSVP on line.
+
+Live Long and Prosper! -- Nathaniel & Trina
+--Where_No_One_Has_Gone_Before
+Content-type: audio/x-sun
+Content-transfer-encoding: base64
+Content-Description: He's dead, Jim
+
+LnNuZAAAACAAAFmUAAAAAQAAH0AAAAABAAAAAAAAAAD////////37+/r7e/z9/f7//93b29z
+//////v37evv7+vr7+/z9/97b2NdVU9OT09RWmFr9+Pd3d3e4+vt/29la21ra2lra3vn3dzn
+9+//YVtaV1tp9/PjzsjHxsbGy87P2ef3c2dz/+vr59/Z09HR1dLR2d7n/2lXTUU+OzY1NTQ1
+OD5ETmXf2dLP1dfb3O9jYWNhX19pb+/ZzsjIzMzP43dfVU1RX2/bybu6u7q7wcjN0+1ra1VR
+Y3fv7+fZzsS/v8LEztXW62FPRj03Mi8sLCwsLTVFVffLwL6+v8XFy9b/W1tdV1RVWVtr2srG
+ytXj/09LRT4/SVtd2MC0r66trrG2uL3I0/dOSE5WWVxn99nGvby7vsfR4VdKRT03MCwoJSYn
+JyowPURb0cfHz9lrZ2tZRktdd+HazcrGv7iwrrC4usHR5W9VTl1pd8i7tLSztbm/xcrb91dF
+Pj9DREhXb+vQv7y7uby/wsr3XUQ7My8pJiYnJiYsOEFK38G9vL7Fzc/R72f/1dXe69/v99/M
+w87v919NSUU+PUVMTme8s66urKyvsrO2vcTNb1JVWVNLTmfr2szKx8HLzs3XY0o9ODQtKCYm
+KCYoLjpHacnIx8fI1W9dU0A9RU5nZf/Zxru2rKuusrK3xM/cb1pvd2PXu7Ovr7O2vtVlS0M9
+ODM2Oj1ATVfr1cq/vbu6ur7DyutSPzoxKiUjJCUkJS48T+29trSxt7zCyNNYS1FYb+/t2c/H
+vrq6x9HbW0E9PDg6QE1dxbGtqaenqayxtbnC3FFNS1NeZ//c18/SzM7Pz/ddTktAOTUvLCgl
+JScmKCw3QV3bvru2trzCyMjrT0hTe+fXy767urCsq7G4vcn3WU4/P0NJTs+6tbS1uL3Oa0Y/
+PDcxMjg+SlBb3si8urq4uru+yNnvW0c9OTApJycmIyQpLzdFY8i8uLS2urzC71b/697j2sfC
+vLivr7W9xM1bRT88ODxHSnu8rqyqqquvtsDXd1NGRERKUVphX+HTz9LV2t/3b1tNRDs2MC8p
+JSUlJSUsND1I2MC3srK1urm7xldHRUtLTF7Vyr61rKutsLa8zmdKPjpDRlHPt6+ur7K7x+FZ
+Rj86NzU3Pk9cb+/Nvry9wb6/w8jV/1pFOzgzLSssKygoLTQ9SNrHvbi3uLq8xNtMTE9eZWfn
+x8G6r6yttLa9ymdGODQ1PUBHzrWtrauutLrC2VlNPj1BRE1be+/jzMLJzNPjXVtKQDo2NDEu
+KygoKignKjA4QGvIv7u4uLq8v83hY2lvc+/Ovrm2r6utr7O7yetURzs5PUdbyrevrq+zusfP
+91lEPjs5OT5FTFBd98zIzcvKy93ja1lPQzs6Mi4sLCsqKzRFT9a5s7O3ubm9vcPnY1dMS1NN
+XdfMw7y9x8rI0V1VT1M+PUdvz76vq6+vr7zCy9VZTkpKTERhc1lr0d3Sy83azs5rXU5EODMu
+KykmJSQkIygyOkTOuLWzrrG/xbu41s7I1+u9ubmyrKytrbS7uuFCOjY0ODlAc8O+tbbAyM3v
+X11LRkZERV7f69PPz9vZyszKx8DYy8vrUT83Li8pJioqKCs0Oz13u7W0srXHwsXcUVQ9PEZE
+QE/XzsC6uL28xcHhRz9HQD5axbavq6mssbC+z+PTRzI8Ojg700ZFd9dN47nDz7+47cjM7ztD
+Qy4qLS0jJygqMDNP38vPu7m8vbq2vsfG19fXzsO1tLOvrbPfu77VO0xFNS9CQEXCzL+3rcvY
+4108Qz0wO0jj/93awr7VwLW83bS6wtTDWzowMzMoIyYpIyorLkJd19W6r7K/vLH/zrz3Pc3M
+P2O/zky5s9HIr+dMPkIvNkpM0LinpJ+fo6qvvU5LSzYuMjk4SdDP3sO9z8vFTzZHQi49U0Rd
+zMnN59zrbzA3PDsuLi0xKTAuKz3rxb6urqWgsbe4n6Sqt6rGLTI2Kibrs8XBq8F7MDIfJCEn
+Ljtfr6ain5+foqK1w0IvHyAoIzBByc+trKepq7HGZTErHx8fHx8fHyAnMkLIr6ifn5+fn5+f
+o6Cx78HhLzRTPylHMysrOiwmTEU4P7Pew7P33atJSa6nc76sr0i2rMNevbs438hIJVlQLyx3
+OCgtNCslMTwuNOPQyLKtqqerrK7GzVvTNS0uRSwmRdM3T63FrqzM377eOT1fKy5XREjJt7Oq
+sLKtqv++wUfXwUss3UYvxNg8c6zLvrLDv75eS2suNy0iLjMfJjsyPa62yq2ipsyrxUq+zT5O
+20e5PTK8yz+8sV/VZ7NFS09NMi9UTTxetFzIu96yvt+7r1ezsMXMa1tO0z05KT0rIkAuMEn3
+QLfnabRWSkfJTDo3yk9DLN+qv1/RyUbrU0lFd0W5tr3Bv7xPZ9EwLsbEPFuvvVO+q7Cup7K4
+Y080HyUgHyMmP0BXraOroammta3Jv0tLOygpKT5EL+mst8q7q6zv0cu1UUFZP003LjMzXS00
+RkYq4z0wz0M4QrNFyrNIvsS3T7HG07DDsrasv7m/XbprOzFILS48KD4wRT3XO0TEUEjJy99M
+3bPQY925RbbEa3OvQzrJOsTXTOvJ+9e/68FMr21aRMbDQcdCxMy92a9OwNXzX0bYOlVR33dO
+rcDjw7V31Uc8d0w/OzxTUz4/VdDz99/Ore3G78xCPjxBNi8/Ly85OTMywT+3b83Gzu3Vx0lD
+70nIzFHIt669966wU8RMUelET0DLNWdMa01ttmfBP824Ok9JPeEw6Va/O2e8ULZS07XHU7rK
+uljVw1vXa/tfQ088STlLSmPnzGO/W7/Wx//TxDxNXm8x/85D30nP5bJMw+PNN9Y9V1bBUc8/
+7bG94cO+zmvHZ0zLRz9CQjhZSztAc0TOXjhO38FTyM1XzXNH5z1JNFMqT0RfTdeyXa/pwcXI
+Y1fS/87bX9nKzGf/vuHHz/e4T8DzuUpX73vFOs/Nzcr/yOf33kC/d9tcYchP671PxNDUSsL3
+uL3VxLXF+8tMS009KjM4LS45OkTjMtBZ3Ei6wtFKb0rBb0zIxcg6u2HP10vM1O8z2M81QzbO
+R8ZYtMzPU8vPz1HKU8tFYWvM90PdVMZY607HTkbcP8c9zuVFVDrINcw/QzzzZ0q9yeu0uLnJ
+u8NXb9rV+06+Srbf80/Zx8/USctaREndOE04XGFvwsW367fR3L7ezzzeOfdEyEhfw0vBd8xD
+xmvOWlNj5+c+Uzk+ODgvQDM+NUE/WVX/yMnAx3fA/7BlvGm+Ub5Rxsq+xdW988XhVmNGVzlL
+OFpjRMLTtc68w9PMy9vT7VnY8+//593aXc3Wz8/I28vv4UHHUVlMUDxANUU9OktJa9fFy8jK
+w9zM49nKTM5Zd1lR90/cXdTdz9vX3l5jUT5VTW9P2b++vbq/wdJ3Y1dKPkBGPUM+TFlvWs/F
+083Hw9HVzu1nRUg+QTU2ODQ2MzpNTGfcw8nLw8nhx9bO3cnVyMrAxrjFwL3Cu9vC92dcQ005
+PD5BS1Prv8TVx87I1Gfe/9NOa3dvT1X3d3NvzM7vz8jI02drXVM8OTk6NDQ2MTw+UljN0MC7
+v77SynfMZ1djUWtOY3PX0+3ry9Xn3ddlWm9LW0pIR/vdyb6+usPB2tTNZUxJUERLSllXa1nv
+x9bP2cfHx8rK1/9NVkM+NzM3NzM4PFxl18fFv8DFy8nMzszfxsXHzcTBwcjEyc/b299fR0M/
+PTs/TvfUyb64usHJ1W9hU01VV1JNUE1TVG9l49PcyNHN193r729OQ0Y1NjQyMTA2NUJKY8rE
+wsfCxtnZ69Tr39nj7+/OztXdysbP5//vW0NDPT9ARf/OvrivsLK1vsPP0P9eU0peWVFUW2v/
+a9nZzdXO1P93d29IPz0+NTIxMC4tNjtFSf/TvLu+ubu5vb3Nztfez9LT19jLxczb/8f3UkRJ
+Qz08QEpV28u/wcS/zdNvVE1fUD9DS0hMW13zzsfCur+3u77Jzd1PSUc/ODk2NTAvNzs/PmXj
+z9bJzd7N1MzP29fMydPIxsbAvr7B0cjTy2fnSkJBQ1lRzce+t7u9xMbTd3tXTEdHSU1Tb9nb
+6c7SzcjEyc/lWWFKOzk4Ly0uLSsuMkNPVM3Dvr28vL+8u73IzcTFzNDGy83X291rX1FGPDo+
+Ojo5P0Jb28C/vb3Ix9HOzNFzVE1VT1lYXWNn59jNx727vb68ydlfU0s/NDAyNS8vNkVGT2fP
+zM3JzN/v1t1nb9/KzNPOzGGryd/Lw06zzMn3XNHb3TfG12dfsfPCVVfvsllvQkfDP8c64+NB
+Ub9Ew9xc0+U8OL9K2FoyLjstOi89NOsuSz22ScPOrsixY8K/+04/47/e373FzcNQuMrTa2Vd
+Y0AsP0M4z7a7q666y6v3R1XhPdZLNMjHQN3Id9m349m3wTTFRMI80zE+RTNfWVwsWka6UVlG
+sk3nv8tR9zYwwq9HONTJa008Zaavus6tu1cxLS8sIjDlvNfJpqemtr61tjYrNiciIB8nPz1Y
+q6Kon6GjpajBYfcvIx8iIx8nHys7vbemrZ+rs03rMy03Vc2trK+rr9c8QDo0PeW2tc6zpKW+
+yMXOTyYkJygjKy9GwaijoJ+fpKqryS8/LB8iIh8gKT0+Pb+wraOgpK9r4UY1LjXJwKu1tNVX
+KikhHyErN0vIuMasoa6/zME6LyYuM1VDY6ynp6Ofn6WyxunfNR8fKB8fIyYp2zU5ta9GwKOf
+xdGtrsZb2rGgpK2sqrc/LSwpHx8jMTQySc64rMfBs7c6PU44M2PZyq+ioqGfp62rtd3dPScu
+Kx8jLiQr2z8uY6jRO7Gfw0ZfwMJPS8WiprW1r18tICQiIR8q08x3tKiuqq3S5VsnHyIoHy89
+X66foaSjqL5ZSUQ1MSk3KyknO0w7R8GzoqlvvqilTe+3vltS466hq7SutjkjJyYfHyAtPt9d
+rau1r6y43U0sNTctNMjVZ6afq6yrvddKLC1FNjM3PS46RitnscU9rqZra6ejXVGzsr/Z072k
+rNDOyDUfHyIfHyAwyb7DtJ+rw7W7dywoKy4rL1C+r6qfoamptNNvTz8/Qi45TCchLln3PDj3
+59/E+2Oon6/rqaHDP9G6p6/KuLdVIyInJiIlSry/ya6tvLnhz1Q1Kjo/LTe5sK6mn6Kps99v
+Pi0kOD4vK0cyLEQ34cG+Ta2rrFU3UKeuNe+wxjI2R7atxdjB3SkfICsiISjntsLNqKOuX8Sx
+xy4kN1xENc6qqKqmrbbHOTs3Ki93Oj7HUTk8Zy85zKpESK6t5+t7p6CqS6yoyDhPxcW+Tklh
+Nh8iKiciLty7tr2vpalLSMPILC9fXE3z27+yq6motVhpYyotLixM5TQ+tOM2O+/7vDQ7vai3
+udA1XbSsVEG31zwzOruvs0/d0zghHyMnKitZq6mpt8GrsTQz7eMtM8/JtM5jwKWjrLOww08x
+Mt1nND20uzUvSW05P08+zae8bbmhs8Y/PrGt3yu/r9UyV7atyTdT9zkfISs0KTTBqrG/taev
+Rjvrxz0wX8LOb//BuK2ssrXYQTA0LixQ1c7JW+9CNSw7SzswP0Jcy6Owtq6zyz0rWa/nLT26
+ezwzS7GtzTFBUCkfJDFHRMKtn6evra/HPzE1NisuRqzXPGOyrq+2wrS4vtfDSDxNX0tGMi84
+Tj4wOrq30dqmp846wKqtRCc4vksvWq3JL0U3OTkuObGvTzbVUzYlL2vEvbSfn6Sps7zAVywn
+Mjw6P0jbwUw7x6/M22dDX0gpKD85Nzw/TmlMOUiyxeO/qqbDWdW5zEzrrVsvNUUzva+v6TpR
+MjQ6Q9exvP84MCwpKTN3uqyfn5+fn6KtvD0pKSsfHy42L0e+xrymuP9P8zgwQELv09c1Lyk5
+NS04tqaosbGvrcw9Pau0TCy+v+tALzTcwikqa8YqKTBNQj1Dt5+qurmzYy8oL9O6wa2fn6Os
+r7bPLyAqLiIgLC0yTT1DvbHL1VlXXVUyNs/GRlG9urvD47iuwsqvqq+80ruqXzT3wzw5Pv/H
+P1tfOSkxQDJBt7nVQEA4W09Uu6irzcvfbzAtQ72/uqafpqyzvVEuIB8fHx8gJCo3QLympbmr
+ornLytvK1jo/+/83b763wK2oqa7L78/RRT0/dz4xLTg5N0BZSFH/T2d3b9RLzcbbd2/bSUnr
+/1V7wcHEyr22sLm2trC4vcZ3Ri4pLjMrKjdDOz5Nb9B3Qz1ZUUI4TsvK1bmmpaqqpKesr7W1
+tM1f71UvNT89NTc3Ny8uNkU7NDg1LzE3Pmm/ta+yuLGuvr+4vMXMVUnZzVFjylZFQzk1OTQr
+MDk4P93Pv73Bxs7zSlVv693EwMbCwr+9u8zR01tJPkNQ28vCu8Lfd15JPT5Oz8nfzr24ymdr
+zk8/Pk5PUT5D2dNBP2fX3evPvrrRSklXRTlB/148SnfF0e/jy8pXUt3aZ+O+w1ta52nn72Fv
+yc/3/8jB2/fGvcrvZ01GPzs4Oj88Slr/WGvOz9nEvsPlW2tINjU/U17Z083Jz93v3efrzsft
+d2tIOj1EU2HZx7u1vMDAurzC2tO/y0lBTkQzLC82SURNz7u4vr+7t73BvbzEzs3O0dLzW/tb
+SEpNSUxVZevrTz9KPTI6OTc8R1FOTkxTXV9VXffr187SycXNycnt2cnL0bmzubO3vr/DXD86
+Ozs7PkZEODU0OD5PZ8i6u7q6vt/dysjvadnHyv9U08zZa8jEUWM3PD0uLDU+S0ZMVFdES2HL
+tbKzs7Czs7W9z9/Z1mNGS2vf/2Np2Ug5NDw7NDc8Zelnb9fLzM6+vLm9wr7Ed0lN4/9GQkJB
+Ozk8RlNNYcbHzMvbWe3W68zEy83MbU1KTz5CTU9AQz9ASVFTY9zT1c/DvLm6v7vAz8vL1Wvp
+7//ryNZ3/+lVUVlbb+fd287L2V/e1eNzX0pKRjk0Okhb5c/KytnV3OPzX1ljZ1NGQUNZ59vB
+s7W8wMr3b3dfT05MXVtfY8a8xNPRzuNIOjg7PTk4RfdzW3fb3dbS07+8v7+8v8PEzN/n81VG
+PTMxNDU1OD5AREtFSFv/3M7CxMXI0tXJxMfKuLi7xOvnztfXzdRPP2NfN3f318jYyMbrU0xV
+5/Nf1cC+yMvf6917e+fba09VTUlEP0FVV0hBPzw+PkNES1Nr7czZ39nIwL68vMnJ7Xfv+1v3
+08vT2uv7/11PTldJPTw7Okbr2My/wMjN2VdaWktFWe9hXevjy8G+vru3xM/VaVNXSj9MV05M
+91FJSlJX99ZvbWFP89X3V83K0W9zSE1NUf/Bws3LydTjyr3IyMj7UfdPUW/U78i+yuFvT0z/
+a1FOXFVcT0A9R1dQW+/z2+drd8rB09TK2U1LPTQ1NjVHW1tSysXS78bIw7q+0v9PR01PU9/J
+xMnD309Y3/db691VR0U/SFvj0cDCy9vtNELKL9xWPcRD1dNZvNdrubjf0dfp18nPzNPY/1BI
+PTg6MT7CPGPIxL5HRePFxtvjSt9KP2FZe83Jv9fj7U5PXlHl2XfvY1ZPS1nTyMC+2GV3Xmvd
+98Y3vrA9PD88uuPRr3vLzEh33l/n3cXDylFPMzE6LjY9TENVa/fny8G9tLfC1N1zOlc+Y9Pa
+uMPNx3Pj42Pr3lNXRj53Szaz6zi+P7m6NsRBQbc+XctXVkvczLfFusXd1VxGymHLZ9Xrad4z
+Oj00TCphxy5S1y/PRFm/39bLZ0jZY9+9U7nP1k+/OEPFN8HNSc33Mki/P89N2rnTx99Kx1Xr
+Os1fyUk6uz1by0mzc/fE9zx33Efc58Tzx0nlwTjKWzpX0MM199lHW7DptOe/30vPd1ZExtTV
+ucA7uz/JwN9lutVDSEtDU9/SU7tBzdRAvE//1z7GRllMO88+uU1ruW+zwUvrb0zEQ8ZLS05W
+PD08R91Gb0ha11XRRcJrb7TXVrpLQ8df17xnylPvvUXKukmxPlS7Ob9KQ7hD3NnIwTjA1Vuy
+Se++81Tbb0Ln31nhzcfUvEbGPM7Pu1S+tjy4Pjm3O8zIUzbE5yzDTVO9vTu1uz62UD27VU+9
+Z3fMzjm+bze+Osq+RDW+7z68OWG8XF9I2zjRNvdc2U1E2Tu+dz64W+W3UVHKvjXF50S/2TDL
+NzfPVTu7SEfITvPn0E292UvKXbhB29W8zle067/3R1awRb/Jzu9t3DhP1zXAW8rYrtA8x7tL
+Rq9Xd+9ETdlXQkXAPbzZRNpZv03Ax8tLZzrjey5Ga8Ezw1s30/suz7bdSk7bWN9TPbtMVdFl
+2TP/TsvFPLtZQ84z1Ek42s7nSbl7V8V7V8NvxVE/1VxRc2drwW9B2rNKT8y9ytXRw8FUQsTR
+PURbVjrJPj5GXLFzTrW4zTvMvlLr90S7e0hDtU1GssnVukfY50s843dEONFTzOfMt2td6dHl
+PzTjPys8Z0BMXdm2ulm5u+/nuMdO3N1PNkhVLlVELzJNWzHZxeO5uv9Z1ckyMlHtuzfWpbNd
+16++RjFR0TUrPbzvLNuyVDSworJCr6a6RSlCsc0mMb3NMy3Nrq/DrKKnwWnntDYqQ0g4LTy2
+vse5srbeZT03LCcrMy4yPMO5t6ymoaWvsMZWQi8lJS4qLC03S764/7mpvDzrvy1ES7rWRUA/
+rkM0zavDSa6828XBPjE9QzD/bS7jt2/zzz1nuk4rQctBvUMxsq8+K/fKNSguurbPOUarrktG
+rKGpvby0rN83Q984JSUuT8LHT66frdwww709JUzSUCsu662xSu+soMMrQK/BLCvvs7Q+L1u+
+ySoqwbIyMMdKSHdKy7CqX8Ct1d3tyMDNxkwxRTAqLO/dOzZdVUgvM1fRw907z7DBMjrPrLZ3
+26up1FHMvus4OExLMi1E49vnwbOusWHMsbBLNsm2Nf9ZLza8MSjfrDouxq3KY+k7Qcc+LNOv
+38quvju7p61rLTq8qUclOLRnJyzXpK4zK8qtQSc3q7dKLVa4tFkqPbyqazzn685ZOzXDuj0q
+OU0+LkG9r7U6OLGkySkv6aOvMi65o7s8JWmosDMsuKvZKTTDqtErKVOs0yw1rrMvLduotNgy
+WKu2LSnAq9U2xbmxxTAytK82N7VrSrz3LsusVjWyyzHerzguxP8oMbG+NkW03TxHWEy3tz06
+ualON83eNkbAQ/Opu109PDU8S93fwL2/Uyouv0Ex26WrTURAPM29OTGzrkUtyaq+ODk7ratR
+K0yyOSY76biw1SdErdEuSv9YrK5ML82nYzNH2XfB1TTzralBM2e090EsUautPzHXres2LVOz
+sb+7V1e6QicnOGW7q9XjrsApKsvTRW9lyqzLJzi+vT8xN8SnxjQvx9dFNEXNwO/rvttTS0Y2
+PK+2PE/APWWyNzO+sTtcvddvvkMxX8//QUq00tjEucVNLylQwrxOMryo/0W/u+fRby1Lrbkt
+U7zN2zw4uqe0Xc7HSigu3aas3F33z1k2K1+qqMo2O0P7bzUyR7S4v9NDTMi/4+OzxEztZ0Zf
+WzE6uPM0OfutrcwoLbiq2TnTzsQvKDi02TvfwSopy62qwSgiVapWLDG3oao/Jy1Ctq3NRV62
+1WVLPz13rr1HSFlD0cBMN1nX18tGOcymwk/LsbpHMig2wbLWW8Oup9EsLOmvv0UvwK7KLTLN
+sK3IOEXHxzwpK+2svG82c7SuVjlWZ29lzlEtSbrKP8O5QS5vw0/Jsj9IvqzdLiEoxKm0NCw1
+O9y+R1OrqNs2JS/BsbTvWee/w005O0e7rLhISbi8Py0+58vGa1PQyc7XU0BJa//Hs7nd/9XO
+zWtCPkvbx8JnQsSzx03nvcXvYUo3M0xb69H398e6y0Q/W9O5v1hC0Md3REY/X8nhXFFbX+ta
+TsW6vcfGb07XzUxE5+9PRT4/Ozc228BnPUA5U8hfR2W/zFVRP0fIvszvtsT7W+lb48LB3s7B
+RjIvUOtPW9VSO0FvVVzRxe/X0/dG97zLyLjDy72270Y/S1bnymVF/7vC3drJzffdZTQuOV1z
+41tAU7e7Z+HDe8u360dOe0k/Ni9Ou8/rXz1G2cXXw7/OwsRdRFF3vMo8NlVvV048Kk7BVztJ
+RDvdwVNPycztz8PKv722sLi+02FCSffRWEM5QVdGPmfUX088OkvnvP89PcS9zsa72NPByGO8
+tr3tX1tV61h33VvfTzw+WktJzspJPVNYVV1vybu9yHfv/1Rdzru7vca/YTxnzsfezM5jd1dv
+XWPX619j+0NLVDw+TUs/Sz82Nu/rXdLZY07r1OdXTlvbwcPfXFdU68jD1dTW1/vfa09Vz9dR
+Rv9WQDdNzrWsvlJO08zR1crEvLvETjk7S93N41tnSjw5RWfMxVw9U/PjXf/v5d3n0dF7d+vL
+wL/FTUvXymvvUE//u8FDOTxFUffvTU5hb2VRW/9d+9nGysr3R0nKxMDMVV1Y51Vj6+lMRkZd
+829nSU/Kw2dET1Hc3crQ3e33/0pKXPfvysLJ2V0+PDxP0c7ad1dIRWPT08q/u793P0ZKYcDD
+Y1vnX0BHTs64uMtj3s27uLu4ucPN70NCSFN370tPTEBBOj1Zys/nTU9XTUZpv77H70lHZ/vO
+11/vx7/N90xGPEbjy8heQz5EQU1NSWvVY0c7RVPnwsjJy87nXXfDtri6yGdJa+9Tae3r5cnT
+TTk8TNnAw15FW95HUdm/ytRdSV/nT0Rr1c9lPzgyP1H3wbm5ub3DzdTdw8fvVz05Oz09Rk5b
+/29rb1rvyr+3s7i+0Vk/PUzMv9FdTEpLU/PLxcHHyvddSEVWe3dTSkpJRDo/78G/xdNrZVdB
+TNfnY01FRT04Nj/NvL/O4+tvUlnXxru/yVtFPkJt59HFy9/pa1Nvyrq4vcTra1FNWuvNzMvd
+a15PX9LJz8vr/2dPRU9r3tvdb0U8PD9Y187Wd1FNSkpV183Cv7zFaU5c993QXUA7NzY2P1/d
+4d3ra29ZV823try/w8fj49fLxsvnTkA9PVHfysfJ111NPzxDX29fU0xJSlRjzsK8ycrjUUZV
+XWve2eNTRkRGTd7JysnMYUpGRlnRx8nVd11KSUVLb8nAwczT5djCvri7vs7Zd0dEZ+PR019R
+TVFNXtfHzNXn/2NMR0JU993pTEE+Q1Pn92NNT09XTlP/ycPP3F9tR0VJZ9nZ1+NJQUVVXc/F
+zVtISUlh68nCwLy/xdPd48y8x9hvU0dEQ0h3zMrU3/9jTlD3ycrnTkVAQ0RI5cbBxc5lRkRc
+y7/AyntNQD5CVtXIy9P7Xlv/49/EvsvaTT48QUdZZWNZc2NVe8a5tLW+ydNdSEdLb9jvYVxf
+WV9ryr27v8d3SURDTf/zW0tDPD9CTWnPz+Prd0xIS0//09leV0ZAQEvvyMrX3t9v7+fp0cnf
+X1lGQExrzL68wMbFy9Lb08G8xe9OQ0JGV//XzfNNTEVET2PZytfvX1dPU2fevri802tZSk5N
+Tuvj/1xIRUZZa93Jz+9ZSUlOX+/PzOdPRkE/Rk1f0tHb4e//59XEuLe8y+lZSkZJc8/P+09T
+VV37387ExM/hWUlGS1zv4e1bT0xHSU1l499dTUhGVXfXxcDE3Onna/fj2NPX41lTTUtUZ+Xh
+33tjX1133tHFvL/EyczIx8nN095zWVZPTlVfc/NjTkhJSkhNUld3d19dXXPp08nAwc/j82FR
+TExNWWNZU1NQV2Vt99v3V1RZXWd38+vvY1FQT0tTa/fa3+/p1dfLx8PAwMfZ/11aZXf73d/z
+5/NrX2Nfa9vZ921jVFlfaefd52ttb19WTk9TV01MU1Bfd+vTx8jNztXz493l39XfY2lfU0lC
+SE5dZ11n7efn3M+/vb6/ws7/Z1dv721TWU5MTlFSWf979+9tU1FOW//nb/Pj7fP3/+/f5WNf
+T0FESE1X7+fj2/dfWFVb3dXb1dt3aWNhb+/pd3NhWWFjd8/DxsfGyM7R0c/K0eP7b1RNTVzj
+yszn4953YV1d99X3a2taT1FWZ9jQ2+t3a05HSkxf93dfWVNUXWPdz+Xv/15bV1Nj2dLd+2tX
+UEtQZ9HS2c7W397Wy7+7wMjVb1VPT1Xz4/drW05LTVRt0+ldV1JHREZT39HT1917Y19v49nf
+Y1RTSkZKV+HFyc/T71tRUWfRzdfva1RNSU7/3dxnXF5fVl3fxr29wcXM193f18zO52NVTEtL
+XuPM2P93XktFSFPr22tbV1BTUWXVys7vWkxJQT9IY+PpZ2dnY1tn3cvK429ZVVNRV93N0mta
+W1lPTunMwsfN0dXX3s/EvsPO3GNWTEtZ18/Xb2FbUU5P5c/L3W9fUk1JUN3Iyc7nb19YUV7b
+3/tZTktLSlfPw7/L2/djVU9b18zT61hJRkNFXNPV7WdYUFFd38W7u8LO2+tjc9nDx9l3WkxJ
+SU3pzNN3U0dDP0NV69brXlVOTlNf0cPK1ltMRD9AS+3X3W9jU01LXNPJy9n/XlFMTV3azNbj
+Y1VKR1bVwL7AxszX5evQwb7I1/tlTk1f28zM329VS0ZFV9fMzttzVUtFVd7Mxcvd711JRE5n
+9/9WTEU+PEFrz8nM1/NjTk7ry8XFzm9rTD9HWefa2+9eUUlL98e+vsTXbVVNVtXDv8LXd1dL
+SmHn09V3VUc+PUV30c3R5V1dUE/dxsPGzV9TRD5G99rT1d1vVUtMXNnIy873Z0pGTnfX0dj3
+V01ESe/Lv7y+x87X79fMxsPK12dTSUz/0snK2FtLRT5Eb+Pb33ddTUZKX9TFxMbdWkhDSVtj
+Z29UST49Qlfr1c7R+15NT+fbycXG0OtOQ0hf697X22NORkvjz8rHx9VvU0dY59XNytLfZ1Zb
+383NzM53UEJATO/W083db1lKVdHKxsfOWUs9PEln3NnT22NJQkZz09XV13tOQj9O39TT2P9b
+ST1Ia8vGw8XM3W/v0cK9wcbdXklHUtvMy9HhV0g+O0lc6+//XUxBQUrvycTJy9VtTlNp2dnn
+Z0xDPTtD+9bLzd1vU0hQ98bCxcnha09LVdrLyM3nXU5DS9/Iw8LJ62tMSFvhysbH1/9bTVLX
+xcHCyfddTkdX1c7Ky+1fSUVLb87GydNvSTs6PlNz5+tzU0M/RHvOy8vNe1RCQVXj1M/W6VND
+QErvysG+wtJrU1nVxL++xutPRD5K28zIyedYQTw/TfPVztVdRjw+TOvOycbYZ0xLVdjIx8zb
+VUc8PUxv0MnO22NLRkrrysTFzW1LQURd0MnJzON3TUZZ08S/wMrZV0ZHVd7NycznWk5JWM/E
+wcTWa1FGR2nbz87X/09DQ0vny8bK1V9IPjtGZ+/t61ZGQT5E+9TIytHvWktJT9nJycrnWUpH
+Rm3Iw8HF229bVmvKwL/H3WNMREdazcPCxdRnT0hJ/87LzeNVS0JEWdvJxcrnY09OXd/GydX/
+TUE/P0rjzMvaa1FMRk3jx7/E1ftRSUNFc8vL0ndSS0xP98O7vcrfZVVLV9PGyONhU0tITV/M
+v8znV0tFRU73zczfZ1VNSlfvw7zC2W9bSkNCSf/dXE5JREFCS+fHyeddVU5NU/PNv813Z1hP
+VV3jwbzJ1d/v//ffw7zA221XS0dKYdfAzOfnd1lSVePHx91fU0dCQE3byc7/d19XVFfjzch7
+TElDQkdMa8vP92tnVlRb5cS+ym9bVEpHSFfdzHtRUUxLTl3PwsfZ6/dnX2PlysPVa11cV1t3
+28XE3HtnVlFTXuvW5VtTU1hl68y/vcXf721OSUpRaW9RS0hISUxf3c7Q91pbWVxj3c/Jz/9v
+d2/z1czEv8zZ4+Hv49fMycnXX1dRTk9YZd3cZ11dVVll69bP31xRSkhMWm/TzeN773d773N7
+615FQkNESlBd4czV7+vt7+Pf1c3Pa1FOTEtMTlfnbU9NWGfz59nJw83f5+Pe3ePZy8jf8+fr
+5d/f08rX/19fV1dPT2n3Z1FXc/fv7dPIzOdlYVVPTUta92FRTlZdW1VZ7+F3XG//d19d99va
+b2/p3ePr3cvEy9LPz9XvZ2/d41dOVVVPSlRp2913d+//Y15r599fV1thWU9f18rM3dbR32NT
+VlpvVUlKTEtITGPUz9vp3ed3XXPbz9n/e29jUU9d89fj79/n/23318bDytfR6W9de9PKytXU
+1ON3Z3fYz+dvZ1BIQkNNXWdbd+t7e//by8fN42lXSkJDS23vc21vY1tPVu/Y3O9vWU1LTmPd
+0+X3/21rW//Xw7/JzdPjXVVr2djhd15RUUpJd9zT3f9rXldQb93S3fNzW1lPU+XKxMbN4WtV
+SUpdaWtYSkdGQUhezMfN0+13X1hr0MnO2G9WVU9Md8rFydPr3fdd6czBydn/V1FJSVvOxsnW
+92VYTVTZzM3bYUxLQkJW6c/U43dhW1NZ2cnL12lNR0I/TOfT0uddVk5FRV/Z0dxjVUxEQkrn
+z9PnY1lVVmPIuLW6v8vZXU5d3czK2OtnWExLd8nEydn/W0pHTPPPzuVfV05ETN3JwMPS32dI
+PkJc39rhb1VMQkVvzcjI2m9fSkBP28zIzuttV0NFWdPExcrZ51dR98jBxM97T0U9QGXayMfV
+62dGQ03bycbR71dHPkBc0cjJ0etfTkhd1MvJ21FJPTg+TO/Pzt1hTj8+TenNzdJrVEVASuvK
+xMXQ72tQT9nDubm9w89jTlXdy8jI0/dVSEdrzsfGyutZS0NId+HT0u9XST5DT9/Kw8TU/0lC
+Rln/3dx3VExCQ2fby8vS81lDPkpd59nV71RGP0Jj18nGyNXpYVfdwr6+wdhtSD9GX9zNyMnf
+W0M/T9/QysnXZ0tFSfPOx8XD1u1RS2PXzMzN2FlDOTpIb93W0XdORD5K+9PPzd9dSz9I79HJ
+xsrYY0pLd8a8vL3D2FdGTevZ19Xcb1dGRHfPwcPF2PtKQkln0M7P22dJPj9Y1czM0/9TQjs/
+WPvj3/9fTD9KZ9HLzNF7Uz48RG/b3N93VEQ9QGfSysbL191dW8+/urvAy+lMP0Nn08vKz+Nc
+S0NW1cvLzfdVSD9J48zEwMjT51NSb8vGyc3/Tj89QV7Xzs7ZX04+PUlr6+f3V0s+PERb08nF
+zetVSkvnw7y7vMjfWktd2c7O1+1fSD5AVc+/wcLPd0pIT93MzM3bX0Q+P1PXzMvPa1A9OTxI
+Z+/l+1lKRkzrxr+/wdddR0VL683Nz+NWRz5Fd9PFwcfP/09OX8a7vb/J50tBRm/NyMfN62NI
+QU/TysrM32dMQENP1cfGxdHpW1NZ28bEx9VZSz87R1zf2NfdVEM9PUdr5+9vSz86OT9j2dLM
+2HdfVmPLvrm5vMHPa1lnz8vR1XtMRT8+T9XJyc/vWVNMV9jLyc3b/1RKTVvRx8rP71VDPjxK
+/+Pf71lQSkxf1cK/yuN3VElNX9PJzNlhSkRCSO3Iw8nX92ldZ9PDvcHU61FGQENdz8nN32VT
+TU1fzsHE1mtRS0ZM783Gy9z3Y1ZRW9fFy99TR0JARFXVy9NrT0dBP0Zf3+VdS0VBP0RN1sXS
+6WdeZ2fZv7S0vMTL0+v/28vCz19OTElNU//Iwdp7W1JOUVnjysxvWlNTWGPdybzF43daTEVD
+TtnO71pfVFRXY87Axf9WT0pNTF/bxt9XTklHRkhdzszna2938/fQvLnG52dTTExMb8jG529r
+X2Nbb8rC129SSkZHSvfGxNh3Y11jXmvdyeNMRD9BRUdezszvZ1NNTk5P88zVa05LSkpNa93K
+0/tpb//r3cW5ub3Fys/V3tnOzN9ZS0lJTVX308vjY11dXF9r49Hhd1VVVV/73czExuFfWUxJ
+SExf/29XU1Fca2/h0+VjTEtLTlRb8+fvWU5PU1tp7dHKz9vb18/MycbGx9xfUU9TU1nz2+dh
+XV1hb+/hzs3rZ1dXY2//48nI13fv49vj7+vjX0tGSE1bXGHn2f9fYWf/92/t0dNpTlFUXVpb
+d9vvXF7/3NXZzMDAxs7V19Xbb+/Z41dRXGtvXV/r3P9bXWd7X1FY/3deUVr/3Xdr2M3VaV1V
+UUlDSF/va1tdZ/djX/PX52tVYWdfTExdb1tOU1dfW2fjyMLHyMbBw8rOy8bO811dXU5IS237
+Y1ldZ15XXf/Mzt3r7d/rd2vRxsza39fa/11d9/tVSktOTkdHT//ta2dv/11VUf/j71hTUk9L
+TmPj29/Z1NPW19HGwcTK0d7vXVJb8+tvaXdnWlNb693d7/P3d1pMTmX3ZVxdYV1RVuPNzdnh
+b19NSVL/3djd5ef/Xl1v2tHd6/9bS0VETWl7Z15fV05V7c6+vb29wMfP09PIyNnpZ1FGSVPv
+3N3j/1VMSU5n6dXa529dUVVf28jFys7XbVpVXvf3c11ORkJAR1lz2+Pra1RNTVbf1Nnj/1dL
+R0x3187P09jfd/PRxr++xdl3UkdFTnfd3+d3XVBPX9nMzNHfd1xMS1dv5d93Y11VTVHhy8nL
+3WtPQUBK/87N0ePzaVVY69HM1e9hTkA8PU138/tnV0tHTHfJv728v8PL09LIwb/J52VMPz9H
+c9PW3WtQREJFX+PZ22tTTklDVeXCv8DGz99bU2vY1d9zT0Y/PURX39jfc1dNRUNSe9PV72NV
+SEhO787Ex9Hjd2lv1cK8u73O91tOTmvjztd3Y1hOV3fOxcPI1ndUUVt729feY1FOTFjr0cTC
+02NRQTxDTF3p63daT0tP98/Hx9BrTUQ9RFVv3Nl7W01FTv/SxL+/ytXr3s/Hv77E2l1HQ0ZY
+/9/bb1RKP0Jb/97b42lOQkBFZ9fJxMXO32tz18vKz95fRj89TP/f0s/f+09ER1f34+fzVUc8
+OkJj2M/N1N3/W2/Lvbu7vcnR/2P/1crHzNp3TURKa9PLzM/nX0tGUe3Uzt5fXUhATG3Ry87r
+X0o8O0NU9+97WVpJSFrRw8DDz29XQ0FRZ+Pnb1lMRUBH38jAw8jT32Nj28fBwsnrU0g/SHPc
+zM/nZ1JFRlPdzsvN32NOSkzny8fFydPna13jzcrN2l9TRT5KX9XOzttfTUREV+3V1elTRz87
+P1n/2NXh92dZa8q+ubm7wtpjVFzbzMnK129QSUdn28/N011PQUFMY9/X3PNZSUFDZ83Gys9z
+TUA+Rl/f2dxfT0dBTO/Xy8vZbU1AQEr3z83VZ05BP0nvysK+w87pZW/Rwr6/x+lfR0BJXdnM
+zdpfT0ZHZ87Gx8rlXU1JT9nKwsLK3W1OTW/b0Nn/WUo+PkNpzsjIzntPRURO69nd91FJPzxD
+UOfc4/9bTklTd8S7urvC12tZXePIxMreW05HSFrpy8fL1WNLRUZb4dHX71dNSUlWzsLAxddX
+SkNBW9zL0N/3WU1KTefIyM3rTkQ/PEjfz83XZ1RLR03rwr2+wszl/+3Zwry8w91RSkRCVdnO
+zdlbS0VGTvfGwMnWY0tGRU7XxL/G129cVVFrzcvcX0dCPj9IZ83I1OlXSERET+PO1mNNRUNC
+R2vWytpdT0pGSWfRwL7L1mdjX13nw73D2WtTU1NfzsK/z+9rVE1OUu/N0fdUTkpLUe/Ev8bd
+XUlFREvvzcfa/2dUTlhrzsLM51pKQ0FBTt3M1mdaU0tPXdG/usTO1efr3dTEvMffW0xHSEhV
+0857T0dCQUZP/8XE3nNXT1Vde8a8xM3Zd2FdVXfPzmFNRkJESlH3ys9nWUxJS01e1c/lV01H
+R09Z3cjM72NbWmP/1cW7v8/b7213b97Ew8zrbV9j7+PNxsfab2teW1he39HnV1FLTldb78/T
+6VNKRkhMWeXOyd9rY15nc+vTzuNnVk1NTk9d5+NbTE1KTlb/z8fI1dvd2c3PzsfK91lTTU5R
+W//Z1XNOSklLVmf309tjWVtn79nOxL/Iz9//b2tXW+vnV0pLTlFcY2vh61hPSExjX2Xn195b
+U1Vj9//v19PrZ3Pz1czLxb6+zd/3/+//a+vW2nddZ2/j92/r1fdbV11jXFNV9+tlWVtv7/9h
+b+9vTUlLU2Nj79nL0vd3d+97ZWfn5/9YVVpbUUlPX1xJSVNbXFt328bGzMvGxM3a39ndZ1hb
+be/3d+PT1GtjaW9zY2/n1etdY//n83vnzsvZ5f9vYU5JVf/3Y3vv429bWXvf/2VjbWdXUFvp
+329pd/tvV1n/29vv7eHR3ePfy8jP2+f/WUxHVe3d7/fj2+9dWW3b3e//e19PSU1z3ePn3en3
+d1xj6/9bUlVPTk1Qa9vc6//vY1dQUXvZ3+vrY09NSVF36+/7d2dlWW/ax8LDw8TJ2/P31tHV
+3v9va1lSa9vP3elnW09IUF7f5+vvb19QTlvcx8fN2fdjSkVMXeXj3ud3XVVP/9HT2edeTkhH
+S2nf63NZUExDRlnn09HV2+/7d+fHvcDGz/9RR0ln2M7O1+tvVElN59fU1913VUlFT93Ny83Z
+82tXWe/R0O9fT0pCP0ph18/T2ftjTUxX18vP1f9hS0dM787Jy9Pn/1ZV886/v8PK0PdXVvfQ
+zt1tU05HQU3nzcvV72NVSEz/2s7Z/19URkVO28fExs7nVkc/S2/r4/dtaU9LV9/Ix83j91lD
+QUpr+2dXTEc/PUFl1c3V6/9vV1nbxLm5vsbTX0xP3svHydlzXUlFT9zMzNZ3W05AQlnjz87Y
+5f9aVWfOxsnQY0o/OzxN3s3K0O//UUdX183HzO9jSz9EVenMzNT3YUxLZc7Cv8PR53dWWdrO
+ydFnU0g+QE//0snP42dTSEzlzMjJ02lPRUBL4c7LzudrTUFFTF3v5+tfV0xNb83Bvr/K3ldJ
+S1v/7/9tT0pAPUtl2c/N1ftbUVrTwry7vcnhY05l2c7IxtZ3V0hN79fNz9dvU0FASXfc2dnl
+b11ST9vEwcXK81hBOj5O59XV3W9SREdX3dvb42FNPz1Ea9fV0tl7W0xT18nCwcXM315VY9TK
+y9P/U0Q/QVXr2dned11RTV/b0c/Z72lVS05r1szO0edlTEdOd97d72FTTUhM38W/w8nW51VN
+Z9PJy9xvTUA6O0/v19ffX1dLRl/Hv7u+ydPvU1Nz0cfIz9zvVkpZ1cfL03dUSD5BY9zRz+1b
+V0lGV+HIx87rUkU7PEr709PeY1RJR07jzs3XY0xGP0Fc5c/R32NUSEpay767vMbN5W9j2sfD
+x9dXSD89RHvVytD3XU1CRVLVx8nN71tKRk3Xv7u9xtxzSkdX387R52dPRkNN/8rBwsrvVUhF
+U9jJx9D/S0M8PFHv1d1vWE1FRlHNvr7B0XtTTlHjyMTG1G9XUk3vxMDF0WFORD9DV9vKzdxf
+TUdFV87Gxs9fRT47O1DhzM3XaVlNS1nbxcXP41VJQ0JXz8TH02VKR0VL28S8vcXN3fdt98y/
+wtdvS0M+P0/Xys3fV0pHRk3ey8XP719RTVNzw7q6vs13U0pFUt/X71NIQENEU9DDwtNvUUhC
+Rk7TyNHvU0A9PT5SzsjO91ZPTlr3zbq3wM5nT0pMWM7Cx99jTk9OU9m/vsxvUUxGRk3/x8Pb
+d1VPTlf/x73B900/PT1AWMa9x+dpYVtb986+vdllTklJS1rPwMV3TURDSFHpyrvC2etnX2dv
+18DB6VlLRkdMX87Dy2dQS0xOVdzHweFRTUtRWWnMvL/R+1VOTUhL79z3SkVCRk1X3cfA111O
+S09SUufJ1WdMREZITWfHwMrrb2dn++PPvbzPd1lPT1NZ2cfRd1VLTE9Vb8i/zm9fXF9rd9nA
+wNLp/2X39+XJwMlfSEA/QkNR183hW1FPWWN33cbD3GVLSE1NWt3N019OS0xca9bIv8v/Z15n
+Y//fzNVvU01PV1/pzsnQb1JRV2Fv0szMe05JSEtVX9/MytdfV1ddVV3/3eVZVFlb8+PQy8XR
+b11bY3f/59/nZ0pER0xZbd3Oy93r79vRzsvGw833Z11bb3P/2dfnWlNbY2t33c/X92Fjb/vr
+79jK1+tr99vX4evX611HQ0NDRUlXb2VPTlNf+//vzsjcb2Nnd29p99TW91dfb+n/d9PK029c
+YXdzV1V361pPU2Xn523dzdNrWmv33f/r287tVVVl7+1z/8/L3Ptvc3dVSlPv811c/9/a/+3W
+ydT3Z+fdc1lTd/9dSkpPVE1KWefa7+XTy8vZ1srAyd/j63dSSk5v4/9cZ21fSUhd59737eXb
+e1NV68zR2tPQz/Npa9/3VUpHSEY/Q1vv+2Nrb/dhUVPdzdbb293vX1j/0tDb3+Pta1VR3cnI
+0dXZ61JHSGHrd29v82tdW+PJyM3R1dv/WWHpz9nj4/NvXFlf1MnZ5+ttTkVASmn/a+/j72db
+We/LzNvb3/dcUVPn2ev/Z1NKQkFV3tnf1dvd93fWx7/Hz9f/TkA/TO/b3+N3YVBHTf/n3+f3
+ZVFGSFXTyMbGydb/XF3d0+t3XU5APT1M69PY3ftvU0ZLY9XO0NXrX01MWNXJys/db1NMTenC
+vb2/x85rT1Fv3t9nWE1GP0BS08PDy9j/XUxN+9XN3mddVEdJ98zExtLnWUQ8PEv/3+n3bVtR
+TV/MwcHJ33dbS0xe2czP22lTRDs+X9na3GtdU1Fb18G9wM73Y0o9QlPb0M3X+3dSUnfQxsTP
+61pMSEf3y8jFzc7T82/by8PL42NLPzo6TO3My/NVTEdGW8/H1e9lX1dKTdnNyMXd3+1MTFvZ
+v7m7w91jXlvf2dXea05HPj5Lb9rMzdDvTTo6Tffb3f9PRENFZ83CvcHL419OVGdr5WtdW01G
+RErdx8TE229JQ0z3z8vI1mtLQ0BN79nU0WtOQ0NZ2crAwL/I3VFHTWvXy8vR51tMXdvJxMnZ
+Y05MW2vT0N/O0vdcSEbau8XI1UxOQjhET0tXYUlb7UdabdvDwsbdVzxPY8e/bVnjZUtVPT3N
+yr21z9drX2/OzdvJz+9bODc9UePOzVvT1udrRC4qLkqyrK++1uNMZ1tJW0vav6u0t71F3VdC
+PzU1OVJrzsZjX+fKurnM39nj42VGZ2NBY0xOSURANLSlo6VPKh8nSq+fn6Gq50YtJCQrLz69
+u7S998+9v797Ni4xKzpvXbWwrq3H32NI1r/D1vdnP0g9KDlCM/tAMTUvOb+vublGOfdOv6+r
+qbC+SkUuLT9OvLzd1UZPzysuTbypt0ElHyy/p6Gfrba+OicfISnPtbOvvbm3sLe8xTkzKiIl
+KCtFva6qqK+tuGPN28O8yE9jOzlrSfPbR0A4NTp716+tt79vV9rPx7i/0c8yKyslLjUpO6ys
+qLkpHyYwtKu+q662sckvPWNXt9nnx8C1qKq0tV09MyMfHyQvO1NFT8zMvbKxr7DId0E0Li43
+Su/XzdLr1c7Fua+vr7zbzse5ra6uq7HLOTAlKjM0Ql1va+M6KjFbycTeOkL397ywuba3vLvK
+UUppy7W62Uk4MzpP78XI5VM4LCYmLT9Ye//hy8fHyMS+vcnbb1Bf18q3tbzH61FVSkNZ59r7
+ST49PUNj0cC+ztPVXkxHQWPN09tpRkVPd8q5vO1dT01RPjpV3MG/xc7Myb++vsf/92VOPjo+
+U99nPTk8SePOzsTD0eljXV1b3b21t8vnc3fbz8e7vclrQDQyMzZK38jGyMzTSEy5REVZ3ExH
+PkdI3b+4v9VfUl5NPTXD0kRBPUhNd86tvM9lSnvrY8K5zsffUDld7V9GSMldT0hOR1w6OstX
+Y1tnVrC5wqy+wb9A20VKzvNDrrr33TlHudDOs0fbTzpBOz9fW9PaO1Tf12XG1XdIXThvQ0JX
+v66suEJETeXl0Ve2vmk9Ly8tP1PNusqvu82/1b7C2UNMNjRbSjU6LzPKuLOqrKi12zk/NTg+
+S0vdwjxAQyMyRS3HxDbQybztNTpIvKWkuMq6srVvOiUutKaswCofJSw5u8a2oafGQC8rc9/V
+tbCno6WzyT4uLyonJSsqNjhGurW1uMrTy+PDtke+rFPMtEu7vjNfPi7vQS334TTD2tuptl3p
+Y7irt8g5MC4kZdH/PiwrQjkpMC/Koaauu1w+TzIvvLCmqNEz2N9JbSooTkg6LjrPprE7QsjG
+vuM5uKq+ubbPsco8u1QsJCI4sqtBNzX3u0Et86ygo+vHrcOtr8a96yUlLShZqK21UiEfISk9
+rqmjn7NfUzQ0VVTVpaeur0I6STMqLzs+21kvRdK0smE3PETVu6+sqLRTNSMtPzVjuffpPic9
+Wb3IzL24SiwpNLKqqKCpuEEnID08zq65sMpCQTIjNbfGvjQjNVVDyKupn6pNQDg221M1trK4
+v0I9vb5bTzU7X0MvP1q+uMrCvr7Me2fExj7G2b+460TBsUozJifZRnvN0cFILWs9QLmyd6u6
+SVYuQ+3de9NvRy8jNberr98uMTU7P/exoKS0UT0vLSQzU/+9tL2xu2dKw+97XTM7NTIyPWm4
+va+pxazKzLGtv7lIVT05NztVyPM9LjtZvs2zqcTGTyzX0b+trMm4QycqKDmuvLezZV88KCxC
+2bO4xLfDPlQuPnfX2b/X/+NXWNXHxrpcW1Q+PzcyP0pTz8LPwbK2t+/O619IKjEtPDdJN9E+
+wttJ07zLvb3MwtHH3Xfvwm9d4zxLPkXnzs7n48Jady5j0//DT9rN71xXOvdP1uNj2etK48rG
+wWfIT1s//9O0ta69w3s9MTA7Y8Xd4c1GRT1Ib8bhzNRfTURFyL64v+NnXUFHZ8nIyndJRTc7
+Vdq6t75vQzU5PkzczcjpRTw8PEhYW9Pd520+PjxJ0cy9vuO1zL67u7a5ws5fTzszOj08Vk9V
+Wj9DUsLMw85vzu9ZU+nfzcrTx8Ld17++vL3K0VtKPEdJ4+PX2W1va2XPxsHG0Fc+PC84Rltv
+zMvZ0UxKNk4+X07PydnHwbTHw8rt9088QEGws29V/9xZ/+/fzf/f4UlBMTpJTtR7181TST9b
+zW/AykvhTUdERj/Dy8Lpykb3Q01C2Gu9XD8+P8d7y8/LuMvtu97Vyr65tblVXEA6aUZv5Vvn
+QmtZxWXv0dpGv2vD58u0Z8M93Dj7P7r3usrf00dCRkLPY2nCTUw/Umfv41Hd/81PPldPZ/Pb
+S8s7Vjlv78i6wrzr50DnTNTfv9hf60fORlTFUbg/7zxRQP/UT7tpWUdEVzu6293G1sVJ2z7A
+wsfTslXWSVPXOsFOtFW5RWVMyEpGvUPISL49ylPO1+NYuFPN6d61xMZnwzfOTU/hvU7fa0Zc
+NVLrvt1H93dNWDXbTLBPUss+vkFXR8FJvl9bW0HMSV9Jx0TZQ2tXQ7ZPtD57N1NOMck4u8tJ
+WEhnSfdIs1fFT2/fU9w/ulnFv1F3PzrOd1e40XfRLkXfOtfA/8iuTVW6Pce+RLXEV8PrPbrH
+97zIPcY2K807Z8Nb7/+8P//fQq/3VcRIU0g3xP/LzVlZ/1VI2M/Gw7pWRdFHTHvdU3dTZ2NZ
+PjdRacjr0ffN1kBtRP/VPk09PEVBy8HFusnR401PUs/K18xHSkJKW02+v76/TE7ZUstZzcrJ
+ZUhHOEVVZ8e640Vvt7XM2uPJtLXF4ffpz+dcVTo9ODo1Nj/P6dvOb+vF1sy7vca8XfNhSENr
+b2XjWVD3UF/Ku7i/yFNLMzQ5NlBDSkJGRUfr1Lu1uLy91sra38Bt49trRDgtPELvuMdfSTlB
+OVtJU19Fd0JGPz5Kvrqusq+ytru9r8G63TkzJSYiKiw4S0vZybuzq6ukp7CxyW9QQUhJ005A
+ODs/Ql/Kt7CvvN3Ma1U5RsO+a0UtLC41V+vVREQxP0lTR9HNwrq+u83Hu7GqoaSpqa+9y/9L
+OykjIR8fHyAiMTxU08m7wrOusauuusnKY1PfTV3vY/9tZ9PJy7+2sbzL6VVJSjtFSThIQko5
+NzIzQOHLY1tAPkjOvLOtrbm4s6+pqaWqrLe72Vk/NDEuMCgoHx8gICcrNFFQzb+zrKekoZ+i
+qq66ytlr+0tTPzIuMjE3RG3NztDR2/vnztXNxl/VQT85OTU9Rk0+Ozo4SExvyry5ubq3vre5
+t7GyucPT22tlV0E+NC8rKywpLi42PEdHV9fOvL23sq+ytbu2srS0sri8zOf3b2PcX2f7UUJH
+QD5JOT4/Pjw1OzlNTO/ey83LxMG8v7vAuMXdZ0pIS0tLWm9jV1lUX1dvZ97e/1VnWV/Ta8XJ
+61lMS01YTE9fd29349/Z3eXb1NNdae/ZzdPHxr3Jz9Hn72NUSFhPRENHSFtZTuvl/2tlY2Nf
+S05ZWVhMWenb0dXMytvf+9Xj6effy8fV18/Kz8/XzMzfe/fn49Xz5dHdV1lcUVJISU1vTklO
+T2FnWl/33G9hY19eW01Za/dfZ+n382Xd0t3rXf//929z38/V4/vtb1hPSFl3Tk1MT09JR2Pz
+3f/3d/NpTWndys3RzcjT1OfVzcrd891391tW5+HtZ+Vv61RMXWNjWFtYb29WY9/b2+Pf2WtL
+SVB36eXX3ON3d2vSzMzP2dvnX1Jd29vn9/dnXldf4dvc6d/l329v68/V43tdWUhGS1rr42tr
+a1dIR1Fp6+//42NJR01319TX0s7be13t0tXb6+vzX1Ra2c7K09LX7V9VXefZe2dVUUk/QlP/
+9+93b2dPTlnTxr/Cv8HV71nt3dPa3/d7WVhd59DT3+HvWk1JTv/7b2ttXV1SV+nZz93/ZVRB
+P0lV/+vf2d1zb+vMwMLGzNfnXVRj2dHj81lOSURLY29tb1lXWU5Z79PN1etVS0NBPdtzR7Zd
+SsxUxd9KubHv91gw105H1ufQxs9lb+P/z8TU19tXSEDj5dG8d72+XMPrTP9hW8/fO1ZCPthL
+X9VNXGdOSv/3c8a/Z7rAz7nAvthV50lIPUI5P1FG1mdVPUpP77vR3co741Jdurm9wrHnM08q
+MMI3vLs7tzAu60tPOq6/tLAzRD02z3exoq28OiwlK0A0v7dLtt5EvVPEws9pZ9c6zEQ1yELj
+t9+wVFa4SLq4TrnN/y7KtLqsVy0sJTytpqCgyTsqIjXF47i57bS/QkIvNNm1rLjVMSw5Pr6z
+ycvzV0lWV2+9s7KzUylbt7mvSywlHylZr6mpvywqHy1D17yqqrOtPDEvJ1e6sq3OPVM8XcZU
+uuFzx2lNQm9fsK+9Wiv/ubW0MykiJtyto6jDMigiOXu9rdXM3ry93s4+RcjDzcTtPNJBSesy
+YUhnuLPUzr93qqtJPr3Jq8YuKh8o0a6kqksrJyBE48S061NPxMzfZzNPxsK9VDdBPUp3S09K
+R1XazsfFuLy6wkKuvrWuQTwjJtOspqtfJSQjO7+/t1w1QNPKyvcuREpzwONfY07pyczlVVtM
+48rBvbqs0zi5wqmv3dcpO821oK/TMicvP8u7bTw+aePNazpEPmPB381ZQlNAT1djy9dOSjbW
+uLZQu620p287KSrNq6OmzjMkI0f3vb9K6b3EvDwqKDjKsb5OLSg1T7e0xt9IO0RpzrFz/7HC
+qPc1SyzKtq6fuO8yKDZfu7rDS1NnzL3L30dHVU9HOC8yP9vCyrxIODgv07q+w6mzpbw0Lizr
+rKWfs0IoIytItKqxtldN5z/XzrSrtXcvJh8qNme6wLlMMyooUU7RrKSoskMpLT20rKu0NzUt
+LFrItL3NOV5N78Jrv77JyE00MC0wU0/CtPdWKyg+NkPbv6+ouzwmKE6tn5+r0zEsL1S3q6jT
+zUtLyWe2t6620Fc1Mi85RHe9vsE8KjEtPF/Gy6nJv0ErQzDCp62q3UEuK0S+sry467uxwLpv
+z8PNSz8uKi4rP2POustMOy8yOT/Hta7KPCgqLNy4prK50TNbWtumta2247C/v7rJzNlKRjor
+LykpOkHfykVILyxfPEzTOLew37RPO1g+r62ssU1NP++tv7y/17a/wsfJuMfZXT03MC4pNDJF
+3mfeRzI2NGPI2bzTvbS0uFNMQE24sauyzG85b8K4ucxeVcK2r8xIOTNBOzM7LTEvN+Pd91M0
+ODk6RUzr1+PIzq2qrb89PzjBqqqor8jlZ0lJQzrMz8HDZVFAPEpLQWc0R89IzGsvM2s381Y7
+777VzK5luLDTqefHs8nHu7G1uMdGXHtX3chDOF00azo090FQa026O09MYc08TznERUfLSLJT
+sLZetEnzssSrZ6++vbZVuGO8VN86OdssSTYwQDIrSzlVPm0v4Uk6zDjaaz/Qv7G+vtq2Q7pG
+3dDK4fu2V7J7xFW9QL9nZ1Q9wD3GNOc1a0JrRFVfQ8VX0EB720xvvtDHvM7O7enr0mfK07/I
+a8Xe48bV1XNtPd84zj/ZRVb3T2tvS+dPXE3pPctHX0xB/+PV40q3SbZKzEPIyT/LN8tU92Nd
+yG1La+FPwVW23Ne4XcNZwTrHP9BdRlRETUPvWm0740e/b0HjXMxbzMBKuN/FSMJvxMZKy/PV
+UsFDe9HtOXfZa99KsljEzO2/493r0NnATbdD0jbzPt3jPuVEtlq6P70/uTxT60zaQL1PxErn
+OOXlMtM0zlB3R0pZPnc32kvlTNFFxvdVxVddV10z3k3v69NjWsJP88lQxDjBO81X6/vnv1LR
+5c02xDPPXnfJ77blvj67Qs89yfv3Wul3Yds6z+vYe+NXVdNYytzMP89MwMtBvsvKxMg3xzVb
+TXdLXVxpY0nEMME/xsZV6dm8T7dIv03AXUjRP85CwVK8y1LXVGn/XMftS8s9zd86Ous8vknb
+Ot4z7XM6wzPBTNtOO7pGs07SyEvOX+FOve/vSk/ZQWdjYbZvwkfnzVzMXc7Mukm+U0jbbT7r
+1N3lQbVNvNPFb7mwQLvIWLlE0bs4yjRt3DbJO0fANs3LL9dGPb9TXE7jc3e8RrrOQq86ybU6
+s01OtTy6SjxNX0tvbW/P20tLvzO+b0W/Qla+L0q/S17ASl3FVP+63cK9Sc7zOdQzPN49c2NF
+RldCXMdRWu9BY8ZFzVpZwdbVx03n2sXVvm+/xGvUSUvlzd3YysjfxUNC7We+usbjyj1XSEpS
+Rk9F607/0eu21b/Azc7rS+NlZdlFT0VMyHfF1eG728tMTUBXZ+v3QUU4Pz9TR19Nzr7Lv0M8
+OmHEsa+94TQ2MkVOT1VFSOv7w7+5v77T1PNKPjU+Tnfze2dv7829sK+urrrIa0pvQ0VOQUM9
+OD42VWtdys/N0ddXzsC/uL7L31NLb0rMa1/PU+NcTlVV23PRXktRQUZdzrvDwO9QSU3lwb29
+xVM9MTdAU9fZzslcb1VZx8zH09XvTE9ITFrf385rX0E/P0nn1c/OX0tPPlpp3dzO1VdJRElL
+42/f/1tJVUta1dvPxc/KzWfrd9O8v7+/3ttMTv/c2ettSFVDSu3nu72zvb7JY9N31evcW0RC
+Mz88a2H/505GPD40Tl931tnv70v737y4trbAxu1XXF3nzOfEZU9BP05h1VvRTV1HPz9AZ1zP
+a/NEPj4/U9nMzcne72/71b7DwMHZ/0pFQVnv88x371NFV0fp08rL2VpRQU//z8rIzuNcTk9Y
+3czL3edbRkBLV+vGyMjR72dbb83Hxb3L2W9NR2ff38/vV0hBPkr/zsLGw9PfWVXj1cvO03tN
+Qz1FV//K3+9RRT47Q2fv39NZa0nfd8i/w8DnaUJERFPj18/e911KSVxzwcHJd0pHUvPT0V9L
+TEn/y7/Fzv9YS05dU1dN2cS3sri/50w+RFXl6+dJOz4+RlbMx8TGyNLP7e/fy8HIzfdpRUVH
+T3dv61BMQz86SG/ZzMrM3M9z59XHz97Wa1pRTV3PzcrVTUk8OVfjw7u/1+9vT1ffxcDD305D
+Qj9AU9labT85OTs7W8i/vc9jRUI/Ud++sbW4wM9aT09czNX/d1xET1ddxLy8zsnOT11OTM7P
+Y18/OTg/RtjHzNDj2tVj08S/urTMz9FrTc7F68BLM089MUJH4bvKT2dXSkhtU9fR21/HVVVn
+5dfOVkc+ST9S6+PK00hV3Ur/1e3DytpO+2k/U0xjwMvpytte71nhu8C93VxDQU09aczj3mdX
+VW9N29PS11RKT0c+Rk7vbef//1vP7XfAxsbKTc7tUdH/zL7Oz9Fve2NTb8rz81tHQTxKU8m9
+7+N3RU5BWdXNwcnJyu/jTE7NycjOWXdAO0dT18FvVFVDRjxSWO/VTUfzPHdbP7zP0MJFXc9E
+3vff2j9VTl9v69u86+ndREJNQ/fAxczvRl9RW3fz389n3/vRVctjxbjAwMZRVUxO2MnBv+dh
+Y0lrTvNd7/NVZ01fTEpS51tr7+PczOPL29DOTkM+REhIbd3M2FdpUFfX98fAxMnj729T32vJ
+2uPZVltWU+fRv87ca1JTVV3V2d9RPj89Q0JPTsvz5V9zQmdMY97Vy+9RWU7f3ci5zs1YVETZ
+///A1+XdPVhARdFvv8/dXEBDRUfr3rzO2fdYTlRfVcfJysv/ymtvzcS6wdHvS05Qd93RzGtM
+TEhIS1dnY1dJT1dHV0hn5+Nf2W1n2fvZw9TOSk5dS1tr0s3Fd1PjTOlpRtJGY15D1f9rym/N
+2FNX62fY3WnOVeFVR+NPb2Vf729JR0tS62/nxefH0+nOb1nnSN3vVtvr2tFPb2vR29v3zFVN
+VT7Rb1DP98nNXl1d///nZ8Npd+dNzNPNy83B3dRK2mNP3D3a52/PX7/KxdHYyEdVSU7LXNnn
+R2s8O11N81lMSkI/Q0hd0+XN52/GXNe907vve/9HUURzysTH52tDY0JMW2/K9//dRVlRb8rK
+ydx7b1xbb+XT0M5n0/ff3t3Ra8xpT3dMRVd7xtfJ69ldYXdK0NPpXndl2f/zzNfeUUNHd1fv
+d3fXUltRSGtTe8DOudnzd2HjU8vjxtlrW1tr8+PYz29UP0lPd+vPzs/KSttLV/890mf31UNj
+Xz9HREvcWdhZQWU8aV1j++v/c2d7bdHCvL7M9+ll/13329vJWFFvS2dX0+vS6Wd7UWtU5+PM
+ztDbydPr0NXR2tnU3Hfbb//a995lX0g9TUbP1tTXX1dMTEj/3e/PZ9lNZ0VjzdHMzuFOS0Nb
+c83O51NEPz1MVdDHx8rd72lPUv/rz8/O1llMRExf43v/b0tPUVvHv8fG+2djXWfZ18Hn71lN
+W1NVY9Tj2GNIQkJCVtfpzmNeTWP/58u+xr7b3/9LZ03Tyr6//19HR1d33czM129ITFZNz9Zv
+a2tX5U370N/B0f/TW1djd8jF299TWlNJQT9P89vf+0xJP1jhz2dPQlPd19/f2czJymNVSU9c
+/+NfW1VZSkxFSczMw83nZUZdd3fGy+POT1tZRmnv2dbO5fdHW2frwrzFwsvVTlNX7+Xpymtn
+Q0tP3MXf2Fd7XklHPUZv2e97UUlGS9nLw73O2l1OTmft0czN0Vv3TGdj38nO1ftfUE5TXsrP
+119BQE5OWd/SxcXPUUk/XdDHusfXXU8+Pk1SxsrLSkVAO2X3a1/Vd/dTPkRU4cPN33da/15c
+/8/M22VTW09ATP/Evcbba2NbW/vQ0cvNZ2NGVG3rwtXV011RX1fby7y+0dhz90pTY9z30d/3
+Szw+RtnPzednW0tHRe3Zy97XT0dDRm/Rv8bPe91LQU5P19vV309KQUNA38nIwtP3UUdO283Q
+b+ffV05HRHu/wr/Za1tDY+PAucLJWVtOTk1e59PH5d9IP0Bd2MZp329XTDo8X83Hy+ddY1xT
+1MnAw9vZY09DTnfBvcbbb1BFSErXy77Gc0I7SkxYY9XG0dtYU1l717y7vM1rVlpXa/fRy19v
+QklCSPvWw9xDQ0lEXWVj3P/ZTEg/Qk3pzdXIxuNVSz9N58zBe0tPPlFPb9HFyMTrVVtr3b/E
+w99VWU9KQ2/Xw8jO41db7+fUyL3D209GP0XfztPZ5WtbVkZP48nP505NSD5Md9nZ62VfXVlM
+Zcy8vdVeTElh49fBw8bZVEpJS1tvXl/37WtRQUNLY9Tj71lNTV//zMfMvb+/20ld7ce+d09P
+V1lFRFTXx9ZVQ0dPUVtt/+f/41tGT2nXw8Pb7+1OTkNEW8/L2HdTSkhf987Mxcn3X1/fycDA
+zNvL129ORV/YwsPaZ1FPVd7b5+Xr0NdRPT5N99nW2d3lT0xW/83F1c9fSUBCXHfb1uP380tJ
+XVHW19zaW09FT3PZzMzR7+NaTk5DT9vnZ1JMRUNOX9fGzNX/911j/8q8ubrQ2efv7//rzthn
+SUZITlFNY9t7XVNJSUVQ39XO71Xv62ldd9zBx93/UldfaW//5+Xt/2FfX2nZx8v/+1FdaV/7
+79XI/09PRuXP0c7P69pvVVFKW2fbzNtbWUth/3Pr69n3a2ddd+9v2vdTUU9PVVdt69/la1tb
+UWdjb/Pje2P/Z2/3593S2ePl73tz/2/n53tbUk9TX+ff3uvv6+Pv0dnPysXL13Nne+3n6dvf
+7XdpXF1dZ2tvWVlYV2FXY2n/6ef/d2/v39/d3eP3X2NaW2l3/+//b+9v9+dvZ+/p5ettb//n
+/3Nrb3d3729nbf/Zzt3r+93naVtYd+ttW11dZ2tv8+/j82drYV9jZ//j/29nd3N37/Pb3/93
+c3NrY2d77+dz93d7///73+H393trc/f/+/f/7f9tY2N77+/j6/fz9/v3997Z3e///29hZWv7
+9/f7d/97d3d3/3d3a2tjX11n++/t9/9zb3dvd/Pv93tvb21v/+/r93f/////d3f37/f3d3t3
+b3t3+/f/9/f3///38/f7/+/z/3t3//v/e3Nvd3d3d/////f7//97d///9///////////+/f3
+/////////////////y8=
+
+--Where_No_One_Has_Gone_Before--
+--Outermost_Trek
+Content-type: MULTIPART/MIXED;boundary=Where_No_Man_Has_Gone_Before
+
+--Where_No_Man_Has_Gone_Before
+Content-type: image/gif
+Content-transfer-encoding: base64
+Content-description: Kirk/Spock/McCoy
+
+R0lGODdhQAHIAKMAAAAAAP+2bQAAACQAAAAASEgAAAAkSEgkJG0kJJEkAABIkZFIJCRttrZt
+SNptSP+RbSwAAAAAQAHIAAAE/hCISecQt+qtJf9gKA4EpmVXZqasZREHp1ozKlJsig38yv9A
+0uAAGx54hUHSSBzCns3jj3hYKBQHae/n2+m4wVx4PO6Syd5zazuTVdq4D/xGl/uEBOEvT6IS
+YDt8ZzhbaTyAYF0rhD00YkCAMTxZBlkHCgsGlZaWlZt+WQQFfqMwDAEMDpWCamyuhq1BWrFT
+sLS0bjV1OCUhbSq6KF8mK39UnkRFlFTJMZKHMU+FLjoYodRzJ8JAXoFESzDLC+MMDQsN5gwL
+6uuoqJkIluHzkwfu6t93Yc9Btq1efKQU8IcEyKwxMQbegvMrzq4Rb0A03EENAwxNoDhp3MiM
+06o+/mmItXC0rdvIRBdYTVLWaZ25KuNinkM3Dp0DdOUwfdqU5wilBuUM6FtjRMKwMEMRCkgS
+g+CPAgCmRFXjQyHVN9lEVsw2MeIGG4RIIjqwKgsTshyzLFCrdu04tvKOOCRaQ1cuNWiRtZU5
+DsECv35jBjZXs8HNcpnWoj2SR0Anqi2M6uPjKCkXipwcD5G1g1/Cpf4OTB0Ci9hX0w8hWmzY
+lcbinhgrOSY7IcvstGzdGlBchRMiioVsNKq29egeSgYaKGjQ+23veAigR58e+C/fmUCzqytL
+GqPjkCiBi1RiqNuiIQCcbULmyUh5bl8QGkU/iE7W0/edYI3YqLPjEpo4/vaJM6K5V5RrZ+G2
+FyXcjEeDVnaYQRpy5VSoWAJZxJPFKAdAVwB0HU63ll8w1TSThbYRmJ4EMRiA0jBmuCDhF6Lp
+RZaNnZi1mSgF9bgPeGd4cAKEqUUo5JAOGSOAUOyhtaQFBD44HxvqKWiJc0gJh5oLQjaoQ14G
+pIJYhxoVYOaZSZx5wCgcZtihdXC5dU5QZXnnomsPAoNaMAQt5kklFWrnVhW7fXKGT87cwU8Z
+Rfri1Vd2zSDadzWSpUCT82khgGRZImHNZohactsBOJn1BxhsVJRLGjcqwMAp5WxkphK0ApGm
+Emta0qYlJDbXGzo6eQTqd3meZ1KeU3i0jCcu/lV4EzrQQvtsAwOaZa1j6QmQh0IFujLJpjfc
+R2SjGojWS22aZNIJBYmWJ0M1pI2i2a6c2EStJfAZK+O+smjyqgOw8ubTU7XOeiutabK55iga
+cqLYw+ouJtqkFtyJ6pbw1qMJFhznpF120JYTbQANkHwYwOMU6ueEmhFrSLcSHBmuRKe5wa5Q
+k9K25BB6/cninl2V9INRcQ2hoTWkorLTQf3tGweVYcIasGcYLGHVIwlvdmvCGzqs0XZNyeZi
+u3qmGlY9rn6cinYAowLw24bZRPLcJU87pzmbMCEgerZ4cJnMNG/pBuBvGAAAkwJmG8ORlRx+
+waTiKiXVJFFtJHAW/yW/hIkflMGI1SE8u4qKyJuk51PVnqZ+8BIIK4HmwgubmavXOVIas3hE
+rpHFcrCaHIAD7gQg/O/AOzAtOiQbBny054yznBVvKabXDqwTRcGRDJE7AhUsGh4VRpVD6Ti2
+/DLyLa2SnC4JDr1dmQWLOBFGVsO4Q2rRjWKmslbpTUmi0NXmsVVBljAwhKkJTQlaxoq2go1F
+dIZUpxie8fIXso9JK27Hi5YGm1cYaKVsf99aH2mGQziHaEtwKExhCry3ovQgYyq1AcCU/Ca+
+8zRlM8PyySa80Jb2OWMANQEYTNbTGBmVbQC7edU97mUbHgLnarTYGutGUbCD4XBCy0CWqv7m
+sru6AcxCGhRZyda2G+xo8IxoROOYPmGCZ0xJe49ymggKJCD27FAS82ljVLhEwy1Ya2Dpy2Ky
+LjcqmlwpYuZ5Wh+UKDXeOG6EtmIEQapnq9UVrHVMcU8WE0UcRaKFd6/ioEzEmIrgIWY3pGIe
+YfiSRuXFbXMpIp8spnKkEtbBLuA6wYry0iK0LNAaLIpKCXOAqNmEg2W16VZtzlEJR7YFlZlQ
+WRGJMwUF3COU4HuGFi/WnzBYzUdbI5g3cZgjimmJGmRp1k2ix8p2CC+CiFnOJUokkwSMIwGA
+AUxhjicTZVmjM5KxZYTIFzgZ6Kw2BPAEBcaGAQ/IcAQp+KFGWP4WKlbARFmLSRlcQoEXf0mN
+Oe1yyjQs0wpKgtN15OnRqGSTCEUdIFCJ8WFMqoCT4Z0CC+7bi2CoQ50OPoudAsMZ0VxIDId+
+QDIw6xKSZACgFMlmYooTKAcINDHNcOSfZ/knRiUWMUpEUz294NmNgIKK5XinW6gCS9nGsxAr
+dqpWBskLtkaKLgiaQ54PO6RyPFiqprDIYdaJCT7ZFJ0OvkQjGhJKDgRU1AnIbA4lnFSA+EXM
+Fh11mBKJaG2ueBYeEet9DWXLgKqFG6Hw0IUvzQkl5vqg83ClWOMcQ/VM2o995AhnsrRGJUAW
+z0H5kFR5kWETa8GJ6tjzLwgwU3IRgP9P3sxLdviahSFuZx9s+dJFD0UhWm/kjD3WLJeO0qo8
+crimHkBlQgEx3XokRqjRcjdRE4sasLJYmpbWjJpRFOBCGnQbhT7IT17NBHME1l9LmM58wMlV
+AeyZzw+haVeoe528SKOIDoAXcDfSxGStSzjhulChQpVjHId0GUqNkIezpE/OeCkqQmmkWtn0
+F8CUo0AKm/NprdXTjPbLYxZUiQEMIlBMeUNaOnK4S0cI59WUGx184jO5BtvQJOA6K6zSZwfU
+rQOggKwzGT5Uxzn7ky9tWct3xRA96fvOCNNcG5dtcmzu3ciW1RWmMSrnhabJrr50PC66nOGb
+uDiKgOYp5rT+gY2lkLpAl2ohwG8mDDAOfvCZqpYGqxFwXBUo8wbQQichUawCn+buemLIAakS
+dACPLdD6bqhZipAvRaVNC1AYwLFZX2FdrnZg9iDyiEH8g1G/+CsWmMU7GhuYNVoZ1lOk2Gg2
+PRg+UBTnmiPyUCEBTgXd+xO29Iwt0773TyHegMwei2ojrkDVP2tzdyn16omxN9a7e2f88HHd
+1vBnoPAJzmW81Q+2Nk0bn4QeTDcXoE/zp9dwRWnrUippYOcbCZ1AtIWvJ+7rNc6h38NBMDNs
+OMNp3NQF1aNjG/rqVyNVmN8pspzfvY6PcnnYZAFLeCUJjIyJRw2AnkYnZSBcTIj/DmzL8Osc
+MibtM6GUtv7mL7+hawWDgwvkoDac01VAFgnE5n3iKrXN9kU0Ne+rKf98Kmmb6avLmY4ss0YF
+tZpOiXRPhJquvYP5OjX3fOmrA19KDyPZoRHJvEu2y164o8vw1pQSjKZ3Ro1RRy5QGXZcqdc7
+nKWGnd2sjzy8SESznuHLbp1JVqxem2lQLUfWzeE0NpdPTb74DVeS5MCkrobR0MtdBVgxZz0e
+RlJwTKJkHyX8FrMKgplOxOWKb2rxMctypj8ApuQMW8TkyoA5jwRVismVUjgCLDpK1Js7siUn
+XfUEt0Ne88/F59eAf9G/77ukWTuyhZzyFpBiO0X91vYp/muCshQ5tMaPUxfq4LUBGiZ5BnAF
+HjdQyydV5UYfj+MBjcN5YnVHo9JPIONBS1MvmAA97GE7OpYf4GEI4VRSbkV4DqQN31JnlAdV
+uYVJwEEGJlV/9ddo04FPsQNXaiFmmlZtjBeAqUdqO4NbgNI4pgVHhLBqwSR9nbdSESgxzLFX
+I7NB+wNrzQF0wcWDEGVCQ+EFAxGC4pQEIrV+CAJ2WRAr7+NlkJM6BrRwRSd4loQEaJIA9nRc
+CRBpBrMEo2JtjgU4i7eDHeABlUKAw6ZYROgCTIIeefAz3VUlLEZazoMdIkM3wgMs0pNTVgAU
+X1Vw2LML2fN6wXd4mGQmlYYG/51UDx6xJNHkPZGXJ1kjaQbjhlT2iq7zhqwUhwxmdG44G4h2
+O172dLn0fyHAcbrlS0TYgFUFdp/iboOWfYyICWcEPMMjPMUTAGxnOUlDa18FLkYGffbxCICU
+SbOjMC74eycxCnfDAOulGAu0Dd50JvoXiwTTia74YH7hSuiQAA1gj/UYHVaENI2zfMbXi7zI
+ARrmJKL2UE4HaploZMqXZmQRDjD2Yu4DPeZQgcTzOxb5AA/wO6vEEWS1HeI3KQD4XeZTQOjj
+NQ3DNLTVb7jSDq+iDmcndZWnhQakXHM4WHPYcAkjj002jxlpPM+SQfcEj0jwYaKxi7qoi5fn
+iwi5JP8BEm7bJmKc8pT+USWfVSflRI1DNJEQZDyZ44wY+YzQmB3RRSj3AG4++I8PIQYDYTnR
+UyawMILepBY/RxuLIUP0F4sLVpMLpo8YworKZVg34QAZKUGBKS3PUotCOSFehpTZhYcBCDge
+5yQ8Mza7OHRxUJR5d2ZGgAzhcJWfAFSpJEZyE40YOZhgmTw4VS8LMDq+1APjN4gsQEBM0Ja/
+xQ8heDCzwBTqxQmmky1VsRQzCYd76WRwuABOxlzMZU+CCTxfeZqRSDyF+ZP3OFis40SM15j/
+l2XKJyQ+QxuU6XeZRXKY8VeKYyWA1ZYDZiJnNDfM2ZzO+Q5BhjnWxGU4I4j/SwVHfrQUtONb
++AKDPZImC2Ma7iZ+HGabSyGPcMhcxnlG9rigyiOYpvmew/OgPmkTcDidyeVNMWSUx0dxSllC
+2uZtfoVpczEFlJEinakgQCUTNNFKyFM3F+mezrk5o1J7ZjmENzaIW8EU+4kza/GOCASgojJV
+LUQg/YhDsvOGxTkTgemTTuqTpZmREfqeXsSVN4GP9iicdugDybeYXXqdAamDFTOQFzeEPsga
+VeUaOJOicfIra7FKzdOiaEQ3pBkApWmnEuoAiKQRVvCAe1SUW0dxmvifCUQ7DIcr0MWjutZQ
+ZlikpgNxsUOTC3aP05kADmCpT2o8USqlEgqNJPMA/8sjLXB4qQm6pQVRlIyJcXiYfMy3G5fi
+WCWGaoTzOJvSC1RVqCvKoobVSsnjOzEapZ36TrHkER75JID6XWAYLluwhWlxdqyGSYiye2WT
+M86aHlrzhnmZpZZqqYb5pJuKpxIKqtEYN9FyqYahpWklFEa5rl+6i0kJLhpmgDckqPsBFgyJ
+RW16HerZjMrDnr7jjMZjp5t6pxJKhbHRck7SoWk5M8LnRIk4G2o2QMBJK5Smc1tglzH0gI7h
+bDfZZHC4rfd4qZnqpN/6lSZLsBV6PFlKqvdoqlQEqBzKqozJi4yTWgfFQKbhY9oyIQnSK/o6
+kUBbrlDqpBZZtF45sMAqof/38lfNFBSfl0KLKjjAV14hpBmgQk73Jwa440fuJjFn0rHDWY/c
+OrIkC6GcKjxSeqdD66TlaqE3KQyJ46XHF7M0q3yTSWsHGTiGKAWI0pkQUyJplKmgiqdTCpZI
+O7DBilNM6T4D+Vj12kD1FYriOAuqNhv1AJzR9mdSwCUSYw2s2GTGubIUSrJIS7hIu5ziCp1p
+9LFemAIFoAlyK6ZeinyX1yrb8ZpcwYBa1ZnRQU8xMZHY4a2HOzymmbaEK7AYabadCmQlgCjh
+Z5fCAS9yFL20IGVTlj6gMlHCd6pTMATOcW67qStmcpM1+bEfe6UjW7pnu6nLCbDE04x6Op12
+OAX/6rqYc7uudQteSOQv63CANCQHNSebxVV2HRS8mlqynTqYJ4u8pSuhlfA47gNz+LEGtiC5
+gwdI0dW92Ys0Y4koPHolyaMO3bBVn+uxozqqgluayqvAJ6vC0Cmd0NKg5vq25lWGs9uh1ZbD
+WaZhBXiKCytR4nVIviunFAqqiBusnMrCAwuhnYoFawYXh5MnlPspqPJHoXI55sm32Ys5liA/
+ZzQTp7kAWoiLnMCOSmq+aEy6DXy8pyuuFapB08K658FCs9ulR6mwAGCrhXJx79qDwNEYY3EW
+1cFKNnHA3xqs76nADHzIzukA6tMJMIc912tVj9wcELMAAQuF/MmnEZk0/yWDyO/JZVssK0qq
+j9SBxvYIpYccoW38pORKqfiIycI5HKNQxzp8w41prBsDu2QWgJwhwEIsJ7s6tM1ZuKD8jHeq
+woz8jE81aB7hXd27EZQCuIQBvNJimtHIlYaRPNs8Ms54zFRqG6NcxgvGjsl1kw+Wxqoso8QL
+rMlrxM7IVxpUi2Zjy/eLw/jbbXqxQDL7rgZBXAkizL+7T5rKxCYLzqdpvOxLsGDJHNKXF32q
+gtN8Ysmifbv6oMaMyAH7zQhtU+pCikHXIWgCZej8uedLssTbzi38ztpMGA1qRuhKDbbMrvbr
+rpsSG7zsrka1h+ejPjUaDwJtRsR8vB2d0KZ7uP8yei/fIWy8vCkRlSDDwi65EadCi7gsnNDt
+WdSniQWVs1Ubon/lPJx9gaAofMBEjcDFYzcHEIf3tErRMRyMOtP3nMM4HKIZILv5DAac90ck
+ElgDXchmm8xajcxpu9BGPKV1AkoR4zdYhLWL0rUwMY+AbdYqjbZXPdhgqQ5YYF0tQc5kTaH3
+CNbJiamoa9jwzM2HlSGj3aD2FHx8NNN1PNdLkmFOt4cKiyDkEQ2holN/XcQBm9FFrciFrcxT
+yhvXdCMelixQPWV8gwI+8buAfcTGq9WfPDd000+bpYgbYnTjm8oHXKHGSdIfi9GH/d2qaw7P
+dsYfiwAwAtsxG9tWx8P/Q3iUXzqSEsWWAh20T2rZmG24SE3cBHsvexfRRWpVRpAgWKXFu03Q
+h73Awc1PqKQXONHMUrjdOamchgyhXBneJuzdqFvQ4K2XwSdFo70A8xt57p3i6JKMq/qui9a6
+JMmnutq2XimYAtvfhL3I7yyu7nkO76RatLF5E0VVV2UQtxFEgS3cx6zCNIHFbfHJ5bBL8CW+
+Z7y24vqgobvaHu7GzNOXOdk+OTnaoBgHKe7euSR1Awim+GyCLXYW3pvfVD2yN47jDHzUKlzQ
+pglKTiuM1sWzWlAvHsQJ03YlDwquho7IoEoYvWtg7oY87xQAjTPOkfq1zDW6TX5cpPpk6iyY
+/y1qnJLWXB6kj187WP9W5mb+PVVXcfTNi8z9OLJwnvsqtOaN4+58uB9u0OQQhbg3coL+5128
+rzPVXfhqE+yM6Ff6FxvC62rh6M/I1YBkBE726eMtP5F6oaR6ziftk/dU0h/iZB607eObnGPu
+N6Zuzw6oYU9C37lMXT1gLVB9JYM8E0Jd0HWu1cP93yn8AIPygAOoWQRy4IdE1aIkzhHVxcRe
+2IfeznpKg5OmU8wOlpvNtxqhlzXZ4ei8rOHOuh47tBjqYBRPg5bwsX+RreydeuUe2wsVc6ru
+oTq4WDsSVwQA1CzKV+fQpMuZ8Ahd60jdvmY9iQoUEZnxPgE/0KJ0NP+UI1yIZ8jFLrCEwSEa
+Qsga9IzlYF1amAXrjcbL1Y5WYxaULt6kfZjcbnTYYPUJylxj7n8nX98JtfJ2XN/RPBttMpvC
+HLjKM7hL39E6f7qqLI2M249QokkpMptvck/hPYdY1wgbYZhLj5HmoOnX4RdOBhMPjwppagTI
+Xr5oTIfDpxhNjlx0ONoVKpxgrdds4PRxyN5tkPY33Fo72K6t73rPOs17Iadxnu/9rcR67618
+38VmtXHhi8GWjFzT0YmlIdWkss6Gq6fGufyCNem5At1jBGSOgDkjgsrrvZPYsexxIxijTtpi
+y+1QRpAJY74jn/qqz6EXUQI7rdMcOpLRXOH+19FKNs/GOJ/A/83Sgrv7Y5gTfi/7uz3AEHBk
+GdWOIqilZ8gkccbnCc7TaZakSRZ4QahMsG1PWhqeCQzBIVg4NAINBCK0dC2dDcfDOD1BHYFF
+oaBsjUYuF2sJ05UP2oMzUbnZAG94XC4XAOoCQx7ovtvj/Tabi4qcIIEPiYMYGJ5GRxUvr4Ar
+k4BKFMzMkk1OzqjIyJIFg8QGBgaFAwM7oUQJRESz10Haig2LxASEHZJMFZEXFhm0D4xD3LQd
+IwZSAMKWo4OkJcimEJ6WRuWjyaMsLS4SKRVsJscdmARF3UWKm4G6Ofn5NtXVPrs7/L/84w5D
+D4iClEHnyKAXKJP+riysVCLTQ06WOj0g8QmUKFcKAjAYxQqAhIGJiLhydShgrWK2UooM0QuF
+ih3BxMyQMeFVDkTKTh1wlmvKBCdfnjDJ1ogbjwVnClBDqEaNQaQrlEndcMjNPKx08Ny7+iae
+V6/6+ln9ECSWkFhJV6y1cpBHqIUSHVrSdGLi3YqgxCUtdeoeTxwkcwgGifOkIlKDA4o8s8tl
+CkgOXoRIknDUAgaJ+JY6kbmVgF1Ghqnx4vSLZCsjqgQAurRL6V/AromAyhZplkBfs9K7oUef
+bj/5wI4VxBOtELOKF02FkvrtRYYON8m9W50iRYt6pbjC/ANwSMLhB/JVMEoRxxWJSWr+QdPl
+0iQri3Ql3NiAIAP7Enhs/PExyIsrkNgiqNKugSKbSFbjAQQuWhLKhS8amQ0hR45AKqA2dqMn
+rDwUUGAPfuL5Src22Cjmpla4Y6QRhMhpEa67qLNuIouu0267RBRoQAGv9iBEMAQSSYI70TZK
+hRT+1NqMsQJgiOKSyFoQwckrUMCviBOm4mUjUkJaIMAZZiBwhAlvDDA/p14DJhIWnyPHKCNg
+msEOeIDT8A8bDFDgFAUG6IcOOEbsx8TPYClMkUXcBKW50yqyzi4TZizhSTPnWnLHPN45iztF
+FNkFhpewiOGAzk5QZIX1tpAGirngE0qoLy/xoUr8OqMvs1j/oDkizAYjgy01cSi60AnXHIRQ
+r0UpVMHCFRCo06o7scJhT7+62sePq367gKSBwEP0rRSwUxZYRyU1V9xQKKIrS2kkICVT5MIr
+41NtLMTEKGaNYHeCJCp7LLJ0yERtP/hM8QUF+5KDZgX2inXQIgS9mJScydKkTUq4TnOOTW1m
+MNFODUXM8xS/CAA5uEBzO0YxAlD8QIYVI1OITQe+BPa66aSTTiIZJ0ZXnEzyS2qBVHh6oyzB
+hk5UGzL0xfchLBicYakGrnsJlIclNghqWrtMIyH7PgDnCYmRzWsFmbLQhahjzZbsl8i0QTsD
+C04OuTdqOQIR22tDhAeDHxPZFFFl/iRRLVmzcdY5Up551jnx9zYaekdVANgjRU47tU2qFaGG
+OqkEwKEptUoaBeM1iT0PNRoQoKlZpC0aFMFtSimWaoliyaU9QgolDJ09NqKV1gCSmQECZRHl
++U0Dk2IZ6TjCFWXU9GrI7YSuSLPX2RK3Iz+BlKQya8Yqeb9tjnOjqlS9WWmUCBP17H2F+0uJ
+vcfkEvXAdoVXotJ90sY3SSkESwkH1nZHM98NkAZdEV5Y8pQ3VfABZYASzjFWJrjPqIg5bXrT
+I6Z3kemgYHFz+VkooHYl/XzoDcbBoJAm17vnXA9q9vmU+8T0Gulk7HTpUh0mPLOwzUyNgOYo
+WwklVA4C/haQBAfxX4vKQSwi1KCByjMAZnayN+UBYlCBi0UsvHSqzdVGGzV7hDiigL3qQCo7
+Jjxhji7HiuQIKUiFww4jimi/SRCOMrFTAunsUr/eza5VPcxPW+aojt85rG154dhbcKcLLmAD
+KdmAH7IO5BQ0SHGKfAtCHsSXqWwFSpR8Q0YLW3GiTy3HNgXRUlRYVD+7oNETetFdJfQlOVUY
+Jx8sBBIMEGAFYdHvf4SMwQv6FbvZ/QtoERJmRHroGQkVQQJCnAYR3fbBb0CymmmCUDKdmEAo
+zsluWeHDVjz0LsBIaw7HONFZvJgTwi1naJ2SZww4Jz8bzdKScauGKvNjOa7A/8MwckwlqyYV
+ETx6QxhLmEYSeKEaEQJtZiDsoZVY4IJf4gcGDZMdNbxJsy+IiY9js9hHeyeZNG3BHePcjQAI
+oAcPES0PH5ngiHJjC5eWjyTqWJJO45koXtwMhAhkSz3naQrztMIZQfgIkFL50EFW9AHpuChl
++qWE1GFCohOtX0JRkAoW7MIHASiPUkhqIIxND5g1a1hbSUrSV47LGmoAhyY3KSJVkOxD9iBn
+yix4IsUgzT8+9Wm/gPrBNUYoKr686pA0VwTzNINOA0EEOOhZuK3G8nNVbUJjfynI90xEIiBk
+nOo4koYD+MAHF2JPR83xQYfYhwZauEBbp/YCtvjqiP5pUoIWWHo3T5LMHkbTIrbGYgucDC4R
+4PEWYQmzi4LKT2Py9GwHklYSOmGOJDQJRgy9irDlMFSbWH2MCJ3JVcU9kzNj9cEEzpom7VTB
+Y7eY7RaUEQMn8s6RKd1CnTYJljqQ4hRG4is/AFwiXBSDCIHLIBCUCp6eSqCpy8WcPacSt9sw
+FkWLScS7NtMlHAx2u+0Lgc1Au67ISSEdcw3r+3AIEetQSrQn5IvB+IMqs67tKdI1ATbOgIGG
+hQ5AHcwXoxxR0qX4lkTRAsRWNuKXCB7PTtoiBEDaCZIQf4CmRNJSZJf7I/DQqy3MJIMrzkKA
+IKU5wkol7nrS7D5GJHNnzv4M5lzHoM1rlFeEk2jVQT+RMxpXUV9jxTF7ShpXYXFum7PZYIXE
+aGe6esxaBvBTA/NEPOEu11qcJEshBiOQlmVwy5pR2g50VGYsx8tLYm5LVIJ0AAJU2cpn0Ix9
+8qMKeGQ3zCrtFzWayThgfwEdJSZ2r/P8JKgddHF//rP9fmjj+uSHBu57SsRgqI0nxOTRjxZg
+N3UBPAbetZOpPQVf8oCWCf7JJCsDyAVXSFOBEiapSUGnejxCEnt68EIYdMbf/lacb7krB3YA
+wqZ45dCqdnXOdSypVSMZrGQPM37K7kYmUPgC/qz21mNzLT7FKIapGMiV5zBITGaDO9rm+r9i
+Af8oCtXTSf8O5x0mMQxlb4AWb4EnMYn5iHoA6x+vRQ9YvvwyOtGSYM6QxCOBe3PTqeFdvDQB
+pU4Qg47dE0JfSEJ7flYX1IqWlKDxBchDbBAYEGcQmfgOKjsoQm0EiNbfWWDlw2EFKfbaj0Lh
+g8qHOZHOEwHHLulBPbkcfEly2YqmkQspr47FGxwckkOE2gidQqdXEPHmaV6VbMHyRBPsvNBv
+PwzrQWMVsulSo7gwCxWuUK2FyszxJBTrm22SG3OkNIW1F9XkJ/+dBdM9xRsAVMIpMgS0wrJF
+wHlaIIfPKUgEyk6jdThzJBm8HWs3DM2whsKyQITT0PkbObbvza9V+MT+7uuUqi9axg+BiRVO
+X/G4LIQK+HEwqQiN47EncZunKQo5Won22Xk0Vqq6McANq/gtddIHwfAIQwiectKUDBgo7RK4
+euAlpUKnW5oneSmc0UKCUjCKntoBFNCPWzo8B5uwOWoX8Vs0qMOOMCA2SKoYans4SVCIt1gL
+M8Ke+IMoRwg69moEV9CCAfqdPOumt/E8bROG2xDAU0EHAiwxJKgbBFQevxoA4pGwpio+KRQZ
+TbkyxTC8D2gZNhiMncsJTLCwRSC1hKCUpuEGhBGNzlkQgsGCwvMKzZimFZyGhCub0uCcFotB
+HeMf2ugnRog2w+kGdYkLg/AMHOCPKrg1pQj+HSGCwvORDSesmCFyglcDo/uSJ6pLCjYIN+DD
+lgPQkRSBo6P7jgpaGWRQLsLYlDUzgFuCEyckg8mhD3L4lhQoGKaxp4eAxAhqLhDAQ2N7qEVJ
+BzU4pmTkqJagjaVhlk9AxIVAiHwpj6bqDtXCMfeKnSDrNktkgSRLufyrL1ewtQHEHSVIinCb
+whDJhypiRJoriwoYi5TZFg4jtTTklhRZERYhPSxgmk6BLIiKAvTZNqa5JRRIqoBkhf15taZz
+qGsQJmFDG2GIPSG8KtqaNqIIK/uSCjfphkMsOb5wPBsDQtgJx4tMuPSbmnFMpJS8ruUISCgc
+n99buZFJES9qwO//CD5BkMekQcNRKZ982483yZc0XBETkL8w2hi5QUgUKLwI0i6+6Jd22QXc
+kki0gUEZECI+kjQOGJAWmy9OdJNNCJBlOYfhe8cnu5DNcCtDa4l04MqLLLGLYoQuI4lVWgdE
+YYb/okLw07Tlm7kDDIsqExsN2hwNxDeh2w9He0QliR5K+ITNqZ5zKMSHUBF3CbNpSooh0bzb
+m6TTMabYm5rYqwVDUyn28AAiwL21mihe3DfCK4IfTBW3hMK41KZiUoOq88QQWMyAXATJMq6+
+BAsrNCV59LdQDIS/WTB+KSiRRDV62hxoJD10gCw6qhIiUx+l5EDPIYygyxzOfLOFup1P/zwm
+IZoAlZitlny11ayC6zycL2CWockMx0stW0G1cawvA0HGT2k4TbS6FWm7BdEBtZgpuhM3AGs8
+6KkKdgrFLVJNfOPH2qAnVWiaRluNFPDIDHuLSCHIjzwc7oEoz1mSMhu8FMTDzIPImgG9yTBP
+1EwJ9ayvLTDJXzLLuDLLowDIBbCcXDKF1UpMs5LRokBGJAMeGgAcIcwG5iA1aQrO4WyyO/g+
+WEjOdXsWTdHJD0A4D+qgC0GVyUQYBbmvtbgwfQIFyKhBkHwIqMylCVNBaRia8auYySBAz2JJ
+JI0it6pRqOi6MpqZtRuaVBA+gzGFAQ3C2hzSlHLJI9XP/azM3//8u+Fcpzj4AFLwwrrZFpUp
+JRfiR9VgzQBxQ1AF0zdcJQ39n4lxCRjBjorLBHW8w8OThansTMOqGCKFpAE5JjxtGKXQgaoB
+SbhoDsxSiG5YLMRoBgnIRlRZkob5ytDANrp8pK6MgW0i1SpKjKZxUgPry3owgAgUCGQwiclC
+sMW4vGa9wQT51HsRVYQ0SBbxs2nsqnLpuqARuDVVTE8hOvETAz/cyoaCLvZsn62MGXmtEp9p
+v4oYVkc1D5oCAIPRKKnAoPoCMpETBoo9om2zvRpTC814l0j1y6Op1MFoHgmINeSg0gb9yZhh
+lCPgwVB1SgVZ2SJrV1O9mnzyhFV9w+//DMgRI4hRmSOYCYqZMCyYadYXYaNV3UHFmj1HKIOe
++wgfZUudHbv0TMfbuSioUCzGfE5GwEtX6FhJ/ctTUjAMiDWfFIQuPJRpukpFGchfrCjnOB8X
+MaMzegnFoTjPaYB6fdV5IYg8LCa4UQEtRayGiEaG0Kpd5LOJxMUc5Rw2ZUgfqDFpMrMjHbs7
+Ezl2/dMxoNAlWQWvFc6UgRdDEZt/mMdMRToicM64EtY0dds4scEOSlX1MS8Zm5RfvALqk77w
+4MxFeMiqiwTrM5NznZlhdcMdtI2zXDsJ+4uPeDJUW5L0rC/XslwOogL5JEBZ6LC+etJ8WL5W
+GIlbQBqz7UnA/2pIC9OLw60oUIVPc20iPBIX7Bgk+XOFvOVbRPEsROqj52CB7oFPZBle3DsK
+Mjsf1hxQywmxY0UhM2jLQRDSssMtMKiN6l2oNFAHpetcPEEeZ0gMMGPOQniWOkFOCXwqm3mO
+1Y3dHuLfanCRfDJhrXpfZ5K/yEqMgQguoiHRN+VXOFVCL6AS7TCbXSSHRzxLDB0BfozZkQyx
+OrC/05IXIAMcW6gr9HstR9OGTqFgUzpQCw4RmNs+bxWsDzYRBkvRYloB82XhtiXeSWEiuY0q
+us2OmoUCL+PRxFAtBOaLnpVVGWgB+rmoAzJTAT6Txf3fghQj5GAqx/WB+T2JyV1P3f9Mm7Yb
+FVtELeBRuSz22JsbKOBJLn3wYJwyuEUgI2NMPTB1WZAsy9ojnYlRHTd2lC/AMjwwjjxoWM+w
+Y4Lw11mF4wvLLwTihk+lgsZkTIX6uFYNFIFaS5IQ3Sf+3toKMmnoTZ7S3EhWVroJJWlhR1EC
+DpMgBev6sbDFObIozA1zqFTSY5tJEKc5Y31JSmGLGc7TMzWCDnGxj4ITAqOjY6TSIE8B2Hud
+JOsjYwH2oEAW5JjFvaE8h/GZ5yAYMEbsXg5YYCfegLay4p2KTiBdKZ5sKQuuA0oNLMMoC7K4
+Up/kjhqSp+ANVc9hiHXGLEAS0WChHRcsDB7dK7XEj6/T2cb/Ck8LawlgRZsx3RoFAebLtY9G
+WwuuEISmykbzGAKUoAX6yk883dWJFgmcCr5KvpvDo5ubaCcw3pZWFOOCShTzZb8fPlOYyDd4
+RVXVAKCMEZZ3ObdyQ4wnu5LNgNOrIrHQUEJntFo9XsoBrpCArswNQoc9yA1yS4+wVYmlri2H
+XlZj2GYr9i1EWKoNsWaM3lZQnADK+rEBINtvLtl79FmjeiWH6NT0hT9Q8CVF0acOzQv3zQ5U
+yQNKJZnLyEbPSGBpUBWqtMpqQg05nY2ebkML+ZapMEogDGweKOpNtr8d4YtDUOZLvSnxpQUN
+eF4jxQXMKOS+qepsfdUjvYkF+2Iw//5CAoWuMQ4YiUxKw9XON9nhXJ5ZcXEVRrKkUaA0e+CP
+avVRsqLlhqxrdtBDXqjVNeHPTcTbCBPBXjzlJQzUTC3s8ugSukGJj74pCD9ATraBkTAFnKsp
+z9XW4uqNw2sn5mxQTD0OmgNYo2rvaBzWM3pfg0WI3zW9mekx3ZFvSvMTAXuymUaFy7hDziRG
+cVbR3pZTG5oawmq0qWjCghg+4vgAmi6JxPa3CFeZA1we5cwJ+kun5Eme4KDsr8U7n1NN62qZ
+6B4E8R7a/jyslVZVP4MP60s0Mr4IVwGbtaWl/CDZj7BnWVRoG1ZB3Ma8wI3LapLLBYo38eap
+TrRLMY0Bzv91IIaUKYU9iVqgcCuV8otusifGX/okjpQxLi7PVk5CJ7EdWXf44n84urNAOE+0
+SxNKSpulCKUhl4JQPBwk4hlnk/kmAFzHA/wujz0xkgbX2cyrSscyn5gYTbdi6lBEmrOopyUk
+HDYlLjzAgVg2Vn9gpwj/4Cmn9HWy8LlMLbzdyd8YpSyWoGy+6jv9mEtV9g2TVpihWPzqHhdW
+Ach0cV+SUE4lIyNrpFcjgAxomV6HMh/VmxBkjDRDLYcaUyFabNGBajjrRE5ZkXwr0IXtpFQY
+t3MzDmPI9n/Ddimvye3lOAeWrEDgJHrUXrHgyXes1If+5oYWQ1g7dXEOK6r6Eon/ZLZ4Z0t+
+dEIRrsUVxfcxK2epyANcz2ADiOvwqY8q0inM28yOqQogQ/WZR8aYcc9kXUy2s7VVMBo7qBZe
+MtZ+uwBKn3IrrSA8ydIGYYGFBPcMyW6Tr8Jc2okrs3au9oBQC8M3Je90lInZqXnWJkha1rAU
+hXppvWstqRkXRwJS4Gwmp2MYkO35jaIhUQd68SUZ1dKCXhoBtpdaPEhUCwtycwNYpqkx38Lw
+DftraW7osiqQ+/ajKX11k6C586vt1QMJSAUxrAHkrLLjKCXxHNo5pXlkWWF555SWCbVQu3t+
+FVq7NDLgTwpcjzWSLfq1/BCDKZrwgHwVDNxm4cpqijPD/geqWC8qP/ztMvP8nWBAXaITxCb9
+sBcOTnrin00GQ1dy9s+NjParm6N9z8DUpe5oF7oqCFhpobXabW657t0DYsdykCaapghrURYW
+x9eGIQeB54RgNAyQQfIBFwaVqZA8FG4TUiMaZRWq1srEJZW6uhhY1Bu2oAAHs0FxBrABArZA
+MJjP5fRBPK9369/5udVAQUKFhEQCiYVBm9xfXx7cW9skZSUjnBufwIHB0RknzkGjHV7BwMHp
+6eYmqkkLBQuWocuHR0jHQxRKSQlSqEnGlswFx9YGGcHOXI5CAINzg4IRQ4PnkSeTIFPSjdYY
+lVWTFWECYWwsU0vFFrFMw8H7/vviIgB9ZOYfHumdPr7f/z9A4sZhQWTwSZkzdfDoe2TpIcR7
+muKAYvNJlCA7pOSI6riJgBJXFdS96OIgyoYPIQKwbMmyQQB2vXZpQNlh2IwwNSgkG9CTwKkF
+QYqYiIYthTYlp5okCvYFgbkqCMJRrSKoChMx7nDCMGLm6xpGDTUxLHunD5+0cQCxKDfobbkS
+5Z70itSoIVpIEfdiwpSH0zU/nxbeAakq1KlWKM6NlFAoQQ0PGnA9cBnTsrAvkzVYvslFJwcL
+N5KhAnrKAMtnDHodkXatVcirTKR25QLrHNMkVk0puVHI6TBiwuuusccozkRH+xjm1SOwSVtx
+U8tB/yXUC5GJThgXjkKe6TtfvpDypDljx0QqOufptEqv+ECsF7+BS7EFwvLLmDD16xd22WUH
+LJ1EQzslALUDDwOYAYQzqxlABiiepGLKNlc1AQYX1s3lwgQTxKdVZjmFodkFJtQTliXOPcLc
+KHjlY6E4CQwyFzlQHXDQjYCpAYB6eOEFXngReccHJ9UYwFEPJpyn0VKbpIKeCoyV5E0wtVTm
+wH8twSRFf/WdhCV+LgHnwGg5xAGUJ6oV8eBRnDyZHoyDtBNGW/PVRt9nwmRQ4J511XPcQwB5
+54iLaz0XDlRTKVqFjOQg4kR2RijE4nh9BSnkJAAdwMCRdbTykT6lsJeYkv/vwUdSF6HZVIsH
+AYCZ35aXbWmMq/tl2ZJTFyBAmgAHDnDND6q9MxMoq6CyG29zwEflC+RgGKIxwpwU4nAjfOLG
+kX5QIomgzfVxR1WLzmgOBeWcgIhVgMHB3SPaSnIpppFsKkSnym7CIz9yFJDDm+1t49srs3j5
+ZU0fyBorwiNyydnC+OkZypk7AKYmEL6gAQjGgjw3nw3WcVhbfZsZM+20CnelCBvz6JXiPe1y
+S1acVIED36OQYjcVYPRst1almcIrnh9pCIHvYfoaegcqS0GJxDnNFthBMShNdpKs/C2cJ5at
+xgqgyT8haEDFPzjjDLEoHGmKxkln3ERcX4jm8bP+NhXsqqs3eWZyF+aZt8d37qbVN0B+Hc1b
+VVLdIBJ2KFDYSaaU/qPWz4GCR5GDR3ckCpMeJUYqDk+mg6oFkXFAMOn9Wb0f6lvUXXBNUWi5
+BXo9Cb3mAQ1S40uxqoBLh1QFMPsxfXaDECCWCJfstgsmFrfHu+9e0nzf+KhlFuFWbIO72itv
+5JDfkQsZtFH7dEQ0R0Qn7QupIQW85wfRSuY6/FtrGVPWdLNeU5dRGJBMDmALm+bYGCANFRyB
+QvrKmMak0rZUeWNqBltJ1Walpy+cwDvZ6p7zLAWkDW4rOc5JoFW4o55tGaBHPbuE94R0CgHu
+bSFKuoMb+mWqpXHjFW3+S4kGCCSt0/FHS9OaH7SodYRTEEABQdBODoTSoGqgjz3LARd0DDGD
+KhmsfrMKU5fyVoIjfeKC0uvgkFCIwpdlilDqGdTR8GCJTe2Mb49LIctiaEROWA5zGDGUQjiC
+PsX1ZlmFgNrdttK6K8YPdcXzT4iyqKRk+G8mRIQHg9Rgto4owRSkQBu40nEndxjMYVh8yRjI
+MJNOkJJvgeJWBjnYl1R6sFsrqwTYqsE9DcKxEn+pWL3qwAqe9ahUxlIBhVZQEgxAzW1B/CEo
+88cOaMWtREc6AhB21UIUNGiAUJoQP7LJO2AwcE6u+2SYYIe8ohyRjsjRCwbPGcafsdJ787r/
+oxpf9spaHocIC4ih+MxwxhEmhhUo4FwN10GD1nkziBJ8UAO7aRsMEeM1FcNcGtMADWvApj3a
+zCZW1reVyoDTYTKZiVCGogDtfDGVzUOLKukZnk0FoBO6NKVKU7QJIHTqiTvL5vgUk75KCtMb
+OyERnmCXqkQ0BT672AV9cuaJBG0kMUZczT8xmSxMniWTJEmAMakWTi7lb6i8GMpQOvFKd6Gy
+Bz3AFgFSGtNANYNTC1ADSrXFTsnFsJEx7OUuGaI5ZQH0eiHpzW9kAlTk8YKbJzjqYbEHDD1p
+Jzs11SV7SCCEFHAEbcnKl1mUZieF2ep1V/soUecF1iCMdA3tlCcc/o7Ev7SudaVBCJYsf7RO
+8ezlDTn4waTypTk58OEwfPVFSBZDAdBM0QUsUGx2FLu55GrFCHEg5XYIg7QjSKpzltUYdvNl
+KJ52g4FcVeZCk0dBE4x2tIzzG/OOlJZ5rHZ/rYVIDyjWIFSMp53eQ+WvxJpGpDW1RzJcAh9F
+klASHQ4pKbjGEfyJvdDpooF0LBYX8XmXw4Din3zFGFUhm8ZdQMULcwJvceGR2PKWd6SsfZ4m
++LAIs9Zjf/vj33szdYRgCYFBk81LvHym44lop4T2wik8xacv3AVXfcwaEQUsnJgcpKCwwGjK
+qj4ajAQ/+F5M4hlHqPvCF2a3yy265BKu/urhPDVYxMM6AYnT7F61kGdIyOkEI0m52hOvdROz
+41QUnpGd+sIxg7at2F31SgrM+TezFF5CcAUs0MwQzJj0CWq0QpkIKajhmVbu7ZUBwaPkKskO
+172uhqWbaNB5GB4lOvOTS0DjNIN1pGpk83PNamk5w/nFL05GTHvQiadSIw1io+ne7KtWW2KL
+U4bq73pa0VukDYbI6gssJ/EX5ZANElrHGwEDzWlljeTj2MqyxgF5t7uzrAiKIkGHk0Ga6pmw
+mtUK4ME5J6Lis9Laxfxz8a1rmSRo0rgTqzZSI+BLS/gKgFMuGtVZlLRspJlBwUsp8qlSJa3W
+ke5uX7o2yQYU/15eSMoNLQxVQ0SorI5kxNPixqzL+IGVRCPBySowA0zarWYe+EMPK1YZI3Nu
+7zgTAM50DlLPSek/0hYp5kBgIoroKuzn7k9QzOEHoUXIS5IrRinb6A2I7ndtrRePfnq60wlc
+cN543uVbaRwFly+qTW5j+ZIc0Qb22rQISMo8ze9WULz/4UV6zPnePfe734EudGgOxQhD/3UQ
+CE5sHQetH3YpCzyP9WX2bJpzAkFF0wq69cwIh7Dd9LDL7cjLQpXd29tR27izOeHumJFJc4jw
+Rb5iBki2pO7lLaE6gzaPFvMcAH2/t9D7/j2hTywIQtGO/sDagNoOu56ofPwpiGav0v7v0186
+hfv6xlTtz5wseb1o2nwaUwK9sOilbZ+wXjOidtU//YzK0aYlK3IvFCDe9iUGSsvskS20zvnW
+tw76751S0A0eaQHBSJGSJIFVjn2RLcXa5ECOkOkOLyEJRvyTCgxAfAQWpHEfYRXYCYDfUJXB
+sZWey3QbcxSA0aReVaEfyo2eepQKEsjAM6SG/bUavL2ReqXV7s2D//2dzuGb8JHV4OmIERWh
+ApSWNRABEJyW4KgSeeyfX9Tc2r1UqPAIRVAS6i0GtOFEnnxefPiC+IlGCdyAB4VKCRLK0TzR
++lFhWZRdVTEbFMQN7VFMDYZVPEXhM/1J8OXc7/3f/7VXnP/tYRocYRFyCnRpx2sFiX29QX69
+mvMAxBtKoBlNX3swWUVlH5/kBAOpQ4F9XwbKx+GcoSien3StoXaFHPu54cjBw8PAgGosUR2K
+FLyxWR76XrYAH+DxIa35Xh8OXq0RohGOlJ1dAyEywPfU1nNBFOP5A8g90eQhnEX9VthJ3HBQ
+Iyge12E1hjZi4w220vlxzyReVIaZ4QQShirSxNfFwDPEHDTEYljhn1ylTD3kog62187tHB/a
+4+DF2REeADAegUWQkgEKnPZYSq+oFxqtUkBIVxup4chhU0VN4+cVVzedgx+VxEhgI32NIo4x
+DxqaYqi5nvsRRkg8mla8ohKuWg3/4h4+WFqv8I+u6Rzg2Rog6qMv2hoCCo0wyh4nCJAC0FY8
+HgfrxaNcrQiToOBQQh5F6E5krdvnaUFFKpQWGdUNBE7ueZC8iSJITuDk7cNalGQDSYGpCcWv
+BcAB9po7spDvaUK2PBPP5SNN+qAfDuBN1lpODuIRAqRF1INPEiQrldUbGeQqkYUa7heheQTq
+raJEWiMXhpIgGdcYmgiftQuLNGF9YZnakaBIYpaRTckIjJMUqAawKKE7Nt1L2tbuuUE92mPf
+zaX/1aXQAaMBCtB5tQFeEmQUntQJRc9gupITmZ8feMpyWF+qNcWYhcj7cF6SHY5dgKNDSM9l
+uszCOWQa/z5dH2VkFqga8oRmnr0G4aWliQkKtqjXH9YkEPYibMamAPnkEaolRdRDe8bLWGUl
+bz4f4CSHsuDLB92F3qQHHrmCcWbdqtTC/YzBYblISUWneC6odNacCbbfkFGlfHhfPJzAZwJb
+Y4kWeO6PT2QCD9zcPJanD/6dnAFfeiLgehaiEemlJAyicXQQrP0FggqmTAnlt5jQhAWaTSkY
+UYHIwbCK8YgSCuRdKxXpI8JaVhalN4baymVg9hHVmeFWhBiehqblrpBVyrilXOKivdnlifpi
+e/pkio7Ueu3aTyqeFGbHWKCWBikpZaqet+0OJl0OYonSqSHSwXwdhODAGfrD4/9FZ266Ubw5
+KNt5WkZ9olelW4R4QqWRV1qSVtN5qFjVwxu4l4jiI05+KQK2J6fmpXlkwq69aG6yGRRAVRgd
+aZtOzpvCqRuOnvggVRwGFdUcTwgOaUE6Z4oByUlBp6qyzH5RhR9VgHGKWOhhgyR9wqMa392N
+x4fyHq7tISBq6k0C4z9WWoS9AScY0Sld5ilIaWCsU1DWqCs1lXKwoCjsy5KpgJ2GpdR8XdjZ
+qijaaLuUlJ8SJYopZD5QCGBxk6ImFihM1rF6XLKSFlD4wd/x4mq1mB/eY10eYMPKphrQZoKt
+WMoc4Y4V5V9sChN5QsD9pWCSkW+uAvUtB/o5SZ1yXDD/gEG1nBmx8KqbEuoo/M3Ltin0AOYH
+8cZRFZYiKEKEcMIWJcRMDawhwlsyvKRq4iKJZipdSquZBuM/foIkZOtPumnurcIFtAm8DuYD
+bm3VnlP5mV0vKYslSmQorWu2jVJzqpPMspnvzee98mqqUu0f4OyBUWmF9RhgJAS2qKQ7qsGB
+pJV5Iq2JMi2YUqs/EuI1/EmLWiyWtqSdmYFQ3K2BodPHDmp9lpvUGZp5AEUwtZzOmtq79gJr
+yBZ6VW3NFSyRWq5JwW3NOujTORaCQVeFlcFXUI7QckrBti3g9iLPdSnhxmZe/qPQpVcaAAqs
+rYEJuAanIYEujeoyCs5+fhAq/6qi7igBNijq535ucnkkfUrEPyRITzAgb0IOsXVLGTKJRUyp
+2bAJJ4BFb1UpeHZO0fogL5ZnD/7u4AVvP46Up64Yt+za2mpKQA7B7D5ZCqCN6n6syxql9LYh
+Z14h7ohucR6YafXKZM6SoNxgpbgZA6KX91buijAC8s4eGlyaHsBvWsKZT8DYlgruD+JvGsRw
+qPIvYNTXIJYu35wBlQLkRaBA4iQBb43vAuPrfpZfZjXVpyBX3Nms+WptcvAATDIeyN5nzcqT
+FYOjgtQutnpcKw3dwKrBCr9kD5qn78LwDLPJruVMPAKwWvgvGnRRcoGFL2BHJaFu66KW2oZs
+GxZmVf8pD9YusUdaZYIG8JyNRfTMlr3C6PONaz4QiV0kh8DerolBsQ7O4zz2Lr6Z8V3yLw0P
+b6Zka9fuW/ryMFgA1EzISDiYnwKn07hKl1eunbHg7R8TnzfO6wVPBILMIpu+ba/eJ/QyshsB
+5znx2sC+1d+25ky+ZSbDMP/WpUcOol2AaBs4lvuea1PmyPVcXT9cJR4vsIpclB0UrL1cS9Ek
+F+Gxb4pdZr0+DlG+5Jwl8Aadln2qM8wysHLwQUjd7q258+4GYhlr8tIqVew+IgCPh6XdLYsa
+i2yoq+L4VRWobkH+MjA/6Jm0y7XQKUvRizAGG0e2zCKv8Ds3B/l+cBwFaij/l+A2u8HtHtEx
+r6VYxaU/L620yjTxefLL3PCbXYvOzJ3HmWzhKMHNIEqiqbLxuhHM1rKhkSBQqEDMNdZHGGkg
+DyrG8jNMJqSfcdAiRuEYkZXZjYc/rrQAHfOIJq2XAvTgHmIp7Z/uGeMW795Frwu+4Gy6rJxR
+SQVV/tVGytvfSHQtS+fUccTYrm+P8V1fd62fZiWvhLQFQ2BJ75iugtEYIWjQgDULrRZV/6A/
+m7FMJ63C6uquVaoeMk7DsQHUwchshMOpxMJAQAdSrKluHrKCxqhRPsm99WwNv6coavAsNeBl
+r5ZP9Mpek1EKKamukqPNUXZ45txBbmkm07QmL6xL/35HJ7A1D86DiWCauP20UCsKzdQJd1sF
+5VaxbE90uYZcSLQGvcBTWnV0INdcc1IyaXgN//zIYyuiGNHsLE03MSfrJAdu8NWac/8uXb4w
+UQKw0LVYWJiHCwlAVVxdJQ0CfIjDqRDCIFABetD3LaMR8/S1EWfsp/BMHsVrn2LQ0fYKSP9e
+3xHpwOnbhlOEF+vzEaDnAAb4c8f07kHPZ8/aVeJTHQDrT08F20AHazMGd6syceddCRZ2+2Wx
+Jii4bo+iBjdvb/IBile53z1n+UqEXwryrlF2WPPujNP4c9Pki+HYZ8uadmwb+lmWdiOKogD5
+OZhDC5xK9rjZBtNrHlNmUv/2CD5pcJTn9ob7zFpY+Q7EuImOdTrDqMABSt4JpJe7GhQDt98B
++DKPObTem0Hqmqz1zH6eayCc9mozinSwDZFXAHSgw0YK6qgKap5/LbeZRaFyZEU7p/a4sw7k
+sk2S6FjjmopzraCjl5l6OW1W+TIDoM9pajOnp1x+sK49UympOWYyeMy0ueFARbXHeVvQ+YVz
+86BAIFejNOsxBLz9tj48eR9Y9pPjecQQOnRn9h9iOTKm2ADuN1i/24fCGJkfe6WPOdKqNSO8
+NKVq+OhVz8q1+aK8+ZvLeaJkJDa+1Dqb7kmrSDkOGuqa5hn+tp6jKiOINcfrwD1Gqz7y+m5i
+6f//drmwG2AulvV/I/sZyzBs4jtX66WlFYoDU3vvyExUTAc6ePdtUMHhCDOSezB5h1yWAS6t
+QzGVW7A7QzUzWvkfXsOI7ruXwpjG5+q8h6mw9/eLhaiu26WYT+vLzxnNMo5+8VbuhcpUFbzN
+34B0wLnPE/kYYiMm8VlhB72eE/0KY7werPdpwndV49NIUzmKe3zgMvc+VnoUN6FqbmqJrecX
+k/mzqrwZO+xN7jpst5jE6/G0N2nhoDKpI/x01Ih3M4aS2bK3yDp/IljSlOC4nwm5H0iDsnNv
+3zoOxOWL1f7hL+2Al/nzXH0xEjPW83fKF7smUz7lrzyuBTyoIuTp81La/lM7kF8BOKg26MeC
+wu88zqCeiHNz2jrHM/3Au50m61c0FIO0OtmxoPPPrffhree7TDItTNIltapo8D8+TALhiX59
+bLp8XSa/cTwhBAg5aRVDlFF4979AQg8RObEUk5RlD+SFj0PDrFsA8kq/D+PAECoOBIyhQiBI
+lILmYKlc5gbQpkTXk265XamBYACPyeKwGH1Gr9cKt3solCvkdfsd7/6m2eDwvy9QTIGN0MAw
+TSprEUBs5yKnp2eiagPksiBhhCQzpCQT5XOlBSHhYKFkRqbgoLIKZydrYtJiAMgggCFXAYpp
+SScqWFgADKrnp9VIckfKymvLzyx6TU3QevAN/o6OgW4b7xt8yI+rrPza2hCR+o8AwD1ywnFx
+9majSuODpkNE3ySU5F8LVAgGLnjxwpSMGTRcveJBQZIsCgN+NBCSi0GRK1ecODGCwYhHKR0P
+ECJwYNY7Hc6MeBmj5CW7PebOocmmLc4QOOF4zpGWiI2aaufSDRLETqWsRmJoPapVCR8rDvpA
+mBjhj4WnEqNKbe1KKsaMqWIbXpjkTqJESgYWXNQlpAjIKRNCbunYkmMQlCFT+npGDhpMQHsA
+1Tx0c6e2nT0Zc2tSE2ZQQeooG6WGFksPJEj6PuqB4Z4He5c+cdAUyh+ogKO4EiQFI5XYhWTL
+2ugc60KQXLt1geno/9H3FhteRCITfuOv4HF8fpIZWhNxt+jcGvN0I62wYcOV1QVVu0OeUnqU
+7N2zhGkfpxNZt2pdwVpgDBTyYYRYqJAVRYcQKRB4yxsjA2zpzyO6lOgFrysA+EFAKBySJbmX
+YopMuZm0iw7DBbyhrro8lotMu2sQIaS7oNpRCR40lomIPKjOQ++DFE7Y5KpPvHoNx7BSeU22
+VmaboRJaKBLCIt6G0KgYJu7Cy6+RlGikCAlsw+0vjbqYELsQD8MwG+q6yWnDDrkxSSjtQKTJ
+Jsv6eBLFeMJr06nQLJkzKk6s8uQqPEnRs6vWYisNNoV+nC1QGAxaCADQGGigAQB1yQUIvv98
+EakLuUaCZCL+fGGpwislPHM7DLd8Q8Nt6GjrSzHHnIZCLdHpo0QwWMxhDbQwS0tKOTl4ET0Z
+88SKvRRaoyEFYvlZRZVkSVmA2QYWKJKBQ2d49r//MGIgUi7ucgJBSg+8wraVsHgEL047Haew
+59C57o1D3CVRgVK/lG5MVa+zUF1qtIx1HVsrMOMdARwY2IGnajAPxjzTQ0GrGnHUip8YWNGH
+2PVKYLZZRi2CNhdnm1200UcdhXSuSYVxklu7+DsLnuEiHGymfANhV01SMfRS1TrahcZVNN01
+k5Z3anVnYKILngiqDl7kdUaF/6lRz6tGsU+ssWhQlqCMNea40a7+GcX42ZBHfosbjUBaklK5
+RFqbB0aadPBlmKexRmZ1SkKs3jnmrW5nUHuG7qh2cCNmaIGPfmq00PBhWr0ZseLTYUBjvI+F
+BLTeercAtv56c0bHDsDIBoLz1skEn3kIHqWaSO4Ll9D0O8002b1pzC69Weybm8yQ+Zoy8x1x
+TTgJZwotggs2fuDEd2W88aYZhjwgGqdi2KvLNdZ8t5A7d3bRsDkWudoABYxyCQc9skJJ4lI6
+C0LSXZJJQn2bi32yd92Ql8NT8c5dOkNadZV3kAnYuNiQFKNJIHku2pVoEmYnp1HPRk3bxMUs
+573NOUBz2+uc90YGOow4ShxFAMDJgkH/qQIJpmSxgNPq/hINdsxvHYVog+xKsiX84eQN3EAA
+ELpUr/7dy0KBCGD9rsEyWg0NAAND4NESmDg6MU+C0osc9Rr2jwo6S4NZ1OL1HgW+zIHOWkJY
+gAFGKAEkJAh9V9qCuG7lGRQBhjAoBNEZhhirEXEpXqYqSQNsp5PaXWd3Pxmiz0IkOBT1ATPG
+m0ACD7a8BqYHII7bR2qedzEsbs8BW9QigDInMjCCUBdkCoMArERC1o0nYKpjHbqqAbsQkcgm
+XIrDTSzCDQN4STG6Y9XfLoTIRmhmeCr6jpSa+MQFRgWKkKSiVQKiAoJgsgGZ1GTnsFckD3bx
+k9jEpvgo0ogT/qGMdZfKjFPgEc6YBHKQsPqZ/Q5Rkhpm44YKCJlOcqlLF0qGl2Yi41KCKUwU
+TYKRLhoNMh+pTElKLgUWnOZCw9hJD17zk50M4Sij0SQWqo8e81jEKrNDGPm5Kh01tB8etxEg
+2+mOX/l0lTt+oYNApDJOTVTaQHk1UILGyE7LzITlsKjQzWVQk57EHkSzCUZtdpGPTDnj2ozA
+SlOWTDzjcl8LzZHOnxWFZiMlAu3coBsiceiP2eCDuqzas8j0E4nDFKgCCVpTpVWleXfqyrMS
+cMmFYo5RDQ2fFyNa1A/ChTO+GQeCqMqRWZ1FETJR7Fids69XjYqktdRbDhHDHHyq7PQcI4RJ
+I9D6T6S5YnHlWdxbl1ZQuTrzWd5L7V2158lHea5rRNXmF7u4KEL8wAmOKMJmiLFYdP1mPFRq
+Ifzm50qiGIU7Wp0XHXgDr7ACMn4whMxH6UbHdgjCXxVoCGiNOVrSFrRxCRBvXetqve0t6j/W
+9KsuuMZX95LNqGjoLfysG05Y9EezcGyVpwQ5MzWpk6Q/vJYt7xadQFLXspcF4HKw69kLNCS0
+NYhwd8FLmvcsgLwaa5ZPPVfN2VZTc67t6zYdei2LiAEZZ2xHFFj5A4zC4hnevOc5BpnS2BU4
+l7Vry4B9aM+yKjifMosAADs=
+
+--Where_No_Man_Has_Gone_Before
+Content-type: image/gif
+Content-transfer-encoding: base64
+Content-description: Star Trek Next Generation
+
+R0lGODdhQAHIAKcAACQkJNsAAAAAALZJJP+SbbZtJLYAANuSSZJJANttSf+2bZIkAJIAAEkk
+AElJSbZtSZJJJG1JSduSbW0kAElJJLaSbZJtSZJtbdu2krYkAP+2kv/bbdtJJG1tSf9tSf/b
+tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAQAHIAEAI/gABCBxIsKDBgwgTKlzIsKHD
+hxAjSpxIsaLFixgzCgCw0WDHgh8zchRJsqTJkxYdoCSokmHLgS8hqpzZMCbCkANxrtzJs6dP
+mD8HIoAwFABRCBIpQIxQ0CZCm0wNOlWoM2jCBQiwah2AoMCABAXAik1wgGwCAmStqqU4NeLQ
+t0QfFJB7FK5dog3y6t3LN2/BqA9VAhbYFsBUwC8Lr0WIYIDWrAgadx1QIKzZsmXREjiwuTPn
+z55Dg+a8mKTiiRLmqpabWi7rAxJgy35wgLbrt31z90XQgPdQvRDy8tabwHdvookJUph6WKDS
+g6d9WqAc+TFWyQO+FsA8WrRoBQTA/osPT15B9+5kL5Te2bZA67kIaLeWHTu27bkH8PZe4LUy
+WO7egUYZdUflVUBdZcmFGVm1HThgcFwF10BT65FUQVjZVQfZAlz1p10CIF523ncKiFdieeFx
+Rl6AaB1wAQYVrnSfa/TN1uBr9TU4VmYqjucjij/+SMCQGBYwVGXaHaAkaAlA0ORbAzSAlEPR
+xQiABRX8pySSXjkGmWT+MchieUsOqcBZn5XllQURtGkBBWz2p+JmIJpHwHYZVSnRaRFQ4Jp7
+q6mW2qC1FfqnXCAquVmQQDbKaJBpkhXlcLlRCpdeT12kp0lYVnBBZJQpWRxkA3rFGXgHLAAA
+nAMQMOCG/tmFtWBZCmRHGZJnhcbgAUWiaaSVgRlmUHzxuXdjoIeqpuWijgLJWZEZVqboZZdt
+l+KQunqlW17jEQDsQpuKVRkCCXTYX3X9bbfdiGVlZ92X/1G7JJp2rshZteV+Ve6mbIErLFAN
+DQbXgXdFCpaH4oJoJoqahSepAhhEjEEEFlR8QK1fPbrikAaHZWBxoRaAEXMm2VQYArx6WRxW
+91aXXaJobnZAhlx5eR2sAyh6b1lgyVwZx7OCWGRS3z5kZAM5j6ZzZwzf6+NnGkfdrJ2f/RcW
+kmiB5yphRfekQGOaYWxdrNSZRa6+CeecqwKyitejiAuOdXDXVAIMUdLMaiwz/gEa8K1A391e
+y3HUcyqNr9DlFuBX3VwzTndCORPQ2Lta9fyszbB2RXnC2yF+tViq2p0pQn1KVHqeFFkG4MXX
+Rkqrw1J3yzqjumJWJIYI+Ps4Qs9N1DtBA7DN1cyOlftlrBgiDzN3JVLdYqItiqx7RfwCq/p5
+A2SwwATZJ8CB9wR4EL73ZJ2pJOHOi8ozktPyKv23g/0rEMWoJ4dQAZtRByp2/Hvof5JmkRlY
+9AUgEG3kgCNBoAITyMAFbiQABmBABCfIgN0dhFwBDGCPGqWBE+mNY88LEc3WtTCOXa0AoVtP
+/KSykBUqRCm/awkCJ7C9rNRsMlpaGtSudTAj7UhE/thSn8gc2MAiGiAAFSSiEou4xKrE6EOf
++5y8TqihrZQqZqAhUwm3lAAaInAtNFnI70g3EMQQpIlMbCAF4xUabHVQa+dxW4uyAkElHjGN
+aMwjHvfoRCuRK4o8w9f1PAMpXrkvO3pcokjGOL2C9C6RfIykAy2gno6wyQIA24gBNhlBBkjQ
+AAswAAMlqEQJBuCUqEylKle5yiMe8ZScnOARGaBKV0IQlRUsSWMguUCUdW51q9ui+3YpyWJ+
+MYEFWGBEFMNLYzqzmXkMZS03CcFXutKT1IQlK1/JylZ2c5qfrKY2bcnNWT4EmgDwyhJzJ4Dt
+ASBm1FpUvlBoTMmg855F/rzJM/fZRFkGYIGx5OQtU7lJbNbyoNwk6Ctp6cpsDhShqExoKyUI
+pwtUgGIHUCg1rSlQbRL0lrnMCT/xCLIFqAQD3GOnhxCgRHri86UjPSZMl0jLb2bTodXc6EMD
+ulGdTrSmNn0oLG0p1G06VKIEhYACNkCBClBAAktd6gEMQLGCCrSTQP3oUEU505h6VaYjEUhX
+x/pVjoDUk9vDJlK9CctwdjKW27zlVQ/60XB2k6jVzKpR7frNvt51kwtw605zCkoP1WxcHeJK
+Y2hGs8kYyVbY4RCYbAXZc93whpalYpcUuFjHTCCsIxXrM8+1KgBM0JjS5KkEV0vBCW5TrwxF
+/qIn1YrTac6VnEUdp18V+tfZ5jaofZ1gWlkbW07uz0iTAdPkHsu/mn2lsTXjEGOJctguNbd/
+7rIVf7rS2cnZkLuXpQwkRdtEyzq2VAM4XWkjSMPZMoCG8A2sVSMq1/ri9a5ylSZD3yvf+9I3
+oPYNMH49+l8I6jWnat0tfSdKQxye5SzwjSAKEysW6lQGudDFzoAuK5nIdIk6L6MMAB5gUYtG
+4AF+AkB4OxQZ8Gbou/9jLmbRy1I8ftiwkiWbYdH7q4a8l6Gd3J6Q3TuBhgYYt0iu5gTIcieQ
+nZAyPI0yXJNM5QIjOYKBxeZshQxfLUtZp/715nZABTLHTobDaL4u/maV+9iwxMeiC3jPAyJw
+0Q3U5royphlW0EthPZPqf2RLbH+sGGMe85h/C+mjaC3I6EaXpnqLMVlC2oKTSoMEtPpUtKM3
+/ZMq2a8nm/p045wz6tJoetGcTnUjKwSB1dQoR8miy6Xq4hAXslAhkmYKpPVZoQ1lTnm/1CCL
+2KVqKwlqNsZ6j5wBJZ/V3GUvv5GQlKB9HDcT5dbQmYioeXfpoHDX1zkeULxmxa5hm5tJFSg2
+SybdkFbTiEb1QRazVaOfbeVlAg3Ad2/2DW2C1aUut3lAtCm1FwqNetffWkC+qni8W8FsTHIs
+d+GYFkTR8Ew9YPwJBG5ULPm82lD4sQ8g/oU9talV7cL15ousH9CkAfz7N5g6CcItkqXtgJjh
+kvVfFNm4w/S1MXb2eh4G0q3ugzAlAoIy1qGOFe/5pGbcMSs50J9mHp5dykj/dnOFsR5zXKu6
+Ap4SV2fGtT/HXLojDnBABBojpoOdbzMYUpNnuVwZOHYGRMGz+QHYve5+EcTW2VaIwO8D66TP
+SN46m7rUm0XxSCnO3nnRj9dlIr/FNBl/WJlAYyzTYsocbLHjVtHMwG1Dtc2LRwvazBt7JKoK
+Z6dWLjHNSSjQ6rjQ+0DOBgvLWT7sQl7Ndl5x1fsGQoE+sb1zJQThzh7PlztNZu9cmzlP9KSu
+428Pf6Pij+cV/tU06yqWcl5ZnlkUVqLzzApJbHuf9OWnp979LioxpFBhhIOAOw0aABdQAAUu
+sJE25Z9cdKJ4AtgonhE3V9MirVIZKcR+Bqdud1IqC8Ah4OEVMAZFO+YY0VV2KCMmrUNIZdJ6
+UTR8y1R5CaFrLYQSL8Fki+c8jtJzK9iCFrdz41J1m5E7jzaCJhE8roJz+zMzF4ZYZvZ9fxZF
+AnR+CWODMlch0SFsswMkXMIfUnQ+PaN45pZBB0MdE7I7oZZtCPc5VTQu2qdznCMuthM0meE5
+ldEQjNR3pPYQgFcavxd1FkcWXpEBB5AB2ZOHeLiHeUgv2EI7TLI+wSc0A7BpTrFC/s0xOoH3
+Tu2COdfhfDl2HZ4nJmoSNOWCemZRiG/Yhn8HausxSDsjL6J4Ki+YPraTL3jDJD0WI4Bnggex
+HHwnOgqheTnXPxcWSKlHiN1lKh1zhs+TKmX1QP/kVbtDQExoHuMBOPaCiafYYdL1M01odydH
+TxlnRuv3iqXGSzW0WNKCRXkTdDwTIHZyGQjAAIl0SsEYWoyWZuGVLuGHfDu0MFQzHmW2Rz3h
+ig6xhh6Rjs1kVQywHZCCLSnCFdsTQQ2ERPyYQKZUTgaAQLMEUcq0FhF5QP6hOqOIRVoDjrZC
+VkS0gEshEZvEkZtURPHFVenoXihpSg6UYMGlYGG2U62F/ldJlkqnlhDl9Vl5NAA4uS4XKY9K
+MhkjxR/NJJSRJBEcmZBEhJKD9VFrtVVI9FB8tVYy6VdHRAE25U8cVV8EBks2GYzFEVYOkG65
+o1KsM3abpypoNCBImZCwuJZNRFdDRVgaNWW/9VZNWWC8lVB36ZRbSVAIMCgUMHRDFwEvcmIF
+QFgN9ZAGFlch6ZYMRIEDUF6F2FXk5ZhHqUempGALZmA39UnWZFtyVVsTtZdamVcuGUG/tU1W
+uVuf9F45ZWWEJQAe1hVIkligAlnad1w3x2He51gOxBXOVJmVCVbNNJzFhIT4hF/kZABddpc1
+9VZI5E/KSZe61UqoOWARlZl7/iWamnmVoOSZUSZXLeZmzlVSGKhmONRd0QVZL0ZZ5oVm+qNw
+lPGM0SJdkhGG7LRPxrlO1mVdBDFbEwCgBdmaDJBaoQlXLllftBRbQFagrNVR1glmRwZcm1lX
+CIVV3Imd3yRBw9VanskhyfNcjjFh2seOItpczxiEf+R9F2ABagZi+hMtZyaJGlZoLpVG+6li
+/tN5hpZYC+FJXZaS8TUBg0WanslXqhRYBhagEKpb4VllUgmby6mdcuVlVMZJDypl9QWPc+Rc
+4acdN7RY6MmeyoWBtvJYGWNnYHdRBQB2SHcAFbBUFIimzhWm9Mlc/3OFRrKjHNGbgJZzhmaj
+GQIR/ljFX8SFoUQloS9ZSxMQFm4jaLHSM3QIFl92WxNKZWg1pP0VZb5VnXhJoah0m4KGouw4
+ptx4ppuXONmBNM3VAC6aWKWagdx1LlnxWO+CXTqmP+gCgbGyP8k1n4uVY5IhUvuIahZRk0ZZ
+dMq6rN9iacYKWsh6EFURrcy6rNd4g9XKa9m6rTj4ONumiGwIrmpBrdxarqtWrfoYe47jd2fU
+lVRhrvC6GHYBb/Imay9XElXiQptoEPu6acdzXl4Rr+zKEzgCG8lmsLFGbwQnbXqBhG64iPOj
+HKVmdBCRrjFCOV66cz2JHgLbrROrEEfhajkysoCibMcicC/3cl23F/Fz/q3fyoARQa4Y4V2P
+MVlhEjTnlrMdKxII2yAm63Q+a7ByBnOQF3nCEXm4BxYMexcNuK6y2GjO+GsOF2zBJHE5myY7
+CxHJMigh92pK1yB1UbS7wRcMCxxjS7ZvMX1Nu7YmEQHa5Wu2CHU6dLV0GylZmxDzBm8nSx/M
+RrR8UbPt2FhfgrIDIxq3EbIpyxs1oW2siCQ8OFk8B3GlCELKVzsJQHSeqBYvwbVxUSwjayPK
+5iDvmHplaXIVJzTHMRz/Bk8HMK8OknK7RjLXShEXsnDgBqnipxlWu7uE1IGu06Kosx4m85eA
+0mqfK3Lz4XHvsSNZNICywzaJA5mNkRd0iLKi/qKyiott4to1dFabNNtw7/hD7QOO5rZ4ljt0
+I8MQFrtuCBcfhoKwX6u3TacsQBSNkxuQk8pdEOByUTh+bqYtC5CFmdsTWGI1vQm3Mya3HCiN
+d9e7JaKMZ7IrslMW6GsVLfEcp0EyB4EfseazXdts97E8G+S8JjeOwyQhqjsZR+Fy4AVzsaiu
+FVLAJwQtKPRdXDGGFrk0KaIwfLMgSNInfZJ4lXsvdcIrXVEyLzyCR1cQfyJnINfE9WoZTEPC
+sbNBAsR8kOcbASzAMLwnbJsnETB0tSst9td5C3B0qfpgaHFDq7J2E6YvHBMiHIMBB1B8GxGY
+BfA0aJE4dAh9sbhr/vg4eUm8KoN3bAl7yFI8wqX4LCFSLbwCFnDEwGRBcJViIIX4xSnxtBAb
+EWCRFVz2MkbsYdqxGfzhpjDEFB8gcLcKjb94FhY5xYH4ZGShAZcsEtsWP9ZIgoXxEijLwVGc
+LMuiyHo8yZglgwkjx84yh8WRG0zGFd5irpeXODQkbi5zRefTMYN2u8bYIqKyKGmiRSC4cLCn
+yeFKgh/bOLPLxEdir8VCXTiHPDwijysCj5jRKhXgAB0wEh3wAFmSAODRKgEZywP0MWrjH+Rc
+bPU3QGCKPxRYq6NclqyXL5JFerRphP58LyxSLf0BFh7piYCXiA2Rui/HM+5yQajCIEJi/r+P
+IsymG4piQZt7iiGa+xNGDIVC5s9Gch2m4od2AoSOiDM8Yj59HBbKZ4YYUi5dHCwvARj9Ws4Q
+ISFJU2Gi0qY6iD9y5M9NQ8VS9ywGWC6a4c8i5mgItwB7TJAI0Kjh8WK0OW4+fauO6Hm4Az2s
+wyt398homMSxW0Yl+LARMSlvF3HfKI16zNJUfB6cs9N3ciYd6xRloSETwDHuUoEI48qb1zmJ
+s4Fjt0XDtj7iIoKCfM5OrSkXAYAqncwj7EGCM8WTW3HLlzY+VDXwCmkXUy5uLRlCQ8O3AiDh
+gT/3ci5OBnVW08mL+9nY2hDxLHX+IXqfEYHc80v2stJUozPz/hIvVyghFsy4bDi7z0EnPKgV
+M/MfXEGLoMIlvwQ1eNeNZaguAzTAGYHBGSHCDgxCk0pyVQzdhrMk0DIqnObeoG3OsuhCdOIV
+1oFC4Tep0OVwUITDds05nciv7peE/Z0Q6/sQP9R4dW0Wb3S/f0iAgXjbCF7LzFo9klZaABAV
+N9y6X9JigCo3U2s5AdLVdJgp/Qp/6bweocfaD8iHe3jWC8ABA/A937OHpJiRAV2Et7PHASvW
+eL22+SoQC3AmyIUVnfM1pee45qIwnOF5ytwqy9fRKNHUm+wTD9cdT7ajhwXJ5ts6cYN3WO4z
+1KhuYG5G5WwT6/JilGPDGoJDMrin/hepJkT0tDKrrLKCJqSr2s57Ma4zfhnCw9GNdw5bIYPR
+loDB3/k4EQrH0GmWLmMoKf3Jema4KwaUkKfkSeNlQVSLkeXxN6oOOH+zMTKjPrc9OaDsk6/e
+ZMC5GLZWfAOBSQ7A3xM+Ee1Ei+0Yheoj0F8Beo5cv/X8VaN+lHTzR+LLJBeOia6X55FKcVkN
+QlfDaOsXRnotQ0wkZOE2FuI4Oy6txt44JNSyT6d0mfbIaDH2vYB7XomSRfKcNcuoFfkELN7u
+SCQurg5Q6gf0Y6BX3vFtit2xjOVSZGh0RO6OThZU2Tis4Ol+70wmPIgUnCUB5l7HHA/PQLHE
+APXOLKvn/oFuoySOIVsOZJAfLwAsr5+7c8yfbkhRh4woYkiaA/OKxO//0vI4Cu4OGVAir0WR
+QaSknkDDyJEQxU3g+ZpMCVKYthOQFIfcQW6GzkMbmZD7WWOdpkBcVpCVGl8ztZCh9GUKhEls
+kkavVERICpct6U0MaVWKCZu4hBEOlEzOxE6NDEw8DTTc5VW5SUSvkk6TiaMYsUkBb5mmBfaK
+n0f2lWB3SZqe+vRD5VafiVu4RK5Tr5N7FBkI9GBmaO6xgpbFBIHEWEzclmmN75jFVZpwf/mj
+HpWMWaFuf5WC5aS0zxAw5UVFVIhYsRFYlIv3l0hZsfrj5RQ+31WxpVAs2ZeL/iqXuHRUs9+X
+q0RRbPJXPuVTrl9N+0hWAs5AhchO5xOBr+JAm1cqyZ+OmWT8o8RRiflfPFWX0kn7/mVLmQmh
+UVpOd8UASoViYIcBAHGhAgULBzYYCGBAYcKEChEyRPgwwMSIACwKAIBRY0aOGz12BOlxAAIE
+GhcgGBmyY4EBKj9+vOhS5suZNWnGpJnTpsuJFCVK9BlxYc+GDiEyJEqRAYOhSYlGTIqQQU+g
+TotStZoQgQQFGyhUqGDhwoYCESJYiKDUoVCFU51CVahT7s6PLAcUQImAZYG5fen+vQkYJE7B
+crNancq2J1OjVdfCRWrVANPDFCNfvSx5cuasCilE/kDQ1LJUhwwWTJAcuS3HBQkKsCwJckHN
+vCNta0Q5MrZf3oV99wbue8LppZsrRxXqNgBj0VEhLqz6dHnzt0+pv518/fhxhwsYY4XscKRd
+va/zwl6AV/3d2ncHjK+dG+97kvPdk7d9Pi8ABK4HbCQssOAGDImlCAj8qLHnTJtgKc4WQ8rB
+ttY6jC3ouNssOqyIoqzC7DTcrrLSvnusqO7cq808+FaM7z3y5suLJNvYg28v+vKbEb/X3rtP
+LxTdW+Ck2RD8zcaUAvyLOeaM6myi4qIbqrillvRQwdEeZDJE5bBjassQQaRKIe+eLO25BXhM
+r70dT2pRTfNg1E2v8848/m9HHFtkDyXYfMTxTCHtU4/Ivlykb6/9LJpyyrkmaA6yL4tbDino
+lvKOws4oDO+o6n5yTqlNJ5MwRMuOm4q4JSk8E7YXSZJTzzbHUzG++vTLLwEaX5UPpTRllJNH
+Xe8EtoDfCuyxvvtutSjZqUxLdLjhupzQuUwdBQ+i03xKiIEGqbxUwWkr3LBaL5N7UtQHnzot
+XWcTnUzGuwpwzTX0ZgXWRzhrO7PQG3ecT6ALLGgAWH0HSJNFBOgk1EYYjQRO4bvYTNi98dxL
+tmJEtTU1UXW9e67Eb8F8aIG2lhszyysfu/Db6q482UHpQNUOucSmQxmlBG4+4GZ4BzigPzpZ
+/pKXT13xw/NeX2vctwGD/MVAILMKOCBW+/Kbj05jjTzv2HjvG3KlG48leGKHx2bPYrMvZjDj
+Zq8d6mNNFxsK5wQomzLlo1B2W0NOv2V2wUSlBPVUvO92Dt4ECCBUPh6NRBjXehfmcSTXNlBA
+ArAikOC1CCrAvKvKGz/6RjYNNfQ9eW8k6Wd9ITf0YLGPJZvoZCk4u+K0Ge3Qwm2D8pbwaV9z
+LWcCDiCg+Jzh3RnvwfOuFqq/iXNW3eymbIjKTLd7KIE/bz3aXcdflfjW0y2goAEEHijgAQQg
+IKgB87sXWLd8AaXvYKLvXC//kYS0n//09j9Y/1oXn9oVMDGB+xu0/qZ0LUn5zoEJGUDwDqAA
+AijgePGK1fByZivlYcqBa1nKcFCmpLtNr3lfSg3CdhaxOBnteznq0elciD+JvRBiqAvg/2LH
+HoVlbYc/7FUQj2Q2mHCkgEY8YhKVuEQmNtGJT4RiFKV4NgdM0Yq1q+IVtXhE2hERibXDSEy0
+GMYtltGMZ0RjGtWIxShmMVlVdKMWI5CsOQKgixYjIxkJs0Y+9tGPfwRkIOMYyALCkZCHRGQi
+FblIKA5SiY7sI4C+iEQ9TrGSjMRkJjMJyYpBkpOF1CQULznJUYbSlKfc5BLrOMVPFvCOTVzl
+2UqJR1mi0pa3BGQrLaZLi3VxkLwEAC9f/hnFWYKRlkcsJi6V2UZb/vKUnoQiBeI4y2Qu05qa
+BOYWz5e+AkjgAJlDXzjPtz4EHNGZa8zmG9NYzWu20538gQA3H/DNbh4gnPIkJ/ogQE7z/dGT
+w3zlJ6sYgWF68Z0HRWgTBxDObzbUm+Dk5j4lSs59us+iDdgnKltZ0GSxE5P+yxHyMOiahB40
+nvqkaEonKtGIqvSiL3WfFh3JUTqa7TOf2SUylZnDANZppCJNwAGIN9ThlbSM6VxiPuW51Hs2
+NX32ZKpKEfDSYFaMpr00myGzykUmIjWR/nOd4uIVVLIKj6hnHV5ajSpIs21zng91aEPvCU56
+zlOcK10fTDE6/lW9TtScOX1kEmPZztTxNGtjzVliD4dWxqb1rGvtYxy7+VRvvhWqc61rXfEp
+0b1C4KWeNZ/7QLvXBPSnAHiFgDot4sbBsrGRt9SVYcVmOMUmtrG3dWxuIbvGyi4Vrr+NK10h
+ylm9FteioC2AaKea1wbAqz8UFWwwA6raAtbxqh8lmGzlQ9uyLja3uAXv8BKw2yUC06l2nSxw
+H8pNcFLUuO8VLXyJ6z6pbnW1ZnSkR89ogRjxz03BK2t4BfzdtAZVkV5dY1PlWs/M9RZ9Dk4f
+P+E74Yoad77FTS15mRgB+WnXdNwVnncHPGINJqAC5EUwUyl7WRZDVH0VLu7GJsDT/gVYlE3x
+xOs9WYpavlb0bK0FpTV9RWMcRRBqQSUx8SpIVOMRGK1BvYA/Tali9D44uJOd52ht7CrEkpUA
+hwvqrVCrYtsOj8fEpa5rAXvLC9gFrI0DnpfDW0E6U9DOdR5xzjin4SRupaUKBm6Vv8nXvXCX
+wMa785cVvSPzjfmeQiVAVCca3wZU9b5qtmYFnJtdkG63zHMuXvEsSMFDkxq3UEYwIVNdMR2v
+j73BtadmIYpBxaa1zre+c6KHGjQYxyexUBsAaoPN13420Y3QtO8fOawisPrwyJAWapKdnNsm
+PzlqaFHmqi1yWVdbdr0Qtuw9u2zrXOPa3HbeNY3eU1wE/kRNfQQgZ0uommxtK/ICQeN0TwlF
+aznbetoC1rWT9Qxk8vb2pLB+66vhir5xK7ncDz83niNtK7zEc2yHg1q7pRrTID+x3lq8N42a
+vW+gRq22CbDg8ND9b3ILeIP/SuLHEZleyjI41gxduGblBm2I9zzi6I4axddnZAmK17n77Fmj
+K51TrR7UAoYjD42vJlKqy43lZ/V5uR2b2ApgYIsy32I8G0zzyrbYofVk+LOHmvWf/5x4QT3t
+0FmKQR7jxaKWNjbek03IzUEdh/4tMtCe3e8lO9nU5daAAiBe4AOAhZlSJHjezTZHK8/1qZgt
+e8LRV+sJtp3tPv/yBpm73JUG/nvY+xwJaKlY0r4juYb5viHwBq/BtdNZ0UrGvcrFe4C7fCYC
+e+l8yqFWAQyq8bpWdKQ8b658u54d3GoXteel//AJCjXMFaevRZfb6LxMeulqBvverRgBDABV
+hoVVYc5yHW1qKz7Sule0zlxDULSYBS2JvYuSIw3pSLfkj760mMizI0xTPhfDssuDKgMEMN37
+vOmzvcIbquMZgNCysJQSLb1LswyUovCzmAq4N6CKNPTIjxyyI4IyQT0xKw3imQlSPOSxEZ6L
+OA16jS+zizMSwMc7otNCwAdjrx48L8MhNwdsQK07q+OZsB7jq0sjLwwAC5FKNxyhnc8oJ4so
+qIGK/iDX0IvDqSC4CyoFgBqzoB0MIL8DoID32Lodkb80Eqia2iIdFK7Lujwq46agYkAhxLW3
+Uyzq+y7egy8LxEG2aqKuu7fWMLL+859C4z9Ssw3x8q/XUD+4U7IHqAAFMMGvKICUi0CdgZcN
+GiJM+0MNrB1pwsCKwbGa+8EfLDo7TDku1JmgIyt5gRdEezK7gy9TCyxt40AlIgBn8Y/yMMSp
+QzIEIKiM+IDPIL4JgD3T6S7hATbZiz5Se8WdicXXSKTp6qqzmSjNk0NxA0KHazujQxoMyq1+
+qz2Vy0RbMa7O+zI+swicOZh0KQ9bCRvTyZmRCLUBMEEA+IALgD3XiaBM/jw5s1o/xnPBCPJC
+j5uiwdqoJLqjuDsvt4qwn4I2x1q8nNmueNEL3tuL0jqYkQo+CCTIBNCrAkAclDiA+wKmXDyj
+C3qNIJkAwcsu05I9oNE/oWpJqSuPgESy0FssUrM9LlSRSFuk68oimhoklhKnF4swCFgoaZSX
+T3tGPCs66+O9lrCADgCADrAAi/m0gTxHlrgonhFLauSjpvMj+SOUl5SY7XHEnpS4m2y2tuQ8
+gVQADQg1Czq5oNEZUDq28jKnOUqnT4qllHqxfNKTn5JIt1y/mxG1MGNBC7jKrITM4rmLnqvJ
+AsNI9zmdqCGA2Vgz5EukAfBCeTQPnXmPk3AP/iRjv5qURz8hsitULMakSibDxF8TuqjxP1YC
+ReuqmMi7QawSRXiSKmiTF1spyQGoIJ7xsjtkQAQYTRSsINgQqgYssNBDngm8Gcm5mY5DKAW4
+mfTYNJvMrtSsvUSLlX6cn51kRcXDxHGMzfzjvSkERU8MLCW0CMBcvay6quWawCtEw9lAC4JQ
+API5SQraROY8t9sawiYrKyfUyC+7i/n0o758rYpBgC9LlQQYDue0ldFRzjpMOdRBz/o4Mp4M
+OpIESTB7T+CRz8+8oqJESauyTyU6PkLDOJawIwwQQwrogA4gKAxwj9C7wwUl0iIESqixSQi9
+xoOClyBpjYOZgMXa/p/xkDNMLK0RFJJ8y0KVU7xXtEk6BMhohI2vi6YKzUBHstECg5O7QDJd
+I56AU0XpO8MNckHrM1DPtCazdFGLUACeiZ7hQDksNBYAC7E+yaEO+5/YZInW6DyTC9MTZQmV
+ZMgZlak1S64IYjlzC0JTA9E49UoZvJmtgRcKMjl3WrULhcvTkNKw8ZGREh19Y5N3HA4Pnbbi
+WSw6ddVKFabfRKMsIgktHELatD1hJVLmJCr5Eyn4NJzxMqU1XNKKiZpFRYAZi068CCvBe0Hi
+mR9ptBlI28T2ZCxIA8qtqU8qdKJUk1SLWEHqLM9baz9VbKxoxCC7QLkt7KRUEr9rjKNI/ouR
+zrNWP4kYR4wa8LRN0zFH1wi18BrXnTkjpALAe6XQLUpEz3u7O7PL9Us5eN3DaITU/sQZYcEl
+DtRTAEDRf9U3PiHJ11BZWAzVNqXBwym3DVLNhS0tJeLVe8XZUQzAPZU8i/lHb7TMqNE6LUys
+LVzQcTTRVuRWjmNHKzIe9Asr2yjNHhE8TmRT87Af92DGnUOeaM3PXtVZ0ESqD23XclvUrh0e
+GRlVhGVXcD3DjxU6z3qniM3ZfGVDAOAZBYitkfOPoOEVfUvNkqPKZaw6kJ28mANFdMVATjq+
+JNrEnQTan0RYnXnXYhW42ZSh08JTU13cTyTZ4UFUnuragQXc/ra0Vaz9ny4zOaozXDOlz1uC
+TciNvnGUG+/sVMsEvWpTVJj0zHRtVnMNzleaI+vbW566QnDMUnuRxsKlNaiputysqo9zgKsK
+zgltojhLwWFlPDokVTuMyk+lyoIMGt89I1E8J+401xw8UdHNWuCpIRtJzMRctHlVJQltosY9
+opuFonaDvurburIigItFnAwYAAIOsO+NQSMNuncJuhbFJPLF2euKgPS4GagFWHt5l61BzDC1
+zpHaTkrFO968W7ut1Lrl2SNi0yplrAJmYQJ2YRZ2yY25xCXDXTxESyRzrnfS3/u0KYKr3jna
+mXY7zXkRq+y0FUIVsY2Nl7dj1h+z/sb0TSQINi2BPNawcVKCSQAOIAAt9gAC6GIPmM7I1UNb
+tc0dAVUHPjC/5OFz5SIgc076OIniBCAMnpEFPJ43Lbxo5MkmPpuCmqMdTqj820njobgXzoAs
+RmSanT4Fhju8ANMv69BmQlzxG1mcuq9YuqMbhdoJSh34cJeptJWJtD7jtGHtOdw1BgBAPir0
+NaP+kF08FNH2ANOjDdNQNdhjlceQ7Tj9hSQBRM5XBSvei0va+lLaAp5aU000Bk0QXuU+EuTt
+hUrvgtfaHGVgk5zLFKq7mIDfveTexCoZHWEpQk4stOI46TAAij3Rkce8IFSqbAmdqBh4pqRJ
+Sqidm81N/tXYY8XVfWHACeLCzY3iAiKf2lklXUo1lPDOK3Uc2Akf9RjkpA1IYREUl4gIebam
+wYNKXKvLunS72hzX06TH2fVnssLRRWqtVXpYvvw6VsHIwOva7hJVdZtOMu6u27PIid6IbBkW
+2D2577KzjU68xONUfRbTTj5eb6xO4nEugFYkS05lKE7cE0am0zjZifFgzutg1Nm5Vnzl48Fp
+J8Fpes6k2O3Kj1w5OdvdvXURkhRWilRqednmQ/LjELapDUxpWnKWqI3H2QMzc7QgDIIV1X27
+7U0AQZmIYfGLU3JEqtNJzmzQXjSsjfQ3d71MnTmNRCpoMg1MJMlrODvq23Nr/rwsTowebP8t
+KwJZjbCeC9iSvdoqyHkBvNRM4upbuY+EZJccjAfuXBMmaGOaCwZxF+xFK8qG5BJlrFVMLAYQ
+jABA7OZeJqmDmKKRbVH+XkhDN47smiLioz8GwIGCaidSyKd2gNUOCaqmk6mkyApyv//16epT
+LJGhi8NW7Z3GpflhaNmTX6xz13pNaF35C1WrqfM1aN/+b46YElih7jpTvCIkVjw8GAOoCeae
+77DGJfxmbJp9Zep7U7v4D5lAEv3STcGsrsWd8JAoDTsOtQCutmlzPdzhCeUu8fm2pSy88Lwk
+PA3POBztjQ9v2K8d2RFvbtT+kFnW3uG2IIJZgIR4/onlCHKZgBQn4QmK8HBbukIk1snJTjTx
+cpHsFgweFwBldt0x6qgmT+yOmB6HmGH322jQhVIm94jJIHOQmBmo4J3vUA5OmQhJ+iO/aA0Q
+W0bZZU557HAKlwmmntSv/eYCivEud4AoC6Ms0ogPGZG6WRaQkPCZ8BJzeZkwoZLcIRwoj6TC
+kEu0JjyRDsH3WPQA4QuxDkXIi+c4L3HymyMB2MrxzghIN/DJWACNSIyXiBnsSQ06pxnoeIjr
+6QkQTyLc2ObgKKdQxeobN56emQ+cbt/7yKNVDwxWDts3Mom4+A0x6YuNmZnbERmI+JsRgo6M
+YAgBkG85/3UU0hvVwJSm/jB25rainBiAZd8JZMQInuHrMhtWaS8nAvkfutCTd66JT3QAb49x
+A9B3ZzGARQcOhoCWUUkNYHcMeVeQO//0w5aiudBmmzgJ1oDoKi08veDy3jgJAvHy3EbIMZd4
+WP8NjpGIOaeZcFkZ7sCSMJkQqcAWYc9zVq+l3mAJucj3jNj1aFtGb6Q4hPeLY5H5Mv/uXYr5
+qneJUMl4K6F4egcXTReR0hANu7F4CFciIsGLnOCLg8kIaKutCeKXlJ8JlIh6mC9w6sJ1q5/v
+bdkSYQd6aWkIy/gOlukUr2+SnreOzAAK5Vb0IOdIjaiiChCAtJ+NBRDlxPqf3Yh7rpn7POry
+/sXH+zg/oLqRjk2/FCfpmGrJ+UcxCyz5EJ5XDedQfFoK60W9dUf/jw5HzibTIbj/cqj/fM7f
+8aHffDL3IOt4cpx/G+vweddHfot3/rdgALPgHM0I/Ksw9op49RL372DiR5TAiCvdC97X8eEv
+8+Ag8N/PCOXJFgvpHeN/977/+cNvfsTvDAaAgAOIAH/hHAvoimIPfEsBiAAGBAYgWFCgAQAK
+AQhg6LAhxIcSI1J8mAABgoYAFiAYsKDAgAILJjoMWfEkSZQqU5JUuBLlQgcvWc6cycDgQQM6
+EQ4sOFAnUII9D/o0wGAn0aRCkwa9WZToUKg4ox5kAOHAgwMRKmDg/lqhQgQLWHUe7Rk0wM2B
+TnNWpem25sMBGOGafNvSLlyYeCUujJB370qEbKPuPJsTaNCfSgUyKLtYKlW1ZpkuVuxzsdUD
+EhRs7RqhQIQLYCFI/lkY6FqpPgGzrggSAYACCWTLRgCywN+ZLlvz1i2zN+vKQy3zLH4ZsWni
+UBvjVKp4uOnHwoc/vrqB84UIFK4/wG4huWCyqQcjZAA8t8OOHUMOOO92N3q98QFQ+D1/pvSq
+BpH6dBwZsVD8OcdcfpdJNV5+hlEFlQEIaAeBAhtoRpYBFHxnVHkLILiUafe556F8H74HHHwi
+QlSgUmsZVlZhTAGYmHRGLUgZdM0lyN+M/kQxEAGKZKHFwARLHXeYURANMEACIXU0UkMf2QZb
+kwgscORtT8p1pZVyNWCiQyWWWNGXXIYIokMTNCZgj2alhZaM1DH4nI0HHlUgcWqhyKabd954
+EwMa4sghYnIVsF6Sg1I55XoigYQlSBcdieWVR2Y56G1KQgrliBGFqRGJwHW0aW+NAakhgXeu
+5diPADoHaI5soRrjcqZiaKCeMWJopn9T8SSopQgkGalIGFFqZaIFDHupoSFN+WilhqoHrKTG
+/toeXAt1Gt9toOa2QGJGjfqqrf3tdBS4NBYXmYtrPvYfiuTWWiuG5CL3XGEdXeTsr4nq++iz
+SD56Kb+SPkkp/nsA21awr0pWeiRDUt62AMQLkZlblQNQIOKacP70LZo6EiTqjyy2Ch66zs1a
+ma4b6shcq+/mxOeZ88475QKOqmesXOoNTCyVxvK8pLOLSkplr8gqaduvH1mas6IeOTmAh9qS
+dHDFUr8Uc8vHeasyUltjPfKL5po8clJzoixqxy4zdROuMtO75GyMvoYRRgYTHLDO/R5rG84G
+8+qRsYN2tGjeDnP06EVisoQw4RYrJFBr3Moor8hpF3Rm2R8DOXmL08E45IyGqfbyuiFbrnZ/
+AbSNWMY6CTs4pEjS7azRe1+57N/P9v0s74JipLRHVBIrpZS5C8rkXlYzVDHViS6U/qoBcE3Q
+osZFISevx2Zt3Ke8orv4OYcoZ33ZhmrFjHqMbJNaOWKI8tv4kbj3PnDReSdLe8L4Ayzw0ev5
+nrP75peziZWkSsJiT9W0AwCvAYkBDZnAAiD4QG7ZCE7v4tN+zLe60yEnfBYUm/jQIr7JuSxr
+6psei9zVIF+9ZlEfCaD+bla/271vaPkbWgTsFj8XJut/HJlfs2TnHgT2L4iEK4C1AICWCDYm
+ghFsk7c0Ni+eoKl6BojgYBooMlvByIqwohXoEGQakL3LhAaAIBrXJyNESSskfXPSegToxhi+
+TnhXwpmhRBOBBgBRX3X8V++IaERlsWRTRqQb0RZGNQBE/sBa3mJin9KIQh+Rp3oJ4hAFyecn
+8EGGeh6MEwiPUyqtkfFOyiEPBJEkyTMxoHh8mw0s39i4QM6ShgobAAQGV6iOiEaPfERWouSH
+pbpRjYj7ew1djPjDRBZMkElciFEgKKompjFXoPvkKVWDvWieL3RU9GI22QLGpZiNlJgzJY6A
+wrtY3iYBB5iAz4oVOEAOToD7utmS3qeeJDVAM72swAWy84ACDHR/kfLhARXJKPbMplKLu1/B
+3CdIZiHQNox8pkKiCclpjkqagKIc56yZH8mZb4vUSY4nvbiqcT4nk4dBWzjH+LUpBgBJB5DN
+AQ7gzkLxKnA+u10teVdLgR3s/ki5dKMEMHAAgAIUAwEFgAQ0Q1D//WyYieqVEWnTUARGhHHG
+ZONEB2kojCaxMdXkaCSxeNJPVmZDHr0i1rA5RXCCEJwyMmf35jVTmX3SVwkgwAEIkCQZVipu
++PSbHBlVVF8dQEJfAQtovhIBzTR2A0c9LM/ACtGe8XShR8RqRYNXTLEK8lNkTaKZHilNtD4x
+PNgM3+XO+BGdKiCwBzjSXFkVNpXC9rU6wVV5mohWmLotpSiTzWANW6XlHg6xfhuawmCnAAlc
+QAENkMBUH3CB7ihAAQ9oQOwUC0Bm8mqXt3Ol8JjXrN/1NKxhfdZpn/mtAF3xrE7ZrW8/ZhTA
+8rcA/jq9KSxxW1y+0rVO4SPXt6jpRDSiBood/GCCZkM82QFLqPZE7HL/RRtjgaUB3iVoASwQ
+Aex+V382NNQPFzbHJRXPvYpc2DJdLGPaxVe+Cw7pZM4yYLmS5SMKAKw7qeROwP5XqwPuYn6l
+KKoF35h1dz2XJ9F5EEQ5bIaIvLA9MzyoABeAAntsAB/BDOY93tO5Kb6qR+oJQOJddZCifY1c
+OCJnkRCtX0+Db42TyBiO4pijBrKkkKpyxtnklABalU2vhpxTYx05yfTt6CYbTC8KXXN0wmGK
+K3ep0Lo5N8vGRDTzMGzlS6HXhmmWM6eDSN5lHq5SBwTcsuBcZTrjOc8LSrkvm4YLaTSy6Zp0
+JQqSdgrYH8fyVwX4sWBve2QpUojPblvVGHMc6NJB23edtSF6sfzcheFR2xSNIaTkHDvSHpJo
+0CK3jI33rIAAADs=
+
+--Where_No_Man_Has_Gone_Before
+Content-Type: APPLICATION/X-BE2;version=12
+
+\begindata{text, 269602880}
+\textdsversion{12}
+\template{messages}
+
+Where no man has gone before...
+
+Click on the "death star" icon to start the animation:
+
+\begindata{fad,270222644}
+$N icon12
+$C 30
+$T 30
+$L andy12
+$P 0,0,20000,256
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$A 29,24 -1,76
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$S 216,264
+"Fire!"
+$V 260,260 417,242
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 321,186 250,158
+$V 319,193 265,172
+$V 316,199 290,187
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 321,186 187,122
+$V 319,193 215,141
+$V 316,199 232,155
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 241,145 179,117
+$V 265,163 179,117
+$V 316,199 180,127
+$V 169,120 163,101
+$V 184,111 188,89
+$V 163,101 172,115
+$V 188,89 190,110
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,108 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,108
+$V 198,108 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 246,158 180,127
+$V 169,120 163,82
+$V 185,109 204,80
+$V 163,82 172,115
+$V 204,80 190,110
+$V 175,133 173,162
+$V 173,162 181,132
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,108 141,158
+$V 141,158 149,161
+$V 141,158 130,154
+$V 130,154 137,171
+$V 137,171 149,161
+$V 130,154 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,108
+$V 198,108 199,118
+$V 199,118 149,161
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 164,61
+$V 185,109 219,66
+$V 164,61 172,115
+$V 219,66 190,110
+$V 177,134 201,167
+$V 173,142 201,167
+$V 188,128 256,143
+$V 256,143 195,120
+$V 175,147 147,192
+$V 147,192 173,142
+$V 168,127 113,103
+$V 113,103 171,136
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 171,109
+$V 171,109 177,130
+$V 171,109 142,135
+$V 142,135 183,120
+$V 183,120 177,130
+$V 142,135 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 132,80
+$V 185,109 195,52
+$V 132,80 175,116
+$V 195,52 190,110
+$V 177,134 192,201
+$V 173,142 192,201
+$V 188,128 241,106
+$V 241,106 195,120
+$V 175,147 234,175
+$V 234,175 173,142
+$V 167,130 113,143
+$V 113,143 171,136
+$V 167,130 160,153
+$V 177,130 210,140
+$V 190,110 208,94
+$V 172,105 172,64
+$V 154,122 120,115
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 171,109
+$V 171,109 177,130
+$V 171,109 142,135
+$V 142,135 183,120
+$V 183,120 177,130
+$V 142,135 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 139,55
+$V 130,154 175,116
+$V 139,55 190,110
+$V 177,134 219,150
+$V 173,142 219,150
+$V 188,128 193,72
+$V 193,72 195,120
+$V 175,147 226,80
+$V 226,80 173,142
+$V 167,130 144,191
+$V 144,191 171,136
+$V 175,147 173,202
+$V 155,138 130,167
+$V 157,128 121,89
+$V 171,109 165,69
+$V 191,115 205,70
+$V 184,116 247,130
+$V 177,130 191,172
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 156,170
+$V 156,170 177,130
+$V 156,170 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 179,200
+$V 130,154 175,116
+$V 179,200 190,110
+$V 177,134 221,72
+$V 173,142 221,72
+$V 188,128 123,82
+$V 123,82 195,120
+$V 175,147 120,89
+$V 120,89 173,142
+$V 167,130 276,111
+$V 276,111 171,136
+$V 172,131 164,203
+$V 153,136 74,143
+$V 152,114 128,56
+$V 179,113 182,44
+$V 199,118 301,141
+$V 194,137 215,183
+$V 151,150 97,225
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 204,106 216,100
+$V 215,110 221,109
+$V 231,98 236,95
+$V 241,102 241,102
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 214,99 214,99
+$V 215,110 221,109
+$V 242,84 236,95
+$V 246,91 246,91
+$V 221,147 221,147
+$V 207,133 207,133
+$V 168,166 168,166
+$V 150,161 150,161
+$V 175,92 175,92
+$V 192,90 192,90
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 220,75 220,75
+$V 215,110 221,109
+$V 242,84 236,95
+$V 278,93 278,93
+$V 248,151 248,151
+$V 211,172 211,172
+$V 176,196 176,196
+$V 137,186 137,186
+$V 178,70 178,70
+$V 198,64 198,64
+$V 169,100 169,100
+$V 192,97 192,97
+$V 204,137 204,137
+$V 151,150 151,150
+$$
+\enddata{fad,270222644}
+\view{fadview,270222644,2,0,349}
+
+... by Curt Galloway
+\enddata{text,269602880}
+--Where_No_Man_Has_Gone_Before
+Content-Type: application/atomicmail;version="1.12"
+
+;
+;
+;
+;
+; This message contains a ATOMICMAIL program. If you are reading
+; this now, that probably means that your mail reader does not know
+; how to handle ATOMICMAIL programs.
+;
+; If you were reading this with a mailer that had been extended to understand
+; the ATOMICMAIL language, this mail message would automatically interact
+; with you and take certain actions based on your responses. However,
+; the language is designed in such a way that ATOMICMAIL programs can
+; NEVER do you serious harm.
+;
+; If your computer has a ATOMICMAIL interpreter but it has not been linked
+; into your mail system, you can run this program by piping the mail
+; through the ATOMICMAIL interpreter. (In Berkeley mail, for example, you simply type
+; "pipe <message number> atomicmail".) Otherwise, you can simply write the mail
+; out to a file and then type "atomicmail that-file-name".
+;
+; If your computer doesn't have any ATOMICMAIL software at all, you
+; should probably reply to the sender of this message to tell
+; him or her that you were unable to run this program.
+;
+
+(&checkversion 1 12)
+
+(defun init-ctrs ()
+ (progn
+ (setq newline "
+")
+ (setq summarizer "mmsurveyor@thumper.bellcore.com")
+ (setq global-survey-qid-ctr 0)
+ (setq global-nesting-level nil)
+ (setq this-level-ctr 0)
+ (setq total-questions 0))
+)
+
+(defun nextctr ()
+ (progn
+ (setq this-level-ctr (plus this-level-ctr 1))
+ (setq global-survey-qid-ctr (plus global-survey-qid-ctr 1))))
+
+(defun pushnesting (txt)
+ (progn
+ (setq global-nesting-level
+ (cons (list this-level-ctr txt) global-nesting-level))
+ (setq this-level-ctr 0)))
+
+(defun popnesting ()
+ (progn
+ (setq this-level-ctr (caar global-nesting-level))
+ (setq global-nesting-level (cdr global-nesting-level))))
+
+(defunq onelevel (i)
+ (strcat (int-to-str (car i)) (cadr i) "."))
+
+(defun apply (f l)
+ (magiceval (cons f l)))
+
+(defun getstring-oneline (prompt def)
+ (newlines-to-spaces (strip-newline (getstring prompt def))))
+
+(defun getstring-notrailers (prompt def)
+ (strip-newline (getstring prompt def)))
+
+(defun newlines-to-spaces (s)
+ (let* ((l (strdecompose newline s)))
+ (cond
+ ((null l) s)
+ (t (strcat (car l) " " (newlines-to-spaces (car (cdr (cdr l)))))))))
+
+(defun strip-newline (s)
+ (do*((len (strlen s) (- len 1)))
+ ((or (lessp len 1)
+ (not (equal newline (substring s (- len 1) 1))))
+ (substring s 0 len))))
+
+(defun cadr (lis) (car (cdr lis)))
+
+(defun cadar (lis) (car (cdr (car lis))))
+
+(defun caddr (lis) (car (cdr (cdr lis))))
+
+(defun cdddr (lis) (cdr (cdr (cdr lis))))
+
+(defun cadddr (lis) (car (cdr (cdr (cdr lis)))))
+
+(defun cddddr (lis) (cdr (cdr (cdr (cdr lis)))))
+
+(defun caar (lis) (car (car lis)))
+
+(defun cddr (lis) (cdr (cdr lis)))
+
+(defun caddar (lis) (car (cdr (cdr (car lis)))))
+
+(defun > (a b) (and (not (lessp a b)) (not (equal a b))))
+
+(defun mapcar (func args)
+ (cond ((null args) NIL)
+ (T
+ (append
+ (list (magiceval (list func (car args))))
+ (mapcar func (cdr args))))))
+
+(defun thislabel ()
+ (strcat
+ (cond
+ ((null global-nesting-level) "")
+ (t (apply (quote strcat) (mapcar (quote onelevel) (revlist global-nesting-level)))))
+ (int-to-str this-level-ctr)))
+
+(defun revlist (l) ; like common lisp REVERSE
+ (cond ((null l) nil)
+ (t (append (revlist (cdr l)) (list (car l))))))
+
+(defun informative (p)
+ (strcat
+ "#"
+ (int-to-str global-survey-qid-ctr)
+ " (of at most "
+ (int-to-str total-questions)
+ "): "
+ p))
+
+(defun survey-multiple-choice (prompt choices)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (car
+ (car
+ (select (cons (list "" (informative prompt) NIL NIL) (cons (list "" "" NIL NIL) choices)))))
+ newline)))
+
+; USAGE:
+;(SURVEY-BRANCH "What is your favorite color?"
+; (quote (
+; ("red" "red" (branch-question-set "red"
+; (quote ((SURVEY-SHORT-ANSWER "Why do you like red?")))))
+; ("green" "green" (branch-question-set "green"
+; (quote ((SURVEY-BOOLEAN-ANSWER "Are you green with envy?"))))))))
+
+(defun survey-branch (prompt choices)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (let* ((ans
+ (select (cons (list "" (informative prompt) NIL NIL)
+ (cons (list "" "" NIL NIL) choices)))))
+ (strcat
+ (caar ans)
+ newline
+ (cadar ans))))))
+
+(defun branch-question-set (branch set)
+ (progn
+ (pushnesting (strcat "/" branch))
+ (let* ((ans (ask-question-set set)))
+ (progn
+ (popnesting)
+ ans))))
+
+; USAGE: (survey-short-answer "How are you? ")
+
+(defun survey-short-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (getstring (informative prompt) "")
+ newline)))
+
+; USAGE: (survey-integer-answer "How old are you? ")
+
+(defun survey-integer-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (int-to-str (getinteger (informative prompt)))
+ newline)))
+
+; USAGE: (survey-boolean-answer "Do you think I am sexy? ")
+
+(defun survey-boolean-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (cond ((getboolean (informative prompt)) "Yes")
+ (T "No"))
+ newline)))
+
+(defunq surv-pkg2 (q)
+ (progn
+ (nextctr)
+ (list (strcat (thislabel)
+ " ("
+ (car q)
+ "): ")
+ (informative (car q)) "" (car (cdr q)))))
+
+(defun formatfillinlist (lis)
+ (cond
+ ((null lis) "")
+ (T (strcat
+ (car (car lis))
+ (cond
+ ((equal (cadar lis) t) "Yes")
+ ((equal (cadar lis) nil) "No")
+ (t (sexp-to-str (car (cdr (car lis))))))
+ newline
+ (formatfillinlist (cdr lis))))))
+
+(defun survey-complex-form (preface qlist)
+ (progn
+ (setq this-level-ctr (+ 1 this-level-ctr))
+ (pushnesting "")
+ (let* ((ans
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar (quote surv-pkg2) qlist))))))
+ (progn
+ (popnesting)
+ ans))))
+
+(defun ask-question-set (qlist)
+ (cond
+ ((null qlist) "")
+ (T (strcat
+ (magiceval (car qlist))
+ (ask-question-set (cdr qlist))))))
+
+(defun qcount (l)
+ (cond
+ ((null l) 0)
+ ((equal (quote survey-branch) (caar l))
+ (plus (branchcount (caddar l) 0)
+ (qcount (cdr l))))
+ ((equal (quote survey-complex-form) (car (car l)))
+ (plus (dcount (magiceval (car (cdr (cdr (car l))))))
+ (qcount (cdr l))))
+ (T (plus 1 (qcount (cdr l))))))
+
+(defun branchcount (l prevmax)
+ (cond
+ ((null l) prevmax)
+ (t (let* ((this (plus 1 (qcount (magiceval (caddr (caddar (magiceval l))))))))
+ (cond
+ ((> this prevmax) this)
+ (t prevmax))))))
+
+(defun dcount (l)
+ (cond
+ ((null l) 0)
+ (T (plus 1 (dcount (cdr l))))))
+
+(defun handle-survey (to summarize subject id qlist)
+ (progn
+ (init-ctrs)
+ (setq total-questions (qcount qlist))
+ (sendmessage
+ (cond
+ (summarize (strcat "\"" to "\" <" summarizer ">"))
+ (t to))
+ nil
+ subject
+ (strcat
+ (cond
+ (summarize (strcat id newline to newline))
+ (t ""))
+ (ask-question-set qlist))
+ NIL
+ 0
+ T)))
+
+(defun maybe-displaytext (tx)
+ (cond
+ ((equal tx NIL) NIL)
+ ((equal tx "") NIL)
+ (T (displaytext tx))))
+
+; THIS IS THE END OF BOILERPLATE CODE FOR THE RECIPIENTS
+
+; user-generated part begins here
+
+(maybe-displaytext
+ "")
+(handle-survey "nsb@greenbush.bellcore.com" T "RSVP NOW!" "nsb.greenbush.bellcore.com.1991.8.17.15.18.4" (quote((SURVEY-BRANCH
+ "So, can you come to the party?"
+ (QUOTE
+ (("Yes, I can come"
+ "Yes, I can come"
+ (BRANCH-QUESTION-SET
+ "Yes, I can come"
+ (QUOTE
+ ((SURVEY-INTEGER-ANSWER
+ "That's great! How many of you do you think will be coming (including yourself)?")
+ (SURVEY-SHORT-ANSWER
+ "What kind of *vegetarian* food would you like to bring, if you have any idea?")
+ )
+ )
+ )
+ )
+ ("No, I can't come."
+ "No, I can't come."
+ (BRANCH-QUESTION-SET
+ "No, I can't come."
+ (QUOTE
+ ((SURVEY-MULTIPLE-CHOICE
+ "Aw, that's too bad. Why not?"
+ (QUOTE
+ ("I'm busy that day."
+ "I hate Star Trek."
+ "I hate you."
+ "None of the above.")
+ )
+ )
+ )
+ )
+ )
+ )
+ ("I really don't know."
+ "I really don't know."
+ (BRANCH-QUESTION-SET
+ "I really don't know."
+ (QUOTE
+ ((SURVEY-BOOLEAN-ANSWER
+ "Well, please don't forget to RSVP when you decide, OK?")
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ ))
+
+
+--Where_No_Man_Has_Gone_Before--
+--Outermost_Trek
+Content-type: audio/x-sun
+Content-transfer-encoding: base64
+Content-Description: Distress calls
+
+LnNuZAAAACAAAIguAAAAAQAAH0AAAAABAAAAAAAAAAD///////93////////////////////
+//////f/9/////////f39/f37+/37///9/fr6+vr5+/r5+Pn4+fv6+/r6+ff2dnT1dXZ29vd
+5+fj6+//9//v///3//dv/2dnXV1dXV1fXVtVVVNVTk5MTU5PTU5XWV1bXV9r//f3/+////f/
+6+///3dfa/93b/d3d/f/5+fr49nZ3d3X3+Pn/29n///3//////f/d2dZUVFPVVVZX1lXWVtX
+X11bW1trX2fv49/b19HO0dHNzMzLx8bGwcbDzc3R193X49nf59vn52/r48/T3c/d119jZ9t3
+79/nz99ZT1/jY29X/+tdRm//Z0lLT01PSkNGW0o9SkRrS05FV0A9Q0tVWUlK3/9fV87Zx8/N
+2bzd68zBwMv3y8Ldzvf3xsrPyd3HyV3VzF3NTtf3zN/jSt93Tmtf71XrZ11VS+s/b3dLTmfn
+91tG3U1N511na+NLym9v19PKwsvZ29P//2PZxs5f78RLQG//TdlTuc9O6//nd13KY1v/2VtC
+vUHNO8VKv0dbw0b/SM9T11NIzlvZWb/n1VvK1WNXuv9FvzlPvzrB2UHHXUvEQ/fZRtlPytXO
+RWf3P0pZUdvTd8Q9RslGSrg378Q+vb7b91/A1V3/1VvFUUTAW0W+3z+4TU7AzUPPU+93/9s/
+T8K7PEZHRd+4vm/AWUc80znrumvMSWf3vknGvufZtkO5/0fLwj27zz/Asz/C20DV/0FLxS+5
+RkLC20jPwz2//0e8x0LCd0jNzzvT6+vLQUPd1zjjuFc/uD85wU8868A3Y74927s/91ldSFfd
+Se9fb3fd1z/f39dI08pHd85AWb3jQcHTT8fOPcnCT0XDz0xj2dFfZ9/F199ryuNfZ0TP/0x3
+PFnr/0lH2/fR0c/I3//jwFfX1ePf0//Z0cbLa8fbvGdjyGtX3+fdd2Pva9ffTufjTu/nY1nn
+1Vdr3UvT31Pd/0ld12d391dn61tKb+NPTu9MROtPVUVrQ0lVS0g/b0JI71VbSvdR2U9Z4+/L
+22PRyG//3dXfyszM68/Rxu/r293rZ+PLa9tfSOvK12f/Wd3j787bW+9r7//f5/9R60/TydfT
+zNfd3crJ2evr387H22Pv2W9X79Vnb2NTZ1dnV91K9+v/513X92drY2d3//9r42P////jb09d
+UVdXWVtNTFVMQU5NS0ZRSE5KTUxZV1X373dv39vR1+vf09fP49vf29vvzuNZb2fv2ev3b2ff
+5/fR21dn293R63fn59fj619VZ29r487X293Lw9f3Y/dRWXfZ72PKxMPBv8bJxcTLb2d3X1Hr
+W1tZ39/d9+ffVWtrU01fY1VP2d/Z1d/v901VSU9XTUk1ODQ8NTxKPDE1QUdCU9/TV2PJ4+tX
+V04/StvRz8e3srCvra20uLm731dFPzQ5Oz05PUzjzMXDubq3ubrEyc7rTEdPTzxGS0pD4+9j
+P/fTyDVP97hLVf+30W/PrLPO17e4XffP9z9Cb/88SF3I/0zR1dk/T189MTY3LCcrLjIvOE9n
+Y8m0rrOura2urq2zsbO2wcK/z993W008Oz04LzU5MjE7QD85VfdfUVPT311r0crj68vG0Xdn
+519RQkY8Pjw7ODxJPD9F/99398Syvd22q6y1sqywr6+vsbe1wbvCvdPNy7vJ0ci/v9vLzMXr
+21tTS0M8OTcxLC0xLSkqMi4rLTUyMDA5PTY3Pk0/Rkj/X3ffy8TBxL23vLy5t7u+v7y8vr2/
+v73AxL++vLy9vr2/vsO9wcfGxcfIy8jd53dVSkI+PDg3NDg1NjQ4ODw/Q0lPTl9n43fd2dXV
+08vV09/j/2tbT0dBRD06OT09PT9ARk1VZ+PZ68vGvr+7uri3tbKwra6tra2ur7O2tr7Fx9Pn
+509PTkVAPDw5NzUzOTc3NTg4OTw6PTw8O0I9Pz49Q0Q/P0NLQU5RRP9Xd1XHX87JzcS+vsC/
+uLu9trG0srGvr662rLuytLXLvsPZzyrGrT8zw18zY808X3dVLz9IKD13PjhJPTQ9RzI9SjU4
+PkQ3Rk4/RFFXY1P/93fd58TRv8zExMXBwe+9xb7Iw8nCvMzCucO5vMq7xNu/z+P3a2dFY0tj
+Pd82QjpNOlVJQldDTznJP9tBwEjB53fO1TzfvFnVxV9Ny+NVvtFPu8pN379J/0PLTUX3PFN3
+QTzNNE85XUs/Z0k859M+X8lbS8zTScfOv8m1zc68vsC6yr24v/+wU1O00V27XbzVstHO2cw4
+we/n17bP1/drWU5HPjs5LCxBMS01Pzw9Qk1Tzl9jw9VX/9dvQ1k+QFNKQd3M6+O6vMPHv8Vv
+2+vP913NWdfP38zVwre9uK6zr62vsre6vHfOPEdHIyY0LyMlRSovPm80QmtOP9HI69HFzffJ
+Z0r/RD45Tj5DSudb0dW8vsqusNdIb+M3L0vfQefBsK+wra2tra2tra2tra2yvNPMyF85RM8t
+OjM0Ki4qLyg9NSwmO1NfLTnN3TxB09P3TUxIW0Y4MkJANjU/59lryrLvvq2rb0+9zC84X29j
+17OvrKysrKysrKysrK23tbiyxGfCw9dDT0o1NDcuLio+KicvNzMtNFc3Ql9LPTxP4zk2UedH
+TVNdP0hnWTxB69dNTdPIw8XEub++w2NPV8fO68q0sravra2tra2tra2tra+vtb7Ly9VfPz81
+MjA1Ni4sLi8yMDQ4Nj5DO0JKV2dKTllNWV1OSUdOUVNbb+tnXVFPT0dMTkxZXV3/38jBwry5
+srW1s7G0sbGyt7m5vMK/x8jJ09HT3dXV1ePd09Xj3+P/W1lRT0dFPzs2NzUyMDAzMjA0Nzg4
+Oj4+P05VY/fT083Iwb/AwL2+vsHBw8PBwsDCx8PHy9HT0+f319nT08vLy8bCwL6+u7+/v7/I
+zc3T629bV1NKRz9KPkBDPkFHRklVT0xGRT8+PEA9OzhHMj03PTo0RjxGPltbd83nweu+wru8
+ura5u8W7tLzFzLvj09PG/13n/+Ndd19jRUpdX1FO69PbycW3vr+4srq5urW/xsjC1W9nW0dD
+PkM5PT07OT09PERGRU1XSExZa01FY01JQkg8Nzk3OTQ8OkM96/d30726vLKtsrWsrrOzsb2+
+wcvbW/dDTUdJPU5BPj5VRU3/5+/ZxrvNvcW8x8rBx9nV3/dPTl9RSl1jb2P3b2dRTkg+Pzs3
+NDI0Mjw6Qj9bU3fj2cnIu8DFvbq6yL++xcXIx9fd2edv53fK11/v481n1cPO1dvTzvf351lX
+TWNVU1lbX1dX//9bb2POV2tv70lGR1E/UVNMQE9XZ1VfX2dj1+vT2c/PydO/77/Cv9l3X99f
+90pITkE6TVU9Rv/rUW+/uL65scS2trrBzsW4d/9vTmdIS0YvLy83LS8vNTtMTkNCyL3Hyb21
+sryztbSwtrXB08G6xb531edjb0w7RkJJOT9JSFtf50TNd07PUWc67706NddPTzxrTj8vb+dL
+Kziw5zA70cHbxbG+w62070jdwDgsQf9DRO++ycC0yf/d/0UxNUs8OFPjwL6zrVWts2M6189E
+Lt/ITreusa6trcbGutE9U0hIRUNdb8rjZ1fZZ0k5TVM4P904NcnfMD3FW0xOSE08QzI9UzYt
+W0gvOUbOPz/Kz0jrxc5OyLa857u1wdPAt8e8s7S2sK+5vrm3yuPM0U/Xs87Rua/A39XXPjg6
+MTM4NS87QjYvODYuKyoqKy4qKDhIOztvv93Au7eyvrnC18K6w7vItK23ta2trrvFrK5X77a4
+xWvBsb/Cys3C0UE+Nzk3Ki05Oz85StXfWU1ISzouKy0vLCoxPD9FXV3Prb43yrVPRWvfxufJ
+us6svbyssd+z0dtfPUM9St1bTbPEz6+7ubXKuszDz9nGz9XOyMXGb9fZ/9s7R2M/MDU1MD0z
+NUM+TElFY1NHRkZJRkdX/9fM68bGv8XLv8vIx8nHxMzOwcHJxsHGz8vO99/M33fT/2/fZ1Nb
+VVFKR0tDPUU4ODs4MzU7OTlCRUddXWvV2dHKx8K/wry8x768ysfCz9HV2+/v929VU1tIS1dN
+SFfvZ3fr/+/d71Vn/29VW93j3c7MysfJyM3O0Wt3b1lOTE5VU11Z/+f/d9nX593b19HTyc7J
+vcHKv8bO29V3VV1MUVtXV3dd79fvz87O0cbGy8PHzMrH39H3b2NHSEpAPDs8PDk5OTg4OTU3
+Ojg7PD4+QUNGS0pVW2//49vNz9XO3f/V4//R3dXJxs2/wMjHs+PftrzHxMrFur3Rxr+9xMG8
+y8O5wcXGxcXGxcfdwLpT57lLLbRPLDxdNS42PTAtMjY5LjE7OTQ6SEFASN/3Z9PJzcnIxsbI
+xb/Hysi9w8y/v762tbm4ur7Bu8rKyNHbb+/dT09rP0VFOT9MMzpCPj5GQkZNY0BHZ0RHXURA
+WUtIT01BU1U9SGNFWU53V07b1ePGvr26ub+3u7y1vbi4vbvBwb/EwL/O68rTz8/jzdHMzt/r
+1f/n509ITk89OTo5OTg9Ozw/QkNHQU5VRj5MRkdXa1/nzdnPvtFj28jI7//FzP/Tu85MzLvE
+X8LDXcq+x1vBvM9DQEdL39HX08rA011DLy8wLCgqLTVVy7u6sa2xtcs+NDUvLTE3RuO0try2
+s7W1vcfMyclv58nJxLi4sMK+r7vbyt1n1+Nfd0dbTDlVSU1jZ8vN1cb/TONdPzg6VUXfzz0x
+SUpPXVtTR1dbOy4uKi42OURTxK6tr7G8x8zvTklPa8zAxc3vwbe8vcDIwr7A3V1PY93vS0/H
+v723ubnTX81HKSs0Ly9XRUHdt89LRFVOVU9RR1NNRExLU+fHwl2+v1X/zk5KTm9r2b61vLy5
+1WtbX+v3v73Dv87ZyL3Dxec4LC0817yvsMPRPzIxNDxBW/9ry7S0srfHY1NKOjpEPVnvY19I
+PzpERUtANjzZSlFvPDZjR0fL37/HX8HjXbG9vLPVssHTuMjfucnEtruvusjZPiwuLDhJY9HP
+U01XvLu7wFEpJCw0za+4vsLDuLXPNCMlMMSsq6u7d0E8QEpd99XJys9dUUk/TlVOU1s4PUpF
+R0xVUUxXd8Kzy81Xz05nrK+6u0tLsbu2rMBPPTIuPWdX2Vk5RV1vQTcyLS1N60RLPjA6P0rn
+yetZy7q4ra20w626xLm/vrmura2trb1fRT9N2c/P1f9XSUE5MzU4PlVvT1E/Ni8vNjtGRj80
+M0FGSltrQUtLTFdI5+fLwV/v28C1trCzwb6ztb28ur3BubW4srW9z2dfd9Pj28LBv7rH2WNH
+1cbbvcNMLCYpKDK4s7vCNiQiIis4Z9FrXUg1OTs4Pz9RUT1JOzxASGdv72NEQ9s8Wb68/7ay
+4721vMStrbG3rbi8r7W9sK+yu8DjWcC/xb6/72/Nw9Hv199OSj0/PUJOVf9nM0z/Q0/VTDs4
+QDYsL0lNY9X3Vzs8SDY1OT1Pb+9JNkhjb9drQzc5Sv/369X/X+PDvte+ts7Ps7rEvdXXta+t
+s7/I38m0t+NvT0/ny8bO38jLwbrL1+NNV29jV1tFPVH3Tj83Ly44RE7Xt7rTWzYvN1XCvbm0
+xcvXW05GQFFPSl1ZST86PD9LVUlBQVXj2+NnQUBO68DZY11f18rN4+fn3+fF02/Gvr7R/8zP
+w7y80cXCyb/HwcPAvc1n/3fnx9XT09fBa0ZBQv++uNFJW8hjPz02Lj1RWz8/a8VfOj0yKC4+
+MyY2yNNCR2/jY2/ra2/Nwr/f28O2tMTHvrCxrrfPtbW+u7O4yMy8vtnEvMTIxMdvR05VRklN
+Sjo6PkEzMTs+Ny8vOzYrKiwuLTM2Ny86Sk89PUvRye//ysW/ubq+u7Ovr66xta6tra6wr62v
+tby9uLO3vb3By9Pb605fb2ddY1lKS1lfSUdKRU1IST1AOz4/OjYyNDo4Ojk7QEU6QEQ/S0g+
+Q0RJSkxVV09O511NW1dbb1vO2c3Ky7/Dwbq/wcDKwr2/vb62vcC2v7vFwry/vb3Rv8Xnv933
+2f/va/9vT2dZSltITl1CXT9ETD1DQD5OPUJNPkJBNk84QE09QVlLR3dIXXdny2v/y9VvxdXn
+wcrHxcbMvsbIt8nDu8XHw8zryt3d79/ZV8tbWWNTU29NX1dOb1v3Z13bX+Pn3e/O18fL2cXj
+b8jM/9PPXczVWd/vV2vnUVFXT0FTVUBPWUdNSEVCRD08PDk+PTo9Pzg6Pjs6Pz5ATk5ATFFN
+UWdba9vb3dHJx8K/vru3ubWzsbGurq2tra2vr66vr7Cvs7e4u72+w8jM1d3jd1tPS0ZBQT8/
+Pj46NzU0NDIwMDAvLzEuLzEyNTQ4OTk7Pj1AP01TV2Nn99/f19HOy8TFxMfFxsDAwcC9v76+
+v7+/xMC7vLy+vr3DvsbFx8/TzdPd3dn3b2NvXWNTTlFIR0REQ0Q/QUNJS0pdWVtnb+/n///f
+d+/r39nn2+vv92dnXVFXWU9XV29dW2dnb+dr99nV29fZ2ePv/29vY//vb//Z793b0dHP1c/T
+zdPZ21Xd1SqtrDwjrqwvLa2sNDK4uEkx77rRMWe8VzDTuj4xybtGPN3PY0vn0d93z8jV583H
+62fV3VFMS8NdPMjdRtFNz+vrSdm060qxzWutUVWtwziurS6+rHdHsVM/tl9Eb7o+vLsxvb5I
+ML3rQD9v7z5H0T8uP7czNEXXPjw5XVEtR10tR+9VPTXv9znZdzeuWzbOu293xe/ZvMHPvky/
+rbxF562t6zStr0i+rsbOta3vzbews+tJurTJyELOtF0409dE71tALU68NShCuzUqMuM9Lz0+
+LTZdQyksRWcyKS/X5ysv004ySt06Q8q2Vz7DrsFD3bmv3cy3t823ssNfwK22Z8W0r9v3vLdr
+0bvFW8i8vFd3urnZUb2832PN2Vdbzuc8Qs3jPDVG31kuQu9HOktXNTx3WT49T9NZNFVvTmdv
+VffO0+9vzsTL3dHCx8nNwL3GxbrFzr6/083HwMpva8lvV2NNTGNXPj5LV05CTmfvSz5Db0s4
+OktCOjg8Ozg+STc8X99ZR1fCx0nPssvZvLvA472vv/+/ucbrb9nDV+vFzcm8u7i6trK4yMHB
+z0s/Tk43O0I3RXdvW2/Kw8jd28/ja1s9OD09PTUuQD80OV9LTkv3yf9nytffy8bGxsu/vs7N
+wr/Dxu/f0dlrWdfJ71Nj9/9MR1VFNzxLRTE4TVc8P2/PUWPI0WPvxsf3Y7i51/e/usTVxL/H
+xbm7v7+/vMnOwcXj1d3r70xVZ01XUU1ZU0FPSzo8T0k2NUtvQzRATEk+TW9MRVtdTElPV9vP
+62v3u77ZybzFzsPDvri5t7S+xbi568e93UVPvtk6SsDrT0TMu01CyE4vNk8/MThjVTo9X9tA
+PURPNzRESTo/WWdHP0rXTkFFystMa8fXT83H11/Zsr7XurG5xLe1vt+8tr/BtLrVwbeyv9u6
+tttZxtlTQd13OD1jTTE3SkgtL0k4MThHPj0/21VI3brX/8fAxN3Xw0k4Tu9KSv+4u99OU2NK
+My9LTUVd/1n/287d3b28zc7j/9tVP+fFw2fNu79vx7rAwsvFxMzjwc3vyL/C09vNzNPbXWfv
+Q0dCPD8/Q0ZRU+9VRGvv51VV9+9FS1dHSVX302NX9z01PEU8PkJZ51lDY1tKTd/b487d6+/N
+x77HvLi5vr++v7y9z8nEvMfByb7FzczL111KU2NDV9lKTOfvSE9NTv/nY01Z23dV3+fvxMhn
+Z0RG3WNHX1fja0RGV0k6S09XQz7JWz5T/0VJRj9ZSV3RX0pbb3dTSs9348HOY8O/vMVTy7bT
+v8PXvrvJ6/fZv8zn09W0tsbNus/GxmdVY+vvQkdvdzs2OTw3Nzs8Nz4/NjZTP0FnSmvnV9Pb
+0+tn28ZKW8fna+fPzk//wNfj/9m9vsrKub/Gu7q8vL23uOPIvsfj48bBzN/L1efMTErI1UhT
+710/PU1OQVlrTzs4PkQvPFFAPUJBQ0A0QFdB//9rRllv1VFNX9XM1c3Nwc7GxtXZysnv67y5
+yFfO393O009vy/9v5+vn3dNNSdPHTU/300o+Y29NZ8znVc7L61PR22fd49vJt8zj78G9yc25
+vdHIvs1rY89v486/10rn60xJ3e9JTk1MOzlAOjw2PFFbOD1AQk02Nj5CVWddV1lbZ2NP92/G
+zePGvbzK1cvK2efHyMXLw8rbyr/Nd+/nyHd323fT2993/3fb1UxZb9tPT1vT3Vdrb1dT9+dr
+X2vTTVPT90ZJR0VTR1NMX9fvV1VZSUpvd1//z8PXTOvN79vN0cLA6+//Y29na2vLxMfV09v/
+d/fPzMnV0ePr319KV93P91vb7/dnTGNjd+/f58rG119T705dV0lN30o9SllMRk5ORT1n60Vd
+9106S19bSVvV09PT23fPx8rC3c3Ly9fJb+u/29vHxs7My/9Nb9dfa2fd18rZ/2tv3XfXX2/Z
+TmfnV1ffV/dvS1lVT05ARW9NZ29NzNlOSlFd301HW1lb/8//WdnVa1tr92d3/99j/8jfb99d
+/83P38/Z093/0ef319Pfy8Z368/nWUdNd2f/6+dda/f/909Pd9/ja+Pn19POb+ffa+tXX3ff
+a//X61Fjd2tXSktbWU53d1VZ987b49X3/83Za3fVwb7vZ+fd591ja+Pn629dP11dT0n3d0pO
+Z2tKb813W+trb9Pf/+Pd12/j31frzNX/z9HbY13r4/dTWfdvTm9ZSFld2/dbXXdbUWdd/1XL
+xltX1+9v79fH0d/O/9ffb9X/SnfdXev/28//6+fn2+/rz29f1c13/+9r39Hv2eP/42fnWVNZ
+Y2NZW1vr1d3/b99nWf9d99HLysXK1efV31Xj12tX2d3/Z2dXSE5ZTE1FR1FXTU9HTElTY0hG
+Ue9bWU5dd193/9nf19Hnb/fv99HZ79PV2dXXb+/j58nL08rV18PH08rG173JzsnO28vCxcvZ
+ztfvT2df7+tMSEJHW1NLSUxZVU9JRUtVTU5RX2P/VUdf31tf71tbb29VWVFMd1FKS11bSEtZ
+X1djU1Pn/1tr52f309n30e/v0dfryc/VzsnH093P2dnMy87OysHHwb7Cvb6/ysTGzMvd59/d
+3dn/X91fa1tVU1dvZ3ddT1dRT0JAP0tPRj9ETVdOTUpISltTSEVET1dbU19XSl9ZSkhFTUlb
+WVn36+fTze/XxM7b18rIwr/GwcK/vrvEz8nNy2//W1VjV19ja1tTW1Nfa+//b/f3//9vb19V
+Y9vf3//v59nj49Xf28/P4+vX3d/fa/f359nj/1tjX1tbUVf349nr19fV19X///9db2tXVU9H
+P0E+RUVKS0lMUU5dTEj3d3fr3etrW2/nXVdd29nja+/n5+vn38932dPXzs7N287f1dvPyNHM
+09Xr92dj4+fv4/d39193XVtdX2/f/19v5+drZ/9v599j9+f/z8zOx8rDycvOx8nLyMnHytPr
+5+tnVV9jWVNZW09KQT9FRzw+Pj5ERUpOTVlLUV9JQ0pNSUlLTVVbTlln//93Y19ZY+ddZ+/b
+y9XV08rH19/b59XKyMLEzsXKy8jNzNfn193R5+vRy8fEw8TDwcbDxNHj393Vzszb/29ZV2Nd
+UVlNVU9TVU1NSUtKSkVIRkRHR1dbXVVOR0tfSUhNPkJRU01NU19rd/fv/1dv3dfOyMjLy8vf
+293j59XR09vf2d3Z083OyM3PxM3My87Jzs7Nz9nf9+/j0etja2NjZ11ZUVdJT0pVU0pOSERD
+P0NHSUZCSUpNSUZLS1drZ+vn783MzdPPzszN0dPJ0+fO3W/r09fV3+fPyM7b2/fV1dVv19PP
+zutZV2d3Z2tra2drV1tfW1lTUWNv39ndTln3W1lXSVFjY93Z7//FyM7n9+/V52N3a+vRzMvf
+29XV1eN32dXP2dP349/j43dTXV9bQ05GQ0tMV09RT11bR0JMTkxbT05MW/fb2/dv5+fO29/d
+zM3PzM/R1+dj39Hj1+dbV3dfWVVn69/r52ddT13X0+Nn59vZztXrX9vR291v3+9v09Hfa1/v
+42dV2ePfY/9ZW+PR0+tb3dfn499jV+vvb3dnd+N3b9fZ71VET09da1lOUUtXT0xNSFtbX1lb
+Y2f/X//NwcjMz2/fybzI0dXPz8rjVV1r485PR0tVXVtJRU9jZ+t3W+/n411jb+vZ1+NrZ9vO
+011MTmfr2c/Tzcvb32/j3+fV0cnFzdlv41dX91tKWWv/409JU1t33+tNT0RGUVtX/+tKSERH
+SVtRWV9La89fU8/H1dvX39/rxLm958C6v7W90f/RwsLfW9nNydH352/DzF1nSlXTa0JNSERH
+Pjw2O/9XSDtO29NTPEFHV93/V0Nb91NbS0JRXcfCy8Xn38rv577N09fPzc7LZ93Rx8HKvb7L
+39nn18bK/+dvd9/N3VdRWUhDUTs4d0xNu1syMjBVzlVJWd/GwNk/MjxOwbxMS0/T07rHb7C/
+69lVPtGuvciwwk6+9z5ESOPIXzRRvv9vSzw+47+/zltFx81bw9lJT79RMT9fRmtTU9VZMdfb
+PES5az/F51m7vM7byOvGvVXjv2fXzTj32UfdTUU/Rt9DOV09Y+NMvj882///Oj3HVdfZS01b
+wbnjd8RbvbzB00mssuuvT0+us7/bQsy7vF3bz0xNtmMxyL430esy68frWWc9PlVTMVfKK10/
+MEhrNjLvNTjLSzV3wDdXzWdjwlvGMa1bvuM/vt/IzetXvs3HwkC32V3V1z9X177LyddMd99j
+ylNrPMnd7/9rZ1XIxUlrY7tbv+c8tcvPY9nJT8a8PUi5U8q9XW//zuPHPkxTvEo4uTk40U9N
+P0Q7W7cyM2ddSf9vMzm6z0tOQC21sDxjyUjHusw8Ubi3ur03Nq3DUUhO3bC6ySVA9+vF5z7J
+vkz/zT5fW7xVRMNIPcxrOsDE3UxZRk9VV0pb10hKZ2vXQ1PIyLi8PFu3x3exzFe7xL7GycFT
+y7Tdzbo3QKzNML3MMj9KZ76vvzNn5yMnQC48trzfvr5NTOf/yr5EO85fz77BXVu0zVFLJz6v
+3y7T22vKti8luq1ET7Ur1a33Kz63rTw+RStLvNs3R99fd71FObGuW7/rMretvD08vbdd6+/P
+r8ROYzpOYz5RvrisrLO5999fNEzvRMEyR7g7X73bLy1A2yN3rc05P7W2NyoyNrOuv8vZtbLN
+Ozg+b9dvPjr/tb/rZ8XKyb7GRU7Nxzorx8ZRQVlCOkjJ4ztX389rSC5RtLfDvb69xOfOx8zF
+LjzDubtjTz4qLT9VPV2urbmvu1Pjz++8dz7f0b671W/Mw7rZNzc0NDs3Ki1HZ99jPEVdTEdd
+Rl29s73PTV24trrHx7y0sdfVV1PL2UosOuPJzkQrO7RXOTZLuqyuua+4tbfDyLzZ98XNyMvI
+41tBOjkzNC4zMCstMjg3OT5DSkI6ODhHwL7C51drzdPIvsW0wri7wLq8zv9fSbq/07ndUXdj
+WbO3y7W6ta+str7BXdfXX+fd2//VO09fSVc9LS82Li0uKzA2LiosLzc+S1XrzevAy8e7ybe3
+u766trOxw8PN49nVWV9rUe9ORknf3VfPV0/Pyc+2ub6zwbm/v8e7vtfZUVNHTU8zRTo1Pj47
+N0A1MSswOjQxLjE4QVVn3by2rqysrKysrKysrKyur6++xc/jTF08MTEpKiktLygsMi89P1c/
+19HIvbyxs7mvvb/Husu/zedfUV9ITUVOQkk+Qz85Pj41RUVGREM/TWPdwb+2rq6tra2tra2t
+rbKyu73DyF9PQTg5LCwrLCwtKiowLTQ7PkZbW9nFu7itra2trbK0sbjBzP9JPT43Oj0/PDY+
+OUBDRUA6PTQ5NTE+PDw+QUlvwrmtq6ysraysrKysrKyytriysrS/70w/ODYuLSwnJCQmLDE2
+QEDdXfe/uq+trrevsLOvuLC7w8/dTz5CNTIuKiYnKCkrKiovMTM9U2PLxcG6uK6tra2tubm0
+uLOwtLa4vbq3uLW2ur7Fvbe5wru+w8xbY1P3V000MCwrKikoIyMjIyQrMD1NTmfZxLWurK2s
+ubm5uLOzv8PO1ePN29PvVzkxLi8xMjIuMjU9Rv/bxry2raysrK2tra2trrG2ur7R51tFPjkv
+KCkpKCYnKycqLTY+TN3r07uvuLKtrq2tsLCurbG0ucHDd01KQDYyLiwzLi81NzU6P0JPU0pR
+V11d493PxsfGybyxtrq/w8PHzMzO39fEa7+0v1nn1cnNwL3AxcO9td9vd9fPXU9C3W9DNDA0
+KyMjKDI3OFHFwrm7sa++y8a/tr7CvLKusK+ytsfbTDstIyIiIiIiIycvOm/Py9vDtq2trq+t
+ra+xubu/tri5zOvN50Y7KigvLCwxODpE58Wxuresq7K8u864xN22s7y3vc+1vPdJPC8sKCQu
+PjA6TkVCQ0rrTTMvLjsvOUtnzri1sbbAvLHC513fwN3/ydfBvrq3xru2v7u6tcHfX1dVWUJL
+RTZAY/80MkdINS4jKCowLCpCZ7vBrbaurbS3t77rw8O7vrmzra+ttb66ylc9LiojKSclKy0q
+Nkb/vK+sq6usrK6wwN/J70VRQe/r/1NrTGc3OSszNikkJC42PTlRwK6svq+svqyuvsy/u7TI
+0bTfu8JJTFFLYzguOCwzOCg6SDJAP0bb693FyK24u768s7PE0d3Xz/fFuLjVtLnHrbi/urS6
+28/Ryf/rTEvCzDk3LzM7KSMvYyszSSw/40Qv98pbUTf3WdPKTcKzu7OyvbW9x7/nzUdKPuc4
+MWPrX1m9ucF3wj9M3SlBOydFxVPjZ7z/xq3VvaznS61DzUGxRi6tuzS5X8RZ374o3UEryy8z
+xlPARdnFStG0M7XLO8JfQLjvX7o/z7stu0g1vzs33zRBWyu9NbhHT641rkwvrkd3tC6xwePf
+sz9vrCesKzmsM7yxNqw2Ub++PMvKNawurj/FSLY0tOc51U02v0I81TXJQ+suvi7B0zPbYy++
+Tz+tONW9QNe6Mm+xL61nN60sb602tK4+tWdBrS+/xi3MySyuNmc/QD3jN7Uqryxf4zW9TTuv
+LcrBP607xa4yrev/rz66zkfj2z/EyU3Ru8rT68zKRMPOSc///z/rW0xKOcs6W+s7V9NOZ8Y2
+a9E9b0VPV13JyUy/U/fTxsi5PK09u8PJua1dsNXKvOfCPve/Mbc7/1n3Sdk21180xTczvyxn
+1yq/My/ZMdVKM8MyN78tV1szxDJbuS2/VT3dw8ay77TLxa7Hxq13rcC9sLdbrT+t2V2yY7yy
+/7xfw9vjPMRZRVHF00PEd1HNS85bTevMOLg4Sc8w9zA6UzlENk88QEkzUz9nNMlH389K20bH
+d120R8K7NbfTP67nVbQvTrQvsUDXV7FAtV/PvNVHsDbLy0nFPMRVY9tHxTg/vy3KPD/rxDa2
+MN9ZZ0S4QblGu9fKXVvTSdvj1T65RuPGb0u6OrxPd3fnX/dV5022W9W9QbRPxWOvQLrAwL7O
+y8i8PL1LtEPXV7k7xDjCO3fO4zq+PlnRREPXO88vQ0jrNmc9Y009RUkvuTRHdzpLPMY5TVnN
+VT21U9XXyGu34768xneyXcLIv0a6P7nV18JXukTvtj/MS89BuFtLv1PZW9tR091CyldDvj3B
+O9XPS9vKOsc7xz1jzTjTUePdVc8/skG7Mt3vb9k/uzq0U9/FQ9tB2c80uzu3OrtLzO/XVdlL
+PMVE107HSl9r3WM9W2dJyzGwLr7ZRtF360++SuPf40PjX0rD3z26QcNIvz+9Qe+7O8LvVbk0
+tfddyNndb0C9VbtVxW/TX09TVUhJ70VXY009a1lH/0rTTfe/yPfD47tnxb+/z7fA2bLTXbVX
+u0m/ScDOY9XOV1fNPms+azTPPTpfOT9FMFs9zzdJQu8+90VfWfc32TjRWW9vV9dv/2u/TblN
+yEK+90K6PLhNxM5DuetVvefbTLjvw+O7zve6v0fDVchOtD+/013CM68wska9T8+/QM5HSc1R
+711GzULKNboqzjvjTi+5Mt1vX0FK40ZX68pfXbtE28fKTsfdZ1XLVePN0zvX91M6wkxOPbg5
+RMI/zWO9SrnDxNXvvlG02f+tWbhPvd/fs0i80bpGw79Iv0vXQE9nMMs8QDZbPUlGMV9ESjnr
+TUVKWzz/70dv28NMzNtFvl/Z49fFXchvWefRa1XFVdlXulXTvc/Rzs3jwc7RyMzIwcTNv8jd
+yMm/19Pf6+dnS2/nTGtPQFlFRz9DPjg6PjxCQEJJSmNM30Z3a2dM7+ffR9/X21m81dPLz83I
+vVnTxM9O3cpTxV9nUbxvU9PvSXdVX0bfR+vZZ8M/vVPNTb8+698+/1e/POvJ28PEuc23612+
+SGM2TD1L9zPFycK/w6zjrLGu17RBz0dVIy0vLykzMDk/Sjc9Zz48Ok1OZ87NraysrKysrK2t
+usNROSYnIiIiIiMpLjVGu6+srK2tra2tr8PXXz0xMS8/SUbfv763vK/Gv8ZGL1NELytBOE9V
+38mxr7q6rb3Bzrj/t8bHtbOzu7CzvMDXT0Y0JiIkIyMjIyMjJis5WcK9rKusrKyssrK0wsnn
+38bbwevEuN9nusrXwkdTzM45Pci+Qbq7tbi1zsW541lO10ZL5+vDvLq9t7W+y9tRNCwnIyIi
+IiIiIiMkKzRLa7Szra2tra2tra+zvv9n31tRTVv/d//n12PXxc/Da8zLv9X31b/G18zByb7K
+xMK/w7/Gv8TGzt/O0Wd33VVrSk0/PTsxLSwqKCkpLCosLzEyPVXXyLazr6ysrayusrjBz85T
+Qz44OTc2NzdHRFNd3f/PvcTn08C9vcG0r7Cysbi0ubnI519ZOjo1OzxHTVn/b13vRU9FPS8u
+Ly4rMDA5QFdb/768tbezrbKvs7Kzt7m8v83Pzd3//1lIR0M/NDIyNCwvMTYzNz1GWU9j1b66
+vbGzrrK1tbq9vdfnY2tOSUlBRklBREFOPEg/TEJMTEljd9XPvbm3t6+1trW5u7zBxsLD1b/J
+vsvH5/fnT0lJOjcvKykoJyUnKCsrLzc7OUz3/09v0cnPu7q2trW2tLy1ub3Cw87N/+tv6+vd
+59HjysLOyb61ysvAv+/3zMzZx8nHwcnFys7Bwcy9x7/J09Nra04+Pzo1NC8uLSkoKSYjJywp
+KTA6OjZT2dV3u7extqysrK6tsbNFvshvZ+/rX+vIzMzEwcTOwc3IzHfdvtXv1c/KZ9e/07+5
+u8DEzNNXV11fS1dn51lTXV88PjwwLSwuKSgpKyskLio0NUZV983Nx825tba8sqystK2vrLO4
+xcPO90xIUUJFV0hn39HTzsXCzdnEU9W/3+drzOtP08DAt7Kys7y9u9l36+vfT2fC0V9r3Ugz
+OTwuKjAsJigtKiMsNjA9StPvW8+7xc3Auq+/r66vsrnAxW9RQzo2OTg9PUNZ5+fKyMHF09vj
+TT7V31E+U+tPZ8m7t66sraysrLm3r661sK2tsK6vvc3TVzcvNSwlIickIyMqKC47REJLY8xT
+0e/AxcPBtbi1wr/dXz88NDQuNDA2OEZVU9HNxtXRzv9R281JQf/XXee3uryysbm6sba2sq2u
+rq2tra2vsLzLZ04/NDEvKSYmKCIrLjgqPEFLPVnO3dO/urO9s7u9ys1jRj0/PDg9P0FDTExj
+TvdTTU1DRztPPktMUd3jy8W6s720uru4urK0rrGtra2tra2vtL3L3Vs+OzsvLyouKjcsPjlE
+PktbRkbv3dvjtLm8ubG7xtv/QDswMi8tLjQ1NkBFTD9LPjw5OzkzO0s/R9fIxL+usrGurbKx
+sK+xrK2srKysrKyzt7jMSm9INi80KyosLywwP0M+S9dVR2/v/9fGvLq0tLW7vtPbTUU7PDM1
+NDU0PTpGPkxISz0+PT41QUdFRc3XyL2vt7Wvr7aysra4sLWztK61r7m3zM9OSDUwLiwrKist
+LjA3QUBXTtXI0d/Fv8m/t6+zrayttLa82U9JPjMxNTQ3NUU/TUxXR05EPTs7MzpERUlf08zN
+ubnAvLS5xrO3u7qvr7GzsbG4wr7NWzU1NTUmJzA1KzI7Ty493/9BRr3rd8qusruura28uLTM
+T013RDNEU0g/V91TR0dXOTI5MzAyPjZB3eNNb7rZzbq3vMO2vMW8uLq2rbK1ubG6x9FZSjIz
+LDE0LDQ5PDJNRVNOwsDvu7u4xrKsxrfIrMPGtrvDyV9bMzQtMCktLTgwOU/nX8zrzkxXQ0M/
+PTpJREjKtnf3v7nv17Szvr21usK7uMC1rbTIuL7Rd01JMi4tKjEqKj0yOT0u32NB48S+wL+t
+u8awr7fKvLPHY73B3V9r7zw2QDgzMTc/PEDr38/DwdVj3e88QUlAPDw0VbxrW86vvdG/rF+u
+vbivwa25ra25vMDrxkItyy8pVzw0KS9HNyZEPC8yukPGz7evSrmw289Zvchb27lnudPrdzVJ
+NSs0NzJBT/fP57i93dtXRzg3OTw1S19T666sw7Wsrry4tb63srG1rKyysLG2y1dPSDsuLC4w
+Ky8+LSg2NzUuOltBRsHT67muu8HBs8LOws7Nyr6+yL6+y29VSjs8PDk8REZn3dvX4+drUUU+
+OTo6Oz1BQUlj19PbwcS/tra4tK+ur7Czub6/yf9nW0A+PjgwMS8uKy8rLTE2Nj5DT+PT08e9
+vb6/vL68tbW0r6+wr661ubi8ydHP3+/bb1tbWU9KTUxDRkhKSEpNS0pNRkFMSkVJTkpJW2/3
+48rHxMDGycbIztfXd1tXSz06OjIuLy8sLi4vLzY7P07v18S7srGwra2tra2tra6ur7O4ur/L
+1e9LQkE7NDI0NTQ4OjtBTFdXX/9fX19KVVFCQ0lGRGPd2cm8uLu1sri4tbm+v73Izc7VW1VR
+PTo8OzM1MzIuMTU0QUh30cS7s7Cvrq2vsK6vtrW1vsfGz+dnX0w/OjYtKyooKSkpLC41O0BT
+Z9fOy77Fvbm9x8fK39fRz9PGv8XFwcvKxcPTycjJzsvK693bZ01ZV0VAQjsvNDMuMT47SVnv
+1cy8urWzsa6ura2vtbW3u73Cw9vvXUM5NC8uLC8tLjQ8PUJVY93PzcjRvsLR0+9jTF9bTevR
+y9HJy9nLzcvZzcrTycvR5+NvT0pIPz86NjAvLi0sLS41PUhP78e8uLSvr62tra2tra2tr7O3
+vsHLY089NjMsLCwsLS4zOD1ESuv/09PFytPN79lH72/32dXFu7+8vrvCv8a8wMHCvcvV09tP
+U3c/PEA6MisuKyorLTE4UVXvzbi8tq+trKysrKysrq+0t7zI4/dbPjUzLysuLi0tODc6Ql1X
+WePJz9vT71lrb0gwxb1B2f//tcHbu8+6tcGxvLmvvcnTz8vZZ1E3Pz0uJyQkJi8xKzJHY2/B
+zbWtrKysrKurq6ysurO6xGdTPTg9NzEwNTU4Oz00N0BAN2dZRT1KTVPvxtHKtrb/99Vn9+/3
+27Gur62urLC4v9VHyT8vPDxILygpJC9CRiw0U0H/TDhfra2urbmtra2tt7TBx8VnNTpLX1tV
+PUJrz0hCPjdDQDzn/0dLY2fd583BvrrfPT1BY+/398ivsK+1trm8tu/v3/dbPkFGPCsvKzXV
+PztVN0BOMUqytcG0t7y1r6y8y7z/b2tdQkdf109FY0pnTzM4Nzg6TlVdQ0FE/7/Bxbm6zutv
+b+fXtrjIuK+tsrq9vt9fZ0pJW0Y5Ni87My9nOiw9Y1M7PsO7wsm9v761rq26ur9rP0pZT1VV
+42/bzspdS0E1ODg6OkZRTEpMZ2fPvrvAy2tb68nDu7Ozs7Ctra6yurzKXV1bVUAxLiooLi8t
+MS8wMjM7R0tv0cvCvLi0uLe4wsXI0efTd11NSUdDRUFDU1/3TmPr3ePX029vWd/f0dPT92Nr
+XdnPzs7L1dPGwbq4tLW1t7a3ubrB1WNKPUJANzUrLSwrKisyNzs3O03/zMC1s7a3tLS0tbq8
+xtNjV1NOSEY8Ojo6RE1PUVlnWUhGTVtjX1dOV1Pfzce/vbu3t7azr62ws7i7vb7IzutfQDky
+LS4uKS4tKTAwNEhFY9nGvLyzra2tra2ur7G5usrbV0U8NC8vLi8tLS8vMzg6R0tT/2dv2+PV
+ysfOxcfNwr++urq5t7a1sK+urrC0try9v9XZX0Y9ODAtLCoqKissMDI5Rknr1cC7tLK0r6+x
+sbK8vsLZY19HOzoyLy4tLi4xMTc5P0pba9fIw8PHys3Nys7JyNnTysvNv8PBuru6t7a1srS2
+ub2/xMnVa2NLPTg0MTAtLi0uLzI3PUROW9nKvrm1sbK1srS4urrI28NIQD4+NTczMTEwMjM7
+PkVPXe/ZzMS/ur3CvcHMx8bXzcnT1dPV29PGxcXFx8PEv8TBwMPHzdfrX01JPjg3MTAuLi0u
+Li8vMzk9Q0xd287Evbm7ubi6urq6vb7D1ed3V0hKR0dJR0ZMV13n1czCv7u5t7e5u7y9w8fX
+2/djSktJSz/3JNW4TEkqTLO8/+fPysnRzN/f61lLPD5NPDw/PDs1Ozo4Mjk+RUhd3c/Hv7y4
+tLCsrLG+yce7u7nD11NJQD9IRUE/PTY3SVnZ3dnbycm/wsPPW1dXUUxBOz1AR2fHv7/Bwb++
+vr7GyWtn52ddT0VDSllbR0Q8Oz5AP0JFVWtNW+vEvby7urq4usPJyMrVSz06Sd3Kz+d3a/9j
+W1VJQDs8RlN35+//08vAv8jTXUs9S1tPPjk5R9W7s7G0uLe4ur7OX1lOVV9bU1djZ1lbWU9H
+RD4+PT1BPT1IW1lVXdPHuri1usn/18jH1Ug8Ru/Dwszr/9/n52dORTo0Nz9TX2Nba82+vL/O
+519FQFNnSzsvTN+6s66vrq2srrjCzdv/VUxPRkVZX0w/QkpTTUk9NThATUs6PVN3y9fTxrq3
+ub3Fz9/r0eNLPUld99nIy9vf2e9ZR0Q+ODQ1OkRRX2/Rx8HDxdHfYz9HX1s9OD13wrm0srCt
+ra28yMrM3VVHQ0ZKT05APD1LS09IPDdMWUE2O03RzGNZY8+1srrLyMDD5/fJx+9V793NycfH
+09vb31lERj88Oj9DSk1b59fn2dlr/1tITW9HP0hfz7y2sLC0sq+3vr6/wc/Z419XW11AODg7
+PDw8OjM0PTw3OkRGR09n59HAt7vAwsHAu8XZ2d3VzMfFwMLMzs/nWVdJQT8+PDs9P1FZXWvb
+729nX1lKPUffY0dHXdvOxrq3vbmvr7e4uLzAx8/Z729j71NJSFVMQT1BPTo5PDo8PT9RWVVO
+58/T0czN3fdf0cxfU+Pn4+PT193359/3T09RSz8/REZHVWvr79HJxcjNysbN0cvV08TKzdfL
+x8rIv76+wL++v8LHzdHd2ddvV0lLR0I/NzY0MzQ1NDY6P0VLUVtfd+/d19PTz8rExsXFw8O/
+xMnHy87Tztff619PTEdKTUxKS05VVVVPVVVZU11rX2/33dnP68rRyMnOz9PX083Jz83T19nO
+0dvj929nX2NXUU9LT0xMTUtPUVNPT09NUUlKTUtTT2dd/9nOycvGwb68vL2/wMTByMzR23dn
+TkVJRT8/QEA+PkFAQEdLV2v/1czFxb/Cv7/Ew8PFx8TIztXX19vd9+//7293X11nZ2tnW1VV
+T0hFRUlDPz8/P0E+PkA/P0ZKV2dv4+vR0c7PzcjBwcC+vr29v77AvsbNztvjd11dT09HTEhM
+SEZJTU1LTlFVVWfd19HPzcTCv76+wcTEy8zOzOvf42fjX2NTV1VVT09NTUpMRkxGSE1JT1FV
+UVFPVVdOTkpMTUxLTlFbXW9rd3fn59vT0c/RycXBwcHBv77BxcHBx8jOyNHV29vd/29ZVUpK
+R0VDREhHS05TXW/n09HOysbIxcjJy8/Tz9XT/2f/V2NXTldXS1dPT05NT01IR0dGRkdLQ0ZD
+Q0dJS09bXWN379fPzcbIxcbCv8PCw7+/wcDEwsHIyszP0fdvZ3dfS01KSkVKSERCRkZGSk5T
+TlNNT1ldb/fbzszIzMjIxMjFw8fIy83Z2eNjUU9TTU5MSUpHSElCSkpHV2d3d+/n6+fj5+/j
+/+//d//j39fb0cvNz8rJx8LCxMfGxcnGys/Ozd3f4+NjWV1RR0dGRj9EPjw+Pzw5PDs8P0RG
+TFddU193///n39nRz87OxsfGysfM19nduTQ9rL0lPqw6P63OQcJrRcTnOee6PWe16z+6ukhr
+uchK3cpn99VZV9vZV3fD91nKyu/Iz3fX1Wfn13dv0+dX1cxF38dnQWtVPkZXPj9dRzxFRzxM
+STk8Q0o9P1tHTs/r78TBwLvCwLi+xsbC32e/yk3CxUe9vUfd1d/b38rTb9m/RVtbxU41Qm9N
+NUs4a2M72URGvtdRyNXPvlvfxNf3zuvLzcvN/87I29lb3evbVUpX701GS0dTU0RAQ05PRUxb
+a+/X0dXOxr3X473BTcNO28TvRs7GTsHbyL+7utnCv9nVWWdBP04wNTQ+LTo7PNs230fjW1Xv
+QWdNz01ZzE7E68G+wrzFvb2/zdPA58nJ113n1UVTRjs7Pjo3PDtFSkpX3cO+xLm5uLS5xbq7
+//9Za+PZzNW+sr68tse7w7/Fd29PU0M6QUZVMURNPDE2TjswRDs9QVM+Ud1TXc7Za8HF09vH
+z87O92/j/9NDX09d/0FnPVk/PEY7TjrvXc69z7rByLa51f/PW1/fTU5d67rNRbDFtsC82bjA
+N7gyrkU0rS65vz+8ONFCSzs9XzjbPk7vV9VPz8n/uMvCvcLJ48/Rd9NKb1HbT0r/V3dXS1dL
+WzpFRUhHXz5n929jX8VvZ7hLY///OtNON79Eb75NvTe1rS+sPUu3Sbw7sV1v2edNW8I1Vc07
+Y01JUzLMMkfRP+9PyVPIS2/VTcBjSb4971u/5zm2SLs8tTeu2d3F3bjV48V3zcLAyNXJa7RO
+u1O808nPXbZGu0O7QUe4RDW4L8tHZ18yzUk3Y/85ty+5Ns2/NMvPLbY0zzpMTDvbvjfr5zvj
+Z09VWedTTcU8yjWtMrvFP7hLd7/du75dtUTFPNm9S1XDO7itLa0rTbUvrDG7ObzEO79BPLYu
+rjbJazu2PlW7Lq850bsurjS00UC8Tj+5OLZIV8I1w0XnTz3AN8LPMK4vRb5Lu9dRyUPPZ9td
+RfdJzkhJ43c/uji0OFm0LrAvtzS878tdU8RvUbsvrje3vj+0OK5IxcJCskS3/z2yU83B3U/f
+vf9byj23P8rLQspOd0pjP2s5zU5GzT/NP91K30xGzDjr1TTP529G21PnU2NHR+dCvNtHxj6u
+McFfW8nVzMz3utlT18pXyMpPxT5vwjmxR0+9P+vDS7/XXbtO/7hfz64urDg2rFnjsTuvb0+u
+U3fTRdV3NP9TP7pBVd0860tZ69s4vj0yvi22TU7XTkrLQkK9Ql/EOlu9ObxNR68vXbhM2blI
+ulu+1+/Zxv/M2dVZvVPP485dOsF3ObpTV0PPUz7vW0n/Re9Ld+tb/8E/zsJj3993zF9r33dK
+51l3/z3ZyTPd10RvTtFbTt9NV1tf59d3d9nKd9XVy83Zv8vTwMDnwc++w2vEzNPj0+vn72vX
+a2/nY+fnT89Ma/db/1Nf/0VfX1dbSN9XT1FjVU1vSmtOX1lrW1tZWU1ZTFtOUVtPSlNPV01b
+VVtXWXdb71Nr6/f373fdY3fR/9HX18fZzNPR1c7b09fjz9fbytPGz83Pzt/L39fZ59X/zt3V
+y9fK08vR08rXz9HV2+Pd6+fva2ddXVtMWUlMSkhJQEtEP0pDSUpFU1VLY0v3Uf9OZ1lRb09b
+/1vr/9fv1+fd3+/d5+vZ79vj29nV29nd7+fd99l35+dv5/fr92v/Y+9f/2tn41vn929r713/
+d293W+93d+v34/fZd9v/69nv59X/0fff3dnj39nr3edr33fj9+dvY2tra2//9+fb5+vf3+Pj
+293b09nd59/jb/9rb2dX91NfV01PSUhPSk5JUUlOUURXRVNRV11VY29rd+/r6+Pv6//392f/
+393Z2dHZzt/N19HX0dfZ2c/b18/Pzs/O19PZ49tr72db51n3W+9jd2dbXVdfVV9Xa1N3WW9v
+d2v/7+tvb/dj62v/b/f/39/d2dHRys/O09PO68/Xa9XM21Fv0dfvZ9/3VVln3VlG511VU01f
+U1FRY1NNUV1vQ0xOTUtR/09ZWV9ra/fv52dv783b9/fb19Xn1dnv0dPNb93Dy2tRzu9n4/9v
+Sl3d31nv229ra19bTe9vZ9VTSVnf1WPZ79/fzdnI2cnJzdu4tlNN2b7CWevn31u7ykFJyLd3
+SV9Bvb9IOUZXzdfPQzLNs0JHbztbvko4PU3V7+c4OufBVz5L51t3zDlOZ19PzOc0Y8vO6+tf
+xMfCb87BwL1rx8Ovul/Zv7iwvUpTvsvE01M3PN/Pazs6O0XGVTMuOG9dOC45R01NRTw6WdFd
+WVnr2dHMw9vf38avw//fua2w2Wu7sL13zsnHx79XWdvKz87rd1nZysn/Y+/bz/9ZVUlI79/3
+R0tO59XvU0pXb1NBODtCPzw6PUVOUXdNVWPVW1dMPkVGRjpBQUld11Xfu7y+wte+tLLI69fH
+zGNFQ1nX09v/zsvT09dZ2XdbY0c7TM/n59vTua+z2cO9tq+2vrm0v8bC1/9JRkpZSz5H07/D
+62tXUzkrJCMjIyMkKTM/W8+/vLmsrMw0MUfIzEo097Kutse7tq+z0Wdb62PnUzhIXdfJWU/V
+vbvDTnfVXetbRUn3vrm+xL+/w+9HT+fbY1lLQjo0MTAvKzI3O0v/ybCssba6v81ZOjAyMjI2
+WVtbsKutvLyyrKy+Ttu/vVVfa8u5sLTGxr7CyFUzLzg5MjAwPVNJTj083+9N6zkvQD1KZ05O
+w7u1rLq/sbr3zcdJQUhJd8TIzL3AxNtnRjAwLzg7QvfZvry3tsTG609VOikmKjNLy7Wxr7G5
+y2M8Oj9G2c7JuK+0vr3Zz7XTNjw9Sus8WcFfr706PVPj4/85LCxEura5vMKxr8w6LDEsOjIt
+Qe+1rKyvsLW4yz0qKSsuMzY6W7K3rL3Ar8HrSj490brPvbG0vMG60c3G09nO0U1rX1NvSi4v
+vGMzRDY231MyMkRXPjdFPsy9XdlTza25yc0+Y7q6W0pL08vJy9m0trO5Y0xOOzIvMUV3xbiu
+tLezr8FvV0EzOjYtQHdn79VvR/d3PTxNUUVRVS9My1PXzNHI519XTUdLTEhFVTs2TF9Dyb6+
+3a6svK2tvMbEubatrbiurbbBuNEpOU42Qkk3RcbAxcdfR0U+KiIiIiIkKCtE71dXP0xRY1E/
+P2ddu7G0r6ysrKyyt7Kssba1tL/Dwc9vuLGxv2++/8pONElBV8vVK0BNPDQyMiwx7009/zwy
+Ny0nLSgoMS43PefTtLy7vs/Oyms+PtW8urSusq2svrq6t62trq2tra2tsrCtvL++v9W972Nb
+OkE4PjY2LC8mKTAqOS1ELjs+MC8uLC00MkhKTF/Nyb3Xysn3V9e+X83nwNu4u1m9zr69vv/R
+ur+yt7e9u7rOvtHP979fs8W+xd3IQudPQlXvQEc7OT8v/yrRP78/VetEyEvHXbjDtd+zd7W8
+00TRa0//X8swxTlZPl87YzFfPj28V1FVZ2/fyUpJd7pOzcpbyLvRuFevV7p3tj2uvb9vz8JM
+zF+9M8VE2z3/O28v1S09M+swRDxNNkJOW01Pd0/jWVG6S8+3L6wvuFe3b1esRqw3rUCvrj+s
+M7rfxFdVu1GuNbU3uEW6OcYub0NZNT1KN9sw2Tw8SUw6Ty/OML9NPdFM39lFvTjH0cjMzLrB
+wbbEuL3XrUyt67atv7nCvrzbt1e2UbpPwMzZU933SEhKvDfvN0JAQixGI04qPCdCJElMJFkj
+Uyw+MjE2RT88XUhHxj+vU7m3v7usa6zMrLutrK2srK6suazLrLmuv66turO1tOO5ys/FU81M
+aztKPzdAKUsiOSkrLykqLCorLihHI04oPzI+QDPPOl0+znfnw3fCzLr3rf+zuq69tK23rcet
+rrmsw6y+razHrMuzw8y6b71L51V3Tl8+TTfrMGMyNMslXTAvPj42Qjc1QTFGPzrVOOM/V1tZ
+02tOv8vTtlW2U69BsUZRt1G2d8lfu8jJV8Bv17xEvdHTxlm4S8nGSrpKtc1nuUK3XcFjRrhj
+d87ZyffZzUnGT83jb1lRxmNv2VFnU2tRWU9JYzxrSENJPUo3STc/Qj5MOlE8RERGTU5bW2Nf
+d+NV2V3Z/9Xv2dXNy83JxcfGwcq+xL+/xbvJvsnJycjN38rZyt/Ib8/r0+fb3+/jd2/nV+9n
+a9332dfv0+f/11PbX1tdR908bz1HTEFRQltAUUhbS1FKSkxHT0RJQkZHPko9ST5ORkpMS1lL
+61nb49/P0crbxc/AyMHHv73Dur+4vbe3ubK3sbK2sruzu7m7vbvDwMTIytvb43drT1lGSD47
+PDk1NTIxLy4tLC0rLSwtLS4vMzE1Nzk8PUJHTFFVV19v39PJyL+/urq2tLOysrGvsK+vr6+v
+r7Gyr7Wytba2uri7u7+9xcfL0dfvd2dXU0tHQz89PTg3MzQyMDAuLi4tLi4sLi0vLzM0NDk8
+Q0ZOWV3vb+Pf1cnMw7+8uLixs7CysLGzs7W2uLi6ube3urW4ure9xMbX2eP3U1VMSkpHTkRO
+RkJFP0E+Pz04NzY8QUNCPDg8NT47Pz8+QkBJTedRTuvDW+9MTP/Ru7S3vsPDvL66s7a3tra3
+tri8vMXIysfX51VTV0lPW11dVVNMSEU/QkZFSkNAP0hGdzFAQFFvTTUrR0m+u0s7191Z02c3
+SOutray5wE/fz7m+uMK90761s7m3srm9wctPQE5TU2drRkhITFc9MDMxLjE2LiwuLzM5ODg+
+OkI+PkQ8QlNvZ+PXy8S5tbO4tbOvra2tra+vsq6zsLa0tre5vL7FzOP/X1dOQE1NVUt3OkVL
+RFU+Rjg4OD01OTUzOD9ER0I/OzxFQ01NRmNrX9Hn2d3Oyr+9w8TMwsa/vru/xM/Ryc3Tb2tX
+d8zMzF9CPT1K9/9XSj4/P01nZ2dPa013b8zZ59/j0c7KxtHr09HGxL7CztvV09nXzNv/X1dR
+R0pKWVVfW0s/QEM+QERNUU9OU2tnz8vM0d/P48rCxMnPy9vDz76/ycXPz+PPZ99bVWNO03fr
+b+vjX+PJ42Nv63dTd8pHPkZZT0vV70I+RD44P29BPkU8Oz1KTkZDVz89Y0FrX29b/9nXvMm1
+x8nFxbS8ure4vLXHurW7t8OuZ8W+79fLuP//290yRUI7S1VjSTdCQj5ZSkBROVs/V91R39NT
+72t3yte7xu/TTE5v/2PT0dHv68hX49vbd1nKTFFfdz1MzndPX8hCTUfjd0jIwzhdy8vLwr9n
+OjI5Ljzd58K62ctryr/AvbxfTEIvNjZFY3fLyGfH4//O3cLDzLvDxba9urvBzmvX51dj61Fr
+XV9fRudLRllEPj48ODUySER30e//a01jXc3Fx7TBur/J18fjxcW/v8LGzu/PZ2NfPjYxLy41
+OknfzMPJwsvR31NKODI1MTtOY8m7tbq5ubm6v73Oz+9f2dPHvLvCwutjRz9HOjxLPD9BPUpZ
+/+Pr998/TEk//0/fY1dXU2/O/8y+077Gv8a+ybrGxs5Z21VVw8jJusvK1Vf/X0lrU1tHVV1T
+/8XIwL3Pyk9MPzY6PTU8Pzg6PEY9SO9TU28+PTo2RkBNye/n505XX+Pnzb/Hy7/Py7y+t7K6
+tbq+u7i5trC4urO70bvNx768wsnBy9PFymfOW1dGOTQpLSoqMC8tOS0pLyUqLS04LjM0LTw5
+RcPIuLS+tsPVwNPbucy8z7q+wa6ut6yvvLW9triyrbG4tMnHyNHDyczEd9vnU05IRUo+Tkk3
+SjtDSUNJPz5DNjJCMTxKOEU+PEk9WWdTXVVIRD0+UUPXzM2/yN3dZ9/R28S9zsLE1+vNwMe7
+s7m3uLm+vsnKz1lrR0T/a9PHu8vXa0Q2MjQuNjg6PkBIX1Vny9P/zttLS+NOT9/ZW2tv53dT
+udHvusVn03fPy76tsbCsv8lnQD02Q933wLLBvsrTd0RIRC41NCsvMTc9OllGPEc5MUc7SdHJ
+uLi0r7i1ubO6vL7KZ1dPNlNbT7/P0cXf491rWff/3ePLydG9487Ka9vV/3fX/2vvTk1OT0Hr
+TlPXTUxvQjlZP0RGUTlFTjZGRFFr787BzO/HU0Rr79/HucC1sLS2tLO8v8PO29nR19vH2d9d
+TUY+PkpDRvdHRO9FS048RTc4MzU4O0NFWWdfVev3SXdna+d3zd3NvMfGub7FuLzAw8O9w729
+xM3b2V9r38jFs6+3srnDzt1PRE1BS01LXVNMT0hJR0JKTT5fSExIPkg+QUdLV0//W0xLQDo1
+NS4vMTA0PkX/X1/vVV/3783Xx72+ubK5sLWys7SwtbKztbi/vr+9wc7TxlvLwN3H1dFvV189
+RmtNR01FPlNFSdNK0989Pjk4MEtIW8PFv8fXVUQ2Ly8sLjlFTtXJ1+9KPjo2PExC0bzLtbjF
+vL6+vbu4wMHM3dl3373IwbvRXWffTtvOx7/KvVvn519vXf9RSEZMRk5r6+vZ3V9LSz46TjhK
+90fKX1P3T1lZ40v331vPycjIyePr10tj3U7ZwdfdyF9fzt/b2+tbTU48TzxNd0/r9/dV60RH
+RD9NQt/jy8fByrzXv85vylnNzM3BzL7F68lnUdtMz9lbxd/Gvby7wsrvPFFKOHdja8/X29vv
+Z1NEODMvLDA3O2vLy7vB2VM8Ny80NEFdV8vVd+f3Rmt3Quv3Z//J1b2zurCvtbSxuL2wubyv
+uri2wMfZz01fV0BTPEI9R0pIVU1DPDoyND42Rk5DZ0FVSkRRS/dVZ1tL/0xr29O+vrm7w8nv
+WVHj69vJwtnM52NNS0xIUVdO21vO09XKwsfRwdvj2dNvyef/zNXK/9nZd09jSEVXSetOa3dK
+PUBGPlP3d8vMXedJR9V3xcDJvmPVyGfJw8XJusDNxcHJ0dPrT0tZUU9VW2dORF02NUEvMzk7
+PT5KPj49OkdBTc1n38Hr38rj983J2dW+zLy6uLC7sLe9vMHK0cd30/ff4+vVV9Vba2NHa0FJ
+Tmf/zc/IyddrWW9jb9Vdb+NRSE8+PD5KOD8+O01ASV9fV91rY8bX0b/Z58LJ47/b17xnzcpT
+3/dZZ+Nd2+9X411j/9fZy8L349NLTldKXV1TTVtZPWdKQsj/3evbW9vb38/O38nNd8zrzs/R
+0+/PX1HdVW9fU1VXRExrTV3/T1lbTEhKTD9RPkldT1FV/0vd6+/PyMDEw9PLz9PEvry2t7e/
+xs/ZZ2Pn5+Pfz93b3+t3V+t33dfX2WPjTmNFRz9CSzo8Qzk+PTpKSDxIRG9f0cfJusjLw19n
+791vxcfJxdfT129jUWdVa+NR0U9V3UxV39NZ/1tPZ1/r3+PR4+fr/87J4727xMXI0dvOV+/X
+SmdrVUhjSj1jTU9Vd0Z3d13X28vGztfO1Wfd213R02fMd99fd/9XY0JbSEFPRlFMZ19NUUFI
+P0pCRVNJd89db+fPzt/CysHMwr/Gx9PIU/9FXVtP0UfrV11nWVnbz9vIwse9xMPGycrJxc7J
+02vn993vyWfT42/nW1lZ30/nTkx3VVFbT0RPX0J3UU3jb3fR/+vP41/vW29ZV1VKVVs/UT9E
+Pj5AQj9FY0Y+SENNRXdf0+PAvt3E28/r28LOucW4uLm4vr7Gyetv31f3Y1NnTutfd11bW1N3
+39nZz8bNv8bIvr2+ubzJxsF3zW9b60lrZ0Y9RDwxPTMvPDg5OUU9O0hIY0ZM1+Nn181va+dn
+6/ffx8zdwMzjvsjEvsfGvd3TydfJwsx3x2dVy0bn52/Pze/nxO/HyNfIzNPbxmNXX0lXSUZO
+O0Y+OjM5OjA+Njo2P0s9TUpTa2/r3dPPyc/fyNnKwMfAv7zFvr7CwMTAwsXDw8PrzMLbytPV
+Y9n/WWNrze9T/11VY1t36/9TZ3dPWUZFXT9EP09CP1FITklXTlvnX/fdz8njw+fr191fb/9T
+719n719Ga0lMVU9db29da1Hn/2Pjz1dj1/fnzd/Gz8/ExMe9vrm9t7rIu7++wMfGzdnZ593n
+W1dJPEM+Pj05PDwvMi41NzY6M0FKOj9PSVdP51v/b9nC08jFvr6/ur2+wc67vdO+xMa5wL3I
+xMbEycfK0cjI283L18TE1cvGY9XXZ8tba11TRkhMQ0VAPjo5NjNDNDQ5Mi4xLi1DNTE+OTU9
+PD1dQ13VW9vRxufEvse6vLaytrWvtbuyu766ssK4v727vrrDwc7PzcTH09PL20tvW1lZb1NZ
+UUhTQUdKRUdMTj9VS0pKSENCUUlnUf9LRktTSk9rY9tbVU9IW1lvb9/Za9fn2+PR39Hf1dnn
+z9/T0crVzc3Ozdfd7+tdY+9v919XUV9OX2dbY+NXTndNY1dXU09PU1tvW2v3TmddV2v/1Wvb
+7+/O09++y8m9xsu+0cjE2cPT79Xb/19Xa1fOb2ffX+PT1efvV9NRT/dI3WffU0nVQ2tP91lv
+d07vUWNOQddL699Db1tHz0rN2UvLZ9XRS87L3bpjycdfvW/v00vMUdFj4/f/V2fdP2NnQONH
+U9U9bzw/Tzo9U0I6RTxEQ0ZTTUrKS8vT3cHb0cnJxsS908C9ybS4wLTEurTGsbfGsr6/u8DN
+tcHNusvNw0zDbz3dQ0FnQTJOLzU+LTk0KT8sKz0mPTYzQy87PTlMVU7KZ9PPyd/AxdW5yr+5
+xLm8urfHssTNutO9vne14/e478O7/77dSbdJ0chN41tE70RXSD5nPVVbPllJSFtO7+M80TU7
+3zDn4zbnO0xfQNFPTctJV8VEyF1nzFPGvlvB09e9yrSy16zfvbLXtbnPu99bvlPHzk3LRznE
+M9H/NtEw/1843TxDX0BXTkBRPUBdP1VXQe9FRP9GV+tCY0dRWT9jY03PY2/D68XI47rHwba/
+urfBtrrFr+O3w8+/0du5Z8fTWb9f3c8+6007Zz4+XzdIPjNHNjlGPEE+QUdAS09OVWtX12Pb
+yufHyf++yr6839HK68fA38/302vIxsDv3VdRz9nja193T2PP/2tNTVVn4+NTTEtLVU9LTkVn
+XUzjSV9ZWevf5+f/779d09fv1+fNyeO+42POW9PGy85n3c/rz/9P219Ty1vXZ9VnX19dY11j
+U+Pd3efnV1/fV9vfzXdfZ11f20ZO2U3db05ZTV1X/29Na0xRV1lb/3d3Z/fX29PX2/fZ99XL
+19HEzsLVz9PX383J1czb3dtXd01j90ZMQkBPREA/P0ZRRUxFTFVBW0tb71Nvb07f29vRX9nb
+zcD32c93zevR08zG0ci/zcnT58/Lx8DFxdvV2dXV09fX49fn51ljXVHdZ9vbU/dVZ3djWVNM
+UUZMQzpCPj1HQEpJREhCT01ESE9N711VX0dVXXfd293P79vbysbAv7u+vr28v8LIzcHBv8jM
+2d/j13ff2///Y+NjZ2dbXW9LT19OV1tNT1VVVU5NSkJORktTSkZMTEhKRUdJR0lRTlNbW2v/
+49/VyMfFw8PAv72+vb+8vr7CxcHFwMHJxs3RzePj//f3a2tRUU5DR0ZCSkNCPT09O0BCRVVI
+TVNRd1//193V0czJysvP08/Z1cnn2+Pv1etnX2tvY29ZW2vn//d3a2djW01NTElOWUtMRkxK
+TE5LS1Vv6+PXz8jEzMnEyb2/wMHEx77EycPGy8rJ29fb/9f//3ddb2v/X1lZW1VZUVdXd1tb
+TFVVTU1bSk1NU1VPX05fd1dOSk5NVV1ba2d34+/r6+PVz8jHx8vHzcnT19vj3evf5/9dV1FT
+UVtTTldVTU9LS09PW1dfX1dfZ2/n5+vr3ePb3efX29PT08/b39nn39PbzM3NzMzRyM/XzM7P
+29fd09fZ3+Pva1tVTk1MT0pGSUE7Pz1KS09RVV9nV2N379/f49nZ3dPOy87M0dPV19nP2//r
+Y11fb//v6+dva/f35+9v4+ff4+fvb2tnXVlTZ1tdb2N3a1tjY19rW1tdZ2tdb+/v32tra2v/
+/2dfXW9b92tjVVdVT1lXY2//3efn59PNy8vLzcnJxcjIxcjOzc7N2+v37+dZVWNfZ2NdWV1b
+U0xOVVVZUe//d+vZzdPOz8zM08vNzs/b0dvf62drd+9rV1NbV01JS0xVTUZLTEpMSUdKQD49
+Q0lDR0VIU05OUVtb/+vf19vT19fR0c3Nz8jIysfJx8rV39XXz8/KyMjCx8nJ1c/Nys7Xy8rJ
+y87Ry9fR0dvdd2tnY11dV11bVUpEQUdEPzw+QkA9QENGSU1VTldrd9nVys3NyMXDxMXFyM3N
+z9vP2d/j9+/vb/9fZ2djV1VZU09XWVFTTk1NTUxOTU5NS0pKTE9ZX//d3ePdztfT29fR0c3X
+0+d3693r79/d2+d3/+f379/r9+vv5/f34+N3W2NfV1t369/r6+vj2dnf29nT09nj29PR0dvd
+29PR2dvXzM3O2dPX2d3V2+vvd1tXWVlNR0lJTEZBQ0lOTk9OV1tVVVVjZ2t3d9XZ09XZ0d/n
+9+v/b1lRS0xLR0pLS0tKSE1VU1NOV2NjY2/r69/Z0cnNz9fRztfd6+v3b//v59nX19PRzdHV
+09vX2dHO087O0c/LzMfJx8rKzc/b3dnT19fVz87b29vj629fW1VTW1FfU09RTExLSERAQT5D
+QD9DRUBDP0pMTU9NXWtjY1tnX2drb/9vd+Pfz83Kzs3IxsPBwsTGycPJyMvV1d/f62dvXVdZ
+VVNOT09PU1tdX2fv7+vf929nY/9nZ2dna19na2936+vd49/r7///7+/f39PT0dXX1c/P2dfj
+5///73f3b/d36+fZ0dXP0c/V2dHZ39tra11RUUtLS0lJTUlETEpOT0xPT1VnX2/r6+9r/+vn
+Z2dnX2NXW3d3//9v//9nY19jZ05XXV1fU1trWVdXU1dbV1FVVWdTd9/b2+/X0cvOycnKyMvR
+zcvFxMG/vL++vL29vry6vr7Ex8vP0dvf5+trX1ldWVNPT0pBRURHSkxLRklLS01MTUxNTU5X
+V1dfVVlRV1NRV0xKR0tESEdOUU9TU1tXX11nd3dr7+vb2+fd6+PvZ3ff593V2efvd29nZ3dv
+993Z29fX1dfTy8nDxsXHxsTHycXFxMbHyMfGys7P2dvn72/v/+P35/9vWVVVU1dVV1tZX19b
+WVNRUUpMS0pOTU5MTElHRkdIS09RT1dZWVlXWV9fX1tfb3fj3dXX0dPV09HLzM3Nzs7Nz8vN
+09fb19Pf3efv/2NjX1tZVVdTVVVXVVdPT01OU1ddY2dra2v/9/9r93d36+/f9//na+vj2dHI
+y8fJxsTExMbGx8rLz9HV29XX29XV29vjb2dvY2tnXWNfX11fV1FNTExLTUxISkpHSUhJSUVJ
+SUpITEpHTU9XY2Njb2vv5//v6/f/7+Pb0dPXztnV1d/j5+vn//9v6+PZ2dPKycfGxcXJxMfJ
+yMzKzs7T0dfb39vf69vvd+93Z1NbT09RUVFNSkpLS0tRUU5PTEpMSUpPXWNn9//v///3393d
+39vd69/d3efn929v5/f34//db2NnZ+93d93V3dvd4+vj99/32ePZz9HOz9nZ429v///n7+//
+//9dWVFVU09VT1VPTFdTU05RW19XT09RXVlrd3f/b//f693n6+fd7+f/9+vvb+Pf3dnf0dXT
+1c3T0c3OxtHLzNPb5+vv79/33+fV4+/373dvb2fn7+/rZ+fj/29v52/3Y3dfV1lNX1FRTFNP
+T0xJS01LTkpRTElHSEtIR0RISUxMV1lv9+/n18/TzcjFys3RysjMysjCyszLz83Mzs3KyMjK
+zMzNy8zJy83TztnZ1+Pn793f/2tnZ1dTT1VPSUpJTU9PVU5LS0hFSENERUNHSklLTExDP0RF
+S008SEZMS0dGSUtPTUpMSUhCSVFVT1V3b//3/2/v1dHHyr++v73EzcnKwMXLzMrNz8fJxMbH
+xMDDvcfFy9nrX2dnb11TXVtOT2Nf7/dva1/n42NfZ1lZZ09NTkFKS05dXWdfVf9PWVtbUU1G
+TkhDTUxITldf/2Nb7/fj49XnzM7MzdPRzdPXy/fO69nXy8/Vy87P1c/L28/Xztf/0e/f1dPN
+999rb1tbXV1XX0tTTk5MP1E7Pzo+Qz9LSEhPU09vb1vb39PMzsPEv8O/v768u7vAvsO9x8zV
+3WNvWetnWVtVTUxbVVdXVWdX42/XZ1lPSU1LR0ZLR01DSkZEPkdKTldr49/J59fTzdXZz8jX
+53fZV19j71Ndd1lvb/d3Y2fd9+PXz8nHyb3BvL+/v7/Av768wb/DyM/b32NFTkE8Pjo6ODM0
+NzY1NTU4PTo6QUpHRl9db3fj59nKzMLGwMDDvsbBxM3Kxb/Izc2/wMrJvr/AybzMwci9wr3A
+3cPTyNnv/19fTldNVVtMSU9JRUg9R1c/SDtFPzo8OTo3Nzo3PTo9Pj88Q0pMU1drd+fd39PZ
+59fNx8O+v769t7rBu77AvMPGw7/BxMPMzNXf7+/va/dnWU9NV2djX2dbXU5VQ01PU11XUVVR
+TD9VVUxbVXfr1czRysTO09XK0czFyc/Z2f93z3fT63dra1tfV+9n9+/j4+vvZ/f/3//r4+dn
+Z1dbUVFPVUhDRkNBODtAP0hHRFNVX1tvb2vv283Fxr++vru9vb24v73Av8rM29Xf7/9nU0tK
+RkxGS1NKV0pGTUlMSUdCP0dKR11r49Pd0dV31d/ZzMnPxMS7v7zAub+7wsTPxM/n91tbW1Fb
+Z19d/3dvZ1NNS0tNU1NZT1NKSkxRTU9NSlNPTldTT1FdV1VOUVVbX2NbXf/359vbz9PrXedb
+53d319XO08XJ0dtba2dr/+fPzM3NzMjJzcvTzdvM2dfbzt3N38vF0dHfz8vPz9HVd9dr6+tn
+a11bR0hHTlFdWVlNR01LU1dRX1tfR0tKQUI/PTo7Ojg7PD4+SUJBSUBLSFVZ3+vXzsvIyMq8
+vr69vr67tra2srCxr6+xrrKztbm7wMbO12tdX1tVVUhOR0xBSUE/Pj5APEM9QDs/ODY5OTk1
+Ojk+PEA/RT8+QE9jV+Nnx8PEwMO+wMPKxMvL18/Zz9fbz+PZ5+9n6+fv987Ry8XTzMjJwsDL
+1dXDwcfJzszHwOdXa2f/d05JSlvnX0pDPk1TUVdNU0VbVUk9PkRXU19Oa//jVVldS1VMX1XL
+vrvvUz5dv7rHwrrVzefZ19HK6+fn1cbbPTAuzr2ttNtIQDw0ODhNb8G9vFdnX/9dTl3dwut3
+z8RFTGNvPd/328O9/0bdXUc/WU7/T2dZTzw1NVnZSz41LzRF3dlrW9/Ty7+9vtusrKw9LzT/
+rKysrK+vtO9GOz7Er6640+vba0c+OEHjwWsvQzovLzEqKUDnQuPMXzxNzGfru7/BucDVa8HP
+x8bP1VlnWbu4zz85L1ljxVFK213OuCQpr6yyNSMoKKyrrMqwrOffNigpPbeszLzDsLTTST0+
+u7mu32P321U6MiswQfdnb1fIztdTPUNfuruyt7S9uNPZ18HJ1dXvVUlfRTY7N0I6Q0BbXdH3
+Wzk2Oj08Sc3fx2eyrSNPra2vUyo2Ma2ttNOtrchTKykqScvNN9u4rc0+ODnXur3VX9u3vkwx
+LDlKY2NHd7uwtt9nY8C4tbu5sq64yklZ59PRV1Nn30s9MDY6QDw6RF3FTkFCTT5LV1tfuLBG
+K66srDolLTe/rLrBra2wUSoqNUHfX0TZra3AOzhT429MTNG4s8o7LzhDPTc7Ss2yrbbXz8rG
+1cjGta2tts5j2e9nSElr529EOjQyMjQuMTxLRD9jQzkySG/nUdHnwLvnsOs8raysQSvNv66t
+rqysrLM0LUxVOTNXya2srMHjw7lRNzlT193vTz9DPS8mJis0PkxjyL282V1LV2PTwLW0t8nj
+a1U+NTg+TU9MSkNGQTw5SOvf39fOwsjvb93ZzdPBure9uLW1uVXbvrPOW9+trKxAd8a9Slf/
+s665TDQ4RSgjIy1FS13jwLm84zs5R1E5Nz/ZzNn3W93nXUFD2cfKxbu5tL3My7+5wM3Py9f3
+WUs/Pzw6ODk1NDY+Pzo/T29j38zNzL/IzNPOytPRx7m8w7aztcnMwsrTvclnzK65SzdTUTo0
+Ok7JyVE1PUs2JictNDQ4Qte9u7u7s7bB09nKzONn3cO/ys7Ju7u9vLzFw9Hj393b3d/3X1FM
+SDw2MTAwLy4wNjc5QUdJT2/j0cfBw8G9vsLLy8G7vby2t7q6usrLxcvTxsv/29PnU0Zv51k+
+RkpEPjwzNDg5MzdBREFFUVNRb+fr39HLyL+8urq6u7q/wsPIzs7O3f9vd19fWVNOTltbV13/
+b2NZW1lTU01LTVdfW1tf///36+/n087N29PV1c/T3dHPysrO1+Pr72dTU1VbT05FQUJCP0FB
+RkxVVVVZY19dWVNMUVFLT1NXb+fd29HIyMXBwb+9vL69vb3CwL28v8LFwL7DztnR3/9ZT09O
+Sj89Pj4/PD1AREdJR0VJTUtPXevf2efd49tnV1dfa19dWWNdT0xLU19nb+fb0c/P19PN0c7M
+y8rDysfJzM7Pzc3R09PR1d/n6+v3a2NfY1lVT09PU05NTk5VWV1fY2/v72/37+vj9+//5+f3
+92NvXWNXVVVdZ11jVVVXUU9LTktPT01NTE1GSktMU2fd0crGxL+8uru8vLq6vLq/vb7Ayc/O
+09vj7/9v7/9nXV9Xa05NTEdTV09OSlFRSE1IUUxZUU5NWV9TU1FXT1dVVU5bVU9OT1FRT09X
+W1lna/fn39XL08nKzc/Oz83NyMrHwMHCwMPEycrJy9XT0dPd3+/na19TSEhPSkhPW1VTWVtV
+TVFZXWNjd2/d3d3r2dXb6+fj/+/rd19bZ2NdW2NrZ/9jY1FZU0tPTE1TS1lVV1tTVWNjd2Pr
+9+fr99/j59/fzs3NzdPVztPj29XO19vMw8vKz8/LztXX387L1dfd2ePb3/d3a+dra2drWVVT
+T0pNSkVGSklFREtLTUZBSEdEQ0ZDSk9TTFNnY/9n49nj2dHTztHM1dXTzNPVzdXX1c3X1dHM
+ysrGx8XHwL3Fxb6+xcbJzMfJ0d3Z2f9Za2tZWXdjTmtTOmNMODlTRDk+Pj46PUI+PkJFRz1T
+90VCVdldP1/LRES77zpZvU1O28XIX9W418a528y8ysrZvbzfQsex1zi8v29HwVnTzNfTxme5
+y0XBtlPvxf+972fMSP/FTUNNX1c3OUdbOFVGQFtOTks4b/83Rk9LU1fr2VfZwNX30dnL91/r
+VVfvS1Pj39HT0ca5vb7Dvb3Ezd/TzN/j2d/Z2e/v187d/2djUUpBQUFNVUpLWXdrX19XZ+v3
+d1NV919JWe/vUU/31VFb99VO78vRTd/r/03v3V0+2fdDSV/nZ9X/2z1Z3ddDOlu1zndR9+/M
+vcH/07C1xOfXzt1vX0BC2b2+y763tLy+zV1LUUU5MztDTl9nX+fMw8fb72v3Y1dMTW/371tv
+Z29rVT89RkRFRUlDQ01MQ0RTSz5NZ1lNd93jycTAwr/DyL21xMG/xdu6xO9nys7RxL5nT09n
+b1l33T1300lMSU5vT0hfa0ZT687V/3dOS2fZz9/Jwb7IxN1ZV1lTR0FXY11ra1NNTldnSlFV
+UU9Zb/drZ9tXTld3d+ffydnKwL7JxcjM6//f32/d093X18rO293r59fX329ZTldTRltZT0NJ
+V+fVzNlLR9HByNtbPTtO00lIUVdf78zNV0hdVztK12NBPzk4O9vCz+/Hxs7MyN0/RGdTSN/L
+x720r7G6uMDNd91nTk7n2dvIvcnOy87nX2tbSk9fZ//f1d////dTUU1ITk9XVU9VXVd3b1lV
+WV9OS2P372N3W0lOX1lvb2ddY+fv/+dnTFFRU1fvU05rzMLAy1tLV1n/02dn/0RL08zZWet3
+Su+5usXj905fv7rMa9vHyL67ushf/9tv68j3RU7XT07XXT06RPdCTvdKP2fr/2tV72dVxsLn
+59nL0c2+v+PZztvj49vvTFnda2fva19bW1lfZ1tr6//jW29fa2NLS13r911b99lnUV9MVdfn
+V0ZId3fT38dbLEu1tetTOCkvPbutuLPFUz1O0f8uKjEtQ8asrLK6uMvEvbpRNzQ6P3e8tsLf
+187O08bLXURMY83AtbW7xMbI2etjX0M9QU5dd+//Q0JHSEVLS01JT//3Z2PvXVVfb2v338//
+Y2v3V1tdW09MX2NTW+vb6+PMzetZZ1tEXdnZ793Z1edf39dd08bX6/9ZUefNwb53SFFOTtPM
+y9VrW/fvxsLdU0xbXd3HyN9PW13v5+t3W0pXWWtZa2/v59/n62/31dv36+tbWWvv2/dj0ddj
+X83jd03f1d131XdKTnfR6+PV/1NEV/ffy8XZY/9bW1tj6/9TV3dbT1XIzGNHPj08P2PKy9fb
+/1NJTD88PD5Gd9fFvsLFwMXGycvnb1dbW//v7/9n7+vb29PV28rb1+Pf2dHOzcrP1dvX09/f
+73drW1dTWVdfU1NOUWPn3dvP31Vfxcv//9XrP013/1n/2/9M//dvSe/Hb01EZ2tX58PCyN9r
+SEtEPjo6PDs6P9/Czd/j19nV53d3WVlbU1Ff19nb3ePRy8jCzc3Oxsa/vry9vb2+zNvX619P
+SkY9PT5APkZHX1lr79nOzs7X3eN3629va3dZ3W/Z79/j9//n3c/dz9XT28/b0dfVd11OXU1M
+SU5CQEZVS0dPX0tTZ91379njWWdr72vv5+9ZU2P/a+/n5+vX0crMzs3X72v/W09RTkxGS1tr
+X3fb5+/V09fV2c/Z29nTz9fX529nW19TUVlfV0/r29PVxcfOyNHN08zDTMzN72/LZ2NFy2ND
+wkhbTUdMOWdZTlH3XWvN5+vT2dfr411fWVtba/f/d9vvX9/j4+fd1+f34+//6//n71tZX01T
+TVFRTFtbV1tRW1lf6/fr1dnV0czV29vX/3f3Z+f/529r92v///d3/+fv5+/r7+fd5+/v32P3
+a+f/7+P35+Pf3+Pd52/v393Zzs7Xz9fNytHP0dVr5/dTU0Tf0z9OXz88PmtVTmPXU0t3129f
+a2NHR0hdT0NTzFFd2dPGPm/GX+vbyt9L69NbT+vrW/fNytHTzcfKzr/Dzc3T29n379drV1dd
+UUxRU0tESk1PVXfr7+fZ19XP08nN1c/Z4//v52t329/j99vv//ff7+vf1+fn7293X2f/W09n
+WWNfXWddWWNfVVtTU11TW1dXX1ln/2vn4+vZ1dnO1czPys/M19Pv7/9fV11fZ2dr/2//Y3dn
+Y29jV11dY1dnW2NfZ19bV1dVT09MVVFNW1tjZ293a+/n7+/b2d3f39HX19fX19vf0+/n3dXV
+2c3PzsfNyMnHxcTIyMfIyc/R09XZ3dvn5+/r92dnWVtdV1VXUUtKS0VFRURES0pJTEtLS01J
+T0tOTlVRV1NXT09OWVVXXWN3b/fr2dfZ2dnT59Xf2+fX9+Pf49/T5+Pd39vV19vT3dXV18/O
+0c7R1dXO183/3dHZ3+vf73fr3+v/73f3Y3drY2NnX2ddWWNbd1tra2djXVlbWVtZW11jb/f/
+72//d+vr/+9vd2v3X/9n52fv6+vv53dvd29v7/ffY/9v/2dv71/37/fv73fr/13d/3f34//v
+//drX1t3WVlvX1dbXV1bXWtnZ2dja/9fb3dn9+t3a3f/b+vv59vd39vb3+vja2Pd/2d3//9r
+2+P/387r1dnP0dPZ2dXf793X59nP987Ta83Oz9fr6+9n62NfXV9VV1tOTlNVTE5OSllRWU1n
+Z2tr3+9399/v5/fn59Pb3+Pv2+Prd+fv3/9j91lfW1NXX1FfV1lfWVtZU/9dW+db2e/329fZ
+3dPT19Pf0dvR19/T3///d9v///9Z93dra2tdU19RV01ZT1VXV2dra1tnd/9369/n2efn1dnV
+19nj793jd9Vnb+fj6//fa2//7+d33Xd34/d3Z+9r3d/V0dXr39Xf3dtv69/jX2vn5133d2f/
+b/drd2dnX2NPW1lZV1NnU09RU0xPVU1VXU1XXU5VXU9rWVnv5+Nd3d/n/9Xf593v79l33ePb
+3dPTz9PT1dHZ1c/P78/b4/fn711nW11r/1tv62//Wevva93rd+/P29/b39vb39XZ19XT59vn
+a3dvW05bV1lRS0pKRklFTk1NTFn/d/drXdNrXefV79nR28rL18hnx9XvxUc/vfdBv8A8wtc9
+U8xdRtXRwi+7PWu7OMa1QUq5zji2OErXYzmwPzy2ON9b50xT389VSsRL42/fSknr71lGVb5K
+33fr69nn1VvZydfn28fPytnT293Ozevn2+Pd/1FTUf9vTVNv/+/dV+PZ0ddVVWfKZ2tv78pb
+QFVrd1VCP87May9bvsLTd0hRzs3/RE3IwNtfb//T109N29lVRNPG5zs4Pm/ZwcjIv7a9WT4+
+Pzc8Oz1MvrO0vrmvtMlVRD45Ojs+W7/Da01TSzo1O0z/0c7Iure2v9fVuMvOxc9d50vLU9HR
+a0DNSVdOvtlL57XBLi+urccpJkE+wsnO262tyTYsNSwkJSguR6+tt7utrcTnTD03SNnvTr+2
+vetbV05Z289v78W7wsXCv7m3vstfR0xGPkBfd/drb1NBR009SE7vY1PVwXc/Nz333z5frqzL
+Rcm760pXut/Iuss9tKywMy9Hb++709m+t8kvJy06Kio9Q125rLxN2bbMR0tdX9O+utXRu7nv
+U9vVb8jCwd/Mvr/Z59HfX0pBOC4zNzo6Pj9FSD8+Nzk6Pzpd38nbz7y821XLra62zF27rq2+
+W9m2wL6/viv3razDLSlbTsvTzWe0rro0LC01LjEwMTu6rblJT7/E1VtZRG+8uNVT37/FV09f
+787AzdXdxclvRUZNS2fv91tdV0g6NzU3Njg7REpNU2NnY03rxsnGwcK9w7+/vlnZtK2vss1j
+ObyvrVE4vK13NEe6xc/J2Tk5RVNATOs8KzdLPjVbxl9DT189NU/XSkvOtLu+t7TAvb6818/E
+v+PXzsT/U05LRFNv/0tNY2dRVUtBO0zrazs6P1FVb0k+SGtZPj/PyNl3y8VrWcC5ubG3wFdb
+yLe5ub7ZO0fMr733T10+NzzZydvRbysszq9nLCYuPe+2t+9jv8Y3LjVIP07O02vCra/Jzr/d
+3cu9zc69u91RUVE/SltbPUprd11rW1lVb1tXT3fO01c+TU5nT+PrzMrH0/9f/9fR31/TybjB
+Pzhrr7PBRT7rt7LROUHJvj41O++2rsk0JyxTz99ES+/D11szOE1rZ1lPd8XJU0NASufOzm9b
+2bq4w8fGxsnK51VR48HIWVFMUVNJRklP6+9b9+vfy8zPzdvX2cvN6+v/Z3fnW9nrzONdSlFZ
+xMZbO0zP20NLZ105O85rPUDDTVtR2etX2bo117PBLzBbrsvBwsVVybe7Qk53d0VVV0ZA09dn
+PVHba13/UUI+VeNXRk9fVVlv42/v18rT0869wMPJzdXOwsHZ09HXZ2vv419bY0pHU1lDS0xf
+b1/v70Zj38nM68zAz8Nra8PHsrlny9fH01XIvlFvS9/jzclJLTnVzGc2Liw5a9dPMjtNPTlD
+zkZRXVE2PFvM/9u+vb28usjb073H083L08jCvsvZ1dFfTU5PW1dTW1NAXVlvT0prX2fPWVdf
+zbrBd9vPz81RRFnbvLXdU1XV5+NPWV/V72M4S9/nVV1X2T9Fw81TWXfDvk5jxcd3vlVMSsy5
+zOvn2V/jW+/dw+vdRUtvb0rb183jWUpFOz1nUWtVRktVT3d3WV1TV8vNa/9n3+dfTk1K/2v/
+12tnyt1Tb1nvvsjO1+/Z690/48fB1XdEUWNXv80+z9tvxFHj2e+7wz5ITOu4wf9dW8+497vR
+/3dd/8F3vb1XzcxXd8e/yD9ARj9K93dnTmdPOjg+ODs4OUBGXc7ZY2d3ysTBys/Pz9HPzcnJ
+ycbI59/Mb9Nba+9jW+/Z33f/XU5ZREhTS0VGP05KRFFEO1NnWV3fzN3DvMtJWb++29Oyw0NZ
+ycK2vudLTve1XUHOucTnQUNTXcO6w8fE31FDOz4+Ql9X99vd599339njyctRVWNnXUVGU0hN
+X0tETnfbV05VW+PVz9vvz8HRWWf/73dM/+tPWeNbQU1nd3dfUevra8e93U7vxL/rRNe5w9fK
+//e9xeNnV13Hzl/LPjbTt2tFOjpn42PJ20zV401GPUxXU09KXW/Nv87Pyb/Gy9tn0cLI02f3
+193v6+93a9fT///3d9HT493b39nvVU9nb1tNRkBGWUpDPUdPTENHRkpHX2dVTOPZUVm7ylXV
+sdtv97fnz765d//Tz2/P/2//xdlORszF6z/bZz4+T0k/P01LRkNKS1Nfa11399Pd2dfT2c7N
+0dPdy8rGxcrMysvMzs7O0cbJ3d/r09/f3/f3b+tjX2djZ29rX1NdZ2tvXW9379nvX13r61tZ
+U11RVUxHSUtOSExFSElLTEBKSl9bW2tvY/f/4+9d53fj3evf39fR09vTzM3R09/d1cvd1c3X
+1cvZ49vT4+fn49Xj3efR3e/d2ffvd2Pn//fvb29v/11jU1VVV1NdUWv//1n/a+tn/2tf5+PZ
+5/ff5/9v9/9dXXddY29rY/93X2vv/2tna/djY1tn63f/d9v/b+9n72//X/fr4//v73fj//f/
+/2vn/+Nf42/d/+P35+ff//93Y+93/2df7//dWV1n6/9rXVvrd+t3593j73fvY2v/X1dv/1VV
+X+9jZ/9vb//rb29v2+vf2c/r3c73593j79XVXefA7+PDyt/E41vAdz/b02dT019I22NFztdn
+a11r92dPX/dN611T/1fnY2NM10/vTUDBOk9fRlPTPl3nOsrFP1Hda+P3791Jz8FdXd/Kz9X/
+/9PD/1v32ePr9+/TzdHV69nM2ffVy9P/09fb1///3+/j/19j1+9MTdnnVdvjW0ffX01R3dFN
+V29JTstRS+vIdzpV0T9Pd91TVcm+S0q9xks+WbzJVUndz9NfSMC9w3fr0c9ROj5J7z09RM/T
+XVvby9vI1VFr2fdHVWPvSVvRXU1d2c1v49XN68vI5+fFv9/VxcXT0c/M513Tb19b929RTmNK
+QFNGTVlRU05F62tNUWN3XWvbSVPOU09T2c1jS2PXy0fKu79fSsy/d//V0azRSGe8wWNDz8HP
+y1nZzdNdR2e/S1Pf70tfV09Cb11BSO9dQERvRj5TTk5KTF9LT13v29/v1ev3VVf3b2/j529Z
+z2P3z8pX78TVWdu+3+vf0cDM/92+zv9Pz9HnU8nL61vXx+fdQkvfW0FI3czZ3/fvY11da09n
+RXfP30df/9lR4+9vX+//SVljW0//a1Pfxe/vzNHG0dHfzdfb2c/37+N342tv/+NXVetjV1FT
+W01GUUljX1lT7/dvTevX21tdzc7j683N09fP1+Pf905bT1lIU//3Tl/Vz01v0e/3VVvIzEhb
+93dTXedrWbzTTNvB30j3w8Rnv8fZ1clrb9vG/0nGykpDa0hBT04+X/9NRlfvb2tV62ddSEl3
+Y0tj3dfV1dHbyt/r09PPxN/XyL/L2cu+71PO0eNbz8znd9dd3ddfW+fdV1FfVUZjVUtO/08/
+Ve9HT11VW9XvV2fMxetHUd/VQj9b3UxIR1XTd1Vb49f/Rf/P41Vj2d/jXffP0ev35+t3d1tj
+z+v/28rK49nTa8C/7/e/ws7Gxt3Jxl1jxM9V38xVb+9PTOtPR0ljU09XWU5nY0lf711T93dV
+XV1OXVljWWPb/2fn2dXn68zN3dvb/+ffZ2vn52tf72tRW3dbW11Vd2tOW/dTWXd3W+Pv7+vP
+9+Pj53fj419n4+N3Z+/j9/fr6//nb1vv6+Nv79/TZ//j7//n6+v3z+P/59nrb9fr79/f4+/d
+4+vf2+vv5+tnZ3dvZ2tnX19nZ133XVlTVVldZ2NnW113/2tv72f37+d349nn3d/V09vb3/fr
+a2Nra1lZa19nX19TU19PT1VLTVVbUVFVWVtZX1dvd3dr3d3V08jPzdnP2dfR19fT09fb2czX
+3czX2c3d39nf7/fj5+/d2+t333dvY1NXU11PV19bVVNrV2tbWVlvX3dd6//v63fv9/9f71/f
+a+//3eP/92fd399v79nv9+vra2//a19TY09ZV1ddT1FOVVdZX1/vXf9fb/dv2f/d09nf09fX
+3dfZ1+PR09PR09ff3dPj1evb19v36+/v4+fZ3e/va/9jZ1VbWVVPW1lXWV1bZ2NfX1VbS1lN
+U0lJWVVNXXdfb3f3991r2e/P1dfX2dPj29PV09vT3dPbz9/Kz8/dzs7f1+fj3+PrZ3fj7/93
+Z1tjUV9LVVVPUU1KS01LTUhNVUxTUVlTW2dZd2tvZ2Nf7+dv39vjz83O0cjNztXI2czTx9PK
+ytPL1dHbzc/T19fn7/93X2djX19XV1VbT1FXVU1NTldRTFFOU1VZY1NXb2NrW2Nbb2vr/3fn
+Z///7/fj59/b4+fr6+fr5//P29XN1cvjz9vZ2+/P2d3d29XR39n/7+vn72P3b29db19RZ2NP
+Y19dX1VdZ1lZVWtfd1lbY3fvd+93X/f3Y/9v//9vWd3dd//jb2trXVtfV11fY1n/WU1vWV1b
+d2fv493j1dvR1czdzs7PzM7HzMnb3ePv39X/z9nf29/33efjd2fnZ2tfd19fV3drd1dna2dP
+VV1dV1lZZ2NrX/9dTedV/2dX51lnX2f3Z+P30/fn9+9f591rb2PvVV33a/9r7+/ra3ffb+f/
+d+P/79nr3czVd83Ry+vT2+Pd39fv5+vn2ff/429n999f/193Z+/rb/dvb+939/9RWV13TEtr
+U05NU1NXTENTWU1MSUxdd1NZ9/fr/93V19PN08zP2c3j18/X48jN3czZ0dnR1Xfr7//n5+v3
+62vr53ff4+vj3+dv91lrZ/9nW2trZ0pdTldRd11ZTlNnUWtbZ11nW+//W1dZU/9P93fv33fr
+42tf2evPb+fv5+dn42ff39vnzdfj/3e6Vb1r39XZ1dPnzWPHa+dj2ePfSe9Na1v/THdZY2P/
+411rY2dPVevvZ+v/b0vV30TTY3f3/2fbV+Pnd2/rV9FdY1t3Tk93X05b61dnd//nb9/O1dPJ
+ysLJycfL383F1dfP1+Pb59X/0//j51vZZ2tjX11ZX1dMU0NJRkdbTVdKd2NjVVFOSVdTY01r
+Z0/jY+Pv0+vfyevf79XfVclv41X/W0/vVe/va+fj02vvzkrNy1vf58tR1cJvzcrv38zNzMzK
+0b/b6//Xb/fE3cX/08pJwP/b51lbTe87Ttc5/0NExi652y+0Lsn3N8tLVzi9PTXEP2/nQ11b
+W81Ld8VB18w2x0TA2U9r0dXT9/fbxE7f72PFWc1Jz8vT79nD3cDDZ8S668P/XbvGTcPdv13/
+x9Pnx+tXy2vRTzr31ztM7z5vWUdTQ0zbPDjOU0HrTlNNz1VdTffvWW9N7+P/R2fd2U/OTV3Z
+W0dL31FH68tZ0d/P2cr30f/M/2PGy8rC48i7ycBfu71jy9tjumvXzP/Ld1fZW1XdS0/nUU9X
+X01bQ9FTPf9Fb1tCb0lX2T1X0VHbV0jnU81KP75RY8rrZ8LZ7/dZwWtOv/drxv9bu0y5wky6
+TffXPtnf/+PMUW/Z//dCWVnfW1NDUc9v4+dVv9dVZ1Vv2UZN6+/VU2Nf/+t3S1dv72dGXcx3
+a//j2//3Z3fjzlNO61Pb70xr41ffb0/ba2vfd+PK1c3CztnX19nX0cbRz8fVxcPbydfZ083j
+3c/jW1vf49/P1ePf1+d372dbS01LSj9MSEVHSkRKTERESEpDXUFLTUlHRk9ZVU1nRU9bVWNb
+3/fv2ev349v/0+dr49PTys/RzsvXzdvJx9nH2cjLzc/Rx8vKycvIzMXOyMPIxcjPzdHb2evX
+02/jZ3f3VWNrWWtdSE5LS0tCR0dESUpISEhHSUBIRkhJRElMSkpNSFFRTE5KVVFRU2Nv2e/v
+zdvO1cnLxcfAxMPCw8jEyNXG2c7ZztvVzdXT0dHPz9PZ19Hj09fb3dnf3+//d29TTldOSkVF
+Q0dHR0hCSkhMS0lVTUtOSU1OWUxfU2v3V//Z29HZysnIycHIvb2+v77FwMXJz9PX6/dbb1tX
+WVtVV2dnUWtfWfddXWf/6+dn7+fv5+9n32tnWVNZVU9NT09OT0dZTWtZU1tTXVVRWVNTV1FV
+XV9jWVl3b///b2d3a+vb2cnXzsvOzMzKzcvFzsXDysTHw8rKxMzK0dHT39Xd59vr3+P32+Pn
+32fdb9/r69vv79/v493v7+9db1tjVU5XS0tKSkVJRUQ/QD89QTw/PDs9OzxAPD5BQ0RLS1FR
+W2dd79nVyMPCv7y+vby7vLq9ury4vb25vb7AxL/DyM7MytXZ2+vj/1tvV11ZWWNdb29TW09f
+SltNTFtjVVNTSlFKRExISkhNSE5JS0xLS05MV1NfV2tfX1VbZ2v3/+fV39Prys/d09Xf39HV
+3d3KycjT183f0+fj29fT3ePR4+Pj29/P59vv3+/r59vr1d9r9+//d99Zd1dRVU5dTndnd2Pr
+Z+tjY1lTU01rUU5ORktPRUpNS1lPU1lMb1lbVV1vU2tv72fn2evZ0dPNzcnPzcnKxca/xMC+
+vr6/xca/wczIzM7P3dPf/29v92dna1VMU05DSFFRQUtBQT8+PEJAP0E/QURGSUlNU1VdZ1ld
+99939+PPd9Pd3/fP38/b1dvXzsvPzNfP48vj1czZxcfIy9PAxcbKxs3Rz8jH2dXT49tf/+db
+d3dnY13rTWtRWVlDW09MTUhJQUJIS0ZJSU1ISEtKT1tPXUxfTVdXXV33Y2v33f/X2+v/39t3
+zd/X3eff1d3Z3dPv6+fv3dvZ18jHy8LIx8jHw8jFxsfNzuPf23fZ3fdr/+9fWVdVU0pRS0hP
+R0pBS0tEVUtTT1lITE5RT2NTX29nb+v32efTz9HIysvRxsjLzcvJys7Mz9XT1dfd3dnj28/d
+3e/f9+tbXVVTW0hPREtIRlFHT0xPSklJQ05GSVFPTl1RW1tfV+tvU3dv1eNr99/r19nZ0dXb
+ys7VzMnKysrMzcvR09nX0+Pd1c/T0dvj593Rb+PZ6+fj5//rW2/vZ/9jZ2tjb2Nj73d3a2tr
+d2dbb3dnWWdV73dbd1FrUVlMWU9fVV1dW1n/Z/9fa19VWWtbb19V19dd4//XzG/j60fE29nJ
+VdXDVczZVdvF/93bW9tOY9dPb2dJV1FOX0RX/29rXW/v5/f3593Z0//XzufRztPnY89n5+tr
+3e9v3+Nvd1vfb2/dY2Pf3e9rW3dvWetrVWdvY13rZ2tr2Xfb4+/V791j7/fv3f/rd/fnd9nr
+a+td//9Vb1dn429fZ3fra3dnd/djW1XrW/93X293Wffjd2PnVXdnd1f/a11j92ffXXf3791v
+5//n32P3a+Nj01fO/93P99fb2+fT2d3n7//d5+fr5+Pf3czZ2dPT19HN3dnX6+v/a//3Y3db
+V2ddXWtXd2drb1lRWU5bYz9IW1VXd0j/W1VvVVvbTmtOY1F3WU5XW19vV11fV19bV/9rb9vX
+69Pd29fV39HZzs7NzcrM1dfG29PM29PZ38jT68fN38hZ68lN01HRT2vnWdVX91NZZ1N3b1tv
+T2NMX1H/UVVbX2tVZ2NPVVdjWV1OWVVjT1VZWV9Zd2Nf7/dv49lrzslnv9fFv9nr38nvyt3f
+zmfOXd1d//fX4113V3dZ91f/71ljV1dXZ3drY19nV09XW19ZY1tVWW9bWWNnY29j42vj1+vj
+zMrOzGfna8JVvlfZ3+93601vd//Td29jd1FdX+/352Pr//fv7+/Z2f/j6+/v/2v/b3dVWe9f
+a2NjX3dvX+N3d9vZY9XXWcdZxshT3WN31ddEyU9O7/9ZX0RZXWt3V1tPVU9bTmtdY3dba3dr
+79nf29/d5+P/a2dr62fr53ff6+vr3d33z9nR19Pb2c7bz83P593v2d932/9vY03n12dN70jv
+//9O70ffRtlX/01bZ05ZX2NJX0pOX2NVXVdTZ2Nv72P35+fr1dvZ2dPR08vR19PM49XT59//
+4+vnb+vn/+fv52/ZW+Pvb/d3a11na+9nb+dnZ+//a29rX2NZa1tZY2Nfb2f/a2/vX2v/91//
+//f/4+Pr6+/v4+fj7+vdd/9vb29rd2/3d3dX3dnvX19P49n/0WNr32dv/1nv3etvb/fr29vX
+49/d1+/v3efj92tvZ3dvb3d39/fv5+/r7//j6/9db2NRZ09nXV9ZV11rWVljb193Z2tfb2Nv
+a2tnX2N3Z+vj79/r3d/Z1c/R087bzdvZ2+Pr28/d3d3v7/f/92Nra1tXXWtXWV1ZZ3dfZ29r
+Y2dbW///929rb2/rd1trb29X/1ddXVnvX29j71/ra+fn/2933+9v29HT30+/zdfZ3d/Lzr/f
+0c5ra9nV1+Pj3Q==
+
+--Outermost_Trek--
+
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Digitizer test
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29550; Thu, 3 Oct 91 13:04:23 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19374; Thu, 3 Oct 91 13:04:04 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12278> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:01 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08969> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:59 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:03:59 -0400 (EDT)
+Resent-Message-Id: <YcurSj60M2Yt8Ta24=@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:03:59 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <sau@sleepy.bellcore.com>
+Date: Fri, 24 May 91 10:40:25 EDT
+From: sau@sleepy.bellcore.com (Stephen A Uhler)
+Message-Id: <9105241440.AA08935@sleepy.bellcore.com>
+To: nsb@sleepy.bellcore.com
+Subject: A cheap digitizer test
+Cc: sau@sleepy.bellcore.com
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary="mail.sleepy.sau.144.8891"
+
+--mail.sleepy.sau.144.8891
+
+Well, here's a sample
+--mail.sleepy.sau.144.8891
+Content-type: image/pgm
+Content-transfer-encoding: base64
+Content-Description: Bellcore mug
+Comments: 256 x Image wrapped by /usr/sau/bin/fetch_video
+Date: Fri May 24 10:35:57 EDT 1991
+
+UDUKMjU2IDI0NAoyNTUKAHaPj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5uCaWl2dml2dnZ2gnZ2goJvfHx8aXx8aWl2Y1dKMTEx
+MT5KSkpKV1dKSkpKSkpKSkpKSldpdnZ2Y1dXV1dKV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKSko+Sj4+Pko+Pj4+PkpKPj4+Pj4+PkpKSko+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTExMTExMTExAAB2iIiIiIiIiIiIiIiVlZWV
+laGVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurq6urq6uqePdnZpaXZ2aWl8fG9vgm9vgnZpfHxpaXZ2Y2NjRDExMTE+Pj5KSkpKSkpKV0pKSkpKSldXaXZ2Y2NjV0pKSkpK
+SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj5KSj5KSj5KSko+Pj4+Sko+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMQAAdo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6en
+p6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShlXxjY3ZpaXx8aXaCb298fG98fG9vfHxpaXZjUEQxJTExPj5KSkpKSkpKSkpKSkpKSkpKXW9vb29dXV1dSkpXV0pKSkpKSldXSldKSkpKSkpKSkpKSkpKSkpKSkpKSkpK
+SkpKSkpKSkpKSj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTE+MTExMTExMTExMTExMTExMTEAAHaIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u
+rq6urrq6rq66urq6urquro98fGNjdnZpdnZ2dnaCb298fG+CgmlpfG9jY1c+MSUxMT4+SkpKSldKSkpXSkpKSkpKV1djb29jV1dKSkpKSkpXSkpKSkpKSkpKSko+SkpKSkpKSkpKSkpKSj5KSkpKSko+PkpKPkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+PjExPj4+MTExMTExMTE+PjExMTExMTExAAB2iIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6rqGVgmlpdnZpdnZ2aXx8aXaC
+dnZ2dmlpfG9vb29KSjcrNzc3RERERERERFBQUFBQUFBQUFBdaWlXY1dXV1dXSldXV0pKSkpKSkpKSkpKSko+SkpKSkpKPj5KSj5KSj4+Sko+Pj5KSkpKSkpKSkpKSkpKSj4+Pj4+Pj4+MT4+Pj4+PjExPj4+PjExMTExMTExMTExMTExMTExMTExMTExMQAA
+doiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrqurrq6urq6uq6hj29vb2NjfHxvb3xpdnZ2dnZ2aWl8fGl2dmNjUDc3JTExPj5KSkpKSkpKSkpKSkpK
+SkpXY2NjY1dXV1dKSldXV1dXSkpKSkpKSkpKSko+SkpKSkpKSkpKSkpKSkpKSj4+Sko+PkpXSldXV1dXV0pKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMTExMTExMTExMTEAAHaIiIiIiIiIiIiIiIiVlZWVlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhoa6urqGurqGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqulZV8Y2Nvb29vfG9vfHxpdnZ2doKCaXZ2dmlpaUo+MSU3NzdKSkpKSkpKSldKSkpKSkpXV2NjY1dXV1dXV0pKV1dKSkpKPkpXSkpKSkpKSko+
+Pj4+Sko+PkpKPkpKPkpKSko+SkpKSldjV1dXV1dXV1dXV1dKV0pKSkpKSj4+Pj4+Pj4+Pj4+Pj4+MTExMT4xMTExMTExJTExMSUxMSU3AAB2iIiIiIiIiIiIiIiIiIiVlZWVlZWVoZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urq6urq6urq6urq6urq6urq6urq66urq6urqurqGIb29vY298b298fGl2dnZ2dnZpaXxvY3Z2Y2NXNzclMTE+PkpXSkpKSkpKSkpKSkpKSkpjY2NjV0pKSkpKV1dXV1dKSkpKSkpKSkpKSkpKSkpKSj5KSkpKSkpKSko+Pj5KSj4+SkpKXV1dXV1dXV1d
+XV1dXV1QUFBQUFBQUFBQUFBQPko+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMQAAdoiIiIiIiIiIiIiIiIiVlZWVlZWVoZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urrqurq6urq6urq6urq6uurq6urq6uq6b
+j29jY3Z2aXZ2aWl8fG98fG9vgnZpdnZ2aWlpSjc3JT4+PkpKSkpKSkpKSkpKSkpKSldXY2NjY2NQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERERERERERERERERERERERERERERERERFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERERERE
+REREN0Q3Nzc3NzclMTEAAHaIiIiIiIiIiIiVlZWVlZWVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6uq66uq6uoYhvb29jb3xvb29vb298b298fGl2dnZjdnZjY0o3NyUx
+MT4+SldKSkpKSkpKSkpKSkpKV2NjY2NXV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKSkpKSkpKSkpKSko+Sko+Pj4+Pj5KSkpKSj5KSkpKSkpKSkpXV1dXV1dKV1dKSkpKSkpKSkpKPkpKSkpKSkpKSj4+Pj4+Pj4+Pj4xAAB2iIiIiIiIiIiIiIiIiIiV
+lZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6urq6rpuPdmNjdmlpdoJvb3x8aXx8aXaCdml2dmlpaVdERCUxMT4+Sko+SkpKSkpKSkpKSkpKSkpjY2NjY0pKSldKSkpK
+SkpKSkpXSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPko+Pj4+Pj4+Pj4+Pj4+Pj4+SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSj4+SkpKSkpKSj4+Pj4+PgAAdoiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGh
+oaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6hgm9vb2NvfGlpdnZjdoJvb3x8aXZ2aWl2dmNjSjc3JTE+Pj5KSkpKSkpKSldKSkpKSkpXY2NjY1BQUFBQUFBQUFBQRERQUFBQUFBQUFBQUFBQUFBERERERERERERERERE
+RERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N0REREREREREREREREREREREREQAAHaCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pj4+hoaGhlaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u
+rq6urq6urq6uurq6urqum492Y2N2dml8fGl2dnZpfHxpdnZ2aXZ2Y29vXUo3JSUxMT5KSkpKSkpKV1dKSkpKSldKSmNjY1dXV0pXV0pKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSko+SkpKSkpKPkpKPj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTE+
+MTExMTExMTExMTExMTExPj4+Pj4+Pj4+Pj4+Pj4+AABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGCdnZpaXZ2aWl2dmN2dml2
+gnZ2dnZpaXxvV2NKNzclMTE+PkpKSkpKSkpKV0pKSkpKSldjY2NXSkpKSkpKSkpKV0pKSldKSldKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj5KPj4+Pj4+Pj4+Pj4+Pj4xMTE+Pj4+PjE+Pj4xMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMQAA
+doiIiIiIiIiVlZWIiIiIiJWVlZWVlZWVlZWVoZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6uurq6urq6rq66rq66uq6uuqebiHZdaXZpaXx8Y29vb298fGl2gm9vb29jb29XSjcrKys3N0pKSkpKSkpKSkpKSkpK
+SkpXY2NjV1dKSldXSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTE+Pj4xMTExMTExMTExMTExMTExJTExMSUxMSUxMTElJTExMTEAAHaIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhrq6hrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urquoYJvb29jdnZpdnZ2aXx8aWl8b29vfGlpfGlpXUoxMSUlNzc3SkpKSkpKSkpXSkpKSkpKV2NjY1dXV0pKSkpKSkpXSkpKSkpKV0pKSko+SkpK
+SkpKPkpKSko+Sj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMTExMTExMTExMTExJTExMTExMSUxMTExMSUlAABpiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6uoa6urq6urq6urq6urq6u
+rq6urq6urq6urq6urq6urq6urq6urq6urq66urq6rpuPb2Nvb29vfG9jdnZpaXx8aXZ2aWl2dmNvb1dKMSUlMTExSkpKSkpKSldKSkpKSkpKSkpjY1dXV1dXV1dKSldKSkpXSkpKSkpKV0pKSkpKSko+SkpKPj5KSkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+Pj4+MT4+MTExMTExMTExMTExMTElMTExMTExJTExJTElJSUlJSUlJQAAdoiIiIiIiIiIiIiIlZWVlZWVlaGVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurq6urqurq6urq6h
+fG9vb298fGlpfGlpfHxvb4J2aXZ2aXZ2aWldSjElJTExPj5KSkpKSkpKSkpKSkpKSkpdXV1dXVBQUFBQUFBQUFBQUFBQUFBQUERERERERERERERERERERERERERERERERERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyUxMTEx
+MTExJTExJSUlJSUxJSUAAHaIiIiIiIiIiIiIiIiIiJWVlZWVlaGVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqhj4J2aWl8fG9vb29vfHxpdoJvb3x8aWl8aWlpSjc3JSUx
+MTFKSkpKSkpKSldKSkpKSkpKV2NjY1dXV0pXV0pKSkpKSkpKSkpXSkpKSkpKSkpKSko+Sko+SkpKPj5KSko+Sj5KPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMSU3Nzc3NzcrKysrKysrKysrKysrKysrAAB2goKCgoKCgoKPj4+Pj4+P
+j4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6d8aXZ2Y298b298fGNvfG9vfHxvb3xvb29vXV0+MSUlJTc3N0RERERQUFBQUFBQUFBQUF1dXV1QUFBQUFBQUFBQ
+UFBQUFBQUFBQPkpKSkpKSj4+SkpKPj4+Sko+SkpKNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyU3NysrKysrKysrKysrKysrKwAAdoiIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6rq6urq6urq6PgnZjY3xvb298b298b298fGl2dnZpdnZpaWlQPjEfHzExMT5KSkpXSkpKSkpKSkpKSkpXY2NjV1dXSkpKSkpKSkpKSkpKSkpKSkpKPkpKPkpKSkpKSko+Pj4+Pko+Pj4+
+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTElJTExJSUxMTExMSUlJTElJTExJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6e0tKenp6enp6enp7S0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSnm3ZpaXZpdoJpaXZ2aXZ2dml8fG98fG9vb29dUDclJSUlNzdERFBQUFBQUFBQUFBQUEREV2NXV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj4+SkpKSj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTEx
+MTExMTExMTExMTExJTExJSUxMSUxMSUlJSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+Pm5uPm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oY+CdmNjfG9vfHxjb3xvb298
+b2+CdnZ2dmlpaUo+MSUlMTE+SkpKSkpKSkpKSkpKSkpKSldjY1dXV0pKSkpKSkpKSkpKSj5KSkpKSj5KSko+Sko+PkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMSU3NyUxMTExMTElMTExMTExJTExMSUxMSUlJSUlJQAA
+doKCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeVdmlpaWl2dmlpfHxpdnZ2doKCb3x8aWl8aV1QPiUlJSU3N0pKSkpKSkpKSkpKSkpK
+SkpXV1dXV0pKV0pKSkpKSkpKSkpKSkpKSkpKSkpKSko+Sko+PkpKPkpKPj5KSj4+Pj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMSUxMTExMTElMTExMSUxJSUxJSUlJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubj6GhoaGh
+oaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6uq6urq6uurq6urq6urqhj4J2Y298b29vb2NjfHxpdnZ2doh2dnZ2aWlpSjc3JSUxMTFERERERERERERERERERFBQUF1dXVBQUFBQUFBQUFBQUFBERFBQUFBQUFBQUD5K
+SkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMSUxMSUlMTExMTElJTExJSUlJSUlAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp7S0tLS0tLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5V2aXZ2aXx8aXZ2aWl2dnZ2iHZpfHxpdnZpXVA3NyUlMTE+Pj5KPj5KSkpKSkpKSkpKSldXV1dXSkpXV1dXV0pKSkpKSkpKSkpKSldKSkpKSkpKSkpKSko+Sko+PkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+MTE+MTExMTExMTExMTExMTExMTExMSUxMTExMSUxMSUlJTExMTElJSUlJQAAaYKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGI
+fG9jb3xvb3x8aXaCb298fG9vgm9vfHxpaWlKNzcfHx8xPj4+PkpKPkpKSkpKSkpKSkpXV1dKSkpKSkpKSkpKSkpKSkpKSkpKV0pKSkpKSkpKSkpKSkpKSj4+Sko3N0RENzdENzc3NzdERDdERDc3Nzc3Nzc3Nzc3Nzc3Nys3Nzc3Nzc3NzclMTElMTExMSUx
+MSUlMSUxMSUlJSUlJSUAAHaIiIiIiIiIiIiIiIiIiIiVlZWVlZWhlZWVlaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6urq6urq6uq6hlW9vb29vb29jdnZ2dnZ2aXaCdnZ2gml2dmlXVzclJSUl
+MTExPko+PkpKSkpKSkpKSkpXV1dXV1dKSldKSldKSkpKSkpKSkpKSkpKSkpKSko+SkpKSkpKSj5KSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMT4+MTE+MTExMTExMTExMTElMTElJTExMTExMTElMTExMSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+P
+j4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oYiIb2N2dnZ2dnZpfHxpaXx8b3x8b298fGlpaUo3JSUlMTE+Pj4+PkpKSkpKSkpKSkpKSldXV0pXV0pKV1dKV1dK
+SkpKSkpKSkpKSkpKSko+Sko+SkpKSkpKSj5KPj4+Pj4+Pj4+Pko+Pj4+MT4+Pj4+PjExPjExMTExMTExMTExMTExJSU3NyUxMTExMTElMTElJSUlJSUxMSUlMSUlJQAAaYKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6en
+p6enp6e0tKenp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6ePb29vY2N8fGl2dmlpfHxpdoJ2aXx8aWl2Y1dXNyUlJTExPj4+Pj4+SkpKSkpKSkpKSkpXV1dXSkpKV0pKV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKPj4+
+Sj4+Pj4+Pj4+Pj4+Pj4+MTE+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMSUlJTElJTElJTExJSUlJSUAAGmCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSbiIhpaXZ2aXZ2dml2dnZ2dnZ2dnZpaXx8aXZjRDcfHx8rKzdENzc3NzdEV0pKV0pKSkpKSldXSkpXSldXV1dXSkpKSkpKSkpKSkpKSkpKSkpKSj5KSkpKSkpKSko+Pj5KPkpKPj5KSjc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3
+Nzc3Nzc3Nzc3KysrKysrKysrKysrKysrKysrHx8fAAB2iIiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6rq6urq6uurq6urqnp4hvY3Z2aXaCaWl8fGl2dnZ2
+dnZpdnZ2dnZpUD4xHx8fKz4+Pj4+Pj4+SkpKSkpKSkpKSldXV1dXSkpXV1dXV0pKSkpKSkpKSldKSkpKV1dXSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Sj4+Sko+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMTExMTExMSUlMSUlJTElJTExJTElJQAA
+aYiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoZWhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urrq6uq6urq6urq6urrq6rq6urq66urq6rpWCgm9jb3xvb29vY298fGl2gm9vfG9vb3xpaV1KMSUlJTE+Pj4+Pj4+PkpKSkpKV0pK
+SkpXV1dKSkpXV1dXV1dXV0pKSkpKV0pKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MT4+MTExMTExMTExMTExMTExMTExMSUxMSUxMSUlJSUlMSUlJSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6rrq6urq6uqenj29jdnZjdnZpaXx8aXZ2dnaCdml2dnZ2dmlQRDEfHysrNzc3Nzc3NzdKSldXV1dKSkpKV1dKV0pKV1dXV1dXV1dXV0pKSkpKV1dKV1dXV1dK
+SkpKSkpKSkpKSj5KSkpKSko+Sko+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+Pj4+MTE+PjExMTExMTExMTExMTExMTExJSUxJTExMSUlMTExMSUlAABpgoKCgoKCgoKCj4+Pj4+Pj5uPj4+bm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0lYJ2dl1vfGlpdnZpdnZ2dnaCaXaCb298fGlpXT4lJSUlNzc3Nzc3Nzc3RFBQUFBQUERQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERQUERERERERERERERERERENzdERDc3
+Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nys3Nzc3NzclMTElMTExJSUxMSUlJSUlJSUlJQAAaYiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq66urq6uq6urq6urq6uurqurrq6p6eI
+b29vb29vfGlpfG9vfHxpaXx8aXZ2aWl8aVdEMR8fHzExPj4+Pj4+Pj5KV2NXV1dKSkpXV0pKSkpXV0pXV1dKV1dXV1dXSkpKSkpKV0pXV0pKSkpKSkpKSkpKSkpKSkpKSj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4+Pj4xMT4+MTExMTExMTExMTExMTEl
+JTExMTElJSUlJSUlJSUAAGmCgoKCgoKCgoKPj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSPgnZpaWl8b298fGl2gnZpfHxpdnZ2aXx8aWlXPjElJTEx
+MTE+PjExMT5KV1dXY1dKSkpKSldXSkpKSkpKV1dXV1dKV1dXSkpKSkpXSldXV1dXV0pKSkpKSkpKSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMTExMSUxMTExMTExMTElJSUlJTExJSUlAABpgoKCgoKPj4+Pj4+Pj4+P
+j4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShoYhpaXZ2Y3Z2aXaCdml8fGl2gnZpfHxpdnZjV0QxHx8fKz4+Pj4+MTE+PkpXY1dXV0pKSkpKSkpKSkpXV1dXV1dX
+V1dKV1dKSkpKSkpXSkpKSkpXSkpKSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTElNzclMTExJSUxJTExJSUlMSUlJSUlJSUlJSUlJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6en
+p6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tJWCgmlpdnZpaYJ2aXaCb298fGl2dmlpfHxpXV0xJRgYGCUxMT4xMTExMUREV1dXV0pKSkpKV0pKSkpXSldXV1dXV1dXV1dXV1dKSldKSldXSldXSkpKSkpKSkpKSkpKSkpK
+SkpKPj5KSj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+MTExPjExMTExMTExMTExMTExJTExJTExMSUxMSUlJSUlJSUAAGmCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSbiGlpdnZpfHxpdoJvb3x8aXZ2dml8fHx8fGlpSkpKNzc3Nzc3Nzc3NzdEREREREREREREREREREREUFBQUF1dXV1dXV1dXUpKV0pKV0pXV1dXSkpXSkpKSj5KSkpKSkpKSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4xMT4+MTEx
+MTExMTExMTExMTExMTExMTExJTExMSUlJSUlJSUlAABpgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnlYKCaWl2dml2dnZpdnZ2do+b
+m6enp7S0tLS0tLS0p6enp6enp6eVlZWIiIiIfHxpXV1KSkpKPj4+SkpKV1dXV2NjY2NXSkpXSkpXSkpKV1dKV1dKSkpKSkpKSkpKSj5KSkpKSj4+Pko+SkpKSkpKPj5KSj4+Sj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTElMTElMTExJSUlJSUlJSUlJQAA
+aYKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp7S0tLS0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6eIaXZ2aWl8fGl2iIihtMfHx8e6urq6x8fHx9PT09PT09Pf39PT09PT08fHx8fHurqn
+p5uPgoJvY1dXV0pKSkpKV1dXV1dXV1dXSkpXV0pKV0pKSkpKSkpKSkpKPkpKPj5KSj5KSkpKSkpKSkpKPkpKPj4+Sj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTExMTExMTElMTExJSUlJSUlJSUAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ub
+m5ubm5ubm6enp6enp6enp6enp6enp6e0tLSntLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSPgnZpaXZ2dnZ2j6fAwMDAtLS0p6e0tLS0wMDAwM3Nzc3Nzc3Nzc3Nzc3Z2dnZ2dnZ2dnZzc3NwMC0tKGVgm9vY1dXV1dKSldKV1dXSkpXV0pK
+SkpKSkpKSkpKSko+Pj5KSkpKPj4+Pj5KSkpKSko+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMSUlMTElMSUlJSUlJSUlJSUlAAB2iIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urq6urq6uurq6rq6urq6urq66urq6urq6uq6hlYJpaXx8aXZ2obS0oaGhlaGhoaGhoaGhrq6uurq6usfHx8fHx8fH08fH08fT09PT09PT09PT0+b4//////jfx7ShlYJvY1dKSkpKSkpKSkpKSkpKSkpKSkpKSko+SkpKSj5KSkpKPj4+SkpKSkpKSko+
+Pj4+Pj4+PjE+PjExMTExMTExMTExMTExMTExMTExMSUxMSUlJSUlJSUlJSUlJQAAaYKCgoKCgoKCgoKCj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p492
+dnZjb3xvb7rNupuPj4+Pj4+Pj5ubm5uurq6uurq6use6usfHx8fHx8fHx8fHx8fT09PT09PT7P//////+Pjs7NnZx66hlYJvV0pKSkpKSkpXSkpKSkpKSkpKSko+SkpKPkpKPj4+Pj5KSj5KSj4+PkpKPj4+Pj4+MTExMTExMTExMTElMTExMTExMTExMTEx
+MSUxMSUxMSUlMSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6urq6urq6urq6rqGVgmNjdnZpdoi62dm6oY+Cj4+Pj4+Pj6GhoaGurq6urq6u
+wMDAwMDAwMDAwMDAwMDAzc3Nzc3NzdnZ2ebm2ebm5ubm5ubZ2dnHx66PgmlQUERERERQUFBQUEREREREREREREREREREREREREREREREREREREQ3N0RENzc3Nzc3Nzc3Nzc3NzcrNzcrNzc3JTc3Kzc3NysrKysrKysrKysrAABpgoKCgoKCgo+Pj4+Pj4+P
+j4+Pm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnj3Z2dmN2gml2rs3m8tnAoYiIiIiIlZWVlaGhoaGurq6uurq6urq6x8fHx8fHx8fHx8fHx8fHx8fZ2dnZ2dnZ2ebm
+2ebm2dnZ5ubm5s26p49vXUpKSkpKPj5KSkpKSkpKSj4+Pko+Pj4+Pj4+Pj5KPj4+Sj4+Sj4+Pj4+Pj4+MTExMTExMTExMTElMTElMTElJTExJTExMTExMSUxMSUlJQAAaYiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlaGVoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+oaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqurrquoY98Y2N2dml2gqfAwN/s7OzTupWViIiIiJubm6enp6enp6e6urrHx8e6x8fHx8fHx8fHx8fHx8fH09PT09PT09PT39/f39PT09PT39/f39/Tx6eIaUo+PkpKPkpKPj5KSj5K
+SkpKSj4+Pj4+Pj4+PkpKPj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMTExJTExMTExJTc3KysrKysrKx8AAGmCgoKCgoKCgoKPj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6entLS0tLSntLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tKeIb29vY3Z2aXauurrHx9nm8vLy07ShlYiIoaGhoaGurq6urq66urrHx8fHx8fHx8fHx8fHx8fHx9PT09PT09PT09PT09PT09PT09PT09/f39/f07SVb1A+Sko+Pj5KPj4+Pj5KSkpKSj4+Sko+PkpKPkpKPkpKPj4+Pj4+Pj4+PjEx
+MTExMTExMTExMTExMSUxMTExMTExMTExJTElJSUlAABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKenj4JjY3ZpaXaCp7TAwMDAwNPT
+5vLy8vLZwKebm5ubp6enp7S0tLTAwMDAwMDAwMDAwMDAwM3Nzc3Nzc3Nzc3Z2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZx66CYz4+Pj4+Pko+Pj4+Pj5KSj5KSj5KSj4+Pj4+Pko+Pj5KPj4+Pj4+PjExMTExMTExMTExMTExMTExMTExMSUxMTExMSUlJSUlJQAA
+aYiIiIiIiIiIiIiIiIiIiJWVlZWVlZWhoZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urrq6uq6urq66urq6urq6oYh2dmlpdnZpfK66urrHx8fHx8fH09Ps7Pj/8t/Huqenp6enp7q6urq6urrHx8fHx8fH
+x8fHx8fH09PT09PT09PT09PT09PT09PT09PT09PT09PT09PHrohXREREREQ3Nzc3RERERERERERERERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Kzc3JTExJTExJSUxMTExMSUlJTElJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5uPm5ubm5ubm5ub
+m5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6ePfF1vb29vb4Knuse6x8fHurq6urrHx8fZ2eb4+Pj4+ObTx7SntLS0tLS0wMDAwMDAwMDNzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ
+2dnZx7Shb0pKPj5KSj4+Pj4+PkpKPj4+Pj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMSUxMSUxMTExMTExMSUxJTExMSUlJSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp7S0tLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShgnZ2aWl2dmmCrrq6urrHurq6urq6urrHx8fT09Pf3+z4////////5tPAwMCurrq6usfHx8fHx8fT08fT09PT09PT09PT09PT09PT09PT09PT09PT08e0oXxXSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTElMTElJTExJTExJSUlJSUlJQAAaYKCgoKCgoKCgoKPj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp7S0tKe0tLS0tKe0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oZV8
+Y298aWl2iK7AwMDAwMDAwMDAwMDAwMDAwMDAwMDNzc3Z2ez////////y8ubT09PHx8fHx8fHx8fH09PT09PT09PT09PT09PT09PT09PT09PT08e0tI9vSkpKPkpKPj5KPj4+Pj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMSUlMSUl
+JTElJTExJTExJSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6urq6urqurq6urpuCdnZjY3Z2Y4i0wMDAwMDAwMDAwLS0tMDAwMDAwMDAwMDA
+wMDAwMDNzc3N2dnm5vLy8vLy8vLm5ubm09PT09PT08fT09PT09PT09PT09PT09PTx7S0oYh2aUpKSko+Pj4+Pj4+Pj5KSkpKSko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMSUlMTElMTExMTElJSUlJSUlAABpgoKCgoKCj4+Pj4+Pj4+P
+j4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeViHxjY3Z2aWmhtMfHx8fHx8fHx7q6urq6x8fHx8fHx8e6urq6urq6urq6x8fHx8fHx8fH09PT09/f39/s7Ozs7N/f
+7N/f7Ozf39/T09PHurquoaGPgnZ2aVdERERERERENzdEREREREREV0o+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExJTElJSUxMSUlMTElMTExJSUlJSUlJQAAaYKCgoKCgoKCj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6en
+p6enp6enp6e0tLS0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oYJpdnZjb3xplbrHx8fHx8fHx8fHx7q6urq6x7q6x8fHurq6urq6urq6uq6urq6urrq6urq6urq6urq6urq6urq6uq66uq6uurqnp6ebm5uIiIiIdnZ2dmNXSkpKSko+Pj4+
+PkpKSkpXV1dXSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMT4xMTExMTExMTExMSUlMSUxMTElJSUlJSUAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0p5WIdmNjfGlpdqG0x8fHx8fHx8fHx8e6urq6usfHx8fHx7q6urq6urq6urq6rq6urq6urq6urq6urq6urq6hoaGhoaGhoaGhusfHrpWViIiIfIh8fHx8b29vV0pKSkpKSj5KSkpKSkpKSkpXSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+PjExMTExMTElMTElMTExMTExMTExMSUxMSUlJSUlAABpgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp7S0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShfGl2dmNvfG+husfHx8fHx8fH
+x8e6urq6usfHx8fHx8e6urq6urq6urqurq6urq6urq6urq6urq6urqGhoaGVlZWVlZWVlae0oY+Pj3x8iIh2dnZ2dnZ2Y1dXSkpKSj5KSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTE+MTExPjExMTExMTElJTExMTExJSUlMSUlJSUxMQAA
+aYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnj492Y2N2aWl2obrHx8fHx8fHx8e6urq6x8fHx8fHx8fHx7q6urq6urq6rq6urq6urq6u
+rq6uoaGhoaGhoaGVlZWVlZWIiIiIiIiIiIiIiHaCgnZ2doJvb29XSkpKSkpKPj5KSkpKV0pKSko+Sko+Pj4+Pj4+Pj4+Pj4+Sko+Pj4+Pj4+Pj4+MTExPjExMTExMSUxMTElJTExJTExJSUlMSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ub
+m5ubm5ubm5ubp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5t8b29vb298b6G6x8fHx8fHx8fHx8fHx8fHx8fHx8fHx7q6urq6urq6rq6urq6urq6urq6urqGhoaGhoZWVlZWVlYiIiIiIiIiIiIiIfHx8fHx8
+fG9vb29jV0pKSkpKSkpKSkpKSj5KSkpKSko+Pj4+Pj4+Pj4+Pj5KSj5KSj4+Pj4+Pj4xMTExMTExMTExMTExMSUlMTElMTElJTExJSUlAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6entLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGViHZjb3xpaXynusfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6hoaGhoaGhlZWhj4+Pj4+Pj4+Pj4+CgoKCdoKCdnZ2dnZ2Y1BQUERERERERERERERERERERERERERERERE
+RERERERERERERERERERERERERDc3Nzc3Nzc3Nys3NyUxMSUlMTExMTExJTExJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnm3Z2
+dnZjdnZpp8DAwMDAwNPHx8fHx8fHx8fHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6urqGhoaGVlZWVlZWVlYiIiIiIiIiIiIh8fHx8fHx8b29vb2NXSj5KSkpKSko+Pko+PkpKPko+Pj4+Pj4+Sko+Sko+PkpKPkpKPkpKPj4+Pj4+Pj4xMTExMTE+MTEx
+MTExMSUlJSUlMTElJTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pm5ubj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6e0tLS0tLSnp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oY+PdmNvfGlpgqe6x8fHx8fHx8fHx8fHx8fHx8fHx8fHx7q6
+urq6urqurq6urq6urq6urq6urqGhoaGhoaGVlZWVlZWViIiIiIiIiHx8fHx8fHx8fHx8b29vSkpKSj5KSj4+Sko+Pko+PkpKPj4+Pj5KSj5KSj4+SkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMSUxMSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+P
+j4+Pj4+Pm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6e0tLS0tLSntLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeVdnZ2dml8fHynx8fHx8fH08fHx8fHx8fHx8fHx8fHx8fHx7q6urq6uq6urq6urq6urq6urqGhoaGhoaGVoZWIlZWI
+iIiIiIiIiIiIiHx8fHx8fHx8fG9vY0pKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+PkpKSkpKSj5KSj4+Sko+Sko+SkpKPkpKNzc3Nzc3Nzc3Nzc3NzcrKysrKysrKysrKysrKwAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pj4+PoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurqurq6hlYhvY3Z2Y2OIrsDAwMDNzc3Nzc3NzcDAwMDAwMDAwMDAwMDAtLS0wMC0tLS0p6enp6enp6enp6enp6ebm5ubm4+Pj4+Pj4+Pj4+PgoKCgoKCgm98fHxpaWlKPko+Pj4+Pj4+
+Pj4+Pj4+SkpKSkpKSkpKSko+Pj4+Pj5KSj4+Sko+Pj4+Pj4+Pj4+MTE+MTExMTExMTExJTElJTElJSUxMSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0p492aXZpaXx8fK7Hx8fHx9PTx8fHx8fHx8fHx7rHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6hoaGhlZWhlZWVlYiIiIiIiIiIiIh8fHx8fHx8fG98fG9jSj4+Pj4+Pj4+Pj4+Pj4+Pj5KSkpKV1dKSko3Nzc3Nzc3NzdERERERERE
+REREREQ3Nzc3Nzc3Nzc3Nzc3NysrKysrKysrKysrAABpiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6uuq6urq6urq6urq66urq6urq6urqurpuIfHxpaXx8aY+6x8fHx9PT09PT
+x8fHx8fHx8e6urrHx8fHurq6urq6urqurq6hoaGhoa6uoa6uoaGhoZWhj4+Pj4+Pj4+Pj4+Pj4+CgoKCdoKCdnZ2dmlpXUREREREREQ3N0RERDc3NzdERFBQUFBQUFA+Pj4+MTE+Pj5KSj4+Pj4+Pj4+PkpKPj4+Pj4+PjExMTExMTExMTExMTExMTExJQAA
+aYKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLTH09Pf7Ozs39/T08fHx66bj29jb29jb2+PtMfHx9PT09PHx8fHx8fHx8fHx8fHx8fHx8e6usfHurq6urq6rq6urq6u
+rq6urqGhoaGVlZWVlZWVlYiIiIiIiIiIiIh8fHx8fHx8fHxvb1c+Pj4+Pj4xPj4+Pj4+Pj4+PkpXV1dXY1BQPj4xPj4+Pj4+MT5KPj4+SkpKSj4+Pj4+Pj4+Pj4+MTE+PjExMTExMTElMTElJTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+PoaGhlaGhoaGh
+oaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6ursfZ7Pj4+Pj///j4+Pj47OzZx7Shj29vb29jj8DA08fH09PT09PHx8fHx8fH08fHx8fHx8fHurq6urq6urq6uq6urq6urq6urq6hoaGVlaGVlZWViIiIiIiIiIiIiHx8fHx8fHx8
+fHx8b2NjPj4+Pj4+Pj4+Pj4+Pj4+Pj5KSkpXV1dXSko+Pj4xPj4+Pj4+Pj5KSj5KPj4+Pj4+Pj4+Pj4xMT4+PjExMTExMTExMTExMSUlAABpgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj6GhlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6u
+rq6urq6urq6urtPs+P/////////////////////4+ObNroh8fI/A09PT09PT09PHx8fHx8fHx8fHx8fHx8fHx8e6x7q6urq6urq6rq6urq6urq6urqGhoaGhoaGVlZWVlZWIiIiIiIiIiHx8fHx8fHxvb29vUD4+MTE+PjE+Pj4+Pj4+Pj4+PkpKSl1dUFBE
+RDExPj4+Pj4+Pj4+Pj4+PkpKPj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMQAAaYKCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6entLS0p7S0tLS0tLS0tLS0tLSnutPs+Pj47Pj47Pj/////////////////
+//jmx6e62dnZ2dnZ2c3Nzc3Nzc3Nzc3Nzc3Nzc3NzcDAwMDAwMDAwMC0tLS0tLS0tKenp6enp6enm5ubm5uPj4+Pj4+Pj4+PfHyIfHx8fHx8fHxpaVc+MT4+Pj4+Pj4+Pj4+Pj4+Pj4+SldXV1dKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4x
+MTExMTExMTExMTExJSUAAGmIiIiIiIiIiIiIiIiIiIiIiJWVlZWVlZWhoaGhoaGVoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6uutPm8vLm5s3Nzc3Nzc3Nzdnm5vLy///////////s7N/T09PT09PT08fTx8fHx8fHx8fHx8fHx8fH
+urrHurq6urq6urq6urqurrqurq6uoaGhoZWVoZWVlZWVlYiIiIiIfIiIfHyIfHx8fG9vb2NKPj4xMTExMTExMTE+PjExMTE+Pj4+SldKSko+Pj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+MTExMTExPj4xMTExMTExMTExJTExMTExAABpiIiIiIiIiIiIiIiIiIiI
+lZWVlZWVlZWVlZWVoaGViJWhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6ursfm5ubZ2cfHx7q6urq6urrHx8fT09Pf7Oz4//////Lm09PH09PT09PT09PT09PHx8fHx8fHx8fHx8e0tLS0tLTAtLS0tLS0tLS0p6enp6enm5ubm5ubj4+P
+j4+Pj4+Pj4+CgoKCdoKCdnZ2dnZpSjExMTExMT4xMTExMTExMTE+Pj4+Pj4+Sko+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+Pj4+MTE+PjExMTExMTExMTExMQAAaXyIiIiIiIiIiIiIiIiIiIiVlZWVlZWVoaGhoaGhj3yPoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urrrZ2ebTx8e6uq6urq6urq6urq6uusfH09PT3+zs+Pj45tPT09PT09PT09PT09PT09PTx8fHx8fHx8e6urq6urq6urq6rq66uq6urq6urq6hoaGhoaGhlZWViIiVlYiIlYiIiIh8fHx8fHx8b29vY0o+Pis3Nzc3Nzc3
+Nzc3Kys3Nzc3Nzc3Nzc3Sj4+Pj4+PkpKPj4+MTExMTE+Pj4xPj4+Pj4xMTExMTExMTExMTExMTExMTExMTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5uPm5ubm5ubgoJvfI+hoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq7H2dnZx8e0p6en
+p6enp6entLSnp6e0wMDNzdnZ2ezs2dnNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NzcDAwMDAtLTAwMC0tLS0tLS0tLS0tKenp6enp6eVlaGVlZWVlZWVlYiIiHx8fHx8fHx8fHx8aWlKMTExMTExMTExMTExMTExMTExMTExMTE+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMTElMTExAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pm5ubm5uPj3xjV2+Pm5ubp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS009PTx7q6rqGhoa6urq6urq6urq6urq66x8fH09PT09PTx9PT09PT08fT
+09PT09PT09PTx8fHx8fHx7q6urq6urq6urq6rrqurq6urq6uoaGhoaGhoaGVlZWViJWVlZWViIiIiHx8fHx8fHxvb29jSjc3JTExMTExMTExMTExMTElMTExMTExMT4+Pj4+Pj4+Sj4+Pj4+MTExMT4+MTExMTExPjExMTExMTExMTExJTExMTExMTExMQAA
+aYiIiIiIiIiIiIiIiIiVlZWVlZWVlZWhlZWIfG9XV2Njgpubm5ubm5unp6enp6enp6enp6enp7S0p6enp6entLS0tLS0wNPTx7q6rqGhoa6urq6urq6urq6urq6urq7AwM3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NwMDAwMDAwMDAwLS0tLS0p6en
+p6enp6enp5ubm5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgnZ2dnZ2aUoxMTExMTExMTExMTExMTExMTExMTExMTExMT4+Pj4+Pj4+PjE+Pj4+Pj4+Pj4+MTExMTExPjExMTExMTElMTExMTExMSUxJSUAAG+IiIiIiIiIiIiIiIiIiIiIlZWVlZWhlYh8b11KV1dK
+Y4ibm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6entLS0tMfHx7q6p6ebp6enp7S0tLS0tLS0tLSnp6enusfH09PT08fHx9PT09PT09PT09PT09PT09PHx8fHrq6Pb2+Cgpu0tLS0wLS0tKenp6enp6enp6enp5WhoY+Pm4+Pj4+Pj4+Pj4+PgoKCgnZ2
+dnZ2dlc+PjElMTExMTExMTExMTExMTExMTExMTExPj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+MTExMTExMTExMSUxMTElMTElJTExJTExJSUlAABpiIiIiIiIiIiIiIiIiJWVlZWVlZWVlYKCaVdXV0pKXV12laGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urrrNzc20tKGhoaGurq6urq66urq6uq6urqGVp7TAwM3Nzc3Nzc3Nzc3Z2dnZ2dnZ2c3Nzc3Nzc20lWk3NzcrHzE+SnantLS0tLSnm5unp6enp6enp6enm5ubm4+Pj4+Pj4+Pj4+Pgo+PfHx8fHx8fGldPjExMTExMTExMTE+MTExMTExMTExMTExMT4+
+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+MTExMTExMSUxMTExMTElNzc3NzclMTElJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pm5ubj49vXV1dSkpXV0pXfI+hoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq66x8fHtKenlaGhrq6urq6urq6urq6urqGViJWn
+usfHx8fHx8fHx8fT09PT09PT09PT09PT08ehfD4lPjESJT4fHzE+PnantLS0tKGurq6urq6hoaGhoaGhlZWVlYiVlYiIlZWIiIh8fIh8fHx8fHxpVzc3NysrKysrKzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N0RERERENzc3Nzc3Nzc3Nzc3NzcrKysrKzc3Nzc3
+Nzc3NyUxMSUlJSUlJSUAAGmIiIiIiIiIiIiIiJWVlZWVlZWVlXxjV1dKSldXRERdUGOIm5ubm6enp6enp6enp6enp6enp6enp6enp6enp6enp6enwM3NwLShoZWhrq6urq6urq6urq66uq6uj3aClafAwMDAwMDNzc3N2dnZ2dnZ2dnZ2dnZzc2uYys+Ph8x
+V0o+SjcYGDExY6G0tLS0tLS0tKenp6enp6eVlaGVlZWVlZWVlYiIiIiIiIh8fHx8fHx8aVc+MTExMTExMTExMTExMTExMTExMTExMTE+Pj4+Pj4+Pj4xMT4+Pj4+MTExMTExMSUxMTElMTExMTExMTExJSUxMSUlMSUlJSUlAABpiIiIiIiIiIiIiIiIiIiI
+lZWViHZjV0o+V1dKSldXSldpgpWhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6ursfHx7quoZWVoaGurq6urq6urq6uurquoZWCaXyhtMfHx8fHx8fT09PT09PT09PT09PT09OnXT4+MVeVp6e0tKGIdlcYJURXj7S0tLS0p6enp6enp6enp5ubm5uP
+j4+Pj4+Pj4+Pj4KCgoJ2dnZ2dnZXNzc3KysrKysrNzc3Nzc3Nzc3Kzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3JSUlJSUlMSUlJSUlJTElMTExJSUxMSUlJSUlJQAAaYiIiIiIiIiIiJWVlZWVlZWVfGlpV0REV0pKV1dKV1dXV4KVoaGhoaGhoaGhoaGh
+oaGhoaGhoa6urq6urq6urq6urq7Hx8e0tJubm6enp6e0tKe0tLS0tLS0tKePdnZ2gqfAwMDAwMDT09PT09PT09PT09PT09O6dlclPo+6urrHurq6uqeVdjcfMWmVrrqurq6urqGurqGhoaGhoaGVlZWVlZWVlZWViIiIiHx8fHx8fG9vVz4xMTExMTExMTEx
+MTExMTExMTExMT4+MTExMTExPjExPjExMTExMTElJTExMTExJSUlJSUlMSUlJSUxJSUlJTElJSUlJSUlJSUAAGmIiIiIiIiIiIiIiIiIlaGVfGldRERXSj5XV0pKV1dKV2N2j6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6ux8fHtKebj4+np7S0
+tLS0tLS0tLS0tLShlYJjY4KhtLS0wMDAwM3Nzc3Nzc3Nzc3N2dnHm1AfXZu0x8fHtI+CdqG6uqGCUBgxaaGhrq6urq6urqGhoaGhoZWhoZWVlZWVlZWViIiIiIiIfHx8fHxvb1c+Pj4xMTExMTExJTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlAABpiIiIiIiIiIiIlZWVlZWIfGlXSldKPkpXSkpdUD5QXUpKb4ibp6enp6enp6enp6enp6enp6enp6enp7S0p7S0tLS0tMfHurqhlZWVp6e0tLS0tLS0tLS0tLS0p4h2dmlpj66urrrHx9PT09PT09PT
+09PT09PTumM+V4i008e6x7pjMTGCp7q6oW9KKyuCrq6urq6urq6uoaGhoaGhlZWVlZWVlZWVlZWIiIiIfHx8fHx8fGlXPjExMTExMTExMTE+MT4+PjExMTExMTExMTElNzc3Nzc3Kys3Nzc3NysrKysrKysfMSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJQAA
+aYiIiIiIiIiIiIiIiIiIdl1EV0o+SldKPldXSkpdXUpdXV2Im5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0p7rHx8eum5uIm6enuq6urq6urq6urq6urqGPgmNjdoKhoa7AwMDNzc3Z2dnZ2dnZ2dnZzY9jN2m007SIb1dEREQlaaGurq6uaSs3So+u
+rq6urq6urq6hoaGVlZWVlZWVlZWVlZWIiIiIiIh8fHx8fHxpVzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyUxMTElJTElJTExMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJSUlJSUYGCUAAGmIiIiIiIiIiIiVlYiIaVdXV0REV0o+V1dKSldXRFdX
+SkpjgpWhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq66x8e6rpubiKGhtLS0tLS0tLS0tLS0p7SniHxvb2N2j5uuurrH09PT09PT09PT09PT09OCN12nwM2nVyU3Nx8fPj4+b5uurqFjJT6Coa6urq6urqGhoaGhoaGVlaGVlZWVlZWVlYiIiIh8fHx8
+fHxvY1A+MTExMTExPjExPj4+Pj4+MT4+PjExMTExMSUxMSUxMSUlMTExMTExJSUxJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUYAABpiIiIiIiIiIiIiIh8XVBQPj5QUDdQUFBEV1dKV1dKSl1dUHaPoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6u
+rq6uusfHuqeVlYihrq6urq6urq6urq6urq6uoY+CY298aYKhrrrHx9PT09PT09PT09PT09OnY0p2p8fTgkpKMR8+PhglNx8xiK6hglclSpuurq6urqGhoaGhoaGhoaGVlaGVlZWVlYiIiIiIiHZ2goJvb29KMTExMTExMTExMTExPj4+Pj4+MTExMTExJTEx
+MSUxJSUlMTExMTElMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJQAAaYiIiIiIiIiViIhpSkpKPkpKSj5XV0REV1dKV1dKSl1dSl18j6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urrrHx7ShlYiIoaGurq6urq6urq6urq6urqGIb29v
+Y2+Cj7TAwM3Nzc3Nzc3N2dnZ2dnHj1BQocfTtHZKJUppUDExMR8fN3yhrqFKJVePp6e0p6enp6enp5ubm5uPj6GVlaGVlZWVlYiIiHx8fHx8fHxjREQxJTExMTExMTExMTExMTExMTExMTExMTExMSUxMSUlJSUxMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRglJRgYJRgAAGmCgoKCj4+Pj29QUFBQRFdXPkpKPj5QUD5QY0pKXV1KSl1daY+hoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq66x7q6oZWIiKGurq6urq6urq6urq6urq6ViHxpaXZ2aYi0tMfT09PT09PT09PT09PT028xfLTH08BjK2mh
+rrq6oXxjPh92rq6bdj4ldqenp6enp6enp6enp5ubm5ubm5uPj4+Pj4+Pj4KCgnZ2gm9vYz4xMTExMTExJTExMTExMTExMTExMTElMTExJTExJSUxMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYAABpiIiIiIiIiGNQUD4+Sko+
+SkpKPkpXRERdXUpXV0pKV1dKXW+ClaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6uwMDArqGViIihrq6urq6urq6urq6urq6uoYhvb29jb3x8p7rHx9PT09PT09PT09PT07pvRHa009OhY0Rpp8DAwMDAfDc3dqG0wHw+PmmVp6enp6enp6enm5ubm4+b
+m5uPm5uIiIiIiIiIfHx8fHx8fGNENysrKysrKysrKysrKzc3Nzc3Nzc3NzcrNyUlMTExJSUxMTExJSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYJRgYGBgYGBgYGBgYGAAAb4iIiIiIfF1KSj5QUD4+Sko3SkpKSldKSkpXSkpXV0pXV0pdiJWhoaGhoaGhoaGh
+oaGhoaGhoaGhrq6urq6urq6urrq6uq6hlYiIp6e0tLS0tLS0tLS0tLS0p5WIfGNjdnZpgrS0x9PT09PT09PT09PT09PHVz6VwM3NrkoxiLTHx8fHx5VQH2mnp7ShVx9XoaGurq6hoaGhoaGhlZWhlZWhlZWVlZWIiIiIiIh8fHx8b29jPjExMTExMTElMTEl
+MTExMTExMTExMSUxMTElMTEfHzElJSUxJTExJSUlJSUlJSUYGCUlJRgYGCUlJSUlJSUlJSUlGBgYGBgYGBgAAGmCgo+CaV1KN0pKPj5KSj5KV0REV0o+Sl1KSl1dSldjV0pdXXaPoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq66x8e0oY+CgqGurq6u
+rq6urq6urq6urq6hiG9vb2N2doKnusfT09PT09PT09PT09PTtGlQj8fTx49dRIK0x8fHx8eISj52p7q6m1c+Soinp6enp6enp6enp6enlZWhlZWVlYiIlYiIiIh8fHx8fHxvXT4xMSU3NysrKysrKysrKzcrKysrKzc3NysrKysrKysrKysrKysrKysrHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fAABpj498aVc+PkpKPkpKPkpKSj5QUEREV0pKV1dKSl1dSldjV1d8j6GhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6uwMDAwKGPgoKnp6e0tLS0tLSntLS0tLSnlYiIaWl2dmmItMDA09PT09PT09PT
+09PT08BKSqHH08eVPj6busfHx7q6oUoldq6urq5vJUSPoaGurqGhoaGhoaGVlaGPj4+Pj4+Pj4+Pj4KCgoJ2goJvb2M+MTExMTExJSUxMSUlMTElJTExMTExMTExMTElJSUlMSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAA
+Sl1pXUREREQ3Sko+PkpKPkpXSkpXSj5KV0pKV1dKV2NKSl1dXYihoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6ursDAwLShj4KCoa6urq6urq6urq6urq6urqGCdnZ2Y3x8iK7Hx8fT09PT09PT09PT09OuY0qVx9m6aVdXlcDAwMDAwIhKSm+hurqhb0o+
+gqenp6enp6enp5ubm5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgm9dNzcrKysrKysrKysrKysrPjExMTExMTExMTExMTElMTExMSUlJSUlJSUlJSUlJSUlGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgAAFdXREREREREREREREQ3SldKPldXRERXSkpXV0pXY1dK
+XV1KV2mClaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq7AwMC0oY+CgqGhrq6urq6urq6urq6urq6ViHxjb3xvb4+6x8fH09PT09PT09PT09PTx1dKocehfFclY67Hx8fHx8ehSiVvp7S0tHYlRIihoa6uoaGhoaGhoZWVoZWVlZWIiJWViIiIiIiIfHyI
+dnZ2Yz4xMTElMTExMTExJTExMTExMTExMTExMTExMTExMTElJSUxJSUlJSUlJSUlJSUlJSUlGBglJSUlJRgYGBgYGBgYGBgYGBgYGBgYAABXSkpKPkpXSj5KVz4+Sko+Sko+SldKPlBQUFBQUFBQY0pKXV1ddo+PoaGhoaGhoaGhoaGhoaGhrq6urq6urq6u
+rq6ursfHuqGVgoKnp7Snp6e0p7S0tLS0tLSnp3xvb29jdnaItMfH09PT09PT09PT09PT37pvSojAiD4lSoiux8fHx8fHj1A+Y6G6uqd2Sj6Cp6enp6enp6enp5ubm5ubm4+Pj4+Pj4+CgoKCgoKCdnZ2dlc+MSUxMTElJTElJTcrKys3Nzc3Nzc3Nzc3Nzcr
+NzcrKysrKysrKysrHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAAN0pKSkpKN0pKSj5KSjdKV0o+V1dKSldKSldXRFdXV0pXY0pXY2+Cm5ubm5unp6enp6enp6enp6enp6enp6enp6enp8DAwMCnlXyIp6enp6entLS0tLS0tLS0oY+PdmNv
+fG9vj7THx9PT09PT09PT09PT09PHYz6PrmlQUGOhwMDAwMDAwK5jJVeIp7q6byVEj6entKenp6enp6enlZWVlZWVlYiIiIiIiIiIiIh8fHx8b29dNysrKysrKysrKysrKysrKys3Nzc3Nzc3Nzc3NzclMTExJSUxJSUlJSUlJSUlJSUlJSUlJSUlGCUYGBgY
+GBgYGBgYGBgYGBgYGBgAADc3UFA3SldXSkpKPkpKSj5QUD5KV0o+V1dKSldXSl1dSldjV1dpgo+bm5ubp6enp6enp6enp6enp6enp6enp6enp6e0x8e6rpWCgqGurq6urq6urq6urq6urq6bgm9vb29vb4+0x8fT09PT09PT09PT09PTx49Kb7R8NzdXV2OI
+p6e0x7qniF0xPoiuoW9KSoinp6enp6enp6ebm5ubm5ubj4+Pj4+Pj4+Pj4KCgoJ2dnZpVzc3JTExMSUxMSUxMTElMTElMTExMTExMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYJSUYGBgYGCUYGBgYGBgYGBgYGBgYAAA+UFA3UGlpdmNQRERXRERX
+SkpKV0pKV1dKSl1KSldXSldjV1djY2+Im5ubp6enp6enp6enp6enp6enp6enp6enp6enwMDAwKePfIihrq6urq6urq6urq6urq6hj3x8Y2N8fGmhwMDNzc3Z2dnZ2dnZ2dnZ2c18PnyhSjdKJRg+PiU+XV1dgnY+MUqCoZVXJWmhrq6urqGhoaGhoaGhlZWV
+lZWViIiIiIiIiIiIiIh2goJ2aUoxJSUxMSUxMSUlMSUlMTElMTExMTExMTExJSUxMSUxMSUlMTElJSUlJSUlJSUlJSUlJSUYJSUYGCUYGBgYGBgYGBgYGBgYGBgYGAAAREREV2l2dnZXSldKPkpKPkpXSj5QUEREXVBQUGNQUGNjSl1dUF1vgo+hoaGhoaGh
+oaGhoaGhoaGurq6urq6urqGhobrHx7Snm4KCoaGurq6urq6urq6urq6uoaF8aXZ2aWl8obTH09PT09PT09Pf39/f39/NlVdXiHxQMT4+Hx8+PhgxMR8fPisriK52Nzd8oa6urq6urqGhoaGVoaGVlZWVlZWViIiIiIiIiHx8fHx8fGlKMTExMTExJTExJTEx
+MTExMTExMTExMTExJTExJTElMSUxMSUlMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBgYGBglJSUlJRgYGBgAAD5XSld8fHx8aUo+Sko+SkpKPkpKSkpXV0RXV0pKXV1QUGNXV2NjV3aPj6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq66x8fHtJuIiKGurq6u
+rq6urq6urq6urqGPgoJpaXx8b6e6x9PT09PT09PT09Pf39/f36dKV4iuuq6PfHxXHx8+JRg+MRglSoiIaT4xgq6urq6urq6urqGhoZWVoZWVlZWIiIiIiIiIiIiIiHx8fG9jSjElJTExMTExJTExJSUlJSUxMTElMTElMTElJTElJSUxJSUlJSUlJSUlJSUl
+JSUlJSUlGBglJSUlJSUlJRgYGCUYGCUlGBgYGBgYAAA+PkpKaWl2dmNKSko+Sko+SldKPldXRFBQUFBQY0pKY1dXY2NKXWlpdo+hoaGhoaGhoaGhrq6urq6urq6urq6urq6uusfHurqhiHybp6enp7S0tLS0tLS0tKenlXxpdnZpdoKnusfT09PT09Pf39/f
+39/T09PHdjdptMfHx9O0fF0xMWNjNzc3JTePjzclY5Wnp7Snp6enp6enp6enm5uPj4+Pj4+Pj4+CgoKCgoKCgnZ2aUo3NzcrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAA
+PlBQPml2dnZ2Vz5QUD5KV0o+UFBERF1dSldXSkpjV0pXY1BQY2NQY3aIlaGhoaGhoaGhoaGhrq6urq6urq6urq6urq7Hx8e0oY98la6urq6urq6urq6urq6uoYiIdmNjfHx8rsDA09PT09PT09PT39/T09PT049dSojHx8fT05U+Pm+hrrqnj4+hj11KN2Ob
+tLS0tLSnp6enm5ubm5ubm4+bj4+Pj4+Cj4+CgoKCgoKCgmNEMSUxMSUxMSUlJTElJTElJSUxMTExMSUxMTElJTElJSUlJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglJSUYGBgYGBgYGBgYGBgAAERERFBddnZ2dl1dRERERERQUFA+UFA+SldKSldXSkpj
+V0pjY0pdaV1dfI+bm6enp6enp6enp6enp6enp6enp6enp6e0wMDAwK6bgoKhoa6urq6urq6urq6urqGPdml2dml2gqe609PT09PT39/f39/f39/f09O0Vz52p8fT08CniF18tMe6use6rpVXJUqbp6e0tKenp6enp6enm5ubm5ubj4+Pj4+Pj4KCj498iIh8
+fHxpSjExMTExMTExMTExMTElMTElJSUxJTExMTExJSUlJSUlMTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYAAA+Sko+Y29XV3ZdRERXRERQUD5XV0pKV1dEV1dKSmNXSmNjV1djY1BjY2N8m5unp6enp6enp6enp6enp6enp6en
+p6enp8DAwMC0oYiIp6entKentLS0tLS0tLSbiIh2Y2N8b2+uwMDT09PT09PT09Pf39/f09PTx6FKMXy0x8fTx8fHx8fHx8fHx6FXJT5vm7S0tKe0tKenp6enp6ebm5ubj5uPj4+Pj4KCgoKCgoKCgoJ2dkoxJTExJSUlJSUlJSUlJSUlMTElJTElJTExJSUl
+JSUlJSUlJSUlJSUlJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglGBgYGAAAREREV0pdNzdjY1dKSldKSkpXSkpdUD5QXUpKV1dEV2NXSmNjUGNjY1dvfIibp6enp6enp6enp6enp6enp6enp6enp6e6x8fHuqeViKGhoa6urq6urq6urq6hoY98b29v
+b3yIrsDNzc3N2dnZ2dnZ2dnZ2dnZ2dmub1A+grrHx8fTx8fHx8fHx6djSjFKj7S0tLS0p6enp6enp6enp5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgm9KNzclMTExMTExJSUxMSUlJTElMTElJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJRgYGBgAAD5XSkpXaTc3aWlKV1c+SldERERXSkpXSj5XV0RXY1dKY2NKV2NjV2NjV2mPoaGhoaGhoaGhoaGhoaGurq6urq6urq6uusfHx8e6oYiVoaGurq6urq6urq6urpuIfHxpdoJ2gq66x9PT09PT09PT09Pf39/f09PT06dKSkppp8fH
+x8fHx8fHuo9vShhKiKe0tLS0tLS0oa6uoaGhoZWhoY+Pm4+Pj4+Pj4+PgoKCgoKCgnZ2Sj4xMTExMTExMTElJSUlJSUlJSUlMSUlJTExJTElJSUxJSUlJSUlJSUlJSUlJRglJSUlJSUlJSUlJSUlJSUlJSUYGBgYGBglGBgYAABERFBQPmlXH0ppV0pKSko+
+V1dKSldXRFdXSkpjV0pdXVBQaWlQY2NXV2Njb4+hoaGhoaGhrq6urq6urq6urq6urq6uoa7Hx9PHuq6VlaGhrq6urq6urq66rqGhj3Z2dnZ2go+nwNPT09PT09PT09PT09/f09PT09PHlUoxSmmPus3Nzc3Np49jMR9KdqG0tLS0tLS0p7Snp6enp6enm5ub
+m5ubm4+Pj4+Cgo+CgoKCgoKCb1dERDc3Nzc3Nzc3Nzc3KysrKysrKysrKysrKysrKysrKysrHx8fHx8fKysrKysrKx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAAPlBQUFBjYytKaVdXSj5KV0pKV1c+UFBQUFBQRFBQUFBjY1dXaV1daWlQY3yIlaen
+p6enp6enp6enp6e0tKenp6enp6enwMDT08e6p5Whrq6urq6urq6urq6um4+Cb29vgm+ItMfH2dnZx9PT09PT39/f39/T09PT07qhXSUxSldpj492aWkxHzFKaaG6uq6urq6urq6urq6hoaGhoaGhj4+Pj4+Pj4+Pj4+CgoKCgoKCdnZjV1dKSkpKSj4+Pj4+
+PjExMTExMTExMTExMTExMTExJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglJSUlJSUlJSUYGBgAAEo+SldKY2MxV2lpSj5KV0pKV0o+V1dKSldXSldjV1djY0pjY1dXY2NKY2Njgpubm6enp6enp6enp6enp6enp6enp6enp7rH09PHx7ShoaGh
+rq6urq6urq6uoaGVfGl2gnaCobrH09PTx5uux9PT39/f39/f09PT09PTunZdSiUlPjEYMT4fHz5XiK66urq6uq6urq6urq6uoaGhoaGhlaGhlZWVlYiIiIiIiIiIfHx8fHx8fG9vb2NjY2NXV1dKSkpKSkpKPj4+Pj4+PjE+PjExMTExMTExMTElJSUlJSUl
+JSUlJSUlJSUlJSUYGCUlJSUlJSUlJSUlJSUYGCUlAABEV1dKSl1pRGN2XVBQUERXSj5KV0pKV1c+UGNKSl1dSldjV1djY1djb11daYKPoaGhoaGhoaGhrq6urq6urq6urq6urq6ux8fT09PHtKGhoa6urq6urq6urq6hiIh8b2+IiJW6x9PT3643N2Obx9nZ
+2dnZ2dnZ2dnZ2dnHoWNjShglPiUYPldvobq6urq6urq6uq6urq6urqGhoaGhoaGhj5ubiIiIiIiIiIiIiIh8fHx8fHx8iHx8fHxvb29jY2NjY2NjY2NXV1dXSkpKSkpKSj4+PjExMTExMTExJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJSUlJSUYGAAA
+REREV0pKdldXb29QRFdXPkpXSkpKV0RQXV1KV1dKV2NXV2NjV1dvY0pjY1d2j6Ghoa6uoaGurq6urq6urq6urq6urq6ursDA09PT08e6p6enp6entLSntLShoZWIiIiIiIinusfZ2dmnMQwAEmO62dnZ2dnZ2dnZ2dnZzc3NoXxvUD5ddoinwMC0wMDAwMC0
+tLS0tKe0tKGhoaGhoaGVlaGPj4+Pj4+PgoKCgoKCgoKCgoKVlZWVlYKCj4+CgoJ2goJvb3x8b29vb2NjY2NXV1dXV0pKSko+Pj4+Pj4xMTExMTExMSUlJSUlJRglJSUlJSUlGBgYGBglJSUlJSUAAEpXSkpXSmlpaXZjV1dKSldKPldXRERXV0pKY0pKY2NQ
+XWlXV2lpV2NjV1dpdoihoaGhoaGhoaGhoa6urq6urq6urq6urq7Azc3Z2dnZ2bqurq6urq6urq6urq6hoZWIiJWVobrN2dnmmyUlDAAAfMfZ2dnZ2dnZ2dnZ2dnZzc3NwMDAtLTHx7rHx8e6urq6urq6rq6urq6urq6hoaGhoZWVlZWIiIiIiIiIiIiIiHx8
+iIiIiJWVlZWVlYiVlYiIlYiIiIiIiIh8fHx8b29vb29vY2NjY2NXV1dKSkpKSj4+Pj4+MTExMTExMTExJSUlJSUlJSUYGCUlJSUlGBglAABKPlBQUERpfHx8fFdKV0pKSldKSldXPlBdSkpdXUpXY1dKY2NXY29dXV1dSmmIm5unp6enp6enp6enp6enp6en
+p6enp6enusfT09/f39/TwLS0p6enp7S0tLS0tKGhoaGVobS0x9nZ2Y8xdnZEAEq609Pf39/f38e609PH2dnZ2dnHx8fHx8fHx8fHx7q6urqurrqurq6urqGhoaGhoaGhoY+Pj4+Pj4+CgoKCgoKCgoKCgo+Pj5uPj4+Pj4+Pj4+Pj4+Pj4+Cgo+Pgo+CgoKC
+gnaCdnZ2dmNjY2NjY1dXV1dXSkpKSko3Nzc3Nzc3NzcrKysrKysrKx8fHx8fHwAASldKSldKV3Z2dnZdXUREV0o+V1dKSl1QUFBdSkpjV0pdXV1daWlQY29XV2NjfJubp6e0tLS0tLS0p6enp7S0p6enp6enp7TAwNPf3+zs39/Nuq6urq6urq66rq6urqGh
+rq6ux9Pf39+PN5vHoR8xp9Pf39/f39NvN4ihlbrT09PT09PHx8fHx8fHx8e6urq6urqurq6urq6hoaGhoaGhlZWhlZWVlYiIiIiIiIiIfIiIiIiIiIiIiIiIiIiIiIiViIiIlYiIlZWIiIiIiIiIiIiIfHx8fHx8fG9vb29vY2NjY2NjV1dXV1dXSkpKSkpK
+Pj4+PjExMTElMTElJSUAAEpKV1dKSld2dnaIY0pXSj5QUFBQUFBEV1dKSldXSldjV1djY1djY2NXaWlKY3yIm666usfHx7q6urq6urq6urqurq6urq6ursDA2ebm5vLy5tnHtLS0tLS0tLS0p6e0tKG0tMDT39/fiDGIx64lMafT09PT09/NVwZKMQBdtM3N
+zc3Nzc3AwMDAwMDAtLS0tLS0tLS0tKenp6enp6enlaGVlZWVlYiIiIiIiIiIiIiIfHx8iHx8iIh2iIh8fIiIfIiIiIiIiHyIiIiIiIh8iIiIfIiIfIiIfHx8fHx8fHxvb29vb29jY2NjY2NjY2NjV1dXV0pKSkpKPj4+PjExAAA+UFBEXV0+b3x8fG9dSkpX
+Sj5XV0pKV1dKV1dKV2NXV1djV1dpaVdpaVBQXV1diKG0x9PT09PT09PTx8fHx8fHurq6uq6urq66usfZ5vLy8vLy5tPHx7SntLS0tLS0p6e0tLTH2dnZ2YgxSnZ2GEq0x6GVp8fZzVcMSiUAV67Hx8fHx8fHx8e6x8e6use6urq6rq6urq6urqGhoaGVoaGV
+laGPj4+Pj4+Pj4KCgoKCgoKCgoKCgoKCgoJ2iIh2goKCdoiIfHyIiHx8iHaCgoKCgoKCgoKCgoKCdoKCgnaCgnZ2dnZ2dnZ2dnZ2dnZ2Y2NjY2NXY2NjV1dXSkpKSgAARERQUD5KSmN8fGlXSldKPldXRERXSj5XV0pKY1dXV2lXV2NjV2NjV1djY0pXaYKh
+wNPT39/f7Ozf39/f39/f09PTx8fHx8fHx8fH09Pm8vLy8vLy8ubZzc3Nzc26usfHx9PTx9/f39+CHwAAAABptGkfBhhdtMBKElclAGO0x9PTx8fHx8fHx8fHx8e6urq6urq6rq6urq6urqGhoaGVoaGPj4+Pj4+Pj4+Pj4KCgoKCgoKCgoKCgnaIiHx8iHx8
+fIh2doh8fHx8fHx8fGl8fHx8fHx8fHx8fHx8b3x8fHx8fG98fHx8fHx8fHx8fHxvb29vb29vY2NjY2NjV1cAAERXSkpXV0RpgmlKV1dEV1dKSldXRFdjSkpXV0pXY1dXY2NQUGNjV2NjSkpXSleIp8DZ2ebm5ubm5ubm8vLm5ubm5tnZ2dnZ2dnHx8fT09/s
++P/4///////4+Pj4+Pj////47Ozf39/faSUlEgAYj6ExBgYGBm+uRBJKJQBjrsfHx8fHx8fTx8fHx7q6urq6urqurq6urq6urqGhoaGhoZWVlZWVlZWIiIiIfIiIiIiIiIh8fIh8fIiIfHyIiHaCgoJ2goKCgoJ2dnZ2dnZ2dnZ2gm98fHxvb3xvb3xvb298
+b298fHx8fG9vb29vfHxvb29vb29vb29vb2NjY2NjAABERERXRERdXV1dUEREV1dKV1dKSldXSldXSldXV0pdXV1dXV1dXWlXV1dKPl18lbTNzdnm5ubm5ubm5ubm5ubm5vLy8ubm5ubm5s3NwNPT3+zs//////////////////////Lm5tnZ2WM+j49EEm9v
+DDFvVwwxiDESVyUGdrS0j4+Pp8DNzc3Nurq6urq6x7q6urqurq6urq6uoaGhoaGhoZWVlYiIiIiIiIiIiIiIiIh8iIh8fI+CdoiIfHyIfHx8fHx8iHx8fHx8fHx8b298b298fG9vfGlpdnZpdnZpaXZ2Y3Z2dml2dmNvb29vb29jb29vY3Z2aWlpaWlpaQAA
+RFdXSkpXSkpKSj5KV0pKV1dEV1dKSldXSldjV0pjY1djY1dXY2NXV1dKPl12iK7H09/f39/f39/f39/f39/f7Ozs7Pjs7Ozs7Ozf08DAzc3f7Pj4///////////////////y8ubZ2ccxY7rTfB9KKx92rpUlN2kYJUoSEo+CPgwABjGIusfHtLSnp6e6urq6
+urqurq6urq6hoaGhoaGhlZWVlZWVlZWIiIiIiIiIiIiIiIh8fIh8fIiIfHyIdnaIiHZ2iHx8iIh2doJ2dnZ2dnZ2dnZ2dmlpdnZpdnZpaXZpaWlpaWlpaWlpaWlpaWldaWlpaWlpXWlpaWlpaWkAAEpKSldKSldXSkpXSj5XV0REV1dKV1dKSl1dRFdjSldj
+Y1djY1dXY1c+PldpiK7A09Pf39/f39/f39/f39/f39/y8vLy5ubm5ubm5ubTx8fH09Pf7Oz4////////////////8ubm2dnHPmO02ZUlMRgMJVBjJSVEEiVKEh9vPgwADAAAJYi6p2kxGBhKdq7Ap4+hrpWVrq6bm5ubm5ubm5ubj4+bj4+Pj4+Cgo+CgoKC
+goKCj4KCj4+CgoKCgoKCgoKCgnaCgm98iHx8fHxvb3x8b3x8b298fGl2dmlpfHxpaWlpaWlpaWlpaWlpaVdpaV1daWldXV1dXV1pV2NjAABKV0o+UFBQUFBQPlBQUFBQUERXV0pKXV1KV1dXSmNjUF1dXV1dXURERERjiKe609PT39/f7Ozf3+zs7Ozs7Ozs
+7Ozs39PT09PT3+zs7NnHx8fH09PT3+zs7Pj4///4+Pj45ubm5tnZx0pKlbp8JTESAAAAAAASJQwlShIlShgAN2ldGABdm1cSAAAAABJpoWMfN0olSoiIYz4+XY+PoaGVlZWVlZWVlYiIiIiIiIh8fIiIiIiVlYiIiIiIiIiIiIiIfHyIdnaIfG+CgnZ2goJv
+fHx8fHx8fHx8b29vb29vb29vb29vb29vY2NjY2NjY1djY1dXY2NXV2NXV1dXVwAASkpKV0pKV1dERFdKPldXSkpdSkpXV0pXV1dKXV1KV2lpV2NjSkpXPj5jiJu0x9PT5ubm5ubm5ubm5ubm5vLy8vLy2cfHx7q6usfT09PT08fHx8fH2dnZ2dnZ2dnZ2dnZ
+2dnZ2dnZ2cBKEhIxGAZKHx9jYzcfHzEAH0oSJSUGEny0tFASPlAYAAwlJQwAH4hKDAwMADFdMQAAAAA3iJubm5uPj4+Pj4+Pj4KCgoKCgoKCgpWhlZWhlYiVlZWIiIiIiIiIiIiIfHyIfHx8fG98fHx8fHxvb3xvb29vb298fG9vb29vb29jb29vY2NjY2Nj
+Y2NjY1djY1dXY1dXV1cAAD5XV0pXV0pXY0pKXV1KSl1QUFBQUFBjV0pXV0pKY1dXY2NQUFBQN0ppfKG6x9PT39/f39/s7Ozs7Ozs7Ozs7Pjs39PHurq6urq6usfHx9PHx7rHx8fHx9PT09PT09PT09PT09/f09O6aTEGBgASdj4Ygrq6p49jEjdKEh8fACWP
+usehdmM+BgZjoaFpGAZXNxIAABIlJQAAEhIADF2VoZWVlZWIiJWIiIiIiIiIiIiIiJWhoaGhoaGhoZWhoZWVlZWVlZWIiIiIfIiIiHx8fHx8fHx8fHxvfHxvb29vb29vb29vb2Nvb29jY2NjY2NjY2NjY2NjY2NjY1dXY2NXAABKSldXSkpXV0RQUFBQUFBQ
+UFBQRF1dPldjV0pdXUpXY1dXY1c+PldXfKG0x9PT09/f3+zs7Ozs7Ozs7Ozs+Pjs7N/T09PTx9PTx8fHx8fHx8fHx8fHx8fHx9PT09PT09PT09Pf39PT08CIUB8MV6F2JVehlXyVdhg+ShIlJQAxlcDNzc20SgYlj7S0jyUANx8GBkp8aR8AMW9vJQY+iIib
+m4+Pj4+Pj4+Pj4+CgoKCgo+Pp6enp6enp6enm5ubm5ubm5uPoZWIiIiIiIiIiIiIiHyIiHZ2gnZ2dnZpdnZ2aXZpaWlpaWlpaWlpaWlpaWldXWlpV2NjV1djV1dXVwAAPldKSldXSldXV0pXV0pKXV1KXV1KSl1dSldXSldXV0pXV0pKV0pdgqG0x8fZ2ebm
+5ubm5ubm5uby8vLy8vLy8vLm5tnZ2ebm2ebZ2dnZ2dnZ2cfHx8fHx8fHx9PT09PT09PT09PT09PT08eursDTrlAMDAwASkoSPj4SMTEGMZvAzcDAp0oMPqHAwKExDCUlACuCp3YYGEp2dj4MMYihoZWhlZWVlZWIiIiIiIiIiIiIlaGhrq6hoaGhoaGhoaGh
+oaGhoZWhoZWVlZWVlZWViIiIiIiIiHx8fHx8fHxvb29vb29vY2NvY2NjY2NjY2NjY2NjY2NjV2NjV2NjV1cAAEREV1dKV2NXSldXSkpdXUpXV0pKXV1KV2NKSl1dSldjSkpXSj5jiKGux8fT09Pf7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs
+7N/f39/T09PHx8fHx9PT09PT09PT09PT09PT09PT39+PMQYAJYhdDD4+DDdXGBiCwLSCdnY+DD6htMehMQYxGAA+j6FjGAAADAAAACt8j6Ghj4+bj4+Pj4KCj4+CgoKCj5ubp6enp6ebp6enp6enp6enm5ubm5ubm5ubm5uPj4+Pj4+Pgo+PfHyIiHaCgnZ2
+dnZ2aXZpaWlpaWlpaV1paVdjY2NXY2NXV1dXV1dXAAA+V0pKXV1KV1dKSkpXSkpjSkpXV0RXV0pKXV1KV2NXSldXPkppgqG0x8fT09Pf39/f39/f3+zs7Ozs7Ozs7Ozf39/s7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7N/s7NnNzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ
+2aFvY4+6dh9KShI+djESUIh2JQBKNwwlj7S0gjEAMRgAPo+nYwwAAAAAAAAxfJWVlZWViIiViIiIiIiIiIiIiJWhoaGhoaGhoaGhoaGhoaGhoaGhlaGhlZWhoZWVoZWVlZWVlZWViIiIiIiIiHyIiHZ2gnZ2dnZpaXZpaWlpaWlpaVdjY1dXY1dXV1dXVwAA
+RERXV0pXY1dKV1dKSl1QRFdXRERdUFBQXUpKY1dKV1c+PldpfKG6x8fT09Pf39/f39/s7Ozs7Ozs7Ozs7Ozs39/f39/f7Ozs7Ozf39/f7Ozs7Ozs7Ozs7Ozs7N/Tx7rHusfT09PT09Pf39/f39/f39/f39PT06dvb1cfSpVjGAAMAAAYb2MYGGOhoVcSEkof
+AESVoWkYGF18fGlpgpubm5uPm5uPj4+Pj4+Pj3yIiIiVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhlaGhoZWhoZWVlZWVlZWIiJWViIiIiIiIiHZ2iHx8fHx8fHxvY29vY2NjY2NjY1dXY1dXV1cAAERXSkpdXUpdXUpKV1dKXV1KSl1dSldjUFBdXURXV0RE
+RFdXgqG0x8fT09PT39/f3+zs7Ozs7Ozs7Ozs7Ozf39/f39/f3+zs7Ozs2dnZ2dnZ5ubm5ubm5vLy8vLy5tnHurq609PT09PT39/f39/f39/f39/f39/f09PAoaG6p10YAAAGUJWVPgYSPjESADFjJQBKlad2GBhdiIhpXYKVoZWVlZWVlZWIiIiIiIiIiIiI
+oaGhoa6uoa6uoaGhoaGhoaGhoaGVlaGVlaGhlaGhlZWVlZWVlZWVlZWIlZWVlZWViIiIiIiIiIiIiHZ2dnZ2dnZpaWlpaWlpXV1dXV1dAABKSkpdXUpdXUpKV0pKV1dKSldKSldXSkpXV0RXVz4+Sldpj6e6x8fT09PT39/f39/f3+zs7Ozs7Ozs7Ozf39/f
+39/f39/s7Ozf39/f39PT09/f39/T3+zs7Ozs39/s38fHx9nZ2dnZ2dnm5ubm5ubm5tnm5tnZ2dnZ2dnZ2c2udldXiLTHrmklAAAAACV2aRIAaaGhfCUMK0orACV8m5ubm4+Pj4+Pj4+Pj4+PgoKPj6e6urq6rq6urq6urq6urq6urq6hoaGVlaGhlaGhj4+b
+m4+hlZWVlZWVlZWVlZWViIiIiIiViIiIiIiIiIh8fHx8fHx8aWlpaWlpaV1dXQAARFdXSldXSkpXVz5KV0pKV1dEV1dKSldXRFBQRERERERvlae6x8fT09PT09Pf39/f39/f3+zs7Ozs7Ozs7N/f39/f39/f3+zZx9PT09PT09PT5ubZ2dnZ5ubZ2dnZ2dnm
+5tnm09PT09/f39/f39/f39/f39/f39/f39PT39PT08fHx8fHx8e0fD4fEjd2oW8fH2+hoaFQEgAAAAxXiJWhlZWVlZWVlZWCj4+PfIiIiJWux8fHx8fHx8fHurq6urq6urqurq6urq6hoaGhlZWVlZWVlZWVlZWIiIiIlZWIiIiIiIiIiIiIiIiIiIiIiHx8
+fHx8fHx8fG9vb29vY2MAAEpKV1dKSl1dSkpdSkpdXT5QXUpKV1c+SldKPlBQN1d8j6e6x8fH09PT09PT39/f39/f39/f3+zs7Ozs7Ozs7Ozs7Ozs7N/f08fHx9PHx8fHx9PT09PT39/f39/f39/f3+zs39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT
+09PHx8fHx8e6p6e0tLSnj3yhtLSnm1cfDAxXiJubm5ubj4+Pj4+Pj4+CgoKCgo+brtPT09PT08fH08fHx8fHx8fHx8fHurq6uq6urqGhlaGVlZWVlZWVlYiIlZWVlYiIiIiIiIiIiIiIiIiIiIh8fHx8fHx8fHx8b3x8b29vAAA+V0pKV1dKSmNXSldXSkpd
+XURXV0REV0o+SkoxPmOCm7THx8fHx9PT09PT39/f39/f39/f3+zs7Ozs3+zs7Ozs7Ozs7OzZ2cfHx8fHx8e6usfHx8fT09PT09PT09PT09PT39/f09Pf39/f39/f39/f39/f39/f39/f39/f09PT08fT08fHx8fHx8fHurq6urq6uq6urqGhlYiIoaGVlaGh
+lZWViIiViIiIiIiIiIiIm7rT09PT09PT09PT08fT08fH09PHx8fHx8e6urqurq6hoaGVlZWVlZWVlYiIiIiIiIiIiIiIiIiIiIiIiIiIfHx8fHx8fHx8fHx8fHxvbwAASkpKXUpKV1c+UF1KSldXSldXSkpXSjdKSjc3Sm+Cobq6x8fHx9PT09PT09Pf39/f
+39/f39/f39/f39/f3+zs7Ozs7Ozf08fHx8fHx8fHusfHx8fHx8fHx8fHx8fH08fHx9nZ2dnZ2dnZ2dnm5ubm5ubm5ubm5ubZ2ebZ2dnZ2c3Nzc3Nzc3Nzc3AwMDAwMC0tLS0tLS0tKGhoaGhoaGVlZWVlZWViIiIiIiIiIiIlaGu09PT09PH09PTx8fTx8fT
+08fH09PHx8fHx8fHurq6urqurq6hoaGhoaGVlZWViIiIiIiIiIiIiIiIiIiIiIh8fHx8fHx8fHx8fHxvfHwAAD5XV0pXV0pXY1BQUFBQUFBQUFBQPj5KPis+SmmPrq7AwMDNzc3NzdnZ2dnZ2dnZ2dnZ2ebm5ubm5ubm5ubm5ubm5ubm5ubZ2c3Nzc3Nzc3N
+zc3Nzc3Nzc3NwMDAwMDAwMDZ5ubZ2dnZ2dnZ2dnm5ubm5ubm5tnZ5ubZ2dnZ2dnZzc3Nzc3Nzc3Nzbq6urq6urq6urq6p6enp6enp5ubm5uPoY+Pj4+Pj4+PfHyIiIihtMfHx8fHx9PHx8fHx8fHx8fT08fH08fHx8fHx8fHx8fHurq6urqurq6hoaGhlZWV
+lYiIiIiIiIiIiIh8fIh8fHx8fHx8fHx8fHx8fHx8AABKSldXSkpjV0pXV0pKXV1EV1dEREREMTE+V3ahrrrHx8fT09PT09PT09PT39/f39/f39/f39/f39/f39/f39/f3+zs7Ozs7Ozs39/f39/T09PT08fH09PHx8fHx8fT09PT09/f39/f39/f39/f39/f
+39/f39/f39/f39/f39PT09PH09PTx8fHx8fHx7q6urq6urqurq6urq6uoaGhoaGhoY+Pj4+Pj4+Pj4+PfIiIla7Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8e6usfHurq6uq6urq6hoaGhlZWhlZWVlYiIiIiIfHx8fHx8fHx8fHx8fG9vfAAA
+SldKSldXSldXSkpXV0RQXUo+V0oxPj4+SoKnusfHx8fT09PT09PT09PT09PT39/f39/f39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7N/f39PT09PT09PT0+bm5tns7NnZ2dnm5ubm5ubm5ubm5ubm2dnm5tnZ2dnZ2dnZ2dnHx9PHx8fHx8fHx7q6
+urq6uq6urq6hoaGhoaGhj4+Pj4+Pj4+Pj4+CgoKPj6GuwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM3AwMDAwMDAtLS0tLSntLSnp6enp5ubj4+Pj4+Pj3x8fHx8fHx8b298b28AAEREV1dKSldXRFdXSkpdSj5QUD4+Pj4xV4inusfHx9PT
+09PT09PT09Pf39/f39/f39/f39PHx9nZ5ubZ2dns39/s7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozf39/f3+zs7Ozs7Ozf39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT09PHx8fHx8fHurrHurq6urq6uqenp6enp6enlZWVlZWVlYiIiIiIiIiIiJWV
+p7q6urq6urrHx7q6urq6x8e6use6urq6urq6urrHx7rHx7rHx8fHx8e6urq6urq6urq6uq6urq6urqGhoaGhlZWVlYiIiIh8fHx8fG9vAABEV0pKV1dKXV1KSl1dSkpXSjdKPjE+XYKnwMDAzc3N2dnN2dnZ2dnZ2dnZ2dnZ2dnZ2dnHx8fT09/fzc3Nzc3Z
+2dnZ5tnm5ubm5ubm5ubm5uby8vLy8vLy8ubm5ubm8vLm5ubm5ubm5ubm5ubm5ubm5ubZ7N/f39/f39/f39/f09PT08fT08fHx8fHx8e6urq6urq6uq6urqGhoaGhoaGVlZWViIiIiIiIiIiIiIiVoaGurq6urrq6rrq6urq6urq6urq6urq6use6urq6urq6
+urq6urq6x7q6urq6urq6urq6urq6uq6urq6urq6urqGhoaGhlZWVlYiIiHxvbwAARERXV0pXV1dKV1dKSldKPlBQMTFEY4i0wMDAwNPT09PT09PT09PT39/f39/f39/f08fHx8fHx8fT09PHx8fHx9PT09PT09Pf39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7N/f
+7Ozs39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT08fHx8fHx8fHx8e6urq6urq6rq6urqGhoaGVlZWVlZWViIiIiIiIiIiIiIiIoaGhoa6urq6urq6urq66urq6urq6urq6urqurrq6urq6urq6urq6urq6urq6urq6urq6rrq6rq6urq6urq6u
+rq6hoaGhoaGhoY98b28AAEpXSkpjV0pXV0pKV1dEV0o3Nzc3V4+0wMDAzdnZ2dnZ2dnZ2dnZ2dnZ2dnZ5ubm09PHurrHx8fHx9PHx8fHx8fH09PHx8fHx9PT09Pf39/f7Ozs7Ozs7Ozf39/f39/f39/f39/f39/s7Ozs7Ozs39/f39/f39/f39/f39/f39/f
+09PTx9PTx8fHx8fHx8fHx7q6urq6rq6urq6hoaGhoZWVlZWVlZWIiIiIiIiIiIiIiIiIiIibm5uPoaGhoaGhoa6urq6urq6uurquurq6urq6urq6urq6urq6uq66urq6urq6urq6rrq6rq6urq6urq6urqGhoaGhoaGPdmNjAABKSkpdUFBdXUpKXUpKV0ox
+Pj4+aZu0x8fHx8fT09PT09PT09/f39/f39/f39/f08fHx8fHx8fHx8fTx8fHx8fHx8fHx8fHx9PT08fHx8fT09Pf39/f39/f09PT09PTwMDA2dnZ5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubZ5ubT09PT09PTx8fHx8fHx7q6urq6urqurq6urq6urqGhoZWV
+lZWVlZWIiIiIiIiIiIiIiHx8fHx8iHx8iIiIiIiIiJWhoaGhrq6urq6urq6urq6urrq6urq6urq6urq6urq6urq6urq6urq6rq6urq6urq6urq6urq6hoaGVfGNXVwAASl1dSldXSkpXV0pXV0o+SjExb6G6x8fH09PT09PT09PT09Pf39/f39/f39/f08e6
+usfHx7q6usfHx8fHx8fHx8fHx8fHx9PT08fHx8fT08fT09PT08fHx8fHx8fTx7q6utPT39/f39/f7Ozs7Ozs7Ozs3+zs39/f39/f39/f09PT09PT09PHx8fHx8fHurq6urq6urqnp6enp6enp5ubm4+bm4iIiIiIiIiIiIiIiHxvb29vb29vb29vb3x8fHx8
+fIiIiJunp6enp6entLS0tLS0tLS0tLS0tLS0tLS0wLS0tLS0tLS0tLS0tLS0p6e0tKenp6enp6ebj29jV0oAAEpKV1dKSldXSkpXRERERCU3b6G0x8fH09PT09PT09PT09PT09/f39/f39/f07q6urrHx8fHx8e6x8fHx9Pf08fHx8fHx8fT09PHx8fHx8fH
+tLS0tLS0tLS0tLS0wMDAwMDN2dnZ2ebm5ubm5ubm8vLm5ubm5ubm5ubm5tnZ2dnZ2dnNzc3Nzc3NwMDAwMDAwLS0tLS0tKenp6enp6enlaGhj4+Pj4+Pj4+Pj4+Pj492dmlpaWlpaWl2dmNvb29vb29vb3yIlZWVlaGhoaGhoaGurq6urq6urq6urrq6urq6
+urq6urq6urqurq6urq6urq6uoaGhoaGhoXZjUF1dAAA+V1dKV2NKSldKPkpKNzc3b6G6x8fH09PT09PT09PT39/f39/f39/f39/fzc3AwMDAwMDAwMDAzc3N2dnZ2dnNzbrHx8fHx8fHx8fH09PHtLShoaGViIiIiJWhoaGhtMC0x9PT39/f39/f7Ozs7Ozs
+7Ozf39/f39/f39/f39/f39PT09PT09PHx8fHx8fHx8fHtLS0tLS0p6enp6eVoaGVlZWVlZWViIiIiIiIlYiIfG9vb29vb29vb29vY2Njb29vb29vfIh8fIiIlaGhlaGhoaGhoa6urq6urq6urrq6urq6uq6urq6uurqurq6urq6urq6uoa6uoYhjV2NjUAAA
+SkpXV0pKV1c+Sko+Pj4+aaG6x8fH09PT09PT09PT09PT09/f39/f3+zf08fHx8fH09/T08fT0+bm5ubm2dnNzdnZ2c3Nzc3Nzc3Nzc3NurquoaGViIh8fHx8fHx8fIiIobrT39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT08fHx8fHx8e6
+urq6urq6rq6urqGhoaGVlaGVlZWVlZWViIiIiIiIiG9vb29vb29vY2NjY2Nvb29vb29vb29vb29vgo+Pj4+Pj4+PoaGurqGhrq6urq6urq6urrq6rq6urq6urq6urq6urq6urq6uoaFvXV1KXWkAAERXSkpXY0pKVz4+Sj4xaafAwM3Nzc3NzdnZ2dnZ2dnZ
+2dnZ5ubm5ubZ2bq6usfHx8fZ2cfH09PT39/f8vLy8vLy8vLm2dnZ2dnZ2cfH08e6urquoZWIiHx8fG9vb29vb4Ku09/f39/f39/f3+zs39/s39/f39/f7N/f39/f39/f09PT09PT08fHx8fHx8e6urq6urq6p7S0p6enp6enp5WVlZWVlZWViIiIiIiIiHZ2
+dmNjb29vb29vb29vb29vb29vb2NjY2NjdnaCgoKCgoKCgpWhoaGhoaGurq6urq6urrqurrq6rq66uq6urq6urq6urq6urqGIY0pXaV1dAABKSldXSkpXVz5KSjFKdqHAwMDNzc3Nzc3NzdnZ2dnZ2dnZ2dnm5ubZzbrHx8fHx8fHx8fHx8fT09PTx8fT09PT
+7Ozs7Ozs7Ozs7Ozf39PT08fT07qurqGhj4+PgoJ2dnZ2rs3f39/f39/f39/f39/f39/f39/f39/f39/f39/T09PT09PTx9PTx8fHx8fHx8e6urqurrqurq6urq6uoaGhoZWVoZWVlZWVlZWViIh8aWlpaWlpaWlpaWlpaXZ2dmlpaWlpaWlpaWl2goKCj4+P
+j4+Pm5ubm5unp6entLS0tLS0tLS0tLS0tLS0tLS0tKenp6enp6eVaVdjY1djYwAARFdKSl1dRERERDdKfKHAwMDNzc3Nzc3NzdnZ2dnZ2ebm5ubm5ubm08DAwMDA08fHx8fZ2dnZx8fTx8fH09/f09/f09PH2dnZ5vLy8vLy8vLy8ubZ2cfHx7Snp6eVlaGP
+j67T39/f39/f39/f7Ozs7Ozf39/f39/f39/f39/f39/T09PH09PTx9PHx8fHx8fHx7q6urqurq6urq6uoaGhlZWhoZWVlZWVlZWIiIh8fHxpaWlpaWlpaWlpaWlpaWlpaXZ2aXZ2aWl2dnZ2goKCj4+Pj4+PoaGhoaGurq6urq6urq6urq6urq6urq6urq6u
+rq6urqGhiGNXSl1dXV0AAEREV1dERFc+Pj5XfKfAwMDAzc3Nzc3N2dnZ2dnZ2dnZ2dnZ2ezfx8e6usfHx8fHx8e6x9nZ2cfHx8fHx9PT39PT09PH09PT39/T09PT7Ozs7Oz4+Pj47OzZ2c3NwMDAtKG0zdnZ2dnm5ubm5ubm5ubm5ubm5ubm2ebm2dnZ2dnZ
+2dnZ2c3Nzc3Nzc3NusfHx7q6uq66uq6urq6hrq6hoaGhlZWhlZWVlZWVlYiIiHZpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXZ2dnZ2dnaCgoKCgoKClZWVoaGhoaGhoa66p6e0tLS0tLS0tLS0tLSnp7S0p6enm29QUGldXV1dAABKV0REV0o+SkpKgrTHx8fH
+09PT09PT09PT09Pf39/f39/f3+zZx8fHusfTx8fHx9PTx8fH09PT08fT09Pf39/fx8fZx9/f39/f39/NzdnZzd/f3+zs7Oz4+Pj4+Pj47OzZ2cfH09Pf39/f39/f39/f39/f39/f39/f39/f09PT09PT08fT08fH08fHx8e6urq6urq6rq6urq6urq6hoaGh
+oaGhlZWhj4+Pj4+PgnZ2dmlpaWlpaWlpaWlpaWlpaWlpaWldaWlpaWlpaXZ2dnZ2doKCgoKPm5ubm5ubp6enp6e0tLS0tLS0tLS0tLSntLShrq6uoXxdXV1dXV1dXQAAPkpXVz4+SjFKiLTHx8fHx8fT09PT09PT09PT09Pf39/f39/fx7S0x8fT08DAwMDA
+wNPTx9PT38fH09PT0+bm2dnZ5ubZ2dnZ2ezs7NPT09Ps7N/f3+zs2dnZ2ez4//j///jNwMDNzdnZ5ubm5ubm5ubm5tnZ2dnZ2dnZ2dnZ2dnZzc3Nzc3Nzc3NzcDAwMDAwMDAtLS0tLS0p6enp6enp6ebm5uPj5ubiIiIiHx8fGlpaWlpaWlpaWlpaWlpaWlp
+aWlpaVdjY2NjY2NjY29vb3x8fHyIiIiVoZWVp6enp6entLS0tLS0tLS0tLS0tLS0tLSnp5tpV1djY1dXaWkAAERXSj5KSjFQlbrHx8fHx8fH09PT09PT09/f39/f39/f39/fx7rH2dnZ5tO0tMDAtMfHx9Pfx8fH09PH2dnZ2dnZ2dnZ2dnZ7Ozs39/f39/f
++N/f39/f39PT09/f8vLf8vLmwKenusfT09/f39/f39/f39/f39/f39/T09/f09PT09PTx8fHx8fHx8fHx8e6use0tLS0tLS0p6enp6enp6enp6eVlZWViIiIiIh8fGlpaWlpaWlpaWlpaWlpaWlpaWlpaV1paV1dXV1dXWlpaWl2dnaCgoKPj4+bm5ubp6en
+p7Snp7S0p6e0tLS0tKentLSntJtvV0pjY1dXaWlQAABKPkpKNzdjm7rHx8fHx8fT09PT09PT09/f39/f39/f39/TwM3Nzc26x9PTx8fT39/f39/fx8fH09PT09PT09Pm8ubT09/f09Pf7Ozs7N/f39/f7Ozs7Ozf7Oz4+Ozf39/f38ehlaGuusfT09/f39/f
+39/f39/f39/f39/f09PT09PT09PTx8fHx8fHx8fHx8e6urq6uq6urq6urq6uoaGhoaGVlZWVlZWIiIiIdnZpaWlpaWlpaWlpaWlpaWlpaWlpXWlpaWlpaWlpaWlpaWlpdnZ2doiIiIiVlaGhoaGhrq6urq6urrq6rq6urq6urq6urq6IV1dpXV1paVdXYwAA
+N1BEN0pjlcDAwMDAwM3Nzc3Nzc3Z2dnZ2dnZ5ubZ2ebTx8fHx9PTx7rH39/T09PHx9nZ2dnZ2ebm2c3N2dnm+Ozs39PT09PH2dnm5ubm2dnZ5vLy8vLy8ubm5ubm2ezs7Nnmx6GVlaGursDA09PT09/f39/f39/f39/f39PT09PH09PTx9PTx8fHx8fHx7q6
+urq6urqurq6urq6uoaGhoaGhoZWVlZWIiIiIfHxvY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Njb29vb29vfHx8iIiVlaGhoaGhoaGhtLS0tLSntLS0tLS0tLS0p6eVaVdXV2lpV1dpXVAAADc3N0pjm8DAwMDAwNPH09PT09PT09PT09Pf39/f39/T
+09PHx8e6usfH09Pf09Pf08fH39/Tx8fT09PT09/f7Pjm2dnN3+zf09/f3/js09Pf3+zs7N/s+Ozf3+zs7N/f39/f39/frpubm5uurq66x8fT09PT09/f39/f39/f39PT09PT09PHx9PHx8fHusfHx8e6uq6urq6urq6urq6um5ubm5ubj4+Pj4+Pj3x8b2NX
+V2NjV2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nvb29vb298fHx8iIiIiJWhoaGhoaGurq6urq6urq6urq6urq6urq6ufFBQY2NXV2lpV2NjAAAxRERpp8fHx8fHx8fH09PH09PT09PT09Pf39/f39/fzc3m5vLy5tnZ2dnZ2ebZzc3Z2dnZ2cfHx9nZ2dnZ2dns
+2c3Z2ebm+Ozs7N/f39/f39/f39/f39/s7Ozs7Pjm2dnm5ubZ2ezTtJWVp6enp6e0wMDAwNPT09/f39/f39/f09PT09PT08fHx8fHx8fHurq6urq6uq6urq6urq6hoaGhoaGhj4+Pj4+Pj3x8fGNKSkpKSkpKSkpXV1djY2NjY2NjY2NjY2NjY3Z2aXZ2dnZ2
+dnaIiIiIlaGhoaGhoaGhoa6urq6urq6uurq6uq6urrquj2NKY2NXaWlXV2ldUAAAMTFpp7rHx8fHx8fHx8fH09PT09PT09PT09Pf39/NrqGuwMDZ2dm6use6usfZ2ebm2c3Nzc3N2c3Nzc3Nzc3Z2c3Z2c3f39/Tx8fH2fLm5vLy5ubm2dnZ5ubm5ubm5vLy
+8v/s7P//7OzZuqenp5WVoaG0tLS0x8fH09/f39/f09PT09PT08fHx8fHx8fHx8fHurq6urq6rq6urq6uoaGhoaGVlZWViIiIiIiIdmlXV0pKSkpKSkpKSkpKSkpKSkpXY2NjY2NjY29vb29vb3x8fIiIiIiVlZWhoaGhoa6urq6urq6urq66uq6urq6uuq6u
+rm9XV1dpaVdXb11dXV0AADdvp8fHx8fHx8fHx8fHx9PT09PT09PT09/f39/Hp6e6x9PTx7q6x9PTx8fHx8fHx8fHx8fZ5uby8ubm5s3N5vLZ2dnZ5vLZ2c3f39/f3+zf39/H3/j4+P/y5vL/7Ozs7Ozs7Pj////y8vLm07SVlZWhoaGhoaGuwMDAwNPT09PT
+09PT09PTx8fH08fHx8fHx7q6urq6rq6urq6uoaGhoaGVoaGPj4+Pj4+PdnZXPj4+SkpKSkpKSkpKV1dKSldXV2NjY2NjY2NjY2NjdnZ2dnaIiIiIiKGhoaGhrq6urq6urq6urrq6rq66urq6rq6uro9XRF1vXV1dXV1daVdXAABpp7q6urrHx8fHx8fT09PT
+09PT09PT39/f39/Ap7TH08e6oaG6urrH09PT09PHx8fH09/Tx8fT5vLy8vLT08ff7Oz4+OzZ2dnZ8ubZ2dns7NPT3+zs7NPm5uby8v//////7Ozs+Pjs7Ozs7Pj4+N+6rq6urqGhoaGurq6uurq6x8fHx8fHx9PTx9PTx8fHx8fHx7q6urqurq6uoaGhoaGh
+oZWVlZWViIh2dmNKNzc3N0pXSkpKSkpKV1dXV1dXV2NjY2Nvb29vb29vfHxvfHx8fHx8j6GhoaGurq6urq6urq6uurqurrqurrq6rq66uqFjPmNjV2lpV2NjY1BjYwAAiLq6urrHx8fHx8fHx8fHx9PT09PT09PT09O6rsDAwNO6urq6usfH08euurrZ2dnN
+zdnZ2ebZx8fHx8fZ7Nnm5sfT7NnNzeby8v//7OzZ2fLy39/y8ubT5ubT5tnN5vLy5vL/////8vLy8vLy///s7Ozfx7qhiIihj4+hoaGurq6urq6urrq6usfHusfHx8fHx7q6urq6urqnp6enp6enlaGhj4+CgmlpXV1ERERERERERERERFBQUFBdaWlpaWlp
+dnaCgoKCdnZ2dnaCgoKCgoKCm5ubrq6urrq6rq6urq66urq6urq6urqurrqurq6IUFBQaWldXW9jV2lpV1cAAJW6urrHx8fHx8fHx8fH09PT09PT09Pf39/f39/Tx8fHrq6ursfT09/fx8e0wMDAzdnZ2dnZx9Py39Pf3/LZzc3Nzc3N39/f39PHx8fH0+bm
+5uby5tnZ2eb4+Ozs7NnZ+P/s7Ozs09Py///////4+P////////Ly066CgnaIiJWhoaGhoaGhoaGhoaGVlaGhoaGhrq6urqGVoZWVlZWIiIh8aWlpXV1dXUpKSkpKSkpKV1dXV1djY2NjY2N2gm98fHx8iIh2doJ2doKCgoKPj4KCj6Gurq6urq6urrq6rq7A
+tLTAwLS0tLS0tLS0tMChXTdXb11daWlXV2lXV2NjAACPurrHx8fHx8fHx8fH09PT09PT09PT0+bZx9Pf39/f38fZ2ebm2dnAwMDf08fH39PT08fHx8fH5ubZ7Ozs39/T09PT08fZ2dnZ2dnZ5ubm5tPfzd/4+P//7Ozs+Pjm8v//8vLy/+zf8vLy39/s7P//
+///////////////42bqhj4KCgoKCgoKPj4+Pj4+Pj4+Pj4+Pj4KCdnZ2gnZ2dmlpaWldXV1dSj5KSkpjY2Njb29vfHx8fIh8fHx8iIiIiIiIiHx8iIiIiJWVlZWViIiVlaGurq6uurq6urq6urq6urq6urq6urq6uq6uurq6fEpdXV1paVdXb2NXaWlQXQAA
+obq6usfHx8fHx8fH09PT09PT09PT09PAm5ubrs3Nzc3N2cC0tLTAwM3m5uby5ubmzc26x7TAzdnZ2ebm2dnHus3N2ebm+ObZ8t/f3+zs39Pm8tPm5ubm09P42dns7Pj4+ObZ2ezs7Ozs7N/f39//+Obm5ubZ7P//////////////+Nm6oXZpaVdXY2Nvb298
+fHyIfHxvb29vb29jY2NjY1dKSko+PkpXV1dvb29vgoKClaGhoaGhoaGurq6hoaGhoY+Pm4+Pj5ubj4+Pj5ubm67AwMDAwLS0wMC0tMDAwMDAtLTAtLS0tLS0tLS0iEpdXV1paVdjY2NXaWlQY2MAAKHAwMDAwMDAzc3Nzc3Nzc3Nzc3N39Ouoa7Hx8fZx8e0
+tMff38euwNPHx7S0tLS0tMfZ2fLy8vLy39/T7NnNzcDNzebm5ubm2cfT09PT09O609Pm5tnZx9nZ5vLy5tnm0+bm///4+P/s3/j47Oz///Ly2dnZ8vLy8vLy//jm5v//////////////8seuurqViIh8fG9XV1dXV1dXV0pXY2NjY29vb4KClaGhoaGurq6h
+oZWVoZWVoaGhoa6urrrHtLTHurq6uq6urq6hoaGhoaG6usfHurrHx7q6x8fHx8e6urq6urq6urq6urq6p2lKSmlpXV1vb1dpaVdjb11QAAChurq6usfHx8fHx8fHx9PT09PT39O6x8fT09O6uq6urtnNzc3Z2c3Nzc26zdnZ2dnZx9nm5ub45sDAwMDf7P//
+//Lf3+zTx9//5tnZ2c3Z2eby8t/f8t/T5vLm5tPf09//+Pj47Nn47N/f8v////Ly39/y8vLy/+zs7N/f7Oz4+N/f39/4///////////////////y8v/47N/N2ebZ2dnZ2cfHx8fHx8e6rq6ursfHrq6urq6urqGhrq6urq66urqnp7q6usfHx8fHx8fHx7q6
+usfHx8fHx8fHx8fHx7q6x8e6urq6urq6urq6unw+V29dXXZpV2lpXV1paVdjYwAAobS0wMDAwMDAwM3Nzc3Nzc3Z2dnNzcDAtLS0tLS0tMDAwNPTx9PAwMDAwMDT7N/f38fHx7q6zd/s7Nnm5ubm08fH09/f8v/y8t/Tx8fZ2ebm5s3Nzc3Z2fLm5ubAwN/s
+2dnZ2dnm5uby8vLf39/f3+zs+P//8ubm2dns//j439PT+Pjf7Ozf39/f39PT8vLy///////4+Pjs7Ozf09/fzc3Z2c3N39/f383Nzc3NtMfHtLS0tLS0x8fHuq6hrrq6uq6urq6urq6uuq6uurq6zc3Nzc3Nzc3Nzc3NwMDAwMDAwMC0wMC0tLS0tKFdUFBQ
+aWlXY3ZpV2lpV1dpXV0AAKHAwMDAwMDAwMDAwNPHx9PT09PT09PT09PTx9PT08fHx8e0p7TTx8fZ2dnZ2dnHx8fHusfHx9PT5ubZ2dnHx8fT39/f7NPHx8fH2ezs7Ozs7NPTx8ff7Ozs39P47Ozs7Ozf09Pf39/s7Nnm2dnZ7Ozs7Oz4+OzT5vLy/////9nZ
+2dns7OzZwNPT0/js2dnZ2dnm09Py8vLy///////47OzfzdnH09PT09PT08fH08fT08fHurrHusfHusfHtLS0tLS0p7qursDAtLS0tKe0x8fH08fHx8fHx8fHx8fHx8fHx7q6urq6urq6x7p2Pkpvb11db2NQaWlXV2lpUGNjAACburrHusfHx8fHx8fH09PT
+09PTx9PT09PT08DT09PT5tnZ2dnZ2dnZ2c3Nurq6urrHx9Pm5tnZ2dnNzbq6x9PT39/f39/Hx8fZ7Ozf7Ozszd/T3/Ly8vLTx+zs7Ozs7Ozs7N/f3+z4+Ozs2dnZ8vLm5vLy/9PH3///7OzZ2dnZ5vLy8vLy8vLy8vLT0/Ly8vLfx8fT7N/TwMDA09Pf7Pj4
++Pj47Ozs39/fx8fH08fHx8eursC0tMe6rq6urq7Huq6uurq6x66urrq6p6e6x8fTx8fT08fHx8fHx8fHx8fHurq6urq6uq66urqVSkppVz5XY1dpaV1daWlQXWlXVwAAlbq6usfHx8fHx8fH08fHx8fH09PT09PTx7TAwNPHurq6urq6x8fH2ezs3+zs39/s
+39/f383NzcC0tN/T09Pf3+zZ2ezsx+by8t/f3/LTx9nm5ubZ2dns09Ps7Ozs7N/f08fZ//Ly8v/m09PT0+by8ubZ2dnZ2ezs7OzZzc3Nzc3f39/f39PH2fLm8vLy8vLm2dnZ2ebm09PT09PT39PTx8fT5vLy5vLy8vLy8ubm2dnHx7q6usfHtLS0tLTHx8fT
+08e6uq6urrq6urqntMfHx9PTx8fHx8fHx8fHx8e6urq6use6urq6urq6Y0pXVz4xPl1vY2Njb1djb2NXY2MAAJW6x8fHx8fHx8fHx8fT09PT09PH09PTx7S0p6e6oaG0tLTH08fHx9PTx67A09PT09/s7Ozs7Ozs7Ozs7N/f39PTx8fT39/s08DA09PT09Pm
+5tnZ2dnZ2fLy5ubm08fT09Py///y5uby8ubm5uby5ubm09//+NnZ2dnZx8fH3+zs7Ozs2dnZ5vj45ubZ2dnAwNnZ5vLy2dnAwM3Z2cfHuq7A09PTx8e6urq6rsfZ2ebm5ubm5tnZ2ce6urq6urq6rq6ursDArq66uq6urrrHx9PHx9PHx8fHx8fHx8fHx8fH
+urq6urq6urrHiEpKY0orSmNjV2NjY2NvY1BjY1dXAACPwMDAwMDAwM3Nzc3Nzc3Nzc3N2dnZwLS0tLS0tLS0tLTTx8fHx8e6urq6x8e0tMDA09PHurrH09Pm8vLy8vLy8vLm5ubTx8fHx9PT0+by39PAwNnZ7Ozf39/T3/Lf0+by8vLf07rH5vLy///4+ObZ
+2dns7Oz/7NnZ5tPm8v/m8vLy5ub45tnZ2ebm09/y8tnZ2dnHx8e60/Ly8vLy2cfHx8fH08DAwMDAwMDT09PHtMfHus3Z2dnZ5ubZ2dnNzc26rq66uq6uuqe0tMfT09PTx8fHx8fHx8fHx8fHx8fHx7q6urq6urq6oUpKb103RGNjY29jV2lpV2NvXV1paQAA
+j7TAwMDAwMDAwNPHx9PT09PT09PT09PT08fHx7quwNO6urq6usfHurq6x7q6x8fH2dm0tMfHx9nZ2dnZx8fHx8fT3+zs+Pj47Ozs3+z45tPTwNnZ2dnZ2ezZzdnZ7Ozfx+by2dnm+Ozs7NPT09Pf8vL///j/+Ozs39////////Ly8tnZx9nZ2dnZ2dnZx8fH
+09PTx8fH2dnZ2fLZx9PT09/f09/f08fHx8fH09O6urq6rrrNwMC0tKfAwMDT09PT39/T08fHx8e0p6fHx8fT08fHx9PHx8fHx8fHx8fHurrHx7q6urq6unZEUFBQN0RXdmlXaWldXXZjV2lpV1cAAI+6urrHx8fHx8fHx9PT09PT09PT09PT09PT39/f7Ozf
+39/f09PTx7rHusfHx8fH09PHx8fHx8fH08fHx8fTx8fT08fT09Pf08ff3+zs7P//////+ObZ2cfH0+bm5tnZ2c3f7Ozs39/s2dnZ2dnZ7Ozf8vLT39PT5ub4+P////////////////j439/Nzc3m2dns7MfH08fm2c20tNPf09/f09Pmzc3Nzc3Z2dnZx7q6
+rsDAwMDAwMDAwMDAtLSnp8fHx8fH09PH09PTx8fTx8fT08fHx8fHx8e6usfHx8e6urq6uq66x5U3RGNXHzFjY1djY1dXaWlQY2NXV2NjAAChurrHx8fHx8fHx8fH09PT09PT09PT09PT09Pf39/f39/f3+zf3+zs7Ozs7Ozs7N/f383Nzc26x9PHx9Pf383N
+zcDA08fZ5tnZ2dnHx8fT5ubm8vL////////4+ObT3/Lm5ubm5uby8tPT39/f7Ozs383Nzd/f3+zs7Ozfx8fH09Pm8vLy///4////////8vLf08fT8ubm5tPT08fHx9PTuse6usfHx7rNzc26uq7A39/Tx9nArq66urq6urq6rq7Hx7Snp6enp7rHx8fTx8fT
+x8fHx8fHx8fHx9PHx8fHtLTAwLS0x6FXPmlpMTFjY1dpaV1daWlQY2NXV2lXSgAAocDAwMDAwMDAzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ5tnZ5ubm5ubm5ubm5ubm5uby8vLy8vLy8vLy5ubZ7Pj4+Pj47Ozf08fH09PT09PH09/f09PTx9ny5ub4+Pj/////
+//jm5tnZzc3N2dnZ5vjs383Z+Ozs7Ozs7Pjfx9Pf3/Ly39/f39/sx8fT7Oz4+P////////j47N/f7OzZ2cfT09PTx9PAwMDT08fHx7q6utPHx8fTx8fH09PT09/s2dnZ2bq6uq6ursfHx9PHx9PTx8fHx8fHx8fHx8fHx8e6urq6urq6urpjN11dRDdKV2lp
+V1dpaVdpaV1daWlXV2MAAIKhoa6urrq6x8fHx9PT09PT09PT09PT09Pf39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Oz4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj///j439/s08fT09/s7Ozf39/f09PT39PT5vLy///////////47OzZzc3m5vj47N/s+N+0tMDAwMDZ
+2ezZ2dnZzdnZ5ubZ5tnNzdnZ7Oz4+Pj///////Ly8se0wMDAwNPT09PTx7S0wMDfx66htLS0x7ShoaGhobrHx8fZ2dnZ2dnZ2dnH09PHx9PHx8fHx8fHx8fHx7q6x8e6urq6useVSkpXV0pKY29jY2NjV2NjY1BpaVBQaV1QAABpiIiIiJWVlZWVoaGhrrq6
+usfT09PT39/f39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7Oz4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4//j/+Ozs7NPTx9PT08fH09PT09PT08fHx9/s7Oz4////////////7NnZ2c3Z7Ozf+Pjm8v/s09Pf39/y5tnZ2cff+N/f7N/f38C0x8ff7Oz4+Pj/
+//j4//j47N/Hurq6utPT09/fzbTA08fHx7qhtMfHurq6oaGhrq7A09PT09PT09PHx8fHx8fHx8fHx8fHx7q6urq6use6VzFjYz4+Y2NXb29XY2NjV2lpV1djY0pdXQAAV2lpaXx8iIiVlZWVlZWVlZWVlZWVoaGurq7AwNPT09Pm5ubm5ubm5ubm8vLy8vLy
+8vLy8vLy8vLy8vLy8vLy8vLy///y8v/y8v/4+Pj4+Pj///j///j47N/f7N/f39/f09PH2ezs7NnZ2c3N5ubm+Pj4////////////+Obm09Pm09/T3+zf39/Hx8fZ2ebm5tnNzbrs39/T09PHusfH09Pm8vLy8vLy8vLy8vLf09O6p7THusfHx7S0tLTAwLSn
+p6e0tLSnp8fZ2cfH08fH08fHx8fHx8fHx8fHurq6urq6x7q6gj5XYz4xRFdpdmNjb29XY2NXV2lpUFBpXVAAAFdpdnZjY3Z2aXx8fHyIiIiVlZWhoaGhoaGhoaGhoaGhoa6ursDAwM3N2dnm5ubm5vLy8vLy8vLy8vLy8vLy8vLy8vLy///////y8v//////
+///////////////4+PjZ2dnZ2dnZ2dnZ2cfT7Ozs2dnZ2fLm09Pf3+z/8vL//////////+zf08fHx9nZ2dnm09PT08fT7Ozf38fH7OzT0+bZ2ce6urrN383Z7Ozs+Pjs7Ozs39/fx8e6urrHx9PT5tO0tLTHx9nZx9PT09PTx8fHx8fHx8fHx8fHx8fHurq6
+urrHrko+Y29KN2N2aWlpdmNjdmlXb29XV2NjUGNjAABXgnZ2iIh2goKCdoiIdnaCdnZ2gm98fHx8j4+PoaGhobS0tLS0tLS0tLS0tLTAwMDNzc3N39/f8vLy8vLy8vLy8vLy8vLy8vLy8vLy////////8v/////////////////////4+Pjf39/f09/y8t/T
+383Nzd/s7N/f7N/Hx8fZ2dns+Pj4///////4+Ozfx8ff+Pjs7Ozs383Nzc3N2dnZx8fH09PT39/f08eux9nNzdnZ5ubm5vLy5ubm2dnArqGuurrH09PT09PTx9PTx8fT09PT08fH08fHx8e6x8fHx8fHumkrV2lEJUpjY2N2Y2N2aV1paWldb29XY2NQUAAA
+V2N8fG9viIh2iIh8fI+Pdo+PgoKViHyPj3x8iHZpdnZ2aWldXW9vfHyPj66uusfHx8fHx8fHx8fH09PT5vLy8vLy8vL////////////////////////////////////////////4///////s39Ps39PT09PT5tnZ2dnZx+zs09Pf09PHx9Pf3+zs////////
++Pj45ubTx7q62dnZ2dnHx8fH0+zZ2bS0tMDAwNPT08fHx7qurq6609PT3+zf3+zs39/f09PT09PT09PH09PTx8fHx8fHx8fHx8e6use6x8e6x5s3N11KN0pXY3ZjV2lpV2lpaVdpaVdjb2NXb28AAFeCb2+CgmmCgnZ2iIhvgo98fI+PdoiViHyVlYKCoY+P
+j2lQUFBQUFBQUFBQUFBQUGl8j6G0x9PT08fHx7q6usfH2eby8vLy8vL////////////////////////////////////////4////////////////8vLy5tnN2dnZ5ubm2dnZ2dnZ7NnZ2dnHx9PT8vLm+Pj4+P//+Pj439/s2dnN2ebm2dnAwMDA09/fzc3N
+urq6x8fT08e6urq6ocfTx9PT09Pf39PT09PT09PH09PT09PTx8fHx8fHx7q6x8fHx7RdMUpvVzdKaWlXaWlXV2ldUGNjV1dpaVBjY1dKAABXaXx8aXyIfG+IiHaCj4J2j498fJWIdo+PgoKViIiVlWlKV0pXaWlpaV1ddpWViIiIb11dXV1pfI+hrsDA09PH
+x8fHurq6x9Pf3/Ly8vL///j///j////4///4+P//+Pj///j///j////4///////////////////////////y8tPHx9/fzebm5ubm5ubm8t/T09PHx9nZ5vLy8v//////8vLm2c3Nzc3N2dnHx7qurq7AwNPT07TT5sChusfTx7q6usfZ2dnZ2dnZ2dnZ2c3N
+zc3Nzc3NwM3Nzc3NwMDAwMB2JUpvUCU+Y1dpdmNjb29daWlXV2lpSldjUFBjMQAAV3xpaYKCaYiVfHyPj3aIlXx8j492j5uCgpubfI+hiGldSjdKY2NXY2NXfKGVlaengoKCb3ybm4iIiHx8fIibrrq6x8fHurqnp6enp6fAwNPm+Pj4//j////4///4+P/4
++P////////j///j4///4////////+P//+P/////////////4+PjZ2ezZzc3f39/T08fHx8fZ2dnHx9nHx9Ps7Oz4+Pj4+Ozs39PAtLTH5ubm2dnZzdnHrqHAzdnZx7qurrrN2dnZ2dnZ2dnZ2c3Nzc3Nzc3Nzc3NwMDAwMDAwMChV0pdSjdKV2l2Y1dpaVdj
+b2NXaWlQXWlpUGNjPgwAAGNjdoh2aYiIb3yIfG+IiHZ2lYh8j498fJuPfI+bgmlXPj5KY1dXaWlKdqGPj6GhgoKCaYKhlYihoY+bm4+PoaGVlaGhobTAwM3NzcDArq6uoaGhtMfZ7Ozs+P/4////+P////////////j/////////////+P//////////////
+///////4////+Pj47Ozfzc3m5sfH08fH2dnmzdnZ2dnNzc3Z2dns+Ozs///s7OzZzcCurq66x9PT08fZx66uwNPT09/f09Pf09PT09PT08fT08fHx9PHx8fHx8fHuseuSkpjY0pKV2lpXV1vY1djb11daWlXY2NXV2NjPhIAAABXgoJpgoJvb4iIb4iIdnaP
+j3aIiHx8m498j6GIiHxXPkpXV1djY1BQfIiIoaGIiIhvb4+bfIihoY+np3yIp6eVp6d8Y2Njb3yIiKGhusfH2c3NzcDArqGurrrH2ezs+P/4//////////////j///j4///4////+P//+P////////j4//j4//j4+P//////+P////Ly383Nzc3NzdnH09Pm
+x8fHx8fHuq66x8fT5ubm+Pj4+Pjm5ubTwLS0tLTH09PH39/f39/f39/f09PTx9PT09PT08fH08fHx8fHx8fHaT5Xb0QlV3xvY3Z2XWlpaV1vb1djb2NXaWlXYyUAAAAAY2N2gm9viIhvgo+Cgo+PdoiVgoKPj3aIm4h8lYhKSko+SmNjV1djY3ybm4iVoXxp
+doiIfJWViJuniIiVlYihrpWCaVdKXV1dXWldXYKnp7TAwMDT09PT39/Tx8e6usfH09Pm8vLy8v////////////j/////////////+P//+Pj/////////////////////8v////////j/+Ozf09/Tx8fH09PT09Ps7NPTx8fHx9nZ2dnZ7Ozs7Ozs7Ozf3826
+utPT39/f39/T09PT09PTx9PT08fT08fH08fHx8fTrjdXb1c3SmlpaXZjY3x8Y29vXV1paVdjY2NXaVcYAAAAAFd8fG+Cgm98iHxviIh2gpWIfIiVgoKViHaPm4JjUD4+V2NXV1dXV4Khj4+hlW98fHx8j498m6ePj4+Pb3yViIiIY0pKV1dXdnZjY4iVlaen
+m5uPj3yVrq7A09PT7Ozf39/H08fH09PT3+zs7Pj4+P////////j////4////////+P//+Pj/+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj///j4//js7N/Hx9PAwNPTx8fH2dnNzc3NzcC0wNPf39/f39/f39/f39/f39/f39/T09/T09PT09PT09PTx8fHx8fHx11E
+XV1KSldpdmNjdnZjb29vXW9vV2NjY1djY1cxAAAAAABXY3aCb2+IiGmCgoJ2lZV2iJWCgpWVgoKbiIiIY0pKV1dXY2NKV4KCdpWVgoKCb2+Pj3aIoY+PoZV2goJ2doKCSkpXSkpjY2NjY298obSbm66VgoKCgoKhoY+hoaGhtMfH09PT09PT39/f09PT09/f
+3+zs+Pj///j////////4+P//+Pj4+Pj/+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4//js7OzT5ubHx8ffx8fHx7rT5ubm5ubm5ubZ2ebm2dnZ2dnZ2dnZ2dnZ2dnN2dnNzc3Nzc3Nzc3N2ac+SnZjRGN8b2Nvb2Njdmldb29jY29jV2lpSl1KBgAAAAAA
+V3ZpaYKCb3yIiHaPj3aClYh8j498iJuPgpWVfHyPgm+IiGlpdnZ2iJWIiJV8Y2+IiHyVoYihro+ClXxpiIh2Y1dXRFdvb2Nvb1eCp5uPrq6IiIh8iKGhj6G0oaGhlYihrq6uroiIobTH09/f39/s39/f09PT09Ps7Oz4//j////////4+P/4+Pj/+Pj/+Pj4
++Pj4//j4+Pj4+Pj4+Pj4+Pj4+Oz4+Pjs+Pjs7Ozf39/ArrrN39/s7N/s7Ozf39/f39/f39/f09Pf39PT09PT09PT09PHx8fHx8fHx8djRGNQPlBjY29vY2Nvb11vb2Njb29XY2NXV2NXGAAAAAAAAFdvgoJpdoh8b4iIdnaPj3aPj3x8j498j6GCgqGVgqGh
+j4+hlXyPp4+Pp6d8iJWIiKGVfI+hiIiVlXZ2iHxpdlBQUFBQXW9vY2OCm4+np5WVlZVpj6F8iK6biKGhiIiurpWurnZXV2Njb3x8iIihtMfT09PT09PT09PT09PT09Pf7Ozs+Pj4+Pj4+P//+P//+Pj4+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj4+Ozs7Ozs7Ozs
+7Pjs7Ozs7Ozs7N/f7N/f39/f39/T09/f09PT09PT09PT09PT09PH08fHx9OnV1djgoJpdnZpaXZ2XWl2Y2Nvb2NjdmNXaWlXPgAAAAAAAABXfG9vgoJpfI98fI+Pb4KPfHyPj3yPj4+ClZWCj6GIiKGhiIinlYihoYiIp5WIoa6VlaebiJubdoKCgm98iFdX
+V0pKaXZpaWlpfKG0oaG0m4KCj4+Pp6ePobShiJWVfJWuoYhpXUpjdmlpdmlpiKGhoa6uoa6ux9Pf39/f39/f08fT08fT09PT3+zs7P////////////Ly//Ly///y///y8vLy8vLy8vLy8vLy8ubm8vLy8vLm5ubm5ubm5ubm5tnm5tnZ2dnZ2dnZ2dnZ2dnZ
+2dnZ2cfH08fH09PTaUppfGlpfG9jb3xjY3ZpXW9vXV1vY1dpaVdXVxgAAAAAAAAASml8fG98j3xviIh2do+CdoiIdnaPj3aPj3x8lZWClaGCgqGVfJWhiIihoYiVoY+Pp6eIoaGPj6GhiJubiHxpV0pKaWlddnZXdqGVla6uiIihiIinlYKbrqGVp6d8fJWC
+do9pRERjY2N2dmlpj6GVrq6hoYh2aZWnm6e6uq7H09Pf39/f39/f08fHx9PT0+bm8v////////////Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLm5ubm5vLm5ubm5ubm5tnm5tnZ2dnZ2dnZ2dnZzdnZzc3Nzc3Nzc3Np1dpaWmCdmN2dmlpdmlXaXZjY3Z2
+V2NjV1dpaT4MAAAAAAAAAERjV1djY2NjdnZ2j498iKGCgpWVfI+biHyVlYKPm4iIm5uClaePj6GhiJWnlYinp4iVp5WIoaGPj6enj6enfHyIdmlpdmNjdoJ2j6eVlaGPdnahgo+np4ihoXx8iIh8fJVvV2NjY3Z2dml2doKhuqGVro9vfKGhiKGhlaGhoZWh
+tLS0x8fH0+bm8vLm5tnZzc3N2dnm5vj4+Pj4+Pj4+Pj4+Pjs+Pjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozf7Ozf3+zs39/s2dnm2dnZ2dnZ2dnZ2dnZ2dnZx9PTx8fT02lKY3x8aWl8b2N2dmNvfGldb29jY3ZpV2lpV1clAAAAAAAAAABpiIiIfHx8SiU3NzdQaWlp
+doh2iJuPj5ubgo+hlYKbm3yIoY98lZWCj6GVgqGhiIihlZWhoY+PrqGPoa6Vla6ulaGulYiViHaVp5ubrqGCgoKCj6Ghlae6p4+hlXaPm4KCaVdKV3x8aXx8Y4Kurpu0tI+PgoKPoZWIoa6hoaGhj6G0p6enj4+hurrH09Pf39/s7N/f09PT09Pf39/y8vLy
+8vLy8vLy8vLy8vLy8vLy5uby5ubm5ubm5ubm5ubm5ubm2ebm2dnZ2dnZ2dnZ2dnN2dnNzc3Nzc3NzadKXXZpaYJ2aXZ2aWl8fGNvb2Njb29XaWldXWk+BgAAAAAAAAAAgqGhoaGuiB8AAAAMGCUlMTExSmNXY3x8fIibj4+hoYiIp4+PoZWCj5uIfJWVfI+h
+j4+np4ibrqGPp6ePoa6hj6enlZW0p5WurpWVtKeIoaGIm66hlae0m5ubm2+Pm3x8b1dKV29vb3x8Y4KuoaGuro+Pj2+IoaGIoa6hj6eVgpu0oaG0j2OIp6enp6entLTHx9Pf7Ozs7Ozf39/T09PT09/s7Ozs+Pjs7Pjs7Ozs7Ozs7Ozs7Ozs7N/f7OzZ5ubm
+2ebm5ubm2dnZ2dnZ2dnZ2dnNzc3Nzc3NzcBdUHZ2doJvb3x8Y2N2dmN8fGNjdmldaWlXY29QGAAAAAAAAAAAAIKhoaGhtIgYAAAAAAYYDAwMGBgYJSUlPlBQUGNvY3aPj4KVoY+bp5uIoaGPj6GVgpWhgoKhoYihrpWVrqGIoa6Vla6ulaG0oZW0tI+ntKGh
+tLSVrq6Vla6hfIiIiHaVgldKSldjfHxpaXyPj66um5ubgm+VroiIrq6brq6IiIiIiKG0lWmIoZWnp6enp6ebm4h2m7rHx8fH09PT39/f39/f09PH09PT09/f7Ozs7Ozs7Ozs7Ozs7Ozs7N/f39/f39/f39/f39/f39/f39PT09PT09PT09PH09ObSl1viHZp
+dnZpaXx8Y3Z2aWl8fGlpdmNjb2NXMQYAAAAAAAAAAACCoaGhobR2GAAAAAASEhISEhISEhISEhISEhISHys3N0pXSldvb2+Cm4iIoaGIm6ePj6GhgpWhiIihoY+htKGVrq6VobShla6ulaG0p5WurpuntLSVrq6VobS0la6uj3xpXV1pfGlpfG9vlaGVobqh
+doKVlZWhoY+hrqGPoY9piJWCgoKCgqG0oZWurpWntKGPoaGVp6ebm6e6usfHx8fH09Pf39/f09PT08fT09Pf7Ozs7Ozs7Ozs39/s7N/s7NnZ5ubZ2dnZ2ebZ2dnZ2dnZ2c3Z2cfH09PHXURvfG98iHZ2goJpdoJpaXZ2Y2N2Y1d2dmNvYxgAAAAAAAAAAAAA
+dqenp6e6bwwAAAAADAwMDBgYGBgYGBgYGBgYGBgMGBgMDBgYGCU3NzdXY1dpfHx8j5uIlaenj6enj5uuoZWnp5WVrqGPoa6Vla6ulae0oaG0tI+uuqGhuq6VrrqhobShiJWhiHyIiGl2m4+Pp7SViIhvfKGhiKG0oaGum3x8iIh2j49jgq6ulae0laG0oY+h
+rqGhrqGPoaGPm66urq66urq6usfH09PT39/f09PHx8fHx9PT39/f3+zs39/s7N/f39/f39/f39/f39/f09PT09PT09PT08fToURdaWmIfGl8fGlpgnZjdnZjY3ZpXW9vY2Nvbz4SAAAAAAAAAAAAAIKhoaGhtF0SAAAABhgYGBgYGBgYDBgYGBgYGBgYGBgY
+GBgYGBgYDBgYGBgYGBglPj5KY2NjfI+CgqGhiKGuoY+np4+hrpuPp6ePoa6hla6ulaG0oZW0tJWnuqeVrrqbp7quobq6m5uurpW0tJunp4+Cla6bj6enj6e0j4+PgoKCj49piKGPp7Sbm66uj6GIb4inp4+hrqGVrq6Poa6hla6uoa6uoa7Hx8fHx8fT09PT
+08fHx8fHx9PT09/f39/f39/f39/f39/f39/f39/T09PT09PT09PTx2k+Y4KCaXx8b2+CgmN2gm9jdnZjb29jY3ZpaWklAAAAAAAAAAAAAAB8oaGhrq5KBgAAAAAMDBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUlPkpKV29vb4iI
+iJunp4+np5WVrqGPp6eVoa6ula66m5u0tJW0tKGhuq6Prrqhobq6obTAoaG0tJWuuqenurqhrrqbm5uPb4KVdoKCb3ybtKGVrq6Vp7SPY3yhj6Gum4+np4+brqGPp6ePoa6hj4+hlae0tLS0x8fHx8fHx8fT08fHx8fHx9PT09PT39/f39/f39/f39PT09PT
+09PT09PT06E+V3Z2doJvb3x8Y298b2N2dmNjdmlddnZjY3ZQEgAAAAAAAAAAAAAAgqGhoa6hMQAAAAAAGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAwYGBgYJTExMUpdXWmIfHyhoY+hrqGVrq6PobShla6ulae0oZW0tJuu
+x66hurqhobqhobS0lafAp5W0tJunuq6btLSPobSnj6Gudnanp5WurpubtLSVlZWVlbquiJWnlZWuro+hrpubrq6Poa6bm6enj6GhlZWurq66urrHx8fHx9PT09PHx8e6x8fH09PT09Pf39Pf39PT09PT09PT09N2PldviIhpfHxpaXx8Y298Y2N8b2N2dmNj
+b29dMQwAAAAAAAAAAAAAAIKnp6e0jyUAAAAAABISEhISEhISEhISEhISEhISEh8fHx8fHx8fHx8fHx8fHx8SHx8fHx8fHx8fHx8fHxISEhISEhISEjE+PlBjY2OIiIiIrqGVp6ePoa6hj6GhiJWuoY+urpuntLSVrrqhobqula6uoaG0tI+uupubtLSVrrqh
+oaGhla66oaG0tJWhlWlpoaGPoZVvfKGhj6GulZWnp4+hrqGVp6eIoa6hj6eniJWniHyPp6enurq6urq6x9PT08fHx8e6urrHx8fT09PT09PT09PT09OuSj52dnZ2iG9vgnZpdoJpaXx8Y2N2Y2N2dl1vVxgAAAAAAAAAAAAAAACCoaGhtHwYAAAAAAwYGBgY
+GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJRgYGBgYGBglJTFKV1dpgnZ2m5uIoaGPj5uPfJWhiIibj3yVp5WVtLSPp7ShobS0lae6p5u0tJWnuqebtLSbp7qum7S0m6e0lW9vlZWhtKGCj6GPm7Shj6enj4+n
+m4ihoY+PtKePoa6VlaeViJWhiIihoY+hoZWhoaGuurrH09PH09PHuse6urrHx8fZ2dnZ2dnZgkpdaXyIdml8fGl2iHZpfHxpaXxvY3Z2XV12djcSAAAAAAAAAAAAAAAAdqGhoaFdEgAAAAAGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIf
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fEhIfHx8rKytEXV1pgoKCj6GPj6GVfHyVfG+Pj3yIoY+Poa6VobSnla66m5u0tI+0tKGhtLSVrq6hobS0laenlaG0tI+ntKGhrqGPoa6bj6enj4+PgoKnp4ibp4+PoaGIm6ePj6GhiJWnlZWnm4iVoZWV
+oaGhobrHx8fHx8fHurq6urrHx8fZtFc+Y4iIb4KCdnaCdml2dnZpfHxjdnZpaXZ2Y2MlAAAAAAAAAAAAAAAAAIinp49vJQYGBgYGEhISEhISEh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fPkpKV2lpaYihiIihlXyPj3x8iIhviJWIiKGhiKG0oZWuro+htKentLSVp7S0lbS0m6e0p5uuuqGhtLSVrq6Voa6hj6GhfIihoYihoZWVp6eIm6ePj6GhiJunj4+bm3ybm4iIm5uCj498iHx8obTAwMDAwMDAtLS0p2k3XYh8b4KCaXyI
+dnZ2gmNvfG9jdnZjY3xvY28+DAAAAAAAAAAAAAAAAACIlYJdNxgGBgYGBhISEhISEhISEhISEhISEhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHxISEhISEjElMURXV1d2doKVlYiI
+lYh2j498fJuIdo+biIihlYihrpubtLSVrrqhobS0j6e0oaG0p5WntKGVrq6Poa6bj6enj6GhlYinp4+hoY+Cm5uIlaePj6GhiIihj4KVlYKPoY+PoY9XY4KCdo+Poa66x8fHrnY+SmlpgoJvb4J2aXaCaWl8fGNvfGlpdnZdaXxjJQYAAAAAAAAAAAAAAAAA
+dnZKKysSAAAMAAASEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx83NzdjY2OClYiIoaGIm6eIiJWIdoiIiHybm4ihrqGhtLSbp7Sn
+m7q6lae0oaGuro+hrqGPp6eVlaeniKGhiIinm4iIiHyPp6eIoaGPj6eniKGhiIihlXyPoYh2aYKCj5uCgoKCgo+hlXZdSld2iHZpgoJpdoh2aXx8Y298aWl2dmNjfGldShIAAAAAAAAAAAAAAAAAAEo3JRgYDAAAAAAMDBgYGBgYGBgYGBgYGBgYGBgYGBgY
+GBgYGBgYGBgYGBgYGCUlJRgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUYGCUlJSUlGBgYGBgYGBgYGBgYGBgYGBgYDBglJSVEV1djfHx8laGPm6enj6GhfHyPfG+PoYiVrqGVrrqhobS0la6uoaGuoY+hrpWVp6eIoaGPj6GhiJuniHaV
+oYibp4+PoaGClaGPj6GVgo+hj4+PY1d8lYh8lZWCj4+Cdoh8b4KPgm+IiG98iHxpgoJpaXx8Y3Z2Y2N2aVdvYyUGAAAAAAAAAAAAAAAAAAAfHwwYKwwAAAAAAAwYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglGBgYGBgYGBgYGBgYGBgYGBgYGBglJRgY
+GBgYGBgYGBgYGBgYGBgYJSUlJRgYGCUlJSUYGBgYGBgYGBglJRgYGBgYJSUlJSUlJSUlGBgYGBgYJSU3V2NjfJuPj7SniIiVfHyPj3aIoYiIp6ePoa6hlbS0j6e0oY+np4+bp5uPp6eIlaebiKGhj4+nlYihoYiIp5WCoaGIiKeVgpubb1B8iHaPj3yIlZV8
+iJWCgo+Pb4KPdnaIiGl8iG9vgoJjdoJpaXx8Y2N2Y0oYAAAAAAAAAAAAAAAAAAAAEhIlJRgMDAAMDAwYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglJSUYGBgYGCUlJSUlJSUYGBgYJSUlJSUlJSUlJSUlJSUYGCUlJSUlGBglJSUlJSUlJSUY
+GCUlJSUlJSUlJSUlGCUlJSUYJSUlJSUYAAAAAAAAGBglSmNjdo+Pj6enj5unj4KVlXaIm4iIoaGPj66hlaenj4+uoYihoY+Pp5uIoaGPj6GViJuniIihlYKVoYiIoZV8j6GIiGlpdoiViHyVlXyIm4KCj492do92aYKCaXaCdmmCgml2gm9jdnZjY3xvY28+
+EgAAAAAAAAAAAAAAAAAAABIrGBgYBgYGBgYGEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwwMDAwMAAAM
+AAAMDBglN1djY4+Pj6e6oaG0p4ibm4KCj4+ClaGPj6eniKGum4+hoYiVp5WIoaGIlaePj6Ghgo+hj4+bm4iIoY+ClYhKaZWIfI+PfIiVgnaIiHZ2j49viIhvb4h8aXx8b2+Cdml2dmNjfG9jb29XJQwAAAAAAAAAAAAAAAAAAAAYGCUSEhIGBgYGBhISEhIS
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8MDAwMDAwMDAwMAAAAAAAADBgYJUpKV3x8iJWnp5Wnp4iIoYh8lZV8
+iKGPj6GhiJWnlYihoYiIp5WIoaGIiKGPgpWhiIihj4KVoYhpaYKCj6GCgpubfI+PfHyPj2+IiHZ2iIhvfIh2doKCaXaCb298fGNjdmlpUBgGBgAAAAAAAAAAAAAAAAAAGBgAABIAAAAAABISEhISEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fDAwMDAAMDAwADAwMAAwMDAwMAAAAAAwMHx8xPldpaXaPj4+hoY+PoZV8j5uCgqGVgpWhj4+np4iVoY+PoaGCj6GIiJubgo+h
+iIiIaWmCm4h8j498iIiIfI+PdoKPgnaIiG98iHZpfHxpdoJ2aYKCaXZ2dmN2djESAAAAAAAAAAAAAAAAAAAAAAwMABglDAAADAwMGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglJSUlGBgYGBgYGBgYGCUlJRgYGBgYGBglJRgYJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwAAAAADAwMDAwMDAwMDAAMDAwAAAAAAAAAABIfHz5XY2OCgoKVoY+PoZV8iJuIfJWVgo+hj4+bm4KPoY+Cm5uIiKGPgpWVfG+VlYKVlYKClYh8iJV8fI+Pb4iIfHyPgml8iG9v
+fHxjdoJvb3x8aWl8b1AlBgYGAAAAAAAAAAAAAAAAAAAGBh9XPgwADAwMDBgYGBgYGBglJSUlJRgYGCUlJSUlJSUlGBgYGBgYJSUlJSUYGBgYGBgYGBglJSUlJSUlJSUlJRgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRgMDAwMAAAMDAwMDAwADAwMDAwMDAwMDAwMDAwMDAwMDAwAAAwYGCU3SldjfHx8j5uIiKGPfIiVgoKPj3yPoYiIoaGCj6GIiJWVfI+biIibm4KPj498lZV8fJWCdo+PdoKVgnaIiG98iHxpgoJpdoJ2aXZ2aWkxEgYGBgYAAAAAAAAAAAAAAAAA
+DDFpiD4SEgYGBhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYG
+BgYGBgYGBgYGBgYGBgYGBgYGEhIlRERXaXZ2iJuIiJWVdoibiHyPj4KPoY+Cm5uIiKGIfJWVgoKbj3yPj3x8j4J2j4+Cgo+PdoiIfHyIiG98fGlpfHxpfHxpaXxXHwwAAAAAAAAAAAAAAAAAAAAAADdpfHw+EgYGBgYSEhISEh8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fKysfHx8fHx8fHwwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAwMDAwMDAwMDAwYJTE+V29v
+b4+Pdo+PgoKViHyPoYiIoaGCj6GIiJuPgo+bgoKVlXaIiHx8j492go98fIiIb2+Idml8fGN2gm9vfHxjRB8MDAAAAAAAAAAAAAAAAAAAAABjfHxvMQwMDAwMGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUlJSUYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgMDAwMDAwAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAADAwMJT4+V29vb4KCdoKbiIibm4KPoY98lZV8iJWCgo+PfHyV
+iHaIiHx8j4JviIh2doiIb4iIb2+CgmN2dmlpaSUSBgYGBgYAAAAAAAAAAAAGBgAAY29jYzEMAAwMDBgYGBgYGBgYJSUlJSUYGCUlJSUlJSUlJRgYJSUlJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwMAAwYJTE+SmlpaXyIdoKVlYKVoYiIm498j498fJWIdoiVgoKPgm+IiHx8j49vfIh2doKCaXx8b298fEofDAwM
+AAAAAAAAAAAAAAAAAAAAAFdXV1clEgYGBgYYGBglJSUYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMJTExRFdjV2mCdnaIiHaIlYJ2j49vfI98fI+PdoKPgm+IiG98j3xviIhpdoh2aXx8aWk3GAwMDAAAAAAAAAAAAAAAAAAAAABKY2NXJRIAABISEh8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHxISEhISBgYGBgYGBgYGBgYGBgYGBgYSEhISEhISEhISEgYGBgYGEhIS
+EhISEhISEgYGBgYGBhISEhISEhISEhISBgYGBgYGGCUxMUpdXV12dnaCgnZ2iIhvgoJvb4h8b4iIfHyPj2+CgnZ2goJpfHxvb4JXJRgMDAwAAAAAAAAAAAAAAAAAAAAASldXVyUMAAASEhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHysrHx8fDAwADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwA
+AAAMDB8rK0pdXV1paV12dnZ2dnZjb3xvb4iIdnaPfHyIiG9vgnZpfHxpPhgGEhISBgYGBgAAAAAAAAYGBgYAAERXV0olDAAAAAwYGBgYGBgYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBglJTdKV0pjY1djb29jb29jb4KCb4iI
+dnaCgmmCgml2aSUMDAwMAAAAAAAAAAAAAAAAAAAAAABEV1dXJQwMDAAMGBgYJSUlGBgYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMTElJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAABglJTFKSj5XV1dXb29XaXxvb4iIb3yIb298fEoYDBgMDAwAAAAAAAAAAAAAAAAAAAAA
+RFBQUCUMDAwMDBgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlMTElJSUxMSUlJSUlJSUlJSUlMSUYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAwfMTE+SkpKY2NXY3x8b4KCb2+Cgl03DAwMDAwMAAAAAAAAAAAAAAAAAAAAAERXV1cYBgYGBgYYGBgYGBgYJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJSUlJSUxMTElJTElJSUlJTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlEhISEhISEhISEhISEhIGBgYGBgYGBgYSEhISEhISEhISEhISEhISEhISEhISEgYGEhIGEhIGBhIS
+EgYGBgYSEhISEgYGEhIGBhISEhISBgYGEhISEhISEhISBgYGBgYYGCU3NzdEXV1daYJvb4KCaXZjJQYGBhISAAAAAAAAAAAAAAwMDAAAAABEUFBQJQwADAwMHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHysrKysrKysrKysrKysr
+KysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrHxISEhISEhISEhISEhISEhIGBhISEhISEhISEhISEhISEhISEhISEhISEhISEhISBgYSEhISEhISEhIGBgYGBhISEhISBhISEhISEhISEhISEhISEhISEhIS
+EhISEhISBgYGBhIlMTE+UFBQb3xvb4JvShgAAAwMDAwAAAAAAAAAAAAAAAwMDAAARFdXVyUMAAAAEh8fHx8fHx8fHx8fHx8fHx8rKysrKysrKysrHysrKysrKx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHzElJSUlJSUlJSUlJSUlJSUlJSUlJTEl
+JSUlJSUlJSUlJSUlMSUxJSUlJSUlJSUSEhISEhISEhISEgYSEgYGEhISEhISEhISEhISEhISEgYGEhISEhISBgYGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEgYSEhISEhISBhISEhISEhISEhISEhISEhIGBgYYJSU3SldXdmNjYz4SAAASEhIG
+BgYGBgYAAAAGBgYGBgYAAERXV1crDAAMDAwYGBgYGCUlJSUlJSUlJSUlJRglJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxJTExMSUlJSUlMTElMSUlJTExJTElJSUlMSUxMSUxMTElJSUlJTExJSUlJSUlMTElMSUlJSUlJSUlEhISEhISEhIS
+EhISBgYGBgYGBhISEhISEhISBhISEhISEhISEhISEhISEhISEhISEhISEhISBgYGBgYSEhISEhISEhISEhISEhISEhISEgYGEhISEhISEhISAAwMDAwMDAwMDAwMDAwYGBg3Nzc3Sj4fBgYGBgYGBgYGBgYAAAAAAAAMDAwMAABEV1dXMRISAAwYJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUxMTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUlJSUlJSUlJTExMTExMSUlJSUlJSUlJSUlJRISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISBgYGBgYGEhISEhIfHx8fDAAAAAwMDAAAAAAAAAAAAAAMDAwMDAAASldXVz4YDAwMGCUlJSUlJSUlJSUlJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJTElMSUlJSUlMTExJTExJSUxJSUlMSUlMTElJTElMSUxMTExMTExJSUlMTExJSUlJSUlJTExMTExJSUxJTExMR8SEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhIGEhISEgYSBgYGEhISEhISEhISEhISEhISEhISEhISEhISEgYGAAAMDAwMAAAAAAAAAAAADAwMDAwAAERXV1c+GAwMDBgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUlJSUlJSUlJSUlJSUxJTElJSUl
+MSUlJSUlJTExJSUlJTExJSUxJSUxMTExMTElJSUlJTExJSUlJSUlEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEgYSEhISEhISEhISEhISEhISEhIS
+EhISBgYGEhISAAAADAwMDAwMDAAAAAAAAAwMDAwMAAA+V1dXRBgGBhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysr
+KysrKysrHx8MDAwMDAwMDAwMDAwMDAwMDAwMDBgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwYGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgGBgYSEhISBgYABgYGBgYGBgYGBgYGBgYGBgYGBgAA
+PlBQXUoYDAwMGBglJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJTElJSUxJSUlJSUxMTElMTElMTElMSUlJSUxJTExMTExMSUlJTElMTExMSUxJTExJTExJSUlJSUlJSUlMTExMR8SEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIGEhIGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIGEgYGBhISBhgMDAwYGAwAAAAMDAwMAAAAAAAAAAAAAAAMDAwAAEpXV2NEGAwMDB8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8xJSUlJSUlMTExJTElMSUlJTElJTExJSUlJSUlJSUlJSUlJSUlJSUxJTExJSUxJSUlJTElMTExMTElMSUxJSUlMSUxMSUlJTExMSUlMSUYGBgMDAwMDAwMDAwMDAwYGAwMDAwMDAwMDAwMDBgYGAwMDAwMGAwMGAwMDAwMDAwMDAwMDAwMDAwM
+GAwMDAwMDAwMDAwMDAwMDAwMDAwMGBgYDAwMDBgYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgGBgYGEhIGBgYGBgYGBgYGBgYGBgYGAABKV2NjSh8SEhIlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUxJSUxJSUlJSUlJSUlJSUl
+MTExMSUlJSUxMTElMTElJSUxJSUxMTExMSUlMSUlMSUlMTElJTExMTExJSUxMTExMTExJSUxGAwMDAwMDBgYGBgYGBgYGBgMDAwYGBgMGBgYDBgYGBgYGAwMDAwMDAwMDAwMDAwMDAwMDAwYGAwMDAwMDAwMGAwMDAwYGBgMDBgYDAwYDAwMDAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgYBgYABgYSEgYGBgYGBgYGBgYGBgYGBgAASmNjY1AfDAwYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJTElJSUlJSUlMSUxMSUxMTElJTExJTElJSUxMSUlJSUlJSUlMSUlMTExJTElJSUxJSUlJSUx
+MTExMTElJTEYAAAAAAAAACsrKysrKx8SEhISEhISEhISEhISEhISEgYGBgYGBhISEhISEhISEhISEhISEhISEhIGBhISEhISEhISEhISEhISEhISEhIGBhISBgYGBgYGBgYGBgYGEhISEhISEhISEhISBgYSEgYSEhISEgYGBgYGBgYGBgYSEgAAAAAMDAwA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHx9PT39/s7Pj4////////
+////////////////AAAAAAAADAwYGCUlMTE+PkpKV1djY29vfHyIiJWVoaGurrq6x8fT09/f7Oz4+P///////////////////////wAAAAAAAAwMGBglJTExPj5KSldXY2Nvb3x8iIiVlaGhrq66usfH09Pf3+zs+Pj/////AA==
+--mail.sleepy.sau.144.8891
+
+
+The digitizer does 64 gray levels at any of:
+ 256 x 244 (included)
+ 128 x 122
+ 66 x 61
+
+At 19.2k baud (it uses an rs232 interface) it takes up to 32 seconds to
+fetch the image (with no compression). The lower resolutions take
+8 and 2 seconds respectively.
+--mail.sleepy.sau.144.8891--
+
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: More Imagery
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29595; Thu, 3 Oct 91 13:05:05 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19384; Thu, 3 Oct 91 13:04:49 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12304> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:44 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08989> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:43 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:04:42 -0400 (EDT)
+Resent-Message-Id: <EcurTOC0M2YtQTa2QW@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:04:42 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <sau@sleepy.bellcore.com>
+Date: Fri, 7 Jun 91 09:09:05 EDT
+From: sau@sleepy.bellcore.com (Stephen A Uhler)
+Message-Id: <9106071309.AA00574@sleepy.bellcore.com>
+To: nsb@sleepy.bellcore.com
+Subject: meta-mail
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary="mail.sleepy.sau.158.532"
+
+--mail.sleepy.sau.158.532
+
+2 Questions:
+
+1) MM_QUIET doesn't seem to work in the 212 version of metamail.
+
+2) I've been pondering how I am going to send you this 40mb mail message.
+ I was going to split it up using the "parts" mechanism, but then it occurred to me.
+ My software has no business worrying about message size. Its up to the mail
+ delivery agent to do that form me. I should be able to compose a very
+ long message, and pass it off to sendmail (well, a pre-processor to sendmail).
+ if the message is too long, based upon the src/dst etc, then sendmail should
+ break it into multiple parts for me, and send it as multiple messages. As the
+ user (sender) I should be unaware this is happening.
+
+ Similarly, on the receiving end, the separate parts should be assembled as they
+ arrive; I shouldn't have do deal with them at all as multiple messages.
+
+ I guess what I am proposing are two utility programs:
+
+ msplit <max_lines> <max_bytes>
+
+ that takes 1 mail message and splits it into N messages and
+
+ mcombine <parts...>
+ mcombine <partially_assembled_thingy> <parts...>
+
+ that any mail system can call to achieve a uniform mechanism of splitting
+ and recombining messages, Kind of like:
+--mail.sleepy.sau.158.532
+Content-type: image/pbm
+Content-transfer-encoding: base64
+Content-Description: Mail architecture slide
+Comments: Image wrapped by /usr/sau/bin/fetch_image
+
+UDQKNzk4IDUzMgoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAANwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAB////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAP///////////AAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////wAAAD////gAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///gAAAAAAH///gAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//wAAAAAAAAA///AAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAAAAAAAAAAf//AAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//wAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAA
+AAAAf/4AAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AP/gAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAfAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABwAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAeAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAHgC4AAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAABcAuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAA/4
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAXATgHgdB0DwDwB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAF4E4GMIwjBGB
+HxjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAABOCODBmEYQwwwwwYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAA
+AAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAATwjggZgGAMMMMIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAEcQ4f+eB4ADDDH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAABHkOGAD4PgHwwxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAQ6DhgAPA8HMGIYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAEPg4YAA4DjDA8GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABBwOHAUGQZgwQBwEAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAQcDg4JBkGYMMAOCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAECA4P8YRhHPz/j/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAHwg/g8F4Xg+Yf8PAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAA/wAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAABgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAQEAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAfwAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADADAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAwBwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAHQRwME/v4HgJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAACM9/DPDAwGMe8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAABhDgwwwwMDBhyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAYAwOMMMDAgYYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAHgMBjDDAwf+GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAA+DAYwwwMGABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAADwwGMMMDBgAYAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAOMBjDDAwYAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAPwAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAABBjAwwwwMHARgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAQYwMMMMDA4IY
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAGEOGDDDIyP8GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAABeDeD/8cHA8H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAD8AAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAIkAAAAAAAAA////////////4AAAAAD////////////gAAAAAB////////////wAAAAAAAAAAAAAAB+
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAjAAAAAAAAAf////////////AAAAAB////////////8AAAAAA////////////+AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAEAA
+AAAAAAAH////////////wAAAAAf////////////AAAAAAP////////////gAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAEUAAAAAAAAB////////////8AAAAAH////////////wAAAAAD////////////4
+AAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAPwAAAAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAA
+AADwAAAAAAAAAAB4AAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAB8AAAAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAH
+gAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAA+AAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAIkAAAAAAAAB4AA
+AAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAA
+AAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAA
+AAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAA
+AAAAeAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAfgAAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAA
+A8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAQAAAAAAAAAeAAAAAAAAAAAPA
+AAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAA+AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAjAAAAAAA
+AAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAA
+AAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AHwAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAA
+AAAAAAAAAHgAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAB8AAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAA
+AAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAARQAAAAAAAAHgAAAAAAA
+AAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAfAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAEAA
+AAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4
+AAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAA8AAAAIwAAAAAAAAHgAAAAAAAAAADwADwAAeAAAAAAAAAAAPAAB4AAPAAAAAAAAAAAHgAAAAAAeAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAABAAAAAAAAAB4AAAAAAAAAAA8AB/AAHgAAAAAAAAAADwAA/
+gADwAAAAAAAAAAB4AAAAAAP4AAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAABFAAAAAAAAAeAAAAAAAAAAAPAAf+AB4AAAAAAAAAAA8AAP/AA8AAAAAAAAAAAeAAAAAAD/wAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAfAAAAiQAAAAAAAAHgAAAAAAAAAADwAH/4AeAAAAAAAAAAAPAAD/8APAAAAAAAAAAAHgAAAAAA//AAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAACMAAAAAAAAB4AAAAAAAAAAA8AA//wH
+gAAAAAAAAAADwAAf/4DwAAAAAAAAAAB4AAAAAAH/+AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAQAAAAAAAAAeAAAAAAAAAAAPAAD//B4AAAAAAAAAAA8AAB//g8AAAAAAAAAAAeAAAAAAAf/4B4AAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAARQAAAAAAAAHgAAAAAAAAAADwAAP/+eAAAAAAAAAAAPAAAH//PAAAAAAAAAAAHgAAAAAAB//w+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAIkAAAAAAAAB4AA
+AAAAAAAAA8AAAf//gAAAAAAAAAADwAAAP//wAAAAAAAAAAB4AAAAAAAD//PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAjAAAAAAAAAeAAAAAAAAAAAP//////4AAAAAAAAAAA///////8AAAAAAAAAAAf///////////
+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAEAAAAAAAAAHgAAAAAAAAAAD//////+AAAAAAAAAAAP///////AAAAAAAAAAAH///////////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeA
+AAEUAAAAAAAAB4AAAAAAAAAAA///////gAAAAAAAAAAD///////wAAAAAAAAAAB///////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAACJAAAAAAAAAeAAAAAAAAAAAP//////4AAAAAAAAAAA///////8AAAAAAA
+AAAAf///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAIwAAAAAAAAHgAAAAAAAAAADwAAA//+AAAAAAAAAAAPAAAAf//AAAAAAAAAAAHgAAAAAAAH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAPAAABAAAAAAAAAB4AAAAAAAAAAA8AAB///gAAAAAAAAAADwAAA///wAAAAAAAAAAB4AAAAAAAP//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AABFAAAAAAAAAeAAAAAAAAAAAPAAD//x4AAAAAAAAAA
+A8AAB//48AAAAAAAAAAAeAAAAAAAf/+eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAiQAAAAAAAAHgAAAAAAAAAADwAD//geAAAAAAAAAAAPAAB//wPAAAAAAAAAAAHgAAAAAAf/8HgAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAACMAAAAAAAAB4AAAAAAAAAAA8AB//gHgAAAAAAAAAADwAA//wDwAAAAAAAAAAB4AAAAAAP/8D4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAQAAAAAAAAAeAAAAAAAAAAAPA
+Af/AB4AAAAAAAAAAA8AAP/gA8AAAAAAAAAAAeAAAAAAD/4A8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAARQAAAAAAAAHgAAAAAAAAAADwAH+AAeAAAAAAAAAAAPAAD/AAPAAAAAAAAAAAHgAAAAAA/wAPAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAIkAAAAAAAAB4AAAAAAAAAAA8AA8AAHgAAAAAAAAAADwAAeAADwAAAAAAAAAAB4AAAAAAHgAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAjAAAAAAA
+AAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAA
+AAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAPAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAA
+AAAAAAAAAHgAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAHgABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAA
+AAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AARQAAAAAAAAHgAAAAAAA
+AAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAB8AAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAEAA
+AAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4
+AAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8ACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAPAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAA
+AADwAAAAAAAAAAB4AAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+ABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAHgAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4ACMAAAAAAAAB4AAAAAAAAAAA8AAAAAH
+gAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAD4AAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AIkAAAAAAAAB4AA
+AAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAADwA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
++AEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgCJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAA
+AAAAeAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAeABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgBFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAA
+A8AAAAAA8AAAAAAAAAAAeAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAQAAAAAAAAAeAAAAAAAAAAAPA
+AAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ARQAAAAAAAAH////////////wAAAAAf////////////AAAAAAP////////////gAAAAAAAHwAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgIkAAAAAAAAB////////////8AAAAAH////////////wAAAAAD////////////4AAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AjAAAAAAA
+AAf////////////AAAAAB////////////8AAAAAA////////////+AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAEAAAAAAAAAD////////////gAAAAAP///////////+AAAAAAH////////////AAAAAA
+AAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAB4CJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAA8BFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwCMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4IkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAwQAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEAA
+AAA/gfAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPkAAAAADwAAAAAM8AAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EUAAAADgBAAAAAAAACAAAAAAAAQAAAAAAAAAAAAAAAAAAAAOHAAAAAAMAAAAAA
+DAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeCJAAAAA4AQAAAAAAABwAAAAAAAEAAAAAAAAAAAAAAAAAAAADAwAAAAADAAAAAAAwAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAHgIwAAAAOAEAAAAAAAAcAAAAAAADAAAAAAAAAAAAAAAAAAAABwEAAAAAAwAAAAAAMAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BAAAAADgBAAAAAAAALgAAAAAABwAAAAAAAAAAAAAAAAAAA
+AcBAAAAAAMAAAAAADAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeBFAAAAA4AQOgPATAAC4APAHgTw/gAAAAAAAAAAAAAAAAAAAHgAHgTwB7BOHAPAQwAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAHwiQAAAAOAEEYMY94ABHAEfGM9+DAAAAAAAAAAAAAAAAAAAAA+AGM9+Bhz334EY8MAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8CMAAAADgBDCGDDkAARwDDDBjhwwAAA
+AAAAAAAAAAAAAAAAAH4DBjhwwMOPHDDDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAQAAAAA4AQwBAwwAAIOAwwgYwMMAAAAAAAAAAAAAAAAAAAAAfggYwMMDDBgwwwwwAAAAADwAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwRQAAAAOAEPA/8MAACDgMMf+MDDAAAAAAAAAAAAAAAAAAAAAB8f+MDGAwwYMAMMMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8IkAAAADgBB8MAD
+AAB/8DDGADAwwAAAAAAAAAAAAAAAAAAAAAHmADAxgMMGDAfDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAjAAAAA4AQHjAAwAAQHAYhgAwMMAAAAAAAAAAAAAAAAAAAAAA5gAwMYDDBgwcwwwAAAAADwAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwEAAAAAOAMAcwAMAAIA4DwYAMDDAAAAAAAAAAAAAAAAAAAABAOYAMDGAwwYMMMMMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+A8EUAAAABwCCDOAjAACAOBAHATAwwAAAAAAAAAAAAAAAAAAAAYDnATAxwMMGDGDDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPCJAAAAAcBggxwQwABABwwA4IwMMAAAAAAAAAAAAAAAAAAAAGAw4IwMMHD
+BgxgwwwAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwIwAAAADxwMIf4MAAQAcP+P8MDDIAAAAAAAAAAAAAAAAAAAA4cP8MDD+8wYMc/MMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAA8BAAAAAAPwC8B4PwAfAfx/w8Pz4cAAAAAAAAAAAAAAAAAAAAL8A8Pz4PI/fvj5v/wAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPhFAAAAAAAAAAAAAAAAAAwMAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAYDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAGAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAABwwAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAHiJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4IwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAHhFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAB/gA/wAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAH4ADwAA
+AAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAA/AAYAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAH4AGAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAA
+AAAAAAAAAB/ABgAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAfwAYAAAAQAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAB4EAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAG+AGAAAAEAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAeEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAeAAAAAAAAAAAAAAAAABnwBgAAADAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAHiJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAY+AYAAABwAAAAAAAAAADgAAAAAAAAAAAA
+AAAAAAAAAAAB4IwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGHwGAD4A8AAAAAB+ABjg4AAAAAAAAAAAAAAAAAAAAAAAAeBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABh8BgDjg/9+P4fhx4H58OH8AAAAAAAAAAAAAAAAAAAAAAHhFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAYPgYBgcBwPB4Bw4HA
+OnDgcAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGB8GAYDAcBwOAYcB4Dxg4MAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABgPhgMA4HAeDwEHAOA8AOGAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAY
+B8YDAOBwDgcDDgDwOADjAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGAfGB//gcA4HAg4AcDgA5gAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABgD5gcAAHAPD4YOAHA4AOwAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAA
+AAAAAAAAAAAAAAAYAfYHAABwBwuEDgBwOAD8AAAAAAAAAAAAAAAAAAAAAAAD4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAD+BwAAcAcbjA4AcDgA/gAAAAAAAAAAAAAAAAAAAAAA
+A8EUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAABgAfgcAAHAHk8gOAHA4AO8AAAAAAAAAAAAAAAAAAAAAAAPCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAADwAAAAAAAAAAAAAAAAAYAH4HAABwA7HYDgBwOADngAAAAAAAAAAAAAAAAAAAAAADwIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAA+B4AgcAOh0A8AcDgA48AAAAAA
+AAAAAAAAAAAAAAAAA8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAABgAHgOAIHAD4fAHAOA4AOHgAAAAAAAAAAAAAAAAAAAAAAPBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAYAA4DwEBwgcDgB4DgOADg8AAAAAAAAAAAAAAAAAAAAAADwiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAAGAfHAeIHA
+4AOBwDgA4HgAAAAAAAAAAAAAAAAAAAAAA8CMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAADwABgD/gD8BgMAB44B8AfA8AAAAAAAAAAAAAAAAAAAAAAPAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAD/AAIAPgAeAIBAAH4B/wf8/wAAAAAAAAAAAAAAAAAAAAADwRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8IkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAeCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAeBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4CMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4CJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAeAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAeAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgCMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+eAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAA+ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAA8ACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAB4ABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAADwAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAPgAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAeAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAIkAAAAAAAAA//////////////////8AAAAAAAAAP//////////////////AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAjAAAAAAA
+AAf//////////////////gAAAAAAAAH//////////////////4AAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAEAAAAAAAAAH//////////////////4AAAAAAAAB//////////////////+AAAAAAAAA
+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAEUAAAAAAAAB//////////////////+AAAAAAAAAf//////////////////gAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AADwAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAHgAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAA
+AAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAH4AAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAA
+AAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAB8AAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAA
+AAAAAAAAAAAHgAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAD4AAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+AAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAB8AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAIkAAAAAAAAB4AA
+AAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAA
+AAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAA
+AAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAA
+B4AAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAB+AAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHg
+AAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAQAAAAAAAAAeAAAAAAAAAAAAA
+AAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAH4AAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAjAAAAAAA
+AAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAA
+AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/
+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAHgAAAAAeAAAAAAAAAAAAAAAAAHgAADwAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAD8AAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAH8AAAAAHgAAAAAAAAAAAAAAAAB4AAD+AAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AP/AA
+AAAB4AAAAAAAAAAAAAAAAAeAAH/gAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAP/wAAAAAeAAAAAAAAAAAAAAAAAHgAH/4AAAAAAAAAAAAAAfAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgf/4AAAAAHgAAAAAAAAAAAAAAAAB4AP/8AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4f/4AAAAAB4AAAAAAAAAAAAAAAAAeAP/8AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAe//4AAAAAAeAAAAAAAAAAAAAAAAAHgf/8AAAAAAAAAAAAAAAH
+4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAH//wAAAAAAHgAAAAAAAAAAAAAAAAB4f/4AAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB//////////4AAAAAAAAAAAAAAAAAf////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAf/////////+AAAAAAAAAAAAAAAAAH////
+////////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAH//////////gAAAAAAAAAAAAAAAAB/////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAD8AAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB//////////4AAAAAAAAAAAAAAAAAf///////////////////9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAf/8AAAAAAAeAAAAAA
+AAAAAAAAAAAHn/+AAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAH//wAAAAAAHgAAAAAAAAAAAAAAAAB4f/4AAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAfgAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB7//gAAAAAB4AAAAAAAAAAAAAAAAAeB//wAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+H/+AAAAAAeAAAAAAAAAAAAAAAAAHgD//AAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgf/4AAAAAHgAAAAAAAAAAAAAAAAB4AP/8AAAAAAAAAAAAAAAAPwAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4A//AAAAAB4AAAAAAAAAAAAAAAAAeAAf/gAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAIkAAAAAAAAB4AA
+AAAAAAAAAAAAAAAeAD/wAAAAAeAAAAAAAAAAAAAAAAAHgAB/4AAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAH8AAAAAHgAAAAAAAAAAAAAAAAB4AAD+AAAAAAAAAA
+AAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAeAAAAAB4AAAAAAAAAAAAAAAAAeAAAPAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAA
+AAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAA
+B4AAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAB+AAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHg
+AAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAA
+AAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAB+A
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAjAAAAAAA
+AAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAA
+AAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAA
+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAD+AAAAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAA
+AAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAA
+AAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAA
+AAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAA
+AAAAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAA
+AAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAA
+AAAAAAAAAAD/gAAAAAAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+AAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAD/g
+AAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAARQAAAAAAAAH//////////////////4AAAAAAAAB//////////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAIkAAAAAAAAB///
+///////////////+AAAAAAAAAf//////////////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAjAAAAAAAAAf//////////////////gAAAAAAAAH//////////////////4AAAAAAAAAAAAAA
+AAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAEAAAAAAAAAD//////////////////wAAAAAAAAA//////////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAB/4A
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAA
+AAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAH/4AAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAAAAAAAAAAAAD//AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAB//wAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAAAAAAAB//8AAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//8AAAAAAAAAP//wAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///gAAAAAAH///gAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH////AAAAP///+AA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4APAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAPADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/////////+AAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwBcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAP///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuAXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAALgJwDwOg6B4B4A8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAD+B8AAAAAAAAgAAAAAAAAAAAAAAAAAAAC8CcDGEYRgjAj4xgAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAOAEAAAAAAAAIAAAAAAABAAAAAAAAAAAAnBHBgzCMIYYYYYMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAADgBAAAAAAAAHAAAAAAAAQAAAAAAAAAAAJ4RwQMwDAGGGGEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAA4AQAAAAAAABwAAAAAAAMAAAAAAAAAAACOIcP/PA8ABhhj/wAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAOAEAAAAAAAAuAAAAAAAHAAAAAAAAAAAAjyHDAB8HwD4YYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAADgBA6A8BMAALgA8AeBPD+AAAAAAAAAAAIdBwwAHgeDmDEMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAA4AQRgxj3gAEcAR8Yz34MAAAAAAAAAAACHwcM
+AAcBxhgeDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAOAEMIYMOQABHAMMMGOHDAAAAAAAAAAAAg4HDgKDIMwYIA4CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAADgBDAEDDAAAg4DDCBjAwwAAAAAAAAAAAIOBwcEgyDMGGAHBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAA4AQ8D/wwAAIOAwx/4wMM
+AAAAAAAAAAACBAcH+MIwjn5/x/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAOAEHwwAMAAH/wMMYAMDDAAAAAAAAAAAD4QfweC8LwfMP+HgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAADgBAeMADAABAcBiGADAwwAAAAAAAAAAAAAAAAAAAAAAGBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAA4AwB
+zAAwAAgDgPBgAwMMAAAAAAAAAAAAAAAAAAAAAADAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAHAIIM4CMAAIA4EAcBMDDAAAAAAAAAAAAAAAAAAAAAAAwEAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAABwGCDHBDAAEAHDADgjAwwAAAAAAAAAAAAAAAAAAAAAAOGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAPHAwh/gwABABw/4/wwMMgAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAA/ALwHg/AB8B/H/Dw/PhwAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAADAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAABgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAYCAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAHDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAABDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAfwPgAAAAAAADwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAgAAAAAAAAMAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIAAAAAAAADABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACAAAAAAAAAwAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAgAAAAAAAAMAcHAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIE8AAB0EcDBP7+B4CYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACPfgAAjPfwzwwMBjHvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAg4cAAYQ4MM
+MMDAwYcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIMDAAGAMDjDDAwIGGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACDAwAB4DAYwwwMH/hgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAABwAgwMf+PgwGMMMDBgAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIMDH/g8MBjDDAwYAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAGDAwAADjAYwwwMGABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAA4BAwMAAQYwMMMMDBwEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAwMDAAEGMDDDDAwOCGAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB44DAwABhDhgwwyMj/BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4D8+AAXg3g//HBwPB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACM
+--mail.sleepy.sau.158.532--
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: PostScript demo
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19503; Mon, 7 Oct 91 09:15:36 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA28214; Mon, 7 Oct 91 09:14:12 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA13347> for mrc@cac.washington.edu; Mon, 7 Oct 91 12:13:58 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA10867> for mrc@cac.washington.edu; Mon, 7 Oct 91 12:13:55 EDT
+Date: Mon, 7 Oct 91 12:13:55 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110071613.AA10867@greenbush.bellcore.com>
+To: mrc@cac.washington.edu
+Subject: An image that went gif->ppm->ps
+MIME-Version: 1.0
+Content-Type: application/postscript
+Content-Description: Captain Picard
+
+%!PS-Adobe-2.0 EPSF-2.)
+%%Creator: ppmtops
+%%Title: noname.ps
+%%Pages: 1
+%%BoundingBox: 147 304 454 496
+%%EndComments
+%%EndProlog
+%%Page 1 1
+/picstr 384 string def
+gsave
+147 304 translate
+1 1 scale
+307 192 scale
+320 200 8
+[ 320 0 0 -200 0 200 ]
+{ currentfile picstr readhexstring pop }
+false 3
+colorimage
+9148489148486d4848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d4848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d48486d48486d48486d48486d24246d48486d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d2424916d6db69191da9191dab6b6dab6b6da9191b69191914848
+6d48484824246d24244824246d24246d24246d24246d24246d24246d2424
+4824246d24244824244824244824246d24246d2424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+6d24246d24246d24244824244824244824246d24246d2424482424482424
+6d24244824246d24244824246d24246d24246d2424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824246d2424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48489148486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d4848914848914848916d6d914848
+9148489148486d48486d48486d48486d24246d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d2424914848b66d6db69191dab6b6da9191b69191b66d6d914848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824244824246d24246d24246d2424482424
+4824244824244824244824244824244824246d24246d24246d2424482424
+4824244824246d24246d24244824246d2424482424482424482424482424
+4824244824246d24246d24244824246d24246d24244824246d2424482424
+6d24246d24244824246d24246d24244824246d24244824244824246d2424
+4824244824244824246d24246d24244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d24246d24246d4848
+6d24246d48486d48486d48486d24246d48486d24246d48486d24246d4848
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d48489148489148489148486d48486d2424
+6d48486d48486d24246d24246d24246d48486d48486d48486d24246d2424
+6d24244824246d2424914848916d6db66d6db66d6db66d6d9148486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d24244824246d2424482424
+4824246d24244824244824244824246d24246d24246d24246d2424482424
+4824246d24244824244824246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824244824244824244824246d2424
+6d24246d24244824246d24244824246d24246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+9148489148489148489148489148489148489148489148486d48486d4848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148486d48489148489148486d48489148489148489148489148486d4848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d24246d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d4848914848
+916d6d916d6d916d6d914848916d6db69191b69191b69191b66d6db66d6d
+b66d6d9148489148486d48486d48486d48486d48486d48486d24246d4848
+6d48486d24246d24246d24246d48489148489148486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d2424482424
+4824244824246d24244824244824244824246d2424482424482424482424
+4824246d24246d24244824244824244824246d2424482424482424482424
+6d24244824246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24244824246d24244824244824246d2424482424482424
+6d24246d24244824246d24244824244824246d24246d2424482424482424
+6d24244824244824244824244824246d24246d24244824246d2424482424
+6d24244824244824246d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d48486d24246d24246d48486d4848
+6d48486d24246d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d
+b66d6db66d6d914848916d6d916d6db66d6db69191b66d6db66d6db69191
+b69191b66d6d916d6d916d6d916d6d9148489148486d24246d48486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824244824244824244824246d24244824246d2424
+6d24244824246d24246d24244824246d24244824246d24246d24246d2424
+4824246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24246d24246d24244824246d2424
+4824244824244824244824244824244824244824246d24246d2424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148486d4848914848914848914848914848914848
+9148489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d24246d2424
+6d24246d24246d24246d24246d48486d24246d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d48486d48486d48486d48486d48486d2424
+914848914848914848914848916d6db66d6dda9191b69191b66d6db66d6d
+9148489148486d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848914848b66d6db69191b69191b66d6d916d6d914848914848
+6d48489148486d48489148486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824246d24244824246d24246d24246d24246d2424
+6d24244824246d24244824244824246d24244824244824244824246d2424
+6d24246d24246d24244824244824246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d2424482424482424
+4824244824246d24246d24244824246d24244824246d24246d2424482424
+6d24244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848914848914848914848914848914848914848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d48486d48486d48486d24246d48486d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d24246d48486d24246d48486d48486d24246d24246d24246d48486d4848
+916d6db66d6db69191b66d6d914848916d6d916d6d6d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6d916d6d6d48486d48486d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24244824246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d2424482424482424482424482424482424
+6d24244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824246d2424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148486d4848914848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d4848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d2424914848914848916d6d
+b66d6dda9191b66d6d916d6d6d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d4848914848914848916d6d916d6d916d6d916d6d
+b69191b69191da9191da9191da9191b69191b66d6d9148489148486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24244824244824246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d24246d24246d2424
+6d24244824246d24246d24244824244824246d24244824244824246d2424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d4848914848914848b66d6d916d6d916d6d
+6d48486d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d4848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6db69191b69191da9191b66d6d916d6db66d6d
+9148489148486d48486d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+4824246d24246d24244824244824244824246d24246d24246d48486d4848
+6d24246d24246d24244824244824244824244824244824246d24246d2424
+4824244824244824244824244824244824246d24244824244824246d2424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+b66d6d914848916d6d916d6d914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+9148489148486d4848914848916d6d916d6db66d6d916d6d9148486d4848
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db69191da9191
+dab6b6dab6b6b69191916d6d9148486d24246d48486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24244824246d24246d24244824244824246d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824244824246d24244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+b69191914848916d6d916d6d916d6d914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48489148489148489148489148489148486d4848914848914848914848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148489148489148486d4848
+6d4848914848916d6d916d6db66d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48489148486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b69191dab6b6da9191b69191b691919148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d2424482424482424482424
+4824244824246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+da9191916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d4848914848914848914848914848914848
+916d6db66d6db69191b66d6d9148486d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d48486d4848
+6d4848914848914848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db69191dab6b6da9191b69191b66d6d9148486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d2424482424482424
+6d24244824244824244824246d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824246d24244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+da9191916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848916d6d914848914848914848916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848916d6d916d6d916d6d914848
+914848916d6d914848914848916d6d916d6d916d6d914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848914848914848916d6db66d6d
+b69191da91919148486d48484824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d48489148489148489148486d4848914848
+914848914848916d6d914848916d6d916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6dda9191dab6b6b69191916d6d
+6d24246d48484824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824246d24244824246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824246d24244824244824244824244824244824246d24246d2424
+4824244824244824244824246d24244824246d2424482424482424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffb6b6916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848916d6d916d6d914848
+914848916d6d916d6d914848916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48486d48486d48486d48489148486d4848914848914848
+6d48486d48486d48489148486d48486d48486d48486d4848914848914848
+914848914848914848914848914848914848914848b66d6db69191b69191
+b691919148486d24244824244824244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d48486d4848
+6d24246d48486d48486d48489148486d4848914848914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+b66d6d916d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6d916d6db66d6db69191dab6b6dab6b6
+b69191916d6d916d6d6d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24244824246d24244824246d24246d24246d24246d24246d2424
+4824246d24246d24246d24244824246d2424482424482424482424482424
+6d24244824244824244824246d24246d2424482424482424482424482424
+4824244824246d24246d24246d48486d48486d48486d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d2424482424482424482424482424
+6d24244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffdadab66d6db66d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848916d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48489148489148486d4848914848
+9148486d48486d48489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+9148486d48489148486d4848914848914848916d6db69191da9191b66d6d
+6d48486d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d4848914848914848914848914848916d6d916d6d916d6d
+914848914848916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6db69191
+dab6b6dab6b6b69191b66d6d6d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48489148486d4848
+6d48486d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24244824244824246d24244824244824246d24246d2424482424482424
+6d24244824244824244824244824244824244824246d24246d2424482424
+4824244824244824244824246d24244824244824244824246d24246d2424
+4824246d24246d4848914848916d6d916d6d9148486d48486d24246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdab66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48489148486d48486d4848914848
+9148489148486d48489148489148486d48486d48486d4848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848916d6db66d6db691919148486d4848
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6d916d6d916d6db66d6db66d6d
+da9191dab6b6ffb6b6dab6b6b66d6d916d6d6d48486d48486d24246d2424
+6d24246d24246d24246d24244824246d24246d4848916d6d916d6d916d6d
+9148486d24244824246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24244824244824246d2424482424
+4824246d24244824246d24246d24244824244824244824246d24246d2424
+6d24244824246d24244824246d24244824246d24246d2424482424482424
+4824246d2424914848914848914848916d6d916d6d9148486d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d24246d2424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+ffffdab66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48489148489148486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d4848914848914848b66d6db66d6d916d6d6d48486d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d24246d24246d24246d48486d48486d4848
+6d48486d4848914848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191dab6b6ffb6b6916d6d916d6d6d48486d2424
+6d24244824244824244824246d24246d2424914848914848914848914848
+9148486d48486d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d2424482424482424482424
+6d24246d24246d24246d24246d24246d24246d24244824246d2424482424
+4824244824246d24246d24246d2424482424482424482424482424482424
+6d24244824246d24246d24244824246d2424482424482424482424482424
+4824246d24246d48489148489148489148489148486d48486d2424482424
+4824244824244824244824246d24244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824246d2424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdab66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848b66d6db69191b69191916d6d6d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6db66d6d916d6d
+916d6d916d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6ddab6b6ffb6b6da9191da9191916d6d914848
+6d24246d24244824246d24244824246d24246d4848914848914848914848
+9148486d48484824244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24244824246d24246d24246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824246d24246d48489148489148489148489148486d48486d2424482424
+4824244824244824244824246d24244824246d2424482424482424482424
+4824244824246d24244824244824244824246d24244824244824246d2424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+ffffdab69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6d916d6d
+b66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848916d6d914848914848914848914848914848914848
+6d4848916d6db69191b69191916d6d6d48486d24246d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d24246d24246d48486d48486d2424
+6d4848914848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6d916d6d916d6d916d6dda9191dab6b6dab6b6dab6b6916d6d
+6d48486d24244824246d24244824246d24246d24249148489148486d4848
+6d48486d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d24246d2424482424482424
+4824244824246d24244824244824244824244824246d24244824246d2424
+4824246d24246d24246d24246d24244824246d24244824246d2424482424
+6d24246d24246d24246d24246d24246d48486d24246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824244824244824246d24246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d2424482424482424482424482424
+ffffdada9191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d916d6d916d6d914848914848914848916d6d916d6db66d6d
+916d6db66d6db69191916d6d6d48486d24246d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b66d6db66d6d916d6d916d6d916d6db69191ffb6b6ffdadadab6b6
+b66d6d9148486d48489148489148486d48486d24246d4848916d6d6d2424
+4824246d24244824244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24246d24246d24246d2424482424482424
+6d24244824246d24244824244824244824244824244824244824246d2424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24244824244824244824244824246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d2424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdada9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848914848914848914848914848916d6d916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6db66d6d916d6d914848914848914848b69191b69191da9191
+b69191b69191916d6d6d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+916d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6db66d6d916d6d916d6d914848b69191dab6b6ffb6b6
+ffdadada9191b66d6db66d6db69191b66d6d916d6d916d6d6d4848482424
+4824244800006d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824244824246d24246d24246d24246d24246d2424
+4824244824244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24244824244824246d24246d2424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824244824246d2424482424
+4824244824244824244824246d24244824246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+ffffdada9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d916d6d916d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848916d6d914848914848914848914848914848914848914848
+916d6db69191b66d6db66d6d916d6d916d6db69191dab6b6dab6b6ffb6b6
+ffb6b69148489148486d24249148486d24246d48486d48486d48486d4848
+6d48486d48486d48486d24246d24246d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6db69191b69191
+ffb6b6ffdadada9191b66d6db69191da9191da9191b66d6d482424482424
+4824246d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824244824246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24244824246d2424482424482424
+4824246d24244824244824246d24246d24246d24246d24246d2424482424
+4824244824244824246d24244824246d24246d24246d2424482424482424
+4824244824246d24244824246d24246d24246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824244824244824246d2424
+ffffdadab6b6dab6b6dab6b6dab6b6da9191b69191b69191b69191da9191
+da9191b69191b69191da9191b69191da9191da9191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191
+b69191b66d6db66d6db66d6db69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6db66d6d916d6d
+914848916d6d914848916d6d914848914848914848916d6d914848914848
+914848914848916d6db66d6d916d6d916d6db69191ffb6b6dab6b6dab6b6
+b66d6d6d48484824246d48486d24249148486d24246d48486d48486d4848
+6d24246d48486d24246d48486d24246d24246d48486d24246d48486d4848
+6d48486d48486d48486d24246d24246d24246d48486d48486d4848914848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6d916d6db66d6d916d6d914848914848
+da9191ffb6b6ffb6b6dab6b6dab6b6dab6b6b66d6d916d6d482424482424
+4824244824244824246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48489148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d2424482424
+4824246d24244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24249148489148489148484824244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdadab6b6dab6b6dab6b6dab6b6da9191da9191b69191b69191da9191
+da9191da9191b69191b69191b69191da9191da9191da9191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d916d6d914848914848916d6db66d6db66d6d916d6d
+916d6d914848914848914848914848916d6d916d6d916d6d916d6d916d6d
+914848914848916d6d914848916d6db69191da9191dab6b6dab6b6b69191
+6d48486d24246d48486d48486d48486d24246d48486d48486d48486d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d4848914848
+914848914848914848916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6ddab6b6ffb6b6ffdadaffb6b6ffb6b6da9191916d6d482424
+6d24244824246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824244824244824246d2424482424
+6d24246d4848914848916d6d916d6d916d6d6d48486d48484824246d2424
+4824246d24244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191dab6b6dab6b6b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191b66d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d914848916d6db66d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6db69191dab6b6ffb6b6b691916d4848
+6d24246d24249148486d24246d48486d48486d48486d48486d24246d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d48486d4848
+6d24246d24246d24246d24246d48486d48486d48486d48486d4848914848
+914848916d6d914848914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+914848914848916d6db69191ffb6b6ffdadaffdadaffdadadab6b6914848
+4824244800004824244824244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824246d24246d2424482424482424482424482424
+4824246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d2424482424482424
+6d24246d48489148489148489148489148489148486d48486d2424482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+ffffdaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191b69191b69191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d914848914848914848
+916d6d914848916d6db66d6db69191da9191ffb6b6da91916d4848914848
+4824249148486d24246d48486d24246d48486d48486d48486d24246d2424
+6d24246d48486d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d24246d48486d48486d4848914848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+916d6d914848914848916d6ddab6b6dab6b6ffdadaffdadaffb6b6b69191
+6d48486d24244824246d24244824246d24244824246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824246d24246d24246d4848916d6d916d6d916d6d9148486d2424
+6d24244824246d24244824246d24244824244824244824246d2424482424
+6d24246d24246d48489148489148489148489148486d4848482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdaffdadaffdadaffb6b6ffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191dab6b6da9191da9191b69191b69191b69191b69191da9191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848916d6d914848914848914848914848b66d6d
+b69191b66d6db69191b69191dab6b6dab6b6dab6b6916d6d4824246d2424
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+da9191da9191b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6d916d6d914848914848b66d6db69191dab6b6ffb6b6ffdadadab6b6
+b69191b66d6d9148486d24244824244824244824244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824246d24244824246d24244824246d2424482424482424
+6d24244824244824246d24244824246d2424482424482424482424482424
+4824244824246d2424914848b66d6dda9191b69191b69191b66d6d914848
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d24246d48489148489148489148489148486d2424482424482424
+4824244824244824244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdaffffdaffdadaffdadaffdadaffffdaffdadaffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191
+dab6b6dab6b6da9191da9191b69191b66d6db69191b69191b69191b69191
+da9191b69191da9191b69191da9191da9191da9191da9191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6db69191b66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d914848914848914848914848914848916d6d
+b66d6db69191dab6b6b69191ffb6b6da9191916d6d6d48486d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d48486d48486d48486d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b66d6db69191b66d6db69191b66d6db69191b69191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d914848914848b69191da9191ffb6b6ffb6b6
+dab6b6dab6b6da9191916d6d6d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24244824246d24244824246d2424
+6d24246d24246d24246d24244824246d24244824244824246d2424482424
+4824244824246d4848916d6db69191da9191dab6b6da9191b69191914848
+6d24244824244824246d24244824244824246d24244824244824246d2424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824246d24246d24244824244824244824244824244824246d2424
+ffdadaffffdaffffdaffdadaffffdaffffdaffdadaffb6b6ffb6b6ffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191b69191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+914848916d6d916d6d916d6d916d6d914848914848914848914848914848
+916d6db66d6dda9191dab6b6da9191b66d6d6d24246d24246d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d48486d24246d4848
+6d24246d24246d48486d48486d4848914848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6dda9191da9191
+da9191b69191b66d6d9148486d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24244824246d24246d2424482424
+4824244824246d4848b66d6dda9191da9191da9191da9191b66d6d914848
+6d24244824244824244824244824246d2424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d24244824244824244824246d2424482424482424482424482424
+6d24244824244824244824246d48486d48486d48486d2424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191da9191da9191da9191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d916d6db66d6d916d6d916d6d
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d914848914848b66d6d
+b69191da9191dab6b6dab6b6b66d6d9148486d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d24246d4848
+6d24246d48486d48486d4848914848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848b66d6ddab6b6
+ffb6b6da9191b691919148489148484824249148484824246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824244824244824246d2424482424
+4824244824246d2424914848b66d6db69191b69191b69191b66d6d6d4848
+6d24244824244824244824246d24244824246d2424482424482424482424
+6d24244824244824246d24246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824246d2424482424482424482424
+4824246d24246d24246d4848916d6d916d6d916d6d9148486d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffdadaffdadaffdadaffdadaffdadadab6b6ffb6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191b69191da9191da9191b69191b69191da9191
+b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848b66d6d
+dab6b6dab6b6ffdadab691919148484824249148486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b66d6db69191b66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d914848914848914848914848
+ffb6b6ffb6b6ffdadada9191916d6d6d24246d24244824246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24244824246d24246d24246d2424482424482424482424482424482424
+4824244824244824246d24246d4848916d6d916d6d9148486d48486d2424
+6d24246d24244824246d24244824246d24244824246d2424482424482424
+4824246d24246d24244824246d24246d2424482424482424482424482424
+6d24244824246d24244824244824246d24246d24244824246d2424482424
+6d24246d48486d4848914848914848914848916d6d9148486d24246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191dab6b6da9191
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6db69191da9191
+ffb6b6ffb6b6dab6b69148486d48486d48486d24249148486d2424914848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d914848914848
+b69191da9191ffdadaffb6b6b691916d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824244824244824244824246d24246d24244824246d2424482424
+4824244824244824246d24246d24244824246d24246d24246d2424482424
+6d24246d24244824244824244824246d24244824244824246d2424482424
+6d24246d24246d24244824244824244824244824246d2424482424482424
+4824246d4848914848914848914848916d6d9148489148486d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6db69191b66d6d916d6d916d6db66d6d916d6d
+916d6d914848916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848916d6dda9191dab6b6ffb6b6
+dab6b6ffb6b6da91919148486d24246d48486d48489148489148486d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191b69191b69191b69191
+b69191da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6db66d6dffb6b6ffb6b6916d6d4824244824244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24244824246d24246d24244824244824246d2424
+4824246d24244824244824244824244824244824246d2424482424482424
+4824246d24244824244824246d24246d24246d24244824244824246d2424
+4824246d24244824246d24244824246d24246d24244824246d2424482424
+6d24246d2424482424482424482424482424482424482424482424482424
+4824246d24249148489148489148489148489148486d48486d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffdada
+ffdadaffdadaffdadaffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191
+dab6b6da9191da9191da9191b69191b69191da9191b69191b69191da9191
+da9191b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db69191b66d6d916d6db66d6d916d6db66d6d
+916d6db66d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848b69191da9191ffb6b6ffb6b6
+ffffdada9191da91919148489148486d48489148486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d4848
+914848914848914848914848914848914848916d6d916d6d916d6d916d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191b69191da9191b69191b69191da9191
+da9191da9191da9191b69191b69191b66d6db66d6db69191b66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+914848916d6ddab6b6ffdadada91919148486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24244824244824246d24244824246d24244824246d2424482424
+6d24244824244824246d24244824246d24246d24246d2424482424482424
+4824246d24244824244824246d24244824244824246d24246d24246d2424
+6d24244824246d24246d48486d48486d48486d24246d24246d2424482424
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191da9191b69191b66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848da9191ffdadaffdada
+ffb6b6da9191b66d6d9148486d48489148486d48486d48486d48486d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d48486d48486d24246d48486d24246d48486d48486d4848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d
+914848914848b69191dab6b6ffdadab691919148486d24246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24244824244824246d24246d24244824244824246d2424482424
+4824244824246d24244824244824246d24246d24244824246d2424482424
+4824244824246d24244824244824246d24244824244824244824246d2424
+6d24246d2424482424482424482424482424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191
+b69191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db66d6db69191b66d6db66d6d916d6db66d6db66d6d916d6d
+b66d6d916d6d914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6d914848b66d6dffb6b6ffffdaffdada
+da9191b66d6d9148486d48486d24246d48486d24246d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d24246d24246d24246d48486d48486d48486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+da9191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d914848b66d6dda9191ffb6b6ffdadadab6b69148486d2424482424
+6d48484824246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d2424482424
+4824246d24246d24246d24246d24244824244824246d24246d2424482424
+4824244824246d24246d24246d24246d24244824244824244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6ffdadaffb6b6dab6b6dab6b6da9191
+dab6b6da9191dab6b6da9191da9191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6db66d6dda9191ffdadaffdadadab6b6
+da91919148489148486d24249148486d24246d48486d24246d48486d4848
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d48486d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b69191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+916d6d914848916d6db69191dab6b6ffdadaffdadab691919148486d2424
+4824244824246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24244824244824244824246d24246d24246d24246d2424
+6d24244824246d24246d24244824244824244824244824246d2424482424
+4824246d24244824244824246d24244824246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191
+da9191dab6b6b69191da9191da9191da9191da9191b69191b69191da9191
+da9191b69191da9191b69191b66d6db66d6db66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191dab6b6ffb6b6ffdadaffdadada9191
+b66d6d916d6d6d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d48486d48486d4848914848914848
+916d6d914848916d6d916d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6ffdadaffb6b6b66d6d6d2424
+4824246d48484824246d48484824246d48484824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24244824246d2424
+4824246d24246d24246d24244824246d24244824244824246d24246d2424
+4824244824246d24246d24246d24246d24244824246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6dab6b6ffb6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191b69191da9191da9191da9191da9191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6d916d6db66d6dda9191ffdadaffdadaffdadadab6b6b66d6d
+9148489148486d48486d48486d48486d48486d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d4848914848916d6d
+b66d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db69191dab6b6ffb6b6ffb6b6dab6b6916d6d914848
+4824246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d2424482424482424
+4824244824244824246d24246d24244824244824244824244824246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffffdaffdadaffb6b6ffdadaffb6b6
+ffb6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffdadadab6b6
+da9191da9191da9191b69191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b69191b66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6ddab6b6ffdadaffdadaffb6b6da9191916d6d
+916d6d6d48489148489148489148486d48486d48486d24246d48486d2424
+6d48486d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d4848914848914848916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6dffb6b6ffdadaffdadada91919148486d2424
+9148486d24246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d48486d24246d48486d2424
+6d24246d48486d48486d24246d48486d24246d24246d48486d24246d2424
+6d48486d24246d24246d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24246d24246d24246d24246d24244824244824246d24246d24246d2424
+6d24244824244824246d24246d24246d2424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6
+dab6b6da9191b69191da9191da9191b69191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d914848b66d6ddab6b6ffdadaffdadadab6b6b69191914848
+9148489148489148489148489148486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d4848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191da9191da9191b69191da9191da9191b69191b69191da9191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6ddab6b6ffb6b6ffdadadab6b6b66d6d914848
+9148486d48489148486d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d48486d24246d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824246d24244824244824246d24244824246d24246d2424
+6d24244824246d24246d24246d2424482424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6da9191b69191b69191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b66d6db66d6db69191b69191b69191b66d6d
+b66d6d916d6db69191b69191b66d6db66d6db69191b66d6db66d6db69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d914848b69191ffb6b6ffdadaffb6b6b69191b66d6d916d6d
+916d6d916d6d9148489148486d48489148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191b69191b69191
+da9191da9191b69191b69191da9191da9191b69191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db69191ffb6b6ffdadaffb6b6b69191914848
+914848916d6d6d48486d48486d24246d48486d24246d48486d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d24246d4848
+6d24246d48486d24246d24246d24246d24246d48486d24246d48486d4848
+6d24246d48486d48486d48486d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24244824244824246d2424
+4824246d24246d24246d24246d24246d24246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffdaffb6b6
+ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191b69191da9191da9191da9191b69191da9191
+da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d914848914848b69191ffb6b6ffdadadab6b6b69191916d6d916d6d
+914848916d6d9148489148486d48486d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d24246d48486d4848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+da9191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6dda9191ffb6b6ffdadab66d6d916d6d
+6d48489148486d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d48486d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824244824246d24244824244824246d2424
+4824246d24244824244824244824244824244824246d24246d2424482424
+ffffdaffffdaffffdaffffffffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffdaffdada
+ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191da9191dab6b6da9191da9191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6d916d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d916d6ddab6b6ffb6b6ffdadadab6b6b66d6db66d6d914848
+916d6d6d48489148486d48489148486d48486d48486d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d48486d2424
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6d916d6db69191ffdadaffdadada9191b66d6d
+9148486d48486d48486d24246d48486d24246d48486d24246d24246d4848
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d48486d24246d48486d24246d48486d4848
+6d48486d48486d48486d48486d24246d24246d48486d24246d48486d4848
+6d48486d48486d48486d48486d24246d48486d24246d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24244824244824246d24246d24244824244824246d2424
+6d24246d24246d24246d24244824244824244824244824246d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadadab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d916d6dda9191ffffdaffb6b6da9191b66d6d916d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d48486d24246d48486d48486d48486d48486d48486d4848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6d916d6db69191dab6b6ffb6b6ffb6b6b69191
+9148486d24246d24244824246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d48486d24246d24246d48486d24246d24246d2424
+6d48486d24246d24246d48486d48486d48486d48486d48486d24246d4848
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824244824244824244824246d2424
+6d24244824246d24246d24244824246d24246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffdadaffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191b69191da9191b69191b69191da9191
+b69191da9191b69191b69191b69191b66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6db69191ffb6b6ffb6b6ffb6b6b66d6db69191916d6d916d6d
+9148489148489148486d48486d48486d48486d48486d24246d48486d4848
+6d24246d48486d24246d24246d48486d24246d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191da9191da9191da9191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6db66d6db69191ffb6b6ffb6b6ffdadadab6b6
+916d6d4824246d48486d24246d48486d24246d48486d48486d48486d2424
+6d24246d24246d48486d48486d48486d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d48486d48486d24246d24246d24246d24246d24246d48486d24246d4848
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffdadaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6db66d6dda9191ffdadaffdadadab6b6b69191916d6db66d6d914848
+9148486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848916d6d914848914848916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191da9191
+da9191dab6b6da9191b69191b69191b66d6db69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db69191dab6b6ffdadaffb6b6b69191
+9148486d48486d24246d48486d24246d48486d24246d24246d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24244824246d24244824244824246d2424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6
+da9191dab6b6da9191da9191da9191da9191b69191b69191dab6b6da9191
+b69191b69191b69191da9191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b69191b66d6d916d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6ddab6b6ffdadaffdadab69191b69191b66d6db66d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848914848914848914848916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6dda9191ffb6b6ffffdaffb6b6b69191
+6d48486d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d48486d24246d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d24246d24246d48486d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d48486d24246d24246d24246d48486d24246d2424
+6d24246d48486d48486d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24244824244824246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6dda9191ffdadaffb6b6da9191b66d6db69191b66d6d916d6d
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d2424
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191da9191da9191
+da9191da9191dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191da9191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6dda9191dab6b6ffdadaffdadab66d6d
+916d6d6d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d24246d48486d48486d24246d48486d48486d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d48486d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d24246d24244824246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffb6b6ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6dab6b6
+da9191b69191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6ddab6b6ffdadaffb6b6b69191b69191b66d6db66d6d916d6d
+9148486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848914848914848916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191b69191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191b69191b69191da9191da9191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6d916d6db66d6ddab6b6ffdadaffdadada9191
+b691916d24249148486d24246d48486d24246d48486d48486d48486d4848
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d24246d24246d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d2424482424
+ffffdaffffdaffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadaffdadaffdadaffb6b6da9191dab6b6dab6b6da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+916d6db66d6dda9191ffdadadab6b6b69191b66d6db66d6db69191916d6d
+6d48486d48486d24246d24246d48486d24246d48486d24246d48486d2424
+6d24246d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191da9191da9191b69191b66d6db69191b66d6db69191b69191
+b69191da9191b69191b69191da9191da9191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6da9191dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191b69191da9191da9191
+da9191da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6dffb6b6ffb6b6ffb6b6dab6b6
+b66d6d9148486d24246d48486d24246d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d48486d48486d24246d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6da9191
+dab6b6da9191da9191dab6b6da9191b69191b69191b69191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d916d6d
+916d6db66d6ddab6b6ffb6b6ffb6b6b69191b69191b66d6db66d6d916d6d
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848914848914848914848916d6d916d6d916d6db66d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b66d6db69191b69191
+da9191da9191b69191da9191da9191da9191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6da9191dab6b6dab6b6dab6b6
+dab6b6da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6ddab6b6ffdadaffb6b6da9191
+9148489148486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffb6b6ffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6da9191
+da9191b69191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+916d6d916d6db69191ffb6b6dab6b6b69191b66d6db69191916d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+da9191dab6b6da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6ddab6b6ffdadaffb6b6914848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191da9191dab6b6da9191b69191b66d6dda9191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db69191da9191da9191b69191b69191b66d6db66d6db66d6d
+916d6d916d6d9148486d48486d48489148486d48486d48486d24246d4848
+6d48486d48486d24246d24246d24246d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d2424914848914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6da9191dab6b6dab6b6b69191b66d6db69191b69191b69191b69191
+da9191dab6b6dab6b6b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191dab6b6dab6b6da9191da9191b69191b66d6db66d6d
+b66d6d916d6d914848914848914848914848916d6d914848914848914848
+916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191da9191ffdadaffdadab69191
+9148484824249148486d24246d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6db69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6ddab6b6da9191da9191b69191b69191b66d6db66d6d
+916d6d9148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d4848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6dab6b6dab6b6ffb6b6da9191b66d6db69191b69191b69191da9191
+dab6b6dab6b6dab6b6b69191b69191b69191b69191da9191da9191da9191
+dab6b6da9191da9191da9191b69191b69191b69191916d6d916d6d916d6d
+9148489148489148489148489148489148489148489148486d4848914848
+916d6db66d6db66d6db66d6db69191b69191b69191da9191b69191b69191
+b69191b69191b66d6db66d6db69191b69191da9191ffdadaffdadadab6b6
+916d6d9148486d24249148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6da9191
+b69191dab6b6dab6b6da9191dab6b6da9191b69191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191dab6b6ffb6b6dab6b6da9191da9191b69191b69191b66d6d
+916d6d916d6db66d6d9148489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6ffb6b6ffb6b6ffb6b6da9191b69191b66d6db69191b66d6dda9191
+dab6b6dab6b6dab6b6b69191b66d6db69191b69191da9191da9191da9191
+da9191b69191b66d6db66d6d916d6d916d6d916d6d9148489148486d4848
+6d48486d4848914848914848914848914848916d6d916d6d914848914848
+914848916d6d916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6dda9191da9191ffdadaffb6b6ffb6b6
+b69191916d6d9148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6dda9191ffb6b6ffb6b6dab6b6b69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6d916d6d916d6d914848914848914848914848
+9148486d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d24246d48486d4848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191da9191da9191
+dab6b6ffb6b6ffb6b6dab6b6da9191b66d6db66d6db69191b69191dab6b6
+ffb6b6ffb6b6dab6b6da9191b66d6db69191b69191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6d916d6d916d6d914848914848
+914848914848916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db66d6db69191da9191ffb6b6ffb6b6ffb6b6da9191
+da9191916d6d916d6d6d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d24246d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824246d24244824246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191da9191b69191b66d6db69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db69191ffb6b6ffb6b6da9191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b69191b69191916d6d914848914848916d6d
+9148486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d24246d48486d48486d48486d4848914848914848914848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+da9191dab6b6ffb6b6dab6b6da9191b66d6d916d6db66d6db69191da9191
+dab6b6dab6b6dab6b6b69191b69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d914848
+914848916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d914848916d6d916d6d916d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db69191da9191ffb6b6ffdadaffb6b6b66d6d
+916d6d6d48486d48486d48489148486d48489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24244824246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b69191b69191b69191b69191
+b69191da9191dab6b6da9191b69191b66d6db66d6db69191b69191b69191
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db69191ffdadaffdadadab6b6b69191da9191b69191b69191b69191
+b66d6d916d6db66d6db69191da9191b69191b66d6d916d6d914848916d6d
+916d6d6d48489148489148486d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d4848914848914848914848916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191dab6b6dab6b6dab6b6da9191916d6db66d6db66d6db69191dab6b6
+dab6b6dab6b6da9191da9191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6d916d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191da9191
+b69191b66d6db66d6db66d6d916d6db66d6d916d6db66d6db69191b66d6d
+b69191b69191b69191b66d6db66d6dda9191ffb6b6ffdadaffb6b6b69191
+9148486d48486d48489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d24246d24246d24246d48486d48486d48486d4848
+6d24246d48486d48486d48486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6dab6b6da9191dab6b6
+ffb6b6dab6b6da9191b69191b69191da9191da9191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffdadaffb6b6da9191da9191b66d6db69191b69191
+b66d6db66d6db66d6dda9191dab6b6da9191da9191b66d6db66d6d916d6d
+916d6d914848916d6d9148489148486d24246d24246d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191da9191dab6b6b69191b66d6db66d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191da9191b69191da9191
+da9191b66d6db66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6db66d6ddab6b6ffdadaffdadadab6b6b66d6d
+916d6db69191b69191b69191916d6d9148489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48489148489148489148486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d24246d48486d48486d24246d48486d2424
+6d24246d48486d48486d24246d24246d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffb6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191b69191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b66d6db69191b66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffffdaffdadaffb6b6dab6b6da9191b69191b66d6db69191
+b66d6d916d6d916d6dda9191ffb6b6ffb6b6ffb6b6da9191b69191916d6d
+914848916d6d916d6db66d6d9148486d48486d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d48486d24246d48486d24246d24246d48486d4848
+6d4848914848914848914848916d6d916d6db66d6db69191b69191b69191
+b69191b69191da9191da9191b69191b66d6d916d6db66d6db66d6dda9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db69191b66d6db69191b66d6db69191b69191
+b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6db69191dab6b6ffdadaffdadaffb6b6da9191
+da9191da9191dab6b6ffb6b6b691919148486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848916d6d9148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d24246d24246d48486d48486d2424
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+da9191dab6b6dab6b6da9191da9191da9191dab6b6da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffffdaffdadaffb6b6da9191da9191b69191b69191
+b66d6d916d6d914848b66d6ddab6b6ffb6b6ffdadaffb6b6dab6b6b69191
+916d6db66d6d916d6db66d6d9148489148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d24246d24246d2424
+6d24246d48486d4848914848914848916d6d916d6db66d6db69191b66d6d
+b69191b69191b69191b69191b69191b66d6d916d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191da9191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db66d6db69191ffb6b6ffb6b6ffb6b6ffb6b6b66d6d
+914848914848916d6ddab6b6dab6b6b66d6d6d48486d48486d24246d4848
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffffffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffffdaffdadaffdadadab6b6dab6b6b69191b69191b66d6d
+b69191b66d6d914848914848b66d6dda9191ffb6b6ffb6b6dab6b6dab6b6
+b66d6db69191b66d6db66d6d9148486d48486d48489148486d48486d4848
+6d48486d24246d24246d48486d24246d24246d24246d24246d48486d4848
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d4848914848916d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6dda9191
+dab6b6dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+b69191da9191b69191b69191b69191b69191da9191da9191b69191b69191
+b69191b69191b66d6db66d6dda9191ffb6b6ffdadada9191b66d6d916d6d
+914848916d6d916d6ddab6b6ffb6b6b69191916d6d6d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848916d6d916d6d916d6d916d6d9148486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6dab6b6ffdadaffb6b6
+dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffdadaffffdadab6b6dab6b6b69191b69191b69191
+b69191b66d6d916d6d914848914848b69191da9191dab6b6ffb6b6ffdada
+b69191b69191b66d6db66d6d9148486d48486d48489148486d48486d4848
+6d24246d24246d48486d48486d24246d48486d24246d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d4848914848916d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d916d6d916d6db66d6db69191
+da9191dab6b6dab6b6da9191da9191b69191b69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6dda9191dab6b6dab6b6b66d6d916d6d916d6d
+b66d6db66d6d916d6db69191ffb6b6b691916d48486d48489148486d4848
+9148486d24249148486d48489148486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848916d6d916d6d9148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6dda9191ffffdaffffdaffdadaffb6b6da9191dab6b6b69191b69191
+b69191b69191b66d6d914848916d6db66d6db69191da9191da9191da9191
+da9191b69191b69191916d6d9148486d48489148489148486d48486d4848
+6d48486d48486d48486d24246d48486d24246d48486d4848914848914848
+914848916d6d9148489148489148489148489148486d48486d48486d2424
+6d48486d48486d48486d48486d24246d24246d24246d4848914848916d6d
+916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db69191
+da9191dab6b6dab6b6da9191dab6b6b69191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b66d6d9148486d48486d4848
+6d24246d24246d24246d48486d48486d4848916d6d916d6db66d6d916d6d
+916d6d916d6db66d6db69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db69191ffb6b6b691919148486d4848914848914848
+6d48486d48486d48489148489148489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6ffb6b6dab6b6da9191da9191dab6b6dab6b6da9191dab6b6da9191
+da9191b69191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b69191b69191b66d6db69191
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffdadaffffdaffdadaffb6b6dab6b6da9191da9191b69191
+b69191b69191b69191b66d6d916d6d916d6db66d6db66d6db69191b69191
+b69191b69191b69191916d6d9148489148486d48486d48486d48486d4848
+9148486d48486d48486d48486d4848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d9148489148486d48486d24246d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6d916d6d6d48486d48486d4848
+6d24246d2424914848b66d6db66d6db66d6db66d6dffb6b6b69191b66d6d
+916d6d914848916d6d914848916d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db66d6db69191b69191b69191b69191b69191
+b69191b66d6db66d6ddab6b6ffb6b6916d6d482424916d6d6d2424916d6d
+6d24249148486d48489148486d48489148486d48486d48486d4848914848
+6d48489148486d48489148489148486d48489148486d48486d48486d4848
+6d48489148489148486d48486d4848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdada
+ffdadaffffdaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191da9191b69191b69191b66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b69191da9191ffdadaffdadaffffdaffb6b6dab6b6da9191da9191b69191
+b69191b69191b69191b69191916d6d914848916d6db69191b69191b69191
+b69191b69191b69191b66d6d916d6d6d48489148486d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6d9148486d48486d48486d24246d2424
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6db66d6d
+b69191da9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191da9191b69191b69191b66d6db66d6d914848914848914848
+6d48486d2424914848b69191916d6d916d6dffb6b6ffb6b6ffdadab69191
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191da9191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db69191dab6b6dab6b6916d6d9148486d24249148486d4848
+9148486d48489148486d48489148486d48486d48486d4848914848914848
+9148486d48486d48489148486d48489148486d48486d48486d4848914848
+9148489148489148489148486d4848914848914848914848914848914848
+9148489148486d48486d48489148489148486d48489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191da9191b69191b66d6d
+b66d6db66d6db69191b69191b66d6db66d6db69191b66d6db66d6db66d6d
+b69191da9191ffb6b6ffdadaffffdaffdadadab6b6da9191dab6b6da9191
+b69191b69191b69191b69191b66d6d916d6db69191b69191b69191b69191
+b66d6db69191b69191b66d6d916d6d6d48486d48486d48486d4848914848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d2424
+6d24246d24246d48486d24246d24246d4848914848916d6d916d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d914848914848914848
+9148486d48486d48486d24246d2424916d6dffb6b6ffb6b6da9191b69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b66d6db66d6db66d6db69191da9191b69191b69191
+b69191b69191b69191ffb6b6da91919148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+9148489148489148489148486d48486d48489148486d48486d48486d4848
+6d48489148489148489148486d48486d4848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffffdaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffdada
+ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6dda9191ffdadaffffdaffdadaffb6b6dab6b6dab6b6dab6b6
+da9191b69191b66d6db66d6db69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b66d6d916d6d6d48486d48486d48486d4848914848
+6d48486d48489148489148486d4848914848914848916d6d916d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6d6d48486d48486d2424
+6d24246d24246d24246d24246d24246d2424914848916d6d916d6d916d6d
+b66d6db69191b69191b69191da9191da9191da9191da9191b69191da9191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6d916d6d916d6d
+9148489148489148486d4848914848b66d6dda9191da9191da9191da9191
+da9191da9191da9191da9191dab6b6dab6b6da9191b69191da9191da9191
+da9191da9191b69191b69191b66d6db66d6db69191b69191b69191b69191
+b69191b69191dab6b6ffb6b6b69191916d6d9148486d4848914848914848
+6d48489148486d48489148489148486d48486d4848914848914848914848
+9148486d48489148489148486d48486d48486d4848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48486d48489148486d48486d48486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d24246d48486d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d24246d24246d48486d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffffdaffffdaffdadaffb6b6dab6b6ffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+916d6db66d6dda9191ffdadaffdadaffffdaffdadaffb6b6dab6b6dab6b6
+da9191da9191b66d6db66d6db69191b66d6db66d6db66d6db69191b69191
+b69191b69191b69191b66d6d9148489148486d48486d48486d4848914848
+9148486d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d914848914848914848916d6d916d6db66d6db66d6d
+b66d6d914848916d6d914848916d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+da9191da9191b69191b69191b69191916d6d916d6db66d6d916d6d916d6d
+916d6d914848914848916d6db66d6db69191b69191da9191da9191dab6b6
+da9191b69191b69191b69191da9191da9191da9191b69191b69191b69191
+da9191b69191b69191b66d6d916d6db66d6db66d6db69191b69191da9191
+b69191b69191dab6b6ffb6b6da9191916d6d6d4848916d6d914848914848
+6d48489148486d48489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48489148486d48486d48486d4848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d4848
+6d24246d24246d48486d24246d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffffdaffffdaffdadaffffdaffb6b6ffb6b6ffdadaffb6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191da9191da9191da9191da9191
+b69191b69191b69191da9191da9191dab6b6da9191b69191b66d6db69191
+b66d6db66d6dda9191ffdadaffffdaffffdaffdadaffb6b6dab6b6da9191
+dab6b6da9191b69191b66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191b69191da9191b69191b66d6db66d6d9148486d4848914848914848
+9148486d48486d48486d48486d4848916d6d916d6d916d6d914848916d6d
+9148486d48486d48486d48486d24246d48486d4848914848916d6db66d6d
+916d6d916d6d9148486d48489148486d48486d48486d24246d24246d4848
+6d48486d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6da9191b69191b69191b69191916d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db69191b69191da9191dab6b6dab6b6dab6b6dab6b6
+da9191b69191b69191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191da9191b69191
+b69191b69191dab6b6ffb6b6ffb6b6916d6d916d6d6d48489148486d4848
+9148489148489148486d48486d48486d48489148486d4848914848914848
+9148489148486d48486d48489148489148489148489148489148486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48489148486d48489148486d48489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d24246d48486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d48486d48486d24246d48486d24246d2424
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6dab6b6
+ffb6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191dab6b6
+da9191da9191b69191b69191b69191b69191b69191da9191b69191da9191
+b69191b69191b69191b69191b69191da9191da9191dab6b6da9191da9191
+b66d6db66d6db69191da9191dab6b6dab6b6dab6b6b69191b66d6db66d6d
+b66d6db66d6dda9191ffb6b6ffffdaffffdaffffdaffb6b6ffb6b6da9191
+da9191da9191b69191b66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191916d6d916d6d6d48486d48486d4848914848
+6d48486d48486d48486d48486d4848916d6d9148489148486d48486d4848
+6d48486d48486d48486d24246d4848916d6db66d6db66d6db69191b66d6d
+b66d6d9148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848b66d6db66d6d
+b66d6db69191b66d6db69191b66d6db69191b69191b69191b69191b69191
+da9191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6db66d6db66d6db69191b69191da9191da9191b69191da9191da9191
+da9191b69191da9191da9191b69191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191da9191
+b66d6db66d6dda9191ffb6b6ffb6b6b66d6d914848914848914848914848
+9148486d48489148489148489148486d48489148486d4848914848914848
+9148489148486d48486d48486d48486d48486d48489148486d4848914848
+9148489148489148489148489148489148486d4848914848914848914848
+6d48489148486d48489148486d48486d48486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d24246d2424
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d2424
+6d48486d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191dab6b6dab6b6dab6b6
+da9191b69191b69191b66d6db69191b69191da9191da9191da9191b69191
+b69191b66d6db69191b69191dab6b6da9191da9191b69191b66d6db66d6d
+b66d6db69191da9191ffb6b6ffdadaffffdaffdadaffdadaffb6b6da9191
+b66d6db66d6db66d6db66d6d916d6db66d6db69191b66d6db66d6db66d6d
+b69191b69191b69191b66d6d9148489148486d4848914848914848914848
+6d48486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48484824246d2424b69191b69191914848da9191ffb6b6
+b66d6d6d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848916d6d916d6db66d6d
+b66d6db69191b69191b69191b69191da9191b69191b69191b69191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191da9191da9191da9191dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191
+da9191b69191b69191b69191b66d6db66d6db66d6db66d6db69191da9191
+b69191b66d6db69191da9191dab6b6b66d6d916d6d914848914848916d6d
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d4848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148486d48486d48489148489148486d48486d4848914848914848914848
+914848916d6d9148489148489148489148489148489148486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191b69191da9191da9191dab6b6dab6b6ffb6b6ffb6b6
+dab6b6da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191da9191da9191b69191b69191b66d6db69191
+b66d6db66d6db66d6dda9191ffb6b6ffdadaffffdaffdadaffb6b6b66d6d
+914848914848914848916d6d916d6db69191b69191b69191b66d6db66d6d
+b66d6db69191b69191916d6d9148486d48486d48489148486d48486d4848
+9148489148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d4848916d6ddab6b6ffb6b6
+b691916d24246d24244824246d48486d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db69191b69191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dda9191
+b69191b69191b66d6dda9191ffb6b6b691919148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148486d4848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48486d48489148486d48486d48489148486d48486d4848914848914848
+9148489148489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d24246d48486d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6ffb6b6ffb6b6ffdadaffffdaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191b69191b69191b69191da9191dab6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6db69191
+b69191b69191916d6db66d6ddab6b6ffdadaffdadaffffdaffb6b6b69191
+6d48486d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b69191b69191b69191916d6d9148486d48486d48486d48486d48486d4848
+916d6db66d6d916d6d6d48486d4848914848916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d4848b66d6dda9191b69191
+916d6d6d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6d916d6d
+b66d6db66d6db69191b66d6db69191b69191b69191da9191da9191b69191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191dab6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6
+dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191da9191b69191
+b69191b69191b66d6db69191b69191b66d6db66d6d916d6db69191da9191
+dab6b6da9191b69191da9191dab6b6dab6b6916d6d914848914848914848
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48489148486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191b69191b69191b69191b69191dab6b6dab6b6ffb6b6ffb6b6
+dab6b6da9191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db69191
+da9191da9191da9191b69191dab6b6ffb6b6ffdadaffdadaffdadadab6b6
+9148486d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b69191b69191b691919148489148486d48486d48486d48486d4848916d6d
+b66d6d916d6d916d6d6d48486d24249148486d4848b66d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d916d6db66d6d916d6d916d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b66d6db69191b69191b69191da9191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191dab6b6b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b66d6d916d6db66d6db69191
+dab6b6dab6b6da9191b69191dab6b6dab6b6916d6d6d4848916d6d6d4848
+9148486d4848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148486d48489148486d4848914848
+6d48489148489148489148489148489148489148489148486d4848914848
+9148489148489148489148486d48489148486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191b69191da9191da9191dab6b6dab6b6
+dab6b6b69191b69191b66d6db69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db69191dab6b6
+ffdadaffb6b6da9191b66d6db66d6dda9191ffb6b6ffdadaffffdaffb6b6
+916d6d6d48486d48486d4848b66d6db69191b69191b69191b66d6d916d6d
+b69191da9191da9191916d6d6d48486d48486d48486d48486d4848916d6d
+b66d6db66d6d9148486d48486d24246d48486d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d6d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848b66d6d
+b66d6db66d6db66d6db69191b66d6db69191b69191da9191da9191da9191
+da9191b69191b69191da9191dab6b6da9191da9191da9191da9191da9191
+da9191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+da9191dab6b6dab6b6da9191dab6b6da9191916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d24246d48486d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffdadaffb6b6ffb6b6ffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191b69191b69191b66d6db69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6dda9191ffdada
+ffdadab691916d48486d24244824246d2424b69191ffb6b6ffffdaffb6b6
+916d6d6d48486d48486d2424916d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b66d6d9148486d48486d48486d48486d4848914848914848
+916d6d916d6d9148486d48486d48486d24246d4848914848916d6d916d6d
+916d6d916d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191b69191da9191da9191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b69191da9191dab6b6dab6b6dab6b6da9191b66d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d916d6db66d6db66d6db66d6d916d6d9148486d4848914848914848
+9148489148486d48489148489148489148489148489148486d4848914848
+9148489148489148489148486d48486d48489148489148486d48486d4848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d24246d24246d48486d4848
+6d48486d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffda
+ffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6ddab6b6ffdada
+ffb6b6b66d6d6d48486d48484824246d24246d2424916d6d916d6d916d6d
+6d48486d48486d2424914848914848b69191b69191b69191b66d6db69191
+b66d6db69191916d6d6d48486d48486d48486d4848914848916d6d914848
+916d6d916d6d916d6d9148489148486d48486d4848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+9148486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d2424914848916d6d
+916d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+916d6db69191da9191dab6b6dab6b6da9191b66d6d916d6d6d4848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db69191dab6b6dab6b6da9191da9191916d6d6d4848914848b66d6d
+b66d6db66d6d914848914848914848914848914848914848914848914848
+9148489148486d48489148486d48489148489148489148486d48486d4848
+9148489148489148489148489148486d48489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d24246d48486d48486d48486d24246d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffda
+ffffdaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db69191b66d6db69191b66d6db66d6d916d6db66d6dffb6b6ffdada
+ffb6b6b69191916d6d9148486d48486d48486d24246d48486d24246d2424
+6d24246d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b66d6db66d6d916d6d9148486d48489148486d48489148486d4848914848
+914848914848914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+b66d6db66d6d916d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db69191da9191dab6b6b69191914848916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+b66d6dda9191dab6b6dab6b6dab6b6dab6b6b69191914848b69191b69191
+dab6b6da9191b66d6d916d6d9148486d4848914848914848914848914848
+9148486d48489148489148489148489148489148486d48486d48486d4848
+9148489148486d48489148489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d48486d2424
+6d24246d48486d48486d24246d48486d24246d48486d48486d24246d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6d916d6db66d6ddab6b6ffffda
+ffb6b6b66d6db66d6d916d6db66d6d9148486d48486d48486d48486d4848
+6d48486d4848914848b66d6db66d6db69191b69191b69191b66d6db66d6d
+b69191b66d6d916d6d9148486d48489148486d48486d48486d48486d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d9148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191da9191da9191dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191b69191b69191b69191da9191da9191
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191
+b66d6d916d6d916d6db69191da9191dab6b6da9191916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+916d6dda9191ffb6b6dab6b6dab6b6dab6b6da9191b66d6ddab6b6ffb6b6
+ffb6b6ffb6b6da9191916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d48486d24246d48486d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6ddab6b6ffb6b6
+ffdadab66d6d916d6d916d6db66d6d916d6d9148486d48486d48486d4848
+6d48486d4848914848916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d9148486d48486d48486d48486d48486d48486d48486d4848
+9148486d4848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db69191b66d6d
+916d6d6d48486d48486d24246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191da9191dab6b6
+dab6b6dab6b6dab6b6da9191da9191b69191da9191da9191b69191da9191
+da9191da9191da9191da9191dab6b6da9191da9191da9191da9191da9191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+916d6d916d6d914848b66d6db69191da9191da9191916d6d916d6d914848
+914848916d6d9148489148489148489148489148489148486d4848914848
+916d6db69191dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6
+dab6b6dab6b6b69191916d6d9148486d48486d48489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d48486d48486d48486d24246d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6ffdadaffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191da9191b69191da9191
+b69191b69191b66d6db69191b69191da9191da9191b69191da9191b69191
+b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6dda9191ffdada
+ffdadab66d6d914848916d6d916d6db66d6d916d6d916d6d9148486d4848
+6d48486d24246d4848914848b66d6db66d6db66d6db69191b66d6db69191
+b66d6d9148486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+916d6d9148486d48486d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424914848
+916d6d916d6db66d6db66d6db69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191da9191dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db69191da9191dab6b6da9191b66d6d914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db66d6db69191da9191dab6b6dab6b6ffb6b6dab6b6ffb6b6
+dab6b6dab6b6b69191914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+6d48486d48489148486d48489148486d48489148486d48486d48486d4848
+6d48489148489148486d48486d48486d48486d48489148486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d24246d48486d48486d48486d24246d48486d48486d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadadab6b6dab6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6b69191da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191da9191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db69191ffb6b6
+ffdadab69191914848916d6d914848916d6d916d6db66d6d916d6d6d4848
+6d48486d24246d2424914848916d6db66d6db66d6db66d6db66d6db69191
+b66d6d9148486d48486d48486d24246d48486d24246d48486d24246d4848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+9148486d48486d24246d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+914848916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b69191
+b69191da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+914848914848916d6d916d6db69191da9191b69191916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d4848916d6dda9191ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6b69191b66d6d9148486d4848914848914848914848914848914848
+9148489148489148489148489148486d4848914848914848914848914848
+9148489148489148489148486d48486d48486d48489148489148486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d48486d48486d2424
+6d24246d24246d48486d48486d48486d48486d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191dab6b6
+da9191da9191b69191da9191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffdadada9191916d6d914848b66d6d914848b66d6db66d6db66d6d914848
+9148486d48486d48486d4848916d6d916d6db69191b66d6db66d6db66d6d
+b66d6d9148486d48486d48486d48486d48486d48486d24246d48486d4848
+916d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d914848
+6d48486d24246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191da9191b69191da9191da9191b69191b69191b69191
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6db69191da9191da9191916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+9148486d4848914848b66d6ddab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191916d6d9148489148486d4848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148489148486d48486d48486d48489148486d4848
+6d48486d48489148489148489148486d48486d48486d48489148486d4848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d24246d24246d48486d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6da9191b69191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffdadada9191b66d6d916d6d916d6d914848916d6d916d6d916d6d914848
+6d48486d48486d48486d4848916d6db66d6db66d6db66d6db66d6db66d6d
+916d6d9148486d48486d24246d24246d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d6d4848
+6d48486d24246d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+914848916d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191da9191b69191b69191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6db66d6db66d6db69191dab6b6da9191914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+9148486d48486d4848b66d6dda9191dab6b6ffb6b6ffb6b6ffb6b6da9191
+b66d6d9148489148486d4848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d24246d24246d48486d24246d24246d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6dab6b6
+dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191b69191da9191b69191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffffdada9191b66d6d914848916d6d916d6d916d6db66d6d916d6d914848
+6d48486d48486d24246d4848914848b66d6db66d6db66d6db66d6db66d6d
+9148486d48486d24246d48486d24246d48486d48486d48486d4848916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d9148486d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+914848916d6d916d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191da9191da9191
+da9191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191da9191dab6b6da9191b66d6d914848916d6d
+914848914848914848914848914848914848914848914848916d6d914848
+9148486d48486d4848914848b66d6dda9191dab6b6da9191da9191b66d6d
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48489148486d48489148486d48486d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d24246d24246d24246d48486d48486d48486d48486d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6dffb6b6
+ffdadadab6b6916d6db66d6d914848916d6d914848916d6d916d6d914848
+6d48486d48486d48486d4848914848b66d6db66d6d916d6d916d6db66d6d
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6d916d6d916d6d916d6d9148486d48486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6db66d6db69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db69191da9191
+da9191da9191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6dffb6b6b69191914848914848914848
+914848914848914848914848914848914848914848916d6db66d6d916d6d
+9148489148486d48486d4848914848916d6d916d6db66d6d916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848914848914848914848914848914848
+9148486d48489148486d48489148486d4848914848914848914848914848
+6d48489148489148489148486d48486d48489148486d48486d48486d4848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d24246d24246d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d48486d4848
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6
+dab6b6dab6b6dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6ddab6b6
+ffdadadab6b6b66d6d914848916d6d914848916d6d916d6d916d6d914848
+6d48486d24246d48486d4848916d6db69191b66d6d916d6db66d6d916d6d
+916d6d6d48486d48486d48486d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6d916d6db66d6db66d6db69191b69191b69191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191da9191
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db66d6db69191dab6b6da9191916d6d916d6d6d4848
+916d6d914848916d6d914848916d6d914848b66d6db66d6db69191b66d6d
+916d6d916d6d9148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+6d48486d48486d48486d48486d48486d48489148489148486d48486d4848
+9148489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d24246d48486d4848
+6d48486d48486d48486d24246d24246d48486d24246d24246d24246d4848
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6dda9191
+ffb6b6dab6b6b66d6d916d6d914848914848914848914848916d6d914848
+6d48486d48486d24246d2424916d6d916d6db66d6db66d6db66d6db66d6d
+916d6d6d48486d48486d48486d24246d48486d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d6d48486d48486d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d4848914848916d6db66d6db66d6db69191b69191b69191b69191da9191
+da9191da9191da9191b69191b69191b66d6db69191b66d6db66d6db66d6d
+b69191da9191dab6b6dab6b6dab6b6da9191da9191b69191b69191b69191
+da9191da9191b69191da9191da9191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6d916d6db69191ffb6b6b66d6d914848914848914848
+914848916d6d914848914848914848916d6db66d6db66d6db66d6db66d6d
+b66d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148489148486d4848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48489148489148489148486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d48486d48486d24246d48486d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffdadaffffdaffdadaffdadaffb6b6dab6b6ffdadadab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db69191
+ffb6b6ffb6b6b69191914848916d6d916d6d914848916d6d916d6d916d6d
+9148489148486d48486d2424914848b66d6db66d6db69191b66d6db66d6d
+b66d6d916d6d9148486d48486d24246d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d6d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d2424
+6d2424914848916d6d916d6db66d6db69191b69191b69191da9191dab6b6
+ffb6b6ffb6b6dab6b6da9191b69191b69191b69191b69191da9191b69191
+b69191b69191da9191ffb6b6dab6b6dab6b6da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6ddab6b6ffb6b6b66d6d916d6d6d4848916d6d
+914848914848914848914848914848914848916d6db69191b69191b66d6d
+b66d6d914848914848914848914848914848916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148489148489148486d48489148489148486d48486d48486d4848
+6d48486d48489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d24246d24246d24246d48486d48486d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadadab6b6ffb6b6dab6b6
+da9191dab6b6dab6b6dab6b6dab6b6da9191b69191b69191da9191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+ffb6b6ffb6b6b69191916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d9148486d48486d48486d4848da9191b69191da9191b66d6db69191
+b66d6d916d6d6d48486d48484824246d48486d24246d48486d4848914848
+914848916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d4848
+6d48486d24246d48486d24246d24246d24246d48486d48486d24246d2424
+6d4848914848916d6d916d6db66d6db66d6db69191da9191ffb6b6ffb6b6
+ffdadaffb6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191da9191dab6b6da9191dab6b6b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6d916d6ddab6b6ffb6b6b66d6d6d48489148486d4848
+9148489148489148486d4848914848914848916d6db66d6db66d6db66d6d
+916d6d914848914848914848916d6db66d6db69191b69191b66d6d914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6d916d6d916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148489148489148489148489148489148489148486d48486d4848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+ffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffda
+ffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db69191b69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6d916d6db66d6d
+dab6b6ffdadada9191b66d6d914848916d6d914848916d6d916d6d916d6d
+916d6d9148486d48486d2424914848b69191ffb6b6da9191da9191b66d6d
+b691919148486d48486d24246d48486d24246d48486d24246d48486d4848
+914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+914848914848916d6d916d6db66d6db66d6dda9191dab6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+da9191b69191b66d6db69191b69191dab6b6dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db69191ffb6b6ffb6b6da9191914848914848914848
+916d6d916d6d916d6d914848916d6d914848916d6d916d6d914848916d6d
+914848914848914848916d6d916d6dda9191dab6b6ffb6b6da9191b66d6d
+914848914848914848914848914848914848914848914848914848916d6d
+b69191b69191b69191b66d6db66d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d48489148486d4848914848914848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6ffb6b6ffdadaffdadaffdadaffdadaffdadadab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+dab6b6ffdadadab6b6b66d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d9148486d48484824246d2424b66d6ddab6b6dab6b6da9191b69191
+916d6d9148486d48486d24246d24246d48486d48486d48486d24246d4848
+914848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6d916d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d24246d24246d48486d24246d24246d4848
+6d4848914848916d6d916d6d916d6db66d6db69191dab6b6da9191dab6b6
+da9191da9191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+da9191da9191b69191b66d6db66d6db66d6db69191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b66d6db69191ffb6b6dab6b6b66d6d916d6d916d6d914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848916d6db66d6ddab6b6ffb6b6ffb6b6dab6b6b69191
+916d6d6d48489148486d4848914848914848914848914848914848b66d6d
+b69191b69191b69191b69191b66d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48489148489148489148489148486d48486d48486d4848
+6d48489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffb6b6
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191da9191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db69191b66d6db66d6db66d6db69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+dab6b6ffdadadab6b6916d6d9148486d4848914848916d6d916d6d916d6d
+916d6d916d6d6d48486d2424482424914848da9191ffb6b6dab6b6da9191
+916d6d916d6d6d24246d24246d24246d48486d24246d48486d48486d4848
+6d4848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d9148486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d48486d48486d48486d2424
+6d4848914848914848916d6d916d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+dab6b6dab6b6dab6b6da9191b69191b66d6db69191da9191dab6b6da9191
+dab6b6b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191da9191dab6b6da9191916d6d916d6d914848916d6d
+916d6d916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848916d6db66d6ddab6b6ffb6b6ffb6b6dab6b6b69191
+916d6d914848914848914848914848914848914848914848914848b66d6d
+b66d6db69191b69191b69191916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148489148486d4848
+6d48486d48486d48486d4848914848916d6db66d6db66d6d914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48489148486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b66d6db66d6db69191b69191b66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+da9191ffb6b6ffb6b6b66d6d914848914848914848916d6d914848b66d6d
+916d6db66d6d6d48486d48484824246d2424916d6ddab6b6dab6b6ffb6b6
+b69191b66d6d9148486d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6db66d6db66d6d916d6d9148486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d4848916d6d916d6db66d6db66d6db69191b69191b66d6d
+b69191b69191b69191b69191b66d6d916d6db66d6db66d6db69191b69191
+da9191da9191dab6b6dab6b6b69191b66d6db66d6dda9191da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db69191b69191b69191dab6b6b69191914848916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848b66d6dda9191dab6b6dab6b6da9191b66d6d
+916d6d916d6d914848916d6d914848914848914848914848914848916d6d
+916d6db66d6db66d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d4848914848914848914848914848
+6d48489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148489148486d48486d48486d48486d4848914848
+6d48486d48486d4848914848916d6db66d6db69191b69191b66d6d914848
+9148486d24246d48486d48489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6dffb6b6ffdadada91919148489148486d2424916d6d916d6db66d6d
+916d6db66d6d9148486d48486d24246d24246d4848da9191dab6b6ffb6b6
+dab6b6b66d6d9148486d48486d24244824246d48486d24246d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6d916d6d9148489148486d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d4848914848914848916d6db66d6db66d6db69191b66d6d
+b69191b69191da9191da9191b66d6db66d6db66d6db69191b69191b69191
+da9191da9191da9191dab6b6da9191da9191b69191b69191da9191dab6b6
+dab6b6da9191da9191da9191b69191da9191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b69191b69191b69191b69191da9191916d6d916d6d914848916d6d914848
+916d6d914848916d6d916d6d914848916d6d914848914848914848914848
+914848914848914848914848916d6db66d6db66d6db66d6db66d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148489148486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848b66d6db66d6db66d6db69191b69191916d6d
+9148486d48486d48486d48486d48489148486d4848914848914848914848
+9148489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffb6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffb6b6dab6b6b66d6d9148489148486d4848914848914848
+916d6d916d6d9148486d48486d48484824246d2424b66d6ddab6b6dab6b6
+ffb6b6b69191916d6d916d6d6d48486d24246d48486d24246d24246d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6db66d6d916d6d9148489148486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848914848916d6db66d6db66d6db69191
+b69191da9191da9191da9191b69191b66d6d916d6db66d6db69191b69191
+b69191b69191b69191da9191dab6b6da9191b69191b69191b69191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db69191b69191b66d6d916d6d914848916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d48486d4848
+9148489148489148489148489148486d48489148486d48486d4848914848
+6d48486d48489148489148489148489148486d4848914848914848914848
+6d48486d48486d4848914848916d6db66d6db69191b69191914848916d6d
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6dab6b6ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191ffb6b6ffb6b6da9191b691919148486d2424916d6d914848
+b66d6d914848916d6d6d48486d48486d24244824246d2424dab6b6dab6b6
+ffb6b6dab6b6b69191b66d6d9148486d24246d24246d48486d24246d4848
+6d48486d48486d4848914848914848916d6d914848916d6d916d6d916d6d
+916d6db66d6d916d6d9148489148486d48489148486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d4848914848916d6db66d6db66d6db69191
+dab6b6dab6b6dab6b6b69191b69191b66d6db69191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848916d6d914848914848914848
+914848916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d48486d4848914848
+9148489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d4848914848916d6db66d6d916d6d9148486d2424
+6d48486d48486d48486d48486d48486d48489148489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffb6b6ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6dab6b6da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db69191ffb6b6ffb6b6dab6b6da91919148486d48486d4848914848
+916d6d916d6d916d6d9148486d48486d48484824246d2424da9191ffb6b6
+ffb6b6ffb6b6da9191da91916d48486d48486d24246d48486d24246d4848
+6d24246d48486d48486d48486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d9148489148489148486d48486d48486d48486d2424
+6d24246d48486d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d4848914848914848914848916d6d916d6db66d6dda9191
+dab6b6dab6b6da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48489148486d48486d48486d48489148486d4848
+9148486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6da9191
+da9191da9191dab6b6dab6b6dab6b6da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6dda9191ffb6b6ffb6b6dab6b6b691919148486d4848914848
+914848916d6d6d48489148486d24246d48486d24246d2424b66d6dffb6b6
+dab6b6dab6b6da9191b69191916d6d6d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d4848914848914848914848916d6d
+916d6d916d6d9148489148489148489148486d48486d48486d48486d4848
+6d24246d48489148486d48486d48486d24246d24246d48486d24246d2424
+6d24246d24246d48486d48489148489148486d48486d24246d24246d2424
+6d24246d4848916d6d916d6db66d6db66d6db66d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191da9191da9191b69191da9191b69191da9191
+b69191b69191b69191b69191da9191b69191da9191b69191b69191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+914848916d6d914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d916d6d916d6d914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48486d48486d48486d48489148486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191dab6b6da9191dab6b6da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6ddab6b6ffdadaffdadaffb6b6b66d6d916d6d6d2424914848
+9148486d48486d48486d48486d48486d48486d2424482424916d6ddab6b6
+dab6b6dab6b6dab6b6b66d6d9148486d48486d48486d24249148486d4848
+6d48486d24246d48486d24246d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6d916d6d9148489148489148486d4848914848
+9148489148489148489148486d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d48486d48489148486d48486d48486d24246d2424
+6d2424914848916d6db66d6db69191b69191b69191b69191b66d6db69191
+da9191dab6b6dab6b6da9191da9191da9191da9191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b66d6d
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848916d6d914848916d6d914848916d6d914848914848914848914848
+914848914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848916d6d
+b66d6db66d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db69191ffb6b6ffdadaffb6b6b66d6d6d4848914848914848
+b66d6d6d48486d48486d24246d48486d48486d24246d24246d4848da9191
+ffb6b6dab6b6dab6b6916d6d9148486d24246d48486d24249148486d4848
+6d24246d24246d24246d24246d24246d24246d48486d4848914848914848
+914848914848916d6d916d6d916d6d9148486d48489148486d4848914848
+916d6d916d6db66d6d916d6d916d6d916d6d9148486d4848914848914848
+914848914848914848916d6d9148486d48486d48486d48486d48486d2424
+6d2424916d6d916d6db66d6db66d6dda9191b69191b69191b66d6dda9191
+da9191dab6b6dab6b6dab6b6dab6b6da9191da9191b69191da9191b69191
+da9191da9191b69191b69191da9191da9191da9191b69191b69191b69191
+b69191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848b69191
+b69191b69191b69191b66d6d9148489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148486d48489148489148486d48486d48489148486d48489148486d4848
+6d48489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffdaffdadaffdada
+ffdadaffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6d916d6db66d6ddab6b6ffdadaffb6b6b69191916d6d914848916d6d
+b66d6d916d6d6d48486d48486d24246d48486d24246d24246d4848b69191
+dab6b6ffb6b6dab6b6b691919148489148486d24246d48486d24246d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848914848914848914848916d6d914848914848914848916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d9148489148486d48486d48486d2424
+6d4848914848916d6db66d6db69191da9191da9191b66d6db66d6db66d6d
+da9191da9191dab6b6da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191da9191da9191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b69191b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848916d6d916d6d914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848916d6d914848914848914848914848914848b66d6db66d6d
+b69191b69191b69191916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+6d48486d48489148489148489148489148486d48486d48486d4848914848
+6d48489148486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6dab6b6b66d6d916d6db66d6d
+b69191b66d6d9148486d48486d48486d48486d48486d48486d2424916d6d
+dab6b6ffb6b6da9191da91919148486d48484824246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d24246d48486d4848
+914848914848914848916d6d9148489148486d4848914848914848916d6d
+916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148489148486d48486d4848
+6d4848916d6d916d6db66d6db69191da9191dab6b6b69191b69191b66d6d
+da9191da9191dab6b6da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191da9191b69191da9191da9191b69191
+b69191b69191da9191da9191da9191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848b66d6db66d6d
+b69191b69191b69191916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48489148486d48486d48489148486d4848
+6d48486d48486d48486d48489148486d48486d48489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffffffffda
+ffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191dab6b6da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db69191ffb6b6ffb6b6da9191916d6db66d6d
+b66d6db66d6d916d6d9148489148486d48486d48486d2424482424914848
+dab6b6ffb6b6dab6b6b69191916d6d6d24246d48486d24246d48486d2424
+6d48486d24246d48486d24246d48486d48486d48486d48486d48486d4848
+9148489148489148489148486d48486d48486d4848914848914848916d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148489148486d4848914848
+914848916d6d916d6db66d6dda9191dab6b6dab6b6da9191b66d6db66d6d
+b66d6dda9191da9191dab6b6da9191dab6b6da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191b69191
+b69191b69191b69191b69191da9191da9191b66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db69191b66d6db69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848916d6d916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848916d6d916d6d
+b66d6db66d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6ddab6b6ffb6b6ffb6b6b66d6d916d6d
+916d6db69191916d6d9148489148489148486d48486d24246d24246d4848
+da9191ffb6b6dab6b6b69191916d6d6d24246d48484824246d24246d2424
+6d48486d24246d48486d48486d48486d48486d24246d48486d48486d4848
+9148489148489148489148486d48486d24246d48486d4848914848916d6d
+b66d6d916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d
+914848916d6d916d6db66d6dda9191dab6b6dab6b6da9191b69191b66d6d
+916d6db69191da9191dab6b6da9191dab6b6da9191b69191b69191da9191
+b69191b69191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191b69191b69191b69191da9191b69191b69191b66d6db69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848
+914848914848914848916d6d914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d4848914848914848914848914848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48489148486d48486d48489148489148489148486d4848
+9148486d48489148486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffffdaffffdaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6d916d6db69191dab6b6ffffdadab6b6b69191
+b69191b66d6db66d6d914848916d6d9148489148486d48484824246d2424
+da9191dab6b6ffb6b6da9191916d6d6d48486d24246d24246d24246d4848
+6d24246d48486d24246d48486d48486d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d48486d24246d48486d4848914848916d6d
+b66d6db66d6d9148489148486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d914848914848916d6d916d6d
+916d6d916d6db66d6db66d6dda9191da9191dab6b6da9191b69191b66d6d
+b66d6db66d6dda9191da9191dab6b6da9191da9191b69191b69191b69191
+b69191da9191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191b69191b69191b69191da9191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d914848914848914848914848
+914848916d6d916d6d914848914848914848914848914848916d6d916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48489148486d4848
+6d48486d48486d48486d48486d48489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d4848914848914848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffffdaffffdaffffdaffdadaffdadaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6db66d6dda9191ffdadaffb6b6
+da9191b66d6db66d6d9148489148486d48486d48486d48486d2424482424
+916d6ddab6b6dab6b6dab6b6b691919148484824246d48484824246d4848
+6d24246d48486d24246d48486d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d48486d48486d24246d24246d4848914848916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191da9191dab6b6da9191b69191b66d6d
+b66d6db66d6dda9191da9191dab6b6da9191da9191da9191da9191b69191
+b69191da9191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191b69191b69191b69191da9191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db66d6db69191b69191b69191b69191b69191
+b69191b66d6d916d6d916d6d914848916d6d916d6d916d6d914848916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48486d48489148486d48486d48489148486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48489148489148486d48486d48486d48486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffdadaffffda
+ffdadaffb6b6ffdadaffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191b69191da9191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db69191b69191
+b69191b66d6db66d6db66d6db69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6ffb6b6
+ffb6b6da9191b691919148489148486d48486d48486d48486d48486d2424
+916d6ddab6b6dab6b6dab6b6da9191916d6d6d48486d24246d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d48486d2424
+6d48486d48486d24246d48486d24246d48486d24246d4848914848916d6d
+9148489148489148486d4848914848914848916d6d914848914848914848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db69191da9191dab6b6da9191b69191b69191
+b66d6db69191da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191da9191b69191b69191b66d6db69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48489148489148486d48489148486d48486d48486d48486d48486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffffdaffdadaffffdaffffdaffffdaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191b69191b69191da9191b69191da9191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db69191dab6b6
+ffdadaffdadada9191916d6d9148486d48486d48486d48486d48486d2424
+914848dab6b6ffb6b6dab6b6ffb6b6b66d6d6d48486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d48486d48486d24246d24246d24246d24246d24246d2424914848916d6d
+9148486d48486d4848914848914848914848916d6d9148486d4848914848
+6d48486d48486d4848914848916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191da9191da9191da9191b66d6db69191
+b66d6db69191da9191da9191da9191da9191b69191b69191b69191da9191
+b69191b69191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148486d48486d48489148489148489148486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48489148489148486d48489148486d48486d48486d4848914848
+9148489148489148486d48486d48489148489148489148486d4848914848
+9148486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffdadaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+b69191dab6b6ffb6b6b69191916d6d9148486d24249148486d2424914848
+b69191da9191ffdadaffb6b6ffb6b6b66d6d9148486d48486d24246d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+6d48489148486d4848914848914848916d6d9148489148489148486d4848
+9148486d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191da9191b69191b69191
+b69191b69191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d916d6d914848914848914848
+914848914848916d6d916d6d916d6d914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+ffb6b6ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffffda
+ffffdaffdadaffdadaffb6b6ffdadaffdadaffdadaffb6b6ffdadaffdada
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6db66d6dda9191da9191b69191b66d6d916d6d916d6d916d6db66d6d
+b69191da9191ffb6b6ffb6b6da9191b691916d24246d48484824246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d48486d4848914848916d6d9148489148489148489148486d4848
+6d48486d48486d4848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191b69191b66d6db66d6db66d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+914848916d6d914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48489148489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffb6b6ffffdaffffdaffffdaffdadaffdadaffdadaffb6b6ffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191da9191da9191
+b69191b69191b69191b69191da9191b69191b66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191916d6db66d6d
+916d6db66d6db66d6db69191b69191da9191b69191da9191b69191b66d6d
+916d6dda9191ffb6b6ffdadaffb6b6b691919148486d48486d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d9148486d48486d4848
+6d48486d48486d4848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d916d6d
+b66d6db66d6db69191b69191b69191b69191b69191da9191b69191b66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848914848914848914848914848916d6d916d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6db66d6d916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48489148486d48489148489148486d4848
+6d48486d48489148489148486d48486d48486d48486d48486d4848914848
+6d48486d48489148489148486d48486d48486d48486d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdada
+ffdadaffffdaffdadaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191b69191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d916d6d
+916d6db69191dab6b6ffb6b6ffb6b6b69191916d6d4824246d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d4848914848916d6d916d6d916d6d9148489148486d48486d4848
+6d24246d48486d4848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db69191b69191b69191b69191b66d6d9148489148486d4848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148489148486d48489148489148486d48489148489148486d4848914848
+6d48489148486d48486d48489148486d48486d48486d48486d48486d4848
+6d48489148489148486d48489148486d48486d48486d24246d48486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db69191ffb6b6dab6b6ffdadab69191916d6d6d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d6d48486d48486d2424
+6d48486d48489148486d4848914848914848914848914848914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848916d6d
+914848914848916d6d914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848b66d6db69191da9191da9191da9191b69191916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148489148489148489148489148489148486d24246d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffdadaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdada
+ffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+b66d6db69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6d916d6db66d6d916d6d916d6d
+914848916d6ddab6b6ffb6b6ffdadada91919148486d24244824246d4848
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d6d48486d24246d2424
+6d24246d24246d48486d48486d48486d48489148486d4848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191da9191da9191b69191
+b69191da9191da9191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db69191b69191b66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db66d6db69191b69191da9191b66d6d916d6d914848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+9148489148486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48489148486d48489148486d48489148486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48489148486d4848
+6d48489148486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d24246d48486d4848
+dab6b6ffffdaffdadaffdadaffdadaffdadaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffdadaffdadaffdadaffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6dda9191ffb6b6ffb6b6dab6b69148489148486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d9148486d24246d4848
+6d24246d24246d24246d48486d24246d4848914848914848916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191da9191b69191da9191dab6b6dab6b6da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6db66d6db69191b66d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48489148489148486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148489148486d48486d4848914848914848
+ffb6b6ffffdaffdadaffdadaffffdaffffdaffffffffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191dab6b6da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d
+916d6d916d6db66d6dffb6b6ffdadaffb6b6b691916d48484824246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d9148486d48486d24246d2424
+6d24246d24246d24246d24246d48486d48486d4848914848916d6d916d6d
+916d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848916d6db66d6d916d6d914848914848914848914848
+9148489148486d48489148489148486d48489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48489148486d48486d48486d48489148486d48486d4848914848
+6d48489148486d48489148489148489148486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffb6b6
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6ddab6b6ffb6b6ffb6b6da91916d2424914848482424
+9148484824246d48484824246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6d916d6d9148486d48486d48486d2424
+6d24246d24246d24246d24246d4848914848914848914848916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+dab6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+916d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6db69191dab6b6ffb6b6da91919148484824246d4848
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+916d6d916d6d916d6db66d6db66d6d916d6d6d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148489148489148489148486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6dab6b6b66d6d6d2424914848
+4824246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+916d6d916d6d916d6db66d6db66d6d916d6d9148486d48486d24246d4848
+6d24246d24246d24246d48486d4848914848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d
+916d6d914848914848914848916d6d914848916d6d916d6d914848916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48489148489148489148486d48486d48486d48486d4848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+b66d6d916d6d916d6d916d6dda9191ffdadadab6b6b691916d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+916d6d916d6db66d6db66d6db66d6d9148489148486d24246d24246d4848
+6d24246d24246d24246d24246d24246d4848914848914848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191
+b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+6d48486d48486d48486d48486d48489148486d48489148486d48486d4848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48489148486d48486d48489148486d48486d48489148486d4848
+6d48486d48486d48489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffdadaffdadaffb6b6ffdadaffdadaffdadaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffdadadab6b6dab6b6da9191dab6b6dab6b6dab6b6dab6b6da9191b69191
+da9191b69191da9191b69191da9191da9191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6dffb6b6ffb6b6b69191916d6d6d2424
+6d48484824246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d24246d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+914848914848916d6d914848914848914848916d6d914848914848914848
+916d6d914848916d6d916d6d914848914848916d6d916d6d914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d48486d4848914848914848914848
+6d48489148486d48486d48489148486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffdadaffdadaffdadaffffdaffdadaffdadaffffdaffffdaffdada
+ffffdaffffdaffffdaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6d916d6d916d6d916d6db66d6ddab6b6ffb6b6dab6b6b66d6d482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d48486d24246d4848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+916d6d914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148486d48486d48489148489148489148489148489148486d4848
+9148489148489148486d48489148489148489148486d4848914848914848
+9148489148489148486d48486d48489148489148489148489148486d4848
+9148486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d4848914848
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191ffb6b6ffb6b6ffb6b6b66d6d6d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848916d6d916d6d916d6d9148489148486d48489148486d48486d4848
+6d48486d48486d48486d4848914848914848914848914848916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848914848914848914848
+916d6d916d6d914848914848916d6d914848914848916d6d916d6d914848
+914848914848914848916d6d914848916d6d914848916d6d916d6d916d6d
+914848916d6d914848916d6d916d6d916d6d916d6d914848914848916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48489148486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d4848b66d6d
+ffdadaffb6b6ffdadaffdadaffdadaffdadaffffdaffdadaffffdaffdada
+ffdadaffb6b6ffb6b6ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db69191dab6b6dab6b6dab6b6dab6b6dab6b6b66d6d6d4848
+4824246d24244824246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d4848914848914848914848914848916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d914848916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+916d6d916d6d916d6d914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48489148486d48486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d48489148489148486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d2424b66d6d6d4848b69191
+ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b66d6db66d6db69191b69191
+b66d6db66d6db69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191dab6b6ffb6b6ffb6b6dab6b6ffb6b6da9191b69191914848
+6d48486d24246d24246d24246d48486d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148486d48489148486d48489148489148486d48486d48486d48486d4848
+6d48486d4848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b69191da9191da9191da9191b69191da9191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848916d6d914848914848914848914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d914848916d6d916d6d914848916d6d914848916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+6d48486d48489148489148486d48486d48486d48489148486d48486d4848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848b66d6db69191
+ffb6b6ffb6b6ffdadaffb6b6ffdadaffdadaffb6b6ffdadaffb6b6ffdada
+ffb6b6ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6dab6b6da9191da9191da9191dab6b6da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6d
+b66d6db69191dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6da9191b66d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d2424914848916d6d6d48486d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848916d6d
+914848916d6d914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848916d6d916d6d916d6d914848916d6d916d6d916d6d
+914848916d6d914848914848914848914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+6d48489148489148486d48486d4848914848914848914848914848914848
+6d48486d48486d48486d48486d48486d4848914848914848914848914848
+6d48489148489148489148486d48486d48486d24246d2424916d6db69191
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6ffdadaffdadaffb6b6
+ffb6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6
+dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6dda9191dab6b6dab6b6ffb6b6dab6b6dab6b6b69191b69191
+916d6d9148486d24246d48486d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d48486d48489148486d24246d24246d2424
+6d24246d48486d48486d48486d24246d24246d24246d48486d48486d4848
+6d4848914848914848914848916d6d914848916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+914848916d6d916d6d916d6d914848916d6d916d6d914848916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848
+914848914848914848916d6d914848914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48489148489148489148489148489148489148486d48489148486d4848
+6d48486d48486d48486d48489148489148486d48486d48486d4848914848
+9148486d48486d48486d48486d48486d48489148489148486d4848914848
+6d48486d48486d48489148486d48486d48486d24246d4848914848b66d6d
+dab6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6d916d6db69191da9191dab6b6dab6b6da9191b69191b66d6db66d6d
+b66d6db66d6d9148486d48486d48486d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d914848916d6d914848916d6d914848914848914848914848916d6d
+914848914848914848914848914848916d6d914848914848914848916d6d
+916d6d916d6d916d6d914848916d6d914848914848916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+9148489148489148486d48489148489148489148489148486d48486d4848
+9148486d48489148489148486d48489148486d4848914848914848914848
+6d48489148489148489148489148489148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b69191b69191b66d6d916d6d9148486d24246d4848916d6d914848b66d6d
+6d48486d48484824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191da9191b69191b66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d916d6d916d6d916d6d914848916d6d
+914848914848914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d914848
+916d6d916d6d914848916d6d916d6d914848916d6d914848914848914848
+914848914848914848916d6d914848916d6d914848916d6d916d6d914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848916d6d914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d4848914848
+6d48489148486d48489148486d48489148489148489148486d48486d4848
+6d48486d48486d48489148486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d
+b69191dab6b6da9191b66d6d6d48486d48486d4848916d6d916d6db66d6d
+9148486d48486d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d914848914848
+916d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6db69191b69191
+b69191b69191b66d6db69191b69191da9191da9191dab6b6dab6b6da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848914848914848914848914848916d6d914848
+914848914848914848914848916d6d914848914848916d6d914848914848
+914848916d6d914848914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848916d6d914848916d6d916d6d914848
+914848914848914848914848914848916d6d916d6d914848916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48489148486d4848b66d6d
+916d6d916d6d9148489148486d48486d48486d48486d48486d48486d4848
+dab6b6ffdadaffb6b6ffb6b6dab6b6ffb6b6ffdadaffb6b6ffdadaffb6b6
+dab6b6dab6b6ffb6b6ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191da9191da9191b69191916d6d916d6d914848914848916d6db66d6d
+b69191916d6d6d48486d24246d24246d48489148486d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848916d6d9148489148489148486d4848914848
+914848916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b66d6dda9191da9191da9191dab6b6ffb6b6dab6b6
+da9191b69191b69191b69191da9191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d916d6d916d6d916d6d914848914848914848916d6d
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d914848916d6d914848914848
+914848914848914848916d6d916d6d916d6d916d6d914848916d6d914848
+916d6d914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d4848914848914848914848914848914848916d6d916d6d914848
+9148489148486d48489148486d48486d48486d48486d48486d4848da9191
+da9191da9191b691919148486d48486d48486d48486d48489148486d4848
+da9191ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffdadaffb6b6ffb6b6ffb6b6
+dab6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d916d6d
+916d6dda9191dab6b6b69191916d6d914848916d6d914848914848916d6d
+b66d6d916d6d9148489148486d48486d4848916d6d9148486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d4848
+6d48486d48489148489148489148489148486d48489148486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191da9191dab6b6ffb6b6dab6b6
+dab6b6da9191da9191b69191da9191b69191b66d6db69191b66d6db69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b66d6db69191
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d914848914848
+914848914848916d6d916d6d916d6d914848916d6d914848914848916d6d
+916d6d914848914848916d6d914848914848914848914848914848916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d916d6d914848914848
+6d48489148489148489148489148486d4848b69191dab6b6ffb6b6da9191
+b69191b691916d48486d48486d48486d48489148486d4848914848da9191
+da9191da9191da9191da91919148486d48486d48486d48486d4848914848
+da9191dab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6b69191da9191da9191da9191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191da9191da9191da9191b69191b66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6dda9191dab6b6da9191916d6d6d48489148486d48486d48486d4848
+914848916d6db66d6d916d6d914848914848916d6d914848b66d6d916d6d
+9148486d48486d24246d48486d24246d48486d24246d48486d24246d4848
+6d48486d48486d48489148489148486d48486d48486d4848914848914848
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191da9191
+b66d6db66d6db66d6db66d6db66d6db69191b69191dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+916d6d914848914848916d6d914848914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848916d6db66d6d916d6d
+9148489148489148486d48486d4848b69191b69191da9191dab6b6da9191
+b69191da91916d48486d24246d48486d48489148486d4848916d6dda9191
+b69191dab6b6b69191da91916d24246d48486d48486d48489148486d4848
+dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+b69191b69191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191dab6b6dab6b6dab6b6dab6b6da9191b69191916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d916d6d916d6d
+916d6db66d6ddab6b6dab6b6b691916d48484824246d48484824246d2424
+6d24246d4848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+9148486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d24246d48486d4848914848
+914848916d6d916d6d916d6db66d6db66d6db69191da9191dab6b6da9191
+b69191b66d6db66d6db66d6db66d6db66d6db69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d
+914848916d6d914848914848916d6d914848914848914848916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848b66d6db69191dab6b6da9191b69191
+b691919148489148486d4848914848b66d6dda9191b69191da9191dab6b6
+da9191b691919148486d48486d48486d4848914848b66d6db69191b66d6d
+da9191da9191da9191b691919148486d24246d48486d48489148486d4848
+dab6b6ffb6b6ffdadaffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191dab6b6
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191b66d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6ddab6b6dab6b6b69191b66d6d4824246d48484824246d2424
+4824246d24246d4848914848916d6d916d6d916d6d916d6db66d6db66d6d
+916d6d6d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d24246d24246d48486d4848914848
+914848916d6d916d6d916d6db66d6db66d6db69191b69191dab6b6da9191
+b69191b66d6db66d6d916d6db66d6db66d6db69191b69191da9191b69191
+b69191da9191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848b69191da9191da9191dab6b6da9191
+dab6b6916d6d6d48486d24246d24246d4848b66d6db69191b69191b69191
+916d6d916d6d9148486d48486d48489148486d48486d4848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6
+dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db69191b66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191da9191da9191dab6b6dab6b6dab6b6da9191b69191916d6d
+916d6db66d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6d
+916d6d916d6db69191dab6b6da9191b66d6d6d24246d24246d24246d2424
+6d24244824244824246d24246d24246d4848914848914848916d6db66d6d
+916d6d916d6d9148486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6db66d6db69191b69191da9191dab6b6dab6b6
+da9191b69191b66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848914848916d6d914848914848914848914848916d6d
+914848914848916d6d914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848b66d6dda9191da9191da9191da9191
+da9191916d6d6d24249148486d4848914848914848916d6d916d6d916d6d
+9148489148486d48489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48489148489148486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6dab6b6da9191dab6b6da9191da9191da9191dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191dab6b6dab6b6b691919148484824246d4848482424
+6d48484824246d24246d24246d24244824246d24246d4848914848916d6d
+b66d6d916d6d9148486d48486d48486d24246d48486d24246d24246d2424
+6d24246d24246d48486d24246d24246d48486d24246d24246d48486d4848
+916d6d916d6d916d6d916d6db66d6db66d6db69191b69191da9191da9191
+da9191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b69191b69191b66d6db66d6db69191b66d6db66d6db69191b66d6d
+b66d6db69191b66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d914848916d6d916d6d916d6d914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+914848916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848916d6d914848916d6d914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848b66d6db69191b69191da9191da9191
+da91919148486d48486d48489148489148486d48486d48486d48486d4848
+6d48486d48486d4848914848916d6d916d6d916d6d916d6d6d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+da9191ffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191da9191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6ddab6b6ffb6b6b691919148486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824246d24246d4848
+914848916d6d916d6d916d6d6d48486d48486d48486d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+914848914848916d6d916d6db66d6db66d6db69191b69191da9191da9191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d916d6d914848916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6d914848916d6d916d6d914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848b66d6d916d6d
+9148489148489148489148489148489148489148486d48489148486d4848
+6d4848916d6d6d4848b69191dab6b6ffb6b6dab6b6dab6b6b691916d4848
+6d48486d48486d48489148486d48489148489148486d48486d48486d4848
+da9191ffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6da9191da9191dab6b6
+da9191da9191b69191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db69191dab6b6ffb6b6da91919148486d48484824246d4848
+4824246d24246d24246d48486d24246d24244824246d2424482424482424
+6d24246d48486d48489148489148489148489148486d24246d4848482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+914848914848916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db69191b66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d914848916d6d914848914848914848914848916d6d914848
+916d6d916d6d916d6d914848914848916d6d914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48489148489148486d48489148486d48486d48486d4848914848
+914848b66d6dda9191da9191dab6b6dab6b6dab6b6dab6b6da91916d2424
+9148486d48486d48486d48489148486d48486d48486d4848914848914848
+da9191dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6d916d6db66d6dda9191dab6b6dab6b6916d6d6d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824244824244824246d24246d4848914848916d6d9148486d48486d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d4848914848914848916d6d916d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+916d6d916d6d916d6d914848916d6d914848914848916d6d914848914848
+914848916d6d914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6db69191b69191da91919148489148486d4848914848914848
+9148489148486d48489148489148489148486d4848914848914848914848
+6d4848916d6dda9191b69191da9191da9191dab6b6da9191da9191916d6d
+914848916d6d9148486d48486d48486d48486d48486d48486d4848914848
+da9191dab6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6da9191da9191
+dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191da9191da9191da9191da9191da9191da9191b69191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6dda9191dab6b6dab6b6b66d6d9148486d48486d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d48486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d4848914848914848916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d914848
+914848916d6d916d6d914848916d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+b69191da9191dab6b6dab6b6ffb6b6b66d6d9148489148486d24246d4848
+6d48486d48489148489148486d48486d4848914848914848914848914848
+916d6d916d6d916d6db69191b66d6db69191da9191da9191b66d6db66d6d
+914848916d6d6d48486d48486d48486d48486d48486d4848914848914848
+da9191dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191da9191da9191da9191da9191da9191da9191b69191da9191
+da9191da9191dab6b6da9191da9191da9191b69191da9191da9191b69191
+da9191da9191da9191da9191da9191da9191b69191b69191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db69191dab6b6dab6b6b691916d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824244824246d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6db69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b66d6db66d6db66d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d914848914848914848
+914848916d6d914848916d6d914848914848914848914848914848914848
+914848914848916d6d914848914848916d6d914848916d6d916d6d916d6d
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848b66d6d
+da9191da9191da9191dab6b6dab6b6b69191916d6d6d48486d4848914848
+6d48489148489148489148489148486d4848914848914848ffb6b6ffb6b6
+ffdadaffdadaffb6b6b66d6d916d6d916d6db69191916d6d916d6d916d6d
+916d6d916d6d9148486d48486d48486d48486d48486d48489148486d4848
+da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+da9191da9191da9191da9191da9191b69191b69191da9191da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6b691919148486d24246d4848
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d4848914848914848916d6d916d6d914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+9148489148486d48486d4848914848916d6d916d6db69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b66d6d
+b66d6db66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848916d6d
+da9191da9191da9191da9191da9191b69191916d6d6d48486d48486d4848
+9148489148489148489148489148486d48486d4848916d6dffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6916d6db66d6d914848914848916d6d916d6d916d6d
+9148489148486d48489148486d48486d48486d4848914848914848914848
+dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191da9191da9191
+da9191b69191da9191da9191b69191b69191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191b69191da9191da9191da9191da9191
+da9191da9191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6da9191916d6d6d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d914848914848914848916d6db66d6db69191b66d6d
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b66d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d4848914848
+b66d6db69191b69191da9191da9191b66d6d916d6d916d6d916d6d914848
+9148489148489148489148486d48486d4848b69191b69191ffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6b66d6d9148486d2424916d6d916d6d916d6d
+9148489148486d48486d48486d48489148489148486d4848914848914848
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191da9191da9191da9191da9191da9191b69191b69191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6da9191916d6d6d48486d2424
+6d24244824246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db69191dab6b6dab6b6dab6b6da9191b66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848916d6d914848914848916d6d916d6d916d6d914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+9148489148486d48489148489148486d2424916d6ddab6b6ffb6b6ffb6b6
+ffdadaffb6b6ffdadaffb6b6916d6d6d24249148486d48486d24246d4848
+6d48486d48486d48489148486d48486d48486d48489148486d4848914848
+dab6b6da9191da9191da9191da9191da9191dab6b6da9191dab6b6da9191
+da9191dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191da9191da9191b69191916d6d6d48486d2424
+6d48486d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848916d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191da9191da9191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191da9191b66d6d916d6d6d24246d24246d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d914848916d6d916d6d
+914848916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+916d6d914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d4848914848916d6d916d6d916d6d916d6d914848914848
+6d48486d48489148489148489148486d48486d4848da9191dab6b6da9191
+ffb6b6ffb6b6ffb6b6dab6b66d24246d48486d2424914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b691919148486d48486d4848
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d48486d48486d48489148486d48489148486d4848
+6d24246d24246d48486d4848914848916d6db66d6db66d6d916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6d9148486d4848482424240000480000240000480000
+2400004800004824244824246d4848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db69191b69191b66d6d916d6d916d6d
+b66d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848916d6d916d6d916d6d914848914848
+9148489148489148489148489148486d48486d48486d4848916d6d916d6d
+b69191b69191b66d6db691916d48489148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+dab6b6dab6b6da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191
+da9191da9191da9191dab6b6dab6b6da9191b69191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191b69191da9191b69191
+da9191b69191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6dda9191b69191916d6d6d24246d2424
+6d24246d48486d24246d24244824246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d4848
+6d48489148486d4848914848914848916d6d916d6d914848914848914848
+914848914848914848914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6db69191
+b69191b69191da9191da9191da9191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b66d6d9148489148486d24246d2424482424482424482424
+4824244800004800004800004800004800004824244824246d4848914848
+914848914848914848916d6db66d6db69191b691919148486d24246d2424
+6d24246d24244824244824244800004824244824246d48486d48486d4848
+6d48486d48486d48486d4848914848914848914848916d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d2424
+6d24246d24246d48486d48489148489148489148486d48489148486d4848
+6d48486d48486d48486d48486d4848914848914848914848914848914848
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d914848916d6db66d6dda9191b69191916d6d6d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d48486d48486d4848914848
+914848914848914848914848914848916d6d916d6d916d6d916d6d914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191da9191da9191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6d9148489148484824246d24244800006d2424480000
+482424482424482424482424482424480000480000480000240000240000
+480000480000480000482424482424480000240000240000240000240000
+240000240000480000240000480000240000480000240000240000240000
+2400004800004800004800004824244824244824246d24246d48486d2424
+6d48486d24244824246d24246d4848916d6d9148486d24246d24246d2424
+6d4848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d4848914848914848914848914848914848
+9148489148489148489148486d48486d48486d48486d48486d48486d4848
+9148489148486d48489148486d48489148486d4848914848914848914848
+da9191dab6b6dab6b6da9191da9191dab6b6da9191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191916d6d914848914848b69191b69191da91919148486d48486d2424
+6d48484824246d48486d24246d24244824246d24246d24246d24246d2424
+6d24246d48486d48486d4848914848914848914848914848916d6d916d6d
+914848916d6d914848914848914848916d6d916d6db66d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6db66d6db69191b69191b69191b69191b69191
+da9191da9191dab6b6dab6b6da9191da9191b69191b66d6db69191b69191
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b691919148489148486d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+480000480000480000480000480000480000480000480000480000480000
+482424482424482424482424482424482424482424480000480000480000
+480000480000480000480000480000480000240000240000240000240000
+240000240000240000240000480000480000480000240000240000480000
+4824246d4848914848916d6d914848916d6d914848914848914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48489148489148486d4848914848914848914848914848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+9148486d48489148486d48486d48489148486d48486d4848914848914848
+9148486d48486d48486d48489148486d48486d4848914848914848914848
+9148486d48489148486d48489148486d48486d48486d4848914848914848
+da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b66d6d
+b69191b69191b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b66d6d6d48486d4848914848b66d6db69191b691919148486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148486d4848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848914848914848916d6d916d6db69191b69191b69191b69191
+b69191b69191da9191da9191b69191b66d6db66d6db66d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d916d6d
+b66d6db66d6db66d6d9148489148486d24246d24246d2424480000482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424480000480000480000480000480000480000480000
+4800006d24246d24246d48489148486d48486d48486d48489148486d4848
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848914848914848914848914848914848
+9148486d48489148486d4848914848914848914848914848914848914848
+da9191dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6da9191da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+b69191b69191b69191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+9148486d2424482424914848b66d6dda9191b69191916d6d4824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+914848914848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6db66d6db69191b69191da9191b69191
+da9191da9191dab6b6da9191da9191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6d9148486d48486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d48486d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d4848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148486d48489148489148489148489148489148486d4848
+9148489148489148489148489148489148486d48489148489148486d4848
+9148489148486d48486d48486d48486d48489148486d4848914848914848
+da9191da9191dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191
+da9191dab6b6dab6b6da9191da9191b69191da9191da9191da9191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191da9191916d6d
+6d24246d2424482424914848916d6dda9191b69191916d6d6d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848914848914848914848914848914848916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d
+914848914848914848916d6d914848916d6d916d6d916d6d914848914848
+6d48486d48486d4848914848914848916d6db66d6db69191b69191b69191
+b69191da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6d916d6d6d48486d24246d2424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d48486d4848
+9148486d48486d48486d24246d2424482424482424482424480000480000
+4800004800004800004800004800006d2424916d6db66d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48489148486d4848914848914848
+9148489148489148489148486d48489148489148486d48486d48486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+da9191da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191da9191b691916d4848
+4824244824244824246d24246d4848b66d6d916d6d9148486d2424482424
+6d24244824246d24244824246d24246d24246d48486d2424914848914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+6d48486d48486d4848914848914848914848916d6db69191b69191b69191
+b69191da9191da9191da9191da9191b69191b66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6d9148486d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d48486d48486d48486d4848
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824244824244800004800004800004800006d24246d24246d4848914848
+6d48489148489148489148489148489148486d48489148486d4848914848
+6d4848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48489148489148489148486d48486d48486d4848914848
+9148489148489148489148489148486d48486d48486d4848914848914848
+6d48489148489148486d48486d48486d48486d48486d48486d4848914848
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191da9191b69191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6dda9191dab6b6da9191b69191b66d6d9148486d24246d2424
+4824244824246d24244824244824246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+6d48486d24246d24246d4848914848914848914848916d6db66d6db69191
+b66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+916d6db66d6d916d6db66d6d9148486d24246d2424482424480000482424
+480000482424482424482424480000482424482424482424482424482424
+482424482424480000482424482424482424480000482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824246d48486d48486d48486d48486d2424482424
+4824244824246d2424482424482424480000482424482424482424482424
+482424482424482424480000480000480000480000480000480000480000
+4800004800006d4848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48486d4848914848914848914848914848914848914848
+da9191da9191dab6b6da9191da9191da9191da9191dab6b6da9191da9191
+da9191dab6b6da9191da9191da9191da9191da9191da9191b69191b69191
+da9191da9191b69191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191ffb6b6ffb6b6b691916d24244800006d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d48486d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+9148489148489148486d48486d48486d4848914848916d6db69191b69191
+da9191b69191da9191b69191da9191b69191b66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6d9148486d48484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424480000482424480000482424482424
+4824244824246d48486d48489148486d48484824246d4848482424482424
+482424482424482424482424480000482424482424482424482424480000
+482424482424482424482424482424480000480000480000480000480000
+4800004800004800002400004824246d24246d4848914848914848914848
+6d48486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+da9191dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6dda9191da91919148486d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+9148489148489148486d48486d24246d4848914848916d6db69191b69191
+da9191da9191da9191b69191b69191b66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d9148486d24246d4848482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24249148486d48486d48486d24246d2424482424482424482424
+482424480000482424480000482424480000482424480000482424482424
+480000482424482424482424480000480000482424480000480000480000
+4800004800004800004800004800004800004800004824246d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+da9191dab6b6dab6b6da9191da9191dab6b6dab6b6da9191da9191da9191
+dab6b6dab6b6da9191da9191dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d916d6d9148489148486d48484824244824246d24246d2424
+6d24246d24246d24246d24244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+916d6d916d6d916d6d916d6d914848916d6d916d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+9148489148489148486d48486d48486d4848914848916d6d916d6db69191
+da9191dab6b6da9191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6d916d6d6d48486d24244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424480000482424
+4824244824244824244824244824244824244824246d24244824246d2424
+6d48489148486d48484824246d2424482424482424482424482424482424
+482424480000482424480000482424480000480000480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+482424480000482424480000482424480000480000480000480000480000
+6d24249148489148489148489148489148486d48486d48486d4848914848
+6d48489148486d48486d48489148489148489148489148489148486d4848
+9148486d48486d48486d4848914848914848914848914848914848914848
+9148489148489148489148486d48486d48489148486d48486d4848914848
+6d48486d48489148489148489148486d48486d48486d4848914848914848
+da9191ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191b69191da9191b69191b69191da9191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b66d6db69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b66d6d916d6d9148486d48486d24246d24246d24246d24246d24246d2424
+4824246d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d4848914848916d6d916d6d
+b69191da9191da9191b69191b66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d9148486d24246d4848482424482424482424
+480000482424482424482424482424482424482424482424480000482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+4824244824244824244824244824244824244824246d48486d2424914848
+6d48486d48486d2424482424482424482424482424482424482424482424
+480000482424480000482424482424482424482424482424482424480000
+482424482424482424480000482424482424480000482424480000480000
+482424482424480000482424482424480000480000480000480000480000
+4800004824246d24246d4848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d4848914848914848914848914848914848
+da9191dab6b6da9191da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b66d6db69191b69191b66d6db69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+916d6d6d48486d24244824246d24244824246d24246d24246d2424482424
+4824244824244824244824246d24246d24246d24246d2424482424482424
+4824246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+9148489148489148489148486d4848914848914848916d6d916d6d916d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6d916d6d6d48486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d48486d48486d4848
+6d24244824246d2424482424482424482424482424482424480000480000
+482424480000482424482424482424482424482424482424482424480000
+480000480000480000480000482424482424482424482424482424482424
+482424480000480000480000482424482424480000480000482424482424
+4800004800004800004800006d48486d4848914848914848914848914848
+6d48489148489148489148489148489148489148489148489148486d4848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48489148486d48486d48489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191916d6d
+6d48486d24244824246d24244824244824244824244824246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d2424482424
+4824246d24244824246d24244824246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b66d6db66d6db66d6db66d6db66d6d916d6d916d6d914848
+9148489148486d48489148486d48486d4848914848914848914848914848
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6d9148486d24246d4848482424482424480000
+482424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d48486d48484824246d2424
+482424482424482424482424482424482424480000482424482424480000
+482424482424482424482424480000482424480000482424482424480000
+480000480000482424482424482424482424480000480000480000482424
+482424482424480000480000480000482424482424482424480000480000
+4800004800004800004800004824246d24246d2424914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48489148486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191dab6b6da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d6d2424
+6d24244824244824244824246d24244824244824244824246d24246d2424
+6d24246d24246d24244824244824246d24246d24246d24246d24246d2424
+6d24244824244824246d24246d24246d24246d24246d2424914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6d914848914848916d6d914848914848914848916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6d6d48486d24244824246d2424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824244824246d24246d48486d48486d48486d2424482424482424
+482424480000482424482424482424482424480000482424480000482424
+482424482424480000482424480000480000480000480000482424480000
+482424482424480000480000480000480000482424480000480000482424
+482424482424482424482424480000480000480000482424480000480000
+4800004824244824244824244800004800004800004824246d2424914848
+9148489148489148489148486d48489148486d4848914848914848914848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48486d48486d48489148486d48489148486d48486d4848914848
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6db69191b66d6db69191b66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b691919148486d4848482424
+4824244824244824246d24244824244824244824244824244824246d2424
+6d24244824246d24244824244824244824246d24246d24246d2424482424
+6d24246d24246d24246d24246d24244824246d24246d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b66d6db66d6d916d6d
+9148486d48486d4848914848916d6d914848916d6d914848914848916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6d916d6db66d6d916d6d6d48486d4848482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424482424482424482424482424482424482424480000
+480000482424482424482424482424482424482424482424482424482424
+4824246d24246d24246d24246d48486d2424482424482424480000482424
+482424482424482424482424482424482424482424480000482424482424
+482424482424480000482424480000480000480000480000482424482424
+482424482424480000480000482424480000480000480000480000480000
+482424480000480000480000480000480000480000480000480000480000
+4800004824244800004824244824244800004824244800004824246d4848
+914848914848914848914848914848914848914848916d6d914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148489148486d48486d48489148486d48486d48486d4848914848914848
+dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db69191b69191b66d6db69191b66d6d
+b69191b66d6db66d6db69191b69191b66d6db69191b69191b66d6db69191
+b69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db69191b69191b69191b691916d48484800006d2424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+916d6d916d6d914848914848916d6d916d6db66d6d916d6db66d6d916d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d24244824246d2424
+482424482424482424482424480000482424482424482424482424482424
+482424482424482424482424480000482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d48486d2424482424480000482424480000482424
+480000482424482424480000482424482424480000480000482424480000
+480000480000480000482424480000480000480000480000482424482424
+480000482424480000482424480000480000482424482424480000480000
+482424482424482424480000482424480000480000482424480000480000
+480000482424480000482424480000482424480000480000480000480000
+6d24246d4848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148489148486d48489148489148486d48486d4848914848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+da9191da9191da9191da9191dab6b6dab6b6da9191da9191dab6b6dab6b6
+da9191da9191da9191da9191dab6b6dab6b6da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191da91919148489148484824246d2424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d2424482424482424
+6d24246d24246d24244824246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191da9191b69191b66d6d
+b66d6d916d6d9148486d4848914848916d6d916d6db66d6d916d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6db66d6d916d6d9148486d2424482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424482424480000480000482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48486d48486d48486d24246d2424480000482424482424482424482424
+482424480000482424480000482424482424482424480000482424480000
+482424480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000482424482424482424480000480000
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000482424482424480000482424480000
+4824246d4848916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+6d48489148486d48486d4848914848914848914848914848914848914848
+b69191da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d6d48486d24244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191b69191b69191
+b66d6d916d6d916d6d916d6d914848b66d6db66d6db69191b66d6db66d6d
+b69191b69191b69191b69191b69191da9191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d24246d2424482424
+6d2424480000482424482424482424482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d4848
+6d48486d2424482424482424480000482424480000482424482424482424
+480000482424480000482424480000480000480000480000480000482424
+482424480000480000480000482424482424480000480000480000480000
+482424482424480000480000480000480000480000482424480000480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000480000482424480000482424480000482424482424480000
+4824244824244824246d4848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48489148486d4848
+da9191da9191da9191da9191da9191dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b691919148486d24246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824246d24246d24246d24244824246d24246d24246d24246d48486d4848
+914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b69191b69191b69191da9191da9191da9191da9191
+b69191b66d6d916d6d916d6d914848b66d6db66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191da9191da9191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6d916d6db66d6db66d6d916d6d6d4848482424482424482424
+482424480000482424482424482424482424482424482424480000480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d48486d48486d2424
+6d2424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424482424480000480000482424482424
+480000480000482424482424482424482424482424480000480000482424
+482424482424480000480000480000482424482424482424480000480000
+482424482424480000480000480000480000480000480000480000480000
+480000480000482424480000480000480000480000482424482424482424
+4824244800004800006d24246d2424914848914848916d6d914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148486d48486d48486d4848914848
+6d48489148489148489148489148486d4848914848914848914848914848
+da9191da9191da9191da9191da9191dab6b6da9191da9191da9191da9191
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+916d6d482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191b69191b66d6db66d6db66d6d916d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b66d6db69191b66d6db69191b69191916d6d6d48486d24244800006d2424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424480000482424480000
+4824244824244824244824244824244824246d48486d48486d4848482424
+482424480000482424480000482424482424480000482424480000482424
+480000480000482424480000482424482424482424480000480000482424
+480000482424480000480000480000480000482424482424480000482424
+480000480000480000480000480000480000480000480000480000480000
+480000480000482424482424480000480000482424480000480000480000
+482424482424482424480000480000480000482424480000480000480000
+4824244800004824244824246d48486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d48489148489148486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d4848914848
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191916d6d
+6d48486d24244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824244824246d24246d24246d24246d24246d24246d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b69191b69191b69191da9191da9191da9191da9191da9191
+b69191b69191b66d6db66d6db66d6d916d6db66d6db66d6db66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b69191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d2424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824246d24246d48486d24246d48486d24246d2424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424480000
+480000482424482424480000482424482424482424482424482424482424
+480000482424480000480000480000482424480000482424480000480000
+480000480000482424482424482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000482424
+4824244800004824244824244800006d24246d4848914848914848914848
+916d6d916d6d916d6d916d6db66d6d914848914848914848914848914848
+9148489148489148486d48486d4848914848914848914848914848914848
+9148486d48486d48489148486d48486d48489148486d4848914848914848
+da9191da9191da9191da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191dab6b6dab6b6da9191da9191da9191da9191da9191
+b69191b69191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6d9148486d2424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824246d24246d24246d24246d2424
+6d4848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191b66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6d9148486d24246d24246d2424480000
+6d2424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+4824244824246d24246d24246d48486d48486d2424482424482424480000
+482424480000482424482424482424482424482424480000482424482424
+482424482424480000480000482424482424482424480000480000482424
+480000482424480000480000480000482424482424482424482424480000
+482424482424482424480000480000482424480000482424480000480000
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000480000480000480000480000482424
+4800004800004824244824244800004800004824246d4848914848914848
+914848916d6d916d6d916d6d914848916d6d914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48489148486d4848914848916d6d
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b69191b69191da9191b66d6d6d48486d4848482424482424
+4824244824244824246d24244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24244824246d24246d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db69191b69191b69191da9191da9191dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b66d6db66d6db69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6d9148484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824246d48486d48486d48486d24246d2424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424480000482424480000480000482424482424482424482424480000
+482424482424480000480000480000482424480000480000480000480000
+480000480000482424482424480000482424480000482424482424480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424480000480000482424480000482424
+4824244800004824244824244824244800004800004824246d4848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+9148486d48489148486d48486d48489148489148489148486d4848916d6d
+da9191dab6b6da9191da9191da9191dab6b6da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b66d6d
+b69191b69191b69191916d6d6d48486d2424482424480000482424482424
+4824244824244824244824244824244824244824246d24244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d4848914848914848916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191da9191dab6b6dab6b6da9191
+dab6b6da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191da9191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191b69191da9191b69191b69191b69191
+b66d6db69191b69191b69191b66d6d9148486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d48486d24246d2424480000482424482424482424
+480000482424480000482424482424482424482424482424482424482424
+482424480000480000482424480000480000482424482424482424482424
+480000480000480000480000480000482424480000482424480000482424
+482424480000480000480000482424480000480000482424482424482424
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000480000480000480000480000482424
+4800004824244824244824244824244824244824244824244824246d2424
+9148486d4848916d6d916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148489148489148486d48486d4848914848914848914848916d6d
+da9191dab6b6da9191da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b66d6d
+b66d6db66d6db69191b69191b66d6db69191b69191b69191b66d6db69191
+b69191b69191b66d6d6d24246d24244824244824244824246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d48486d4848914848914848916d6d916d6d916d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191da9191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b66d6db66d6db69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191da9191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d9148486d24244824246d2424480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48489148486d48486d2424482424480000482424480000482424480000
+482424480000482424482424482424482424482424482424482424480000
+482424482424482424480000482424482424482424482424482424480000
+482424480000482424482424480000482424480000482424480000480000
+480000482424480000480000480000480000480000482424482424480000
+480000480000482424480000480000480000480000480000480000480000
+480000480000480000480000480000482424482424480000482424480000
+480000482424482424482424482424482424482424482424480000480000
+6d24246d4848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d4848914848914848916d6d
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db69191
+da9191da9191b69191916d6d6d24246d24244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824246d24246d24246d24246d4848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b69191b69191b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191916d6d9148486d4848482424482424482424482424
+482424480000482424482424482424480000482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424914848
+9148486d48486d2424482424480000482424480000482424480000482424
+480000482424482424482424482424482424480000482424480000482424
+482424482424482424482424482424480000482424480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+480000480000480000482424480000480000480000482424480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000482424482424482424482424482424482424482424482424
+482424914848914848914848916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48489148486d4848914848914848914848914848
+da9191da9191dab6b6da9191dab6b6da9191da9191da9191da9191dab6b6
+da9191da9191da9191da9191dab6b6da9191da9191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b66d6db66d6db69191b66d6db66d6db69191b69191
+b69191b66d6db69191b69191b66d6d916d6d6d48486d24246d24246d2424
+6d24246d48486d48486d24244824244824244824246d2424482424482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d24246d4848914848914848914848916d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191da9191b69191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db691919148489148486d4848482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d4848914848
+6d24246d2424480000482424482424482424482424482424480000482424
+480000482424482424482424482424482424482424482424482424480000
+482424482424482424482424480000480000482424482424482424482424
+482424482424482424480000482424480000480000480000482424482424
+480000480000482424482424482424480000480000480000480000480000
+482424482424482424480000482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424480000482424482424482424482424
+4824246d48486d48486d4848914848914848916d6d914848914848914848
+6d48489148489148489148489148489148489148489148486d4848914848
+6d48489148486d48489148486d4848914848914848914848914848914848
+da9191da9191da9191da9191da9191da9191da9191dab6b6da9191dab6b6
+da9191da9191dab6b6dab6b6da9191da9191da9191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6d9148486d2424482424482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191b69191b69191da9191b69191
+b69191b69191da9191da9191b69191b69191b69191b69191b66d6db69191
+b66d6db66d6db66d6d6d48486d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d48486d48489148486d2424
+6d2424482424482424480000482424480000482424482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+480000480000480000482424480000480000482424480000482424480000
+480000480000482424480000482424480000480000480000480000480000
+482424480000480000480000482424480000480000480000482424480000
+480000480000480000480000482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424482424482424482424482424482424
+4824244824246d24244824246d4848914848916d6d914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+6d48489148489148489148489148486d48486d48486d48486d4848914848
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b69191b66d6db66d6db66d6db66d6db69191
+b66d6d916d6d6d48486d24246d24244824244824246d24246d24246d2424
+4824244824244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824246d24244824246d24246d24246d24246d48486d4848
+6d4848914848916d6d916d6db66d6db66d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191da9191
+da9191da9191dab6b6dab6b6da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191b69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6db66d6d916d6d6d48486d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d48486d48486d48486d24246d2424
+482424482424480000482424480000482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424480000480000480000482424482424
+480000480000482424480000482424480000480000480000480000482424
+480000480000480000480000480000482424482424482424480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000480000482424480000482424480000480000
+482424480000480000480000480000480000480000482424482424482424
+482424482424482424482424914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191b69191b69191b69191da9191b69191b69191
+da9191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b69191b66d6db69191b66d6db69191b66d6db69191b66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6db66d6d
+916d6d9148486d24246d24244824246d24244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d2424
+6d24246d48486d4848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191da9191
+da9191da9191da9191da9191dab6b6da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6d916d6d9148486d48486d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24246d48489148486d4848482424482424480000
+6d2424480000482424480000482424482424482424480000482424482424
+482424482424482424482424480000482424482424480000482424480000
+482424480000480000482424482424480000482424482424480000480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000482424480000482424480000480000480000480000480000
+480000480000480000482424482424480000480000480000480000480000
+480000480000480000480000480000480000480000482424480000480000
+480000482424482424482424480000482424482424482424482424482424
+4824244824244824244824246d4848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148489148486d4848914848916d6d
+da9191dab6b6da9191dab6b6da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b66d6db66d6db69191b69191b66d6db66d6d916d6d
+6d48486d24246d48484824246d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d4848914848914848916d6d916d6d916d6d916d6d
+916d6db66d6d916d6d916d6db66d6db66d6db66d6db69191da9191da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+914848916d6d9148486d48486d24246d2424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824246d24246d48486d48486d2424482424482424482424482424
+482424482424480000482424482424480000480000482424482424482424
+482424482424482424480000480000482424482424482424482424482424
+482424480000482424482424482424482424480000482424482424480000
+480000482424480000482424480000482424480000482424482424482424
+480000480000480000482424480000480000480000480000480000480000
+480000480000480000480000482424480000480000482424480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000482424482424480000482424480000480000482424480000482424
+4824244824244824244824244824246d2424914848916d6d914848914848
+914848916d6d916d6d916d6d9148489148489148489148489148486d4848
+9148486d48486d48486d48489148486d4848914848914848914848914848
+b69191da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db69191b66d6db69191b66d6db69191b66d6d
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b66d6db69191da9191da9191b69191b66d6d914848
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db69191b69191
+b69191da9191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6d916d6d
+9148486d48486d48486d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d4848482424482424480000482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424480000480000480000
+482424482424480000480000482424482424480000482424480000482424
+482424482424480000480000482424480000480000482424480000480000
+480000482424480000482424480000480000480000480000480000482424
+482424482424480000480000480000482424482424480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424482424480000480000480000482424
+482424482424482424482424482424482424914848914848916d6d914848
+916d6d916d6db66d6db66d6db66d6d914848914848914848914848914848
+9148489148489148489148486d48489148486d48489148486d4848914848
+da9191da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191da9191b69191916d6d6d24246d2424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d2424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d48486d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6db66d6db69191b66d6db69191
+da9191da9191da9191da9191dab6b6da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db66d6db69191da9191da9191da9191b69191916d6d916d6d
+9148489148486d24246d2424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48486d48486d24244824246d2424480000482424482424482424482424
+482424482424482424480000480000482424482424482424482424482424
+480000482424480000482424482424482424482424480000480000482424
+482424482424482424482424482424482424480000482424480000480000
+482424480000480000482424480000482424482424480000482424482424
+480000480000480000480000482424480000482424480000480000482424
+480000480000482424480000480000480000480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000482424480000482424480000482424482424482424
+4824244824244824244824244824244824246d4848914848916d6d916d6d
+914848b66d6d916d6db66d6d916d6d916d6d9148489148489148486d4848
+9148486d48489148486d48486d48486d48486d48486d48489148486d4848
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6d9148486d48486d24246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d24246d48486d48486d4848
+914848914848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191da9191da9191da9191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191da9191b69191b66d6d914848914848
+9148486d24246d24246d2424482424482424482424482424482424482424
+4824244824244824244824244824244800004824246d24246d24246d4848
+6d24246d2424482424482424480000482424480000482424482424482424
+480000482424482424482424482424482424480000482424480000480000
+480000482424482424482424482424482424480000482424482424480000
+480000480000482424480000482424482424482424480000480000480000
+482424480000482424482424480000482424482424480000482424482424
+480000480000480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000480000480000482424480000
+482424480000480000482424480000482424482424482424482424482424
+4824244824244824244824244824244824246d4848914848914848916d6d
+916d6d914848b66d6db66d6db66d6d916d6d9148486d4848914848914848
+9148486d48489148489148489148489148486d48486d4848914848914848
+b69191da9191dab6b6da9191da9191da9191b69191da9191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848
+6d48486d24246d2424482424482424480000482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d24246d48486d4848
+6d48486d48486d4848914848916d6d916d6d916d6db66d6db66d6d916d6d
+b69191b69191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191da9191b69191916d6d9148486d4848
+6d48486d24246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d48486d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424480000482424480000480000482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424480000482424480000480000482424
+482424480000482424480000480000482424482424480000480000480000
+480000482424480000482424480000482424482424480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000482424482424482424482424
+4800004824244824244824244824246d24246d24246d48486d2424914848
+914848914848916d6d914848916d6d916d6d9148486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+b69191da9191da9191da9191da9191da9191b69191da9191b69191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d6d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d24244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d48486d48486d4848914848916d6d916d6d916d6db66d6d
+b66d6db66d6db69191da9191da9191b69191da9191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b69191b66d6db69191b69191b69191
+b66d6db66d6d916d6db66d6db66d6db66d6d916d6d9148486d4848914848
+6d48486d2424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d48486d24246d2424482424
+482424482424482424482424482424482424482424482424482424480000
+482424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424480000480000480000
+482424482424482424480000480000482424480000480000480000480000
+480000480000482424480000480000480000482424480000482424482424
+480000480000480000480000480000480000480000482424482424482424
+480000480000482424482424480000480000480000482424480000480000
+480000480000480000480000480000482424480000480000482424482424
+4800004824244824244824244824244824246d24246d24246d2424914848
+916d6d916d6d916d6d914848914848916d6d914848916d6d914848914848
+9148489148489148486d4848914848914848914848914848916d6d914848
+b69191da9191da9191da9191da9191da9191b69191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db69191b69191916d6d6d2424482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d48486d48486d4848914848914848916d6d916d6d
+b66d6db66d6db69191b69191da9191da9191da9191b69191b69191b66d6d
+b69191b69191b69191da9191b69191b66d6db66d6db66d6db69191b66d6d
+b66d6db66d6d916d6db66d6db69191916d6d6d48486d48489148486d4848
+6d48486d2424482424482424482424482424480000482424482424482424
+4824244824244824246d24246d48486d48486d2424482424482424482424
+482424482424482424482424482424482424482424482424480000480000
+480000482424480000482424482424482424482424482424482424482424
+480000482424480000482424482424482424480000482424482424482424
+480000480000480000482424482424482424480000482424480000482424
+480000482424482424480000480000482424480000480000480000482424
+480000482424480000482424480000480000482424480000482424480000
+482424480000480000480000482424480000480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000482424480000482424482424482424482424482424
+4824244824244800004824244824244824244824246d24246d2424914848
+916d6d9148489148489148489148489148489148489148486d4848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+b69191b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191da9191da9191da9191b66d6d6d4848482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824246d24246d24246d24246d24246d48486d48486d4848914848914848
+916d6db66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6d916d6d9148489148486d24249148486d48489148486d4848
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d24246d48486d4848482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424480000480000
+482424482424482424482424482424482424480000482424482424482424
+482424482424480000482424482424482424480000482424482424482424
+482424480000482424480000480000482424482424482424482424480000
+480000482424482424482424482424480000482424480000482424480000
+480000482424480000482424482424480000480000482424482424482424
+482424480000480000482424480000480000480000480000480000480000
+480000480000480000482424482424482424482424482424482424482424
+4800004824244824244824244824244824244824246d24246d2424914848
+916d6d916d6d914848914848914848916d6d916d6d914848914848914848
+916d6d9148486d48486d48486d48486d48489148486d48489148486d4848
+da9191da9191da9191b69191da9191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6d9148489148486d48486d48486d48486d48486d48486d24246d2424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24244824244824246d24246d24246d24246d48486d48486d4848
+914848914848916d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6d9148486d48486d24246d48489148489148486d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d48486d2424482424480000480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424480000480000482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424480000480000482424482424482424482424480000
+482424482424480000482424482424480000480000482424482424480000
+482424482424482424482424482424482424480000482424482424480000
+480000480000480000480000480000482424480000482424482424480000
+480000480000480000482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24244824246d2424
+6d4848916d6d914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d4848914848914848914848
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191916d6d6d4848
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d4848916d6d6d48484800004824244800004824246d4848914848
+9148486d48484824244824244824244824246d24246d24246d48486d4848
+6d48486d48486d4848914848916d6db66d6db69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191da9191916d6d
+916d6d6d48486d24244824246d48486d48489148486d48486d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+482424482424482424480000480000482424482424482424482424480000
+482424482424480000482424480000482424480000482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424480000482424482424482424482424480000482424
+482424482424482424480000482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424480000
+480000482424480000482424482424482424482424480000482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d4848916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+9148489148489148489148486d48486d48489148489148486d4848916d6d
+b69191da9191da9191b69191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d9148486d48486d2424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+482424916d6d916d6d6d48484824244800006d24246d4848b66d6db66d6d
+b66d6d916d6d6d48484824244824244800004824244824246d24246d2424
+6d48486d48486d48486d48486d4848914848914848b66d6db66d6db69191
+b66d6db69191b69191b66d6db66d6db69191da9191da9191da9191914848
+6d24246d24244824246d24246d48489148486d24246d4848482424482424
+4824244824244824244824244824244824246d24244824246d24246d2424
+6d2424482424482424480000482424482424480000482424480000482424
+480000482424482424480000482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24244824244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424480000482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+6d2424916d6d916d6d914848916d6d914848914848914848914848914848
+9148489148486d48486d48489148489148486d4848914848914848914848
+b69191da9191b69191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b69191916d6d9148486d24246d24246d24246d2424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824246d2424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424916d6db66d6d916d6d
+b66d6db66d6d9148484824244824244824244824246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d4848914848914848916d6d
+916d6db66d6db66d6db66d6db66d6db69191da9191b66d6d916d6d6d2424
+4824246d24244824246d24249148489148486d48486d48486d2424482424
+4824244824244824244824244824244824246d48486d24246d24246d2424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d4848482424482424482424482424482424
+4824244824244824244800004824244824244824244824246d2424482424
+6d24249148489148489148489148489148489148486d4848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+b69191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6d9148486d48484824244824246d24246d48484824246d2424
+4824246d24244824244824244824244824244824244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824246d24246d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000480000916d6db69191da9191
+da9191dab6b69148489148484800006d48484824246d48486d48486d4848
+6d24246d24244824244824246d24246d48486d48486d48486d48486d4848
+6d4848914848914848914848914848916d6d916d6d6d48486d2424482424
+6d24244824246d24246d24249148486d48486d48486d24246d2424482424
+6d24244824244824244824244824244824246d24246d48486d2424482424
+482424482424482424482424482424480000482424482424482424482424
+480000482424482424480000482424482424482424480000482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d48486d24246d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d2424482424
+4824244824244800004824244824244824244824244824244824246d4848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48489148489148486d48486d48486d48486d48486d4848914848914848
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6d6d48486d24244824246d24244824246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+4824244824244824244824246d24244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d24246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824244824244800006d2424916d6db66d6d
+da9191b691916d48484824244824244824246d48486d24246d24246d2424
+6d48486d24244824244824244824246d24246d48486d4848914848914848
+916d6d9148489148489148486d48486d48486d48486d48486d24246d4848
+6d48486d48486d24246d24249148486d24246d24246d2424482424482424
+4824244824244824244824246d24246d48484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d48489148489148486d48486d48486d4848914848
+6d48486d48489148489148489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48484824246d24244824244824244824244824244824244824246d2424
+914848914848914848916d6d914848914848914848914848914848914848
+9148486d48486d48489148489148486d4848914848914848914848914848
+b69191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d6d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24244824244824244824244824246d2424482424482424482424
+6d24246d24246d24244824244824244824246d24246d24246d2424482424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24244824246d2424482424482424482424
+6d24244824244824244824244824244824246d24246d4848914848916d6d
+916d6d6d48484824244824244824246d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d48486d4848
+9148486d48486d24244824246d24244824246d24244824246d2424482424
+4824246d24246d24246d48486d48486d4848482424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424482424480000482424480000482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48489148486d48489148489148486d48489148489148489148486d4848
+6d48486d48486d48486d48489148489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d2424482424482424482424482424482424
+6d4848914848914848914848914848914848916d6d914848914848914848
+6d48486d48489148486d48486d48486d4848914848914848914848914848
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6d916d6d916d6d914848914848914848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24244824244824246d24244824244824246d2424482424482424
+4824246d24246d24246d24246d24246d24246d24244824246d24246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24244824244824244824244824244824246d24246d2424482424
+4824244824244824244824244824244824246d24244824246d2424482424
+6d24246d24244824246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+4824244824244824244824244824244824246d24246d2424da9191b69191
+b691919148484824244824244800006d48486d48486d48486d48486d4848
+6d24244824244824246d24246d24246d24246d24246d48486d48486d4848
+9148489148486d48486d24246d24244824244824244824246d24246d2424
+6d24246d48486d48486d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d48486d48486d48486d48486d4848914848914848914848914848
+9148489148489148489148486d48486d48489148489148486d4848914848
+9148486d48486d48486d48489148486d48486d48489148486d4848914848
+9148489148489148486d48486d48486d48486d48486d48489148486d4848
+6d48489148489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d2424482424482424482424482424
+6d2424914848914848914848914848916d6d914848914848916d6d914848
+9148486d48489148486d48489148486d48486d48486d48486d4848914848
+b69191da9191da9191b69191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6d916d6d9148486d48486d48489148486d48486d4848
+6d24246d48486d24246d48486d24246d24246d24246d2424482424482424
+6d24246d24244824244824244824244824244824246d2424482424482424
+4824244824244824246d24246d2424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824246d24246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824244824244824244824244824246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d4848914848914848914848914848
+9148489148489148489148489148489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d24246d24246d4848914848914848
+6d48486d24246d24244824244824246d24246d4848916d6d916d6d914848
+914848482424482424482424482424482424482424482424482424482424
+6d24246d24246d24244824244824244824246d24246d24246d4848914848
+9148486d48486d24246d2424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d24246d48486d48486d48489148489148489148486d4848
+9148489148486d48486d48486d48489148486d48489148486d48486d4848
+6d48489148489148489148489148489148486d48486d4848914848914848
+6d48489148489148489148486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d2424482424
+4824246d48486d4848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d4848914848
+b69191da9191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6d916d6d916d6d916d6d9148486d48486d48486d48486d4848914848
+6d48486d24246d24246d2424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24244824244824244824246d2424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824244824244824246d24244824246d24246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d24244824244824246d2424b66d6dda9191916d6d
+916d6d4824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d48486d4848914848914848
+6d48486d24246d24244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d48486d48489148489148486d4848914848
+9148489148486d48489148489148486d48489148489148486d4848914848
+6d48489148489148489148489148486d48486d4848914848914848914848
+6d48489148486d48486d48489148489148489148489148486d4848914848
+6d48486d48486d48486d48486d48489148489148489148486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d24246d24246d2424
+4824246d24246d24246d4848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148486d4848914848
+b69191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6d916d6d
+916d6d916d6d916d6d9148486d48486d48486d48486d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d2424482424482424
+4824246d24246d24244824246d24246d24246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824244824246d2424482424
+4824246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d4848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d2424482424482424
+4824246d24246d24246d24244824244824246d48486d48486d48486d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48489148486d4848914848914848914848914848
+9148486d48489148489148489148489148489148489148489148486d4848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148486d48489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d24246d24246d24246d4848914848914848914848914848914848914848
+9148489148486d48489148486d48489148486d48486d4848914848914848
+grestore
+showpage
+%%Trailer
+
+--owatagusiam
+Content-type: image/gif
+Content-Transfer-Encoding: quoted-printable
+Content-Description: Quoted-Printable test
+
+GIF87a@=01=C8=00=D7=00=00=00=00=00=00=00=AA=00=AA=00=00=AA=AA=AA=00=00=AA=
+=00=AA=AAU=00=AA=AA=AAUUUUU=FFU=FFUU=FF=FF=FFUU=FFU=FF=FF=FFU=FF=FF=FF=00=
+=00=00=14=14=14 ,,,888EEEQQQaaaqqq=82=82=82=92=92=92=A2=A2=A2=B6=B6=B6=CB=
+=CB=CB=E3=E3=E3=FF=FF=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=FF=FF=00=BE=
+=FF=00}=FF=00A=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=BE=FF=00}=FF=00A=FF=
+=00=00=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=BE=FF=00}=FF=00A=FF}}=FF=9E=
+}=FF=BE}=FF=DF}=FF=FF}=FF=FF}=DF=FF}=BE=FF}=9E=FF}}=FF=9E}=FF=BE}=FF=DF}=FF=
+=FF}=DF=FF}=BE=FF}=9E=FF}}=FF}}=FF=9E}=FF=BE}=FF=DF}=FF=FF}=DF=FF}=BE=FF}=
+=9E=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=
+=C7=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=C7=FF=B6=
+=B6=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=C7=FF=00=
+=00q=1C=00q8=00qU=00qq=00qq=00Uq=008q=00=1Cq=00=00q=1C=00q8=00qU=00qq=00Uq=
+=008q=00=1Cq=00=00q=00=00q=1C=00q8=00qU=00qq=00Uq=008q=00=1Cq88qE8qU8qa8qq8=
+qq8aq8Uq8Eq88qE8qU8qa8qq8aq8Uq8Eq88q88qE8qU8qa8qq8aq8Uq8EqQQqYQqaQqiQqqQqqQ=
+iqQaqQYqQQqYQqaQqiQqqQiqQaqQYqQQqQQqYQqaQqiQqqQiqQaqQYq=00=00A=10=00A =00A0=
+=00AA=00AA=000A=00 A=00=10A=00=00A=10=00A =00A0=00AA=000A=00 A=00=10A=00=00=
+A=00=00A=10=00A =00A0=00AA=000A=00 A=00=10A A( A0 A8 AA AA 8A 0A (A A( A0=
+ A8 AA 8A 0A (A A A( A0 A8 AA 8A 0A (A,,A0,A4,A<,AA,AA,<A,4A,0A,,A0,A4,A<=
+,AA,<A,4A,0A,,A,,A0,A4,A<,AA,<A,4A,0A=00M=B2=00Q=AE=00Q=AA=00U=AA=00Y=A6=00=
+Y=A2=00]=A2=00a=9E,=00=00=00=00@=01=C8=00=00=08=FC=00G=08=1CH=B0=A0=C1=83=
+=04I(\H=C2=04C=12%J<ThB=E2D=85=11O=B4HR#=A2=C7=8F C=864A=F2D=8C(0`=14=A1=81=
+dE=0B=175h=E0=B0Q=03=07=8D+/r=C2=88=C1=02=85=CF=14'=82=06=F5=89"=86=0D=1D2V=
+=10=95=11#E=0A=1C.l=C8=98A=A3=06=16!7=92=DCh=A2$=06=8C=9C-h=C8=B8=E1S=A8Y=
+=14f=85=A2H!=83=A8=D3=15U=8D=DC=F0=DA=94h=DA=B4h=EF=E2=D5=CB=B7/=D1=BFle=EC=
+=F4=A1dG=8E=AF9_=CC=F8=91CG=8D=C74hP=B5=01c=07=90=15*T8MQ=E3F=8B=150G=94=10=
+A:D=08=11+L=AB=0E=01=02=A1=EB=D7=09/Z\X=02=C6=C4=12C*^=84=18=11=85=90=13$E=
+=0A=1FN=D2=04=8C((=98=F2=C0=0C#f=D5=1A6=A2=DF=98=81=18=86=0C"0\=E4=C5K=E3=
+=85=8B=9C=FB2R=AC=AD=D1=85=C5=0C=AF=93u=F0H=E2=C3=07=12"7=82x=B5.C=F3=DF=BB=
+=E2=EF=A2P=E1"=85=8D=14,=BC=10C=0FC =91=03S5=ECPW_3=F4=95=16P=0EFx=C2_?=F9=
+=97X=0B3=9C=87XN9=C4=F0=82J6=00aD=0D04=D1=02=0B2=FC=C0=02f=99=B1=15C=17/=8D=
+@=9A=08=AB=D5h=1A=08=AD=C1=A6cA=139=84=D1l=10=B5=00dDC=A4=E0=11C=1F=99=F0=
+=02=0B=DFU4\=92=C4=157=D4O+=1C8=83=13K=001=C5=0D\=E2=F0=D8W.=CC=F0BYS=0E=95=
+B=0B-=D8PE=0C+=BC=D0F=0C8=A5$C=15=86=ED D=11G=14=01D=0E8=F0=C0=E7N2=B0 =9E]=
+g=C1=B0=9DZ(=90(C=0B=87=C9=80=C3=0FG=EC0=C6=0B=F6=11z=16=0D=87:=98=9F=84~Q=
+=A8B=0D1=00=A1C=0C4|=CA=14=0E2=B8=F0=9Dw=FBD=FC=E0=02=0F6=A0=19=D3=0A=B4f=
+=A6=C2=0A2Dq=82=88A=B8`B=0D=A5=D9=B8=1A=8E;=16;=82l=16=95 =A4G'=E40=D2w=C4=
+=95=A0=A4=10K=00=17=91=93O=A2 RqRN=D8=C3=0A=F2=C90=C5=0E<=B4=E7=83=0E8=DC =
+=D8N4=B40(Z>=05=E1Sf/(=B1=C6=126=A0H=C3=86;=F1=A9C=12KH=C1=85=12A=E8=90=03=
+=0FL=A5TDJ/=BC=FB=17=0C?=909=E5Z+=FC=80CN3=04=11=DD=BE=9Ai=E6=F0_cr:=14=0F=
+=99=8A=8C=A8[*=EC=D4=E2~.=B5=FB=9D=AA=AA=C2=80=E6=87,=D4\=EB=AD+=0AZB=CD=99=
+=AD #=8D=C2=AAF=AC=B1=B0=3D=E4=91=92=D7=DA`=82=14%=8CY=C3=09=1E=E1P=02=0A/I=
+-=DC=09=3D=B4 F=CA=13B=0D=E5H-=04w-I)=94tB=0A<=A4@]=0C=06=F30=C4=13FlQ=05=
+=9FY=FB=14=E1UKN=01=16=83=0F-=F4=10C=0C7=0CqC=0Di=D6=94=93=11E=08a=C3=0D9=
+=FC=F0D=12Z=0CQ=84=C1b=8C=1A=C3=0C+=CC=90=B7Oc=12EC=12=1F=13=A5=02=19^%=06=
+=C3=0C8=C8=DA=82=0A7|=EC=12=0C=9Br=8AB=11=12=9B|r=0A*=E0=00=03=0E=95=8A=0E=
+=AB=0D=17=A2yb=CD+=D2=0AZ=0A.=D0=C0=C2=0D=0E=B5=B9B=093=A8=F0s=D07=E6H=F4A=
+=0B=C5=D0PD2/=D9=C2=09.=A0=C9=BC=AA-=BC=B0=03I=8A=B9=80=14=0A=15=C9pto0=E4=
+=FB=92=11:`=1B=D1=B2=CF=02=C7-I(=D8=B0=14=18)=B1=98=0E=82=90=848x=81`9=08=
+=82XJ=17=9EA=8D)=05+=E8=C1=0E\=90=12>-=0Cf=14=D4I=10=D2=85=83%=FC=00=0AJ=F0=
+=C1=0Et=B0=83=190=E5=07=A5=DA=DCO|B=83=14=A0K=05=14=FA=C9=0C=14=C40=8C=A9=
+=8A=09=FB< U=0D=9Cb=AB=16@G)=B6[K[J&2=0A=A5@,=1F{B=11R4=84=94=CC=80Q=AF=0AP=
+=0BR=C7$&=99=E47$=18=08DF=E0=B3=D5=91=00h=C2=1A=DA=F5x=A4=10=17=94=00x=FDYL=
+=9BzP=03=D0=D0=E0=06*=A8=D9K\=10=83f=A9=EF=06R=01=9F=D7L=80=03=FE=A5@=093=
+=B8=C2w=F4w4=AF=8D=E4=04,=08JIJ=82<6=F1=E0C9=E8L=E3=CC@=06'=0C=C1=09=06=A3=
+=82=1Bb =03=1Exl-;=C8=CC=0C=80=A0=AE=0Fy=05+7=80=99N8=A9=B8=C0=09=A1=07<=E0=
+=C1=0E=A0=D0=03=13=C6*U=1E=E3=E1=A0\=08=86=17=C8 =08=83=8AA=85L=A8=83&x=07=
+=83=95I=C9=DFT =98=1E=BC=003=B5=93=90Oz0(=DB=9D=E5'*=08Q=EDP=90=03=EF=E4=E0=
+'=3D=C0=81=13\=A0=02#=B8`=0Al=11=82O=94=B5=02=16d=FCqF2=FA=99=08~=06=83=11=
+=D8H=8Cc<=16EV=D7=03=FEP=ED=08.@=91=10=02=AA=02=1E=F4@y,h=CE=0D=0C%=83|=C9Q=
+]=DCb=0B=0Arr=83%=04=E1=06U=D0_qre=BF=A3=99`Q.=08J=0E=D4"=03=1F=C4`U=BB=BB=
+=01=0E=8E0=05,X=81=09#,!S=0A=A4=92=CD=A8=E0=05I=D8I=0D=3D=94=18U=FA=12=07D=
+=00=C2=E2l=B0=84=AC=CC=84=07b=91A=1B[dS=17H=C1cLJ=89=A0=9C=92J=9BV=C5=A7=AB=
+=FA=10=0C=94=E0=18=01Ie=88D=EC=D4=104=17M=93=FDE0=99RJ=0A=CE=90=17=14=A8=8A=
+$< gD=DA=B8=83=1F=B1=A0=05=F2=9C=11=18=A9=C7=1A=10=CCs=8C=181J=0EX =91=88=
+=F0=A6=04f4N=0B=8A=D0=02"=9C=88=99MXB=11=DA=D8=CE=18=B4=C03=DE=01=0E=13f =
+=85=F05=87=075=F0A[M=90=1C=16=FB=88-I=FC=F3=C1A=81=90=D0=B393'=DF=81A=0Ev`=
+=03=1C=E0=A0=07=B0=8Ce=12vP=83=A9<=92=05=1E=C3=8C=B8=E6=93=18=D8b=D0=94r=E1=
+=13=1E=A1=D3=19)H=86*=C9=E3=9B=AD|=90=99=1B,=C7%=A8=C3J=A90=13=03O~=06gs=FC=
+[q_=80=84=1B=FC@=08B=80A=16=1C=16D=14=80&be-=E2O=80=00!=A1=00=A5=7F}$=ADF*2=
+!"`=E4{<Z=81^=F7=CA=D7=BE=92=86he4=E3n.B=D8=DA=14=A5=05=06=A5-=0F=94@=AB=16=
+=E8=00=09:=F8=8AuN=D0=A6=F0=BD$=077=00=AE=85u=C0=B9=1A=A8=80[=85,I=AA^"=A0=
+=15=A4=A0t=AA=BA=C1=12g=B28=1F=E4=80=0A=EC=A9=81=09=C7=F2][1AT=7F=9B=0Fb0=
+=A8=AA=17=E4=E0=09)iN$i=E2=1C=1A=F4=800=C5=B3=15=AEj=00=04=1A=04=C1|=80c=82=
+=11b=05=FB=17=198=81=084=E0A=0B~=DC=83:=D8 =06=1DTfN=8Cr=B9@=C1=EE>f]=0B=10=
+`=F8.k"=EA=C5}1-=B7=B4=F5=11=86=BC` =03=9EQ=81k=E4=D7=03=EB=88=045P=88k=16=
+=9C=ACD=CE=C0=04=1A=19s=CEXP=95"T=14=06=F2=E2=81=11z=85=A6@=DDU=09=DEy=C1=
+=10p;=04=B4=ECOJ)=09=B1=0D=1Ac=E2!=E4=80=C8=98=06-s=7F=E0=9C=0C%=90*R^Aoe=
+=C0=84=83=1D=C6=B8=C7=3D=E6*=CBU=DB=1EH!=09Sp=C1=12Nd<=E5=D1=C0=0A1x=02=0F=
+=B0=13m=DB=E6=C0=87k=A2=F6=DF=A6R=15&D=C7=07@=C0A=E9`=80=84=180=95Bq=A6=1A=
+=0D=EA=02g;=9B)-=A7=EE(C=0C=12=E8`=0Dz5z=CD=E7kvS=02=A8=F17=C4'=8A=1E=AE|`=
+=85"tHspy=C1=F0=88w=D7=98=9D=CBP=1A=8D=F7=84=FB83K=1E=CC=A0f=1E.=F2=EE=0C=
+=0A=99=C8H&=094=D8=C1gjUY$d=88=07QHW=0C=9C=00=84c=C2=CC=06=C7=DD]=11=C2=A9=
+=03#=13A=E16k'=0Bt=80=14=A6=FC-%=A02=02O=17=9AdN"a=D5=B1=D4=13=97n=B0=EE$=
+=CB@=06)=8CaX=F12=AF=C3=98=18=86=E8v=F7]=E2-=EF=85=D0=BB=DE=F7=AE=D1=80=F5=
+=BDo=1FQ=846=C6=81O=0Bb=00*=19=E4=C0=09=3D=90=8A'W=80=03=86=13Ox-=08=83=0B=
+=80=08=1CE=9A=0D=D2=13=97m=13=1E=E5a4=C1=ECQ=82=B3=81=0F=AA=12=97=1DL=81p=
+=C63=9E=0B=8C@=95=1B=E8=C0=06=04=8BA=10\Ed=0C=02np=DD=0C5hi@=84=BB=EBX2=0Bt=
+=BA2=C3`:=1C=90p=869=B0=83=0F=8A=B0=F9#X=FB=06;PfRT=98=F5N=B9E=C7X=8F=A1=D6=
+=E1=BD?=19=14=E7=06G=9B=FC=F7=0C=04Ro=02=DF{=ECd_4=BF?*=CC=1D=00%=05/=F0Bt=
+=A82rZ=DDu=82=C2+|=9Av=B2=96=17D\=98=13*=CA=87=FC=B4=04_=86=DF=05@=E8=C1=0F=
+^0=AA=92j=DA=06IxA=0Dr=D0.=E3=BDd=07G=00=06=91=01=1D=81=E3r=9D=A7*q=F5!z0=
+=03=E4=F3=12=0B=F7P[=10rC`=04M@=1DI=B6!Z=C5.=1B=94=03K=F06N=90=04"=92.~=93=
+=12:0=04=E5=F3I=C2=C7=17=14b=03\=B0(.&uSg;=DC=B2=02%=E1+=FD&H=18aF=05q`z=15=
+v=AA=11h=D1'}=B7=E1=02N=00=05=D2=02in=C5'=00h=03X=D0}=C4=E3=02=05=F3=12d=80=
+L=91a~=1A=15=14=CE=C4&:=F0=1D=C0=D3Y=85=A7*=DDTd=C9=D3=03cp=03/=D0.}=93s=15=
+vRA0=03C`=034=00=06=87q=80=07=B88=FCP=81w=0B=D7=02VP0=8F=E1=03A=10I=C45^N'=
+=03H=80=03C=A0=03Z=F2=04U=90=04hP'>G8=BDu=03=FD=14:=96=92~1tD^=D1=03=82=02=
+=83=C3=A7=17!u6L=10R(=A0=03=CCc=02=89=94E=C7r,=D2=13hAco?=08}A=88=3D=0F=D1Q=
+=19=01=03O`x=C3c<5=A3p-0=169Q=03=00=B5=85/=00=05=F5=91)(=A0<=F8=A2J0=83=87=
+=EA=93q.=80G7=E0=03=1E=C6p=BA(GQ=81=04=DD=11K=D9=B1=8C=EFGd=D0=E1=1Dxh=8DG=
+=B0\vX=88=A5S\r=C2=03=81=83=05*=15=04@=00)D =04<=A0=03>@=8C=DB=06\=FF=C1{0=
+=08=836=C0]K`r=94=18_Z=87=02K =1EH0=07=82=04=13w=F5=02b $$=10=16=FA=97W=CDG=
+=1A*=E0|=A7=01=8B=B1h=10Hr=1BG=98=13=FCrtq=C6=03=04N =19L=86=15<PA=DEaP=B1#=
+=14b"=03-Gdx8Gx=07=13@0=03X=10>v=97=93=E5C[0=93=12=0D=18=93=9DW=13=A9=F4=93=
+=E1=F7=02?=F0=86=E1=D6!=E7h:=08C*=06=A3R=15e.=B2=14=7F&t9?=80=19=9B=B1=8F=
+=FC=882'iY=B7=12=901=18!+=A0=16* T> =03=E0=88=93ibn.`|=99=81=8A3R=91z=B50=
+=7Ff=91=17=99o=19=89=3D@b4=D22!=ED=B4p=91=C7=02(=E4q=8F=B18L0=7F=18=93 =DB=
+=D1=86o=F4!=06(~=E1=87w[=E8=026=01s=0F=C8p}=B3=04<=95qw=18=933y=80=9E=F9=12=
+F=C1=03=AE=E77=E3=A5=13=D5![=1Ch=877=80=04;p.:=F6t=ED"=03Y=E0b=FA=B8B=FB=A8=
+=1Dkq&=14t:N=80=03)=F0=03=A13|04%=FC2PT5=D0=80v=D7N=86=A8"xE=10' =1070=02a=
+=90^=AE(v=18=99=97=B1=C1o=FD=96&9=14~+=12G!G=155=00+9=10=1D=A2=C201=B0=055P=
+=16B=F4=04=0BU\'=05=99=A193Q=D1+K@=999yW=9EI=9F=A1=C9=8D=A1) 0=A0=05:aR5p=
+=9A=82=C8I=9Da=03=1A=E3%U=96!h=F0j,=B2=19=FA=F8=041=D4=13n=91=03=99=C1F=E1D=
+*Q=C7=89=EE=E6^g=A5L_1=9F9=A7/=93=A3=3D$=80=02#=A0=02$p=03=0A=9194=F0N|=05=
+=84=DA=A9=91=DC=C9=145=D3=99/=10=04=F5q+&=94=03=E7bP?=A0=06\=F2=03=B9=07=03=
+P=D0=1F=BE=B1=16B4=1F=0B=E3L=00Z=9F=0E=88&O=A0=7F=97iwU=DA=022C=A5^=0A=941S=
+dH=80=81=08=0A=034=90@F=90.L=D6dqwW=FC=D1=93K=14=AAK14=03=80=01=1Ag=06=05A`=
+=05=BFt=9B=BD=17g1=F0=03J=F0)H=E0=98=97c=03r=B1$=B4=82=03=99=C3=1C=BA=D3=02=
+=10Q<=C0=E3=A2=D3C=3D6z=A3=DB=89$%=A0=02=A3=C2a0`=12=8B=F3!>=80;m9=03M@BG`=
+=04F=F0=03a=80=0390=04t!=16=18Z=14=10=E3=157=C0r?=D0*D`=03=82=F3=A5=91=99=
+=A5:=C9=AB=C0=0A=A6=AA=E4!=9D=87=8E;a[=B8g=13<=C0=048=B0=03P=11=04=94e+=BAD=
+=A1?@M=97h=1Foqf=FD=F2]|=DAnq=F6bM=E4=06Z=A0;;=E1=04;@=03M=D0=8F=91Gr=15F=
+=12=FFQ=04=A1=18=14=82F=A9xi=A9=97zX=F6=F3=02=D2=82=02=94=06:>!=03=A32=039=
+=D0=04B=D0=04X=90=04H =87@=B5.1=11=1E=BB=C9L=7FccW=B08zB=9F=FCZ=1A=93=FB=99=
+=B1=C1=BA=B1=DD=F8*t=98U:A=04QP=05kv.=DF=A1=03=CCFr=99Ai<=C4=1F=CB=C1{N=F1=
+=03L=A5=024@=19=1C=F2t=B8=03=96=02=E9{g=C2=13O7=1F=9C=D4P\=C2"=B6=823,P=04=
+=E4=05=B0) 3yU=A3=F5j=AF=02=B1`?=E2=114=F8QC=D1e|=B2=07=04d=05W =04=92`=18=
+=E7=C1IC=00=A7*=D0=02=D5:=159=A0=04=85=D9=03=E2=97=B1n=FB=B6r=C4=B1=1B=FB=
+=B1=C5=8A=9A=9Fc:3=90=9ChB<=B42=1D=EDD=03D=AB=AC=B6=B9KN=01=B0+=C2&/A{=CA$=
+=16q=84;=BD=D3=A7=11rVX=F9t=F1=B3m6=C9=7FD=DB3=ED=A4@P=90=03nx+=B91`=AD=F8=
+=B4P+=B5=B4=01=12R=E2=1B=E6=03=9F>=F0AF@=04H=A0=05:@=04;=C0=14Sp=94H=E01=AC=
+=13&TA=15=FC4=E0=06M=F0K=D3=08=B7=C4=FB=ABr=0B=ACt[=B7=1FR=93=8F9=8D'=C2=04=
+_1=04H=B5=12=C6s=18=A1d=9BU=90=B3=00"8=EBF=88<=B0=04=B6=F5=15=06=F5-:=BB=B3=
+=D7dD=9A=A1k=0A=14=039=E0X=99=9B=19J=C0=02=D2=EB=92B`=04=E1=84+=C4#=BA=F86O=
+=A4k=AF=A6{=BA=1E=95=A68=F1!=D01@=B8=15K;@k;VR;=80=95R=F6=A71=11=1D=AF=19"=
+=AF=E6=AB=C5=FB=B6=C7=8B=BC=C9=AB=BC=8Ag=80=C2cY=BE=94 T=D1P=87aB4=F0=03=A3=
+=02\q=8A9=91=B10=9C=F4/K`01=B0=04=E7=A6=95=DE=AA=1F0=88;=08c=A8_A=B9E[=B4=
+=A0A*=E79=1D*=10=9D=12=D9|_dh=AFQ=02=D1=F7=BF=00<6=13=82=18=D5=1A=1DH=00)=
+=3D=A0=03=07e=03;=96=B7=99=DB25=10=04'9=054ax=EDr=C1=FCf=CC=9F=19=EC=A5=1BL=
+=87=E88=9F=BC=D8=02C=E0=02=B1=1B=19=0B|=1EP=B7=03K`q=C0%=AD=10=14=16=F3=F1=
+=05=CE=AA=03G=80=04=A9bb=DDj=C3=E7=BB=8Fj=B3c.A+?=FA=C3=B7=A2=03E`=81/=C0=
+=03=BA=C1|=13y`3=A0=C4E#=11=80=E5=C4S+=03G`qR=F0=14=3DI=045@k=B4V=03B =19OW=
+=04?0<=AB=A3=B9=A9=C4=021Q=C1w%3g|=C1=FD)=AC=9DW=9Fk=1C=CC=9D=D7=1CG@=13I=
+=90=03R=E0s1=90=04=02;=13=01=B7=A8=3D=A3c=3Dp=18=B2=95=C0=9EQ+Yi=C3=DA=AC=
+=95=B8=93=04<a=C8=B8C]qj+^=00\=C2=C9>=16=01h=A2Qo$A=04=7FU,=DB=83=8A;"=1B=
+=DCYH10"=86=F7=02YA=13U=E1\=CFE=15:=80=05=86=11=9E=B9=D6N.=90=05_=81=99=BB=
+=0C=B7T=FA=8C=FC=DB(=CC=10=1D=D1=B0=06=8D=B7J=04M=B9m=E2=B6=13U@=88>=903.=
+=B1=03s=A1=13=C7=94/)t=C8N=BA=CD1dn4=B0=04=C0=17=A7=C8=E3=D2-=DBB=83=02=03=
+=A3b=1B=02Q=02G=90W?3=02=0D=03X%0=A3^=87=10R;5Or=84=0C=F4=12=AF=85{=1E=F7=
+=CF5[d=98C=B4=81i=04Yj=C1=0B=8D=BC=80( =12=0D3=F3=99=D5=18d=86=BE=E4=AC*5[=
+=CA=1Cd=FB=C2If=DD=04b=96=06=0D=07=B20=00=04=16=D7=CA=83=9B=C3=DC=AC=CD-=0B=
+=D3v=3D=ADo=A1T,=90=039`=02p=F1N=07=E63=C7b=02.=90O=10Q=04{=09=CA=B4=01>=15=
+=81-0P=1C=E4=02=8E3id=B2=B6=05P78N=90=8B=EB=EA}=0BM=BCi=FC=1200=04=8Fa\=CD=
+=9B=BC=C0=C6=D5=1F=E2z2a=03B@=04=82,d=17=B8=13A=F6t=19=82=8D=FB=BE=F4=02M=
+=A0a=072 =94=F2=D2.=DD06\=BEFt=D7=C4=CDC=EA=BA<=8F=03=04=F2,=10=F5=C1=13=94=
+=A6o=10A=83.=A0=D8=1C=B9w$=C6?=DD=11=14'=B9e3i=03=3D`=06=BA=DA=15=97=14+j=
+=E8}=9B]=8D=9D=FD=D9=9Fi>=8B=E3![=ED=027=B9=C6=8A=D1SY=9D=13R=90=12P0=17A=
+=16=DB=CA=F47=9E;CUAcW=CC=05=17=18=03G=C0=14=C6=A3=02=00=91B=C5@=18-P=1CD=
+=98P=E1B=86=0AS<=84=18Qb=C4=81=03W=D0=D8=D1"=C5=08=8E#=9C=8C =F1=02=87=8A=
+=157:=9ED=99=92#=09=12=3D\=98@=C1r=05K=9A4]=D4=C4Y=A2=C4=09=1B-t=ACp=D1=C2E=
+=91=87Bd=B4`=D1Bi=0B=186t=B8x:=03=C7=0E)6\=B0X=91U=EBV=AEYY|=05=1BV=EC=D7=
+=A5e=CD=9EE[=D6=85=8B=170=8E=FC=D0=F0=11=E3=C5=DA=19k=ED=DE=C5=BB=F6E=0F=B9=
+/=FC=FA=CD=1BX/=0C=BF0f=EC=D0=01=03F=0C=C5=8Dc<=9E!c=06=93%:v=F0=E8=11%=0A=
+=8E=1B7b=C8P=1C=83=87=0E=1E/b$=81=E1=E3G=0C#)=1A.t=FDZ=B6=8A=89=B5!V=B4H=BB=
+=C5=0B=12%:=D6=CDZQ=E5p=944=93=E4hQ=C2D=09=9C4=997g=A9=13=C5=09=185n=F8=B4=
+=91#=86W=B0J](=B9=BE=D4=C5=10=1A=87_=88=9D=D1U=FDX=F6I=CB=BEHk=96F|=EFk=89=
+=DC=A8=91C=86_=1Fs=EF=FA=17=AC-=1B=14=FB=EB/=C1=F0*=10=06+=1AcP=B1=19=BC=C0=
+A=88=1CvX"=87=1CtHb=08.=98=F8!=07=1C=8C(B=06=D0b=B8=81=88!vXL=86=AC=1E=92=
+=CD=A1=16e=B3M =DCp=CB=017=16=82=92=AB7=96=90=C2=8A6=E2~=04)=BA=CEZ=B8=E1=
+=B9=16=FBrz=AE9=9D=94;!(=16j0B=88=1A=E6=13K)=1A=9Ct=8F=06=CE=80=A8!=06$Z=F0=
+=01)=F5=BAj/=AC=A0=C4=13=CA;=B5=CC:3>=BB=FC=CA=A1=08=A9t=E8=01=C0=B5x8=90-=
+=91t=E8=AB@;=03K=901=C2=1Ad0=86=19h =02=07=1Cr=10=A2=89/=E8=F8"=08=0A=8D=90=
+=E1=B1=C7^=F8=E1P=1DhP=81=06=16_DH=05P=1B=92=88$=8CjXa=C6=8Az=E0j=AD=E5X2=
+=01=BE f`a=A3=94p =01H=96b0=A1=85=8B`=D0=C9=84=1BrB!=C9=9A=82]b=06=14=B6H!=
+=06:=8D=A8a=A9=EEZ=E8)=AC=B2=A2h=A1=07!x=1Cs=BD=B1=BC#=EC=87=1Enp!=07=17=CC=
+X+=CC=FA=D8l=13=AD7y=B0=C1=86=1A=E2=A5=C1=C0=B6f=F8=F3?=D3:=D3=C1O=03=01=F5=
+=D7_B=17=D3=C1=C3=0B=83=00"=0B'=9A=10b=07F=E9=FB=84=8C=86=1ES=8Dh=D4=83h=B3=
+=D8=A1=DAHbA.*V=98x=89=1B=B0=FAj=05=A5jxA=A7!\=80a=88=87=86c=EE=C7=9AL=80=
+=01=89=14=ECz5=BA=12=A0=D0=09=C9%M0=E1=84=FDfH";=ABx$+#k=BB=BB1=ACn=C9lOM=
+=17=84=C0a=88=19=06=E4=01=887=DB=1Do=AE=B4=FEs=EB=07 |=C0=01=B4=BFb=A0=81=
+=B0=03=0B=D3=81=0C=19=86=D8/`=00=E5=96=BB=C1=CFd =1B=07=1D=94=F8=C1=0B$=8C@=
+=C2=B2=F2*=95=81=08=C3=06z=A8=D6OG]<=E3=83b$=E9=86'=82=D0=01+=A0=8A=A0=E1=
+=86=1F=90=96=E1=04^=83=A6=01=05=D7=86#=81=85)rM=89&#x*=F9+=1D=96[=B2=04=17d=
+=80=3Dv=DB=95k=01=08=BF=E2ms=AC=1C=CA=14=EBi=A8=81=17=EF=05=CD=7F=98=97=B0=
+=18j=80=E1=AE=1F`=C0a=AD=B3=F2j+J-=94=D0=FBa=86=C4b=A0=FA=87|=F5=F2=8B=87 l=
+=B8=A1=07=C2=E8F=BFn=C7p=E0=A2=08=1Ft=B8A=D1#=C4=E5=E1=B2=1B=E8=AC=94=87!|=
+=A5M"=C71v=1CB =A7=02=16=88=88=07=F3y=02\=AA=E3=83=82=84=C7=05(`=C1=09$=88=
+=10=E2=94=00=064YIsZ`=02'=A1=CC=05G=88=DD=CAnP;=DB=AD=00hA;=C1=13=C2=C4=16i=
+=01=0Fx=C2=E3=8A=0B=AD=05.=1B=00A=0A5=10=83=0D=F0e=847=ED=E0=07yb=8B=9EH=14=
+=04=1F=F4@=0A=16=CA=81=0F=F0S =04=FD=05=06oc=82=A0^P=AF=F4U=B10=8D=91=C1=0D=
+=84=E0=03=0F=91=EF=06>=B0=81=0Fj =87=C8Hf>C=88=01=AB&=92=B1=14=AC=80(=01=14=
+=DD=00=09x=03=91=A5Q2=92=99=C1=BC=92=E0=9F=17(=EB=06=09!N=E9.=08=1D=E7(G=06=
+8=E8=81UN=80=C2=E9$e.?=8B=1D=0Ap=FC=D0=17=89=9DE=86=EC=81=E1=F0.Y=1F=17=EC=
+=00=076=A0=01=10=C8=B7;=E3=D5=00=07m=D1=0B=A0n`=033=90/^6=08=C2=0D=86=E0=84=
+=1C=9C=0F}-=B3=C1'=91=80=19*Z=B1=8AX=C4=01=E6NS=B4x=D5`J=E5=99=01=10.=E3+_I=
+=EC=05;=A8=18=E3R =03=00:."=3D=A0=88=0A=80=90=14=EDU=EA33=00=E7=94=8Ci=03C=
+=B1=ACR=AE=19C=0AX=92=BA=12=80=CC=04::V=B1Jp=04=C2@=81<=9DK=C1=14g=B7=03=B9=
+=D4=8E=84@+H=0F=92=B0L=B3=B80=93=C2=BB=E4=0C=BD=F3!=F2=FD=E0=078=80A=0F~p=
+=03 P=ED{`=83=DE*=C7=F9Jq=F5I`N=CC=81=17k`=B6=04=05=CC=96=B7TL=18k=A9=98=1C=
+$=E1=06<=10=82=0D=B2@=CE"L=B1=06,=C0=8Dm=A4=19=83=FE=05p"-=98=D1=0Ax =A2=C7=
+=88H{6=D8B=11=FB=13=E9=99=F3=015=05.=1B=C1L=8AC=02=13=F8=00.=CC=E9YoJ=B0=9B=
+=1E=08=85=06=E5Q=81d=822=87=1A=A0(=05'\d=EC=18=F9=98/H=AF=A0=98<=E8=D3=98=
+=B6I=EF=DC=C042 =03=11*S=B8U>=81=07}=8A=81=F3=DE=D4=83.t(=074=B0=01=0F:=D3=
+=19=FD4=A1N=01=BB=81bd=C0=83& =E1=0B:HL=82NZ=A0 XQ=B2=91-=8C=10=BCW=1D=96=
+=C5@(=96[A=0D=10=17=A3=17=3DD=050=E8=A95K=85=03Z=1D=E1=0AK=A8=81=0E=08=07=
+=06=1E=94=954=98=F1=81=17=E2=B7=D2=18=14=01=06+=D8=C8N=A0=B3=B3=F9=E8$=ADKR=
+=01=B9X0=9F"=A4j=05G=01=13kT=F0=82=1C=9C=E0=05J8ax=83=A6=B6=BB=E0=80).@=C2M=
+=9D=16=D71%=94=3DlzA=13=A2@6=1A=18=D3=BEtj=A9=9D6=DB=16C}=B1=07K8=FB=82=16^=
+:=D4=93=DA=12=06Y=F8P=0E=80p=04=DD=0A=CA1t=93b=FA=14=03=D1=04=DD=85[=C1=C9i=
+=8Cb=C3=906>=04_=FD=DB0=A86=C6=82=B8=1C=EAJ=A1=F9=0C\&$=04"8a=08=1D=9A=0A=
+=10=9Ex?O=81=E4F2=E9=0D=D0:=B9=83=F34!=06KJ=81y=91&&=90=89=85=BB.8=8D=0BL=
+=18=DE?=1A=8A1z!Q.=F1=14<=F6j=D2=BD=D3R=0B=A6=EC=FB=D5y=BD=F2=08B(=0D=1F=EF=
+C =EDX=86NE=D8=02=10`=9C=83=19=10=A80}Z=8C=85=16u=03=8C=EC=A0R=05=AB%=DD`=
+=F0=BC=FD=9ATw=80=B1K=0B=B5=82=1B=E4j=18"=02=94=08=0E=BE=0Ab6F=E4=06+=A0=15=
+=1AyP=04=DA=A2=B8=BE=16=12=82=11=94=F0=05%H*=89=DE=A4=01=C5X=B2=17=17=94=80=
+=06:i=1E=91d=00=96=1B4=01h=F0=F3K=C9=B62=D7=85=A6G=82=FB=8B=04=DA=09P=F0D$=
+=D8=F5=05Cxe=13=94=B6=DE*s=87xH{o=0Bd=A0=97=1B=84=A1*=C7T=AC=1A8=D3=19=0A=
+=17h=B1=FCL=02=10=16U=84%P=01=0EH=A0=1Cg=B4=C3=A0x=15J=07=BD=8EW=FC=18:=157=
+=9B4.=8D=F1%=A0=ED=12=A6m=06E+:=C8=B0=0Af`=E8l=AE=C0P =0Eq=8BJU=03!=18=024S=
+=D1=C1=BBc=E0=03# X=07=08=B3=AD=15=84P=B0#=F03=8B1=C0)=09ZP=83)=A4@#/=11=C3=
+=0E,=97=14=3DeA=82=95=CA=E3=AC=BB=13-=B3=0Ch./=C0=B5=04k=90O=BFpa=08Q=D0=03=
+=97=D7=A2^=D4=B2=D7=BD=D1=8A=1A=BBZF=D1)!=B2=07>(Cg(=DD=97=C6=D0@=0A;=C8O=
+=FC=80=00=04&t=E1=0DH=18=02=0F=E2=F4=18=18l=0F=07G=80=A8=A5d0=CE=C5=12=01=
+=EB=93l=B7=9F$K=03=07=F7=FC=B9=8A\t=82=0D=88=80=14p=A6j X=09=0A=BF)Rq%=02<=
+=E0=AF=89=C8=F2=A6=B0=9D\^=B7R=A1Y=CC=0F=82=80=035=1F=E1=08A =82=0E=B8=10=
+=19=C6=1C=11=075h=A7=AF@=C6=96=1C=A0=80U =93=CA=0C=9E=00=85=17=D8@t=AB=DEJ=
+=10=8A=A0=B5=93=99=CB=055x=13"wp=EB=13=E0=BBe#=BA=9F=A2i=D0=83=1Dh=C9=06=B2=
+=F6y=D4=BE=85=DD!=7F=AD=B4qI=C2=A4=8F=10=85=1FxR=B7=8DQ=D4=0E|`=C3t=DF`=F5`=
+xB=8BG=B3=83=19X=8A=FBA=E0A=14=EC<=A5=D1=AF=A1=0A=0C=D6=01g|pxx=0B=EC=89Gh=
+=BB=DB=03=D6=84=A9_^=9C3=F8=01=134=AF=E4 =18=01=A7=1A=061N1=99=1B=C8=08=BF=
+=93&=D7z=81,=A8=08=81=A0=81=A1=12=82=D0=A0=81,P,-b=82=1E=E8=01'=E8=01=B8=10=
+=11=9ES=81=16=E0>S=01=02=FD=FC=E0)=92=88=81=1C=08=82=1F0=82=B2=81-=19=B1=08=
+=AF=A0=81=C4=90=81"H=0C=18=B0=AB@=13=15=14=88=81[C=01=15@=11K=A9=01p=9A=81=
+=1Cx=82=1F=A0=81=FD=E9=B9L=02=9E=AB=10=0B=17x=82v=A1=8F7=81=81* =02!=98=822=
+@=82L=CB=81=F8a=8C=18=98=82 `=02=CE=E0=0C=F2=C1=01$@=02=1F("=1F=D8=81=0C=DC=
+=C0=0Bd=82=1C=90=02Q=E2=02=0F=B1=8C)(=02+=00=82!P=14=13=8C?=CD=92=AC(h3=06=
+=F1=AC=B6P=14=F2=E9=BD=19=B8=01$X=1E=1E=98.=C8S=82=D5=BA=8Dlr=AD#(=1C;c3*=
+=008=11=83=88=1E=D8=02=80=BB=BBy)=0C4=A2=81=1FH=B7=C4=B2=AF=C8=B8<=0C=F3=01=
+=C3!=C1=1C=A0=82=06=AC=91=AC=B8=0C=0D=99=BE=18=90=02=92=A8=08=AD=F8=A1=1E=
+=C0=9B=AF"=1F=11=A1=82=F3y"=18=10=82=E9@=88=15=18=02+=B8=1B=13=FC=03=02=A4=
+=E9=AA=E0=AB=92i=99!=F2=09=8A =98=1E=041=9F=1D @ @=02+X=82-=D2=01#=80=BF=CF=
+=D0=B6i{=A59=CB=01=1Ep(.@=02P=CA#=CA=F1=10E=19=02j#=82"=08=02,=A0=02(X=02(H=
+=BF=1B=A0=9C=F8#=94=12=BC=01#=D8=C3[=DA=01<=EC=01E=013=1D=12=11=C9=A8=81=81=
+=92=81Ps=C4=87=A0=18=D7=DA=01=92=F8=95=82=98H=FF=C1=C4=14=08=B5=0D=83=88es3=
+=C2=90=81&=C0=0Fc=FA*=13S=90=A3=B0;:=F3=15 @=01=E3=C1=811H=15Oi=01=D6@C=1A=
+=18=BB=1B=B9E=16 =1A=1D:=94*=B0=AF\b=02`=B4=81JI=886=92=82e=E3>=19p=82 =B8=
+=0A=CD=F3=B7=B8z/#=14=0A=96[=8A=EA=D0=01mC=13=F18%=BD=90=01# =9F"=F8=81'=D8=
+=02" =82=D1X?=19=F8=02$=B0=10=F8Y=A5x=F9=A4\=FCz<=1Cp=02'8=A6"=D8=01=19=F4=
+=90=C5=8A=1F!P=82=05{=02&x=02=19=84=1F=F6s=10=BB=E1=9E=D5=E3=0BBA)=19=E8=01=
++X=94=0B=E1=812=9A=81=89#=02#=08%=BB=9B=11N=C1=8D=97=0A=A7IR=11=BF;4L=F4=14=
+=87P=81=85=03=02=C68=1B=BF=88%=13=AC=813=C8:=8C=FB=0A=D5R=81=9E=00=99=0D=C2=
+=01Y+=A0i=E2=AE)=B9=1A=8C=B3;=93i=8B=A1:=14=0A=89=82N=FB=A4,=FCE=E3y=02=DA=
+=B8=C18=0A+!=BB=81C=81JgD=8A=DDh=9E=1D=08=0A=B6pJ=F1`=99=F1=F1=1A=B7=F2=0E=
+=1D=88G=C3=C9B=1B=10=82=A33C=1F=A8=9F=C5=8B=8C=1A8=91=F9)=82'x=A5W=D2"=8D:=
+=A6$8=82=FF=D2=C7i=CBF=F1=09=02%(=02"=106=1DH,=9C=F4G=06=D1=9E'(=03=C3=04=
+=C4=BA=D9=C2=C9b=B1YYH=C9=801=E0T=95=87=FC=E4=1F=02"=02=11a=C5=E7C=1B=D1=FC=
+;=C0k=1C=14=88=02r:=0F(=88=018=F8=8F0x=81+=80=BD=8D=EB=01=AC=D8P=15=E8=0D=
+=83=88=89=9D=08,=A4p=01]=DB=1C=AFz4=0C=AB=08=17=88=8C=80=AC=1F=F5<H=FCH=1B=
+=C9r=01 =C0=18=D7=AA=0B =D0=0A=1BH=82'=A92=B0X6=E6A=AC=DE=EB=92=CBC=AC&=1C=
+=8A=1E=00=02-8%=EF=11=02=AF=11=0A=E3!=1Feb=82W=CA=01C`O=CC=08=83 @=A6d=0A=
+=03=89=0A=A9=FAtS0=B2=AFq2=02=1F=F8B=BE=A46=E93=C3=1E=E0=01%=A8=81=A1=B2=94=
+=C7=98=C1=09=BB=1B%Z=94.q=D0>=B3=9B =98=95=1F=F4&=19=D0=AD=FD=88=16=AFh=C1=
+=15@=0C=16=E0=81=AC=C8=A5=C5H=82=E8=B1E=9D=CA=18eY=88=1B=90=01=19Q6=FFH=93=
+=16=C0=81 =00=0B=DB=BCE=90X=01cA=81+9=81!=08=16=89=FC=B1UY#=B4=BFz=C8=1D=D8=
+=01=B3,=02=D1B"Sb=8C=1A=08=13=BF=1B=88 =B0=08E,=B6=A4=D8=A2=19=88=8B=A2=14#=
+=1A(=03=DD=A2=81)8=02=1Bx=81*=90=17c=9A=01sU=A6=E6=01=1B7=EC=81=A0<=D5!=88=
+=13=A8=CB@p=AA/=1B=88Gx9=D7=8Dz=A5>u=A8=89=8A%A=E5G$h=82 @=82(=08=82)Y=C8'0=
+=14D=E1=BA=18=B8=82!=18=02=1DX=BD$=E2=93=81=C1X=C7=10=02=CA[=D4=A2*=AA=C9=
+=84V=D4=D2=0FC%N=1AH=82=EE=83=1Ej=15=D1=D1=CC=18=19=10=A0=838=02=01B=0C=D3=
+=F8A=A5 =B6A=D37=8Ex=A7=0CZ=01=1D`=0E=1B=DD*=1D 2=AFP=B2=FEI=CD=18=C8=0E)=
+=10=82%=C0=02p=E3=A21<<r=E9=1FN=BC=C5&=C8=01=9F=EB=A4=D7K=13=16@=8E=AB=A1=
+=C1=C9=9C=90"@=02=ED=E9=AD=AF=BA=D7=D5 /=A8=FC=18=02%=9A=A8s=8D=9Fx=E9=82=
+=1E=C8=81c2=DBP=E2=92y=E9=D3=BC=D5[7}K/=8A=9FD=A1(y1[=1D=98=02!=F8*0=13;=1A=
+=A04 =82*=80:=AE=CB=D8=8C=ED=D8=C9-*=A3z=CA=15=88=02#=E0=D8=C9=EA=01#=10A=
+=11=C9=C8=DA=80#=84=D0=C1=E98=81=14H=D4=83=90=01=AB=F8=3D"=E0Zd{U=19=D8=01=
+=8E0=89=8E(=9Dw*=A4=9D=C8=0A=F5j=01&H=0F=D1=E9>=98=E2=01&=10=02/h=02=A5=95=
+=D7#=E8P=C2Q=11=90=A1Z=15=F8=01=CD=BC=C5=A7=E1=02=B0(=B2=A4=00=0B=F8`=8A;=
+=E0=8B=1D8=82/=B4W0=92=97t=93=D7=97=A3=13%*=A6=BD=3D=02%=00%=BA=AD/=BD=85_=
+=F8=D5=D3=FA=EC=8C=B6-=C9=1A=10=17=C4=08A!x=82&8=14=11YVE=C1=0F]=84\=C4=8B=
+=DC=7F=9C=DC=0A=A5=CC)=DB8=EE{=0C.:=02(=FCp=E0s=0BM=D6=1A]=DA=C3=E0=14X=A4=
+=E6=19=88=13@,=A1H=81=19=E8=AA=16=98=81X=F1=01=95=D0=91=0C=8A=8E=17=D8=9Bji=
+=01#=80=81=CEC=01=87=81=81"=B8=81$R=3D0=D3=DF=F5{=8C=CB=A0=AD=DF=9BZU=11=E2=
+k=15=8B=D2=88=9E=1BX_&(=8F=AF=D2=1C=0BY=A5(PM=C6=C0=C3U=BA=DF=BC]=A5w=AD[=
+=B3=8D=DF-=E6=D7=F9],=1C=E0S=1B=E8=01(=F0=01.p=18=D2=D0=81=89{=02;=E0=C5=C5=
+=BA=90=B4\=D4=03=C6=D8E-"p2=AA=1E0D=1A=A0=83=A00=8D-=DC=01 =D0=A8=87=C4=DE$=
+=82=02=11=1D]=D3=C5`=B4=02=9A=1C=D41=1A=08=1A=17=C8=A5C=D1=09=16=D0=01=95=
+=90=81=94=D0=89=93=99=0A=16 =02=BB=D0=01=09J=01=E5=C9=8E44Td5\=A2z=8CO=E2=
+=01=1FP=01=1E=10=E2W=FET=A9=04=8B=11=DE=8D=84=9D=17{=9D/=E0=FC=12=82=CE=C0=
+=02-=90,<=9C:=FC=0CJcR=D2,=E62.=96=DF=F9Uf=FAU=0Dw\=94<=B4=8C4=F4=01=F9=CAC=
+=92=DC=01I=81=A8=81A=91=03=EEX=19=F8=01=D2=D8=81=A21Wcj=82=B6P=D0Tc=81G=CB0=
+U=85#=0C=C6=B5=13=0A=96=12P=01=13=E8=81=E5=B8=8A=15=18=A4=DEx=81=D4Aa=12=18=
+UN=16=9DS=CA=A7\=13=02=B6=88G=1B=98=A83=F0=81=CA=A8=01=F0=13=910=E8=09=CD=
+=83=E5=89=B6Hg=EC=8Er=81=BA=1A=D0E'x=16( =82=1F8=02=0BY=D2=091A=1F=18=D7-3=
+=E6=94~=DF=F8M=DF=A0\=E6=97=BE=81+P=B0=82Q=14h=DE=81=88e=82#=A8(O=C2=81o=96=
+=92=04U=8CF=95=DCE=95=8C5=00=82=ED=03.=CE@#=94U=0C=AB=EA=91=054=B4C=16 =DA=
+=03=9A=DB=91g=96=00=96=E4@=9D=9D=05=92=958=81=B0=9A=01=9D=FC=08=82=87(=01!P=
+=0E=13H=01'`=0B(=B0e=F2=F0=CD2b=C8$=10=D2=DC=A0=E8=E9=BD=D2=B0=88a-=99=97Ir=
+=8A=A5=A5=03p=EB=10%=C8T=19d=CF"=C8=02=93T=E9=94v=E9=97VfQb=EC=EA=BB=E1=9A=
+=8E>&=F8=E8 H=02-=90=02)8=91=1Cp=82n*`B=99T=04=AE=D0=1E=C0=AC=0Bi=82!(=02=
+=9D=06=BB!=90=82 =80=8C=03z=EA=95=D5H=8B=F1=C8=19&F=D1=81=E7=ACR=92}=D6=D9=
+=11=10=01=11=D0=95=12=C0=01=DD=0E=09=B5R=01j=DC=0D#=00?=B3=05=C2=C8=98=81"=
+=E8-=CB!4=E9=ADk"=9E=CD=FA =D7=B6=C0,=CB=AE=82nL=02=CCj0=C9X=82?=1E`=F7M=EC=
+c=B6bK=1D=9F=C7=8E=1F=BET=E6=CE=D06j=AB=E9=09A=18-=10=D7! =ED=96=FA=01o=02j=
+a=EC=EF=C5=10;m=81O=0Ba=82 =90=82{=CC=DC=1C=FB=00=88=1DOb=C8=88Ap=06=10=16*=
+=16=A6h=E8=F0=E1C=14=12'R=AC=98=02=C7=C4=14Bf=C0@=91=E2=85=0C=140T=98(Q=82=
+=04=CA=94#V=92X=E9R=04L=972Y=DA8I=02F=CA=9C&K=98=A0=E1=A2=05=D0=19In=D8=082=
+=83=C6=0C":b=0C)=92=83=8A=8B=15,VP=AD=B1b!=D6=ACZUP=ED=EA=F5=EBW=16b=C7=B2h=
+=F1=D3=85=0B=1AHr=F88Rd=88=93 :v=CC=90a=F7=0B=8D=1A;j=D0=E8=EB=F7/=E0=1A=82=
+=07=13=AEa=E3=87=12(D=89=DA`l=E3=F1=0D=1CAp=DC=A8=EC=B8=B1=E5=CC=91s=00=89=
+=C2=E4=07=10$h=80=F0=F8Q=C4G=8D=1FB=0A=C2h=ED=BAu=8C=A51^=D36=18=E3(=0E!HvH=
+n"D=8A=13&Cx=E8=08B$HA=BB3=8A(d=08=F1y=C3=8A=D2)=AA=80=F1P=85=0D%;z|q=01=FB=
+=D2=85=89=F0&U=CE=94=093fy=13-e=E6Li=D2=C4=09=144=C4=B6=88a=C3=05=16=18:=A0=
+=E4=D5=F1=02=C7=112=1C=D5=C2X=3DL=B5=15=82=0B=81=B5 Xd=8D=05=94Y/=00=D1=D8=
+=12>,=D1=83=19;=E8=80=84=0Eu=09x=94=0D=80=89=98W=0D=A8=15v=A2=0DBtA=C5=10@0=
+f=C4c1V=E6C=0EQhv=A3f8pf=DA=0E?=F4`=C4=12E=D0`=84]=B6=19D=1B=0C=06=E1=00=C5=
+=91H&i=D0=0C8=00=01=84=0E9=E0=90=9BjE=1CA=C5=0E;lT=90A9HQ=03=0E:\=A5=02t=D0=
+M']CR=A5pU=10=A8=B5=F6=82=0BD=0D=89=82Y6=ADW=DEy}=96=F7'K:=B5=90C=0E=DA=D1g=
+=03P.=D8=E0=D4=0D?=045=C3=0CL=C9=F0D_S=9D=99=A0V=0Cj*=95=83=F4=01=F5=02=0F=
+=A8=FD=80=04=12<=F0=B0=03=0F=3D=F0=FB=05)=AB=90=8E=F8=D7=89=83=D9=80=A2=0D>=
+=F8=00=04=13=95=E9xYe9 qe=0E7R&l=99=94=05=DBD=89H=D9=05=A6=91=CD=DA0=84=0F=
+=B29=F9=A4=0CJ=D8&E=10=BA=0A=E1=A3=A9<=D8p=AA]=FE=D9@=04=0E1x=DB=C2=0E[=B9=
+=90f=0Akz=E4=90=0A>$)=04=0EI=BEp/Z=9F=1E=E1=C2=09'=D9=C4g=9F=E7=01=FA=A7N%=
+=9C=90=C4=0A6LA`Yb=B9=80C=94=0C=97=05=04=0E-=D00=ABU4P=85i=A6`=B5=B0i=A7=9E=
+=BE=D0=02=0C=3D`|=D8=11>=E0 E=88=AD=BA|=94=88=82=3DV=C3=0D2=CB=8C=F1=CD=8D=
+=F9@=05=11-F=91m=8EB=DC=BA=05=8E7=E8P=F4=95=95=CD=BC=EAQ=CB=E6=D0lAu=D9=A0=
+=03=B3=D3=1AD=03l2=0Ca=04Q8=D4:=E1=0F=CA=0AH=C3=11-=D4=D5=97=0C=A8=B5=80=83=
+B=ECBd=11D*=D4=9C=FB=DD=0F=B3=C5p=83O?=91=FC=83=0C,=F0=84BI9=AD=14=B0=9F=03=
+=CF=D4=9EI'@(=D6=0F=0F=FA=F0=82=83=F3I=F5=F1W=1Cg=C5 =0Bl[JU=C8=13=F3=F0B=
+=0CV=CE=10=B6`R=F8pC=13<(=11=06_H=BD=0Cs_=B2=FA=D0=C3=85=95=FD=00=99=101=CA=
+=A8D=CA=3D=F8=F0C=10=C1f=F6=AB=10_=08a%=0EJ=E4he=D1=991a=04=1C=3D8=C1j=0F=
+=CC=DA=16=AE=0E54ku=0E=AE=C5=00D=92=13=12=D6=EA=B8@X=B5BB+=C8=90=95=DBn=A3=
+=197W@ =05e=0E60=01=D4=83$=CF=E0B=0D=EA=CDD=B8=C0=0C=E7=92=F6=90=80'=AD=C9=
+=97=83Z =04!=88=A5+=94=93J=E5,=A7 M=B1=80Fd=91X=02=B9 =BF[=11=C1/58=02_=0A=
+=D3=97=D7=C1=0EV=82=01^=A8~p=03!=E8=C0=07=BA=93Q=9C|`=85=1F=FC=80=09=C3=FC=
+=A2=0C=0Ev=00=04,=F8@.T=18=96ep=10=05=1D$=EDF=C1*=CD=11=BA4=17 =E4=A0.=06)=
+=92=0C=A6=A0=83=B7=10=A1=07F=9A=16=92=A2=F0=02(t=AF=087=98=81^z@=03=A2=08!*=
+W=91=0A=FB=DA=A7=C6=E7p=A5=06< =88=0Cn =A4=D91=AC=055SB=0DX=E0?=80=11N=80=
+=03D=89=DFJ@=03=03=C2=00=8C^=A1=0F=E46=D5=15=09N=10d1=A8A=0B =97=C7=90=01=
+=85=06=C7sB=0F~=D0=BA=D6=89=88=84%4a=0D=8C =04?=8C=C1V=AA=B9=81=12 =A3=AB!=
+=04a=09NHB=10~=90=03=DEL=A6LD=F8=DD=0F=8E@D ^)=0AC=B8=12=10=83=B8=851=00A=
+=08C=98=0B=11=92=C0=03=0F=C9=80l=90=EA=01eh`=BD=D9`qN=F7=A2=13=9D=EES=03=B4=
+=DC=C0=05e=A1=01=10.=95=C65=92=B3!=0A=FA=C1=12=945:D]P=9A=FB/=A8=C1=BFX=F2=
+=92>=FAQ'4CA|<=C2=95=B0=B0=00=06/P=A489=06PN=F5=05=0A=F8+=8B=C4 =E4=9D=18q=
+=F2U=9E|U=EC=04=03-=1F=A4=CAVJ=18V=9C=8A=F3=83+@=A1=0AK=08=C2=10r=F0=03=1D<=
+A=07K0&=15=8E=10=04!=F0=E0=97=C0=BC=12,=91=17=CC=C8=E8h=08=A8=CA=C1\D*4H=11=
+=81=85<XB=95|0=CDjZ=13_z[\=0B=94=E0=95 =04=B4=9Cj|_=0A=14D=83L=DA@=06< =93`=
+,=D5@=15=C4=E0=05%=10BJz =CF=11=D0=93`=883=89B=FF`=AE=A8l=AE+=0D=D3=14#=1By=
+=C8=06=B1=C0=05=A7*=1B=84n=00=A1D=B9=00=06$j=A8_<=C9*=0F=8EH0M=B8U=0Fx=D0=
+=84=B7=00=EF=06=3D=F0=15=12~=87=04'=18=81=08R=98K=10=80 =05=E0]a=09tP=C2=D6=
+d=9AC=1C=10=FC=01=08S=10m=10=AF=C4B=9C=12=8AP:=D0=C1=14`=9A=06=CE=EC=86=079=
+=A8A=12vP5=A2=CE=E9>t=92X=047=E6=1C=A7=B6o=05}AM=0C=AA=17=03o=C2=01=06=0E[P=
+=0B=90=E0=82=9D,=17%#hIY=0F=D7=93=18=AC =05=F6i=81=0C=08=04*,=04=A1=06=CE-=
+=0B=E6=C0=12W=B9~=85=A9n=A5O=0Ex=00=04:=3D=AC=06#=D3[_q=F0=03=13=09&=B0=82=
+=FD$=89@i=18=1C=14=A1=86g=80=A5=14r=F3=04=1A=B6=E5=07=888=82=13LE=04%=00=81=
+=86S`=C2=11=B0P=04=B9=B8=D6J98=0E=14=E4=F2=D2=11=BF=D4=B5=1Aj=A1=10z=10=84=
+=04_iU=D3l=92=93=A0=D0=1A)=E6=C0=8B=DD=1CY[=03=8A=15=E2=D2=E0=A9\=01=EF=0A=
+=B8=BB=83=BA=86mAb=99=0F=BF=14G=A0=12=8C=E5=04=01=13`J=AE=CA=82G=FET.W=E0=
+=01=B9=9A=D9@E=FB=AE=17+=9A=AA=D3=05=81b=83#=80=01=065=EB=C1=10=D0=D2=85=B3=
+=F45F*L=82=7FI=08=D1=AB=12=C6D=8D=D1=01=10=9E`=85=93F=AB=08Lx=82=ADl=15=D9=
+=C4=1Ea=09J=A0!=0D=95=80=04+`=E1T=A7=FA=01=114=94=D9=1Fh=E8J=14#10[=AB=83#=
+=B8A=08L(=C2=166K=A8& =85=06=CD=B2=8D=93=08R=83=19=98=9415=08^=0B=8E=D0=C0=
+=E6 =88=B8Pm_VZ=A0=A0 =E4x=05-=E8=01=1C=B6=D0W=7F=A2=80+3=C8AWHP8(=A3=E4=06=
+%=80A=1Eg=C0=02=19=04!D=3D=F8=C9=96=E1=DA=E5}b=EE^=07=85=C2=10=F2=82=14=BEL=
+=E1b.=EA=ABwlz=83[=FAN=08#=943`H=9D=17=A3=DD=16c7=B0a=9C0\=04"=18=A1KE(=82=
+=16=800=BB@=F7=A0=E0=816e=85=07=DE=D3 p=81=09Q=D8=EF=940I=FB%L=8F8=07=C7=EC=
+=E1=10=96@=06=D0Fz=96=CB|1=AA_c=9B!h!x*"=C3`=BE=F8=CFE=D6=BA=9Cm#'=82vp=A9=
+=B0|=11=0A*=8B=0D=9A=ECX=82=EB=06=D0=8F=D7=B5I=09=BA=AB=82=13x=07=066=90=02=
+Y6=B5m=F6=FA=9A,.h=02=0Cp=F0=93=9A=BD=FB=AF=82=E9=9E?_=00=031=E4=B0=09;=18=
+=0E=0F\=F7:=13zM070=82=C9@h=98=1FD=D8wK=08MbQU=05=DF=15=BC=EEv?8=DEsy=84)=
+=ADt=09]P=C2=8A=89#Z=1DU=DCW@=A0p=10=90=D0=04=DFhH=08;$=0E=A4=84z=EA=C97=B3=
+J<(p=08=87=93m=1D=0F=B7};=E8=B1S=B52=03=04q=CE=05A=A0=81=0E=94=E0=83=19|=AC=
+=05=F1D=8F=CF=03=C5=93=9D=80=CA,@=E1=81=0B=9C=D0k=AF,=DD=C8=AC=DFA=0BV%=98#=
+=BC =07=AE#=91=C9=FB=9A=F0=18)=C0 =07Z=8F=81=13p=9AX=B1=8F=DD=92^=F8=96c=A1=
+)=B4=9DI=C1d6=B0=D2~=E9Nr=1F=B0=D4Tv?=BF=EF=F0~=F0=D2N=09=08AH=82=12=A4 =9C=
+#=84T=B4=AC=ADxH=F1=FC=03#@=81=09X=E8R=D8=AD=9A4=81=DC=E4AI=8A=E9=00=0F=94=
+=1B=AB=D0L=0B =8A=13=0C=01H=E8=D8S=CD@w=D9=9A=E5pN=D3=C8=C0=84=DC@Y=E4=80=
+=09=C4=DE=C0=9C=95=09=88=04=06%=1D=A7=F0=DE=B6=1D=12=13=94=C5=AA=D9@=0C=D0=
+=05=0D=84=0Ae=C0=D1Q=E4=12=DD=11Ae=B8E=D6 `=B4=00=C1CQ=D4=EC@=D3n\=81=10\Y=
+=DD=8DJe=08\=10=DA]=B7=A4=CA=DD=A9=1F=A2=EDW=A0E=A1=FBM=81=11=FC[=A8=B4=96=
+=16j=07=A1=8CX=12=94=09=10=F4=08E=A1=0AH=9D=0D=01R=1E=0D=EC@=0E|=C1=0C=F4@=
+=87=B8=CA=BC=11=C4=10H=1B=E7=FCu=1E=FC=04=DF=AD=95=93=04=AD=C0=F9=F4=C0=0Dt=
+=95=148=1B=17=90=C0=15=EC=09=A0=18=E2=E1=14=0C|=BCI=09>=C8=CA=B1\=97Q=C5=C7=
+=B0@=88H=81=0Ch=C1=10=0CA=F5,G=16=D8=94=87=D0=80=0FlG=0FL=01=0D=D9P=13=19=
+=01=94P=81=80=10=C1=0C=1C=C1=EB=84=DD_=CC=0A=B4=A0Pi=18=C1=11=98=86=A5=99=
+=C1=15=14\=136a=DD=F9=80=14 =81=12=1E=DC=A8=DCb=14R!=83=99=8A=86=A8=A1=A6QI=
+=17=92=98e<=86=15=08=C6Q=F0=C0=B2=14 6*=C7=87=90=0F=0F0=C1=F4=E4=91pq=0C=BC=
+=A8=80BL=D9U=10=979=ED=E19=E2SI=C4=93=1F=9D=84Y=B9=07O=F4=95=0E=E4=85=C44=
+=0Ex5NW=D8@=1Db=8AW=CC=CA=12=A4M=0DT=C9=17=99=97=1C=85]@=DA=05E-=01=114=C1=
+=11=08A=10=F0F=0DX=CF=87,=CB=7F=1D=D7=BC=A1Y=0F=14A=13=FC@=01=140=D1=87=0D=
+=C1=134Z/:=E1=DD=B1=DD=10=FC@=15L!=15=14=D81=BEd=10=18=C1=10=94=C1=15p=C1k=
+=11=07=11pHka=DA=12=DC=00=120=CD=00^#=E5a#=1C-=8Br=F0@f=D9@=13tS8.D=0D=A8=
+=8BVh=01Q=D8!=CC=AD=D7=0A=C8=0B=0C=04=0E"=86=E0=0A=90=07=01=95=80=0B=A8=80=
+=0C =DBm=1D=D5=0B=FC!H=CC=87X=9CO?=92=9ET=B0Z=0CT=0A$=95=85=90=B8=C0=E7=BD[=
+A=9C=D6=FB=85=C1=18=9CT=95l=8D5=16=A5Q*=C7C=F5=C5=0A=F1@=1E=D4=88=14=94=01=
+=13=08=C12=96FI=A2=9F=C1=F9=80=128=A4=11 Zh4=01=10,=01=A2=09c=85!Zg=0D=8119=
+A=18=0A=01=11l=01=12=10=81m=0D=9E=1A.=C1=0C=FD=A4=0D@=CC`=16=E0=1Ff=E3`=0AH=
+=16=C8=80?e[V=D0=1AW,=04=13dE=CD=C8=C0=99=FC=A0cT=E9=1A#=F9=80=3Db=84W=B2=
+=C7=E1DE=01=11=90<=8E=E0=09=E4=C0Q=A1=05=0C=84=9DP=DD@=13=11=08$^=E0[J=DBTL=
+"=E7=C8Z]=B9@=E8=D0=80,%=09=0E=A0=C1=11@A=11,=10A=F2=00=0E=DC=E6`^dD=D9@=85=
+=15=01h%=01=14t=09=00v=8B=EF=80F=A09=C1=C1A=01=11x=C1=DE=B5=9F=A2%=C1=94=80=
+=C6=188=E8gN=09D=AE=941=19=C1=83V=89=16=96=D8=02%=C6=E5=AD=DA`=09=A5=914=13=
+Q=0A=E6`=1AA=0Dd=01=B6}=05r*=88=8B=AA@^=80=CE=A5=A0=E3V=AC@"}=E7=0A4=8EX=E8=
+=80=10=98=80=D4=1D=0E=0D=EC=09=09=B4=00=0A=AC=C0=0C=B4=87z=14=0CO=9C@=D9=9C=
+=05b0M=A3=B4a=91=B4=E5=E5T=0A=E7t=05=05=9E=A0T=D8=C0=AF=E4=85=0C=D8UQ=F8=CE=
+=138=C1=89=89=91=870=D3}=16=A6=07=DD=80=13=FC8=01=18=18A=13=94=CA=A9=00h/fR=
+N=1EA=15=D8=DC=0Fd=01=13@=01=17=00=1E=A8!=81=16h=01=E0MI=130=C1=14=B4=9F=A2=
+=BA=9F=10,=C1=14hA=12=98=A6=1B=EA=80=15h=A1=A6M=01=1B=CC=E9c=91=1A=9A=DEf}=
+=12=A5Q=D2=05=A4|=08=0DP=8A=A5P=A0=0B8=CA=99$=CCU=B0@=A4L$=DE<=E5=8E=85=1E=
+=E9%=C1=E6=1D=92=1C=3D%X=FDQ=10$iN=C4=80=0F=F4=8B=11=A4=84V=16=D0N=98=C0=0B=
+=0C=81=0D=D0Wc=00=0FR=C8=D7$QE=15P)W=F0=C0=03=01T=E6=D0@=84=FC=04=11=94H=14=
+(c=E3)g=AB=D8=E76V_=0C=EAg=11=18=81I=F6=80j=02(=AA=F8=A2=98=06#=15=FC=80=14=
+X=81=11T=81=13=10=81=16D=C1=11 =C1=98=0E=01=11P=81i=1EA=12=B4=C8=A2=16=13=
+=12$=01=194AO=F5=94=86Z=AAk-=81=12=ED=80=12=FC\fP^=81E=0A=88=0E=F4=D4=19=DE=
+g=11=1C=C5`y=D3Z=D6=95=1BJ=A2=AB:=CCQD=01R4=95=1E&=88T=F4=CD=C7@=C1W=B8=C0=
+=15,=17x=EDI=0A=B8=C0=11=A0@t=FA=0B=05=8AE=E0=18L;=EE=04O=F8=C4G=A0=C5=BD=
+=D0=80&.=8B=0DX=8A=97Q=E9=CA=B9,X(A=1D=D1G=B5=91=09=0EX=C1p=00=01=DA9=E6=E7=
+u=AAE=0EVC=A9l_<=CB=130=81=F9u=8B=BB=BE=AB=80=0E=A8=14=84=01=E0%=C1=13=18=
+=81=11 A=BA=D2=AD=11=8C=81=11$A0&=C1=16=FC]fMI=A1=BA=1F=10=0CA=14=18=C1=13=
+=F0=C0e=3D,=C4=E2=D4k=3D=AE&=B1=CA=1C=0A=C8=D8=10=81=0DxH=01=DEf=1B=EE=C5T=
+=F5=C5=E7=B0=00=DE=94J=F7=94=0B=0Bpn=0Dt=81A=BC=C0=1B=80=8EV=B4=AC=CB=B2@=
+=0D8=DF=96I=0EU=E0@=BF=B4=C7>=A1=8B=BF=F4=CB=D3=FC=E8=00x=BC=C7=0B=A0=00=D1=
+=1E=EB=08=A2=00=12d=DD=93=10=04=DE=1C=C8zY=85:R=C5#>=08=0CX=C8=C5\=8C=AA=F0=
+E=D8]=EE=A8j=AE=CB=C4 =0E=10=96=CC=F0"=B8=BAk=13.=E1=C3!=01=134A=C1=FE=1B=
+=FB=FE=1B=DE=D2=ED=11=F4=AB=18P=01C=06=81=FD=06=81J)*=0D=C5=DD=E3j=A1=16@=
+=ECk=B9k:=E5=C5=F7=82H=EC=1E=01=DA=10`=D8=CE=00*-=01=0E=F0@=12`L=19=D9=C7=
+=00B=CA=10=AC=8D=80=14=81=15=10=81=0C=F4=00=E7U=E5=E5\=0E=0B=BC=00=A4=9C=17=
+=EC2=CCO=00=C1=09=A4=00J=A8=87=0A=D8=91Y=10=01O=B0=CD;-E|=04o=C5=B4c=BF=BC=
+=07|=E4=91?A=CD=17=82c\=8D=1E#Q=10}=A8=CA_\=95=AB=8C+=F7=82=EC`%=D6=DA=0D=
+=06=83=CD=CE=9C=D2=A9=F9=15=DC-=DAJ=14=CC-=FF=11=01=11=B0=EF=FB=F6k=FC=8Eq=
+=FC=FC6=81k=0AA=11\=81=14@d=FBqA=12=E4=E4=E5=3D=EE=E3=AE=A6=16=060i$=D6c=CC=
+i=1EP=C9=B7=E6=80QR=1Ea=0A=01=1E=9D=CA\=98=CA=ED=E0=80=D6=A9=A2=0C|=8B=11=
+=D0=88=A9=C9=C0Or=04q=B6=1C=AE=D5h=8E=F5=D3=13=00A=F0=99M=0C<=08G=8C=07>=A1=
+=80=0C=EC=C5=0B=A4=00=D1=11=08=C9=E8=DA=FED=D7=0E=C4=C0=09=C0=07x=84Gx=9C@>=
+=BD=D6lPM=0D=F0=C1=0Cl=81or=0C=0FD=A2=91=D1G=9D=18=05=CCh/=137q=09=D5=00c=
+=A6=18R.=06=F0=04,=99=92=EF=15N=D8=14|=F1=16=E4=92=13=EC=9B=17=7F=F1=BFM=D6=
+=11=D8=E2=12$=01=12=1CA=17=8Cs=BFV=81=12P=C1=15D=EA=FD=DAo=CF=00=AC=17=BC=
+=C1\=BCV|=BEV=1D=EB@=0F=EC=C0=14\=E8=0F=E8=C8=14=ED3=AAlo=91=9CZ=B8=E0@=F4=
+=B1A=A5=EA=80=16=F4=80=10=FClA[=C8=A6m=90=89=12=10=E1j=C8=00s`=A9=04V2V4=C7=
+=0A=18=1F=E7\=89f~A=BD=09=01$=E5=00=81=98W;=9A=80=15x=8B=11,EC=10=84X=B0=CD=
+=E4=D4=15=0BP=81=BD=BC=C0+=C7=80,=D3r=0A =85=0A=B0=1A=1Aq=15=10=14q=0A=D2=
+=15=E8=12E=13=B9=0C=16=B0=0A=B9=96k5=0AF=AAtI=DD=F5$wV=B3=16l=07=80j"=15=D4=
+=E2=100A=18=A8A=104=81=13l07=A7=AB=96=A4=01=14 =C1=14L=01=18=90=CA=14=90=0A=
+\'=C6=14(A=85=B6=F3=02=19S=10=F0@=A6=FE=C6=89=D51=F9uA=E21=01=1E =01=17=B2=
+=C5=E5EA=A7=022=10(=87=86|X'N=81=13 Af=D5'=98=C8=00=BD=04=81=EFP=1B=1D=F6=
+=A3=8Fy=D9$=9A=9A=81=B8j_8A=DA=80=AC=C9,A=BE=A8=CC,=FF=C4#=ED=00=0C4=C1=9B=
+=185}=18=C1=0B=FB=D0=C0=D0=9D=00=13=D0=B2ngD=05M=819=B9=EAo=C2=CF=D2=3D-=E7=
+|S=F6=C8@=0D=C4=E7=A9P=9F@=DB=E7`i=D2=A9=A0]!=9B&=86=92A=F0=DC=E4=DAZ1=0De=
+=D8=12=08=07=C0zq=11`A=12T=C1=11X=01=14PA=16<=C1=184=81=14D=C1=19,A=C2=8A=
+=F38=BF=C5=02=19=C7=FD.=10=9C=DE=A2=10=C0=ED=A42=EE=E3=16=01=10\A=14D=81=C0=
+=BE=96=12=D4=A3=EBL=01@=C8=10(#FA=83=05w=10=14=C8CI=0E=1D@=844=09=E3D=88=90=
+=1CMf=10=8C=01=E3=85=8E*3j=D8=E8!c=85=0A=93'M=AEH=B1=92eK=96'Y=ACX1=83=C6=
+=19=198p=D4pq=A3=C8@=194=80=02=E5=91=03=87=8B=19(=90=98H=F1=82E=0B=187p=A0=
+=94=C9=82*U=1C-=B0f=CD=F1b=05=0C=18'R=9C=10=8B=02E=0A=1C)T=B0=F4a=D2e=FBK=
+=94o=E1=C2=959w=05=D5=17.h=F0=E8=D1=03=09=D0=19=7F=01=FB=14,=E3/=93);=9C8=
+=D1=F1d=C8=0E=1DC=AE=F0=81r$=08=0F=1D=0Eud=D6=B1=833g=1E=9F}=14ILeHi"=A7=8D=
+0=F1=A3=C4J=13=D7=89=9D<=81=E2=C4=8A=923J=92 I=B2d=CB=10/C=AC=08=09=12D=C8=
+=14%Q=BE$=112=87=B3=E6=1C=CF=A1=E7P2=86=0A=12=D1=3D=1C=F3=B8Q#=E3=E0=83=075=
+=C6=A82d=09=8E=1CQ=80=141B=04=8C=E3 =03i=E8=E8=D1=E2n=0B=165d=FC=88=CB=B6m[=
+=15%g=EAa=86=1F=82=88!=87)=8A(h =1Bh*c=BB=EDj=D8=08=AF=B1R=A8K&=1AJJ=A9=AE#=
+=B2r!=08=17Z=E8=C1=85=11a=18=8E,=1AP=18=8B=AC=B2=DC=EA=CF=A5=FD`<=89=AE=B9=
+=A8=CAJ=86=1Ap=F4k=06=1D=FE=1Al=A0=BFj=08=03=08=1El=FB8=02=87=1Fx=D8=01=88*=
+=84p=C2=8C(=84=E8A=B3);=F3=8C=07=1F=96=98=E2=B4=D2=868=ED=88"=9Eh=C2=09*=98=
+h=02=8B=C4=B2x=02=0D(=D8=CC=E2=0B=DC=92HB=091=95 M=B8=E1=82`=E2=88'=A8=10=
+=02=0A=1F=A6=8C=EE9=9Ct=98=C2=87=1E=AEh=0E=8A=1C=82=E8=CE=A7=EF -=08=88%=82=
+=C8=E1=06=F9|=F8=C3=07=CEn=D8=81=0A=81h=AA!=A6=19W=A0a?=17_4i=060th=A1T=1E^=
+=C0LA=C2p=B8!I=1F~8b=87=1B`=D8=08=87=15G=CDP=C3=BAZ=08=E9=85=1B=82=E0=C8=86=
+=BBFt=81=A4=18dXQZ=16_@=D5=BF=18=E3=92i=0BR=ABj=C1=A8=1A=82=0A)0=1F=09+=02=
+=07=A0=B83=02=A8&xxb=09=1F=D6+-=3D*=A6=DC=8C=B3!>=DB=EB=CB"=B8$=E2=08&=ACp=
+=E2=88+=98(316=B6=F8=A2=0C1=C4=88=FB=02=8A(=A8Pb=09=DC=B2=80=A2=0D%=88=A8=
+=08=CF ~=F8=C1=87C=B1+=C2=B1=CB=A0=BBl=87ZE=1A=A2=87#z=A0=E1=87=1C=8E=A0=C1=
+=D1=81"5=C8+=19`=90=01=8A=1Bn=00b=07(=88=C0=C1=0B=99A=FD=8B=08 =82=C5=D6Z=
+=B7=FE=BBA=08=19=EB=CAO=A3=FCj=B0l=07#&=0B=CD=E9=8Dd=E0a%=AC=84=FD=8F*=1BZ=
+=10.>=A0=80Pb=06$nx=C1m=18x=D0B=86=14=C8=A2{=C5=A5=FB=C3=F6=AD=19]=A5=B1=A9=
+bk=88=C2=06=1C{=F0!=08=1An =DA=C7!d=06=0C=B0=C4=D1-=BC=87!=89x=97=CA=CEx=B8=
+=A2=08$pM=A2=09.=BB,B=8A.=C0X=A2=E0&=C8=18=A3=0A+=AEh=1D=0B;=A2=808=8D=88=
+=E3L=A2=08"=86=A8H=F7)=8E@2=C9=91=EB=CD=8C=87"=84=00"=87=C1=C1=BD=C1=07=C7=
+=C7=A5=B9=E6=8D=BC=8A^=86#l=18=1C=FB=F2=C6=09=DB!=0AQ=E7=D2=1B=EF=97f(i=A5=
+=94h=CA(=86=1F=08=1B=C2!=DC=A1@=E2=0B$p=C8=CC+%=D2=B2=81=86$`Z=A1=05=C7.=FD=
+=8B=06=1Cqd=83Nq=84#/hAZ=C6g7=16}=8Fi=FB!=D5=8ChP=15=17t=0B=069=A9=C1=C7x@=
+=842=F0=A0y=03=11=90`=98=17=AA=1E|=C6=0A=9D=B9=DC=0E=AE=E4=83'=CC=09=09J=08=
+B=C6@G=04=CF1a=09M=88=82=13=AA@=85*=A4=E1=0B`=E0=C2=18=B2 =06)D=C1=0A=13=93=
+X=13=1E=96;=DD=09=A1K=FE=FA]=F04=B3=03!=18=C1c=DB=D1Q=07=05=13=A9=F0=F8=A0=
+=80/=08B=13vP=BD+=DE=C0@H=10B=11Z=90=83=B0=C1=88=81h=C9=01=02=C7=F7=1F=17=
+=E0=00g=D0=9AA=A5t=10=04%p!=0DK=B0=C8H=A0=A5=03=93xe=06)I=09=0Bh=90=03=C2=
+=CC`Wd=18=02=10p=FC`=83=E7=08=81=80M=B8A]=B8=D2F=17=C1h&=11=8CIL=800=97=FDU=
+=A5[.=B0A=18=91=E5=98=1D=8C=84\=E4b=1E=0D=CCS%=CB\=AE=07Lb=82=10=94=90=05)L=
+a=89C(=02=13=8Ap=84*,=C1tV=D8=C2=14=A8=E0=85.=B0=E1=0Cc=08=83=16=DA0=05)=A0=
+!=0DRP=C2=11=F9=D5=04#=E8=8EKQxb=14=A7=E4=03=86=1C=AA=09=E0b^+=9D=07-=19=F4=
+D=06#=E1"=B3\=F0=83=9D=F8=C0=068=D8=C1=05q=00=85=1D=00fTJ=D3$=0B^=B4=82=1E=
+=D4=E0=05=1B=89=01=18~=D0=83=1C(=A9=09|=F2=A5=0E=B6#=03#=0C!=06(BK=1C=A7=82=
+=1FFzs=95M=00=14=CAp=E6=95=18=CC=E03=82=D4d=DEb=14=B5=1C=B8=CC=93,0J=0C=B0B=
+=B6=0E=89=87P9=90=C2=0E|=80N=1F=91=B4J^@=92=09=A9=94=84$=F1=E0=07D=80=D7=14=
+=FB=C2T=91=D2=14a=09Z=88=82=9CL=E7=04/=E8p=0BhhC=00=0A1=86-xA=9AQ=D8=82=11=
+=A1p=87%d=01wJ=1D=C2=97=86=F4=03=CCM=A1^=98=E3=0B=CF=1A=D7#=9D=0AF1GP=1F=12=
+tP=14=10=81=E8=05=D3=D3=01=0F=82P#!=10=94=06=3D=E0=CA=1A=DFrR=97=F4`=05J=A0=
+=C1=15r`=D0=18=DC=A0=07=B6=02=02=AE=82=80=9D{=0AD=0A.x=C2Q=02=1A=03)=18!=06=
+=DC=D9L=97=9E=10=04 \=E6HP0(H|=B0=02=17=BC=E8=A4=D8=9A=CA=0Bh=B2=03=F8=0D=
+=14~/=10g=B3F=F4=02=B8=C9`=0A?)Bw=D4=A9=D3W=D6 =A1=0C=B9R=F0Ti%.=E9=CE=08J=
+=18B=99"=A6=04)=88=01=0D]=98=82=1D=B4=F0=861=D0=81=0Dj=F0B=16=90i=05=1A.au=
+=C1=E4=97=EE=9C0=85=B1=1E=81=89B =02_=80'=BC*=0D!=92=E7=9C=FB=ABO=F6r=99 =
+=DC=C0=06:X=16V=9A"A=FB=C8=04iU=01=CA=11=10{=12=C5=B6=A5=A03iB=0C=84+=04-X=
+=0A=07S=C0=D7=0D=920=03=1B=08Df%AB=0Ah=F0=12=93=E8@!=0A=15=C2=12=D2P=9B =08=
+=D8|9=BBJ=86\=F2=C66=EA=ED?=FA#=9EB'=DB=B8=FFUo=07^t=9Bp}PZhi=D1=95=CBU=DE=
+=0F=B6y=04(f=863@8=C2=96t=E7=C7!D=01=09M0=E2=12=AC=10=85=EDfa=0C`=00=C3=17=
+=D2=80=05=1D=E2=D0=CB=05K=02=EE=A6+=1C% =A16O=F0=01=1F~=97=DF=CE=8CP/=F6=0B=
+!=08Y=E9=13=C7=95=81=08<8=82=0Fp=B4=11=FBPe=82T=C1=D0=03=BB=A7=82#=9C=EA=A4=
+e)=8B=0Al0>=C3=0A=B7=060=D8=15=F2B=12=D7K=ED=C0U=8C=15=DFEU=A0=04=AFd=06=07=
+=EEC=02=14=BAt=99!d=E4=07=FBv,1=7F=FES=C8=1D=DB6=A3Q=D8=E7"=83=B2=9D=EA=1D!=
+=A4=D1=EB=D5=F3@=18B=1F$.=809=80H=11f=09E+=E5=8A=97=BD$B=11jW=A7=D9=C5=CE=
+=0EXh=DD=15=ACP=851=90=A1=09=05[=02=18=BA =85=DB=81=AE=09I=F0=98u=AE=90=E7=
+=FCb=A9=D0@=90=1C=B8=80r=83=10=D2d=08=AED=1C=0D=82p=A8=00n=AF=06=08=0E=96=
+=FEf=C4c=09W=FAnu=93=81=16x=D0=C5=1B a=06J=00=02Pt=00p!=B4=C0=06TYA=A7=F8=
+=C3=12=1E=D4Q=08<=F1=C1=11=A0`=84=EC0wj=E8=EB=A7IJ=8E=E3=EF=F1=BA.@ =C2=B8h=
+=E2=05=1C=10A=9F@=E0=19=14p=16=D2=E7%=F7G48B=0E~PX=96=A9L=80=F6tHt=A8,=B2=
+=BD|=ACc=1B=AB=08=11=94=D0=06(d=01=AAurX=14=18V=86/l=81=0D=89)7=13=AEp=04=
+=DA=E8=FC=09=CE=BE=0C=E6^>=D3=98=E6L=89=0AE=00=82i=8C=80=A5'=F0=A0=09@1=02H=
+=80=82=83=E3=B6=120=83=0B=D7=0C@T=15Rm+=C2=FE=FC^=B5=148-=82=AC@=06=C0}=C1=
+=B2J=D4=B6=C1-X&*`=C1=C2=15=88=83^=3De=07=BD=DB=94vp=80=DC=18df+<=C0P=0Bt=
+=00=86B=86=DCZ=FBy=F4=8C=EE=A38*=EC=8A3=99:=14D=19=B5=04 =0C=DD =83=B9c=0E=
+=A0`=AF+=14=8E=AD8=11=14f2=E3=03(=08=81=0AO=F8=97=D5=87=101'4=01Nr=FAB=C5=
+=D8=84=86'd!1X(7=15=B6=FF=84`n=C94D=90=AF8=E5=9D_=CD=F4@=08Q0=03E=AA=E03=A2=
+=E2=80z<=00B=18=B8cf=92=8DGh=E2=7FpD=06=B2=C2=E3HE=E1=E0=02o=E0h%=A4e,P=04=
+=05=86=03=06=9Ae><=04D=B0=E2=08^=AE=D0=C2=86n=FC=C4=82=06\=A0Wp=E4A=CA@fFL =
+=0A=A2=08=A6 =B28f=06=04=04@=1E=10U`=04}D=C5[=0CHJh` fl=07=C0=80=08=8C=C0=
+=EE=A2=80(=96`=08=AA=A0=F8jf=A7j=C0V6=A3=07=9C`s=A8`=0C=1A=E2=F9=A2h=07=E6@=
+=08=92=E0=0B=A2@=09xi8=84=C0=0A|c=08=BE*7=E4=E46=02=06=0A=9E=006\=A3=09j=C3=
+=0F=98`=3D=D8/=FB=12=A3=08=E0/=FE=EA=C5!=00=0B=E5=F8=E0=0A=86=E0=B4=9E@=07=
+=12c=07=A6=80=09=E4=AA=C9=1C=07(=90=00=EED=90)=0E=AE=F3=180=B16I=05|=E0%=CC=
+=C2n=C4=02,N=C0+V V=B2"+p=80=94^NXV =A7=AA=C5=04=90=00=05=10=CF<\/(vp =8A=
+=E0=06\=A0=06pB=06=CE=A0=C4=EA=02=F4=AC=A5=F1(=AF=07=06g=06=AA@=07=84KAf=A0=
+=09r=A0"=9C =0C=FC=9A=A0x.C=09FN!v =09=07b=0A=FC'>=94 3=A8/v=AE =3D=9C=A3d2=
+=A3=07=94 =0C=84 W=B4Ic=86=83=08=88=83=89=82=09=09t#7=96=E0=0C=C4=AC=09=C6`=
+=FC=A8=00=0A=AC@=98=92=00=0Bn=87=FDh=08=0Bz=C7w=AAD=96X/=08=C8=00=08=D6C=08=
+=92D>T=8E=07=98+=AE=D0)=84h=00=85=CE=08?j =E3=16=8D.=1E1.=02J=05f=E0=F3V=C2=
+P=00jE.Q,=98@,T=80=06^=C0=08=16M=F1=16=F0?<=8F=E3\=E0=04L=C0=04p=C0=07=BC=
+=C5=05=96@$=CA'=A7=9C=C0/=A8=A2=07**&\=05=12k=CF=81=EA=E2=8Ed=E0P=82=00=07=
+=84`jn=02=08=82=00=09=E2k=08=C8=CA!~M=06=80@=05!=05=84=A2=00=DA|=E09v`=CE=
+=F0=80=09=90 =08=BA@=08=00=05=EA6C=0A=0C=92=0D=00K=89=EE=C4=EA=FC=B8=10=0A=
+=E6L=1D=D5q=0A=D4=11=0C=A6=C0.=A1=20=0D=BE$=08=8B =1F=03=A6=09=82@=0D=C2=80=
+=09=86=80=8Ax=80=0Az@=EEVi=07=B4=C0=FD,=E5=06=96`/4k=84p=C4=F5=FCk=A7=FE=02=
+=0BB=E8xxF%=13=EE=11=1B=EF-=86=CF$b=82G42=C5=C4"&M =06L=E0=04=DCf#=A8=C0=A5=
+=12=AC.>=91=05=08=A2BL=8D*=AC=C0=04J=A0=04L=00=06n0=07=8AE=08=06'=07=BE=00=
+=08=9A=00'F%=912=E4=F3P=C2=01=F7&|=A6=82=9D*=E2=0A=94 '=8Ao=06=F8=CF=07=B6 =
+J=B4=E02=AC=80=0Dr=C0=09=E2=07+=BFC=D0x=AA8=8A=80=B5=A0,w=0ECJH=E69=AC=B1=
+=3D7C=07=DE=C0=0B=E0=CC=1C=AD.=09=AE b=AA=00=09=8E=E0=08=90=A0=0B=08t7=8E=
+=C0=08=D4=CE:=FE=F24NC4zg=0Bn=C78=AE=CD=08=E2oL=FC=CC=F1=07=08=0CY=F4B=08=
+=98=A0=0C=F0=ED=8A=04=F0G=98=C7~=1Ag=C0=12O=01WR=E1x@=E1X`=0Bf=80=0F=F0C=07=
+z%3=C8b5Y=D3=04^=F25c@=B7=BC=A5=90=9C=C2>p0=076r=13[=E0=04P=80=04~=13=06f %=
+G=AAXZ=00=08=9E=00=06=1A1&0R:=F7=06g@=02=09=08"=91=E0=85H=04=82=0A8"=92=FCq=
+3=14R=09=84=A0=10=99L r=80=07=A2=E0!=F2=C03=AAG/=FCod=9E/=DA4=03=DA=90@=86=
+=B8=00=08=FC=948=A4@s=BE=B0=86=06=B4P=07T@=8D Q=FD2=1FS=CE=08=9E =0A=A8=8Ee=
+=E0=EE3=FEQ/=A8=EE=07=00=E8=07=06G/=E2=AA|=0AQ=3D=FD=07HV1rR=12=D2F=F3-fOo=
+=EAB=16=93 <=0AB=B8=EE=03&}=F37O=E07M=A0=05~=E5=05Rd=F5=08=8A=05p@G=FCu=C0(=
+=86=E0=06N=80=04L=80=04H ,=80=C0[jDT=A8=82Ig=80)=C2=06=B1=18([F=82=08=AC`=
+=F0=AEb=D6=BA=83=06l =06\`=B0BB=A8p`b=F4MM=99=EC=C1r =D4h"'=80b=09=AA=84=F5=
+=04e=C3=B8=D1L{=A0c=FC=F4O)=03O*=87=0A=C4@@=13TQ=17=F5/=FFr=08f=08=09~ =0A(=
+=B3=0E=E3=0F=85=F2=85=17s$Dq=0033=13T=ED=C7=F5=08=C7~=EC=E7]`=A0Te=E2=B0=1C=
+1=A5j@4=11=E9=06=C2=A3=80`=E0=14Q &=7F=B3e=7F=13=05^`V=81=D5@J=A0VK=C27=954=
+=06=90ugk=16=07h=E4=D1=ECb]=AD=94=8D=AA=15%0=EC
+*=06=BA=00\j`=07X =06D=AFM=FB=E2=06X=E0=05V=B1e=9A=F6=83=B2=88=C9D=8C=08=88=
+&#f=80[A=E2=07=82=E7=F9=D0R=0A=FC=DA=C5=08=A2=E0=91T=CB)-=EEO=95=A0=0B=B6 7=
+=A4 =09=8C`=0C=04vs=12=F5=09=92@=8F=C2@=0A<fR?#=FE=06=97=CF=98=801L=94=06=
+=A2=80=15=CD=09=8B=8A=8E3=A2=C4=08=86=048p"$=82`=D10W=09=1E=88(_=80=D7=A0=
+=86=05R=B0=9D=18=C7m=82+=05=F0bV]=F65K@IM=C0=E3f`uK=C0=0CJ=82=04F=80=04z`=
+=04p=97=04=10=D0=05b=E0=0A\`F=A0@=7Fpsh=AF=B4he=84&=16=17=F1=A8b#=FBF>&=C8Y=
+]*=AEdf=F0=1E%=09=ED=8BG=08c =AE=00>=A0L=09=A2k=09=E6=93=0A=ED=CB=BEP'7=8A'=
+_=F3=D5`=AF=8C=FAp'6=044=0A=1E=13=87=8C=C0=0A=00=B7:=A4=C0=CE=04=97R1=E7=BE=
+(=93=07=82=10=08l`T=F7=CDq=8B=C6=7F|@=08=EC=E53 =B5=06=88=00=07A=04=96=FC=
+=1A=D1T=F5=87=07N5%=AA`*=EEcHh=82c=08,=B8l`=08b=D6ey=F6X=91=D5=08j6=C9vv=05=
+h=17wk=97=85I=E0ij=E0=15#=18=83=93=A0=F1j=EB$=92=E7FF=EA=F3V=C0=062D=7F=E4i=
+&=A6=82=06=9E=00{=00#!=D2I=8B=E8=88=F0h=80=185=8B=07=D8=80=AD=CC=D6=0A=BA1c=
+=92@=0B=E4K8=D0=17}=07=B1=09=E0V=0B=B4=00=09$T=8F=B8=00=0A=98 =0B<=06=0A=AA=
+=80@=89=A0=B9=06W=DE>=A3=CB=8C=00=0C=12=C3=8AXq=80?U=D0@=A5=B0=A8,6=902=80=
+=9D=05\Z=AD=E30X&=A8=D3B=EA=02(*XF=D4I@4=D4=7Fj@=09=E4=C9[Z=E0=08`=B2ew=16Y=
+Ixg=C3=07=05Z=80v=91=95=85Ey=04=E6=C8=04=10=B8*~`Zi=84+=1A=F0=86=FF=03=C3=
+=C0=C5=90X=F2$=FA=86F=BC%=0B=FCj=C0=08Z=89=F8v0+I=CCqb=98=07=E4=B2"=88=02'=
+=CC=03=EA~=C0O'=8A=1C=CF=17_=81 =09=C6=00=08=92=F9=99[=08}=8F@?=FF=E4c|=C0=
+=09=B4Y=9A=0F=E5=8D=AB=04=0A=92`=08=94=A0=08=92=19y=82=02=8F=F3x0&=F6=06=1C=
+=A2"=8A`2=D0=00=076=02=E9=12=09\=1B=8D=03=A7=02=E1=96=B7H=17yX=9Ab=06,=E5(=
+=F3=C2=025=D0[r=00Ig5=93=19zuek=85G=B9=04r7w=91=15=05=AE=02\=D6=82=C7=0Eh=
+=C2R=E2Fn=00%=98@=96MS*0X=B8d =0BF=D4U!=85=8C=D6=14T"=A9=06=10=B6=88=16=CA=
+=98=9D=AFM=89@=8C3FI=9C w=C8=E3=0A:=C6=99=11=B6c=84=AF=09=90@=9A;FA=B9=EFc(=
+s=A9=F9=EC=8D=7Fj3<=B4=0C=86(=07=F2=80=07J.=9D=F1x=97=BBf=A2=DEy=09=9C=A9o=
+=FC=0F=C4=8E=0A=F0!=F1=02=8D=9ABT=80b=F5=FCY4e=02zw=B0=A0=DA=A6Cj@=8D=14I=
+=05\`u=19=9A=A2I@=85qW=04=FC=FA=AFG=B9vY=D7+b=EB=119=1A=D3n@=0B=DE=C2=05=E0=
+bdIz'=9A=C0=06=9C=92=83|d=E8T=BA=F8|B=C8`Z2i=DA=98KfO=FF*3=B8=E0=0A=CA=E009=
+=E6=A7Q=1B=B5=8B=80=0A<=E6c=AA=E0=07=86=C0=09=02=97=A9=DFxp=B1=801=88=E09=
+=80=80M=9A=A0=08=16=B3=B20ZD=F7=8DD]=F5F=EC=898=8E=A0=09*=E7J=06b=09=F2=E2=
+=15=85s=06=DCY=07.H=06=96 $,R4}x*=D0=C7>~=A0[=C0=85F]`=A139=B0kW=A2G=E0=AF=
+=D5{=94=91=D5V=91=14-B6F8z=05b=E0=F3=A0S=C5RjUg=C0=09=98=80H=FEL=F5=88=0F<=
+=16Bfp=C4=0A=AA=87g=FB=A0=02=07=00=02=87@=819r=E8=00=A2=A3`=C1=1D:v=F0=E0=
+=E1=E3=C7=0F =12+Z<=82=E4=87=8F=8D=1C=81 =91=E2=A3=87=C8=87$I=8A=BCb=06=88=
+=C0"?=8E=18=F9=F10=88=C6!5l=D0=B8y=93=C7=8C=9D<g=1C=99Ad=86=0C*3b=C402#=07=
+=0E&=12=A7=88=DC=E1=83=86=0C=193v=0C=C9RDh=8F!C=B4=C8=90B=C3=06=8B=15-V=ACP=
+=816=AD=DA=B5i=CD=BA}=D1=82=85=DC=B9r=CB=A68a=A2D=09=12|=F9=8E=F8=0B=180=89=
+=BF"=0A=1B=16=F1=97=C4=15=12\J=DC=90=F2"=C6=8D=1Efk=9Ce=BB6=85=E6=CD=9C;sVQ=
+=A3=05Z=CF=9B1=A3]=C1b=C6=10=19=92k=DC=E4=A9=A3=C7=8C/9vH5=8A;=B7=EE=182=A4=
+=AA=0E=C2=C3a=C4=1D=08q$=19H=B0=B6=12=85=CC}=F0(R=C4=C8=94 =14+r=8Ch=FD=FAF=
+=91=FB=DCKr=E7=BE=11=C8=8D=1B=19=BB_=11re=07=12=1C"=9DG=ED=09=1F=A1s=1D=ACa=
+=C0(rd<=91=1E!=8F=FC=A0!=14k7=E0=E0=1C=80;=C4`=C3=0C=A1=B1=C0=C4Xn=99=06=A1=
+=0An=A1=E6 ]t=B5=00=03=0Ay=ED=D5=D7`=81}H=D8a=85=05fB=0Eq=DD`=03=10S=01=E8`=
+=84=A3=91=06=A3f+=CC=A0B=8C=A5=AD=B5=82=0C/=0C=E1D=0B6=B4pC=0D<=D10=05=0F=
+=B1=09Q=D4nJ=1A=B5=A2=824=04=B7=03=16L@=F1=12B9=0C=A1=14r=06e=99=DC=10G=0C=
+=C1=84=14J$A=1D=10f=12=A1=9Dv=DF=AD=D9=03IB=F8=A0=C5=10=FC=F1=F7=03=0E9=D8`=
+C=0FRP=B1=91=10=EC=F5p=03=0F6x!=C4k=F0=F5=F4d=0F=099=11=03=0C/=E4=00=83=14x=
+6=91g=0D=AE=EDDC=0E/=1Ca=E9=0C4=84=B6C=0B388=E1e.=E2X=FC=EA=84,8=E1=02=0F-=
+=B4=E0B=0B<=BC=A0W=09y=F9=05=E2=87"=8E8=02_%=94%=AA=0FJ=E8@C=10.=B4=C8=16=
+=8Dj=D9H=1A=0D;0{#[=AA=B2=C0=03M=9F=FEP=04=0F=06=DD=B6dn2=F8=B0=93=0E<`=01=
+=84=10=0D=09=D1D=13HP=A7=84G=05!g'=BC=06=F1 E=10Jt=B1=84=10e=1E=E1_=9A=1C=
+=B1=09^=0FU=04=C7C=0FH=00=B1DH<8=F1=83=0D=96=1E=91=A7=11A=D6=84=83=A58=01x(=
+=A6=0F=0D=B1C=13;=E0 =86=A3/=D8=97=83=A5&c|S=0D7=C0=8A=03=0CM=1C;WY=A5=9E=
+=8AY=AA=D3=CA%=A9=A3.=9CP=E3=0A=1C=EE=95k`=BB=F6:=82c-=A4=A0=82=0E1=10=08=
+=C5=0C7=90jV[;`=06mi,T=1D-=AAf=BD=E0=B4=C9dt!D=0EA=C4=89=9B}fwKUp=B5=111=84=
+=12d,1D=10=FB>=E8=80%=BCv=E3pP=0FN=98=19Q=14Lp=11=84=10h=D2I=91=9A=EDm=E7=
+=03=10Z@=B7=C3=0EF=10=81=1F=13>=04=B1=D4=C4=0E=1B=14=E4MM\lh=C67=BD=C9=1F=
+=117X=F5=82=0B0=B8p=03=106M=F1=A9=A7=AE=0D=01E]-=BC=F0=02=D4=A9=D2=AC=B5=CD=
+=14=C2=DACXA=C8=90=1A=0B5=C0=E0X_A=87hXbn=E50=C3Y(=8C|=FA=16p=F4 =B3=AAA\&Z=
+ZX=D7=88=B5f=10=8E%=D7=11-=88]=93=0D7,=A1=83=82=F5=99}=B6=92=00=D2=D0DA?H=
+=B1E=11L=08=E1=C6=11=09=BD{=B7@*=E1=BD=83=89=04=A1!D=F8=81C=1C=82=05+ A=09=
+=16Q=C2=12=8EP0=1F =E1:O`B=11t=E0=03!,=C1=0AO@=08=8A,=E5=03=93Y=8A=07=9C=BB=
+X=C6=86t=93$=DC=A0ax:]=AC\=D0=05=17=B8@N*=83=FB=01=18 =F6=AA=B8=CC=E5=06.=
+=D0=9D=A9p'!=1F=9AE.R=C1=01=11xp=05=1D=04a=09<=A8A=0C=84=C7=03=E3=05=ED0=7F=
+=B1=C1=09=E6R=02=16=BC=EA=04'8=16=0D=E4b3=1A=FC=E0X;XA=0CrT=96=17=D9=88=06=
+=DD=F3^=CDT5;=D4=DD=A0=08=06=99=82}=A0=C0=BE<*=89*=9F=1AH=BDz=F0&=1D4d=7F=
+=FD=83=97=12=A1=10=04"(eK=06=09B=11=B0p=04&LA"@P=C2=11=17v=04D=B4=04;O=F0S=
+=BD=92p=06=A6=14=01=07=0E;=19N,=C6=B9=13"*e&=8BH=CBb=05=AB=D9=D5=00=072|=01=
+=10=B4=A5C=A8]Ow@=0C=A2=AAt=A7C=18=D4 =08X=98=95=16e=E0=A3=18=A4=C0=8C40=81=
+=87t%=02=12=A8 =8A' =1A_\=10=83Z=B9=00=09=D5=9B=10=CC`=00=07=16`S=06=3D=88A=
+=0D=A2B=83=15=B4=B1j=FBm=B9L=A9b=E6=82=D2=D9=00zy=AC'=0Cp#=83=1D$=A1=069=08=
+=CE=C1r=90=86=E3=E0`=0A=82dNBt=E0'=81=8Cg@=03=C9=C1=14=80@=04=1C|=AC=A1Fh=
+=1B=1D=96p=05=99H=01=08A`=88=14=88`=04'dD=82H=C8=01=A0d2=84"4=A1`@=F0A=13P=
+=C99=D7tN=95%=FC=94=C5\=98=C3W=AD,=87Gp=08=CCRe=BB=A8=E1=CE=87Z=0C=EAXZ=15=
+=83?=C0=B2=05=B5=EA=10=AE@4=B4=C4=FC=A5=07}=11B=09"#!0=A2=C6R=15=E3=81=0Cl=
+=F0=83=DE]=A64=EB=B4=11=8Evi=16%=C4%=A7=B0=8A'=ED=E8iO=FB=E8=E6[=06a"B=832=
+=83=0C=FE=A06=09=09N=DDp=B0=D0=F144=08=02E=CE=10x=A0=AF#Pa=08=1A=D4=C1=13t=
+=B0=D7=B8-=A1=0AP=B8=C2=0F=0689=C1*a;=10=F1=01M`ZS=9A=D6=FC=F4=94x=9A=E7Rc=
+=A6=C3WA=E1&Z=15=E2=0F#=E4=DA=15=8C=AC=B5=0FJ=01=0APp=02=14=04=89CO=3D=DE=
+=AExE=B4=C4=90=A0=045xAor=A9E=B0=0A=C5SK=F8=C1X=CB=0A=AD=9A=01=81=06qA=9D=
+=0Ck=E0=02=1B=C4=D3=AD=B4=8B+=FB=E6J=83=01=C9=C6'R=99=CA=0Cx=90=04"0=84=B1@=
+=18=C3=16=00=1BX=86=E2`=08QP=A8`=F1=E6=1F=8D=10A=07=02=A1=DC=0Erp=D9$8a=09A=
+=18=02|Qd=03%=DC@=08=1A=F1=C1=18=FC =04#=98=B2=84=A2-=A1Lk=10V'=E4=A0P=AF=
+=B2=D0\j=C0=02=B881=A8=B7=A3YlQ=03=17=DA=0A=C9>y=A1=81^X =C5=E3!/=AA=1F"=81=
+=09=84=A7=04=95=A5=96?4(=C2=0D=A6=12=83&=9E=05=BA=CCR=CBX=82=E8=02!=98=8F=
+=06=3D=D8=81=90=E6=08=83=1C@=E1=05Q=F0=AE=1Eq=D3=03=B6=FB]p=0A=AC=012o=92=
+=02=04=864=CE?<=B8A=0E=E2{_=1D=D47=BE8=A0=82=12}@=AE=8F=0DH=B0L=F9=81=156=
+=12=04=04=97=16Os=13=C2=0A=9D#=93=09=9B=D0s6=3DY=0D8=85=13"=B4=94=06@=E0=02=
+r/D=97=1C=F8=80=C4=0F2=F1/=CDR=3D=B9=08A=08=8Fn+=8By=1Bc=19=FF6y=81=B11vg=
+=00=83=0E=CB=8E=096`=9EQ<=05=84=E7~=86=C8j=91=01jb=BD=82=1A@4A<=11a=8A=1A=
+=05=84F=91L=D8=F5=D4M=0D=04=E9eo=8Dw=06N=E8=01=1C=8C=C0=044=A39^=F1U=02A=84=
+=F0=05?!!=BEb=D3=08=7F=8E=A0=E0=3D=E3=C9=CFx=FA=C1xz=B0=04=FE=94R=A63]e=0D=
+=C2=A0=04=1E4=E1=06>=90=82=C5~0=03;=C5=E0=05<=00=C2=A3=E72-=1CPz=A8=A5=8A=
+=0Bj=BEH=97=17=F8 =97?=90]=0BN=A0=FB=17=A7=CAXh=A3FL=AF=FAb+=0DeQ=06?Hu[=C3=
+=DB=A8=18=FC@=09=0E=BB=01=F78s=DB=94o=8F=058h=C19{=E3=9B=16=1C=01@=E6=15c=
+=8A|`=03R=DE=A0=A2=EB=A3=81Q=E2=8AO=1D=D8=80*=BC=D1M=0E=A6=D2=1BLY=0AO=F7=
+=95=F6=0Dt=E0=F4-=B4=D4Oh=0EB=14=800=04"=F0I=CF=BE.e)=83=84'=D0=E2D=95=9E=
+=B2=81=14=08=E4=1F=AF=F7 =D5p=F97mS=D5=CDJ=9F=CA,=E9\:uq=E0=EF=98=E1 =DF0K=
+=ED=14l=B0=82$@=1C=D4=13=9F1=8D=053=DC=E1v=F1=08m=AF%=ED=1A=85=03!4=CA=07'=
+=B7m=CA/=8F=82=18=B9=80-=A1=B2=01=0F=96F=84y=D2'=9F=822=C9=0F=A8~=83 H=017J=
+`=0D=B1=AF=8C=C1!=D8=A0[L=AAy=08=E7}f=BC=D9=97=CDh=CERI=C7=B3g3=C3=97=09-=
+=C5=F9=BA=FBMv|=11=B2{=B4=9E=ABA=9B=1C=C2=03$d=E4=9C=B3=CA=A1=87=DF~b =0E=
+=F1&O=90=C1=10b=C0=93W=99;=07E=D0=EE=AB~$=97=1D,=C7D=A0~=A6=A8G=1D4=8C=C7=
+=0A=05F=08=0B=0F=A1w=CE H(=05c=D1=0C=E6=FF=7F[=DB#!=FCU=03R=90=03=F84=03B=
+=D08Tp=04E=C0`=09e=10f=B2wecOF=01=058=F0sW=86O2P=7FE=B0=04F=D0=03=06=C4=12@=
+=A0=14N=E7tQ i=C0=B7gF=C0tXG=05`=93|=CA=07BbWS6=B1=13=E6=A5=03H=D0=10f=80n>=
+=B0=04(=02=04=A5c}=1Evb%fb,=D0=031CsG=80=03/=A0=03=AE=01=05:=00=03X=90]1=A0=
+C4=80[=1B=E2T=BD=D5+&@q""c=18g+&`=02.=A0#.=F0=05MFr=95=07=03*=00=80o(=80*=
+=C0=02=FCY=01d2=90H=03T-L=E0=04R=80.=FE=85=04~=02 =14X=811=A04@'=03=0CvN,=
+=D1ohp0?=C0=04WW=82N=17|=0B=85=03=94=A3gL=87=897=00=05=0C=14=83=CA7Z5U=03NF=
+=03_=80tU=A1DN=90RG=E0=039=80=044 C=B1t}E83Ce=19C=B4=02.@=05=E4=F7}:=10=14=
+=00=12=04=E6=93=043=90C.=D0LM=B5=850=E0+6=E0~#=80=18^=18=7F=88=D7!za=02*=90=
+!:b[0=90=80=96=87=028=A0=8Doxyq=C8=02@ =14L=12=03O=F0_F=10=05P@=05=F5=C2=04=
+=05A=13B=D0=03=AE=97=81@'=04=DE=85O?=90=04=06=B1=15G0=05Z=C0Q=FF=85=1C=82%=
+=1E=97XZ=81=05X=E2=86=906=10=04=0B91=9E=F8=891=D5=03*=13fK0=8E2 H:=90=04=1A=
+T=05=FCd"H"=A3]=B0=04b=00'=8BBEpj1D5=F0=1C=CB=85BO=12=04F=D0v=FA=B7!z=F1=02=
+=D1X=028=004=D04=024=F0L=CE8=02=B4=B2=85=BE"=8Ddhy=B9=B5=03=99=A7r=DE=08=87=
+=D1u=1A,=F0$I=A2z=06v=04@=D0=80C=D0=04=07=15=1BH=E7zVvO2`=8F=BC=91>=1Af=15R=
+ =05Z@&?@=04L0Q=0D=B5P=09=89=90=AA=C3=96o)h=0F=C9JX=01=11:P=04=AE'=03=CBqYH=
+pQ=E62{=A44K=99=02=03=162=92r7w=A8=B2O=01B=15=AEC=03=9F=15=04S=E6=02N=90=02=
+dX=02V=A5=19A=A0=17B=A1=02'=B0=8C!R4\=E9=026=96=18=BFR=02=C8(M=B5=82=9Ad=C8=
+E\=D4=8DH=89=94J=B9=944=E0=03gS=14C0 =0A=C3=10=FD=B6=06=E3=98=1BZ=09=03R=08=
+t5`.=FCu"QB =95L0=04=E9=91=10Zr=109p=05=04=F9=96{=A6|a'=83q=F9=90=C8=A7=8AA=
+P=1B=90=02=03=99=92=03=FEA=04T=A2(N@N=96=F2_=B1B=84=85=D9K&I=16>=12=06S=C1=
+=03Apl=09s=06+d>CP:B=80=17&=F0=02)=F0=02=DC=88=02D0=16Q=D4=99=CC=18M%0=04"=
+=C3[0=80=1A.`+=3D0=8D=B6=12=94dh=02=AB=B9=9A=AE=F9=9A1b=92O=03+=D8=98G/=F0R=
+5Q=056=90=04=0E=01Y=F28=8F=DE=B5=03q%Q=1Du=03=F1#=10Q=D0=04!=D18=CB=19/=8D=
+=A4=11=D0=19=9D=D4Y=9D=3D=BA|=17=A32K7=9D=A9=A3=12=ABa=1FZ=80=05=E3=01:=10=
+=11=05=18SJA=D0v-=90=84"9=92=EB=C9=03*=E0=1C9=B7=814=A0=80=0E=F1=14w=B2o0p=
+=17'=A0=032=E4=02=02=FA1=A1=C1T=FC%=00"2=C0+%`=16:=C0=02z=01 =A4=B43&=E0=8A=
+=AA=19=03=AAI=A1=16z=A1=18=9A=94=9D=A1=02P=B0=8AE%=155=91:=07rO=FB=06+t=10^=
+3 =1B=E6=05=A9>=D0=9B=BEYl=1B87g=06|7a=07=8Fe.FR=1B\=E0=03P=D0=03=0Ec=03>p`=
+p=E9=A3>=0A=8A=A5=DA=90=CA=87"=03=92=A0a=1A=03Q`>=F2s=03=AFA=03L=A0=04=F9=
+=E1=02M=C8=03=E9Y=A54=E3=3D,=90 8=10=1CL=928=0D=81XE 7gV=04QV=94(=00)F=B0V=
+=09=A6=03>)E=89Wj(=00=11/`=02=C2(=A53=90=02=B1b=13<=A3=02=A6S=A1c=EA=A7\=04=
+=A8=81=EA=19=12"=03:=08~;=00=05G i=AF=E6d=D5=A5=13=E6=85=95Jb=A9=DF=F5]=BC=
+=11=91=0C%=04w=99tM=D0I9=80=10;=10=82&=13=AB=E6=A6=A3=FC<=BA=AA=3D=0A=A4a=
+=D1=AB=9D=C8t!=91=AD=F1t%=08V>=EF=91oL=D0S1=D0=03w=E0q=B5S=17=007=17\=E0Z=
+=D2=F2=03=DAc5fQ=872@=04=F8=CAXR=D0=04=84=C2=04=88=F5y=E4=8A=023=C2=04:T=8B=
+=83=07j7=E6=17}aN; =03)=00<1=93=03, =A0=DA5t=B7=D5Dcz[=7F=FA=AE=FFgV9=D2e9=
+=00=82^=C5$3=80=8Ed!N=8AI{=82=18=B0r=B5=B4U=A0h8P=04=DC=12d=85=C2=A5=04EZ=
+=E2aD=A9=CAtwB=B1=15[Jfba&=D5=05x=02XK=B7B=F46=04eZK=F7=D40=EC=86=92=E9=83=
+=04E=F0_=E7u=030=90 =E3=A3E=B1=82];p,#=99=03M=F0C=B3=864=A8=B3=95=EB!iA=80=
+=04X=90/=068{1=00=04=DEs=8BP=A3E:=C0T=D24\:=B0=02$PM=FC$=D0=02B=B0=02:q=1A=
+=EF=84Z=0A=B2o=0D=A1=8DG=10=80)'=03[=ABr=1A=AA=02=94sO3=90=044=F7=10=F1=98=
+=03n=C1=02!t^x=B9G=09=F8z=01K=88=AB=08=04=17=C4=15W=10r1@=04=81=D8}+2LQ=B0.=
+=EA=92gU=B7B=12=D1u=A5=D5ue&{=97=E2)=11=B9=03td`I=B0nz=02)=A5c=A6=A6C=1C6P=
+=05=0A=B2tU=F0=026=C0=8FR=A1=03e=F0)>=F0U1=00.=19dp=85I-=97=A1=7F=95=874-=
+=10B=F7T1=0AqzE@=04=EAe=1B=AC=C1=03?4Dr=11=04L=B9!C=D0EXH=02=B9[=93eq=3Dm=
+=E1#sA9=AF=F2B=F1=84L=A3!=037=00=803=C0=BC=99G=1A=12=92=AE=C6=01y=80=F7=BB,=
+=D0=10=95=E6 .g\=16=89=97=0A2=03=0B=F9=BDo=C5=C5z=C4=1A=FC=04X=14=CC=1A=FC-=
+c6J=E3-M=14o=FC=B1=11r=C6f=92=C3=82=D3=B9=8Al=09`=3D=10=06=E2=88)A=10=06m=
+=02=11=86=C0=B76=80=96=E2a>B=08+/ =04/=F0=03=FF=E9=8F=3DP=042=F4=0461K=E6=
+=E4,6!LfH=A5E=08b=00=EC=16@=E0=024=E2=7FH=A3=C9\i=1F=9F"^=DB=E1=10=12C=15=
+=9B=C7=9E+=90=AB=B0=F4=03=B8u,=B0=12=03W=A4=02%=C0=1BGv=1Ae=81=84=A1=E1=025=
+p=05/=10=06=04,Cp=10=03=B1=13=196=10=05=98=B7=19[=9B=C4>=07=03=16=99=8BS!=
+=A0=B7,=15=D2=C2=02K=03`=8AI=15=89$=04b{O"=C3=C5oU=812P=06=8F=85j=F6=D8=1B=
+=82=D2=10M=009J=B0=90=FD=B6=03=94=A8=03P=90nG0=BD6e>=E4&nO=C0=12=3DE=13TQ=
+=89=C2=F1/Jff0 :=89=BBTm5=C8=0A=89S=FC>=0C=A5/=C4=B9=8E=B2V=95,=8B-=00 ;=85=
+=032=C0=3D)W#*=F0=04702T`f9g/Q=B0=04=B6!=14<=00=D1=EET=CD=09=F2)=9A=D2=B4=
+=C0=D3=C2=96{=02>g*!Y=CD=D5r=06=C3tfP=1A=19=E0=EC=BA=94Z#=C8=CCF=AD=E9=8D0"=
+=AFK#$=94=BB=164 ]4=F08=08=15 =E7EHV=10=8F=F6=F1=CD=DF\l=3D0=BE=92=F7=BD=E5=
+,WC,HEPAO=10=95J=80z=82e=10=92=1B=B7=9F=E2=04NP=89LH=B8=05=F9t=05=81=04=A4=
+=F8-=E2r=C2[=F0=04=D8=82"Fp=86=F1DK9=B5Tx=F7=84=1E=97C=D8=B5VC=E0v=B2xF=98&=
+NA@=9EUa=86R=A0N=97=C7=1B6 20=C0=04=80i)U=E0=A4=C1h=CB=E9*<=BD=01=15F`=85)=
+=C0=D4+0=06=80=85=81=B6=854:,=03V=D0=9D=FB=EFSh/=AA#=99}:M=EB=7F=01=B8=02CM=
+=D4=F0Z=1Ad=11=CAQ35=C1-=A8=12"=15=FA$$=A5=E8y=09=D5=03=F3=B5=A2W=8D=D5=DF%=
+1=07=A8=95=84=98gx#=CF=EA=88=05=8E=14Q=E3a`=04=A1DB=12=16H=C0=05=12=A1-=C2'=
+=D7=FE=E3z.=81A=1A=A4@a6:0=D0=03=C0l=A6l=D5=02A=00+(=A2j=B2=133=B18=D1S=AAE=
+=D3Es=ACa=05C=B0=02(=00=10(P=9C@!=04=86=0C=18/^=C0=A0"C=C6=0D=1B4h=F8=90=D1=
+=C3=C5=8F=15=19Ul\=A1BJ=0C >=B8=0C=E1=B1Cc=0E=1E4=1A=CEP=91=C2e=0A=8E+j=E0=
+=901=A3=C6M=1E6 =DA=F8A#=C6=90=84/l=D4`=B1=F1%=CC-=3D=8E=0Ad=DA=B4=E9Q=15Tf=
+=E4=90=B1=83=88=8C#-aju=C9=B5=AB=0A%D=82=D0`=C1=A3F=0C$7f=FB=D0=D8=01=11=07=
+=8E=84A=15=CE=A5=AB=10=C6=DD=BB3=A2 =C4K=17=EF=DD=18Ar=BC=1D=02=C4J=91);t=
+=E0=C8=C1=C5=06=10)9=988=E9Q=C3!=8D=1E;|=F4=F0=E1=03=09=12=1C=10o=84=C6=C1$=
+=C8=94=180b=84=CEaD=08=922S=84=10=A9=92=D0=C5=ED=16=B9u=EF=DE=ED=E2=89=0B=
+=16=C1=85=03=17^<xF=E4=C9=95#g=D1b=08=8D=1FOr=CC=A0~6F=EA=1D*=06=9E8=11C=A0=
+=8A=16=B7]=BC=B8qWG=11=84:=C8"=DFhT=C5A=1CF~=08=8E=D1QE=0C=199=A6=CB=E0=AAB=
+=B8e=19|=C0=8C=B2=1E=98=10=E2=86=1Bx=C8=C2=A7=BB=84=90!=A5=96=DA=CB=E2=05=
+=AF=9C=B2=90=A9=A3`j=A1=AD=16:=A2=E1%=AF2=84=89=05#=82X=C1=85=8CZ`=E2=07=9B=
+h=B8=09=07 =80h=A2.=1A=FD=FA+=86#=F8=AA=F1=C6=00G=D3A=87#=AAX=FB=03=8A'\=AC=
+*=0A=1E=82=98a=87=EBbXR=07=1E=9C =E2=090=80(=02A=1Bl=90"=A4=1D=DE=C2K=86=C5=
+v=F8!=8A#=8A=E0!=89=18|H=0D7=DEt=13N7=19Z0=AE=B99=85[=EEN=E5=82=B3=01=07=B6=
+z=90=08=07=1DnHM=06=A0T=D0=81;=EEv=E8=01=87=17=C4=AB=E1=B6=1Cj=C8=CD=05+MdN=
+=86=14P=B0=E1=05"g=C0a=06=162=EA=01=85=1C^=B0=0F=C4=99r=90B=B5=9A=B8=D0A=AC=
+#=8E=F8=01=08=1C=82=F8!=B5=D4X=95a=05=1ClX!=86)=CEZ=EAB=0B3T=E1=06=1Fv=C8JD=
+f_=0Ao=85=A28=A2=AE9"lp"=07=1Fj=D4=D6F=C0h=D0qG=D5p=18=02=09"=10=84=E8&=19=
+=9A=84=01=87#pHM=07=D5b(=02=A5q=DF=E8=82=89=18Y=ABa=07)h=E8=EB=85=D5=AE=BC=
+=A1=8C=CD=16u=11=88=1B=80bs=FC=B7:=9B#=AA=E19=F1=948=A3=FFn=ABA"=3D=A2=18=
+=82=8B=B9R=A8=81;=13L8=A1=D1=85lhS=D2=E2=92=ABa=06=FC=B8=9B=B0Q9)=DEA=0C=13=
+\0=0AD=FFb=B8!]es=D0A=08+=AE8=02=88 =16=CB=C2=085U=F3A=87.=0F2D=88=1AB$=F6B=
+=A8X>=B5Y=11Y=FAj#=1D=90=0B=8F=05 Z=F0A=BC=DB=B6=05Wi=B9h=BC=ABK=1B=8Cxk=A7=
+=1B=90=D0!=D7=BB=B2=BC1=07=1F=83@=02=0E*=96=98=17F4=DBB=B0=AEu=CD=C5A=8D=04=
+%=AAA=CB0nh=D4=858=E5=8487=88=ED=9Cxb=E36l"K=1B=86P(n=84=06=0AYd=EEV=F0=D4=
+=05=CA=E9<.=B9=E0>=D7!=E4&n=10=EE=A1=F0`(=A1=84=9B=BB=82i=05=9F=F0=F3!=87=
+=1D=82P=E2=0B%\=D3=EF=E2=EB=F2=C2=C1=CC=9FX=98a=85=1B2=9C=9A=EA=AE=FB=FE=14=
+=11C=AC=8F=B2/&h=9B=9BT=BC=B9=80H{=DB=BB~=18=DF/=FD=E6=AEj=07=1An=18L=BF=D4=
+=E6=8A=DF.=18=A6bl=D6-=8A=E8=81=E9=B9K=BD=EB=06"<N!<=10=C2=1187=14=C5=DD=E4=
+=070=E3M=E5=1C=18=AA=CB]=CE8.=E0A=AF=04=F58=D5=B0=0C=057=18]=C8N=84&=17=94*=
+b=D0=92=9C=F7p=90=82=DB=95 =06M`=C1=0Bv=B0=03=17=84=AC=04$p=81e2D=83@!=04=
+=08]=CA=C1=0F=86 %!=EC=00[=DEJ=1E=0C\ =11=96=F9=A0(=CC=9A^=B1=BA=F2=82=19=
+=B8d=05=99"V=F6=B6=D2=1E=E64=87li[=D7]=CCF=BF=18l=8Bn=80=01=D3=D0Z=83=83,=
+=C8=85=8B=0F=E2=C1=0D=B4p=84%pF-=F3=F3=0B=D9^=90=84=1C=EC=EF&{=A4=01=0E|=10=
+=85=19=A1=8Ea=0FlX=04#h=9C=1A =C1=050h=01=0E=C86=FB=1E=18=9C=A0=83=1DT=C2=
+=0BNP*=15=18!M=B9=D9=95=0AVP=84=16=90=C5=057PA=0AK =B2=DB=85=12=0A3$=01=09=
+=A8b=83L=BDd=050(B=AE=06s=03/l=86=07<`Z=0Dx=00=86t=81=A9=06K=10=9A=F3p=10"*=
+6=D1)]=81=81V=94=99=BD=8D=D4=809=A8[S=0D=12=F2=03=83=D8eg^=FC=E2=0B=82=A0-n=
+=DA=E5G_@B=13x=C0=17=C7=D5=A8=07M=C8=8F=B9=12=14=11=FD=FC=A0=0B=3Dx=DCE=C8=
+=16F=85=C4=80=07O=D0=C9Ut=B0=04"=90!]=E4=11d=09=1DXBC=1E=B2Nm=BA=CD=17=AE=
+=B3=1D=13=98Rd=A3K=01=0CvP=839=84g=06=FCY=0B=0DPG=CF=16=DC=AE=95"-=81=80|`=
+=82V=8E=00=09S=C4=DE=104d=17=9D=D9=A0=07=9C=D9=82=0Dt=B0=D1t=E1=E7A=DB=DBH=
+=0B=AE=A7=CCe=C2$=06.=01*=0A=FB=9A=D5=1E=FF\=EC=05=B5=B4A=16l =846=E2=11/=
+=D8=B2=C1=BF=BEx=BE=85=10N=065=C0R=0E=D4(=C6=1E=FC=E0=807=D1=09\l=F3=C8G=BE=
+=00=07B@=D8=0Fz=B0=AE=1F=FC=00"n+=A8A=09=99P=CCU=AEM%IB=0FZ=00=03=19=A60=A2=
+=B73=01=C0v =83#=BE =05'=90=01=0D=86=00=85=85=1C=E1v(`e+I0C=13=84j=86#=E0=
+=CE2=C7`T=FC=18=91eC=B9=D8=CA=A8=93S0=CD@N@=C8]=0A=94=F2=92=A2.S=05=98J=E63=
+_=B2=83=17=B0=80=06+=E8P=0BJ=02=04=EA$=90sI@=13=0D=8A@=13=ABv=B3=9B=98=E1=
+=01=12=F6=D49=AC=12.=07N=18=C2=0D=82=C09=88=98A'=F5=B4=E3m=12=82=03=AE=0A=
+=E5s=8F=A1=C1=9E`=C8&BZ=0E=AF=DDK=EF=9B=1AH=83|=E2=80=B2=82M=E1x=E4D=10=90=
+=9D=C0=052=A5=FB=C9`-=FB=DF=FF=C2=80=05$=18=C1=08d =90=DA2=E5=06(=98=81=00=
+=930=94=D3n=B4=07:[=82=EC=DA=93=BD=D8>=A5=08,=8D=AD=88V0=E1!=84=12=A73=90C=
+=0D|`=03=1F`=891>8=10_=C2=A9\=B3=C1=170L=18=02B=E8=92=D6=B9p=F5w=FA=91=02g=
+=82pA=92=A1=F5=91?=C0=ADx=8C=00E=1B=88=A1=06[=AAjA=DB=BB=DE=F6=CEI70=F3=C1=
+=13@=E6ASJ=F4=06.=18=82)=8F0=1E=A2=84=C7=04-=00pJG`Y=DB=15=F8=06B=D0]S=10=
+=C5=02=14=08=F2=06=7F=F5=815=05=F4=84P=9ES=06=D1=C2Z=86e=EBg=11y=D29<=C0=CF=
+=FE=12=B4=03=1E=84=B5]=A1=D1=81=0F=8A=C0=05w=FD=05=C60=86A=0F =12=04=1E=CCE=
+=C8=E3=F9=C1Z=DF5=03=1D(A=0BB0=C2Y=C9=C6P=17=C0=E5=07[<K=C9j=10W=C79=D9=81=
+=FBP&d=0E=A4<=A9,)$l=83=CDr=96M=80X_=EB=E08z:=81=11,[`d=17=98=04S=18=F0=08=
+=BC=95=AE=96 =18=054=90=A4=09b=C0=D87=C9=D5=B8(=82=96=0E=82=F0=84=1Bt=A4=CF=
+~=C6^=86_=C2=82=AF=CC=00=06W=C1=CFZ=14=83=04# =C1=0B?P=C2b=FC=E8=901FW[=E0=
+=D3=16=A1d@=06x)d=D3=90=F4=C9\=E0=E3=03 =FC=E0=08=A6=AEko^0=84=BA=8EG<@=E0=
+=01=0Cl0=83!=AC=15u=82=84=18^=8B=03=9C=12=1AtC"=DFo=0El=90=84=06=CB=A0=DAY=
+=06=F0=EDVY=D9S=8A'=05H=00=EC=7F=93}=F33=97`=04DH=CD=14=8F=A2=B0.=97=E0=05=
+=92=EA=D0=B0o=B0=84=8E@k=05T=00X=A8=90ITr=1BU;=1D=06Q=D2u=CB=B2=18=ECO=08=
+=C0=13B=13=9C=90=05=A2=E9=E0}s=FBKr=B7=B5im=05=FB=0A=C8=03=87=A2m=E6R=DE=A1=
+d)=D3=E2iSo=1A=BE=B0=B1=B9=C0=0C!=1CeX=17=C2=F1=11=1A28-=10=82_=A30=C8=C1=
+=9B,=E4=B9=A9=81=11=F67=D4-0x=09=FE-s=09(=0B`=0E=DE=0E=06=BBm=81=B5=CD=8Csd=
+=93=A0W5=B9=9A=F3=82=D0=82=17=A0=F4=B25=E36rz=80T=A4=CA=84}=E2=1EV=D4=A7^T=
+=97=D0@=058=A4=C1=0C=16=D8$=FC=18a=09=C1=FB=02=1D=BE=D0=84=AD=BF=85=08=BD=
+=CF=B7=A4=01=93=B6=81=97=8DF1X=8C=C0=07=CE=83=B4=EE=EFS=F3=B2~=C3=19=CA=9B=
+=88O*<=BDy=A0=E0=C5=D6=02$=C4=A0=06=08=E3S=EA=C2C=83=D4=BD=C9E=A4=0E=B3=0B$=
+=99=C22=8F=B4=B2=AD$=F3e=E7=8C=A0=CA=02=3D=9C=DB=A8=DC=E8=8F=EE=E9=BF=19=02=
+=98=AFA=AA=1C=80=3D=A4=1A=B7=A8K=B0=E9=91=A2=15H=17=8B=1A=82r=19=14=19(=02=
+=B7=E9=FB!&=18=89$=10=BB=1C=18=BE=1C=10=02=1C=F0=02u+;=BC=B0=02=E8=8B>=E9=
+=B3=91=AA=E2.=B4=92=C1=17=C8=96=17=C8=0F=AE"=02=BA=AB;=DD=80?=1E=DC=8D=17h=
+=A0=BB=8A P)=0E=17Z=0B=88=DB=8D=1BP=02=C0[=9D=1809=1Dh=81=1B=B0&=82=10=A9=
+=FC=BB=AC+<=A5=12h=81=1CH=82c=13=C0d=03=02=14=D1=8A=D8;=91=19@=B6=04=F4)O=
+=82=3D+2=96=A83*=DC=D3=A4=1F=F0=01=18@=93=17=E0%B=BB=8BtA=10=1C=88=82(=E8=
+=81=92=F8'&=D8(=AC=CB=B7\a=1Au=B3=0B=19=8C=BE=E9=A3'=17|=A4=19(=9B4=A1=81=
+=85{=9C=1F=BC=C4K=8C2=08=F2=B8=FD=92=82.=89D=1Dp=01=1D=A8*=86i=01=19`=B3=14=
+(=82=8D[=81(=B4=81=13=B0=C2=FCs=81+\=81VB=81=9A=E9=81=CF=03=C3=11=D0=08=F7=
+=087=15`=14=02s=82dK=81=0D=FC=D9=AD=07l=C3=9Fz=C3&=FA=AB=0B=F3=A4=8C=C0=8F#=
+=18#W=E9=81,=01/=B6=A0=8E=1CP=17=15=04=BE=1B=80=82=BB=90=82=EE{D=1C=83=A2E=
+=DC4,=B8'=80=81=88"=F3>L=FCAML=19<=D9D=8Ai=0Ek=19=82=18P=82!=B0=013=A0=C6=
+=BAj=1B=18=E0=01=EEx=01=1A=B0E=13@=81=AEq=01X=BCB=85=A4!=13X=81=CD=D2E5c=AC=
+=15x=01=14=88=01=B2`=81=18=98=AD=9B#=01 H=02p=83 =1Bx:d=84:=DCk=A2=14P,=11q=
+=01=98x=81=09=CB=8F=B8=98=11=19=10H$8=AD=9C*=A2m<=82W=11=82=3D=C1!=B8(Gq=E4=
+=B7=9F=04=1F=E9=BB=09=85i=C7L|=C7=F2=BB=93=C1s=02=AE=F2=96-=C0=89R=B9=BB;=
+=DC=81=D1=B1=1D=C2=8A=B9W\H=AD=BC=AC"xH=88t=A1=81P=01=1B=88!=02=DB=C5=11=10=
+=01=B4<=B3=DD2=01=FD=FC`=01=CB=F09=DD=19=C9=AF =C9=A9=91=C0=A7=A8")=A2=01!=
+=98=01=1E=10=9F=1BC=1D=16H=03=DFj=02=FC(=CC=CD=03=BE=EB=A8=8A[=C2=81=C3=FA=
+=9C=A0=8C=BE=18x=CCG=0A#=F5=CB=01v=AC;=A4\()3=02y=C4"=D3y=9C=88=A0=8E=8E=92=
+=88=B6P=08=8B0=C5=85=88=81=C0=EA=B5=14=BA<=85=04=C3=FF=A2=01=AF=14@=11(=CB=
+=11(=8B=93<K=11=D0=CD=B4=94,eS=0DxA=B0=14=D0=3D=91TIc=B1=89%=D0=3D=EF=88=C0=
+=09=AC=1E=16=18KH=CA=A7=1B=D8=81=A2=83=16=CF=90=81*=C0=15=9Ah=92=07!=02"=90=
++=9D0"=C9=1CO=F2=84=81-;=CA=CC=942#=84=81,=00=0E=F6=C0"=16=00=1E,P=0F=D1=94=
+=08)(=AD=93=8B5=C6=D3=A4=FB3=A5,=CC?=88L=B6=D5=B3M=D0C=CB=B4=14=82=17 =82=
+=901P=06=D5=B9=DDT=B6=D53=AA=1F=F0=0F/=FC=B80=A8=F0=012$=8A=18H=82hSF=DB=DA=
+=1D=DE=BA=01"=F3*=18=C8#=9B=B0=0F=D31=1Dzd=8C=19@=93=C4=8C=8C$X=9C#=B0&=F2=
+=FC=C9=1F=1C=B8=EFk=93=F4=9C=1CF=D2=A2=1Cp=11=16=18%^=04=8F=C1=FB=01=F4=A8=
+=09=BDT=A0W=AB=01"=10=CFTK=01)=E0=CB=1C=C1=BF=AD=FC=C2=00=15=D0=D9=BC9=06E=
+=CB=168=C3-=FD=D2=07]=BDL=CB=8B"=E8=1A=07=DC=88]=E1=AD=0A=AC=89(R&D!=08r=EB=
+=8AG=91=01$=C0I=C6=FC=82n=E9=90=AD=F1=0F=BE=1C>"=C2)$=A5=01-P=A4=10=CB=8D=
+=C7=A1#=9F|D=A3\T=1D=DD=D1=85=F2(=C6=B3=81)=F8=C7 =B8=98=0E=89=BD=95q=88=19=
+=88=88=FD=E9=01Yq.=99Z=A0=85=10=1D^KH=AD=BCR0=D4=B9=02=0D=81=100=D03=03=D3/=
+-=B0=13P=81=19=80=88=1F=8DL=9EB=AA=83=88I=18=FC0=02=1Bx=90=0E=BD=907=1DV8=
+=05=AA=AFX=82'=FC=A7=10=B4=C0"=90=08=D31=16=8C=D4=81=9E=A8=89=12=FD=D5=DE=
+=BB=A8=98l=81=18=D0=0D;:1M=93LF=0D=D7=F8s=D4=E2=08=8F=1C=00=02=14=DB=81,q=
+=91[=D9=9E=DD=D1=0C=87=A8=89WC=B4D=EB=81=A2=19=8A=A0=B8=82=F8XMS}MT=FD=D7=
+=02cU=81}P=B4=14XV=DD=D2=D9=E1=95=1Ep=0D x=90!`=01%p@=DE"=82l=A4=81J=9D=A2`=
+u3b%=D6g=FA0@\=88=B7X=82=E6J=97=F6yX=DD=A9=02)R=81=1D(Q=E40.=1A=E0=01=CF=81=
+=01&=A0=93=EF=B1=A3=19=F0=CBpTTq=0DWr=BD5=17=08=02=19h=027@=A2=DE=8BDR=EA=
+=0A$=18=82=1F@=8D=AA =02=1D=18T"0=02(=98=C3=1Bx=02=DC=12=02R-=D5~=05P=80=FD=
+W=83mU=17=10=81=AF=1DX=06=FC5=81!=E8=88!=98=0A(=B0X=08=82=3D=DE=C9=01,=D8=
+=A8=EB=A8*=87=D8=9D=03c=8A=8D=CD=DB*2=8A=A2h=AC\=09=02U=EC=90=FA`=01=1D=A0-=
+*=92=A5=948&=8EX=8FZ=9DG=EF=A9=A1DT=88$=F0=17=B9=08=CA=9D=E5=D9=9Eu=AFI=A1=
+=C6=9BP=82=FD=08=AB=16=F0=0A=15=A8=B1=EB=98=8A=1D=90=04!=B8=02+H=82 =D8=03=
+=C6h=AC=15=D8=0E=19=C02=FF\=C8=AE=05=D8=B1%=81=B15X=06=1D=81=BFJ=81=C2E=82=
+=A5:=0E=89=C5=12|#=A3=3D=DB=D6d=CA[=E65=C9=96p=88R)=19=DD=FB=8A=1F=A0K)=C2=
+=BE=9B!=0B=8EpF=12=D2=81Fi4|=AA=82(h=D2=BA=B8=BES=BB=DCEm/=CC,W=F0=B3=12=88=
+=B8=02lILgJ=81=C6=B2=A6=83 =02=C6=00=03$H=02,@=BE&=98=0E=EA=BB=DB=14=98\8=
+=8B=A8=17=90(*=B5=D2=DB=0D=D0=B1=FC=DD=81=DD%[=11=E0=0E=17`=82"H=01=1B=08=
+=99=0E=D1=88;=B1=81&`=0B'=C8=15=1C=D0=027=80=01{=A4"=E6m=DE=E6=14=B4lT=0D_!=
+*=98=90@=D7=AA^=A8=D8=9A=8C=A8=81 H=B5.a=82=C4m=8B=B7=C8=95=BF=B4=A30z$=F4M=
+=DF=83=1A=BFEM=18=1B=F8=CE=EF=04=02'(=8F=180=9F=E5D=01=16=F0[=D5=18=82=C1=
+=08=83=1F0=02#8=02=C5h=82=8D=B2=99=14=A0C=F2=B0=81k=83=81=15=E8=0E=1D(=A5=
+=FDS=E0=05=D6=C5=B1=15=DB=07n=D5=DD$=01=15=10=0A=8A=99=A1=BF=82=0BOz=1E=1C=
+=A0=18=F2=F0U=96=C9*$=88=1A%=B8=15=818a=BDmN=A0=B5=82 =80=82=18=18K=0E=13=
+=88=19(=16=91=DC=0A=D5zN?I=A0=C5=19=8D=12=F9Q}=B3=0B=E1=F5=AE=1D=1Cb=DE`$=
+=F1=D3=AB=1Ax=01<=13=BFv=04=9F=B7*=022=8A=A5=820*=17=FC=F0F=8B=92=AB=1BP=03=
+=B7=E2=A5=DF!=3D4=FD=A6I=C1=81=E0=C0=8F=AD=CD=C56=06=C38~=E09=EE=3DZ$=81=1B=
+=A0=E3=11@=81V=A2=BEf=DD=B3y=BC=8D0R=0D=3Dl=8A=D8Ed=14=A6=9A=DD=F3=09=1C=A8=
+=8C=D7S=81=1C=A8-8s=8A'=08*=9C9=91=1C@=83=E0=02=82=99=B0=81=EB=9AB=0B=FC=A2=
+=B3=10=E3=17=D0=02=D5=08=C2=1B]=18L=BC=CC:q=A4%=F8=D9=B1=F4g=A3=C4f=BF=A8ZL=
+=19=08=81=A8=81-=C8f=18=D0=01t=B5=01=933=8Bk=D5^h)(}2=99W\c=00;f=06N=E6=AFE=
+K=18=10=820=18=81=1B=90=D5=9B=93=13=1F=12f=F6=1A=BC=F0=A8=A1=A3=BB=DB=CER=
+=01o=FEf=0C=A9^=1Cp=82=14D=1D=A8s=81=09=A4=9E=14X=81,=88=93g=B3=89F=DB=81=
+=B4=08=0D=1BX=02=FD=E0=A6=17=93=1F=ED=CB=89=C8=E4=C1M=0BhG=9A=94|=FC=8A#=BB=
+z=93=FD=EA=A3=DB=88Bn=AD=BB=B4J=88=1B=E8=BD @=14=14=B8=A8=06S=88=1F`=82s=B9=
+=18=89=80=0Ey|=93P=11=9Db=C6B=91vc=92=1E=D8=11X=88"=D8R=15p=D53=DB=88_=B3=
+=99G=96=93=F1=1B=8F=00y=0C=ED@=943=CE=E9=8D=AD=CB=8E=D0=D6=7F=0CV=1B=00=B4=
+=AEX=01=16i=B7=1E=D8!=1E=E8=0C=1F=A80=D6P=0F=FC=A88=C2=B9=8E=C1@W=A5e =BB=
+=1B%=F0B+=AC^$'=B5=01=DC=FA=EA=CCq=81=B8=95=E7=88=F3=BE=B4jX(=E2X=D7=C2=BE=
+=17 =D16=82=9A=C5Y=0B'=00=02=E6=B8=E4=16J=3D=CA=FB=AF=FD=EBkd=FE=EBV-lX=DD=
+=D2=11He=F8=BB=AC=16=10=83 t=CB=DB=B8=829@=02=A3Z=82=ED=A0lD=AE=CB=19@=02d=
+=F5=A4=CD=8E@=97=08=C9)NL(X=1A=02"=02Y!=9A=D0=C0=82=EA:=A7=A9=0E=17=D0=FB8=
+=17=9D=F82=C9i=01=80h!=B0=85=8B=176j=B8H=A8=D0=C5@=82=0Aa=F0=E0=F1=82=06=12=
+=1B=0CY`\=A1=11#F=81.|=DC=B0q#=A1G=1D=0C=1B*|=A1=D0=C6=12=164P=9C=88=19=13=
+=85=0A=19PT=9A<R=E3=C5=8B=1B2=04=BE=C8=B8=91=05A=81O`=9C(=A1ti=09=13$=9EB=
+=1D!u*=D5=AAV=AFN=0D=A1u+=D7=AD"Dt=F5=FAu=EC=08=15$F=9CE{=82=85=09=137t=C0t=
+=C1$EL=172=EF=E2=CD=ABW&=8A=BE~=FB=A6`=D1=03F=0C=1A)=FE=FE=BD=8B=F8o=0A=15=
+=3D|=D6hQ#F=0C=19;r=EC@=93=A4=CA=93)@t=0C=C1=81D=86=0C=18=A6y=A2~a:F=8D=1CA=
+|=D4=A8=A1#=88=95=16=1C=1B=12=C4=11r!o=DE0r=80=A1ac=C8=8C .b=B4=D08=B4=87@=
+=1A=3D=C7=F4=A0Q0=E1=8B=1C$=3D=FB=BA=90b=10=E7=8A=18/z=C4lk"=E6D=1A0=16=92a=
+8=DB=05G=E5-=B0=D88=B2=03=87u=98mK@q=E2=A2=04=D4=FEi=B1=02=18=E0=08 =10=18=
+=96=81=07=8E%=C2=08_UU=C2~O=B9pER&=D8=D5=D6=0A=E2=8D=B7=97=86z-=06=D8=0A-=
+=C8=C0=85=0D=88i=D8a=0A=8D=BD=D0=C2=10:=AC=F6=18=0EF=00=91=84=13O=0C=B1D=0E=
+8=04AC=0C=A6=C1=90=1A=8F=A6=CD=D0=84=11C=EC@=C3=167=D8=C6=11Q'%4=83=1E=3D=
+=BA=C0Co=BC=BD0DH5=D0@=03=18=F21=E4=1E=0D9=EC=94=84=0DF=F0=E0=83=0C1=E8=F0=
+=C2=0F=3D=00q=1D=88/=D8=C8C=8F1=F4u=02=0A1`x=C2=0B=87=EDh=03J=03=B9=B0=83m=
+=CA}H=C3=0C6=D8=E0=C5=9E'=A4=B0=C3=9D2=98=C0=9F=7FQ=09XiU=04bz=A0=A6b%=C8=
+=E0=083=A0=05U=A4J=DD=FB =1E=A4=18f=B8=A1=AA3u=A8=C2=0C5=1C=96=D8=86=1D=A2p=
+=E2=0Ag=9A=B6=C3=0D7=E2p=C4=0FA=14=E1=83=1D=98=CD=B0=83=0E8=F4=C8S=18;=C2P=
+=DAj2=18jhd=1C=11AC=0D=12=15T]=0D7=DC=10=C3=94=0A=FD=1AC=10J=D8=A0=C3=0D=86=
+=1A=E1=02=A1=1AI6=C5=0EF`=89=A5=0F"=0D=F1=03=0E=0A=A1do=139=C0pXx=E1=B5=F5=
+=AF=9D+=B8=10FCJr=84=C3=0A*=F0 =83=0D=3D8=91=C3=99=93=B5p=03=110D*j=7FNY=DA=
+=F1=80=98=16=B8)=82=9DzJU=7FL5=85j=C0=AB=B2=BC=98=0A*=D4=F0=9B=0A~=B5=BCXc4=
+=90=C6ca=DC=DE=00D=11=11=E5`=C3h=CC=9Av=03O1=18=C1=DAj=94=91=A6=83=0EBu=F4=
+=02=119=BCpl=0FE =AB=D2JT=02=E1D=0C7=E0=10=05=0F3=CC=80D=0C,,=EC^=91I=CC=90=
+=FCe=96=B1=F5=C0C=D6=D7=B1=C7=C2=0C<@=B1=03=AC=F6=A9,=9E=9D0=E8=E0=C3y=17!=
+=DC=C2D+=A40C=0C9=14a=85=99=0B=CF@=14=0B7LXBR=93>=E5=B1=80 g*rX$=97<=D5=C9L=
+=F5=8D=97=BF,=EF=85X=0A=85=B5@3=EA=AA7=A6B=0CH=F4H=18=0E@=CCk=03=13X=CEp&e0=
+=10=11C=15=91=F1=FA=C4=8ED=F0`=BC=154=B8=A4=1CQK=B80E=12R=F4=80=83=0D>=F0=
+=B0#O=D4i=BD=10O=BF=F1=90=03=13=A4=D5=A0=D1=CB*=18=1AD=0Ed=BF=FDC=0D=07]=1B=
+=C4=E0=0A=E50=04=CC:=04=3D=B9i'=F4=3D=1E=0AC=F4=A0=07CP=C9=0B=94=90=A4=9F=
+=10$=08dB=81=FC=96p=83"`)#,`=82@=F87=83=B5HJR=98=03=D0Sj=F0=1F=ABp=AEs=9E=
+=D3=0A=E8=14$=95=17=8Cn)(@=15=0B4=14=83X=A1./=FB~9=8C=0A=80=00=C3=182=06=06=
+,=90=81=E2l=E0=9D=170k=098=88=81=0D=EA=C0&=0A=F2=80=06Dp=02=B4>4&&tk=07/=C8=
+=11=10j =83=85=BDl#J=F0At=DE=F6=BE=D6=C4=CC4O=A0Zo=BE=B744=01=81=09=E7S=C1=
+=87|=C2=C3=1A=F8 =09T=C8=01H=10=85=03=19=14a$=05!L=0A=BA=A3=12=82=C8=80=7F=
+=E3=19U=CA=DA=02=B3=17=F4=C5i-P=C1=E4=AC=E3=91=17=AC=E0=04=A5Q=8D=0E=90=A0=
+=83=E4=AC@=09<=B0=C1=0E=E2=D6=02:=C1@),=C0=1C)=1D=E4=82=CC=05(=84"=F4\=A7V=
+=F0=C1=11=9C=A0=05=92"=02L=9A=D2=02;=B5=C5z0=A1=CB]R`C=D7=C5=90/}=F9=81=BA=
+=EA=84=C3=19f=01=06B=10=C2=0Fn=80=84=D4=00=D1[=17I=DB"=B3=C7F=F4=DD@=088=98=
+=01&yp=83=97=F9=E0|-=F0=81t=0C=D56)=DC=FC=E0=8B"=A1=CF=0D=92=06%=EF=F9=880S=
+=90=01=1BO=C4=C8=17=8CM=065=D8A=12"=C2=83=00=E2=C0z=97=81=12jz@=17=1D=02=81=
+=06=3D=F0=C1=0A=EF=83=B2=B6=ECp=850=B1=13lx=103=82Ha=06L=18OA*=F6I=B4=AD=A0=
+=06EXB=13=E8I=14"=B4=A0=08-=C0=98=83=96B=02=A5=BC=94=05A=8B=81=A4,=A5=CA=90=
+=89=0Ct#=B0=0DLI=B0=83=90*E=05QjKB&=8A=823=1C&=92=8Ai=96=AC=82=C9=AA=C4=0D=
+=C1=98=AF=EB=0Bi|=F8=02=D9(=E14=DB=DB^B=B0=A4=82=13=A5=C0=057=00+=0BL=C3=1E=
+{J=C1=05_=05k=16=AD=08=ADM=E2 =097X=82Hl=00=04"=DC=91=AB=D9:=9A=19w=94=84=
+=17=AC5=05E=80=01=12=86P=99b=E9`=07L=B0=02=16=A6p=04=DDdM%10=D3DO=90=83=BA=
+=80=C8=A1=0F=8D=C2=A9=FCV=85=1B=FB=04=C1=81<A=81=0CR=80!=19 =89#6=90=01=0A`=
+=90N=1D=BA$n=8Ct=81=10d=C0=02=17=1C=A1uj=ED=81=0AZ=E0=14T=DA=F4=A6a=19=01=
+=0C=18D=16=05=91=85=04h=93e_j=9B=82)=B8=C0=08Bu=02=0E=0C=DA=17=EBT=A7=B2vB=
+=01=10l=F0U`:=150=3D=90j=CD=D8=F8=9Df=C9=8EY;=98=1Fu=9A=A0=03=1E=9E=A8/t=AA=
+U=10d=F0=020=E8 V=8D=01kc\=F0=93=D5=CE=A0=07y=8BH=0F=84`%=1B=D4V{=DD=E3=96=
+=10=08=D3#=D4=02a=06/=DB=C1a=EC=D9=B0=18=B8a=8E:p=C2=10=9C@=063=FC =07=E9=
+=E4=17>=BB=E3=02=BA=04le-$$=CA=F8=B7=94=80=A9=E4=0A3P=C2ar9=AA=13=D8=C5aoq=
+=C1e=A5=99$=15=9CK]5=E8=01$=7F0=83=B0=1A=C4A=94=F2=D8MA=A0)=11=90=A0=B7#X=
+=C1=08=F8#=FB=BA=13=08!=068=BE-Qp`8=16H)=07-=98=81i=EA=85=C7'=A8=0EZ=B1=02o=
+0=A9z=BA=D7=AD=E0 =8B|=D9=89=B0=14=03=1E0=A1I=A8Y=CD=0E=8E=DC*=1C=E4=F7%=B5=
+b=CC=CD~pXc=ED=C0=07J=80=C2=0F=84=086=F9}o!0=C0=1A=BF=CE=D3I=C4=A1=00=91=B5=
+J=81}=09=03-=F5=8D=CB=0BqHB=10=8E=F5=03=D3=80=81=B5}=19Q=E9\=C0b=A6=C8=92t:=
+H=97ml=00=03=14(=85=902h=0AiM2=83=17=B4E=8A=0Eq=8BGb=C9=82=14=85=B2)$=88=C1=
+=92=99,=DC'+=C8U%X=01$I=E0=16=97=9CX]=CE=BB=CD@P=83(=1E=CC=8C1*@=16=0E=BEK=
+=DE=F0=A2=A0=08=DAUU=AD`=90=1C=D5}S2=02=D9f=B2T=13=032=84=DBfI=10t_4=BD=E9=
+=19=AC@qh=12C=0EtP=84!h!=09O=08qH=FB=84P=04#=F0=04=07=EF=93=CC=0D=ACD=19=E6=
+=F8=E0=85=84^=01=EDbP=84,=D0=A7=0A[0=02=8D=C2=A7=83=1D=1D=99=07=BCl=8Bi5=DB=
+=16=DF=A2,=D7-=86=09=0CT =86=16=80'=E6K=B9W=EBJ=80=83=A5=9C=A0=06=99.=81=14=
+L`=83Q=01=DB=A5=FD=D1=1CU=9A=9CS=05a=11#%=08=CC=0A=D0=96=C5=AAc=A4G=02a=EF=
+=1Fi=90=9C=99=C5nG=E6fsxy=B0=EEY=F9=1Bv_U=01=0C=84c=83 =FC=FA=058=F8=01=A6=
+=C9;=CC[=DBL5=93~=96=C9=C7=C5=05),!=09=C7=CA=81=82U=833=16=B0d=0DJ=00l=B8=
+=83P=A7=19=B6@G=A6A-=A0}`y=1E=EC`=9E=E2Z=C1x=01=86!=9D=E3Z=E7=A4=CB=F5=09=
+=96 =84=A4=BF=9A)*I=FDea=9DA=0D.=9D=E9Sq=FA=A6=BER=03=0A)=10=08'=E0,=16=D9=
+=A8=11k=09=C4=06R=FA=FB=01=11=B2=95=10=19=0CQ=9F=E8C#k=F2M=F66=F3rU(x=89=0C=
+g=A8=82=17=8Ca=07=BE=BA=E3o@=04=03=A1=9F=BD=B5=EBf=CC=0EwD=1F=B9=E5=C0gG(=
+=82=10.=B3=83*8=EB=05W=88A=1B )<?=B5=E0tR=ED=F7=0C=F6=18=1B=DDp=8Bg,=81=13=
+=F4N=0E=D4=DDD}=9E=E8=A5=9E=02=AA=C4=B1=B1X=0A=A5=C0=10=F4=D4S=94=80,=8D=8E=
+=C6=F8=87=EC5=DD=B3A=9B=820D=17TF=EC=B0=11=0B=FC=80m=B5@=13|=1F=8C=D8=C0`=
+=01=11=D5=ECY=0C=90=8D=E1=F0=84=0D=C4=17=898=95=E9=A0=CE=05=A5=CE=EA=E8Me=
+=E4@a=F5=80w=B0@=0A=D0=A0Z=85_=F4=09=13`=D4=84=B3=88=8B=C5 =81=E5=F5=93=0E=
+=D8=80=A1=10F=E4t=C1=DEp=DAVQ=9F=0C=B9=00=F0L=92j=CC@H Jl=BCM=CD=AD@=F6=B0V=
+=14`=8Cf=89^=1B=92=CEx=FC=08A=E8=C1=DE=C9=98=80=04NJ(=F9=C7=1C6=9B=06~=8C*u=
+=05=09pE=82=A0=C5=0A=10=16=9D=C5N=9C=09=8E=0B=D4@=DB=BCMB=F5=C0=B1H=D1=9EY=
+=92=A1=19=C4=D85=D5=0D=CA=04=0D=B0=0C=12=E2=C5=0CM=86=0DT=17=C4=D8Vi=F9=05=
+=0D=EA=85=96U=1F=AC0=D2=0F=A8F=0D=84=81=11pK=15=F0@=12,=D4=85=01=C1=B9=C8=
+=CE=9E=F8E=CC=B8=00J=DDZ^=F4=CB=89h=D5=0B=CC=C0=14=D0=11=19=D2=003=A1=8D=0E=
+=90=19a=BC=80 =B1=A1=1BJ=A3=09=B4=80=0E$=85=1EBE=05=EA=A1=B2=91R=06=F2=E1=
+=EC=F9aX=A4=00=C9=8C=80=BD\S`=9DOH=BD=17=FB=C4=06=A2=88D=F6=D4=8Ej=C0"=10XI=
+=B3=1C=E0%b=E2=EB=CC=CA=A6=01=01=BF=F0=00=12,=C1"=AD=19L=90V=F8q=08=3D=FD=
+=86=0D|=0F=0D=E8=00=13=CC=CBc=D4=C0=12=14=CB=B4DT=0A=18=81Q=FC=1D=C6=ABpb'=
+=A6=80=18`=CB=0BH=C1=8E=0C=C1-=CA=00Je=84"RM=9F=ED=D2xH#S=E4a=8B=ED=C0=A8t=
+=A36b=CE)u=E3=1E=F2a8=86=00XxE=0AH=05r=0D"=16=B1U=9C5=07i=B4=8D=13=18=C1=0E=
+4=93=88=E4=00=98=DC@=BD=B9=80=A1TU=AA=0D=E4=B9=E5=E3U=CEDc=C0=80=13=00=81j=
+=A8D=0E=10=813q=A2=0A=B4=CC=B8]=0C=0D<ej=C4=80=12=CC=1D=0F=08A=12=B8=80=0D,=
+Lc=18=93&=DA=89=9D=00=99RA=A3b=F0=90u$=C4=E0=D8KB=08=CD=19t=84C=9C=9C=A1=DD=
+=9AxH=E3M=BE=18Kac6>=E6M=02=D77J=85N=86=C0=0C=90=90=08=E0=0C=09=D4=DCY(W=11=
+=B0=C0Z-!=C9=DD=11i=CC=C7=0E=FC=93=E5=1D=CB=0D=C8A=C4$=8BiX=DD=0B=81U=87`em=
+=DA=09=0B8=C1d=94U=0C=DC=CB=F7T=C6=D9=FCI=DF=0CX=1F=8F=F0=84=9CP=0D=0Ct=A5=
+=BB=B5=07]j=08=0D=1C=81=E7=AD=CC=09\_=99H=11oD=C6@(=89G=D8=00=18=10=93=0C4=
+=81=0B=DC=CD=0C,=A0dfc=0B=95=0AyF=E6MR=E6=A5pNW=88=80=0A|=05=09=04Fc,=0C=0B=
+L=1Ba=F1=17}=F6Nex=C1[=F8=00=11<=1C=10=84=D8=DCM=06=8FP=01=15=FC=94=3DU%>=
+=DA=A6S=05#`=B0=C0=D9=18A=1E=F4=88=94=94=01pjH=0D(=06=0A=84=E1=0C=E0=80=D6l=
+O=8F=E0_=0B=88=C4=9D=BD=99=E7=C5=C4=8A=A9=0C=8E=EDH=84&=CBI8=84@p[Q$=07=0DD=
+R4=A2=0Cy=8E=8E=0A=A4=E7S=A0=90=8E=C6=DEz=82=E3*=B9=C0W=B0=00=10=B4=80O=F5=
+=91=89=DD=92=09=A0=00=F8=B1=95=0A=AC =D3lXaM=81=19 =01=10=04=01}d=C1=0F=AC=
+=17a=A0=09=0Cj=1C=ADXe=83J=9Fv=FC=F9=85O=10=01=10=C4=C0=0F=FC=05=0E=E4=E3=
+=0CU=DD=B70=C9=0DL=81=17"!=AA=E8=094=E6=C9=99=C4=CA=A9=0D=01=8Cb'F=E4=C8HH=
+=13=0C=1C=1D=8E=BA$=90f=A3=09=EC=1A=A3=EA=A8=90=82=10N=ED=A4=08x=D7=0B1=8C=
+=A6Q=86=0E@=81=9C8=01=A1=A9=00=DC=E9]=0C=E0=95=C1=0D=01=144=C1=12=1C=81=10=
+=E4@=12=FD=0E=0C=F6=00=C0=CD&=99b=A8=99=EE=C5=14pH=0A=04=01o=96=CF=89=E6=C5=
+=8Ar=08=0A=AC=A3B=91=91=9B`=07j=B0=96L=94=0E=9F=8A=07*=C6=C4=0B]=D1=87=FCQ5=
+=DA=00GH=E1j=9DG=0D\=A4=A2*=1D=A4>ELz=ABzJ*{z=05Y=EE=CF=91=9D=00=13=08A=AC=
+=C0=C0=0F=0CA=15=D4\
+=A3=C3=10=1E=98=B2=A5=E5=0DA=13H=81=B1=DC@=12=F8N=D1=08=C1N=C8=E6=82=DA=A0m=
+=EAix=A5=C8=F60=01=DE=E9E=FFD=E7=09=FCtK=F8,=81=B8LAZ=16=90=BB9=C4=82mS=0C(=
+U=FF0k=86=F8=8D=9D=CC=80=10=B4=CDw=AA=C4=F7q=EC=14nK=88=C1=C0=05)=A6=E8=85k=
+=7F=F0@=CCN=CA=B8^=85=93=F1@=FE=A1=00=0B=A0=C5T=F4=00=09=AC=80=0E=A8=80=AE=
+=84=E1=99=04=E1=10B=E8=DA=ADc=B3=08=01=B7=B4=E5=0E=84=81D=F6=80=EFT=01=9C=
+=F0=00=10=C0=00=13=94=DB=AC:=1F=83=C6=90=C3>=EC=ACl^=ED=88=19=C7=E6)=D8=86-=
+=0A=B0=A2=C4=F1=00=0B&${D=9EW2=04=0DH=C9{}l[=F8=00=DA=92=C7q=04=81=BD=80=A8=
+=B8=04F=B1=A0=816=F5KL=F4=C0=CB=E6(=CD>=C5=0C(=EE+=D9lV=A4@=09=BC'f=BA=E7N=
+=12=8C=0C=8C=80=BA=CC=80=D0=D6`_=B0=80=12=1E=9B=D4=C0=80=12=A4=13=1C=D5@=B5=
+=E0La=F0=D0=D8$=16=0D=18=C1o=F8=00=D6R=18=AD=DAL=BB=01'=DA=FC=E6=C9=86=0Cd=
+=0A@A=16=E8=C0=0A=AC=1B=EE=86=ECL=B8=00=91=D1=80=0A<=81=0D=F0=8B=F0=84=D4A=
+=08=CD=D1=C4=97w=C0=A9=F0=AE=E8=F4=C5=A5=F0yG=C5=98=C6=89=B8=C0=0F\=C1=12D=
+=C7=0E=C8N~1=C1=09P=DB=AB5=EES$=A9=B7>n=80=F0=A4=C8=FC=E4W=90=CD=DAu.c=C8=
+=00=13=90=98=BDi=929=91M=18=DE=80=1E\=D1=CB=14=C1=F9=0C,=C1=0EZ=87=AC=80=A3=
+=EC=05=F5=E6=AE=0C)F=0A=FC=06=10=05/=F5=F2EaxS=B8YT=0A=FC@c=C8=C0=9A4=D3=8E=
+ =81v=8D=87=0A0p=C0=C0D=F6JF=0A=C4=E1=0D=D4I=0A=AC=14=81N=CB=0E=B8=00=0E=84=
+S=0E=E8\=FA>=05=0AHfM=B5=EF=E6p =09y=05Z=B4=16=7FuH=84=A6@=12=18=0EI=14=DF=
+=BC=DC=C0=0FPA=84=CE=C0=11=88f~=1A=B0=CB=F4=C5=F5=F9=EA]=94=F0=F0=96=C8e=00=
+=11$=FCU=D6=16=EBI=0A=E0=CCK =05=A3=CC=D0=0Cx=81=0E(=81=8D=E0=911=E1nC9lL=
+=C4Y=8FL=94]=C6=84=0C(=E2N0=02=DC=0A=84=96=8D'=0E=BF=94=B8=F6p=A54=19=A5z=
+=C5=F5=F0=00=16=3D=A5=EBP=15=D8E=10=E1dD=0E=84=A6F\=11X=1DY=FD=D2=CA=9A=B9=
+=F0H=9CA=99=DE-=05o=08=D85=8Apjq=09=DB=09=8B=94i=ADP=C6[T=CB=DE(+=DA=C6=DC=
+=8A=B6=C0=15=10=01=1A R=8A=B5=05w=FD=91=97=B1=80=0E=90=C1N=D0=92C=3D=A0 =FF=
+=967=16rp=1D=B2{F=EE=85=A4M=11x=07=9A=1A=13=C6 =05=C6=AE=CB=1A=A9@~i2=99=A6=
+=C0=11=CCPYy=17(o1=B0=96=C8=F5=15U,3=F0Zt=81=0D4=E7S=9A=C6=EDT=AB=E2=9C1=D8=
+=BA!=AA=D4=C0m=F1=A9 =D9%=0B=1C=01CP=B2=DD@=91=86=06A,=BD@=E2=0Ar=CD=C5&s=
+=C7=1C=F2*U=AA=82pS=97=D5=EA=CB=CA=D2=0B=EC@=0C=AC=11=15W=B1=CD=A8=0B[=1D=
+=07=0E=0C=A45=A2=F38sq=F5ip=EB *=C3q=C8NT=E2L=C7I=A8=86=D1$=AB=CA=B4d!9*=
+=8D=81=ED=09=AC=88B=02=11=0BpA=13`=9EET=04!=C9=E4A=E3dB=A7=D2B;=99{N=C5=84=
+=08=12YrH[@=85=13=BC=14r=AE=00=10=04=96=82=D2=AE=87=C8=C0"=B3=D1=11=80=C6=
+=0D=FC=1AM8=DE)=8B=F4=C1=A6=8E=0D=FC@=0BA=A7=F0=E2=18=0Ex=AC=9D=F8=84=D6=10=
+=04=19=F5@=10=DC(=AA=CCt=AE=05L=13=F0=CD=1B=EEmRM=1F=0C=80=C4=D9=08f=02N=A0=
+ =1B=B5=E6=04=04=00;=
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: q-p vs. base64 test
+
+Received: from thumper.bellcore.com by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA18271; Sat, 26 Oct 91 06:35:15 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA13322> for mrc@akbar.cac.washington.edu; Sat, 26 Oct 91 09:35:12 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA01130> for mrc@akbar.cac.washington.edu; Sat, 26 Oct 91 09:35:10 EDT
+Date: Sat, 26 Oct 91 09:35:10 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110261335.AA01130@greenbush.bellcore.com>
+To: mrc@akbar.cac.washington.edu
+Subject: Audio in q-p
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=hal_9000
+
+--hal_9000
+Content-Type: AUDIO/BASIC
+Content-Description: I'm sorry, Dave (q-p)
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+
+=EA=EE=F9=FD=FB=F7=F1=E9=E3=E3=E2=E4=E7=E7=EC=F3=FD=FExwsomhca`]]^\ZYY[^c=
+k=FA=E9=E4=DD=DB=D7=D5=D5=D7=DA=DC=DD=DE=DE=DF=DF=DF=E2=E4=EB=F2=FCpmh^\Z=
+YYYXZ\^`dghhkmmnuy=FE=F4=EB=E4=E0=DD=DC=DA=D9=DC=DC=DD=DF=E3=E5=E3=E0=DF=E0=
+=E4=EE{jf_[YVUWZ]_bfmy=F8=EF=EA=E4=DE=D9=D8=D5=D5=D6=D9=DE=E5=EFynomiea`_=
+_cfcba^\Z[aku=F0=EA=E4=DE=DC=DC=DD=DF=DF=E0=E5=EA=ED=F4=F7~tz}=7F=FC=FE}|=
+=FE=FDzzvpmlou~=F4=EC=E5=E1=DF=DE=DF=DF=E7=ED=EF=F6=F7=FA=FExqmf^XSQPQSVW=
+VX[cu=F4=EA=DF=DC=D8=D3=D0=CF=CE=CE=CE=CF=D1=D5=D7=D8=DB=DE=E5=F4tf]XRPQR=
+SSTVY]bo~=EE=E7=E1=DF=E1=DF=E1=E5=E7=EA=EC=EA=EA=E7=E4=E5=E6=E5=E8=EF=F4=F9=
+{kee`\^___afgkpz=FB=F2=EC=E4=DE=DD=DC=DC=DC=DE=E3=E6=E9=ED=FEonjlrplhgmlj=
+npqsz=FA=F3=ED=E8=E8=EC=EE=EE=ED=EC=E4=DE=DE=DE=E7=EE=F4xlb\YWWUVX[]_bhs=FE=
+=EF=EB=E8=E2=DE=DC=DC=DB=DD=DF=DF=DF=DE=DC=DB=DC=DD=DE=DF=DE=E0=E8=F6voic=
+_^[YUSRSVW[\]^dp=F1=E3=DB=D6=D3=D1=D1=D1=D1=D5=DA=DF=EA=EF=EF=EF=F5=FFwkg=
+c]ZXVUV[an=7F=F7=EF=ED=ED=E9=E6=E5=E4=E4=E3=E5=E5=E9=F3=FB=FCzmjjihjlppqz=
+=F7=EE=E8=E1=DF=DF=DF=DF=E4=EA=F8vtqs}=FDzwskihddgijhilrz||}=FD=F5=ED=EA=E8=
+=E8=E3=E1=DF=DF=E5=E7=EE=F0=F4=F4=F4=F9=FA=F9=F7=FEnia]\YWUSTY^dmz~=FC=F1=
+=EE=E8=E0=DC=D8=D6=D4=D2=D0=CE=CE=CE=CF=D0=D3=D6=D7=DE=E8=F8vj`[VSOLJIHGH=
+HJLNQV[d=7F=E4=DA=D2=CC=C9=C8=C8=C8=C9=CA=CC=CD=CE=D0=D3=D6=DA=DF=E9=F9yu=
+oha\VRONMLLLMPW]gr=F6=EA=E1=DD=DB=D9=DA=DB=DB=DB=DA=DA=DA=DA=DC=DB=DE=E3=E8=
+=ED=F4wqjc`_bdbda_bged__cccchr=F6=EA=E6=E3=DE=DB=DB=D9=D9=D9=D7=D5=D5=D7=DA=
+=DF=E7=EE=FCrrnd^[YTSTVW[^biv=FB=F4=F2=F0=EB=E9=E6=DF=DC=DB=DA=D8=D9=DB=DD=
+=DF=E3=E7=EC=F0=F9=FEqhd__^\ZWUUSRRVZ^do=F3=E7=E0=DC=D9=D7=D4=D3=D1=CF=CE=
+=CE=D0=D4=D7=D8=DA=DC=DF=E5=EC=F9}umic\WVTSRNLKLNOUY]bio=F9=E9=DE=D9=D5=D1=
+=CE=CE=CE=CE=CF=D1=D4=D7=D8=D8=D9=D9=DA=DB=DD=E6=F6tdYSONMMLLLNPSY^dk|=EF=
+=E6=DE=DA=D7=D2=CF=CC=CA=C9=C7=C7=C9=CA=CC=D2=DB=E8xcVOLIGFGHIJKLNOT[_q=EC=
+=DE=D5=CF=CB=C7=C4=C3=C3=C3=C4=C6=C8=CC=D1=D9=E3=F3i[TOLJIHHILNQV[ao=F6=E7=
+=DF=DC=DA=D8=D7=D6=D5=D7=D8=D9=DB=E0=E9=F2=F9=FDxnjjfehlmlmrpoqw=FF=F3=EF=
+=EF=EF=F4=F2=F0=EC=E7=E6=E3=E0=E0=E4=E9=EC=F5|og^ZWVVUSRSTW[_n=FA=E6=DB=D7=
+=D3=D1=CE=CC=CA=C9=C8=C8=CA=CC=CF=D5=DB=E8x`XSPNMKJIJLMNPSY]f}=EE=E5=DD=DA=
+=D7=D4=D2=CF=CE=CE=CE=CF=CF=D2=D7=DC=E2=EB=F7zpjea^ZYZZZ[[\]][Z[]agr=FA=EA=
+=DF=DB=D6=D2=CF=CE=CD=CB=CC=CD=CF=D6=DC=E6=F6se]WRNKJIIIJMSX\aq=ED=DF=D8=D2=
+=CD=CB=C8=C8=C8=CA=CB=CD=CF=D3=D8=DD=E2=E8=F2ug_[VPNOOOPPPONMMNQV^k=F4=E0=
+=D7=CF=CB=C8=C5=C3=C2=C2=C4=C7=CA=CF=D6=E1=F6i\UOMJHGGGIKMQZfz=E7=DE=DA=D7=
+=D6=D5=D2=D2=D3=D4=D5=D6=D6=D8=DB=DE=E0=DF=E4=EA=F1=FEsib\YUTSTTUXY\`m=FF=
+=EE=E6=E3=E0=DE=DD=DF=DF=E4=E7=EB=F6=F8|=7F=FD=FB=F1=EE=EE=EE=EF=FA~~|uun=
+jooox=7F=FFxyutnmms=FA=F3=EC=E6=DF=DA=D7=D3=D2=D4=D6=D9=DE=E9=FDi]XTSROOO=
+OQTW]`jr=FE=F1=E8=DC=D7=D6=D5=D5=D6=D9=DC=DD=E0=E4=E6=E9=EB=EE=EB=E7=E1=E2=
+=E8=EB=EF=F6|qjig_\[ZYXY\]_hy=F3=EC=E8=E5=DF=DD=DD=E2=E7=E6=E5=E7=ED=F6=FB=
+~vmhfb^\[[^beq=FC=EA=DF=DB=D6=D2=CF=CF=CF=CF=D0=D3=D7=DE=EA=FEk`]YWUQONML=
+KJJMOU[dz=E7=DD=D7=D2=CF=CD=CD=CC=CA=C9=C8=C9=C8=C8=C9=CB=CE=D2=D6=DB=E7=FC=
+k]WSNKIFDCCDEGJNSY]`p=E9=DF=DB=D7=D4=D0=D0=CF=CE=CE=CD=CD=CE=CE=CF=CF=CF=CF=
+=CF=D0=D3=D7=DA=DC=E4=F9l_[WROLKIHHHILPVZ]h=F9=E8=E3=DF=D9=D1=CD=CD=CE=CD=
+=CC=CA=CB=CF=D6=DC=E5=F6sg]WSQRTST[fgban=EC=E2=E4=ED=E5=DB=D7=D8=DE=E3=E0=
+=DB=DC=E5=F7ptld\Y[\]]]cl=FF=F6=F3=EC=E7=E4=EE=7F=FA=EA=E1=E2=E9=E3=DC=DC=
+=E0=ED=F5=F1=F2=F3=F8z}=F8=FB=F1=F8vomptjb][]a]WRRV[][\n=F9=FC}=F8=E7=DE=DB=
+=D9=D6=CF=CE=CF=CF=CE=CE=CD=CD=CD=CE=D2=D6=DC=E5=EEyjaWONNQOMKKMOQSWZ]_dj=
+u=EF=DE=DC=DB=D9=D6=CE=CC=CE=D8=DC=D9=D9=DE=EE=7Fz=F0=F7loz=FC=FFnikv}np=F2=
+=EE=F9vq=F1=E8=ED=EF=F0=F6=F8=F8=F2=F8=F8ujtyi__iodVR\hb]bp{tdds=FD=F9=ED=
+=DF=DC=DE=DD=D9=D0=CD=CE=D1=CE=CB=C9=D0=DC=DA=D8=DFgUQRUMGHNOKIJOY[V_=E2=D2=
+=CF=E3WP=DB=C2=C7=ECa=D9=BF=C6=DE=C7=B6=B8c9B=CD=B5=BC=E5w=DDjG;25Be=EBmV=
+EBEGJ`=E0=DB=C5=B7=B2=C5F>_=BC=BA=DAOV=CB=CCTEQ=D1=C5=D2=FB=EA=CE=C9=E0U]=
+=DF=D0=D5w`mp}YEGHg=CFVEBL=D9=F0UIONA=CD=B9=AA=B32+/=CF=A9=AD=B7=C3=B4=B2=
+o/'.F=C8=BD=C1=C3=CF=EDG;:Em=CE=C2=C3=C2=CC=ECRA>?KZd=FF=DC=D5=E3cXn=D9=CE=
+=CE=CF=CF=D0=DA=ED=F7=E5=D9=DD=F5z=E8=DFvYXbyo][gjVMOTXWa=E5uk=DF=DC=CE=E1=
+]=EA=CB=BEY?=C5=BB=B7=BF<37k=C7=DA=BA=B9=BF=C1O0-6AX=D8=C3=BE=C2=DFE?GUh=D4=
+=C3=BC=BA=C4=DCSB?AMq=ECd=DB=CC=DDeP=EC=CE=C7=D6_=D5=C8=D9^]=E7=DA=E2\[=FA=
+=E0mZfs=FBb_dp^KLOc\K`=DD=D7=F4[=DA=CE=D8pL=CB=BC=BD=BFJ<;N=CC=CF=BB=B5=B7=
+=BBu:/5<J=EE=C2=BC=C1=CFUE@DO|=C9=C0=BE=C4=D7]D?=3D?G[=D8=CD=E8u=CE=CB=E8[=
+t=CA=BF=C6=EC=EF=C8=C9wOi=D6=D9cPc=EAkLKb=F1^IK`uVIKf=E4te=F2=DE=D3=D4=D7=
+=D6=EC=E7=C8=BE=B5=BFH8?=DE=CB=D5=C7=BB=B3=BDP618:>Y=CD=BF=C4=E2TIILMg=CE=
+=C0=BE=C6=D6mNF@BKZ~=E1=D2=C9=CD=E8p=E4=C8=CC=E1=EA=CB=BE=C5{X=F8=CF=EFNO=
+=FE=E0^JK\[JDLXYOM[=FEqiw=E5=DA=CE=CA=D2=D7=CB=C8=CA=DAW=F2=C4=B7=BAY:<o=
+=D7mU=D4=B8=B7=DB>5;<;?]=CC=C4=CFeOR]Za=F5=CD=C0=BF=C6=D1=EB\OKMS_u=DC=CC=
+=C6=CB=DD=7F=EE=DA=E0la=F0=D9=D9sUUa`VSZgum_^cf_^ao=F8=EF=EE=F2=EC=E6=DF=D7=
+=D1=CF=D1=D4=D4=D6=D9=E2=F1gZm=DF=CF=DBWECO_fgw=D8=CE=D3=F7TKIO[m=F7=F4=ED=
+=EA=E2=DF=DE=DC=D8=D4=D1=D4=DC=E7ue^]^]XSQTZ_m=F1=DE=D7=D4=D3=D1=D1=D3=D9=
+=DE=E8=EE{f_an=FB=F8xoruoia]]][ZZ\_j}=EC=DF=DA=D9=D8=D8=D7=D8=D7=D8=D9=D9=
+=DE{k=ED=DA=E3bNJN\]VYl=E3=D9=D9=EEc\\amujghms=F0=E3=DA=D0=CD=CB=CC=D1=DC=
+=E5=EC=FBk]UMHFGKOYe}=E1=D4=CD=CD=CE=CF=CE=CE=CF=D6=DE=E6=EF=FBu|=FC|=FEx=
+jb^ZVUROPPQSW\_k{=EC=E4=DC=D5=D2=CF=CF=D5=DC=D1=C7=C7=D4=F9`\ixk~=DA=CF=CF=
+=D7=EE]QQPPQNKKORWb}=E3=D6=D0=D2=D5=DA=E0=E5=ED{e^[SSUYd=F4=DF=DA=D0=CA=C8=
+=C9=C8=C8=CB=CD=D7=EFpja\[\YWWSRSRQQSSTVX[fv=F7=E8=DC=D6=CE=CB=CB=CC=CB=CA=
+=CA=CF=DE=E6=D7=CF=D4|PJJJKLT{=DC=DE~]RONQ[dlnlo=FE=E6=D7=CE=C9=C4=C2=C3=C6=
+=CB=D1=DA=E5w_QJEDDDEHO\o=E7=DA=D1=CB=CA=CC=CD=D2=DA=DF=E7=EB=E9=E9=EC=F2=
+=F6=FB=7Fxroie]XUSUVW[]`go=F8=E9=E5=DE=D9=D7=D5=D5=D5=D8=E2=E4=D9=DB=E3=E0=
+=F5o=FDeYcpy=E9=E4=ED=F5p_XSRRVZXTW]dx=EC=DF=D8=D5=D6=D5=D2=D2=D5=DA=E0=E9=
+w_YY\eq=F7=E8=DD=D9=D9=D7=D4=D5=D8=D9=E4=FBl^YUUUWYZZY]_bjmppnku=FE=F7=EC=
+=E7=E2=DC=D9=DA=D9=D5=D0=D2=D5=CC=DAai=F3=ECg\fjphVNS[\[]q=F9z=FC=F3=FA=E7=
+=DB=E1=E7=E1=DE=E8po=F1=E9=E8=F2=FE=F7=EC=F6g^g=E6=EE`l=F3=F5=EEe[=FA=EF~=
+ia=F9=EDm=FDn=F0=E3{=ED=F0=E1=EB=F1{|=E8rl=FDp=EAs=FDz=FD=DD^=DFqz=DCZ=DC=
+wt=DDR=F4U]cT=F7e=E1=F6zm=EE=EC=F4=DD=FA=D2=DF=E0=DA_=D6=FA=EB=E3=EE=E4=FC=
+=F8jyx^}^|=FB^]`[d]Zk`a]ckk=F6a=EFzy=EBv=DD=E7=DD=D5=EA=D5=E9=DC=D3=E2=CA=
+=DB=D0=DA=DF=DF=E1=F6=F0=EAy=EFn^\LbSkmS=F0L_TUePh\l=EE_|w\=DFo=E3=E2=EC=D0=
+=DF=D7=D8=DA=D7=D6=D3=D5=D5=D4=EB=D4^=E0j_=EASyTg\[]KZM[ZU=FFW=EA_=E1=EA=FA=
+=CF=FA=CC=DF=D2=D3=EC=C6=F7=CE=DB=D9=CD{=D0a=D5^=F5]TiKvIaWIfG^NQoN=D9^=E8=
+=FE=E6=D8=E4=D9=DF=D3=D7=D0=D8=D0=D4=D3=D8=DC=DC=DC=E9=ED=F5=EA=E6hk_^ZVS=
+XO\RVOWOfVd=FF=F5=DF=EF=D9=E5=CD=DC=D3=D7=CF=D2=D3=D9=CF=E6=D9=F3=EB=E8_s=
+NkKbO_[d[_=FC^wxw=E7=DF=EB=DE{=D5c=DBq=E0=DC=FF=DEx=DF=E3e=FCX}]=F8sl=E7_=
+=E5^=EFZl=FEe=DDW=DDo=E4=DD_=CE=FF=D1=E0=D6=D5=FC=D6ml=EEO=F5L=F1SYdJ=FFH=
+rN=FFXnmy=F2=ED=DD=EB=D2=DD=E2=D1=DD=D2=D7=E3=D7=E5=D1=F4=DB=E9=EB=E8g=E7=
+Tp\crfdjf=FB[gUgeZ=ECZ=E8i=EAy=EA=EF=FB=FC=EF=F6u=E7=F6=EEm=EBk=E4=FD=E0l=
+=DC`=E9agkq=E9|=DEz=DA=E4=DE=EB=E0=E6=DDu=E7f=F2s^[h^hcW=EAi=F8tk=E4=FCk=DA=
+X=D7W=ED=FE=E7=DC]=D7g=DB=F4r=E0g=E8va=EEb=FCW=DFb=F0y=E7=ED=FF=F1=EC=EF=F2=
+=F4=EDm=E8ok[=F5{Z=E5L=E0]u^\=E0=FE=DBi=DF=DE=D6=DC=DF=DD=E9=F9=DAh=D0k=E9=
+n=DF=F1b=F7Z=DAV=F1\\=EES^S^_Q_Qlq]=E2{=D4=F6=E1=DC=EB=D9=DB=F6=DC=EF=DA=E3=
+=E7=E7=E3=D9=E0=FA=DC=EEy=E0Z=EE=E6|=F6\=E4omzOVoM`N[=F1Y=DAT=E4=F6t=E9Z=D8=
+e=D3]=EA=E5=FF=DCo=F0=D9=D3=EE=E1=D7n=D3m=F8=F8kja=EEh=DBx=EA=D2f=D1i=EBr=
+gtqVQ^QoN_Uea[]`=D4h=D7w=F3=DF=D5~=DC=DE=E5=D5=D8=F9=D0=EF=CEx=D5=DA=DA=F0=
+ocxm=E7=EFV=EDaqoJfM^=FCRmV=EFu[=7FU=D2X=EAjg=E4=E3]p=ED=DEu=E2=E7=D8=E0=CE=
+=DC=D1=E0=CF=DD=D9=D4=E6=E6=DEl=DAh=F8=EDR=F8K^PZYITNR=FEIyao=FCh=DD\=DD\=
+=DB=D6=F3=DE=D7=D6=D8=DD=DF=CE=CF=D7=D3=EA=C2=EC=D4=EB=EF=DBT]i\=E4JXAQLF=
+TLj\=E3n=EFhm=EB=E6=DD=EA=DE=D5=D4=D0=DC=CE=CB=D1=CD=D8=E2=DA=DE=EE=DFjid=
+Y]MNJONIXQYOOZ=F9p=E3j=CF=D5=CA=D7=D2=CE=D7=CC=E0=D2=D0=C9=D9=CF=DB=CE=D3=
+=EAUvi=EEYKS`pMMI_[POKSWY]X^=7F=F0=FE~=FD=DC=D0=DB=CE=CD=CD=C9=CF=CF=D0=C6=
+=D2=CE=C9=E0=CC=F0pmjsSYJMVNLBLPLTG\^=F0_o=E9=DF=D4=E9=D8=DB=D6=D1=D4=CD=D0=
+=D7=D0=CE=CC=D6=D4=F6=D8=DD=E2=EBbej=FA^XRNXUNJHLR\S]u|zit=DE=D8=FE=EE=D4=
+=C7=CB=E6=F4=D5=C2=C6=D5=E0=D8=CB=D5he=EE=DA=E6XNX~gSNN^SHJMNW_PZ=D9=D9=E0=
+x^=FB=D3=CA=D9=EA=D5=C8=BF=C7=DE=E2=D1=C7=C9=D5=ED=E7=DA=E1hVQW[UJFIHIFDE=
+KSNP^=EF=DFx_=E4=CB=C2=C4=CC=C4=BC=BB=BD=CD=E0=BD=B9=C8=D5=EDdmH8Ab=E3=E0=
+iORI:303;FOi=E6=CF=C7=C4=C7=C5=C0=BF=BE=C3=C2=BC=BC=D1LDw=C6=CAfLx=CA=D1=
+R@L=DF=CE=E6\X^XIBBFN^]Z^e=EA=E6mQU=FB=D9=CB=CF=D1=CA=D5=E9=C3=BE=BF=BB=CE=
+L>Fe=CB=BC=BE=C4=C6=CEZ</,/7AX=ED=D6=CF=CA=CE=DB=EAv=E8=DB=CD=CB=CD=CB=C6=
+=CCoPHRagfj=DD=D3=D7=E2=E7=DD=D2=D3=E0o\SNNLO]=FA=EA=EA=F5ueh=EE=DD=DF=EF=
+iZ^=E2=C9=C6=D1=E5m_=D4=C1=D3b=FD]B?>Ao=BE=BB=C6=CDo?776:K=EE=C7=BB=B8=BC=
+=C5=CE=DA=E1=DE=D5=CF=CB=C7=C6=C3=CFY@9;BLLNd=FA=E9=E8=F9=FE=EC=D7=D4=D8=DF=
+{ge=7F=E7=D7=D0=D5=DA=DF=DE=F1_RSm=E3=DF_LJQq=DB=D1}H=3DR=C8=C7=C7=D1K><L=EE=
+=C5=B2=B6=BE=BF=DEF746>d=CB=BF=BC=BE=CB=E1=EDk_|=DB=CE=C8=C5=CA=D2=E5O=3D9;=
+AKX_m=DD=DD=E2=E5=EC=DD=DD=D8=DB=EA=ED=F6=EE=F5=E5=D8=D4=D2=DE=FFc^]VTYg=E9=
+=DDsSLN[=FD=DA=DC=EF_Lu=C6=C5=C3=CDT>9D{=C2=B0=B3=BD=CAj@789@q=C7=BB=BB=C0=
+=D3YOPO`=E4=CF=C7=C6=CB=DA=F6J:8;IYo=EA=EC=D3=D0=D8=D7=D9=D6=D3=CF=D0=E0=
+=F0ut}=EF=E5=F5|ja`ZVOOWZ`{=F5n_UQ]=E7=D1=CE=D2=D7gn=C4=BF=C2=BF=E4D:=3D^=CA=
+=B2=AF=BC=CD_?4479J=DF=C5=C1=C9=D9XKPVk=D7=CB=C8=C6=C6=C9=D9VB>CUjl=F1=E0=
+=D8=DB=DC=DB=D7=D1=D3=D8=E3=EAu]^`=FD=DD=DC=EEcYVUPOQS]o=EF=F0`]^`=E8=DC=D7=
+=D7=D7=D4s=DB=C0=C2=BF=CBN;7E=EC=C1=B2=B6=C3=E1M<58;Bn=C9=BD=C0=CA=DFON\l=
+=E4=D6=CE=CC=CA=C7=D3YA=3D>DQU\=EC=D3=CF=DA=D8=D9=DD=D7=DB=DF=F1=E8=E8m~=EC=
+=E0=E2=F4yce`TOR_q=FFxlfhkis=EA=DE=E5=E4=DCj=E4=C3=C2=BF=CDI:8M=D4=BF=B3=
+=B9=C6=DDO>56<H=E2=C4=BE=C9=DCrMNXa=EC=DC=CE=CA=C9=C9=EFH?AM[^]j=DB=D4=DA=
+=DE=DB=D1=D1=D5=DB=EE=F1=EF=EB=E7=E0=DC=E3=F5zovmaVNTcqhZW]fnlegppfV=E4=C2=
+=C1=BE=CCL<;T=CB=BC=B3=BA=C4=D2[D97=3DI=F7=C8=BE=C4=D6=FD_]dj=F5=E1=D9=CD=CD=
+=D1=FEF;<GVTOX=F5=DC=E2=E8=DF=D1=CA=CB=CF=DA=DF=E3=ED=EB=E1=DA=D6=E0=F7=FB=
+=EF=E8z]X[hh^ZRU^mvbYXYX=FA=CB=C7=C6=D6L>>X=C9=BC=B7=BA=C1=CBrI=3D:@O=F3=CB=
+=C4=C8=D4=FAd[Z]i=EC=DF=D4=CF=D5sIADHNOQg=E4=D6=D7=DA=CF=C9=C7=C9=CE=D5=E1=
+zeao=F0=F6xy=F8r[ONRY`d`_eo}=EB=E4=EA=E8{]=DF=C7=BE=BE=F0?<H=DA=C3=C2=C2=C2=
+=C2=CC_@76=3DN=EA=D0=CD=D1=DC=E8=F3wj_n=E5=D7=CD=CB=D7YEFR^TLU=E5=CF=DAjk=D9=
+=C8=C7=CC=CE=CE=D6=E8l`]]l=E9=DC=DF=EFiXPUZVOMS`ldd^MU=D8=C3=BD=DB?;L=CE=BF=
+=C1=C5=BF=BA=BC=CDU=3D:@R=F6=DE=DB=D9=D5=D1=D7=EBcX]n=EF=E7=E8xSECKRROY=ED=
+=D3=D2=DC=DF=D8=CD=CA=CC=CF=D2=D4=D9=E9ynr=FB=FCsi]UR\rcNISvyZTUd=DD=CC=C3=
+=CCN<E=DE=C1=C3=CD=C8=BC=BA=C7dA<>I_=FC=F9=E4=D7=CF=D3=E9k^`=FC=DD=DC=E9=F6=
+=F7gOFGMX_i=F0=DC=D9=D8=D3=CE=CB=CC=CF=D3=DA=E6v_Z\dq=F2=F0=FFk\\i]MIL\xx=
+^Y=E8=CB=C2=C9V<?y=C7=C5=D0=CE=BC=B6=BD=E5G>BINQTj=DF=D3=D4=E4=F6=F9=EB=E5=
+=EB=EE=E8=DE=FBRIHJMO\=FA=E0=D9=DA=DA=D3=CB=C5=C7=CD=CF=CF=D6=EFf`hl_[]]Y=
+V[\QLR_cWONi=D2=C9=CDoKO=E3=CB=CC=D6=CD=BC=B6=BE=DDQKOMHFLe=E3=E2=FAm=FD=E1=
+=E4rk=E8=DB=F9[Y\TNOYglgm=EA=D8=D4=D5=CF=C7=C2=C5=CB=D4=DC=E1=F0{gguj`n=EE=
+^KLX\SR\V]=DC=D8=FDQDESu=F3=F7=D7=BF=BA=C1=D2=7FgaOHHU=E6=D7=DE=DC=CF=CD=D1=
+=D7=D0=CA=CE=FBc=F4=FEL?CQXKBL=EC=DCn_=DB=C7=C9=D4=D4=CB=C9=D1=E9=F5=EB=ED=
+vkt=EB=DF=EEh`ce\TXWJ]=D6=E5[PRYVTZ=EA=C6=C1=CB=CB=C9=D2uSPOMQXby=E5=E2=DE=
+=CE=C6=C8=D7=D2=C6=CB=F6XUWOF?CQSOX=FC=DE=DF=E0=D6=D3=D8=DB=DA=DC=E4=E4=DF=
+=E7=E1=D6=CE=CE=E1=EE=DA=D9=EEn=E8=E8Tk=D9cUQMMFAH\pZ[=E4=D3=F1X[uaMOt=F2=
+q=EB=D4=C6=C1=C5=C4=BF=BD=C0=C9=CA=D2=E4eNIFCBFJMXl=EC=EB=F8=F1=E0=DC=E5=FB=
+=F7=EC=7Fh=FC=E7=F4ff=ED=DD=E6=FB=DE=D7w=DB=CC=EC~ra]OJNg=EF=FD=EC=DA=D5=ED=
+nkd\VV[YYeo=E5=D8=D4=CE=C9=C5=C8=CA=CB=D0=D3=EBdXMHCBDKOXi=EB=DC=D5=CF=CD=
+=CF=D2=D5=E0=FE|d\\UT^n=ED=DC=F2=E6=C5=CFm=EA=DD=F2h[]=E6=DC=EA=DC=CD=CD=D4=
+=DE=EFzePLLHDFHKWV_=EF=EC=DC=D3=D4=D6=D6=D2=DD=F4l`_]]f{=EF=E1=D9=D2=CC=C7=
+=C4=C3=C2=C4=C7=CD=DF=EBiQPICFHKJIsmO_iZTTVb~=EF=E5=D9=D1=CE=D0=CC=CC=CC=CE=
+=CF=D0=D4=E8=FEz_bj`o=FA=F2=FDpm`cZONMHGGGJOXo=E2=D5=CB=C5=C3=C0=BF=BF=C1=
+=C4=C9=D1=D9=E0lVYVOMXwTY}fUUX]Y^jw=FB=E4=E6=7F=EE=E7=FCw=EC=EA=E9=EB=E1=EB=
+=F5=E0=EC=EF=E1=E6=DE=E1=E4=E3=E9=F2jZTNKIHGJLS\l=E9=D7=D0=CD=C9=CA=CA=C9=
+=CA=CD=CE=D2=D9=DE=F9}=EDaj=D4m_=DD=FAZc`Y]idowvk_\]YVbd]etea=FEsl=EC=EC=DF=
+=DE=E1=DD=DD=E2=DF=E8zlj[YYZ]en=EE=DE=DA=D4=CF=CF=CF=CF=D1=D7=DA=E3=EB|_[=
+[UTOf=F9Xj=E0=F2=FC=E2=F2=F0=EC=F6{=F2=EE=EA=F9|=F6=F2omve\^d]]mof=7F=EB=E6=
+=EC=E6=DB=E6=ED=EBtcf_[]\\`ho=F3=E8=DD=D4=D4=D1=D1=CF=D2=DA=D9=EEpng_[]YR=
+^=E9jb=E3=E7=F7=E4=F1=FD=E8=E2=EB=E1=DD=DB=DC=E4=EA=EF=F9qtkc^_[[\Y_a_emj=
+eimsonla]]Z[_g{=EE=EA=DE=D2=CE=CC=CA=C9=C8=C6=CA=CD=CE=D5=E0=EA=FCo]VTSWS=
+RYYVZ]]\]_lyw=F3=E8=E4=E4=E0=DF=DF=DD=DE=E0=E1=EC=FA|of^[Y[\^fkkr~=F8=F4=F5=
+=ED=E5=E1=E6=ED=F4~=FD=F6=F2=ED=E7=E2=DE=DC=DD=DF=DE=DF=E3=E3=E8=EE=F5yss=
+upkhgfcaaa_ccdhkruusroux|=F6=ED=E9=E5=E3=E1=DE=DE=DF=E2=E8=EF=F4=F9=FDztm=
+igd`^^^^]]]^`k~=EF=E5=DE=D9=D5=D3=D3=D5=D6=D8=DB=DF=E5=EB=EE=EF=F1=EE=F3=FD=
+}{}xyz}zsuoh^YXXXXXZ^aimoptzwsqnv=FE=F6=EF=EC=E9=E4=E1=DF=DC=DA=D7=D5=D5=D6=
+=D7=D9=DD=E2=EB=EF=FC|=FE~||umkgc_\ZXVVX[]_cioz=F4=E9=DF=D9=D6=D5=D5=D3=D5=
+=D9=DC=DF=E7=ED=F7qjd^]\ZXXWWWY[]cn=F9=ED=E8=E0=DE=DE=DC=DC=DD=DD=DD=DE=DF=
+=E1=E2=E2=E6=EA=EC=EF=EF=ED=F0|rlg`^]\]^_bdhox=F9=F2=EE=E8=E1=E0=DE=DC=DC=
+=DE=DF=DE=DF=E2=E8=F0=FEphc^]\[ZXWY[_el{=F9=EF=EC=E6=E4=E4=E4=E4=E7=E8=E7=
+=E8=E7=EA=EB=E8=EA=ED=EF=F7=FFzxqmkijmoonlklmoz=F6=ED=E6=E1=DF=DE=DE=DE=DB=
+=D9=D9=DB=DD=E0=E4=EA=F2zf^YWUVTRRRSTVWZ_hkx=F3=EB=E0=DB=D7=D2=D0=CF=CF=D1=
+=D3=D5=D8=DB=DF=E5=E8=EC=F2=FEuvvunkd^\ZXWWWWY\_bgn=FE=EB=E0=DC=D8=D5=D4=D2=
+=D2=D3=D4=D7=DB=DF=E8=EE~oe][YXY[]``_^^^]^ahp|=FA=F0=E8=E6=E3=DE=DC=D9=D6=
+=D4=D3=D4=D6=D7=D9=DB=DF=E2=EE{qie`\YVVUTTUVZ]ajv=F8=E9=DF=DA=D7=D3=D1=D2=
+=D5=D7=DB=DF=E5=EE_^=CBdDLQ=D3=DAi`_=EF=EBgmfiZ=E4=D3=DFh=F1=E4|=D7=E6=EB=
+aTS=CA=BF=EFfH`=E8`h=F5k=F5i=DE=ED=F4TS=F9eji=DD=F9~dw=D6=FCTb=EA=DF=DA=E8=
+=DC=D0=DFj=DA=E0=DEn=F2=DA=D1=DF{=EA=EA`WSYNNLS]NPY`ng=F6=DD=D7=DF=DE=D3=D6=
+=D7=D5=DA=D5=D6=DC=DC=DB=D9=DC=D7=DC=DD=E5=EB=F5sb]fUUNNMKIKNQU]by=E4=E4=DD=
+=DB=D5=CF=CE=D3=D2=CF=CD=D2=D5=D6=CF=D9=DE=EF=E5=E8s_fgWQQTQNNSZVV`rpj=EF=
+=DA=D5=D8=DA=D2=CF=D6=DD=D3=D9=EBz=F5=DF=EB_[y=EEndn=EFx[Z^\X[\PTh=F0=E7=ED=
+=DD=CA=C6=CE=D6=D0=CE=D7qetbNKORMLY|=F7=EE=DA=CE=C9=CC=D6=D8=D3=E1_SY\WUU=
+J\=CE_=E5=C7sa`\=EB=EA]=E7=C7=CC=D8=E5=ED=D8=F8NU=FDiX[=EF=DA=EB=EF=D8=C9=
+=CC=D7=D2=CE=D9nZPH>879>HEL=E2=CF=C3=BF=BE=B7=B3=B7=BD=C3=C6=CEhJOSJEIWc]=
+e=EC=DD=EDf^^p=7FM?NNIOOh=C9=D1[=CC=B4=BF=C4=C7=DD=D6wO\=E3=DE=F2=DC=C9=CC=
+`QaTD>ENOIT=F5=DD=E0=DF=CB=C5=C9=CD=CE=D2wNGFC<=3DLfd=F3=D7=C3=B7=BA=BF=BB=B9=
+=BE=C6=E0YRRRH@M_oj\e=DE=DEg\fiYJHLe[K]=EDnr{=DD=CB=C6=D7P=E3=B5=C4Kc=CF=EF=
+N?J=D2=C3=DB=E2=BC=B8=D9Q^`J?@Y=E8X]=CF=C6=CE=D5=CF=C6=C8=E1b_^F<<<<=3DJ\bY=
+=D5=BB=C2=D6=C6=B9=BB=C8=E1=DA=C7=D1VY=D6=D0^[=E4=DDnSVrcHIXSHFKSUQSo=F1Y=
+^=D9=D5=DB=D4=CA=C8~=CE=B6=C0\`=C3=C4a?Q=C3=BB=DEk=BF=B9x?ARD67Q=FDOR=D9=CA=
+=DDj=EE=D3=DE[UqmMFOWTPa=D2=CA=E5=E1=C2=BD=C7=CE=CA=C3=C1=D0=7F=DA=CC=FBQ=
+Zq_OJTZKBMZNIM\`UTl=EDw=EC=CF=CF=CF=CE=CD=CC=CE=CD=C9=C7=CF=D5=CFZ=F8=C4=DF=
+JU=D8jD=3DD=EB=D2QS=CC=C6R>APF;?=ED=CF=E1=D4=BF=BC=C7=D0=CA=C4=CE=EC=EA=D4=DF=
+aT\_OO^fOR=EF=D9=ECs=E9=CE=D5_[=ED=FA\OSgh_h=DF=E4=E9=DC=D4=DE=E5=E7=DB=E3=
+sa=EE=EBni=F9=E2=E1{=F4=DF=FB`{=E4nZo=EAo=3DD=C0=C6D?=DB=C6`:?=D1=BD=E6t=BC=
+=B5=CCTX=E8^>B=DD=CAks=C9=C3=DFVj=DCwNQ=F6=FBOKU]LFP=F9jP|=CC=CB=D2=CF=C5=
+=BE=C5=DB=DA=CD=D6{ksxe_`d_\^[OMTSLMX\\^j=F6=E5=DE=D9=D6=D4=D0=D6=DB=D5=D2=
+=D2=CB=CB=D0=D0=E2Q=CE=BD=FDCS=DAx>6F=DE=F2GZ=C7=CAOCQjI>S=D3=DC{=CF=BF=BF=
+=D0=D5=C7=C4=DB~=DA=D5dOU[ODL=FFjHK=DF=D6p\=E8=CE=CF=F7=F3=D5=D0=FDi=E1=DA=
+=ED=E2=D7=D2=DFuv=F4ePNWZTYgbZ_|=F8ig=EA=DD=F3y=F5=E0=E0=E4=F0=D8=CB=DF=F6=
+=D6]=EC=BE=CFW_=E7=DFU>D=F4=D1n=F9=C7=C6=F4PNUK=3DBq=ED`=F3=CF=CB=D6}=DF=CD=
+=D8w=EF=DC=F1\Y^XLNe=EF[S=DD=CF=D6=D5=CC=C5=C1=CB=D7=CE=CE~y=EFbXjke`WXjW=
+LU^PNRTUQTanp=EF=E1=DE=D7=D4=DB=D2=CD=D1=D2=D2=DB=CF=D0=DB=E3\=F4=BE=D1P[=
+=EF=EFUBEg=E4n=FD=CD=D0~XOJE>E\ce=DE=D3=D6=DC=E7=DF=DA=E5=E8=DC=DF=E7=E5=EC=
+}|je=F3=EC=EDr=E3=D5=D4=DA=D2=CC=CB=D1=DC=ED=F5=EChSU_XW[YVVTWUPR]_\_ba=FD=
+=E7~=F5=DA=D4=DD=E2=DB=D4=D4=DC=DE=D7=CD=D3=DF=D7=D0=DC=EAe\=CE=CAUMpvXIB=
+M=FCh_=E2=CE=DC=FDdYPJENa\p=D6=D1=D3=D3=D4=D0=D0=D8=D7=D4=D7=DF=E3=F2hYSR=
+Z\XTk=E0=E0=ED=E3=D6=CD=CF=E6=F6=DE=E5fmf`=F5=EEm=F6=F1c^bZZ\Y_vjZ[_hf]m=DA=
+=D6=DE=DF=D5=CF=D0=E3=DE=CF=D2=F8=F0=DC=DC=EDnqa`=DC=E9VOW=F6^CAVis=F2=D3=
+=CA=CA=D7|g[KJ\mj=E3=D0=CE=CE=D4=D3=D3=D4=DF=E2=E8=FDmcXOKKSVSQj=E4=DE=E3=
+=DA=D5=D1=D5=D9=DA=D4=D8=E1=E1=E1=F7=FC=E3=DF=DC=EB=F6=F2=EAjVTUTTNMSRNNQ=
+SX^u=F5^=FE=CA=D1~=DA=CB=CC=CA=D0=CF=C3=C5=D2=D7=DD=CD=C7=E3YYnbLCI\njo=DC=
+=DBnXNJE?BKTW{=DC=D2=CF=D3=CD=C8=C9=CE=D2=D1=D0=DD=FDj__c_Z^=ED=E0=E7=DA=D0=
+=CE=CB=D3=DD=D6=DCukjWQXVQTTTXYV[XSWXZcig=F8=E2=E6=DB=D2=D6=D5=CE=CB=CC=CF=
+=D2=CD=D6=E9=DF=E5t=FE=F0hqnR=F2=CD[GQ_TNIL=EF=EDu=DF=CC=D2=E5=E2=EBo_NPb=
+kc{=DB=D6=D3=D0=CD=CE=CF=CF=D4=E4=F9fWOKJOTOOn=DE=EB=EB=D3=CF=DB=E2=DF=E0=
+=F1ljegghrx=FF}=F8=F0=EE=FE|=E7=EB~=F2=E9~m{=7Fq=FE=E6=EAn~=E6=E3=FBbt=F1=
+{lgfmdSn=E6Mh=C3=EAMa=EE{=FA[O=DC=C7=D9=DC=C6=C4=D9=D9=E3c[OLPXU]x=EC=DF=DF=
+=DF=DA=D5=D8=E5=F3u^YOIJOOKW=E9=E4=DE=D6=CF=CA=C5=CE=D3=C8=CB=DB=E4=E0=EA=
+|s_cjbbdYPWVQY[V]e]`wv=7F=E6=E4=E5=D8=D7=E0=DD=DB=EA=F2=DC=D7=E0}=E9=D9=DF=
+MU=CB=D7NMb`hTM{=CE=E5=EF=CE=C6=DA=DD=DB=F4dZY\eY]=FB=E9|=E9=DE=DD=DD=DD=E0=
+=F0vd[QLIJGGNZ^l=DB=D1=CF=CE=CA=C6=C7=C9=CA=CF=D3=D3=D5=DF=E9=E0=F0z=F5=FD=
+`_bUNNNKIIMOPT\_h=EC=DC=E1=DD=D6=D4=D0=CE=CE=D1=C9=C6=D2=DB=E7=E1=C8=D5XX=
+=F3^OMJM[Z\=F9=DB=EE=EE=EAo`]ZZ]^aw=F1=EB=DF=DB=D9=DA=D4=D1=D6=DC=DF=EFcV=
+VXNJSwqk=DA=CF=CE=CB=C9=CD=CC=CD=DA=E3=E5=FAfghfga^jb[YYTRSNJLSNO[dd=EF=DE=
+=DB=DC=D8=CC=CA=CF=CD=C9=C8=CD=CC=CB=DD=DC=E4=D3=D6=FD[^hXOLK[d\\w=FAen_X=
+WXSW\_l=F3=EC=E7=D9=D3=CD=CC=CB=CD=CE=D1=DD=EFqgZQUYYVc=F2=EF=E7=DE=D7=D6=
+=D7=DF=EB=EDoii^YX`]]]bnuo|=EF=F6qx=F8{v=FA=ED=E9=DF=DE=DC=DC=DA=DB=D6=D4=
+=DD=ED=DB=DAtk=F8_Nk=E6YNZZTOMObim=E7=D6=D4=D7=D6=DA=DF=EC{=F1=FAmm=EF=F6=
+x=EB=E5=EA=E0=DD=DB=DB=E1=F0=FDmZOJKKNPXu=DF=D7=D5=CB=C5=C8=CB=CE=CF=D4=DA=
+=E1=F7=F2qa[`a]]^^ZTQNMLKMQSXi=F9=E8=DC=D7=D5=D3=CA=C9=CC=CF=CA=C7=CE=DF=F4=
+=D8=C6=DBV`=DDvOLRa\W_ztdgbYUTUZZ[g=FD=F8=E9=E0=DD=DA=D0=CD=CE=D0=CF=D9=E8=
+=EFoZOZXTX_t=E3=D6=DA=D1=C8=CC=DB=DC=D6=F9^_[VNPTYZ_=FA=E9=E1=E7=E2=E5=ED=
+=FEnpsg`ep{=EF=EF=F6~=F0=E6=F4=FD=F8}o=FAh\=E3=DEdo=E1=DF=FF=F9=ED=DE=D8=DA=
+=DA=CF=CE=DF=ED=EEjXQOOPOOZ]`h=FE=E9=E4=DE=DA=D6=DA=DE=E9=F9qeYWX[_dw=E5=DB=
+=D5=D0=CC=C9=CC=D3=D6=D8=EBog]WVSV\^g{=FD{=F4yifg]^b^i=EF=E6=ED=E3=D9=D8=DB=
+=D4=D4=DD=E8=E8=E7fTk=EBcSZs\QTi=F8=F4~=E6=D1=D4=E4=E6=DB=E6dbrmdi=FA=EC=EB=
+=EF=E3=D8=D5=D6=D5=D6=E2=7Fe^QHEEHIMUk=E1=D7=CF=C8=C5=C4=C4=C6=C8=CD=D4=DC=
+=E7ud^[X\]\[YWXXNLNNJJOUZ^r=DF=D3=CF=CC=C4=C1=C2=C3=C7=C7=C1=C9=E4~=DC=EB=
+VMS^\TR_lXMQVNILTVR]=F3=DD=DA=D2=C8=C2=C0=C3=C4=C5=CB=E0pfTH@CHIHNl=DC=D3=
+=CE=C9=C5=C5=CC=D3=D7=DD=FBga\VRV_eck=F7=E8=EC=F8vweXTU[XU^~=F2=EF=E5=DC=D5=
+=D0=D4=E6=D9=C8=CF|d=F4}[Vb=E8=D8=DE=E8=D3=C9=D7s|=EEhRPXYPMQ\]\k=E3=D9=DE=
+=DF=DE=DF=FAg[TOIGKVg=FA=D7=CA=C4=BF=BE=BD=BD=BE=C4=CB=CF=DBrYPLJFDGJKKOT=
+XXUTX]_\]=FF=DE=DC=D8=CE=C6=C2=C2=C3=C3=BF=BD=C2=D2=E9=E4=FAZMLQZVOPYUMKL=
+NLJJOTX^|=DB=D1=CD=C9=C6=C3=C4=C9=CD=CE=D1=E6i]ZSNLO`{=EE=E3=D7=CE=CE=CF=D0=
+=CF=D3=E2q^VQMLLPWY^dr=FA}vkgv=F5|o=FC=E3=DB=E1=E1=D5=CD=CD=D1=D7=D7=CF=D4=
+=E4=E4=F0h[VTTUQUbjaaijf]W[egel=F1=DF=D8=D7=D4=CD=CA=CA=CC=CE=D0=D3=DDw^Z=
+VLHIOWVVb=EA=DE=E2=E5=DB=D3=D7=E6=F5=E9=E6{`f=FE=F3|y=EC=DF=E1=EC=F4=F5p`=
+fh\Z\hq^d=EF=DF=E4=F2=E5=D1=CF=DD=F1=E3=D3=DE]Td=F9mYWo=DD=E4mb=F7=E0=E9t=
+i~=EA=EEmh|=EC=E8=F0=FA=EB=E2=DF=E6=ECze_[TOLLPTST^s=EC=E5=E8=D9=CF=CE=CE=
+=CC=C9=C9=CA=CD=CE=CE=D1=DA=E1=E3=E7n[Y[YNMPRNKLMRPP\]cn|=DD=D4=D9=D3=CD=CC=
+=CC=D0=CF=CD=D0=DE=ED=DE=D9=DFvh=FA=E6zYW_bWMMPTOMR]kyy}=EA=DD=D8=D8=D6=D0=
+=CE=CE=D2=D7=D5=D7=DA=DE=E8=EF=F6tle]cb^]ZVUSRVYUR[krjo=E7=DA=D6=D9=DA=D1=
+=CD=CE=D2=D1=CC=CD=D5=D9=DB=DA=E7no=F9oihbWOKKPOKKQ]`[^n=ED=EB=ED=DE=DA=D3=
+=CF=D5=DA=D6=D4=D4=D4=D8=D9=D9=DC=E6=EE=EF=ECu[\__SLNTSQU[^iz=EE=E6=E7=DF=
+=DA=D9=DE=E9=E1=D9=DA=DF=E5=E9=E7=E8=E3=DC=DC=DF=DF=DC=DD=ECrp=FFn[SVYTSV=
+[]^cjt=FC=FE=F5=EC=F0=F6=F0=ED=EB=EA=F1=FC=FE=E9=DF=E2=E6=E6=DF=DE=DF=E3=ED=
+=F1=F1=EB=EF{nk~=F9wklnnibfnmd_gjj_Z`b_ak=F5=F1=F6=E7=DD=D5=CF=D0=D0=D0=D5=
+=DA=DA=DE=E4=EB=E5=E0=E9=F5=FF=FA{kb^^ZQONQOMOQW[[c}=EF=E8=E3=DB=D9=D8=D6=
+=D3=CE=CE=D4=D7=D6=D2=D8=DF=E8=EC=E7=FEf_`da[ZY[]^_bq=ED=EF=F6=ED=E1=DC=E2=
+=EB=F7=F0=F1}ncehm{jh=FA=E9=E7~dk=EF=FBg_emppn=FE=F5=E7=DB=DB=DB=DB=D9=D6=
+=D7=DC=EA=ED=E6=EA=F3qhrqf`_gi]Z\__\X]mxifu=EF=EB=EB=EB=E5=E1=DF=D9=D6=D7=
+=DA=E1=E5=DA=DB=E9=F0=F9=F7=FCj]Z]`a][^imkkj|=F7{st=F1=E5=E5=E9=EB=EC=E5=E6=
+=F5=EF=EE=F0=F2vv|=FD=F8=FEtw=F6=E7=E5=F0=F8=EB=E3=EB=FEorts`Z\^\_jmoo~=ED=
+=E1=E1=E6=E1=DA=D9=DD=E0=E1=E7=E1=DE=DD=DD=E8wgi_WUQPRRRUZ_k=EE=E1=EF=F0=DE=
+=DB=D8=D9=E4=E9=DD=D9=DF=EC=F1=EE=E7=EB=F3=F6=ED=E6=E3=DD=DC=E6=EB=EF=EB=E2=
+=F6nbj=F4gY\[__YVTWYYZ[[d=FB=F5=FA=FC=EE=DD=D9=D6=D5=D2=CE=CE=CF=D6=DD=DD=
+=E8znnkg^\fjc]`m=FDwdbr=F1=FB{=FC=EE=ED=F5=FA=F8=E4=ED=FF=EF=FF=F8=F0=EB=EB=
+=EF=E9=EE=ED=F5vmihe`ahkme^j~=F5}|=EC=DF=DF=EA=ED=E2=D8=DC=E5=DE=DF=E9=F4=
+vkdb_[YY[ZZWVX_cm=F8=EF=E4=D7=D3=D3=D1=CE=CA=C8=CA=CC=CF=CD=CE=D8=DB=ECpm=
+^ULHHGGEEIMORXYa=FC=E7=DF=DB=DA=D1=CC=CD=CE=CD=CB=CA=CD=CF=D8=DC=DD=E6=F8=
+idc`_YTU]]W[^ftjfgg}=F6vrx=EB=DE=DF=E2=DF=D8=D2=D4=DA=D6=D0=D3=DB=DF=E4=E7=
+=F1ohb^ZWVSOOTZ[UV]__\V[p=EF=ED=E8=E3=D6=CF=CF=D1=D4=D0=CF=D2=D8=DC=DD=DF=
+=DE=DC=DE=ED=FC=FF=F6=F9f]cpf[YWXXWVX\]_^ai{=F9zz=F3=EA=E6=E6=E7=DD=D5=D4=
+=D5=D7=D5=D5=D5=D4=D6=D5=D8=DE=E5=E2=E1=FA^Z[UNJFEHJKNNOYfx=F8=EC=DD=D0=CC=
+=CB=CD=CC=C8=C5=C7=CB=CD=CD=CE=D4=DD=E9=E6=EFg^XYVLHIJKKKNSX[]jw=F1=DE=D6=
+=D1=D5=D3=CE=CB=CD=D5=D7=D1=CF=D2=DA=DA=D8=DC=E2{lx`XZXWQONMLMNRXWZh=FE=FA=
+=FE=E5=DC=D9=D3=D5=D4=CF=D1=CF=D1=D8=D9=D8=DA=DB=DB=DD=E5=E6=E7=F8=F4=EE=FB=
+wjpna\USTQPQSVWUWZZ\dlp=F3=EF=F1=E0=D6=D4=D7=D8=D4=CE=CD=CE=D3=D2=CF=CE=D3=
+=D7=DC=EC=FCrjf\TWXYZQT]_c\Zbokl|=ED=E4=EF=FBw=FB=EF=F5}n|=ED=E6=E8=F2=E9=
+=DE=D9=DB=E0=E0=DC=DC=DF=ED=FE|qlhfa\]afeaeu=F8=F7=F9=7F=F2=EB=F9|}=FC=F4=
+=F6=F1=EA=E8=E7=E7=E2=DD=DA=DE=E6=EA=EA=EFn^[aa[VQV^_YV]gf__n=F1=E9=E9=DF=
+=D9=D3=D0=D0=D0=CE=CF=D1=D0=D2=D3=D7=DA=DE=DF=E8uc`hfYPQUVPNOVXUV[anrn~=F6=
+=F2=EE=F0=F5=E6=DD=DE=E0=DE=DC=D9=D7=D9=D9=DC=DF=E2=DE=DE=E3=EB=EF=E7=E4=F3=
+wljnnb]aeha\_cgecdjvz{y=FE=F2=ED=EB=F1=F4=ED=EA=EF=FEz=F9=ED=EB=ED=EC=E4=DE=
+=E2=EB=F4=EB=EA=EE=F0=FC=FC{tpkc][Z[\_en|=FB=F9=FA=F3=EB=E4=E5=E7=EB=ED=EA=
+=EA=EC=ED=EC=EB=EB=EC=EC=E9=ED=F1=EF=F0=FBwoica_ahjknpy=FB=F7=FA=F5=ED=E8=
+=E4=E5=E3=E1=DF=E2=EB=F6=FFyyxtnkliiijjihea__^_dkr=FC=ED=E5=DF=DF=DF=E0=E0=
+=DF=E2=E6=EC=F5=F7=F9xxz=F8=ED=EA=E4=E2=DE=DA=DB=DE=E1=EC=F8=7Fwl`[YYXWWY=
+\^bcbgq~=FA=F1=ED=E9=E6=E9=F3=FB=F4=EF=EC=E9=E8=E4=DE=DA=D9=D9=DA=DA=DB=DC=
+=E6=F1|ne\WVXZ[[^gljomo}=F8=F0=EF=EC=E6=E7=E7=E9=E7=E9=EB=EF=FAy~=F6=F2=EF=
+=F0=F1=F0=ED=EC=F0=F6=F5=F3=F2=F0=FA=7Fwvvr||{uoidcegggghifhku=7F=EE=E2=DD=
+=DB=DC=DE=DF=E4=E8=EF=FEoige`\[ZZZ\]do|=ED=E3=DC=D6=D1=CE=CD=CA=C9=C8=CA=CC=
+=CF=D6=D9=E5=FAi]WQMKJJIGGGHKOSX^hx=E9=DE=D9=D4=D1=CE=CD=CC=CD=CD=CD=CD=CE=
+=D0=D4=D6=D8=DC=E3=E5=EC=F0=FCoc^ZVVUVXXYYZ\^aelv=FB=FB}vty{{=F8=F1=F2=EA=
+=E3=E0=E0=DF=DF=DD=DD=DF=DD=D8=D6=DB=E6=EE=F3=F3r_XWXXVW]h}=F6=F2=EC=E6=DF=
+=DD=DE=DF=E2=E7=EC=F6=FEwprnmmiea^^^_ciqy=F4=EA=E7=E3=E2=DF=DE=DF=E2=E2=E6=
+=E4=E5=E9=ED=EF=F0=F4=F4=FDunha_]\\\[\^dm{=F7=F4=EA=E3=DF=DF=E2=E4=E3=E0=DF=
+=DE=DF=E1=E0=DF=E2=E8=EC=EF=F5~xnfb``adhnolmmmjeb^^__]^_behnu=F2=EC=EA=E4=
+=DF=DA=D7=D5=D3=D3=D0=CF=CF=D2=D4=D5=D4=D5=D7=DC=E1=EC=FAsd[WUQOMLLKLNOTX=
+YZ[^cm=FC=EB=E5=DF=DA=D6=D5=D2=D0=D0=CF=CE=CE=CE=CD=CE=CF=CF=D0=D3=D6=DA=E3=
+=F1o`XSQOMMLKKLMOTWZ^gp=FD=EE=E8=E0=DC=DA=D6=D6=D6=D5=D4=D4=D7=D8=D7=D7=D7=
+=D8=D9=DA=DB=DD=E3=EA=F3=FDyme]YWSNKJJMOOQTZ\_cj=FD=E9=DC=D7=D2=CF=CE=CC=CA=
+=C8=C8=C8=C8=C9=CA=CB=CC=CE=D4=DB=E8=FEk_YRMJGEDBBCDEFGJMR\j=ED=DA=D1=CC=CA=
+=C8=C7=C6=C5=C5=C5=C5=C5=C5=C6=C8=CA=CC=CE=D0=D7=DE=E9|_UOKGDBABCCCEHJMQW=
+`t=E8=DB=D4=CE=CB=C9=C8=C8=C7=C7=C7=C8=CB=CD=D1=D8=DD=E5=EF=FEsgb^[\^`a^]=
+[ZYYZ[]]\\]]``gmpz=F5=E9=E3=DE=DC=DB=DA=D8=D6=D4=D3=D4=D3=D3=D6=DB=E1=EB=FB=
+nb\[ZZ[]_ejlnnnmoqt~=F2=ED=E8=E1=DD=DC=DC=DF=E7=F0|ojda^\[[[[_dhlpuz=FA=F4=
+=ED=E8=E6=E6=E8=E4=E2=DF=DE=DE=DB=DA=D8=D7=D7=DA=D9=DA=DC=DE=DF=E5=ED=FDo=
+bZSOLKJIJLNQTX]gy=ED=DF=D9=D4=D0=CE=CD=CD=CC=CB=CB=CD=CD=CD=CF=D2=D8=DD=EA=
+=FDod]WPMKKJJJKMNOTY_ht=FC=EC=E2=DB=D6=D2=CE=CC=CC=CC=CB=CA=CA=CA=CB=CC=CD=
+=D1=D8=DF=F2m]VPMLKKLLLMNOT[dp=FB=EE=E8=E3=DF=DE=DE=DB=DB=DB=D9=D9=D6=D5=D5=
+=D7=D9=DA=DC=DE=DF=DF=E0=E7=ED}mb\XSPPQRPPSTUVZ^gv=F2=E2=DB=D6=D1=CF=CE=CD=
+=CD=CE=CF=D0=D2=D3=D5=D9=DC=E2=EB=F5{ohc^ZWVWVXXXYZZYXWWXY[]_dm=FE=EC=DF=DA=
+=D7=D4=D1=D0=CF=CE=CF=D0=D1=D2=D4=D7=DA=DC=DF=E1=E4=ED=F9ymifb^ZVSSRRSVXX=
+WXXY^es=F5=EB=E3=DF=DB=D7=D3=CF=CE=CD=CC=CC=CE=D1=D5=D9=DF=EErd\ZWUSRRSUX=
+[]^_`dem=FE=F5=F3=EE=EA=E6=DF=DD=DA=DA=D9=D7=D7=D5=D4=D5=D6=D8=DB=DE=E4=EF=
+xi`\YVRQQRSV[^`hz=ED=E3=DD=D9=D6=D5=D6=D7=D7=DA=DD=DE=E2=EA=F8vljff_\YVTT=
+VXXX[]_cm=F5=E8=DF=DA=D7=D4=D1=CF=CE=CD=CD=CE=CF=D0=D2=D5=D9=DD=E0=E7=F7n=
+a[VQONNMMMNORVYcp=FE=F4=EC=E5=DF=DD=DB=D9=D7=D6=D5=D6=D9=DD=E3=EB=F2=FExq=
+njhhhkns=FD=F2=F2=EF=EB=EC=EE=EF=F5=FB=FD~yz=FC=F3=EE=E9=E8=E9=EE=FA=7Fri=
+fd`_]]\^acfmv=FD=EF=EB=E9=E9=E4=DF=DE=DE=DF=DD=DD=DC=DC=DD=E2=E7=ED=FCsni=
+f_^^\\[[ZYY[]_dgmz=FE=F1=E9=E6=DF=DC=D8=D4=D3=D2=D1=D0=CE=CF=CF=D0=D2=D4=D8=
+=DE=E2=E9=F7pb[TOLLKJIIJLMORUX\hnw=F6=EB=E1=DA=D7=D2=CF=CE=CC=CA=C9=C9=CA=
+=CA=CB=CC=CE=D1=D5=DA=DE=E6=ED|oic[TOMLJJHHHHJKNSY]cl=FD=E6=DD=D7=D2=CE=CB=
+=C9=C7=C6=C5=C5=C6=C8=CB=CE=D2=DA=DF=E8=F5qe\UPNLIHFGIKMPU[bjw=F8=ED=E6=DF=
+=DC=D8=D1=CF=CE=CD=CE=CE=CE=CF=CF=CF=D0=D3=D6=D8=DC=E4=EExh^XQNMKKLLMOOSX=
+\_er=FB=EB=E2=DD=DA=D6=D0=CE=CD=CC=CD=CE=D0=D3=D7=DC=E0=E6=EA=F3sje`]]]\Z=
+Z[[]]]^_^^^__dhnw=F4=EA=E2=DD=DB=DA=D7=D5=D4=D4=D5=D5=D7=D9=DA=DE=E3=E5=E6=
+=EA=EF=F4=F3=F8~{via^[XURQONNMNQW\ds=F2=E9=E3=DE=DC=D9=D7=D6=D5=D4=D5=D6=D7=
+=D8=DB=DD=DE=E1=E2=E0=DF=E0=E2=E3=E5=EB=ED=EE=F7rg^ZWUTUWXY\^biot|=FB}wus=
+ngiklo{=F0=E9=E4=DF=DC=D9=D8=D7=D6=D6=D8=D9=DA=DE=E3=EA=F2=FAwojhecbca^]]=
+`cbbfiiklko=7F=F0=EB=E3=DF=DE=DD=DC=DB=D9=DA=DB=DD=E5=EF=7Fvmjijkjgc_\[ZX=
+XY\`iv=F9=ED=E8=E6=E3=E0=DF=DE=DE=DC=DB=DB=DB=DC=DC=DA=D9=D9=D8=D7=D8=DC=DF=
+=E5=F0zl_ZXTQPONKLLLNOSW\cm=FA=E8=DE=D9=D7=D3=D0=CE=CD=CD=CD=CC=CB=CA=CA=C9=
+=CB=CC=CE=D3=D9=DF=EDnaYQMKHGEDDEFGILOT[`k=F6=E4=D9=D2=CC=C9=C7=C5=C4=C3=C2=
+=C2=C3=C4=C7=CB=CE=D6=DF=F0n`ZUONLJJJIIJKKLMOQU[`k=F5=E0=D8=D1=CD=CB=C9=C8=
+=C7=C7=C9=CB=CD=D2=D8=DF=EC=F3=FBqkgb]ZWVUSTUVZ]`hmy=F7=EA=E4=DE=DE=DE=E1=
+=E4=E6=E4=E1=E0=DF=E2=E5=EA=ED=F6vnieecdgikliea`_`dfity|}{=FD=F4=EB=E7=E3=
+=DE=DB=D9=D6=D5=D5=D5=D6=D8=D9=DC=DE=E0=E9=F4=FEsd][VQONMMNORUZ`jy=EF=E7=E2=
+=DE=DC=DA=D9=D8=D8=D7=D4=D3=D1=CF=D0=CF=D0=D3=D6=DA=DF=EB~m_ZVOMKJJJLNPU\=
+m=F6=E7=DD=D9=D7=D7=D8=DA=DC=E0=E4=E6=ED=EF=F1=FA|sw{=FA=EF=EA=E4=E0=DF=DE=
+=DF=E2=E1=E1=E4=E6=E7=EA=EE=F9zpjhc_]\ZZ[[\\]\\\]`fkns=FA=EF=EB=E9=E5=E3=DE=
+=DB=D7=D5=D4=D3=D0=CF=CF=D1=D4=D8=DD=E0=E3=EA=EE=F3wi^WQNLKJJIIKLOT[f}=E5=
+=DC=D6=CF=CD=CB=C9=C9=C9=C9=CA=CC=CD=CE=D1=D5=DB=E2=EDxjhc^XTQNMLLKMMMNPS=
+X\bo=EF=DF=D9=D4=CF=CC=CA=C8=C8=C8=C9=CB=CC=CF=D3=D7=DC=E8=FFi^WOLJHHJKNS=
+Zam=7F=EE=E7=E1=DE=DC=D9=D8=D9=DB=DE=E2=E3=E1=DF=DF=E0=E3=E8=E9=E9=EC=EF=F2=
+=FCzwtvwqplfb_^[YYZ\\[^_cl{=F0=EB=E7=E1=DF=DE=DD=DB=DA=DA=D7=D4=D3=D3=D4=D4=
+=D8=DD=E3=EC=FFog\VQMKKKLNPU]n=ED=DE=D7=D3=D0=CE=CD=CE=CE=D0=D2=D5=D8=DB=E4=
+=F1vf^YVTUUUVWY[\]`fm|=F9=EF=EB=E7=E2=E1=DF=DF=DD=DA=DA=D8=D6=D4=D3=D6=D8=
+=DA=DB=DE=E5=EE|lc]YSONLLKLNRX^hy=F2=E8=DE=DA=D7=D4=D2=CF=CE=CE=CF=D0=D1=D2=
+=D3=D3=D3=D7=DB=E4=F0ra[TPNKIHIIJLNSY_l=FE=EC=E4=DB=D5=D1=CF=CD=CD=CD=CC=CD=
+=CE=CF=D2=D7=DB=DD=DF=E3=EA=ED=F1=7Fl`[YTPPPQSSWZ]djs|~}wtpmp{=7F=F7=EF=E8=
+=E3=E0=DD=DB=D7=D7=D7=D7=D9=DB=DC=DE=E3=EA=F1{mf`___acdflm|=F5=F7=F9{ywpq=
+sow~{utxy=FE=F3=EE=EC=E8=E9=E9=EA=ED=EE=F2=F9|umjiknu=FC=F4=EB=E6=E2=E2=EC=
+=FAsja][XXY\_cgmy=F8=EB=E0=DC=D5=CF=CE=CC=CB=CB=CC=CD=CF=D5=DC=E8=FEi\UPM=
+LKJKLLLNQY_iy=F6=E9=E2=E1=DD=DB=DA=D9=D8=D8=D6=D4=D2=D2=D2=D2=D3=D5=D8=DA=
+=DD=E2=EA=F6xldb^[[\[[\\^`b_^]]]]^^^`deintx=FB=EB=E2=DD=D8=D6=D3=CF=CE=CE=
+=CD=CD=CD=CD=CE=CF=D4=D8=DB=E0=ECyj`WSOMLJJIJLNPUY]bkw=F0=E5=E0=DD=DC=DB=D9=
+=D9=D9=D8=D9=D9=DA=DB=DC=DC=DD=DC=DB=DC=DB=DB=DC=DC=E1=E9=F1=FC~vmha\\ZXU=
+TTTSSUX[_cir~=F2=EC=E7=E1=DF=DE=DD=DB=DA=D8=D8=D8=D9=DA=DC=DF=E8=ED=EF=F1=
+=F5=F8=F9=F6=F4=F5=FCxnmmjge_``__```aeijhgfhmr}=FB=F4=EE=E6=DE=D9=D4=D1=CE=
+=CF=D1=D5=D8=DC=E2=ED=FBvjeb^[XWVWY[`efims{=FE=F9=F4=EC=E4=DF=DD=DC=DD=DE=
+=DE=DE=DC=DD=DD=DE=E3=EB=FEnf^ZVROOOOPPSX^j=FD=EA=DE=D7=D1=CE=CC=CA=C8=C7=
+=C8=C9=C9=CC=CE=D3=DA=E4=F5n^VOKIHGFFGILPX_jt=FA=EC=E4=DD=DA=D9=D8=D6=D5=D4=
+=D3=D3=D4=D4=D3=D5=D8=DB=DD=DD=E1=E8=EE=F4=F5=FCrj_ZXTSRSSRPQSX[]bm=FA=E7=
+=DC=D4=CF=CD=CB=C9=C9=CB=CC=CE=D2=D8=DE=E5=FAkd^XSQONMLMMNQW\_f{=EA=E1=DE=
+=D8=D5=D3=D0=CE=CD=CC=CD=CF=D7=DE=EA=FBqlke___^_`a`cjllklnnkmot}=F8=F1=EC=
+=E9=E8=E7=E3=E1=E7=EA=ED=EF=F2=EE=EA=EB=E9=EE=F6=FAytlgb__`bdhnz=F9=EF=EA=
+=E7=E7=EA=EB=EF}xqomllls}=F4=F0=EF=F0=F8=FExtuy=FB=F3=EF=EF=EE=EA=E7=E2=E0=
+=DE=DD=DC=DA=DA=DB=E0=E6=ED=FDk`\XURRRRRSUX\aen=FE=EF=E5=DE=DC=DC=DD=DE=DE=
+=DE=DE=E0=DE=DD=DD=DC=DB=DE=E2=E5=E9=EE=F9zrmnorpolgjilomlnnnuuw}=FD=FF=F9=
+=F5=FB=FB}tmhc_acegihimnu=FD=F5=EB=E4=DF=DA=D7=D4=D1=D1=D2=D4=D6=D9=DC=DE=
+=E2=E8=F4sg^YXVWWY[_dddc`adglprxz=7F=FF}|=FE=F8=EF=E8=E3=E0=DF=DE=DB=D9=DA=
+=DA=DA=D9=DB=DE=E3=E8=EB=EF=F8}}yqnf_]XVTSTVXY[_bdkz=F2=E7=DF=DA=D8=D8=D9=
+=DB=DC=DE=DF=E0=E1=E4=E5=E2=E2=E1=E2=E3=E1=DE=DD=DE=E3=E9=F8re]ZVQONNOPSW=
+Z^beilpw=FC=F1=EC=E5=E2=DF=E0=DF=DC=D9=D4=D1=D0=CF=CD=CC=CC=CE=D0=D5=DA=DE=
+=E2=E5=EFwh^ZUOMKKJJJKNPSY]afp=FF=F4=EC=E2=DD=D9=D3=CF=CD=CD=CD=CC=CB=CC=CD=
+=CD=CF=D3=D7=DC=E3=F7m_XQNLKKKJKKKLNSXao=F1=E3=D8=CF=CC=C8=C7=C6=C6=C7=C8=
+=C9=CA=CB=CD=CE=D0=D6=DE=E8t_WMIECA@@@@ACFJOYg=F4=DD=D3=CC=C8=C3=BF=BE=BD=
+=BC=BC=BD=BF=C2=C6=CA=D2=DD=EAw_WPLHECA@@@ABEGILQZi=F3=E1=D8=D0=CC=C9=C7=C5=
+=C4=C3=C3=C4=C4=C6=C9=CD=D3=D9=DF=EA=FEmd_[VRPONMNNOQTXZ^dp=F8=EC=E5=DF=DE=
+=DC=DC=DC=DC=DD=DD=DF=E0=E4=E9=F9rjdca`]\]^^bfkv=F3=EA=E1=DA=D6=D1=CF=CF=CE=
+=CE=CF=CF=D0=D3=D9=E4=F3sf\VQOMLLNNPSWXY[]`gn=FE=EF=E4=DB=D7=D4=D2=D2=D3=D5=
+=D7=D6=D7=D8=D8=D8=D7=D7=DB=DC=E0=EA=F2=F9wjc^ZWUSSTUUUXYZ\]afjs=FE=EE=E7=
+=E2=DE=DC=D9=D7=D4=D3=D4=D4=D3=D4=D5=D7=D9=DE=EA=FDl_YUTUVX\_hnt{=FA=F9=FA=
+=F6=FFy{spoolpopy=FC=EE=EB=E4=DE=DA=D8=D9=D8=DB=DD=DE=E0=E9=ED=F2zmliedba=
+`_^\\YWY[_ho=FB=EA=E0=DC=DE=DE=DD=DD=DF=E3=E6=E8=EA=ED=EF=F4=FA}rmhba__bc=
+ffhlu=F4=E8=E0=DA=D4=D2=D1=CF=CE=CF=CF=D1=D4=D7=DC=E4=EFte^XSOMKHGFEFGILN=
+U^m=F0=E1=D6=CF=CC=C9=C8=C7=C4=C3=C3=C3=C5=C7=CB=CF=D5=DE=EDxg]WQQONMLLMN=
+OSY`hu=ED=E6=E1=DE=DD=DB=DB=DB=DB=DD=DF=E3=E4=E8=ED=F6vkd_\[]__gox~=FA=F1=
+=EC=E8=E5=E2=DD=DB=DD=DE=DF=E2=E9=EA=EC=F4=FA=F6=F6=F6=FFvokihhe^]^^_bilp=
+y=FD=F9=F0=F3=F4=EE=EA=E7=E7=E9=EC=EF=F4=F3=EF=F2=F3=F6=FC=FF=FF=F4=EE=ED=
+=EA=EA=E7=E3=E4=E2=E0=DF=E2=EDzjc^[XVTVXZ\]_gjpy}=FA=F1=EC=E8=E5=E2=E3=E4=
+=E2=E2=E3=E4=E3=E2=E5=E7=EA=EA=EC=EE=EE=EE=ED=EE=EC=EA=E7=E4=E2=E0=E2=E1=E3=
+=E7=EA=F7xokjheb^ZURPOMMMNOQTZ`k=FD=ED=E1=DD=D8=D2=CF=CE=CE=CD=CD=CD=CD=CD=
+=CD=CD=CE=CE=CF=D5=DA=E1=F1o_YVRNMLMMMNOQTY_hpu=FF=F6=F4=ED=EA=E4=DD=D9=D5=
+=D2=D0=CF=CE=CF=D2=D5=DB=DF=EB=FB|qhdd_^[XUSQPRSW]dos=FA=EF=E7=DE=DC=DB=DA=
+=DB=DB=DE=E3=E7=EA=E7=E9=E7=E7=E7=E4=E3=DF=DD=DD=DE=E0=E3=E8=EA=ED=EC=E9=EC=
+=EF=F2=FFl]XTNKIGFEFHLOW_q=E7=DA=D3=CE=CC=CA=C9=C8=C7=C6=C6=C8=CA=CD=D2=D8=
+=DC=E0=E7=EE=FBoe^YWSQOOONNMLMORW\_k|=F1=E3=DC=D6=D1=CE=CC=CB=C9=C9=C9=CB=
+=CD=CE=D1=D9=E2=EEuf]WSOMLLLKLMNNQW]ep=FA=EF=E6=DD=D8=D3=CE=CD=CC=CA=CA=C8=
+=C8=C8=C8=C9=CB=CD=D1=D8=E3=FEdXPMJHGEEEEFHJKPW^n=F5=E7=DD=D7=D1=CE=CB=CB=
+=CA=C9=CA=CB=CC=CE=CF=D1=D5=D7=DB=E3=EB=F9tj_[YXVUTSSSTWY\_dfgilowy=F8=EA=
+=E2=DD=DB=D9=D8=D7=D4=D3=D2=D2=D2=D4=D6=DA=DC=E0=F1vf]XTSRPNNNORUW[^bjko=FF=
+=EF=E9=DF=D8=D1=CE=CB=C9=C7=C7=C6=C5=C5=C6=C8=CA=CE=D7=E4pZOKGDBBBBAEIIIL=
+NPYn=DF=D3=D0=D7=DD=D9=D1=CF=CE=CC=C8=C3=C0=C2=C6=CC=CE=CD=CF=D3=D9=DE=DE=
+=E3=FAaVNLJHHGGGHHIJMR\jy=ED=DE=DA=D6=D0=CF=CE=CD=CC=CB=CC=CD=CE=D3=DB=E5=
+=E9=EA=EE=FEj_^^^[\\c=FDn\SU\b^SVes=EE=F3u|=E9=D7=D3=D4=DB=E5=DA=D1=CE=CE=
+=D4=D8=DA=D6=D4=DA=E3=F9=FC=EA=E9=FC_UOSVOKEBGKHDELVbl=FD=DD=D4=D1=CF=CB=C5=
+=C0=BF=C1=C6=C7=C7=C8=C9=CD=D3=D0=D5=DF=EFo\ROOMMKIJHC?AEHMTe=F2=ED=E3=DB=
+=D1=CF=D5=D2=CA=C5=C2=C3=C7=CA=CC=CE=D8=E3=EB=E2=D8=D8=F3YSYYRF?LyWNK=3DCh=D2=
+=CB=7FWQd=CF=E9n=D9=CC=C2=C8=DA=DC=CE=C8=CE=DD=DF=D4=C6=C9=EDXVmvXKGO[YXP=
+NVc|nUKS=F2=E3lX[t=DB=D2=DC=E3=E8=DA=CA=C4=C6=CD=CF=CD=CD=CF=E3x~=FBjVMM`=
+oWE?M=F5=DB=FCSTa=E1=D3=E4^Q]=DF=CD=CF=F2^j=DC=CD=CF=EA_^}=DB=DA|\o=D9=D1=
+=D9{h=EC=D7=DAmWWn=D8=DD_KJ[=F7=FCVNY^efjq=EF=ECl=E8=D5=CF=E0X=EF=C3=B9=C3=
+M?Y=C0=B9=C9dX=FB=EFdI?GReoijWMST[|=EE=D9=CB=C5=CFv]]=EE=E0=F5ci=D9=D2=E7=
+l_l=E4=DF=E0=D6=CC=C9=DC=FF=F6=D7=CE=EDZV=DB=D1U??=FB=DAH8<s=C5=ED6-I=B6=B3=
+=CD?9Dz=CB=C0=B7=AE=AE=B2=BBw<<J=F1=CB=C4=C3=C3=CFV>9?Lo=E5=E8=DB=E6[D;8;=
+?H[m=E0=DA=DB=D9=DA=D8=CF=C2=BC=BE=C7=CB=D7=CF=C4=C6=D2=F8^m=E8=EBUFJX^XK=
+EKTVOKO_=EA=DF=EF=E1=CE=CA=CC=D6=DC=CE=CB=D4=E4=E8=CD=CB=CF=DE\=E1cG=DC=CD=
+=BA=CA1()O=BA=BE=D1=DA=C6=C1U/+2K=CA=BF=BF=C0=C3=CFeGJ=FB=C9=BA=B9=B9=BA=C3=
+=DARB?HQd=F9=E3=E0s^]ZSSu=CF=C8=CD=F9Uc=FA~WQ^=FE=E2mOLVefa^k=F8=F6m\b=F8=
+=DF=DB=DB=D8=CD=CA=CF=DD=E9=D7=D4=DF=E7=DE=CF=D6=D7=EFIs=D3=CA=C1Q7.:=D7=
+=C7=C4=CD=D7=C8=D0D0.7S=D2=C4=C5=CB=CE=E7VINg=CA=BD=BC=BC=C2=CC=E8UIEKYv=EA=
+=E7=E5=E5xYR^=EC=DA=D7=E0=E5=E6=FB_VZbp=F9=F9{ja^dw=F7=F7=F9=F5=F1=F1sd`f=
+=FF=E8=DD=DA=DB=D8=DE=EE=FE=7F=E0=E0=F9j=FD=DAnR=F8=DE=CD=CBd?8Ab=D5=BC=BD=
+=C6=C6=CEkC;8=3DY=D0=C7=C8=CF=F1]ZVUj=E2=D4=CA=C8=CF=EFXLFHLOZk=EB=D7=CE=CF=
+=D6=D9=D6=D1=D0=D0=D2=D1=D1=D4=DA=E2=EF|il~xl_VQOOMKKKLNOOQYd=FE=E0=D8=D5=
+=D0=CF=D0=CE=CC=CD=CD=CD=CE=CE=CE=D2=E4=EB=DD=E3=E7=E9dNNUU]t_T\[S[d[[a[T=
+WXT\|=EB=DC=D4=D0=CE=C9=C6=C7=C7=CA=CF=D6=DC=EEgYQMLJLOU^p=EE=E1=D8=D3=D4=
+=D0=CF=D7=DF=E6=F3zqcYVWVWZ[]ba[[[\^cnz=EF=E6=DE=DB=D8=D1=CE=CC=CB=C9=C9=CA=
+=CB=D0=E2=E0=D9=E4=FCnTGINMXt`W^ZP\k^cn[QSON[s=F7=DE=D3=D1=CE=C8=C7=C7=C5=
+=C7=CA=CE=D5=E8m^PLLLMRZb=FC=E3=D8=D2=CF=CF=CF=D1=D7=DF=F6hZTPOONOSX\_cdg=
+jmop~=F6=EA=DE=D8=D4=D3=D1=D2=D3=CD=C8=C6=C7=C9=CF=DA=E8=ED=EA=F4fUJ@>AEK=
+YYSUWVb=EE=E6=E0=E0=F7hjoy=E3=D5=D1=CD=C9=C7=C6=C5=C6=C8=CA=CC=D4=E0{ZKEA=
+@@ACFNXf=FB=E0=D4=CD=CB=CD=CE=CF=D5=D9=DF=E7=EA=EA=F2slko=FF=F1=EF=EC=EF|=
+rg^YZ\^a``cchx=ED=E4=DD=DD=EAm{=DD=DE=E9=E5=F6XT\[v=D1=CF=DC=D8=DB=F5=E2=D3=
+=D4=D1=D2=EAaYOLQY]krlo|=F9=EC=E8=EE=F5ze]ULIJLPZe=F1=D8=CD=CA=C7=C2=BF=BD=
+=BC=BE=C1=C8=CF=DA=EBk^WNJEA@@ACEEFHILQXh=EA=DB=D4=CD=C8=C3=C1=BF=BD=BC=BD=
+=BF=C1=CA=CC=C8=CD=E1vXD?A?CQRLNJEKW]y=E4=EEtlac=F9=E2=D9=CF=CC=CA=C9=C9=CA=
+=CB=CE=CF=D2=DB=EAnUJGFFHJMVeiq=D7=CB=CB=CC=CD=C9=C5=C7=CF=D9=D7=D6=E1za[=
+SOPSUTOLJGFEEJU^\]{=D8=CB=C4=C1=BE=BC=BC=BD=C5=CD=C7=C1=C3=C9=D5_HC>?Mh=FC=
+=ECyRF@=3D=3DBISh=EB=E1=DC=DC=D9=D7=CF=C9=C4=C0=BF=C2=C9=D4=EBfXNHFKR^q=FE=F5=
+=E4=DD=E0=E1=DD=DC=DC=D9=DC=E7=ED=F8qmnp|=F2=EC=FAma[^_\YVUVWYc=FE=E5=E0=E0=
+=DC=DA=D6=D3=D2=D6=DC=ED=FA=ECcV=F1=DB=D7=D7=F6LBCEU=D0=C0=BE=BF=CBsOHDIU=
+g=E6=D4=D1=D7=E3ueg=FC=E0=D4=CF=D5=E4mXNMLGEIV=F1=DB=D6=D5=D3=CD=CA=CB=CC=
+=CD=D3=D9=DD=E4=EC=EF=FEkifekj`[[]abYONPTUV^ovmjy=E2=D9=D2=CF=D0=CC=CC=D2=
+=E1c]=D8=C8=C6=CA=DCUFBCU=CF=BF=BD=C2=D0eJB>>FS=EF=D2=CE=D6=F6\TVb=E9=D7=D1=
+=D1=DDv[NJLNLQm=E1=D3=CE=CF=CD=C9=C7=C8=C8=CB=CF=DA=DF=DE=DF=E5{_USWZYSNK=
+MPRQONR[ct=E8=E2=DD=D7=D5=D1=CD=C6=C3=C5=C8=CD=D8=E1=E5rS[=DE=D4=D9=F2QD@=
+?>K=EE=CF=CA=CD=E1aPGDIV~=DA=CF=CD=CD=D0=D9=E4=EB=DF=D4=CE=CD=CE=D9=F5\OL=
+KIINWbw=F6=DF=D6=D0=CE=CE=CE=CF=D2=D7=DA=DA=DA=DE=E8{_\]__\XSRQOOQSX[_dek=
+=FB=E1=DA=DB=E4=F6=E4=D1=CB=CB=D1=D9=DD=DE=DD=DE=E2=E4=D2=C8=C9=D4|RMSVZe=
+rlfaXNJFEINVbt=7F=FA=EA=DF=DA=D3=CE=CA=C9=C9=CC=CF=D8=E4z__jjlgcdgl=FD=E6=
+=DD=DC=DE=DA=D9=D6=D9=E2=EA=EB=ECp_\\^ZVUUVUQMNS[gk_]t=E4=DA=DA=DF=D9=CE=CD=
+=CF=CF=D0=D0=CE=D0=D5=D7=DD=E8=E9=E4=ED=F2=FAupdbc^ZSPOONNQTTY`a`ch`^n=EF=
+=DB=D3=D5=D2=CF=CD=CD=CF=CD=CC=CD=CE=CF=CF=D1=D7=E6wc\^`]WOPW\WQOU`a_oncb=
+fw=EF=ED|o=E9=DE=DE=DA=DB=DD=DF=E6=E0=DA=D4=D7=E5=F1=F7=F2=F7ukchot=F3ua[=
+_glm]^x=F8=F9ogz=FE|mv=EB=E4=E5=ED=EC=EC=F6=F1=E0=DB=DD=E0=E5=DE=D9=E3=F3=
+~|oga_d[[^\WRWX\`[r=E5=EC=F9=ED=D6=CC=CE=DB=D9=CF=CD=D2=DB=DC=D9=D9=E3yhh=
+ha^^Z]ZUUSX^beaaimomn=FA=E1=E4=EB=E7=E0=DE=E7=F4=FD=F6=E9=DD=DC=DD=DA=DA=D9=
+=D6=DA=EC=F7~~sfd]XVX\[WUZv}z=EE=E8=E7=E1=DC=DB=DC=DC=DC=DD=DD=DE=EC~|}wh=
+fefmy|igv|rligehgkyyoo=FD=F4|q=FC=E7=DF=E3=E8=DE=DC=DF=E8=E8=DF=DE=DE=E5=EE=
+=ED}pj`]XY__ZUV\bip=F6=EA=EA=EA=EA=E8=DF=DC=DA=DA=D7=D7=DC=DD=DE=E0=DF=E9=
+=EF=FA|sg_^_[Y[\adagkomgfx=FD=F9=F9~=ED=E3=E7=E8=E9=E6=E0=EA=F5=ED=E6=E5=E7=
+=EC=E7=E4=DE=DF=EB=E2=DE=E4=E8=F8yuf_[WWXX[ZY\\ail{=F0=E2=DE=DE=E0=E4=E1=D9=
+=DD=EC=E7=DE=D8=DA=E8=F1=7Fulhjmh_[Y^c_ehl=F9=EF=EA=E0=DF=DF=DC=DA=D7=D6=D8=
+=DB=E3=E1=DE=DF=EC|sp}=FFe]YX\[VQOPPPUYX_n|=F6=E4=DB=D8=D2=D1=D0=D0=CF=CF=
+=CF=D1=D5=DA=DC=E2=E9=F1=F9{xvfoyli^\[\]`aZ\^i}=FCyx=F8=ED=F2yngcejn|z~=EA=
+=DC=D8=D9=DA=D8=DB=E0=E7=EF=F8=F3=F5wtkghnod_jr=F4=FDjz=F0=EF=F5=FC=F1=F5=
+=F9=F6=FD=EC=E9=F9=FE=F1=EB=F1=EF=EA=F3=F3=EF=EE=F2necca^]`i~{lmx=EF=F2pj=
+l{=F6~=F1=F1=EA=E1=E7=E3=E0=E6=EC=EC=E3=E3=E9=E5=EF=F4zpslkejoilt~=FFlccj=
+nl^^jmfdj=FE=FCz=F4=E8=DD=DB=DA=D6=D5=CF=CE=CE=CE=CF=D3=DC=E4=EFxq\VXVVQM=
+NRTTSVY\__mnq=F6=EA=E4=DF=DC=D5=D7=D7=D4=D8=D1=CD=D7=DF=DB=D6=D7=DC=F1l=D9=
+=CDdEGl=D3=E9RKOb\LJLNW=EF=D9=DC=ED=FE=DF=CE=CC=CF=DB=DB=D2=D6=E2=EC=F0=E5=
+=E8=FDy=EC=EB_PW=FB=FEWPh=E5`Qfw_QT=FA=D9=E9g=E5=CF=D5=EBl=E8=D0=DA=F9=FA=
+=EC=FA=F0=D4=D9w]V^w=EF=EDxa\t=EF=E3=DA=ECk|=FDtm~k\ece=ED=DA=E1eU]=E3=CF=
+=D3=F7QQw=D9=E5gVLY=D4=CA=DB^N_=CC=C4=D6ia=ED=D0=CA=D0=E4k_og][bfOJV=F1=DF=
+hNOa=E3=D6=E3`Yg=D8=CE=E6=E5=D1=D8e_=D0=C9=D8{U\=DF=D9=DB=F2\PT=ED=CD=D2u=
+UPi=D4=D8eFD]}ah=E8{U\=DD=CC=D1=FAx=DD=DCn=FD=CA=CCZJ[=CF=CC_EMp=D6=D9vll=
+=EA=D6=EEg=F3=E7=EF]d=D9=CD=D5eOv=CB=D2jV^=E6=D6=DEZISi~{cOHO=F1=DBfIEo=CB=
+=D4cZ=E6=CE=CD=D1=D1=CE=CC=CE=D5=D1=CD=CC=CF=EBt=FC=ED=EArNGHNZOHIJMV_ecp=
+{=ED=D2=CE=DD=E8=D7=CE=D0=D5=D9=D5=D9=E4=DD=D6=DC=F8gn=EF=DC=DF]LNd=F3dME=
+Ml=7FYO_=F3=E2=DF=DF=D3=D0=DB=DE=DA=D2=D1=D6=DF=F4=E6=D4=D4=E4]P^=DE=DAU@=
+DZ=F1_NVzkLIh=D1=D5=F2=EA=CF=C9=CF=D7=D2=D0=DB=E8=D9=D0=DD]Sd=F5mYVa=ED=DB=
+=E5`T_=DF=D8lNU|mLBIX\Zu=D6=CE=D2=D3=CC=C6=C5=C9=CC=CF=D7=DE=DA=D7=FEOJPQ=
+KJQjrNCZ=C8=C9ZFU=F8XFU=D0=C1=C2=C3=BB=BA=C9=ED=EA=E6XCEYnZY=7F=DF=DE=D5=C9=
+=CA=D9=E8=E2=E9WA<<<76<H]=FB=DA=C6=BC=B8=BB=BE=BE=BB=BE=CB=DC=DC=D3=DAcR\=
+=EE=E8mckpgVKHQmX@<L=F0mRZ=DF=D3pu=C2=BD=EEK~=CA=E4B?s=C2=BE=CC=DC=C8=C0=D8=
+MABHKTk}=DD=C7=C1=C6=C9=C7=C2=C7=D2=E8j^M=3D67;=3D;=3DM~=E2=EA=E5=CE=C3=C2=C7=C7=
+=BF=BC=BE=C9=CE=C8=C4=CA=DC=F9=E1=D7=E8THObS?;>FC<?KetF;=E9=B9=C5IM=CD=C5=
+=E8MU=C5=AD=AE=C3=C7=B7=BA=E5C=3DAMb^W=DC=C4=C8=DA=E1=DD=DC=DCvQTeN<8;>>=3D@V=
+=DA=D5=F2=E6=C9=C0=C6=CC=C9=BE=BC=C4=DE=E2=D0=CE=E5l=7F=D7=D0=E4]Zs=F9ZGB=
+HNH@APu=FCXIh=BD=BFWK=D4=C7=FCAD=D4=B3=B4=CE=CC=B4=B7r<=3DJPHE[=CF=C5=D4=E9=
+=D4=CB=D3=F1kjbdXE=3D@C??BO=EB=D8{=EA=C5=BE=C6=CD=C5=BD=BD=C7=D3=CD=C9=D7ml=
+=E2=DDx[^qaOMOND>AGECH[=DF=EDIG=C7=BA=DES=D5=C8=D4]R=D9=B6=B4=C5=C2=B1=B6=
+=E7GISMDFp=CE=D5~=EB=D2=DD\RZeUDDMF::@FEKj=D0=C8=CD=CC=BF=BC=C1=C5=BD=BB=
+=BF=C9=CB=C9=CF=FF^~=E7dRV\OB?HLE>?FJHIW=ED=DB=D7=CC=DD=D2=B7=BA=CC=C5=C1=
+=CF=EA~=F5=D1=BF=C5=CD=BF=C0pGC@95:ENLS=F1=D8=DC=EB=DB=CD=CF=DC=E6=E1=DC=
+rLL\\NS=E3=CC=CA=D2=CE=C2=BF=CA=D3=CD=CB=D6=EFmcXLKQRMOZ^VLKYcOMZb][s=DF=D4=
+=CD=CB=CB=C9=E0=CE=BA=C6=DA=CD=DCeVV\=EC=D7=E1=D2=C7=DELGG>:;GZ]c=D5=C6=C7=
+=CB=C8=C1=C0=C7=CD=CF=DAoZOC>ADEJ\=DF=D6=DC=D3=CB=CC=D8=E4=DE=DEw^h=FDg_s=
+=E6=DF=DF=DF=E2=E7=ECh]qqWU]TVggh=FB=DF=D6=D7bP=C4=C3d=E2=E4YQNNf=D2=CF=CD=
+=BE=BF=DEniNC@GW_]=E7=CA=CE=CD=C7=C5=C4=C7=CA=CD=DB`LI?66;=3D<AW=DA=D5=D4=C9=
+=C0=C1=CA=CE=CE=D0=D8=E4=E1=D7=DE=E5=DA=D0=D7=DB=DA=D5=DD{dVQ\QFJLJIJNZ[i=
+=DFoL=D2=BD=ED=D9=C4=F2Z=EC=EFz=D0=C6=C6=BB=BA=C8=D7=DF[HA?ILJV=F1|}=DA=D5=
+=D6=D6=D1=D6=E3bTN?:=3D?@DQ=FA=CE=C9=C9=BF=BB=BD=C0=BF=C3=C9=CD=D8=E6~j_^Y=
+W`^ZZYPLKMTOKKP[[[=FD=D8=D7=D8=D4=EB=C6=BA=DC=D1=BC=D6Uz=ECp=EF=D7=D2=C4=C4=
+=D6=EEyOC?>?DDKdcj=D8=CF=CE=C8=C6=C6=C6=CC=D9=EB[GBEDAL^=E7=CE=CD=C8=C1=C3=
+=C8=C9=CC=D5=DF=FD]SNJJLNWZ^vo]Yd=F0=F9hm=F4=E6=FCp=EA=D8=CC=CA=DBe=E1=B7=
+=CDQ=C6=D5KPSOZ=FD=E9=CF=C5=CD=DB=E8jNHADJII]=F4y=DC=CE=CB=C6=C4=C3=C3=CB=
+=DDnYI>9:=3D>FV=F2=CD=C6=C3=BE=BC=BD=C0=C5=CA=D4=E9n[YRRY^ewm|=F1o^[]_TNNM=
+OOP^=F2=D6=E4O=CC=BB=EB=CD=C5o=FA=E3_y=D6=D9=CE=C0=C6=CB=CE=E5k\LGLJFMWVh=
+=E5=DB=CE=C9=C8=C5=C9=D4=F4WKD;69<>EW=F0=CF=C6=C2=BE=BC=BE=C1=C3=C8=CF=D5=
+=E9=FD=FFe]ky=FD=EF=EC=ED=F2mWUYOGFFHFHSf=F6_{=BC=C3=DA=BE=C4=E1=D8=EEm=D9=
+=DE=FF=CF=C5=D0=D8=D9=E8lUJMMDDNQO_=EB=DA=CF=CD=C8=C1=C6=CE=D2=DF\LFB=3D;=3DC=
+LRh=D7=C7=BF=BE=BE=BB=BB=BF=C7=CD=D9=EBdVQLNOPW_glh[[]VTOQQNOR\h~=E4=D8=CD=
+=C9=C7=C7=C5=C4=C8=CC=D0=D4=D8=DB=E3=E5=DF=DF=E3=F5ug^WQPNNNMMNOOOQRUZ^g{=
+=EF=E7=DC=D6=D2=CF=CD=CC=CC=CD=CF=D0=D5=DD=DF=DF=EC=F4=F3=F4=F6=FEtzyuxyo=
+kh][YWUVVWYYZ\]\`ho{=F8=ED=E7=E7=E6=E0=DD=DB=D7=D3=D0=CE=CD=CB=CA=CC=CE=D3=
+=D7=DD=E8=7Fi_YVSRPOOQTUUX\[^`bfiku=EF=E6=DE=D9=D7=D8=D9=D9=DD=DE=E2=EB=EA=
+=EA=EE=F1=F0=EF=EE=F5=FC=FB=F9}xvkiihlihotux=F4=F0=F7=F9=FF=FB~rqvsptztq=FF=
+=FD~=F8=F0=ED=ED=ED=EB=ED=F4=F5=FFrqos{=FE=F9=F1=EC=E6=E3=DE=DC=DC=DE=E1=E2=
+=EB|okd_`OW=C7h<Ch=D9=E9Z\=E1=EDKG=FE=EDWH=DA=B8=BEwo=CF=CA_J=F4=C2=C9^=FE=
+=C4=C2rHV=CC=CEQK=EF=D4bL\=D1=D7KCUcJ?L=EA=DFi=FC=CB=C7=DCh=E7=CF=E3WZ=DE=
+=D6=FD_=E2=CD=D0=FA=FE=D7=CD=D8=EA=E7=CF=CEjZ^fXKMWYOMUZZVU]g_b=F9=FCn=F7=
+=DE=D8=D5=D3=D1=CA=C8=CC=CC=CA=C9=CB=D0=D1=D3=D4=E4wwl^UONKGFGFEGJNPV^`=F5=
+=DE=DC=D9=D9=F4=D8=BB=C0=D9=D5=C7=CFuP_=CB=BF=C8=C5=BA=BB=CEWJH>46>HEIg=D4=
+=D3=E2=D8=C8=C7=D3=EE=EA=EA[HGLQW]f=FC=D1=CA=CB=C8=C4=C4=C1=C8=D9=E7=EAo^=
+TY`[STZ_ZS[`daRO\XLO=E2=D1=F0=F6=DA=D0=CB=D2=CB=BB=C8]=C3=BA=C7=CEL:DL@J=
+=CA=BC=C4=C7=CDdC:228=3DP=D8=CA=C8=C2=C5=C9=CF=DE=DE=DA=D6=D5=D2=D1=D7=E6=FE=
+m^RO]pXU=ED=CF=CC=CD=CF=D3=CE=DDZLPNEBKe=F1pi=E6=E0oVU\c][q=DC=D9=E1=DF=D2=
+=CE=CD=CB=CD=CF=D8=D5=D7=D7=CD=C9=EFAl=BE=CFOC;<A<:s=B8=BA=C9=CB=D8W>35F=
+}=CE=BF=B9=B8=BA=C4=DEe]_f=F1=DC=D1=D2=E0`LC>=3D?GHJk=CF=CA=CD=CD=C7=C3=CCz=
+j=DD=D8=E1=EE=E4=CF=C9=D0=DF=DF=EB[MNMMVdf_ZUVRLIU=E7=DCvr=E5=D7=CC=CC=C8=
+=D3U=CB=B5=B9=D5O>C^OL=C6=AF=B5=C9z?5314E=D8=BF=BB=C1=D7wXKGNn=CE=BF=BD=C2=
+=CB=DD`LA=3DBUm^k=D0=C4=C5=D7=FD=DA=C8=D2XP=F5=CC=CC=F9Z}=D6=DDdTVgkSLUq=EC=
+=E8kW[ZVX]Xk=CD=C4=CF=7Fr=D9=C7=C2=D1PA=E0=BC=C7ZBBOuOG=CF=B4=B8=CF]D>A>>=
+S=CE=BE=BC=C2=D5=F9la[f=E3=CD=C5=C6=D1=FBWJB>=3DDONL`=DB=D9=E3t=FC=D0=CD=F3=
+[=EE=CF=C9=CC=D7=DD=D6=D1=D7=D9=D9=DA=DB=E2=FCignxw^UNGELTVZ=FD=D5=D6qNW=E2=
+=D1=CF=EANK=CC=BB=C4yHKw=D9_S=C7=B5=B7=C6pIBFADW=DD=C7=C5=D2hPMLMTh=DB=CD=
+=D1=E3y^PIIKMW=FE=DC=D1=CB=CA=CC=CB=C7=CB=D6=DA=DC=D2=C9=C9=DFX`=E9=E0oON=
+TWKBFOUPMOTVUU]q=E8=CE=C4=C5=CF=D9=D0=C9=C0=C2=CE=D6r=EF=BE=BA=CFD=3DP=EDm>=
+=3D=DE=BD=C5W=3D<@A:=3DM=DE=C7=C7=D4=E6=E2=DD=E6=ED=DD=CD=C1=BC=BF=C8=D7=EFxYG=
+AL_wwfy=DA=DDbT_=E7=DEvey=E6=EAlb|=D5=D6fS_=EF=EEdYl=D8=DC_Sc=EE=FC][=E9=D3=
+=EB[x=CF=C8=CF=ECkV=EC=C4=CC[FT=EC=E9TD=F4=B9=BB=E6R=F5=E8P<9M=D8=CF=EA=FC=
+=DB=D2=E7QHU=F5=DC=DB=D6=CF=D1=DFt^JCNcl`n=DC=C9=C8=E2o=E2=D0=CE=D8=E2=E3=
+=D6=D4=DF=E0=DF=E9~nd`[OKSb[QTanZJKf=EEnRT=F0=D4=CE=CE=C8=D7^=CD=BA=C3WK=E6=
+=C8=D0HF=C3=AF=BD_i=CF=E5@2:]=FAOO=E8=D3=FBGF\=FC][=DA=C6=CB=F8b=E7=E1Q@=
+M=EF=FAY]=DD=C6=C9=E9=E6=C6=C3=D4=E5=D9=CD=D4}x=DA=D3=EEaik^RMOUNLT[YSX_`=
+_j=F3=F0=EF=E7=DD=CD=C3=C3=D3g=DB=BE=C9XMk=DB=F7GF=DF=C2=D0b=E7=D0sD<DLIM=
+r=D5=D9=DF=DE=D2=D2=EB=EC=D4=C9=CB=D1=D9=D4=D5bKNXJELT_^Xh=DC=DB=E9=D9=CF=
+=D2=DF=F4=E5=DF=E7=FE=E1=D7=E4p=F2=DFy\Zlx[Ydma_n=FDujak=E7=E6=EA=D1=C7=CF=
+=FC=D1=BE=CFai=E7{OFN=F6=EF^c=E4=F5UHEGD@I\_i=DB=CE=CE=CD=CA=C6=C3=C5=C8=C3=
+=C2=CD=DF=E3=FC^XWVY\[_mjdf{sgdf_[VTWX\dosv=FF=F5=F8=ED=E8=E1=DF=DE=DB=D6=
+=D4=D2=D2=DC=DF=DE=DF=E0=E2=E5=E5=E6=EEi=DF=DF\[nVNONNYb`n=E8=EA|=FErnd^b=
+|wz=EA=DD=DA=D3=CE=CE=CF=CC=CB=CF=DC=E7=F3eWNLKIJJLO[_o=E9=E0=DC=D4=CF=D3=
+=D5=D4=D5=D6=D9=DD=DE=E0=E5=EE=EC=EA=EE=FAzvmg_[ZYVTVVWZ]__r=F9=FA=EA=E8=E6=
+=E2=DC=E2=DE=D6=DC=DE=DF=E1=DB=D7=D9=DC=D6=D7=DE=E1=E6=EF{=F6=FBnd^\XVSOO=
+PQRSUX\_bk=FE=EF=E6=DF=DF=DD=DA=D9=D6=D5=D5=D3=CF=CF=CF=CF=CF=D2=D5=DA=E0=
+=E7=FAjb_\YXVSPOPPQSTUY]bkw~=FB=EF=E9=E2=DE=DC=D9=D7=D4=D4=D4=D5=D4=D6=DA=
+=DB=DE=E2=EB=F4=F2=EF=EE=EF=F1=FErong`_^^^^`cbeeca___`aehjlv=7F=F4=F1=ED=EA=
+=E8=E4=E1=DF=DF=DE=DD=DC=DC=DD=E1=E6=E7=EA=EF=F7=F9=F9=F7=F8=F9=FA=F5=F1=F5=
+=FB=F7=F2=F9}xqlid][[[YY[^emy=F7=EF=ED=EA=E6=E5=E5=E5=E6=EA=EB=E7=E4=E1=DE=
+=DE=DE=E1=E8=EF=FC=7F=7F|yrnlhb_[YZZZ[^ajsy=FB=F0=EA=E4=E0=E1=E1=DF=DE=DC=
+=D9=D8=D7=D9=DC=DD=E1=EBzjgd`_^^^bb__agjjikllms=FD=F7=F3=EE=E9=E3=DF=DC=D8=
+=D6=D5=D4=D3=D6=DB=DF=E2=E3=EB=F8xpnd`^]\XTSTTUWXY]`fmv=FA=EC=E8=E8=E6=E4=
+=DE=DA=D8=D7=D8=D9=DB=DD=E1=E4=E5=E7=EC=F2=F8=FA=FE~~~=FB=FBupospqvrpniea=
+deeksy|=FC=FE}=FE=FE=FF=FE=FA=FB=FF=FB=F1=F0=EF=EB=EA=EC=EA=EB=F4=FFwspon=
+poqtvsrnotu}~=FA=EE=EB=EB=EC=EC=EC=ED=F1=F6=FC=FD~xx{~=FA=F2=F0=F3=F5=F5=F2=
+=EE=EB=E8=E7=E5=E5=E5=EA=F0=FFqh`[VVWYYZ\_hms}=F3=EA=E9=E6=E2=DF=DF=DE=DD=
+=DD=DE=DF=DF=E1=E4=E7=EA=EA=E9=E7=EB=EC=EC=F2=FBtjd`]ZZ[[[[[\^`dn=FD=EE=E3=
+=DE=DB=D9=D8=D8=D8=D8=D8=DA=DC=E3=EC=FDmd_^]Z[\^__eps|=F8=EF=E9=E5=E4=E1=E0=
+=E4=E6=E9=ED=F5=F7=F3=F5=F8=F3=EF=EF=F2=EF=F0=F6=FA=FD|vomjhjheccdbeihkty=
+y=F8=F1=F3=EF=F0=F1=ED=E8=E2=DD=DB=D9=D8=D9=DA=DE=E4=EB=FBuokd_\ZZXXZZ]^^=
+`foy=F5=EC=E3=DE=DC=D9=D8=D7=D8=D8=D9=DA=DD=DF=E3=EB=F7ynifc_][\\]^_ceitz=
+=FD=F6=EE=E7=E2=E1=E0=E2=E0=E1=E3=E5=E7=EA=EE=F8yqolhZ=EC=E4JG=CF=C5?I=CB=
+=CF=CC=D4TQ=C2=C3>L=D9lVv=CE=E7O=FAkZ=FEmU=F1=C9=FCP~=F1mu=EF=DF=DF=D0=DF=
+=EF=DD=E6z=EF=F9=EE=EE=EF=ED}nj^X\[\bedgnuor=F3=F3=EC=E4=E9=EC=ED=EB=E8=E8=
+=E9=F9=EC=EE=ED=EAz=FA~=EA=E3=EC=DF=DD=E2=E5=E0=ED=F5=E9nodZcXX\TZUO[ZY^i=
+=F9=EF=F2=EA=DD=D7=DE=DD=DC=DC=D3=DB=DA=D3=DA=D9=DA=E5=DE=DC=F0=E9=E6qefk=
+b]\]beZS[\W[Ye=EB=E6=FB=FD=FBfwlw=E1=ED=E3=D7=D4=D7=E4=E1=D6=D5=E3=DC=DB=E1=
+=E1o=FE=FFn_V\b[PSXWUOVZYrun=E9=D6=D5=DB=DD=D9=D0=D5=DA=D9=DA=D9=E0=F3=E9=
+=EC~=FB=E8=ED=E8=EB=F9=F8opmmecheZSUYUW^dhcl=F3=EE=F5=E8=D8=D5=EEm=EF=DE=E9=
+oo=E8=E0=F0=FE=7F=EF=E5=DC=F3s=EC=DF=E9x=EB=DC=DE=F0=FC=EFtZ_=E2=D7=E8[Y=F4=
+yUN]=F0gVb=F2i[\]r=F1wd=EB=D3=E8a=EB=CC=D3jz=D2=CF=E3=EB=D7=D4=F7l=DF=D9=EF=
+p=E1=D1=D7kYh=F9YKO`bRMRRMLPX\oog`=F8=D9=D7=D8=D1=CB=C9=CA=CF=D4=D1=C8=C3=
+=D3s=D0=BE=D2MLSe=DD_IV=E4M:X=DFI@=E1=CA`Z=EF=DF=E5[O\=E5qHS=D8=D6k=F4=CB=
+=C6=D6=ED=E2=DA=CE=CF=DB=F4=DE=CC=D0oKLtwQI\=DE=E7x=F6=DF=ECkz=FCzq=E9=D5=
+=E8dnyk=E8=D4=DErhPEOan=EDOK=C9=BAm9G=D7=EEC>=E8=B4=B6=DA=D4=B4=B8V8D=F2\=
+B\=C0=BE=DDc=EA=E1UER=D4=CD=EBo=DF=E6[KEGMJEKf=FAv|=E9=D4=C9=CC=D6=D8=CD=CD=
+=DF=EB=D6=CD=D6=EC=DB=CD=D8pb=F0=DChMMk=DCnHKanJEWN}=BC=C7[N=E7=E1H?H=D9=B3=
+=B5=CA=C8=BD=D2<0;IO\=DE=C0=BE=CEg\oaW=EA=CA=C2=C6=C9=CE=FCPEBHE@Gl=E5q=FD=
+=D9=CE=CC=CE=D5=D8=D6=D2=D3=D5=D7=D6=D8=DEq]as]PP\PHK[v`MO`k_hXU=C3=B7=D1=
+M=E6=C6=F3BE=E5=B2=AE=C6=D8=BD=C6A/8JKKl=CC=C6=DDQMYZO^=CE=C1=C4=CB=CF=D2=
+=DEYBAT~]Sm=CE=C9=DEZx=CA=CBm^=DD=CD=DF_f=E3=DAjSgyWLQi`MNf=E1=E6]Vkp`h=EB=
+=E9[=D3=B8=BDkT=DC=D8K=3DJ=C5=AF=BD=EC=CE=C2f62BYh=F4=CE=C0=C7uO[m_e=DB=C6=C1=
+=CB=DB=F3dMBCDCOnv\^=E8=DA=E8iu=D5=CB=DA=FF=DD=CA=CD=E0=EE=D5=CD=DAo`}=F6=
+aZe{lRP_m`NOd=7F^W=E0=E5c=CD=C3=DC\d^FG^=D7=B6=B2=C3=CC=C7=F9;3?W=ED=D3=CB=
+=C0=C2=DCSKW_d=E4=CE=C5=CA=DDeOIC??EOu=F2fh=E9=D5=DFp=DE=CB=C6=D1=F3=DC=CC=
+=CE=ECu=D4=CD=E1ll=EFiOO_=F2fQV\_]OLW=FA=FAi=F5=CE=CEk=DD=C5=C7=DAaQQd=ED=
+=E5=BF=B5=C0=D9=F7WA9<Gf=DB=DC=D4=D6zSLRc=EB=D2=C9=C1=C3=CF=EDZJDGJLOp=DB=
+=DA=E9=FA=E0=D5=DD=E5=D9=CB=CA=DC=F5=E7=D7=DDd^uo]RV]\X_=FD=FAeajl_[}=D7=DC=
+ac=DE=CF=D8=E3=EEW=F1=C2=C9=F4WQNR[[=CE=B8=BD=D6=F4hN@>H}=CF=D1=D1=D2=EF[=
+KLa=E2=D7=CF=C9=C7=D7eJGLF@L=F2=DDpZj=DB=D8k[=D4=BF=C3=D9=E3=CC=C3=CEm}=CD=
+=CA=ECTV=F1=EBUITmYJIOOJNZ[OM^=D7=CF=FA=F0=DD=D9=C0=BD=CDbl=D4=D1=E1i=D7=B6=
+=B2=C3n`=F2T=3D:I=E7=DAiWTWMDFT=EC=DA=D6=D0=D1=E4^OSWRO\=DF=D3=E4ir=DA=CF=D6=
+=DF=D7=C9=C7=D2=E6=EC=D8=D1=DDzz=E2=EE]SV[XSYnmSMTbh[Ww=D8=DAl^=E6=CF=EB=DE=
+=CC=CB=CF=F2[M^ux=CF=C2=C0=C8=DF`I?>CVr=DC=D3=DD=F9fY_n=E0=CF=C6=C2=C5=CB=
+=DFbUNNKHNWhaSQ]=FBsm=F5=DD=D2=D8=DE=D8=D3=D2=DA=DC=D8=D3=D8=E6=E6=E6=EA=FC=
+g]^feYSRZ\TOOg=E2=F8bj=F1d=E9=C9=C8=CF=F8_d=F2~_=D3=BC=B9=BF=D9fQF?>Ik=DB=
+=D7=EFe_XXT^=E7=CE=C9=CF=D6=E8m]NHHM[=FC=F1hf=EC=DE=DE=E0=DF=D4=CC=CA=CC=CC=
+=CD=D2=DA=DF=DF=DB=E1=EFphh]XPNNMKJLNLHKX]al=EA=D9=D6=E8r=D4=BF=BB=C3=DAy=
+=D8=CA=D0=E6=D5=BD=B7=BD=DCTNKB;>MnrPJJMNLU=F2=CF=CA=C9=CC=D3=D9=E9t`TT_=F1=
+=E9=F7=F6=E7=D7=D9=ED|s=F2=EC=F0=FD=E9=DF=E8=EFr|=F0=F4|v=F9=EF=FAmcdddZU=
+[a^XZg=FD=F4{=FD=DE=D3=D4=DA=DE=DD=DC=D7=D8=DB=DA=E7=FFieh=FB=E2=DC=D5=D4=
+=D9=E8aRMMQ[hjvoe_`el=FB=EA=E2=E3=EA}jc\XVVX^gv=F6=E7=DE=D8=D2=D0=D0=CE=CC=
+=CD=CE=CF=CF=D4=D7=DD=EB=F9widc_[WQMKIJJKLMPV\f|=E9=DC=D4=D0=CF=CD=CC=CC=CD=
+=CC=CC=CD=CE=D2=D7=DD=E8=F2=FDywrqqh_ZWTRRTWZ^^]]dkknv}yz}=FC=F6=ED=E6=E5=
+=E5=E6=E5=E5=E4=E3=DF=DF=DF=DF=DF=DE=DE=DD=DB=DB=DC=DF=E6=E8=EE=FAuke\ZXV=
+VVWXZZXXWY^hz=F7=E4=DD=DB=D9=D8=DA=DC=DA=D9=DA=DC=DD=DB=DB=DE=E3=E8=ED=EF=
+=F8}uohfhklkmnmhbbggffikkknplifglpz=F4=E9=EA=EA=EC=F0=ED=ED=EB=E8=E3=E1=E2=
+=E4=E4=E4=E2=E2=E4=E3=DF=E0=E1=E3=EA=F2}j`^[YXYY[[ZYZ^biv=EE=E6=DF=DB=D7=D5=
+=D4=D4=D5=D7=DC=DE=E2=ED=F5~vmfa`achlmlkiihhjmnqusppv|=F7=ED=E9=E5=E6=E7=E5=
+=E6=EA=E8=EA=EA=E8=E8=ED=F6xjfghjlpsqkfinor=FC=F2=EB=E1=DF=DF=DE=DD=DC=DF=
+=E5=EA=EB=F1=F9yuod^[YXWWXY[]do{=F6=EA=E1=DE=DA=D8=D7=D6=D7=D8=DB=DF=E6=EF=
+=F6~vrpxvtmlkiihikmnmklmo{~=FD=FC=F8=F2=F5=7Fy{xvx}=7F=FD=FF=FC=F5=EF=EE=EC=
+=EE=EF=EF=F1=F0=FB=FC=F6=F4=EE=EB=EA=EA=EB=ED=EC=EF=F3=F9=FDwojf_\\_cchmo=
+ps=FC=F3=EA=E1=DF=DE=DD=DC=DA=DB=DD=DF=E8=F3=FCsg`^ZXWVXXX[^_djn=FE=F3=E8=
+=DD=D9=D7=D5=D4=D3=D4=D7=DB=DE=DF=E2=E3=E6=E7=E9=EA=EE=F6xld_\[]^^acbdgil=
+spoonmmnopw=FB=F0=F0=F0=EE=ED=EC=EE=F1=F1=EF=F0=F1=EE=EA=EA=E8=E7=E4=DF=E0=
+=DE=DD=DD=DD=E1=E5=E8=F3vojd^]]\[ZWXZ[^dn=FE=EF=E9=E7=E7=E9=EA=ED=EF=F0=EE=
+=E9=E3=E0=E2=E1=E2=E7=EE=F8=FB=FCypnmkkkmr|=F3=EE=EB=EE=EF=EB=E8=E8=EB=EE=
+=FAvka^^^^agmw=FE=F5=F2=ED=E9=E9=E5=E7=EB=ED=EC=EC=EE=F4=F6=FA=FD=FCzxtnj=
+fccacgnw=F9=EF=EB=E6=E1=E1=E2=E4=E6=EA=F3=F9=FB}|usuuxuuz}=F9=EF=EA=E7=E6=
+=E7=EA=EC=EF=FDyojf`^^____`__`ehmy=F6=EB=E6=E3=DF=DD=DB=DB=DC=DB=DA=DB=DB=
+=DB=DB=DE=E2=E5=EF~roojf`][[[\][]^^aelv=FF=F4=EC=E2=DE=DC=DD=DD=DC=DD=DE=DF=
+=DF=E3=EC=F8~oiea`__^^`dfinpw=7F=F7=EE=E7=E0=DD=DB=DA=D8=D5=D2=D1=D3=D4=D6=
+=DB=E1=F0yj_[URONNOOQTW[^floz=F5=EC=E6=E2=DE=DE=DC=DA=DA=D9=D9=DA=DB=DB=DB=
+=DC=DE=DF=E0=E2=E4=E7=EE=FByplhd_]\[\\[_cgijr=FE=F5=F1=EF=EC=EA=EB=EE=EF=F5=
+|qmnlkkq=FF=F5=ED=EC=E9=E3=E0=DD=DA=D8=D5=D4=D5=D9=DF=EBrf^YXUUVVVXYYZ\_b=
+fkty=F0=E9=E3=DC=DA=D7=D4=D2=D0=CF=D0=D1=D1=D3=D5=D8=DC=E0=EA=F5sf`^\]ZWV=
+TRRSSUWXZY[aekw=F8=EB=E3=E0=DD=D9=D8=D5=D3=D1=CF=CF=D0=D3=D5=D7=DA=DE=E3=ED=
+=FAvha^[VUUVYZ^hnx=FE=F6=F3=F3=FA~tmmmmpusvwuqons{=FE=FA=F4=EE=E9=E3=E1=DF=
+=DC=DC=DB=DB=DA=D9=DC=DE=E0=E1=DF=E3=E6=E8=EC=F9=FEyog_[WTRPNNNNOOPSX^gx=ED=
+=E2=DC=D6=D2=CF=CE=CE=CC=CC=CC=CD=CE=D0=D5=D9=DB=DF=E8=EE=EF=FDonieb__\XV=
+SPONOOPRUY^dl|=F0=E7=DF=D9=D4=D1=CF=CE=CE=CE=CE=CE=CE=CF=CF=D3=D7=DA=DC=E3=
+=EC=FEk]VPMKJJIJJKLNSY_iy=EC=E0=D9=D4=D0=CD=CC=CA=CA=CA=CA=CB=CB=CC=CE=D0=
+=D5=D9=DD=E7=F3o_YTOLKKJJJJKMOSX^jz=F2=E3=DC=D8=D5=D3=D0=CE=CC=CB=CA=CA=CC=
+=CD=CF=D1=D5=DB=E1=EEyf[TROLJIIJKNQZeu=EE=E4=DD=DA=D8=D6=D7=D8=D8=EB=D9=BF=
+uF=DB=E2=CE=CCU=E9=D5x=F8Y=F1=D5VX=E0=D2=DDFd=C7OU>N=CB\=EF[=FA=CCHTUZ=F9=
+E=E7=DA=EB=CEi=D0=CD~=D0=DE=CE=CC=E9=D0=E2=D0=DCVwdkfOlz=DDmy=DCl=E9\O`HO=
+MNKOkm=E2=EC~=D0=CF=E2=EF=CA=CA=CC=D7=EF=CA=C9=D1\=E9=D3=FCiSw=D9rWW=FF\S=
+NJtYGQamOS=E4=E2=D2zf=CF=CA=D9V=DE=CA=CE=D8=EA=D2=C5=D4Uh=CC=F2`S\=D2wKO=DA=
+fORW=E7nO_=EF=F8W=FC=D9=FD~g=E1=CF=F3U=F7=CA=D9bm=DC=D1=E7\~=CD=D0mc=DB=D3=
+nNY=F7tNHUp[JN~=E7h]=FB=D9=DC=EB=E4=D6=D1=D6=DE=D5=CB=CF=DC=DA=CE=D3=E6~=FF=
+=DE=E1l[g~]SVTOIFJKFCNbdY]=E5=D4=DA=DF=D1=C9=C7=CA=CE=CA=C5=CA=D3=D0=CB=CD=
+=D7=E9=E5=DE=F1c[cbTLPWPJIPVNJP_aXYu=EF~u=E6=D5=D6=DC=D7=CA=C5=CD=D8=D0=C8=
+=C8=D6=DE=D7=D2=DEo_a^QOOOONSZXSTl=FCmjw=ED=E4=F3r=F4=DF=DE=FBp=DE=D0=D9=F3=
+=F5=DA=D8=DF=EF=EB=D4=CF=D9=EC=DD=D0=DFgbuoZXYQMLNZ]VZ=ED=DB=EEu=EA=D7=D3=
+=DFpfsfQKMU\bu=DD=CD=C7=C4=C1=C1=C1=BF=C3=C9=D2=DF=ECvXJ>?SM@KMIHIJT=F8id=
+=D9=C7=CE=E2=DA=D4=CF=D0=DF=E8=DD=D4=DF=F4=ED=DC=D1=CF=D0=CE=C8=C5=CA=D4=DC=
+=DFpM><>=3D96:AIJQ=EE=CD=C4=C2=BE=BA=B6=B7=BC=C2=C5=C9=D4wTTVNH@BHJJKOU\_Y=
+Tx=D3=D4=D2=D0=D2=CD=C7=C9=CD=C6=C1=C1=C1=C2=CA=D8=E3iVKC?>><<<?EJR_=F7=D3=
+=CB=CD=D2=D5=CF=CC=D2=F9g=FE=E8=EEln=E3=D2=CA=CA=C7=BF=BA=B8=BA=BD=C0=C4=CB=
+=DE]OID?;657989;?JQMU=DB=CC=CC=CD=CD=CD=C7=C2=C6=C6=BF=BE=BE=BE=C0=C7=CA=CC=
+=D6=E7pZROJFEFJMPUi=DA=DB=E8=E1=E6=E0=DFmUTUMGDEMYh=FA=D8=CC=C4=BE=BD=BC=BB=
+=BA=BC=BF=C7=CD=CF=DBsTJEEC??@CFGENcdd\UTckd=FE=E5=DB=CF=C9=CA=CB=C9=C9=C9=
+=CA=CE=D5=DA=DE=E4=EE{jl|=EE=EB=DF=D4=D4=D7=E0}xr\MIFCA?>AIR_=F6=DB=CD=C5=
+=C0=BE=BD=BE=BE=BF=C4=CB=D0=DD=FF[MFEEBCDIKKVc=FC=DE=F0ik=E6=D9=D7=DB=E9=E2=
+=D2=CE=D6=E0=F5=FC=E8=E2=F0megntliu=EB=DB=D4=D1=D1=CD=C9=CE=D9=F1rvnWJGGK=
+KIJSi=E6=D9=CF=CC=C7=C2=C2=C5=C9=CE=D9=E9v_WLD?@BB@AEKO`=FB=D9=CF=D3=D5=D1=
+=CA=C6=C3=C3=C6=C4=C1=C2=C7=CD=D7=DD=DE=EAlYOJIHFFGKS^hy=EA=E1=E3=EFlckkb=
+YTU[bhq=EB=D7=CD=C8=C5=C2=BF=BF=BF=C1=C4=C9=D0=DD=FBaVLD?=3D>?@CGJNa=F0=DD=D9=
+=EAz=EB=D7=D2=D4=DB=DB=D3=CD=CE=D6=E2=FFx=FEo^YUW\ej~=EB=DD=D3=CE=CA=C7=C6=
+=C9=CC=D5=DF=F4hXNHDCBACHO]y=E0=D5=CB=C6=C4=C3=C4=C4=C7=CC=D6=DF=ECv\MDAC=
+FHIKM]=E9=D6=CD=CF=DC=DB=CE=C8=C9=CD=D5=D4=CD=CC=D3=EA\SUSMHDDGJKLR`=EC=D4=
+=CC=C7=C3=C2=C2=C5=CC=D2=D8=E0w^PKHHGGLVk=EA=DB=D1=CA=C6=C2=C1=C2=C5=C8=CD=
+=D3=DD=FB^OHEFGEEJNOWg=E5=D5=DCxj=F3=DE=DC=E7or=E5=DB=DD=E8qj=FA=EC=FBi\]=
+iooqz=F2=DD=D1=CB=C7=C5=C8=CA=CE=D6=DD=F5bTKEB@??@FQc=ED=D9=CD=C6=BF=BD=BD=
+=BD=BE=BE=C1=CB=DB=EDrZI?>?DDDGM]=E9=DA=D6=D9=E5=EE=E9=EC=F9=FDmo=EC=E8=FB=
+l_^msnmiflqro=FE=EB=D9=CE=CA=C4=C0=C2=C7=CB=CF=D3=DA=F4]OIGD@?CJVf{=E3=D3=
+=CC=C7=C5=C7=C9=CC=D1=DD=F5aUNKIHIKMQ[t=E3=DA=D7=D5=CF=CD=CD=CE=D2=DD=E9{=
+f]ZWSRPTYXZ\d=F6=E0=E4=F7{=FE=F1=F6w=F1=D8=CC=C7=C4=C3=C2=C0=C0=C6=CE=DD=FC=
+k]OF?=3D<<=3D?EN^=EB=D6=CD=C9=C6=C4=C2=C2=C4=C8=CB=CD=D4=D9=E8s_YUOJGEDDDDDEG=
+KSZh=EF=DD=D4=CF=CB=C6=C1=BF=BF=BE=BF=BF=C0=C2=C5=C8=CD=D8=E5pYNIGECBBEHN=
+QUY_lu=F6=EB=E1=DE=DE=DD=D8=D4=D3=D4=D3=D0=CF=D2=D5=D6=DA=DC=E4=F1=FBoe]U=
+QONMLLMNPRW^ju=FF=EE=E6=DF=D9=D6=D6=D5=D3=D1=D2=D2=D3=D2=CF=D0=D3=D5=D7=DA=
+=DF=EA=F3=FBzlf^YTOMKLMOQUZam=F9=EA=DE=D9=D5=D1=CF=D0=D4=D5=D6=D8=DC=E4=EA=
+=F3}mea`_][Y[aaaefjkfkov}unx=FA=F9=FD~=FA=F4=E9=E4=E0=DD=DC=DB=DA=DB=DB=DC=
+=DF=E2=E4=E5=E7=EC=EE=F7=FEtoommkha]\YYYZ\_^al}=F6=EF=E6=DE=DA=DB=DC=DC=DC=
+=DF=E9=E8=E7=EE=F3xmhd_]___cfmr}=F0=F3=F3=EC=E9=EC=F5=ED=E8=E6=EA=EF=EF=EC=
+=E6=E7=EA=E9=E7=E8=EF=FDz=FC~=FDthknmmikrx~kdcb_ZY]hmln=EB=DF=E0=E3=DF=D9=
+=D5=D7=D9=DC=DE=DD=DF=EC=F3=F5wmlh`_]^_bd_ev=EB=F8o{=EE=E7=ED=FD=FD=F6=E8=
+=DC=EDbm=DA=D5=DF=EF=E3=D0=CF=EEURg=F8`T\=EF=E4w^]hyo]WW\]XY_ggaf=FB=E4=DB=
+=DA=D7=D1=CE=CD=CF=CF=CE=CF=D4=D9=D9=DA=DD=E2=E4=E5=E1=E2=E6=EE=F6=F3l]ZZ=
+SLKLNLJJKOSTW^q}w=F8=DC=D5=DC=E5=DB=D1=D4=DD=DE=D2=CC=CB=CF=D2=D0=D1=DB=F0=
+=F1=E6=F0ian=7Fkabnt`VON]=F9lVMPg=FFbZz=D3=CC=D5=EC=E1=D0=D3=F5`i=EC=E0=F8=
+o=EB=D8=D3=DB=E4=DD=D2=D1=DC=EA=F7pZIAEIE=3D<J|=E3{=ED=C9=BC=BE=CC=D9=CB=C2=
+=CB=FB_=E7=CE=DB[Qo=DE=FA]a=ED=E3oQKNOHCFJNTYmzh=D8=BF=BD=C3=CA=CD=C6=CC=FF=
+\=E1=C2=BD=C4=CE=C8=C7=EDI>>ED?ANp=EC=FA=EF=DA=CE=D0=DD=EC=EFxXG?=3D=3D=3D?GW}=DF=
+=CC=BF=BD=C0=C2=BE=B9=B9=BF=C9=C1=BC=C7pVi=E6eIGYbJ=3D>GH?=3DHXPHO=FF=F1bh=D4=
+=C0=C4=D4=D8=C5=BE=C7=E3=DE=CB=CB=DD=E8=DD=DDUR=C3=B9=CBL@=EE=CCO8=3D=CC=BB=
+=CCXX=D9=E8>02?JD@T=CD=C4=CF=DB=CE=C4=C2=CE=D7=CD=C5=CD=EAn=FE=F2hORo=DF=7F=
+`=F5=D3=CD=CE=D6=CE=C8=C6=D2=F5peZQOPTRQPOMNNKIGKQOP]=EB=D8=D7=E1=E3=CD=C6=
+=C8=C7=C5=C4=C2=C1=C4=CE=D9=D7=D7=CD=D0=ED=F0}Nk=D1|H=3D=3DX=F9A6F=CD=C5=E8KL=
+=ECx=3D6?[=E8=E2=DE=CB=BC=BB=C3=CC=CC=C8=C5=CD=D5=CF=C9=CF=E6c\SF=3D<I]MAJ=DD=
+=C6=CDq_=D5=C9=E0NX=D0=CC=FCX=DC=BE=C2bHq=C7=D4JBt=CB=EBA?h=D1gGO=E3=D1{N=
+[=D4=C8=CD=DD=DA=C9=CB=FCWTm=D4pK[=CC=C5=D8D?=DD=BA=C9G;L=CD=C8O=3D`=BD=BB=CE=
+]P=F8{B7;M=F6=E4|=EE=D2=CE=E0ia=FC=DB=CD=C9=C9=C9=CA=D1=E9eVQMU=F4=D3=E6a=
+=DF=C7=C2=CDxb=DB=D3fKZ=F6sRLPcTGDJSWUOYlko=F1=DD=CF=CC=D2=DD=D7=CD=CB=CD=
+=CB=C4=BF=C0=C9=D2=D8=DD=EFe]RMMPOOLJJGBEPg=FB`Sg=D5=D7=E9=ED=D3=C2=BC=C4=
+=D9=DF=D9=EF`Y]=EE=D9=DD=EEl_aqeZ^=ED=D6=D7=E4|=EE=E6pSKSk=F7YU=DF=C8=C8=E3=
+]o=D2=D7]Ny=CD=D4ZIQ=DE=DEUK^=F9jUNm=D6=E1bs=D3=D4yV^=D6=CA=D9w=DC=CA=C6=D1=
+uy=D8=D5=EFjx=FChYSS\hZ]XMKQOg=DE=FC\WL]=CF=D0x=F1=CA=BF=BD=C7=E9=F8=D3=F0=
+MJW=E9=CF=D9=F4=E1=D3=DFhOR=FA=D3=DFfi=F2=FCUA?LaQDW=CF=C8=DDd=EE=C6=BE=CE=
+f=E0=C1=C1=FCU=ED=CA=CBlKV=E2lHEO]XLLV[MIV=F0=DC=EDv=EF=D5=CC=D2=D7=CD=C1=
+=BE=CB=E4=E0=CE=CC=DEicx=DE=DF=EA=E9oZTKHU=E1dHKLKH@C\=DF=FA=7F=CF=CF=E6=FA=
+=F3=DD=DB=E2=D9=C6=BE=C3=CA=CB=CD=CC=DA|=7F=E1=DC=E3=F5f[WKDCED=3D>Ks=EDhl=DE=
+=CF=D7=F9z=D7=C5=CB=DB=D7=CD=CA=D5xf=DA=CC=E1\p=E1=EA\LU=EBvIE[nTJJe=E0cN=
+=F9=CB=CE=DF=EC=E4=DF=E9=ED=D9=CB=CD=D6=D5=D2=CE=D9mh=D6=CD=E3_]f=DD=D9dN=
+QMDBHJQc=F7=F6iVXrdM^=D1=C6=CF=EE=DC=C6=C5=E6b=DA=C7=CA=DD=EB=D7=C9=D1ca=E3=
+=E0S>?R=EDbJU=D9=CFjM]=D5=CB=DEh=EC=D2=DE_f=E8=E9=F8\XkhTS_\T\_ekgm=E5=D2=
+=CF=D0=D2=D5=CF=CA=CA=D0=D9=D6=D3=D4=DFbl=E9mRILScfLKPNM^aqqX=EB=C2=C8QC|=
+=C6=C5=E1]=CC=BB=C7QQ=D2=C7=E1OT=DA=D7RHY=E3=DF]Oe=ECYFQ=ED=DB=E8o=E6=D1=D7=
+_NWn=ED=E6=EB=DC=CD=CC=CF=DA=EE=EF=DB=D9=F9k=FE=DD=D3zNTiRCBFNQIKk=F2ZV{=DC=
+=D5=D4=D2=C9=C3=C6=CC=CF=CB=C4=C2=C4=C6=CB=D0=D5=EESNXZYRKHI?9ATOUUU=F6=E5=
+MI=E1=CF=D6=CC=D1=CE=C6=CE=F3=E9=CB=CD=D4=CC=CB=CE~On=CF=DCNK~=CF=DAI>U=D9=
+zIJo=D6=DAo=F4=D2=D6=EC=DF=D1=D3qUt=D2=D7WJd=DB=F6J@P=DC=E2KDa=D3=DC\V=F1=
+=CD=D0d]=DD=D6t]=EA=CF=D0=FFb=E0=CF=DBgv=CF=C7=D2y=EF=D0=CFuU=FE=CE=D4_LQ=
+v=F2TH[=DDyG?M=E8=E3K@_=D6jGM=F1=DBjm=CA=BE=C8bc=C4=B8=C6_=EA=BB=B6=CBMJ=EF=
+=D3V>E|=EFI>DZ^KJ_=D9=DEbk=D7=CE=D6=DF=D7=CF=CE=E0bj=DD=DA=EB=F5=FE=F0=E1=
+=EA^X_hv=F1=F9=F9=7Fc\=EB=D7=FC[Zd=EF=E4j]=FC=DE=F7rh\f=E2=DEr`h=E9=D9=F2=
+W[=E5=D4=DFiZj=ECwi=F8=DF=DA=DF=E0=E1=ED=FE=EC=DC=E2=F7=EA=DB=D6=DAx_=F5=E4=
+[Oat_iYNNUNPXT[=EE=DF{dh=F8=DA=CF=DE=EC=D8=CE=CE=D2=DF=DC=CC=C9=D5=EB=DF=DA=
+=E1~l=FB=EAtUPRMJIKLJL^=EFoPU=E5=CC=CD=DE=DC=C8=C2=CB=D7=CF=C3=BF=CB=E2=DB=
+=D1=DAcUUOMLJGDA@EJILd=EF=FA=F6=E6=E1=DD=D2=CD=C9=C7=CA=C9=C4=C3=C8=CD=CF=
+=CB=C9=CD=DF=E8=EEbROQQOMIHJJKGELVZUR_=FE=F8}=F5=DC=CB=C5=C8=CB=CA=C7=C5=C7=
+=CB=CA=C5=C2=C8=D5=EBihq\OORPMGACJIHMUYXUXc}=DE=CE=C9=CC=D2=CF=C7=C7=CF=D8=
+=CF=C6=C6=CE=E7t=F2=E7=F8q=FE=F6o_VPPONLOQQQOOX`cr=EC=DC=D5=D4=D4=D1=CB=C8=
+=C9=CF=DB=DC=D5=D2=E0na=FD=D8=DC]O[=EC=F7XLT=EA=E8ZORZ[NJQdjfcj~=DC=DB=D8=
+=D5=D3=D0=CD=CD=D5=D6=D3=D5=D7=D8=DB=D9=D8=DD=E4=E2=EDmZWWVPLKKJIMTZSUp=D6=
+=D3=EB=FD=DA=CA=C8=D6=E6=D5=C8=CA=E1y=EE=DC=ED_\{=E2=F4[Uco_Yay=FBbX_=EF=ED=
+jf=EE=D9=D9=E8=F1=E2=D8=DB=E8=F3=EC=E0=E5=FDjedfa[XZ]a^Y\itv~=EB=DF=DC=DA=
+=DA=D6=D5=D9=DE=E6=EC=E9=E9=EFvh_\[[^cghjim|=FC=FE=F9=E8=DE=DD=DF=EB=E1=D8=
+=D6=D7=DC=D9=D6=DA=E5|ung]Y[[ZVSUXYVVWZZ[dv=EE=E6=E1=DE=DD=DC=DD=DE=DE=DC=
+=DB=DB=DB=D8=D6=D6=D5=D6=D7=D7=D8=DB=DC=E3=EF{lb[UPNMMMOSVY]aix=F9=ED=EC=E9=
+=E5=E2=DF=DA=D7=D7=D7=D7=D7=D9=DA=DC=DF=E5=EE=F7=FC}ojf^YSPORUVYY]hx=FB=F0=
+=E6=DE=DB=DA=D9=D7=D5=D5=D5=D6=D4=D5=D5=D7=D9=DC=E1=EA=F1ucZTQOOOPRUVV[_f=
+ko=FA=EB=DF=DB=D6=D4=D2=D2=D3=D4=D6=D7=DB=DF=E8=F7ukb\ZYYZ\_ejs|=FC=F5=F6=
+}yw{=FF=FA=F9=F7=FEz|=FF=F5=F6=F2=EE=F2=EA=E5=E0=DE=DE=DE=E1=E5=E8=E8=EB=EF=
+=FCzxql`_```fkmmjgc__^_cfmw=FD=EF=EA=E7=E2=DF=DF=DE=DD=E1=E2=E1=E3=E4=E4=E5=
+=E5=E7=EB=EF=F1=FBvmjjgfegghmq{=FC=F7=EC=EA=E9=ED=FFqje`eijmnpnluz{=FF|us=
+tw=FA=EF=E9=E4=DF=DF=E0=DF=DF=DE=DE=DC=DC=DB=DA=DE=E1=E9=ED=FCmklgda_][XU=
+TTRQSSUX[^fu=F4=E5=DC=D7=D4=D0=CD=CD=CD=CD=CC=CC=CD=CE=D1=D6=DB=E2=EC|kec=
+a\Y[[YYWTRSRRPPQPPRSV[du=EB=DC=D3=CC=C8=C5=C3=C2=C2=C1=C1=C3=C5=C7=CC=CF=D8=
+=E0=F5eWNJFDCBCCBBDFIMS]n=EF=DE=D6=CE=CA=C8=C6=C4=C3=C3=C2=C3=C4=C5=C8=CE=
+=D2=CF=DBhTNKLLHFHFA?ACIUf=F6=DF=D9=D8=D3=CD=C9=C6=C4=C3=C5=C8=CC=D1=D7=DB=
+=DC=DC=DF=EBu`XOLKIIKLNPQVZbv=EB=DB=D7=D3=D0=CF=CE=CC=CB=CB=CC=CE=D4=DC=F0=
+la\WTTSRRUW[]`ed`^\^h|=F1=E8=DD=D9=D4=D1=CF=CE=CF=CE=CF=D0=D3=D8=DE=E5=E9=
+=ED=F6uf]ZXRNMMNOQSV[_ait=F9=E8=DF=DB=D6=D1=D0=D0=CE=CC=CD=CD=CE=D0=D4=D7=
+=DA=DB=E2=EFvf^WQMKJIHIJJKKNSX_i}=E9=DF=D8=D3=CF=CC=C9=C6=C5=C4=C4=C4=C4=C4=
+=C6=C9=CC=D2=D9=E0=F5n^WRMJGEDCCB?FJCGENa_r=F4=D7=CF=D4=D0=CC=C7=C4=C3=C0=
+=BE=BC=BE=C0=C5=CB=D0=DB=EDql`]XTSPNLLLLMLLLLMMOTZ_ck=FA=E3=DA=D4=CF=CB=C8=
+=C5=C5=C6=C5=C6=C9=CC=CE=D1=D3=D7=DC=DE=E6=F2p_VPLGB@@AABDHJMS[g=F7=E0=D7=
+=CF=CC=C8=C4=C1=C0=BF=C0=C1=C4=C6=C8=CA=CD=CF=D4=DB=E4=FFbXOJFC??>>>?@AEH=
+NZn=E3=D5=CD=C7=C3=C0=BF=BE=BE=BE=BE=BE=BE=C0=C3=C7=CC=D5=E5nZOJEB@@??@BC=
+FIKMSZj=F7=E3=D7=CE=CA=C7=C5=C5=C4=C5=C5=C4=C5=C7=C9=CC=D0=D6=DD=EA{eZSMI=
+FFFGGHJLNORZcs=F1=E4=D9=D1=CF=CC=CB=CA=C9=C9=CA=CB=CB=CC=CE=D3=D9=DE=E8=FD=
+k`ZXTRPNOOOPRVXZ^`fmx=F6=EE=EE=EC=E6=DE=DC=DC=DC=DB=D8=D7=D8=D8=DA=DB=DE=E3=
+=EA=EF=F7=FF=FCzvpnlljfgedfijkkjgfffjlx=F4=F0=ED=E9=E6=E1=E0=DF=E0=E4=E6=EF=
+=F9yrngils}=7F=FE=F7=F1=EC=E9=E6=E4=E3=E3=E4=E6=EC=EF=F4=FDuj`_^[XZVQlZ_d=
+Z=FE=F3or=EF=E1=D7=DF=EA=DC=D7=D9=DB=E8=E1=DB=E1=EA=E9=E4=DE=E0=F3=E7=E4=E3=
+=E6=FCx=F9|qjhpob]\\^YUZ]]ZWX]^]^do=F7=ED=E8=DF=DB=DA=DB=DD=D9=D3=D1=CF=D0=
+=D2=D3=D6=D8=D9=DA=DC=E1=F5yohb^\[[ZYXXYYYZ[ZYY[^ejy=F0=E6=DD=D9=D8=D5=D0=
+=CF=CE=CE=CE=CF=D2=D5=D9=DC=E3=F8od\UONMLLLNOORW\es=F9=EA=DE=D7=D1=CF=CC=CA=
+=C9=C9=C9=CA=CB=CD=CF=D4=D8=DD=E1=EEzkc]VOKIGEDDEGIKNS[do=F5=E3=D8=D0=CC=C8=
+=C5=C2=C0=C0=C0=C1=C5=C8=CB=CE=D4=DD=E9{cYQLIGECCCCDEHKOV_p=EE=DC=D5=CE=CB=
+=C9=C6=C3=C2=C1=C1=C3=C4=C7=CA=CE=D3=D8=E3=F7j\UOKHGFEEEFHIKMPV]fy=EA=DE=D8=
+=CF=CD=CB=C9=C8=C8=C8=C9=CB=CD=CF=D2=D6=D9=DC=E3=ED=FCoc[UOLKJJJJKMOSY^h=FE=
+=E9=DC=D5=CF=CC=C9=C8=C6=C6=C5=C5=C7=CA=CC=CF=D8=E4yf\SOKHFECBBBBDGKR[j=EF=
+=DE=D7=CE=CB=C9=C6=C4=C2=C3=C3=C4=C6=C8=C9=CA=CD=D0=D7=DE=EC}f[WROLKIHGHI=
+KLMOSY]dks=F7=EA=DF=DD=D8=D2=CF=CE=CC=CA=C9=C9=C9=CA=CB=CD=CF=D3=D8=DD=E1=
+=E9=FBk]WQNLJGFFGHHJMQXav=EE=E0=DA=D4=CF=CE=CD=CC=CB=CB=C9=C7=C8=C9=CA=CC=
+=CF=D4=DC=E9q_YTQOOOONMLMMNSY_fr=7F=F4=E9=E1=DD=DD=DC=DA=D8=D7=D8=D9=DA=DB=
+=DD=DD=DD=E0=E7=ED=F0=FC{vtvv=7F=F3=EF=ED=EB=ED=EE=EF=EF=EF=F2=F8ymlhb^[X=
+USTTUUWZZ[[^`i=FC=EA=E1=D9=D3=CE=CB=C9=C7=C5=C5=C7=C9=CC=CF=D7=DE=E9|j_[W=
+QNLIHGHJLNSX^gn=FC=EA=E0=DB=D5=CF=CD=CC=CB=CC=CB=CC=CD=CE=D1=D6=DC=E0=E6=F0=
+|l_YTOONMMLKKKLLNPW]gv=EF=DF=D8=CF=CB=C8=C5=C4=C2=C2=C3=C4=C6=C8=CB=CE=D4=
+=DD=EA=FDka^XRNLJHFEEDEFGJMPV_p=F9=E4=D7=CF=CC=C9=C8=C7=C7=C8=C8=C8=C8=C9=
+=CA=CC=CE=D3=DA=E2=F0sf^WOKHFEDEGILRY`n=F8=E5=DB=D5=D2=D0=CF=CE=CD=CD=CD=CE=
+=D1=D4=D5=D7=D9=DD=E3=EC=FCulg^YUSONMNORVY\`jt=F7=EA=E5=DF=DC=DB=DA=DA=DA=
+=D9=D9=DA=D8=D8=D9=DA=DD=E1=E9=FCrjfa`aefcb__][[\^`dhlnpw=FF=F5=EA=E1=DD=D8=
+=D3=CE=CC=CD=CE=CE=CF=D1=D6=DD=E4=ED=FBvc]YUUQOOONMLLLMOQV]j=FE=E9=DE=D7=D3=
+=CF=CD=CC=CB=CB=CB=CA=CB=CC=CD=CF=D4=D9=DE=E4=EB=EB=EF=FDxk^WRMLKKKKLNOSW=
+[bn~=FB=F4=EC=E7=E3=DD=DC=DA=D7=D4=D3=D2=D0=D0=D1=D4=D6=D8=D9=D9=DB=DF=E4=
+=EB=ED=FCywtrmd`_\YUQOMKKLLLMNRY^l=EF=DE=D5=CF=CC=CA=C7=C4=C3=C2=C3=C5=C7=
+=C9=CD=D3=DC=EDn^TNKIIIJLNORVX\`el~=EF=E4=DE=DA=D8=D3=D0=CF=CE=CD=CE=CE=D1=
+=D7=DD=E9=FDi^XRONMNORW]`el}=F0=EA=E4=DE=DA=DA=DA=DA=D7=D6=D5=D6=D7=D6=D8=
+=DA=DC=DE=E2=EA=F3zoh^XTOMLLLNOQVZ_hs=F1=E5=E0=DF=DE=DC=D9=D6=D4=D2=D0=CE=
+=CD=CD=CD=CD=CE=CF=D1=D6=DC=E1=F0xf_\VSQOMKKLKLLMNQV\ep=FD=E9=DF=DC=DA=D8=
+=D5=D0=CD=CC=CB=C9=C8=C9=CA=CC=CE=D2=D9=E0=EC~k^ZUQOOOPRSV[_``fmt=FD=FB=F6=
+=EE=E9=E7=E6=E7=EB=ED=ED=EF=F1=F2=EF=EE=EE=ED=F9}{vvtwx=FF=FA=F1=E8=E5=E3=
+=E0=DF=E2=E3=E3=E4=E3=E7=E7=EC=F0=FAunkjjid`__]]^bgijhd__][]ajv=FE=EC=E1=DB=
+=D6=D4=D3=D0=CF=CF=CF=CF=D2=D8=DB=DF=E5=E3=EA=F9oc^ZTPNMKLNOUX]afmv=F9=EB=
+=E1=DE=DC=DA=DA=D9=D7=D5=D5=D6=D5=D4=D6=D8=D9=DB=DE=E0=E1=E3=E2=E3=EB=FBw=
+--hal_9000
+Content-Type: AUDIO/BASIC
+Content-Description: I'm sorry, Dave (BASE64)
+Content-Transfer-Encoding: BASE64
+
+6u75/fv38enj4+Lk5+fs8/3+eHdzb21oY2FgXV1eXFpZWVteY2v66eTd29fV1dfa3N3e3t/f
+3+Lk6/L8cG1oXlxaWVlZWFpcXmBkZ2hoa21tbnV5/vTr5ODd3NrZ3Nzd3+Pl4+Df4OTue2pm
+X1tZVlVXWl1fYmZtefjv6uTe2djV1dbZ3uXveW5vbWllYWBfX2NmY2JhXlxaW2FrdfDq5N7c
+3N3f3+Dl6u309350en1//P59fP79enp2cG1sb3V+9Ozl4d/e39/n7e/29/r+eHFtZl5YU1FQ
+UVNWV1ZYW2N19Orf3NjT0M/Ozs7P0dXX2Nve5fR0Zl1YUlBRUlNTVFZZXWJvfu7n4d/h3+Hl
+5+rs6urn5OXm5ejv9Pl7a2VlYFxeX19fYWZna3B6+/Ls5N7d3Nzc3uPm6e3+b25qbHJwbGhn
+bWxqbnBxc3r68+3o6Ozu7u3s5N7e3ufu9HhsYlxZV1dVVlhbXV9iaHP+7+vo4t7c3Nvd39/f
+3tzb3N3e397g6PZ2b2ljX15bWVVTUlNWV1tcXV5kcPHj29bT0dHR0dXa3+rv7+/1/3drZ2Nd
+WlhWVVZbYW5/9+/t7enm5eTk4+Xl6fP7/HptamppaGpscHBxevfu6OHf39/f5Or4dnRxc339
+endza2loZGRnaWpoaWxyenx8ff317ero6OPh39/l5+7w9PT0+fr59/5uaWFdXFlXVVNUWV5k
+bXp+/PHu6ODc2NbU0tDOzs7P0NPW197o+HZqYFtWU09MSklIR0hISkxOUVZbZH/k2tLMycjI
+yMnKzM3O0NPW2t/p+Xl1b2hhXFZST05NTExMTVBXXWdy9urh3dvZ2tvb29ra2trc297j6O30
+d3FqY2BfYmRiZGFfYmdlZF9fY2NjY2hy9urm497b29nZ2dfV1dfa3+fu/HJybmReW1lUU1RW
+V1teYml2+/Ty8Ovp5t/c29rY2dvd3+Pn7PD5/nFoZF9fXlxaV1VVU1JSVlpeZG/z5+Dc2dfU
+09HPzs7Q1NfY2tzf5ez5fXVtaWNcV1ZUU1JOTEtMTk9VWV1iaW/56d7Z1dHOzs7Oz9HU19jY
+2dna293m9nRkWVNPTk1NTExMTlBTWV5ka3zv5t7a19LPzMrJx8fJyszS2+h4Y1ZPTElHRkdI
+SUpLTE5PVFtfceze1c/Lx8TDw8PExsjM0dnj82lbVE9MSklISElMTlFWW2Fv9uff3NrY19bV
+19jZ2+Dp8vn9eG5qamZlaGxtbG1ycG9xd//z7+/v9PLw7Ofm4+Dg5Ons9XxvZ15aV1ZWVVNS
+U1RXW19u+ubb19PRzszKycjIyszP1dvoeGBYU1BOTUtKSUpMTU5QU1ldZn3u5d3a19TSz87O
+zs/P0tfc4uv3enBqZWFeWllaWlpbW1xdXVtaW11hZ3L66t/b1tLPzs3LzM3P1tzm9nNlXVdS
+TktKSUlJSk1TWFxhce3f2NLNy8jIyMrLzc/T2N3i6PJ1Z19bVlBOT09PUFBQT05NTU5RVl5r
+9ODXz8vIxcPCwsTHys/W4fZpXFVPTUpIR0dHSUtNUVpmeufe2tfW1dLS09TV1tbY297g3+Tq
+8f5zaWJcWVVUU1RUVVhZXGBt/+7m4+De3d/f5Ofr9vh8f/378e7u7u/6fn58dXVuam9vb3h/
+/3h5dXRubW1z+vPs5t/a19PS1NbZ3un9aV1YVFNST09PT1FUV11ganL+8ejc19bV1dbZ3N3g
+5Obp6+7r5+Hi6Ovv9nxxamlnX1xbWllYWVxdX2h58+zo5d/d3eLn5uXn7fb7fnZtaGZiXlxb
+W15iZXH86t/b1tLPz8/P0NPX3ur+a2BdWVdVUU9OTUxLSkpNT1VbZHrn3dfSz83NzMrJyMnI
+yMnLztLW2+f8a11XU05LSUZEQ0NERUdKTlNZXWBw6d/b19TQ0M/Ozs3Nzs7Pz8/Pz9DT19rc
+5PlsX1tXUk9MS0lISEhJTFBWWl1o+ejj39nRzc3OzczKy8/W3OX2c2ddV1NRUlRTVFtmZ2Jh
+buzi5O3l29fY3uPg29zl93B0bGRcWVtcXV1dY2z/9vPs5+Tuf/rq4eLp49zc4O318fLz+Hp9
++Pvx+HZvbXB0amJdW11hXVdSUlZbXVtcbvn8ffjn3tvZ1s/Oz8/Ozs3Nzc7S1tzl7nlqYVdP
+Tk5RT01LS01PUVNXWl1fZGp1797c29nWzszO2NzZ2d7uf3rw92xvevz/bmlrdn1ucPLu+XZx
+8ejt7/D2+Pjy+Ph1anR5aV9faW9kVlJcaGJdYnB7dGRkc/357d/c3t3Z0M3O0c7LydDc2tjf
+Z1VRUlVNR0hOT0tJSk9ZW1Zf4tLP41dQ28LH7GHZv8bex7a4YzlCzbW85Xfdakc7MjVCZett
+VkVCRUdKYODbxbeyxUY+X7y62k9Wy8xURVHRxdL76s7J4FVd39DVd2BtcH1ZRUdIZ89WRUJM
+2fBVSU9OQc25qrMyKy/Pqa23w7Syby8nLkbIvcHDz+1HOzpFbc7Cw8LM7FJBPj9LWmT/3NXj
+Y1hu2c7Oz8/Q2u335dnd9Xro33ZZWGJ5b11bZ2pWTU9UWFdh5XVr39zO4V3qy75ZP8W7t788
+Mzdrx9q6ub/BTzAtNkFY2MO+wt9FP0dVaNTDvLrE3FNCP0FNcexk28zdZVDszsfWX9XI2V5d
+59riXFv64G1aZnP7Yl9kcF5LTE9jXEtg3df0W9rO2HBMy7y9v0o8O07Mz7u1t7t1Oi81PEru
+wrzBz1VFQERPfMnAvsTXXUQ/PT9HW9jN6HXOy+hbdMq/xuzvyMl3T2nW2WNQY+prTEti8V5J
+S2B1VklLZuR0ZfLe09TX1uznyL61v0g4P97L1ce7s71QNjE4Oj5Zzb/E4lRJSUxNZ87AvsbW
+bU5GQEJLWn7h0snN6HDkyMzh6su+xXtY+M/vTk/+4F5KS1xbSkRMWFlPTVv+cWl35drOytLX
+y8jK2lfyxLe6WTo8b9dtVdS4t9s+NTs8Oz9dzMTPZU9SXVph9c3Av8bR61xPS01TX3XczMbL
+3X/u2uBsYfDZ2XNVVWFgVlNaZ3VtX15jZl9eYW/47+7y7Obf19HP0dTU1tni8Wdabd/P21dF
+Q09fZmd32M7T91RLSU9bbff07eri397c2NTR1NzndWVeXV5dWFNRVFpfbfHe19TT0dHT2d7o
+7ntmX2Fu+/h4b3J1b2lhXV1dW1paXF9qfezf2tnY2NfY19jZ2d57a+3a42JOSk5cXVZZbOPZ
+2e5jXFxhbXVqZ2htc/Dj2tDNy8zR3OXs+2tdVU1IRkdLT1llfeHUzc3Oz87Oz9be5u/7dXz8
+fP54amJeWlZVUk9QUFFTV1xfa3vs5NzV0s/P1dzRx8fU+WBcaXhrftrPz9fuXVFRUFBRTktL
+T1JXYn3j1tDS1drg5e17ZV5bU1NVWWT039rQysjJyMjLzdfvcGphXFtcWVdXU1JTUlFRU1NU
+VlhbZnb36NzWzsvLzMvKys/e5tfP1HxQSkpKS0xUe9zefl1ST05RW2Rsbmxv/ubXzsnEwsPG
+y9Ha5XdfUUpFRERERUhPXG/n2tHLyszN0trf5+vp6ezy9vt/eHJvaWVdWFVTVVZXW11gZ2/4
+6eXe2dfV1dXY4uTZ2+Pg9W/9ZVljcHnp5O31cF9YU1JSVlpYVFddZHjs39jV1tXS0tXa4Ol3
+X1lZXGVx9+jd2dnX1NXY2eT7bF5ZVVVVV1laWlldX2JqbXBwbmt1/vfs5+Lc2drZ1dDS1cza
+YWnz7GdcZmpwaFZOU1tcW11x+Xr88/rn2+Hn4d7ocG/x6ejy/vfs9mdeZ+buYGzz9e5lW/rv
+fmlh+e1t/W7w43vt8OHr8Xt86HJs/XDqc/16/d1e33F63Frcd3TdUvRVXWNU92Xh9npt7uz0
+3frS3+DaX9b66+Pu5Pz4anl4Xn1efPteXWBbZF1aa2BhXWNra/Zh73p563bd593V6tXp3NPi
+ytvQ2t/f4fbw6nnvbl5cTGJTa21T8ExfVFVlUGhcbO5ffHdc32/j4uzQ39fY2tfW09XV1OvU
+XuBqX+pTeVRnXFtdS1pNW1pV/1fqX+Hq+s/6zN/S0+zG987b2c170GHVXvVdVGlLdklhV0lm
+R15OUW9O2V7o/ubY5Nnf09fQ2NDU09jc3Nzp7fXq5mhrX15aVlNYT1xSVk9XT2ZWZP/13+/Z
+5c3c09fP0tPZz+bZ8+voX3NOa0tiT19bZFtf/F53eHfn3+vee9Vj23Hg3P/eeN/jZfxYfV34
+c2znX+Ve71ps/mXdV91v5N1fzv/R4NbV/NZtbO5P9UzxU1lkSv9Ick7/WG5tefLt3evS3eLR
+3dLX49fl0fTb6evoZ+dUcFxjcmZkamb7W2dVZ2Va7Froaep56u/7/O/2def27m3ra+T94Gzc
+YOlhZ2tx6Xzeetrk3uvg5t1152byc15baF5oY1fqafh0a+T8a9pY11ft/ufcXddn2/Ry4Gfo
+dmHuYvxX32Lweeft//Hs7/L07W3ob2tb9Xta5UzgXXVeXOD+22nf3tbc393p+dpo0Gvpbt/x
+Yvda2lbxXFzuU15TXl9RX1FscV3ie9T24dzr2dv23O/a4+fn49ng+tzueeBa7uZ89lzkb216
+T1ZvTWBOW/FZ2lTk9nTpWthl013q5f/cb/DZ0+7h127Tbfj4a2ph7mjbeOrSZtFp63JndHFW
+UV5Rb05fVWVhW11g1GjXd/Pf1X7c3uXV2PnQ78541dra8G9jeG3n71btYXFvSmZNXvxSbVbv
+dVt/VdJY6mpn5ONdcO3edeLn2ODO3NHgz93Z1Obm3mzaaPjtUvhLXlBaWUlUTlL+SXlhb/xo
+3VzdXNvW897X1tjd387P19PqwuzU6+/bVF1pXORKWEFRTEZUTGpc427vaG3r5t3q3tXU0NzO
+y9HN2OLa3u7famlkWV1NTkpPTklYUVlPT1r5cONqz9XK19LO18zg0tDJ2c/bztPqVXZp7llL
+U2BwTU1JX1tQT0tTV1ldWF5/8P5+/dzQ287NzcnPz9DG0s7J4MzwcG1qc1NZSk1WTkxCTFBM
+VEdcXvBfb+nf1OnY29bR1M3Q19DOzNbU9tjd4utiZWr6XlhSTlhVTkpITFJcU111fHppdN7Y
+/u7Ux8vm9NXCxtXg2MvVaGXu2uZYTlh+Z1NOTl5TSEpNTldfUFrZ2eB4XvvTytnq1ci/x97i
+0cfJ1e3n2uFoVlFXW1VKRklISUZERUtTTlBe7994X+TLwsTMxLy7vc3gvbnI1e1kbUg4QWLj
+4GlPUkk6MzAzO0ZPaebPx8THxcC/vsPCvLzRTER3xspmTHjK0VJATN/O5lxYXlhJQkJGTl5d
+Wl5l6uZtUVX72cvP0crV6cO+v7vOTD5GZcu8vsTGzlo8LywvN0FY7dbPys7b6nbo283LzcvG
+zG9QSFJhZ2Zq3dPX4ufd0tPgb1xTTk5MT1366ur1dWVo7t3f72laXuLJxtHlbV/UwdNi/V1C
+Pz5Bb767xs1vPzc3NjpL7se7uLzFztrh3tXPy8fGw89ZQDk7QkxMTmT66ej5/uzX1Njfe2dl
+f+fX0NXa397xX1JTbePfX0xKUXHb0X1IPVLIx8fRSz48TO7Fsra+v95GNzQ2PmTLv7y+y+Ht
+a198287IxcrS5U89OTtBS1hfbd3d4uXs3d3Y2+rt9u715djU0t7/Y15dVlRZZ+ndc1NMTlv9
+2tzvX0x1xsXDzVQ+OUR7wrCzvcpqQDc4OUBxx7u7wNNZT1BPYOTPx8bL2vZKOjg7SVlv6uzT
+0NjX2dbTz9Dg8HV0fe/l9XxqYWBaVk9PV1pge/VuX1VRXefRztLXZ27Ev8K/5EQ6PV7Ksq+8
+zV8/NDQ3OUrfxcHJ2VhLUFZr18vIxsbJ2VZCPkNVamzx4Njb3NvX0dPY4+p1XV5g/d3c7mNZ
+VlVQT1FTXW/v8GBdXmDo3NfX19Rz28DCv8tOOzdF7MGytsPhTTw1ODtCbsm9wMrfT05cbOTW
+zszKx9NZQT0+RFFVXOzTz9rY2d3X29/x6Ohtfuzg4vR5Y2VgVE9SX3H/eGxmaGtpc+re5eTc
+auTDwr/NSTo4TdS/s7nG3U8+NTY8SOLEvsncck1OWGHs3M7KycnvSD9BTVteXWrb1Nre29HR
+1dvu8e/r5+Dc4/V6b3ZtYVZOVGNxaFpXXWZubGVncHBmVuTCwb7MTDw7VMu8s7rE0ltEOTc9
+SffIvsTW/V9dZGr14dnNzdH+Rjs8R1ZUT1j13OLo39HKy8/a3+Pt6+Ha1uD3++/oel1YW2ho
+XlpSVV5tdmJZWFlY+svHxtZMPj5Yyby3usHLckk9OkBP88vEyNT6ZFtaXWns39TP1XNJQURI
+Tk9RZ+TW19rPycfJztXhemVhb/D2eHn4cltPTlJZYGRgX2Vvfevk6uh7Xd/Hvr7wPzxI2sPC
+wsLCzF9ANzY9TurQzdHc6PN3al9u5dfNy9dZRUZSXlRMVeXP2mpr2cjHzM7O1uhsYF1dbOnc
+3+9pWFBVWlZPTVNgbGRkXk1V2MO92z87TM6/wcW/urzNVT06QFL23tvZ1dHX62NYXW7v5+h4
+U0VDS1JST1nt09Lc39jNyszP0tTZ6Xlucvv8c2ldVVJccmNOSVN2eVpUVWTdzMPMTjxF3sHD
+zci8usdkQTw+SV/8+eTXz9Ppa15g/N3c6fb3Z09GR01YX2nw3NnY087LzM/T2uZ2X1pcZHHy
+8P9rXFxpXU1JTFx4eF5Z6MvCyVY8P3nHxdDOvLa95Uc+QklOUVRq39PU5Pb56+Xr7uje+1JJ
+SEpNT1z64Nna2tPLxcfNz8/W72ZgaGxfW11dWVZbXFFMUl9jV09OadLJzW9LT+PLzNbNvLa+
+3VFLT01IRkxl4+L6bf3h5HJr6Nv5W1lcVE5PWWdsZ23q2NTVz8fCxcvU3OHwe2dndWpgbu5e
+S0xYXFNSXFZd3Nj9UURFU3Xz99e/usHSf2dhT0hIVebX3tzPzdHX0MrO+2P0/kw/Q1FYS0JM
+7NxuX9vHydTUy8nR6fXr7XZrdOvf7mhgY2VcVFhXSl3W5VtQUllWVFrqxsHLy8nSdVNQT01R
+WGJ55eLezsbI19LGy/ZYVVdPRj9DUVNPWPze3+DW09jb2tzk5N/n4dbOzuHu2tnubujoVGvZ
+Y1VRTU1GQUhccFpb5NPxWFt1YU1PdPJx69TGwcXEv73AycrS5GVOSUZDQkZKTVhs7Ov48eDc
+5fv37H9o/Of0Zmbt3eb73td328zsfnJhXU9KTmfv/eza1e1ua2RcVlZbWVllb+XY1M7JxcjK
+y9DT62RYTUhDQkRLT1hp69zVz83P0tXg/nxkXFxVVF5u7dzy5sXPberd8mhbXebc6tzNzdTe
+73plUExMSERGSEtXVl/v7NzT1NbW0t30bGBfXV1me+/h2dLMx8TDwsTHzd/raVFQSUNGSEtK
+SXNtT19pWlRUVmJ+7+XZ0c7QzMzMzs/Q1Oj+el9iamBv+vL9cG1gY1pPTk1IR0dHSk9Yb+LV
+y8XDwL+/wcTJ0dngbFZZVk9NWHdUWX1mVVVYXVleanf75OZ/7uf8d+zq6evh6/Xg7O/h5t7h
+5OPp8mpaVE5LSUhHSkxTXGzp19DNycrKycrNztLZ3vl97WFq1G1f3fpaY2BZXWlkb3d2a19c
+XVlWYmRdZXRlYf5zbOzs397h3d3i3+h6bGpbWVlaXWVu7t7a1M/Pz8/R19rj63xfW1tVVE9m
++Vhq4PL84vLw7PZ78u7q+Xz28m9tdmVcXmRdXW1vZn/r5uzm2+bt63RjZl9bXVxcYGhv8+jd
+1NTR0c/S2tnucG5nX1tdWVJe6Wpi4+f35PH96OLr4d3b3OTq7/lxdGtjXl9bW1xZX2FfZW1q
+ZWltc29ubGFdXVpbX2d77ure0s7MysnIxsrNztXg6vxvXVZUU1dTUllZVlpdXVxdX2x5d/Po
+5OTg39/d3uDh7Pp8b2ZeW1lbXF5ma2tyfvj09e3l4ebt9H799vLt5+Le3N3f3t/j4+ju9Xlz
+c3Vwa2hnZmNhYWFfY2NkaGtydXVzcm91eHz27enl4+He3t/i6O/0+f16dG1pZ2RgXl5eXl1d
+XV5ga37v5d7Z1dPT1dbY29/l6+7v8e7z/X17fXh5en16c3VvaF5ZWFhYWFhaXmFpbW9wdHp3
+c3Fudv727+zp5OHf3NrX1dXW19nd4uvv/Hz+fnx8dW1rZ2NfXFpYVlZYW11fY2lvevTp39nW
+1dXT1dnc3+ft93FqZF5dXFpYWFdXV1lbXWNu+e3o4N7e3Nzd3d3e3+Hi4ubq7O/v7fB8cmxn
+YF5dXF1eX2JkaG94+fLu6OHg3tzc3t/e3+Lo8P5waGNeXVxbWlhXWVtfZWx7+e/s5uTk5OTn
+6Ofo5+rr6Ort7/f/enhxbWtpam1vb25sa2xtb3r27ebh397e3tvZ2dvd4OTq8npmXllXVVZU
+UlJSU1RWV1pfaGt48+vg29fS0M/P0dPV2Nvf5ejs8v51dnZ1bmtkXlxaWFdXV1dZXF9iZ27+
+6+Dc2NXU0tLT1Nfb3+jufm9lXVtZWFlbXWBgX15eXl1eYWhwfPrw6Obj3tzZ1tTT1NbX2dvf
+4u57cWllYFxZVlZVVFRVVlpdYWp2+Onf2tfT0dLV19vf5e5fXstkRExR09ppYF/v62dtZmla
+5NPfaPHkfNfm62FUU8q/72ZIYOhgaPVr9Wne7fRUU/llamnd+X5kd9b8VGLq39ro3NDfatrg
+3m7y2tHfe+rqYFdTWU5OTFNdTlBZYG5n9t3X397T1tfV2tXW3Nzb2dzX3N3l6/VzYl1mVVVO
+Tk1LSUtOUVVdYnnk5N3b1c/O09LPzdLV1s/Z3u/l6HNfZmdXUVFUUU5OU1pWVmBycGrv2tXY
+2tLP1t3T2et69d/rX1t57m5kbu94W1peXFhbXFBUaPDn7d3Kxs7W0M7XcWV0Yk5LT1JNTFl8
+9+7azsnM1tjT4V9TWVxXVVVKXM5f5cdzYWBc6+pd58fM2OXt2PhOVf1pWFvv2uvv2MnM19LO
+2W5aUEg+ODc5PkhFTOLPw7++t7O3vcPGzmhKT1NKRUlXY11l7N3tZl5ecH9NP05OSU9PaMnR
+W8y0v8TH3dZ3T1zj3vLcycxgUWFURD5FTk9JVPXd4N/LxcnNztJ3TkdGQzw9TGZk89fDt7q/
+u7m+xuBZUlJSSEBNX29qXGXe3mdcZmlZSkhMZVtLXe1ucnvdy8bXUOO1xEtjz+9OP0rSw9vi
+vLjZUV5gSj9AWehYXc/GztXPxsjhYl9eRjw8PDw9SlxiWdW7wtbGubvI4drH0VZZ1tBeW+Td
+blNWcmNISVhTSEZLU1VRU2/xWV7Z1dvUysh+zrbAXGDDxGE/UcO73mu/uXg/QVJENjdR/U9S
+2crdau7T3ltVcW1NRk9XVFBh0srl4cK9x87Kw8HQf9rM+1FacV9PSlRaS0JNWk5JTVxgVVRs
+7Xfsz8/Pzs3Mzs3Jx8/Vz1r4xN9KVdhqRD1E69JRU8zGUj5BUEY7P+3P4dS/vMfQysTO7OrU
+32FUXF9PT15mT1Lv2exz6c7VX1vt+lxPU2doX2jf5Onc1N7l59vjc2Hu625p+eLhe/Tf+2B7
+5G5ab+pvPUTAxkQ/28ZgOj/RveZ0vLXMVFjoXj5C3cprc8nD31Zq3HdOUfb7T0tVXUxGUPlq
+UHzMy9LPxb7F29rN1ntrc3hlX2BkX1xeW09NVFNMTVhcXF5q9uXe2dbU0Nbb1dLSy8vQ0OJR
+zr39Q1PaeD42Rt7yR1rHyk9DUWpJPlPT3HvPv7/Q1cfE237a1WRPVVtPREz/akhL39ZwXOjO
+z/fz1dD9aeHa7eLX0t91dvRlUE5XWlRZZ2JaX3z4aWfq3fN59eDg5PDYy9/21l3svs9XX+ff
+VT5E9NFu+cfG9FBOVUs9QnHtYPPPy9Z9383Yd+/c8VxZXlhMTmXvW1Pdz9bVzMXBy9fOzn55
+72JYamtlYFdYaldMVV5QTlJUVVFUYW5w7+He19Tb0s3R0tLbz9Db41z0vtFQW+/vVUJFZ+Ru
+/c3QflhPSkU+RVxjZd7T1tzn39rl6Nzf5+XsfXxqZfPs7XLj1dTa0szL0dzt9exoU1VfWFdb
+WVZWVFdVUFJdX1xfYmH953712tTd4tvU1Nze183T39fQ3OplXM7KVU1wdlhJQk38aF/iztz9
+ZFlQSkVOYVxw1tHT09TQ0NjX1Nff4/JoWVNSWlxYVGvg4O3j1s3P5vbe5WZtZmD17m328WNe
+YlpaXFlfdmpaW19oZl1t2tbe39XP0OPez9L48Nzc7W5xYWDc6VZPV/ZeQ0FWaXPy08rK13xn
+W0tKXG1q49DOztTT09Tf4uj9bWNYT0tLU1ZTUWrk3uPa1dHV2drU2OHh4ff849/c6/by6mpW
+VFVUVE5NU1JOTlFTWF519V7+ytF+2svMytDPw8XS193Nx+NZWW5iTENJXG5qb9zbblhOSkU/
+QktUV3vc0s/TzcjJztLR0N39al9fY19aXu3g59rQzsvT3dbcdWtqV1FYVlFUVFRYWVZbWFNX
+WFpjaWf44ubb0tbVzsvMz9LN1unf5XT+8GhxblLyzVtHUV9UTklM7+1138zS5eLrb19OUGJr
+Y3vb1tPQzc7Pz9Tk+WZXT0tKT1RPT27e6+vTz9vi3+DxbGplZ2docnj/ffjw7v585+t+8ul+
+bXt/cf7m6m5+5uP7YnTxe2xnZm1kU27mTWjD6k1h7nv6W0/cx9ncxsTZ2eNjW09MUFhVXXjs
+39/f2tXY5fN1XllPSUpPT0tX6eTe1s/Kxc7TyMvb5ODqfHNfY2piYmRZUFdWUVlbVl1lXWB3
+dn/m5OXY1+Dd2+ry3NfgfenZ301Vy9dOTWJgaFRNe87l787G2t3b9GRaWVxlWV376Xzp3t3d
+3eDwdmRbUUxJSkdHTlpebNvRz87KxsfJys/T09Xf6eDwevX9YF9iVU5OTktJSU1PUFRcX2js
+3OHd1tTQzs7RycbS2+fhyNVYWPNeT01KTVtaXPnb7u7qb2BdWlpdXmF38evf29na1NHW3N/v
+Y1ZWWE5KU3dxa9rPzsvJzczN2uPl+mZnaGZnYV5qYltZWVRSU05KTFNOT1tkZO/e29zYzMrP
+zcnIzczL3dzk09b9W15oWE9MS1tkXFx3+mVuX1hXWFNXXF9s8+zn2dPNzMvNztHd73FnWlFV
+WVlWY/Lv597X1tff6+1vaWleWVhgXV1dYm51b3zv9nF4+Ht2+u3p397c3Nrb1tTd7dvadGv4
+X05r5llOWlpUT01PYmlt59bU19ba3+x78fptbe/2eOvl6uDd29vh8P1tWk9KS0tOUFh139fV
+y8XIy87P1Nrh9/JxYVtgYV1dXl5aVFFOTUxLTVFTWGn56NzX1dPKyczPysfO3/TYxttWYN12
+T0xSYVxXX3p0ZGdiWVVUVVpaW2f9+Ong3drQzc7Qz9no729aT1pYVFhfdOPW2tHIzNvc1vle
+X1tWTlBUWVpf+unh5+Ll7f5ucHNnYGVwe+/v9n7w5vT9+H1v+mhc495kb+Hf//nt3tja2s/O
+3+3ualhRT09QT09aXWBo/unk3trW2t7p+XFlWVdYW19kd+Xb1dDMyczT1tjrb2ddV1ZTVlxe
+Z3v9e/R5aWZnXV5iXmnv5u3j2djb1NTd6OjnZlRr62NTWnNcUVRp+PR+5tHU5Obb5mRicm1k
+afrs6+/j2NXW1dbif2VeUUhFRUhJTVVr4dfPyMXExMbIzdTc53VkXltYXF1cW1lXWFhOTE5O
+SkpPVVpect/Tz8zEwcLDx8fByeR+3OtWTVNeXFRSX2xYTVFWTklMVFZSXfPd2tLIwsDDxMXL
+4HBmVEhAQ0hJSE5s3NPOycXFzNPX3ftnYVxWUlZfZWNr9+js+HZ3ZVhUVVtYVV5+8u/l3NXQ
+1ObZyM98ZPR9W1Zi6Nje6NPJ13N87mhSUFhZUE1RXF1ca+PZ3t/e3/pnW1RPSUdLVmf618rE
+v769vb7Ey8/bcllQTEpGREdKS0tPVFhYVVRYXV9cXf/e3NjOxsLCw8O/vcLS6eT6Wk1MUVpW
+T1BZVU1LTE5MSkpPVFhefNvRzcnGw8TJzc7R5mldWlNOTE9ge+7j187Oz9DP0+JxXlZRTUxM
+UFdZXmRy+n12a2d29Xxv/OPb4eHVzc3R19fP1OTk8GhbVlRUVVFVYmphYWlqZl1XW2VnZWzx
+39jX1M3KyszO0NPdd15aVkxISU9XVlZi6t7i5dvT1+b16eZ7YGb+83x57N/h7PT1cGBmaFxa
+XGhxXmTv3+Ty5dHP3fHj095dVGT5bVlXb93kbWL34Ol0aX7q7m1ofOzo8Prr4t/m7HplX1tU
+T0xMUFRTVF5z7OXo2c/OzszJycrNzs7R2uHj525bWVtZTk1QUk5LTE1SUFBcXWNufN3U2dPN
+zMzQz83Q3u3e2d92aPrmellXX2JXTU1QVE9NUl1reXl96t3Y2NbQzs7S19XX2t7o7/Z0bGVd
+Y2JeXVpWVVNSVllVUltrcmpv59rW2drRzc7S0czN1dnb2udub/lvaWhiV09LS1BPS0tRXWBb
+Xm7t6+3e2tPP1drW1NTU2NnZ3Obu7+x1W1xfX1NMTlRTUVVbXml67ubn39rZ3unh2drf5enn
+6OPc3N/f3N3scnD/bltTVllUU1ZbXV5janT8/vXs8Pbw7evq8fz+6d/i5ubf3t/j7fHx6+97
+bmt++XdrbG5uaWJmbm1kX2dqal9aYGJfYWv18fbn3dXP0NDQ1dra3uTr5eDp9f/6e2tiXl5a
+UU9OUU9NT1FXW1tjfe/o49vZ2NbTzs7U19bS2N/o7Of+Zl9gZGFbWllbXV5fYnHt7/bt4dzi
+6/fw8X1uY2VobXtqaPrp535ka+/7Z19lbXBwbv7159vb29vZ1tfc6u3m6vNxaHJxZmBfZ2ld
+WlxfX1xYXW14aWZ17+vr6+Xh39nW19rh5drb6fD59/xqXVpdYGFdW15pbWtranz3e3N08eXl
+6evs5eb17+7w8nZ2fP34/nR39ufl8Pjr4+v+b3J0c2BaXF5cX2ptb29+7eHh5uHa2d3g4efh
+3t3d6HdnaV9XVVFQUlJSVVpfa+7h7/De29jZ5Ond2d/s8e7n6/P27ebj3dzm6+/r4vZuYmr0
+Z1lcW19fWVZUV1lZWltbZPv1+vzu3dnW1dLOzs/W3d3oem5ua2deXGZqY11gbf13ZGJy8ft7
+/O7t9fr45O3/7//48Ovr7+nu7fV2bWloZWBhaGttZV5qfvV9fOzf3+rt4tjc5d7f6fR2a2Ri
+X1tZWVtaWldWWF9jbfjv5NfT09HOysjKzM/Nztjb7HBtXlVMSEhHR0VFSU1PUlhZYfzn39va
+0czNzs3Lys3P2Nzd5vhpZGNgX1lUVV1dV1teZnRqZmdnffZ2cnjr3t/i39jS1NrW0NPb3+Tn
+8W9oYl5aV1ZTT09UWltVVl1fX1xWW3Dv7ejj1s/P0dTQz9LY3N3f3tze7fz/9vlmXWNwZltZ
+V1hYV1ZYXF1fXmFpe/l6evPq5ubn3dXU1dfV1dXU1tXY3uXi4fpeWltVTkpGRUhKS05OT1lm
+ePjs3dDMy83MyMXHy83NztTd6ebvZ15YWVZMSElKS0tLTlNYW11qd/He1tHV087LzdXX0c/S
+2trY3OJ7bHhgWFpYV1FPTk1MTU5SWFdaaP76/uXc2dPV1M/Rz9HY2dja29vd5ebn+PTu+3dq
+cG5hXFVTVFFQUVNWV1VXWlpcZGxw8+/x4NbU19jUzs3O09LPztPX3Oz8cmpmXFRXWFlaUVRd
+X2NcWmJva2x87eTv+3f77/V9bnzt5ujy6d7Z2+Dg3Nzf7f58cWxoZmFcXWFmZWFldfj3+X/y
+6/l8ffz09vHq6Ofn4t3a3ubq6u9uXlthYVtWUVZeX1lWXWdmX19u8enp39nT0NDQzs/R0NLT
+19re3+h1Y2BoZllQUVVWUE5PVlhVVlthbnJufvby7vD15t3e4N7c2dfZ2dzf4t7e4+vv5+Tz
+d2xqbm5iXWFlaGFcX2NnZWNkanZ6e3n+8u3r8fTt6u/+evnt6+3s5N7i6/Tr6u7w/Px7dHBr
+Y11bWltcX2VufPv5+vPr5OXn6+3q6uzt7Ovr7Ozp7fHv8Pt3b2ljYV9haGprbnB5+/f69e3o
+5OXj4d/i6/b/eXl4dG5rbGlpaWpqaWhlYV9fXl9ka3L87eXf39/g4N/i5uz19/l4eHr47erk
+4t7a297h7Ph/d2xgW1lZWFdXWVxeYmNiZ3F++vHt6ebp8/v07+zp6OTe2tnZ2trb3ObxfG5l
+XFdWWFpbW15nbGpvbW99+PDv7Obn5+nn6evv+nl+9vLv8PHw7ezw9vXz8vD6f3d2dnJ8fHt1
+b2lkY2VnZ2dnaGlmaGt1f+7i3dvc3t/k6O/+b2lnZWBcW1paWlxdZG987ePc1tHOzcrJyMrM
+z9bZ5fppXVdRTUtKSklHR0dIS09TWF5oeOne2dTRzs3Mzc3Nzc7Q1NbY3OPl7PD8b2NeWlZW
+VVZYWFlZWlxeYWVsdvv7fXZ0eXt7+PHy6uPg4N/f3d3f3djW2+bu8/NyX1hXWFhWV11offby
+7Obf3d7f4ufs9v53cHJubW1pZWFeXl5fY2lxefTq5+Pi397f4uLm5OXp7e/w9PT9dW5oYV9d
+XFxcW1xeZG179/Tq49/f4uTj4N/e3+Hg3+Lo7O/1fnhuZmJgYGFkaG5vbG1tbWplYl5eX19d
+Xl9iZWhudfLs6uTf2tfV09PQz8/S1NXU1dfc4ez6c2RbV1VRT01MTEtMTk9UWFlaW15jbfzr
+5d/a1tXS0NDPzs7Ozc7Pz9DT1trj8W9gWFNRT01NTEtLTE1PVFdaXmdw/e7o4Nza1tbW1dTU
+19jX19fY2drb3ePq8/15bWVdWVdTTktKSk1PT1FUWlxfY2r96dzX0s/OzMrIyMjIycrLzM7U
+2+j+a19ZUk1KR0VEQkJDREVGR0pNUlxq7drRzMrIx8bFxcXFxcXGyMrMztDX3ul8X1VPS0dE
+QkFCQ0NDRUhKTVFXYHTo29TOy8nIyMfHx8jLzdHY3eXv/nNnYl5bXF5gYV5dW1pZWVpbXV1c
+XF1dYGBnbXB69enj3tzb2tjW1NPU09PW2+Hr+25iXFtaWltdX2VqbG5ubm1vcXR+8u3o4d3c
+3N/n8HxvamRhXlxbW1tbX2RobHB1evr07ejm5ujk4t/e3tva2NfX2tna3N7f5e39b2JaU09M
+S0pJSkxOUVRYXWd57d/Z1NDOzc3My8vNzc3P0tjd6v1vZF1XUE1LS0pKSktNTk9UWV9odPzs
+4tvW0s7MzMzLysrKy8zN0djf8m1dVlBNTEtLTExMTU5PVFtkcPvu6OPf3t7b29vZ2dbV1dfZ
+2tze39/g5+19bWJcWFNQUFFSUFBTVFVWWl5ndvLi29bRz87Nzc7P0NLT1dnc4uv1e29oY15a
+V1ZXVlhYWFlaWllYV1dYWVtdX2Rt/uzf2tfU0dDPzs/Q0dLU19rc3+Hk7fl5bWlmYl5aVlNT
+UlJTVlhYV1hYWV5lc/Xr49/b19PPzs3MzM7R1dnf7nJkXFpXVVNSUlNVWFtdXl9gZGVt/vXz
+7urm393a2tnX19XU1dbY297k73hpYFxZVlJRUVJTVlteYGh67ePd2dbV1tfX2t3e4ur4dmxq
+ZmZfXFlWVFRWWFhYW11fY2316N/a19TRz87Nzc7P0NLV2d3g5/duYVtWUU9OTk1NTU5PUlZZ
+Y3D+9Ozl393b2dfW1dbZ3ePr8v54cW5qaGhoa25z/fLy7+vs7u/1+/1+eXr88+7p6Onu+n9y
+aWZkYF9dXVxeYWNmbXb97+vp6eTf3t7f3d3c3N3i5+38c25pZl9eXlxcW1taWVlbXV9kZ216
+/vHp5t/c2NTT0tHQzs/P0NLU2N7i6fdwYltUT0xMS0pJSUpMTU9SVVhcaG539uvh2tfSz87M
+ysnJysrLzM7R1dre5u18b2ljW1RPTUxKSkhISEhKS05TWV1jbP3m3dfSzsvJx8bFxcbIy87S
+2t/o9XFlXFVQTkxJSEZHSUtNUFVbYmp3+O3m39zY0c/Ozc7Ozs/Pz9DT1tjc5O54aF5YUU5N
+S0tMTE1PT1NYXF9lcvvr4t3a1tDOzczNztDT19zg5urzc2plYF1dXVxaWltbXV1dXl9eXl5f
+X2Robnf06uLd29rX1dTU1dXX2dre4+Xm6u/08/h+e3ZpYV5bWFVSUU9OTk1OUVdcZHPy6ePe
+3NnX1tXU1dbX2Nvd3uHi4N/g4uPl6+3u93JnXlpXVVRVV1hZXF5iaW90fPt9d3Vzbmdpa2xv
+e/Dp5N/c2djX1tbY2dre4+ry+ndvamhlY2JjYV5dXWBjYmJmaWlrbGtvf/Dr49/e3dzb2drb
+3eXvf3Ztamlqa2pnY19cW1pYWFlcYGl2+e3o5uPg397e3Nvb29zc2tnZ2NfY3N/l8HpsX1pY
+VFFQT05LTExMTk9TV1xjbfro3tnX09DOzc3NzMvKysnLzM7T2d/tbmFZUU1LSEdFRERFRkdJ
+TE9UW2Br9uTZ0szJx8XEw8LCw8THy87W3/BuYFpVT05MSkpKSUlKS0tMTU9RVVtga/Xg2NHN
+y8nIx8fJy83S2N/s8/txa2diXVpXVlVTVFVWWl1gaG159+rk3t7e4eTm5OHg3+Ll6u32dm5p
+ZWVjZGdpa2xpZWFgX2BkZml0eXx9e/306+fj3tvZ1tXV1dbY2dze4On0/nNkXVtWUU9OTU1O
+T1JVWmBqee/n4t7c2tnY2NfU09HP0M/Q09ba3+t+bV9aVk9NS0pKSkxOUFVcbfbn3dnX19ja
+3ODk5u3v8fp8c3d7+u/q5ODf3t/i4eHk5ufq7vl6cGpoY19dXFpaW1tcXF1cXFxdYGZrbnP6
+7+vp5ePe29fV1NPQz8/R1Njd4OPq7vN3aV5XUU5MS0pKSUlLTE9UW2Z95dzWz83LycnJycrM
+zc7R1dvi7XhqaGNeWFRRTk1MTEtNTU1OUFNYXGJv79/Z1M/MysjIyMnLzM/T19zo/2leV09M
+SkhISktOU1phbX/u5+He3NnY2dve4uPh39/g4+jp6ezv8vx6d3R2d3FwbGZiX15bWVlaXFxb
+Xl9jbHvw6+fh397d29ra19TT09TU2N3j7P9vZ1xWUU1LS0tMTlBVXW7t3tfT0M7Nzs7Q0tXY
+2+TxdmZeWVZUVVVVVldZW1xdYGZtfPnv6+fi4d/f3dra2NbU09bY2tve5e58bGNdWVNPTkxM
+S0xOUlheaHny6N7a19TSz87Oz9DR0tPT09fb5PByYVtUUE5LSUhJSUpMTlNZX2z+7OTb1dHP
+zc3NzM3Oz9LX293f4+rt8X9sYFtZVFBQUFFTU1daXWRqc3x+fXd0cG1we3/37+jj4N3b19fX
+19nb3N7j6vF7bWZgX19fYWNkZmxtfPX3+Xt5d3Bxc293fnt1dHh5/vPu7Ojp6ert7vL5fHVt
+amlrbnX89Ovm4uLs+nNqYV1bWFhZXF9jZ215+Ovg3NXPzszLy8zNz9Xc6P5pXFVQTUxLSktM
+TExOUVlfaXn26eLh3dva2djY1tTS0tLS09XY2t3i6vZ4bGRiXltbXFtbXFxeYGJfXl1dXV1e
+Xl5gZGVpbnR4++vi3djW08/Ozs3Nzc3Oz9TY2+DseWpgV1NPTUxKSklKTE5QVVldYmt38OXg
+3dzb2dnZ2NnZ2tvc3N3c29zb29zc4enx/H52bWhhXFxaWFVUVFRTU1VYW19jaXJ+8uzn4d/e
+3dva2NjY2drc3+jt7/H1+Pn29PX8eG5tbWpnZV9gYF9fYGBgYWVpamhnZmhtcn379O7m3tnU
+0c7P0dXY3OLt+3ZqZWJeW1hXVldZW2BlZmltc3v++fTs5N/d3N3e3t7c3d3e4+v+bmZeWlZS
+T09PT1BQU1heav3q3tfRzszKyMfIycnMztPa5PVuXlZPS0lIR0ZGR0lMUFhfanT67OTd2tnY
+1tXU09PU1NPV2Nvd3eHo7vT1/HJqX1pYVFNSU1NSUFFTWFtdYm3659zUz83LycnLzM7S2N7l
++mtkXlhTUU9OTUxNTU5RV1xfZnvq4d7Y1dPQzs3Mzc/X3ur7cWxrZV9fX15fYGFgY2psbGts
+bm5rbW90ffjx7Ono5+Ph5+rt7/Lu6uvp7vb6eXRsZ2JfX2BiZGhuevnv6ufn6uvvfXhxb21s
+bGxzffTw7/D4/nh0dXn78+/v7urn4uDe3dza2tvg5u39a2BcWFVSUlJSUlNVWFxhZW7+7+Xe
+3Nzd3t7e3uDe3d3c297i5enu+XpybW5vcnBvbGdqaWxvbWxubm51dXd9/f/59fv7fXRtaGNf
+YWNlZ2loaW1udf316+Tf2tfU0dHS1NbZ3N7i6PRzZ15ZWFZXV1lbX2RkZGNgYWRnbHByeHp/
+/318/vjv6OPg397b2dra2tnb3uPo6+/4fX15cW5mX11YVlRTVFZYWVtfYmRrevLn39rY2Nnb
+3N7f4OHk5eLi4eLj4d7d3uPp+HJlXVpWUU9OTk9QU1daXmJlaWxwd/zx7OXi3+Df3NnU0dDP
+zczMztDV2t7i5e93aF5aVU9NS0tKSkpLTlBTWV1hZnD/9Ozi3dnTz83NzczLzM3Nz9PX3OP3
+bV9YUU5MS0tLSktLS0xOU1hhb/Hj2M/MyMfGxsfIycrLzc7Q1t7odF9XTUlFQ0FAQEBAQUNG
+Sk9ZZ/Td08zIw7++vby8vb/CxsrS3ep3X1dQTEhFQ0FAQEBBQkVHSUxRWmnz4djQzMnHxcTD
+w8TExsnN09nf6v5tZF9bVlJQT05NTk5PUVRYWl5kcPjs5d/e3Nzc3N3d3+Dk6flyamRjYWBd
+XF1eXmJma3bz6uHa1tHPz87Oz8/Q09nk83NmXFZRT01MTE5OUFNXWFlbXWBnbv7v5NvX1NLS
+09XX1tfY2NjX19vc4Ory+XdqY15aV1VTU1RVVVVYWVpcXWFmanP+7ufi3tzZ19TT1NTT1NXX
+2d7q/WxfWVVUVVZYXF9obnR7+vn69v95e3Nwb29scG9wefzu6+Te2tjZ2Nvd3uDp7fJ6bWxp
+ZWRiYWBfXlxcWVdZW19ob/vq4Nze3t3d3+Pm6Ort7/T6fXJtaGJhX19iY2ZmaGx19Ojg2tTS
+0c/Oz8/R1Nfc5O90ZV5YU09NS0hHRkVGR0lMTlVebfDh1s/MycjHxMPDw8XHy8/V3u14Z11X
+UVFPTk1MTE1OT1NZYGh17ebh3t3b29vb3d/j5Ojt9nZrZF9cW11fX2dveH768ezo5eLd293e
+3+Lp6uz0+vb29v92b2tpaGhlXl1eXl9iaWxwef358PP07urn5+ns7/Tz7/Lz9vz///Tu7erq
+5+Pk4uDf4u16amNeW1hWVFZYWlxdX2dqcHl9+vHs6OXi4+Ti4uPk4+Ll5+rq7O7u7u3u7Orn
+5OLg4uHj5+r3eG9ramhlYl5aVVJQT01NTU5PUVRaYGv97eHd2NLPzs7Nzc3Nzc3Nzs7P1drh
+8W9fWVZSTk1MTU1NTk9RVFlfaHB1//b07erk3dnV0tDPzs/S1dvf6/t8cWhkZF9eW1hVU1FQ
+UlNXXWRvc/rv597c29rb297j5+rn6efn5+Tj393d3uDj6Ort7Ons7/L/bF1YVE5LSUdGRUZI
+TE9XX3Hn2tPOzMrJyMfGxsjKzdLY3ODn7vtvZV5ZV1NRT09PTk5NTE1PUldcX2t88ePc1tHO
+zMvJycnLzc7R2eLudWZdV1NPTUxMTEtMTU5OUVddZXD67+bd2NPOzczKysjIyMjJy83R2OP+
+ZFhQTUpIR0VFRUVGSEpLUFdebvXn3dfRzsvLysnKy8zOz9HV19vj6/l0al9bWVhWVVRTU1NU
+V1lcX2RmZ2lsb3d5+Ori3dvZ2NfU09LS0tTW2tzg8XZmXVhUU1JQTk5OT1JVV1teYmprb//v
+6d/Y0c7LycfHxsXFxsjKztfkcFpPS0dEQkJCQkFFSUlJTE5QWW7f09DX3dnRz87MyMPAwsbM
+zs3P09ne3uP6YVZOTEpISEdHR0hISUpNUlxqee3e2tbQz87NzMvMzc7T2+Xp6u7+al9eXl5b
+XFxj/W5cU1VcYl5TVmVz7vN1fOnX09Tb5drRzs7U2NrW1Nrj+fzq6fxfVU9TVk9LRUJHS0hE
+RUxWYmz93dTRz8vFwL/BxsfHyMnN09DV3+9vXFJPT01NS0lKSEM/QUVITVRl8u3j29HP1dLK
+xcLDx8rMztjj6+LY2PNZU1lZUkY/THlXTks9Q2jSy39XUWTP6W7ZzMLI2tzOyM7d39TGye1Y
+Vm12WEtHT1tZWFBOVmN8blVLU/LjbFhbdNvS3OPo2srExs3Pzc3P43h++2pWTU1gb1dFP031
+2/xTVGHh0+ReUV3fzc/yXmrczc/qX15929p8XG/Z0dl7aOzX2m1XV27Y3V9LSlv3/FZOWV5l
+Zmpx7+xs6NXP4Fjvw7nDTT9ZwLnJZFj772RJP0dSZW9paldNU1RbfO7Zy8XPdl1d7uD1Y2nZ
+0udsX2zk3+DWzMnc//bXzu1aVtvRVT8/+9pIODxzxe02LUm2s80/OUR6y8C3rq6yu3c8PErx
+y8TDw89WPjk/TG/l6NvmW0Q7ODs/SFtt4Nrb2drYz8K8vsfL18/ExtL4Xm3o61VGSlheWEtF
+S1RWT0tPX+rf7+HOyszW3M7L1OTozcvP3lzhY0fczbrKMSgpT7q+0drGwVUvKzJLyr+/wMPP
+ZUdK+8m6ubm6w9pSQj9IUWT54+BzXl1aU1N1z8jN+VVj+n5XUV7+4m1PTFZlZmFea/j2bVxi
++N/b29jNys/d6dfU3+fez9bX70lz08rBUTcuOtfHxM3XyNBEMC43U9LExcvO51ZJTmfKvby8
+wszoVUlFS1l26ufl5XhZUl7s2tfg5eb7X1ZaYnD5+XtqYV5kd/f3+fXx8XNkYGb/6N3a29je
+7v5/4OD5av3ablL43s3LZD84QWLVvL3Gxs5rQzs4PVnQx8jP8V1aVlVq4tTKyM/vWExGSExP
+Wmvr187P1tnW0dDQ0tHR1Nri73xpbH54bF9WUU9PTUtLS0xOT09RWWT+4NjV0M/QzszNzc3O
+zs7S5Ovd4+fpZE5OVVVddF9UXFtTW2RbW2FbVFdYVFx869zU0M7JxsfHys/W3O5nWVFNTEpM
+T1VecO7h2NPU0M/X3+bzenFjWVZXVldaW11iYVtbW1xeY2567+be29jRzszLycnKy9Di4Nnk
+/G5UR0lOTVh0YFdeWlBca15jbltRU09OW3P33tPRzsjHx8XHys7V6G1eUExMTE1SWmL849jS
+z8/P0dff9mhaVFBPT05PU1hcX2NkZ2ptb3B+9ure2NTT0dLTzcjGx8nP2ujt6vRmVUpAPkFF
+S1lZU1VXVmLu5uDg92hqb3nj1dHNycfGxcbIyszU4HtaS0VBQEBBQ0ZOWGb74NTNy83Oz9XZ
+3+fq6vJzbGtv//Hv7O98cmdeWVpcXmFgYGNjaHjt5N3d6m173d7p5fZYVFxbdtHP3Njb9eLT
+1NHS6mFZT0xRWV1rcmxvfPns6O71emVdVUxJSkxQWmXx2M3Kx8K/vby+wcjP2utrXldOSkVB
+QEBBQ0VFRkhJTFFYaOrb1M3Iw8G/vby9v8HKzMjN4XZYRD9BP0NRUkxOSkVLV1155O50bGFj
++eLZz8zKycnKy87P0tvqblVKR0ZGSEpNVmVpcdfLy8zNycXHz9nX1uF6YVtTT1BTVVRPTEpH
+RkVFSlVeXF172MvEwb68vL3FzcfBw8nVX0hDPj9NaPzseVJGQD09QklTaOvh3NzZ18/JxMC/
+wsnU62ZYTkhGS1Jecf715N3g4d3c3Nnc5+34cW1ucHzy7PptYVteX1xZVlVWV1lj/uXg4Nza
+1tPS1tzt+uxjVvHb19f2TEJDRVXQwL6/y3NPSERJVWfm1NHX43VlZ/zg1M/V5G1YTk1MR0VJ
+VvHb1tXTzcrLzM3T2d3k7O/+a2lmZWtqYFtbXWFiWU9OUFRVVl5vdm1qeeLZ0s/QzMzS4WNd
+2MjGytxVRkJDVc+/vcLQZUpCPj5GU+/Sztb2XFRWYunX0dHddltOSkxOTFFt4dPOz83Jx8jI
+y8/a397f5XtfVVNXWllTTktNUFJRT05SW2N06OLd19XRzcbDxcjN2OHlclNb3tTZ8lFEQD8+
+S+7Pys3hYVBHRElWftrPzc3Q2eTr39TOzc7Z9VxPTEtJSU5XYnf239bQzs7Oz9LX2tra3uh7
+X1xdX19cWFNSUU9PUVNYW19kZWv74drb5Pbk0cvL0dnd3t3e4uTSyMnUfFJNU1ZaZXJsZmFY
+TkpGRUlOVmJ0f/rq39rTzsrJyczP2OR6X19qamxnY2RnbP3m3dze2tnW2eLq6+xwX1xcXlpW
+VVVWVVFNTlNbZ2tfXXTk2trf2c7Nz8/Q0M7Q1dfd6Onk7fL6dXBkYmNeWlNQT09OTlFUVFlg
+YWBjaGBebu/b09XSz83Nz83Mzc7Pz9HX5ndjXF5gXVdPUFdcV1FPVWBhX29uY2Jmd+/tfG/p
+3t7a293f5uDa1Nfl8ffy93VrY2hvdPN1YVtfZ2xtXV54+PlvZ3r+fG126+Tl7ezs9vHg293g
+5d7Z4/N+fG9nYV9kW1teXFdSV1hcYFty5ez57dbMztvZz83S29zZ2eN5aGhoYV5eWl1aVVVT
+WF5iZWFhaW1vbW764eTr5+De5/T99und3N3a2tnW2uz3fn5zZmRdWFZYXFtXVVp2fXru6Ofh
+3Nvc3Nzd3d7sfnx9d2hmZWZteXxpZ3Z8cmxpZ2VoZ2t5eW9v/fR8cfzn3+Po3tzf6Ojf3t7l
+7u19cGpgXVhZX19aVVZcYmlw9urq6uro39za2tfX3N3e4N/p7/p8c2dfXl9bWVtcYWRhZ2tv
+bWdmeP35+X7t4+fo6ebg6vXt5uXn7Ofk3t/r4t7k6Ph5dWZfW1dXWFhbWllcXGFpbHvw4t7e
+4OTh2d3s597Y2ujxf3VsaGptaF9bWV5jX2VobPnv6uDf39za19bY2+Ph3t/sfHNwff9lXVlY
+XFtWUU9QUFBVWVhfbnz25NvY0tHQ0M/Pz9HV2tzi6fH5e3h2Zm95bGleXFtcXWBhWlxeaX38
+eXj47fJ5bmdjZWpufHp+6tzY2drY2+Dn7/jz9Xd0a2dobm9kX2py9P1qevDv9fzx9fn2/ezp
++f7x6/Hv6vPz7+7ybmVjY2FeXWBpfntsbXjv8nBqbHv2fvHx6uHn4+Dm7Ozj4+nl7/R6cHNs
+a2Vqb2lsdH7/bGNjam5sXl5qbWZkav78evTo3dva1tXPzs7Oz9Pc5O94cVxWWFZWUU1OUlRU
+U1ZZXF9fbW5x9urk39zV19fU2NHN19/b1tfc8WzZzWRFR2zT6VJLT2JcTEpMTlfv2dzt/t/O
+zM/b29LW4uzw5ej9eezrX1BX+/5XUGjlYFFmd19RVPrZ6Wflz9XrbOjQ2vn67Prw1Nl3XVZe
+d+/teGFcdO/j2uxrfP10bX5rXGVjZe3a4WVVXePP0/dRUXfZ5WdWTFnUytteTl/MxNZpYe3Q
+ytDka19vZ11bYmZPSlbx32hOT2Hj1uNgWWfYzubl0dhlX9DJ2HtVXN/Z2/JcUFTtzdJ1VVBp
+1NhlRkRdfWFo6HtVXN3M0fp43dxu/crMWkpbz8xfRU1w1tl2bGzq1u5n8+fvXWTZzdVlT3bL
+0mpWXubW3lpJU2l+e2NPSE/x22ZJRW/L1GNa5s7N0dHOzM7V0c3Mz+t0/O3qck5HSE5aT0hJ
+Sk1WX2VjcHvt0s7d6NfO0NXZ1dnk3dbc+Gdu79zfXUxOZPNkTUVNbH9ZT1/z4t/f09Db3trS
+0dbf9ObU1ORdUF7e2lVARFrxX05WemtMSWjR1fLqz8nP19LQ2+jZ0N1dU2T1bVlWYe3b5WBU
+X9/YbE5VfG1MQklYXFp11s7S08zGxcnMz9fe2tf+T0pQUUtKUWpyTkNayMlaRlX4WEZV0MHC
+w7u6ye3q5lhDRVluWll/397VycrZ6OLpV0E8PDw3NjxIXfvaxry4u76+u77L3NzT2mNSXO7o
+bWNrcGdWS0hRbVhAPEzwbVJa39NwdcK97kt+yuRCP3PCvszcyMDYTUFCSEtUa33dx8HGycfC
+x9Loal5NPTY3Oz07PU1+4urlzsPCx8e/vL7JzsjEytz54dfoVEhPYlM/Oz5GQzw/S2V0Rjvp
+ucVJTc3F6E1Vxa2uw8e3uuVDPUFNYl5X3MTI2uHd3Nx2UVRlTjw4Oz4+PUBW2tXy5snAxszJ
+vrzE3uLQzuVsf9fQ5F1ac/laR0JITkhAQVB1/FhJaL2/V0vUx/xBRNSztM7MtLdyPD1KUEhF
+W8/F1OnUy9Pxa2piZFhFPUBDPz9CT+vYe+rFvsbNxb29x9PNyddtbOLdeFtecWFPTU9ORD5B
+R0VDSFvf7UlHx7reU9XI1F1S2ba0xcKxtudHSVNNREZwztV+69LdXFJaZVVERE1GOjpARkVL
+atDIzcy/vMHFvbu/ycvJz/9efudkUlZcT0I/SExFPj9GSkhJV+3b18zd0re6zMXBz+p+9dG/
+xc2/wHBHQ0A5NTpFTkxT8djc69vNz9zm4dxyTExcXE5T48zK0s7Cv8rTzcvW721jWExLUVJN
+T1peVkxLWWNPTVpiXVtz39TNy8vJ4M66xtrN3GVWVlzs1+HSx95MR0c+OjtHWl1j1cbHy8jB
+wMfNz9pvWk9DPkFERUpc39bc08vM2OTe3ndeaP1nX3Pm39/f4ufsaF1xcVdVXVRWZ2do+9/W
+12JQxMNk4uRZUU5OZtLPzb6/3m5pTkNAR1dfXefKzs3HxcTHys3bYExJPzY2Oz08QVfa1dTJ
+wMHKzs7Q2OTh197l2tDX29rV3XtkVlFcUUZKTEpJSk5aW2nfb0zSve3ZxPJa7O960MbGu7rI
+199bSEE/SUxKVvF8fdrV1tbR1uNiVE4/Oj0/QERR+s7Jyb+7vcC/w8nN2OZ+al9eWVdgXlpa
+WVBMS01UT0tLUFtbW/3Y19jU68a63NG81lV67HDv19LExNbueU9DPz4/RERLZGNq2M/OyMbG
+xszZ61tHQkVEQUxe587NyMHDyMnM1d/9XVNOSkpMTldaXnZvXVlk8PlobfTm/HDq2MzK22Xh
+t81RxtVLUFNPWv3pz8XN2+hqTkhBREpJSV30edzOy8bEw8PL3W5ZST45Oj0+RlbyzcbDvry9
+wMXK1OluW1lSUlleZXdtfPFvXltdX1ROTk1PT1Be8tbkT8y7683Fb/rjX3nW2c7AxsvO5Wtc
+TEdMSkZNV1Zo5dvOycjFydT0V0tEOzY5PD5FV/DPxsK+vL7Bw8jP1en9/2Vda3n97+zt8m1X
+VVlPR0ZGSEZIU2b2X3u8w9q+xOHY7m3Z3v/PxdDY2ehsVUpNTURETlFPX+vaz83IwcbO0t9c
+TEZCPTs9Q0xSaNfHv76+u7u/x83Z62RWUUxOT1BXX2dsaFtbXVZUT1FRTk9SXGh+5NjNycfH
+xcTIzNDU2Nvj5d/f4/V1Z15XUVBOTk5NTU5PT09RUlVaXmd77+fc1tLPzczMzc/Q1d3f3+z0
+8/T2/nR6eXV4eW9raF1bWVdVVlZXWVlaXF1cYGhve/jt5+fm4N3b19PQzs3LyszO09fd6H9p
+X1lWU1JQT09RVFVVWFxbXmBiZmlrde/m3tnX2NnZ3d7i6+rq7vHw7+71/Pv5fXh2a2lpaGxp
+aG90dXj08Pf5//t+cnF2c3B0enRx//1++PDt7e3r7fT1/3Jxb3N7/vnx7Obj3tzc3uHi63xv
+a2RfYE9Xx2g8Q2jZ6Vpc4e1LR/7tV0jauL53b8/KX0r0wsle/sTCckhWzM5RS+/UYkxc0ddL
+Q1VjSj9M6t9p/MvH3Gjnz+NXWt7W/V/izdD6/tfN2Ornz85qWl5mWEtNV1lPTVVaWlZVXWdf
+Yvn8bvfe2NXT0crIzMzKycvQ0dPU5Hd3bF5VT05LR0ZHRkVHSk5QVl5g9d7c2dn02LvA2dXH
+z3VQX8u/yMW6u85XSkg+NDY+SEVJZ9TT4tjIx9Pu6upbSEdMUVddZvzRysvIxMTByNnn6m9e
+VFlgW1NUWl9aU1tgZGFST1xYTE/i0fD22tDL0su7yF3DusfOTDpETEBKyrzEx81kQzoyMjg9
+UNjKyMLFyc/e3trW1dLR1+b+bV5ST11wWFXtz8zNz9PO3VpMUE5FQktl8XBp5uBvVlVcY11b
+cdzZ4d/Szs3Lzc/Y1dfXzcnvQWy+z09DOzxBPDpzuLrJy9hXPjM1Rn3Ov7m4usTeZV1fZvHc
+0dLgYExDPj0/R0hKa8/Kzc3Hw8x6at3Y4e7kz8nQ39/rW01OTU1WZGZfWlVWUkxJVefcdnLl
+18zMyNNVy7W51U8+Q15PTMavtcl6PzUzMTRF2L+7wdd3WEtHTm7Ov73Cy91gTEE9QlVtXmvQ
+xMXX/drI0lhQ9czM+Vp91t1kVFZna1NMVXHs6GtXW1pWWF1Ya83Ez39y2cfC0VBB4LzHWkJC
+T3VPR8+0uM9dRD5BPj5Tzr68wtX5bGFbZuPNxcbR+1dKQj49RE9OTGDb2eN0/NDN81vuz8nM
+193W0dfZ2drb4vxpZ254d15VTkdFTFRWWv3V1nFOV+LRz+pOS8y7xHlIS3fZX1PHtbfGcElC
+RkFEV93HxdJoUE1MTVRo283R43leUElJS01X/tzRy8rMy8fL1trc0snJ31hg6eBvT05UV0tC
+Rk9VUE1PVFZVVV1x6M7Exc/Z0MnAws7Wcu++us9EPVDtbT493r3FVz08QEE6PU3ex8fU5uLd
+5u3dzcG8v8jX73hZR0FMX3d3Znna3WJUX+fedmV55upsYnzV1mZTX+/uZFls2NxfU2Pu/F1b
+6dPrW3jPyM/sa1bsxMxbRlTs6VRE9Lm75lL16FA8OU3Yz+r829LnUUhV9dzb1s/R33ReSkNO
+Y2xgbtzJyOJv4tDO2OLj1tTf4N/pfm5kYFtPS1NiW1FUYW5aSktm7m5SVPDUzs7I117NusNX
+S+bI0EhGw6+9X2nP5UAyOl36T0/o0/tHRlz8XVvaxsv4YufhUUBN7/pZXd3GyenmxsPU5dnN
+1H142tPuYWlrXlJNT1VOTFRbWVNYX2BfavPw7+fdzcPD02fbvslYTWvb90dG38LQYufQc0Q8
+RExJTXLV2d/e0tLr7NTJy9HZ1NViS05YSkVMVF9eWGjc2+nZz9Lf9OXf5/7h1+Rw8t95XFps
+eFtZZG1hX279dWpha+fm6tHHz/zRvs9haed7T0ZO9u9eY+T1VUhFR0RASVxfadvOzs3KxsPF
+yMPCzd/j/F5YV1ZZXFtfbWpkZntzZ2RmX1tWVFdYXGRvc3b/9fjt6OHf3tvW1NLS3N/e3+Di
+5eXm7mnf31xbblZOT05OWWJgbujqfP5ybmReYnx3eurd2tPOzs/My8/c5/NlV05MS0lKSkxP
+W19v6eDc1M/T1dTV1tnd3uDl7uzq7vp6dm1nX1taWVZUVlZXWl1fX3L5+uro5uLc4t7W3N7f
+4dvX2dzW197h5u979vtuZF5cWFZTT09QUVJTVVhcX2Jr/u/m39/d2tnW1dXTz8/Pz8/S1drg
+5/pqYl9cWVhWU1BPUFBRU1RVWV1ia3d+++/p4t7c2dfU1NTV1Nba297i6/Ty7+7v8f5yb25n
+YF9eXl5eYGNiZWVjYV9fX2BhZWhqbHZ/9PHt6ujk4d/f3t3c3N3h5ufq7/f5+ff4+fr18fX7
+9/L5fXhxbGlkXVtbW1lZW15lbXn37+3q5uXl5ebq6+fk4d7e3uHo7/x/f3x5cm5saGJfW1la
+WlpbXmFqc3n78Ork4OHh397c2djX2dzd4et6amdkYF9eXl5iYl9fYWdqamlrbGxtc/338+7p
+49/c2NbV1NPW29/i4+v4eHBuZGBeXVxYVFNUVFVXWFldYGZtdvrs6Ojm5N7a2NfY2dvd4eTl
+5+zy+Pr+fn5++/t1cG9zcHF2cnBuaWVhZGVla3N5fPz+ff7+//76+//78fDv6+rs6uv0/3dz
+cG9ucG9xdHZzcm5vdHV9fvru6+vs7Ozt8fb8/X54eHt++vLw8/X18u7r6Ofl5eXq8P9xaGBb
+VlZXWVlaXF9obXN98+rp5uLf397d3d7f3+Hk5+rq6efr7Ozy+3RqZGBdWlpbW1tbW1xeYGRu
+/e7j3tvZ2NjY2Nja3OPs/W1kX15dWltcXl9fZXBzfPjv6eXk4eDk5unt9ffz9fjz7+/y7/D2
++v18dm9tamhqaGVjY2RiZWloa3R5efjx8+/w8e3o4t3b2djZ2t7k6/t1b2tkX1xaWlhYWlpd
+Xl5gZm959ezj3tzZ2NfY2Nna3d/j6/d5bmlmY19dW1xcXV5fY2VpdHr99u7n4uHg4uDh4+Xn
+6u74eXFvbGha7ORKR8/FP0nLz8zUVFHCwz5M2WxWds7nT/prWv5tVfHJ/FB+8W1179/f0N/v
+3eZ67/nu7u/tfW5qXlhcW1xiZWRnbnVvcvPz7OTp7O3r6Ojp+ezu7ep6+n7q4+zf3eLl4O31
+6W5vZFpjWFhcVFpVT1taWV5p+e/y6t3X3t3c3NPb2tPa2drl3tzw6eZxZWZrYl1cXWJlWlNb
+XFdbWWXr5vv9+2Z3bHfh7ePX1Nfk4dbV49zb4eFv/v9uX1ZcYltQU1hXVU9WWllydW7p1tXb
+3dnQ1drZ2tng8+nsfvvo7ejr+fhvcG1tZWNoZVpTVVlVV15kaGNs8+716NjV7m3v3ulvb+jg
+8P5/7+Xc83Ps3+l469ze8PzvdFpf4tfoW1n0eVVOXfBnVmLyaVtcXXLxd2Tr0+hh68zTanrS
+z+Pr19T3bN/Z73Dh0ddrWWj5WUtPYGJSTVJSTUxQWFxvb2dg+NnX2NHLycrP1NHIw9Nz0L7S
+TUxTZd1fSVbkTTpY30lA4cpgWu/f5VtPXOVxSFPY1mv0y8bW7eLazs/b9N7M0G9LTHR3UUlc
+3ud49t/sa3r8enHp1ehkbnlr6NTecmhQRU9hbu1PS8m6bTlH1+5DPui0ttrUtLhWOETyXEJc
+wL7dY+rhVUVS1M3rb9/mW0tFR01KRUtm+nZ86dTJzNbYzc3f69bN1uzbzdhwYvDcaE1Na9xu
+SEthbkpFV059vMdbTufhSD9I2bO1ysi90jwwO0lPXN7Avs5nXG9hV+rKwsbJzvxQRUJIRUBH
+bOVx/dnOzM7V2NbS09XX1tjecV1hc11QUFxQSEtbdmBNT2BrX2hYVcO30U3mxvNCReWyrsbY
+vcZBLzhKS0tszMbdUU1ZWk9ezsHEy8/S3llCQVR+XVNtzsneWnjKy21e3c3fX2bj2mpTZ3lX
+TFFpYE1OZuHmXVZrcGBo6+lb07i9a1Tc2Es9SsWvvezOwmY2MkJZaPTOwMd1T1ttX2XbxsHL
+2/NkTUJDRENPbnZcXuja6Gl11cva/93KzeDu1c3ab2B99mFaZXtsUlBfbWBOT2R/Xlfg5WPN
+w9xcZF5GR17XtrLDzMf5OzM/V+3Ty8DC3FNLV19k5M7Fyt1lT0lDPz9FT3XyZmjp1d9w3svG
+0fPczM7sddTN4Wxs72lPT1/yZlFWXF9dT0xX+vpp9c7Oa93Fx9phUVFk7eW/tcDZ91dBOTxH
+Ztvc1NZ6U0xSY+vSycHDz+1aSkRHSkxPcNva6frg1d3l2cvK3PXn191kXnVvXVJWXVxYX/36
+ZWFqbF9bfdfcYWPez9jj7lfxwsn0V1FOUltbzri91vRoTkA+SH3P0dHS71tLTGHi18/Jx9dl
+SkdMRkBM8t1wWmrb2Gtb1L/D2ePMw85tfc3K7FRW8etVSVRtWUpJT09KTlpbT01e18/68N3Z
+wL3NYmzU0eFp17ayw25g8lQ9Oknn2mlXVFdNREZU7NrW0NHkXk9TV1JPXN/T5Gly2s/W39fJ
+x9Lm7NjR3Xp64u5dU1ZbWFNZbm1TTVRiaFtXd9jabF7mz+vezMvP8ltNXnV4z8LAyN9gST8+
+Q1Zy3NPd+WZZX27gz8bCxcvfYlVOTktITldoYVNRXftzbfXd0tje2NPS2tzY09jm5ubq/Gdd
+XmZlWVNSWlxUT09n4vhiavFk6cnIz/hfZPJ+X9O8ub/ZZlFGPz5Ja9vX72VfWFhUXufOyc/W
+6G1dTkhITVv88Whm7N7e4N/UzMrMzM3S2t/f2+HvcGhoXVhQTk5NS0pMTkxIS1hdYWzq2dbo
+ctS/u8PaedjK0ObVvbe93FROS0I7Pk1uclBKSk1OTFXyz8rJzNPZ6XRgVFRf8en39ufX2e18
+c/Ls8P3p3+jvcnzw9Hx2+e/6bWNkZGRaVVthXlhaZ/30e/3e09Ta3t3c19jb2uf/aWVo++Lc
+1dTZ6GFSTU1RW2hqdm9lX2BlbPvq4uPqfWpjXFhWVlheZ3b2597Y0tDQzszNzs/P1Nfd6/l3
+aWRjX1tXUU1LSUpKS0xNUFZcZnzp3NTQz83MzM3MzM3O0tfd6PL9eXdycXFoX1pXVFJSVFda
+Xl5dXWRra252fXl6ffz27ebl5ebl5eTj39/f39/e3t3b29zf5uju+nVrZVxaWFZWVldYWlpY
+WFdZXmh69+Td29nY2tza2drc3dvb3uPo7e/4fXVvaGZoa2xrbW5taGJiZ2dmZmlra2tucGxp
+ZmdscHr06erq7PDt7evo4+Hi5OTk4uLk49/g4ePq8n1qYF5bWVhZWVtbWllaXmJpdu7m39vX
+1dTU1dfc3uLt9X52bWZhYGFjaGxtbGtpaWhoam1ucXVzcHB2fPft6eXm5+Xm6ujq6ujo7fZ4
+amZnaGpscHNxa2Zpbm9y/PLr4d/f3t3c3+Xq6/H5eXVvZF5bWVhXV1hZW11kb3v26uHe2tjX
+1tfY29/m7/Z+dnJweHZ0bWxraWloaWttbm1rbG1ve379/Pjy9X95e3h2eH1//f/89e/u7O7v
+7/Hw+/z29O7r6urr7ezv8/n9d29qZl9cXF9jY2htb3Bz/PPq4d/e3dza293f6PP8c2dgXlpY
+V1ZYWFhbXl9kam7+8+jd2dfV1NPU19ve3+Lj5ufp6u72eGxkX1xbXV5eYWNiZGdpbHNwb29u
+bW1ub3B3+/Dw8O7t7O7x8e/w8e7q6ujn5N/g3t3d3eHl6PN2b2pkXl1dXFtaV1haW15kbv7v
+6efn6ert7/Du6ePg4uHi5+74+/x5cG5ta2trbXJ88+7r7u/r6Ojr7vp2a2FeXl5eYWdtd/71
+8u3p6eXn6+3s7O709vr9/Hp4dG5qZmNjYWNnbnf57+vm4eHi5Obq8/n7fXx1c3V1eHV1en35
+7+rn5ufq7O/9eW9qZmBeXl9fX19gX19gZWhtefbr5uPf3dvb3Nva29vb297i5e9+cm9vamZg
+XVtbW1xdW11eXmFlbHb/9Ozi3tzd3dzd3t/f4+z4fm9pZWFgX19eXmBkZmlucHd/9+7n4N3b
+2tjV0tHT1Nbb4fB5al9bVVJPTk5PT1FUV1teZmxvevXs5uLe3tza2tnZ2tvb29ze3+Di5Ofu
++3lwbGhkX11cW1xcW19jZ2lqcv718e/s6uvu7/V8cW1ubGtrcf/17ezp4+Dd2tjV1NXZ3+ty
+Zl5ZWFVVVlZWWFlZWlxfYmZrdHnw6ePc2tfU0tDP0NHR09XY3ODq9XNmYF5cXVpXVlRSUlNT
+VVdYWllbYWVrd/jr4+Dd2djV09HPz9DT1dfa3uPt+nZoYV5bVlVVVllaXmhueP728/P6fnRt
+bW1tcHVzdnd1cW9uc3v++vTu6ePh39zc29va2dze4OHf4+bo7Pn+eW9nX1tXVFJQTk5OTk9P
+UFNYXmd47eLc1tLPzs7MzMzNztDV2dvf6O7v/W9uaWViX19cWFZTUE9OT09QUlVZXmRsfPDn
+39nU0c/Ozs7Ozs7Pz9PX2tzj7P5rXVZQTUtKSklKSktMTlNZX2l57ODZ1NDNzMrKysrLy8zO
+0NXZ3efzb19ZVE9MS0tKSkpKS01PU1heanry49zY1dPQzszLysrMzc/R1dvh7nlmW1RST0xK
+SUlKS05RWmV17uTd2tjW19jY69m/dUbb4s7MVenVePhZ8dVWWODS3UZkx09VPk7LXO9b+sxI
+VFVa+UXn2uvOadDNftDezszp0OLQ3FZ3ZGtmT2x63W153GzpXE9gSE9NTktPa23i7H7Qz+Lv
+ysrM1+/KydFc6dP8aVN32XJXV/9cU05KdFlHUWFtT1Pk4tJ6Zs/K2Vbeys7Y6tLF1FVozPJg
+U1zSd0tP2mZPUlfnbk9f7/hX/Nn9fmfhz/NV98rZYm3c0edcfs3QbWPb025OWfd0TkhVcFtK
+Tn7naF372dzr5NbR1t7Vy8/c2s7T5n7/3uFsW2d+XVNWVE9JRkpLRkNOYmRZXeXU2t/RycfK
+zsrFytPQy83X6eXe8WNbY2JUTFBXUEpJUFZOSlBfYVhZde9+debV1tzXysXN2NDIyNbe19Le
+b19hXlFPT09PTlNaWFNUbPxtanft5PNy9N/e+3De0Nnz9drY3+/r1M/Z7N3Q32didW9aWFlR
+TUxOWl1WWu3b7nXq19PfcGZzZlFLTVVcYnXdzcfEwcHBv8PJ0t/sdlhKPj9TTUBLTUlISUpU
++Glk2cfO4trUz9Df6N3U3/Tt3NHP0M7IxcrU3N9wTT48Pj05NjpBSUpR7s3Ewr66tre8wsXJ
+1HdUVFZOSEBCSEpKS09VXF9ZVHjT1NLQ0s3Hyc3GwcHBwsrY42lWS0M/Pj48PDw/RUpSX/fT
+y83S1c/M0vln/ujubG7j0srKx7+6uLq9wMTL3l1PSUQ/OzY1Nzk4OTs/SlFNVdvMzM3NzcfC
+xsa/vr6+wMfKzNbncFpST0pGRUZKTVBVadrb6OHm4N9tVVRVTUdERU1ZaPrYzMS+vby7ury/
+x83P23NUSkVFQz8/QENGR0VOY2RkXFVUY2tk/uXbz8nKy8nJycrO1dre5O57amx87uvf1NTX
+4H14clxNSUZDQT8+QUlSX/bbzcXAvr2+vr/Ey9Dd/1tNRkVFQkNESUtLVmP83vBpa+bZ19vp
+4tLO1uD1/Oji8G1lZ250bGl169vU0dHNyc7Z8XJ2bldKR0dLS0lKU2nm2c/Mx8LCxcnO2el2
+X1dMRD9AQkJAQUVLT2D72c/T1dHKxsPDxsTBwsfN193e6mxZT0pJSEZGR0tTXmh56uHj72xj
+a2tiWVRVW2JocevXzcjFwr+/v8HEydDd+2FWTEQ/PT4/QENHSk5h8N3Z6nrr19LU29vTzc7W
+4v94/m9eWVVXXGVqfuvd087Kx8bJzNXf9GhYTkhEQ0JBQ0hPXXng1cvGxMPExMfM1t/sdlxN
+REFDRkhJS01d6dbNz9zbzsjJzdXUzczT6lxTVVNNSERER0pLTFJg7NTMx8PCwsXM0tjgd15Q
+S0hIR0dMVmvq29HKxsLBwsXIzdPd+15PSEVGR0VFSk5PV2fl1dx4avPe3OdvcuXb3ehxavrs
++2lcXWlvb3F68t3Ry8fFyMrO1t31YlRLRUJAPz9ARlFj7dnNxr+9vb2+vsHL2+1yWkk/Pj9E
+RERHTV3p2tbZ5e7p7Pn9bW/s6PtsX15tc25taWZscXJv/uvZzsrEwMLHy8/T2vRdT0lHREA/
+Q0pWZnvj08zHxcfJzNHd9WFVTktJSElLTVFbdOPa19XPzc3O0t3pe2ZdWldTUlBUWVhaXGT2
+4OT3e/7x9nfx2MzHxMPCwMDGzt38a11PRj89PDw9P0VOXuvWzcnGxMLCxMjLzdTZ6HNfWVVP
+SkdFRERERERFR0tTWmjv3dTPy8bBv7++v7/AwsXIzdjlcFlOSUdFQ0JCRUhOUVVZX2x19uvh
+3t7d2NTT1NPQz9LV1trc5PH7b2VdVVFPTk1MTE1OUFJXXmp1/+7m39nW1tXT0dLS09LP0NPV
+19rf6vP7emxmXllUT01LTE1PUVVaYW356t7Z1dHP0NTV1tjc5OrzfW1lYWBfXVtZW2FhYWVm
+amtma292fXVuePr5/X769Onk4N3c29rb29zf4uTl5+zu9/50b29tbWtoYV1cWVlZWlxfXmFs
+ffbv5t7a29zc3N/p6Ofu83htaGRfXV9fX2NmbXJ98PPz7Ons9e3o5urv7+zm5+rp5+jv/Xr8
+fv10aGtubW1pa3J4fmtkY2JfWlldaG1sbuvf4OPf2dXX2dze3d/s8/V3bWxoYF9dXl9iZF9l
+duv4b3vu5+39/fbo3O1ibdrV3+/j0M/uVVJn+GBUXO/kd15daHlvXVdXXF1YWV9nZ2Fm++Tb
+2tfRzs3Pz87P1NnZ2t3i5OXh4ubu9vNsXVpaU0xLTE5MSkpLT1NUV15xfXf43NXc5dvR1N3e
+0szLz9LQ0dvw8ebwaWFuf2thYm50YFZPTl35bFZNUGf/Ylp608zV7OHQ0/Vgaezg+G/r2NPb
+5N3S0dzq93BaSUFFSUU9PEp843vtyby+zNnLwsv7X+fO21tRb976XWHt429RS05PSENGSk5U
+WW16aNi/vcPKzcbM/1zhwr3EzsjH7Uk+PkVEP0FOcOz679rO0N3s73hYRz89PT0/R1d938y/
+vcDCvrm5v8nBvMdwVmnmZUlHWWJKPT5HSD89SFhQSE//8WJo1MDE1NjFvsfj3svL3ejd3VVS
+w7nLTEDuzE84Pcy7zFhY2eg+MDI/SkRAVM3Ez9vOxMLO183Fzepu/vJoT1Jv339g9dPNztbO
+yMbS9XBlWlFPUFRSUVBPTU5OS0lHS1FPUF3r2Nfh483GyMfFxMLBxM7Z19fN0O3wfU5r0XxI
+PT1Y+UE2Rs3F6EtM7Hg9Nj9b6OLey7y7w8zMyMXN1c/Jz+ZjXFNGPTxJXU1BSt3GzXFf1cng
+TljQzPxY3L7CYkhxx9RKQnTL60E/aNFnR0/j0XtOW9TIzd3aycv8V1Rt1HBLW8zF2EQ/3brJ
+RztMzchPPWC9u85dUPh7Qjc7TfbkfO7SzuBpYfzbzcnJycrR6WVWUU1V9NPmYd/Hws14YtvT
+Zkta9nNSTFBjVEdESlNXVU9ZbGtv8d3PzNLd183LzcvEv8DJ0tjd72VdUk1NUE9PTEpKR0JF
+UGf7YFNn1dfp7dPCvMTZ39nvYFld7tnd7mxfYXFlWl7t1tfkfO7mcFNLU2v3WVXfyMjjXW/S
+111Oec3UWklR3t5VS175alVObdbhYnPT1HlWXtbK2XfcysbRdXnY1e9qePxoWVNTXGhaXVhN
+S1FPZ978XFdMXc/QePHKv73H6fjT8E1KV+nP2fTh099oT1L6099mafL8VUE/TGFRRFfPyN1k
+7sa+zmbgwcH8Ve3Ky2xLVuJsSEVPXVhMTFZbTUlW8Nztdu/VzNLXzcG+y+TgzszeaWN43t/q
+6W9aVEtIVeFkSEtMS0hAQ1zf+n/Pz+b6893b4tnGvsPKy83M2nx/4dzj9WZbV0tEQ0VEPT5L
+c+1obN7P1/l618XL29fNytV4ZtrM4Vxw4epcTFXrdklFW25USkpl4GNO+cvO3+zk3+nt2cvN
+1tXSztltaNbN419dZt3ZZE5RTURCSEpRY/f2aVZYcmRNXtHGz+7cxsXmYtrHyt3r18nRY2Hj
+4FM+P1LtYkpV2c9qTV3Vy95o7NLeX2bo6fhcWGtoVFNfXFRcX2VrZ23l0s/Q0tXPysrQ2dbT
+1N9ibOltUklMU2NmTEtQTk1eYXFxWOvCyFFDfMbF4V3Mu8dRUdLH4U9U2tdSSFnj311PZexZ
+RlHt2+hv5tHXX05Xbu3m69zNzM/a7u/b2flr/t3Tek5UaVJDQkZOUUlLa/JaVnvc1dTSycPG
+zM/LxMLExsvQ1e5TTlhaWVJLSEk/OUFUT1VVVfblTUnhz9bM0c7GzvPpy83UzMvOfk9uz9xO
+S37P2kk+Vdl6SUpv1tpv9NLW7N/R03FVdNLXV0pk2/ZKQFDc4ktEYdPcXFbxzdBkXd3WdF3q
+z9D/YuDP22d2z8fSee/Qz3VV/s7UX0xRdvJUSFvdeUc/TejjS0Bf1mpHTfHbam3KvshiY8S4
+xl/qu7bLTUrv01Y+RXzvST5EWl5LSl/Z3mJr187W39fPzuBiat3a6/X+8OHqXlhfaHbx+fl/
+Y1zr1/xbWmTv5Gpd/N73cmhcZuLecmBo6dnyV1vl1N9pWmrsd2n439rf4OHt/uzc4vfq29ba
+eF/15FtPYXRfaVlOTlVOUFhUW+7fe2Ro+NrP3uzYzs7S39zMydXr39rhfmz76nRVUFJNSklL
+TEpMXu9vUFXlzM3e3MjCy9fPw7/L4tvR2mNVVU9NTEpHREFARUpJTGTv+vbm4d3SzcnHysnE
+w8jNz8vJzd/o7mJST1FRT01JSEpKS0dFTFZaVVJf/vh99dzLxcjLysfFx8vKxcLI1etpaHFc
+T09SUE1HQUNKSUhNVVlYVVhjfd7OyczSz8fHz9jPxsbO53Ty5/hx/vZvX1ZQUE9OTE9RUVFP
+T1hgY3Ls3NXU1NHLyMnP29zV0uBuYf3Y3F1PW+z3WExU6uhaT1JaW05KUWRqZmNqftzb2NXT
+0M3N1dbT1dfY29nY3eTi7W1aV1dWUExLS0pJTVRaU1Vw1tPr/drKyNbm1cjK4Xnu3O1fXHvi
+9FtVY29fWWF5+2JYX+/tambu2dno8eLY2+jz7ODl/WplZGZhW1haXWFeWVxpdHZ+69/c2trW
+1dne5uzp6e92aF9cW1teY2doamltfPz++eje3d/r4djW19zZ1trlfHVuZ11ZW1taVlNVWFlW
+VldaWltkdu7m4d7d3N3e3tzb29vY1tbV1tfX2Nvc4+97bGJbVVBOTU1NT1NWWV1haXj57ezp
+5eLf2tfX19fX2drc3+Xu9/x9b2pmXllTUE9SVVZZWV1oePvw5t7b2tnX1dXV1tTV1dfZ3OHq
+8XVjWlRRT09PUFJVVlZbX2Zrb/rr39vW1NLS09TW19vf6Pd1a2JcWllZWlxfZWpzfPz19n15
+d3v/+vn3/np8//X28u7y6uXg3t7e4eXo6Ovv/Hp4cWxgX2BgYGZrbW1qZ2NfX15fY2Ztd/3v
+6ufi39/e3eHi4ePk5OXl5+vv8ft2bWpqZ2ZlZ2dobXF7/Pfs6unt/3FqZWBlaWptbnBubHV6
+e/98dXN0d/rv6eTf3+Df397e3Nzb2t7h6e38bWtsZ2RhX11bWFVUVFJRU1NVWFteZnX05dzX
+1NDNzc3NzMzNztHW2+LsfGtlY2FcWVtbWVlXVFJTUlJQUFFQUFJTVltkdevc08zIxcPCwsHB
+w8XHzM/Y4PVlV05KRkRDQkNDQkJERklNU11u797WzsrIxsTDw8LDxMXIztLP22hUTktMTEhG
+SEZBP0FDSVVm9t/Z2NPNycbEw8XIzNHX29zc3+t1YFhPTEtJSUtMTlBRVlpiduvb19PQz87M
+y8vMztTc8GxhXFdUVFNSUlVXW11gZWRgXlxeaHzx6N3Z1NHPzs/Oz9DT2N7l6e32dWZdWlhS
+Tk1NTk9RU1ZbX2FpdPno39vW0dDQzszNzc7Q1Nfa2+LvdmZeV1FNS0pJSElKSktLTlNYX2l9
+6d/Y08/MycbFxMTExMTGyczS2eD1bl5XUk1KR0VEQ0NCP0ZKQ0dFTmFfcvTXz9TQzMfEw8C+
+vL7AxcvQ2+1xbGBdWFRTUE5MTExMTUxMTExNTU9UWl9ja/rj2tTPy8jFxcbFxsnMztHT19ze
+5vJwX1ZQTEdCQEBBQUJESEpNU1tn9+DXz8zIxMHAv8DBxMbIys3P1Nvk/2JYT0pGQz8/Pj4+
+P0BBRUhOWm7j1c3Hw8C/vr6+vr6+wMPHzNXlblpPSkVCQEA/P0BCQ0ZJS01TWmr349fOysfF
+xcTFxcTFx8nM0Nbd6ntlWlNNSUZGRkdHSEpMTk9SWmNz8eTZ0c/My8rJycrLy8zO09ne6P1r
+YFpYVFJQTk9PT1BSVlhaXmBmbXj27u7s5t7c3Nzb2NfY2Nrb3uPq7/f//Hp2cG5sbGpmZ2Vk
+Zmlqa2tqZ2ZmZmpsePTw7enm4eDf4OTm7/l5cm5naWxzfX/+9/Hs6ebk4+Pk5uzv9P11amBf
+XltYWlZRbFpfZFr+829y7+HX3+rc19nb6OHb4erp5N7g8+fk4+b8ePl8cWpocG9iXVxcXllV
+Wl1dWldYXV5dXmRv9+3o39va293Z09HP0NLT1tjZ2tzh9XlvaGJeXFtbWllYWFlZWVpbWllZ
+W15lannw5t3Z2NXQz87Ozs/S1dnc4/hvZFxVT05NTExMTk9PUldcZXP56t7X0c/MysnJycrL
+zc/U2N3h7nprY11WT0tJR0VEREVHSUtOU1tkb/Xj2NDMyMXCwMDAwcXIy87U3el7Y1lRTElH
+RUNDQ0NERUhLT1ZfcO7c1c7LycbDwsHBw8THys7T2OP3alxVT0tIR0ZFRUVGSElLTVBWXWZ5
+6t7Yz83LycjIyMnLzc/S1tnc4+38b2NbVU9MS0pKSkpLTU9TWV5o/unc1c/MycjGxsXFx8rM
+z9jkeWZcU09LSEZFQ0JCQkJER0tSW2rv3tfOy8nGxMLDw8TGyMnKzdDX3ux9ZltXUk9MS0lI
+R0hJS0xNT1NZXWRrc/fq393Y0s/OzMrJycnKy83P09jd4en7a11XUU5MSkdGRkdISEpNUVhh
+du7g2tTPzs3My8vJx8jJyszP1NzpcV9ZVFFPT09PTk1MTU1OU1lfZnJ/9Onh3d3c2tjX2Nna
+293d3eDn7fD8e3Z0dnZ/8+/t6+3u7+/v8vh5bWxoYl5bWFVTVFRVVVdaWltbXmBp/Orh2dPO
+y8nHxcXHyczP197pfGpfW1dRTkxJSEdISkxOU1heZ2786uDb1c/NzMvMy8zNztHW3ODm8Hxs
+X1lUT09OTU1MS0tLTExOUFddZ3bv39jPy8jFxMLCw8TGyMvO1N3q/WthXlhSTkxKSEZFRURF
+RkdKTVBWX3D55NfPzMnIx8fIyMjIycrMztPa4vBzZl5XT0tIRkVERUdJTFJZYG745dvV0tDP
+zs3Nzc7R1NXX2d3j7Px1bGdeWVVTT05NTk9SVllcYGp09+rl39zb2tra2dna2NjZ2t3h6fxy
+amZhYGFlZmNiX19dW1tcXmBkaGxucHf/9erh3djTzszNzs7P0dbd5O37dmNdWVVVUU9PT05N
+TExMTU9RVl1q/une19PPzczLy8vKy8zNz9TZ3uTr6+/9eGteV1JNTEtLS0tMTk9TV1tibn77
+9Ozn493c2tfU09LQ0NHU1tjZ2dvf5Ovt/Hl3dHJtZGBfXFlVUU9NS0tMTExNTlJZXmzv3tXP
+zMrHxMPCw8XHyc3T3O1uXlROS0lJSUpMTk9SVlhcYGVsfu/k3trY09DPzs3OztHX3en9aV5Y
+Uk9OTU5PUlddYGVsffDq5N7a2tra19bV1tfW2Nrc3uLq83pvaF5YVE9NTExMTk9RVlpfaHPx
+5eDf3tzZ1tTS0M7Nzc3Nzs/R1tzh8HhmX1xWU1FPTUtLTEtMTE1OUVZcZXD96d/c2tjV0M3M
+y8nIycrMztLZ4Ox+a15aVVFPT09QUlNWW19gYGZtdP379u7p5+bn6+3t7/Hy7+7u7fl9e3Z2
+dHd4//rx6OXj4N/i4+Pk4+fn7PD6dW5ramppZGBfX11dXmJnaWpoZF9fXVtdYWp2/uzh29bU
+09DPz8/P0tjb3+Xj6vlvY15aVFBOTUtMTk9VWF1hZm12+evh3tza2tnX1dXW1dTW2Nnb3uDh
+4+Lj6/t3
+--hal_9000--
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Multiple encapsulation
+
+Received: from tomobiki-cho.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA17676; Thu, 24 Oct 91 17:34:03 -0700
+Date: Thu, 24 Oct 1991 17:32:56 -0700 (PDT)
+From: Mark Crispin <MRC@CAC.Washington.EDU>
+Sender: Mark Crispin <mrc@Tomobiki-Cho.CAC.Washington.EDU>
+Subject: Here's some more
+To: Mark Crispin <MRC@CAC.Washington.EDU>
+Message-Id: <MailManager.688350776.11603.mrc@Tomobiki-Cho.CAC.Washington.EDU>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;BOUNDARY="16819560-2078917053-688350843:#11603"
+
+--16819560-2078917053-688350843:#11603
+Content-Type: APPLICATION/POSTSCRIPT
+Content-Transfer-Encoding: BASE64
+Content-Description: The Simpsons!!
+
+JSEKJSVCb3VuZGluZ0JveDogKGF0ZW5kKQolJVBhZ2VzOiAoYXRlbmQpCiUl
+RG9jdW1lbnRGb250czogKGF0ZW5kKQolJUVuZENvbW1lbnRzCiUKJSBGcmFt
+ZU1ha2VyIFBvc3RTY3JpcHQgUHJvbG9nIDIuMCwgZm9yIHVzZSB3aXRoIEZy
+YW1lTWFrZXIgMi4wCiUgQ29weXJpZ2h0IChjKSAxOTg2LDg3LDg5IGJ5IEZy
+YW1lIFRlY2hub2xvZ3ksIEluYy4gIEFsbCByaWdodHMgcmVzZXJ2ZWQuCiUK
+JSBLbm93biBQcm9ibGVtczoKJQlEdWUgdG8gYnVncyBpbiBUcmFuc2NyaXB0
+LCB0aGUgJ1BTLUFkb2JlLScgaXMgb21pdHRlZCBmcm9tIGxpbmUgMQovRk12
+ZXJzaW9uICgyLjApIGRlZiAKJSBTZXQgdXAgQ29sb3IgdnMuIEJsYWNrLWFu
+ZC1XaGl0ZQoJL0ZNUHJpbnRJbkNvbG9yIHN5c3RlbWRpY3QgL2NvbG9yaW1h
+Z2Uga25vd24gZGVmCiUgVW5jb21tZW50IHRoaXMgbGluZSB0byBmb3JjZSBi
+Jncgb24gY29sb3IgcHJpbnRlcgolICAgL0ZNUHJpbnRJbkNvbG9yIGZhbHNl
+IGRlZgovRnJhbWVEaWN0IDE5MCBkaWN0IGRlZiAKc3lzdGVtZGljdCAvZXJy
+b3JkaWN0IGtub3duIG5vdCB7L2Vycm9yZGljdCAxMCBkaWN0IGRlZgoJCWVy
+cm9yZGljdCAvcmFuZ2VjaGVjayB7c3RvcH0gcHV0fSBpZgolIFRoZSByZWFk
+bGluZSBpbiAyMy4wIGRvZXNuJ3QgcmVjb2duaXplIGNyJ3MgYXMgbmwncyBv
+biBBcHBsZVRhbGsKRnJhbWVEaWN0IC90bXByYW5nZWNoZWNrIGVycm9yZGlj
+dCAvcmFuZ2VjaGVjayBnZXQgcHV0IAplcnJvcmRpY3QgL3JhbmdlY2hlY2sg
+e0ZyYW1lRGljdCAvYnVnIHRydWUgcHV0fSBwdXQgCkZyYW1lRGljdCAvYnVn
+IGZhbHNlIHB1dCAKbWFyayAKJSBTb21lIFBTIG1hY2hpbmVzIHJlYWQgcGFz
+dCB0aGUgQ1IsIHNvIGtlZXAgdGhlIGZvbGxvd2luZyAzIGxpbmVzIHRvZ2V0
+aGVyIQpjdXJyZW50ZmlsZSA1IHN0cmluZyByZWFkbGluZQowMAowMDAwMDAw
+MDAwCmNsZWFydG9tYXJrIAplcnJvcmRpY3QgL3JhbmdlY2hlY2sgRnJhbWVE
+aWN0IC90bXByYW5nZWNoZWNrIGdldCBwdXQgCkZyYW1lRGljdCAvYnVnIGdl
+dCB7IAoJL3JlYWRsaW5lIHsKCQkvZ3N0cmluZyBleGNoIGRlZgoJCS9nZmls
+ZSBleGNoIGRlZgoJCS9naW5kZXggMCBkZWYKCQl7CgkJCWdmaWxlIHJlYWQg
+cG9wIAoJCQlkdXAgMTAgZXEge2V4aXR9IGlmIAoJCQlkdXAgMTMgZXEge2V4
+aXR9IGlmIAoJCQlnc3RyaW5nIGV4Y2ggZ2luZGV4IGV4Y2ggcHV0IAoJCQkv
+Z2luZGV4IGdpbmRleCAxIGFkZCBkZWYgCgkJfSBsb29wCgkJcG9wIAoJCWdz
+dHJpbmcgMCBnaW5kZXggZ2V0aW50ZXJ2YWwgdHJ1ZSAKCQl9IGRlZgoJfSBp
+ZgovRk1WRVJTSU9OIHsKCUZNdmVyc2lvbiBuZSB7CgkJL1RpbWVzLVJvbWFu
+IGZpbmRmb250IDE4IHNjYWxlZm9udCBzZXRmb250CgkJMTAwIDEwMCBtb3Zl
+dG8KCQkoRnJhbWVNYWtlciB2ZXJzaW9uIGRvZXMgbm90IG1hdGNoIHBvc3Rz
+Y3JpcHRfcHJvbG9nISkKCQlkdXAgPQoJCXNob3cgc2hvd3BhZ2UKCQl9IGlm
+Cgl9IGRlZiAKL0ZNTE9DQUwgewoJRnJhbWVEaWN0IGJlZ2luCgkwIGRlZiAK
+CWVuZCAKCX0gZGVmIAoJL2dzdHJpbmcgRk1MT0NBTAoJL2dmaWxlIEZNTE9D
+QUwKCS9naW5kZXggRk1MT0NBTAoJL29yZ3hmZXIgRk1MT0NBTAoJL29yZ3By
+b2MgRk1MT0NBTAoJL29yZ2FuZ2xlIEZNTE9DQUwKCS9vcmdmcmVxIEZNTE9D
+QUwKCS95c2NhbGUgRk1MT0NBTAoJL3hzY2FsZSBGTUxPQ0FMCgkvbWFudWFs
+ZmVlZCBGTUxPQ0FMCgkvcGFwZXJoZWlnaHQgRk1MT0NBTAoJL3BhcGVyd2lk
+dGggRk1MT0NBTAovRk1ET0NVTUVOVCB7IAoJYXJyYXkgL0ZNZm9udHMgZXhj
+aCBkZWYgCgkvI2NvcGllcyBleGNoIGRlZgoJRnJhbWVEaWN0IGJlZ2luCgkw
+IG5lIGR1cCB7c2V0bWFudWFsZmVlZH0gaWYKCS9tYW51YWxmZWVkIGV4Y2gg
+ZGVmCgkvcGFwZXJoZWlnaHQgZXhjaCBkZWYKCS9wYXBlcndpZHRoIGV4Y2gg
+ZGVmCglzZXRwYXBlcm5hbWUKCW1hbnVhbGZlZWQge3RydWV9IHtwYXBlcnNp
+emV9IGlmZWxzZSAKCXttYW51YWxwYXBlcnNpemV9IHtmYWxzZX0gaWZlbHNl
+IAoJe2Rlc3BlcmF0ZXBhcGVyc2l6ZX0gaWYKCS95c2NhbGUgZXhjaCBkZWYK
+CS94c2NhbGUgZXhjaCBkZWYKCWN1cnJlbnR0cmFuc2ZlciBjdmxpdCAvb3Jn
+eGZlciBleGNoIGRlZgoJY3VycmVudHNjcmVlbiBjdmxpdCAvb3JncHJvYyBl
+eGNoIGRlZgoJL29yZ2FuZ2xlIGV4Y2ggZGVmIC9vcmdmcmVxIGV4Y2ggZGVm
+CgllbmQgCgl9IGRlZiAKCS9wYWdlc2F2ZSBGTUxPQ0FMCgkvb3JnbWF0cml4
+IEZNTE9DQUwKCS9sYW5kc2NhcGUgRk1MT0NBTAovRk1CRUdJTlBBR0UgeyAK
+CUZyYW1lRGljdCBiZWdpbiAKCS9wYWdlc2F2ZSBzYXZlIGRlZgoJMy44NiBz
+ZXRtaXRlcmxpbWl0CgkvbGFuZHNjYXBlIGV4Y2ggMCBuZSBkZWYKCWxhbmRz
+Y2FwZSB7IAoJCTkwIHJvdGF0ZSAwIGV4Y2ggbmVnIHRyYW5zbGF0ZSBwb3Ag
+CgkJfQoJCXtwb3AgcG9wfQoJCWlmZWxzZQoJeHNjYWxlIHlzY2FsZSBzY2Fs
+ZQoJL29yZ21hdHJpeCBtYXRyaXggZGVmCglnc2F2ZSAKCX0gZGVmIAovRk1F
+TkRQQUdFIHsKCWdyZXN0b3JlIAoJcGFnZXNhdmUgcmVzdG9yZQoJZW5kIAoJ
+c2hvd3BhZ2UKCX0gZGVmIAovRk1ERUZJTkVGT05UIHsgCglGcmFtZURpY3Qg
+YmVnaW4KCWZpbmRmb250IAoJUmVFbmNvZGUgCgkyIGluZGV4IGV4Y2ggCglk
+ZWZpbmVmb250IGV4Y2ggCglzY2FsZWZvbnQgCglGTWZvbnRzIDMgMSByb2xs
+IAoJcHV0CgllbmQgCgl9IGJpbmQgZGVmCi9GTU5PUk1BTElaRUdSQVBISUNT
+IHsgCgluZXdwYXRoCgkwLjAgMC4wIG1vdmV0bwoJMSBzZXRsaW5ld2lkdGgK
+CTAgc2V0bGluZWNhcAoJMCAwIDAgc2V0aHNiY29sb3IKCTAgc2V0Z3JheSAK
+CX0gYmluZCBkZWYKCS9meCBGTUxPQ0FMCgkvZnkgRk1MT0NBTAoJL2ZoIEZN
+TE9DQUwKCS9mdyBGTUxPQ0FMCgkvbGx4IEZNTE9DQUwKCS9sbHkgRk1MT0NB
+TAoJL3VyeCBGTUxPQ0FMCgkvdXJ5IEZNTE9DQUwKL0ZNQkVHSU5FUFNGIHsg
+CgllbmQgCgkvRk1FUFNGIHNhdmUgZGVmIAoJL3Nob3dwYWdlIHt9IGRlZiAK
+CUZNTk9STUFMSVpFR1JBUEhJQ1MgCglbL2Z5IC9meCAvZmggL2Z3IC91cnkg
+L3VyeCAvbGx5IC9sbHhdIHtleGNoIGRlZn0gZm9yYWxsIAoJZnggZnkgdHJh
+bnNsYXRlIAoJcm90YXRlCglmdyB1cnggbGx4IHN1YiBkaXYgZmggdXJ5IGxs
+eSBzdWIgZGl2IHNjYWxlIAoJbGx4IG5lZyBsbHkgbmVnIHRyYW5zbGF0ZSAK
+CX0gYmluZCBkZWYKL0ZNRU5ERVBTRiB7CglGTUVQU0YgcmVzdG9yZQoJRnJh
+bWVEaWN0IGJlZ2luIAoJfSBiaW5kIGRlZgpGcmFtZURpY3QgYmVnaW4gCi9z
+ZXRtYW51YWxmZWVkIHsKJSVCZWdpbkZlYXR1cmUgKk1hbnVhbEZlZWQgVHJ1
+ZQoJIHN0YXR1c2RpY3QgL21hbnVhbGZlZWQgdHJ1ZSBwdXQKJSVFbmRGZWF0
+dXJlCgl9IGRlZgovbWF4IHsyIGNvcHkgbHQge2V4Y2h9IGlmIHBvcH0gYmlu
+ZCBkZWYKL21pbiB7MiBjb3B5IGd0IHtleGNofSBpZiBwb3B9IGJpbmQgZGVm
+Ci9pbmNoIHs3MiBtdWx9IGRlZgovcGFnZWRpbWVuIHsgCglwYXBlcmhlaWdo
+dCBzdWIgYWJzIDE2IGx0IGV4Y2ggCglwYXBlcndpZHRoIHN1YiBhYnMgMTYg
+bHQgYW5kCgl7L3BhcGVybmFtZSBleGNoIGRlZn0ge3BvcH0gaWZlbHNlCgl9
+IGRlZgoJL3BhcGVyc2l6ZWRpY3QgRk1MT0NBTAovc2V0cGFwZXJuYW1lIHsg
+CgkvcGFwZXJzaXplZGljdCAxNCBkaWN0IGRlZiAKCXBhcGVyc2l6ZWRpY3Qg
+YmVnaW4KCS9wYXBlcm5hbWUgL3Vua25vd24gZGVmIAoJCS9MZXR0ZXIgOC41
+IGluY2ggMTEuMCBpbmNoIHBhZ2VkaW1lbgoJCS9MZXR0ZXJTbWFsbCA3LjY4
+IGluY2ggMTAuMTYgaW5jaCBwYWdlZGltZW4KCQkvVGFibG9pZCAxMS4wIGlu
+Y2ggMTcuMCBpbmNoIHBhZ2VkaW1lbgoJCS9MZWRnZXIgMTcuMCBpbmNoIDEx
+LjAgaW5jaCBwYWdlZGltZW4KCQkvTGVnYWwgOC41IGluY2ggMTQuMCBpbmNo
+IHBhZ2VkaW1lbgoJCS9TdGF0ZW1lbnQgNS41IGluY2ggOC41IGluY2ggcGFn
+ZWRpbWVuCgkJL0V4ZWN1dGl2ZSA3LjUgaW5jaCAxMC4wIGluY2ggcGFnZWRp
+bWVuCgkJL0EzIDExLjY5IGluY2ggMTYuNSBpbmNoIHBhZ2VkaW1lbgoJCS9B
+NCA4LjI2IGluY2ggMTEuNjkgaW5jaCBwYWdlZGltZW4KCQkvQTRTbWFsbCA3
+LjQ3IGluY2ggMTAuODUgaW5jaCBwYWdlZGltZW4KCQkvQjQgMTAuMTI1IGlu
+Y2ggMTQuMzMgaW5jaCBwYWdlZGltZW4KCQkvQjUgNy4xNiBpbmNoIDEwLjEy
+NSBpbmNoIHBhZ2VkaW1lbgoJZW5kCgl9IGRlZgovcGFwZXJzaXplIHsKCXBh
+cGVyc2l6ZWRpY3QgYmVnaW4KCQkvTGV0dGVyIHtsZXR0ZXJ0cmF5fSBkZWYK
+CQkvTGV0dGVyU21hbGwge2xldHRlcnRyYXkgbGV0dGVyc21hbGx9IGRlZgoJ
+CS9UYWJsb2lkIHsxMXgxN3RyYXl9IGRlZgoJCS9MZWRnZXIge2xlZGdlcnRy
+YXl9IGRlZgoJCS9MZWdhbCB7bGVnYWx0cmF5fSBkZWYKCQkvU3RhdGVtZW50
+IHtzdGF0ZW1lbnR0cmF5fSBkZWYKCQkvRXhlY3V0aXZlIHtleGVjdXRpdmV0
+cmF5fSBkZWYKCQkvQTMge2EzdHJheX0gZGVmCgkJL0E0IHthNHRyYXl9IGRl
+ZgoJCS9BNFNtYWxsIHthNHRyYXkgYTRzbWFsbH0gZGVmCgkJL0I0IHtiNHRy
+YXl9IGRlZgoJCS9CNSB7YjV0cmF5fSBkZWYKCQkvdW5rbm93biB7dW5rbm93
+bn0gZGVmCglwYXBlcnNpemVkaWN0IGR1cCBwYXBlcm5hbWUga25vd24ge3Bh
+cGVybmFtZX0gey91bmtub3dufSBpZmVsc2UgZ2V0CgllbmQKCXN0YXR1c2Rp
+Y3QgYmVnaW4gc3RvcHBlZCBlbmQgCgl9IGRlZgovbWFudWFscGFwZXJzaXpl
+IHsKCXBhcGVyc2l6ZWRpY3QgYmVnaW4KCQkvTGV0dGVyIHtsZXR0ZXJ9IGRl
+ZgoJCS9MZXR0ZXJTbWFsbCB7bGV0dGVyc21hbGx9IGRlZgoJCS9UYWJsb2lk
+IHsxMXgxN30gZGVmCgkJL0xlZGdlciB7bGVkZ2VyfSBkZWYKCQkvTGVnYWwg
+e2xlZ2FsfSBkZWYKCQkvU3RhdGVtZW50IHtzdGF0ZW1lbnR9IGRlZgoJCS9F
+eGVjdXRpdmUge2V4ZWN1dGl2ZX0gZGVmCgkJL0EzIHthM30gZGVmCgkJL0E0
+IHthNH0gZGVmCgkJL0E0U21hbGwge2E0c21hbGx9IGRlZgoJCS9CNCB7YjR9
+IGRlZgoJCS9CNSB7YjV9IGRlZgoJCS91bmtub3duIHt1bmtub3dufSBkZWYK
+CXBhcGVyc2l6ZWRpY3QgZHVwIHBhcGVybmFtZSBrbm93biB7cGFwZXJuYW1l
+fSB7L3Vua25vd259IGlmZWxzZSBnZXQKCWVuZAoJc3RvcHBlZCAKCX0gZGVm
+Ci9kZXNwZXJhdGVwYXBlcnNpemUgewoJc3RhdHVzZGljdCAvc2V0cGFnZXBh
+cmFtcyBrbm93bgoJCXsKCQlwYXBlcndpZHRoIHBhcGVyaGVpZ2h0IDAgMSAK
+CQlzdGF0dXNkaWN0IGJlZ2luCgkJe3NldHBhZ2VwYXJhbXN9IHN0b3BwZWQg
+cG9wIAoJCWVuZAoJCX0gaWYKCX0gZGVmCi9zYXZlbWF0cml4IHsKCW9yZ21h
+dHJpeCBjdXJyZW50bWF0cml4IHBvcAoJfSBiaW5kIGRlZgovcmVzdG9yZW1h
+dHJpeCB7CglvcmdtYXRyaXggc2V0bWF0cml4Cgl9IGJpbmQgZGVmCi9kbWF0
+cml4IG1hdHJpeCBkZWYKL2RwaSAgICA3MiAwIGRtYXRyaXggZGVmYXVsdG1h
+dHJpeCBkdHJhbnNmb3JtCiAgICBkdXAgbXVsIGV4Y2ggICBkdXAgbXVsIGFk
+ZCAgIHNxcnQgZGVmCi9mcmVxIGRwaSAxOC43NSBkaXYgOCBkaXYgcm91bmQg
+ZHVwIDAgZXEge3BvcCAxfSBpZiA4IG11bCBkcGkgZXhjaCBkaXYgZGVmCi9z
+YW5nbGUgMSAwIGRtYXRyaXggZGVmYXVsdG1hdHJpeCBkdHJhbnNmb3JtIGV4
+Y2ggYXRhbiBkZWYKL0RpYWNyaXRpY0VuY29kaW5nIFsKLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYKLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgL3NwYWNlIC9leGNsYW0gL3F1b3RlZGJsCi9udW1iZXJzaWduIC9k
+b2xsYXIgL3BlcmNlbnQgL2FtcGVyc2FuZCAvcXVvdGVzaW5nbGUgL3BhcmVu
+bGVmdAovcGFyZW5yaWdodCAvYXN0ZXJpc2sgL3BsdXMgL2NvbW1hIC9oeXBo
+ZW4gL3BlcmlvZCAvc2xhc2ggL3plcm8gL29uZQovdHdvIC90aHJlZSAvZm91
+ciAvZml2ZSAvc2l4IC9zZXZlbiAvZWlnaHQgL25pbmUgL2NvbG9uIC9zZW1p
+Y29sb24KL2xlc3MgL2VxdWFsIC9ncmVhdGVyIC9xdWVzdGlvbiAvYXQgL0Eg
+L0IgL0MgL0QgL0UgL0YgL0cgL0ggL0kgL0ogL0sKL0wgL00gL04gL08gL1Ag
+L1EgL1IgL1MgL1QgL1UgL1YgL1cgL1ggL1kgL1ogL2JyYWNrZXRsZWZ0IC9i
+YWNrc2xhc2gKL2JyYWNrZXRyaWdodCAvYXNjaWljaXJjdW0gL3VuZGVyc2Nv
+cmUgL2dyYXZlIC9hIC9iIC9jIC9kIC9lIC9mIC9nIC9oCi9pIC9qIC9rIC9s
+IC9tIC9uIC9vIC9wIC9xIC9yIC9zIC90IC91IC92IC93IC94IC95IC96IC9i
+cmFjZWxlZnQgL2JhcgovYnJhY2VyaWdodCAvYXNjaWl0aWxkZSAvLm5vdGRl
+ZiAvQWRpZXJlc2lzIC9BcmluZyAvQ2NlZGlsbGEgL0VhY3V0ZQovTnRpbGRl
+IC9PZGllcmVzaXMgL1VkaWVyZXNpcyAvYWFjdXRlIC9hZ3JhdmUgL2FjaXJj
+dW1mbGV4IC9hZGllcmVzaXMKL2F0aWxkZSAvYXJpbmcgL2NjZWRpbGxhIC9l
+YWN1dGUgL2VncmF2ZSAvZWNpcmN1bWZsZXggL2VkaWVyZXNpcwovaWFjdXRl
+IC9pZ3JhdmUgL2ljaXJjdW1mbGV4IC9pZGllcmVzaXMgL250aWxkZSAvb2Fj
+dXRlIC9vZ3JhdmUKL29jaXJjdW1mbGV4IC9vZGllcmVzaXMgL290aWxkZSAv
+dWFjdXRlIC91Z3JhdmUgL3VjaXJjdW1mbGV4Ci91ZGllcmVzaXMgL2RhZ2dl
+ciAvLm5vdGRlZiAvY2VudCAvc3RlcmxpbmcgL3NlY3Rpb24gL2J1bGxldAov
+cGFyYWdyYXBoIC9nZXJtYW5kYmxzIC9yZWdpc3RlcmVkIC9jb3B5cmlnaHQg
+L3RyYWRlbWFyayAvYWN1dGUKL2RpZXJlc2lzIC8ubm90ZGVmIC9BRSAvT3Ns
+YXNoIC8ubm90ZGVmIC8ubm90ZGVmIC8ubm90ZGVmIC8ubm90ZGVmCi95ZW4g
+Ly5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYg
+Ly5ub3RkZWYKL29yZGZlbWluaW5lIC9vcmRtYXNjdWxpbmUgLy5ub3RkZWYg
+L2FlIC9vc2xhc2ggL3F1ZXN0aW9uZG93bgovZXhjbGFtZG93biAvbG9naWNh
+bG5vdCAvLm5vdGRlZiAvZmxvcmluIC8ubm90ZGVmIC8ubm90ZGVmCi9ndWls
+bGVtb3RsZWZ0IC9ndWlsbGVtb3RyaWdodCAvZWxsaXBzaXMgLy5ub3RkZWYg
+L0FncmF2ZSAvQXRpbGRlCi9PdGlsZGUgL09FIC9vZSAvZW5kYXNoIC9lbWRh
+c2ggL3F1b3RlZGJsbGVmdCAvcXVvdGVkYmxyaWdodAovcXVvdGVsZWZ0IC9x
+dW90ZXJpZ2h0IC8ubm90ZGVmIC8ubm90ZGVmIC95ZGllcmVzaXMgL1lkaWVy
+ZXNpcwovZnJhY3Rpb24gL2N1cnJlbmN5IC9ndWlsc2luZ2xsZWZ0IC9ndWls
+c2luZ2xyaWdodCAvZmkgL2ZsIC9kYWdnZXJkYmwKL3BlcmlvZGNlbnRlcmVk
+IC9xdW90ZXNpbmdsYmFzZSAvcXVvdGVkYmxiYXNlIC9wZXJ0aG91c2FuZAov
+QWNpcmN1bWZsZXggL0VjaXJjdW1mbGV4IC9BYWN1dGUgL0VkaWVyZXNpcyAv
+RWdyYXZlIC9JYWN1dGUKL0ljaXJjdW1mbGV4IC9JZGllcmVzaXMgL0lncmF2
+ZSAvT2FjdXRlIC9PY2lyY3VtZmxleCAvLm5vdGRlZiAvT2dyYXZlCi9VYWN1
+dGUgL1VjaXJjdW1mbGV4IC9VZ3JhdmUgL2RvdGxlc3NpIC9jaXJjdW1mbGV4
+IC90aWxkZSAvbWFjcm9uCi9icmV2ZSAvZG90YWNjZW50IC9yaW5nIC9jZWRp
+bGxhIC9odW5nYXJ1bWxhdXQgL29nb25layAvY2Fyb24KXSBkZWYKL1JlRW5j
+b2RlIHsgCglkdXAgCglsZW5ndGggCglkaWN0IGJlZ2luIAoJewoJMSBpbmRl
+eCAvRklEIG5lIAoJCXtkZWZ9IAoJCXtwb3AgcG9wfSBpZmVsc2UgCgl9IGZv
+cmFsbAoJRW5jb2RpbmcgU3RhbmRhcmRFbmNvZGluZyBlcSAKCXsKCQkvRW5j
+b2RpbmcgRGlhY3JpdGljRW5jb2RpbmcgZGVmCgl9aWYKCWN1cnJlbnRkaWN0
+IAoJZW5kIAoJfSBiaW5kIGRlZgovZ3JheW1vZGUgdHJ1ZSBkZWYKCS9id2lk
+dGggRk1MT0NBTAoJL2Jwc2lkZSBGTUxPQ0FMCgkvYnN0cmluZyBGTUxPQ0FM
+Cgkvb25iaXRzIEZNTE9DQUwKCS9vZmZiaXRzIEZNTE9DQUwKCS94aW5kZXgg
+Rk1MT0NBTAoJL3lpbmRleCBGTUxPQ0FMCgkveCBGTUxPQ0FMCgkveSBGTUxP
+Q0FMCi9zZXRwYXR0ZXJuIHsKCSAvYndpZHRoICBleGNoIGRlZgoJIC9icHNp
+ZGUgIGV4Y2ggZGVmCgkgL2JzdHJpbmcgZXhjaCBkZWYKCSAvb25iaXRzIDAg
+ZGVmICAvb2ZmYml0cyAwIGRlZgoJIGZyZXEgc2FuZ2xlIGxhbmRzY2FwZSB7
+OTAgYWRkfSBpZiAKCQl7L3kgZXhjaCBkZWYKCQkgL3ggZXhjaCBkZWYKCQkg
+L3hpbmRleCB4IDEgYWRkIDIgZGl2IGJwc2lkZSBtdWwgY3ZpIGRlZgoJCSAv
+eWluZGV4IHkgMSBhZGQgMiBkaXYgYnBzaWRlIG11bCBjdmkgZGVmCgkJIGJz
+dHJpbmcgeWluZGV4IGJ3aWR0aCBtdWwgeGluZGV4IDggaWRpdiBhZGQgZ2V0
+CgkJIDEgNyB4aW5kZXggOCBtb2Qgc3ViIGJpdHNoaWZ0IGFuZCAwIG5lCgkJ
+IHsvb25iaXRzICBvbmJpdHMgIDEgYWRkIGRlZiAxfQoJCSB7L29mZmJpdHMg
+b2ZmYml0cyAxIGFkZCBkZWYgMH0KCQkgaWZlbHNlCgkJfQoJCXNldHNjcmVl
+bgoJIHt9IHNldHRyYW5zZmVyCgkgb2ZmYml0cyBvZmZiaXRzIG9uYml0cyBh
+ZGQgZGl2IEZNc2V0Z3JheQoJL2dyYXltb2RlIGZhbHNlIGRlZgoJfSBiaW5k
+IGRlZgovZ3JheW5lc3MgewoJRk1zZXRncmF5CglncmF5bW9kZSBub3QgewoJ
+CS9ncmF5bW9kZSB0cnVlIGRlZgoJCW9yZ3hmZXIgY3Z4IHNldHRyYW5zZmVy
+CgkJb3JnZnJlcSBvcmdhbmdsZSBvcmdwcm9jIGN2eCBzZXRzY3JlZW4KCQl9
+IGlmCgl9IGJpbmQgZGVmCgkvSFVFIEZNTE9DQUwKCS9TQVQgRk1MT0NBTAoJ
+L0JSSUdIVCBGTUxPQ0FMCgkvQ29sb3JzIEZNTE9DQUwKRk1QcmludEluQ29s
+b3IgCgkKCXsKCS9IVUUgMCBkZWYKCS9TQVQgMCBkZWYKCS9CUklHSFQgMCBk
+ZWYKCSUgYXJyYXkgb2YgYXJyYXlzIEh1ZSBhbmQgU2F0IHZhbHVlcyBmb3Ig
+dGhlIHNlcGFyYXRpb25zIFtIVUUgQlJJR0hUXQoJL0NvbG9ycyAgIAoJW1sw
+ICAgIDAgIF0gICAgJSBibGFjawoJIFswICAgIDAgIF0gICAgJSB3aGl0ZQoJ
+IFswLjAwIDEuMF0gICAgJSByZWQKCSBbMC4zNyAxLjBdICAgICUgZ3JlZW4K
+CSBbMC42MCAxLjBdICAgICUgYmx1ZQoJIFswLjUwIDEuMF0gICAgJSBjeWFu
+CgkgWzAuODMgMS4wXSAgICAlIG1hZ2VudGEKCSBbMC4xNiAxLjBdICAgICUg
+Y29tbWVudCAvIHllbGxvdwoJIF0gZGVmCiAgICAgIAoJL0JFR0lOQklUTUFQ
+Q09MT1IgeyAKCQlCSVRNQVBDT0xPUn0gZGVmCgkvQkVHSU5CSVRNQVBDT0xP
+UmMgeyAKCQlCSVRNQVBDT0xPUmN9IGRlZgoJL0sgeyAKCQlDb2xvcnMgZXhj
+aCBnZXQgZHVwCgkJMCBnZXQgL0hVRSBleGNoIHN0b3JlIAoJCTEgZ2V0IC9C
+UklHSFQgZXhjaCBzdG9yZQoJCSAgSFVFIDAgZXEgQlJJR0hUIDAgZXEgYW5k
+CgkJCXsxLjAgU0FUIHN1YiBzZXRncmF5fQoJCQl7SFVFIFNBVCBCUklHSFQg
+c2V0aHNiY29sb3J9IAoJCSAgaWZlbHNlCgkJfSBkZWYKCS9GTXNldGdyYXkg
+eyAKCQkvU0FUIGV4Y2ggMS4wIGV4Y2ggc3ViIHN0b3JlIAoJCSAgSFVFIDAg
+ZXEgQlJJR0hUIDAgZXEgYW5kCgkJCXsxLjAgU0FUIHN1YiBzZXRncmF5fQoJ
+CQl7SFVFIFNBVCBCUklHSFQgc2V0aHNiY29sb3J9IAoJCSAgaWZlbHNlCgkJ
+fSBiaW5kIGRlZgoJfQoJCgl7CgkvQkVHSU5CSVRNQVBDT0xPUiB7IAoJCUJJ
+VE1BUEdSQVl9IGRlZgoJL0JFR0lOQklUTUFQQ09MT1JjIHsgCgkJQklUTUFQ
+R1JBWWN9IGRlZgoJL0ZNc2V0Z3JheSB7c2V0Z3JheX0gYmluZCBkZWYKCS9L
+IHsgCgkJcG9wCgkJfSBkZWYKCX0KaWZlbHNlCi9ub3JtYWxpemUgewoJdHJh
+bnNmb3JtIHJvdW5kIGV4Y2ggcm91bmQgZXhjaCBpdHJhbnNmb3JtCgl9IGJp
+bmQgZGVmCi9kbm9ybWFsaXplIHsKCWR0cmFuc2Zvcm0gcm91bmQgZXhjaCBy
+b3VuZCBleGNoIGlkdHJhbnNmb3JtCgl9IGJpbmQgZGVmCi9sbm9ybWFsaXpl
+IHsgCgkwIGR0cmFuc2Zvcm0gZXhjaCBjdmkgMiBpZGl2IDIgbXVsIDEgYWRk
+IGV4Y2ggaWR0cmFuc2Zvcm0gcG9wCgl9IGJpbmQgZGVmCi9IIHsgCglsbm9y
+bWFsaXplIHNldGxpbmV3aWR0aAoJfSBiaW5kIGRlZgovWiB7CglzZXRsaW5l
+Y2FwCgl9IGJpbmQgZGVmCi9YIHsgCglmaWxscHJvY3MgZXhjaCBnZXQgZXhl
+YwoJfSBiaW5kIGRlZgovViB7IAoJZ3NhdmUgZW9maWxsIGdyZXN0b3JlCgl9
+IGJpbmQgZGVmCi9OIHsgCglzdHJva2UKCX0gYmluZCBkZWYKL00ge25ld3Bh
+dGggbW92ZXRvfSBiaW5kIGRlZgovRSB7bGluZXRvfSBiaW5kIGRlZgovRCB7
+Y3VydmV0b30gYmluZCBkZWYKL08ge2Nsb3NlcGF0aH0gYmluZCBkZWYKCS9u
+IEZNTE9DQUwKL0wgeyAKIAkvbiBleGNoIGRlZgoJbmV3cGF0aAoJbm9ybWFs
+aXplCgltb3ZldG8gCgkyIDEgbiB7cG9wIG5vcm1hbGl6ZSBsaW5ldG99IGZv
+cgoJfSBiaW5kIGRlZgovWSB7IAoJTCAKCWNsb3NlcGF0aAoJfSBiaW5kIGRl
+ZgoJL3gxIEZNTE9DQUwKCS94MiBGTUxPQ0FMCgkveTEgRk1MT0NBTAoJL3ky
+IEZNTE9DQUwKCS9yYWQgRk1MT0NBTAovUiB7IAoJL3kyIGV4Y2ggZGVmCgkv
+eDIgZXhjaCBkZWYKCS95MSBleGNoIGRlZgoJL3gxIGV4Y2ggZGVmCgl4MSB5
+MQoJeDIgeTEKCXgyIHkyCgl4MSB5MgoJNCBZIAoJfSBiaW5kIGRlZgovUlIg
+eyAKCS9yYWQgZXhjaCBkZWYKCW5vcm1hbGl6ZQoJL3kyIGV4Y2ggZGVmCgkv
+eDIgZXhjaCBkZWYKCW5vcm1hbGl6ZQoJL3kxIGV4Y2ggZGVmCgkveDEgZXhj
+aCBkZWYKCW5ld3BhdGgKCXgxIHkxIHJhZCBhZGQgbW92ZXRvCgl4MSB5MiB4
+MiB5MiByYWQgYXJjdG8KCXgyIHkyIHgyIHkxIHJhZCBhcmN0bwoJeDIgeTEg
+eDEgeTEgcmFkIGFyY3RvCgl4MSB5MSB4MSB5MiByYWQgYXJjdG8KCWNsb3Nl
+cGF0aAoJMTYge3BvcH0gcmVwZWF0Cgl9IGJpbmQgZGVmCi9DIHsgCglncmVz
+dG9yZQoJZ3NhdmUKCVIgCgljbGlwCgl9IGJpbmQgZGVmCi9VIHsgCglncmVz
+dG9yZQoJZ3NhdmUKCX0gYmluZCBkZWYKL0YgeyAKCUZNZm9udHMgZXhjaCBn
+ZXQKCXNldGZvbnQKCX0gYmluZCBkZWYKL1QgeyAKCW1vdmV0byBzaG93Cgl9
+IGJpbmQgZGVmCi9SRiB7IAoJcm90YXRlCgkwIG5lIHstMSAxIHNjYWxlfSBp
+ZgoJfSBiaW5kIGRlZgovVEYgeyAKCWdzYXZlCgltb3ZldG8gCglSRgoJc2hv
+dwoJZ3Jlc3RvcmUKCX0gYmluZCBkZWYKL1AgeyAKCW1vdmV0bwoJMCAzMiAz
+IDIgcm9sbCB3aWR0aHNob3cKCX0gYmluZCBkZWYKL1BGIHsgCglnc2F2ZQoJ
+bW92ZXRvIAoJUkYKCTAgMzIgMyAyIHJvbGwgd2lkdGhzaG93CglncmVzdG9y
+ZQoJfSBiaW5kIGRlZgovUyB7IAoJbW92ZXRvCgkwIGV4Y2ggYXNob3cKCX0g
+YmluZCBkZWYKL1NGIHsgCglnc2F2ZQoJbW92ZXRvCglSRgoJMCBleGNoIGFz
+aG93CglncmVzdG9yZQoJfSBiaW5kIGRlZgovQiB7IAoJbW92ZXRvCgkwIDMy
+IDQgMiByb2xsIDAgZXhjaCBhd2lkdGhzaG93Cgl9IGJpbmQgZGVmCi9CRiB7
+IAoJZ3NhdmUKCW1vdmV0bwoJUkYKCTAgMzIgNCAyIHJvbGwgMCBleGNoIGF3
+aWR0aHNob3cKCWdyZXN0b3JlCgl9IGJpbmQgZGVmCgkveCBGTUxPQ0FMCgkv
+eSBGTUxPQ0FMCgkvZHggRk1MT0NBTAoJL2R5IEZNTE9DQUwKCS9kbCBGTUxP
+Q0FMCgkvdCBGTUxPQ0FMCgkvdDIgRk1MT0NBTAoJL0NvcyBGTUxPQ0FMCgkv
+U2luIEZNTE9DQUwKCS9yIEZNTE9DQUwKL1cgeyAKCWRub3JtYWxpemUKCS9k
+eSBleGNoIGRlZgoJL2R4IGV4Y2ggZGVmCglub3JtYWxpemUKCS95ICBleGNo
+IGRlZgoJL3ggIGV4Y2ggZGVmCgkvZGwgZHggZHggbXVsIGR5IGR5IG11bCBh
+ZGQgc3FydCBkZWYKCWRsIDAuMCBndCB7CgkJL3QgY3VycmVudGxpbmV3aWR0
+aCBkZWYKCQlzYXZlbWF0cml4CgkJL0NvcyBkeCBkbCBkaXYgZGVmCgkJL1Np
+biBkeSBkbCBkaXYgZGVmCgkJL3IgW0NvcyBTaW4gU2luIG5lZyBDb3MgMC4w
+IDAuMF0gZGVmCgkJL3QyIHQgMi41IG11bCAzLjUgbWF4IGRlZgoJCW5ld3Bh
+dGgKCQl4IHkgdHJhbnNsYXRlCgkJciBjb25jYXQKCQkwLjAgMC4wIG1vdmV0
+bwoJCWRsIHQgMi43IG11bCBzdWIgMC4wIHJsaW5ldG8KCQlzdHJva2UKCQly
+ZXN0b3JlbWF0cml4CgkJeCBkeCBhZGQgeSBkeSBhZGQgdHJhbnNsYXRlCgkJ
+ciBjb25jYXQKCQl0IDAuNjcgbXVsIHNldGxpbmV3aWR0aAoJCXQgMS42MSBt
+dWwgbmVnICAwLjAgdHJhbnNsYXRlCgkJMC4wIDAuMCBtb3ZldG8KCQl0MiAx
+LjcgbXVsIG5lZyAgdDIgMi4wIGRpdiAgICAgbW92ZXRvCgkJMC4wIDAuMCBs
+aW5ldG8KCQl0MiAxLjcgbXVsIG5lZyAgdDIgMi4wIGRpdiBuZWcgbGluZXRv
+CgkJc3Ryb2tlCgkJdCBzZXRsaW5ld2lkdGgKCQlyZXN0b3JlbWF0cml4CgkJ
+fSBpZgoJfSBiaW5kIGRlZgovRyB7IAoJZ3NhdmUKCW5ld3BhdGgKCW5vcm1h
+bGl6ZSB0cmFuc2xhdGUgMC4wIDAuMCBtb3ZldG8gCglkbm9ybWFsaXplIHNj
+YWxlIAoJMC4wIDAuMCAxLjAgNSAzIHJvbGwgYXJjIAoJY2xvc2VwYXRoIGZp
+bGwKCWdyZXN0b3JlCgl9IGJpbmQgZGVmCi9BIHsgCglnc2F2ZQoJc2F2ZW1h
+dHJpeAoJbmV3cGF0aAoJMiBpbmRleCAyIGRpdiBhZGQgZXhjaCAzIGluZGV4
+IDIgZGl2IHN1YiBleGNoIAoJbm9ybWFsaXplIDIgaW5kZXggMiBkaXYgc3Vi
+IGV4Y2ggMyBpbmRleCAyIGRpdiBhZGQgZXhjaCAKCXRyYW5zbGF0ZSAKCXNj
+YWxlIAoJMC4wIDAuMCAxLjAgNSAzIHJvbGwgYXJjIAoJcmVzdG9yZW1hdHJp
+eAoJc3Ryb2tlCglncmVzdG9yZQoJfSBiaW5kIGRlZgoJL3ggRk1MT0NBTAoJ
+L3kgRk1MT0NBTAoJL3cgRk1MT0NBTAoJL2ggRk1MT0NBTAoJL3h4IEZNTE9D
+QUwKCS95eSBGTUxPQ0FMCgkvd3cgRk1MT0NBTAoJL2hoIEZNTE9DQUwKCS9G
+TXNhdmVvYmplY3QgRk1MT0NBTAoJL0ZNb3B0b3AgRk1MT0NBTAoJL0ZNZGlj
+dHRvcCBGTUxPQ0FMCi9CRUdJTlBSSU5UQ09ERSB7IAoJL0ZNZGljdHRvcCBj
+b3VudGRpY3RzdGFjayAxIGFkZCBkZWYgCgkvRk1vcHRvcCBjb3VudCA0IHN1
+YiBkZWYgCgkvRk1zYXZlb2JqZWN0IHNhdmUgZGVmCgl1c2VyZGljdCBiZWdp
+biAKCS9zaG93cGFnZSB7fSBkZWYgCglGTU5PUk1BTElaRUdSQVBISUNTIAoJ
+MyBpbmRleCBuZWcgMyBpbmRleCBuZWcgdHJhbnNsYXRlCgl9IGJpbmQgZGVm
+Ci9FTkRQUklOVENPREUgewoJY291bnQgLTEgRk1vcHRvcCB7cG9wIHBvcH0g
+Zm9yIAoJY291bnRkaWN0c3RhY2sgLTEgRk1kaWN0dG9wIHtwb3AgZW5kfSBm
+b3IgCglGTXNhdmVvYmplY3QgcmVzdG9yZSAKCX0gYmluZCBkZWYKL2duIHsg
+CgkwIAoJewk0NiBtdWwgCgkJY2YgcmVhZCBwb3AgCgkJMzIgc3ViIAoJCWR1
+cCA0NiBsdCB7ZXhpdH0gaWYgCgkJNDYgc3ViIGFkZCAKCQl9IGxvb3AKCWFk
+ZCAKCX0gYmluZCBkZWYKCS9zdHIgRk1MT0NBTAovY2ZzIHsgCgkvc3RyIHNs
+IHN0cmluZyBkZWYgCgkwIDEgc2wgMSBzdWIge3N0ciBleGNoIHZhbCBwdXR9
+IGZvciAKCXN0ciBkZWYgCgl9IGJpbmQgZGVmCi9pYyBbIAoJMCAwIDAgMCAw
+IDAgMCAwIDAgMCAwIDAgMCAwIDAgMDIyMwoJMCAwIDAgMCAwIDAgMCAwIDAg
+MCAwIDAgMCAwIDAgMDIyMwoJMAoJezAgaHh9IHsxIGh4fSB7MiBoeH0gezMg
+aHh9IHs0IGh4fSB7NSBoeH0gezYgaHh9IHs3IGh4fSB7OCBoeH0gezkgaHh9
+Cgl7MTAgaHh9IHsxMSBoeH0gezEyIGh4fSB7MTMgaHh9IHsxNCBoeH0gezE1
+IGh4fSB7MTYgaHh9IHsxNyBoeH0gezE4IGh4fQoJezE5IGh4fSB7Z24gaHh9
+IHswfSB7MX0gezJ9IHszfSB7NH0gezV9IHs2fSB7N30gezh9IHs5fSB7MTB9
+IHsxMX0gezEyfQoJezEzfSB7MTR9IHsxNX0gezE2fSB7MTd9IHsxOH0gezE5
+fSB7Z259IHswIHdofSB7MSB3aH0gezIgd2h9IHszIHdofQoJezQgd2h9IHs1
+IHdofSB7NiB3aH0gezcgd2h9IHs4IHdofSB7OSB3aH0gezEwIHdofSB7MTEg
+d2h9IHsxMiB3aH0KCXsxMyB3aH0gezE0IHdofSB7Z24gd2h9IHswIGJsfSB7
+MSBibH0gezIgYmx9IHszIGJsfSB7NCBibH0gezUgYmx9IHs2IGJsfQoJezcg
+Ymx9IHs4IGJsfSB7OSBibH0gezEwIGJsfSB7MTEgYmx9IHsxMiBibH0gezEz
+IGJsfSB7MTQgYmx9IHtnbiBibH0KCXswIGZsfSB7MSBmbH0gezIgZmx9IHsz
+IGZsfSB7NCBmbH0gezUgZmx9IHs2IGZsfSB7NyBmbH0gezggZmx9IHs5IGZs
+fQoJezEwIGZsfSB7MTEgZmx9IHsxMiBmbH0gezEzIGZsfSB7MTQgZmx9IHtn
+biBmbH0KCV0gZGVmCgkvc2wgRk1MT0NBTAoJL3ZhbCBGTUxPQ0FMCgkvd3Mg
+Rk1MT0NBTAoJL2ltIEZNTE9DQUwKCS9icyBGTUxPQ0FMCgkvY3MgRk1MT0NB
+TAoJL2xlbiBGTUxPQ0FMCgkvcG9zIEZNTE9DQUwKL21zIHsgCgkvc2wgZXhj
+aCBkZWYgCgkvdmFsIDI1NSBkZWYgCgkvd3MgY2ZzIAoJL2ltIGNmcyAKCS92
+YWwgMCBkZWYgCgkvYnMgY2ZzIAoJL2NzIGNmcyAKCX0gYmluZCBkZWYKNDAw
+IG1zIAovaXAgeyAKCWlzIAoJMCAKCWNmIGNzIHJlYWRsaW5lIHBvcCAKCXsJ
+aWMgZXhjaCBnZXQgZXhlYyAKCQlhZGQgCgkJfSBmb3JhbGwgCglwb3AgCgkK
+CX0gYmluZCBkZWYKL3doIHsgCgkvbGVuIGV4Y2ggZGVmIAoJL3BvcyBleGNo
+IGRlZiAKCXdzIDAgbGVuIGdldGludGVydmFsIGltIHBvcyBsZW4gZ2V0aW50
+ZXJ2YWwgY29weSBwb3AKCXBvcyBsZW4gCgl9IGJpbmQgZGVmCi9ibCB7IAoJ
+L2xlbiBleGNoIGRlZiAKCS9wb3MgZXhjaCBkZWYgCglicyAwIGxlbiBnZXRp
+bnRlcnZhbCBpbSBwb3MgbGVuIGdldGludGVydmFsIGNvcHkgcG9wCglwb3Mg
+bGVuIAoJfSBiaW5kIGRlZgovczEgMSBzdHJpbmcgZGVmCi9mbCB7IAoJL2xl
+biBleGNoIGRlZiAKCS9wb3MgZXhjaCBkZWYgCgkvdmFsIGNmIHMxIHJlYWRo
+ZXhzdHJpbmcgcG9wIDAgZ2V0IGRlZgoJcG9zIDEgcG9zIGxlbiBhZGQgMSBz
+dWIge2ltIGV4Y2ggdmFsIHB1dH0gZm9yCglwb3MgbGVuIAoJfSBiaW5kIGRl
+ZgovaHggeyAKCTMgY29weSBnZXRpbnRlcnZhbCAKCWNmIGV4Y2ggcmVhZGhl
+eHN0cmluZyBwb3AgcG9wIAoJfSBiaW5kIGRlZgoJL2ggRk1MT0NBTAoJL3cg
+Rk1MT0NBTAoJL2QgRk1MT0NBTAoJL2xiIEZNTE9DQUwKCS9iaXRtYXBzYXZl
+IEZNTE9DQUwKCS9pcyBGTUxPQ0FMCgkvY2YgRk1MT0NBTAovd2J5dGVzIHsg
+CglkdXAgCgk4IGVxIHtwb3B9IHsxIGVxIHs3IGFkZCA4IGlkaXZ9IHszIGFk
+ZCA0IGlkaXZ9IGlmZWxzZX0gaWZlbHNlCgl9IGJpbmQgZGVmCi9CRUdJTkJJ
+VE1BUEJXYyB7IAoJMSB7fSBDT01NT05CSVRNQVBjCgl9IGJpbmQgZGVmCi9C
+RUdJTkJJVE1BUEdSQVljIHsgCgk4IHt9IENPTU1PTkJJVE1BUGMKCX0gYmlu
+ZCBkZWYKL0JFR0lOQklUTUFQMkJJVGMgeyAKCTIge30gQ09NTU9OQklUTUFQ
+YwoJfSBiaW5kIGRlZgovQ09NTU9OQklUTUFQYyB7IAoJL3IgZXhjaCBkZWYK
+CS9kIGV4Y2ggZGVmCglnc2F2ZQoJdHJhbnNsYXRlIHJvdGF0ZSBzY2FsZSAv
+aCBleGNoIGRlZiAvdyBleGNoIGRlZgoJL2xiIHcgZCB3Ynl0ZXMgZGVmIAoJ
+c2wgbGIgbHQge2xiIG1zfSBpZiAKCS9iaXRtYXBzYXZlIHNhdmUgZGVmIAoJ
+ciAgICAgICAgICAgICAgICAgICAgCgkvaXMgaW0gMCBsYiBnZXRpbnRlcnZh
+bCBkZWYgCgl3cyAwIGxiIGdldGludGVydmFsIGlzIGNvcHkgcG9wIAoJL2Nm
+IGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBkIFt3IDAgMCBoIG5lZyAwIGhdIAoJ
+e2lwfSBpbWFnZSAKCWJpdG1hcHNhdmUgcmVzdG9yZSAKCWdyZXN0b3JlCgl9
+IGJpbmQgZGVmCi9CRUdJTkJJVE1BUEJXIHsgCgkxIHt9IENPTU1PTkJJVE1B
+UAoJfSBiaW5kIGRlZgovQkVHSU5CSVRNQVBHUkFZIHsgCgk4IHt9IENPTU1P
+TkJJVE1BUAoJfSBiaW5kIGRlZgovQkVHSU5CSVRNQVAyQklUIHsgCgkyIHt9
+IENPTU1PTkJJVE1BUAoJfSBiaW5kIGRlZgovQ09NTU9OQklUTUFQIHsgCgkv
+ciBleGNoIGRlZgoJL2QgZXhjaCBkZWYKCWdzYXZlCgl0cmFuc2xhdGUgcm90
+YXRlIHNjYWxlIC9oIGV4Y2ggZGVmIC93IGV4Y2ggZGVmCgkvYml0bWFwc2F2
+ZSBzYXZlIGRlZiAKCXIgICAgICAgICAgICAgICAgICAgIAoJL2lzIHcgZCB3
+Ynl0ZXMgc3RyaW5nIGRlZgoJL2NmIGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBk
+IFt3IDAgMCBoIG5lZyAwIGhdIAoJe2NmIGlzIHJlYWRoZXhzdHJpbmcgcG9w
+fSBpbWFnZQoJYml0bWFwc2F2ZSByZXN0b3JlIAoJZ3Jlc3RvcmUKCX0gYmlu
+ZCBkZWYKCS9wcm9jMSBGTUxPQ0FMCgkvcHJvYzIgRk1MT0NBTAoJL25ld3By
+b2MgRk1MT0NBTAovRm1jYyB7CiAgICAvcHJvYzIgZXhjaCBjdmxpdCBkZWYK
+ICAgIC9wcm9jMSBleGNoIGN2bGl0IGRlZgogICAgL25ld3Byb2MgcHJvYzEg
+bGVuZ3RoIHByb2MyIGxlbmd0aCBhZGQgYXJyYXkgZGVmCiAgICBuZXdwcm9j
+IDAgcHJvYzEgcHV0aW50ZXJ2YWwKICAgIG5ld3Byb2MgcHJvYzEgbGVuZ3Ro
+IHByb2MyIHB1dGludGVydmFsCiAgICBuZXdwcm9jIGN2eAp9IGJpbmQgZGVm
+Ci9uZ3JheXQgMjU2IGFycmF5IGRlZgovbnJlZHQgMjU2IGFycmF5IGRlZgov
+bmJsdWV0IDI1NiBhcnJheSBkZWYKL25ncmVlbnQgMjU2IGFycmF5IGRlZgoJ
+L2dyeXQgRk1MT0NBTAoJL2JsdXQgRk1MT0NBTAoJL2dybnQgRk1MT0NBTAoJ
+L3JlZHQgRk1MT0NBTAoJL2luZHggRk1MT0NBTAoJL2N5bnUgRk1MT0NBTAoJ
+L21hZ3UgRk1MT0NBTAoJL3llbHUgRk1MT0NBTAoJL2sgRk1MT0NBTAoJL3Ug
+Rk1MT0NBTAovY29sb3JzZXR1cCB7CgljdXJyZW50Y29sb3J0cmFuc2ZlcgoJ
+L2dyeXQgZXhjaCBkZWYKCS9ibHV0IGV4Y2ggZGVmCgkvZ3JudCBleGNoIGRl
+ZgoJL3JlZHQgZXhjaCBkZWYKCTAgMSAyNTUgewoJCS9pbmR4IGV4Y2ggZGVm
+CgkJL2N5bnUgMSByZWQgaW5keCBnZXQgMjU1IGRpdiBzdWIgZGVmCgkJL21h
+Z3UgMSBncmVlbiBpbmR4IGdldCAyNTUgZGl2IHN1YiBkZWYKCQkveWVsdSAx
+IGJsdWUgaW5keCBnZXQgMjU1IGRpdiBzdWIgZGVmCgkJL2sgY3ludSBtYWd1
+IG1pbiB5ZWx1IG1pbiBkZWYKCQkvdSBrIGN1cnJlbnR1bmRlcmNvbG9ycmVt
+b3ZhbCBleGVjIGRlZgoJCW5yZWR0IGluZHggMSAwIGN5bnUgdSBzdWIgbWF4
+IHN1YiByZWR0IGV4ZWMgcHV0CgkJbmdyZWVudCBpbmR4IDEgMCBtYWd1IHUg
+c3ViIG1heCBzdWIgZ3JudCBleGVjIHB1dAoJCW5ibHVldCBpbmR4IDEgMCB5
+ZWx1IHUgc3ViIG1heCBzdWIgYmx1dCBleGVjIHB1dAoJCW5ncmF5dCBpbmR4
+IDEgayBjdXJyZW50YmxhY2tnZW5lcmF0aW9uIGV4ZWMgc3ViIGdyeXQgZXhl
+YyBwdXQKCX0gZm9yCgl7MjU1IG11bCBjdmkgbnJlZHQgZXhjaCBnZXR9Cgl7
+MjU1IG11bCBjdmkgbmdyZWVudCBleGNoIGdldH0KCXsyNTUgbXVsIGN2aSBu
+Ymx1ZXQgZXhjaCBnZXR9Cgl7MjU1IG11bCBjdmkgbmdyYXl0IGV4Y2ggZ2V0
+fQoJc2V0Y29sb3J0cmFuc2ZlcgoJe3BvcCAwfSBzZXR1bmRlcmNvbG9ycmVt
+b3ZhbAoJe30gc2V0YmxhY2tnZW5lcmF0aW9uCgl9IGJpbmQgZGVmCgkvdHJh
+biBGTUxPQ0FMCi9mYWtlY29sb3JzZXR1cCB7CgkvdHJhbiAyNTYgc3RyaW5n
+IGRlZgoJMCAxIDI1NSB7L2luZHggZXhjaCBkZWYgCgkJdHJhbiBpbmR4CgkJ
+cmVkIGluZHggZ2V0IDc3IG11bAoJCWdyZWVuIGluZHggZ2V0IDE1MSBtdWwK
+CQlibHVlIGluZHggZ2V0IDI4IG11bAoJCWFkZCBhZGQgMjU2IGlkaXYgcHV0
+fSBmb3IKCWN1cnJlbnR0cmFuc2ZlcgoJezI1NSBtdWwgY3ZpIHRyYW4gZXhj
+aCBnZXQgMjU1LjAgZGl2fQoJZXhjaCBGbWNjIHNldHRyYW5zZmVyCn0gYmlu
+ZCBkZWYKL0JJVE1BUENPTE9SIHsgCgkvZCA4IGRlZgoJZ3NhdmUKCXRyYW5z
+bGF0ZSByb3RhdGUgc2NhbGUgL2ggZXhjaCBkZWYgL3cgZXhjaCBkZWYKCS9i
+aXRtYXBzYXZlIHNhdmUgZGVmIAoJY29sb3JzZXR1cAoJL2lzIHcgZCB3Ynl0
+ZXMgc3RyaW5nIGRlZgoJL2NmIGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBkIFt3
+IDAgMCBoIG5lZyAwIGhdIAoJe2NmIGlzIHJlYWRoZXhzdHJpbmcgcG9wfSB7
+aXN9IHtpc30gdHJ1ZSAzIGNvbG9yaW1hZ2UgCgliaXRtYXBzYXZlIHJlc3Rv
+cmUgCglncmVzdG9yZQoJfSBiaW5kIGRlZgovQklUTUFQQ09MT1JjIHsgCgkv
+ZCA4IGRlZgoJZ3NhdmUKCXRyYW5zbGF0ZSByb3RhdGUgc2NhbGUgL2ggZXhj
+aCBkZWYgL3cgZXhjaCBkZWYKCS9sYiB3IGQgd2J5dGVzIGRlZiAKCXNsIGxi
+IGx0IHtsYiBtc30gaWYgCgkvYml0bWFwc2F2ZSBzYXZlIGRlZiAKCWNvbG9y
+c2V0dXAKCS9pcyBpbSAwIGxiIGdldGludGVydmFsIGRlZiAKCXdzIDAgbGIg
+Z2V0aW50ZXJ2YWwgaXMgY29weSBwb3AgCgkvY2YgY3VycmVudGZpbGUgZGVm
+IAoJdyBoIGQgW3cgMCAwIGggbmVnIDAgaF0gCgl7aXB9IHtpc30ge2lzfSB0
+cnVlIDMgY29sb3JpbWFnZQoJYml0bWFwc2F2ZSByZXN0b3JlIAoJZ3Jlc3Rv
+cmUKCX0gYmluZCBkZWYKL0JJVE1BUEdSQVkgeyAKCTgge2Zha2Vjb2xvcnNl
+dHVwfSBDT01NT05CSVRNQVAKCX0gYmluZCBkZWYKL0JJVE1BUEdSQVljIHsg
+Cgk4IHtmYWtlY29sb3JzZXR1cH0gQ09NTU9OQklUTUFQYwoJfSBiaW5kIGRl
+ZgovRU5EQklUTUFQIHsKCX0gYmluZCBkZWYKZW5kIAolJUVuZFByb2xvZwol
+JUJlZ2luU2V0dXAKKDIuMCkgRk1WRVJTSU9OCjEgMSA2MTIgNzkyIDAgMSAx
+IEZNRE9DVU1FTlQKL2ZpbGxwcm9jcyAzMiBhcnJheSBkZWYKZmlsbHByb2Nz
+IDAgeyAwLjAwMDAwMCBncmF5bmVzcyB9IHB1dApmaWxscHJvY3MgMSB7IDAu
+MTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyIHsgMC4zMDAwMDAg
+Z3JheW5lc3MgfSBwdXQKZmlsbHByb2NzIDMgeyAwLjUwMDAwMCBncmF5bmVz
+cyB9IHB1dApmaWxscHJvY3MgNCB7IDAuNzAwMDAwIGdyYXluZXNzIH0gcHV0
+CmZpbGxwcm9jcyA1IHsgMC45MDAwMDAgZ3JheW5lc3MgfSBwdXQKZmlsbHBy
+b2NzIDYgeyAwLjk3MDAwMCBncmF5bmVzcyB9IHB1dApmaWxscHJvY3MgNyB7
+IDEuMDAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyA4IHs8MGYxZTNj
+NzhmMGUxYzM4Nz4gOCAxIHNldHBhdHRlcm4gfSBwdXQKZmlsbHByb2NzIDkg
+ezwwZjg3YzNlMWYwNzgzYzFlPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxs
+cHJvY3MgMTAgezxjY2NjY2NjY2NjY2NjY2NjPiA4IDEgc2V0cGF0dGVybiB9
+IHB1dApmaWxscHJvY3MgMTEgezxmZmZmMDAwMGZmZmYwMDAwPiA4IDEgc2V0
+cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTIgezw4MTQyMjQxODE4MjQ0Mjgx
+PiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTMgezwwMzA2MGMx
+ODMwNjBjMDgxPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTQg
+ezw4MDQwMjAxMDA4MDQwMjAxPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxs
+cHJvY3MgMTUge30gcHV0CmZpbGxwcm9jcyAxNiB7IDEuMDAwMDAwIGdyYXlu
+ZXNzIH0gcHV0CmZpbGxwcm9jcyAxNyB7IDAuOTAwMDAwIGdyYXluZXNzIH0g
+cHV0CmZpbGxwcm9jcyAxOCB7IDAuNzAwMDAwIGdyYXluZXNzIH0gcHV0CmZp
+bGxwcm9jcyAxOSB7IDAuNTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9j
+cyAyMCB7IDAuMzAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMSB7
+IDAuMTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMiB7IDAuMDMw
+MDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMyB7IDAuMDAwMDAwIGdy
+YXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyNCB7PGYwZTFjMzg3MGYxZTNjNzg+
+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyNSB7PGYwNzgzYzFl
+MGY4N2MzZTE+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyNiB7
+PDMzMzMzMzMzMzMzMzMzMzM+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxw
+cm9jcyAyNyB7PDAwMDBmZmZmMDAwMGZmZmY+IDggMSBzZXRwYXR0ZXJuIH0g
+cHV0CmZpbGxwcm9jcyAyOCB7PDdlYmRkYmU3ZTdkYmJkN2U+IDggMSBzZXRw
+YXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyOSB7PGZjZjlmM2U3Y2Y5ZjNmN2U+
+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAzMCB7PDdmYmZkZmVm
+ZjdmYmZkZmU+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAzMSB7
+fSBwdXQKJSVFbmRTZXR1cAowIDYgL0hlbHZldGljYSBGTURFRklORUZPTlQK
+JSVQYWdlOiAiMSIgMQolJUJlZ2luUGFwZXJTaXplOiBMZXR0ZXIKJSVFbmRQ
+YXBlclNpemUKNjEyIDc5MiAwIEZNQkVHSU5QQUdFCjcyIDc0NiA1NDAgNzU2
+IFIKNyBYCjAgSwpWCjcyIDMyLjY3IDU0MCA0Mi42NyBSClYKNSBYCjExNSAx
+ODAgNi4xNSA2LjE1IDMyMi42OSA0ODIuOTEgRwowLjUgSAoyIFoKMCBYCjEx
+NSAxODAgNi4xNSA2LjE1IDMyMi42OSA0ODIuOTEgQQozMjAuNDkgNDg5Ljc3
+IDMyMC40OSA0ODIuOTQgMzIzLjE1IDQ4Mi4wNSAzMjcuOTYgNDg2Ljg2IDQg
+TAo3IFgKVgo0NjUuMTQgMTA1LjkxIE0KIDQ2My42MSA4NS45IDQ3MC42IDY2
+Ljk4IDQ3Ni42OCA0OS41NiBECjAgWApOCjQzOCA5My4zNiBNCiA0MzguODEg
+NzguMzcgNDQyLjg2IDY3Ljg0IDQ0Ny43MiA1OS43NCBECk4KNDI0LjIzIDk3
+LjgxIE0KIDQyMy40MiA4MS42MSA0MjUuODcgNjkuMTggNDMwLjczIDYxLjA4
+IEQKTgozOTYuNjkgMTAzLjA4IE0KIDM5NS44OCA4Ni44OCAzOTggODAuNTIg
+NDAxLjI0IDcxLjIxIEQKTgozOTMuNTIgMTE1LjUxIE0KIDQwMi45NiAxMjQu
+MSA0MzMuMTQgMTY4LjQxIDQ0NS4yOSAxNzguMjggRAogNDUxLjI0IDE1My44
+NiA0NDguNzEgMTQ5LjU1IDQ0Ni45MyAxMzguNjcgRAogNDMxLjc1IDEzMC40
+NCA0MjkuNzIgMTIzLjg2IDQ0Mi4yNSAxMjIuNDcgRAogNDQxLjQ5IDExOS4x
+OCA0NDEuMjQgMTE4LjY3IDQ0MC45OSAxMTYuNjUgRAogNDIyLjYzIDExMy45
+OSAzOTUuODggMTE0LjE1IDM5NC40MSAxMTMuODYgRAo0IFgKVgo0NzUuNzkg
+MTU4LjI5IDQ5OS4yMSAxMjAuODIgNDY2LjE3IDExNy4wMyA0NjkuMjEgMTM1
+LjI1IDQgTApWCjMxMC40MiA1NzguMTcgTQogMzA3LjQ4IDU4Ni42NiAzMDcu
+MTggNjAxLjI1IDMxNy43MSA2MDQuOSBECiAzMTQuODcgNjE2LjY0IDMxOC45
+MiA2MTkuMDcgMzIyLjk3IDYyMi43MiBECiAzMjAuMTQgNjMzLjY1IDMyNC4x
+OSA2NDEuNzYgMzMxLjg4IDY0MC41NCBECiAzMjcuMDIgNjU2LjM0IDMzNC43
+MiA2NjMuNjMgMzQxLjYgNjY0Ljg0IEQKIDMzNy41NSA2ODEuODUgMzQyLjQx
+IDY4NC42OSAzNDguNzYgNjg5Ljg3IEQKIDM0Ni4wNiA3MDguNTggMzU3Ljgg
+NzEyLjYzIDM2NS4xIDcxMC4yIEQKIDM2NS41IDcyNy42MiAzNzEuOTggNzI5
+LjY1IDM4MS4zIDczMi40OCBECiAzODAuODkgNzQ2LjY2IDM4OS40IDc0Ni4y
+NSAzOTUuODggNzQ2LjY2IEQKIDM5OS4xMiA3NjQuODggNDEyLjA4IDc2My42
+NyA0MTguNTYgNzYyLjQ1IEQKIDQzMy45NSA3NzQuMiA0NDMuNjcgNzc0LjIg
+NDUwLjUxIDc2Ny41OCBECiA0NzUuNjcgNzczLjM5IDQ3NC4wNSA3NjQuODgg
+NDc2LjE1IDc1OC40MyBECiA0NzYuOTQgNzU3LjkzIDQ3Ny43MiA3NTcuNCA0
+NzguNSA3NTYuODYgRAogNDk2LjMyIDc1OCA0OTEuMDYgNzUxLjExIDQ5MS41
+MSA3NDEuNjUgRAogNTAxLjU5IDczMi40OCA0OTkuOTcgNzI0LjM4IDQ5My44
+OSA3MTcuOSBECiA0OTcuOTQgNzA4Ljk5IDQ5MS44NyA3MDEuNyA0ODcuNDEg
+NzAwLjA4IEQKIDQ4Ny40MSA2ODcuOTMgNDg2LjIgNjg2LjMxIDQ3My4yMyA2
+ODYuMzEgRAogNDczLjY0IDY3Ni41OSA0NzUuNjcgNjcwLjUxIDQ1OS4wNiA2
+NjguODkgRAogNDYxLjg5IDY1Ni4zNCA0NTAuNTUgNjUzLjEgNDQ2LjEgNjUx
+Ljg4IEQKIDQ0OS4zNCA2MzQuMDYgNDQwLjQzIDYzMi4wNCA0MzUuNTcgNjMw
+LjgyIEQKIDQ0MC40MyA2MTcuMDUgNDM0Ljc2IDYxMy40IDQyOC4yOCA2MTEu
+NzggRAogNDMzLjE0IDYwMS42NiA0MjcuMDYgNTk0Ljc3IDQyMC4xOCA1OTEu
+NTMgRAogNDIyLjYxIDU3OC45OCA0MTYuNTMgNTczLjcxIDQxMS4yNyA1Njgu
+ODUgRAogNDE4LjE1IDU1My4wNiA0MDguODQgNTQ1Ljc2IDQwMi44NSA1NDEu
+ODcgRAogNDAzLjE3IDUyOC4zNSAzOTUuNDcgNTIyLjY4IDM4OS44IDUxOS44
+NCBECiAzODcuNzggNTA0LjQ1IDM3OC40NiA1MDQuODYgMzcxLjE3IDUwNC4w
+NSBECjMgWApWCjAgWApOCjIzMi42NSA1NjYuNDIgMjU2LjY3IDU3NC4zOSAy
+MzguMSA1NTMuODIgMyBMCjUgWApWCjIxOS4yMSAxMTcuODcgMjE5LjI5IDEz
+NC4yNiAyMjEuNDMgMTM1LjI3IDIyNS44NiAxMzMuMjMgMjI1LjE4IDExOS41
+NiA1IEwKNCBYClYKMTcxLjkgNzcuMTYgMTc4LjMyIDcyLjMyIDIwMC42NiA1
+MC40MiAyMDEuNTkgNDYuNDYgMjAzLjkgNDYuMzcgMjE4LjUyIDY0LjA0IDIx
+Ny4yNyA3My4wNQogMjEwLjYzIDc2Ljc1IDggTAowIFgKVgoxMzcuOTggODEu
+MjEgMTMyLjkyIDcwLjU3IDEzMS4yNyA2NC4zNyAxNDQuMDUgNTcuOTIgMTQ5
+Ljg4IDcxLjcxIDE1My4yNyA3Ni4zNSAxNTMuMjcgODQuMTIgNyBMClYKMTUz
+LjI3IDEwMi42NyBNCiAxMzIuOTIgMTIwLjE5IDEzMi45MiA5MS44NCAxMzYu
+NDYgODEuNzEgRAogMTQyLjAzIDc4LjE3IDE0OC41OCA3OC43NiAxNTIuNjYg
+NzkuMTggRAo0IFgKVgowIFgKTgoyNzUuNjkgMTAwLjQ0IE0KIDI5MS40MSA5
+Ni41OSAzMjguODUgNzEuMDggMzIwLjI0IDYyLjk4IEQKIDMwOC41OSA1NS44
+OSAyOTEuMDUgNjEuMTQgMjc4LjIyIDU5Ljk0IEQKVgpOCjMwNi4zMiAzMDIu
+NDUgMjIyLjkzIDIyNy4wMSAyMDkuMTYgMjAzLjkzIDE2OS4wNiAyMTMuNjUg
+MTcwLjY4IDIzMS4wNiAxODQuODYgMjUxLjcyIDIyNi41OCAzMDUuNTkgNyBM
+CjQgWApWCjE3Mi43MSA5OC42MiAxNzIuNzEgNzYuNzUgMjE1LjY0IDc2Ljc1
+IDIxNS42NCA5OC4yMiA0IEwKVgowIFgKTgoxOTMuNzcgMTQxLjE1IDIxOS4y
+OSAxMzQuMjYgMjE4Ljg4IDk4LjYyIDE3My4xMiA5OC42MiAxNzMuMTIgMTMx
+LjQzIDUgTAo0IFgKVgowIFgKTgoyODEuMjYgMTYzLjAyIDI4MS4yNiA5Ny40
+MSAyNTguNTcgMTIzLjMzIDI1OC45OCAxNjMuNDIgNCBMCjQgWApWCjAgWApO
+CjI4MS42NiAyNTkuMDEgMjgxLjY2IDE2Ni42NiAyMjQuMTUgMjMxLjA2IDMg
+TAo0IFgKVgowIFgKTgoxNzcuMTYgMjI0LjE4IE0KIDE4My4xNSAyMTkuMzYg
+MTg4LjkxIDIwMy41MiAxOTAuMTMgMTk1LjgzIEQKIDE4Ni44OCAxOTIuNTkg
+MTc3LjI4IDE4OS4wNyAxNzIuMyAxODYuNTEgRAo2IFgKVgowIFgKTgoxNDku
+NjIgNTAuOTYgTQogMTQzLjA0IDM4LjkzIDE2MC4zOSA0MC42MiAxNzUuOTUg
+NDAuMyBECiAxOTEuMDMgMzkuOTkgMjA3LjM0IDM4LjQyIDIwMC41IDUwLjQ1
+IEQKMyBYClYKMCBYCk4KODkuMjggMTQ4LjQ0IE0KIDg3LjUzIDEzOS4yNyA5
+My4zMiAxMzkuOTMgOTQuMTQgMTMzLjg2IEQKIDkzLjYyIDExOC4yNiA5My42
+NyAxMTEuMjIgOTUuMzUgMTAzLjQ4IEQKIDExNi40MSA5Ny44MSAxMjguNTYg
+MTAxLjQ2IDE1NC40OCAxMDIuNjcgRAogMTU1LjcgMTAzLjg5IDE1NC44OSAx
+MDUuMSAxNTUuMjkgMTA3LjEzIEQKIDE2Mi4xOCAxMDUuOTEgMTc2LjQyIDEw
+OC4wOCAxODUuNjcgMTA3LjUzIEQKIDE4Ni44OCAxMTUuMjMgMTg1LjI2IDEx
+OC44NyAxODYuODggMTI4LjU5IEQKIDE5NS4zOSAxMzUuNDggMTk2LjYxIDE0
+MC43NCAxOTUuOCAxNDUuMiBECjMgWApWCjAgWApOCjIzOC43MyA2MjUuOTYg
+TQogMjM0LjU2IDYzNC4yOSAyMzAuMTIgNjM3LjYgMjIxLjUxIDYzOS42MyBE
+CiAyMDEuNzcgNjY4LjQ5IDE2Mi44NyA2NzQuMTYgMTM4LjQyIDY0Ny43MyBE
+CiAxMTQuOTIgNjIyLjMyIDEyMS4yNyA1ODYuNDcgMTUwLjEzIDU1OC42MyBE
+CiAxNjMuMTggNTQ2LjEgMTgyLjAyIDUyMS42NyAxNzcuNDcgNDkwLjc4IEQK
+TgoxNzQuOTQgNTYyLjY4IDE1My4xNyA1ODEuOTEgMTU3LjcyIDU2MC4xNCAx
+NDAuNTEgNTY3Ljc0IDE0OS42MiA1NDIuNDIgNSBMCk4KMTc0LjQzIDU1Mi4w
+NCBNCiAxNjAuNjggNTY5LjUxIDEzNC45NCA1MzAuNzggMTY5LjM3IDUzNC4z
+MiBECjcgWApWCjAgWApOCjE1OS4yNCA1MzkuODkgTQogMTYwLjc2IDU1My4w
+NiAxNzAuMzggNTQ3LjQ5IDE3My40MiA1NDYuOTggRApOCjE3MC4zOCA1MzYu
+MzUgTQogMTY0LjMgNTM4Ljg4IDE2NS4zMiA1NDYuNDcgMTcyLjkxIDU0Ni40
+NyBECk4KNyBYCjkwIDQ1MCAxOC40OCAxOC40OCAyMzcuNDYgNjA5LjUxIEcK
+MCBYCjkwIDQ1MCAxOC40OCAxOC40OCAyMzcuNDYgNjA5LjUxIEEKNyBYCjkw
+IDQ1MCAyMy4yOSAyMi4yOCAyMDkuMzYgNTk0LjA2IEcKMCBYCjkwIDQ1MCAy
+My4yOSAyMi4yOCAyMDkuMzYgNTk0LjA2IEEKOTAgNDUwIDIuNzggMi43OCAy
+MjEuNzcgNTgzLjY5IEcKOTAgNDUwIDIuNzggMi43OCAyMjEuNzcgNTgzLjY5
+IEEKOTAgNDUwIDIuNzggMi43OCAyNDYuNTggNTk5Ljg5IEcKOTAgNDUwIDIu
+NzggMi43OCAyNDYuNTggNTk5Ljg5IEEKMjMyLjY1IDU2Ni40MiBNCiAyMDku
+NTcgNTU1LjA4IDE3OC43OSA1MjkuOTcgMjAyLjI4IDUwMi45MyBECiAyMjIu
+NjEgNDc5LjYgMjQ5LjM2IDQ4My4xOSAyNTAuODggNTE3LjExIEQKIDI1NC45
+MyA1MjIuMTcgMjYwLjUgNTE4LjEyIDI1OS45OSA1MzIuMyBECjUgWApWCjAg
+WApOCjI1NC45MyA1NzQuMzIgTQogMjczLjE1IDU3NS44NCAzMDMuNjMgNTUy
+Ljk4IDI3My42NiA1MzcuODcgRAogMjUzLjQxIDUyNy43NCAyMjUuMDYgNTIz
+LjY5IDIwMy43OSA1MjUuNzIgRAo1IFgKVgowIFgKTgoyMDcuMzQgNTI5LjI2
+IDIwMC43NiA1MjEuMTYgMiBMCk4KMTY3LjM0IDY2My45MyBNCiAxNTYuOTQg
+Njc5IDEyNS4zMiA2NjIuOTIgMTQyLjAzIDYzOC42MiBECk4KMTQ5LjYyIDY1
+Ni44NCBNCiAxMzAuODkgNjYzLjkzIDExNS4yIDY0NS43IDEyNy44NSA2MjYu
+OTcgRApOCjE3Ny45OCA0OTguODggTQogMTUwLjY0IDQ5Ny4zNiAyMDkuMzYg
+NDc0LjU4IDIzMC4xMiA0NjguNTEgRApOCjE3MC45OSA0OTYuNzYgMTUyLjE1
+IDQ2OC41MSAyIEwKTgoxNTIuMTUgNDY4LjUxIE0KIDE3My40MiA0NDguNzYg
+MTk3LjcyIDQ0MC4xNiAyMjQuMDUgNDM3LjYzIEQKTgoyMzAuMTIgNDY4LjUx
+IDIyNC4wNSA0MzcuNjMgMiBMCk4KMjQxLjIgNDkyLjM3IDI1MC44OCA0ODgu
+MjUgMjYxIDQ4MS42NyAzIEwKTgoyNDEuMiA0OTIuMzcgTQogMjQ5Ljg3IDQ3
+My4wNiAyNzQuNjcgNDI4LjAxIDI3Ny4yIDM5OC4xNCBECiAzMDMuNTMgMzc5
+LjQgMzE0Ljc5IDM1MC41MiAzMTQuMTYgMzEwLjU1IEQKIDMxMi42NCAyOTMu
+ODQgMjM4LjQyIDI5MC43IDIxNS42NCAyOTAuMiBECjcgWApWCjAgWApOCjE1
+Ni4yMSA0NjQuOTYgMTQ1LjA3IDQ1MS4yOSAyIEwKTgoxMTUuMiAzODYuNDkg
+TQogMTA0LjQ3IDM5My4wMyA5Ny45OCAzOTcuNjMgOTUuNDUgNDA1LjczIEQK
+IDk2LjQ2IDQyNS45OCAxMzguNDkgNDY1LjQ3IDE2OC4zNiA0NDcuNzUgRAog
+MTg0LjA0IDQzOC4zNSAxNzcuNzIgMzk3LjY1IDE2OC44NiAzODUuNDggRApO
+CjEwNi4wOCAzMTEuMDUgNzYuMjEgMzUxLjU2IDg3Ljg2IDM1MC4wNCA4Ny44
+NiAzNjIuMTkgOTguNDkgMzU2LjYyIDk3LjQ4IDM3Mi4zMiAxMDcuMSAzNjMu
+NzEKIDEwNS4wNyAzODEuOTMgMTE3LjczIDM3MS4zIDExNi4yMSAzODkuMDIg
+MTI4Ljg3IDM3OS40IDEyNy44NSAzOTUuNiAxMzguOTkgMzg3IDEzOC40OSA0
+MDEuNjggMTQ5LjYyIDM5Mi41NwogMTUwLjY0IDQwNS4yMiAxNTYuMjEgMzk2
+LjExIDE2MC4yNiA0MDUuMjIgMTggTApOCjk4LjQ5IDM5OC4xNCBNCiA2NC41
+NyAzNjAuNjcgNjQuOTIgMjkzLjQzIDkzLjkzIDI1NC4zNSBECk4KMTA2LjA4
+IDMxMS4wNSBNCiAxMTMuNjggMzAyLjk1IDExMy42OCAyOTcuODkgMTIwLjc3
+IDI4Ny43NyBECk4KMTIwLjc3IDI4Ny43NyBNCiAxNTAuMTMgMjk1LjM2IDE2
+My44IDI2MS40NCAxMjQuODIgMjY3LjAxIEQKTgozMDYuMzIgMzAyLjQ1IE0K
+IDMwMS43NyAyNjYgMjYyLjUyIDIzOC42NiAyMjYuNTggMjMxLjA2IEQKNCBY
+ClYKMCBYCk4KMTYwLjI2IDQwNS4yMiBNCiAxNjguMTIgMzg3Ljk2IDE3MS45
+IDM2Ny41NiAxODUuMDcgMzUwLjEgRAogMTkyLjk2IDM1MC41NSAxOTIuOTYg
+MzQ5LjMzIDE5NC4xOCAzNDQuOTggRApOCjkwIDQ1MCAxNC41OCAxNy44MiAx
+OTEuNzUgMzI2LjY1IEEKOTAgNDUwIDIuNjMgMi42MyAxODkuOTIgMzI5LjI4
+IEcKOTAgNDUwIDIuNjMgMi42MyAxODkuOTIgMzI5LjI4IEEKNyBYCjkwIDQ1
+MCAyMC40NSAyMS4wNiAxNjcuMjQgMzEzLjI4IEcKMCBYCjkwIDQ1MCAyMC40
+NSAyMS4wNiAxNjcuMjQgMzEzLjI4IEEKOTAgNDUwIDIuNjMgMi42MyAxNTcu
+NTIgMzEzLjA4IEcKOTAgNDUwIDIuNjMgMi42MyAxNTcuNTIgMzEzLjA4IEEK
+MTg3LjcgMzA5LjY0IE0KIDIyMC45MSAzMjguNjcgMjIwLjEgMjk5LjUxIDE5
+OS44NSAyOTYuNjggRAo3IFgKVgowIFgKTgoyMDguNzYgMzAwLjMyIE0KIDIx
+NS42NCAyOTAuMiAyMTQuNDMgMjkyLjIyIDIyNS4zNiAyODAuMDcgRAogMjEz
+LjYyIDI1OC42IDE3OC4zOCAyNjMuMDYgMTYyLjU4IDI2NC42OCBECjcgWApW
+CjAgWApOCjE1OC4xMyAyNjMuMDYgMTY0LjIgMjY3LjUyIDIgTApOCjE5Mi45
+NiAyNjMuODcgTQogMTkyLjU2IDI1OS4wMSAxOTAuMTMgMjU2LjU4IDE4MS4y
+MSAyNTYuMTcgRAogMTgwIDI1MC45MSAxNzYuNjYgMjM3Ljk4IDE3Mi41NCAy
+MzMuMjQgRAo0IFgKVgowIFgKTgoxNTIuODYgMjM4LjM1IE0KIDE2My44IDIz
+MS4wNiAxNzEuMDkgMjM1LjExIDE3NS45NSAyMzQuMyBECiAxNzcuNTcgMjE4
+LjkxIDE3NS41NCAyMTAuODEgMTc2LjM1IDE5OS44OCBECiAxOTcuNTUgMTcx
+LjEgMjM2LjI5IDEzNS45NCAxNDIuMzMgMTM3LjEgRAogMTA5LjUzIDEzNy41
+IDU2LjQ3IDE0MS4xNSAxMDQuMjYgMTkxLjM3IEQKNiBYClYKMCBYCk4KMTU0
+Ljg5IDExNC40MiAxNTUuMjkgMTA2LjMyIDIgTApOCjEwNC4yNiAxOTUuMDIg
+TQogMTA2LjU2IDE3NC4wOSAxMDIuMjQgMTM5LjEyIDEwNi4yOSAxMjMuNzMg
+RAogMTEwLjA5IDExOS40OSAxMTQuMjkgMTE4LjkxIDEyNC4xMSAxMTkuNjgg
+RAogMTMyLjIxIDEwNS4xIDEzOC42OSAxMDcuOTQgMTI3LjQ3IDEzNC44NyBE
+CiAxMjcuNDcgMTQ5LjA1IDEyNi45NCAxNzIuNzQgMTMyLjYxIDE4OS43NSBE
+CjcgWApWCjAgWApOCjEyNC4xMSAxMTkuNjggTQogMTEyLjM2IDg1LjY2IDEw
+NS44OCAxMDYuNzIgMTExLjU1IDEwOS4xNSBECjcgWApWCjAgWApOCjEwOS4z
+NyAxMjYuNzcgMTA0LjU3IDEyMy4xIDEwMC44OSAxMTQuMTEgMTE3LjIyIDEx
+MC4xOSAxMjIuMDMgMTE2LjUyIDEyNS41NyAxMjMuOTkgNiBZCjcgWApWCjEw
+OC43MiAxMTguNDcgTQogOTEuMyAxMDUuOTEgMTE0Ljc5IDEwMy4wOCAxMTYu
+NDEgMTE2LjQ0IEQKVgowIFgKTgoxMDYuMjkgMTI0Ljk1IE0KIDk3Ljc4IDEy
+MS4zIDk0Ljk0IDEwOS4xNSAxMDMuNDUgMTE0LjAxIEQKNyBYClYKMCBYCk4K
+MTA3LjAzIDIyMS42MSBNCiAxMDAuNjIgMjExLjIyIDk5IDIwMy4xMiA5OS40
+IDE5OC4yNiBECiAxMDcuMjEgMTkxLjUyIDEyOS4zNyAxODguOTQgMTQwLjcx
+IDE5MC4xNiBECiAxNDUuMTcgMTk4LjY2IDEzOC4wOCAyMTIuNjkgMTQxLjAy
+IDIyMC40MyBECjYgWApWCjAgWApOCjExOS4yNSAyNDcuNzcgTQogMTI5LjYg
+MjQwLjcyIDEzNy45IDIzNy40NSAxNDQuMDQgMjM2LjcgRAogMTYwLjkxIDIz
+NC42MyAxNjIuMjggMjUyLjgzIDE0MC41MSAyNTguNCBECiAxMjguMjMgMjYx
+LjYxIDEzMC44OSAyNjIuOTYgMTI0LjgyIDI2Ny4wMSBECjcgWApWCjAgWApO
+CjkzLjkzIDI1NC4zNSBNCiA4OS44OCAyMTkuOTMgMTM5LjUgMTk1LjYyIDE0
+MS4wMiAyMjAuNDMgRAo3IFgKVgowIFgKTgoxMTUuNyAyMzAuNTYgTQogMTI4
+LjM2IDIxMy4zNCAxNjcuMzQgMjE5LjQyIDE0Ny42IDIzNi4xMyBECjcgWApW
+CjAgWApOCjE3Mi43MSAxMDcuMjggMTcyLjcxIDc2Ljc1IDIgTApOCjE1My4y
+NyAxMDIuNjcgMTUzLjI3IDc2LjM1IDIgTApOCjE1My4yNyA3Ny4xNiBNCiAx
+NjIuOTkgNjguMjQgMTcyLjcxIDczLjEgMTcxLjkgNzcuMTYgRAogMTk4LjIz
+IDY3LjAzIDEzNC4yMyA2NC42IDE1My4yNyA3Ny4xNiBECjcgWApWCjAgWApO
+CjcgWAo5MCA0NTAgNS44NyA1LjQ3IDE1Ny41MiA2MS41NiBHCjAgWAo5MCA0
+NTAgNS44NyA1LjQ3IDE1Ny41MiA2MS41NiBBCjE3OC4zMiA3Mi4zMiBNCiAx
+ODkuMjYgNjguNjcgMjAwLjI1IDYwLjk2IDIwMC42NiA1MC40MiBECjcgWApW
+CjAgWApOCjIwMC42NiA1MC40MiAxNTIuMDUgNTAuNDIgMiBMCk4KMTMwLjE4
+IDcyLjcgMTMwLjE4IDEwMC42NSAyIEwKTgoxMDguMzEgOTQuMTcgMTA4LjMx
+IDcyLjI5IDIgTApOCjEwOC4yNCA5NC41IE0KIDEwNS45NiA5Ni42NSAxMDQu
+NTcgOTcuNjYgMTA0LjY5IDEwMS4zMyBECk4KMTI4LjQ5IDQyLjg1IE0KIDEz
+MC40NiA0Mi4xNSAxMzAuMiA2NS45MSAxMzEuOSA2NS4xMyBECiAxNDMuOCA1
+OS42OCAxNTMuMjIgNDkuODcgMTUyLjg2IDQzLjEzIEQKNyBYClYKMCBYCk4K
+MTQ5Ljg4IDcxLjcxIE0KIDE0Ni43MSA2Ny4wMyAxNDUuNyA2Ni42NSAxNDQu
+MDUgNTcuOTIgRAo3IFgKVgowIFgKTgoxMDIuNjQgNDUuMTYgTQogOTcuMzgg
+MzMuODIgMTEzLjQgMzMuNzQgMTI4Ljk3IDMzLjQxIEQKIDE0NC4wNSAzMy4x
+IDE1OS43NSAzMS43OSAxNTIuNDYgNDIuMzIgRAozIFgKVgowIFgKTgoxMDQu
+MjYgNjYuNjMgTQogMTAxLjgyIDU1LjI4IDEwMi4yNCA0OC40IDEwMy4wNSA0
+NC43NSBECiAxMjAuODcgNDAuMyAxNDAuNzYgNDIuMSAxNTIuOTEgNDIuMzUg
+RAo3IFgKVgowIFgKTgoxMDguMzEgNzIuMjkgTQogMTE3LjIyIDY4LjY1IDEy
+My4zIDcwLjY3IDEzMC4xOCA3Mi43IEQKIDE0OCA1OC41MyA4Ni4wOSA2MC4y
+IDEwNy43MyA3NS4xMyBECjcgWApWCjAgWApOCjEyMi42NiA2Mi4zNSAxMjgu
+ODcgNjIuODUgMTMwLjY0IDYzLjk5IDEzMS4yNyA2NC4zNyAxMzEuOSA2NC4y
+NSAxMzMuNDIgNjMuNjEgMTMyLjQxIDUxLjQ2CiAxMjUuOTYgNTIuMjIgOCBM
+CjcgWApWCjIyMS4zMSAxMzUuMDcgMjA5LjU3IDEyOC41OSAyMDkuNTcgMTM2
+LjI5IDE5NS4zOSAxMzAuMjEgMTk2LjIgMTQ2LjgyIDE3Ny41NyAxNTMuMyAx
+ODcuMjkgMTY4LjY5CiAxNjkuNDcgMTgxLjI1IDE5MC4xMyAxOTUuODMgMTg1
+LjY3IDIxNi4wOCAyMTEuNTkgMjEzLjI0IDIyMC41IDIzNi4zMyAyMzkuNTQg
+MjIxLjM0IDI2MS40MSAyMzIuMjggMjYyLjIyIDIxMi4wMwogMjc4LjQyIDIx
+NC40NiAyNzIuNzUgMTk1LjAyIDI4NS43MSAxODYuOTIgMjc2LjggMTc2LjM5
+IDI4Ni4xMSAxNjUuMDQgMjY4LjQ3IDE1Ny4yNyAyMSBMClYKMCBYCk4KMjEx
+LjU5IDE0NS4yIE0KIDIyMi41MyAxMzMuMDUgMjE5LjY5IDEzNC42NyAyNDAu
+MzUgMTI5LjQgRApOCjkwIDQ1MCAxNC40MiAxNC45OSAyMTkuNDUgMTcyLjc0
+IEEKOTAgNDUwIDEzLjE2IDEzLjE2IDI1My41MSAxODEuODUgQQo5MCA0NTAg
+Mi4wMyAyLjAzIDI1My43MSAxODguNTQgRwo5MCA0NTAgMi4wMyAyLjAzIDI1
+My43MSAxODguNTQgQQo5MCA0NTAgMi4wMyAyLjAzIDIyMi45MyAxODAuMDMg
+Rwo5MCA0NTAgMi4wMyAyLjAzIDIyMi45MyAxODAuMDMgQQoxOTcuODIgMTc0
+Ljc3IDIwNS4xMSAxNzQuMzYgMiBMCk4KMjA1LjUyIDE5MC41NiAyMTAuNzgg
+MTg0LjQ5IDIgTApOCjIyNS4zNiAxOTYuMjMgMjI0LjE1IDE4Ni45MiAyIEwK
+TgoyNDAuMzUgMTk4LjI2IDI0NC44IDE5Mi4xOCAyIEwKTgoyNTQuNTIgMjAx
+LjkgMjU0LjUyIDE5NC42MSAyIEwKTgoyNjguNyAxOTcuODUgMjYzLjg0IDE5
+MC4xNiAyIEwKTgoyMzkuMTMgMjIwLjUzIE0KIDI0My4xOCAyMTQuNDYgMjQy
+Ljc4IDIxMC40MSAyNDAuMzUgMjA1LjU1IEQKTgoyMTEuNTkgMjExLjYyIE0K
+IDIxMS41OSAyMDUuMTQgMjA5LjE2IDIwMy45MyAyMTcuNjcgMTk3LjA0IEQK
+TgoyMTcuNjcgMTk3LjA0IDIyNi45OCAyMDMuNTIgMjMzLjQ2IDIwNS4xNCAy
+NDAuMzUgMjA1LjU1IDQgTApOCjIzOS4xMyAyMjAuNTMgMjMxLjQ0IDIxMi40
+MyAyMjYuOTggMjEwLjgxIDIxMS41OSAyMTEuNjIgNCBMCk4KMjMxLjQ0IDIx
+Mi40MyAyMzMuNDYgMjA1LjE0IDIgTApOCjIyNi45OCAyMTAuODEgMjI2Ljk4
+IDIwMy41MiAyIEwKTgoyMDQuMyAxNTQuNTIgTQogMTg5LjcyIDE1My4zIDIw
+My45IDEzOC43MiAyMTEuNTkgMTQ1LjIgRApOCjI0MC43NSAxNjkuOTEgTQog
+MjUzLjcxIDE4MC44NCAyNTguOTggMTY0LjIzIDI0My45OSAxNjAuMTggRAo3
+IFgKVgowIFgKTgoyMDQuMTEgMTUxLjY4IE0KIDE5OC44OCAxNTEuMjQgMjAz
+Ljk3IDE0Ni4wMSAyMDYuNzMgMTQ4LjM0IEQKTgoyNjUuNDYgMTY3LjQ3IE0K
+IDI3My41NiAxNjguNjkgMjcwLjMyIDE1NS43MyAyNjQuNjUgMTU1LjMzIEQK
+TgoyNjUuNTkgMTY1LjQ1IE0KIDI2OC4zIDE2NS44NSAyNjcuMjEgMTYxLjUz
+IDI2NS4zMiAxNjEuNCBECk4KMjU2LjU1IDE1MS4yOCBNCiAyNDIuMzcgMTc5
+LjYzIDIxOC4yMiAxMTguMjkgMjUzLjc5IDEzMC4xOSBECk4KMjY0LjY1IDE1
+NS4zMyBNCiAyNjMuNDMgMTUyLjA5IDI2My40MyAxNTAuMDYgMjYwLjYgMTQ3
+LjIyIEQKNyBYClYKMCBYCk4KMjI1LjM2IDEzMy44NiAyMjUuMzYgMTIwLjkg
+MiBMCk4KMjI1LjM2IDEyMC45IE0KIDIwNi43MyAxMTIuOCAxOTcuNDIgODEu
+MjEgMjE2LjQ1IDczLjEgRAo3IFgKVgowIFgKTgoyMjMuMzQgOTIuNTUgTQog
+MjE3LjE1IDgyLjQ4IDIxNC44MyA2OS4wNSAyMjAuNSA1OS43NCBECk4KMjE4
+LjQ4IDYzLjk5IE0KIDIxMS41OSA2NC40IDIwMy4wOSA1NS4yOCAyMDMuNDkg
+NDUuOTcgRAo3IFgKVgowIFgKTgoyMDMuOSA0Ni4zNyBNCiAxNzguNzkgNDcu
+OTkgMTY3Ljg1IDI4LjE1IDIxNC44MyAyOS4zNiBECjcgWApWCjAgWApOCjIx
+NC44MyAyOS4zNiBNCiAyMjguMiAxOC4wMiAzMDcuOTkgMjIuODggMjgwLjQ1
+IDQ1LjE2IEQKTgoyNTguNTcgMTIzLjMzIDI1OC41NyAxMzEuODMgMiBMCk4K
+MjQ4LjQ1IDE1MC44NyBNCiAyNjkuMTEgMTU3LjM1IDI2OC4zIDEyMy43MyAy
+NDkuMjYgMTMxLjQzIEQKNyBYClYKMCBYCk4KMjQ4LjQ1IDE0My45OCBNCiAy
+NjQuNjUgMTQ1LjIgMjU1Ljc0IDEzNS44OCAyNDguNDUgMTM3LjUgRApOCjI2
+NC4zIDE1Ni42NCAyNjIuOSAxNTEuOTYgMjYxLjM4IDE0OC45MiAyNTkuMzYg
+MTUwLjQ0IDI1Ny43MSAxNTEuNzEgMjU2LjQ1IDE1Ny4yNyA2IEwKNyBYClYK
+MjU1LjE4IDEyMy4xIDI1OS4zNiAxMjMuMSAyNTkuMzYgMTE5Ljk0IDMgTApW
+CjE5MS4zOSAyNTguMjggMTg4LjQ4IDI1Ni44OCAxODUuNyAyNTYuMTIgMTgz
+LjU0IDI0OS4wNCAxOTIuOTEgMjQ4LjkxIDE5NC45MyAyNDkuMTYgNiBMCjQg
+WApWCjE5MS45IDI2Mi43MSAxOTEuNjQgMjYwLjkzIDE5MC42MyAyNTkuNDEg
+MTg4Ljg2IDI1OC4yOCAxODYuMDcgMjU5LjQxIDUgTAo3IFgKVgoyODEuMTMg
+ODMuMjMgMjczLjAzIDgyLjcyIDI3My40MSA3MC45NSAyODIuNjUgNjYuNzgg
+NCBMCjAgWApWCjI3NC43NyA3Mi43IE0KIDI4NC4wOSA2Mi4xNyAyODQuMDkg
+NTAuODMgMjc5LjIzIDQwLjMgRAo3IFgKVgowIFgKTgoyNTguNiAxMjMuNzMg
+TQogMjgyLjUgMTIyLjExIDI4Ni43MyA5Mi4xNCAyNzQuMzcgNzguMzcgRAo3
+IFgKVgowIFgKTgoyNjcuMDggMTA3LjEzIE0KIDI3My4xOCA5Ny4wNSAyNzYu
+OCA3Ny41NiAyNzMuOTYgNjYuNjMgRAo3IFgKVgowIFgKTgoxMzcuNiA4MC44
+MyAxMzIuOTIgNzAuNTcgMiBMCk4KMjE3LjEyIDYyLjM1IDIwNi44MyA1Mi45
+OCAyMDQuNjggNDguNTUgMjA0LjQzIDQzLjg3IDIxOC44NiA0OC41NSA1IEwK
+NyBYClYKMjMwLjEyIDQ2OC41MSAyNTEuODkgNDQ2LjIzIDI2MSA0ODEuNjcg
+MjcyLjY1IDQ0Ni4yMyAyNjAuNSA0NTEuMjkgNSBMCjAgWApOCjIyNy44NCAx
+MjAuOTUgMjIzLjQxIDExOS4zIDIyNC40MyAxMTUuNzYgMyBMCjcgWApWCjI1
+Mi45IDU5OC40MiBNCiAyNzguNDIgNTg5LjkxIDI2MSA1NjkuNjYgMjMyLjY1
+IDU2Ni40MiBEClYKMCBYCk4KNSBYCjkwIDQ1MCA2LjQ4IDYuNDggMzYxLjQ1
+IDQ3Ny4zMiBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDM2MS40NSA0NzcuMzIg
+QQo1IFgKOTAgNDUwIDYuNDggNi40OCAzNDguMDggNDc0Ljg5IEcKMCBYCjkw
+IDQ1MCA2LjQ4IDYuNDggMzQ4LjA4IDQ3NC44OSBBCjUgWAo5MCA0NTAgNi40
+OCA2LjQ4IDMzMy4xIDQ3NC4wOCBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDMz
+My4xIDQ3NC4wOCBBCjUgWAowIDc4IDYuMTUgNi4xNSAzNzAgNDg5LjM5IEcK
+MCBYCjAgNzggNi4xNSA2LjE1IDM3MCA0ODkuMzkgQQozMTguMTEgNTg1LjA1
+IE0KIDMyMS4zNSA1ODcuODkgMzIxLjc2IDU4OC4yOSAzMjIuNTcgNTkzLjU2
+IEQKIDMyOS40NSA1ODkuOTEgMzMyLjY5IDU5My41NiAzMzUuMTIgNTk4LjAx
+IEQKIDMzOS41OCA1OTEuOTQgMzQ0LjAzIDU5NS4xOCAzNDcuMjcgNTk4LjAx
+IEQKIDM0OC4wOCA1ODcuNDggMzU3LjQgNTg4LjcgMzY0LjI5IDU4OS41MSBE
+CiAzNjMuNDggNTc4LjE3IDM3Mi43OSA1ODIuNjIgMzc1LjIyIDU4MC42IEQK
+IDM3MS45OCA1NjguNDUgMzc5LjY3IDU2OC44NSAzODAuODkgNTY3LjY0IEQK
+IDM3NS4yMiA1NjYuMDIgMzcwLjYgNTU2LjggMzc3LjY1IDU1MS44NCBECiAz
+NjkuOTUgNTQwLjEgMzc4Ljg2IDUzNi44NSAzODAuODkgNTM2LjQ1IEQKIDM5
+MC42MSA1NDkuNDEgNDAwLjc0IDUxNC41OCAzNzMuMiA1MjAuMjUgRAo3IFgK
+VgowIFgKTgo3IFgKOTAgNDUwIDE4LjQzIDIxLjA2IDMyNS4yIDU2NS42MSBH
+CjAgWAo5MCA0NTAgMTguNDMgMjEuMDYgMzI1LjIgNTY1LjYxIEEKMzMyLjI5
+IDU1Mi4yNSBNCiAzMDUuNTYgNTc3LjM2IDI5Ni4yNCA1NDAuMSAzMjIuNTcg
+NTQwLjEgRAo3IFgKVgowIFgKTgo5MCA0NTAgMi43OCAyLjc4IDMxNC4yMSA1
+NzMuMTUgRwo5MCA0NTAgMi43OCAyLjc4IDMxNC4yMSA1NzMuMTUgQQozNDQu
+NDQgNTgxLjQxIDM0MS42IDU4OC4yOSAyIEwKTgozNTQuOTcgNTgxIDM1Ny44
+IDU4Ni4yNyAyIEwKTgozNjMuNDggNTc0LjEyIDM2Ny45MyA1NzkuNzkgMiBM
+Ck4KMzY3LjkzIDU2MS45NyAzNzIuMzkgNTY0LjggMiBMCk4KMzI4LjY0IDU4
+Ny40OCAzMjkuMDUgNTk0Ljc3IDIgTApOCjMwOC4zOSA1NzQuOTMgMzAzLjk0
+IDU3OC41NyAyIEwKTgozMTMuMjUgNTgxLjgxIDMwOC44IDU4Ny40OCAyIEwK
+TgozMTkuNzMgNTg2LjI3IDMxOC4xMSA1OTEuOTQgMiBMCk4KMzM1LjQzIDU0
+NS40NiAzMzMuMjcgNTQ4LjM3IDMyNC41NCA1NDYuOTggMzI1LjA1IDU0MC41
+MiA0IEwKNyBYClYKMzc4LjA1IDUzMC4zNyBNCiAzODYuOTcgNTM1LjY0IDM4
+Ni41NiA1MjQuMyAzNzguMDUgNTI0LjMgRAowIFgKTgozMDguOSA1MjcuMjkg
+TQogMzEyLjg1IDUxOS40NCAzMTIuODUgNTE5LjQ0IDMyMC41NCA1MTMuNzcg
+RApOCjM0MC43NCA1NDEuMTYgTQogMzQ4LjcyIDUzNy45OSAzNTUuMTcgNTQw
+LjI3IDM1OC40NyA1NDEuNzYgRAogMzY3LjU5IDU0NS45MSAzNjUuMDkgNTI3
+LjM3IDM0My4yNyA1MjkuNTEgRAo3IFgKVgo5MCA0NTAgMTguNDMgMjEuMDYg
+MzQ5LjEgNTYwLjc1IEcKMCBYCjkwIDQ1MCAxOC40MyAyMS4wNiAzNDkuMSA1
+NjAuNzUgQQo5MCA0NTAgMi43OCAyLjc4IDM0MS4zNSA1NjUuMDUgRwo5MCA0
+NTAgMi43OCAyLjc4IDM0MS4zNSA1NjUuMDUgQQozNDYuMTggNTMxLjU0IDM2
+My4yNyA1MzYuMSAzNzIuNzcgNTIyLjE3IDM3MS4zNyA1MDIuNDMgMzQ1LjQz
+IDUyOS4xMyA1IEwKNyBYClYKMzA3LjE4IDU0OC42IE0KIDI1MC44OCA1MzQu
+ODMgMzQ2LjU0IDUxMy4wNSAzNjQuNjkgNTI3Ljk0IEQKMCBYCk4KMzYzLjA3
+IDUzMS4xOCBNCiAzNjUuOSA1MjkuNTYgMzY1LjkgNTI3LjEzIDM2NS41IDUy
+My40OSBECk4KMzczLjIgNTIwLjI1IDM3MC43NyA0ODkuODcgMiBMCk4KNSBY
+CjkwIDQ1MCA2LjQ4IDYuNDggMzcyLjc5IDQ4My44IEcKMCBYCjkwIDQ1MCA2
+LjQ4IDYuNDggMzcyLjc5IDQ4My44IEEKMzE5LjIzIDQ3MC41MyBNCiAzMTUu
+MTggNDM2LjExIDI5NS45NCA0NDQuNzEgMjk2Ljk1IDQxNC4zNCBECiAyOTgu
+MDcgMzgwLjc1IDMzNC40MSAzNzkuNCAzMjMuMjggMzE5LjE2IEQKIDMwOC4y
+NCAyMzguNzQgMjk3Ljk2IDIyMS45NSAyOTkuOTkgMTUzLjEgRAogMzA3LjA4
+IDEzOS45MyAzNjMuNzggMTI5LjMgMzkxLjEyIDEzNC4zNiBECk4KMzc0LjQx
+IDQ3Ny4xMSBNCiAzOTcuNyA0NDAuNjYgNDQ5Ljg1IDQxMy44MyA0MjAuNDgg
+MzQ4LjUyIEQKTgozNjUuMyA0MzUuNiBNCiAzODYuMDUgNDIwLjkyIDM5NS40
+MiAzOTMuMDcgMzkwLjM2IDM3MS44MSBECk4KMzgwLjk5IDQxOC44OSBNCiAz
+ODIuNTEgMzk1LjEgMzg4LjA4IDM3Mi4zMiAzOTguMiAzNTQuNiBECk4KMzA1
+LjQzIDQ0MC4wMyBNCiAzMjUuNjggNDMxLjkzIDM0OC40NiA0MjkuMjcgMzY4
+LjcxIDQzMi44MiBECk4KMjY0LjA0IDQ0My4xOSBNCiAyODkuMzYgNDQwLjE2
+IDI5OC40NyA0NjQuNDYgMzE4LjIxIDQ2NC40NiBECk4KMjc1LjE4IDQwOS43
+OCBNCiAyODYuMzIgNDExLjggMjg2LjcgNDEyLjA2IDI5Ni44MiA0MTguNjQg
+RApOCjI2OS42MSA0NTUuODUgMjg5LjM2IDQ1MC4yOCAyIEwKTgoyOTMuNDkg
+MzgzLjAyIDMxMS41MiAzODEuNDIgMiBMCk4KMjg3Ljg0IDQxMi44MiAyODYu
+MzggMzkwLjYzIDIgTApOCjQyMi41MSA0NDUuNzMgTQogNDUxLjM2IDQ0MC4x
+NiA0MDguODQgNDAwLjY3IDQwOS44NSA0MjkuNTIgRAo3IFgKVgowIFgKTgo0
+MDguMzMgNDY0LjQ2IE0KIDQ0NS4zNCA0NjcuOSA0MDIuODkgNDAwLjY3IDM5
+NS4yOSA0NDMuNyBECjcgWApWCjAgWApOCjM4Mi42NCA0NjYuODYgTQogNDQy
+LjM4IDQ5My43IDM4NC45MiA0MDguMzkgMzg2Ljk0IDQ2MS41NSBECjcgWApW
+CjAgWApOCjM4MiA0NjYuNDggMzg2LjMxIDQ2MS40MiAyIEwKTgoyNDguNDUg
+MTUwLjg3IDI0OC40NSAxNDMuOTggMiBMCk4KMjQ4LjQ1IDEzNy41IDI0OS4y
+NiAxMzEuNDMgMiBMCk4KMzI0LjMzIDE0MC4xOCBNCiAzMjYuMjEgMTE1LjYz
+IDMyMy43OCAxMDAuMjQgMjk2LjA0IDkwLjkgRApOCjMxMi4zIDc5LjA0IE0K
+IDMzNS4xMiA4NC40NSAzNDcuMjcgOTAuOTMgMzUyLjEzIDk1LjM4IEQKIDM1
+Mi41NCA5MC4xMiAzNTEuMzIgODUuMjYgMzUxLjczIDc5Ljk5IEQKIDM0OC4z
+NSA3Ny4xNiAzNDMuNjMgNzQuNzIgMzM2LjAzIDc1LjI4IEQKIDMzNC41NCA3
+NS4xNyAzMzguNSA3OS43OSAzMzUuNTMgNzkuMTggRAogMzMxLjcgNzguMzkg
+MzI1LjMxIDcyLjk5IDMxOC4xMSA3MS44OSBECjIgWApWCjAgWApOCjM0OS43
+IDEzNS4wNyAzNTIuMTMgOTUuMzggMiBMCk4KMzc3LjY3IDEzMy4xNCBNCiAz
+NzguNjMgMTA5LjcgMzk0LjYgODYuNjMgMzcxIDc4LjU2IEQKIDM1Ni4xMiA3
+My41IDM1Mi42OSA2Ni4xMSAzNDguNDkgNjMuMzkgRApOCjM4Mi45MiA1OC41
+MyBNCiAzNjYuMzEgNjAuMTUgMzUyLjk0IDUwLjgzIDM0OC40OSA2My4zOSBE
+CiAzMjAuNTQgMzkuNDkgMzUwLjUxIDQ1LjU2IDM3NS42MyA0Ni4zNyBECjIg
+WApWCjAgWApOCjQ0NS4yOSAyMjcuMDEgNDcyLjgzIDIyMS4zNCA0NzIuODMg
+MjM4LjM1IDQ5Mi4yNyAyMzkuMTYgNDg4LjIyIDI1OC42IDUwNi40NSAyNzEu
+MTYgNDg5Ljg0IDI4OC41OAogNTA2Ljg1IDMxMi4wNyA0ODAuMTIgMzE3LjMz
+IDQ4Ny4wMSAzNDYuMDkgNDU1LjgyIDM0MC4wMSA0NDUuMjkgMzYyLjcgNDIy
+LjIgMzQ2LjQ5IDM5Ny45IDM1NSAzOTMuMDQgMzMxLjEKIDM3MS4xNyAzMzAu
+MjkgMzgwLjA4IDI5OS41MSAxNyBMCk4KNyBYCjkwIDQ1MCAxOS4wNCAyMS40
+NyAzOTIuNjQgMjgzLjcyIEcKMCBYCjkwIDQ1MCAxOS4wNCAyMS40NyAzOTIu
+NjQgMjgzLjcyIEEKNDQxLjY0IDI4My4zMSA0NTAuMTUgMjg1Ljc0IDIgTApO
+CjQzNS44OCAyOTQuNDQgNDQxLjY0IDI5OC4zIDIgTApOCjQyNy40MyAyOTku
+NyA0MjkuMDkgMzA2LjQgMiBMCk4KNDE1LjI5IDI5OS45MyA0MTQuMSAzMDYu
+OCAyIEwKTgo0MDAuODQgMzAzLjA5IDQwMy45OCAzMTEuMjYgMiBMCk4KMzky
+LjY0IDMwNS4xOCAzOTEuNDIgMzE0LjUgMiBMCk4KMzgyLjg5IDMwMi4yIDM3
+OS4yNyAzMTAuMDQgMiBMCk4KMzc2LjM1IDI5NC44MyAzNzAuMzYgMjk5LjUx
+IDIgTApOCjkwIDQ1MCAyLjYzIDIuNjMgMzg1Ljk1IDI5Ny42OSBHCjkwIDQ1
+MCAyLjYzIDIuNjMgMzg1Ljk1IDI5Ny42OSBBCjM3NS42MyAyNjguMzMgTQog
+MzE0Ljg3IDI1OS4wMSA0MTIuMDggMjM1LjkyIDQzMi43MyAyNDQuODMgRApO
+CjQzMCAyNDkuNTkgTQogNDMzLjI0IDI0NS41NCA0MzMuNjQgMjQ0LjMzIDQz
+My42NCAyMzkuODcgRApOCjM4OC4yMSAyNjIuODMgTQogMzkwLjcgMjY2LjI3
+IDQwMC43NCAyNzIuMjkgNDAxLjUgMjcwLjMgRAogNDAyLjI2IDI2OC4yOCA0
+MDMuMjcgMjY2IDQwNS44IDI2My44NCBECiA0MDcuODMgMjYyLjExIDM5MC45
+OSAyNTkuNDEgMzg5LjczIDI2MC44MSBECjcgWApWCjkwIDQ1MCAyMS4wNiAy
+MS40NyA0MjAuOTkgMjc5LjI2IEcKMCBYCjkwIDQ1MCAyMS4wNiAyMS40NyA0
+MjAuOTkgMjc5LjI2IEEKOTAgNDUwIDIuNjMgMi42MyA0MTEuNDcgMjg4LjM3
+IEcKOTAgNDUwIDIuNjMgMi42MyA0MTEuNDcgMjg4LjM3IEEKNDAwLjMzIDI3
+NC44MSBNCiAzODIuNTEgMjkxLjgyIDM1OS4wMiAyNjcuOTIgMzg4LjU5IDI2
+MS40NCBECjcgWApWCjAgWApOCjQ0My42NyAyNTIuMTMgTQogNDYwLjI1IDI2
+My4yMiA0NjYuNzYgMjMyLjI4IDQ0Mi40NSAyMzIuMjggRApOCjQ0Ni4yNCAy
+NDYuNDggTQogNDUzLjcgMjUxLjQ3IDQ1Ni42MyAyMzcuNTQgNDQ1LjY5IDIz
+Ny41NCBECk4KMzg5LjQgMjQ2LjA1IE0KIDQwNy4zNSAyMzIuOTkgNDE2LjEz
+IDIwNy4xNyA0MDYuODEgMTc5LjYzIEQKTgo3IFgKOTAgNDUwIDUuODcgNS44
+NyA0MDYuMjEgMjA2Ljk3IEcKMCBYCjkwIDQ1MCA1Ljg3IDUuODcgNDA2LjIx
+IDIwNi45NyBBCjcgWAo5MCA0NTAgNS44NyA1Ljg3IDQxNi4zMyAyMDMuMzIg
+RwowIFgKOTAgNDUwIDUuODcgNS44NyA0MTYuMzMgMjAzLjMyIEEKNyBYCjkw
+IDQ1MCA1Ljg3IDUuODcgNDI5LjI5IDIwNS43NSBHCjAgWAo5MCA0NTAgNS44
+NyA1Ljg3IDQyOS4yOSAyMDUuNzUgQQo0NDQuODggMTc4LjAxIE0KIDQyOS40
+OSAxNzUuNTggNDIwLjU4IDE3Mi43NCA0MDQuNzkgMTgwLjAzIEQKIDQwMy42
+OSAxNzQuNjEgMzk2LjY5IDEyNi4xNiAzNzQuODIgMTEwLjc3IEQKIDM4NC41
+NCAxMTAuMzcgMzg2Ljk3IDExMi44IDM5NC42NiAxMTQuMDEgRAo0IFgKVgow
+IFgKTgozOTQuNjYgMTE0LjAxIDM4OC41OSA5Ny44MSA0MDQuMzggMTA3Ljk0
+IDQwNy4yMiA4Ny42OSA0MjIuNjEgMTAwLjY1IDQzMC43MSA4NC44NSA0NDIu
+ODYgOTkuMDMKIDQ1OS40NiA4NC44NSA0NjMuOTIgMTA2LjMyIDQ4MS43NCA5
+NS43OSA0ODEuMzQgMTE0LjgyIDQ5OC4zNSAxMDcuMTMgNDk1LjkyIDEyMC40
+OSAxMyBMCjQgWApWCjAgWApOCjQ3NS4yOSAxNTkuNzYgTQogNDgxLjM0IDE0
+NS42IDQ5Ny45NCAxMjkuODEgNTA4LjQ4IDEyMy4zMyBECiA1MDAuMzcgMTIw
+LjkgNTAwLjc4IDEyMC45IDQ5NS45MiAxMjAuNDkgRAo0IFgKVgowIFgKTgo0
+NDIuMDUgMjMyLjI4IE0KIDQ0NS42OSAyMjMuMzcgNDQ4LjkzIDIxOC4xIDQ0
+OS4zNCAyMTIuMDMgRAogNDgwLjUzIDE5Mi41OSA0NzguOTEgMTU0LjUyIDQ2
+OS4xOCAxMzUuMDcgRAogNDcxLjYxIDExNi4wNCA0NjYuMzUgMTA5Ljk2IDQ2
+Mi4zIDExMi4zOSBECjcgWApWCjAgWApOCjQ2MS4wOCAxMjAuNDkgTQogNDY0
+LjczIDEwMS40NiA0NDguMTIgMTAwLjI0IDQ1MS43NyAxMjAuMDkgRAo3IFgK
+VgowIFgKTgo0NTEuNzcgMTIwLjA5IE0KIDQ0NS4yOSAxMDcuMTMgNDM2LjM4
+IDEwOS41NiA0NDMuNjcgMTI0Ljk1IEQKNyBYClYKMCBYCk4KNDQyLjQ1IDEy
+Mi41MiBNCiA0MjYuNjYgMTI0LjU0IDQzNS45NyAxMzEuODMgNDQ2LjkxIDEz
+OC43MiBECiA0NDkuMzQgMTQ2LjAxIDQ1MC45NiAxNjIuMjEgNDQzLjY3IDE4
+NC40OSBECk4KNyBYCjkwIDQ1MCA1Ljg3IDUuODcgNDUxLjE2IDIxNC42NiBH
+CjAgWAo5MCA0NTAgNS44NyA1Ljg3IDQ1MS4xNiAyMTQuNjYgQQo3IFgKOTAg
+NDUwIDUuODcgNS44NyA0NDEuMDQgMjA5LjQgRwowIFgKOTAgNDUwIDUuODcg
+NS44NyA0NDEuMDQgMjA5LjQgQQo0NzYuODggNDkuMjEgTQogNDkxLjA2IDMy
+LjIgMzg5LjggMTcuNiA0MzEuMTEgNDcuOTkgRAogNDMzLjU0IDQzLjU0IDQz
+NC4zNSA0MS45MiA0MzcuMTkgMzcuNDYgRAogNDQwLjQzIDM5LjA4IDQ0Mi4z
+IDQwLjE2IDQ0Ny4zMSA0Mi43MyBECiA0NDYuNSA0Ny45OSA0NDMuNjcgNTAu
+NDIgNDQwLjgzIDU1LjY5IEQKIDQ0My45IDU3LjQxIDQ0NC45MSA1OC40MiA0
+NDcuMzEgNTkuNzQgRAogNDU0LjYgNTUuNjkgNDU2LjIyIDUwLjQyIDQ1OS40
+NiA0Mi4zMiBECiA0NjcuOTcgNDUuOTcgNDc0Ljc5IDUwLjI0IDQ3Ny4yOSA0
+OS4yMSBECjYgWApWCjAgWApOCjQzMC43MSA2MC45NiBNCiA0NDQuODggNDMu
+OTQgMzQ2LjA2IDMxLjM5IDM4Ny4zNyA2MS43NyBECiAzODkuOCA1Ny4zMSAz
+OTEuODMgNTYuMDkgMzk0LjY2IDUxLjY0IEQKIDM5Ny45IDUzLjI2IDM5Ni4x
+MyA1MS45IDQwMS4xNCA1NC40NyBECiA0MDAuMzMgNTkuNzQgMzk3LjUgNjIu
+MTcgMzk0LjY2IDY3LjQzIEQKIDM5Ni40MyA2OS4wNSAzOTguMiA3MC4xOSA0
+MDEuMTQgNzEuNDggRAogNDA4LjQzIDY3LjQzIDQxMC4wNSA2Mi4xNyA0MTMu
+MjkgNTQuMDcgRAogNDIxLjggNTcuNzIgNDI4LjM2IDYxLjk4IDQzMC44NiA2
+MC45NiBECjYgWApWCjAgWApOCjQzMS4xMSA0Ny45OSA0NDAuODMgNTUuNjkg
+MiBMCk4KMzg3LjM3IDYxLjc3IDM5NC42NiA2Ny40MyAyIEwKTgozMjAuNTQg
+NTEzLjc3IDMyMC41NCA0ODIuOTQgMiBMCk4KNSBYCjkwIDQ1MCA2LjQ4IDYu
+NDggMzE5LjczIDQ3Ni41MSBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDMxOS43
+MyA0NzYuNTEgQQowIEYKKE9yaWdpbmFsIGltYWdlIHNjYW5uZWQgZnJvbSBM
+QSBUKSAwIC0yNzAgNTgwLjM4IDE4LjMgVEYKKGltZXMgYnkgRG91ZyBLcmF1
+c2UpIDAgLTI3MCA1ODAuMzggMTA5LjA2IFRGCihUKSAwIC0yNzAgNTg2LjU3
+IDE4LjMgVEYKKHJhY2VkIGluIEZyYW1lIE1ha2VyIGJ5IENodWNrIE11c2Np
+YW5vKSAwIC0yNzAgNTg2LjU3IDIxLjc0IFRGCihDb3B5cmlnaHQsIDE5OTAs
+IEVsIEJhcnRvIEVudGVycHJpc2VzKSAwIC0yNzAgNTkyLjc2IDE4LjMgVEYK
+Rk1FTkRQQUdFCiUlRW5kUGFnZTogIjEiIDAKJSVUcmFpbGVyCiUlQm91bmRp
+bmdCb3g6IDAgMCA2MTIgNzkyCiUlUGFnZXM6IDEgLTEKJSVEb2N1bWVudEZv
+bnRzOiBIZWx2ZXRpY2EKCgo=
+
+--16819560-2078917053-688350843:#11603
+Content-Type: BINARY;name="Alices_PDP-10"
+Content-Transfer-Encoding: BASE64
+Content-Description: Alice's PDP-10 w/ TECO & DDT
+
+CQkJCUFsaWNlJ3MgUERQLTEwCgo7OzsgV2l0aCB0aGFua3MgKGFuZCBhcG9s
+b2dpZXMpIHRvIENocmlzIFN0YWN5LCBBbGFuIFdlY2hzbGVyLCBOb2VsIENo
+aWFwcGEsCjs7OyBMYXJyeSBBbGxlbiwgYW5kIG9mIGNvdXJzZSBBcmxvIEd1
+dGhyaWUsIGFuZCBwYXJ0aWN1bGFybHkgdG8gQW5uIE1hcmllIEZpbm4KOzs7
+IHdobyBpcyBhIGtpbmQgc291bCBhbmQgbm90IGF0IGFsbCBsaWtlIHRoZSBw
+ZXJzb24gcG9ydHJheWVkIGhlcmVpbi4KOzs7CQkJCQkJCQktLSBzcmEgMyBN
+YXkgODUKClRoaXMgc29uZyBpcyBjYWxsZWQgIkFsaWNlJ3MgUERQLTEwIi4g
+IEJ1dCBBbGljZSBkb2Vzbid0IG93biBhIFBEUC0xMCwgaW4gZmFjdApBbGlj
+ZSBpc24ndCBldmVuIGluIHRoZSBzb25nLiAgSXQncyBqdXN0IHRoZSBuYW1l
+IG9mIHRoZSBzb25nLiAgVGhhdCdzIHdoeSBJCmNhbGxlZCB0aGlzIHNvbmcg
+IkFsaWNlJ3MgUERQLTEwIi4KCllvdSBzZWUsIGl0IGFsbCBzdGFydGVkIGFi
+b3V0IHR3byBpbmNvbXBhdGlibGUgbW9uaXRvciB2ZXJzaW9ucyBhZ28sIGFi
+b3V0IHR3bwptb250aHMgYWdvIG9uIGEgVHVlc2RheSwgd2hlbiBteSBmcmll
+bmQgYW5kIEkgU1VQRFVQJ2Qgb3ZlciB0byBNSVQtT1ogdG8gcGljawp1cCBz
+b21lIGhhY2tlcnMgdG8gZ28gb3V0IGZvciBhIENoaW5lc2UgZGlubmVyLiAg
+QnV0IEFJIGhhY2tlcnMgZG9uJ3QgbGl2ZSBvbgpNSVQtT1osIHRoZXkgbGl2
+ZSBvbiB2YXJpb3VzIGFzc29ydGVkIGxpc3BtcyBhbmQgc3VjaCwgYW5kIHNl
+ZWluZyBhcyBhbmQgaG93CnRoZXkgbmV2ZXIgbG9nIGluIGV4Y2VwdCB2aWEg
+dGhlIGZpbGUgc2VydmVyLCB0aGV5IGhhZG4ndCBnb3R0ZW4gYXJvdW5kIHRv
+CmRvaW5nIGZpbGVzeXN0ZW0gZ2FyYmFnZSBjb2xsZWN0aW9uIGZvciBhIGxv
+bmcgdGltZS4KCldlIGdvdCBvdmVyIHRoZXJlLCBzYXcgNjAwIHBhZ2VzIGZy
+ZWUsIDEwMDAwIHBhZ2VzIGluIHVzZSBvbiBhIDUgcGFjayBQUzosIGFuZApk
+ZWNpZGVkIGl0IHdvdWxkIGJlIGEgZnJpZW5kbHkgZ2VzdHVyZSB0byBydW4g
+Q0hFQ0tEIGZvciB0aGVtIGFuZCB0cnkgdG8KcmVjbGFpbSBzb21lIG9mIHRo
+YXQgbG9zdCBzcGFjZS4gIFNvIHdlIHJlbG9hZGVkIHRoZSBzeXN0ZW0gd2l0
+aCB0aGUgZmxvcHBpZXMKYW5kIHRoZSBzd2l0Y2ggcmVnaXN0ZXJzIGFuZCBv
+dGhlciBpbXBsZW1lbnRzIG9mIGRlc3RydWN0aW9uLCBhbmQgYW5zd2VyZWQg
+IlkiCnRvIFJVTiBDSEVDS0Q/CgpCdXQgd2hlbiB3ZSBnb3QgdGhlIHN5c3Rl
+bSB1cCBhbmQgdHJpZWQgdG8gcmVsZWFzZSBhbGwgdGhlIGxvc3QgcGFnZXMg
+dGhlcmUgd2FzCmEgbG91ZCBiZWVwaW5nIGFuZCBhIGJpZyBtZXNzYWdlIGZs
+YXNoZWQgdXAgb24gb3VyIHNjcmVlbiBzYXlpbmc6CiAgICAgICAgICAgICAg
+ICAgICAgICBQRVJNSVNTSU9OIERFTklFRCBCWSBBQ0oKCldlbGwsIHdlJ2Qg
+bmV2ZXIgaGVhcmQgb2YgYSB2ZXJzaW9uIG9mIEFDSiB0aGF0IHdvdWxkIGxl
+dCB5b3UgZ28gaW50byBNRERUIGZyb20KQU5PTllNT1VTIGJ1dCBub3QgcnVu
+IENIRUNLRCwgYW5kIHNvLCB3aXRoIHRlYXJzIGluIG91ciBleWVzLCB3ZSBo
+ZWFkZWQgb2ZmCm92ZXIgdGhlIENoYW9zbmV0IGxvb2tpbmcgZm9yIGEgZmls
+ZXN5c3RlbSB3aXRoIGVub3VnaCBmcmVlIHBhZ2VzIHRvIHdyaXRlIG91dAp0
+aGUgTE9TVC1QQUdFUy5CSU4gZmlsZS4gIERpZG4ndCBmaW5kIG9uZS4uLgoK
+VW50aWwgd2UgZ290IHRvIFhYLTExLCBhbmQgYXQgdGhlIG90aGVyIGVuZCBv
+ZiBYWC0xMSB3YXMgYW5vdGhlciBNSVQgVHdlbmV4LAphbmQgaW4gUFM6PE9Q
+RVJBVE9SPiBvbiB0aGF0IE1JVCBUd2VuZXggd2FzIGFub3RoZXIgTE9TVC1Q
+QUdFUy5CSU4gZmlsZS4gIEFuZAp3ZSBkZWNpZGVkIHRoYXQgb25lIGJpZyBM
+T1NULVBBR0VTLkJJTiBmaWxlIHdhcyBiZXR0ZXIgdGhhbiB0d28gbGl0dGxl
+CkxPU1QtUEFHRVMuQklOIGZpbGUsIGFuZCByYXRoZXIgdGhhbiBwYWdlIHRo
+YXQgb25lIGluIHdlIHRob3VnaHQgd2UnZCB3cml0ZQpvdXJzIG91dC4gIFNv
+IHRoYXQncyB3aGF0IHdlIGRpZC4KCldlbnQgYmFjayB0byBPWiwgZm91bmQg
+c29tZSBoYWNrZXJzIGFuZCB3ZW50IG91dCBmb3IgYSBDaGluZXNlIGRpbm5l
+ciB0aGF0CmNvdWxkbid0IGJlIGJlYXQsIGFuZCBkaWRuJ3QgZ2V0IHVwIHVu
+dGlsIHRoZSBuZXh0IG1vcm5pbmcgd2hlbiB3ZSBnb3QgYSBTRU5ECmZyb20g
+QW5uIE1hcmllIEZpbm4uICBTaGUgc2FpZCwgIktpZCwgd2UgZm91bmQgeW91
+ciBpbml0aWFscyBpbiBTSVhCSVQgaW4gdGhlCnJpZ2h0IGhhbGYgb2YgYSBQ
+T1BKIGF0IHRoZSBlbmQgb2YgYSB0d28gbWVnYXdvcmQgY29yZSBkdW1wIGZ1
+bGwgb2YgZ2FyYmFnZSwKanVzdCB3YW50ZWQgdG8ga25vdyBpZiB5b3UgaGFk
+IGFueSBpbmZvcm1hdGlvbiBhYm91dCBpdCIuICBBbmQgSSBzYWlkLCAiWWVz
+Cm1hJ2FtIEFubiBNYXJpZSwgSSBjYW5ub3QgdGVsbCBhIGxpZSwgSSBwdXQg
+dGhhdCBYVU5BTUUgaW50byB0aGF0IGhhbGZ3b3JkIi4KCkFmdGVyIHRhbGtp
+bmcgYmFjayBhbmQgZm9ydGggd2l0aCBBbm4gZm9yIGFib3V0IDQ1IG1lc3Nh
+Z2VzIHdlIGFycml2ZWQgYXQgdGhlCnRydXRoIG9mIHRoZSBtYXR0ZXIgYW5k
+IEFubiBzYWlkIHRoYXQgd2UgaGFkIHRvIGdvIHJlYnVpbGQgdGhlIGJpdHRh
+YmxlIGFuZCB3ZQphbHNvIGhhZCB0byBjb21lIGRvd24gYW5kIHRhbGsgdG8g
+aGVyIGluIHJvb20gTkU0My01MDEuICBOb3cgZnJpZW5kcywgdGhlcmUgd2Fz
+Cm9ubHkgb25lIG9mIHR3byB0aGluZ3MgdGhhdCBBbm4gY291bGQgb2YgZG9u
+ZSB3aXRoIHVzIGRvd24gYXQgcm9vbSA1MDEsIGFuZCB0aGUKZmlyc3Qgb25l
+IHdhcyB0aGF0IHNoZSBjb3VsZCBoYXZlIGhpcmVkIHVzIG9uIHRoZSBzcG90
+IGZvciBhY3R1YWxseSBrbm93aW5nCmVub3VnaCBhYm91dCBUd2VuZXggdG8g
+c2NyZXcgaXQgdXAgdGhhdCBiYWRseSwgd2hpY2ggd2Fzbid0IHZlcnkgbGlr
+ZWx5IGFuZCB3ZQpkaWRuJ3QgZXhwZWN0IGl0LCBhbmQgdGhlIG90aGVyIHdh
+cyB0aGF0IHNoZSBjb3VsZCBoYXZlIGJhd2xlZCB1cyBvdXQgYW5kIHRvbGQK
+dXMgbmV2ZXIgdG8gYmUgc2VlbiBoYWNraW5nIGZpbGVzeXN0ZW1zIGFnYWlu
+LCB3aGljaCB3YXMgd2hhdCB3ZSBleHBlY3RlZC4gIEJ1dAp3aGVuIHdlIGdv
+dCB0byByb29tIDUwMSB3ZSBkaXNjb3ZlcmVkIHRoYXQgdGhlcmUgd2FzIGEg
+dGhpcmQgcG9zc2liaWxpdHkgdGhhdAp3ZSBoYWRuJ3QgZXZlbiBjb3VudGVk
+IHVwb24sIGFuZCB3ZSB3YXMgYm90aCBpbW1lZGlhdGVseSBkZS13aGVlbGVk
+LgpDRCVESVInZWQuICBBbmQgSSBzYWlkICJBbm4sIEkgZG9uJ3QgdGhpbmsg
+SSBjYW4gcmVidWlsZCB0aGUgYml0dGFibGUgd2l0aCB0aGlzCmhlcmUgRklM
+RVMtT05MWSBiaXQgc2V0LiIgIEFuZCBzaGUgc2FpZCAiWE9GRiwga2lkLCBn
+ZXQgaW50byB0aGlzIFVEUCBwYWNrZXQiCmFuZCB0aGF0J3Mgd2hhdCB3ZSBk
+aWQgYW5kIHJvZGUgdXAgdG8gdGhlIHNxdWFyZSBicmFja2V0IGFzY2l6IHNs
+YXNoIHNjZW5lIG9mCnRoZSBjcmltZSBzbGFzaCBjbG9zZSBzcXVhcmUgYnJh
+Y2tldC4KCk5vdyBmcmllbmRzLCBJIHdhbnQgdG8gdGVsbCB5b3UgYWJvdXQg
+dGhlIG5pbnRoIGZsb29yIG9mIGJ1aWxkaW5nIE5FNDMgd2hlcmUKdGhpcyBo
+YXBwZW5lZC4gIFRoZXkgZ290IHRocmVlIEtMMTBzLCAyNCBMSVNQTXMsIGFu
+ZCBhYm91dCAzMiBWQVhlbiBydW5uaW5nIDQuMgp1bml4LiAgQnV0IHdoZW4g
+d2UgZ290IHRvIHRoZSBzcXVhcmUgYnJhY2tldCBhc2NpeiBzbGFzaCBzY2Vu
+ZSBvZiB0aGUgY3JpbWUKc2xhc2ggY2xvc2Ugc3F1YXJlIGJyYWNrZXQgdGhl
+cmUgd2FzIGZpdmUgdHdlbmV4IGhhY2tlcnMgcGFzdCBhbmQgcHJlc2VudCwg
+dGhpcwpiZWluZyB0aGUgYmlnZ2VzdCBsb3NzYWdlIHlldCBieSBhbiBSTVMg
+Y2xvbmUgYW5kIGV2ZXJ5Ym9keSB3YW50ZWQgdG8gZ2V0IGluCnRoZWlyIHN1
+Z2dlc3Rpb24gZm9yIGEgbmV3IHN5c3RlbSBkYWVtb24gdGhhdCB3b3VsZCBo
+YXZlIGtlcHQgaXQgZnJvbSBldmVyCmhhdmluZyBoYXBwZW5lZCBpbiB0aGUg
+Zmlyc3QgcGxhY2UuICBBbmQgdGhleSB3YXMgdXNpbmcgdXAgYWxsIGtpbmRz
+IG9mCmRlYnVnZ2luZyBlcXVpcG1lbnQgdGhhdCB0aGV5IGhhZCBseWluZyBh
+cm91bmQgb24gVjNBIFNXU0tJVCB0YXBlcy4gIFRoZXkgd2VyZQpkb2luZyBE
+U3MsIE1PTlJEcywgYW5kIFJTVFJTSHMsIGFuZCB0aGV5IG1hZGUgMjcwMDAg
+cGFnZXMgb2YgY29yZSBkdW1wcyBhbmQKcGhvdG8gZmlsZXMgb24gYW4gUlAw
+NiB3aXRoIGNvbW1lbnRzIGFuZCAtUkVBRC0uLVRISVMtIGZpbGVzIHRvIGJl
+IHVzZWQgYXMKZXZpZGVuY2UgYWdhaW5zdCB1cy4KCkFmdGVyIHRoZSBvcmRl
+YWwsIEFubiB0b29rIHVzIGJhY2sgZG93bnN0YWlycyBhbmQgbGVmdCB1cyB3
+aXRoIHRoZSBDTFUgaGFja2Vycy4KU2hlIHNhaWQgIktpZCwgSSdtIGdvbm5h
+IGxlYXZlIHlvdSB3aXRoIHRoZSBDTFUgaGFja2Vycy4gIEkgd2FudCB5b3Vy
+IGpzeXMKbWFudWFsIGFuZCB5b3VyIFJPTE0gRFRJIi4gIEkgc2FpZCAiQW5u
+LCBJIGNhbiB1bmRlcnN0YW5kIHlvdXIgd2FudGluZyBteSBqc3lzCm1hbnVh
+bCBzbyBJIHdvbid0IHJlbWluZCB0aGUgQ0xVIGhhY2tlcnMgb2YgZ3JvZHkg
+dGhpbmdzIGxpa2Ugb3BlcmF0aW5nCnN5c3RlbXMsIGJ1dCB3aGF0IGRvIHlv
+dSB3YW50IG15IERUSSBmb3I/IiBhbmQgc2hlIHNhaWQgIktpZCwgd2UgZG9u
+J3Qgd2FudCBhbnkKVlRTIGVycm9ycyIuICBJIHNhaWQgIkFubiwgZGlkIHlv
+dSB0aGluayBJIHdhcyBnb2luZyB0byB0cnkgdG8gY3Jhc2ggdGhlIHN5c3Rl
+bQpmb3IgbGl0dGVyaW5nPyIgIEFubiBzYWlkIHRoYXQgc2hlIHdhcyBtYWtp
+bmcgc3VyZSwgYW5kIGZyaWVuZHMsIEFubiB3YXMsCidjYXVzZSBzaGUgY2xl
+YXJlZCBhbGwgbXkgbGVmdC1oYW5kIHByaXZzIGJpdHMgc28gSSBjb3VsZG4n
+dCBsb2dvdXQuICBBbmQgc2hlCmRpc2FibGVkIHRoZSBUUkVQTEFDRSBjb21t
+YW5kIHNvIEkgY291bGRuJ3QgY3JvY2sgaW4gYW4gWENUIFswXSBpbnN0cnVj
+dGlvbiwKY2F1c2UgYW4gaWxsZWdhbCBpbnN0cnVjdGlvbiBpbnRlcnJ1cHQg
+dG8gTUVYRUMsIGFuZCBzbmVhayBpbnRvIE1ERFQuICBZZWFoLApBbm4gd2Fz
+IG1ha2luZyBzdXJlLCBhbmQgaXQgd2FzIGFib3V0IGZvdXIgb3IgZml2ZSBo
+b3VycyBsYXRlciB0aGF0IENoaWFwcGEKKHJlbWVtYmVyIENoaWFwcGE/ICBU
+aGlzIHNvbmcncyBuZXZlciBldmVuIG1lbnRpb25lZCBDaGlhcHBhKSBDaGlh
+cHBhIGNhbWUgYnkKYW5kIHdpdGggYSBmZXcgZ3JhdHVpdG91cyBpbnN1bHRz
+IHRvIHRoZSBDTFUgaGFja2VycyBiYWlsZWQgdXMgb3V0IG9mIHRoZXJlLAph
+bmQgd2Ugd2VudCBvdXQgYW5kIGhhZCBhbm90aGVyIENoaW5lc2UgZGlubmVy
+IHRoYXQgY291bGRuJ3QgYmUgYmVhdCwgYW5kCmRpZG4ndCBnZXQgdXAgdW50
+aWwgdGhlIG5leHQgbW9ybmluZyB3aGVuIHdlIGFsbCBoYWQgdG8gZ28gdG8g
+TENTIENvbXB1dGF0aW9uYWwKUmVzb3VyY2VzIHN0YWZmIG1lZXRpbmcuCgpX
+ZSB3YWxrZWQgaW4sIHNhdCBkb3duLiAgQW5uIGNhbWUgaW4gd2l0aCB0aGUg
+UlAwNiBkaXNrIHBhY2sgd2l0aCB0aGUgMjcwMDAKcGFnZXMgd2l0aCB0aGUg
+Y29tbWVudHMgYW5kIHRoZSAtUkVBRC0uLVRISVMtIGZpbGVzIGFuZCBhIHR3
+byBsaXRlciBjb2ZmZWUgbXVnLApzYXQgZG93bi4gIEVzdGhlciBGZWxpeCBj
+b21lcyBpbiBzYXlzICJBbGwgcmlzZSIsIHdlIHN0b29kIHVwLCBBbm4gc3Rv
+b2QgdXAKd2l0aCB0aGUgMjcwMDAgcGFnZSBSUDA2IHBhY2ssIGFuZCBEYXZl
+IENsYXJrIGNvbWVzIGluIHdpdGggYW4gSUJNIFBDLiAgSGUgc2l0cwpkb3du
+LCB3ZSBzaXQgZG93biwgQW5uIGxvb2tzIGF0IHRoZSBJQk0gUEMuICBUaGVu
+IGF0IHRoZSAyNzAwMCBwYWdlIFJQMDYgcGFjaywKdGhlbiBhdCB0aGUgSUJN
+IFBDLCB0aGVuIGF0IHRoZSAyNzAwMCBwYWdlIFJQMDYgcGFjaywgYW5kIGJl
+Z2FuIHRvIGNyeSwgYmVjYXVzZQpBbm4gaGFkIGNvbWUgdG8gdGhlIHJlYWxp
+emF0aW9uIHRoYXQgaXQgd2FzIGEgdHlwaWNhbCBjYXNlIG9mIDM2JTg9PTQg
+YW5kIHRoYXQKdGhlcmUgd2FzIG5vIHdheSB0byBkaXNwbGF5IHRob3NlIGxh
+c3QgZm91ciBiaXRzLCBhbmQgdGhhdCBEYXZlIHdhc24ndCBnb25uYQpsb29r
+IGF0IHRoZSAyNzAwMCBwYWdlcyBvZiBjb3JlIGR1bXBzIGFuZCBwaG90byBm
+aWxlcyBvbiB0aGUgUlAwNiBwYWNrIHdpdGggdGhlCmNvbW1lbnRzIGFuZCAt
+UkVBRC0uLVRISVMtIGZpbGVzIGV4cGxhaW5pbmcgd2hhdCBlYWNoIG9uZSB3
+YXMgdG8gYmUgdXNlZCBhcwpldmlkZW5jZSBhZ2FpbnN0IHVzLgoKQW5kIHdl
+IHdlcmUgcGVybWFuZW50bHkgYXNzaWduZWQgdG8gdGhlIGJhdGNoIGRyZWdz
+IHF1ZXVlIGFuZCBoYWQgdG8gcmVidWlsZAp0aGUgYml0dGFibGUgKGluIHRo
+ZSBiYXRjaCBkcmVncyBxdWV1ZSkuICBCdXQgdGhhdCdzIG5vdCB3aGF0IEkg
+Y2FtZSBoZXJlIHRvCnRhbGsgYWJvdXQuICBJIGNhbWUgaGVyZSB0byB0YWxr
+IGFib3V0IERFQy4KIAo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09
+PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpUaGV5
+IGdvdCBhIGJ1aWxkaW5nIHVwIHRoZXJlIGluIE1hcmxib3JvIHdoZXJlIHlv
+dSB3YWxrIGluIGFuZCBnZXQgYXZlcnRlZCwKZGl2ZXJ0ZWQsIGludmVydGVk
+LCByZXZlcnRlZCwgYW5kIHBlcnZlcnRlZC4gIEkgd2VudCB1cCB0aGVyZSBv
+bmUgZGF5IHRvIHBpY2sKdXAgYSBuZXcgY29weSBvZiB0aGUgdG9vbHMgdGFw
+ZS4gIERyb3ZlIGRvd24gdG8gUGhpbGx5IGZvciBhIEdyZWF0ZnVsIERlYWQK
+Y29uY2VydCB0aGUgbmlnaHQgYmVmb3JlLCBzbyBJIGxvb2tlZCBhbmQgZmVs
+dCBteSBiZXN0IHdoZW4gSSB3ZW50IGluIHRoYXQKbW9ybmluZy4gICdDYXVz
+ZSBJIHdhbnRlZCB0byBsb29rIGxpa2UgYSByZWFsIGxpdmUgdHdlbmV4IGhh
+Y2tlciBmcm9tIE1JVC4gIEkKd2FudGVkIHRvIGZlZWwgbGlrZSwgSSB3YW50
+ZWQgdG8gYmUgYSByZWFsIGxpdmUgdHdlbmV4IGhhY2tlciBmcm9tIE1JVC4g
+IEkKd2Fsa2VkIGluIGFuZCBJIHdhcyBodW5nIGRvd24sIGJydW5nIGRvd24s
+IGh1bmcgdXAsIGFuZCBzcGFjZWQgb3V0LiAgVGhlCnJlY2VwdGlvbmlzdCBo
+YW5kcyBiZSBhIHBpZWNlIG9mIHBhcGVyIHNheWluZyAiS2lkLCB0aGUgRURJ
+VC0yMCBtYWludGFpbmVycyBhcmUKcG9sbGluZyB1c2VyIG9waW5pb25zIHRv
+ZGF5IGFuZCB3b3VsZCBsaWtlIHlvdSB0byBzdG9wIGJ5IHJvb20gNjA0IHdo
+aWxlIHlvdSdyZQpoZXJlLiIKCkkgd2Fsa2VkIGluIHRoZXJlIGFuZCBJIHNh
+aWQgIkRyb2lkcywgSSB3YW50IHRvIGxvc2UuICBJIG1lYW4sIEkgd2FudCB0
+byBsb3NlLgpJIHdhbnQgdG8gc2VlIGxpbmUgZWRpdG9ycyBvbiBDUlRzIGFu
+ZCBudWxscyBpbiBteSBmaWxlcy4gIFdyaXRlIDM2IGJpdCBhc2NpaQp0aGF0
+IGNhbid0IGJlIHJlYWQgZXhjZXB0IHdpdGggdGhlIG1vbml0b3IgZmlsdGVy
+aW5nIGl0LiAgSSBtZWFuIExPU0UsIExPU0UsCkxPU0UhIiAgQW5kIEkgc3Rh
+cnRlZCBqdW1waW5nIHVwIGFuZCBkb3duIHllbGxpbmcgIkxPU0UsIExPU0Ui
+LCBhbmQgS2V2aW4KUGFldHpvbGQgY2FtZSBpbiB3ZWFyaW5nIGhpcyBtb29z
+ZSBlYXIgaGF0IGFuZCBzdGFydGVkIGp1bXBpbmcgdXAgYW5kIGRvd24gd2l0
+aAptZSB5ZWxsaW5nICJMT1NFLCBMT1NFIiwgYW5kIGEgREVDIHNhbGVzIHJl
+cCBjYW1lIG92ZXIsIHB1dCBhbiBhcm0gYXJvdW5kIG15CnNob3VsZGVyLCBh
+bmQgc2FpZCAiSG93J2QgeW91IGxpa2UgbWUgdG8gc2hvdyB5b3UgYSAqcmVh
+bCogZWRpdG9yIHRoYXQgaGFzCm1hY3JvcyBhbmQgdGhpbmdzIGxpa2UgdGhh
+dD8gIFdlIGhhdmUgb25lLCBpdCdzIGNhbGxlZCBUVi4uLi4iCgpEaWRuJ3Qg
+ZmVlbCB0b28gZ29vZCBhYm91dCBpdC4KClByb2NlZWRlZCBvbiBkb3duIHRo
+ZSBoYWxsIGdldHRpbmcgbW9yZSBkaXZlcnNpb25zIGFuZCBwZXJ2ZXJzaW9u
+cy4gIE1hbiwgSSB3YXMKaW4gdGhlcmUgZm9yIHR3byBob3VycywgdGhyZWUg
+aG91cnMsIGZvdXIgaG91cnMsIEkgd2FzIGluIHRoZXJlIGZvciBhIGxvbmcK
+dGltZSwgYW5kIHRoZXkgd2FzIGRvaW5nIGFsbCBraW5kcyBvZiBtZWFuIG5h
+c3R5IHVnbHkgdGhpbmdzLCBhbmQgSSB3YXMganVzdApoYXZpbmcgYSB0b3Vn
+aCB0aW1lIHRoZXJlLiAgVGhleSB3YXMgZGl2ZXJ0aW5nIGFuZCBpbnZlcnRp
+bmcgZXZlcnkgc2luZ2xlIHBhcnQKb2YgbWUgYW5kIHRoZXkgd2FzIGxlYXZp
+bmcgbm8gYml0IHVudG91Y2hlZC4KCkZpbmFsbHkgSSBnb3QgdG8gdGhlIHZl
+cnkgbGFzdCBvZmZpY2UgKEknZCBiZWVuIGluIGFsbCB0aGUgcmVzdCksIHRo
+ZSB2ZXJ5IGxhc3QKZGVzaywgYWZ0ZXIgdGhhdCB3aG9sZSBiaWcgdGhpbmcg
+dGhlcmUsIGFuZCBJIHdhbGsgb3ZlciBhbmQgc2F5ICJ3aGF0IGRvIHlvdQp3
+YW50PyIgYW5kIHRoZSBtYW4gc2F5cyAiS2lkLCB3ZSBvbmx5IGdvdCBvbmUg
+cXVlc3Rpb246IGhhdmUgeW91IGV2ZXIgYmVlbgpkZXdoZWVsZWQ/IgoKU28g
+SSBwcm9jZWVkZWQgdG8gdGVsbCBoaW0gdGhlIHN0b3J5IG9mIHRoZSAxMDYw
+MCBwYWdlIGZpdmUgcGFjayBQUzogIHdpdGggZnVsbApvcmNoZXN0cmF0aW9u
+IGFuZCBmaXZlIHBhcnQgaGFybW9ueSBhbmQgb3RoZXIgcGhlbm9tZW5hIGFu
+ZCBoZSBzdG9wcGVkIG1lIHJpZ2h0CnRoZXJlIGFuZCBzYWlkICJLaWQsIGRp
+ZCB5b3UgZXZlciBnZXQgaGF1bGVkIG9uIHRoZSBjYXJwZXQgZm9yIGl0PyIK
+ClNvIEkgcHJvY2VlZGVkIHRvIHRlbGwgaGltIGFib3V0IHRoZSAyNzAwMCBw
+YWdlIFJQMDYgcGFjayB3aXRoIHRoZSBjb21tZW50cyBhbmQKdGhlIC1SRUFE
+LS4tVEhJUy0gZmlsZXMgYW5kIGhlIHN0b3BwZWQgbWUgcmlnaHQgdGhlcmUg
+YW5kIHNhaWQgIktpZCwgSSB3YW50IHlvdQp0byBnbyBzaXQgb3ZlciB0aGVy
+ZSBvbiB0aGF0IGJlbmNoIG1hcmtlZCBMYXJnZSBTeXN0ZW1zIFNJRy4gIE5P
+VywgS0lEISIKCkksIEkgd2Fsa2VkIG92ZXIgdG8gdGhlIGJlbmNoIHRoZXJl
+Li4uIFNlZSwgdGhlIExDRyBncm91cCBpcyB3aGVyZSB0aGV5IHB1dCB5b3UK
+aWYgdGhleSB0aGluayB5b3UgbWF5IG5vdCBiZSBjb21wYXRpYmxlIHdpdGgg
+dGhlIHJlc3Qgb2YgREVDJ3MgcHJvZHVjdCBsaW5lLgoKVGhlcmUgd2FzIGFs
+bCBraW5kcyBvZiBtZWFuIG5hc3R5IHVnbHkgcGVvcGxlIHRoZXJlIG9uIHRo
+ZSBiZW5jaC4uLiBDaGFvc25ldApkZXNpZ25lcnMuLi4gTGlzcCBoYWNrZXJz
+Li4uIFRFQ08gaGFja2Vycy4gIFRFQ08gaGFja2VycyByaWdodCB0aGVyZSBv
+biB0aGUKYmVuY2ggd2l0aCBtZSEgIEFuZCB0aGUgbWVhbmVzdCBvbmUgb2Yg
+dGhlbSwgdGhlIGhhaXJpZXN0IFRFQ08gaGFja2VyIG9mIHRoZW0KYWxsIHdh
+cyBjb21pbmcgb3ZlciB0byBtZS4gIEFuZCBoZSB3YXMgbWVhbiBhbmQgbmFz
+dHkgYW5kIGhvcnJpYmxlIGFuZAp1bmRvY3VtZW50ZWQgYW5kIGFsbCBraW5k
+cyBvZiBzdHVmZi4gIEFuZCBoZSBzYXQgZG93biBuZXh0IHRvIG1lIGFuZCBz
+YWlkOgogCi4oNjc1MDQxNjQwNzQ0LmY2dzAwNzE0MTAwNDc0NS5mNnc2NDM3
+MDAwMDAwMDAuZjYpLC5meCpbMEBmdF5dMCR3XlwKCkFuZCBJIHNhaWQgIkkg
+ZGlkbid0IGdldCBub3RoaW5nLCBJIGhhZCB0byByZWJ1aWxkIHRoZSBiaXR0
+YWJsZSBpbiBxdWV1ZSBzaXgiCmFuZCBoZSBzYWlkOgogCi4oNjc1MDQxNjQw
+MDY3LmY2dzQxNjMwMDcxNTc2NS5mNncwMDQ0NDU2NzUwNDUuZjYKICA0NTU0
+NDU0NDAwNDYuZjZ3NTc2MjAwNTM1MTQ0LmY2dzM3MDAwMDAwMDAwMC5mNgop
+LC5meCpbMEBmdF5dMCR3XlwKCkFuZCBJIHNhaWQgIkxpdHRlcmluZyIuICBB
+bmQgdGhleSBhbGwgbW92ZWQgYXdheSBmcm9tIG1lIG9uIHRoZSBiZW5jaCB0
+aGVyZSwKd2l0aCB0aGUgaGFpcnkgZXllYmFsbCBhbmQgYWxsIGtpbmRzIG9m
+IG1lYW4gbmFzdHkgdWdseSBzdHVmZiB1bnRpbCBJIHNhaWQgImFuZAptYWtp
+bmcgdW5kb2N1bWVudGVkIGNoYW5nZXMgdG8gdGhlIGRlZmF1bHQgRU1BQ1Mg
+a2V5IGJpbmRpbmdzIi4gIEFuZCB0aGV5IGFsbApjYW1lIGJhY2ssIHNob29r
+IG15IGhhbmQsIGFuZCB3ZSBoYWQgYSBncmVhdCB0aW1lIG9uIHRoZSBiZW5j
+aCB0YWxraW5nIGFib3V0CkNoYW9zbmV0IGhhY2tpbmcgYW5kIExpc3AgaW50
+ZXJwcmV0ZXJzIHdyaXR0ZW4gaW4gVEVDTywgYW5kIGV2ZXJ5dGhpbmcgd2Fz
+CmZpbmUuICBBbmQgd2Ugd2VyZSBlYXRpbmcgUGVraW5nIHJhdnMgYW5kIHNt
+b2tpbmcgYWxsIGtpbmRzIG9mIHRoaW5ncyB1bnRpbCB0aGUKZ3V5IGZyb20g
+RERDIGNhbWUgb3ZlciwgaGFkIHNvbWUgcGFwZXIgaW4gaGlzIGhhbmQsIHNh
+aWQ6CgpLSURTLVRISVMtU1BSLUZPUk0tSEFTLUZJRlRZLUVJR0hULUxJTkVT
+LVRISVJUWS1TRVZFTi1CT1hFUy1BTictU0lYVFktRUlHSFQKUVVFU1RJT05T
+LVdFLVdBTlQtVE8tS05PVy1USEUtREVUQUlMUy1PRi1USEUtQlVHLVRIRS1M
+T0FELUZBQ1RPUi1XSEVOLUlUCkhBUFBFTkVELUFORC1BTlktT1RIRVItS0lO
+RC1PRi1USElORy1ZT1UtR09ULVRPLVNBWS1XRS1XQU5ULVRPLUtOT1ctVEhF
+LUYtUwpHVVknUy1OQU1FLUFORC1IT1ctTUFOWS1UUkFDS1MtT04tWU9VUi1U
+QVBFLURSSVZFLUFORC1BTlktT1RIRVItS0lORC1PRi1USElORwpZT1UtR09U
+LVRPLVNBWQoKYW5kIGhlIHRhbGtlZCBmb3IgZm9ydHktZml2ZSBtaW51dGVz
+IGFuZCBub2JvZHkgdW5kZXJzdG9vZCBhIHdvcmQgdGhhdCBoZSBzYWlkCm9y
+IHdoeSB3ZSB3ZXJlIGRvaW5nIHRoaXMgYnV0IHdlIGhhZCBmdW4gZmlsbGlu
+ZyBvdXQgdGhlIGZvcm1zIGluIHRyaXBsaWNhdGUKYW5kIHNwZWN1bGF0aW5n
+IG9uIHdoeSB3ZSB3ZXJlIGZpbGxpbmcgb3V0IFNQUnMgb24gdW5zdXBwb3J0
+ZWQgcHJvZHVjdHMuCgpJIGZpbGxlZCBvdXQgdGhlIHNwZWNpYWwgZm9ybSB3
+aXRoIHRoZSBmb3VyLWxldmVsIG1hY3JvIGRlZmluaW5nIG1hY3Jvcy4gIFR5
+cGVkCml0IGluIHRoZXJlIGp1c3QgbGlrZSBpdCB3YXMgYW5kIGV2ZXJ5dGhp
+bmcgd2FzIGZpbmUuICBBbmQgSSBwdXQgZG93biBteQprZXlib2FyZCwgYW5k
+IEkgc3dpdGNoZWQgYnVmZmVycywgYW5kIHRoZXJlIC4uLiBpbiB0aGUgb3Ro
+ZXIgYnVmZmVyLi4uIGNlbnRlcmVkCmluIHRoZSBvdGhlciBidWZmZXIuLi4g
+YXdheSBmcm9tIGV2ZXJ5dGhpbmcgZWxzZSBpbiB0aGUgYnVmZmVyLi4uIGlu
+CnBhcmVudGhlc2VzLCBjYXBpdGFsIGxldHRlcnMsIGluIHJldmVyc2Ugdmlk
+ZW8sIHJlYWQgdGhlIGZvbGxvd2luZyB3b3JkczoKCiJLaWQsIGhhdmUgeW91
+IHRha2VuIHRoZSBgYFZNUyBmb3IgVE9QUy0yMCBtYW5hZ2VycycnIGNvdXJz
+ZSB5ZXQ/IgoKSSB3YWxrZWQgb3ZlciB0byB0aGUgbWFuIGFuZCBJIHNhaWQg
+Ik1pc3RlciwgeW91IGdvdCBhIGxvdCBvZiBkYW1uZWQgZ2FsbAphc2tpbmcg
+bWUgaWYgSSd2ZSB0YWtlbiB0aGUgYGBWTVMgZm9yIFRPUFMtMjAgbWFuYWdl
+cnMnJyBjb3Vyc2UgeWV0LiAgSSBtZWFuLi4uCkkgbWVhbi4uLiBJIG1lYW4s
+IEknbSBzaXR0aW5nIGhlcmUgb24gdGhlIGJlbmNoLCBJJ20gc2l0dGluZyBo
+ZXJlIG9uIHRoZSBMQ0cKU0lHIGJlbmNoLCAnY2F1c2UgeW91IHdhbnQgdG8g
+a25vdyBpZiBJJ20gYnJhaW5kYW1hZ2VkIGVub3VnaCB0cmFkZSBteSBQRFAt
+MTAKZm9yIHBhcnRpYWwgY3JlZGl0IG9uIGEgc3lzdGVtIHRoYXQgZG9lc24n
+dCBldmVuIGhhbmRsZSBmaWxlbmFtZSBjb21wbGV0aW9uCmFmdGVyIGJlaW5n
+IGEgbGl0dGVyYnVnLiIKCkhlIGxvb2tlZCBhdCBtZSBhbmQgc2FpZCAiS2lk
+LCB0aGUgZnJvbnQgb2ZmaWNlIGRvbid0IGxpa2UgeW91ciBraW5kLCBzbyB3
+ZSdyZQpnb2luZyB0byBwdXQgeW91IG9uIG91ciBWQVgvVk1TIG1haWxpbmcg
+bGlzdC4iICBBbmQgZnJpZW5kcywgc29tZXdoZXJlIGRvd24gaW4KdGhlIE5F
+NDMgcmVjZWl2aW5nIHJvb20gaXMgYSBsYXJnZSB0cmFzaCBiYXJyZWwgd2l0
+aCBhIGJpZyBzaWduIG9uIGl0IHRoYXQgc2F5cwoiVkFYL1ZNUyBkb2N1bWVu
+dHMiLgoKQW5kIHRoZSBvbmx5IHJlYXNvbiBJJ20gc2luZ2luZyB5b3UgdGhl
+IHNvbmcgbm93IGlzIHRoYXQgc29tZWRheSB5b3UgbWF5IGtub3cKc29tZWJv
+ZHkgaW4gYSBzaW1pbGFyIHNpdHVhdGlvbi4uLiBvciB5b3UgbWF5IGJlIGlu
+IGEgc2ltaWxhciBzaXR1YXRpb24uICBBbmQKaWYgeW91J3JlIGluIGEgc2l0
+dWF0aW9uIGxpa2UgdGhhdCB0aGVyZSdzIG9ubHkgb25lIHRoaW5nIHlvdSBj
+YW4gZG8sIGFuZAp0aGF0J3MgY2FsbCB1cCB0aGUgRGlnaXRhbCBFZHVjYXRp
+b25hbCBTZXJ2aWNlcyBvZmZpY2UgbmVhcmVzdCB5b3UgYW5kIHNpbmcKIllv
+dSBjYW4gaGFjayBhbnl0aGluZyB5b3Ugd2FudCB3aXRoIFRFQ08gYW5kIERE
+VCIgYW5kIGhhbmcgdXAuCgpZb3Uga25vdywgaWYgb25lIHBlcnNvbiwganVz
+dCBvbmUgcGVyc29uLCBkb2VzIGl0LCB0aGV5IG1heSB0aGluayBoZSdzIHJl
+YWxseQpkYW5nZXJvdXMgYW5kIHRoZXkgd29uJ3QgdGFrZSBoaXMgbWFjaGlu
+ZS4KCkFuZCBpZiB0d28gcGVvcGxlIGRvIGl0LCBpbiBoYXJtb255LCB0aGV5
+IG1heSB0aGluayB0aGV5J3JlIGJvdGggSVRTIGhhY2tlcnMKYW5kIHRoZXkg
+d29uJ3QgdG91Y2ggZWl0aGVyIG9mIHRoZW0uCgpBbmQgaWYgdGhyZWUgcGVv
+cGxlIGRvIGl0ISAgQ2FuIHlvdSBpbWFnaW5lIHRocmVlIHBlb3BsZSBjYWxs
+aW5nIHVwLCBzaW5naW4nIGEKYmFyIG9mICJBbGljZSdzIFBEUC0xMCIgYW5k
+IGhhbmdpbmcgdXA/ICBUaGV5IG1heSB0aGluayBpdCdzIGFuIHJlLWltcGxl
+bWVudGEtCnRpb24gb2YgdGhlIENoYW9zbmV0IHByb3RvY29sLgoKQW5kIGNh
+biB5b3UgaW1hZ2luZSBmaWZ0eSBwZW9wbGUgYSBkYXk/ICBJIHNhaWQgRklG
+VFkgcGVvcGxlIGEgZGF5LCBjYWxsaW5nIHVwLApzaW5naW4nIGEgYmFyIG9m
+ICJBbGljZSdzIFBEUC0xMCIgYW5kIGhhbmdpbmcgdXA/ICBGcmllbmRzLCB0
+aGV5IG1heSB0aGluayBpdCdzCmEgTU9WRU1FTlQsIGFuZCB0aGF0J3Mgd2hh
+dCBpdCBpczogVEhFIDM2LUJJVCBBTlRJLUxPU1NBR0UgTU9WRU1FTlQhICBB
+bmQgYWxsCnlvdSBnb3R0YSBkbyB0byBqb2luIGlzIHRvIHNpbmcgaXQgdGhl
+IG5leHQgdGltZSBpdCBjb21lcyB1cCB0byB0aGUgaGVhZCBvZiB0aGUKR09M
+U1QuCiAKV2l0aCBmZWVsaW4nLgogCiAKWW91IGNhbiBoYWNrIGFueXRoaW5n
+IHlvdSB3YW50LCB3aXRoIFRFQ08gYW5kIEREVC4KWW91IGNhbiBoYWNrIGFu
+eXRoaW5nIHlvdSB3YW50LCB3aXRoIGp1c3QgVEVDTyBhbmQgRERULgokVSBp
+biBhbmQgYmVnaW4gdG8gaGFjay4KVHdpZGRsZSBiaXRzIGluIGEgY29yZSBk
+dW1wIGFuZCB3cml0ZSBpdCBiYWNrLgpZb3UgY2FuIGhhY2sgYW55dGhpbmcg
+eW91IHdhbnQsIHdpdGggVEVDTyBhbmQgRERULgooQnV0IGJlIGNhcmVmdWwg
+dHlwaW5nIDxSRVQ+KQpKdXN0IHdpdGggVEVDTyBhbmQgRERUIQo=
+
+--16819560-2078917053-688350843:#11603
+Content-Type: MESSAGE/RFC822
+Content-Description: Going deeper
+
+Date: Thu, 24 Oct 1991 17:08:20 -0700 (PDT)
+From: Nathaniel S. Borenstein <nsb@thumper.bellcore.com>
+Subject: A Multipart message
+MIME-Version: 1.0
+Content-type: multipart/mixed;boundary=foobarbazola
+To: nsb@thumper.bellcore.com
+
+--foobarbazola
+
+This is a text prefix.
+You should be able to read this, no matter where you are.
+
+Once you've had a chance to read this, interesting things should start to
+happen. In particular, a picture and audio should appear,
+more or less *in parallel*.
+
+After that, an atomicmail message will ask you a few questions.
+--foobarbazola
+Content-type: multipart/parallel;boundary=seconddivider
+
+--seconddivider
+Content-type: image/gif
+Content-Transfer-Encoding: base64
+Content-Description: Bunny
+
+R0lGODdhQAHIAKMAAP///7YAAP+RkbZIALZtSP/akdq2kba2tm1tbf9tbW3/bf//bW1t//9t/23//wAAACwAAAAAQAHIAEAE/lDJSau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBI5D0OyKQy+XgYntBoc7l8FK/YrPAh6Hq9x2X02Sw/BugB
+AOAcU98Hq3ZOr5+aazY+z++3x4CBBlNwYVRydomKi3+AZg8BkZKTkmWCl5gGSIRwSk2LoKE6jxKNgQdsCaqqD2ummbCCm3GdtYiiuLkWR3xJfb/Av0jBxHlIUrGXtHJhX85gTLrSobzF1tfY2J1NmFwC0Rjez1+b0+Z0htnq2rXtcR6Etx/iX/Ln96PuqOv8
+xvrt9vAJHDjBkD5j/fr9cxeQoENdBm1VeKVpX0IAx6BEXLjR08OP/rmOGBxZ0FMyQR0XFlIZJ49Bdu9AygSSchOsmiy5RWF56GLMmUBz4Cw0ZijRWDzftFq3FGPDoFBRGN12KYyhq0iT2vL5NOo9iiencgR7EqVWjgnLeSXohGzZt3ADMdNpdpvbsl3X6tV7
+N66jvYADF6wai2JewYgTK17MuPGoXQrsWTnsuLLly5gza97MubOMMjzLeB6dZSjKpXQdASTNukVTPXuKbbIkxczOpJRb694l1w8lSn2TcZolcbfxC20zoYJUJkATVhj9wiIuFtxxwcOHqVPi6BFe6U9ms0kiwMzq61B5KbnInl3worR+Rj4w7plV9DLDtN+P
+EXe4cWpt/kDPM/LhJ81G/KV1Vm4GOiZWgtudZV2DmVX3UxsWQbjGAeFJeAiFmE11i0hlwaGah/ExNFFEDIIokBnIHYIXikpJ+Np2Lbo4TXWGcWTTSR7eGGGOOipioUnwcZQMjS0pJFKRPzxCSghHVgGeMkz2pFCBUMbwXmGg2SXll35VmdN81nhCZJczjOnm
+m6KNYspsZMa1Jps61vnWnXjimRoZmTTCZ5+E+kmBZJIVqqgJU8ID46KQRirppJRWaumlmGaq6aacuvZmp5Rmh+KgoFY2VVE3FVcqZ1LSyV1RTQSQhhqunKLiqg4OWM8er5IhZRq13sYRrosJmU2dog5LLGCC/pbhUmyuMHflIB3VROqyopD1GiS/TaKncp4A
+dC226EzHbbeVRDfItFCEqyy5+PS1HCurJNAKh+zKEl+V48JbA2h9qAlWhnwE960bIsVxZL/+3kEwhBl12Alh4C0zHxL1ZcdwwyVUs9/DHwPql8WlYFwfNFxyTIPHGrYc8J/dVLHLyeSkrDIMHoPsMkz/zGUGgTYXRPM3G9+sAcs7y7bg0c4E2IGuXQRttMO+
+JM2zSkU/HefUK9S0odXCBMn1gQeBHbaNY2e7EEJm9zdq2kZibZPOEGcpNdxB8HjobRpGbLfTeBMh4oqYrJdmEhqZycStgQ9h1Ig/Tqv4Sj1f7Eey/482rkLCi8vHop1/E5cTRjjem7XmGVhbWOhOBMkV6jY8XpVWthW+IHt3w051zyfmBKSNuJ8Ou5mvLLwk
+jcbKZrruLCgO1khHTZdl8ml+wnwKkxtce1aspyX8pDCn+maQ+Qob+iYKfr/owVIgz36g51fBz4Q3/xF++dK5CZdY3lUV5ogbydzw3oe/bmyNcHI5BAFRcr1iFVAuDUzMAm8SQcVMkIEVZMzzbvKXDFaIFDAToAdHmLfmkbAxiUrhCTcjD0RYz3qRWeFl4ETD
+T8nwhjjMoQ53yMMe+vCHQAyiEIdIxCIa8YhITKISHRemQhxwiWyZHBNgCEVcNFFMY7KF+v6q6IJkyWh12+AiOtxxGviFUYxFMM8b5MKtgtXljGg0gq6gh6/amGFWwWof4+J4A6jVQwx2vCOw2OCGd/GxTa7YFbT8gThqlWGQczrTIRF5kfdwY2mT/ExCDua8
+TL6gDdta5OWulBJrebJrvSmYc8xQiXwFUIunvANKVImuSFxQYqIrRCxHoC1a1tI5BazCHncpIOWc65f3Wlf5ZjMWYh5NmbJgA7qcpa5gdq5nW/yhvFyxigd0c0MPbNe+JOnMvSknFfWyFzjD2S6rhGaXLCpF4aJFTUKyU5xT4JcYW/WGgEXMEaMszLSWYRUv
+fkiJ+klQI0l5JfEYRJHVQiLSWv62hMP9E5oje5IhaDacbOLtJR/TTnsy8j6SNWNoV5FohujWtmBw6GCzgNzQykM/IKajpfxBVkoPNVOa5g6HE8XplqZYmG/UNDI9/akMWcZSoTKSIcUTx1GFNjSlnrAaTW2pfyZSH8Ahh2ZWvWpWwYbJrxKIQX4kWhAT6tQN
+lTV1UiUS1MJawZu21a20AwEVqUQPj/orInd9KjnLyYGUfA2nYiOsgMrWNuQpNnWMbSwqEvvYQ/2jrY6tbMkue1izMcmvPKxOLySbWcUeya1a/exjLcSM8JC1Iqotp94G41qX4Yt1sq2cOcWpUPPJDY6n5N9EsDRWf+oRN+cJrm5LJv8vw/XiolLMJXAPJUIj
+Dq4kcSifQXOyXE74Y0xErJZHNhu5MkX3EGOxiBofVgXQwotzooMckvZ3voW5YqxN2pB739vEvQZwT/Wl3Zb2itDopepv0aVe9biIk76cV03ivJ33qjgUt5yXNoAA3uuWKKI36mN70pOwkzispN5h7XciHrFKT5wk33FPw9mwiFfXipsWlzjErltHS2Zs0xrb
+mCHHQ56T6LpU2iXwml/EcY7n96QiOm92KRpJkD87YesuyIDtDU59NxzeK4czfgpWnuYwzMZG7UKKt4wwmKsMN07WN81kiB8T2Dw2Aj64vPi78zbS12Z2JRjOx5VzUF06mz6kJ86Vb7pnzEIzvoOyFa90ahwoFZ2/55EZylr8lgilFMH7URo8BNaWEwu431VJ
+Sbs1THVuwscJQGtEs33UH6VLDWvaKprWtY7Mp3Gda1e/Otc98PUggO0DbQmng8QONkoEWpRk523Z3Wi2s5kIq2gTeNpz4DS2PajqbvN625Gp7tHEDW4ctLAgcylIuQUHGRWu23Htjve7t3CoM0Nm3sE2Ib5zEQEAOxoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+--seconddivider
+Content-type: audio/basic
+Content-Transfer-Encoding: base64
+Content-Description: TV Theme songs
+
+8vp7cnRvc29ubWxcUebUX0tM1N5bauLZ5ejv2N5772hiX1lVVFdZWWPm0dDU1+LubmZhW1VXXVlfdOnZ0tHPz9TY3/ZgV1VUV2Lw7e/0cmRcV1FPUFZeb+7g2dLOzc7U5HJfYFxYWF/66uni3el0Zl5ZXFtdZnPx4djOyMXGz3FJPkzjXFVf/NfX2dzf5vj2
+fXn39OLe9k46N1zAwM1maM7wVkxLW+bQ1dzi49zW0NDX4mBAOVfHyNDcdHfe4mdbUUtHTnXYz8/U09Ta2dTQ7UhJ1cvnT0VP//fd2O9WRkI+PEVlzsC/yM7PycG+vcpFOO69zF9PbOx+YFJJOzY2MzM8X8i7u7y7uLOxtsRLQ8m+2U5KXurcWVNRRTs3My0q
+LkDZv7+8tq+trrHGU9i5vtxUVW3ldFJPRzszMi4qKS9Oxbu8tq6qqay31Me3v9tRT2p3X05IPzwxLi8tKSk0aMO/vrSurKuvvb+ysr/mWGH1Z0tFPz01LS4vLiwvTcO5v8K6tK+wt7evr7nJ6+7qa05DPjs4ListLS0uO9C2tsDBvbm1trKur7W9zeF8Xk1F
+Pjo3MSsrLSwtL027rrK9vbq1sa+trrK4v8vacVZORT88OTAqLCsrLDFnubK9wLixraysrrG4vszkaVtaW1tbXllDQlBd7GJLQj04NTg9RlJgeuna0tDV2dfQzczJxcPL6M2+u8VZPDQxMDM4P0pQWmfu3dbb4ePc0czIw7+/zta+t7rdRDg0MzM3PkhPVl58
+4Nra3t/b0szIw7++w9jNu7W67EM5NjY4PEFITFVgde/k3+br4tnPy8XBwdJq28a5uc9SPzk2OD1FTVRn+Ofb2Nbe3tXOysbExMjn787FurnFbEE5PTw4Nj1aU09f4dbZ29nLzc3IysnReOrWzsXBxd1NOzU0NzxBT23h1cvL2e9pX15u8drO1tvN0c/Gv8LU
+fltMRkM/Pj08PT9HU23s3dbPycS+vsnMy8vBvsV3RDkxLi4zOj5FSU/XubS0srKys7S2ucJhPzQyPkc5MzAwMC8xOd2qqauqrK6xtLvLTzUxRfpMOjI5UkY6MzNdr7K2sLS1u8TdYExGPjIySeXc63xQPT7PrbPFzMjE2mlNRklOREJKbdTV5djFy7usu9No
+SGzb/E5EOS8qLj7nx7/DzNi9paSrtEgzPWDyUz8/PC4wOkZ76lI/SrSpr620vs5RP2DK7z88Uezc1NPV3XTCr8htSzw0KSQkLlXi139R6b2sp6y3ppicqLfhSzcnHR0jKikpIx0dJEq1qqOYkZWXnaq9xHArJztMQTstHxoaHy05b6uorKeoqbG72i86p6m8
+v8s7Jyg38MG1pqm7zkU1LSceFhEorautyllGxqikn5yWmaKpuMZWNiccEw0bNC0eGB03rqGfn5eUmZqfpaiwyz4qHBcoRiQgKC09Rz47YcLHr6Whpq+5x1gsJXGtbU+8vMi4w+G4sdZeRDUtIx0bGxoisaO6v6edn6OjpJ6Yoayyy1UvIh0XDg8pLRwYIEW8
+pJ2dmJGVmZykrMBlPCkaHOndKh0eKCg0RztHwcy+qKOhqa+zzi4uq6fSODhvbLevytO3x0Q9LiojHRscHB6/n7G3qZyfnZ2mp6CmtLrOVDYpHxoVDxcgGh8u5K6kn6KdlpWam5+ntc1FLiQbHicocM/qOygrKCxJW8+rpqu0s7nLTC46XsiioaSwYlZKSr/Q
+OjYuKSMeGxodJFN41p2Xk5ijpqmpn6Kzvuw/LiciHBQQGx0YK1Gwo6alqqeZmJydpK24yVY0IhwrLx8rab7IRDw2MzAyXL69zNrOz+o/M1TyPGquop+lqrC+3E5PRzo3NS8tLSorOT452qugnqSqrrrMYVhZT0xBNzQyLisuKy/ptauorK+1vcra19R2WlBA
+Pj85LiouX7mvrK+1vM1XQT9LWFdeX0pHU2VGLzq+r62vtbq7yVA/P0VAOzk5ODY6REo3Pbyspqessba7zmpLQj04Nzc2NjY+Tzs317aurK61urrCzuDreUtLcFU+MDlKNC5NvbOwtLi9y9J9VFZST1Rk6eh+7+pnT1RcUklITVNVVFlb69LPzczN0+NfVFRT
+Yebc5+PQy9V9Tz8/QUBBSVNq4NXW29TFv8LKzczS9XJgTlVkYFZPU1xeWVJJQUpia+jIt7O3uLvJ1vVOPDY6UVtPT0Y+PD88OtKnqa2tsrrCzWxFNS82S0o9NC43QGmsqq6ts7nG12tJPDg0MDE0Liw1WK+en5+pub/rW0Q3Mi0qKywsKzA/r5udo7XMyLmy
+u95EOTIsKSgoKD61tbndW8m8uLu5wcv2SEg/Li25oqeuPzVETE1HPzguJiMnMdqinZ+fs97NxctNKiQzVEw6PLCdoaOseDMuLConHx0fKCwsUJ2Sko6TrkVV2MTORi4mIh4aH0tsWL21JB9Vwaiip6+3ub/UrqWuqqmlvRweIyQuMjI1NTEwOE3NtKujnKo0
+PklNTTEtMUJX07mvrqytr6u9MjhEQTUsLC88OjM+3rmup6Kr32vYzb/eTktGQjcxOU1KPWLNZM63tLS1wOZj9tzg53teSkw+KiktR7musbi7yd3i3t/Px8TXPDU4Nj5GMzXmvbW0tra5v8LBWz9Z3s3ZRDUyMDEuOtK2r7GxtcrWw73E4U5JSkpDOzY1NzI5
+5sO+tLG1u8PN0971eG1eVEY/PDY1Nzvit7KxtLi+xcjS29vwYE9FQTs2OkA+O0nNu7i4urq/xM3o1NF+b/5PPjY2Ozc2RM+7t7rBwcnT0dLS315TVEc9OT9NRkltxLq4uby8xdHkdmtcT09iWUc9PTw3RN7Curq7vsHNbU9e3+lVSU5QSkQ/OT3rwrq3ur7F
+xcXM3X5uWElHS01HPkBDTdjHycu7vsy+v83T5uP9WElHSkc7NjU9a9PEvr27vcfSz8ra6eBeV1VJPjg0LS5D4sO7vL67t7m+w9j5Vz84NzU1Oz86PVzOt66xtri7wdlXOzIxLzI7Qk/jwN9S1sW4rq+6zFY+OzYvLTZGx6qwsLfZ4Fp1Tz40LS4vLzBFpJSd
+p7G4v95JNSwgHBwcHDKmk5mbn6Wnv1xNNyQbFRIdapyboqOnpamtu10uIB0YEzucrKmiqq/E0EAxKiUhHCmgmaqfoq6tqLM9Jh0aFRTVnb3IzK2oqqmlsOc6Gxyfla6+LiwvKSMgHxwsNs2LiJGWq1c0RDMoHBURDxDOj5idqb6oqK23xkMqHS0mtpK3sD1J
+Yy0mIB0aLKOnNJaModREqq/VQSsdHMlcHhlFqigpX7Osnpmdp5edXSQznz0ZEBMVDxgptI+LlKipi5arQxwgHRgREtCdrkYkm6RPyDK2rrr+O7OqKy0vmYytzhoZHxgXFiqdnqw0OI6RlKVduzMlHyTMwyYaCyTCITy8kY6Ynqygma88HR2j0B0RDRIRHzuo
+jImSoUuVjarvGx4ZEhAVxJejbBospOinra2lv1kuM8m7s8y0jqVYIBEcFxQRFryWqzHFhYWPmjQ7OSAeHSTcKhQMFD3kl5mUj5ums8CioDUbGLQ9EwwX0jkuKTWljJGfspOOujQWFQ8aXmi6np80GSasQrexsq0+LBkkmY2YsbeTsS0dEx0XEQ4UQ5+voY+C
+hZepJywpGhoaHTUrDwwZtpmMjpCTqMBSMuenNBstxB0PFzuwTSQpQaiNlLehjprbIxEYLs3KzjvCxhgYS8i7qrmqxRwfr5+Zj6ZmoqZKKxcZHhALCgwui4mTiIWPnz0tVysZFREPHRMKZpOcl5KVk5u2bz0pP8MbH14hSq7IttMnFxQgsZGdqo+atzISJaKg
+r/4tI0w5Fi9fJ3Dfy7I1LKqZnJaTuradzS0bFBoTDAkJFZuBh4qEjZ3BJSksIRwWDw4VGSeamJqSm5+irb5JLSJIyBoiuq2gvTpbIhQTHUSfkaWfkr0mN06lobnELBsbTi0dPS01ukIrsp6XlqaqnKNNqaw1IRMPEgwFDTKbhYWNiouYqkIkJyEWEg4ID1TL
+nJarop2fn6PDXk8mH0EdGaealZ7wNyQWDxEb4JSWqJmrL2ipl5arSycbFik0HDc3HScrLquUjo6ar7WlScGpQCgZEw8IBQ0jo4uEio+MlZ/DMzAkGg4IDBy8pK2hrbuon5uisFkvLR0cSr6elpqsPiwfFQ4PGzSllKq+nZWZnZqfvDIeGRYaOyYaHxwbMpmS
+ko+YpLTWo6d4tPooGQ8LCA0VIKSQiIaRj5Ccqc81HxQLBQse9Jefs7ZOtp2YnKXRLiQZFEKopJGSotopGxcQDQ8dwpKcPKSRkI+PnbUxGxoXFio5GxwPDDiXj42Lk6K0Q7OdutBOKxwLCRYcFxcvp5GEi4+PmqT+MSMPDBgcGyOdltHSL02pnJyqxzkaDySf
+k6KalavTLB8aEg4OEyG3n5qNjY+QkKC5TSIaExIYMiAPCRG2l42Oi5KfrlhZopu8RyAXCggXIh8eSLuji4iPk5SbrjgaDAwdJyIksJe8Py1Bs5uYpbgrFBo5s5SYmZi5Xi8hGBgRDA4PG5SHjI2Tjo+hyUQsHxsTDx4eDRsmLq2WjIuRnaKvzLWr1zUaDhwt
+JR0aJE3dtZiTl5yoy0InFh4+P0s8Sa6pu7LC676yvzkiGSDBt8V3zqOhvLjCYVI5LSgqJSTfr6mwvqScrLPIRl9fRyseGRs6yLm3uamftbOst7hmSEQyHxwuVFhFNm+lq6usxbi6w9c8KCw8Pz9CRv2xy9WzyMXRzsA/IyxV2MLGZFCrqcK52dNNNzUpIDDV
+z+rTwM2ooq6sv9FDMjIrICQ1Slla19Gqn66xvbu5zEQuKCw+RUpNRzlau8KyrbO3vdNIMjFgyM9uPDFJsbnXzM/K1WNHPjZARkJc3tvXraq0uNdnWkg4KiEvatbZ6uXYr6mzrrO9zT0vKCUvOz5W7F1OuqSqp6/Q3GFPNC88V0c6Nzg5XK+/vLK7vsz4TE5P
+XubX1FY1OMfP18dcTE9ANkPZu7rXYlVMZq+vvrr2RDUwM0FORExd8fTj46qktLjOxsVoODI0NDQ0NzxOUbirsqqtt8hUOztFS0ZFRDcsLNOptbq8w8nhXERdzNNNQljuTd2uwVNIRkdESkZT89bDv9L0TPStrbjA1Fg5LCYvWHZYYllcWM2qp7G61EdFUElF
+RzwzODw6PE21qbu9uLe84lRT8ONPT1A/Nis3yMe6vcbL18zGv8Pb6Gs8NjvhtuNNTlZMPEJa29JZVsa7x2PVsLa6vuFSNi0pKjdLbNzrZ92zo6iurrz9NTI7SVY7LjA6OTtnr622uMPCzdtiW9nE1lZJPDIzV3pS1MvUalLPurvCy+tVSTk43rbJ3UI6TkdE
+ZdTd09bjd1dXw6+6tLfZSz04ODYzPklQXk1GvKOmrbPG3lQ+R0c6OzgxMjo9U7mysay7zn9d295MUM7HbD02O87F481PQENATcO7x9fp+G585L27xb12NDM/R0pCR3bIxs/19r+0v7yyxVc5NTw+Ojs+PkdbY82pqKuy21pIRlRNPj8/OC41QVW6tbSvuL/Z
+Vunac1ppz19FPj7Sv9fuSD9GT3jwxb6/xllBT+S2sMLIYj4xMz08SElX19997+W5q7G7035SUk45OTtIXkM7SGXDrqywu95BPkdNbWxkfk47OUTkta+6zGts2dbpX1FLRUNHR0pXxrm9vdtPRkpUU2F/XVBKREBPs6qxudNSPkFKQkFEPj9AaMrPuKyvuM5s
+T0ZYW0w+PU9EQUZcvK6zudFUUUxFP2X9YFZER0laxbi9xNRVS/nvWv5r4O1HRkhOzrrA9/RxUldJTevaz+NNTFxs4buwvclmTlVGQkRGTEI7PEbsw6+rs7S/1+tPRkJAOzQ0O0NJUb2rr7jJaWRaUkVDT1xoaWXy276zxX9MPkVCRExh693Vy8TJ1Me2vMpm
+Pj5AP0BNa9LfTkdNb825uL2+zvJYSldfQjk0OD5JZNC1rbC1v8LGW0xKTE04LjA6S3LFtbe1wu1eTeXR3OllX313ee3Cu/BFPERJPkhf7uZeWeHJvr+6tLvUS0NCODk9P0xVbGFSUdm3sLa+xtftTUhSZE88ODhLaN2/srPAzt7Y39zkZ0hDRz9BRUtjz8K/
+wcrdVkti3PxiXVRWVVTexLrBdkE8Slnt5FpKSUt/zb+1tbO+zO1KPzw8PDs8RlHi1eLZv7Cwub/Pb09IUU5CPzk2OU7TyL7BzMnT3c/IvcVePzw/PT5LWtjP587V9nla+dfY4/vrbvrS0sW/0dhQOTk/WupuZGlqb9G6uLGxzl5JPT4/Q0Y+PkNPTmrJysO3
+t7rB08zZ/VlMRTo7NzQ8Tty8vMDM5cu/vcTL6UY+PUNUZ97CyvpvTU3n1GxWTEhe7dTEx8i6udJlRzxBR0ZNTkteaXjJv7+3s8dgQzpAWF1OQjk9RUjrw7+9t7zHzdDLyNZnTTo3OTo/Uu7QwMHKzdvVvsDM0vNQPj5KWmP21+BPUFlOW3T38/ztbGTVw7/G
+xL7cUEtDQ0RLT0FARFHozbqzs7O/3W1gZUU+QTk3NkXt7tLAuLO8xtLYxdlfRz5GPDo/QU/qycJ/UmfZyM2+w31oT09ZW2xfbtpnR0Bezn5282T8du3YzsK+v8HE0FJLWklISEFDOzxK9cy/trW3vNN452ZfTz86NDQ7XOXnzsjBwcnLx8fHz3lLREM/R0pN
+bdvK0Xvx08nIxsn0Vko/RVfy1N7l6mBr2M/pZGFORVb8283Hvsbb5XpYVFVNQT5BQEZHWdnHurS3vcbR325tU0I9PD88PFDfysbLx8rDur29xtxPOTs+PkVQZ3T4zt/szsa7x9PRf1NNR0ZKXV9Y8mhkYl7Kyu9oW1JV28fJwsfV21lVa1ZfXE1JRUhJXd/M
+vr++t7vEzXhtWVJMQj8+SU1R2NLs2NnLvcbLwXde70ZDTl9PUU5DT2rb4Xj179rgz8fW3HtUYGFRTkZKd9jb2Np7fOt83MnEvsthUEVDZsm/yOpKQEZBQ09X2cjKv75n4sDL121NQD42NELYuM5fz1VE8ca5vL/Ob1Q/P3RGP9njWEdMVdzGxsPCvcDKzEg/
++ExBRUFJQE/m1sO4tsta2Ng4etlIT0NIP0Bcyru2t87sw080dUs7R01ifObj2MK4tsTnz0cuU/9HXF1ZWlV7yLWsrbnS2TorOzw2RWnv7P9u3Mm8tLS3tvo0PDYvP3FrUkE6S8S0rquts3E5Pjg3Rtj7Sz4zOP26sq6srsBqRzw1MUvg+lc9Nk/Ata2ts8Ru
+Pjc5MT3h3l46LTbMqqGkq7t7Oy8vKi5MUFA+Lix3opyfpLHKSjUwKClHWEczJyA4n5aZn67KRS8rKDPO0U0wJh4mrZmbn63CbjYpLVvBy08xKB8fWp2bnqjEUi4iNrOst2oxJh8eL6qam5+xezUiLbyzvN04KyQgLr6inZ6mvkclIGOyt8M/KyEdJdShm5qf
+uE0pHS66rrJgKx8bHTmnnJqdrdE4JivPrK7LMCIcGS2km5yers06JibuqKe3OSUbFSWgmZyertwwHyTIqqq+NyYeFx+klpqfr8ovHTCxrrDXMyIdGRu6lZabprkuHPimr7tfPyccGBkxnZecpLc4H9GgqK3JVy0eGBgmuZ2coro3HT+eoKW00TghGRcidKad
+n630HyGqoKOt2zsmHRgeSaKXm6jMKBxqoqKnyzgjGxgcRJ+WmqXELhwwqJ+kxDsmHBcVL56Ym6bBNR02pZ6fskkmHRkVJqCbnqfFLBxKnpyerlEnGxkXIKaanqLMJB+5nqCltk4qHBsdH7ydn560JiS4n6KswT8sHxoaH2Khnp6pLCK9n5+nsWItHxoXHT2v
+pKCkTiDjnJugsN41JBwYGzSyp6erySQvo5qcpb8/JRwZGSm3oqGrzSUhtZuaoLtEJRsbGiiunqKszCshyZybn7s6IhscGx+0m56kyiYivJybn69KJBkZGx7Fnp+k6iEurpyaoa1LJRsbHR9IqqOlWx8+qp2bpKtxKh8bGh9Dv62juypDrJ+bo6rlLCMeGhw8
+xLqnrzZDqZ2Zo63dLSEcGho8uMyvrUUvuZ6XnavIMiYeHBktuMe7tkor96GXmqazQSgdHBwrxLqxuEEiNKiWlJ+tRicdGhokzaqotDwgO6aYlaGuRiUdGxsjS66lrjkj6qialJ+tVSYeHBsjR8Cxch4eZ72kl5ufrG0/Pzg5PDQ6LBccT8momZygp7jNc0ZA
+RjUuKRgaP2y5npyfprbI0Vk/RD8zLh0YMNm1n5+oqrXI6UtJ7WM0KR4YJ9K3npujqLTH2k1FXVw6KyAXHVa8o5yjp6+/ye7mzt1ALCEYGjzPq52krLvMx9XQxMTXQCYYGzNTuqSnr7rMx9bOwMncRygdKT5YuqSjssHiytxx62BtSycfMkz0u6WgrbjZYEhE
+TkE+OyghN0xN1a2hrbztWkxDTVh53zkmOWFe4ryruutCPkpjwb/SytIvOexR+8uxvt5bTlhJWkkzMMxeObm6uLarr8LkRjswNjkvNsYtKLitqqihpbjQRzQoIyMqbkRHy7Oprq+rrNE8KyYkKuitrKG58LfL2VtXUiwkIiM9sqGVmZ+zejskJCQqLSMn26ud
+mZ+jrX4mIScoIiAttJ+cmaKrwVw1KCIdIR8iQ7uWjJWet0gzJyUkJB4iSSykj6Gdq7hdKh8fJy3Dpjwpm5WotjU9LSEeIiRtornp2I+VuMZBKhYXHzG9obPROcuQlK04HBgZHiuxnaOo1z+fkZ83FxwaGSC1nrevyUyejJNIEBIdJihQpLi72ii4jI+tFQ4a
+ICzDmJ6uxSw2lZGlHAwWGiM6npmkpugtnYuXRQsOFx4ut5uqq8c5Y46NrxkMFhkke5eaqbRBMKiLmyEMEBsfMqqbrKetWbmLkCcPDhoeJGCdpLSvSU6Miy4XDhgpMtafmquo3DWUkSAaEBQkJDewlZygqlaYjDcaEQ0eKStTn52mo8CqjK0ZFw8dKio8oJSg
+p7jUk5YdFA8YKSgvupacoaG8nY1tExISJSksP5+bqqO8rY6lFhIQHjQvOKiUn6Guu5GdGRMOFionLOCal5+fo5ObJBgSFCspISutl5+inpqdQRwYFixELz3Bm56rn6qy3B4aFiA6LznlmZCeoK68uSMZFRw6Li5Ho5Sfpp2lxSYZGBs6OTFUspiYn5+lqS8Y
+FxUkNio3SKKUmpmZnMwcFRYiNikmKdGcoJ+amKkpGxssRDE0LmSfoqikr7QrHB4kPEY9LnObmJaaU8EqFRgcP2w9OOyel5SWM7y8GhURHTNE0bWek46QQT+uIhcTGSEeIjezkYuMo+CkLRsZGSMmJiorsZeRlbikxC4kGyk2OTMtyZuaoTRMvzQuJ0NgPDg4
+xJmTls0lKxYXIUW+v7/X25+Qj6YsORwRDxIhSbKmoZaKjKgvyzcZEREVFRYmrpOEhZm5rb0mGRocHBkcISacipihmpi6Ix4jKyooNDetmi0Y6Jaaxj85MzMvRcOhj6oaGiYnHje6r6q69bygjZI1HywtGw8PGEmkopuUiYqyJTRxMRgQERITFzmYhICMxtSw
+0CgZGhkXFBcer4ySop6YnMMoISMkIyQor5U0FSKnkJm1RjMpLDxdn47SHBweKCZftay0uL/En4udLispNiEVCxA+pJ2clomQTlPjr+oiDgwODRBlk4SAl7a2pqHCIBUWDg4SGVmMkJuamJKgRh4lHRscHj6bzRIfooqNmzgwJR4qOLSToRsYGTFvsLS4tVxn
+VqiPmUcvKDdAJQ8NHtagopWNmLHJybq0zxoLDA0WKJyJjpKcpaShoUYSERITFRk61ZqRk5KUlqsfGyEcHyA9Lh0cM5iNi5HiIicgLz+8uDEoJCooxZKcXt86Rlyvn7JqNTg8UWQcDCO0qJ6clqW/ybSvsLUvDgkMDjKWjZOXoaeloJutHAsODxMgMS+akJKP
+lpWePRYZGhorKxIdNd6Zj4uMniQeJSZ6+RogQCs6PqmWl7pLzT7Gsigot2bMV0BLMx8d3rOfmL8rs7Gpo7K5SRwKDBRHlJzGoJufm56epkoPCw4PISYqn5GTj5SWm6clFBcUHyEPEi2vjouMjpfXGiEiOzoVEyUsOMGjj42bPjEuTn4gIjzZROZbSnbdNdus
+o6wnKmykqKisxk8uFA4eRLP9wKeam5uanqnIIg4QFBYWId6Xjo6Pl5ykxBwYGxgOCxA1npiNjY+Tny8eLCoaDg8cT2SqmpWRlq0yV0YnGRkmw8jLwdrGsad+vKs/Ih8uqqGmpLpiMiYUHLPZLjlApZebl52pvEEXDhgPDyXHnI6UlZabn6c5GB4OCAsPLZuP
+jIuQmJysKjIdDA4OGD3IqpaWj4+W2jk3FRcWGjy1v7jFsZ+fs7uuKRwcIfugp66vv3pBLSxKIygvPbeamJ2dqb9SLhgZFRUcIrmUjZGVm6Grzx8YEwwNECSpkI+PkZWapFUfGA4ODhMorqCYj4+SmKc1HxkWFBYh0b3FsKSalZW+KCEeICMvvKu0s77Cw69S
+Ijs2MDFBqZufo6+9wc4mDREdHj21nZKXmJuiqLQ7EA0PDRMoxJqPjo6Vmp2oHg4PDRIWHs2cmJmTj5GU6xYZFRUWGkOuvq+dmJ2VnyImIx4jIjq1zcCutrCephseNC1HScigoq+svr6zVhIOHR9Dw7ialJqanqWoyBcKDQ4ZLEadj5OTlJaXni4NDQ4QFhcx
+nJiSjpGQlawbEhQTFhgeZ8C1mpeTjpdGGx0dHyMjRrzCtL6ym5tIIiotNT9KvqWsr76/rskkFRwdKr65pZiYm6Gtq8MtEAsOFjo41paRkJGYmqbgGQsNDxUZKaGZmY6QkpefQxEQERMWGi7CqZaVlZCVpCQbHRodHCbht7Kyq5ubn1QkKyQsLDC5rLSvtbrX
+3jIcJjZHa0ytnqShqLC8zScNDhsrQcSilJeZm6WrsUMSDRESGCa7m5OPj5WcoLMdDxQSFRkcR6eYk5WQk5jOGBkXFRscLr3DtKiZl5ufMigqHyQgLbS2u7C9xrioPB87Nz0+T6eosKqvt73qGw8iLy/luJ6Ynp6kq7nLJQ8SEhQnY6SSk5SXm6KuQRMQFBQY
+FyWomJaPj5KZqiMUGRYXGB/ryOSnl5SSlbkkIx0cHCF8tMS5ubqunq8yUj0wLCdbr7avsLS6zjEYHzMxzMqqnKKorbGyvEkZDhEUJlC0k4+Ulp2ms9AhDxESEhUdrpSWj46Rl5/+FhIUExcaL8vOoZOUk5GcMRscFhgbLLm/urO9qZmavDI4JyEiKru3u62y
+ssX9MiMwP05iXqqir6uvs7/XNhMOFiRltpmQmJufqLTDThgNERAUG9mUkY6OlJqksSUPEhITFxxQtKCTk4+Ql7QaFRYVGR1EtsO9u6GWlprOKiccHR0ysrSxub6+va3FKz1LRzhCra+2uL+/y+0kDxgxR8+plpaeoq++y8wxEQ4REhxmmpCPkJaepqxxGQ4S
+EBQXKq+aj4+QlJiiMRQTExUYIsi8uKyck5GSpSUcHBgYHW26vrO5vraenOw4Ti0kJOK0xbm8ws/NShwZPE/QuqSao6att8fWWR8ODxIdWKeQjpGWn6myvDARDhAQEhq0lJCOj5OaosgaDxMSFRcywsOilJCTkZldGBgWFRgqvMu5u76umJGhPS8sIR8ow7+6
+tcj9S1M9Kjvi6dbPoaCoqrbBbEYtFAwSH+Svl5CXmJ2kt81SHw0ODw8Y1ZGQjo6WnqivOBMPExAVH2LLoI+OkJOZriIUFxMWHT931b29pJSQl6woJh0aIEPC3Mls6U5OvaxNQ9BATrqnr663w1s7NCEPETLjvJ6TmJyhqbLD2TEVCw4OF8iSkJCQmJ6pr9Yj
+DxAQDxguwJ2Nj5CUm6RVFxATEhorTUDbu52Pj5GcNxkcGRoq3+vhb0pO/6SWqDFCMypMra+8uczYQTwyHhYuz22rm5uko6uvv+tEIg4KDxjElZCTlZuhqLTBQRgLDQ4TJ66Wjo+RlJylvCgODxARHj1JT66XjY+Rl64cEhcVIE31Qko9Trmbj5fKJikgJ9i3
+wcToT0s7NTw7MUlP3KifqKuwubvPRS4bCw0f052UlZmdpamvv3stEAsODh2sko+Qk5eep7DrHA0PDxUrOUiokY6RlJmjPhQUFRctPC82PlKulY6Sni4dIB49vt1hSTw4OT/Cn7IuQznWpaqwt7vCXzsxKxgOJciwmZeeoqeqsL3fPB4MCw8Wr5GTlZaanqix
+wjsVDA8PHDRDppGPk5SbpLUnEhUVHDQuLzZPqJKQlZixHxsfJuXmRz80NTY/tJiaSCw6Nbyts7bJeEQ1Li4pHSXQ1qWboKSprLS+cTspFQsOGbySl5eYnaGnrLriJw4LDhQtX5+QkpWYnaSs3xwQFBYlKyg3f5+RkpeapTkYGx428jg4LzE0TKmWlqwsJClG
+rr2+zVY7MTAtNVBLPEy7n6SoqK62x2k0KyISDBrOmZiamp+lqqu7dTUZCwwRH8udkpaXmp2lr7g2FQ8SGSorNtCdk5WYnKO7JBgcIz44LzAvON+flZed0h8gK9C7ztRFNi8wMkyrpEAwW7ipr6y3xd5PNSsrHxMaz6WbnZ+lq620wvNFKBMLDxvNmpWZnJ6j
+qK614CURDxUiLTK+m5OXmp+mrHMdFh0sMysvLzXSnJWanKkwGyNBzHZVOS8yNTFCo5mwLC/wsLCvtct9QTIrLSshIEayoZ6kp62ts75aPC4fEA0X25mYmpyhqKuvuMY7Gg4PHCw0tZmXm5yhrK67MRcWIS4rLDA3wZyWnZ2j0R8bLF5sRTwvLi42cJ+VoVIn
+Nr2vt7vOZDsuKiowN0xLdKugp6mrr7vTSTIuJxoOE12Zmp6eo6essMbbSSURDhkpNrOXmJyepK2ys/8gExkoLS0zOLuZl56hpbE7HB8+Ujo2Mi8zONWfmJ6uLCFPt7vC2UA2MS4qL02tvy7DpquurrfB2EcxLS0mGRI5nJ2enqetr7XL+04wHg8SJDyslpme
+n6WuurzjMxkSHysrNEixmZmeo6ivzykYJD83NDYwMTy6nZufpdshKMzMdnFDODMxLC7Un6I9N7KstLO60WZDMiwuKyQdLqWeoKKor7S73EQ6MScaDxlBppibnqKqsL7L6zsjEhQpLzFPqJmcnqKttso/HRgrODM5ODQ/rJmcn6K1Lxww21tRQTUxNDM2vZye
+vTI/try8v/xIPjYtLC0tOEvOpZ+kqLK9zHNCOjMrIhYUNJ+anJ2jrLTA3U06LhwSHC8x2Z6XnZ+irbnNUCgXHDEvMzw3Q6eYnaClslogHz1HPUU3Ly4zPK6an6pPKWq6ycd3PjIyMC4uMMetVrufp6uvvvxNQjYyLSkeFCadmZ2cpLDBxutOOy4kFRMmOMqa
+l5+ipq+9zVAzHBQhNS87Qd6imp2fo7HTLxohQDs/PTAsL1ujmJ6jsysp089tZj0wLC0tMkWtnshGq6evsMNMODcxLi4rJx4lppqenaSyyORNPTwwKR0TGjywmJeeoqq0wMxVNycWFis2Nk60m5qfoaauyUEfFyk8Mjo2LzHPnZidoa4+Hi3dSUhBMi0uLjPZ
+n5usP02stcDJSjg0MS4wLy0yQr6enKGotMlpRDc3NC0nGhMtoZeZnKCtutliRjYsHhMcOTVNppeanqOttc1KKRcZMjEvNzhAsJqbnp+r0CYcN0U2OzMvLjA0upqZobsw7LPS70Y8My4rLC8x8qu/tpyjrbrMVzwzLy8rKSIXIZ2Vm52jrcLvRz01LycYFCg+
+9Z6Wmp6jrr3aSDIfFSA4LTAyWqOXm56grME3HB9BODIuLC0sPKqXmp+pPCrXz0g+My8rKisuOq+brFyoorO+fEY2MC4sKysrIiellZydpbHHbkk6NC4qHxQZP7WZlpyepq/E5kc0KRgVJy4sOcydmJudoq3BVCUYKTssLSkpLX6el5udpM4kL89APzctKCgr
+L1WjmKDW5ai1yN0+NC8vLCsqKzhbt5ybn6a46Uk/OjUtKCYbEimjl5ecoKq1x/lAMSwfEhkvLD2tmZqdnaKtyV81GhkxListKi7Ampeanqe3Lx4+TTU3LCclKC/AnJmdqzvltd/kPjEsKSkrKy35pa6um6GpufZDNTIwLCYmJRofoJWZmp+sxN1fQzItKBoT
+IzNIoZeanqCoss5SOiYXHi4mLCs3r5qZnJ+puEsiI0YzMiwmJSY1r5manqToNMbeR0IyLScoKywvvZugvaGgsrt5RzIvMC0mJCgkKKWUnJyjr8xcWUExKiggFhkybp6WnJ6lrLfKTjwuHRgoKCUtRKSZm5yfrL3YMR8uOSkqJicoOqaXmp6iuDE+yT06Misn
+JSgsObGanK+xo7bVbz82LiwpJiMlLU6rmJugpLLIaUw+NiskIBwYKLedmJyepKy6zFg5LygaHSskK3efmZydoqu/aUEmIjcrJCQjKEihmJueprNVLmBNMS8pJiQlKUWmmZ2itrCv22I9NC0pJycmJS+yoZ+Zn6m5y2xAODEsJSEgHSGumJqdn6m6zNxbOi4o
+HxskKSvHnZqfoaixxvRIMyUoLCQkIylhn5qdnqaz1D47VDguKCQjIyhQn5mdoKu9uclXPDEtJiIjJyc7qJyinp+su/JQOTAvLSYiIyIpsJibnqOtvvRdTjsvLCUeICgwtZucoKWttcNtSzksJyknJiYqy56cnp+qt8tVOzc1LSgjIiIm/p2Znp+oudDbWDou
+KSUgIiUpRaGZnqCmrsDtSzguLCsoIyMoPqianJ6lschuUkg9LyonJiYlM62bnqOos77NZj84NzEmIiYlLMKdnqChrLzW4e0/Li0nISAhKNKcm5+iqKu2V0o8LSkiICImKmqfmpubqbe/VT8yLSooJiMiJ22bmKKfqrnfSkE7Ny0oJScyLzCom6KlrbnL2lQ6
+NDI+OiIiJiu0np+io63E53LSxTUnKB8fHym+nJygpK2tqL42OCskIB4fISvMnpydl57Bzlo2LyooIyQjIijYl5GgpKrGc0I6MjAuJyQmMt3gp5ulp6/A7FZNOTIuM2Q2ICg0rp6lpquzyF9KSb3NJiQiHiArtJ2foaexvKynQi0uIx8eHx8stZyeoJuYrUte
+NCwpJiIiJCUsvpiPlqmuzEc8My0rLCglJCrOop+epKu32k09PjcwLSxFxyojUaigp6i0vtFYRTx6rz8fIx8gMqqfpKGptcm9pbMsLCUeHh8gLqubn6Ghm51oOzcoJyMfHiInMq+alI+fw808NS4rJiYmJSUobZiTpKSsxWA/NjAzLywpNL3RKdefpKmrvPdf
+TT84Qre2KR8jIECmoaqorr7q5q6kVSUnHh4eITWonaKjqaObrDUzKiQhHx4fJTumm5mPlbldQy4sKCUiIyMlJ1KVjJuoqtNHOTErLCwqJyhjr9u9naWsr8pKPT02MzBwrXAgIShgo6WrrrjRZUrSpqwuIiEcHSJNpJ+jpK2wnp3ULC0kHx4eHiRZn5yelJCh
+STwvJiUjHx8hIilflouQo7PPODAuKScoJSQmNa+gn5+jrLrfPjQ2Ly0tM7+uOiEwtaOorLPCbUFAQrajyiMfHxwnwaGjpKq3ybCdqTYnJR0cHR4nvZ6foJyVmcAwLiUhIR4dHyIqvZiOjZi6ZDkrKyclJyYiJCrCl4+cp6nMTTgvLi0qKyxEr7svO6WkrK7B
+ZEM6NjZVqqs5Hh8fLK2hqKivxnjWp5/AKSEfGx0eLa2doaKjnZikRykpIB4dHR4hL6yZlI2Qp0M2KyQkIiEgHyEnXJSKlKWuyjcuKykoKCcnK82suriioq+00kM2MS8vNcaovSweJU6npKqsut1LRsWjqFEiHh0bHz2mn6SkrKyenbctJCMcHBscIEKhm5uR
+j5zRLi0iISAeHh4hJlyVio6ateI3KysmJSUlJCY7rp+anaaqv3s5Ly0tLS47t65vJia8paertNFOPjpEs6O3LRwdHCLRoqOmqLXArZ2maiUgHRocHCHlnp2emZKWqzcoJh8fHRwdHyjLmIyMkqhOOSkmJCIhICEhKcyZjpWoqsBINCwqKCsqK0avtkQ1raCr
+rLv1Pjk2M1erqmQiGx4ntaCmqa685c2mn7A2Hh0aGxwmup6eoKCbl5/HKCQgHRwbHB4qt5qSjI+ddC4sIyMgHx4dHyREl4qOnbHEOy4qKScmKCYrzauzr6agqrHFTDcuLy80yam4Ox0eNq6iqau62UxEwqOlxiYbHBkcLayeo6OpqJycqj4hIRwaGhweLaqa
+mZCOl64vKyUfHx4dHB4iOpqJjJSr6D0qKSYmJCMiIzWypJuapqq8dj0yLSwsLDm6rs4sH1CopquuxFI/O0qyoq4+HBsbHDumnqWmrriqnJ+7Kh4dGRobHzamm52YkJShViYmHx0cGxscIj2djIuPnl45KyYlJCIfHx8o5Z6RkKKst0s4Ly0oKisrQ7OzcS7b
+oKmus8tBNzk3aKmnxigaHB5GpKGorbbOyKWdqU8eHBkYGh9IpJ2fnpmVnK8uIyEcGxobGyBOno+MjZe6MS8mIyIhHh0fITeejYyZrrpGLywsKCUoJirarrnFtqGosrtiPC8zNDi/p7VGHhok+6KlqbHG5Vi3n6K2KxkaGBog+6CfoaOimZmi3yIhHRoZGhsf
++52Wj4yTpDcrKCAgHx0bHSArp4uKkabYSS0rKigjIyQjN7Ospp+lqbXNTjovLjAvRa+txTAbJ76kp6u151tJ7qqfq14cGBkZIsifpaWor6Kanq0uHR0ZGBocIdOcm5aOkpzJKCghHx4dGxsfKa2Mio6Z2T0vKSgmJCAhICjLppyTnq+w7EE0LysrLC/frbfy
+JyevpayuvFU+Pj/Fo6a8KxcZGiW6oKerrbuxnZym1h8bGRgaHCe/nJ6ckpGapzYjIx0dGxsbHiuwj4qNk68xLiglJCEeHR8hPKeWjZaytmM3Ly0qJykoNLmuvGQ5rKWysctLNjc4R62ks1seGB0qrqKprbbIzqicoq4vGRoXGBwrsp+hoZqUmJ/DIh8dGxoa
+Gx0trJSNjZGePSkqISEfHhsdHyyskIuPo8dpMy0sKSUkJCdirrKvraqosrzhRjUwMTLWqbHGMxkcNqymrK/I71W7n6Cqzx4WGBccMqmipqWkmZiepzcdHRkYGhsdMqaYkI2RmbgpJyIeHh0bGx4mto6Kjpa+PTEoKCYjICEgMLeuppidsrLLSzkvLCwsObWu
+v9onHVOprK+260hCYaqfqbI0GBcZHUWmpquprqGZnaK8IhoaFxkaHjyinJiOkpmiPiEhHRwbGRodJL6Oio6QpjUtKSQkIB4dHibVqp+Pkq681ToyLSooKS3ur7u+ZCzZprK1wUw5OT6/o6iy2SEWGR9+payws7+xnZ2kqzwaGBcXGh9foqGdkpKbnrUnHR0a
+GRkZGya3j4uOj5r7JygiHx8dHBwgQKiZjY6exVE2KiomIyMlNbq2vra2r620wuk9Ly8wSayntL49HBgovaiutcPx0qadpam+JhYXFxknvKOno5mUmp6mSx0bGhcYGBsor5OMjpGYrSwgIBwdGxkaHS2pkYyNla42LiUkIx8fICfnsbusm5uzu8pCNiwrKzHA
+p7O9zy4cMK2vtLzpRkK5n6SrsT8aFBcZLq2orKujmZqgpLkqGRkVFhgbMaiZj42TmKLqIB0cGhoYGRsmqo2LjZCeQycnHyAeHh4fOLazpJGQpc3dNCwoJiYnQa+xwsFmNkmstcDLRDcxU6qjrbHVKxYVHDyqra+1taCboaWtdh4VFxUYHEaln5WOlJqfrzEb
+HBkXFxgaIa2MjI+QmbclIR8cHRwcGyfJrJ2Ojpi5PjMlJCEhIizMsb++v6+qs7bNVDguLDO/pauyw0kiFiDarbK3wN2xnp+nrL40FxQVFR/fpqedkpWcn6rNIRgZFRUWGCGvjoyPkZmmOx4eHBsaGRkeRaiWjI2ToDorJR8gHh8jOre6vbKel6rJzTwuKCYo
+QK2qt8F4Oh4kurG7wWxCZKifqKu2fCETFBcjvamtp5qWnaCnsz0bFxYUFhgmrpONjpKZoronGxwaGRcYGyunkYyOkZnKJCEeHR0cHSV6ub6ql4+bxVI3KSUiIyrRrLvC1GY+Prqyw9BFNTXBo6irsss5GxMYK7Wss7Com52ipq7FKhYTFBQYKa6ckI6VmaGs
+Vh4YGRcWFhggr46MjpCXqDAdHhwbGxscLr25pJKNk6ZCLicgIB8hM7e5xcfQuaiuwMJdOiwqOa+nrrLKUiwYFzezs7q+xaiepKetuksdERMUGDKso5mPlpuhqrkxGRcXFBYXHcKOi46Ql6DKIhscGhoZGR9isZ+OjZKaySYjHx4eHiNNusvFw6uZnMvcTi4o
+Jipqqa61xGU/KB07rb3D1kzLo6GoqrbVLxgRFRo7q6ujlZadoKix3yUVFRQTFhzvkoyPkJigrz4cGRoXFxcaLbKcjY2SlqguHh8cHB0dKNfAx7ejkpOrUkIsJCQkLrqsub/pVkE3VLC83ls4Obeiqau000ElFBMdQqywr6CZnaGlsL5AGxIUExUdYpqOj5KY
+oau/KxgZFxUWFx/Gl42MkZWf1B8dHRobGx0zwsawnZCQnMcuKx8gHyM8tbvHzl7wuKu9wfk7LytCrKitsM5OMx4UHt6yuLu0op2io6245CsVEhUUHtefl4+TmZ+pslsfFhcTExYaO5eMjo+Wnq80GxwaGBkZH0+9r5iOj5alLyUiHR4eJVa6zsjbxqSYrPra
+MSooLOSprrPEWEAvHR+8s8jD47yhoqarudI8HhETFh7Cp6KUlJueqK/FNBgUFBEUGS2ajI+Plp6qzCQZGxcWFxorvamUjJCUnO8hIB0bHR4q28fPwLeckZ3nSjEjIyQuvq26v21JQTQwv7Lm7UE/taKoq7PNTy0ZERglu6yunpecoKSuumUjExMTExkrn46O
+kZWeqLRAHBgYFBUWHVGhkYyQlJuvKxweGhobHS/M0r2rl4+XrTctIh8gIzq2usvWTlnSr7S+00Y2Lkirpq2vylY6JxYYM7m0uq6fnKKjrbnXNxkQExMYMaaWj5KXnqivzCkXFxQSFRktn46Nj5SbqGAeHBsXGRkfScO6oJGOlZ1VJiQdHh8kU7rPz/7tspuh
+2dc6Kyks4aqutMVPPjQhG0SxycTOuaCipqu6z0cmFBEVGT6ooJSSmZ2nrrtLHRQUERMYI6KMjY+UnKa4LxsaFxYXGCnKtJuOj5SZsygfHhscHSrYzt7NzaaUl7lTOSYkJS+6rbm/VT89Ny7sr9PcUkqvoaeqttJRMh0SFR1oq62clZqfpa+76iwWEhIRFiGn
+jY6QlJ6nsXMgFxcUFBUdTKuWjZCUmac9Hh0aGhseNtDaxbackJSiVS8mHyElQrO6yHRFR1+0r7/TTDkwUqqprrTVTjcqGRUnxrO3qpycoqWvu9dBHhESEhYpqZOPkZaeqK/ANBkWFBIUGS6kkI2QlJqkwyQbGxgZGiFS2cmplo+Vm78oIx0dHyfZvd7nTk3B
+nZ3Id0AsKi3EqrK1ykw6NCUbMbXCv76qnqSnrbvRTiwXEBMXM6qckZKZnqmwvlUhFBMREhYjoY6OkJScprU6GxkXFhcZMNHDn5CPlZiqLh4dGxweNcXz+n3+q5aWrk47KCUlO7CyusVMPTk2MFC3zd1l2qelq6282EszHxMSG0StqZiUm5+ps7/vMhkQEREV
+H6SNj5GVnaey0SYYFxUUFiL4tZqOkJSZo1geHBsZGyNTfVfjzaGTlJ/3LSYfHyjctMbMTD47R7mvwtJXPTq/pq6vutpLNiwbEx/dtLKgl52iqLK+4EgiEhAREyKoj4+Slp6os787GxUVEhMaO6uWjZGVmqO7KBoaGBgcLWZM5rebkZSZsyohHh0fM7vK41lC
+Qe+hnLpiTS8tP62ttbjWTTYzJxolvr7Br56eqKiyvupOLxkPERQmrJWQlJifqrS/eicVEhERFymlko6RlZulsUgdFxgWGB9HVeyqlo+VmKQ6Hh0bHCV8yFVaRkjCm5WnTDorJCvPrru710g3NTErM8TF782uoaqrsb/bTDgjFA8XK6+dk5Wbn6qywNM9HRER
+ERQhpY+PkZadqLHJLBcWFhQZLVz4o5KQlpmgxyAaGxkdM9ZJTU1TsJiSnL8tKB8iO7a7xdRGOzY677bD3GtG2qqqr7C+1Uo4LR4RFzi2qJiWnqOstMPdTioUDhESHamPkZOWnqm1wEgfFBQSFCJM0p+PkJWZobI0GhkZGSVOTERX8KeUkpimMiAfHSnPu9Ti
+RTw4PrmerUZRMzW9rLW1vt1JNS8pGxhDuLqfmaCnrLS/4FE1Hg8PEhuuj5GUl5+ptr/iMRgREhEbOr+dj5GVmqOv5CQWGBcbMEc8TdKhkpSYnt0eHBwePsLuVkM7O0WumJ1jOC8mSLC2vL/qSjYxLyojQrnYrJ2hq6y1vtpOOSkYDhIcvJOTlpqhrLbB30Uj
+EQ8RFCu5mo+TlpumsMQ9GhUXFiI+PEfAnZGVmZ2wKhkcGyjd/0dEPD5eppWYrTMpIirBtsHE/kU3MTA5X9nWbtWppa6vt8PdSTctIhMQHsqblZicpK22w+VLMxoODxIftpeQk5ecp7LB4ioWFBQZLz5Ft5qQlZmfrFwdGBodOF07Pjo/0p+UmJ9gIB8gPrvN
+0l8+NzIzRaukaEhK1aqwtLjI4EY2LSsfFSC9p5mYn6avtcLdSjkmEw4RGr6TkpWYnqm1w99BHhESFCA8Qq6WkZaaoa7CLhgWGSJDNzg7Q7ybk5mcricbHifczGlYOzg0OvCfmLY4OTa/r7y70P5DNS0sLCEnxrejmqSqsrnF70g2LR0PDxnmkpOXmaCqtr/u
+Uy4XDxEYMF2qk5KWmqGvvmojFRYbKzsxPUqtlpOZnKdJHBofMdRLQzY1NT2+m5WhUikqObm/yttRPjEuLDFC3dzXtaClr7O+yWpKNC0nGQ8Yb5eTmZukrLi+5E08IhEOFCLUoZGUmJylssfWOBsSFh0xMjhnppSVmZ6ovykYGiJBTTg3MTdFrJeWnK8pHyha
+veDgSjswLy42vaC2QNuzqLS3v9NfRTUrKyQZGGSfl5mep7K7yOdEOywaDg8bdpuTlZmeqbPK304pFRAWIzU2y56Slpmfq7hMHxYbKUI0MzQ4aaKUmJukRh0dLt3uUEQzLi4xPauYoko3T7i1wcP8Tz0zKyssKCdcrZ6aoqq2v899RDYuIxYPF1aWk5eaoKu3
+xP5VNx4QEBsuPriYk5iboK683jAZFBwtMS41OMWbk5mcosAmGR84XUA9MC0uNHCglZyzLipNvsjRekQ6MCwrL0C7vd6rnqWvt8feWkQzLSgfFBRGmJWZm6awvcpjRzsqGA4TJEiplJSZnKSwxdVEJRQUHy0uMkOvl5SZnaS1Px0YJD8/NTAsLjm3mpWao0gf
+KHPMbl4+My0sKzDNn6RM8qupt7zNXEo9MSoqJh4aO56Ymp6ouMbTXkA6Lh8SDhtXnpOVmp6ns8fYUzIcEBYlLjFxopWWmp6otm0rFxgqOi8uLS1BqJaXmqC9JRsv91lKPi8rLC04r5mcuzZds7rFzFRCPDIrKysqMlCwnZuhqbnO3FxCOS8mHBATQ5mUl5qi
+rLnM+U45KBYPGiwzypuVmZugrLrdPSAUGy4uLC8uWZ+VmJufsTwcHTxWQj0vKiwvSaSWmqVKKVq+zdlWOzYvKiosNrqt966dpa672WJOPjYuKCIZEi6ZlZmbpLG9zG9MPC4eEBEjOrWXlZqcoq690kwuGhMfLisvNcmblZmcorH8JxghQzw1LikqL9udlZqf
+uyUm7NNbUDovLCoqLUymncRWqae2vdZORj41LSonIhwrn5aanKW2xs9mST0vJRkPGD2plpWbnqayxNdONyQUFCYsLDyxmZaanaWy1zsdFyw+Ly4rKTG7mpaanqw4HS/QT0o9LiorKzDNnZmoR2ysucjQTT07MiwqKCgvQrSbmp+putjxWEM7LycfFRAuoJaX
+m6CrtsrcUDotHA8aLSxGp5eYmp2ns9FOLBgbOC8rLSo0rpeWmp6s5iEdRVU8Oy0nKCszuJqZn78u17fc2007NC8rKiosXa2/q5qkrLzeWE1DOzEoJB0TIJ2VmZujr73L41k9MCYVESYwTJ+Wmp2fq7fTUTgfFSMxKCwrOqqWmJufrsoxGyVdOTYtJycqOq2X
+mZ6qMyy/z2ZSOzEsKyosMMGeq9efo7S85VNCPzcvKSUjHB6llJycpbTL2HtPPjEpHREZNmudlpueo628zlg+LRkYLCopLkejl5mcn67HTSUbN0MvLScoKT+nlpmepe8kO8NJTDgvKSgpKziymp/RuqS6xuZMPTg0LSkmJicstZabnqe60GxpSj8yKiMZFCu6
+nJWcnqixv89kPzQiFR4uJzBjn5ibnaSxzF02HSFDLSooJypFoZaanqa9LSXeWD46LSgmKixCqZicrmGys97WRz4zMSwpKCgxz6+fmaGpvdhcT0c8MyklHxYerpqXnKCsvMnkXT00KhoXKSouwpyYnZ6otM9dQiccLTEmKSYsYJ2Wm56ouEclNew4OSwnJSct
+Xp+YnKPbUbfxbEY3MCsqKCkpO6uirJyisL1nV0A/ODEpJSUeHq6Wm5ykr8ba+VY+My4hGB8vMK+amp6hq7nNWUkzHyEvJigoLcObmJyerL14LihHOi8sJCUlLs+cmJ2gujvj0kdHMy4pJycpLE+fm6ylornCaEs+OTUuKiUmJymwlZyepbjNZF1KPzEsJhwb
+Kz2qmJyfprDA1VY/OSkeJykkKjGympqdoK3F60AqMD0sKiQjJi+2mZieoa9NQNlIPTQsKCQmKTHInJmkq6m43mZAOTEuKyglJSxLp5mcoKm+8E9IPjkwKSYiHidkopidoaq6z/hWPTYvJSEnJio/p5mcn6Wwy2ZROC4yLSYkIyY1q5ianqay0UVKRTYvKiQi
+JCc2r5qanqazv31NPjQuLCcmJiYuuZubnqKuxV5IPzk1LygkJScsyZuZn6Ovw/hWTTwyLiomJCUrX5+an6GrudVRRj84LiglISMnQaKZnZ+pus7kTjk1LSciICMnQaOYnJ6irtRfSjgyLCklJCYoOamWmJ6nst5OPjcyLiwmIyQqRqybmqCnt99URT84MCwp
+LCokL7mcnKGlscPvTD86QD0mIiQiKtKdm56grb/q18o8LS0kISAiKd+cmZ6gp6nDP0MzLSkkISElKlqfmJearrz9PjowLiooJSIkKsqZmZ6grb5bRj05Ni4rJis8KzWlmp6jqrvhX0c7NjdQLx8kIy6wm52ipbLOXVfF2SoqJB8fISy5mpqfo62trEk2Nioo
+IR8fIy67m5mZlqfh9Dw2LisoJCQjJCy4kpOioa/PSjw3Ly8sJyQmPVxPn5qhpbHITUdANjAvPE4jHyg6pZyfpau55ExHark9IiUfICE2qZucn6ezuqq4LzMrJSAeHyE2qpmcnZmd8kU/LiwnJiAgIyQwrJKOnaqvY0M2MiwrKyYiJDG5qJ+aoqi56kU8PDUv
+LC5aQB8q1J+do6a0wmlKPj/CvSYfIh4kQqKcn6Grvde1p0ooLSEfHR8hP6KanaCemq02Py4pJiIfHiIlPaWVj5Ksuuo6Ni0rKCgjIiMqtpWYn5+tv089NTMwLComN8YvJ7Cdn6WrvvZQPzs0SrNMHSEgJc6fnqOlsMRm4qquLCclHR4eJF+em56hqZ+d7y81
+KCUgHh0gJ2OemZWOnfVwOS8rKCUiISAjJ8aPjqGjrd9ENjAtLSooJCjpvTytmqKnr8tMRD43NDPbti0dJCu5nZ+mqrbXWk3Fps4jIh4cHibJnpyhpa+vn6c1LCshHh0dHinInJqbkpS2Pj8uKScjHx8gIirFkoqWqK7ePDQvKiopJiMkN6+opJugqrbgQDk5
+My8tNsLLJCA4rp6jqbHCa0lBSbOpNh4fHR0qt56foqi2w6yevCopIx0cHR4qtZubnpqUnU44MSglIh4dHyEsu5aMjp+84jgvLCgmJiIhIinDmJWdoqrBWzszMy8tLCpAtFYkQ6Wfpqq52k9BPzpyqrclHh8eL6yeo6Stu9nJpKFOJiQdGx0eLK6bnZ+jnJiu
+Ni8pIh8dHB4gLa+Zko2UtVI8LSsoJCMgHyElRZeMl6Wpykk5MC8uKyonKs+vTFufn6qswllDPDk4N8ysUx8eITGon6eossx7W7ygrDAfHhscHy+qnKCiqKucncgtKiMeHRwdITGrmZmRjp9eOTIoJyMgHx4gJTqcio6frsk8My8rKykmIyQzs6utn5+qs85E
+Ozg0MDA5vbM2HiNJp6CprLvlT0tgr6HDJR0dGx85p52ipa24qpukRCckHRscHCA3ppqdmZKXtDcvKSMiHh0dHyQ7n4yLlazuQS4tKigmIyEhKN+fl5mjqLf9PzY1Ly4tLEOxvi4mz6GlqrDGWURCP+moqjweHB0fTKOfpqq2ycSjnbUtIB4aGxwhQ6Kcn5+a
+lZ/4LCoiHx0cHR4lTp6QjI6ffjwwKikmJCAfICQ3oI6Poay6RzkvLywqKSgq2q3POrueqa247UY6Ojk6w6a+KxweI9ygpKiuwdtouJ6j9CMdGxocI9yfnqKkppqarDkmJB0cGxweJeicmJCNmLkyMCglIyEeHR4iLaiLi5mtxEIwLiwqJyYkJDOyrK+noKiy
+v1M+NjU0ND+xrV8kHS27oKertdpYTnarn7AxGxsaGye9nqOmqbKlmp/EJyIeGhsbHie9m5uYj5SjQysrIiEfHRweIi6rjIqQoXNHLiwqKCUiISEo0aSclqKqs+5DNTEtLi0vXa67PiExqqWrr79SQT9Cy6Sn3SEZHBstrp+nqq+/uJ6cqkEeHhoZGx4tr5ue
+nZWTnLcsKCMeHhwcHCAxqI+LjpnNNDAnKCUjHx4fIzyjko6bsbVOODEuKykqKS+9rsRASqOmr7PRRzY6OUKxpLg5GxsfOKahqa+70eOsnKO9JhobGBsfO6eeoaOelpqmTCIjHRsaGxwfP6KUjY2VqjAsKCMiIB4cHiEtpY2Lk6rGTi8uLColJiYmSq2vrqql
+qLS/WDsvLzEyZ6qv2igaJN+kp6y13llNyqKfrUUbGRkaIeSho6anqp2Zn7MpHx4aGhocIeedmZKNk55XJyggHx4dGxwfLKiLio+dfD4tKSgmIyAgIS64qJ2Voq616kAyLiorLDa8rr5YICmxpquvvVc+Pk+voKm9JRgaGii2n6errrepm56nUx4cGRgaHCe8
+nZ2ZkJOcrisjIB0dGxobHi2mjIqOlb0xLSYkIyEeHR4n3aKWjZmyuFQ2LiwoJygsT7G2vkc7qaexttFDMzQ416eot0MbGBwtraOqsLvPvaCdprUoGRkXGRwvrZ+inpaUnKJcICAdGxoaGx40oo6LjZKjNSonISAgHx4hKd6cko+Yvj4yJycpKyouNfqknqGi
+yzxJOzYwLSwzRsSqo6OqaigtNTxRXVRMXc+5rqqqwyomKy07VNrAta+wtrOwwS4lLCsuOUf6vK6opqapujIgJyouOT9EbMKzq6ejpcQrJycoMDtHZs3BuLCrqLJBNz02NDU7SN/At7Syr7o8LjxCWezzUVDtyLy2sLg/JiwxPWjMvru3u8K/urtdKiktLjpN
+2r6xq6qrrrfmKyUsLjc/SGXJta2qp6evRCcqKy45RFTnybu1rqqrwDc3NzE2O0FQ2MG7ubSzxzg1SEpZXk1KXtnHvbi2yjQqNT5Wz8XFwcHEwr67wT8pLTE5T9bEuK+ur7S7xEYnKS8xOkBO3bqvq6mqrcktJistNT5IUPXGua6rqaxtMjUwMjo+R2LPxL+7
+tLDMPEpVUltJREtvz8S+vL9rLzRMXcnFyM3Y1tDMxsHcMSkwOE7Pxbq2tLW9x8reNSgtMjZAT96+tK2srrC8RCgqLjM/R0xfzrqvrKqquD8vMS84PURT99HHv7eusu9BTUpFPz1BTPzNx8S/wFc4RV3ez9jxWF3n1szBv2cvMT5L6dDBvry7w8zKxl9i5snL
+3XllZu/b3n5fVk5LTWNy78zGz9/p7m9idO/3WU9PTFZv38i/xdtfXWn/9u/iX0xOUV3t1snK0OB5amT48m75a1JTWl9cXtrLzeVxallj6eXd4mNORkpTZuPPzexjcnXt1NDXcVROTlFXduDPyM/f3Nvf7O/p9GBOTU1OYdzJw8vfZFZbbuXq9F9JSE1WddbH
+yN5nXWtu9tzs+XNYU1llcnXZzdHtXm1obtzW2N9sWU5PYOjMys/7VF1269bT1nlOTFBkeOfQ09nvYm/z6ehramRaVFJfYm3XzM7be2pVU2ju5G9dTkpXet/NxMffXF9z7eng6WNZVVhp5dvY1dDY5/x1bV9mcW5gWFhXVWTfysjV9ltXW2j37eptUE5Z+NrX
+y8zhbF5kZmp6Y15ma/f87OHp0svO42ZhVU9f/d3feG9faujazMnQckxOVWXr7ef9W1tk/evn2tvmcl1mXmBzaXL9Z1pZXl1h3svI2WReWVp84uLvZlBNWuzRyMTH3VlWX2Ns/mlXUlRj7uTb1c/P3GhXWVVRW2z182FdZWrw1cjJ2GRNT1p82drrblNQXu/d
+2M3S911b/uft3+xubGZmZ3n3et3Q1OdfYmpm7t/h/11XT1Z23szL2W5PVGnz5e15Wk1PW/bd4NbT335hY2JjaWBfbHBxannl4c/Fx9pjWFRWbt/d9GBaWWfcz83Kz3VNTVxrdnR0ZVtfevPq4OHd2eJyYFtdZ3ro3+1mYF5eetvLyNlkV1Zf79nd621TUV/o
+0M/Ny91jW2RkZXxoXGFn+e3w29jTzdJ8UlJPTV355eZlX2Vm59TOzttfSUpWauLl9fVeWmzv39rU1NzwZW52aPH4cu32aGVz8OXSyMjVX1ZWT115a2FdU1Be48/IyMvdU0xVWFljYltbX3be1tLNztHcZU9QVVNaavTkf2nw5d7QyczdZU5NVmnn431zXlRk
+4tfWzs7jZFddbmZsc1xcXlxfft/h1szQ4V5UW1VYZ2lfWVdWX+PTysfS6llOWWNpbnJoXF504M7Py8jU6GdYV1teXWJyfvN8+tve2M3T/FhPT1JcbvDvcXRoaOTX2NLU9ltYXmx+ee/3aWpuanzj4NfR2ev8bHJ6am98bltbX2nn2MzHzuD2ZF9jZV5ZWVNU
+YfzXz83Hz+9oXlZPU1ZYX19len7b1tfO0/lWUlNTXW196nds/m/o2dXS1+tiYXf+5d7m4m5bX2Boe+ba1t3+cX9v/v1maV1PTVNd/trOy8/rd3RfZGxhWFZRU2bk0srIyc/6XmJdVVlcW2NpceXY0s3Nz9b4WFRWVF1udPX6a/rm4NnS1eVuWVplZm1+Z2Vf
+VFpu7urd19vje236bGFmWFdZVVRc/uPRycvP3v7zbV9nYFlWVlt62tHMx8vR5GdqZVlTUlJVXGN63djTy8/Z7V9WU1FWXmJob3F94Nzazs/eb1xcZGZrfH1ubGZn8+Hk3NTZ3+379fZ6em1fXFpZXvji08rM1N3s+25eW1hVT09XZt7UzsfJ1ORuYllRTk1P
+UFdl/dnOzcjL23ZdV1FSV11qbHHp39XP0M3P42BZXmVud3b2fWlvbvvq7uTf63Ft+fn17XtvXlNTVVtq79fNz9vl297q/WNZU01MU2Ho183JytHe4O5tYltTUlBWafDbz8rIy9r9cG5eWlxbXltba+vc19PQ1+plXGFgZGtjYWdhZm787ujc4/hqbPd6b/Z8
+bGZcXWB48ebW0dXf6d7f7+95Zl1ZV1lp69vPztPc8fl1ZF5bVlFRVmB+49rOzdbmdnFsY11bWl5iZGvx3dvSztXpb2ViYGl5b2prb27+5N3Wz9bvaWz3fXN6e29nY2Nm++nl2Nbd7Pz09/51aWJaVVZWYfzez9DZ5/R9bGZjXVxXUVVd/eHZzcrR4fh+bWlq
+X2FmaG1z6NbRzs3X9GdnXVtndHlqXmRqdOnh2tnhaVljbXB8ff79Z2Fka/jn3tvd6f309X7v+m5qXVZZX2zr1MvN1+jk5n15cV9cV09SYurZ0MvL0+v7/2JdXllWW11hbPPd08/S3W5dYFtWX2xvaV5fa/7q3dbb6mdYX2119fPy7nptcfTs5djZ3+j79v50
+7+rw/mpdWV9pcd/V2ettcP5zePh0YlpUUFz65djR1Nvs9/N3a25qXlxfYmzs39PN0Nrqdm5mX2f99XJlYGrs4NrR2OxrXl9r++vo7XRlYV908u/g6HBob399ffTr7HNgXFVZYWTx3eP/cXr+9uzq7mpXU1BZ9t7Vz9fg5+Pn6uv4e2JWWmFs7ODY0Nbm+3lk
+Xl9eanVkXmBn8eHc2N10Xl5bYfjl4+h3am9v+unp5OpqXm/28uff3t/wZ2RiaHz159ze7+7s9ejh6O9tWlhYXfzc1dbefXj0fXb8cmZdUlVifebd2dnffmZpX1peXV9ramhufunb1dfe/19gXl1/4N7g83j159/c2NzwZFlgfffp4eXp+2pmaWxrevx9/3F0
++3j75OPycWFcXGNy39LW3vdy/fp/7/BuXlhYaOXa0s7U3vJ0cG1maW1maHX+7t/d2tHU4XhfWlhXY+3m7PN6cfXj4NredFpSVGB3693h/G1sZ2x/fPPvbmZpb/Xv7uLd6XVsY2N4893P1N3rfn306unsb1pWV17r2tLP235lZ2Zna2hmXlhdcPTd2trV2e5q
+ZF9eYmh+7fv7f3Lu3dnW3HtcWl1n7d7a3vptcnvy4d/g7WdaYXns39/i5/BuaWhmb3nx5Ot4dHJv/Ojn8nRdWFhbb+XZ1eFtX2ZwdvT8aV1YXG7v2tPR0trrb21uamxydPf69Ojr49zU1eB5W1ZYW2zt6up7am977Obf4XdcU1hqfuXe4uz8d3F1/v726e78
+a239/fHk3uh+aF5eaPbc09TefWpv+/Hs7HBdV1tq69zSz9frbmNgZWRiZmJkZ2z25t7a1df1YFdSVFhi+/Z+c2xy9eDd2t5zW1ZceOzi3OLveX348OHg4N/wdm9v+fj79PV4ZWNgX27x3tbd72teX2VwdHBlWVZecODW0M7a8mhiY2VraGBeX2vy5NfT0tDX
+6WleWlhcavju9/Lu7eLX1Nbc/FtTVl5t++nteGx08ere3ODwaFxbXWVzev54cmdnbXL57uPf7HNnYGJpf+71/WpiZ37e1dDP2vJta3d4+/ttYV1je+rZ09TW4v5gW1xaWl1jamtv9Ozf2dTU4HxcVVVda/fs6/5ve+3f29nb7mddX2/25uLn7v51b3rz6+nh
+3+n8fPz89e7q+mxfW1tl7dzU09v3a2hub29xZFpUWGby29TT2ed2Y15gYF9fYWVmbPHk29bT1N/+ZF5fZnjs5eTueP/t3tra2+5lW11p/u3o93BjXWJr9unn5Ot6bG/6/Hx9dm1gXF9n+t/W0NPd/GdobHJzbmhgXWT/39LNzdXpbGJkZ2tsZ2VmZnvl29bU
+1druZVtaXWJrcnZ4bmx18eDa2uD8X1ldZXT8+m9iXV5s/Orj4ebzdG7/7erv9XhybGdv++fb1dPY4vN3dnN3empfW1pfcefZ1NPc9GlkamhiYV1bXF5x69zV1NXc6HZmYGBfZWhqbm/+8eLa08/Y7WpfYWpx+fT9bWZr/uTb2trf9G5qefXyfGxlXl9ka3rs
+4tva4vF6dW1rZmZgWlZXXm7p29XV3fhrZGZpZWBcW1xo9N/Vz8/S3PF2bGhmZ2prbXB27+be2tra5GxcWl1lbXp7b2piaW/z4uHk8G5laXzu6ev9bGJeZm157efi4ury9Ozp7fhya2VeYGr84NnU1dzrfHh0endqYF1cZHjr3NjX2+lwZ2traWViX2JlbH3w
+5eHi5+9tX19iZ21zcWxoZmpy9N/b3OP7cn/u5uXo83ZpYmz97eTf3uPv+vLu7PJ6Zl5dWlxjcu/i3N7p/H3/fHd8bWFdW2Fu8N7Z19rl/HX7+XZyamRjZnju5drX19vk8nNxc3N1d29saG7569/b2d3ubGNpb3v8f3JqZGRv+ern5un3bWlve/7/e2tgXV1g
+aH/p4d/n9Hn87e3v735oX15r9ODY1NTa5PX69vZ4bGVfXl9s+une29vj+21paGhna2xlZGRs/+rf3Nvg+mtqdPv9+fd0a2hqdfTk4eHi6vb+8ejq7e58a2ZpbW/x4t3e6O728unq8f1uY1pbY3br4dzd6fx1c3r9dmddW1teZ33m397f7XFrbmtpaWtrZWZ0
+9erd2NnZ3vFucH3y9Pfxe21uevrr39/o8ndrbHH8/Hp4bmdgZWxtfu3m5/b+8e7q6ez5c21fXGJw7OXf3efw7urq6er0bWVhaG/+4t3c3OPy/fLyfXd2dnBobXp/7eTi6PF2aWlw/ff8fnVmZWxxffPu+m1mZnF++fH6eG9nX2Rqb3vw5+Tp8enl5OHj7H5x
+ZmJoeujg3+Ht//zu7fX2d2FcW19qee3k5enydmpvfXBtb25uaGt78+be3ef7cGZlb33183tza2/16eTj4eh9bm/56enq6/p4cW1uefPz7ejr73p68vT07vpqX19eY3bu4uLtfm9x+/N6cWpeWFpic/Tq5OXu/3dxd/d+c3p3efry6uXe29rf9nJsaG998uv2
+enZ5/e/q7O58YVtfavjv8vB+cnF6enn4+Pr4enl+/Ozs7urvemtpZ2d96d7c5e739+zn6/N7amBkcPLg3Nze6vr+/H7+eW1pZ2l3+PHo4t/h63NnYmBkanP/fG9tcXz05+XseWJcXmn/7/D3eW9w+u7r5OPp8fz87Ofk5ent9XxtbW1u/Ojh4u18cnT9+/p4
+a2JdXml+5t3e5vpvbnr5+n9waGlu/u3o5N/g6vluaGtwdf3u7evu9PDt6ODe6n9pYWV57ufl7PT/fPjv7u3ue2pmaXfv7vL1/HRvbW1ubHX58vh8b213/H//emtlX19neerf3ufu8/Pr5uvw/21qbvzq4uDe3ub3fHZ2fG9qbnJzd3Z5+ezn6PNvZF5fanr2
+7fN7e/rz7efl6X5nYmh77ery8/T5/PTu7u7t7e72+/z67uvr7O31em9uc/nk3d/se3V3ffj9e29mYGJy8+jh5Oj1b2prbWtmYWJma3b79+/l4uj4bGNfYGdz8ejo7vHr5N7b2+L2al9ldPLs7/j+/n337ezr8n1sZ2Zpbm9zeXx5dnNwcn3z5eTr/mtobn/4
+8/J7bGts/+rh3eHwfHJyffT2e3Zycv7v5+Tl3+Dr/G5qZ2pucvvz+3hzfO/o4uDsb19dX2l7+/h4a2xuefbt7PdxZF9janF1cXR3cXF5/vXt5uDh6PR/eP7v7e3t/G9scP3p39zh829sbm51eG1oYWBneO/o5OPn73lucG1sbm93fHv7+fHq4t7h6n5mYmRs
+//Hq7f599urm4N7l9HVrcn7x7PH09fl9/vLx7+3w9fp5eXV5+/n5enFpYmZt+efj6f1qZ2tyen12amNiZXL26ePl6vJ+eH/4+n9+/fn18O3p5N7c4ez+a2ZocX75+PxvaXD97+rl729lYGRu//b8e3VycXP47u3p6vP8e379+vT5+ndvbmt17+Xd3OHsfXl9
+9fL4+HVpaW7559/b3ebyfXd3fXdta2tqbXX57urk5Ox9cmxnZ2xtb3Jtampv/e3o5ut3ZmZpbv709/n+/vfu5t/e4Ojv+vn19vX5+/l/d3x///Tr5+vyem5tbXJ1b29tZGRree7j3uLr+nx7c3h8bmpqaW/87OTg3+Dl7/34e29zcnV7fv/47ebf3d/j73Bs
+bXL99vj+enV4+O/p4+n0e29xeHl8eHBubGlqcXN3+PLx9X1zbm1xc21qamVja37p3dja3+ny+vz79n5uamxy9ubg3t7i7Pp0b21oZmZma3B1ffTu6ujr9XpsaGpufvj4fnZ5e/bt6ePo9Xt1e/rv7vT6fnl5fu/r7Ofn6+vt7/Dy8v53cW1ubnn27ePf5+76
+enV1dm5qZWFjaHbx6uXl6/l1b2tqaGVjYmNqc3v37+3q7fX9/Xt7+/v38fX48+/q5+Ti5fB9enn/9PR9eXJsbm53/vz7/3l1ff58fnxxbW1pbG9zfvfs6Ons8PT6/v50bWpkZGt67eLc3N/l7vD4fHltaGdobn7u5+He3+Pr+X51bmxpa25ydXz38+7p7PR8
+bGhpa3V4cnJuampwd3z3+n10b3J1d3v+e3d6eXz17+vl4ODj6O3u8Pb5em9tamtx/e7m4eXt+Xlxb2tqaGNjYmh19+3n4+bu+Xl3dnN3cm5zdXv57evp5+ny/np++PXv7/b4+3/98+3s6ent8Pf18fLw8Ph6dHJyc3727Ont8vj5+v55b2tpZmVobXn67uru
+9P55dG9ua2dmZmdtd/Hn4N3f5+vu8vj5+316ff738Orn5+Xn7/19enp6dW9xbm5xcXl++fTy/Xp8f/379vv+eW9vb3X99u3r7/Tz7/Hy9Px4eHd3ef707efk5urt7vP7/H53dXByd//v6ubj6fV+eXRvb3JwcnN0dnr78+/u9npvbWxsbnFzdXRxcnN4e/35
+fHVyevnv6+jp6uzx9fb08e7r6+7x8e7w7uzz+3x0bmpqcHr58vf+f314eXp2b21qam53+e7q5+vx+P9+e318dXV3eX367+nl4ebs8vr9/vTx9PT6/Pj17+7s6+71/f349fH1/n96dXRxdnl/9/j7fXz/fH39eHFsaWhnam96+PX5fnwuc25kAAAAIAAA9RIA
+AAABAAAfQAAAAAEAAAAAAAAAAP//////////////////////////////////////9//////////////////////////////////////////////3//////////f/////////////////////////////////////////////////////////////////////
+////9/////////////////////////////////////////////////////////////////////////////////////////////f/////////9/////////////////////////f/////////9///////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9//////////3////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////f39/f////////////////////////////////////////////////39/f39/////////////////////////////f39/f///f/////////////////////9/f3/////3d3d3d3d//3
+6+fr7/f///93d3d3d29va2/d1dXd5+93b29vb29rb29vd3f/9+/v7+/v9/93b29vb///////d3d3b3f379PT33dnY2NnZ2dra2dnZ2dr//fv7+vv7+//d3d39//r/+fr593b59vv62tr729jX1tZX2drZ2drb19dY+/3zsTN2dXr4+v3b29fWV3LvsjP0edn
+WU9ITUtGP0ZAPkJMSkzn79vXzs7Lx8fKzMjHxcC6uLm8vsDHyNVda0m0vlk+PDw1Ozs6Pj4+PkQ9OD9NTEtJTlVPTl1fVWv//+/b9+vCyL6/vMO7u7y6urq8vbu2t8W/xL29z8Vn06i4Z2dJQDUxLzAmZ0g/LSssLC8zMjg9O0E+T3dv38rNysjZ69PN2dXr
+193M28vL0ca7s7Kztba2tbWztsSloau2vbvNb2dTTUo+ODc2OTQxNTI2LzU0Njc7ND9DPUxPUznd4/drXcbBy8Hb511d/8jJy9HO39nCzdvGT6OrucXDt9vFzctPqry91ztCNjg6NThAODk4OD1HQUVES0w8OWNLV0VRSl1d49Nva8vMurqut7q8tLi7vsF3
+p6a1v8a/T1v//2NLPEdCPzM6NTw8PUNDS1NX7/9f/1tZTkk6R0NKQUv/V1NbU01Ta+vjv7u/wb20uru7vbefrcHM2WtR7909rsJjb005Mj04ND1LQ0xMX1XVzNnLvtHra01L92tKR11LZ05NPknbX8LLxsbMxL7Vzs1LtqrCvdXJQFFOUeddSUpfSTw9U0NB
+SFFna+dvY83L3dPBz+dnREFPQ/dba1FKRU9DOV1K23fDzMbBvcLGwmO1p7bJxsVr69fXX6m7xN9PYzZBNztMQz1ZSURZ691j68vJ53dCV05DSUFEREZPPTpIV0Zr39vR2efF1dPP37KqvNfLv1vFyc/M42tn1U1PVWt3SlHvd2NjW1lvY2Pb5+NXWVE9Se9b
+U0dKSj85ODpBPvfRz8e7vry/vcD3oqq+y8DGXdfPY7WmycjnTz5BQ0U9/2NKU1lJVWdvX2/R92dRUVFJS0dLRD89Pj05PUVN69nLy73Fv8m/vcetp7PJxsr348bTxs1fS1tMP0U7REBJTWdMRkJATEpRVWNbV1lESU3n691dT1VTSzhFSkhv38jIvrW5xLW0
+w7yhrsu/zvdb3VNCpsXTd1cyODk4Nz9AOkxFPkR3TE7/3f/vZ1ljY1Ff71tdVU1PPUdnRsnTz8m3u8y+vrrEsaWxx9HZTULO3c7bTkhdQzY8OzY7PkNDRFM+QFFMTXfrb2fnd2dvxs7A2dnfY11HTWtZZ8nTvb+6wcnDvMZDobDK31vvOl//OanLZ19bNi46
+NTVEP0lNSUNN6/9Ozs7f2+t322/N18rLb9njZ09DW1l3yO+8y83PyczHyDursMZXTVcwX2tf70ZATkA4Mjk3NUM/X1tJUVXr53fJz8zLzc/Z17vBu7rX281RSEREb07nw8jAytnNxczD782sw1tXTTg+Pry8XVtjQS0yNzI0PWtIZ11XZ87I78jGxsjJzczJ
+yMzKwN3bzltERELfWd3Ly87b1c7X281rU6vLR0w8NzTnSlNMPz89NTU4NjdCTe9v6+fXzsjLwb++xsnP0b27ur28289dS0U5P05CzffXzuPd3cfNyvdFqrldT0s6Ojmux+tZSTgwMjM5N2tGZ9X/6/++xsbDv8HLyNvNvsHCw7/TxVdRQjpKUUXXa/9jU+/j
+X9/bbzWrr+M9QUs0SmddX1VPUUU9Nz88093vzcPN18PAy8fCxtHb79u6vcjIx8ljW0I+N0VCX3dv12Nvd9vd59vZNMeu50RNWyurw+fNS0c4Pj02Pk9Z99XVwcHOvsLBysHEx9vvxcvE49nX0U9FRzw3O0HrWWNrXVtvZ3dv498/N6nJPGNVQUxZ68prX1FK
+PTs83f/d58bAy9fFw8vJzMbL52/TvcPP79/fVT5APjI+RMpD99Xj69vX1+/X/0g1pclN9zy6vsrP30o6REI3NldX3/fVwr2/wr7Evc3Gztnd1dfX71HbUV08QDo6O0BnV2dd52PX3f/fd9lvYzeoyVvn909HxtfL61FLQjxAR+/rXc3Kxs3bzs/T79vra9Xd
+xs3TXWtPa0E/PTxAT0vMa+PR38/T483f5+trPKnJZ+84rb7EzedjOEA+PD9Lb//rzsvEwdnRys7d3e/Z591nU0tVTUtNPTo8PU1ESclra9HXzdnbytPX1903pbPG7+frT+vE529LO0FEQD/RVevV18/Ta9/v2fdnb9f3wM7d205PTUlCOz1JSE9R0WPN38rI
+ydHJy83T20y9qb1PPKvDxMdjTjg9SzxGQT7M3/fVycrn/2vj91tb1/dnXUxKU0ZLR0k/RE5VV1frwuvTvs3Iy8bHy9vfVTCnvVXb991Vx/dNR0VFPkdESmvf3d/b19tV/+t3XXfZ39vj01VdTEhJR0pCUVtbd+vTwdfIyMPLysjI3d//PLyx0UKut3fH/0M4
+PT87R09AZ87n09fNze9r2d9vb9fn6+9MU0tdS0NFXUVja9tr593dycXIzc7Gys5j42dNOKu9Y87XTUxvQkw/PTxHTkxOZ8hnb9fv22t36+vv53fC0etfX2dHRUR3Rltv99ff1cbIzcHEz8jO01v3b0wxpq/TRKu8Y19MRTM5NjxNSUVdb9vT2dXL9+9v5+Pv
+1ePdUVlfUVdIS1FnZ+PRyu/Nxtm9y8vZ1cvvWUtfRlkwq7vr2WdnPkhMPj49P05vTV3rwm/f1d3MZ1vX12tvvc/V/3dfS0hLR2P3/8zFzczHv73Mxb/by+drTU1ZU1M0qOdTsNtMOj42NTw4SHdTTmPM4+fbz8fRUdXVwGvI1edfV01LTU9AWdnf1cjJyt3X
+wNfL49/bZ1NFUU9dVzettcZTTT9EO1U6R0VXa+9nWcnf3d/d0c5Td8vnz8jF19dnWUxET0dG583rzcnIx928zdvF4+djVVdJTV1fT0GlXa/OZ1s2QS4+S09n3WNXyc7VzdvLzf/R08TNzs/T909LQ0hTREj3zdHZx8jT18fKb+dvUVFNSk1RWV1XNLq0Y0c+
+UzNJSk3nW+Pf4/fT1ff32eP/x3f328TOzNXjXUhIQUxKT//Ix93EwsfOzL7Z791OTFNTSU9ZX19bNLSub3dNPT08Rmtjb9lr18vX0dXX48PLzc/JztHb2VdbPUZDUUdV48/NzMXNz8drzs9RT1NPWUpMXVlbY1kvqMpHOV1FOVnv39nv2f/F62/M5/fdys3O
+X8nM0ddvd1tFPz9VQXdf28vH1cfMv8zfyV3vY1tRX1tXY11n/z/VvqpVd01DRm9dd3fP39vL/9nfx+/G1cXX1c/j21lVQj45P05Ia+fb2cHb28Pd413vX0xKW2t3X/93Y/dfTy6s70JRTXdRz9vf7+vXd91nV+dj69PPzOPP29vjU1FKSjlNRVvd4+vNwdHd
+vsPd32fOS2PvY+vf9+9dZ19RKc+qyetb00dV2W/r2dPbXXdX3ffXzcnK1et3/29OREM6OkpM/93d3c3KxOPNz29rb+tXVf/V5+Pb/2tjU0c4R77LR+vVTu/MY+/TV+NvV/9fZ+Pn1e+/3XdnWVVDR0JJTVXn687X183MxsXV39lja9t339Xb1d3/X09RRzon
+p7DfxtnrRedfzuffUVffUV3L1c/d1dfjU1VKSz88PEhPzdfrz87b57zP3dvf91lfz3fdzs3f5/dZTD89OSiot2vT48pK3+/rd0hT409V/9vb99PHyN9XTE5LQUFRS+vd0d/O0//CzdHn79Pj9//J09vMytPrb05DPz47J6yfvMnTzT9d21lfSV9XWUvZ28rd
+1dnr2U9JRE1CPlNNz8XI49fTym/JyV3b92f398rT983Na/dXSEA4OjQzU63EZ9vVRdPXVVFLTVtZW3fV2XfGz8/fUURBT0ZJV9fL2crb28bX69nR3//j1c7nwsvXz9HrWU1DPjs9PytCn7Bvy9FLWWdNXUtDTF1n493Oz+Pn43dVP0NRSkxPxMfFzePLy+dn
+48FbWdvZ393Hxeff319MRDs8NTdNSjWlt0/Zx3dR9+dPSUVb39//583H19vdWW8+QU1jWdvGx9PdxNHjZ0rv62/nyszFzci819tv/0ZBPD05QkJHMZ+svMbMXTrrXUxPQUnd09/XxM3fZ2Nr/0dEWW9b1dHI0czXZ1tfXVn372vjys3V18rv/19TPj4+Oj83
+5/dCsbCyWc/bSGv3R0hIa9fV78/HyNlR/1VTUUNVX83X08vPz89bTuNJTmfO2cO/v9XPwuNrS08+Pj1DPz9TQUvTn63PxWf/R0dTS01b29HO58rJ31dbXXf/TlNfw2P/57/b/11rWVtNd9HK07zGz9XV605OQz0/PkVBOP9f9zWxrcz3ye88TVM/b3fd/8LV
+1ePnU0xRd+drSNPR12vD0d9f60xTU0z/yrzDvMDG1c/rXz9BRj5DR0FHTEY3rsylvc/PSTxBTEz3V93Rzdfd7/ddTl/34+dLzu/nX9/R319fU0lXXefJvsK+v8fZ91VTP0pDRURKTEdPa8tvNaa42etXSTpKX2vdb9XKy/9Zd2NPUffr71HX3+vI3+fn3VtZ
+Tldj28O+u8TAxuvrXU5BQ0VGREVXTEtMO6tPp7K5R09EOE7Va1fd49vX505b405P79P/1VN32Wtva9vrVU5bX93Lwr63yMTLX1lCTUdGR0tGTv9BV+Nnaz+9qb1HW0pFVd/348zV1W9fR0prUVXv32/dY+/f0Wdn299Zb11vzsrFvra/zNNrWU5MRUNFTEJZ
+TE7nP3ey11+juj9RSU1fzs9fzW93V+9GSXdbV+vXb9Nb73dXTv/R919n5+/HwcO9vb7rb1dOQ0ZTRkhLY09K11vvZ2tnP8W3/0Rr91fj0dPO22NVb0dFZ2f3Wdd33cxXa29r7/fd93fn2dPJxcG/v1vrb1lMR09KSltXUVPnSzyvx85M67JnRGvZd9vjZ19O
+XVVrUUhn9//Vb3fN51dZTmfX293b98zMyrzEzMrKRV1RS0ZJTFtOTldJ79nb/1tnYzr/s8VLz87Td83fZ3dMTFNPSHdv11VX487bTFnv/+vZz9Hb08fVxs3N2c9OTm9dSEtTWVFOU+NvT0SxxdNrRlWzx1XHxtVnd1VPTlFRVU9RY9NvY0nba2dGQ2t349HR
+y83Hx8TF0e/390lFVVlKVVPZSVdTd8jX2eNMSk4xsrLHd8S/9/dj3XdNR1NVTFfr1WdPVW/nX0ZO59/308/LzdPIz9fv72NnW0x3Z01Zb19jT9vj/1HBu/9vRkqxvt/NwsVrb1VfTUdLWUxNb9lnZ0ldS11XRUtn19fOwMfIysfK2XdnTFlPS11VX1fjb19N
+z+//ynfvQk06t7XH1cjFzuNjY3dVP0hTTUfdd19ZSVNOW1dNV+PX0c/HxsnPzszdV2tVWWNv/2tZa/dda2PV2Wf3d93/TMG4vt/Mw8/d911bSUtESE1RXd9nVVf3TGtVUU9X39HOyr/EyMrPy+NPWUtRVWNbXW9f3fdb1//3Z3dTSVVdu8Xb48THzd3jb2NK
+T0lITU53a09bT19Ib19bY2vPz8zIw8XJ1dPnY0xPTVf/42Nvb2Pn72/P62d3619X37q+xt3Kxs3RY+ddVUFLUUJKU3ddUU9T611HY11d39PTysO/xM3J4/dnU0pMW2v3XVXd9/fd79/fVWNdTVvVwc/j783Ly93fb2dPRFNVSUxO91NNSUx3701ZV3fO0cvG
+xcbDy8n/XVlPSVNn/+vvW+fj/+/PY+tOWVNZXcu9xtnPycbT391da01DSltMTVVnU1VEY2dnW1Vb49HTzcvDwsjMz3dbVVdJSf9jd1tj39vfd85jX1lOTVdbyL/G49PTydPV3/9rT0xRU1dKTlFPUUbna2f/WWvT187Kzc3Mzc/da0tLTklR92drb+fn3eff
+129ZWU1Ra+PKycrR0czN3d3va1lXUVlRXVVfT1lJU2vfa3dfd9Pr0c7OzMbJ3+9rSktRS1Ffd19r3+vZ98vj71tOS0dr09nMy8vN59nT6/d3TGNVV01Ra0dNU0vvY93r6+Pd1dHPx8zJzczja19JR0tTW1dfWd3vd+9322tvU0lGV2fR083JwMjRztnn/29f
+d2NVTVt3R0hPW2933efr59Pf2dfRysjK1eNbTkdHTF1ZVV/nd9/v98nd/29OREZja9fRzcTFzevT09/v/2dnX1tTa1dLSUv/72ff4/f/2Xfny8vLztvd/1VJTUZMW11ZXefn5+/P591bX09MTXfn48nNw8TLztfj929va19dTG9VU0lOb2P3d+/j/+/T5+vP
+y8/M1+t3UUVMTU5rV1t37+vv09/d42NVTklOWffdzsbExc3b59vj7+v/Y1XnW09MU3dOY+Pf4/f/293v483M0dHva11OR0pOTlt3XV3369Pd/3drX1VTT2tn3dfIw8LCx9Pn9+tnb2td91NVTU3/W09r5993d//Z49vbzs3O0eN3WU1LSlVO7+dVY2fX3Xd3
+d/9bTE1Na2Pfz8vAwcnP2+fn5+//b3dbW1lOX2dZU2vd4+tn59ff39XPzs/fb1tPS0xLX1lnZ1tdb93ja/9rY09VTFvr39XJxb/AxMzX43drb2tjY1NPUWtOV19ja+/jd1v329Pf29XN2ffjVU5NTE9vX2NfX+tr5/drd3djXVFL7+/d18jDvsHO0dnnZ3dr
+a1lVS1d3XVdf/2dn9+NnZ93n2dfR0c/jb2dOTE1PU/93W13jb2f3d+9jX1dRV1Hf1dPMx8LBv87X0+NVV29fV1NOXWdXW2v/Z2Nv611d3dXZ29XP2e9vY05NS1Fr9+tf9+9nY/93b29jXVtdX9vT0c7IxsbG3efjZ1NVY11VUVNnZ193b+9rY2v3/2/b09vX
+ztff519TUU1PV2d3711db2djY3fnXV1XX+930cvLzMnIysrj7913TU5nWVdPY2drb//rb2dbY+vvb8/V59nX5+t3W1lRTlNr72/jZ2dva2Nrd/dvY2937+vbztHP087R2eP/a11ZV2NXUVVjb/937/f/Z19n7+Pn19nb09vr62tXXV1TX3fv9+93XWf/Y2dv
+d11fZ/fn39/Tztvb0+vj3/fvb19XW2Nba3dv6+/n92dZY2v/69nX29vn5+t3X1tbWVdr7/fr6/93Y3dnb2drd2/37+/Z49fX2evbd+/va2Njb19ZX11na/fn69/rd19n///j29/b2+Pj63ddWV1jb2d36+d3/2djb2drXWdna/d33d/d2d3Z6/dnb+/3d3dn
+W1lf/3f/7+vj3+trX2vvb93b3d/v7+/vZ2tdXWN3a+/n7+vrb2NrZ2dfZ29393fv29vf3df/b/93b/9vZ29nWV13b3fn793j6/9fY3dr69vj3+Pn5/drX2NjZ/9v9+Pv7+9va2tvZ29ra///7+/f3ePd3/dfX2tv5+dv92tjZ+v/9//32/f//19nb3fr29/n
+6/f//2tjb11ja//36+vj7+9rb19nb2Nvd+/r/93f39/n71//b2v3/2v//29nd///7+vf3+frY11vd/ff3+Pr5+//b2NjY2N362/v/+/3d29rY2dra2dv7+/r49vZ4+P3Y11nb/fr6/fvb2//9+v/6+Pn/+9jZ3dv7+vn6/f39/93b2tnX2P/9//r6/fr92Nd
+Z19rY2/n7+Pr3dnj5+93a2tfZ/93b+v3a29v6+v37+Pr929n/2f/9+fj5+/37/9nY2dja//39//r9/93Z2Nna3drb/f34+fj1+Pv92dfX2f/5+P37/d3a2vv7///9/d3a/9va/f/6+Pv9////29nZ19na+vn9+fn5/9rZ2Nja29369/n59/b5+/3/2djX2v/
+92/r529rb+/r//fv5/93b2dvb2/n3+f//+//b2tra2//6///6+v39/9vY2d3d2937+fr59vr//93XV1n/+Pj5+vj929n7+P/d3fvd2//d3dvb+vn93d393dvZ2dnXWv35+vn5+fvd2tdZ293d+/f6+/j2+fvd/drW2Nv/+/v5+t3b2vv6+v/9+Pr929rb2P/
+9+vn6//vd2tnX2tjd29v9///9/93a2Nv6+v//9/f7+PZ6+93/19Za3fn3+fr6+93Y+/r/2Nv/3f/d3dra+//4+fr/+//a2tdZ19n9+vj//fnb2trd+/j3evv6/f/9+Prd19vZ1ldY2//5+vv92d339nb9/fv/29na2dv73fr5+93/3d3b2vvb3d36+v/9/d3
+d29rd/fv///r93f34+tvZ2NjWV9v9+/v7/f/a+/f2+dv//////fvd//v//f392tn/+/j3dPf7+v//2tv/2djX1tdX/939+vr5+fn429n3ef/Y11nZ//r/29nb2f/6+vd3d/j6/dv//93/2t3X11ra2trd/9n///3/293/2N3Z2tvd/935+fn49/j/2/f53dr
+b29n//drb2tjTlfAub7M2fdnY11dX1lTV2NfVWdjX2f//3fn5+/3b3d3Xdnd29/b39/Z293j729dU1VbU1vOyP/Pz99v5+/n/29ZU1NRTVFRUVNZW1NXX3f/79/r1dPP08/IzM3V09nj/+fra11PT0pMR0dNxNNJd7jG3XddT1lrXV/va11v6+fr19vf2dfZ
+zt/n7/9vb3f349fnd+9jY2dbW05MSkJDRURBTFXBwNfn29/Z02/Ny8z37+/v39PJzM/P2d/d79vv73dnXWtRb19ZX1tvY29VTF1VW0pFSURLST88TaKpy9XvR0FXU/fZd1tXU09T99PZzM3Tys7Z08/3/2PvY1fR3c/OzdfZb+dra1dRT0dJRUQ/Qjs/rrfZ
+729jRlt3d9Vj5+/3d2PK1crRy+vj611fXevTW1Nbd2Nf72f/5+frTvf/709OTE1fVUo1vramwW9NRjRPXUtZR0tPRkRO4+vdyca/zL/CwMvPz8PPx7zO1dXZ2efrd2dKS0tGS0k/Q09ORT5Aq85ZX1UuO0M7PElIQltLa1fJ28q9vcW9vLrDu7rDtbrVwMXN
+ycXLzuNO429RTTs+Ozw4LrfbNsauXT7XSy7fXz4/Qzk1PTs/TldRz9X/08zf28/nur+1ur+8trnAvL7AwNt3Y+dRSldITltDRTkuo9NJR0kzRcx3UeNVRHdjVU/RTllTZ2NRQV33Tedn10vv18jLzb3CzLe7vc/d3WtZOtm011c4K7Hrb0FIPkzfzVlva2d3
+69nRz8n328hrRUVJTVNdT8xn7+e+yszB0cnXwcTd9853//dfX0NBPiyqzuNRP0E9wc1b/9VRT1lTWd/T28nL3/9NREc+QDs/SUdfX8n/yc3Gxr+7vr3JxsW+z8b/VUYvq61vO0lnQ1+/S1fnSmvvZ2Nnz+//d1lBPDs+TjpEXVXvysjVs7q7u760vL7Oxc1d
+Y1VOST9EN++vyVFGV0rr6+9R2Vljd1FJU+/rS0hIP0A9OTw9PkNOTHfDwsmyuLS1t7q+wcS/ztdMS0hCPiuurNtBRkxFxM/bwcf/09dnS+/v91tTTD0/QTk5Pjk+TVtP1d3NvLmwr7C0s7rCzd3rU0hPSzUuU79ZSUZFSsfXy9XOxnfbWWfd2+dj918/Qj46
+Nz06Pz9ATltv18a6squtr7G6xsvv/0xNRj84R1lKSUQ/SUvZZ9vN1d3DysvZ28rP993ZWUxXS0RNSUVHPkdXTv/T18i7tbGzur3Cz2dbRDw3OkI/PjxBQUNMzu/MwcjJyci7x7+8xtPZd2NIQz8+QUFKSkhMSuN3z8zPv7q+sru7wdPvUU1XRDo1NTQ1Oj1F
+Te/V2cbAwcC8u764vMPK0/9fV09MQEk9RkRBRk13TO/fz8a8urO3ssDDyVtBOjo1NDEzOEI/SU1X1+vLusS/urq+vbq7wMrX2VlNRERMPUVFTU5RSE/3Z1fXxb68u7/GwW9ORj83R0BBPDxBRUdNUf/n57mur7S8xczLzd/D13dOTVFJS0ZVTE9PV0R3T+9f
+b+/Lv7q9zctXU1FTPkVKTU9OV01LS1dPxNHDzs/Myc7Hys7X219TWfdZWWNfZ1dXZ11VTGtPXU132dPFyb2wuct3X0k5QkI8PD5DOzrZrq6xvMTL79nd98//TF1vW2dvY/9bWf9RP2dXZ09NZ1NZY1nKyc7NvsLZ78rPWV9ORUBB/1NbW2tv2dXVy8vLz//Z
+ztv/V1NfW1NVVUlTS0xd72Nra09IzFvL78brvqu12UxFPDo6QEo7xqy0wt/fWWf3X1VrVdld42fZ09nrd8/rd1Hdd3dZRUtAPtvbWVNTVU6vrLDO02tfRU13//9KTUhKQ0Hr4+PT19vO3eP///9jW01jUVdvV2P3y87j51nOW28909vPP8Wqz2djXUA8V0RX
+O7qstsBba0NrSmNfd0tAR0tOWetj19fNzePftru/xWvJ1/9GZz9nXUs4orxGTlU/RD9rWf9OS1lNPURvZ03dyse7zr69zM3Rd+f/d11VT1tj087nyr3PTUxIOeM/NajFQEQ8ODE0S0RTRDequdVX984+48Hnt9HFvry+u726vb7EwcfH473K58Zfbzk0Njw4
+NCinvjs9OzUtNkFBV1Nd5/9rZ9dZx8rGubi2vLm7vcS7vbq8v7/LxMPHyde/zVc/UTk2OCwrRcU2LSwqJCwrMzU4PDY7q+fLa29fW8m12bWutq+vqaytqaiop6ijp6WsoaypxtdPRP8xOSOlXS4sKh8hISEkJigjJSMiJygvLi40SUA6T//Vzr66tLeztK6v
+raqmqaSmqKeurq+ttsWvn6rBvdE6PDMvNjo1NTMtNMtKRiw7JSoqKiovMDBJRO9R49nIvLu6r66sq72xv8XNvdHZPJ+uvrq9413rb/fRb1dv3T9IUT5LNDczLzgsLjcuNzI3PUVARUZRb9+/uri7x8TOv75ryE3Mprjbwb9O3cZrxL7Gv7jAVaWlrLLvvjE8
+NDkzMjs0OTA1Nzk4PD5T3bnX70c/QjpJQDs9I6DjTM9jRl3Fy7+srrWvsLW2sba9vsO9ycVb5/drUd1bSVFjVU1BRndvY1E5Ni0oLTgqLR+2uTw8Sj1r08r/w7u/t7a70dWfp6yusLnfy9XdyltPx2/v18zL58i9srjCY087KysuKSMoH7Y5LjI2PTM+SFHD
+47rCuby1sLa8vrnF1dnVycO/xcS6zb+9v7u+xK21tsHfUTQ1Kj0qJyJOUzIsMy0uOVc9Sc7nzsTT1VO1obC2uGNV72/Nd8TZ97/TvMS9sLiqq6qvtshnPzQ+Li4kLb41Nzw1LkZKNU3nVV3/Tf9r09d3Z+tjRUTb49tZ09tny8K+u7axrKyura+7z1M9Pj0r
+Kbq16ztdSTVIW0/3zsbJyUpnb0M3qshd3TAwLjpAOT49OD3fVdnCvK6pqayos7/d4z/vKjVnt9lKZ1lKWcPnysDFyL3T99tT919LOe86MTFAPTk4QzY3R0Jda9PGs7O1tr6/XVk82TkzRbL/3W/j78vXurS2vbuwvMW9z+c1vsfC4y01Mzo5OUU8MzFTPmNb
+38TJvLvLzE0/Nj45K9271+dn18nJvbG3rrGysLG9usHJ4/drPGc1NDtLPE5GQj46XTxVWWPG09/d60ZFOTcxOSg/1chJZ+PT2cSyva2zubOvube2w+9nvdvHRTg5SEFBP1VJOE132dHPu77Gx9tNRjYzLjIpLjxfZ01X99nVvb+0s72ztbe4ub7Dz9VORV1H
+PkZda2tRX0hnzv/v68XAycPJ/1FAOjcuLzQ7Pkc/SENdX3e/vri+wrvAw73D1dnva1PLSVNMW29nT3dfXevVzdO+ur22u8TN40c9MTcsOTs/S1NjV1njxcS/wcG/xsjF0f9fV0g9VTdAQERjV2vZ/2vEycfMxbq4uLjL2+9HQTQ3P05ZVedd72fvzcXFwsXO
+ztHL2XdbUUw6ODM1ODo/R11XUVnTzM3PycG6uLa2vczG40NEO0o+V1dj2WfZ28LHwLW4xcvLx85vTUtHOTMyOzE6PD9LV01VyevPycS/vbu5uL7P4008PUBESu9f1c/Jz+e7xry/u7zJy+tva04/RD00MzA0NjxAP1VNV8n3zcnOy8W7v72+ydXbTUFIQl/f
+a+/VzsbZuce7uri4v8rF719VPj48NjQxOzc6R0VKSEzP593Mz8jFxsHAwMnXb0U7RD9FY1njy8nHxL26ura1trvIxfdrV0lBQzg2NTU5N0VBSU1ja9frzMrGysjJzc/nb09EPkRARedb9+vZ2cDHvsC3uba7wsXR/29OSkk8PTs5PkFLUW9r593N28rGyNHP
+2+ff619PQjs+Qz9XY1nj08rMyMLEv7y6usfMd1NVWUpGRk9DSU5TTVdr4+fPy87Ry8jO2d//93dPRUc+RkRFR2dZZ/fT19HLzcLJvcTT091rW11TXVlVR05na2Pr18/Zz8rLysfAxM3TY2v/U0ZJQz5EQkNGS1VbX+ff49PKzMfMzv9nY0tNSk5PSEtbb2/j
+3dHGx83Av8LGw8PIz8/f3/dLU0hDT1VTT11XY05r/3fn0dfb529OR0pJSUNMTUtdXVFn5//b2cjMy8HCxcXBx8zM0e/V/2dvVV9bXVtjW11j7+vj993vY993Y05PUU9KSExLS0tPZ3f349Xj583JzczMydHV3eP/1+P3/19vU19v32Nb29/n39PV49/Xd/9v
+T05RT0lOXVNXTlVj99/d3/dv4+f/09Xb2dfn999rXf9n/+vv7+frb93r39nd69/343djV1dZX19ZUWdjd29j39//29vf49vf7/ff62dr919rY1lfU0xd72Pn0ePn3dHT5+fV5+Pfb2NnXVdvX1lvd//v/9vd69vn/2/r7/9r//9dV2NbY2dbWVlOZ2Nb/+/j
+d93Z2d3Z2d/f329j///r/+d339XV5+vd19nVz/f///9vV/dvd1VOTExTTU5bU1lbXWNvd2/f39fX2+Pn7/fvb+f/6+/v79XRzN3/2dn/1czX3evnd2f/a3dTV1NZW1dLUU5PX2tbb2djb2//9+fv0ePv92dvd/9v92tv2dfn3evX3dPN0c/V3ffvb/fn52td
+V1tnTlFOVVlXU2tf/+vv6/f3d///Z/f/9+/j39/r7+fT6+vr2dXj693ba11j4/fj1+NrX2NvWVNRUV9fZ1nvZ2djb19bY293b29vb/fn293r729349//6+fX493O3dv//99359nj73drX19dUVdZ7+v/a29vX/9fd//n3+t3XU1ZX2trd/f/d//n4//v5+Pv
+4+fX7+//59/X3+f/62f/d2NnX2NrWVnj2+ff3ff3b3fvZ2NbZ19v/2/////35+9vW3f3b+/r7/d3b+v33+vnb2tjb2tnb+fv6/dv/2/v39/r79nj3eNvY2/3//93a2tna29r93fv62ff7293a19jW/f/429fY2NXa13/Y2vf4+/f593X3dHZ3dvX72Nra2f/
+6+fvXWt3/3f/7/f/7/9vb3d39+vj/+93b19bZ05XY1Nj7+Pj2dfn5//j3+fv2d3d9/fv62t3b29na/9rX2dvd//n5+/3W3frZ3fr3W9dWV1bX+t369v/d+Pn59nX6+/va93n7/9v6/9jZ/9jY13r3////2tn/+9v93dn72t34+N3/2tdZ11fZ/93/+vZ4+/f
+29vj3ef392NrXWPn/+Pr92tvZ+9nY2f/b+fj9+fvb///d29nd2dfXVtbV/f379vX39/3193n6+vna2tvY1tr7/f//+9va11rb11v73fn3fff92N3d//v5993Z2NjX11vZ//n5+vf6+Pf7+fv9+vv629fY//v9+t3/2dn7+drW2dnX+//5+9r///34/9vb29j
+Z29bWWf/3dvd1dfd193j6+/392djVV9rd+/n7/fva193b2f/b2d3b+/vb3f339vr9+9vZ19nZ2dra//3b/fr5+fn9+fn//fv411j9+fv63f3/29n6+9rb/9r/2Nj92dna+/d5+drd29va3f/d+/j2+//92fv6//j3+Pr7+9fVV93Z//3/2/39+/rY2tna/f3
+9+Pv92//9+v/d/fvd2v/93fv39/r9///9+N3d2939/d3b19d//fr9/f/b11vb2Nr6/9v/+vn4/d39/93d29ja2/v4+/r593n69vd3+/f/2dnb2/va/d3X293b/9jd2tb/+9nY//v/+v37+/3d+v37+93b/9rY2djd+vd3d/r4+fd4+939/df/29vZ2dvd+v/
+a2/3d2frd19ja2f/b2/v5+/v3+Pfd29vW1ddb//n69nb4+Pn3ePr7+/3d3f/d2tnX/93/2Nr/3ddd+NnY2v/b3fv59//9+9j/3dvb2Njb29n6+//d+vf493r3+/d493j93d3Y1tr/2/3a+/rb+fvd2N3b/93/2/v7293a2vra29nZ19fY/fr6/fv6/fv4+Pd
+3+fr93d363dv/+vv929nb2NfZ3d3b2fv6/d33dvv63f3/2Nnb2Nj///39+vv7/fvd3f37/fn4+vvd/9rX3f3a2d3b2d3Y2/3d/9v5+fnd+f/d2v/a3d36+trb29rZ//n7+vn7+fv7+fv5+/r93f/63f//+/37+93/19jd2v3d2dnZ2t373f/72tva2ddXV9d
+Z3fr493j6+/v///v9/d369/v9/f39+v3/+f/6+t3d/f///dvd+9vb/f/93d393dfXWdfXWNnb//r//d3Z2dr63f3393v7/93Z2t3/+vf5+Pn4+/r6+/r5+Pj///rd3f/7+/rb2drX1lr7/fv5+//d2NfY3dfWV9nY2dvZ2Nv9/fn7/fr6+d39+fj2+Pd2efn
+3+fj3+fv7/93b29nY2dfa3f3/293Y2Nvb2d3d3dvb11nZ29rb/f/a2vv79/f69PX3dvrZ19jY2dra3dnY2t3a2/v993Z1dvn5+t37+Pnd/9vZ2/f3edra2dna19nXVdXXV9r73fj6293b+fv7+/r7/9vd11j/+/f3d/f293jd+v/3+vn93dva/d3Z2v3d29r
+b2dja2dnZ2t3d99r5/fv/+fV4+vd6/ddTe9fWV1v/29dY29ra+vNzMrLzNHX193n699ja09RUU9VWVlTVW/nY19fV11ZV29ra//3///n49vb0czMz8rOy8/L1dfd2df/9+9dZ1dbWVdTTFNMTU1NS0RDU1VKR0xVY1v319nOzdXNztPKysrIyMbLytnR0dXZ
+6//3Uc+1uMVnV0k7Pj0+PD07P0lIY1v/6+vV2dPZ9+djY0xrTk9Zb2tjyM/N1czLzs/RyMnPvsjJ0dfvX1tvZ0dTQUfRb1VXVWtXd11351/3291r5+9nV09NR0xHRj1HS1FHT+tf/9XLxr6/vsG8vL27u7q5wsPb4//va1NGMdGnr+tLOzguNTQ6Njk4MzQ8
+TEz/293X3XdrW19VT09jd1v3/2dn08LCv766ube2tbexsK+vr6+vuLi8xrvIsK/JQTYqJCsqKSoqJigsKzEzNENIW1dbW2NTRtfDzevVU1FCQ0ZBSf9r1ee8vbu0s66wra+vsrezt66345+fn6q50TM6MjM0OSsuLysrLjc5MjpDPTs3LjQ3NUTZb1lr40pj
+Ss3Az8S8wsnA1b68vLu4vce7zuf/W2tJoayuvM3fREtRTUZHRTxEOT09Pz5RU0xGST07MzY5PeNL/+dOQ0BDQU7V59O1s7a0trCxr7WxurzH1dvRPt2fn7a690EuNS4vQjk1Pjo4P01ISv/fzsPfX1tTY0lMyu//70VDNzBDT0FGRtn/59XCvsO9u7y9w9Xf
+72tDrKey173LT0lMPkNORkRNQzpFRkJH79fRz93vX1/v1dnjvM7JY1k9OjtHSkVMxMHP2cLOxcK8yufI2VdV6z5frZ+8u81KPjU3QUFDTklZQkZR41fXzMzKw83/19v351fnXWdZSDk7Qk5RSdnv503rwNHJwLzEyc3ZXUxrNqKpw9/bxz5n/0zf72/N70xE
+b1U/X9NV72NRT0hLU1vvw9HvTlc+Ojo/T0jvb83V29PCvcXHwsbb3fdrWVMyuMKlt7jEPWc+TmdL18/dWWP/a1X/69PVZ2dVT0tRT07/UVFIPzwsOTxGQVnj2d//ucXIvru6vsnT1+fP5+9Do7tO50RBT1NGZ8bd2/dbU0xvTm/j719fSj9CX0FIUetVWUg7
+RzUvOz9N19PGy8G1trWus7W8v8TN2czZ0TWfR6my328/TDHO/0NXTEQ/RUo/Q+9jU1FHS09OSVFLa0lVQUA2Pjo/5+PKycjVzLu6xbW5vsPT0dPj/8/ZS622v0lVSz1G22vXT29jRz4+QU1Oa1NbT0VIUWtX/8a+53dNSD4+M01ZStPFyse/uLS/s6+7y8HR
+x9/b08xnTq1HtcFTQlNOL09JPEc/PDlBSl1NV/9ZT1//b+Ndus/bSExCTkVBVW/Jx7/Cu8G/v7i8vr7T71vnd11R61E3pNs6PTc/P1Vf319jWUdAOT4+XVNVX01nY/fV62fAxsb3a00/SUhL18PJu7q4t7a4sb23ws3r3+Nrb0//SjynZya+uUdCRks2Pzcw
+NTk2OldZb3fR2c7TvsC9wrO4zv9dTVFIQlNjycTGwsK+vsa0xcnZ32Pfa2dOVUo9LaldLjs4NTZET1tPPURBOj1BV0/v13fX09HFxcm7tbrDzU7vQUZGTdHfy9O4ubu5v7LBv8zLd///Z1VHSi05uzwuOia6W1/nSz43Qj43PVlT19Pb18XAwr21va2vuczf
+a2s+TD1C/1nV58rGzOfCvOP3b+9j/2PjWUZXM7/LQzdANjA9W053S0BbTztIX1/d2dHbwcC/vcO/s72+zcpTVUZMTUdbym/Pzb3K3b+91W/f4+t3b19OSDgvrkozODYuMTiqu75fX0pIQ0XX1+PdyufR277JwcS3v8fVVUtEPztERE13/8/OwM3nvsXPd+/d
+62tnX05FRTWyZzk2OzZKTmfXa01J519FX9XP68vbyb7Lvci+trvHztFXRDxJRD9FXev/18jZa+fF49f36/d369VZW2dNMqtnQjg2QkBJNKiqzuvK40hX0efZze/FwN3N3ce4ytvIZ0o8Nk04Oz5XW1nOzuf31cXZ0+Pj/+ff93dVX0dntVM4Nz9BU/fnzMxb
+Z93d52/Kysjfz8nDzczMysnZ0edXQTw4PTk9P05OY9PX3XfZxM/V999v39vj1d/MU7W4z0VDRj3v9//XOqu/u8/fb1Xr9+9v43dra29fzt3GT0xEQTs8R0FFTmtV19Hb79PEz8rZxN3X3+tv78td97nRTkI6Qm/v5+PK/1XR583v2+fn3993//9n52/ZV9dV
+QkNBOj4+REdM5+PMxtPTz8HRy8/N69PZ087Tv9FXV8nLY01HTXfr218xyrTD09/dUV1XX+NdV1FT71VV61dESU5ISENHTffdX+fPa///0cXT49fbydfNx8TC02Pr591dX1FP29XL03f32Wv/U/9na1//a1tXT07vX1dXT1NHS0E+P0lOY2//38nZ18jM09Pf
+zMvMzM7Kx83N0dvj0ePrXXdd///nSUG5u87b/1lMXV9ZUVtRT2tTSk1GQkFGSUFHUU/n/+/n2d/bzc7N0dfKy8jOxsTHx8fN02PV92dvZ19r4/djXdNdZ01XTk5PV0pOT1NTX1tXd0pMTklLTFFd/9/Zzc3VzMrKzc3b/+PNx8/My8vN09vbd3fvZ1tfXVFd
+XVNXV11jX2/3W2tvd/djXWt372ddWU9fT01ZT1fn79fNzNvbzdXn09Xn793J38/R0c/Zb29bWVtXW2tnY2trY1d37/9dZ/9bV2drXV1vWWdnXWdbX3dfW3dr9+fZz8/J483Rz9XV6+d358zd2+fn3ev/W1lXW2dfa2tv/1tdU1dfd//n7+tfa29v92d372tj
+Y1NVW1lbXW9j9/fb3c/Xz9XZzdXV2c7VxNXT2+Pfb/9XXV9bW1dXVV1ZV19ZV2dvb2trd29v/3f36/fra29TTmN3Z1dZ6+fn1dvd1+PX39vf6+Pf49/b3d/3d+P/X01bVVv34+fnd2tnd2NXa2Njd2ddW1td9+/n9+d37+9r62/na+vj9+v37+vb4+Pn2/f3
+/3fr29Xn3W93/2dZV1tZW133b3frV1trX1Vrb3f/d+/ja+fX2dfjz9/r//936+P3/2/n92fv//fn92/3Y11nb2/v5+9ra2v3Y1lZT1tVZ+fr699v7+v3d+vf693f4+d3693b3+vn52tfb1n3Z3fr/+dva3dr9+Pr4/9vY3dvWWNr7+t3d2dZUVljd11n5+fr
+2+fv43f/79fb4+vd4+vn2dnr1+vja19nZ/dn5/93//fr92NbW19jX3dbb2Nr7+vrd2tn73dZY2tnX+/d5+vj7+ff/3ff7+fv7+vvd+Pd2d/j4/dvY//vd1//72v3b2N3X1tbb2d3b2/3Y2dr/9vvY+vra1dZb11n7///69/3//9vb9nZ49/j3+Pf29Xf3d/d
+5+f/a3dj52//a2NjY2tnW1FVVVNZW19dXf/v73ddb/fr5//j7+/f19PX29nX1ePv5+/v6+Prb///3ef/d29va2tna3frb2//Z2dvY1tZV1dbU1dfX29fZ/fv91/j59//793d7+Pf39vVztnj693b9+/r//f/6+Pb6/9vd19bW19ZY2tvY29fY19jX2NZV2Nj
+X///6/fj2dnrXe/r7+/r29/v1dfb09vP2+Pj3+Pvb2tj//9vd/9nZ1tbXWdfa2NrY2dnd3d3d3dva2NdZ1tbX2d329/r6/9vd+Pr3+fn3dfX2evb39Xbb2/rd2trY2Nfb/fn3+frd2tvZ11na2trd29jW1tbZ2NjX1lXX2dvd/ff59XO4+Pr4+Pr3+Pj3d/d
+3evf3dtv//dv/2dvW2Nv6293729nY2NnY293/3dv//9va/9rd2dfa19rZ2fvd+fd3eNv7/d34+vn2+/d3d/b59nb5/9rb19nXWtRV19n/+/jd2tvd11ZY2vr5+vn7/d373f/a2tvZ2/39/f36+fX19vn7/fn7//r6+vf4+Pv/3djX19na29jY11bb2/r7+/3
+b//vXW//d//37//372/3d+93a2tvd//j9+f33dnr4+v3/2dvb3fv5+v35+fvd29jY2NrZ2tna19v/3d35+Nra/drZ1///+P39+P36+fv92djb/93//f3//fn3+Pna2//92dfa2P/6+fv5+/vZ19va3f/929jZ//v5+PZ3e/373d3a/fr6+fv9/d3d2dra2tn
+d29va3d39+Pv4+t3a/fvY2P/7+vr7+vr//93Y2Nja+t3d/93b/d39//n5+/r/2Nvd+fn3+Pf49/n63djY2tvZ3d3a1/3d//3d2trb3djXV9jZ/fn6+vjd/93d2//7+Pv7/f36+vn5+Pnb/93a2v39+/j4+Pv6//v/29v7+//9/9fZ3dr5+9vb2drX11dY/93
+a29rd/93Z2dfX/f/7+/v6+/v3ePj3d/f7+fv7+Pd2+Pn3+Pn7+v/Y11ZX2Nrb2drd293Z2NfX293b2dnZ2t3/3f/9/d3b19j7+vr4+Pn7+fd39/r4+P/7/f37+/36+fn7+v39/9na2dnb29rZ2v39/d3b29nb19jX113//93b+/r//93a3f34+vr4+vj493j
+4/f/d2N3/+/j3+Pf7+937/9392dfY1tfY19rb///d2dnb+/37/dva//v5+fr5+f393dva3d3d2/rd3fr7//r62tra/93d//n7/936+fr5+fn/2dnY2/v6+/v5+vv93d3/29va2NnZ2//b3f/b/9vXWtr7+/r5+f/7+vv7+Pnb29vY2f379/r7+/v9/fn6+fn
+93dra29va3f/6+v/a2dv//f/a2tvd///a/93//9rZ2Nvb+//92//6+/v6+93b/fr7+fv7+v/7/fv5+f3929vZ2tn/+v/d//f93drd3dr/3dnZ2f/729nd3dra11fd/f/7+vf6//f5+vv3e/3d+9va/f36+/////v//fv73drd2d3/2tvd+fv7/f//2/v72dv
+b+/f929ra2drZ293b29v//dvb+/n7/fv/29vd/f34+vr63fv6+fr6+/v/3d3b3f39+//7/f/d/9rY3f3a29v7+frZ2/3a2NnZ2f/7+v/929vd3f39/f/b2trZ2/37+vv4+Pr7+fr4+vr7+/n///39/f/7/dv72tnb3djX2f/5/d3/29nXW93b29v/293/3dv
+7+fv6+9vb/d37+vv6/fv9//39/f39+v/d/9v////5+f3/3f3d2t3d3dn//f/b2trb29ra2tn7+vv7/93/+/n5/fv7/f/d2d37+//6+/n92/3////9/93//93/3f////3//93/2tra2f37+vn6//vd2tnb/fr6+fn/293///v/+/37/9rb/9vb///b29vb///
+d+/vd3dvb3f/9/fr63dr/3f/9+/rd+fn7///d3dv////7/fv9/f/d293//f/d/f/b2d3d2t36///////d2936/f/9////+v3/+v3d////29vb3f/9+/v9+/3d293b2/n6+/393dvb/f/7+/36/93a2tva//v9/dv9/d3d/f//293b29v/2dr9+//b3fr6+/v
+7/fr7+f3d293d/93/+Pvd293b293d//v//d3/29v7+v3//fv63fv/3d3d3d3a29vb/f/b2/v92t39+93d3f/9/f35/fr5+/3d2/35+v3/+/////39/93/3f/a2dra29vb29vZ2939/f39/9vd///b29v7+v3d3fv5+vv9+/3/+vn/3f/9/9v7+fv73f39+//
+b/93Z29vb2dj/3dv/2Nna/93a/////f/b2t3/+/j3+/v5+Pv7+Pr7//r92937+vv/+/v/2tvd29r///v/29vd3djb///b3d3d2//d2tnb2937/f3b//3d////+vr7/fn92//6+vn5+/3//fn6+936+93b2tvb2v/b/d3d3drb2dvZ2drb/fv6/f3d2/3/+/3
+6+v/d/93d2//7+/37+v/a3f36/fv6+//b+vv9//r9/d3d3d3d///93d3/2drb3dvb//37/fv9/9vb/f///fr9293//d3d+/37+/v6/dv///39+vv93f36+/vb+/v/2trb2t3////a3d3b2tra2dnd//v5+fv/293d/f37+v39/fv9////+/v5+vn9293a3f/
+/+//a3f/b2//7/9vd///b/////939/dvb///Z2v////r5+vvd293//fv7+/3/+vr/+/r6+/v7/9nb/f/d/93d3dvd/9rb//3b2//d/9vb3d3d3fv/2937/drd/f3/+vn5+/39/93/+/vd///9/dv9+/39//v7/////f/9/93d2///2t3//f/d29r//fv//9v
+//f/d3f37//37+//6+vn93dvb2tv6+f///f393d37+/3//d3b/93d///d3dvb//vb3f/93d3d/f37+f/d3f//29vd//3d/9vd3fv5+v//+//b//v73d39/f/b2/3d3dvd3dv9/f39////3d3//f/9+/3///////v6/9va293a3f/7+/37+/v//fn7+/v7/93
+/+/39//373d3d29v/3dvb293d/////93a2t3/3f/d3dvd/fv7+fn73d3//dv///37/f3d293/+vv//fr7+//9/dvb3f3d3f37/f/d29nb2//////b2t3b/9ra293///37/f36+v3a2//////6+vn4+fr9///7/f///f/b3f3929vd/9rd/9rb293b293b293
+/+//d/f/93d373dvd///9+vn5+///+/37/fv7////3d3d/f3b293///3//93b2t3//939////29vb3f/9/f//3d3/3f/b2/39+/v6+//7+vv/3f/7+/v7/fv9/f3//939/93d3d3b293d3d3b29vd2///3d3b29vb3f/7+/v9/f3//f37+/39+/v7/fv7/d3
+b//39///////9/f/b//v/3d3d/93/+/3/29vb29vb3f//3dvb2tvd//3//fv6+/3///39/f37+//9+vn7/f//29v////9/fv7/93//9vb29va29v/3dvd29vb29vd//3///3//f//+/v6+vv9///9+/3/+/v9/fv6+v3/3f/d3f//29vb3d3/3d3d29ra29v
+b//37/f39/f///////f///93d3f39/fv7+/v9/f39//37+/v7+/v7/d3b3dva29vb29v/3d3b3d3d3d3d29v/2939/f///f3//fv6+/39/f39+/r6+/39/f////3/////3dv//f/d293b2tvd3d3//f3//93////d2trb//37+vr7/f39/f////r7///7+/v
+9+/v9+/3/3d3d29vb3d3/3f3///3d2trb2tvb293d//3//////f////39///7+/v7/fv/+//9+/v9/////fv7//3//9vd////3d3b29vb//3//9vb29vd/////fv7/d3d3d3//9vb//v7+/r5+//9/d3d3f39/f3///v7+/v93dvb29vd29vb29vb3f///f3
+7/9rb2//b3f3//f/////////9///9/f///fv9/f////39+/39/////////f3//9vb3d3d3f//3d3d/93//f3b29vd////+/v9/f3/2939+/3///v6+/v9/f/7/f/d3d3//f//3f///f3/////3dvb293b3f/d3dv////93dvb2939//37+vv9/f/d/fr7+//
+9+/v7+/393f/9/93d//v9/93b29vd3d3d2tvb29vb3d3//93d//39/fr/3d3//fv7+/r7+/v9/f/d/f3///36+/v5+vv/3f3/3f/d/f/d29vb29vb2//d293b29v//d3//////f/9/9vd///9+/37+vv7+/////v5+v///fv//f3/3dvb3dva29v//9vb3d3
+d///9/93b///d3d3////////93f393d3d//37+/v6+vv7+/3///393d39//37/f3929vb3d3d////29vd29vd//////3/3dvb3f/9//39+/v7/fv///39/fv7+/v6/f///93d///b3f/9/f39/93b29vb29rd///d29vd3d3d/f3/3d3//939+/39/f39/f3
+9/f3/3f//+/v7+/v9+/v93dvd3dvb3d3///393dvb29vb2//d3d3b2////fv7+vv93f/9//v7/f39+/v9+/39///9/////////f/9/93/////29vd//39/93d29vb29rb3d3//93b3d3/+/v7/f////3/+/v7+vv9////+/3//93d3f/9/f3//f3//93b2tr
+b293//fv9/f/d3dvb293////d/////fv6+/v7/f/d//39/f///93//f39/f3////d3d3d/f3/+/39///d2tv///39/f//3d3b29rb//37/f39/939/f37/f3////////9/f39/d3d//39////29v////9/f37/f/b29va2t3d//37+/3////d3f///fv////
+///37/f39///d3d3//f39/f3//////f3////d3d3d3f////39///d3d3d3f/9+/v9/f/d3d3d3f/9/f///////fv7/f39///d3d3d//3////d3f//////3d3d////////////3dvb29vd3f/9+/39/93d3d3//fv7+/39/f/9/f39/f/d29vb3d3////d3f/
+////9/f//3f/9/f/9/fv9/f3/3f/d3f/9/fv9+///3dvb3d3d///9///////9/f39/f//3f/////9/d3d/93d////3d3//f3///39/f39/f///////////f3////d29vb2///////3d3d////3f/9/93d3d3d/f37/f37+/39/f3////9/f39/f//3d3////
+///////39/f3/3d3////d///d//39////3d3d3f//3d3////9+/v9///9//39/93d3f39/f3///39/f///93//f////39/////////////////////93d3d3d///d3dvb293d3f///////f////v7+/v6+vv9/f3//93////9/f3/////3d3//93b3f////3
+////////d3d3d3d3d293d///////////9/////fv7+/v7+/39+/3//f3//////93//93d3d3d293d293d3dvd///d///d///d3f/////////////////////9/fv7+/v7/f/////9/f39/f/9/f3//93d3dvb293d////29vd/93d///////d/////f39/f3
+9/f///f/////9+/v7/f/////d3f///f//////3f///////93d/////f3/////3d3/////3d3d3f//3d3////////9/f///f39+/v9/f///93d3f/9/f/9/f/9/f/d3d3d3d3//fv9/////////93d3d3d3d3/3f/d//////3//////93//f37/f/9/f39//3
+7/f39/f3///39/////9vb3f//////3d3//9vb3d3d3f/////9/93d3d3d3d3d//////3//fv7/f///93d/f3////////9+/3/3f//3d3d3f/d/////////93d3d3d3f/d//3//////f/////9/f/d/////f3/3f///////////f39/f39/f/////////9/fv
+7/f3/3f///93d//3//93d/93d3d3d3d3d3d3d///////9+/3//////////93d/////fv9/////////////fv9/f39/////f///////93/3dvd3dvb293//93d3d3d//3//////f39/f39/f37+/v7+/v9/f//3d3d//39/f3////d//////////////3////
+/3d3d3d3d3d3d3d3////////d//////39///9+/37+/39/f393f////39///d3d3/3d3d3d3d//////////39///d3f//////////////3d3///////////39//39//39/93///39+/v9/f39/f//////////3d3////////d3f/d/////////f/////////
+/3d3///////39/f39/////////f39+/39/////////////93d3d3d3d3d3f//3d3d/93d///9/f3///////////39/f39//////39/f//3d3//f39/f39/f3/3d3//////////////93d/93d3d3d3d3/3d3//f////////////39/f39/f39/f3////d3d3
+//93/3d3d///d3f//3d3////9/f///f/////////////////9/93d3f/d//////3////////////////d///////9/////93/////3f////39///////9//3//////////f39/f//3dvd3f/////d3f////////////////3///////3///39///d//////3
+////////////////////9/f3///////////3/////////////////3f////////////////3//////////f3//f///////////f////39///////d///9/////////////////////////93d////3d3/////3d3///39/f/////////////////////////
+////9///////9/f3//93d/////////////////////////////////////////f////////39//37/f/////d3f/////////d//////////////39/f39////////////3d3d///////d3f/////////////d////3f///93d3f////////////3/3f///93
+////9/f39/////////f///////////////93d///////d3d3///////3//////////f3//////f39/f/////9/f3///39///d////////////////3d3d///////////////9///////d///////////////////////d///9+/39/f/////////////////
+//93d///////9/////93///39///////////////////////9/f///////f39/f39/f////39/////////f//////3f/9//////////3/////3d3d/////f39////////////////3d3///39/f39/////////////////////93d3d3///v//93////////
+//////////f3////////////////////////9/f3////////////////////////////////////////////////////9/f/////////9/93d3d3d3f/////////////////////////////////////9/////////f3/////////////3d3d3d3//////93
+d/////f3///39/f/////////////////////9///////////9/f39////////3f///////93d////////3d3///////3///3////9/f39/f///////////f//////////////////3d3d////////////3d3d3f/////d3d3d/////////f3////9/f///f3
+///3//////f3//////////////f3//////93d////////3d3//93d3d3d3f///////////////f39/f39/f39//////39////////////3f///93d3f/d3f/////////d3f///////93d/////////f///////f39///////9/f////////////3////////
+9/f///93d///////////////////////////////////////9/////f/////////////////////////////////////////////////////9//////////3////////////////////////////9/////////////////////f////////////////39/f3
+///////39/////////////////////////////93d/////////93///39////3f////////3//93//////f/////9///////d////////+///3d3///////////////////////3//f3////////////////9///////9//3//////////93d///////////
+/////////////////////////3f/////////////d//////////////////////3////d3f/////////d3d3d3f//////3d3///////////////39/f3//////f///f/////////9/f39//3//////93d/////////93d3d3///////////3//////f/////
+/////////////3f//////////////////////////3d3//////////93d3f////////////////////////////3//////////////////////////////////////////////f/////////9//////////3//////////////f////////////////////3
+9////////////////////////////3f/////////////9/f////////////3////////////////////////////////////////////////////////////////////////////////9////////////////3d3///////3//////////f/////////////
+////////////////////////////////////d3f/////////d3d3d///////////////9/////////////////////////////////////////////f///93/////////////3f///////////////////f///////f39/////////////////////93d///
+d3f///////93//////////////////f///////////////////////////////////////////////93/3f///////////////////////////f39///////////////////////////////////////////////d3d3d///////////////////////////
+///////////////////////////////////////////////////////////////////////////////////39+/////////39/f///////////////93d3d3//////////////////////f/////////////////////////////////////////////////
+////d3d3d///////////////////////9///9///9//3//////////////f3////////////9/////////////////93///////////////////////39/f///////////////////////////////////////////////////93d///////////////////
+///////////////////////////3//f39/f///93d//////////////////3//////////////f39///////9//3//////////////////////////////////////////////////////93d3f/////////////////////////////////////////////
+////9/f3////////////////////////////////////////9/f39///////9//////////3/////////////////////3f//////////////////////////////////////////////////////////////////3d3d///////////////d/////f/////
+//////////////////////////////////////////////////////////f///f////////////////////3////////////////////////////////d///////////////////////////////////////////////////////9///////////////////
+///////////////////////////////39/f///////////////////////////////////f/////////9///////d///////////////////////////////////////////////////////////////////////////////////////////////////////
+//////f39///////////////9//////////////////////////////////////////////////////////////39//////////////////////////////////////////////////////////////////////////////39//3////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////3////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9///////////////////////////////////
+///////////////////////////////////////////////////////////////////////////3///////////////////////////////////////////////////////////////////3////////////////////////9///////////////////////
+////////////////////////d/////////////////////////////////////f/////////////////////////////////////////////9/////////////////////////f/////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////9/////////////////////f/////////9///////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////////////3//////////////f/////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////3////////////////////////////////////////////////////9//////////3////////////////////////9///////////////////////////////////////////////
+//////////////////////////////f39/////////f////////////////////////////////////////////////////3///39/f///////////////////////////fv9///d/////93///3/////////////3d3d3d3d///////9/f39/f39/f3////
+////////d3d3d3d3d3d3d3d3////9/f39/f3//93d29vb29vb3d3d///9+/v7+vr4+Pj5+vr7+/v9/93b2tnZ2NfX2Nr/+/r6+/v9+/v7+/r7+/v9+/n6+v3d29nY19fX11dX19ja2939+/n49/f3d3f49/j5+fr5+v3d29nXVlXV1fr1dn/Y19dXWNn49/b
+69nb2dvT0dPX1+9vWVFJRENDQ0dPT13/39vTyr++vb7BxMPHzc/T5/dXUUhCPDs6NDzTwOdfW11VU1nTztXLzNHd38zKzs7R219jVU9IUU5Zb+vX19fNzM3IxsfKz9vvX1FOSkVFPz07OzUxMjUzNjXvur/N3cfBvLm5t6+0tra3vby5vr+/xtF3S0VBPz9E
+P0BLTEpARVPb21VNV0tDOjo8Pj0+OUe7zedHOzo2NzastN3/49vPxbavp62wr6y5ubmxt7vFxNvvV0dRRUpPR0VGPT5APjg4PUM9ODgyMjA8OkBbW1dZUUlDR01ANq6pwl9r2dndxciwtL65ubvBura5s7m3u87L2dV329//0/frUVNXQ+NdUU5CPTAvLzEs
+O87VX0c6PDYxOTc0t7bIZ11r/8jFwLS/xs3L1WfRys7R0dvX31nvz8HJvru5t7vFwsTn2b/G409PSD01OTk2ODg9PDk5Mzk+OTBTsdNIW1VJ1Wu+sL3Jw8LKv7+5vMDCxd/3d1X37/fZycPCyuPX0+9Xys/3Rz09LjCv0UxESkEzOjc2QkQ/Pi6px2/nd/d3
+58fCttPZ2cTFyrq5uMDDys7f/1/L1cvR0ctv/93Z/8zO399KSUVDP0JGPENGQjlFSUZJWWtPTarEW1FPT1N3ysfBWdfOyMTEurrD3dvva0VFR01v38/K1cr3587jb83L3WNdXy+ux9dGRTktNDQ0PUo+UWc4qLPX2//fSMjb58prd9HbzcC6vMrX29/ba1tN
+39/KzdHLz9vTys3N693NT1lTRkBDST46Ozs2NkpETF3/6z3BqMVJ/1tXyNfvy3fj387Cz8C/yc9fb2dOR1ndy7/D1b7L1c3GxtXO0cDOZ0vGrV/bOjUsLS4sL0BCO1VnLE+nwU/j3V+/yM/C78nFw72/ub7EzXdfb1lCVVnP0c/nzdXX0cvL1+/fd/dTa1FL
+RFE9MTMwLzEyRUZLV+tHLKW/S1lfY8vK3fe/38DFvL+7u7/O1VXnVU9j2bjBwMq9wsPRz9Ff13df90U8qudCRD8tJSwrKy9AO0XdVddDxKrB987L08fK58HDxsO4u8fKxdn3VW9vRE5M0dfZy8/K2eff2W9OTEdNW/dZTj1ERjUuLzM0NF9Lb+vX10w3pLlv
+wOu7z8jZyri7u7u4vLy+0/9ZUU1FTVvn02vIxtvf92tbY0hnTFM9X7DvS0c+LywvLS45TUZv5+u81VGir9G/zb3Kx//fzrnJv8HBy7/jUUFERDY7PkpbY1Xby93/a/ffU1NJVVnJb+tXX0dFNzIzNTpZWefjvt93Obyqxd/Cy9nLwtvHscm+vsnNzudNRTtJ
+PjtGQ9dR78zX2f9rT19O42t3a8+rz8FTSTQyMi42OEZLy1/IxMZTRaG577/N2cfVW+fCu8++0/fXb01EQD89Oj87UVdVd9nN729rW2NPU3fN/8Lrv1VJa1M1Oj9AT2PRz8jP0WM2n7bLyePHzcjX1b26zL7H13frR0Q4Pzw/PT5PW9tfzc/VZ1njVV/v789J
+or251Ug7OjYvODtOV81vy8jD3zmktL1338930efvZ7zrws7va+9VQT9BO0E+OUbdW1XFzNFfX2tVX19fzee51cjJW1lVPjg7QlXb1dXbwd1rP6ukuWvTz+PF4//Tws3VyltVTUY9Nz46SEBAP+PDY8HFydvf12fXyNfXvqayvdFVPTs+KzI7QmNrX8zTyVkz
+n6u/19/3/91XX9/R0d3OY1tdSj48QTtBRTc7791rzcLMy9vr0+PZ28XMw7zJxedfTkM4NzhTSN9P0d/ZQzOhtsjjd+9nzl1r19fT281PXVlLPztIO0xTTEz3vtnHvsHD09vV3cXRPqGtvb3/bzRGNzIyNjlCYz++X9VbMK2uvs3fY2PZ2U/Xzu+/619Id05B
+Pz48P0U+U/fZ2c6+ysfN787f09/L2b/Ru83nS1E/Ozo7TEfjSdNLT0kvtLa//+NV78fT48rHd+vFW1lPUT8/P0pVVVXfxLrRwb2/y8hr42/M69+hscnO50E0PjgwOTQ7Q0tMv9drTDSftszK62fv0dnn1cld0W9dUWdJSEI/SEhDSVvI0cvCwMPDyf/V2+vX
+y9XFz9VdZz9CPDU4OkJCSVfjTkdHMam1v+fOa//Ax83IvdnM1WfdT1tCTURMY1ndW8DEysDAxdHNVWNT9zqttb7I40swQD8wMTAyPEI6RtPn7102oq+7z9N3UcnZz8vH2czd51P3T1NFREpRSFt3w8PRv8DCxNXfX29fW93R78rdU0M6Pzg3Mj07RD1VU1dO
+TzKgsrvby+dnxci+u8nNwsVZT+NPQEdER/dO2c++xtO/vsXI21tdSlk6obPOxsxDMDU4MjUvOkM9Odnr711LQ6GuwcfX3Vff08C778/F3VlPXVE/RD5ISD/fXc/G0cDFvsfRb29ZWdvM0V/D1UY/OTs6MjBJTEVB/01OVUM8oa69ydfL582/urfTy77vWT9L
+OkA6Q1VfTtfZvsbbvsfJzeNbV0k7vKm6zr53Mjc0Miw4Mz9JPEDj59X/Vzqfr7rJzNVnz8m7us7Kx8xLQkk6QTk/TEtIW93By9u/xsPMa1dXT2PXy87v1e9KPzc8Lzg+RElNRl1f32tGxp+vyL7RyszDwrG31+u7T006PzM1Pz9vY09MucTX68LE1dFZVV1H
+N6SwtcvR4zI+MTEsOTlGU0xMW8V331dHn7C9wMPT18nLtLnOZ79OXT5FODRCQVVNR07EzMfVy8vH/19MX1fj0cLGzGPVSzg3PC88S0prTllJX+PR7zilrLzKy8f/v7+yu8RvzlE3PDM3N0RP6+td586/yePK3df3U09ZOa6us7vEa0FBNDE1NjFCRVVZ729r
+yddLPp+z18PK48/KyrW7z1n3UTc1STE1SEpfVVVd/77P18bT1etVV+Pd18y/xcxvX1k2NDo/PkxF3V9RT+vf3eNKobfFz7/X07++urrVd0xLLzI2NDVGY/dja+fPzMPL2dXZ51tXZ1WmsrrDwFs+QS4rPjwwSk5vV3d3a2vHX0ejtcTJz8Xrv8O/t+ddTUI6
+LzcyOExAXU5fVf/Hxd/Z2WffXffJztHCv8PKz0s6QS9XPElNU+NKY/93d9X/OJ+zws7BwNG+ybm660dEODgrPTE9TF9rZ8t30crDa8bvZ99vTc+ksbm6xOdCQTI2Mz81ONNIY13/b13/d/+/qrrX0cLDyL/EwrxXVUk4NDI5NDhrP3fr7+vXxdXVyF1b1+fP
+vcLGxbzf11tNNzVJSEVAd0tdQVv/b1tbQ6apv9vOu8G+wr2/y0hHPzAsPTk9PtlfzszZ1cjJ68TKb+vn90Kfqbi8v8s9QjwvLzs5OjjvUdtVVe9n52dOp6i938zGxb2+x853U0RBNC8vQzdISff379HZzcr/wd33/87Lw7y7ysbJz29HPzc1P05EQUlvSVFT
+22vrVUalqLvfvb3Ht7vHxVs+PTsxLjQ3V1V3/8jbydvAyVvE5+P/zkeun667xMNFPj43Lzs2NDHdQWtnX1nv79lvO66tvd/JvM64vMjLTD0+PDcxNTk81T7Z03fT2cbVY8Zvd9vNx723v83JvUtVQDs6O0VLN1FKU0tOX+f/90lEo7S7xru6vru2x8VENT04
+LjI6QE732+u8zs3XxtdLy1/r299Fn6m0wsfKNz44NDo1NDo+RE/P61VZ3d/JRUCisr/Xv7vAvL/dzUk0PD02NDlEX1dbytPf0d/R20z/92vLx8W+ucPM08xPQkI5OjtDPE09S0Nv3+//5+NPPZ+ywL66ubu3w8j3QC41NC84P093Vdd3vd3b49NrSGd3a9FI
+raGttMLGXTw/OTo1MC9GTTtFXV/bVdHO61VEn7C8vrzDvbvf12s8MDg9ODVISWNHz+/bx3ff0V9dWWPjyb3Ht77Fz8hvSj09Pjc5PURBP0xVU8vNb9Vj42elsLy2ury4usvOVzMtMDc7OlPnTFvPztlry0zjSVVrZ+Pn36Klr7++30o+Pjo8Mi4/Pkg+4+tf
+W8Vv0dVMvaavvbm8vr3CXeNVLzM1PTs9Tv9FTs/v3VP//+vfPGdn2b+8u7fExcnTTkU/RDw3O0w/RUlV3ffV687nvUS0p6q5tLm1vcNnWzooLi09O0Nn609byMj3U0LdY1dRW+vZTcykqLa6wllDPTk5OTAvW0FCT/fX2/93v+/JO6ekrr69uLvB60x3Ni4x
+PEVBTGf3Q93vy11JRuPNP0Z328u8vrq8v9XOd0I4Rjo5ODpLTkRbY9fN39vKw+c5oqGru7q3u8rVSUMtKi40PkdP69FEzMrTa0FC7+dZR+/Z40GfrLOzxetMPTs3NjYuOD1GQOv/38/Z68LLykzZn6bAuLi7601JPzctL0BKR0z/d01V3dlfTD7Pb11NY8jF
+ur/BvsFd42M9Rjw3OztBPz9MY2Pdy+fbvN3IVzafp7q4t7tvbz44MiwsPklPT99fd/fN129NPtXZZ1Xfx+fboLW4tedRSj06Njg1LkpEP1lb3d/V78O53dPVSqmisr+4vkNEQDc7MDRKW19ZXVdv31nV/0dvb+NOZ93Vvr29xsHNW1VPQkI2OT06Qk9JQctb
+xdvTx8HNztXT05+mvLPL/0Y3MjE4MEZbb/df92vBX+d3d09I429Z091Co6y1ybtRQD5LPjU1MTlKRk9Ezsznzsm/48zOxMREoZ+uxONPODw5N0BCR1trd01N/9Vdb2fPUV9nX2/Vyb+8w8bO208+Sz5END08Qz9Mb0zrd7/KvMTRyc2+vFHTn6/L1VE4NDYy
+PT1IU1PXW1fXxF1X51NnQGtby99XvKSuvtnEUzpCO0MzOS0+Qkpf28/f17rTt+vbwcDE/0WfreNvOjo1Pj9NXVVJT/dXUevba1XLTuNLVe/Tzci7u77K21tKPDs9Qzk7OUZGT1fZ19nPwcHB0dO9vL/F/6Wrv1dJNzY6QUFVU0pNb+dR293vVV//50hRZ93d
+QqCsscn3XTxBOjtAOTQxP0xVa86/0cTGxr/d0cO+x85nq6y8RD46OkVXXetNS09B71Vf52ffTGvNWVF398fCvbzB6+dMOEU4PTtHPDpFXVFV277IvcTOutnDv73Ex+OvrbtPPzc6PUdMTFVEU0drZ1/r99tBQvfvWVvdQqimrbfv1TxARD07QzY4Mkhd9//I
+vr6+z7rjys/Cys7R36uw0UY/O0NMWXdXS0dIQlVv/13TXVFfU8lZ39O/wbq6xuP/Qz85OThCPT09S11X58rFvMjHx3fByb3GxsS+s8DPQzs8PElNUVdIPllAd+/fV85PSVl3yV/j72ezp62/2103Szo7PD85OD1CXePjv8G40cvHW8rKw8/OxsG80+tCPENE
+79tXTkxFSEVj4+Pra293Wf/E99fNvry+wet3Qz07Nzo8Qj5ET1tK99W9vcPBv9HjusO9w8XCvsTrX0Q7QERPU0pJSEBLRefb99lTW2dnd8/jyc/CtbO3xXdJPEA8Pz9FQj9TTkxjy76+xsy+4/+9zszJ38vB3VdMRD5FW/9jT01bT0tV48rbY+tj3+PnxsvH
+w7y9v9HjST8zNzw+RUZFU2NVR9nGu83JycLj18HFxcPNxr7ra01ESEZXVWtLSU1XSllVzM9TW0/f3+Pby8vJv7vAzN9TQDo8PkJHS0Vrb1NT48C71czGvf/JzcnMzNPDz19KVT1KSmNjW0dFX01ZT1nK3Vv/b+fXxtO/w8K/wsz/TUA4NDg9QUtKWef/WVHT
+uLzNzcK+zsnExMnJ08bnW01NRUZDTVNOS0RPV1VNT8/nTmPr18/Hyby6wLy+yNtLQzg3O0JDR1FO6+/vX86+vtfXzMDM29PLx9XR02tNTktBU1NVVU5ISFFrU0pjzudf9+PVxefDwbrExtHfVT46NzQ8QEVPV2/n/3fnyb/BzePLwMTLycu/zNPTZ09JS0RL
+Y0lVTE5IW2dfVWffUVtv28vG18a/wMrK7+NMQj0+PUdLTV1Z393f3dXRv8vZ487E4+vbycrM42NTSUtHSFfdW1tMU1Nd/1lfZ83dX3fdxM/PzMfIzutZd0xCQD5CSVNfa3fn2evZ4+PE0ff31cPj29nVys/vVV9MT1NLU99va1dfW+/j/19n2W9OZ8zCzdnC
+ycrPb1NbR0RAP0tKT1Nrb+/O787T27//d+fK1dnd1c/HzP9nTltfVV9nz/dZWWNZ7/9vU1vdX1lnztnNd9nZ3WtbT1FLSEVBV1db5+ff287Zz9Xjw/db68fR1ePf09XZa1NTT1tMT1n//11fZ29v52tjT99L/+/K1c7b19Hn6/dXV1FPS01fW2/v9+/T293T
+0f/Kd//V69vj993X09VvW0xbX19rb9n/Y29jd193Z1NO51Xvz+/J1d3n329nW1tdU1FOU1lv79fn49fX1+ff/91rVdPj1ePr69vd32d3TlVnWWNn4/93//93//93V3dbV9XX69HP5+tva2dZXU9jT1ldWWNn2dvj3dPV32/NXf/j/93n593d387jd/dVXf/3
+b+vd9+dr72f3WV9O31dd993n2dH/91/3WVf/U2NTWWtdd+vd1dnn09133fdvY2tn3dXj39/n0+9r/1NZZ19fX+9r72//Z+9dd91bV1vr5+/X0+PrX1v3WWNj/2dn72Nr6+fV0+fX29//a2P/Z//332vX4//Z52djU1t3Y/f/5/fna+//52P/Z2dvd//b39vV
+3e9vX11jZ2NrZ2N3Z1/d59/X7+v/d/9f91tna+fb7/fZ693f7/9bY/ddZ2dr5//jd+f/b/dda1lf/+fr387n929nW2Nja/93a3dna9/r2d/j9+N3a19fZ2d39+v/5+vv3d//a11j92drd/fj9+f/7993Y2/3Z2fn2Xfn0/d392NZb29na1lnX/9369/b5+fn
+Y/9rZ1tvY3fb5+v/3efZ2+PnWW//Y2df9/fn52P342tjX3dXX3fvd+fV9/fvX1tb/3fn9//ja9/n2dvv699va2dbY29db+tvY//rd+Pr93dZY/dvb2ff6+fn9+ffb2dr73dr2+dv59vv63dnX/dr//9rXfdn6+vf2ffn/1tZY19rY1//4/dj9+Nv2e/n73df
+b2drY+fb49135+Prd1/rb2fr43fr1+9va2NVZ2tvd29n73fn69vj7/fvZ1tdXf//Xf/j52v35+Pb//f3Z2ff/29r593j3evv6+trY/dna+P3b//r52f/XVv/b/dva1//6/fj29fvd2dnWWNfb/djb+PfY+v359/n69/3Z3fva2vd19vj6/f/62tbZ2tbd/f/
+9///b2djW2dv9/dv/+/n693Z2ff372ddWWf/72Nr/+9j6+fr729vb2tv//d3b+fX393v9/fva2drb2//b+///+t3b2N3Z/ffb2f//+/v39vb/2d3Y1lfY2/vXf//63f/49/f/+vnb19v/2v/3dnX4+93d+t3W2t3b3d373f/b2d3Z2f//+N3Z/fv7/fn3d93
+d29jWV1n9/9v73fv59/f4+t3929n9+9v93ff5+fva/dr729fZ29vb2//9+f//293///v529j9+P34+ff42tva2NZX2P/b2/v9+/3593n5//jb1tn92fv793d5+dr72//b2dn///3a//rb2v//2d3b//rb2f/4//n393372trY1tdd/d3///v/+/f4/f/Z29n
+Y3f3/3d35+v37////+t3Z2d3b3dnd+Pr//frd2//6/9na+/v/+vn4+9rb29nW11vb2936+t3993n5/f/919bd3drb+ff4+t39/9rd+9na3fv/2f/4+////dr9//r62t39+f/99/n7/9na11fY3dvb2/343fn3+/3d19vW13352935/f373f393f3b3drZ///
+Z/fd5+9372f/5+t3a///63f/6+Nn/3dvY1v/b29rd+v/b9/f6+v/b19VVf/r79/T3+v/d/9nZ29ra2tvb3dvd+fn////d//v3f9v///v9+fn5/fv/2tnZ/93d2/34/933efv93dvZ11Z7/9399/v/29rd293b2NrX2NnZ3fv493v63dr63fr//fr6+/37+dv
+Y/dnX1ln993Z18/V3e/j/29nX19bVVVrZ/fn3e/3d/9ja29jXWdrY293d93OztXb2+Pn4933Z2dva2Nvb29rd+f3a/fv//9jd3dnY+/X62drZ1FTa2dnb1137/937/fn3+fn29POz9PP29nvd01FPzY8ycvfa9/R43ddV2dba19f393d4+vHz7+/wcfb53dZ
+TUlTXV//79/b29PN083XZ1VR/0dHPj4/PTw+Ozs7177H62Pv79vKxsLCw8THyby7uLu8ur7Av83X41FMR0ZBOjs+Q0tHSUtNSkdMVUs2M1m9y19ZVdfZb2tdRj5KTkpPW1tMY19jzMO5vru8vru5s7W2uby8v8XRyM/dZ0s8R0hET01MSD9KUT1LTD46r669
+71FHQ0JCR0c8O0tITEl3b8HBvby0tbu3ur/Ay83bZ1VPQzs2MDs8NTE3Niq4vd9JUUA2UVdCoKe0zsfZ0+ddZ1nvSe9BRXdvxc7AvLi5ure3ub/Da1VVW0g3N1FJ20NOQUz3Pf9KWU1ZT01PRN2ftl/fUUM+Rjw4b0g+REA6TE3Ty729vriyt6+/vMvd60lb
+Wz9DSsfjX1NrTKG8xd/3PzRGSUFPM62p0TzvVztHPE1JT0n/SU1Md9W/ubivsriyuLTA5+M/bzs7PklIQl1n/2tJwUZHXU5fWWdCSfdFL6PIQD83Ly5FNUtNWU9DV11X1bu4r6ytrrWvsrrGW0o4LDI4V0pRWVtHob20x+9RRsjjU2tZR1csn8ZGWz4vT91A
+PlE9Sk9LUdXJuriwrq+1urm61V9BVTdENlM2PDg8U1U+19O9X8vZz1PR4+dP/y6hyU9XSz9FUT1GS0NGPk9Pa+u0u7CqrLO6t8HjSUouOjpIOVtCPbesu+/DWURfwndKZ1tf929HLZ+5Y+db2Tr3Pz/I/0hva0j/zL22u7S6u9Pn30ZVRDY4NTozQjpdVVNr
+18DR08nf7+9Xd+9PRy5dpsZHZ7w3b99JT11jTldPTOOxwLyyu7fT3VE+Pj04OEF3OTmmtLTb0//vzs7vsG9A4+vnWfdOMKC6TkjnPjjMSl1nTkPrW0s+xrbIvsm/wc9TRFc/RTcvRzM8QE9fzc2+vbu4z7+13VPX32dMPTOit11fTMJIa+9KSkE9Q0tBW1HB
+t7u+xt3VXVNnQ1lXQVvNV8fCvsjEybzHyMfTytPIZ+fOW13bunfnSUBjR0bb2UljREhDS1NI0dPP09fvXVs8/0ZVPkFBX0/Z99fXxsHEv8rI273H7+PN51dn27/H00pMY01PS9VFU0VHSllZUUvD3e/jZ1NIQUpVz9Nn/75LznddXdHH1cDKzefjxt/n0+s+
+/8/VUUNEPUl3Z83P4//3UUdn59F32c/V59tnZ0tnX1NOU0/XVdN3z9vRwc/O19nIa+fr49vdWU1LREVIRTxDa01fZ3fv2ePXb9Hb7+PV6+/nz+fXd9H/999r59HrTHdZXf//b+/Hb//d0Wdd3WtER05HV1VNSv9f1+fjzdvV7+tj3dXb1dHP49/322/32+vr
+69Nvzdtra913X//jXWP/5/frXe9dW0tZV0tITkZXWeffd9l339137/f/4+fb09n/63fj/2/j4+/NZ+vLa1NTX0pda93b3/drX9/T1dN3Sj9OT0FKa1Xvd+PM39vKzc/X0dPV09nO/+drWUFGS1dOd3dPS1FMSUdKW93v4+vT29vLz9/r3cvMwMO+zsrNzMLD
+w8PCw8HKztHd12tfS0Q/ODI0MjI4NTU4Ojc8OTc4ODc8OT5JP0JjuLG0uL67vLmvuLe1ubWurqypqaimpKWprLG+zv9ZR0U+OzZXMjMvMSwrKSssKiYjIiEfHy8yLywtLjFBRNG8wL68r7Gvq6eipKajpaapq6iqrLK3ubvJycDL0/f3V04/Qj05MSssKbas
+wVU0KyEoJycpLS8uNS83RW/GwLGzqquxub7Va1lPY9fT59nAtbS6sLi1uruysr/A69td97673Ts7MjMuLCkrLScxLzc8U//n1/dra9lVSefv69fj0WfTvsO+r7OtrK2yuMO5vdf/QUmfoa/XXS8rNi4uMiwmMy4wMllfZ0/Iu9lLVUlAMzAzNDQ/Y0vJwbWu
+qbCssLmxtbO+2d01pafC0b7rOdfPT2dHREzrOkTr38hZ00pJODAuLygmJCYsLTE5QlNrwri/w7/Fur68ubzETqqfn6e10U1N611C3zZNX8HfuraysLCvvrTjTTQ2IiEiICksLSozPj7PzLzC0cHZtb7VX9cqn66wyUhjX8LNycP/58DCuLeyr6+zsrvCZz85
+MCsvLScsNSwxLz00SUxXymdvX3dd601fSSyfn7G2wjBvQmtGXfdVu8a1sqqmqaqoqL+tyGM7KjUoKzsvOTc5R0TTV+fDRVM7Y0c/MiwhoOs2Okw4Z0drd+tj28LLvbqsqKmqqq65229dPS8yOzQ6W1dMTVfDxLXEvsVjW09GPzcwHz+1odvvQyv3KTlCLzpB
+TnfGv6alo6SrqLHDz1c8Ni45M/863W/3xruuq7DIr8RXwudAMiohoNMuLyswNyw7NEgzOjxG/1nVra6xsbzfU0BFOjg+Pv/DvbvIvbi2r6autba0ysXOSDcuJ6VO57LvRTBnMDQ7LTE1QTRVXc61tK69ve//SP85PjwwQNlb0+O+sqispqivr8XHr8VJPUMu
+qPc+OiwvPjA+Oj80OkE6OU/PzrXTyEI6My03Li42PU7LzrvAt7CqpqSqr7O1v8vOTz9CKp9vrLr/VT5IND5NLjk1PztJ48O+rtdXRzk8NjYxLjM2RG9Pb925taqmrKy8ucbDyk5DOiahuF9NSzM92TzvXUhJSe9HS867v8Oz51EvLi4tMCw6MWfZb8vGvrav
+r7Gyx8njb09bP0gtr7BJu6nOS8NVRkXVOlVV70vItri9urVRQj47Oi48PTs561FO68PGuLWpuLXA3UxK9zZNMSus9zg9QC5rxkrj71fTd8BvwL+zwMPOzDw4Li44OTpFY2uvwLi7t7CurrO9a/89QDQuOirbrMMpvqln38JfW0dOPF13Y/fGvr3Du+fZPDs+
+VTXva1lNw87Iw7Wzr7Krvs5vQjs1Kzo1NLzKOzU7QTxbU1v362Pb377JvsK2xsHbV1cvPD03SVFr/7vCrruyvKu0tLXNRzw0NS0sMDLdwXcwLK68wMfL61Fb1/fvz1/jx8LD1+9NRDw9X0PH48F3usi5urixrrOuw+tVOy80LjAw3ddPNzQvPkDXNsXRZ9fK
+18e/x8y/1+dOUzRCNEc8W9fZxrC9rLStrq2wtctvTzYyMjMzLDvA9z0wLLvCtO/Z21VOz13fy29j51VdU1s5O1dTa2/3vcC7wbi6sbivtLi7WVdDNDY8NjI+5+ffQUo+RFddP7vj78TPa8nN0dHTW0xfOz0+W0xGT//j0bXHrbeztbrHz2NKQjg4SDg8Pl3j
+z1tXX9FfuuO1ytPM12djd1lZW1FFVUY+QkZNW2vr11m+77rnzcHKxtXPXU9OTmtRW0nn09Pv5+Nfb8BRt8LDzMbZb9Vv/293QkZFQD5JT08/S1ld29m7z8TG09dnd0ZNPTxOTTtH/9/n23fP18Xbzr6+y9XFztnRy+9rVUxVTVNOd293V3fT12PjzONZW2dV
+Sz9JSEhISV9Va+vv19V3//dr/9nMvL3Ry7vExMrF2dPvd1NXV1dva1dMZ2tTXf/M2ffZ729RTUZKRklLU0xdVf/f3e/b291Xa8vfZ2PM59f/63fn91nf59935+dnd9nP4+/rxd3r92vnb1dbW0tfVU9Xz9vf69/r52tZV2Nfydf399/b1+vvY29jY+/32ef3
+7/9P92fr79Pbd2tbd2//WWv/d1lvY19bWV/r3+/r691359fvXV/vX2tVUVVVa1H/69nRy87X5+fP493j3/9bVU9jW1t3Z1fj99fjzufZ1dn39/9jXevv42fvb+fna11jVVFVWW9d//d37/fv3ffX39XOU/dTd2d3a/dn/+/j59fV48/P1XdvX+dZ92//a2Pv
+/3dVU0xTSFNf72fj7+NbY+fv3e/j3+9fW2Pnb3fZ5+/f2dnf09Hbzsrj9+ddU/93b2djY1H3X1tfX1Vbb29f7+Nja13ja+vb59nZa29na19d/3dj6/fjzNvv3c3N0dnb62Nr92dZXV1rb2dTXU9XT01dX/ff4+djY+tn99/X32//Vef/9//X5+fV3dff5+fd
+1efj69frVWvb629v92dVTV1fV0pOa3drb/d3Y13j///349v/XW9v62dr0Wtr7+vr3+Pf39PV793v73fr//fr/2f/52Nr91tdUVNnZ2tva91fZ2f/7+Pva1tfW/drY3fr59v/79fn3+f309vra/fvVV3rd+t33f9bZ9/ra29n39vn1+Pd/3ff99vb4+djW19j
+/11rb1lfa3fja11jY+/jb+fn/3dra1//Y2//Z1vn7/dn/+/n6+fn2dvr42tv99/f59trb/fvb2tn72/v7+N3593//+Pv72tjWVVd/1tdY1Vd9+NfY2Nv6+ff3c/d69/Z7+/Z/2tnX//v52935+9j92/n73fv7+/3W/f3Z13r4//n//9vWV1v/1lVWfdv9+fb
+5+fj29//3+/j//f/7+v39/9j9+v/3+v/72/vb+Pj2f9rXVVrY1Vn71dXd+//W2Nn7/ff29/v793Z39vb6+/3V2//d11r6+fr79nf5+drW3dnY93vX19da2Nnb3dnX/ffb2tdXedv6+Pb193f52v3///nd2Pv6+/vZ/9j7+t3393j62/vZ+tv6193b29r/2dd
+a1Vb/+//a+//a+/b2ePn/+ff793f72drZ2dnW11nb3djZ+fv6/dr793v9+/3Z1trb3f/d3dna2/j919fa+9v3dnn3e/j4//f39Vnb2f/d2tvd2tnX+/r/2vvZ2//X/fj92vrd//3a11ra1tv5+//Xet3Y93d3+dv6/f/7+vf6+/3/+vnd3dn5/9vd3fn7+Nj
+a2/3b+v/719372tnY2NXZ+Pn/+f3Z29j//f3Z3fr6//v93dna2//5+/v7+fv9+vb3evvd+/v9/fv63dr5+P392djW1lfd+frd+dbW2/33/dn73dj/3frXWddX11n73fb3dnn63f33eP39+/b3+PV7///319ja19jZ+vX93djXWP37//vY2///2NjY1tnY2N3
+d2f//+///+Pb4+frZ2//7/fn///r2dvd3ffrZ2/r//dvd+tbXWtv33f/7//3629rW11nWWtda11r39fn6+Pj4+P/Z2/33+Prd+vn7+/3/29jb+fn69/n72/vd+t3b+P3/29fb19jX2NZX2dnb///d+vv4+v3b2/34+/j93d37/f/Z2vn9/fn5+f/59//X2d3
+72/v6+fr5+d3b2NvY19dX1tdd3fn4//n5/f3a+/n2+//5/93/19na2dn9+N37+Prb/f39+fr92/f4+f/b2Nja2dva+f/3+f3a2/v4///d//r99/jY1336+N3b2drXf/3/193b3fnX2f3b2t3/+//7+Prd+/363fn9/93/+/v///va//n52v/23dj7+/vd2vv
+9/dj6/93a+frb2d39/9vZ+/j3ed3Z2drZ2t3d+936/939+Pj2+PvZ19fY/drZ3fr49/va2tv7+fda2/3b+/ra2drY2d359n35+vva3dd99/n4+Pn///rd2/3///n42tf/1Vjd///a19jZ2N36+/3/+djY1t3/2NjX3fn5+/n7/f33+/v293b3+P/3+vj2ePf
+3ev3d/9XT1tVb29fd2dbY/9rd29nb29na3dvXWv39+9d7+v//+vr49nX2ePr9+/n6+vr4+/v5+/r92t319vrd2dvXVlrb/9n//fra2NrY19fY3dvY19rW11vZ+/v69/d3+fn6/fr3+PZ39/f5/dna+f/6+9n529ra+fvd2t3/29va29nXWNn929fb2trZ2tn
+Z/fv5+vn5+vv3d1vb3fv//f/b2/v393f4+Prb2/va/93b3d3b3fvd3f3b293a2tjW1lnZ3d3/+937+9v7+fr7+fn529vd2tn9+936+//9/9r/+vr593d6/9373djd/fv73drd2drb2djZ2vr92Nva3fv6+fr6/f/7/dvZ2tr9//r9+Prd+/n72f/5+P/7+fj
+7+/n6+vr/29jZ2tfa3dvZ/f/b/93//9n/+/nb29jb3f/9/fv9//j729j92//d+fb3+vj3+v3d/fr33dv////b//vb2v39/dvX2fvZ2trb29v//93a3f/6+Pvb/9va/93a/9379/36+ff6/fd5+/39/9vb2tdY2//d293/2dr7/fv5+t3b2tfd2//7+/r93fr
+93d39293b//v//fr5/f3593f7+v/Z2drZ2tv//9v//9vZ2v/b+/v//dvb///d+///+/v//9nb/f3d2/v7+9r9+/v5+ff3+/v6/dna29v/+vv/3drX1lja19n5+Pn4/drZ2dv7/f/b/9vZ2t3d2Nvd//r93dv7+939+/v9+v/d3f//+vf4+fn3ef36+Pr9+fr
+/29fX3drb+/r7/93/2Nna3d3Y19ja2dfd/f//+/3629vb2tra/f/79/f6/fv92dv4/fn4+/nb29rY93b2+//Z2NfX2Nnd+9va/93a2fv69/3/3drY29va2drb3fr4+/v4+/v4+vr49nf4+tjZ29rb3f//+vr6+/r5/f3d2NrZ1tZWV1jY3dd/2tZb2/b5+PT
+2dfVb9v/b2dnW1dZWV9ZVWdrX+ff09vR2c7T2dfOzs/Z23djWdn/Y11NVVVNR0ZFQEBF3b7G2fdrV1dZa3fX19PMzdvN0dnTzcrNzNPb52t3a//n//9vWVVXV01IRklLRT1DXf9JR05VZ2///93TztXFxMjDxcHHycrX3efj9+/3///j69lj2c3n2XdnZ09T
+UXdHZ05CUUxLTkdITVNTzbi/yf/n91tPW1ldS0I4Qrvr12tXSVdJTFXv92P/083GvsfBvrqysrW2vcHIvl3dZ1VMS0M/STw7PEFIRkRZwNtASVU/Q0FNSk9FT0JDSUpIZ1Nb32fdb2Nna1/HxsG9sbSyramrrqappKuqsbu2tsu8uG9POy4tJCcpJW8tKiEj
+JyAoKy81NzZEr7/H29lfPkxLPUJBNzdCQ0pnwLu9r6mrqqempaipoqSoqKuprKirqrS4w+dXPjUlTLlGKCgpISEnKygnJyUtLSctQT4zTuNNV07/2W9T38rKsrSsra2ssKytq6q2scjPyuPvxb/Kw8XZwdHNXzGsujxVOTkqLyssMSkkxd88Kzs9JS8+Q0lP
+TT9PX0Lrx7+7rayrp6yvrKKqrrevsMpf311d329Ob+9rTT0xK6fbPUJjQywzMDg9TlFAS3dN9/9COUVLTTo3OTw2MjU6Rf9vvaits6uqqKmxrq6528f/xudrb2vG729NPD0sJ6jLNUk8Rj1MRFFEOKO1yUlZP0JVPllvRTJMQzgvOkJGS9vIvbyzrKaqr6+y
+wr5PyFtZS0ZOSuNNS009bzkrn7JbytdNTdU/Sc5HTk5TSWNZPD9APz9LNDc5Ni4yMTxDW8y+wL6srKqlq7K0sve2vetXY1XD087Z2UZrXyyiv79NQfc4SWdNL6TB/2NTPTI2NjZHPjk7Nj45PDQ5RUVfzcXHvruvo6+ssbuw78/JZ2PIyMDG2dtdTFlfXTKf
+rc277+M8R0z/PjkxNT0vLjIyMi46NjMxODU8Nj5DVffLu7a3rqmmo6qssrm3u8bJ71m8zsHZ70s/V1NrKq6uz99vWS9MOSaqSDwvZywoMzsxMzc3PS84REVAQ//Jx765ubGmqaqop7K7uKvbwb7f78PJz8pVW1tLRkMrb63XPkk0NjdFNjY+LTM+LzY6PzY+
+QU4/PkVNVVvv18/Lt8C9saurrKiqr8Svr8LPy+v/x93V605PQ0dHTz0vq9s3P1MvPT0kql9ZM004LD86REdDRl1bTWv/01+9wcC7v7qzr7Cvr6e2t769t9/FxtPLyd9RTEE+Pzs5NDCsYzRdNygvOTc2NzE3SP9CU0tP91vr1edP2dHPY7/EwbzKubu0sLCu
+r66/vcDOS11R99tv/1dHQD9GOD4zJabOTEI7My86Kay2Tj87QkNVW1NXXVfr0WPTy8jAube7uK61uri0t7m5vd9nv05JSVFETktTQT9BNjg2OTwkp77nQzo9N1NPT0Q+PldZymfZ1cbT48bE5+vOyMa/yMq4yb/Jx77K0bK6yV3/V0dETU5XTEk8Pz04Qjk4
+NSOnsv87Sz8+QjkwpsHZzMTGd8DPy8Pf27zP99vD193Ay7m8ycvIwMLVyNvjRVc7SzpPPDw+Pjo8Njo2MDo7J7euyU/n70/ny87Ld2vG27zJxsLF2+PfzVlNU/drZ2/T389ra9fdWb29xttnd1U4vlP3PkZVREhNRTw+PEEta6y7S05nV29HTqm/ytHNW+/D
+68zN/13Lym9rzstVzMxr529Ma2f3U01ZZ0NES0BI2U1ZUVtrTVVNUU33/09XrLDL0/fv/2Nd6+93W9tMUcTP293ra1HfT1tX611nvmfZ929r3+PGzcnVb2NCPz/TSVFHTm9V/0lIWV1nT9W2tbvH08/ZX0qqtLvT39c/vT2/SGdRVV9PTFVRTkvOT1VXTkdT
+W19KZ2NdUUrnVUxvxU1nd+vvb2/v/+tPyLquuLzN39vO2+f/11tT509HSlfbTEhPSEZATUJXTc5nUU9RU113b+Pjzd93b+NjVcl3/9XZ/+PrV99b/89TZ8W2u8PXz1XfsbW7ztlOQkTGSltHPkVJUT9LRUb/SFlZUVVvUWNMX1XK0ePj09fvusHEzsrf19fX
+2dnZxdXby8/I411jT0xTUV1XZ05NRudJW01fW1NdT1FPZ+v3b9/rVWv/b3fb99HXZ+vjb3fE48ndztPR0+Pv683f1+vMzMbM2fdZU+tZWVtfTktHa0pRPkNHV1lLRUtnUWdnd2//3e/Vd/fVzcPGxcXD0by8yc/d1dXf619r1+dva9nXa2djWVFRW05HT01I
+RT1VZ01OU05PTU1TW2Nd43fv4+PZ7//R19fRzMjByMrXw7zJyM7P2dHVztHV39nb39/3X05CRkBKUUdLS0REQUxvSUNGT1dXZ1VVX19vb/fX1dvT49fTz8nFxcnCx8vDusXM38/d3dHX72fra3dd619XSEpHS0RJRkhMT1FTT1VVRFVNVUtvW1Vn6+ff987f
+1dfXysjExMTHy8XFzs7O0cjV7/d3919ZX1VPXV1bX11LUURNSVNRR0xKWWNXa2drXV9ra9F379nV19fN3dfZ09n3z9nRxsbK0dXR1dPV23djY11jY1tRWVdPTk9ZVUhOSV9LTllba/9312/v4//r/2vv79/369/j493d2+fj4+ffz9vb1c/R2dnd1+Pd5/dX
+Y1dbX01OW1ddUV1ZTFVOY19b/1tjW2f/7//363dr7+Pd49/f2dnT093V0d/b6+/f6+PZ3+fn6+vn93fvXVVZY2NdW1VfWW9bZ2dRTk5jW0trb3dv793nW2/f3+93693f39fN09HZ1dXX3dfr593d99vdb+P/d/9vb+ddVU9ZTkxLSFlRT1lbd19nZ2tjd+Pr
+/93d49/r19Hb2//v39vv/+/n2efX4933Z///687n0+trb/dfa19jY1lRV1VnWV9XX2Nna2tj/1VdXev39+/j29fP2ePn39vd6/9v3//f6+/f49/n2fdrb2Nf72trd2dfZ1tnZ11nb2dZV19XZ13362tj72vr///v63fd39fX39vd1dXd19fd693Zb2P/9//3
+b13/XU1TVVNZY2t3b2N3d3drb/fvb2Nn//f3Z2/36+/j9+v3X+/Za//vz9/b49/v9+vd3+tja//jb/fv5+tv9+Nva2djXWNfY2NnVVtdV1VbWV9fX2Nj7+Pv993f39XX09v/d9/d3c7R4+vZ5+vj5+vr//dr93d3/2f//+tj/1tTT09ZV11bW1lXV2NnW29n
+63fr59/Z69vj5+fb3evd3+/n2dv349fj39/d5+/f5+vfd1t3/2dnY2drZ2NVXVlPVVVXV19nd2Nda11bd2N3b+9v6+fb29vf39fd0c7O0d/Ly9nPztXr3/dvd3djX19ZT1NbWVdVWWdfW1FbT1NPU11rd+/v6//r3+9359/n99vj0dnZ09vd2dfb3efj7+Pr
+Z2ff62tv/2tnZ3drZ11VV29jY2ddY19fb/9nX19bWVlra3f/7//v5+/b3dnT1c7Tz8/P09vr59/n9/dj9+9j929vY2Nna11ZVVVfX1Vba2NbZ2f3b29jWWdfd3f/6+vr49/d99vb59ff293f3d/n49PR5/fr7+vr9///d11ZV2ddd+93W1tv/1trXVdj/3f3
+d/d37/9352Njb19rX//r4+tv7/d35+fj3ePb2dXb09fvY//v59/v//93b19rZ11dZ3djZ2NfZ/93V2Nrb2v/63f/d2fv/3fr9+Pf6//j39/n3efr5+9372/369/d5/9fb29jb3djd293a3fr9//n7+9r93dnb2NRW19rW2//d/fr/+tva+P/9//r5+vn3dvj
+4+Pn5+vn6+vr9/fv/2Nbb193b2djb293b2dvZ3d363df72//42tv/9//39/3//9vd/f3/+/35+fv5//r5+vn5/93a29vZ/fv7+9rZ11ra2drb2Nna2djb+/r9+vf9+fn9///Z1t36+/r6+Pn7/9va2tvd/dvd+vr7+/n7+Pj/+vr9+/////v92djXV1na3f/
+Y2vn7+9nd+939/f/a+v3d+9vX2vv5+/f6+Pv63f3b29vb/d3///373dva/9na29r93f36+Pv79/3b/939/f/9933d3fn629v6+93d/dra19bWWf//+tv7/fvd/d3////d+//9+vj9//3/3fv7/93/2/v3/9vd2dfY29vd2dv6+Pn7//j9//n5+vn6/fj33dn
+b2/r73d3Z2ddZ2NdY2//5+v///f//293b293b3fv/+/r9/fj/29392/v/2//7/f/9+/v9+/d3efv9//3/2drb/fn9+/f63dv929ra2//b29vb2tjX2tdW2dvZ293d+frZ3drY/fn7/f37+fb4+v35+vj7+Pj5+vn39/vd3fv/+/3/2trX2NjWVljZ3dva2tv
+/29r9/dna3drb3f34+vr5/9nd/93//f//+/v59/n4+vv39vj5+fj9/9va3d3//9vd2tfb3dra/93d3dvd///Z13//2tnb2tja2//72dnb2/v9+/39+/n39/n4+fn5/f35+//7+vj6+/v6+/v73d3b2Nvb2dZW1tdXV1v79/b393j6//v/2tnd//vd11dX1td
+Z3fn6+ff3dnb2ePn/2/3d19dX2dnX2935+Pf2dfZ4+vf4/9jX1tXU1dfa2trd/f3b+fd39XX2dHb7/dnX05XV1dfZ/f3///j39/r3ePr911ZT05OUVdn693Tzs/RzcLFzM/T2XdbSktIQ0dIS1Vf/+Pf59/Vy9Pf3XdfUU9VUU9Vd+fr1czMycjLycHEzd3/
+Z01DQ0NFR0tOT11r69/f4+Pd29v35+v/XV1r93fv3dnRzdPTz83V2+P/Y1lOTEpLTk5RV1tbb3fn29HRzc/b1dn3Z19OU1lb7+/3493Z09HRycvP3/d3W0VAQUJHT09fY2tr493ZzsfJz9ff7+9XTk1VX1/r7+ff3czPyMfHys/rX0xFRD4+QE5bZ2dv49PL
+x8S/wsjP71dKQElFQ0tba+vb0c/JxcrMxs3dd19MPzw7RUlOV2vf49vMw7+/vb/GzN1rT0E+QU5JTFFbb+//39HPz9fX2+9fb11MTlNZ39/b2dnM08nDx8zZ52tdSUU/Oz5BRGddXf/v29fRzsXO29Xd519bW1VXW2vdys3bzsjCytXX71dbWUlNRD9JRkhK
+W+P3b93Nx8zNzs/f2+/r/19LT09rb+vFz8rGy9vN7/9ZU0hPS09OQT9BSUxX28zNyc7Vz93MysTHzu9dTENGTEtNZ+fr17i91dff51vvZ09RSUI9PENXb93PwsLIy9XVz87j4+9rW09dT0dJa1VO/7vR3d3vb1VRUWtvT01IRD9BWe/Rz8W9vcS+wr+/2dXO
+1+v/419FR1F3TevFw7tPY1lXTkhTT0tIPzw+PkNT79vnysDRa1NVa1VO3fd3991jWWdd1+fvvqywu83F087Rzc7j4/9TS0ZIT/dr3WNnv6+6yOfvTFdvY3fdRTs9Pm9DQ0PGqrJXO0M9Ozo8PUA4NjM1NztPd2/fz+N3TUZLV9/ZyMLCuMDR48W/v8q0qKOs
+vtvjv8q/tbm+xsz3TUlfd3dCMkapsMxOPjk8QUZOWfdXNi8vPEU2Ok63uFk5MUg5NUtOSUg/PTk5Pmvb62fj2ed3T1Vnzcy7t7OusL/VY73F5+esoafTz1nP3czAub3J1dtLP0NLd1c4O6axwF9CNDZEQk1r0e89NjE7QTUzOK21xWc2PD8+R+fj91FPPDw8
+StXM2dnZ3+9j/1dZz9fVv7u2zVlK39NnQ8ChrbhVb0zj09vGu7jN0WM/PkBVy9HrP8antddTSTo7VVXfy1sxOTA9ODcvq8m+sUo/PlVFTdlvW1dRQD5IQvfI1//Mzutv42NnyMXNx7G0xd1vStFnO6Otvr5IUVXHXW/Cutvbb084Oj1G529FMqOtx0I+QzJd
+Z0zMvzlBOTk4RjHGrcS9ukc7QFVCb2tTSmtLOj5APs/V2c3G391n4+POvLq8rKu0vM/jW8BV75+vvstjTENjSevF2UpKQz8xMzU8Sz88Pi6jvkY6RUw9PVe8zko5QT9NVS+hrr/HrVs7Z0pr0/dNV0pZSExKSMHG08y9z9NvXczj18Tnr63ZzufjRGsyzKrG
+38tdOkVjRlvB41FRPTo9NTg/Yz5OWT66rXdP62syOte70WM+Rkg+d02ir+evtclP90tF2eNn31VTWd1BU8zIXb/Z0dVvRtvX61dduLPR40fZOz84zKx363dIOTxCOktH4/dnPzg8PDo9/0lj1Ui+pbxfzs1jO8Wuts1KTsxR3Uihrr6rtcvOa0RC2U1H22NM
+TVFNW9fP78tv2VlRPkxdV0JTxLrNdz9TQzwsvq3n21NKPT4+OWc+OV3EXT5KPUpR21vvyGs5t6i/y87FOsSyxb5fXULHXTGisrmpzd3I31tC1UxCVWc6SUQ7X+Nfb8nN//9XPkpVSUpLvtfI1UtGPUEsvqzO0VdLNkNIPk5OOUlP3dtZRUFba1n3yff/RZ+6
+wnfZTFO6xb/H6z+5VTWlrqmu08fJvU5N59tI/19bT0VITWdVa9FrW19NPkE+RT1XyWPrX0w+PUIotqzD50ZZOUI3O11vO0r/705TY9XRze/Lyc/dOJ+uwFX/zc/DycXZ513KYzKjo6zfwt9VwEZOUW8+U13vOE9LSu//Y8vX21NRSj5BQEXNxW9310dERUUl
+QKy9U0d3NkA4M0FORz5Z91U5QO/Tys7Tws3ZOJ+oumPbv7/Nv8HIyVHVRT2fpLjruudXwVdGS1NDQWNrNTk6XU1r/8vR3VVrVzg9PEfO01t33f9ERUYoya69a0ZjP1U6N0lPRD7Vz2dPREnLzr3Hus33N6apulvvu87/193F11VLPMmfsr13y19r42tLbz47
+QO8/Qz09R1NZV3fX40H/TkE+Ql3R213j3cxdZz0rq7G702P/QeM/SFVdR013zV/dSlPnY9HIvsn3QFOjv81Tx9NHU1nA0V83NVufwL7Pyf9d6+d3TzpEWU4/WT1R/1tV383j42PJ/0hFT1vGz/fv38/d/zQprqnAa1f/U28+S1FNR1tRa033S1ffU0rFusnZ
+SUyft8Jnwb5ISL/bd/8vUT2osr1f103OXd/bRjlPV0hRRTpLX1tPU7zZ0+PM3VlFUc2933fny8vdWTUrqre1W1VbR2NAPEtIS1FMTVFRRUxdUUnjur1v7zpfo7vZxr9AW+9NR1vPL2OqucVvy0/Rd9HX6z9nWVVRTkFC71NTXcnGytXD0/dT27nL2eNn2Xdr
+Ri86q8LDd29HTmtESU1BP0c9PUY+SFVrTktnzb7v3ec4obe+12PTRVNFT7BCJrWru8/Fz1W/ztXB70FR3U1Ob1FE41NVX9/KwsLB09l3zsHbd2/V90jvQi0sqc3b5+dAWd9XXUFPPk8/Pz89R0NdT1lZWce/2f8toa+4Rc5ZN0hNuucwJ6mqwcPrw/+9ze+8
+7z9J11dPY09B111r79nEwLfEys/MvcfvZ+vHQEzbPC42p8vnzt87RXdOWUM/O1E/PDg7OkJNSlFZSMC7x0wroqK7d2PrN1m7RU41LLSsvM3bym/H09vD2WdG2VVLV1FP11/Vzc7PvbbB2869vN/n291jQlnbTixXqblb2V9LS2tdW0I/PlM9ODg5O09OSlFX
+RL68zj4quKG1RkzrOL87RV8vJqqtxcnP2ffP292+yFdf21VJQ2NVZ1vXx8jBuLi8ysm0um/B2etfTktjSinKpbtv4+tBX+dTT047Pko7MzU2N1s/Qk1nW9e450kzd5/F50b3x10xR0o4Jaitv8PP23e/29W8v2tj/1c/SE1fa+fZy8q+t7e/w72xvdvN291T
+S0rrRCixp7/b59lHV+dEa0k2PUs6Li4zOkk8RU1ZVeu920M51Z+33UTFsS48XUM1Kqevv7nP2ePNzsjJvt9ZX01FPD5jVU3TysnDvLfEzbmyvU93yv9KSk5XPif3qr3X79trU093z04+QU48NDA1PD49QWfN2f/PvUw8z5+400O3xi49O0QsL62zvr3XW8/v
+y8K/vtfnV0NJOjxfW0LZxLu5v7W8yriwwV/ny1dASUNLNSq8sMV3zedO30zvzuM/QkVJMTM5PkQ/S//Ly87Zz9Hf95+vyVe/Uy86OzwoS6230b69TWvjXc7Bze9nPkM9M0VZU0jVyL+7ube8trSzwOvN119PSz5FNyqrs73nxN1Pa+/XztFAQENAMjQ6P0Y+
+S9vLytHX5923PrCpxNnHRy05OS8nL6zG18W7T+dn0cbHx+P3O0A0O0BrUVPjxsa7vbe7uK/Mw9fVb09TR0A/Mi6ossHvu81Ha+PK2dlPP0E7Ojg5Q0lDTM/EzsvVX8euO66qwbY7QjE9Ni0p3bTf58K6SGdVxsRjyMjnNjM6PUBvRFfbxr/Bt7fCr7PCy7/P
+WUlCQFE3MTqqtd/IudNT/+vD11VvSz42OzQ8Rk9AW9fHxcvHV8u5TLahs688MDU0NC0p37RdTse1SlFZyb/vXbpvPS89PD1TRUPnwb7AvLu8rbe+ysrHWUI/QUs5Mmuls1+9vM/r482/xVdB3UMyMTk7Qk1GTtvOxsvJd7zNRz2fuatFLzMxMisrsblPTL66
+71trx8vOWcPIRiw7OkQ+TUJvu8C+wLm3rrnKusrvb047PUQ9LTqjtmfZtd/v/8u/2c9RX0Y2Lz86RkZJTWvRzsvJU7HfYzWlr6tIMjkuLywrrrNdRM6350xn69lr79F3RSstPUBDTkJXu7vGurqyqre8v7XR/0pHP0lFMz6jtM7Rtc7Z68LDy1vbT0kwLzo6
+P0dCRHfvX9PTZ7fbTzWrpaZDMDYxLSsqvrFrR/+0zFlX1cpjTb7nUzAuPkJLSUh3vLrFubq1rrW8w7/GT01IOz8/LeensN3dvMvR99m+yU1r3UEzMjs9QUtESs3Zb9/F0bfR/1UxoZ9bNzU4KioqrrZdQFHCw+tM0cjfSdPHQTYuPT1HREvfxr7BvryvtLG5
+wMnV3UhEOzo7L6qrutfvxP/LY2vAzFNn90UwNjk7P0lCWc/rZ9/Rv7PJz28vtJ/MRzc5KSc2rb53S1fOvNFXz8nRXdfO/zszPEhGQk1v08TCwL6vt7m1wsrZWUg/OzM0LrixvuP/zt/ZY//Cyktdb0IyNz09SEpGb9Hj/8zXyrHFxMk/Tp+u2zw5LyI+rd1f
+UVN3xs1Vzc3RY/dVXzw6NzpGO0d348bJv7+ttLa3wc3vW0s+PzQtNq2xznfR98nn/8/Bymv/Wz02Nz49OktL/9/v3cbVybPCxc9IPp+uxj84MCDCt99bTV1Iw8z30c7n9+tKWUdCOjdLPkXv38/Bu7qstrK1v8n/Vz8+PDAlPqvA993ZUb//X8/Iz03vTzs5
+Oz4/OUhKd9ff0cPNuri7vstZa+Oou1U3NCOyt2tbT/9HyNPT29ddd29LSExIOjo/PUbd9+u/vryturK1v83/V0I6Oi8mvq/nd9vdUch3Z8vK3U7dVz08Nz9FPj3n2d/b08nbs7y6vdHXvz2nvk89Lz28w0lRWVlP69fJ69NZa1dEPl09PEE/OGvb2+e9v7+r
+tbO1vsx3XVc8OjMqrrjRV9dbTsvr79vZ60pbRjs5MzlAQDpFz2/v79Hfs7a9v9nbuy+ksWs1L0q51V9N91n3682+2etb/1dHP2NIPjw9N07r12PEx7+usri3vclvX1E5Ni0rrbXn59fbUcnZ3c/X52f3RkM7OzY8OztCze/f1+vVuLa7x93Fvyuyrd0uK8G+
+VUROZ1NRX9G+31db905BPUxbRTs6REl3zf/Oz7qvrrS0ub3vV2NDOyw9r7j//9HZXd3Vzs3b5/f3RERDODw4OjdN/2vv3V9jubW3vOPIvC7Lqdk/Ka7MT0RIa1lTTcu9xmNv71NDPkNVVzc3O1dZ1d/V2bi0rrK3t73XV2NINi13uMnvV+/v4+PVw8vXb+Nr
+R0lIPkQ+OjhK/+fZ391vuba0uXfEuD0rpMc+K63OPEpBXUt3PtHEyFNTY0lEPT5ZXTcyOEzj48nfzbu1rq+4trrKW29TNC7ZvWNb9+933d/nvc/Z7+P/RUxETEQ9PDU7Z9HjyuPfvLq0umPLrkMnq6s6OK6+MEdTQFtfQVfFxVlNd0pGRDxnU1E1Okf319XT
+07i8rq63ubfEVWNLMCmywUFE19tTxl/XvMd3d+PXS0pJVUk6PTs+TdXLzczrtrywuf9rrkouKadFzbfONDVKPkpPPVvX2d9OVz9DPj9Mb0Y/OkhP0cnI1bm4r6qxuLK6z2tXOje2wj9FY29V2VnbxMROW2NnRUBDTkc4Nz09RVPXy8/nubyuvcvRr0tFLsy7
+rrrfSDg+P1lTRM5f3+93W0VFRURJW0M9OUZRW87I08C+uK21vbe+z29fNPfBzTtCW09n09/MxMtjWff3TkFHX0Y2MjtESkhn09HZwcCzts7Is3dHOC+qpsZXV0g2Pl9Ta8xRXc3Pa1FVSFdTTUw9P05dV2PDwcLGt6+0vL3Lz2s/M7ndRDtBSUJHX9Xjz///
+XWvvV0tJ/0s6NDlRWU7v38u/vb+2ssXGre9VPymmp85IXVMwO0lNztNfTtPZb01LS1NTQFlEPU9jXVnjvbrGtrGutsLHyuM0y8lZOjlAR05KQde/21vfV19rV05VZ103NjhFXVv3zv+/t8G5tb7Lr/dnRCmuo8hPTmc1OEg8yr5rPtXO1UpMRFVMPD5FPUBO
+T0/b29O9ubmzssfRxdc1sbtGOzs/R2dVT2O9zXfXZ+PTXVtVZ1c3NTlEU1fdyNHrsrK+urzXs9tPPj+/pcJfSVVCNz1MTOvdP1/vzE9DQltLRjpMRT5NWf/fx9fBtby0sc3Xx2NKr8U8OkU+R2NnXdnT48HnUWfXX1lRa0s9NDhDVWfrw8nbv766uLr3tc1K
+ObtCpMDrSElbN0VZV1df20Jf0fc/QklJPTlPRj9DXefjzcHfxbC2tsbXyEdMrN07NEk/RVtd99vM52/EV+vfXW9rVV1CPT5Ka1fLv8TZuMnEr713ssE+P7REu7XnV0RNMkJVSlFLPVFn3V0/R0lEQDr/S09A39PRzrnMyrmuucbZ00e1tlc8OUBEQkxX1d/L
+2WPPy+9bVXdfSkpGQT9D61HGyL/dtM3XvrNXwsc4yaw9b67DVVNHNkZVSF33PD3P210/TElKQD5ZY1NGb9HTy77Dy768r8z360+oxUE+O0JISz7j1f/rzV1Pd8dPUWNvTEs/RT1DVWvXx87RvMHZyb7Gy80zra89K6i2409BPUxOTkfXTDdGwONDRk5XRT9V
+63dT3d/L073Eyr6/t7v/TMWqz0c+QDxHSTxdzF1j091NWdXfV0tVQUg+RD1HV3fZ48fGy77LzM7R57k3tLZAJ++syu9JQD9fU0bbZ0I/3czVP05LW0dL49dv59Hvx7vCzL+/vr3ETb2rvkZBPTxATDhFb99V/9dLY2NjWWtdQ0lNSURFV+vd3dG81bvE08zI
+Wbv3sLtEOiuovr9GR0w/U01ja1NBV+/JRElFTEpEa93n2dvn777C08bBwLrTSrKowkdGQTo+SjtHd1dr5+dOW99jU1tOU0dNTUJLVXfV38rFw8S+09HA7//MsLz3Pyqssb5NR1lAPmdPX1lFTl3RTT5PRE5IS87f29vf58u+zNPAwb3JR7euv1NISzZBSD5L
+XUlO22dRY9lrXU9KRlNJR0hKU1vn283Aw8W779HDyUCrssnbSS09q7pbSWNCPuNdTGdOR13VVzpLSUxCT9Pn3evZ6+PFvXfXwMPRT6q75/dJQTg8P0JRXUVf1W93W9fn609KSUxMT0dXTVl3683Ixs7A2dfIzUGuodNTZzMuqrfPZ2dIP11vT99vS/fP50NK
+QUpMSe/N32vn22/Xv9FvyMrXWam7W05bQzY5OkJJSkBrd2df7+fX11U/V09IZ19nTufZ0c2+vsPFxMrLz0GqoMdHT0syqrvFWVU4Nk9XSFdvPnfj40BZTD1TVVnTznf/3ePNy8tnxsb/b6e90VdKUTo7NUZIRjlZ41Fb91vj3VNERm9FP+vvU03X18jBvLvT
+u8HFz0PDorVTV0guqa2+119MM1lrR013PkNrY0Y/Wz48T1//5+9f3V/d0c5rzchP96ax59tTQ0JAOztVTEBJ0+9M3/ff2+tKTlP/SkzJW1Nf187Hx77V477Pbz/AqLFOY0ko2am9d+9GOEhnTUjvV0Fnb2NKSlNFSPfXyd//29//zsdd3cdVW6etz+/bSDpG
+TjlETT1K4/9jWd3d32NHVUxVU09XX2tZ/9vLxcDG68LM7z69rqvHRU8yQaq3yVk8OjtVS1lIX0pXX2trSE1RU03du9dv98vf1cvH38NfR6atwtN3V0M+PE5FRTpK72djZ1vT03dLXW9OSWNnUUfdWf/nxcbK98LDbzy3rr6xSzs5Na27tVM6Nz5MSllPTFNN
+X13dUU1N12dV1bjnVd3F187FxdHPS6arssPbT1lMPTzvQjs92/93UVlr42dLSFtLSE/jV0ZF519nd8fR2e/CXznnrb7Dvz0yNqy3yds5MjlRTl9RT01LWVnbY1lf59VZzs/H32/LzdfCxdHj96ats7/LRUlrQj9BSEE6Vef3Y1dv9+NjVV1KU1NdX01LQ3fV
+V9vX39f/YzO9r8/buEItNq61x0Y9OjlMVW9nVVlXZ9nTb1/R593rwsXV783Vz92/vs9TvqiuuL7dT0hKR0dANzxHRmtnb1tfd/9Pa1NRU3dXUWtfSUpfzWPX3cfrZzqysc/rvVkvLa6yxE02NEFETl1rV0pNSmPPWWPn3WdbvcHbX9/L3+fBxMZNraiztrzK
+XVFGQVdBNDY9VVNTU1tVZ1VHX1VKTfddTFvjTUtr39vTz8rrW+ett8fRt3c1NquxtU44MzhLVVt3VUdMR2PZXVt361VP18T/V1Nj9+vVz91Xr6y9u7bMXUpGR09JOTk8S2fdV+9r72dVV2djb+f/W2/341Xnb13jxc/jSL+vudvjuMU0NauzwFM4NzQ+TG9v
+UUNKR13v62fj91NjzdHR51tX79vK1W9dq63CyrjCb19EU04/OzlBQ1Xv9/93b1tdWVtX1d93a9nfXWPr/1X/1c7vRLKwu+djy783PquyxkQ6ODU5RUx3TD5AR1n/d1nV/1tnzNv/92NZW/fO1/fjqa+5ybe+0+tHX2tGPD5JRVfv61fNX2Nfd/9R69Fr7+vZ
+WU9n/05d78xJS7i2x+9Z9749Q6+zyj84Nzc1PUPnQkY6TVd37//T2W/nysn3Z2NrXePVy0OurLS/wLfBzeNG//9COz1ESkzvZ+dn51f3Z+tZ3dPr4+PT/2dTY1lV6/9Vd6q+1edrU75FN6+03Uc7NTQ3O0ZOTjg9RlFf/+vb4+Pf1cb/Z1FR7+/XXU6srLfD
+wre6xN9Vd29DP0Y/Q1n/413ZZ1nv9+9f29/r0dXT51tdVVdZ42c7/63F3+trS8TZW6633UM7NTE0O05PQzg5Pedf//fN59XVzsjvX1lMY9XHVU2pq8S8xLq5vl9V71tAQkJAPUT/0XdjTV9b02vv29f3ys/M1WddVVdO22c9vrHBWdX3R13K0a2150JCPTI2
+O0hjRTU1RFHbY+PO2d/Nys5vZ1NIX2fXSuOtsMbOyby7v9VT30xFQ0VDO0NX09lnR1H/4+/309Hb08nF0Xf3WVFTX2dEs7vE///Xa1HMs624Z0RBPzY0PkVZSzU1QWdrXc7T09fOwc7rY1FLVf9vP/etuM/VxMO6v99rY0tFRUFBPD1X791dSk5R7/fnytvX
+ytO/zev/W1lXW1FRr7bN/2PfTl/Hqa3AWUdDQjk3OkpPQDU7QGNn793H187Lycn/X05NX/dnQr+yv8rfysPAvO/ZX0pFSkI+PkFT499NSFdX/1v/38Xby8rGxdt3W2PvVUjEscLNZ1tPWUCxpa/NRUtFQzs4QU1OQTE6SFldWdPVytPLxdf3X05Td99OVbO9
+zsXdyMfByc3Pb0lCV0tDP0pX3+NIQ07vX19n487L1cfJy9t3U13/SkayuP/f51NLTTvFpLHjSkhLRT87R1VNPzM5Q/9nd+vXyMjJw9n/71td9+9X67LD3cvOzsbH2c7E70RCTElDP0JZ4/9NQEtr/2f/59fXyMbKzs/rY2dvSlG3uF3j32dHTTvIo6/PRkVE
+PkY9R1VLPTU3Q1fvb/fX1cu9yd1360//52tPvLS+/9HPzMHH19/VxkZIR00/SkVZ9+NOP0hbW/fn59nd47vL0dXnY91nQVOxvU5T3U9NTzm+pLDBRkVAQj5LRU1HPDY3RE9n6+PXzs7Iu+fr62NvzWtVr7bIW8/V2b/I293bb2dNRUZGQkxXW11LREFOTWPX
+09XZ18fGzszX92frS821y29R0UlLTjrBpa7bd0Y9QUFDTk9AODY6P1FTa9vVz8zDzdvLd2v/12/fsLjZd9vR3b/BzNPX/2tOU0RKR0rvY09LREdGT05v1dXf2crj077vd29dRbm80VdV3VdMRT27qq3RW0pPP0ZHV1lBOTY9QklZb+PVzMjCyev30V/v50zb
+rr9fW+vT2crNytHd/+tXRWdHT1f/WVNLS0tGV1330c3Mys/V2dfFZ/9KUbTAZ0tZX11NPjpfra3Ra0w7U0NFVU8/OzU8P0dd/93Xz7+9zNndd9nb50jEsLtZV9vTz8fL083Md+9vTUFVWVdrU0hXTEhDSWP/1dfOxtHb19Hr0dlFY7G950xfW1tjQDtPrq61
+b08+PEVPVV89Njw7PUZOY+/d58m60+fb51/X30e6tMfZV+vRz8nKzNPP3//vV0hCTttrT0ZPb0pDSV/d09fPyMzbzs/T3+tVb7S/X1lZV09fRD7vsreyd0xDPz9H/1M7Njg+PkRNa+/j4829zuvf52/3/121tsj/92/nzdPKy83f49v/X0tKUffZWURZVU9D
+SGPZ19nJys3OzdfR2/9G77HFW05vUUxjRkDjsb22z1NEQkZGT1c9Njw7RUJHWffd28vE09nn729vV0+vsd1vY2v/yt/Pys7V7/f/X0tLX+9XV01ZX0xMSF3d087Lx8/Px83Oz/dPzLHFX1ldZ1dPRUbbsL+/301EQ0hKUz49Ojs6PkVGT2Pn2c7H1+fn729v
+TtG1uNtn51PVzuPTx8vZ1Wtfb1tO9+tfQlH/XUpFTVPr483Jy8vTzsvNzddXuLvG711XU3dKTEvEsL7GyE9JSUlTTEA5OUE5Oz9GTU9v1crJ03df52tjS9+yx3ff207ny9PZys/Ty/9VWW9d7+NPSEdNb0xCR0/n59fJzMzP1c/O0d9nsrzPVeNbU2dLWU66
+r7zGzllHU1lfSkQ7Oz5EQUJETV932c3M12NXZ2dVR8q10U1J3U5n79XZ1dvVz19OUVfn5+9KSlFXUW9VSlXjz83HxsfE1cvTzP/fr7nbV1Vra1VBSv+2tLrPd1tBR3dbSj48Oz09Rk9HTl/XzcfKyu9X//9jS7u11V1OW2/nY93Z1efd1fdVR01n2VlAQU9P
+SERLU0lZ59PTzM7F08/R1XfArrvna2NX6/dMStWwsLbRd81MS93nSD4+PT5AQUtVTVHnzsnMz9VjXV1bTrm4yVtNV1td3+/b79/X0f//S0dr011HQFVTTElKRmdVa9fTy83JzMvX713Tr73vY19VWU5dR9W4sbfOa9n/S+fvSz4/P0FHS0hHb1ln18bIzt3/
+Z2tRU7W0yl9OU2djWd3NZ2vb1+f/SUZn61FJQklNTUhKQENn9/f3ysvJy87R90/jrrrVX2NfY1VIWWe4tbbJ52vZWW//SkdCPUNKVU9JT//f48vH09/vd3dTScKxxlNOT1//W1/T2VVj093rV0hv52dITkpHU01JRkZMb+d318nLy83X3U33r7POT19dW1lM
+S061tb6+2Vvv2193T0VJPUBIX1tLTmf/zcbMyd/n5+9fS8S0v11OUWP/b2//2+tn79HXWUtfZ2dIS0xGT0xMREdRUVXn29/Kz83V30rvr7fNV1dbV1NOVVm6sbrLzldJ2e9nb01GPD9NXWdXT29r78XG19Xj4+NdS7uyx+9ZV1v39+//d2dr29/TX1NfY19R
+T0hBWVFFRkdNV09r3c/dzs/Z70rbsrjfVV1bTVFPV2+9trrG93dM3+tXY1lJPDtjX1VbU+tn48zJzf/X299rU7Wzv2dVd1lr39/jb29b28t391tdV2NnX0o+SGNFQkpNW1Fnb9HK387Tb0VjsLr/VVNnTEZRV92/ucHO3VNP69VOXWdRPDld91VTWf/3483V
+y9lv0ddrY6+vwW9Vb3dn79Xf72dnY8jRd29vWU9f/04/P1tOQ0ZOV1lda//Jz93TZ0jPsb/jV05ZXUVJXe+7vMHZ2WNFd81RTXddPTtR711XU29r2dHZ/9Xd6+NnzrCzymtdd3fv79XZb19v99/M3//vX1Nd31lIQ09RTUpMW1VnX/frytnvb07Ftb/vU1dR
+TUdITv+9vL/OW/9PU+d3SllfQzxFa2NdW2dv2c7b92/X6+9X2a+2z+9j72/3287jd1nr59vjztv3V2dX72tPRU5RTE5MTFdjY2vf583d/0a6tsRdV1lTTUZOUf+9v7/PXU1ZU9tbV01XRT9IX29fa2dn49Hd////42dn1ay61f//6+9n58nda1nj29nd28zO
+/0/3Z2dZTE5OT09RTVVfZ2fb3d9vW1G7ts1XUVlPS0VKWd+9v8DZXU5OV3fnTldJR0VTX3d3//dv39Pd//f3Z2NnvLC60W/v599n48zTXVPn1evZ29ff92ddd1tfV1VRV1NVT11bXf/n091rS9u3v9VNTllRR0NIXdO/v83fV09PVVHZX0VHQUhZX29v/+/3
+29vr72/nZ1dTtq+702/f3df/2c/VW1vn19/f19XXa+/3WVtXW11VWVVRU19bW2vn39ljSFe1vV1XU1NRTEVLW86/wdfnWUtXXVlva1VAQktvZ//3d2vd2evna/djZ09bta++d+/f79nn2c/nZ1/j0+fb28vPd13f70pNY19VXVdOTV9ZUfd33etbSdO6xudJ
+V05RTUtRXcq+xNXrT1FXZ113Z1tGPVN3/+fva2vb0+drb11nU03Zt7rF32Pv79/n19djVe/j49nf1c/L22/va2dLd29vd1tPU1tfXW/d599XTL28yvdZS05RSkhVd8i/y+tVWUhTVVtv90hBREVn79vna2/XztlvXfddV0fZsr7T1+tj3+vn191TTtvj5+fb
+79HX1f9rW1lZTmP371lTV1//d/fn1edbWbS4zmtZX0pZSUxfzcXAxmtPUU5MWVdd50ZAQk5O69vrY2Pv09trW19nSETKscLv/+v3a+Pf1edbUdfb69/r2dvP2d/nW1lPY03d329ZV1dj6+/n391La7S3zldnW1lKT05rycXCx29NV01bVVtX20w+QE5nU9PZ
+a1/v1dVrWVtbSz/Ls8DZW+9f71vd2+NRU+PX4+Pv59Pb1d/bb0xOX2Nn0d9jV1ln3ePf5+tOZ7a4yl9RW09RQ1F3xcfCzG9PU09XX1dX629DPk9Xa2/N919v1dPfX1dbTELKtL3ZY1tjb3dr2+tNU+vb393n59nR29fb61NKXVVnd9N3U09v49/Z4+9KTri4
+ze9VTFlTS0nfxcXIze9VT1NXWV1P7+tLQklVX+PVzv/33czZ/11fT0DCtb7ba19T/1/3a/dLS2Pf39/369nj2+fb92tRTWNX39vfZ1Nb3dXV3/9RUbW6xvdbTklVRVFXxsPHz/9VTVNdWVNTU+9NSEtOa2vO19/n59XT3WtdT0bVtMHX729fa+9r21tMT3fv
+39nr/9nZ69/r72tZTElVXddva1VZY93N63dLWbS4zu9ZVU5LTUvjzMTGzmdVVVVja1lRV2dbTExRU2frzOvf39fd3etdS0bHtsPV43dr/3fnd29KT+ff693X493T2+PZ/+9jV01OY/fnW1VTZ2/b2V1Ha7m73VtbUUtLSEpdxcvL02tNT2NvZ2NbU11jXVlV
+WWfZ39Pj28/P49lrTEfBtsfX9+9n//f3/1lVS2/d2//Z0d/b193j9//3XU5RV/dna1NVd//d02tIZ7m5311XXUtJR0ldycDT3f9ZR13/W19XV1FdY19VUWfr2+vR2dvO1+t3XUjGtLzX6//v7+fr61VKUVXn2et319vf39/Ta/f/Y09LU2t3WWtfX/fj319K
+S7i432NfVU1MSVH/zcfP72NbS1n//19RXVNf73djU1/v3//Z09vZ0+djW0nDtb7X/3dn4/fv709HSGN37+/369nX69/R63fvX1VVV2trVWf/Y2/V3V1RUbi8y29fVU5NSk5ryM7N419NT1tb92tVV1df599dWV9r72//ztnd5+N3TUjVtb/X52dv5+/n91tM
+R/f/513f49/Z3d/X22vvWV1ZY1tnY0/da3fv32NHa7a5z/dbW1NTU0z/xdX/21tJTl1Z6+9fW1dr49dVW2tv9+vr2c3d4+93UUjMtsXb//93d+P3Z1VJP13vd2//2ePfb9fb6+///1FnY2//b1dd43fvX2tMWby0xfdnWVlTU07jxtf/a11PTFVv//dXV1Nb
+99n/W29va2vr79PM33dbWUrTtrvP92tj//fvd2tLTGP3d2tZa9Xd7/fn9/ff72Nn62tZX1NM59/X/11NX762xnf/Z1lPTEvTw87ZXV9MTE1XXW93V2tnW9vnZ1nvb2/f5+/T091fWUrbt73Rb2d33dHR32NFS19b511dXd/j3d3f3evna1FZ92tZY19Va9Xr
+Z1FHSLq3y+drY19bTlfTxdVrWU1VT2dfXe9vV1lbY+/Tb2NjY//R1/fjy91rSETrtrvL5+d3b83X21dPUVljWXdj6+fn39vfd3fnY1132fdTT1VRWXfbd05GRsC1wd1jd1dNSkvjyc//71lPWUlZW3d3611R7+/T3+v/69/T01v/zc//V0lVuLvZ/2fv387R
+d1VTVVljV113a+f///fN29vbZ2Nj311TVWNfT2Pj/11JR8G3v/9TV1VLTVndydX3XVFJTUxdX1tjX2NXd+PR2133a9vN193V09X/TEZTubnA/1vd3cnP6/fvV05XTU9nb93R42/f2f9rb+/j/2tMXXdbW1/b21tKRWO8vtnn/1VPTk9rzNHZb01HQ0xXX2Nn
+XVdXX2v37+vf1+PV3+PnZ9ffZ0lIvLfBZ+/dysnZ5+9jTVtj/11Tb+vjb+vVzuNj99vjd11db91XWVtZX2NXS1u5vsprUUxfY1ddzsfZZ1VLRUtbV1tPSk5V79/v29v3a//ZztXrd+vrXUtEv7PE73fb49XR1dHrY1FbW01NXdH/693R13fj38/V719ZZ1lf
+Z/fv7+9VRUG7vchrU1lTX0xvy8PPb29FP0BLX19ZV29XW2Nf78/b7+vXzuf3Z/f/91s/TLvGb+vRzszK23fvX1VZa1dNW+/n78/O1+ff3efvZ2/vb9/nX19dZ+v/Skq8tsddVWtrXVdVzcbf/1lJREVOW0tHSl1RU1133dPX93fRxt3nb1Ndb1tGVbi8ymPb
+3c3Izu/n901bW1lLZ+/v2evnb+f/0c3d611dUe9vV/9dd3djS0zRucBbV2tZV1NnyLvD41tJRkNPT1NXUVtZVVvj2c/J92/nz+tra1nv629GQsG+y2vb1c/J2+f//1VZa0xJTF9r29fbz+P/793r9/9d/2f3Z29bXWtjY0pfvMrrV29v52djyL/Ha11MS0pL
+T1NfV2N3Y2/v18/R6+fLxs93XVlfX2dNRN29yvd368zEzXdfX05LTElPW2v/4+/r6//v3e/X3WtfX2ff2/9fVVFdX0trub/bY1tbZ133vr7JY1tXSklKU1dXU1VdVWfvzMfM09fRz89ra19bW3dTQeO7ws9r587I22Nd6+9OTU5MTFdr593f4+v/b+vd0edn
+W13nb3dbX/9vZ05Tw8DZa29bW2PXxb3M//9PSUJDSlNOT2dnb2fvz9nd0c3X0d1vd11db/9RRVu9xut308jE0Wv/71lITU1ZTlVr993Z2dPdY2/V72dfWffLzu9dU11bU1dTz8TOXV1jW+PVv8fM709MQUpEVU1RV0hTV/f30eP3ydfX383j91VNd09PRuO5
+vM3vytPP49/T611GUU1VWVvf69Pf2eNbWevJ3+t3b2vd129fV1tbVUxFzL7K91dZU2fnxMLB3VdVR0Q/SEtXWUlVb3f/29PRyM/d/933U01f72tdUV/ExM/b0dPb2+vd/2dbW1dVX05X/+fVzNX32dnX/13352vfyP9TU133Y1NHzsDNV0xZV2/XvLvGY01V
+QEJAQ05MSkxv6+t3389v39Hd/87N62dbU1dXS0nGu8rf59nTzdnX319NR1NVXVN30c/O3dnn693V02/v/2/TyNPrZ1tdTkNF/8DC31trW1Pvzr7H709OQj5FRk5PUUpZb1Nd49fj18nV3+vZZ09VY19TTELGtr7N19vj3Xf/61lTY/dvV05b6+fV0dPX/+fT
+z+PR19vZ09V3W1NnV1lRTb7Fd1NZXV/bzb7J61lPR0RCPURDREtf///n3dfvb9vj4+PdWVNPXXdfVUfGu8HnY+vj09vR2f9VT19VWVFd/93b1crL1d/V1+fd4+/bycvX529nUUpFVb/F/1FXWWPPvL3N/0lDQDw+Q0lMT19vX1Nj499vY9vT4+PZ729ZW1tV
+Rj/LuLzZ59fR3293//dbUVFZVVVf183b19XZ92/32c3NztHKw893Y2NbV0pGTcK/yu9nU1PbwsTNb1FNSkhDRkNFR1drXWNd9+Pr48rG099rX09TV19dU0trtr73Wevn7+drZ2drX2d3Z09RX2Nj49fV3+vVzs/X3dfLyM7jZ11ba11dT8e7zlVMTFPLvbzA
+1VlMQkJEQURJTExXX9/X2d139//Z2efj9//v62dVTkdPvL3NX/9fb9vZ2d/nT01JTUdNXWvd29Pdb1Nj3dPP993N08vO1efvW0xMPVG+wuNrZ2/Ixb7L2d1OSUJDP0dVZ+9dd2Nra19r79PNv8nf62NnZ19ZZ0tHw77H29vj9+9dZ19rTlVfa11OU1Ffb2/n
+d1tR/9vX08i/ztPjZ19VT1FVSPe5v9NNUVvbxL3I1+NLT09OSUtXTk5MX2/n293P09ndz8fn5//fZ1NbX2dT07zE91dTTf9r39vba05RTEpGS1Vb91lfX1v/2dXn2dfP03fd3dXv/1tNREO/v85bW19vxL+9v85ZRkxAQUNMX1vr79Xfa2tv1+PVzMbb99nZ
+1ef/Z1dHS8HJ2+/3d9/Z/+tbXVdRU0hERllfd+drZ09RXWdf68rFxtvT1edZUVFNRkHJvM9db/fbxcXBx+9NRE1PU09nb1VbVd3jY2fn19/Ty7y+09nj62NZV29dU729x2tTU13bd/93a1dOWVFVS01XU05KT09na2fXzsjH0Xfb2/9vVU1NS0zZuc1jU05b
+48i+v9f3WUpMRUpTV1NNXWv3Xf/Pz9fby8nK3+fZ2d3n29tdT8O6yF1PVXfn49/R0f9dV0xDQUpbXVtV/3dZUVn318/O03fn993f/1tLTUJPy8zfU1dd3cK8ydP3SkdIS0pMT11jb+Pnb2t3a//bysTCx9HX09nn/29VT9++zGtjd9nR0ev/b2NXT05RV1Vf
+V1VTXV9VU1l3683Hz9HN1+d3UUpITEhOwLvVV09Md8S9yNdfR0dLVVFdX11LSUtVXW/j69vRy8bFzdfV2d3nZ1tjU9G3wdtjWV//69fR09l3W09VTllbU05TY1dRW+vTzMrL1Xf/d//fd1dbWURI177TT0hJb8vEyc7bW09OSkJETVFTT1f/d19nb+fZzcrL
+zdvj087T3+frUWPBzetr59vZ08/V199fWVdVSk1RU1tj3+tfV1tr99/Zy87R2e/jd2NTU0dH78DPTklN58G9w9d3T0VGSVlbXVFMSkxfV11ZW2v3z83MzcXL0dNvW1ddTHe7vc5v7+vn18/O1+dZWWNbV1fvd2dfV1lPUWfVz9XMzM3X62/d42NTTEhFZ73H
+70lITmfP1c/R909HSFFZTk5MR0xOW1dZW+/NyMXOysvf39vT393/V1vNvdX3Y2dn2dHX1+v/b29dTkpMT05b/2tvXW/r3ePRy9XX6/fR0+tnd2tRXcLHY0ZMWePKyMfL3U5FS1tOTExJSElRY2NRV2d32+fjx8DN59vb3f9fV0t3wsvba11v18vO193/VVNT
+V1dfX2NnY/9rXWtrZ+vO1dPd49/VzeNbTlVIUc3A41NPTnfNx8XK31dKVW9bVVNTU09VTUxLV+vd09nTzcbJ3+Pj32ddU0hbv73X/1tnd//v99Xjb1ldVU5jZ29TW2djXVtra9PNy83d71/v19fnZ2tOSW/Az3dTSFX3ysbI22dVb+dvU0hFS01ZWVdPXff/
+39nMyszR5+vj3/f/a1NRz73jU1dv39/v69f3X11rX09TW2NPU29362///9/d99nb19nX1dffY/9jSVfHyvdPQU7vyL7Dy11RUVVfUVtPUUtOUUpdX/dvb+fXzN3Oz9XM1d1jW0RM2brDb29nd2dr19HbXV9fXVVV9293b+dnW09T693X3dPX7+/d08/Z929Z
+QkLOv9FZR0tn1cvL1f9rZ3dfSkpNU09PUU9dW11ba/fNxc3M1ePn2+Pn51dPa77G0etv/2Nv39/rZ13/529vd1lVX3f/d19f/2dr59fN09fd29vfd2dnRkn3v9NJQET/zcTG1/ddV2NrVVFRTEpGSk1fa2tr9+fj3+PTzNHV299rZ1tZZ7+7xOtVV1X/0dPd
+229jb11v/2f/43ddX1V343d349vd3+Pd2dHM1e9VQj9NxcjrSEdVd8jJz9PvX1tXS09JSU1MU19nV1tVY+fd193f19Pd39Pd32tTSv+6wuNva1/r2+vn6+/r3/dnY2fr6//v72djZ1ljb9/Rz9Hd39vX32dTTUVIzsdnR0dR28vN1d9bV1NXX1VNSktITlt3
+d19dZ/d359XTycnT3d/b9+tnWf+7vM5nTmN36+fv6///Y9/j/93n711bV2tvX29r9+PV1dfZ1dHP229VTENHY8HNTkNGZ9vMy9vnV1FTW0tJTEZOSU9dXVtVd/fn5+/f69fX19fP0d/3XWfDusDjY19n9+/33c/R1dv3Z+/na3f/d/djXV9jY+/X1dfv69nb
+5+f3WUk9QdfRV0dKY9nNzdf/X1FbY1FHSEdGS1vd2XdjV1dZa+/b1d3Vz9fV0dnfa1/TvcP/WVv/59/r6+dvb+vj9/fjd2tnb+tjXVdnZ3f369fZzsvN6+9dTUtARu/F21NMXevPyt3jZ1FRV1FJVVlXT1NnY1tdWV9nX//f5+vb2dPV29/rXVdfxbvN/1dV
+UVtd/9fV6+vd3d//b2N3d/9nV1dZ493Lztfb1d3n3evnY1FGS2vNzVlVV3fXz913X1///1dISEpHTFf/529fW2NbXXfn6//n2dnn38/P1f9f1cPRW1lRV1Vr793d///r33dnX1NZY/f3/2tv93fv69/O09fZ1d/f/1FMSlPOyGNTUefKycrbd1dXU1FRT09P
+WWfn3fdnV1VXWWdja2/rzMzP0dnn/1NH3b+/409KSUxVb+/n/2v/b1tbb2v3719TTU1V793Xz8rBy83N19vj905EP0PVub7v79HP1dX3X1lNVVNTT1NMTFn/1+9VR0ZJWffj19vn29Xd187N0WtNVcG801lERElTW///b//O0W9bSklKT1VZVVFj987R283K
+ztfR4+Pvb29rY1XbvcH/b9XRyMvV5/9bW1tRUUxMSk3/5+9r/1lPUUlPVW/3187X19HBzltHROPB11FEP0pZ08fb/1//Y1lRUV9RWWvn71tf59Pd49PR0dvRysfb/19PTENRx7vT3dnRx83b/1dNTU1PT0pVa+fO0f9RSkNGVVFja+fXzsfX3dPH41FHPF/D
+w91bSUFPW+NjX//v519jV2tZWW9v91Vj99nZzsDCwNfX2f9ja+drW0xPx77K4+Pd2eNfV0ZJT1v3/1tNTUpf/2P/VUlNY19j7+/Ry8rva9PKy+NvT2PLyO9GRUFV19n/a93372NRUVFMR1FbY2/j0cvN59nf3///ztnj79ffa2tP98DC3+/X49fd52tdVU9d
+UU1JTV/j4+/jVUpMVVdbW1fd0crN08jAw+9PQT9rxc3vVUdd3edv9+9va01MT01XXf/j32trX2Pv993X19/v0d//a93na1dHTdnD0c7N09f3W0xHSl9v92NPVe/v39nvVUtLUW9v/9/GwMLJ09fRzN1rV0tjwcDXV0tRa1tXX19fXVtfX09VT0tPV19rY2vv
+3+Pf5+/v4+NbX+/j7/ddT9/DyNHfY/fnX1tPXWPv52tXUW9db93/W1dba9/n7+fXztHOzszJxcvna09dzsbTXU1NW13v9//na2NPSkRNTUtj99//Y2PX72tjXWNjb/fvb//T0+dLQE3X0dPX393nXVdVVVtj/2NKTm9v59fd32tRW2tr/+/TxsbExsjGwM7X
+/01Pb8PG129jY2vna1tdUU9bV09VV01b/+djT1Fja2NbW2trb+dra2Nr711NRU33zMvX19//W1FIS2dv3/9dZ/fv3dnd72NRY9nT3dPLx8bEx9PPz87M62NRXcu/yPdbRU5bV1drW1FjVVtXUUtOXW9vY2/r6+v/Z2tnZ29jXWvXzt1XU0pK18nT12tVY1tN
+UV3/52NRVU5OXWfd2WNXZ9nX19/X19fMx8fMzMTDzN9nV93Jyc/3XVdnb2trW2NjUUpJSk5RW+/3Y2dr929LSVNja+9v/+/fztX3U0Y/Y8bDxtfr92NXVV1jY1dnZ1NdZ2vf1/dra2Pr6+vd39PKx87Mz8zK2e9dW1vVv8bXVWNbY1tOU1ddW29VU1VXb+9r
+W11b9+tjXV1ja+vn92/32ef3Y01DSe+/vc/rV0xLS1PvY11jV05bXVfv/2tdZ+/d2d/Z3c7Mz9PZ2c7FydPrY13Zvb7NXVNTV2dbb2tv9+9vTk5OV2f37///729jZ2djY2tra//f3efnZ05GQ1HOxdPfXVFNS1FXV2P/TkdJTFNr//dvZ2/V2d3/Z93MzM7O
+08rKycjV71fvwL3J52Nbb2dr//dja/djWVlda13//2/v3/9XXVlnZ2v//+fn2+fnWUdBPUvNv8XdW0hOTlFMSFlbU0lISVlra99nWVvr1dvf/9PPz8/TzMnHz8jN62//yL3C1fdrW2dTb//32e//V1ldb29n31vr529nZ2tn713r3+vrb+vv909JPT/fyL/P
+Z0xGT0xJRlNbV0xER0tRWWtrWW/v2d1r/93R0dPf08rPysvM1d/nzL3DzO9va2/39+v3393r62tvWWP37//r3f/va29vXVdd59nda2vva1tLQD5M28bL71NKV1dKRk9jU0o+P05TXWdrV2vr3dX3///b29vr28fMy8zIz9n368G+wd9ra1dv/+vf5/fr32dn
+T13r7+vv3f9rXWvvY11n3dnd7+fZ529KREJM2cbD1W9ZWV1FR0xZWUpCP0pMXV1nb2f39/dnb2/33/fn383N2dvPyNXfb9W+xttvb11vb2/n//f32+djXV3f//fr39v392/3b2P31cvf5+vf629PRUlK2cjG0f9jWV1FSU5OU0hFRU9OU1lZb2/39//39+vr
+3//329XO2dfXytXnb9m9yN9jVVNnb133b2/32/dZTl33///369//9/ff62/329Xf29vZ329XVU9N38rGzd/3b1NISVNjXU5FSUdHTVNd9/f39////29v92dv59vf69vPxs33X9/DyddnV11jb/f3//fn29lZTGdvb//f383jZ/f3b2/329fj29HV42dZV1NK
+VdfJz9nb91dTSEpVTUZHSEZHR1Nj9/f//2f3b2/3Y2Pr2dXR3+POzNfjb+PDws7nZ2///29v/+vb3/dVX/f/6//349v3//9n9//n2+fj29n3929dU0dGT9HLx8zVb09NTE9PS0tPT0dKS09nb+P/Z2Nv/+fjb9/f6+v35+PO0dHr99vOwdH3XW9nX2NZ99vV
+1edfWV9db2/319fn9/fn2ePj3+fb2+f3/2f/b11NT+fRztHb611TSE1TTExLS0VBQ0pZX29j9+P3//9vY2dv6+Pj69vKyc7n49vTyc7nZ2dvb+fj29vX511fY19jZ2Pjztvb9//n5/f392/b39vV5/9vXU1JSl3OzNHb4/dfTU1PTUtIRkY/QVP34/dfZ/dn
+Z/9v9/dv69fb29XOyd/36+ffzMvR1fdnX2Nv9+fb919jZ19jZ2Pj19/rb2f/5/ff39XR3+f3b//3Y1VOTWPOxMrV3+NZTk1ISk9ITVFIQkpRW11Vb/dfX2//6/dvb+vr69/V1efb1dHTzMjP62Nnb/fr99vR62/nb11dVWf36/ff5+vV1d33Y2/d3/f3b/f3
+Z1lbVVXdx8jb4/dnXVVZY2NJRklERElRZ19j/+Nv/29n92dbXW9v69/OytPb5/dvb9fN2/9jY/fn39vb52//X1VVT2/n18/P3W/j6+f//9vX3ePj6+PrZ19ZTUxj28rNzM3Xb05JTk9JR0xLREdVb2dn//f3X19fb2Nf9+PT293X39/j4+vnb/fKzd1vY2P/
+9/fnb19vZ29nX19v//fn6/fj3eP/39PO09////d3b2dZV1t3z8XKzNf/b1lRW1dOTVFMS0dOWVFZX29fZ1/32+t3Z29n/+vb2d3Z39vd/+vTzONnW1139+Pd2d//b11NTFVn/+vr493T0933693f3+tnZ//34+NvW1lXd9fNyc/r/19db19ZUU1KSEhMUVFf
+99vZ/2dvd2dnZ2//993X09PT2+v3W1vr0933Z3fb3d/3/29fXVlTS1Vf99/r39/d3/93/+Pr2dnf3+v3/3dVV11dZ//KwsbX42ddXVVVTkxMSk5XW1tnZ2d3W2dv/29vb//d49/f39vb4/93W2fry8rZb13///f3491vb19bWVldX/9n/+vf3f/32dHX2etn
+/2dv//9bWWdnb//KxMvVX1tZW1dbX1tfUVFKSFFZZ2f/X2/r69v3///d4//r69HV09vj62d399PjZ2dn63fj49frZ1tNUUhNU1tvb9fP0eP/4+vd9+v/9///3evjb3dnV1/XwsnR///j/3dZW01TTkxMSFdXX2dv/3f/Z3dfX2/34+vbzs7R43dfd19n69PX
+9+vr2evr2et3WVVRV1VX/3f//+Pd63f/4+Pr6+Pj6/f/9/9nW19fUV/OwsDP/2dnW1tVW1tXVVVPUVtbX1tXW2dnZ3d369nZ3f//493j4/dv/2dn68/T3fdvb2fr3ePrd29vb1dXW1tfd93Z3ffj2d3Z493db19v////d///W2fvzM3jb29rW19bX2tfX1dN
+TE5NU1FTb+Pv7+//9//v7//r2d3d3e/36/9rZ+PZ43dnb//V0dnjb1dTV05XW1tr993Z2d3d6///9+Pr/+/j493d63dXU1dr0crR3e9rZ3d3a19bV09KTVNXW1tb//9nW2N3/+/j3efr1dXV2ff//2dXX//Rzt3n//fj5+f/a19rW1NXU2t39+vn7/f/Z///
+99nZ5/fvd///a29dW1df38zM1edjX11fY2NvZ19fY1dXV01PZ2t3/3f/4+PZ1d3b29/r6//36/93d//VzudvW2fv39/3/2tnW11ZVVdXW2t399/r/+/369//a3f37+Pj6/9nZ11d69XR2/9nXWv//+/n/2ddU01NT1Fda2v///fr4+Pj5//n49/d493b3/9n
+XVvn1dfrb3f339vj7/dnV1VPVVtjZ//r493/Z11jb+/r9/f359/f53dnW01NV+PO0d//b293/3dvZ1tnX1VTU1dfd////2v/69/b39vZ1dXX3ePn/3dvb2vf09Prd///7/f/b3dfW19XW19vb2dfa/9ja2dn//f/7+Pr4+/v/2tnW1dRVevKyd1vV11nd+vr
+92dvY11ZT1dZW193/+/j49nV29vd5+fn693b5/d3a2/v1dXvb2Nn9+vr5/f/d2tbWVVba2P/6/d3d2d36////3d36+Pn6///Y1lNTFn/1+f/Z19f/+vv/3drW1FMTFNbX3f359/d29vr7+Pb2dvd3dfX293vb2dr49fb73f/7+vj3/9rXVtfX19rZ19v6/fv
+d1lfa2t37/fn39vX3/drW1FMTFH32dv/b2Njd/9rX2NfWVVMTFVZXWNrZ//v3dnj39/d29fb19PZ29/n9+/r39PX1+f3/2vv39vr/29rZ2N3a2NjY2tvY1tja2///+vn6/fv/+//Z1lRTVF319Pba1tfY/93d/9rX1lRTE1NUVtnd//r3+//59/f3//3393d
+29vf3+fj6+/j3+fv7//f29vj5+vv729vY19rd/f//2v/d29rZ//v39/j9/f/Z2NTTk5Z9+Pfd2dda2//73dfVVNMT1FRWVlfY+fb2+//7/fj5+/j29fX0dvf6/frd/fb29//Z2vr39vn//93//9vXWt3b+93Y2Nna3f//+Pf39/j29/ja11VU1ld/9vZ7/93
+X1lZ/2tnX1FOTU1NWVldZ2vrd293////6//f3+ff39vf2efnd2vn29Xrb2P/79/b9+fn39///293Z/9r/+9nd2dnb+ff3+tv7///Z1tVU11Va+vn72Pfb2NjY1VZXf/vZ11dV1dda+fn/11r59/f69/f59fR0dfZzs3R62tj/9PT2e9jXWt3b29jY2dZU1NT
+WVld//9na2v/d1tPW2dr/3f///fb3+9nT0lV2c7R3+//Z2dja29vZ29vY2dvd/fr93d3b////+/3593b39/n49vr/3drZ+vV1+tnb2tvZ19nZ2tnb2djZ2N3/3dvZ2f//3dra29v//f////v9/9vX1VX/9XR3f9nXVtbY/93a29nY2drZ3f/d2tr//fr9+/3
+493n5+/r59vd5+9va+/Z0+NrY2Nrb2tvd3d3/3dfY19rb3f//////29vd29vd3dv//f39/dvX1lVY+Pb3/dvZ11dY2trb/9rW11dY2///3dvd+vv6/9v9+fj4+Pr39/f2+P/Z2/f0d/3d2t3d2///3dv/29ra293b3f/d////2dva29vd//////v//9rW1lR
+Xe/Z2/dvW2NjY2Nba29nXVtbX3d39/9ra//v7/f/7+fn5+/j393f3+P//2/f1dnf/3dvb2N3/3fv//9va293/2/3a///d29vd2//b//3//d3////a11RUW/f193/X1tfX11bX29nX1dbXWdrb3drd//v93d39+vr6/fr3+Pj4+Pr9//j19fj/3d3d//////3
+9///d3drb//////3//93d3dva2//9+93d/93a19ZU1/v393/Z11fa11bX29nXVdXX2dvb3drb//37////+/v7//v3+fj5+ff7///39Xb93d3a3f///f////3d3dnb//////3/3dvd/93b3f/7/f//+//d2dZWV/v39vrd29rb1tZXWtrY1tXXV1na3d3d///
+/3d3d3f3///36+fv9+/f6/f/79fd63d3a2//d//////393drb///////7///d/93b//35/f///f/d2tbX19339/r/3dra19dY2NnX11dX2dna2d3d///d//////3///37+fv7+/j7/f/79fb729nZ293b///d//3/2tjZ///////9/93//f/d//37/f39+/3
+/29nZ19349/j7/93b19fZ2tvY11dX11jZ2v/////////d3f/d3f/9/f//+vf4/dv99/f73dra293///////372tjb3d3//f35/d3//93///37/f36+/3/2tra2Nr7+Pr7/f3b2dfX2djXV1fXV1dY2//////d/93d/9vb//v7+vv/+vn6/93/+Pd6/d3d/93
+/3f///f3/2tr////////9/93/3f////39//39///d29rY11n/+fj5+v/Z2NjY2djY2NnX19jZ3d3//93b3f///93//f//////+vr6////+vd4/9vd3dvb2v//+/v/3drb29vd//v7/f////3///3//f39///d3f/b2djd+vr6+//b2tjZ2tjY2NjXVtbY2tv
+d2////////93d////////+vn7///9+/n5/93d3f/////9/f/d293d3d3d//v9/////////////////f///9vZ2Njb+/r6/f///9rZ2trZ2NfX11da////3dv/3d3////////9/f/9+vn///////v6+/3//93d/////f/d3d3d3d3d//3////d///////9+/3
+//////93b2tnd+/n4/f///9nZ2Nja2Nna2NdX2dvb293/3d3//////////////f3///37+/v5+/3d3f/////9/f/////d3dvb/////////f39/93//////////93d29va//r5/f//3d3b293d2tfY19fY2t3d2///////3f//293/3f//+/r7///////9+/v
+/3d3//////f/////b29rd//37+////////////f///////93d29nZ2//6+/v7/f/b2drb2djZ2djY2///3f///93d3d3/3f///f//////////////+/v//93d///////d/93/3d3d///////////////9+/3////////d29vb//v5+vv9///b2tvb2tra2tn
+Y2tva293/3d3d/////93d3f////3//////////fv//9vd///////////d29nb3f///////f3//////////////////93b2///+/r9///d3d3d29ra2dnZ2dra3f/////////d3f///////f39+////93d//39////////////3d3d29nb3f/////////////
+/////////////29vd3f//+/n6/f//3d3b29va2tna293d3f/d/93d/////////////////////93d//36///d///////////d3dvb3d3////////////9/f//3f/d////3d3d3f//+/r7/d3d3d3b293d3dva2tra293d/93////////////////9/f3////
+////9/////////////93d2tva2tvd/////f3//////////////////////9vd//r7/f//////3d3a2tva2trb293d3f//////3d3///////39/f//3f//3f/9/f///////////93b29vb2///////////////////////////3d3d3dvd//r6+////93d293
+d3dvb29vd3d3d3d3//////////////////////////////f/////////////////b293d3f///////////////////////////93d//39/////93d3d3//93d29vb29vb2/////////////////////////////////////////3////d293b293d///////
+/////////////////////3dvd///9/f//////////3d3b29vb3d3d3f///93d////////////////////3d3///3////////////d/93d3d3/////////////////////////////3d3d3f///f///93d3d3/////3f/d3d3b293////////////////////
+///////////3//93d/////////93d3d3d3d3////////////////////////////d3f///////93/////////3d3b29vd3f///////////////////////////93d/////////////////93d293d///////////d/////////////////93d293////////
+////////d///d3d3d/////////////////////////////////////////////////93/3d3////////////////////////////////d3d3//////93d///////////d3d3d3d3//////////////////////////////////////////////////93d3f/
+////////////////////////////d3d3d/////////////////93d3d3d///////////////////////////////////////////////////////////////////////////////////////d3d3d///////////////////d3d3d3f/////////////////
+/////////////////////////////////////////////////////////////////////3d3d////////////////////3d3d3d3//////////////////////////////////////////////////////////////////////////////////////93d///
+/////////////////3d3////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////LnNuZAAAACAAAHDqAAAAAQAAH0AAAAABAAAA
+AAAAAADT087My8rM1W9369/j493b4+tvVVFVZ3djXUpVVVlfa2v/39/Z19vb3ePj5+/n3dvNxsLCwL+/wMXIyMzf93dnVVNVTktGQkNAPz9ERUlKV+/r7+/fzczLyMjAv7/Bw8TGycvOztXn/1dIQj89PT9DSU5TWV1j/+PbzsnLysjJxsPDwr/CxMPHy8/P
+19/n19XfX1lTTElKS0pJSkpNU1FbZ//r3dfT0dXV19/3Y+u/s7vFy9t358SysLnMa0U/S+/HydtOOTIxOERv901BOTQ3R93Au8TX91X3x7qurrW91V9j6761uMF3PzUzOU3f72tKPDk9Tc67u8TdTkZT1bmytL3Ob0xjzcG3u9VnQz5La8a9xt9KOTc7TNnE
+12dIOTg/a8C4uslnQ0NO17qztsLfZ1nTubCtssTdS0FP/8W+3Vc9MDA5R+fjVUI3Mjg/27u3vc13T1Xbvqumsr/rXVfvwbi5yF9HNzU9T//vSjs1LzhI97++1e9LRE3rvLCxt8brX+/Eta6wvttJPkR3v7m6zk05NThH3czNU0A9PTdZ9+fLXe/rY8XTvb/R
+003Z49Ovsa2z3ds+Tc3ArbnESzUwKj13zbp3RCwpLCtVysS5X2M7O+PZr6mvrtPdT1W4tq2vx9c2RENOw9vbUTk2KjlE373bzk88Y0PIv7y41873U7/CtbHCwU9VT1XFxL7XXXc6U3dfu+//SjM4NE3Oz8JPTDk1TOe4rrm9WVFDTMG4rqi2vv9NS//AtLS7
+40QuNTtDz8xnTjMwLTFBY9PJ499V793Gtre4usfByru8v71jd1dL2efOzU4/MTAwP3fby1lBPzI7S9uytrrMWUo+/7+vqKy250tLRdO6urHPbz46TlXVvtPRSDw1PU1Z2e9OSTlFSO+7yMfTY2d3xrq0ssDD/+vDxa+wubxMRDk+2efEyk45Ky0uPPdj9z84
+Ni5EZ8KqtbfK/+vrvq2tp7fEQklnVcG7x8A/OS4zSknn/09ONkA9XcbLwdln90nN0cCxysbbX8nZvbjDy0pKOEjOybW5z+c0OTdEv8u/3UQ1KjI767a9u2NNQjrbvLWntbfRW9vfvK+xrb/RPUBTRtnvU/c1OjQ2TD9KQTo/NUlbzrbDvtXXxcqwrrCvzNVM
+V8fKubvNTTYzLUDvd79ZRjQpMjZjssa63Ug9PGfPuKu6u19vXV23tLGu1Wc7RFfjvru+x0pKOE/X78ZXTkMzQDpM2WPvS0lNVc7HubfEyVPPvr2ssLG562tM37vBt8xrNy4zMU3NXWs1MCkoNDz3uMq/b1NP77+yq6iztm/Zzcu3u728T0g5QVVb3VVTPjM8
+OVfn/91GUU9Lxsy8uc3rRlHdzLextMHdWT1d09O3v8rjRkVD777HvN9OMjI4Om/L99k/PDQ4W9e9sr+763ffxLWwray9wl3Xx8+5xszNOj80O0tJUzw4MS46OlN3TndDWd/ZtbKwr77C6866ua+uu9NMRDdP093FW0g5LDEyQs3/11M8NDVHY7+xurxrU1VO
+w723rL3C92PO177ByMRNW1njwsq+71VON0VJTs9XTzcyODRJ3cm+19dCTefTtKyvrsDF/+O5tq6tvMZFRTpF2e93WTo1KC4vNU9ISkQ0Oj53w7qtr7vJ27+/sa6xr8jrVU7PzsDKXU0yNj0+9/fjRDo5MkBv973FxWtKW2/NtsC8zdFHU9nJt7S+wEpKP1O+
+u7e1zGs4QEFZzdXjVTUyLTxDTcnr/19Aa+/EubWuwcXIyLS2rrK7u/9f71/J3+dLNzUtLz0/Z0tBMy4yOFG+vrG8vs1nxLiuqa+vztVRX8HAwLlvVzM4NTlnd2PnOTYsND1Lycrr40JRTc28vrbB31tNzMi2sLi451ldTsLBurvP0UxJ32PI391XOjkyNE5L
+71dLOTVAVdm1u7fFzGPru7KtqK+z6+vv17q2wMZFPiswMjVNST4/LC4qNEZdwMvVz1vFwrOusq+2zMrZvLy3t83bRD5RTN/rb008OjU6U0rjY2NNOUdVz7q6t8/fRkH31cSzubrZb05Rz72+uL/MSEpra8G7y9VAPTU6RD5XWUE+Mjg/Z87LvcHV0+e+tLKt
+s7W5zsG+vbK1uetJNjI7P0pRPzwsLS4ySF932/9VPlnIu7CqrrLCx+fAuLe0t8fvP0lGXc/f91E5Mi8+P1FrSkM2ND1F08m/vdX/X1PRxLm/w8hvd87FuLe4zedjSffIwre9v18/OzxI1/f3S0AtLzU8V8vRyGdVRWu/ubSstLnLxr+4r66xtMjvRUtGSFdE
+OTQqLS82PERJQjs8PFPPurW2tb7JwcSxra+2wcz/Tdnby8fMX0I7OTx3Z29nSjwzNz1H1dvGx9lfY+vX2cLLx/fv79vJw8i6w8rT39PXysbZxef3/1dNRktIP0k7Pz9EPT5LUU7f68q9vL/CvLq/t7ezsbi3wMjM28tXTko9OTExNjM7NjdDP0BHTf9ny8rD
+ubq0tbe1vrW4xLzGwszd90z/W0dZSldDREk5Qjk4Rz1LSUlrTGfj3cDMydXVz//d29nAy72/wMPTy9fVv8i+zdfTd+tRTudVS0JAQTxBPjtXSUpTXePdxcG+t766u7e3urC5vLW9ucff70dJOzU8NzgvLzIwNjk7T0xda/fT18i7uK+zs7a7w8jAwL66ztNv
+V05JWU9P61FPRD4+PEFCS29VTVNf6+PJxc7E2+dv69/Xz8vIvMLEzs7V3czPz8LKzf9VS0VLREpXSkk/Ozk6QUVN09fKycvPxbq5uLK2tbq+xsW7u72+y2tHPzY2My42d1M9NzEsLjv/s7LKxmdNa7+rqKitum9Ob8W1r7jNXTsxO1vbxdNIQTAuNTtn3/9X
+OzI5QtO4sb/Kdzk/XcyxsrrNb0VE/7eurLfMTj5FZ7+ztcVVPSwuPUzOxVNEMCouSdG8tsTIV+vbvauqrLe9WePCuKirrsxGMyo4O+/ZRTQjJCMtRee1/182Lzt3rqiirLvJPEvjsaOhr9VMKDA6a6ysrVk6JiQ0R7KvvGsvKSQwW7Cnr7hAPTI52bustbpd
+O0VB2baqtLrCOFFJ2bq3tu9fLzI8PdVRY0U5Ly1BRM3Mz87j1/e5r62yvLzfycu8rrC10c9OR/dK51M7OS8tKkBDTuc/RTpBP3e1sq68wM/Xv7irq6y7a289Vde6tMr3NDMuOPfMs8dILSgoLVXMs7d3Vy4wO9mvp67Nay8uOF+tp6W01zwuP12wp6ev1zwr
+MkTJt7fNNi8mJjJAwMrFXTc6PGO8pqirr1vvZ9WwqKavtE1CTVW/vMBMPickKyw+R05BOzcrO0jfuLi5xsZrx7SvqrCwxtffU83Ew9PbSztVTOvR2+9OSjM/RkX3T0dBTUFdvsO4yHdHRFtvtra6y19nR+vItqyzwUtEN03ZwK27/z0rJy1AXbnGUTgqKSpP
+uKeisbtIPkzNraiirb9CNT5Xta2ruVE7JiovPd/dUzUvJyw0V7W2tNNPR0XKsqOjq7FVY1nNtqyux+8vMjQ9zb6461U3Ljg5V9/NSUc7LUFEz7u4uff/OkHjybnDwV3Z9++/ure+vVnvyeO+vcRjWz0yS0j/50E4LTErRd/ZuczZZ1vfuKmmqLPGa1Xjv6yn
+rcJVOy09PeO+b0osJR8kNE23vdtDMS8127eqorC8W1VPva+oprjXPjU3W8S8s91rOy44QL67vs46MSkwPsC2u71DPjw/17mvuLtDPD5Jwq6psLhnOEZFx7uwtdt3Lzg5PdPO30Q9Ki02O9nMxd3f71vMu62wsLnMyP/FvLe5x8xGY2tryl1GNzgrMkA/WVNH
+Ojo5RMC9trjH1ffvy6ypq67E2UFXTs6wucVZOyw1RHe1uMNXNSorOUq6tM9bNS8vU8uvqrzfODY0X72yqbO/7zpI47uvrLfRWzI4Pdm9ur5ENSkrMEXLyMVLPTg3Vb6rqqqz09Ndzrqtq6+4/0xESOvDuc3jOCwtKjVEa1NIQC43OkrRvbvOyevrxcKzs6++
+yMPdxsrHx9NrPltZZ83T3UlPPD/3W99vTzs6NzrPxr23z+dHTD/3uLixvdNLVf/nr66vu3c/ND9IwLS+zj8zKzE8Z77G/zwzLTRv3bGst75NTlXGs6yossBKPUNfurSuvE86LTA0Z9fX7zQvKS4707m6ulVDPTzjw6ioq693X0Zrvq6qsbpCNzg2d8W3vsxB
+Lzc1T9nCyk9DMDM5Rt3EuevnT0JvWcjGv8df1WPNu7e3vb1P5+Njw8XIa1M1O0hB42tZRjkuLEpRybvJzPfvTL+urqmvuMrn/8uzsrK+3Vk3PkD/zv9INC0nLzNDytvnQToxOu/Pr7C6x1VZV7yzraq9yFM/R9G8u7HbTjowOlfEu7nfQDErNT3RvLm9UUE0
+O2+8rrOzXTw8PNu5rKuwxUdIQGPBuLjJdzc4OD5Xz81RVzU0Oz1j2cPRb1tI18y3tLi519X/zry7u7/TRFFbY8vP0VtBLC06P29v71dFOjNd58q5w8t3X0nHtrSutcLZWVnMvLe6yOdKPD5Byr2/ykk4MTc8/8TO6z82MDpvxrS1v188Nzznuq+uu8xVRF2+
+t66uxfc1MDdEzsK82z8sKi8928i/0UU9ND7ZtayqrctrV+u+sKurst9NP0jdx7q+z1kuMC0yRndZRUEvODxE38fC21s/Q+PPubS1vMzvZ7+8ube/50lVT/fR18vrTjo8S13b7/dVQzQ4Z/+/usbRb01Az8W/uMLMWU9Zz7OysbrTSz0+RcjBwM5INzE1Ot+/
+yddAOC45U8W2t7zjSz5RxrisrrrEST1K37yysL73ODM1SNnKyFU/Lis2TNPLv8NVPjQ9b8K5trTXZ2Nvybuvr7fDV09HX8i7xc3vPD8/Rd/V11lMNjQ7Q3fT08znTElnb9PGy83Xd2PJxMG5uL7J2Wtv91dvd2NKQkJBSEJIZ1VGP0VFUWfvz8DGzcbFwby7
+uLW5vL/FzcrOzL/I1+9nRUBCPkJGQz5CPjpASlFb//9jUUhZZ+vMv7q7wNPZxsrHvrzF2eNXVVVV993Zd1dMREM/TP/va1dPT2Nn58W+xd3j72/j38/DvsXGytHNysTGzt1rVURGWWfj/2dKSkA9SE9ra+9VTk1N69HPxcXGz9130cPJwsDA3+//b9vO1d93
+Uz47ODznwMZbRjQ2OmO9u8rnUzc5WcO1rbG6zE9L0cC2sLi+Yz06V93Kv83rRzQzOF3v0+dZPTg/Tc29u7/vST9L38C5ub3Ob1F3wLqyu9NTPDY6U9vIy1FGMTQ9SO/T02dbNj1HZ7u7t7XDyl3JvrewvLvT9+Pju7/ByFVAMTtf587vQjkuMDdXw7y+UT42
+Kz9Vy662u+NMPj69r6ututk1PENZuba4zEA1LTlTzLu87z8tNDNXwLixv8pMNUxXw7C5uNVdQ13KvLC3vchBSE9XzcrJ2U5EOknf69FTVz0yR0XnzN/dTlFGa8G8ubzTW0VM5760sb7bXz84S/fBt9tZOy42OVu+wL5ZSTAxP1nFusfGXVFH9721r7HAxW9b
+68q+vsfdU0hDXdvdzElJPjRDSufG599ORkBI18O4vNX3RD5OzbKws71vQTlD28S2wv8/NTA0XcnIuGdNMzE2Ot+/xr/vST9j076vsrm9X2//zrS4ur3VY0zf48y+b2M7Njs+493fb0BBMj1jZ8rTXUU7PEPXtrWxv8xfQ//Ovq7Cw29FNzhdz72472cyNTk8
+28jKzFtNOk/Vzr28xtFOTVPTvcC8z9XdTsnAwLnO0UhBTkvOvb++WU41OFFdv81vRjQxMT/LvbS/2082PVvGsa6xyHdDOWfKt6u8wVs3MzJJ0cC390svMzpFzcLNxz84NDvvy7yzvcJra93Js7a6xm9fTtnPy7j/Z0o5QkFjzd1vQzsxPl/XurTO2UhISlu4
+uLi441M8SN/GtLbIbz85M0fn1b/P/0g4Qz9Xxt/XXT81Plvrx73Mx1lf2+O7uL2909lf47i8tbfNX0NPW9u8ytlBOjExQmfbvfdROTAzPWO8vcX/YzhFd8qvrre9Vz44SsW7srfTUTU6PmPFyMdMPzYxQ2/LuMjIXUFMZ8m5ub7T/z9O48aztr3BU0dIW8a9
+tsH/Rz1Ea9G5vMtJNC4uPmfbvN1dPjI4SNe3urfMYz1JY9Gzrbe3a09GVcfBubv/RDU4OknVb+tbPjw3SlVv0Vt3TE3X0bu4vcDV3XfPu723xdlvRlv30bzP50s6LzlN78zD72c6PD1jt7S2ueNKOUt3xrW3xOtGQT9vycm95048OklnzrzP204/OD5Nb9Hf
+a009R0zvvLu+zfdLTXe/t7C2we9ZX+u8r7O3zko4O05nz8TdTDo0MD9j18bXZz4yPEDnt7q6xWtKS2PNvrbAxk1JUVnNwsXHUUI5PExd2e9vTDxCR2/KvrzV3V1J/9PBusfPX1Vn38O4uL7PYz5M/864t73GTT07S/fv2VVGODI1PP/M0c5XRj1Da8u9t8bG
+/1/nxrq4s7nL60hf587B92NCNTk6Tevj40Q+MzVFWcK8xNFbTk/Zt7WwuMpXRU73xLS3ve9NNjdN98++32s7NzU73b29uMX/P1Fd27i4wsxIQj9bzsO8v+dGOkZJ48bFxdljTUTr18/XV006OT9H78vPZ01FQE/dx7u/w99Pa9/DsLO1u8NrWdPBvLfB00Q7
+Njxb5+PdSjsvND5M78zZ601BP2vNvbW6ye9HWV/KvL2/ylVEPk3rycf/WTo1PEL/x8jZW0tAS9PHvLjE/0lAR92+tLW+yFtDVefDtre7yV1CQuO9u7i/ZzUxMjZI3+/vSjkyNT9f0cDO2VdJU+fHuba3vcfv08K7t7vC00lFQ0vf1+NRQDUxOEVV193/RDw8
+Quu8urW6yF1O98/AtLm+z19FWf/TxMHZazs9P0vj0c/RWUxGWdnHvMDN40dFT9/Mx8bdW0tFXdXFxNXjW0VJWca7t7/jXz09TVnV21VFOjQ4P//jz9XjRT5Da9G6uba9y+fdx7q3rbO81WtOW9nNz8pdSTk7PENRV05dRTw+Q1Pn08vV0f/n18nEyszN729v
+18/T3+tvSkVMTWtvX1NLSUZT3efT19frW2drd9Xbz8nK7+fXys7BysfDytPV0c/RxMPAydHdU01JQEU/Pz9BRD89PURTU11jb+/349XdyMa5tbq9wcLFz8DCyMnnb1lTQ0NVTU5KQTw6PUVHXVljb+Pnb9fCyb/CxMrR1+vRydHL1dXZb3dXWV1j519bU1lv
+b93X48r/d+Pj/2v/b2ffX2fv59vr1+9n3d/b19vR2dHK2cjOz9//d01FPjtBQEpLSEZCR0tMWVnvydHV187Hwru6uLW7vsHGxc/GzNfT711TTVNMT0Y9REVGQ0RMSllnd9/by9HRyNfOz9vN29PT29n/3et3d1FZU01MSllRTf9dW11j/2vr//fdZ19n2dHd
+yMvNxdPMzcS8x8PD0cvNx8fFvs3ZW0hHQkRCPkI/Pjs4RkZPT09ZV29r2cfNx769vcC6vL69v8HE0e9r42dTU1NMQ0VAPD5CREVHSUdbZ2/r08nGyMzLycnTz9HLxtHVzNn36+dvX///W2djXfd3a2vb0e/39+dnX1tVa/djZ+/3Z+/V2dnLy8/Z39XPyc/V
+18mzuetfPzM2O0rX50hBMS42PPfDz9l3PztH1butrbW8zv/fw6+ur7rTU0BHW8q9yvdMOC82RWfXa01FNzRDW8O0ucHfTEdTyLe1uMf/PTtT57m2xGM+MC8668G3yVM2LTI967qtusx3NDVIY7+wvcjXR073u6+ts85vOzpfuaurrsFDMS40Uc3O51U4MTA1
+SGdjX0Q6Ok/VvK61zNFDQdfArqeossZVO0V3yb3Rd0w6NDpN1dXvPzctLDVdtrK7yE8+PEHRvK+0vsxTT2PRu73M01E6R3fZvLfdZ0gzOkvZu7O621c5Ok5fy8X/VUc+ScyzsLXOTz86P8ivqaq1008+OUTjwchvRjIyODhOyFdDOiwtNkrGr67Cz0A6Sve1
+qaissMN348nEu8FnW00+U8nBwvc5MCsqM0fHurzHUT5DROu8uL7Nd0vnyr6vts9ONS00RsmxrsNbNSssNkfTusDM2UdBb+PNyW9TRT9Vw7Kur73RTEBHb7yurrK4329vY8/F21NFOThHX93PUTcwKis4Ucq1utlfPzZH276vr7e7w9nIuLK1w+tHODhGzbS2
+v1cxKSksO+fEws1TPDxFU8m+ydNfTnfEuLGxw/9INT1b2bSuvs9bOTY9U9XF1fdZUVPZvrzHVzw0MjnrtK6wv103Mzpbuq6stb7dTWvRw7e4z2c6OEdV073dSDImJjA6a7u7zlc7NjhG0bu7vr/DxLqvrK28/0tGS+e2ra69XzssKzQ/3cPPd0g3PE33ysrj
+d0Y8d8O1q628WzgtMkPMsay02z80MDZBX9l3U0tFSd+/vMVvPzg3P+e6ra+8y0U5Pk/CsrC0ut/318W8t77TVz08X8C6t79KLyYlKzrZvrrVSzoyN0Xvv77P52/rybmsqrS/XT0+T8qvrbDBTjEtLzxv1d3vRjg5P1Pv32dNPzxHzravrrzjRTY9Wc61rrrI
+40FFSWfKzfdTPj5P17m0xO9AMi41TLyvsLz/OjMzQ9O+t7jA22vZwry3ucjdTUnnv7ivu2c9LCovM0XZ3VtJODY5QUVRr2tOUWfOtKqqtMlbR0fjta2rsL5NPDc/d8/L31tENTtT783MX0k3M0X3va+ss81DNjtN37qyuM1bRkRKXdvjT0E5OkNfxrvEZz04
+NTtrxLa5w89JPkdXzb3Bxs9r68+5tLS+yls+Tdu4rq2xy0AtLTI+d9HRWz01NTtEY8zrTEM8RefDsa62wv9ISvfDr62yuddGPUJrz9fbXz07P1Hn2WdLPi8vO++/trXCZ0A6R2/RubXA011K99XKu8FbRzw+U9u7t7/fPzQyOFe+tLjHWTs2OUnOxsbKX0hP
+4762tLvHW0RNyLavrLHDbzk5QEVn029LOzE1O0hV/1k+Nzg9VcO0tr7bR0JL57asrLC961FNZ8i9vMjvSTlBXdnAy2dMNC44Rte4srnMRzlCVdu7t73MXUhR48/Exe9DODc+Z8S/x1U7NTM7W8y8vMjnRj5FWcm/zttfQ03bwba1vcdXPkrnvq6qrbjfPDxM
+/87BxWdANzQ7RVfdY0M5NTpXzL23xGdJP0Vnx7SytbrRX13/yL3Dze9KQUvrxsXTW0AvLTdO0b28y107NjtZ0b+4xedPSG/LxLq4zl9APUZvv7i3v1E2MTM5yLy7wttPQUJJ48zR31c9QVPXvLjB1WNIQWvAuK6vtL1nS2Pjyb6/0088OD9TZ19VQzMuMjlX
+w7m81UY1Nj9vu7SzutVnW+fKvba/yutDRm/Nu7rMVT80NkNdxrzGz2M/Q1Hr0cLH23dPXdHCwsfPXT8+Rl/XzcDKUT82NjxT483Ob1FHUf/Xw89vXUVDT+u+uL7Haz89R+fCtrO1u9Hn58zJycLJ71NPW3f342NEOTc3PVF31cnTa0pIRkrn1c3Gys7Oy8XC
+ytXbb11308bBydfrSTw9TFNZX2v3WU9XX11VXVtZW1fXw8fNy87jW1vv38/Ry8XN519VTUpMUVVfd9vX5+fnXVFbS09r79/V2dvZ71dfb3ff2ce+vr+6vcXR1+vn793OyONfWU0+OTk/RUdTW2dfY2tbV1tXY/f33cjEycjJ09nva+fVzsvKzMzbY1tMTFtn
+Z+vb4+ff92vv//9vb+/b08/TzuNvW1dfa2v31+93Z1tTT0xJSktKV//f3dvn43dXX11d69HT09tvb/9fXffr39PTx8G/wcHCx9Pn/+fX2dfN62NbRj49PkVVY11v929vZ1tvb1lf49XOys/LxtHf3+Pv19Xdzs/d29t3V01JS01NW9v/d+drVVNKT2vn79fN
+2d3f7+fj/+fb3+fZ29nX3W9jTEVKT1fv3ffj929jZ11nZ2/r5/fv29vj42tn92/jzsHCv7y/xsvO0dPV19njb2NdX1VGP0FCQkVNWXfZ4+tnTExVX+fTzc3V/2t338O/yNv/UU9ryL68zWNJP0RZ18bDy9tVTFPbxr3G3WdCQ093xb7P71FAPUVdzcDF2Vc+
+Oj1N387L71dIP0vVyL/C2VlNT0PVzNXJX29MTNt3wczOy1vT19O5urfF/1k6TePOtrzPPjY1LkfbwLfP20E6Pz/NwL6+XW89Pm/3uri/wEdRP0zAvbe711kyPT//vsnD2Uw/NU5Zyr/bzEtCW0/NysfMVV8/S9PjwcfT20pXS1HN681rTU0+a+vOv3fnSENL
+Ssu9vsBXXTk8TPe5uL7OV2NF58S7srfAyUdLRv/Au73XXTsvPkBnys7VZ0k+PE9rzcbR61NNY1vZ2d3vX19d58nNy9P/WU933cvAzN9fTUxrzcG9wv9rS0lb3ca1wNNNQjM4SdXAt8pbPDo3Q9/FxMFrQDc+Tt3Nw83vU0dI983CwsTXTkdGRHfNzNXfXVf3
+18vCyNvnY+PJwry9xGtTQEFMz8vE2Wc/Pz1V0b7DzF9VP0JXz7660W8/Oz1N07u4vs1RPUNJ47vEw8lJQD1R69XC53dPRFNrycHLyFdRS0X/78nKd/dOUUZr1dXOX09HTPfMwb7V/0M7Pk/Lubm82U86Oj9vvL3I209DPVHRvbS7yVNLS2/EuLa0yu8+QEVX
+3cbO01lEPEdj39HR/2NPW2/Ryc//T0FCSXfXys/jS0RHV+PDw8v3V0VO2726tMTfS0E+58WwtrrRWTk9Qt/Bu89rQDk4QGfNxctdOj5BR9m8vb/dRjg+UdnAu8HJ/1FEWffV0+tnVUpjb9fI129RTVP3yr+9w833SVv/zb7AxetMQkJjx8LB1f8+OUFfybO5
+wl9FNDdC78u4xuNIRD1NY8zFxOtTQVFrz73ByNNLRUNf0cXBy9dXU1Vdzs7R91NOTFvn1ct3Uz49RF3Pv8XGY1M/RWfVyL/MzUtISF/NwMXnZ0M5R0zRubzE70o9PGfIu7S5zFVOTE/Vx8S/32NGTWfvzcjna05LTG/MxcjXb1FHTU1v22ddTENKUf/Rz91R
+TT9I58a6t77OW1NN47y6uMzIV0VPW9O+vsLvUz48Stu/v89rPzU3OUnXzMXTVT5ATFvOwszPa1NPd8zIx85dTUdFT9vHzM5rTENIW9/IvMvOb05n387AxcTvW01X18K9wM3vQkFDb8LAw8b/ST5JX9/Gxt1KPz4+XdXIv91XPT1HSdO7xMHZUT9O68+9usbM
+W01NX8rOyspdV0ZHZ9/IzedMPz5EVePfze9rSkn339HKzttTT1V3x72+0fdNPEVT/77Cy2tLPjxP08u8wc9VTlVfzL7Fv+NXQ0xvzsG7x8tjSkJd1c7ByNn/Sk9X/8zr3Vs/QkZNZ9/RX0w9PElrzcvJyGNMTV/Jv7i6wNFvW/fPu7y93WNEPkpd37vP2Uw9
+NDdHd9/I6088RUVryMXMxmtTRmvXwr290f9NTUvf08/O/05GQU5T3c/fd1dKb+PKx8Tf90lNX8rGvsPBd1VLWee8v73Pd0RJUd/BusXjS0E5RGPXycXrRDs7P2PVw8TbXUVBT1vPvcbF2Wdjd8zExcLvd0xKb9vO23dvQERJV+vn319HPjxR78vEzNVRRktj
+wcG8wdPrS0Vn977Aw9tNPj9D/+u/yd9NSENO2cHBvNVbQkdT3by7ychfT0hjzsq9vczPSFNr38LKxcpPS0FVX2/jXVFAOkVHb+fT2VVKR03OzcHKy3dbVd3Ds7e2x+NTSFvRyrfE20M9Nj5d69HJY0s3OTpF68/J2UE9PkNExri6u8Lb29vEvby6xM93T1v3
+4+t3Z0VFSFf359l3WU1FTuvXyszRa0hGV9vEwcPL209MWeu/vb/Ld0ZGUWvZv8fvSj05PU3b18jfWUU+P1njxc3K51tLY+PGvLm/xmtfY9vLz8rGb1VFS1Nv//d3T0BJSFNb/+9XTFFX0c7JyszjX1P/17+/w9PnW01Rb+vG1/dRRkNHV9vZydH/V1fdY2Pd
+68jXb1lXb/ffzc3AztfV4+fvzMjMxdXT92NdV+NnTndOTUJDRURRUW/XW11OW19d18/EvcrJ29fTzb+/ur7P219XV0/rb//3Skc9PUFMa09bY0xIPlVn38DNxcd3X2Pf08a6wcTRb/9fb2//zF9jVUNNSVNfb+tb/19Xa1/n3dnM3dHfa//vzs/Tyc/Jzd/X
+18zbz8jZ0d3vd2dnV1ldSlFXTExKTVFfWU1jZ2N3b9/b1dHRyM7XzMrHxsrK1dvn/+N3Y11dXU5PTklMS0tJS0xGT11bXf/X1c/P49Pb49nOyMvLzNPX42NvXf937/9jW1FPWVNjd9vna3djV19r59XO2+//d2Pj08XJxcfT19ffzszFys3V629ZV1tOUUhF
+R0FCQ0RRSldPUVdv39ndzdHOy8XDw72/xL/Ex83NytnX91VTSUpFU1dKR0c+OztT5+NrUUpJSuvCub3K12td58W6t7bE11lHWe/Rw9f/Qzw6PVfV2d1bRTxBS2/GwMrbWUdIU82/urzRb05Xb8a5tbrLd1NK59W/usfnRTg7PUzbzutNPjc4Pk3OxMfdTktL
+V8i6tr7E42vny8K7v8TvX0tJZ9/Z21VEOjk/Sm/d/0g+OzxVzbq5xndTOv/bua21t29JOjvXxq+zwlU0NDJT37m10Us0MzJPzL2122M7OD5Zvbmw0f9MM05dua6srd9vOD/jvK2xsV0+Ly07U77CzEYwMCo6Ssq6w908QjxTzbustrXbWW9Ovrasr8DRP1E+
+T9nJy11LMjQ5OElM50tXSTpMSevfxMfXw13M0c7Hy7/NvsbRxWPjVetv19dbaz5APkJV68Vv20s6QjZMTM+81b9HTz1E0cGutLrZSV1DxLusrbnNPT86Y8e7s99nNy4uOWfVuOtRQCs4Ndm6ra/v9zU6XcSuraq/0z81S+u0srTRPjwqNjz3vcHdNjQrL0Bn
+tLu3/z5ANWvKr623vD9XP0zGsq24vj88STvZz7jJ0WMxQDRCa8rD79E4Qkc71dm80c9rPe9IzMm8u8u+487Pz7/Iu+PH0Vt3RVk/U0w/3zpKPjw8NV9CxMXdy0z/RtnOuK64td3f/++8wa66vtU9PTNKU8jKUVkuMiwyW+u613c9Lzs5wL+vsMT3PUJJvrCu
+qr/NQjhDS7m2r79LOCs3N9HIvcE+PjEyP+e5wrhNSEU0Tm+5u7jIUWtAb9e5sLW032NTR9XNt8jKXztENktMb91KWTU9P0X348Nb0/9P2d2/wbXKy7/vxcu/xMbGTtdNU11NV0BTN0trPltCTj9GS03M2cfbd2tL2WO8uLq73+dHZ9/Hsbu/d04+PF9jv8LV
+SDg4L19VyLvRYzs4M1fNwrDGzU08PT+9u6yuwdlBTVG9trSw2003MjlPy93ATTk0LDY808q9z0tHOE1dvLGzstPnV1/FuK2ztddNU0Fn68TTZ0IzODQ/S2fvSUxNQkpN49W/99//RufryMi5vd3DX+fRzcnRyE/P609jU2NJYztd401rS1NDT0hDydXO019V
+SutLwr3Dvtv/Su/RvrCyuM/vRkXrY76+0U08Ni5HQ9/E/2s6NjE93cyyusZdR0lOvLasrL3ZS0pLxby2sd1LNzEwQv/nv11AMy0yNdvHubtdTTpBTMC2t7DP411M78a2urfXTVVDY+vDx81bOj46Rk3Zy2d3PD5DQHfnvtfd/0dnUdXOv73OwN/ZzMm+vrjX
+wc9ba0xfSnc/RF08Rj49OkRKPNV3/9lId13L57SvubXEyPfOy8mvtr3ZVz87VUnM1V1RNDArND5Xx9v3QDg2O9/JsrK9201OTMG6r6q5yldDQOvEvrLMd0E2Nznj18DJSz4vNTr3z8++Tkg9PE3dvsi641NVR93FtrK0xF3rY8/DubrM0Ts6PDtP/8dfSjgv
+OjhP58XN73dCWf/LvLWzxb/f/8rGvLy439v/SVlNWU1bRDpKOD9GQUVGVTrd1/fN79lj0WPft8G7x8vj4+d3u8LAw2NMRVlOyMTV3UQ8Mz5Ga8Xb10U7OTpbWby9yudHQ0PRy7etvL7vX1Pvu7ivuNdHOTk9a1/T3UU7LzE0Xf/vxU9MQkVjwra2sMPT2f/I
+vrGxsbljY0pT58HK4281MTMzPE/nTUs3Lzw5V+u/w8/rS2/bx720tMK91ePMz8jJwN1rb05nW19r309FZ0dNTUtPV189UW9R42trVf9KV8nZyc7R39XP2be7u7nG3XfjVcrG7+tKQTY+PT1vS1dEPDo+U1PCxsfE92fvyce3srm7y9vrz8PIvcXZTD8+PUk+
+TE5DRzg5O01OT81n73ddd+PK27y9zMjTy8vDzse749fjb2/b12PJX01ZSUZFY0x3W0VVRUlHV2930Wfv91tv3cjOvrzHvszVzcO9w7zH02dHQkdRRFdPRUM6ODlIP07bW+9jX2PRyM63u73AxsjJvsO4tr/C2+trZ09Lb0lIPDk4OD44RkdFTUZOTln338rR
+ycjMysvDx76+v73FztXZ72vj59vva19jVUdRSlVVT1tTU0lPU0tfVV3j72dX/1/r39HGvr7IxMfLwcO/vb7T/1NGS0ZOVVdPPD85OUFFVXfrd1lbVXfbyL27vcfM08/Cwb62uMfO/1VjWV1jb0Q6OzU6PERTV11GSUpJZ+vMx8jT49vrzcTBvb2+zdXX3c/V
+ztXZa09RS1VVY19VTUNJQ0hXXWdrd0hMV1H349PLysrn1dHNw766vb7L1ePr3f93Z11FPT48QENJU01TRklGT2Pvx8bGyNHV3cfCvbm6usXGzM7KysTLz19MQT09PEJAREM8PTk/QU3/d+Nj7+//09PHwcLHy8XKwcXFvsbXd/dd7/f32fdnR0hDQVVT/3dZ
+TUNNRVljZ9n/b1dVVWvOyr/BxcfMycu/v7/AwL/Xa2dBOj5NX19AOzcyO0Bbzs/ZUUxDTtvAtLW703dZZ8e3rK63b1VKPNnHvrjnQy0rKzJP58PjSD4tNjpVvLi2xFNJPU/Rua6vtstd613Lv8K42edJPU5K3dffZz9HOENTU89n/1lGW0tb09fO3f/nd8rH
+wb7Nxt/Kv825x85nQUo6TFtd70M/NTM+P+vL1eNJTEdMzMC4s8PJX2fNybe0srvP00jjyMi8ytdHOzkwPEVRWUE9NDc/Sffb1+9TWV9vx8LIvdHTa+PDv7a0v91TWUjfyMa+2V9ANjs+Y8vJ00s9NjY+/8a6xONAPkE+3by5scDMXVHZxrixtrzZd0BITl3j
+a1tEOTs4PEhOWVVKS0xd99fK18vb08nIu729vt/O18e9vLq/00xBSEhbZ2NXPDg1MjpIXcrva0hAS0r3yL66x8hZ49PXvLm5us3fT2/Z283Ob1lISUdTW09fR0BEPV3v689nY0lJb/fFvsLLZ29N18S/srG7zk1OS//FxsfdRTovMjpMz8/3RDs8Mkfnzra/
+yPdX///Ct7Owu8Rfb9fLu7i6v/9fTUZfWV9bQjwyODo7S0hZTEVNR+fd2cV30e/vysu+u7vC2dP31crDwMbnZ0JGSl/O2d9fPj41O0ZryNfbS0NMP3fby73O0V9b29XDu7u4vszbzcbJxM1nY0JFSEZRRUxAOD04RV9f31dZQ1H/37y3urzb713bxLu1s77P
+T1VX2b68wutFPC42OkXj72NGODk1RWfKuMDXT0tfZ8S8uLXDzuNrz8m9u8LRTk5PS+ff22dJSTlCRURfT/dnQU1IY87Cv8/fTVFv2cW4vL7jb05dyry6t8r/Ojw8Ru/j61k8PC85PUXb09P3Rk9H78u9tLa/zNnM2cG9u7rEzM/dz9vKzl93Q0VKP0k9QTo4
+QDtNZ1XfUVtLTffNwbnCyV9vV928tra2yNFIU1Nrv8LI0Uo/NkBKX8/fX0A4PTlV38+/02tORGNvxLy9vdvf7+vEvri1wsZna1tV0d/fXz48NDpKTu9VTjs5PkJnx8O+zdtPV8u/uLO5v/9vW/e+u7u4zd89RkRJ7/93Wzs5MTs+TNvjb1NJU0znzca+y9Xr
+a87OvbvCv+Pn2+vK08vZW11GTllV/05VRDtAQ0/r69Nrd05P39PHvsjIb2dZa8TCv7zO2Vdv/9nIzttnQT82QkdM/1tNQTo/QOPT08Pnb2P30de5tb2+3+/n2726trnO60pNd+vIy+dEODcxPFVX91lJNzdAS9PBxcP/Z0pP0cO+tLu9b29nd8a+vbzTYz9H
+T1fRzedZPTo1P0pj1+ddTUNVTtnNx77V3+NnzMrBvMPC0dvLzsHGy89PT0VCU09fSEo/Nz1BSWtf91VTSUR3ysC6vL3Z5+/jwby9t8nNY2vn48nH0dFMQTg/R0v3X0tCOT48TNnZzdtXU0dIXbirvb/P9+vfwLy+vNvrUUlv79XP31M9PjpBV1/db1dEOkhV
+68HEws5vVUjnyMC6u8ZXTl9XyL69vNlbQT5ISGPdd19CPTc8S07r3V9fRFVrd8XHvr7PytPKv8G7w8bXd87NxcfOzlNRS0VfU11jTEM1O0NGX/frX0tPP13f2cLHydNn52/Ovr68w85nXefjz8rV401IQ0BdU1trSEY8PUhj2d3T51NbWdfFwbvBxdNj3c/C
+uru6yv9XTmfn09N3WTo6PD1PW2/3RkA3PE93z8bI2Wd3Wc6/vLi8yd9b993Gur+/01tGS19j499fVzw6PEFfW3fnTEc/S2fbyMnJ3V9r98rDv7nBz29b49XKxszZU0tHSltf73dTSjpBSEvn29PdWU9Hb87JwsfL3e/v98nHysfN1/9Z2f/n2f9nR0NDSl1M
+VV9LRj5GTlnd/+fnXf/r08PAvMLG19vOzMHAwL7X5/9d3+fd42NOQUREQ05RVU5JSj1MVVfn/9/vb/f308vNx8fF19HO2c/Lzc/v/1tjY1VZWVVMREZDSk1JXV9nXVdja+PXz8rMysrIzNPFx8bGx8zPz9v35+9vXVlTS0xLREdLS0pKSUdIU1dfb+vd09nb
+0czKx8nIyM3Tz8/RysvP2evvb2NfVVVdU0lHTkxMTVFRWVtbXff/Z/ff3dnNz9POy83LzM/NztPf3+vn429bV1dNS05OUVlTUVVXW2f39+ff2+vr49vPy83Pz9Xd49vZ0c/X33dfTlNbVV1jW05HRkNITU9fX1lTVVlV3c/PycrM0dPRz8a/wsHGy9HR0ePP
+093rX1VNS01MZ19TT0pKR0lPVe9nZ/93b2vr09PLzM3L19vb1dHXztfd3W9dWVtTRlNOS0xISkhPXWff5/f3d/fv18zJw8nMyM7PzsnGysnP2dv/a2NvW1VbU01LSUlHS0tIW1NPWWdnWWvv79XRzsrP19/V0dXMyMjFztPb43df9/frb11VTVFLSVldX2NV
+T0xVW2/j6+vd3+d359HMyMfP12dV/8rCztdrXVtj3+dVS0E4QFnfxb7M60w5PUlnxrq5y99VTGfTyb/A32dvZ+/Fu8DdRDk4OkbrwLnJWT40NDdE28TK01lIUVfrwr3G1VdOZ9HBta+4zWNBSffPv7q/b01BO0dn69v/SURCQ1vbxcp3STs8QFPItbS5zE5G
+SU7RurrB0UxKVVvdwMhZQDQxPUrjwLzfUUQ2PVXjv7jEztVvZ9/LwMTV7+vrzb24vMXdQzs7PF/Hyc7XRzo8PkVr91tjRT5T283Bvt1PPTZD2761rbfPZz8/Tf/Ct77O71NKWWfn510+Pz9Dd769wM9KOTY5QtG7vLvGZ2t399fN2+v/U//Fvr293Uc8MzdM
+78S8xOdNPjxDV9nCwvdbWVfrxsPI1U9ASU/ZuK+xvm8+Ojo+/8m/w9lPQkVFS2dfVU9CRF/dysLKa008PVf/w7K2vMr3U13n08G+zufj69/KydNdPzg4PVvGu7rHVzo2OD13ycDDze9VX+PRxcjna0ZK682+tr7VSzYyOklnw73RVUM8PUljz8jd/+vv48vA
+xM5TRktX3bixsLnOVT9ASnfHwsbTVUZMUWPfZ09IOzpX18W6v99FNjI7Vcm3s7vZV0pKU//Nv8vf493bx8HG30U7OTtI67y4wONMOzc6SdvNycj3VW/f3czV62tLSFnEube0vl87MzpJ3cO4vetJPzxFT//Iz2dZXXfTxbzFZ0U7OkFdvrGxts5NPzxA68XC
+wtFZS05d//9dTD85PVPRwr/G/z82NUVn07q2xuNTTF/rz8HD0f9348y8t7zCVzo4OD/Zu7a70Ug5ODlM0cfEzWNRV1vXzs/nW0dFX8u+t7XG7z82PERdyrzF41E/QURHWf9bTFNj3cO6u7/jRj07Rtu6s7K5119MRW/Tx8HF41lPY//b1WtRQTU7U+fFu8DZ
+SDY3PEnrvrnE3VtOU1v/z8znY1/30765u8FjRT89R93GvsLXWUQ/QlXb69vfV0tv38/I2fdbPjtF2762srrZQzg9TG/DvLzTWUxIUVdv1W9JR0lV1cC6vNVVQD0/S8+9u7zOW05IT9vOzMnTX2/dzcrR71NANzlN3cjBwd9FNzZAUWPKwNV3V0pv5+PMzV9N
+T2vPvrW0vNdKQz9I3b27vMdXRkI+WdHP1ddbS1P/z8zZ611FPUfnyb+6vtlRPEBVXdvBxdtdTU5ZWWPvW0M/RE33xL27yFFBQkZfw7i4vddRS0ZZysDAw9drV2/XxL/P4109O0df0cC+yf8+OD1HW9HGzfdTTFlnZ93XXUlLTmvJv76+20xGR1PRwbzAz19L
+SEhX18/X31FHWW/Vw8n3WUA4Q13Ovrm8zk88Pkrvy769zWdPTv/v48/fTUZHSG/Rx8LOa0tDRV/TxMHD011RSEv319HNz2v/283ExMzfUT8/S93LwsLVTDw4Pktd08PbVUtEVevfzctjSkZJXc69u7vLU0JBRuu/ury+901HQ1fn2dPM/1Nb2cvJztdrQT1E
+683HvsPdVz9FX/ffyMfdW01X99vf2/dCOT5DW8rDxs9OPDw/Ucq6ur3TV0xKW9nNz87ZZ1fjyL+9wc/vSD5K983CvL/fRj5DTl3vzM1rTk5d29XV2WdAPkRV58bAwetHQT5K/8zBwsrfX1lf48vR91tOQ07bw7q9x9tFNztH787AwMlbPj9O993TxN9TS1XZ
+ysfBxmNIPENX28y/v+9RRURJXdfOzddfVU7nz83ba1dGT2PPvbi7xOtHQEZj2cfByeNOR0tn6+vbd0pBPk3by8bJ51U+Oj9O3c3DwdNZTU1T99HLyN1vZ+vRyMfL41NFSFvTw8C9w9tfSU1da+PR0/dVXW/v3+vrZ05ITmfZz8zO60tCP0NVa+/j3WdXV2N3
+///f92dr78/GyM7T905GU3fZz8rDw91r/3ddWWPd1/fnzszv7+//Y1tZb93r59nP62tjV1NNTFdn9//n299rW11bWWPr28/N1d/dd1Vba3ff3dPP0ePd1etdU1VbV1Nd29/v4+drW1ddb+/3383T3d/3b1dXXWdrb/fj3dv/Z/djXWvn4+Pd393Z/2NfY2Nr
+7+PZ29PMz9vvY11bXV/v1dnZ1etdVU5VXWN3593nd2Nrd11VY29fXWNn693v6+trV1tv/+vb1czX//9nY2t32c7M087K19/f93dnZ2d3b2v/4/drY2Pr92f/7/f//2tva1NRVVtVX3dv7+fr92tbXf/d1dnZ19fvd+d3Z2/359vd08Xv20xVTUPRyMG+/9dH
+SVFOyMXJvf/XSFH/XdvIzNFNSjk/SErP0dHLU04+Rmdjz8fPzll3R2PbRNPn68lZzOfdxtnI2/fnTetR/9f/zf/n3T5rS1fT78HF18tPd0pOZ0/P09/RSW9KT+9TzNvjyVHjVWPV78PRa2c9TT9N/8yutdX/Mjo5Rrmvt8dnOi49S824ucTrPT07d8e/udFd
+PzhMW867x9tXPjpDd92/v8tHOUnr27jL02c/PD/fr7Ouw+c7NUB3uKq1vEI5LTFXzbyrzF80MTVLx7KyvFU7KTQ/Z7K0wMQ+My893ciuuc1XPUtNzLnIxlM+PD7Vxr2x71k9O1FjwLa/40U9N1m+xLDIbz4jNLusp66vzUUyLj1rwau7zDw0MTFrysmw/1c5
+M0VXx7TJxj48MEbKzbG7zdM4Skl3uMG9zk9APXf3xsBbUzs/WWu1uLnERT0wQM3GrrvOTTI1ON+6vbVfWT4xT+e9qr3GQzguO3fCr6/GzDM6P0i+u7m7V0I0Q2/dtsXdSzpEQdXBxLpnY0k632PMvWtvQT5FWcTKwFc+PzRN08ewwc1RNjU877a0rsTVPDdA
+Xbmss7VnRDY5Z927r9FjNTI1RdG+sb9vSi9GQ1+5yMTjPj08Xd3HwGtdOkNvd7i6xMtHPzpOysW30fdBOExbxbfAxEg+N0LCw7i13VM2NDvbt7GvyWM/MErdyqq950M2MjRXzLy061EuOURVvLi8v0k9NEL3zbrAyt04SVHRvsPFXVNDRcbEvbrvWTY4QFe+
+u7jZUT00TtXIsb7HSzczOee4ubTXTS4vPF25rba5TkAzPWfXt7LNZzc8PnfDubfKX0U1T2vHtcbOVzg9O2vFvLnvXTw6Xe+6uczjPz4/Tr/BvNlGMi06Vc21ur5NOzA1/8W9rsnVPTY+Sb+tsbLjSzI8a8quqbzTODYyQdPBuLlnQDE9SNW8wsRjPz4599PF
+vndfOzhMTcW6wMlRRDxVyMO0w99FND9O57a6ue9DLi5TzLuuuso8NzQ7y7q4s9f3Nj5KXbuwvM5IPzlj08m3yXdEMT5B773Dwv9CODVZ27663e86OEpVwLy7yVNIOUfJwLK0wk87PEBvwb230Wc6NEpr07a9wVk9NDn/wb231VcyNTtJw7e8vk8/N0n3yrm2
+ym88QUrbw76+11FAOFvnw7bHz1M5Pj/jwr2/b044Pl/Xu7vC40JAQVW7ubfBYzYtOU3jvL3BVz4xMU7dybbHzj04PUXRt7i87001TevNtK+7yD9BPl/Iv7m8Z0Y2Q0/Ov8bJWT1APuPNv73rXzs1SlfMvsDXTEA7SdvKt8HLZzpCVeu7vrvnTDMwSNnDt73I
+QTo4PtnBvLXVVzg9SvfAtb3FT0M5a86/tLjLXzU+Pm/CwL/RRjs2TVnIvNPfQDlDSszLwc1OQjk92826u8BdQEBF5768t83jQjdOd824wcJTPTU3b8O7tsn/MTc6Rsi7vb9OPTBBY867uMbrQEdFzL69uc9dQjdja8S5w8dZPD9J28+9xvdROD5b773Cwuc/
+PDxRwbuzuslBNj5M376/vWdJNDVNZ9m8z3c9OjtL3b2/wetIM0tf2bi0vslHQj3nwL23vN9HOk5fwbrDykw8Ozzfyr2701U7OEhVzsK+304+OEnnz7vBxU88Slvdurq311kzNEjvy7m/zj89OT/jw8K+50w3QU7bw7rE20c+Of/NwLe8yv89TVfHvcC+1UU6
+Pl/nwcXbazo3P07Oyr3LTDs0OU3Mvr6+XUE/SOPDvbTH0UU+V+/JuL3GW0Y2Q1/Nwb7Pazk+Pk7LxszOSzszS1/Pv8TTUT1ETsK7u7fJXUM9Y9/Du8PRWz4+TtvKvc3rTTtDUffHxcTZSj4/UePLv7/HUURNZ9W/vcTjVz9CVd/Z0eNPQz4+WefN093/SD5J
+W+POytHvV0pN48rIxr/XXVdd48zBzcbOT01VU+/b087bb1FVZ/9rVe/rV1tbY1lVXffdd2vZ1d3j59/N1d/bz2dVWVdjZ2/f2+9dT1VTUW/Vztv3U19bU1nZzdXf4/9nWWPTx8jTzs7vXffd2c3P2+9fSEhOY2fr3W9dT0lNTVVVa+tnX1trY2/d38zL29PX
+09/f1d/K2dvR429bXedf6+dv91lPS1VbTG/db2tXVVFTX1fR09nT1d3vd9PTy8nX1+drVV3r69fR399dUU9R///f0e//X1NTWeN308vn4/9nb+/T28vM/9vn/2tj3e93W1VbUU9TU+tVV2ddZ1lbW1VnUWvfd9nb3ef/3+fNzdXJz9nvb9v/39PdzttvVWPv
+X+/d9/9TTk1Td2Pj1/dnV1lfX+fjzcjX09vZ/2/Z49/3Y+9nXVVn52tnd2v3Z19ba2NT53dv929vXV9vb9nR2cTJ3WP/2d/Ixs3O22tVV2tj9+fr91tMRk1XT2/3Z1tNTUxTX1/Rz+Pb3evn2c7RyszXztPd///jd9/b39tvX05NWV/d2V1NRkNHW+fbzmtT
+Tkxj58y9v9HdSGNj27u8ue9dPzpVX8DCwNtCRDxMb8W/zd09QD1K48+6x85XREtO08a0wsxnPk9Cd8a5t9nvNkI/QNXKvN3jPDhIOmvny93j2T1XR03rzsPbv//d22PXa8bPx8b3y1tXR1HR3bvN21VBQzpn98i+13c+PjhR78a0xMJNRT1E0cCussJ3Ozo1
+79e4s89vMzo2VcK/ttddNzE6Ssy8tdN3TDFGRcK4sbNnWTM9d8ivsbB3UTcyXd+4trtfOjwuP1nKub/jODguOlvdtLq+S0E9Nd/Er662zztPPVXEtrG/yTs/RD7n27zP11c6SjhGXc/db9U7V0lI/9W/97/d99FfyefI1crG/9VIY11Zd//D69VdSFVF/1HL
+yufVSE89T9/NuMvFZ1NDQMvRtbvN1UJJPdnKxLnn/z06OUzNy7rrZ0ExOjjfzby9TU0vNj/jtrivze86NlPvsrKvu2dPMURTyLS2uWNJMjZIV7i9uu88Ni0/TL60usBATTtC58CvtLdbS009a2e9wMfOPUo6QVXZwt/MRE9NP2tZzW/ja0jrSd9v087dwuPL
+3d3L58lbzcrTy2/RW+dZU81f41VZSURbQNPnW1lDRTpdScq+78xTd0jry8yzxMPVXVdTysq5w9vrPEQ8/8rBuXdbMzQ2PtHRvM9bOTE9PsC8t7PR3TtJT9OytK/BXzg0Pkq8u7i+UTwuO0HJu7q6SkMzN0TXu7u2X0tIOm9rube3u0pXPkn/xrjCwk1NTENr
+d8bV21M+Tj5ZT+t3TFM7S0hb4+/HUd3vb9F3xse+zOvH98/V09Hb30vT3/fZ7+tRdz5r51X3UW9GU0tHyOPN1WNvW9fju7jDxGdVQlFjzL7O20tDOkBrz77L40c7OTrv576/4282OThdxLuvucpFQUNru7exttVCOT5Lwby4vV9CMzk8z8W/vU5EMTI4QMnH
+u8lTRzpLb728uLp3b1Fj3765vr9ZSkhLX9/BytFLP0U9UUzX52tnPExER2fTyOvRd+vT68jEucrN2Wf3UXd31+9r11tvT1VZV99P29dvXUldR/9r/8Pb1V1fVVvV0be7yt9TUUnd27m3zdtFQDhT3cy4zONHODQ6W3fBz2tONDU0W8q6ssPbQD4/77q3rrfK
+SD0/S8LDtbvjWTc7PmfMxb33TDc3PEzKyb3OU0Y8Tuu8uri592tLXevBvcXISUZARV1rwc/dVTtFP1Pvx8z3XzlBR0rnyL3T21NHd//NwLm/091XX/fd1cTHZ+tPU1VM7+/PX1NjRE9DSk9TV0/P62trW29329+7uMfJ3+dZ09nLucnN60s/SGtvv8nraz04
+NUZdz7/d/0A6N0Pvzbq7x/dLR07GwLe2ydFHPj1Vycm7xW9DNztFz83DvF9JOztH183LvWdMQkBTyL2+utdbUUz30bq7vslJSkJKd8a/ydFAPUA/TuvL3V9CNjs/TuPFyd9rR05j29G8uMjL62ff79fKvsvr31FjVV333+9Nd2dVWU1PZ1tFa9339///Z9tf
+977AwcjRZ2/3Z8bDz89vSD5HSv/K1dtnSDxASVnNyttjTDw/V1/GvMbIb0tEY8y+tLrI70Y/SdvOw73jV0I6QHfj18RvTDw3Om/j3cbfX0k/S2/Cw7u62+9VWePBvr2541tVTFPXycnKd0RDQEZv193fXUBAQUZL38/bz2dV///vzLzAycxv5+tf99PT99N3
+V1tFSP//VdnXX2NNR1n3VXfL5+9jS1nf39m+xM/R/2fj19m/vdnvY05PX2fny+tXUUxATUZO61dPTktASVlb39fv2dvvZ9/MzsbHx8jT69nV3d/V7+P3XW9vW09jX19dVVdjWUtVWV/r/+fN2+//2dnJxszDydt3/3dn49nT2WNNS0tJU2vn23dOTUtDTv/n
+393/W1NOXd3OyMHFzvdfW93OzcTD3VdOSlHv99/L50xJQ0NdW1ff/1dKRUZOa+/Jxc/3/2Nv18rGvL/T2+dn69XRxtH3X1tNV2Nr5+9dSEk/P1drd/fvX1NXTvfP0cjFx9Pj/+vP19PI0etfX1Nfd2Pf52NMT05ZX2P/3+tXV1dX93f/ys/X7933483RyMjN
+2dvvd+/f0c/fZ11bTVFZY/dfU0pMP0NLT2drd2tXVU//39fLz83d7+/Px87Gyc//b19r193d099rTU1MV1ld/2dfSUxMWfdv29Hbd2fd3cjLycLN42vr79vV18n3XU5VT093Z+PrV0dJTUhj3+fZX1VJTFd3zczEzNnrX1tn2c7BtLnZTEA8Re/Iu7/fQDk2
+O2vXwsH/STU1OkzJwrjJ7z8+SF2/trW4z09MV//Fu7q990c/R1nVxMjPTz83PExb0cz3WUA/Q2vZxL/N11VT/9XFvb3L/09PW9/Iys3nTkRAVV/b1dtfQkVCWdnPzudjQ0NT68e/vsvvZ1dd18XFvdnfZ0zOzs3N628+SUFEzNHNb0Q5LzhE673HzUc7Ojhf
+xLe0vstHPU5nurCyuW9ZNzxn57q2wc08OjI8/9G703c+Mjs/58O8utlXRT7/z7y1vcFTS1tRyMnCv2trQExdV8332V9ARkJv3dfRWVc/PVlrz8LP0V1NTU/Gwr271+NFSv/Mvb7G70lIO1Xj08rRXTo5Oj9f3c/NX0o9P1/jxr7JzVdZa//Hv7m7zudRa9nb
+x8rVZ05MT3f///dJQD8/Ud3R1W9VPT9Lb7u4u8TrXz9P3cm2usTdSTs+Vc6/uc9vPTY4Qu/CwL53QzU3SWvHvMXVTElEZ8nJurvP51Nr782+xMTdUUlP39nN1V9MPD1HSd3f428/OzpG59O9xtVPQ0ZPyru7t8zfTUdr18a5w9lNRz9O2d3MyGNOPEBGTd3v
+0eNLS0lj59HEzchv49HdwMvMyuN3WePH0cjdW0hDSmPdztvfRT88PPfPzsXjUTw8QGfBu7zG41U8TWfGtLm/2Uc9P1HLvbnB6zw3OkBdysvMWUQ3OUpb18TM705MSu/PyL7Hze9Z48vCvcHL71NOVe/GyM33Sjo9SFvNzdlnPTc0Qt3Fu7/TTD1AU8m6t7fO
+WUI/V9nGtr/PUT48Rl/Txb/3Uzo+RE3OzMrTSkk/Ue/VwsPOb1N378nBx8LTb19Z2dXLxe9nSEZXXdvr71dCPjhR73fN929LP0NKz7/GwtddR1Hrxry2vsdXSklXy8bFv/9OPEBNY8/T3VNAPjpRd+fH2eNPQ1vryLu9xdlrS1PZv7y7xfdFR0pfwsPCzE07
+NT9f18LJ0Ug8ODpb0ce+zmc/PEnvyre5u9NRPklr1b65wMtERUNM39fJzFVGOD1DV+Pj01tFSklv79nI3+djUdvPwsHFyOv//9vHycLP41lEUV/fzd3bW0U6PE1r3913XURFRV3PxMTE22dJVffNw77H12NRR2/Xz8fN51NCS073ztfdZ0ZGSWfV19PnX0dI
+Y93DvsXLW1dRX8HAvrzZUT5HX9vCv8PvSzozRFvvydNvQzo6PlnIxr3LZz5ATWfHury9911OW8/EvrzJ50pJUXfXzMzjU0U9S1dr32tfSj1IVd/Tycrnb1NZ2cm/v8LOU1ld68bIw8dvSD9MY+PI0dVXQT4/X+ffyPddRUFMY9fHy8ffW0xvz8zBv8/dTU9b
+28bOzs1PS0JO/+Pjd1tDPEFI69Xf3VtNQkXr0cfByt1PUV/bv7m5vctbQFHv076/x+dHOztL49XF0Vs6ODo/98nKwutPQUFnz8O6v8ZrTEtj2cbCv9VjQkhZa8/Tz91HRUJR993R53dLP0tj18/MzGNZT1XMycO8x+tRU2fbwsXDzllJQFXr58zb/0c9O0NX
+4+PVb089SVf/y8fO01VPS+vGwr6/z/dNXefJxMnL901LQ2/b2dlvTz88R1Hn29tvT0lBUd/Jv8LF409Xa9G9vb3J90NEW9nKv8fXSj87Q/fPzsb/TT09Q2PdyszTWUc/Ue/OwL7G1VNXa9HDxL/Ea1FIVePNy9HnTT0/RF/v4+NVQzw7TffXyMzrT0ZHb8XC
+u7rJ909j3dG+v77Jb0ZJXdvdyc9vQUE/RWvj999XRT5JX+/XydHbVVljzcbHxsz/W0r/08zK0edXRUlR587O1XdOPUJX/9PO0WtJRERVzMjFy9VRTV3Xyb6/wdFjSUzv0czEz+9LRkJRd93n/1NHPUVKZ+Pf3+9LSlf32dXJy9XrY+fNx9HOye939//V1dXv
+b1lJS11ja1ljU0xIR1dnZ//34/9v59vRzMzJys/j3dvf1dvbz91rW2drU2fv7/9fX1FXU0pjd2dnd29TV2Nn4+Pf083T7+fT2dfVzcvX629v72Pf3+f/W1VNTEtJX11VT1FOTVljY9fj6+/j7+fXyc7JzM/T193f0czf1+frZ1lZWWNrV19TUU1VV1FXW1dv
+VWv/3dvd29Pd2evZz9HX39//a+f36+fj629nV1VvZ2tv62NZU1NZd/f/6+P/X19r99fRzcrP3+fn3+PV0dHb52trZ2dn6/9nX1VOUVFXV2NdU1tfW11n7/fj6+vb2dvX1c7Zz8/Z19XX29/j/+//XV9fWVdRU09ZVU5RUU9VV2v/4+Pf3d/n2c7KzMrM09nr
+99/Z2efj42dfW1trb2dbZ1dNU1tbX19vXV9dW3f36+vf2ePb29PNzs/X3d/3393n3ev/a11ZU1tdU1VTS05n62dfTUlMWf/XyNPj91Vr18vAvcTOb11r2czDwM/vTURNVXfPzudOQz4/TGvXz91RQkRJW8/Fv8jZXUxj59G/vcr3V0tP99nJw9dvR0FGTvfN
+zdNbRT5EV+vKyNtrSUVM98rDvMXvXVNb48K9vclnTEhNZ+vGxutbRD5GTf/T0fdGPj9Hd9/JzG9OP0V3yr23t8h3RkFf28u9t8f3Z1FOX2Nj909FRFFr683XWUg3Oz9M07q5v9NNRE1O3by5vsTjZ//Z2cnJZ1dCPU3XzMfA60M/NjtRb8/Ax3dNS0hO79XK
+1etjd9nMvrzFz1VHTVPrwru/0107PERCa9njY1VDQFdnY9HvTUxDQ+/Cvrq742tTRmPZxbu6wNn3WVXn79/dXUxNTVvPxc7nTDY2Oz5ryMbFx1NCV1ld2crPy9tjzsPJyMprU0E8W93Ov73LV0Q4OUlT38nNd/9jU/fva3dXRE9n476ztLvJSj4/R2vNvLy/
+zE5LSUhTXVlbTUNZ29nO0VNGPDVAWdfAtrjN90dHW/fTw7/Mz8vTy8bP710/PUNP38e8x+9LODQ3QV/Px8nTUVNfVf/P1dndX+fHvr65vu9MOztN78y+uc1jTDs9Q0tf3f9ja3fv19PnY0RCREz327e1u8VfTEpJ58XDvr7XW2dvW/9dT01AP0zZz8bH90U3
+MjlL98i4u9d3TUhTW9vDx8/Z29XNwcPH1U1ESUlrxL3Bx2c8Ojg8U+fr091RTFNRd/dja2dRWcu8u7m803c+PE1n0b25v9tNPkFFRm/fZ1dnX2/Ryc/bSj09QE3Ot7W4xVlKPz9j0cG9vMf/d2tdd1lPUUdBXc3HwsLnVzswOkZT172+1WdJRlVOa9HL49/X
+zsS8vsPdSEVESOu8t7rDY0Q8NjtM79/O1VtRX1Vnd1NdY0ZT3cO9t7nK60E+TO/Zv7m+3VlJS0pGUf9VT1NX/8/M091EPT1BT9u8ub3BY0pEQ2/HwL+902fr59/R911bRT5K487Ev9NRODA1Qk/fv7/P50pITERV3//32dnRvry/w2tPTExT3by6vL3vTEA7
+QWNnd8/rTFFfW/drTE9BPEvVw765vM5TPT9Z98+5tcXOX0hTT03j/1NRVVFZ3dHO1Us/QT5H28C/vsdnVUZCd9PVxr7T59fXz8zvZ2tHQFvRy8XB0e9CNj1HSu/FxdtrSEdOSl3v/1tjb/fOwb/Az2NVTUr3wLu7vs/3TT5BT1//1dlnX1FTa19RW1NBSXfb
+x73Bxt9HR1dv2b+5vsv3X29ZU2f3TU5PT//T0dXvQjw+PEnrwsHD2WNOQ0Zj1dPHw9XZ0dPJw93v3UdBT9vMv73M/z41O0ZLa8TG2W9IS09ETe9dVVlfd8zGxsh3T01MWdm8uLq802dMQ1Hb3+PO61FTY2/b90xOPjg+Y9XFv8PPTjw/W+/TurfAyndVb/9r
+z9V3XVlRX9/X1d1IPz08RtvHxsHOVUtFRG/Z287I63fb0crJ2d9nSUj/zsu/wNH3QDpHU2PTwdf/VUZNVU5361tMT0tZ08fIyXdKR0NKzrm6u7zdX0xFZ93Zz8rdT1tnd+NbV1E/Okvr28y/zt9NPUpd/8u6usbPd2/f/93O3VNZV1XfyMvTYz07OjpPysLF
+xOdRRTxL99vTwsRva9/VyMnZ1e9FTO/MyL2+zlc6PUdNX8vC0/dTSVdXUWvjTk5TU3fMyczbSkJCQlvPvL28wOtjTE3nzNXLyHdf59/X1W9bTTk8R3fbycbXXTw4QFF3zbvCy9N37/9Jwcvd//dv98/LzdFdRz87Pk7Xz8rH91FJRE13Z/fP22/j0czL0+fr
+T0hb08fDwMjVU0JFTllj18rb52dfY19TWVlTUVFb78zX3+dbSkVDS+fO0cO9zN/3b29dX93G1dvNz+dnTlVbTEtn92d37+/3WVFZV09Z7+PVycfGydXvZ11j993XyMfV52dVSEdIT19bWe/f719j/19RUWPvb+vMxcfV3dv/Y2Pf19nOx8XV/29jT09VX2v3
+39nZ/2tfTkxNUVld593V7293V0tOU1lbb9PKytPOzOdna+vr79XLxMzf4+9RS0xbWV1r7+dbU1dPTExVY2v348/KzsrN3/dvY2fv3czIz9XnV0tKSk1TXe/rd2v3Z1dXW2tjb/fX09XTz9fn7+vr//ff08/T0d33Z1lVV1db/93X2d3va1VNTE5Vd9/n3+Nj
+WVtXV11na3f3693Rz9PX3e9rX3fn49XKz9vrY09RVVNnd2//5+9nX1VTWVlj49/TzdXPzdv/9+t39+vr1df3d+t3WU9NTEtKU+vj5+P/U1NTVWvv387Kz9XV2+fZ29/b3+vf593X3etvW09PVVn35+PX2f9XV1VRUVn/4+vv3+NrW1NRV1Vf39XZ1c7V6+Pn
+69/r49vX393X1993Z11RTVFVW+vd4/dbTUlMVXfdzMnGy93r/+//99vP2e/j2/9rZ2dnU01OWVNRZ+vjd2drZ1Vbd+ff1c7Jye/dX19nWczIycb/909b/1nZ6/f3U9X3X1tNY0//1+vMTlVXRF1b3b/R301NRUNvzb6+09tFS0xOw7+/vv9bPUZV977CyGdF
+STtP69W/4+9LPktI67++v9PfSElfZ8TFzMlfX0tK5//bY1tVQVNb991n/0lNUUnP0czJ/+dZX9vXwcPJ21tnY9/NxcR3XUM9SVXdusHPX0k7PEfvv7rO20hGREfjy8zOY1E+RV3dxL/M1VdNQF/Mz77G0dVVX01f22/X3//vW+v391lJTURV5+/EzN/vTlVP
+Z8jIv87rV0lT/8y8ydtIRz48V9PJu+NfPz08QmfGv7/X70tNVWfKyMrD5+9bd8/Tzev/WU9dVd3T691dZ19R53fd/1djS13f2cXV50dBQULvycm72eNKP0VO3b7DwOtvRktb/8HC1dtMVUhO0crByGtMPUZDY8vNxMzra0hjW//L48vZd+Nv09v/Y05bR13d
+/85nV09CVU1dzN/VX1lRTtvRw7vT3UhTWW+/ury+d1E7P0lrxb/K41dNPUdd67/R2/9IT0lfzM/F319GP05R3czZy2NnW0v3b+PT687v39Pjze9nU0xvY9HCy8lRTURFXWfRwePvSEhJX87Jv833XUFMV926wMTVTz86R1/OvcnZTkVDPlf328n/d05RZ3fR
+zNPTZ29Pd9HPwtPX2VNrW3fP/+dXX2NO2dXZ11FTQUt3/76+zuNKRD1R39O/yt9OP0lK38bMxmNZRUNfd8m4ysxXTD9Cb9G+uc7PTUpJT93f085nd0tX293V7+NjTndf1cXR0W9nVU7v79nba2tITVVX29/jb0xFQl/f0b3Hz1VNR0rjwr+6xuNMSk1dysHM
+zVVPQEhn48bI3e9BRkJb1c7EzutnSVdRd9fr2e9fd1nf49/vVWtMUdvjxsjdXU1XUf/JzMbdb0xKZ+vMwdXfSkpESd/NxsHfbz9FRU3MvsLA31lCSlf3xMXV2U1OSFf33c9nX0xFW1fdz9nZXWdjW9XVztfv31v/29nEz91fTFlMX8vKyONjPkBRTuPBystr
+VT9Gd+fGv9PfSk1JVc/Myc5rXUBMUV3Ly87ZVU9GZ+/bxNnd61lva9PR2dFdXV1X59fO12tnRktda8nKz+ddWUtd3dvF0+9KRk5R48/OzVtPPUFZXdfCzdNXTkFI38/Dvc/fWV9Td8bJy85vZ01fb9vN3+dTS09L99vn0W9rWVN3Y93d699VY3dn09nV/1VZ
+R1fb28bM3U1MU0hvzMvF2/9ITGdnzMTO0VtdR1HXz8nI3/9CS0VOz8rJyvdbR1tf78nV1eNVWVH/39vRb19KRltf29Pd3U1RVVPf383T5+NbZ+fjxc7RX1djU/fNycff70dFT073ydHRa1FARl33zcnb505TTWPPy8rTa1dAU1nfzM7TX01JRGdv28nd32tX
+X2PR0c7Lb3dfY9vOxs3Z/0lRUWPT089vY1FLX//Ry+PrS1FdY9PKzONbVT5MZ//Nzdl3T0g/T+PVxsvXX1lbWdXKzcvvZ09Z59fJyNXjS1FNX9/j2d1fX0tbW//V9+trTl1j3c7L11lbSEtv58vFzedKTENJ79nKy99ZSlNd58XLzfdjU0l3zsu/y9VfU09O
+39HVzXdnSVdfXdvX5+dVWVdv0dXO2V9RRVFf28vT21dLTEdn69fR/3dMT2/rx8XK51VZS1vTysHH0W9JTk9fzs7M2WdHRU9n58/b41NVTFPfzcrH419GT1Vvz8vX21dVR1dnZ9/vZ19NW1/bzNfZd1dTW+vNw8bP3V1XX//PyM3bZ05LT2vZz9HfUUpKV2/R
+yc7VX0tFTv/vzc3ra09ITGvd3dVvWU9TZ+vPx9PbX05VX9nJxcfdZ1FOXd/Iyc3nU0tJTffVztf/TUdKU/fPycvb/1dLV2vjysvRa1lOT1tr6+Pra1VbX11r/9/nd2dn9+vn39vj49/Z3dPf9+dnX/ff3+vvWVdVU13f1dXnb11XU1n31czd93dPT1lv39nd
+d19VTFNn3dfT1etrXVl349/by9nvb2trb/fj183bb/9fV1Vdb2/n93d3Z1tjd+/n09Hf92ddXW/n39Xbd2dRSkxRY/fT13drV05RW+fTydXf3/9dZ+PVzsnN0d1vXWP/7+fZ22dbU1FXZ3fr1e9rZ11XXXfr49Xn72tbV1lrd//d7+f3XV1f9+fr29/f73dn
+b+vv99Xd5//3d2fv9+/Z//d3Y1VVXV134+vn73dnXWt379fX3+/v/3fvd//n/3ddW1tfb11n72//d/9na+9v69vf2dvn7/fb393P2dfjd2NdX11d/2v3d2NXVV1da+/r2+v3a2N3a+vT29nrb1NTXV/v2dnd72dZWWv3383X3e9jX2t3993R3d//Y2dn9/9v
+6/frb1tZXWtfb93f3efn92/vd/fZ3dfd91lZX1/v11v/Rkg+SNHCusr/QjtDQsq5t7//UzpJWce4u7xbZ0E/a9u+xsdLPkY6Z9m+v9PbOUA6Q9e/t8TMQD9EPtvFucDPUThJPXfLv7vV3T9HSE/PzLzR2VdBX0zj0cbG3dlDU1lTY1/PY9HrV/dLV0zd58/B
+381MTE9X2d+9xs3jP0E9Tl/FvMjRPkA5QW/Et73FQjs9P9PHtLXCUzk5Rcm7tbDP7zc2OD/DurnBTTcwOUDMs7a8U0AzOUzItLK6U0U1MkrZubK3WTw7Lkvvu668xD8+ND3ju660yUE7OzzTvLO300g2PDd3y7y61185QD5byb+3zd1JQUxF0cW/yOtVO0hE
+a9vVzP/rSk1RXfdvyevL12vnW+tbz9fTvdfN71dJV+vvws7nY0E/PFlvxsDX5z9DPVfdzLjG0UM+PkfPzbzC1WM8QUPbw7q5z2s6Pj5dvLm1x1U3NDxNvLa0v01BLzpDv7a1vUs/MDZH3bSzt9VENzVG57Ovtck/ODA9W72yu8o/OzU5Xdm1u8RMPT06d9e5
+uMPLRklFWcy+usfOSkhKR+fbyN/nT0JPSGdf5+dv21/vd9/Xa9lf3dHj32/rT/9d68XZ0/9nSExjY8PJz+NXTT9bV8+9zdFXTj5RX+e8y9NfRDxCZ2+9xM93Q0M+3c28tsvnQUNByFfXudVfQTY6SMXCtsJbSTQ9R868tbnvTjY8R8+0srDNTjo1R2u3tbnL
+QD4zPU3Tub/LPz45PlvXtr7C70FFQl3dusXK1z5LRVHdwbzXzkRHV03j3cXv22NJa2PV28/3a9tTb//r1///Sut3a99351FnSWPL48rO42tRVVHTys7La1VETUjnw9XMa04+TVHru83K/0k9Qf/duLfG1UdIPuvNv7PL40g7OU7VybfMY0U0Nkbfxra85002
+Oz7TvLWx01c3NT53vLizx1FEND5TyLm5wEhCNT1O1by7vl9HPkBP2bq+vN1GSj9R77/DzNE+SUpN69G/3cpIR19R39fEd9/vQldLXe/Ra1PXT2v/a+Pr1U3d71/R79Pr12vnx+PN1ePra29Vz9Xj21lRRVdJb8j/3WNKP0Vfd7y/y9lLSj//68a50dlPQz5b
+0cq3yd1POzlH38q4vddbOTo868e4ss//Ozg9Z8S/s8NbSTU8SdG+uLtdTzc8SeO8ubjTTj07QVfDwr3TRkM6RlPBv8PJQ0pDSmfXvcrGS0ldV9nOu87M40djV+/ZxudZZz9VV13f19FHZ0ZK53fN0chXZ+NN3ePOz9NvTd9v59Xd13d3SGvjY9n/71NjWVnJ
+49vfZ1tM91vPxdvTXVNDXf/Zv9PTa0dCTffZvL/O40U/PmvVv7rOdz46OU7NzbrF7084O0bdyLu8/188PUrrv7q2xP9EQElvw725yVtEO0RRzMnFzkdHPD9P78HJxU9JS0r338HJy9tFVU5r08HI3eNFT1Vf28rGY29HRWdd1dPIb193Rvdv2c/V6073XWff
+99Xn509X22PZ29Xv72dPztPV0e9vTVdEY83jzOddR0pOV8bNyc9VS0JPXce9x8djTUZR68m7vstbRz5E792/vtX3Pjo6S93LvNnjRTw/S8zDuLzfTz0/Ts7CvLrjWUA+S93JxL13U0I9SlXLycHvTEs/T13PxMnJTktJS+/Ows3OWUtTT/fVxs/da0lbW+vT
+zMtra0dPW2ff3dFXWVVO72/f2dl3T3db59vZ0d3rTtvT083T0XdvSVXN0czb91NJRkXb29PZWU0/SEnVwMbLa1dJSVvXwcHF51tJQ19rxb/Iz0xBPEZv07/P01lCP0Xr08DC029FREXv08nB0e9ORUpv0dfG1eNdSFlf0dfLzv9vS19r6+Pj0WNfVU///9n/
+2+9bX1nr79vj5+9b///Xz9HN499VY2vv19fO/3dPS1tVd+/n71VOR1Nb/9nVzndvV13//9nOyeN3a1v/Z+PMzd9nX0xbVVnd62tZd1FTZ//R08vv/3dPa2vjz9F3zt9TX2dv19XP93dXVWNf393Z2W9fTVdXb+Pv12dfT1dnZ9/f1eNjY1V3b9/R189371/v
+393O0893a2db///f2dtvVVdNVVdv5+vjV1lTWWvvz87P229rV2/rzcfNz/93VVljZ/fn2/dTS0lVa9vX2dlbVU5Zd9vT1c5jX1db9+fR1dNvVVtV//fb0dvbVVtXX+vd1dvnVU5VV+/r19Pfb05ZVffn2c3b3VtbX2/n18zV2+dfZ1/v29PO5+9XW11f6+/f
+d2dXTFFTZ+/n4/f/V2Njd9fZ1d3bZ2d3b9vX19Xb91dbV2tvb9/r411VU1N3a+fd29NOY29TZ03ryM7D1ddMSVFI38PIvdV3STxJRme+xLzZVz06SUzVvMbBTlNJPuf/w7vT2z5KSVfIxr/Da+9DV//rv7/M00RXQE3Rz7/Oa1M8UUhvzNvRS1FGQdvvxsLj
+d0NLa2vEwcbTT1VDW87Tv8jjWTpIQFfEy7/fVUE7VVXNusnLR0c9Qu/GvLrTaz1AQ1fLvcHCa107RUxfyL/JzVNVPk1r48rn52NRX0v349vXd/fj/9tj5+9Zd2fnw9XI/2dLSmPfxbvKy0ZLPkD318G821s7Pz1Oz8jEw2NXPUld/8a/v8VXT0JT49e+xM9r
+SVFDb9nn0V1bRkzvd8/Xd19JT3fjyc/Md1NRRvfN077X60pDSFHVvsbCY1U6QlFdwcHG0UdEOkR307q80VtBSUNbxsm8zndRQVtR773Mv+NdTk93/8/I3dNPXVdT42v3d1d3UW/bb9ddU1NT29vKy+fbREtRWcLKxcZrUz1GWe+8w8lnS0Q7TefbvM3ZWURE
+RV3Ny7vb90dKXVnZx8/GU2dNT9lv285390tnY+vPd/9TTl9T18/VyllTSUbf48m9zt9KRkBVy8e9v99fPT9GX7u+usBbRDg9Udm2u8JrRD43SN/TucbbUT5CRF/CxrjJY0ZDUVXTvcvEWVNLSHfvz8p390ZMY1nT0+9vUVtVd9Xnz+tbV03v2dXE0dlbS0tT
+z8fLxvdfP0RRXcnCzM1RSD1HZ+O/wdn3RUlIW9XXwc7vd0RbWWvO18lvW/9V2+vn02frY2vT583r929KX1tnytnPd09CQVfX073L10xGQEfXwcO722s+P0hvwLrEw1FLO0Jj48O80d8/QDpG3c6/vfdZPURJd8TIw8tXT0BZ787Az9FTRltK48/TzVtdTkfv
+b9nP629GTVtVy83GyFlPP13Z37/KzmdJR0Frxsy+zedDQExOz77Kwl1TQkZdZ86+2dVJS0VKd/fMyu/3S2tna9Xv2dNb42ff4+PZW29bT9nnztNrXUFLXWPGzM/3UUU8VdnTv8nOUUlGQ9nEy73b60dFS03Tv8vGW1E9RGN3x77f60NIRVnO1cnKZ1FBWWvR
+w87NY0xTS9nTzMX3/01GZ2POx8rNUVNGR9/bwsXZWUBOTl3Jz8bbY0U+Wf/TwMnLUU1GSd3Lzr7d50hLU0/ZyNHLWVdETG9ry9X3d0hZTm/d69PnX11O6+/Xz//dVVPrb87X2+dTY1Nj0ePP729KRFld48fZzFldSEbr1dHB09VNUUhLzsnKw/9ZQEtZ98LD
+0dlNT0Fd39XDzudNQ05M38/VxmNbSUj/b87J19FTU1FT09nIy293SE/v78nT1V9LWUtrz9PH7/9CQmdb08XTzFVXQ03X3cvA1ddHUUhTzNPGxWtVP05Xb8TV0f9OST9nZ9fE3d9RSl9P1dfXx2dvZ1fjZ9Pb49VRa29d0XfVXVlvUd/T585j701N92fVztvR
+TltBStnjycXr/0VRS1/FzM3OY2c/X2PXws7NY0pXSNvd3cT/91FHZ1PZ2dvPU1NJStn/yMvv70VOa3fH08fvV1FCWd/TxNfXSEVPR+PL08X//0JFX1vVwszHXV1DSdvZyL/X50ZRT13L083NY1NBX2/nyc7ZY0tXSePX38jrY1lHX13dzuPTZ1FZTevrztN3
+705V/2fO29PVVV9TWdvfzNvjW0hfa2/R389bXVFG59vjx+fZT19ZXdfN185vZ0lb73fO0e/nSU9KW9Pr1dlXU0VTX9nI3ddnTFdM29HNw+/vVUhnb87LydNZU0hM79vHzNfvRExRV8zPyNNvSz9N/+PK1c1jU01Hd9vbx9fnT01VV+PN283rZ0pZ93fXz9/b
+TT1RyNXv2c5vd1Nna+/na91vV/9d32v332/f/1/Z/+dr999jd/dr32N3XWfrU3fdZ+dZd3fv51vv2WfjZ+Nv69v31+dr3Xfna/fXd+/vX+tfd2/j12d3/133W9/n699f9/dZ/2vb7+d3W3dnX+fv22d371drb2fX699v/29VZ+fv1+/fa3dnUevj99v352Nn
+W13r4+vX6+tXd29n4+fv1XdnU2drY+/f5+tja1d3X13d5+/vY3dbd3d30dvv4+/vXefv79fv6913b1v392vd7+/vZ3dd52t34//j/2/3Xe9nZ913//f//1vvd//b/+Pr/3dd5/9r3Xfr/29rW+dvd93/92tv92vja/ffY3d3/+f/5/dr62Nv39vb5+NbRlNX
+/+/n72/nWV//b9/f2/9j/1Nn7/fV2dfnY3dbY+Pn1c7Xd1t3V2fd69Pf92dZb1dj2/fX43dfXf9b/9133/f3b1v/WV/fb9/j629f/19n43fn9/f/b99fd99nb1ln08TJb1NNRFH3x7/L/0ZBREvTwb/Hb0w9P2PVwrvEb0xFPVnHxLzJa0o9RU7vvsPK70w+
+QFPfxL3N609GRlHLw8XHW1FIR2PTvb7dXT8+RWvGwr3dSUc8Ru/NvcHVVT5ARWPCvb7TVz48SG/Dur3MTT46PufFubjOUz09QFu9ur3NT0M6SPfPuL/fW0E9RGPGv7zKTz87PU/JvsXDZ0xTWe/Pw89VSD1H38y7uMhLODY4S9PFsrzfVz0/SV3JvcP/XU9J
+18TGxvdJPUBEU7m1u71fPDc5Rue5vMXPTEdHXd3Vyf9TSkNn18i5w/dAPDtH2cu6uO9fRz5JXc/KwdVJV0tPyr/EzV8+Oj9L1bW4v99COTlFZ7+6xdFMRkpVzsbG11dHO0rn07m4zW9ANTlK2cS0u9VdPz1Jb93OzltRTFHOvr6/3Uo8Oz9vvLu4vldJPT5O
+98rIx+dKWWNvxsTXX0Y4PvfRvbC7zEs4NTx3Vb22xd1LQkpX39fTa1FLRuO+v7zFWUQ6N0Tnv7221V9IQERP3dnX209b59nHxMt3QTo6Rtm/srTBVTs3N0vbyrvJ401DT3fTzc5rR0c+V766ubnbSjg2O0/AvLW330c+QElrzM3Pa01TY9XGx8lbPzo8TeO6
+s7rIU0E8QE//xMLb3VFO79nOz+NRPz9AVb24t7vfPjQ2Pf+9urbDWz89SGPXycz3Q0JNa8W8vsVVPjU8TWu9s73GVzw5PlHfvsfV51FOW9/O2d9RR0RI3by5uMpbPDc7T8i/u7nrV0VCW/fb09tOP01v0b67w2tENTRGZ8ixsrrjQTc3Q3fJvcXXWUxX99HL
+3WtGQTxNxby5t89RPTU4Wc/GuL3ZXUlJUWfn399OR2fjzL7AxVk8NTdL37y0t8RRPjw9U9vHxdV3S03r08jG40lCPDt3wLmzuc1HNjQ6b8K5tLvjS0RLWf/V2etKSVf3ycXGzUo5Nz5R0763vtdNP0BIVdvB0dvrTGfbzsnTY0Y/PUvXubm5wl86NDhLzcC8
+uNlPRURda+fP10tATF3Tv76/3Uo2N0RfzbS4vs5KPT5GXc/K29tjU2PdzNXnVUE9PFHFvbq601M9OUXnzse6x2tZSWPd3dvVZ0JBa+PIu7zJbz0zO0dnv7S5yFM/PT5T2cnF129LU//TxcvfWUU8PPfCwbm/21k8OEr318e6yGtXUWfr59/fXz5Ka9vDvr/M
+STk4P1fVubi+3UdDP0r31cfj519Hb9XMw81dSD43Rd2+vbe97z83PEzTz7652VlKUffr2dfbTkJMZ8++v77fQjs8SFnVub7E40lFSUxnzdV3601O2czHyd9RQj08TsG9u7zRSTo4QuvLyb3IZ01Ga93f089nQUFT/8K/wcRZPTY+S2O/ub/FY0RCSFfjx9Xj
+71Fb38vHze9MQT1B28K+u8l3Pzk+X9HVxb53XUlR29PX1dlVPU9n2769wc5JNzpDW8u4u8JrRz9DWefIx9//Rkpn18XF1WNJPzpZyMe8vtldPzpHb9fOvcpdUVNr39fZ2f8+RmPrxb+/yFU8OEBX17y8wttIRENP79PI6+9XQmPXzcTK/0lAOEfJw8C5xu9B
+Oj5L1dnCveNPSlHr59PZ301BS2PVv8G/40Y9PU1jx7q/xmNLSU9378vd9+9NU9fKw8rbX0M8PE7Gv7y9zE86OkBr087AyV9JRmPd187P6z09TWfHvb/Ab0Y2PU5dxbnAxmdGQUhv38TL519HS//PxMrNV0M7PWPOx76/0Uk+QUz399fA53dba9nb3fd3Vz9R
+69fBvsLVTjw6TmfVvsLLd0lHSe/bzs3vWUVFb8m9wMz/RTw5SsnAvLrL/0A8QWvZ0cHRX01Nd9/VytfnRD1MX9vBvb5vUT09SWfMwsXRWVNNV+vd2etjTUVZ1cbDy9FvTD1B59PRw8PPU0VPY+sf
+--seconddivider--
+This is junk.
+
+What will it do?
+--foobarbazola
+Content-type: application/atomicmail
+
+; This is a a MAGICMAIL message that contains a survey.
+; If you are reading this after receiving it in the mail, that
+; means that your mail-reading program needs to be upgraded
+; to know how to run MAGICMAIL programs.
+;
+; If you are reading this in the hope of editing your survey
+; before sending it, you should start at the END of the message,
+; which is where you'll find the parts you're likely to want
+; to try to edit.
+
+(setq newline "\n")
+
+(setq global-survey-qid-ctr 0)
+(setq total-questions 0)
+
+(defun nextctr ()
+ (setq global-survey-qid-ctr (plus global-survey-qid-ctr 1)))
+
+; USAGE: (survey-multiple-choice "Which is best? " '("bar" "baz" "ola"))
+
+(defun informative (p)
+ (strcat
+ "#"
+ (int-to-str global-survey-qid-ctr)
+ " of "
+ (int-to-str total-questions)
+ ": "
+ p))
+
+(defun survey-multiple-choice (prompt choices)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (car
+ (car
+ (select (cons (list "" (informative prompt) NIL NIL) (cons (list "" "" NIL NIL) choices)))))
+ newline))
+
+; USAGE: (survey-short-answer "How are you? ")
+
+(defun survey-short-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (getstring (informative prompt) "")
+ newline))
+
+; USAGE: (survey-integer-answer "How old are you? ")
+
+(defun survey-integer-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (int-to-str (getinteger (informative prompt)))
+ newline))
+
+; USAGE: (survey-boolean-answer "Do you think I'm sexy? ")
+
+(defun survey-boolean-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (cond ((getboolean (informative prompt)) "Yes")
+ (T "No"))
+ newline))
+
+(defun mapcar (func args)
+ (cond ((null args) NIL)
+ (T
+ (append
+ (list (magiceval (list func (car args))))
+ (mapcar func (cdr args))))))
+
+(defun surv-pkg (q)
+ (list (strcat (int-to-str (nextctr))
+ " ("
+ q
+ "): ")
+ (informative q) "" "s"))
+
+(defun surv-pkg2 (q)
+ (list (strcat (int-to-str (nextctr))
+ " ("
+ (car q)
+ "): ")
+ (informative (car q)) "" (car (cdr q))))
+
+(defun formatfillinlist (lis)
+ (cond
+ ((null lis) "")
+ (T (strcat
+ (car (car lis))
+ (sexp-to-str (car (cdr (car lis))))
+ newline
+ (formatfillinlist (cdr lis))))))
+
+; USAGE: (survey-simple-form "preface" '("foo" "bar" "baz")))
+
+(defun survey-simple-form (preface qlist)
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar 'surv-pkg qlist)))))
+
+; USAGE: (survey-complex-form "preface" '('("foo" "i") '("bar" "s") '("baz" "b")))
+
+(defun survey-complex-form (preface qlist)
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar 'surv-pkg2 qlist)))))
+
+(defun ask-question-set (qlist)
+ (cond
+ ((null qlist) "")
+ (T (strcat
+ (magiceval
+ (cons
+ (car (car qlist))
+ (cdr (car qlist))))
+ (ask-question-set (cdr qlist))))))
+
+(defun qcount (l)
+ (cond
+ ((null l) 0)
+ ((eq 'survey-simple-form (car (cdr (car (car l)))))
+ (plus (dcount (car (cdr (car (cdr (cdr (car l)))))))
+ (qcount (cdr l))))
+ (T (plus 1 (qcount (cdr l))))))
+
+(defun dcount (l)
+ (cond
+ ((null l) 0)
+ (T (plus 1 (dcount (cdr l))))))
+
+(defun handle-survey (to cc subject qlist)
+ (progn
+ (setq total-questions (qcount qlist))
+ (sendmessage
+ to
+ cc
+ subject
+ (ask-question-set qlist)
+ NIL
+ 0
+ T)))
+
+(defun maybe-displaytext (t)
+ (cond
+ ((equal t NIL) NIL)
+ ((equal t "") NIL)
+ (T (displaytext t))))
+
+; This is the user-generated portion of the survey
+; Be careful in editing this, because you don't want to
+; mess up the LISP syntax. (Be especially careful if you
+; don't know how to program in LISP!)
+; Note that lines that begin with semicolons are COMMENTS.
+
+(maybe-displaytext "Thank you for your patience. I would now like to ask you four questions about what this message did when you tried to read it.")
+(handle-survey
+ "nsb" NIL
+ "Re: Multipart test"
+ '(
+ ('survey-multiple-choice "Were you able to read the introductory text?"
+ '(
+ "Yes"
+ "No"
+ "I don't know"
+ ))
+ ('survey-multiple-choice "What happened with the audio part?"
+ '(
+ "I heard it fine on my SPARC."
+ "I saw a message saying I can't read it because I'm not on a SPARC"
+ "I saw garbage"
+ "I don't know"
+ ))
+ ('survey-multiple-choice "What happened with the picture?"
+ '(
+ "I saw the picture just fine"
+ "I saw a message saying I can't see it unless I run X."
+ "I saw garbage"
+ "I don't know."
+ ))
+ ('survey-short-answer "Please enter any other comments: ")
+ ))
+(maybe-displaytext "")
+
+--foobarbazola
+Content-Type: MESSAGE/RFC822
+Content-Description: Yet another level deeper...
+
+Date: Thu, 24 Oct 1991 17:09:10 -0700 (PDT)
+From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+Subject: Monster!
+MIME-Version: 1.0
+Content-Description: I'm Twying...
+Content-Transfer-Encoding: base64
+Content-type: AUDIO/X-SUN
+
+LnNuZAAAADAAANeKAAAAAQAAH0AAAAABRWxtZXIgRnVkZCB0YWxraW5nAAAAAAAAVFZYan39
++Flh7N3X6t7f2dzc811za3dsaWhl4tvY3ebd4ej/fnn+cGX+YWNubP3t5d7W2+b3bVhZVVlX
+Vmj52s3JxcrLz83Q3d/x/V5QTFFsVllraHPf2mdqXVpcTUZJR05YXV5mYlBqyrmz3y4qKEq0
+qqK4UUU3SGho4M3Gxtpy4djHwc/sVVrTx8PH3+9dVFdNR0VIR0Q+OTpE8cfGXEJI6L64vMbO
+z9LrW1dXaO3xaFFt3s/qS0VJUl7r6d/7e/LYxsPJz9Hf+NjKy3NbRUBLvp6nNyIXK7ufmbcz
+LDNFzWBmyNTJWk9e2sPJUz44QmfWxNlPSEVOVlvLwL64v/JCO0Hbv7q92N79Y0c/TtO6vdxG
+NzdCTEY+Ozc/WMu5vtT29+HZ3MnKz893Tz89S8S2sbK8uLreOS88v62txDswOVbc6kxDP0vp
+3u1VQ0Q/Pkk+PEJPSD87NkjFtbrKx7a0vTQoLsGfnaK9X368r7TcQTxQyLe9UDc3Q1dtpZ0j
+EAkOPKSMks4iJDavpLq2TGBKNTo3TdN4Qjk/t6egorfb7Mqyt66kqjYXDBVVno+cWx4ZHjRz
+xs9BPz1Nzr23t+dKSNSyr7X0zJ6QnBYEBBPEj4qiMiI/rqSwxd3JtupKO07CxD8qJDi9qarq
+Lz+pkpAzCQEJNpaIlMopLV2+1zA8O93sPjg94bS1YTs60a6loarH256SrScJCSmni5K2IBwk
+S89JPy/lx73J4tGxr7DpMS86tainmKIiFQUNMqCMkKUuIBouMUJAMUtPyLa1uKutq7G6vb+5
+uKmamJ8cCgYQvJWNnMUhISAsKCcrMXu+tL7Bzbi7tsju3bihmp/GIRggbKCeqmAwJiUjJCgq
+OUFISOi8r6yxt8C3oJqf3B0XJrKXkZrMKhwcHyQpKS0uNTpU1sq/vr/JtZmRl7IUDxjdlI2R
+pkgfJCAmIR8kL0/GxHPqbcnNy7eYkZq5Dg0btoyHjJ5fGyUdHBEMDhg8tq5XPDJuwK+jnZeZ
+pzsgKO6ZjoySoNYvIhgSDQ4SHzjf51FMRGHSu6uempmdqre+uKmjpazVNSkhHhwbGxwfKTJA
+UVjYtaensmg7XKyZkI+Vna3MPSsfGRYWGB0lKzU8T9SvoKW2MSYsyJ2Vk5icoKCmtjkdFhYZ
+HR8iKCw1RtO2pJyirMfCtKumqK29u8rGynhJNCwmIyMhIB8gJCw93LOus7y+tKykoZ+gpKit
+srvPSS8mISAhISMiHyAsPXnIwbWtpp6cnJ6gpaWio6a0YzQnHxwaFhQSExgdJzjvvayjnZiX
+lZWXmZ6jq7bZQzAlIx8cGhgXFxocIi9Nx7apoZyXlZSWmZ2gpaqy1TwpIR0aGBQUFRgcIS5U
+xLSpoZuXlJSWmZ2foaSpuk4tJSIeHBgTEQ8TGB0mNOmvoZuXlpWUk5WZnqe230w3KiIeHBkY
+FxcZGx0iKTfqs6efm5iWlJOVmJueprHKSTYqIR0aGBcXFxgbHycxRsmvqKKenZybmpudoaqv
+tb9jOSoiHh0cGRYWGBwiL0nBq5+bmZiYl5ianaSuvs3hUDovJCAhIR0ZFhUXGyIuTL6ro5yX
+lZSWmJmbn6m4ZD4yKighHh4dHB8mIR0eJi891LyuopuYlpWYnJ6hp7TPRDEqJiAdGxscHiEj
+JSo1S2B+xL+so56cnJqZm56iq7C31zorJiIeHh4dHh8jKCouND5Wd72vpaKkoJ6enJ2kq7PG
+z1AxLyglJSMiJCIjJyorMzhE9su7rquhnp6dnZ2gpbHE4F5HNzEqKCUkJiUmJCUkKzdJ4tW8
+ua6ppaGinp6io6assLznW0IzLSYhHx8gIyYoLTVBVG/cwby5ta6pqaioqq6xtb7G1GROQz46
+NS4sKSosLDA0NztP/NXAuK+urq2trK2wtLzO1Mr8Wl1KP0Q/Ojk5NjU0NTo+Q05ledzYxsDA
+xL+9vbvAyczY5F1ZTEQ/P2vR2U9ISEVIWXXlxsbLzsnd2cjb3t7a4NfZ7+9fWvRJSk5DS1Bn
+9HbZyMnOzNHe4dXAyNzscEk+UU5TaMrN6ONjaVtSSUhMUVVf5+/Z2H7c3XdsVmFWT1N53Vpa
+XV3tZtHWzr/K2NztSExI98vCxby3yMPdz1NLR0nmSjhIVGVhbtbl9VRiU15JRk5GT2p3W2Ds
+0UG3wbzD0cn+XD9WR2FXSFrg4cO+u7/Gv8LJ2eZdTUlNQEU/R0hKWFTqbtZx+XRc/lVVXOhf
+bf3Szsu/xLe+u8fXyNPQa9fobHde0kx9UlzZSl08Tj5OTEdWRtFE90JN2UfWRmFnUuZGz05q
+bPjIZcfevMrF2bzkYdxf2FdzUs3X3WLew87Jcda+3NhCdOlOTT9gXdg7WVPnzUJKR89PWTxG
+5fb1RkruzOjV2NTCyN/haNfc+vDg1FxmTVtdb3dc5WfL//vX6szd3+/50srAycrW1tXoUEJI
+VGlcRUlf8droftvPysbN2/Lo9nFZVVFNfXV35mdu/FlLTkpZ7WNOS0lqcWRwZuXP0NHR2uTe
+3uR4bfrf7nXrdWjp2d7l5t3c8+DZ9W/f0vB2/tjS2d7dd1RrVUhDRV1aUktNSVJ5VE5LWdvV
+yM7Z7dvN4vz+5NTT0t74YnT3ZWFVS05OU1haWPvW1dTc0sS+vcfb5dTbeWFOXm9weV1YUlFO
+TU5PbdXn3d7v9dbM0NHcxsDIyeBlXnFZXF1Z6/x/b19STl5VTEpJTl1dWllUT2VpX3f6zcXG
+ytfS3tnQ3d3s3e9zcWz2YWN8aF5eaWpoaldXVW3r9tnPxcbIyNLb6n5WU1pTW2ZlXlRPS09O
+WltfbmxcS0tPVl5eaerZysPDx8/OzcvT097p41tKQkFGT+TMwcTEze9cQEE/PT5ASmS8tre7
+v8C9sKytuGMzKCYqMkvMubm602lNPjcvKignKi9Kv6qhnp2dnqKosLm2v8lEIhUODA4ZMLOf
+m5ufrNQ7KiIiIiYw66ufl5OTlZiboKeurKy+RxwNBQIFDB+/mpGQlqTKMCMcGhkbITO1m5CM
+i42Sm6Wuvlg9LSgrLCcfFg4MDxgurpyUlJijvjclHRseJDrGpJqWlpidpqy6z2XEraiv3hsK
+AwMKG62QiIiMmtweEg4PFBoq+aOUjYyNk6C7Qjg1O0NDR8Kqq7ooDgUCBxbJkomIjZs9GA4L
+Dhcpx52QjIyQmrFJMzNJqpaOk6gXAwAADTSXiYSLmVAZEg0QDxUbOaiRiomLmrUwKitB1b+3
+v7/Lq7DMIQ4IBg8snY2Kj6ksEw8QHCzQqpuTkpOcq0k4Pa+TjZG6EAIBCzWVh4WMsxgOCw8T
+FhIYIr2WjIuRn10/RaudmpytxjwzLPC31zIRCAcOOpuMjZJfFhARIDLLuKmjmJebor7bNlmm
+j4qRQwkAAAtPjoGBkWMSEhUfHRMPDzKmjYuOqSAZHL2cj5OYrO1xS9JN2jUgFw4RGUykmp20
+NBobHCxLz7aup6Ogp6i1ur3Fu6WXkJhBDgEGFKSKhIqlJA8TFR4ZFxUd7aWXm5+yrayrrsjh
+fq6gi4yuGQAACC6Ng4afPBUdQCslDg0TvZWMl0UmIb6dlZ2ju6+kpab+LTaooakeAwAFJJOG
+iqAdDxg4+H4sIz6pko+bu2FAz7q8USg8mouKswYAAAynhISPTxoqVKgoFw0bvJSNnUwaLsib
+najM466noLjNOEM/W6+pMhcNCBpTmZWc1jcxJ1gtMTRXuqalrbg6T3nEz2m8louNUgUAAyqQ
+gomfMyRPPzsTFRxqnZ22IyUyqqSnvde1pJ6rukZHSnlSPrGjWR0JBRXRk4+cMiksPccxLjre
+taa2wMHEsrS1uc71mYyLpgYAABmWhoigsEDLKBUOEDK9pVYwKV61u9FJvK2co6q1vL7iVTk4
+RJuXLAwACDWbj5q8RKrHPCgdPaqnw89Wq6Sv22y7rLCoj42TFQAADKmPipumscYoDA4RPr/G
+Lytqva9GR9ain6OrrqGkrko/UFLKlp4ZDAIMWZ+ZnJ+qnSsYGym1pcooybSerTwv46253OuP
+ho4UAAAKqZKTm5OPnDAHDR29xCYcL6qs1SM2qZqgu8Sol568KjO+vryYmR0MAQcsq5ucl56b
+KBIdL7u8Nx/Ir6zEdsKoqLnSTJ+OiZ0KAQken6WbmY6ZOhEKJTzhKi1oqa4tKTOtqa/DqqGl
+sFB85Mg+K8+OmBIGARSypq6djZCZFAwg1L05LUSerGY/r6Cgxznkua2ul4+oCQQJHKOunpiN
+ny8PDjEvJyBRr5+9KjvOs7yxrJ+ku8e/vs1NJkuOmQ4JBRO4draWhpWfFxRLMB8esaqa/SlI
+uruxpaGctNnJvjhgnJsTBwsSsN29noyWuiMROygcHsesobQuzbvCXa6poqu+sLLJPC8rmose
+BQsNRkE+n4WJqs8Y1kMYE3efqKQ9vLNFH1yvqKe0qazKKy4pLkKkrhUYEBtQQa2ajaW0JR4v
+Hyg8qLClub3N7+3brLeyc8fsW9qsj6gJDBIjuUibjoitQiUeLBEVJqi2t7e4s0UvTK67t7ms
+rcZHUGs3LS2ZkBMJDhNEKNWahI6vxSfLHhccqZ6nn6utPygiwrG0q6upw0csRDcmOZeeCw4O
+GcItrZKFmq1fKbYfHCmkrrSwv7EzJSnLU3fXu69mSUBPO7GZmxsKFhM7K7qPiY20ty5GGxEh
+Zqq4o6Sf1SotPexqtKuhr8XlTzosIx+jkxMKGRVeKEmbiZXCqSq/JBcjsLDIpK6lczE9s9Xb
+trWvXz9JUisvs5urDQ0WGzYio4+ImLe3UkYSGCe5w7Ohn6I6MjJcS8yvpKO5w/ppNykizpS8
+Cg8XHTMdqY2JnKKqxdsPEiPXY62inaM8Re/jPfK+p69z3FE4KCvbnpoYBxoSKSFHkYqNppnR
+wBsMFyA5OaOfl6XDur3GYrixn6q/veo+LCo2rZ4yChUUFyUcopCLm5mmwUsMDxMiLb2gmpis
+rL/eP1HXr6y6sMLkPDNUuKa5DhEXEiscr5eLlJiauq4SDw8cKkKsoZKdn6euvN1tzKzDxd9I
+NScmM+u2OA0aGRwvI66bjpqWoaatHRkSHiQ0RrKcnpuipKmy2s6/T0Q1Mi4sLD7PukUeLSUs
+NClJ4a2vqKmmo7a6UU1GQjs2QVDJyb60rq6ytcPNdT8zMDIzNTk4MDo9NzoxMkDrwLmuqqmq
+rre9ydtMNi4sMDo/Wc28r6qrqquusrzN6kw+MyYiIB8hIycuTM28s66rp6isr7S6yGY9NjY4
+OT1azLesp6Khoqiwv95NOi0hHRwbHB4iLEH9wLKqpaOlqKuxucxLODMxMjIyO2S+sKuop6Wp
+sLzVTTktIh4dHh4fJCtBy7erpp+dnp+or7fDaz4uLCwvLzlvv7Krp6Wipq6/5UgzLSIbGhoZ
+HB4jLk/DsKijoJ6eoqaststLNi8tMjU7eLytpKCipKartsxNNC4kHBsZGBocHypQxLCrpqCe
+naCiqbHA5UMuLjFAZywxraWdnp6aoKxIN7yzSwsFCQsZExw8nY+OlZ2WoLghFxclLil3r5eR
+l5ydoay5Qjs7NzctKS23np4jCQwJGBUaxpmJi42gnbojDwkPG1VbpJaNi5OdrKu5wPjdx11E
+LyknO8O2wQwHCAccGcqgiYiKk8CsIBwLEh7CoaSVl5CcplfJtMm04rrM5jEnJCQ8cbs3DQwL
+EyUvs5uIioqdvU4nHA8bIauinJ2bm6WvNVNKu8HT43NgMC4oLT28rMoTDAoOIy+1pY2KiZbD
+OB4cEhUZUamcm52dn6PVPzZQwri//d9YST08ONWvrCQNCwsfO7Gpl42LjKHNIh8ZGhgbMMef
+n52koaq9WzpJU8pcWDo7Pz0/Z7m1+xkRDRUq8q6omJSOk5quNyofIh0fJDi5p5ydm56jsMpa
+NzYuLy8tLzI+0K6uSSIeHys8VubbvbSpp6OmrbPByU88KyktM0122da/urOzvcDN2WBMPjo7
+OD5HNysrLDA3SEM+V8qvopydoKKkqK28QysjISYsLS82S8y3r62qqqustcpXPTEsJx4aGhse
+KCwrNVW5p5yam5qbnJyfsu45LiwrKiYqL0i9r6qnpqeqstJFNy0nIBoXFhkcJSgrOc6to5mZ
+mpmZmpyit2w7MS4uLSouPuK9sa+tq6yuuMbpSzspHhkXGBkeISUtTbypnp6enp2dnqKxy04+
+OTMsLC44Zce1r6uoq6mvv+FLOSwfGhgZGx4jJCYy4LSlnp6cmpiZm6W/aUI4LiwmKTFC/cm7
+u7KvsbbF/F1iRSwfGxobHR8eIS1qtKWen5ybmpyeqb7abFk/PTMzOUjt1MC5rqurr7zSVkEy
+JRsXFxkbHyEkL+2ypp6fnpyZmpyfrb7Sbz40LCktNUZezr2zrKyvxM7F110uHhgWFxofHxod
+K7uknZ2emZeVm6W5zd9IOykwQr2trquurK+7XUY9MjE+yegeDQoMGSc6RKyYkpKjv0hqOSgb
+HVGompuXlY+RnsVKWU1CJiQoMy4pICTon+gIAwQVSrqunIaEhJhVKSsfDQ4QQ5+WmZaQk5tp
+Lju8yt5E+b5KLikpIS5LtywNDQ4mRO6sloeKj7hINiMTDxwypaiin5qerOJDzczhQ2VFTS4q
+LjItNr2dZAoMDSNVyKWOgouPtshAIw4MIjusrqWfmai/393Bd/nDsOhALjU1Lx4p0qU6BgsS
+KjI5o4uDk5u4rUAXDhvNy6ukl5mhZtvDUjpKubTLOGlPPSolLE6utwwKGxU5LKSTho6emsH6
+FBISQEReoJuWpK+/rFhD2aqos/tK+S4jJCkjMb68EBEZGTcwqZiJl5qbuPQaFxgxJ9annZ2q
+rLCvR0RqwtPiUedNMDg7PEG2pBkLHhEmKbWWi46aj6x5FhcUHhsqpJ6cn52krjpJ5WFavrzC
+fDs9NywkPaglCB4YHyLHmZCPpI6gxCAhGhocJauupZ+Ynqe5ucNrW728R0tBOzAuME22Lwss
+HxIfP6Whkp2NmrftTCEUHBw9Wa6emZyanam38UhHNTNJNy81PT09uFYOKRwPHCzItZWcjpih
+sMEjGB0YIy3Fp5uYkpWco6zSQi8oKScmKDM72a0fHcgUFSA6J7CfnJWcmZ+uKjYdGB0jKUyy
+npeVk5Sbo6tOLCQeGRsdLNYcIKYlJ2ixIW2q5q+upbet77pILT9GLDbFwLKinZybnqKv7jss
+HRsbGiQ8GDO8Izuvui6cp7mmna+6rU1CLzAvLio9SWmupamfnqWmqsBNOyshIB8jLxwe6yIl
+ysIqq5/Kp5isx6TIOFZRKDFRNDfNzcevqailoaOpq7XjOy0nHx4iHxwpMitJyP7gqbG+qKvA
+tK7HwrleP1Y4Lj4+Nk7n376ws7mxusbD3U9GOjU4NDE1MC8xPUlvzs65srCsq62vsr7H1FpK
+QUI8PTw8P0JTf83Cwby6ubvBzdpbRD04NjQ5OTxASmRf7tnPwr2+vb2/xc7pc1dJSUlPT159
+7M7U1dfW3PDta19MRkE/Ozk8P0ZNaNbMzMO8uLi3uru8wMXGz+3xcmdXUUdLS0dMTE9PT05O
+TUtJRkJETU9hb+Da29XV4ujZ3dzXyMbBwsfGzNTc6V5aWnBhX3v8dGdrY1lVVmJmZ19ZZlhO
+UFJXVV9f+ufl1dPKx728ubq9v8PK2/pjTkVKR0dFSElKTEVOW1RaVVBXXV5obHB2aW9nWFpX
+bNnTzsXBxsTFy83O1d7l7mrn/vn1e917eVRRTEhTTExXW15baV5dWFpeVmRvfPLU1dvc2NXX
+2t3r9Pl5ZuPta9jl29ff5nphZFhealtnc2JaXmJfYmVkauDq7d3o4vNsamTz3d3Vz9LU6uHY
+fOZ5+f9y8Hvt9Pbv3+Dj4NzY3d7sbXFrXlRtcGp4aV1ZZV9WVlpiX3F9eO7r+n9tXlpTT05Q
+U1phZ/HZ1c7S2NTY1uPc0t7m4vv+7uXv6u7+7Wjl4uDm3uji7GJYTUpHSk5QV2d24d3Y2Nze
+7Otx5fdeXl5WWWdjXV915uLc293qbvDn3uB3b2VseWxeauDa1M7LzdXY6vjs+2pYWl1eUU5W
+UFFb5Orezs3U0svRzdDP2+HxX2VmWFhVUE5eb2/4/vpzaVdcXVthXF9mfurf1dfW3Nz1cWlf
+XFx5afXyaGhtZWRkb+vj19fU1trf5u5iZV5iZ2NmW2d+de/b3t/r4OVyaF1YVk1SZF91X3jl
+5+fj3+nj393naGBfX21uc3JfXnP5eGd26O3s6ev08eX9bmz7dPrc6ujd19zf6Ozp5O3m52pq
+YWlpYl5eXWLu49rT0NPS09Xc6uNka21iaF1PWe7t7X3udm9vdnxjWVpXVFlrXVNkZ2Zq6/Xi
+9HX+dfn9bmFmdPPj5Ofk9/zd3tXU3N7b2uDg2eJ3Z2N0enT+9ujf3trj+2FOTU1NS1BPTlhh
+Z19jafTp3+PW3u3f7Hdxelpm/fXl39Pa39jT0tjX3+/gZOxqSnBYXmhlTV9fVXl539/VenF9
+V3zrXG3o29LNzcvW397R2dXX59/r/lVNT0pARkxRTVJqetbe2MXXzFn0Wld8VFhXXmtu73Ht
+6uHPxtB+09/Z+2Vd29z9Ze/e5Ovo+HrwamlUT0paZlxrYHzy7M3O4U5L1Z6tI05L5ygsJlqm
+OsG5nrizRlbdKSEgPyw5Premn6SknaeszNtFOCQeJyUlJr6jUS49Ss9GNDWqnbmvq6Gqtz40
+3CwlIzlK7Me/oKCmr7G730IsMC8tLC4vLC2+l60fMC5RQS8sxJGfoKOdnq52FxscGRQYKDip
+pKCbmp2uuUtBPiwnJzIxOTk4Xe7QbNm0nZ45LDL6uL2+y5qVmqGwtnlPHhUVGB4iMj23oqCm
+rrPOezcqKSksLTU3QElUV1/Vpp1MMS4v0r6u2qScnZ2nrH7LPiciICQoRUu+qqahpae8x1E1
+LikrKC4xO0E/R0VmZHDOp6C+dzk6cb2z2baxr6mvvkVBMC0rIyMlNT/xz8C0sK+/xudlZ1NK
+Ozs5Pj9CRkhRc8/IvqWZp/1AMEjltsbPsbGoqa3VRz4uLykoJi0uMj5AXFzc18XAx7/JyNPk
+X09MPz07Pj9RadzJzMW+tKmqt/w9OEnKxL68vb+/zn5VOzg4Nzg6OTc5O0BPdOTez7+9ub2+
+w9LP1MzMysjJxsHFxsnP3GZbZWnr82RURUZJXXhdWk5NVVpRT1NHREA9PT07PkZKbN7SycrJ
+xr+8vr2/wsO/wsnT4OzwdFVUW1FXWFFYXlxYVVxoaP7c3d/T193u72BcV1BURkRCP0VMUFpf
+WXZ43szJysjFxcPAwcTMz83c2/FmY1pQVFFHQ0VFPz9DSk5MT17u29jc4tDJxMPCv8zW0Pts
+XVVWWF1t8fbu3dHNyMO/wsbExs3I0nZqVE9LTkxFQkJJTkhIRENKSUhMR0pRV/9s4dPZ29bN
+z8rJ0czLzs3N2dz44trf2t/Y29bPz9HnbmZdXFpUUUxLVFFMV09OWWX8/uZtf2NRV3F7XFVc
+c+vYztrr3NvTzdbf5tzY6e1qXV5RWFpeZFxdb+bl6X3+cfB+b+jnb2Nnafjn4uXq7Ozj/Vpb
+WV5tZ/39+d7T1NbSzdXMzM/Nys3X7O5iU1BHTlBUbGhkXFhRVGbzY21xYnhgWFVPTlBoZmL1
+++Tj59/o1eHl4OPU3+Po5uTd5+bc6N/l29vu2tzc1+RyY1phVV9sT09iZ2lla/VtZWxfcur8
+4W1bZl/6bFpnbvrm2Hpl++F77NPvaWNYY2rs2NzhbfL6fOfh3t/i39jr5OxebV9naF5rZ1N9
++FtybdzX087c19n1XXnf1dLecfDP2Hl5U0dMYvz2YltgW/7u7ub9bGZnZl9WV1VUYV5dbVpZ
+8+/q39z9bebd1NTc39/Y1tPW5Hfp8Xvc3Nzy7+To7vVZWlFn7+puTVBR/mluVk9hfd74ZmFq
+dmz9d2Jhbm5Xa2B88HJgZ9/Z1NvieO3a3s/Ydfnt3enZ2+z9eurd299nbWnn/1BQXGl79Vhb
+WFNPV+zf2tLX187HztHOz91+al5x4+by721Vd2pYZ2NqaWVOSEtQW2RaW1VXXVx+6+x99f/s
+3N/o5d/8393d2+Lf3+bf1tDM1dTvfnHm5t3c5mVeXk5eWFtkX15XTlhhbGzuY1RUU19re+/R
+y87T6H10bl/6xr29zFIxJiYoNruhm5ujukIrJCMkKCwxO1zMua6rqqurrKyvtrvHzNHhRi0g
+GhcaJ0qxnpyfprlPNCspLDI9YMu7sq2rq6ytrK+zvMxhQzpAVj0tHxcSExw1uqCbnqe3dDUn
+IiYxdbSloqGgoqaqsbq+vLrBwsfIyk4zHhMODREdaaKUkZObri4aEQ4SHTqxnpeUlpqepq6y
+tLazraqqtlcmFg4MDhQk6aGZmJ2vOR0VERIZKs+kmpSTlZuirrrEwL2zq6WkrlciEw0OEyJj
+pZmXmqPdJRcRERYm5qWZlJOWnKSwzEpNwqyfnaZWGg0JCxAlxZyVkpquMxoRDhAZK8iimZSU
+l5yirrS9vbyzr66zx0ImGhQTGCE9tqmipq9fLB4ZGR0u2qqcl5eboq28uq2in6XEKBcPERk6
+qpaSlqk3GA0LDA8cOLainZucnqSjpJ+enaClrrzReEI3Jx0YFhcbJUS4q6av1isfGhsfNMKj
+m5eXm6Kuu7OknJmdrycUDA8aQ6SVkZWmOhkNCgsQGzXBp6KeoKOlpaWioZ+hp7HM+2xtVjcg
+GA8QFSN1o5qZoLctGRMTGyy1npSSlJujt87etKGYk5qvHA0GCRE6nY+OlqorFw0NDhgjRsGt
+qaanpaSkoqKkpqiwtsK8tbfaNBwRDhIZM66bm564Mx0XGSA8t5+cmZyfrrm3vLuxp5+dq+wd
+EA0PJcWclZOizygbFhQWGyEsUt63r6ekop6dn6GkqbTAv7yvv3wiFg4NGCmvo5yfq0olHBse
+KEjGr66qq6iqqaippqmmo5ucrEwYExAVJO+empq0OhkSDxYeL1XnurqsrKWqqq6usLWxsKmi
+n6nLHxUNFia1mJOVsDQYEhEbJj/jvbKxq6+prausrrCwpJ2apNQaDQsSOaGNjJCvIQ8LDhUi
+LUdlx7eqpJ+foaOpqq6sqqGepr8cDwoOIsGUkpSxKRUQFRspOkZJ0r6pqKSqs7a9trGelpGb
+4RULChR7mYyOmUEYDw8XIi0yLTNeuKCen6zFycetpJ2alpurMQ8OCx+3l46UnykVDQ8THCEn
+K0exo5uiqbu9rqadmpSSm7obCwoOP52NjZbPGA8OEhkeHyguv6WamKS4TuS0oZmXmJWcrS8O
+DQser5aNl6kfEg4SGCAgKzy9npuYpLzoz66fmpmZl5+6Hg0ND0yjkJOkOBUPDxIWGRkjNa2c
+mZyyzv23opqZm5uYnasrDQ0OPp2OjZvMGxMRFhcaFx0wtJudoLL217GhmpyfoJmZozQLCQkm
+oI+OoksaGRsfGhcSHDetmp2m2UvNqZ6ZnqWjl5OePwwJCyybjo2iVB4eHx8WEQ8ZS6eXnKjA
+bryopKStt6iakp1gDgkNJJyUkqn4MjU3KRYODxlTq5+nwN7Nsqelp6yrmo+Qph4IChBlnpqd
+udTdxj0cDQwPIdmtqK6xrKelpKmqq6eclZaqMRIPFy6uqqy9wtnTOR4RDxMfPc62r6qnpaap
+rrOxqp6Wl64uERAYL7y4tcS6t7DVKhcSFB0sP2vHr6Senp+kqKqqoZubqkEdFBojRHPpUe3Z
+x8RCJx0bHicxPli+qZ+dnqCkp6ytqZ+esj4fHCAxfF1INTc/3uE/KCMjJi0xLzI7cb+zqqal
+pKSkn56fr04uKCksNDMuKisuRtnM1eXe2Ma/w9hMQkRR3MO+t7WxqaGgq7h8RkFOOi0lIiEn
+N0RJQz5D6svXTDQrKS04T9m+r6ifmJOTmaCtxVtFLyMdHB0hLTU2MDM0QPR/TDw3PFbGuK6s
+q6ainpmWl56ns9dNNiccGhgZHSUpLC0yO1PWz3NOQUVd18W7s6ymn5yYlJSYobFPLyMcFhQU
+FRgfKTA8TnbJuLm8xcvLxri0r6ypo5+dnJiXmaCwTSofGRcTEhEUGiEtPFLQvrCsrK+/7EpM
+69W/s6qjnZuZlpaYnq9SKR4YFBISExYbIzRM3Mq4rqytsL3bV17bxLm2sauknpybm5udo7dM
+JR0VEREQEhUdKD6/rKqjoKSoq7fbXk9GT/jCtKukn52cnZ6hpa3fMR4YExARERQWHCY507St
+qaWlpqmuvNVaQkRlyLuvqKOfnJubnaGpsn0uHhcSDxASFhoiLEm7qqain6Cjpqy3xX1GOzc6
+Rfq9r6WfnZyeoqy3xEAnHBQQDg8RFRsjMX6sn5ybmZmcnqOuxWk+MjU9SF7Dtqmhn52eoqu7
+3U4wIRkRDg0OExkfLFa5opmVlZWXnJ+lsPY5KiIiKC44asqzo52dnqGqudFQQzMcFhAODQ0U
+GzHNrKKYkZSWmZ6pscs7Ly8wMDxS2rqvp6KfoaWrvWk3JyAhIysgFh8dFBAeLCi1opmUjpCT
+laGxzTMdHBwYHSw1U7CinZiWlpicqLRjKyMdFxUXGygfGE0fFRw1Ni+YmpWPjI+RlKSuUiUe
+HRYVIR8gTLytoJiZmJqdo7VcOSgcGxwYGSQ9IRy8HhUlMDBKlZqOjIyNkZmmriwdGxMOEhkc
+JlK5qJ6ZmJuip7dFNiskHh4fHh8mMfXCJWasGi9at8ifjpeMjo+Xn7F0OBoXFg8PFh0fNcy5
+q6Sfn6Koqb59UjUqJCMgISMoME2uxjKeuyrVybdVl5yYj5mWn6tXTiUUFhQQFh0pOsiuop6f
+nqK0xfIyLSooJyUpLj0+W8mxnq0qorYgTd2q5ZaXl5CioLZNIB4aDg8UFRsoPtWtqaCbnqGl
+rtDeRTMvLCorNDFB5Gnt672om9YtnC0cNMSuxZGdlJGjpLNMFxUUCwwRFh43xaiYmZqYnq62
+zToyLCsrLzdjzXHPy8fqYXi9osocurseMf6ht5eYm4+mu9pSGhEYDw4RFR421LWblpuamp+2
+yEc1Liw3RlRdubO5tbq8z0kzSK2nHh6qJis4p5+elbGUnkUtLSENDxATGxwm7K2xqJiboKms
+sr9oPndYRU3KyMnO3cLVY07mXDszXZ2iHC6uOj44q6OUoPKWoT4eJBsPDgoSJB4fxJyenpub
+mqbQyLDUPVS/s7vHvqu4R0BVTjIvMz89L1qcmyEjVDf2KDPDlZxBn52rPh4aGRwNCyBFUcam
+mZGSn5+cp2Y1OUziT1mzo627yu9ZLyUjKyolKCw6+p+YPCo8Kz8rMDmckq2foKa0PBsQGRUO
+FiE9sZ2dmI6Sm6a3b0tIMj3WurCqs73JSDAmIh0fIyMrOEj4qZOZOyseI0k8NzGZk5WUprG9
+7RkMCwoOGh8ss5eQjY+YnJ2pVDcuMlfBvbWprbDNNCMfHx0fHyMvUcy8oZKadTEbHjntTzqf
+nJWPmrA4KxgRDwkJEyjPoZWRi4iLkJy2MCsmISItQ7ejrL1PNSkmHxgXGB4sQ1W+mY+bry4W
+HjS9xKyfp5SQlaVPHg4PDwsMEhs5no+MiouQlZqrSCcaGiAy4a6mrK+30UApHRcXGh4pMj/g
+npGXnVcXGR9aup+erp6cmp2qMA8LCQoRGyM1uJ6QiIeJj529PjArJiAfKG+soJ+xSiskIR8e
+GxodI0GkkpCXpyIbITqtn5qoqaWln6bGIRIKBwsRGyxTuqKTjImJjpu2NyUkJSgsMT7Nraus
+vD4pIR4fJSkrQ6+fnJvAJx8fOLSal5qetr7ByUwsGQsHBwsYOq+el5WRjo2Okp9oJBobJDRo
+xcHKyc3XdT8vJR4gNr+xrmwfGh00qJONj5emyGNIPC8fEQsKDRc5rZ2YmZqYl5aXn7wxHxwe
+K0nOwczcdeTKxNDbvLm/6SsZFhkptJeOjI+ap78+KiAWDQoJDBg0rpuWlpebnqKqvk0vJSYq
+M0zm2e5QQt+mmpaYtx0PDBAppJKNjpafq7nK4C8XCwUFChlcnI+Oj5SZnaOv/yoaFRQZJjpc
++b+qpJ2g2iwbGyTJnJKMj5Wfr89TNx8UCwcIDh7AmY+Nj5GZoK9wLxwUEBEVHTC6oJ2fzS0e
+HSzRnZaRlZqeoaKorFAfEAsMFCu9npiXmZucnqa9MRwUEBIVHTHVt7DJKSAbITqvm5mWnJ+k
+pKirxiwbDw8VKMmgmJaUlpaanq5uKx0YFhklP1JLHxMPFCfAl5KSmaWur6y0xycVDQ4WOqSW
+j5KVmZqdoK9NKBwYGSJGxsoyFQ4NGkyekpSYqLS8sbjSJhMNDxxanZOPk5WYmp6mujsmHR4q
+X/FDGwwLDiSwko6Rmaiytbb1LRQMDBQypJSQkpmcn6OsuU0yLSw8u6u+SRcMCw8fzJ2dnKKo
+qKa0OBwNDA8hwpyVk5SYmZ2kvF8xKz2tnZ21HAoHCxvPn5ueoaGcm6E/FQkIDiO4n5mampqZ
+napOLi9KxMW8qrC/OBMNCxIet5yXlJmdoq0uHhAOEB9doZWVlZyhsslHTEdMuJyaqGsPCQoT
+J76jp56cmZmeORYMChInvqKYl5SUmKTXLCk8S91SYK2mpdQYCwoUKKabnZ+hpqa4JRUMDhjv
+pZiXm5+lsufXWL2nmZ++IwwMEyvIqKWmm5iYom4UDQwPHTDUrpyXk5ei5TUvSbexr9c+x52f
+qR8JBw4pu5qhpqOeoaUzEg4RI9Siop6kqq+0ybafmpxeHQ0RINimp6eupqanVh4PDhYkSd2/
+vamin6m+SUi+p52eo6/Heq2lyS0RCxAus6OftLyxsV0vEw8ZMrmjoqepuvgzNs2akJlpGg0a
+Uaqmt8q6np6uJBIMFyM+6O69p5ucplkxLsmpn6KlqK/E1arMLRwMDRhorZ2cqaioxisbDhUi
++6qcnZ2hukYsN7aXlbkcDg43qpyipaacmKoqDgsMGiUzP76ilpeoQSUtR7auqaadmpy6RbK+
+Ox8KCBNqppqcq6Wgrz0eDRUpxKmfpKGep7lCO76XkaMeDQkd5rGvp52Uj6UpDgsNFxgbI3me
+kZSn2jpozclc27KfmqLNNLOetjUNCBI+pqmktqGWmVQeEBQ1zMDTsqycpeMuOVupl5iuIBYQ
+HyksQKuWkJKvLxwaGBcRFSW4mpabp6u2vUgwNL6noKSyzE/CnLIcDwgUMq/cr52akpo/Gx4Z
+Jy4vRaGbmqZINtu4wa6rm5fMHREUJEDJt5iVkpvFHhgSDg4PGEyel5aeo6u6MCQkN7yqqKWj
+rLs/vasnFwwNHcitsJaVkZSsIiMdHiUkLbWcnp+zys/HRE1orJaR0xkSDiAjJzKdko+Ws0Q3
+HQ4LCRIvtqWZlpKVp1g1KzJDYLKfnJ+oxkVZrzAUDQsdNX95n5GPkqbeX0sfGRQeM8O7q6Kc
+n67My7/iyLiqqS0ZFRcoLEPSlpOSm6jsPR0PDQwVIDnIoJuWl5yns345NT3rva+rp6/HOCMl
+YzccGBMmRUlLrpmUlKKsq647IhocIyUoP7ignJ6dnqW6TTA3TWk4FxoZICwmSLeanJyloqi5
+LiIdHB0bHic9v6ugmJWVmZ2nsdg8LCkpKSsrLi4xS+VJODcvXVlJYMezs7a4r66ts7bAzvlK
+ODk6O0VR5svJw7y4ub/S3+tuVUQ8NjY1NDQ0OUBV28bBuLO2vcLM2u9tYk9WYF524d56XmVY
+U1xletvV2NHK0d1sWGFUSkdAOjo+PUv40szCu7eysbK2vsfebWRaS0BBRktOWVdf+3t45e1Y
+SEA8ODg5Oz08P0NKYNfKv768ubaurrO0uLy8v8xhSUM+P0RLUk9PXE9XYE1OTUpLUmx8ZWh0
+U01UTkhLWerOv7e2tLO0tbS1vMnZeVVGREA8PDw+QE9t9eZvYlFPTUVBQEJBREhERUtYVWnf
+1MrBvb27t7W3u73DztDOz9jU3/n/dFpJSkdDRUlOSEdHR0JBSEJBQ0pUXuv3ZHne2sW6tLK0
+s7a4ur/YVk4/Ozw/QUFJTVJaX25SRENDP0hZWGNzc+XZz8zLz9Hc1M7GxsbCxsbAvcbS+FtY
+Tk9YTEtLUVhu8nNmVk9OV1xfXmd56vJ2Xk5MTExe6tDLyLy7ure3u8LaXkpESkY/PT9ES2Jf
+XltaTUtOSENBRlVr7tra2dnV1dbKy83JxL68uLm9xcvP5m1fYFlMTlNUZWFWT0ZHQ0FDP0BL
+U2zw6Op0X1hST05RTlFl2cm/ura2v8S/2M/3emNsaGrj2fpmamFqb2BITEtPYm5aXVpk9m74
+VVJNSm3YysbFvbzCvr/Axd5wTUhaYGj7919YTU1aSEE+QEZy3Gb/9HDhX01JSkhBRFDXzczL
+vLe+xMPHz+NfX+fqyGBXz+7N3eXlXlVURE5s3Tw8PkRrQTpO9Fte5cOtrbe6uLXAV0RNOjAv
+NENXS1bNtrS7uLW/3UxIRTYtLT/65DUvSExYLTJeur/FqZ+ZnaWoqLY7JB8iHhkbLeS5rZ+X
+lpyjrbNkKB4dHx0bHSY2U72uyL69U8k8VrympJ6ampihtuA7IhcREhgdJES5oJmYl5ecqcw3
+KiAbGhoaI7yuJCknKcAlMrSUlJeUl4+aZy0nHBUPEidUdq+dk4+Smp2lbyIaGhsaGiU9VVA+
+PEE2KCQmK7ufLj60z5mmqKKOk6WasarDIxYcGxccGy+6uLyonqClueBoQSwtM0LK4PjT9z0u
+KyouJyS5lr0q4jyiofXdm4ymoarRri8ZFSMiHS0zuKCkr6ejyEcpHyg0Ly9zva+35UM/OCot
+Lzg7XZyXU1pIOrw9PS+WlayctrVvPxwPIxseKzy9p5iipJ+qsdpBKzY/LThP1dbDWThKTDw+
+SERZ1X1r6FVQoJVSL0U2vdToJq6X17ZKMC82PhQZIB87UcrLoJifnqq1u7bWL1fc27zFfV3A
+6D5JNyokLqusMDIjMbeqrzucmp2XukgjNy4XHRYdPr6vv6Wppp+6QS01MTVRNUS7rq+7zT48
+VUI8PUBK0shSv6KnxEkkIjy5t7elu62ep88qIRgaJx4eJzfWqJicnp6qsbrQNSouKztv2sG2
+tcHB0UtCNi0rLDnFurvOPUFMta2pqN3D0t/kQDYeHiAfLjpLUsuuqJ2bnaWzyz07Pjs8O0Jc
+ysTCvtpqSDMuTrjBuNwtLzfFr52bsrhOPzs/QCsqHh0oOs22sLm+tru1rrLLRTUrLDg5PUxT
+VezNw7a0uMS/sru60EE5LDQ/uaKgm6Otv0IxJiYgGhoaHidAuqqenZ6fpau3wW1BNiolIyMn
+METet6+ura+3x/hFQE5SatdoTUdLfrqno5+hrso9LCEfISAkJigrM0zTsKWgn6Gkq7XNZ0w/
+PTw3Mjc4PU3mxb69vb/Fx8PGzvRDOThM5Mewrq6wtslOOy0oJiYnKi84PmTIubOwsLW4v8bN
+195xTklJQkE/S2njzr/K1ftNWO7Dv7mzvMThXEI9OjY9Q0xXYe3m9HFj2MrDvbu+ytlhTEg/
+OTg7PEJb49jJx8HCx8zR397qV0k/RlH1yr62tbnB2GdJPjw9PTs/SVNZX1lYW3rm1cW9u7y8
+vcPNc1VDREZIVubOz8/V4Pl3X09Xb9DL1t5/X3PCv728xtJoTkI8PTs6Ozs/SFBVS0hBQkdO
+4Me8t7Kxsba/y/JbU01KVVxcZ2poXV5OTVFc2sjOzOJZVG3Yy8HK0N5qZU5KR0VIRExbUEpD
+QD08QU3yybivrq2utb/YVkA9PENNU1t3e/7vWlZVTUdGUG/byL7Ey9rubHDf6dnIysbV7PdZ
+VU9PPjg4MjM3PUvpw7CrqaanrbW/5k5APDk2OT5IWWTkbVVYTUpd48fL39tTS09e39O3u8TO
+U0M0Njg/UF1gRTcxMjdKxbWso6Gjp624zVVFQTk3MzIxNT9GZdPM0M/tfvvazcTPe/8+Oj9Y
+2MCzubvVSz85QU9dTEM5KystNmi9sKuoqamusra930w9MSwqLTJCesi+v8TN3ci1sqy9PC8j
+Iy7VrKSdqblwOjYyOjc1Jx4fHy3gq5+bm5+kqq2ur71OKx8dHyc2Ud3U419i38a+w2s5OlPc
+rrFUPCcnL7OclpKcquExLSsrIx0UDhEaM7CblpSVmJianaSzOyQcGh0mMD1KTFjZyb7FXzsx
+KywzOkhD4aymnZ6uwkzux6eirLQ5LCcpKB8eGRocI0O2pJ6dn52cnJ6kukkyKS45RExBOTg6
+OUpOPzQrJSRSq6ag7B8eI82djpGWnbSrrbD7KhEJCg0bN9i1qqWYjYuNl7UrHx0eIBwbHSZV
+pZ6frFEvLCstLiYjJSxRppSSlJ8+MzfVr6eyU/hLsaiuTx8RDREVHiouRLadj4uNkp2vwspV
+MygeHy1Mvbl6PDQuOUY3KyMiMK6cm55IISs8qZ2dwF9WcK2zXSAUDRIaIjAtL9Kfk42Nkpuj
+qqy9RSkcHChCcfZRRVbQu7n5Qy8oNPqrprjyKCxGtKKlqcm+v7DANh0UEhIaHR8kL86il5GT
+mJqdn6i/MSIfJCw2PkBE2LCoqK7NNzI7/6+6OCocJ2mnnp+nvq2zsc0tFxAPEx0fIyo2uJyV
+k5WZnZ+mrs4zJyYoLTQwMj/fua+ur8NeaGPCsPItIx8yzaqkoqurqLG5Qh8UExEWGx0kNMei
+mJaVmJ2gprr9NCgpLTE1Nj/Nta2opqmvtrm7tbFDJx4cK0m2raetqaKqszocEQ8OERMVHSvJ
+oJeSkZOXmqG4TS0kIyQkKjFDx7auqaanqrK4wLar0zkmHylGtbOstK6kqbRFJRkZFhQUFBcf
+O7ujnJaSk5Wbpr5WNSkjHyAnL0HOua6npaqsrrKpsjYpHyU31sfMrq2hoanNOiseHBgUERYZ
+IC9PrZ2WkpKVl5ymvz8qIiAfISQtRMivpaGioKSipb86KigtMjQ567uurbe+zVc0JhwaGhka
+Gx4pQbmpoJyYlpeboq7OQi0kHx8jJy9MwquloJ2en5+nusxSOzkzLS8zNT48P0xGPTs2Li0q
+KCkqKzA5RFzTvLCtqqSjpaaorbO6x+pPP0E8PEdSbtrHx8bBxtDidFpQU09QTE9USj9CQjs6
+OTYxMjQzOTxCQU1q58W5sa+sqKako6WorLC3wd1ZTz43ODY1NjcyMjc2Njg8PT5KfM/Hv7y9
+v73DzN5bRTs8PDo7PUFDSFdl6dfQzcS5tK+urKyurbC3v9RlR0E/Pzw6OjY1OTo4OTw9QExt
+2cnDwL+9vsTcXFFGQ0JGT1dcd2BbXVpOSk5SY868ubSurrCxtLjG31ZIQD9DQkJISUdIRUA7
+PD1ETF3o2czHxcXFx+pcXWFZW97Tz9DMy8zQ6lVGT1Zl6trNxr++ubq8y+R0UUlGSUlEQENF
+QkI/Ozg0NjpEVenNx7u4uLm/yNf1++nn3s3OzcW/xcnO5VhRSUdLTFll4NDMzNnhbU9NTE1N
+UF5rZfbjZ1ZNST8+QkRQaeTUysHFy9njdk9UV2lr48nGv7y9xsjV8GVTT1RPTm3o38/O3O9o
+VUxJTkc/SFpn6NjV4uvd3mZdbe7u2cXDwMHH0/N7cF9WT05LZO/w1NfoX1FOTUxJTE5O/NrW
+2eHmXltdWVNLTU1c7tPOzMbMy83U2/diXmFo3dDR1dzM0t3T3WhUWFlPU1xbZHrt+Xp2dmRY
+W2Fo9/Xwd29fW1lVVU5TW1JYfdPNyszP0tfZ6GdmZlpSbdrz7tXP39nN53Hhe1Rh7WRvb2rv
+2PP02k5JX0dKb/L91M/MxdDUzt7h8nBz5Gte81Zi4O3l51lg309L22pV795W9P1N7F9WVltY
+X/VZZeJsZ97iafx7U2dv4M7e2srPydjF41FkP0tISWdO1tT3ur3Dw9tubexHSuc/SV9GaGnL
+SkdmQ1xZ4fZywszNw8vHaW5WSuBNU89PdMrl2snNYFfaREf0RfJgZvDnuOnMy2nRRl1dZHVK
+3l1t3endR2VOPl9v1OzMxMa517W++Nlj9EXXaUrq5M3kxfBc7klRV1FLUGxKX+TbZ25gPkhA
+Pk5EU3tz7M/K2djtZWT6UP3K78rHy8LHxdTW0d5QZ2dbc3rX1NbW4dJLTV1CQU9iSd5oWtDV
+7V/ePVLpOEzoWU7Iz9m9d23YRkJfW0jY6VnW9crF3+Bk3lzw5fbJVdrE/cDH93lPZkvx6lbd
+WuHQ4s7seU5bWEbiREjI2tm7vcPIwlLYzDxUX0JGV9RZ3NpEeGRaW2VfVdZmWcjN/Wt8aE1K
+Sk9NSGHT29/Iyd7P0Pj87GVr3OJb18zo0srQZmXlcmD+4eHr28/K01pX287YQzs9MzAwQdLQ
+yLqzusnJ6Vk7Lzc5OT5HZOXQwbqys7W5urvgY7GcrxUZMScbG2Gsoq2qkpWqQE/vGw8RGyYj
+N6uYlJeSkJekt8c+IhwdHhsbICQjQpifGSS3eB8qqJ6XoZuMkKtErc8VEBYdHR86r56mqZ2g
+yzY/NCIfJCorLzI1sZK7FtvLJxxVo6iWq5qOn91KoS8QGx8tLk2vnputoJmrP0RWJyAlJikk
+JCwvPanVGjo2HyFHtq6ZrJiPn7KwoyccKiY9RMyxnqO2n6f3PUk0JionKiwjQpi6DznUHhcp
+wNqeR6KOpWm7mC8ZLSxEOOeympywm5yxfLnHPj0vNTMsJjar0hEl8BwXJcM0rc+1k6xf2p82
+G0A4Pz3Nt6GkvaGhwEu3ukxRRUo5LSyrnhgduisdIrVRrsFGlqRNOKHVFikvOjtav6mlxqmh
+tky7tHvUz9tJNi6/qRkY0CUZIL7orq/Ul6JIPahfGSo2Pm7CrZ+fuaunyUvLwVjp/FU+SKrT
+FyotGxomQlOmRaiWtD+9rx8hLy5M7LqkmqShmqrb5tJLUU5HPjbAqC0cTiQYHS07x8DYmqNH
+1K83HiopLj9MtqOlqJ6juMG+y+l2TUI/sqYhIN4dGiJCYKy1vpetO8nAIxwoIzJC96yjqqee
+q7/Nb2LnZ+vJvqa+HTswFRkfLkSptJyUssO1PhweHh0mLfepoJ2YmqOsvMvaX0dN7LWlPSPC
+HhQbJC9AtbqYpsarxywgJBwhKz+xqKCamp6jq7jI3ks7WLy0Rx5MHhAdIz9NqKqYoLyqXycf
+HxsfK0C1qp6bm5+kr8Ta/1c/2K62UyI/Hw4ZGy0zqKGYmKmfuzgnJBoZIShYtqGamZqcoa+4
+00YyOs7LeyItKwwYHCYusqagma2frlU3LB8dKCs+y66hnZycnaSorr7sTV7OUjQeKxkOHRws
+O6innZqtq9E5KSUeHiktarennpqanJ6jpq6zwOTHzVU1HSoTDhkYKDukq5iepqfXQikgGh4f
+Jk3BqqCbmpqcn6auudtc4FlPNBktFhMfHCw0qbydn6ikvuc+NyYnKCc4V7qqoJ2cnZ6gqK6/
+UD88MjgdGiUOHB4oM8S0uaC5qrjS4005MzswQnrNtaumop+gn6WnrLvQWEo5Px4WJw4ZICU7
+2Ke1m6ikrMvWQTUvNC01PlbCtauopKOio6Sor7rdUkEvOR8ZIg4cHiY37LC2namhrLK7TUA8
+Ni80OErcvbCsqKSlpqWprb/uSzkxLygZKRkYKh5AO7y9q6SjoaqotcfWRTw6NzpCa9TCurOu
+qqqtsbjMTDgwKywoHy8dICYiPju4wKuqp6emqa+zwtZJSUBBTF3mzMC6s7Kxtr/RVEg4MjIt
+MS8oNCQoJys6PsS+rquko6Cip6myvuFXPjo2OkFLaOzOwL6+wsHM12FEPzo1NS0rKSUoJi0v
+QGbMurOqp6Wnp6uutcTqUEk+QT0+Q0/nzL21s7O3uL/RVD85My8qKiUkJCYrLz9axrmtpaGf
+oKGmq7C830g1LiwqKy45RvTLv7m5tba8xfZQPjYzLCgkIyQlLjlW2bqvqqCdnJudoKasuM5I
+NiwmJCUoKzZF7r+zr7G2u8XqWz8yLisqKSsoKSorMj7fu6ynoZyamZqcn6e00z0sJB8eHiEl
+Lj5wyr22s7W3usl1QTYsKSooKSwsLjI5TuO4rqijn52dnJ2eoqmxwlI2LSknJicoKy83P0VO
+WvHp5XxoWlpJOjMvLzAzO0JKe9i+tK+rqqenpaanp6qttLvL9UM3MSwpJigmJyotMjxHU2ff
+zL/Ey8/sT0xYYczAxsO4vMW8vLm7trCvsrCuub7M3E5APDQvLSwtLS8zODs8P0VKXG1g2VnM
+0dC6177Rydde2GLP3v3NwsvGwb69xb2+u8fFwObS62lrR0s8NTktMS4uOS5BRl3K3sPNyM3O
+y9nPxt3FscC8zbW6R/JI3jo+Xk/NPsvE+d3hxE9qS0dNPk1NUTxOSD1SRGd3WsfD2721vbvC
+wt1eWEZoN098ZPrrq7dV2bDmTd/ouuhWx6nDW6++XzovNC8kJCwoJDI9T9PLsq2xsKm0wL3r
+5VlDQkw7N0VMUOWnqCy+rEfCSLnBrTtVm2bLx7jnOT8qSCcbJSclKEvitq2onZ2iqavKWDIo
+LCQfIi0tNVXLn5c/35xlyji7xKrCL5Wu1m/PcSIoGisqFx43dEi1qZ+cp6ahqOdnSTguIikw
+LSc2Sj9NtJWqLKm7rncwzq6fHrOdt7FLuVH9Hxs5JB4fPd64urmdn6u5r8s/LCYrJyYjMz9a
+zsmwsKqamzxZzjdJKjr+nsTonK2t5kMuLygTGyUkLEO9qJyjpaCovk0+NjgtLDhITEvWwru3
+v87I2dufljotxzBnLCo8n74mpri+4TQrMj4YGikmMD50r5yjrqKmtc5NOmZELDRDTFZ8zrOv
+tLO2w93AoJ8mLlA9yiw52prANqbdu0cpKDY0GCEqKzk3YK2hra6nqazOTG7XPS8xN0A2Nk+/
+usPHzczQq5xBJ+Ipzjsvy6CeN6Crt7IqKicwGRkuKUM9V6+foqufpqm4W2RoPi0vLzg4LzzX
+zeDDv7vL7auaZSa+K8EtINW2uSqjvq+tIUM2KR8fKCc2KkKpq6Ohnpqeqq6txEg3LDExJyo0
+R11f07231eCrodlCuTC49B2+ykXgX1K7vTHa3DZNNixGNig5OkrAz76qqq2oqq+ywNxvU0A7
+Ozg5Ozo+SkZESkpb7dDHyL28vrzBy9BiTUY5O0RCR1BTWFxUTEhFRUtPXuHOwbq3sK6trbGx
+tru6u8LSXkxBMzEtKCcnKCksMzlEcdLIvLu4trKur6+ws7jF3F9EOTYzLisrLC02Qm/Hu7Ou
+q6qrq6mrsLS6xM78T0Q+PToyLi0uKyYxTXrPv73Fx8C+s6+1v8PRXEpAPzkxLiwsLzM4RFjT
+vLWuqqmoqq62w+lQQDs4Nzc4PU7u9e3TztLN1c2+vL28uba4vsXiUkI0LCgkIB8fIycrN0zM
+tKuloJ2bnJydoaq52z4uJyIgHh8jKy4yRlNj9um9sa2pqaWioqOmrLrhPC0mHxwZGBscHyk4
+d76vqKGdnJ6en6Onr7nMTz0yLSooJyozOkFq1MXEwbmzr62urKusrbK92UYuJh8aGBYWGBof
+KUDHrqSempmZmpygqbHA8ko8NC8sLC01NzQ7QU1g3b2vrKqpqKaprLTF6kIyKiUfHBoaGx0i
+KDRUv6ylnpybnJ2gp6686kM6Mi4sLDI6Pj1BPkJNTOS/tK2sqaWlp6q1zWA6KyEdHRwdHyEo
+Lz5uxLGqpqKhoaGkp6uyvupJPDQuLzdBQz1HSk1SbtS/s7Cxsayqq6y4yHw7LiUdHBsaGx0i
+LT36va2moZ6eoKSqsbrD4FdKQj0/SUY5OTk4O0NYyretqKakpaaqtc9LNS0oIh8fHiAjKC86
+Td7Gu7GrpaOqr7G8zuRLNzz+cvE2IiQsOTVJvKqbl5ucm56ir0cqIx8cGRgaIi04RWe/r6ur
+srSxtr3cUUE+PzQuNL2fomEgFxsuNvyxnI6Li5GZo6/hHxQQERkdHyQv2a+tvtHt+NVfPjVF
+yL3FTzs2Nj/Gqrw3GxIbKMikmpGOi42Qn8s6IyEdGxwiL1LAxsTQ4/xFNy4vNklv2NLw6GhJ
+PN+suT0aDxMdvZ2UlJWVlpepVyEdHiYuMzs+2r6wrrPNUk9qzMrXUkdDREQ7Lyguxa68LBAO
+EDSgkI6VmJ6bnqNdJhoaJ0m5uMhESdC1r7Z1OzxSw7TESTAoLDA8VL5gLBoRFRpVrpuZmJyh
+o6evz0UtMDR+wLK6xlZZZMy9xcToe+/c33Q7MCckMsCuvisUDw8ouZeSlqC7ycGxs8Q6LCg5
+xqylrMpCOW7LtbO+/UM8QkdEOi0qNOnSXigcFxs5r5iYma7RQ0/8wszpRUBfxaqqqrnVYXDP
+t7S0wXRENTAqMlXOWy0XDw4bbJ2Pj5WtUi86XsTF6jg1P8+tpqWvzEU/UM63r7e98zwvLC01
+V0s2HhUQFS2vlZKTpNMtJys/3MPXU1natqykpKm40mp+3sC9wd9bS1TKXjgcFBEWLLuZlZam
+zywmJjA+T1FPUGq8tamqqq+2wczLytXH0OBMOTxO6lw0HxcSGymum5WaqeUsKTPvvba/0kdW
+WMy8r6mnp6u00FNGYr2+PR8PDQ8pqo+Nj6QuGBQbLOK7vFtOQ8m4qqmtuc3awbOqpKSpuXAy
+KSYvNDgoHBYUHz2il5OeyCUaGyjeraaqtNvg2LStpqiuucjjzbSlp8shDQkLH6+Pi42fLBUP
+FyjhvMg6LSlKvqeiprDBy8Gvq6iprsVTQFhs10IpHRYaJcGfmp69LRwaJFK4qa67Rj1Zwa2p
+p6uzu7urnp6vJw8LCx+0kIuOnywWEBkq07jEMSsrbq2hnqez0vjNu7CutL1rXt7H0jogFxEZ
+J7iemqNRJRodK92xrr7dRHC7qqGjprG7tqebm6ssDwsMJKmPi4+rHREPGi3QxU4kICdTraCf
+q7560riqp6myw1XbvbfPKhsSFR5qoJyf1CkcHi3Rs6/LSTxNtKagpay2tqqcl52/GgwLFc6W
+i46cMBQPFidW3zslHCIwuaejrMXTxa+kn6KqvNC+rKy8LBsTGi21nZ+xKBsXIUG5tsg2LDF/
+raimsby7p5mUnM0XCwwaqI6HjaIjERAbNsXKLh4ZIju0qau66862pp+gqrpp2biwtjkeFxgv
+vJyerisVEhkvwK+7UjJB362mqrTOxa2bkI+fMw4KDiyZi4mX2RcQFillyjggGhwybrK4zEpj
+tqKbm6Cxy9qzr69MHRUTKbealqBGFxEVJ3WuuuI1PNKuoqSswdG/ppiQlsAbCwwWv5GKjacn
+Dw8YLmLVMSMeJkzEs9ZKO2Gtnpqaoa+/ya2nrWccExAev56UnsAfFhckPsbPPzAwf7imqKy4
+xLywo5qWne4aDA4ZtZGKjaMuEhAVJTpQLigkL/u8r8ZqQfSzpZ6epK26tqWiqj4XEA4ex5yW
+n8IgGx0qQtVBLikx3K+hpKezvritp5+cmZvBJg4LDieikYyVrCQYFRwlLSohJTDKrqWrvG1X
+1bWrqaqyuaqcnacqEgoLHFaZlpiuOionLzc7KSMhMs6pnZ2eqa+6urzAwbGjnqZQGg4MFTun
+mpyl/jYuMzovIRoZHzq2oZ6fp7G2uba6wb+rnZqfxyIUEBksxq633D9L5LezxzAcFhggQLWn
+oJ6dnp+iq7nZ17mqs1shFBMbPbemtF0yNGKypKfCKxwaIDL7u7aurqiinp+krbu0q6WvPh0R
+EBcrTsZCKiMoS7Gfoa5XKiMqQtW4uru/u6+npKamra+rray/NSQdIS7fzWApHRkfNcGpq7b2
+P0Tsu7O0zU05QFvLubKusa2lnqG+PicnLmG+wzofGhghNdG9y0gvLTNRxLW1wNxq17utqqan
+qKWem5yqzzUuMT/cTy0ZEg8WIT29ucdPQEjMrKWkrbvZ4Mu0rKimqKytpJ6hvjYhHiAtPDkq
+GREQFyE30b/HW1zDrJ+cnKOvxNPMurGrqq2tsKyloLA3IhocIjQ5NCEYFRkmQrmqqK7Fw8Gt
+qaertsZp/Ny5rqytsbbCxbmpqNEmExATHjI9PicgHCdDtqKenqq2v8C4u7nM3k9a3buqo5+j
+pay6y9O5sWciDwkKDiNUqaqvwOa8saajp7g+MSs0Scy8sa2sqKejpKauus1dQzQ8Tdw9JREJ
+CQseeJmTkZakrdnNZEoyIhwaIjS6o5uYl5ufo6yxxM5mUV1GRD9fxs89HgwIBQwer4+LiY+Y
+qtFUMiweGRYWHzeznZeVmaCsv8tsWVtR71FWQU/MuLZdHQwHBQ8omYqFh4+dZzQmJSYcGxgd
+LcqdlJCSnKvrOzU5VOzOzmBYPj04UbbC/hsLBwcUOJOIg4eRojsrIiAhHRsaHjS2m5SRlqfS
+LSgtPsW6tb3Zbjg3LywuMFFCNh8QDw8prY6HhYmVqDknJCIkHh0dIDi9n5iXnK9oLyotQci8
+ucBfRzIvLS0wLTRFaU8sGBUTLK+PiIaIlKM9KCAdHxsYGhsyxpyTk5es+iYgIyhLxrKttrV1
+QS4nKSMpKjzlZOQmJR4tspqLi4mRnrwyLiQoIRkaFyQ9rpuanK5pKiEnMF26t6++zkgvKSAm
+Ji02RLmyrL8mJRclbJ2LioaOl61BLx8bFQ0NDRk3rZaUlJ21ajY4TU7MeMbPx7pYRyolIiQt
+M0rEuLXHLywfQ66YjI2Mm6d5LS0eHRQNDg8eRKeamZyirKu1r7pOQiw6O09hNy0mKzVWvrm1
+rK2uvDAqHSvHoo+PjZikvzYuIhoUDQwPGjyunZmdnKGfoKmvSDUrKC0vLislJio918G3uq2p
+q6xPLR8gQLeYlpSbpKmzt+UxGg8LDRMeN/G4rqGYk4+Sm6tWLiUfHRgVFxspS7qrq66qn52d
+qjEgGCLaoJKVmKOmqaixVB8PDAwRGSEkJCpJrJmSj5Oan6asuUciFhEUGiUxQEz8taCYlZee
+xlBdza6srMroTMm1s71IKB4dHB4fHh0eJj3DrKamp6ainpyco78yIiAoMTgwJB8kNcmspKKj
+paCenqbBPjZR1M1VLyUkLk/P1z0qIyMoLS4qJyk0/7Wqqqutraahn6Orsri4uL3QSTUuLS4y
+MC8uLTJHx7ayuMPKxb64vdVVU+XP2E05LiwtLSonJCgxRtG+vbiuqqakpqqvr7O4vsx7VkxT
+W09MSEI6PUFBP0ZQVmzhy7+9v8vc6+59Vkc/Ozw/QT0/Pjw9R1h/0MjAwLy3trS1s7i+wsfa
+bVdOTVBXVVRUTUZDQ0VGSExJX/Tvz8jK0+ppV05HPzo5OztETnjSy7+8t7Oytbm6vr2/w8TW
+7F9TS0NAOjg6PEBPU19za+p9/35yWVleYmX94e12XE5PTU1LQT8/QkpX6Mq/t7Kvrq6wtbq+
+xtLZ4+txWU9SRjw8Ojc2NTU5QlF83cu/vr++vcPR2uBrUF5bWFtWd2BXWE1NUl5k7c/FwsW9
+v8bJ3OpnYF5YV1VfWFZaV0xLS0dIRUdOUVNp69zLyc/W33JcWl1t7dXIwbu4u7/N3V9MTElK
+TU5Za+jY3fRvYVZSTUxMTlp4f+HPz+dxVk1KPkJBSE9c7dHBwMnN0c7WdPr17d3OzMvExM/j
++HZ+XWreeXzh3eD0819aTklKQklNUmju1MjBw8vN225hUUxLS1Js9tbM1PpYUU1IR05YX/vU
+z8rGxtPv7WNTTE1OT19v+enWztHa3+rvYX3mbunXztLS1NtvVllPS1VdadzLyL/BytHxWU9F
+Pz0+Q01fdex7eGVdV1ptan7y3M7LxsLK3fNoUU9JRU9Ua8/Ky8jH0eD5cFxOXFlW69jX1tfa
+bltSWFtUWl5u39zg1NTZ7O54ZV1aX2fr1c7EvsLEyMzQ/VdcTEhpTztqu+9T3ks3Nz2+PzJN
+XdfMx3XJzkxjd1Bt/1nZyttgw7VebeBZfs5HOMbk+ri9aVHe8T85Qj1FR1rRxMPQtbrcwsvk
+dd/Y6UpIV09XXEZMS0RPYmR5y8G/yMXL62dfTEhKRD5IWl1f+9ne9N7S5NvedmbW5evF0dXf
+XldSWF32c37JyMbZ59Xw73lqVdxcU9Xv7dPe5dPP18zO4tdmX+tf19/3bltNQ0A5Ojs7Slpw
+89PZ+t7d4vbu3tpv7rnPbM9pVlRRT1FmXfrJycbBw8jVzdDY3PxZZmlaaVZiV1dhWnZYWEhN
+WFNl8fLc7V5kT1VZU1Rl9WvPxczDzMzN6mzxSUdQTllca1ZoX1peYOp48tDZzcTLys3I4NrG
+WTU5Oi82Z9vAsrO3r7jBx0w+OjIsMDs+Z83FubW0s6+xubrL6WdEPTo2Mjk4Qa25JDBFIB4/
+W9inpqKfprOuuDgpKR8eKDBIvq2qpqatsbvU3G9HP0E3LjEvMzR+nJwdH7onGTmnrZ6empSb
+vsG0IxceJR4lQuW8tLGutWZG3fdG78VLMjI1MDM6PTk2Ni44m440GbizLDidmJObpJaU1Rsy
+LBMPHjE6UWC7rdI94Mo2NMy4v83gY085NVXaQjlJQC5OkpEeJbHGMHybmI+nup2nGA0dGxYc
+ObyouH+xqjYiPttBR8m8x0kvN09BPmDJVzxAT9G8mIy4Ma+xt2qrqpekJUfaKQ8bJitSTd6x
+qDUjNjYsOWnNrrLg/NQ8Mj0/SmhMO1bHvLStmYufHCUnODNHy52NtWPFVyEaIiHnxnLRsdgq
+Li49xrvVv75GLi4uO87FvbCvucjL1Gaejb8bJB1Cwri/kY9OQSYeHysjMqq1urHYNUAwLte7
+52hBKCkyOOCvqa2+Vjc3NjZIpZK6IicfuKiorpeWak4eHCvr38Kls7rWKB4oMTbUv87Ibjw9
+X9C7tb7PUDQvLjFXyudSno22JBgPTKqmq5idsaglGyxLz7XKKzQ7JiUoN6ydo67AUU1MOUPL
+v8ZMMDI/SXN9tJS2HBkNLaOWl5WXsKIzGSQvfbS7LTM7LDk4S6udoKxMIyYwOFvLxbrfOS8u
+MD9PPjxFSdyakdo5HRylnJiipjQ8UhkcIjWvnq5sSTI+STd6raagqFEvMz16zVxDOCwuLyw8
+w66TlDsrEhm4oJqdnzjRTB8kIzS0pLxnMCI4TGK1q6iirEotJSc7SEtrRkJjWEdj8dC6v7eb
+oTYuDxfEm46Plzo3Jh0iHCPvpKCeuygoKju9tLOvuctULSk1Vb6ssL5bLywrLjranpm9MxES
+MK6Uk5hTODQsRSomLcmrnqNGJBweOMe3rq+6ub/eXUJG0L67t+g8OjQ5TXmumKZZJg0WMKeR
+jZW5xDQ+OyIdIS7KpK/fLBseMeq0sb64vb7EUjc+WdG0tX45LCoydqKfw0EYFjCvko6PqM07
+LE4sIh4jMrahprwrHSAv2q6vs6+wra6+RjYyQNHK9jcnJSznnp6yPBEWK6qOjJK8TiYsPisk
+GholvaKeqjEgJDTFqaars7iws8Y+LCcvSlxdOywoPKeXnasdDhkymIuJlbkvHTI0LB8VFiiu
+m5aiRyUhLcqttrvfzbzDXzQlICs4W20/MjytmZqhMhAVH6mPio6qQB0oMi8rGhghxJ2TlbAw
+HyA4yb7B1NK7tLxSLSMmL0TMzU9Jt52anbsXFBtNlYyMns4hIDIxMh4XGz+ml5WnNx8cJ0Zp
+c0hB2rm1xkMpKDRPzcxYOUqqmpuiJw4UH6WOi5KuQiI72FMzGxcitJmSmMMlGx0rS09DQvqw
+parNNSYrP1zTWS4pVZ6Ynb4RDBMsmYyMnsk9N6+9OB4VGDOgl5arJhsfLvLFUUvYsqGfr0ws
+JzjTyt08Jiq0mp6uHQoOH6eMipKsvk29qzIbEhImqJWWoDccIy9L3T4vT6+gnqtJLC4/zOI1
+JBsd7ZujxyENGNCWioqZbtBgy7YhEhIcbZmRpF4nHzfaUUI3Pqybm6DGKSQtMzoxIx8gLKmW
+p2keDyWulo+Ro9uqwcdRGRMcNb2grzQwLjbsXDdVvaibna5eLiUvPTY2MS0uNsecn3YqERjn
+nI+PkqWgn8BVGA4QIEO/rkxA1cnHzUM9vKuio7RQPC8sNTQ+TkEqIjSookkgDhm7louNkaWf
+pDwlDgsQKu2/r7upo61wMCk+saqts7vExGQ6MjBBSi8gJcWsQisVGLmaj5KUoJ6dORwODhkz
+Q1yzq56dtTMwNF29yL+xqaiyOystMjcuIR7hpzIcFRa0m5Wak5aXlzMYExkcKy0vtp+aoLNJ
+1b/+T0vcr6Sx0kw3MS0jISguxq8eFBogtKOdnI2OlptIKyklFRskMraqtLquuLvcP0XS9NfH
+y8b8NScqLTE0PbS/GxsbKsSro5uMkpahvzYvHRIcHyxSuLqnoqqwtMnr803vwr/XdTs5NzEu
+McupIxMcHC8yfrSPj5iZm6tQJBQbGxcaOc6soaCemqO3xcHI50xLZE08PEU+Oty3Ih0nIigq
+OsyZnJ6XlaS26S4rHxUVHyEq46yhm5ycm5+tue45LiwsLCkpTLUvJmU9Li1BWrDFy6GeuLCr
+veVALiwsHyAvNTjQs6mioJ6dpa+9eTosIx8fICwxLD17SVO8u7qutbSttr6urr7Cz05OSDs7
+Ozc/SU/Pu7Wur7O0vmdDNSspJSQpLi8yPVnfzr+8vri2t7KvsbCytri7wM5dOzUxLjE9Qkpt
+z727tbO4v9RlUk1BNjUzNDc5OUNFSv/Xyry4trGurq2trrK8zmpBPD00LzA1O0NPX/Dna2Zc
+ZnpaXPf9bVZbVERFPz5IXHjWysS+vru4uru6vcK9wNT2XFJMSEhIRkZGRUlLTFZaWu7o69TU
+alZLPj5CQ0hPcHXRw8jKyMbOyMK/xsnJ3NrS2+F2XVVOTE5PS0hFS09RZ2BdXlNOYnlw+Gp8
+3c3FwsvT3F772ujrYv/w1s3X3n/+c37Rv76+wcPBxsjQ805FPjw9Ojs6ODg8RU9kZ+3b3MXD
+y87S19bMztDeZllZTVFgXVxnddzNw8XW2N3u6P1xcFRWVk9ce19cYVRQ6drPy9jUzsvQ22NU
+VUtNVl126n725N3Z1dr4a27x2u5yVUlIRUhKR0RGSl3s3s/Ozs7PysbM4fNlV1xmYmN0bGXp
+3tja3+/27+jNyMHG1djX8mlPR0BBSE902srHyMrIys/nVlZNTVhRUVRNSk1LTVJTYmVw19PG
+yM3Mz9/d52VcSklOT1Nf/+Doz8zLzNPb/WR+437i297Y931dS0dEQklW9dLMycDDxMrN8WFf
+UU9PWV9VVmtWWVVdXlpvc+zkzM3U0trfe2hMQ0E+Q0pVb+HOwr+/v8fM4nJXUGTwemvo339p
+VklIR1Ficc7Ix8W+y9zeVExDPT9IVt7WzcDBv73Eytbf5Ojo3ONka911Wk5CPDs5PEpRXnnn
+0dHS0eJvVUxBRUxWcmvmzs3N0uDxa2vm29nKwsC9vsfP71RFPz4+SFTo0se/wMDJz/xkTkpJ
+Q0VKV2B3e2NUS0pNTlZn99bKy8O/w8rmXktFPkBARlX708W/w8jJ1NXvWmRWWezb08rM1+la
+S0pMSUxb886/vr3CzXxUSz48PUNGT+7Rx8PFxcnOzc/R0t565dDi6djoXEpAPT8/P0hQ+8zG
+x8vN71pQTUlGOT58SdDMw8tz/UpkZnru3drQvrq4urzA2XBaTUI+P0VMXOLZ1tdpXFtbSVNQ
+SlRi0tW7Z2ttTFU+WElQUdjLwb7Q19Xd6M3K6ExDRFNi48rR4evm/XpbXmRib87EyMl+an5E
+Pj5ERkJP5c7Gxrq+wdvr3f9LQEFBSUrWzeHzy87UzNfM29zPxMvS2dzW3lZNR0Q+PU9cVVDn
+3mZl/Nrta2N8WE5QVllWS131cF1Td+Jw88u9ube1usHT/L22fjw6OTIvMEXa8HW9r7O+0M9l
+Ni40OTk3QtO+xcW3tbm7v8PVTzssQqS2Kzg7PDQoMauxOfW2vNBETLpcKjTTu7zLrp6jr6yo
+sd08OkIvIyopLbDDIjItJywlLa+kv6ifoabGScVUKS85csTcsp+hp6yvrMMtKisiHx01pUkg
+OzJUMSc8psI+s6+pulV1uT0qN0/HaUmxoKSmqKmu1T1WXzUmRZ1THSweOR4VIq6vS6iqna5O
+bK9OKy0+s8ptraGtue/CuUovQj0u+qM9LUccWyYqUaq006zOrUQxM0IpJzX1qK2pnZ6qsb6w
+uDwqLi8uLzBHv52wLd4jLSUaLsmsyp2emqdRVk4qHCIqwr6+pp2jq7zM1DwyNji5ny8vOx8t
+GBwyslu/o6Sf0Ffc5yUlK1G71rekpbxaR9TC8V2+vsw7NJ6XJjwrLUUYIdKiOrvAqbEsKOdO
+KjdRoqG2r6Cx+S0vPComPWzjoaNEsywhMRklWK7Pn6Ofpj84OCkeJi3Oubuon625zsu35T5K
+R66dKjNKLjggKc6hRMCvrr80M+tVJDFlsrPOwaSueFfUzHA5a7nNSUOlnS8lKCUoFyO1oNmu
+p6PEJiY6Lx8ryKGlrKKcr0o6SUYsJjVhpKUywFk3LyQxu6ntqaamv0I3SS8fK0bBv9a8q8JM
+VfhMPjfcu89dW6iaMB82IikZIdmkvq2cnq02MkI7IjPNqq28t6q5QDpCRTk5WMi9op/W2jYl
+Kx8mSLLCqqeqtD4tMiodJzXGtbOmnai92FdSPTBG3qKaPbi/LS0eKD++MaufpLJbSD0lGyk1
+VejEsKq/y8LQPzQ2PT1WpKHHvW1JNyIp6cs3v7SvvPrfwT0mOV3XxbmpoLXvz2g9Nzdey/n/
+6Vyony9DPB8jHSjaq92mn6vSNjQ0JR40S97Cr6iovMfAXz5CU85YPaSYNjtYKy4cKN6uSqmc
+obF9ZEclHDdOXc+wqajHzrhvMzE4Ojk7t5qo8qzNRicmN0cuNrC5xNTAyUIpOFg/ScSvr8HH
+vOpFQ1BFP0j/3lGznFw93iYpHitOsdywnKeyzc1LMyEuOjNPtKuprbWx0jw/TT0/Q01Lu6rr
+z9c2MisuPvJHu6u2z9xoQzUuPj07WLy4uLiyt+xKRz89RVRSbZukLqpLLCoiLDs+MqCmqquo
+tM85MzsnLTlERNzDr7TCwMdBOlJGOUajozeztUhIL1NRRi61vFXStrxLPUN4P1LGyOvBvLjJ
+1MXjPTg2Ly9Lr3VAr/g9Nj5FRjtbtOjVurTP5/VpPzZEUkRuwMrExbq3wcrAzHJhQUGsvS+6
+Ty40OUZcU12uyNLFyGheSkQ9OlrkZNHDyMna0uJRTVtANi8ypK4iocE2WT9TPm48rGLPurnJ
+u73YzjtVRTc891TPxsfCxsbQZkE9z7cmTbYuSkjgTMzMsLhqus5RP1NBST/q3E7Ntr6+tLzL
+31VJPTY+PDMsVqk0O6Q6RU1hQltsvrfotb3H279aXEFFPzM8XWHuuLm7uLK1wdvkPS61xxi4
+OyFSL18+xEit3sO42eDUzFfNW8fc8cO95MPL3nNTT0hCS0xLTDvJpzBHoyxJPUQ2YHjWwE21
+z8rbxEFLRT1BOkjNzdSwubWytbq7ydZaNzLFvhu/xSDQTsVmscaxvty4S05NWTZPP1FTS+Db
+X9C/Zez49W5eVl1FREREuswsr10sVkVTRcdbtcy4uL/Fy8ti50hLSFjf0u27trqytL7J4FI/
+Lyg2uhsisRtAU9hkt7K4rXer6Nl66zVJSD5aQd7Q0sW3wra0uLzCzd5aT2c+NUmuOB6oLidC
+OT43v1Kq9ayvvLq83E7iNU01PVtRWbu8xLS1usXJ1V09Rkk8OlQ+RKtLKqcxMuRDSVHPTbNd
+sbnNycpGPEEvQjg9Vnp4ta+up66zuc5JRTUuKT+rHyakH0zezWbKsVumUqi8yc/LSzjxNX05
+WNtUT7u8z7rN1mxPUlg/Sk46MUy3JS6qITI/SkzGsq2ovaG2vb+8Vkc6OT0uStrrza2ws7K5
+wc7uSTcuLS0tMz01Ua+9M62+M8ZK1Ux35q+6uqi6tsrfUko7Tjs3VFF7vbG2r7bB3UtEPS8r
+K1ZZF0X1HUpCvNSur5+qtqXQ6j1ELzcuPkg33r25sKaqqa64wO5VUj82NTEyLy0pO7IqIbEj
+MlRZsbuuq524pK25w1RCNi0tUC9F4M66rKursr7DTDYvLy0sLS82vfoxtHMuUTvn+d+7pa2u
+p7u25l1IPCk3Ni1EfLuwqaakqaqvv+o/OC8nICImPkok0WYu5Eu4u720orC2r724Z2pwQi0/
+ODA5ReTZybSrrq6ut9JKQDcrKCYiJk44IW45O0s8r7Cvp5yfnqeoq2RGPzYtLyw5OEvHvbOu
+ra61xttQPDQvKyglKD47JjU9LjYs7MDWt6WkpKqqo7bP3l03Lio0QkjItK2opqOjq7fRRy0m
+JCQlJig0PjE5TVpcP1nQ1P26q6amqKalrL7O30wyLC0xMj3fvrm1sK2xu8TjUTsvLCssLDk/
+NjU8RTw7SnXMx7y3rqqrqKartr/MXDsxLSwrLTI+WeDJu7e0s7W3v8vgT0U7Nzg8Njc6NTc8
+RVX00cG5sq+tqKepqq+5xWs7LysqKSgtLjhE/cbAvLu5vc3cZ0w/Ojo3NDk7NjpDYsu8s66r
+qqyrqquutbnC7Ec3LyspKy0uMztM0723r66yusxzRjg4ODEtLS4yOEThxry5r6qpqKamrbO9
+x9ZUQTozLiwtMjhDX+DPy8i+vcDH3kk2Ly4tKi0tKi03U8G1rKajo6Sjo6arsLjQVkM8Ni8u
+LS0vNj9O28fCv7y9xtFaPjcvKykrKy0uMjZD3r6wq6ejoaOlpamutb3WRTcyLSwsLzY7PUFb
+4ca9ur3K0+pyVT89OzcwLS8wNDlIYNC8s6unpqOio6eqrbXE5kk4MCsqKistNz9BUW7u5ePl
+6+x5W01EPjw7OTU3PUNIX9nHurOuq6qqqKmtsbK2welPPDQwLi4vMTU5PD1DT+7Qy83WzM/X
+zczO1X5MPz0/QEdZX2vZx8C8trSytLm5vL/J6VRIRDw6ODg5NjUzMDI6P0RV+dfIwL28uLW2
+ub/L09ja8+zc29LXz83Ixcvdb1pWT1piXWhy9251fPFkTEY+Pz49PjxASlT10MzAv8DKz87M
+yNLb3NzydGZjb2ZkZV1VUFNi/OPV0s3N0NnX09tqW05MTURERklMTk9UZubo3NXa3dXQ2dvT
+zMjMy8/R0Nr1Y01IT01JTllic+Ha2NbW4mZQUVFOTEpKTlBcZFxo8O7s3urk2eDZ19vQ1dDP
+1c/L0HlmYVlZV11p7+bey9Hfz9TY6fnrd2Zna11dU09WWl9mZF9iavvq6HXhzNTZ0tHP2eLc
+/WtoVUxFQkJCQ0lWYvbRysjIysbO43Bq/mJaW11SVFtdT0laYV116OLPzMjDwsLDw8vS3+zd
+fFpYUE5IR0hJTE9VVV503dfv3tzh7/x6bFhYWVZVVFxpVFNYX3xm3dTaz8bDxMTHyM3X4H1t
+T09ZS0hLUFFYX+LX0crNzt1r6G9WfN3Z4d7m3Or3Zk9KRU1OUvzWzMbDxMjHxM7edU9KVkpK
+VE1IUFc/TGP37jBERvnG1rtsuL3G4V+4XsxXU0A8TkBrXc/LwsDEvcDK0cfZ5HZlVEJLTlBI
+Qk5BRj1AST5QU/vp0sO7v7/DvsLX6FZPQUpHSkpffX3a6s7M2dnPz+7N68zO1cnOzOPH4GpL
+Pjw1Njg9PUBY48rBwr/CyszmUldEQktX7cXOxLvnyGvn51beTc7Zwr/DtL25x9PxV0dDPzk+
+PE4/RmV6Y0R2VFtLQWpOSELS5dnQe7vLVV5fSERFQeF378K4ubm8vLLUz7/aS19oSvRATOxC
+Wv5wSd9STNVF+OpYY87M1dPfv8ZXVe5FOjk4PTk8ROng0sXQwszK0sbXc+lk29zJytXU18h0
+RUVGTkBDVuPIxL+5trq9vsXfV0Y6Oj48NTg+S1t43ce+zsvMycXZ18/V0czZ6u9cemVIPz8+
+QEc+PlFw19XUzcm/wcjLwczr/33pYFFNSUVJTWT9WktBR0xOWujFvbavsLa6v8nXWko8NTc2
+NTpBT+HPzcnT2s3S1s/h6+JkYV9cUEpJSElVWk1HRUdW683BvLm3tLW5vMnb5F9RSD02NTk9
+Pj9HV2xs19LLwcLAwMXJy9J9VE1TcGNeS0FDTPjLv769v7y2tbW7y3ZMPTk2MzMzNz9Z49TU
+231ZUE5MSEpLT3jY0MnJy8bAwcHPXEU+Ql7Sw7zAys7Mw7q8x+FIPDw8P0VBPj1AT+nRycnM
+43fl29LS3V5NR0dV4sXAwc1aREBM+8i+w+NTSkxvzsPC1VtFPDo8QEZMSkZIWc68s7C0vvdH
+PTs+S1peWktK88Gvq6qvwF07OlPTu7a7yVU/Pkhi1M3dUjkzNjxIaeBcRT0/Tde6srTC3Ec8
+Pj9TYlNNSlD6xrmxr7jG+ldZ5czGydt8SD5CTGPsellCNjI2PVfYyr68vb+9u8LM/Eo5MC8y
+ND50y721sbCxtr7F1ntjXm/5amtaSU1MTExJRD45ODs8RWzTyMbCw8TIz9b7T0I7ODk/SFvP
+vbWwr7O6v8PKyMPG0O9RSUM7P0ZSbmNVSEQ9QE/nwLa2uL3Hy975fE49NCspKi46T8Wwqaiq
+tcbSfPHLv8HKakQ7NjpCYeTyaUU6NjZCYMe1s7S7yu9PR0tJRTswLCwuNV24qKCfpK/GZEpY
+zrqytMRgOy4tMDtaaFc+My4tPOe1qaisuulCNzQ3Pj44LysqKzJYt6efn6Ktwt9S2bqvrbjX
+QDEuNknZu7zORC4pKTRkuquqr8JOQDc3PkRCOS4qJyw81KyhnJ2msPZCQErJubOz00M7N0bl
+yb3KQSsiJCs/vaynq7jRTDw9PD05LyolIys94LCjnp2frLtYPkdL1b65u8bS6m3t0/FsRC4p
+Jiw4abWrqay73Ec2NjAtLCgnJyw7XMSvp6KfpK7APTo8XbWvrbLOWEJC69rmWjAnJCc50qyh
+oKOuwfI+NjAoJyEdHyMvY72zrqekoKWvyjMsMFOsoZyeq79VPkRPPzIhHBsePbCemZqgsO1C
+MiwqJB8eHiYvUb+9vLOsqqe4QSkeLW2mlZOYosFANzY5LiEcGB0twJ6Yl5qouus9MSomIyEp
+NUF92eHjYVfQt6+rwS0oIz2nmpKWpMs1LTo9NSodGx41taWiqLvX+23qQy8qKDBXx7rGVzw5
+RmbetamurlssMC3ApJ+cpLbYQ0VcOCwmICxKw6yutbjDubXGXTQoKjJGY0k7Mi4/e9rgPV+t
+p5+sLCEeNaabmJ/SO0xuv3YlHBkl1qykp7i7sq2t3S0fHShE/Xs+MTdL0L9YNDavnpqeLx4f
+NZ+Xl6NXNUf0/DgZFh04rKWruNG9qqm1TSsqNkRtQzMwLzI3My8yPU7Zqp2en70wQEW5oqSq
+sL3Cv00sIBwlMkbPxsizra+4aUA7O0ZOR1Bqan9IODYvMjRAoJmmrh8ePcmenaatpqupwCIb
+GiA4TDtSyrOiqvczKjPM08vM4b+43D0lGh0nM1LU3rSuu7uknaeyMRw2yaeZnqKhqbhPHxYY
+HCpDebapqa7LLiQlJCszTLKlpKW6SDsuJh8+mZ/LOhAfuramrrSmm6TWNx0qbkJOTXqtnKez
+yThTRCoqLz2yq77hPi06NScnISQ5ZsytrLGvnpSf4iMaMMi/rqedmJy2LyMeHx4dKfysoai9
+ylc5MCYoN0vUzvHYy+xfTkVHNzmekLk/JBN7wy6+q6GZnnVATycsIxxVr6ueoa2muS8yKyxE
+LzDY1mFYNjFTTTMvKjFbTuu+noyTvDwnT1QiKr+lpq1SY78zHhodNmpGy6ynpbpZyMRPNDBI
+wuQ1NT1NWDUvdLzHeUqfjp86KiNISR87pJyfr8CsqS0ZHCI0NTOzmpyksriuZx8eJy83Ljq9
+tXI9OkQ5JB0vnZXeari0oEEzrZ2sREplrzkdJzM8LSg9p6jU2b2us0hKtrz5TUjb2zg0PTYr
+KCtDakvJsa+xt6aVnT05OjE1Ijaln6yzq6vbHhgfHhweNbmrsq+mp748PE0/Mjy/tsfQwbvX
+Py0srpPaHr0u9Tcdpp+otaumpVAfTjcnHx0220A/q6KkrrWstzo0QkM/MUHIWzo9SU42Kzle
+RUH4t621vaiYmsLcuksuICvCWjbptK7UNk9iLCIjKi8qL9e8uK6knqeyr7PSPjhANisuRXBI
+RKeStiu0LkQiFb9aVc6vn6XDuaRgNSgpMB4fMk5V6LWho6ypqbNZNDo1KisyOz09Z8HMxr++
+ucW+tre7uquryn5xTUAsLD85MjA9VURDXtpqRj9ISEZObdDKxrSurq6xs7jGzM18VlNSWlRM
+TUtMTEc+UlpJaVLnztzQzMzsaklZVEdLQEc+PUZNU1JRUWLr28/FwL+9tbS3t7m/zd74clFH
+QkNNT1JUUkw+PURGS1dlZG7UyMrJ0HdUTU5UX1JMU17u7fb2fVlNW2v+3NvW0Ma7u7u6vMXC
+u7u9xMvN3N3rWk5JPzk5ODk5NDA0OkZTXOTWzcjDw7/AwsTEv7y5usDL6ltGQDozMTY7QVfu
+2c7OzMK+v8HGzc3P0dzg6VhNRkM/PT09QEtY5dPR0NXQ0NHOyszU5d7X2uHqfV5bZXNnXk1J
+T1FSUlBWXlxs5dLIxsrLycfGx83cbWxeTUlFPjw5Ojw8Pz9FS1D80snBxMnGv7+/wMXFycfJ
+1OFeT0pGT1FbYFBacPHi3dDY9F557N7LzcvHwby/xcnT5GtbV05NRT8+Pj4/QUI/RU9ddGrl
+2dfPysHCxczMz9rW2eBsWFFPUk1QV1ZXX37j29vifOzh3NvY0tfY2dHNy8rO0tjq/fx4VUtH
+Q0A/Pz8+Pz9DTFdUWmpj6s/Mx8bAw8LAxMHG0+tx7/pfX1tUT1VZT05QUk9Xa/Ph3Orf5OHp
+8uN+XVxgbfL86efv3dfOzcvM1uLqaVxYT05LUFxi++rj4uDs3NnSz9POycbHxM3l49bf9l9V
+U1NJRUQ/PT0/Pz4/SlJd9ubUz9PO3dnX3dXU19zY3ujd6Ofb2dfRy8rHxsbLyc3ebFlOS0pF
+R0hGRERIS0tUXvH169XSzMrKzc/N0Nvc725hV05JSUlIR0VHRkxPU2F34t/Qy8jIxsPAx83O
+7l1dUk9WXWd67d3c3crHx8zN1Pd0X1hWTE1PTk5KSU1eXWnc3tfP1MjIysjR1uh3cP7q6+rr
++nNiXVlYUUtNSEdJSlJeaG18d+vt9Ojv9+/o29bS1c7V7Pvo4t7SzM7Pz8/V2+lvXVhcVkxI
+SlJQTUxMS1NfX3T45ejh4HJvfeLj4dXQ09zf5+vl4eTn2dvd4O3W1tznZF9SUllUT05PTk1K
+SUlNVGLrzsXDwL69wcjL093obE9FSElMTVJYbHH0fX3X2tjPzMvT1NXg4uHudFtPTU9OUFxT
+T1dOS1RhbO7OyMHDwsDCys3bZ1dKRkJCQD9ITlZYYWJjbmRaZF9lcefQzMrEw7++x87l6ets
+YmFdU1FWWFlacufw59ja4+frZ2RlUE1NSEhLS1VYaeLe29XP2NfW4v3zffrl4+h/6ud0a15o
+YVtcavfZ1tjNzdHY19fh3n5kVkxJQUJJSUxZZ/Pazb+8urm5vL/CzOP4bVVMSUdCQE1LS09K
+TFZaaW1q9+7h2Nzc7GVral9kWVBOTVJVcPXj09DMysfIyc3X2uT/amxjZWVUXWBcX1xbV+rc
+3Njb5+t5dWxga2thX2RtVVtbW253Xl9jc+7n2dnd3NPY5+fvc+79bVxNTVhaa3X28v14ZWRy
+e3Tf0M7P3t7W1tLWz9LZ3H1hWk1NSkpIR1Ns3cvBwcLBvsDNzs3U3HdaSkE/Pz5AUFdnbnN1
+buzr2trs8W365O/67nBlX19aT1FdeG3449/i3M/Nz8zKzc7hfmZcal1RT01HTVpeZ+Xg08nJ
+ys7g535WUFNTT0xTX3Ty5Whrb112YXvuZXFvbmht7u3d19Xh7m1aUU5beGz3d2Zxe/fl2tnS
+2Nrg7eDX3+79dlVOX19oed/Y+lxaVVth7eHu2NbQzczJxMfJzM7P1et9X1hOR0JEQ0dMVW7i
+2Xbo2u94/ntwWltSU1NWVVtcXHPu4uzn18/P0dzf4/Dc6fXieHxyaHNt+uLc0tzy7f5s6tvd
+1Nbc7e99T1BUVVRWV05TVWBgZ/ns2+Xj/Wjub2haT1dPS1FSWWvq3NbX0tbVycbN09foZGX1
+enf6eX18/WJj8+fo7O/rbGlqX1lec/b6dm5z59nKzc7V6fHf2eHf1tr3aFpURUhfXVxbWVpV
+VltkeXZv5eTvfmp78uT9Z1pYV1ll+d3c497W2uLb1M7T2ezq615gXFlqanTs3tvq5NPU2955
+7G5edGBvXk5NSUxSUl737OPd2+V+7Ojz59zu9H1hTUhbWFhuXlxw7+PT1tPP0tfZ3fz93+H9
+eWN3YGj7d3r78+/n7Hlc+t7k5F9RVk9RT1hddezf1dHZ3MzEv7/Dw8ja5H9fW1VPTk5SRkpg
+X1hZX15mYmRlfPhyXlpVTkhIV1tp9PDn+erW0dPWz9LX1drn2NXV3O7o5/j57tTR1OX/7P1a
+UFxtfV1YV0pHSk5TXWj//u39a/3l19bk2+x45v16bmxeXl5WV1pf6dvf5t/b3NXP3NPQ5eH6
+YFFPUFr6c3z8d/Xt3drOy3flvE9HW0ZWPllJSl5b5tHCvr7Mv8rDzNTV7epZT0pUQ09PXX9l
+2VZQVEtFVdxW8dDQZT7aTWxt/klKTlH81sxq2NbN0cS/x8HIxezy31VXVlpVS1deX+vf3t7P
+1djY1mhNTVFAPUJGT1XZ09jJzd3d2eFWXlRhSFFKV0xb197JxsHQzuXdYuj57FFVS/bnz9LR
+w9TUYtzg+Vf3WU9YRU1Rfs91yszp5ExUSkxeZE9e2/1u0by7tbi6uuPXWU5GQD06PkBFRujL
+xcO+u8nX2VROTUg/PTw5OEJw7cW7urzJyU9EQz08OExFRmXu4rmtubaru8W+3NM/S1IvRkI9
+TnC+xc+8zk/1Sk9FVGpDW91d8dDMytfRTkFBPDYyNzo2Zbe4xbeqzb6w43RXXk40T/o4U9Hd
+ycK4vsGvvNXNY0U4NzcvMj03PUtIU3npzr+foVCfrCuuSHvPM69APLxRUchsZXdqtjg++y09
+PEjf/LuvvrK16UY0LSMcHx4eLjhZuaSYnqCUm7ivskItODQoLklwfMWorsG2vVk/QjsuLDcy
+Ljo8ODtETkc+4Fe9lLIlnS0cryjKzkSet7ecs76q5dtOJEonID8tPVpctry+rsanqE/ZKR0c
+GyAnNb21rJ6lo6WfpzVctiAdbPdAyp+it6OrVTknLCAYKC42yK6hn6ekr0g6KyQjJTI9UsrH
+nZQ9O5spEC1OIjqpq6WgoJu1PuMjGyAgLy5Iq6yqo6qpp7bFQx8eGhwoN8CloKGkpJqtFSvt
+EBfWtrOelpifrdJcHhIbGh8zS66usKGmwMhMNyggNTc2uKqnpbdftp8aCkInD0een5uXmZul
+VH5HGyAtMXVKraDkWttLOC2vmau2rjsiGBgpK1CppKCts5q3EB5vHBzCm5ubnp+hLhcmGxYf
+Pqi7vJui8i8pNCQZPLavpaWeoNk2RLLPDhHYTR9KlpGvt6Gz7Swxp7Mva7O4SyQxPh8r3K+j
+sbehyh4fJj1PXqSrUpubDw4sFxl0pJONm7af3xYdIUi6ZZ6dvlAlJCgdNLiml5qmtiEODxQo
+qpaNjpq1vbgNAxUvPa6WjZCsJBwqGRfKmY+avsQ1HBQYMb6zpJ2erDIv4l9GNLKMkx4eeDAR
+ElSmoLJWnZ4eFSA0XCzNnKdhKytZMCnPqai2MkDoOk1PO+SRmRMfQh/AXbuLjrFOTTYyFR+i
+qaa02Kc/GB0iO9iykZOqJhASFRY2p5WWsEk/vjcNGK+enZ+cl6UiDRlNPueql4+5GxknWjYw
+qZeYsywxMyYoLr2urpOgGRgeIC5FrpuQnSs0QSktJmybm565MEUtHjFlqapBQ0lN3jcs3JGc
+EBUZHqYxP5KOlrojJGMfGS1LmZvBwkk3KRIcwp+OkKCqMRgXFSiunpmntpmxEAkKG7+km5eO
+kLQmGhwoMPqtn5yrTi8jHx8gObynoau7PCftozsTFS2xn6G5p5mzKh8cPrPOvq+jmqszJiI/
+tbisqauxOSUjHRonnqBIPg8auUvCvKyYlKtvKx1TKyI1WJ+Rm6LTJDvJq5+rvMNKQTQoMjk+
+OSgzu8QtHBgfSq+tr6uopqvkLiQoODw/S8yqoKK0RztJ4sDUQjHAkpZ2IA0PQrq+tLasnJzB
+Ox4fTjs6OS6xmpmcQR0uOMC0/r7F5dUwISisorHjDw8iOaqzxbWtqp+2KSkeMcpDRVNrp6Kj
+o7G7xkA5NzvSt6+zemK7ud03HxslStF2WEt3ubi8Z0Pvyr2+XXDmyLr6R0FA2Ly1wnKpoL1g
+GA8eJsWzOz7gu6Gfu8JFRrrT/0EpO9Ht2TM3zb2mo6mqucrA0cpoLzBBPzIsGBYkOLOosa61
+saWvxlgxPUVBTzo4TVDLvsK+xryvs85NtKS1viIUHy68qL3W7e24qspSNCE3S1XKNzE+OcOz
+rqOopqi9y2E2LzqzsLdDEhYaKrS/11VOwp6gpbQvPj5Pw+FOfD9XYk+6rqOco7NPN0fpx9A7
+KCg4REIvHBkeK8+1v8jYuKelqL5AQkxP4VNW9vrCxNfW38y8zMael6CpHw0UGT2rubfLVK2h
+qq46HiYqTNEtNkpirb9zwcKhnKKlvldJLiYtyWJrKA4SEiSyqp6kq6Wfo6PKJiMdJDU0Wsm3
+oZ+en6amrcHTSjw4LfuoscopEhEYJ1q/zFfcwKuirtQtKUzu10gu1KqclJmjuT47MyxMqKa1
+MQkGChS1nJSVnp6YmqDBFw4OEig5RdjEoJKQkasqIymzmpqf9iQfHykqIjn52LQxGhoaOKid
+mp+vtK2srVQlHhwlPtOwq6imqrLMR0A/OyktusW+2x0cJ1efmJusUDI25Uc0IRciMr2hp6io
+qqCm7jElPKaZlZzNIhUhv7arHAAAAhOqj4iKkpKPlZXOFAwIESxprayvnZyanzsjKU6el5qu
+KR4fLOVBKSMjLEI9KBwfMLCXkpej50xd/NIyHxsZK76kmp2kqbq+0DssHyG1nJyeJg0PGcaV
+kpquQEqxsrouDw8WKqWen6a1p5+kqywZHS+pnKHCHxQpqqCbIwQEBR6bi4WMnaassKcpEgwJ
+Gc2bj5ejp7u5zj48OWisp6StcD85OjwvOD00OSsfHyVIs6Cdpa/K7spjRjIqLjA9e23Dsq2j
+pa68RzowJzGvoKiuHgsOFE2XkZKcwL2xtLI8FxMTH7isoafLu7iyq1AsLTuklZScTRkPFO+p
+ncEKBQYWn4qDh5jITz++XRoRChE9oZCRob4tKDlPubi4t8a+vMjURzU12ainrWIcFRcg16aj
+qcw/Qtm9uE0qIyjyqaaq4DM6zaOaoL4mFxYkpZSRlCEKCQxKmY2Kmr9LRrqn0CoWDRsur5qf
+r2syXsnEyy4sOcKclZajKBAMDBY1raWqayQtWKeVlZuvMysuRL68UUEvNM28rKq5vcq+rK25
+USkfHyMtx6WoqU0fHR0xsZ+dpsM6LzZM0zYoHRciNq2al5eiyj4wPtC4qrPITS4mLa2fmp4b
+DQQFEzqPhYOIl8FLPk5cHxcNDBY1n5GPlao0IB0wup6YnatPJyQpRsXoRDMsLi8vLjA1QLym
+nZqis0sqJCEpOUFFODzQqpqUk5m6KBgVGR9HpaGevh4cFyawnZKVpdExKDf41b86IBoUIUap
+mJaYpMtNO+G2s7DdPikeHjW1qJ1IGQsDCxK3jYaDipm7Lyk4N0YnGhMQHEOfj42PnN0+OMqo
+pKbsHxIOFCqrl5KYxh4PDRYtsZubo8Y0NVOyoZ+nXhwTEh6/l4uKjpvoJh0fO7+utyQQCgkU
+NpmKiIqYZR8XGyg7cTwhGRQdPKSRjY6XuC8fIjW+oqCpzSkcFxkrTLWt8EAfHCU7npGNjpiv
+MhwaHig/T0svIicxtJqRjpKewioeGx4mLzo2Ni4tMjvHr6qmrL5YMCwvNEL53c1zPjs77Lys
+pKSlrrm6vLW5xk8qJSIkNThtPSksJkfDoJeVl56vRCofISEsMTU4LzhJ0aqemZeco7rSV0dO
+QC8hGBEQFSPcpJiXnrYtHhwkWKibmJ2teywmKD6/rKizVismLF+nmpebrzQeGx8z07mzSS8j
+Hyw7sp6bmaC6Nh8aGhwnM0luWnpdxa+kmpaTlZym0y0dFRMWHixZ9t47JCIfMdGilZCRl6PJ
+MiQkJzhIYm88MykmL1ynl5COkZu1KRYODxUeO8u8wTMoHyA5u5ySj5CasDofHB4nOU/J0Tkp
+HiNDp5GMi4+fRRwVFRkiLkM2KiIcHyhqqJmSkZegvy0fGx8pOWHLxd1aS0/OsaOZk5GXpXAj
+FxAQFR0sUmvXUzMuKTm/qJyYmJ2ySSojKDBWvba9Ry4lIze4m5KTmq48Ix0fKU/IvcwyJxsa
+JDqunZiXna1WKCEiKDtQxcpPPzA4RsWklo6Nkp9NHBMRFyI7X9swJhwbJ1Gfko2Nj57BJRoX
+GB4pPj8yKicsSbqgmJCPk5ijwDEdFhIRFBopRb+0tbfOw7+0pqKiprdXLygrNXq/trTROywr
+QMClnJyjyCwdHSpLtK65RhoUExzSn5KQl6dEJSElL0piVykbGBszsZiOjY+Xo66+wcpTPS4e
+GxURFREbIz6snpSQkZWcrsUyJh4ZGhgZHyvlqp6WlZWWmJeco7gwGxIOEBUiTcmuscvmTNa1
+rKmy/DIqKzrgw7nORTYtN+60pqOptOhOTUQ9Wri9tE4aFg8XP6+Uj5ebq7OuuclLGxAODRos
+xqajnZuZkpOXnsFALCcqIixRP84jExEMGS+xm5qdnKGem6u8LhkWFRooKz3auZ+alpKVmJys
+wToiHRkZGyN8vba4PDpFP8LDXWxCV7u1srpeYH5a3DUkJS62nJeXnqquu07srUUiFQMIDxiu
+pZiNkI2Nm7AyFBUTDRURGeGqlo6Sj5ShpOQvOC0yQC4rJj+frLglDRUZKDg3856SkI+ntc83
+LhwPExsk5syunZmVl6Cipr9LJhseHx40trCrrejOvkIvKiErNTBC2b+op7GuttlvLiUzPMis
+raOfqavBQaiZNBkVCB0iETW2mo2Vn5ibulwXESQYFh0ks5memZOXlqg57c1NXDRLuj8iN6Pn
+HBsMISMRJsmclZiej5Cs1CIfJBINGB1Br7aYj5WYnKijwSQlJBgZIPVcNqyurKD4ZKNxLjgs
+PkYoO7S/wrzPrb4wQVFqvsvCp63Ry0suXKI9DywbEyYV75qmoJWWnaI+StAXGB8eKjVDoJef
+l5qhqMs+zj4rT0lGVS4mvp0gFskYGx8awa7ao4+bnJy3plIcLScUGyIts8enkpmhmqOvvisu
+LhQouxIcqSxGo76koFu4rjMrPS0uPStdxDnFrsW5vsGpuMypsN7A4jYuJ76tECO/ESU0Zqau
+qJmVsqCjPkolKCodHklHOqqrpqWqp6pQ4uQrMTAsLiolLq+sFrmhF9e3Pa/RzaO526auNL3d
+OUYqPUMxSbHfsqSxqaq9t8YwQS8bKa8wErmwGuGhT66mvaS7SK3BLWnWLkFcSvRF2LtUwqq5
+uKvK4FsxNCgeIUKvFRqfFyilV6SuoJ2mtKieKsvWKjwoQDs/TbjHT6a3tq2ts7rC7HwsNi8h
+Jy0pJjSzqx+8mh7IpkmuzresX/6z7jfJRzY+Mkg7PL/XRri5c7vCy8xn9GI5N0I3/LA7OqdG
+Ma9Ld7lXvb5Oy7td0sJk0U4+2kM/zFJRydXbynR9+FPbY0xWRD8/RUZa2cPP07rg1MzWvdXO
+vs3PvL/Dw9dtRTc1My45PDpSS1tc6cbMzMHLaeF+Pm9ISFpVY2XxYcHYw8Pst+PsxObNzszM
+xdb14GzM1F7jWVZMOUU/QEM7bDZZO0ZaSM5Ryl2/0ujMUbxnv97Ov0y11bvA671KzkRH2kbH
+VW79TPpK8ltN0k3tZUJMNlM8TFBc3UDKR9reXsdgwdzNzru4trq3usTFyr7Fyct220lJQTPW
+Ml1BOVQ0TjtfSV1ST35LTF481VJPxEDUTM9OzM5JxFa8ec3K2LjBu7y7yb3Jz8jYzOVqUUtA
+P0E7QUw+TUFN80TsbEzBSOlaWfRm1j7CR+fOXb1kwefBzXjGOL1NTc9E61jRV87Kdr1bz9VU
+31VLZk1LYklQYu9YvFXbzEfHRldr40TabEDBTtDF5b7HxL7B0cHSwufFWuzOOr49bz9IQ0m5
+LsNWPtI27DpmRk/ONbM+5LtJvlJ1TWFS9t9H0Gj30urh2s/PzNbLzfzo1fNjWu3lXdBj1t1m
+3d70Xdf7SOptQHN1a/Fz32bh6/loUVx7Rm1IT05K3lpcbOlW0F7LvULMd13DR+xJfbk1vEfn
+vma5QbVzV8I742c9WVZl/M/YyLXRucBxzldRVUA7T0pPZOVt5tPbZcrYyc5OxlZoWuJpUs9k
+9Gjazmpy8WFYS+FHTVk/xGYu52E8u95z089OQ8VEVFZZble7Wsu9z7TLt9e+4UXaPE1BWk5n
+10vKWcbQ4MdW5EdQTkP2OU/eS7pMz+hpx2HHRcNCPts7ak3kzM/NwM/WucnoxetFV1pDRkJC
+TkhLfUn111XXyMnazWXczmrQ0c3O0dfm39hr4k3cWUfWWOpsztjixMrp09vQ4+vfSUo+SE87
+V0pb/WPPa9jJ0GrXwz9TTkVcP1lg3+5c0u3o39hw3vZPc+5q0d5dvt7aylDQyU7S2mlQ4FBO
+y05PeXNtfcfc2ctn3FhsV0tlP9lkP2VXWGHnS83NS9NhXWZV4m5o/erm2tvRdtLZz9D90FpW
++OLcUuDNYO7ryeXQaGfPSkpMTkZQ8GTxXWvcWsXd6r/o3cveVOvbas3UZMjZT9Ns8uVZW93R
+TlnmSvJQT+9HVUxTXvbUX1pxRk5pXun34+7PyF7XwNzOYeBpSltD6Xv7z2bJzW/Q99/f0N7s
+01f620b1XWppYdlNXWhLTufURljOallj1WJM0+v9WWphT3v73OJp9tLlbs/jWszd7Nbm38/r
+zsxNUU9qQ1Db6tvse0zoX1fQXNPdXMzXbWTIXl/oR11zemPeycvsyMnq1Mviychuz25SWWpF
+S1FFTUlUR2vqSM1tbv5g+Fpuee1f7s/ebNHN7/3g2VVMW11HUGlZavLy7MvEyL7Gx8fZ4mJc
+TEhJPFFFRk1uXFDnbM3J0cjQxN7u1llUXWpKXVP9Wk7fbul3bW9uWG7ca+DQ09DMxszN4en0
+WlRKTE9NUVJod2776tLi2NLT1X7fX15KQldJTGFtzM7UycTQ59Lj5Gzf1uPOysfN0NTX11pT
+WEdETEc/QkVITk5b7tjTzdLMzNls/WpGRkZESE5ZWH3z8s7Z08fIycPP583Z09Lfz9bfX3vv
+XmJSVXBVTUZPc3F23dTIzdnT6n9eWklEQj4/R01KTntm387Yzs3MzcnF0ePe6/brbHJcXGfx
+5G7k9up26+9ld3Dd/uXf0srLz8rL325eSkhBPz5DVVVqZtfIxr7GwL7BzNriYVVAQkQ7S1tr
+3+fb3ub69+VkXFlQWlpcWmrm6vNy+mxhfephV2RiZ/be3drOyMPHys/P0tXmYlRHRUVFSEhK
+UFxl2tHS1tDT5mlZX1lu++jm6uH2ZXra4WpqWE1PS0tPW/XmzsjIy8nExdLe3F9VTE1JRUlJ
+Tl3l5NnNx8LEw9HkeWFrXkxMTllha/xyfO9sXGVWUF/609TXy8i/u7y/wsbQ7FtWRz5DQz09
+PD9OdefSyMXFx9ngdFlWT0tFQENNVGL97ubc19vf6npjae725evezszJx8TCxMrU8F5PSk9M
+SElPXFzo287JzNPd311PSkdEQUZDSFNZa+vWz9PP0tvm6+fo+GVYUGR6bN3Ny83i+mBYVmNx
+av7tavjXzcrGwcLHz9xeUkxMS0tNSUlKSFJd8c/JyMHDycnN2WlfVU1YbXr52dfX6P3haGRf
+X2dfWlVeZffkft/i7fBcT0hESEdAQkdLTWbezci/u72/w8nf2+tlZ1lbWV5q59/b2NHP0c/r
+/n1xYFZWTVJOWF9n7drW4+xvUk1RTktKTE9YZ+/a087LzdDS52lfXltXUU9OTmHw5tPKxcbI
+ydnxfX37W1xVUE1dcGzc1NLMzdZ9aWVPUlVXV01YX/ncz8vJx8nK2N3f7PNlXVRPTVrz4s/U
+zN/n+1hcV1JOTEpEQUtTXW/26e3u+G1w/vB4W1daUlvv3NHLx8bHxs7Z4uTh4u/ycF9ra9/e
+49nX2fb072FSUk1NSEZHUl97+eXW1s7O2flcXVlNT0xKR0dVd/fd2NXQz9TT1ePb4el0d3n5
+2djSz87Z4Ojv+mJZXFNXbmXo2djV2c7RcXxjTk1JSk5YWVpl9NXOzMvExMPJ0tvsWU5XTUhE
+R09Vfv/q1eDfd2xhW15UX15STlZlbfX37uPc3vb0bF9lX2ZqaPDX18vHxcTEx8zU2N1+XEpI
+RUpNU1ta8N3a3Njne+n7WVdMR0dASU1WYe/OztDX5Gxwb1pYWmpncdvPzczHw8jO2fZ4ZVFM
+S0RCRk1f3tbLwsPExczT3N75YVBIRUpYUl9ub2ry4OvV0M/L0tjq8N3Y1M/Ozc/Y3l1geWFT
+TkdDPj9CTGj+187Ky9bt+V1XWE1MRkNHSlh33NHLyMXHysnH0dbeXVBPVVhcderc29zV3Wlo
+bWZiV1RVc9ze2c7KytDZ6WtXTE5KRURAPj5GVFfq0s7LzcrNztLg+GxjT09PXH772t/Tz9nY
+4d7p3tt8bWRq6t3l7dvW3+j18extXFlWSEBARFRo39jKwr/Av8XJyNntZFlOSUpSVFBe3+vf
+2uTm3vRmfu9mT0xZeW9oZ/t7fWhbUU9NRUZLS1Bk48vFwb6/wMXI0c7a8HleXU9NUmV/b3rv
+3trX3+Ts+GVaX1hNUF5j+/l1bWBZZVhRVE9PUllZZG/dysjHy9PS1uB1fW5cUkxIREZV9/3j
+09TKx8bIzc/e9WJVTUpOWGzv/WRmd93VbXNlYn9ia3bq2MzGxsPKycjN0t3lWEQ9Pj4+SFRr
++dnd28vL0NPX71tMTE5KSE5QTlRYW15+ffbY0MzO2NTX6+7bzcnNztTQ1u1x62dOTEVLTUxX
+6NHS0tDKzNfe3OFrV0lGRD49QktPWnH818nLxcPE1e90VVJYW2f0+9/o3Njo3OHg6nZSUFld
+ft/Z2Nni5s/O2NXe3G1OSz8/RU1Ubebi2s3Lzs3LzdD9aGJPVWFv6N3h3NzW0t3d4u/nWE5M
+S09ZX13v6ejm0s/r5WlNQj08PUFFT1h62dPMx7+/wsHL1X1cWlJWZfjs3d3SzszPy8vM0OJc
+SUtGREdLVFZ27uPX2NrV33RSPz4/REpTdOXezcjHwcPJyc/rWk1JS0xNU15u6tnQ0cjFycbU
+7lFBQ0VITV9r5uX36+Pc2NDb4vVdWlpZUndx8NbN0czGw8K+xdJ4UlFVWF5ycOjl39vZ0M7N
+3O9VQzw9P0BGR0tOVl5e5tze2el7XktKTl1aaG7m0drDxca+xMnV81JNTVxw9t/Wy8jExsfH
+y9xvX1NIPkA+P0RIT1Vk6dvWzM7f7mlOPj1IU1rt39/W0dLRys3T131zVUhOXF5+6Nzk18vS
+1MrO2Nb6Zk9ITlBXYPPp383KzMjJ1tb+UUs9PElSTFNp9tbMzcm/w8PGz9hdRktOTUv+9HzT
+1NPM0dtlVE9LSkRQW1xj49rh2dTa62xkXFlWUkhBTFNbZ33OxsXCwb/Cx8/h9lhER0pKVGLf
+2uTf1dXP0+f1al5MSVBVV1pjb/vv393g3OpfWE5MSUVJT1/v3NDPzcbLycvN3GhYT0dDSU1Z
+a9/YefrWzsvHyNXjalJLRktTXnTj0dPP3ebV6u5qZF1dWVNu5d7RxsLGy87Rzs/edlBKS0E+
+R05UVvff39POzs3T3mJVTUQ7OkFJTFZ19N/OzMrGzMzQ2tj+dflr7tng1dre1NLQ0NLka1pR
+SURDTFpc/dze3tnd2t7laVhbT0hJTVdrcOzW29bP0MvO0uNcT0VCP0BMUWPmzc3OzMnI0u14
+cllYS0hNWnfpzMjIxMTDwcjL1N7+TkxGRUdJTFpeZXTs2dnPztzk5l5ZW3bc3dPN3+t0Z+xr
+aFxQUEhIR0lZau3S09fP18/W4vpoT0pFQEJKVl/j0MvPycPBw8nO425aU1FNVl5093zl393c
+1c/Q2vRvVlRMSltpal93eltj9Ox673xZT1ROUF5+287K1NPY3NTa3ntcUkxMTUpOYfLa0tTT
+zM3N09zk/lFIRUpMTG7Z3d3e3tHL0tfU4mFbVlFVXfPg1c/NyMbIxMDFxs7lYk5KSEVOWm/o
+bWJzcmX7aVtOREM/Pj9GT1JUbXPdz87N0MzQ7V1iZmJm/+bfzsnNycTK1MjLb0pJTUtMVv3f
+0NTW0cnFzc/R5FRMSEVCP0hLT05RXevW4e7Z0+9oXFBRU1rj3tbe4tnX2+rb2fJlT05SUWv4
+597i6eLb18/Sz9bfaFZISlpd0s7d1M/X1tBhzczffFFkQDpWcOjDxNPJx9btycrPXU5KPj9A
+Q01Za19ObdnP2dXN3O3vRjhDYmz10N7SzdtPVmb05PlyV1FUXtXLzMnU1cbFztPP2vpeWk5F
+X1dS/V15ZNTZ89z26eV7XEtJTUpIWV76b97b39Xj9W9QTE1ESlVZWeHk8drLx8nHycXfcWFR
+YV375d7gZuzay9Dy4PXd6tria1xVXnbp+l9JTFBVbmzf1dtsVGJ+3MK7wdfTzdrTz9n5WUZU
+XU5MUVlZT1dSVGdv3fj94mRsXE9XT09a5vVnaX3Z3d/d1dfu2tdvV/PyfNXe2tbPz9nM1tzb
+aWFcWllo4uR8dlpfUVd0XV93WUdFTVRFVO9VTt3k2s/R2Nvm23Ro3WVlYOxsb2fczm7g0tra
+ye5nfX1PSXjsatHN1trX0NprfnxGUWBSTVFjbFH7X+vS5N7vztDb29HJeuXK0dnUb+1eZ1FL
+XX1oRFtSREhk2tnTwtPXyuVuctt7R0ZPQkFRTFdiUXDl28jByMa9xc7Z5V9QdPVMV/5XP27c
+SUD9yklbv9JqybzBurW9yL3JVklCPjU5OzY1PD07R1hNPV73TFrPyH7Qv9zVwcPIt7O0tbO6
+xcbSZEVVQTEyODIvPE5ESNR9bMrCxL62uMnLwu7b3m/Q3ejZ39vrR1RmP0tLRlJLSGlv3spk
+7cpe2dRY5dtNU2pjfc7MzcPD39ra5F1GQDo5Ny8wOi88Vu3Nus7Hr8G0r7CvrrKzsr+9x8jW
+Wz42LCcpKiUpLiwwRXDfuaumpKCipqmuumxFNSglIx4hICEsPTtasr6xn6emnqesq7zRys9B
+Ndo2Ikg5J01PLEXVOETTVVzCvbuxsL++t95s105p3WrbyMrGvbu9y8tbPDs6Li00JyIqJic5
+N+2xrqejoJ2en6Gprr9IQDgiKSMcKyclOD/tt66opKOkrrK2Pz43JisoHSIjIy02Rc2vpqin
+nJ+mn5+ns6/KLVY0HTEnGygmICswNFPT7si6t6+rqKmnqLOwrMnFnMspoh0fshQmyB8nrCUu
+sihJxUdavX1M3+NPxrbEt6vEuKu+uau3uLPjPkQ0Ky4rKjEvND5HX/e9rKytq6usrq6str26
+z0xdTSpAPio9LyYtKykuLi0zNzM6P1DYvq+pp6aloJe+rpkeq7MXsUscW8se1sQfwk897e5I
+Ue87N2VAR8fCvK6wsK2tu7rLSV45LjQoKCsrOUtL2Lizta6vt7O0uL7Kzczzy8pO3VM/60lH
+TkVANi8qJyknLjY5TVD+x7i2qpzbnpcnm7MeoVwqq88lrGkyrFvjvWM7SDwtPU4+5f1cxMbJ
+ucS9wdbvSUE6Mi8yNTA9UklbxOhfu7/Eu7nEyrXGwazJzLjp0MVa3OJJQT00Li8tLjMvMjg3
+PHeuOTuWKjeVI0iZJ06YKVaaKK6ePairUd/YNzFUMDfQOEhvU93dyWjXzj1qSDE+OT9EPsRV
+Y7PQy7jDzrzAP83dO9ntW87i7dXBx8q6ysvVRUg8LjQyLzYzMDk7vsI8sbDSura0y7ux1sCu
+ylCktjOevT2fNDBwIygvLy9BUDJ1ZzvbQkhxPu6+MT6iHHelGp6sNpmqv56tv67PWEQ8LCw9
+KC05LEZS2MC7ra+urLu60kdUNS4sIyUlIyYtOTJDyV7cwbqur6SnraGksqSiuqioQsVhKzs3
+MT4/MTM+MTc/Lz5LQ7PPLJ44IpogcJwpqKE/rK5Dvls5OTkqJjolLzooSknerrGgoaKfqay3
+0cdTQC4oKSUlJi1DMEu+X8XBvLW6qK65qq6/q6pqrbU4wz8tNC0sNTYqLi8qMzEvPz1sr9k4
+mUQpkyS+liaonjqpqUqvxD1POSssPSw1SStCQTy9zLCorKarsbvZ30hFOi8vLC48KzjaNUnO
+WufCvN61qMqzq9Gxrs61us/jbkMwOi0uSi8xOis2MS43Pq42OpkqSZouvZjPV5fQMJQ0QJoi
+zrooRzUtNHRPO7pJ/bJSt7fPtOhVTi0tKT0xIsgqJa00Rqe9xaanWqKqLqPEO6k5O+M8XmLN
+zsrBUMzuPl4/Q1IwNC8nKjs0Ir9SJ7HJVLissribunOZc1+bT92kL0zRLDg+QkrB0FK6XkvM
+OEhOLTkxWDAcqR0omxmvnCyanW+co0KpwjLXPCxGRCdGPCzLXu2rt6ysuLTaaUI5QCstKCEt
+MSk2aT/dwry3uq20tqm32a+8Pqy2PalYN7o7SdVcyru93NFxOz8xLjUoLkU4H7L5Fpo4LY8z
+qZNUoJ5btMAvNTMlIDAjJUEiOes4srOynqmkp7Sy1lNJNjMqKSooLTlXS+2zvryutLO3rrDI
+uL5Myr5JyLw06VIz0UFG2dtt/Xg+P0UtNS8pMykq2Tsgn1ookTW2jk2alsKfoUy9zyg1LyQq
+MyAtLyJIPTy0wLKmrKyqtLzFeEY0NCosKyUsLjXp3v6yr7isq66vsrW+xb/oTshOPMgzO+ww
+bdpay+VmXko/OTo3NjUwNjM0PV+tMLWXHp6cH5WrK5nYKaE6JaooKrgkNewxPss9fLRruazK
+q6y+rLrKw0o/NCwsKCsmKDA7Qka2xsejsrCnrq+yrNHouTA63Cg7TyhTTTvT28e+vb/N0F1L
+Rzg9ODE9MjM/zE0wmjgykyW1liimnjmrqDTbtidL2yhC/yU/TSf1STy3ycKvvrq8ytpnSjw2
+Ny8zLzDQRDqkSk2eVbSdt7ShuWCpTCq6JyhIHykwJzZFSOC4vLeturWzzdB5OTMxKSowKS9s
+yUO2pOyspbGprquyzKi/NbXIKrq9HsHkHLlDH801KtJnPLy9VbnISchMPF5COUVLvLMunLMk
+ki/Jly6mrvHj30kkXSYgMCUjOCosSjhX3cS2rqilpKOoq6+751owKycfICQiKDc8zLSrpZub
+op2er6+6TTwwKysoKzQrQ+Q8vbrzt8ZteEg1Ly4pKS0tMTtGWs++tK2pqqykm73Yki4onhco
+vh4wVVYsva4wtbRvxK/YSfVVKzpcKkm+TsOqt7Krt8rIVDozMSkmLissXU/YpqOpnpusqKPX
+4r84LTMoISgqJy89PErU1OK+vtjFyl/i201k4lpdUFBPXN/x08XXdradxCqZrBi7qhstoDQi
+nq8nqadcxau3Nb+6IS5AHiEwKytR007Gs8vBuOpd50o9Sj5EW72fdl2SOciZNrDFy/c52DEx
+LDUoIkAsNVv47de/r7Gtqayvr7nVb0w/ODU3Lyw2MTNLRFzCvrSurKiiqLqsvDBTaC44bz9C
+Z3N4RuzkQVRFMzI3NjA0PkRGaM3Oybu3t7q+w11QzkxBzWCqmz3VlykpsSc1P0jNP+m78UvB
+xTtaYF1EOF4+Me3T9sSwtsnN3j4vNT81N1RUS3rNwsGyo5ytyJ9KK8hN3Unbt1xJUUIpJi4m
+IScvNj6/r7SqpKu0u8PbSFNuQj5k7HNPaNBPU+xpR0zpbGi0n7s7o6gsNrTASr+vv8jXyc4x
+M0wvKzMuLC8/Rj9LT0xLVWvb0cu5s77LwL3DwsO7vs60lpkhzrAUHxws0Sq5pK/P0sM/Lig4
+LyE0TOzHvK6puLm2v8/Rw9JrWd/4S1BhUEc9QUE0O0db0cqzuayeprnDuEw9ODhOMTpGNS8q
+KC8tLz9OdM++tba3raqsrrGtr7u5v95GOjUwLC0uLTY8RVFp18e6uKCb1T2qWCIo97jAvLKj
+rknGwzErJisuHSY3NDc0eslluaehp62nruNLQEJCO/XC0NPBtrrd5+ZMq6YqK0McIBslstqz
+qKKmyVE9OyMfKCo0Ls+qrKumnqKtraqsut7HVzEvKiwoJzU8RERMz95lzs/R0dG+ur62raq5
+S3I/KCwmN1AxUuPc6E9XSkY+P2vy3r+xrKyppqmtr7W5y15LRTs0MTExLzAxOj5HTVj07NDL
+xb26rbJhRjk3Mi5byL+7u7S1yllRUD4xLS81NTtZyL25rq2srrazu8XhYnxNUVFJXFdaal1r
+Rzw3NzgyOk1W42JK3OpqafrJz8zP3s3e7mZzztXT1enp+1xRTlxzbWXQvru5trO0trq+zeNT
+Pzw0Ly8vLjQ5OkZW8/Lu7Hx03cS9v9X3clVeYeLGwsrNx8TPeVpNTEY/P1Bz5dXJvru8ube2
+t7u6vsHKz9bR11tBOjcvLisqLCssLS4zO0zw2ca4sK+1urzI2NjKvru3t7q4v91VTEpKQTs5
+PD4+Qk7UwL66s6+ytrq+wdVVR0FDPzs9PTo2MzQ0NDQ0MzM4PElw3L6yraqrr7S6vcHCwMPF
+v8HLyM3rW05GPjo4Nzo2MjM6SVvXvrOura2rrLG7x+VPQzo2NTAyMzIzNTY4Nzg7PEJDSl/T
+wLSrp6WmqKenq7O8yV8/NDAzNTc1NDk9Q0xVX/n9YlpUVVp70cbEv727v8DByNPzXUg+Pzw8
+QUA9PDk4PEhPTExb7//Tvrevrq+wsLG5vLzI0PR5WEQ9NzY8RUlOTlVOTFBFP0ZDQUZGTV7s
+3tnP0Mq/vby+wMXJyczP2u7sWUhIPTg5NTU3PEZPY9vEvLWwrq2urq6xub/F1+H1a2hcTkhK
+SkRAOzMvLCsrLTU+YePNvbu7vcfN1HlRTkg8PkNLaNfJwbeztbOztLa6vcvu+FtYSUdFQUZI
+REM/Pj49SE9a5c7Fwr28vLy/yNHjV0tHPjo7PEVER0lHUFpeTkxGPT9KUWvdyr+7t7W0trKy
+uL/M7/NgTU1FSU9PV2Ls3M/MztTpbWRdUk5OSUtERkU/REtTSz8/SFzuz8O7t7K2ubm5vMLK
+7lpIREVFSkhMVVpnZGFhV+LnTT87OzxEWXXlzcK+vbu5t7S1vcrcYVxWWE5LVllVUUtITE1Q
+TUZKT19kYHzd3N3da19UR0hLRT9BRlPsx7+9uLa1tri6vLzB2XNdWE9QTElISEdDPTo7Nzw/
+NzU3PUpY3c/HuLSzsq6usbO1vs3lWE5JRz87Ozw5OTw8Qk5lXObFvry7t7m7v8twWVY/PUI+
+Ozg/S2fPy764s7G0s7a9wMXjWE1EPDs8OTg1MzY3NTQ2PURAR1Nd7c/KwLu6tLSyr7CxsbK4
+u7/J32JtWktAQEE8PDs8QkpJTVphdOHM0OH/fU9IRTo4OTs5PURO9se9ubazsK+yub6+v8r0
+YGpTSk1OTE9XT0tJQ0NGRz48QEhJVF3+7nzYzs7S2MrIxMC/uLKzsrK0ucDL5lVCPDczMTE0
+MzM6Rk5h9ODVz9nb9l10amZfW2Hz19HLx76/x8PLz9X4Z2tpZlxj5e3g0szLzth9Y11PQTw6
+MzU3Oj5FT1hdZPTOxcO/vbm2srKzrq+ytbrE90s+Ni8vLSssLS0xOT9Z68/Lzr/ByszIzvHu
+3u3VycjExMXM09H4bGheYlZOWlp339DCvbm5vL7L3XdNQTg3NDAyNTo+PkBOat/UzcfHv728
+u7u1r6+ws7i8xv1NRj03MjEwMTMzNz1BP0VUVFZrYFNYXmDr2ce9ubW2uLS4wcXI0+5dUVRT
+TVVa28/QyMbL3el2WExBQkFBQ0hWaE9ISkZCRUg+P0FGWFl038i8vL26tba6ury7v8zZ1s7c
+5dXN2WJdSkA7Ni8tLCwuMDhAZca9u7avs7OyuLzG09/3+11s+d/da/zzWVhdWV7zd/3s3MrK
+xcbKxMlwZlFGPjUvLjA0NTpDTU1d0MzKvsDEx8jAu7a2tbGyt7i8wttLPDIuLCoqLC4yPUhf
+2MvAu7u3uLq9xsjO3fLm9HtbTE9aX2ZiUlNJRklYfd/JxsLAztDO5GZRSEhFOzxASW/k3en0
+6mpfYmNlXF976NTNwbi3ur3Gw8jR3/TeaVZRS0g/QEg/REtFS09QVVtnXWvd3trLw729vLzA
+v8HHyNTna/vZ3Nn0ft9OQz02NzUyMjU3QFfmz8W+ura6vcbOzv5QRz88PD9GTlVU/+vu2tDD
+vru3u769vsK7ury/w859alFEOzQtKioqLTU9SlJb4uTbztHNwMDAv8G/u7S1vcrT/U0+PTo3
+Oz09QEhiy76/v7+5u8PCzd3f9lJGRT9DTlRcUVllRUJCQkRKbOTcz8K7uLS3vb7Cxszd6FtP
+U0xYat7Szcvdak1HRz04NjM4PD0/SlhtzMrIzNXKycnW6m/dzdXNwb27uL3D3W15WUU8NjQ8
+Pz9NeunSycPDzMvFzd1kVFlndffs3fdacm5RQT4/QD4+QU770sW9ubm6uLfA2NzpW0Q8ODxE
+Umhpd/fczcrY5+LeYUhDQ0lnz8rPzszExM/d8ftgRzw9QEpx0cjHxsfBw8vdevN0UUtQUV3V
+y87cbF3r3+JaSUtLSENLX+zLxc/Pz/b+YUo+OTc5ODg9Su/Buby6uLq7u8LZ5mRdT0pNUdjJ
+1N3g2+rgc1dXTlNWUmB15czJzN98Z1xPTktCP0VERFN138rAwcvN2t/g4HpKSEpQUlNz3MbE
+x8zL3drR9FZFSU5SVmL1y729xcnQ4v1bXk0/P0ZPS05t38nIzc7O1s/X23pTWm5hYOvl0dHZ
++mVcT1n4bU9LT1pq8dnHv8HKz99fUk5LQDg2Oz9FTmbWx8PIyc3Z1dLY8ExLV1lYXe7PwcPL
+y9fd1NjdXkxSV1VPT1rv1dLa6WVZVlVIRElOWVdf6s/DvsLM2OV8ZW9XTE9eYFJYcuLP0tnm
+5H1RVl9STmTr7uPSycfEy8jH0eRZU01FP0FCR01XXnXMxMTDx87g1d7o79bM0MrW4c3LzdHl
+42FWTz8/SllPWFZOYezq+HBWW1hOT0hASEpLS0lZ287Jv8LDyNrqY05PXnP62c7Gv76/xs3S
+2mtSSERESk9SVn7r2cbEysvV3vZcSkA7PEE8QEhIVHTu3M/Ex8zYd11g+3b32s7KxMXQe3lv
+XVJKSURMXWDl187Hvr/O1NHd7WFRS0NNTURSYV7o6H3b5NrO0tfS3+zcbHnm4dfMwMDDy9Vs
+VEhEPz0/Q0xfbe3UztDn8dfvYl5dU1hcXPv2d+7s2Nzx9mRYXGNtaH3m5ubw6/LfycvK0ndo
+VklFQEBETVViz8nGvr/Ey9LdZFFHQ0lPXFxmfWnUyszIz97kYF9aT115Xk1NTlJqaWNj6dre
+7WlbUmhs6tnb28zJx8TR3ndbS0Y/RmN57N3Mxb/DytDtfV1NSUI+RWd54dDMwL7BxdTX1t3m
+Zk1AQ0ZFUU9OW+fe2c7P1dz9W01OWFhNUlNr1dXZ6VxXaF5lW1BZZW7l1dPCwcbL5mZjUVRW
+UWjw2c/N0cvBv7/G2OhpT01DPjo7PDs9P1LvzMnLx83S2ONYU2VTVlhWVl/m3Nvk19nra15e
+7+TayM7Tz8bHz9j5Y3NjUU1QT1J4+vPW2dXU6X1hZXBuRENGRVbc0MjExcDDx8vO1XhNP0A+
+PEdc/d3Jvb2/y9DP3l9KSkg9PUZNWE5RbWdOTVp67Pn52MzMysO/wczP1vNVTVFMTEdLW19f
+79HLysS9vcLJ2W1VRD5DQUFDTm7u3N/c3HNZT0tFQkdVZmPZx8XExMPGyNDjfFtLQkxUTVpq
+Yl9dZu/n2tXSz9Le6s7Rz8PH12ZabFhaS0tCPTo4NT5a5L29tLSztbq9xM3kSD07PkZU3t3Q
+1OHsdmHn6nRzSU1dZ/fM1Nd1bVNLSERIS0NAP0NacdXVzdrGv8PI1Nfc1fRpX3ly0MrHxsvO
+0+xSS0U/PDs8TXDIvr27vr3EzNpbRz44Njc2PkVTWHJo6NHNys/Oy87d9+/r4NfS0+/5WPz1
+3PZdWE9IRkVb4sfBwcvKzM3N9VVART5FTFDfzb6+ytvVX3hUSklLXU1YadbCvbu3u8XKytDh
+X0Y/OTg6RkxRVnbv3eff1vFxXWFrX1VUbdXbzsvW7mNkWk5LREBRUHHWzLy/uL7K1e5yWVdM
+R0hcXunT1M3Szc/16/NUSkNDRklRWF7c0s3Dx8vN1N9dVEQ+Qz8+T23l+3Da19bP2NXkaWRk
+2+7bxcbM19fN8FBcSUlBQD9NT2zPxb3CvsPA2c7YdmFHRTs7PU5j583KxsDCysfO4GVZU1h1
+98/O2tfazs3z7lZGQDw2Nzg8Q01v3dfNzM3NztfRzeJ75+Ps39XN2G1sUFVWTlNJTkxP8t3L
+vb26ury8ydjlW00/Ozo3NzxIUWLj2NPY1dfc2Nr2X3hwbuHY18/Z3XVTVEZAPjs7RE1S7cm9
+urm7uLrBzNXzWk5HTUpLS1RzbGJkcmVWW1VVYmTl1tXAvL27vMLK0d9xSz49Ojg6SFds79DB
+vLu6vsbO1OhfT0RFRkRHR01IR0dITEpMT1VaVubIw7+9uru9v8LMYk5MSD45PEVLS2fZw8XJ
+v769wsG/zN1bX+NnU2r6YkxEREQ+ODg7Ojk8TPHVxry2tLW0s7zM4/1QQjk2ODs+Q1Zs/PrU
+ys/NxsXI1M/Jw8rb2dfaalZiTz86PDo3OD5ZfdrCure0ure7wM3j4vtaTUtQSUBHYlxPXO59
+ZPbj1t//zsTI1NbJx8zo5OdPPDU0NDMxMzo/S/3Gvbu4tba7w8XI1GNSWWNbVE5RTE1JXFxT
+Y2/37NrLv7q8u7e3vMLL7VZEPjcvLi0uNDpDUuTRyr+8uLe6wsjO2/D1ZlVhaFE+Oz5CQD1F
+T2D95su/vry0srm8vb/N71lPTUI3NDc6Oj5PW/zf2MS9vsC9u8POy7/E1tTM1O5MQD49Ozc6
+PD5GSnbHv7m1uLzBxsjP+l1IPj02NDM1ODpBTlZq3c/OxsS9ubi7vLu7vsPM421MQzs5Ojc5
+PENOc8/EvLe3ubm6wc3UeFRRRT9DPDU0Nzs8PUdNWHHdysK+vby7vb6+vsDT4Ox5Tzs0MzEx
+NjxLZfjOvri3trOzub/H0tfkW01JQz49PTw7PURHRU5b+NjIvrm0tLS1s7i8w9D5WEY6NjM2
+Njs/PEn7yr6+v728v8XHzdp5XFdRS0A8Ozk1Njk8PkBN9tPIvLe0tLS2ube8v8r2Z1RDPDc2
+Ojo+Q0pTYtzLv7/Av8HHz8rJysnU6nFPQTw1MTAwMjU3QmLZwbq2s7O0tLe8xs33WE5CPDw/
+PDs9RkpKVG7XzsrFwb69vbq4vb/Jz9f6XkpEOjY2MzI3O0VWd83Avbq4t7i5ucLL1NrV5mlQ
+RTw5NTY6Ojs8QlRu38O4sbKyr7S4u8HWaEw/NzMwLi83Oj9JW+fTxby6ubi8vr7ExcXCzPNW
+SUE/Pzs5Ojo6PEdc2sW/vLq5uby9xc7W+FlIPz07Pz48PkBDQEhU7NDLxMbAwLu3t7q+wszc
+/lJDPTk1MjI1OTxHcdnKwbm4t7i6usDI0N5ueU9HQzk5Nzg5PENJVePKv7u4tLKxsLK2vMHI
+3WdIPjg2NjAyMzY6P0RKdtLNysbGxMPCxcXFzs3P2HBXSkI8ODg3Oz1FWN3Mvrm2t7Sys7e9
+w87XWElAPDw8Ojo6OD1BTV/j1szGw76+ubq9vsXTd01HPjw5NTIxNzxKXt3Mxr69u7q6u7y/
+xs3W1uFkXFVMRz45ODc5PD9OZNnFvrq5t7S0trq9x9xRQjo3Njg4NjU6Qkpm08nBu7q3tbKy
+tri+z+ZWTkk+PDk1MjAwMzlFWuHOx8O/vbq8vb3Dz8/0XUtDSEhDQEA9P0JGUP3VycK+u7u5
+trm9v8PN8GBTS0M7Ojs4NjY8QkhZ+uHTxr66t7q6vMbZ8l9TTUNBPj46ODg8Qkxp3MvFxb64
+ubm5uLvG2epqVEtEQTs5OTo+QklYYeDNx728uLi+u73L0e5iYUZFRT9EQ0RIRld78dbLvry9
+u7y+xMvW0+FbWFVKREE8Njc5Oj9LT1Rr39HLy8XHysfJxszZ7fh8YVJMRz49PkBHVnvdz8S8
+ure1uLu9v8jpZVhKSUZCPz48ODo9Q09WZuzs2MnFwcPAx9DW72daVkxRTENFRUNHRklWX2nq
+4dvQxry3s7a6v8TM315HQT07Oz1CTWbm18HBvr3Kys/gX2vv+vb0cunbfuZgUE1IRkVPV3zW
+0snBwcjVZmNYT05OTU9iaGh++GJeT01PSkhPZd7PysHBxMPHyM3/XVVLSk5RXW1n8d7Z1dzn
+/mVUWV9g993Rzc3HxsnP4HdUSUQ/PT0+REhV9v/q4ejqdGz98trQzs3Ozc3T3GddTUI+PkRP
+X33SzMnExs3b3PZYTk9c9tzfzsvK1OZpWk1CPj5DTGvY3ce/wr/EytzV2t3mYvvr9uXoffjy
+Z1VVT0xaW/vVys3MydXY+1pQRDw8PD1EVl1r4NjS0Nvu6u9tYWhidt7U0MjDysvS3/lXTEZN
+VFN059zIxMXI0tfqUktJR0xZaO3TycrU4mBaTkZDRktOZ+z66GxeXU5PTEhOXOjOy8bCwcHI
+0fRUS0c/Rk9c3tXHxMjAv8je4eL9a2RbXFxt5Ovz4OpoSj5EPz9FTXD43cu9vLi7wsnW3llR
+T05OTUpNTlTh4/tcX15hWlrqz8vJwMPCwtVhQTo4NDU3PE5b2MC+vr7J1OJ4e+re1szHy8zG
+ys7bWUY/PT1ATFrr0tLIyMjJ0+bvWE1UT05IVVRZeVttb1hST0xVbNnKycXGycfK3/RNRkhD
+Q0tYceXVzMvBxsjMeWVNQENITVVy2NbCvMPCz3dXPzk9ODtJWc3Cu7i6vcPV4G9f7+biz8jH
+yL/Ez3pIPTc3OEFKZG3n3m/P1XZTTUlBPkRIWN7ey8rMy81sTEY+PD5KadDEwry4ubW3v8Xa
+WUVAQ0VPXWvx4uHc9GXv42JaV1383tzczcvPytdPTUI3NjY7RV/Yy8K/vsHS/FxGP0NIU8/B
+v7y7vsXZTkE7MzAzO1HWw7qzrq6xuMvbZkA7NzhCUWXq29vbZlBMVFBITVhi1sG+trO0u8TP
+aU5AOz0+RVj+zs3Tz/J5YElHTkpKU17q0MTAv8XfXE4+Ojg1OD5KZ869uLa5v8TnWkxFRk5o
+d+HCvb25yeRmQj49Oz5IXPLPwsK9v9zrX0ZFQUFLXnblzMbGxdphUEI6OztBTGXVybq1trnC
+2F5DOzo7Qk1b9M/KycPR6OhZTlFKTm3f0s28uru+0tr8TkA/Ozw/R051yb+9vr6/ytvoZWv0
+6NPOycrP7E5KQDg3Njg+Q1/Xw7q9u8rd4VRFRD9BSldifc7HwMl6WE1CQ0xKVHHjzL+7uLS6
+xs7gVU9NRklPVmTdzs3OX1RYTElMT2nf1NjLx83XfUtGQD0+P0RS+t/Mv8G/wdPha05HRkVI
+XPXozMHFxMrW6VZEQERHTv3czb67vsDM62tQS0xNTlvm49fP3+dcTkpIREpabO/azsa8vL2/
+yd38X0tKR0FFU17cz8zJ2GhOS0Q+QkVGVuzRw7+8vsjY72VKQURES2Dz0MzMx8zdc15LR01S
+V2/e38vDv8TO6WpnRT4/Q0hj7d3Jw8bL1l5WTz0+QEJNZHvQv7/BydRhUUM7SEdO6M/IwL69
+vcLK2t9PQ0A8PEZTcNXIwMnH2OtuSUdCQk1oc+LSxsTO0PhhTz89QENe0si9uLS6wMXU3mRP
+U01Iad7dzMjJx9JtU0I5NTY2OUh72snBw8nN3mdKOzY4OT1gz8O7trW4vcvnelJIR0ZQd9nW
+ycG/xM/WZ1RGQEJHU3fb18fEy9j/VEpCOj4/QlXizb+7urzJ505AODQ3PUZs1MvAvry8v8p7
+WEg8PT5EX9nKvbi3ub7PWUM5MzQ3PE9o1MG5tLi6yu9UQTs9QE3k3czCv77EzOVhVUpMWFvX
+ysO9vb7I1ldBOjYxNDo+UtnJvrm4usLTVUE3Ly8yOkd70b21srS6yW9SQDs6PEdf28e+trKx
+tMTvTT47PD9Oa/3Sx8LDx81tTEE5Nzc6RF7ayru3ur/USzw3Mzg6Ql/OxbqzsrS5xWBFNzM0
+Nz5gzsW2sbO1vdhgRTk2NTc+TvfNvri3ub7XUz85Njg7SvDYxrq2t7rC3GRKPj48QVnQxLqz
+s7K6yV9HPDg2OkVW3OXJvb3B0H1BNy8uLjE7U+vHubOytrzSV0M5Nzc5Plnexbmysra70FtJ
+PTs/Q1Dczca8uLi+x9pRRTs3NzlI8tnLvbm8x91URD05PD1AXtHMv7i7vtVWRDgwLzM4P/HD
+vLOusLS91mRGOzc4OUNq3cO1sre5wtlTPTY1Nz5V3Mi9tba9wNdkSTw6Oj1G7sq/trCytsDd
+WEE8PD0+Smz53MK/xdBeTEU6OT09RWvVyry4ub3PaU09NjY5Pkps0sW6tre7yNT+Rjw7Oz5P
+7s7Bu7q6v83WZUpCP0BBS1v40MO/ws3tYUI6Ozw+Rm/Pxr+9vsPRd04/Ozo+QE9u07+5uLzG
+5WxKPT88PkVX7s2/uri5v83STkA9Ojs9SVbcyr+/w8vO2FlNS0lKbd/Sw8O+vcXX7GBIRUdN
+UW7tzsXNzM9zU1hIRkNFTVnj2Me/wsbcX0w/Ozo9QkpZbs/Gwr7Fz+Z/YEtLTE9aY+DMxcLB
+y9Lkc1xUU1p72dLMyNHbc1RUSUNBP0dMUPfcyMjHxdJ9XE1DQkJESFrwz8PEwsjZ/lxMSD8+
+Slzk07+9v7zE3PxVTExGS0xbdunKxcHBzObqYE1KS1Np+nPn3+La71NXVEtNXW7TycjCvb2/
+wNJjUUQ9PDxGSlbXy8XHycvja1tPVE9KUFZmf+La3Nl9VUpIQEFJT2zm1cbBv7/EzeZ3YVZQ
+UFZn3dbMw8XK22BTTEpLTlRq79XPy8XL0fJYTEA+PT4/SVj1zcbEycrXc2VVVU5NS09l+93Q
+ys7cf3lYUF1hdd7TzcXCycfPW1JLQ0FER1Bfbd7Ny8vK3GtTR0ZHVnrmzsW+vL69v8zU61pP
+TFNZYPrf3dHU1+teaU5ERUNGRkxOYuLn49j7XFxNSkpMUGN18dvf2c3N5G/yaWVwe+/d1MfA
+v77A0+h1W1VUTE5q/d/QzdHZ2f1fWU1IRkVLTVdmamz+fGNbaV5aZXhubOTv7N3Z1N3a2O58
++mleWWZr6ePo5+92Y/xrcO/v8nvp3NDGxcjP5u9nV1BMTk1JSk1o8evU3NPMz+Hd2uTa4tjQ
+0tDN2uXlZlVMSEZIWmh16evfztt3aFpPUE1ISlBd9NfX3O5oXE1PW2B969/X0s7Lztbc4ehf
+UlVVW1pi5uvc193T2NfPz9Tc43tjV0RLT0xRTVBl++De2tDc92hcZ1hPXlt429TP2NdrVEtM
+UllraPDh4ePV1+PqaG9ZYfhza+fc0snK1PV0X1Rbb+zi2/RtdGJeYVlvZ1/v5tfNysTFzc/Q
+0dTo3ftfX2BaXVhOUEpOVk9ea1lfZnVxbGNhVlpnYVteZW71bvplWFVUXeXUx8K9ur3Cz2pN
+R0hHSk5b5NDHwby+zfVSQ0JERU1Ye9vOxcLN3XdjWUtFPj9CQ0xz7uzl2d/7+W9sYO3c3M/Y
+29v6d19keWz73uHZ3O1kY2xp9G72917349rP5HpmU1hXW2dfetnc2t3ocmXmy87JxsjKz9TV
+0+VlTUhRXFz4fPvl93FNQDs4Oj9IVOnNwr29vMTbbklAQD1AQ0pb3cm9vsDH5mlcS01NUm7c
+y8LCyszO1d3d3N5tbF1T+PFkWExJT1nx3dfMysvXflI/PD5CR0tYftvOyL6/y3tOSklLT1Jc
+/N7W2NzgWVT25+TT1NPb1M3W52hOQkZOWfra0MzHxdLcb1A+QENAUGPZzL+2t7/Oak1JTlti
+9djKxry7wclkQz46OTk4P0/sztbb5e9wY/J2UkxITFz18GVsT0lMXtDLyMTCv7/Dz/dRRkxV
+X2JwzsK7uby+31FIQEdMTkxV9dnO1thhSD5GVlRMSE5yyMTIze9eTk3v2t/a9P7e/e5vSkM9
+TFp9z8W8t7XAyM9UPDM1PkBFR1i+r7CytchcPkdPREFHUV95c97O5V9c3MvPysnVzMfKyc5f
+R0Re1t/S09Pb3GhEPzw7ODc6QVjUw7+8vcvrRzIxPFzv7NzaxL68ubrLTz07R0pMYszAwcXL
+x9NzUFV+73PkdOn4/2hNQzxCYtXPysi/vsbJ315MNjA0OlrRzcTBvr7X4WxPQDU4PkV3wcjP
+xsfJz1pNTlTieejM1dbFwcrZbu5dVXBaUE1COkpdX+ns2NXWv8bVzXpLV+P0fk5CRl/FubSw
+r7e7vsxuRjowNDg6yaivKRcVHsyhmpqfoqlBGxMSIE64rKqquTgsNFu1r7CysrrSUFLdv73a
+PT3MxLVEFBARH7GflY6OlaM5GxscKklvzt3w4z4tNTvOrKejqb9GLS9BRTMqKCxcua+rKxUY
+G76ZlI6PlZ/FHhcaHz3JraSrudUsJCotS8+7s7e4yT0uNzg6Pi83zLu3zBYTGSKglY6Li5Cc
+VBUWFx8/TLysra24P0RGSrmzsLRZMCQdIS01QjwrT7zEwhoKDxRNmZCLiYyUoiQWGRk6wa2d
+mp6rVygtLEHEtqimsto7LSwuMDYsJDFDTFwYDRcgq5iSjo+WqS4PEBgmuKKamJijvCocHB9G
+sqOdnaxdNCguOz4+OkDGs61WDgoOH6eYkY+Ok58wFRgbLeXDq6arvVcsLjTTpqGdnatZJhsb
+JUC3sK6/Wrmw7RIFBQ3ul4uJh4iSuxUNDxcnO7Wkm56qWyMfIi9et6mipbJtPVduUE0/Nyoa
+FyuznrkYFBq5lZKPk5Wfxh0VGiqxq6Kio6xHGxAVHC7sqJiUl6pILCgnKzdIaE40KCIoS6aa
+qRgMDh+mlIuLio2bOxIMCxgvxqaZlJuqKxsaGx8qzqKZm6Kzelo4LS4pJyUkKDXIpp2Xk5ko
+CwgLN5qLh4eJj60WCgcNGiZvraCouC0bGhonQrKemZujvkFLVua6u7/lKxwdJ2G7r5+WkdcU
+DxDLmY2LjJCctBQKBw0iQLy1qa2vMhkaGizKpZmXn7k5Jiwz6q6tr8owIR4hPMKvp6WdkkUS
+ExK0ko6OkZmm5g8MDBE6vauor1E+IRQWHEyek4+RncEqHCM9v62xv0wsKCw/wb3StbGfli0a
+GB6cj42Sm7reHwoMDiHCr6qpwTIdDhMi0JuRkZah3C4gJUXYvc1GOzc6SubDvcXPr51OICAk
+nI+OkJq01B0ICgoauZ6YmqO3TxwVFh7TqJ6cqLdpNDpXclQvJCYuU7mrn6GinScNDQ1QkomG
+h46Zwg8JCA0i3qidnqu2KhkYHE+ooqa4Sjg5VrOttPYmHh4eLMiel5OTIw8PE7OTioiKlJ1j
+EhMNFjBJxrm/Ry8ZGBslvJ+YlqBNKR8fNGK3sMhBLCg17K6alZG2DQsLI5qMhoaLlaMbCwgI
+HEysoqCu0icRExcsu6KZmJ+6RCgqOGC8wGI2JydCv6qfnZybJAoJCjqYioWHjJGqEwoHDSzS
+qaWnsdcfEhESK6uZk5SfvjMeIjmynqG3Oh8bJD6vn5+Zk64VCQYWrJKJiYyOmykPCgwf9Kae
+o6vKKBIODx67nZSUmac/GhUcO6WamqNVIBwkXq6kmpSnGQsHE72ZjIuNj5wtEgsLHkiupams
+vSwWEhMlvZ+VkpagcR0XGzijl5ae7CIdIkWmlpVREAoLJrSZj42OlrccEw8ZOsmsp6iw8B4U
+EhYrw6SamZuhvyscGyi/npicsT4iHCfQn5KSRRILCymul42MjZCqGxANFTXOqp+dpr8fExET
+J8unm5mcor8qHxwjRrOfnaa/OiglKTaxmpGeGwwIEl6fkIyLj5o2Ew4PIVe0o5+kvCwTDhEc
+Y6aZlZadsjchHyZFsKOforJBJx8oPd2/v7Sdk7ATCAUYrpOLjI+VqxwOCxE8r6Cgq802GxAR
+GEKelJCUnrRSJh8oS6ibmp61NiIcHixMxr28rpqdHgsFCjqdjYmMkpw7EQ0OJKydmZ2vSyUS
+EBcqp5aSk5yv2isfJC66npqcqjgfGhskO8avsKWYuRIJBRC8l4uJjpakHw4MDSe0oJuftUQk
+FRUZKqiWkJGZqcAxHx4kyZyVlZ9OJBkYIUO4r7ekmTwPCAUbqpKKjJKZuRUMChBOqp2dq+Uy
+GhEWHseYkY+Unq5dHhccPJyQkJe3JBcSGTmzpq3DopspDggJOJuOio+boD4PDAsasqKanrZO
+LBcTGiarlZCQlp68IRATKaSPjpWnLxcPEyPKqa1xUJ6fGw4JEaaSi4uUnqcnDQwLIaqhm6PC
+PioXExssopOPjpWrMRUPH8ebj5OezRwSExtZpqbBMDKqmzsRDRGxko+OmKGoMw8NDx+roqKq
+20QtGxggO56PkZSmQyccH06pm5mrPCEZGyVKrqi6OS3LnJ0gEA8fmZKPk5yjuh8NDxY/oqaj
+r85LJxgbMrCXlZykvkMtKT2yp6zpJh4eIC1Iw7bN6cOjmx4ODxadjouMlJ+zJwoKDR2qpJ+m
+tl8wGhQhWpmPkZio3zstLdquqa82HxwdKETGr7xS2KabKA8OFZ6Pi4yXoLgfCgkLHa2knqe3
+yzYaExkunY+OlKXNOyorYK6ho9QqHR0pSLiqt7ulmq4PCgofmIyHi5KauBUGBgotq5+cqbFc
+IBIRGu2ZkI6VpVsjGyXrpJmcsTIcGh81uKm0wKiatxMLCSObjoqNlJu6FgkIDTGuoqCtuksf
+EhIa3pqRjpKjTx8YIn6jlpimPx0aIDm9sqmcmNwOCAkknY6JjZKb3hMJBw46rp6dqrc/HBAP
+F0qckY2PlKAxEg4Y75iPkJxTHRQWKMquqaCZpRoMCRWrkomJjZSnHwoHCRvLo5icpb8lEQ4R
+JKmWjo+Um7MiEA8cuJaPkZ9DGxIVJfGvpZuZQRIKCiqjkIqMj5jGFQsJEDmynpufrUMbDw8V
+NaSVjpCYpGMkHBonxKCWlqDXJBkaIDfBr6udmUwRCQklpI+JjI+ZTxELCRVQrZudqcIpFQ8Q
+G8Oaj46UnrcwHR0n26OcmqC/MB8bHyxjta6zvq6bvBUNCR+fkIuOl5+8Gg0MEVqmnJ2v9TEd
+FRYdu5iSkZut8y0fHynTn5udrD8pIR4nO8WqqbtLMT6lmjMUDA7enZSUm56g7hgTDyW0rai5
++WE/IhweMKealpigqrdJKykvw6Kcn7cwHx0dKD/KsLPBQickLfadnSgWDRLbqpmUlJWZyB4a
+Film4rq+vsBWJCElNrGknZ2hrLpCLTI8uqWhp8A3KSUnNUpj1G5ROisqMMmanikYDBmtm4+O
+kZSaPxkUDyQ/1rrHzNc7IyQmS6uhnZ+quNczLzhTsqyvt9RENCklKjFH82BLPT5lrperGA8L
+I56Qi42QmKIjDw4PLMqzrrvWSyoZGh9XoJiWmaKv2i8qKz23pZ+gs0YmGRkdKvq3ucvatpy8
+DQkHHpiMh4qPnLEXCwsPWZ+YnKzgNB0RFBzDl46OlJ+vVB0WGjeej46Xwx8TDhIeT6qjrsO2
+n68UCggZno2Ii5GfxRcLCw47o5iZpdgqGw8QGECbjoyPl6ZCGQ8XO5yOjZSzHxANFC2unKLq
+Okq3ni0NDQ+vj4yMk56/Lw0JDBi1oZyjvTsoGxMZJ6yTjo2UpT8dGSuvmZOYpy8TDREhvJ6d
+qzwqXtSpnhgPFBielpCOk5/HKQsPEx+wua65czYsHhst9Z2UmJypyl1MTbmnop+4MCEcITbV
+razJVTsu3Ku8mK4OFQsboJqOkpmrrB4OFQ0p4OCvyFdNQyUsOMmbl5ibr9tSOEHKxLq/RTUs
+LUdz59XoYuvWTtGqr/gtHiROu6qor661TC8oJi4/SVNYT048MzleuqunqKy4/z05O0ZbeN/J
+vLzgVEY9xrdTqJsrFQ4MPpeNio6itC0PDg0Xv5+anLZGLBsZHjakmJidtkNEUMmtr8NBJCMs
+Pb+0uK+doqOZHAoKDMmNiIiNplgfCQgLFsKfnZ+5LyETEBs+nI+PmLYxLTrbrKmzcigfJzK/
+qKmnoaydpQ8NDhuVi4iJkKhaFQMHChmxpp+oYy4iFRUiQJ2SlJmrTzkvSLOsq7k5KSYpS7aq
+rLyxo5S1DQsKLZKKhouSolgPAwYJL6iioMlAKBwQEh7Tko2NmcwqHRwru56UmK0vGx05r52f
+raGcmi8JBwzEj4iGjJOsJwgDBhWvnZmofi0aDwkPIqSPjZCerGMhFRcun46Nl8UgFhYotJ6d
+np2YqA8GBRKijYWHi5W3GgYFCSOmmpipUSITCwsUO5iNjJGiXiYaGSm7mY+Rm88lGxwq3Kql
+rMC3oKIfCwcNu5SLi5OdvSENDA8qopuZrTcfFhARHeaWjIyUszQfGhwqwJyRkJm+IRUTHEui
+k5CbZxsTGDifkJ4lEAsYO8SfmJKSnjAZFRgsPdu6trj8Kx0dJuOooZ+jqLhCKyk5u6OenqO3
+ZDQlICIuz6qmq8Q8KyUoN1zCwbSTmiMQAwkxoI2KjJGYLg8KCB++nZadreYmFBIVMJ+SjZGe
+sk4oJytjoZqZn1MjGxYZHi2/qKauQSMdHit4r6Ojr18uKayVxyoRDuuilZafpqZcHRoWPKKc
+mqxpSjEkJCnYnJaXo+o4LSQkKT6yqrFbKB8dGx0lPrGkp7k5KSgsRr2so6Ktt5yXTxcLC0OZ
+ioiNl6Q/EgwLFdyjmZ7HMiUbGBwrq5WQlKRfLyQfJTWznZylQhsSEBgvtZ6crD8iGx83uJ+c
+m4+VJg4HDcOThoWKlKQlDAkIGcykmqXfNSUaGyJSmo+Olqw/LCgmLU+qm5unNxsWFx86u6ir
+cyoiJjfyt6eVrRsVDS2ekIuQl6bMGhEPEzzEq63iPTAoJjnZn5WUlaDPLB4fRKWWkZzrHA4O
+EyPIqarBMiMjLUnXtq+bkzMWDxCulIyLk5/DKQwLDBnCr6KxSDYqIylYp5KOkpu7Rjk49q2m
+qL4tHhgZJTrJuO4zJyc4yrm8tq2wuK/SMiAWI8aZjo+Wti0TDA0SLrGenKO5Szkzb62emZuf
+rLrEw7KtuFAlHCEmKC0+7FQtISY278PG3czJy7/GvK2+TzsrMei4p6S110UwLCkoLDZH0MfH
+yM+/vbuuqamqrKyssrrTaUU5ODAuMS4uMj1nUzEpLkJZW9mupLwtIjOuoq1gNSs1Vdeurb3F
+YjkvKCs4TX3W18bCu62rq6uusLK0r62zyVI8OUNIS0UvJCImKi00PEqysT81O9LCJxkmro6H
+iZOrNR0WERUfPrKrvD0oJC/Mp5uXm6W1wsC5sbloKhsaIUC/7jk9qbGvqQwDCAuri4aBiZat
+JAsMFCKgnqCkOigeFhkquZaOkZq/LCYpU6+nquQhGRsnzq2ypqOemBoJCw+fioaFj59HGAgF
+ChS5o6SsKyYdFxoj6ZyPj5SoPCcdKbqbj46ZzR4UHEOom6TUXtirzA8NDi6RjImPnLUsEQkN
+F8iep7QpHBgQEBg6n46Olak7MigmL2ujlJOeySMcIzm0o6Sora6rGwsMEKyQioqSoG0eDAwQ
+J6Scm60zHxgTFSBYnJGRmbkzJyElOMKgmZqlUSUfJDjHr6mswee+qMsaDw0koJKNlKHENRsU
+Fh+1nZqjRR8ZFhceM6qUj5OjTSkjKjrZrJ2ZnLEuHBodLWu3pp+juy8fHyxUt6qqqJ2XUhAI
+Bh+kjomNlJ9+FQ0LFtijmqLqMCMbGR01pJSQlKdXMSgpLEWunJmfxicdGx4qP72loaprIxwd
+JD+5pZ6eqFglHB8+rZ2UjqofDQYUM6OQj5GZsCQXEBhGrJucqcc2IR0dKLmemJmo2DQnIyYu
+7KujpcEsHRocIjPGpp6grUYoIytfr6WforM/JyQwx6GTl0wZDRA0rpmTlJmkSB0XGCvJr6iz
+zkwvJiYra6adnqfHQTApKC1NsqiqyS8iHyEqOua+veY9MTdlw6qblqsdDw0mpZKMjpSfyCAV
+ExtOsqetXjAnISApSqeYlpqtUy8lJCv0ppubrC0XExYfOb2rrmEpLkyktxQTEumOioiRonYu
+GREYIK+cnqo0HhsdIC/GnY6Pl64rIic3uaCbm6k2GxMVI16vrXYnHh8rXbadrB8cFUiVjoqP
+nLk6HBUYG1yvrK86JiIjJjjCoZaXnrk6LjJOvrKvtFAqIB8qRdLDejw0NEBwuauhlr0VEQ9B
+lI2KkqHEKxEMDRZXq6SuQi0rJipNqpaPkp7MMSs07LettVwrIB4iMGu2tUwsJy1vuLStra+v
+r20eFhtLl46Ola42Gw4MEB/Bo6Or5i4rK0avnpeYo7pRP+y7rq3MOSsoKjM/9cDKXD88S9nL
+vMM/Tsi7rbLU8EgrJB8oyKOcnq5lKx0ZGBwqS8OyvM/eY862qJ+eo6uvrqqsrbhWMCgnMElP
+TjYqNTE1Ti49a8WcpzoXCxNOlouKkJ/LIBQODh35pZ6uQi0kHyQ3q5aPkJy3TELLrau03TQp
+IiMsQc/C+DcqO063lz0PEQw7lI6Kjp21NhQMDxRUp6ml4DMvJyY6y6SSkZemOyoyU6+hn6C2
+Mh8cJkm9rLQ9IyRKr5m+Dw4NMpWQjJOhwSsUCw8XVqWtrzchHxobKMuejo2Rm7jcR0XcvKqf
+oq52KyUoNeHBwtJOPjUrMWCtmywPDg3bnJSPm6a6OxgUFB/DurZOKSoqJyg4vpuUlp212+Xf
+zrqpm5WarS8bGyIz6b6zv0QtIiU6xaGgHxAMD86fko+UmqJSGhQSJuC/uXY9MSomKju1mpSW
+na/C6EdGz66gn6jANyghICYz3q6ruzwgGRohNt+xnpWmHAwGE8iXi4uOlaQoFQ4TM7iko7dU
+NCQeHyuumZKTnK/jPS8vPr+mn6O9Lh4aGyAsTbCorugoHBodKVOun52gszcnKDW9qqWkoZat
+GQ0GFLuWi4ySnbUfEQ4VUqSYmKfOPScdHSe/nZeZqU4uJyQnL2iqoqnNJhoZGyU3yKSdnq5C
+KyozZbiooJ+ozy4jIi1auaumqK/ONCQiKz++qaantUstJSYwVsS3vc7PpaodFA0YqJWNj5uu
+cB8SEhY/pJ6fvi4lIB8nOa+XkpajVi0rLDl4t6Wfps0pHBsfLE++rKau5y0iJjHlsKmoqrLW
+NigsULSmpa/bPC4uMTlrqp4zGA8RzZ2TkZifr0IcFhYmwa6pukg3MiosPbuemJmiu1tEQUhs
+wqulq9coGxsiNceysbxMLiQmMtKtq8REV7ObwBEOD86OioqToMEtEQsOGc2oqcgwKisqLF2p
+lo6SnrxFOjk2O166q69hJRoaHy5P1MzuQjQwOti6r66up5maMBUOGKaTjY+csjgYDQwRLayl
+psc7Ni4vP7adkI+asjMoKzjFq6iuXCMXFRkq5bS2TiolK0a+raurtOi5pZmdJBERIZ6Rj5Of
+vy4WDQ8aPailrbpvST476KublZmivEo6Pl3Ny0ktIB0cIS5K8VQ2MTg/Y9DAt7Gwr7XuPee5
+pqHYNzsySGpMxLK/xU8xNTI1T9m5r7m+yV5KQkrAr6+zxXFSRlHX6VA3LTEzOWja1kQtM0jv
+0c7Gu8JOQ269sL1XU8q4uUI2Ly5DaLuorMHuSEBaSne5tMNWPT5NZ9rDvbm/1dPJvrm+5l1R
+RjwwNUr8WzksLTA2Rlq4uFI+QEy5r7qvucHMIBok1piPkpy+Kx8ZGSI0yK+z3T0tLlOzn5yf
+qb5hY+jIvsk+JBwdKlTD0EE5MzA4VbiioZ6eHQsOFaKKiouVszYZDhIfbJ+hu0ojHR8hPKua
+kZKhSCIcKs+onZ+1Nx8bHy/itrPhMiUnNtOnoZ6YLQ4TENCRlI6YtW8gExQcK7uty9QvKDgx
+z6KZj4+Yp80xMEBUv7i5vE4sIR8mLjtPTz82LSsvNU/CvZ+cJxkVE7ScmpKesM4jFhkeNa6u
+t7FMP1g24qyelpaeqr5KTE5rs6mnqtopHBofLT/dy1kwIR4iL2/JxsanmzgXEhC2mZiTnqm3
+LxoXHDGsqbG1QjU9L0O7pZmWnKez2mxRTb2uqqu/NiMcHSMtRfvO0zwnHyIuT7qvrrC6yOrS
+oqUtKh83nJ+ho3dHLxseJDKyp7G57jxGQz7QsaefqsD3PjxGRWvKys1fMygkJCw3SdrCwtBZ
+Q0FGbcu6sa6vv87sXG3SuK64zE9LS0VEvaBOHRsVUJ+nm5+zq0wfHx8qzcHUzVQ7QjM4ybio
+oa7JWTk2My46Y8m6xV1LOzU5Q3W9tLW5yvRUUvXNwbmxsrjKTDc3Qu/DwMHeYlA/TFrnxsSl
+nEAfGhVqp62dprivOiAmJTi8yVzaQDQ1JShJxKigqK/DPTYwLkfQvLC40lg8NTQ4X72zr7fJ
+Xj8/SPXBu7e1tsH7RTxN38/Ky8xpOjBZrkceHRxknaakp+xSMBsdJjHBs7u1u9bQVj7Qs6yo
+s9fzRTg6Ola/vr/LT0U5MDU+f726u7/qVkdN7MK5u7vFzGNAOjXco8YnJB9enqurqMG8TR4j
+LDfAvs25uc7TTkXHubSwv9bqQzg1N1DmaU49ODYuLjU9X9rm1ehWY9a8sa+3vr66qZyuKiIf
+NqiurqGvvmseHCkuWM9by8lEOjM00q+rpqettXswMDA0RUJJbk0+My45S3jg8FhaXlPbwLe1
+u8jOv7KfoywfJyqvo72nqVdWJhcmOlKwsLepuEE9ND2+tbSsrrrDSTQ4ODxCOj1DODU0O1zM
+wcfO2ODp7N3LwLy+y83KyNxv46+fuiUiIzKwvtKru0lBIB43PF+4xLetyU9MPm+3ubWtsbzO
+OjM/PUpWTnv5Oy4wNEJke8/LzNT8fc++urrDx8DBytXoY1nOrrspHyMswLPErKrLay4jPOLZ
+uLi6sMY8Pj5C0MfHub77QTEuNj9P6tPM4Ec2Nz5iz8e7vMj3VFRtzsa/wL+/ycvLztLX5GzL
+rK1JKSox7sTOvLK5zEIsOFpm1MbLv9U1LjE4R1dRdeNdQzk7S19YZnTb12RJUdC/vb7Av7/J
+cG3Vv77H0c/N0GhOVvvjbuft2sXA0DwxMjxh08zBvdlNPztEXmzo2c7cVUNLV05WVGXf+llO
+UXTZ2d7c2dXvXmzu7Ofe4dva7ubazsbLzNXV3PNwWm9/9N/lz9Th2VxPSEVKS1hg9dXOys/T
+1Otw+G5sd1lWTkpZWFtu8O7scl/q9G5tZFtoa1xfXVNWYvTf3+vl9mlcWmNo5tPOzczQzsvM
+y8/R6Xp6WE9OTE5PTVRo9mZiX1xibnv3cX7ebFthWV9cXWFcWmr1fuDX19Pc5N/a39nP2N7t
++m1sbmlmdv14e9/U1NfV09Hc4+tnX1pJREVFSU1QWGzk6ere6mM=
+
+--foobarbazola--
+--16819560-2078917053-688350843:#11603--
+--owatagusiam--
+
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-1 b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-1
new file mode 100644
index 0000000000..4c71b75923
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-1
@@ -0,0 +1,11 @@
+Content-Type: multipart/mixed; boundary=boundary
+Content-Transfer-Encoding: base64
+
+This part shouldn't appear
+--boundary
+Content-Type: text/plain
+Content-Transfer-Encoding: base64
+
+TXVsdGlwYXJ0IGJhc2U2NCBlbmNvZGVkIHRleHQu
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-2 b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-2
new file mode 100644
index 0000000000..1ccfcff011
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-2
@@ -0,0 +1,12 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+yadda yadda
+
+--boundary
+Content-Type: text/html
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+YmFzZTY0ZW5jb2RlZCBIVE1MIHRleHQgaW5zaWRlIGEgbXVsdGlwYXJ0IG1lc3N
+hZ2UuPC9oZWFkPjwvaHRtbD4=
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-3 b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-3
new file mode 100644
index 0000000000..c0876a8d44
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart-base64-3
@@ -0,0 +1,10 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+etc etc
+--boundary
+Content-Type: text/html
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+VGhpcyB0aW1lLCB0aGUgdGFncw0Kc2hvdWxkIGJlIHN0cmlwcGVkIG91dC48L2hlYWQ+PC9odG1sPg==
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex1 b/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex1
new file mode 100644
index 0000000000..f9473072d8
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex1
@@ -0,0 +1,35 @@
+Content-Type: multipart/mixed; boundary="boundary"
+
+This shouldn't appear.
+--boundary
+Content-Type: application/octet-stream
+Content-Transfer-Encoding: base64
+
+VGhpcyBpc24ndCByZWFsbHkgYW4gYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtLiA7KQ=='
+
+--boundary
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+TmVpdGhlciBpcyB0aGlzIGFuIGltYWdlL3BuZy4=
+
+--boundary
+Content-Type: multipart/related; boundary="boundary2"
+
+--boundary2
+Content-Type: text/html
+
+<html><head>This part should be returned.</head></html>
+
+--boundary2--
+
+--boundary
+Content-Type: text/plain
+
+This part shouldn't.
+
+--boundary
+
+Neither should this part!
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex2 b/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex2
new file mode 100644
index 0000000000..0898d02810
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart-complex2
@@ -0,0 +1,62 @@
+From - Mon Jun 02 19:00:00 2008
+Content-Type: multipart/mixed; boundary="bou"
+Message-Id: <123456@example.com>
+
+Part 1
+--bou
+Content-Type: multipart/related; boundary="bound"
+
+Part 2
+--bound
+Content-Type: multipart/digest; boundary="boundar"
+
+Part 3
+--boundar
+Content-Type: multipart/alternative; boundary="boundary"
+
+Part 4
+--boundary
+Content-Type: application/octet-stream
+
+Wow, what alternatives!
+
+We're trying to confuse the parser here.
+
+--bou
+
+--bound
+
+--boundar
+
+--boundary
+Content-Type: application/pdf
+
+A choice between a PDF and an octet stream! How marvellous!
+
+--boundary--
+
+--boundar
+Content-Type: multipart/mixed; boundary="boundary123456"
+
+--boundary123456
+Content-Type: text/plain
+
+This is the correct answer.
+
+--boundary123456--
+
+--boundar--
+
+--bound
+Content-Type: text/plain
+
+One last attempt at confusing the parser.
+
+--bound--
+
+--bou
+Content-Type: text/html
+
+<html><body>No harm in making another.</body></html>
+
+--bou--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart1 b/comm/mailnews/mime/jsmime/test/unit/data/multipart1
new file mode 100644
index 0000000000..4c6b2a0745
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart1
@@ -0,0 +1,12 @@
+Content-Type: multipart/mixed;
+ boundary="boundary"
+
+This is a text message in MIME format.
+This part shouldn't appear in the output.
+
+--boundary
+Content-Type: text/plain
+
+Hello, world! (yet again...)
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart2 b/comm/mailnews/mime/jsmime/test/unit/data/multipart2
new file mode 100644
index 0000000000..8735523f45
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart2
@@ -0,0 +1,13 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+This is a text/html message. This part shouldn't appear at all!
+
+--boundary
+Content-Type: text/html
+
+<html><body>Multipart HTML message with just a single part!
+</body></html>
+
+--boundary--
+
+Actually, this part shouldn't appear either.
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart3 b/comm/mailnews/mime/jsmime/test/unit/data/multipart3
new file mode 100644
index 0000000000..2183cbd6a2
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart3
@@ -0,0 +1,29 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+--boundary
+Content-Type: text/html
+
+<html>
+
+
+<body>
+
+
+
+Here, the HTML tags should be stripped out.
+
+
+
+</body>
+
+
+
+</html>
+
+
+
+
+
+
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipart4 b/comm/mailnews/mime/jsmime/test/unit/data/multipart4
new file mode 100644
index 0000000000..934b5b1cc2
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipart4
@@ -0,0 +1,7 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+--boundary
+
+This has no headers, so should be recognized as plain text.
+
+--boundary--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/multipartmalt-detach b/comm/mailnews/mime/jsmime/test/unit/data/multipartmalt-detach
new file mode 100644
index 0000000000..2703afbb3d
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/multipartmalt-detach
@@ -0,0 +1,54 @@
+From
+X-Account-Key: account1
+X-UIDL: 0397aedc0eee392343488772c79f110d
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 10000000
+X-Mozilla-Keys:
+Return-Path: <ef@hg.de>
+X-Flags: 0000
+Date: Tue, 29 Aug 2006 16:42:08 GMT
+From: abc <ef@hg.de>
+To: abc <ef@hg.de>
+Subject: detach test
+Message-ID: <xxxyy@zzz>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="gmxboundary=-1156956072-29266-top"
+
+--gmxboundary=-1156956072-29266-top
+Content-Type: text/plain; charset="iso-8859-1"
+
+plain body
+--gmxboundary=-1156956072-29266-top
+Content-Type: multipart/related; boundary="gmxboundary=-1156956072-29266-sub"
+
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/html; charset="iso-8859-1"
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
+w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns=3D"http://www.w3.org/1999/xhtml" xml:lang=3D"de" lang=3D"de">
+<head>
+<title>Update</title>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-1" />
+</head>
+
+<body> body hello
+</body>
+</html>
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/plain
+Content-Disposition: inline; filename="head_update.txt"
+
+head_update.txt
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/plain
+Content-Disposition: inline; filename="smurf_update_neu.txt"
+
+smurf_update_neu.txt
+--gmxboundary=-1156956072-29266-sub--
+--gmxboundary=-1156956072-29266-top
+Content-Type: text/plain
+Content-Disposition: attachment; filename="head_update.txt"
+
+headUpdate.text
+--gmxboundary=-1156956072-29266-top--
diff --git a/comm/mailnews/mime/jsmime/test/unit/data/shift-jis-image b/comm/mailnews/mime/jsmime/test/unit/data/shift-jis-image
new file mode 100644
index 0000000000..e18619a678
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/data/shift-jis-image
@@ -0,0 +1,21 @@
+Subject: Shift-JIS and PNG test
+Content-Type: multipart/mixed; boundary="vungrzvzr"
+
+--vungrzvzr
+Content-Type: text/plain; charset=Shift-JIS
+Content-Transfer-Encoding: 8bit
+Content-Description: ƒPƒcƒ@ƒ‹ƒRƒAƒgƒ‹
+
+Portable Network Graphicsiƒ|[ƒ^ƒuƒ‹Eƒlƒbƒgƒ[ƒNEƒOƒ‰ƒtƒBƒbƒNƒXAPNGj‚̓Rƒ“ƒsƒ…[ƒ^‚Ńrƒbƒgƒ}ƒbƒv‰æ‘œ‚ðˆµ‚¤ƒtƒ@ƒCƒ‹ƒtƒH[ƒ}ƒbƒg‚Å‚ ‚éBˆ³kƒAƒ‹ƒSƒŠƒYƒ€‚Æ‚µ‚ÄDeflate‚ðÌ—p‚µ‚Ä‚¢‚éAˆ³k‚É‚æ‚é‰æŽ¿‚Ì—ò‰»‚Ì‚È‚¢‰Â‹tˆ³k‚̉摜ƒtƒ@ƒCƒ‹ƒtƒH[ƒ}ƒbƒg‚Å‚ ‚éB
+
+--vungrzvzr
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-Description: ƒPƒcƒ@ƒ‹ƒRƒAƒgƒ‹
+
+iVBORw0KGgoAAAANSUhEUgAAAIAAAABECAIAAADGJao+AAAAwklEQVR4Xu3UgQbDMBRA0bc03f//
+b7N0VuqJEmwoc+KqNEkDh9b+2HuJu1KNO4f+AQCAAAAQAAACAEAAAAgAAAEAIAAABACAAAAQAAAC
+AEAAAAgAAAEAIAAAANReamRLlPWYfNH0klxcPs+cP3NxWF+vi3lb7pa2R+vx6tHOtuN1O+a5lY3H
+zgM5ya/GM5N7ZjfPq7/5yS8IgAAAEAAAAgBAAAAIAAABACAAAAQAgAAAEAAAAgBAAAAIAAABACAA
+AIw322gDIPvtlmUAAAAASUVORK5CYII=
+--vungrzvzr--
diff --git a/comm/mailnews/mime/jsmime/test/unit/head_xpcshell_glue.js b/comm/mailnews/mime/jsmime/test/unit/head_xpcshell_glue.js
new file mode 100644
index 0000000000..6f7fca89f1
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/head_xpcshell_glue.js
@@ -0,0 +1,176 @@
+// This file needs to contain glue to rephrase the Mocha testsuite framework in
+// a way that the xpcshell test suite can understand.
+
+var { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+var requireCache = new Map();
+
+// Preload an assert module
+var assert = new Assert();
+assert.doesNotThrow = function (block, message) {
+ message = message ? " " + message : ".";
+ try {
+ block();
+ } catch (e) {
+ this.report(true, e, null, "Got unwanted exception" + message);
+ }
+};
+requireCache.set("assert", assert);
+
+// Preload an fs module
+var fs = {
+ readFile(filename, options, callback) {
+ if (callback === undefined) {
+ callback = options;
+ options = {};
+ }
+
+ // Convert according to encoding. For the moment, we don't support this
+ // node.js feature in the shim since we don't need to.
+ var translator = contents => contents;
+ if (options !== undefined && "encoding" in options) {
+ translator = function () {
+ throw new Error("I can't do this!");
+ };
+ }
+
+ Promise.resolve(filename)
+ .then(do_get_file)
+ .then(file => IOUtils.read(file.path))
+ .then(translator)
+ .then(contents => callback(undefined, contents), callback);
+ },
+};
+requireCache.set("fs", fs);
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+requireCache.set("jsmime", jsmime);
+
+function require(path) {
+ if (requireCache.has(path)) {
+ return requireCache.get(path);
+ }
+
+ let file;
+ if (path.startsWith("test/")) {
+ let name = path.substring("test/".length);
+ file = "resource://testing-common/jsmime/" + name + ".js";
+ } else {
+ file = "resource:///modules/jsmime/" + path + ".js";
+ }
+
+ var globalObject = {
+ define: innerDefine.bind(this, path),
+ };
+ Services.scriptloader.loadSubScript(file, globalObject);
+ return requireCache.get(path);
+}
+
+function innerDefine(moduleName, dfn) {
+ if (typeof dfn !== "function") {
+ throw new Error("What is going on here?");
+ }
+ function resolvingRequire(path) {
+ if (path.startsWith("./")) {
+ path = path.substring(2);
+ }
+ return require(path);
+ }
+ var result = dfn(resolvingRequire);
+ requireCache.set(moduleName, result);
+}
+
+var define = innerDefine.bind(this, "xpcshell-test");
+
+// Mocha TDD UI Bindings
+// ---------------------
+
+/**
+ * A block of tests, from the suite class.
+ */
+function MochaSuite(name) {
+ this.name = name;
+ this.setup = [];
+ this.tests = [];
+ this.teardown = [];
+ this.suites = [];
+}
+
+// The real code for running a suite of tests, written as async function.
+MochaSuite.prototype._runSuite = async function () {
+ info("Running suite " + this.name);
+ for (let setup_ of this.setup) {
+ await runFunction(setup_);
+ }
+ for (let test_ of this.tests) {
+ info("Running test " + test_.name);
+ await runFunction(test_.test);
+ }
+ for (let suite_ of this.suites) {
+ await suite_.runSuite();
+ }
+ for (let fn of this.teardown) {
+ await runFunction(fn);
+ }
+ info("Finished suite " + this.name);
+};
+
+// The outer call to run a test suite, which returns a promise of completion.
+MochaSuite.prototype.runSuite = function () {
+ return this._runSuite();
+};
+
+// Run the given function, returning a promise of when the test will complete.
+function runFunction(fn) {
+ let completed = new Promise(function (resolve, reject) {
+ function onEnd(error) {
+ if (error !== undefined) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ }
+ // If the function is expecting an argument, that argument is the callback
+ // above. If it's not, then it may be returning a promise.
+ if (fn.length == 1) {
+ fn(onEnd);
+ } else {
+ // Promise.resolve nicely handles both promises and not-promise values for
+ // us.
+ resolve(fn());
+ }
+ });
+ return completed;
+}
+
+var currentSuite = new MochaSuite("");
+function suite(name, tests) {
+ name = name.toString();
+ if (/[\x80-]/.exec(name)) {
+ name = "<unprintable name>";
+ }
+ let suiteParent = currentSuite;
+ currentSuite = new MochaSuite(name);
+ suiteParent.suites.push(currentSuite);
+ tests();
+ currentSuite = suiteParent;
+}
+function test(name, block) {
+ name = name.toString();
+ if (/[\x80-]/.exec(name)) {
+ name = "<unprintable name>";
+ }
+ currentSuite.tests.push({ name, test: block });
+}
+function setup(block) {
+ currentSuite.setup.push(block);
+}
+function teardown(block) {
+ currentSuite.teardown.push(block);
+}
+
+// The actual binding xpcshell needs to do its work.
+function run_test() {
+ add_task(() => currentSuite.runSuite());
+ run_next_test();
+}
diff --git a/comm/mailnews/mime/jsmime/test/unit/mock_date.js b/comm/mailnews/mime/jsmime/test/unit/mock_date.js
new file mode 100644
index 0000000000..87a6454968
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/mock_date.js
@@ -0,0 +1,86 @@
+"use strict";
+
+/* globals define */
+
+define(function (require) {
+ /**
+ * A class which appears to act like the Date class with customizable timezone
+ * offsets.
+ *
+ * @param {string} iso8601String An ISO-8601 date/time string including a
+ * timezone offset.
+ */
+ function MockDate(iso8601String) {
+ // Find the timezone offset (Z or ±hhmm) from the ISO-8601 date string, and
+ // then convert that into a number of minutes.
+ let parse = /\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(Z|[+-]\d{4})/.exec(
+ iso8601String
+ );
+ let tzOffsetStr = parse[1];
+ if (tzOffsetStr == "Z") {
+ this._tzOffset = 0;
+ } else {
+ this._tzOffset =
+ parseInt(tzOffsetStr.substring(1, 3)) * 60 +
+ parseInt(tzOffsetStr.substring(3));
+ if (tzOffsetStr[0] == "-") {
+ this._tzOffset = -this._tzOffset;
+ }
+ }
+
+ // To store the offset, we store both the real time in _realDate and a time
+ // that is offset by the tzOffset in _shiftedDate. Only the getUTC* methods
+ // should be used on these properties, to avoid problems caused by daylight
+ // savings time or other timezone effects. This shifting is always legal
+ // because ES6 is specified to assume that leap seconds do not exist, so there
+ // are always 60 seconds in a minute.
+ this._realDate = new Date(iso8601String);
+ this._shiftedDate = new Date(
+ this._realDate.getTime() + this._tzOffset * 60 * 1000
+ );
+ }
+ MockDate.prototype = {
+ getTimezoneOffset() {
+ // This property is reversed from how it's defined in ISO 8601, i.e.,
+ // UTC +0100 needs to return -60.
+ return -this._tzOffset;
+ },
+ getTime() {
+ return this._realDate.getTime();
+ },
+ };
+
+ // Provide an implementation of Date methods that will be need in JSMime. For
+ // the time being, we only need .get* methods.
+ for (let name of Object.getOwnPropertyNames(Date.prototype)) {
+ // Only copy getters, not setters or x.toString.
+ if (!name.startsWith("get")) {
+ continue;
+ }
+ // No redefining any other names on MockDate.
+ if (MockDate.prototype.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name.includes("UTC")) {
+ // 'name' is already supposed to be freshly bound per newest ES6 drafts, but
+ // current ES6 implementations reuse the bindings. Until implementations
+ // catch up, use a new let to bind it freshly.
+ let boundName = name;
+ Object.defineProperty(MockDate.prototype, name, {
+ value(...aArgs) {
+ return Date.prototype[boundName].call(this._realDate, aArgs);
+ },
+ });
+ } else {
+ let newName = "getUTC" + name.substr(3);
+ Object.defineProperty(MockDate.prototype, name, {
+ value(...aArgs) {
+ return Date.prototype[newName].call(this._shiftedDate, aArgs);
+ },
+ });
+ }
+ }
+
+ return MockDate;
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_custom_headers.js b/comm/mailnews/mime/jsmime/test/unit/test_custom_headers.js
new file mode 100644
index 0000000000..85c86628de
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_custom_headers.js
@@ -0,0 +1,43 @@
+"use strict";
+define(function (require) {
+ var assert = require("assert");
+ var jsmime = require("jsmime");
+
+ suite("Custom decoder support", function () {
+ function customDecoder(values) {
+ let value = values.join("");
+ return atob(value);
+ }
+ function customEncoder(value) {
+ this.addText(btoa(value), true);
+ }
+ test("addStructuredEncoder", function () {
+ assert.equal(
+ "X-Base64: String\r\n",
+ jsmime.headeremitter.emitStructuredHeader("X-Base64", "String", {})
+ );
+ jsmime.headeremitter.addStructuredEncoder("X-Base64", customEncoder);
+ assert.equal(
+ "X-Base64: U3RyaW5n\r\n",
+ jsmime.headeremitter.emitStructuredHeader("X-Base64", "String", {})
+ );
+ assert.equal(
+ "X-Base64: U3RyaW5n\r\n",
+ jsmime.headeremitter.emitStructuredHeader("x-bASe64", "String", {})
+ );
+ });
+ test("addStructuredDecoder", function () {
+ assert.throws(function () {
+ jsmime.headerparser.parseStructuredHeader("X-Base64", "U3RyaW5n");
+ }, /Unknown structured header/);
+ jsmime.headerparser.addStructuredDecoder("X-Base64", customDecoder);
+ assert.equal(
+ "String",
+ jsmime.headerparser.parseStructuredHeader("X-Base64", "U3RyaW5n")
+ );
+ assert.throws(function () {
+ jsmime.headerparser.addStructuredDecoder("To", customDecoder);
+ }, /Cannot override header/);
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_header.js b/comm/mailnews/mime/jsmime/test/unit/test_header.js
new file mode 100644
index 0000000000..f00ec372ef
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_header.js
@@ -0,0 +1,1224 @@
+"use strict";
+define(function (require) {
+ var headerparser = require("jsmime").headerparser;
+ var assert = require("assert");
+
+ function smartDeepEqual(actual, expected) {
+ assert.deepEqual(actual, expected);
+ // XXX: instanceof Map don't work for actual. Unclear why.
+ if ("entries" in actual && "entries" in expected) {
+ assert.deepEqual(
+ Array.from(actual.entries()),
+ Array.from(expected.entries())
+ );
+ }
+ }
+
+ function arrayTest(data, fn) {
+ fn.toString = function () {
+ let text = Function.prototype.toString.call(this);
+ text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
+ return JSON.stringify(data[p]);
+ });
+ return text;
+ };
+ return test(data[0], fn);
+ }
+ suite("headerparser", function () {
+ suite("parseParameterHeader", function () {
+ let header_tests = [
+ ["multipart/related", ["multipart/related", {}]],
+ ["a ; b=v", ["a", { b: "v" }]],
+ ["a ; b='v'", ["a", { b: "'v'" }]],
+ ['a; b = "v"', ["a", { b: "v" }]],
+ ["a;b=1;b=2", ["a", { b: "1" }]],
+ ["a;b=2;b=1", ["a", { b: "2" }]],
+ ['a;b="a;b"', ["a", { b: "a;b" }]],
+ ['a;b="\\\\"', ["a", { b: "\\" }]],
+ ['a;b="a\\b\\c"', ["a", { b: "abc" }]],
+ ["a;b=1;c=2", ["a", { b: "1", c: "2" }]],
+ ['a;b="a\\', ["a", { b: "a" }]],
+ ["a;b", ["a", {}]],
+ ['a;b=";";c=d', ["a", { b: ";", c: "d" }]],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let testMap = new Map();
+ for (let key in data[1][1]) {
+ testMap.set(key, data[1][1][key]);
+ }
+ testMap.preSemi = data[1][0];
+ assert.deepEqual(
+ headerparser.parseParameterHeader(data[0], false, false),
+ testMap
+ );
+ });
+ });
+ });
+ suite("parseParameterHeader (2231/2047 support)", function () {
+ let header_tests = [
+ // Copied from test_MIME_params.js and adapted
+ ["attachment;", ["attachment", {}]],
+ ["attachment; filename=basic", ["attachment", { filename: "basic" }]],
+ ['attachment; filename="\\""', ["attachment", { filename: '"' }]],
+ ['attachment; filename="\\x"', ["attachment", { filename: "x" }]],
+ ['attachment; filename=""', ["attachment", { filename: "" }]],
+ ["attachment; filename=", ["attachment", { filename: "" }]],
+ ["attachment; filename X", ["attachment", {}]],
+ [
+ "attachment; filename = foo-A.html",
+ ["attachment", { filename: "foo-A.html" }],
+ ],
+ ['attachment; filename="', ["attachment", { filename: "" }]],
+ [
+ "attachment; filename=foo; trouble",
+ ["attachment", { filename: "foo" }],
+ ],
+ [
+ "attachment; filename=foo; trouble ",
+ ["attachment", { filename: "foo" }],
+ ],
+ ["attachment", ["attachment", {}]],
+ ["attachment; filename=foo", ["attachment", { filename: "foo" }]],
+ ['attachment; filename="foo"', ["attachment", { filename: "foo" }]],
+ ["attachment; filename='foo'", ["attachment", { filename: "'foo'" }]],
+ [
+ 'attachment; filename="=?UTF-8?Q?foo?="',
+ ["attachment", { filename: "foo" }],
+ ],
+ [
+ "attachment; filename==?UTF-8?Q?foo?=",
+ ["attachment", { filename: "foo" }],
+ ],
+ // 2231/5987 tests from test_MIME_params.js
+ [
+ "attachment; filename*=UTF-8''extended",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename=basic; filename*=UTF-8''extended",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename*=UTF-8''extended; filename=basic",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename*0=foo; filename*1=bar",
+ ["attachment", { filename: "foobar" }],
+ ],
+ [
+ "attachment; filename*0=first; filename*0=wrong; filename=basic",
+ ["attachment", { filename: "first" }],
+ ], // or basic?
+ [
+ "attachment; filename*0=first; filename*1=second; filename*0=wrong",
+ ["attachment", { filename: "firstsecond" }],
+ ], // or nothing?
+ [
+ "attachment; filename=basic; filename*0=foo; filename*1=bar",
+ ["attachment", { filename: "foobar" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0=first; filename*0=wrong; " +
+ "filename*=UTF-8''extended",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo" +
+ "; filename*1=bar",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename*0=foo; filename*2=bar",
+ ["attachment", { filename: "foo" }],
+ ],
+ [
+ "attachment; filename*0=foo; filename*01=bar",
+ ["attachment", { filename: "foo" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi; filename*1=line" +
+ "; filename*2*=%20extended",
+ ["attachment", { filename: "multiline extended" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi; filename*1=line" +
+ "; filename*3*=%20extended",
+ ["attachment", { filename: "multiline" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi; filename*1=line" +
+ "; filename*0*=UTF-8''wrong; filename*1=bad; filename*2=evil",
+ ["attachment", { filename: "multiline" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0=UTF-8''multi; filename*=UTF-8'" +
+ "'extended; filename*1=line; filename*2*=%20extended",
+ ["attachment", { filename: "extended" }],
+ ],
+ [
+ "attachment; filename*0=UTF-8''unescaped; filename*1*=%20so%20includes" +
+ "%20UTF-8''%20in%20value",
+ [
+ "attachment",
+ { filename: "UTF-8''unescaped so includes UTF-8'' in value" },
+ ],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi; filename*1=line" +
+ "; filename*0*=UTF-8''wrong; filename*1=bad; filename*2=evil",
+ ["attachment", { filename: "multiline" }],
+ ],
+ [
+ "attachment; filename=basic; filename*1=foo; filename*2=bar",
+ ["attachment", { filename: "basic" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*1=1; filen" +
+ "ame*2=2;filename*3=3;filename*4=4;filename*5=5;filename*6=6;filename" +
+ "*7=7;filename*8=8;filename*9=9;filename*10=a;filename*11=b;filename*" +
+ "12=c;filename*13=d;filename*14=e;filename*15=f",
+ ["attachment", { filename: "0123456789abcdef" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*1=1; filen" +
+ "ame*2=2;filename*3=3;filename*4=4;filename*5=5;filename*6=6;filename" +
+ "*7=7;filename*8=8;filename*9=9;filename*10=a;filename*11=b;filename*" +
+ "12=c;filename*14=e",
+ ["attachment", { filename: "0123456789abc" }],
+ ],
+ [
+ "attachment; filename*1=multi; filename*2=line; filename*3*=%20extended",
+ ["attachment", {}],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*1=1; filen" +
+ "ame*2=2;filename*3=3;filename*4=4;filename*5=5;filename*6=6;filename" +
+ "*7=7;filename*8=8;filename*9=9;filename*10=a;filename*11=b;filename*" +
+ "12=c;filename*13=d;filename*15=f;filename*14=e",
+ ["attachment", { filename: "0123456789abcdef" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*1a=1",
+ ["attachment", { filename: "0" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*1111111111" +
+ "1111111111111111111111111=1",
+ ["attachment", { filename: "0" }],
+ ],
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0; filename*-1=1",
+ ["attachment", { filename: "0" }],
+ ],
+ [
+ 'attachment; filename=basic; filename*0="0"; filename*1=1; filename*' +
+ "2*=%32",
+ ["attachment", { filename: "012" }],
+ ],
+ [
+ "attachment; filename=basic; filename**=UTF-8''0;",
+ ["attachment", { filename: "basic" }],
+ ],
+ [
+ "attachment; filename=IT839\x04\xB5(m8)2.pdf;",
+ ["attachment", { filename: "IT839\u00b5(m8)2.pdf" }],
+ ],
+ ["attachment; filename*=utf-8''%41", ["attachment", { filename: "A" }]],
+ // See bug 651185 and bug 703015
+ [
+ "attachment; filename*=\"utf-8''%41\"",
+ ["attachment", { filename: "A" }],
+ ],
+ ["attachment; filename *=utf-8''foo-%41", ["attachment", {}]],
+ ["attachment; filename*=''foo", ["attachment", {}]],
+ ["attachment; filename*=a''foo", ["attachment", {}]],
+ // Bug 692574: we should ignore this one...
+ [
+ "attachment; filename*=UTF-8'foo-%41",
+ ["attachment", { filename: "foo-A" }],
+ ],
+ ["attachment; filename*=foo-%41", ["attachment", {}]],
+ [
+ "attachment; filename*=UTF-8'foo-%41; filename=bar",
+ ["attachment", { filename: "foo-A" }],
+ ],
+ [
+ "attachment; filename*=ISO-8859-1''%c3%a4",
+ ["attachment", { filename: "\u00c3\u00a4" }],
+ ],
+ [
+ "attachment; filename*=ISO-8859-1''%e2%82%ac",
+ ["attachment", { filename: "\u00e2\u201a\u00ac" }],
+ ],
+ ["attachment; filename*=UTF-8''A%e4B", ["attachment", {}]],
+ [
+ "attachment; filename*=UTF-8''A%e4B; filename=fallback",
+ ["attachment", { filename: "fallback" }],
+ ],
+ [
+ "attachment; filename*0*=UTF-8''A%e4B; filename=fallback",
+ ["attachment", { filename: "fallback" }],
+ ],
+ [
+ "attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8" +
+ "859-1''currency-sign%3d%a4",
+ ["attachment", { filename: "currency-sign=\u00a4" }],
+ ],
+ [
+ "attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=IS" +
+ "O-8859-15''euro-sign%3d%a4",
+ ["attachment", { filename: "currency-sign=\u00a4" }],
+ ],
+ [
+ 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\' +
+ 'r"',
+ ["attachment", { filename: "foobar" }],
+ ],
+ [
+ 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\',
+ ["attachment", { filename: "fooba" }],
+ ],
+ ['attachment; filename="\\b\\a\\', ["attachment", { filename: "ba" }]],
+ // According to comments and bugs, this works in necko, but it doesn't
+ // appear that it ought to. See bug 732369 for more info.
+ [
+ "attachment; extension=bla filename=foo",
+ ["attachment", { extension: "bla" }],
+ ],
+ [
+ "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=",
+ ["attachment", { filename: "foo-\u00e4.html" }],
+ ],
+ [
+ 'attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="',
+ ["attachment", { filename: "foo-\u00e4.html" }],
+ ],
+ [
+ 'attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="; filename*=UTF' +
+ "-8''5987",
+ ["attachment", { filename: "5987" }],
+ ],
+ // ABC\u202Etxt.zip dir switch char in middle.
+ [
+ "attachment; filename*=UTF-8''%41%42%43%E2%80%AE%2E%74%78%74%2E%7A%69%70",
+ ["attachment", { filename: "ABC .txt.zip" }],
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let testMap = new Map();
+ for (let key in data[1][1]) {
+ testMap.set(key, data[1][1][key]);
+ }
+ testMap.preSemi = data[1][0];
+ smartDeepEqual(
+ headerparser.parseParameterHeader(data[0], true, true),
+ testMap
+ );
+ });
+ });
+ });
+ suite("parseAddressingHeader", function () {
+ let header_tests = [
+ ["", []],
+ [
+ "Joe Schmoe <jschmoe@invalid.invalid>",
+ [{ name: "Joe Schmoe", email: "jschmoe@invalid.invalid" }],
+ ],
+ [
+ "user@tinderbox.invalid",
+ [{ name: "", email: "user@tinderbox.invalid" }],
+ ],
+ [
+ "Hello Kitty <a@b.c>, No Kitty <b@b.c>",
+ [
+ { name: "Hello Kitty", email: "a@b.c" },
+ { name: "No Kitty", email: "b@b.c" },
+ ],
+ ],
+ [
+ "undisclosed-recipients:;",
+ [{ name: "undisclosed-recipients", group: [] }],
+ ],
+ ["me@[127.0.0.1]", [{ name: "", email: "me@[127.0.0.1]" }]],
+ ['"me"@a.com', [{ name: "", email: "me@a.com" }]],
+ ['"!"@a.com', [{ name: "", email: '"!"@a.com' }]],
+ ['"\\!"@a.com', [{ name: "", email: '"!"@a.com' }]],
+ ['"\\\\!"@a.com', [{ name: "", email: '"\\\\!"@a.com' }]],
+ [
+ "Coward (not@email) <real@email.com>",
+ [{ name: "Coward (not@email)", email: "real@email.com" }],
+ ],
+ [
+ '"y@example.com" <xx@example.com>',
+ [{ name: "y@example.com", email: "xx@example.com" }],
+ ],
+ ["@@@", [{ name: "", email: '"@@"@' }]],
+ [
+ "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@",
+ [{ name: "", email: '"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"@' }],
+ ],
+ [
+ "Nat Ex <{admin@example.com|stocks@example.com|stockmarket@example.com|trades@example.com|money@example.com|broker@example.com|deals@example.com|offers@example.com|news@example.com|binary@example.com|forex@example.com|marketing@example.com|mail@example.com|inboxdeals@example.com|packages@example.com>",
+ [
+ {
+ name: "Nat Ex",
+ email:
+ '"{admin@example.com|stocks@example.com|stockmarket@example.com|trades@example.com|money@example.com|broker@example.com|deals@example.com|offers@example.com|news@example.com|binary@example.com|forex@example.com|marketing@example.com|mail@example.com|inboxdeals@example.com|packages"@example.com',
+ },
+ ],
+ ],
+ [
+ "Group: a@b.com, b@c.com;",
+ [
+ {
+ name: "Group",
+ group: [
+ { name: "", email: "a@b.com" },
+ { name: "", email: "b@c.com" },
+ ],
+ },
+ ],
+ ],
+ [
+ "a@invalid.invalid, Group: a@b.com;",
+ [
+ { name: "", email: "a@invalid.invalid" },
+ { name: "Group", group: [{ name: "", email: "a@b.com" }] },
+ ],
+ ],
+ [
+ "Group A: a@b.com;, Group B: b@b.com;",
+ [
+ { name: "Group A", group: [{ name: "", email: "a@b.com" }] },
+ { name: "Group B", group: [{ name: "", email: "b@b.com" }] },
+ ],
+ ],
+ [
+ 'Crazy (<Stupid "name") <simple@a.email>',
+ [{ name: "Crazy (<Stupid name)", email: "simple@a.email" }],
+ ],
+ [
+ "Group: Real <a@b.com>, Fake <a@b.com>",
+ [
+ {
+ name: "Group",
+ group: [
+ { name: "Real", email: "a@b.com" },
+ { name: "Fake", email: "a@b.com" },
+ ],
+ },
+ ],
+ ],
+ [
+ '"Joe Q. Public" <john.q.public@example.com>,' +
+ 'Test <"abc!x.yz"@foo.invalid>, Test <test@[xyz!]>,' +
+ '"Giant; \\"Big\\" Box" <sysservices@example.net>',
+ [
+ { name: "Joe Q. Public", email: "john.q.public@example.com" },
+ { name: "Test", email: '"abc!x.yz"@foo.invalid' },
+ { name: "Test", email: "test@[xyz!]" },
+ { name: 'Giant; "Big" Box', email: "sysservices@example.net" },
+ ],
+ ],
+ [
+ "Unfortunate breaking < so . many . spaces @ here . invalid >",
+ [
+ {
+ name: "Unfortunate breaking",
+ email: "so.many.spaces@here.invalid",
+ },
+ ],
+ ],
+ [
+ "so . many . spaces @ here . invalid",
+ [{ name: "", email: "so.many.spaces@here.invalid" }],
+ ],
+ ["abc@foo.invalid", [{ name: "", email: "abc@foo.invalid" }]],
+ ["foo <ghj@foo.invalid>", [{ name: "foo", email: "ghj@foo.invalid" }]],
+ [
+ "abc@foo.invalid, foo <ghj@foo.invalid>",
+ [
+ { name: "", email: "abc@foo.invalid" },
+ { name: "foo", email: "ghj@foo.invalid" },
+ ],
+ ],
+ [
+ "foo bar <foo@bar.invalid>",
+ [{ name: "foo bar", email: "foo@bar.invalid" }],
+ ],
+ [
+ "foo bar <foo@bar.invalid>, abc@foo.invalid, foo <ghj@foo.invalid>",
+ [
+ { name: "foo bar", email: "foo@bar.invalid" },
+ { name: "", email: "abc@foo.invalid" },
+ { name: "foo", email: "ghj@foo.invalid" },
+ ],
+ ],
+ [
+ "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>",
+ [
+ { name: "foo\u00D0 bar", email: "foo@bar.invalid" },
+ { name: "\u00F6foo", email: "ghj@foo.invalid" },
+ ],
+ ],
+ [
+ "Undisclosed recipients:;",
+ [{ name: "Undisclosed recipients", group: [] }],
+ ],
+ [
+ '" "@a a;b',
+ [
+ { name: "", email: '" "@a a' },
+ { name: "b", email: "" },
+ ],
+ ],
+ [
+ "Undisclosed recipients:;\0:; foo <ghj@veryveryveryverylongveryveryver" +
+ "yveryinvalidaddress.invalid>",
+ [
+ { name: "Undisclosed recipients", group: [] },
+ {
+ name: "foo",
+ email:
+ "ghj@veryveryveryverylongveryveryveryveryinvali" +
+ "daddress.invalid",
+ },
+ ],
+ ],
+ // XXX: test_nsIMsgHeaderParser2 has an empty one here...
+ [
+ "<a;a@invalid",
+ [
+ { name: "", email: "a" },
+ { name: "", email: "a@invalid" },
+ ],
+ ],
+ ["me@foo.invalid", [{ name: "", email: "me@foo.invalid" }]],
+ [
+ "me@foo.invalid, me2@foo.invalid",
+ [
+ { name: "", email: "me@foo.invalid" },
+ { name: "", email: "me2@foo.invalid" },
+ ],
+ ],
+ [
+ '"foo bar" <me@foo.invalid>',
+ [{ name: "foo bar", email: "me@foo.invalid" }],
+ ],
+ [
+ '"foo bar" <me@foo.invalid>, "bar foo" <me2@foo.invalid>',
+ [
+ { name: "foo bar", email: "me@foo.invalid" },
+ { name: "bar foo", email: "me2@foo.invalid" },
+ ],
+ ],
+ [
+ "A Group:Ed Jones <c@a.invalid>,joe@where.invalid,John <jdoe@one.invalid>;",
+ [
+ {
+ name: "A Group",
+ group: [
+ { name: "Ed Jones", email: "c@a.invalid" },
+ { name: "", email: "joe@where.invalid" },
+ { name: "John", email: "jdoe@one.invalid" },
+ ],
+ },
+ ],
+ ],
+ [
+ "mygroup:;, empty:;, foo@foo.invalid, othergroup:bar@foo.invalid, bar2" +
+ "@foo.invalid;, y@y.invalid, empty:;",
+ [
+ { name: "mygroup", group: [] },
+ { name: "empty", group: [] },
+ { name: "", email: "foo@foo.invalid" },
+ {
+ name: "othergroup",
+ group: [
+ { name: "", email: "bar@foo.invalid" },
+ { name: "", email: "bar2@foo.invalid" },
+ ],
+ },
+ { name: "", email: "y@y.invalid" },
+ { name: "empty", group: [] },
+ ],
+ ],
+ [
+ "Undisclosed recipients:;;;;;;;;;;;;;;;;,,,,,,,,,,,,,,,,",
+ [{ name: "Undisclosed recipients", group: [] }],
+ ],
+ [
+ "a@xxx.invalid; b@xxx.invalid",
+ [
+ { name: "", email: "a@xxx.invalid" },
+ { name: "", email: "b@xxx.invalid" },
+ ],
+ ],
+ [
+ "a@xxx.invalid; B <b@xxx.invalid>",
+ [
+ { name: "", email: "a@xxx.invalid" },
+ { name: "B", email: "b@xxx.invalid" },
+ ],
+ ],
+ [
+ '"A " <a@xxx.invalid>; b@xxx.invalid',
+ [
+ { name: "A", email: "a@xxx.invalid" },
+ { name: "", email: "b@xxx.invalid" },
+ ],
+ ],
+ [
+ "A <a@xxx.invalid>; B <b@xxx.invalid>",
+ [
+ { name: "A", email: "a@xxx.invalid" },
+ { name: "B", email: "b@xxx.invalid" },
+ ],
+ ],
+ [
+ "A (this: is, a comment;) <a.invalid>; g: (this: is, <a> comment;) C" +
+ "<c.invalid>, d.invalid;",
+ [
+ { name: "A (this: is, a comment;)", email: "a.invalid" },
+ {
+ name: "g",
+ group: [
+ { name: "(this: is, <a> comment;) C", email: "c.invalid" },
+ { name: "d.invalid", email: "" },
+ ],
+ },
+ ],
+ ],
+ [
+ "Mary Smith <mary@x.invalid>, extra:;, group:jdoe@example.invalid; Who" +
+ '? <one@y.invalid>; <boss@nil.invalid>, "Giant; \\"Big\\" Box" <sysse' +
+ "rvices@example.invalid>, ",
+ [
+ { name: "Mary Smith", email: "mary@x.invalid" },
+ { name: "extra", group: [] },
+ {
+ name: "group",
+ group: [{ name: "", email: "jdoe@example.invalid" }],
+ },
+ { name: "Who?", email: "one@y.invalid" },
+ { name: "", email: "boss@nil.invalid" },
+ { name: 'Giant; "Big" Box', email: "sysservices@example.invalid" },
+ ],
+ ],
+ [
+ "Undisclosed recipients: a@foo.invalid ;;extra:;",
+ [
+ {
+ name: "Undisclosed recipients",
+ group: [{ name: "", email: "a@foo.invalid" }],
+ },
+ { name: "extra", group: [] },
+ ],
+ ],
+ [
+ "Undisclosed recipients:;;extra:a@foo.invalid;",
+ [
+ { name: "Undisclosed recipients", group: [] },
+ { name: "extra", group: [{ name: "", email: "a@foo.invalid" }] },
+ ],
+ ],
+ ["a < <a@b.c>", [{ name: "a", email: "a@b.c" }]],
+ [
+ "Name <incomplete@email",
+ [{ name: "Name", email: "incomplete@email" }],
+ ],
+ [
+ "Name <space here@email.invalid>",
+ [{ name: "Name", email: '"space here"@email.invalid' }],
+ ],
+ ["Name <not an email>", [{ name: "Name", email: "not an email" }]],
+ [
+ "=?UTF-8?Q?Simple?= <a@b.c>",
+ [{ name: "=?UTF-8?Q?Simple?=", email: "a@b.c" }],
+ ],
+ ["No email address", [{ name: "No email address", email: "" }]],
+ // Thought we were parsing an address, but it was a name.
+ [
+ "name@example.com <receiver@example.com>",
+ [{ name: "name@example.com", email: "receiver@example.com" }],
+ ],
+ [
+ "name@huhu.com <receiver@example.com>",
+ [{ name: "name@huhu.com", email: "receiver@example.com" }],
+ ],
+ // Some names with quotes.
+ [
+ '"name@huhu.com" <receiver@example.com>',
+ [{ name: "name@huhu.com", email: "receiver@example.com" }],
+ ],
+ [
+ '"Chaplin, Charlie" <receiver@example.com>',
+ [{ name: "Chaplin, Charlie", email: "receiver@example.com" }],
+ ],
+ [
+ '"name@huhu.com and name@haha.com" <receiver@example.com>',
+ [
+ {
+ name: "name@huhu.com and name@haha.com",
+ email: "receiver@example.com",
+ },
+ ],
+ ],
+ // Handling of comments and legacy display-names as per RFC 5322 §3.4
+ [
+ "(c1)n(c2) <(c3)a(c4)@(c5)b(c6).(c7)d(c8)> (c9(c10)c11)",
+ [{ name: "(c1) n (c2) (c9(c10)c11)", email: "a@b.d" }],
+ ],
+ [
+ "<(c3)a(c4)@(c5)b(c6).(c7)d(c8)> (c9(c10)c11)",
+ [{ name: "(c9(c10)c11)", email: "a@b.d" }],
+ ],
+ [
+ "(c3)a(c4)@(c5)b(c6).(c7)d(c8)(c9(c10)c11)",
+ [{ name: "c9(c10)c11", email: "a@b.d" }],
+ ],
+ [
+ "(c1)n(c2) <(c3)a(c4)@(c5)b(c6).(c7)d(c8)> (c9(c10)c11)(c12)",
+ [{ name: "(c1) n (c2) (c9(c10)c11) (c12)", email: "a@b.d" }],
+ ],
+ [
+ "<(c3)a(c4)@(c5)b(c6).(c7)d(c8)> (c9(c10)c11)(c12)",
+ [{ name: "(c9(c10)c11) (c12)", email: "a@b.d" }],
+ ],
+ [
+ "(c3)a(c4)@(c5)b(c6).(c7)d(c8)(c9(c10)c11)(c12)",
+ [{ name: "c12", email: "a@b.d" }],
+ ],
+ // Collapse extraneous whitespace and make sure unexpected characters aren't there.
+ [
+ 'Friend "<friend@huhu.com>" \t \t \u00A0\u00A0\u2003 \u00AD \x20\u200B\x20\u200B\x20 \t \u034F \u2028 \uDB40\uDD01 \u2800 \t <ws@example.com>',
+ [{ name: "Friend <friend@huhu.com>", email: "ws@example.com" }],
+ ],
+ // Collapse multiple "special" spaces like NBSP (\u00A0), EM space (\u2003), etc.
+ [
+ "Foe \u00A0\u00A0\u2003 A <foe@example.com>",
+ [{ name: "Foe A", email: "foe@example.com" }],
+ ],
+ // Remove tabs.
+ [
+ "Tabby \t \t A\t\tB <tab@example.com>",
+ [{ name: "Tabby A B", email: "tab@example.com" }],
+ ],
+ // Ensure it's the address in angles that is parsed as address.
+ [
+ "<attacker@example.com>friend@example.com\t, bystander@example.com,Someone (some@invalid) <someone@example.com>,",
+ [
+ { name: "", email: "attacker@example.com" },
+ { name: "", email: "bystander@example.com" },
+ { name: "Someone (some@invalid)", email: "someone@example.com" },
+ ],
+ ],
+ [
+ "me (via foo@example.com) <attacker2@example.com>friend2@example.com ",
+ [
+ {
+ name: "me (via foo@example.com)",
+ email: "attacker2@example.com",
+ },
+ ],
+ ],
+ [
+ "<attacker@example.com> <friend@example.com>,friend2@example.com<attacker2@example.com>",
+ [
+ {
+ name: "",
+ email: "attacker@example.com",
+ },
+ {
+ name: "friend2@example.com",
+ email: "attacker2@example.com",
+ },
+ ],
+ ],
+ [
+ 'My "XX ><<friend2@example.com>" YY <attacker@example.com> <friend@example.com> ("mr ><x")',
+ [
+ {
+ name: "My XX ><<friend2@example.com> YY (mr ><x)",
+ email: "attacker@example.com",
+ },
+ ],
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.deepEqual(
+ headerparser.parseAddressingHeader(data[0], false),
+ data[1]
+ );
+ });
+ });
+ });
+ suite("parseAddressingHeader (RFC 2047 support)", function () {
+ let header_tests = [
+ ["Simple <a@b.c>", [{ name: "Simple", email: "a@b.c" }]],
+ ["=?UTF-8?Q?Simple?= <a@b.c>", [{ name: "Simple", email: "a@b.c" }]],
+ ["=?UTF-8?Q?=3C@b.c?= <a@b.c>", [{ name: "<@b.c", email: "a@b.c" }]],
+
+ // RFC 2047 tokens should not interfere with lexical processing
+ ["=?UTF-8?Q?a@b.c,?= <b@b.c>", [{ name: "a@b.c,", email: "b@b.c" }]],
+ ["=?UTF-8?Q?a@b.c=2C?= <b@b.c>", [{ name: "a@b.c,", email: "b@b.c" }]],
+ ["=?UTF-8?Q?<?= <a@b.c>", [{ name: "<", email: "a@b.c" }]],
+ [
+ "Simple =?UTF-8?Q?<?= a@b.c>",
+ [{ name: "", email: '"Simple < a"@b.c' }],
+ ],
+ ["Tag <=?UTF-8?Q?email?=@b.c>", [{ name: "Tag", email: "email@b.c" }]],
+ // handling of comments and legacy display-names as per RFC 5322 §3.4
+ [
+ "jl1@b.c (=?ISO-8859-1?Q?Joe_L=F6we?=)",
+ [{ name: "Joe Löwe", email: "jl1@b.c" }],
+ ],
+ [
+ "(=?ISO-8859-1?Q?Joe_L=F6we?=) jl2@b.c",
+ [{ name: "Joe Löwe", email: "jl2@b.c" }],
+ ],
+ [
+ "(=?ISO-8859-1?Q?Joe_L=F6we?=) jl3@b.c (c2)",
+ [{ name: "c2", email: "jl3@b.c" }],
+ ],
+ [
+ "=?ISO-8859-1?Q?Joe_L=F6we?= <jl3@b.c> (c2)",
+ [{ name: "Joe Löwe (c2)", email: "jl3@b.c" }],
+ ],
+ [
+ "(=?ISO-8859-1?Q?Joe_L=F6we?=) <jl3@b.c> (c2)",
+ [{ name: "(Joe Löwe) (c2)", email: "jl3@b.c" }],
+ ],
+ // Bug 1141446: Malformed From addresses with erroneous quotes,
+ // note: acute accents: a \u00E1, e \u00E9, i \u00ED, o \u00F3, u \u00FA.
+ [
+ '"=?UTF-8?Q?Jazzy_Fern=C3=A1ndez_Nunoz?= jazzy.f.nunoz@example.com ' +
+ '[BCN-FC]" <Barcelona-Freecycle-noreply@yahoogroups.com>',
+ [
+ {
+ name: "Jazzy Fern\u00E1ndez Nunoz jazzy.f.nunoz@example.com [BCN-FC]",
+ email: "Barcelona-Freecycle-noreply@yahoogroups.com",
+ },
+ ],
+ ],
+ [
+ '"=?UTF-8?B?TWlyaWFtIEJlcm5hYsOpIFBlcmVsbMOz?= miriam@example.com ' +
+ '[BCN-FC]" <Barcelona-Freecycle-noreply@yahoogroups.com>',
+ [
+ {
+ name: "Miriam Bernab\u00E9 Perell\u00F3 miriam@example.com [BCN-FC]",
+ email: "Barcelona-Freecycle-noreply@yahoogroups.com",
+ },
+ ],
+ ],
+ [
+ '"=?iso-8859-1?Q?First_Mar=EDa_Furi=F3_Gancho?= mail@yahoo.es ' +
+ '[BCN-FC]" <Barcelona-Freecycle-noreply@yahoogroups.com>',
+ [
+ {
+ name: "First Mar\u00EDa Furi\u00F3 Gancho mail@yahoo.es [BCN-FC]",
+ email: "Barcelona-Freecycle-noreply@yahoogroups.com",
+ },
+ ],
+ ],
+ [
+ '"=?iso-8859-1?B?U29maWEgQ2FzdGVsbPMgUm9tZXJv?= sonia@example.com ' +
+ '[BCN-FC]" <Barcelona-Freecycle-noreply@yahoogroups.com>',
+ [
+ {
+ name: "Sofia Castell\u00F3 Romero sonia@example.com [BCN-FC]",
+ email: "Barcelona-Freecycle-noreply@yahoogroups.com",
+ },
+ ],
+ ],
+ [
+ "=?iso-8859-1?Q?Klaus_Eisschl=E4ger_=28k=2Eeisschlaeger=40t-onli?=" +
+ "=?iso-8859-1?Q?ne=2Ede=29?= <k.eisschlaeger@t-online.de>",
+ [
+ {
+ name: "Klaus Eisschläger (k.eisschlaeger@t-online.de)",
+ email: "k.eisschlaeger@t-online.de",
+ },
+ ],
+ ],
+ [
+ '"=?UTF-8?Q?=22Claudia_R=C3=B6hschicht=22?= Claudia_Roehschicht@web.de [freecycle-berlin]" ' +
+ "<freecycle-berlin-noreply@yahoogroups.de>",
+ [
+ {
+ name: '"Claudia Röhschicht" Claudia_Roehschicht@web.de [freecycle-berlin]',
+ email: "freecycle-berlin-noreply@yahoogroups.de",
+ },
+ ],
+ ],
+ // Collapse multiple consecutive "special" spaces, like zero width space
+ // etc.
+ // \u00AD is soft hyphen. \u200B is zero width space.
+ [
+ `invisiblespaceA@friend.example.com B \u200B\u00AD \u200B \u200B A. <foeA@example.com>`,
+ [
+ {
+ name: "invisiblespaceA@friend.example.com B A.",
+ email: "foeA@example.com",
+ },
+ ],
+ ],
+
+ // Collapse multiple consecutive "special" spaces, like zero width space
+ // etc. also when encoded.
+ // \u00AD is soft hyphen. \u200B is zero width space.
+ // unescape(encodeURIComponent(source)) encodes the JavaScript UTF-16 representation
+ // of the string into UTF-8. Example: encodeURIComponent("ö") returns "%C3%B6",
+ // unescape("%C3%B6") returns the bytes 0xC3B6 which is the UTF-8 encoding of "ö".
+ // See bug 1551746 for other ways.
+ [
+ //"=?UTF-8?B?IMKgIGJsw7YgPGludmlzaWJsZXNwYWNlQGZyaWVuZC5leGFtcGxlLmNvbT4g4oCLIOKAiyDigIsu=?= <foe@example.com>"
+ `=?UTF-8?B?${btoa(
+ unescape(
+ encodeURIComponent(
+ " \u00AD blö <invisiblespace@friend.example.com> \u200B \u200B \u200B."
+ )
+ )
+ )}?= <foe@example.com>`,
+ [
+ {
+ name: "blö <invisiblespace@friend.example.com> .",
+ email: "foe@example.com",
+ },
+ ],
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.deepEqual(
+ headerparser.parseAddressingHeader(data[0], true),
+ data[1]
+ );
+ });
+ });
+ });
+ suite("parseDateHeader", function () {
+ let header_tests = [
+ // Some basic tests, derived from searching for Date headers in a mailing
+ // list archive.
+ ["Thu, 06 Sep 2012 08:08:21 -0700", "2012-09-06T08:08:21-0700"],
+ ["Thu, 6 Sep 2012 14:49:05 -0400", "2012-09-06T14:49:05-0400"],
+ ["Fri, 07 Sep 2012 07:30:11 -0700 (PDT)", "2012-09-07T07:30:11-0700"],
+ ["9 Sep 2012 21:03:59 -0000", "2012-09-09T21:03:59Z"],
+ ["Sun, 09 Sep 2012 19:10:59 -0400", "2012-09-09T19:10:59-0400"],
+ ["Wed, 17 Jun 2009 10:12:25 +0530", "2009-06-17T10:12:25+0530"],
+
+ // Exercise all the months.
+ ["Mon, 28 Jan 2013 13:35:05 -0500", "2013-01-28T13:35:05-0500"],
+ ["Wed, 29 Feb 2012 23:43:26 +0000", "2012-02-29T23:43:26+0000"],
+ ["Sat, 09 Mar 2013 18:24:47 -0500", "2013-03-09T18:24:47-0500"],
+ ["Sat, 27 Apr 2013 12:51:48 -0400", "2013-04-27T12:51:48-0400"],
+ ["Tue, 28 May 2013 17:21:13 +0800", "2013-05-28T17:21:13+0800"],
+ ["Mon, 17 Jun 2013 22:15:41 +0200", "2013-06-17T22:15:41+0200"],
+ ["Wed, 18 Jul 2012 13:50:47 +0900", "2012-07-18T13:50:47+0900"],
+ ["Mon, 13 Aug 2012 13:55:16 +0200", "2012-08-13T13:55:16+0200"],
+ ["Thu, 06 Sep 2012 19:49:47 -0400", "2012-09-06T19:49:47-0400"],
+ ["Mon, 22 Oct 2012 02:27:23 -0700", "2012-10-22T02:27:23-0700"],
+ ["Thu, 22 Nov 2012 09:04:24 +0800", "2012-11-22T09:04:24+0800"],
+ ["Sun, 25 Dec 2011 12:27:13 +0000", "2011-12-25T12:27:13+0000"],
+
+ // Try out less common timezone offsets.
+ ["Sun, 25 Dec 2011 12:27:13 +1337", "2011-12-25T12:27:13+1337"],
+ ["Sun, 25 Dec 2011 12:27:13 -1337", "2011-12-25T12:27:13-1337"],
+
+ // Leap seconds! Except that since dates in JS don't believe they exist,
+ // they get shoved to the next second.
+ ["30 Jun 2012 23:59:60 +0000", "2012-07-01T00:00:00Z"],
+ ["31 Dec 2008 23:59:60 +0000", "2009-01-01T00:00:00Z"],
+ // This one doesn't exist (they are added only as needed on an irregular
+ // basis), but it's plausible...
+ ["30 Jun 2030 23:59:60 +0000", "2030-07-01T00:00:00Z"],
+ // ... and this one isn't.
+ ["10 Jun 2030 13:39:60 +0000", "2030-06-10T13:40:00Z"],
+ // How about leap seconds in other timezones?
+ ["30 Jun 2012 18:59:60 -0500", "2012-07-01T00:00:00Z"],
+
+ // RFC 5322 obsolete date productions
+ ["Sun, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+ ["Tue, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+ ["Thu, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+ ["Sun, 26 Jan 2014 17:14:22 EST", "2014-01-26T17:14:22-0500"],
+ ["Sun, 26 Jan 2014 17:14:22 CST", "2014-01-26T17:14:22-0600"],
+ ["Sun, 26 Jan 2014 17:14:22 MST", "2014-01-26T17:14:22-0700"],
+ ["Sun, 26 Jan 2014 17:14:22 PST", "2014-01-26T17:14:22-0800"],
+ ["Sun, 26 Jan 2014 17:14:22 AST", "2014-01-26T17:14:22-0400"],
+ ["Sun, 26 Jan 2014 17:14:22 NST", "2014-01-26T17:14:22-0330"],
+ ["Sun, 26 Jan 2014 17:14:22 MET", "2014-01-26T17:14:22+0100"],
+ ["Sun, 26 Jan 2014 17:14:22 EET", "2014-01-26T17:14:22+0200"],
+ ["Sun, 26 Jan 2014 17:14:22 JST", "2014-01-26T17:14:22+0900"],
+ ["Sun, 26 Jan 2014 17:14:22 GMT", "2014-01-26T17:14:22+0000"],
+ ["Sun, 26 Jan 2014 17:14:22 UT", "2014-01-26T17:14:22+0000"],
+ // Daylight savings timezones, even though these aren't actually during
+ // daylight savings time for the relevant jurisdictions.
+ ["Sun, 26 Jan 2014 17:14:22 EDT", "2014-01-26T17:14:22-0400"],
+ ["Sun, 26 Jan 2014 17:14:22 CDT", "2014-01-26T17:14:22-0500"],
+ ["Sun, 26 Jan 2014 17:14:22 MDT", "2014-01-26T17:14:22-0600"],
+ ["Sun, 26 Jan 2014 17:14:22 PDT", "2014-01-26T17:14:22-0700"],
+ ["Sun, 26 Jan 2014 17:14:22 BST", "2014-01-26T17:14:22+0100"],
+ // Unknown time zone--assume UTC
+ ["Sun, 26 Jan 2014 17:14:22 QMT", "2014-01-26T17:14:22+0000"],
+
+ // The following days of the week are incorrect.
+ ["Tue, 28 Jan 2013 13:35:05 -0500", "2013-01-28T13:35:05-0500"],
+ ["Thu, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+ ["Fri, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+ ["Mon, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+ // And for these 2 digit years, they are correct for the other century.
+ ["Mon, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+ ["Wed, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+ ["Wed, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+
+ // Try with some illegal names for days of the week or months of the year.
+ ["Sam, 05 Apr 2014 15:04:13 -0500", "2014-04-05T15:04:13-0500"],
+ ["Lun, 01 Apr 2014 15:04:13 -0500", "2014-04-01T15:04:13-0500"],
+ ["Mar, 02 Apr 2014 15:04:13 -0500", "2014-04-02T15:04:13-0500"],
+ ["Mar, 02 April 2014 15:04:13 -0500", "2014-04-02T15:04:13-0500"],
+ ["Mar, 02 Avr 2014 15:04:13 -0500", NaN],
+ ["Tue, 02 A 2014 15:04:13 -0500", NaN],
+
+ // A truly invalid date
+ ["Coincident with the rapture", NaN],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.equal(
+ headerparser.parseDateHeader(data[0]).toString(),
+ new Date(data[1]).toString()
+ );
+ });
+ });
+ });
+
+ suite("decodeRFC2047Words", function () {
+ let header_tests = [
+ // Some basic sanity tests for the test process
+ ["Test", "Test"],
+ ["Test 2", "Test 2"],
+ ["Basic words", "Basic words"],
+ ["Not a =? word", "Not a =? word"],
+
+ // Simple 2047 decodings
+ ["=?UTF-8?Q?Encoded?=", "Encoded"],
+ ["=?UTF-8?q?Encoded?=", "Encoded"],
+ ["=?ISO-8859-1?Q?oxyg=e8ne?=", "oxyg\u00e8ne"],
+ ["=?UTF-8?B?QmFzZTY0?=", "Base64"],
+ ["=?UTF-8?b?QmFzZTY0?=", "Base64"],
+ ["=?UTF-8?Q?A_space?=", "A space"],
+ ["=?UTF-8?Q?A space?=", "A space"],
+ ["A =?UTF-8?Q?B?= C", "A B C"],
+ ["=?UTF-8?Q?A?= =?UTF-8?Q?B?=", "AB"],
+ ["=?UTF-8?Q?oxyg=c3=a8ne?=", "oxyg\u00e8ne"],
+ ["=?utf-8?Q?oxyg=C3=A8ne?=", "oxyg\u00e8ne"],
+ ["=?UTF-8?B?b3h5Z8OobmU=?=", "oxyg\u00e8ne"],
+ ["=?UTF-8*fr?B?b3h5Z8OobmU=?=", "oxyg\u00e8ne"],
+ [
+ "=?BIG5?Q?=B9=CF=AE=D1=C0]SSCI=A4=CEJCR=B8=EA=AE=C6=AEw=C1=BF=B2=DF=A1A=A8" +
+ "=F3=A7U=B1z=A1u=B4=A3=A4=C9=AC=E3=A8s=AF=C0=BD=E8=BBP=AE=C4=B2v=A5H=A4=CE" +
+ "=A7=EB=BDZ=B5=A6=B2=A4=AA=BA=B9B=A5=CE=A1v=A1A=C5w=AA=EF=B3=F8=A6W=B0=D1" +
+ "=A5[=A1C?=",
+ "\u5716\u66F8\u9928SSCI\u53CAJCR\u8CC7\u6599\u5EAB\u8B1B" +
+ "\u7FD2\uFF0C\u5354\u52A9\u60A8\u300C\u63D0\u5347\u7814\u7A76\u7D20\u8CEA" +
+ "\u8207\u6548\u7387\u4EE5\u53CA\u6295\u7A3F\u7B56\u7565\u7684\u904B\u7528" +
+ "\u300D\uFF0C\u6B61\u8FCE\u5831\u540D\u53C3\u52A0\u3002",
+ ],
+
+ // Invalid decodings
+ ["=?UTF-8?Q?=f0ab?=", "\ufffdab"],
+ ["=?UTF-8?Q?=f0?= ab", "\ufffd ab"],
+ [
+ "=?UTF-8?Q?=ed=a0=bd=ed=b2=a9?=",
+ "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd",
+ ],
+ ["=?NoSuchCharset?Q?ab?=", "=?NoSuchCharset?Q?ab?="],
+ ["=?UTF-8?U?Encoded?=", "=?UTF-8?U?Encoded?="],
+ ["=?UTF-8?Q?Almost", "=?UTF-8?Q?Almost"],
+
+ // Try some non-BMP characters in various charsets
+ ["=?UTF-8?B?8J+SqQ==?=", "\ud83d\udca9"],
+ // The goal for the next one is to be a non-BMP in a non-full-Unicode
+ // charset. The only category where this exists is a small set of
+ // characters in Big5, which were previously mapped to a PUA in an older
+ // version but then reassigned to Plane 1. However, Big5 is really a set
+ // of slightly different, slightly incompatible charsets.
+ // TODO: This requires future investigation. Bug 912470 discusses the
+ // changes to Big5 proposed within Mozilla.
+ // ["=?Big5?Q?=87E?=", "\ud85c\ude67"],
+ ["=?GB18030?B?lDnaMw==?=", "\ud83d\udca9"],
+
+ // How to handle breaks in multi-byte encoding
+ ["=?UTF-8?Q?=f0=9f?= =?UTF-8?Q?=92=a9?=", "\ud83d\udca9"],
+ ["=?UTF-8?B?8J+S?= =?UTF-8?B?qQ==?=", "\ud83d\udca9"],
+ ["=?UTF-8?B?8J+S?= =?UTF-8?Q?=a9?=", "\ud83d\udca9"],
+ ["=?UTF-8?B?8J+S?= =?ISO-8859-1?B?qQ==?=", "\ufffd\u00a9"],
+ ["=?UTF-8?Q?=f0?= =?UTF-8?Q?ab?=", "\ufffdab"],
+
+ // This is a split non-BMP character.
+ [
+ "=?UTF-8?B?YfCfkqnwn5Kp8J+SqfCfkqnwn5Kp8J+SqfCfkqnvv70=?= =?UTF-8?B?77+9?=",
+ "a\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D" +
+ "\uDCA9\uD83D\uDCA9\uFFFD\uFFFD",
+ ],
+
+ // Spaces in RFC 2047 tokens
+ ["=?UTF-8?Q?Invalid token?=", "Invalid token"],
+
+ // More tests from bug 493544
+ [
+ "AAA =?UTF-8?Q?bbb?= CCC =?UTF-8?Q?ddd?= EEE =?UTF-8?Q?fff?= GGG",
+ "AAA bbb CCC ddd EEE fff GGG",
+ ],
+ [
+ "=?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiC" +
+ "Ag?=\n =?UTF-8?B?4oiJICDiiIogIOKIiyAg4oiMICDiiI0gIOKIjiAg4oiP?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__=E2" +
+ "?=\n =?utf-8?Q?=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__" +
+ "=E2=88?=\n =?utf-8?Q?=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8" +
+ "E__=E2=88=8F?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA" +
+ "==?=\n =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?UTF-8?b?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA" +
+ "==?=\n =?UTF-8?b?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" +
+ " =?utf-8?Q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" +
+ " =?utf-8?Q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" +
+ " =?utf-8?Q?=E2=88=8F?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?utf-8?q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" +
+ " =?utf-8?q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" +
+ " =?utf-8?q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" +
+ " =?utf-8?q?=E2=88=8F?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+ [
+ "=?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA=" +
+ "==?=\n =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ "\u2200 \u2201 \u2202 \u2203 \u2204 \u2205 \u2206 \u2207 " +
+ "\u2208 \u2209 \u220a \u220b \u220c \u220d \u220e \u220f",
+ ],
+
+ // Some interesting headers found in the wild:
+ // Bug 1498795: Tolerate spaces in base64-encoded RFC 2047 tokens.
+ [
+ "=?UTF-8?B?Q29uc2lndW Ug dG9kbyBlbCBmw7p0Ym9sIA==?= =?iso-8859-1?" +
+ "B?Y2 9uI EZVU0nTTiBjb24g?= =?windows-1252?B?ZX N 0ZSBvZmVydPNu?=",
+ "Consigue todo el f\u00fatbol con FUSI\u00d3N con este ofert\u00f3n",
+ ],
+ ["=?us-ascii?Q?=09Edward_Rosten?=", "\tEdward Rosten"],
+ [
+ "=?us-ascii?Q?=3D=3FUTF-8=3FQ=3Ff=3DC3=3DBCr=3F=3D?=",
+ "=?UTF-8?Q?f=C3=BCr?=",
+ ],
+ // We don't decode unrecognized charsets (This one is actually UTF-8).
+ ["=??B?Sy4gSC4gdm9uIFLDvGRlbg==?=", "=??B?Sy4gSC4gdm9uIFLDvGRlbg==?="],
+
+ // Test for bug 1374149 with ISO-2022-JP where we shouldn't stream
+ // if the first token ends in ESC(B.
+ // GyRCJCIbKEI= is the base64 encoding of ESC$B$"ESC(B.
+ [
+ "=?ISO-2022-JP?B?GyRCJCIbKEI=?==?ISO-2022-JP?B?GyRCJCIbKEI=?=",
+ "ã‚ã‚",
+ ],
+
+ // Tolerate invalid split of character, € = 0xE2 0x82 0xAC in UTF-8.
+ [
+ "Split =?UTF-8?Q?=E2?= =?UTF-8?Q?=82=AC?= after first byte",
+ "Split € after first byte",
+ ],
+ [
+ "Split =?UTF-8?Q?=E2=82?= =?UTF-8?Q?=AC?= after second byte",
+ "Split € after second byte",
+ ],
+ ["Byte missing =?UTF-8?Q?=E2=82?=", "Byte missing \ufffd"], // Replacement character for invalid input.
+
+ // Test for bug 1301989: Tolerate invalid base64 encoding.
+ ["=?us-ascii?B?YWJjZA==?=", "abcd"], // correct
+ ["=?us-ascii?B?YWJjZA=?=", "abc"], // not a multiple of 4
+ ["=?us-ascii?B?Y=WJjZA==?=", "abcd"], // invalid =
+ ["=?us-ascii?B?Y=WJj==ZA==?=", "abcd"], // invalid =
+ ["=?us-ascii?B?YWJjZA===?=", "abcd"], // excess = at the end, see bug 227290.
+
+ // Test for bug 1437282: Tolerate extra = padding at the end.
+ ["=?us-ascii?B?VGVzdA==?=", "Test"], // This is correct.
+ ["=?us-ascii?B?VGVzdA===?=", "Test"],
+ ["=?us-ascii?B?VGVzdA====?=", "Test"],
+ ["=?us-ascii?B?VGVzdA=====?=", "Test"],
+ ["=?us-ascii?B?VGVzdA======?=", "Test"],
+ ["=?us-ascii?B?VGVzdA========?=", "Test"],
+ ["=?us-ascii?B?VGVzdA=========?=", "Test"],
+ ["=?us-ascii?B?VGVzdA==========?=", "Test"],
+ ["=?us-ascii?B?VGVzdA===========?=", "Test"],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.deepEqual(headerparser.decodeRFC2047Words(data[0]), data[1]);
+ });
+ });
+ });
+ suite("8-bit header processing", function () {
+ let header_tests = [
+ // Non-ASCII header values
+ ["oxyg\xc3\xa8ne", "oxyg\u00e8ne", "UTF-8"],
+ ["oxyg\xc3\xa8ne", "oxyg\u00e8ne", "ISO-8859-1"], // UTF-8 overrides
+ ["oxyg\xc3\xa8ne", "oxyg\u00e8ne"], // default to UTF-8 if no charset
+ ["oxyg\xe8ne", "oxyg\ufffdne", "UTF-8"],
+ ["oxyg\xe8ne", "oxyg\u00e8ne", "ISO-8859-1"],
+ ["\xc3\xa8\xe8", "\u00e8\ufffd", "UTF-8"],
+ ["\xc3\xa8\xe8", "\u00c3\u00a8\u00e8", "ISO-8859-1"],
+
+ // Don't fallback to UTF-16 or UTF-32
+ ["\xe8S!0", "\ufffdS!0", "UTF-16"],
+ ["\xe8S!0", "\ufffdS!0", "UTF-16be"],
+ ["\xe8S!0", "\ufffdS!0", "UTF-32"],
+ ["\xe8S!0", "\ufffdS!0", "utf-32"],
+
+ // Don't combine encoded-word and header charset decoding
+ ["=?UTF-8?Q?=c3?= \xa8", "\ufffd \ufffd", "UTF-8"],
+ ["=?UTF-8?Q?=c3?= \xa8", "\ufffd \u00a8", "ISO-8859-1"],
+ ["\xc3 =?UTF-8?Q?=a8?=", "\ufffd \ufffd", "UTF-8"],
+ ["\xc3 =?UTF-8?Q?=a8?=", "\u00c3 \ufffd", "ISO-8859-1"],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.deepEqual(
+ headerparser.decodeRFC2047Words(
+ headerparser.convert8BitHeader(
+ data[0],
+ data.length > 2 ? data[2] : null
+ )
+ ),
+ data[1]
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_header_emitter.js b/comm/mailnews/mime/jsmime/test/unit/test_header_emitter.js
new file mode 100644
index 0000000000..d9a7ecd5aa
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_header_emitter.js
@@ -0,0 +1,519 @@
+"use strict";
+define(function (require) {
+ var assert = require("assert");
+ var jsmime = require("jsmime");
+ var headeremitter = jsmime.headeremitter;
+ var MockDate = require("test/mock_date");
+
+ function arrayTest(data, fn) {
+ fn.toString = function () {
+ let text = Function.prototype.toString.call(this);
+ text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
+ return JSON.stringify(data[p]);
+ });
+ return text;
+ };
+ return test(JSON.stringify(data[0]), fn);
+ }
+
+ suite("headeremitter", function () {
+ suite("addAddresses", function () {
+ let handler = {
+ reset(expected) {
+ this.output = "";
+ this.expected = expected;
+ },
+ deliverData(data) {
+ this.output += data;
+ },
+ deliverEOF() {
+ assert.equal(this.output, this.expected + "\r\n");
+ for (let line of this.output.split("\r\n")) {
+ assert.ok(line.length <= 30, "Line is too long");
+ }
+ },
+ };
+ let header_tests = [
+ [[{ name: "", email: "" }], ""],
+ [[{ name: "", email: "a@example.com" }], "a@example.com"],
+ [
+ [{ name: "John Doe", email: "a@example.com" }],
+ "John Doe <a@example.com>",
+ ],
+ [
+ [
+ { name: "", email: "a@b.c" },
+ { name: "", email: "b@b.c" },
+ ],
+ "a@b.c, b@b.c",
+ ],
+ [
+ [
+ { name: "JD", email: "a@a.c" },
+ { name: "SD", email: "b@b.c" },
+ ],
+ "JD <a@a.c>, SD <b@b.c>",
+ ],
+ [
+ [
+ { name: "John Doe", email: "a@example.com" },
+ { name: "Sally Doe", email: "b@example.com" },
+ ],
+ "John Doe <a@example.com>,\r\n Sally Doe <b@example.com>",
+ ],
+ [
+ [
+ {
+ name: "My name is really long and I split somewhere",
+ email: "a@a.c",
+ },
+ ],
+ "My name is really long and I\r\n split somewhere <a@a.c>",
+ ],
+ // Note that the name is 29 chars here, so adding the email needs a break.
+ [
+ [{ name: "My name is really really long", email: "a@a.c" }],
+ "My name is really really long\r\n <a@a.c>",
+ ],
+ [
+ [
+ { name: "", email: "a@a.c" },
+ { name: "This name is long", email: "b@b.c" },
+ ],
+ "a@a.c,\r\n This name is long <b@b.c>",
+ ],
+ [
+ [
+ { name: "", email: "a@a.c" },
+ { name: "This name is also long", email: "b@b.c" },
+ ],
+ "a@a.c,\r\n This name is also long\r\n <b@b.c>",
+ ],
+ [[{ name: "", email: "hi!bad@all.com" }], '"hi!bad"@all.com'],
+ [[{ name: "", email: '"hi!bad"@all.com' }], '"hi!bad"@all.com'],
+ [[{ name: "Doe, John", email: "a@a.com" }], '"Doe, John" <a@a.com>'],
+ // This one violates the line length, so it underquotes instead.
+ [
+ [
+ {
+ name: "A really, really long name to quote",
+ email: "a@example.com",
+ },
+ ],
+ 'A "really," really long name\r\n to quote <a@example.com>',
+ ],
+ [
+ [
+ {
+ name: "Group",
+ group: [
+ { name: "", email: "a@a.c" },
+ { name: "", email: "b@b.c" },
+ ],
+ },
+ ],
+ "Group: a@a.c, b@b.c;",
+ ],
+ [[{ name: "No email address", email: "" }], "No email address"],
+ [
+ [{ name: "]user[ domain", email: "user@d.com" }],
+ '"]user[ domain" <user@d.com>',
+ ],
+ [
+ [
+ {
+ name: "Group",
+ group: [
+ { name: "]u[ d", email: "a@a.c" },
+ { name: "]u[ c", email: "b@b.c" },
+ ],
+ },
+ ],
+ 'Group: "]u[ d" <a@a.c>,\r\n "]u[ c" <b@b.c>;',
+ ],
+ [
+ [{ name: "user@domain", email: "user@d.com" }],
+ '"user@domain" <user@d.com>',
+ ],
+ [
+ [
+ {
+ name: "Group",
+ group: [
+ { name: "u@d", email: "a@a.c" },
+ { name: "u@c", email: "b@b.c" },
+ ],
+ },
+ ],
+ 'Group: "u@d" <a@a.c>,\r\n "u@c" <b@b.c>;',
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {
+ softMargin: 30,
+ useASCII: false,
+ });
+ handler.reset(data[1]);
+ emitter.addAddresses(data[0]);
+ emitter.finish(true);
+ });
+ });
+ });
+ suite("addAddresses (RFC 2047)", function () {
+ let handler = {
+ reset(expected) {
+ this.output = "";
+ this.expected = expected;
+ },
+ deliverData(data) {
+ this.output += data;
+ },
+ deliverEOF() {
+ assert.equal(this.output, this.expected + "\r\n");
+ for (let line of this.output.split("\r\n")) {
+ assert.ok(line.length <= 30, "Line is too long");
+ }
+ },
+ };
+ let header_tests = [
+ [[{ name: "\u0436", email: "a@a.c" }], "=?UTF-8?B?0LY=?= <a@a.c>"],
+ [
+ [{ name: "dioxyg\u00e8ne", email: "a@a.c" }],
+ "=?UTF-8?Q?dioxyg=C3=A8ne?=\r\n <a@a.c>",
+ ],
+ // Prefer QP if base64 and QP are exactly the same length
+ [
+ [{ name: "oxyg\u00e8ne", email: "a@a.c" }],
+ // =?UTF-8?B?b3h5Z8OobmU=?=
+ "=?UTF-8?Q?oxyg=C3=A8ne?=\r\n <a@a.c>",
+ ],
+ [
+ [
+ {
+ name: "\ud83d\udca9\ud83d\udca9\ud83d\udca9\ud83d\udca9",
+ email: "a@a.c",
+ },
+ ],
+ "=?UTF-8?B?8J+SqfCfkqnwn5Kp?=\r\n =?UTF-8?B?8J+SqQ==?= <a@a.c>",
+ ],
+ // Bug 1088975: Since the encoded-word should be recognized as an atom,
+ // encode commas.
+ [
+ [{ name: "B\u00fcg 1088975, FirstName", email: "a@b.c" }],
+ "=?UTF-8?Q?B=C3=BCg_1088975?=\r\n" +
+ " =?UTF-8?Q?=2C_FirstName?=\r\n <a@b.c>",
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {
+ softMargin: 30,
+ useASCII: true,
+ });
+ handler.reset(data[1]);
+ emitter.addAddresses(data[0]);
+ emitter.finish(true);
+ });
+ });
+ });
+ suite("addUnstructured (RFC 2047)", function () {
+ let handler = {
+ reset(expected) {
+ this.output = "";
+ this.expected = expected;
+ },
+ deliverData(data) {
+ this.output += data;
+ },
+ deliverEOF() {
+ assert.equal(this.output, this.expected + "\r\n");
+ for (let line of this.output.split("\r\n")) {
+ assert.ok(line.length <= 30, "Line is too long");
+ }
+ },
+ };
+ let header_tests = [
+ ["My house burned down!", "My house burned down!"],
+
+ // Which of the 32 "special" characters need to be encoded in QP encoding?
+ // Note: Encoding is forced by adding a \x7f at the end.
+ // These 5 don't need encoding:
+ [" ! * + - / \x7f", "=?UTF-8?Q?_!_*_+_-_/_=7F?="],
+
+ // Bug 1438590: RFC2047 [5. (3)] requests the
+ // encoding of these 27 "special" characters:
+ // " # $ % & ' ( ) , . : ; < = > ? @ [ \ ] ^ _ ` { | } ~.
+ // Note: If there are enough characters for padding,
+ // QP is used and not base64.
+ ['Test " # \x7f', "=?UTF-8?Q?Test_=22_=23_=7F?="],
+ ["Test $ % \x7f", "=?UTF-8?Q?Test_=24_=25_=7F?="],
+ ["Test & ' \x7f", "=?UTF-8?Q?Test_=26_=27_=7F?="],
+ ["Test ( ) \x7f", "=?UTF-8?Q?Test_=28_=29_=7F?="],
+ ["Test , . \x7f", "=?UTF-8?Q?Test_=2C_=2E_=7F?="],
+ ["Test : ; \x7f", "=?UTF-8?Q?Test_=3A_=3B_=7F?="],
+ ["Test < = \x7f", "=?UTF-8?Q?Test_=3C_=3D_=7F?="],
+ ["Test > ? \x7f", "=?UTF-8?Q?Test_=3E_=3F_=7F?="],
+ ["Test @ [ \x7f", "=?UTF-8?Q?Test_=40_=5B_=7F?="],
+ ["Test \\ ] \x7f", "=?UTF-8?Q?Test_=5C_=5D_=7F?="],
+ ["Test ^ _ \x7f", "=?UTF-8?Q?Test_=5E_=5F_=7F?="],
+ ["Test ` { \x7f", "=?UTF-8?Q?Test_=60_=7B_=7F?="],
+ ["Test | } \x7f", "=?UTF-8?Q?Test_=7C_=7D_=7F?="],
+ ["Test ~ \x7f", "=?UTF-8?Q?Test_=7E_=7F?="],
+
+ // But the 32 printable "special" characters don't need it in the first place!
+ [
+ "! \" # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \\ ] ^ _ ` { | } ~",
+ "! \" # $ % & ' ( ) * + , - . /\r\n" +
+ " : ; < = > ? @ [ \\ ] ^ _ ` { |\r\n" +
+ " } ~",
+ ],
+
+ // Test to make sure 2047-encoding chooses the right values.
+ ["\u001f", "=?UTF-8?Q?=1F?="],
+ ["\u001fa", "=?UTF-8?Q?=1Fa?="],
+ ["\u001faa", "=?UTF-8?B?H2Fh?="],
+ ["\u001faaa", "=?UTF-8?Q?=1Faaa?="],
+ ["\u001faaa\u001f", "=?UTF-8?B?H2FhYR8=?="],
+ ["\u001faaa\u001fa", "=?UTF-8?B?H2FhYR9h?="],
+ ["\u001faaa\u001faa", "=?UTF-8?Q?=1Faaa=1Faa?="],
+ ["\u001faaa\u001faa\u001faaaa", "=?UTF-8?B?H2FhYR9hYR9hYWFh?="],
+
+ // Make sure line breaking works right at the edge cases
+ ["\u001faaa\u001faaaaaaaaa", "=?UTF-8?Q?=1Faaa=1Faaaaaaaaa?="],
+ [
+ "\u001faaa\u001faaaaaaaaaa",
+ "=?UTF-8?Q?=1Faaa=1Faaaaaaaaa?=\r\n =?UTF-8?Q?a?=",
+ ],
+
+ // Choose base64/qp independently for each word
+ [
+ "\ud83d\udca9\ud83d\udca9\ud83d\udca9a",
+ "=?UTF-8?B?8J+SqfCfkqnwn5Kp?=\r\n =?UTF-8?Q?a?=",
+ ],
+
+ // Don't split a surrogate character!
+ [
+ "a\ud83d\udca9\ud83d\udca9\ud83d\udca9a",
+ "=?UTF-8?B?YfCfkqnwn5Kp?=\r\n =?UTF-8?B?8J+SqWE=?=",
+ ],
+
+ // Spacing a UTF-8 string
+ [
+ "L'oxyg\u00e8ne est un \u00e9l\u00e9ment chimique du groupe des " +
+ "chalcog\u00e8nes",
+ // 1 2 3
+ // 123456789012345678901234567890
+ "=?UTF-8?B?TCdveHlnw6huZSBl?=\r\n" +
+ " =?UTF-8?B?c3QgdW4gw6lsw6lt?=\r\n" +
+ " =?UTF-8?Q?ent_chimique_du_g?=\r\n" +
+ " =?UTF-8?Q?roupe_des_chalcog?=\r\n" +
+ " =?UTF-8?B?w6huZXM=?=",
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {
+ softMargin: 30,
+ useASCII: true,
+ });
+ handler.reset(data[1]);
+ emitter.addUnstructured(data[0]);
+ emitter.finish(true);
+ });
+ });
+ });
+ suite("addDate", function () {
+ let handler = {
+ reset(expected) {
+ this.output = "";
+ this.expected = expected;
+ },
+ deliverData(data) {
+ this.output += data;
+ },
+ deliverEOF() {
+ assert.equal(this.output, this.expected + "\r\n");
+ },
+ };
+ let header_tests = [
+ // Test basic day/month names
+ ["2000-01-01T00:00:00Z", "Sat, 1 Jan 2000 00:00:00 +0000"],
+ ["2000-02-01T00:00:00Z", "Tue, 1 Feb 2000 00:00:00 +0000"],
+ ["2000-03-01T00:00:00Z", "Wed, 1 Mar 2000 00:00:00 +0000"],
+ ["2000-04-01T00:00:00Z", "Sat, 1 Apr 2000 00:00:00 +0000"],
+ ["2000-05-01T00:00:00Z", "Mon, 1 May 2000 00:00:00 +0000"],
+ ["2000-06-01T00:00:00Z", "Thu, 1 Jun 2000 00:00:00 +0000"],
+ ["2000-07-01T00:00:00Z", "Sat, 1 Jul 2000 00:00:00 +0000"],
+ ["2000-08-01T00:00:00Z", "Tue, 1 Aug 2000 00:00:00 +0000"],
+ ["2000-09-01T00:00:00Z", "Fri, 1 Sep 2000 00:00:00 +0000"],
+ ["2000-10-01T00:00:00Z", "Sun, 1 Oct 2000 00:00:00 +0000"],
+ ["2000-11-01T00:00:00Z", "Wed, 1 Nov 2000 00:00:00 +0000"],
+ ["2000-12-01T00:00:00Z", "Fri, 1 Dec 2000 00:00:00 +0000"],
+
+ // Test timezone offsets
+ ["2000-06-01T12:00:00Z", "Thu, 1 Jun 2000 12:00:00 +0000"],
+ ["2000-06-01T12:00:00+0100", "Thu, 1 Jun 2000 12:00:00 +0100"],
+ ["2000-06-01T12:00:00+0130", "Thu, 1 Jun 2000 12:00:00 +0130"],
+ ["2000-06-01T12:00:00-0100", "Thu, 1 Jun 2000 12:00:00 -0100"],
+ ["2000-06-01T12:00:00-0130", "Thu, 1 Jun 2000 12:00:00 -0130"],
+ ["2000-06-01T12:00:00+1345", "Thu, 1 Jun 2000 12:00:00 +1345"],
+ ["2000-06-01T12:00:00-1200", "Thu, 1 Jun 2000 12:00:00 -1200"],
+ ["2000-06-01T12:00:00+1337", "Thu, 1 Jun 2000 12:00:00 +1337"],
+ ["2000-06-01T12:00:00+0101", "Thu, 1 Jun 2000 12:00:00 +0101"],
+ ["2000-06-01T12:00:00-1337", "Thu, 1 Jun 2000 12:00:00 -1337"],
+
+ // Try some varying hour, minute, and second amounts, to double-check
+ // padding and time dates.
+ ["2000-06-01T01:02:03Z", "Thu, 1 Jun 2000 01:02:03 +0000"],
+ ["2000-06-01T23:13:17Z", "Thu, 1 Jun 2000 23:13:17 +0000"],
+ ["2000-06-01T00:05:04Z", "Thu, 1 Jun 2000 00:05:04 +0000"],
+ ["2000-06-01T23:59:59Z", "Thu, 1 Jun 2000 23:59:59 +0000"],
+ ["2000-06-01T13:17:40Z", "Thu, 1 Jun 2000 13:17:40 +0000"],
+ ["2000-06-01T11:15:34Z", "Thu, 1 Jun 2000 11:15:34 +0000"],
+ ["2000-06-01T04:09:09Z", "Thu, 1 Jun 2000 04:09:09 +0000"],
+ ["2000-06-01T04:10:10Z", "Thu, 1 Jun 2000 04:10:10 +0000"],
+ ["2000-06-01T09:13:17Z", "Thu, 1 Jun 2000 09:13:17 +0000"],
+ ["2000-06-01T13:12:14Z", "Thu, 1 Jun 2000 13:12:14 +0000"],
+ ["2000-06-01T14:16:48Z", "Thu, 1 Jun 2000 14:16:48 +0000"],
+
+ // Try varying month, date, and year values.
+ ["2000-01-31T00:00:00Z", "Mon, 31 Jan 2000 00:00:00 +0000"],
+ ["2000-02-28T00:00:00Z", "Mon, 28 Feb 2000 00:00:00 +0000"],
+ ["2000-02-29T00:00:00Z", "Tue, 29 Feb 2000 00:00:00 +0000"],
+ ["2001-02-28T00:00:00Z", "Wed, 28 Feb 2001 00:00:00 +0000"],
+ ["2000-03-31T00:00:00Z", "Fri, 31 Mar 2000 00:00:00 +0000"],
+ ["2000-04-30T00:00:00Z", "Sun, 30 Apr 2000 00:00:00 +0000"],
+ ["2000-05-31T00:00:00Z", "Wed, 31 May 2000 00:00:00 +0000"],
+ ["2000-06-30T00:00:00Z", "Fri, 30 Jun 2000 00:00:00 +0000"],
+ ["2000-07-31T00:00:00Z", "Mon, 31 Jul 2000 00:00:00 +0000"],
+ ["2000-08-31T00:00:00Z", "Thu, 31 Aug 2000 00:00:00 +0000"],
+ ["2000-09-30T00:00:00Z", "Sat, 30 Sep 2000 00:00:00 +0000"],
+ ["2000-10-31T00:00:00Z", "Tue, 31 Oct 2000 00:00:00 +0000"],
+ ["2000-11-30T00:00:00Z", "Thu, 30 Nov 2000 00:00:00 +0000"],
+ ["2000-12-31T00:00:00Z", "Sun, 31 Dec 2000 00:00:00 +0000"],
+ ["1900-01-01T00:00:00Z", "Mon, 1 Jan 1900 00:00:00 +0000"],
+ ["9999-12-31T23:59:59Z", "Fri, 31 Dec 9999 23:59:59 +0000"],
+
+ // Tests that are not actually missing:
+ // We don't actually need to test daylight savings time issues, so long as
+ // getTimezoneOffset is correct. We've confirmed black-box that the value
+ // is being directly queried on every instance, since we have tests that
+ // make MockDate.getTimezoneOffset return different values.
+ // In addition, ES6 Date objects don't support leap seconds. Invalid dates
+ // per RFC 5322 are handled in a later run of code.
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {});
+ handler.reset(data[1]);
+ emitter.addDate(new MockDate(data[0]));
+ emitter.finish(true);
+ });
+ });
+
+ // An invalid date should throw an error instead of make a malformed header.
+ test("Invalid dates", function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {});
+ assert.throws(function () {
+ emitter.addDate(new Date(NaN));
+ }, /Cannot encode an invalid date/);
+ assert.throws(function () {
+ emitter.addDate(new Date("1850-01-01"));
+ }, /Date year is out of encodable range/);
+ assert.throws(function () {
+ emitter.addDate(new Date("10000-01-01"));
+ }, /Cannot encode an invalid date/);
+ });
+
+ // Test preferred breaking for the date header.
+ test("Break spot", function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {
+ softMargin: 30,
+ });
+ handler.reset("Overly-Long-Date:\r\n Sat, 1 Jan 2000 00:00:00 +0000");
+ emitter.addHeaderName("Overly-Long-Date");
+ emitter.addDate(new MockDate("2000-01-01T00:00:00Z"));
+ emitter.finish();
+ });
+
+ test("Correctness of date", function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {});
+ handler.reset();
+ let now = new Date();
+ emitter.addDate(now);
+ emitter.finish();
+ // All engines can parse the date strings we produce
+ let reparsed = new Date(handler.output);
+
+ // Now and reparsed should be correct to second-level precision.
+ assert.equal(reparsed.getMilliseconds(), 0);
+ assert.equal(now.getTime() - now.getMilliseconds(), reparsed.getTime());
+ });
+ });
+
+ suite("Header lengths", function () {
+ let handler = {
+ reset(expected) {
+ this.output = "";
+ this.expected = expected;
+ },
+ deliverData(data) {
+ this.output += data;
+ },
+ deliverEOF() {
+ assert.equal(this.output, this.expected + "\r\n");
+ },
+ };
+ let header_tests = [
+ [
+ [{ name: "Supercalifragilisticexpialidocious", email: "a@b.c" }],
+ "Supercalifragilisticexpialidocious\r\n <a@b.c>",
+ ],
+ [
+ [
+ {
+ email:
+ "supercalifragilisticexpialidocious@" +
+ "the.longest.domain.name.in.the.world.invalid",
+ },
+ ],
+ "supercalifragilisticexpialidocious\r\n" +
+ " @the.longest.domain.name.in.the.world.invalid",
+ ],
+ [
+ [
+ {
+ name:
+ "Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphi" +
+ "paraomelitokatakechymenokichlepikossyphophattoperisteralektryonoptek" +
+ "ephalliokigklopeleiolagoiosiraiobaphetraganopterygon",
+ email: "a@b.c",
+ },
+ ],
+ new Error(),
+ ],
+ ];
+ header_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ let emitter = headeremitter.makeStreamingEmitter(handler, {
+ softMargin: 30,
+ hardMargin: 50,
+ useASCII: false,
+ });
+ handler.reset(data[1]);
+ if (data[1] instanceof Error) {
+ assert.throws(function () {
+ emitter.addAddresses(data[0]);
+ }, /Cannot encode/);
+ } else {
+ assert.doesNotThrow(function () {
+ emitter.addAddresses(data[0]);
+ });
+ emitter.finish(true);
+ }
+ });
+ });
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_mime_tree.js b/comm/mailnews/mime/jsmime/test/unit/test_mime_tree.js
new file mode 100644
index 0000000000..c1d8647b25
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_mime_tree.js
@@ -0,0 +1,838 @@
+"use strict";
+define(function (require) {
+ var assert = require("assert");
+ var jsmime = require("jsmime");
+ var fs = require("fs");
+
+ function arrayTest(data, fn) {
+ fn.toString = function () {
+ let text = Function.prototype.toString.call(this);
+ text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
+ return JSON.stringify(data[p]);
+ });
+ return text;
+ };
+ return test(data[0], fn);
+ }
+
+ // Returns and deletes object[field] if present, or undefined if not.
+ function extract_field(object, field) {
+ if (field in object) {
+ var result = object[field];
+ delete object[field];
+ return result;
+ }
+ return undefined;
+ }
+
+ // A file cache for read_file.
+ var file_cache = {};
+
+ /**
+ * Read a file into a string (all line endings become CRLF).
+ *
+ * @param file The name of the file to read, relative to the data/ directory.
+ * @param start The first line of the file to return, defaulting to 0
+ * @param end The last line of the file to return, defaulting to the number of
+ * lines in the file.
+ * @returns Promise<String> The contents of the file as a binary string.
+ */
+ function read_file(file, start, end) {
+ if (!(file in file_cache)) {
+ var realFile = new Promise(function (resolve, reject) {
+ fs.readFile("data/" + file, function (err, data) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ var loader = realFile.then(function (contents) {
+ var inStrForm = "";
+ while (contents.length > 0) {
+ inStrForm += String.fromCharCode.apply(
+ null,
+ contents.subarray(0, 1024)
+ );
+ contents = contents.subarray(1024);
+ }
+ return inStrForm.split(/\r\n|[\r\n]/);
+ });
+ file_cache[file] = loader;
+ }
+ return file_cache[file].then(function (contents) {
+ if (start !== undefined) {
+ contents = contents.slice(start - 1, end - 1);
+ }
+ return contents.join("\r\n");
+ });
+ }
+
+ /**
+ * Helper for body tests.
+ *
+ * Some extra options are listed too:
+ * _split: The contents of the file will be passed in packets split by this
+ * regex. Be sure to include the split delimiter in a group so that they
+ * are included in the output packets!
+ * _eol: The CRLFs in the input file will be replaced with the given line
+ * ending instead.
+ *
+ * @param test The name of test
+ * @param file The name of the file to read (relative to mailnews/data)
+ * @param opts Options for the mime parser, as well as a few extras detailed
+ * above.
+ * @param partspec An array of [partnum, line start, line end] detailing the
+ * expected parts in the body. It will be expected that the
+ * accumulated body part data for partnum would be the contents
+ * of the file from [line start, line end) [1-based lines]
+ */
+ function make_body_test(test, file, opts, partspec) {
+ var results = Promise.all(
+ partspec.map(p => Promise.all([p[0], read_file(file, p[1], p[2])]))
+ );
+ var eol = extract_field(opts, "_eol");
+ var msgtext = read_file(file).then(function (msgcontents) {
+ var packetize = extract_field(opts, "_split");
+ if (packetize !== undefined) {
+ msgcontents = msgcontents.split(packetize);
+ }
+ if (eol !== undefined) {
+ msgcontents = msgcontents.replace(/\r\n/g, eol);
+ }
+ return msgcontents;
+ });
+ if (eol !== undefined) {
+ results = results.then(function (results_) {
+ for (let part of results_) {
+ part[1] = part[1].replace(/\r\n/g, eol);
+ }
+ return results_;
+ });
+ }
+ return [test, msgtext, opts, results];
+ }
+
+ /**
+ * Execute a single MIME tree test.
+ *
+ * @param message Either the text of the message, an array of textual message
+ * part data (imagine coming on different TCP packets), or a
+ * promise that resolves to any of the above.
+ * @param opts A set of options for the parser and for the test.
+ * @param results The expected results of the call. This may either be a
+ * dictionary of part number -> header -> values (to check
+ * headers), or an array of [partnum, partdata] for expected
+ * results to deliverPartData, or a promise for the above.
+ * @returns A promise containing the results of the test.
+ */
+ function testParser(message, opts, results) {
+ var uncheckedValues;
+ var checkingHeaders;
+ var calls = 0;
+ var fusingParts = extract_field(opts, "_nofuseparts") === undefined;
+ var emitter = {
+ stack: [],
+ startMessage: function emitter_startMsg() {
+ assert.equal(this.stack.length, 0);
+ calls++;
+ this.partData = "";
+ },
+ endMessage: function emitter_endMsg() {
+ assert.equal(this.stack.length, 0);
+ calls++;
+ },
+ startPart: function emitter_startPart(partNum, headers) {
+ this.stack.push(partNum);
+ if (checkingHeaders) {
+ assert.ok(partNum in uncheckedValues);
+ // Headers is a map, convert it to an object.
+ var objmap = {};
+ for (let pair of headers) {
+ objmap[pair[0]] = pair[1];
+ }
+ var expected = uncheckedValues[partNum];
+ var convresults = {};
+ for (let key in expected) {
+ try {
+ convresults[key] = jsmime.headerparser.parseStructuredHeader(
+ key,
+ expected[key]
+ );
+ } catch (e) {
+ convresults[key] = expected[key];
+ }
+ }
+ assert.deepEqual(objmap, convresults);
+ if (fusingParts) {
+ assert.equal(this.partData, "");
+ }
+ delete uncheckedValues[partNum];
+ }
+ },
+ deliverPartData: function emitter_partData(partNum, data) {
+ assert.equal(this.stack[this.stack.length - 1], partNum);
+ if (!checkingHeaders) {
+ if (fusingParts) {
+ this.partData += data;
+ } else {
+ let check = uncheckedValues.shift();
+ assert.equal(partNum, check[0]);
+ assert.equal(data, check[1]);
+ }
+ }
+ },
+ endPart: function emitter_endPart(partNum) {
+ if (this.partData != "") {
+ let check = uncheckedValues.shift();
+ assert.equal(partNum, check[0]);
+ assert.equal(this.partData, check[1]);
+ this.partData = "";
+ }
+ assert.equal(this.stack.pop(), partNum);
+ },
+ };
+ opts.onerror = function (e) {
+ throw e;
+ };
+
+ return Promise.all([message, results]).then(function (vals) {
+ let [message_, results_] = vals;
+ // Clone the results array into uncheckedValues
+ if (Array.isArray(results_)) {
+ uncheckedValues = Array.from(results_);
+ checkingHeaders = false;
+ } else {
+ uncheckedValues = {};
+ for (let key in results_) {
+ uncheckedValues[key] = results_[key];
+ }
+ checkingHeaders = true;
+ }
+ if (!Array.isArray(message_)) {
+ message_ = [message_];
+ }
+ var parser = new jsmime.MimeParser(emitter, opts);
+ message_.forEach(function (packet) {
+ parser.deliverData(packet);
+ });
+ parser.deliverEOF();
+ assert.equal(calls, 2);
+ if (!checkingHeaders) {
+ assert.equal(0, uncheckedValues.length);
+ } else {
+ assert.deepEqual({}, uncheckedValues);
+ }
+ });
+ }
+
+ suite("MimeParser", function () {
+ // This is the expected part specifier for the multipart-complex1 test file,
+ // specified here because it is used in several cases.
+ let mpart_complex1 = [
+ ["1", 8, 10],
+ ["2", 14, 16],
+ ["3.1", 22, 24],
+ ["4", 29, 31],
+ ["5", 33, 35],
+ ];
+
+ suite("Simple tests", function () {
+ let parser_tests = [
+ // The following tests are either degenerate or error cases that should
+ // work
+ ["Empty string", "", {}, { "": {} }],
+ ["No value for header", "Header", {}, { "": { Header: [""] } }],
+ [
+ "No trailing newline",
+ "To: eof@example.net",
+ {},
+ { "": { To: ["eof@example.net"] } },
+ ],
+ [
+ "Header no val",
+ "To: eof@example.net\r\n",
+ {},
+ { "": { To: ["eof@example.net"] } },
+ ],
+ ["No body no headers", "\r\n\r\n", {}, { "": {} }],
+ ["Body no headers", "\r\n\r\nA", {}, { "": {} }],
+ // Basic cases for headers
+ [
+ "Multiparts get headers",
+ read_file("multipart-complex1"),
+ {},
+ {
+ "": { "Content-Type": ['multipart/mixed; boundary="boundary"'] },
+ 1: {
+ "Content-Type": ["application/octet-stream"],
+ "Content-Transfer-Encoding": ["base64"],
+ },
+ 2: {
+ "Content-Type": ["image/png"],
+ "Content-Transfer-Encoding": ["base64"],
+ },
+ 3: {
+ "Content-Type": ['multipart/related; boundary="boundary2"'],
+ },
+ 3.1: { "Content-Type": ["text/html"] },
+ 4: { "Content-Type": ["text/plain"] },
+ 5: {},
+ },
+ ],
+ ];
+ parser_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ return testParser(data[1], data[2], data[3]);
+ });
+ });
+ });
+
+ suite("Body tests", function () {
+ let parser_tests = [
+ // Body tests from data
+ // (Note: line numbers are 1-based. Also, to capture trailing EOF, add 2
+ // to the last line number of the file).
+ make_body_test("Basic body", "basic1", {}, [["", 3, 5]]),
+ make_body_test("Basic multipart", "multipart1", {}, [["1", 10, 12]]),
+ make_body_test("Basic multipart", "multipart2", {}, [["1", 8, 11]]),
+ make_body_test(
+ "Complex multipart",
+ "multipart-complex1",
+ {},
+ mpart_complex1
+ ),
+ make_body_test("Truncated multipart", "multipart-complex2", {}, [
+ ["1.1.1.1", 21, 25],
+ ["2", 27, 57],
+ ["3", 60, 62],
+ ]),
+ make_body_test("No LF multipart", "multipartmalt-detach", {}, [
+ ["1", 20, 21],
+ ["2.1", 27, 38],
+ ["2.2", 42, 43],
+ ["2.3", 47, 48],
+ ["3", 53, 54],
+ ]),
+ make_body_test("Raw body", "multipart1", { bodyformat: "raw" }, [
+ ["", 4, 14],
+ ]),
+ [
+ "Base64 decode 1",
+ read_file("base64-1"),
+ { bodyformat: "decode" },
+ [
+ [
+ "",
+ "\r\nHello, world! (Again...)\r\n\r\nLet's see how well base64 " +
+ "text is handled. Yay, lots of space" +
+ "s! There's even a CRLF at the end and one at the beginning, bu" +
+ "t the output shouldn't have it.\r\n",
+ ],
+ ],
+ ],
+ [
+ "Base64 decode 2",
+ read_file("base64-2"),
+ { bodyformat: "decode" },
+ [
+ [
+ "",
+ "<html><body>This is base64 encoded HTML text, and the tags sho" +
+ "uldn't be stripped.\r\n<b>Bold text is bold!</b></body></html>" +
+ "\r\n",
+ ],
+ ],
+ ],
+ [
+ "Base64 decode line issues",
+ read_file("base64-2").then(function (s) {
+ return s.split(/(\r\n)/);
+ }),
+ { bodyformat: "decode" },
+ [
+ [
+ "",
+ "<html><body>This is base64 encoded HTML text, and the tags sho" +
+ "uldn't be stripped.\r\n<b>Bold text is bold!</b></body></html>" +
+ "\r\n",
+ ],
+ ],
+ ],
+ make_body_test("Base64 nodecode", "base64-1", {}, [["", 4, 9]]),
+ [
+ "QP decode",
+ read_file("bug505221"),
+ { pruneat: "1", bodyformat: "decode" },
+ [
+ [
+ "1",
+ '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"' +
+ '>\r\n<HTML><HEAD>\r\n<META HTTP-EQUIV="Content-Type" CONTENT=' +
+ '"text/html; charset=us-ascii">\r\n\r\n\r\n<META content="MSHT' +
+ 'ML 6.00.6000.16735" name=GENERATOR></HEAD>\r\n<BODY> bbb\r\n<' +
+ "/BODY></HTML>",
+ ],
+ ],
+ ],
+ [
+ "Nested messages",
+ read_file("message-encoded"),
+ { bodyformat: "decode" },
+ [
+ ["1$", "This is a plain-text message."],
+ ["2$", "I am a plain-text message."],
+ ["3$", "I am an encoded plain-text message."],
+ ],
+ ],
+ [
+ "Nested message headers",
+ read_file("message-encoded"),
+ {},
+ {
+ "": {
+ "Content-Type": ['multipart/mixed; boundary="iamaboundary"'],
+ },
+ 1: { "Content-Type": ["message/rfc822"] },
+ "1$": { Subject: ["I am a subject"] },
+ 2: {
+ "Content-Type": ["message/global"],
+ "Content-Transfer-Encoding": ["base64"],
+ },
+ "2$": { Subject: ["\u79c1\u306f\u3001\u4ef6\u540d\u5348\u524d"] },
+ 3: {
+ "Content-Type": ["message/news"],
+ "Content-Transfer-Encoding": ["quoted-printable"],
+ },
+ "3$": { Subject: ["\u79c1\u306f\u3001\u4ef6\u540d\u5348\u524d"] },
+ },
+ ],
+ ];
+ parser_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ return testParser(data[1], data[2], data[3]);
+ });
+ });
+ });
+
+ suite("Torture tests", function () {
+ // Generate a very long message for tests
+ let teststr = "a";
+ for (let i = 0; i < 16; i++) {
+ teststr += teststr;
+ }
+ let parser_tests = [
+ [
+ "Base64 very long decode",
+ "Content-Transfer-Encoding: base64\r\n\r\n" + btoa(teststr) + "\r\n",
+ { bodyformat: "decode" },
+ [["", teststr]],
+ ],
+ make_body_test("Torture regular body", "mime-torture", {}, [
+ ["1", 17, 21],
+ ["2$.1", 58, 75],
+ ["2$.2.1", 83, 97],
+ ["2$.3", 102, 130],
+ ["3$", 155, 7742],
+ ["4", 7747, 8213],
+ ["5", 8218, 8242],
+ ["6$.1.1", 8284, 8301],
+ ["6$.1.2", 8306, 8733],
+ ["6$.2.1", 8742, 9095],
+ ["6$.2.2", 9100, 9354],
+ ["6$.2.3", 9357, 11794],
+ ["6$.2.4", 11797, 12155],
+ ["6$.3", 12161, 12809],
+ ["7$.1", 12844, 12845],
+ ["7$.2", 12852, 13286],
+ ["7$.3", 13288, 13297],
+ ["8$.1", 13331, 13358],
+ ["8$.2", 13364, 13734],
+ ["9$", 13757, 20179],
+ ["10", 20184, 21200],
+ ["11$.1", 21223, 22031],
+ ["11$.2", 22036, 22586],
+ ["12$.1", 22607, 23469],
+ ["12$.2", 23474, 23774],
+ ["12$.3$.1", 23787, 23795],
+ ["12$.3$.2.1", 23803, 23820],
+ ["12$.3$.2.2", 23825, 24633],
+ ["12$.3$.3", 24640, 24836],
+ ["12$.3$.4$", 24848, 25872],
+ ]),
+ make_body_test("Torture pruneat", "mime-torture", { pruneat: "4" }, [
+ ["4", 7747, 8213],
+ ]),
+
+ // Test packetization problems
+ make_body_test(
+ "Large packets",
+ "multipart-complex1",
+ { _split: /(.{30})/ },
+ mpart_complex1
+ ),
+ make_body_test(
+ "Split on newline",
+ "multipart-complex1",
+ { _split: /(\r\n)/ },
+ mpart_complex1
+ ),
+ make_body_test(
+ "Pathological splitting",
+ "multipart-complex1",
+ { _split: "" },
+ mpart_complex1
+ ),
+
+ // Non-CLRF line endings?
+ make_body_test(
+ "LF-based messages",
+ "multipart-complex1",
+ { _eol: "\n" },
+ mpart_complex1
+ ),
+ make_body_test(
+ "CR-based messages",
+ "multipart-complex1",
+ { _eol: "\r" },
+ mpart_complex1
+ ),
+ ];
+ parser_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ return testParser(data[1], data[2], data[3]);
+ });
+ });
+ });
+
+ suite("Header tests", function () {
+ let parser_tests = [
+ // Basic cases for headers
+ [
+ "Multiparts get headers",
+ read_file("multipart-complex1"),
+ {},
+ {
+ "": { "Content-Type": ['multipart/mixed; boundary="boundary"'] },
+ 1: {
+ "Content-Type": ["application/octet-stream"],
+ "Content-Transfer-Encoding": ["base64"],
+ },
+ 2: {
+ "Content-Type": ["image/png"],
+ "Content-Transfer-Encoding": ["base64"],
+ },
+ 3: {
+ "Content-Type": ['multipart/related; boundary="boundary2"'],
+ },
+ 3.1: { "Content-Type": ["text/html"] },
+ 4: { "Content-Type": ["text/plain"] },
+ 5: {},
+ },
+ ],
+ // 'From ' is not an [iterable] header
+ [
+ "Exclude mbox delimiter",
+ read_file("bugmail11"),
+ {},
+ {
+ "": {
+ "X-Mozilla-Status": ["0001"],
+ "X-Mozilla-Status2": ["00000000"],
+ "X-Mozilla-Keys": [""],
+ "Return-Path": [
+ "<example@example.com>",
+ "<bugzilla-daemon@mozilla.org>",
+ ],
+ "Delivered-To": ["bugmail@example.org"],
+ Received: [
+ "by 10.114.166.12 with SMTP id o12cs163262wae;" +
+ " Fri, 11 Apr 2008 07:17:31 -0700 (PDT)",
+ "by 10.115.60.1 with SMTP id n1mr214763wak.181.1207923450166;" +
+ " Fri, 11 Apr 2008 07:17:30 -0700 (PDT)",
+ "from webapp-out.mozilla.org (webapp01.sj.mozilla.com [63.245.208.1" +
+ "46]) by mx.google.com with ESMTP id n38si6807242wag.2.2008." +
+ "04.11.07.17.29; Fri, 11 Apr 2008 07:17:30 -0700 (PDT)",
+ "from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])" +
+ "\tby webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m3BEHTGU" +
+ "030132\tfor <bugmail@example.org>; Fri, 11 Apr 2008 07:17:29 -0700",
+ "(from root@localhost)" +
+ "\tby mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m3BEHTk4030129;" +
+ "\tFri, 11 Apr 2008 07:17:29 -0700",
+ ],
+ "Received-Spf": [
+ "neutral (google.com: 63.245.208.146 is neither perm" +
+ "itted nor denied by best guess record for domain of bugzilla-daemo" +
+ "n@mozilla.org) client-ip=63.245.208.146;",
+ ],
+ "Authentication-Results": [
+ "mx.google.com; spf=neutral (google.com: 6" +
+ "3.245.208.146 is neither permitted nor denied by best guess record" +
+ " for domain of bugzilla-daemon@mozilla.org) smtp.mail=bugzilla-dae" +
+ "mon@mozilla.org",
+ ],
+ Date: ["Fri, 11 Apr 2008 07:17:29 -0700"],
+ "Message-ID": [
+ "<200804111417.m3BEHTk4030129@mrapp51.mozilla.org>",
+ ],
+ From: ["bugzilla-daemon@mozilla.org"],
+ To: ["bugmail@example.org"],
+ Subject: ["Bugzilla: confirm account creation"],
+ "X-Bugzilla-Type": ["admin"],
+ "Content-Type": ['text/plain; charset="UTF-8"'],
+ "MIME-Version": ["1.0"],
+ },
+ },
+ ],
+ ];
+ parser_tests.forEach(function (data) {
+ arrayTest(data, function () {
+ return testParser(data[1], data[2], data[3]);
+ });
+ });
+ });
+
+ suite("Charset tests", function () {
+ function buildTree(file, options) {
+ var tree = new Map();
+ var emitter = {
+ startPart(part, headers) {
+ tree.set(part, { headers, body: null });
+ },
+ deliverPartData(part, data) {
+ var obj = tree.get(part);
+ if (obj.body === null) {
+ obj.body = data;
+ } else if (typeof obj.body === "string") {
+ obj.body += data;
+ } else {
+ var newData = new Uint8Array(obj.body.length + data.length);
+ newData.set(obj.body);
+ newData.subarray(obj.body.length).set(data);
+ obj.body = newData;
+ }
+ },
+ };
+ return file.then(function (data) {
+ var parser = new jsmime.MimeParser(emitter, options);
+ parser.deliverData(data);
+ parser.deliverEOF();
+ return tree;
+ });
+ }
+ test("Unicode decoding", function () {
+ return buildTree(read_file("shift-jis-image"), {
+ strformat: "unicode",
+ bodyformat: "decode",
+ }).then(function (tree) {
+ // text/plain should be transcoded...
+ assert.equal(
+ tree.get("1").headers.get("Content-Type").get("charset"),
+ "Shift-JIS"
+ );
+ assert.equal(tree.get("1").headers.charset, "Shift-JIS");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb"
+ );
+ assert.equal(
+ tree.get("1").body,
+ "Portable Network Graphics\uff08" +
+ "\u30dd\u30fc\u30bf\u30d6\u30eb\u30fb\u30cd\u30c3\u30c8\u30ef\u30fc" +
+ "\u30af\u30fb\u30b0\u30e9\u30d5\u30a3\u30c3\u30af\u30b9\u3001PNG" +
+ "\uff09\u306f\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u3067\u30d3\u30c3" +
+ "\u30c8\u30de\u30c3\u30d7\u753b\u50cf\u3092\u6271\u3046\u30d5\u30a1" +
+ "\u30a4\u30eb\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u3042\u308b" +
+ "\u3002\u5727\u7e2e\u30a2\u30eb\u30b4\u30ea\u30ba\u30e0\u3068\u3057" +
+ "\u3066Deflate\u3092\u63a1\u7528\u3057\u3066\u3044\u308b\u3001" +
+ "\u5727\u7e2e\u306b\u3088\u308b\u753b\u8cea\u306e\u52a3\u5316\u306e" +
+ "\u306a\u3044\u53ef\u9006\u5727\u7e2e\u306e\u753b\u50cf\u30d5\u30a1" +
+ "\u30a4\u30eb\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u3042\u308b" +
+ "\u3002\r\n"
+ );
+ // ... but not image/png
+ assert.ok(!tree.get("2").headers.get("Content-Type").has("charset"));
+ assert.equal(tree.get("2").headers.charset, "");
+ assert.equal(
+ tree.get("2").headers.get("Content-Description"),
+ "\ufffdP\ufffdc\ufffd@\ufffd\ufffd\ufffdR\ufffdA\ufffdg\ufffd\ufffd"
+ );
+ assert.equal(
+ tree.get("2").headers.getRawHeader("Content-Description"),
+ "\x83\x50\x83\x63\x83\x40\x83\x8b\x83\x52\x83\x41\x83\x67\x83\x8b"
+ );
+ var imageData =
+ "iVBORw0KGgoAAAANSUhEUgAAAIAAAABECAIAAADGJao+AAAAwklE" +
+ "QVR4Xu3UgQbDMBRA0bc03f//b7N0VuqJEmwoc+KqNEkDh9b+2HuJu1KNO4f+AQCAAA" +
+ "AQAAACAEAAAAgAAAEAIAAABACAAAAQAAACAEAAAAgAAAEAIAAAANReamRLlPWYfNH0" +
+ "klxcPs+cP3NxWF+vi3lb7pa2R+vx6tHOtuN1O+a5lY3HzgM5ya/GM5N7ZjfPq7/5yS" +
+ "8IgAAAEAAAAgBAAAAIAAABACAAAAQAgAAAEAAAAgBAAAAIAAABACAAAIw322gDIPvt" +
+ "lmUAAAAASUVORK5CYII=";
+ imageData = atob(imageData);
+ var asArray = new Uint8Array(imageData.length);
+ for (var i = 0; i < asArray.length; i++) {
+ asArray[i] = imageData.charCodeAt(i);
+ }
+ assert.deepEqual(tree.get("2").body, asArray);
+
+ // Touching the header charset should change the interpretation.
+ tree.get("1").headers.charset = "Shift-JIS";
+ assert.equal(tree.get("1").headers.charset, "Shift-JIS");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb"
+ );
+ });
+ });
+ test("Fallback charset decoding", function () {
+ return buildTree(read_file("shift-jis-image"), {
+ strformat: "unicode",
+ charset: "ISO-8859-1",
+ bodyformat: "decode",
+ }).then(function (tree) {
+ // text/plain should be transcoded...
+ assert.equal(
+ tree.get("1").headers.get("Content-Type").get("charset"),
+ "Shift-JIS"
+ );
+ assert.equal(tree.get("1").headers.charset, "Shift-JIS");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb"
+ );
+ assert.equal(
+ tree.get("1").body,
+ "Portable Network Graphics\uff08" +
+ "\u30dd\u30fc\u30bf\u30d6\u30eb\u30fb\u30cd\u30c3\u30c8\u30ef\u30fc" +
+ "\u30af\u30fb\u30b0\u30e9\u30d5\u30a3\u30c3\u30af\u30b9\u3001PNG" +
+ "\uff09\u306f\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u3067\u30d3\u30c3" +
+ "\u30c8\u30de\u30c3\u30d7\u753b\u50cf\u3092\u6271\u3046\u30d5\u30a1" +
+ "\u30a4\u30eb\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u3042\u308b" +
+ "\u3002\u5727\u7e2e\u30a2\u30eb\u30b4\u30ea\u30ba\u30e0\u3068\u3057" +
+ "\u3066Deflate\u3092\u63a1\u7528\u3057\u3066\u3044\u308b\u3001" +
+ "\u5727\u7e2e\u306b\u3088\u308b\u753b\u8cea\u306e\u52a3\u5316\u306e" +
+ "\u306a\u3044\u53ef\u9006\u5727\u7e2e\u306e\u753b\u50cf\u30d5\u30a1" +
+ "\u30a4\u30eb\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u3042\u308b" +
+ "\u3002\r\n"
+ );
+ // ... but not image/png
+ assert.ok(!tree.get("2").headers.get("Content-Type").has("charset"));
+ assert.equal(tree.get("2").headers.charset, "ISO-8859-1");
+ assert.equal(
+ tree.get("2").headers.get("Content-Description"),
+ "\u0192P\u0192c\u0192@\u0192\u2039\u0192R\u0192A\u0192g\u0192\u2039"
+ );
+ assert.equal(
+ tree.get("2").headers.getRawHeader("Content-Description"),
+ "\x83\x50\x83\x63\x83\x40\x83\x8b\x83\x52\x83\x41\x83\x67\x83\x8b"
+ );
+ var imageData =
+ "iVBORw0KGgoAAAANSUhEUgAAAIAAAABECAIAAADGJao+AAAAwklE" +
+ "QVR4Xu3UgQbDMBRA0bc03f//b7N0VuqJEmwoc+KqNEkDh9b+2HuJu1KNO4f+AQCAAA" +
+ "AQAAACAEAAAAgAAAEAIAAABACAAAAQAAACAEAAAAgAAAEAIAAAANReamRLlPWYfNH0" +
+ "klxcPs+cP3NxWF+vi3lb7pa2R+vx6tHOtuN1O+a5lY3HzgM5ya/GM5N7ZjfPq7/5yS" +
+ "8IgAAAEAAAAgBAAAAIAAABACAAAAQAgAAAEAAAAgBAAAAIAAABACAAAIw322gDIPvt" +
+ "lmUAAAAASUVORK5CYII=";
+ imageData = atob(imageData);
+ var asArray = new Uint8Array(imageData.length);
+ for (var i = 0; i < asArray.length; i++) {
+ asArray[i] = imageData.charCodeAt(i);
+ }
+ assert.deepEqual(tree.get("2").body, asArray);
+
+ // Touching the header charset should change the interpretation.
+ tree.get("1").headers.charset = "Shift-JIS";
+ assert.equal(tree.get("1").headers.charset, "Shift-JIS");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb"
+ );
+ });
+ });
+ test("Forced charset decoding", function () {
+ return buildTree(read_file("shift-jis-image"), {
+ strformat: "unicode",
+ charset: "ISO-8859-1",
+ "force-charset": true,
+ bodyformat: "decode",
+ }).then(function (tree) {
+ // text/plain should be transcoded...
+ assert.equal(
+ tree.get("1").headers.get("Content-Type").get("charset"),
+ "Shift-JIS"
+ );
+ assert.equal(tree.get("1").headers.charset, "ISO-8859-1");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u0192P\u0192c\u0192@\u0192\u2039\u0192R\u0192A\u0192g\u0192\u2039"
+ );
+ assert.equal(
+ tree.get("1").body,
+ "Portable Network Graphics\u0081i" +
+ "\u0192|\u0081[\u0192^\u0192u\u0192\u2039\u0081E\u0192l\u0192b" +
+ "\u0192g\u0192\u008f\u0081[\u0192N\u0081E\u0192O\u0192\u2030\u0192t" +
+ "\u0192B\u0192b\u0192N\u0192X\u0081APNG\u0081j\u201a\u00cd\u0192R" +
+ "\u0192\u201c\u0192s\u0192\u2026\u0081[\u0192^\u201a\u00c5\u0192r" +
+ "\u0192b\u0192g\u0192}\u0192b\u0192v\u2030\u00e6\u2018\u0153\u201a" +
+ "\u00f0\u02c6\u00b5\u201a\u00a4\u0192t\u0192@\u0192C\u0192\u2039" +
+ "\u0192t\u0192H\u0081[\u0192}\u0192b\u0192g\u201a\u00c5\u201a\u00a0" +
+ "\u201a\u00e9\u0081B\u02c6\u00b3\u008fk\u0192A\u0192\u2039\u0192S" +
+ "\u0192\u0160\u0192Y\u0192\u20ac\u201a\u00c6\u201a\u00b5\u201a" +
+ "\u00c4Deflate\u201a\u00f0\u008d\u00cc\u2014p\u201a\u00b5\u201a" +
+ "\u00c4\u201a\u00a2\u201a\u00e9\u0081A\u02c6\u00b3\u008fk\u201a" +
+ "\u00c9\u201a\u00e6\u201a\u00e9\u2030\u00e6\u017d\u00bf\u201a\u00cc" +
+ "\u2014\u00f2\u2030\u00bb\u201a\u00cc\u201a\u00c8\u201a\u00a2\u2030" +
+ "\u00c2\u2039t\u02c6\u00b3\u008fk\u201a\u00cc\u2030\u00e6\u2018" +
+ "\u0153\u0192t\u0192@\u0192C\u0192\u2039\u0192t\u0192H\u0081[\u0192" +
+ "}\u0192b\u0192g\u201a\u00c5\u201a\u00a0\u201a\u00e9\u0081B\r\n"
+ );
+ // ... but not image/png
+ assert.ok(!tree.get("2").headers.get("Content-Type").has("charset"));
+ assert.equal(tree.get("2").headers.charset, "ISO-8859-1");
+ assert.equal(
+ tree.get("2").headers.get("Content-Description"),
+ "\u0192P\u0192c\u0192@\u0192\u2039\u0192R\u0192A\u0192g\u0192\u2039"
+ );
+ assert.equal(
+ tree.get("2").headers.getRawHeader("Content-Description"),
+ "\x83\x50\x83\x63\x83\x40\x83\x8b\x83\x52\x83\x41\x83\x67\x83\x8b"
+ );
+ var imageData =
+ "iVBORw0KGgoAAAANSUhEUgAAAIAAAABECAIAAADGJao+AAAAwklE" +
+ "QVR4Xu3UgQbDMBRA0bc03f//b7N0VuqJEmwoc+KqNEkDh9b+2HuJu1KNO4f+AQCAAA" +
+ "AQAAACAEAAAAgAAAEAIAAABACAAAAQAAACAEAAAAgAAAEAIAAAANReamRLlPWYfNH0" +
+ "klxcPs+cP3NxWF+vi3lb7pa2R+vx6tHOtuN1O+a5lY3HzgM5ya/GM5N7ZjfPq7/5yS" +
+ "8IgAAAEAAAAgBAAAAIAAABACAAAAQAgAAAEAAAAgBAAAAIAAABACAAAIw322gDIPvt" +
+ "lmUAAAAASUVORK5CYII=";
+ imageData = atob(imageData);
+ var asArray = new Uint8Array(imageData.length);
+ for (var i = 0; i < asArray.length; i++) {
+ asArray[i] = imageData.charCodeAt(i);
+ }
+ assert.deepEqual(tree.get("2").body, asArray);
+
+ // Touching the header charset should change the interpretation.
+ tree.get("1").headers.charset = "Shift-JIS";
+ assert.equal(tree.get("1").headers.charset, "Shift-JIS");
+ assert.equal(
+ tree.get("1").headers.get("Content-Description"),
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb"
+ );
+ });
+ });
+ test("Charset conversion", function () {
+ return buildTree(read_file("charsets"), {
+ strformat: "unicode",
+ bodyformat: "decode",
+ }).then(function (tree) {
+ var numParts = 12;
+ for (let i = 1; i < numParts; i += 2) {
+ assert.equal(tree.get("" + i).body, tree.get("" + (i + 1)).body);
+ }
+ assert.ok(!tree.has("" + (numParts + 1)));
+ });
+ });
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_structured_header_emitters.js b/comm/mailnews/mime/jsmime/test/unit/test_structured_header_emitters.js
new file mode 100644
index 0000000000..8915c372a0
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_structured_header_emitters.js
@@ -0,0 +1,133 @@
+"use strict";
+define(function (require) {
+ var assert = require("assert");
+ var headeremitter = require("jsmime").headeremitter;
+ var MockDate = require("test/mock_date");
+
+ function arrayTest(data, fn) {
+ fn.toString = function () {
+ let text = Function.prototype.toString.call(this);
+ text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
+ return JSON.stringify(data[p]);
+ });
+ return text;
+ };
+ return test(JSON.stringify(data[0]), fn);
+ }
+
+ function testHeader(header, tests) {
+ suite(header, function () {
+ tests.forEach(function (data) {
+ arrayTest(data, function () {
+ assert.deepEqual(
+ headeremitter.emitStructuredHeader(header, data[0], {
+ softMargin: 100,
+ useASCII: true,
+ }),
+ (header + ": " + data[1]).trim() + "\r\n"
+ );
+ });
+ });
+ });
+ }
+
+ suite("Structured header emitters", function () {
+ // Ad-hoc header tests
+ // TODO: add structured encoder tests for Content-Type when it is added.
+
+ testHeader("Content-Transfer-Encoding", [
+ ["", ""],
+ ["8bit", "8bit"],
+ ["invalid", "invalid"],
+ ]);
+
+ // Non-ad-hoc header tests
+ let addressing_headers = [
+ "From",
+ "To",
+ "Cc",
+ "Bcc",
+ "Sender",
+ "Reply-To",
+ "Resent-Bcc",
+ "Resent-To",
+ "Resent-From",
+ "Resent-Cc",
+ "Resent-Sender",
+ "Approved",
+ "Disposition-Notification-To",
+ "Delivered-To",
+ "Return-Receipt-To",
+ "Resent-Reply-To",
+ "Mail-Reply-To",
+ "Mail-Followup-To",
+ ];
+ let address_tests = [
+ [{ name: "", email: "" }, ""],
+ [
+ { name: "John Doe", email: "john.doe@test.invalid" },
+ "John Doe <john.doe@test.invalid>",
+ ],
+ [
+ [{ name: "John Doe", email: "john.doe@test.invalid" }],
+ "John Doe <john.doe@test.invalid>",
+ ],
+ [
+ { name: "undisclosed-recipients", group: [] },
+ "undisclosed-recipients: ;",
+ ],
+ ];
+ addressing_headers.forEach(function (header) {
+ testHeader(header, address_tests);
+ });
+
+ let date_headers = [
+ "Date",
+ "Expires",
+ "Injection-Date",
+ "NNTP-Posting-Date",
+ "Resent-Date",
+ ];
+ let date_tests = [
+ [
+ new MockDate("2012-09-06T08:08:21-0700"),
+ "Thu, 6 Sep 2012 08:08:21 -0700",
+ ],
+ ];
+ date_headers.forEach(function (header) {
+ testHeader(header, date_tests);
+ });
+
+ let unstructured_headers = [
+ "Comments",
+ "Content-Description",
+ "Keywords",
+ "Subject",
+ ];
+ let unstructured_tests = [
+ ["", ""],
+ ["This is a subject", "This is a subject"],
+ [
+ "\u79c1\u306f\u4ef6\u540d\u5348\u524d",
+ "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?=",
+ ],
+ ];
+ unstructured_headers.forEach(function (header) {
+ testHeader(header, unstructured_tests);
+ });
+
+ test("emitStructuredHeaders", function () {
+ let headers = new Map();
+ headers.set("From", [{ name: "", email: "bugzilla-daemon@mozilla.org" }]);
+ headers.set("subject", ["[Bug 939557] browsercomps.dll failed to build"]);
+ headers.set("x-capitalization-test", ["should capitalize"]);
+ let str = headeremitter.emitStructuredHeaders(headers, {});
+ assert.equal(
+ str,
+ "From: bugzilla-daemon@mozilla.org\r\n" +
+ "Subject: [Bug 939557] browsercomps.dll failed to build\r\n" +
+ "X-Capitalization-Test: should capitalize\r\n"
+ );
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/test_structured_headers.js b/comm/mailnews/mime/jsmime/test/unit/test_structured_headers.js
new file mode 100644
index 0000000000..8e8e1bb6a6
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/test_structured_headers.js
@@ -0,0 +1,254 @@
+"use strict";
+define(function (require) {
+ var assert = require("assert");
+ var headerparser = require("jsmime").headerparser;
+
+ function smartDeepEqual(actual, expected) {
+ assert.deepEqual(actual, expected);
+ if (actual instanceof Map && expected instanceof Map) {
+ assert.deepEqual(
+ Array.from(actual.entries()),
+ Array.from(expected.entries())
+ );
+ }
+ }
+
+ function arrayTest(data, fn) {
+ fn.toString = function () {
+ let text = Function.prototype.toString.call(this);
+ text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
+ return JSON.stringify(data[p]);
+ });
+ return text;
+ };
+ return test(data[0], fn);
+ }
+
+ function testHeader(header, tests) {
+ suite(header, function () {
+ tests.forEach(function (data) {
+ arrayTest(data, function () {
+ smartDeepEqual(
+ headerparser.parseStructuredHeader(header, data[0]),
+ data[1]
+ );
+ });
+ });
+ });
+ }
+
+ function makeCT(media, sub, params) {
+ var object = new Map();
+ object.mediatype = media;
+ object.subtype = sub;
+ object.type = media + "/" + sub;
+ for (let k in params) {
+ object.set(k, params[k]);
+ }
+ return object;
+ }
+ suite("Structured headers", function () {
+ // Ad-hoc header tests
+ testHeader("Content-Type", [
+ ["text/plain", makeCT("text", "plain", {})],
+ ["text/html", makeCT("text", "html", {})],
+ [
+ 'text/plain; charset="UTF-8"',
+ makeCT("text", "plain", { charset: "UTF-8" }),
+ ],
+ ["text/", makeCT("text", "", {})],
+ ["text", makeCT("text", "plain", {})],
+ ["image/", makeCT("image", "", {})],
+ ["image", makeCT("text", "plain", {})],
+ ["hacker/x-mailnews", makeCT("hacker", "x-mailnews", {})],
+ ["hacker/x-mailnews;", makeCT("hacker", "x-mailnews", {})],
+ ["HACKER/X-MAILNEWS", makeCT("hacker", "x-mailnews", {})],
+ [
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ makeCT(
+ "application",
+ "vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ {}
+ ),
+ ],
+ [
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;\r" +
+ '\n name="Presentation.pptx"',
+ makeCT(
+ "application",
+ "vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ { name: "Presentation.pptx" }
+ ),
+ ],
+ ["", makeCT("text", "plain", {})],
+ [" ", makeCT("text", "plain", {})],
+ ["text/plain; c", makeCT("text", "plain", {})],
+ ["text/plain; charset=", makeCT("text", "plain", { charset: "" })],
+ ['text/plain; charset="', makeCT("text", "plain", { charset: "" })],
+ ["text\\/enriched", makeCT("text\\", "enriched", {})],
+ ['multipart/mixed ";" wtf=stupid', makeCT("multipart", "mixed", {})],
+ [
+ "multipart/mixed; wtf=stupid",
+ makeCT("multipart", "mixed", { wtf: "stupid" }),
+ ],
+ [
+ "text/plain; CHARSET=Big5",
+ makeCT("text", "plain", { charset: "Big5" }),
+ ],
+ [
+ 'text/html; CHARSET="Big5"',
+ makeCT("text", "html", { charset: "Big5" }),
+ ],
+ ['text/html; CHARSET="Big5', makeCT("text", "html", { charset: "Big5" })],
+ [["text/html", "multipart/mixed"], makeCT("text", "html", {})],
+ ]);
+ testHeader("Content-Transfer-Encoding", [
+ ["", ""],
+ ["8bit", "8bit"],
+ ["8BIT", "8bit"],
+ ["QuOtEd-PrInTaBlE", "quoted-printable"],
+ ["Base64", "base64"],
+ ["7bit", "7bit"],
+ [["7bit", "8bit"], "7bit"],
+ ["x-uuencode", "x-uuencode"],
+ ]);
+
+ // Non-ad-hoc header tests
+ let addressing_headers = [
+ "From",
+ "To",
+ "Cc",
+ "Bcc",
+ "Sender",
+ "Reply-To",
+ "Resent-Bcc",
+ "Resent-To",
+ "Resent-From",
+ "Resent-Cc",
+ "Resent-Sender",
+ "Approved",
+ "Disposition-Notification-To",
+ "Delivered-To",
+ "Return-Receipt-To",
+ "Resent-Reply-To",
+ "Mail-Reply-To",
+ "Mail-Followup-To",
+ ];
+ let address_tests = [
+ ["", []],
+ ["a@example.invalid", [{ name: "", email: "a@example.invalid" }]],
+ [
+ "John Doe <a@example.invalid>",
+ [{ name: "John Doe", email: "a@example.invalid" }],
+ ],
+ [
+ "John Doe <A@EXAMPLE.INVALID>",
+ [{ name: "John Doe", email: "A@EXAMPLE.INVALID" }],
+ ],
+ [
+ "=?UTF-8?B?5bGx55Sw5aSq6YOO?= <a@example.invalid>",
+ [{ name: "\u5c71\u7530\u592a\u90ce", email: "a@example.invalid" }],
+ ],
+ [
+ "undisclosed-recipients:;",
+ [{ name: "undisclosed-recipients", group: [] }],
+ ],
+ [
+ "world: a@example.invalid, b@example.invalid;",
+ [
+ {
+ name: "world",
+ group: [
+ { name: "", email: "a@example.invalid" },
+ { name: "", email: "b@example.invalid" },
+ ],
+ },
+ ],
+ ],
+ // TODO when we support IDN:
+ // This should be \u4f8b.invalid instead (Japanese kanji for "example")
+ [
+ "\u5c71\u7530\u592a\u90ce <a@xn--fsq.invalid>",
+ [{ name: "\u5c71\u7530\u592a\u90ce", email: "a@xn--fsq.invalid" }],
+ ],
+ [
+ "\u5c71\u7530\u592a\u90ce <a@\u4f8b.invalid>",
+ [{ name: "\u5c71\u7530\u592a\u90ce", email: "a@\u4f8b.invalid" }],
+ ],
+ [
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb@\u4f8b.invalid",
+ [
+ {
+ name: "",
+ email:
+ "\u30b1\u30c4\u30a1\u30eb\u30b3\u30a2\u30c8\u30eb@\u4f8b.invalid",
+ },
+ ],
+ ],
+ [
+ ["a@example.invalid", "b@example.invalid"],
+ [
+ { name: "", email: "a@example.invalid" },
+ { name: "", email: "b@example.invalid" },
+ ],
+ ],
+ ];
+ addressing_headers.forEach(function (header) {
+ testHeader(header, address_tests);
+ });
+
+ let date_headers = [
+ "Date",
+ "Expires",
+ "Injection-Date",
+ "NNTP-Posting-Date",
+ "Resent-Date",
+ ];
+ let date_tests = [
+ ["Thu, 06 Sep 2012 08:08:21 -0700", new Date("2012-09-06T08:08:21-0700")],
+ ["This is so not a date", new Date(NaN)],
+ ];
+ date_headers.forEach(function (header) {
+ testHeader(header, date_tests);
+ });
+
+ let multiple_unstructured_headers = ["In-Reply-To", "References"];
+ let multiple_unstructured_tests = [
+ ["<asdasdasd@asdasdasd.com>", "<asdasdasd@asdasdasd.com>"],
+ ["<asd@asd.com> <asdf@asdf.com>", "<asd@asd.com> <asdf@asdf.com>"],
+
+ // This test is needed for clients sending non-compliant headers, see bug 1154521
+ [
+ "<asd@asd.com>,<asdf@asdf.com>,<asdfg@asdfg.com>",
+ "<asd@asd.com> <asdf@asdf.com> <asdfg@asdfg.com>",
+ ],
+ // Test for bug 1197686
+ [
+ "<asd@asd.com><asdf@asdf.com><asdfg@asdfg.com>",
+ "<asd@asd.com> <asdf@asdf.com> <asdfg@asdfg.com>",
+ ],
+ ];
+ multiple_unstructured_headers.forEach(function (header) {
+ testHeader(header, multiple_unstructured_tests);
+ });
+
+ let unstructured_headers = [
+ "Comments",
+ "Content-Description",
+ "Keywords",
+ "Subject",
+ ];
+ let unstructured_tests = [
+ ["", ""],
+ ["This is a subject", "This is a subject"],
+ [["Subject 1", "Subject 2"], "Subject 1"],
+ [
+ "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?=",
+ "\u79c1\u306f\u4ef6\u540d\u5348\u524d",
+ ],
+ ];
+ unstructured_headers.forEach(function (header) {
+ testHeader(header, unstructured_tests);
+ });
+ });
+});
diff --git a/comm/mailnews/mime/jsmime/test/unit/xpcshell.ini b/comm/mailnews/mime/jsmime/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..aa348059b7
--- /dev/null
+++ b/comm/mailnews/mime/jsmime/test/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head=head_xpcshell_glue.js
+tail=
+support-files=data/**
+
+[test_custom_headers.js]
+[test_header_emitter.js]
+[test_header.js]
+[test_mime_tree.js]
+[test_structured_header_emitters.js]
+[test_structured_headers.js]
diff --git a/comm/mailnews/mime/moz.build b/comm/mailnews/mime/moz.build
new file mode 100644
index 0000000000..17ac8b805d
--- /dev/null
+++ b/comm/mailnews/mime/moz.build
@@ -0,0 +1,23 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "src",
+ "emitters",
+ "cthandlers",
+]
+
+TEST_DIRS += ["test"]
+
+EXTRA_JS_MODULES.jsmime += [
+ "jsmime/jsmime.js",
+]
+
+TESTING_JS_MODULES.jsmime += [
+ "jsmime/test/unit/mock_date.js",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["jsmime/test/unit/xpcshell.ini"]
diff --git a/comm/mailnews/mime/public/MimeEncoder.h b/comm/mailnews/mime/public/MimeEncoder.h
new file mode 100644
index 0000000000..19a06810a6
--- /dev/null
+++ b/comm/mailnews/mime/public/MimeEncoder.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef MimeEncoder_h__
+#define MimeEncoder_h__
+
+#include "nscore.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/// A class for encoding the bodies of MIME parts.
+class MimeEncoder {
+ public:
+ virtual ~MimeEncoder() {}
+
+ /// A callback for writing the encoded output
+ typedef nsresult (*OutputCallback)(const char* buf, int32_t size,
+ void* closure);
+
+ /// Encodes the string in the buffer and sends it to the callback
+ virtual nsresult Write(const char* buffer, int32_t size) = 0;
+ /// Flush all pending data when no more data exists
+ virtual nsresult Flush() { return NS_OK; }
+
+ /// Get an encoder that outputs Base64-encoded data
+ static MimeEncoder* GetBase64Encoder(OutputCallback callback, void* closure);
+ /// Get an encoder that outputs quoted-printable data
+ static MimeEncoder* GetQPEncoder(OutputCallback callback, void* closure);
+
+ protected:
+ MimeEncoder(OutputCallback callback, void* closure);
+ OutputCallback mCallback;
+ void* mClosure;
+ uint32_t mCurrentColumn;
+};
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/mime/public/MimeHeaderParser.h b/comm/mailnews/mime/public/MimeHeaderParser.h
new file mode 100644
index 0000000000..489e35f7e6
--- /dev/null
+++ b/comm/mailnews/mime/public/MimeHeaderParser.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef MimeHeaderParser_h__
+#define MimeHeaderParser_h__
+
+#include "nsCOMArray.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class msgIAddressObject;
+
+namespace mozilla {
+namespace mailnews {
+
+/**
+ * This is used to signal that the input header value has already been decoded
+ * according to RFC 2047 and is in UTF-16 form.
+ */
+nsCOMArray<msgIAddressObject> DecodedHeader(const nsAString& aHeader);
+
+/**
+ * This is used to signal that the input header value needs to be decoded
+ * according to RFC 2047. The charset parameter indicates the charset to assume
+ * that non-ASCII data is in; if the value is null (the default), then the
+ * charset is assumed to be UTF-8.
+ */
+nsCOMArray<msgIAddressObject> EncodedHeader(const nsACString& aHeader,
+ const char* aCharset = nullptr);
+/**
+ * Same deal, but we're starting with an nsAString.
+ */
+nsCOMArray<msgIAddressObject> EncodedHeaderW(const nsAString& aHeader);
+
+namespace detail {
+void DoConversion(const nsTArray<nsString>& aUTF16, nsTArray<nsCString>& aUTF8);
+};
+/**
+ * This is a class designed for use as temporaries so that methods can pass
+ * an nsTArray<nsCString> into methods that expect nsTArray<nsString> for out
+ * parameters (this does not work for in-parameters).
+ *
+ * It works by internally providing an nsTArray<nsString> which it uses for its
+ * external API operations. If the user requests an array of nsCString elements
+ * instead, it converts the UTF-16 array to a UTF-8 array on destruction.
+ */
+template <uint32_t N = 5>
+class UTF16ArrayAdapter {
+ public:
+ explicit UTF16ArrayAdapter(nsTArray<nsCString>& aUTF8Array)
+ : mUTF8Array(aUTF8Array) {}
+ ~UTF16ArrayAdapter() { detail::DoConversion(mUTF16Array, mUTF8Array); }
+ operator nsTArray<nsString>&() { return mUTF16Array; }
+
+ private:
+ nsTArray<nsCString>& mUTF8Array;
+ AutoTArray<nsString, N> mUTF16Array;
+};
+
+/**
+ * Given a name and an email, both encoded in UTF-8, produce a string suitable
+ * for writing in an email header by quoting where necessary.
+ *
+ * If name is not empty, the output string will be name <email>. If it is empty,
+ * the output string is just the email. Note that this DOES NOT do any RFC 2047
+ * encoding.
+ */
+void MakeMimeAddress(const nsACString& aName, const nsACString& aEmail,
+ nsACString& full);
+
+/**
+ * Given a name and an email, produce a string suitable for writing in an email
+ * header by quoting where necessary.
+ *
+ * If name is not empty, the output string will be name <email>. If it is empty,
+ * the output string is just the email. Note that this DOES NOT do any RFC 2047
+ * encoding.
+ */
+void MakeMimeAddress(const nsAString& aName, const nsAString& aEmail,
+ nsAString& full);
+
+/**
+ * Given a name and an email, both encoded in UTF-8, produce a string suitable
+ * for displaying in UI.
+ *
+ * If name is not empty, the output string will be name <email>. If it is empty,
+ * the output string is just the email.
+ */
+void MakeDisplayAddress(const nsAString& aName, const nsAString& aEmail,
+ nsAString& full);
+
+/**
+ * Returns a copy of the input which may have had some addresses removed.
+ * Addresses are removed if they are already in either of the supplied
+ * address lists.
+ *
+ * Addresses are considered to be the same if they contain the same email
+ * address parts, ignoring case. Display names or comments are not compared.
+ *
+ * @param aHeader The addresses to remove duplicates from.
+ * @param aOtherEmails Other addresses that the duplicate removal process also
+ * checks for duplicates against. Addresses in this list
+ * will not be added to the result.
+ * @return The original header with duplicate addresses removed.
+ */
+void RemoveDuplicateAddresses(const nsACString& aHeader,
+ const nsACString& aOtherEmails,
+ nsACString& result);
+
+/**
+ * Given a message header, extract all names and email addresses found in that
+ * header into the two arrays.
+ */
+void ExtractAllAddresses(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& names, nsTArray<nsString>& emails);
+
+/**
+ * Given a raw message header value, extract display names for every address
+ * found in the header.
+ */
+void ExtractDisplayAddresses(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& addresses);
+
+/**
+ * Given a raw message header value, extract all the email addresses into an
+ * array.
+ *
+ * Duplicate email addresses are not removed from the output list.
+ */
+void ExtractEmails(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& emails);
+
+/**
+ * Given a raw message header value, extract the first name/email address found
+ * in the header. This is essentially equivalent to grabbing the first entry of
+ * ExtractAllAddresses.
+ */
+void ExtractFirstAddress(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& name, nsACString& email);
+
+/**
+ * Given an RFC 2047-decoded message header value, extract the first name/email
+ * address found in the header. This is essentially equivalent to grabbing the
+ * first entry of ExtractAllAddresses.
+ */
+void ExtractFirstAddress(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsAString& name, nsACString& email);
+
+/**
+ * Given a raw message header value, extract the first email address found in
+ * the header.
+ */
+void ExtractEmail(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& email);
+
+/**
+ * Given a raw message header value, extract and clean up the first display
+ * name found in the header. If there is no display name, the email address is
+ * used instead.
+ */
+void ExtractName(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& name);
+
+/**
+ * Given an RFC 2047-decoded message header value, extract the first display
+ * name found in the header. If there is no display name, the email address is
+ * returned instead.
+ */
+void ExtractName(const nsCOMArray<msgIAddressObject>& aDecodedHeader,
+ nsAString& name);
+
+} // namespace mailnews
+} // namespace mozilla
+
+#endif
diff --git a/comm/mailnews/mime/public/moz.build b/comm/mailnews/mime/public/moz.build
new file mode 100644
index 0000000000..d048796af9
--- /dev/null
+++ b/comm/mailnews/mime/public/moz.build
@@ -0,0 +1,28 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "msgIStructuredHeaders.idl",
+ "nsIMimeContentTypeHandler.idl",
+ "nsIMimeConverter.idl",
+ "nsIMimeEmitter.idl",
+ "nsIMimeHeaders.idl",
+ "nsIMimeStreamConverter.idl",
+ "nsIMsgHeaderParser.idl",
+ "nsIPgpMimeProxy.idl",
+ "nsISimpleMimeConverter.idl",
+]
+
+XPIDL_MODULE = "mime"
+
+EXPORTS += [
+ "nsIMimeObjectClassAccess.h",
+ "nsMailHeaders.h",
+]
+
+EXPORTS.mozilla.mailnews += [
+ "MimeEncoder.h",
+ "MimeHeaderParser.h",
+]
diff --git a/comm/mailnews/mime/public/msgIStructuredHeaders.idl b/comm/mailnews/mime/public/msgIStructuredHeaders.idl
new file mode 100644
index 0000000000..2f1e56a7b3
--- /dev/null
+++ b/comm/mailnews/mime/public/msgIStructuredHeaders.idl
@@ -0,0 +1,175 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#include "nsCOMArray.h"
+
+#define NS_ISTRUCTUREDHEADERS_CONTRACTID \
+ "@mozilla.org/messenger/structuredheaders;1"
+%}
+
+interface msgIAddressObject;
+interface nsIUTF8StringEnumerator;
+
+/**
+ * A collection of MIME headers that are stored in a rich, structured format.
+ *
+ * The structured forms defined in this method use the structured decoder and
+ * encoder functionality found in jsmime to interconvert between the raw string
+ * forms found in the actual message text and the structured forms supported by
+ * this interface. Extensions can register their own custom structured headers
+ * by listing the source URL of their code under the category
+ * "custom-mime-encoder".
+ *
+ * The alternative modes of access for specific headers are expected to only
+ * work for the headers for which that mode of access is the correct one. For
+ * example, retrieving the "To" header from getUnstructuredHeader would fail,
+ * since the To header is not an unstructured header but an addressing header.
+ * They are provided mostly as a convenience to C++ which is much less able to
+ * utilize a fully generic format.
+ *
+ * With the exception of mismatched headers, the methods do not throw an
+ * exception if the header is missing but rather return an appropriate default
+ * value as indicated in their documentation.
+ */
+[scriptable, uuid(e109bf4f-788f-47ba-bfa8-1236ede05597)]
+interface msgIStructuredHeaders : nsISupports {
+ /**
+ * Retrieve the value of the header stored in this set of headers. If the
+ * header is not present, then undefined is returned.
+ *
+ * @param aHeaderName The name of the header to retrieve.
+ */
+ jsval getHeader(in string aHeaderName);
+
+ /**
+ * Return true if and only if the given header is already set.
+ *
+ * @param aHeaderName The name of the header to retrieve.
+ */
+ bool hasHeader(in string aHeaderName);
+
+ /**
+ * Retrieve the value of the header as if it is an unstructured header. Such
+ * headers include most notably the Subject header. If the header is not
+ * present, then null is returned. This is reflected in C++ as an empty string
+ * with IsVoid() set to true (distinguishing it from a header that is present
+ * but contains an empty string).
+ *
+ * @param aHeaderName The name of the header to retrieve.
+ */
+ AString getUnstructuredHeader(in string aHeaderName);
+
+ /**
+ * Retrieve the value of the header if it is an addressing header, such as the
+ * From or To headers. If the header is not present, then an empty array is
+ * returned.
+ *
+ * @param aHeaderName The name of the header to retrieve.
+ * @param aPreserveGroups If false (the default), then the result is a flat
+ * list of addresses, with all group annotations
+ * removed.
+ * If true, then some address objects may represent
+ * groups in the header, preserving the original header
+ * structure.
+ */
+ Array<msgIAddressObject> getAddressingHeader(in string aHeaderName,
+ [optional] in boolean aPreserveGroups);
+
+ /**
+ * Retrieve a raw version of the header value as would be represented in MIME.
+ * This form does not include the header name and colon, trailing whitespace,
+ * nor embedded CRLF pairs in the case of very long header names.
+ *
+ * @param aHeaderName The name of the header to retrieve.
+ */
+ AUTF8String getRawHeader(in string aHeaderName);
+
+ /**
+ * Retrieve an enumerator of the names of all headers in this set of headers.
+ * The header names returned may be in different cases depending on the
+ * precise implementation of this interface, so implementations should not
+ * rely on an exact kind of case being returned.
+ */
+ readonly attribute nsIUTF8StringEnumerator headerNames;
+
+ /**
+ * Retrieve the MIME representation of all of the headers.
+ *
+ * The header values are emitted in an ASCII form, unless internationalized
+ * email addresses are involved. The extra CRLF indicating the end of headers
+ * is not included in this representation.
+ *
+ * This accessor is provided mainly for the benefit of C++ consumers of this
+ * interface, since the JSMime headeremitter functionality allows more
+ * fine-grained customization of the results.
+ *
+ * @param sanitizeDate If true convert the date to UTC and round to closest minute.
+ */
+ AUTF8String buildMimeText([optional] in boolean sanitizeDate);
+
+};
+
+/**
+ * An interface that enhances msgIStructuredHeaders by allowing the values of
+ * headers to be modified.
+ */
+[scriptable, uuid(5dcbbef6-2356-45d8-86d7-b3e73f9c9a0c)]
+interface msgIWritableStructuredHeaders : msgIStructuredHeaders {
+ /**
+ * Store the given value for the given header, overwriting any previous value
+ * that was stored for said header.
+ *
+ * @param aHeaderName The name of the header to store.
+ * @param aValue The rich, structured value of the header to store.
+ */
+ void setHeader(in string aHeaderName, in jsval aValue);
+
+ /**
+ * Forget any previous value that was stored for the given header.
+ *
+ * @param aHeaderName The name of the header to delete.
+ */
+ void deleteHeader(in string aHeaderName);
+
+ /**
+ * Copy all of the structured values from another set of structured headers to
+ * the current one, overwriting any values that may have been specified
+ * locally. Note that the copy is a shallow copy of the value.
+ *
+ * @param aOtherHeaders A set of header values to be copied.
+ */
+ void addAllHeaders(in msgIStructuredHeaders aOtherHeaders);
+
+ /**
+ * Set the value of the header as if it were an unstructured header. Such
+ * headers include most notably the Subject header.
+ *
+ * @param aHeaderName The name of the header to store.
+ * @param aValue The value to store.
+ */
+ void setUnstructuredHeader(in string aHeaderName, in AString aValue);
+
+ /**
+ * Set the value of the header as if it were an addressing header, such as the
+ * From or To headers.
+ *
+ * @param aHeaderName The name of the header to set.
+ * @param aAddresses The addresses to store.
+ */
+ void setAddressingHeader(in string aHeaderName,
+ in Array<msgIAddressObject> aAddresses);
+
+ /**
+ * Store the value of the header using a raw version as would be represented
+ * in MIME.
+ *
+ * @param aHeaderName The name of the header to store.
+ * @param aValue The raw MIME header value to store.
+ */
+ void setRawHeader(in string aHeaderName, in AUTF8String aValue);
+
+};
diff --git a/comm/mailnews/mime/public/nsIMimeContentTypeHandler.idl b/comm/mailnews/mime/public/nsIMimeContentTypeHandler.idl
new file mode 100644
index 0000000000..872ef6873c
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeContentTypeHandler.idl
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This interface is implemented by content type handlers that will be
+ * called upon by libmime to process various attachments types. The primary
+ * purpose of these handlers will be to represent the attached data in a
+ * viewable HTML format that is useful for the user
+ *
+ * Note: These will all register by their content type prefixed by the
+ * following: mimecth:text/vcard
+ *
+ * libmime will then use the XPCOM Component Manager to
+ * locate the appropriate Content Type handler
+ */
+#include "nsISupports.idl"
+
+%{C++
+typedef struct {
+ bool force_inline_display;
+} contentTypeHandlerInitStruct;
+
+#include "mimecth.h"
+%}
+
+[ptr] native MimeObjectClassPtr(MimeObjectClass);
+[ptr] native CTHInitStructPtr(contentTypeHandlerInitStruct);
+
+[scriptable, builtinclass, uuid(20DABD99-F8B5-11d2-8EE0-00A024A7D144)]
+interface nsIMimeContentTypeHandler : nsISupports {
+ readonly attribute string contentType;
+
+ [noscript] MimeObjectClassPtr CreateContentTypeHandlerClass(
+ in string content_type,
+ in CTHInitStructPtr initStruct
+ );
+};
diff --git a/comm/mailnews/mime/public/nsIMimeConverter.idl b/comm/mailnews/mime/public/nsIMimeConverter.idl
new file mode 100644
index 0000000000..98e474d234
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeConverter.idl
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Encode/decode mail headers (via libmime).
+ */
+[scriptable, uuid(0d3f5531-2dbe-40d3-9280-f6ac45a6f5e0)]
+interface nsIMimeConverter : nsISupports {
+ /**
+ * Suggested byte length limit for use when calling encodeMimePartIIStr_UTF8.
+ */
+ const long MIME_ENCODED_WORD_SIZE = 72;
+ const long MAX_CHARSET_NAME_LENGTH = 64;
+
+ /**
+ * Encode a UTF-8 string into a form containing only ASCII characters using
+ * RFC 2047 encoded words where necessary.
+ *
+ * @param aHeader UTF-8 header to encode.
+ * @param aAddressingHeader Is the header a list of email addresses?
+ * @param aFieldNameLen Header field name length (ex: "From: " = 6)
+ * @param aMaxLineLen Maximum length of an individual line. Use
+ * MIME_ENCODED_WORD_SIZE for best results.
+ *
+ * @return The encoded header.
+ */
+ AUTF8String encodeMimePartIIStr_UTF8(in AUTF8String aHeader,
+ in boolean aAddressingHeader,
+ in long aFieldNameLen,
+ in long aMaxLineLen);
+
+ /**
+ * Decode a MIME header to UTF-8 if conversion is required. Marked as
+ * noscript because the return value may contain non-ASCII characters.
+ *
+ * @param header A (possibly encoded) header to decode.
+ * @param default_charset The charset to apply to un-labeled non-UTF-8 data.
+ * @param override_charset If true, default_charset is used instead of any
+ * charset labeling other than UTF-8.
+ * @param eatContinuations If true, unfold headers.
+ *
+ * @return UTF-8 encoded value if conversion was required, nullptr if no
+ * conversion was required.
+ */
+ AUTF8String decodeMimeHeaderToUTF8(in ACString header,
+ in string default_charset,
+ in boolean override_charset,
+ in boolean eatContinuations);
+
+ /**
+ * Decode a MIME header to UTF-16.
+ *
+ * @param header A (possibly encoded) header to decode.
+ * @param default_charset The charset to apply to un-labeled non-UTF-8 data.
+ * @param override_charset If true, default_charset is used instead of any
+ * charset labeling other than UTF-8.
+ * @param eatContinuations If true, unfold headers.
+ *
+ * @return UTF-16 encoded value as an AString.
+ */
+ AString decodeMimeHeader(in string header,
+ in string default_charset,
+ in boolean override_charset,
+ in boolean eatContinuations);
+};
diff --git a/comm/mailnews/mime/public/nsIMimeEmitter.idl b/comm/mailnews/mime/public/nsIMimeEmitter.idl
new file mode 100644
index 0000000000..ef292787a1
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeEmitter.idl
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+#include "nsISupports.idl"
+#include "nsrootidl.idl"
+
+interface nsIOutputStream;
+interface nsIInputStream;
+interface nsIURI;
+interface nsIStreamListener;
+interface nsIChannel;
+
+[scriptable, uuid(eb9beb09-44de-4ad2-a560-f572b1afd534)]
+interface nsMimeHeaderDisplayTypes : nsISupports
+{
+ const long MicroHeaders = 0;
+ const long NormalHeaders = 1;
+ const long AllHeaders = 2;
+};
+
+%{C++
+#define NS_IMIME_MISC_STATUS_KEY "@mozilla.org/MimeMiscStatus;1?type="
+%}
+
+[scriptable, uuid(7a57166f-2891-4122-9a74-6c3fab0caac3)]
+interface nsIMimeEmitter : nsISupports {
+
+ // Output listener to allow access to it from mime.
+ attribute nsIStreamListener outputListener;
+
+ // These will be called to start and stop the total operation.
+ void initialize(in nsIURI url, in nsIChannel aChannel, in long aFormat);
+ void complete();
+
+ // Set the output stream/listener for processed data.
+ void setPipe(in nsIInputStream inputStream, in nsIOutputStream outStream);
+
+ // Header handling routines.
+ void startHeader(in boolean rootMailHeader, in boolean headerOnly,
+ [const] in string msgID, [const] in string outCharset);
+ void addHeaderField([const] in string field, [const] in string value);
+ void addAllHeaders(in ACString allheaders);
+
+ /**
+ * Write the HTML Headers for the current attachment.
+ * Note: Book case this with an EndHeader call.
+ *
+ * @param name The name of this attachment.
+ */
+ void writeHTMLHeaders([const] in AUTF8String name);
+
+ /**
+ * Finish writing the headers for the current attachment.
+ *
+ * @param name The name of this attachment.
+ */
+ void endHeader([const] in AUTF8String name);
+
+ void updateCharacterSet([const] in string aCharset);
+
+ // Attachment handling routines.
+ void startAttachment([const] in AUTF8String name,
+ [const] in string contentType,
+ [const] in string url, in boolean aNotDownloaded);
+ void addAttachmentField([const] in string field, [const] in string value);
+ void endAttachment();
+
+ void endAllAttachments();
+
+ // Body handling routines.
+ void startBody(in boolean bodyOnly, [const] in string msgID, [const] in string outCharset);
+ void writeBody([const] in AUTF8String buf, out uint32_t amountWritten);
+ void endBody();
+
+ // Generic write routine. This is necessary for output that
+ // libmime needs to pass through without any particular parsing
+ // involved (i.e. decoded images, HTML Body Text, etc...
+ void write([const] in ACString buf, out uint32_t amountWritten);
+ void utilityWrite([const] in string buf);
+};
diff --git a/comm/mailnews/mime/public/nsIMimeHeaders.idl b/comm/mailnews/mime/public/nsIMimeHeaders.idl
new file mode 100644
index 0000000000..3a0d05c491
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeHeaders.idl
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "msgIStructuredHeaders.idl"
+
+%{C++
+#define NS_IMIMEHEADERS_CONTRACTID \
+ "@mozilla.org/messenger/mimeheaders;1"
+%}
+
+/**
+ * An interface that can extract individual headers from a body of headers.
+ */
+[scriptable, uuid(a9222679-b991-4786-8314-f8819c3a2ba3)]
+interface nsIMimeHeaders : msgIStructuredHeaders {
+ /// Feed in the text of headers
+ void initialize(in ACString allHeaders);
+
+ /**
+ * Get the text of a header.
+ *
+ * Leading and trailing whitespace from headers will be stripped from the
+ * return value. If getAllOfThem is set to true, then the returned string will
+ * have all of the values of the header, in order, joined with the ',\r\n\t'.
+ *
+ * If the header is not present, then the returned value is NULL.
+ */
+ ACString extractHeader(in string headerName, in boolean getAllOfThem);
+
+ /**
+ * The current text of all header data.
+ *
+ * Unlike the asMimeText property, this result preserves the original
+ * representation of the header text, including alternative line endings or
+ * custom, non-8-bit text. For instances of this interface, this attribute is
+ * usually preferable to asMimeText.
+ */
+ readonly attribute ACString allHeaders;
+};
diff --git a/comm/mailnews/mime/public/nsIMimeObjectClassAccess.h b/comm/mailnews/mime/public/nsIMimeObjectClassAccess.h
new file mode 100644
index 0000000000..1ec2aa5a1f
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeObjectClassAccess.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This interface is implemented by libmime. This interface is used by
+ * a Content-Type handler "Plug In" (i.e. vCard) for accessing various
+ * internal information about the object class system of libmime. When
+ * libmime progresses to a C++ object class, this would probably change.
+ */
+#ifndef nsIMimeObjectClassAccess_h_
+#define nsIMimeObjectClassAccess_h_
+
+// {C09EDB23-B7AF-11d2-B35E-525400E2D63A}
+#define NS_IMIME_OBJECT_CLASS_ACCESS_IID \
+ { \
+ 0xc09edb23, 0xb7af, 0x11d2, { \
+ 0xb3, 0x5e, 0x52, 0x54, 0x0, 0xe2, 0xd6, 0x3a \
+ } \
+ }
+
+#include "nsISupports.h"
+
+class nsIMimeObjectClassAccess : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMIME_OBJECT_CLASS_ACCESS_IID)
+
+ // These methods are all implemented by libmime to be used by
+ // content type handler plugins for processing stream data.
+
+ // This is the write call for outputting processed stream data.
+ NS_IMETHOD MimeObjectWrite(void* mimeObject, char* data, int32_t length,
+ bool user_visible_p) = 0;
+
+ // The following group of calls expose the pointers for the object
+ // system within libmime.
+ NS_IMETHOD GetmimeInlineTextClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeLeafClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeObjectClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeContainerClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeMultipartClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeMultipartSignedClass(void** ptr) = 0;
+ NS_IMETHOD GetmimeEncryptedClass(void** ptr) = 0;
+
+ NS_IMETHOD MimeCreate(char* content_type, void* hdrs, void* opts,
+ void** ptr) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIMimeObjectClassAccess,
+ NS_IMIME_OBJECT_CLASS_ACCESS_IID)
+
+#endif /* nsIMimeObjectClassAccess_h_ */
diff --git a/comm/mailnews/mime/public/nsIMimeStreamConverter.idl b/comm/mailnews/mime/public/nsIMimeStreamConverter.idl
new file mode 100644
index 0000000000..b7fa35050e
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMimeStreamConverter.idl
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsrootidl.idl"
+#include "nsIMimeHeaders.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgHdr.idl"
+
+interface nsIURI;
+
+typedef long nsMimeOutputType;
+
+[scriptable, uuid(fdc2956e-d558-43fb-bfdd-fb9511229aa5)]
+interface nsMimeOutput : nsISupports
+{
+ const long nsMimeMessageSplitDisplay = 0;
+ const long nsMimeMessageHeaderDisplay = 1;
+ const long nsMimeMessageBodyDisplay = 2;
+ const long nsMimeMessageQuoting = 3;
+ const long nsMimeMessageBodyQuoting = 4;
+ const long nsMimeMessageRaw = 5;
+ const long nsMimeMessageDraftOrTemplate = 6;
+ const long nsMimeMessageEditorTemplate = 7;
+ const long nsMimeMessagePrintOutput = 9;
+ const long nsMimeMessageSaveAs = 10;
+ const long nsMimeMessageSource = 11;
+ const long nsMimeMessageFilterSniffer = 12;
+ const long nsMimeMessageDecrypt = 13;
+ const long nsMimeMessageAttach = 14;
+ const long nsMimeUnknown = 15;
+};
+
+[scriptable, uuid(FA81CAA0-6261-11d3-8311-00805F2A0107)]
+interface nsIMimeStreamConverterListener : nsISupports{
+ void onHeadersReady(in nsIMimeHeaders headers);
+};
+
+/**
+ * This interface contains mailnews mime specific information for stream
+ * converters. Most of the code is just stuff that has been moved out
+ * of nsIStreamConverter.idl to make it more generic.
+ */
+[scriptable, uuid(d894c833-29c5-495b-880c-9a9f847bfdc9)]
+interface nsIMimeStreamConverter : nsISupports {
+
+ /**
+ * Set the desired mime output type on the converer.
+ */
+ void SetMimeOutputType(in nsMimeOutputType aType);
+
+ void GetMimeOutputType(out nsMimeOutputType aOutFormat);
+
+ /**
+ * This is needed by libmime for MHTML link processing...the url is the URL
+ * string associated with this input stream.
+ */
+ void SetStreamURI(in nsIURI aURI);
+
+ /**
+ * Used to extract headers while parsing a message.
+ */
+ void SetMimeHeadersListener(in nsIMimeStreamConverterListener listener, in nsMimeOutputType aType);
+
+ /**
+ * This is used for forward inline, both as a filter action, and from the UI.
+ */
+ attribute boolean forwardInline;
+
+ /**
+ * This is used for a forward inline filter action. When streaming is done,
+ * we won't open a compose window with the editor contents.
+ */
+ attribute boolean forwardInlineFilter;
+
+ /**
+ * Address for the forward inline filter to forward the message to.
+ */
+ attribute AString forwardToAddress;
+ /**
+ * Use the opposite compose format, used for forward inline.
+ */
+ attribute boolean overrideComposeFormat;
+
+ /**
+ * This is used for OpenDraft, OpenEditorTemplate and Forward inline (which use OpenDraft)
+ */
+ attribute nsIMsgIdentity identity;
+ attribute AUTF8String originalMsgURI;
+ attribute nsIMsgDBHdr origMsgHdr;
+};
diff --git a/comm/mailnews/mime/public/nsIMsgHeaderParser.idl b/comm/mailnews/mime/public/nsIMsgHeaderParser.idl
new file mode 100644
index 0000000000..8064cb104a
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIMsgHeaderParser.idl
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#define NS_MAILNEWS_MIME_HEADER_PARSER_CONTRACTID \
+ "@mozilla.org/messenger/headerparser;1"
+%}
+
+/**
+ * A structured representation of an address.
+ *
+ * This is meant to correspond to the address production from RFC 5322. As a
+ * result, an instance of this interface is either a single mailbox or a group
+ * of mailboxes. The difference between the two forms is in which attribute is
+ * undefined: mailboxes leave the members attribute undefined while groups leave
+ * the email attribute undefined.
+ *
+ * For example, an address like "John Doe <jdoe@machine.example>" will, when
+ * parsed, result in an instance with the name attribute set to "John Doe", the
+ * email attribute set to "jdoe@machine.example", and the members variable left
+ * undefined.
+ *
+ * A group like "undisclosed-recipients:;" will, when parsed, result in an
+ * instance with the name attribute set to "undisclosed-recipients", the email
+ * attribute left defined, and the members variable set to an empty array.
+ *
+ * In general, the attributes of this interface are always meant to be in a form
+ * suitable for display purposes, and not in a form usable for MIME emission. In
+ * particular, email addresses could be fully internationalized and non-ASCII,
+ * RFC 2047-encoded words may appear in names, and the name or email parameters
+ * are unquoted.
+ */
+[scriptable, uuid(b19f5636-ebc4-470e-b46c-98b5fc7e88c9)]
+interface msgIAddressObject : nsISupports {
+ /// The name of the mailbox or group.
+ readonly attribute AString name;
+
+ /// The email of the mailbox.
+ readonly attribute AString email;
+
+ /**
+ * The member mailboxes of this group, which may be an empty list.
+ *
+ * Due to the limitations of XPIDL, the type of this attribute cannot be
+ * properly reflected. It is actually an array of msgIAddressObject instances,
+ * although it is instead undefined if this object does not represent a group.
+ */
+ readonly attribute jsval group;
+
+ /// Return a string form of this object that is suitable for display.
+ AString toString();
+};
+
+/**
+ * A utility service for manipulating addressing headers in email messages.
+ *
+ * This interface is designed primarily for use from JavaScript code; code in
+ * C++ should use the methods in MimeHeaderParser.h instead, as it is better
+ * designed to take advantage of C++'s features, particularly with respect to
+ * arrays.
+ *
+ * There are two methods for parsing MIME headers, one for RFC 2047-decoded
+ * strings, and one for non-RFC 2047-decoded strings.
+ *
+ * In general, this API attempts to preserve the format of addresses as
+ * faithfully as possible. No case normalization is performed at any point.
+ * However, internationalized email addresses generally need extra processing to
+ * work properly, so while this API should handle them without issues, consumers
+ * of this API may fail to work properly when presented with such addresses. To
+ * ease use for such cases, future versions of the API may incorporate necessary
+ * normalization steps to make oblivious consumers more likely to work properly.
+ */
+[scriptable, uuid(af2f9dd1-0226-4835-b981-a4f88b5e97cc)]
+interface nsIMsgHeaderParser : nsISupports {
+ /**
+ * Parse an address-based header that has not yet been 2047-decoded.
+ *
+ * The result of this method is an array of objects described in the above
+ * comment. Note that the header is a binary string that will be decoded as if
+ * passed into nsIMimeConverter.
+ *
+ * @param aEncodedHeader The RFC 2047-encoded header to parse.
+ * @param aHeaderCharset The charset to assume for raw octets.
+ * @param aPreserveGroups If false (the default), the result is a flat array
+ * of mailbox objects, containing no group objects.
+ * @return An array corresponding to the header description.
+ */
+ Array<msgIAddressObject> parseEncodedHeader(in ACString aEncodedHeader,
+ [optional] in string aHeaderCharset,
+ [optional] in bool aPreserveGroups);
+
+ /**
+ * Parse an address-based header that has not yet been 2047-decoded and does not
+ * contain raw octets but instead wide (UTF-16) characters.
+ *
+ * @param aEncodedHeader The RFC 2047-encoded header to parse.
+ * @return An array corresponding to the header description.
+ */
+ Array<msgIAddressObject> parseEncodedHeaderW(in AString aEncodedHeader);
+
+/**
+ * Parse an address-based header that has been 2047-decoded.
+ *
+ * The result of this method is an array of objects described in the above
+ * comment. Note that the header is a binary string that will be decoded as if
+ * passed into nsIMimeConverter.
+ *
+ * @param aDecodedHeader The non-RFC 2047-encoded header to parse.
+ * @param aPreserveGroups If false (the default), the result is a flat array
+ * of mailbox objects, containing no group objects.
+ * @return An array corresponding to the header description.
+ */
+ Array<msgIAddressObject> parseDecodedHeader(in AString aDecodedHeader,
+ [optional] in bool aPreserveGroups);
+
+ /**
+ * Given an array of addresses, make a MIME header suitable for emission.
+ *
+ * The return value of this method is not directly suitable for use in a MIME
+ * message but rather needs to be passed through nsIMimeConverter first to
+ * have RFC-2047 encoding applied and the resulting output wrapped to adhere
+ * to maximum line length formats.
+ *
+ * @param aAddresses An array corresponding to the header description.
+ * @return A string that is suitable for writing in a MIME message.
+ */
+ AString makeMimeHeader(in Array<msgIAddressObject> aAddresses);
+
+ /**
+ * Return the first address in the list in a format suitable for display.
+ *
+ * This is largely a convenience method for handling From headers (or similar),
+ * which are expected to only have a single element in them. It is exactly
+ * equivalent to saying (parseDecodedHeader(decodedHeader))[0].toString().
+ *
+ * @param decodedHeader The non-RFC 2047-encoded header to parse.
+ * @return The first address, suitable for display.
+ */
+ AString extractFirstName(in AString aDecodedHeader);
+
+ /**
+ * Returns a copy of the input which may have had some addresses removed.
+ * Addresses are removed if they are already in either of the supplied
+ * address lists.
+ *
+ * Addresses are considered to be the same if they contain the same email
+ * part (case-insensitive). Since the email part should never be RFC
+ * 2047-encoded, this method should work whether or not the header is
+ * RFC 2047-encoded.
+ *
+ * @param aAddrs The addresses to remove duplicates from.
+ * @param aOtherAddrs Other addresses that the duplicate removal process also
+ * checks for duplicates against. Addresses in this list
+ * will not be added to the result.
+ * @return The original header with duplicate addresses removed.
+ */
+ AUTF8String removeDuplicateAddresses(in AUTF8String aAddrs,
+ [optional] in AUTF8String aOtherAddrs);
+
+ /// Return a structured mailbox object having the given name and email.
+ msgIAddressObject makeMailboxObject(in AString aName, in AString aEmail);
+
+ /// Return a structured group object having the given name and members.
+ msgIAddressObject makeGroupObject(in AString aName,
+ in Array<msgIAddressObject> aMembers);
+
+ /**
+ * Return an array of structured mailbox objects for the given display name
+ * string.
+ *
+ * The string is expected to be a comma-separated sequence of strings that
+ * would be produced by msgIAddressObject::toString(). For example, the string
+ * "Bond, James <agent007@mi5.invalid>" would produce one address object,
+ * while the string "webmaster@nowhere.invalid, child@nowhere.invalid" would
+ * produce two address objects.
+ */
+ Array<msgIAddressObject> makeFromDisplayAddress(in AString aDisplayAddresses);
+
+ /**
+ * Given a string which contains a list of Header addresses, returns a
+ * comma-separated list of just the `mailbox' portions.
+ *
+ * @param aLine The header line to parse.
+ * @return A comma-separated list of just the mailbox parts
+ * of the email-addresses.
+ */
+ ACString extractHeaderAddressMailboxes(in ACString aLine);
+
+ /**
+ * Given a name and email address, produce a string that is suitable for
+ * emitting in a MIME header (after applying RFC 2047 encoding).
+ *
+ * @note This is a temporary method.
+ */
+ /* @deprecated */ AString makeMimeAddress(in AString aName, in AString aEmail);
+};
diff --git a/comm/mailnews/mime/public/nsIPgpMimeProxy.idl b/comm/mailnews/mime/public/nsIPgpMimeProxy.idl
new file mode 100644
index 0000000000..cd581bf107
--- /dev/null
+++ b/comm/mailnews/mime/public/nsIPgpMimeProxy.idl
@@ -0,0 +1,90 @@
+/* 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/. */
+
+#include "nsIStreamListener.idl"
+#include "nsIURI.idl"
+
+%{C++
+typedef int (*MimeDecodeCallbackFun)(const char *buf, int32_t buf_size, void *output_closure);
+
+#define NS_PGPMIMEPROXY_CLASSNAME "PGP/Mime Decryption"
+#define NS_PGPMIMEPROXY_CONTRACTID "@mozilla.org/mime/pgp-mime-decrypt;1"
+
+#define NS_PGPMIMEPROXY_CID \
+{ /* 815c4fbe-0e7c-45b6-8324-f7044c7252ac */ \
+ 0x815c4fbe, 0x0e7c, 0x45b6, \
+{0x83, 0x24, 0xf0, 0x04, 0x4C, 0x72, 0x52, 0xac } }
+%}
+
+native MimeDecodeCallbackFun(MimeDecodeCallbackFun);
+
+/**
+ * nsIPgpMimeProxy is a proxy for a (JS-)addon for OpenPGP/MIME decryption
+ */
+
+[scriptable, uuid(815c4fbe-0e7c-45b6-8324-f7044c7252ac)]
+interface nsIPgpMimeProxy : nsIStreamListener
+{
+ /**
+ * set the decoder callback into mimelib
+ */
+ [noscript] void setMimeCallback(in MimeDecodeCallbackFun outputFun,
+ in voidPtr outputClosure,
+ in nsIURI myUri);
+
+ [noscript] void removeMimeCallback();
+
+ /**
+ * init function
+ */
+ void init();
+
+ /**
+ * process encoded data received from mimelib
+ */
+ void write(in string buf, in unsigned long count);
+
+ /**
+ * finish writing (EOF) from mimelib
+ */
+ void finish();
+
+ /**
+ * the listener that receives the OpenPGP/MIME data stream and decrypts
+ * the message
+ */
+ attribute nsIStreamListener decryptor;
+
+ attribute ACString contentType;
+
+ /**
+ * holds the URI of the message currently being processed
+ */
+ readonly attribute nsIURI messageURI;
+
+ /**
+ * The particular part number of the multipart object we are working on. The
+ * numbering is the same as in URLs that use the form "...?part=1.1.2".
+ *
+ * The value stored in mimePart is only the number, e.g. "1" or "1.1.2"
+ */
+ attribute ACString mimePart;
+
+ /**
+ * The application may restrict automatic decryption to the top level
+ * MIME part ("1"). In certain scenarios the application may allow
+ * a MIME part at another level ("nested") in the MIME tree to be
+ * decrypted, too. If the current MIME part is a nested MIME part,
+ * then this flag indicates whether automatic decryption is allowed.
+ */
+ attribute boolean allowNestedDecrypt;
+
+ /**
+ * Pass the decrypted data back from the decryptor and onto to libMime.
+ */
+ void outputDecryptedData(in string buf, in unsigned long count);
+};
+
+
+///////////////////////////////////////////////////////////////////////////////
diff --git a/comm/mailnews/mime/public/nsISimpleMimeConverter.idl b/comm/mailnews/mime/public/nsISimpleMimeConverter.idl
new file mode 100644
index 0000000000..b6c7027971
--- /dev/null
+++ b/comm/mailnews/mime/public/nsISimpleMimeConverter.idl
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+interface nsIMailChannel;
+interface nsIURI;
+
+/**
+ * nsISimpleMimeConverter provides an interface for rendering raw mime objects
+ * as HTML. It's used to provide converters for mime types not handled
+ * directly by the mime code.
+ * "text/calendar" - see calendar/base/src/CalMimeConverter.jsm
+ * "text/vcard" - see mailnews/addrbook/modules/VCardUtils.jsm
+ */
+[scriptable, uuid(FC6E8234-BBF3-44A1-9802-5F023A929173)]
+interface nsISimpleMimeConverter : nsISupports
+{
+ // uri of message getting displayed
+ attribute nsIURI uri;
+
+ // mailChannel of message getting displayed
+ attribute nsIMailChannel mailChannel;
+
+ /**
+ * Render mime data into HTML.
+ * NOTE: it is important that this function doesn't do anything which
+ * would allows other events to processed on the thread (that means
+ * calls to NS_ProcessNextEvent()). Using a synchronous XMLHttpRequest
+ * is a prime example - it spins the thread queue, processing other
+ * events while it waits.
+ *
+ * It's an issue because it's likely that convertToHTML() is being called
+ * in response to data coming in from an async inputstream. Letting other
+ * events be handled on the thread means that more data might come in
+ * from the stream, recursively calling the nsIStreamListener
+ * onDataAvailable() handler we're already inside! And it's tricky to
+ * track down this kind of problem - it only occurs if the data is big
+ * enough that it comes through in multiple chunks.
+ * See bug 1679299.
+ */
+ AUTF8String convertToHTML(in ACString contentType,
+ in AUTF8String data);
+};
+
+%{C++
+
+#define NS_SIMPLEMIMECONVERTERS_CATEGORY "simple-mime-converters"
+
+%}
diff --git a/comm/mailnews/mime/public/nsMailHeaders.h b/comm/mailnews/mime/public/nsMailHeaders.h
new file mode 100644
index 0000000000..29313eb6c8
--- /dev/null
+++ b/comm/mailnews/mime/public/nsMailHeaders.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This interface allows any module to access the encoder/decoder
+ * routines for RFC822 headers. This will allow any mail/news module
+ * to call on these routines.
+ */
+#ifndef nsMailHeaders_h_
+#define nsMailHeaders_h_
+
+/*
+ * These are the defines for standard header field names.
+ */
+#define HEADER_BCC "BCC"
+#define HEADER_CC "CC"
+#define HEADER_CONTENT_BASE "Content-Base"
+#define HEADER_CONTENT_LOCATION "Content-Location"
+#define HEADER_CONTENT_ID "Content-ID"
+#define HEADER_CONTENT_DESCRIPTION "Content-Description"
+#define HEADER_CONTENT_DISPOSITION "Content-Disposition"
+#define HEADER_CONTENT_ENCODING "Content-Encoding"
+#define HEADER_CONTENT_LANGUAGE "Content-Language"
+#define HEADER_CONTENT_LENGTH "Content-Length"
+#define HEADER_CONTENT_NAME "Content-Name"
+#define HEADER_CONTENT_TRANSFER_ENCODING "Content-Transfer-Encoding"
+#define HEADER_CONTENT_TYPE "Content-Type"
+#define HEADER_DATE "Date"
+#define HEADER_DISTRIBUTION "Distribution"
+#define HEADER_FCC "FCC"
+#define HEADER_FOLLOWUP_TO "Followup-To"
+#define HEADER_FROM "From"
+#define HEADER_STATUS "Status"
+#define HEADER_LINES "Lines"
+#define HEADER_LIST_POST "List-Post"
+#define HEADER_MAIL_FOLLOWUP_TO "Mail-Followup-To"
+#define HEADER_MAIL_REPLY_TO "Mail-Reply-To"
+#define HEADER_MESSAGE_ID "Message-ID"
+#define HEADER_MIME_VERSION "MIME-Version"
+#define HEADER_NEWSGROUPS "Newsgroups"
+#define HEADER_ORGANIZATION "Organization"
+#define HEADER_REFERENCES "References"
+#define HEADER_REPLY_TO "Reply-To"
+#define HEADER_RESENT_COMMENTS "Resent-Comments"
+#define HEADER_RESENT_DATE "Resent-Date"
+#define HEADER_RESENT_FROM "Resent-From"
+#define HEADER_RESENT_MESSAGE_ID "Resent-Message-ID"
+#define HEADER_RESENT_SENDER "Resent-Sender"
+#define HEADER_RESENT_TO "Resent-To"
+#define HEADER_RESENT_CC "Resent-CC"
+#define HEADER_SENDER "Sender"
+#define HEADER_SUBJECT "Subject"
+#define HEADER_TO "To"
+#define HEADER_APPROVED_BY "Approved-By"
+#define HEADER_X_MAILER "X-Mailer"
+#define HEADER_USER_AGENT "User-Agent"
+#define HEADER_X_NEWSREADER "X-Newsreader"
+#define HEADER_X_POSTING_SOFTWARE "X-Posting-Software"
+#define HEADER_X_MOZILLA_STATUS "X-Mozilla-Status"
+#define HEADER_X_MOZILLA_STATUS2 "X-Mozilla-Status2"
+#define HEADER_X_MOZILLA_NEWSHOST "X-Mozilla-News-Host"
+#define HEADER_X_MOZILLA_DRAFT_INFO "X-Mozilla-Draft-Info"
+#define HEADER_X_UIDL "X-UIDL"
+#define HEADER_XREF "XREF"
+#define HEADER_X_SUN_CHARSET "X-Sun-Charset"
+#define HEADER_X_SUN_CONTENT_LENGTH "X-Sun-Content-Length"
+#define HEADER_X_SUN_CONTENT_LINES "X-Sun-Content-Lines"
+#define HEADER_X_SUN_DATA_DESCRIPTION "X-Sun-Data-Description"
+#define HEADER_X_SUN_DATA_NAME "X-Sun-Data-Name"
+#define HEADER_X_SUN_DATA_TYPE "X-Sun-Data-Type"
+#define HEADER_X_SUN_ENCODING_INFO "X-Sun-Encoding-Info"
+#define HEADER_X_PRIORITY "X-Priority"
+
+#define HEADER_PARM_CHARSET "charset"
+#define HEADER_PARM_START "start"
+#define HEADER_PARM_BOUNDARY "BOUNDARY"
+#define HEADER_PARM_FILENAME "FILENAME"
+#define HEADER_PARM_NAME "NAME"
+#define HEADER_PARM_TYPE "TYPE"
+
+#define HEADER_X_MOZILLA_PART_URL "X-Mozilla-PartURL"
+#define HEADER_X_MOZILLA_PART_SIZE "X-Mozilla-PartSize"
+#define HEADER_X_MOZILLA_PART_DOWNLOADED "X-Mozilla-PartDownloaded"
+#define HEADER_X_MOZILLA_CLOUD_PART "X-Mozilla-Cloud-Part"
+#define HEADER_X_MOZILLA_IDENTITY_KEY "X-Identity-Key"
+#define HEADER_X_MOZILLA_ACCOUNT_KEY "X-Account-Key"
+#define HEADER_X_MOZILLA_KEYWORDS "X-Mozilla-Keys"
+#endif /* nsMailHeaders_h_ */
diff --git a/comm/mailnews/mime/src/MimeHeaderParser.cpp b/comm/mailnews/mime/src/MimeHeaderParser.cpp
new file mode 100644
index 0000000000..cc489e2ec5
--- /dev/null
+++ b/comm/mailnews/mime/src/MimeHeaderParser.cpp
@@ -0,0 +1,232 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/DebugOnly.h"
+#include "nsMemory.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgHeaderParser.h"
+#include "mozilla/Components.h"
+
+namespace mozilla {
+namespace mailnews {
+
+void detail::DoConversion(const nsTArray<nsString>& aUTF16Array,
+ nsTArray<nsCString>& aUTF8Array) {
+ uint32_t count = aUTF16Array.Length();
+ aUTF8Array.SetLength(count);
+ for (uint32_t i = 0; i < count; ++i)
+ CopyUTF16toUTF8(aUTF16Array[i], aUTF8Array[i]);
+}
+
+void MakeMimeAddress(const nsACString& aName, const nsACString& aEmail,
+ nsACString& full) {
+ nsAutoString utf16Address;
+ MakeMimeAddress(NS_ConvertUTF8toUTF16(aName), NS_ConvertUTF8toUTF16(aEmail),
+ utf16Address);
+
+ CopyUTF16toUTF8(utf16Address, full);
+}
+
+void MakeMimeAddress(const nsAString& aName, const nsAString& aEmail,
+ nsAString& full) {
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+
+ nsCOMPtr<msgIAddressObject> address;
+ headerParser->MakeMailboxObject(aName, aEmail, getter_AddRefs(address));
+ nsTArray<RefPtr<msgIAddressObject>> addresses;
+ addresses.AppendElement(address);
+ headerParser->MakeMimeHeader(addresses, full);
+}
+
+void MakeDisplayAddress(const nsAString& aName, const nsAString& aEmail,
+ nsAString& full) {
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+
+ nsCOMPtr<msgIAddressObject> object;
+ headerParser->MakeMailboxObject(aName, aEmail, getter_AddRefs(object));
+ object->ToString(full);
+}
+
+void RemoveDuplicateAddresses(const nsACString& aHeader,
+ const nsACString& aOtherEmails,
+ nsACString& result) {
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+
+ headerParser->RemoveDuplicateAddresses(aHeader, aOtherEmails, result);
+}
+
+/////////////////////////////////////////////
+// These are the core shim methods we need //
+/////////////////////////////////////////////
+
+nsCOMArray<msgIAddressObject> DecodedHeader(const nsAString& aHeader) {
+ nsCOMArray<msgIAddressObject> retval;
+ if (aHeader.IsEmpty()) {
+ return retval;
+ }
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+ NS_ENSURE_TRUE(headerParser, retval);
+ nsTArray<RefPtr<msgIAddressObject>> addresses;
+ nsresult rv = headerParser->ParseDecodedHeader(aHeader, false, addresses);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Javascript jsmime returned an error!");
+ if (NS_SUCCEEDED(rv) && addresses.Length() > 0) {
+ retval.SetCapacity(addresses.Length());
+ for (auto& addr : addresses) {
+ retval.AppendElement(addr);
+ }
+ }
+ return retval;
+}
+
+nsCOMArray<msgIAddressObject> EncodedHeader(const nsACString& aHeader,
+ const char* aCharset) {
+ nsCOMArray<msgIAddressObject> retval;
+ if (aHeader.IsEmpty()) {
+ return retval;
+ }
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+ NS_ENSURE_TRUE(headerParser, retval);
+ nsTArray<RefPtr<msgIAddressObject>> addresses;
+ nsresult rv =
+ headerParser->ParseEncodedHeader(aHeader, aCharset, false, addresses);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "This should never fail!");
+ if (NS_SUCCEEDED(rv) && addresses.Length() > 0) {
+ retval.SetCapacity(addresses.Length());
+ for (auto& addr : addresses) {
+ retval.AppendElement(addr);
+ }
+ }
+ return retval;
+}
+
+nsCOMArray<msgIAddressObject> EncodedHeaderW(const nsAString& aHeader) {
+ nsCOMArray<msgIAddressObject> retval;
+ if (aHeader.IsEmpty()) {
+ return retval;
+ }
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ components::HeaderParser::Service());
+ NS_ENSURE_TRUE(headerParser, retval);
+ nsTArray<RefPtr<msgIAddressObject>> addresses;
+ nsresult rv = headerParser->ParseEncodedHeaderW(aHeader, addresses);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "This should never fail!");
+ if (NS_SUCCEEDED(rv) && addresses.Length() > 0) {
+ retval.SetCapacity(addresses.Length());
+ for (auto& addr : addresses) {
+ retval.AppendElement(addr);
+ }
+ }
+ return retval;
+}
+
+void ExtractAllAddresses(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& names,
+ nsTArray<nsString>& emails) {
+ uint32_t count = aHeader.Length();
+
+ // Prefill arrays before we start
+ names.SetLength(count);
+ emails.SetLength(count);
+
+ for (uint32_t i = 0; i < count; i++) {
+ aHeader[i]->GetName(names[i]);
+ aHeader[i]->GetEmail(emails[i]);
+ }
+
+ if (count == 1 && names[0].IsEmpty() && emails[0].IsEmpty()) {
+ names.Clear();
+ emails.Clear();
+ }
+}
+
+void ExtractDisplayAddresses(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& displayAddrs) {
+ uint32_t count = aHeader.Length();
+
+ displayAddrs.SetLength(count);
+ for (uint32_t i = 0; i < count; i++) aHeader[i]->ToString(displayAddrs[i]);
+
+ if (count == 1 && displayAddrs[0].IsEmpty()) displayAddrs.Clear();
+}
+
+/////////////////////////////////////////////////
+// All of these are based on the above methods //
+/////////////////////////////////////////////////
+
+void ExtractEmails(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsTArray<nsString>& emails) {
+ nsTArray<nsString> names;
+ ExtractAllAddresses(aHeader, names, emails);
+}
+
+void ExtractEmail(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& email) {
+ AutoTArray<nsString, 1> names;
+ AutoTArray<nsString, 1> emails;
+ ExtractAllAddresses(aHeader, names, emails);
+
+ if (emails.Length() > 0)
+ CopyUTF16toUTF8(emails[0], email);
+ else
+ email.Truncate();
+}
+
+void ExtractFirstAddress(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& name, nsACString& email) {
+ AutoTArray<nsString, 1> names, emails;
+ ExtractAllAddresses(aHeader, names, emails);
+ if (names.Length() > 0) {
+ CopyUTF16toUTF8(names[0], name);
+ CopyUTF16toUTF8(emails[0], email);
+ } else {
+ name.Truncate();
+ email.Truncate();
+ }
+}
+
+void ExtractFirstAddress(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsAString& name, nsACString& email) {
+ AutoTArray<nsString, 1> names, emails;
+ ExtractAllAddresses(aHeader, names, emails);
+ if (names.Length() > 0) {
+ name = names[0];
+ CopyUTF16toUTF8(emails[0], email);
+ } else {
+ name.Truncate();
+ email.Truncate();
+ }
+}
+
+void ExtractName(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsACString& name) {
+ nsCString email;
+ ExtractFirstAddress(aHeader, name, email);
+ if (name.IsEmpty()) name = email;
+}
+
+void ExtractName(const nsCOMArray<msgIAddressObject>& aHeader,
+ nsAString& name) {
+ AutoTArray<nsString, 1> names;
+ AutoTArray<nsString, 1> emails;
+ ExtractAllAddresses(aHeader, names, emails);
+ if (names.Length() > 0) {
+ if (names[0].IsEmpty())
+ name = emails[0];
+ else
+ name = names[0];
+ } else {
+ name.Truncate();
+ }
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/mime/src/MimeJSComponents.jsm b/comm/mailnews/mime/src/MimeJSComponents.jsm
new file mode 100644
index 0000000000..35acba7c26
--- /dev/null
+++ b/comm/mailnews/mime/src/MimeJSComponents.jsm
@@ -0,0 +1,547 @@
+/* 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 EXPORTED_SYMBOLS = [
+ "MimeHeaders",
+ "MimeWritableStructuredHeaders",
+ "MimeAddressParser",
+ "MimeConverter",
+];
+
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+function HeaderHandler() {
+ this.value = "";
+ this.deliverData = function (str) {
+ this.value += str;
+ };
+ this.deliverEOF = function () {};
+}
+
+function StringEnumerator(iterator) {
+ this._iterator = iterator;
+ this._next = undefined;
+}
+StringEnumerator.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIUTF8StringEnumerator"]),
+ [Symbol.iterator]() {
+ return this._iterator;
+ },
+ _setNext() {
+ if (this._next !== undefined) {
+ return;
+ }
+ this._next = this._iterator.next();
+ },
+ hasMore() {
+ this._setNext();
+ return !this._next.done;
+ },
+ getNext() {
+ this._setNext();
+ let result = this._next;
+ this._next = undefined;
+ if (result.done) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ return result.value;
+ },
+};
+
+/**
+ * If we get XPConnect-wrapped objects for msgIAddressObjects, we will have
+ * properties defined for 'group' that throws off jsmime. This function converts
+ * the addresses into the form that jsmime expects.
+ */
+function fixXpconnectAddresses(addrs) {
+ return addrs.map(addr => {
+ // This is ideally !addr.group, but that causes a JS strict warning, if
+ // group is not in addr, since that's enabled in all chrome code now.
+ if (!("group" in addr) || addr.group === undefined || addr.group === null) {
+ return MimeAddressParser.prototype.makeMailboxObject(
+ addr.name,
+ addr.email
+ );
+ }
+ return MimeAddressParser.prototype.makeGroupObject(
+ addr.name,
+ fixXpconnectAddresses(addr.group)
+ );
+ });
+}
+
+/**
+ * This is a base handler for supporting msgIStructuredHeaders, since we have
+ * two implementations that need the readable aspects of the interface.
+ */
+function MimeStructuredHeaders() {}
+MimeStructuredHeaders.prototype = {
+ getHeader(aHeaderName) {
+ let name = aHeaderName.toLowerCase();
+ return this._headers.get(name);
+ },
+
+ hasHeader(aHeaderName) {
+ return this._headers.has(aHeaderName.toLowerCase());
+ },
+
+ getUnstructuredHeader(aHeaderName) {
+ let result = this.getHeader(aHeaderName);
+ if (result === undefined || typeof result == "string") {
+ return result;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ },
+
+ getAddressingHeader(aHeaderName, aPreserveGroups) {
+ let addrs = this.getHeader(aHeaderName);
+ if (addrs === undefined) {
+ addrs = [];
+ } else if (!Array.isArray(addrs)) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ return fixArray(addrs, aPreserveGroups);
+ },
+
+ getRawHeader(aHeaderName) {
+ let result = this.getHeader(aHeaderName);
+ if (result === undefined) {
+ return result;
+ }
+
+ let value = jsmime.headeremitter.emitStructuredHeader(
+ aHeaderName,
+ result,
+ {}
+ );
+ // Strip off the header name and trailing whitespace before returning...
+ value = value.substring(aHeaderName.length + 2).trim();
+ // ... as well as embedded newlines.
+ value = value.replace(/\r\n/g, "");
+ return value;
+ },
+
+ get headerNames() {
+ return new StringEnumerator(this._headers.keys());
+ },
+
+ buildMimeText(sanitizeDate) {
+ if (this._headers.size == 0) {
+ return "";
+ }
+ let handler = new HeaderHandler();
+ let emitter = jsmime.headeremitter.makeStreamingEmitter(handler, {
+ useASCII: true,
+ sanitizeDate,
+ });
+ for (let [value, header] of this._headers) {
+ emitter.addStructuredHeader(value, header);
+ }
+ emitter.finish();
+ return handler.value;
+ },
+};
+
+function MimeHeaders() {}
+MimeHeaders.prototype = {
+ __proto__: MimeStructuredHeaders.prototype,
+ classDescription: "Mime headers implementation",
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMimeHeaders",
+ "msgIStructuredHeaders",
+ ]),
+
+ initialize(allHeaders) {
+ this._headers = MimeParser.extractHeaders(allHeaders);
+ },
+
+ extractHeader(header, getAll) {
+ if (!this._headers) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ // Canonicalized to lower-case form
+ header = header.toLowerCase();
+ if (!this._headers.has(header)) {
+ return null;
+ }
+ var values = this._headers.getRawHeader(header);
+ if (getAll) {
+ return values.join(",\r\n\t");
+ }
+ return values[0];
+ },
+
+ get allHeaders() {
+ return this._headers.rawHeaderText;
+ },
+};
+
+function MimeWritableStructuredHeaders() {
+ this._headers = new Map();
+}
+MimeWritableStructuredHeaders.prototype = {
+ __proto__: MimeStructuredHeaders.prototype,
+ QueryInterface: ChromeUtils.generateQI([
+ "msgIStructuredHeaders",
+ "msgIWritableStructuredHeaders",
+ ]),
+
+ setHeader(aHeaderName, aValue) {
+ this._headers.set(aHeaderName.toLowerCase(), aValue);
+ },
+
+ deleteHeader(aHeaderName) {
+ this._headers.delete(aHeaderName.toLowerCase());
+ },
+
+ addAllHeaders(aHeaders) {
+ for (let header of aHeaders.headerNames) {
+ this.setHeader(header, aHeaders.getHeader(header));
+ }
+ },
+
+ setUnstructuredHeader(aHeaderName, aValue) {
+ this.setHeader(aHeaderName, aValue);
+ },
+
+ setAddressingHeader(aHeaderName, aAddresses) {
+ this.setHeader(aHeaderName, fixXpconnectAddresses(aAddresses));
+ },
+
+ setRawHeader(aHeaderName, aValue) {
+ try {
+ this.setHeader(
+ aHeaderName,
+ jsmime.headerparser.parseStructuredHeader(aHeaderName, aValue)
+ );
+ } catch (e) {
+ // This means we don't have a structured encoder. Just assume it's a raw
+ // string value then.
+ this.setHeader(aHeaderName, aValue.trim());
+ }
+ },
+};
+
+// These are prototypes for nsIMsgHeaderParser implementation
+var Mailbox = {
+ toString() {
+ return this.name ? this.name + " <" + this.email + ">" : this.email;
+ },
+};
+
+var EmailGroup = {
+ toString() {
+ return this.name + ": " + this.group.map(x => x.toString()).join(", ");
+ },
+};
+
+// A helper method for parse*Header that takes into account the desire to
+// preserve group and also tweaks the output to support the prototypes for the
+// XPIDL output.
+function fixArray(addresses, preserveGroups, count) {
+ function resetPrototype(obj, prototype) {
+ let prototyped = Object.create(prototype);
+ for (let key of Object.getOwnPropertyNames(obj)) {
+ if (typeof obj[key] == "string") {
+ // eslint-disable-next-line no-control-regex
+ prototyped[key] = obj[key].replace(/\x00/g, "");
+ } else {
+ prototyped[key] = obj[key];
+ }
+ }
+ return prototyped;
+ }
+ let outputArray = [];
+ for (let element of addresses) {
+ if ("group" in element) {
+ // Fix up the prototypes of the group and the list members
+ element = resetPrototype(element, EmailGroup);
+ element.group = element.group.map(e => resetPrototype(e, Mailbox));
+
+ // Add to the output array
+ if (preserveGroups) {
+ outputArray.push(element);
+ } else {
+ outputArray = outputArray.concat(element.group);
+ }
+ } else {
+ element = resetPrototype(element, Mailbox);
+ outputArray.push(element);
+ }
+ }
+
+ if (count) {
+ count.value = outputArray.length;
+ }
+ return outputArray;
+}
+
+function MimeAddressParser() {}
+MimeAddressParser.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgHeaderParser"]),
+
+ parseEncodedHeader(aHeader, aCharset, aPreserveGroups) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_ALL_I18N,
+ aCharset
+ );
+ return fixArray(value, aPreserveGroups);
+ },
+ parseEncodedHeaderW(aHeader) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS |
+ MimeParser.HEADER_OPTION_DECODE_2231 |
+ MimeParser.HEADER_OPTION_DECODE_2047,
+ undefined
+ );
+ return fixArray(value, false);
+ },
+ parseDecodedHeader(aHeader, aPreserveGroups) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(aHeader, MimeParser.HEADER_ADDRESS);
+ return fixArray(value, aPreserveGroups);
+ },
+
+ makeMimeHeader(addresses) {
+ addresses = fixXpconnectAddresses(addresses);
+ // Don't output any necessary continuations, so make line length as large as
+ // possible first.
+ let options = {
+ softMargin: 900,
+ hardMargin: 900,
+ useASCII: false, // We don't want RFC 2047 encoding here.
+ };
+ let handler = new HeaderHandler();
+ let emitter = new jsmime.headeremitter.makeStreamingEmitter(
+ handler,
+ options
+ );
+ emitter.addAddresses(addresses);
+ emitter.finish(true);
+ return handler.value.replace(/\r\n( |$)/g, "");
+ },
+
+ extractFirstName(aHeader) {
+ let addresses = this.parseDecodedHeader(aHeader, false);
+ return addresses.length > 0 ? addresses[0].name || addresses[0].email : "";
+ },
+
+ removeDuplicateAddresses(aAddrs, aOtherAddrs) {
+ // This is actually a rather complicated algorithm, especially if we want to
+ // preserve group structure. Basically, we use a set to identify which
+ // headers we have seen and therefore want to remove. To work in several
+ // various forms of edge cases, we need to normalize the entries in that
+ // structure.
+ function normalize(email) {
+ // XXX: This algorithm doesn't work with IDN yet. It looks like we have to
+ // convert from IDN then do lower case, but I haven't confirmed yet.
+ return email.toLowerCase();
+ }
+
+ // The filtration function, which removes email addresses that are
+ // duplicates of those we have already seen.
+ function filterAccept(e) {
+ if ("email" in e) {
+ // If we've seen the address, don't keep this one; otherwise, add it to
+ // the list.
+ let key = normalize(e.email);
+ if (allAddresses.has(key)) {
+ return false;
+ }
+ allAddresses.add(key);
+ } else {
+ // Groups -> filter out all the member addresses.
+ e.group = e.group.filter(filterAccept);
+ }
+ return true;
+ }
+
+ // First, collect all of the emails to forcibly delete.
+ let allAddresses = new Set();
+ for (let element of this.parseDecodedHeader(aOtherAddrs, false)) {
+ allAddresses.add(normalize(element.email));
+ }
+
+ // The actual data to filter
+ let filtered = this.parseDecodedHeader(aAddrs, true).filter(filterAccept);
+ return this.makeMimeHeader(filtered);
+ },
+
+ makeMailboxObject(aName, aEmail) {
+ let object = Object.create(Mailbox);
+ object.name = aName;
+ object.email = aEmail ? aEmail.trim() : aEmail;
+ return object;
+ },
+
+ makeGroupObject(aName, aMembers) {
+ let object = Object.create(EmailGroup);
+ object.name = aName;
+ object.group = aMembers;
+ return object;
+ },
+
+ makeFromDisplayAddress(aDisplay) {
+ if (aDisplay.includes(";") && !/:.*;/.test(aDisplay)) {
+ // Using semicolons as mailbox separators in against the standard, but
+ // used in the wild by some clients.
+ // Looks like this isn't using group syntax, so let's assume it's a
+ // non-standards compliant input string, and fix it.
+ // Replace semicolons with commas, unless the semicolon is inside a quote.
+ // The regexp uses tricky lookahead, see bug 1059988 comment #70 for details.
+ aDisplay = aDisplay.replace(/;(?=(?:(?:[^"]*"){2})*[^"]*$)/g, ",");
+ }
+
+ // The basic idea is to split on every comma, so long as there is a
+ // preceding @ or <> pair.
+ let output = [];
+ while (aDisplay.length > 0) {
+ let lt = aDisplay.indexOf("<");
+ let gt = aDisplay.indexOf(">");
+ let at = aDisplay.indexOf("@");
+ let start = 0;
+ // An address doesn't always contain both <> and @, the goal is to find
+ // the first comma after <> or @.
+ if (lt != -1 && gt > lt) {
+ start = gt;
+ }
+ if (at != -1) {
+ start = Math.min(start, at);
+ }
+ let comma = aDisplay.indexOf(",", start);
+ let addr;
+ if (comma > 0) {
+ addr = aDisplay.substr(0, comma);
+ aDisplay = aDisplay.substr(comma + 1);
+
+ // Make sure we don't have any "empty addresses" (multiple commas).
+ comma = 0;
+ while (/[,\s]/.test(aDisplay.charAt(comma))) {
+ comma++;
+ }
+ aDisplay = aDisplay.substr(comma);
+ } else {
+ addr = aDisplay;
+ aDisplay = "";
+ }
+ addr = addr.trimLeft();
+ if (addr) {
+ output.push(this._makeSingleAddress(addr));
+ }
+ }
+ return output;
+ },
+
+ /**
+ * Construct a single email address from an |name <local@domain>| token.
+ *
+ * @param {string} aInput - a string to be parsed to a mailbox object.
+ * @returns {msgIAddressObject} the mailbox parsed from the input.
+ */
+ _makeSingleAddress(aInput) {
+ // If the whole string is within quotes, unquote it first.
+ aInput = aInput.trim().replace(/^"(.*)"$/, "$1");
+
+ if (/<.*>/.test(aInput)) {
+ // We don't want to look for the address within quotes, so first remove
+ // all quoted strings containing angle chars.
+ let cleanedInput = aInput.replace(/".*[<>]+.*"/g, "");
+
+ // Extract the address from within the quotes.
+ let addrMatch = cleanedInput.match(/<([^><]*)>/);
+
+ let addr = addrMatch ? addrMatch[1] : "";
+ let addrIdx = aInput.indexOf("<" + addr + ">");
+ return this.makeMailboxObject(aInput.slice(0, addrIdx).trim(), addr);
+ }
+ return this.makeMailboxObject("", aInput);
+ },
+
+ extractHeaderAddressMailboxes(aLine) {
+ return this.parseDecodedHeader(aLine)
+ .map(addr => addr.email)
+ .join(", ");
+ },
+
+ makeMimeAddress(aName, aEmail) {
+ let object = this.makeMailboxObject(aName, aEmail);
+ return this.makeMimeHeader([object]);
+ },
+};
+
+function MimeConverter() {}
+MimeConverter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMimeConverter"]),
+
+ encodeMimePartIIStr_UTF8(aHeader, aStructured, aFieldNameLen, aLineLength) {
+ // Compute the encoding options. The way our API is structured in this
+ // method is really horrendous and does not align with the way that JSMime
+ // handles it. Instead, we'll need to create a fake header to take into
+ // account the aFieldNameLen parameter.
+ let fakeHeader = "-".repeat(aFieldNameLen);
+ let options = {
+ softMargin: aLineLength,
+ useASCII: true,
+ };
+ let handler = new HeaderHandler();
+ let emitter = new jsmime.headeremitter.makeStreamingEmitter(
+ handler,
+ options
+ );
+
+ // Add the text to the be encoded.
+ emitter.addHeaderName(fakeHeader);
+ if (aStructured) {
+ // Structured really means "this is an addressing header"
+ let addresses = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_DECODE_2047
+ );
+ // This happens in one of our tests if there is a "bare" email but no
+ // @ sign. Without it, the result disappears since our emission code
+ // assumes that an empty email is not worth emitting.
+ if (
+ addresses.length === 1 &&
+ addresses[0].email === "" &&
+ addresses[0].name !== ""
+ ) {
+ addresses[0].email = addresses[0].name;
+ addresses[0].name = "";
+ }
+ emitter.addAddresses(addresses);
+ } else {
+ emitter.addUnstructured(aHeader);
+ }
+
+ // Compute the output. We need to strip off the fake prefix added earlier
+ // and the extra CRLF at the end.
+ emitter.finish(true);
+ let value = handler.value;
+ value = value.replace(new RegExp(fakeHeader + ":\\s*"), "");
+ return value.substring(0, value.length - 2);
+ },
+
+ decodeMimeHeader(aHeader, aDefaultCharset, aOverride, aUnfold) {
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_UNSTRUCTURED | MimeParser.HEADER_OPTION_ALL_I18N,
+ aDefaultCharset
+ );
+ if (aUnfold) {
+ value = value.replace(/[\r\n]\t/g, " ").replace(/[\r\n]/g, "");
+ }
+ return value;
+ },
+
+ // This is identical to the above, except for factors that are handled by the
+ // xpconnect conversion process
+ decodeMimeHeaderToUTF8(...aArgs) {
+ return this.decodeMimeHeader(...aArgs);
+ },
+};
diff --git a/comm/mailnews/mime/src/comi18n.cpp b/comm/mailnews/mime/src/comi18n.cpp
new file mode 100644
index 0000000000..90bfd17abf
--- /dev/null
+++ b/comm/mailnews/mime/src/comi18n.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "comi18n.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIMimeConverter.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/EncodingDetector.h"
+
+using namespace mozilla;
+
+////////////////////////////////////////////////////////////////////////////////
+// BEGIN PUBLIC INTERFACE
+extern "C" {
+
+void MIME_DecodeMimeHeader(const char* header, const char* default_charset,
+ bool override_charset, bool eatContinuations,
+ nsACString& result) {
+ nsresult rv;
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ if (NS_FAILED(rv)) {
+ result.Truncate();
+ return;
+ }
+ mimeConverter->DecodeMimeHeaderToUTF8(nsDependentCString(header),
+ default_charset, override_charset,
+ eatContinuations, result);
+}
+
+nsresult MIME_detect_charset(const char* aBuf, int32_t aLength,
+ nsACString& aCharset) {
+ mozilla::UniquePtr<mozilla::EncodingDetector> detector =
+ mozilla::EncodingDetector::Create();
+ mozilla::Span<const uint8_t> src =
+ mozilla::AsBytes(mozilla::Span(aBuf, aLength));
+ Unused << detector->Feed(src, true);
+ auto encoding = detector->Guess(nullptr, true);
+ encoding->Name(aCharset);
+ return NS_OK;
+}
+
+} /* end of extern "C" */
+// END PUBLIC INTERFACE
diff --git a/comm/mailnews/mime/src/comi18n.h b/comm/mailnews/mime/src/comi18n.h
new file mode 100644
index 0000000000..0c9f7cbaf7
--- /dev/null
+++ b/comm/mailnews/mime/src/comi18n.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef _COMI18N_LOADED_H_
+#define _COMI18N_LOADED_H_
+
+#include "msgCore.h"
+#include "mozilla/Encoding.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/**
+ * Decode MIME header to UTF-8.
+ * Uses MIME_ConvertCharset if the decoded string needs a conversion.
+ *
+ *
+ * @param header [IN] A header to decode.
+ * @param default_charset [IN] Default charset to apply to ulabeled
+ * non-UTF-8 8bit data
+ * @param override_charset [IN] If true, default_charset used instead of any
+ * charset labeling other than UTF-8
+ * @param eatContinuations [IN] If true, unfold headers
+ * @param result [OUT] Decoded buffer
+ */
+void MIME_DecodeMimeHeader(const char* header, const char* default_charset,
+ bool override_charset, bool eatContinuations,
+ nsACString& result);
+
+nsresult MIME_detect_charset(const char* aBuf, int32_t aLength,
+ nsACString& aCharset);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif // _COMI18N_LOADED_H_
diff --git a/comm/mailnews/mime/src/components.conf b/comm/mailnews/mime/src/components.conf
new file mode 100644
index 0000000000..cfd19671ca
--- /dev/null
+++ b/comm/mailnews/mime/src/components.conf
@@ -0,0 +1,87 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{d1258011-f391-44fd-992e-c6f4b461a42f}",
+ "contract_ids": ["@mozilla.org/messenger/mimeheaders;1"],
+ "jsm": "resource:///modules/MimeJSComponents.jsm",
+ "constructor": "MimeHeaders",
+ },
+ {
+ "cid": "{c560806a-425f-4f0f-bf69-397c58c599a7}",
+ "contract_ids": ["@mozilla.org/messenger/structuredheaders;1"],
+ "jsm": "resource:///modules/MimeJSComponents.jsm",
+ "constructor": "MimeWritableStructuredHeaders",
+ },
+ {
+ "cid": "{96bd8769-2d0e-4440-963d-22b97fb3ba77}",
+ "contract_ids": ["@mozilla.org/messenger/headerparser;1"],
+ "jsm": "resource:///modules/MimeJSComponents.jsm",
+ "constructor": "MimeAddressParser",
+ "name": "HeaderParser",
+ "interfaces": ["nsIMsgHeaderParser"],
+ },
+ {
+ "cid": "{93f8c049-80ed-4dda-9000-94ad8daba44c}",
+ "contract_ids": ["@mozilla.org/messenger/mimeconverter;1"],
+ "jsm": "resource:///modules/MimeJSComponents.jsm",
+ "constructor": "MimeConverter",
+ "name": "MimeConverter",
+ "interfaces": ["nsIMimeConverter"],
+ },
+ {
+ "cid": "{403b0540-b7c3-11d2-b35e-525400e2d63a}",
+ "contract_ids": ["@mozilla.org/messenger/mimeobject;1"],
+ "type": "nsMimeObjectClassAccess",
+ "headers": ["/comm/mailnews/mime/src/nsMimeObjectClassAccess.h"],
+ },
+ {
+ "cid": "{faf4f9a6-60ad-11d3-989a-001083010e9b}",
+ "contract_ids": [
+ "@mozilla.org/streamconv;1?from=message/rfc822&to=application/xhtml+xml",
+ "@mozilla.org/streamconv;1?from=message/rfc822&to=text/html",
+ "@mozilla.org/streamconv;1?from=message/rfc822&to=*/*",
+ ],
+ "type": "nsStreamConverter",
+ "headers": ["/comm/mailnews/mime/src/nsStreamConverter.h"],
+ },
+ {
+ "cid": "{f0a8af16-dcce-11d2-a411-00805f613c79}",
+ "contract_ids": ["@mozilla.org/messenger/mimeemitter;1?type=text/html"],
+ "type": "nsMimeHtmlDisplayEmitter",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/mime/emitters/nsMimeHtmlEmitter.h"],
+ "categories": {
+ "mime-emitter": "@mozilla.org/messenger/mimeemitter;1?type=text/html"
+ },
+ },
+ {
+ "cid": "{977e418f-e392-11d2-a2ac-00a024a7d144}",
+ "contract_ids": ["@mozilla.org/messenger/mimeemitter;1?type=text/xml"],
+ "type": "nsMimeXmlEmitter",
+ "headers": ["/comm/mailnews/mime/emitters/nsMimeXmlEmitter.h"],
+ "categories": {
+ "mime-emitter": "@mozilla.org/messenger/mimeemitter;1?type=text/xml"
+ },
+ },
+ {
+ "cid": "{e8892265-7653-46c5-a290-307f3404d0f3}",
+ "contract_ids": ["@mozilla.org/messenger/mimeemitter;1?type=text/plain"],
+ "type": "nsMimePlainEmitter",
+ "headers": ["/comm/mailnews/mime/emitters/nsMimePlainEmitter.h"],
+ "categories": {
+ "mime-emitter": "@mozilla.org/messenger/mimeemitter;1?type=text/plain"
+ },
+ },
+ {
+ "cid": "{f0a8af16-dcff-11d2-a411-00805f613c79}",
+ "contract_ids": ["@mozilla.org/messenger/mimeemitter;1?type=raw"],
+ "type": "nsMimeRawEmitter",
+ "headers": ["/comm/mailnews/mime/emitters/nsMimeRawEmitter.h"],
+ "categories": {"mime-emitter": "@mozilla.org/messenger/mimeemitter;1?type=raw"},
+ },
+]
diff --git a/comm/mailnews/mime/src/extraMimeParsers.jsm b/comm/mailnews/mime/src/extraMimeParsers.jsm
new file mode 100644
index 0000000000..b20bd543a1
--- /dev/null
+++ b/comm/mailnews/mime/src/extraMimeParsers.jsm
@@ -0,0 +1,31 @@
+/* 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/. */
+
+/* globals jsmime */
+
+function parseNewsgroups(headers) {
+ let ng = [];
+ for (let header of headers) {
+ ng = ng.concat(header.split(/\s*,\s*/));
+ }
+ return ng;
+}
+
+function emitNewsgroups(groups) {
+ // Don't encode the newsgroups names in RFC 2047...
+ if (groups.length == 1) {
+ this.addText(groups[0], false);
+ } else {
+ this.addText(groups[0], false);
+ for (let i = 1; i < groups.length; i++) {
+ this.addText(",", false); // only comma, no space!
+ this.addText(groups[i], false);
+ }
+ }
+}
+
+jsmime.headerparser.addStructuredDecoder("Newsgroups", parseNewsgroups);
+jsmime.headerparser.addStructuredDecoder("Followup-To", parseNewsgroups);
+jsmime.headeremitter.addStructuredEncoder("Newsgroups", emitNewsgroups);
+jsmime.headeremitter.addStructuredEncoder("Followup-To", emitNewsgroups);
diff --git a/comm/mailnews/mime/src/jsmime.jsm b/comm/mailnews/mime/src/jsmime.jsm
new file mode 100644
index 0000000000..407c2aacd1
--- /dev/null
+++ b/comm/mailnews/mime/src/jsmime.jsm
@@ -0,0 +1,72 @@
+/* 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/. */
+// vim:set ts=2 sw=2 sts=2 et ft=javascript:
+
+/**
+ * This file exports the JSMime code, polyfilling code as appropriate for use in
+ * Gecko.
+ */
+
+// Load the core MIME parser. Since it doesn't define EXPORTED_SYMBOLS, we must
+// use the subscript loader instead.
+Services.scriptloader.loadSubScript("resource:///modules/jsmime/jsmime.js");
+
+var EXPORTED_SYMBOLS = ["jsmime"];
+
+function bytesToString(buffer) {
+ var string = "";
+ for (var i = 0; i < buffer.length; i++) {
+ string += String.fromCharCode(buffer[i]);
+ }
+ return string;
+}
+
+// Our UTF-7 decoder.
+function UTF7TextDecoder(options = {}, manager) {
+ this.manager = manager;
+ this.collectInput = "";
+}
+UTF7TextDecoder.prototype = {
+ // Since the constructor checked, this will only be called for UTF-7.
+ decode(input, options = {}) {
+ let more = "stream" in options ? options.stream : false;
+ // There are cases where this is called without input.
+ if (!input) {
+ return "";
+ }
+ this.collectInput += bytesToString(input);
+ if (more) {
+ return "";
+ }
+ return this.manager.utf7ToUnicode(this.collectInput);
+ },
+};
+
+/* exported MimeTextDecoder */
+function MimeTextDecoder(charset, options) {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].createInstance(
+ Ci.nsICharsetConverterManager
+ );
+ // The following will throw if the charset is unknown.
+ let newCharset = manager.getCharsetAlias(charset);
+ if (newCharset.toLowerCase() == "utf-7") {
+ return new UTF7TextDecoder(options, manager);
+ }
+ return new TextDecoder(newCharset, options);
+}
+
+// The following code loads custom MIME encoders.
+var CATEGORY_NAME = "custom-mime-encoder";
+Services.obs.addObserver(function (subject, topic, data) {
+ subject = subject.QueryInterface(Ci.nsISupportsCString).data;
+ if (data == CATEGORY_NAME) {
+ let url = Services.catMan.getCategoryEntry(CATEGORY_NAME, subject);
+ Services.scriptloader.loadSubScript(url, {}, "UTF-8");
+ }
+}, "xpcom-category-entry-added");
+
+for (let { data } of Services.catMan.enumerateCategory(CATEGORY_NAME)) {
+ let url = Services.catMan.getCategoryEntry(CATEGORY_NAME, data);
+ Services.scriptloader.loadSubScript(url, {}, "UTF-8");
+}
diff --git a/comm/mailnews/mime/src/mime.def b/comm/mailnews/mime/src/mime.def
new file mode 100644
index 0000000000..6b1c36bf9e
--- /dev/null
+++ b/comm/mailnews/mime/src/mime.def
@@ -0,0 +1,7 @@
+; 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/.
+
+LIBRARY mime.dll
+
+EXPORTS
diff --git a/comm/mailnews/mime/src/mimeParser.jsm b/comm/mailnews/mime/src/mimeParser.jsm
new file mode 100644
index 0000000000..95256ba41c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeParser.jsm
@@ -0,0 +1,546 @@
+/* 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/. */
+// vim:set ts=2 sw=2 sts=2 et ft=javascript:
+
+var EXPORTED_SYMBOLS = ["MimeParser"];
+
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+// Emitter helpers, for internal functions later on.
+var ExtractMimeMsgEmitter = {
+ getAttachmentName(part) {
+ if (!part || !part.hasOwnProperty("headers")) {
+ return "";
+ }
+
+ if (part.headers.hasOwnProperty("content-disposition")) {
+ let filename = MimeParser.getParameter(
+ part.headers["content-disposition"][0],
+ "filename"
+ );
+ if (filename) {
+ return filename;
+ }
+ }
+
+ if (part.headers.hasOwnProperty("content-type")) {
+ let name = MimeParser.getParameter(
+ part.headers["content-type"][0],
+ "name"
+ );
+ if (name) {
+ return name;
+ }
+ }
+
+ return "";
+ },
+
+ // All parts of content-disposition = "attachment" are returned as attachments.
+ // For content-disposition = "inline", all parts except those with content-type
+ // text/plain, text/html and text/enriched are returned as attachments.
+ isAttachment(part) {
+ if (!part) {
+ return false;
+ }
+
+ let contentType = part.contentType || "text/plain";
+ if (contentType.search(/^multipart\//i) === 0) {
+ return false;
+ }
+
+ let contentDisposition = "";
+ if (
+ Array.isArray(part.headers["content-disposition"]) &&
+ part.headers["content-disposition"].length > 0
+ ) {
+ contentDisposition = part.headers["content-disposition"][0];
+ }
+
+ if (
+ contentDisposition.search(/^attachment/i) === 0 ||
+ contentType.search(/^text\/plain|^text\/html|^text\/enriched/i) === -1
+ ) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /** JSMime API */
+ startMessage() {
+ this.mimeTree = {
+ partName: "",
+ contentType: "message/rfc822",
+ parts: [],
+ size: 0,
+ headers: {},
+ attachments: [],
+ // No support for encryption.
+ isEncrypted: false,
+ };
+ // partsPath is a hierarchical stack of parts from the root to the
+ // current part.
+ this.partsPath = [this.mimeTree];
+ this.options = this.options || {};
+ },
+
+ endMessage() {
+ // Prepare the mimeMsg object, which is the final output of the emitter.
+ this.mimeMsg = null;
+ if (this.mimeTree.parts.length == 0) {
+ return;
+ }
+
+ // Check if only a specific mime part has been requested.
+ if (this.options.getMimePart) {
+ if (this.mimeTree.parts[0].partName == this.options.getMimePart) {
+ this.mimeMsg = this.mimeTree.parts[0];
+ }
+ return;
+ }
+
+ this.mimeTree.attachments.sort((a, b) => a.partName > b.partName);
+ this.mimeMsg = this.mimeTree;
+ },
+
+ startPart(partNum, headerMap) {
+ let contentType = headerMap.contentType?.type
+ ? headerMap.contentType.type
+ : "text/plain";
+
+ let headers = {};
+ for (let [headerName, headerValue] of headerMap._rawHeaders) {
+ // MsgHdrToMimeMessage always returns an array, even for single values.
+ let valueArray = Array.isArray(headerValue) ? headerValue : [headerValue];
+ // Return a binary string, to mimic MsgHdrToMimeMessage.
+ headers[headerName] = valueArray.map(value => {
+ return MailStringUtils.stringToByteString(value);
+ });
+ }
+
+ // Get the most recent part from the hierarchical parts stack, which is the
+ // parent of the new part to by added.
+ let parentPart = this.partsPath[this.partsPath.length - 1];
+
+ // Add a leading 1 to the partNum and convert the "$" sub-message deliminator.
+ let partName = "1" + (partNum ? "." : "") + partNum.replaceAll("$", ".1");
+
+ // MsgHdrToMimeMessage differentiates between the message headers and the
+ // headers of the first part. jsmime.js however returns all headers of
+ // the message in the first multipart/* part: Merge all headers into the
+ // parent part and only keep content-* headers.
+ if (parentPart.contentType.startsWith("message/")) {
+ for (let [k, v] of Object.entries(headers)) {
+ if (!parentPart.headers[k]) {
+ parentPart.headers[k] = v;
+ }
+ }
+ headers = Object.fromEntries(
+ Object.entries(headers).filter(h => h[0].startsWith("content-"))
+ );
+ }
+
+ // Add default content-type header.
+ if (!headers.hasOwnProperty("content-type")) {
+ headers["content-type"] = ["text/plain"];
+ }
+
+ let newPart = {
+ partName,
+ body: "",
+ headers,
+ contentType,
+ size: 0,
+ parts: [],
+ // No support for encryption.
+ isEncrypted: false,
+ };
+
+ // Add nested new part.
+ parentPart.parts.push(newPart);
+ // Push the newly added part into the hierarchical parts stack.
+ this.partsPath.push(newPart);
+ },
+
+ endPart(partNum) {
+ let deleteBody = false;
+ // Get the most recent part from the hierarchical parts stack.
+ let currentPart = this.partsPath[this.partsPath.length - 1];
+
+ // Add size.
+ let size = currentPart.body.length;
+ currentPart.size += size;
+ let partSize = currentPart.size;
+
+ if (this.isAttachment(currentPart)) {
+ currentPart.name = this.getAttachmentName(currentPart);
+ this.mimeTree.attachments.push({ ...currentPart });
+ deleteBody = !this.options.getMimePart;
+ }
+
+ if (deleteBody || currentPart.body == "") {
+ delete currentPart.body;
+ }
+
+ // Remove content-disposition and content-transfer-encoding headers.
+ currentPart.headers = Object.fromEntries(
+ Object.entries(currentPart.headers).filter(
+ h =>
+ !["content-disposition", "content-transfer-encoding"].includes(h[0])
+ )
+ );
+
+ // Set the parent of this part to be the new current part.
+ this.partsPath.pop();
+
+ // Add the size of this part to its parent as well.
+ currentPart = this.partsPath[this.partsPath.length - 1];
+ currentPart.size += partSize;
+ },
+
+ /**
+ * The data parameter is either a string or a Uint8Array.
+ */
+ deliverPartData(partNum, data) {
+ // Get the most recent part from the hierarchical parts stack.
+ let currentPart = this.partsPath[this.partsPath.length - 1];
+
+ if (typeof data === "string") {
+ currentPart.body += data;
+ } else {
+ currentPart.body += MailStringUtils.uint8ArrayToByteString(data);
+ }
+ },
+};
+
+var ExtractHeadersEmitter = {
+ startPart(partNum, headers) {
+ if (partNum == "") {
+ this.headers = headers;
+ }
+ },
+};
+
+var ExtractHeadersAndBodyEmitter = {
+ body: "",
+ startPart: ExtractHeadersEmitter.startPart,
+ deliverPartData(partNum, data) {
+ if (partNum == "") {
+ this.body += data;
+ }
+ },
+};
+
+// Sets appropriate default options for chrome-privileged environments
+function setDefaultParserOptions(opts) {
+ if (!("onerror" in opts)) {
+ opts.onerror = Cu.reportError;
+ }
+}
+
+var MimeParser = {
+ /***
+ * Determine an arbitrary "parameter" part of a mail header.
+ *
+ * @param {string} headerStr - The string containing all parts of the header.
+ * @param {string} parameter - The parameter we are looking for.
+ *
+ *
+ * 'multipart/signed; protocol="xyz"', 'protocol' --> returns "xyz"
+ *
+ * @return {string} String containing the value of the parameter; or "".
+ */
+
+ getParameter(headerStr, parameter) {
+ parameter = parameter.toLowerCase();
+ headerStr = headerStr.replace(/[\r\n]+[ \t]+/g, "");
+
+ let hdrMap = jsmime.headerparser.parseParameterHeader(
+ ";" + headerStr,
+ true,
+ true
+ );
+
+ for (let [key, value] of hdrMap.entries()) {
+ if (parameter == key.toLowerCase()) {
+ return value;
+ }
+ }
+
+ return "";
+ },
+
+ /**
+ * Triggers an asynchronous parse of the given input.
+ *
+ * The input is an input stream; the stream will be read until EOF and then
+ * closed upon completion. Both blocking and nonblocking streams are
+ * supported by this implementation, but it is still guaranteed that the first
+ * callback will not happen before this method returns.
+ *
+ * @param input An input stream of text to parse.
+ * @param emitter The emitter to receive callbacks on.
+ * @param opts A set of options for the parser.
+ */
+ parseAsync(input, emitter, opts) {
+ // Normalize the input into an input stream.
+ if (!(input instanceof Ci.nsIInputStream)) {
+ throw new Error("input is not a recognizable type!");
+ }
+
+ // We need a pump for the listener
+ var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ pump.init(input, 0, 0, true);
+
+ // Make a stream listener with the given emitter and use it to read from
+ // the pump.
+ var parserListener = MimeParser.makeStreamListenerParser(emitter, opts);
+ pump.asyncRead(parserListener);
+ },
+
+ /**
+ * Triggers an synchronous parse of the given input.
+ *
+ * The input is a string that is immediately parsed, calling all functions on
+ * the emitter before this function returns.
+ *
+ * @param input A string or input stream of text to parse.
+ * @param emitter The emitter to receive callbacks on.
+ * @param opts A set of options for the parser.
+ */
+ parseSync(input, emitter, opts) {
+ // We only support string parsing if we are trying to do this parse
+ // synchronously.
+ if (typeof input != "string") {
+ throw new Error("input is not a recognizable type!");
+ }
+ setDefaultParserOptions(opts);
+ var parser = new jsmime.MimeParser(emitter, opts);
+ parser.deliverData(input);
+ parser.deliverEOF();
+ },
+
+ /**
+ * Returns a stream listener that feeds data into a parser.
+ *
+ * In addition to the functions on the emitter that the parser may use, the
+ * generated stream listener will also make calls to onStartRequest and
+ * onStopRequest on the emitter (if they exist).
+ *
+ * @param emitter The emitter to receive callbacks on.
+ * @param opts A set of options for the parser.
+ */
+ makeStreamListenerParser(emitter, opts) {
+ var StreamListener = {
+ onStartRequest(aRequest) {
+ try {
+ if ("onStartRequest" in emitter) {
+ emitter.onStartRequest(aRequest);
+ }
+ } finally {
+ this._parser.resetParser();
+ }
+ },
+ onStopRequest(aRequest, aStatus) {
+ this._parser.deliverEOF();
+ if ("onStopRequest" in emitter) {
+ emitter.onStopRequest(aRequest, aStatus);
+ }
+ },
+ onDataAvailable(aRequest, aStream, aOffset, aCount) {
+ var scriptIn = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scriptIn.init(aStream);
+ // Use readBytes instead of read to handle embedded NULs properly.
+ this._parser.deliverData(scriptIn.readBytes(aCount));
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+ };
+ setDefaultParserOptions(opts);
+ StreamListener._parser = new jsmime.MimeParser(emitter, opts);
+ return StreamListener;
+ },
+
+ /**
+ * Returns a new raw MIME parser.
+ *
+ * Prefer one of the other methods where possible, since the input here must
+ * be driven manually.
+ *
+ * @param emitter The emitter to receive callbacks on.
+ * @param opts A set of options for the parser.
+ */
+ makeParser(emitter, opts) {
+ setDefaultParserOptions(opts);
+ return new jsmime.MimeParser(emitter, opts);
+ },
+
+ /**
+ * Returns a mimeMsg object for the given input. The returned object tries to
+ * be compatible with the return value of MsgHdrToMimeMessage. Differences:
+ * - no support for encryption
+ * - returned attachments include the body and not the URL
+ * - returned attachments match either allInlineAttachments or
+ * allUserAttachments (decodeSubMessages = false)
+ * - does not eat TABs in headers, if they follow a CRLF
+ *
+ * The input is any type of input that would be accepted by parseSync.
+ *
+ * @param input A string of text to parse.
+ */
+ extractMimeMsg(input, options) {
+ var emitter = Object.create(ExtractMimeMsgEmitter);
+ // Set default options.
+ emitter.options = {
+ getMimePart: "",
+ decodeSubMessages: true,
+ };
+ // Override default options.
+ for (let option of Object.keys(options)) {
+ emitter.options[option] = options[option];
+ }
+
+ MimeParser.parseSync(input, emitter, {
+ // jsmime does not use the "1." prefix for the partName.
+ // jsmime uses "$." as sub-message deliminator.
+ pruneat: emitter.options.getMimePart
+ .split(".")
+ .slice(1)
+ .join(".")
+ .replaceAll(".1.", "$."),
+ decodeSubMessages: emitter.options.decodeSubMessages,
+ bodyformat: "decode",
+ stripcontinuations: true,
+ strformat: "unicode",
+ });
+ return emitter.mimeMsg;
+ },
+
+ /**
+ * Returns a dictionary of headers for the given input.
+ *
+ * The input is any type of input that would be accepted by parseSync. What
+ * is returned is a JS object that represents the headers of the entire
+ * envelope as would be received by startPart when partNum is the empty
+ * string.
+ *
+ * @param input A string of text to parse.
+ */
+ extractHeaders(input) {
+ var emitter = Object.create(ExtractHeadersEmitter);
+ MimeParser.parseSync(input, emitter, { pruneat: "", bodyformat: "none" });
+ return emitter.headers;
+ },
+
+ /**
+ * Returns the headers and body for the given input message.
+ *
+ * The return value is an array whose first element is the dictionary of
+ * headers (as would be returned by extractHeaders) and whose second element
+ * is a binary string of the entire body of the message.
+ *
+ * @param input A string of text to parse.
+ */
+ extractHeadersAndBody(input) {
+ var emitter = Object.create(ExtractHeadersAndBodyEmitter);
+ MimeParser.parseSync(input, emitter, { pruneat: "", bodyformat: "raw" });
+ return [emitter.headers, emitter.body];
+ },
+
+ // Parameters for parseHeaderField
+
+ /**
+ * Parse the header as if it were unstructured.
+ *
+ * This results in the same string if no other options are specified. If other
+ * options are specified, this causes the string to be modified appropriately.
+ */
+ HEADER_UNSTRUCTURED: 0x00,
+ /**
+ * Parse the header as if it were in the form text; attr=val; attr=val.
+ *
+ * Such headers include Content-Type, Content-Disposition, and most other
+ * headers used by MIME as opposed to messages.
+ */
+ HEADER_PARAMETER: 0x02,
+ /**
+ * Parse the header as if it were a sequence of mailboxes.
+ */
+ HEADER_ADDRESS: 0x03,
+
+ /**
+ * This decodes parameter values according to RFC 2231.
+ *
+ * This flag means nothing if HEADER_PARAMETER is not specified.
+ */
+ HEADER_OPTION_DECODE_2231: 0x10,
+ /**
+ * This decodes the inline encoded-words that are in RFC 2047.
+ */
+ HEADER_OPTION_DECODE_2047: 0x20,
+ /**
+ * This converts the header from a raw string to proper Unicode.
+ */
+ HEADER_OPTION_ALLOW_RAW: 0x40,
+
+ // Convenience for all three of the above.
+ HEADER_OPTION_ALL_I18N: 0x70,
+
+ /**
+ * Parse a header field according to the specification given by flags.
+ *
+ * Permissible flags begin with one of the HEADER_* flags, which may be or'd
+ * with any of the HEADER_OPTION_* flags to modify the result appropriately.
+ *
+ * If the option HEADER_OPTION_ALLOW_RAW is passed, the charset parameter, if
+ * present, is the charset to fallback to if the header is not decodable as
+ * UTF-8 text. If HEADER_OPTION_ALLOW_RAW is passed but the charset parameter
+ * is not provided, then no fallback decoding will be done. If
+ * HEADER_OPTION_ALLOW_RAW is not passed, then no attempt will be made to
+ * convert charsets.
+ *
+ * @param text The value of a MIME or message header to parse.
+ * @param flags A set of flags that controls interpretation of the header.
+ * @param charset A default charset to assume if no information may be found.
+ */
+ parseHeaderField(text, flags, charset) {
+ // If we have a raw string, convert it to Unicode first
+ if (flags & MimeParser.HEADER_OPTION_ALLOW_RAW) {
+ text = jsmime.headerparser.convert8BitHeader(text, charset);
+ }
+
+ // The low 4 bits indicate the type of the header we are parsing. All of the
+ // higher-order bits are flags.
+ switch (flags & 0x0f) {
+ case MimeParser.HEADER_UNSTRUCTURED:
+ if (flags & MimeParser.HEADER_OPTION_DECODE_2047) {
+ text = jsmime.headerparser.decodeRFC2047Words(text);
+ }
+ return text;
+ case MimeParser.HEADER_PARAMETER:
+ return jsmime.headerparser.parseParameterHeader(
+ text,
+ (flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0,
+ (flags & MimeParser.HEADER_OPTION_DECODE_2231) != 0
+ );
+ case MimeParser.HEADER_ADDRESS:
+ return jsmime.headerparser.parseAddressingHeader(
+ text,
+ (flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0
+ );
+ default:
+ throw new Error("Illegal type of header field");
+ }
+ },
+};
diff --git a/comm/mailnews/mime/src/mimeTextHTMLParsed.cpp b/comm/mailnews/mime/src/mimeTextHTMLParsed.cpp
new file mode 100644
index 0000000000..225b63f1ba
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeTextHTMLParsed.cpp
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* Most of this code is copied from mimethsa. If you find a bug here, check that
+ * class, too. */
+
+/* This runs the entire HTML document through the Mozilla HTML parser, and
+ then outputs it as string again. This ensures that the HTML document is
+ syntactically correct and complete and all tags and attributes are closed.
+
+ That prevents "MIME in the middle" attacks like efail.de.
+ The base problem is that we concatenate different MIME parts in the output
+ and render them all together as a single HTML document in the display.
+
+ The better solution would be to put each MIME part into its own <iframe
+ type="content">. during rendering. Unfortunately, we'd need <iframe seamless>
+ for that. That would remove the need for this workaround, and stop even more
+ attack classes.
+*/
+
+#include "mimeTextHTMLParsed.h"
+#include "prmem.h"
+#include "prlog.h"
+#include "msgCore.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/DOMParser.h"
+#include "mozilla/dom/Document.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/Preferences.h"
+#include "nsIParserUtils.h"
+#include "nsIDocumentEncoder.h"
+#include "mozilla/ErrorResult.h"
+#include "mimethtm.h"
+
+#define MIME_SUPERCLASS mimeInlineTextHTMLClass
+MimeDefClass(MimeInlineTextHTMLParsed, MimeInlineTextHTMLParsedClass,
+ mimeInlineTextHTMLParsedClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextHTMLParsed_parse_line(const char*, int32_t,
+ MimeObject*);
+static int MimeInlineTextHTMLParsed_parse_begin(MimeObject* obj);
+static int MimeInlineTextHTMLParsed_parse_eof(MimeObject*, bool);
+static void MimeInlineTextHTMLParsed_finalize(MimeObject* obj);
+
+static int MimeInlineTextHTMLParsedClassInitialize(
+ MimeInlineTextHTMLParsedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "problem with superclass");
+ oclass->parse_line = MimeInlineTextHTMLParsed_parse_line;
+ oclass->parse_begin = MimeInlineTextHTMLParsed_parse_begin;
+ oclass->parse_eof = MimeInlineTextHTMLParsed_parse_eof;
+ oclass->finalize = MimeInlineTextHTMLParsed_finalize;
+
+ return 0;
+}
+
+static int MimeInlineTextHTMLParsed_parse_begin(MimeObject* obj) {
+ MimeInlineTextHTMLParsed* me = (MimeInlineTextHTMLParsed*)obj;
+ me->complete_buffer = new nsString();
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ return 0;
+}
+
+static int MimeInlineTextHTMLParsed_parse_eof(MimeObject* obj, bool abort_p) {
+ if (obj->closed_p) return 0;
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+ MimeInlineTextHTMLParsed* me = (MimeInlineTextHTMLParsed*)obj;
+
+ // We have to cache all lines and parse the whole document at once.
+ // There's a useful sounding function parseFromStream(), but it only allows
+ // XML mimetypes, not HTML. Methinks that's because the HTML soup parser needs
+ // the entire doc to make sense of the gibberish that people write.
+ if (!me || !me->complete_buffer) return 0;
+
+ nsString& rawHTML = *(me->complete_buffer);
+ if (rawHTML.IsEmpty()) return 0;
+ nsString parsed;
+ nsresult rv;
+
+ // Parse the HTML source.
+ mozilla::ErrorResult rv2;
+ RefPtr<mozilla::dom::DOMParser> parser =
+ mozilla::dom::DOMParser::CreateWithoutGlobal(rv2);
+ nsCOMPtr<mozilla::dom::Document> document = parser->ParseFromString(
+ rawHTML, mozilla::dom::SupportedType::Text_html, rv2);
+ if (rv2.Failed()) return -1;
+
+ // Remove meta http-equiv="refresh".
+ RefPtr<nsContentList> metas = document->GetElementsByTagName(u"meta"_ns);
+ uint32_t length = metas->Length(true);
+ for (uint32_t i = length; i > 0; i--) {
+ RefPtr<nsGenericHTMLElement> node =
+ nsGenericHTMLElement::FromNodeOrNull(metas->Item(i - 1));
+ nsAutoString header;
+ node->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, header);
+ nsContentUtils::ASCIIToLower(header);
+ if (nsGkAtoms::refresh->Equals(header)) {
+ node->Remove();
+ }
+ }
+
+ // Serialize it back to HTML source again.
+ nsCOMPtr<nsIDocumentEncoder> encoder = do_createDocumentEncoder("text/html");
+ NS_ENSURE_TRUE(encoder, -1);
+ uint32_t aFlags = nsIDocumentEncoder::OutputRaw |
+ nsIDocumentEncoder::OutputDisallowLineBreaking;
+ rv = encoder->Init(document, u"text/html"_ns, aFlags);
+ NS_ENSURE_SUCCESS(rv, -1);
+ rv = encoder->EncodeToString(parsed);
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ bool stripConditionalCSS = mozilla::Preferences::GetBool(
+ "mail.html_sanitize.drop_conditional_css", true);
+
+ nsCString resultCStr;
+ if (stripConditionalCSS) {
+ nsString cssCondStripped;
+ nsCOMPtr<nsIParserUtils> parserUtils =
+ do_GetService(NS_PARSERUTILS_CONTRACTID);
+ parserUtils->RemoveConditionalCSS(parsed, cssCondStripped);
+ parsed.Truncate();
+ resultCStr = NS_ConvertUTF16toUTF8(cssCondStripped);
+ } else {
+ resultCStr = NS_ConvertUTF16toUTF8(parsed);
+ }
+
+ // Write it out.
+
+ // XXX: adding the doc source resultCStr to what we have here is not nice:
+ // We already have the stuff up to and including <body> written.
+ // So we are dumping <head> content into <body>. Tagsoup ohoy!
+
+ MimeInlineTextHTML_insert_lang_div(obj, resultCStr);
+ MimeInlineTextHTML_remove_plaintext_tag(obj, resultCStr);
+ status =
+ ((MimeObjectClass*)&MIME_SUPERCLASS)
+ ->parse_line(resultCStr.BeginWriting(), resultCStr.Length(), obj);
+ rawHTML.Truncate();
+ return status;
+}
+
+void MimeInlineTextHTMLParsed_finalize(MimeObject* obj) {
+ MimeInlineTextHTMLParsed* me = (MimeInlineTextHTMLParsed*)obj;
+
+ if (me && me->complete_buffer) {
+ obj->clazz->parse_eof(obj, false);
+ delete me->complete_buffer;
+ me->complete_buffer = NULL;
+ }
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeInlineTextHTMLParsed_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeInlineTextHTMLParsed* me = (MimeInlineTextHTMLParsed*)obj;
+
+ if (!me || !(me->complete_buffer)) return -1;
+
+ nsCString linestr(line, length);
+ NS_ConvertUTF8toUTF16 line_ucs2(linestr.get());
+ if (length && line_ucs2.IsEmpty()) CopyASCIItoUTF16(linestr, line_ucs2);
+ (me->complete_buffer)->Append(line_ucs2);
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimeTextHTMLParsed.h b/comm/mailnews/mime/src/mimeTextHTMLParsed.h
new file mode 100644
index 0000000000..99a84d0dd8
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeTextHTMLParsed.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETEXTHTMLPARSED_H_
+#define _MIMETEXTHTMLPARSED_H_
+
+#include "mimethtm.h"
+
+typedef struct MimeInlineTextHTMLParsedClass MimeInlineTextHTMLParsedClass;
+typedef struct MimeInlineTextHTMLParsed MimeInlineTextHTMLParsed;
+
+struct MimeInlineTextHTMLParsedClass {
+ MimeInlineTextHTMLClass html;
+};
+
+extern MimeInlineTextHTMLParsedClass mimeInlineTextHTMLParsedClass;
+
+struct MimeInlineTextHTMLParsed {
+ MimeInlineTextHTML html;
+ nsString* complete_buffer; // Gecko parser expects wide strings
+};
+
+#define MimeInlineTextHTMLParsedClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextHTMLClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETEXTHTMLPARSED_H_ */
diff --git a/comm/mailnews/mime/src/mimebuf.cpp b/comm/mailnews/mime/src/mimebuf.cpp
new file mode 100644
index 0000000000..d1db4d67d5
--- /dev/null
+++ b/comm/mailnews/mime/src/mimebuf.cpp
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+/*
+ * mimebuf.c - libmsg like buffer handling routines for libmime
+ */
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+
+extern "C" int mime_GrowBuffer(uint32_t desired_size, uint32_t element_size,
+ uint32_t quantum, char** buffer, int32_t* size) {
+ if ((uint32_t)*size <= desired_size) {
+ char* new_buf;
+ uint32_t increment = desired_size - *size;
+ if (increment < quantum) /* always grow by a minimum of N bytes */
+ increment = quantum;
+
+ new_buf =
+ (*buffer ? (char*)PR_Realloc(*buffer, (*size + increment) *
+ (element_size / sizeof(char)))
+ : (char*)PR_MALLOC((*size + increment) *
+ (element_size / sizeof(char))));
+ if (!new_buf) return MIME_OUT_OF_MEMORY;
+ *buffer = new_buf;
+ *size += increment;
+ }
+ return 0;
+}
+
+/* The opposite of mime_LineBuffer(): takes small buffers and packs them
+ up into bigger buffers before passing them along.
+
+ Pass in a desired_buffer_size 0 to tell it to flush (for example, in
+ in the very last call to this function.)
+ */
+extern "C" int mime_ReBuffer(const char* net_buffer, int32_t net_buffer_size,
+ uint32_t desired_buffer_size, char** bufferP,
+ int32_t* buffer_sizeP, uint32_t* buffer_fpP,
+ int32_t (*per_buffer_fn)(char* buffer,
+ uint32_t buffer_size,
+ void* closure),
+ void* closure) {
+ int status = 0;
+
+ if (desired_buffer_size >= (uint32_t)(*buffer_sizeP)) {
+ status = mime_GrowBuffer(desired_buffer_size, sizeof(char), 1024, bufferP,
+ buffer_sizeP);
+ if (status < 0) return status;
+ }
+
+ do {
+ int32_t size = *buffer_sizeP - *buffer_fpP;
+ if (size > net_buffer_size) size = net_buffer_size;
+ if (size > 0) {
+ memcpy((*bufferP) + (*buffer_fpP), net_buffer, size);
+ (*buffer_fpP) += size;
+ net_buffer += size;
+ net_buffer_size -= size;
+ }
+
+ if (*buffer_fpP > 0 && *buffer_fpP >= desired_buffer_size) {
+ status = (*per_buffer_fn)((*bufferP), (*buffer_fpP), closure);
+ *buffer_fpP = 0;
+ if (status < 0) return status;
+ }
+ } while (net_buffer_size > 0);
+
+ return 0;
+}
+
+static int convert_and_send_buffer(
+ char* buf, int length, bool convert_newlines_p,
+ int32_t (*per_line_fn)(char* line, uint32_t line_length, void* closure),
+ void* closure) {
+ /* Convert the line terminator to the native form.
+ */
+ char* newline;
+
+#if (MSG_LINEBREAK_LEN == 2)
+ /*
+ * This is a patch to support a mail DB corruption cause by earlier version
+ * that lead to a crash. What happened is that the line terminator is
+ * CR+NULL+LF. Therefore, we first process a line terminated by CR then a
+ * second line that contains only NULL+LF. We need to ignore this second line.
+ * See bug http://bugzilla.mozilla.org/show_bug.cgi?id=61412 for more
+ * information.
+ */
+ if (length == 2 && buf[0] == 0x00 && buf[1] == '\n') return 0;
+#endif
+
+ NS_ASSERTION(buf && length > 0, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!buf || length <= 0) return -1;
+ newline = buf + length;
+ NS_ASSERTION(newline[-1] == '\r' || newline[-1] == '\n',
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (newline[-1] != '\r' && newline[-1] != '\n') return -1;
+
+ if (!convert_newlines_p) {
+ }
+#if (MSG_LINEBREAK_LEN == 1)
+ else if ((newline - buf) >= 2 && newline[-2] == '\r' && newline[-1] == '\n') {
+ /* CRLF -> CR or LF */
+ buf[length - 2] = MSG_LINEBREAK[0];
+ length--;
+ } else if (newline > buf + 1 && newline[-1] != MSG_LINEBREAK[0]) {
+ /* CR -> LF or LF -> CR */
+ buf[length - 1] = MSG_LINEBREAK[0];
+ }
+#else
+ else if (((newline - buf) >= 2 && newline[-2] != '\r') ||
+ ((newline - buf) >= 1 && newline[-1] != '\n')) {
+ /* LF -> CRLF or CR -> CRLF */
+ length++;
+ buf[length - 2] = MSG_LINEBREAK[0];
+ buf[length - 1] = MSG_LINEBREAK[1];
+ }
+#endif
+
+ return (*per_line_fn)(buf, length, closure);
+}
+
+extern "C" int mime_LineBuffer(
+ const char* net_buffer, int32_t net_buffer_size, char** bufferP,
+ int32_t* buffer_sizeP, uint32_t* buffer_fpP, bool convert_newlines_p,
+ int32_t (*per_line_fn)(char* line, uint32_t line_length, void* closure),
+ void* closure) {
+ int status = 0;
+ if (*buffer_fpP > 0 && *bufferP && (*buffer_fpP < (uint32_t)*buffer_sizeP) &&
+ (*bufferP)[*buffer_fpP - 1] == '\r' && net_buffer_size > 0 &&
+ net_buffer[0] != '\n') {
+ /* The last buffer ended with a CR. The new buffer does not start
+ with a LF. This old buffer should be shipped out and discarded. */
+ if ((uint32_t)*buffer_sizeP <= *buffer_fpP) return -1;
+ status = convert_and_send_buffer(*bufferP, *buffer_fpP, convert_newlines_p,
+ per_line_fn, closure);
+ if (status < 0) return status;
+ *buffer_fpP = 0;
+ }
+ while (net_buffer_size > 0) {
+ const char* net_buffer_end = net_buffer + net_buffer_size;
+ const char* newline = 0;
+ const char* s;
+
+ for (s = net_buffer; s < net_buffer_end; s++) {
+ /* Move forward in the buffer until the first newline.
+ Stop when we see CRLF, CR, or LF, or the end of the buffer.
+ *But*, if we see a lone CR at the *very end* of the buffer,
+ treat this as if we had reached the end of the buffer without
+ seeing a line terminator. This is to catch the case of the
+ buffers splitting a CRLF pair, as in "FOO\r\nBAR\r" "\nBAZ\r\n".
+ */
+ if (*s == '\r' || *s == '\n') {
+ newline = s;
+ if (newline[0] == '\r') {
+ if (s == net_buffer_end - 1) {
+ /* CR at end - wait for the next character. */
+ newline = 0;
+ break;
+ } else if (newline[1] == '\n')
+ /* CRLF seen; swallow both. */
+ newline++;
+ }
+ newline++;
+ break;
+ }
+ }
+
+ /* Ensure room in the net_buffer and append some or all of the current
+ chunk of data to it. */
+ {
+ const char* end = (newline ? newline : net_buffer_end);
+ uint32_t desired_size = (end - net_buffer) + (*buffer_fpP) + 1;
+
+ if (desired_size >= (uint32_t)(*buffer_sizeP)) {
+ status = mime_GrowBuffer(desired_size, sizeof(char), 1024, bufferP,
+ buffer_sizeP);
+ if (status < 0) return status;
+ }
+ memcpy((*bufferP) + (*buffer_fpP), net_buffer, (end - net_buffer));
+ (*buffer_fpP) += (end - net_buffer);
+ (*bufferP)[*buffer_fpP] = '\0';
+ }
+
+ /* Now *bufferP contains either a complete line, or as complete
+ a line as we have read so far.
+
+ If we have a line, process it, and then remove it from `*bufferP'.
+ Then go around the loop again, until we drain the incoming data.
+ */
+ if (!newline) return 0;
+
+ status = convert_and_send_buffer(*bufferP, *buffer_fpP, convert_newlines_p,
+ per_line_fn, closure);
+ if (status < 0) return status;
+
+ net_buffer_size -= (newline - net_buffer);
+ net_buffer = newline;
+ (*buffer_fpP) = 0;
+ }
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimebuf.h b/comm/mailnews/mime/src/mimebuf.h
new file mode 100644
index 0000000000..3cd52fac2c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimebuf.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+#ifndef _MIMEBUF_H_
+#define _MIMEBUF_H_
+
+extern "C" int mime_GrowBuffer(uint32_t desired_size, uint32_t element_size,
+ uint32_t quantum, char** buffer, int32_t* size);
+
+extern "C" int mime_LineBuffer(
+ const char* net_buffer, int32_t net_buffer_size, char** bufferP,
+ int32_t* buffer_sizeP, int32_t* buffer_fpP, bool convert_newlines_p,
+ int32_t (*per_line_fn)(char* line, int32_t line_length, void* closure),
+ void* closure);
+
+extern "C" int mime_ReBuffer(const char* net_buffer, int32_t net_buffer_size,
+ uint32_t desired_buffer_size, char** bufferP,
+ uint32_t* buffer_sizeP, uint32_t* buffer_fpP,
+ int32_t (*per_buffer_fn)(char* buffer,
+ uint32_t buffer_size,
+ void* closure),
+ void* closure);
+
+#endif /* _MIMEBUF_H_ */
diff --git a/comm/mailnews/mime/src/mimecms.cpp b/comm/mailnews/mime/src/mimecms.cpp
new file mode 100644
index 0000000000..e9d2d33ae5
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecms.cpp
@@ -0,0 +1,772 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsICMSMessage.h"
+#include "nsICMSMessageErrors.h"
+#include "nsICMSDecoder.h"
+#include "mimecms.h"
+#include "mimemcms.h"
+#include "mimemsig.h"
+#include "nspr.h"
+#include "mimemsg.h"
+#include "mimemoz2.h"
+#include "nsIURI.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgSMIMEHeaderSink.h"
+#include "nsCOMPtr.h"
+#include "nsIX509Cert.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsIMailChannel.h"
+
+using namespace mozilla::mailnews;
+
+// The name "mime encrypted" is misleading, because this code is used
+// both for CMS messages that are encrypted, and also for messages that
+// aren't encrypted, but only contain a signature.
+
+#define MIME_SUPERCLASS mimeEncryptedClass
+MimeDefClass(MimeEncryptedCMS, MimeEncryptedCMSClass, mimeEncryptedCMSClass,
+ &MIME_SUPERCLASS);
+
+static void* MimeCMS_init(MimeObject*,
+ int (*output_fn)(const char*, int32_t, void*), void*);
+static int MimeCMS_write(const char*, int32_t, void*);
+static int MimeCMS_eof(void*, bool);
+static char* MimeCMS_generate(void*);
+static void MimeCMS_free(void*);
+
+extern int SEC_ERROR_CERT_ADDR_MISMATCH;
+
+static int MimeEncryptedCMSClassInitialize(MimeEncryptedCMSClass* clazz) {
+#ifdef DEBUG
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.2 <mscott@netscape.com> 01 Nov 2001 17:59");
+#endif
+
+ MimeEncryptedClass* eclass = (MimeEncryptedClass*)clazz;
+ eclass->crypto_init = MimeCMS_init;
+ eclass->crypto_write = MimeCMS_write;
+ eclass->crypto_eof = MimeCMS_eof;
+ eclass->crypto_generate_html = MimeCMS_generate;
+ eclass->crypto_free = MimeCMS_free;
+
+ return 0;
+}
+
+typedef struct MimeCMSdata {
+ int (*output_fn)(const char* buf, int32_t buf_size, void* output_closure);
+ void* output_closure;
+ nsCOMPtr<nsICMSDecoder> decoder_context;
+ nsCOMPtr<nsICMSMessage> content_info;
+ bool ci_is_encrypted;
+ char* sender_addr;
+ bool decoding_failed;
+ bool skip_content;
+ uint32_t decoded_bytes;
+ MimeObject* self;
+ bool any_parent_is_encrypted_p;
+ bool any_parent_is_signed_p;
+ nsCOMPtr<nsIMsgSMIMEHeaderSink> smimeHeaderSink;
+ nsCString url;
+
+ MimeCMSdata()
+ : output_fn(nullptr),
+ output_closure(nullptr),
+ ci_is_encrypted(false),
+ sender_addr(nullptr),
+ decoding_failed(false),
+ skip_content(false),
+ decoded_bytes(0),
+ self(nullptr),
+ any_parent_is_encrypted_p(false),
+ any_parent_is_signed_p(false) {}
+
+ ~MimeCMSdata() {
+ if (sender_addr) PR_Free(sender_addr);
+
+ // Do an orderly release of nsICMSDecoder and nsICMSMessage //
+ if (decoder_context) {
+ nsCOMPtr<nsICMSMessage> cinfo;
+ decoder_context->Finish(getter_AddRefs(cinfo));
+ }
+ }
+} MimeCMSdata;
+
+/* SEC_PKCS7DecoderContentCallback for SEC_PKCS7DecoderStart() */
+static void MimeCMS_content_callback(void* arg, const char* buf,
+ unsigned long length) {
+ int status;
+ MimeCMSdata* data = (MimeCMSdata*)arg;
+ if (!data) return;
+
+ if (!data->output_fn) return;
+
+ PR_SetError(0, 0);
+ status = data->output_fn(buf, length, data->output_closure);
+ if (status < 0) {
+ PR_SetError(status, 0);
+ data->output_fn = 0;
+ return;
+ }
+
+ data->decoded_bytes += length;
+}
+
+bool MimeEncryptedCMS_encrypted_p(MimeObject* obj) {
+ bool encrypted;
+
+ if (!obj) return false;
+ if (mime_typep(obj, (MimeObjectClass*)&mimeEncryptedCMSClass)) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ MimeCMSdata* data = (MimeCMSdata*)enc->crypto_closure;
+ if (!data || !data->content_info) return false;
+ data->content_info->ContentIsEncrypted(&encrypted);
+ return encrypted;
+ }
+ return false;
+}
+
+bool MimeEncOrMP_CMS_signed_p(MimeObject* obj) {
+ bool is_signed;
+
+ if (!obj) return false;
+ if (mime_typep(obj, (MimeObjectClass*)&mimeMultipartSignedCMSClass)) {
+ return true;
+ }
+ if (mime_typep(obj, (MimeObjectClass*)&mimeEncryptedCMSClass)) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ MimeCMSdata* data = (MimeCMSdata*)enc->crypto_closure;
+ if (!data || !data->content_info) return false;
+ data->content_info->ContentIsSigned(&is_signed);
+ return is_signed;
+ }
+ return false;
+}
+
+bool MimeAnyParentCMSEncrypted(MimeObject* obj) {
+ MimeObject* o2 = obj;
+ while (o2 && o2->parent) {
+ if (MimeEncryptedCMS_encrypted_p(o2->parent)) {
+ return true;
+ }
+ o2 = o2->parent;
+ }
+ return false;
+}
+
+bool MimeAnyParentCMSSigned(MimeObject* obj) {
+ MimeObject* o2 = obj;
+ while (o2 && o2->parent) {
+ if (MimeEncOrMP_CMS_signed_p(o2->parent)) {
+ return true;
+ }
+ o2 = o2->parent;
+ }
+ return false;
+}
+
+bool MimeCMSHeadersAndCertsMatch(nsICMSMessage* content_info,
+ nsIX509Cert* signerCert, const char* from_addr,
+ const char* from_name, const char* sender_addr,
+ const char* sender_name,
+ bool* signing_cert_without_email_address) {
+ nsCString cert_addr;
+ bool match = true;
+ bool foundFrom = false;
+ bool foundSender = false;
+
+ /* Find the name and address in the cert.
+ */
+ if (content_info) {
+ // Extract any address contained in the cert.
+ // This will be used for testing, whether the cert contains no addresses at
+ // all.
+ content_info->GetSignerEmailAddress(getter_Copies(cert_addr));
+ }
+
+ if (signing_cert_without_email_address)
+ *signing_cert_without_email_address = cert_addr.IsEmpty();
+
+ /* Now compare them --
+ consider it a match if the address in the cert matches the
+ address in the From field (or as a fallback, the Sender field)
+ */
+
+ /* If there is no addr in the cert at all, it can not match and we fail. */
+ if (cert_addr.IsEmpty()) {
+ match = false;
+ } else {
+ if (signerCert) {
+ if (from_addr && *from_addr) {
+ NS_ConvertASCIItoUTF16 ucs2From(from_addr);
+ if (NS_FAILED(signerCert->ContainsEmailAddress(ucs2From, &foundFrom))) {
+ foundFrom = false;
+ }
+ } else if (sender_addr && *sender_addr) {
+ NS_ConvertASCIItoUTF16 ucs2Sender(sender_addr);
+ if (NS_FAILED(
+ signerCert->ContainsEmailAddress(ucs2Sender, &foundSender))) {
+ foundSender = false;
+ }
+ }
+ }
+
+ if (!foundSender && !foundFrom) {
+ match = false;
+ }
+ }
+
+ return match;
+}
+
+class nsSMimeVerificationListener : public nsISMimeVerificationListener {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISMIMEVERIFICATIONLISTENER
+
+ nsSMimeVerificationListener(const char* aFromAddr, const char* aFromName,
+ const char* aSenderAddr, const char* aSenderName,
+ const char* aMsgDate,
+ nsIMsgSMIMEHeaderSink* aHeaderSink,
+ int32_t aMimeNestingLevel,
+ const nsCString& aMsgNeckoURL,
+ const nsCString& aOriginMimePartNumber);
+
+ protected:
+ virtual ~nsSMimeVerificationListener() {}
+
+ /**
+ * It is safe to declare this implementation as thread safe,
+ * despite not using a lock to protect the members.
+ * Because of the way the object will be used, we don't expect a race.
+ * After construction, the object is passed to another thread,
+ * but will no longer be accessed on the original thread.
+ * The other thread is unable to access/modify self's data members.
+ * When the other thread is finished, it will call into the "Notify"
+ * callback. Self's members will be accessed on the other thread,
+ * but this is fine, because there is no race with the original thread.
+ * Race-protection for XPCOM reference counting is sufficient.
+ */
+ bool mSinkIsNull;
+ nsMainThreadPtrHandle<nsIMsgSMIMEHeaderSink> mHeaderSink;
+ int32_t mMimeNestingLevel;
+ nsCString mMsgNeckoURL;
+ nsCString mOriginMimePartNumber;
+
+ nsCString mFromAddr;
+ nsCString mFromName;
+ nsCString mSenderAddr;
+ nsCString mSenderName;
+ nsCString mMsgDate;
+};
+
+class SignedStatusRunnable : public mozilla::Runnable {
+ public:
+ SignedStatusRunnable(
+ const nsMainThreadPtrHandle<nsIMsgSMIMEHeaderSink>& aSink,
+ int32_t aNestingLevel, int32_t aSignatureStatus, nsIX509Cert* aSignerCert,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber);
+ NS_DECL_NSIRUNNABLE
+ nsresult mResult;
+
+ protected:
+ nsMainThreadPtrHandle<nsIMsgSMIMEHeaderSink> m_sink;
+ int32_t m_nestingLevel;
+ int32_t m_signatureStatus;
+ nsCOMPtr<nsIX509Cert> m_signerCert;
+ nsCString m_msgNeckoURL;
+ nsCString m_originMimePartNumber;
+};
+
+SignedStatusRunnable::SignedStatusRunnable(
+ const nsMainThreadPtrHandle<nsIMsgSMIMEHeaderSink>& aSink,
+ int32_t aNestingLevel, int32_t aSignatureStatus, nsIX509Cert* aSignerCert,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber)
+ : mozilla::Runnable("SignedStatusRunnable"),
+ mResult(NS_ERROR_UNEXPECTED),
+ m_sink(aSink),
+ m_nestingLevel(aNestingLevel),
+ m_signatureStatus(aSignatureStatus),
+ m_signerCert(aSignerCert),
+ m_msgNeckoURL(aMsgNeckoURL),
+ m_originMimePartNumber(aOriginMimePartNumber) {}
+
+NS_IMETHODIMP SignedStatusRunnable::Run() {
+ mResult =
+ m_sink->SignedStatus(m_nestingLevel, m_signatureStatus, m_signerCert,
+ m_msgNeckoURL, m_originMimePartNumber);
+ return NS_OK;
+}
+
+nsresult ProxySignedStatus(
+ const nsMainThreadPtrHandle<nsIMsgSMIMEHeaderSink>& aSink,
+ int32_t aNestingLevel, int32_t aSignatureStatus, nsIX509Cert* aSignerCert,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber) {
+ RefPtr<SignedStatusRunnable> signedStatus = new SignedStatusRunnable(
+ aSink, aNestingLevel, aSignatureStatus, aSignerCert, aMsgNeckoURL,
+ aOriginMimePartNumber);
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "ProxySignedStatus"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(signedStatus));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return signedStatus->mResult;
+}
+
+NS_IMPL_ISUPPORTS(nsSMimeVerificationListener, nsISMimeVerificationListener)
+
+nsSMimeVerificationListener::nsSMimeVerificationListener(
+ const char* aFromAddr, const char* aFromName, const char* aSenderAddr,
+ const char* aSenderName, const char* aMsgDate,
+ nsIMsgSMIMEHeaderSink* aHeaderSink, int32_t aMimeNestingLevel,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber)
+ : mMsgNeckoURL(aMsgNeckoURL), mOriginMimePartNumber(aOriginMimePartNumber) {
+ mHeaderSink = new nsMainThreadPtrHolder<nsIMsgSMIMEHeaderSink>(
+ "nsSMimeVerificationListener::mHeaderSink", aHeaderSink);
+ mSinkIsNull = !aHeaderSink;
+ mMimeNestingLevel = aMimeNestingLevel;
+
+ mFromAddr = aFromAddr;
+ mFromName = aFromName;
+ mSenderAddr = aSenderAddr;
+ mSenderName = aSenderName;
+ mMsgDate = aMsgDate;
+}
+
+NS_IMETHODIMP nsSMimeVerificationListener::Notify(
+ nsICMSMessage* aVerifiedMessage, nsresult aVerificationResultCode) {
+ // Only continue if we have a valid pointer to the UI
+ NS_ENSURE_FALSE(mSinkIsNull, NS_OK);
+
+ NS_ENSURE_TRUE(aVerifiedMessage, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIX509Cert> signerCert;
+ aVerifiedMessage->GetSignerCert(getter_AddRefs(signerCert));
+
+ int32_t signature_status = nsICMSMessageErrors::GENERAL_ERROR;
+
+ if (NS_FAILED(aVerificationResultCode)) {
+ if (NS_ERROR_MODULE_SECURITY ==
+ NS_ERROR_GET_MODULE(aVerificationResultCode))
+ signature_status = NS_ERROR_GET_CODE(aVerificationResultCode);
+ else if (NS_ERROR_NOT_IMPLEMENTED == aVerificationResultCode)
+ signature_status = nsICMSMessageErrors::VERIFY_ERROR_PROCESSING;
+ } else {
+ bool signing_cert_without_email_address;
+
+ bool good_p = MimeCMSHeadersAndCertsMatch(
+ aVerifiedMessage, signerCert, mFromAddr.get(), mFromName.get(),
+ mSenderAddr.get(), mSenderName.get(),
+ &signing_cert_without_email_address);
+ if (!good_p) {
+ if (signing_cert_without_email_address)
+ signature_status = nsICMSMessageErrors::VERIFY_CERT_WITHOUT_ADDRESS;
+ else
+ signature_status = nsICMSMessageErrors::VERIFY_HEADER_MISMATCH;
+ } else {
+ PRTime sigTime;
+ if (NS_FAILED(aVerifiedMessage->GetSigningTime(&sigTime))) {
+ // Signing time attribute is optional in CMS messages.
+ signature_status = nsICMSMessageErrors::SUCCESS;
+ } else {
+ // If it's present, check for a rough match with the message date.
+ PRTime msgTime;
+ if (PR_ParseTimeString(mMsgDate.get(), false, &msgTime) != PR_SUCCESS) {
+ signature_status = nsICMSMessageErrors::VERIFY_TIME_MISMATCH;
+ } else {
+ PRTime delta;
+
+ if (sigTime > msgTime) {
+ delta = sigTime - msgTime;
+ } else {
+ delta = msgTime - sigTime;
+ }
+
+ if (delta / PR_USEC_PER_SEC > 60 * 60 * 1) {
+ signature_status = nsICMSMessageErrors::VERIFY_TIME_MISMATCH;
+ } else {
+ signature_status = nsICMSMessageErrors::SUCCESS;
+ }
+ }
+ }
+ }
+ }
+
+ if (NS_IsMainThread()) {
+ mHeaderSink->SignedStatus(mMimeNestingLevel, signature_status, signerCert,
+ mMsgNeckoURL, mOriginMimePartNumber);
+ } else {
+ ProxySignedStatus(mHeaderSink, mMimeNestingLevel, signature_status,
+ signerCert, mMsgNeckoURL, mOriginMimePartNumber);
+ }
+
+ return NS_OK;
+}
+
+int MIMEGetRelativeCryptoNestLevel(MimeObject* obj) {
+ /*
+ the part id of any mimeobj is mime_part_address(obj)
+ our currently displayed crypto part is obj
+ the part shown as the toplevel object in the current window is
+ obj->options->part_to_load
+ possibly stored in the toplevel object only ???
+ but hopefully all nested mimeobject point to the same displayooptions
+
+ we need to find out the nesting level of our currently displayed crypto
+ object wrt the shown part in the toplevel window
+ */
+
+ // if we are showing the toplevel message, aTopMessageNestLevel == 0
+ int aTopMessageNestLevel = 0;
+ MimeObject* aTopShownObject = nullptr;
+ if (obj && obj->options->part_to_load) {
+ bool aAlreadyFoundTop = false;
+ for (MimeObject* walker = obj; walker; walker = walker->parent) {
+ if (aAlreadyFoundTop) {
+ if (!mime_typep(walker, (MimeObjectClass*)&mimeEncryptedClass) &&
+ !mime_typep(walker, (MimeObjectClass*)&mimeMultipartSignedClass)) {
+ ++aTopMessageNestLevel;
+ }
+ }
+ if (!aAlreadyFoundTop) {
+ char* addr = mime_part_address(walker);
+ if (!strcmp(addr, walker->options->part_to_load)) {
+ aAlreadyFoundTop = true;
+ aTopShownObject = walker;
+ }
+ PR_FREEIF(addr);
+ }
+ if (!aAlreadyFoundTop && !walker->parent) {
+ // The mime part part_to_load is not a parent of the
+ // the crypto mime part passed in to this function as parameter obj.
+ // That means the crypto part belongs to another branch of the mime
+ // tree.
+ return -1;
+ }
+ }
+ }
+
+ bool CryptoObjectIsChildOfTopShownObject = false;
+ if (!aTopShownObject) {
+ // no sub part specified, top message is displayed, and
+ // our crypto object is definitively a child of it
+ CryptoObjectIsChildOfTopShownObject = true;
+ }
+
+ // if we are the child of the topmost message, aCryptoPartNestLevel == 1
+ int aCryptoPartNestLevel = 0;
+ if (obj) {
+ for (MimeObject* walker = obj; walker; walker = walker->parent) {
+ // Crypto mime objects are transparent wrt nesting.
+ if (!mime_typep(walker, (MimeObjectClass*)&mimeEncryptedClass) &&
+ !mime_typep(walker, (MimeObjectClass*)&mimeMultipartSignedClass)) {
+ ++aCryptoPartNestLevel;
+ }
+ if (aTopShownObject && walker->parent == aTopShownObject) {
+ CryptoObjectIsChildOfTopShownObject = true;
+ }
+ }
+ }
+
+ if (!CryptoObjectIsChildOfTopShownObject) {
+ return -1;
+ }
+
+ return aCryptoPartNestLevel - aTopMessageNestLevel;
+}
+
+static void* MimeCMS_init(MimeObject* obj,
+ int (*output_fn)(const char* buf, int32_t buf_size,
+ void* output_closure),
+ void* output_closure) {
+ MimeCMSdata* data;
+ nsresult rv;
+
+ if (!(obj && obj->options && output_fn)) return 0;
+
+ data = new MimeCMSdata;
+ if (!data) return 0;
+
+ data->self = obj;
+ data->output_fn = output_fn;
+ data->output_closure = output_closure;
+ PR_SetError(0, 0);
+
+ data->any_parent_is_signed_p = MimeAnyParentCMSSigned(obj);
+
+ if (data->any_parent_is_signed_p) {
+ // Parent is signed.
+ // We don't know yet if this child is signed or encrypted.
+ // (We'll know after decoding has completed and EOF is called.)
+ // We don't support "inner encrypt" with outer sign, because the
+ // inner encrypted part could have been produced by an attacker who
+ // stripped away a part containing the signature (S/MIME doesn't
+ // have integrity protection).
+ // A sign-then-sign encoding is confusing, too, because it could be
+ // an attempt to influence which signature is shown.
+ data->skip_content = true;
+ }
+
+ if (!data->skip_content) {
+ data->decoder_context = do_CreateInstance(NS_CMSDECODER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ delete data;
+ return 0;
+ }
+
+ rv = data->decoder_context->Start(MimeCMS_content_callback, data);
+ if (NS_FAILED(rv)) {
+ delete data;
+ return 0;
+ }
+ }
+
+ data->any_parent_is_encrypted_p = MimeAnyParentCMSEncrypted(obj);
+
+ mime_stream_data* msd =
+ (mime_stream_data*)(data->self->options->stream_closure);
+ if (msd) {
+ nsIChannel* channel = msd->channel; // note the lack of ref counting...
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ rv = uri->GetSpec(data->url);
+
+ // We only want to update the UI if the current mime transaction
+ // is intended for display.
+ // If the current transaction is intended for background processing,
+ // we can learn that by looking at the additional header=filter
+ // string contained in the URI.
+ //
+ // If we find something, we do not set smimeHeaderSink,
+ // which will prevent us from giving UI feedback.
+ //
+ // If we do not find header=filter, we assume the result of the
+ // processing will be shown in the UI.
+
+ if (!strstr(data->url.get(), "?header=filter") &&
+ !strstr(data->url.get(), "&header=filter") &&
+ !strstr(data->url.get(), "?header=attach") &&
+ !strstr(data->url.get(), "&header=attach")) {
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(channel);
+ if (mailChannel) {
+ mailChannel->GetSmimeHeaderSink(
+ getter_AddRefs(data->smimeHeaderSink));
+ }
+ }
+ }
+ } // if channel
+ } // if msd
+
+ return data;
+}
+
+static int MimeCMS_write(const char* buf, int32_t buf_size, void* closure) {
+ MimeCMSdata* data = (MimeCMSdata*)closure;
+ nsresult rv;
+
+ if (!data || !data->output_fn || !data->decoder_context) return -1;
+
+ if (!data->decoding_failed && !data->skip_content) {
+ PR_SetError(0, 0);
+ rv = data->decoder_context->Update(buf, buf_size);
+ data->decoding_failed = NS_FAILED(rv);
+ }
+
+ return 0;
+}
+
+void MimeCMSGetFromSender(MimeObject* obj, nsCString& from_addr,
+ nsCString& from_name, nsCString& sender_addr,
+ nsCString& sender_name, nsCString& msg_date) {
+ MimeHeaders* msg_headers = 0;
+
+ /* Find the headers of the MimeMessage which is the parent (or grandparent)
+ of this object (remember, crypto objects nest.) */
+ MimeObject* o2 = obj;
+ msg_headers = o2->headers;
+ while (o2 && o2->parent &&
+ !mime_typep(o2->parent, (MimeObjectClass*)&mimeMessageClass)) {
+ o2 = o2->parent;
+ msg_headers = o2->headers;
+ }
+
+ if (!msg_headers) return;
+
+ /* Find the names and addresses in the From and/or Sender fields.
+ */
+ nsCString s;
+
+ /* Extract the name and address of the "From:" field. */
+ s.Adopt(MimeHeaders_get(msg_headers, HEADER_FROM, false, false));
+ if (!s.IsEmpty()) ExtractFirstAddress(EncodedHeader(s), from_name, from_addr);
+
+ /* Extract the name and address of the "Sender:" field. */
+ s.Adopt(MimeHeaders_get(msg_headers, HEADER_SENDER, false, false));
+ if (!s.IsEmpty())
+ ExtractFirstAddress(EncodedHeader(s), sender_name, sender_addr);
+
+ msg_date.Adopt(MimeHeaders_get(msg_headers, HEADER_DATE, false, true));
+}
+
+void MimeCMSRequestAsyncSignatureVerification(
+ nsICMSMessage* aCMSMsg, const char* aFromAddr, const char* aFromName,
+ const char* aSenderAddr, const char* aSenderName, const char* aMsgDate,
+ nsIMsgSMIMEHeaderSink* aHeaderSink, int32_t aMimeNestingLevel,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber,
+ const nsTArray<uint8_t>& aDigestData, int16_t aDigestType) {
+ RefPtr<nsSMimeVerificationListener> listener =
+ new nsSMimeVerificationListener(
+ aFromAddr, aFromName, aSenderAddr, aSenderName, aMsgDate, aHeaderSink,
+ aMimeNestingLevel, aMsgNeckoURL, aOriginMimePartNumber);
+
+ long verifyFlags = 0;
+ if (mozilla::Preferences::GetBool(
+ "mail.smime.accept_insecure_sha1_message_signatures", false)) {
+ verifyFlags |= nsICMSVerifyFlags::VERIFY_ALLOW_WEAK_SHA1;
+ }
+
+ if (aDigestData.IsEmpty())
+ aCMSMsg->AsyncVerifySignature(verifyFlags, listener);
+ else
+ aCMSMsg->AsyncVerifyDetachedSignature(verifyFlags, listener, aDigestData,
+ aDigestType);
+}
+
+static int MimeCMS_eof(void* crypto_closure, bool abort_p) {
+ MimeCMSdata* data = (MimeCMSdata*)crypto_closure;
+ nsresult rv;
+ int32_t status = nsICMSMessageErrors::SUCCESS;
+
+ if (!data || !data->output_fn) {
+ return -1;
+ }
+
+ if (!data->skip_content && !data->decoder_context) {
+ // If we don't skip, we should have a context.
+ return -1;
+ }
+
+ int aRelativeNestLevel = MIMEGetRelativeCryptoNestLevel(data->self);
+
+ /* Hand an EOF to the crypto library. It may call data->output_fn.
+ (Today, the crypto library has no flushing to do, but maybe there
+ will be someday.)
+
+ We save away the value returned and will use it later to emit a
+ blurb about whether the signature validation was cool.
+ */
+
+ PR_SetError(0, 0);
+ if (!data->skip_content) {
+ rv = data->decoder_context->Finish(getter_AddRefs(data->content_info));
+ if (NS_FAILED(rv)) status = nsICMSMessageErrors::GENERAL_ERROR;
+
+ data->decoder_context = nullptr;
+ }
+
+ nsCOMPtr<nsIX509Cert> certOfInterest;
+
+ if (!data->smimeHeaderSink) return 0;
+
+ if (aRelativeNestLevel < 0) return 0;
+
+ // maxWantedNesting 1: only want outermost nesting level
+ if (aRelativeNestLevel > 1) return 0;
+
+ if (data->decoding_failed) status = nsICMSMessageErrors::GENERAL_ERROR;
+
+ nsAutoCString partnum;
+ partnum.Adopt(mime_part_address(data->self));
+
+ if (data->skip_content) {
+ // Skipping content means, we detected a forbidden combination
+ // of CMS objects, so let's make sure we replace the parent status
+ // with a bad status.
+ if (data->any_parent_is_signed_p) {
+ data->smimeHeaderSink->SignedStatus(aRelativeNestLevel,
+ nsICMSMessageErrors::GENERAL_ERROR,
+ nullptr, data->url, partnum);
+ }
+ if (data->any_parent_is_encrypted_p) {
+ data->smimeHeaderSink->EncryptionStatus(
+ aRelativeNestLevel, nsICMSMessageErrors::GENERAL_ERROR, nullptr,
+ data->url, partnum);
+ }
+ return 0;
+ }
+
+ if (!data->content_info) {
+ if (!data->decoded_bytes) {
+ // We were unable to decode any data.
+ status = nsICMSMessageErrors::GENERAL_ERROR;
+ } else {
+ // Some content got decoded, but we failed to decode
+ // the final summary, probably we got truncated data.
+ status = nsICMSMessageErrors::ENCRYPT_INCOMPLETE;
+ }
+
+ // Although a CMS message could be either encrypted or opaquely signed,
+ // what we see is most likely encrypted, because if it were
+ // signed only, we probably would have been able to decode it.
+
+ data->ci_is_encrypted = true;
+ } else {
+ rv = data->content_info->ContentIsEncrypted(&data->ci_is_encrypted);
+
+ if (NS_SUCCEEDED(rv) && data->ci_is_encrypted) {
+ data->content_info->GetEncryptionCert(getter_AddRefs(certOfInterest));
+ } else {
+ // Existing logic in mimei assumes, if !ci_is_encrypted, then it is
+ // signed. Make sure it indeed is signed.
+
+ bool testIsSigned;
+ rv = data->content_info->ContentIsSigned(&testIsSigned);
+
+ if (NS_FAILED(rv) || !testIsSigned) {
+ // Neither signed nor encrypted?
+ // We are unable to understand what we got, do not try to indicate
+ // S/Mime status.
+ return 0;
+ }
+
+ nsCString from_addr;
+ nsCString from_name;
+ nsCString sender_addr;
+ nsCString sender_name;
+ nsCString msg_date;
+
+ MimeCMSGetFromSender(data->self, from_addr, from_name, sender_addr,
+ sender_name, msg_date);
+
+ MimeCMSRequestAsyncSignatureVerification(
+ data->content_info, from_addr.get(), from_name.get(),
+ sender_addr.get(), sender_name.get(), msg_date.get(),
+ data->smimeHeaderSink, aRelativeNestLevel, data->url, partnum, {}, 0);
+ }
+ }
+
+ if (data->ci_is_encrypted) {
+ data->smimeHeaderSink->EncryptionStatus(aRelativeNestLevel, status,
+ certOfInterest, data->url, partnum);
+ }
+
+ return 0;
+}
+
+static void MimeCMS_free(void* crypto_closure) {
+ MimeCMSdata* data = (MimeCMSdata*)crypto_closure;
+ if (!data) return;
+
+ delete data;
+}
+
+static char* MimeCMS_generate(void* crypto_closure) { return nullptr; }
diff --git a/comm/mailnews/mime/src/mimecms.h b/comm/mailnews/mime/src/mimecms.h
new file mode 100644
index 0000000000..bb4746dfd7
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecms.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMECMS_H_
+#define _MIMECMS_H_
+
+#include "mimecryp.h"
+
+class nsICMSMessage; // for function arguments in mimecms.h
+
+/* The MimeEncryptedCMS class implements a type of MIME object where the
+ object is passed through a CMS decryption engine to decrypt or verify
+ signatures. That module returns a new MIME object, which is then presented
+ to the user. See mimecryp.h for details of the general mechanism on which
+ this is built.
+ */
+
+typedef struct MimeEncryptedCMSClass MimeEncryptedCMSClass;
+typedef struct MimeEncryptedCMS MimeEncryptedCMS;
+
+struct MimeEncryptedCMSClass {
+ MimeEncryptedClass encrypted;
+};
+
+extern MimeEncryptedCMSClass mimeEncryptedCMSClass;
+
+struct MimeEncryptedCMS {
+ MimeEncrypted encrypted; /* superclass variables */
+};
+
+#define MimeEncryptedCMSClassInitializer(ITYPE, CSUPER) \
+ { MimeEncryptedClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEPKCS_H_ */
diff --git a/comm/mailnews/mime/src/mimecom.cpp b/comm/mailnews/mime/src/mimecom.cpp
new file mode 100644
index 0000000000..17438302bb
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecom.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "mimei.h"
+#include "mimeobj.h" /* MimeObject (abstract) */
+#include "mimecont.h" /* |--- MimeContainer (abstract) */
+#include "mimemult.h" /* | |--- MimeMultipart (abstract) */
+#include "mimemsig.h" /* | | |--- MimeMultipartSigned (abstract)*/
+#include "mimetext.h" /* | |--- MimeInlineText (abstract) */
+#include "mimecryp.h"
+#include "mimecth.h"
+
+/*
+ * These calls are necessary to expose the object class hierarchy
+ * to externally developed content type handlers.
+ */
+extern "C" void* XPCOM_GetmimeInlineTextClass(void) {
+ return (void*)&mimeInlineTextClass;
+}
+
+extern "C" void* XPCOM_GetmimeLeafClass(void) { return (void*)&mimeLeafClass; }
+
+extern "C" void* XPCOM_GetmimeObjectClass(void) {
+ return (void*)&mimeObjectClass;
+}
+
+extern "C" void* XPCOM_GetmimeContainerClass(void) {
+ return (void*)&mimeContainerClass;
+}
+
+extern "C" void* XPCOM_GetmimeMultipartClass(void) {
+ return (void*)&mimeMultipartClass;
+}
+
+extern "C" void* XPCOM_GetmimeMultipartSignedClass(void) {
+ return (void*)&mimeMultipartSignedClass;
+}
+
+extern "C" void* XPCOM_GetmimeEncryptedClass(void) {
+ return (void*)&mimeEncryptedClass;
+}
+
+extern "C" int XPCOM_MimeObject_write(void* mimeObject, char* data,
+ int32_t length, bool user_visible_p) {
+ return MIME_MimeObject_write((MimeObject*)mimeObject, data, length,
+ user_visible_p);
+}
+
+extern "C" void* XPCOM_Mime_create(char* content_type, void* hdrs, void* opts) {
+ return mime_create(content_type, (MimeHeaders*)hdrs,
+ (MimeDisplayOptions*)opts);
+}
diff --git a/comm/mailnews/mime/src/mimecom.h b/comm/mailnews/mime/src/mimecom.h
new file mode 100644
index 0000000000..d170a70829
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecom.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * XP-COM Bridges for C function calls
+ */
+#ifndef _MIMECOM_H_
+#define _MIMECOM_H_
+
+#include <stdint.h>
+
+/*
+ * These functions are exposed by libmime to be used by content type
+ * handler plugins for processing stream data.
+ */
+/*
+ * This is the write call for outputting processed stream data.
+ */
+extern "C" int XPCOM_MimeObject_write(void* mimeObject, const char* data,
+ int32_t length, bool user_visible_p);
+/*
+ * The following group of calls expose the pointers for the object
+ * system within libmime.
+ */
+extern "C" void* XPCOM_GetmimeInlineTextClass(void);
+extern "C" void* XPCOM_GetmimeLeafClass(void);
+extern "C" void* XPCOM_GetmimeObjectClass(void);
+extern "C" void* XPCOM_GetmimeContainerClass(void);
+extern "C" void* XPCOM_GetmimeMultipartClass(void);
+extern "C" void* XPCOM_GetmimeMultipartSignedClass(void);
+extern "C" void* XPCOM_GetmimeEncryptedClass(void);
+
+extern "C" void* XPCOM_Mime_create(char* content_type, void* hdrs, void* opts);
+
+#endif /* _MIMECOM_H_ */
diff --git a/comm/mailnews/mime/src/mimecont.cpp b/comm/mailnews/mime/src/mimecont.cpp
new file mode 100644
index 0000000000..8b139e1fbf
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecont.cpp
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "prio.h"
+#include "mimecont.h"
+#include "nsMimeStringResources.h"
+
+#define MIME_SUPERCLASS mimeObjectClass
+MimeDefClass(MimeContainer, MimeContainerClass, mimeContainerClass,
+ &MIME_SUPERCLASS);
+
+static int MimeContainer_initialize(MimeObject*);
+static void MimeContainer_finalize(MimeObject*);
+static int MimeContainer_add_child(MimeObject*, MimeObject*);
+static int MimeContainer_parse_eof(MimeObject*, bool);
+static int MimeContainer_parse_end(MimeObject*, bool);
+static bool MimeContainer_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs);
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeContainer_debug_print(MimeObject*, PRFileDesc*, int32_t depth);
+#endif
+
+static int MimeContainerClassInitialize(MimeContainerClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)&clazz->object;
+
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ oclass->initialize = MimeContainer_initialize;
+ oclass->finalize = MimeContainer_finalize;
+ oclass->parse_eof = MimeContainer_parse_eof;
+ oclass->parse_end = MimeContainer_parse_end;
+ oclass->displayable_inline_p = MimeContainer_displayable_inline_p;
+ clazz->add_child = MimeContainer_add_child;
+
+#if defined(DEBUG) && defined(XP_UNIX)
+ oclass->debug_print = MimeContainer_debug_print;
+#endif
+ return 0;
+}
+
+static int MimeContainer_initialize(MimeObject* object) {
+ /* This is an abstract class; it shouldn't be directly instantiated. */
+ NS_ASSERTION(object->clazz != (MimeObjectClass*)&mimeContainerClass,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeContainer_finalize(MimeObject* object) {
+ MimeContainer* cont = (MimeContainer*)object;
+
+ /* Do this first so that children have their parse_eof methods called
+ in forward order (0-N) but are destroyed in backward order (N-0)
+ */
+
+ /* If we're being destroyed, prior to deleting any data, mark
+ * flush data in all children and mark them as closed, to avoid
+ * flushing during subsequent mime_free of the children.
+ * This also helps if this (parent) object is already marked as
+ * closed, but a child is not yet marked as closed.
+ */
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(object, true);
+ if (cont->children) {
+ int i;
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* kid = cont->children[i];
+ if (kid && !kid->closed_p) {
+ kid->clazz->parse_eof(kid, true);
+ }
+ }
+ }
+
+ if (!object->parsed_p) object->clazz->parse_end(object, false);
+
+ if (cont->children) {
+ int i;
+ for (i = cont->nchildren - 1; i >= 0; i--) {
+ MimeObject* kid = cont->children[i];
+ if (kid) mime_free(kid);
+ cont->children[i] = 0;
+ }
+ PR_FREEIF(cont->children);
+ cont->nchildren = 0;
+ }
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeContainer_parse_eof(MimeObject* object, bool abort_p) {
+ MimeContainer* cont = (MimeContainer*)object;
+ int status;
+
+ /* We must run all of this object's parent methods first, to get all the
+ data flushed down its stream, so that the children's parse_eof methods
+ can access it. We do not access *this* object again after doing this,
+ only its children.
+ */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(object, abort_p);
+ if (status < 0) return status;
+
+ if (cont->children) {
+ int i;
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* kid = cont->children[i];
+ if (kid && !kid->closed_p) {
+ int lstatus = kid->clazz->parse_eof(kid, abort_p);
+ if (lstatus < 0) return lstatus;
+ }
+ }
+ }
+ return 0;
+}
+
+static int MimeContainer_parse_end(MimeObject* object, bool abort_p) {
+ MimeContainer* cont = (MimeContainer*)object;
+ int status;
+
+ /* We must run all of this object's parent methods first, to get all the
+ data flushed down its stream, so that the children's parse_eof methods
+ can access it. We do not access *this* object again after doing this,
+ only its children.
+ */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_end(object, abort_p);
+ if (status < 0) return status;
+
+ if (cont->children) {
+ int i;
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* kid = cont->children[i];
+ if (kid && !kid->parsed_p) {
+ int lstatus = kid->clazz->parse_end(kid, abort_p);
+ if (lstatus < 0) return lstatus;
+ }
+ }
+ }
+ return 0;
+}
+
+static int MimeContainer_add_child(MimeObject* parent, MimeObject* child) {
+ MimeContainer* cont = (MimeContainer*)parent;
+ MimeObject **old_kids, **new_kids;
+
+ NS_ASSERTION(parent && child, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!parent || !child) return -1;
+
+ old_kids = cont->children;
+ new_kids =
+ (MimeObject**)PR_MALLOC(sizeof(MimeObject*) * (cont->nchildren + 1));
+ if (!new_kids) return MIME_OUT_OF_MEMORY;
+
+ if (cont->nchildren > 0)
+ memcpy(new_kids, old_kids, sizeof(MimeObject*) * cont->nchildren);
+ new_kids[cont->nchildren] = child;
+ PR_Free(old_kids);
+ cont->children = new_kids;
+ cont->nchildren++;
+
+ child->parent = parent;
+
+ /* Copy this object's options into the child. */
+ child->options = parent->options;
+
+ return 0;
+}
+
+static bool MimeContainer_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs) {
+ return true;
+}
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeContainer_debug_print(MimeObject* obj, PRFileDesc* stream,
+ int32_t depth) {
+ MimeContainer* cont = (MimeContainer*)obj;
+ int i;
+ char* addr = mime_part_address(obj);
+ for (i = 0; i < depth; i++) PR_Write(stream, " ", 2);
+ /*
+ PR_Write(stream, "<%s %s (%d kid%s) 0x%08X>\n",
+ obj->clazz->class_name,
+ addr ? addr : "???",
+ cont->nchildren, (cont->nchildren == 1 ? "" : "s"),
+ (uint32_t) cont);
+ */
+ PR_FREEIF(addr);
+
+ /*
+ if (cont->nchildren > 0)
+ fprintf(stream, "\n");
+ */
+
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* kid = cont->children[i];
+ int status = kid->clazz->debug_print(kid, stream, depth + 1);
+ if (status < 0) return status;
+ }
+
+ /*
+ if (cont->nchildren > 0)
+ fprintf(stream, "\n");
+ */
+
+ return 0;
+}
+#endif
diff --git a/comm/mailnews/mime/src/mimecont.h b/comm/mailnews/mime/src/mimecont.h
new file mode 100644
index 0000000000..71120c8c6e
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecont.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMECONT_H_
+#define _MIMECONT_H_
+
+#include "mimeobj.h"
+
+/* MimeContainer is the class for the objects representing all MIME
+ types which can contain other MIME objects within them. In addition
+ to the methods inherited from MimeObject, it provides one method:
+
+ int add_child (MimeObject *parent, MimeObject *child)
+
+ Given a parent (a subclass of MimeContainer) this method adds the
+ child (any MIME object) to the parent's list of children.
+
+ The MimeContainer `finalize' method will finalize the children as well.
+ */
+
+typedef struct MimeContainerClass MimeContainerClass;
+typedef struct MimeContainer MimeContainer;
+
+struct MimeContainerClass {
+ MimeObjectClass object;
+ int (*add_child)(MimeObject* parent, MimeObject* child);
+};
+
+extern MimeContainerClass mimeContainerClass;
+
+struct MimeContainer {
+ MimeObject object; /* superclass variables */
+
+ MimeObject** children; /* list of contained objects */
+ int32_t nchildren; /* how many */
+};
+
+#define MimeContainerClassInitializer(ITYPE, CSUPER) \
+ { MimeObjectClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMECONT_H_ */
diff --git a/comm/mailnews/mime/src/mimecryp.cpp b/comm/mailnews/mime/src/mimecryp.cpp
new file mode 100644
index 0000000000..b985c53fbe
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecryp.cpp
@@ -0,0 +1,507 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+#endif
+#include "mimecryp.h"
+#include "mimemoz2.h"
+#include "nsMimeTypes.h"
+#include "nsMimeStringResources.h"
+#include "mimebuf.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "mimemult.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+using namespace mozilla;
+
+static mozilla::LazyLogModule gMimeCryptLog("MIMECRYPT");
+
+#define MIME_SUPERCLASS mimeContainerClass
+MimeDefClass(MimeEncrypted, MimeEncryptedClass, mimeEncryptedClass,
+ &MIME_SUPERCLASS);
+
+static int MimeEncrypted_initialize(MimeObject*);
+static void MimeEncrypted_finalize(MimeObject*);
+static int MimeEncrypted_parse_begin(MimeObject*);
+static int MimeEncrypted_parse_buffer(const char*, int32_t, MimeObject*);
+static int MimeEncrypted_parse_line(const char*, int32_t, MimeObject*);
+static int MimeEncrypted_parse_decoded_buffer(const char*, int32_t,
+ MimeObject*);
+static int MimeEncrypted_parse_eof(MimeObject*, bool);
+static int MimeEncrypted_parse_end(MimeObject*, bool);
+static int MimeEncrypted_add_child(MimeObject*, MimeObject*);
+
+static int MimeHandleDecryptedOutput(const char*, int32_t, void*);
+static int MimeHandleDecryptedOutputLine(char*, int32_t, MimeObject*);
+static int MimeEncrypted_close_headers(MimeObject*);
+static int MimeEncrypted_emit_buffered_child(MimeObject*);
+
+static int MimeEncryptedClassInitialize(MimeEncryptedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeContainerClass* cclass = (MimeContainerClass*)clazz;
+
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.2 <mscott@netscape.com> 01 Nov 2001 17:59");
+ oclass->initialize = MimeEncrypted_initialize;
+ oclass->finalize = MimeEncrypted_finalize;
+ oclass->parse_begin = MimeEncrypted_parse_begin;
+ oclass->parse_buffer = MimeEncrypted_parse_buffer;
+ oclass->parse_line = MimeEncrypted_parse_line;
+ oclass->parse_eof = MimeEncrypted_parse_eof;
+ oclass->parse_end = MimeEncrypted_parse_end;
+
+ cclass->add_child = MimeEncrypted_add_child;
+
+ clazz->parse_decoded_buffer = MimeEncrypted_parse_decoded_buffer;
+
+ return 0;
+}
+
+static int MimeEncrypted_initialize(MimeObject* obj) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj);
+}
+
+static int MimeEncrypted_parse_begin(MimeObject* obj) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ MimeDecoderData* (*fn)(MimeConverterOutputCallback, void*) = 0;
+
+ if (enc->crypto_closure) return -1;
+
+ enc->crypto_closure = (((MimeEncryptedClass*)obj->clazz)->crypto_init)(
+ obj, MimeHandleDecryptedOutput, obj);
+ if (!enc->crypto_closure) return -1;
+
+ /* (Mostly duplicated from MimeLeaf, see comments in mimecryp.h.)
+ Initialize a decoder if necessary.
+ */
+ if (!obj->encoding)
+ ;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_BASE64))
+ fn = &MimeB64DecoderInit;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_QUOTED_PRINTABLE)) {
+ enc->decoder_data =
+ MimeQPDecoderInit(/* The (MimeConverterOutputCallback) cast is to turn
+ the `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)((MimeEncryptedClass*)
+ obj->clazz)
+ ->parse_decoded_buffer),
+ obj);
+
+ if (!enc->decoder_data) return MIME_OUT_OF_MEMORY;
+ } else if (!PL_strcasecmp(obj->encoding, ENCODING_UUENCODE) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE2) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE3) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE4))
+ fn = &MimeUUDecoderInit;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_YENCODE))
+ fn = &MimeYDecoderInit;
+ if (fn) {
+ enc->decoder_data =
+ fn(/* The (MimeConverterOutputCallback) cast is to turn the `void'
+ argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)((MimeEncryptedClass*)obj->clazz)
+ ->parse_decoded_buffer),
+ obj);
+
+ if (!enc->decoder_data) return MIME_OUT_OF_MEMORY;
+ }
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+}
+
+static int MimeEncrypted_parse_buffer(const char* buffer, int32_t size,
+ MimeObject* obj) {
+ /* (Duplicated from MimeLeaf, see comments in mimecryp.h.)
+ */
+
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+
+ if (obj->closed_p) return -1;
+
+ /* Don't consult output_p here, since at this point we're behaving as a
+ simple container object -- the output_p decision should be made by
+ the child of this object. */
+
+ if (enc->decoder_data)
+ return MimeDecoderWrite(enc->decoder_data, buffer, size, nullptr);
+ else
+ return ((MimeEncryptedClass*)obj->clazz)
+ ->parse_decoded_buffer(buffer, size, obj);
+}
+
+static int MimeEncrypted_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ NS_ERROR("This method shouldn't ever be called.");
+ return -1;
+}
+
+static int MimeEncrypted_parse_decoded_buffer(const char* buffer, int32_t size,
+ MimeObject* obj) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ return ((MimeEncryptedClass*)obj->clazz)
+ ->crypto_write(buffer, size, enc->crypto_closure);
+}
+
+static int MimeEncrypted_parse_eof(MimeObject* obj, bool abort_p) {
+ int status = 0;
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+
+ if (obj->closed_p) return 0;
+ NS_ASSERTION(!obj->parsed_p, "1.2 <mscott@netscape.com> 01 Nov 2001 17:59");
+
+ /* (Duplicated from MimeLeaf, see comments in mimecryp.h.)
+ Close off the decoder, to cause it to give up any buffered data that
+ it is still holding.
+ */
+ if (enc->decoder_data) {
+ int status = MimeDecoderDestroy(enc->decoder_data, false);
+ enc->decoder_data = 0;
+ if (status < 0) return status;
+ }
+
+ /* If there is still data in the ibuffer, that means that the last
+ *decrypted* line of this part didn't end in a newline; so push it out
+ anyway (this means that the parse_line method will be called with a
+ string with no trailing newline, which isn't the usual case.) */
+ if (!abort_p && obj->ibuffer_fp > 0) {
+ int status =
+ MimeHandleDecryptedOutputLine(obj->ibuffer, obj->ibuffer_fp, obj);
+ obj->ibuffer_fp = 0;
+ if (status < 0) {
+ obj->closed_p = true;
+ return status;
+ }
+ }
+
+ /* Now run the superclass's parse_eof, which (because we've already taken
+ care of ibuffer in a way appropriate for this class, immediately above)
+ will only set closed_p to true.
+ */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ /* Now close off the underlying crypto module. At this point, the crypto
+ module has all of the input. (DecoderDestroy called parse_decoded_buffer
+ which called crypto_write, with the last of the data.)
+ */
+ if (enc->crypto_closure) {
+ status = ((MimeEncryptedClass*)obj->clazz)
+ ->crypto_eof(enc->crypto_closure, abort_p);
+ if (status < 0 && !abort_p) return status;
+ }
+
+ /* Now we have the entire child part in the part buffer.
+ We are now able to verify its signature, emit a blurb, and then
+ emit the part.
+ */
+ if (abort_p)
+ return 0;
+ else
+ return MimeEncrypted_emit_buffered_child(obj);
+}
+
+static int MimeEncrypted_parse_end(MimeObject* obj, bool abort_p) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_end(obj, abort_p);
+}
+
+static void MimeEncrypted_cleanup(MimeObject* obj, bool finalizing_p) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+
+ if (enc->part_buffer) {
+ MimePartBufferDestroy(enc->part_buffer);
+ enc->part_buffer = 0;
+ }
+
+ if (finalizing_p && enc->crypto_closure) {
+ /* Don't free these until this object is really going away -- keep them
+ around for the lifetime of the MIME object, so that we can get at the
+ security info of sub-parts of the currently-displayed message. */
+ ((MimeEncryptedClass*)obj->clazz)->crypto_free(enc->crypto_closure);
+ enc->crypto_closure = 0;
+ }
+
+ /* (Duplicated from MimeLeaf, see comments in mimecryp.h.)
+ Free the decoder data, if it's still around. */
+ if (enc->decoder_data) {
+ MimeDecoderDestroy(enc->decoder_data, true);
+ enc->decoder_data = 0;
+ }
+
+ if (enc->hdrs) {
+ MimeHeaders_free(enc->hdrs);
+ enc->hdrs = 0;
+ }
+}
+
+static void MimeEncrypted_finalize(MimeObject* obj) {
+ MimeEncrypted_cleanup(obj, true);
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeHandleDecryptedOutput(const char* buf, int32_t buf_size,
+ void* output_closure) {
+ /* This method is invoked by the underlying decryption module.
+ The module is assumed to return a MIME object, and its associated
+ headers. For example, if a text/plain document was encrypted,
+ the encryption module would return the following data:
+
+ Content-Type: text/plain
+
+ Decrypted text goes here.
+
+ This function will then extract a header block (up to the first
+ blank line, as usual) and will then handle the included data as
+ appropriate.
+ */
+ MimeObject* obj = (MimeObject*)output_closure;
+
+ /* Is it truly safe to use ibuffer here? I think so... */
+ return mime_LineBuffer(buf, buf_size, &obj->ibuffer, &obj->ibuffer_size,
+ &obj->ibuffer_fp, true,
+ ((int (*)(char*, int32_t, void*))
+ /* This cast is to turn void into MimeObject */
+ MimeHandleDecryptedOutputLine),
+ obj);
+}
+
+static int MimeHandleDecryptedOutputLine(char* line, int32_t length,
+ MimeObject* obj) {
+ /* Largely the same as MimeMessage_parse_line (the other MIME container
+ type which contains exactly one child.)
+ */
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ int status = 0;
+
+ if (!line || !*line) return -1;
+
+ /* If we're supposed to write this object, but aren't supposed to convert
+ it to HTML, simply pass it through unaltered. */
+ if (obj->output_p && obj->options && !obj->options->write_html_p &&
+ obj->options->output_fn)
+ return MimeObject_write(obj, line, length, true);
+
+ /* If we already have a child object in the buffer, then we're done parsing
+ headers, and all subsequent lines get passed to the inferior object
+ without further processing by us. (Our parent will stop feeding us
+ lines when this MimeMessage part is out of data.)
+ */
+ if (enc->part_buffer)
+ return MimePartBufferWrite(enc->part_buffer, line, length);
+
+ /* Otherwise we don't yet have a child object in the buffer, which means
+ we're not done parsing our headers yet.
+ */
+ if (!enc->hdrs) {
+ enc->hdrs = MimeHeaders_new();
+ if (!enc->hdrs) return MIME_OUT_OF_MEMORY;
+ }
+
+ status = MimeHeaders_parse_line(line, length, enc->hdrs);
+ if (status < 0) return status;
+
+ /* If this line is blank, we're now done parsing headers, and should
+ examine our content-type to create our "body" part.
+ */
+ if (*line == '\r' || *line == '\n') {
+ status = MimeEncrypted_close_headers(obj);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeEncrypted_close_headers(MimeObject* obj) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+
+ // Notify the JS Mime Emitter that this was an encrypted part that it should
+ // hopefully not analyze for indexing...
+ if (obj->options && obj->options->notify_nested_bodies)
+ mimeEmitterAddHeaderField(obj->options, "x-jsemitter-encrypted", "1");
+
+ if (enc->part_buffer) return -1;
+ enc->part_buffer = MimePartBufferCreate();
+ if (!enc->part_buffer) return MIME_OUT_OF_MEMORY;
+
+ return 0;
+}
+
+static int MimeEncrypted_add_child(MimeObject* parent, MimeObject* child) {
+ MimeContainer* cont = (MimeContainer*)parent;
+ if (!parent || !child) return -1;
+
+ /* Encryption containers can only have one child. */
+ if (cont->nchildren != 0) return -1;
+
+ return ((MimeContainerClass*)&MIME_SUPERCLASS)->add_child(parent, child);
+}
+
+#ifdef MOZ_LOGGING
+static int DebugOut(const char* buf, int32_t size, void* closure) {
+ MOZ_LOG(gMimeCryptLog, LogLevel::Debug,
+ ("MimeEncrypted_emit_buffered_child: (partial) decrypted body\n%.*s",
+ size, buf));
+ return 0;
+}
+#endif
+
+static int MimeEncrypted_emit_buffered_child(MimeObject* obj) {
+ MimeEncrypted* enc = (MimeEncrypted*)obj;
+ int status = 0;
+ char* ct = 0;
+ MimeObject* body;
+
+ NS_ASSERTION(enc->crypto_closure,
+ "1.2 <mscott@netscape.com> 01 Nov 2001 17:59");
+
+#ifdef MOZ_LOGGING
+ if (enc->hdrs && enc->hdrs->all_headers) {
+ MOZ_LOG(gMimeCryptLog, LogLevel::Debug,
+ ("MimeEncrypted_emit_buffered_child: decrypted headers:\n%.*s",
+ enc->hdrs->all_headers_fp, enc->hdrs->all_headers));
+ }
+
+ if (enc->part_buffer) {
+ status = MimePartBufferRead(enc->part_buffer, DebugOut, 0);
+ if (status < 0) return status;
+ }
+#endif
+
+ /* Emit some HTML saying whether the signature was cool.
+ But don't emit anything if in FO_QUOTE_MESSAGE mode.
+
+ Also, don't emit anything if the enclosed object is itself a signed
+ object -- in the case of an encrypted object which contains a signed
+ object, we only emit the HTML once (since the normal way of encrypting
+ and signing is to nest the signature inside the crypto envelope.)
+ */
+ if (enc->crypto_closure && obj->options &&
+ obj->options->headers != MimeHeadersCitation &&
+ obj->options->write_html_p && obj->options->output_fn) {
+ /* Now that we have written out the crypto stamp, the outermost header
+ block is well and truly closed. If this is in fact the outermost
+ message, then run the post_header_html_fn now.
+ */
+ if (obj->options && obj->options->state &&
+ obj->options->generate_post_header_html_fn &&
+ !obj->options->state->post_header_html_run_p) {
+ MimeHeaders* outer_headers = nullptr;
+ MimeObject* p;
+ for (p = obj; p->parent; p = p->parent) outer_headers = p->headers;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "1.2 <mscott@netscape.com> 01 Nov 2001 17:59");
+ char* html = obj->options->generate_post_header_html_fn(
+ NULL, obj->options->html_closure, outer_headers);
+ obj->options->state->post_header_html_run_p = true;
+ if (html) {
+ status = MimeObject_write(obj, html, strlen(html), false);
+ PR_FREEIF(html);
+ if (status < 0) return status;
+ }
+ }
+ } else if (enc->crypto_closure && obj->options && obj->options->decrypt_p) {
+ /* Do this just to cause `mime_set_crypto_stamp' to be called, and to
+ cause the various `decode_error' and `verify_error' slots to be set:
+ we don't actually use the returned HTML, because we're not emitting
+ HTML. It's maybe not such a good thing that the determination of
+ whether it was encrypted or not is tied up with generating HTML,
+ but oh well. */
+ char* html = (((MimeEncryptedClass*)obj->clazz)
+ ->crypto_generate_html(enc->crypto_closure));
+ PR_FREEIF(html);
+ }
+
+ if (enc->hdrs)
+ ct = MimeHeaders_get(enc->hdrs, HEADER_CONTENT_TYPE, true, false);
+ body = mime_create((ct ? ct : TEXT_PLAIN), enc->hdrs, obj->options);
+
+#ifdef MIME_DRAFTS
+ if (obj->options->decompose_file_p) {
+ if (mime_typep(body, (MimeObjectClass*)&mimeMultipartClass))
+ obj->options->is_multipart_msg = true;
+ else if (obj->options->decompose_file_init_fn)
+ obj->options->decompose_file_init_fn(obj->options->stream_closure,
+ enc->hdrs);
+ }
+#endif /* MIME_DRAFTS */
+
+ PR_FREEIF(ct);
+
+ if (!body) return MIME_OUT_OF_MEMORY;
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, body);
+ if (status < 0) {
+ mime_free(body);
+ return status;
+ }
+
+ /* Now that we've added this new object to our list of children,
+ start its parser going. */
+ status = body->clazz->parse_begin(body);
+ if (status < 0) return status;
+
+ /* If this object (or the parent) is being output, then by definition
+ the child is as well. (This is only necessary because this is such
+ a funny sort of container...)
+ */
+ if (!body->output_p &&
+ (obj->output_p || (obj->parent && obj->parent->output_p)))
+ body->output_p = true;
+
+ /* If the body is being written raw (not as HTML) then make sure to
+ write its headers as well. */
+ if (body->output_p && obj->output_p && !obj->options->write_html_p) {
+ status = MimeObject_write(body, "", 0, false); /* initialize */
+ if (status < 0) return status;
+ status = MimeHeaders_write_raw_headers(body->headers, obj->options, false);
+ if (status < 0) return status;
+ }
+
+ if (enc->part_buffer) /* part_buffer is 0 for 0-length encrypted data. */
+ {
+#ifdef MIME_DRAFTS
+ if (obj->options->decompose_file_p && !obj->options->is_multipart_msg) {
+ status = MimePartBufferRead(
+ enc->part_buffer,
+ /* The (MimeConverterOutputCallback) cast is to turn the `void'
+ argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)obj->options->decompose_file_output_fn),
+ obj->options->stream_closure);
+ } else {
+#endif /* MIME_DRAFTS */
+
+ status = MimePartBufferRead(
+ enc->part_buffer,
+ /* The (MimeConverterOutputCallback) cast is to turn the `void'
+ argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)body->clazz->parse_buffer), body);
+#ifdef MIME_DRAFTS
+ }
+#endif /* MIME_DRAFTS */
+ }
+ if (status < 0) return status;
+
+ /* The child has been fully processed. Close it off.
+ */
+ status = body->clazz->parse_eof(body, false);
+ if (status < 0) return status;
+
+ status = body->clazz->parse_end(body, false);
+ if (status < 0) return status;
+
+#ifdef MIME_DRAFTS
+ if (obj->options->decompose_file_p && !obj->options->is_multipart_msg)
+ obj->options->decompose_file_close_fn(obj->options->stream_closure);
+#endif /* MIME_DRAFTS */
+
+ /* Put out a separator after every encrypted object. */
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+
+ MimeEncrypted_cleanup(obj, false);
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimecryp.h b/comm/mailnews/mime/src/mimecryp.h
new file mode 100644
index 0000000000..dd8c846350
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecryp.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMECRYP_H_
+#define _MIMECRYP_H_
+
+#include "mimecont.h"
+// #include "mimeenc.h"
+#include "modmimee.h"
+#include "mimepbuf.h"
+
+/* The MimeEncrypted class implements a type of MIME object where the object
+ is passed to some other routine, which then returns a new MIME object.
+ This is the basis of a decryption module.
+
+ Oddly, this class behaves both as a container and as a leaf: it acts as a
+ container in that it parses out data in order to eventually present a
+ contained object; however, it acts as a leaf in that this container may
+ itself have a Content-Transfer-Encoding applied to its body. This violates
+ the cardinal rule of MIME containers, which is that encodings don't nest,
+ and therefore containers can't have encodings. But, the fact that the
+ S/MIME spec doesn't follow the groundwork laid down by previous MIME specs
+ isn't something we can do anything about at this point...
+
+ Therefore, this class duplicates some of the work done by the MimeLeaf
+ class, to meet its dual goals of container-hood and leaf-hood. (We could
+ alternately have made this class be a subclass of leaf, and had it duplicate
+ the effort of MimeContainer, but that seemed like the harder approach.)
+
+ The MimeEncrypted class provides the following methods:
+
+ void *crypto_init(MimeObject *obj,
+ int (*output_fn) (const char *data, int32 data_size,
+ void *output_closure),
+ void *output_closure)
+
+ This is called with the MimeObject representing the encrypted data.
+ The obj->headers should be used to initialize the decryption engine.
+ NULL indicates failure; otherwise, an opaque closure object should
+ be returned.
+
+ output_fn is what the decryption module should use to write a new MIME
+ object (the decrypted data.) output_closure should be passed along to
+ every call to the output routine.
+
+ The data sent to output_fn should begin with valid MIME headers indicating
+ the type of the data. For example, if decryption resulted in a text
+ document, the data fed through to the output_fn might minimally look like
+
+ Content-Type: text/plain
+
+ This is the decrypted data.
+ It is only two lines long.
+
+ Of course, the data may be of any MIME type, including multipart/mixed.
+ Any returned MIME object will be recursively processed and presented
+ appropriately. (This also imples that encrypted objects may nest, and
+ thus that the underlying decryption module must be reentrant.)
+
+ int crypto_write (const char *data, int32 data_size, void *crypto_closure)
+
+ This is called with the raw encrypted data. This data might not come
+ in line-based chunks: if there was a Content-Transfer-Encoding applied
+ to the data (base64 or quoted-printable) then it will have been decoded
+ first (handing binary data to the filter_fn.) `crypto_closure' is the
+ object that `crypto_init' returned. This may return negative on error.
+
+ int crypto_eof (void *crypto_closure, bool abort_p)
+
+ This is called when no more data remains. It may call `output_fn' again
+ to flush out any buffered data. If `abort_p' is true, then it may choose
+ to discard any data rather than processing it, as we're terminating
+ abnormally.
+
+ char * crypto_generate_html (void *crypto_closure)
+
+ This is called after `crypto_eof' but before `crypto_free'. The crypto
+ module should return a newly-allocated string of HTML code which
+ explains the status of the decryption to the user (whether the signature
+ checked out, etc.)
+
+ void crypto_free (void *crypto_closure)
+
+ This will be called when we're all done, after `crypto_eof' and
+ `crypto_emit_html'. It is intended to free any data represented
+ by the crypto_closure. output_fn may not be called.
+
+
+ int (*parse_decoded_buffer) (const char *buf, int32 size, MimeObject *obj)
+
+ This method, of the same name as one in MimeLeaf, is a part of the
+ afforementioned leaf/container hybridization. This method is invoked
+ with the content-transfer-decoded body of this part (without line
+ buffering.) The default behavior of this method is to simply invoke
+ `crypto_write' on the data with which it is called. It's unlikely that
+ a subclass will need to specialize this.
+ */
+
+typedef struct MimeEncryptedClass MimeEncryptedClass;
+typedef struct MimeEncrypted MimeEncrypted;
+
+struct MimeEncryptedClass {
+ MimeContainerClass container;
+
+ /* Duplicated from MimeLeaf, see comments above.
+ This is the callback that is handed to the decoder. */
+ int (*parse_decoded_buffer)(const char* buf, int32_t size, MimeObject* obj);
+
+ /* Callbacks used by decryption module. */
+ void* (*crypto_init)(MimeObject* obj,
+ int (*output_fn)(const char* data, int32_t data_size,
+ void* output_closure),
+ void* output_closure);
+ int (*crypto_write)(const char* data, int32_t data_size,
+ void* crypto_closure);
+ int (*crypto_eof)(void* crypto_closure, bool abort_p);
+ char* (*crypto_generate_html)(void* crypto_closure);
+ void (*crypto_free)(void* crypto_closure);
+};
+
+extern MimeEncryptedClass mimeEncryptedClass;
+
+struct MimeEncrypted {
+ MimeContainer container; /* superclass variables */
+ void* crypto_closure; /* Opaque data used by decryption module. */
+ MimeDecoderData* decoder_data; /* Opaque data for the Transfer-Encoding
+ decoder. */
+ MimeHeaders* hdrs; /* Headers of the enclosed object (including
+ the type of the *decrypted* data.) */
+ MimePartBufferData* part_buffer; /* The data of the decrypted enclosed
+ object (see mimepbuf.h) */
+};
+
+#define MimeEncryptedClassInitializer(ITYPE, CSUPER) \
+ { MimeContainerClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMECRYP_H_ */
diff --git a/comm/mailnews/mime/src/mimecth.cpp b/comm/mailnews/mime/src/mimecth.cpp
new file mode 100644
index 0000000000..0a091b77fb
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecth.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "mimecth.h"
+
+/*
+ * These calls are necessary to expose the object class hierarchy
+ * to externally developed content type handlers.
+ */
+MimeInlineTextClass* MIME_GetmimeInlineTextClass(void) {
+ return &mimeInlineTextClass;
+}
+
+MimeLeafClass* MIME_GetmimeLeafClass(void) { return &mimeLeafClass; }
+
+MimeObjectClass* MIME_GetmimeObjectClass(void) { return &mimeObjectClass; }
+
+MimeContainerClass* MIME_GetmimeContainerClass(void) {
+ return &mimeContainerClass;
+}
+
+MimeMultipartClass* MIME_GetmimeMultipartClass(void) {
+ return &mimeMultipartClass;
+}
+
+MimeMultipartSignedClass* MIME_GetmimeMultipartSignedClass(void) {
+ return &mimeMultipartSignedClass;
+}
+
+MimeEncryptedClass* MIME_GetmimeEncryptedClass(void) {
+ return &mimeEncryptedClass;
+}
diff --git a/comm/mailnews/mime/src/mimecth.h b/comm/mailnews/mime/src/mimecth.h
new file mode 100644
index 0000000000..ed65163452
--- /dev/null
+++ b/comm/mailnews/mime/src/mimecth.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This is the definitions for the Content Type Handler plugins for
+ * libmime. This will allow developers the dynamically add the ability
+ * for libmime to render new content types in the MHTML rendering of
+ * HTML messages.
+ */
+
+#ifndef _MIMECTH_H_
+#define _MIMECTH_H_
+
+#include "mimei.h"
+#include "mimeobj.h" /* MimeObject (abstract) */
+#include "mimecont.h" /* |--- MimeContainer (abstract) */
+#include "mimemult.h" /* | |--- MimeMultipart (abstract) */
+#include "mimemsig.h" /* | | |--- MimeMultipartSigned (abstract)*/
+#include "mimetext.h" /* | |--- MimeInlineText (abstract) */
+#include "mimecryp.h"
+
+/*
+ This header exposes functions that are necessary to access the
+ object hierarchy for the mime chart. The class hierarchy is:
+
+ MimeObject (abstract)
+ |
+ |--- MimeContainer (abstract)
+ | |
+ | |--- MimeMultipart (abstract)
+ | | |
+ | | |--- MimeMultipartMixed
+ | | |
+ | | |--- MimeMultipartDigest
+ | | |
+ | | |--- MimeMultipartParallel
+ | | |
+ | | |--- MimeMultipartAlternative
+ | | |
+ | | |--- MimeMultipartRelated
+ | | |
+ | | |--- MimeMultipartAppleDouble
+ | | |
+ | | |--- MimeSunAttachment
+ | | |
+ | | |--- MimeMultipartSigned (abstract)
+ | | |
+ | | |--- MimeMultipartSigned
+ | |
+ | |--- MimeXlateed (abstract)
+ | | |
+ | | |--- MimeXlateed
+ | |
+ | |--- MimeMessage
+ | |
+ | |--- MimeUntypedText
+ |
+ |--- MimeLeaf (abstract)
+ | |
+ | |--- MimeInlineText (abstract)
+ | | |
+ | | |--- MimeInlineTextPlain
+ | | |
+ | | |--- MimeInlineTextHTML
+ | | |
+ | | |--- MimeInlineTextRichtext
+ | | | |
+ | | | |--- MimeInlineTextEnriched
+ | | |
+ | | |--- MimeInlineTextVCard
+ | |
+ | |--- MimeInlineImage
+ | |
+ | |--- MimeExternalObject
+ |
+ |--- MimeExternalBody
+ */
+
+#include "nsIMimeContentTypeHandler.h"
+
+/*
+ * These functions are exposed by libmime to be used by content type
+ * handler plugins for processing stream data.
+ */
+/*
+ * This is the write call for outputting processed stream data.
+ */
+extern int MIME_MimeObject_write(MimeObject*, const char* data, int32_t length,
+ bool user_visible_p);
+/*
+ * The following group of calls expose the pointers for the object
+ * system within libmime.
+ */
+extern MimeInlineTextClass* MIME_GetmimeInlineTextClass(void);
+extern MimeLeafClass* MIME_GetmimeLeafClass(void);
+extern MimeObjectClass* MIME_GetmimeObjectClass(void);
+extern MimeContainerClass* MIME_GetmimeContainerClass(void);
+extern MimeMultipartClass* MIME_GetmimeMultipartClass(void);
+extern MimeMultipartSignedClass* MIME_GetmimeMultipartSignedClass(void);
+extern MimeEncryptedClass* MIME_GetmimeEncryptedClass(void);
+
+/*
+ * These are the functions that need to be implemented by the
+ * content type handler plugin. They will be called by by libmime
+ * when the module is loaded at runtime.
+ */
+
+/*
+ * MIME_GetContentType() is called by libmime to identify the content
+ * type handled by this plugin.
+ */
+extern "C" char* MIME_GetContentType(void);
+
+/*
+ * This will create the MimeObjectClass object to be used by the libmime
+ * object system.
+ */
+extern "C" MimeObjectClass* MIME_CreateContentTypeHandlerClass(
+ const char* content_type, contentTypeHandlerInitStruct* initStruct);
+
+/*
+ * Typedefs for libmime to use when locating and calling the above
+ * defined functions.
+ */
+typedef char* (*mime_get_ct_fn_type)(void);
+typedef MimeObjectClass* (*mime_create_class_fn_type)(
+ const char*, contentTypeHandlerInitStruct*);
+
+#endif /* _MIMECTH_H_ */
diff --git a/comm/mailnews/mime/src/mimedrft.cpp b/comm/mailnews/mime/src/mimedrft.cpp
new file mode 100644
index 0000000000..07d05cf19b
--- /dev/null
+++ b/comm/mailnews/mime/src/mimedrft.cpp
@@ -0,0 +1,2135 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+#include "nsCOMPtr.h"
+#include "modmimee.h"
+#include "mimeobj.h"
+#include "modlmime.h"
+#include "mimei.h"
+#include "mimebuf.h"
+#include "mimemoz2.h"
+#include "mimemsg.h"
+#include "nsMimeTypes.h"
+#include <ctype.h>
+
+#include "prmem.h"
+#include "plstr.h"
+#include "prprf.h"
+#include "prio.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "msgCore.h"
+#include "nsIMsgSend.h"
+#include "nsMimeStringResources.h"
+#include "nsNetUtil.h"
+#include "comi18n.h"
+#include "nsIMsgAttachment.h"
+#include "nsIMsgCompFields.h"
+#include "nsIMsgComposeService.h"
+#include "nsMsgAttachmentData.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIMIMEService.h"
+#include "nsIMsgAccountManager.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+#include "mozilla/dom/Promise.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+
+using namespace mozilla::mailnews;
+
+//
+// Header strings...
+//
+#define HEADER_NNTP_POSTING_HOST "NNTP-Posting-Host"
+#define MIME_HEADER_TABLE \
+ "<TABLE CELLPADDING=0 CELLSPACING=0 BORDER=0 " \
+ "class=\"moz-email-headers-table\">"
+#define HEADER_START_JUNK "<TR><TH VALIGN=BASELINE ALIGN=RIGHT NOWRAP>"
+#define HEADER_MIDDLE_JUNK ": </TH><TD>"
+#define HEADER_END_JUNK "</TD></TR>"
+
+//
+// Forward declarations...
+//
+extern "C" char* MIME_StripContinuations(char* original);
+int mime_decompose_file_init_fn(void* stream_closure, MimeHeaders* headers);
+int mime_decompose_file_output_fn(const char* buf, int32_t size,
+ void* stream_closure);
+int mime_decompose_file_close_fn(void* stream_closure);
+extern int MimeHeaders_build_heads_list(MimeHeaders* hdrs);
+
+#define NS_MSGCOMPOSESERVICE_CID \
+ { /* 588595FE-1ADA-11d3-A715-0060B0EB39B5 */ \
+ 0x588595fe, 0x1ada, 0x11d3, { \
+ 0xa7, 0x15, 0x0, 0x60, 0xb0, 0xeb, 0x39, 0xb5 \
+ } \
+ }
+static NS_DEFINE_CID(kCMsgComposeServiceCID, NS_MSGCOMPOSESERVICE_CID);
+
+mime_draft_data::mime_draft_data()
+ : url_name(nullptr),
+ format_out(0),
+ stream(nullptr),
+ obj(nullptr),
+ options(nullptr),
+ headers(nullptr),
+ messageBody(nullptr),
+ curAttachment(nullptr),
+ decoder_data(nullptr),
+ mailcharset(nullptr),
+ forwardInline(false),
+ forwardInlineFilter(false),
+ overrideComposeFormat(false),
+ autodetectCharset(false) {}
+////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////
+// THIS SHOULD ALL MOVE TO ANOTHER FILE AFTER LANDING!
+////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////
+
+// safe filename for all OSes
+#define SAFE_TMP_FILENAME "nsmime.tmp"
+
+//
+// Create a file for the a unique temp file
+// on the local machine. Caller must free memory
+//
+nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile) {
+ if (!tFileName || !*tFileName) tFileName = SAFE_TMP_FILENAME;
+
+ nsresult rv =
+ GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, tFileName, tFile);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = (*tFile)->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ if (NS_FAILED(rv)) NS_RELEASE(*tFile);
+
+ return rv;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////
+// END OF - THIS SHOULD ALL MOVE TO ANOTHER FILE AFTER LANDING!
+////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////
+
+typedef enum {
+ nsMsg_RETURN_RECEIPT_BOOL_HEADER_MASK = 0,
+ nsMsg_ENCRYPTED_BOOL_HEADER_MASK,
+ nsMsg_SIGNED_BOOL_HEADER_MASK,
+ nsMsg_UUENCODE_BINARY_BOOL_HEADER_MASK,
+ nsMsg_ATTACH_VCARD_BOOL_HEADER_MASK,
+ nsMsg_LAST_BOOL_HEADER_MASK // last boolean header mask; must be the last one
+ // DON'T remove.
+} nsMsgBoolHeaderSet;
+
+#ifdef NS_DEBUG
+extern "C" void mime_dump_attachments(nsMsgAttachmentData* attachData) {
+ int32_t i = 0;
+ class nsMsgAttachmentData* tmp = attachData;
+
+ while (tmp && tmp->m_url) {
+ printf("Real Name : %s\n", tmp->m_realName.get());
+
+ if (tmp->m_url) {
+ ;
+ printf("URL : %s\n", tmp->m_url->GetSpecOrDefault().get());
+ }
+
+ printf("Desired Type : %s\n", tmp->m_desiredType.get());
+ printf("Real Type : %s\n", tmp->m_realType.get());
+ printf("Real Encoding : %s\n", tmp->m_realEncoding.get());
+ printf("Description : %s\n", tmp->m_description.get());
+ printf("Mac Type : %s\n", tmp->m_xMacType.get());
+ printf("Mac Creator : %s\n", tmp->m_xMacCreator.get());
+ printf("Size in bytes : %d\n", tmp->m_size);
+ i++;
+ tmp++;
+ }
+}
+#endif
+
+nsresult CreateComposeParams(nsCOMPtr<nsIMsgComposeParams>& pMsgComposeParams,
+ nsIMsgCompFields* compFields,
+ nsMsgAttachmentData* attachmentList,
+ MSG_ComposeType composeType,
+ MSG_ComposeFormat composeFormat,
+ nsIMsgIdentity* identity,
+ const nsACString& originalMsgURI,
+ nsIMsgDBHdr* origMsgHdr) {
+#ifdef NS_DEBUG
+ mime_dump_attachments(attachmentList);
+#endif
+
+ nsresult rv;
+ nsMsgAttachmentData* curAttachment = attachmentList;
+ if (curAttachment) {
+ nsAutoCString spec;
+
+ while (curAttachment && curAttachment->m_url) {
+ rv = curAttachment->m_url->GetSpec(spec);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgAttachment> attachment = do_CreateInstance(
+ "@mozilla.org/messengercompose/attachment;1", &rv);
+ if (NS_SUCCEEDED(rv) && attachment) {
+ nsAutoString nameStr;
+ rv = nsMsgI18NConvertToUnicode("UTF-8"_ns, curAttachment->m_realName,
+ nameStr);
+ if (NS_FAILED(rv))
+ CopyASCIItoUTF16(curAttachment->m_realName, nameStr);
+ attachment->SetName(nameStr);
+ attachment->SetUrl(spec);
+ attachment->SetTemporary(true);
+ attachment->SetContentType(curAttachment->m_realType.get());
+ attachment->SetMacType(curAttachment->m_xMacType.get());
+ attachment->SetMacCreator(curAttachment->m_xMacCreator.get());
+ attachment->SetSize(curAttachment->m_size);
+ if (!curAttachment->m_cloudPartInfo.IsEmpty()) {
+ nsCString provider;
+ nsCString cloudUrl;
+ nsCString cloudPartHeaderData;
+
+ provider.Adopt(
+ MimeHeaders_get_parameter(curAttachment->m_cloudPartInfo.get(),
+ "provider", nullptr, nullptr));
+ cloudUrl.Adopt(MimeHeaders_get_parameter(
+ curAttachment->m_cloudPartInfo.get(), "url", nullptr, nullptr));
+ cloudPartHeaderData.Adopt(
+ MimeHeaders_get_parameter(curAttachment->m_cloudPartInfo.get(),
+ "data", nullptr, nullptr));
+
+ attachment->SetSendViaCloud(true);
+ attachment->SetCloudFileAccountKey(provider);
+ attachment->SetContentLocation(cloudUrl);
+ attachment->SetCloudPartHeaderData(cloudPartHeaderData);
+ }
+ compFields->AddAttachment(attachment);
+ }
+ }
+ curAttachment++;
+ }
+ }
+
+ MSG_ComposeFormat format = composeFormat; // Format to actually use.
+ if (identity && composeType == nsIMsgCompType::ForwardInline) {
+ bool composeHtml = false;
+ identity->GetComposeHtml(&composeHtml);
+ if (composeHtml)
+ format = (composeFormat == nsIMsgCompFormat::OppositeOfDefault)
+ ? nsIMsgCompFormat::PlainText
+ : nsIMsgCompFormat::HTML;
+ else
+ format = (composeFormat == nsIMsgCompFormat::OppositeOfDefault)
+ ? nsIMsgCompFormat::HTML
+ : nsIMsgCompFormat::PlainText;
+ }
+
+ pMsgComposeParams =
+ do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ pMsgComposeParams->SetType(composeType);
+ pMsgComposeParams->SetFormat(format);
+ pMsgComposeParams->SetIdentity(identity);
+ pMsgComposeParams->SetComposeFields(compFields);
+ if (!originalMsgURI.IsEmpty())
+ pMsgComposeParams->SetOriginalMsgURI(originalMsgURI);
+ if (origMsgHdr) pMsgComposeParams->SetOrigMsgHdr(origMsgHdr);
+ return NS_OK;
+}
+
+nsresult CreateTheComposeWindow(nsIMsgCompFields* compFields,
+ nsMsgAttachmentData* attachmentList,
+ MSG_ComposeType composeType,
+ MSG_ComposeFormat composeFormat,
+ nsIMsgIdentity* identity,
+ const nsACString& originalMsgURI,
+ nsIMsgDBHdr* origMsgHdr) {
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams;
+ nsresult rv = CreateComposeParams(pMsgComposeParams, compFields,
+ attachmentList, composeType, composeFormat,
+ identity, originalMsgURI, origMsgHdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgComposeService> msgComposeService =
+ do_GetService(kCMsgComposeServiceCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return msgComposeService->OpenComposeWindowWithParams(
+ nullptr /* default chrome */, pMsgComposeParams);
+}
+
+nsresult ForwardMsgInline(nsIMsgCompFields* compFields,
+ nsMsgAttachmentData* attachmentList,
+ MSG_ComposeFormat composeFormat,
+ nsIMsgIdentity* identity,
+ const nsACString& originalMsgURI,
+ nsIMsgDBHdr* origMsgHdr) {
+ nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams;
+ nsresult rv =
+ CreateComposeParams(pMsgComposeParams, compFields, attachmentList,
+ nsIMsgCompType::ForwardInline, composeFormat,
+ identity, originalMsgURI, origMsgHdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgComposeService> msgComposeService =
+ do_GetService(kCMsgComposeServiceCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // create the nsIMsgCompose object to send the object
+ nsCOMPtr<nsIMsgCompose> pMsgCompose(
+ do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /** initialize nsIMsgCompose, Send the message, wait for send completion
+ * response **/
+ rv = pMsgCompose->Initialize(pMsgComposeParams, nullptr, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::dom::Promise> promise;
+ rv = pMsgCompose->SendMsg(nsIMsgSend::nsMsgDeliverNow, identity, nullptr,
+ nullptr, nullptr, getter_AddRefs(promise));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgFolder> origFolder;
+ origMsgHdr->GetFolder(getter_AddRefs(origFolder));
+ if (origFolder)
+ origFolder->AddMessageDispositionState(
+ origMsgHdr, nsIMsgFolder::nsMsgDispositionState_Forwarded);
+ }
+ return rv;
+}
+
+nsresult CreateCompositionFields(
+ const char* from, const char* reply_to, const char* to, const char* cc,
+ const char* bcc, const char* fcc, const char* newsgroups,
+ const char* followup_to, const char* organization, const char* subject,
+ const char* references, const char* priority, const char* newspost_url,
+ const nsTArray<nsString>& otherHeaders, char* charset,
+ nsIMsgCompFields** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv;
+ *_retval = nullptr;
+
+ nsCOMPtr<nsIMsgCompFields> cFields =
+ do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cFields, NS_ERROR_OUT_OF_MEMORY);
+
+ nsAutoCString val;
+ nsAutoString outString;
+
+ if (from) {
+ nsMsgI18NConvertRawBytesToUTF16(
+ nsDependentCString(from),
+ charset ? nsDependentCString(charset) : EmptyCString(), outString);
+ cFields->SetFrom(outString);
+ }
+
+ if (subject) {
+ MIME_DecodeMimeHeader(subject, charset, false, true, val);
+ cFields->SetSubject(
+ NS_ConvertUTF8toUTF16(!val.IsEmpty() ? val.get() : subject));
+ }
+
+ if (reply_to) {
+ nsMsgI18NConvertRawBytesToUTF16(
+ nsDependentCString(reply_to),
+ charset ? nsDependentCString(charset) : EmptyCString(), outString);
+ cFields->SetReplyTo(outString);
+ }
+
+ if (to) {
+ nsMsgI18NConvertRawBytesToUTF16(
+ nsDependentCString(to),
+ charset ? nsDependentCString(charset) : EmptyCString(), outString);
+ cFields->SetTo(outString);
+ }
+
+ if (cc) {
+ nsMsgI18NConvertRawBytesToUTF16(
+ nsDependentCString(cc),
+ charset ? nsDependentCString(charset) : EmptyCString(), outString);
+ cFields->SetCc(outString);
+ }
+
+ if (bcc) {
+ nsMsgI18NConvertRawBytesToUTF16(
+ nsDependentCString(bcc),
+ charset ? nsDependentCString(charset) : EmptyCString(), outString);
+ cFields->SetBcc(outString);
+ }
+
+ if (fcc) {
+ MIME_DecodeMimeHeader(fcc, charset, false, true, val);
+ cFields->SetFcc(NS_ConvertUTF8toUTF16(!val.IsEmpty() ? val.get() : fcc));
+ }
+
+ if (newsgroups) {
+ // fixme: the newsgroups header had better be decoded using the server-side
+ // character encoding,but this |charset| might be different from it.
+ MIME_DecodeMimeHeader(newsgroups, charset, false, true, val);
+ cFields->SetNewsgroups(
+ NS_ConvertUTF8toUTF16(!val.IsEmpty() ? val.get() : newsgroups));
+ }
+
+ if (followup_to) {
+ MIME_DecodeMimeHeader(followup_to, charset, false, true, val);
+ cFields->SetFollowupTo(
+ NS_ConvertUTF8toUTF16(!val.IsEmpty() ? val.get() : followup_to));
+ }
+
+ if (organization) {
+ MIME_DecodeMimeHeader(organization, charset, false, true, val);
+ cFields->SetOrganization(
+ NS_ConvertUTF8toUTF16(!val.IsEmpty() ? val.get() : organization));
+ }
+
+ if (references) {
+ MIME_DecodeMimeHeader(references, charset, false, true, val);
+ cFields->SetReferences(!val.IsEmpty() ? val.get() : references);
+ }
+
+ if (priority) {
+ MIME_DecodeMimeHeader(priority, charset, false, true, val);
+ nsMsgPriorityValue priorityValue;
+ NS_MsgGetPriorityFromString(!val.IsEmpty() ? val.get() : priority,
+ priorityValue);
+ nsAutoCString priorityName;
+ NS_MsgGetUntranslatedPriorityName(priorityValue, priorityName);
+ cFields->SetPriority(priorityName.get());
+ }
+
+ if (newspost_url) {
+ MIME_DecodeMimeHeader(newspost_url, charset, false, true, val);
+ cFields->SetNewspostUrl(!val.IsEmpty() ? val.get() : newspost_url);
+ }
+
+ nsTArray<nsString> cFieldsOtherHeaders;
+ cFields->GetOtherHeaders(cFieldsOtherHeaders);
+ for (auto otherHeader : otherHeaders) {
+ if (!otherHeader.IsEmpty()) {
+ MIME_DecodeMimeHeader(NS_ConvertUTF16toUTF8(otherHeader).get(), charset,
+ false, true, val);
+ cFieldsOtherHeaders.AppendElement(NS_ConvertUTF8toUTF16(val));
+ } else {
+ cFieldsOtherHeaders.AppendElement(u""_ns);
+ }
+ }
+ cFields->SetOtherHeaders(cFieldsOtherHeaders);
+ cFields.forget(_retval);
+ return rv;
+}
+
+static int dummy_file_write(char* buf, int32_t size, void* fileHandle) {
+ if (!fileHandle) return -1;
+
+ nsIOutputStream* tStream = (nsIOutputStream*)fileHandle;
+ uint32_t bytesWritten;
+ tStream->Write(buf, size, &bytesWritten);
+ return (int)bytesWritten;
+}
+
+static int mime_parse_stream_write(nsMIMESession* stream, const char* buf,
+ int32_t size) {
+ mime_draft_data* mdd = (mime_draft_data*)stream->data_object;
+ NS_ASSERTION(mdd, "null mime draft data!");
+
+ if (!mdd || !mdd->obj) return -1;
+
+ return mdd->obj->clazz->parse_buffer((char*)buf, size, mdd->obj);
+}
+
+static void mime_free_attachments(nsTArray<nsMsgAttachedFile*>& attachments) {
+ if (attachments.Length() <= 0) return;
+
+ for (uint32_t i = 0; i < attachments.Length(); i++) {
+ if (attachments[i]->m_tmpFile) {
+ attachments[i]->m_tmpFile->Remove(false);
+ attachments[i]->m_tmpFile = nullptr;
+ }
+ delete attachments[i];
+ }
+}
+
+static nsMsgAttachmentData* mime_draft_process_attachments(
+ mime_draft_data* mdd) {
+ if (!mdd) return nullptr;
+
+ nsMsgAttachmentData *attachData = NULL, *tmp = NULL;
+ nsMsgAttachedFile* tmpFile = NULL;
+
+ // It's possible we must treat the message body as attachment!
+ bool bodyAsAttachment = false;
+ if (mdd->messageBody && !mdd->messageBody->m_type.IsEmpty() &&
+ mdd->messageBody->m_type.LowerCaseFindASCII("text/html") == kNotFound &&
+ mdd->messageBody->m_type.LowerCaseFindASCII("text/plain") == kNotFound &&
+ !mdd->messageBody->m_type.LowerCaseEqualsLiteral("text")) {
+ bodyAsAttachment = true;
+ }
+
+ if (!mdd->attachments.Length() && !bodyAsAttachment) return nullptr;
+
+ int32_t totalCount = mdd->attachments.Length();
+ if (bodyAsAttachment) totalCount++;
+ attachData = new nsMsgAttachmentData[totalCount + 1];
+ if (!attachData) return nullptr;
+
+ tmp = attachData;
+
+ for (int i = 0, attachmentsIndex = 0; i < totalCount; i++, tmp++) {
+ if (bodyAsAttachment && i == 0)
+ tmpFile = mdd->messageBody;
+ else
+ tmpFile = mdd->attachments[attachmentsIndex++];
+
+ if (tmpFile->m_type.LowerCaseEqualsLiteral("text/vcard") ||
+ tmpFile->m_type.LowerCaseEqualsLiteral("text/x-vcard"))
+ tmp->m_realName = tmpFile->m_description;
+
+ if (tmpFile->m_origUrl) {
+ nsAutoCString tmpSpec;
+ if (NS_FAILED(tmpFile->m_origUrl->GetSpec(tmpSpec))) goto FAIL;
+
+ if (NS_FAILED(
+ nsMimeNewURI(getter_AddRefs(tmp->m_url), tmpSpec.get(), nullptr)))
+ goto FAIL;
+
+ if (tmp->m_realName.IsEmpty()) {
+ if (!tmpFile->m_realName.IsEmpty())
+ tmp->m_realName = tmpFile->m_realName;
+ else {
+ if (tmpFile->m_type.LowerCaseFindASCII(MESSAGE_RFC822) != kNotFound)
+ // we have the odd case of processing an e-mail that had an unnamed
+ // eml message attached
+ tmp->m_realName = "ForwardedMessage.eml";
+
+ else
+ tmp->m_realName = tmpSpec.get();
+ }
+ }
+ }
+
+ tmp->m_desiredType = tmpFile->m_type;
+ tmp->m_realType = tmpFile->m_type;
+ tmp->m_realEncoding = tmpFile->m_encoding;
+ tmp->m_description = tmpFile->m_description;
+ tmp->m_cloudPartInfo = tmpFile->m_cloudPartInfo;
+ tmp->m_xMacType = tmpFile->m_xMacType;
+ tmp->m_xMacCreator = tmpFile->m_xMacCreator;
+ tmp->m_size = tmpFile->m_size;
+ }
+ return attachData;
+
+FAIL:
+ delete[] attachData;
+ return nullptr;
+}
+
+static void mime_intl_insert_message_header_1(
+ char** body, const char* hdr_value, const char* hdr_str,
+ const char* html_hdr_str, const char* mailcharset, bool htmlEdit) {
+ if (!body || !hdr_value || !hdr_str) return;
+
+ if (htmlEdit) {
+ NS_MsgSACat(body, HEADER_START_JUNK);
+ } else {
+ NS_MsgSACat(body, MSG_LINEBREAK);
+ }
+ if (!html_hdr_str) html_hdr_str = hdr_str;
+ NS_MsgSACat(body, html_hdr_str);
+ if (htmlEdit) {
+ NS_MsgSACat(body, HEADER_MIDDLE_JUNK);
+ } else
+ NS_MsgSACat(body, ": ");
+
+ // MIME decode header
+ nsAutoCString utf8Value;
+ MIME_DecodeMimeHeader(hdr_value, mailcharset, false, true, utf8Value);
+ if (!utf8Value.IsEmpty()) {
+ if (htmlEdit) {
+ nsCString escaped;
+ nsAppendEscapedHTML(utf8Value, escaped);
+ NS_MsgSACat(body, escaped.get());
+ } else {
+ NS_MsgSACat(body, utf8Value.get());
+ }
+ } else {
+ NS_MsgSACat(body, hdr_value); // raw MIME encoded string
+ }
+
+ if (htmlEdit) NS_MsgSACat(body, HEADER_END_JUNK);
+}
+
+char* MimeGetNamedString(int32_t id) {
+ static char retString[256];
+
+ retString[0] = '\0';
+ char* tString = MimeGetStringByID(id);
+ if (tString) {
+ PL_strncpy(retString, tString, sizeof(retString));
+ PR_Free(tString);
+ }
+ return retString;
+}
+
+void MimeGetForwardHeaderDelimiter(nsACString& retString) {
+ nsCString defaultValue;
+ defaultValue.Adopt(MimeGetStringByID(MIME_FORWARDED_MESSAGE_HTML_USER_WROTE));
+
+ nsString tmpRetString;
+ NS_GetLocalizedUnicharPreferenceWithDefault(
+ nullptr, "mailnews.forward_header_originalmessage",
+ NS_ConvertUTF8toUTF16(defaultValue), tmpRetString);
+
+ CopyUTF16toUTF8(tmpRetString, retString);
+}
+
+/* given an address string passed though parameter "address", this one will be
+ converted and returned through the same parameter. The original string will
+ be destroyed
+*/
+static void UnquoteMimeAddress(nsACString& mimeHeader, const char* charset) {
+ if (!mimeHeader.IsEmpty()) {
+ nsTArray<nsCString> addresses;
+ ExtractDisplayAddresses(EncodedHeader(mimeHeader, charset),
+ UTF16ArrayAdapter<>(addresses));
+ mimeHeader.Truncate();
+
+ uint32_t count = addresses.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ if (i != 0) mimeHeader.AppendASCII(", ");
+ mimeHeader += addresses[i];
+ }
+ }
+}
+
+static void mime_insert_all_headers(char** body, MimeHeaders* headers,
+ MSG_ComposeFormat composeFormat,
+ char* mailcharset) {
+ bool htmlEdit = (composeFormat == nsIMsgCompFormat::HTML);
+ char* newBody = NULL;
+ char* html_tag = nullptr;
+ if (*body && PL_strncasecmp(*body, "<HTML", 5) == 0)
+ html_tag = PL_strchr(*body, '>') + 1;
+ int i;
+
+ if (!headers->done_p) {
+ MimeHeaders_build_heads_list(headers);
+ headers->done_p = true;
+ }
+
+ nsCString replyHeader;
+ MimeGetForwardHeaderDelimiter(replyHeader);
+ if (htmlEdit) {
+ NS_MsgSACopy(&(newBody), MIME_FORWARD_HTML_PREFIX);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ NS_MsgSACat(&newBody, MIME_HEADER_TABLE);
+ } else {
+ NS_MsgSACopy(&(newBody), MSG_LINEBREAK MSG_LINEBREAK);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ }
+
+ for (i = 0; i < headers->heads_size; i++) {
+ char* head = headers->heads[i];
+ char* end = (i == headers->heads_size - 1
+ ? headers->all_headers + headers->all_headers_fp
+ : headers->heads[i + 1]);
+ char *colon, *ocolon;
+ char* contents;
+ char* name = 0;
+
+ // Hack for BSD Mailbox delimiter.
+ if (i == 0 && head[0] == 'F' && !strncmp(head, "From ", 5)) {
+ colon = head + 4;
+ contents = colon + 1;
+ } else {
+ /* Find the colon. */
+ for (colon = head; colon < end; colon++)
+ if (*colon == ':') break;
+
+ if (colon >= end) continue; /* junk */
+
+ /* Back up over whitespace before the colon. */
+ ocolon = colon;
+ for (; colon > head && IS_SPACE(colon[-1]); colon--)
+ ;
+
+ contents = ocolon + 1;
+ }
+
+ /* Skip over whitespace after colon. */
+ while (contents <= end && IS_SPACE(*contents)) contents++;
+
+ /* Take off trailing whitespace... */
+ while (end > contents && IS_SPACE(end[-1])) end--;
+
+ name = (char*)PR_MALLOC(colon - head + 1);
+ if (!name) return /* MIME_OUT_OF_MEMORY */;
+ memcpy(name, head, colon - head);
+ name[colon - head] = 0;
+
+ nsAutoCString headerValue;
+ headerValue.Assign(contents, end - contents);
+
+ /* Do not reveal bcc recipients when forwarding a message!
+ See http://bugzilla.mozilla.org/show_bug.cgi?id=41150
+ */
+ if (PL_strcasecmp(name, "bcc") != 0) {
+ if (!PL_strcasecmp(name, "resent-from") || !PL_strcasecmp(name, "from") ||
+ !PL_strcasecmp(name, "resent-to") || !PL_strcasecmp(name, "to") ||
+ !PL_strcasecmp(name, "resent-cc") || !PL_strcasecmp(name, "cc") ||
+ !PL_strcasecmp(name, "reply-to"))
+ UnquoteMimeAddress(headerValue, mailcharset);
+
+ mime_intl_insert_message_header_1(&newBody, headerValue.get(), name, name,
+ mailcharset, htmlEdit);
+ }
+ PR_Free(name);
+ }
+
+ if (htmlEdit) {
+ NS_MsgSACat(&newBody, "</TABLE>");
+ NS_MsgSACat(&newBody, MSG_LINEBREAK "<BR><BR>");
+ if (html_tag)
+ NS_MsgSACat(&newBody, html_tag);
+ else if (*body)
+ NS_MsgSACat(&newBody, *body);
+ } else {
+ NS_MsgSACat(&newBody, MSG_LINEBREAK MSG_LINEBREAK);
+ if (*body) NS_MsgSACat(&newBody, *body);
+ }
+
+ if (newBody) {
+ PR_FREEIF(*body);
+ *body = newBody;
+ }
+}
+
+static void mime_insert_normal_headers(char** body, MimeHeaders* headers,
+ MSG_ComposeFormat composeFormat,
+ char* mailcharset) {
+ char* newBody = nullptr;
+ char* subject = MimeHeaders_get(headers, HEADER_SUBJECT, false, false);
+ char* resent_comments =
+ MimeHeaders_get(headers, HEADER_RESENT_COMMENTS, false, false);
+ char* resent_date = MimeHeaders_get(headers, HEADER_RESENT_DATE, false, true);
+ nsCString resent_from(
+ MimeHeaders_get(headers, HEADER_RESENT_FROM, false, true));
+ nsCString resent_to(MimeHeaders_get(headers, HEADER_RESENT_TO, false, true));
+ nsCString resent_cc(MimeHeaders_get(headers, HEADER_RESENT_CC, false, true));
+ char* date = MimeHeaders_get(headers, HEADER_DATE, false, true);
+ nsCString from(MimeHeaders_get(headers, HEADER_FROM, false, true));
+ nsCString reply_to(MimeHeaders_get(headers, HEADER_REPLY_TO, false, true));
+ char* organization =
+ MimeHeaders_get(headers, HEADER_ORGANIZATION, false, false);
+ nsCString to(MimeHeaders_get(headers, HEADER_TO, false, true));
+ nsCString cc(MimeHeaders_get(headers, HEADER_CC, false, true));
+ char* newsgroups = MimeHeaders_get(headers, HEADER_NEWSGROUPS, false, true);
+ char* followup_to = MimeHeaders_get(headers, HEADER_FOLLOWUP_TO, false, true);
+ char* references = MimeHeaders_get(headers, HEADER_REFERENCES, false, true);
+ const char* html_tag = nullptr;
+ if (*body && PL_strncasecmp(*body, "<HTML", 5) == 0)
+ html_tag = PL_strchr(*body, '>') + 1;
+ bool htmlEdit = composeFormat == nsIMsgCompFormat::HTML;
+
+ if (from.IsEmpty())
+ from.Adopt(MimeHeaders_get(headers, HEADER_SENDER, false, true));
+ if (resent_from.IsEmpty())
+ resent_from.Adopt(
+ MimeHeaders_get(headers, HEADER_RESENT_SENDER, false, true));
+
+ UnquoteMimeAddress(resent_from, mailcharset);
+ UnquoteMimeAddress(resent_to, mailcharset);
+ UnquoteMimeAddress(resent_cc, mailcharset);
+ UnquoteMimeAddress(reply_to, mailcharset);
+ UnquoteMimeAddress(from, mailcharset);
+ UnquoteMimeAddress(to, mailcharset);
+ UnquoteMimeAddress(cc, mailcharset);
+
+ nsCString replyHeader;
+ MimeGetForwardHeaderDelimiter(replyHeader);
+ if (htmlEdit) {
+ NS_MsgSACopy(&(newBody), MIME_FORWARD_HTML_PREFIX);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ NS_MsgSACat(&newBody, MIME_HEADER_TABLE);
+ } else {
+ NS_MsgSACopy(&(newBody), MSG_LINEBREAK MSG_LINEBREAK);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ }
+ if (subject)
+ mime_intl_insert_message_header_1(&newBody, subject, HEADER_SUBJECT,
+ MimeGetNamedString(MIME_MHTML_SUBJECT),
+ mailcharset, htmlEdit);
+ if (resent_comments)
+ mime_intl_insert_message_header_1(
+ &newBody, resent_comments, HEADER_RESENT_COMMENTS,
+ MimeGetNamedString(MIME_MHTML_RESENT_COMMENTS), mailcharset, htmlEdit);
+ if (resent_date)
+ mime_intl_insert_message_header_1(
+ &newBody, resent_date, HEADER_RESENT_DATE,
+ MimeGetNamedString(MIME_MHTML_RESENT_DATE), mailcharset, htmlEdit);
+ if (!resent_from.IsEmpty()) {
+ mime_intl_insert_message_header_1(
+ &newBody, resent_from.get(), HEADER_RESENT_FROM,
+ MimeGetNamedString(MIME_MHTML_RESENT_FROM), mailcharset, htmlEdit);
+ }
+ if (!resent_to.IsEmpty()) {
+ mime_intl_insert_message_header_1(
+ &newBody, resent_to.get(), HEADER_RESENT_TO,
+ MimeGetNamedString(MIME_MHTML_RESENT_TO), mailcharset, htmlEdit);
+ }
+ if (!resent_cc.IsEmpty()) {
+ mime_intl_insert_message_header_1(
+ &newBody, resent_cc.get(), HEADER_RESENT_CC,
+ MimeGetNamedString(MIME_MHTML_RESENT_CC), mailcharset, htmlEdit);
+ }
+ if (date)
+ mime_intl_insert_message_header_1(&newBody, date, HEADER_DATE,
+ MimeGetNamedString(MIME_MHTML_DATE),
+ mailcharset, htmlEdit);
+ if (!from.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, from.get(), HEADER_FROM,
+ MimeGetNamedString(MIME_MHTML_FROM),
+ mailcharset, htmlEdit);
+ }
+ if (!reply_to.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, reply_to.get(), HEADER_REPLY_TO,
+ MimeGetNamedString(MIME_MHTML_REPLY_TO),
+ mailcharset, htmlEdit);
+ }
+ if (organization)
+ mime_intl_insert_message_header_1(
+ &newBody, organization, HEADER_ORGANIZATION,
+ MimeGetNamedString(MIME_MHTML_ORGANIZATION), mailcharset, htmlEdit);
+ if (!to.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, to.get(), HEADER_TO,
+ MimeGetNamedString(MIME_MHTML_TO),
+ mailcharset, htmlEdit);
+ }
+ if (!cc.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, cc.get(), HEADER_CC,
+ MimeGetNamedString(MIME_MHTML_CC),
+ mailcharset, htmlEdit);
+ }
+ /*
+ Do not reveal bcc recipients when forwarding a message!
+ See http://bugzilla.mozilla.org/show_bug.cgi?id=41150
+ */
+ if (newsgroups)
+ mime_intl_insert_message_header_1(&newBody, newsgroups, HEADER_NEWSGROUPS,
+ MimeGetNamedString(MIME_MHTML_NEWSGROUPS),
+ mailcharset, htmlEdit);
+ if (followup_to) {
+ mime_intl_insert_message_header_1(
+ &newBody, followup_to, HEADER_FOLLOWUP_TO,
+ MimeGetNamedString(MIME_MHTML_FOLLOWUP_TO), mailcharset, htmlEdit);
+ }
+ // only show references for newsgroups
+ if (newsgroups && references) {
+ mime_intl_insert_message_header_1(&newBody, references, HEADER_REFERENCES,
+ MimeGetNamedString(MIME_MHTML_REFERENCES),
+ mailcharset, htmlEdit);
+ }
+ if (htmlEdit) {
+ NS_MsgSACat(&newBody, "</TABLE>");
+ NS_MsgSACat(&newBody, MSG_LINEBREAK "<BR><BR>");
+ if (html_tag)
+ NS_MsgSACat(&newBody, html_tag);
+ else if (*body)
+ NS_MsgSACat(&newBody, *body);
+ } else {
+ NS_MsgSACat(&newBody, MSG_LINEBREAK MSG_LINEBREAK);
+ if (*body) NS_MsgSACat(&newBody, *body);
+ }
+ if (newBody) {
+ PR_FREEIF(*body);
+ *body = newBody;
+ }
+ PR_FREEIF(subject);
+ PR_FREEIF(resent_comments);
+ PR_FREEIF(resent_date);
+ PR_FREEIF(date);
+ PR_FREEIF(organization);
+ PR_FREEIF(newsgroups);
+ PR_FREEIF(followup_to);
+ PR_FREEIF(references);
+}
+
+static void mime_insert_micro_headers(char** body, MimeHeaders* headers,
+ MSG_ComposeFormat composeFormat,
+ char* mailcharset) {
+ char* newBody = NULL;
+ char* subject = MimeHeaders_get(headers, HEADER_SUBJECT, false, false);
+ nsCString from(MimeHeaders_get(headers, HEADER_FROM, false, true));
+ nsCString resent_from(
+ MimeHeaders_get(headers, HEADER_RESENT_FROM, false, true));
+ char* date = MimeHeaders_get(headers, HEADER_DATE, false, true);
+ nsCString to(MimeHeaders_get(headers, HEADER_TO, false, true));
+ nsCString cc(MimeHeaders_get(headers, HEADER_CC, false, true));
+ char* newsgroups = MimeHeaders_get(headers, HEADER_NEWSGROUPS, false, true);
+ const char* html_tag = nullptr;
+ if (*body && PL_strncasecmp(*body, "<HTML", 5) == 0)
+ html_tag = PL_strchr(*body, '>') + 1;
+ bool htmlEdit = composeFormat == nsIMsgCompFormat::HTML;
+
+ if (from.IsEmpty())
+ from.Adopt(MimeHeaders_get(headers, HEADER_SENDER, false, true));
+ if (resent_from.IsEmpty())
+ resent_from.Adopt(
+ MimeHeaders_get(headers, HEADER_RESENT_SENDER, false, true));
+ if (!date) date = MimeHeaders_get(headers, HEADER_RESENT_DATE, false, true);
+
+ UnquoteMimeAddress(resent_from, mailcharset);
+ UnquoteMimeAddress(from, mailcharset);
+ UnquoteMimeAddress(to, mailcharset);
+ UnquoteMimeAddress(cc, mailcharset);
+
+ nsCString replyHeader;
+ MimeGetForwardHeaderDelimiter(replyHeader);
+ if (htmlEdit) {
+ NS_MsgSACopy(&(newBody), MIME_FORWARD_HTML_PREFIX);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ NS_MsgSACat(&newBody, MIME_HEADER_TABLE);
+ } else {
+ NS_MsgSACopy(&(newBody), MSG_LINEBREAK MSG_LINEBREAK);
+ NS_MsgSACat(&newBody, replyHeader.get());
+ }
+
+ if (!from.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, from.get(), HEADER_FROM,
+ MimeGetNamedString(MIME_MHTML_FROM),
+ mailcharset, htmlEdit);
+ }
+ if (subject)
+ mime_intl_insert_message_header_1(&newBody, subject, HEADER_SUBJECT,
+ MimeGetNamedString(MIME_MHTML_SUBJECT),
+ mailcharset, htmlEdit);
+ /*
+ if (date)
+ mime_intl_insert_message_header_1(&newBody, date, HEADER_DATE,
+ MimeGetNamedString(MIME_MHTML_DATE),
+ mailcharset, htmlEdit);
+ */
+ if (!resent_from.IsEmpty()) {
+ mime_intl_insert_message_header_1(
+ &newBody, resent_from.get(), HEADER_RESENT_FROM,
+ MimeGetNamedString(MIME_MHTML_RESENT_FROM), mailcharset, htmlEdit);
+ }
+ if (!to.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, to.get(), HEADER_TO,
+ MimeGetNamedString(MIME_MHTML_TO),
+ mailcharset, htmlEdit);
+ }
+ if (!cc.IsEmpty()) {
+ mime_intl_insert_message_header_1(&newBody, cc.get(), HEADER_CC,
+ MimeGetNamedString(MIME_MHTML_CC),
+ mailcharset, htmlEdit);
+ }
+ /*
+ Do not reveal bcc recipients when forwarding a message!
+ See http://bugzilla.mozilla.org/show_bug.cgi?id=41150
+ */
+ if (newsgroups)
+ mime_intl_insert_message_header_1(&newBody, newsgroups, HEADER_NEWSGROUPS,
+ MimeGetNamedString(MIME_MHTML_NEWSGROUPS),
+ mailcharset, htmlEdit);
+ if (htmlEdit) {
+ NS_MsgSACat(&newBody, "</TABLE>");
+ NS_MsgSACat(&newBody, MSG_LINEBREAK "<BR><BR>");
+ if (html_tag)
+ NS_MsgSACat(&newBody, html_tag);
+ else if (*body)
+ NS_MsgSACat(&newBody, *body);
+ } else {
+ NS_MsgSACat(&newBody, MSG_LINEBREAK MSG_LINEBREAK);
+ if (*body) NS_MsgSACat(&newBody, *body);
+ }
+ if (newBody) {
+ PR_FREEIF(*body);
+ *body = newBody;
+ }
+ PR_FREEIF(subject);
+ PR_FREEIF(date);
+ PR_FREEIF(newsgroups);
+}
+
+// body has to be encoded in UTF-8
+static void mime_insert_forwarded_message_headers(
+ char** body, MimeHeaders* headers, MSG_ComposeFormat composeFormat,
+ char* mailcharset) {
+ if (!body || !headers) return;
+
+ int32_t show_headers = 0;
+ nsresult res;
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &res));
+ if (NS_SUCCEEDED(res))
+ prefBranch->GetIntPref("mail.show_headers", &show_headers);
+
+ switch (show_headers) {
+ case 0:
+ mime_insert_micro_headers(body, headers, composeFormat, mailcharset);
+ break;
+ default:
+ case 1:
+ mime_insert_normal_headers(body, headers, composeFormat, mailcharset);
+ break;
+ case 2:
+ mime_insert_all_headers(body, headers, composeFormat, mailcharset);
+ break;
+ }
+}
+
+static void convert_plaintext_body_to_html(char** body) {
+ // We need to convert the plain/text to HTML in order to escape any HTML
+ // markup.
+ nsCString escapedBody;
+ nsAppendEscapedHTML(nsDependentCString(*body), escapedBody);
+
+ nsCString newBody;
+ char* q = escapedBody.BeginWriting();
+ char* p;
+ int prevQuoteLevel = 0;
+ bool isFlowed = false;
+ bool haveSig = false;
+
+ // First detect whether this appears to be flowed or not.
+ p = q;
+ while (*p) {
+ // At worst we read the null byte terminator.
+ if (*p == ' ' && (*(p + 1) == '\r' || *(p + 1) == '\n')) {
+ // This looks flowed, but don't get fooled by a signature separator:
+ // --space
+ if (p - 3 >= q && (*(p - 3) == '\r' || *(p - 3) == '\n') &&
+ *(p - 2) == '-' && *(p - 1) == '-') {
+ p++;
+ continue;
+ }
+ if (p - 2 == q && *(p - 2) == '-' && *(p - 1) == '-') {
+ p++;
+ continue;
+ }
+ isFlowed = true;
+ break;
+ }
+ p++;
+ }
+
+ while (*q) {
+ p = q;
+ // Detect quotes. A quote character is a ">" which was escaped to &gt;.
+ // In non-flowed messages the quote character can be optionally followed by
+ // a space. Examples: Level 0
+ // > Level 0 (with leading space)
+ // > Level 1
+ // > > Level 1 (with leading space, note the two spaces between the quote
+ // characters)
+ // >> Level 2
+ // > > Level 2 (only when non-flowed, otherwise Level 1 with leading space)
+ // >>> Level 3
+ // > > > Level 3 (with leading space, only when non-flowed, otherwise Level
+ // 1)
+ int quoteLevel = 0;
+ while (strncmp(p, "&gt;", 4) == 0) {
+ p += 4;
+ if (!isFlowed && *p == ' ') p++;
+ quoteLevel++;
+ }
+
+ // Eat space following quote character, for non-flowed already eaten above.
+ if (quoteLevel > 0 && isFlowed && *p == ' ') p++;
+
+ // Close any open signatures if we find a quote. Strange, that shouldn't
+ // happen.
+ if (quoteLevel > 0 && haveSig) {
+ newBody.AppendLiteral("</pre>");
+ haveSig = false;
+ }
+ if (quoteLevel > prevQuoteLevel) {
+ while (prevQuoteLevel < quoteLevel) {
+ if (isFlowed)
+ newBody.AppendLiteral("<blockquote type=\"cite\">");
+ else
+ newBody.AppendLiteral(
+ "<blockquote type=\"cite\"><pre wrap class=\"moz-quote-pre\">");
+ prevQuoteLevel++;
+ }
+ } else if (quoteLevel < prevQuoteLevel) {
+ while (prevQuoteLevel > quoteLevel) {
+ if (isFlowed)
+ newBody.AppendLiteral("</blockquote>");
+ else
+ newBody.AppendLiteral("</pre></blockquote>");
+ prevQuoteLevel--;
+ }
+ }
+ // Position after the quote.
+ q = p;
+
+ // Detect signature.
+ bool forceBR = false;
+ if (quoteLevel == 0) {
+ if (strncmp(q, "-- \r", 4) == 0 || strncmp(q, "-- \n", 4) == 0) {
+ haveSig = true;
+ forceBR = true;
+ newBody.AppendLiteral("<pre class=\"moz-signature\">");
+ }
+ }
+
+ bool seenSpace = false;
+ while (*p && *p != '\r' && *p != '\n') {
+ seenSpace = (*p == ' ');
+ p++;
+ continue;
+ }
+ if (!*p) {
+ // We're at the end of the string.
+ if (p > q) {
+ // Copy last bit over.
+ newBody.Append(q);
+ }
+ break;
+ }
+ if (*p == '\r' &&
+ *(p + 1) == '\n') { // At worst we read the null byte terminator.
+ // Skip the CR in CRLF.
+ *p = 0; // don't copy skipped \r.
+ p++;
+ }
+ *p = 0;
+ newBody.Append(q);
+ if (!isFlowed || !seenSpace || forceBR) newBody.AppendLiteral("<br>");
+ q = p + 1;
+ }
+
+ // Close all open quotes.
+ while (prevQuoteLevel > 0) {
+ if (isFlowed)
+ newBody.AppendLiteral("</blockquote>");
+ else
+ newBody.AppendLiteral("</pre></blockquote>");
+ prevQuoteLevel--;
+ }
+
+ // Close any open signatures.
+ if (haveSig) {
+ newBody.AppendLiteral("</pre>");
+ haveSig = false;
+ }
+
+ PR_Free(*body);
+ *body = ToNewCString(newBody);
+}
+
+static void mime_parse_stream_complete(nsMIMESession* stream) {
+ mime_draft_data* mdd = (mime_draft_data*)stream->data_object;
+ nsCOMPtr<nsIMsgCompFields> fields;
+ int htmlAction = 0;
+ int lineWidth = 0;
+
+ char* host = 0;
+ char* news_host = 0;
+ char* to_and_cc = 0;
+ char* re_subject = 0;
+ char* new_refs = 0;
+ char* from = 0;
+ char* repl = 0;
+ char* subj = 0;
+ char* id = 0;
+ char* refs = 0;
+ char* to = 0;
+ char* cc = 0;
+ char* bcc = 0;
+ char* fcc = 0;
+ char* org = 0;
+ char* grps = 0;
+ char* foll = 0;
+ char* priority = 0;
+ char* draftInfo = 0;
+ char* contentLanguage = 0;
+ char* identityKey = 0;
+ nsTArray<nsString> readOtherHeaders;
+
+ bool forward_inline = false;
+ bool bodyAsAttachment = false;
+ bool charsetOverride = false;
+
+ NS_ASSERTION(mdd, "null mime draft data");
+
+ if (!mdd) return;
+
+ if (mdd->obj) {
+ int status;
+
+ status = mdd->obj->clazz->parse_eof(mdd->obj, false);
+ mdd->obj->clazz->parse_end(mdd->obj, status < 0 ? true : false);
+
+ // RICHIE
+ // We need to figure out how to pass the forwarded flag along with this
+ // operation.
+
+ // forward_inline = (mdd->format_out != FO_CMDLINE_ATTACHMENTS);
+ forward_inline = mdd->forwardInline;
+
+ NS_ASSERTION(mdd->options == mdd->obj->options,
+ "mime draft options not same as obj->options");
+ mime_free(mdd->obj);
+ mdd->obj = 0;
+ if (mdd->options) {
+ // save the override flag before it's unavailable
+ charsetOverride = mdd->options->override_charset;
+ // Override the charset only if requested. If the message doesn't have
+ // one and we're not overriding, we'll detect it later.
+ if (charsetOverride && mdd->options->default_charset) {
+ PR_FREEIF(mdd->mailcharset);
+ mdd->mailcharset = strdup(mdd->options->default_charset);
+ }
+
+ // mscott: aren't we leaking a bunch of strings here like the charset
+ // strings and such?
+ delete mdd->options;
+ mdd->options = 0;
+ }
+ if (mdd->stream) {
+ mdd->stream->complete((nsMIMESession*)mdd->stream->data_object);
+ PR_Free(mdd->stream);
+ mdd->stream = 0;
+ }
+ }
+
+ //
+ // Now, process the attachments that we have gathered from the message
+ // on disk
+ //
+ nsMsgAttachmentData* newAttachData = mime_draft_process_attachments(mdd);
+
+ //
+ // time to bring up the compose windows with all the info gathered
+ //
+ if (mdd->headers) {
+ subj = MimeHeaders_get(mdd->headers, HEADER_SUBJECT, false, false);
+ if (forward_inline) {
+ if (subj) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString fwdPrefix;
+ prefBranch->GetCharPref("mail.forward_subject_prefix", fwdPrefix);
+ char* newSubj = PR_smprintf(
+ "%s: %s", !fwdPrefix.IsEmpty() ? fwdPrefix.get() : "Fwd", subj);
+ if (newSubj) {
+ PR_Free(subj);
+ subj = newSubj;
+ }
+ }
+ }
+ } else {
+ from = MimeHeaders_get(mdd->headers, HEADER_FROM, false, false);
+ repl = MimeHeaders_get(mdd->headers, HEADER_REPLY_TO, false, false);
+ to = MimeHeaders_get(mdd->headers, HEADER_TO, false, true);
+ cc = MimeHeaders_get(mdd->headers, HEADER_CC, false, true);
+ bcc = MimeHeaders_get(mdd->headers, HEADER_BCC, false, true);
+
+ /* These headers should not be RFC-1522-decoded. */
+ grps = MimeHeaders_get(mdd->headers, HEADER_NEWSGROUPS, false, true);
+ foll = MimeHeaders_get(mdd->headers, HEADER_FOLLOWUP_TO, false, true);
+
+ host = MimeHeaders_get(mdd->headers, HEADER_X_MOZILLA_NEWSHOST, false,
+ false);
+ if (!host)
+ host = MimeHeaders_get(mdd->headers, HEADER_NNTP_POSTING_HOST, false,
+ false);
+
+ id = MimeHeaders_get(mdd->headers, HEADER_MESSAGE_ID, false, false);
+ refs = MimeHeaders_get(mdd->headers, HEADER_REFERENCES, false, true);
+ priority = MimeHeaders_get(mdd->headers, HEADER_X_PRIORITY, false, false);
+
+ if (host) {
+ char* secure = NULL;
+
+ secure = PL_strcasestr(host, "secure");
+ if (secure) {
+ *secure = 0;
+ news_host = PR_smprintf("snews://%s", host);
+ } else {
+ news_host = PR_smprintf("news://%s", host);
+ }
+ }
+
+ // Other headers via pref.
+ nsCString otherHeaders;
+ nsTArray<nsCString> otherHeadersArray;
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ pPrefBranch->GetCharPref("mail.compose.other.header", otherHeaders);
+ if (!otherHeaders.IsEmpty()) {
+ ToLowerCase(otherHeaders);
+ ParseString(otherHeaders, ',', otherHeadersArray);
+ for (auto otherHeader : otherHeadersArray) {
+ otherHeader.Trim(" ");
+ nsAutoCString result;
+ result.Assign(
+ MimeHeaders_get(mdd->headers, otherHeader.get(), false, false));
+ readOtherHeaders.AppendElement(NS_ConvertUTF8toUTF16(result));
+ }
+ }
+ }
+
+ CreateCompositionFields(from, repl, to, cc, bcc, fcc, grps, foll, org, subj,
+ refs, priority, news_host, readOtherHeaders,
+ mdd->mailcharset, getter_AddRefs(fields));
+
+ contentLanguage =
+ MimeHeaders_get(mdd->headers, HEADER_CONTENT_LANGUAGE, false, false);
+ if (contentLanguage) {
+ fields->SetContentLanguage(contentLanguage);
+ }
+
+ draftInfo = MimeHeaders_get(mdd->headers, HEADER_X_MOZILLA_DRAFT_INFO,
+ false, false);
+
+ // We always preserve an existing message ID, if present, apart from some
+ // exceptions.
+ bool keepID = fields != nullptr;
+
+ // Don't keep ID when forwarding inline.
+ if (forward_inline) keepID = false;
+
+ // nsMimeOutput::nsMimeMessageEditorTemplate is used for editing a message
+ // "as new", creating a message from a template or editing a template.
+ // Only in the latter case we want to preserve the ID.
+ if (mdd->format_out == nsMimeOutput::nsMimeMessageEditorTemplate &&
+ !PL_strstr(mdd->url_name, "&edittempl=true"))
+ keepID = false;
+
+ if (keepID) fields->SetMessageId(id);
+
+ if (draftInfo && fields && !forward_inline) {
+ char* parm = 0;
+ parm = MimeHeaders_get_parameter(draftInfo, "vcard", NULL, NULL);
+ fields->SetAttachVCard(parm && !strcmp(parm, "1"));
+ PR_FREEIF(parm);
+
+ parm = MimeHeaders_get_parameter(draftInfo, "receipt", NULL, NULL);
+ if (!parm || !strcmp(parm, "0"))
+ fields->SetReturnReceipt(false);
+ else {
+ int receiptType = 0;
+ fields->SetReturnReceipt(true);
+ sscanf(parm, "%d", &receiptType);
+ // slight change compared to 4.x; we used to use receipt= to tell
+ // whether the draft/template has request for either MDN or DNS or both
+ // return receipt; since the DNS is out of the picture we now use the
+ // header type - 1 to tell whether user has requested the return receipt
+ fields->SetReceiptHeaderType(((int32_t)receiptType) - 1);
+ }
+ PR_FREEIF(parm);
+ parm = MimeHeaders_get_parameter(draftInfo, "DSN", NULL, NULL);
+ fields->SetDSN(parm && !strcmp(parm, "1"));
+ PR_Free(parm);
+ parm = MimeHeaders_get_parameter(draftInfo, "html", NULL, NULL);
+ if (parm) sscanf(parm, "%d", &htmlAction);
+ PR_FREEIF(parm);
+ parm = MimeHeaders_get_parameter(draftInfo, "linewidth", NULL, NULL);
+ if (parm) sscanf(parm, "%d", &lineWidth);
+ PR_FREEIF(parm);
+ parm = MimeHeaders_get_parameter(draftInfo, "attachmentreminder", NULL,
+ NULL);
+ if (parm && !strcmp(parm, "1"))
+ fields->SetAttachmentReminder(true);
+ else
+ fields->SetAttachmentReminder(false);
+ PR_FREEIF(parm);
+ parm = MimeHeaders_get_parameter(draftInfo, "deliveryformat", NULL, NULL);
+ if (parm) {
+ int32_t deliveryFormat = nsIMsgCompSendFormat::Unset;
+ sscanf(parm, "%d", &deliveryFormat);
+ fields->SetDeliveryFormat(deliveryFormat);
+ }
+ PR_FREEIF(parm);
+ }
+
+ // identity to prefer when opening the message in the compose window?
+ identityKey = MimeHeaders_get(mdd->headers, HEADER_X_MOZILLA_IDENTITY_KEY,
+ false, false);
+ if (identityKey && *identityKey) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ nsCOMPtr<nsIMsgIdentity> overrulingIdentity;
+ rv = accountManager->GetIdentity(nsDependentCString(identityKey),
+ getter_AddRefs(overrulingIdentity));
+
+ if (NS_SUCCEEDED(rv) && overrulingIdentity) {
+ mdd->identity = overrulingIdentity;
+ fields->SetCreatorIdentityKey(identityKey);
+ }
+ }
+ }
+
+ if (mdd->messageBody) {
+ MSG_ComposeFormat composeFormat = nsIMsgCompFormat::Default;
+ if (!mdd->messageBody->m_type.IsEmpty()) {
+ if (mdd->messageBody->m_type.LowerCaseFindASCII("text/html") !=
+ kNotFound)
+ composeFormat = nsIMsgCompFormat::HTML;
+ else if (mdd->messageBody->m_type.LowerCaseFindASCII("text/plain") !=
+ kNotFound ||
+ mdd->messageBody->m_type.LowerCaseEqualsLiteral("text"))
+ composeFormat = nsIMsgCompFormat::PlainText;
+ else
+ // We cannot use this kind of data for the message body! Therefore,
+ // move it as attachment
+ bodyAsAttachment = true;
+ } else
+ composeFormat = nsIMsgCompFormat::PlainText;
+
+ char* body = nullptr;
+
+ if (!bodyAsAttachment && mdd->messageBody->m_tmpFile) {
+ int64_t fileSize;
+ nsCOMPtr<nsIFile> tempFileCopy;
+ mdd->messageBody->m_tmpFile->Clone(getter_AddRefs(tempFileCopy));
+ mdd->messageBody->m_tmpFile = tempFileCopy;
+ tempFileCopy = nullptr;
+ mdd->messageBody->m_tmpFile->GetFileSize(&fileSize);
+ uint32_t bodyLen = 0;
+
+ // The stream interface can only read up to 4GB (32bit uint).
+ // It is highly unlikely to encounter a body lager than that limit,
+ // so we just skip it instead of reading it in chunks.
+ if (fileSize < UINT32_MAX) {
+ bodyLen = fileSize;
+ body = (char*)PR_MALLOC(bodyLen + 1);
+ }
+ if (body) {
+ memset(body, 0, bodyLen + 1);
+
+ uint32_t bytesRead;
+ nsCOMPtr<nsIInputStream> inputStream;
+
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
+ mdd->messageBody->m_tmpFile);
+ if (NS_FAILED(rv)) return;
+
+ inputStream->Read(body, bodyLen, &bytesRead);
+
+ inputStream->Close();
+
+ // Convert the body to UTF-8
+ char* mimeCharset = nullptr;
+ // Get a charset from the header if no override is set.
+ if (!charsetOverride)
+ mimeCharset = MimeHeaders_get_parameter(
+ mdd->messageBody->m_type.get(), "charset", nullptr, nullptr);
+ // If no charset is specified in the header then use the default.
+ nsAutoCString bodyCharset;
+ if (mimeCharset) {
+ bodyCharset.Adopt(mimeCharset);
+ } else if (mdd->mailcharset) {
+ bodyCharset.Assign(mdd->mailcharset);
+ }
+ if (bodyCharset.IsEmpty()) {
+ nsAutoCString detectedCharset;
+ // We need to detect it.
+ rv = MIME_detect_charset(body, bodyLen, detectedCharset);
+ if (NS_SUCCEEDED(rv) && !detectedCharset.IsEmpty()) {
+ bodyCharset = detectedCharset;
+ }
+ }
+ if (!bodyCharset.IsEmpty()) {
+ nsAutoString tmpUnicodeBody;
+ rv = nsMsgI18NConvertToUnicode(
+ bodyCharset, nsDependentCString(body), tmpUnicodeBody);
+ if (NS_FAILED(rv)) // Tough luck, ASCII/ISO-8859-1 then...
+ CopyASCIItoUTF16(nsDependentCString(body), tmpUnicodeBody);
+
+ char* newBody = ToNewUTF8String(tmpUnicodeBody);
+ if (newBody) {
+ PR_Free(body);
+ body = newBody;
+ }
+ }
+ }
+ }
+
+ bool convertToPlainText = false;
+ if (forward_inline) {
+ if (mdd->identity) {
+ bool identityComposeHTML;
+ mdd->identity->GetComposeHtml(&identityComposeHTML);
+ if ((identityComposeHTML && !mdd->overrideComposeFormat) ||
+ (!identityComposeHTML && mdd->overrideComposeFormat)) {
+ // In the end, we're going to compose in HTML mode...
+
+ if (body && composeFormat == nsIMsgCompFormat::PlainText) {
+ // ... but the message body is currently plain text.
+ convert_plaintext_body_to_html(&body);
+ }
+ // Body is now HTML, set the format too (so headers are inserted in
+ // correct format).
+ composeFormat = nsIMsgCompFormat::HTML;
+ } else if ((identityComposeHTML && mdd->overrideComposeFormat) ||
+ !identityComposeHTML) {
+ // In the end, we're going to compose in plain text mode...
+
+ if (composeFormat == nsIMsgCompFormat::HTML) {
+ // ... but the message body is currently HTML.
+ // We'll do the conversion later on when headers have been
+ // inserted, body has been set and converted to unicode.
+ convertToPlainText = true;
+ }
+ }
+ }
+
+ mime_insert_forwarded_message_headers(&body, mdd->headers,
+ composeFormat, mdd->mailcharset);
+ }
+
+ MSG_ComposeType msgComposeType = 0; // Keep compilers happy.
+ if (mdd->format_out == nsMimeOutput::nsMimeMessageEditorTemplate) {
+ if (PL_strstr(mdd->url_name, "?redirect=true") ||
+ PL_strstr(mdd->url_name, "&redirect=true"))
+ msgComposeType = nsIMsgCompType::Redirect;
+ else if (PL_strstr(mdd->url_name, "?editasnew=true") ||
+ PL_strstr(mdd->url_name, "&editasnew=true"))
+ msgComposeType = nsIMsgCompType::EditAsNew;
+ else if (PL_strstr(mdd->url_name, "?edittempl=true") ||
+ PL_strstr(mdd->url_name, "&edittempl=true"))
+ msgComposeType = nsIMsgCompType::EditTemplate;
+ else
+ msgComposeType = nsIMsgCompType::Template;
+ }
+
+ if (body && msgComposeType == nsIMsgCompType::EditAsNew) {
+ // When editing as new, we respect the identities preferred format
+ // which can be overridden.
+ if (mdd->identity) {
+ bool identityComposeHTML;
+ mdd->identity->GetComposeHtml(&identityComposeHTML);
+
+ if (composeFormat == nsIMsgCompFormat::HTML &&
+ identityComposeHTML == mdd->overrideComposeFormat) {
+ // We we have HTML:
+ // If they want HTML and they want to override it (true == true)
+ // or they don't want HTML and they don't want to override it
+ // (false == false), then convert. Conversion happens below.
+ convertToPlainText = true;
+ composeFormat = nsIMsgCompFormat::PlainText;
+ } else if (composeFormat == nsIMsgCompFormat::PlainText &&
+ identityComposeHTML != mdd->overrideComposeFormat) {
+ // We have plain text:
+ // If they want HTML and they don't want to override it (true !=
+ // false) or they don't want HTML and they want to override it
+ // (false != true), then convert.
+ convert_plaintext_body_to_html(&body);
+ composeFormat = nsIMsgCompFormat::HTML;
+ }
+ }
+ } else if (body && mdd->overrideComposeFormat &&
+ (msgComposeType == nsIMsgCompType::Template ||
+ msgComposeType == nsIMsgCompType::EditTemplate ||
+ !mdd->forwardInline)) // Draft processing.
+ {
+ // When using a template and overriding, the user gets the
+ // "other" format.
+ if (composeFormat == nsIMsgCompFormat::PlainText) {
+ convert_plaintext_body_to_html(&body);
+ composeFormat = nsIMsgCompFormat::HTML;
+ } else {
+ // Conversion happens below.
+ convertToPlainText = true;
+ composeFormat = nsIMsgCompFormat::PlainText;
+ }
+ }
+
+ // convert from UTF-8 to UTF-16
+ if (body) {
+ fields->SetBody(NS_ConvertUTF8toUTF16(body));
+ PR_Free(body);
+ }
+
+ //
+ // At this point, we need to create a message compose window or editor
+ // window via XP-COM with the information that we have retrieved from
+ // the message store.
+ //
+ if (mdd->format_out == nsMimeOutput::nsMimeMessageEditorTemplate) {
+ // Set the draft ID when editing a template so the original is
+ // overwritten when saving the template again.
+ // Note that always setting the draft ID here would cause drafts to be
+ // overwritten when edited "as new", which is undesired.
+ if (msgComposeType == nsIMsgCompType::EditTemplate) {
+ fields->SetDraftId(nsDependentCString(mdd->url_name));
+ fields->SetTemplateId(nsDependentCString(
+ mdd->url_name)); // Remember original template ID.
+ }
+
+ if (convertToPlainText) fields->ConvertBodyToPlainText();
+
+ CreateTheComposeWindow(fields, newAttachData, msgComposeType,
+ composeFormat, mdd->identity,
+ mdd->originalMsgURI, mdd->origMsgHdr);
+ } else {
+ if (mdd->forwardInline) {
+ if (convertToPlainText) fields->ConvertBodyToPlainText();
+ if (mdd->overrideComposeFormat)
+ composeFormat = nsIMsgCompFormat::OppositeOfDefault;
+ if (mdd->forwardInlineFilter) {
+ fields->SetTo(mdd->forwardToAddress);
+ ForwardMsgInline(fields, newAttachData, composeFormat,
+ mdd->identity, mdd->originalMsgURI,
+ mdd->origMsgHdr);
+ } else
+ CreateTheComposeWindow(fields, newAttachData,
+ nsIMsgCompType::ForwardInline, composeFormat,
+ mdd->identity, mdd->originalMsgURI,
+ mdd->origMsgHdr);
+ } else {
+ if (convertToPlainText) fields->ConvertBodyToPlainText();
+ fields->SetDraftId(nsDependentCString(mdd->url_name));
+ CreateTheComposeWindow(fields, newAttachData, nsIMsgCompType::Draft,
+ composeFormat, mdd->identity,
+ mdd->originalMsgURI, mdd->origMsgHdr);
+ }
+ }
+ } else {
+ //
+ // At this point, we need to create a message compose window via
+ // XP-COM with the information that we have retrieved from the message
+ // store.
+ //
+ if (mdd->format_out == nsMimeOutput::nsMimeMessageEditorTemplate) {
+#ifdef NS_DEBUG
+ printf(
+ "RICHIE: Time to create the EDITOR with this template - NO "
+ "body!!!!\n");
+#endif
+ CreateTheComposeWindow(fields, newAttachData, nsIMsgCompType::Template,
+ nsIMsgCompFormat::Default, mdd->identity,
+ EmptyCString(), mdd->origMsgHdr);
+ } else {
+#ifdef NS_DEBUG
+ printf("Time to create the composition window WITHOUT a body!!!!\n");
+#endif
+ if (mdd->forwardInline) {
+ MSG_ComposeFormat composeFormat =
+ (mdd->overrideComposeFormat) ? nsIMsgCompFormat::OppositeOfDefault
+ : nsIMsgCompFormat::Default;
+ CreateTheComposeWindow(fields, newAttachData,
+ nsIMsgCompType::ForwardInline, composeFormat,
+ mdd->identity, mdd->originalMsgURI,
+ mdd->origMsgHdr);
+ } else {
+ fields->SetDraftId(nsDependentCString(mdd->url_name));
+ CreateTheComposeWindow(fields, newAttachData, nsIMsgCompType::Draft,
+ nsIMsgCompFormat::Default, mdd->identity,
+ EmptyCString(), mdd->origMsgHdr);
+ }
+ }
+ }
+ } else {
+ CreateCompositionFields(from, repl, to, cc, bcc, fcc, grps, foll, org, subj,
+ refs, priority, news_host, readOtherHeaders,
+ mdd->mailcharset, getter_AddRefs(fields));
+ if (fields)
+ CreateTheComposeWindow(fields, newAttachData, nsIMsgCompType::New,
+ nsIMsgCompFormat::Default, mdd->identity,
+ EmptyCString(), mdd->origMsgHdr);
+ }
+
+ if (mdd->headers) MimeHeaders_free(mdd->headers);
+
+ //
+ // Free the original attachment structure...
+ // Make sure we only cleanup the local copy of the memory and not kill
+ // files we need on disk
+ //
+ if (bodyAsAttachment)
+ mdd->messageBody->m_tmpFile = nullptr;
+ else if (mdd->messageBody && mdd->messageBody->m_tmpFile)
+ mdd->messageBody->m_tmpFile->Remove(false);
+
+ delete mdd->messageBody;
+
+ for (uint32_t i = 0; i < mdd->attachments.Length(); i++)
+ mdd->attachments[i]->m_tmpFile = nullptr;
+
+ PR_FREEIF(mdd->mailcharset);
+
+ mdd->identity = nullptr;
+ PR_Free(mdd->url_name);
+ mdd->origMsgHdr = nullptr;
+ PR_Free(mdd);
+
+ PR_FREEIF(host);
+ PR_FREEIF(to_and_cc);
+ PR_FREEIF(re_subject);
+ PR_FREEIF(new_refs);
+ PR_FREEIF(from);
+ PR_FREEIF(repl);
+ PR_FREEIF(subj);
+ PR_FREEIF(id);
+ PR_FREEIF(refs);
+ PR_FREEIF(to);
+ PR_FREEIF(cc);
+ PR_FREEIF(grps);
+ PR_FREEIF(foll);
+ PR_FREEIF(priority);
+ PR_FREEIF(draftInfo);
+ PR_Free(identityKey);
+
+ delete[] newAttachData;
+}
+
+static void mime_parse_stream_abort(nsMIMESession* stream, int status) {
+ mime_draft_data* mdd = (mime_draft_data*)stream->data_object;
+ NS_ASSERTION(mdd, "null mime draft data");
+
+ if (!mdd) return;
+
+ if (mdd->obj) {
+ int status = 0;
+
+ if (!mdd->obj->closed_p)
+ status = mdd->obj->clazz->parse_eof(mdd->obj, true);
+ if (!mdd->obj->parsed_p) mdd->obj->clazz->parse_end(mdd->obj, true);
+
+ NS_ASSERTION(mdd->options == mdd->obj->options,
+ "draft display options not same as mime obj");
+ mime_free(mdd->obj);
+ mdd->obj = 0;
+ if (mdd->options) {
+ delete mdd->options;
+ mdd->options = 0;
+ }
+
+ if (mdd->stream) {
+ mdd->stream->abort((nsMIMESession*)mdd->stream->data_object, status);
+ PR_Free(mdd->stream);
+ mdd->stream = 0;
+ }
+ }
+
+ if (mdd->headers) MimeHeaders_free(mdd->headers);
+
+ mime_free_attachments(mdd->attachments);
+
+ PR_FREEIF(mdd->mailcharset);
+
+ PR_Free(mdd);
+}
+
+static int make_mime_headers_copy(void* closure, MimeHeaders* headers) {
+ mime_draft_data* mdd = (mime_draft_data*)closure;
+
+ NS_ASSERTION(mdd && headers, "null mime draft data and/or headers");
+
+ if (!mdd || !headers) return 0;
+
+ NS_ASSERTION(mdd->headers == NULL, "non null mime draft data headers");
+
+ mdd->headers = MimeHeaders_copy(headers);
+ mdd->options->done_parsing_outer_headers = true;
+
+ return 0;
+}
+
+int mime_decompose_file_init_fn(void* stream_closure, MimeHeaders* headers) {
+ mime_draft_data* mdd = (mime_draft_data*)stream_closure;
+ nsMsgAttachedFile* newAttachment = 0;
+ int nAttachments = 0;
+ // char *hdr_value = NULL;
+ char* parm_value = NULL;
+ bool creatingMsgBody = true;
+
+ NS_ASSERTION(mdd && headers, "null mime draft data and/or headers");
+ if (!mdd || !headers) return -1;
+
+ if (mdd->options->decompose_init_count) {
+ mdd->options->decompose_init_count++;
+ NS_ASSERTION(mdd->curAttachment,
+ "missing attachment in mime_decompose_file_init_fn");
+ if (mdd->curAttachment)
+ mdd->curAttachment->m_type.Adopt(
+ MimeHeaders_get(headers, HEADER_CONTENT_TYPE, false, true));
+ return 0;
+ } else
+ mdd->options->decompose_init_count++;
+
+ nAttachments = mdd->attachments.Length();
+
+ if (!nAttachments && !mdd->messageBody) {
+ // if we've been told to use an override charset then do so....otherwise use
+ // the charset inside the message header...
+ if (mdd->options && mdd->options->override_charset) {
+ if (mdd->options->default_charset)
+ mdd->mailcharset = strdup(mdd->options->default_charset);
+ else {
+ mdd->mailcharset = strdup("");
+ mdd->autodetectCharset = true;
+ }
+ } else {
+ char* contentType;
+ contentType = MimeHeaders_get(headers, HEADER_CONTENT_TYPE, false, false);
+ if (contentType) {
+ mdd->mailcharset =
+ MimeHeaders_get_parameter(contentType, "charset", NULL, NULL);
+ PR_FREEIF(contentType);
+ }
+ }
+
+ mdd->messageBody = new nsMsgAttachedFile;
+ if (!mdd->messageBody) return MIME_OUT_OF_MEMORY;
+ newAttachment = mdd->messageBody;
+ creatingMsgBody = true;
+ } else {
+ /* always allocate one more extra; don't ask me why */
+ newAttachment = new nsMsgAttachedFile;
+ if (!newAttachment) return MIME_OUT_OF_MEMORY;
+ mdd->attachments.AppendElement(newAttachment);
+ }
+
+ char* workURLSpec = nullptr;
+ char* contLoc = nullptr;
+
+ newAttachment->m_realName.Adopt(MimeHeaders_get_name(headers, mdd->options));
+ contLoc = MimeHeaders_get(headers, HEADER_CONTENT_LOCATION, false, false);
+ if (!contLoc)
+ contLoc = MimeHeaders_get(headers, HEADER_CONTENT_BASE, false, false);
+
+ if (!contLoc && !newAttachment->m_realName.IsEmpty())
+ workURLSpec = ToNewCString(newAttachment->m_realName);
+ if (contLoc && !workURLSpec) workURLSpec = strdup(contLoc);
+
+ PR_FREEIF(contLoc);
+
+ mdd->curAttachment = newAttachment;
+ newAttachment->m_type.Adopt(
+ MimeHeaders_get(headers, HEADER_CONTENT_TYPE, false, false));
+
+ //
+ // This is to handle the degenerated Apple Double attachment.
+ //
+ parm_value = MimeHeaders_get(headers, HEADER_CONTENT_TYPE, false, false);
+ if (parm_value) {
+ char* boundary = NULL;
+ char* tmp_value = NULL;
+ boundary = MimeHeaders_get_parameter(parm_value, "boundary", NULL, NULL);
+ if (boundary) tmp_value = PR_smprintf("; boundary=\"%s\"", boundary);
+ if (tmp_value) newAttachment->m_type = tmp_value;
+ newAttachment->m_xMacType.Adopt(
+ MimeHeaders_get_parameter(parm_value, "x-mac-type", NULL, NULL));
+ newAttachment->m_xMacCreator.Adopt(
+ MimeHeaders_get_parameter(parm_value, "x-mac-creator", NULL, NULL));
+ PR_FREEIF(parm_value);
+ PR_FREEIF(boundary);
+ PR_FREEIF(tmp_value);
+ }
+
+ newAttachment->m_size = 0;
+ newAttachment->m_encoding.Adopt(
+ MimeHeaders_get(headers, HEADER_CONTENT_TRANSFER_ENCODING, false, false));
+ newAttachment->m_description.Adopt(
+ MimeHeaders_get(headers, HEADER_CONTENT_DESCRIPTION, false, false));
+ //
+ // If we came up empty for description or the orig URL, we should do something
+ // about it.
+ //
+ if (newAttachment->m_description.IsEmpty() && workURLSpec)
+ newAttachment->m_description = workURLSpec;
+
+ PR_FREEIF(workURLSpec); // resource leak otherwise
+
+ newAttachment->m_cloudPartInfo.Adopt(
+ MimeHeaders_get(headers, HEADER_X_MOZILLA_CLOUD_PART, false, false));
+
+ nsCOMPtr<nsIFile> tmpFile = nullptr;
+ {
+ // Let's build a temp file with an extension based on the content-type:
+ // nsmail.<extension>
+
+ nsAutoCString newAttachName("nsmail");
+ nsAutoCString fileExtension;
+ // the content type may contain a charset. i.e. text/html; ISO-2022-JP...we
+ // want to strip off the charset before we ask the mime service for a mime
+ // info for this content type.
+ nsAutoCString contentType(newAttachment->m_type);
+ int32_t pos = contentType.FindChar(';');
+ if (pos > 0) contentType.SetLength(pos);
+ int32_t extLoc = newAttachment->m_realName.RFindChar('.');
+ int32_t specLength = newAttachment->m_realName.Length();
+ // @see nsExternalHelperAppService::GetTypeFromURI()
+ if (extLoc != -1 && extLoc != specLength - 1 &&
+ // nothing over 20 chars long can be sanely considered an
+ // extension.... Dat dere would be just data.
+ specLength - extLoc < 20) {
+ fileExtension = Substring(newAttachment->m_realName, extLoc + 1);
+ } else {
+ nsCOMPtr<nsIMIMEService> mimeFinder(
+ do_GetService(NS_MIMESERVICE_CONTRACTID));
+ if (mimeFinder) {
+ mimeFinder->GetPrimaryExtension(contentType, ""_ns, fileExtension);
+ }
+ }
+
+ if (fileExtension.IsEmpty()) {
+ newAttachName.AppendLiteral(".tmp");
+ } else {
+ newAttachName.Append('.');
+ newAttachName.Append(fileExtension);
+ }
+
+ nsMsgCreateTempFile(newAttachName.get(), getter_AddRefs(tmpFile));
+ }
+ nsresult rv;
+
+ // This needs to be done so the attachment structure has a handle
+ // on the temp file for this attachment...
+ if (tmpFile) {
+ nsAutoCString fileURL;
+ rv = NS_GetURLSpecFromFile(tmpFile, fileURL);
+ if (NS_SUCCEEDED(rv))
+ nsMimeNewURI(getter_AddRefs(newAttachment->m_origUrl), fileURL.get(),
+ nullptr);
+ }
+
+ if (!tmpFile) return MIME_OUT_OF_MEMORY;
+
+ mdd->tmpFile = tmpFile;
+
+ newAttachment->m_tmpFile = mdd->tmpFile;
+
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mdd->tmpFileStream),
+ tmpFile, PR_WRONLY | PR_CREATE_FILE,
+ 00600);
+ if (NS_FAILED(rv)) return MIME_UNABLE_TO_OPEN_TMP_FILE;
+
+ // For now, we are always going to decode all of the attachments
+ // for the message. This way, we have native data
+ if (creatingMsgBody) {
+ MimeDecoderData* (*fn)(MimeConverterOutputCallback, void*) = 0;
+
+ //
+ // Initialize a decoder if necessary.
+ //
+ if (newAttachment->m_encoding.LowerCaseEqualsLiteral(ENCODING_BASE64))
+ fn = &MimeB64DecoderInit;
+ else if (newAttachment->m_encoding.LowerCaseEqualsLiteral(
+ ENCODING_QUOTED_PRINTABLE)) {
+ mdd->decoder_data =
+ MimeQPDecoderInit(/* The (MimeConverterOutputCallback) cast is to turn
+ the `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)dummy_file_write),
+ mdd->tmpFileStream);
+ if (!mdd->decoder_data) return MIME_OUT_OF_MEMORY;
+ } else if (newAttachment->m_encoding.LowerCaseEqualsLiteral(
+ ENCODING_UUENCODE) ||
+ newAttachment->m_encoding.LowerCaseEqualsLiteral(
+ ENCODING_UUENCODE2) ||
+ newAttachment->m_encoding.LowerCaseEqualsLiteral(
+ ENCODING_UUENCODE3) ||
+ newAttachment->m_encoding.LowerCaseEqualsLiteral(
+ ENCODING_UUENCODE4))
+ fn = &MimeUUDecoderInit;
+ else if (newAttachment->m_encoding.LowerCaseEqualsLiteral(ENCODING_YENCODE))
+ fn = &MimeYDecoderInit;
+
+ if (fn) {
+ mdd->decoder_data = fn(/* The (MimeConverterOutputCallback) cast is to
+ turn the `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)dummy_file_write),
+ mdd->tmpFileStream);
+ if (!mdd->decoder_data) return MIME_OUT_OF_MEMORY;
+ }
+ }
+
+ return 0;
+}
+
+int mime_decompose_file_output_fn(const char* buf, int32_t size,
+ void* stream_closure) {
+ mime_draft_data* mdd = (mime_draft_data*)stream_closure;
+ int ret = 0;
+
+ NS_ASSERTION(mdd && buf, "missing mime draft data and/or buf");
+ if (!mdd || !buf) return -1;
+ if (!size) return 0;
+
+ if (!mdd->tmpFileStream) return 0;
+
+ if (mdd->autodetectCharset) {
+ nsAutoCString detectedCharset;
+ nsresult res = NS_OK;
+ res = MIME_detect_charset(buf, size, detectedCharset);
+ if (NS_SUCCEEDED(res) && !detectedCharset.IsEmpty()) {
+ mdd->mailcharset = ToNewCString(detectedCharset);
+ mdd->autodetectCharset = false;
+ }
+ }
+
+ if (mdd->decoder_data) {
+ int32_t outsize;
+ ret = MimeDecoderWrite(mdd->decoder_data, buf, size, &outsize);
+ if (ret == -1) return -1;
+ mdd->curAttachment->m_size += outsize;
+ } else {
+ uint32_t bytesWritten;
+ mdd->tmpFileStream->Write(buf, size, &bytesWritten);
+ if ((int32_t)bytesWritten < size) return MIME_ERROR_WRITING_FILE;
+ mdd->curAttachment->m_size += size;
+ }
+
+ return 0;
+}
+
+int mime_decompose_file_close_fn(void* stream_closure) {
+ mime_draft_data* mdd = (mime_draft_data*)stream_closure;
+
+ if (!mdd) return -1;
+
+ if (--mdd->options->decompose_init_count > 0) return 0;
+
+ if (mdd->decoder_data) {
+ MimeDecoderDestroy(mdd->decoder_data, false);
+ mdd->decoder_data = 0;
+ }
+
+ if (!mdd->tmpFileStream) {
+ // it's ok to have a null tmpFileStream if there's no tmpFile.
+ // This happens for cloud file attachments.
+ NS_ASSERTION(!mdd->tmpFile, "shouldn't have a tmp file bu no stream");
+ return 0;
+ }
+ mdd->tmpFileStream->Close();
+
+ mdd->tmpFileStream = nullptr;
+
+ mdd->tmpFile = nullptr;
+
+ return 0;
+}
+
+extern "C" void* mime_bridge_create_draft_stream(
+ nsIMimeEmitter* newEmitter, nsStreamConverter* newPluginObj2, nsIURI* uri,
+ nsMimeOutputType format_out) {
+ int status = 0;
+ nsMIMESession* stream = nullptr;
+ mime_draft_data* mdd = nullptr;
+ MimeObject* obj = nullptr;
+
+ if (!uri) return nullptr;
+
+ mdd = new mime_draft_data;
+ if (!mdd) return nullptr;
+
+ nsAutoCString turl;
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsCOMPtr<nsIURI> aURL;
+ nsAutoCString urlString;
+ nsresult rv;
+
+ // first, convert the rdf msg uri into a url that represents the message...
+ if (NS_FAILED(uri->GetSpec(turl))) goto FAIL;
+
+ rv = GetMessageServiceFromURI(turl, getter_AddRefs(msgService));
+ if (NS_FAILED(rv)) goto FAIL;
+
+ rv = msgService->GetUrlForUri(turl, nullptr, getter_AddRefs(aURL));
+ if (NS_FAILED(rv)) goto FAIL;
+
+ if (NS_SUCCEEDED(aURL->GetSpec(urlString))) {
+ int32_t typeIndex = urlString.Find("&type=application/x-message-display");
+ if (typeIndex != -1)
+ urlString.Cut(typeIndex,
+ sizeof("&type=application/x-message-display") - 1);
+
+ mdd->url_name = ToNewCString(urlString);
+ if (!(mdd->url_name)) goto FAIL;
+ }
+
+ newPluginObj2->GetForwardInline(&mdd->forwardInline);
+ newPluginObj2->GetForwardInlineFilter(&mdd->forwardInlineFilter);
+ newPluginObj2->GetForwardToAddress(mdd->forwardToAddress);
+ newPluginObj2->GetOverrideComposeFormat(&mdd->overrideComposeFormat);
+ newPluginObj2->GetIdentity(getter_AddRefs(mdd->identity));
+ newPluginObj2->GetOriginalMsgURI(mdd->originalMsgURI);
+ newPluginObj2->GetOrigMsgHdr(getter_AddRefs(mdd->origMsgHdr));
+ mdd->format_out = format_out;
+ mdd->options = new MimeDisplayOptions;
+ if (!mdd->options) goto FAIL;
+
+ mdd->options->url = strdup(mdd->url_name);
+ mdd->options->format_out = format_out; // output format
+ mdd->options->decompose_file_p = true; /* new field in MimeDisplayOptions */
+ mdd->options->stream_closure = mdd;
+ mdd->options->html_closure = mdd;
+ mdd->options->decompose_headers_info_fn = make_mime_headers_copy;
+ mdd->options->decompose_file_init_fn = mime_decompose_file_init_fn;
+ mdd->options->decompose_file_output_fn = mime_decompose_file_output_fn;
+ mdd->options->decompose_file_close_fn = mime_decompose_file_close_fn;
+
+ mdd->options->m_prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) goto FAIL;
+
+#ifdef ENABLE_SMIME
+ /* If we're attaching a message (for forwarding) then we must eradicate all
+ traces of xlateion from it, since forwarding someone else a message
+ that wasn't xlated for them doesn't work. We have to dexlate it
+ before sending it.
+ */
+ mdd->options->decrypt_p = true;
+#endif /* ENABLE_SMIME */
+
+ obj = mime_new((MimeObjectClass*)&mimeMessageClass, (MimeHeaders*)NULL,
+ MESSAGE_RFC822);
+ if (!obj) goto FAIL;
+
+ obj->options = mdd->options;
+ mdd->obj = obj;
+
+ stream = PR_NEWZAP(nsMIMESession);
+ if (!stream) goto FAIL;
+
+ stream->name = "MIME To Draft Converter Stream";
+ stream->complete = mime_parse_stream_complete;
+ stream->abort = mime_parse_stream_abort;
+ stream->put_block = mime_parse_stream_write;
+ stream->data_object = mdd;
+
+ status = obj->clazz->initialize(obj);
+ if (status >= 0) status = obj->clazz->parse_begin(obj);
+ if (status < 0) goto FAIL;
+
+ return stream;
+
+FAIL:
+ if (mdd) {
+ PR_Free(mdd->url_name);
+ if (mdd->options) delete mdd->options;
+ PR_Free(mdd);
+ }
+ PR_Free(stream);
+ PR_Free(obj);
+
+ return nullptr;
+}
diff --git a/comm/mailnews/mime/src/mimeebod.cpp b/comm/mailnews/mime/src/mimeebod.cpp
new file mode 100644
index 0000000000..755574b1b0
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeebod.cpp
@@ -0,0 +1,441 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "mimeebod.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "prio.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsINetUtil.h"
+#include <ctype.h>
+
+#define MIME_SUPERCLASS mimeObjectClass
+MimeDefClass(MimeExternalBody, MimeExternalBodyClass, mimeExternalBodyClass,
+ &MIME_SUPERCLASS);
+
+#ifdef XP_MACOSX
+extern MimeObjectClass mimeMultipartAppleDoubleClass;
+#endif
+
+static int MimeExternalBody_initialize(MimeObject*);
+static void MimeExternalBody_finalize(MimeObject*);
+static int MimeExternalBody_parse_line(const char*, int32_t, MimeObject*);
+static int MimeExternalBody_parse_eof(MimeObject*, bool);
+static bool MimeExternalBody_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs);
+
+#if 0
+# if defined(DEBUG) && defined(XP_UNIX)
+static int MimeExternalBody_debug_print (MimeObject *, PRFileDesc *, int32_t);
+# endif
+#endif /* 0 */
+
+static int MimeExternalBodyClassInitialize(MimeExternalBodyClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ oclass->initialize = MimeExternalBody_initialize;
+ oclass->finalize = MimeExternalBody_finalize;
+ oclass->parse_line = MimeExternalBody_parse_line;
+ oclass->parse_eof = MimeExternalBody_parse_eof;
+ oclass->displayable_inline_p = MimeExternalBody_displayable_inline_p;
+
+#if 0
+# if defined(DEBUG) && defined(XP_UNIX)
+ oclass->debug_print = MimeExternalBody_debug_print;
+# endif
+#endif /* 0 */
+
+ return 0;
+}
+
+static int MimeExternalBody_initialize(MimeObject* object) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeExternalBody_finalize(MimeObject* object) {
+ MimeExternalBody* bod = (MimeExternalBody*)object;
+ if (bod->hdrs) {
+ MimeHeaders_free(bod->hdrs);
+ bod->hdrs = 0;
+ }
+ PR_FREEIF(bod->body);
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeExternalBody_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeExternalBody* bod = (MimeExternalBody*)obj;
+ int status = 0;
+
+ NS_ASSERTION(line && *line, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!line || !*line) return -1;
+
+ if (!obj->output_p) return 0;
+
+ /* If we're supposed to write this object, but aren't supposed to convert
+ it to HTML, simply pass it through unaltered. */
+ if (obj->options && !obj->options->write_html_p && obj->options->output_fn)
+ return MimeObject_write(obj, line, length, true);
+
+ /* If we already have a `body' then we're done parsing headers, and all
+ subsequent lines get tacked onto the body. */
+ if (bod->body) {
+ int L = strlen(bod->body);
+ char* new_str = (char*)PR_Realloc(bod->body, L + length + 1);
+ if (!new_str) return MIME_OUT_OF_MEMORY;
+ bod->body = new_str;
+ memcpy(bod->body + L, line, length);
+ bod->body[L + length] = 0;
+ return 0;
+ }
+
+ /* Otherwise we don't yet have a body, which means we're not done parsing
+ our headers.
+ */
+ if (!bod->hdrs) {
+ bod->hdrs = MimeHeaders_new();
+ if (!bod->hdrs) return MIME_OUT_OF_MEMORY;
+ }
+
+ status = MimeHeaders_parse_line(line, length, bod->hdrs);
+ if (status < 0) return status;
+
+ /* If this line is blank, we're now done parsing headers, and should
+ create a dummy body to show that. Gag.
+ */
+ if (*line == '\r' || *line == '\n') {
+ bod->body = strdup("");
+ if (!bod->body) return MIME_OUT_OF_MEMORY;
+ }
+
+ return 0;
+}
+
+char* MimeExternalBody_make_url(const char* ct, const char* at,
+ const char* lexp, const char* size,
+ const char* perm, const char* dir,
+ const char* mode, const char* name,
+ const char* url, const char* site,
+ const char* svr, const char* subj,
+ const char* body) {
+ char* s;
+ uint32_t slen;
+ if (!at) {
+ return 0;
+ } else if (!PL_strcasecmp(at, "ftp") || !PL_strcasecmp(at, "anon-ftp")) {
+ if (!site || !name) return 0;
+
+ slen = strlen(name) + strlen(site) + (dir ? strlen(dir) : 0) + 20;
+ s = (char*)PR_MALLOC(slen);
+
+ if (!s) return 0;
+ PL_strncpyz(s, "ftp://", slen);
+ PL_strcatn(s, slen, site);
+ PL_strcatn(s, slen, "/");
+ if (dir) PL_strcatn(s, slen, (dir[0] == '/' ? dir + 1 : dir));
+ if (s[strlen(s) - 1] != '/') PL_strcatn(s, slen, "/");
+ PL_strcatn(s, slen, name);
+ return s;
+#ifdef XP_UNIX
+ } else if (!PL_strcasecmp(at, "local-file") || !PL_strcasecmp(at, "afs")) {
+ if (!name) return 0;
+
+ if (!PL_strcasecmp(at, "afs")) /* only if there is a /afs/ directory */
+ {
+ nsCOMPtr<nsIFile> fs = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ bool exists = false;
+ if (fs) {
+ fs->InitWithNativePath("/afs/."_ns);
+ fs->Exists(&exists);
+ }
+ if (!exists) return 0;
+ }
+
+ slen = (strlen(name) * 3 + 20);
+ s = (char*)PR_MALLOC(slen);
+ if (!s) return 0;
+ PL_strncpyz(s, "file:", slen);
+
+ nsCString s2;
+ MsgEscapeString(nsDependentCString(name), nsINetUtil::ESCAPE_URL_PATH, s2);
+ PL_strcatn(s, slen, s2.get());
+ return s;
+#endif
+ } else if (!PL_strcasecmp(at, "mail-server")) {
+ if (!svr) return 0;
+
+ slen = (strlen(svr) * 4 + (subj ? strlen(subj) * 4 : 0) +
+ (body ? strlen(body) * 4 : 0) +
+ 25); // dpv xxx: why 4x? %xx escaping should be 3x
+ s = (char*)PR_MALLOC(slen);
+ if (!s) return 0;
+ PL_strncpyz(s, "mailto:", slen);
+
+ nsCString s2;
+ MsgEscapeString(nsDependentCString(svr), nsINetUtil::ESCAPE_XALPHAS, s2);
+ PL_strcatn(s, slen, s2.get());
+
+ if (subj) {
+ MsgEscapeString(nsDependentCString(subj), nsINetUtil::ESCAPE_XALPHAS, s2);
+ PL_strcatn(s, slen, "?subject=");
+ PL_strcatn(s, slen, s2.get());
+ }
+ if (body) {
+ MsgEscapeString(nsDependentCString(body), nsINetUtil::ESCAPE_XALPHAS, s2);
+ PL_strcatn(s, slen, (subj ? "&body=" : "?body="));
+ PL_strcatn(s, slen, s2.get());
+ }
+ return s;
+ } else if (!PL_strcasecmp(at, "url")) /* RFC 2017 */
+ {
+ if (url)
+ return strdup(url); /* it's already quoted and everything */
+ else
+ return 0;
+ } else
+ return 0;
+}
+
+static int MimeExternalBody_parse_eof(MimeObject* obj, bool abort_p) {
+ int status = 0;
+ MimeExternalBody* bod = (MimeExternalBody*)obj;
+
+ if (obj->closed_p) return 0;
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+#ifdef XP_MACOSX
+ if (obj->parent &&
+ mime_typep(obj->parent, (MimeObjectClass*)&mimeMultipartAppleDoubleClass))
+ goto done;
+#endif /* XP_MACOSX */
+
+ if (!abort_p && obj->output_p && obj->options && obj->options->write_html_p) {
+ bool all_headers_p = obj->options->headers == MimeHeadersAll;
+ MimeDisplayOptions* newopt = obj->options; /* copy it */
+
+ char* ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ char *at, *lexp, *size, *perm;
+ char *url, *dir, *mode, *name, *site, *svr, *subj;
+ char *h = 0, *lname = 0, *lurl = 0, *body = 0;
+ MimeHeaders* hdrs = 0;
+
+ if (!ct) return MIME_OUT_OF_MEMORY;
+
+ at = MimeHeaders_get_parameter(ct, "access-type", NULL, NULL);
+ lexp = MimeHeaders_get_parameter(ct, "expiration", NULL, NULL);
+ size = MimeHeaders_get_parameter(ct, "size", NULL, NULL);
+ perm = MimeHeaders_get_parameter(ct, "permission", NULL, NULL);
+ dir = MimeHeaders_get_parameter(ct, "directory", NULL, NULL);
+ mode = MimeHeaders_get_parameter(ct, "mode", NULL, NULL);
+ name = MimeHeaders_get_parameter(ct, "name", NULL, NULL);
+ site = MimeHeaders_get_parameter(ct, "site", NULL, NULL);
+ svr = MimeHeaders_get_parameter(ct, "server", NULL, NULL);
+ subj = MimeHeaders_get_parameter(ct, "subject", NULL, NULL);
+ url = MimeHeaders_get_parameter(ct, "url", NULL, NULL);
+ PR_FREEIF(ct);
+
+ /* the *internal* content-type */
+ ct = MimeHeaders_get(bod->hdrs, HEADER_CONTENT_TYPE, true, false);
+
+ uint32_t hlen = ((at ? strlen(at) : 0) + (lexp ? strlen(lexp) : 0) +
+ (size ? strlen(size) : 0) + (perm ? strlen(perm) : 0) +
+ (dir ? strlen(dir) : 0) + (mode ? strlen(mode) : 0) +
+ (name ? strlen(name) : 0) + (site ? strlen(site) : 0) +
+ (svr ? strlen(svr) : 0) + (subj ? strlen(subj) : 0) +
+ (ct ? strlen(ct) : 0) + (url ? strlen(url) : 0) + 100);
+
+ h = (char*)PR_MALLOC(hlen);
+ if (!h) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+
+ /* If there's a URL parameter, remove all whitespace from it.
+ (The URL parameter to one of these headers is stored with
+ lines broken every 40 characters or less; it's assumed that
+ all significant whitespace was URL-hex-encoded, and all the
+ rest of it was inserted just to keep the lines short.)
+ */
+ if (url) {
+ char *in, *out;
+ for (in = url, out = url; *in; in++)
+ if (!IS_SPACE(*in)) *out++ = *in;
+ *out = 0;
+ }
+
+ hdrs = MimeHeaders_new();
+ if (!hdrs) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+
+#define FROB(STR, VAR) \
+ if (VAR) { \
+ PL_strncpyz(h, STR ": ", hlen); \
+ PL_strcatn(h, hlen, VAR); \
+ PL_strcatn(h, hlen, MSG_LINEBREAK); \
+ status = MimeHeaders_parse_line(h, strlen(h), hdrs); \
+ if (status < 0) goto FAIL; \
+ }
+ FROB("Access-Type", at);
+ FROB("URL", url);
+ FROB("Site", site);
+ FROB("Server", svr);
+ FROB("Directory", dir);
+ FROB("Name", name);
+ FROB("Type", ct);
+ FROB("Size", size);
+ FROB("Mode", mode);
+ FROB("Permission", perm);
+ FROB("Expiration", lexp);
+ FROB("Subject", subj);
+#undef FROB
+ PL_strncpyz(h, MSG_LINEBREAK, hlen);
+ status = MimeHeaders_parse_line(h, strlen(h), hdrs);
+ if (status < 0) goto FAIL;
+
+ lurl = MimeExternalBody_make_url(ct, at, lexp, size, perm, dir, mode, name,
+ url, site, svr, subj, bod->body);
+ if (lurl) {
+ lname = MimeGetStringByID(MIME_MSG_LINK_TO_DOCUMENT);
+ } else {
+ lname = MimeGetStringByID(MIME_MSG_DOCUMENT_INFO);
+ all_headers_p = true;
+ }
+
+ all_headers_p = true; /* #### just do this all the time? */
+
+ if (bod->body && all_headers_p) {
+ char* s = bod->body;
+ while (IS_SPACE(*s)) s++;
+ if (*s) {
+ const char* pre = "<P><PRE>";
+ const char* suf = "</PRE>";
+ int32_t i;
+ // The end condition requires i to be negative, so it's ok to
+ // allow the starting value to be negative.
+ for (i = strlen(s) - 1; i >= 0 && IS_SPACE(s[i]); i--) s[i] = 0;
+ nsCString s2;
+ nsAppendEscapedHTML(nsDependentCString(s), s2);
+ body = (char*)PR_MALLOC(strlen(pre) + s2.Length() + strlen(suf) + 1);
+ if (!body) {
+ goto FAIL;
+ }
+ PL_strcpy(body, pre);
+ PL_strcat(body, s2.get());
+ PL_strcat(body, suf);
+ }
+ }
+
+ newopt->fancy_headers_p = true;
+ newopt->headers = (all_headers_p ? MimeHeadersAll : MimeHeadersSome);
+
+ FAIL:
+ if (hdrs) MimeHeaders_free(hdrs);
+ PR_FREEIF(h);
+ PR_FREEIF(lname);
+ PR_FREEIF(lurl);
+ PR_FREEIF(body);
+ PR_FREEIF(ct);
+ PR_FREEIF(at);
+ PR_FREEIF(lexp);
+ PR_FREEIF(size);
+ PR_FREEIF(perm);
+ PR_FREEIF(dir);
+ PR_FREEIF(mode);
+ PR_FREEIF(name);
+ PR_FREEIF(url);
+ PR_FREEIF(site);
+ PR_FREEIF(svr);
+ PR_FREEIF(subj);
+ }
+
+#ifdef XP_MACOSX
+done:
+#endif
+
+ return status;
+}
+
+#if 0
+# if defined(DEBUG) && defined(XP_UNIX)
+static int
+MimeExternalBody_debug_print (MimeObject *obj, PRFileDesc *stream, int32_t depth)
+{
+ MimeExternalBody *bod = (MimeExternalBody *) obj;
+ int i;
+ char *ct, *ct2;
+ char *addr = mime_part_address(obj);
+
+ if (obj->headers)
+ ct = MimeHeaders_get (obj->headers, HEADER_CONTENT_TYPE, false, false);
+ if (bod->hdrs)
+ ct2 = MimeHeaders_get (bod->hdrs, HEADER_CONTENT_TYPE, false, false);
+
+ for (i=0; i < depth; i++)
+ PR_Write(stream, " ", 2);
+/***
+ fprintf(stream,
+ "<%s %s\n"
+ "\tcontent-type: %s\n"
+ "\tcontent-type: %s\n"
+ "\tBody:%s\n\t0x%08X>\n\n",
+ obj->clazz->class_name,
+ addr ? addr : "???",
+ ct ? ct : "<none>",
+ ct2 ? ct2 : "<none>",
+ bod->body ? bod->body : "<none>",
+ (uint32_t) obj);
+***/
+ PR_FREEIF(addr);
+ PR_FREEIF(ct);
+ PR_FREEIF(ct2);
+ return 0;
+}
+# endif
+#endif /* 0 */
+
+static bool MimeExternalBody_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs) {
+ char* ct = MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false);
+ char* at = MimeHeaders_get_parameter(ct, "access-type", NULL, NULL);
+ bool inline_p = false;
+
+ if (!at)
+ ;
+ else if (!PL_strcasecmp(at, "ftp") || !PL_strcasecmp(at, "anon-ftp") ||
+ !PL_strcasecmp(at, "local-file") ||
+ !PL_strcasecmp(at, "mail-server") || !PL_strcasecmp(at, "url"))
+ inline_p = true;
+#ifdef XP_UNIX
+ else if (!PL_strcasecmp(at, "afs")) /* only if there is a /afs/ directory */
+ {
+ nsCOMPtr<nsIFile> fs = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ bool exists = false;
+ if (fs) {
+ fs->InitWithNativePath("/afs/."_ns);
+ fs->Exists(&exists);
+ }
+ if (!exists) return 0;
+
+ inline_p = true;
+ }
+#endif /* XP_UNIX */
+
+ PR_FREEIF(ct);
+ PR_FREEIF(at);
+ return inline_p;
+}
diff --git a/comm/mailnews/mime/src/mimeebod.h b/comm/mailnews/mime/src/mimeebod.h
new file mode 100644
index 0000000000..fcb33ee664
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeebod.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEEBOD_H_
+#define _MIMEEBOD_H_
+
+#include "mimeobj.h"
+
+/* The MimeExternalBody class implements the message/external-body MIME type.
+ (This is not to be confused with MimeExternalObject, which implements the
+ handler for application/octet-stream and other types with no more specific
+ handlers.)
+ */
+
+typedef struct MimeExternalBodyClass MimeExternalBodyClass;
+typedef struct MimeExternalBody MimeExternalBody;
+
+struct MimeExternalBodyClass {
+ MimeObjectClass object;
+};
+
+extern MimeExternalBodyClass mimeExternalBodyClass;
+
+struct MimeExternalBody {
+ MimeObject object; /* superclass variables */
+ MimeHeaders* hdrs; /* headers within this external-body, which
+ describe the network data which this body
+ is a pointer to. */
+ char* body; /* The "phantom body" of this link. */
+};
+
+#define MimeExternalBodyClassInitializer(ITYPE, CSUPER) \
+ { MimeObjectClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEEBOD_H_ */
diff --git a/comm/mailnews/mime/src/mimeenc.cpp b/comm/mailnews/mime/src/mimeenc.cpp
new file mode 100644
index 0000000000..1c9df3794b
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeenc.cpp
@@ -0,0 +1,999 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include <stdio.h>
+#include "mimei.h"
+#include "prmem.h"
+#include "mimeobj.h"
+#include "mozilla/RangedPtr.h"
+#include "mozilla/mailnews/MimeEncoder.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+typedef enum mime_encoding {
+ mime_Base64,
+ mime_QuotedPrintable,
+ mime_uuencode,
+ mime_yencode
+} mime_encoding;
+
+typedef enum mime_decoder_state {
+ DS_BEGIN,
+ DS_BODY,
+ DS_END
+} mime_decoder_state;
+
+struct MimeDecoderData {
+ mime_encoding encoding; /* Which encoding to use */
+
+ /* A read-buffer used for QP and B64. */
+ char token[4];
+ int token_size;
+
+ /* State and read-buffer used for uudecode and yencode. */
+ mime_decoder_state ds_state;
+ char* line_buffer;
+ int line_buffer_size;
+
+ MimeObject* objectToDecode; // might be null, only used for QP currently
+ /* Where to write the decoded data */
+ MimeConverterOutputCallback write_buffer;
+ void* closure;
+};
+
+static int mime_decode_qp_buffer(MimeDecoderData* data, const char* buffer,
+ int32_t length, int32_t* outSize) {
+ /* Warning, we are overwriting the buffer which was passed in.
+ This is ok, because decoding these formats will never result
+ in larger data than the input, only smaller. */
+ const char* in = buffer;
+ char* out = (char*)buffer;
+ char token[3];
+ int i;
+
+ NS_ASSERTION(data->encoding == mime_QuotedPrintable,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (data->encoding != mime_QuotedPrintable) return -1;
+
+ /* For the first pass, initialize the token from the unread-buffer. */
+ i = 0;
+ while (i < 3 && data->token_size > 0) {
+ token[i] = data->token[i];
+ data->token_size--;
+ i++;
+ }
+
+ /* #### BUG: when decoding quoted-printable, we are required to
+ strip trailing whitespace from lines -- since when encoding in
+ qp, one is required to quote such trailing whitespace, any
+ trailing whitespace which remains must have been introduced
+ by a stupid gateway. */
+
+ /* Treat null bytes as spaces when format_out is
+ nsMimeOutput::nsMimeMessageBodyDisplay (see bug 243199 comment 7) */
+ bool treatNullAsSpace =
+ data->objectToDecode && data->objectToDecode->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyDisplay;
+
+ while (length > 0 || i != 0) {
+ while (i < 3 && length > 0) {
+ token[i++] = *in;
+ in++;
+ length--;
+ }
+
+ if (i < 3) {
+ /* Didn't get enough for a complete token.
+ If it might be a token, unread it.
+ Otherwise, just dump it.
+ */
+ memcpy(data->token, token, i);
+ data->token_size = i;
+ i = 0;
+ length = 0;
+ break;
+ }
+ i = 0;
+
+ if (token[0] == '=') {
+ unsigned char c = 0;
+ if (token[1] >= '0' && token[1] <= '9')
+ c = token[1] - '0';
+ else if (token[1] >= 'A' && token[1] <= 'F')
+ c = token[1] - ('A' - 10);
+ else if (token[1] >= 'a' && token[1] <= 'f')
+ c = token[1] - ('a' - 10);
+ else if (token[1] == '\r' || token[1] == '\n') {
+ /* =\n means ignore the newline. */
+ if (token[1] == '\r' && token[2] == '\n')
+ ; /* swallow all three chars */
+ else {
+ in--; /* put the third char back */
+ length++;
+ }
+ continue;
+ } else {
+ /* = followed by something other than hex or newline -
+ pass it through unaltered, I guess. (But, if
+ this bogus token happened to occur over a buffer
+ boundary, we can't do this, since we don't have
+ space for it. Oh well. Screw it.) */
+ if (in > out) *out++ = token[0];
+ if (in > out) *out++ = token[1];
+ if (in > out) *out++ = token[2];
+ continue;
+ }
+
+ /* Second hex digit */
+ c = (c << 4);
+ if (token[2] >= '0' && token[2] <= '9')
+ c += token[2] - '0';
+ else if (token[2] >= 'A' && token[2] <= 'F')
+ c += token[2] - ('A' - 10);
+ else if (token[2] >= 'a' && token[2] <= 'f')
+ c += token[2] - ('a' - 10);
+ else {
+ /* We got =xy where "x" was hex and "y" was not, so
+ treat that as a literal "=", x, and y. (But, if
+ this bogus token happened to occur over a buffer
+ boundary, we can't do this, since we don't have
+ space for it. Oh well. Screw it.) */
+ if (in > out) *out++ = token[0];
+ if (in > out) *out++ = token[1];
+ if (in > out) *out++ = token[2];
+ continue;
+ }
+
+ *out++ = c ? (char)c : ((treatNullAsSpace) ? ' ' : (char)c);
+ } else {
+ *out++ = token[0];
+
+ token[0] = token[1];
+ token[1] = token[2];
+ i = 2;
+ }
+ }
+
+ // Fill the size
+ if (outSize) *outSize = out - buffer;
+
+ /* Now that we've altered the data in place, write it. */
+ if (out > buffer)
+ return data->write_buffer(buffer, (out - buffer), data->closure);
+ else
+ return 1;
+}
+
+static int mime_decode_base64_token(const char* in, char* out) {
+ /* reads 4, writes 0-3. Returns bytes written.
+ (Writes less than 3 only at EOF.) */
+ int j;
+ int eq_count = 0;
+ unsigned long num = 0;
+
+ for (j = 0; j < 4; j++) {
+ unsigned char c = 0;
+ if (in[j] >= 'A' && in[j] <= 'Z')
+ c = in[j] - 'A';
+ else if (in[j] >= 'a' && in[j] <= 'z')
+ c = in[j] - ('a' - 26);
+ else if (in[j] >= '0' && in[j] <= '9')
+ c = in[j] - ('0' - 52);
+ else if (in[j] == '+')
+ c = 62;
+ else if (in[j] == '/')
+ c = 63;
+ else if (in[j] == '=') {
+ c = 0;
+ eq_count++;
+ } else
+ NS_ERROR("Invalid character");
+ num = (num << 6) | c;
+ }
+
+ *out++ = (char)(num >> 16);
+ *out++ = (char)((num >> 8) & 0xFF);
+ *out++ = (char)(num & 0xFF);
+
+ if (eq_count == 0)
+ return 3; /* No "=" padding means 4 bytes mapped to 3. */
+ else if (eq_count == 1)
+ return 2; /* "xxx=" means 3 bytes mapped to 2. */
+ else if (eq_count == 2)
+ return 1; /* "xx==" means 2 bytes mapped to 1. */
+ else {
+ // "x===" can't happen, because "x" would then be encoding only
+ // 6 bits, not the min of 8.
+ NS_ERROR("Count is 6 bits, should be at least 8");
+ return 1;
+ }
+}
+
+static int mime_decode_base64_buffer(MimeDecoderData* data, const char* buffer,
+ int32_t length, int32_t* outSize) {
+ if (outSize) *outSize = 0;
+
+ // Without input there is nothing to do.
+ if (length == 0) return 1;
+
+ /* Warning, we are overwriting the buffer which was passed in.
+ This is ok, because decoding these formats will never result
+ in larger data than the input, only smaller. */
+ const char* in = buffer;
+ char* out = (char*)buffer;
+ char token[4];
+ int i;
+ bool leftover = (data->token_size > 0);
+
+ NS_ASSERTION(data->encoding == mime_Base64,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ /* For the first pass, initialize the token from the unread-buffer. */
+ i = 0;
+ while (i < 4 && data->token_size > 0) {
+ token[i] = data->token[i];
+ data->token_size--;
+ i++;
+ }
+
+ while (length > 0) {
+ while (i < 4 && length > 0) {
+ if ((*in >= 'A' && *in <= 'Z') || (*in >= 'a' && *in <= 'z') ||
+ (*in >= '0' && *in <= '9') || *in == '+' || *in == '/' || *in == '=')
+ token[i++] = *in;
+ in++;
+ length--;
+ }
+
+ if (i < 4) {
+ /* Didn't get enough for a complete token. */
+ memcpy(data->token, token, i);
+ data->token_size = i;
+ length = 0;
+ break;
+ }
+ i = 0;
+
+ if (leftover) {
+ /* If there are characters left over from the last time around,
+ we might not have space in the buffer to do our dirty work
+ (if there were 2 or 3 left over, then there is only room for
+ 1 or 2 in the buffer right now, and we need 3.) This is only
+ a problem for the first chunk in each buffer, so in that
+ case, just write prematurely. */
+ int n;
+ n = mime_decode_base64_token(token, token);
+ if (outSize) *outSize += n;
+ n = data->write_buffer(token, n, data->closure);
+ if (n < 0) /* abort */
+ return n;
+
+ /* increment buffer so that we don't write the 1 or 2 unused
+ characters now at the front. */
+ buffer = in;
+ out = (char*)buffer;
+
+ leftover = false;
+ } else {
+ int n = mime_decode_base64_token(token, out);
+ /* Advance "out" by the number of bytes just written to it. */
+ out += n;
+ }
+ }
+
+ if (outSize) *outSize += out - buffer;
+ /* Now that we've altered the data in place, write it. */
+ if (out > buffer)
+ return data->write_buffer(buffer, (out - buffer), data->closure);
+ else
+ return 1;
+}
+
+static int mime_decode_uue_buffer(MimeDecoderData* data,
+ const char* input_buffer,
+ int32_t input_length, int32_t* outSize) {
+ /* First, copy input_buffer into state->line_buffer until we have
+ a complete line.
+
+ Then decode that line in place (in the line_buffer) and write
+ it out.
+
+ Then pull the next line into line_buffer and continue.
+ */
+ if (!data->line_buffer) {
+ data->line_buffer_size = 128;
+ data->line_buffer = (char*)PR_MALLOC(data->line_buffer_size);
+ if (!data->line_buffer) return -1;
+ data->line_buffer[0] = 0;
+ }
+
+ int status = 0;
+ char* line = data->line_buffer;
+ char* line_end = data->line_buffer + data->line_buffer_size - 1;
+
+ NS_ASSERTION(data->encoding == mime_uuencode,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (data->encoding != mime_uuencode) return -1;
+
+ if (data->ds_state == DS_END) {
+ status = 0;
+ goto DONE;
+ }
+
+ while (input_length > 0) {
+ /* Copy data from input_buffer to `line' until we have a complete line,
+ or until we've run out of input.
+
+ (line may have data in it already if the last time we were called,
+ we weren't called with a buffer that ended on a line boundary.)
+ */
+ {
+ char* out = line + strlen(line);
+ while (input_length > 0 && out < line_end) {
+ *out++ = *input_buffer++;
+ input_length--;
+
+ if (out[-1] == '\r' || out[-1] == '\n') {
+ /* If we just copied a CR, and an LF is waiting, grab it too.
+ */
+ if (out[-1] == '\r' && input_length > 0 && *input_buffer == '\n') {
+ input_buffer++;
+ input_length--;
+ }
+
+ /* We have a line. */
+ break;
+ }
+ }
+ *out = 0;
+
+ /* Ignore blank lines.
+ */
+ if (*line == '\r' || *line == '\n') {
+ *line = 0;
+ continue;
+ }
+
+ /* If this line was bigger than our buffer, truncate it.
+ (This means the data was way corrupted, and there's basically
+ no chance of decoding it properly, but give it a shot anyway.)
+ */
+ if (out == line_end) {
+ out--;
+ out[-1] = '\r';
+ out[0] = 0;
+ }
+
+ /* If we didn't get a complete line, simply return; we'll be called
+ with the rest of this line next time.
+ */
+ if (out[-1] != '\r' && out[-1] != '\n') {
+ NS_ASSERTION(input_length == 0,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ break;
+ }
+ }
+
+ /* Now we have a complete line. Deal with it.
+ */
+
+ if (data->ds_state == DS_BODY && line[0] == 'e' && line[1] == 'n' &&
+ line[2] == 'd' && (line[3] == '\r' || line[3] == '\n')) {
+ /* done! */
+ data->ds_state = DS_END;
+ *line = 0;
+ break;
+ } else if (data->ds_state == DS_BEGIN) {
+ if (!strncmp(line, "begin ", 6)) data->ds_state = DS_BODY;
+ *line = 0;
+ continue;
+ } else {
+ /* We're in DS_BODY. Decode the line. */
+ char *in, *out;
+ int32_t i;
+ long lost;
+
+ NS_ASSERTION(data->ds_state == DS_BODY,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ /* We map down `line', reading four bytes and writing three.
+ That means that `out' always stays safely behind `in'.
+ */
+ in = line;
+ out = line;
+
+#undef DEC
+#define DEC(c) (((c) - ' ') & 077)
+ i = DEC(*in); /* get length */
+
+ /* all the parens and casts are because gcc was doing something evil.
+ */
+ lost = ((long)i) - (((((long)strlen(in)) - 2L) * 3L) / 4L);
+
+ if (lost > 0) /* Short line!! */
+ {
+ /* If we get here, then the line is shorter than the length byte
+ at the beginning says it should be. However, the case where
+ the line is short because it was at the end of the buffer and
+ we didn't get the whole line was handled earlier (up by the
+ "didn't get a complete line" comment.) So if we've gotten
+ here, then this is a complete line which is internally
+ inconsistent. We will parse from it what we can...
+
+ This probably happened because some gateway stripped trailing
+ whitespace from the end of the line -- so pretend the line
+ was padded with spaces (which map to \000.)
+ */
+ i -= lost;
+ }
+
+ for (++in; i > 0; in += 4, i -= 3) {
+ char ch;
+ NS_ASSERTION(out <= in, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ if (i >= 3) {
+ /* We read four; write three. */
+ ch = DEC(in[0]) << 2 | DEC(in[1]) >> 4;
+ *out++ = ch;
+
+ NS_ASSERTION(out <= in + 1,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ ch = DEC(in[1]) << 4 | DEC(in[2]) >> 2;
+ *out++ = ch;
+
+ NS_ASSERTION(out <= in + 2,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ ch = DEC(in[2]) << 6 | DEC(in[3]);
+ *out++ = ch;
+
+ NS_ASSERTION(out <= in + 3,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ } else {
+ /* Handle a line that isn't a multiple of 4 long.
+ (We read 1, 2, or 3, and will write 1 or 2.)
+ */
+ NS_ASSERTION(i > 0 && i < 3,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ ch = DEC(in[0]) << 2 | DEC(in[1]) >> 4;
+ *out++ = ch;
+
+ NS_ASSERTION(out <= in + 1,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ if (i == 2) {
+ ch = DEC(in[1]) << 4 | DEC(in[2]) >> 2;
+ *out++ = ch;
+
+ NS_ASSERTION(out <= in + 2,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ }
+ }
+ }
+
+ /* If the line was truncated, pad the missing bytes with 0 (SPC). */
+ while (lost > 0) {
+ *out++ = 0;
+ lost--;
+ in = out + 1; /* just to prevent the assert, below. */
+ }
+#undef DEC
+
+ /* Now write out what we decoded for this line.
+ */
+ NS_ASSERTION(out >= line && out < in,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (out > line)
+ status = data->write_buffer(line, (out - line), data->closure);
+
+ // The assertion above tells us this is >= 0
+ if (outSize) *outSize = out - line;
+
+ /* Reset the line so that we don't think it's partial next time. */
+ *line = 0;
+
+ if (status < 0) /* abort */
+ goto DONE;
+ }
+ }
+
+ status = 1;
+
+DONE:
+
+ return status;
+}
+
+static int mime_decode_yenc_buffer(MimeDecoderData* data,
+ const char* input_buffer,
+ int32_t input_length, int32_t* outSize) {
+ /* First, copy input_buffer into state->line_buffer until we have
+ a complete line.
+
+ Then decode that line in place (in the line_buffer) and write
+ it out.
+
+ Then pull the next line into line_buffer and continue.
+ */
+ if (!data->line_buffer) {
+ data->line_buffer_size =
+ 1000; // let make sure we have plenty of space for the header line
+ data->line_buffer = (char*)PR_MALLOC(data->line_buffer_size);
+ if (!data->line_buffer) return -1;
+ data->line_buffer[0] = 0;
+ }
+
+ int status = 0;
+ char* line = data->line_buffer;
+ char* line_end = data->line_buffer + data->line_buffer_size - 1;
+
+ NS_ASSERTION(data->encoding == mime_yencode, "wrong decoder!");
+ if (data->encoding != mime_yencode) return -1;
+
+ if (data->ds_state == DS_END) return 0;
+
+ while (input_length > 0) {
+ /* Copy data from input_buffer to `line' until we have a complete line,
+ or until we've run out of input.
+
+ (line may have data in it already if the last time we were called,
+ we weren't called with a buffer that ended on a line boundary.)
+ */
+ {
+ char* out = line + strlen(line);
+ while (input_length > 0 && out < line_end) {
+ *out++ = *input_buffer++;
+ input_length--;
+
+ if (out[-1] == '\r' || out[-1] == '\n') {
+ /* If we just copied a CR, and an LF is waiting, grab it too. */
+ if (out[-1] == '\r' && input_length > 0 && *input_buffer == '\n') {
+ input_buffer++;
+ input_length--;
+ }
+
+ /* We have a line. */
+ break;
+ }
+ }
+ *out = 0;
+
+ /* Ignore blank lines. */
+ if (*line == '\r' || *line == '\n') {
+ *line = 0;
+ continue;
+ }
+
+ /* If this line was bigger than our buffer, truncate it.
+ (This means the data was way corrupted, and there's basically
+ no chance of decoding it properly, but give it a shot anyway.)
+ */
+ if (out == line_end) {
+ out--;
+ out[-1] = '\r';
+ out[0] = 0;
+ }
+
+ /* If we didn't get a complete line, simply return; we'll be called
+ with the rest of this line next time.
+ */
+ if (out[-1] != '\r' && out[-1] != '\n') {
+ NS_ASSERTION(input_length == 0, "empty buffer!");
+ break;
+ }
+ }
+
+ /* Now we have a complete line. Deal with it.
+ */
+ const char* endOfLine = line + strlen(line);
+
+ if (data->ds_state == DS_BEGIN) {
+ int new_line_size = 0;
+ /* this yenc decoder does not support yenc v2 or multipart yenc.
+ Therefore, we are looking first for "=ybegin line="
+ */
+ if ((endOfLine - line) >= 13 && !strncmp(line, "=ybegin line=", 13)) {
+ /* ...then couple digits. */
+ for (line += 13; line < endOfLine; line++) {
+ if (*line < '0' || *line > '9') break;
+ new_line_size = (new_line_size * 10) + *line - '0';
+ }
+
+ /* ...next, look for <space>size= */
+ if ((endOfLine - line) >= 6 && !strncmp(line, " size=", 6)) {
+ /* ...then couple digits. */
+ for (line += 6; line < endOfLine; line++)
+ if (*line < '0' || *line > '9') break;
+
+ /* ...next, look for <space>name= */
+ if ((endOfLine - line) >= 6 && !strncmp(line, " name=", 6)) {
+ /* we have found the yenc header line.
+ Now check if we need to grow our buffer line
+ */
+ data->ds_state = DS_BODY;
+ if (new_line_size > data->line_buffer_size &&
+ new_line_size <= 997) /* don't let bad value hurt us! */
+ {
+ PR_Free(data->line_buffer);
+ data->line_buffer_size =
+ new_line_size +
+ 4; // extra chars for line ending and potential escape char
+ data->line_buffer = (char*)PR_MALLOC(data->line_buffer_size);
+ if (!data->line_buffer) return -1;
+ }
+ }
+ }
+ }
+ *data->line_buffer = 0;
+ continue;
+ }
+
+ if (data->ds_state == DS_BODY && line[0] == '=') {
+ /* look if this this the final line */
+ if (!strncmp(line, "=yend size=", 11)) {
+ /* done! */
+ data->ds_state = DS_END;
+ *line = 0;
+ break;
+ }
+ }
+
+ /* We're in DS_BODY. Decode the line in place. */
+ {
+ char* src = line;
+ char* dest = src;
+ char c;
+ for (; src < line_end; src++) {
+ c = *src;
+ if (!c || c == '\r' || c == '\n') break;
+
+ if (c == '=') {
+ src++;
+ c = *src;
+ if (c == 0) return -1; /* last character cannot be escape char */
+ c -= 64;
+ }
+ c -= 42;
+ *dest = c;
+ dest++;
+ }
+
+ // The assertion below is helpful, too
+ if (outSize) *outSize = dest - line;
+
+ /* Now write out what we decoded for this line. */
+ NS_ASSERTION(dest >= line && dest <= src, "nothing to write!");
+ if (dest > line) {
+ status = data->write_buffer(line, dest - line, data->closure);
+ if (status < 0) /* abort */
+ return status;
+ }
+
+ /* Reset the line so that we don't think it's partial next time. */
+ *line = 0;
+ }
+ }
+
+ return 1;
+}
+
+int MimeDecoderDestroy(MimeDecoderData* data, bool abort_p) {
+ int status = 0;
+ /* Flush out the last few buffered characters. */
+ if (!abort_p && data->token_size > 0 && data->token[0] != '=') {
+ if (data->encoding == mime_Base64)
+ while ((unsigned int)data->token_size < sizeof(data->token))
+ data->token[data->token_size++] = '=';
+
+ status = data->write_buffer(data->token, data->token_size, data->closure);
+ }
+
+ if (data->line_buffer) PR_Free(data->line_buffer);
+ PR_Free(data);
+ return status;
+}
+
+static MimeDecoderData* mime_decoder_init(mime_encoding which,
+ MimeConverterOutputCallback output_fn,
+ void* closure) {
+ MimeDecoderData* data = PR_NEW(MimeDecoderData);
+ if (!data) return 0;
+ memset(data, 0, sizeof(*data));
+ data->encoding = which;
+ data->write_buffer = output_fn;
+ data->closure = closure;
+ data->line_buffer_size = 0;
+ data->line_buffer = nullptr;
+
+ return data;
+}
+
+MimeDecoderData* MimeB64DecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure) {
+ return mime_decoder_init(mime_Base64, output_fn, closure);
+}
+
+MimeDecoderData* MimeQPDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure, MimeObject* object) {
+ MimeDecoderData* retData =
+ mime_decoder_init(mime_QuotedPrintable, output_fn, closure);
+ if (retData) retData->objectToDecode = object;
+ return retData;
+}
+
+MimeDecoderData* MimeUUDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure) {
+ return mime_decoder_init(mime_uuencode, output_fn, closure);
+}
+
+MimeDecoderData* MimeYDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure) {
+ return mime_decoder_init(mime_yencode, output_fn, closure);
+}
+
+int MimeDecoderWrite(MimeDecoderData* data, const char* buffer, int32_t size,
+ int32_t* outSize) {
+ NS_ASSERTION(data, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!data) return -1;
+ switch (data->encoding) {
+ case mime_Base64:
+ return mime_decode_base64_buffer(data, buffer, size, outSize);
+ case mime_QuotedPrintable:
+ return mime_decode_qp_buffer(data, buffer, size, outSize);
+ case mime_uuencode:
+ return mime_decode_uue_buffer(data, buffer, size, outSize);
+ case mime_yencode:
+ return mime_decode_yenc_buffer(data, buffer, size, outSize);
+ default:
+ NS_ERROR("Invalid decoding");
+ return -1;
+ }
+}
+
+namespace mozilla {
+namespace mailnews {
+
+MimeEncoder::MimeEncoder(OutputCallback callback, void* closure)
+ : mCallback(callback), mClosure(closure), mCurrentColumn(0) {}
+
+class Base64Encoder : public MimeEncoder {
+ unsigned char in_buffer[3];
+ int32_t in_buffer_count;
+
+ public:
+ Base64Encoder(OutputCallback callback, void* closure)
+ : MimeEncoder(callback, closure), in_buffer_count(0) {}
+ virtual ~Base64Encoder() {}
+
+ virtual nsresult Write(const char* buffer, int32_t size) override;
+ virtual nsresult Flush() override;
+
+ private:
+ static void Base64EncodeBits(RangedPtr<char>& out, uint32_t bits);
+};
+
+nsresult Base64Encoder::Write(const char* buffer, int32_t size) {
+ if (size == 0)
+ return NS_OK;
+ else if (size < 0) {
+ NS_ERROR("Size is less than 0");
+ return NS_ERROR_FAILURE;
+ }
+
+ // If this input buffer is too small, wait until next time.
+ if (size < (3 - in_buffer_count)) {
+ NS_ASSERTION(size == 1 || size == 2, "Unexpected size");
+ in_buffer[in_buffer_count++] = buffer[0];
+ if (size == 2) in_buffer[in_buffer_count++] = buffer[1];
+ NS_ASSERTION(in_buffer_count < 3, "Unexpected out buffer size");
+ return NS_OK;
+ }
+
+ // If there are bytes that were put back last time, take them now.
+ uint32_t i = in_buffer_count, bits = 0;
+ if (in_buffer_count > 0) bits = in_buffer[0];
+ if (in_buffer_count > 1) bits = (bits << 8) + in_buffer[1];
+ in_buffer_count = 0;
+
+ // If this buffer is not a multiple of three, put one or two bytes back.
+ uint32_t excess = ((size + i) % 3);
+ if (excess) {
+ in_buffer[0] = buffer[size - excess];
+ if (excess > 1) in_buffer[1] = buffer[size - excess + 1];
+ in_buffer_count = excess;
+ size -= excess;
+ NS_ASSERTION(!((size + i) % 3), "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ }
+
+ const uint8_t* in = (const uint8_t*)buffer;
+ const uint8_t* end = (const uint8_t*)(buffer + size);
+ MOZ_ASSERT((end - in + i) % 3 == 0, "Need a multiple of 3 bytes to decode");
+
+ // Populate the out_buffer with base64 data, one line at a time.
+ char out_buffer[80]; // Max line length will be 80, so this is safe.
+ RangedPtr<char> out(out_buffer);
+ while (in < end) {
+ // Accumulate the input bits.
+ while (i < 3) {
+ bits = (bits << 8) | *in++;
+ i++;
+ }
+ i = 0;
+
+ Base64EncodeBits(out, bits);
+
+ mCurrentColumn += 4;
+ if (mCurrentColumn >= 72) {
+ // Do a linebreak before column 76. Flush out the line buffer.
+ mCurrentColumn = 0;
+ *out++ = '\x0D';
+ *out++ = '\x0A';
+ nsresult rv = mCallback(out_buffer, (out.get() - out_buffer), mClosure);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out = out_buffer;
+ }
+ }
+
+ // Write out the unwritten portion of the last line buffer.
+ if (out.get() > out_buffer) {
+ nsresult rv = mCallback(out_buffer, out.get() - out_buffer, mClosure);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult Base64Encoder::Flush() {
+ if (in_buffer_count == 0) return NS_OK;
+
+ // Since we need to some buffering to get a multiple of three bytes on each
+ // block, there may be a few bytes left in the buffer after the last block has
+ // been written. We need to flush those out now.
+ char buf[4];
+ RangedPtr<char> out(buf);
+ uint32_t bits = ((uint32_t)in_buffer[0]) << 16;
+ if (in_buffer_count > 1) bits |= (((uint32_t)in_buffer[1]) << 8);
+
+ Base64EncodeBits(out, bits);
+
+ // Pad with equal-signs.
+ if (in_buffer_count == 1) buf[2] = '=';
+ buf[3] = '=';
+
+ return mCallback(buf, 4, mClosure);
+}
+
+void Base64Encoder::Base64EncodeBits(RangedPtr<char>& out, uint32_t bits) {
+ // Convert 3 bytes to 4 base64 bytes
+ for (int32_t j = 18; j >= 0; j -= 6) {
+ unsigned int k = (bits >> j) & 0x3F;
+ if (k < 26)
+ *out++ = k + 'A';
+ else if (k < 52)
+ *out++ = k - 26 + 'a';
+ else if (k < 62)
+ *out++ = k - 52 + '0';
+ else if (k == 62)
+ *out++ = '+';
+ else if (k == 63)
+ *out++ = '/';
+ else
+ MOZ_CRASH("6 bits should only be between 0 and 64");
+ }
+}
+
+class QPEncoder : public MimeEncoder {
+ public:
+ QPEncoder(OutputCallback callback, void* closure)
+ : MimeEncoder(callback, closure) {}
+ virtual ~QPEncoder() {}
+
+ virtual nsresult Write(const char* buffer, int32_t size) override;
+};
+
+nsresult QPEncoder::Write(const char* buffer, int32_t size) {
+ nsresult rv = NS_OK;
+ static const char* hexdigits = "0123456789ABCDEF";
+ char out_buffer[80];
+ RangedPtr<char> out(out_buffer);
+ bool white = false;
+
+ // Populate the out_buffer with quoted-printable data, one line at a time.
+ const uint8_t* in = (uint8_t*)buffer;
+ const uint8_t* end = in + size;
+ for (; in < end; in++) {
+ if (*in == '\r' || *in == '\n') {
+ // If it's CRLF, swallow two chars instead of one.
+ if (in + 1 < end && in[0] == '\r' && in[1] == '\n') in++;
+
+ // Whitespace cannot be allowed to occur at the end of the line, so we
+ // back up and replace the whitespace with its code.
+ if (white) {
+ out--;
+ char whitespace_char = *out;
+ *out++ = '=';
+ *out++ = hexdigits[whitespace_char >> 4];
+ *out++ = hexdigits[whitespace_char & 0xF];
+ }
+
+ // Now write out the newline.
+ *out++ = '\r';
+ *out++ = '\n';
+ white = false;
+
+ rv = mCallback(out_buffer, out.get() - out_buffer, mClosure);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out = out_buffer;
+ mCurrentColumn = 0;
+ } else if (mCurrentColumn == 0 && *in == '.') {
+ // Just to be SMTP-safe, if "." appears in column 0, encode it.
+ goto HEX;
+ } else if (mCurrentColumn == 0 && *in == 'F' &&
+ (in >= end - 1 || in[1] == 'r') &&
+ (in >= end - 2 || in[2] == 'o') &&
+ (in >= end - 3 || in[3] == 'm') &&
+ (in >= end - 4 || in[4] == ' ')) {
+ // If this line begins with "From " (or it could but we don't have enough
+ // data in the buffer to be certain), encode the 'F' in hex to avoid
+ // potential problems with BSD mailbox formats.
+ goto HEX;
+ } else if ((*in >= 33 && *in <= 60) |
+ (*in >= 62 &&
+ *in <= 126)) // Printable characters except for '='
+ {
+ white = false;
+ *out++ = *in;
+ mCurrentColumn++;
+ } else if (*in == ' ' || *in == '\t') // Whitespace
+ {
+ white = true;
+ *out++ = *in;
+ mCurrentColumn++;
+ } else {
+ // Encode the characters here
+ HEX:
+ white = false;
+ *out++ = '=';
+ *out++ = hexdigits[*in >> 4];
+ *out++ = hexdigits[*in & 0xF];
+ mCurrentColumn += 3;
+ }
+
+ MOZ_ASSERT(mCurrentColumn <= 76, "Why haven't we added a line break yet?");
+
+ if (mCurrentColumn >= 73) // Soft line break for readability
+ {
+ *out++ = '=';
+ *out++ = '\r';
+ *out++ = '\n';
+
+ rv = mCallback(out_buffer, out.get() - out_buffer, mClosure);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out = out_buffer;
+ white = false;
+ mCurrentColumn = 0;
+ }
+ }
+
+ // Write out the unwritten portion of the last line buffer.
+ if (out.get() != out_buffer) {
+ rv = mCallback(out_buffer, out.get() - out_buffer, mClosure);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+MimeEncoder* MimeEncoder::GetBase64Encoder(OutputCallback callback,
+ void* closure) {
+ return new Base64Encoder(callback, closure);
+}
+
+MimeEncoder* MimeEncoder::GetQPEncoder(OutputCallback callback, void* closure) {
+ return new QPEncoder(callback, closure);
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/mime/src/mimeeobj.cpp b/comm/mailnews/mime/src/mimeeobj.cpp
new file mode 100644
index 0000000000..2bc858b4b4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeeobj.cpp
@@ -0,0 +1,215 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "mimeeobj.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "mimemapl.h"
+#include "nsMimeTypes.h"
+
+#define MIME_SUPERCLASS mimeLeafClass
+MimeDefClass(MimeExternalObject, MimeExternalObjectClass,
+ mimeExternalObjectClass, &MIME_SUPERCLASS);
+
+static int MimeExternalObject_initialize(MimeObject*);
+static void MimeExternalObject_finalize(MimeObject*);
+static int MimeExternalObject_parse_begin(MimeObject*);
+static int MimeExternalObject_parse_buffer(const char*, int32_t, MimeObject*);
+static int MimeExternalObject_parse_line(const char*, int32_t, MimeObject*);
+static int MimeExternalObject_parse_decoded_buffer(const char*, int32_t,
+ MimeObject*);
+static bool MimeExternalObject_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs);
+
+static int MimeExternalObjectClassInitialize(MimeExternalObjectClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeLeafClass* lclass = (MimeLeafClass*)clazz;
+
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ oclass->initialize = MimeExternalObject_initialize;
+ oclass->finalize = MimeExternalObject_finalize;
+ oclass->parse_begin = MimeExternalObject_parse_begin;
+ oclass->parse_buffer = MimeExternalObject_parse_buffer;
+ oclass->parse_line = MimeExternalObject_parse_line;
+ oclass->displayable_inline_p = MimeExternalObject_displayable_inline_p;
+ lclass->parse_decoded_buffer = MimeExternalObject_parse_decoded_buffer;
+ return 0;
+}
+
+static int MimeExternalObject_initialize(MimeObject* object) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeExternalObject_finalize(MimeObject* object) {
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeExternalObject_parse_begin(MimeObject* obj) {
+ int status;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ // If we're writing this object, and we're doing it in raw form, then
+ // now is the time to inform the backend what the type of this data is.
+ //
+ if (obj->output_p && obj->options && !obj->options->write_html_p &&
+ !obj->options->state->first_data_written_p) {
+ status = MimeObject_output_init(obj, 0);
+ if (status < 0) return status;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ }
+
+ //
+ // If we're writing this object as HTML, do all the work now -- just write
+ // out a table with a link in it. (Later calls to the `parse_buffer' method
+ // will simply discard the data of the object itself.)
+ //
+ if (obj->options && obj->output_p && obj->options->write_html_p &&
+ obj->options->output_fn) {
+ MimeDisplayOptions newopt = *obj->options; // copy it
+ char* id = 0;
+ char* id_url = 0;
+ char* id_name = 0;
+ nsCString id_imap;
+ bool all_headers_p = obj->options->headers == MimeHeadersAll;
+
+ id = mime_part_address(obj);
+ if (obj->options->missing_parts) id_imap.Adopt(mime_imap_part_address(obj));
+ if (!id) return MIME_OUT_OF_MEMORY;
+
+ if (obj->options && obj->options->url) {
+ const char* url = obj->options->url;
+ if (!id_imap.IsEmpty() && id) {
+ // if this is an IMAP part.
+ id_url = mime_set_url_imap_part(url, id_imap.get(), id);
+ } else {
+ // This is just a normal MIME part as usual.
+ id_url = mime_set_url_part(url, id, true);
+ }
+ if (!id_url) {
+ PR_Free(id);
+ return MIME_OUT_OF_MEMORY;
+ }
+ }
+ if (!strcmp(id, "0")) {
+ PR_Free(id);
+ id = MimeGetStringByID(MIME_MSG_ATTACHMENT);
+ } else {
+ const char* p = "Part ";
+ uint32_t slen = strlen(p) + strlen(id) + 1;
+ char* s = (char*)PR_MALLOC(slen);
+ if (!s) {
+ PR_Free(id);
+ PR_Free(id_url);
+ return MIME_OUT_OF_MEMORY;
+ }
+ // we have a valid id
+ if (id) id_name = mime_find_suggested_name_of_part(id, obj);
+ PL_strncpyz(s, p, slen);
+ PL_strcatn(s, slen, id);
+ PR_Free(id);
+ id = s;
+ }
+
+ if (all_headers_p &&
+ // Don't bother showing all headers on this part if it's the only
+ // part in the message: in that case, we've already shown these
+ // headers.
+ obj->options->state && obj->options->state->root == obj->parent)
+ all_headers_p = false;
+
+ newopt.fancy_headers_p = true;
+ newopt.headers = (all_headers_p ? MimeHeadersAll : MimeHeadersSome);
+
+ /******
+ RICHIE SHERRY
+ GOTTA STILL DO THIS FOR QUOTING!
+ status = MimeHeaders_write_attachment_box(obj->headers, &newopt,
+ obj->content_type,
+ obj->encoding,
+ id_name? id_name : id, id_url, 0)
+ *****/
+
+ // obj->options really owns the storage for this.
+ newopt.part_to_load = nullptr;
+ newopt.default_charset = nullptr;
+ PR_FREEIF(id);
+ PR_FREEIF(id_url);
+ PR_FREEIF(id_name);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeExternalObject_parse_buffer(const char* buffer, int32_t size,
+ MimeObject* obj) {
+ NS_ASSERTION(!obj->closed_p, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (obj->closed_p) return -1;
+
+ // Currently, we always want to stream, in order to determine the size of the
+ // MIME object.
+
+ /* The data will be base64-decoded and passed to
+ MimeExternalObject_parse_decoded_buffer. */
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_buffer(buffer, size, obj);
+}
+
+static int MimeExternalObject_parse_decoded_buffer(const char* buf,
+ int32_t size,
+ MimeObject* obj) {
+ /* This is called (by MimeLeafClass->parse_buffer) with blocks of data
+ that have already been base64-decoded. This will only be called in
+ the case where we're not emitting HTML, and want access to the raw
+ data itself.
+
+ We override the `parse_decoded_buffer' method provided by MimeLeaf
+ because, unlike most children of MimeLeaf, we do not want to line-
+ buffer the decoded data -- we want to simply pass it along to the
+ backend, without going through our `parse_line' method.
+ */
+
+ /* Don't do a roundtrip through XPConnect when we're only interested in
+ * metadata and size. This includes when we are writing HTML (otherwise, the
+ * contents of binary attachments will just get dumped into messages when
+ * reading them) and the JS emitter (which doesn't care about attachment data
+ * at all). 0 means ok, the caller just checks for negative return value.
+ */
+ if (obj->options &&
+ (obj->options->metadata_only || obj->options->write_html_p))
+ return 0;
+ else
+ return MimeObject_write(obj, buf, size, true);
+}
+
+static int MimeExternalObject_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ NS_ERROR(
+ "This method should never be called (externals do no line buffering).");
+ return -1;
+}
+
+static bool MimeExternalObject_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs) {
+ return false;
+}
+
+#undef MIME_SUPERCLASS
+#define MIME_SUPERCLASS mimeExternalObjectClass
+MimeDefClass(MimeSuppressedCrypto, MimeSuppressedCryptoClass,
+ mimeSuppressedCryptoClass, &MIME_SUPERCLASS);
+
+static int MimeSuppressedCryptoClassInitialize(
+ MimeSuppressedCryptoClass* clazz) {
+ MimeExternalObjectClass* lclass = (MimeExternalObjectClass*)clazz;
+ return MimeExternalObjectClassInitialize(lclass);
+}
diff --git a/comm/mailnews/mime/src/mimeeobj.h b/comm/mailnews/mime/src/mimeeobj.h
new file mode 100644
index 0000000000..069a94f60c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeeobj.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEEOBJ_H_
+#define _MIMEEOBJ_H_
+
+#include "mimeleaf.h"
+
+/* The MimeExternalObject class represents MIME parts which contain data
+ which cannot be displayed inline -- application/octet-stream and any
+ other type that is not otherwise specially handled. (This is not to
+ be confused with MimeExternalBody, which is the handler for the
+ message/external-object MIME type only.)
+ */
+
+typedef struct MimeExternalObjectClass MimeExternalObjectClass;
+typedef struct MimeExternalObject MimeExternalObject;
+
+struct MimeExternalObjectClass {
+ MimeLeafClass leaf;
+};
+
+extern "C" MimeExternalObjectClass mimeExternalObjectClass;
+
+struct MimeExternalObject {
+ MimeLeaf leaf;
+};
+
+#define MimeExternalObjectClassInitializer(ITYPE, CSUPER) \
+ { MimeLeafClassInitializer(ITYPE, CSUPER) }
+
+typedef struct MimeSuppressedCryptoClass MimeSuppressedCryptoClass;
+typedef struct MimeSuppressedCrypto MimeSuppressedCrypto;
+
+struct MimeSuppressedCryptoClass {
+ MimeExternalObjectClass eobj;
+};
+
+extern "C" MimeSuppressedCryptoClass mimeSuppressedCryptoClass;
+
+struct MimeSuppressedCrypto {
+ MimeExternalObject eobj;
+};
+
+#define MimeSuppressedCryptoClassInitializer(ITYPE, CSUPER) \
+ { MimeExternalObjectClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEEOBJ_H_ */
diff --git a/comm/mailnews/mime/src/mimefilt.cpp b/comm/mailnews/mime/src/mimefilt.cpp
new file mode 100644
index 0000000000..61b1ac6ae8
--- /dev/null
+++ b/comm/mailnews/mime/src/mimefilt.cpp
@@ -0,0 +1,349 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* mimefilt.c --- test harness for libmime.a
+
+ This program reads a message from stdin and writes the output of the MIME
+ parser on stdout.
+
+ Parameters can be passed to the parser through the usual URL mechanism:
+
+ mimefilt BASE-URL?headers=all&rot13 < in > out
+
+ Some parameters can't be affected that way, so some additional switches
+ may be passed on the command line after the URL:
+
+ -fancy whether fancy headers should be generated (default)
+
+ -no-fancy opposite; this uses the headers used in the cases of
+ FO_SAVE_AS_TEXT or FO_QUOTE_MESSAGE
+
+ -html whether we should convert to HTML (like FO_PRESENT);
+ this is the default if no ?part= is specified.
+
+ -raw don't convert to HTML (FO_SAVE_AS);
+ this is the default if a ?part= is specified.
+
+ -outline at the end, print a debugging overview of the MIME structure
+
+ Before any output comes a blurb listing the content-type, charset, and
+ various other info that would have been put in the generated URL struct.
+ It's printed to the beginning of the output because otherwise this out-
+ of-band data would have been lost. (So the output of this program is,
+ in fact, a raw HTTP response.)
+ */
+
+#include "mimemsg.h"
+#include "prglobal.h"
+
+#include "key.h"
+#include "cert.h"
+#include "secrng.h"
+#include "secmod.h"
+#include "pk11func.h"
+#include "nsMimeStringResources.h"
+
+#ifndef XP_UNIX
+ERROR! This is a unix-only file for the "mimefilt" standalone program.
+ This does not go into libmime.a.
+#endif
+
+
+static char *
+test_file_type (const char *filename, void *stream_closure)
+{
+ const char* suf = PL_strrchr(filename, '.');
+ if (!suf) return 0;
+ suf++;
+
+ if (!PL_strcasecmp(suf, "txt") || !PL_strcasecmp(suf, "text"))
+ return strdup("text/plain");
+ else if (!PL_strcasecmp(suf, "htm") || !PL_strcasecmp(suf, "html"))
+ return strdup("text/html");
+ else if (!PL_strcasecmp(suf, "gif"))
+ return strdup("image/gif");
+ else if (!PL_strcasecmp(suf, "svg"))
+ return strdup("image/svg+xml");
+ else if (!PL_strcasecmp(suf, "jpg") || !PL_strcasecmp(suf, "jpeg"))
+ return strdup("image/jpeg");
+ else if (!PL_strcasecmp(suf, "pjpg") || !PL_strcasecmp(suf, "pjpeg"))
+ return strdup("image/pjpeg");
+ else if (!PL_strcasecmp(suf, "xbm"))
+ return strdup("image/x-xbitmap");
+ else if (!PL_strcasecmp(suf, "xpm"))
+ return strdup("image/x-xpixmap");
+ else if (!PL_strcasecmp(suf, "xwd"))
+ return strdup("image/x-xwindowdump");
+ else if (!PL_strcasecmp(suf, "bmp"))
+ return strdup("image/x-MS-bmp");
+ else if (!PL_strcasecmp(suf, "au"))
+ return strdup("audio/basic");
+ else if (!PL_strcasecmp(suf, "aif") || !PL_strcasecmp(suf, "aiff") ||
+ !PL_strcasecmp(suf, "aifc"))
+ return strdup("audio/x-aiff");
+ else if (!PL_strcasecmp(suf, "ps"))
+ return strdup("application/postscript");
+ else
+ return 0;
+}
+
+static int test_output_fn(char* buf, int32_t size, void* closure) {
+ FILE* out = (FILE*)closure;
+ if (out)
+ return fwrite(buf, sizeof(*buf), size, out);
+ else
+ return 0;
+}
+
+static int test_output_init_fn(const char* type, const char* charset,
+ const char* name, const char* x_mac_type,
+ const char* x_mac_creator,
+ void* stream_closure) {
+ FILE* out = (FILE*)stream_closure;
+ fprintf(out, "CONTENT-TYPE: %s", type);
+ if (charset) fprintf(out, "; charset=\"%s\"", charset);
+ if (name) fprintf(out, "; name=\"%s\"", name);
+ if (x_mac_type || x_mac_creator)
+ fprintf(out, "; x-mac-type=\"%s\"; x-mac-creator=\"%s\"",
+ x_mac_type ? x_mac_type : "", x_mac_creator ? x_mac_type : "");
+ fprintf(out, CRLF CRLF);
+ return 0;
+}
+
+static void* test_image_begin(const char* image_url, const char* content_type,
+ void* stream_closure) {
+ return ((void*)strdup(image_url));
+}
+
+static void test_image_end(void* image_closure, int status) {
+ char* url = (char*)image_closure;
+ if (url) PR_Free(url);
+}
+
+static char* test_image_make_image_html(void* image_data) {
+ char* url = (char*)image_data;
+#if 0
+ const char *prefix = "<P><CENTER><IMG SRC=\"";
+ const char *suffix = "\"></CENTER><P>";
+#else
+ const char* prefix =
+ ("<P><CENTER><TABLE BORDER=2 CELLPADDING=20"
+ " BGCOLOR=WHITE>"
+ "<TR><TD ALIGN=CENTER>"
+ "an inlined image would have gone here for<BR>");
+ const char* suffix = "</TD></TR></TABLE></CENTER><P>";
+#endif
+ uint32_t buflen = strlen(prefix) + strlen(suffix) + strlen(url) + 20;
+ char* buf = (char*)PR_MALLOC(buflen);
+ if (!buf) return 0;
+ *buf = 0;
+ PL_strcatn(buf, buflen, prefix);
+ PL_strcatn(buf, buflen, url);
+ PL_strcatn(buf, buflen, suffix);
+ return buf;
+}
+
+static int test_image_write_buffer(const char* buf, int32_t size,
+ void* image_closure) {
+ return 0;
+}
+
+static char* test_passwd_prompt(PK11SlotInfo* slot, void* wincx) {
+ char buf[2048], *s;
+ fprintf(stdout, "#### Password required: ");
+ s = fgets(buf, sizeof(buf) - 1, stdin);
+ if (!s) return s;
+ size_t s_len = strlen(s);
+ if (s_len && s[slen - 1] == '\r' || s[slen - 1] == '\n') s[slen - 1] = '\0';
+ return s;
+}
+
+int test(FILE* in, FILE* out, const char* url, bool fancy_headers_p,
+ bool html_p, bool outline_p, bool dexlate_p,
+ bool variable_width_plaintext_p) {
+ int status = 0;
+ MimeObject* obj = 0;
+ MimeDisplayOptions* opt = new MimeDisplayOptions;
+ // memset(opt, 0, sizeof(*opt));
+
+ if (dexlate_p) html_p = false;
+
+ opt->fancy_headers_p = fancy_headers_p;
+ opt->headers = MimeHeadersSome;
+ opt->rot13_p = false;
+
+ status = mime_parse_url_options(url, opt);
+ if (status < 0) {
+ PR_Free(opt);
+ return MIME_OUT_OF_MEMORY;
+ }
+
+ opt->url = url;
+ opt->write_html_p = html_p;
+ opt->dexlate_p = dexlate_p;
+ opt->output_init_fn = test_output_init_fn;
+ opt->output_fn = test_output_fn;
+ opt->charset_conversion_fn = 0;
+ opt->rfc1522_conversion_p = false;
+ opt->file_type_fn = test_file_type;
+ opt->stream_closure = out;
+
+ opt->image_begin = test_image_begin;
+ opt->image_end = test_image_end;
+ opt->make_image_html = test_image_make_image_html;
+ opt->image_write_buffer = test_image_write_buffer;
+
+ opt->variable_width_plaintext_p = variable_width_plaintext_p;
+
+ obj = mime_new((MimeObjectClass*)&mimeMessageClass, (MimeHeaders*)NULL,
+ MESSAGE_RFC822);
+ if (!obj) {
+ PR_Free(opt);
+ return MIME_OUT_OF_MEMORY;
+ }
+ obj->options = opt;
+
+ status = obj->class->initialize(obj);
+ if (status >= 0) status = obj->class->parse_begin(obj);
+ if (status < 0) {
+ PR_Free(opt);
+ PR_Free(obj);
+ return MIME_OUT_OF_MEMORY;
+ }
+
+ while (1) {
+ char buf[255];
+ int size = fread(buf, sizeof(*buf), sizeof(buf), stdin);
+ if (size <= 0) break;
+ status = obj->class->parse_buffer(buf, size, obj);
+ if (status < 0) {
+ mime_free(obj);
+ PR_Free(opt);
+ return status;
+ }
+ }
+
+ status = obj->class->parse_eof(obj, false);
+ if (status >= 0) status = obj->class->parse_end(obj, false);
+ if (status < 0) {
+ mime_free(obj);
+ PR_Free(opt);
+ return status;
+ }
+
+ if (outline_p) {
+ fprintf(
+ out,
+ "\n\n"
+ "###############################################################\n");
+ obj->class->debug_print(obj, stderr, 0);
+ fprintf(
+ out,
+ "###############################################################\n");
+ }
+
+ mime_free(obj);
+ PR_Free(opt);
+ return 0;
+}
+
+static char* test_cdb_name_cb(void* arg, int vers) {
+ static char f[1024];
+ if (vers <= 4)
+ sprintf(f, "%s/.netscape/cert.db", getenv("HOME"));
+ else
+ sprintf(f, "%s/.netscape/cert%d.db", getenv("HOME"), vers);
+ return f;
+}
+
+static char* test_kdb_name_cb(void* arg, int vers) {
+ static char f[1024];
+ if (vers <= 2)
+ sprintf(f, "%s/.netscape/key.db", getenv("HOME"));
+ else
+ sprintf(f, "%s/.netscape/key%d.db", getenv("HOME"), vers);
+ return f;
+}
+
+extern void SEC_Init(void);
+
+int main(int argc, char** argv) {
+ int32_t i = 1;
+ char* url = "";
+ bool fancy_p = true;
+ bool html_p = true;
+ bool outline_p = false;
+ bool dexlate_p = false;
+ char filename[1000];
+ CERTCertDBHandle* cdb_handle;
+ SECKEYKeyDBHandle* kdb_handle;
+
+ PR_Init("mimefilt", 24, 1, 0);
+
+ cdb_handle = (CERTCertDBHandle*)calloc(1, sizeof(*cdb_handle));
+
+ if (SECSuccess != CERT_OpenCertDB(cdb_handle, false, test_cdb_name_cb, NULL))
+ CERT_OpenVolatileCertDB(cdb_handle);
+ CERT_SetDefaultCertDB(cdb_handle);
+
+ RNG_RNGInit();
+
+ kdb_handle = SECKEY_OpenKeyDB(false, test_kdb_name_cb, NULL);
+ SECKEY_SetDefaultKeyDB(kdb_handle);
+
+ PK11_SetPasswordFunc(test_passwd_prompt);
+
+ sprintf(filename, "%s/.netscape/secmodule.db", getenv("HOME"));
+ SECMOD_init(filename);
+
+ SEC_Init();
+
+ if (i < argc) {
+ if (argv[i][0] == '-')
+ url = strdup("");
+ else
+ url = argv[i++];
+ }
+
+ if (url && (PL_strstr(url, "?part=") || PL_strstr(url, "&part=")))
+ html_p = false;
+
+ while (i < argc) {
+ if (!strcmp(argv[i], "-fancy"))
+ fancy_p = true;
+ else if (!strcmp(argv[i], "-no-fancy"))
+ fancy_p = false;
+ else if (!strcmp(argv[i], "-html"))
+ html_p = true;
+ else if (!strcmp(argv[i], "-raw"))
+ html_p = false;
+ else if (!strcmp(argv[i], "-outline"))
+ outline_p = true;
+ else if (!strcmp(argv[i], "-dexlate"))
+ dexlate_p = true;
+ else {
+ fprintf(
+ stderr,
+ "usage: %s [ URL [ -fancy | -no-fancy | -html | -raw | -outline | "
+ "-dexlate ]]\n"
+ " < message/rfc822 > output\n",
+ (PL_strrchr(argv[0], '/') ? PL_strrchr(argv[0], '/') + 1 : argv[0]));
+ i = 1;
+ goto FAIL;
+ }
+ i++;
+ }
+
+ i = test(stdin, stdout, url, fancy_p, html_p, outline_p, dexlate_p, true);
+ fprintf(stdout, "\n");
+ fflush(stdout);
+
+FAIL:
+
+ CERT_ClosePermCertDB(cdb_handle);
+ SECKEY_CloseKeyDB(kdb_handle);
+
+ exit(i);
+}
diff --git a/comm/mailnews/mime/src/mimehdrs.cpp b/comm/mailnews/mime/src/mimehdrs.cpp
new file mode 100644
index 0000000000..6df9028765
--- /dev/null
+++ b/comm/mailnews/mime/src/mimehdrs.cpp
@@ -0,0 +1,785 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "msgCore.h"
+#include "mimei.h"
+#include "prmem.h"
+#include "prlog.h"
+#include "plstr.h"
+#include "mimebuf.h"
+#include "mimemoz2.h"
+#include "comi18n.h"
+#include "nsMailHeaders.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "nsMsgI18N.h"
+#include "mimehdrs.h"
+#include "nsIMIMEHeaderParam.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include <ctype.h>
+#include "nsMsgUtils.h"
+#include "mozilla/Unused.h"
+
+// Forward declares...
+int32_t MimeHeaders_build_heads_list(MimeHeaders* hdrs);
+
+void MimeHeaders_convert_header_value(MimeDisplayOptions* opt, nsCString& value,
+ bool convert_charset_only) {
+ if (value.IsEmpty()) return;
+
+ if (convert_charset_only) {
+ nsAutoCString output;
+ nsMsgI18NConvertRawBytesToUTF8(
+ value,
+ opt->default_charset ? nsDependentCString(opt->default_charset)
+ : EmptyCString(),
+ output);
+ value.Assign(output);
+ return;
+ }
+
+ if (opt && opt->rfc1522_conversion_p) {
+ nsAutoCString temporary;
+ MIME_DecodeMimeHeader(value.get(), opt->default_charset,
+ opt->override_charset, true, temporary);
+
+ if (!temporary.IsEmpty()) {
+ value = temporary;
+ }
+ } else {
+ // This behavior, though highly unusual, was carefully preserved
+ // from the previous implementation. It may be that this is dead
+ // code, in which case opt->rfc1522_conversion_p is no longer
+ // needed.
+ value.Truncate();
+ }
+}
+
+MimeHeaders* MimeHeaders_new(void) {
+ MimeHeaders* hdrs = (MimeHeaders*)PR_MALLOC(sizeof(MimeHeaders));
+ if (!hdrs) return 0;
+
+ memset(hdrs, 0, sizeof(*hdrs));
+ hdrs->done_p = false;
+
+ return hdrs;
+}
+
+void MimeHeaders_free(MimeHeaders* hdrs) {
+ if (!hdrs) return;
+ PR_FREEIF(hdrs->all_headers);
+ PR_FREEIF(hdrs->heads);
+ PR_FREEIF(hdrs->obuffer);
+ PR_FREEIF(hdrs->munged_subject);
+ hdrs->obuffer_fp = 0;
+ hdrs->obuffer_size = 0;
+
+#ifdef DEBUG__
+ {
+ int i, size = sizeof(*hdrs);
+ uint32_t* array = (uint32_t*)hdrs;
+ for (i = 0; i < (size / sizeof(*array)); i++)
+ array[i] = (uint32_t)0xDEADBEEF;
+ }
+#endif /* DEBUG */
+
+ PR_Free(hdrs);
+}
+
+int MimeHeaders_parse_line(const char* buffer, int32_t size,
+ MimeHeaders* hdrs) {
+ int status = 0;
+ int desired_size;
+
+ NS_ASSERTION(hdrs, "1.22 <rhp@netscape.com> 22 Aug 1999 08:48");
+ if (!hdrs) return -1;
+
+ /* Don't try and feed me more data after having fed me a blank line... */
+ NS_ASSERTION(!hdrs->done_p, "1.22 <rhp@netscape.com> 22 Aug 1999 08:48");
+ if (hdrs->done_p) return -1;
+
+ if (!buffer || size == 0 || *buffer == '\r' || *buffer == '\n') {
+ /* If this is a blank line, we're done.
+ */
+ hdrs->done_p = true;
+ return MimeHeaders_build_heads_list(hdrs);
+ }
+
+ /* Tack this data on to the end of our copy.
+ */
+ desired_size = hdrs->all_headers_fp + size + 1;
+ if (desired_size >= hdrs->all_headers_size) {
+ status = mime_GrowBuffer(desired_size, sizeof(char), 255,
+ &hdrs->all_headers, &hdrs->all_headers_size);
+ if (status < 0) return status;
+ }
+ memcpy(hdrs->all_headers + hdrs->all_headers_fp, buffer, size);
+ hdrs->all_headers_fp += size;
+
+ return 0;
+}
+
+MimeHeaders* MimeHeaders_copy(MimeHeaders* hdrs) {
+ MimeHeaders* hdrs2;
+ if (!hdrs) return 0;
+
+ hdrs2 = (MimeHeaders*)PR_MALLOC(sizeof(*hdrs));
+ if (!hdrs2) return 0;
+ memset(hdrs2, 0, sizeof(*hdrs2));
+
+ if (hdrs->all_headers) {
+ hdrs2->all_headers = (char*)PR_MALLOC(hdrs->all_headers_fp);
+ if (!hdrs2->all_headers) {
+ PR_Free(hdrs2);
+ return 0;
+ }
+ memcpy(hdrs2->all_headers, hdrs->all_headers, hdrs->all_headers_fp);
+
+ hdrs2->all_headers_fp = hdrs->all_headers_fp;
+ hdrs2->all_headers_size = hdrs->all_headers_fp;
+ }
+
+ hdrs2->done_p = hdrs->done_p;
+
+ if (hdrs->heads) {
+ int i;
+ hdrs2->heads = (char**)PR_MALLOC(hdrs->heads_size * sizeof(*hdrs->heads));
+ if (!hdrs2->heads) {
+ PR_FREEIF(hdrs2->all_headers);
+ PR_Free(hdrs2);
+ return 0;
+ }
+ hdrs2->heads_size = hdrs->heads_size;
+ for (i = 0; i < hdrs->heads_size; i++) {
+ hdrs2->heads[i] =
+ (hdrs2->all_headers + (hdrs->heads[i] - hdrs->all_headers));
+ }
+ }
+ return hdrs2;
+}
+
+static bool find_header_starts(MimeHeaders* hdrs, bool counting) {
+ const char* end = hdrs->all_headers + hdrs->all_headers_fp;
+ char* s = hdrs->all_headers;
+ int i = 0;
+
+ if (counting) {
+ // For the start pointer
+ hdrs->heads_size = 1;
+ } else {
+ hdrs->heads[i++] = hdrs->all_headers;
+ }
+
+ while (s < end) {
+ SEARCH_NEWLINE:
+ while (s < end && *s != '\r' && *s != '\n') s++;
+
+ if (s >= end) break;
+
+ /* If "\r\n " or "\r\n\t" is next, that doesn't terminate the header. */
+ else if (s + 2 < end && (s[0] == '\r' && s[1] == '\n') &&
+ (s[2] == ' ' || s[2] == '\t')) {
+ s += 3;
+ goto SEARCH_NEWLINE;
+ }
+ /* If "\r " or "\r\t" or "\n " or "\n\t" is next, that doesn't terminate
+ the header either. */
+ else if (s + 1 < end && (s[0] == '\r' || s[0] == '\n') &&
+ (s[1] == ' ' || s[1] == '\t')) {
+ s += 2;
+ goto SEARCH_NEWLINE;
+ }
+
+ /* At this point, `s' points before a header-terminating newline.
+ Move past that newline, and store that new position in `heads'.
+ */
+ if (*s == '\r') s++;
+
+ if (s >= end) break;
+
+ if (*s == '\n') s++;
+
+ if (s < end) {
+ if (counting) {
+ hdrs->heads_size++;
+ } else {
+ NS_ASSERTION(i < hdrs->heads_size,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (i >= hdrs->heads_size) return false;
+ hdrs->heads[i++] = s;
+ }
+ }
+ }
+ if (!counting) {
+ NS_ASSERTION(i == hdrs->heads_size, "unexpected");
+ }
+ return true;
+}
+
+int MimeHeaders_build_heads_list(MimeHeaders* hdrs) {
+ NS_ASSERTION(hdrs, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!hdrs) return -1;
+
+ NS_ASSERTION(hdrs->done_p && !hdrs->heads,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!hdrs->done_p || hdrs->heads) return -1;
+
+ if (hdrs->all_headers_fp == 0) {
+ /* Must not have been any headers (we got the blank line right away.) */
+ PR_FREEIF(hdrs->all_headers);
+ hdrs->all_headers_size = 0;
+ return 0;
+ }
+
+ /* At this point, we might as well realloc all_headers back down to the
+ minimum size it must be (it could be up to 1k bigger.) But don't
+ bother if we're only off by a tiny bit. */
+ NS_ASSERTION(hdrs->all_headers_fp <= hdrs->all_headers_size,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (hdrs->all_headers_fp + 60 <= hdrs->all_headers_size) {
+ char* ls = (char*)PR_Realloc(hdrs->all_headers, hdrs->all_headers_fp);
+ if (ls) /* can this ever fail? we're making it smaller... */
+ {
+ hdrs->all_headers = ls; /* in case it got relocated */
+ hdrs->all_headers_size = hdrs->all_headers_fp;
+ }
+ }
+
+ find_header_starts(hdrs, true);
+
+ /* Now allocate storage for the pointers to each of those headers.
+ */
+ hdrs->heads = (char**)PR_MALLOC((hdrs->heads_size) * sizeof(char*));
+ if (!hdrs->heads) return MIME_OUT_OF_MEMORY;
+ memset(hdrs->heads, 0, (hdrs->heads_size) * sizeof(char*));
+
+ /* Now make another pass through the headers, and this time, record the
+ starting position of each header.
+ */
+ if (!find_header_starts(hdrs, false)) {
+ return -1;
+ }
+
+ return 0;
+}
+
+char* MimeHeaders_get(MimeHeaders* hdrs, const char* header_name, bool strip_p,
+ bool all_p) {
+ int i;
+ int name_length;
+ char* result = 0;
+
+ if (!hdrs) return 0;
+ NS_ASSERTION(header_name, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!header_name) return 0;
+
+ /* Specifying strip_p and all_p at the same time doesn't make sense... */
+ NS_ASSERTION(!(strip_p && all_p), "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ /* One shouldn't be trying to read headers when one hasn't finished
+ parsing them yet... but this can happen if the message ended
+ prematurely, and has no body at all (as opposed to a null body,
+ which is more normal.) So, if we try to read from the headers,
+ let's assume that the headers are now finished. If they aren't
+ in fact finished, then a later attempt to write to them will assert.
+ */
+ if (!hdrs->done_p) {
+ int status;
+ hdrs->done_p = true;
+ status = MimeHeaders_build_heads_list(hdrs);
+ if (status < 0) return 0;
+ }
+
+ if (!hdrs->heads) /* Must not have been any headers. */
+ {
+ NS_ASSERTION(hdrs->all_headers_fp == 0,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ return 0;
+ }
+
+ name_length = strlen(header_name);
+
+ for (i = 0; i < hdrs->heads_size; i++) {
+ char* head = hdrs->heads[i];
+ char* end =
+ (i == hdrs->heads_size - 1 ? hdrs->all_headers + hdrs->all_headers_fp
+ : hdrs->heads[i + 1]);
+ char *colon, *ocolon;
+
+ NS_ASSERTION(head, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!head) continue;
+ size_t headLen = end - head;
+
+ /* Quick hack to skip over BSD Mailbox delimiter. */
+ if (i == 0 && head[0] == 'F' && headLen >= 5 && !strncmp(head, "From ", 5))
+ continue;
+
+ /* Find the colon. */
+ for (colon = head; colon < end; colon++)
+ if (*colon == ':') break;
+
+ if (colon >= end) continue;
+
+ /* Back up over whitespace before the colon. */
+ ocolon = colon;
+ for (; colon > head && IS_SPACE(colon[-1]); colon--)
+ ;
+
+ /* If the strings aren't the same length, it doesn't match. */
+ if (name_length != colon - head) continue;
+
+ /* If the strings differ, it doesn't match. */
+ if (PL_strncasecmp(header_name, head, name_length)) continue;
+
+ /* Otherwise, we've got a match. */
+ {
+ char* contents = ocolon + 1;
+ char* s;
+
+ /* Skip over whitespace after colon. */
+ while (contents < end && IS_SPACE(contents[0])) {
+ /* Mac or Unix style line break, followed by space or tab. */
+ if (contents < (end - 1) &&
+ (contents[0] == '\r' || contents[0] == '\n') &&
+ (contents[1] == ' ' || contents[1] == '\t'))
+ contents += 2;
+ /* Windows style line break, followed by space or tab. */
+ else if (contents < (end - 2) && contents[0] == '\r' &&
+ contents[1] == '\n' &&
+ (contents[2] == ' ' || contents[2] == '\t'))
+ contents += 3;
+ /* Any space or tab. */
+ else if (contents[0] == ' ' || contents[0] == '\t')
+ contents++;
+ /* If we get here, it's because this character is a line break
+ followed by non-whitespace, or a line break followed by
+ another line break
+ */
+ else {
+ end = contents;
+ break;
+ }
+ }
+
+ /* If we're supposed to strip at the first token, pull `end' back to
+ the first whitespace or ';' after the first token.
+ */
+ if (strip_p) {
+ for (s = contents; s < end && *s != ';' && *s != ',' && !IS_SPACE(*s);
+ s++)
+ ;
+ end = s;
+ }
+
+ /* Now allocate some storage.
+ If `result' already has a value, enlarge it.
+ Otherwise, just allocate a block.
+ `s' gets set to the place where the new data goes.
+ */
+ if (!result) {
+ result = (char*)PR_MALLOC(end - contents + 1);
+ if (!result) return 0;
+ s = result;
+ } else {
+ int32_t L = strlen(result);
+ s = (char*)PR_Realloc(result, (L + (end - contents + 10)));
+ if (!s) {
+ PR_Free(result);
+ return 0;
+ }
+ result = s;
+ s = result + L;
+
+ /* Since we are tacking more data onto the end of the header
+ field, we must make it be a well-formed continuation line,
+ by separating the old and new data with CR-LF-TAB.
+ */
+ *s++ = ','; /* #### only do this for addr headers? */
+ *s++ = MSG_LINEBREAK[0];
+#if (MSG_LINEBREAK_LEN == 2)
+ *s++ = MSG_LINEBREAK[1];
+#endif
+ *s++ = '\t';
+ }
+
+ /* Take off trailing whitespace... */
+ while (end > contents && IS_SPACE(end[-1])) end--;
+
+ if (end > contents) {
+ /* Now copy the header's contents in...
+ */
+ memcpy(s, contents, end - contents);
+ s[end - contents] = 0;
+ } else {
+ s[0] = 0;
+ }
+
+ /* If we only wanted the first occurrence of this header, we're done. */
+ if (!all_p) break;
+ }
+ }
+
+ if (result && !*result) /* empty string */
+ {
+ PR_Free(result);
+ return 0;
+ }
+
+ return result;
+}
+
+char* MimeHeaders_get_parameter(const char* header_value, const char* parm_name,
+ char** charset, char** language) {
+ if (!header_value || !parm_name || !*header_value || !*parm_name)
+ return nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIMIMEHeaderParam> mimehdrpar =
+ do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) return nullptr;
+
+ nsCString result;
+ rv = mimehdrpar->GetParameterInternal(nsDependentCString(header_value),
+ parm_name, charset, language,
+ getter_Copies(result));
+ return NS_SUCCEEDED(rv) ? PL_strdup(result.get()) : nullptr;
+}
+
+#define MimeHeaders_write(HDRS, OPT, DATA, LENGTH) \
+ MimeOptions_write((HDRS), (OPT), (DATA), (LENGTH), true);
+
+#define MimeHeaders_grow_obuffer(hdrs, desired_size) \
+ ((((long)(desired_size)) >= ((long)(hdrs)->obuffer_size)) \
+ ? mime_GrowBuffer((desired_size), sizeof(char), 255, &(hdrs)->obuffer, \
+ &(hdrs)->obuffer_size) \
+ : 0)
+
+int MimeHeaders_write_all_headers(MimeHeaders* hdrs, MimeDisplayOptions* opt,
+ bool attachment) {
+ int status = 0;
+ int i;
+ bool wrote_any_p = false;
+
+ NS_ASSERTION(hdrs, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!hdrs) return -1;
+
+ /* One shouldn't be trying to read headers when one hasn't finished
+ parsing them yet... but this can happen if the message ended
+ prematurely, and has no body at all (as opposed to a null body,
+ which is more normal.) So, if we try to read from the headers,
+ let's assume that the headers are now finished. If they aren't
+ in fact finished, then a later attempt to write to them will assert.
+ */
+ if (!hdrs->done_p) {
+ hdrs->done_p = true;
+ status = MimeHeaders_build_heads_list(hdrs);
+ if (status < 0) return 0;
+ }
+
+ char* charset = nullptr;
+ if (opt->format_out == nsMimeOutput::nsMimeMessageSaveAs) {
+ if (opt->override_charset)
+ charset = PL_strdup(opt->default_charset);
+ else {
+ char* contentType =
+ MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (contentType)
+ charset = MimeHeaders_get_parameter(contentType, HEADER_PARM_CHARSET,
+ nullptr, nullptr);
+ PR_FREEIF(contentType);
+ }
+ }
+
+ for (i = 0; i < hdrs->heads_size; i++) {
+ char* head = hdrs->heads[i];
+ char* end =
+ (i == hdrs->heads_size - 1 ? hdrs->all_headers + hdrs->all_headers_fp
+ : hdrs->heads[i + 1]);
+ char *colon, *ocolon;
+ char* contents = end;
+ size_t headLen = end - head;
+
+ /* Hack for BSD Mailbox delimiter. */
+ if (i == 0 && head[0] == 'F' && headLen >= 5 &&
+ !strncmp(head, "From ", 5)) {
+ /* For now, we don't really want this header to be output so
+ we are going to just continue */
+ continue;
+ /* colon = head + 4; contents = colon + 1; */
+ } else {
+ /* Find the colon. */
+ for (colon = head; colon < end && *colon != ':'; colon++)
+ ;
+
+ /* Back up over whitespace before the colon. */
+ ocolon = colon;
+ for (; colon > head && IS_SPACE(colon[-1]); colon--)
+ ;
+
+ contents = ocolon + 1;
+ }
+
+ /* Skip over whitespace after colon. */
+ while (contents < end && IS_SPACE(*contents)) contents++;
+
+ /* Take off trailing whitespace... */
+ while (end > contents && IS_SPACE(end[-1])) end--;
+
+ nsAutoCString name(Substring(head, colon));
+ nsAutoCString hdr_value;
+
+ if ((end - contents) > 0) {
+ hdr_value = Substring(contents, end);
+ }
+
+ // MW Fixme: more?
+ bool convert_charset_only = name.LowerCaseEqualsLiteral("to") ||
+ name.LowerCaseEqualsLiteral("from") ||
+ name.LowerCaseEqualsLiteral("cc") ||
+ name.LowerCaseEqualsLiteral("bcc") ||
+ name.LowerCaseEqualsLiteral("reply-to") ||
+ name.LowerCaseEqualsLiteral("sender");
+ MimeHeaders_convert_header_value(opt, hdr_value, convert_charset_only);
+ // if we're saving as html, we need to convert headers from utf8 to message
+ // charset, if any
+ if (opt->format_out == nsMimeOutput::nsMimeMessageSaveAs && charset) {
+ nsAutoCString convertedStr;
+ if (NS_SUCCEEDED(nsMsgI18NConvertFromUnicode(
+ nsDependentCString(charset), NS_ConvertUTF8toUTF16(hdr_value),
+ convertedStr))) {
+ hdr_value = convertedStr;
+ }
+ }
+
+ if (attachment) {
+ if (NS_FAILED(
+ mimeEmitterAddAttachmentField(opt, name.get(), hdr_value.get())))
+ status = -1;
+ } else {
+ if (NS_FAILED(
+ mimeEmitterAddHeaderField(opt, name.get(), hdr_value.get())))
+ status = -1;
+ }
+
+ if (status < 0) return status;
+ if (!wrote_any_p) wrote_any_p = (status > 0);
+ }
+ mimeEmitterAddAllHeaders(opt, hdrs->all_headers, hdrs->all_headers_fp);
+ PR_FREEIF(charset);
+
+ return 1;
+}
+
+/* Strip CR+LF runs within (original).
+ Since the string at (original) can only shrink,
+ this conversion is done in place. (original)
+ is returned. */
+extern "C" char* MIME_StripContinuations(char* original) {
+ char *p1, *p2;
+
+ /* If we were given a null string, return it as is */
+ if (!original) return NULL;
+
+ /* Start source and dest pointers at the beginning */
+ p1 = p2 = original;
+
+ while (*p2) {
+ /* p2 runs ahead at (CR and/or LF) */
+ if ((p2[0] == '\r') || (p2[0] == '\n'))
+ p2++;
+ else if (p2 > p1)
+ *p1++ = *p2++;
+ else {
+ p1++;
+ p2++;
+ }
+ }
+ *p1 = '\0';
+
+ return original;
+}
+
+extern int16_t INTL_DefaultMailToWinCharSetID(int16_t csid);
+
+/* Given text purporting to be a qtext header value, strip backslashes that
+ may be escaping other chars in the string. */
+char* mime_decode_filename(const char* name, const char* charset,
+ MimeDisplayOptions* opt) {
+ nsresult rv;
+ nsCOMPtr<nsIMIMEHeaderParam> mimehdrpar =
+ do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) return nullptr;
+ nsAutoCString result;
+ rv = mimehdrpar->DecodeParameter(nsDependentCString(name), charset,
+ opt ? opt->default_charset : nullptr,
+ opt ? opt->override_charset : false, result);
+ return NS_SUCCEEDED(rv) ? PL_strdup(result.get()) : nullptr;
+}
+
+/* Pull the name out of some header or another. Order is:
+ Content-Disposition: XXX; filename=NAME (RFC 1521/1806)
+ Content-Type: XXX; name=NAME (RFC 1341)
+ Content-Name: NAME (no RFC, but seen to occur)
+ X-Sun-Data-Name: NAME (no RFC, but used by MailTool)
+ */
+char* MimeHeaders_get_name(MimeHeaders* hdrs, MimeDisplayOptions* opt) {
+ char *s = 0, *name = 0, *cvt = 0;
+ char* charset = nullptr; // for RFC2231 support
+
+ s = MimeHeaders_get(hdrs, HEADER_CONTENT_DISPOSITION, false, false);
+ if (s) {
+ name = MimeHeaders_get_parameter(s, HEADER_PARM_FILENAME, &charset, NULL);
+ PR_Free(s);
+ }
+
+ if (!name) {
+ s = MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (s) {
+ free(charset);
+
+ name = MimeHeaders_get_parameter(s, HEADER_PARM_NAME, &charset, NULL);
+ PR_Free(s);
+ }
+ }
+
+ if (!name) name = MimeHeaders_get(hdrs, HEADER_CONTENT_NAME, false, false);
+
+ if (!name) name = MimeHeaders_get(hdrs, HEADER_X_SUN_DATA_NAME, false, false);
+
+ if (name) {
+ /* First remove continuation delimiters (CR+LF+space), then
+ remove escape ('\\') characters, then attempt to decode
+ mime-2 encoded-words. The latter two are done in
+ mime_decode_filename.
+ */
+ MIME_StripContinuations(name);
+
+ /* Argh. What we should do if we want to be robust is to decode qtext
+ in all appropriate headers. Unfortunately, that would be too scary
+ at this juncture. So just decode qtext/mime2 here. */
+ cvt = mime_decode_filename(name, charset, opt);
+
+ free(charset);
+
+ if (cvt && cvt != name) {
+ PR_Free(name);
+ name = cvt;
+ }
+ }
+
+ return name;
+}
+
+#ifdef XP_UNIX
+/* This piece of junk is so that I can use BBDB with Mozilla.
+ = Put bbdb-srv.perl on your path.
+ = Put bbdb-srv.el on your lisp path.
+ = Make sure gnudoit (comes with xemacs) is on your path.
+ = Put (gnuserv-start) in ~/.emacs
+ = setenv NS_MSG_DISPLAY_HOOK bbdb-srv.perl
+ */
+void MimeHeaders_do_unix_display_hook_hack(MimeHeaders* hdrs) {
+ static const char* cmd = 0;
+ if (!cmd) {
+ /* The first time we're invoked, look up the command in the
+ environment. Use "" as the `no command' tag. */
+ cmd = getenv("NS_MSG_DISPLAY_HOOK");
+ if (!cmd) cmd = "";
+ }
+
+ /* Invoke "cmd" at the end of a pipe, and give it the headers on stdin.
+ The command is expected to be safe from hostile input!!
+ */
+ if (cmd && *cmd) {
+ FILE* fp = popen(cmd, "w");
+ if (fp) {
+ mozilla::Unused << fwrite(hdrs->all_headers, 1, hdrs->all_headers_fp, fp);
+ pclose(fp);
+ }
+ }
+}
+#endif /* XP_UNIX */
+
+static void MimeHeaders_compact(MimeHeaders* hdrs) {
+ NS_ASSERTION(hdrs, "1.22 <rhp@netscape.com> 22 Aug 1999 08:48");
+ if (!hdrs) return;
+
+ PR_FREEIF(hdrs->obuffer);
+ hdrs->obuffer_fp = 0;
+ hdrs->obuffer_size = 0;
+
+ /* These really shouldn't have gotten out of whack again. */
+ NS_ASSERTION(hdrs->all_headers_fp <= hdrs->all_headers_size &&
+ hdrs->all_headers_fp + 100 > hdrs->all_headers_size,
+ "1.22 <rhp@netscape.com> 22 Aug 1999 08:48");
+}
+
+/* Writes the headers as text/plain.
+ This writes out a blank line after the headers, unless
+ dont_write_content_type is true, in which case the header-block
+ is not closed off, and none of the Content- headers are written.
+ */
+int MimeHeaders_write_raw_headers(MimeHeaders* hdrs, MimeDisplayOptions* opt,
+ bool dont_write_content_type) {
+ int status;
+
+ if (hdrs && !hdrs->done_p) {
+ hdrs->done_p = true;
+ status = MimeHeaders_build_heads_list(hdrs);
+ if (status < 0) return 0;
+ }
+
+ if (!dont_write_content_type) {
+ char nl[] = MSG_LINEBREAK;
+ if (hdrs) {
+ status =
+ MimeHeaders_write(hdrs, opt, hdrs->all_headers, hdrs->all_headers_fp);
+ if (status < 0) return status;
+ }
+ status = MimeHeaders_write(hdrs, opt, nl, strlen(nl));
+ if (status < 0) return status;
+ } else if (hdrs) {
+ int32_t i;
+ for (i = 0; i < hdrs->heads_size; i++) {
+ char* head = hdrs->heads[i];
+ char* end =
+ (i == hdrs->heads_size - 1 ? hdrs->all_headers + hdrs->all_headers_fp
+ : hdrs->heads[i + 1]);
+
+ NS_ASSERTION(head, "1.22 <rhp@netscape.com> 22 Aug 1999 08:48");
+ if (!head) continue;
+
+ /* Don't write out any Content- header. */
+ if (!PL_strncasecmp(head, "Content-", 8)) continue;
+
+ /* Write out this (possibly multi-line) header. */
+ status = MimeHeaders_write(hdrs, opt, head, end - head);
+ if (status < 0) return status;
+ }
+ }
+
+ if (hdrs) MimeHeaders_compact(hdrs);
+
+ return 0;
+}
+
+// XXX Fix this XXX //
+char* MimeHeaders_open_crypto_stamp(void) { return nullptr; }
+
+char* MimeHeaders_finish_open_crypto_stamp(void) { return nullptr; }
+
+char* MimeHeaders_close_crypto_stamp(void) { return nullptr; }
+
+char* MimeHeaders_make_crypto_stamp(bool encrypted_p, bool signed_p,
+ bool good_p, bool unverified_p,
+ bool close_parent_stamp_p,
+ const char* stamp_url) {
+ return nullptr;
+}
diff --git a/comm/mailnews/mime/src/mimehdrs.h b/comm/mailnews/mime/src/mimehdrs.h
new file mode 100644
index 0000000000..028092246a
--- /dev/null
+++ b/comm/mailnews/mime/src/mimehdrs.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEHDRS_H_
+#define _MIMEHDRS_H_
+
+#include "modlmime.h"
+
+/* This file defines the interface to message-header parsing and formatting
+ code, including conversion to HTML. */
+
+/* Other structs defined later in this file.
+ */
+
+/* Creation and destruction.
+ */
+extern MimeHeaders* MimeHeaders_new(void);
+// extern void MimeHeaders_free (MimeHeaders *);
+// extern MimeHeaders *MimeHeaders_copy (MimeHeaders *);
+
+/* Feed this method the raw data from which you would like a header
+ block to be parsed, one line at a time. Feed it a blank line when
+ you're done. Returns negative on allocation-related failure.
+ */
+extern int MimeHeaders_parse_line(const char* buffer, int32_t size,
+ MimeHeaders* hdrs);
+
+/* Converts a MimeHeaders object into HTML, by writing to the provided
+ output function.
+ */
+extern int MimeHeaders_write_headers_html(MimeHeaders* hdrs,
+ MimeDisplayOptions* opt,
+ bool attachment);
+
+/*
+ * Writes all headers to the mime emitter.
+ */
+extern int MimeHeaders_write_all_headers(MimeHeaders*, MimeDisplayOptions*,
+ bool);
+
+/* Writes the headers as text/plain.
+ This writes out a blank line after the headers, unless
+ dont_write_content_type is true, in which case the header-block
+ is not closed off, and none of the Content- headers are written.
+ */
+extern int MimeHeaders_write_raw_headers(MimeHeaders* hdrs,
+ MimeDisplayOptions* opt,
+ bool dont_write_content_type);
+
+/* Some crypto-related HTML-generated utility routines.
+ * XXX This may not be needed. XXX
+ */
+extern char* MimeHeaders_open_crypto_stamp(void);
+extern char* MimeHeaders_finish_open_crypto_stamp(void);
+extern char* MimeHeaders_close_crypto_stamp(void);
+extern char* MimeHeaders_make_crypto_stamp(bool encrypted_p,
+
+ bool signed_p,
+
+ bool good_p,
+
+ bool unverified_p,
+
+ bool close_parent_stamp_p,
+
+ const char* stamp_url);
+
+/* Does all the heuristic silliness to find the filename in the given headers.
+ */
+extern char* MimeHeaders_get_name(MimeHeaders* hdrs, MimeDisplayOptions* opt);
+
+extern char* mime_decode_filename(const char* name, const char* charset,
+ MimeDisplayOptions* opt);
+
+extern "C" char* MIME_StripContinuations(char* original);
+
+/**
+ * Convert this value to a unicode string, based on the charset.
+ */
+extern void MimeHeaders_convert_header_value(MimeDisplayOptions* opt,
+ nsCString& value,
+ bool convert_charset_only);
+#endif /* _MIMEHDRS_H_ */
diff --git a/comm/mailnews/mime/src/mimei.cpp b/comm/mailnews/mime/src/mimei.cpp
new file mode 100644
index 0000000000..3001189f8e
--- /dev/null
+++ b/comm/mailnews/mime/src/mimei.cpp
@@ -0,0 +1,1716 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+// clang-format off
+#include "nsCOMPtr.h"
+#include "mimeobj.h" /* MimeObject (abstract) */
+#include "mimecont.h" /* |--- MimeContainer (abstract) */
+#include "mimemult.h" /* | |--- MimeMultipart (abstract) */
+#include "mimemmix.h" /* | | |--- MimeMultipartMixed */
+#include "mimemdig.h" /* | | |--- MimeMultipartDigest */
+#include "mimempar.h" /* | | |--- MimeMultipartParallel */
+#include "mimemalt.h" /* | | |--- MimeMultipartAlternative */
+#include "mimemrel.h" /* | | |--- MimeMultipartRelated */
+#include "mimemapl.h" /* | | |--- MimeMultipartAppleDouble */
+#include "mimesun.h" /* | | |--- MimeSunAttachment */
+#include "mimemsig.h" /* | | |--- MimeMultipartSigned (abstract)*/
+#ifdef ENABLE_SMIME
+#include "mimemcms.h" /* | | |---MimeMultipartSignedCMS */
+#endif
+#include "mimecryp.h" /* | |--- MimeEncrypted (abstract) */
+#ifdef ENABLE_SMIME
+#include "mimecms.h" /* | | |--- MimeEncryptedPKCS7 */
+#endif
+#include "mimemsg.h" /* | |--- MimeMessage */
+#include "mimeunty.h" /* | |--- MimeUntypedText */
+#include "mimeleaf.h" /* |--- MimeLeaf (abstract) */
+#include "mimetext.h" /* | |--- MimeInlineText (abstract) */
+#include "mimetpla.h" /* | | |--- MimeInlineTextPlain */
+#include "mimethpl.h" /* | | | |--- M.I.TextHTMLAsPlaintext */
+#include "mimetpfl.h" /* | | |--- MimeInlineTextPlainFlowed */
+#include "mimethtm.h" /* | | |--- MimeInlineTextHTML */
+#include "mimethsa.h" /* | | | |--- M.I.TextHTMLSanitized */
+#include "mimeTextHTMLParsed.h" /*| | |--- M.I.TextHTMLParsed */
+#include "mimetric.h" /* | | |--- MimeInlineTextRichtext */
+#include "mimetenr.h" /* | | | |--- MimeInlineTextEnriched */
+/* SUPPORTED VIA PLUGIN | | |--- MimeInlineTextVCard */
+#include "mimeiimg.h" /* | |--- MimeInlineImage */
+#include "mimeeobj.h" /* | |--- MimeExternalObject */
+#include "mimeebod.h" /* |--- MimeExternalBody */
+ /* If you add classes here,also add them to mimei.h */
+// clang-format on
+
+#include "prlog.h"
+#include "prmem.h"
+#include "prenv.h"
+#include "plstr.h"
+#include "prlink.h"
+#include "prprf.h"
+#include "mimecth.h"
+#include "mimebuf.h"
+#include "mimemoz2.h"
+#include "nsIMimeContentTypeHandler.h"
+#include "nsICategoryManager.h"
+#include "nsCategoryManagerUtils.h"
+#include "nsXPCOMCID.h"
+#include "nsISimpleMimeConverter.h"
+#include "nsSimpleMimeConverterStub.h"
+#include "nsTArray.h"
+#include "nsMimeStringResources.h"
+#include "nsMimeTypes.h"
+#include "nsMsgUtils.h"
+#include "nsIPrefBranch.h"
+#include "mozilla/Preferences.h"
+#include "imgLoader.h"
+
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgHdr.h"
+#include "nsIMailChannel.h"
+
+using namespace mozilla;
+
+// forward declaration
+void getMsgHdrForCurrentURL(MimeDisplayOptions* opts, nsIMsgDBHdr** aMsgHdr);
+
+#define IMAP_EXTERNAL_CONTENT_HEADER "X-Mozilla-IMAP-Part"
+#define EXTERNAL_ATTACHMENT_URL_HEADER "X-Mozilla-External-Attachment-URL"
+
+/* ==========================================================================
+ Allocation and destruction
+ ==========================================================================
+ */
+static int mime_classinit(MimeObjectClass* clazz);
+
+/*
+ * These are the necessary defines/variables for doing
+ * content type handlers in external plugins.
+ */
+typedef struct {
+ char content_type[128];
+ bool force_inline_display;
+} cthandler_struct;
+
+nsTArray<cthandler_struct*>* ctHandlerList = nullptr;
+
+/*
+ * This will return TRUE if the content_type is found in the
+ * list, FALSE if it is not found.
+ */
+bool find_content_type_attribs(const char* content_type,
+ bool* force_inline_display) {
+ *force_inline_display = false;
+ if (!ctHandlerList) return false;
+
+ for (size_t i = 0; i < ctHandlerList->Length(); i++) {
+ cthandler_struct* ptr = ctHandlerList->ElementAt(i);
+ if (PL_strcasecmp(content_type, ptr->content_type) == 0) {
+ *force_inline_display = ptr->force_inline_display;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void add_content_type_attribs(const char* content_type,
+ contentTypeHandlerInitStruct* ctHandlerInfo) {
+ cthandler_struct* ptr = nullptr;
+ bool force_inline_display;
+
+ if (find_content_type_attribs(content_type, &force_inline_display)) return;
+
+ if (!content_type || !ctHandlerInfo) return;
+
+ if (!ctHandlerList) ctHandlerList = new nsTArray<cthandler_struct*>();
+
+ if (!ctHandlerList) return;
+
+ ptr = (cthandler_struct*)PR_MALLOC(sizeof(cthandler_struct));
+ if (!ptr) return;
+
+ PL_strncpy(ptr->content_type, content_type, sizeof(ptr->content_type));
+ ptr->force_inline_display = ctHandlerInfo->force_inline_display;
+ ctHandlerList->AppendElement(ptr);
+}
+
+/*
+ * This routine will find all content type handler for a specific content
+ * type (if it exists)
+ */
+bool force_inline_display(const char* content_type) {
+ bool force_inline_disp;
+
+ find_content_type_attribs(content_type, &force_inline_disp);
+ return force_inline_disp;
+}
+
+/*
+ * This routine will find all content type handler for a specific content
+ * type (if it exists) and is defined to the nsRegistry
+ */
+MimeObjectClass* mime_locate_external_content_handler(
+ const char* content_type, contentTypeHandlerInitStruct* ctHandlerInfo) {
+ if (!content_type || !*(content_type)) // null or empty content type
+ return nullptr;
+
+ MimeObjectClass* newObj = nullptr;
+ nsresult rv;
+
+ nsAutoCString lookupID("@mozilla.org/mimecth;1?type=");
+ nsAutoCString contentType;
+ ToLowerCase(nsDependentCString(content_type), contentType);
+ lookupID += contentType;
+
+ nsCOMPtr<nsIMimeContentTypeHandler> ctHandler =
+ do_CreateInstance(lookupID.get(), &rv);
+ if (NS_FAILED(rv) || !ctHandler) {
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return nullptr;
+
+ nsCString value;
+ rv = catman->GetCategoryEntry(NS_SIMPLEMIMECONVERTERS_CATEGORY, contentType,
+ value);
+ if (NS_FAILED(rv) || value.IsEmpty()) return nullptr;
+ rv = MIME_NewSimpleMimeConverterStub(contentType.get(),
+ getter_AddRefs(ctHandler));
+ if (NS_FAILED(rv) || !ctHandler) return nullptr;
+ }
+
+ rv = ctHandler->CreateContentTypeHandlerClass(contentType.get(),
+ ctHandlerInfo, &newObj);
+ if (NS_FAILED(rv)) return nullptr;
+
+ add_content_type_attribs(contentType.get(), ctHandlerInfo);
+ return newObj;
+}
+
+/* This is necessary to expose the MimeObject method outside of this DLL */
+int MIME_MimeObject_write(MimeObject* obj, const char* output, int32_t length,
+ bool user_visible_p) {
+ return MimeObject_write(obj, output, length, user_visible_p);
+}
+
+MimeObject* mime_new(MimeObjectClass* clazz, MimeHeaders* hdrs,
+ const char* override_content_type) {
+ int size = clazz->instance_size;
+ MimeObject* object;
+ int status;
+
+ /* Some assertions to verify that this isn't random junk memory... */
+ NS_ASSERTION(clazz->class_name && strlen(clazz->class_name) > 0,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ NS_ASSERTION(size > 0 && size < 1000,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ if (!clazz->class_initialized) {
+ status = mime_classinit(clazz);
+ if (status < 0) return 0;
+ }
+
+ NS_ASSERTION(clazz->initialize && clazz->finalize,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ if (hdrs) {
+ hdrs = MimeHeaders_copy(hdrs);
+ if (!hdrs) return 0;
+ }
+
+ object = (MimeObject*)PR_MALLOC(size);
+ if (!object) return 0;
+
+ memset(object, 0, size);
+ object->clazz = clazz;
+ object->headers = hdrs;
+ object->dontShowAsAttachment = false;
+
+ if (override_content_type && *override_content_type)
+ object->content_type = strdup(override_content_type);
+
+ status = clazz->initialize(object);
+ if (status < 0) {
+ clazz->finalize(object);
+ PR_Free(object);
+ return 0;
+ }
+
+ return object;
+}
+
+void mime_free(MimeObject* object) {
+#ifdef DEBUG__
+ int i, size = object->clazz->instance_size;
+ uint32_t* array = (uint32_t*)object;
+#endif /* DEBUG */
+
+ object->clazz->finalize(object);
+
+#ifdef DEBUG__
+ for (i = 0; i < (size / sizeof(*array)); i++) array[i] = (uint32_t)0xDEADBEEF;
+#endif /* DEBUG */
+
+ PR_Free(object);
+}
+
+bool mime_is_allowed_class(const MimeObjectClass* clazz,
+ int32_t types_of_classes_to_disallow) {
+ if (types_of_classes_to_disallow == 0) return true;
+ bool avoid_html = (types_of_classes_to_disallow >= 1);
+ bool avoid_images = (types_of_classes_to_disallow >= 2);
+ bool avoid_strange_content = (types_of_classes_to_disallow >= 3);
+ bool allow_only_vanilla_classes = (types_of_classes_to_disallow == 100);
+
+ if (allow_only_vanilla_classes)
+ /* A "safe" class is one that is unlikely to have security bugs or to
+ allow security exploits or one that is essential for the usefulness
+ of the application, even for paranoid users.
+ What's included here is more personal judgement than following
+ strict rules, though, unfortunately.
+ The function returns true only for known good classes, i.e. is a
+ "whitelist" in this case.
+ This idea comes from Georgi Guninski.
+ */
+ return (clazz == (MimeObjectClass*)&mimeInlineTextPlainClass ||
+ clazz == (MimeObjectClass*)&mimeInlineTextPlainFlowedClass ||
+ clazz == (MimeObjectClass*)&mimeInlineTextHTMLSanitizedClass ||
+ clazz == (MimeObjectClass*)&mimeInlineTextHTMLAsPlaintextClass ||
+ /* The latter 2 classes bear some risk, because they use the Gecko
+ HTML parser, but the user has the option to make an explicit
+ choice in this case, via html_as. */
+ clazz == (MimeObjectClass*)&mimeMultipartMixedClass ||
+ clazz == (MimeObjectClass*)&mimeMultipartAlternativeClass ||
+ clazz == (MimeObjectClass*)&mimeMultipartDigestClass ||
+ clazz == (MimeObjectClass*)&mimeMultipartAppleDoubleClass ||
+ clazz == (MimeObjectClass*)&mimeMessageClass ||
+ clazz == (MimeObjectClass*)&mimeExternalObjectClass ||
+ /* mimeUntypedTextClass? -- does uuencode */
+#ifdef ENABLE_SMIME
+ clazz == (MimeObjectClass*)&mimeMultipartSignedCMSClass ||
+ clazz == (MimeObjectClass*)&mimeEncryptedCMSClass ||
+#endif
+ clazz == 0);
+
+ /* Contrairy to above, the below code is a "blacklist", i.e. it
+ *excludes* some "bad" classes. */
+ return !(
+ (avoid_html && (clazz == (MimeObjectClass*)&mimeInlineTextHTMLParsedClass
+ /* Should not happen - we protect against that in
+ mime_find_class(). Still for safety... */
+ )) ||
+ (avoid_images && (clazz == (MimeObjectClass*)&mimeInlineImageClass)) ||
+ (avoid_strange_content &&
+ (clazz == (MimeObjectClass*)&mimeInlineTextEnrichedClass ||
+ clazz == (MimeObjectClass*)&mimeInlineTextRichtextClass ||
+ clazz == (MimeObjectClass*)&mimeSunAttachmentClass ||
+ clazz == (MimeObjectClass*)&mimeExternalBodyClass)));
+}
+
+void getMsgHdrForCurrentURL(MimeDisplayOptions* opts, nsIMsgDBHdr** aMsgHdr) {
+ *aMsgHdr = nullptr;
+
+ if (!opts) return;
+
+ mime_stream_data* msd = (mime_stream_data*)(opts->stream_closure);
+ if (!msd) return;
+
+ nsCOMPtr<nsIChannel> channel =
+ msd->channel; // note the lack of ref counting...
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIMsgMessageUrl> msgURI;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ msgURI = do_QueryInterface(uri);
+ if (msgURI) {
+ msgURI->GetMessageHeader(aMsgHdr);
+ if (*aMsgHdr) return;
+ nsCString rdfURI;
+ msgURI->GetUri(rdfURI);
+ if (!rdfURI.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgDBHdrFromURI(rdfURI, getter_AddRefs(msgHdr));
+ NS_IF_ADDREF(*aMsgHdr = msgHdr);
+ }
+ }
+ }
+ }
+
+ return;
+}
+
+MimeObjectClass* mime_find_class(const char* content_type, MimeHeaders* hdrs,
+ MimeDisplayOptions* opts, bool exact_match_p) {
+ MimeObjectClass* clazz = 0;
+ MimeObjectClass* tempClass = 0;
+ contentTypeHandlerInitStruct ctHandlerInfo;
+
+ // Read some prefs
+ nsIPrefBranch* prefBranch = GetPrefBranch(opts);
+ int32_t html_as = 0; // def. see below
+ int32_t types_of_classes_to_disallow = 0; /* Let only a few libmime classes
+ process incoming data. This protects from bugs (e.g. buffer overflows)
+ and from security loopholes (e.g. allowing unchecked HTML in some
+ obscure classes, although the user has html_as > 0).
+ This option is mainly for the UI of html_as.
+ 0 = allow all available classes
+ 1 = Use hardcoded blacklist to avoid rendering (incoming) HTML
+ 2 = ... and images
+ 3 = ... and some other uncommon content types
+ 4 = show all body parts
+ 100 = Use hardcoded whitelist to avoid even more bugs(buffer overflows).
+ This mode will limit the features available (e.g. uncommon
+ attachment types and inline images) and is for paranoid users.
+ */
+ if (opts && opts->format_out != nsMimeOutput::nsMimeMessageFilterSniffer &&
+ opts->format_out != nsMimeOutput::nsMimeMessageDecrypt &&
+ opts->format_out != nsMimeOutput::nsMimeMessageAttach)
+ if (prefBranch) {
+ prefBranch->GetIntPref("mailnews.display.html_as", &html_as);
+ prefBranch->GetIntPref("mailnews.display.disallow_mime_handlers",
+ &types_of_classes_to_disallow);
+ if (types_of_classes_to_disallow > 0 && html_as == 0)
+ // We have non-sensical prefs. Do some fixup.
+ html_as = 1;
+ }
+
+ // First, check to see if the message has been marked as JUNK. If it has,
+ // then force the message to be rendered as simple, unless this has been
+ // called by a filtering routine.
+ bool sanitizeJunkMail = false;
+
+ // it is faster to read the pref first then figure out the msg hdr for the
+ // current url only if we have to
+ // XXX instead of reading this pref every time, part of mime should be an
+ // observer listening to this pref change and updating internal state
+ // accordingly. But none of the other prefs in this file seem to be doing
+ // that...=(
+ if (prefBranch)
+ prefBranch->GetBoolPref("mail.spam.display.sanitize", &sanitizeJunkMail);
+
+ if (sanitizeJunkMail &&
+ !(opts && opts->format_out == nsMimeOutput::nsMimeMessageFilterSniffer)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ getMsgHdrForCurrentURL(opts, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsCString junkScoreStr;
+ (void)msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (html_as == 0 && junkScoreStr.get() && atoi(junkScoreStr.get()) > 50)
+ html_as = 3; // 3 == Simple HTML
+ } // if msgHdr
+ } // if we are supposed to sanitize junk mail
+
+ /*
+ * What we do first is check for an external content handler plugin.
+ * This will actually extend the mime handling by calling a routine
+ * which will allow us to load an external content type handler
+ * for specific content types. If one is not found, we will drop back
+ * to the default handler.
+ */
+ if ((tempClass = mime_locate_external_content_handler(
+ content_type, &ctHandlerInfo)) != nullptr) {
+#ifdef MOZ_THUNDERBIRD
+ // This is a case where we only want to add this property if we are a
+ // thunderbird build AND we have found an external mime content handler for
+ // text/calendar This will enable iMIP support in Lightning
+ if (hdrs && (!PL_strncasecmp(content_type, "text/calendar", 13))) {
+ char* full_content_type =
+ MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (full_content_type) {
+ char* imip_method = MimeHeaders_get_parameter(
+ full_content_type, "method", nullptr, nullptr);
+
+ mime_stream_data* msd = (mime_stream_data*)(opts->stream_closure);
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(msd->channel);
+ if (mailChannel) {
+ mailChannel->SetImipMethod(
+ nsCString(imip_method ? imip_method : "nomethod"));
+ }
+
+ // PR_Free checks for null
+ PR_Free(imip_method);
+ PR_Free(full_content_type);
+ }
+ }
+#endif
+
+ if (types_of_classes_to_disallow > 0 &&
+ (!PL_strncasecmp(content_type, "text/vcard", 10) ||
+ !PL_strncasecmp(content_type, "text/x-vcard", 12)))
+ /* Use a little hack to prevent some dangerous plugins, which ship
+ with Mozilla, to run.
+ For the truly user-installed plugins, we rely on the judgement
+ of the user. */
+ {
+ if (!exact_match_p)
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass; // As attachment
+ } else
+ clazz = (MimeObjectClass*)tempClass;
+ } else {
+ if (!content_type || !*content_type ||
+ !PL_strcasecmp(content_type, "text")) /* with no / in the type */
+ clazz = (MimeObjectClass*)&mimeUntypedTextClass;
+
+ /* Subtypes of text...
+ */
+ else if (!PL_strncasecmp(content_type, "text/", 5)) {
+ if (!PL_strcasecmp(content_type + 5, "html")) {
+ if (opts &&
+ (opts->format_out == nsMimeOutput::nsMimeMessageSaveAs ||
+ opts->format_out == nsMimeOutput::nsMimeMessageFilterSniffer ||
+ opts->format_out == nsMimeOutput::nsMimeMessageDecrypt ||
+ opts->format_out == nsMimeOutput::nsMimeMessageAttach))
+ // SaveAs in new modes doesn't work yet.
+ {
+ // Don't use the parsed HTML class if we're ...
+ // - saving the HTML of a message
+ // - getting message content for filtering
+ // - snarfing attachments (nsMimeMessageDecrypt used in
+ // SnarfMsgAttachment)
+ // - processing attachments (like deleting attachments).
+ clazz = (MimeObjectClass*)&mimeInlineTextHTMLClass;
+ types_of_classes_to_disallow = 0;
+ } else if (html_as == 0 || html_as == 4) // Render sender's HTML
+ clazz = (MimeObjectClass*)&mimeInlineTextHTMLParsedClass;
+ else if (html_as == 1) // convert HTML to plaintext
+ // Do a HTML->TXT->HTML conversion, see mimethpl.h.
+ clazz = (MimeObjectClass*)&mimeInlineTextHTMLAsPlaintextClass;
+ else if (html_as == 2) // display HTML source
+ /* This is for the freaks. Treat HTML as plaintext,
+ which will cause the HTML source to be displayed.
+ Not very user-friendly, but some seem to want this. */
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainClass;
+ else if (html_as == 3) // Sanitize
+ // Strip all but allowed HTML
+ clazz = (MimeObjectClass*)&mimeInlineTextHTMLSanitizedClass;
+ else // Goofy pref
+ /* User has an unknown pref value. Maybe he used a newer Mozilla
+ with a new alternative to avoid HTML. Defaulting to option 1,
+ which is less dangerous than defaulting to the raw HTML. */
+ clazz = (MimeObjectClass*)&mimeInlineTextHTMLAsPlaintextClass;
+ } else if (!PL_strcasecmp(content_type + 5, "enriched"))
+ clazz = (MimeObjectClass*)&mimeInlineTextEnrichedClass;
+ else if (!PL_strcasecmp(content_type + 5, "richtext"))
+ clazz = (MimeObjectClass*)&mimeInlineTextRichtextClass;
+ else if (!PL_strcasecmp(content_type + 5, "rtf"))
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ else if (!PL_strcasecmp(content_type + 5, "plain")) {
+ // Preliminary use the normal plain text
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainClass;
+
+ if (opts &&
+ opts->format_out != nsMimeOutput::nsMimeMessageFilterSniffer &&
+ opts->format_out != nsMimeOutput::nsMimeMessageAttach &&
+ opts->format_out != nsMimeOutput::nsMimeMessageRaw) {
+ bool disable_format_flowed = false;
+ if (prefBranch)
+ prefBranch->GetBoolPref(
+ "mailnews.display.disable_format_flowed_support",
+ &disable_format_flowed);
+
+ if (!disable_format_flowed) {
+ // Check for format=flowed, damn, it is already stripped away from
+ // the contenttype!
+ // Look in headers instead even though it's expensive and clumsy
+ // First find Content-Type:
+ char* content_type_row =
+ hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false)
+ : 0;
+ // Then the format parameter if there is one.
+ // I would rather use a PARAM_FORMAT but I can't find the right
+ // place to put the define. The others seems to be in net.h
+ // but is that really really the right place? There is also
+ // a nsMimeTypes.h but that one isn't included. Bug?
+ char* content_type_format =
+ content_type_row
+ ? MimeHeaders_get_parameter(content_type_row, "format",
+ nullptr, nullptr)
+ : 0;
+
+ if (content_type_format &&
+ !PL_strcasecmp(content_type_format, "flowed"))
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainFlowedClass;
+ PR_FREEIF(content_type_format);
+ PR_FREEIF(content_type_row);
+ }
+ }
+ } else if (!exact_match_p)
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainClass;
+ }
+
+ /* Subtypes of multipart...
+ */
+ else if (!PL_strncasecmp(content_type, "multipart/", 10)) {
+ // When html_as is 4, we want all MIME parts of the message to
+ // show up in the displayed message body, if they are MIME types
+ // that we know how to display, and also in the attachment pane
+ // if it's appropriate to put them there. Both
+ // multipart/alternative and multipart/related play games with
+ // hiding various MIME parts, and we don't want that to happen,
+ // so we prevent that by parsing those MIME types as
+ // multipart/mixed, which won't mess with anything.
+ //
+ // When our output format is nsMimeOutput::nsMimeMessageAttach,
+ // i.e., we are reformatting the message to remove attachments,
+ // we are in a similar boat. The code for deleting
+ // attachments properly in that mode is in mimemult.cpp
+ // functions which are inherited by mimeMultipartMixedClass but
+ // not by mimeMultipartAlternativeClass or
+ // mimeMultipartRelatedClass. Therefore, to ensure that
+ // everything is handled properly, in this context too we parse
+ // those MIME types as multipart/mixed.
+ bool basic_formatting =
+ (html_as == 4) ||
+ (opts && opts->format_out == nsMimeOutput::nsMimeMessageAttach);
+ if (!PL_strcasecmp(content_type + 10, "alternative"))
+ clazz = basic_formatting
+ ? (MimeObjectClass*)&mimeMultipartMixedClass
+ : (MimeObjectClass*)&mimeMultipartAlternativeClass;
+ else if (!PL_strcasecmp(content_type + 10, "related"))
+ clazz = basic_formatting ? (MimeObjectClass*)&mimeMultipartMixedClass
+ : (MimeObjectClass*)&mimeMultipartRelatedClass;
+ else if (!PL_strcasecmp(content_type + 10, "digest"))
+ clazz = (MimeObjectClass*)&mimeMultipartDigestClass;
+ else if (!PL_strcasecmp(content_type + 10, "appledouble") ||
+ !PL_strcasecmp(content_type + 10, "header-set"))
+ clazz = (MimeObjectClass*)&mimeMultipartAppleDoubleClass;
+ else if (!PL_strcasecmp(content_type + 10, "parallel"))
+ clazz = (MimeObjectClass*)&mimeMultipartParallelClass;
+ else if (!PL_strcasecmp(content_type + 10, "mixed"))
+ clazz = (MimeObjectClass*)&mimeMultipartMixedClass;
+#ifdef ENABLE_SMIME
+ else if (!PL_strcasecmp(content_type + 10, "signed")) {
+ /* Check that the "protocol" and "micalg" parameters are ones we
+ know about. */
+ char* ct =
+ hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false) : 0;
+ char* proto =
+ ct ? MimeHeaders_get_parameter(ct, PARAM_PROTOCOL, nullptr, nullptr)
+ : 0;
+ char* micalg =
+ ct ? MimeHeaders_get_parameter(ct, PARAM_MICALG, nullptr, nullptr)
+ : 0;
+
+ if (proto && ((/* is a signature */
+ !PL_strcasecmp(proto, APPLICATION_XPKCS7_SIGNATURE) ||
+ !PL_strcasecmp(proto, APPLICATION_PKCS7_SIGNATURE)) &&
+ micalg &&
+ (!PL_strcasecmp(micalg, PARAM_MICALG_MD5) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_MD5_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_3) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_4) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_5) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA256) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA256_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA256_3) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA384) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA384_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA384_3) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA512) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA512_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA512_3) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_MD2))))
+ clazz = (MimeObjectClass*)&mimeMultipartSignedCMSClass;
+ else
+ clazz = 0;
+
+ PR_FREEIF(proto);
+ PR_FREEIF(micalg);
+ PR_FREEIF(ct);
+ }
+#endif
+
+ if (!clazz && !exact_match_p)
+ /* Treat all unknown multipart subtypes as "multipart/mixed" */
+ clazz = (MimeObjectClass*)&mimeMultipartMixedClass;
+
+ /* If we are sniffing a message, let's treat alternative parts as mixed */
+ if (opts && opts->format_out == nsMimeOutput::nsMimeMessageFilterSniffer)
+ if (clazz == (MimeObjectClass*)&mimeMultipartAlternativeClass)
+ clazz = (MimeObjectClass*)&mimeMultipartMixedClass;
+ }
+
+ /* Subtypes of message...
+ */
+ else if (!PL_strncasecmp(content_type, "message/", 8)) {
+ if (!PL_strcasecmp(content_type + 8, "rfc822") ||
+ !PL_strcasecmp(content_type + 8, "news"))
+ clazz = (MimeObjectClass*)&mimeMessageClass;
+ else if (!PL_strcasecmp(content_type + 8, "external-body"))
+ clazz = (MimeObjectClass*)&mimeExternalBodyClass;
+ else if (!PL_strcasecmp(content_type + 8, "partial"))
+ /* I guess these are most useful as externals, for now... */
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ else if (!exact_match_p)
+ /* Treat all unknown message subtypes as "text/plain" */
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainClass;
+ }
+
+ /* The magic image types which we are able to display internally...
+ */
+ else if (!PL_strncasecmp(content_type, "image/", 6)) {
+ if (imgLoader::SupportImageWithMimeType(
+ nsDependentCString(content_type),
+ AcceptedMimeTypes::IMAGES_AND_DOCUMENTS))
+ clazz = (MimeObjectClass*)&mimeInlineImageClass;
+ else
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ }
+#ifdef ENABLE_SMIME
+ else if (!PL_strcasecmp(content_type, APPLICATION_XPKCS7_MIME) ||
+ !PL_strcasecmp(content_type, APPLICATION_PKCS7_MIME)) {
+
+ if (opts->is_child) {
+ // We do not allow encrypted parts except as top level.
+ // Allowing them would leak the plain text in case the part is
+ // cleverly hidden and the decrypted content gets included in
+ // replies and forwards.
+ clazz = (MimeObjectClass*)&mimeSuppressedCryptoClass;
+ } else {
+ char* ct =
+ hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false)
+ : nullptr;
+ char* st =
+ ct ? MimeHeaders_get_parameter(ct, "smime-type", nullptr, nullptr)
+ : nullptr;
+
+ /* by default, assume that it is an encrypted message */
+ clazz = (MimeObjectClass*)&mimeEncryptedCMSClass;
+
+ /* if the smime-type parameter says that it's a certs-only or
+ compressed file, then show it as an attachment, however
+ (MimeEncryptedCMS doesn't handle these correctly) */
+ if (st && (!PL_strcasecmp(st, "certs-only") ||
+ !PL_strcasecmp(st, "compressed-data")))
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ else {
+ /* look at the file extension... less reliable, but still covered
+ by the S/MIME specification (RFC 3851, section 3.2.1) */
+ char* name = (hdrs ? MimeHeaders_get_name(hdrs, opts) : nullptr);
+ if (name) {
+ char* suf = PL_strrchr(name, '.');
+ bool p7mExternal = false;
+
+ if (prefBranch)
+ prefBranch->GetBoolPref("mailnews.p7m_external", &p7mExternal);
+ if (suf &&
+ ((!PL_strcasecmp(suf, ".p7m") && p7mExternal) ||
+ !PL_strcasecmp(suf, ".p7c") || !PL_strcasecmp(suf, ".p7z")))
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ }
+ PR_Free(name);
+ }
+ PR_Free(st);
+ PR_Free(ct);
+ }
+ }
+#endif
+ /* A few types which occur in the real world and which we would otherwise
+ treat as non-text types (which would be bad) without this special-case...
+ */
+ else if (!PL_strcasecmp(content_type, APPLICATION_PGP) ||
+ !PL_strcasecmp(content_type, APPLICATION_PGP2))
+ clazz = (MimeObjectClass*)&mimeInlineTextPlainClass;
+
+ else if (!PL_strcasecmp(content_type, SUN_ATTACHMENT))
+ clazz = (MimeObjectClass*)&mimeSunAttachmentClass;
+
+ /* Everything else gets represented as a clickable link.
+ */
+ else if (!exact_match_p)
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+
+ if (!mime_is_allowed_class(clazz, types_of_classes_to_disallow)) {
+ /* Do that check here (not after the if block), because we want to allow
+ user-installed plugins. */
+ if (!exact_match_p)
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ else
+ clazz = 0;
+ }
+ }
+
+#ifdef ENABLE_SMIME
+ // see bug #189988
+ if (opts && opts->format_out == nsMimeOutput::nsMimeMessageDecrypt &&
+ (clazz != (MimeObjectClass*)&mimeEncryptedCMSClass)) {
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ }
+#endif
+
+ if (!exact_match_p)
+ NS_ASSERTION(clazz, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!clazz) return 0;
+
+ NS_ASSERTION(clazz, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ if (clazz && !clazz->class_initialized) {
+ int status = mime_classinit(clazz);
+ if (status < 0) return 0;
+ }
+
+ return clazz;
+}
+
+MimeObject* mime_create(const char* content_type, MimeHeaders* hdrs,
+ MimeDisplayOptions* opts,
+ bool forceInline /* = false */) {
+ /* If there is no Content-Disposition header, or if the Content-Disposition
+ is ``inline'', then we display the part inline (and let mime_find_class()
+ decide how.)
+
+ If there is any other Content-Disposition (either ``attachment'' or some
+ disposition that we don't recognise) then we always display the part as
+ an external link, by using MimeExternalObject to display it.
+
+ But Content-Disposition is ignored for all containers except `message'.
+ (including multipart/mixed, and multipart/digest.) It's not clear if
+ this is to spec, but from a usability standpoint, I think it's necessary.
+ */
+
+ MimeObjectClass* clazz = 0;
+ char* content_disposition = 0;
+ MimeObject* obj = 0;
+ char* override_content_type = 0;
+
+ /* We've had issues where the incoming content_type is invalid, of a format:
+ content_type="=?windows-1252?q?application/pdf" (bug 659355)
+ We decided to fix that by simply trimming the stuff before the ?
+ */
+ if (content_type) {
+ const char* lastQuestion = strrchr(content_type, '?');
+ if (lastQuestion)
+ content_type = lastQuestion + 1; // the substring after the last '?'
+ }
+
+ /* There are some clients send out all attachments with a content-type
+ of application/octet-stream. So, if we have an octet-stream attachment,
+ try to guess what type it really is based on the file extension. I HATE
+ that we have to do this...
+ */
+ if (hdrs && opts && opts->file_type_fn &&
+
+ /* ### mwelch - don't override AppleSingle */
+ (content_type ? PL_strcasecmp(content_type, APPLICATION_APPLEFILE)
+ : true) &&
+ /* ## davidm Apple double shouldn't use this #$%& either. */
+ (content_type ? PL_strcasecmp(content_type, MULTIPART_APPLEDOUBLE)
+ : true) &&
+ (!content_type ||
+ !PL_strcasecmp(content_type, APPLICATION_OCTET_STREAM) ||
+ !PL_strcasecmp(content_type, UNKNOWN_CONTENT_TYPE))) {
+ char* name = MimeHeaders_get_name(hdrs, opts);
+ if (name) {
+ override_content_type = opts->file_type_fn(name, opts->stream_closure);
+ // appledouble isn't a valid override content type, and makes
+ // attachments invisible.
+ if (!PL_strcasecmp(override_content_type, MULTIPART_APPLEDOUBLE))
+ override_content_type = nullptr;
+ PR_FREEIF(name);
+
+ // Workaround for saving '.eml" file encoded with base64.
+ // Do not override with message/rfc822 whenever Transfer-Encoding is
+ // base64 since base64 encoding of message/rfc822 is invalid.
+ // Our MimeMessageClass has no capability to decode it.
+ if (!PL_strcasecmp(override_content_type, MESSAGE_RFC822)) {
+ nsCString encoding;
+ encoding.Adopt(MimeHeaders_get(hdrs, HEADER_CONTENT_TRANSFER_ENCODING,
+ true, false));
+ if (encoding.LowerCaseEqualsLiteral(ENCODING_BASE64))
+ override_content_type = nullptr;
+ }
+
+ // If we get here and it is not the unknown content type from the
+ // file name, let's do some better checking not to inline something bad
+ if (override_content_type && *override_content_type &&
+ (PL_strcasecmp(override_content_type, UNKNOWN_CONTENT_TYPE)))
+ content_type = override_content_type;
+ }
+ }
+
+ clazz = mime_find_class(content_type, hdrs, opts, false);
+
+ NS_ASSERTION(clazz, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!clazz) goto FAIL;
+
+ if (opts && opts->part_to_load)
+ /* Always ignore Content-Disposition when we're loading some specific
+ sub-part (which may be within some container that we wouldn't otherwise
+ descend into, if the container itself had a Content-Disposition of
+ `attachment'. */
+ content_disposition = 0;
+
+ else if (mime_subclass_p(clazz, (MimeObjectClass*)&mimeContainerClass) &&
+ !mime_subclass_p(clazz, (MimeObjectClass*)&mimeMessageClass))
+ /* Ignore Content-Disposition on all containers except `message'.
+ That is, Content-Disposition is ignored for multipart/mixed objects,
+ but is obeyed for message/rfc822 objects. */
+ content_disposition = 0;
+
+ else {
+ /* Check to see if the plugin should override the content disposition
+ to make it appear inline. One example is a vcard which has a content
+ disposition of an "attachment;" */
+ if (force_inline_display(content_type))
+ NS_MsgSACopy(&content_disposition, "inline");
+ else
+ content_disposition =
+ hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_DISPOSITION, true, false)
+ : 0;
+ }
+
+ if (!content_disposition || !PL_strcasecmp(content_disposition, "inline"))
+ ; /* Use the class we've got. */
+ else {
+ // override messages that have content disposition set to "attachment"
+ // even though we probably should show them inline.
+ if ((clazz != (MimeObjectClass*)&mimeMessageClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineImageClass) &&
+ (!opts->show_attachment_inline_text ||
+ ((clazz != (MimeObjectClass*)&mimeInlineTextHTMLClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextHTMLParsedClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextHTMLSanitizedClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextHTMLAsPlaintextClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextRichtextClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextEnrichedClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextPlainClass) &&
+ (clazz != (MimeObjectClass*)&mimeInlineTextPlainFlowedClass)))) {
+ // not a special inline type, so show as attachment
+ // However, mimeSuppressedCryptoClass is treated identically as
+ // mimeExternalObjectClass, let's not lose that type information.
+ if (clazz != (MimeObjectClass*)&mimeSuppressedCryptoClass) {
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ }
+ }
+ }
+
+ /* If the option `Show Attachments Inline' is off, now would be the time to
+ * change our mind... */
+ /* Also, if we're doing a reply (i.e. quoting the body), then treat that
+ * according to preference. */
+ if (opts &&
+ ((!opts->show_attachment_inline_p && !forceInline) ||
+ (!opts->quote_attachment_inline_p &&
+ (opts->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ opts->format_out == nsMimeOutput::nsMimeMessageBodyQuoting)))) {
+ if (mime_subclass_p(clazz, (MimeObjectClass*)&mimeInlineTextClass)) {
+ /* It's a text type. Write it only if it's the *first* part
+ that we're writing, and then only if it has no "filename"
+ specified (the assumption here being, if it has a filename,
+ it wasn't simply typed into the text field -- it was actually
+ an attached document.) */
+ if (opts->state && opts->state->first_part_written_p)
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ else {
+ /* If there's a name, then write this as an attachment. */
+ char* name = (hdrs ? MimeHeaders_get_name(hdrs, opts) : nullptr);
+ if (name) {
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ PR_Free(name);
+ }
+ }
+ } else if (mime_subclass_p(clazz, (MimeObjectClass*)&mimeContainerClass) &&
+ !mime_subclass_p(clazz, (MimeObjectClass*)&mimeMessageClass))
+ /* Multipart subtypes are ok, except for messages; descend into
+ multiparts, and defer judgement.
+
+ Encrypted blobs are just like other containers (make the crypto
+ layer invisible, and treat them as simple containers. So there's
+ no easy way to save encrypted data directly to disk; it will tend
+ to always be wrapped inside a message/rfc822. That's ok.) */
+ ;
+ else if (opts && opts->part_to_load &&
+ mime_subclass_p(clazz, (MimeObjectClass*)&mimeMessageClass))
+ /* Descend into messages only if we're looking for a specific sub-part. */
+ ;
+ else {
+ /* Anything else, and display it as a link (and cause subsequent
+ text parts to also be displayed as links.) */
+ clazz = (MimeObjectClass*)&mimeExternalObjectClass;
+ }
+ }
+
+ PR_FREEIF(content_disposition);
+ obj = mime_new(clazz, hdrs, content_type);
+
+FAIL:
+
+ /* If we decided to ignore the content-type in the headers of this object
+ (see above) then make sure that our new content-type is stored in the
+ object itself. (Or free it, if we're in an out-of-memory situation.)
+ */
+ if (override_content_type) {
+ if (obj) {
+ PR_FREEIF(obj->content_type);
+ obj->content_type = override_content_type;
+ } else {
+ PR_Free(override_content_type);
+ }
+ }
+
+ return obj;
+}
+
+static int mime_classinit_1(MimeObjectClass* clazz, MimeObjectClass* target);
+
+static int mime_classinit(MimeObjectClass* clazz) {
+ int status;
+ if (clazz->class_initialized) return 0;
+
+ NS_ASSERTION(clazz->class_initialize,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (!clazz->class_initialize) return -1;
+
+ /* First initialize the superclass.
+ */
+ if (clazz->superclass && !clazz->superclass->class_initialized) {
+ status = mime_classinit(clazz->superclass);
+ if (status < 0) return status;
+ }
+
+ /* Now run each of the superclass-init procedures in turn,
+ parentmost-first. */
+ status = mime_classinit_1(clazz, clazz);
+ if (status < 0) return status;
+
+ /* Now we're done. */
+ clazz->class_initialized = true;
+ return 0;
+}
+
+static int mime_classinit_1(MimeObjectClass* clazz, MimeObjectClass* target) {
+ int status;
+ if (clazz->superclass) {
+ status = mime_classinit_1(clazz->superclass, target);
+ if (status < 0) return status;
+ }
+ return clazz->class_initialize(target);
+}
+
+bool mime_subclass_p(MimeObjectClass* child, MimeObjectClass* parent) {
+ if (child == parent) return true;
+ if (!child->superclass) return false;
+ return mime_subclass_p(child->superclass, parent);
+}
+
+bool mime_typep(MimeObject* obj, MimeObjectClass* clazz) {
+ return mime_subclass_p(obj->clazz, clazz);
+}
+
+/* URL munging
+ */
+
+/* Returns a string describing the location of the part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ */
+char* mime_part_address(MimeObject* obj) {
+ if (!obj->parent) return strdup("0");
+
+ /* Find this object in its parent. */
+ int32_t i, j = -1;
+ char buf[20];
+ char* higher = 0;
+ MimeContainer* cont = (MimeContainer*)obj->parent;
+ NS_ASSERTION(mime_typep(obj->parent, (MimeObjectClass*)&mimeContainerClass),
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ for (i = 0; i < cont->nchildren; i++)
+ if (cont->children[i] == obj) {
+ j = i + 1;
+ break;
+ }
+ if (j == -1) {
+ NS_ERROR("No children under MeimContainer");
+ return 0;
+ }
+
+ PR_snprintf(buf, sizeof(buf), "%ld", j);
+ if (obj->parent->parent) {
+ higher = mime_part_address(obj->parent);
+ if (!higher) return 0; /* MIME_OUT_OF_MEMORY */
+ }
+
+ if (!higher) return strdup(buf);
+
+ uint32_t slen = strlen(higher) + strlen(buf) + 3;
+ char* s = (char*)PR_MALLOC(slen);
+ if (!s) {
+ PR_Free(higher);
+ return 0; /* MIME_OUT_OF_MEMORY */
+ }
+ PL_strncpyz(s, higher, slen);
+ PL_strcatn(s, slen, ".");
+ PL_strcatn(s, slen, buf);
+ PR_Free(higher);
+ return s;
+}
+
+/* Returns a string describing the location of the *IMAP* part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ This part is explicitly passed in the X-Mozilla-IMAP-Part header.
+ Return value must be freed by the caller.
+ */
+char* mime_imap_part_address(MimeObject* obj) {
+ if (!obj || !obj->headers) return 0;
+ return MimeHeaders_get(obj->headers, IMAP_EXTERNAL_CONTENT_HEADER, false,
+ false);
+}
+
+/* Returns a full URL if the current mime object has a
+ EXTERNAL_ATTACHMENT_URL_HEADER header. Return value must be freed by the
+ caller.
+*/
+char* mime_external_attachment_url(MimeObject* obj) {
+ if (!obj || !obj->headers) return 0;
+ return MimeHeaders_get(obj->headers, EXTERNAL_ATTACHMENT_URL_HEADER, false,
+ false);
+}
+
+#ifdef ENABLE_SMIME
+/* Asks whether the given object is one of the cryptographically signed
+ or encrypted objects that we know about. (MimeMessageClass uses this
+ to decide if the headers need to be presented differently.)
+ */
+bool mime_crypto_object_p(MimeHeaders* hdrs, bool clearsigned_counts,
+ MimeDisplayOptions* opts) {
+ char* ct;
+ MimeObjectClass* clazz;
+
+ if (!hdrs) return false;
+
+ ct = MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, true, false);
+ if (!ct) return false;
+
+ /* Rough cut -- look at the string before doing a more complex comparison. */
+ if (PL_strcasecmp(ct, MULTIPART_SIGNED) &&
+ PL_strncasecmp(ct, "application/", 12)) {
+ PR_Free(ct);
+ return false;
+ }
+
+ /* It's a candidate for being a crypto object. Let's find out for sure... */
+ clazz = mime_find_class(ct, hdrs, opts, true);
+ PR_Free(ct);
+
+ if (clazz == ((MimeObjectClass*)&mimeEncryptedCMSClass)) return true;
+
+ if (clearsigned_counts &&
+ clazz == ((MimeObjectClass*)&mimeMultipartSignedCMSClass))
+ return true;
+
+ return false;
+}
+
+#endif // ENABLE_SMIME
+
+/* Puts a part-number into a URL. If append_p is true, then the part number
+ is appended to any existing part-number already in that URL; otherwise,
+ it replaces it.
+ */
+char* mime_set_url_part(const char* url, const char* part, bool append_p) {
+ const char* part_begin = 0;
+ const char* part_end = 0;
+ bool got_q = false;
+ const char* s;
+ char* result;
+
+ if (!url || !part) return 0;
+
+ nsAutoCString urlString(url);
+ int32_t typeIndex = urlString.Find("?type=application/x-message-display");
+ if (typeIndex != -1) {
+ urlString.Cut(typeIndex, sizeof("?type=application/x-message-display") - 1);
+ if (urlString.CharAt(typeIndex) == '&')
+ urlString.Replace(typeIndex, 1, '?');
+ url = urlString.get();
+ }
+
+ for (s = url; *s; s++) {
+ if (*s == '?') {
+ got_q = true;
+ if (!PL_strncasecmp(s, "?part=", 6)) part_begin = (s += 6);
+ } else if (got_q && *s == '&' && !PL_strncasecmp(s, "&part=", 6))
+ part_begin = (s += 6);
+
+ if (part_begin) {
+ while (*s && *s != '?' && *s != '&') s++;
+ part_end = s;
+ break;
+ }
+ }
+
+ uint32_t resultlen = strlen(url) + strlen(part) + 10;
+ result = (char*)PR_MALLOC(resultlen);
+ if (!result) return 0;
+
+ if (part_begin) {
+ if (append_p) {
+ memcpy(result, url, part_end - url);
+ result[part_end - url] = '.';
+ result[part_end - url + 1] = 0;
+ } else {
+ memcpy(result, url, part_begin - url);
+ result[part_begin - url] = 0;
+ }
+ } else {
+ PL_strncpyz(result, url, resultlen);
+ if (got_q)
+ PL_strcatn(result, resultlen, "&part=");
+ else
+ PL_strcatn(result, resultlen, "?part=");
+ }
+
+ PL_strcatn(result, resultlen, part);
+
+ if (part_end && *part_end) PL_strcatn(result, resultlen, part_end);
+
+ /* Semi-broken kludge to omit a trailing "?part=0". */
+ {
+ int L = strlen(result);
+ if (L > 6 && (result[L - 7] == '?' || result[L - 7] == '&') &&
+ !strcmp("part=0", result + L - 6))
+ result[L - 7] = 0;
+ }
+
+ return result;
+}
+
+/* Puts an *IMAP* part-number into a URL.
+ Strips off any previous *IMAP* part numbers, since they are absolute, not
+ relative.
+ */
+char* mime_set_url_imap_part(const char* url, const char* imappart,
+ const char* libmimepart) {
+ char* result = 0;
+ char* whereCurrent = PL_strstr(url, "/;section=");
+ if (whereCurrent) {
+ *whereCurrent = 0;
+ }
+
+ uint32_t resultLen =
+ strlen(url) + strlen(imappart) + strlen(libmimepart) + 17;
+ result = (char*)PR_MALLOC(resultLen);
+ if (!result) return 0;
+
+ PL_strncpyz(result, url, resultLen);
+ PL_strcatn(result, resultLen, "/;section=");
+ PL_strcatn(result, resultLen, imappart);
+ PL_strcatn(result, resultLen, "?part=");
+ PL_strcatn(result, resultLen, libmimepart);
+
+ if (whereCurrent) *whereCurrent = '/';
+
+ return result;
+}
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches, and returns the MimeObject (else NULL.)
+ (part is not a URL -- it's of the form "1.3.5".)
+ */
+MimeObject* mime_address_to_part(const char* part, MimeObject* obj) {
+ /* Note: this is an N^2 operation, but the number of parts in a message
+ shouldn't ever be large enough that this really matters... */
+
+ bool match;
+
+ if (!part || !*part) {
+ match = !obj->parent;
+ } else {
+ char* part2 = mime_part_address(obj);
+ if (!part2) return 0; /* MIME_OUT_OF_MEMORY */
+ match = !strcmp(part, part2);
+ PR_Free(part2);
+ }
+
+ if (match) {
+ /* These are the droids we're looking for. */
+ return obj;
+ } else if (!mime_typep(obj, (MimeObjectClass*)&mimeContainerClass)) {
+ /* Not a container, pull up, pull up! */
+ return 0;
+ } else {
+ int32_t i;
+ MimeContainer* cont = (MimeContainer*)obj;
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* o2 = mime_address_to_part(part, cont->children[i]);
+ if (o2) return o2;
+ }
+ return 0;
+ }
+}
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches; if one is found, returns the Content-Name of that part.
+ Else returns NULL. (part is not a URL -- it's of the form "1.3.5".)
+ */
+char* mime_find_content_type_of_part(const char* part, MimeObject* obj) {
+ char* result = 0;
+
+ obj = mime_address_to_part(part, obj);
+ if (!obj) return 0;
+
+ result = (obj->headers ? MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE,
+ true, false)
+ : 0);
+
+ return result;
+}
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches; if one is found, returns the Content-Name of that part.
+ Else returns NULL. (part is not a URL -- it's of the form "1.3.5".)
+ */
+char* mime_find_suggested_name_of_part(const char* part, MimeObject* obj) {
+ char* result = 0;
+
+ obj = mime_address_to_part(part, obj);
+ if (!obj) return 0;
+
+ result =
+ (obj->headers ? MimeHeaders_get_name(obj->headers, obj->options) : 0);
+
+ /* If this part doesn't have a name, but this part is one fork of an
+ AppleDouble, and the AppleDouble itself has a name, then use that. */
+ if (!result && obj->parent && obj->parent->headers &&
+ mime_typep(obj->parent, (MimeObjectClass*)&mimeMultipartAppleDoubleClass))
+ result = MimeHeaders_get_name(obj->parent->headers, obj->options);
+
+ /* Else, if this part is itself an AppleDouble, and one of its children
+ has a name, then use that (check data fork first, then resource.) */
+ if (!result &&
+ mime_typep(obj, (MimeObjectClass*)&mimeMultipartAppleDoubleClass)) {
+ MimeContainer* cont = (MimeContainer*)obj;
+ if (cont->nchildren > 1 && cont->children[1] && cont->children[1]->headers)
+ result = MimeHeaders_get_name(cont->children[1]->headers, obj->options);
+
+ if (!result && cont->nchildren > 0 && cont->children[0] &&
+ cont->children[0]->headers)
+ result = MimeHeaders_get_name(cont->children[0]->headers, obj->options);
+ }
+
+ /* Ok, now we have the suggested name, if any.
+ Now we remove any extensions that correspond to the
+ Content-Transfer-Encoding. For example, if we see the headers
+
+ Content-Type: text/plain
+ Content-Disposition: inline; filename=foo.text.uue
+ Content-Transfer-Encoding: x-uuencode
+
+ then we would look up (in mime.types) the file extensions which are
+ associated with the x-uuencode encoding, find that "uue" is one of
+ them, and remove that from the end of the file name, thus returning
+ "foo.text" as the name. This is because, by the time this file ends
+ up on disk, its content-transfer-encoding will have been removed;
+ therefore, we should suggest a file name that indicates that.
+ */
+ if (result && obj->encoding && *obj->encoding) {
+ int32_t L = strlen(result);
+ const char** exts = 0;
+
+ /*
+ I'd like to ask the mime.types file, "what extensions correspond
+ to obj->encoding (which happens to be "x-uuencode") but doing that
+ in a non-sphagetti way would require brain surgery. So, since
+ currently uuencode is the only content-transfer-encoding which we
+ understand which traditionally has an extension, we just special-
+ case it here! Icepicks in my forehead!
+
+ Note that it's special-cased in a similar way in libmsg/compose.c.
+ */
+ if (!PL_strcasecmp(obj->encoding, ENCODING_UUENCODE)) {
+ static const char* uue_exts[] = {"uu", "uue", 0};
+ exts = uue_exts;
+ }
+
+ while (exts && *exts) {
+ const char* ext = *exts;
+ int32_t L2 = strlen(ext);
+ if (L > L2 + 1 && /* long enough */
+ result[L - L2 - 1] == '.' && /* '.' in right place*/
+ !PL_strcasecmp(ext, result + (L - L2))) /* ext matches */
+ {
+ result[L - L2 - 1] = 0; /* truncate at '.' and stop. */
+ break;
+ }
+ exts++;
+ }
+ }
+
+ return result;
+}
+
+/* Parse the various "?" options off the URL and into the options struct.
+ */
+int mime_parse_url_options(const char* url, MimeDisplayOptions* options) {
+ const char* q;
+
+ if (!url || !*url) return 0;
+ if (!options) return 0;
+
+ MimeHeadersState default_headers = options->headers;
+
+ q = PL_strrchr(url, '?');
+ if (!q) return 0;
+ q++;
+ while (*q) {
+ const char *end, *value, *name_end;
+ end = q;
+ while (*end && *end != '&') end++;
+ value = q;
+ while (*value != '=' && value < end) value++;
+ name_end = value;
+ if (value < end) value++;
+ if (name_end <= q)
+ ;
+ else if (!PL_strncasecmp("headers", q, name_end - q)) {
+ if (end > value && !PL_strncasecmp("only", value, end - value))
+ options->headers = MimeHeadersOnly;
+ else if (end > value && !PL_strncasecmp("none", value, end - value))
+ options->headers = MimeHeadersNone;
+ else if (end > value && !PL_strncasecmp("all", value, end - value))
+ options->headers = MimeHeadersAll;
+ else if (end > value && !PL_strncasecmp("some", value, end - value))
+ options->headers = MimeHeadersSome;
+ else if (end > value && !PL_strncasecmp("micro", value, end - value))
+ options->headers = MimeHeadersMicro;
+ else if (end > value && !PL_strncasecmp("cite", value, end - value))
+ options->headers = MimeHeadersCitation;
+ else if (end > value && !PL_strncasecmp("citation", value, end - value))
+ options->headers = MimeHeadersCitation;
+ else
+ options->headers = default_headers;
+ } else if (!PL_strncasecmp("part", q, name_end - q) &&
+ options->format_out != nsMimeOutput::nsMimeMessageBodyQuoting) {
+ PR_FREEIF(options->part_to_load);
+ if (end > value) {
+ options->part_to_load = (char*)PR_MALLOC(end - value + 1);
+ if (!options->part_to_load) return MIME_OUT_OF_MEMORY;
+ memcpy(options->part_to_load, value, end - value);
+ options->part_to_load[end - value] = 0;
+ }
+ } else if (!PL_strncasecmp("rot13", q, name_end - q)) {
+ options->rot13_p =
+ end <= value || !PL_strncasecmp("true", value, end - value);
+ } else if (!PL_strncasecmp("emitter", q, name_end - q)) {
+ if ((end > value) && !PL_strncasecmp("js", value, end - value)) {
+ // the js emitter needs to hear about nested message bodies
+ // in order to build a proper representation.
+ options->notify_nested_bodies = true;
+ // show_attachment_inline_p has the side-effect of letting the
+ // emitter see all parts of a multipart/alternative, which it
+ // really appreciates.
+ options->show_attachment_inline_p = true;
+ // however, show_attachment_inline_p also results in a few
+ // subclasses writing junk into the body for display purposes.
+ // put a stop to these shenanigans by enabling write_pure_bodies.
+ // current offenders are:
+ // - MimeInlineImage
+ options->write_pure_bodies = true;
+ // we don't actually care about the data in the attachments, just the
+ // metadata (i.e. size)
+ options->metadata_only = true;
+ }
+ }
+
+ q = end;
+ if (*q) q++;
+ }
+
+ /* Compatibility with the "?part=" syntax used in the old (Mozilla 2.0)
+ MIME parser.
+
+ Basically, the problem is that the old part-numbering code was totally
+ busted: here's a comparison of the old and new numberings with a pair
+ of hypothetical messages (one with a single part, and one with nested
+ containers).
+ NEW: OLD: OR:
+ message/rfc822
+ image/jpeg 1 0 0
+
+ message/rfc822
+ multipart/mixed 1 0 0
+ text/plain 1.1 1 1
+ image/jpeg 1.2 2 2
+ message/rfc822 1.3 - 3
+ text/plain 1.3.1 3 -
+ message/rfc822 1.4 - 4
+ multipart/mixed 1.4.1 4 -
+ text/plain 1.4.1.1 4.1 -
+ image/jpeg 1.4.1.2 4.2 -
+ text/plain 1.5 5 5
+
+ The "NEW" column is how the current code counts. The "OLD" column is
+ what "?part=" references would do in 3.0b4 and earlier; you'll see that
+ you couldn't directly refer to the child message/rfc822 objects at all!
+ But that's when it got really weird, because if you turned on
+ "Attachments As Links" (or used a URL like "?inline=false&part=...")
+ then you got a totally different numbering system (seen in the "OR"
+ column.) Gag!
+
+ So, the problem is, ClariNet had been using these part numbers in their
+ HTML news feeds, as a sleazy way of transmitting both complex HTML layouts
+ and images using NNTP as transport, without invoking HTTP.
+
+ The following clause is to provide some small amount of backward
+ compatibility. By looking at that table, one can see that in the new
+ model, "part=0" has no meaning, and neither does "part=2" or "part=3"
+ and so on.
+
+ "part=1" is ambiguous between the old and new way, as is any part
+ specification that has a "." in it.
+
+ So, the compatibility hack we do here is: if the part is "0", then map
+ that to "1". And if the part is >= "2", then prepend "1." to it (so that
+ we map "2" to "1.2", and "3" to "1.3".)
+
+ This leaves the URLs compatible in the cases of:
+
+ = single part messages
+ = references to elements of a top-level multipart except the first
+
+ and leaves them incompatible for:
+
+ = the first part of a top-level multipart
+ = all elements deeper than the outermost part
+
+ Life s#$%s when you don't properly think out things that end up turning
+ into de-facto standards...
+ */
+
+ if (options->part_to_load &&
+ !PL_strchr(options->part_to_load, '.')) /* doesn't contain a dot */
+ {
+ if (!strcmp(options->part_to_load, "0")) /* 0 */
+ {
+ PR_Free(options->part_to_load);
+ options->part_to_load = strdup("1");
+ if (!options->part_to_load) return MIME_OUT_OF_MEMORY;
+ } else if (strcmp(options->part_to_load, "1")) /* not 1 */
+ {
+ const char* prefix = "1.";
+ uint32_t slen = strlen(options->part_to_load) + strlen(prefix) + 1;
+ char* s = (char*)PR_MALLOC(slen);
+ if (!s) return MIME_OUT_OF_MEMORY;
+ PL_strncpyz(s, prefix, slen);
+ PL_strcatn(s, slen, options->part_to_load);
+ PR_Free(options->part_to_load);
+ options->part_to_load = s;
+ }
+ }
+
+ return 0;
+}
+
+/* Some output-generation utility functions...
+ */
+
+int MimeOptions_write(MimeHeaders* hdrs, MimeDisplayOptions* opt,
+ const char* data, int32_t length, bool user_visible_p) {
+ int status = 0;
+ void* closure = 0;
+ if (!opt || !opt->output_fn || !opt->state) return 0;
+
+ closure = opt->output_closure;
+ if (!closure) closure = opt->stream_closure;
+
+ // PR_ASSERT(opt->state->first_data_written_p);
+
+ if (opt->state->separator_queued_p && user_visible_p) {
+ opt->state->separator_queued_p = false;
+ if (opt->state->separator_suppressed_p)
+ opt->state->separator_suppressed_p = false;
+ else {
+ const char* sep = "<BR><FIELDSET CLASS=\"moz-mime-attachment-header\">";
+ int lstatus = opt->output_fn(sep, strlen(sep), closure);
+ opt->state->separator_suppressed_p = false;
+ if (lstatus < 0) return lstatus;
+
+ nsCString name;
+ name.Adopt(MimeHeaders_get_name(hdrs, opt));
+ MimeHeaders_convert_header_value(opt, name, false);
+
+ if (!name.IsEmpty()) {
+ sep = "<LEGEND CLASS=\"moz-mime-attachment-header-name\">";
+ lstatus = opt->output_fn(sep, strlen(sep), closure);
+ opt->state->separator_suppressed_p = false;
+ if (lstatus < 0) return lstatus;
+
+ nsCString escapedName;
+ nsAppendEscapedHTML(name, escapedName);
+
+ lstatus =
+ opt->output_fn(escapedName.get(), escapedName.Length(), closure);
+ opt->state->separator_suppressed_p = false;
+ if (lstatus < 0) return lstatus;
+
+ sep = "</LEGEND>";
+ lstatus = opt->output_fn(sep, strlen(sep), closure);
+ opt->state->separator_suppressed_p = false;
+ if (lstatus < 0) return lstatus;
+ }
+
+ sep = "</FIELDSET>";
+ lstatus = opt->output_fn(sep, strlen(sep), closure);
+ opt->state->separator_suppressed_p = false;
+ if (lstatus < 0) return lstatus;
+ }
+ }
+ if (user_visible_p) opt->state->separator_suppressed_p = false;
+
+ if (length > 0) {
+ status = opt->output_fn(data, length, closure);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+int MimeObject_write(MimeObject* obj, const char* output, int32_t length,
+ bool user_visible_p) {
+ if (!obj->output_p) return 0;
+
+ // if we're stripping attachments, check if any parent is not being output
+ if (obj->options &&
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach) {
+ // if true, mime generates a separator in html - we don't want that.
+ user_visible_p = false;
+
+ for (MimeObject* parent = obj->parent; parent; parent = parent->parent) {
+ if (!parent->output_p) return 0;
+ }
+ }
+ if (obj->options && !obj->options->state->first_data_written_p) {
+ int status = MimeObject_output_init(obj, 0);
+ if (status < 0) return status;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ }
+
+ return MimeOptions_write(obj->headers, obj->options, output, length,
+ user_visible_p);
+}
+
+int MimeObject_write_separator(MimeObject* obj) {
+ if (obj->options && obj->options->state &&
+ // we never want separators if we are asking for pure bodies
+ !obj->options->write_pure_bodies)
+ obj->options->state->separator_queued_p = true;
+ return 0;
+}
+
+int MimeObject_output_init(MimeObject* obj, const char* content_type) {
+ if (obj && obj->options && obj->options->state &&
+ !obj->options->state->first_data_written_p) {
+ int status;
+ const char* charset = 0;
+ char *name = 0, *x_mac_type = 0, *x_mac_creator = 0;
+
+ if (!obj->options->output_init_fn) {
+ obj->options->state->first_data_written_p = true;
+ return 0;
+ }
+
+ if (obj->headers) {
+ char* ct;
+ name = MimeHeaders_get_name(obj->headers, obj->options);
+
+ ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ if (ct) {
+ x_mac_type =
+ MimeHeaders_get_parameter(ct, PARAM_X_MAC_TYPE, nullptr, nullptr);
+ x_mac_creator = MimeHeaders_get_parameter(ct, PARAM_X_MAC_CREATOR,
+ nullptr, nullptr);
+ /* if don't have a x_mac_type and x_mac_creator, we need to try to get
+ * it from its parent */
+ if (!x_mac_type && !x_mac_creator && obj->parent &&
+ obj->parent->headers) {
+ char* ctp = MimeHeaders_get(obj->parent->headers, HEADER_CONTENT_TYPE,
+ false, false);
+ if (ctp) {
+ x_mac_type = MimeHeaders_get_parameter(ctp, PARAM_X_MAC_TYPE,
+ nullptr, nullptr);
+ x_mac_creator = MimeHeaders_get_parameter(ctp, PARAM_X_MAC_CREATOR,
+ nullptr, nullptr);
+ PR_Free(ctp);
+ }
+ }
+
+ if (!(obj->options->override_charset)) {
+ char* charset =
+ MimeHeaders_get_parameter(ct, "charset", nullptr, nullptr);
+ if (charset) {
+ PR_FREEIF(obj->options->default_charset);
+ obj->options->default_charset = charset;
+ }
+ }
+ PR_Free(ct);
+ }
+ }
+
+ if (mime_typep(obj, (MimeObjectClass*)&mimeInlineTextClass))
+ charset = ((MimeInlineText*)obj)->charset;
+
+ if (!content_type) content_type = obj->content_type;
+ if (!content_type) content_type = TEXT_PLAIN;
+
+ //
+ // Set the charset on the channel we are dealing with so people know
+ // what the charset is set to. Do this for quoting/Printing ONLY!
+ //
+ extern void ResetChannelCharset(MimeObject * obj);
+ if ((obj->options) &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyQuoting ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageSaveAs ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessagePrintOutput))
+ ResetChannelCharset(obj);
+
+ status = obj->options->output_init_fn(content_type, charset, name,
+ x_mac_type, x_mac_creator,
+ obj->options->stream_closure);
+ PR_FREEIF(name);
+ PR_FREEIF(x_mac_type);
+ PR_FREEIF(x_mac_creator);
+ obj->options->state->first_data_written_p = true;
+ return status;
+ }
+ return 0;
+}
+
+char* mime_get_base_url(const char* url) {
+ if (!url) return nullptr;
+
+ const char* s = strrchr(url, '?');
+ if (s && !strncmp(s, "?type=application/x-message-display",
+ sizeof("?type=application/x-message-display") - 1)) {
+ const char* nextTerm = strchr(s, '&');
+ // strlen(s) cannot be zero, because it matches the above text
+ s = nextTerm ? nextTerm : s + strlen(s) - 1;
+ }
+ // we need to keep the ?number part of the url, or we won't know
+ // which local message the part belongs to.
+ if (s && *s && *(s + 1) &&
+ !strncmp(s + 1, "number=", sizeof("number=") - 1)) {
+ const char* nextTerm = strchr(++s, '&');
+ s = nextTerm ? nextTerm : s + strlen(s) - 1;
+ }
+ char* result = (char*)PR_MALLOC(strlen(url) + 1);
+ NS_ASSERTION(result, "out of memory");
+ if (!result) return nullptr;
+
+ memcpy(result, url, s - url);
+ result[s - url] = 0;
+ return result;
+}
diff --git a/comm/mailnews/mime/src/mimei.h b/comm/mailnews/mime/src/mimei.h
new file mode 100644
index 0000000000..d3a0684224
--- /dev/null
+++ b/comm/mailnews/mime/src/mimei.h
@@ -0,0 +1,405 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEI_H_
+#define _MIMEI_H_
+
+/*
+ This module, libmime, implements a general-purpose MIME parser.
+ One of the methods provided by this parser is the ability to emit
+ an HTML representation of it.
+
+ All Mozilla-specific code is (and should remain) isolated in the
+ file mimemoz.c. Generally, if the code involves images, netlib
+ streams it should be in mimemoz.c instead of in the main body of
+ the MIME parser.
+
+ The parser is object-oriented and fully buzzword-compliant.
+ There is a class for each MIME type, and each class is responsible
+ for parsing itself, and/or handing the input data off to one of its
+ child objects.
+
+ The class hierarchy is:
+
+ MimeObject (abstract)
+ |
+ +--- MimeContainer (abstract)
+ | |
+ | +--- MimeMultipart (abstract)
+ | | |
+ | | +--- MimeMultipartMixed
+ | | |
+ | | +--- MimeMultipartDigest
+ | | |
+ | | +--- MimeMultipartParallel
+ | | |
+ | | +--- MimeMultipartAlternative
+ | | |
+ | | +--- MimeMultipartRelated
+ | | |
+ | | +--- MimeMultipartAppleDouble
+ | | |
+ | | +--- MimeSunAttachment
+ | | |
+ | | \--- MimeMultipartSigned (abstract)
+ | | |
+ | | \--- MimeMultipartSignedCMS
+ | |
+ | +--- MimeEncrypted (abstract)
+ | | |
+ | | \--- MimeEncryptedPKCS7
+ | |
+ | +--- MimeXlateed (abstract)
+ | | |
+ | | \--- MimeXlateed
+ | |
+ | +--- MimeMessage
+ | |
+ | \--- MimeUntypedText
+ |
+ +--- MimeLeaf (abstract)
+ | |
+ | +--- MimeInlineText (abstract)
+ | | |
+ | | +--- MimeInlineTextPlain
+ | | | |
+ | | | \--- MimeInlineTextHTMLAsPlaintext
+ | | |
+ | | +--- MimeInlineTextPlainFlowed
+ | | |
+ | | +--- MimeInlineTextHTML
+ | | | |
+ | | | +--- MimeInlineTextHTMLParsed
+ | | | |
+ | | | \--- MimeInlineTextHTMLSanitized
+ | | |
+ | | +--- MimeInlineTextRichtext
+ | | | |
+ | | | \--- MimeInlineTextEnriched
+ | | |
+ | | +--- MimeInlineTextVCard
+ | |
+ | +--- MimeInlineImage
+ | |
+ | \--- MimeExternalObject
+ |
+ \--- MimeExternalBody
+
+
+ =========================================================================
+ The definition of these classes is somewhat idiosyncratic, since I defined
+ my own small object system, instead of giving the C++ virus another foothold.
+ (I would have liked to have written this in Java, but our runtime isn't
+ quite ready for prime time yet.)
+
+ There is one header file and one source file for each class (for example,
+ the MimeInlineText class is defined in "mimetext.h" and "mimetext.c".)
+ Each header file follows the following boiler-plate form:
+
+ TYPEDEFS: these come first to avoid circular dependencies.
+
+ typedef struct FoobarClass FoobarClass;
+ typedef struct Foobar Foobar;
+
+ CLASS DECLARATION:
+ This structure defines the callback routines and other per-class data
+ of the class defined in this module.
+
+ struct FoobarClass {
+ ParentClass superclass;
+ ...any callbacks or class-variables...
+ };
+
+ CLASS DEFINITION:
+ This variable holds an instance of the one-and-only class record; the
+ various instances of this class point to this object. (One interrogates
+ the type of an instance by comparing the value of its class pointer with
+ the address of this variable.)
+
+ extern FoobarClass foobarClass;
+
+ INSTANCE DECLARATION:
+ This structure defines the per-instance data of an object, and a pointer
+ to the corresponding class record.
+
+ struct Foobar {
+ Parent parent;
+ ...any instance variables...
+ };
+
+ Then, in the corresponding .c file, the following structure is used:
+
+ CLASS DEFINITION:
+ First we pull in the appropriate include file (which includes all necessary
+ include files for the parent classes) and then we define the class object
+ using the MimeDefClass macro:
+
+ #include "foobar.h"
+ #define MIME_SUPERCLASS parentlClass
+ MimeDefClass(Foobar, FoobarClass, foobarClass, &MIME_SUPERCLASS);
+
+ The definition of MIME_SUPERCLASS is just to move most of the knowledge of the
+ exact class hierarchy up to the file's header, instead of it being scattered
+ through the various methods; see below.
+
+ METHOD DECLARATIONS:
+ We will be putting function pointers into the class object, so we declare
+ them here. They can generally all be static, since nobody outside of this
+ file needs to reference them by name; all references to these routines should
+ be through the class object.
+
+ extern int FoobarMethod(Foobar *);
+ ...etc...
+
+ CLASS INITIALIZATION FUNCTION:
+ The MimeDefClass macro expects us to define a function which will finish up
+ any initialization of the class object that needs to happen before the first
+ time it is instantiated. Its name must be of the form "<class>Initialize",
+ and it should initialize the various method slots in the class as
+ appropriate. Any methods or class variables which this class does not wish
+ to override will be automatically inherited from the parent class (by virtue
+ of its class-initialization function having been run first.) Each class
+ object will only be initialized once.
+
+ static int
+ FoobarClassInitialize(FoobarClass *class)
+ {
+ clazz->method = FoobarMethod.
+ ...etc...
+ }
+
+ METHOD DEFINITIONS:
+ Next come the definitions of the methods we referred to in the class-init
+ function. The way to access earlier methods (methods defined on the
+ superclass) is to simply extract them from the superclass's object.
+ But note that you CANNOT get at methods by indirecting through
+ object->clazz->superclass: that will only work to one level, and will
+ go into a loop if some subclass tries to continue on this method.
+
+ The easiest way to do this is to make use of the MIME_SUPERCLASS macro that
+ was defined at the top of the file, as shown below. The alternative to that
+ involves typing the literal name of the direct superclass of the class
+ defined in this file, which will be a maintenance headache if the class
+ hierarchy changes. If you use the MIME_SUPERCLASS idiom, then a textual
+ change is required in only one place if this class's superclass changes.
+
+ static void
+ Foobar_finalize (MimeObject *object)
+ {
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object); // RIGHT
+ parentClass.whatnot.object.finalize(object); // (works...)
+ object->clazz->superclass->finalize(object); // WRONG!!
+ }
+
+ If you write a libmime content type handler, libmime might create several
+ instances of your class at once and call e.g. the same finalize code for
+ 3 different objects in a row.
+ */
+
+#include "mimehdrs.h"
+#include "nsTArray.h"
+
+typedef struct MimeObject MimeObject;
+typedef struct MimeObjectClass MimeObjectClass;
+
+/* (I don't pretend to understand this.) */
+#define cpp_stringify_noop_helper(x) #x
+#define cpp_stringify(x) cpp_stringify_noop_helper(x)
+
+#define MimeObjectClassInitializer(ITYPE, CSUPER) \
+ cpp_stringify(ITYPE), sizeof(ITYPE), (MimeObjectClass*)CSUPER, \
+ (int (*)(MimeObjectClass*))ITYPE##ClassInitialize, 0
+
+/* Macro used for setting up class definitions.
+ */
+#define MimeDefClass(ITYPE, CTYPE, CVAR, CSUPER) \
+ static int ITYPE##ClassInitialize(ITYPE##Class*); \
+ ITYPE##Class CVAR = {ITYPE##ClassInitializer(ITYPE, CSUPER)}
+
+/* Creates a new (subclass of) MimeObject of the given class, with the
+ given headers (which are copied.)
+ */
+extern MimeObject* mime_new(MimeObjectClass* clazz, MimeHeaders* hdrs,
+ const char* override_content_type);
+
+/* Destroys a MimeObject (or subclass) and all data associated with it.
+ */
+extern "C" void mime_free(MimeObject* object);
+
+/* Given a content-type string, finds and returns an appropriate subclass
+ of MimeObject. A class object is returned. If `exact_match_p' is true,
+ then only fully-known types will be returned; that is, if it is true,
+ then "text/x-unknown" will return MimeInlineTextPlainType, but if it is
+ false, it will return NULL.
+ */
+extern MimeObjectClass* mime_find_class(const char* content_type,
+ MimeHeaders* hdrs,
+ MimeDisplayOptions* opts,
+ bool exact_match_p);
+
+/** Given a content-type string, creates and returns an appropriate subclass
+ * of MimeObject. The headers (from which the content-type was presumably
+ * extracted) are copied. forceInline is set to true when the caller wants
+ * the function to ignore opts->show_attachment_inline_p and force inline
+ * display, e.g., mimemalt wants the body part to be shown inline.
+ */
+extern MimeObject* mime_create(const char* content_type, MimeHeaders* hdrs,
+ MimeDisplayOptions* opts,
+ bool forceInline = false);
+
+/* Querying the type hierarchy */
+extern bool mime_subclass_p(MimeObjectClass* child, MimeObjectClass* parent);
+extern bool mime_typep(MimeObject* obj, MimeObjectClass* clazz);
+
+/* Returns a string describing the location of the part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ */
+extern char* mime_part_address(MimeObject* obj);
+
+/* Returns a string describing the location of the *IMAP* part (like "2.5.3").
+ This is not a full URL, just a part-number.
+ This part is explicitly passed in the X-Mozilla-IMAP-Part header.
+ Return value must be freed by the caller.
+ */
+extern char* mime_imap_part_address(MimeObject* obj);
+
+extern char* mime_external_attachment_url(MimeObject* obj);
+
+/* Puts a part-number into a URL. If append_p is true, then the part number
+ is appended to any existing part-number already in that URL; otherwise,
+ it replaces it.
+ */
+extern char* mime_set_url_part(const char* url, const char* part,
+ bool append_p);
+
+/*
+ cut the part of url for display a attachment as a email.
+*/
+extern char* mime_get_base_url(const char* url);
+
+/* Puts an *IMAP* part-number into a URL.
+ */
+extern char* mime_set_url_imap_part(const char* url, const char* part,
+ const char* libmimepart);
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches, and returns the MimeObject (else NULL.)
+ (part is not a URL -- it's of the form "1.3.5".)
+ */
+extern MimeObject* mime_address_to_part(const char* part, MimeObject* obj);
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches; if one is found, returns the Content-Name of that part.
+ Else returns NULL. (part is not a URL -- it's of the form "1.3.5".)
+ */
+extern char* mime_find_suggested_name_of_part(const char* part,
+ MimeObject* obj);
+
+/* Given a part ID, looks through the MimeObject tree for a sub-part whose ID
+ number matches; if one is found, returns the Content-Name of that part.
+ Else returns NULL. (part is not a URL -- it's of the form "1.3.5".)
+ */
+extern char* mime_find_content_type_of_part(const char* part, MimeObject* obj);
+
+/* Parse the various "?" options off the URL and into the options struct.
+ */
+extern int mime_parse_url_options(const char* url, MimeDisplayOptions*);
+
+#ifdef ENABLE_SMIME
+
+/* Asks whether the given object is one of the cryptographically signed
+ or encrypted objects that we know about. (MimeMessageClass uses this
+ to decide if the headers need to be presented differently.)
+ */
+extern bool mime_crypto_object_p(MimeHeaders*, bool clearsigned_counts,
+ MimeDisplayOptions*);
+
+/* Tells whether the given MimeObject is a message which has been encrypted
+ or signed. (Helper for MIME_GetMessageCryptoState()).
+ */
+extern void mime_get_crypto_state(MimeObject* obj, bool* signed_p,
+ bool* encrypted_p, bool* signed_ok,
+ bool* encrypted_ok);
+
+/* How the crypto code tells the MimeMessage object what the crypto stamp
+ on it says. */
+extern void mime_set_crypto_stamp(MimeObject* obj, bool signed_p,
+ bool encrypted_p);
+#endif // ENABLE_SMIME
+
+class MimeParseStateObject {
+ public:
+ MimeParseStateObject() {
+ root = 0;
+ separator_queued_p = false;
+ separator_suppressed_p = false;
+ first_part_written_p = false;
+ post_header_html_run_p = false;
+ first_data_written_p = false;
+ strippingPart = false;
+ }
+ MimeObject* root; /* The outermost parser object. */
+
+ bool separator_queued_p; /* Whether a separator should be written out
+ before the next text is written (this lets
+ us write separators lazily, so that one
+ doesn't appear at the end, and so that more
+ than one don't appear in a row.) */
+
+ bool separator_suppressed_p; /* Whether the currently-queued separator
+ should not be printed; this is a kludge to
+ prevent seps from being printed just after
+ a header block... */
+
+ bool first_part_written_p; /* State used for the `Show Attachments As
+ Links' kludge. */
+
+ bool post_header_html_run_p; /* Whether we've run the
+ options->generate_post_header_html_fn */
+
+ bool first_data_written_p; /* State used for Mozilla lazy-stream-
+ creation evilness. */
+
+ nsTArray<nsCString>
+ partsToStrip; /* if we're stripping parts, what parts to strip */
+ nsTArray<nsCString> detachToFiles; /* if we're detaching parts, where each
+ part was detached to */
+ bool strippingPart;
+ nsCString detachedFilePath; /* if we've detached this part, filepath of
+ detached part */
+};
+
+/* Some output-generation utility functions...
+ */
+extern int MimeObject_output_init(MimeObject* obj, const char* content_type);
+
+/* The `user_visible_p' argument says whether the output that has just been
+ written will cause characters or images to show up on the screen, that
+ is, it should be false if the stuff being written is merely structural
+ HTML or whitespace ("<P>", "</TABLE>", etc.) This information is used
+ when making the decision of whether a separating <HR> is needed.
+ */
+extern int MimeObject_write(MimeObject*, const char* data, int32_t length,
+ bool user_visible_p);
+extern int MimeOptions_write(MimeHeaders*, MimeDisplayOptions*,
+ const char* data, int32_t length,
+ bool user_visible_p);
+
+/* Writes out the right kind of HR (or rather, queues it for writing.) */
+extern int MimeObject_write_separator(MimeObject*);
+
+extern bool MimeObjectIsMessageBody(MimeObject* obj);
+
+struct MimeDisplayData { /* This struct is what we hang off of
+ (context)->mime_data, to remember info
+ about the last MIME object we've
+ parsed and displayed. See
+ MimeGuessURLContentName() below.
+ */
+ MimeObject* last_parsed_object;
+ char* last_parsed_url;
+};
+
+#endif /* _MIMEI_H_ */
diff --git a/comm/mailnews/mime/src/mimeiimg.cpp b/comm/mailnews/mime/src/mimeiimg.cpp
new file mode 100644
index 0000000000..f2ac8a0e09
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeiimg.cpp
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "mimeiimg.h"
+#include "mimemoz2.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+#include "nsMimeStringResources.h"
+#include "nsINetUtil.h"
+#include "nsMsgUtils.h"
+
+#define MIME_SUPERCLASS mimeLeafClass
+MimeDefClass(MimeInlineImage, MimeInlineImageClass, mimeInlineImageClass,
+ &MIME_SUPERCLASS);
+
+static int MimeInlineImage_initialize(MimeObject*);
+static void MimeInlineImage_finalize(MimeObject*);
+static int MimeInlineImage_parse_begin(MimeObject*);
+static int MimeInlineImage_parse_line(const char*, int32_t, MimeObject*);
+static int MimeInlineImage_parse_eof(MimeObject*, bool);
+static int MimeInlineImage_parse_decoded_buffer(const char*, int32_t,
+ MimeObject*);
+
+static int MimeInlineImageClassInitialize(MimeInlineImageClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeLeafClass* lclass = (MimeLeafClass*)clazz;
+
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ oclass->initialize = MimeInlineImage_initialize;
+ oclass->finalize = MimeInlineImage_finalize;
+ oclass->parse_begin = MimeInlineImage_parse_begin;
+ oclass->parse_line = MimeInlineImage_parse_line;
+ oclass->parse_eof = MimeInlineImage_parse_eof;
+ lclass->parse_decoded_buffer = MimeInlineImage_parse_decoded_buffer;
+
+ return 0;
+}
+
+static int MimeInlineImage_initialize(MimeObject* object) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeInlineImage_finalize(MimeObject* object) {
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeInlineImage_parse_begin(MimeObject* obj) {
+ MimeInlineImage* img = (MimeInlineImage*)obj;
+
+ int status;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ if (!obj->output_p) return 0;
+
+ if (!obj->options || !obj->options->output_fn ||
+ // don't bother processing if the consumer doesn't want us
+ // gunking the body up.
+ obj->options->write_pure_bodies)
+ return 0;
+
+ if (obj->options && obj->options->image_begin && obj->options->write_html_p &&
+ obj->options->image_write_buffer) {
+ char *html, *part, *image_url;
+ const char* ct;
+
+ part = mime_part_address(obj);
+ if (!part) return MIME_OUT_OF_MEMORY;
+
+ char* no_part_url = nullptr;
+ if (obj->options->part_to_load &&
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay)
+ no_part_url = mime_get_base_url(obj->options->url);
+
+ if (no_part_url) {
+ image_url = mime_set_url_part(no_part_url, part, true);
+ PR_Free(no_part_url);
+ } else
+ image_url = mime_set_url_part(obj->options->url, part, true);
+
+ if (!image_url) {
+ PR_Free(part);
+ return MIME_OUT_OF_MEMORY;
+ }
+ PR_Free(part);
+
+ ct = obj->content_type;
+ if (!ct) ct = IMAGE_GIF; /* Can't happen? Close enough. */
+
+ // Fill in content type and attachment name here.
+ nsAutoCString url_with_filename(image_url);
+ url_with_filename += "&type=";
+ url_with_filename += ct;
+ char* filename = MimeHeaders_get_name(obj->headers, obj->options);
+ if (filename) {
+ nsCString escapedName;
+ MsgEscapeString(nsDependentCString(filename), nsINetUtil::ESCAPE_URL_PATH,
+ escapedName);
+ url_with_filename += "&filename=";
+ url_with_filename += escapedName;
+ PR_Free(filename);
+ }
+
+ // We need to separate images with HR's...
+ MimeObject_write_separator(obj);
+
+ img->image_data = obj->options->image_begin(url_with_filename.get(), ct,
+ obj->options->stream_closure);
+ PR_Free(image_url);
+
+ if (!img->image_data) return MIME_OUT_OF_MEMORY;
+
+ html = obj->options->make_image_html(img->image_data);
+ if (!html) return MIME_OUT_OF_MEMORY;
+
+ status = MimeObject_write(obj, html, strlen(html), true);
+ PR_Free(html);
+ if (status < 0) return status;
+ }
+
+ //
+ // Now we are going to see if we should set the content type in the
+ // URI for the url being run...
+ //
+ if (obj->options && obj->options->stream_closure && obj->content_type) {
+ mime_stream_data* msd = (mime_stream_data*)(obj->options->stream_closure);
+ if ((msd) && (msd->channel)) {
+ msd->channel->SetContentType(nsDependentCString(obj->content_type));
+ }
+ }
+
+ return 0;
+}
+
+static int MimeInlineImage_parse_eof(MimeObject* obj, bool abort_p) {
+ MimeInlineImage* img = (MimeInlineImage*)obj;
+ int status;
+ if (obj->closed_p) return 0;
+
+ /* Force out any buffered data from the superclass (the base64 decoder.) */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) abort_p = true;
+
+ if (img->image_data) {
+ obj->options->image_end(img->image_data,
+ (status < 0 ? status : (abort_p ? -1 : 0)));
+ img->image_data = 0;
+ }
+
+ return status;
+}
+
+static int MimeInlineImage_parse_decoded_buffer(const char* buf, int32_t size,
+ MimeObject* obj) {
+ /* This is called (by MimeLeafClass->parse_buffer) with blocks of data
+ that have already been base64-decoded. Pass this raw image data
+ along to the backend-specific image display code.
+ */
+ MimeInlineImage* img = (MimeInlineImage*)obj;
+ int status;
+
+ /* Don't do a roundtrip through XPConnect when we're only interested in
+ * metadata and size. 0 means ok, the caller just checks for negative return
+ * value
+ */
+ if (obj->options && obj->options->metadata_only) return 0;
+
+ if (obj->output_p && obj->options && !obj->options->write_html_p) {
+ /* in this case, we just want the raw data...
+ Make the stream, if it's not made, and dump the data out.
+ */
+
+ if (!obj->options->state->first_data_written_p) {
+ status = MimeObject_output_init(obj, 0);
+ if (status < 0) return status;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ }
+
+ return MimeObject_write(obj, buf, size, true);
+ }
+
+ if (!obj->options || !obj->options->image_write_buffer) return 0;
+
+ /* If we don't have any image data, the image_end method must have already
+ been called, so don't call image_write_buffer again. */
+ if (!img->image_data) return 0;
+
+ /* Hand this data off to the backend-specific image display stream.
+ */
+ status = obj->options->image_write_buffer(buf, size, img->image_data);
+
+ /* If the image display stream fails, then close the stream - but do not
+ return the failure status, and do not give up on parsing this object.
+ Just because the image data was corrupt doesn't mean we need to give up
+ on the whole document; we can continue by just skipping over the rest of
+ this part, and letting our parent continue.
+ */
+ if (status < 0) {
+ obj->options->image_end(img->image_data, status);
+ img->image_data = 0;
+ status = 0;
+ }
+
+ return status;
+}
+
+static int MimeInlineImage_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ NS_ERROR(
+ "This method should never be called (inline images do no line "
+ "buffering).");
+ return -1;
+}
diff --git a/comm/mailnews/mime/src/mimeiimg.h b/comm/mailnews/mime/src/mimeiimg.h
new file mode 100644
index 0000000000..b95400b967
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeiimg.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEIIMG_H_
+#define _MIMEIIMG_H_
+
+#include "mimeleaf.h"
+
+/* The MimeInlineImage class implements those MIME image types which can be
+ displayed inline.
+ */
+
+typedef struct MimeInlineImageClass MimeInlineImageClass;
+typedef struct MimeInlineImage MimeInlineImage;
+
+struct MimeInlineImageClass {
+ MimeLeafClass leaf;
+};
+
+extern MimeInlineImageClass mimeInlineImageClass;
+
+struct MimeInlineImage {
+ MimeLeaf leaf;
+
+ /* Opaque data object for the backend-specific inline-image-display code
+ (internal-external-reconnect nastiness.) */
+ void* image_data;
+};
+
+#define MimeInlineImageClassInitializer(ITYPE, CSUPER) \
+ { MimeLeafClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEIIMG_H_ */
diff --git a/comm/mailnews/mime/src/mimeleaf.cpp b/comm/mailnews/mime/src/mimeleaf.cpp
new file mode 100644
index 0000000000..24d8c8989f
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeleaf.cpp
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "modmimee.h"
+#include "mimeleaf.h"
+#include "nsMimeTypes.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeStringResources.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+#define MIME_SUPERCLASS mimeObjectClass
+MimeDefClass(MimeLeaf, MimeLeafClass, mimeLeafClass, &MIME_SUPERCLASS);
+
+static int MimeLeaf_initialize(MimeObject*);
+static void MimeLeaf_finalize(MimeObject*);
+static int MimeLeaf_parse_begin(MimeObject*);
+static int MimeLeaf_parse_buffer(const char*, int32_t, MimeObject*);
+static int MimeLeaf_parse_line(const char*, int32_t, MimeObject*);
+static int MimeLeaf_close_decoder(MimeObject*);
+static int MimeLeaf_parse_eof(MimeObject*, bool);
+static bool MimeLeaf_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs);
+
+static int MimeLeafClassInitialize(MimeLeafClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ oclass->initialize = MimeLeaf_initialize;
+ oclass->finalize = MimeLeaf_finalize;
+ oclass->parse_begin = MimeLeaf_parse_begin;
+ oclass->parse_buffer = MimeLeaf_parse_buffer;
+ oclass->parse_line = MimeLeaf_parse_line;
+ oclass->parse_eof = MimeLeaf_parse_eof;
+ oclass->displayable_inline_p = MimeLeaf_displayable_inline_p;
+ clazz->close_decoder = MimeLeaf_close_decoder;
+
+ /* Default `parse_buffer' method is one which line-buffers the now-decoded
+ data and passes it on to `parse_line'. (We snarf the implementation of
+ this method from our superclass's implementation of `parse_buffer', which
+ inherited it from MimeObject.)
+ */
+ clazz->parse_decoded_buffer =
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_buffer;
+
+ return 0;
+}
+
+static int MimeLeaf_initialize(MimeObject* obj) {
+ /* This is an abstract class; it shouldn't be directly instantiated. */
+ NS_ASSERTION(obj->clazz != (MimeObjectClass*)&mimeLeafClass,
+ "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+
+ // Initial size is -1 (meaning "unknown size") - we'll correct it in
+ // parse_buffer.
+ MimeLeaf* leaf = (MimeLeaf*)obj;
+ leaf->sizeSoFar = -1;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj);
+}
+
+static void MimeLeaf_finalize(MimeObject* object) {
+ MimeLeaf* leaf = (MimeLeaf*)object;
+ object->clazz->parse_eof(object, false);
+
+ /* Free the decoder data, if it's still around. It was probably freed
+ in MimeLeaf_parse_eof(), but just in case... */
+ if (leaf->decoder_data) {
+ MimeDecoderDestroy(leaf->decoder_data, true);
+ leaf->decoder_data = 0;
+ }
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeLeaf_parse_begin(MimeObject* obj) {
+ MimeLeaf* leaf = (MimeLeaf*)obj;
+ MimeDecoderData* (*fn)(MimeConverterOutputCallback, void*) = 0;
+
+ /* Initialize a decoder if necessary.
+ */
+ if (!obj->encoding ||
+ // If we need the object as "raw" for saving or forwarding,
+ // don't decode attachment parts if headers are also written
+ // via the parent, so that the header matches the encoding.
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageRaw &&
+ obj->parent && obj->parent->output_p))
+ /* no-op */;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_BASE64))
+ fn = &MimeB64DecoderInit;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_QUOTED_PRINTABLE))
+ leaf->decoder_data = MimeQPDecoderInit(
+ ((MimeConverterOutputCallback)((MimeLeafClass*)obj->clazz)
+ ->parse_decoded_buffer),
+ obj, obj);
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_UUENCODE) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE2) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE3) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE4))
+ fn = &MimeUUDecoderInit;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_YENCODE))
+ fn = &MimeYDecoderInit;
+
+ if (fn) {
+ leaf->decoder_data =
+ fn(/* The MimeConverterOutputCallback cast is to turn the `void'
+ argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)((MimeLeafClass*)obj->clazz)
+ ->parse_decoded_buffer),
+ obj);
+
+ if (!leaf->decoder_data) return MIME_OUT_OF_MEMORY;
+ }
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+}
+
+static int MimeLeaf_parse_buffer(const char* buffer, int32_t size,
+ MimeObject* obj) {
+ MimeLeaf* leaf = (MimeLeaf*)obj;
+
+ NS_ASSERTION(!obj->closed_p, "1.1 <rhp@netscape.com> 19 Mar 1999 12:00");
+ if (obj->closed_p) return -1;
+
+ /* If we're not supposed to write this object, bug out now.
+ */
+ if (!obj->output_p || !obj->options || !obj->options->output_fn) return 0;
+
+ int rv;
+ if (leaf->sizeSoFar == -1) leaf->sizeSoFar = 0;
+
+ if (leaf->decoder_data && obj->options &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessageDecrypt &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessageAttach) {
+ int outSize = 0;
+ rv = MimeDecoderWrite(leaf->decoder_data, buffer, size, &outSize);
+ leaf->sizeSoFar += outSize;
+ } else {
+ rv = ((MimeLeafClass*)obj->clazz)->parse_decoded_buffer(buffer, size, obj);
+ leaf->sizeSoFar += size;
+ }
+ return rv;
+}
+
+static int MimeLeaf_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ NS_ERROR("MimeLeaf_parse_line shouldn't ever be called.");
+ return -1;
+}
+
+static int MimeLeaf_close_decoder(MimeObject* obj) {
+ MimeLeaf* leaf = (MimeLeaf*)obj;
+
+ if (leaf->decoder_data) {
+ int status = MimeDecoderDestroy(leaf->decoder_data, false);
+ leaf->decoder_data = 0;
+ return status;
+ }
+
+ return 0;
+}
+
+static int MimeLeaf_parse_eof(MimeObject* obj, bool abort_p) {
+ MimeLeaf* leaf = (MimeLeaf*)obj;
+ if (obj->closed_p) return 0;
+
+ /* Close off the decoder, to cause it to give up any buffered data that
+ it is still holding.
+ */
+ if (leaf->decoder_data) {
+ int status = MimeLeaf_close_decoder(obj);
+ if (status < 0) return status;
+ }
+
+ /* Now run the superclass's parse_eof, which will force out the line
+ buffer (which we may have just repopulated, above.)
+ */
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+}
+
+static bool MimeLeaf_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs) {
+ return true;
+}
diff --git a/comm/mailnews/mime/src/mimeleaf.h b/comm/mailnews/mime/src/mimeleaf.h
new file mode 100644
index 0000000000..cca651e3d0
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeleaf.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMELEAF_H_
+#define _MIMELEAF_H_
+
+#include "mimeobj.h"
+#include "modmimee.h"
+
+/* MimeLeaf is the class for the objects representing all MIME types which
+ are not containers for other MIME objects. The implication of this is
+ that they are MIME types which can have Content-Transfer-Encodings
+ applied to their data. This class provides that service in its
+ parse_buffer() method:
+
+ int (*parse_decoded_buffer) (const char *buf, int32_t size, MimeObject
+ *obj)
+
+ The `parse_buffer' method of MimeLeaf passes each block of data through
+ the appropriate decoder (if any) and then calls `parse_decoded_buffer'
+ on each block (not line) of output.
+
+ The default `parse_decoded_buffer' method of MimeLeaf line-buffers the
+ now-decoded data, handing each line to the `parse_line' method in turn.
+ If different behavior is desired (for example, if a class wants access
+ to the decoded data before it is line-buffered) the `parse_decoded_buffer'
+ method should be overridden. (MimeExternalObject does this.)
+ */
+
+typedef struct MimeLeafClass MimeLeafClass;
+typedef struct MimeLeaf MimeLeaf;
+
+struct MimeLeafClass {
+ MimeObjectClass object;
+ /* This is the callback that is handed to the decoder. */
+ int (*parse_decoded_buffer)(const char* buf, int32_t size, MimeObject* obj);
+ int (*close_decoder)(MimeObject* obj);
+};
+
+extern MimeLeafClass mimeLeafClass;
+
+struct MimeLeaf {
+ MimeObject object; /* superclass variables */
+
+ /* If we're doing Base64, Quoted-Printable, or UU decoding, this is the
+ state object for the decoder. */
+ MimeDecoderData* decoder_data;
+
+ /* We want to count the size of the MimeObject to offer consumers the
+ * opportunity to display the sizes of attachments.
+ */
+ int sizeSoFar;
+};
+
+#define MimeLeafClassInitializer(ITYPE, CSUPER) \
+ { MimeObjectClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMELEAF_H_ */
diff --git a/comm/mailnews/mime/src/mimemalt.cpp b/comm/mailnews/mime/src/mimemalt.cpp
new file mode 100644
index 0000000000..82e56ee6e8
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemalt.cpp
@@ -0,0 +1,549 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ BACKGROUND
+ ----------
+
+ At the simplest level, multipart/alternative means "pick one of these and
+ display it." However, it's actually a lot more complicated than that.
+
+ The alternatives are in preference order, and counterintuitively, they go
+ from *least* to *most* preferred rather than the reverse. Therefore, when
+ we're parsing, we can't just take the first one we like and throw the rest
+ away -- we have to parse through the whole thing, discarding the n'th part if
+ we are capable of displaying the n+1'th.
+
+ Adding a wrinkle to that is the fact that we give the user the option of
+ demanding the plain-text alternative even though we are perfectly capable of
+ displaying the HTML, and it is almost always the preferred format, i.e., it
+ almost always comes after the plain-text alternative.
+
+ Speaking of which, you can't assume that each of the alternatives is just a
+ basic text/[whatever]. There may be, for example, a text/plain followed by a
+ multipart/related which contains text/html and associated embedded
+ images. Yikes!
+
+ You also can't assume that there will be just two parts. There can be an
+ arbitrary number, and the ones we are capable of displaying and the ones we
+ aren't could be interspersed in any order by the producer of the MIME.
+
+ We can't just throw away the parts we're not displaying when we're processing
+ the MIME for display. If we were to do that, then the MIME parts that
+ remained wouldn't get numbered properly, and that would mean, for example,
+ that deleting attachments wouldn't work in some messages. Indeed, that very
+ problem is what prompted a rewrite of this file into its current
+ architecture.
+
+ ARCHITECTURE
+ ------------
+
+ Parts are read and queued until we know whether we're going to display
+ them. If the first pending part is one we don't know how to display, then we
+ can add it to the MIME structure immediately, with output_p disabled. If the
+ first pending part is one we know how to display, then we can't add it to the
+ in-memory MIME structure until either (a) we encounter a later, more
+ preferred part we know how to display, or (b) we reach the end of the
+ parts. A display-capable part of the queue may be followed by one or more
+ display-incapable parts. We can't add them to the in-memory structure until
+ we figure out what to do with the first, display-capable pending part,
+ because otherwise the order and numbering will be wrong. All of the logic in
+ this paragraph is implemented in the flush_children function.
+
+ The display_cached_part function is what actually adds a MIME part to the
+ in-memory MIME structure. There is one complication there which forces us to
+ violate abstrations... Even if we set output_p on a child before adding it to
+ the parent, the parse_begin function resets it. The kluge I came up with to
+ prevent that was to give the child a separate options object and set
+ output_fn to nullptr in it, because that causes parse_begin to set output_p to
+ false. This seemed like the least onerous way to accomplish this, although I
+ can't say it's a solution I'm particularly fond of.
+
+ Another complication in display_cached_part is that if we were just a normal
+ multipart type, we could rely on MimeMultipart_parse_line to notify emitters
+ about content types, character sets, part numbers, etc. as our new children
+ get created. However, since we defer creation of some children, the
+ notification doesn't happen there, so we have to handle it
+ ourselves. Unfortunately, this requires a small abstraction violation in
+ MimeMultipart_parse_line -- we have to check there if the entity is
+ multipart/alternative and if so not notify emitters there because
+ MimeMultipartAlternative_create_child handles it.
+
+ - Jonathan Kamens, 2010-07-23
+
+ When the option prefer_plaintext is on, the last text/plain part
+ should be preferred over any other part that can be displayed. But
+ if no text/plain part is found, then the algorithm should go as
+ normal and convert any html part found to text. To achieve this I
+ found that the simplest way was to change the function display_part_p
+ into returning priority as an integer instead of boolean can/can't
+ display. Then I also changed the function flush_children so it selects
+ the last part with the highest priority. (Priority 0 means it cannot
+ be displayed and the part is never chosen.)
+
+ - Terje Bråten, 2013-02-16
+*/
+
+#include "mimemalt.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+#include "nsMimeStringResources.h"
+#include "nsIPrefBranch.h"
+#include "mimemoz2.h" // for prefs
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+extern "C" MimeObjectClass mimeMultipartRelatedClass;
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartAlternative, MimeMultipartAlternativeClass,
+ mimeMultipartAlternativeClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartAlternative_initialize(MimeObject*);
+static void MimeMultipartAlternative_finalize(MimeObject*);
+static int MimeMultipartAlternative_parse_eof(MimeObject*, bool);
+static int MimeMultipartAlternative_create_child(MimeObject*);
+static int MimeMultipartAlternative_parse_child_line(MimeObject*, const char*,
+ int32_t, bool);
+static int MimeMultipartAlternative_close_child(MimeObject*);
+
+static int MimeMultipartAlternative_flush_children(MimeObject*, bool,
+ priority_t);
+static priority_t MimeMultipartAlternative_display_part_p(
+ MimeObject* self, MimeHeaders* sub_hdrs);
+static priority_t MimeMultipartAlternative_prioritize_part(
+ char* content_type, bool prefer_plaintext);
+
+static int MimeMultipartAlternative_display_cached_part(MimeObject*,
+ MimeHeaders*,
+ MimePartBufferData*,
+ bool);
+
+static int MimeMultipartAlternativeClassInitialize(
+ MimeMultipartAlternativeClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeMultipartAlternative_initialize;
+ oclass->finalize = MimeMultipartAlternative_finalize;
+ oclass->parse_eof = MimeMultipartAlternative_parse_eof;
+ mclass->create_child = MimeMultipartAlternative_create_child;
+ mclass->parse_child_line = MimeMultipartAlternative_parse_child_line;
+ mclass->close_child = MimeMultipartAlternative_close_child;
+ return 0;
+}
+
+static int MimeMultipartAlternative_initialize(MimeObject* obj) {
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+
+ NS_ASSERTION(!malt->part_buffers, "object initialized multiple times");
+ NS_ASSERTION(!malt->buffered_hdrs, "object initialized multiple times");
+ malt->pending_parts = 0;
+ malt->max_parts = 0;
+ malt->buffered_priority = PRIORITY_UNDISPLAYABLE;
+ malt->buffered_hdrs = nullptr;
+ malt->part_buffers = nullptr;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj);
+}
+
+static void MimeMultipartAlternative_cleanup(MimeObject* obj) {
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+ int32_t i;
+
+ for (i = 0; i < malt->pending_parts; i++) {
+ MimeHeaders_free(malt->buffered_hdrs[i]);
+ MimePartBufferDestroy(malt->part_buffers[i]);
+ }
+ PR_FREEIF(malt->buffered_hdrs);
+ PR_FREEIF(malt->part_buffers);
+ malt->pending_parts = 0;
+ malt->max_parts = 0;
+}
+
+static void MimeMultipartAlternative_finalize(MimeObject* obj) {
+ MimeMultipartAlternative_cleanup(obj);
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeMultipartAlternative_flush_children(MimeObject* obj,
+ bool finished,
+ priority_t next_priority) {
+ /*
+ The cache should always have at the head the part with highest priority.
+
+ Possible states:
+
+ 1. Cache contains nothing: do nothing.
+
+ 2. Finished, and the cache contains one displayable body followed
+ by zero or more bodies with lower priority:
+
+ 3. Finished, and the cache contains one non-displayable body:
+ create it with output off.
+
+ 4. Not finished, and the cache contains one displayable body
+ followed by zero or more bodies with lower priority, and the new
+ body we're about to create is higher or equal priority:
+ create all cached bodies with output off.
+
+ 5. Not finished, and the cache contains one displayable body
+ followed by zero or more bodies with lower priority, and the new
+ body we're about to create has lower priority: do nothing.
+
+ 6. Not finished, and the cache contains one non-displayable body:
+ create it with output off.
+ */
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+ bool have_displayable, do_flush, do_display;
+
+ /* Case 1 */
+ if (!malt->pending_parts) return 0;
+
+ have_displayable = (malt->buffered_priority > next_priority);
+
+ if (finished && have_displayable) {
+ /* Case 2 */
+ do_flush = true;
+ do_display = true;
+ } else if (finished && !have_displayable) {
+ /* Case 3 */
+ do_flush = true;
+ do_display = false;
+ } else if (!finished && have_displayable) {
+ /* Case 5 */
+ do_flush = false;
+ do_display = false;
+ } else if (!finished && !have_displayable) {
+ /* Case 4 */
+ /* Case 6 */
+ do_flush = true;
+ do_display = false;
+ } else {
+ NS_ERROR("mimemalt.cpp: logic error in flush_children");
+ return -1;
+ }
+
+ if (do_flush) {
+ for (int32_t i = 0; i < malt->pending_parts; i++) {
+ MimeHeaders* hdrs = malt->buffered_hdrs[i];
+ char* ct =
+ (hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, true, false) : 0);
+ bool display_part =
+ (i == 0) || (ct && !PL_strncasecmp(ct, "text/calendar", 13));
+ MimeMultipartAlternative_display_cached_part(obj, malt->buffered_hdrs[i],
+ malt->part_buffers[i],
+ do_display && display_part);
+ MimeHeaders_free(malt->buffered_hdrs[i]);
+ MimePartBufferDestroy(malt->part_buffers[i]);
+ }
+ malt->pending_parts = 0;
+ }
+ return 0;
+}
+
+static int MimeMultipartAlternative_parse_eof(MimeObject* obj, bool abort_p) {
+ int status = 0;
+
+ if (obj->closed_p) return 0;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ status = MimeMultipartAlternative_flush_children(obj, true,
+ PRIORITY_UNDISPLAYABLE);
+ if (status < 0) return status;
+
+ MimeMultipartAlternative_cleanup(obj);
+
+ return status;
+}
+
+static int MimeMultipartAlternative_create_child(MimeObject* obj) {
+ if (obj->options) obj->options->is_child = true;
+
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+
+ priority_t priority =
+ MimeMultipartAlternative_display_part_p(obj, mult->hdrs);
+
+ MimeMultipartAlternative_flush_children(obj, false, priority);
+
+ mult->state = MimeMultipartPartFirstLine;
+
+ int32_t pending_parts = malt->pending_parts;
+ int32_t max_parts = malt->max_parts;
+
+ int32_t i = pending_parts++;
+
+ if (i == 0) {
+ malt->buffered_priority = priority;
+ }
+
+ if (pending_parts > max_parts) {
+ max_parts = pending_parts;
+ MimeHeaders** newBuf = (MimeHeaders**)PR_REALLOC(
+ malt->buffered_hdrs, max_parts * sizeof(*malt->buffered_hdrs));
+ NS_ENSURE_TRUE(newBuf, MIME_OUT_OF_MEMORY);
+ malt->buffered_hdrs = newBuf;
+
+ MimePartBufferData** newBuf2 = (MimePartBufferData**)PR_REALLOC(
+ malt->part_buffers, max_parts * sizeof(*malt->part_buffers));
+ NS_ENSURE_TRUE(newBuf2, MIME_OUT_OF_MEMORY);
+ malt->part_buffers = newBuf2;
+ }
+
+ malt->buffered_hdrs[i] = MimeHeaders_copy(mult->hdrs);
+ NS_ENSURE_TRUE(malt->buffered_hdrs[i], MIME_OUT_OF_MEMORY);
+
+ malt->part_buffers[i] = MimePartBufferCreate();
+ NS_ENSURE_TRUE(malt->part_buffers[i], MIME_OUT_OF_MEMORY);
+
+ malt->pending_parts = pending_parts;
+ malt->max_parts = max_parts;
+ return 0;
+}
+
+static int MimeMultipartAlternative_parse_child_line(MimeObject* obj,
+ const char* line,
+ int32_t length,
+ bool first_line_p) {
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+
+ NS_ASSERTION(malt->pending_parts,
+ "should be pending parts, but there aren't");
+ if (!malt->pending_parts) return -1;
+ int32_t i = malt->pending_parts - 1;
+
+ /* Push this line into the buffer for later retrieval. */
+ return MimePartBufferWrite(malt->part_buffers[i], line, length);
+}
+
+static int MimeMultipartAlternative_close_child(MimeObject* obj) {
+ MimeMultipartAlternative* malt = (MimeMultipartAlternative*)obj;
+ MimeMultipart* mult = (MimeMultipart*)obj;
+
+ /* PR_ASSERT(malt->part_buffer); Some Mac brokenness trips this...
+ if (!malt->part_buffer) return -1; */
+
+ if (malt->pending_parts)
+ MimePartBufferClose(malt->part_buffers[malt->pending_parts - 1]);
+
+ /* PR_ASSERT(mult->hdrs); I expect the Mac trips this too */
+
+ if (mult->hdrs) {
+ MimeHeaders_free(mult->hdrs);
+ mult->hdrs = 0;
+ }
+
+ return 0;
+}
+
+static priority_t MimeMultipartAlternative_display_part_p(
+ MimeObject* self, MimeHeaders* sub_hdrs) {
+ priority_t priority = PRIORITY_UNDISPLAYABLE;
+ char* ct = MimeHeaders_get(sub_hdrs, HEADER_CONTENT_TYPE, true, false);
+ if (!ct) return priority;
+
+ /* RFC 1521 says:
+ Receiving user agents should pick and display the last format
+ they are capable of displaying. In the case where one of the
+ alternatives is itself of type "multipart" and contains unrecognized
+ sub-parts, the user agent may choose either to show that alternative,
+ an earlier alternative, or both.
+ */
+
+ // We must pass 'true' as last parameter so that text/calendar is
+ // only displayable when Lightning is installed.
+ MimeObjectClass* clazz = mime_find_class(ct, sub_hdrs, self->options, true);
+ if (clazz && clazz->displayable_inline_p(clazz, sub_hdrs)) {
+ // prefer_plaintext pref
+ bool prefer_plaintext = false;
+ nsIPrefBranch* prefBranch = GetPrefBranch(self->options);
+ if (prefBranch) {
+ prefBranch->GetBoolPref("mailnews.display.prefer_plaintext",
+ &prefer_plaintext);
+ }
+ prefer_plaintext =
+ prefer_plaintext &&
+ (self->options->format_out != nsMimeOutput::nsMimeMessageSaveAs) &&
+ (self->options->format_out != nsMimeOutput::nsMimeMessageRaw);
+
+ priority = MimeMultipartAlternative_prioritize_part(ct, prefer_plaintext);
+ }
+
+ PR_FREEIF(ct);
+ return priority;
+}
+
+/**
+ * RFC 1521 says we should display the last format we are capable of displaying.
+ * But for various reasons (mainly to improve the user experience) we choose
+ * to ignore that in some cases, and rather pick one that we prioritize.
+ */
+static priority_t MimeMultipartAlternative_prioritize_part(
+ char* content_type, bool prefer_plaintext) {
+ /*
+ * PRIORITY_NORMAL is the priority of text/html, multipart/..., etc. that
+ * we normally display. We should try to have as few exceptions from
+ * PRIORITY_NORMAL as possible
+ */
+
+ /* (with no / in the type) */
+ if (!PL_strcasecmp(content_type, "text")) {
+ if (prefer_plaintext) {
+ /* When in plain text view, a plain text part is what we want. */
+ return PRIORITY_HIGH;
+ }
+ /* We normally prefer other parts over the unspecified text type. */
+ return PRIORITY_TEXT_UNKNOWN;
+ }
+
+ if (!PL_strncasecmp(content_type, "text/", 5)) {
+ char* text_type = content_type + 5;
+
+ if (!PL_strncasecmp(text_type, "plain", 5)) {
+ if (prefer_plaintext) {
+ /* When in plain text view,
+ the text/plain part is exactly what we want */
+ return PRIORITY_HIGHEST;
+ }
+ /*
+ * Because the html and the text part may be switched,
+ * or we have an extra text/plain added by f.ex. a buggy virus checker,
+ * we prioritize text/plain lower than normal.
+ */
+ return PRIORITY_TEXT_PLAIN;
+ }
+
+ /* Need to white-list all text/... types that are or could be implemented.
+ */
+ if (!PL_strncasecmp(text_type, "html", 4) ||
+ !PL_strncasecmp(text_type, "enriched", 8) ||
+ !PL_strncasecmp(text_type, "richtext", 8) ||
+ !PL_strncasecmp(text_type, "rtf", 3)) {
+ return PRIORITY_HIGH;
+ }
+
+ if (!PL_strncasecmp(text_type, "calendar", 8)) {
+ // Prioritise text/calendar below text/html, etc. since we always show
+ // it anyway.
+ return PRIORITY_NORMAL;
+ }
+
+ /* We prefer other parts over unknown text types. */
+ return PRIORITY_TEXT_UNKNOWN;
+ }
+
+ // Guard against rogue messages with incorrect MIME structure and
+ // don't show images when plain text is requested.
+ if (!PL_strncasecmp(content_type, "image", 5)) {
+ if (prefer_plaintext)
+ return PRIORITY_UNDISPLAYABLE;
+ else
+ return PRIORITY_LOW;
+ }
+
+ return PRIORITY_NORMAL;
+}
+
+static int MimeMultipartAlternative_display_cached_part(
+ MimeObject* obj, MimeHeaders* hdrs, MimePartBufferData* buffer,
+ bool do_display) {
+ int status;
+ bool old_options_no_output_p;
+
+ char* ct =
+ (hdrs ? MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, true, false) : 0);
+ const char* dct = (((MimeMultipartClass*)obj->clazz)->default_part_type);
+ MimeObject* body;
+ /** Don't pass in NULL as the content-type (this means that the
+ * auto-uudecode-hack won't ever be done for subparts of a
+ * multipart, but only for untyped children of message/rfc822.
+ */
+ const char* uct = (ct && *ct) ? ct : (dct ? dct : TEXT_PLAIN);
+
+ // We always want to display the cached part inline.
+ body = mime_create(uct, hdrs, obj->options, true);
+ PR_FREEIF(ct);
+ if (!body) return MIME_OUT_OF_MEMORY;
+ body->output_p = do_display;
+
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, body);
+ if (status < 0) {
+ mime_free(body);
+ return status;
+ }
+ /* add_child assigns body->options from obj->options, but that's
+ just a pointer so if we muck with it in the child it'll modify
+ the parent as well, which we definitely don't want. Therefore we
+ need to make a copy of the old value and restore it later. */
+ old_options_no_output_p = obj->options->no_output_p;
+ if (!do_display) body->options->no_output_p = true;
+
+#ifdef MIME_DRAFTS
+ /* if this object is a child of a multipart/related object, the parent is
+ taking care of decomposing the whole part, don't need to do it at this
+ level. However, we still have to call decompose_file_init_fn and
+ decompose_file_close_fn in order to set the correct content-type. But don't
+ call MimePartBufferRead
+ */
+ bool multipartRelatedChild =
+ mime_typep(obj->parent, (MimeObjectClass*)&mimeMultipartRelatedClass);
+ bool decomposeFile = do_display && obj->options &&
+ obj->options->decompose_file_p &&
+ obj->options->decompose_file_init_fn &&
+ !mime_typep(body, (MimeObjectClass*)&mimeMultipartClass);
+
+ if (decomposeFile) {
+ status = obj->options->decompose_file_init_fn(obj->options->stream_closure,
+ hdrs);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ /* Now that we've added this new object to our list of children,
+ notify emitters and start its parser going. */
+ MimeMultipart_notify_emitter(body);
+
+ status = body->clazz->parse_begin(body);
+ if (status < 0) return status;
+
+#ifdef MIME_DRAFTS
+ if (decomposeFile && !multipartRelatedChild)
+ status = MimePartBufferRead(buffer, obj->options->decompose_file_output_fn,
+ obj->options->stream_closure);
+ else
+#endif /* MIME_DRAFTS */
+
+ status = MimePartBufferRead(
+ buffer,
+ /* The MimeConverterOutputCallback cast is to turn the
+ `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)body->clazz->parse_buffer), body);
+
+ if (status < 0) return status;
+
+ /* Done parsing. */
+ status = body->clazz->parse_eof(body, false);
+ if (status < 0) return status;
+ status = body->clazz->parse_end(body, false);
+ if (status < 0) return status;
+
+#ifdef MIME_DRAFTS
+ if (decomposeFile) {
+ status =
+ obj->options->decompose_file_close_fn(obj->options->stream_closure);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ /* Restore options to what parent classes expects. */
+ obj->options->no_output_p = old_options_no_output_p;
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimemalt.h b/comm/mailnews/mime/src/mimemalt.h
new file mode 100644
index 0000000000..622704d8f4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemalt.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMALT_H_
+#define _MIMEMALT_H_
+
+#include "mimemult.h"
+#include "mimepbuf.h"
+
+/* The MimeMultipartAlternative class implements the multipart/alternative
+ MIME container, which displays only one (the `best') of a set of enclosed
+ documents.
+ */
+
+typedef struct MimeMultipartAlternativeClass MimeMultipartAlternativeClass;
+typedef struct MimeMultipartAlternative MimeMultipartAlternative;
+
+struct MimeMultipartAlternativeClass {
+ MimeMultipartClass multipart;
+};
+
+extern "C" MimeMultipartAlternativeClass mimeMultipartAlternativeClass;
+
+enum priority_t {
+ PRIORITY_UNDISPLAYABLE,
+ PRIORITY_LOW,
+ PRIORITY_TEXT_UNKNOWN,
+ PRIORITY_TEXT_PLAIN,
+ PRIORITY_NORMAL,
+ PRIORITY_HIGH,
+ PRIORITY_HIGHEST
+};
+
+struct MimeMultipartAlternative {
+ MimeMultipart multipart; /* superclass variables */
+
+ MimeHeaders** buffered_hdrs; /* The headers of pending parts */
+ MimePartBufferData** part_buffers; /* The data of pending parts
+ (see mimepbuf.h) */
+ int32_t pending_parts;
+ int32_t max_parts;
+ priority_t buffered_priority; /* Priority of head of pending parts */
+};
+
+#define MimeMultipartAlternativeClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMALT_H_ */
diff --git a/comm/mailnews/mime/src/mimemapl.cpp b/comm/mailnews/mime/src/mimemapl.cpp
new file mode 100644
index 0000000000..4fca1defe1
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemapl.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "mimemapl.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "nsMimeTypes.h"
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartAppleDouble, MimeMultipartAppleDoubleClass,
+ mimeMultipartAppleDoubleClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartAppleDouble_parse_begin(MimeObject*);
+static bool MimeMultipartAppleDouble_output_child_p(MimeObject*, MimeObject*);
+
+static int MimeMultipartAppleDoubleClassInitialize(
+ MimeMultipartAppleDoubleClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+
+ NS_ASSERTION(!oclass->class_initialized, "mime class not initialized");
+ oclass->parse_begin = MimeMultipartAppleDouble_parse_begin;
+ mclass->output_child_p = MimeMultipartAppleDouble_output_child_p;
+ return 0;
+}
+
+static int MimeMultipartAppleDouble_parse_begin(MimeObject* obj) {
+ /* #### This method is identical to MimeExternalObject_parse_begin
+ which kinda s#$%s...
+ */
+ int status;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ /* If we're writing this object, and we're doing it in raw form, then
+ now is the time to inform the backend what the type of this data is.
+ */
+ if (obj->output_p && obj->options && !obj->options->write_html_p &&
+ !obj->options->state->first_data_written_p) {
+ status = MimeObject_output_init(obj, 0);
+ if (status < 0) return status;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "first data not written");
+ }
+
+#ifdef XP_MACOSX
+ if (obj->options && obj->options->state) {
+ // obj->options->state->separator_suppressed_p = true;
+ goto done;
+ }
+ /*
+ * It would be nice to not showing the resource fork links
+ * if we are displaying inline. But, there is no way we could
+ * know ahead of time that we could display the data fork and
+ * the data fork is always hidden on MAC platform.
+ */
+#endif
+ /* If we're writing this object as HTML, then emit a link for the
+ multipart/appledouble part (both links) that looks just like the
+ links that MimeExternalObject emits for external leaf parts.
+ */
+ if (obj->options && obj->output_p && obj->options->write_html_p &&
+ obj->options->output_fn) {
+ char* id = 0;
+ char* id_url = 0;
+ char* id_imap = 0;
+
+ id = mime_part_address(obj);
+ if (!id) return MIME_OUT_OF_MEMORY;
+ if (obj->options->missing_parts) id_imap = mime_imap_part_address(obj);
+
+ if (obj->options && obj->options->url) {
+ const char* url = obj->options->url;
+ if (id_imap && id) {
+ /* if this is an IMAP part. */
+ id_url = mime_set_url_imap_part(url, id_imap, id);
+ } else {
+ /* This is just a normal MIME part as usual. */
+ id_url = mime_set_url_part(url, id, true);
+ }
+ if (!id_url) {
+ PR_Free(id);
+ return MIME_OUT_OF_MEMORY;
+ }
+ }
+
+ /**********************
+ if (!strcmp (id, "0"))
+ {
+ PR_Free(id);
+ id = MimeGetStringByID(MIME_MSG_ATTACHMENT);
+ }
+ else
+ {
+ const char *p = "Part ";
+ char *s = (char *)PR_MALLOC(strlen(p) + strlen(id) + 1);
+ if (!s)
+ {
+ PR_Free(id);
+ PR_Free(id_url);
+ return MIME_OUT_OF_MEMORY;
+ }
+ PL_strcpy(s, p);
+ PL_strcat(s, id);
+ PR_Free(id);
+ id = s;
+ }
+
+ if (all_headers_p &&
+ // Don't bother showing all headers on this part if it's the only
+ // part in the message: in that case, we've already shown these
+ // headers.
+ obj->options->state &&
+ obj->options->state->root == obj->parent)
+ all_headers_p = false;
+
+ newopt.fancy_headers_p = true;
+ newopt.headers = (all_headers_p ? MimeHeadersAll : MimeHeadersSome);
+
+ //
+ RICHIE SHERRY
+ GOTTA STILL DO THIS FOR QUOTING!
+ status = MimeHeaders_write_attachment_box(obj->headers, &newopt,
+ obj->content_type,
+ obj->encoding,
+ id_name? id_name : id, id_url, 0)
+ //
+ *********************************************************************************/
+
+ // FAIL:
+ PR_FREEIF(id);
+ PR_FREEIF(id_url);
+ PR_FREEIF(id_imap);
+ if (status < 0) return status;
+ }
+
+#ifdef XP_MACOSX
+done:
+#endif
+
+ return 0;
+}
+
+static bool MimeMultipartAppleDouble_output_child_p(MimeObject* obj,
+ MimeObject* child) {
+ MimeContainer* cont = (MimeContainer*)obj;
+
+ /* If this is the first child, and it's an application/applefile, then
+ don't emit a link for it. (There *should* be only two children, and
+ the first one should always be an application/applefile.)
+ */
+
+ if (cont->nchildren >= 1 && cont->children[0] == child &&
+ child->content_type &&
+ !PL_strcasecmp(child->content_type, APPLICATION_APPLEFILE)) {
+#ifdef XP_MACOSX
+ if (obj->output_p && obj->options &&
+ obj->options->write_html_p) // output HTML
+ return false;
+#else
+ /* if we are not on a Macintosh, don't emitte the resources fork at all. */
+ return false;
+#endif
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/mime/src/mimemapl.h b/comm/mailnews/mime/src/mimemapl.h
new file mode 100644
index 0000000000..28d29bf5f7
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemapl.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMAPL_H_
+#define _MIMEMAPL_H_
+
+#include "mimemult.h"
+
+/* The MimeMultipartAppleDouble class implements the multipart/appledouble
+ MIME container, which provides a method of encapsulating and reconstructing
+ a two-forked Macintosh file.
+ */
+
+typedef struct MimeMultipartAppleDoubleClass MimeMultipartAppleDoubleClass;
+typedef struct MimeMultipartAppleDouble MimeMultipartAppleDouble;
+
+struct MimeMultipartAppleDoubleClass {
+ MimeMultipartClass multipart;
+};
+
+extern MimeMultipartAppleDoubleClass mimeMultipartAppleDoubleClass;
+
+struct MimeMultipartAppleDouble {
+ MimeMultipart multipart;
+};
+
+#define MimeMultipartAppleDoubleClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMAPL_H_ */
diff --git a/comm/mailnews/mime/src/mimemcms.cpp b/comm/mailnews/mime/src/mimemcms.cpp
new file mode 100644
index 0000000000..d2e5b18f19
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemcms.cpp
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsICMSMessage.h"
+#include "nsICMSMessageErrors.h"
+#include "nsICMSDecoder.h"
+#include "nsICryptoHash.h"
+#include "mimemcms.h"
+#include "mimecryp.h"
+#include "nsMimeTypes.h"
+#include "nspr.h"
+#include "nsMimeStringResources.h"
+#include "mimemsg.h"
+#include "mimemoz2.h"
+#include "nsIURI.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgSMIMEHeaderSink.h"
+#include "nsCOMPtr.h"
+#include "nsIX509Cert.h"
+#include "plstr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIMailChannel.h"
+
+#define MIME_SUPERCLASS mimeMultipartSignedClass
+MimeDefClass(MimeMultipartSignedCMS, MimeMultipartSignedCMSClass,
+ mimeMultipartSignedCMSClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartSignedCMS_initialize(MimeObject*);
+
+static void* MimeMultCMS_init(MimeObject*);
+static int MimeMultCMS_data_hash(const char*, int32_t, void*);
+static int MimeMultCMS_sig_hash(const char*, int32_t, void*);
+static int MimeMultCMS_data_eof(void*, bool);
+static int MimeMultCMS_sig_eof(void*, bool);
+static int MimeMultCMS_sig_init(void*, MimeObject*, MimeHeaders*);
+static char* MimeMultCMS_generate(void*);
+static void MimeMultCMS_free(void*);
+static void MimeMultCMS_suppressed_child(void* crypto_closure);
+
+extern int SEC_ERROR_CERT_ADDR_MISMATCH;
+
+static int MimeMultipartSignedCMSClassInitialize(
+ MimeMultipartSignedCMSClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartSignedClass* sclass = (MimeMultipartSignedClass*)clazz;
+
+ oclass->initialize = MimeMultipartSignedCMS_initialize;
+
+ sclass->crypto_init = MimeMultCMS_init;
+ sclass->crypto_data_hash = MimeMultCMS_data_hash;
+ sclass->crypto_data_eof = MimeMultCMS_data_eof;
+ sclass->crypto_signature_init = MimeMultCMS_sig_init;
+ sclass->crypto_signature_hash = MimeMultCMS_sig_hash;
+ sclass->crypto_signature_eof = MimeMultCMS_sig_eof;
+ sclass->crypto_generate_html = MimeMultCMS_generate;
+ sclass->crypto_notify_suppressed_child = MimeMultCMS_suppressed_child;
+ sclass->crypto_free = MimeMultCMS_free;
+
+ PR_ASSERT(!oclass->class_initialized);
+ return 0;
+}
+
+static int MimeMultipartSignedCMS_initialize(MimeObject* object) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+typedef struct MimeMultCMSdata {
+ int16_t hash_type;
+ nsCOMPtr<nsICryptoHash> data_hash_context;
+ nsCOMPtr<nsICMSDecoder> sig_decoder_context;
+ nsCOMPtr<nsICMSMessage> content_info;
+ char* sender_addr;
+ bool decoding_failed;
+ bool reject_signature;
+ unsigned char* item_data;
+ uint32_t item_len;
+ MimeObject* self;
+ nsCOMPtr<nsIMsgSMIMEHeaderSink> smimeHeaderSink;
+ nsCString url;
+
+ MimeMultCMSdata()
+ : hash_type(0),
+ sender_addr(nullptr),
+ decoding_failed(false),
+ reject_signature(false),
+ item_data(nullptr),
+ self(nullptr) {}
+
+ ~MimeMultCMSdata() {
+ PR_FREEIF(sender_addr);
+
+ // Do a graceful shutdown of the nsICMSDecoder and release the nsICMSMessage
+ if (sig_decoder_context) {
+ nsCOMPtr<nsICMSMessage> cinfo;
+ sig_decoder_context->Finish(getter_AddRefs(cinfo));
+ }
+
+ delete[] item_data;
+ }
+} MimeMultCMSdata;
+
+/* #### MimeEncryptedCMS and MimeMultipartSignedCMS have a sleazy,
+ incestuous, dysfunctional relationship. */
+extern bool MimeAnyParentCMSSigned(MimeObject* obj);
+extern void MimeCMSGetFromSender(MimeObject* obj, nsCString& from_addr,
+ nsCString& from_name, nsCString& sender_addr,
+ nsCString& sender_name, nsCString& msg_date);
+extern void MimeCMSRequestAsyncSignatureVerification(
+ nsICMSMessage* aCMSMsg, const char* aFromAddr, const char* aFromName,
+ const char* aSenderAddr, const char* aSenderName, const char* aMsgDate,
+ nsIMsgSMIMEHeaderSink* aHeaderSink, int32_t aMimeNestingLevel,
+ const nsCString& aMsgNeckoURL, const nsCString& aOriginMimePartNumber,
+ const nsTArray<uint8_t>& aDigestData, int16_t aDigestType);
+extern char* MimeCMS_MakeSAURL(MimeObject* obj);
+extern char* IMAP_CreateReloadAllPartsUrl(const char* url);
+extern int MIMEGetRelativeCryptoNestLevel(MimeObject* obj);
+
+static void* MimeMultCMS_init(MimeObject* obj) {
+ MimeHeaders* hdrs = obj->headers;
+ MimeMultCMSdata* data = 0;
+ char *ct, *micalg;
+ int16_t hash_type;
+ nsresult rv;
+
+ data = new MimeMultCMSdata;
+ if (!data) return 0;
+
+ data->self = obj;
+
+ mime_stream_data* msd =
+ (mime_stream_data*)(data->self->options->stream_closure);
+ if (msd) {
+ nsIChannel* channel = msd->channel; // note the lack of ref counting...
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ rv = uri->GetSpec(data->url);
+
+ // We only want to update the UI if the current mime transaction
+ // is intended for display.
+ // If the current transaction is intended for background processing,
+ // we can learn that by looking at the additional header=filter
+ // string contained in the URI.
+ //
+ // If we find something, we do not set smimeHeaderSink,
+ // which will prevent us from giving UI feedback.
+ //
+ // If we do not find header=filter, we assume the result of the
+ // processing will be shown in the UI.
+
+ if (!strstr(data->url.get(), "?header=filter") &&
+ !strstr(data->url.get(), "&header=filter") &&
+ !strstr(data->url.get(), "?header=attach") &&
+ !strstr(data->url.get(), "&header=attach")) {
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(channel);
+ if (mailChannel) {
+ mailChannel->GetSmimeHeaderSink(
+ getter_AddRefs(data->smimeHeaderSink));
+ }
+ }
+ }
+ } // if channel
+ } // if msd
+
+ if (obj->parent && MimeAnyParentCMSSigned(obj)) {
+ // Parent is signed. We know this part is a signature, too, because
+ // multipart doesn't allow encryption.
+ // We don't support "inner sign" with outer sign, because the
+ // inner encrypted part could have been produced by an attacker who
+ // stripped away a part containing the signature (S/MIME doesn't
+ // have integrity protection).
+ // Also we don't want to support sign-then-sign, that's misleading,
+ // which part would be shown as having a signature?
+ // We skip signature verfication, and return bad signature status.
+
+ // Note: We must return a valid pointer to ourselve's data,
+ // otherwise the parent will attempt to re-init us.
+
+ data->reject_signature = true;
+ if (data->smimeHeaderSink) {
+ int aRelativeNestLevel = MIMEGetRelativeCryptoNestLevel(data->self);
+ nsAutoCString partnum;
+ partnum.Adopt(mime_part_address(data->self));
+ data->smimeHeaderSink->SignedStatus(aRelativeNestLevel,
+ nsICMSMessageErrors::GENERAL_ERROR,
+ nullptr, data->url, partnum);
+ }
+ return data;
+ }
+
+ ct = MimeHeaders_get(hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (!ct) {
+ delete data;
+ return 0; /* #### bogus message? out of memory? */
+ }
+ micalg = MimeHeaders_get_parameter(ct, PARAM_MICALG, NULL, NULL);
+ PR_Free(ct);
+ ct = 0;
+ if (!micalg) {
+ delete data;
+ return 0; /* #### bogus message? out of memory? */
+ }
+
+ bool allowSha1 = mozilla::Preferences::GetBool(
+ "mail.smime.accept_insecure_sha1_message_signatures", false);
+
+ if (allowSha1 && (!PL_strcasecmp(micalg, PARAM_MICALG_SHA1) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_3) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_4) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA1_5)))
+ hash_type = nsICryptoHash::SHA1;
+ else if (!PL_strcasecmp(micalg, PARAM_MICALG_SHA256) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA256_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA256_3))
+ hash_type = nsICryptoHash::SHA256;
+ else if (!PL_strcasecmp(micalg, PARAM_MICALG_SHA384) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA384_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA384_3))
+ hash_type = nsICryptoHash::SHA384;
+ else if (!PL_strcasecmp(micalg, PARAM_MICALG_SHA512) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA512_2) ||
+ !PL_strcasecmp(micalg, PARAM_MICALG_SHA512_3))
+ hash_type = nsICryptoHash::SHA512;
+ else {
+ data->reject_signature = true;
+ if (data->smimeHeaderSink) {
+ int aRelativeNestLevel = MIMEGetRelativeCryptoNestLevel(data->self);
+ nsAutoCString partnum;
+ partnum.Adopt(mime_part_address(data->self));
+ data->smimeHeaderSink->SignedStatus(aRelativeNestLevel,
+ nsICMSMessageErrors::GENERAL_ERROR,
+ nullptr, data->url, partnum);
+ }
+ PR_Free(micalg);
+ return data;
+ }
+
+ PR_Free(micalg);
+ micalg = 0;
+
+ data->hash_type = hash_type;
+
+ data->data_hash_context =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ if (NS_FAILED(rv)) {
+ delete data;
+ return 0;
+ }
+
+ rv = data->data_hash_context->Init(data->hash_type);
+ if (NS_FAILED(rv)) {
+ delete data;
+ return 0;
+ }
+
+ PR_SetError(0, 0);
+
+ return data;
+}
+
+static int MimeMultCMS_data_hash(const char* buf, int32_t size,
+ void* crypto_closure) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ if (!data) {
+ return -1;
+ }
+
+ if (data->reject_signature) {
+ return 0;
+ }
+
+ if (!data->data_hash_context) {
+ return -1;
+ }
+
+ PR_SetError(0, 0);
+ nsresult rv = data->data_hash_context->Update((unsigned char*)buf, size);
+ data->decoding_failed = NS_FAILED(rv);
+
+ return 0;
+}
+
+static int MimeMultCMS_data_eof(void* crypto_closure, bool abort_p) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ if (!data) {
+ return -1;
+ }
+
+ if (data->reject_signature) {
+ return 0;
+ }
+
+ if (!data->data_hash_context) {
+ return -1;
+ }
+
+ nsAutoCString hashString;
+ data->data_hash_context->Finish(false, hashString);
+ PR_SetError(0, 0);
+
+ data->item_len = hashString.Length();
+ data->item_data = new unsigned char[data->item_len];
+ if (!data->item_data) return MIME_OUT_OF_MEMORY;
+
+ memcpy(data->item_data, hashString.get(), data->item_len);
+
+ // Release our reference to nsICryptoHash //
+ data->data_hash_context = nullptr;
+
+ /* At this point, data->item.data contains a digest for the first part.
+ When we process the signature, the security library will compare this
+ digest to what's in the signature object. */
+
+ return 0;
+}
+
+static int MimeMultCMS_sig_init(void* crypto_closure,
+ MimeObject* multipart_object,
+ MimeHeaders* signature_hdrs) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ char* ct;
+ int status = 0;
+ nsresult rv;
+
+ if (data->reject_signature) {
+ return 0;
+ }
+
+ if (!signature_hdrs) {
+ return -1;
+ }
+
+ ct = MimeHeaders_get(signature_hdrs, HEADER_CONTENT_TYPE, true, false);
+
+ /* Verify that the signature object is of the right type. */
+ if (!ct || /* is not a signature type */
+ (PL_strcasecmp(ct, APPLICATION_XPKCS7_SIGNATURE) != 0 &&
+ PL_strcasecmp(ct, APPLICATION_PKCS7_SIGNATURE) != 0)) {
+ status = -1; /* #### error msg about bogus message */
+ }
+ PR_FREEIF(ct);
+ if (status < 0) return status;
+
+ data->sig_decoder_context = do_CreateInstance(NS_CMSDECODER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return 0;
+
+ rv = data->sig_decoder_context->Start(nullptr, nullptr);
+ if (NS_FAILED(rv)) {
+ status = PR_GetError();
+ if (status >= 0) status = -1;
+ }
+ return status;
+}
+
+static int MimeMultCMS_sig_hash(const char* buf, int32_t size,
+ void* crypto_closure) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ nsresult rv;
+
+ if (!data) {
+ return -1;
+ }
+
+ if (data->reject_signature) {
+ return 0;
+ }
+
+ if (!data->sig_decoder_context) {
+ return -1;
+ }
+
+ rv = data->sig_decoder_context->Update(buf, size);
+ data->decoding_failed = NS_FAILED(rv);
+
+ return 0;
+}
+
+static int MimeMultCMS_sig_eof(void* crypto_closure, bool abort_p) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+
+ if (!data) {
+ return -1;
+ }
+
+ if (data->reject_signature) {
+ return 0;
+ }
+
+ /* Hand an EOF to the crypto library.
+
+ We save away the value returned and will use it later to emit a
+ blurb about whether the signature validation was cool.
+ */
+
+ if (data->sig_decoder_context) {
+ data->sig_decoder_context->Finish(getter_AddRefs(data->content_info));
+
+ // Release our reference to nsICMSDecoder //
+ data->sig_decoder_context = nullptr;
+ }
+
+ return 0;
+}
+
+static void MimeMultCMS_free(void* crypto_closure) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ if (!data) return;
+
+ delete data;
+}
+
+static void MimeMultCMS_suppressed_child(void* crypto_closure) {
+ // I'm a multipart/signed. If one of my cryptographic child elements
+ // was suppressed, then I want my signature to be shown as invalid.
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ if (data && data->smimeHeaderSink) {
+ if (data->reject_signature) {
+ return;
+ }
+
+ nsAutoCString partnum;
+ partnum.Adopt(mime_part_address(data->self));
+ data->smimeHeaderSink->SignedStatus(
+ MIMEGetRelativeCryptoNestLevel(data->self),
+ nsICMSMessageErrors::GENERAL_ERROR, nullptr, data->url, partnum);
+ }
+}
+
+static char* MimeMultCMS_generate(void* crypto_closure) {
+ MimeMultCMSdata* data = (MimeMultCMSdata*)crypto_closure;
+ if (!data) return 0;
+ nsCOMPtr<nsIX509Cert> signerCert;
+
+ int aRelativeNestLevel = MIMEGetRelativeCryptoNestLevel(data->self);
+
+ if (aRelativeNestLevel < 0) return nullptr;
+
+ if (aRelativeNestLevel >= 0) {
+ // maxWantedNesting 1: only want outermost nesting level
+ if (aRelativeNestLevel > 1) return nullptr;
+ }
+
+ nsAutoCString partnum;
+ partnum.Adopt(mime_part_address(data->self));
+
+ if (data->self->options->missing_parts) {
+ // We were not given all parts of the message.
+ // We are therefore unable to verify correctness of the signature.
+
+ if (data->smimeHeaderSink) {
+ data->smimeHeaderSink->SignedStatus(
+ aRelativeNestLevel, nsICMSMessageErrors::VERIFY_NOT_YET_ATTEMPTED,
+ nullptr, data->url, partnum);
+ }
+ return nullptr;
+ }
+
+ if (!data->content_info) {
+ /* No content_info at all -- since we're inside a multipart/signed,
+ that means that we've either gotten a message that was truncated
+ before the signature part, or we ran out of memory, or something
+ awful has happened.
+ */
+ return nullptr;
+ }
+
+ nsCString from_addr;
+ nsCString from_name;
+ nsCString sender_addr;
+ nsCString sender_name;
+ nsCString msg_date;
+
+ MimeCMSGetFromSender(data->self, from_addr, from_name, sender_addr,
+ sender_name, msg_date);
+
+ nsTArray<uint8_t> digest;
+ digest.AppendElements(data->item_data, data->item_len);
+
+ if (!data->reject_signature && data->smimeHeaderSink) {
+ MimeCMSRequestAsyncSignatureVerification(
+ data->content_info, from_addr.get(), from_name.get(), sender_addr.get(),
+ sender_name.get(), msg_date.get(), data->smimeHeaderSink,
+ aRelativeNestLevel, data->url, partnum, digest, data->hash_type);
+ }
+
+ if (data->content_info) {
+#if 0 // XXX Fix this. What do we do here? //
+ if (SEC_CMSContainsCertsOrCrls(data->content_info))
+ {
+ /* #### call libsec telling it to import the certs */
+ }
+#endif
+ }
+
+ return nullptr;
+}
diff --git a/comm/mailnews/mime/src/mimemcms.h b/comm/mailnews/mime/src/mimemcms.h
new file mode 100644
index 0000000000..e8bfc6aac0
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemcms.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMPKC_H_
+#define _MIMEMPKC_H_
+
+#include "mimemsig.h"
+
+class nsICMSMessage; // for function arguments in mimemcms.c
+
+/* The MimeMultipartSignedCMS class implements a multipart/signed MIME
+ container with protocol=application/x-CMS-signature, which passes the
+ signed object through CMS code to verify the signature. See mimemsig.h
+ for details of the general mechanism on which this is built.
+ */
+
+typedef struct MimeMultipartSignedCMSClass MimeMultipartSignedCMSClass;
+typedef struct MimeMultipartSignedCMS MimeMultipartSignedCMS;
+
+struct MimeMultipartSignedCMSClass {
+ MimeMultipartSignedClass msigned;
+};
+
+extern MimeMultipartSignedCMSClass mimeMultipartSignedCMSClass;
+
+struct MimeMultipartSignedCMS {
+ MimeMultipartSigned msigned;
+};
+
+#define MimeMultipartSignedCMSClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartSignedClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMPKC_H_ */
diff --git a/comm/mailnews/mime/src/mimemdig.cpp b/comm/mailnews/mime/src/mimemdig.cpp
new file mode 100644
index 0000000000..89500e3074
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemdig.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimemdig.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartDigest, MimeMultipartDigestClass,
+ mimeMultipartDigestClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartDigestClassInitialize(MimeMultipartDigestClass* clazz) {
+#ifdef DEBUG
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+#endif
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+ mclass->default_part_type = MESSAGE_RFC822;
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimemdig.h b/comm/mailnews/mime/src/mimemdig.h
new file mode 100644
index 0000000000..1701004611
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemdig.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMDIG_H_
+#define _MIMEMDIG_H_
+
+#include "mimemult.h"
+
+/* The MimeMultipartDigest class implements the multipart/digest MIME
+ container, which is just like multipart/mixed, except that the default
+ type (for parts with no type explicitly specified) is message/rfc822
+ instead of text/plain.
+ */
+
+typedef struct MimeMultipartDigestClass MimeMultipartDigestClass;
+typedef struct MimeMultipartDigest MimeMultipartDigest;
+
+struct MimeMultipartDigestClass {
+ MimeMultipartClass multipart;
+};
+
+extern MimeMultipartDigestClass mimeMultipartDigestClass;
+
+struct MimeMultipartDigest {
+ MimeMultipart multipart;
+};
+
+#define MimeMultipartDigestClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMDIG_H_ */
diff --git a/comm/mailnews/mime/src/mimemmix.cpp b/comm/mailnews/mime/src/mimemmix.cpp
new file mode 100644
index 0000000000..a4c6d22d6c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemmix.cpp
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimemmix.h"
+#include "prlog.h"
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartMixed, MimeMultipartMixedClass,
+ mimeMultipartMixedClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartMixedClassInitialize(MimeMultipartMixedClass* clazz) {
+#ifdef DEBUG
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+#endif
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimemmix.h b/comm/mailnews/mime/src/mimemmix.h
new file mode 100644
index 0000000000..6d24939ff6
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemmix.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMMIX_H_
+#define _MIMEMMIX_H_
+
+#include "mimemult.h"
+
+/* The MimeMultipartMixed class implements the multipart/mixed MIME container,
+ and is also used for any and all otherwise-unrecognised subparts of
+ multipart/.
+ */
+
+typedef struct MimeMultipartMixedClass MimeMultipartMixedClass;
+typedef struct MimeMultipartMixed MimeMultipartMixed;
+
+struct MimeMultipartMixedClass {
+ MimeMultipartClass multipart;
+};
+
+extern MimeMultipartMixedClass mimeMultipartMixedClass;
+
+struct MimeMultipartMixed {
+ MimeMultipart multipart;
+};
+
+#define MimeMultipartMixedClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMMIX_H_ */
diff --git a/comm/mailnews/mime/src/mimemoz2.cpp b/comm/mailnews/mime/src/mimemoz2.cpp
new file mode 100644
index 0000000000..e5e4a6a644
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemoz2.cpp
@@ -0,0 +1,1862 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "prlog.h"
+#include "nsCOMPtr.h"
+#include "modlmime.h"
+#include "mimeobj.h"
+#include "mimemsg.h"
+#include "mimetric.h" /* for MIME_RichtextConverter */
+#include "mimethtm.h"
+#include "mimemsig.h"
+#include "mimemrel.h"
+#include "mimemalt.h"
+#include "mimebuf.h"
+#include "mimemapl.h"
+#include "prprf.h"
+#include "mimei.h" /* for moved MimeDisplayData struct */
+#include "mimebuf.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "mimemoz2.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIStringBundle.h"
+#include "nsString.h"
+#include "nsMimeStringResources.h"
+#include "nsStreamConverter.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIMIMEService.h"
+#include "nsIImapUrl.h"
+#include "nsMsgI18N.h"
+#include "nsICharsetConverterManager.h"
+#include "nsMimeTypes.h"
+#include "nsIIOService.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsMsgUtils.h"
+#include "nsIChannel.h"
+#include "nsIMailChannel.h"
+#include "mimeebod.h"
+#include "mimeeobj.h"
+// <for functions="HTML2Plaintext,HTMLSantinize">
+#include "nsXPCOM.h"
+#include "nsLayoutCID.h"
+#include "nsIParserUtils.h"
+// </for>
+#include "mozilla/Components.h"
+#include "mozilla/Unused.h"
+
+void ValidateRealName(nsMsgAttachmentData* aAttach, MimeHeaders* aHdrs);
+
+static MimeHeadersState MIME_HeaderType;
+static bool MIME_WrapLongLines;
+static bool MIME_VariableWidthPlaintext;
+
+mime_stream_data::mime_stream_data()
+ : url_name(nullptr),
+ orig_url_name(nullptr),
+ pluginObj2(nullptr),
+ istream(nullptr),
+ obj(nullptr),
+ options(nullptr),
+ headers(nullptr),
+ output_emitter(nullptr) {}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Attachment handling routines
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+MimeObject* mime_get_main_object(MimeObject* obj);
+
+// Appends a "filename" parameter with the attachment name to the object url.
+void AppendFilenameParameterToAttachmentDataUrl(
+ const nsMsgAttachmentData* attachmentData, nsCString& url) {
+ url.AppendLiteral("&filename=");
+ nsAutoCString aResult;
+ if (NS_SUCCEEDED(MsgEscapeString(attachmentData->m_realName,
+ nsINetUtil::ESCAPE_XALPHAS, aResult))) {
+ url.Append(aResult);
+ } else {
+ url.Append(attachmentData->m_realName);
+ }
+ if (attachmentData->m_realType.EqualsLiteral("message/rfc822") &&
+ !StringEndsWith(url, ".eml"_ns, nsCaseInsensitiveCStringComparator)) {
+ url.AppendLiteral(".eml");
+ }
+}
+
+nsresult MimeGetSize(MimeObject* child, int32_t* size) {
+ bool isLeaf = mime_subclass_p(child->clazz, (MimeObjectClass*)&mimeLeafClass);
+ bool isContainer =
+ mime_subclass_p(child->clazz, (MimeObjectClass*)&mimeContainerClass);
+ bool isMsg =
+ mime_subclass_p(child->clazz, (MimeObjectClass*)&mimeMessageClass);
+
+ if (isLeaf) {
+ *size += ((MimeLeaf*)child)->sizeSoFar;
+ } else if (isMsg) {
+ *size += ((MimeMessage*)child)->sizeSoFar;
+ } else if (isContainer) {
+ int i;
+ MimeContainer* cont = (MimeContainer*)child;
+ for (i = 0; i < cont->nchildren; ++i) {
+ MimeGetSize(cont->children[i], size);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult ProcessBodyAsAttachment(MimeObject* obj, nsMsgAttachmentData** data) {
+ nsMsgAttachmentData* tmp;
+ char* disp = nullptr;
+ char* charset = nullptr;
+
+ // Ok, this is the special case when somebody sends an "attachment" as the
+ // body of an RFC822 message...I really don't think this is the way this
+ // should be done. I believe this should really be a multipart/mixed message
+ // with an empty body part, but what can ya do...our friends to the North seem
+ // to do this.
+ MimeObject* child = obj;
+
+ *data = new nsMsgAttachmentData[2];
+ if (!*data) return NS_ERROR_OUT_OF_MEMORY;
+
+ tmp = *data;
+ tmp->m_realType = child->content_type;
+ tmp->m_realEncoding = child->encoding;
+ disp =
+ MimeHeaders_get(child->headers, HEADER_CONTENT_DISPOSITION, false, false);
+ tmp->m_realName.Adopt(
+ MimeHeaders_get_parameter(disp, "name", &charset, NULL));
+ if (!tmp->m_realName.IsEmpty()) {
+ char* fname = NULL;
+ fname = mime_decode_filename(tmp->m_realName.get(), charset, obj->options);
+ free(charset);
+ if (fname) tmp->m_realName.Adopt(fname);
+ } else {
+ tmp->m_realName.Adopt(MimeHeaders_get_name(child->headers, obj->options));
+
+ if (tmp->m_realName.IsEmpty() &&
+ tmp->m_realType.LowerCaseEqualsLiteral(MESSAGE_RFC822)) {
+ // We haven't actually parsed the message "attachment", so just give it a
+ // generic name.
+ tmp->m_realName = "AttachedMessage.eml";
+ }
+ }
+
+ tmp->m_hasFilename = !tmp->m_realName.IsEmpty();
+
+ if (tmp->m_realName.IsEmpty() &&
+ StringBeginsWith(tmp->m_realType, "text"_ns,
+ nsCaseInsensitiveCStringComparator))
+ ValidateRealName(tmp, child->headers);
+
+ tmp->m_displayableInline =
+ obj->clazz->displayable_inline_p(obj->clazz, obj->headers);
+
+ char* tmpURL = nullptr;
+ char* id = nullptr;
+ char* id_imap = nullptr;
+
+ id = mime_part_address(obj);
+ if (obj->options->missing_parts) id_imap = mime_imap_part_address(obj);
+
+ tmp->m_isDownloaded = !id_imap;
+
+ if (!id) {
+ delete[] * data;
+ *data = nullptr;
+ PR_FREEIF(id_imap);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (obj->options && obj->options->url) {
+ const char* url = obj->options->url;
+ nsresult rv;
+ if (id_imap && id) {
+ // if this is an IMAP part.
+ tmpURL = mime_set_url_imap_part(url, id_imap, id);
+ rv = nsMimeNewURI(getter_AddRefs(tmp->m_url), tmpURL, nullptr);
+ } else {
+ // This is just a normal MIME part as usual.
+ tmpURL = mime_set_url_part(url, id, true);
+ nsCString urlString(tmpURL);
+ if (!tmp->m_realName.IsEmpty()) {
+ AppendFilenameParameterToAttachmentDataUrl(tmp, urlString);
+ }
+ rv = nsMimeNewURI(getter_AddRefs(tmp->m_url), urlString.get(), nullptr);
+ }
+
+ if (!tmp->m_url || NS_FAILED(rv)) {
+ delete[] * data;
+ *data = nullptr;
+ PR_FREEIF(id);
+ PR_FREEIF(id_imap);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ PR_FREEIF(id);
+ PR_FREEIF(id_imap);
+ PR_FREEIF(tmpURL);
+ tmp->m_description.Adopt(MimeHeaders_get(
+ child->headers, HEADER_CONTENT_DESCRIPTION, false, false));
+
+ tmp->m_size = 0;
+ MimeGetSize(child, &tmp->m_size);
+
+ return NS_OK;
+}
+
+int32_t CountTotalMimeAttachments(MimeContainer* aObj) {
+ int32_t i;
+ int32_t rc = 0;
+
+ if ((!aObj) || (!aObj->children) || (aObj->nchildren <= 0)) return 0;
+
+ if (!mime_typep(((MimeObject*)aObj), (MimeObjectClass*)&mimeContainerClass))
+ return 0;
+
+ for (i = 0; i < aObj->nchildren; i++)
+ rc += CountTotalMimeAttachments((MimeContainer*)aObj->children[i]) + 1;
+
+ return rc;
+}
+
+void ValidateRealName(nsMsgAttachmentData* aAttach, MimeHeaders* aHdrs) {
+ // Sanity.
+ if (!aAttach) return;
+
+ // Do we need to validate?
+ if (!aAttach->m_realName.IsEmpty()) return;
+
+ // Internal MIME structures need not be named!
+ if (aAttach->m_realType.IsEmpty() ||
+ StringBeginsWith(aAttach->m_realType, "multipart"_ns,
+ nsCaseInsensitiveCStringComparator))
+ return;
+
+ //
+ // Now validate any other name we have for the attachment!
+ //
+ if (aAttach->m_realName.IsEmpty()) {
+ aAttach->m_realName = "attachment";
+ nsresult rv = NS_OK;
+ nsAutoCString contentType(aAttach->m_realType);
+ int32_t pos = contentType.FindChar(';');
+ if (pos > 0) contentType.SetLength(pos);
+
+ nsCOMPtr<nsIMIMEService> mimeFinder(
+ do_GetService(NS_MIMESERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString fileExtension;
+ rv = mimeFinder->GetPrimaryExtension(contentType, EmptyCString(),
+ fileExtension);
+
+ if (NS_SUCCEEDED(rv) && !fileExtension.IsEmpty()) {
+ aAttach->m_realName.Append('.');
+ aAttach->m_realName.Append(fileExtension);
+ }
+ }
+ }
+}
+
+static int32_t attIndex = 0;
+
+nsresult GenerateAttachmentData(MimeObject* object, const char* aMessageURL,
+ MimeDisplayOptions* options,
+ bool isAnAppleDoublePart, int32_t attSize,
+ nsMsgAttachmentData* aAttachData) {
+ nsCString imappart;
+ nsCString part;
+ bool isExternalAttachment = false;
+
+ /* be sure the object has not be marked as Not to be an attachment */
+ if (object->dontShowAsAttachment) return NS_OK;
+
+ part.Adopt(mime_part_address(object));
+ if (part.IsEmpty()) return NS_ERROR_OUT_OF_MEMORY;
+
+ if (options->missing_parts) imappart.Adopt(mime_imap_part_address(object));
+
+ char* urlSpec = nullptr;
+ if (!imappart.IsEmpty()) {
+ urlSpec = mime_set_url_imap_part(aMessageURL, imappart.get(), part.get());
+ } else {
+ char* no_part_url = nullptr;
+ if (options->part_to_load &&
+ options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay)
+ no_part_url = mime_get_base_url(aMessageURL);
+ if (no_part_url) {
+ urlSpec = mime_set_url_part(no_part_url, part.get(), true);
+ PR_Free(no_part_url);
+ } else {
+ // if the mime object contains an external attachment URL, then use it,
+ // otherwise fall back to creating an attachment url based on the message
+ // URI and the part number.
+ urlSpec = mime_external_attachment_url(object);
+ isExternalAttachment = urlSpec ? true : false;
+ if (!urlSpec) urlSpec = mime_set_url_part(aMessageURL, part.get(), true);
+ }
+ }
+
+ if (!urlSpec) return NS_ERROR_OUT_OF_MEMORY;
+
+ if ((options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay) &&
+ (PL_strncasecmp(aMessageURL, urlSpec, strlen(urlSpec)) == 0))
+ return NS_OK;
+
+ nsCString urlString(urlSpec);
+
+ nsMsgAttachmentData* tmp = &(aAttachData[attIndex++]);
+
+ tmp->m_realType = object->content_type;
+ tmp->m_realEncoding = object->encoding;
+ tmp->m_isExternalAttachment = isExternalAttachment;
+ tmp->m_isExternalLinkAttachment =
+ (isExternalAttachment &&
+ StringBeginsWith(urlString, "http"_ns,
+ nsCaseInsensitiveCStringComparator));
+ tmp->m_size = attSize;
+ tmp->m_sizeExternalStr = "-1";
+ tmp->m_disposition.Adopt(MimeHeaders_get(
+ object->headers, HEADER_CONTENT_DISPOSITION, true, false));
+ tmp->m_displayableInline =
+ object->clazz->displayable_inline_p(object->clazz, object->headers);
+
+ char* part_addr = mime_imap_part_address(object);
+ tmp->m_isDownloaded = !part_addr;
+ PR_FREEIF(part_addr);
+
+ int32_t i;
+ char* charset = nullptr;
+ char* disp = MimeHeaders_get(object->headers, HEADER_CONTENT_DISPOSITION,
+ false, false);
+ if (disp) {
+ tmp->m_realName.Adopt(
+ MimeHeaders_get_parameter(disp, "filename", &charset, nullptr));
+ if (isAnAppleDoublePart)
+ for (i = 0; i < 2 && tmp->m_realName.IsEmpty(); i++) {
+ PR_FREEIF(disp);
+ free(charset);
+ disp = MimeHeaders_get(((MimeContainer*)object)->children[i]->headers,
+ HEADER_CONTENT_DISPOSITION, false, false);
+ tmp->m_realName.Adopt(
+ MimeHeaders_get_parameter(disp, "filename", &charset, nullptr));
+ }
+
+ if (!tmp->m_realName.IsEmpty()) {
+ // check encoded type
+ //
+ // The parameter of Content-Disposition must use RFC 2231.
+ // But old Netscape 4.x and Outlook Express etc. use RFC2047.
+ // So we should parse both types.
+
+ char* fname = nullptr;
+ fname = mime_decode_filename(tmp->m_realName.get(), charset, options);
+ free(charset);
+
+ if (fname) tmp->m_realName.Adopt(fname);
+ }
+
+ PR_FREEIF(disp);
+ }
+
+ disp = MimeHeaders_get(object->headers, HEADER_CONTENT_TYPE, false, false);
+ if (disp) {
+ tmp->m_xMacType.Adopt(
+ MimeHeaders_get_parameter(disp, PARAM_X_MAC_TYPE, nullptr, nullptr));
+ tmp->m_xMacCreator.Adopt(
+ MimeHeaders_get_parameter(disp, PARAM_X_MAC_CREATOR, nullptr, nullptr));
+
+ if (tmp->m_realName.IsEmpty()) {
+ tmp->m_realName.Adopt(
+ MimeHeaders_get_parameter(disp, "name", &charset, nullptr));
+ if (isAnAppleDoublePart)
+ // the data fork is the 2nd part, and we should ALWAYS look there first
+ // for the file name
+ for (i = 1; i >= 0 && tmp->m_realName.IsEmpty(); i--) {
+ PR_FREEIF(disp);
+ free(charset);
+ disp = MimeHeaders_get(((MimeContainer*)object)->children[i]->headers,
+ HEADER_CONTENT_TYPE, false, false);
+ tmp->m_realName.Adopt(
+ MimeHeaders_get_parameter(disp, "name", &charset, nullptr));
+ tmp->m_realType.Adopt(
+ MimeHeaders_get(((MimeContainer*)object)->children[i]->headers,
+ HEADER_CONTENT_TYPE, true, false));
+ }
+
+ if (!tmp->m_realName.IsEmpty()) {
+ // check encoded type
+ //
+ // The parameter of Content-Disposition must use RFC 2231.
+ // But old Netscape 4.x and Outlook Express etc. use RFC2047.
+ // So we should parse both types.
+
+ char* fname = nullptr;
+ fname = mime_decode_filename(tmp->m_realName.get(), charset, options);
+ free(charset);
+
+ if (fname) tmp->m_realName.Adopt(fname);
+ }
+ }
+
+ if (tmp->m_isExternalLinkAttachment) {
+ // If an external link attachment part's Content-Type contains a
+ // |size| parm, store it in m_sizeExternalStr. Let the msgHeaderSink
+ // addAttachmentField() figure out if it's sane, and don't bother
+ // strtol'ing it to an int only to emit it as a string.
+ char* sizeStr = MimeHeaders_get_parameter(disp, "size", nullptr, nullptr);
+ if (sizeStr) tmp->m_sizeExternalStr = sizeStr;
+ }
+
+ PR_FREEIF(disp);
+ }
+
+ tmp->m_description.Adopt(MimeHeaders_get(
+ object->headers, HEADER_CONTENT_DESCRIPTION, false, false));
+
+ // Now, do the right thing with the name!
+ if (tmp->m_realName.IsEmpty() &&
+ !(tmp->m_realType.LowerCaseEqualsLiteral(MESSAGE_RFC822))) {
+ // Keep in mind that the name was provided by us and this is probably not a
+ // real attachment.
+ tmp->m_hasFilename = false;
+ /* If this attachment doesn't have a name, just give it one... */
+ tmp->m_realName.Adopt(MimeGetStringByID(MIME_MSG_DEFAULT_ATTACHMENT_NAME));
+ if (!tmp->m_realName.IsEmpty()) {
+ char* newName = PR_smprintf(tmp->m_realName.get(), part.get());
+ if (newName) tmp->m_realName.Adopt(newName);
+ } else
+ tmp->m_realName.Adopt(mime_part_address(object));
+ } else {
+ tmp->m_hasFilename = true;
+ }
+
+ if (!tmp->m_realName.IsEmpty() && !tmp->m_isExternalAttachment) {
+ AppendFilenameParameterToAttachmentDataUrl(tmp, urlString);
+ } else if (tmp->m_isExternalAttachment) {
+ // Allows the JS mime emitter to figure out the part information.
+ urlString.AppendLiteral("?part=");
+ urlString.Append(part);
+ } else if (tmp->m_realType.LowerCaseEqualsLiteral(MESSAGE_RFC822)) {
+ // Special case...if this is a enclosed RFC822 message, give it a nice
+ // name.
+ if (object->headers->munged_subject) {
+ nsCString subject;
+ subject.Assign(object->headers->munged_subject);
+ MimeHeaders_convert_header_value(options, subject, false);
+ tmp->m_realName.Assign(subject);
+ tmp->m_realName.AppendLiteral(".eml");
+ } else
+ tmp->m_realName = "ForwardedMessage.eml";
+ }
+
+ nsresult rv =
+ nsMimeNewURI(getter_AddRefs(tmp->m_url), urlString.get(), nullptr);
+
+ PR_FREEIF(urlSpec);
+
+ if (NS_FAILED(rv) || !tmp->m_url) return NS_ERROR_OUT_OF_MEMORY;
+
+ ValidateRealName(tmp, object->headers);
+
+ return NS_OK;
+}
+
+nsresult BuildAttachmentList(MimeObject* anObject,
+ nsMsgAttachmentData* aAttachData,
+ const char* aMessageURL) {
+ nsresult rv;
+ int32_t i;
+ MimeContainer* cobj = (MimeContainer*)anObject;
+ bool found_output = false;
+
+ if ((!anObject) || (!cobj->children) || (!cobj->nchildren) ||
+ (mime_typep(anObject, (MimeObjectClass*)&mimeExternalBodyClass)))
+ return NS_OK;
+
+ for (i = 0; i < cobj->nchildren; i++) {
+ MimeObject* child = cobj->children[i];
+ char* ct = child->content_type;
+
+ // We're going to ignore the output_p attribute because we want to output
+ // any part with a name to work around bug 674473
+
+ // Skip the first child that's being output if it's in fact a message body.
+ // Start by assuming that it is, until proven otherwise in the code below.
+ bool skip = true;
+ if (found_output)
+ // not first child being output
+ skip = false;
+ else if (!ct)
+ // no content type so can't be message body
+ skip = false;
+ else if (PL_strcasecmp(ct, TEXT_PLAIN) && PL_strcasecmp(ct, TEXT_HTML) &&
+ PL_strcasecmp(ct, TEXT_MDL))
+ // not a type we recognize as a message body
+ skip = false;
+ // we're displaying all body parts
+ if (child->options->html_as_p == 4) skip = false;
+ if (skip && child->headers) {
+ // If it has a filename, we don't skip it regardless of the
+ // content disposition which can be "inline" or "attachment".
+ // Inline parts are not shown when attachments aren't displayed
+ // inline, so the only chance to see the part is as attachment.
+ char* name = MimeHeaders_get_name(child->headers, nullptr);
+ if (name) skip = false;
+ PR_FREEIF(name);
+ }
+
+ found_output = true;
+ if (skip) continue;
+
+ // We should generate an attachment for leaf object only but...
+ bool isALeafObject =
+ mime_subclass_p(child->clazz, (MimeObjectClass*)&mimeLeafClass);
+
+ // ...we will generate an attachment for inline message too.
+ bool isAnInlineMessage =
+ mime_typep(child, (MimeObjectClass*)&mimeMessageClass);
+
+ // AppleDouble part need special care: we need to fetch the part as well its
+ // two children for the needed info as they could be anywhere, eventually,
+ // they won't contain a name or file name. In any case we need to build only
+ // one attachment data
+ bool isAnAppleDoublePart =
+ mime_typep(child, (MimeObjectClass*)&mimeMultipartAppleDoubleClass) &&
+ ((MimeContainer*)child)->nchildren == 2;
+
+ // The function below does not necessarily set the size to something (I
+ // don't think it will work for external objects, for instance, since they
+ // are neither containers nor leafs).
+ int32_t attSize = 0;
+ MimeGetSize(child, &attSize);
+
+ if (isALeafObject || isAnInlineMessage || isAnAppleDoublePart) {
+ rv = GenerateAttachmentData(child, aMessageURL, anObject->options,
+ isAnAppleDoublePart, attSize, aAttachData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Now build the attachment list for the children of our object...
+ if (!isALeafObject && !isAnAppleDoublePart) {
+ rv = BuildAttachmentList((MimeObject*)child, aAttachData, aMessageURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+extern "C" nsresult MimeGetAttachmentList(MimeObject* tobj,
+ const char* aMessageURL,
+ nsMsgAttachmentData** data) {
+ MimeObject* obj;
+ MimeContainer* cobj;
+ int32_t n;
+ bool isAnInlineMessage;
+
+ if (!data) return NS_ERROR_INVALID_ARG;
+ *data = nullptr;
+
+ obj = mime_get_main_object(tobj);
+ if (!obj) return NS_OK;
+
+ if (!mime_subclass_p(obj->clazz, (MimeObjectClass*)&mimeContainerClass))
+ return ProcessBodyAsAttachment(obj, data);
+
+ isAnInlineMessage = mime_typep(obj, (MimeObjectClass*)&mimeMessageClass);
+
+ cobj = (MimeContainer*)obj;
+ n = CountTotalMimeAttachments(cobj);
+ if (n <= 0)
+ // XXX n is a regular number here, not meaningful as an nsresult
+ return static_cast<nsresult>(n);
+
+ // in case of an inline message (as body), we need an extra slot for the
+ // message itself that we will fill later...
+ if (isAnInlineMessage) n++;
+
+ *data = new nsMsgAttachmentData[n + 1];
+ if (!*data) return NS_ERROR_OUT_OF_MEMORY;
+
+ attIndex = 0;
+
+ // Now, build the list!
+
+ nsresult rv;
+
+ if (isAnInlineMessage) {
+ int32_t size = 0;
+ MimeGetSize(obj, &size);
+ rv = GenerateAttachmentData(obj, aMessageURL, obj->options, false, size,
+ *data);
+ if (NS_FAILED(rv)) {
+ delete[] * data; // release data in case of error return.
+ *data = nullptr;
+ return rv;
+ }
+ }
+ rv = BuildAttachmentList((MimeObject*)cobj, *data, aMessageURL);
+ if (NS_FAILED(rv)) {
+ delete[] * data; // release data in case of error return.
+ *data = nullptr;
+ }
+ return rv;
+}
+
+extern "C" void NotifyEmittersOfAttachmentList(MimeDisplayOptions* opt,
+ nsMsgAttachmentData* data) {
+ nsMsgAttachmentData* tmp = data;
+
+ if (!tmp) return;
+
+ while (tmp->m_url) {
+ // The code below implements the following logic:
+ // - Always display the attachment if the Content-Disposition is
+ // "attachment" or if it can't be displayed inline.
+ // - If there's no name at all, just skip it (we don't know what to do with
+ // it then).
+ // - If the attachment has a "provided name" (i.e. not something like "Part
+ // 1.2"), display it.
+ // - If we're asking for all body parts and NOT asking for metadata only,
+ // display it.
+ // - Otherwise, skip it.
+ if (!tmp->m_disposition.EqualsLiteral("attachment") &&
+ tmp->m_displayableInline &&
+ (tmp->m_realName.IsEmpty() ||
+ (!tmp->m_hasFilename &&
+ (opt->html_as_p != 4 || opt->metadata_only)))) {
+ ++tmp;
+ continue;
+ }
+
+ nsAutoCString spec;
+ if (tmp->m_url) {
+ if (tmp->m_isExternalLinkAttachment)
+ mozilla::Unused << tmp->m_url->GetAsciiSpec(spec);
+ else
+ mozilla::Unused << tmp->m_url->GetSpec(spec);
+ }
+
+ nsAutoCString sizeStr;
+ if (tmp->m_isExternalLinkAttachment)
+ sizeStr.Append(tmp->m_sizeExternalStr);
+ else
+ sizeStr.AppendInt(tmp->m_size);
+
+ nsAutoCString downloadedStr;
+ downloadedStr.AppendInt(tmp->m_isDownloaded);
+
+ mimeEmitterStartAttachment(opt, tmp->m_realName.get(),
+ tmp->m_realType.get(), spec.get(),
+ tmp->m_isExternalAttachment);
+ mimeEmitterAddAttachmentField(opt, HEADER_X_MOZILLA_PART_URL, spec.get());
+ mimeEmitterAddAttachmentField(opt, HEADER_X_MOZILLA_PART_SIZE,
+ sizeStr.get());
+ mimeEmitterAddAttachmentField(opt, HEADER_X_MOZILLA_PART_DOWNLOADED,
+ downloadedStr.get());
+
+ if ((opt->format_out == nsMimeOutput::nsMimeMessageQuoting) ||
+ (opt->format_out == nsMimeOutput::nsMimeMessageBodyQuoting) ||
+ (opt->format_out == nsMimeOutput::nsMimeMessageSaveAs) ||
+ (opt->format_out == nsMimeOutput::nsMimeMessagePrintOutput)) {
+ mimeEmitterAddAttachmentField(opt, HEADER_CONTENT_DESCRIPTION,
+ tmp->m_description.get());
+ mimeEmitterAddAttachmentField(opt, HEADER_CONTENT_TYPE,
+ tmp->m_realType.get());
+ mimeEmitterAddAttachmentField(opt, HEADER_CONTENT_ENCODING,
+ tmp->m_realEncoding.get());
+ }
+
+ mimeEmitterEndAttachment(opt);
+ ++tmp;
+ }
+ mimeEmitterEndAllAttachments(opt);
+}
+
+// Utility to create a nsIURI object...
+extern "C" nsresult nsMimeNewURI(nsIURI** aInstancePtrResult, const char* aSpec,
+ nsIURI* aBase) {
+ if (nullptr == aInstancePtrResult) return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIIOService> pService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(pService, NS_ERROR_FACTORY_NOT_REGISTERED);
+
+ return pService->NewURI(nsDependentCString(aSpec), nullptr, aBase,
+ aInstancePtrResult);
+}
+
+extern "C" nsresult SetMailCharacterSetToMsgWindow(MimeObject* obj,
+ const char* aCharacterSet) {
+ nsresult rv = NS_OK;
+
+ if (obj && obj->options) {
+ mime_stream_data* msd = (mime_stream_data*)(obj->options->stream_closure);
+ if (msd) {
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(msd->channel);
+ if (mailChannel) {
+ if (!PL_strcasecmp(aCharacterSet, "us-ascii")) {
+ mailChannel->SetMailCharacterSet("ISO-8859-1"_ns);
+ } else {
+ mailChannel->SetMailCharacterSet(nsCString(aCharacterSet));
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+static char* mime_file_type(const char* filename, void* stream_closure) {
+ char* retType = nullptr;
+ char* ext = nullptr;
+ nsresult rv;
+
+ ext = PL_strrchr(filename, '.');
+ if (ext) {
+ ext++;
+ nsCOMPtr<nsIMIMEService> mimeFinder(
+ do_GetService(NS_MIMESERVICE_CONTRACTID, &rv));
+ if (mimeFinder) {
+ nsAutoCString type;
+ mimeFinder->GetTypeFromExtension(nsDependentCString(ext), type);
+ retType = ToNewCString(type);
+ }
+ }
+
+ return retType;
+}
+
+int ConvertToUTF8(const char* stringToUse, int32_t inLength,
+ const char* input_charset, nsACString& outString) {
+ nsresult rv = NS_OK;
+
+ // Look up Thunderbird's special aliases from charsetalias.properties.
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ nsCString newCharset;
+ rv = ccm->GetCharsetAlias(input_charset, newCharset);
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ if (newCharset.Equals("UTF-7", nsCaseInsensitiveCStringComparator)) {
+ nsAutoString utf16;
+ rv = CopyUTF7toUTF16(nsDependentCSubstring(stringToUse, inLength), utf16);
+ if (NS_FAILED(rv)) return -1;
+ CopyUTF16toUTF8(utf16, outString);
+ return 0;
+ }
+
+ auto encoding = mozilla::Encoding::ForLabel(newCharset);
+ NS_ENSURE_TRUE(encoding,
+ -1); // Impossible since GetCharsetAlias() already checked.
+
+ rv = encoding->DecodeWithoutBOMHandling(
+ nsDependentCSubstring(stringToUse, inLength), outString);
+ return NS_SUCCEEDED(rv) ? 0 : -1;
+}
+
+static int mime_convert_charset(const char* input_line, int32_t input_length,
+ const char* input_charset,
+ nsACString& convertedString,
+ void* stream_closure) {
+ return ConvertToUTF8(input_line, input_length, input_charset,
+ convertedString);
+}
+
+static int mime_output_fn(const char* buf, int32_t size, void* stream_closure) {
+ uint32_t written = 0;
+ mime_stream_data* msd = (mime_stream_data*)stream_closure;
+ if ((!msd->pluginObj2) && (!msd->output_emitter)) return -1;
+
+ // Fire pending start request
+ ((nsStreamConverter*)msd->pluginObj2)->FirePendingStartRequest();
+
+ // Now, write to the WriteBody method if this is a message body and not
+ // a part retrevial
+ if (!msd->options->part_to_load ||
+ msd->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay) {
+ if (msd->output_emitter) {
+ msd->output_emitter->WriteBody(Substring(buf, buf + size), &written);
+ }
+ } else {
+ if (msd->output_emitter) {
+ msd->output_emitter->Write(Substring(buf, buf + size), &written);
+ }
+ }
+ return written;
+}
+
+extern "C" int mime_display_stream_write(nsMIMESession* stream, const char* buf,
+ int32_t size) {
+ mime_stream_data* msd =
+ (mime_stream_data*)((nsMIMESession*)stream)->data_object;
+
+ MimeObject* obj = (msd ? msd->obj : 0);
+ if (!obj) return -1;
+
+ return obj->clazz->parse_buffer((char*)buf, size, obj);
+}
+
+extern "C" void mime_display_stream_complete(nsMIMESession* stream) {
+ mime_stream_data* msd =
+ (mime_stream_data*)((nsMIMESession*)stream)->data_object;
+ MimeObject* obj = (msd ? msd->obj : 0);
+ if (obj) {
+ int status;
+ bool abortNow = false;
+
+ if ((obj->options) && (obj->options->headers == MimeHeadersOnly))
+ abortNow = true;
+
+ status = obj->clazz->parse_eof(obj, abortNow);
+ obj->clazz->parse_end(obj, (status < 0 ? true : false));
+
+ //
+ // Ok, now we are going to process the attachment data by getting all
+ // of the attachment info and then driving the emitter with this data.
+ //
+ if (!msd->options->part_to_load ||
+ msd->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay) {
+ nsMsgAttachmentData* attachments;
+ nsresult rv = MimeGetAttachmentList(obj, msd->url_name, &attachments);
+ if (NS_SUCCEEDED(rv)) {
+ NotifyEmittersOfAttachmentList(msd->options, attachments);
+ }
+ delete[] attachments;
+ }
+
+ // Release the conversion object - this has to be done after
+ // we finish processing data.
+ if (obj->options) {
+ NS_IF_RELEASE(obj->options->conv);
+ }
+
+ // Destroy the object now.
+ PR_ASSERT(msd->options == obj->options);
+ mime_free(obj);
+ obj = NULL;
+ if (msd->options) {
+ delete msd->options;
+ msd->options = 0;
+ }
+ }
+
+ if (msd->headers) MimeHeaders_free(msd->headers);
+
+ if (msd->url_name) free(msd->url_name);
+
+ if (msd->orig_url_name) free(msd->orig_url_name);
+
+ delete msd;
+}
+
+extern "C" void mime_display_stream_abort(nsMIMESession* stream, int status) {
+ mime_stream_data* msd =
+ (mime_stream_data*)((nsMIMESession*)stream)->data_object;
+
+ MimeObject* obj = (msd ? msd->obj : 0);
+ if (obj) {
+ if (!obj->closed_p) obj->clazz->parse_eof(obj, true);
+ if (!obj->parsed_p) obj->clazz->parse_end(obj, true);
+
+ // Destroy code....
+ PR_ASSERT(msd->options == obj->options);
+ mime_free(obj);
+ if (msd->options) {
+ delete msd->options;
+ msd->options = 0;
+ }
+ }
+
+ if (msd->headers) MimeHeaders_free(msd->headers);
+
+ if (msd->url_name) free(msd->url_name);
+
+ if (msd->orig_url_name) free(msd->orig_url_name);
+
+ delete msd;
+}
+
+static int mime_output_init_fn(const char* type, const char* charset,
+ const char* name, const char* x_mac_type,
+ const char* x_mac_creator,
+ void* stream_closure) {
+ mime_stream_data* msd = (mime_stream_data*)stream_closure;
+
+ // Now, all of this stream creation is done outside of libmime, so this
+ // is just a check of the pluginObj member and returning accordingly.
+ if (!msd->pluginObj2)
+ return -1;
+ else
+ return 0;
+}
+
+static void* mime_image_begin(const char* image_url, const char* content_type,
+ void* stream_closure);
+static void mime_image_end(void* image_closure, int status);
+static char* mime_image_make_image_html(void* image_data);
+static int mime_image_write_buffer(const char* buf, int32_t size,
+ void* image_closure);
+
+/* Interface between libmime and inline display of images: the abomination
+ that is known as "internal-external-reconnect".
+ */
+class mime_image_stream_data {
+ public:
+ mime_image_stream_data();
+
+ mime_stream_data* msd;
+ char* url;
+ nsMIMESession* istream;
+};
+
+mime_image_stream_data::mime_image_stream_data() {
+ url = nullptr;
+ istream = nullptr;
+ msd = nullptr;
+}
+
+static void* mime_image_begin(const char* image_url, const char* content_type,
+ void* stream_closure) {
+ mime_stream_data* msd = (mime_stream_data*)stream_closure;
+ class mime_image_stream_data* mid;
+
+ mid = new mime_image_stream_data;
+ if (!mid) return nullptr;
+
+ mid->msd = msd;
+
+ mid->url = (char*)strdup(image_url);
+ if (!mid->url) {
+ PR_Free(mid);
+ return nullptr;
+ }
+
+ mid->istream = (nsMIMESession*)msd->pluginObj2;
+ return mid;
+}
+
+static void mime_image_end(void* image_closure, int status) {
+ mime_image_stream_data* mid = (mime_image_stream_data*)image_closure;
+
+ PR_ASSERT(mid);
+ if (!mid) return;
+
+ PR_FREEIF(mid->url);
+ delete mid;
+}
+
+static char* mime_image_make_image_html(void* image_closure) {
+ mime_image_stream_data* mid = (mime_image_stream_data*)image_closure;
+
+ PR_ASSERT(mid);
+ if (!mid) return 0;
+
+ /* Internal-external-reconnect only works when going to the screen. */
+ if (!mid->istream)
+ return strdup(
+ "<DIV CLASS=\"moz-attached-image-container\"><IMG "
+ "SRC=\"resource://gre-resources/loading-image.png\" "
+ "ALT=\"[Image]\"></DIV>");
+
+ const char* prefix;
+ const char* url;
+ char* buf;
+ /* Wouldn't it be nice if attributes were case-sensitive? */
+ const char* scaledPrefix =
+ "<DIV CLASS=\"moz-attached-image-container\"><IMG "
+ "CLASS=\"moz-attached-image\" shrinktofit=\"yes\" SRC=\"";
+ const char* suffix = "\"></DIV>";
+ // Thunderbird doesn't have this pref.
+#ifdef MOZ_SUITE
+ const char* unscaledPrefix =
+ "<DIV CLASS=\"moz-attached-image-container\"><IMG "
+ "CLASS=\"moz-attached-image\" SRC=\"";
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ nsCOMPtr<nsIPrefService> prefSvc(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ bool resize = true;
+
+ if (prefSvc) prefSvc->GetBranch("", getter_AddRefs(prefBranch));
+ if (prefBranch)
+ prefBranch->GetBoolPref("mail.enable_automatic_image_resizing",
+ &resize); // ignore return value
+ prefix = resize ? scaledPrefix : unscaledPrefix;
+#else
+ prefix = scaledPrefix;
+#endif
+
+ if ((!mid->url) || (!(*mid->url)))
+ url = "";
+ else
+ url = mid->url;
+
+ uint32_t buflen = strlen(prefix) + strlen(suffix) + strlen(url) + 20;
+ buf = (char*)PR_MALLOC(buflen);
+ if (!buf) return 0;
+ *buf = 0;
+
+ PL_strcatn(buf, buflen, prefix);
+ PL_strcatn(buf, buflen, url);
+ PL_strcatn(buf, buflen, suffix);
+ return buf;
+}
+
+static int mime_image_write_buffer(const char* buf, int32_t size,
+ void* image_closure) {
+ mime_image_stream_data* mid = (mime_image_stream_data*)image_closure;
+ mime_stream_data* msd = mid->msd;
+
+ if (((!msd->output_emitter)) && ((!msd->pluginObj2))) return -1;
+
+ return size;
+}
+
+MimeObject* mime_get_main_object(MimeObject* obj) {
+ MimeContainer* cobj;
+ if (!(mime_subclass_p(obj->clazz, (MimeObjectClass*)&mimeMessageClass))) {
+ return obj;
+ }
+ cobj = (MimeContainer*)obj;
+ if (cobj->nchildren != 1) return obj;
+ obj = cobj->children[0];
+ while (obj) {
+ if ((!mime_subclass_p(obj->clazz,
+ (MimeObjectClass*)&mimeMultipartSignedClass)) &&
+ (PL_strcasecmp(obj->content_type, MULTIPART_SIGNED) != 0)) {
+ return obj;
+ } else {
+ if (mime_subclass_p(obj->clazz, (MimeObjectClass*)&mimeContainerClass)) {
+ // We don't care about a signed/smime object; Go inside to the
+ // thing that we signed or smime'ed
+ //
+ cobj = (MimeContainer*)obj;
+ if (cobj->nchildren > 0)
+ obj = cobj->children[0];
+ else
+ obj = nullptr;
+ } else {
+ // we received a message with a child object that looks like a signed
+ // object, but it is not a subclass of mimeContainer, so let's
+ // return the given child object.
+ return obj;
+ }
+ }
+ }
+ return nullptr;
+}
+
+static bool MimeObjectIsMessageBodyNoClimb(MimeObject* parent,
+ MimeObject* looking_for,
+ bool* stop) {
+ MimeContainer* container = (MimeContainer*)parent;
+ int32_t i;
+ char* disp;
+
+ NS_ASSERTION(stop, "NULL stop to MimeObjectIsMessageBodyNoClimb");
+
+ for (i = 0; i < container->nchildren; i++) {
+ MimeObject* child = container->children[i];
+ bool is_body = true;
+
+ // The body can't be something we're not displaying.
+ if (!child->output_p)
+ is_body = false;
+ else if ((disp = MimeHeaders_get(child->headers, HEADER_CONTENT_DISPOSITION,
+ true, false))) {
+ PR_Free(disp);
+ is_body = false;
+ } else if (PL_strcasecmp(child->content_type, TEXT_PLAIN) &&
+ PL_strcasecmp(child->content_type, TEXT_HTML) &&
+ PL_strcasecmp(child->content_type, TEXT_MDL) &&
+ PL_strcasecmp(child->content_type, MESSAGE_NEWS) &&
+ PL_strcasecmp(child->content_type, MESSAGE_RFC822))
+ is_body = false;
+
+ if (is_body || child == looking_for) {
+ *stop = true;
+ return child == looking_for;
+ }
+
+ // The body could be down inside a multipart child, so search recursively.
+ if (mime_subclass_p(child->clazz, (MimeObjectClass*)&mimeContainerClass)) {
+ is_body = MimeObjectIsMessageBodyNoClimb(child, looking_for, stop);
+ if (is_body || *stop) return is_body;
+ }
+ }
+ return false;
+}
+
+/* Should this be static in mimemult.cpp? */
+bool MimeObjectIsMessageBody(MimeObject* looking_for) {
+ bool stop = false;
+ MimeObject* root = looking_for;
+ while (root->parent) root = root->parent;
+ return MimeObjectIsMessageBodyNoClimb(root, looking_for, &stop);
+}
+
+//
+// New Stream Converter Interface
+//
+
+// Get the connection to prefs service manager
+nsIPrefBranch* GetPrefBranch(MimeDisplayOptions* opt) {
+ if (!opt) return nullptr;
+
+ return opt->m_prefBranch;
+}
+
+// Get the text converter...
+mozITXTToHTMLConv* GetTextConverter(MimeDisplayOptions* opt) {
+ if (!opt) return nullptr;
+
+ return opt->conv;
+}
+
+MimeDisplayOptions::MimeDisplayOptions() {
+ conv = nullptr; // For text conversion...
+ format_out = 0; // The format out type
+ url = nullptr;
+
+ memset(&headers, 0, sizeof(headers));
+ fancy_headers_p = false;
+
+ output_vcard_buttons_p = false;
+
+ variable_width_plaintext_p = false;
+ wrap_long_lines_p = false;
+ rot13_p = false;
+ part_to_load = nullptr;
+
+ no_output_p = false;
+ write_html_p = false;
+
+ decrypt_p = false;
+
+ whattodo = 0;
+ default_charset = nullptr;
+ override_charset = false;
+ force_user_charset = false;
+ stream_closure = nullptr;
+
+ /* For setting up the display stream, so that the MIME parser can inform
+ the caller of the type of the data it will be getting. */
+ output_init_fn = nullptr;
+ output_fn = nullptr;
+
+ output_closure = nullptr;
+
+ charset_conversion_fn = nullptr;
+ rfc1522_conversion_p = false;
+
+ file_type_fn = nullptr;
+
+ passwd_prompt_fn = nullptr;
+
+ html_closure = nullptr;
+
+ generate_header_html_fn = nullptr;
+ generate_post_header_html_fn = nullptr;
+ generate_footer_html_fn = nullptr;
+ generate_reference_url_fn = nullptr;
+ generate_mailto_url_fn = nullptr;
+ generate_news_url_fn = nullptr;
+
+ image_begin = nullptr;
+ image_end = nullptr;
+ image_write_buffer = nullptr;
+ make_image_html = nullptr;
+ state = nullptr;
+
+#ifdef MIME_DRAFTS
+ decompose_file_p = false;
+ done_parsing_outer_headers = false;
+ is_multipart_msg = false;
+ decompose_init_count = 0;
+
+ signed_p = false;
+ caller_need_root_headers = false;
+ decompose_headers_info_fn = nullptr;
+ decompose_file_init_fn = nullptr;
+ decompose_file_output_fn = nullptr;
+ decompose_file_close_fn = nullptr;
+#endif /* MIME_DRAFTS */
+
+ attachment_icon_layer_id = 0;
+
+ missing_parts = false;
+ show_attachment_inline_p = false;
+ show_attachment_inline_text = false;
+ quote_attachment_inline_p = false;
+ notify_nested_bodies = false;
+ write_pure_bodies = false;
+ metadata_only = false;
+}
+
+MimeDisplayOptions::~MimeDisplayOptions() {
+ PR_FREEIF(part_to_load);
+ PR_FREEIF(default_charset);
+}
+////////////////////////////////////////////////////////////////
+// Bridge routines for new stream converter XP-COM interface
+////////////////////////////////////////////////////////////////
+extern "C" void* mime_bridge_create_display_stream(
+ nsIMimeEmitter* newEmitter, nsStreamConverter* newPluginObj2, nsIURI* uri,
+ nsMimeOutputType format_out, uint32_t whattodo, nsIChannel* aChannel) {
+ int status = 0;
+ MimeObject* obj;
+ mime_stream_data* msd;
+ nsMIMESession* stream = 0;
+
+ if (!uri) return nullptr;
+
+ msd = new mime_stream_data;
+ if (!msd) return NULL;
+
+ // Assign the new mime emitter - will handle output operations
+ msd->output_emitter = newEmitter;
+
+ // Store the URL string for this decode operation
+ nsAutoCString urlString;
+ nsresult rv;
+
+ // Keep a hold of the channel...
+ msd->channel = aChannel;
+ rv = uri->GetSpec(urlString);
+ if (NS_SUCCEEDED(rv)) {
+ if (!urlString.IsEmpty()) {
+ msd->url_name = ToNewCString(urlString);
+ if (!(msd->url_name)) {
+ delete msd;
+ return NULL;
+ }
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(uri);
+ if (msgUrl) {
+ nsAutoCString orgSpec;
+ msgUrl->GetOriginalSpec(orgSpec);
+ msd->orig_url_name = ToNewCString(orgSpec);
+ }
+ }
+ }
+
+ msd->format_out = format_out; // output format
+ msd->pluginObj2 = newPluginObj2; // the plugin object pointer
+
+ msd->options = new MimeDisplayOptions;
+ if (!msd->options) {
+ delete msd;
+ return 0;
+ }
+ // memset(msd->options, 0, sizeof(*msd->options));
+ msd->options->format_out = format_out; // output format
+
+ msd->options->m_prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ delete msd;
+ return nullptr;
+ }
+
+ // Need the text converter...
+ rv = CallCreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID, &(msd->options->conv));
+ if (NS_FAILED(rv)) {
+ msd->options->m_prefBranch = nullptr;
+ delete msd;
+ return nullptr;
+ }
+
+ //
+ // Set the defaults, based on the context, and the output-type.
+ //
+ MIME_HeaderType = MimeHeadersAll;
+ msd->options->write_html_p = true;
+ switch (format_out) {
+ case nsMimeOutput::nsMimeMessageSplitDisplay: // the wrapper HTML output to
+ // produce the split
+ // header/body display
+ case nsMimeOutput::nsMimeMessageHeaderDisplay: // the split header/body
+ // display
+ case nsMimeOutput::nsMimeMessageBodyDisplay: // the split header/body
+ // display
+ msd->options->fancy_headers_p = true;
+ msd->options->output_vcard_buttons_p = true;
+ break;
+
+ case nsMimeOutput::nsMimeMessageSaveAs: // Save As operations
+ case nsMimeOutput::nsMimeMessageQuoting: // all HTML quoted/printed output
+ case nsMimeOutput::nsMimeMessagePrintOutput:
+ msd->options->fancy_headers_p = true;
+ break;
+
+ case nsMimeOutput::nsMimeMessageBodyQuoting: // only HTML body quoted
+ // output
+ MIME_HeaderType = MimeHeadersNone;
+ break;
+
+ case nsMimeOutput::nsMimeMessageAttach: // handling attachment storage
+ msd->options->write_html_p = false;
+ break;
+ case nsMimeOutput::nsMimeMessageRaw: // the raw RFC822 data (view source)
+ // and attachments
+ case nsMimeOutput::nsMimeMessageDraftOrTemplate: // Loading drafts &
+ // templates
+ case nsMimeOutput::nsMimeMessageEditorTemplate: // Loading templates into
+ // editor
+ case nsMimeOutput::nsMimeMessageFilterSniffer: // generating an output that
+ // can be scan by a message
+ // filter
+ break;
+
+ case nsMimeOutput::nsMimeMessageDecrypt:
+ msd->options->decrypt_p = true;
+ msd->options->write_html_p = false;
+ break;
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Now, get the libmime prefs...
+ ////////////////////////////////////////////////////////////
+
+ MIME_WrapLongLines = true;
+ MIME_VariableWidthPlaintext = true;
+ msd->options->force_user_charset = false;
+
+ if (msd->options->m_prefBranch) {
+ msd->options->m_prefBranch->GetBoolPref("mail.wrap_long_lines",
+ &MIME_WrapLongLines);
+ msd->options->m_prefBranch->GetBoolPref("mail.fixed_width_messages",
+ &MIME_VariableWidthPlaintext);
+ //
+ // Charset overrides takes place here
+ //
+ // We have a bool pref (mail.force_user_charset) to deal with attachments.
+ // 1) If true - libmime does NO conversion and just passes it through to
+ // raptor
+ // 2) If false, then we try to use the charset of the part and if not
+ // available, the charset of the root message
+ //
+ msd->options->m_prefBranch->GetBoolPref(
+ "mail.force_user_charset", &(msd->options->force_user_charset));
+ msd->options->m_prefBranch->GetBoolPref(
+ "mail.inline_attachments", &(msd->options->show_attachment_inline_p));
+ msd->options->m_prefBranch->GetBoolPref(
+ "mail.inline_attachments.text",
+ &(msd->options->show_attachment_inline_text));
+ msd->options->m_prefBranch->GetBoolPref(
+ "mail.reply_quote_inline", &(msd->options->quote_attachment_inline_p));
+ msd->options->m_prefBranch->GetIntPref("mailnews.display.html_as",
+ &(msd->options->html_as_p));
+ }
+ /* This pref is written down in with the
+ opposite sense of what we like to use... */
+ MIME_VariableWidthPlaintext = !MIME_VariableWidthPlaintext;
+
+ msd->options->wrap_long_lines_p = MIME_WrapLongLines;
+ msd->options->headers = MIME_HeaderType;
+
+ // We need to have the URL to be able to support the various
+ // arguments
+ status = mime_parse_url_options(msd->url_name, msd->options);
+ if (status < 0) {
+ PR_FREEIF(msd->options->part_to_load);
+ PR_Free(msd->options);
+ delete msd;
+ return 0;
+ }
+
+ if (msd->options->headers == MimeHeadersMicro &&
+ (msd->url_name == NULL || (strncmp(msd->url_name, "news:", 5) != 0 &&
+ strncmp(msd->url_name, "snews:", 6) != 0)))
+ msd->options->headers = MimeHeadersMicroPlus;
+
+ msd->options->url = msd->url_name;
+ msd->options->output_init_fn = mime_output_init_fn;
+
+ msd->options->output_fn = mime_output_fn;
+
+ msd->options->whattodo = whattodo;
+ msd->options->charset_conversion_fn = mime_convert_charset;
+ msd->options->rfc1522_conversion_p = true;
+ msd->options->file_type_fn = mime_file_type;
+ msd->options->stream_closure = msd;
+ msd->options->passwd_prompt_fn = 0;
+
+ msd->options->image_begin = mime_image_begin;
+ msd->options->image_end = mime_image_end;
+ msd->options->make_image_html = mime_image_make_image_html;
+ msd->options->image_write_buffer = mime_image_write_buffer;
+
+ msd->options->variable_width_plaintext_p = MIME_VariableWidthPlaintext;
+
+ // If this is a part, then we should emit the HTML to render the data
+ // (i.e. embedded images)
+ if (msd->options->part_to_load &&
+ msd->options->format_out != nsMimeOutput::nsMimeMessageBodyDisplay)
+ msd->options->write_html_p = false;
+
+ obj = mime_new((MimeObjectClass*)&mimeMessageClass, (MimeHeaders*)NULL,
+ MESSAGE_RFC822);
+ if (!obj) {
+ delete msd->options;
+ delete msd;
+ return 0;
+ }
+
+ obj->options = msd->options;
+ msd->obj = obj;
+
+ /* Both of these better not be true at the same time. */
+ PR_ASSERT(!(obj->options->decrypt_p && obj->options->write_html_p));
+
+ stream = PR_NEW(nsMIMESession);
+ if (!stream) {
+ delete msd->options;
+ delete msd;
+ PR_Free(obj);
+ return 0;
+ }
+
+ memset(stream, 0, sizeof(*stream));
+ stream->name = "MIME Conversion Stream";
+ stream->complete = mime_display_stream_complete;
+ stream->abort = mime_display_stream_abort;
+ stream->put_block = mime_display_stream_write;
+ stream->data_object = msd;
+
+ status = obj->clazz->initialize(obj);
+ if (status >= 0) status = obj->clazz->parse_begin(obj);
+ if (status < 0) {
+ PR_Free(stream);
+ delete msd->options;
+ delete msd;
+ PR_Free(obj);
+ return 0;
+ }
+
+ return stream;
+}
+
+//
+// Emitter Wrapper Routines!
+//
+nsIMimeEmitter* GetMimeEmitter(MimeDisplayOptions* opt) {
+ mime_stream_data* msd = (mime_stream_data*)opt->stream_closure;
+ if (!msd) return NULL;
+
+ nsIMimeEmitter* ptr = (nsIMimeEmitter*)(msd->output_emitter);
+ return ptr;
+}
+
+mime_stream_data* GetMSD(MimeDisplayOptions* opt) {
+ if (!opt) return nullptr;
+ mime_stream_data* msd = (mime_stream_data*)opt->stream_closure;
+ return msd;
+}
+
+bool NoEmitterProcessing(nsMimeOutputType format_out) {
+ if (format_out == nsMimeOutput::nsMimeMessageDraftOrTemplate ||
+ format_out == nsMimeOutput::nsMimeMessageEditorTemplate ||
+ format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ format_out == nsMimeOutput::nsMimeMessageBodyQuoting)
+ return true;
+ else
+ return false;
+}
+
+extern "C" nsresult mimeEmitterAddAttachmentField(MimeDisplayOptions* opt,
+ const char* field,
+ const char* value) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->AddAttachmentField(field, value);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterAddHeaderField(MimeDisplayOptions* opt,
+ const char* field,
+ const char* value) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->AddHeaderField(field, value);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterAddAllHeaders(MimeDisplayOptions* opt,
+ const char* allheaders,
+ const int32_t allheadersize) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->AddAllHeaders(
+ Substring(allheaders, allheaders + allheadersize));
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterStartAttachment(MimeDisplayOptions* opt,
+ const char* name,
+ const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->StartAttachment(nsDependentCString(name), contentType, url,
+ aIsExternalAttachment);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterEndAttachment(MimeDisplayOptions* opt) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ if (emitter)
+ return emitter->EndAttachment();
+ else
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterEndAllAttachments(MimeDisplayOptions* opt) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ if (emitter)
+ return emitter->EndAllAttachments();
+ else
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterStartBody(MimeDisplayOptions* opt, bool bodyOnly,
+ const char* msgID,
+ const char* outCharset) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->StartBody(bodyOnly, msgID, outCharset);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterEndBody(MimeDisplayOptions* opt) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->EndBody();
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterEndHeader(MimeDisplayOptions* opt,
+ MimeObject* obj) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+
+ nsCString name;
+ if (msd->format_out == nsMimeOutput::nsMimeMessageSplitDisplay ||
+ msd->format_out == nsMimeOutput::nsMimeMessageHeaderDisplay ||
+ msd->format_out == nsMimeOutput::nsMimeMessageBodyDisplay ||
+ msd->format_out == nsMimeOutput::nsMimeMessageSaveAs ||
+ msd->format_out == nsMimeOutput::nsMimeMessagePrintOutput) {
+ if (obj->headers) {
+ nsMsgAttachmentData attachment;
+ attIndex = 0;
+ nsresult rv = GenerateAttachmentData(obj, msd->url_name, opt, false, 0,
+ &attachment);
+
+ if (NS_SUCCEEDED(rv)) name.Assign(attachment.m_realName);
+ }
+ }
+
+ MimeHeaders_convert_header_value(opt, name, false);
+ return emitter->EndHeader(name);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterUpdateCharacterSet(MimeDisplayOptions* opt,
+ const char* aCharset) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->UpdateCharacterSet(aCharset);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeEmitterStartHeader(MimeDisplayOptions* opt,
+ bool rootMailHeader, bool headerOnly,
+ const char* msgID,
+ const char* outCharset) {
+ // Check for draft processing...
+ if (NoEmitterProcessing(opt->format_out)) return NS_OK;
+
+ mime_stream_data* msd = GetMSD(opt);
+ if (!msd) return NS_ERROR_FAILURE;
+
+ if (msd->output_emitter) {
+ nsIMimeEmitter* emitter = (nsIMimeEmitter*)msd->output_emitter;
+ return emitter->StartHeader(rootMailHeader, headerOnly, msgID, outCharset);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+extern "C" nsresult mimeSetNewURL(nsMIMESession* stream, char* url) {
+ if ((!stream) || (!url) || (!*url)) return NS_ERROR_FAILURE;
+
+ mime_stream_data* msd = (mime_stream_data*)stream->data_object;
+ if (!msd) return NS_ERROR_FAILURE;
+
+ char* tmpPtr = strdup(url);
+ if (!tmpPtr) return NS_ERROR_OUT_OF_MEMORY;
+
+ PR_FREEIF(msd->url_name);
+ msd->url_name = tmpPtr;
+ return NS_OK;
+}
+
+#define MIME_URL "chrome://messenger/locale/mime.properties"
+
+extern "C" char* MimeGetStringByID(int32_t stringID) {
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ stringBundleService->CreateBundle(MIME_URL, getter_AddRefs(stringBundle));
+ if (stringBundle) {
+ nsString v;
+ if (NS_SUCCEEDED(stringBundle->GetStringFromID(stringID, v)))
+ return ToNewUTF8String(v);
+ }
+
+ return strdup("???");
+}
+
+extern "C" char* MimeGetStringByName(const char16_t* stringName) {
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ stringBundleService->CreateBundle(MIME_URL, getter_AddRefs(stringBundle));
+ if (stringBundle) {
+ nsString v;
+ if (NS_SUCCEEDED(stringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(stringName).get(), v)))
+ return ToNewUTF8String(v);
+ }
+
+ return strdup("???");
+}
+
+void ResetChannelCharset(MimeObject* obj) {
+ if (obj->options && obj->options->stream_closure &&
+ obj->options->default_charset && obj->headers) {
+ mime_stream_data* msd = (mime_stream_data*)(obj->options->stream_closure);
+ char* ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ if (ct && msd && msd->channel) {
+ char* cSet = MimeHeaders_get_parameter(ct, "charset", nullptr, nullptr);
+ if (cSet) {
+ // The content-type does specify a charset. First, setup the channel.
+ msd->channel->SetContentType(nsDependentCString(ct));
+
+ // Second, if this is a Save As operation, then we need to convert
+ // to override the output charset.
+ if (msd->format_out == nsMimeOutput::nsMimeMessageSaveAs) {
+ // The previous version of this code would have entered an infinite
+ // loop. But it never showed up, so it's not clear that we ever get
+ // here... See bug #1597891.
+ PR_FREEIF(obj->options->default_charset);
+ obj->options->default_charset = cSet;
+ cSet = nullptr; // Ownership was transferred.
+ obj->options->override_charset = true;
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "Ahh. So this code _is_ run after all! (see bug 1597891)");
+ }
+ PR_FREEIF(cSet);
+ }
+ }
+ PR_FREEIF(ct);
+ }
+}
+
+////////////////////////////////////////////////////////////
+// Function to get up mail/news fontlang
+////////////////////////////////////////////////////////////
+
+nsresult GetMailNewsFont(MimeObject* obj, bool styleFixed,
+ int32_t* fontPixelSize, int32_t* fontSizePercentage,
+ nsCString& fontLang) {
+ nsresult rv = NS_OK;
+
+ nsIPrefBranch* prefBranch = GetPrefBranch(obj->options);
+ if (prefBranch) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+ nsAutoCString charset;
+
+ // get a charset
+ if (!text->initializeCharset)
+ ((MimeInlineTextClass*)&mimeInlineTextClass)->initialize_charset(obj);
+
+ if (!text->charset || !(*text->charset))
+ charset.AssignLiteral("us-ascii");
+ else
+ charset.Assign(text->charset);
+
+ nsCOMPtr<nsICharsetConverterManager> charSetConverterManager2;
+ nsAutoCString prefStr;
+
+ ToLowerCase(charset);
+
+ charSetConverterManager2 =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // get a language, e.g. x-western, ja
+ rv = charSetConverterManager2->GetCharsetLangGroup(charset.get(), fontLang);
+ if (NS_FAILED(rv)) return rv;
+
+ // get a font size from pref
+ prefStr.Assign(!styleFixed ? "font.size.variable."
+ : "font.size.monospace.");
+ prefStr.Append(fontLang);
+ rv = prefBranch->GetIntPref(prefStr.get(), fontPixelSize);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIPrefBranch> prefDefBranch;
+ nsCOMPtr<nsIPrefService> prefSvc(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (prefSvc)
+ rv = prefSvc->GetDefaultBranch("", getter_AddRefs(prefDefBranch));
+
+ if (!prefDefBranch) return rv;
+
+ // get original font size
+ int32_t originalSize;
+ rv = prefDefBranch->GetIntPref(prefStr.get(), &originalSize);
+ if (NS_FAILED(rv)) return rv;
+
+ // calculate percentage
+ *fontSizePercentage =
+ originalSize
+ ? (int32_t)((float)*fontPixelSize / (float)originalSize * 100)
+ : 0;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * This function synchronously converts an HTML document (as string)
+ * to plaintext (as string) using the Gecko converter.
+ *
+ * @param flags see nsIDocumentEncoder.h
+ */
+nsresult HTML2Plaintext(const nsString& inString, nsString& outString,
+ uint32_t flags, uint32_t wrapCol) {
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(inString, flags, wrapCol, outString);
+}
+
+/**
+ * This function synchronously sanitizes an HTML document (string->string)
+ * using the Gecko nsTreeSanitizer.
+ */
+nsresult HTMLSanitize(const nsString& inString, nsString& outString) {
+ // If you want to add alternative sanitization, you can insert a conditional
+ // call to another sanitizer and an early return here.
+
+ uint32_t flags = nsIParserUtils::SanitizerCidEmbedsOnly |
+ nsIParserUtils::SanitizerDropForms;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ bool dropPresentational = true;
+ bool dropMedia = false;
+ prefs->GetBoolPref(
+ "mailnews.display.html_sanitizer.drop_non_css_presentation",
+ &dropPresentational);
+ prefs->GetBoolPref("mailnews.display.html_sanitizer.drop_media", &dropMedia);
+ if (dropPresentational)
+ flags |= nsIParserUtils::SanitizerDropNonCSSPresentation;
+ if (dropMedia) flags |= nsIParserUtils::SanitizerDropMedia;
+
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->Sanitize(inString, flags, outString);
+}
diff --git a/comm/mailnews/mime/src/mimemoz2.h b/comm/mailnews/mime/src/mimemoz2.h
new file mode 100644
index 0000000000..daec700240
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemoz2.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMOZ_H_
+#define _MIMEMOZ_H_
+
+#include "nsStreamConverter.h"
+#include "nsIMimeEmitter.h"
+#include "nsIURI.h"
+#include "mozITXTToHTMLConv.h"
+#include "modmimee.h"
+#include "nsMsgAttachmentData.h"
+
+// SHERRY - Need to get these out of here eventually
+
+#ifdef XP_UNIX
+# undef Bool
+#endif
+
+#include "mimei.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "nsIPrefBranch.h"
+
+typedef struct _nsMIMESession nsMIMESession;
+
+/* stream functions */
+typedef unsigned int (*MKSessionWriteReadyFunc)(nsMIMESession* stream);
+
+#define MAX_WRITE_READY \
+ (((unsigned)(~0) << 1) >> 1) /* must be <= than MAXINT!!!!! */
+
+typedef int (*MKSessionWriteFunc)(nsMIMESession* stream, const char* str,
+ int32_t len);
+
+typedef void (*MKSessionCompleteFunc)(nsMIMESession* stream);
+
+typedef void (*MKSessionAbortFunc)(nsMIMESession* stream, int status);
+
+/* streamclass function */
+struct _nsMIMESession {
+ const char* name; /* Just for diagnostics */
+
+ void* window_id; /* used for progress messages, etc. */
+
+ void* data_object; /* a pointer to whatever
+ * structure you wish to have
+ * passed to the routines below
+ * during writes, etc...
+ *
+ * this data object should hold
+ * the document, document
+ * structure or a pointer to the
+ * document.
+ */
+
+ MKSessionWriteReadyFunc is_write_ready; /* checks to see if the stream is
+ * ready for writing. Returns 0 if
+ * not ready or the number of bytes
+ * that it can accept for write
+ */
+ MKSessionWriteFunc put_block; /* writes a block of data to the stream */
+ MKSessionCompleteFunc complete; /* normal end */
+ MKSessionAbortFunc abort; /* abnormal end */
+
+ bool is_multipart; /* is the stream part of a multipart sequence */
+};
+
+/*
+ * This is for the reworked mime parser.
+ */
+class mime_stream_data { /* This object is the state we pass around
+ amongst the various stream functions
+ used by MIME_MessageConverter(). */
+ public:
+ mime_stream_data();
+
+ char* url_name;
+ char* orig_url_name; /* original url name */
+ nsCOMPtr<nsIChannel> channel;
+ nsMimeOutputType format_out;
+ void* pluginObj2; /* The new XP-COM stream converter object */
+ nsMIMESession*
+ istream; /* Holdover - new stream we're writing out image data-if any. */
+ MimeObject* obj; /* The root parser object */
+ MimeDisplayOptions* options; /* Data for communicating with libmime.a */
+ MimeHeaders* headers; /* Copy of outer most mime header */
+
+ nsIMimeEmitter* output_emitter; /* Output emitter engine for libmime */
+};
+
+//
+// This object is the state we use for loading drafts and templates...
+//
+class mime_draft_data {
+ public:
+ mime_draft_data();
+ char* url_name; // original url name */
+ nsMimeOutputType
+ format_out; // intended output format; should be FO_OPEN_DRAFT */
+ nsMIMESession* stream; // not used for now
+ MimeObject* obj; // The root
+ MimeDisplayOptions* options; // data for communicating with libmime
+ MimeHeaders* headers; // Copy of outer most mime header
+ nsTArray<nsMsgAttachedFile*> attachments; // attachments
+ nsMsgAttachedFile* messageBody; // message body
+ nsMsgAttachedFile* curAttachment; // temp
+
+ nsCOMPtr<nsIFile> tmpFile;
+ nsCOMPtr<nsIOutputStream> tmpFileStream; // output file handle
+
+ MimeDecoderData* decoder_data;
+ char* mailcharset; // get it from CHARSET of Content-Type
+ bool forwardInline;
+ bool forwardInlineFilter;
+ bool overrideComposeFormat; // Override compose format (for forward inline).
+ nsString forwardToAddress;
+ nsCOMPtr<nsIMsgIdentity> identity;
+ nsCString originalMsgURI; // the original URI of the message we are currently
+ // processing
+ nsCOMPtr<nsIMsgDBHdr> origMsgHdr;
+ bool autodetectCharset; // Used to indicate pending autodetection while
+ // streaming contents.
+};
+
+////////////////////////////////////////////////////////////////
+// Bridge routines for legacy mime code
+////////////////////////////////////////////////////////////////
+
+// Create bridge stream for libmime
+extern "C" void* mime_bridge_create_display_stream(
+ nsIMimeEmitter* newEmitter, nsStreamConverter* newPluginObj2, nsIURI* uri,
+ nsMimeOutputType format_out, uint32_t whattodo, nsIChannel* aChannel);
+
+// To get the mime emitter...
+extern "C" nsIMimeEmitter* GetMimeEmitter(MimeDisplayOptions* opt);
+
+// To support 2 types of emitters...we need these routines :-(
+extern "C" nsresult mimeSetNewURL(nsMIMESession* stream, char* url);
+extern "C" nsresult mimeEmitterAddAttachmentField(MimeDisplayOptions* opt,
+ const char* field,
+ const char* value);
+extern "C" nsresult mimeEmitterAddHeaderField(MimeDisplayOptions* opt,
+ const char* field,
+ const char* value);
+extern "C" nsresult mimeEmitterAddAllHeaders(MimeDisplayOptions* opt,
+ const char* allheaders,
+ const int32_t allheadersize);
+extern "C" nsresult mimeEmitterStartAttachment(MimeDisplayOptions* opt,
+ const char* name,
+ const char* contentType,
+ const char* url,
+ bool aIsExternalAttachment);
+extern "C" nsresult mimeEmitterEndAttachment(MimeDisplayOptions* opt);
+extern "C" nsresult mimeEmitterEndAllAttachments(MimeDisplayOptions* opt);
+extern "C" nsresult mimeEmitterStartBody(MimeDisplayOptions* opt, bool bodyOnly,
+ const char* msgID,
+ const char* outCharset);
+extern "C" nsresult mimeEmitterEndBody(MimeDisplayOptions* opt);
+extern "C" nsresult mimeEmitterEndHeader(MimeDisplayOptions* opt,
+ MimeObject* obj);
+extern "C" nsresult mimeEmitterStartHeader(MimeDisplayOptions* opt,
+ bool rootMailHeader, bool headerOnly,
+ const char* msgID,
+ const char* outCharset);
+extern "C" nsresult mimeEmitterUpdateCharacterSet(MimeDisplayOptions* opt,
+ const char* aCharset);
+
+extern "C" nsresult MimeGetAttachmentList(MimeObject* tobj,
+ const char* aMessageURL,
+ nsMsgAttachmentData** data);
+
+/* To Get the connection to prefs service manager */
+extern "C" nsIPrefBranch* GetPrefBranch(MimeDisplayOptions* opt);
+
+// Get the text converter...
+mozITXTToHTMLConv* GetTextConverter(MimeDisplayOptions* opt);
+
+nsresult HTML2Plaintext(const nsString& inString, nsString& outString,
+ uint32_t flags, uint32_t wrapCol);
+nsresult HTMLSanitize(const nsString& inString, nsString& outString);
+
+extern "C" char* MimeGetStringByID(int32_t stringID);
+extern "C" char* MimeGetStringByName(const char16_t* stringName);
+
+// Utility to create a nsIURI object...
+extern "C" nsresult nsMimeNewURI(nsIURI** aInstancePtrResult, const char* aSpec,
+ nsIURI* aBase);
+
+extern "C" nsresult SetMailCharacterSetToMsgWindow(MimeObject* obj,
+ const char* aCharacterSet);
+
+extern "C" nsresult GetMailNewsFont(MimeObject* obj, bool styleFixed,
+ int32_t* fontPixelSize,
+ int32_t* fontSizePercentage,
+ nsCString& fontLang);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* _MIMEMOZ_H_ */
diff --git a/comm/mailnews/mime/src/mimempar.cpp b/comm/mailnews/mime/src/mimempar.cpp
new file mode 100644
index 0000000000..2c27e1e95d
--- /dev/null
+++ b/comm/mailnews/mime/src/mimempar.cpp
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimempar.h"
+#include "prlog.h"
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartParallel, MimeMultipartParallelClass,
+ mimeMultipartParallelClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartParallelClassInitialize(
+ MimeMultipartParallelClass* clazz) {
+#ifdef DEBUG
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+#endif
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimempar.h b/comm/mailnews/mime/src/mimempar.h
new file mode 100644
index 0000000000..7c29f67331
--- /dev/null
+++ b/comm/mailnews/mime/src/mimempar.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMPAR_H_
+#define _MIMEMPAR_H_
+
+#include "mimemult.h"
+
+/* The MimeMultipartParallel class implements the multipart/parallel MIME
+ container, which is currently no different from multipart/mixed, since
+ it's not clear that there's anything useful it could do differently.
+ */
+
+typedef struct MimeMultipartParallelClass MimeMultipartParallelClass;
+typedef struct MimeMultipartParallel MimeMultipartParallel;
+
+struct MimeMultipartParallelClass {
+ MimeMultipartClass multipart;
+};
+
+extern MimeMultipartParallelClass mimeMultipartParallelClass;
+
+struct MimeMultipartParallel {
+ MimeMultipart multipart;
+};
+
+#define MimeMultipartParallelClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMPAR_H_ */
diff --git a/comm/mailnews/mime/src/mimemrel.cpp b/comm/mailnews/mime/src/mimemrel.cpp
new file mode 100644
index 0000000000..b3efb3323d
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemrel.cpp
@@ -0,0 +1,1113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+/* Thoughts on how to implement this:
+
+ = if the type of this multipart/related is not text/html, then treat
+ it the same as multipart/mixed.
+
+ = For each part in this multipart/related
+ = if this part is not the "top" part
+ = then save this part to a tmp file or a memory object,
+ kind-of like what we do for multipart/alternative sub-parts.
+ If this is an object we're blocked on (see below) send its data along.
+ = else
+ = emit this part (remember, it's of type text/html)
+ = at some point, layout may load a URL for <IMG SRC="cid:xxxx">.
+ we intercept that.
+ = if one of our cached parts has that cid, return the data for it.
+ = else, "block", the same way the image library blocks layout when it
+ doesn't yet have the size of the image.
+ = at some point, layout may load a URL for <IMG SRC="relative/yyy">.
+ we need to intercept that too.
+ = expand the URL, and compare it to our cached objects.
+ if it matches, return it.
+ = else block on it.
+
+ = once we get to the end, if we have any sub-part references that we're
+ still blocked on, map over them:
+ = if they're cid: references, close them ("broken image" results.)
+ = if they're URLs, then load them in the normal way.
+
+ --------------------------------------------------
+
+ Ok, that's fairly complicated. How about an approach where we go through
+ all the parts first, and don't emit until the end?
+
+ = if the type of this multipart/related is not text/html, then treat
+ it the same as multipart/mixed.
+
+ = For each part in this multipart/related
+ = save this part to a tmp file or a memory object,
+ like what we do for multipart/alternative sub-parts.
+
+ = Emit the "top" part (the text/html one)
+ = intercept all calls to NET_GetURL, to allow us to rewrite the URL.
+ (hook into netlib, or only into imglib's calls to GetURL?)
+ (make sure we're behaving in a context-local way.)
+
+ = when a URL is loaded, look through our cached parts for a match.
+ = if we find one, map that URL to a "cid:" URL
+ = else, let it load normally
+
+ = at some point, layout may load a URL for <IMG SRC="cid:xxxx">.
+ it will do this either because that's what was in the HTML, or because
+ that's how we "rewrote" the URLs when we intercepted NET_GetURL.
+
+ = if one of our cached parts has the requested cid, return the data
+ for it.
+ = else, generate a "broken image"
+
+ = free all the cached data
+
+ --------------------------------------------------
+
+ How hard would be an approach where we rewrite the HTML?
+ (Looks like it's not much easier, and might be more error-prone.)
+
+ = if the type of this multipart/related is not text/html, then treat
+ it the same as multipart/mixed.
+
+ = For each part in this multipart/related
+ = save this part to a tmp file or a memory object,
+ like what we do for multipart/alternative sub-parts.
+
+ = Parse the "top" part, and emit slightly different HTML:
+ = for each <IMG SRC>, <IMG LOWSRC>, <A HREF>? Any others?
+ = look through our cached parts for a matching URL
+ = if we find one, map that URL to a "cid:" URL
+ = else, let it load normally
+
+ = at some point, layout may load a URL for <IMG SRC="cid:xxxx">.
+ = if one of our cached parts has the requested cid, return the data
+ for it.
+ = else, generate a "broken image"
+
+ = free all the cached data
+ */
+#include "nsCOMPtr.h"
+#include "mimemrel.h"
+#include "mimemapl.h"
+#include "prmem.h"
+#include "prprf.h"
+#include "prlog.h"
+#include "plstr.h"
+#include "mimemoz2.h"
+#include "nsString.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include "nsMimeTypes.h"
+#include "mimebuf.h"
+#include "nsMsgUtils.h"
+#include <ctype.h>
+
+//
+// External Defines...
+//
+
+extern nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile);
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartRelated, MimeMultipartRelatedClass,
+ mimeMultipartRelatedClass, &MIME_SUPERCLASS);
+
+class MimeHashValue {
+ public:
+ MimeHashValue(MimeObject* obj, char* url) {
+ m_obj = obj;
+ m_url = strdup(url);
+ }
+ virtual ~MimeHashValue() {
+ if (m_url) PR_Free((void*)m_url);
+ }
+
+ MimeObject* m_obj;
+ char* m_url;
+};
+
+static int MimeMultipartRelated_initialize(MimeObject* obj) {
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)obj;
+ relobj->base_url =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_BASE, false, false);
+ /* rhp: need this for supporting Content-Location */
+ if (!relobj->base_url) {
+ relobj->base_url =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_LOCATION, false, false);
+ }
+ /* rhp: need this for supporting Content-Location */
+
+ /* I used to have code here to test if the type was text/html. Then I
+ added multipart/alternative as being OK, too. Then I found that the
+ VCard spec seems to talk about having the first part of a
+ multipart/related be an application/directory. At that point, I decided
+ to punt. We handle anything as the first part, and stomp on the HTML it
+ generates to adjust tags to point into the other parts. This probably
+ works out to something reasonable in most cases. */
+
+ relobj->hash = PL_NewHashTable(20, PL_HashString, PL_CompareStrings,
+ PL_CompareValues, (PLHashAllocOps*)NULL, NULL);
+
+ if (!relobj->hash) return MIME_OUT_OF_MEMORY;
+
+ relobj->input_file_stream = nullptr;
+ relobj->output_file_stream = nullptr;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj);
+}
+
+static int mime_multipart_related_nukehash(PLHashEntry* table, int indx,
+ void* arg) {
+ if (table->key) PR_Free((char*)table->key);
+
+ if (table->value) delete (MimeHashValue*)table->value;
+
+ return HT_ENUMERATE_NEXT; /* XP_Maphash will continue traversing the hash */
+}
+
+static void MimeMultipartRelated_finalize(MimeObject* obj) {
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)obj;
+ PR_FREEIF(relobj->base_url);
+ PR_FREEIF(relobj->curtag);
+ if (relobj->buffered_hdrs) {
+ PR_FREEIF(relobj->buffered_hdrs->all_headers);
+ PR_FREEIF(relobj->buffered_hdrs->heads);
+ PR_FREEIF(relobj->buffered_hdrs);
+ }
+ PR_FREEIF(relobj->head_buffer);
+ relobj->head_buffer_fp = 0;
+ relobj->head_buffer_size = 0;
+ if (relobj->hash) {
+ PL_HashTableEnumerateEntries(relobj->hash, mime_multipart_related_nukehash,
+ NULL);
+ PL_HashTableDestroy(relobj->hash);
+ relobj->hash = NULL;
+ }
+
+ if (relobj->input_file_stream) {
+ relobj->input_file_stream->Close();
+ relobj->input_file_stream = nullptr;
+ }
+
+ if (relobj->output_file_stream) {
+ relobj->output_file_stream->Close();
+ relobj->output_file_stream = nullptr;
+ }
+
+ if (relobj->file_buffer) {
+ relobj->file_buffer->Remove(false);
+ relobj->file_buffer = nullptr;
+ }
+
+ if (relobj->headobj) {
+ // In some error conditions when MimeMultipartRelated_parse_eof() isn't run
+ // (for example, no temp disk space available to extract message parts),
+ // the head object is also referenced as a child.
+ // If we free it, we remove the child reference first ... or crash later :-(
+ MimeContainer* cont = (MimeContainer*)relobj;
+ for (int i = 0; i < cont->nchildren; i++) {
+ if (cont->children[i] == relobj->headobj) {
+ // Shift remaining children down.
+ for (int j = i + 1; j < cont->nchildren; j++) {
+ cont->children[j - 1] = cont->children[j];
+ }
+ cont->children[--cont->nchildren] = nullptr;
+ break;
+ }
+ }
+
+ mime_free(relobj->headobj);
+ relobj->headobj = nullptr;
+ }
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+#define ISHEX(c) \
+ (((c) >= '0' && (c) <= '9') || ((c) >= 'a' && (c) <= 'f') || \
+ ((c) >= 'A' && (c) <= 'F'))
+#define NONHEX(c) (!ISHEX(c))
+
+extern "C" char* escape_unescaped_percents(const char* incomingURL) {
+ const char* inC;
+ char* outC;
+ char* result = (char*)PR_Malloc(strlen(incomingURL) * 3 + 1);
+
+ if (result) {
+ for (inC = incomingURL, outC = result; *inC != '\0'; inC++) {
+ if (*inC == '%') {
+ /* Check if either of the next two characters are non-hex. */
+ if (!*(inC + 1) || NONHEX(*(inC + 1)) || !*(inC + 2) ||
+ NONHEX(*(inC + 2))) {
+ /* Hex characters don't follow, escape the
+ percent char */
+ *outC++ = '%';
+ *outC++ = '2';
+ *outC++ = '5';
+ } else {
+ /* Hex characters follow, so assume the percent
+ is escaping something else */
+ *outC++ = *inC;
+ }
+ } else
+ *outC++ = *inC;
+ }
+ *outC = '\0';
+ }
+
+ return result;
+}
+
+/* This routine is only necessary because the mailbox URL fed to us
+ by the winfe can contain spaces and '>'s in it. It's a hack. */
+static char* escape_for_mrel_subst(char* inURL) {
+ char *output, *inC, *outC, *temp;
+
+ int size = strlen(inURL) + 1;
+
+ for (inC = inURL; *inC; inC++)
+ if ((*inC == ' ') || (*inC == '>'))
+ size += 2; /* space -> '%20', '>' -> '%3E', etc. */
+
+ output = (char*)PR_MALLOC(size);
+ if (output) {
+ /* Walk through the source string, copying all chars
+ except for spaces, which get escaped. */
+ inC = inURL;
+ outC = output;
+ while (*inC) {
+ if (*inC == ' ') {
+ *outC++ = '%';
+ *outC++ = '2';
+ *outC++ = '0';
+ } else if (*inC == '>') {
+ *outC++ = '%';
+ *outC++ = '3';
+ *outC++ = 'E';
+ } else
+ *outC++ = *inC;
+
+ inC++;
+ }
+ *outC = '\0';
+
+ temp = escape_unescaped_percents(output);
+ if (temp) {
+ PR_FREEIF(output);
+ output = temp;
+ }
+ }
+ return output;
+}
+
+static bool MimeStartParamExists(MimeObject* obj, MimeObject* child) {
+ char* ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ char* st =
+ (ct ? MimeHeaders_get_parameter(ct, HEADER_PARM_START, NULL, NULL) : 0);
+
+ PR_FREEIF(ct);
+ if (!st) return false;
+
+ PR_FREEIF(st);
+ return true;
+}
+
+static bool MimeThisIsStartPart(MimeObject* obj, MimeObject* child) {
+ bool rval = false;
+ char *ct, *st, *cst;
+
+ ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ st = (ct ? MimeHeaders_get_parameter(ct, HEADER_PARM_START, NULL, NULL) : 0);
+
+ PR_FREEIF(ct);
+ if (!st) return false;
+
+ cst = MimeHeaders_get(child->headers, HEADER_CONTENT_ID, false, false);
+ if (!cst)
+ rval = false;
+ else {
+ char* tmp = cst;
+ if (*tmp == '<') {
+ int length;
+ tmp++;
+ length = strlen(tmp);
+ if (length > 0 && tmp[length - 1] == '>') {
+ tmp[length - 1] = '\0';
+ }
+ }
+
+ rval = (!strcmp(st, tmp));
+ }
+
+ PR_FREEIF(st);
+ PR_FREEIF(cst);
+ return rval;
+}
+/* rhp - gotta support the "start" parameter */
+
+char* MakeAbsoluteURL(char* base_url, char* relative_url) {
+ char* retString = nullptr;
+ nsIURI* base = nullptr;
+
+ // if either is NULL, just return the relative if safe...
+ if (!base_url || !relative_url) {
+ if (!relative_url) return nullptr;
+
+ NS_MsgSACopy(&retString, relative_url);
+ return retString;
+ }
+
+ nsresult err = nsMimeNewURI(&base, base_url, nullptr);
+ if (NS_FAILED(err)) return nullptr;
+
+ nsAutoCString spec;
+
+ nsIURI* url = nullptr;
+ err = nsMimeNewURI(&url, relative_url, base);
+ if (NS_FAILED(err)) goto done;
+
+ err = url->GetSpec(spec);
+ if (NS_FAILED(err)) {
+ retString = nullptr;
+ goto done;
+ }
+ retString = ToNewCString(spec);
+
+done:
+ NS_IF_RELEASE(url);
+ NS_IF_RELEASE(base);
+ return retString;
+}
+
+static bool MimeMultipartRelated_output_child_p(MimeObject* obj,
+ MimeObject* child) {
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)obj;
+
+ if ((relobj->head_loaded) ||
+ (MimeStartParamExists(obj, child) && !MimeThisIsStartPart(obj, child))) {
+ /* This is a child part. Just remember the mapping between the URL
+ it represents and the part-URL to get it back. */
+
+ char* location =
+ MimeHeaders_get(child->headers, HEADER_CONTENT_LOCATION, false, false);
+ if (!location) {
+ char* tmp =
+ MimeHeaders_get(child->headers, HEADER_CONTENT_ID, false, false);
+ if (tmp) {
+ char* tmp2 = tmp;
+ if (*tmp2 == '<') {
+ int length;
+ tmp2++;
+ length = strlen(tmp2);
+ if (length > 0 && tmp2[length - 1] == '>') {
+ tmp2[length - 1] = '\0';
+ }
+ }
+ location = PR_smprintf("cid:%s", tmp2);
+ PR_Free(tmp);
+ }
+ }
+
+ if (location) {
+ char* base_url =
+ MimeHeaders_get(child->headers, HEADER_CONTENT_BASE, false, false);
+ char* absolute =
+ MakeAbsoluteURL(base_url ? base_url : relobj->base_url, location);
+
+ PR_FREEIF(base_url);
+ PR_Free(location);
+ if (absolute) {
+ nsAutoCString partnum;
+ nsAutoCString imappartnum;
+ partnum.Adopt(mime_part_address(child));
+ if (!partnum.IsEmpty()) {
+ if (obj->options->missing_parts) {
+ char* imappart = mime_imap_part_address(child);
+ if (imappart) imappartnum.Adopt(imappart);
+ }
+
+ /*
+ AppleDouble part need special care: we need to output only the data
+ fork part of it. The problem at this point is that we haven't yet
+ decoded the children of the AppleDouble part therefore we will have
+ to hope the datafork is the second one!
+ */
+ if (mime_typep(child,
+ (MimeObjectClass*)&mimeMultipartAppleDoubleClass))
+ partnum.AppendLiteral(".2");
+
+ char* part;
+ if (!imappartnum.IsEmpty())
+ part = mime_set_url_imap_part(obj->options->url, imappartnum.get(),
+ partnum.get());
+ else {
+ char* no_part_url = nullptr;
+ if (obj->options->part_to_load &&
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyDisplay)
+ no_part_url = mime_get_base_url(obj->options->url);
+ if (no_part_url) {
+ part = mime_set_url_part(no_part_url, partnum.get(), false);
+ PR_Free(no_part_url);
+ } else
+ part = mime_set_url_part(obj->options->url, partnum.get(), false);
+ }
+ if (part) {
+ char* name = MimeHeaders_get_name(child->headers, child->options);
+ // let's stick the filename in the part so save as will work.
+ if (!name) {
+ // Mozilla platform code will correct the file extension
+ // when copying the embedded image. That doesn't work
+ // since our MailNews URLs don't allow setting the file
+ // extension. So provide a filename and valid extension.
+ char* ct = MimeHeaders_get(child->headers, HEADER_CONTENT_TYPE,
+ false, false);
+ if (ct) {
+ name = ct;
+ char* slash = strchr(name, '/');
+ if (slash) *slash = '.';
+ char* semi = strchr(name, ';');
+ if (semi) *semi = 0;
+ }
+ }
+ if (name) {
+ char* savePart = part;
+ part = PR_smprintf("%s&filename=%s", savePart, name);
+ PR_Free(savePart);
+ PR_Free(name);
+ }
+ char* temp = part;
+ /* If there's a space in the url, escape the url.
+ (This happens primarily on Windows and Unix.) */
+ if (PL_strchr(part, ' ') || PL_strchr(part, '>') ||
+ PL_strchr(part, '%'))
+ temp = escape_for_mrel_subst(part);
+ MimeHashValue* value = new MimeHashValue(child, temp);
+ PL_HashTableAdd(relobj->hash, absolute, value);
+
+ /* rhp - If this part ALSO has a Content-ID we need to put that into
+ the hash table and this is what this code does
+ */
+ {
+ char* tloc;
+ char* tmp = MimeHeaders_get(child->headers, HEADER_CONTENT_ID,
+ false, false);
+ if (tmp) {
+ char* tmp2 = tmp;
+ if (*tmp2 == '<') {
+ int length;
+ tmp2++;
+ length = strlen(tmp2);
+ if (length > 0 && tmp2[length - 1] == '>') {
+ tmp2[length - 1] = '\0';
+ }
+ }
+
+ tloc = PR_smprintf("cid:%s", tmp2);
+ PR_Free(tmp);
+ if (tloc) {
+ MimeHashValue* value;
+ value =
+ (MimeHashValue*)PL_HashTableLookup(relobj->hash, tloc);
+
+ if (!value) {
+ value = new MimeHashValue(child, temp);
+ PL_HashTableAdd(relobj->hash, tloc, value);
+ } else
+ PR_smprintf_free(tloc);
+ }
+ }
+ }
+ /* rhp - End of putting more stuff into the hash table */
+
+ /* it's possible that temp pointer is the same than the part
+ pointer, therefore be careful to not freeing twice the same
+ pointer */
+ if (temp && temp != part) PR_Free(temp);
+ PR_Free(part);
+ }
+ }
+ }
+ }
+ } else {
+ /* Ah-hah! We're the head object. */
+ relobj->head_loaded = true;
+ relobj->headobj = child;
+ relobj->buffered_hdrs = MimeHeaders_copy(child->headers);
+ char* base_url =
+ MimeHeaders_get(child->headers, HEADER_CONTENT_BASE, false, false);
+ if (!base_url) {
+ base_url = MimeHeaders_get(child->headers, HEADER_CONTENT_LOCATION, false,
+ false);
+ }
+
+ if (base_url) {
+ /* If the head object has a base_url associated with it, use
+ that instead of any base_url that may have been associated
+ with the multipart/related. */
+ PR_FREEIF(relobj->base_url);
+ nsCOMPtr<nsIURI> url;
+ nsresult rv = nsMimeNewURI(getter_AddRefs(url), base_url, nullptr);
+ if (NS_SUCCEEDED(rv)) {
+ relobj->base_url = base_url;
+ }
+ }
+ }
+ if (obj->options && !obj->options->write_html_p
+#ifdef MIME_DRAFTS
+ && !obj->options->decompose_file_p
+#endif /* MIME_DRAFTS */
+ ) {
+ return true;
+ }
+
+ // Don't actually parse this child; we'll handle all that at eof time.
+ return false;
+}
+
+static int MimeMultipartRelated_parse_child_line(MimeObject* obj,
+ const char* line,
+ int32_t length,
+ bool first_line_p) {
+ MimeContainer* cont = (MimeContainer*)obj;
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)obj;
+ MimeObject* kid;
+
+ if (obj->options && !obj->options->write_html_p
+#ifdef MIME_DRAFTS
+ && !obj->options->decompose_file_p
+#endif /* MIME_DRAFTS */
+ ) {
+ /* Oh, just go do the normal thing... */
+ return ((MimeMultipartClass*)&MIME_SUPERCLASS)
+ ->parse_child_line(obj, line, length, first_line_p);
+ }
+
+ /* Throw it away if this isn't the head object. (Someday, maybe we'll
+ cache it instead.) */
+ PR_ASSERT(cont->nchildren > 0);
+ if (cont->nchildren <= 0) return -1;
+ kid = cont->children[cont->nchildren - 1];
+ PR_ASSERT(kid);
+ if (!kid) return -1;
+ if (kid != relobj->headobj) return 0;
+
+ /* Buffer this up (###tw much code duplication from mimemalt.c) */
+ /* If we don't yet have a buffer (either memory or file) try and make a
+ memory buffer. */
+ if (!relobj->head_buffer && !relobj->file_buffer) {
+ int target_size = 1024 * 50; /* try for 50k */
+ while (target_size > 0) {
+ relobj->head_buffer = (char*)PR_MALLOC(target_size);
+ if (relobj->head_buffer) break; /* got it! */
+ target_size -= (1024 * 5); /* decrease it and try again */
+ }
+
+ if (relobj->head_buffer) {
+ relobj->head_buffer_size = target_size;
+ } else {
+ relobj->head_buffer_size = 0;
+ }
+
+ relobj->head_buffer_fp = 0;
+ }
+
+ nsresult rv;
+ /* Ok, if at this point we still don't have either kind of buffer, try and
+ make a file buffer. */
+ if (!relobj->head_buffer && !relobj->file_buffer) {
+ nsCOMPtr<nsIFile> file;
+ rv = nsMsgCreateTempFile("nsma", getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, -1);
+ relobj->file_buffer = file;
+
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(relobj->output_file_stream), relobj->file_buffer,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+ NS_ENSURE_SUCCESS(rv, -1);
+ }
+
+ PR_ASSERT(relobj->head_buffer || relobj->output_file_stream);
+
+ /* If this line will fit in the memory buffer, put it there.
+ */
+ if (relobj->head_buffer &&
+ relobj->head_buffer_fp + length < relobj->head_buffer_size) {
+ memcpy(relobj->head_buffer + relobj->head_buffer_fp, line, length);
+ relobj->head_buffer_fp += length;
+ } else {
+ /* Otherwise it won't fit; write it to the file instead. */
+
+ /* If the file isn't open yet, open it, and dump the memory buffer
+ to it. */
+ if (!relobj->output_file_stream) {
+ if (!relobj->file_buffer) {
+ nsCOMPtr<nsIFile> file;
+ rv = nsMsgCreateTempFile("nsma", getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, -1);
+ relobj->file_buffer = file;
+ }
+
+ nsresult rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(relobj->output_file_stream), relobj->file_buffer,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ if (relobj->head_buffer && relobj->head_buffer_fp) {
+ uint32_t bytesWritten;
+ rv = relobj->output_file_stream->Write(
+ relobj->head_buffer, relobj->head_buffer_fp, &bytesWritten);
+ if (NS_FAILED(rv) || (bytesWritten < relobj->head_buffer_fp))
+ return MIME_UNABLE_TO_OPEN_TMP_FILE;
+ }
+
+ PR_FREEIF(relobj->head_buffer);
+ relobj->head_buffer_fp = 0;
+ relobj->head_buffer_size = 0;
+ }
+
+ /* Dump this line to the file. */
+ uint32_t bytesWritten;
+ rv = relobj->output_file_stream->Write(line, length, &bytesWritten);
+ if ((int32_t)bytesWritten < length || NS_FAILED(rv))
+ return MIME_UNABLE_TO_OPEN_TMP_FILE;
+ }
+
+ return 0;
+}
+
+static int real_write(MimeMultipartRelated* relobj, const char* buf,
+ int32_t size) {
+ MimeObject* obj = (MimeObject*)relobj;
+ void* closure = relobj->real_output_closure;
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->decompose_file_output_fn) {
+ // the buf here has already been decoded, but we want to use general output
+ // functions here that permit decoded or encoded input, using the closure
+ // to tell the difference. We'll temporarily disable the closure's decoder,
+ // then restore it when we are done. Not sure if we shouldn't just turn it
+ // off permanently though.
+
+ mime_draft_data* mdd = (mime_draft_data*)obj->options->stream_closure;
+ MimeDecoderData* old_decoder_data = mdd->decoder_data;
+ mdd->decoder_data = nullptr;
+ int status = obj->options->decompose_file_output_fn(buf, size, (void*)mdd);
+ mdd->decoder_data = old_decoder_data;
+ return status;
+ } else
+#endif /* MIME_DRAFTS */
+ {
+ if (!closure) {
+ MimeObject* lobj = (MimeObject*)relobj;
+ closure = lobj->options->stream_closure;
+ }
+ return relobj->real_output_fn(buf, size, closure);
+ }
+}
+
+static int push_tag(MimeMultipartRelated* relobj, const char* buf,
+ int32_t size) {
+ if (size + relobj->curtag_length > relobj->curtag_max) {
+ relobj->curtag_max += 2 * size;
+ if (relobj->curtag_max < 1024) relobj->curtag_max = 1024;
+
+ char* newBuf = (char*)PR_Realloc(relobj->curtag, relobj->curtag_max);
+ NS_ENSURE_TRUE(newBuf, MIME_OUT_OF_MEMORY);
+ relobj->curtag = newBuf;
+ }
+ memcpy(relobj->curtag + relobj->curtag_length, buf, size);
+ relobj->curtag_length += size;
+ return 0;
+}
+
+static bool accept_related_part(MimeMultipartRelated* relobj,
+ MimeObject* part_obj) {
+ if (!relobj || !part_obj) return false;
+
+ /* before accepting it as a valid related part, make sure we
+ are able to display it inline as an embedded object. Else just ignore
+ it, that will prevent any bad surprise... */
+ MimeObjectClass* clazz = mime_find_class(
+ part_obj->content_type, part_obj->headers, part_obj->options, false);
+ if (clazz ? clazz->displayable_inline_p(clazz, part_obj->headers) : false)
+ return true;
+
+ /* ...but we always accept it if it's referenced by an anchor */
+ return (relobj->curtag && relobj->curtag_length >= 3 &&
+ (relobj->curtag[1] == 'A' || relobj->curtag[1] == 'a') &&
+ IS_SPACE(relobj->curtag[2]));
+}
+
+static int flush_tag(MimeMultipartRelated* relobj) {
+ int length = relobj->curtag_length;
+ char* buf;
+ int status;
+
+ if (relobj->curtag == NULL || length == 0) return 0;
+
+ status = push_tag(relobj, "", 1); /* Push on a trailing NULL. */
+ if (status < 0) return status;
+ buf = relobj->curtag;
+ PR_ASSERT(*buf == '<' && buf[length - 1] == '>');
+ while (*buf) {
+ char c;
+ char* absolute;
+ char* part_url;
+ char* ptr = buf;
+ char* ptr2;
+ char quoteDelimiter = '\0';
+ while (*ptr && *ptr != '=') ptr++;
+ if (*ptr == '=') {
+ /* Ignore = and leading space. */
+ /* Safe, because there's a '>' at the end! */
+ do {
+ ptr++;
+ } while (IS_SPACE(*ptr));
+ if (*ptr == '"' || *ptr == '\'') {
+ quoteDelimiter = *ptr;
+ /* Take up the quote and leading space here as well. */
+ /* Safe because there's a '>' at the end */
+ do {
+ ptr++;
+ } while (IS_SPACE(*ptr));
+ }
+ }
+ status = real_write(relobj, buf, ptr - buf);
+ if (status < 0) return status;
+ buf = ptr;
+ if (!*buf) break;
+ if (quoteDelimiter) {
+ ptr = PL_strnchr(buf, quoteDelimiter, length - (buf - relobj->curtag));
+ } else {
+ for (ptr = buf; *ptr; ptr++) {
+ if (*ptr == '>' || IS_SPACE(*ptr)) break;
+ }
+ PR_ASSERT(*ptr);
+ }
+ if (!ptr || !*ptr) break;
+
+ while (buf < ptr) {
+ /* ### mwelch For each word in the value string, see if
+ the word is a cid: URL. If so, attempt to
+ substitute the appropriate mailbox part URL in
+ its place. */
+ ptr2 = buf; /* walk from the left end rightward */
+ while ((ptr2 < ptr) && (!IS_SPACE(*ptr2))) ptr2++;
+ /* Compare the beginning of the word with "cid:". Yuck. */
+ if (((ptr2 - buf) > 4) &&
+ ((buf[0] == 'c' || buf[0] == 'C') &&
+ (buf[1] == 'i' || buf[1] == 'I') &&
+ (buf[2] == 'd' || buf[2] == 'D') && buf[3] == ':')) {
+ // Make sure it's lowercase, otherwise it won't be found in the hash
+ // table
+ buf[0] = 'c';
+ buf[1] = 'i';
+ buf[2] = 'd';
+
+ /* Null terminate the word so we can... */
+ c = *ptr2;
+ *ptr2 = '\0';
+
+ /* Construct a URL out of the word. */
+ absolute = MakeAbsoluteURL(relobj->base_url, buf);
+
+ /* See if we have a mailbox part URL
+ corresponding to this cid. */
+ part_url = nullptr;
+ MimeHashValue* value = nullptr;
+ if (absolute) {
+ value = (MimeHashValue*)PL_HashTableLookup(relobj->hash, buf);
+ part_url = value ? value->m_url : nullptr;
+ PR_FREEIF(absolute);
+ }
+
+ /*If we found a mailbox part URL, write that out instead.*/
+ if (part_url && accept_related_part(relobj, value->m_obj)) {
+ status = real_write(relobj, part_url, strlen(part_url));
+ if (status < 0) return status;
+ buf = ptr2; /* skip over the cid: URL we substituted */
+
+ /* don't show that object as attachment */
+ if (value->m_obj) value->m_obj->dontShowAsAttachment = true;
+ }
+
+ /* Restore the character that we nulled. */
+ *ptr2 = c;
+ }
+ /* rhp - if we get here, we should still check against the hash table! */
+ else {
+ char holder = *ptr2;
+ char* realout;
+
+ *ptr2 = '\0';
+
+ /* Construct a URL out of the word. */
+ absolute = MakeAbsoluteURL(relobj->base_url, buf);
+
+ /* See if we have a mailbox part URL
+ corresponding to this cid. */
+ MimeHashValue* value;
+ if (absolute)
+ value = (MimeHashValue*)PL_HashTableLookup(relobj->hash, absolute);
+ else
+ value = (MimeHashValue*)PL_HashTableLookup(relobj->hash, buf);
+ realout = value ? value->m_url : nullptr;
+
+ *ptr2 = holder;
+ PR_FREEIF(absolute);
+
+ if (realout && accept_related_part(relobj, value->m_obj)) {
+ status = real_write(relobj, realout, strlen(realout));
+ if (status < 0) return status;
+ buf = ptr2; /* skip over the cid: URL we substituted */
+
+ /* don't show that object as attachment */
+ if (value->m_obj) value->m_obj->dontShowAsAttachment = true;
+ }
+ }
+ /* rhp - if we get here, we should still check against the hash table! */
+
+ /* Advance to the beginning of the next word, or to
+ the end of the value string. */
+ while ((ptr2 < ptr) && (IS_SPACE(*ptr2))) ptr2++;
+
+ /* Write whatever original text remains after
+ cid: URL substitution. */
+ status = real_write(relobj, buf, ptr2 - buf);
+ if (status < 0) return status;
+ buf = ptr2;
+ }
+ }
+ if (buf && *buf) {
+ status = real_write(relobj, buf, strlen(buf));
+ if (status < 0) return status;
+ }
+ relobj->curtag_length = 0;
+ return 0;
+}
+
+static int mime_multipart_related_output_fn(const char* buf, int32_t size,
+ void* stream_closure) {
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)stream_closure;
+ char* ptr;
+ int32_t delta;
+ int status;
+ while (size > 0) {
+ if (relobj->curtag_length > 0) {
+ ptr = PL_strnchr(buf, '>', size);
+ if (!ptr) {
+ return push_tag(relobj, buf, size);
+ }
+ delta = ptr - buf + 1;
+ status = push_tag(relobj, buf, delta);
+ if (status < 0) return status;
+ status = flush_tag(relobj);
+ if (status < 0) return status;
+ buf += delta;
+ size -= delta;
+ }
+ ptr = PL_strnchr(buf, '<', size);
+ if (ptr && ptr - buf >= size) ptr = 0;
+ if (!ptr) {
+ return real_write(relobj, buf, size);
+ }
+ delta = ptr - buf;
+ status = real_write(relobj, buf, delta);
+ if (status < 0) return status;
+ buf += delta;
+ size -= delta;
+ PR_ASSERT(relobj->curtag_length == 0);
+ status = push_tag(relobj, buf, 1);
+ if (status < 0) return status;
+ PR_ASSERT(relobj->curtag_length == 1);
+ buf++;
+ size--;
+ }
+ return 0;
+}
+
+static int MimeMultipartRelated_parse_eof(MimeObject* obj, bool abort_p) {
+ /* OK, all the necessary data has been collected. We now have to spew out
+ the HTML. We let it go through all the normal mechanisms (which
+ includes content-encoding handling), and intercept the output data to do
+ translation of the tags. Whee. */
+ MimeMultipartRelated* relobj = (MimeMultipartRelated*)obj;
+ MimeContainer* cont = (MimeContainer*)obj;
+ int status = 0;
+ MimeObject* body;
+ char* ct;
+ const char* dct;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) goto FAIL;
+
+ if (!relobj->headobj) return 0;
+
+ ct =
+ (relobj->buffered_hdrs ? MimeHeaders_get(relobj->buffered_hdrs,
+ HEADER_CONTENT_TYPE, true, false)
+ : 0);
+ dct = (((MimeMultipartClass*)obj->clazz)->default_part_type);
+
+ relobj->real_output_fn = obj->options->output_fn;
+ relobj->real_output_closure = obj->options->output_closure;
+
+ obj->options->output_fn = mime_multipart_related_output_fn;
+ obj->options->output_closure = obj;
+
+ body = mime_create(((ct && *ct) ? ct : (dct ? dct : TEXT_HTML)),
+ relobj->buffered_hdrs, obj->options);
+
+ PR_FREEIF(ct);
+ if (!body) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+ // replace the existing head object with the new object
+ for (int iChild = 0; iChild < cont->nchildren; iChild++) {
+ if (cont->children[iChild] == relobj->headobj) {
+ // cleanup of the headobj is performed explicitly in our finalizer now
+ // that it does not get cleaned up as a child.
+ cont->children[iChild] = body;
+ body->parent = obj;
+ body->options = obj->options;
+ }
+ }
+
+ if (!body->parent) {
+ NS_WARNING("unexpected mime multipart related structure");
+ goto FAIL;
+ }
+
+ body->dontShowAsAttachment =
+ body->clazz->displayable_inline_p(body->clazz, body->headers);
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->decompose_file_init_fn &&
+ (relobj->file_buffer || relobj->head_buffer)) {
+ status = obj->options->decompose_file_init_fn(obj->options->stream_closure,
+ relobj->buffered_hdrs);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ /* if the emitter wants to know about nested bodies, then it needs
+ to know that we jumped back to this body part. */
+ if (obj->options->notify_nested_bodies) {
+ char* part_path = mime_part_address(body);
+ if (part_path) {
+ mimeEmitterAddHeaderField(obj->options, "x-jsemitter-part-path",
+ part_path);
+ PR_Free(part_path);
+ }
+ }
+
+ /* Now that we've added this new object to our list of children,
+ start its parser going. */
+ status = body->clazz->parse_begin(body);
+ if (status < 0) goto FAIL;
+
+ if (relobj->head_buffer) {
+ /* Read it out of memory. */
+ PR_ASSERT(!relobj->file_buffer && !relobj->input_file_stream);
+
+ status = body->clazz->parse_buffer(relobj->head_buffer,
+ relobj->head_buffer_fp, body);
+ } else if (relobj->file_buffer) {
+ /* Read it off disk. */
+ char* buf;
+
+ PR_ASSERT(relobj->head_buffer_size == 0 && relobj->head_buffer_fp == 0);
+ PR_ASSERT(relobj->file_buffer);
+ if (!relobj->file_buffer) {
+ status = -1;
+ goto FAIL;
+ }
+
+ buf = (char*)PR_MALLOC(FILE_IO_BUFFER_SIZE);
+ if (!buf) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+
+ // First, close the output file to open the input file!
+ if (relobj->output_file_stream) relobj->output_file_stream->Close();
+
+ nsresult rv = NS_NewLocalFileInputStream(
+ getter_AddRefs(relobj->input_file_stream), relobj->file_buffer);
+ if (NS_FAILED(rv)) {
+ PR_Free(buf);
+ status = MIME_UNABLE_TO_OPEN_TMP_FILE;
+ goto FAIL;
+ }
+
+ while (1) {
+ uint32_t bytesRead = 0;
+ rv = relobj->input_file_stream->Read(buf, FILE_IO_BUFFER_SIZE - 1,
+ &bytesRead);
+ if (NS_FAILED(rv) || !bytesRead) {
+ status = NS_FAILED(rv) ? -1 : 0;
+ break;
+ } else {
+ /* It would be really nice to be able to yield here, and let
+ some user events and other input sources get processed.
+ Oh well. */
+
+ status = body->clazz->parse_buffer(buf, bytesRead, body);
+ if (status < 0) break;
+ }
+ }
+ PR_Free(buf);
+ }
+
+ if (status < 0) goto FAIL;
+
+ /* Done parsing. */
+ status = body->clazz->parse_eof(body, false);
+ if (status < 0) goto FAIL;
+ status = body->clazz->parse_end(body, false);
+ if (status < 0) goto FAIL;
+
+FAIL:
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->decompose_file_close_fn &&
+ (relobj->file_buffer || relobj->head_buffer)) {
+ status =
+ obj->options->decompose_file_close_fn(obj->options->stream_closure);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ obj->options->output_fn = relobj->real_output_fn;
+ obj->options->output_closure = relobj->real_output_closure;
+
+ return status;
+}
+
+static int MimeMultipartRelatedClassInitialize(
+ MimeMultipartRelatedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeMultipartRelated_initialize;
+ oclass->finalize = MimeMultipartRelated_finalize;
+ oclass->parse_eof = MimeMultipartRelated_parse_eof;
+ mclass->output_child_p = MimeMultipartRelated_output_child_p;
+ mclass->parse_child_line = MimeMultipartRelated_parse_child_line;
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimemrel.h b/comm/mailnews/mime/src/mimemrel.h
new file mode 100644
index 0000000000..e861e7268c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemrel.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMREL_H_
+#define _MIMEMREL_H_
+
+#include "mimemult.h"
+#include "plhash.h"
+#include "prio.h"
+#include "nsNetUtil.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+/* The MimeMultipartRelated class implements the multipart/related MIME
+ container, which allows `sibling' sub-parts to refer to each other.
+ */
+
+typedef struct MimeMultipartRelatedClass MimeMultipartRelatedClass;
+typedef struct MimeMultipartRelated MimeMultipartRelated;
+
+struct MimeMultipartRelatedClass {
+ MimeMultipartClass multipart;
+};
+
+extern "C" MimeMultipartRelatedClass mimeMultipartRelatedClass;
+
+struct MimeMultipartRelated {
+ MimeMultipart multipart; // superclass variables.
+
+ char* base_url; // Base URL (if any) for the whole multipart/related.
+
+ char* head_buffer; // Buffer used to remember the text/html 'head'
+ // part.
+ uint32_t head_buffer_fp; // Active length.
+ uint32_t head_buffer_size; // How big it is.
+
+ nsCOMPtr<nsIFile> file_buffer; // The nsIFile of a temp file used when we
+ // run out of room in the head_buffer.
+ nsCOMPtr<nsIInputStream> input_file_stream; // A stream to it.
+ nsCOMPtr<nsIOutputStream> output_file_stream; // A stream to it.
+
+ MimeHeaders* buffered_hdrs; // The headers of the 'head' part. */
+
+ bool head_loaded; // Whether we've already passed the 'head' part.
+ MimeObject* headobj; // The actual text/html head object.
+
+ PLHashTable* hash;
+
+ MimeConverterOutputCallback real_output_fn;
+ void* real_output_closure;
+
+ char* curtag;
+ int32_t curtag_max;
+ int32_t curtag_length;
+};
+
+#define MimeMultipartRelatedClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMREL_H_ */
diff --git a/comm/mailnews/mime/src/mimemsg.cpp b/comm/mailnews/mime/src/mimemsg.cpp
new file mode 100644
index 0000000000..fcd86651f7
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemsg.cpp
@@ -0,0 +1,847 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsCOMPtr.h"
+#include "mimemsg.h"
+#include "mimemoz2.h"
+#include "prmem.h"
+#include "prio.h"
+#include "plstr.h"
+#include "msgCore.h"
+#include "prlog.h"
+#include "prprf.h"
+#include "nsMimeStringResources.h"
+#include "nsMimeTypes.h"
+#include "nsMsgMessageFlags.h"
+#include "nsString.h"
+#include "mimetext.h"
+#include "mimecryp.h"
+#include "mimetpfl.h"
+#include "nsINetUtil.h"
+#include "nsMsgUtils.h"
+#include "nsMsgI18N.h"
+
+#define MIME_SUPERCLASS mimeContainerClass
+MimeDefClass(MimeMessage, MimeMessageClass, mimeMessageClass, &MIME_SUPERCLASS);
+
+static int MimeMessage_initialize(MimeObject*);
+static void MimeMessage_finalize(MimeObject*);
+static int MimeMessage_add_child(MimeObject*, MimeObject*);
+static int MimeMessage_parse_begin(MimeObject*);
+static int MimeMessage_parse_line(const char*, int32_t, MimeObject*);
+static int MimeMessage_parse_eof(MimeObject*, bool);
+static int MimeMessage_close_headers(MimeObject* obj);
+static int MimeMessage_write_headers_html(MimeObject*);
+static char* MimeMessage_partial_message_html(const char* data, void* closure,
+ MimeHeaders* headers);
+
+#ifdef XP_UNIX
+extern void MimeHeaders_do_unix_display_hook_hack(MimeHeaders*);
+#endif /* XP_UNIX */
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeMessage_debug_print(MimeObject*, PRFileDesc*, int32_t depth);
+#endif
+
+extern MimeObjectClass mimeMultipartClass;
+
+static int MimeMessageClassInitialize(MimeMessageClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeContainerClass* cclass = (MimeContainerClass*)clazz;
+
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeMessage_initialize;
+ oclass->finalize = MimeMessage_finalize;
+ oclass->parse_begin = MimeMessage_parse_begin;
+ oclass->parse_line = MimeMessage_parse_line;
+ oclass->parse_eof = MimeMessage_parse_eof;
+ cclass->add_child = MimeMessage_add_child;
+
+#if defined(DEBUG) && defined(XP_UNIX)
+ oclass->debug_print = MimeMessage_debug_print;
+#endif
+ return 0;
+}
+
+static int MimeMessage_initialize(MimeObject* object) {
+ MimeMessage* msg = (MimeMessage*)object;
+ msg->grabSubject = false;
+ msg->bodyLength = 0;
+ msg->sizeSoFar = 0;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeMessage_finalize(MimeObject* object) {
+ MimeMessage* msg = (MimeMessage*)object;
+ if (msg->hdrs) MimeHeaders_free(msg->hdrs);
+ msg->hdrs = 0;
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeMessage_parse_begin(MimeObject* obj) {
+ MimeMessage* msg = (MimeMessage*)obj;
+
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ if (obj->parent) {
+ msg->grabSubject = true;
+ }
+
+ /* Messages have separators before the headers, except for the outermost
+ message. */
+ return MimeObject_write_separator(obj);
+}
+
+static int MimeMessage_parse_line(const char* aLine, int32_t aLength,
+ MimeObject* obj) {
+ const char* line = aLine;
+ int32_t length = aLength;
+
+ MimeMessage* msg = (MimeMessage*)obj;
+ int status = 0;
+
+ NS_ASSERTION(line && *line, "empty line in mime msg parse_line");
+ if (!line || !*line) return -1;
+
+ msg->sizeSoFar += length;
+
+ if (msg->grabSubject) {
+ if ((!PL_strncasecmp(line, "Subject: ", 9)) && (obj->parent)) {
+ if ((obj->headers) && (!obj->headers->munged_subject)) {
+ obj->headers->munged_subject = (char*)PL_strndup(line + 9, length - 9);
+ char* tPtr = obj->headers->munged_subject;
+ while (*tPtr) {
+ if ((*tPtr == '\r') || (*tPtr == '\n')) {
+ *tPtr = '\0';
+ break;
+ }
+ tPtr++;
+ }
+ }
+ }
+ }
+
+ /* If we already have a child object, then we're done parsing headers,
+ and all subsequent lines get passed to the inferior object without
+ further processing by us. (Our parent will stop feeding us lines
+ when this MimeMessage part is out of data.)
+ */
+ if (msg->container.nchildren) {
+ MimeObject* kid = msg->container.children[0];
+ bool nl;
+ PR_ASSERT(kid);
+ if (!kid) return -1;
+
+ msg->bodyLength += length;
+
+ /* Don't allow MimeMessage objects to not end in a newline, since it
+ would be inappropriate for any following part to appear on the same
+ line as the last line of the message.
+
+ #### This assumes that the only time the `parse_line' method is
+ called with a line that doesn't end in a newline is when that line
+ is the last line.
+ */
+ nl = (length > 0 && (line[length - 1] == '\r' || line[length - 1] == '\n'));
+
+#ifdef MIME_DRAFTS
+ if (!mime_typep(kid, (MimeObjectClass*)&mimeMessageClass) && obj->options &&
+ obj->options->decompose_file_p && !obj->options->is_multipart_msg &&
+ obj->options->decompose_file_output_fn && !obj->options->decrypt_p) {
+ // If we are processing a flowed plain text line, we need to parse the
+ // line in mimeInlineTextPlainFlowedClass.
+ if (mime_typep(kid, (MimeObjectClass*)&mimeInlineTextPlainFlowedClass)) {
+ return kid->clazz->parse_line(line, length, kid);
+ } else {
+ status = obj->options->decompose_file_output_fn(
+ line, length, obj->options->stream_closure);
+ if (status < 0) return status;
+ if (!nl) {
+ status = obj->options->decompose_file_output_fn(
+ MSG_LINEBREAK, MSG_LINEBREAK_LEN, obj->options->stream_closure);
+ if (status < 0) return status;
+ }
+ return status;
+ }
+ }
+#endif /* MIME_DRAFTS */
+
+ if (nl)
+ return kid->clazz->parse_buffer(line, length, kid);
+ else {
+ /* Hack a newline onto the end. */
+ char* s = (char*)PR_MALLOC(length + MSG_LINEBREAK_LEN + 1);
+ if (!s) return MIME_OUT_OF_MEMORY;
+ memcpy(s, line, length);
+ PL_strncpyz(s + length, MSG_LINEBREAK, MSG_LINEBREAK_LEN + 1);
+ status = kid->clazz->parse_buffer(s, length + MSG_LINEBREAK_LEN, kid);
+ PR_Free(s);
+ return status;
+ }
+ }
+
+ /* Otherwise we don't yet have a child object, which means we're not
+ done parsing our headers yet.
+ */
+ if (!msg->hdrs) {
+ msg->hdrs = MimeHeaders_new();
+ if (!msg->hdrs) return MIME_OUT_OF_MEMORY;
+ }
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ !obj->options->is_multipart_msg &&
+ obj->options->done_parsing_outer_headers &&
+ obj->options->decompose_file_output_fn) {
+ status = obj->options->decompose_file_output_fn(
+ line, length, obj->options->stream_closure);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ status = MimeHeaders_parse_line(line, length, msg->hdrs);
+ if (status < 0) return status;
+
+ /* If this line is blank, we're now done parsing headers, and should
+ examine our content-type to create our "body" part.
+ */
+ if (*line == '\r' || *line == '\n') {
+ status = MimeMessage_close_headers(obj);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeMessage_close_headers(MimeObject* obj) {
+ MimeMessage* msg = (MimeMessage*)obj;
+ int status = 0;
+ char* ct = 0; /* Content-Type header */
+ MimeObject* body;
+
+ // Do a proper decoding of the munged subject.
+ if (obj->headers && msg->hdrs && msg->grabSubject &&
+ obj->headers->munged_subject) {
+ // nsMsgI18NConvertToUnicode wants nsAStrings...
+ nsDependentCString orig(obj->headers->munged_subject);
+ nsAutoString dest;
+ // First, get the Content-Type, then extract the charset="whatever" part of
+ // it.
+ nsCString charset;
+ nsCString contentType;
+ contentType.Adopt(
+ MimeHeaders_get(msg->hdrs, HEADER_CONTENT_TYPE, false, false));
+ if (!contentType.IsEmpty())
+ charset.Adopt(MimeHeaders_get_parameter(contentType.get(), "charset",
+ nullptr, nullptr));
+
+ // If we've got a charset, use nsMsgI18NConvertToUnicode to magically decode
+ // the munged subject.
+ if (!charset.IsEmpty()) {
+ nsresult rv = nsMsgI18NConvertToUnicode(charset, orig, dest);
+ // If we managed to convert the string, replace munged_subject with the
+ // UTF8 version of it, otherwise, just forget about it (maybe there was an
+ // improperly encoded string in there).
+ PR_Free(obj->headers->munged_subject);
+ if (NS_SUCCEEDED(rv))
+ obj->headers->munged_subject = ToNewUTF8String(dest);
+ else
+ obj->headers->munged_subject = nullptr;
+ } else {
+ PR_Free(obj->headers->munged_subject);
+ obj->headers->munged_subject = nullptr;
+ }
+ }
+
+ if (msg->hdrs) {
+ bool outer_p = !obj->headers; /* is this the outermost message? */
+
+#ifdef MIME_DRAFTS
+ if (outer_p && obj->options &&
+ (obj->options->decompose_file_p ||
+ obj->options->caller_need_root_headers) &&
+ obj->options->decompose_headers_info_fn) {
+# ifdef ENABLE_SMIME
+ if (obj->options->decrypt_p &&
+ !mime_crypto_object_p(msg->hdrs, false, obj->options))
+ obj->options->decrypt_p = false;
+# endif /* ENABLE_SMIME */
+ if (!obj->options->caller_need_root_headers ||
+ (obj == obj->options->state->root))
+ status = obj->options->decompose_headers_info_fn(
+ obj->options->stream_closure, msg->hdrs);
+ }
+#endif /* MIME_DRAFTS */
+
+ /* If this is the outermost message, we need to run the
+ `generate_header' callback. This happens here instead of
+ in `parse_begin', because it's only now that we've parsed
+ our headers. However, since this is the outermost message,
+ we have yet to write any HTML, so that's fine.
+ */
+ if (outer_p && obj->output_p && obj->options &&
+ obj->options->write_html_p && obj->options->generate_header_html_fn) {
+ int lstatus = 0;
+ char* html = 0;
+
+ /* The generate_header_html_fn might return HTML, so it's important
+ that the output stream be set up with the proper type before we
+ make the MimeObject_write() call below. */
+ if (!obj->options->state->first_data_written_p) {
+ lstatus = MimeObject_output_init(obj, TEXT_HTML);
+ if (lstatus < 0) return lstatus;
+ PR_ASSERT(obj->options->state->first_data_written_p);
+ }
+
+ html = obj->options->generate_header_html_fn(
+ NULL, obj->options->html_closure, msg->hdrs);
+ if (html) {
+ lstatus = MimeObject_write(obj, html, strlen(html), false);
+ PR_Free(html);
+ if (lstatus < 0) return lstatus;
+ }
+ }
+
+ /* Find the content-type of the body of this message.
+ */
+ {
+ bool ok = true;
+ char* mv = MimeHeaders_get(msg->hdrs, HEADER_MIME_VERSION, true, false);
+
+#ifdef REQUIRE_MIME_VERSION_HEADER
+ /* If this is the outermost message, it must have a MIME-Version
+ header with the value 1.0 for us to believe what might be in
+ the Content-Type header. If the MIME-Version header is not
+ present, we must treat this message as untyped.
+ */
+ ok = (mv && !strcmp(mv, "1.0"));
+#else
+ /* #### actually, we didn't check this in Mozilla 2.0, and checking
+ it now could cause some compatibility nonsense, so for now, let's
+ just believe any Content-Type header we see.
+ */
+ ok = true;
+#endif
+
+ if (ok) {
+ ct = MimeHeaders_get(msg->hdrs, HEADER_CONTENT_TYPE, true, false);
+
+ /* If there is no Content-Type header, but there is a MIME-Version
+ header, then assume that this *is* in fact a MIME message.
+ (I've seen messages with
+
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: quoted-printable
+
+ and no Content-Type, and we should treat those as being of type
+ MimeInlineTextPlain rather than MimeUntypedText.)
+ */
+ if (mv && !ct) ct = strdup(TEXT_PLAIN);
+ }
+
+ PR_FREEIF(mv); /* done with this now. */
+ }
+
+ /* If this message has a body which is encrypted and we're going to
+ decrypt it (without converting it to HTML, since decrypt_p and
+ write_html_p are never true at the same time)
+ */
+ if (obj->output_p && obj->options && obj->options->decrypt_p
+#ifdef ENABLE_SMIME
+ && !mime_crypto_object_p(msg->hdrs, false, obj->options)
+#endif /* ENABLE_SMIME */
+ ) {
+ /* The body of this message is not an encrypted object, so we need
+ to turn off the decrypt_p flag (to prevent us from s#$%ing the
+ body of the internal object up into one.) In this case,
+ our output will end up being identical to our input.
+ */
+ obj->options->decrypt_p = false;
+ }
+
+ /* Emit the HTML for this message's headers. Do this before
+ creating the object representing the body.
+ */
+ if (obj->output_p && obj->options && obj->options->write_html_p) {
+ /* If citation headers are on, and this is not the outermost message,
+ turn them off. */
+ if (obj->options->headers == MimeHeadersCitation && !outer_p)
+ obj->options->headers = MimeHeadersSome;
+
+ /* Emit a normal header block. */
+ status = MimeMessage_write_headers_html(obj);
+ if (status < 0) {
+ PR_FREEIF(ct);
+ return status;
+ }
+ } else if (obj->output_p) {
+ /* Dump the headers, raw. */
+ status = MimeObject_write(obj, "", 0, false); /* initialize */
+ if (status < 0) {
+ PR_FREEIF(ct);
+ return status;
+ }
+ status = MimeHeaders_write_raw_headers(msg->hdrs, obj->options,
+ obj->options->decrypt_p);
+ if (status < 0) {
+ PR_FREEIF(ct);
+ return status;
+ }
+ }
+
+#ifdef XP_UNIX
+ if (outer_p && obj->output_p) /* Kludge from mimehdrs.c */
+ MimeHeaders_do_unix_display_hook_hack(msg->hdrs);
+#endif /* XP_UNIX */
+ }
+
+ /* Never put out a separator after a message header block. */
+ if (obj->options && obj->options->state)
+ obj->options->state->separator_suppressed_p = true;
+
+#ifdef MIME_DRAFTS
+ if (!obj->headers && /* outer most message header */
+ obj->options && obj->options->decompose_file_p && ct)
+ obj->options->is_multipart_msg = PL_strcasestr(ct, "multipart/") != NULL;
+#endif /* MIME_DRAFTS */
+
+ body = mime_create(ct, msg->hdrs, obj->options);
+
+ PR_FREEIF(ct);
+ if (!body) return MIME_OUT_OF_MEMORY;
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, body);
+ if (status < 0) {
+ mime_free(body);
+ return status;
+ }
+
+ // Only do this if this is a Text Object!
+ if (mime_typep(body, (MimeObjectClass*)&mimeInlineTextClass)) {
+ ((MimeInlineText*)body)->needUpdateMsgWinCharset = true;
+ }
+
+ /* Now that we've added this new object to our list of children,
+ start its parser going. */
+ status = body->clazz->parse_begin(body);
+ if (status < 0) return status;
+
+ // Now notify the emitter if this is the outer most message, unless
+ // it is a part that is not the head of the message. If it's a part,
+ // we need to figure out the content type/charset of the part
+ //
+ bool outer_p = !obj->headers; /* is this the outermost message? */
+
+ if ((outer_p || obj->options->notify_nested_bodies) &&
+ (!obj->options->part_to_load ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay)) {
+ // call SetMailCharacterSetToMsgWindow() to set a menu charset
+ if (mime_typep(body, (MimeObjectClass*)&mimeInlineTextClass)) {
+ MimeInlineText* text = (MimeInlineText*)body;
+ if (text && text->charset && *text->charset)
+ SetMailCharacterSetToMsgWindow(body, text->charset);
+ }
+
+ char* msgID = MimeHeaders_get(msg->hdrs, HEADER_MESSAGE_ID, false, false);
+
+ const char* outCharset = NULL;
+ if (!obj->options
+ ->force_user_charset) /* Only convert if the user prefs is false */
+ outCharset = "UTF-8";
+
+ mimeEmitterStartBody(obj->options,
+ (obj->options->headers == MimeHeadersNone), msgID,
+ outCharset);
+ PR_FREEIF(msgID);
+
+ // setting up truncated message html fotter function
+ char* xmoz =
+ MimeHeaders_get(msg->hdrs, HEADER_X_MOZILLA_STATUS, false, false);
+ if (xmoz) {
+ uint32_t flags = 0;
+ char dummy = 0;
+ if (sscanf(xmoz, " %x %c", &flags, &dummy) == 1 &&
+ flags & nsMsgMessageFlags::Partial) {
+ obj->options->html_closure = obj;
+ obj->options->generate_footer_html_fn =
+ MimeMessage_partial_message_html;
+ }
+ PR_FREEIF(xmoz);
+ }
+ }
+
+ return 0;
+}
+
+static int MimeMessage_parse_eof(MimeObject* obj, bool abort_p) {
+ int status;
+ bool outer_p;
+ MimeMessage* msg = (MimeMessage*)obj;
+ if (obj->closed_p) return 0;
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ outer_p = !obj->headers; /* is this the outermost message? */
+
+ // Hack for messages with truncated headers (bug 244722)
+ // If there is no empty line in a message, the parser can't figure out where
+ // the headers end, causing parsing to hang. So we insert an extra newline
+ // to keep it happy. This is OK, since a message without any empty lines is
+ // broken anyway...
+ if (outer_p && msg->hdrs && !msg->hdrs->done_p) {
+ MimeMessage_parse_line("\n", 1, obj);
+ }
+
+ // Once we get to the end of parsing the message, we will notify
+ // the emitter that we are done the the body.
+
+ // Mark the end of the mail body if we are actually emitting the
+ // body of the message (i.e. not Header ONLY)
+ if ((outer_p || obj->options->notify_nested_bodies) && obj->options &&
+ obj->options->write_html_p) {
+ if (obj->options->generate_footer_html_fn) {
+ mime_stream_data* msd = (mime_stream_data*)obj->options->stream_closure;
+ if (msd) {
+ char* html = obj->options->generate_footer_html_fn(
+ msd->orig_url_name, obj->options->html_closure, msg->hdrs);
+ if (html) {
+ int lstatus = MimeObject_write(obj, html, strlen(html), false);
+ PR_Free(html);
+ if (lstatus < 0) return lstatus;
+ }
+ }
+ }
+ if ((!obj->options->part_to_load ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay) &&
+ obj->options->headers != MimeHeadersOnly)
+ mimeEmitterEndBody(obj->options);
+ }
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->done_parsing_outer_headers &&
+ !obj->options->is_multipart_msg &&
+ !mime_typep(obj, (MimeObjectClass*)&mimeEncryptedClass) &&
+ obj->options->decompose_file_close_fn) {
+ status =
+ obj->options->decompose_file_close_fn(obj->options->stream_closure);
+
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ /* Put out a separator after every message/rfc822 object. */
+ if (!abort_p && !outer_p) {
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeMessage_add_child(MimeObject* parent, MimeObject* child) {
+ MimeContainer* cont = (MimeContainer*)parent;
+ PR_ASSERT(parent && child);
+ if (!parent || !child) return -1;
+
+ /* message/rfc822 containers can only have one child. */
+ PR_ASSERT(cont->nchildren == 0);
+ if (cont->nchildren != 0) return -1;
+
+#ifdef MIME_DRAFTS
+ if (parent->options && parent->options->decompose_file_p &&
+ !parent->options->is_multipart_msg &&
+ !mime_typep(child, (MimeObjectClass*)&mimeEncryptedClass) &&
+ parent->options->decompose_file_init_fn) {
+ int status = 0;
+ status = parent->options->decompose_file_init_fn(
+ parent->options->stream_closure, ((MimeMessage*)parent)->hdrs);
+ if (status < 0) return status;
+ }
+#endif /* MIME_DRAFTS */
+
+ return ((MimeContainerClass*)&MIME_SUPERCLASS)->add_child(parent, child);
+}
+
+// This is necessary to determine which charset to use for a reply/forward
+char* DetermineMailCharset(MimeMessage* msg) {
+ char* retCharset = nullptr;
+
+ if ((msg) && (msg->hdrs)) {
+ char* ct = MimeHeaders_get(msg->hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (ct) {
+ retCharset = MimeHeaders_get_parameter(ct, "charset", NULL, NULL);
+ PR_Free(ct);
+ }
+
+ if (!retCharset) {
+ // If we didn't find "Content-Type: ...; charset=XX" then look
+ // for "X-Sun-Charset: XX" instead. (Maybe this should be done
+ // in MimeSunAttachmentClass, but it's harder there than here.)
+ retCharset =
+ MimeHeaders_get(msg->hdrs, HEADER_X_SUN_CHARSET, false, false);
+ }
+ }
+
+ if (!retCharset)
+ return strdup("ISO-8859-1");
+ else
+ return retCharset;
+}
+
+static int MimeMessage_write_headers_html(MimeObject* obj) {
+ MimeMessage* msg = (MimeMessage*)obj;
+ int status;
+
+ if (!obj->options || !obj->options->output_fn) return 0;
+
+ PR_ASSERT(obj->output_p && obj->options->write_html_p);
+
+ // To support the no header option! Make sure we are not
+ // suppressing headers on included email messages...
+ if ((obj->options->headers == MimeHeadersNone) &&
+ (obj == obj->options->state->root)) {
+ // Ok, we are going to kick the Emitter for a StartHeader
+ // operation ONLY WHEN THE CHARSET OF THE ORIGINAL MESSAGE IS
+ // NOT US-ASCII ("ISO-8859-1")
+ //
+ // This is only to notify the emitter of the charset of the
+ // original message
+ char* mailCharset = DetermineMailCharset(msg);
+
+ if ((mailCharset) && (PL_strcasecmp(mailCharset, "US-ASCII")) &&
+ (PL_strcasecmp(mailCharset, "ISO-8859-1")))
+ mimeEmitterUpdateCharacterSet(obj->options, mailCharset);
+ PR_FREEIF(mailCharset);
+ return 0;
+ }
+
+ if (!obj->options->state->first_data_written_p) {
+ status = MimeObject_output_init(obj, TEXT_HTML);
+ if (status < 0) {
+ mimeEmitterEndHeader(obj->options, obj);
+ return status;
+ }
+ PR_ASSERT(obj->options->state->first_data_written_p);
+ }
+
+ // Start the header parsing by the emitter
+ char* msgID = MimeHeaders_get(msg->hdrs, HEADER_MESSAGE_ID, false, false);
+ bool outer_p = !obj->headers; /* is this the outermost message? */
+ if (!outer_p &&
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay &&
+ obj->options->part_to_load) {
+ // Maybe we are displaying a embedded message as outer part!
+ char* id = mime_part_address(obj);
+ if (id) {
+ outer_p = !strcmp(id, obj->options->part_to_load);
+ PR_Free(id);
+ }
+ }
+
+ // Ok, we should really find out the charset of this part. We always
+ // output UTF-8 for display, but the original charset is necessary for
+ // reply and forward operations.
+ //
+ char* mailCharset = DetermineMailCharset(msg);
+ mimeEmitterStartHeader(obj->options, outer_p,
+ (obj->options->headers == MimeHeadersOnly), msgID,
+ mailCharset);
+
+ // Change the default_charset by the charset of the original message
+ // ONLY WHEN THE CHARSET OF THE ORIGINAL MESSAGE IS NOT US-ASCII
+ // ("ISO-8859-1") and default_charset and mailCharset are different,
+ // or when there is no default_charset (this can happen with saved messages).
+ if ((!obj->options->default_charset ||
+ ((mailCharset) && (PL_strcasecmp(mailCharset, "US-ASCII")) &&
+ (PL_strcasecmp(mailCharset, "ISO-8859-1")) &&
+ (PL_strcasecmp(obj->options->default_charset, mailCharset)))) &&
+ !obj->options->override_charset) {
+ PR_FREEIF(obj->options->default_charset);
+ obj->options->default_charset = strdup(mailCharset);
+ }
+
+ PR_FREEIF(msgID);
+ PR_FREEIF(mailCharset);
+
+ status = MimeHeaders_write_all_headers(msg->hdrs, obj->options, false);
+ if (status < 0) {
+ mimeEmitterEndHeader(obj->options, obj);
+ return status;
+ }
+
+ // If this is the outermost message, then now is the time to run the
+ // post_header_html_fn.
+ if (obj->options && obj->options->state &&
+ obj->options->generate_post_header_html_fn &&
+ !obj->options->state->post_header_html_run_p) {
+ char* html = 0;
+ PR_ASSERT(obj->options->state->first_data_written_p);
+ html = obj->options->generate_post_header_html_fn(
+ NULL, obj->options->html_closure, msg->hdrs);
+ obj->options->state->post_header_html_run_p = true;
+ if (html) {
+ status = MimeObject_write(obj, html, strlen(html), false);
+ PR_Free(html);
+ if (status < 0) {
+ mimeEmitterEndHeader(obj->options, obj);
+ return status;
+ }
+ }
+ }
+
+ mimeEmitterEndHeader(obj->options, obj);
+
+ // rhp:
+ // For now, we are going to parse the entire message, even if we are
+ // only interested in headers...why? Well, because this is the only
+ // way to build the attachment list. Now we will have the attachment
+ // list in the output being created by the XML emitter. If we ever
+ // want to go back to where we were before, just uncomment the conditional
+ // and it will stop at header parsing.
+ //
+ // if (obj->options->headers == MimeHeadersOnly)
+ // return -1;
+ // else
+
+ return 0;
+}
+
+static char* MimeMessage_partial_message_html(const char* data, void* closure,
+ MimeHeaders* headers) {
+ MimeMessage* msg = (MimeMessage*)closure;
+ nsAutoCString orig_url(data);
+ char* uidl = MimeHeaders_get(headers, HEADER_X_UIDL, false, false);
+ char* msgId = MimeHeaders_get(headers, HEADER_MESSAGE_ID, false, false);
+ char* msgIdPtr = PL_strchr(msgId, '<');
+
+ int32_t pos = orig_url.Find("mailbox-message");
+ if (pos != -1) orig_url.Cut(pos + 7, 8);
+
+ pos = orig_url.FindChar('#');
+ if (pos != -1) orig_url.Replace(pos, 1, "?number=", 8);
+
+ if (msgIdPtr)
+ msgIdPtr++;
+ else
+ msgIdPtr = msgId;
+ char* gtPtr = PL_strchr(msgIdPtr, '>');
+ if (gtPtr) *gtPtr = 0;
+
+ bool msgBaseTruncated = (msg->bodyLength > MSG_LINEBREAK_LEN);
+
+ nsCString partialMsgHtml;
+ nsCString item;
+
+ partialMsgHtml.AppendLiteral(
+ "<div style=\"margin: 1em auto; border: 1px solid black; width: 80%\">");
+ partialMsgHtml.AppendLiteral(
+ "<div style=\"margin: 5px; padding: 10px; border: 1px solid gray; "
+ "font-weight: bold; text-align: center;\">");
+
+ partialMsgHtml.AppendLiteral("<span style=\"font-size: 120%;\">");
+ if (msgBaseTruncated)
+ item.Adopt(MimeGetStringByName(u"MIME_MSG_PARTIAL_TRUNCATED"));
+ else
+ item.Adopt(MimeGetStringByName(u"MIME_MSG_PARTIAL_NOT_DOWNLOADED"));
+ partialMsgHtml += item;
+ partialMsgHtml.AppendLiteral("</span><hr>");
+
+ if (msgBaseTruncated)
+ item.Adopt(MimeGetStringByName(u"MIME_MSG_PARTIAL_TRUNCATED_EXPLANATION"));
+ else
+ item.Adopt(
+ MimeGetStringByName(u"MIME_MSG_PARTIAL_NOT_DOWNLOADED_EXPLANATION"));
+ partialMsgHtml += item;
+ partialMsgHtml.AppendLiteral("<br><br>");
+
+ partialMsgHtml.AppendLiteral("<a href=\"");
+ partialMsgHtml.Append(orig_url);
+
+ if (msgIdPtr) {
+ partialMsgHtml.AppendLiteral("&messageid=");
+
+ MsgEscapeString(nsDependentCString(msgIdPtr), nsINetUtil::ESCAPE_URL_PATH,
+ item);
+
+ partialMsgHtml.Append(item);
+ }
+
+ if (uidl) {
+ partialMsgHtml.AppendLiteral("&uidl=");
+
+ MsgEscapeString(nsDependentCString(uidl), nsINetUtil::ESCAPE_XALPHAS, item);
+
+ partialMsgHtml.Append(item);
+ }
+
+ partialMsgHtml.AppendLiteral("\">");
+ item.Adopt(MimeGetStringByName(u"MIME_MSG_PARTIAL_CLICK_FOR_REST"));
+ partialMsgHtml += item;
+ partialMsgHtml.AppendLiteral("</a>");
+
+ partialMsgHtml.AppendLiteral("</div></div>");
+
+ return ToNewCString(partialMsgHtml);
+}
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeMessage_debug_print(MimeObject* obj, PRFileDesc* stream,
+ int32_t depth) {
+ MimeMessage* msg = (MimeMessage*)obj;
+ char* addr = mime_part_address(obj);
+ int i;
+ for (i = 0; i < depth; i++) PR_Write(stream, " ", 2);
+ /*
+ fprintf(stream, "<%s %s%s 0x%08X>\n",
+ obj->clazz->class_name,
+ addr ? addr : "???",
+ (msg->container.nchildren == 0 ? " (no body)" : ""),
+ (uint32_t) msg);
+ */
+ PR_FREEIF(addr);
+
+# if 0
+ if (msg->hdrs)
+ {
+ char *s;
+
+ depth++;
+
+# define DUMP(HEADER) \
+ for (i = 0; i < depth; i++) PR_Write(stream, " ", 2); \
+ s = MimeHeaders_get(msg->hdrs, HEADER, false, true);
+/**
+ \
+ PR_Write(stream, HEADER ": %s\n", s ? s : ""); \
+**/
+
+ PR_FREEIF(s)
+
+ DUMP(HEADER_SUBJECT);
+ DUMP(HEADER_DATE);
+ DUMP(HEADER_FROM);
+ DUMP(HEADER_TO);
+ /* DUMP(HEADER_CC); */
+ DUMP(HEADER_NEWSGROUPS);
+ DUMP(HEADER_MESSAGE_ID);
+# undef DUMP
+
+ PR_Write(stream, "\n", 1);
+ }
+# endif
+
+ PR_ASSERT(msg->container.nchildren <= 1);
+ if (msg->container.nchildren == 1) {
+ MimeObject* kid = msg->container.children[0];
+ int status = kid->clazz->debug_print(kid, stream, depth + 1);
+ if (status < 0) return status;
+ }
+ return 0;
+}
+#endif
diff --git a/comm/mailnews/mime/src/mimemsg.h b/comm/mailnews/mime/src/mimemsg.h
new file mode 100644
index 0000000000..ec8c45336c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemsg.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMSG_H_
+#define _MIMEMSG_H_
+
+#include "mimecont.h"
+
+/* The MimeMessage class implements the message/rfc822 and message/news
+ MIME containers, which is to say, mail and news messages.
+ */
+
+typedef struct MimeMessageClass MimeMessageClass;
+typedef struct MimeMessage MimeMessage;
+
+struct MimeMessageClass {
+ MimeContainerClass container;
+};
+
+extern MimeMessageClass mimeMessageClass;
+
+struct MimeMessage {
+ MimeContainer container; /* superclass variables */
+ MimeHeaders* hdrs; /* headers of this message */
+ bool newline_p; /* whether the last line ended in a newline */
+
+ bool grabSubject; /* Should we try to grab the subject of this message */
+ int32_t bodyLength; /* Used for determining if the body has been truncated */
+ int32_t sizeSoFar; /* The total size of the MIME message, once parsing is
+ finished. */
+};
+
+#define MimeMessageClassInitializer(ITYPE, CSUPER) \
+ { MimeContainerClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMSG_H_ */
diff --git a/comm/mailnews/mime/src/mimemsig.cpp b/comm/mailnews/mime/src/mimemsig.cpp
new file mode 100644
index 0000000000..5f390abd94
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemsig.cpp
@@ -0,0 +1,716 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "modmimee.h"
+#include "mimemsig.h"
+#include "nspr.h"
+
+#include "prmem.h"
+#include "plstr.h"
+#include "prerror.h"
+#include "nsMimeTypes.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "mimeeobj.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+#include "mozilla/Attributes.h"
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeMultipartSigned, MimeMultipartSignedClass,
+ mimeMultipartSignedClass, &MIME_SUPERCLASS);
+
+static int MimeMultipartSigned_initialize(MimeObject*);
+static int MimeMultipartSigned_create_child(MimeObject*);
+static int MimeMultipartSigned_close_child(MimeObject*);
+static int MimeMultipartSigned_parse_line(const char*, int32_t, MimeObject*);
+static int MimeMultipartSigned_parse_child_line(MimeObject*, const char*,
+ int32_t, bool);
+static int MimeMultipartSigned_parse_eof(MimeObject*, bool);
+static void MimeMultipartSigned_finalize(MimeObject*);
+
+static int MimeMultipartSigned_emit_child(MimeObject* obj);
+
+extern "C" MimeSuppressedCryptoClass mimeSuppressedCryptoClass;
+
+static int MimeMultipartSignedClassInitialize(MimeMultipartSignedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+
+ oclass->initialize = MimeMultipartSigned_initialize;
+ oclass->parse_line = MimeMultipartSigned_parse_line;
+ oclass->parse_eof = MimeMultipartSigned_parse_eof;
+ oclass->finalize = MimeMultipartSigned_finalize;
+ mclass->create_child = MimeMultipartSigned_create_child;
+ mclass->parse_child_line = MimeMultipartSigned_parse_child_line;
+ mclass->close_child = MimeMultipartSigned_close_child;
+
+ PR_ASSERT(!oclass->class_initialized);
+ return 0;
+}
+
+static int MimeMultipartSigned_initialize(MimeObject* object) {
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)object;
+
+ /* This is an abstract class; it shouldn't be directly instantiated. */
+ PR_ASSERT(object->clazz != (MimeObjectClass*)&mimeMultipartSignedClass);
+
+ sig->state = MimeMultipartSignedPreamble;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeMultipartSigned_cleanup(MimeObject* obj, bool finalizing_p) {
+ MimeMultipart* mult = (MimeMultipart*)obj; /* #58075. Fix suggested by jwz */
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)obj;
+ if (sig->part_buffer) {
+ MimePartBufferDestroy(sig->part_buffer);
+ sig->part_buffer = 0;
+ }
+ if (sig->body_hdrs) {
+ MimeHeaders_free(sig->body_hdrs);
+ sig->body_hdrs = 0;
+ }
+ if (sig->sig_hdrs) {
+ MimeHeaders_free(sig->sig_hdrs);
+ sig->sig_hdrs = 0;
+ }
+
+ mult->state = MimeMultipartEpilogue; /* #58075. Fix suggested by jwz */
+ sig->state = MimeMultipartSignedEpilogue;
+
+ if (finalizing_p && sig->crypto_closure) {
+ /* Don't free these until this object is really going away -- keep them
+ around for the lifetime of the MIME object, so that we can get at the
+ security info of sub-parts of the currently-displayed message. */
+ ((MimeMultipartSignedClass*)obj->clazz)->crypto_free(sig->crypto_closure);
+ sig->crypto_closure = 0;
+ }
+
+ if (sig->sig_decoder_data) {
+ MimeDecoderDestroy(sig->sig_decoder_data, true);
+ sig->sig_decoder_data = 0;
+ }
+}
+
+static int MimeMultipartSigned_parse_eof(MimeObject* obj, bool abort_p) {
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)obj;
+ int status = 0;
+
+ if (obj->closed_p) return 0;
+
+ /* Close off the signature, if we've gotten that far.
+ */
+ if (sig->state == MimeMultipartSignedSignatureHeaders ||
+ sig->state == MimeMultipartSignedSignatureFirstLine ||
+ sig->state == MimeMultipartSignedSignatureLine ||
+ sig->state == MimeMultipartSignedEpilogue) {
+ status = (((MimeMultipartSignedClass*)obj->clazz)->crypto_signature_eof)(
+ sig->crypto_closure, abort_p);
+ if (status < 0) return status;
+ }
+
+ if (!abort_p) {
+ /* Now that we've read both the signed object and the signature (and
+ have presumably verified the signature) write out a blurb, and then
+ the signed object.
+ */
+ status = MimeMultipartSigned_emit_child(obj);
+ if (status < 0) {
+ obj->closed_p = true;
+ return status;
+ }
+ }
+
+ MimeMultipartSigned_cleanup(obj, false);
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+}
+
+static void MimeMultipartSigned_finalize(MimeObject* obj) {
+ MimeMultipartSigned_cleanup(obj, true);
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeMultipartSigned_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)obj;
+ MimeMultipartParseState old_state = mult->state;
+ bool hash_line_p = true;
+ bool no_headers_p = false;
+ int status = 0;
+
+ /* First do the parsing for normal multipart/ objects by handing it off to
+ the superclass method. This includes calling the create_child and
+ close_child methods.
+ */
+ status =
+ (((MimeObjectClass*)(&MIME_SUPERCLASS))->parse_line(line, length, obj));
+ if (status < 0) return status;
+
+ /* The instance variable MimeMultipartClass->state tracks motion through
+ the various stages of multipart/ parsing. The instance variable
+ MimeMultipartSigned->state tracks the difference between the first
+ part (the body) and the second part (the signature.) This second,
+ more specific state variable is updated by noticing the transitions
+ of the first, more general state variable.
+ */
+ if (old_state != mult->state) /* there has been a state change */
+ {
+ switch (mult->state) {
+ case MimeMultipartPreamble:
+ PR_ASSERT(0); /* can't move *in* to preamble state. */
+ sig->state = MimeMultipartSignedPreamble;
+ break;
+
+ case MimeMultipartHeaders:
+ /* If we're moving in to the Headers state, then that means
+ that this line is the preceding boundary string (and we
+ should ignore it.)
+ */
+ hash_line_p = false;
+
+ if (sig->state == MimeMultipartSignedPreamble)
+ sig->state = MimeMultipartSignedBodyFirstHeader;
+ else if (sig->state == MimeMultipartSignedBodyFirstLine ||
+ sig->state == MimeMultipartSignedBodyLine)
+ sig->state = MimeMultipartSignedSignatureHeaders;
+ else if (sig->state == MimeMultipartSignedSignatureFirstLine ||
+ sig->state == MimeMultipartSignedSignatureLine)
+ sig->state = MimeMultipartSignedEpilogue;
+ break;
+
+ case MimeMultipartPartFirstLine:
+ if (sig->state == MimeMultipartSignedBodyFirstHeader) {
+ sig->state = MimeMultipartSignedBodyFirstLine;
+ no_headers_p = true;
+ } else if (sig->state == MimeMultipartSignedBodyHeaders)
+ sig->state = MimeMultipartSignedBodyFirstLine;
+ else if (sig->state == MimeMultipartSignedSignatureHeaders)
+ sig->state = MimeMultipartSignedSignatureFirstLine;
+ else
+ sig->state = MimeMultipartSignedEpilogue;
+ break;
+
+ case MimeMultipartPartLine:
+
+ PR_ASSERT(sig->state == MimeMultipartSignedBodyFirstLine ||
+ sig->state == MimeMultipartSignedBodyLine ||
+ sig->state == MimeMultipartSignedSignatureFirstLine ||
+ sig->state == MimeMultipartSignedSignatureLine);
+
+ if (sig->state == MimeMultipartSignedBodyFirstLine)
+ sig->state = MimeMultipartSignedBodyLine;
+ else if (sig->state == MimeMultipartSignedSignatureFirstLine)
+ sig->state = MimeMultipartSignedSignatureLine;
+ break;
+
+ case MimeMultipartEpilogue:
+ sig->state = MimeMultipartSignedEpilogue;
+ break;
+
+ default: /* bad state */
+ NS_ERROR("bad state in MultipartSigned parse line");
+ return -1;
+ break;
+ }
+ }
+
+ /* Perform multipart/signed-related actions on this line based on the state
+ of the parser.
+ */
+ switch (sig->state) {
+ case MimeMultipartSignedPreamble:
+ /* Do nothing. */
+ break;
+
+ case MimeMultipartSignedBodyFirstLine:
+ /* We have just moved out of the MimeMultipartSignedBodyHeaders
+ state, so cache away the headers that apply only to the body part.
+ */
+ NS_ASSERTION(mult->hdrs, "null multipart hdrs");
+ NS_ASSERTION(!sig->body_hdrs,
+ "signed part shouldn't have already have body_hdrs");
+ sig->body_hdrs = mult->hdrs;
+ mult->hdrs = 0;
+
+ /* fall through. */
+ [[fallthrough]];
+ case MimeMultipartSignedBodyFirstHeader:
+ case MimeMultipartSignedBodyHeaders:
+ case MimeMultipartSignedBodyLine:
+
+ if (!sig->crypto_closure) {
+ /* Set error change */
+ PR_SetError(0, 0);
+ /* Initialize the signature verification library. */
+ sig->crypto_closure =
+ (((MimeMultipartSignedClass*)obj->clazz)->crypto_init)(obj);
+ if (!sig->crypto_closure) {
+ status = PR_GetError();
+ NS_ASSERTION(status < 0, "got non-negative status");
+ if (status >= 0) status = -1;
+ return status;
+ }
+ }
+
+ if (hash_line_p) {
+ /* this is the first hashed line if this is the first header
+ (that is, if it's the first line in the header state after
+ a state change.)
+ */
+ bool first_line_p =
+ (no_headers_p || sig->state == MimeMultipartSignedBodyFirstHeader);
+
+ if (sig->state == MimeMultipartSignedBodyFirstHeader)
+ sig->state = MimeMultipartSignedBodyHeaders;
+
+ /* The newline issues here are tricky, since both the newlines
+ before and after the boundary string are to be considered part
+ of the boundary: this is so that a part can be specified such
+ that it does not end in a trailing newline.
+
+ To implement this, we send a newline *before* each line instead
+ of after, except for the first line, which is not preceded by a
+ newline.
+
+ For purposes of cryptographic hashing, we always hash line
+ breaks as CRLF -- the canonical, on-the-wire linebreaks, since
+ we have no idea of knowing what line breaks were used on the
+ originating system (SMTP rightly destroys that information.)
+ */
+
+ /* Remove the trailing newline... */
+ if (length > 0 && line[length - 1] == '\n') length--;
+ if (length > 0 && line[length - 1] == '\r') length--;
+
+ PR_ASSERT(sig->crypto_closure);
+
+ if (!first_line_p) {
+ /* Push out a preceding newline... */
+ char nl[] = CRLF;
+ status = (((MimeMultipartSignedClass*)obj->clazz)
+ ->crypto_data_hash(nl, 2, sig->crypto_closure));
+ if (status < 0) return status;
+ }
+
+ /* Now push out the line sans trailing newline. */
+ if (length > 0)
+ status = (((MimeMultipartSignedClass*)obj->clazz)
+ ->crypto_data_hash(line, length, sig->crypto_closure));
+ if (status < 0) return status;
+ }
+ break;
+
+ case MimeMultipartSignedSignatureHeaders:
+
+ if (sig->crypto_closure && old_state != mult->state) {
+ /* We have just moved out of the MimeMultipartSignedBodyLine
+ state, so tell the signature verification library that we've
+ reached the end of the signed data.
+ */
+ status = (((MimeMultipartSignedClass*)obj->clazz)->crypto_data_eof)(
+ sig->crypto_closure, false);
+ if (status < 0) return status;
+ }
+ break;
+
+ case MimeMultipartSignedSignatureFirstLine:
+ /* We have just moved out of the MimeMultipartSignedSignatureHeaders
+ state, so cache away the headers that apply only to the sig part.
+ */
+ PR_ASSERT(mult->hdrs);
+ PR_ASSERT(!sig->sig_hdrs);
+ sig->sig_hdrs = mult->hdrs;
+ mult->hdrs = 0;
+
+ /* If the signature block has an encoding, set up a decoder for it.
+ (Similar logic is in MimeLeafClass->parse_begin.)
+ */
+ {
+ MimeDecoderData* (*fn)(MimeConverterOutputCallback, void*) = 0;
+ nsCString encoding;
+ encoding.Adopt(MimeHeaders_get(
+ sig->sig_hdrs, HEADER_CONTENT_TRANSFER_ENCODING, true, false));
+ if (encoding.IsEmpty())
+ ;
+ else if (!PL_strcasecmp(encoding.get(), ENCODING_BASE64))
+ fn = &MimeB64DecoderInit;
+ else if (!PL_strcasecmp(encoding.get(), ENCODING_QUOTED_PRINTABLE)) {
+ sig->sig_decoder_data = MimeQPDecoderInit(
+ ((MimeConverterOutputCallback)(((MimeMultipartSignedClass*)
+ obj->clazz)
+ ->crypto_signature_hash)),
+ sig->crypto_closure);
+ if (!sig->sig_decoder_data) return MIME_OUT_OF_MEMORY;
+ } else if (!PL_strcasecmp(encoding.get(), ENCODING_UUENCODE) ||
+ !PL_strcasecmp(encoding.get(), ENCODING_UUENCODE2) ||
+ !PL_strcasecmp(encoding.get(), ENCODING_UUENCODE3) ||
+ !PL_strcasecmp(encoding.get(), ENCODING_UUENCODE4))
+ fn = &MimeUUDecoderInit;
+ else if (!PL_strcasecmp(encoding.get(), ENCODING_YENCODE))
+ fn = &MimeYDecoderInit;
+ if (fn) {
+ sig->sig_decoder_data =
+ fn(((MimeConverterOutputCallback)(((MimeMultipartSignedClass*)
+ obj->clazz)
+ ->crypto_signature_hash)),
+ sig->crypto_closure);
+ if (!sig->sig_decoder_data) return MIME_OUT_OF_MEMORY;
+ }
+ }
+
+ /* Show these headers to the crypto module. */
+ if (hash_line_p) {
+ status =
+ (((MimeMultipartSignedClass*)obj->clazz)->crypto_signature_init)(
+ sig->crypto_closure, obj, sig->sig_hdrs);
+ if (status < 0) return status;
+ }
+
+ /* fall through. */
+ [[fallthrough]];
+ case MimeMultipartSignedSignatureLine:
+ if (hash_line_p) {
+ /* Feed this line into the signature verification routines. */
+
+ if (sig->sig_decoder_data)
+ status =
+ MimeDecoderWrite(sig->sig_decoder_data, line, length, nullptr);
+ else
+ status =
+ (((MimeMultipartSignedClass*)obj->clazz)
+ ->crypto_signature_hash(line, length, sig->crypto_closure));
+ if (status < 0) return status;
+ }
+ break;
+
+ case MimeMultipartSignedEpilogue:
+ /* Nothing special to do here. */
+ break;
+
+ default: /* bad state */
+ PR_ASSERT(0);
+ return -1;
+ }
+
+ return status;
+}
+
+static int MimeMultipartSigned_create_child(MimeObject* parent) {
+ /* Don't actually create a child -- we call the superclass create_child
+ method later, after we've fully parsed everything. (And we only call
+ it once, for part #1, and never for part #2 (the signature.))
+ */
+ MimeMultipart* mult = (MimeMultipart*)parent;
+ mult->state = MimeMultipartPartFirstLine;
+ return 0;
+}
+
+static int MimeMultipartSigned_close_child(MimeObject* obj) {
+ /* The close_child method on MimeMultipartSigned doesn't actually do
+ anything to the children list, since the create_child method also
+ doesn't do anything.
+ */
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeContainer* cont = (MimeContainer*)obj;
+ MimeMultipartSigned* msig = (MimeMultipartSigned*)obj;
+
+ if (msig->part_buffer)
+ /* Closes the tmp file, if there is one: doesn't free the part_buffer. */
+ MimePartBufferClose(msig->part_buffer);
+
+ if (mult->hdrs) /* duplicated from MimeMultipart_close_child, ugh. */
+ {
+ MimeHeaders_free(mult->hdrs);
+ mult->hdrs = 0;
+ }
+
+ /* Should be no kids yet. */
+ PR_ASSERT(cont->nchildren == 0);
+ if (cont->nchildren != 0) return -1;
+
+ return 0;
+}
+
+static int MimeMultipartSigned_parse_child_line(MimeObject* obj,
+ const char* line,
+ int32_t length,
+ bool first_line_p) {
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)obj;
+ MimeContainer* cont = (MimeContainer*)obj;
+ int status = 0;
+
+ /* Shouldn't have made any sub-parts yet. */
+ PR_ASSERT(cont->nchildren == 0);
+ if (cont->nchildren != 0) return -1;
+
+ switch (sig->state) {
+ case MimeMultipartSignedPreamble:
+ case MimeMultipartSignedBodyFirstHeader:
+ case MimeMultipartSignedBodyHeaders:
+ // How'd we get here? Oh well, fall through.
+ NS_ERROR("wrong state in parse child line");
+ [[fallthrough]];
+ case MimeMultipartSignedBodyFirstLine:
+ PR_ASSERT(first_line_p);
+ if (!sig->part_buffer) {
+ sig->part_buffer = MimePartBufferCreate();
+ if (!sig->part_buffer) return MIME_OUT_OF_MEMORY;
+ }
+ /* fall through */
+ [[fallthrough]];
+ case MimeMultipartSignedBodyLine: {
+ /* This is the first part; we are buffering it, and will emit it all
+ at the end (so that we know whether the signature matches before
+ showing anything to the user.)
+ */
+
+ /* The newline issues here are tricky, since both the newlines
+ before and after the boundary string are to be considered part
+ of the boundary: this is so that a part can be specified such
+ that it does not end in a trailing newline.
+
+ To implement this, we send a newline *before* each line instead
+ of after, except for the first line, which is not preceded by a
+ newline.
+ */
+
+ /* Remove the trailing newline... */
+ if (length > 0 && line[length - 1] == '\n') length--;
+ if (length > 0 && line[length - 1] == '\r') length--;
+
+ PR_ASSERT(sig->part_buffer);
+ PR_ASSERT(first_line_p ==
+ (sig->state == MimeMultipartSignedBodyFirstLine));
+
+ if (!first_line_p) {
+ /* Push out a preceding newline... */
+ char nl[] = MSG_LINEBREAK;
+ status = MimePartBufferWrite(sig->part_buffer, nl, MSG_LINEBREAK_LEN);
+ if (status < 0) return status;
+ }
+
+ /* Now push out the line sans trailing newline. */
+ if (length > 0)
+ status = MimePartBufferWrite(sig->part_buffer, line, length);
+ if (status < 0) return status;
+ } break;
+
+ case MimeMultipartSignedSignatureHeaders:
+ // How'd we get here? Oh well, fall through.
+ NS_ERROR("should have already parse sig hdrs");
+ [[fallthrough]];
+ case MimeMultipartSignedSignatureFirstLine:
+ case MimeMultipartSignedSignatureLine:
+ /* Nothing to do here -- hashing of the signature part is handled up
+ in MimeMultipartSigned_parse_line().
+ */
+ break;
+
+ case MimeMultipartSignedEpilogue:
+ /* Too many kids? MimeMultipartSigned_create_child() should have
+ prevented us from getting here. */
+ NS_ERROR("too many kids?");
+ return -1;
+ break;
+
+ default: /* bad state */
+ NS_ERROR("bad state in multipart signed parse line");
+ return -1;
+ break;
+ }
+
+ return status;
+}
+
+static int MimeMultipartSigned_emit_child(MimeObject* obj) {
+ MimeMultipartSigned* sig = (MimeMultipartSigned*)obj;
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeContainer* cont = (MimeContainer*)obj;
+ int status = 0;
+ MimeObject* body;
+
+ if (!sig->crypto_closure) {
+ // We might have decided to skip processing this part.
+ return 0;
+ }
+
+ NS_ASSERTION(sig->crypto_closure, "no crypto closure");
+
+ /* Emit some HTML saying whether the signature was cool.
+ But don't emit anything if in FO_QUOTE_MESSAGE mode.
+ */
+ if (obj->options && obj->options->headers != MimeHeadersCitation &&
+ obj->options->write_html_p && obj->options->output_fn &&
+ sig->crypto_closure) {
+ // Calling crypto_generate_html may trigger wanted side effects,
+ // but we're no longer using its results.
+ char* html = (((MimeMultipartSignedClass*)obj->clazz)
+ ->crypto_generate_html(sig->crypto_closure));
+ PR_FREEIF(html);
+
+ /* Now that we have written out the crypto stamp, the outermost header
+ block is well and truly closed. If this is in fact the outermost
+ message, then run the post_header_html_fn now.
+ */
+ if (obj->options && obj->options->state &&
+ obj->options->generate_post_header_html_fn &&
+ !obj->options->state->post_header_html_run_p) {
+ MimeHeaders* outer_headers = nullptr;
+ MimeObject* p;
+ for (p = obj; p->parent; p = p->parent) outer_headers = p->headers;
+ NS_ASSERTION(obj->options->state->first_data_written_p,
+ "should have already written some data");
+ html = obj->options->generate_post_header_html_fn(
+ NULL, obj->options->html_closure, outer_headers);
+ obj->options->state->post_header_html_run_p = true;
+ if (html) {
+ status = MimeObject_write(obj, html, strlen(html), false);
+ PR_Free(html);
+ if (status < 0) return status;
+ }
+ }
+ }
+
+ /* Oh, this is fairly nasty. We're skipping over our "create child" method
+ and using the one our superclass defines. Perhaps instead we should add
+ a new method on this class, and initialize that method to be the
+ create_child method of the superclass. Whatever.
+ */
+
+ /* The superclass method expects to find the headers for the part that it's
+ to create in mult->hdrs, so ensure that they're there. */
+ NS_ASSERTION(!mult->hdrs, "shouldn't already have hdrs for multipart");
+ if (mult->hdrs) MimeHeaders_free(mult->hdrs);
+ mult->hdrs = sig->body_hdrs;
+ sig->body_hdrs = 0;
+
+ /* Run the superclass create_child method.
+ */
+ status = (((MimeMultipartClass*)(&MIME_SUPERCLASS))->create_child(obj));
+ if (status < 0) return status;
+
+ // Notify the charset of the first part.
+ if (obj->options && !(obj->options->override_charset)) {
+ MimeObject* firstChild = ((MimeContainer*)obj)->children[0];
+ char* disposition = MimeHeaders_get(
+ firstChild->headers, HEADER_CONTENT_DISPOSITION, true, false);
+ // check if need to show as inline
+ if (!disposition) {
+ const char* content_type = firstChild->content_type;
+ if (!PL_strcasecmp(content_type, TEXT_PLAIN) ||
+ !PL_strcasecmp(content_type, TEXT_HTML) ||
+ !PL_strcasecmp(content_type, TEXT_MDL) ||
+ !PL_strcasecmp(content_type, MULTIPART_ALTERNATIVE) ||
+ !PL_strcasecmp(content_type, MULTIPART_RELATED) ||
+ !PL_strcasecmp(content_type, MESSAGE_NEWS) ||
+ !PL_strcasecmp(content_type, MESSAGE_RFC822)) {
+ char* ct =
+ MimeHeaders_get(mult->hdrs, HEADER_CONTENT_TYPE, false, false);
+ if (ct) {
+ char* cset = MimeHeaders_get_parameter(ct, "charset", NULL, NULL);
+ if (cset) {
+ mimeEmitterUpdateCharacterSet(obj->options, cset);
+ SetMailCharacterSetToMsgWindow(obj, cset);
+ PR_Free(cset);
+ }
+ PR_Free(ct);
+ }
+ }
+ }
+ }
+
+ // The js emitter wants to know about the newly created child. Because
+ // MimeMultipartSigned dummies out its create_child operation, the logic
+ // in MimeMultipart_parse_line that would normally provide this notification
+ // does not get to fire.
+ if (obj->options && obj->options->notify_nested_bodies) {
+ MimeObject* kid = ((MimeContainer*)obj)->children[0];
+ // The emitter is expecting the content type with parameters; not the fully
+ // parsed thing, so get it from raw. (We do not do it in the charset
+ // notification block that just happened because it already has complex
+ // if-checks that do not jive with us.
+ char* ct = MimeHeaders_get(mult->hdrs, HEADER_CONTENT_TYPE, false, false);
+ mimeEmitterAddHeaderField(obj->options, HEADER_CONTENT_TYPE,
+ ct ? ct : "text/plain");
+ PR_Free(ct);
+
+ char* part_path = mime_part_address(kid);
+ if (part_path) {
+ mimeEmitterAddHeaderField(obj->options, "x-jsemitter-part-path",
+ part_path);
+ PR_Free(part_path);
+ }
+ }
+
+ /* Retrieve the child that it created.
+ */
+ NS_ASSERTION(cont->nchildren == 1, "should only have one child");
+ if (cont->nchildren != 1) return -1;
+ body = cont->children[0];
+ NS_ASSERTION(body, "missing body");
+ if (!body) return -1;
+
+ if (mime_typep(body, (MimeObjectClass*)&mimeSuppressedCryptoClass)) {
+ ((MimeMultipartSignedClass*)obj->clazz)
+ ->crypto_notify_suppressed_child(sig->crypto_closure);
+ }
+
+#ifdef MIME_DRAFTS
+ if (body->options->decompose_file_p) {
+ body->options->signed_p = true;
+ if (!mime_typep(body, (MimeObjectClass*)&mimeMultipartClass) &&
+ body->options->decompose_file_init_fn)
+ body->options->decompose_file_init_fn(body->options->stream_closure,
+ body->headers);
+ }
+#endif /* MIME_DRAFTS */
+
+ /* If there's no part_buffer, this is a zero-length signed message? */
+ if (sig->part_buffer) {
+#ifdef MIME_DRAFTS
+ if (body->options->decompose_file_p &&
+ !mime_typep(body, (MimeObjectClass*)&mimeMultipartClass) &&
+ body->options->decompose_file_output_fn)
+ status =
+ MimePartBufferRead(sig->part_buffer,
+ /* The (MimeConverterOutputCallback) cast is to
+ turn the `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)
+ body->options->decompose_file_output_fn),
+ body->options->stream_closure);
+ else
+#endif /* MIME_DRAFTS */
+
+ status = MimePartBufferRead(
+ sig->part_buffer,
+ /* The (MimeConverterOutputCallback) cast is to turn the
+ `void' argument into `MimeObject'. */
+ ((MimeConverterOutputCallback)body->clazz->parse_buffer), body);
+ if (status < 0) return status;
+ }
+
+ MimeMultipartSigned_cleanup(obj, false);
+
+ /* Done parsing. */
+ status = body->clazz->parse_eof(body, false);
+ if (status < 0) return status;
+ status = body->clazz->parse_end(body, false);
+ if (status < 0) return status;
+
+#ifdef MIME_DRAFTS
+ if (body->options->decompose_file_p &&
+ !mime_typep(body, (MimeObjectClass*)&mimeMultipartClass) &&
+ body->options->decompose_file_close_fn)
+ body->options->decompose_file_close_fn(body->options->stream_closure);
+#endif /* MIME_DRAFTS */
+
+ /* Put out a separator after every multipart/signed object. */
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimemsig.h b/comm/mailnews/mime/src/mimemsig.h
new file mode 100644
index 0000000000..5581778f80
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemsig.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMSIG_H_
+#define _MIMEMSIG_H_
+
+#include "mimemult.h"
+#include "mimepbuf.h"
+#include "modmimee.h"
+
+/* The MimeMultipartSigned class implements the multipart/signed MIME
+ container, which provides a general method of associating a cryptographic
+ signature to an arbitrary MIME object.
+
+ The MimeMultipartSigned class provides the following methods:
+
+ void *crypto_init (MimeObject *multipart_object)
+
+ This is called with the object, the object->headers of which should be
+ used to initialize the dexlateion engine. NULL indicates failure;
+ otherwise, an opaque closure object should be returned.
+
+ int crypto_data_hash (const char *data, int32_t data_size,
+ void *crypto_closure)
+
+ This is called with the raw data, for which a signature has been computed.
+ The crypto module should examine this, and compute a signature for it.
+
+ int crypto_data_eof (void *crypto_closure, bool abort_p)
+
+ This is called when no more data remains. If `abort_p' is true, then the
+ crypto module may choose to discard any data rather than processing it,
+ as we're terminating abnormally.
+
+ int crypto_signature_init (void *crypto_closure,
+ MimeObject *multipart_object,
+ MimeHeaders *signature_hdrs)
+
+ This is called after crypto_data_eof() and just before the first call to
+ crypto_signature_hash(). The crypto module may wish to do some
+ initialization here, or may wish to examine the actual headers of the
+ signature object itself.
+
+ int crypto_signature_hash (const char *data, int32_t data_size,
+ void *crypto_closure)
+
+ This is called with the raw data of the detached signature block. It will
+ be called after crypto_data_eof() has been called to signify the end of
+ the data which is signed. This data is the data of the signature itself.
+
+ int crypto_signature_eof (void *crypto_closure, bool abort_p)
+
+ This is called when no more signature data remains. If `abort_p' is true,
+ then the crypto module may choose to discard any data rather than
+ processing it, as we're terminating abnormally.
+
+ char * crypto_generate_html (void *crypto_closure)
+
+ This is called after `crypto_signature_eof' but before `crypto_free'.
+ The crypto module should return a newly-allocated string of HTML code
+ which explains the status of the dexlateion to the user (whether the
+ signature checks out, etc.)
+
+ void crypto_free (void *crypto_closure)
+
+ This will be called when we're all done, after `crypto_signature_eof' and
+ `crypto_emit_html'. It is intended to free any data represented by the
+ crypto_closure.
+ */
+
+typedef struct MimeMultipartSignedClass MimeMultipartSignedClass;
+typedef struct MimeMultipartSigned MimeMultipartSigned;
+
+typedef enum {
+ MimeMultipartSignedPreamble,
+ MimeMultipartSignedBodyFirstHeader,
+ MimeMultipartSignedBodyHeaders,
+ MimeMultipartSignedBodyFirstLine,
+ MimeMultipartSignedBodyLine,
+ MimeMultipartSignedSignatureHeaders,
+ MimeMultipartSignedSignatureFirstLine,
+ MimeMultipartSignedSignatureLine,
+ MimeMultipartSignedEpilogue
+} MimeMultipartSignedParseState;
+
+struct MimeMultipartSignedClass {
+ MimeMultipartClass multipart;
+
+ /* Callbacks used by dexlateion (really, signature verification) module. */
+ void* (*crypto_init)(MimeObject* multipart_object);
+
+ int (*crypto_data_hash)(const char* data, int32_t data_size,
+ void* crypto_closure);
+ int (*crypto_signature_hash)(const char* data, int32_t data_size,
+ void* crypto_closure);
+
+ int (*crypto_data_eof)(void* crypto_closure, bool abort_p);
+ int (*crypto_signature_eof)(void* crypto_closure, bool abort_p);
+
+ int (*crypto_signature_init)(void* crypto_closure,
+ MimeObject* multipart_object,
+ MimeHeaders* signature_hdrs);
+
+ char* (*crypto_generate_html)(void* crypto_closure);
+
+ void (*crypto_notify_suppressed_child)(void* crypto_closure);
+
+ void (*crypto_free)(void* crypto_closure);
+};
+
+extern "C" MimeMultipartSignedClass mimeMultipartSignedClass;
+
+struct MimeMultipartSigned {
+ MimeMultipart multipart;
+ MimeMultipartSignedParseState state; /* State of parser */
+
+ void* crypto_closure; /* Opaque data used by signature
+ verification module. */
+
+ MimeHeaders* body_hdrs; /* The headers of the signed object. */
+ MimeHeaders* sig_hdrs; /* The headers of the signature. */
+
+ MimePartBufferData* part_buffer; /* The buffered body of the signed
+ object (see mimepbuf.h) */
+
+ MimeDecoderData* sig_decoder_data; /* The signature is probably base64
+ encoded; this is the decoder used
+ to get raw bits out of it. */
+};
+
+#define MimeMultipartSignedClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMSIG_H_ */
diff --git a/comm/mailnews/mime/src/mimemult.cpp b/comm/mailnews/mime/src/mimemult.cpp
new file mode 100644
index 0000000000..29caf5f5b0
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemult.cpp
@@ -0,0 +1,676 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "mimemult.h"
+#include "mimemoz2.h"
+#include "mimeeobj.h"
+
+#include "prlog.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prio.h"
+#include "nsMimeStringResources.h"
+#include "nsMimeTypes.h"
+#include <ctype.h>
+
+#ifdef XP_MACOSX
+extern MimeObjectClass mimeMultipartAppleDoubleClass;
+#endif
+
+#define MIME_SUPERCLASS mimeContainerClass
+MimeDefClass(MimeMultipart, MimeMultipartClass, mimeMultipartClass,
+ &MIME_SUPERCLASS);
+
+static int MimeMultipart_initialize(MimeObject*);
+static void MimeMultipart_finalize(MimeObject*);
+static int MimeMultipart_parse_line(const char* line, int32_t length,
+ MimeObject*);
+static int MimeMultipart_parse_eof(MimeObject* object, bool abort_p);
+
+static MimeMultipartBoundaryType MimeMultipart_check_boundary(MimeObject*,
+ const char*,
+ int32_t);
+static int MimeMultipart_create_child(MimeObject*);
+static bool MimeMultipart_output_child_p(MimeObject*, MimeObject*);
+static int MimeMultipart_parse_child_line(MimeObject*, const char*, int32_t,
+ bool);
+static int MimeMultipart_close_child(MimeObject*);
+
+extern "C" MimeObjectClass mimeMultipartAlternativeClass;
+extern "C" MimeObjectClass mimeMultipartRelatedClass;
+extern "C" MimeObjectClass mimeMultipartSignedClass;
+extern "C" MimeObjectClass mimeInlineTextVCardClass;
+extern "C" MimeExternalObjectClass mimeExternalObjectClass;
+extern "C" MimeSuppressedCryptoClass mimeSuppressedCryptoClass;
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeMultipart_debug_print(MimeObject*, PRFileDesc*, int32_t);
+#endif
+
+static int MimeMultipartClassInitialize(MimeMultipartClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeMultipart_initialize;
+ oclass->finalize = MimeMultipart_finalize;
+ oclass->parse_line = MimeMultipart_parse_line;
+ oclass->parse_eof = MimeMultipart_parse_eof;
+
+ mclass->check_boundary = MimeMultipart_check_boundary;
+ mclass->create_child = MimeMultipart_create_child;
+ mclass->output_child_p = MimeMultipart_output_child_p;
+ mclass->parse_child_line = MimeMultipart_parse_child_line;
+ mclass->close_child = MimeMultipart_close_child;
+
+#if defined(DEBUG) && defined(XP_UNIX)
+ oclass->debug_print = MimeMultipart_debug_print;
+#endif
+
+ return 0;
+}
+
+static int MimeMultipart_initialize(MimeObject* object) {
+ MimeMultipart* mult = (MimeMultipart*)object;
+ char* ct;
+
+ /* This is an abstract class; it shouldn't be directly instantiated. */
+ PR_ASSERT(object->clazz != (MimeObjectClass*)&mimeMultipartClass);
+
+ ct = MimeHeaders_get(object->headers, HEADER_CONTENT_TYPE, false, false);
+ mult->boundary =
+ (ct ? MimeHeaders_get_parameter(ct, HEADER_PARM_BOUNDARY, NULL, NULL)
+ : 0);
+ PR_FREEIF(ct);
+ mult->state = MimeMultipartPreamble;
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeMultipart_finalize(MimeObject* object) {
+ MimeMultipart* mult = (MimeMultipart*)object;
+
+ object->clazz->parse_eof(object, false);
+
+ PR_FREEIF(mult->boundary);
+ if (mult->hdrs) MimeHeaders_free(mult->hdrs);
+ mult->hdrs = 0;
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+int MimeWriteAString(MimeObject* obj, const nsACString& string) {
+ const nsCString& flatString = PromiseFlatCString(string);
+ return MimeObject_write(obj, flatString.get(), flatString.Length(), true);
+}
+
+static int MimeMultipart_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeContainer* container = (MimeContainer*)obj;
+ int status = 0;
+ MimeMultipartBoundaryType boundary;
+
+ NS_ASSERTION(line && *line, "empty line in multipart parse_line");
+ if (!line || !*line) return -1;
+
+ NS_ASSERTION(!obj->closed_p, "obj shouldn't already be closed");
+ if (obj->closed_p) return -1;
+
+ /* If we're supposed to write this object, but aren't supposed to convert
+ it to HTML, simply pass it through unaltered. */
+ if (obj->output_p && obj->options && !obj->options->write_html_p &&
+ obj->options->output_fn &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessageAttach)
+ return MimeObject_write(obj, line, length, true);
+
+ if (mult->state == MimeMultipartEpilogue) /* already done */
+ boundary = MimeMultipartBoundaryTypeNone;
+ else
+ boundary =
+ ((MimeMultipartClass*)obj->clazz)->check_boundary(obj, line, length);
+
+ if (boundary == MimeMultipartBoundaryTypeTerminator ||
+ boundary == MimeMultipartBoundaryTypeSeparator) {
+ /* Match! Close the currently-open part, move on to the next
+ state, and discard this line.
+ */
+ bool endOfPart = (mult->state != MimeMultipartPreamble);
+ if (endOfPart) status = ((MimeMultipartClass*)obj->clazz)->close_child(obj);
+ if (status < 0) return status;
+
+ if (boundary == MimeMultipartBoundaryTypeTerminator)
+ mult->state = MimeMultipartEpilogue;
+ else {
+ mult->state = MimeMultipartHeaders;
+
+ /* Reset the header parser for this upcoming part. */
+ NS_ASSERTION(!mult->hdrs, "mult->hdrs should be null here");
+ if (mult->hdrs) MimeHeaders_free(mult->hdrs);
+ mult->hdrs = MimeHeaders_new();
+ if (!mult->hdrs) return MIME_OUT_OF_MEMORY;
+ if (obj->options && obj->options->state &&
+ obj->options->state->partsToStrip.Length() > 0) {
+ nsAutoCString newPart(mime_part_address(obj));
+ newPart.Append('.');
+ newPart.AppendInt(container->nchildren + 1);
+ obj->options->state->strippingPart = false;
+ // check if this is a sub-part of a part we're stripping.
+ for (uint32_t partIndex = 0;
+ partIndex < obj->options->state->partsToStrip.Length();
+ partIndex++) {
+ nsCString& curPartToStrip =
+ obj->options->state->partsToStrip[partIndex];
+ if (newPart.Find(curPartToStrip) == 0 &&
+ (newPart.Length() == curPartToStrip.Length() ||
+ newPart.CharAt(curPartToStrip.Length()) == '.')) {
+ obj->options->state->strippingPart = true;
+ if (partIndex < obj->options->state->detachToFiles.Length())
+ obj->options->state->detachedFilePath =
+ obj->options->state->detachToFiles[partIndex];
+ break;
+ }
+ }
+ }
+ }
+
+ // if stripping out attachments, write the boundary line. Otherwise, return
+ // to ignore it.
+ if (obj->options &&
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach) {
+ // Because MimeMultipart_parse_child_line strips out the
+ // the CRLF of the last line before the end of a part, we need to add that
+ // back in here.
+ if (endOfPart) MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK));
+
+ status = MimeObject_write(obj, line, length, true);
+ }
+ return 0;
+ }
+
+ /* Otherwise, this isn't a boundary string. So do whatever it is we
+ should do with this line (parse it as a header, feed it to the
+ child part, ignore it, etc.) */
+
+ switch (mult->state) {
+ case MimeMultipartPreamble:
+ case MimeMultipartEpilogue:
+ /* Ignore this line. */
+ break;
+
+ case MimeMultipartHeaders:
+ /* Parse this line as a header for the sub-part. */
+ {
+ status = MimeHeaders_parse_line(line, length, mult->hdrs);
+ bool stripping = false;
+
+ if (status < 0) return status;
+
+ // If this line is blank, we're now done parsing headers, and should
+ // now examine the content-type to create this "body" part.
+ //
+ if (*line == '\r' || *line == '\n') {
+ if (obj->options && obj->options->state &&
+ obj->options->state->strippingPart) {
+ stripping = true;
+ bool detachingPart =
+ obj->options->state->detachedFilePath.Length() > 0;
+
+ nsAutoCString fileName;
+ fileName.Adopt(MimeHeaders_get_name(mult->hdrs, obj->options));
+ // clang-format off
+ if (detachingPart) {
+ char *contentType =
+ MimeHeaders_get(mult->hdrs, "Content-Type", false, false);
+ if (contentType) {
+ MimeWriteAString(obj, "Content-Type: "_ns);
+ MimeWriteAString(obj, nsDependentCString(contentType));
+ PR_Free(contentType);
+ }
+ MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK));
+ MimeWriteAString(obj, "Content-Disposition: attachment; filename=\""_ns);
+ MimeWriteAString(obj, fileName);
+ MimeWriteAString(obj, "\""_ns MSG_LINEBREAK);
+ MimeWriteAString(obj, "X-Mozilla-External-Attachment-URL: "_ns);
+ MimeWriteAString(obj, obj->options->state->detachedFilePath);
+ MimeWriteAString(obj, nsLiteralCString(MSG_LINEBREAK));
+ MimeWriteAString(obj, "X-Mozilla-Altered: AttachmentDetached; date=\""_ns);
+ } else {
+ nsAutoCString header("Content-Type: text/x-moz-deleted; name=\"Deleted: ");
+ header.Append(fileName);
+ MimeWriteAString(obj, header);
+ MimeWriteAString(obj, "\""_ns MSG_LINEBREAK
+ "Content-Transfer-Encoding: 8bit"_ns MSG_LINEBREAK);
+ MimeWriteAString(obj, "Content-Disposition: inline; filename=\"Deleted: "_ns);
+ MimeWriteAString(obj, fileName);
+ MimeWriteAString(obj, "\""_ns MSG_LINEBREAK
+ "X-Mozilla-Altered: AttachmentDeleted; date=\""_ns);
+ }
+ nsCString result;
+ char timeBuffer[128];
+ PRExplodedTime now;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);
+ PR_FormatTimeUSEnglish(timeBuffer, sizeof(timeBuffer),
+ "%a %b %d %H:%M:%S %Y", &now);
+ MimeWriteAString(obj, nsDependentCString(timeBuffer));
+ MimeWriteAString(obj, "\""_ns MSG_LINEBREAK);
+ MimeWriteAString(obj, MSG_LINEBREAK
+ "You deleted an attachment from this message. The original "_ns
+ "MIME headers for the attachment were:"_ns MSG_LINEBREAK);
+ MimeHeaders_write_raw_headers(mult->hdrs, obj->options, false);
+ // clang-format on
+ }
+ int32_t old_nchildren = container->nchildren;
+ status = ((MimeMultipartClass*)obj->clazz)->create_child(obj);
+ if (status < 0) return status;
+ NS_ASSERTION(mult->state != MimeMultipartHeaders,
+ "mult->state shouldn't be MimeMultipartHeaders");
+
+ if (!stripping && container->nchildren > old_nchildren &&
+ obj->options &&
+ !mime_typep(obj,
+ (MimeObjectClass*)&mimeMultipartAlternativeClass)) {
+ // Notify emitter about content type and part path.
+ MimeObject* kid = container->children[container->nchildren - 1];
+ MimeMultipart_notify_emitter(kid);
+ }
+ }
+ break;
+ }
+
+ case MimeMultipartPartFirstLine:
+ /* Hand this line off to the sub-part. */
+ status = (((MimeMultipartClass*)obj->clazz)
+ ->parse_child_line(obj, line, length, true));
+ if (status < 0) return status;
+ mult->state = MimeMultipartPartLine;
+ break;
+
+ case MimeMultipartPartLine:
+ /* Hand this line off to the sub-part. */
+ status = (((MimeMultipartClass*)obj->clazz)
+ ->parse_child_line(obj, line, length, false));
+ if (status < 0) return status;
+ break;
+
+ default:
+ NS_ERROR("unexpected state in parse line");
+ return -1;
+ }
+
+ if (obj->options &&
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach &&
+ (!(obj->options->state && obj->options->state->strippingPart) &&
+ mult->state != MimeMultipartPartLine))
+ return MimeObject_write(obj, line, length, false);
+ return 0;
+}
+
+void MimeMultipart_notify_emitter(MimeObject* obj) {
+ char* ct = nullptr;
+
+ NS_ASSERTION(obj->options,
+ "MimeMultipart_notify_emitter called with null options");
+ if (!obj->options) return;
+
+ ct = MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ if (obj->options->notify_nested_bodies) {
+ mimeEmitterAddHeaderField(obj->options, HEADER_CONTENT_TYPE,
+ ct ? ct : TEXT_PLAIN);
+ char* part_path = mime_part_address(obj);
+ if (part_path) {
+ mimeEmitterAddHeaderField(obj->options, "x-jsemitter-part-path",
+ part_path);
+ PR_Free(part_path);
+ }
+ }
+
+ // Examine the headers and see if there is a special charset
+ // (i.e. non US-ASCII) for this message. If so, we need to
+ // tell the emitter that this is the case for use in any
+ // possible reply or forward operation.
+ if (ct &&
+ (obj->options->notify_nested_bodies || MimeObjectIsMessageBody(obj))) {
+ char* cset = MimeHeaders_get_parameter(ct, "charset", NULL, NULL);
+ if (cset) {
+ mimeEmitterUpdateCharacterSet(obj->options, cset);
+ if (!obj->options->override_charset)
+ // Also set this charset to msgWindow
+ SetMailCharacterSetToMsgWindow(obj, cset);
+ PR_Free(cset);
+ }
+ }
+
+ PR_FREEIF(ct);
+}
+
+static MimeMultipartBoundaryType MimeMultipart_check_boundary(MimeObject* obj,
+ const char* line,
+ int32_t length) {
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ int32_t blen;
+ bool term_p;
+
+ if (!mult->boundary || line[0] != '-' || line[1] != '-')
+ return MimeMultipartBoundaryTypeNone;
+
+ /* This is a candidate line to be a boundary. Check it out... */
+ blen = strlen(mult->boundary);
+ term_p = false;
+
+ /* strip trailing whitespace (including the newline.) */
+ while (length > 2 && IS_SPACE(line[length - 1])) length--;
+
+ /* Could this be a terminating boundary? */
+ if (length == blen + 4 && line[length - 1] == '-' &&
+ line[length - 2] == '-') {
+ term_p = true;
+ }
+
+ // looks like we have a separator but first, we need to check it's not for one
+ // of the part's children.
+ MimeContainer* cont = (MimeContainer*)obj;
+ if (cont->nchildren > 0) {
+ MimeObject* kid = cont->children[cont->nchildren - 1];
+ if (kid)
+ if (mime_typep(kid, (MimeObjectClass*)&mimeMultipartClass)) {
+ // Don't ask the kid to check the boundary if it has already detected a
+ // Teminator
+ MimeMultipart* mult = (MimeMultipart*)kid;
+ if (mult->state != MimeMultipartEpilogue)
+ if (MimeMultipart_check_boundary(kid, line, length) !=
+ MimeMultipartBoundaryTypeNone)
+ return MimeMultipartBoundaryTypeNone;
+ }
+ }
+
+ if (term_p) length -= 2;
+
+ if (blen == length - 2 && !strncmp(line + 2, mult->boundary, length - 2))
+ return (term_p ? MimeMultipartBoundaryTypeTerminator
+ : MimeMultipartBoundaryTypeSeparator);
+ else
+ return MimeMultipartBoundaryTypeNone;
+}
+
+static int MimeMultipart_create_child(MimeObject* obj) {
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ int status;
+ char* ct = (mult->hdrs ? MimeHeaders_get(mult->hdrs, HEADER_CONTENT_TYPE,
+ true, false)
+ : 0);
+ const char* dct = (((MimeMultipartClass*)obj->clazz)->default_part_type);
+ MimeObject* body = NULL;
+
+ mult->state = MimeMultipartPartFirstLine;
+ if (obj->options) obj->options->is_child = true;
+
+ /* Don't pass in NULL as the content-type (this means that the
+ auto-uudecode-hack won't ever be done for subparts of a
+ multipart, but only for untyped children of message/rfc822.
+ */
+ body = mime_create(((ct && *ct) ? ct : (dct ? dct : TEXT_PLAIN)), mult->hdrs,
+ obj->options);
+ PR_FREEIF(ct);
+ if (!body) return MIME_OUT_OF_MEMORY;
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, body);
+ if (status < 0) {
+ mime_free(body);
+ return status;
+ }
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->is_multipart_msg && obj->options->decompose_file_init_fn) {
+ if (!mime_typep(obj, (MimeObjectClass*)&mimeMultipartRelatedClass) &&
+ !mime_typep(obj, (MimeObjectClass*)&mimeMultipartAlternativeClass) &&
+ !mime_typep(obj, (MimeObjectClass*)&mimeMultipartSignedClass) &&
+ /* bug 21869 -- due to the fact that we are not generating the
+ correct mime class object for content-typ multipart/signed part
+ the above check failed. to solve the problem in general and not
+ to cause early termination when parsing message for opening as
+ draft we can simply make sure that the child is not a multipart
+ mime object. this way we could have a proper decomposing message
+ part functions set correctly */
+ !mime_typep(body, (MimeObjectClass*)&mimeMultipartClass) &&
+ !((mime_typep(body, (MimeObjectClass*)&mimeExternalObjectClass) ||
+ mime_typep(body, (MimeObjectClass*)&mimeSuppressedCryptoClass)) &&
+ (!strcmp(body->content_type, "text/vcard") ||
+ !strcmp(body->content_type, "text/x-vcard")))) {
+ status = obj->options->decompose_file_init_fn(
+ obj->options->stream_closure, mult->hdrs);
+ if (status < 0) return status;
+ }
+ }
+#endif /* MIME_DRAFTS */
+
+ /* Now that we've added this new object to our list of children,
+ start its parser going (if we want to display it.)
+ */
+ body->output_p =
+ (((MimeMultipartClass*)obj->clazz)->output_child_p(obj, body));
+ if (body->output_p) {
+ status = body->clazz->parse_begin(body);
+
+#ifdef XP_MACOSX
+ /* if we are saving an apple double attachment, we need to set correctly the
+ * content type of the channel */
+ if (mime_typep(obj, (MimeObjectClass*)&mimeMultipartAppleDoubleClass)) {
+ mime_stream_data* msd = (mime_stream_data*)body->options->stream_closure;
+ if (!body->options->write_html_p && body->content_type &&
+ !PL_strcasecmp(body->content_type, APPLICATION_APPLEFILE)) {
+ if (msd && msd->channel)
+ msd->channel->SetContentType(nsLiteralCString(APPLICATION_APPLEFILE));
+ }
+ }
+#endif
+
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static bool MimeMultipart_output_child_p(MimeObject* obj, MimeObject* child) {
+ /* We don't output a child if we're stripping it. */
+ if (obj->options && obj->options->state && obj->options->state->strippingPart)
+ return false;
+ /* if we are saving an apple double attachment, ignore the appledouble wrapper
+ * part */
+ return (obj->options && obj->options->write_html_p) ||
+ PL_strcasecmp(child->content_type, MULTIPART_APPLEDOUBLE);
+}
+
+static int MimeMultipart_close_child(MimeObject* object) {
+ MimeMultipart* mult = (MimeMultipart*)object;
+ MimeContainer* cont = (MimeContainer*)object;
+
+ if (!mult->hdrs) return 0;
+
+ MimeHeaders_free(mult->hdrs);
+ mult->hdrs = 0;
+
+ NS_ASSERTION(cont->nchildren > 0, "badly formed mime message");
+ if (cont->nchildren > 0) {
+ MimeObject* kid = cont->children[cont->nchildren - 1];
+ // If we have a child and it has not already been closed, process it.
+ // The kid would be already be closed if we encounter a multipart section
+ // that did not have a fully delineated header block. No header block means
+ // no creation of a new child, but the termination case still happens and
+ // we still end up here. Obviously, we don't want to close the child a
+ // second time and the best thing we can do is nothing.
+ if (kid && !kid->closed_p) {
+ int status;
+ status = kid->clazz->parse_eof(kid, false);
+ if (status < 0) return status;
+ status = kid->clazz->parse_end(kid, false);
+ if (status < 0) return status;
+
+#ifdef MIME_DRAFTS
+ if (object->options && object->options->decompose_file_p &&
+ object->options->is_multipart_msg &&
+ object->options->decompose_file_close_fn) {
+ // clang-format off
+ if (!mime_typep(object, (MimeObjectClass *)&mimeMultipartRelatedClass) &&
+ !mime_typep(object, (MimeObjectClass *)&mimeMultipartAlternativeClass) &&
+ !mime_typep(object, (MimeObjectClass *)&mimeMultipartSignedClass) &&
+ /* bug 21869 -- due to the fact that we are not generating the
+ correct mime class object for content-typ multipart/signed part
+ the above check failed. to solve the problem in general and not
+ to cause early termination when parsing message for opening as
+ draft we can simply make sure that the child is not a multipart
+ mime object. this way we could have a proper decomposing message
+ part functions set correctly */
+ !mime_typep(kid, (MimeObjectClass *)&mimeMultipartClass) &&
+ !((mime_typep(kid, (MimeObjectClass *)&mimeExternalObjectClass) ||
+ mime_typep(kid, (MimeObjectClass *)&mimeSuppressedCryptoClass)) &&
+ (!strcmp(kid->content_type, "text/vcard") ||
+ !strcmp(kid->content_type, "text/x-vcard")))) {
+ status = object->options->decompose_file_close_fn(
+ object->options->stream_closure);
+ if (status < 0) return status;
+ }
+ // clang-format on
+ }
+#endif /* MIME_DRAFTS */
+ }
+ }
+ return 0;
+}
+
+static int MimeMultipart_parse_child_line(MimeObject* obj, const char* line,
+ int32_t length, bool first_line_p) {
+ MimeContainer* cont = (MimeContainer*)obj;
+ int status;
+ MimeObject* kid;
+
+ PR_ASSERT(cont->nchildren > 0);
+ if (cont->nchildren <= 0) return -1;
+
+ kid = cont->children[cont->nchildren - 1];
+ PR_ASSERT(kid);
+ if (!kid) return -1;
+
+#ifdef MIME_DRAFTS
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->is_multipart_msg &&
+ obj->options->decompose_file_output_fn) {
+ if (!mime_typep(obj, (MimeObjectClass*)&mimeMultipartAlternativeClass) &&
+ !mime_typep(obj, (MimeObjectClass*)&mimeMultipartRelatedClass) &&
+ !mime_typep(obj, (MimeObjectClass*)&mimeMultipartSignedClass) &&
+ /* bug 21869 -- due to the fact that we are not generating the
+ correct mime class object for content-typ multipart/signed part
+ the above check failed. to solve the problem in general and not
+ to cause early termination when parsing message for opening as
+ draft we can simply make sure that the child is not a multipart
+ mime object. this way we could have a proper decomposing message
+ part functions set correctly */
+ !mime_typep(kid, (MimeObjectClass*)&mimeMultipartClass) &&
+ !((mime_typep(kid, (MimeObjectClass*)&mimeExternalObjectClass) ||
+ mime_typep(kid, (MimeObjectClass*)&mimeSuppressedCryptoClass)) &&
+ (!strcmp(kid->content_type, "text/vcard") ||
+ !strcmp(kid->content_type, "text/x-vcard"))))
+ return obj->options->decompose_file_output_fn(
+ line, length, obj->options->stream_closure);
+ }
+#endif /* MIME_DRAFTS */
+
+ /* The newline issues here are tricky, since both the newlines before
+ and after the boundary string are to be considered part of the
+ boundary: this is so that a part can be specified such that it
+ does not end in a trailing newline.
+
+ To implement this, we send a newline *before* each line instead
+ of after, except for the first line, which is not preceded by a
+ newline.
+ */
+
+ /* Remove the trailing newline... */
+ if (length > 0 && line[length - 1] == '\n') length--;
+ if (length > 0 && line[length - 1] == '\r') length--;
+
+ if (!first_line_p) {
+ /* Push out a preceding newline... */
+ char nl[] = MSG_LINEBREAK;
+ status = kid->clazz->parse_buffer(nl, MSG_LINEBREAK_LEN, kid);
+ if (status < 0) return status;
+ }
+
+ /* Now push out the line sans trailing newline. */
+ return kid->clazz->parse_buffer(line, length, kid);
+}
+
+static int MimeMultipart_parse_eof(MimeObject* obj, bool abort_p) {
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ MimeContainer* cont = (MimeContainer*)obj;
+
+ if (obj->closed_p) return 0;
+
+ /* Push out the last trailing line if there's one in the buffer. If
+ this happens, this object does not end in a trailing newline (and
+ the parse_line method will be called with a string with no trailing
+ newline, which isn't the usual case.)
+ */
+ if (!abort_p && obj->ibuffer_fp > 0) {
+ /* There is leftover data without a terminating newline. */
+ int status = obj->clazz->parse_line(obj->ibuffer, obj->ibuffer_fp, obj);
+ obj->ibuffer_fp = 0;
+ if (status < 0) {
+ obj->closed_p = true;
+ return status;
+ }
+ }
+
+ /* Now call parse_eof for our active child, if there is one.
+ */
+ if (cont->nchildren > 0 && (mult->state == MimeMultipartPartLine ||
+ mult->state == MimeMultipartPartFirstLine)) {
+ MimeObject* kid = cont->children[cont->nchildren - 1];
+ NS_ASSERTION(kid, "not expecting null kid");
+ if (kid) {
+ int status = kid->clazz->parse_eof(kid, abort_p);
+ if (status < 0) return status;
+ }
+ }
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+}
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeMultipart_debug_print(MimeObject* obj, PRFileDesc* stream,
+ int32_t depth) {
+ /* MimeMultipart *mult = (MimeMultipart *) obj; */
+ MimeContainer* cont = (MimeContainer*)obj;
+ char* addr = mime_part_address(obj);
+ int i;
+ for (i = 0; i < depth; i++) PR_Write(stream, " ", 2);
+ /**
+ fprintf(stream, "<%s %s (%d kid%s) boundary=%s 0x%08X>\n",
+ obj->clazz->class_name,
+ addr ? addr : "???",
+ cont->nchildren, (cont->nchildren == 1 ? "" : "s"),
+ (mult->boundary ? mult->boundary : "(none)"),
+ (uint32_t) mult);
+ **/
+ PR_FREEIF(addr);
+
+ /*
+ if (cont->nchildren > 0)
+ fprintf(stream, "\n");
+ */
+
+ for (i = 0; i < cont->nchildren; i++) {
+ MimeObject* kid = cont->children[i];
+ int status = kid->clazz->debug_print(kid, stream, depth + 1);
+ if (status < 0) return status;
+ }
+
+ /*
+ if (cont->nchildren > 0)
+ fprintf(stream, "\n");
+ */
+
+ return 0;
+}
+#endif
diff --git a/comm/mailnews/mime/src/mimemult.h b/comm/mailnews/mime/src/mimemult.h
new file mode 100644
index 0000000000..9afa74885a
--- /dev/null
+++ b/comm/mailnews/mime/src/mimemult.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEMULT_H_
+#define _MIMEMULT_H_
+
+#include "mimecont.h"
+
+/* The MimeMultipart class class implements the objects representing all of
+ the "multipart/" MIME types. In addition to the methods inherited from
+ MimeContainer, it provides the following methods and class variables:
+
+ int create_child (MimeObject *obj)
+
+ When it has been determined that a new sub-part should be created,
+ this method is called to do that. The default value for this method
+ does it in the usual multipart/mixed way. The headers of the object-
+ to-be-created may be found in the `hdrs' slot of the `MimeMultipart'
+ object.
+
+ bool output_child_p (MimeObject *parent, MimeObject *child)
+
+ Whether this child should be output. Default method always says `yes'.
+
+ int parse_child_line (MimeObject *obj, const char *line, int32_t length,
+ bool first_line_p)
+
+ When we have a line which should be handed off to the currently-active
+ child object, this method is called to do that. The `first_line_p'
+ variable will be true only for the very first line handed off to this
+ sub-part. The default method simply passes the line to the most-
+ recently-added child object.
+
+ int close_child (MimeObject *self)
+
+ When we reach the end of a sub-part (a separator line) this method is
+ called to shut down the currently-active child. The default method
+ simply calls `parse_eof' on the most-recently-added child object.
+
+ MimeMultipartBoundaryType check_boundary (MimeObject *obj,
+ const char *line, int32_t length)
+
+ This method is used to examine a line and determine whether it is a
+ part boundary, and if so, what kind. It should return a member of
+ the MimeMultipartBoundaryType describing the line.
+
+ const char *default_part_type
+
+ This is the type which should be assumed for sub-parts which have
+ no explicit type specified. The default is "text/plain", but the
+ "multipart/digest" subclass overrides this to "message/rfc822".
+ */
+
+typedef struct MimeMultipartClass MimeMultipartClass;
+typedef struct MimeMultipart MimeMultipart;
+
+typedef enum {
+ MimeMultipartPreamble,
+ MimeMultipartHeaders,
+ MimeMultipartPartFirstLine,
+ MimeMultipartPartLine,
+ MimeMultipartEpilogue
+} MimeMultipartParseState;
+
+typedef enum {
+ MimeMultipartBoundaryTypeNone,
+ MimeMultipartBoundaryTypeSeparator,
+ MimeMultipartBoundaryTypeTerminator
+} MimeMultipartBoundaryType;
+
+struct MimeMultipartClass {
+ MimeContainerClass container;
+ const char* default_part_type;
+
+ int (*create_child)(MimeObject*);
+ bool (*output_child_p)(MimeObject* self, MimeObject* child);
+ int (*close_child)(MimeObject*);
+ int (*parse_child_line)(MimeObject*, const char* line, int32_t length,
+ bool first_line_p);
+ MimeMultipartBoundaryType (*check_boundary)(MimeObject*, const char* line,
+ int32_t length);
+};
+
+extern MimeMultipartClass mimeMultipartClass;
+
+struct MimeMultipart {
+ MimeContainer container; /* superclass variables */
+ char* boundary; /* Inter-part delimiter string */
+ MimeHeaders* hdrs; /* headers of the part currently
+ being parsed, if any */
+ MimeMultipartParseState state; /* State of parser */
+};
+
+extern void MimeMultipart_notify_emitter(MimeObject*);
+
+#define MimeMultipartClassInitializer(ITYPE, CSUPER) \
+ { MimeContainerClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEMULT_H_ */
diff --git a/comm/mailnews/mime/src/mimeobj.cpp b/comm/mailnews/mime/src/mimeobj.cpp
new file mode 100644
index 0000000000..7ad7e290d1
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeobj.cpp
@@ -0,0 +1,301 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+#include "mimeobj.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prio.h"
+#include "mimebuf.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+#include "nsMimeStringResources.h"
+#include "nsMsgUtils.h"
+#include "mimemsg.h"
+#include "mimemapl.h"
+
+/* Way to destroy any notions of modularity or class hierarchy, Terry! */
+#include "mimetpla.h"
+#include "mimethtm.h"
+#include "mimecont.h"
+
+MimeDefClass(MimeObject, MimeObjectClass, mimeObjectClass, NULL);
+
+static int MimeObject_initialize(MimeObject*);
+static void MimeObject_finalize(MimeObject*);
+static int MimeObject_parse_begin(MimeObject*);
+static int MimeObject_parse_buffer(const char*, int32_t, MimeObject*);
+static int MimeObject_parse_line(const char*, int32_t, MimeObject*);
+static int MimeObject_parse_eof(MimeObject*, bool);
+static int MimeObject_parse_end(MimeObject*, bool);
+static bool MimeObject_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs);
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeObject_debug_print(MimeObject*, PRFileDesc*, int32_t depth);
+#endif
+
+static int MimeObjectClassInitialize(MimeObjectClass* clazz) {
+ NS_ASSERTION(!clazz->class_initialized,
+ "class shouldn't already be initialized");
+ clazz->initialize = MimeObject_initialize;
+ clazz->finalize = MimeObject_finalize;
+ clazz->parse_begin = MimeObject_parse_begin;
+ clazz->parse_buffer = MimeObject_parse_buffer;
+ clazz->parse_line = MimeObject_parse_line;
+ clazz->parse_eof = MimeObject_parse_eof;
+ clazz->parse_end = MimeObject_parse_end;
+ clazz->displayable_inline_p = MimeObject_displayable_inline_p;
+
+#if defined(DEBUG) && defined(XP_UNIX)
+ clazz->debug_print = MimeObject_debug_print;
+#endif
+ return 0;
+}
+
+static int MimeObject_initialize(MimeObject* obj) {
+ /* This is an abstract class; it shouldn't be directly instantiated. */
+ NS_ASSERTION(obj->clazz != &mimeObjectClass,
+ "should directly instantiate abstract class");
+
+ /* Set up the content-type and encoding. */
+ if (!obj->content_type && obj->headers)
+ obj->content_type =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, true, false);
+ if (!obj->encoding && obj->headers)
+ obj->encoding = MimeHeaders_get(
+ obj->headers, HEADER_CONTENT_TRANSFER_ENCODING, true, false);
+
+ /* Special case to normalize some types and encodings to a canonical form.
+ (These are nonstandard types/encodings which have been seen to appear in
+ multiple forms; we normalize them so that things like looking up icons
+ and extensions has consistent behavior for the receiver, regardless of
+ the "alias" type that the sender used.)
+ */
+ if (!obj->content_type || !*(obj->content_type))
+ ;
+ else if (!PL_strcasecmp(obj->content_type, APPLICATION_UUENCODE2) ||
+ !PL_strcasecmp(obj->content_type, APPLICATION_UUENCODE3) ||
+ !PL_strcasecmp(obj->content_type, APPLICATION_UUENCODE4)) {
+ PR_Free(obj->content_type);
+ obj->content_type = strdup(APPLICATION_UUENCODE);
+ } else if (!PL_strcasecmp(obj->content_type, IMAGE_XBM2) ||
+ !PL_strcasecmp(obj->content_type, IMAGE_XBM3)) {
+ PR_Free(obj->content_type);
+ obj->content_type = strdup(IMAGE_XBM);
+ } else {
+ // MIME-types are case-insenitive, but let's make it lower case internally
+ // to avoid some hassle later down the road.
+ nsAutoCString lowerCaseContentType;
+ ToLowerCase(nsDependentCString(obj->content_type), lowerCaseContentType);
+ PR_Free(obj->content_type);
+ obj->content_type = ToNewCString(lowerCaseContentType);
+ }
+
+ if (!obj->encoding)
+ ;
+ else if (!PL_strcasecmp(obj->encoding, ENCODING_UUENCODE2) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE3) ||
+ !PL_strcasecmp(obj->encoding, ENCODING_UUENCODE4)) {
+ PR_Free(obj->encoding);
+ obj->encoding = strdup(ENCODING_UUENCODE);
+ } else if (!PL_strcasecmp(obj->encoding, ENCODING_COMPRESS2)) {
+ PR_Free(obj->encoding);
+ obj->encoding = strdup(ENCODING_COMPRESS);
+ } else if (!PL_strcasecmp(obj->encoding, ENCODING_GZIP2)) {
+ PR_Free(obj->encoding);
+ obj->encoding = strdup(ENCODING_GZIP);
+ }
+
+ return 0;
+}
+
+static void MimeObject_finalize(MimeObject* obj) {
+ obj->clazz->parse_eof(obj, false);
+ obj->clazz->parse_end(obj, false);
+
+ if (obj->headers) {
+ MimeHeaders_free(obj->headers);
+ obj->headers = 0;
+ }
+
+ /* Should have been freed by parse_eof, but just in case... */
+ NS_ASSERTION(!obj->ibuffer, "buffer not freed");
+ NS_ASSERTION(!obj->obuffer, "buffer not freed");
+ PR_FREEIF(obj->ibuffer);
+ PR_FREEIF(obj->obuffer);
+
+ PR_FREEIF(obj->content_type);
+ PR_FREEIF(obj->encoding);
+
+ if (obj->options && obj->options->state) {
+ delete obj->options->state;
+ obj->options->state = nullptr;
+ }
+}
+
+static int MimeObject_parse_begin(MimeObject* obj) {
+ NS_ASSERTION(!obj->closed_p, "object shouldn't be already closed");
+
+ /* If we haven't set up the state object yet, then this should be
+ the outermost object... */
+ if (obj->options && !obj->options->state) {
+ NS_ASSERTION(
+ !obj->headers,
+ "headers should be null"); /* should be the outermost object. */
+
+ obj->options->state = new MimeParseStateObject;
+ if (!obj->options->state) return MIME_OUT_OF_MEMORY;
+ obj->options->state->root = obj;
+ obj->options->state->separator_suppressed_p = true; /* no first sep */
+ const char* delParts = PL_strcasestr(obj->options->url, "&del=");
+ const char* detachLocations =
+ PL_strcasestr(obj->options->url, "&detachTo=");
+ if (delParts) {
+ const char* delEnd = PL_strcasestr(delParts + 1, "&");
+ if (!delEnd) delEnd = delParts + strlen(delParts);
+ ParseString(Substring(delParts + 5, delEnd), ',',
+ obj->options->state->partsToStrip);
+ }
+ if (detachLocations) {
+ detachLocations += 10; // advance past "&detachTo="
+ ParseString(nsDependentCString(detachLocations), ',',
+ obj->options->state->detachToFiles);
+ }
+ }
+
+ /* Decide whether this object should be output or not... */
+ if (!obj->options || obj->options->no_output_p ||
+ !obj->options->output_fn
+ /* if we are decomposing the message in files and processing a multipart
+ object, we must not output it without parsing it first */
+ || (obj->options->decompose_file_p &&
+ obj->options->decompose_file_output_fn &&
+ mime_typep(obj, (MimeObjectClass*)&mimeMultipartClass)))
+ obj->output_p = false;
+ else if (!obj->options->part_to_load)
+ obj->output_p = true;
+ else {
+ char* id = mime_part_address(obj);
+ if (!id) return MIME_OUT_OF_MEMORY;
+
+ // We need to check if a part is the subpart of the part to load.
+ // If so and this is a raw or body display output operation, then
+ // we should mark the part for subsequent output.
+
+ // First, check for an exact match
+ obj->output_p = !strcmp(id, obj->options->part_to_load);
+ if (!obj->output_p &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageRaw ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageBodyDisplay ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach)) {
+ // Then, check for subpart
+ unsigned int partlen = strlen(obj->options->part_to_load);
+ obj->output_p = (strlen(id) >= partlen + 2) && (id[partlen] == '.') &&
+ !strncmp(id, obj->options->part_to_load, partlen);
+ }
+
+ PR_Free(id);
+ }
+
+ // If we've decided not to output this part, we also shouldn't be showing it
+ // as an attachment.
+ obj->dontShowAsAttachment = !obj->output_p;
+
+ return 0;
+}
+
+static int MimeObject_parse_buffer(const char* buffer, int32_t size,
+ MimeObject* obj) {
+ NS_ASSERTION(!obj->closed_p, "object shouldn't be closed");
+ if (obj->closed_p) return -1;
+
+ return mime_LineBuffer(buffer, size, &obj->ibuffer, &obj->ibuffer_size,
+ &obj->ibuffer_fp, true,
+ ((int (*)(char*, int32_t, void*))
+ /* This cast is to turn void into MimeObject */
+ obj->clazz->parse_line),
+ obj);
+}
+
+static int MimeObject_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ NS_ERROR("shouldn't call this method");
+ return -1;
+}
+
+static int MimeObject_parse_eof(MimeObject* obj, bool abort_p) {
+ if (abort_p) {
+ obj->closed_p = true;
+ return 0;
+ }
+ if (obj->closed_p) return 0;
+ NS_ASSERTION(!obj->parsed_p, "obj already parsed");
+
+ /* If there is still data in the ibuffer, that means that the last line of
+ this part didn't end in a newline; so push it out anyway (this means that
+ the parse_line method will be called with a string with no trailing
+ newline, which isn't the usual case.)
+ */
+ if (obj->ibuffer_fp > 0) {
+ int status = obj->clazz->parse_line(obj->ibuffer, obj->ibuffer_fp, obj);
+ obj->ibuffer_fp = 0;
+ if (status < 0) {
+ obj->closed_p = true;
+ return status;
+ }
+ }
+
+ obj->closed_p = true;
+ return 0;
+}
+
+static int MimeObject_parse_end(MimeObject* obj, bool abort_p) {
+ if (obj->parsed_p) {
+ NS_ASSERTION(obj->closed_p, "object should be closed");
+ return 0;
+ }
+
+ /* We won't be needing these buffers any more; nuke 'em. */
+ PR_FREEIF(obj->ibuffer);
+ obj->ibuffer_fp = 0;
+ obj->ibuffer_size = 0;
+ PR_FREEIF(obj->obuffer);
+ obj->obuffer_fp = 0;
+ obj->obuffer_size = 0;
+
+ obj->parsed_p = true;
+ return 0;
+}
+
+static bool MimeObject_displayable_inline_p(MimeObjectClass* clazz,
+ MimeHeaders* hdrs) {
+ NS_ERROR("shouldn't call this method");
+ return false;
+}
+
+#if defined(DEBUG) && defined(XP_UNIX)
+static int MimeObject_debug_print(MimeObject* obj, PRFileDesc* stream,
+ int32_t depth) {
+ int i;
+ char* addr = mime_part_address(obj);
+ for (i = 0; i < depth; i++) PR_Write(stream, " ", 2);
+ /*
+ fprintf(stream, "<%s %s 0x%08X>\n", obj->clazz->class_name,
+ addr ? addr : "???",
+ (uint32_t) obj);
+ */
+ PR_FREEIF(addr);
+ return 0;
+}
+#endif
diff --git a/comm/mailnews/mime/src/mimeobj.h b/comm/mailnews/mime/src/mimeobj.h
new file mode 100644
index 0000000000..9a74481c65
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeobj.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEOBJ_H_
+#define _MIMEOBJ_H_
+
+#include "mimei.h"
+#include "prio.h"
+/* MimeObject is the base-class for the objects representing all other
+ MIME types. It provides several methods:
+
+ int initialize (MimeObject *obj)
+
+ This is called from mime_new() when a new instance is allocated.
+ Subclasses should do whatever setup is necessary from this method,
+ and should call the superclass's initialize method, unless there's
+ a specific reason not to.
+
+ void finalize (MimeObject *obj)
+
+ This is called from mime_free() and should free all data associated
+ with the object. If the object points to other MIME objects, they
+ should be finalized as well (by calling mime_free(), not by calling
+ their finalize() methods directly.)
+
+ int parse_buffer (const char *buf, int32_t size, MimeObject *obj)
+
+ This is the method by which you feed arbitrary data into the parser
+ for this object. Most subclasses will probably inherit this method
+ from the MimeObject base-class, which line-buffers the data and then
+ hands it off to the parse_line() method.
+
+ If this object uses a Content-Transfer-Encoding (base64, qp, uue)
+ then the data may be decoded by parse_buffer() before parse_line()
+ is called. (The MimeLeaf class provides this functionality.)
+
+ int parse_begin (MimeObject *obj)
+ Called after `init' but before `parse_line' or `parse_buffer'.
+ Can be used to initialize various parsing machinery.
+
+ int parse_line (const char *line, int32_t length, MimeObject *obj)
+
+ This method is called (by parse_buffer()) for each complete line of
+ data handed to the parser, and is the method which most subclasses
+ will override to implement their parsers.
+
+ When handing data off to a MIME object for parsing, one should always
+ call the parse_buffer() method, and not call the parse_line() method
+ directly, since the parse_buffer() method may do other transformations
+ on the data (like base64 decoding.)
+
+ One should generally not call parse_line() directly, since that could
+ bypass decoding. One should call parse_buffer() instead.
+
+ int parse_eof (MimeObject *obj, bool abort_p)
+
+ This is called when there is no more data to be handed to the object:
+ when the parent object is done feeding data to an object being parsed.
+ Implementors of this method should be sure to also call the parse_eof()
+ methods of any sub-objects to which they have pointers.
+
+ This is also called by the finalize() method, just before object
+ destruction, if it has not already been called.
+
+ The `closed_p' instance variable is used to prevent multiple calls to
+ `parse_eof'.
+
+ int parse_end (MimeObject *obj)
+ Called after `parse_eof' but before `finalize'.
+ This can be used to free up any memory no longer needed now that parsing
+ is done (to avoid surprises due to unexpected method combination, it's
+ best to free things in this method in preference to `parse_eof'.)
+ Implementors of this method should be sure to also call the parse_end()
+ methods of any sub-objects to which they have pointers.
+
+ This is also called by the finalize() method, just before object
+ destruction, if it has not already been called.
+
+ The `parsed_p' instance variable is used to prevent multiple calls to
+ `parse_end'.
+
+
+ bool displayable_inline_p (MimeObjectClass *class, MimeHeaders *hdrs)
+
+ This method should return true if this class of object will be displayed
+ directly, as opposed to being displayed as a link. This information is
+ used by the "multipart/alternative" parser to decide which of its children
+ is the ``best'' one to display. Note that this is a class method, not
+ an object method -- there is not yet an instance of this class at the time
+ that it is called. The `hdrs' provided are the headers of the object that
+ might be instantiated -- from this, the method may extract additional
+ information that it might need to make its decision.
+ */
+
+/* this one is typdedef'ed in mimei.h, since it is the base-class. */
+struct MimeObjectClass {
+ /* Note: the order of these first five slots is known by MimeDefClass().
+ Technically, these are part of the object system, not the MIME code.
+ */
+ const char* class_name;
+ int instance_size;
+ struct MimeObjectClass* superclass;
+ int (*class_initialize)(MimeObjectClass* clazz);
+ bool class_initialized;
+
+ /* These are the methods shared by all MIME objects. See comment above.
+ */
+ int (*initialize)(MimeObject* obj);
+ void (*finalize)(MimeObject* obj);
+ int (*parse_begin)(MimeObject* obj);
+ int (*parse_buffer)(const char* buf, int32_t size, MimeObject* obj);
+ int (*parse_line)(const char* line, int32_t length, MimeObject* obj);
+ int (*parse_eof)(MimeObject* obj, bool abort_p);
+ int (*parse_end)(MimeObject* obj, bool abort_p);
+
+ bool (*displayable_inline_p)(MimeObjectClass* clazz, MimeHeaders* hdrs);
+
+#if defined(DEBUG) && defined(XP_UNIX)
+ int (*debug_print)(MimeObject* obj, PRFileDesc* stream, int32_t depth);
+#endif
+};
+
+extern "C" MimeObjectClass mimeObjectClass;
+
+/* this one is typdedef'ed in mimei.h, since it is the base-class. */
+struct MimeObject {
+ MimeObjectClass* clazz; /* Pointer to class object, for `type-of' */
+
+ MimeHeaders* headers; /* The header data associated with this object;
+ this is where the content-type, disposition,
+ description, and other meta-data live.
+
+ For example, the outermost message/rfc822 object
+ would have NULL here (since it has no parent,
+ thus no headers to describe it.) However, a
+ multipart/mixed object, which was the sole
+ child of that message/rfc822 object, would have
+ here a copy of the headers which began the
+ parent object (the headers which describe the
+ child.)
+ */
+
+ char* content_type; /* The MIME content-type and encoding. */
+ char* encoding; /* In most cases, these will be the same as the
+ values to be found in the `headers' object,
+ but in some cases, the values in these slots
+ will be more correct than the headers.
+ */
+
+ MimeObject* parent; /* Backpointer to a MimeContainer object. */
+
+ MimeDisplayOptions* options; /* Display preferences set by caller. */
+
+ bool closed_p; /* Whether it's done being written to. */
+ bool parsed_p; /* Whether the parser has been shut down. */
+ bool output_p; /* Whether it should be written. */
+ bool dontShowAsAttachment; /* Force an object to not be shown as attachment,
+ but when is false, it doesn't mean it will be
+ shown as attachment; specifically, body parts
+ are never shown as attachments. */
+
+ /* Read-buffer and write-buffer (on input, `parse_buffer' uses ibuffer to
+ compose calls to `parse_line'; on output, `obuffer' is used in various
+ ways by various routines.) These buffers are created and grow as needed.
+ `ibuffer' should be generally be considered hands-off, and `obuffer'
+ should generally be considered fair game.
+ */
+ char *ibuffer, *obuffer;
+ int32_t ibuffer_size, obuffer_size;
+ int32_t ibuffer_fp, obuffer_fp;
+};
+
+#define MimeObject_grow_obuffer(obj, desired_size) \
+ (((desired_size) >= (obj)->obuffer_size) \
+ ? mime_GrowBuffer((uint32_t)(desired_size), (uint32_t)sizeof(char), \
+ 1024, &(obj)->obuffer, \
+ (int32_t*)&(obj)->obuffer_size) \
+ : 0)
+
+#endif /* _MIMEOBJ_H_ */
diff --git a/comm/mailnews/mime/src/mimepbuf.cpp b/comm/mailnews/mime/src/mimepbuf.cpp
new file mode 100644
index 0000000000..c428f66eb4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimepbuf.cpp
@@ -0,0 +1,251 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include "mimepbuf.h"
+#include "mimemoz2.h"
+#include "prmem.h"
+#include "prio.h"
+#include "plstr.h"
+#include "nsMimeStringResources.h"
+#include "nsNetUtil.h"
+#include "nsMsgUtils.h"
+//
+// External Defines...
+//
+extern nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile);
+
+/* See mimepbuf.h for a description of the mission of this file.
+
+ Implementation:
+
+ When asked to buffer an object, we first try to malloc() a buffer to
+ hold the upcoming part. First we try to allocate a 50k buffer, and
+ then back off by 5k until we are able to complete the allocation,
+ or are unable to allocate anything.
+
+ As data is handed to us, we store it in the memory buffer, until the
+ size of the memory buffer is exceeded (including the case where no
+ memory buffer was able to be allocated at all.)
+
+ Once we've filled the memory buffer, we open a temp file on disk.
+ Anything that is currently in the memory buffer is then flushed out
+ to the disk file (and the memory buffer is discarded.) Subsequent
+ data that is passed in is appended to the file.
+
+ Thus only one of the memory buffer or the disk buffer ever exist at
+ the same time; and small parts tend to live completely in memory
+ while large parts tend to live on disk.
+
+ When we are asked to read the data back out of the buffer, we call
+ the provided read-function with either: the contents of the memory
+ buffer; or blocks read from the disk file.
+ */
+
+#define TARGET_MEMORY_BUFFER_SIZE (1024 * 50) /* try for 50k mem buffer */
+#define TARGET_MEMORY_BUFFER_QUANTUM (1024 * 5) /* decrease in steps of 5k */
+#define DISK_BUFFER_SIZE (1024 * 10) /* read disk in 10k chunks */
+
+struct MimePartBufferData {
+ char* part_buffer; /* Buffer used for part-lookahead. */
+ int32_t part_buffer_fp; /* Active length. */
+ int32_t part_buffer_size; /* How big it is. */
+
+ nsCOMPtr<nsIFile> file_buffer; /* The nsIFile of a temp file used when we
+ run out of room in the head_buffer. */
+ nsCOMPtr<nsIInputStream> input_file_stream; /* A stream to it. */
+ nsCOMPtr<nsIOutputStream> output_file_stream; /* A stream to it. */
+ MimePartBufferData()
+ : part_buffer(nullptr), part_buffer_fp(0), part_buffer_size(0) {}
+};
+
+MimePartBufferData* MimePartBufferCreate(void) {
+ return new MimePartBufferData();
+}
+
+void MimePartBufferClose(MimePartBufferData* data) {
+ NS_ASSERTION(data, "MimePartBufferClose: no data");
+ if (!data) return;
+
+ if (data->input_file_stream) {
+ data->input_file_stream->Close();
+ data->input_file_stream = nullptr;
+ }
+
+ if (data->output_file_stream) {
+ data->output_file_stream->Close();
+ data->output_file_stream = nullptr;
+ }
+}
+
+void MimePartBufferReset(MimePartBufferData* data) {
+ NS_ASSERTION(data, "MimePartBufferReset: no data");
+ if (!data) return;
+
+ PR_FREEIF(data->part_buffer);
+ data->part_buffer_fp = 0;
+
+ if (data->input_file_stream) {
+ data->input_file_stream->Close();
+ data->input_file_stream = nullptr;
+ }
+
+ if (data->output_file_stream) {
+ data->output_file_stream->Close();
+ data->output_file_stream = nullptr;
+ }
+
+ if (data->file_buffer) {
+ data->file_buffer->Remove(false);
+ data->file_buffer = nullptr;
+ }
+}
+
+void MimePartBufferDestroy(MimePartBufferData* data) {
+ NS_ASSERTION(data, "MimePartBufferDestroy: no data");
+ if (!data) return;
+ MimePartBufferReset(data);
+ delete data;
+}
+
+int MimePartBufferWrite(MimePartBufferData* data, const char* buf,
+ int32_t size) {
+ NS_ASSERTION(data && buf && size > 0, "MimePartBufferWrite: Bad param");
+ if (!data || !buf || size <= 0) return -1;
+
+ /* If we don't yet have a buffer (either memory or file) try and make a
+ memory buffer.
+ */
+ if (!data->part_buffer && !data->file_buffer) {
+ int target_size = TARGET_MEMORY_BUFFER_SIZE;
+ while (target_size > 0) {
+ data->part_buffer = (char*)PR_MALLOC(target_size);
+ if (data->part_buffer) break; // got it!
+ target_size -= TARGET_MEMORY_BUFFER_QUANTUM; // decrease it and try again
+ }
+
+ if (data->part_buffer)
+ data->part_buffer_size = target_size;
+ else
+ data->part_buffer_size = 0;
+
+ data->part_buffer_fp = 0;
+ }
+
+ /* Ok, if at this point we still don't have either kind of buffer, try and
+ make a file buffer. */
+ if (!data->part_buffer && !data->file_buffer) {
+ nsCOMPtr<nsIFile> tmpFile;
+ nsresult rv = nsMsgCreateTempFile("nsma", getter_AddRefs(tmpFile));
+ NS_ENSURE_SUCCESS(rv, MIME_UNABLE_TO_OPEN_TMP_FILE);
+ data->file_buffer = tmpFile;
+
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(data->output_file_stream), data->file_buffer,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+ NS_ENSURE_SUCCESS(rv, MIME_UNABLE_TO_OPEN_TMP_FILE);
+ }
+
+ NS_ASSERTION(data->part_buffer || data->output_file_stream,
+ "no part_buffer or file_stream");
+
+ /* If this buf will fit in the memory buffer, put it there.
+ */
+ if (data->part_buffer &&
+ data->part_buffer_fp + size < data->part_buffer_size) {
+ memcpy(data->part_buffer + data->part_buffer_fp, buf, size);
+ data->part_buffer_fp += size;
+ }
+
+ /* Otherwise it won't fit; write it to the file instead. */
+ else {
+ /* If the file isn't open yet, open it, and dump the memory buffer
+ to it. */
+ if (!data->output_file_stream) {
+ nsresult rv;
+ if (!data->file_buffer) {
+ nsCOMPtr<nsIFile> tmpFile;
+ rv = nsMsgCreateTempFile("nsma", getter_AddRefs(tmpFile));
+ NS_ENSURE_SUCCESS(rv, MIME_UNABLE_TO_OPEN_TMP_FILE);
+ data->file_buffer = tmpFile;
+ }
+
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(data->output_file_stream), data->file_buffer,
+ PR_WRONLY | PR_CREATE_FILE, 00600);
+ NS_ENSURE_SUCCESS(rv, MIME_UNABLE_TO_OPEN_TMP_FILE);
+
+ if (data->part_buffer && data->part_buffer_fp) {
+ uint32_t bytesWritten;
+ nsresult rv = data->output_file_stream->Write(
+ data->part_buffer, data->part_buffer_fp, &bytesWritten);
+ NS_ENSURE_SUCCESS(rv, MIME_ERROR_WRITING_FILE);
+ }
+
+ PR_FREEIF(data->part_buffer);
+ data->part_buffer_fp = 0;
+ data->part_buffer_size = 0;
+ }
+
+ /* Dump this buf to the file. */
+ uint32_t bytesWritten;
+ nsresult rv = data->output_file_stream->Write(buf, size, &bytesWritten);
+ if (NS_FAILED(rv) || (int32_t)bytesWritten < size)
+ return MIME_OUT_OF_MEMORY;
+ }
+
+ return 0;
+}
+
+int MimePartBufferRead(MimePartBufferData* data,
+ MimeConverterOutputCallback read_fn, void* closure) {
+ int status = 0;
+ NS_ASSERTION(data, "no data");
+ if (!data) return -1;
+
+ if (data->part_buffer) {
+ // Read it out of memory.
+ status = read_fn(data->part_buffer, data->part_buffer_fp, closure);
+ } else if (data->file_buffer) {
+ /* Read it off disk.
+ */
+ char* buf;
+ int32_t buf_size = DISK_BUFFER_SIZE;
+
+ NS_ASSERTION(data->part_buffer_size == 0 && data->part_buffer_fp == 0,
+ "buffer size is not null");
+ NS_ASSERTION(data->file_buffer, "no file buffer name");
+ if (!data->file_buffer) return -1;
+
+ buf = (char*)PR_MALLOC(buf_size);
+ if (!buf) return MIME_OUT_OF_MEMORY;
+
+ // First, close the output file to open the input file!
+ if (data->output_file_stream) data->output_file_stream->Close();
+
+ nsresult rv = NS_NewLocalFileInputStream(
+ getter_AddRefs(data->input_file_stream), data->file_buffer);
+ if (NS_FAILED(rv)) {
+ PR_Free(buf);
+ return MIME_UNABLE_TO_OPEN_TMP_FILE;
+ }
+ while (1) {
+ uint32_t bytesRead = 0;
+ rv = data->input_file_stream->Read(buf, buf_size - 1, &bytesRead);
+ if (NS_FAILED(rv) || !bytesRead) {
+ break;
+ } else {
+ /* It would be really nice to be able to yield here, and let
+ some user events and other input sources get processed.
+ Oh well. */
+
+ status = read_fn(buf, bytesRead, closure);
+ if (status < 0) break;
+ }
+ }
+ PR_Free(buf);
+ }
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimepbuf.h b/comm/mailnews/mime/src/mimepbuf.h
new file mode 100644
index 0000000000..b0e19ca379
--- /dev/null
+++ b/comm/mailnews/mime/src/mimepbuf.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEPBUF_H_
+#define _MIMEPBUF_H_
+
+#include "mimei.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+/* This file provides the ability to save up the entire contents of a MIME
+ object (of arbitrary size), and then emit it all at once later. The
+ buffering is done in an efficient way that works well for both very large
+ and very small objects.
+
+ This is used in two places:
+
+ = The implementation of multipart/alternative uses this code to do a
+ one-part-lookahead. As it traverses its children, it moves forward
+ until it finds a part which cannot be displayed; and then it displays
+ the *previous* part (the last which *could* be displayed.) This code
+ is used to hold the previous part until it is needed.
+*/
+
+/* An opaque object used to represent the buffered data.
+ */
+typedef struct MimePartBufferData MimePartBufferData;
+
+/* Create an empty part buffer object.
+ */
+extern MimePartBufferData* MimePartBufferCreate(void);
+
+/* Assert that the buffer is now full (EOF has been reached on the current
+ part.) This will free some resources, but leaves the part in the buffer.
+ After calling MimePartBufferReset, the buffer may be used to store a
+ different object.
+ */
+void MimePartBufferClose(MimePartBufferData* data);
+
+/* Reset a part buffer object to the default state, discarding any currently-
+ buffered data.
+ */
+extern void MimePartBufferReset(MimePartBufferData* data);
+
+/* Free the part buffer itself, and discard any buffered data.
+ */
+extern void MimePartBufferDestroy(MimePartBufferData* data);
+
+/* Push a chunk of a MIME object into the buffer.
+ */
+extern int MimePartBufferWrite(MimePartBufferData* data, const char* buf,
+ int32_t size);
+
+/* Read the contents of the buffer back out. This will invoke the provided
+ read_fn with successive chunks of data until the buffer has been drained.
+ The provided function may be called once, or multiple times.
+ */
+extern int MimePartBufferRead(MimePartBufferData* data,
+ MimeConverterOutputCallback read_fn,
+ void* closure);
+
+#endif /* _MIMEPBUF_H_ */
diff --git a/comm/mailnews/mime/src/mimesun.cpp b/comm/mailnews/mime/src/mimesun.cpp
new file mode 100644
index 0000000000..03e25336c9
--- /dev/null
+++ b/comm/mailnews/mime/src/mimesun.cpp
@@ -0,0 +1,313 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimesun.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include <ctype.h>
+
+#define MIME_SUPERCLASS mimeMultipartClass
+MimeDefClass(MimeSunAttachment, MimeSunAttachmentClass, mimeSunAttachmentClass,
+ &MIME_SUPERCLASS);
+
+static MimeMultipartBoundaryType MimeSunAttachment_check_boundary(MimeObject*,
+ const char*,
+ int32_t);
+static int MimeSunAttachment_create_child(MimeObject*);
+static int MimeSunAttachment_parse_child_line(MimeObject*, const char*, int32_t,
+ bool);
+static int MimeSunAttachment_parse_begin(MimeObject*);
+static int MimeSunAttachment_parse_eof(MimeObject*, bool);
+
+static int MimeSunAttachmentClassInitialize(MimeSunAttachmentClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeMultipartClass* mclass = (MimeMultipartClass*)clazz;
+
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->parse_begin = MimeSunAttachment_parse_begin;
+ oclass->parse_eof = MimeSunAttachment_parse_eof;
+ mclass->check_boundary = MimeSunAttachment_check_boundary;
+ mclass->create_child = MimeSunAttachment_create_child;
+ mclass->parse_child_line = MimeSunAttachment_parse_child_line;
+ return 0;
+}
+
+static int MimeSunAttachment_parse_begin(MimeObject* obj) {
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ /* Sun messages always have separators at the beginning. */
+ return MimeObject_write_separator(obj);
+}
+
+static int MimeSunAttachment_parse_eof(MimeObject* obj, bool abort_p) {
+ int status = 0;
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ /* Sun messages always have separators at the end. */
+ if (!abort_p) {
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static MimeMultipartBoundaryType MimeSunAttachment_check_boundary(
+ MimeObject* obj, const char* line, int32_t length) {
+ /* ten dashes */
+
+ if (line && line[0] == '-' && line[1] == '-' && line[2] == '-' &&
+ line[3] == '-' && line[4] == '-' && line[5] == '-' && line[6] == '-' &&
+ line[7] == '-' && line[8] == '-' && line[9] == '-' &&
+ (line[10] == '\r' || line[10] == '\n'))
+ return MimeMultipartBoundaryTypeSeparator;
+ else
+ return MimeMultipartBoundaryTypeNone;
+}
+
+static int MimeSunAttachment_create_child(MimeObject* obj) {
+ if (obj->options) obj->options->is_child = true;
+
+ MimeMultipart* mult = (MimeMultipart*)obj;
+ int status = 0;
+
+ char* sun_data_type = 0;
+ const char *mime_ct = 0, *sun_enc_info = 0, *mime_cte = 0;
+ char* mime_ct2 = 0; /* sometimes we need to copy; this is for freeing. */
+ MimeObject* child = 0;
+
+ mult->state = MimeMultipartPartLine;
+
+ sun_data_type =
+ (mult->hdrs
+ ? MimeHeaders_get(mult->hdrs, HEADER_X_SUN_DATA_TYPE, true, false)
+ : 0);
+ if (sun_data_type) {
+ int i;
+ static const struct {
+ const char *in, *out;
+ } sun_types[] = {
+
+ /* Convert recognised Sun types to the corresponding MIME types,
+ and convert unrecognized ones based on the file extension and
+ the mime.types file.
+
+ These are the magic types used by MailTool that I can determine.
+ The only actual written spec I've found only listed the first few.
+ The rest were found by inspection (both of real-world messages,
+ and by running `strings' on the MailTool binary, and on the file
+ /usr/openwin/lib/cetables/cetables (the "Class Engine", Sun's
+ equivalent to .mailcap and mime.types.)
+ */
+ {"default", TEXT_PLAIN},
+ {"default-doc", TEXT_PLAIN},
+ {"text", TEXT_PLAIN},
+ {"scribe", TEXT_PLAIN},
+ {"sgml", TEXT_PLAIN},
+ {"tex", TEXT_PLAIN},
+ {"troff", TEXT_PLAIN},
+ {"c-file", TEXT_PLAIN},
+ {"h-file", TEXT_PLAIN},
+ {"readme-file", TEXT_PLAIN},
+ {"shell-script", TEXT_PLAIN},
+ {"cshell-script", TEXT_PLAIN},
+ {"makefile", TEXT_PLAIN},
+ {"hidden-docs", TEXT_PLAIN},
+ {"message", MESSAGE_RFC822},
+ {"mail-message", MESSAGE_RFC822},
+ {"mail-file", TEXT_PLAIN},
+ {"gif-file", IMAGE_GIF},
+ {"jpeg-file", IMAGE_JPG},
+ {"ppm-file", IMAGE_PPM},
+ {"pgm-file", "image/x-portable-graymap"},
+ {"pbm-file", "image/x-portable-bitmap"},
+ {"xpm-file", "image/x-xpixmap"},
+ {"ilbm-file", "image/ilbm"},
+ {"tiff-file", "image/tiff"},
+ {"photocd-file", "image/x-photo-cd"},
+ {"sun-raster", "image/x-sun-raster"},
+ {"audio-file", AUDIO_BASIC},
+ {"postscript", APPLICATION_POSTSCRIPT},
+ {"postscript-file", APPLICATION_POSTSCRIPT},
+ {"framemaker-document", "application/x-framemaker"},
+ {"sundraw-document", "application/x-sun-draw"},
+ {"sunpaint-document", "application/x-sun-paint"},
+ {"sunwrite-document", "application/x-sun-write"},
+ {"islanddraw-document", "application/x-island-draw"},
+ {"islandpaint-document", "application/x-island-paint"},
+ {"islandwrite-document", "application/x-island-write"},
+ {"sun-executable", APPLICATION_OCTET_STREAM},
+ {"default-app", APPLICATION_OCTET_STREAM},
+ {0, 0}};
+ for (i = 0; sun_types[i].in; i++)
+ if (!PL_strcasecmp(sun_data_type, sun_types[i].in)) {
+ mime_ct = sun_types[i].out;
+ break;
+ }
+ }
+
+ /* If we didn't find a type, look at the extension on the file name.
+ */
+ if (!mime_ct && obj->options && obj->options->file_type_fn) {
+ char* name = MimeHeaders_get_name(mult->hdrs, obj->options);
+ if (name) {
+ mime_ct2 = obj->options->file_type_fn(name, obj->options->stream_closure);
+ mime_ct = mime_ct2;
+ PR_Free(name);
+ if (!mime_ct2 || !PL_strcasecmp(mime_ct2, UNKNOWN_CONTENT_TYPE)) {
+ PR_FREEIF(mime_ct2);
+ mime_ct = APPLICATION_OCTET_STREAM;
+ }
+ }
+ }
+ if (!mime_ct) mime_ct = APPLICATION_OCTET_STREAM;
+
+ PR_FREEIF(sun_data_type);
+
+ /* Convert recognised Sun encodings to the corresponding MIME encodings.
+ However, if the X-Sun-Encoding-Info field contains more than one
+ encoding (that is, contains a comma) then assign it the encoding of
+ the *rightmost* element in the list; and change its Content-Type to
+ application/octet-stream. Examples:
+
+ Sun Type: Translates To:
+ ================== ====================
+ type: TEXT type: text/plain
+ encoding: COMPRESS encoding: x-compress
+
+ type: POSTSCRIPT type: application/x-compress
+ encoding: COMPRESS,UUENCODE encoding: x-uuencode
+
+ type: TEXT type: application/octet-stream
+ encoding: UNKNOWN,UUENCODE encoding: x-uuencode
+ */
+
+ sun_data_type =
+ (mult->hdrs ? MimeHeaders_get(mult->hdrs, HEADER_X_SUN_ENCODING_INFO,
+ false, false)
+ : 0);
+ sun_enc_info = sun_data_type;
+
+ /* this "adpcm-compress" pseudo-encoding is some random junk that
+ MailTool adds to the encoding description of .AU files: we can
+ ignore it if it is the leftmost element of the encoding field.
+ (It looks like it's created via `audioconvert -f g721'. Why?
+ Who knows.)
+ */
+ if (sun_enc_info && !PL_strncasecmp(sun_enc_info, "adpcm-compress", 14)) {
+ sun_enc_info += 14;
+ while (IS_SPACE(*sun_enc_info) || *sun_enc_info == ',') sun_enc_info++;
+ }
+
+ /* Extract the last element of the encoding field, changing the content
+ type if necessary (as described above.)
+ */
+ if (sun_enc_info && *sun_enc_info) {
+ const char* prev;
+ const char* end = PL_strrchr(sun_enc_info, ',');
+ if (end) {
+ const char* start = sun_enc_info;
+ sun_enc_info = end + 1;
+ while (IS_SPACE(*sun_enc_info)) sun_enc_info++;
+ for (prev = end - 1; prev > start && *prev != ','; prev--)
+ ;
+ if (*prev == ',') prev++;
+
+ if (!PL_strncasecmp(prev, "uuencode", end - prev))
+ mime_ct = APPLICATION_UUENCODE;
+ else if (!PL_strncasecmp(prev, "gzip", end - prev))
+ mime_ct = APPLICATION_GZIP;
+ else if (!PL_strncasecmp(prev, "compress", end - prev))
+ mime_ct = APPLICATION_COMPRESS;
+ else if (!PL_strncasecmp(prev, "default-compress", end - prev))
+ mime_ct = APPLICATION_COMPRESS;
+ else
+ mime_ct = APPLICATION_OCTET_STREAM;
+ }
+ }
+
+ /* Convert the remaining Sun encoding to a MIME encoding.
+ If it isn't known, change the content-type instead.
+ */
+ if (!sun_enc_info || !*sun_enc_info)
+ ;
+ else if (!PL_strcasecmp(sun_enc_info, "compress"))
+ mime_cte = ENCODING_COMPRESS;
+ else if (!PL_strcasecmp(sun_enc_info, "uuencode"))
+ mime_cte = ENCODING_UUENCODE;
+ else if (!PL_strcasecmp(sun_enc_info, "gzip"))
+ mime_cte = ENCODING_GZIP;
+ else
+ mime_ct = APPLICATION_OCTET_STREAM;
+
+ PR_FREEIF(sun_data_type);
+
+ /* Now that we know its type and encoding, create a MimeObject to represent
+ this part.
+ */
+ child = mime_create(mime_ct, mult->hdrs, obj->options);
+ if (!child) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+
+ /* Fake out the child's content-type and encoding (it probably doesn't have
+ one right now, because the X-Sun- headers aren't generally recognised by
+ the rest of this library.)
+ */
+ PR_FREEIF(child->content_type);
+ PR_FREEIF(child->encoding);
+ PR_ASSERT(mime_ct);
+ child->content_type = (mime_ct ? strdup(mime_ct) : 0);
+ child->encoding = (mime_cte ? strdup(mime_cte) : 0);
+
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, child);
+ if (status < 0) {
+ mime_free(child);
+ child = 0;
+ goto FAIL;
+ }
+
+ /* Sun attachments always have separators between parts. */
+ status = MimeObject_write_separator(obj);
+ if (status < 0) goto FAIL;
+
+ /* And now that we've added this new object to our list of
+ children, start its parser going. */
+ status = child->clazz->parse_begin(child);
+ if (status < 0) goto FAIL;
+
+FAIL:
+ PR_FREEIF(mime_ct2);
+ PR_FREEIF(sun_data_type);
+ return status;
+}
+
+static int MimeSunAttachment_parse_child_line(MimeObject* obj, const char* line,
+ int32_t length,
+ bool first_line_p) {
+ MimeContainer* cont = (MimeContainer*)obj;
+ MimeObject* kid;
+
+ /* This is simpler than MimeMultipart->parse_child_line in that it doesn't
+ play games about body parts without trailing newlines.
+ */
+
+ PR_ASSERT(cont->nchildren > 0);
+ if (cont->nchildren <= 0) return -1;
+
+ kid = cont->children[cont->nchildren - 1];
+ PR_ASSERT(kid);
+ if (!kid) return -1;
+
+ return kid->clazz->parse_buffer(line, length, kid);
+}
diff --git a/comm/mailnews/mime/src/mimesun.h b/comm/mailnews/mime/src/mimesun.h
new file mode 100644
index 0000000000..8d3b15bd1c
--- /dev/null
+++ b/comm/mailnews/mime/src/mimesun.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMESUN_H_
+#define _MIMESUN_H_
+
+#include "mimemult.h"
+
+/* MimeSunAttachment is the class for X-Sun-Attachment message contents, which
+ is the Content-Type assigned by that pile of garbage called MailTool. This
+ is not a MIME type per se, but it's very similar to multipart/mixed, so it's
+ easy to parse. Lots of people use MailTool, so what the hell.
+
+ The format is this:
+
+ = Content-Type is X-Sun-Attachment
+ = parts are separated by lines of exactly ten dashes
+ = just after the dashes comes a block of headers, including:
+
+ X-Sun-Data-Type: (manditory)
+ Values are Text, Postscript, Scribe, SGML, TeX, Troff, DVI,
+ and Message.
+
+ X-Sun-Encoding-Info: (optional)
+ Ordered, comma-separated values, including Compress and Uuencode.
+
+ X-Sun-Data-Name: (optional)
+ File name, maybe.
+
+ X-Sun-Data-Description: (optional)
+ Longer text.
+
+ X-Sun-Content-Lines: (manditory, unless Length is present)
+ Number of lines in the body, not counting headers and the blank
+ line that follows them.
+
+ X-Sun-Content-Length: (manditory, unless Lines is present)
+ Bytes, presumably using Unix line terminators.
+ */
+
+typedef struct MimeSunAttachmentClass MimeSunAttachmentClass;
+typedef struct MimeSunAttachment MimeSunAttachment;
+
+struct MimeSunAttachmentClass {
+ MimeMultipartClass multipart;
+};
+
+extern MimeSunAttachmentClass mimeSunAttachmentClass;
+
+struct MimeSunAttachment {
+ MimeMultipart multipart;
+};
+
+#define MimeSunAttachmentClassInitializer(ITYPE, CSUPER) \
+ { MimeMultipartClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMESUN_H_ */
diff --git a/comm/mailnews/mime/src/mimetenr.cpp b/comm/mailnews/mime/src/mimetenr.cpp
new file mode 100644
index 0000000000..600cec7397
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetenr.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimetenr.h"
+#include "prlog.h"
+
+/* All the magic for this class is in mimetric.c; since text/enriched and
+ text/richtext are so similar, it was easiest to implement them in the
+ same method (but this is a subclass anyway just for general goodness.)
+ */
+
+#define MIME_SUPERCLASS mimeInlineTextRichtextClass
+MimeDefClass(MimeInlineTextEnriched, MimeInlineTextEnrichedClass,
+ mimeInlineTextEnrichedClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextEnrichedClassInitialize(
+ MimeInlineTextEnrichedClass* clazz) {
+#ifdef DEBUG
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+#endif
+ MimeInlineTextRichtextClass* rclass = (MimeInlineTextRichtextClass*)clazz;
+ rclass->enriched_p = true;
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimetenr.h b/comm/mailnews/mime/src/mimetenr.h
new file mode 100644
index 0000000000..9ff2d6eab4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetenr.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETENR_H_
+#define _MIMETENR_H_
+
+#include "mimetric.h"
+
+/* The MimeInlineTextEnriched class implements the text/enriched MIME content
+ type, as defined in RFC 1563. It does this largely by virtue of being a
+ subclass of the MimeInlineTextRichtext class.
+ */
+
+typedef struct MimeInlineTextEnrichedClass MimeInlineTextEnrichedClass;
+typedef struct MimeInlineTextEnriched MimeInlineTextEnriched;
+
+struct MimeInlineTextEnrichedClass {
+ MimeInlineTextRichtextClass text;
+};
+
+extern MimeInlineTextEnrichedClass mimeInlineTextEnrichedClass;
+
+struct MimeInlineTextEnriched {
+ MimeInlineTextRichtext richtext;
+};
+
+#define MimeInlineTextEnrichedClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextRichtextClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETENR_H_ */
diff --git a/comm/mailnews/mime/src/mimetext.cpp b/comm/mailnews/mime/src/mimetext.cpp
new file mode 100644
index 0000000000..d13b8f8eb4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetext.cpp
@@ -0,0 +1,442 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+#include "mimetext.h"
+#include "mimebuf.h"
+#include "mimethtm.h"
+#include "comi18n.h"
+#include "mimemoz2.h"
+
+#include "prlog.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsMsgUtils.h"
+#include "nsMimeTypes.h"
+#include "nsServiceManagerUtils.h"
+
+#define MIME_SUPERCLASS mimeLeafClass
+MimeDefClass(MimeInlineText, MimeInlineTextClass, mimeInlineTextClass,
+ &MIME_SUPERCLASS);
+
+static int MimeInlineText_initialize(MimeObject*);
+static void MimeInlineText_finalize(MimeObject*);
+static int MimeInlineText_rot13_line(MimeObject*, char* line, int32_t length);
+static int MimeInlineText_parse_eof(MimeObject* obj, bool abort_p);
+static int MimeInlineText_parse_end(MimeObject*, bool);
+static int MimeInlineText_parse_decoded_buffer(const char*, int32_t,
+ MimeObject*);
+static int MimeInlineText_rotate_convert_and_parse_line(char*, int32_t,
+ MimeObject*);
+static int MimeInlineText_open_dam(char* line, int32_t length, MimeObject* obj);
+static int MimeInlineText_initializeCharset(MimeObject* obj);
+
+static int MimeInlineTextClassInitialize(MimeInlineTextClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ MimeLeafClass* lclass = (MimeLeafClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeInlineText_initialize;
+ oclass->finalize = MimeInlineText_finalize;
+ oclass->parse_eof = MimeInlineText_parse_eof;
+ oclass->parse_end = MimeInlineText_parse_end;
+ clazz->rot13_line = MimeInlineText_rot13_line;
+ clazz->initialize_charset = MimeInlineText_initializeCharset;
+ lclass->parse_decoded_buffer = MimeInlineText_parse_decoded_buffer;
+ return 0;
+}
+
+static int MimeInlineText_initialize(MimeObject* obj) {
+ // This is an abstract class; it shouldn't be directly instantiated.
+ PR_ASSERT(obj->clazz != (MimeObjectClass*)&mimeInlineTextClass);
+
+ ((MimeInlineText*)obj)->initializeCharset = false;
+ ((MimeInlineText*)obj)->needUpdateMsgWinCharset = false;
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(obj);
+}
+
+static int MimeInlineText_initializeCharset(MimeObject* obj) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ text->inputAutodetect = false;
+ text->charsetOverridable = false;
+
+ // Figure out an appropriate charset for this object.
+ if (!text->charset && obj->headers) {
+ if (obj->options && obj->options->override_charset) {
+ if (obj->options->default_charset) {
+ text->charset = strdup(obj->options->default_charset);
+ } else {
+ text->charsetOverridable = true;
+ text->inputAutodetect = true;
+ text->needUpdateMsgWinCharset = true;
+ text->charset = strdup("");
+ }
+ } else {
+ char* ct =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false);
+ if (ct) {
+ text->charset = MimeHeaders_get_parameter(ct, "charset", NULL, NULL);
+ PR_Free(ct);
+ }
+
+ if (!text->charset) {
+ // If we didn't find "Content-Type: ...; charset=XX", then look
+ // for "X-Sun-Charset: XX" instead. (Maybe this should be done
+ // in MimeSunAttachmentClass, but it's harder there than here.)
+ text->charset =
+ MimeHeaders_get(obj->headers, HEADER_X_SUN_CHARSET, false, false);
+ }
+
+ // iMIP entities without an explicit charset parameter default to
+ // US-ASCII (RFC 2447, section 2.4). However, Microsoft Outlook generates
+ // UTF-8 but omits the charset parameter.
+ // When no charset is defined by the container (e.g. iMIP), iCalendar
+ // files default to UTF-8 (RFC 2445, section 4.1.4).
+ if (!text->charset && obj->content_type &&
+ !PL_strcasecmp(obj->content_type, TEXT_CALENDAR))
+ text->charset = strdup("UTF-8");
+
+ if (!text->charset) {
+ text->charsetOverridable = true;
+ text->inputAutodetect = true;
+ text->needUpdateMsgWinCharset = true;
+
+ if (obj->options && obj->options->default_charset)
+ text->charset = strdup(obj->options->default_charset);
+ else
+ text->charset = strdup("UTF-8");
+ }
+ }
+ }
+
+ if (text->inputAutodetect) {
+ // We need to prepare lineDam for charset detection.
+ text->lineDamBuffer = (char*)PR_Malloc(DAM_MAX_BUFFER_SIZE);
+ text->lineDamPtrs = (char**)PR_Malloc(DAM_MAX_LINES * sizeof(char*));
+ text->curDamOffset = 0;
+ text->lastLineInDam = 0;
+ if (!text->lineDamBuffer || !text->lineDamPtrs) {
+ text->inputAutodetect = false;
+ PR_FREEIF(text->lineDamBuffer);
+ PR_FREEIF(text->lineDamPtrs);
+ }
+ }
+
+ text->initializeCharset = true;
+
+ return 0;
+}
+
+static void MimeInlineText_finalize(MimeObject* obj) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ obj->clazz->parse_eof(obj, false);
+ obj->clazz->parse_end(obj, false);
+
+ PR_FREEIF(text->charset);
+
+ // Should have been freed by parse_eof, but just in case...
+ PR_ASSERT(!text->cbuffer);
+ PR_FREEIF(text->cbuffer);
+
+ if (text->inputAutodetect) {
+ PR_FREEIF(text->lineDamBuffer);
+ PR_FREEIF(text->lineDamPtrs);
+ text->inputAutodetect = false;
+ }
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeInlineText_parse_eof(MimeObject* obj, bool abort_p) {
+ int status;
+
+ if (obj->closed_p) return 0;
+ NS_ASSERTION(!obj->parsed_p, "obj already parsed");
+
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ // Flush any buffered data from the MimeLeaf's decoder.
+ status = ((MimeLeafClass*)&MIME_SUPERCLASS)->close_decoder(obj);
+ if (status < 0) return status;
+
+ // If there is still data in the ibuffer, that means that the last
+ // line of this part didn't end in a newline; so push it out anyway
+ // (this means that the parse_line method will be called with a string
+ // with no trailing newline, which isn't the usual case). We do this
+ // here, rather than in MimeObject_parse_eof, because MimeObject isn't
+ // aware of the rotating-and-converting / charset detection we need to
+ // do first.
+ if (!abort_p && obj->ibuffer_fp > 0) {
+ status = MimeInlineText_rotate_convert_and_parse_line(obj->ibuffer,
+ obj->ibuffer_fp, obj);
+ obj->ibuffer_fp = 0;
+ if (status < 0) {
+ // We haven't found charset yet? Do it before return.
+ if (text->inputAutodetect)
+ status = MimeInlineText_open_dam(nullptr, 0, obj);
+
+ obj->closed_p = true;
+ return status;
+ }
+ }
+
+ // We haven't found charset yet? Now is the time.
+ if (text->inputAutodetect) status = MimeInlineText_open_dam(nullptr, 0, obj);
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+}
+
+static int MimeInlineText_parse_end(MimeObject* obj, bool abort_p) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ if (obj->parsed_p) {
+ PR_ASSERT(obj->closed_p);
+ return 0;
+ }
+
+ // We won't be needing this buffer any more; nuke it.
+ PR_FREEIF(text->cbuffer);
+ text->cbuffer_size = 0;
+
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_end(obj, abort_p);
+}
+
+// This maps A-M to N-Z and N-Z to A-M. All other characters are left alone.
+// (Comments in GNUS imply that for Japanese, one should rotate by 47?)
+static const unsigned char MimeInlineText_rot13_table[256] = {
+ 0, 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, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87,
+ 88, 89, 90, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
+ 77, 91, 92, 93, 94, 95, 96, 110, 111, 112, 113, 114, 115, 116, 117,
+ 118, 119, 120, 121, 122, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
+ 107, 108, 109, 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};
+
+static int MimeInlineText_rot13_line(MimeObject* obj, char* line,
+ int32_t length) {
+ unsigned char *s, *end;
+ PR_ASSERT(line);
+ if (!line) return -1;
+ s = (unsigned char*)line;
+ end = s + length;
+ while (s < end) {
+ *s = MimeInlineText_rot13_table[*s];
+ s++;
+ }
+ return 0;
+}
+
+static int MimeInlineText_parse_decoded_buffer(const char* buf, int32_t size,
+ MimeObject* obj) {
+ PR_ASSERT(!obj->closed_p);
+ if (obj->closed_p) return -1;
+
+ // MimeLeaf takes care of this.
+ PR_ASSERT(obj->output_p && obj->options && obj->options->output_fn);
+ if (!obj->options) return -1;
+
+ // If we're supposed to write this object, but aren't supposed to convert
+ // it to HTML, simply pass it through unaltered.
+ if (!obj->options->write_html_p &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessageAttach)
+ return MimeObject_write(obj, buf, size, true);
+
+ // This is just like the parse_decoded_buffer method we inherit from the
+ // MimeLeaf class, except that we line-buffer to our own wrapper on the
+ // `parse_line` method instead of calling the `parse_line` method directly.
+ return mime_LineBuffer(buf, size, &obj->ibuffer, &obj->ibuffer_size,
+ &obj->ibuffer_fp, true,
+ ((int (*)(char*, int32_t, void*))
+ /* This cast is to turn void into MimeObject */
+ MimeInlineText_rotate_convert_and_parse_line),
+ obj);
+}
+
+#define MimeInlineText_grow_cbuffer(text, desired_size) \
+ (((desired_size) >= (text)->cbuffer_size) \
+ ? mime_GrowBuffer((desired_size), sizeof(char), 100, &(text)->cbuffer, \
+ &(text)->cbuffer_size) \
+ : 0)
+
+static int MimeInlineText_convert_and_parse_line(char* line, int32_t length,
+ MimeObject* obj) {
+ int status;
+ nsAutoCString converted;
+
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ // In case of charset autodetection, charset can be overridden by meta
+ // charset.
+ if (text->charsetOverridable) {
+ if (mime_typep(obj, (MimeObjectClass*)&mimeInlineTextHTMLClass)) {
+ MimeInlineTextHTML* textHTML = (MimeInlineTextHTML*)obj;
+ if (textHTML->charset && *textHTML->charset &&
+ strcmp(textHTML->charset, text->charset)) {
+ // If meta tag specified charset is different from our detected result,
+ // use meta charset, but we don't want to redo previous lines.
+ PR_FREEIF(text->charset);
+ text->charset = strdup(textHTML->charset);
+
+ // Update MsgWindow charset if we are instructed to do so.
+ if (text->needUpdateMsgWinCharset && *text->charset)
+ SetMailCharacterSetToMsgWindow(obj, text->charset);
+ }
+ }
+ }
+
+ status = obj->options->charset_conversion_fn(
+ line, length, text->charset, converted, obj->options->stream_closure);
+
+ if (status == 0) {
+ line = (char*)converted.get();
+ length = converted.Length();
+ }
+
+ // Now that the line has been converted, call the subclass's parse_line
+ // method with the decoded data.
+ status = obj->clazz->parse_line(line, length, obj);
+
+ return status;
+}
+
+// In this function call, all buffered lines in lineDam will be sent to charset
+// detector and a charset will be used to parse all those line and following
+// lines in this mime obj.
+static int MimeInlineText_open_dam(char* line, int32_t length,
+ MimeObject* obj) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+ nsAutoCString detectedCharset;
+ nsresult res = NS_OK;
+ int status = 0;
+ int32_t i;
+
+ if (text->curDamOffset <= 0) {
+ // There is nothing in dam, use current line for detection.
+ if (length > 0) {
+ res = MIME_detect_charset(line, length, detectedCharset);
+ }
+ } else {
+ // We have stuff in dam, use the one.
+ res = MIME_detect_charset(text->lineDamBuffer, text->curDamOffset,
+ detectedCharset);
+ }
+
+ // Set the charset for this object.
+ if (NS_SUCCEEDED(res) && !detectedCharset.IsEmpty()) {
+ PR_FREEIF(text->charset);
+ text->charset = ToNewCString(detectedCharset);
+
+ // Update MsgWindow charset if we are instructed to do so.
+ if (text->needUpdateMsgWinCharset && text->charset)
+ SetMailCharacterSetToMsgWindow(obj, text->charset);
+ }
+
+ // Process dam and line using the charset.
+ if (text->curDamOffset) {
+ for (i = 0; i < text->lastLineInDam - 1; i++) {
+ status = MimeInlineText_convert_and_parse_line(
+ text->lineDamPtrs[i], text->lineDamPtrs[i + 1] - text->lineDamPtrs[i],
+ obj);
+ }
+ status = MimeInlineText_convert_and_parse_line(
+ text->lineDamPtrs[i],
+ text->lineDamBuffer + text->curDamOffset - text->lineDamPtrs[i], obj);
+ }
+
+ if (length) status = MimeInlineText_convert_and_parse_line(line, length, obj);
+
+ PR_Free(text->lineDamPtrs);
+ PR_Free(text->lineDamBuffer);
+ text->lineDamPtrs = nullptr;
+ text->lineDamBuffer = nullptr;
+ text->inputAutodetect = false;
+
+ return status;
+}
+
+static int MimeInlineText_rotate_convert_and_parse_line(char* line,
+ int32_t length,
+ MimeObject* obj) {
+ int status = 0;
+ MimeInlineTextClass* textc = (MimeInlineTextClass*)obj->clazz;
+
+ PR_ASSERT(!obj->closed_p);
+ if (obj->closed_p) return -1;
+
+ // Rotate the line, if desired (this happens on the raw data, before any
+ // charset conversion).
+ if (obj->options && obj->options->rot13_p) {
+ status = textc->rot13_line(obj, line, length);
+ if (status < 0) return status;
+ }
+
+ // Now convert to the canonical charset, if desired.
+ bool doConvert = true;
+ // Don't convert vCard data
+ if (((obj->content_type) &&
+ (!PL_strcasecmp(obj->content_type, TEXT_VCARD))) ||
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageSaveAs) ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach)
+ doConvert = false;
+
+ // Only convert if the user prefs is false.
+ if ((obj->options && obj->options->charset_conversion_fn) &&
+ (!obj->options->force_user_charset) && (doConvert)) {
+ MimeInlineText* text = (MimeInlineText*)obj;
+
+ if (!text->initializeCharset) {
+ MimeInlineText_initializeCharset(obj);
+ // Update MsgWindow charset if we are instructed to do so.
+ if (text->needUpdateMsgWinCharset && *text->charset)
+ SetMailCharacterSetToMsgWindow(obj, text->charset);
+ }
+
+ // If autodetect is on, push line to dam.
+ if (text->inputAutodetect) {
+ // See if we reach the lineDam buffer limit, if so, there is no need to
+ // keep buffering.
+ if (text->lastLineInDam >= DAM_MAX_LINES ||
+ DAM_MAX_BUFFER_SIZE - text->curDamOffset <= length) {
+ // We let open dam process this line as well as thing that already in
+ // Dam. In case there is nothing in dam because this line is too big, we
+ // need to perform autodetect on this line.
+ status = MimeInlineText_open_dam(line, length, obj);
+ } else {
+ // Buffering current line.
+ text->lineDamPtrs[text->lastLineInDam] =
+ text->lineDamBuffer + text->curDamOffset;
+ memcpy(text->lineDamPtrs[text->lastLineInDam], line, length);
+ text->lastLineInDam++;
+ text->curDamOffset += length;
+ }
+ } else
+ status = MimeInlineText_convert_and_parse_line(line, length, obj);
+ } else
+ status = obj->clazz->parse_line(line, length, obj);
+
+ return status;
+}
diff --git a/comm/mailnews/mime/src/mimetext.h b/comm/mailnews/mime/src/mimetext.h
new file mode 100644
index 0000000000..d3a7a29677
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetext.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETEXT_H_
+#define _MIMETEXT_H_
+
+#include "mimeleaf.h"
+
+/* The MimeInlineText class is the superclass of all handlers for the
+ MIME text/ content types (which convert various text formats to HTML,
+ in one form or another.)
+
+ It provides two services:
+
+ = if ROT13 decoding is desired, the text will be rotated before
+ the `parse_line' method it called;
+
+ = text will be converted from the message's charset to the "target"
+ charset before the `parse_line' method is called.
+
+ The contract with charset-conversion is that the converted data will
+ be such that one may interpret any octets (8-bit bytes) in the data
+ which are in the range of the ASCII characters (0-127) as ASCII
+ characters. It is explicitly legal, for example, to scan through
+ the string for "<" and replace it with "&lt;", and to search for things
+ that look like URLs and to wrap them with interesting HTML tags.
+
+ The charset to which we convert will probably be UTF-8 (an encoding of
+ the Unicode character set, with the feature that all octets with the
+ high bit off have the same interpretations as ASCII.)
+
+ #### NOTE: if it turns out that we use JIS (ISO-2022-JP) as the target
+ encoding, then this is not quite true; it is safe to search for the
+ low ASCII values (under hex 0x40, octal 0100, which is '@') but it
+ is NOT safe to search for values higher than that -- they may be
+ being used as the subsequent bytes in a multi-byte escape sequence.
+ It's a nice coincidence that HTML's critical characters ("<", ">",
+ and "&") have values under 0x40...
+ */
+
+typedef struct MimeInlineTextClass MimeInlineTextClass;
+typedef struct MimeInlineText MimeInlineText;
+
+struct MimeInlineTextClass {
+ MimeLeafClass leaf;
+ int (*rot13_line)(MimeObject* obj, char* line, int32_t length);
+ int (*convert_line_charset)(MimeObject* obj, char* line, int32_t length);
+ int (*initialize_charset)(MimeObject* obj);
+};
+
+extern MimeInlineTextClass mimeInlineTextClass;
+
+#define DAM_MAX_BUFFER_SIZE 8 * 1024
+#define DAM_MAX_LINES 1024
+
+struct MimeInlineText {
+ MimeLeaf leaf; /* superclass variables */
+ char* charset; /* The charset from the content-type of this
+ object, or the caller-specified overrides
+ or defaults. */
+ bool charsetOverridable;
+ bool needUpdateMsgWinCharset;
+ char* cbuffer; /* Buffer used for charset conversion. */
+ int32_t cbuffer_size;
+
+ bool inputAutodetect;
+ bool initializeCharset;
+ int32_t lastLineInDam;
+ int32_t curDamOffset;
+ char* lineDamBuffer;
+ char** lineDamPtrs;
+};
+
+#define MimeInlineTextClassInitializer(ITYPE, CSUPER) \
+ { MimeLeafClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETEXT_H_ */
diff --git a/comm/mailnews/mime/src/mimethpl.cpp b/comm/mailnews/mime/src/mimethpl.cpp
new file mode 100644
index 0000000000..d21efb82bb
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethpl.cpp
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* TODO:
+ - If you Save As File .html with this mode, you get a total mess.
+ - Print is untested (crashes in all modes).
+*/
+/* If you fix a bug here, check, if the same is also in mimethsa, because that
+ class is based on this class. */
+
+#include "mimethpl.h"
+#include "prlog.h"
+#include "msgCore.h"
+#include "mimemoz2.h"
+#include "nsString.h"
+#include "nsIDocumentEncoder.h" // for output flags
+
+#define MIME_SUPERCLASS mimeInlineTextPlainClass
+/* I should use the Flowed class as base (because our HTML->TXT converter
+ can generate flowed, and we tell it to) - this would get a bit nicer
+ rendering. However, that class is more picky about line endings
+ and I currently don't feel like splitting up the generated plaintext
+ into separate lines again. So, I just throw the whole message at once
+ at the TextPlain_parse_line function - it happens to work *g*. */
+MimeDefClass(MimeInlineTextHTMLAsPlaintext, MimeInlineTextHTMLAsPlaintextClass,
+ mimeInlineTextHTMLAsPlaintextClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextHTMLAsPlaintext_parse_line(const char*, int32_t,
+ MimeObject*);
+static int MimeInlineTextHTMLAsPlaintext_parse_begin(MimeObject* obj);
+static int MimeInlineTextHTMLAsPlaintext_parse_eof(MimeObject*, bool);
+static void MimeInlineTextHTMLAsPlaintext_finalize(MimeObject* obj);
+
+static int MimeInlineTextHTMLAsPlaintextClassInitialize(
+ MimeInlineTextHTMLAsPlaintextClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "problem with superclass");
+ oclass->parse_line = MimeInlineTextHTMLAsPlaintext_parse_line;
+ oclass->parse_begin = MimeInlineTextHTMLAsPlaintext_parse_begin;
+ oclass->parse_eof = MimeInlineTextHTMLAsPlaintext_parse_eof;
+ oclass->finalize = MimeInlineTextHTMLAsPlaintext_finalize;
+
+ return 0;
+}
+
+static int MimeInlineTextHTMLAsPlaintext_parse_begin(MimeObject* obj) {
+ MimeInlineTextHTMLAsPlaintext* textHTMLPlain =
+ (MimeInlineTextHTMLAsPlaintext*)obj;
+ textHTMLPlain->complete_buffer = new nsString();
+ // Let's just hope that libmime won't have the idea to call begin twice...
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+}
+
+static int MimeInlineTextHTMLAsPlaintext_parse_eof(MimeObject* obj,
+ bool abort_p) {
+ if (obj->closed_p) return 0;
+
+ // This is a hack. We need to call parse_eof() of the super class to flush out
+ // any buffered data. We can't call it yet for our direct super class, because
+ // it would "close" the output (write tags such as </pre> and </div>). We'll
+ // do that after parsing the buffer.
+ int status =
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->superclass->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ MimeInlineTextHTMLAsPlaintext* textHTMLPlain =
+ (MimeInlineTextHTMLAsPlaintext*)obj;
+
+ if (!textHTMLPlain || !textHTMLPlain->complete_buffer) return 0;
+
+ nsString& cb = *(textHTMLPlain->complete_buffer);
+
+ // could be empty, e.g., if part isn't actually being displayed
+ if (cb.Length()) {
+ nsString asPlaintext;
+ uint32_t flags = nsIDocumentEncoder::OutputFormatted |
+ nsIDocumentEncoder::OutputWrap |
+ nsIDocumentEncoder::OutputFormatFlowed |
+ nsIDocumentEncoder::OutputLFLineBreak |
+ nsIDocumentEncoder::OutputNoScriptContent |
+ nsIDocumentEncoder::OutputNoFramesContent |
+ nsIDocumentEncoder::OutputBodyOnly;
+ HTML2Plaintext(cb, asPlaintext, flags, 80);
+
+ NS_ConvertUTF16toUTF8 resultCStr(asPlaintext);
+ // TODO parse each line independently
+ status =
+ ((MimeObjectClass*)&MIME_SUPERCLASS)
+ ->parse_line(resultCStr.BeginWriting(), resultCStr.Length(), obj);
+ cb.Truncate();
+ }
+
+ if (status < 0) return status;
+
+ // Second part of the flush hack. Pretend obj wasn't closed yet, so that our
+ // super class gets a chance to write the closing.
+ bool save_closed_p = obj->closed_p;
+ obj->closed_p = false;
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ // Restore closed_p.
+ obj->closed_p = save_closed_p;
+ return status;
+}
+
+void MimeInlineTextHTMLAsPlaintext_finalize(MimeObject* obj) {
+ MimeInlineTextHTMLAsPlaintext* textHTMLPlain =
+ (MimeInlineTextHTMLAsPlaintext*)obj;
+ if (textHTMLPlain && textHTMLPlain->complete_buffer) {
+ // If there's content in the buffer, make sure that we output it.
+ // don't care about return codes
+ obj->clazz->parse_eof(obj, false);
+
+ delete textHTMLPlain->complete_buffer;
+ textHTMLPlain->complete_buffer = NULL;
+ /* It is important to zero the pointer, so we can reliably check for
+ the validity of it in the other functions. See above. */
+ }
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeInlineTextHTMLAsPlaintext_parse_line(const char* line,
+ int32_t length,
+ MimeObject* obj) {
+ MimeInlineTextHTMLAsPlaintext* textHTMLPlain =
+ (MimeInlineTextHTMLAsPlaintext*)obj;
+
+ if (!textHTMLPlain || !(textHTMLPlain->complete_buffer)) {
+#if DEBUG
+ printf("Can't output: %s\n", line);
+#endif
+ return -1;
+ }
+
+ /*
+ To convert HTML->TXT synchronously, I need the full source at once,
+ not line by line (how do you convert "<li>foo\n" to plaintext?).
+ parse_decoded_buffer claims to give me that, but in fact also gives
+ me single lines.
+ It might be theoretically possible to drive this asynchronously, but
+ I don't know, which odd circumstances might arise and how libmime
+ will behave then. It's not worth the trouble for me to figure this all out.
+ */
+ nsCString linestr(line, length);
+ NS_ConvertUTF8toUTF16 line_ucs2(linestr.get());
+ if (length && line_ucs2.IsEmpty()) CopyASCIItoUTF16(linestr, line_ucs2);
+ (textHTMLPlain->complete_buffer)->Append(line_ucs2);
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimethpl.h b/comm/mailnews/mime/src/mimethpl.h
new file mode 100644
index 0000000000..8a06e6a2b4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethpl.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* The MimeInlineTextHTMLAsPlaintext class converts HTML->TXT->HTML, i.e.
+ HTML to Plaintext and the result to HTML again.
+ This might sound crazy, maybe it is, but it is for the "View as Plaintext"
+ option, if the sender didn't supply a plaintext alternative (bah!).
+ */
+
+#ifndef _MIMETHPL_H_
+#define _MIMETHPL_H_
+
+#include "mimetpla.h"
+#include "nsString.h"
+
+typedef struct MimeInlineTextHTMLAsPlaintextClass
+ MimeInlineTextHTMLAsPlaintextClass;
+typedef struct MimeInlineTextHTMLAsPlaintext MimeInlineTextHTMLAsPlaintext;
+
+struct MimeInlineTextHTMLAsPlaintextClass {
+ MimeInlineTextPlainClass plaintext;
+};
+
+extern MimeInlineTextHTMLAsPlaintextClass mimeInlineTextHTMLAsPlaintextClass;
+
+struct MimeInlineTextHTMLAsPlaintext {
+ MimeInlineTextPlain plaintext;
+ nsString* complete_buffer; // Gecko parser expects wide strings
+};
+
+#define MimeInlineTextHTMLAsPlaintextClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextPlainClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETHPL_H_ */
diff --git a/comm/mailnews/mime/src/mimethsa.cpp b/comm/mailnews/mime/src/mimethsa.cpp
new file mode 100644
index 0000000000..19c1e5460a
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethsa.cpp
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* Most of this code is copied from mimethpl; see there for source comments.
+ If you find a bug here, check that class, too.
+*/
+
+/* The MimeInlineTextHTMLSanitized class cleans up HTML
+
+ This removes offending HTML features that have no business in mail.
+ It is a low-level stop gap for many classes of attacks,
+ and intended for security conscious users.
+ Paranoia is a feature here, and has served very well in practice.
+
+ It has already prevented countless serious exploits.
+
+ It pushes the HTML that we get from the sender of the message
+ through a sanitizer (nsTreeSanitizer), which lets only allowed tags through.
+ With the appropriate configuration, this protects from most of the
+ security and visual-formatting problems that otherwise usually come with HTML
+ (and which partly gave HTML in email the bad reputation that it has).
+
+ However, due to the parsing and serializing (and later parsing again)
+ required, there is an inherent, significant performance hit, when doing the
+ santinizing here at the MIME / HTML source level. But users of this class
+ will most likely find it worth the cost.
+ */
+
+#include "mimethsa.h"
+#include "prmem.h"
+#include "prlog.h"
+#include "msgCore.h"
+#include "mimemoz2.h"
+#include "nsString.h"
+#include "mimethtm.h"
+
+#define MIME_SUPERCLASS mimeInlineTextHTMLClass
+MimeDefClass(MimeInlineTextHTMLSanitized, MimeInlineTextHTMLSanitizedClass,
+ mimeInlineTextHTMLSanitizedClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextHTMLSanitized_parse_line(const char*, int32_t,
+ MimeObject*);
+static int MimeInlineTextHTMLSanitized_parse_begin(MimeObject* obj);
+static int MimeInlineTextHTMLSanitized_parse_eof(MimeObject*, bool);
+static void MimeInlineTextHTMLSanitized_finalize(MimeObject* obj);
+
+static int MimeInlineTextHTMLSanitizedClassInitialize(
+ MimeInlineTextHTMLSanitizedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "problem with superclass");
+ oclass->parse_line = MimeInlineTextHTMLSanitized_parse_line;
+ oclass->parse_begin = MimeInlineTextHTMLSanitized_parse_begin;
+ oclass->parse_eof = MimeInlineTextHTMLSanitized_parse_eof;
+ oclass->finalize = MimeInlineTextHTMLSanitized_finalize;
+
+ return 0;
+}
+
+static int MimeInlineTextHTMLSanitized_parse_begin(MimeObject* obj) {
+ MimeInlineTextHTMLSanitized* me = (MimeInlineTextHTMLSanitized*)obj;
+ me->complete_buffer = new nsString();
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ return 0;
+}
+
+static int MimeInlineTextHTMLSanitized_parse_eof(MimeObject* obj,
+ bool abort_p) {
+ if (obj->closed_p) return 0;
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+ MimeInlineTextHTMLSanitized* me = (MimeInlineTextHTMLSanitized*)obj;
+
+ // We have to cache all lines and parse the whole document at once.
+ // There's a useful sounding function parseFromStream(), but it only allows
+ // XML mimetypes, not HTML. Methinks that's because the HTML soup parser needs
+ // the entire doc to make sense of the gibberish that people write.
+ if (!me || !me->complete_buffer) return 0;
+
+ nsString& cb = *(me->complete_buffer);
+ if (cb.IsEmpty()) return 0;
+ nsString sanitized;
+
+ // Sanitize.
+ HTMLSanitize(cb, sanitized);
+
+ // Write it out.
+ NS_ConvertUTF16toUTF8 resultCStr(sanitized);
+ MimeInlineTextHTML_insert_lang_div(obj, resultCStr);
+ // Call to MimeInlineTextHTML_remove_plaintext_tag() not needed since
+ // sanitization already removes that tag.
+ status =
+ ((MimeObjectClass*)&MIME_SUPERCLASS)
+ ->parse_line(resultCStr.BeginWriting(), resultCStr.Length(), obj);
+ cb.Truncate();
+ return status;
+}
+
+void MimeInlineTextHTMLSanitized_finalize(MimeObject* obj) {
+ MimeInlineTextHTMLSanitized* me = (MimeInlineTextHTMLSanitized*)obj;
+
+ if (me && me->complete_buffer) {
+ obj->clazz->parse_eof(obj, false);
+ delete me->complete_buffer;
+ me->complete_buffer = NULL;
+ }
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(obj);
+}
+
+static int MimeInlineTextHTMLSanitized_parse_line(const char* line,
+ int32_t length,
+ MimeObject* obj) {
+ MimeInlineTextHTMLSanitized* me = (MimeInlineTextHTMLSanitized*)obj;
+
+ if (!me || !(me->complete_buffer)) return -1;
+
+ nsCString linestr(line, length);
+ NS_ConvertUTF8toUTF16 line_ucs2(linestr.get());
+ if (length && line_ucs2.IsEmpty()) CopyASCIItoUTF16(linestr, line_ucs2);
+ (me->complete_buffer)->Append(line_ucs2);
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimethsa.h b/comm/mailnews/mime/src/mimethsa.h
new file mode 100644
index 0000000000..5d4f318fd2
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethsa.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETHSA_H_
+#define _MIMETHSA_H_
+
+#include "mimethtm.h"
+#include "nsString.h"
+
+typedef struct MimeInlineTextHTMLSanitizedClass
+ MimeInlineTextHTMLSanitizedClass;
+typedef struct MimeInlineTextHTMLSanitized MimeInlineTextHTMLSanitized;
+
+struct MimeInlineTextHTMLSanitizedClass {
+ MimeInlineTextHTMLClass html;
+};
+
+extern MimeInlineTextHTMLSanitizedClass mimeInlineTextHTMLSanitizedClass;
+
+struct MimeInlineTextHTMLSanitized {
+ MimeInlineTextHTML html;
+ nsString* complete_buffer; // Gecko parser expects wide strings
+};
+
+#define MimeInlineTextHTMLSanitizedClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextHTMLClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETHPL_H_ */
diff --git a/comm/mailnews/mime/src/mimethtm.cpp b/comm/mailnews/mime/src/mimethtm.cpp
new file mode 100644
index 0000000000..e35b6a6446
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethtm.cpp
@@ -0,0 +1,229 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "mimethtm.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "prprf.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include <ctype.h>
+
+#define MIME_SUPERCLASS mimeInlineTextClass
+MimeDefClass(MimeInlineTextHTML, MimeInlineTextHTMLClass,
+ mimeInlineTextHTMLClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextHTML_parse_line(const char*, int32_t, MimeObject*);
+static int MimeInlineTextHTML_parse_eof(MimeObject*, bool);
+static int MimeInlineTextHTML_parse_begin(MimeObject* obj);
+
+static int MimeInlineTextHTMLClassInitialize(MimeInlineTextHTMLClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->parse_begin = MimeInlineTextHTML_parse_begin;
+ oclass->parse_line = MimeInlineTextHTML_parse_line;
+ oclass->parse_eof = MimeInlineTextHTML_parse_eof;
+
+ return 0;
+}
+
+static int MimeInlineTextHTML_parse_begin(MimeObject* obj) {
+ int status = ((MimeObjectClass*)&mimeLeafClass)->parse_begin(obj);
+ if (status < 0) return status;
+
+ if (!obj->output_p) return 0;
+
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+
+ MimeInlineTextHTML* textHTML = (MimeInlineTextHTML*)obj;
+
+ textHTML->charset = nullptr;
+
+ /* If this HTML part has a Content-Base header, and if we're displaying
+ to the screen (that is, not writing this part "raw") then translate
+ that Content-Base header into a <BASE> tag in the HTML.
+ */
+ if (obj->options && obj->options->write_html_p && obj->options->output_fn) {
+ char* base_hdr =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_BASE, false, false);
+
+ /* rhp - for MHTML Spec changes!!! */
+ if (!base_hdr) {
+ base_hdr =
+ MimeHeaders_get(obj->headers, HEADER_CONTENT_LOCATION, false, false);
+ }
+ /* rhp - for MHTML Spec changes!!! */
+
+ if (base_hdr) {
+ uint32_t buflen = strlen(base_hdr) + 20;
+ char* buf = (char*)PR_MALLOC(buflen);
+ const char* in;
+ char* out;
+ if (!buf) return MIME_OUT_OF_MEMORY;
+
+ /* The value of the Content-Base header is a number of "words".
+ Whitespace in this header is not significant -- it is assumed
+ that any real whitespace in the URL has already been encoded,
+ and whitespace has been inserted to allow the lines in the
+ mail header to be wrapped reasonably. Creators are supposed
+ to insert whitespace every 40 characters or less.
+ */
+ PL_strncpyz(buf, "<BASE HREF=\"", buflen);
+ out = buf + strlen(buf);
+
+ for (in = base_hdr; *in; in++) /* ignore whitespace and quotes */
+ if (!IS_SPACE(*in) && *in != '"') *out++ = *in;
+
+ /* Close the tag and argument. */
+ *out++ = '"';
+ *out++ = '>';
+ *out++ = 0;
+
+ PR_Free(base_hdr);
+
+ status = MimeObject_write(obj, buf, strlen(buf), false);
+ PR_Free(buf);
+ if (status < 0) return status;
+ }
+ }
+
+ return 0;
+}
+
+static int MimeInlineTextHTML_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeInlineTextHTML* textHTML = (MimeInlineTextHTML*)obj;
+
+ if (!obj->output_p) return 0;
+
+ if (!obj->options || !obj->options->output_fn) return 0;
+
+ if (!textHTML->charset) {
+ char* cp;
+ // First, try to detect a charset via a META tag!
+ if ((cp = PL_strncasestr(line, "META", length)) &&
+ (cp = PL_strncasestr(cp, "HTTP-EQUIV=", length - (int)(cp - line))) &&
+ (cp = PL_strncasestr(cp, "CONTENT=", length - (int)(cp - line))) &&
+ (cp = PL_strncasestr(cp, "CHARSET=", length - (int)(cp - line)))) {
+ char* cp1 = cp + 8; // 8 for the length of "CHARSET="
+ char* cp2 = PL_strnpbrk(cp1, " \"\'", length - (int)(cp1 - line));
+ if (cp2) {
+ char* charset = PL_strndup(cp1, (int)(cp2 - cp1));
+
+ // Fix bug 101434, in this case since this parsing is a char*
+ // operation, a real UTF-16 or UTF-32 document won't be parse
+ // correctly, if it got parse, it cannot be UTF-16 nor UTF-32
+ // there fore, we ignore them if somehow we got that value
+ // 6 == strlen("UTF-16") or strlen("UTF-32"), this will cover
+ // UTF-16, UTF-16BE, UTF-16LE, UTF-32, UTF-32BE, UTF-32LE
+ if ((charset != nullptr) && PL_strncasecmp(charset, "UTF-16", 6) &&
+ PL_strncasecmp(charset, "UTF-32", 6)) {
+ textHTML->charset = charset;
+
+ // write out the data without the charset part...
+ if (textHTML->charset) {
+ int err = MimeObject_write(obj, line, cp - line, true);
+ if (err == 0)
+ err =
+ MimeObject_write(obj, cp2, length - (int)(cp2 - line), true);
+
+ return err;
+ }
+ }
+ PR_FREEIF(charset);
+ }
+ }
+ }
+
+ // Now, just write out the data...
+ return MimeObject_write(obj, line, length, true);
+}
+
+static int MimeInlineTextHTML_parse_eof(MimeObject* obj, bool abort_p) {
+ int status;
+ MimeInlineTextHTML* textHTML = (MimeInlineTextHTML*)obj;
+ if (obj->closed_p) return 0;
+
+ PR_FREEIF(textHTML->charset);
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ return 0;
+}
+
+/*
+ * The following function adds <div class="moz-text-html" lang="..."> or
+ * <div class="moz-text-html"> as the first tag following the <body> tag in the
+ * serialised HTML of a message. This becomes a no-op if no <body> tag is found.
+ */
+void MimeInlineTextHTML_insert_lang_div(MimeObject* obj, nsCString& message) {
+ if (obj->options->format_out != nsMimeOutput::nsMimeMessageBodyDisplay &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessagePrintOutput)
+ return;
+
+ // Make sure we have a <body> before we start.
+ int32_t index = message.LowerCaseFindASCII("<body");
+ if (index == kNotFound) return;
+ index = message.FindChar('>', index) + 1;
+
+ // Insert <div class="moz-text-html" lang="..."> for the following two
+ // purposes:
+ // 1) Users can configure their HTML display via CSS for .moz-text-html.
+ // 2) The language group in the 'lang' attribute is used by Gecko to determine
+ // which font to use.
+ int32_t fontSize; // default font size
+ int32_t fontSizePercentage; // size percentage
+ nsAutoCString fontLang; // langgroup of the font.
+ if (NS_SUCCEEDED(GetMailNewsFont(obj, false, &fontSize, &fontSizePercentage,
+ fontLang))) {
+ message.Insert(
+ "<div class=\"moz-text-html\" lang=\""_ns + fontLang + "\">"_ns, index);
+ } else {
+ message.Insert("<div class=\"moz-text-html\">"_ns, index);
+ }
+
+ nsACString::const_iterator begin, end;
+ message.BeginReading(begin);
+ message.EndReading(end);
+ nsACString::const_iterator messageBegin = begin;
+ if (RFindInReadable("</body>"_ns, begin, end,
+ nsCaseInsensitiveCStringComparator)) {
+ message.InsertLiteral("</div>", begin - messageBegin);
+ }
+}
+
+/*
+ * The following function replaces <plaintext> tags with <x-plaintext>.
+ * <plaintext> is a funny beast: It leads to everything following it
+ * being displayed verbatim, even a </plaintext> tag is ignored.
+ */
+void MimeInlineTextHTML_remove_plaintext_tag(MimeObject* obj,
+ nsCString& message) {
+ if (obj->options->format_out != nsMimeOutput::nsMimeMessageBodyDisplay &&
+ obj->options->format_out != nsMimeOutput::nsMimeMessagePrintOutput)
+ return;
+
+ // Replace all <plaintext> and </plaintext> tags.
+ int32_t index = 0;
+ bool replaced = false;
+ while ((index = message.LowerCaseFindASCII("<plaintext", index)) !=
+ kNotFound) {
+ message.Insert("x-", index + 1);
+ index += 12;
+ replaced = true;
+ }
+ if (replaced) {
+ index = 0;
+ while ((index = message.LowerCaseFindASCII("</plaintext", index)) !=
+ kNotFound) {
+ message.Insert("x-", index + 2);
+ index += 13;
+ }
+ }
+}
diff --git a/comm/mailnews/mime/src/mimethtm.h b/comm/mailnews/mime/src/mimethtm.h
new file mode 100644
index 0000000000..f5e178a937
--- /dev/null
+++ b/comm/mailnews/mime/src/mimethtm.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETHTM_H_
+#define _MIMETHTM_H_
+
+#include "mimetext.h"
+
+/* The MimeInlineTextHTML class implements the text/html MIME content type.
+ */
+
+typedef struct MimeInlineTextHTMLClass MimeInlineTextHTMLClass;
+typedef struct MimeInlineTextHTML MimeInlineTextHTML;
+
+struct MimeInlineTextHTMLClass {
+ MimeInlineTextClass text;
+};
+
+extern MimeInlineTextHTMLClass mimeInlineTextHTMLClass;
+
+struct MimeInlineTextHTML {
+ MimeInlineText text;
+ char* charset; /* If we sniffed a charset, do some converting! */
+};
+
+#define MimeInlineTextHTMLClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextClassInitializer(ITYPE, CSUPER) }
+
+void MimeInlineTextHTML_insert_lang_div(MimeObject* obj, nsCString& message);
+void MimeInlineTextHTML_remove_plaintext_tag(MimeObject* obj,
+ nsCString& message);
+#endif /* _MIMETHTM_H_ */
diff --git a/comm/mailnews/mime/src/mimetpfl.cpp b/comm/mailnews/mime/src/mimetpfl.cpp
new file mode 100644
index 0000000000..d354217e55
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetpfl.cpp
@@ -0,0 +1,613 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimetpfl.h"
+#include "mimebuf.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsString.h"
+#include "nsMimeStringResources.h"
+#include "nsIPrefBranch.h"
+#include "mimemoz2.h"
+#include "prprf.h"
+#include "nsMsgI18N.h"
+
+static const uint32_t kSpacesForATab = 4; // Must be at least 1.
+
+#define MIME_SUPERCLASS mimeInlineTextClass
+MimeDefClass(MimeInlineTextPlainFlowed, MimeInlineTextPlainFlowedClass,
+ mimeInlineTextPlainFlowedClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextPlainFlowed_parse_begin(MimeObject*);
+static int MimeInlineTextPlainFlowed_parse_line(const char*, int32_t,
+ MimeObject*);
+static int MimeInlineTextPlainFlowed_parse_eof(MimeObject*, bool);
+
+static MimeInlineTextPlainFlowedExData* MimeInlineTextPlainFlowedExDataList =
+ nullptr;
+
+// From mimetpla.cpp
+extern "C" void MimeTextBuildPrefixCSS(
+ int32_t quotedSizeSetting, // mail.quoted_size
+ int32_t quotedStyleSetting, // mail.quoted_style
+ nsACString& citationColor, // mail.citation_color
+ nsACString& style);
+// Definition below
+static nsresult Line_convert_whitespace(const nsString& a_line,
+ const bool a_convert_all_whitespace,
+ nsString& a_out_line);
+
+static int MimeInlineTextPlainFlowedClassInitialize(
+ MimeInlineTextPlainFlowedClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "class not initialized");
+ oclass->parse_begin = MimeInlineTextPlainFlowed_parse_begin;
+ oclass->parse_line = MimeInlineTextPlainFlowed_parse_line;
+ oclass->parse_eof = MimeInlineTextPlainFlowed_parse_eof;
+
+ return 0;
+}
+
+static int MimeInlineTextPlainFlowed_parse_begin(MimeObject* obj) {
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ status = MimeObject_write(obj, "", 0, true); /* force out any separators... */
+ if (status < 0) return status;
+
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // The output will be
+ // inserted in the
+ // composer as quotation
+ bool plainHTML =
+ quoting || (obj->options && obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageSaveAs);
+ // Just good(tm) HTML. No reliance on CSS.
+
+ // Setup the data structure that is connected to the actual document
+ // Saved in a linked list in case this is called with several documents
+ // at the same time.
+ /* This memory is freed when parse_eof is called. So it better be! */
+ struct MimeInlineTextPlainFlowedExData* exdata =
+ (MimeInlineTextPlainFlowedExData*)PR_MALLOC(
+ sizeof(struct MimeInlineTextPlainFlowedExData));
+ if (!exdata) return MIME_OUT_OF_MEMORY;
+
+ MimeInlineTextPlainFlowed* text = (MimeInlineTextPlainFlowed*)obj;
+
+ // Link it up.
+ exdata->next = MimeInlineTextPlainFlowedExDataList;
+ MimeInlineTextPlainFlowedExDataList = exdata;
+
+ // Initialize data
+
+ exdata->ownerobj = obj;
+ exdata->inflow = false;
+ exdata->quotelevel = 0;
+ exdata->isSig = false;
+
+ // check for DelSp=yes (RFC 3676)
+
+ char* content_type_row =
+ (obj->headers
+ ? MimeHeaders_get(obj->headers, HEADER_CONTENT_TYPE, false, false)
+ : 0);
+ char* content_type_delsp =
+ (content_type_row
+ ? MimeHeaders_get_parameter(content_type_row, "delsp", NULL, NULL)
+ : 0);
+ ((MimeInlineTextPlainFlowed*)obj)->delSp =
+ content_type_delsp && !PL_strcasecmp(content_type_delsp, "yes");
+ PR_Free(content_type_delsp);
+ PR_Free(content_type_row);
+
+ // Get Prefs for viewing
+
+ exdata->fixedwidthfont = false;
+ // Quotes
+ text->mQuotedSizeSetting = 0; // mail.quoted_size
+ text->mQuotedStyleSetting = 0; // mail.quoted_style
+ text->mCitationColor.Truncate(); // mail.citation_color
+ text->mStripSig = true; // mail.strip_sig_on_reply
+
+ nsIPrefBranch* prefBranch = GetPrefBranch(obj->options);
+ if (prefBranch) {
+ prefBranch->GetIntPref("mail.quoted_size", &(text->mQuotedSizeSetting));
+ prefBranch->GetIntPref("mail.quoted_style", &(text->mQuotedStyleSetting));
+ prefBranch->GetCharPref("mail.citation_color", text->mCitationColor);
+ prefBranch->GetBoolPref("mail.strip_sig_on_reply", &(text->mStripSig));
+ mozilla::DebugOnly<nsresult> rv = prefBranch->GetBoolPref(
+ "mail.fixed_width_messages", &(exdata->fixedwidthfont));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to get pref");
+ // Check at least the success of one
+ }
+
+ // Get font
+ // only used for viewing (!plainHTML)
+ nsAutoCString fontstyle;
+ nsAutoCString fontLang; // langgroup of the font
+
+ // generic font-family name ( -moz-fixed for fixed font and NULL for
+ // variable font ) is sufficient now that bug 105199 has been fixed.
+
+ if (exdata->fixedwidthfont) fontstyle = "font-family: -moz-fixed";
+
+ if (nsMimeOutput::nsMimeMessageBodyDisplay == obj->options->format_out ||
+ nsMimeOutput::nsMimeMessagePrintOutput == obj->options->format_out) {
+ int32_t fontSize; // default font size
+ int32_t fontSizePercentage; // size percentage
+ nsresult rv = GetMailNewsFont(obj, exdata->fixedwidthfont, &fontSize,
+ &fontSizePercentage, fontLang);
+ if (NS_SUCCEEDED(rv)) {
+ if (!fontstyle.IsEmpty()) {
+ fontstyle += "; ";
+ }
+ fontstyle += "font-size: ";
+ fontstyle.AppendInt(fontSize);
+ fontstyle += "px;";
+ }
+ }
+
+ // Opening <div>.
+ if (!quoting)
+ /* 4.x' editor can't break <div>s (e.g. to interleave comments).
+ We'll add the class to the <blockquote type=cite> later. */
+ {
+ nsAutoCString openingDiv("<div class=\"moz-text-flowed\"");
+ // We currently have to add formatting here. :-(
+ if (!plainHTML && !fontstyle.IsEmpty()) {
+ openingDiv += " style=\"";
+ openingDiv += fontstyle;
+ openingDiv += '"';
+ }
+ if (!plainHTML && !fontLang.IsEmpty()) {
+ openingDiv += " lang=\"";
+ openingDiv += fontLang;
+ openingDiv += '\"';
+ }
+ openingDiv += ">";
+ status =
+ MimeObject_write(obj, openingDiv.get(), openingDiv.Length(), false);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeInlineTextPlainFlowed_parse_eof(MimeObject* obj, bool abort_p) {
+ int status = 0;
+ struct MimeInlineTextPlainFlowedExData* exdata = nullptr;
+
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // see above
+
+ // Has this method already been called for this object?
+ // In that case return.
+ if (obj->closed_p) return 0;
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) goto EarlyOut;
+
+ // Look up and unlink "our" extended data structure
+ // We do it in the beginning so that if an error occur, we can
+ // just free |exdata|.
+ struct MimeInlineTextPlainFlowedExData** prevexdata;
+ prevexdata = &MimeInlineTextPlainFlowedExDataList;
+
+ while ((exdata = *prevexdata) != nullptr) {
+ if (exdata->ownerobj == obj) {
+ // Fill hole
+ *prevexdata = exdata->next;
+ break;
+ }
+ prevexdata = &exdata->next;
+ }
+ NS_ASSERTION(exdata, "The extra data has disappeared!");
+
+ if (!obj->output_p) {
+ status = 0;
+ goto EarlyOut;
+ }
+
+ for (; exdata->quotelevel > 0; exdata->quotelevel--) {
+ status = MimeObject_write(obj, "</blockquote>", 13, false);
+ if (status < 0) goto EarlyOut;
+ }
+
+ if (exdata->isSig && !quoting) {
+ status = MimeObject_write(obj, "</div>", 6, false); // .moz-txt-sig
+ if (status < 0) goto EarlyOut;
+ }
+ if (!quoting) // HACK (see above)
+ {
+ status = MimeObject_write(obj, "</div>", 6, false); // .moz-text-flowed
+ if (status < 0) goto EarlyOut;
+ }
+
+ status = 0;
+
+EarlyOut:
+ PR_Free(exdata);
+
+ // Clear mCitationColor
+ MimeInlineTextPlainFlowed* text = (MimeInlineTextPlainFlowed*)obj;
+ text->mCitationColor.Truncate();
+
+ return status;
+}
+
+static int MimeInlineTextPlainFlowed_parse_line(const char* aLine,
+ int32_t length,
+ MimeObject* obj) {
+ int status;
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // see above
+ bool plainHTML =
+ quoting || (obj->options && obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageSaveAs);
+ // see above
+
+ struct MimeInlineTextPlainFlowedExData* exdata;
+ exdata = MimeInlineTextPlainFlowedExDataList;
+ while (exdata && (exdata->ownerobj != obj)) {
+ exdata = exdata->next;
+ }
+
+ NS_ASSERTION(exdata, "The extra data has disappeared!");
+
+ NS_ASSERTION(length > 0, "zero length");
+ if (length <= 0) return 0;
+
+ uint32_t linequotelevel = 0;
+ nsAutoCString real_line(aLine, length);
+ char* line = real_line.BeginWriting();
+ const char* linep = real_line.BeginReading();
+ // Space stuffed?
+ if (' ' == *linep) {
+ line++;
+ linep++;
+ length--;
+ } else {
+ // count '>':s before the first non-'>'
+ while ('>' == *linep) {
+ linep++;
+ linequotelevel++;
+ }
+ // Space stuffed?
+ if (' ' == *linep) {
+ linep++;
+ }
+ }
+
+ // Look if the last character (after stripping ending end
+ // of lines and quoting stuff) is a SPACE. If it is, we are looking at a
+ // flowed line. Normally we assume that the last two chars
+ // are CR and LF as said in RFC822, but that doesn't seem to
+ // be the case always.
+ bool flowed = false;
+ bool sigSeparator = false;
+ int32_t index = length - 1;
+ while (index >= 0 && ('\r' == line[index] || '\n' == line[index])) {
+ index--;
+ }
+ if (index > linep - line && ' ' == line[index])
+ /* Ignore space stuffing, i.e. lines with just
+ (quote marks and) a space count as empty */
+ {
+ flowed = true;
+ sigSeparator =
+ (index - (linep - line) + 1 == 3) && !strncmp(linep, "-- ", 3);
+ if (((MimeInlineTextPlainFlowed*)obj)->delSp && !sigSeparator)
+ /* If line is flowed and DelSp=yes, logically
+ delete trailing space. Line consisting of
+ dash dash space ("-- "), commonly used as
+ signature separator, gets special handling
+ (RFC 3676) */
+ {
+ length = index;
+ line[index] = '\0';
+ }
+ }
+
+ if (obj->options && obj->options->decompose_file_p &&
+ obj->options->decompose_file_output_fn) {
+ return obj->options->decompose_file_output_fn(line, length,
+ obj->options->stream_closure);
+ }
+
+ mozITXTToHTMLConv* conv = GetTextConverter(obj->options);
+
+ bool skipConversion =
+ !conv || (obj->options && obj->options->force_user_charset);
+
+ nsAutoString lineSource;
+ nsString lineResult;
+
+ char* mailCharset = NULL;
+ nsresult rv;
+
+ if (!skipConversion) {
+ // Convert only if the source string is not empty
+ if (length - (linep - line) > 0) {
+ uint32_t whattodo = obj->options->whattodo;
+ if (plainHTML) {
+ if (quoting)
+ whattodo = 0;
+ else
+ whattodo = whattodo & ~mozITXTToHTMLConv::kGlyphSubstitution;
+ /* Do recognition for the case, the result is viewed in
+ Mozilla, but not GlyphSubstitution, because other UAs
+ might not be able to display the glyphs. */
+ }
+
+ const nsDependentCSubstring& inputStr =
+ Substring(linep, linep + (length - (linep - line)));
+
+ // For 'SaveAs', |line| is in |mailCharset|.
+ // convert |line| to UTF-16 before 'html'izing (calling ScanTXT())
+ if (obj->options->format_out == nsMimeOutput::nsMimeMessageSaveAs) {
+ // Get the mail charset of this message.
+ MimeInlineText* inlinetext = (MimeInlineText*)obj;
+ if (!inlinetext->initializeCharset)
+ ((MimeInlineTextClass*)&mimeInlineTextClass)->initialize_charset(obj);
+ mailCharset = inlinetext->charset;
+ if (mailCharset && *mailCharset) {
+ rv = nsMsgI18NConvertToUnicode(nsDependentCString(mailCharset),
+ PromiseFlatCString(inputStr),
+ lineSource);
+ NS_ENSURE_SUCCESS(rv, -1);
+ } else // this probably never happens...
+ CopyUTF8toUTF16(inputStr, lineSource);
+ } else // line is in UTF-8
+ CopyUTF8toUTF16(inputStr, lineSource);
+
+ // This is the main TXT to HTML conversion:
+ // escaping (very important), eventually recognizing etc.
+ rv = conv->ScanTXT(lineSource, whattodo, lineResult);
+ NS_ENSURE_SUCCESS(rv, -1);
+ }
+ } else {
+ CopyUTF8toUTF16(nsDependentCString(line, length), lineResult);
+ status = 0;
+ }
+
+ nsAutoCString preface;
+
+ /* Correct number of blockquotes */
+ int32_t quoteleveldiff = linequotelevel - exdata->quotelevel;
+ if ((quoteleveldiff != 0) && flowed && exdata->inflow) {
+ // From RFC 2646 4.5
+ // The receiver SHOULD handle this error by using the 'quote-depth-wins'
+ // rule, which is to ignore the flowed indicator and treat the line as
+ // fixed. That is, the change in quote depth ends the paragraph.
+
+ // We get that behaviour by just going on.
+ }
+
+ // Cast so we have access to the prefs we need.
+ MimeInlineTextPlainFlowed* tObj = (MimeInlineTextPlainFlowed*)obj;
+ while (quoteleveldiff > 0) {
+ quoteleveldiff--;
+ preface += "<blockquote type=cite";
+
+ nsAutoCString style;
+ MimeTextBuildPrefixCSS(tObj->mQuotedSizeSetting, tObj->mQuotedStyleSetting,
+ tObj->mCitationColor, style);
+ if (!plainHTML && !style.IsEmpty()) {
+ preface += " style=\"";
+ preface += style;
+ preface += '"';
+ }
+ preface += '>';
+ }
+ while (quoteleveldiff < 0) {
+ quoteleveldiff++;
+ preface += "</blockquote>";
+ }
+ exdata->quotelevel = linequotelevel;
+
+ nsAutoString lineResult2;
+
+ if (flowed) {
+ // Check RFC 2646 "4.3. Usenet Signature Convention": "-- "+CRLF is
+ // not a flowed line
+ if (sigSeparator) {
+ if (linequotelevel > 0 || exdata->isSig) {
+ preface += "--&nbsp;<br>";
+ } else {
+ exdata->isSig = true;
+ preface +=
+ "<div class=\"moz-txt-sig\"><span class=\"moz-txt-tag\">"
+ "--&nbsp;<br></span>";
+ }
+ } else {
+ Line_convert_whitespace(lineResult, false /* Allow wraps */, lineResult2);
+ }
+
+ exdata->inflow = true;
+ } else {
+ // Fixed paragraph.
+ Line_convert_whitespace(lineResult,
+ !plainHTML && !obj->options->wrap_long_lines_p
+ /* If wrap, convert all spaces but the last in
+ a row into nbsp, otherwise all. */
+ ,
+ lineResult2);
+ lineResult2.AppendLiteral("<br>");
+ exdata->inflow = false;
+ } // End Fixed line
+
+ if (!(exdata->isSig && quoting && tObj->mStripSig)) {
+ status = MimeObject_write(obj, preface.get(), preface.Length(), true);
+ if (status < 0) return status;
+ nsAutoCString outString;
+ if (obj->options->format_out != nsMimeOutput::nsMimeMessageSaveAs ||
+ !mailCharset || !*mailCharset)
+ CopyUTF16toUTF8(lineResult2, outString);
+ else { // convert back to mailCharset before writing.
+ rv = nsMsgI18NConvertFromUnicode(nsDependentCString(mailCharset),
+ lineResult2, outString);
+ NS_ENSURE_SUCCESS(rv, -1);
+ }
+ status = MimeObject_write(obj, outString.get(), outString.Length(), true);
+ return status;
+ }
+ return 0;
+}
+
+/**
+ * Maintains a small state machine with three states. "Not in tag",
+ * "In tag, but not in quote" and "In quote inside a tag". It also
+ * remembers what character started the quote (" or '). The state
+ * variables are kept outside this function and are included as
+ * parameters.
+ *
+ * @param in/out a_in_tag, if we are in a tag right now.
+ * @param in/out a_in_quote_in_tag, if we are in a quote inside a tag.
+ * @param in/out a_quote_char, the kind of quote (" or ').
+ * @param in a_current_char, the next char. It decides which state
+ * will be next.
+ */
+static void Update_in_tag_info(
+ bool* a_in_tag, /* IN/OUT */
+ bool* a_in_quote_in_tag, /* IN/OUT */
+ char16_t* a_quote_char, /* IN/OUT (pointer to single char) */
+ char16_t a_current_char) /* IN */
+{
+ if (*a_in_tag) {
+ // Keep us informed of what's quoted so that we
+ // don't end the tag too soon. For instance in
+ // <font face="weird>font<name">
+ if (*a_in_quote_in_tag) {
+ // We are in a quote. A quote is ended by the same
+ // character that started it ('...' or "...")
+ if (*a_quote_char == a_current_char) {
+ *a_in_quote_in_tag = false;
+ }
+ } else {
+ // We are not currently in a quote, but we may enter
+ // one right this minute.
+ switch (a_current_char) {
+ case '"':
+ case '\'':
+ *a_in_quote_in_tag = true;
+ *a_quote_char = a_current_char;
+ break;
+ case '>':
+ // Tag is ended
+ *a_in_tag = false;
+ break;
+ default:
+ // Do nothing
+ ;
+ }
+ }
+ return;
+ }
+
+ // Not in a tag.
+ // Check if we are entering a tag by looking for '<'.
+ // All normal occurrences of '<' should have been replaced
+ // by &lt;
+ if ('<' == a_current_char) {
+ *a_in_tag = true;
+ *a_in_quote_in_tag = false;
+ }
+}
+
+/**
+ * Converts whitespace to |&nbsp;|, if appropriate.
+ *
+ * @param in a_current_char, the char to convert.
+ * @param in a_next_char, the char after the char to convert.
+ * @param in a_convert_all_whitespace, if also the last whitespace
+ * in a sequence should be
+ * converted.
+ * @param out a_out_string, result will be appended.
+ */
+static void Convert_whitespace(const char16_t a_current_char,
+ const char16_t a_next_char,
+ const bool a_convert_all_whitespace,
+ nsString& a_out_string) {
+ NS_ASSERTION('\t' == a_current_char || ' ' == a_current_char,
+ "Convert_whitespace got something else than a whitespace!");
+
+ uint32_t number_of_nbsp = 0;
+ uint32_t number_of_space = 1; // Assume we're going to output one space.
+
+ /* Output the spaces for a tab. All but the last are made into &nbsp;.
+ The last is treated like a normal space.
+ */
+ if ('\t' == a_current_char) {
+ number_of_nbsp = kSpacesForATab - 1;
+ }
+
+ if (' ' == a_next_char || '\t' == a_next_char || a_convert_all_whitespace) {
+ number_of_nbsp += number_of_space;
+ number_of_space = 0;
+ }
+
+ while (number_of_nbsp--) {
+ a_out_string.AppendLiteral("&nbsp;");
+ }
+
+ while (number_of_space--) {
+ // a_out_string += ' '; gives error
+ a_out_string.Append(' ');
+ }
+
+ return;
+}
+
+/**
+ * Passes over the line and converts whitespace to |&nbsp;|, if appropriate
+ *
+ * @param in a_convert_all_whitespace, if also the last whitespace
+ * in a sequence should be
+ * converted.
+ * @param out a_out_string, result will be appended.
+ */
+static nsresult Line_convert_whitespace(const nsString& a_line,
+ const bool a_convert_all_whitespace,
+ nsString& a_out_line) {
+ bool in_tag = false;
+ bool in_quote_in_tag = false;
+ char16_t quote_char;
+
+ for (uint32_t i = 0; a_line.Length() > i; i++) {
+ const char16_t ic = a_line[i]; // Cache
+
+ Update_in_tag_info(&in_tag, &in_quote_in_tag, &quote_char, ic);
+ // We don't touch anything inside a tag.
+ if (!in_tag) {
+ if (ic == ' ' || ic == '\t') {
+ // Convert the whitespace to something appropriate
+ Convert_whitespace(
+ ic, a_line.Length() > i + 1 ? a_line[i + 1] : '\0',
+ a_convert_all_whitespace || !i, // First char on line
+ a_out_line);
+ } else if (ic == '\r') {
+ // strip CRs
+ } else {
+ a_out_line += ic;
+ }
+ } else {
+ // In tag. Don't change anything
+ a_out_line += ic;
+ }
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/src/mimetpfl.h b/comm/mailnews/mime/src/mimetpfl.h
new file mode 100644
index 0000000000..110808e63d
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetpfl.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETPFL_H_
+#define _MIMETPFL_H_
+
+#include "mimetext.h"
+
+/* The MimeInlineTextPlainFlowed class implements the
+ text/plain MIME content type for the special case of a supplied
+ format=flowed. See
+ ftp://ftp.ietf.org/internet-drafts/draft-gellens-format-06.txt for
+ more information.
+ */
+
+typedef struct MimeInlineTextPlainFlowedClass MimeInlineTextPlainFlowedClass;
+typedef struct MimeInlineTextPlainFlowed MimeInlineTextPlainFlowed;
+
+struct MimeInlineTextPlainFlowedClass {
+ MimeInlineTextClass text;
+};
+
+extern MimeInlineTextPlainFlowedClass mimeInlineTextPlainFlowedClass;
+
+struct MimeInlineTextPlainFlowed {
+ MimeInlineText text;
+ bool delSp; // DelSp=yes (RFC 3676)
+ int32_t mQuotedSizeSetting; // mail.quoted_size
+ int32_t mQuotedStyleSetting; // mail.quoted_style
+ nsCString mCitationColor; // mail.citation_color
+ bool mStripSig; // mail.strip_sig_on_reply
+};
+
+/*
+ * Made to contain information to be kept during the whole message parsing.
+ */
+struct MimeInlineTextPlainFlowedExData {
+ struct MimeObject* ownerobj; /* The owner of this struct */
+ bool inflow; /* If we currently are in flow */
+ bool fixedwidthfont; /* If we output text for fixed width font */
+ uint32_t quotelevel; /* How deep is your love, uhr, quotelevel I meen. */
+ bool isSig; // we're currently in a signature
+ struct MimeInlineTextPlainFlowedExData* next;
+};
+
+#define MimeInlineTextPlainFlowedClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETPFL_H_ */
diff --git a/comm/mailnews/mime/src/mimetpla.cpp b/comm/mailnews/mime/src/mimetpla.cpp
new file mode 100644
index 0000000000..081d7951a4
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetpla.cpp
@@ -0,0 +1,409 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimetpla.h"
+#include "mimebuf.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsMimeStringResources.h"
+#include "mimemoz2.h"
+#include "nsIPrefBranch.h"
+#include "prprf.h"
+#include "nsMsgI18N.h"
+
+#define MIME_SUPERCLASS mimeInlineTextClass
+MimeDefClass(MimeInlineTextPlain, MimeInlineTextPlainClass,
+ mimeInlineTextPlainClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextPlain_parse_begin(MimeObject*);
+static int MimeInlineTextPlain_parse_line(const char*, int32_t, MimeObject*);
+static int MimeInlineTextPlain_parse_eof(MimeObject*, bool);
+
+static int MimeInlineTextPlainClassInitialize(MimeInlineTextPlainClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ NS_ASSERTION(!oclass->class_initialized, "class not initialized");
+ oclass->parse_begin = MimeInlineTextPlain_parse_begin;
+ oclass->parse_line = MimeInlineTextPlain_parse_line;
+ oclass->parse_eof = MimeInlineTextPlain_parse_eof;
+ return 0;
+}
+
+extern "C" void MimeTextBuildPrefixCSS(
+ int32_t quotedSizeSetting, // mail.quoted_size
+ int32_t quotedStyleSetting, // mail.quoted_style
+ nsACString& citationColor, // mail.citation_color
+ nsACString& style) {
+ switch (quotedStyleSetting) {
+ case 0: // regular
+ break;
+ case 1: // bold
+ style.AppendLiteral("font-weight: bold; ");
+ break;
+ case 2: // italic
+ style.AppendLiteral("font-style: italic; ");
+ break;
+ case 3: // bold-italic
+ style.AppendLiteral("font-weight: bold; font-style: italic; ");
+ break;
+ }
+
+ switch (quotedSizeSetting) {
+ case 0: // regular
+ break;
+ case 1: // large
+ style.AppendLiteral("font-size: large; ");
+ break;
+ case 2: // small
+ style.AppendLiteral("font-size: small; ");
+ break;
+ }
+
+ if (!citationColor.IsEmpty()) {
+ style += "color: ";
+ style += citationColor;
+ style += ';';
+ }
+}
+
+static int MimeInlineTextPlain_parse_begin(MimeObject* obj) {
+ int status = 0;
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // The output will be
+ // inserted in the
+ // composer as quotation
+ bool plainHTML =
+ quoting || (obj->options && (obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageSaveAs));
+ // Just good(tm) HTML. No reliance on CSS.
+ bool rawPlainText =
+ obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageFilterSniffer ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach);
+
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ if (status < 0) return status;
+
+ if (!obj->output_p) return 0;
+
+ if (obj->options && obj->options->write_html_p && obj->options->output_fn) {
+ MimeInlineTextPlain* text = (MimeInlineTextPlain*)obj;
+ text->mCiteLevel = 0;
+
+ // Get the prefs
+
+ // Quoting
+ text->mBlockquoting = true; // mail.quoteasblock
+
+ // Viewing
+ text->mQuotedSizeSetting = 0; // mail.quoted_size
+ text->mQuotedStyleSetting = 0; // mail.quoted_style
+ text->mCitationColor.Truncate(); // mail.citation_color
+ text->mStripSig = true; // mail.strip_sig_on_reply
+ bool graphicalQuote = true; // mail.quoted_graphical
+
+ nsIPrefBranch* prefBranch = GetPrefBranch(obj->options);
+ if (prefBranch) {
+ prefBranch->GetIntPref("mail.quoted_size", &(text->mQuotedSizeSetting));
+ prefBranch->GetIntPref("mail.quoted_style", &(text->mQuotedStyleSetting));
+ prefBranch->GetCharPref("mail.citation_color", text->mCitationColor);
+ prefBranch->GetBoolPref("mail.strip_sig_on_reply", &(text->mStripSig));
+ prefBranch->GetBoolPref("mail.quoted_graphical", &graphicalQuote);
+ prefBranch->GetBoolPref("mail.quoteasblock", &(text->mBlockquoting));
+ }
+
+ if (!rawPlainText) {
+ // Get font
+ // only used for viewing (!plainHTML)
+ nsAutoCString fontstyle;
+ nsAutoCString fontLang; // langgroup of the font
+
+ // generic font-family name ( -moz-fixed for fixed font and NULL for
+ // variable font ) is sufficient now that bug 105199 has been fixed.
+
+ if (!obj->options->variable_width_plaintext_p)
+ fontstyle = "font-family: -moz-fixed";
+
+ if (nsMimeOutput::nsMimeMessageBodyDisplay == obj->options->format_out ||
+ nsMimeOutput::nsMimeMessagePrintOutput == obj->options->format_out) {
+ int32_t fontSize; // default font size
+ int32_t fontSizePercentage; // size percentage
+ nsresult rv =
+ GetMailNewsFont(obj, !obj->options->variable_width_plaintext_p,
+ &fontSize, &fontSizePercentage, fontLang);
+ if (NS_SUCCEEDED(rv)) {
+ if (!fontstyle.IsEmpty()) {
+ fontstyle += "; ";
+ }
+ fontstyle += "font-size: ";
+ fontstyle.AppendInt(fontSize);
+ fontstyle += "px;";
+ }
+ }
+
+ // Opening <div>. We currently have to add formatting here. :-(
+ nsAutoCString openingDiv;
+ if (!quoting)
+ /* 4.x' editor can't break <div>s (e.g. to interleave comments).
+ We'll add the class to the <blockquote type=cite> later. */
+ {
+ openingDiv = "<div class=\"moz-text-plain\"";
+ if (!plainHTML) {
+ if (obj->options->wrap_long_lines_p)
+ openingDiv += " wrap=true";
+ else
+ openingDiv += " wrap=false";
+
+ if (graphicalQuote)
+ openingDiv += " graphical-quote=true";
+ else
+ openingDiv += " graphical-quote=false";
+
+ if (!fontstyle.IsEmpty()) {
+ openingDiv += " style=\"";
+ openingDiv += fontstyle;
+ openingDiv += '\"';
+ }
+ if (!fontLang.IsEmpty()) {
+ openingDiv += " lang=\"";
+ openingDiv += fontLang;
+ openingDiv += '\"';
+ }
+ }
+ openingDiv += "><pre wrap class=\"moz-quote-pre\">\n";
+ } else
+ openingDiv = "<pre wrap class=\"moz-quote-pre\">\n";
+
+ /* text/plain objects always have separators before and after them.
+ Note that this is not the case for text/enriched objects. */
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+
+ status =
+ MimeObject_write(obj, openingDiv.get(), openingDiv.Length(), true);
+ if (status < 0) return status;
+ }
+ }
+
+ return 0;
+}
+
+static int MimeInlineTextPlain_parse_eof(MimeObject* obj, bool abort_p) {
+ int status;
+
+ // Has this method already been called for this object?
+ // In that case return.
+ if (obj->closed_p) return 0;
+
+ nsCString citationColor;
+ MimeInlineTextPlain* text = (MimeInlineTextPlain*)obj;
+ if (text && !text->mCitationColor.IsEmpty())
+ citationColor = text->mCitationColor;
+
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // see above
+
+ bool rawPlainText =
+ obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageFilterSniffer ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach);
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ if (!obj->output_p) return 0;
+
+ if (obj->options && obj->options->write_html_p && obj->options->output_fn &&
+ !abort_p && !rawPlainText) {
+ MimeInlineTextPlain* text = (MimeInlineTextPlain*)obj;
+ if (text->mIsSig && !quoting) {
+ status = MimeObject_write(obj, "</div>", 6, false); // .moz-txt-sig
+ if (status < 0) return status;
+ }
+ status = MimeObject_write(obj, "</pre>", 6, false);
+ if (status < 0) return status;
+ if (!quoting) {
+ status = MimeObject_write(obj, "</div>", 6, false);
+ // .moz-text-plain
+ if (status < 0) return status;
+ }
+
+ /* text/plain objects always have separators before and after them.
+ Note that this is not the case for text/enriched objects.
+ */
+ status = MimeObject_write_separator(obj);
+ if (status < 0) return status;
+ }
+
+ return 0;
+}
+
+static int MimeInlineTextPlain_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ int status;
+ bool quoting =
+ (obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageQuoting ||
+ obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageBodyQuoting)); // see above
+ bool plainHTML =
+ quoting || (obj->options && obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageSaveAs);
+ // see above
+
+ bool rawPlainText =
+ obj->options &&
+ (obj->options->format_out == nsMimeOutput::nsMimeMessageFilterSniffer ||
+ obj->options->format_out == nsMimeOutput::nsMimeMessageAttach);
+
+ // this routine gets called for every line of data that comes through the
+ // mime converter. It's important to make sure we are efficient with
+ // how we allocate memory in this routine. be careful if you go to add
+ // more to this routine.
+
+ NS_ASSERTION(length > 0, "zero length");
+ if (length <= 0) return 0;
+
+ mozITXTToHTMLConv* conv = GetTextConverter(obj->options);
+ MimeInlineTextPlain* text = (MimeInlineTextPlain*)obj;
+
+ bool skipConversion = !conv || rawPlainText ||
+ (obj->options && obj->options->force_user_charset);
+
+ char* mailCharset = NULL;
+ nsresult rv;
+
+ if (!skipConversion) {
+ nsDependentCSubstring inputStr(line, length);
+ nsAutoString lineSourceStr;
+
+ // For 'SaveAs', |line| is in |mailCharset|.
+ // convert |line| to UTF-16 before 'html'izing (calling ScanTXT())
+ if (obj->options->format_out ==
+ nsMimeOutput::nsMimeMessageSaveAs) { // Get the mail charset of this
+ // message.
+ MimeInlineText* inlinetext = (MimeInlineText*)obj;
+ if (!inlinetext->initializeCharset)
+ ((MimeInlineTextClass*)&mimeInlineTextClass)->initialize_charset(obj);
+ mailCharset = inlinetext->charset;
+ if (mailCharset && *mailCharset) {
+ rv = nsMsgI18NConvertToUnicode(nsDependentCString(mailCharset),
+ inputStr, lineSourceStr);
+ NS_ENSURE_SUCCESS(rv, -1);
+ } else // this probably never happens ...
+ CopyUTF8toUTF16(inputStr, lineSourceStr);
+ } else // line is in UTF-8
+ CopyUTF8toUTF16(inputStr, lineSourceStr);
+
+ nsAutoCString prefaceResultStr; // Quoting stuff before the real text
+
+ // Recognize quotes
+ uint32_t oldCiteLevel = text->mCiteLevel;
+ uint32_t logicalLineStart = 0;
+ rv = conv->CiteLevelTXT(lineSourceStr.get(), &logicalLineStart,
+ &(text->mCiteLevel));
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ // Find out, which recognitions to do
+ uint32_t whattodo = obj->options->whattodo;
+ if (plainHTML) {
+ if (quoting)
+ whattodo = 0; // This is done on Send. Don't do it twice.
+ else
+ whattodo = whattodo & ~mozITXTToHTMLConv::kGlyphSubstitution;
+ /* Do recognition for the case, the result is viewed in
+ Mozilla, but not GlyphSubstitution, because other UAs
+ might not be able to display the glyphs. */
+ if (!text->mBlockquoting) text->mCiteLevel = 0;
+ }
+
+ // Write blockquote
+ if (text->mCiteLevel > oldCiteLevel) {
+ prefaceResultStr += "</pre>";
+ for (uint32_t i = 0; i < text->mCiteLevel - oldCiteLevel; i++) {
+ nsAutoCString style;
+ MimeTextBuildPrefixCSS(text->mQuotedSizeSetting,
+ text->mQuotedStyleSetting, text->mCitationColor,
+ style);
+ if (!plainHTML && !style.IsEmpty()) {
+ prefaceResultStr += "<blockquote type=cite style=\"";
+ prefaceResultStr += style;
+ prefaceResultStr += "\">";
+ } else
+ prefaceResultStr += "<blockquote type=cite>";
+ }
+ prefaceResultStr += "<pre wrap class=\"moz-quote-pre\">\n";
+ } else if (text->mCiteLevel < oldCiteLevel) {
+ prefaceResultStr += "</pre>";
+ for (uint32_t i = 0; i < oldCiteLevel - text->mCiteLevel; i++)
+ prefaceResultStr += "</blockquote>";
+ prefaceResultStr += "<pre wrap class=\"moz-quote-pre\">\n";
+ }
+
+ // Write plain text quoting tags
+ if (logicalLineStart != 0 && !(plainHTML && text->mBlockquoting)) {
+ if (!plainHTML) prefaceResultStr += "<span class=\"moz-txt-citetags\">";
+
+ nsString citeTagsSource(StringHead(lineSourceStr, logicalLineStart));
+
+ // Convert to HTML
+ nsString citeTagsResultUnichar;
+ rv = conv->ScanTXT(citeTagsSource, 0 /* no recognition */,
+ citeTagsResultUnichar);
+ if (NS_FAILED(rv)) return -1;
+
+ prefaceResultStr.Append(NS_ConvertUTF16toUTF8(citeTagsResultUnichar));
+ if (!plainHTML) prefaceResultStr += "</span>";
+ }
+
+ // recognize signature
+ if ((lineSourceStr.Length() >= 4) && lineSourceStr.First() == '-' &&
+ Substring(lineSourceStr, 0, 3).EqualsLiteral("-- ") &&
+ (lineSourceStr[3] == '\r' || lineSourceStr[3] == '\n')) {
+ text->mIsSig = true;
+ if (!quoting) prefaceResultStr += "<div class=\"moz-txt-sig\">";
+ }
+
+ /* This is the main TXT to HTML conversion:
+ escaping (very important), eventually recognizing etc. */
+ nsString lineResultUnichar;
+
+ rv = conv->ScanTXT(Substring(lineSourceStr, logicalLineStart), whattodo,
+ lineResultUnichar);
+ NS_ENSURE_SUCCESS(rv, -1);
+
+ if (!(text->mIsSig && quoting && text->mStripSig)) {
+ status = MimeObject_write(obj, prefaceResultStr.get(),
+ prefaceResultStr.Length(), true);
+ if (status < 0) return status;
+ nsAutoCString outString;
+ if (obj->options->format_out != nsMimeOutput::nsMimeMessageSaveAs ||
+ !mailCharset || !*mailCharset)
+ CopyUTF16toUTF8(lineResultUnichar, outString);
+ else { // convert back to mailCharset before writing.
+ rv = nsMsgI18NConvertFromUnicode(nsDependentCString(mailCharset),
+ lineResultUnichar, outString);
+ NS_ENSURE_SUCCESS(rv, -1);
+ }
+
+ status = MimeObject_write(obj, outString.get(), outString.Length(), true);
+ } else {
+ status = 0;
+ }
+ } else {
+ status = MimeObject_write(obj, line, length, true);
+ }
+
+ return status;
+}
diff --git a/comm/mailnews/mime/src/mimetpla.h b/comm/mailnews/mime/src/mimetpla.h
new file mode 100644
index 0000000000..de70a277ec
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetpla.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/* The MimeInlineTextPlain class implements the text/plain MIME content type,
+ and is also used for all otherwise-unknown text/ subtypes.
+ */
+
+#ifndef _MIMETPLA_H_
+#define _MIMETPLA_H_
+
+#include "mimetext.h"
+
+typedef struct MimeInlineTextPlainClass MimeInlineTextPlainClass;
+typedef struct MimeInlineTextPlain MimeInlineTextPlain;
+
+struct MimeInlineTextPlainClass {
+ MimeInlineTextClass text;
+};
+
+extern MimeInlineTextPlainClass mimeInlineTextPlainClass;
+
+struct MimeInlineTextPlain {
+ MimeInlineText text;
+ uint32_t mCiteLevel;
+ bool mBlockquoting;
+ // bool mInsideQuote;
+ int32_t mQuotedSizeSetting; // mail.quoted_size
+ int32_t mQuotedStyleSetting; // mail.quoted_style
+ nsCString mCitationColor; // mail.citation_color
+ bool mStripSig; // mail.strip_sig_on_reply
+ bool mIsSig;
+};
+
+#define MimeInlineTextPlainClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETPLA_H_ */
diff --git a/comm/mailnews/mime/src/mimetric.cpp b/comm/mailnews/mime/src/mimetric.cpp
new file mode 100644
index 0000000000..79eedea75d
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetric.cpp
@@ -0,0 +1,356 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "mimetric.h"
+#include "mimebuf.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "msgCore.h"
+#include <ctype.h>
+
+#define MIME_SUPERCLASS mimeInlineTextClass
+MimeDefClass(MimeInlineTextRichtext, MimeInlineTextRichtextClass,
+ mimeInlineTextRichtextClass, &MIME_SUPERCLASS);
+
+static int MimeInlineTextRichtext_parse_line(const char*, int32_t, MimeObject*);
+static int MimeInlineTextRichtext_parse_begin(MimeObject*);
+static int MimeInlineTextRichtext_parse_eof(MimeObject*, bool);
+
+static int MimeInlineTextRichtextClassInitialize(
+ MimeInlineTextRichtextClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->parse_begin = MimeInlineTextRichtext_parse_begin;
+ oclass->parse_line = MimeInlineTextRichtext_parse_line;
+ oclass->parse_eof = MimeInlineTextRichtext_parse_eof;
+ return 0;
+}
+
+/* This function has this clunky interface because it needs to be called
+ from outside this module (no MimeObject, etc.)
+ */
+int MimeRichtextConvert(const char* line, int32_t length, MimeObject* obj,
+ char** obufferP, int32_t* obuffer_sizeP,
+ bool enriched_p) {
+ /* RFC 1341 (the original MIME spec) defined text/richtext.
+ RFC 1563 superseded text/richtext with text/enriched.
+ The changes from text/richtext to text/enriched are:
+ - CRLF semantics are different
+ - << maps to <
+ - These tags were added:
+ <VERBATIM>, <NOFILL>, <PARAM>, <FLUSHBOTH>
+ - These tags were removed:
+ <COMMENT>, <OUTDENT>, <OUTDENTRIGHT>, <SAMEPAGE>, <SUBSCRIPT>,
+ <SUPERSCRIPT>, <HEADING>, <FOOTING>, <PARAGRAPH>, <SIGNATURE>,
+ <LT>, <NL>, <NP>
+ This method implements them both.
+
+ draft-resnick-text-enriched-03.txt is a proposed update to 1563.
+ - These tags were added:
+ <FONTFAMILY>, <COLOR>, <PARAINDENT>, <LANG>.
+ However, all of these rely on the magic <PARAM> tag, which we
+ don't implement, so we're ignoring all of these.
+ Interesting fact: it's by Peter W. Resnick from Qualcomm (Eudora).
+ And it also says "It is fully expected that other text formatting
+ standards like HTML and SGML will supplant text/enriched in
+ Internet mail."
+ */
+ int status = 0;
+ char* out;
+ const char* data_end;
+ const char* last_end;
+ const char* this_start;
+ const char* this_end;
+ unsigned int desired_size;
+
+ // The code below must never expand the input by more than 5x;
+ // if it does, the desired_size multiplier (5) below must be changed too
+#define BGROWTH 5
+ if ((uint32_t)length >= ((uint32_t)0xfffffffe) / BGROWTH) return -1;
+ desired_size = (length * BGROWTH) + 1;
+#undef BGROWTH
+ if (desired_size >= (uint32_t)*obuffer_sizeP)
+ status = mime_GrowBuffer(desired_size, sizeof(char), 1024, obufferP,
+ obuffer_sizeP);
+ if (status < 0) return status;
+
+ if (enriched_p) {
+ for (this_start = line; this_start < line + length; this_start++)
+ if (!IS_SPACE(*this_start)) break;
+ if (this_start >= line + length) /* blank line */
+ {
+ PL_strncpyz(*obufferP, "<BR>", *obuffer_sizeP);
+ return MimeObject_write(obj, *obufferP, strlen(*obufferP), true);
+ }
+ }
+
+ uint32_t outlen = (uint32_t)*obuffer_sizeP;
+ out = *obufferP;
+ *out = 0;
+
+ data_end = line + length;
+ last_end = line;
+ this_start = last_end;
+ this_end = this_start;
+ uint32_t addedlen = 0;
+ while (this_end < data_end) {
+ /* Skip forward to next special character. */
+ while (this_start < data_end && *this_start != '<' && *this_start != '>' &&
+ *this_start != '&')
+ this_start++;
+
+ this_end = this_start;
+
+ /* Skip to the end of the tag. */
+ if (this_start < data_end && *this_start == '<') {
+ this_end++;
+ while (this_end < data_end && !IS_SPACE(*this_end) && *this_end != '<' &&
+ *this_end != '>' && *this_end != '&')
+ this_end++;
+ }
+
+ this_end++;
+
+ /* Push out the text preceding the tag. */
+ if (last_end && last_end != this_start) {
+ memcpy(out, last_end, this_start - last_end);
+ out += this_start - last_end;
+ *out = 0;
+ outlen -= (this_start - last_end);
+ }
+
+ if (this_start >= data_end)
+ break;
+ else if (*this_start == '&') {
+ PL_strncpyz(out, "&amp;", outlen);
+ addedlen = strlen(out);
+ outlen -= addedlen;
+ out += addedlen;
+ } else if (*this_start == '>') {
+ PL_strncpyz(out, "&gt;", outlen);
+ addedlen = strlen(out);
+ outlen -= addedlen;
+ out += addedlen;
+ } else if (enriched_p && this_start < data_end + 1 &&
+ this_start[0] == '<' && this_start[1] == '<') {
+ PL_strncpyz(out, "&lt;", outlen);
+ addedlen = strlen(out);
+ outlen -= addedlen;
+ out += addedlen;
+ } else if (this_start != this_end) {
+ /* Push out this ID. */
+ const char* old = this_start + 1;
+ const char* tag_open = 0;
+ const char* tag_close = 0;
+ if (*old == '/') {
+ /* This is </tag> */
+ old++;
+ }
+
+ switch (*old) {
+ case 'b':
+ case 'B':
+ if (!PL_strncasecmp("BIGGER>", old, 7)) {
+ tag_open = "<FONT SIZE=\"+1\">";
+ tag_close = "</FONT>";
+ } else if (!PL_strncasecmp("BLINK>", old, 6))
+ // Of course, both text/richtext and text/enriched must be
+ // enhanced *somehow*... Or else what would people think.
+ {
+ tag_open = "<BLINK>";
+ tag_close = "</BLINK>";
+ } else if (!PL_strncasecmp("BOLD>", old, 5)) {
+ tag_open = "<B>";
+ tag_close = "</B>";
+ }
+ break;
+
+ case 'c':
+ case 'C':
+ if (!PL_strncasecmp("CENTER>", old, 7)) {
+ tag_open = "<CENTER>";
+ tag_close = "</CENTER>";
+ } else if (!enriched_p && !PL_strncasecmp("COMMENT>", old, 8)) {
+ tag_open = "<!-- ";
+ tag_close = " -->";
+ }
+ break;
+
+ case 'e':
+ case 'E':
+ if (!PL_strncasecmp("EXCERPT>", old, 8)) {
+ tag_open = "<BLOCKQUOTE>";
+ tag_close = "</BLOCKQUOTE>";
+ }
+ break;
+
+ case 'f':
+ case 'F':
+ if (!PL_strncasecmp("FIXED>", old, 6)) {
+ tag_open = "<TT>";
+ tag_close = "</TT>";
+ } else if (enriched_p && !PL_strncasecmp("FLUSHBOTH>", old, 10)) {
+ tag_open = "<P ALIGN=JUSTIFY>";
+ tag_close = "</P>";
+ } else if (!PL_strncasecmp("FLUSHLEFT>", old, 10)) {
+ tag_open = "<P ALIGN=LEFT>";
+ tag_close = "</P>";
+ } else if (!PL_strncasecmp("FLUSHRIGHT>", old, 11)) {
+ tag_open = "<P ALIGN=RIGHT>";
+ tag_close = "</P>";
+ } else if (!enriched_p && !PL_strncasecmp("FOOTING>", old, 8)) {
+ tag_open = "<H6>";
+ tag_close = "</H6>";
+ }
+ break;
+
+ case 'h':
+ case 'H':
+ if (!enriched_p && !PL_strncasecmp("HEADING>", old, 8)) {
+ tag_open = "<H6>";
+ tag_close = "</H6>";
+ }
+ break;
+
+ case 'i':
+ case 'I':
+ if (!PL_strncasecmp("INDENT>", old, 7)) {
+ tag_open = "<UL>";
+ tag_close = "</UL>";
+ } else if (!PL_strncasecmp("INDENTRIGHT>", old, 12)) {
+ tag_open = 0;
+ tag_close = 0;
+ } else if (!PL_strncasecmp("ITALIC>", old, 7)) {
+ tag_open = "<I>";
+ tag_close = "</I>";
+ }
+ break;
+
+ case 'l':
+ case 'L':
+ if (!enriched_p && !PL_strncasecmp("LT>", old, 3)) {
+ tag_open = "&lt;";
+ tag_close = 0;
+ }
+ break;
+
+ case 'n':
+ case 'N':
+ if (!enriched_p && !PL_strncasecmp("NL>", old, 3)) {
+ tag_open = "<BR>";
+ tag_close = 0;
+ }
+ if (enriched_p && !PL_strncasecmp("NOFILL>", old, 7)) {
+ tag_open = "<NOBR>";
+ tag_close = "</NOBR>";
+ }
+ break;
+
+ case 'o':
+ case 'O':
+ if (!enriched_p && !PL_strncasecmp("OUTDENT>", old, 8)) {
+ tag_open = 0;
+ tag_close = 0;
+ } else if (!enriched_p && !PL_strncasecmp("OUTDENTRIGHT>", old, 13)) {
+ tag_open = 0;
+ tag_close = 0;
+ }
+ break;
+
+ case 'p':
+ case 'P':
+ if (enriched_p && !PL_strncasecmp("PARAM>", old, 6)) {
+ tag_open = "<!-- ";
+ tag_close = " -->";
+ } else if (!enriched_p && !PL_strncasecmp("PARAGRAPH>", old, 10)) {
+ tag_open = "<P>";
+ tag_close = 0;
+ }
+ break;
+
+ case 's':
+ case 'S':
+ if (!enriched_p && !PL_strncasecmp("SAMEPAGE>", old, 9)) {
+ tag_open = 0;
+ tag_close = 0;
+ } else if (!enriched_p && !PL_strncasecmp("SIGNATURE>", old, 10)) {
+ tag_open = "<I><FONT SIZE=\"-1\">";
+ tag_close = "</FONT></I>";
+ } else if (!PL_strncasecmp("SMALLER>", old, 8)) {
+ tag_open = "<FONT SIZE=\"-1\">";
+ tag_close = "</FONT>";
+ } else if (!enriched_p && !PL_strncasecmp("SUBSCRIPT>", old, 10)) {
+ tag_open = "<SUB>";
+ tag_close = "</SUB>";
+ } else if (!enriched_p && !PL_strncasecmp("SUPERSCRIPT>", old, 12)) {
+ tag_open = "<SUP>";
+ tag_close = "</SUP>";
+ }
+ break;
+
+ case 'u':
+ case 'U':
+ if (!PL_strncasecmp("UNDERLINE>", old, 10)) {
+ tag_open = "<U>";
+ tag_close = "</U>";
+ }
+ break;
+
+ case 'v':
+ case 'V':
+ if (enriched_p && !PL_strncasecmp("VERBATIM>", old, 9)) {
+ tag_open = "<PRE>";
+ tag_close = "</PRE>";
+ }
+ break;
+ }
+
+ if (this_start[1] == '/') {
+ if (tag_close) PL_strncpyz(out, tag_close, outlen);
+ addedlen = strlen(out);
+ outlen -= addedlen;
+ out += addedlen;
+ } else {
+ if (tag_open) PL_strncpyz(out, tag_open, outlen);
+ addedlen = strlen(out);
+ outlen -= addedlen;
+ out += addedlen;
+ }
+ }
+
+ /* now go around again */
+ last_end = this_end;
+ this_start = last_end;
+ }
+ *out = 0;
+
+ return MimeObject_write(obj, *obufferP, out - *obufferP, true);
+}
+
+static int MimeInlineTextRichtext_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ bool enriched_p = (((MimeInlineTextRichtextClass*)obj->clazz)->enriched_p);
+
+ return MimeRichtextConvert(line, length, obj, &obj->obuffer,
+ &obj->obuffer_size, enriched_p);
+}
+
+static int MimeInlineTextRichtext_parse_begin(MimeObject* obj) {
+ int status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+ char s[] = "";
+ if (status < 0) return status;
+ return MimeObject_write(obj, s, 0, true); /* force out any separators... */
+}
+
+static int MimeInlineTextRichtext_parse_eof(MimeObject* obj, bool abort_p) {
+ int status;
+ if (obj->closed_p) return 0;
+
+ /* Run parent method first, to flush out any buffered data. */
+ status = ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/src/mimetric.h b/comm/mailnews/mime/src/mimetric.h
new file mode 100644
index 0000000000..6ec0961aed
--- /dev/null
+++ b/comm/mailnews/mime/src/mimetric.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMETRIC_H_
+#define _MIMETRIC_H_
+
+#include "mimetext.h"
+
+/* The MimeInlineTextRichtext class implements the (obsolete and deprecated)
+ text/richtext MIME content type, as defined in RFC 1341, and also the
+ text/enriched MIME content type, as defined in RFC 1563.
+ */
+
+typedef struct MimeInlineTextRichtextClass MimeInlineTextRichtextClass;
+typedef struct MimeInlineTextRichtext MimeInlineTextRichtext;
+
+struct MimeInlineTextRichtextClass {
+ MimeInlineTextClass text;
+ bool enriched_p; /* Whether we should act like text/enriched instead. */
+};
+
+extern MimeInlineTextRichtextClass mimeInlineTextRichtextClass;
+
+struct MimeInlineTextRichtext {
+ MimeInlineText text;
+};
+
+#define MimeInlineTextRichtextClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMETRIC_H_ */
diff --git a/comm/mailnews/mime/src/mimeunty.cpp b/comm/mailnews/mime/src/mimeunty.cpp
new file mode 100644
index 0000000000..6cdea05268
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeunty.cpp
@@ -0,0 +1,523 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimeunty.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "prlog.h"
+#include "nsMimeTypes.h"
+#include "msgCore.h"
+#include "nsMimeStringResources.h"
+#include <ctype.h>
+
+#define MIME_SUPERCLASS mimeContainerClass
+MimeDefClass(MimeUntypedText, MimeUntypedTextClass, mimeUntypedTextClass,
+ &MIME_SUPERCLASS);
+
+static int MimeUntypedText_initialize(MimeObject*);
+static void MimeUntypedText_finalize(MimeObject*);
+static int MimeUntypedText_parse_begin(MimeObject*);
+static int MimeUntypedText_parse_line(const char*, int32_t, MimeObject*);
+
+static int MimeUntypedText_open_subpart(MimeObject* obj,
+ MimeUntypedTextSubpartType ttype,
+ const char* type, const char* enc,
+ const char* name, const char* desc);
+static int MimeUntypedText_close_subpart(MimeObject* obj);
+
+static bool MimeUntypedText_uu_begin_line_p(const char* line, int32_t length,
+ MimeDisplayOptions* opt,
+ char** type_ret, char** name_ret);
+static bool MimeUntypedText_uu_end_line_p(const char* line, int32_t length);
+
+static bool MimeUntypedText_yenc_begin_line_p(const char* line, int32_t length,
+ MimeDisplayOptions* opt,
+ char** type_ret, char** name_ret);
+static bool MimeUntypedText_yenc_end_line_p(const char* line, int32_t length);
+
+static bool MimeUntypedText_binhex_begin_line_p(const char* line,
+ int32_t length,
+ MimeDisplayOptions* opt);
+static bool MimeUntypedText_binhex_end_line_p(const char* line, int32_t length);
+
+static int MimeUntypedTextClassInitialize(MimeUntypedTextClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ PR_ASSERT(!oclass->class_initialized);
+ oclass->initialize = MimeUntypedText_initialize;
+ oclass->finalize = MimeUntypedText_finalize;
+ oclass->parse_begin = MimeUntypedText_parse_begin;
+ oclass->parse_line = MimeUntypedText_parse_line;
+ return 0;
+}
+
+static int MimeUntypedText_initialize(MimeObject* object) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->initialize(object);
+}
+
+static void MimeUntypedText_finalize(MimeObject* object) {
+ MimeUntypedText* uty = (MimeUntypedText*)object;
+
+ if (uty->open_hdrs) {
+ /* Oops, those shouldn't still be here... */
+ MimeHeaders_free(uty->open_hdrs);
+ uty->open_hdrs = 0;
+ }
+
+ /* What about the open_subpart? We're going to have to assume that it
+ is also on the MimeContainer->children list, and will get cleaned
+ up by that class. */
+
+ ((MimeObjectClass*)&MIME_SUPERCLASS)->finalize(object);
+}
+
+static int MimeUntypedText_parse_begin(MimeObject* obj) {
+ return ((MimeObjectClass*)&MIME_SUPERCLASS)->parse_begin(obj);
+}
+
+static int MimeUntypedText_parse_line(const char* line, int32_t length,
+ MimeObject* obj) {
+ MimeUntypedText* uty = (MimeUntypedText*)obj;
+ int status = 0;
+ char *name = 0, *type = 0;
+ bool begin_line_p = false;
+
+ NS_ASSERTION(line && *line, "empty line in mime untyped parse_line");
+ if (!line || !*line) return -1;
+
+ /* If we're supposed to write this object, but aren't supposed to convert
+ it to HTML, simply pass it through unaltered. */
+ if (obj->output_p && obj->options && !obj->options->write_html_p &&
+ obj->options->output_fn)
+ return MimeObject_write(obj, line, length, true);
+
+ /* Open a new sub-part if this line demands it.
+ */
+ if (line[0] == 'b' && MimeUntypedText_uu_begin_line_p(
+ line, length, obj->options, &type, &name)) {
+ /* Close the old part and open a new one. */
+ status = MimeUntypedText_open_subpart(obj, MimeUntypedTextSubpartTypeUUE,
+ type, ENCODING_UUENCODE, name, NULL);
+ PR_FREEIF(name);
+ PR_FREEIF(type);
+ if (status < 0) return status;
+ begin_line_p = true;
+ }
+
+ else if (line[0] == '=' && MimeUntypedText_yenc_begin_line_p(
+ line, length, obj->options, &type, &name)) {
+ /* Close the old part and open a new one. */
+ status = MimeUntypedText_open_subpart(obj, MimeUntypedTextSubpartTypeYEnc,
+ type, ENCODING_YENCODE, name, NULL);
+ PR_FREEIF(name);
+ PR_FREEIF(type);
+ if (status < 0) return status;
+ begin_line_p = true;
+ }
+
+ else if (line[0] == '(' && line[1] == 'T' &&
+ MimeUntypedText_binhex_begin_line_p(line, length, obj->options)) {
+ /* Close the old part and open a new one. */
+ status = MimeUntypedText_open_subpart(obj, MimeUntypedTextSubpartTypeBinhex,
+ APPLICATION_BINHEX, NULL, NULL, NULL);
+ if (status < 0) return status;
+ begin_line_p = true;
+ }
+
+ /* Open a text/plain sub-part if there is no sub-part open.
+ */
+ if (!uty->open_subpart) {
+ // rhp: If we get here and we are being fed a line ending, we should
+ // just eat it and continue and if we really get more data, we'll open
+ // up the subpart then.
+ //
+ if (line[0] == '\r') return 0;
+ if (line[0] == '\n') return 0;
+
+ PR_ASSERT(!begin_line_p);
+ status = MimeUntypedText_open_subpart(obj, MimeUntypedTextSubpartTypeText,
+ TEXT_PLAIN, NULL, NULL, NULL);
+ PR_ASSERT(uty->open_subpart);
+ if (!uty->open_subpart) return -1;
+ if (status < 0) return status;
+ }
+
+ /* Hand this line to the currently-open sub-part.
+ */
+ status =
+ uty->open_subpart->clazz->parse_buffer(line, length, uty->open_subpart);
+ if (status < 0) return status;
+
+ /* Close this sub-part if this line demands it.
+ */
+ if (begin_line_p)
+ ;
+ else if (line[0] == 'e' && uty->type == MimeUntypedTextSubpartTypeUUE &&
+ MimeUntypedText_uu_end_line_p(line, length)) {
+ status = MimeUntypedText_close_subpart(obj);
+ if (status < 0) return status;
+ NS_ASSERTION(!uty->open_subpart, "no open subpart");
+ } else if (line[0] == '=' && uty->type == MimeUntypedTextSubpartTypeYEnc &&
+ MimeUntypedText_yenc_end_line_p(line, length)) {
+ status = MimeUntypedText_close_subpart(obj);
+ if (status < 0) return status;
+ NS_ASSERTION(!uty->open_subpart, "no open subpart");
+ } else if (uty->type == MimeUntypedTextSubpartTypeBinhex &&
+ MimeUntypedText_binhex_end_line_p(line, length)) {
+ status = MimeUntypedText_close_subpart(obj);
+ if (status < 0) return status;
+ NS_ASSERTION(!uty->open_subpart, "no open subpart");
+ }
+
+ return 0;
+}
+
+static int MimeUntypedText_close_subpart(MimeObject* obj) {
+ MimeUntypedText* uty = (MimeUntypedText*)obj;
+ int status;
+
+ if (uty->open_subpart) {
+ status = uty->open_subpart->clazz->parse_eof(uty->open_subpart, false);
+ uty->open_subpart = 0;
+
+ PR_ASSERT(uty->open_hdrs);
+ if (uty->open_hdrs) {
+ MimeHeaders_free(uty->open_hdrs);
+ uty->open_hdrs = 0;
+ }
+ uty->type = MimeUntypedTextSubpartTypeText;
+ if (status < 0) return status;
+
+ /* Never put out a separator between sub-parts of UntypedText.
+ (This bypasses the rule that text/plain subparts always
+ have separators before and after them.)
+ */
+ if (obj->options && obj->options->state)
+ obj->options->state->separator_suppressed_p = true;
+ }
+
+ PR_ASSERT(!uty->open_hdrs);
+ return 0;
+}
+
+static int MimeUntypedText_open_subpart(MimeObject* obj,
+ MimeUntypedTextSubpartType ttype,
+ const char* type, const char* enc,
+ const char* name, const char* desc) {
+ MimeUntypedText* uty = (MimeUntypedText*)obj;
+ int status = 0;
+ char* h = 0;
+
+ if (!type || !*type || !PL_strcasecmp(type, UNKNOWN_CONTENT_TYPE))
+ type = APPLICATION_OCTET_STREAM;
+ if (enc && !*enc) enc = 0;
+ if (desc && !*desc) desc = 0;
+ if (name && !*name) name = 0;
+
+ if (uty->open_subpart) {
+ status = MimeUntypedText_close_subpart(obj);
+ if (status < 0) return status;
+ }
+ NS_ASSERTION(!uty->open_subpart, "no open subpart");
+ NS_ASSERTION(!uty->open_hdrs, "no open headers");
+
+ /* To make one of these implicitly-typed sub-objects, we make up a fake
+ header block, containing only the minimum number of MIME headers needed.
+ We could do most of this (Type and Encoding) by making a null header
+ block, and simply setting obj->content_type and obj->encoding; but making
+ a fake header block is better for two reasons: first, it means that
+ something will actually be displayed when in `Show All Headers' mode;
+ and second, it's the only way to communicate the filename parameter,
+ aside from adding a new slot to MimeObject (which is something to be
+ avoided when possible.)
+ */
+
+ uty->open_hdrs = MimeHeaders_new();
+ if (!uty->open_hdrs) return MIME_OUT_OF_MEMORY;
+
+ uint32_t hlen = strlen(type) + (enc ? strlen(enc) : 0) +
+ (desc ? strlen(desc) : 0) + (name ? strlen(name) : 0) + 100;
+ h = (char*)PR_MALLOC(hlen);
+ if (!h) return MIME_OUT_OF_MEMORY;
+
+ PL_strncpyz(h, HEADER_CONTENT_TYPE ": ", hlen);
+ PL_strcatn(h, hlen, type);
+ PL_strcatn(h, hlen, MSG_LINEBREAK);
+ status = MimeHeaders_parse_line(h, strlen(h), uty->open_hdrs);
+ if (status < 0) goto FAIL;
+
+ if (enc) {
+ PL_strncpyz(h, HEADER_CONTENT_TRANSFER_ENCODING ": ", hlen);
+ PL_strcatn(h, hlen, enc);
+ PL_strcatn(h, hlen, MSG_LINEBREAK);
+ status = MimeHeaders_parse_line(h, strlen(h), uty->open_hdrs);
+ if (status < 0) goto FAIL;
+ }
+
+ if (desc) {
+ PL_strncpyz(h, HEADER_CONTENT_DESCRIPTION ": ", hlen);
+ PL_strcatn(h, hlen, desc);
+ PL_strcatn(h, hlen, MSG_LINEBREAK);
+ status = MimeHeaders_parse_line(h, strlen(h), uty->open_hdrs);
+ if (status < 0) goto FAIL;
+ }
+ if (name) {
+ PL_strncpyz(h, HEADER_CONTENT_DISPOSITION ": inline; filename=\"", hlen);
+ PL_strcatn(h, hlen, name);
+ PL_strcatn(h, hlen, "\"" MSG_LINEBREAK);
+ status = MimeHeaders_parse_line(h, strlen(h), uty->open_hdrs);
+ if (status < 0) goto FAIL;
+ }
+
+ /* push out a blank line. */
+ PL_strncpyz(h, MSG_LINEBREAK, hlen);
+ status = MimeHeaders_parse_line(h, strlen(h), uty->open_hdrs);
+ if (status < 0) goto FAIL;
+
+ /* Create a child... */
+ {
+ bool horrid_kludge = (obj->options && obj->options->state &&
+ obj->options->state->first_part_written_p);
+ if (horrid_kludge) obj->options->state->first_part_written_p = false;
+
+ uty->open_subpart = mime_create(type, uty->open_hdrs, obj->options);
+
+ if (horrid_kludge) obj->options->state->first_part_written_p = true;
+
+ if (!uty->open_subpart) {
+ status = MIME_OUT_OF_MEMORY;
+ goto FAIL;
+ }
+ }
+
+ /* Add it to the list... */
+ status = ((MimeContainerClass*)obj->clazz)->add_child(obj, uty->open_subpart);
+ if (status < 0) {
+ mime_free(uty->open_subpart);
+ uty->open_subpart = 0;
+ goto FAIL;
+ }
+
+ /* And start its parser going. */
+ status = uty->open_subpart->clazz->parse_begin(uty->open_subpart);
+ if (status < 0) {
+ /* MimeContainer->finalize will take care of shutting it down now. */
+ uty->open_subpart = 0;
+ goto FAIL;
+ }
+
+ uty->type = ttype;
+
+FAIL:
+ PR_FREEIF(h);
+
+ if (status < 0 && uty->open_hdrs) {
+ MimeHeaders_free(uty->open_hdrs);
+ uty->open_hdrs = 0;
+ }
+
+ return status;
+}
+
+static bool MimeUntypedText_uu_begin_line_p(const char* line, int32_t length,
+ MimeDisplayOptions* opt,
+ char** type_ret, char** name_ret) {
+ const char* s;
+ char* name = 0;
+ char* type = 0;
+
+ if (type_ret) *type_ret = 0;
+ if (name_ret) *name_ret = 0;
+
+ if (strncmp(line, "begin ", 6)) return false;
+ /* ...then three or four octal digits. */
+ s = line + 6;
+ if (*s < '0' || *s > '7') return false;
+ s++;
+ if (*s < '0' || *s > '7') return false;
+ s++;
+ if (*s < '0' || *s > '7') return false;
+ s++;
+ if (*s == ' ')
+ s++;
+ else {
+ if (*s < '0' || *s > '7') return false;
+ s++;
+ if (*s != ' ') return false;
+ }
+
+ while (IS_SPACE(*s)) s++;
+
+ int name_len = (line + length) - s;
+
+ name = (char*)PR_MALLOC(name_len + 1);
+ if (!name) return false; /* grr... */
+ memcpy(name, s, name_len);
+ name[name_len] = 0;
+
+ if (name_len) {
+ /* take off newline. */
+ if (name_len && name[name_len - 1] == '\n') {
+ name[name_len] = 0;
+ name_len--;
+ }
+ if (name_len && name[name_len - 1] == '\r') {
+ name[name_len] = 0;
+ }
+ }
+
+ /* Now try and figure out a type.
+ */
+ if (opt && opt->file_type_fn)
+ type = opt->file_type_fn(name, opt->stream_closure);
+ else
+ type = 0;
+
+ if (name_ret)
+ *name_ret = name;
+ else
+ PR_FREEIF(name);
+
+ if (type_ret)
+ *type_ret = type;
+ else
+ PR_FREEIF(type);
+
+ return true;
+}
+
+static bool MimeUntypedText_uu_end_line_p(const char* line, int32_t length) {
+#if 0
+ /* A strictly conforming uuencode end line. */
+ return (line[0] == 'e' &&
+ line[1] == 'n' &&
+ line[2] == 'd' &&
+ (line[3] == 0 || IS_SPACE(line[3])));
+#else
+ /* ...but, why don't we accept any line that begins with the three
+ letters "END" in any case: I've seen lots of partial messages
+ that look like
+
+ BEGIN----- Cut Here-----
+ begin 644 foo.gif
+ Mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ Mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ Mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ END------- Cut Here-----
+
+ so let's be lenient here. (This is only for the untyped-text-plain
+ case -- the uudecode parser itself is strict.)
+ */
+ return (line[0] == ' ' || line[0] == '\t' ||
+ ((line[0] == 'e' || line[0] == 'E') &&
+ (line[1] == 'n' || line[1] == 'N') &&
+ (line[2] == 'd' || line[2] == 'D')));
+#endif
+}
+
+static bool MimeUntypedText_yenc_begin_line_p(const char* line, int32_t length,
+ MimeDisplayOptions* opt,
+ char** type_ret,
+ char** name_ret) {
+ const char* s;
+ const char* endofline = line + length;
+ char* name = 0;
+ char* type = 0;
+
+ if (type_ret) *type_ret = 0;
+ if (name_ret) *name_ret = 0;
+
+ /* we don't support yenc V2 neither multipart yencode,
+ therefore the second parameter should always be "line="*/
+ if (length < 13 || strncmp(line, "=ybegin line=", 13)) return false;
+
+ /* ...then couple digits. */
+ for (s = line + 13; s < endofline; s++)
+ if (*s < '0' || *s > '9') break;
+
+ /* ...next, look for <space>size= */
+ if ((endofline - s) < 6 || strncmp(s, " size=", 6)) return false;
+
+ /* ...then couple digits. */
+ for (s += 6; s < endofline; s++)
+ if (*s < '0' || *s > '9') break;
+
+ /* ...next, look for <space>name= */
+ if ((endofline - s) < 6 || strncmp(s, " name=", 6)) return false;
+
+ /* anything left is the file name */
+ s += 6;
+
+ int name_len = endofline - s;
+
+ name = (char*)PR_MALLOC(name_len + 1);
+ if (!name) return false; /* grr... */
+ memcpy(name, s, name_len);
+ name[name_len] = 0;
+
+ if (name_len) {
+ /* take off newline. */
+ if (name_len && name[name_len - 1] == '\n') {
+ name[name_len] = 0;
+ name_len--;
+ }
+ if (name_len && name[name_len - 1] == '\r') {
+ name[name_len] = 0;
+ }
+ }
+
+ /* Now try and figure out a type.
+ */
+ if (opt && opt->file_type_fn)
+ type = opt->file_type_fn(name, opt->stream_closure);
+ else
+ type = 0;
+
+ if (name_ret)
+ *name_ret = name;
+ else
+ PR_FREEIF(name);
+
+ if (type_ret)
+ *type_ret = type;
+ else
+ PR_FREEIF(type);
+
+ return true;
+}
+
+static bool MimeUntypedText_yenc_end_line_p(const char* line, int32_t length) {
+ if (length < 11 || strncmp(line, "=yend size=", 11)) return false;
+
+ return true;
+}
+
+#define BINHEX_MAGIC "(This file must be converted with BinHex 4.0)"
+#define BINHEX_MAGIC_LEN 45
+
+static bool MimeUntypedText_binhex_begin_line_p(const char* line,
+ int32_t length,
+ MimeDisplayOptions* opt) {
+ if (length <= BINHEX_MAGIC_LEN) return false;
+
+ while (length > 0 && IS_SPACE(line[length - 1])) length--;
+
+ if (length != BINHEX_MAGIC_LEN) return false;
+
+ if (!strncmp(line, BINHEX_MAGIC, BINHEX_MAGIC_LEN))
+ return true;
+ else
+ return false;
+}
+
+static bool MimeUntypedText_binhex_end_line_p(const char* line,
+ int32_t length) {
+ if (length > 0 && line[length - 1] == '\n') length--;
+ if (length > 0 && line[length - 1] == '\r') length--;
+
+ if (length != 0 && length != 64)
+ return true;
+ else
+ return false;
+}
diff --git a/comm/mailnews/mime/src/mimeunty.h b/comm/mailnews/mime/src/mimeunty.h
new file mode 100644
index 0000000000..580cbbb8db
--- /dev/null
+++ b/comm/mailnews/mime/src/mimeunty.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _MIMEUNTY_H_
+#define _MIMEUNTY_H_
+
+#include "mimecont.h"
+
+/* The MimeUntypedText class is used for untyped message contents, that is,
+ it is the class used for the body of a message/rfc822 object which had
+ *no* Content-Type header, as opposed to an unrecognized content-type.
+ Such a message, technically, does not contain MIME data (it follows only
+ RFC 822, not RFC 1521.)
+
+ This is a container class, and the reason for that is that it loosely
+ parses the body of the message looking for ``sub-parts'' and then
+ creates appropriate containers for them.
+
+ More specifically, it looks for uuencoded data. It may do more than that
+ some day.
+
+ Basically, the algorithm followed is:
+
+ if line is "begin 644 foo.gif"
+ if there is an open sub-part, close it
+ add a sub-part with type: image/gif; encoding: x-uue
+ hand this line to it
+ and hand subsequent lines to that subpart
+ else if there is an open uuencoded sub-part, and line is "end"
+ hand this line to it
+ close off the uuencoded sub-part
+ else if there is an open sub-part
+ hand this line to it
+ else
+ open a text/plain subpart
+ hand this line to it
+
+ Adding other types than uuencode to this (for example, PGP) would be
+ pretty straightforward.
+ */
+
+typedef struct MimeUntypedTextClass MimeUntypedTextClass;
+typedef struct MimeUntypedText MimeUntypedText;
+
+struct MimeUntypedTextClass {
+ MimeContainerClass container;
+};
+
+extern MimeUntypedTextClass mimeUntypedTextClass;
+
+typedef enum {
+ MimeUntypedTextSubpartTypeText, /* text/plain */
+ MimeUntypedTextSubpartTypeUUE, /* uuencoded data */
+ MimeUntypedTextSubpartTypeYEnc, /* yencoded data */
+ MimeUntypedTextSubpartTypeBinhex /* Mac BinHex data */
+} MimeUntypedTextSubpartType;
+
+struct MimeUntypedText {
+ MimeContainer container; /* superclass variables */
+ MimeObject* open_subpart; /* The part still-being-parsed */
+ MimeUntypedTextSubpartType type; /* What kind of type it is */
+ MimeHeaders* open_hdrs; /* The faked-up headers describing it */
+};
+
+#define MimeUntypedTextClassInitializer(ITYPE, CSUPER) \
+ { MimeContainerClassInitializer(ITYPE, CSUPER) }
+
+#endif /* _MIMEUNTY_H_ */
diff --git a/comm/mailnews/mime/src/modlmime.h b/comm/mailnews/mime/src/modlmime.h
new file mode 100644
index 0000000000..14d53c9cd6
--- /dev/null
+++ b/comm/mailnews/mime/src/modlmime.h
@@ -0,0 +1,388 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _LIBMIME_H_
+#define _LIBMIME_H_
+
+#ifdef XP_UNIX
+# undef Bool
+#endif
+
+#include "nsString.h"
+#include "nsMailHeaders.h"
+#include "nsIMimeStreamConverter.h"
+#include "mozilla/Encoding.h"
+#include "nsIPrefBranch.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsCOMPtr.h"
+#include "modmimee.h" // for MimeConverterOutputCallback
+
+#define MIME_DRAFTS
+
+/* Opaque object describing a block of message headers, and a couple of
+ routines for extracting data from one.
+ */
+
+typedef struct MimeHeaders {
+ char* all_headers; /* A char* of the entire header section. */
+ int32_t all_headers_fp; /* The length (it is not NULL-terminated.) */
+ int32_t all_headers_size; /* The size of the allocated block. */
+
+ bool done_p; /* Whether we've read the end-of-headers marker
+ (the terminating blank line.) */
+
+ char** heads; /* An array of length heads_size which points
+ to the beginning of each distinct header:
+ just after the newline which terminated
+ the previous one. This is to speed search.
+ This is not initialized until all the
+ headers have been read.
+ */
+ int32_t heads_size; /* The number of entries (pointers) in the heads
+ array (and consequently, how many
+ distinct headers are in here.) */
+
+ char* obuffer; /* This buffer is used for output. */
+ int32_t obuffer_size;
+ int32_t obuffer_fp;
+
+ char* munged_subject; /* What a hack. This is a place to write down
+ the subject header, after it's been
+ charset-ified and stuff. Remembered so that
+ we can later use it to generate the
+ <TITLE> tag. (Also works for giving names to
+ RFC822 attachments) */
+} MimeHeaders;
+
+class MimeDisplayOptions;
+class MimeParseStateObject;
+typedef struct MSG_AttachmentData MSG_AttachmentData;
+
+/* Given the name of a header, returns the contents of that header as
+ a newly-allocated string (which the caller must free.) If the header
+ is not present, or has no contents, NULL is returned.
+
+ If `strip_p' is true, then the data returned will be the first token
+ of the header; else it will be the full text of the header. (This is
+ useful for getting just "text/plain" from "text/plain; name=foo".)
+
+ If `all_p' is false, then the first header encountered is used, and
+ any subsequent headers of the same name are ignored. If true, then
+ all headers of the same name are appended together (this is useful
+ for gathering up all CC headers into one, for example.)
+ */
+extern char* MimeHeaders_get(MimeHeaders* hdrs, const char* header_name,
+ bool strip_p, bool all_p);
+
+// clang-format off
+/* Given a header of the form of the MIME "Content-" headers, extracts a
+ named parameter from it, if it exists. For example,
+ MimeHeaders_get_parameter("text/plain; charset=us-ascii", "charset")
+ would return "us-ascii".
+
+ Returns NULL if there is no match, or if there is an allocation failure.
+
+ RFC2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets,
+ Languages, and Continuations
+
+ RFC2231 has added the character sets, languages, and continuations mechanism.
+ charset, and language information may also be returned to the caller.
+ Note that charset and language should be free()'d while
+ the return value (parameter) has to be PR_FREE'd.
+
+ For example,
+ MimeHeaders_get_parameter("text/plain; name*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A", "name")
+ MimeHeaders_get_parameter("text/plain; name*0*=us-ascii'en-us'This%20is%20; CRLFLWSPname*1*=%2A%2A%2Afun%2A%2A%2A", "name") would return "This is ***fun***" and *charset = "us-ascii", *language = "en-us"
+ */
+// clang-format on
+
+extern char* MimeHeaders_get_parameter(const char* header_value,
+ const char* parm_name, char** charset,
+ char** language);
+
+extern MimeHeaders* MimeHeaders_copy(MimeHeaders* srcHeaders);
+
+extern void MimeHeaders_free(MimeHeaders* hdrs);
+
+typedef enum {
+ MimeHeadersAll, /* Show all headers */
+ MimeHeadersSome, /* Show all "interesting" headers */
+ MimeHeadersSomeNoRef, /* Same, but suppress the `References' header
+ (for when we're printing this message.) */
+ MimeHeadersMicro, /* Show a one-line header summary */
+ MimeHeadersMicroPlus, /* Same, but show the full recipient list as
+ well (To, CC, etc.) */
+ MimeHeadersCitation, /* A one-line summary geared toward use in a
+ reply citation ("So-and-so wrote:") */
+ MimeHeadersOnly, /* Just parse and output headers...nothing else! */
+ MimeHeadersNone /* Skip showing any headers */
+} MimeHeadersState;
+
+/* The signature for various callbacks in the MimeDisplayOptions structure.
+ */
+typedef char* (*MimeHTMLGeneratorFunction)(const char* data, void* closure,
+ MimeHeaders* headers);
+
+class MimeDisplayOptions {
+ public:
+ MimeDisplayOptions();
+ virtual ~MimeDisplayOptions();
+ mozITXTToHTMLConv* conv; // For text conversion...
+ nsCOMPtr<nsIPrefBranch> m_prefBranch; /* prefBranch-service */
+ nsMimeOutputType format_out; // The format out type
+
+ const char* url; /* Base URL for the document. This string should
+ be freed by the caller, after the parser
+ completes (possibly at the same time as the
+ MimeDisplayOptions itself.) */
+
+ MimeHeadersState headers; /* How headers should be displayed. */
+ bool fancy_headers_p; /* Whether to do clever formatting of headers
+ using tables, instead of spaces. */
+
+ bool output_vcard_buttons_p; /* Whether to output the buttons */
+ /* on vcards. */
+
+ bool variable_width_plaintext_p; /* Whether text/plain messages should
+ be in variable width, or fixed. */
+ bool wrap_long_lines_p; /* Whether to wrap long lines in text/plain
+ messages. */
+
+ bool rot13_p; /* Whether text/plain parts should be rotated
+ Set by "?rot13=true" */
+ char* part_to_load; /* The particular part of the multipart which
+ we are extracting. Set by "?part=3.2.4" */
+
+ bool no_output_p; /* Will never write output when this is true.
+ (When false, output or not may depend on other things.)
+ This needs to be in the options, because the method
+ MimeObject_parse_begin is controlling the property
+ "output_p" (on the MimeObject) so we need a way for other
+ functions to override it and tell that we do not want
+ output. */
+
+ bool write_html_p; /* Whether the output should be HTML, or raw. */
+
+ bool decrypt_p; /* Whether all traces of xlateion should be
+ eradicated -- this is only meaningful when
+ write_html_p is false; we set this when
+ attaching a message for forwarding, since
+ forwarding someone else a message that wasn't
+ xlated for them doesn't work. We have to
+ dexlate it before sending it.
+ */
+
+ /* Whether this MIME part is a child of another part (and not top level). */
+ bool is_child = false;
+
+ uint32_t whattodo; /* from the prefs, we'll get if the user wants to do glyph
+ or structure substitutions and set this member variable.
+ */
+
+ char* default_charset; /* If this is non-NULL, then it is the charset to
+ assume when no other one is specified via a
+ `charset' parameter.
+ */
+ bool override_charset; /* If this is true, then we will assume that
+ all data is in the default_charset, regardless
+ of what the `charset' parameter of that part
+ says. (This is to cope with the fact that, in
+ the real world, many messages are mislabelled
+ with the wrong charset.)
+ */
+ bool force_user_charset; /* this is the new strategy to deal with incorrectly
+ labeled attachments */
+
+ /* =======================================================================
+ Stream-related callbacks; for these functions, the `closure' argument
+ is what is found in `options->stream_closure'. (One possible exception
+ is for output_fn; see "output_closure" below.)
+ */
+ void* stream_closure;
+
+ /* For setting up the display stream, so that the MIME parser can inform
+ the caller of the type of the data it will be getting. */
+ int (*output_init_fn)(const char* type, const char* charset, const char* name,
+ const char* x_mac_type, const char* x_mac_creator,
+ void* stream_closure);
+
+ /* How the MIME parser feeds its output (HTML or raw) back to the caller. */
+ MimeConverterOutputCallback output_fn;
+
+ /* Closure to pass to the above output_fn. If NULL, then the
+ stream_closure is used. */
+ void* output_closure;
+
+ /* A hook for the caller to perform charset-conversion before HTML is
+ returned. Each set of characters which originated in a mail message
+ (body or headers) will be run through this filter before being converted
+ into HTML. (This should return bytes which may appear in an HTML file,
+ ie, we must be able to scan through the string to search for "<" and
+ turn it in to "&lt;", and so on.)
+
+ `input' is a non-NULL-terminated string of a single line from the message.
+ `input_length' is how long it is.
+ `input_charset' is a string representing the charset of this string (as
+ specified by MIME headers.)
+ The conversion is always to UTF-8.
+ `output_ret' is where a newly-malloced string is returned. It may be
+ NULL if no translation is needed.
+ `output_size_ret' is how long the returned string is (it need not be
+ NULL-terminated.).
+ */
+ int (*charset_conversion_fn)(const char* input_line, int32_t input_length,
+ const char* input_charset,
+ nsACString& output_ret, void* stream_closure);
+
+ /* If true, perform both charset-conversion and decoding of
+ MIME-2 header fields (using RFC-1522 encoding.)
+ */
+ bool rfc1522_conversion_p;
+
+ /* A hook for the caller to turn a file name into a content-type. */
+ char* (*file_type_fn)(const char* filename, void* stream_closure);
+
+ /* A hook by which the user may be prompted for a password by the security
+ library. (This is really of type `SECKEYGetPasswordKey'; see sec.h.) */
+ void* (*passwd_prompt_fn)(void* arg1, void* arg2);
+
+ /* =======================================================================
+ Various callbacks; for all of these functions, the `closure' argument
+ is what is found in `html_closure'.
+ */
+ void* html_closure;
+
+ /* For emitting some HTML before the start of the outermost message
+ (this is called before any HTML is written to layout.) */
+ MimeHTMLGeneratorFunction generate_header_html_fn;
+
+ /* For emitting some HTML after the outermost header block, but before
+ the body of the first message. */
+ MimeHTMLGeneratorFunction generate_post_header_html_fn;
+
+ /* For emitting some HTML at the very end (this is called after libmime
+ has written everything it's going to write.) */
+ MimeHTMLGeneratorFunction generate_footer_html_fn;
+
+ /* For turning a message ID into a loadable URL. */
+ MimeHTMLGeneratorFunction generate_reference_url_fn;
+
+ /* For turning a mail address into a mailto URL. */
+ MimeHTMLGeneratorFunction generate_mailto_url_fn;
+
+ /* For turning a newsgroup name into a news URL. */
+ MimeHTMLGeneratorFunction generate_news_url_fn;
+
+ /* =======================================================================
+ Callbacks to handle the backend-specific inlined image display
+ (internal-external-reconnect junk.) For `image_begin', the `closure'
+ argument is what is found in `stream_closure'; but for all of the
+ others, the `closure' argument is the data that `image_begin' returned.
+ */
+
+ /* Begins processing an embedded image; the URL and content_type are of the
+ image itself. */
+ void* (*image_begin)(const char* image_url, const char* content_type,
+ void* stream_closure);
+
+ /* Stop processing an image. */
+ void (*image_end)(void* image_closure, int status);
+
+ /* Dump some raw image data down the stream. */
+ int (*image_write_buffer)(const char* buf, int32_t size, void* image_closure);
+
+ /* What HTML should be dumped out for this image. */
+ char* (*make_image_html)(void* image_closure);
+
+ /* =======================================================================
+ Other random opaque state.
+ */
+ MimeParseStateObject* state; /* Some state used by libmime internals;
+ initialize this to 0 and leave it alone.
+ */
+
+#ifdef MIME_DRAFTS
+ /* =======================================================================
+ Mail Draft hooks -- 09-19-1996
+ */
+ bool decompose_file_p; /* are we decomposing a mime msg
+ into separate files */
+ bool done_parsing_outer_headers; /* are we done parsing the outer message
+ headers; this is really useful when
+ we have multiple Message/RFC822
+ headers */
+ bool is_multipart_msg; /* are we decomposing a multipart
+ message */
+
+ int decompose_init_count; /* used for non multipart message only */
+
+ bool signed_p; /* to tell draft this is a signed message */
+
+ bool caller_need_root_headers; /* set it to true to receive the message main
+ headers through the callback
+ decompose_headers_info_fn */
+
+ /* Callback to gather the outer most headers so we could use the
+ information to initialize the addressing/subject/newsgroups fields
+ for the composition window. */
+ int (*decompose_headers_info_fn)(void* closure, MimeHeaders* headers);
+
+ /* Callbacks to create temporary files for drafts attachments. */
+ int (*decompose_file_init_fn)(void* stream_closure, MimeHeaders* headers);
+
+ MimeConverterOutputCallback decompose_file_output_fn;
+
+ int (*decompose_file_close_fn)(void* stream_closure);
+#endif /* MIME_DRAFTS */
+
+ int32_t attachment_icon_layer_id; /* Hackhackhack. This is zero if we have
+ not yet emitted the attachment layer
+ stuff. If we have, then this is the
+ id number for that layer, which is a
+ unique random number every time, to keep
+ evil people from writing javascript code
+ to hack it. */
+
+ bool missing_parts; /* Whether or not this message is going to contain
+ missing parts (from IMAP Mime Parts On Demand) */
+
+ bool show_attachment_inline_p; /* Whether or not we should display attachment
+ inline (whatever say the
+ content-disposition) */
+
+ bool show_attachment_inline_text; /* Whether or not we should display text
+ attachment inline (whatever the
+ content-disposition says) */
+
+ bool quote_attachment_inline_p; /* Whether or not we should include inlined
+ attachments in quotes of replies) */
+
+ int32_t html_as_p; /* How we should display HTML, which allows us to know if
+ we should display all body parts */
+
+ /**
+ * Should StartBody/EndBody events be generated for nested MimeMessages. If
+ * false (the default value), the events are only generated for the outermost
+ * MimeMessage.
+ */
+ bool notify_nested_bodies;
+
+ /**
+ * When true, compels mime parts to only write the actual body
+ * payload and not display-gunk like links to attachments. This was
+ * primarily introduced for the benefit of the javascript emitter.
+ */
+ bool write_pure_bodies;
+
+ /**
+ * When true, only processes metadata (i.e. size) for streamed attachments.
+ * Mime emitters that expect any attachment data (including inline text and
+ * image attachments) should leave this as false (the default value). At
+ * the moment, only the JS mime emitter uses this.
+ */
+ bool metadata_only;
+};
+
+#endif /* _MODLMIME_H_ */
diff --git a/comm/mailnews/mime/src/modmimee.h b/comm/mailnews/mime/src/modmimee.h
new file mode 100644
index 0000000000..14c6933265
--- /dev/null
+++ b/comm/mailnews/mime/src/modmimee.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+/*
+ mimeenc.c --- MIME encoders and decoders, version 2 (see mimei.h)
+ Copyright (c) 1996 Netscape Communications Corporation, all rights reserved.
+ Created: Jamie Zawinski <jwz@netscape.com>, 15-May-96.
+*/
+
+#ifndef _MIMEENC_H_
+#define _MIMEENC_H_
+
+#include "nsError.h"
+#include "nscore.h" // for nullptr
+
+typedef int (*MimeConverterOutputCallback)(const char* buf, int32_t size,
+ void* closure);
+
+/* This file defines interfaces to generic implementations of Base64,
+ Quoted-Printable, and UU decoders; and of Base64 and Quoted-Printable
+ encoders.
+ */
+
+/* Opaque objects used by the encoder/decoder to store state. */
+typedef struct MimeDecoderData MimeDecoderData;
+
+struct MimeObject;
+
+/* functions for creating that opaque data.
+ */
+MimeDecoderData* MimeB64DecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure);
+
+MimeDecoderData* MimeQPDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure, MimeObject* object = nullptr);
+
+MimeDecoderData* MimeUUDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure);
+MimeDecoderData* MimeYDecoderInit(MimeConverterOutputCallback output_fn,
+ void* closure);
+
+/* Push data through the encoder/decoder, causing the above-provided write_fn
+ to be called with encoded/decoded data. */
+int MimeDecoderWrite(MimeDecoderData* data, const char* buffer, int32_t size,
+ int32_t* outSize);
+
+/* When you're done encoding/decoding, call this to free the data. If
+ abort_p is false, then calling this may cause the write_fn to be called
+ one last time (as the last buffered data is flushed out.)
+ */
+int MimeDecoderDestroy(MimeDecoderData* data, bool abort_p);
+
+#endif /* _MODMIMEE_H_ */
diff --git a/comm/mailnews/mime/src/moz.build b/comm/mailnews/mime/src/moz.build
new file mode 100644
index 0000000000..7bd35ca1c6
--- /dev/null
+++ b/comm/mailnews/mime/src/moz.build
@@ -0,0 +1,88 @@
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "mimecont.h",
+ "mimecryp.h",
+ "mimecth.h",
+ "mimehdrs.h",
+ "mimei.h",
+ "mimeleaf.h",
+ "mimemoz2.h",
+ "mimemsig.h",
+ "mimemult.h",
+ "mimeobj.h",
+ "mimepbuf.h",
+ "mimetext.h",
+ "modlmime.h",
+ "modmimee.h",
+ "nsMimeStringResources.h",
+ "nsStreamConverter.h",
+]
+
+SOURCES += [
+ "comi18n.cpp",
+ "mimebuf.cpp",
+ "mimecms.cpp",
+ "mimecom.cpp",
+ "mimecont.cpp",
+ "mimecryp.cpp",
+ "mimecth.cpp",
+ "mimedrft.cpp",
+ "mimeebod.cpp",
+ "mimeenc.cpp",
+ "mimeeobj.cpp",
+ "mimehdrs.cpp",
+ "MimeHeaderParser.cpp",
+ "mimei.cpp",
+ "mimeiimg.cpp",
+ "mimeleaf.cpp",
+ "mimemalt.cpp",
+ "mimemapl.cpp",
+ "mimemcms.cpp",
+ "mimemdig.cpp",
+ "mimemmix.cpp",
+ "mimemoz2.cpp",
+ "mimempar.cpp",
+ "mimemrel.cpp",
+ "mimemsg.cpp",
+ "mimemsig.cpp",
+ "mimemult.cpp",
+ "mimeobj.cpp",
+ "mimepbuf.cpp",
+ "mimesun.cpp",
+ "mimetenr.cpp",
+ "mimetext.cpp",
+ "mimeTextHTMLParsed.cpp",
+ "mimethpl.cpp",
+ "mimethsa.cpp",
+ "mimethtm.cpp",
+ "mimetpfl.cpp",
+ "mimetpla.cpp",
+ "mimetric.cpp",
+ "mimeunty.cpp",
+ "nsMimeObjectClassAccess.cpp",
+ "nsSimpleMimeConverterStub.cpp",
+ "nsStreamConverter.cpp",
+]
+
+EXTRA_COMPONENTS += [
+ "msgMime.manifest",
+]
+
+EXTRA_JS_MODULES += [
+ "extraMimeParsers.jsm",
+ "jsmime.jsm",
+ "MimeJSComponents.jsm",
+ "mimeParser.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
+
+DEFINES["ENABLE_SMIME"] = True
diff --git a/comm/mailnews/mime/src/msgMime.manifest b/comm/mailnews/mime/src/msgMime.manifest
new file mode 100644
index 0000000000..c6060c321b
--- /dev/null
+++ b/comm/mailnews/mime/src/msgMime.manifest
@@ -0,0 +1 @@
+category custom-mime-encoder A-extra resource:///modules/extraMimeParsers.jsm
diff --git a/comm/mailnews/mime/src/nsMimeObjectClassAccess.cpp b/comm/mailnews/mime/src/nsMimeObjectClassAccess.cpp
new file mode 100644
index 0000000000..a769ae192e
--- /dev/null
+++ b/comm/mailnews/mime/src/nsMimeObjectClassAccess.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include <stdio.h>
+#include "mimecom.h"
+#include "nscore.h"
+#include "nsMimeObjectClassAccess.h"
+
+/*
+ * The following macros actually implement addref, release and
+ * query interface for our component.
+ */
+NS_IMPL_ISUPPORTS(nsMimeObjectClassAccess, nsIMimeObjectClassAccess)
+
+/*
+ * nsMimeObjectClassAccess definitions....
+ */
+
+/*
+ * Inherited methods for nsMimeObjectClassAccess
+ */
+nsMimeObjectClassAccess::nsMimeObjectClassAccess() {}
+
+nsMimeObjectClassAccess::~nsMimeObjectClassAccess() {}
+
+nsresult nsMimeObjectClassAccess::MimeObjectWrite(void* mimeObject, char* data,
+ int32_t length,
+ bool user_visible_p) {
+ int rc = XPCOM_MimeObject_write(mimeObject, data, length, user_visible_p);
+ NS_ENSURE_FALSE(rc < 0, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeInlineTextClass(void** ptr) {
+ *ptr = XPCOM_GetmimeInlineTextClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeLeafClass(void** ptr) {
+ *ptr = XPCOM_GetmimeLeafClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeObjectClass(void** ptr) {
+ *ptr = XPCOM_GetmimeObjectClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeContainerClass(void** ptr) {
+ *ptr = XPCOM_GetmimeContainerClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeMultipartClass(void** ptr) {
+ *ptr = XPCOM_GetmimeMultipartClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeMultipartSignedClass(void** ptr) {
+ *ptr = XPCOM_GetmimeMultipartSignedClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::GetmimeEncryptedClass(void** ptr) {
+ *ptr = XPCOM_GetmimeEncryptedClass();
+ return NS_OK;
+}
+
+nsresult nsMimeObjectClassAccess::MimeCreate(char* content_type, void* hdrs,
+ void* opts, void** ptr) {
+ *ptr = XPCOM_Mime_create(content_type, hdrs, opts);
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/src/nsMimeObjectClassAccess.h b/comm/mailnews/mime/src/nsMimeObjectClassAccess.h
new file mode 100644
index 0000000000..d131ffa1a6
--- /dev/null
+++ b/comm/mailnews/mime/src/nsMimeObjectClassAccess.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * This interface is implemented by libmime. This interface is used by
+ * a Content-Type handler "Plug In" (i.e. vCard) for accessing various
+ * internal information about the object class system of libmime. When
+ * libmime progresses to a C++ object class, this would probably change.
+ */
+#ifndef nsMimeObjectClassAccess_h_
+#define nsMimeObjectClassAccess_h_
+
+#include "mozilla/Attributes.h"
+#include "nsISupports.h"
+#include "nsIMimeObjectClassAccess.h"
+
+class nsMimeObjectClassAccess : public nsIMimeObjectClassAccess {
+ public:
+ nsMimeObjectClassAccess();
+
+ /* this macro defines QueryInterface, AddRef and Release for this class */
+ NS_DECL_ISUPPORTS
+
+ // These methods are all implemented by libmime to be used by
+ // content type handler plugins for processing stream data.
+
+ // This is the write call for outputting processed stream data.
+ NS_IMETHOD MimeObjectWrite(void* mimeObject, char* data, int32_t length,
+ bool user_visible_p) override;
+
+ // The following group of calls expose the pointers for the object
+ // system within libmime.
+ NS_IMETHOD GetmimeInlineTextClass(void** ptr) override;
+ NS_IMETHOD GetmimeLeafClass(void** ptr) override;
+ NS_IMETHOD GetmimeObjectClass(void** ptr) override;
+ NS_IMETHOD GetmimeContainerClass(void** ptr) override;
+ NS_IMETHOD GetmimeMultipartClass(void** ptr) override;
+ NS_IMETHOD GetmimeMultipartSignedClass(void** ptr) override;
+ NS_IMETHOD GetmimeEncryptedClass(void** ptr) override;
+
+ NS_IMETHOD MimeCreate(char* content_type, void* hdrs, void* opts,
+ void** ptr) override;
+
+ private:
+ virtual ~nsMimeObjectClassAccess();
+};
+
+#endif /* nsMimeObjectClassAccess_h_ */
diff --git a/comm/mailnews/mime/src/nsMimeStringResources.h b/comm/mailnews/mime/src/nsMimeStringResources.h
new file mode 100644
index 0000000000..7857086093
--- /dev/null
+++ b/comm/mailnews/mime/src/nsMimeStringResources.h
@@ -0,0 +1,40 @@
+/* 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/. */
+
+#ifndef _NAME_OF_THIS_HEADER_FILE__
+#define _NAME_OF_THIS_HEADER_FILE__
+
+/* Note that the negative values are not actually strings: they are error
+ * codes masquerading as strings. Do not pass them to MimeGetStringByID()
+ * expecting to get anything back for your trouble.
+ */
+#define MIME_OUT_OF_MEMORY -1000
+#define MIME_UNABLE_TO_OPEN_TMP_FILE -1001
+#define MIME_ERROR_WRITING_FILE -1002
+#define MIME_MHTML_SUBJECT 1000
+#define MIME_MHTML_RESENT_COMMENTS 1001
+#define MIME_MHTML_RESENT_DATE 1002
+#define MIME_MHTML_RESENT_SENDER 1003
+#define MIME_MHTML_RESENT_FROM 1004
+#define MIME_MHTML_RESENT_TO 1005
+#define MIME_MHTML_RESENT_CC 1006
+#define MIME_MHTML_DATE 1007
+#define MIME_MHTML_SENDER 1008
+#define MIME_MHTML_FROM 1009
+#define MIME_MHTML_REPLY_TO 1010
+#define MIME_MHTML_ORGANIZATION 1011
+#define MIME_MHTML_TO 1012
+#define MIME_MHTML_CC 1013
+#define MIME_MHTML_NEWSGROUPS 1014
+#define MIME_MHTML_FOLLOWUP_TO 1015
+#define MIME_MHTML_REFERENCES 1016
+#define MIME_MHTML_MESSAGE_ID 1021
+#define MIME_MHTML_BCC 1023
+#define MIME_MSG_LINK_TO_DOCUMENT 1026
+#define MIME_MSG_DOCUMENT_INFO 1027
+#define MIME_MSG_ATTACHMENT 1028
+#define MIME_MSG_DEFAULT_ATTACHMENT_NAME 1040
+#define MIME_FORWARDED_MESSAGE_HTML_USER_WROTE 1041
+
+#endif /* _NAME_OF_THIS_HEADER_FILE__ */
diff --git a/comm/mailnews/mime/src/nsSimpleMimeConverterStub.cpp b/comm/mailnews/mime/src/nsSimpleMimeConverterStub.cpp
new file mode 100644
index 0000000000..0ff3bd686a
--- /dev/null
+++ b/comm/mailnews/mime/src/nsSimpleMimeConverterStub.cpp
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mimecth.h"
+#include "mimeobj.h"
+#include "mimetext.h"
+#include "mimemoz2.h"
+#include "mimecom.h"
+#include "nsString.h"
+#include "nsComponentManagerUtils.h"
+#include "nsICategoryManager.h"
+#include "nsCOMPtr.h"
+#include "nsISimpleMimeConverter.h"
+#include "nsServiceManagerUtils.h"
+#include "nsSimpleMimeConverterStub.h"
+#include "nsIMailChannel.h"
+
+typedef struct MimeSimpleStub MimeSimpleStub;
+typedef struct MimeSimpleStubClass MimeSimpleStubClass;
+
+struct MimeSimpleStubClass {
+ MimeInlineTextClass text;
+};
+
+struct MimeSimpleStub {
+ MimeInlineText text;
+ nsCString* buffer;
+ nsCOMPtr<nsISimpleMimeConverter> innerScriptable;
+};
+
+#define MimeSimpleStubClassInitializer(ITYPE, CSUPER) \
+ { MimeInlineTextClassInitializer(ITYPE, CSUPER) }
+
+MimeDefClass(MimeSimpleStub, MimeSimpleStubClass, mimeSimpleStubClass, NULL);
+
+static int BeginGather(MimeObject* obj) {
+ MimeSimpleStub* ssobj = (MimeSimpleStub*)obj;
+ int status = ((MimeObjectClass*)XPCOM_GetmimeLeafClass())->parse_begin(obj);
+
+ if (status < 0) return status;
+
+ if (!obj->output_p || !obj->options || !obj->options->write_html_p) {
+ return 0;
+ }
+
+ ssobj->buffer->Truncate();
+ return 0;
+}
+
+static int GatherLine(const char* line, int32_t length, MimeObject* obj) {
+ MimeSimpleStub* ssobj = (MimeSimpleStub*)obj;
+
+ if (!obj->output_p || !obj->options || !obj->options->output_fn) {
+ return 0;
+ }
+
+ if (!obj->options->write_html_p)
+ return MimeObject_write(obj, line, length, true);
+
+ ssobj->buffer->Append(line, length);
+ return 0;
+}
+
+static int EndGather(MimeObject* obj, bool abort_p) {
+ MimeSimpleStub* ssobj = (MimeSimpleStub*)obj;
+
+ if (obj->closed_p) return 0;
+
+ int status = ((MimeObjectClass*)MIME_GetmimeInlineTextClass())
+ ->parse_eof(obj, abort_p);
+ if (status < 0) return status;
+
+ if (ssobj->buffer->IsEmpty()) return 0;
+
+ mime_stream_data* msd = (mime_stream_data*)(obj->options->stream_closure);
+ nsIChannel* channel = msd->channel; // note the lack of ref counting...
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ ssobj->innerScriptable->SetUri(uri);
+
+ nsCOMPtr<nsIMailChannel> mailChannel = do_QueryInterface(channel);
+ ssobj->innerScriptable->SetMailChannel(mailChannel);
+ }
+ // Remove possible embedded NULL bytes.
+ // Parsers can't handle this but e.g. calendar invitation might contain such
+ // as fillers.
+ ssobj->buffer->StripChar('\0');
+ nsCString asHTML;
+ nsresult rv = ssobj->innerScriptable->ConvertToHTML(
+ nsDependentCString(obj->content_type), *ssobj->buffer, asHTML);
+ if (NS_FAILED(rv)) {
+ NS_ASSERTION(NS_SUCCEEDED(rv), "converter failure");
+ return -1;
+ }
+
+ // MimeObject_write wants a non-const string for some reason, but it doesn't
+ // mutate it.
+ status = MimeObject_write(obj, asHTML.get(), asHTML.Length(), true);
+ if (status < 0) return status;
+ return 0;
+}
+
+static int Initialize(MimeObject* obj) {
+ MimeSimpleStub* ssobj = (MimeSimpleStub*)obj;
+
+ nsresult rv;
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return -1;
+
+ nsAutoCString contentType; // lowercase
+ ToLowerCase(nsDependentCString(obj->content_type), contentType);
+
+ nsCString value;
+ rv = catman->GetCategoryEntry(NS_SIMPLEMIMECONVERTERS_CATEGORY, contentType,
+ value);
+ if (NS_FAILED(rv) || value.IsEmpty()) return -1;
+
+ ssobj->innerScriptable = do_CreateInstance(value.get(), &rv);
+ if (NS_FAILED(rv) || !ssobj->innerScriptable) return -1;
+ ssobj->buffer = new nsCString();
+ ((MimeObjectClass*)XPCOM_GetmimeLeafClass())->initialize(obj);
+
+ return 0;
+}
+
+static void Finalize(MimeObject* obj) {
+ MimeSimpleStub* ssobj = (MimeSimpleStub*)obj;
+ ssobj->innerScriptable = nullptr;
+ delete ssobj->buffer;
+}
+
+static int MimeSimpleStubClassInitialize(MimeSimpleStubClass* clazz) {
+ MimeObjectClass* oclass = (MimeObjectClass*)clazz;
+ oclass->parse_begin = BeginGather;
+ oclass->parse_line = GatherLine;
+ oclass->parse_eof = EndGather;
+ oclass->initialize = Initialize;
+ oclass->finalize = Finalize;
+ return 0;
+}
+
+class nsSimpleMimeConverterStub : public nsIMimeContentTypeHandler {
+ public:
+ explicit nsSimpleMimeConverterStub(const char* aContentType)
+ : mContentType(aContentType) {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD GetContentType(char** contentType) override {
+ *contentType = ToNewCString(mContentType);
+ return *contentType ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+ }
+ NS_IMETHOD CreateContentTypeHandlerClass(
+ const char* contentType, contentTypeHandlerInitStruct* initString,
+ MimeObjectClass** objClass) override;
+
+ private:
+ virtual ~nsSimpleMimeConverterStub() {}
+ nsCString mContentType;
+};
+
+NS_IMPL_ISUPPORTS(nsSimpleMimeConverterStub, nsIMimeContentTypeHandler)
+
+NS_IMETHODIMP
+nsSimpleMimeConverterStub::CreateContentTypeHandlerClass(
+ const char* contentType, contentTypeHandlerInitStruct* initStruct,
+ MimeObjectClass** objClass) {
+ NS_ENSURE_ARG_POINTER(objClass);
+
+ *objClass = (MimeObjectClass*)&mimeSimpleStubClass;
+ (*objClass)->superclass = (MimeObjectClass*)XPCOM_GetmimeInlineTextClass();
+ NS_ENSURE_TRUE((*objClass)->superclass, NS_ERROR_UNEXPECTED);
+
+ initStruct->force_inline_display = true;
+ return NS_OK;
+ ;
+}
+
+nsresult MIME_NewSimpleMimeConverterStub(const char* aContentType,
+ nsIMimeContentTypeHandler** aResult) {
+ RefPtr<nsSimpleMimeConverterStub> inst =
+ new nsSimpleMimeConverterStub(aContentType);
+ NS_ENSURE_TRUE(inst, NS_ERROR_OUT_OF_MEMORY);
+
+ inst.forget(aResult);
+ return NS_OK;
+}
diff --git a/comm/mailnews/mime/src/nsSimpleMimeConverterStub.h b/comm/mailnews/mime/src/nsSimpleMimeConverterStub.h
new file mode 100644
index 0000000000..ec32091598
--- /dev/null
+++ b/comm/mailnews/mime/src/nsSimpleMimeConverterStub.h
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef NS_SIMPLE_MIME_CONVERTER_STUB_H_
+#define NS_SIMPLE_MIME_CONVERTER_STUB_H_
+
+nsresult MIME_NewSimpleMimeConverterStub(const char* aContentType,
+ nsIMimeContentTypeHandler** aResult);
+
+#endif /* NS_SIMPLE_MIME_CONVERTER_STUB_H_ */
diff --git a/comm/mailnews/mime/src/nsStreamConverter.cpp b/comm/mailnews/mime/src/nsStreamConverter.cpp
new file mode 100644
index 0000000000..7311295402
--- /dev/null
+++ b/comm/mailnews/mime/src/nsStreamConverter.cpp
@@ -0,0 +1,981 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "nsCOMPtr.h"
+#include <stdio.h>
+#include "mimecom.h"
+#include "modmimee.h"
+#include "nscore.h"
+#include "nsStreamConverter.h"
+#include "prmem.h"
+#include "prprf.h"
+#include "prlog.h"
+#include "plstr.h"
+#include "mimemoz2.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsMemory.h"
+#include "nsIPipe.h"
+#include "nsMimeStringResources.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsNetUtil.h"
+#include "nsIMsgQuote.h"
+#include "nsNetUtil.h"
+#include "mozITXTToHTMLConv.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsINntpUrl.h"
+#include "nsIMsgWindow.h"
+#include "nsICategoryManager.h"
+#include "nsMsgUtils.h"
+#include "mozilla/ArrayUtils.h"
+
+#define PREF_MAIL_DISPLAY_GLYPH "mail.display_glyph"
+#define PREF_MAIL_DISPLAY_STRUCT "mail.display_struct"
+
+////////////////////////////////////////////////////////////////
+// Bridge routines for new stream converter XP-COM interface
+////////////////////////////////////////////////////////////////
+
+extern "C" void* mime_bridge_create_draft_stream(
+ nsIMimeEmitter* newEmitter, nsStreamConverter* newPluginObj2, nsIURI* uri,
+ nsMimeOutputType format_out);
+
+extern "C" void* bridge_create_stream(nsIMimeEmitter* newEmitter,
+ nsStreamConverter* newPluginObj2,
+ nsIURI* uri, nsMimeOutputType format_out,
+ uint32_t whattodo, nsIChannel* aChannel) {
+ if ((format_out == nsMimeOutput::nsMimeMessageDraftOrTemplate) ||
+ (format_out == nsMimeOutput::nsMimeMessageEditorTemplate))
+ return mime_bridge_create_draft_stream(newEmitter, newPluginObj2, uri,
+ format_out);
+ else
+ return mime_bridge_create_display_stream(newEmitter, newPluginObj2, uri,
+ format_out, whattodo, aChannel);
+}
+
+void bridge_destroy_stream(void* newStream) {
+ nsMIMESession* stream = (nsMIMESession*)newStream;
+ if (!stream) return;
+
+ PR_FREEIF(stream);
+}
+
+void bridge_set_output_type(void* bridgeStream, nsMimeOutputType aType) {
+ nsMIMESession* session = (nsMIMESession*)bridgeStream;
+
+ if (session) {
+ // BAD ASSUMPTION!!!! NEED TO CHECK aType
+ mime_stream_data* msd = (mime_stream_data*)session->data_object;
+ if (msd) msd->format_out = aType; // output format type
+ }
+}
+
+nsresult bridge_new_new_uri(void* bridgeStream, nsIURI* aURI,
+ int32_t aOutputType) {
+ nsMIMESession* session = (nsMIMESession*)bridgeStream;
+ const char** fixup_pointer = nullptr;
+
+ if (session) {
+ if (session->data_object) {
+ bool* override_charset = nullptr;
+ char** default_charset = nullptr;
+ char** url_name = nullptr;
+
+ if ((aOutputType == nsMimeOutput::nsMimeMessageDraftOrTemplate) ||
+ (aOutputType == nsMimeOutput::nsMimeMessageEditorTemplate)) {
+ mime_draft_data* mdd = (mime_draft_data*)session->data_object;
+ if (mdd->options) {
+ default_charset = &(mdd->options->default_charset);
+ override_charset = &(mdd->options->override_charset);
+ url_name = &(mdd->url_name);
+ }
+ } else {
+ mime_stream_data* msd = (mime_stream_data*)session->data_object;
+
+ if (msd->options) {
+ default_charset = &(msd->options->default_charset);
+ override_charset = &(msd->options->override_charset);
+ url_name = &(msd->url_name);
+ fixup_pointer = &(msd->options->url);
+ }
+ }
+
+ if (default_charset && override_charset && url_name) {
+ // Check whether we need to auto-detect the charset.
+ nsCOMPtr<nsIMsgI18NUrl> i18nUrl(do_QueryInterface(aURI));
+ if (i18nUrl) {
+ bool autodetectCharset = false;
+ nsresult rv = i18nUrl->GetAutodetectCharset(&autodetectCharset);
+ if (NS_SUCCEEDED(rv) && autodetectCharset) {
+ *override_charset = true;
+ *default_charset = nullptr;
+ } else {
+ *override_charset = false;
+ // Special treatment for news: URLs. Get the server default charset.
+ nsCOMPtr<nsINntpUrl> nntpURL(do_QueryInterface(aURI));
+ if (nntpURL) {
+ nsCString charset;
+ rv = nntpURL->GetCharset(charset);
+ if (NS_SUCCEEDED(rv)) {
+ *default_charset = ToNewCString(charset);
+ } else {
+ *default_charset = strdup("UTF-8");
+ }
+ } else {
+ *default_charset = strdup("UTF-8");
+ }
+ }
+ }
+ nsAutoCString urlString;
+ if (NS_SUCCEEDED(aURI->GetSpec(urlString))) {
+ if (!urlString.IsEmpty()) {
+ free(*url_name);
+ *url_name = ToNewCString(urlString);
+ if (!(*url_name)) return NS_ERROR_OUT_OF_MEMORY;
+
+ // rhp: Ugh, this is ugly...but it works.
+ if (fixup_pointer) *fixup_pointer = (const char*)*url_name;
+ }
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+static int mime_headers_callback(void* closure, MimeHeaders* headers) {
+ // We get away with this because this doesn't get called on draft operations.
+ mime_stream_data* msd = (mime_stream_data*)closure;
+
+ NS_ASSERTION(msd && headers, "null mime stream data or headers");
+ if (!msd || !headers) return 0;
+
+ NS_ASSERTION(!msd->headers, "non-null mime stream data headers");
+ msd->headers = MimeHeaders_copy(headers);
+ return 0;
+}
+
+nsresult bridge_set_mime_stream_converter_listener(
+ void* bridgeStream, nsIMimeStreamConverterListener* listener,
+ nsMimeOutputType aOutputType) {
+ nsMIMESession* session = (nsMIMESession*)bridgeStream;
+
+ if ((session) && (session->data_object)) {
+ if ((aOutputType == nsMimeOutput::nsMimeMessageDraftOrTemplate) ||
+ (aOutputType == nsMimeOutput::nsMimeMessageEditorTemplate)) {
+ mime_draft_data* mdd = (mime_draft_data*)session->data_object;
+ if (mdd->options) {
+ if (listener) {
+ mdd->options->caller_need_root_headers = true;
+ mdd->options->decompose_headers_info_fn = mime_headers_callback;
+ } else {
+ mdd->options->caller_need_root_headers = false;
+ mdd->options->decompose_headers_info_fn = nullptr;
+ }
+ }
+ } else {
+ mime_stream_data* msd = (mime_stream_data*)session->data_object;
+
+ if (msd->options) {
+ if (listener) {
+ msd->options->caller_need_root_headers = true;
+ msd->options->decompose_headers_info_fn = mime_headers_callback;
+ } else {
+ msd->options->caller_need_root_headers = false;
+ msd->options->decompose_headers_info_fn = nullptr;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// find a query element in a url and return a pointer to its data
+// (query must be in the form "query=")
+static const char* FindQueryElementData(const char* aUrl, const char* aQuery) {
+ if (aUrl && aQuery) {
+ size_t queryLen = 0; // we don't call strlen until we need to
+ aUrl = PL_strcasestr(aUrl, aQuery);
+ while (aUrl) {
+ if (!queryLen) queryLen = strlen(aQuery);
+ if (*(aUrl - 1) == '&' || *(aUrl - 1) == '?') return aUrl + queryLen;
+ aUrl = PL_strcasestr(aUrl + queryLen, aQuery);
+ }
+ }
+ return nullptr;
+}
+
+// case-sensitive test for string prefixing. If |string| is prefixed
+// by |prefix| then a pointer to the next character in |string| following
+// the prefix is returned. If it is not a prefix then |nullptr| is returned.
+static const char* SkipPrefix(const char* aString, const char* aPrefix) {
+ while (*aPrefix)
+ if (*aPrefix++ != *aString++) return nullptr;
+ return aString;
+}
+
+//
+// Utility routines needed by this interface
+//
+nsresult nsStreamConverter::DetermineOutputFormat(const char* aUrl,
+ nsMimeOutputType* aNewType) {
+ // sanity checking
+ NS_ENSURE_ARG_POINTER(aNewType);
+ if (!aUrl || !*aUrl) {
+ // default to html for the entire document
+ *aNewType = nsMimeOutput::nsMimeMessageQuoting;
+ mOutputFormat = "text/html";
+ return NS_OK;
+ }
+
+ // shorten the url that we test for the query strings by skipping directly
+ // to the part where the query strings begin.
+ const char* queryPart = PL_strchr(aUrl, '?');
+
+ // First, did someone pass in a desired output format. They will be able to
+ // pass in any content type (i.e. image/gif, text/html, etc...but the "/" will
+ // have to be represented via the "%2F" value
+ const char* format = FindQueryElementData(queryPart, "outformat=");
+ if (format) {
+ // NOTE: I've done a file contents search of every file (*.*) in the mozilla
+ // directory tree and there is not a single location where the string
+ // "outformat" is added to any URL. It appears that this code has been
+ // orphaned off by a change elsewhere and is no longer required. It will be
+ // removed in the future unless someone complains.
+ MOZ_ASSERT(false, "Is this code actually being used?");
+
+ while (*format == ' ') ++format;
+
+ if (*format) {
+ mOverrideFormat = "raw";
+
+ // set mOutputFormat to the supplied format, ensure that we replace any
+ // %2F strings with the slash character
+ const char* nextField = PL_strpbrk(format, "&; ");
+ mOutputFormat.Assign(format, nextField ? nextField - format : -1);
+ mOutputFormat.ReplaceSubstring("%2F", "/");
+ mOutputFormat.ReplaceSubstring("%2f", "/");
+
+ // Don't muck with this data!
+ *aNewType = nsMimeOutput::nsMimeMessageRaw;
+ return NS_OK;
+ }
+ }
+
+ // is this is a part that should just come out raw
+ const char* part = FindQueryElementData(queryPart, "part=");
+ if (part && !mToType.EqualsLiteral("application/xhtml+xml")) {
+ // default for parts
+ mOutputFormat = "raw";
+ *aNewType = nsMimeOutput::nsMimeMessageRaw;
+
+ // if we are being asked to fetch a part....it should have a
+ // content type appended to it...if it does, we want to remember
+ // that as mOutputFormat
+ const char* typeField = FindQueryElementData(queryPart, "type=");
+ if (typeField && !strncmp(typeField, "application/x-message-display",
+ sizeof("application/x-message-display") - 1)) {
+ const char* secondTypeField = FindQueryElementData(typeField, "type=");
+ if (secondTypeField) typeField = secondTypeField;
+ }
+ if (typeField) {
+ // store the real content type...mOutputFormat gets deleted later on...
+ // and make sure we only get our own value.
+ char* nextField = PL_strchr(typeField, '&');
+ mRealContentType.Assign(typeField,
+ nextField ? nextField - typeField : -1);
+ if (mRealContentType.EqualsLiteral("message/rfc822")) {
+ mRealContentType = "application/x-message-display";
+ mOutputFormat = "text/html";
+ *aNewType = nsMimeOutput::nsMimeMessageBodyDisplay;
+ } else if (mRealContentType.EqualsLiteral(
+ "application/x-message-display")) {
+ mRealContentType = "";
+ mOutputFormat = "text/html";
+ *aNewType = nsMimeOutput::nsMimeMessageBodyDisplay;
+ }
+ }
+
+ return NS_OK;
+ }
+
+ const char* emitter = FindQueryElementData(queryPart, "emitter=");
+ if (emitter) {
+ const char* remainder = SkipPrefix(emitter, "js");
+ if (remainder && (!*remainder || *remainder == '&'))
+ mOverrideFormat = "application/x-js-mime-message";
+ }
+
+ // if using the header query
+ const char* header = FindQueryElementData(queryPart, "header=");
+ if (header) {
+ struct HeaderType {
+ const char* headerType;
+ const char* outputFormat;
+ nsMimeOutputType mimeOutputType;
+ };
+
+ // place most commonly used options at the top
+ static const struct HeaderType rgTypes[] = {
+ {"filter", "text/html", nsMimeOutput::nsMimeMessageFilterSniffer},
+ {"quotebody", "text/html", nsMimeOutput::nsMimeMessageBodyQuoting},
+ {"print", "text/html", nsMimeOutput::nsMimeMessagePrintOutput},
+ {"only", "text/xml", nsMimeOutput::nsMimeMessageHeaderDisplay},
+ {"none", "text/html", nsMimeOutput::nsMimeMessageBodyDisplay},
+ {"quote", "text/html", nsMimeOutput::nsMimeMessageQuoting},
+ {"saveas", "text/html", nsMimeOutput::nsMimeMessageSaveAs},
+ {"src", "text/plain", nsMimeOutput::nsMimeMessageSource},
+ {"attach", "raw", nsMimeOutput::nsMimeMessageAttach}};
+
+ // find the requested header in table, ensure that we don't match on a
+ // prefix by checking that the following character is either null or the
+ // next query element
+ const char* remainder;
+ for (uint32_t n = 0; n < MOZ_ARRAY_LENGTH(rgTypes); ++n) {
+ remainder = SkipPrefix(header, rgTypes[n].headerType);
+ if (remainder && (*remainder == '\0' || *remainder == '&')) {
+ mOutputFormat = rgTypes[n].outputFormat;
+ *aNewType = rgTypes[n].mimeOutputType;
+ return NS_OK;
+ }
+ }
+ }
+
+ // default to html for just the body
+ mOutputFormat = "text/html";
+ *aNewType = nsMimeOutput::nsMimeMessageBodyDisplay;
+
+ return NS_OK;
+}
+
+nsresult nsStreamConverter::InternalCleanup(void) {
+ if (mBridgeStream) {
+ bridge_destroy_stream(mBridgeStream);
+ mBridgeStream = nullptr;
+ }
+
+ return NS_OK;
+}
+
+/*
+ * Inherited methods for nsMimeConverter
+ */
+nsStreamConverter::nsStreamConverter() {
+ // Init member variables...
+ mWrapperOutput = false;
+ mBridgeStream = nullptr;
+ mOutputFormat = "text/html";
+ mAlreadyKnowOutputType = false;
+ mForwardInline = false;
+ mForwardInlineFilter = false;
+ mOverrideComposeFormat = false;
+
+ mPendingRequest = nullptr;
+}
+
+nsStreamConverter::~nsStreamConverter() { InternalCleanup(); }
+
+NS_IMPL_ISUPPORTS(nsStreamConverter, nsIStreamListener, nsIRequestObserver,
+ nsIStreamConverter, nsIMimeStreamConverter)
+
+///////////////////////////////////////////////////////////////
+// nsStreamConverter definitions....
+///////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsStreamConverter::Init(nsIURI* aURI,
+ nsIStreamListener* aOutListener,
+ nsIChannel* aChannel) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsresult rv = NS_OK;
+ mOutListener = aOutListener;
+ mOutgoingChannel = aChannel;
+
+ // mscott --> we need to look at the url and figure out what the correct
+ // output type is...
+ nsMimeOutputType newType = mOutputType;
+ if (!mAlreadyKnowOutputType) {
+ nsAutoCString urlSpec;
+ rv = aURI->GetSpecIgnoringRef(urlSpec);
+ DetermineOutputFormat(urlSpec.get(), &newType);
+ mAlreadyKnowOutputType = true;
+ mOutputType = newType;
+ }
+
+ switch (newType) {
+ case nsMimeOutput::nsMimeMessageSplitDisplay: // the wrapper HTML output to
+ // produce the split
+ // header/body display
+ mWrapperOutput = true;
+ mOutputFormat = "text/html";
+ break;
+ case nsMimeOutput::nsMimeMessageHeaderDisplay: // the split header/body
+ // display
+ mOutputFormat = "text/xml";
+ break;
+ case nsMimeOutput::nsMimeMessageBodyDisplay: // the split header/body
+ // display
+ mOutputFormat = "text/html";
+ break;
+
+ case nsMimeOutput::nsMimeMessageQuoting: // all HTML quoted output
+ case nsMimeOutput::nsMimeMessageSaveAs: // Save as operation
+ case nsMimeOutput::nsMimeMessageBodyQuoting: // only HTML body quoted
+ // output
+ case nsMimeOutput::nsMimeMessagePrintOutput: // all Printing output
+ mOutputFormat = "text/html";
+ break;
+
+ case nsMimeOutput::nsMimeMessageAttach:
+ case nsMimeOutput::nsMimeMessageDecrypt:
+ case nsMimeOutput::nsMimeMessageRaw: // the raw RFC822 data and attachments
+ mOutputFormat = "raw";
+ break;
+
+ case nsMimeOutput::nsMimeMessageSource: // the raw RFC822 data (view
+ // source) and attachments
+ mOutputFormat = "text/plain";
+ mOverrideFormat = "raw";
+ break;
+
+ case nsMimeOutput::nsMimeMessageDraftOrTemplate: // Loading drafts &
+ // templates
+ mOutputFormat = "message/draft";
+ break;
+
+ case nsMimeOutput::nsMimeMessageEditorTemplate: // Loading templates into
+ // editor
+ mOutputFormat = "text/html";
+ break;
+
+ case nsMimeOutput::nsMimeMessageFilterSniffer: // output all displayable
+ // part as raw
+ mOutputFormat = "text/html";
+ break;
+
+ default:
+ NS_ERROR("this means I made a mistake in my assumptions");
+ }
+
+ // the following output channel stream is used to fake the content type for
+ // people who later call into us..
+ nsCString contentTypeToUse;
+ GetContentType(getter_Copies(contentTypeToUse));
+ // mscott --> my theory is that we don't need this fake outgoing channel.
+ // Let's use the original channel and just set our content type ontop of the
+ // original channel...
+
+ aChannel->SetContentType(contentTypeToUse);
+
+ // rv = NS_NewInputStreamChannel(getter_AddRefs(mOutgoingChannel), aURI,
+ // nullptr, contentTypeToUse, -1); if (NS_FAILED(rv))
+ // return rv;
+
+ // Set system principal for this document, which will be dynamically generated
+
+ // We will first find an appropriate emitter in the repository that supports
+ // the requested output format...note, the special exceptions are
+ // nsMimeMessageDraftOrTemplate or nsMimeMessageEditorTemplate where we don't
+ // need any emitters
+ //
+
+ if ((newType != nsMimeOutput::nsMimeMessageDraftOrTemplate) &&
+ (newType != nsMimeOutput::nsMimeMessageEditorTemplate)) {
+ nsAutoCString categoryName("@mozilla.org/messenger/mimeemitter;1?type=");
+ if (!mOverrideFormat.IsEmpty())
+ categoryName += mOverrideFormat;
+ else
+ categoryName += mOutputFormat;
+
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCString contractID;
+ catman->GetCategoryEntry("mime-emitter", categoryName, contractID);
+ if (!contractID.IsEmpty()) categoryName = contractID;
+ }
+
+ mEmitter = do_CreateInstance(categoryName.get(), &rv);
+
+ if ((NS_FAILED(rv)) || (!mEmitter)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ // initialize our emitter
+ if (mEmitter) {
+ // Now we want to create a pipe which we'll use for converting the data.
+ nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1");
+ rv = pipe->Init(true, true, 4096, 8);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // These always succeed because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetInputStream(getter_AddRefs(mInputStream)));
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetOutputStream(getter_AddRefs(mOutputStream)));
+
+ mEmitter->Initialize(aURI, aChannel, newType);
+ mEmitter->SetPipe(mInputStream, mOutputStream);
+ mEmitter->SetOutputListener(aOutListener);
+ }
+
+ uint32_t whattodo = mozITXTToHTMLConv::kURLs;
+ bool enable_emoticons = true;
+ bool enable_structs = true;
+
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (pPrefBranch) {
+ rv = pPrefBranch->GetBoolPref(PREF_MAIL_DISPLAY_GLYPH, &enable_emoticons);
+ if (NS_FAILED(rv) || enable_emoticons) {
+ whattodo = whattodo | mozITXTToHTMLConv::kGlyphSubstitution;
+ }
+ rv = pPrefBranch->GetBoolPref(PREF_MAIL_DISPLAY_STRUCT, &enable_structs);
+ if (NS_FAILED(rv) || enable_structs) {
+ whattodo = whattodo | mozITXTToHTMLConv::kStructPhrase;
+ }
+ }
+
+ if (mOutputType == nsMimeOutput::nsMimeMessageSource)
+ return NS_OK;
+ else {
+ mBridgeStream =
+ bridge_create_stream(mEmitter, this, aURI, newType, whattodo, aChannel);
+ if (!mBridgeStream)
+ return NS_ERROR_OUT_OF_MEMORY;
+ else {
+ SetStreamURI(aURI);
+
+ // Do we need to setup an Mime Stream Converter Listener?
+ if (mMimeStreamConverterListener)
+ bridge_set_mime_stream_converter_listener((nsMIMESession*)mBridgeStream,
+ mMimeStreamConverterListener,
+ mOutputType);
+
+ return NS_OK;
+ }
+ }
+}
+
+NS_IMETHODIMP nsStreamConverter::GetContentType(char** aOutputContentType) {
+ if (!aOutputContentType) return NS_ERROR_NULL_POINTER;
+
+ // since this method passes a string through an IDL file we need to use
+ // nsMemory to allocate it and not strdup!
+ // (1) check to see if we have a real content type...use it first...
+ if (!mRealContentType.IsEmpty())
+ *aOutputContentType = ToNewCString(mRealContentType);
+ else if (mOutputFormat.EqualsLiteral("raw"))
+ *aOutputContentType =
+ (char*)moz_xmemdup(UNKNOWN_CONTENT_TYPE, sizeof(UNKNOWN_CONTENT_TYPE));
+ else
+ *aOutputContentType = ToNewCString(mOutputFormat);
+ return NS_OK;
+}
+
+//
+// This is the type of output operation that is being requested by libmime. The
+// types of output are specified by nsIMimeOutputType enum
+//
+nsresult nsStreamConverter::SetMimeOutputType(nsMimeOutputType aType) {
+ mAlreadyKnowOutputType = true;
+ mOutputType = aType;
+ if (mBridgeStream) bridge_set_output_type(mBridgeStream, aType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStreamConverter::GetMimeOutputType(
+ nsMimeOutputType* aOutFormat) {
+ nsresult rv = NS_OK;
+ if (aOutFormat)
+ *aOutFormat = mOutputType;
+ else
+ rv = NS_ERROR_NULL_POINTER;
+
+ return rv;
+}
+
+//
+// This is needed by libmime for MHTML link processing...this is the URI
+// associated with this input stream
+//
+nsresult nsStreamConverter::SetStreamURI(nsIURI* aURI) {
+ mURI = aURI;
+ if (mBridgeStream)
+ return bridge_new_new_uri((nsMIMESession*)mBridgeStream, aURI, mOutputType);
+ else
+ return NS_OK;
+}
+
+nsresult nsStreamConverter::SetMimeHeadersListener(
+ nsIMimeStreamConverterListener* listener, nsMimeOutputType aType) {
+ mMimeStreamConverterListener = listener;
+ bridge_set_mime_stream_converter_listener((nsMIMESession*)mBridgeStream,
+ listener, aType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetForwardInline(bool aForwardInline) {
+ mForwardInline = aForwardInline;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetForwardToAddress(nsAString& aAddress) {
+ aAddress = mForwardToAddress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetForwardToAddress(const nsAString& aAddress) {
+ mForwardToAddress = aAddress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetOverrideComposeFormat(bool* aResult) {
+ if (!aResult) return NS_ERROR_NULL_POINTER;
+ *aResult = mOverrideComposeFormat;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetOverrideComposeFormat(bool aOverrideComposeFormat) {
+ mOverrideComposeFormat = aOverrideComposeFormat;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetForwardInline(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mForwardInline;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetForwardInlineFilter(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mForwardInlineFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetForwardInlineFilter(bool aForwardInlineFilter) {
+ mForwardInlineFilter = aForwardInlineFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetIdentity(nsIMsgIdentity** aIdentity) {
+ if (!aIdentity) return NS_ERROR_NULL_POINTER;
+ // We don't have an identity for the local folders account,
+ // we will return null but it is not an error!
+ NS_IF_ADDREF(*aIdentity = mIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetIdentity(nsIMsgIdentity* aIdentity) {
+ mIdentity = aIdentity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetOriginalMsgURI(const nsACString& originalMsgURI) {
+ mOriginalMsgURI = originalMsgURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetOriginalMsgURI(nsACString& result) {
+ result = mOriginalMsgURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::SetOrigMsgHdr(nsIMsgDBHdr* aMsgHdr) {
+ mOrigMsgHdr = aMsgHdr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStreamConverter::GetOrigMsgHdr(nsIMsgDBHdr** aMsgHdr) {
+ if (!aMsgHdr) return NS_ERROR_NULL_POINTER;
+ NS_IF_ADDREF(*aMsgHdr = mOrigMsgHdr);
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// Methods for nsIStreamListener...
+/////////////////////////////////////////////////////////////////////////////
+//
+// Notify the client that data is available in the input stream. This
+// method is called whenever data is written into the input stream by the
+// networking library...
+//
+nsresult nsStreamConverter::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* aIStream,
+ uint64_t sourceOffset,
+ uint32_t aLength) {
+ nsresult rc = NS_OK; // should this be an error instead?
+ uint32_t written;
+
+ // If this is the first time through and we are supposed to be
+ // outputting the wrapper two pane URL, then do it now.
+ if (mWrapperOutput) {
+ char outBuf[1024];
+ const char output[] =
+ "\
+<HTML>\
+<FRAMESET ROWS=\"30%%,70%%\">\
+<FRAME NAME=messageHeader SRC=\"%s?header=only\">\
+<FRAME NAME=messageBody SRC=\"%s?header=none\">\
+</FRAMESET>\
+</HTML>";
+
+ nsAutoCString url;
+ if (NS_FAILED(mURI->GetSpec(url))) return NS_ERROR_FAILURE;
+
+ PR_snprintf(outBuf, sizeof(outBuf), output, url.get(), url.get());
+
+ if (mEmitter) mEmitter->Write(nsDependentCString(outBuf), &written);
+
+ // rhp: will this stop the stream???? Not sure.
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIInputStream> stream = aIStream;
+ NS_ENSURE_TRUE(stream, NS_ERROR_NULL_POINTER);
+ char* buf = (char*)PR_Malloc(aLength);
+ if (!buf) return NS_ERROR_OUT_OF_MEMORY; /* we couldn't allocate the object */
+
+ uint32_t readLen;
+ stream->Read(buf, aLength, &readLen);
+
+ // We need to filter out any null characters else we will have a lot of
+ // trouble as we use c string everywhere in mime
+ char* readPtr;
+ char* endPtr = buf + readLen;
+
+ // First let see if the stream contains null characters
+ for (readPtr = buf; readPtr < endPtr && *readPtr; readPtr++)
+ ;
+
+ // Did we find a null character? Then, we need to cleanup the stream
+ if (readPtr < endPtr) {
+ char* writePtr = readPtr;
+ for (readPtr++; readPtr < endPtr; readPtr++) {
+ if (!*readPtr) continue;
+
+ *writePtr = *readPtr;
+ writePtr++;
+ }
+ readLen = writePtr - buf;
+ }
+
+ if (mOutputType == nsMimeOutput::nsMimeMessageSource) {
+ rc = NS_OK;
+ if (mEmitter) {
+ rc = mEmitter->Write(Substring(buf, buf + readLen), &written);
+ }
+ } else if (mBridgeStream) {
+ nsMIMESession* tSession = (nsMIMESession*)mBridgeStream;
+ // XXX Casting int to nsresult
+ rc = static_cast<nsresult>(
+ tSession->put_block((nsMIMESession*)mBridgeStream, buf, readLen));
+ }
+
+ PR_FREEIF(buf);
+ return rc;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// Methods for nsIRequestObserver
+/////////////////////////////////////////////////////////////////////////////
+//
+// Notify the observer that the URL has started to load. This method is
+// called only once, at the beginning of a URL load.
+//
+nsresult nsStreamConverter::OnStartRequest(nsIRequest* request) {
+#ifdef DEBUG_rhp
+ printf("nsStreamConverter::OnStartRequest()\n");
+#endif
+
+#ifdef DEBUG_mscott
+ mConvertContentTime = PR_IntervalNow();
+#endif
+
+ // here's a little bit of hackery....
+ // since the mime converter is now between the channel
+ // and the
+ if (request) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+ if (channel) {
+ nsCString contentType;
+ GetContentType(getter_Copies(contentType));
+
+ channel->SetContentType(contentType);
+ }
+ }
+
+ // forward the start request to any listeners
+ if (mOutListener) {
+ if (mOutputType == nsMimeOutput::nsMimeMessageRaw) {
+ // we need to delay the on start request until we have figure out the real
+ // content type
+ mPendingRequest = request;
+ } else
+ mOutListener->OnStartRequest(request);
+ }
+
+ return NS_OK;
+}
+
+//
+// Notify the observer that the URL has finished loading. This method is
+// called once when the networking library has finished processing the
+//
+nsresult nsStreamConverter::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ // Make sure we fire any pending OnStartRequest before we do OnStop.
+ FirePendingStartRequest();
+
+ //
+ // Now complete the stream!
+ //
+ if (mBridgeStream) {
+ nsMIMESession* tSession = (nsMIMESession*)mBridgeStream;
+
+ if (mMimeStreamConverterListener) {
+ MimeHeaders** workHeaders = nullptr;
+
+ if ((mOutputType == nsMimeOutput::nsMimeMessageDraftOrTemplate) ||
+ (mOutputType == nsMimeOutput::nsMimeMessageEditorTemplate)) {
+ mime_draft_data* mdd = (mime_draft_data*)tSession->data_object;
+ if (mdd) workHeaders = &(mdd->headers);
+ } else {
+ mime_stream_data* msd = (mime_stream_data*)tSession->data_object;
+ if (msd) workHeaders = &(msd->headers);
+ }
+
+ if (workHeaders) {
+ nsresult rv;
+ nsCOMPtr<nsIMimeHeaders> mimeHeaders =
+ do_CreateInstance(NS_IMIMEHEADERS_CONTRACTID, &rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ if (*workHeaders)
+ mimeHeaders->Initialize(Substring((*workHeaders)->all_headers,
+ (*workHeaders)->all_headers_fp));
+ mMimeStreamConverterListener->OnHeadersReady(mimeHeaders);
+ } else
+ mMimeStreamConverterListener->OnHeadersReady(nullptr);
+ }
+
+ mMimeStreamConverterListener = nullptr; // release our reference
+ }
+
+ tSession->complete((nsMIMESession*)mBridgeStream);
+ }
+
+ //
+ // Now complete the emitter and do necessary cleanup!
+ //
+ if (mEmitter) {
+ mEmitter->Complete();
+ }
+
+ // First close the output stream...
+ if (mOutputStream) mOutputStream->Close();
+
+ if (mOutgoingChannel) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ mOutgoingChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (loadGroup) loadGroup->RemoveRequest(mOutgoingChannel, nullptr, status);
+ }
+
+ // Make sure to do necessary cleanup!
+ InternalCleanup();
+
+ // forward on top request to any listeners
+ if (mOutListener) mOutListener->OnStopRequest(request, status);
+
+ mAlreadyKnowOutputType = false;
+
+ // since we are done converting data, lets close all the objects we own...
+ // this helps us fix some circular ref counting problems we are running
+ // into...
+ Close();
+
+ // Time to return...
+ return NS_OK;
+}
+
+nsresult nsStreamConverter::Close() {
+ mOutgoingChannel = nullptr;
+ mEmitter = nullptr;
+ mOutListener = nullptr;
+ return NS_OK;
+}
+
+// nsIStreamConverter implementation
+
+// No synchronous conversion at this time.
+NS_IMETHODIMP nsStreamConverter::Convert(nsIInputStream* aFromStream,
+ const char* aFromType,
+ const char* aToType,
+ nsISupports* aCtxt,
+ nsIInputStream** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// Stream converter service calls this to initialize the actual stream converter
+// (us).
+NS_IMETHODIMP nsStreamConverter::AsyncConvertData(const char* aFromType,
+ const char* aToType,
+ nsIStreamListener* aListener,
+ nsISupports* aCtxt) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgQuote> aMsgQuote = do_QueryInterface(aCtxt, &rv);
+ nsCOMPtr<nsIChannel> aChannel;
+
+ if (aMsgQuote) {
+ nsCOMPtr<nsIMimeStreamConverterListener> quoteListener;
+ rv = aMsgQuote->GetQuoteListener(getter_AddRefs(quoteListener));
+ if (quoteListener)
+ SetMimeHeadersListener(quoteListener, nsMimeOutput::nsMimeMessageQuoting);
+ rv = aMsgQuote->GetQuoteChannel(getter_AddRefs(aChannel));
+ } else {
+ aChannel = do_QueryInterface(aCtxt, &rv);
+ }
+
+ mFromType = aFromType;
+ mToType = aToType;
+
+ NS_ASSERTION(aChannel && NS_SUCCEEDED(rv),
+ "mailnews mime converter has to have the channel passed in...");
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIURI> aUri;
+ aChannel->GetURI(getter_AddRefs(aUri));
+ return Init(aUri, aListener, aChannel);
+}
+
+NS_IMETHODIMP nsStreamConverter::FirePendingStartRequest() {
+ if (mPendingRequest && mOutListener) {
+ mOutListener->OnStartRequest(mPendingRequest);
+ mPendingRequest = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStreamConverter::GetConvertedType(const nsACString& aFromType,
+ nsIChannel* aChannel,
+ nsACString& aToType) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/mime/src/nsStreamConverter.h b/comm/mailnews/mime/src/nsStreamConverter.h
new file mode 100644
index 0000000000..1d2e1bcccc
--- /dev/null
+++ b/comm/mailnews/mime/src/nsStreamConverter.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#ifndef nsStreamConverter_h_
+#define nsStreamConverter_h_
+
+#include "nsIStreamConverter.h"
+#include "nsIMimeStreamConverter.h"
+#include "nsIMimeEmitter.h"
+#include "nsIURI.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIChannel.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+#define MIME_FORWARD_HTML_PREFIX "<HTML><BODY><BR><BR>"
+
+class nsStreamConverter : public nsIStreamConverter,
+ public nsIMimeStreamConverter {
+ public:
+ nsStreamConverter();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIMimeStreamConverter support
+ NS_DECL_NSIMIMESTREAMCONVERTER
+ // nsIStreamConverter methods
+ NS_DECL_NSISTREAMCONVERTER
+ // nsIStreamListener methods
+ NS_DECL_NSISTREAMLISTENER
+
+ // nsIRequestObserver methods
+ NS_DECL_NSIREQUESTOBSERVER
+
+ ////////////////////////////////////////////////////////////////////////////
+ // nsStreamConverter specific methods:
+ ////////////////////////////////////////////////////////////////////////////
+ NS_IMETHOD Init(nsIURI* aURI, nsIStreamListener* aOutListener,
+ nsIChannel* aChannel);
+ NS_IMETHOD GetContentType(char** aOutputContentType);
+ NS_IMETHOD InternalCleanup(void);
+ NS_IMETHOD DetermineOutputFormat(const char* url, nsMimeOutputType* newType);
+ NS_IMETHOD FirePendingStartRequest(void);
+
+ private:
+ virtual ~nsStreamConverter();
+ nsresult Close();
+
+ // the input and output streams form a pipe...they need to be passed around
+ // together..
+ nsCOMPtr<nsIAsyncOutputStream> mOutputStream; // output stream
+ nsCOMPtr<nsIAsyncInputStream> mInputStream;
+
+ nsCOMPtr<nsIStreamListener> mOutListener; // output stream listener
+ nsCOMPtr<nsIChannel> mOutgoingChannel;
+
+ nsCOMPtr<nsIMimeEmitter> mEmitter; // emitter being used...
+ nsCOMPtr<nsIURI> mURI; // URI being processed
+ nsMimeOutputType mOutputType; // the output type we should use for the
+ // operation
+ bool mAlreadyKnowOutputType;
+
+ void* mBridgeStream; // internal libmime data stream
+
+ // Type of output, entire message, header only, body only
+ nsCString mOutputFormat;
+ nsCString mRealContentType; // if we know the content type for real, this
+ // will be set (used by attachments)
+
+ nsCString
+ mOverrideFormat; // this is a possible override for emitter creation
+ bool mWrapperOutput; // Should we output the frame split message display
+
+ nsCOMPtr<nsIMimeStreamConverterListener> mMimeStreamConverterListener;
+ bool mForwardInline;
+ bool mForwardInlineFilter;
+ bool mOverrideComposeFormat;
+ nsString mForwardToAddress;
+ nsCOMPtr<nsIMsgIdentity> mIdentity;
+ nsCString mOriginalMsgURI;
+ nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr;
+
+ nsCString mFromType;
+ nsCString mToType;
+#ifdef DEBUG_mscott
+ PRTime mConvertContentTime;
+#endif
+ nsIRequest* mPendingRequest; // used when we need to delay to fire
+ // onStartRequest
+};
+
+#endif /* nsStreamConverter_h_ */
diff --git a/comm/mailnews/mime/test/TestMimeCrash.cpp b/comm/mailnews/mime/test/TestMimeCrash.cpp
new file mode 100644
index 0000000000..f041d8f910
--- /dev/null
+++ b/comm/mailnews/mime/test/TestMimeCrash.cpp
@@ -0,0 +1,63 @@
+// This is a crash test for Bug 556351
+
+#include "nsCOMPtr.h"
+#include "nsIMimeConverter.h"
+#include "nsServiceManagerUtils.h"
+
+#include "prshma.h"
+#include "prsystem.h"
+
+#include "TestHarness.h"
+
+nsresult mime_encoder_output_fn(const char* buf, int32_t size, void* closure) {
+ return NS_OK;
+}
+
+nsresult do_test(const char* aBuffer, const uint32_t aSize) {
+ nsresult rv;
+ MimeEncoderData* encodeData = nullptr;
+ int32_t written = 0;
+
+ nsCOMPtr<nsIMimeConverter> converter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = converter->QPEncoderInit(mime_encoder_output_fn, nullptr, &encodeData);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = converter->EncoderWrite(encodeData, aBuffer, aSize, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = converter->EncoderDestroy(encodeData, false);
+ return rv;
+}
+
+int main(int argc, char** argv) {
+ ScopedXPCOM xpcom("TestMimeCrash");
+ if (xpcom.failed()) return 1;
+
+ // We cannot use malloc() since this crashes depends on memory allocation.
+ // By using mmap()/PR_MemMap(), end of buffer that is last in the page
+ // sets LF.
+
+ uint32_t bufsize = PR_GetPageSize();
+ PRFileMap* fm = PR_OpenAnonFileMap(".", bufsize, PR_PROT_READWRITE);
+ if (!fm) return 1;
+ char* addr = (char*)PR_MemMap(fm, 0, bufsize);
+ if (!addr) return 1;
+ memset(addr, '\r', bufsize);
+
+ nsresult rv = do_test(addr, bufsize);
+
+ PR_MemUnmap(addr, bufsize);
+ PR_CloseFileMap(fm);
+
+ if (NS_FAILED(rv)) {
+ fail("cannot use nsIMimeConverter error=%08x\n", rv);
+ return -1;
+ }
+
+ passed("no crash");
+
+ return 0;
+}
diff --git a/comm/mailnews/mime/test/moz.build b/comm/mailnews/mime/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/mime/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/mime/test/unit/custom_header.js b/comm/mailnews/mime/test/unit/custom_header.js
new file mode 100644
index 0000000000..af2af97780
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/custom_header.js
@@ -0,0 +1,11 @@
+// Custom header support for testing structured_headers.js
+
+/* globals jsmime */
+
+jsmime.headerparser.addStructuredDecoder("X-Unusual", function (hdrs) {
+ return Number.parseInt(hdrs[hdrs.length - 1], 16);
+});
+
+jsmime.headeremitter.addStructuredEncoder("X-Unusual", function (val) {
+ this.addUnstructured(val.toString(16));
+});
diff --git a/comm/mailnews/mime/test/unit/head_mime.js b/comm/mailnews/mime/test/unit/head_mime.js
new file mode 100644
index 0000000000..868a7ceace
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/head_mime.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+/**
+ * Utility code for converting encoded MIME data.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
+
+function apply_mime_conversion(msgUri, smimeHeaderSink) {
+ let service = MailServices.messageServiceFromURI(msgUri);
+
+ // This is what we listen on in the end.
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Make the underlying channel--we need this for the converter parameter.
+ let url = service.getUrlForUri(msgUri);
+
+ let channel = Services.io.newChannelFromURI(
+ url,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ channel.QueryInterface(Ci.nsIMailChannel).smimeHeaderSink = smimeHeaderSink;
+
+ // Make the MIME converter, using the listener we first set up.
+ let converter = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService)
+ .asyncConvertData("message/rfc822", "text/html", listener, channel);
+
+ // Now load the message, run it through the converter, and wait for all the
+ // data to stream through.
+ channel.asyncOpen(converter);
+ return listener;
+}
diff --git a/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js b/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js
new file mode 100644
index 0000000000..71d31da9ac
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+// This tests minimal mime encoding fixed in bug 458685
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ var i;
+
+ var checks = [
+ ["", false, ""],
+ ["\u0436", false, "=?UTF-8?B?0LY=?="], // CYRILLIC SMALL LETTER ZHE
+ ["IamASCII", false, "IamASCII"],
+ // Although an invalid email, we shouldn't crash on it (bug 479206)
+ ["crash test@foo.invalid>", true, '"crash test"@foo.invalid'],
+ [
+ "MXR now displays links to Github log & Blame for\r\n Gaia/Rust/Servo",
+ false,
+ "MXR now displays links to Github log & Blame for\r\n Gaia/Rust/Servo",
+ ],
+ ["-----------------------:", false, "-----------------------:"],
+ ];
+
+ for (i = 0; i < checks.length; ++i) {
+ Assert.equal(
+ MailServices.mimeConverter.encodeMimePartIIStr_UTF8(
+ checks[i][0],
+ checks[i][1],
+ "Subject".length,
+ 72
+ ),
+ checks[i][2]
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js b/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js
new file mode 100644
index 0000000000..2dfeabf7ff
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js
@@ -0,0 +1,58 @@
+/* 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 { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+const P7M_ATTACHMENT = "dGhpcyBpcyBub3QgYSByZWFsIHMvbWltZSBwN20gZW50aXR5";
+var messageGenerator = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+var msgHdr;
+
+add_setup(async function () {
+ // Create a message with a p7m attachment.
+ let synMsg = messageGenerator.makeMessage({
+ attachments: [
+ {
+ body: P7M_ATTACHMENT,
+ filename: "test.txt.p7m",
+ contentType: "application/pkcs7-mime",
+ format: "",
+ encoding: "base64",
+ },
+ ],
+ });
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+ msgHdr = synSet.getMsgHdr(0);
+});
+
+add_task(async function test_mime_p7m_external_foo_pref() {
+ Services.prefs.setBoolPref("mailnews.p7m_external", true);
+
+ await new Promise(resolve => {
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ Assert.ok(aMimeMsg.allUserAttachments.length == 1);
+ resolve();
+ });
+ });
+});
+add_task(async function test_mime_p7m_external_all_external_pref() {
+ Services.prefs.setBoolPref("mailnews.p7m_external", false);
+
+ await new Promise(resolve => {
+ MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) {
+ Assert.ok(aMimeMsg.allUserAttachments.length == 1);
+ resolve();
+ });
+ });
+});
diff --git a/comm/mailnews/mime/test/unit/test_attachment_size.js b/comm/mailnews/mime/test/unit/test_attachment_size.js
new file mode 100644
index 0000000000..cf6e68f0bf
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_attachment_size.js
@@ -0,0 +1,322 @@
+/* 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/. */
+
+/**
+ * This test creates some messages with attachments of different types and
+ * checks that libmime reports the expected size for each of them.
+ */
+
+var {
+ MessageGenerator,
+ SyntheticPartLeaf,
+ SyntheticPartMultiMixed,
+ SyntheticMessageSet,
+} = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Somehow we hit the blocklist service, and that needs appInfo defined
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+// Create a message generator
+var msgGen = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+
+/* Today's gory details (thanks to Jonathan Protzenko): libmime somehow
+ * counts the trailing newline for an attachment MIME part. Most of the time,
+ * assuming attachment has N bytes (no matter what's inside, newlines or
+ * not), libmime will return N + 1 bytes. On Linux and Mac, this always
+ * holds. However, on Windows, if the attachment is not encoded (that is, is
+ * inline text), libmime will return N + 2 bytes.
+ */
+const EPSILON = "@mozilla.org/windows-registry-key;1" in Cc ? 4 : 2;
+
+const TEXT_ATTACHMENT =
+ "Can't make the frug contest, Helen; stomach's upset. I'll fix you, " +
+ "Ubik! Ubik drops you back in the thick of things fast. Taken as " +
+ "directed, Ubik speeds relief to head and stomach. Remember: Ubik is " +
+ "only seconds away. Avoid prolonged use.";
+
+const BINARY_ATTACHMENT = TEXT_ATTACHMENT;
+
+const IMAGE_ATTACHMENT =
+ "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" +
+ "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" +
+ "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" +
+ "SNQAAlmAY+71EgFoAAAAASUVORK5CYII=";
+const IMAGE_SIZE = 188;
+
+const UU_ATTACHMENT =
+ "begin 644 /home/jvporter/Desktop/out.txt\n" +
+ 'M0V%N)W0@;6%K92!T:&4@9G)U9R!C;VYT97-T+"!(96QE;CL@<W1O;6%C:"=S\n' +
+ "M('5P<V5T+B!))VQL(&9I>\"!Y;W4L(%5B:6LA(%5B:6L@9')O<',@>6]U(&)A\n" +
+ "M8VL@:6X@=&AE('1H:6-K(&]F('1H:6YG<R!F87-T+B!486ME;B!A<R!D:7)E\n" +
+ "M8W1E9\"P@56)I:R!S<&5E9',@<F5L:65F('1O(&AE860@86YD('-T;VUA8V@N\n" +
+ "M(%)E;65M8F5R.B!58FEK(&ES(&]N;'D@<V5C;VYD<R!A=V%Y+B!!=F]I9\"!P\n" +
+ ".<F]L;VYG960@=7-E+@H`\n" +
+ "`\n" +
+ "end";
+
+const YENC_TEXT =
+ "Hello there --\n" +
+ "=ybegin line=128 size=174 name=jane\n" +
+ "\x76\x99\x98\x91\x9e\x8f\x97\x9a\x9d\x56\x4a\x94\x8f\x4a\x97\x8f" +
+ "\x4a\x9d\x9f\x93\x9d\x4a\x8d\x99\x9f\x8d\x92\xed\xd3\x4a\x8e\x8f" +
+ "\x4a\x8c\x99\x98\x98\x8f\x4a\x92\x8f\x9f\x9c\x8f\x58\x4a\x7a\x8b" +
+ "\x9c\x90\x99\x93\x9d\x56\x4a\xed\xca\x4a\x9a\x8f\x93\x98\x8f\x4a" +
+ "\x97\x8b\x4a\x8c\x99\x9f\x91\x93\x8f\x4a\xed\xd3\x9e\x8f\x93\x98" +
+ "\x9e\x8f\x56\x4a\x97\x8f\x9d\x4a\xa3\x8f\x9f\xa2\x4a\x9d\x8f\x4a" +
+ "\x90\x8f\x9c\x97\x8b\x93\x8f\x98\x9e\x4a\x9d\x93\x4a\xa0\x93\x9e" +
+ "\x8f\x4a\x9b\x9f\x8f\x4a\x94\x8f\x4a\x98\x51\x8b\xa0\x8b\x93\x9d" +
+ "\x0d\x0a\x4a\x9a\x8b\x9d\x4a\x96\x8f\x4a\x9e\x8f\x97\x9a\x9d\x4a" +
+ "\x8e\x8f\x4a\x97\x8f\x4a\x8e\x93\x9c\x8f\x4a\x64\x4a\xec\xd5\x4a" +
+ "\x74\x8f\x4a\x97\x51\x8f\x98\x8e\x99\x9c\x9d\x58\x4a\xec\xe5\x34" +
+ "\x0d\x0a" +
+ "=yend size=174 crc32=7efccd8e\n";
+const YENC_SIZE = 174;
+
+const PART_HTML = new SyntheticPartLeaf(
+ "<html><head></head><body>I am HTML! Woo! </body></html>",
+ {
+ contentType: "text/html",
+ }
+);
+
+var attachedMessage1 = msgGen.makeMessage({ body: { body: TEXT_ATTACHMENT } });
+var attachedMessage2 = msgGen.makeMessage({
+ body: { body: TEXT_ATTACHMENT },
+ attachments: [
+ {
+ body: IMAGE_ATTACHMENT,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+});
+
+add_task(async function test_text_attachment() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "ubik.txt",
+ format: "",
+ },
+ ],
+ size: TEXT_ATTACHMENT.length,
+ });
+});
+
+// (inline) image attachment
+add_task(async function test_inline_image_attachment() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: IMAGE_ATTACHMENT,
+ contentType: "image/png",
+ filename: "lines.png",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ size: IMAGE_SIZE,
+ });
+});
+
+// binary attachment, no encoding
+add_task(async function test_binary_attachment_no_encoding() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: BINARY_ATTACHMENT,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ format: "",
+ },
+ ],
+ size: BINARY_ATTACHMENT.length,
+ });
+});
+
+// binary attachment, b64 encoding
+add_task(async function test_binary_attachment_b64_encoding() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: IMAGE_ATTACHMENT,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ size: IMAGE_SIZE,
+ });
+});
+
+// uuencoded attachment
+add_task(async function test_uuencoded_attachment() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: UU_ATTACHMENT,
+ contentType: "application/x-uuencode",
+ filename: "ubik",
+ format: "",
+ encoding: "uuencode",
+ },
+ ],
+ size: TEXT_ATTACHMENT.length,
+ });
+});
+
+// yencoded attachment
+add_task(async function test_yencoded_attachment() {
+ await test_message_attachments({
+ bodyPart: new SyntheticPartLeaf("I am text! Woo!\n\n" + YENC_TEXT, {
+ contentType: "",
+ }),
+ subject: 'yEnc-Prefix: "jane" 174 yEnc bytes - yEnc test (1)',
+ size: YENC_SIZE,
+ });
+});
+
+// an attached eml that used to return a size that's -1
+add_task(async function test_incorrect_attached_eml() {
+ await test_message_attachments({
+ bodyPart: new SyntheticPartMultiMixed([PART_HTML, attachedMessage1]),
+ size: get_message_size(attachedMessage1),
+ });
+});
+
+// this is an attached message that itself has an attachment
+add_task(async function test_recursive_attachment() {
+ await test_message_attachments({
+ bodyPart: new SyntheticPartMultiMixed([PART_HTML, attachedMessage2]),
+ size: get_message_size(attachedMessage2),
+ });
+});
+
+// an "attachment" that's really the body of the message
+add_task(async function test_body_attachment() {
+ await test_message_attachments({
+ body: {
+ body: TEXT_ATTACHMENT,
+ contentType: "application/x-ubik; name=attachment.ubik",
+ },
+ size: TEXT_ATTACHMENT.length,
+ });
+});
+
+// a message/rfc822 "attachment" that's really the body of the message
+add_task(async function test_rfc822_attachment() {
+ await test_message_attachments({
+ bodyPart: attachedMessage1,
+ size: get_message_size(attachedMessage1),
+ });
+});
+
+// an external http link attachment (as constructed for feed enclosures) - no 'size' parm.
+add_task(async function test_external_http_link_without_size() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "This MIME attachment is stored separately from the message.",
+ contentType: 'application/unknown; name="somefile"',
+ extraHeaders: {
+ "X-Mozilla-External-Attachment-URL": "http://myblog.com/somefile",
+ },
+ disposition: 'attachment; filename="somefile"',
+ },
+ ],
+ size: -1,
+ });
+});
+
+// an external http link attachment (as constructed for feed enclosures) - file with 'size' parm.
+add_task(async function test_external_http_link_wit_file_size() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "This MIME attachment is stored separately from the message.",
+ contentType: 'audio/mpeg; name="file.mp3"; size=123456789',
+ extraHeaders: {
+ "X-Mozilla-External-Attachment-URL": "https://myblog.com/file.mp3",
+ },
+ disposition: 'attachment; name="file.mp3"',
+ },
+ ],
+ size: 123456789,
+ });
+});
+
+add_task(function endTest() {
+ messageInjection.teardownMessageInjection();
+});
+
+async function test_message_attachments(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+ await PromiseTestUtils.promiseDelay(200);
+ let streamListener = new PromiseTestUtils.PromiseStreamListener({
+ onStopRequest(request) {
+ request.QueryInterface(Ci.nsIMailChannel);
+ for (let attachment of request.attachments) {
+ let attachmentSize = parseInt(attachment.get("X-Mozilla-PartSize"));
+ dump(
+ "*** Size is " + attachmentSize + " (expecting " + info.size + ")\n"
+ );
+ Assert.ok(Math.abs(attachmentSize - info.size) <= EPSILON);
+ break;
+ }
+ },
+ });
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ null,
+ null,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ false
+ );
+
+ await streamListener.promise;
+}
+
+/**
+ * Return the size of a synthetic message. Much like the above comment, libmime
+ * counts bytes differently on Windows, where it counts newlines (\r\n) as 2
+ * bytes. Mac and Linux treats them as 1 byte.
+ *
+ * @param message a synthetic message from makeMessage()
+ * @returns the message's size in bytes
+ */
+function get_message_size(message) {
+ let messageString = message.toMessageString();
+ if (EPSILON == 4) {
+ // Windows
+ return messageString.length;
+ }
+ return messageString.replace(/\r\n/g, "\n").length;
+}
diff --git a/comm/mailnews/mime/test/unit/test_badContentType.js b/comm/mailnews/mime/test/unit/test_badContentType.js
new file mode 100644
index 0000000000..1202f3319b
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_badContentType.js
@@ -0,0 +1,115 @@
+/* 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/. */
+
+/**
+ * This test checks handling of bad content type of the
+ * type reported in bug 659355.
+ * Adapted from test_attachment_size.js
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+var messageGenerator = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+
+const IMAGE_ATTACHMENT =
+ "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" +
+ "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" +
+ "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" +
+ "SNQAAlmAY+71EgFoAAAAASUVORK5CYII=";
+
+add_task(async function test_image_attachment_normal_content_type() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: IMAGE_ATTACHMENT,
+ contentType: "image/png",
+ filename: "lines.png",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ testContentType: "image/png",
+ });
+});
+
+add_task(async function test_image_attachment_bad_content_type() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: IMAGE_ATTACHMENT,
+ contentType: "=?windows-1252?q?application/pdf",
+ filename: "lines.pdf",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ testContentType: "application/pdf",
+ });
+});
+
+add_task(function endTest() {
+ messageInjection.teardownMessageInjection();
+});
+
+async function test_message_attachments(info) {
+ let synMsg = messageGenerator.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener({
+ onStopRequest(request, statusCode) {
+ request.QueryInterface(Ci.nsIMailChannel);
+ let msgHdrSinkContentType =
+ request.attachments[0].getProperty("contentType");
+ Assert.equal(msgHdrSinkContentType, info.testContentType);
+ },
+ });
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ null,
+ null,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ false
+ );
+ await streamListener.promise;
+}
+
+function MsgHeaderSinkHandleAttachments() {
+ this._promise = new Promise(resolve => {
+ this._resolve = resolve;
+ });
+}
+
+MsgHeaderSinkHandleAttachments.prototype = {
+ handleAttachment(
+ aContentType,
+ aUrl,
+ aDisplayName,
+ aUri,
+ aIsExternalAttachment
+ ) {
+ this._resolve(aContentType);
+ },
+
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/mime/test/unit/test_bug493544.js b/comm/mailnews/mime/test/unit/test_bug493544.js
new file mode 100644
index 0000000000..f0da7ef167
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_bug493544.js
@@ -0,0 +1,106 @@
+//
+// Tests if a multi-line MIME header is parsed even if it violates RFC 2047
+//
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ const headers = [
+ {
+ // Bug 833028
+ encoded:
+ "Subject: AAA =?UTF-8?Q?bbb?= CCC =?UTF-8?Q?ddd?= EEE =?UTF-8?Q?fff?= GGG",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: AAA bbb CCC ddd EEE fff GGG",
+ },
+ {
+ // Bug 493544
+ encoded:
+ "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiCAg?=\n" +
+ " =?UTF-8?B?4oiJICDiiIogIOKIiyAg4oiMICDiiI0gIOKIjiAg4oiP?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Bug 476741
+ encoded:
+ "Subject: =?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__=E2?=\n" +
+ " =?utf-8?Q?=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__=E2=88?=\n" +
+ " =?utf-8?Q?=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__=E2=88=8F?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Normal case
+ encoded:
+ "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA==?=\n" +
+ " =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Normal case with the encoding character in lower case
+ encoded:
+ "Subject: =?UTF-8?b?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA==?=\n" +
+ " =?UTF-8?b?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Normal case
+ encoded:
+ "Subject: =?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" +
+ " =?utf-8?Q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" +
+ " =?utf-8?Q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" +
+ " =?utf-8?Q?=E2=88=8F?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Normal case with the encoding character in lower case
+ encoded:
+ "Subject: =?utf-8?q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" +
+ " =?utf-8?q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" +
+ " =?utf-8?q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" +
+ " =?utf-8?q?=E2=88=8F?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ {
+ // Regression test for bug 227290
+ encoded:
+ "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA===?=\n" +
+ " =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=",
+ defaultCharset: "UTF-8",
+ overrideCharset: false,
+ eatContinuation: false,
+ decoded: "Subject: ∀ ∠∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∠∎ âˆ",
+ },
+ ];
+
+ for (let i = 0; i < headers.length; ++i) {
+ let decoded = MailServices.mimeConverter.decodeMimeHeader(
+ headers[i].encoded,
+ headers[i].defaultCharset,
+ headers[i].overrideCharset,
+ headers[i].eatContinuation
+ );
+ Assert.equal(decoded, headers[i].decoded);
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_handlerRegistration.js b/comm/mailnews/mime/test/unit/test_handlerRegistration.js
new file mode 100644
index 0000000000..5a2aef8799
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_handlerRegistration.js
@@ -0,0 +1,64 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+var { EnigmailVerify } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/mimeVerify.jsm"
+);
+
+/**
+ * Tests switching content-type handlers on demand.
+ */
+add_task(function () {
+ const CONTRACT_ID = "@mozilla.org/mimecth;1?type=multipart/signed";
+ const INTERFACE = Ci.nsIMimeContentTypeHandler;
+
+ Assert.ok(
+ !Components.manager.isContractIDRegistered(CONTRACT_ID),
+ "no factory is registered initially"
+ );
+
+ EnigmailVerify.registerPGPMimeHandler();
+ Assert.equal(
+ Cc[CONTRACT_ID].number,
+ EnigmailVerify.pgpMimeFactory.classID.number,
+ "pgpMimeFactory is the registered factory"
+ );
+ Assert.ok(
+ Cc[CONTRACT_ID].createInstance(INTERFACE),
+ "pgpMimeFactory successfully created an instance"
+ );
+
+ EnigmailVerify.unregisterPGPMimeHandler();
+ Assert.ok(
+ !Components.manager.isContractIDRegistered(CONTRACT_ID),
+ "pgpMimeFactory has been unregistered"
+ );
+ Assert.throws(
+ () => Cc[CONTRACT_ID].createInstance(INTERFACE),
+ /NS_ERROR_XPC_CI_RETURNED_FAILURE/,
+ "exception correctly thrown"
+ );
+
+ EnigmailVerify.registerPGPMimeHandler();
+ Assert.equal(
+ Cc[CONTRACT_ID].number,
+ EnigmailVerify.pgpMimeFactory.classID.number,
+ "pgpMimeFactory is the registered factory"
+ );
+ Assert.ok(
+ Cc[CONTRACT_ID].createInstance(INTERFACE),
+ "pgpMimeFactory successfully created an instance"
+ );
+
+ EnigmailVerify.unregisterPGPMimeHandler();
+ Assert.ok(
+ !Components.manager.isContractIDRegistered(CONTRACT_ID),
+ "pgpMimeFactory has been unregistered"
+ );
+ Assert.throws(
+ () => Cc[CONTRACT_ID].createInstance(INTERFACE),
+ /NS_ERROR_XPC_CI_RETURNED_FAILURE/,
+ "exception correctly thrown"
+ );
+});
diff --git a/comm/mailnews/mime/test/unit/test_hidden_attachments.js b/comm/mailnews/mime/test/unit/test_hidden_attachments.js
new file mode 100644
index 0000000000..1203c9a8ea
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_hidden_attachments.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+/**
+ * This test creates some messages with attachments of different types and
+ * checks that libmime emits (or doesn't emit) the attachments as appropriate.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+var messageGenerator = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+
+add_task(async function test_without_attachment() {
+ await test_message_attachments({});
+});
+
+/* Attachments with Content-Disposition: attachment */
+// inline-able attachment with a name
+add_task(
+ async function test_content_disposition_attachment_inlineable_attachment_with_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "ubik.txt",
+ disposition: "attachment",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: attachment */
+// inline-able attachment with no name
+add_task(
+ async function test_content_disposition_attachment_inlineable_attachment_no_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ disposition: "attachment",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: attachment */
+// non-inline-able attachment with a name
+add_task(
+ async function test_content_disposition_attachment_non_inlineable_attachment_with_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "ubik.ubk",
+ disposition: "attachment",
+ contentType: "application/x-ubik",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: attachment */
+// non-inline-able attachment with no name
+add_task(
+ async function test_content_disposition_attachment_non_inlineable_attachment_no_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ disposition: "attachment",
+ contentType: "application/x-ubik",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: inline */
+// inline-able attachment with a name
+add_task(
+ async function test_content_disposition_inline_inlineable_attachment_with_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "ubik.txt",
+ disposition: "inline",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: inline */
+// inline-able attachment with no name
+add_task(
+ async function test_content_disposition_inline_inlineable_attachment_no_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ disposition: "inline",
+ format: "",
+ shouldShow: false,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: inline */
+// non-inline-able attachment with a name
+add_task(
+ async function test_content_disposition_inline_non_inlineable_attachment_with_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "ubik.ubk",
+ disposition: "inline",
+ contentType: "application/x-ubik",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+/* Attachments with Content-Disposition: inline */
+// non-inline-able attachment with no name
+add_task(
+ async function test_content_disposition_inline_non_inlineable_attachment_no_name() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "attachment",
+ filename: "",
+ disposition: "inline",
+ contentType: "application/x-ubik",
+ format: "",
+ shouldShow: true,
+ },
+ ],
+ });
+ }
+);
+
+async function test_message_attachments(info) {
+ let synMsg = messageGenerator.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener({
+ onStopRequest(request, status) {
+ request.QueryInterface(Ci.nsIMailChannel);
+ let expectedAttachments = (info.attachments || [])
+ .filter(i => i.shouldShow)
+ .map(i => i.filename);
+ Assert.equal(request.attachments.length, expectedAttachments.length);
+
+ for (let i = 0; i < request.attachments.length; i++) {
+ // If the expected attachment's name is empty, we probably generated a
+ // name like "Part 1.2", so don't bother checking that the names match
+ // (they won't).
+ if (expectedAttachments[i]) {
+ Assert.equal(
+ request.attachments[i].getProperty("displayName"),
+ expectedAttachments[i]
+ );
+ }
+ }
+ },
+ });
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ null,
+ null,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ false
+ );
+
+ await streamListener.promise;
+}
diff --git a/comm/mailnews/mime/test/unit/test_jsmime_charset.js b/comm/mailnews/mime/test/unit/test_jsmime_charset.js
new file mode 100644
index 0000000000..865c8ae02f
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_jsmime_charset.js
@@ -0,0 +1,43 @@
+/* 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/. */
+
+// This tests that the charset decoding uses nsICharsetDecoder instead of
+// TextDecoder, to get some extra charsets.
+
+const { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+
+var tests = [
+ ["=?UTF-7?Q?+AKM-1?=", "\u00A31"],
+ ["=?UTF-7?Q?+AK?= =?UTF-7?Q?M-1?=", "\u00A31"],
+ ["=?UTF-8?Q?=C2?=", "\uFFFD"], // Replacement character for invalid input.
+ ["=?NotARealCharset?Q?text?=", "=?NotARealCharset?Q?text?="],
+ ["\xC2\xA31", "\u00A31", "ISO-8859-2"],
+ ["\xA31", "\u01411", "ISO-8859-2"],
+ ["\xC21", "\u00C21", "ISO-8859-1"],
+ // "Here comes the text." in Japanese encoded in Shift_JIS, also using Thunderbird's alias cp932.
+ [
+ "=?shift_jis?Q?=82=b1=82=b1=82=c9=96=7b=95=b6=82=aa=82=ab=82=dc=82=b7=81=42?=",
+ "ã“ã“ã«æœ¬æ–‡ãŒãã¾ã™ã€‚",
+ ],
+ ["=?shift_jis?B?grGCsYLJlnuVtoKqgquC3IK3gUI=?=", "ã“ã“ã«æœ¬æ–‡ãŒãã¾ã™ã€‚"],
+ [
+ "=?cp932?Q?=82=b1=82=b1=82=c9=96=7b=95=b6=82=aa=82=ab=82=dc=82=b7=81=42?=",
+ "ã“ã“ã«æœ¬æ–‡ãŒãã¾ã™ã€‚",
+ ],
+ ["=?cp932?B?grGCsYLJlnuVtoKqgquC3IK3gUI=?=", "ã“ã“ã«æœ¬æ–‡ãŒãã¾ã™ã€‚"],
+];
+
+function run_test() {
+ for (let test of tests) {
+ dump("Testing message " + test[0]);
+ let value = test[0];
+ if (test.length > 2) {
+ value = jsmime.headerparser.convert8BitHeader(value, test[2]);
+ }
+ Assert.equal(
+ jsmime.headerparser.parseStructuredHeader("Subject", value),
+ test[1]
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_message_attachment.js b/comm/mailnews/mime/test/unit/test_message_attachment.js
new file mode 100644
index 0000000000..653a7078af
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_message_attachment.js
@@ -0,0 +1,168 @@
+/* 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/. */
+
+/**
+ * This test verifies that we generate proper attachment filenames.
+ */
+
+var {
+ MessageGenerator,
+ SyntheticMessageSet,
+ SyntheticPartMultiMixed,
+ SyntheticPartLeaf,
+} = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+// Create a message generator
+var msgGen = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+// The attachments need to have some content or the stream converter won't
+// display them inline. In the case of the email attachment it must have
+// trailing CRLFs or it will fail to parse.
+const TEXT_ATTACHMENT = "inline text attachment";
+const EMAIL_ATTACHMENT = "Subject: fake email\r\n\r\n";
+const HTML_ATTACHMENT = "<html><body></body></html>";
+
+add_setup(function () {
+ Services.prefs.setBoolPref("mail.inline_attachments.text", true);
+});
+
+// Unnamed email attachment.
+add_task(async function test_unnamed_email_attachment() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "test.txt",
+ format: "",
+ },
+ {
+ body: EMAIL_ATTACHMENT,
+ expectedFilename: "ForwardedMessage.eml",
+ contentType: "message/rfc822",
+ },
+ ],
+ });
+});
+
+// Named email attachment.
+add_task(async function test_named_email_attachment() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "test.txt",
+ format: "",
+ },
+ {
+ body: EMAIL_ATTACHMENT,
+ filename: "Attached Message",
+ contentType: "message/rfc822",
+ },
+ ],
+ });
+});
+
+// Escaped html attachment.
+add_task(async function test_foo() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "test.html",
+ format: "",
+ },
+ {
+ body: HTML_ATTACHMENT,
+ filename:
+ "<iframe src=&quote;http://www.example.com&quote></iframe>.htm",
+ expectedFilename:
+ "&lt;iframe src=&amp;quote;http://www.example.com&amp;quote&gt;&lt;/iframe&gt;.htm",
+ contentType: "text/html;",
+ },
+ ],
+ });
+});
+
+// No named email attachment with subject header.
+add_task(async function test_no_named_email_attachment_with_subject_header() {
+ await test_message_attachments({
+ attachments: [
+ {
+ body: "",
+ expectedFilename: "testSubject.eml",
+ },
+ ],
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartLeaf("plain body text"),
+ msgGen.makeMessage({
+ subject: "=?UTF-8?B?dGVzdFN1YmplY3Q=?=", // This string is 'testSubject'.
+ charset: "UTF-8",
+ }),
+ ]),
+ });
+});
+
+async function test_message_attachments(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ msgWindow,
+ null,
+ true, // Have them create the converter.
+ "header=filter",
+ false
+ );
+
+ let streamedData = await streamListener.promise;
+
+ // Check that the attachments' filenames are as expected. Just use a regex
+ // here because it's simple.
+ let regex1 =
+ /<legend class="moz-mime-attachment-header-name">(.*?)<\/legend>/gi;
+
+ for (let attachment of info.attachments) {
+ let match = regex1.exec(streamedData);
+ Assert.notEqual(match, null);
+ Assert.equal(match[1], attachment.expectedFilename || attachment.filename);
+ }
+ Assert.equal(regex1.exec(streamedData), null);
+
+ // Check the attachments' filenames are listed for printing.
+ let regex2 = /<td class="moz-mime-attachment-file">(.*?)<\/td>/gi;
+
+ for (let attachment of info.attachments) {
+ let match = regex2.exec(streamedData);
+ Assert.notEqual(match, null);
+ Assert.equal(match[1], attachment.expectedFilename || attachment.filename);
+ }
+ Assert.equal(regex2.exec(streamedData), null);
+}
+
+add_task(function endTest() {
+ messageInjection.teardownMessageInjection();
+});
diff --git a/comm/mailnews/mime/test/unit/test_mimeContentType.js b/comm/mailnews/mime/test/unit/test_mimeContentType.js
new file mode 100644
index 0000000000..fb40549f35
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_mimeContentType.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+function run_test() {
+ const headers = [
+ {
+ header:
+ "Content-Type: text/plain\r\n" +
+ "Content-Disposition: inline\r\n" +
+ "\r\n",
+ result: "text/plain",
+ },
+ {
+ header:
+ "Content-Type:\r\n" +
+ "\tapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ 'Content-Disposition: attachment; filename="List.xlsx"\r\n' +
+ "\r\n",
+ result:
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ },
+ {
+ header:
+ "Content-Type: \r\n" +
+ " application/vnd.openxmlformats-officedocument.presentationml.presentation;\r\n" +
+ ' name="Presentation.pptx"\r\n' +
+ "\r\n",
+ result:
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation;" +
+ ' name="Presentation.pptx"',
+ },
+ {
+ header:
+ "Content-Type:\r\n" +
+ "text/plain; charset=utf-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "Content-Disposition: inline\r\n" +
+ "\r\n",
+ result: "",
+ },
+ {
+ header: "Content-Type:\r\n\r\n",
+ result: "",
+ },
+ {
+ /* possible crash case for Bug 574961 */
+ header:
+ "Content-Type: \r\n" +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " \r\n",
+ result: "",
+ },
+ ];
+
+ let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+
+ for (let i = 0; i < headers.length; i++) {
+ mimeHdr.initialize(headers[i].header);
+ let receivedHeader = mimeHdr.extractHeader("Content-Type", false);
+
+ dump(
+ "\nTesting Content-Type: " +
+ receivedHeader +
+ " == " +
+ headers[i].result +
+ "\n"
+ );
+
+ Assert.equal(receivedHeader, headers[i].result);
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_mimeStreaming.js b/comm/mailnews/mime/test/unit/test_mimeStreaming.js
new file mode 100644
index 0000000000..3e82fe51db
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_mimeStreaming.js
@@ -0,0 +1,88 @@
+/* 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/. */
+
+/**
+ * This test iterates over the test files in gTestFiles, and streams
+ * each as a message and makes sure the streaming doesn't assert or crash.
+ */
+const { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var gTestFiles = ["../../../data/bug505221", "../../../data/bug513543"];
+
+var gMessages;
+
+var gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+var gUrlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ do_test_finished();
+ },
+};
+
+localAccountUtils.loadLocalMailAccount();
+
+add_task(async function run_the_test() {
+ do_test_pending();
+ localAccountUtils.inboxFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ for (let fileName of gTestFiles) {
+ localAccountUtils.inboxFolder.addMessage(
+ await IOUtils.readUTF8(do_get_file(fileName).path)
+ );
+ }
+ gMessages = [
+ ...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages(),
+ ];
+ doNextTest();
+});
+
+function streamMsg(msgHdr) {
+ let msgURI = localAccountUtils.inboxFolder.getUriForMsg(msgHdr);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+ msgService.streamMessage(
+ msgURI,
+ gStreamListener,
+ null,
+ gUrlListener,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ true
+ );
+}
+
+var gStreamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ _stream: null,
+ // nsIRequestObserver part
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ doNextTest();
+ },
+
+ /* okay, our onDataAvailable should actually never be called. the stream
+ converter is actually eating everything except the start and stop
+ notification. */
+ // nsIStreamListener part
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ if (this._stream === null) {
+ this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ this._stream.init(aInputStream);
+ }
+ this._stream.read(aCount);
+ },
+};
+
+function doNextTest() {
+ if (gMessages.length > 0) {
+ let msgHdr = gMessages.shift();
+ streamMsg(msgHdr);
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js
new file mode 100644
index 0000000000..4636136f17
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgHeaderParser functions.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ var checks = [
+ ["", "test@foo.invalid", "test@foo.invalid"],
+ ["Test", "test@foo.invalid", "Test <test@foo.invalid>"],
+ ["Test", '"abc!x.yz"@foo.invalid', 'Test <"abc!x.yz"@foo.invalid>'],
+ ["Test", "test.user@foo.invalid", "Test <test.user@foo.invalid>"],
+ ["Test", "test@[xyz!]", "Test <test@[xyz!]>"],
+ // Based on RFC 2822 A.1.1
+ ["John Doe", "jdoe@machine.example", "John Doe <jdoe@machine.example>"],
+ // Next 2 tests Based on RFC 2822 A.1.2
+ [
+ "Joe Q. Public",
+ "john.q.public@example.com",
+ '"Joe Q. Public" <john.q.public@example.com>',
+ ],
+ [
+ 'Giant; "Big" Box',
+ "sysservices@example.net",
+ '"Giant; \\"Big\\" Box" <sysservices@example.net>',
+ ],
+ ["trailing", "t1@example.com ", "trailing <t1@example.com>"],
+ ["leading", " t2@example.com", "leading <t2@example.com>"],
+ [
+ "leading trailing",
+ " t3@example.com ",
+ "leading trailing <t3@example.com>",
+ ],
+ ["", " t4@example.com ", "t4@example.com"],
+ ];
+
+ // Test - empty strings
+
+ Assert.equal(MailServices.headerParser.makeMimeAddress("", ""), "");
+
+ // Test - makeMimeAddress
+
+ for (let i = 0; i < checks.length; ++i) {
+ Assert.equal(
+ MailServices.headerParser.makeMimeAddress(checks[i][0], checks[i][1]),
+ checks[i][2]
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js
new file mode 100644
index 0000000000..5d1df70330
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgHeaderParser functions:
+ * extractHeaderAddressMailboxes
+ * extractFirstName
+ * parseDecodedHeader
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // In this array, the sub arrays consist of the following elements:
+ // 0: input string (a comma separated list of recipients)
+ // 1: expected output from extractHeaderAddressMailboxes
+ // 2: list of recipient names in the string
+ // 3: first recipient name in the string
+ const checks = [
+ [
+ "abc@foo.invalid",
+ "abc@foo.invalid",
+ "abc@foo.invalid",
+ "abc@foo.invalid",
+ ],
+ ["foo <ghj@foo.invalid>", "ghj@foo.invalid", "foo", "foo"],
+ [
+ "abc@foo.invalid, foo <ghj@foo.invalid>",
+ "abc@foo.invalid, ghj@foo.invalid",
+ "abc@foo.invalid, foo",
+ "abc@foo.invalid",
+ ],
+ ["foo bar <foo@bar.invalid>", "foo@bar.invalid", "foo bar", "foo bar"],
+ [
+ "foo bar <foo@bar.invalid>, abc@foo.invalid, foo <ghj@foo.invalid>",
+ "foo@bar.invalid, abc@foo.invalid, ghj@foo.invalid",
+ "foo bar, abc@foo.invalid, foo",
+ "foo bar",
+ ],
+ // UTF-8 names
+ [
+ "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>",
+ "foo@bar.invalid, ghj@foo.invalid",
+ "foo\u00D0 bar, \u00F6foo",
+ "foo\u00D0 bar",
+ ],
+ // More complicated examples drawn from RFC 2822
+ [
+ '"Joe Q. Public" <john.q.public@example.com>,Test <"abc!x.yz"@foo.invalid>, Test <test@[xyz!]>,"Giant; \\"Big\\" Box" <sysservices@example.net>',
+ 'john.q.public@example.com, "abc!x.yz"@foo.invalid, test@[xyz!], sysservices@example.net',
+ 'Joe Q. Public, Test, Test, Giant; "Big" Box',
+ // extractFirstName returns unquoted names, hence the difference.
+ "Joe Q. Public",
+ ],
+ // Bug 549931
+ [
+ "Undisclosed recipients:;",
+ "", // Mailboxes
+ "", // Address Names
+ "",
+ ], // Address Name
+ ];
+
+ // Test - empty strings
+
+ Assert.equal(MailServices.headerParser.extractHeaderAddressMailboxes(""), "");
+ Assert.equal(MailServices.headerParser.extractFirstName(""), "");
+
+ // Test - extractHeaderAddressMailboxes
+
+ for (let i = 0; i < checks.length; ++i) {
+ Assert.equal(
+ MailServices.headerParser.extractHeaderAddressMailboxes(checks[i][0]),
+ checks[i][1]
+ );
+ let _names = MailServices.headerParser
+ .parseDecodedHeader(checks[i][0])
+ .map(addr => addr.name || addr.email)
+ .join(", ");
+ Assert.equal(_names, checks[i][2]);
+ Assert.equal(
+ MailServices.headerParser.extractFirstName(checks[i][0]),
+ checks[i][3]
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js
new file mode 100644
index 0000000000..06be599d93
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgHeaderParser function removeDuplicateAddresses:
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ const checks = [
+ {
+ addrs: "test@foo.invalid",
+ otherAddrs: "",
+ expectedResult: "test@foo.invalid",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>",
+ otherAddrs: "",
+ expectedResult: "foo bar <test@foo.invalid>",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>, abc@foo.invalid",
+ otherAddrs: "",
+ expectedResult: "foo bar <test@foo.invalid>, abc@foo.invalid",
+ },
+ {
+ addrs:
+ "foo bar <test@foo.invalid>, abc@foo.invalid, test <test@foo.invalid>",
+ otherAddrs: "",
+ expectedResult: "foo bar <test@foo.invalid>, abc@foo.invalid",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>",
+ otherAddrs: "abc@foo.invalid",
+ expectedResult: "foo bar <test@foo.invalid>",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>",
+ otherAddrs: "foo bar <test@foo.invalid>",
+ expectedResult: "",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>, abc@foo.invalid",
+ otherAddrs: "foo bar <test@foo.invalid>",
+ expectedResult: "abc@foo.invalid",
+ },
+ {
+ addrs: "foo bar <test@foo.invalid>, abc@foo.invalid",
+ otherAddrs: "abc@foo.invalid",
+ expectedResult: "foo bar <test@foo.invalid>",
+ },
+ {
+ addrs:
+ "foo bar <test@foo.invalid>, abc@foo.invalid, test <test@foo.invalid>",
+ otherAddrs: "abc@foo.invalid",
+ expectedResult: "foo bar <test@foo.invalid>",
+ },
+ // UTF-8 names
+ {
+ addrs: "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>",
+ otherAddrs: "",
+ expectedResult:
+ "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>",
+ },
+ {
+ addrs: "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>",
+ otherAddrs: "foo\u00D0 bar <foo@bar.invalid>",
+ expectedResult: "\u00F6foo <ghj@foo.invalid>",
+ },
+ {
+ addrs:
+ "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>, foo\u00D0 bar <foo@bar.invalid>",
+ otherAddrs: "\u00F6foo <ghj@foo.invalid>",
+ expectedResult: "foo\u00D0 bar <foo@bar.invalid>",
+ },
+ // Test email groups
+ {
+ addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;",
+ otherAddrs: "foo <ghj@foo.invalid>",
+ expectedResult: "A group: foo bar <foo@bar.invalid>;",
+ },
+ {
+ addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;",
+ otherAddrs: "foo bar <ghj@foo.invalid>",
+ expectedResult: "A group: foo bar <foo@bar.invalid>;",
+ },
+ {
+ addrs: "A group: foo bar <foo@bar.invalid>;, foo <ghj@foo.invalid>",
+ otherAddrs: "foo <foo@bar.invalid>",
+ expectedResult: "A group: ; , foo <ghj@foo.invalid>",
+ },
+ ];
+
+ // Test - empty strings
+
+ Assert.equal(MailServices.headerParser.removeDuplicateAddresses("", ""), "");
+ Assert.equal(
+ MailServices.headerParser.removeDuplicateAddresses("", "test@foo.invalid"),
+ ""
+ );
+
+ // Test - removeDuplicateAddresses
+
+ for (let i = 0; i < checks.length; ++i) {
+ dump("Test " + i + "\n");
+ Assert.equal(
+ MailServices.headerParser.removeDuplicateAddresses(
+ checks[i].addrs,
+ checks[i].otherAddrs
+ ),
+ checks[i].expectedResult
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js
new file mode 100644
index 0000000000..e4573ae37a
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js
@@ -0,0 +1,199 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/**
+ * Test suite for nsIMsgHeaderParser::makeFromDisplayAddress.
+ * This is what is used to parse in the user input from addressing fields.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ const checks = [
+ { displayString: "", addresses: [] },
+ {
+ displayString: "test@foo.invalid",
+ addresses: [["", "test@foo.invalid"]],
+ },
+ {
+ displayString: "test@foo.invalid, test2@foo.invalid",
+ addresses: [
+ ["", "test@foo.invalid"],
+ ["", "test2@foo.invalid"],
+ ],
+ },
+ {
+ displayString: "John Doe <test@foo.invalid>",
+ addresses: [["John Doe", "test@foo.invalid"]],
+ },
+ // Trim spaces.
+ {
+ displayString: " John Doe <test@foo.invalid>",
+ addresses: [["John Doe", "test@foo.invalid"]],
+ },
+ // No space before the email address.
+ {
+ displayString: " John Doe<test@foo.invalid>",
+ addresses: [["John Doe", "test@foo.invalid"]],
+ },
+ // Additional text after the email address to be ignored.
+ {
+ displayString: " John Doe<test@foo.invalid> Junior",
+ addresses: [["John Doe", "test@foo.invalid"]],
+ },
+ {
+ displayString: "Doe, John <test@foo.invalid>",
+ addresses: [["Doe, John", "test@foo.invalid"]],
+ },
+ {
+ displayString:
+ "Doe, John <test@foo.invalid>, Bond, James <test2@foo.invalid>",
+ addresses: [
+ ["Doe, John", "test@foo.invalid"],
+ ["Bond, James", "test2@foo.invalid"],
+ ],
+ },
+ // Additional text after the email address to be ignored, multiple addresses.
+ {
+ displayString:
+ "Doe, John <test@foo.invalid>Junior, Bond, James <test2@foo.invalid>007",
+ addresses: [
+ ["Doe, John", "test@foo.invalid"],
+ ["Bond, James", "test2@foo.invalid"],
+ ],
+ },
+ // Multiple commas
+ {
+ displayString:
+ "Doe,, John <test@foo.invalid>,, Bond, James <test2@foo.invalid>, , Gold Finger <goldfinger@example.com> ,, ",
+ addresses: [
+ ["Doe,, John", "test@foo.invalid"],
+ ["Bond, James", "test2@foo.invalid"],
+ ["Gold Finger", "goldfinger@example.com"],
+ ],
+ },
+ // More tests where the user forgot to close the quote or added extra quotes.
+ {
+ displayString: '"Yatter King1 <a@a.a.a>',
+ addresses: [['"Yatter King1', "a@a.a.a"]],
+ },
+ {
+ displayString: 'Yatter King2" <a@a.a.a>',
+ addresses: [['Yatter King2"', "a@a.a.a"]],
+ },
+ {
+ displayString: '"Yatter King3" <a@a.a.a>',
+ addresses: [['"Yatter King3"', "a@a.a.a"]],
+ },
+ {
+ displayString: 'Yatter "XXX" King4 <a@a.a.a>',
+ addresses: [['Yatter "XXX" King4', "a@a.a.a"]],
+ },
+ {
+ displayString: '"Yatter "XXX" King5" <a@a.a.a>',
+ addresses: [['"Yatter "XXX" King5"', "a@a.a.a"]],
+ },
+ {
+ displayString: '"Yatter King6 <a@a.a.a>"',
+ addresses: [["Yatter King6", "a@a.a.a"]],
+ },
+ {
+ displayString: '"Yatter King7 <a@a.a.a>" <b@b.b.b>',
+ addresses: [['"Yatter King7 <a@a.a.a>"', "b@b.b.b"]],
+ },
+ // Handle invalid mailbox separation with semicolons gracefully.
+ {
+ displayString:
+ 'Bart <bart@example.com> ; lisa@example.com; "Homer, J; President" <pres@example.com>, Marge <marge@example.com>; ',
+ addresses: [
+ ["Bart", "bart@example.com"],
+ ["", "lisa@example.com"],
+ ['"Homer, J; President"', "pres@example.com"],
+ ["Marge", "marge@example.com"],
+ ],
+ },
+ // Junk after a bracketed email address to be ignored.
+ {
+ displayString: "<attacker@example.com>friend@example.com",
+ addresses: [["", "attacker@example.com"]],
+ },
+ {
+ displayString:
+ "<attacker2@example.com><friend2@example.com>,foo <attacker3@example.com><friend3@example.com>",
+ addresses: [
+ ["", "attacker2@example.com"],
+ ["foo", "attacker3@example.com"],
+ ],
+ },
+ {
+ displayString:
+ 'jay "bad" ass <name@evil.com> <someone-else@bad.com> <name@evil.commercial.org>',
+ addresses: [['jay "bad" ass', "name@evil.com"]],
+ },
+
+ {
+ displayString:
+ 'me "you" (via foo@example.com) <attacker2@example.com> friend@example.com,',
+ addresses: [['me "you" (via foo@example.com)', "attacker2@example.com"]],
+ },
+
+ // An uncompleted autocomplete...
+ {
+ displayString: "me >> test <joe@examp.com>",
+ addresses: [["me >> test", "joe@examp.com"]],
+ },
+
+ // A mail list.
+ {
+ displayString: "Holmes and Watson <Tenants221B>, foo@example.com",
+ addresses: [
+ ["Holmes and Watson", "Tenants221B"],
+ ["", "foo@example.com"],
+ ],
+ },
+
+ // A mail list with a space in the name.
+ {
+ displayString: 'Watson and Holmes <"Quoted Tenants221B">',
+ addresses: [["Watson and Holmes", '"Quoted Tenants221B"']],
+ },
+
+ // Mail Merge template
+ {
+ displayString: "{{PrimaryEmail}} <>",
+ addresses: [["{{PrimaryEmail}}", ""]],
+ },
+
+ // Quoted heart.
+ {
+ displayString: 'Marge "<3" S <qheart@example.com>',
+ addresses: [['Marge "<3" S', "qheart@example.com"]],
+ },
+
+ // Heart.
+ {
+ displayString: "Maggie <3 S <heart@example.com>",
+ addresses: [["Maggie <3 S", "heart@example.com"]],
+ },
+
+ // Unbalanced quotes.
+ {
+ displayString: 'Homer <3 "B>" "J <unb@example.com>',
+ addresses: [['Homer <3 "B>" "J', "unb@example.com"]],
+ },
+ ];
+
+ // Test - strings
+
+ for (let i = 0; i < checks.length; ++i) {
+ let addrs = MailServices.headerParser.makeFromDisplayAddress(
+ checks[i].displayString
+ );
+ let checkaddrs = checks[i].addresses;
+ Assert.equal(addrs.length, checkaddrs.length, "Number of parsed addresses");
+ for (let j = 0; j < addrs.length; j++) {
+ Assert.equal(addrs[j].name, checkaddrs[j][0], "Parsed name");
+ Assert.equal(addrs[j].email, checkaddrs[j][1], "Parsed email");
+ }
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js
new file mode 100644
index 0000000000..a118d44641
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgHeaderParser functions:
+ * parseDecodedHeader
+ * parseEncodedHeader
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function equalArrays(arr1, arr2) {
+ Assert.equal(arr1.length, arr2.length);
+ for (let i = 0; i < arr1.length; i++) {
+ Assert.equal(arr1[i].name, arr2[i].name);
+ Assert.equal(arr1[i].email, arr2[i].email);
+ }
+}
+
+function run_test() {
+ // In this array, the sub arrays consist of the following elements:
+ // 0: input string
+ // 1: expected output from parseDecodedHeader
+ // 2: expected output from parseEncodedHeader
+ const checks = [
+ [
+ "abc@foo.invalid",
+ [{ name: "", email: "abc@foo.invalid" }],
+ [{ name: "", email: "abc@foo.invalid" }],
+ ],
+ [
+ "foo <ghj@foo.invalid>",
+ [{ name: "foo", email: "ghj@foo.invalid" }],
+ [{ name: "foo", email: "ghj@foo.invalid" }],
+ ],
+ [
+ "abc@foo.invalid, foo <ghj@foo.invalid>",
+ [
+ { name: "", email: "abc@foo.invalid" },
+ { name: "foo", email: "ghj@foo.invalid" },
+ ],
+ [
+ { name: "", email: "abc@foo.invalid" },
+ { name: "foo", email: "ghj@foo.invalid" },
+ ],
+ ],
+ // UTF-8 names
+ [
+ "foo\u00D0 bar <foo@bar.invalid>, \u00C3\u00B6foo <ghj@foo.invalid>",
+ [
+ { name: "foo\u00D0 bar", email: "foo@bar.invalid" },
+ { name: "\u00C3\u00B6foo", email: "ghj@foo.invalid" },
+ ],
+ [
+ { name: "foo\uFFFD bar", email: "foo@bar.invalid" },
+ { name: "\u00F6foo", email: "ghj@foo.invalid" },
+ ],
+ ],
+ // Bug 961564
+ [
+ "someone <>",
+ [{ name: "someone", email: "" }],
+ [{ name: "someone", email: "" }],
+ ],
+ // Bug 1423432: Encoded strings with null bytes,
+ // in base64 a single null byte can be encoded as AA== to AP==.
+ // parseEncodedHeader will remove the nullbyte.
+ [
+ '"null=?UTF-8?Q?=00?=byte" <nullbyte@example.com>',
+ [{ name: "null=?UTF-8?Q?=00?=byte", email: "nullbyte@example.com" }],
+ [{ name: "nullbyte", email: "nullbyte@example.com" }],
+ ],
+ [
+ '"null=?UTF-8?B?AA==?=byte" <nullbyte@example.com>',
+ [{ name: "null=?UTF-8?B?AA==?=byte", email: "nullbyte@example.com" }],
+ [{ name: "nullbyte", email: "nullbyte@example.com" }],
+ ],
+ ["", [], []],
+ [" \r\n\t", [], []],
+ [
+ // This used to cause memory read overruns.
+ '" "@a a;b',
+ [
+ { name: "", email: '" "@a a' },
+ { name: "b", email: "" },
+ ],
+ [
+ { name: "", email: "@a a" },
+ { name: "b", email: "" },
+ ],
+ ],
+ ];
+
+ for (let check of checks) {
+ equalArrays(
+ MailServices.headerParser.parseDecodedHeader(check[0]),
+ check[1]
+ );
+ equalArrays(
+ MailServices.headerParser.parseEncodedHeader(check[0], "UTF-8"),
+ check[2]
+ );
+ }
+}
diff --git a/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js b/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js
new file mode 100644
index 0000000000..0e5748fd9c
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js
@@ -0,0 +1,415 @@
+/* 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/. */
+
+/**
+ * Tests to ensure signed and/or encrypted OpenPGP messages are
+ * processed correctly by mime.
+ */
+
+const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+const { EnigmailSingletons } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/singletons.jsm"
+);
+const { EnigmailVerify } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/mimeVerify.jsm"
+);
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+const { EnigmailDecryption } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/decryption.jsm"
+);
+
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var messageInjection = new MessageInjection({ mode: "local" });
+let gInbox = messageInjection.getInboxFolder();
+
+const keyDir = "../../../../mail/test/browser/openpgp/data/keys/";
+const browserEMLDir = "../../../../mail/test/browser/openpgp/data/eml/";
+
+const contents = "Sundays are nothing without callaloo.";
+
+/**
+ * This implements some of the methods of Enigmail.hdrView.headerPane so we can
+ * intercept and record the calls to updateSecurityStatus().
+ */
+const headerSink = {
+ expectResults(maxLen) {
+ this._deferred = PromiseUtils.defer();
+ this.expectedCount = maxLen;
+ this.countReceived = 0;
+ this.results = [];
+ EnigmailSingletons.messageReader = this;
+ return this._deferred.promise;
+ },
+ isCurrentMessage() {
+ return true;
+ },
+ isMultipartRelated() {
+ return false;
+ },
+ displaySubPart() {
+ return true;
+ },
+ hasUnauthenticatedParts() {
+ return false;
+ },
+ processDecryptionResult() {},
+ updateSecurityStatus(
+ unusedUriSpec,
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ uri,
+ extraDetails,
+ mimePartNumber
+ ) {
+ if (statusFlags & EnigmailConstants.PGP_MIME_SIGNED) {
+ this.results.push({
+ type: "signed",
+ status: statusFlags,
+ keyId,
+ });
+ } else if (statusFlags & EnigmailConstants.PGP_MIME_ENCRYPTED) {
+ this.results.push({
+ type: "encrypted",
+ status: statusFlags,
+ keyId,
+ });
+ }
+
+ this.countReceived++;
+ this.checkFinished();
+ },
+ modifyMessageHeaders() {},
+
+ checkFinished() {
+ if (this.countReceived == this.expectedCount) {
+ this._deferred.resolve(this.results);
+ }
+ },
+};
+
+/**
+ * @name Test
+ * @property {string} filename - Name of the eml file found in ${browserEMLDir}.
+ * @property {string} contents - Contents to expect in the file.
+ * @property {string} from - The email address the message is from.
+ * @property {string} [keyId] - The key id to expect the message from.
+ * @property {boolean} sig - If true, indicates the message is signed.
+ * @property {boolean} enc - If true, indicates the message is encrypted.
+ * @property {string[]} flags - A list of flags corresponding to those found in
+ * EnigmailConstants that we should expect the processed message to posses.
+ * Prefix a flag with "-" to indicate it should not be present.
+ * @property {boolean} [skip] - If true, the test will be skipped.
+ */
+
+/**
+ * All the tests we are going to run.
+ *
+ * @type Test[]
+ */
+const tests = [
+ {
+ description:
+ "signed, unencrypted message, with key attached, from verified sender",
+ filename:
+ "signed-by-0xfbfcc82a015e7330-to-0xf231550c4f47e38e-unencrypted-with-key.eml",
+ contents,
+ from: "bob@openpgp.example",
+ keyId: OpenPGPTestUtils.BOB_KEY_ID,
+ sig: true,
+ flags: ["GOOD_SIGNATURE", "-DECRYPTION_OKAY"],
+ },
+ {
+ description: "signed, unencrypted message, from verified sender",
+ filename:
+ "signed-by-0xfbfcc82a015e7330-to-0xf231550c4f47e38e-unencrypted.eml",
+ contents,
+ from: "bob@openpgp.example",
+ keyId: OpenPGPTestUtils.BOB_KEY_ID,
+ sig: true,
+ flags: ["GOOD_SIGNATURE", "-DECRYPTION_OKAY"],
+ },
+ {
+ description:
+ "unsigned, encrypted message, with key attached, from verified sender",
+ filename:
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330-with-key.eml",
+ contents,
+ from: "bob@openpgp.example",
+ enc: true,
+ flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"],
+ },
+ {
+ description: "unsigned, encrypted message, from verified sender",
+ filename:
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330.eml",
+ contents,
+ from: "bob@openpgp.example",
+ enc: true,
+ flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"],
+ },
+ {
+ description:
+ "signed, encrypted message, with key attached from verified sender",
+ filename:
+ "signed-by-0xfbfcc82a015e7330-encrypted-to-0xf231550c4f47e38e-with-key.eml",
+ from: "bob@openpgp.example",
+ keyId: OpenPGPTestUtils.BOB_KEY_ID,
+ contents,
+ enc: true,
+ sig: true,
+ flags: ["DECRYPTION_OKAY", "GOOD_SIGNATURE"],
+ },
+ {
+ description: "signed, encrypted message, from verified sender",
+ filename:
+ "signed-by-0xfbfcc82a015e7330-encrypted-to-0xf231550c4f47e38e.eml",
+ from: "bob@openpgp.example",
+ keyId: OpenPGPTestUtils.BOB_KEY_ID,
+ contents,
+ enc: true,
+ sig: true,
+ flags: ["DECRYPTION_OKAY", "GOOD_SIGNATURE"],
+ },
+ // Sender with no public key registered or accepted.
+ {
+ description:
+ "signed, unencrypted message, with key attached from sender not in database",
+ filename:
+ "signed-by-0x3099ff1238852b9f-to-0xf231550c4f47e38e-unencrypted-with-key.eml",
+ contents,
+ from: "carol@openpgp.example",
+ keyId: OpenPGPTestUtils.CAROL_KEY_ID,
+ sig: true,
+ flags: ["-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE", "NO_PUBKEY"],
+ },
+ {
+ description: "signed, unencrypted message, from sender not in database",
+ filename:
+ "signed-by-0x3099ff1238852b9f-to-0xf231550c4f47e38e-unencrypted.eml",
+ contents,
+ from: "carol@openpgp.example",
+ keyId: OpenPGPTestUtils.CAROL_KEY_ID,
+ sig: true,
+ flags: ["-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE", "NO_PUBKEY"],
+ },
+ {
+ description:
+ "unsigned, encrypted message, with key attached, from sender not in database",
+ filename:
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0x3099ff1238852b9f-with-key.eml",
+ contents,
+ from: "carol@openpgp.example",
+ enc: true,
+ flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"],
+ },
+ {
+ description: "unsigned, encrypted message, from sender not in database",
+ filename:
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0x3099ff1238852b9f.eml",
+ contents,
+ from: "carol@openpgp.example",
+ enc: true,
+ flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"],
+ },
+ {
+ description:
+ "signed, encrypted message, with key attached, from sender not in database",
+ filename:
+ "signed-by-0x3099ff1238852b9f-encrypted-to-0xf231550c4f47e38e-with-key.eml",
+ contents,
+ from: "carol@openpgp.example",
+ keyId: OpenPGPTestUtils.CAROL_KEY_ID,
+ enc: true,
+ sig: true,
+ resultCount: 1,
+ flags: ["-DECRYPTION_FAILED", "-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE"],
+ },
+ {
+ description: "signed, encrypted message, from sender not in database",
+ filename:
+ "signed-by-0x3099ff1238852b9f-encrypted-to-0xf231550c4f47e38e.eml",
+ contents,
+ from: "carol@openpgp.example",
+ keyId: OpenPGPTestUtils.CAROL_KEY_ID,
+ enc: true,
+ sig: true,
+ resultCount: 1,
+ flags: ["-DECRYPTION_FAILED", "-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE"],
+ },
+ // Last two characters of signature swapped.
+ {
+ description: "signed message, signature damaged",
+ filename: "bob-to-alice-signed-damaged-signature.eml",
+ from: "bob@openpgp.example",
+ contents,
+ sig: true,
+ flags: ["-GOOD_SIGNATURE", "BAD_SIGNATURE"],
+ },
+];
+
+/**
+ * Initialize OpenPGP, import Alice and Bob's keys, then install the messages
+ * we are going to test.
+ */
+add_setup(async function () {
+ await OpenPGPTestUtils.initOpenPGP();
+
+ await OpenPGPTestUtils.importPrivateKey(
+ null,
+ do_get_file(`${keyDir}alice@openpgp.example-0xf231550c4f47e38e-secret.asc`)
+ );
+
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}bob@openpgp.example-0xfbfcc82a015e7330-pub.asc`)
+ );
+
+ for (let test of tests) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+
+ MailServices.copy.copyFileMessage(
+ do_get_file(`${browserEMLDir}${test.filename}`),
+ gInbox,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+
+ await promiseCopyListener.promise;
+ promiseCopyListener = null;
+ }
+});
+
+/**
+ * This executes a test for each entry in the tests array. We test mostly
+ * that the contents are correct and updateSecurityStatus() repoorts the
+ * status flags the test specifies.
+ */
+add_task(async function testMimeDecryptOpenPGPMessages() {
+ let hdrIndex = 0;
+ for (let test of tests) {
+ if (test.skip) {
+ info(`Skipped test: ${test.description}`);
+ continue;
+ }
+
+ info(`Running test: ${test.description}`);
+
+ let testPrefix = `${test.filename}:`;
+ let expectedResultCount =
+ test.resultCount || (test.enc && test.sig) ? 2 : 1;
+ let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex);
+ let uri = hdr.folder.getUriForMsg(hdr);
+ let sinkPromise = headerSink.expectResults(expectedResultCount);
+
+ // Set the message window so displayStatus() invokes the hooks we are
+ // interested in.
+ EnigmailVerify.lastWindow = {};
+
+ // Stub this function so verifyDetached() can get the correct email.
+ EnigmailDecryption.getFromAddr = () => test.from;
+
+ // Trigger the actual mime work.
+ let conversion = apply_mime_conversion(uri, headerSink);
+
+ await conversion.promise;
+
+ let msgBody = conversion._data;
+
+ if (!test.sig || test.flags.indexOf("GOOD_SIGNATURE")) {
+ Assert.ok(
+ msgBody.includes(test.contents),
+ `${testPrefix} message contents match`
+ );
+ }
+
+ // Check that we're also using the display output.
+ Assert.ok(
+ msgBody.includes("<html>"),
+ `${testPrefix} message displayed as html`
+ );
+ await sinkPromise;
+
+ let idx = 0;
+ let { results } = headerSink;
+
+ Assert.equal(
+ results.length,
+ expectedResultCount,
+ `${testPrefix} updateSecurityStatus() called ${expectedResultCount} time(s)`
+ );
+
+ if (test.enc) {
+ Assert.equal(
+ results[idx].type,
+ "encrypted",
+ `${testPrefix} message recognized as encrypted`
+ );
+
+ if (expectedResultCount > 1) {
+ idx++;
+ }
+ }
+
+ if (test.sig) {
+ Assert.equal(
+ results[idx].type,
+ "signed",
+ `${testPrefix} message recognized as signed`
+ );
+ }
+
+ if (test.keyId) {
+ Assert.equal(
+ results[idx].keyId,
+ test.keyId,
+ `${testPrefix}key ids match`
+ );
+ }
+
+ // Test the expected message flags match the message status.
+ // We combine the signed and encrypted flags via bitwise OR to
+ // test in one place.
+ if (test.flags) {
+ for (let flag of test.flags) {
+ let flags = results.reduce((prev, curr) => prev | curr.status, 0);
+ let negative = flag[0] === "-";
+ flag = negative ? flag.slice(1) : flag;
+
+ if (negative) {
+ Assert.ok(
+ !(flags & EnigmailConstants[flag]),
+ `${testPrefix} status flag "${flag}" not detected`
+ );
+ } else {
+ Assert.ok(
+ flags & EnigmailConstants[flag],
+ `${testPrefix} status flag "${flag}" detected`
+ );
+ }
+ }
+ }
+
+ hdrIndex++;
+ }
+});
diff --git a/comm/mailnews/mime/test/unit/test_parser.js b/comm/mailnews/mime/test/unit/test_parser.js
new file mode 100644
index 0000000000..979e50975c
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_parser.js
@@ -0,0 +1,322 @@
+/* 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/. */
+
+// This file is used to test the mime parser implemented in JS, mostly by means
+// of creating custom emitters and verifying that the methods on that emitter
+// are called in the correct order. This also tests that the various
+// HeaderParser methods are run correctly.
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+// Utility method to compare objects
+function compare_objects(real, expected) {
+ // real is a Map; convert it into an object for uneval purposes
+ if (typeof real == "object") {
+ var newreal = {};
+ for (let [k, v] of real) {
+ newreal[k] = v;
+ }
+ real = newreal;
+ }
+ var a = uneval(real),
+ b = uneval(expected);
+ // Very long strings don't get printed out fully (unless they're wrong)
+ if ((a.length > 100 || b.length > 100) && a == b) {
+ Assert.ok(a == b);
+ } else {
+ Assert.equal(a, b);
+ }
+}
+
+// Returns and deletes object[field] if present, or undefined if not.
+function extract_field(object, field) {
+ if (field in object) {
+ var result = object[field];
+ delete object[field];
+ return result;
+ }
+ return undefined;
+}
+
+// A file cache for read_file.
+var file_cache = {};
+
+/**
+ * Read a file into a string (all line endings become CRLF).
+ */
+async function read_file(file, start, end) {
+ if (!(file in file_cache)) {
+ var realFile = do_get_file("../../../data/" + file);
+ file_cache[file] = (await IOUtils.readUTF8(realFile.path)).split(
+ /\r\n|[\r\n]/
+ );
+ }
+ var contents = file_cache[file];
+ if (start !== undefined) {
+ contents = contents.slice(start - 1, end - 1);
+ }
+ return contents.join("\r\n");
+}
+
+/**
+ * Helper for body tests.
+ *
+ * Some extra options are listed too:
+ * _split: The contents of the file will be passed in packets split by this
+ * regex. Be sure to include the split delimiter in a group so that they
+ * are included in the output packets!
+ * _eol: The CRLFs in the input file will be replaced with the given line
+ * ending instead.
+ *
+ * @param test The name of test
+ * @param file The name of the file to read (relative to mailnews/data)
+ * @param opts Options for the mime parser, as well as a few extras detailed
+ * above.
+ * @param partspec An array of [partnum, line start, line end] detailing the
+ * expected parts in the body. It will be expected that the
+ * accumulated body part data for partnum would be the contents
+ * of the file from [line start, line end) [1-based lines]
+ */
+async function make_body_test(test, file, opts, partspec) {
+ let results = [];
+ for (let p of partspec) {
+ results.push([p[0], await read_file(file, p[1], p[2])]);
+ }
+
+ let msgcontents = await read_file(file);
+ return [test, msgcontents, opts, results];
+}
+
+async function make_bodydecode_test(test, file, opts, expected) {
+ let msgcontents = await read_file(file);
+ return [test, msgcontents, opts, expected];
+}
+
+// This is the expected part specifier for the multipart-complex1 test file,
+// specified here because it is used in several cases.
+var mpart_complex1 = [
+ ["1", 8, 10],
+ ["2", 14, 16],
+ ["3.1", 22, 24],
+ ["4", 29, 31],
+ ["5", 33, 35],
+];
+
+// Format of tests:
+// entry[0] = name of the test
+// entry[1] = message (a string or an array of packets)
+// entry[2] = options for the MIME parser
+// entry[3] = A checker result:
+// either a {partnum: header object} (to check headers)
+// or a [[partnum body], [partnum body], ...] (to check bodies)
+// (the partnums refer to the expected part numbers of the MIME test)
+// For body tests, unless you're testing decoding, use make_body_test.
+// For decoding tests, use make_bodydecode_test
+var parser_tests = [
+ // Body tests from data
+ // (Note: line numbers are 1-based. Also, to capture trailing EOF, add 2 to
+ // the last line number of the file).
+ make_body_test("Basic body", "basic1", {}, [["", 3, 5]]),
+ make_body_test("Basic multipart", "multipart1", {}, [["1", 10, 12]]),
+ make_body_test("Basic multipart", "multipart2", {}, [["1", 8, 11]]),
+ make_body_test("Complex multipart", "multipart-complex1", {}, mpart_complex1),
+ make_body_test("Truncated multipart", "multipart-complex2", {}, [
+ ["1.1.1.1", 21, 25],
+ ["2", 27, 57],
+ ["3", 60, 62],
+ ]),
+ make_body_test("No LF multipart", "multipartmalt-detach", {}, [
+ ["1", 20, 21],
+ ["2.1", 27, 38],
+ ["2.2", 42, 43],
+ ["2.3", 47, 48],
+ ]),
+ make_body_test("Raw body", "multipart1", { bodyformat: "raw" }, [
+ ["", 4, 14],
+ ]),
+ make_bodydecode_test(
+ "Base64 decode 1",
+ "base64-1",
+ { bodyformat: "decode" },
+ [
+ [
+ "",
+ "\r\nHello, world! (Again...)\r\n\r\nLet's see how well base64 text" +
+ " is handled. Yay, lots of spaces! There" +
+ "'s even a CRLF at the end and one at the beginning, but the output" +
+ " shouldn't have it.\r\n",
+ ],
+ ]
+ ),
+ make_bodydecode_test(
+ "Base64 decode 2",
+ "base64-2",
+ { bodyformat: "decode" },
+ [
+ [
+ "",
+ "<html><body>This is base64 encoded HTML text, and the tags shouldn" +
+ "'t be stripped.\r\n<b>Bold text is bold!</b></body></html>\r\n",
+ ],
+ ]
+ ),
+ make_body_test("Base64 nodecode", "base64-1", {}, [["", 4, 9]]),
+ make_bodydecode_test(
+ "QP decode",
+ "bug505221",
+ { pruneat: "1", bodyformat: "decode" },
+ [
+ [
+ "1",
+ '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r' +
+ '\n<HTML><HEAD>\r\n<META HTTP-EQUIV="Content-Type" CONTENT="text/h' +
+ 'tml; charset=us-ascii">\r\n\r\n\r\n<META content="MSHTML 6.00.600' +
+ '0.16735" name=GENERATOR></HEAD>\r\n<BODY> bbb\r\n</BODY></HTML>',
+ ],
+ ]
+ ),
+
+ // Comprehensive tests from the torture test
+ make_body_test("Torture regular body", "mime-torture", {}, [
+ ["1", 17, 21],
+ ["2$.1", 58, 75],
+ ["2$.2.1", 83, 97],
+ ["2$.3", 102, 130],
+ ["3$", 155, 7742],
+ ["4", 7747, 8213],
+ ["5", 8218, 8242],
+ ["6$.1.1", 8284, 8301],
+ ["6$.1.2", 8306, 8733],
+ ["6$.2.1", 8742, 9095],
+ ["6$.2.2", 9100, 9354],
+ ["6$.2.3", 9357, 11794],
+ ["6$.2.4", 11797, 12155],
+ ["6$.3", 12161, 12809],
+ ["7$.1", 12844, 12845],
+ ["7$.2", 12852, 13286],
+ ["7$.3", 13288, 13297],
+ ["8$.1", 13331, 13358],
+ ["8$.2", 13364, 13734],
+ ["9$", 13757, 20179],
+ ["10", 20184, 21200],
+ ["11$.1", 21223, 22031],
+ ["11$.2", 22036, 22586],
+ ["12$.1", 22607, 23469],
+ ["12$.2", 23474, 23774],
+ ["12$.3$.1", 23787, 23795],
+ ["12$.3$.2.1", 23803, 23820],
+ ["12$.3$.2.2", 23825, 24633],
+ ["12$.3$.3", 24640, 24836],
+ ["12$.3$.4$", 24848, 25872],
+ ]),
+ make_body_test("Torture pruneat", "mime-torture", { pruneat: "4" }, [
+ ["4", 7747, 8213],
+ ]),
+];
+
+function test_parser(message, opts, results) {
+ var checkingHeaders = !(results instanceof Array);
+ var calls = 0,
+ dataCalls = 0;
+ var fusingParts = extract_field(opts, "_nofuseparts") === undefined;
+ var emitter = {
+ stack: [],
+ startMessage: function emitter_startMsg() {
+ Assert.equal(this.stack.length, 0, "no stack at start");
+ calls++;
+ this.partData = "";
+ },
+ endMessage: function emitter_endMsg() {
+ Assert.equal(this.stack.length, 0, "no stack at end");
+ calls++;
+ },
+ startPart: function emitter_startPart(partNum, headers) {
+ this.stack.push(partNum);
+ if (checkingHeaders) {
+ Assert.ok(partNum in results);
+ compare_objects(headers, results[partNum]);
+ if (fusingParts) {
+ Assert.equal(this.partData, "");
+ }
+ }
+ },
+ deliverPartData: function emitter_partData(partNum, data) {
+ Assert.equal(this.stack[this.stack.length - 1], partNum);
+ try {
+ if (!checkingHeaders) {
+ if (fusingParts) {
+ this.partData += data;
+ } else {
+ Assert.equal(partNum, results[dataCalls][0]);
+ compare_objects(data, results[dataCalls][1]);
+ }
+ }
+ } finally {
+ if (!fusingParts) {
+ dataCalls++;
+ }
+ }
+ },
+ endPart: function emitter_endPart(partNum) {
+ if (this.partData != "") {
+ Assert.equal(partNum, results[dataCalls][0]);
+ compare_objects(this.partData, results[dataCalls][1]);
+ dataCalls++;
+ this.partData = "";
+ }
+ Assert.equal(this.stack.pop(), partNum);
+ },
+ };
+ opts.onerror = function (e) {
+ throw e;
+ };
+ MimeParser.parseSync(message, emitter, opts);
+ Assert.equal(calls, 2);
+ if (!checkingHeaders) {
+ Assert.equal(dataCalls, results.length);
+ }
+}
+
+// Format of tests:
+// entry[0] = header
+// entry[1] = flags
+// entry[2] = result to match
+var header_tests = [
+ // Parameter passing
+ ["multipart/related", MimeParser.HEADER_PARAMETER, ["multipart/related", {}]],
+ ["a ; b=v", MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
+ ["a ; b='v'", MimeParser.HEADER_PARAMETER, ["a", { b: "'v'" }]],
+ ['a; b = "v"', MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
+ ["a;b=1;b=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1" }]],
+ ["a;b=2;b=1", MimeParser.HEADER_PARAMETER, ["a", { b: "2" }]],
+ ['a;b="a;b"', MimeParser.HEADER_PARAMETER, ["a", { b: "a;b" }]],
+ ['a;b="\\\\"', MimeParser.HEADER_PARAMETER, ["a", { b: "\\" }]],
+ ['a;b="a\\b\\c"', MimeParser.HEADER_PARAMETER, ["a", { b: "abc" }]],
+ ["a;b=1;c=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1", c: "2" }]],
+ ['a;b="a\\', MimeParser.HEADER_PARAMETER, ["a", { b: "a" }]],
+ ["a;b", MimeParser.HEADER_PARAMETER, ["a", {}]],
+ ['a;b=";";c=d', MimeParser.HEADER_PARAMETER, ["a", { b: ";", c: "d" }]],
+];
+
+function test_header(headerValue, flags, expected) {
+ let result = MimeParser.parseHeaderField(headerValue, flags);
+ Assert.equal(result.preSemi, expected[0]);
+ compare_objects(result, expected[1]);
+}
+
+add_task(async function testit() {
+ for (let test of parser_tests) {
+ test = await test;
+ dump("Testing message " + test[0]);
+ if (test[1] instanceof Array) {
+ dump(" using " + test[1].length + " packets");
+ }
+ dump("\n");
+ test_parser(test[1], test[2], test[3]);
+ }
+ for (let test of header_tests) {
+ dump("Testing value ->" + test[0] + "<- with flags " + test[1] + "\n");
+ test_header(test[0], test[1], test[2]);
+ }
+});
diff --git a/comm/mailnews/mime/test/unit/test_rfc822_body.js b/comm/mailnews/mime/test/unit/test_rfc822_body.js
new file mode 100644
index 0000000000..8a64e68d42
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_rfc822_body.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+/**
+ * This test verifies that we emit a message/rfc822 body part as an attachment
+ * whether or not mail.inline_attachments is true.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+var msgGen = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+var inbox = messageInjection.getInboxFolder();
+
+add_task(async function test_rfc822_body_display_inline() {
+ Services.prefs.setBoolPref("mail.inline_attachments", true);
+ await help_test_rfc822_body({
+ // a message whose body is itself a message
+ bodyPart: msgGen.makeMessage(),
+ attachmentCount: 1,
+ });
+ await help_test_rfc822_body({
+ // a message whose body is itself a message, and which has an attachment
+ bodyPart: msgGen.makeMessage({
+ attachments: [
+ {
+ body: "I'm an attachment!",
+ filename: "attachment.txt",
+ format: "",
+ },
+ ],
+ }),
+ attachmentCount: 2,
+ });
+});
+
+add_task(async function test_rfc822_body_no_display_inline() {
+ Services.prefs.setBoolPref("mail.inline_attachments", false);
+ await help_test_rfc822_body({
+ // a message whose body is itself a message
+ bodyPart: msgGen.makeMessage(),
+ attachmentCount: 1,
+ });
+ await help_test_rfc822_body({
+ // a message whose body is itself a message, and which has an attachment
+ bodyPart: msgGen.makeMessage({
+ attachments: [
+ {
+ body: "I'm an attachment!",
+ filename: "attachment.txt",
+ format: "",
+ },
+ ],
+ }),
+ attachmentCount: 1,
+ });
+});
+
+async function help_test_rfc822_body(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener({
+ onStopRequest(request, statusCode) {
+ request.QueryInterface(Ci.nsIMailChannel);
+ Assert.equal(request.attachments.length, info.attachmentCount);
+ },
+ });
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ null,
+ null,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ false
+ );
+
+ await streamListener.promise;
+}
diff --git a/comm/mailnews/mime/test/unit/test_smime_decrypt.js b/comm/mailnews/mime/test/unit/test_smime_decrypt.js
new file mode 100644
index 0000000000..815f786224
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_smime_decrypt.js
@@ -0,0 +1,701 @@
+/* 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/. */
+
+/**
+ * Tests to ensure signed and/or encrypted S/MIME messages are
+ * processed correctly, and the signature status is treated as good
+ * or bad as expected.
+ */
+
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { SmimeUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/smimeUtils.jsm"
+);
+
+add_setup(function () {
+ let messageInjection = new MessageInjection({ mode: "local" });
+ gInbox = messageInjection.getInboxFolder();
+ SmimeUtils.ensureNSS();
+
+ SmimeUtils.loadPEMCertificate(
+ do_get_file(smimeDataDirectory + "TestCA.pem"),
+ Ci.nsIX509Cert.CA_CERT
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Alice.p12"),
+ "nss"
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Bob.p12"),
+ "nss"
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Dave.p12"),
+ "nss"
+ );
+});
+
+add_task(async function verifyTestCertsStillValid() {
+ // implementation of nsIDoneFindCertForEmailCallback
+ var doneFindCertForEmailCallback = {
+ findCertDone(email, cert) {
+ Assert.notEqual(cert, null);
+ if (!cert) {
+ Assert.ok(
+ false,
+ "The S/MIME test certificates are invalid today.\n" +
+ "Please look at the expiration date in file comm/mailnews/test/data/smime/expiration.txt\n" +
+ "If that date is in the past, new certificates need to be generated and committed.\n" +
+ "Follow the instructions in comm/mailnews/test/data/smime/README.md\n" +
+ "If that date is in the future, the test failure is unrelated to expiration and indicates " +
+ "an error in certificate validation."
+ );
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIDoneFindCertForEmailCallback"]),
+ };
+
+ let composeSecure = Cc[
+ "@mozilla.org/messengercompose/composesecure;1"
+ ].createInstance(Ci.nsIMsgComposeSecure);
+ composeSecure.asyncFindCertByEmailAddr(
+ "Alice@example.com",
+ doneFindCertForEmailCallback
+ );
+});
+
+var gInbox;
+
+var smimeDataDirectory = "../../../data/smime/";
+
+let smimeHeaderSink = {
+ expectResults(maxLen) {
+ // dump("Restarting for next test\n");
+ this._deferred = PromiseUtils.defer();
+ this._expectedEvents = maxLen;
+ this.countReceived = 0;
+ this._results = [];
+ this.haveSignedBad = false;
+ this.haveEncryptionBad = false;
+ this.resultSig = null;
+ this.resultEnc = null;
+ this.resultSigFirst = undefined;
+ return this._deferred.promise;
+ },
+ signedStatus(aNestingLevel, aSignedStatus, aSignerCert) {
+ console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel);
+ // dump("Signed message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveSignedBad) {
+ // override with newer allowed
+ this.resultSig = {
+ type: "signed",
+ status: aSignedStatus,
+ certificate: aSignerCert,
+ };
+ if (aSignedStatus != 0) {
+ this.haveSignedBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = true;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) {
+ console.log(
+ "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel
+ );
+ // dump("Encrypted message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveEncryptionBad) {
+ // override with newer allowed
+ this.resultEnc = {
+ type: "encrypted",
+ status: aEncryptedStatus,
+ certificate: aRecipientCert,
+ };
+ if (aEncryptedStatus != 0) {
+ this.haveEncryptionBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = false;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ checkFinished() {
+ if (this.countReceived == this._expectedEvents) {
+ if (this.resultSigFirst) {
+ this._results.push(this.resultSig);
+ if (this.resultEnc != null) {
+ this._results.push(this.resultEnc);
+ }
+ } else {
+ this._results.push(this.resultEnc);
+ if (this.resultSig != null) {
+ this._results.push(this.resultSig);
+ }
+ }
+ this._deferred.resolve(this._results);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]),
+};
+
+/**
+ * Note on FILENAMES taken from the NSS test suite:
+ * - env: CMS enveloped (encrypted)
+ * - dsig: CMS detached signature (with multipart MIME)
+ * - sig: CMS opaque signature (content embedded inside signature)
+ * - bad: message text does not match signature
+ * - mismatch: embedded content is different
+ *
+ * Control variables used for checking results:
+ * - env: If true, we expect a report to encryptionStatus() that message
+ * is encrypted.
+ * - sig: If true, we expect a report to signedStatus() that message
+ * is signed.
+ * - sig_good: If true, we expect that the reported signature has a
+ * good status.
+ * If false, we expect a report of bad status.
+ * Because of the sequential processing caused by nested
+ * messages, additional calls to signedStatus() might
+ * override an earlier decision.
+ * (An earlier bad status report cannot be overridden by a
+ * later report of a good status.)
+ * - extra: If set to a number > 0, we expect that nested processing of
+ * MIME parts will trigger the given number of additional
+ * status calls.
+ * (default is 0.)
+ * - dave: If true, we expect that the outermost message was done by
+ * Dave's certificate.
+ * (default is false, which means we expect Alice's cert.)
+ */
+
+var gMessages = [
+ {
+ filename: "alice.env.eml",
+ enc: true,
+ sig: false,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.future.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.sig.SHA1.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA256.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA384.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA512.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+
+ // encrypt-then-sign
+ {
+ filename: "alice.env.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.env.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+
+ // encrypt-then-sign, then sign again
+ {
+ filename: "alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+
+ // sign, then sign again
+ {
+ filename: "alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+
+ {
+ filename: "alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+];
+
+let gCopyWaiter = PromiseUtils.defer();
+
+add_task(async function copy_messages() {
+ for (let msg of gMessages) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+
+ MailServices.copy.copyFileMessage(
+ do_get_file(smimeDataDirectory + msg.filename),
+ gInbox,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+
+ await promiseCopyListener.promise;
+ promiseCopyListener = null;
+ }
+ gCopyWaiter.resolve();
+});
+
+add_task(async function check_smime_message() {
+ await gCopyWaiter.promise;
+
+ let hdrIndex = 0;
+
+ for (let msg of gMessages) {
+ console.log("checking " + msg.filename);
+
+ let numExpected = 1;
+ if (msg.enc && msg.sig) {
+ numExpected++;
+ }
+
+ let eventsExpected = numExpected;
+ if ("extra" in msg) {
+ eventsExpected += msg.extra;
+ }
+
+ let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex);
+ let uri = hdr.folder.getUriForMsg(hdr);
+ let sinkPromise = smimeHeaderSink.expectResults(eventsExpected);
+
+ let conversion = apply_mime_conversion(uri, smimeHeaderSink);
+ await conversion.promise;
+
+ let contents = conversion._data;
+ // dump("contents: " + contents + "\n");
+
+ if (!msg.sig || msg.sig_good || "check_text" in msg) {
+ let expected = "This is a test message from Alice to Bob.";
+ Assert.ok(contents.includes(expected));
+ }
+ // Check that we're also using the display output.
+ Assert.ok(contents.includes("<html>"));
+
+ await sinkPromise;
+
+ let r = smimeHeaderSink._results;
+ Assert.equal(r.length, numExpected);
+
+ let sigIndex = 0;
+
+ if (msg.enc) {
+ Assert.equal(r[0].type, "encrypted");
+ Assert.equal(r[0].status, 0);
+ Assert.equal(r[0].certificate, null);
+ sigIndex = 1;
+ }
+ if (msg.sig) {
+ Assert.equal(r[sigIndex].type, "signed");
+ let cert = r[sigIndex].certificate;
+ if (msg.sig_good) {
+ Assert.notEqual(cert, null);
+ }
+ if (cert) {
+ if ("dave" in msg) {
+ Assert.equal(cert.emailAddress, "dave@example.com");
+ } else {
+ Assert.equal(cert.emailAddress, "alice@example.com");
+ }
+ }
+ if (msg.sig_good) {
+ Assert.equal(r[sigIndex].status, 0);
+ } else {
+ Assert.notEqual(r[sigIndex].status, 0);
+ }
+ }
+
+ hdrIndex++;
+ }
+});
diff --git a/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js b/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js
new file mode 100644
index 0000000000..3e2eff43ae
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js
@@ -0,0 +1,717 @@
+/* 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/. */
+
+/**
+ * This file is mostly a copy of test_smime_decrypt.js
+ * with the difference that pref
+ * mail.smime.accept_insecure_sha1_message_signatures is set to true,
+ * and tests using sha-1 are expected to pass.
+ *
+ * This file must not run in parallel with other s/mime tests.
+ */
+
+/**
+ * Tests to ensure signed and/or encrypted S/MIME messages are
+ * processed correctly, and the signature status is treated as good
+ * or bad as expected.
+ */
+
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { SmimeUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/smimeUtils.jsm"
+);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(
+ "mail.smime.accept_insecure_sha1_message_signatures"
+ );
+});
+
+add_setup(function () {
+ Services.prefs.setBoolPref(
+ "mail.smime.accept_insecure_sha1_message_signatures",
+ true
+ );
+
+ let messageInjection = new MessageInjection({ mode: "local" });
+ gInbox = messageInjection.getInboxFolder();
+ SmimeUtils.ensureNSS();
+
+ SmimeUtils.loadPEMCertificate(
+ do_get_file(smimeDataDirectory + "TestCA.pem"),
+ Ci.nsIX509Cert.CA_CERT
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Alice.p12"),
+ "nss"
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Bob.p12"),
+ "nss"
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Dave.p12"),
+ "nss"
+ );
+});
+
+add_task(async function verifyTestCertsStillValid() {
+ // implementation of nsIDoneFindCertForEmailCallback
+ var doneFindCertForEmailCallback = {
+ findCertDone(email, cert) {
+ Assert.notEqual(cert, null);
+ if (!cert) {
+ Assert.ok(
+ false,
+ "The S/MIME test certificates are invalid today.\n" +
+ "Please look at the expiration date in file comm/mailnews/test/data/smime/expiration.txt\n" +
+ "If that date is in the past, new certificates need to be generated and committed.\n" +
+ "Follow the instructions in comm/mailnews/test/data/smime/README.md\n" +
+ "If that date is in the future, the test failure is unrelated to expiration and indicates " +
+ "an error in certificate validation."
+ );
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIDoneFindCertForEmailCallback"]),
+ };
+
+ let composeSecure = Cc[
+ "@mozilla.org/messengercompose/composesecure;1"
+ ].createInstance(Ci.nsIMsgComposeSecure);
+ composeSecure.asyncFindCertByEmailAddr(
+ "Alice@example.com",
+ doneFindCertForEmailCallback
+ );
+});
+
+var gInbox;
+
+var smimeDataDirectory = "../../../data/smime/";
+
+let smimeHeaderSink = {
+ expectResults(maxLen) {
+ // dump("Restarting for next test\n");
+ this._deferred = PromiseUtils.defer();
+ this._expectedEvents = maxLen;
+ this.countReceived = 0;
+ this._results = [];
+ this.haveSignedBad = false;
+ this.haveEncryptionBad = false;
+ this.resultSig = null;
+ this.resultEnc = null;
+ this.resultSigFirst = undefined;
+ return this._deferred.promise;
+ },
+ signedStatus(aNestingLevel, aSignedStatus, aSignerCert) {
+ console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel);
+ // dump("Signed message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveSignedBad) {
+ // override with newer allowed
+ this.resultSig = {
+ type: "signed",
+ status: aSignedStatus,
+ certificate: aSignerCert,
+ };
+ if (aSignedStatus != 0) {
+ this.haveSignedBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = true;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) {
+ console.log(
+ "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel
+ );
+ // dump("Encrypted message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveEncryptionBad) {
+ // override with newer allowed
+ this.resultEnc = {
+ type: "encrypted",
+ status: aEncryptedStatus,
+ certificate: aRecipientCert,
+ };
+ if (aEncryptedStatus != 0) {
+ this.haveEncryptionBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = false;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ checkFinished() {
+ if (this.countReceived == this._expectedEvents) {
+ if (this.resultSigFirst) {
+ this._results.push(this.resultSig);
+ if (this.resultEnc != null) {
+ this._results.push(this.resultEnc);
+ }
+ } else {
+ this._results.push(this.resultEnc);
+ if (this.resultSig != null) {
+ this._results.push(this.resultSig);
+ }
+ }
+ this._deferred.resolve(this._results);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]),
+};
+
+/**
+ * Note on FILENAMES taken from the NSS test suite:
+ * - env: CMS enveloped (encrypted)
+ * - dsig: CMS detached signature (with multipart MIME)
+ * - sig: CMS opaque signature (content embedded inside signature)
+ * - bad: message text does not match signature
+ * - mismatch: embedded content is different
+ *
+ * Control variables used for checking results:
+ * - env: If true, we expect a report to encryptionStatus() that message
+ * is encrypted.
+ * - sig: If true, we expect a report to signedStatus() that message
+ * is signed.
+ * - sig_good: If true, we expect that the reported signature has a
+ * good status.
+ * If false, we expect a report of bad status.
+ * Because of the sequential processing caused by nested
+ * messages, additional calls to signedStatus() might
+ * override an earlier decision.
+ * (An earlier bad status report cannot be overridden by a
+ * later report of a good status.)
+ * - extra: If set to a number > 0, we expect that nested processing of
+ * MIME parts will trigger the given number of additional
+ * status calls.
+ * (default is 0.)
+ * - dave: If true, we expect that the outermost message was done by
+ * Dave's certificate.
+ * (default is false, which means we expect Alice's cert.)
+ */
+
+var gMessages = [
+ {
+ filename: "alice.env.eml",
+ enc: true,
+ sig: false,
+ sig_good: false,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ check_text: true,
+ },
+ {
+ filename: "alice.dsig.SHA1.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA256.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA384.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.bad.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.dsig.SHA512.multipart.mismatch-econtent.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ },
+ {
+ filename: "alice.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ check_text: true,
+ },
+ {
+ filename: "alice.sig.SHA1.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ check_text: true,
+ },
+ {
+ filename: "alice.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA256.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA384.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: true,
+ },
+ {
+ filename: "alice.sig.SHA512.opaque.env.eml",
+ enc: true,
+ sig: true,
+ sig_good: true,
+ },
+
+ // encrypt-then-sign
+ {
+ filename: "alice.env.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ extra: 1,
+ },
+
+ // encrypt-then-sign, then sign again
+ {
+ filename: "alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+
+ // sign, then sign again
+ {
+ filename: "alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+
+ {
+ filename: "alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename: "alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+ {
+ filename:
+ "alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml",
+ enc: false,
+ sig: true,
+ sig_good: false,
+ dave: 1,
+ extra: 1,
+ },
+];
+
+let gCopyWaiter = PromiseUtils.defer();
+
+add_task(async function copy_messages() {
+ for (let msg of gMessages) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+
+ MailServices.copy.copyFileMessage(
+ do_get_file(smimeDataDirectory + msg.filename),
+ gInbox,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+
+ await promiseCopyListener.promise;
+ promiseCopyListener = null;
+ }
+ gCopyWaiter.resolve();
+});
+
+add_task(async function check_smime_message() {
+ await gCopyWaiter.promise;
+
+ let hdrIndex = 0;
+
+ for (let msg of gMessages) {
+ console.log("checking " + msg.filename);
+
+ let numExpected = 1;
+ if (msg.enc && msg.sig) {
+ numExpected++;
+ }
+
+ let eventsExpected = numExpected;
+ if ("extra" in msg) {
+ eventsExpected += msg.extra;
+ }
+
+ let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex);
+ let uri = hdr.folder.getUriForMsg(hdr);
+ let sinkPromise = smimeHeaderSink.expectResults(eventsExpected);
+
+ let conversion = apply_mime_conversion(uri, smimeHeaderSink);
+ await conversion.promise;
+
+ let contents = conversion._data;
+ // dump("contents: " + contents + "\n");
+
+ if (!msg.sig || msg.sig_good || "check_text" in msg) {
+ let expected = "This is a test message from Alice to Bob.";
+ Assert.ok(contents.includes(expected));
+ }
+ // Check that we're also using the display output.
+ Assert.ok(contents.includes("<html>"));
+
+ await sinkPromise;
+
+ let r = smimeHeaderSink._results;
+ Assert.equal(r.length, numExpected);
+
+ let sigIndex = 0;
+
+ if (msg.enc) {
+ Assert.equal(r[0].type, "encrypted");
+ Assert.equal(r[0].status, 0);
+ Assert.equal(r[0].certificate, null);
+ sigIndex = 1;
+ }
+ if (msg.sig) {
+ Assert.equal(r[sigIndex].type, "signed");
+ let cert = r[sigIndex].certificate;
+ if (msg.sig_good) {
+ Assert.notEqual(cert, null);
+ }
+ if (cert) {
+ if ("dave" in msg) {
+ Assert.equal(cert.emailAddress, "dave@example.com");
+ } else {
+ Assert.equal(cert.emailAddress, "alice@example.com");
+ }
+ }
+ if (msg.sig_good) {
+ Assert.equal(r[sigIndex].status, 0);
+ } else {
+ Assert.notEqual(r[sigIndex].status, 0);
+ }
+ }
+
+ hdrIndex++;
+ }
+});
diff --git a/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js b/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js
new file mode 100644
index 0000000000..4ae1374f1a
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js
@@ -0,0 +1,274 @@
+/* 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/. */
+
+/**
+ * Tests to ensure signed and/or encrypted S/MIME messages are
+ * processed correctly, and the signature status is treated as good
+ * or bad as expected.
+ */
+
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { SmimeUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/smimeUtils.jsm"
+);
+const { EnigmailPersistentCrypto } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/persistentCrypto.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let gCertValidityResult = 0;
+
+/**
+ * @implements nsICertVerificationCallback
+ */
+class CertVerificationResultCallback {
+ constructor(callback) {
+ this.callback = callback;
+ }
+ verifyCertFinished(prErrorCode, verifiedChain, hasEVPolicy) {
+ gCertValidityResult = prErrorCode;
+ this.callback();
+ }
+}
+
+function testCertValidity(cert, date) {
+ let prom = new Promise((resolve, reject) => {
+ const certificateUsageEmailRecipient = 0x0020;
+ let result = new CertVerificationResultCallback(resolve);
+ let flags = Ci.nsIX509CertDB.FLAG_LOCAL_ONLY;
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ certdb.asyncVerifyCertAtTime(
+ cert,
+ certificateUsageEmailRecipient,
+ flags,
+ "Alice@example.com",
+ date,
+ result
+ );
+ });
+ return prom;
+}
+
+add_setup(async function () {
+ let messageInjection = new MessageInjection({ mode: "local" });
+ gInbox = messageInjection.getInboxFolder();
+ SmimeUtils.ensureNSS();
+
+ SmimeUtils.loadPEMCertificate(
+ do_get_file(smimeDataDirectory + "TestCA.pem"),
+ Ci.nsIX509Cert.CA_CERT
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Alice.p12"),
+ "nss"
+ );
+ SmimeUtils.loadCertificateAndKey(
+ do_get_file(smimeDataDirectory + "Bob.p12"),
+ "nss"
+ );
+});
+
+var gInbox;
+
+var smimeDataDirectory = "../../../data/smime/";
+
+let smimeHeaderSink = {
+ expectResults(maxLen) {
+ // dump("Restarting for next test\n");
+ this._deferred = PromiseUtils.defer();
+ this._expectedEvents = maxLen;
+ this.countReceived = 0;
+ this._results = [];
+ this.haveSignedBad = false;
+ this.haveEncryptionBad = false;
+ this.resultSig = null;
+ this.resultEnc = null;
+ this.resultSigFirst = undefined;
+ return this._deferred.promise;
+ },
+ signedStatus(aNestingLevel, aSignedStatus, aSignerCert) {
+ console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel);
+ // dump("Signed message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveSignedBad) {
+ // override with newer allowed
+ this.resultSig = {
+ type: "signed",
+ status: aSignedStatus,
+ certificate: aSignerCert,
+ };
+ if (aSignedStatus != 0) {
+ this.haveSignedBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = true;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) {
+ console.log(
+ "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel
+ );
+ // dump("Encrypted message\n");
+ Assert.equal(aNestingLevel, 1);
+ if (!this.haveEncryptionBad) {
+ // override with newer allowed
+ this.resultEnc = {
+ type: "encrypted",
+ status: aEncryptedStatus,
+ certificate: aRecipientCert,
+ };
+ if (aEncryptedStatus != 0) {
+ this.haveEncryptionBad = true;
+ }
+ if (this.resultSigFirst == undefined) {
+ this.resultSigFirst = false;
+ }
+ }
+ this.countReceived++;
+ this.checkFinished();
+ },
+ checkFinished() {
+ if (this.countReceived == this._expectedEvents) {
+ if (this.resultSigFirst) {
+ if (this.resultSig) {
+ this._results.push(this.resultSig);
+ }
+ if (this.resultEnc) {
+ this._results.push(this.resultEnc);
+ }
+ } else {
+ if (this.resultEnc) {
+ this._results.push(this.resultEnc);
+ }
+ if (this.resultSig) {
+ this._results.push(this.resultSig);
+ }
+ }
+ this._deferred.resolve(this._results);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]),
+};
+
+/**
+ * Note on FILENAMES taken from the NSS test suite:
+ * - env: CMS enveloped (encrypted)
+ * - dsig: CMS detached signature (with multipart MIME)
+ * - sig: CMS opaque signature (content embedded inside signature)
+ * - bad: message text does not match signature
+ * - mismatch: embedded content is different
+ *
+ * Control variables used for checking results:
+ * - env: If true, we expect a report to encryptionStatus() that message
+ * is encrypted.
+ */
+
+var gMessages = [{ filename: "alice.env.eml", enc: true }];
+
+var gDecFolder;
+
+add_task(async function copy_messages() {
+ for (let msg of gMessages) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+
+ MailServices.copy.copyFileMessage(
+ do_get_file(smimeDataDirectory + msg.filename),
+ gInbox,
+ null,
+ true,
+ 0,
+ "",
+ promiseCopyListener,
+ null
+ );
+
+ await promiseCopyListener.promise;
+ promiseCopyListener = null;
+ }
+ gInbox.server.rootFolder.createSubfolder("decrypted", null);
+ gDecFolder = gInbox.server.rootFolder.getChildNamed("decrypted");
+});
+
+add_task(async function check_smime_message() {
+ let hdrIndex = 0;
+
+ for (let msg of gMessages) {
+ console.log("checking " + msg.filename);
+
+ let numExpected = 1;
+
+ let eventsExpected = numExpected;
+
+ let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex);
+ let uri = hdr.folder.getUriForMsg(hdr);
+ let sinkPromise = smimeHeaderSink.expectResults(eventsExpected);
+
+ let conversion = apply_mime_conversion(uri, smimeHeaderSink);
+ await conversion.promise;
+
+ let contents = conversion._data;
+ // dump("contents: " + contents + "\n");
+
+ // Check that we're also using the display output.
+ Assert.ok(contents.includes("<html>"));
+
+ await sinkPromise;
+
+ let r = smimeHeaderSink._results;
+ Assert.equal(r.length, numExpected);
+
+ if (msg.enc) {
+ Assert.equal(r[0].type, "encrypted");
+ Assert.equal(r[0].status, 0);
+ Assert.equal(r[0].certificate, null);
+ }
+
+ await EnigmailPersistentCrypto.cryptMessage(
+ hdr,
+ gDecFolder.URI,
+ false,
+ null
+ );
+
+ eventsExpected = 0;
+
+ hdr = mailTestUtils.getMsgHdrN(gDecFolder, hdrIndex);
+ uri = hdr.folder.getUriForMsg(hdr);
+ sinkPromise = smimeHeaderSink.expectResults(eventsExpected);
+
+ conversion = apply_mime_conversion(uri, smimeHeaderSink);
+ await conversion.promise;
+
+ contents = conversion._data;
+ // dump("contents: " + contents + "\n");
+
+ // Check that we're also using the display output.
+ Assert.ok(contents.includes("<html>"));
+
+ // A message without S/MIME content didn't produce any events,
+ // so we must manually force this check.
+ smimeHeaderSink.checkFinished();
+ await sinkPromise;
+
+ // If the result length is 0, it wasn't decrypted.
+ Assert.equal(smimeHeaderSink._results.length, 0);
+
+ hdrIndex++;
+ }
+});
diff --git a/comm/mailnews/mime/test/unit/test_structured_headers.js b/comm/mailnews/mime/test/unit/test_structured_headers.js
new file mode 100644
index 0000000000..aedc70ac59
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_structured_headers.js
@@ -0,0 +1,249 @@
+/* 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/. */
+
+// This tests the msgIStructuredHeaders and msgIWritableStructuredHeaders
+// interfaces.
+
+// Verify that a specific XPCOM error code is thrown.
+function verifyError(block, errorCode) {
+ let caught = undefined;
+ try {
+ block();
+ } catch (actual) {
+ caught = actual.result;
+ }
+ Assert.equal(caught, errorCode);
+}
+
+var StructuredHeaders = CC(
+ "@mozilla.org/messenger/structuredheaders;1",
+ Ci.msgIWritableStructuredHeaders
+);
+
+add_task(async function check_addressing() {
+ let headers = new StructuredHeaders();
+ headers.setHeader("To", [{ name: "undisclosed-recipients", group: [] }]);
+ Assert.ok(Array.isArray(headers.getHeader("To")));
+ let flat = headers.getAddressingHeader("To", false);
+ Assert.ok(Array.isArray(flat));
+ Assert.equal(flat.length, 0);
+ let full = headers.getAddressingHeader("To", true);
+ Assert.ok(Array.isArray(full));
+ Assert.equal(full.length, 1);
+ Assert.equal(full[0].name, "undisclosed-recipients");
+ Assert.ok(Array.isArray(full[0].group));
+ Assert.equal(headers.getRawHeader("To"), "undisclosed-recipients: ;");
+
+ headers.setHeader("To", [{ name: "\u00D3", email: "test@foo.invalid" }]);
+ Assert.equal(
+ headers.getRawHeader("To"),
+ "=?UTF-8?B?w5M=?= <test@foo.invalid>"
+ );
+ headers.setAddressingHeader("To", [
+ { name: "Comma, Name", email: "test@foo.invalid" },
+ ]);
+ Assert.equal(headers.getRawHeader("To"), '"Comma, Name" <test@foo.invalid>');
+});
+
+add_task(async function check_custom_header() {
+ // Load an extension for our custom header.
+ let url = Services.io.newFileURI(do_get_file("custom_header.js")).spec;
+ let promise = new Promise((resolve, reject) => {
+ function observer(subject, topic, data) {
+ Assert.equal(topic, "xpcom-category-entry-added");
+ Assert.equal(data, "custom-mime-encoder");
+ resolve();
+ Services.obs.removeObserver(observer, "xpcom-category-entry-added");
+ }
+ Services.obs.addObserver(observer, "xpcom-category-entry-added");
+ });
+ Services.catMan.addCategoryEntry(
+ "custom-mime-encoder",
+ "X-Unusual",
+ url,
+ false,
+ true
+ );
+ // The category manager doesn't fire until a later timestep.
+ await promise;
+ let headers = new StructuredHeaders();
+ headers.setRawHeader("X-Unusual", "10");
+ Assert.equal(headers.getHeader("X-Unusual"), 16);
+ headers.setHeader("X-Unusual", 32);
+ Assert.equal(headers.getRawHeader("X-Unusual"), "20");
+});
+
+add_task(async function check_raw() {
+ let headers = new StructuredHeaders();
+ Assert.ok(!headers.hasHeader("Date"));
+ let day = new Date("2000-01-01T00:00:00Z");
+ headers.setHeader("Date", day);
+ Assert.ok(headers.hasHeader("Date"));
+ Assert.ok(headers.hasHeader("date"));
+ Assert.equal(headers.getHeader("Date"), day);
+ Assert.equal(headers.getHeader("date"), day);
+ verifyError(
+ () => headers.getUnstructuredHeader("Date"),
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ verifyError(
+ () => headers.getAddressingHeader("Date"),
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ // This is easier than trying to match the actual value for the Date header,
+ // since that depends on the current timezone.
+ Assert.equal(new Date(headers.getRawHeader("Date")).getTime(), day.getTime());
+
+ // Otherwise, the string values should work.
+ headers.setRawHeader("Custom-Date", "1 Jan 2000 00:00:00 +0000");
+ Assert.equal(
+ headers.getRawHeader("Custom-Date"),
+ "1 Jan 2000 00:00:00 +0000"
+ );
+ headers.deleteHeader("Custom-Date");
+
+ headers.setUnstructuredHeader("Content-Description", "A description!");
+ Assert.equal(headers.getHeader("Content-Description"), "A description!");
+ Assert.equal(
+ headers.getUnstructuredHeader("Content-Description"),
+ "A description!"
+ );
+ verifyError(
+ () => headers.getAddressingHeader("Content-Description"),
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ Assert.equal(headers.getRawHeader("Content-Description"), "A description!");
+
+ Assert.ok(!headers.hasHeader("Subject"));
+ Assert.ok(headers.getUnstructuredHeader("Subject") === null);
+ headers.setRawHeader("Subject", "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?=");
+ Assert.equal(
+ headers.getHeader("Subject"),
+ "\u79c1\u306f\u4ef6\u540d\u5348\u524d"
+ );
+ Assert.equal(
+ headers.getRawHeader("Subject"),
+ "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?="
+ );
+
+ // Multiple found headers
+ Assert.equal(headers.getHeader("Not-Found-Anywhere"), undefined);
+ Assert.notEqual(headers.getHeader("Not-Found-Anywhere"), "");
+ Assert.equal(headers.getRawHeader("Not-Found-Anywhere"), undefined);
+ headers.setHeader("Not-Found-Anywhere", 515);
+ Assert.equal(headers.getHeader("Not-Found-Anywhere"), 515);
+ headers.deleteHeader("not-found-anywhere");
+ Assert.equal(headers.getHeader("Not-Found-Anywhere"), undefined);
+
+ // Check the enumeration of header values.
+ headers.setHeader("unabashed-random-header", false);
+ let headerList = [
+ "Date",
+ "Content-Description",
+ "Subject",
+ "Unabashed-Random-Header",
+ ];
+ for (let value of headers.headerNames) {
+ Assert.equal(value.toLowerCase(), headerList.shift().toLowerCase());
+ }
+
+ // Check that copying works
+ let moreHeaders = new StructuredHeaders();
+ moreHeaders.addAllHeaders(headers);
+ for (let value of headers.headerNames) {
+ Assert.equal(moreHeaders.getHeader(value), headers.getHeader(value));
+ }
+ headers.deleteHeader("Date");
+ Assert.ok(moreHeaders.hasHeader("Date"));
+});
+
+add_task(async function check_nsIMimeHeaders() {
+ let headers = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ Assert.ok(headers instanceof Ci.msgIStructuredHeaders);
+ Assert.equal(false, headers instanceof Ci.msgIWritableStructuredHeaders);
+ headers.initialize(
+ mailTestUtils.loadFileToString(do_get_file("../../../data/draft1"))
+ );
+ Assert.equal(headers.getHeader("To").length, 1);
+ Assert.equal(headers.getHeader("To")[0].email, "bugmail@example.org");
+ Assert.equal(headers.getAddressingHeader("To").length, 1);
+ Assert.equal(headers.getHeader("Content-Type").type, "text/html");
+
+ let headerList = [
+ "X-Mozilla-Status",
+ "X-Mozilla-Status2",
+ "X-Mozilla-Keys",
+ "FCC",
+ "BCC",
+ "X-Identity-Key",
+ "Message-ID",
+ "Date",
+ "From",
+ "X-Mozilla-Draft-Info",
+ "User-Agent",
+ "MIME-Version",
+ "To",
+ "Subject",
+ "Content-Type",
+ "Content-Transfer-Encoding",
+ ];
+ for (let value of headers.headerNames) {
+ Assert.equal(value.toLowerCase(), headerList.shift().toLowerCase());
+ }
+});
+
+add_task(async function checkBuildMimeText() {
+ let headers = new StructuredHeaders();
+ headers.setHeader("To", [
+ { name: "François Smith", email: "user@☃.invalid" },
+ ]);
+ headers.setHeader("From", [{ name: "John Doe", email: "jdoe@test.invalid" }]);
+ headers.setHeader(
+ "Subject",
+ "A subject that spans a distance quite in " +
+ "excess of 80 characters so as to force an intermediary CRLF"
+ );
+ headers.setHeader(
+ "User-Agent",
+ "Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101 Thunderbird/40.0a1"
+ );
+ let mimeText =
+ "To: =?UTF-8?Q?Fran=C3=A7ois_Smith?= <user@☃.invalid>\r\n" +
+ "From: John Doe <jdoe@test.invalid>\r\n" +
+ "Subject: A subject that spans a distance quite in excess of 80 characters so\r\n" +
+ " as to force an intermediary CRLF\r\n" +
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101\r\n" +
+ " Thunderbird/40.0a1\r\n";
+ Assert.equal(headers.buildMimeText(), mimeText);
+
+ // Check the version used for the nsIMimeHeaders implementation. This requires
+ // initializing with a UTF-8 version.
+ let utf8Text = mimeText.replace("☃", "\xe2\x98\x83");
+ let mimeHeaders = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ mimeHeaders.initialize(utf8Text);
+ Assert.equal(mimeHeaders.getHeader("To")[0].email, "user@☃.invalid");
+ Assert.equal(mimeHeaders.buildMimeText(), mimeText);
+ Assert.equal(mimeHeaders.allHeaders, utf8Text);
+
+ // Check date header sanitization
+ headers = new StructuredHeaders();
+ headers.setHeader("Date", new Date("Fri, 6 Mar 2020 00:12:34 +0100"));
+ mimeText = "Date: Thu, 5 Mar 2020 23:12:00 +0000\r\n";
+ Assert.equal(headers.buildMimeText(true), mimeText);
+});
+
+/**
+ * Test that very long message id can be encoded without error.
+ */
+add_task(async function test_longMessageId() {
+ let msgId =
+ "<loqrvrxAUJXbUjUpqbrOJ8nHnJ49hmTREaUhehHZQv0AELQUM7ym6MUklPkt13aw4UD81bYIwO91pQL2OaeKMYVYD5hvZiRT2lSUmGtJkthgb3p5-y03p9bkxbnixgary7va1z0rv6hmd0yy69dm9exwga43h5k6266uwwchtjuxail7ipjhu6307yuft5bm186nu9vejf2joegwtq309cz9m-o3gwPZsvyB4qDpaAkxaj8iyh4OHc0kJsbQPQG8c5z6l3mmtwJuFHC4PxJnzAx9TyQzfnxhiXetQqFaNfvjNYetmNGMd4oq-sihw-d26z-bmdkvy47cloy2vwrnEYPKxtmjXtsmyFJGNxL7d1CeFIAOloSFAwccA6Onq6zPC9lfwWcAOFFje5XqkGVK2XNsUsFao5PR51WsOZStvoCzkqPuWB5PpJ791D9gzPXvGVa45ahuwgpmr1v8g1h5dalaytuxtpettthl506s7l4odqnkhufkvqkja56ulbd4ukgpbd88o3msjz3qk906pbfq6cahdecxoidplpbtsm-673718934717750999799265953521388769563044829819888815300763892678635939321303281062602679958225188.n050jeqcu1blxrm38i58q9dsws108c2m3xcc1tfmlgx8ya2wjyvzxyikgaaed3q6r@ZGCDPKIGJZGEPNVFFMXMTCMUFOPRMBFLIIPXSXECXKGNXBSDNPPHRBCXQRPCTUOCDDZVBEXYODLMFEQTUGBMHDJYUYus-575687677-2.673718934717750999799265953521388769563044829819888815300763892678635939321303281062602679958225188.invalid>";
+ let headers = new StructuredHeaders();
+ headers.setHeader("Message-ID", msgId);
+ Assert.equal(headers.getRawHeader("message-id"), msgId);
+});
diff --git a/comm/mailnews/mime/test/unit/test_text_attachment.js b/comm/mailnews/mime/test/unit/test_text_attachment.js
new file mode 100644
index 0000000000..bea9d5c31a
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/test_text_attachment.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+/**
+ * This test verifies that we don't display text attachments inline
+ * when mail.inline_attachments is false.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const TEXT_ATTACHMENT = "inline text attachment";
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+var msgGen = new MessageGenerator();
+var inbox;
+var messageInjection = new MessageInjection({ mode: "local" });
+var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
+
+add_setup(function () {
+ inbox = messageInjection.getInboxFolder();
+});
+
+add_task(async function test_message_attachments_no_inline() {
+ Services.prefs.setBoolPref("mail.inline_attachments", false);
+ Services.prefs.setBoolPref("mail.inline_attachments.text", true);
+ await test_message_attachments({
+ // text attachment
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "test.txt",
+ format: "",
+ },
+ ],
+ });
+});
+
+add_task(async function test_message_attachments_no_inline_text() {
+ Services.prefs.setBoolPref("mail.inline_attachments", true);
+ Services.prefs.setBoolPref("mail.inline_attachments.text", false);
+ await PromiseTestUtils.promiseDelay(100);
+ await test_message_attachments({
+ // text attachment
+ attachments: [
+ {
+ body: TEXT_ATTACHMENT,
+ filename: "test.txt",
+ format: "",
+ },
+ ],
+ });
+});
+
+async function test_message_attachments(info) {
+ let synMsg = msgGen.makeMessage(info);
+ let synSet = new SyntheticMessageSet([synMsg]);
+ await messageInjection.addSetsToFolders([inbox], [synSet]);
+
+ let msgURI = synSet.getMsgURI(0);
+ let msgService = MailServices.messageServiceFromURI(msgURI);
+
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ msgService.streamMessage(
+ msgURI,
+ streamListener,
+ msgWindow,
+ null,
+ true, // have them create the converter
+ // additional uri payload, note that "header=" is prepended automatically
+ "filter",
+ false
+ );
+
+ let data = await streamListener.promise;
+ // check that text attachment contents didn't end up inline.
+ Assert.ok(!data.includes(TEXT_ATTACHMENT));
+}
diff --git a/comm/mailnews/mime/test/unit/xpcshell.ini b/comm/mailnews/mime/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..4e1e665f91
--- /dev/null
+++ b/comm/mailnews/mime/test/unit/xpcshell.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+head = head_mime.js
+tail =
+support-files =
+ custom_header.js
+ ../../../../mail/test/browser/openpgp/data/keys/*
+ ../../../../mail/test/browser/openpgp/data/eml/*
+
+[test_EncodeMimePartIIStr_UTF8.js]
+[test_alternate_p7m_handling.js]
+[test_attachment_size.js]
+[test_badContentType.js]
+[test_bug493544.js]
+[test_handlerRegistration.js]
+[test_hidden_attachments.js]
+[test_jsmime_charset.js]
+[test_message_attachment.js]
+[test_mimeContentType.js]
+[test_mimeStreaming.js]
+[test_nsIMsgHeaderParser1.js]
+[test_nsIMsgHeaderParser2.js]
+[test_nsIMsgHeaderParser3.js]
+[test_nsIMsgHeaderParser4.js]
+[test_nsIMsgHeaderParser5.js]
+[test_openpgp_decrypt.js]
+[test_parser.js]
+[test_rfc822_body.js]
+[test_smime_decrypt.js]
+
+[test_smime_decrypt_allow_sha1.js]
+run-sequentially = Because it set's a pref that must not be active for other tests.
+
+[test_smime_perm_decrypt.js]
+[test_structured_headers.js]
+[test_text_attachment.js]
diff --git a/comm/mailnews/moz.build b/comm/mailnews/moz.build
new file mode 100644
index 0000000000..cf14ebc3d6
--- /dev/null
+++ b/comm/mailnews/moz.build
@@ -0,0 +1,90 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "addrbook",
+ "base",
+ "compose",
+ "db/gloda",
+ "db/mork",
+ "db/msgdb",
+ "export/modules",
+ "extensions",
+ "imap/public",
+ "imap/src",
+ "import/modules",
+ "import/public",
+ "import/src",
+ "intl",
+ "jsaccount",
+ "local/public",
+ "local/src",
+ "mime",
+ "news",
+ "search/public",
+ "search/src",
+]
+
+TEST_DIRS += [
+ "imap/test",
+ "import/test",
+ "local/test",
+ "search/test",
+]
+
+if CONFIG["MOZ_MAPI_SUPPORT"]:
+ DIRS += [
+ "mapi/mapiDll",
+ "mapi/mapihook",
+ ]
+ TEST_DIRS += ["mapi/test"]
+
+DIRS += [
+ "import/build",
+]
+
+DEFINES["OS_ARCH"] = CONFIG["OS_ARCH"]
+DEFINES["MOZ_WIDGET_TOOLKIT"] = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+TESTING_JS_MODULES.mailnews += [
+ "test/fakeserver/Auth.jsm",
+ "test/fakeserver/Binaryd.jsm",
+ "test/fakeserver/Imapd.jsm",
+ "test/fakeserver/Ldapd.jsm",
+ "test/fakeserver/Maild.jsm",
+ "test/fakeserver/Nntpd.jsm",
+ "test/fakeserver/Pop3d.jsm",
+ "test/fakeserver/Smtpd.jsm",
+ "test/resources/IMAPpump.jsm",
+ "test/resources/LocalAccountUtils.jsm",
+ "test/resources/MailTestUtils.jsm",
+ "test/resources/MessageGenerator.jsm",
+ "test/resources/MessageInjection.jsm",
+ "test/resources/NetworkTestUtils.jsm",
+ "test/resources/PromiseTestUtils.jsm",
+ "test/resources/smimeUtils.jsm",
+]
+
+if "comm" in CONFIG["MOZ_BUILD_APP"]:
+ test_harness_base = TEST_HARNESS_FILES.xpcshell.comm
+else:
+ test_harness_base = TEST_HARNESS_FILES.xpcshell
+
+test_harness_base.mailnews.data += [
+ "/comm/mailnews/test/data/**",
+]
+
+test_harness_base.mailnews.resources += [
+ "/comm/mailnews/test/resources/**",
+]
+
+JS_PREFERENCE_PP_FILES += [
+ "mailnews.js",
+]
+
+Library("mail")
+FINAL_LIBRARY = "xul"
diff --git a/comm/mailnews/moz.configure b/comm/mailnews/moz.configure
new file mode 100644
index 0000000000..268feed966
--- /dev/null
+++ b/comm/mailnews/moz.configure
@@ -0,0 +1,26 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# =========================================================
+# = MAPI support (Windows only)
+# =========================================================
+
+option("--disable-mapi", help="Disable MAPI support", when=target_is_windows)
+
+
+@depends_if("--enable-mapi", when=target_is_windows)
+def mapi_support(arg):
+ return True
+
+
+set_config("MOZ_MAPI_SUPPORT", mapi_support)
+set_define("MOZ_MAPI_SUPPORT", mapi_support)
+
+# =========================================================
+# = OpenPGP integration
+# =========================================================
+
+include("../third_party/openpgp.configure")
diff --git a/comm/mailnews/news/content/downloadheaders.js b/comm/mailnews/news/content/downloadheaders.js
new file mode 100644
index 0000000000..80d31935b8
--- /dev/null
+++ b/comm/mailnews/news/content/downloadheaders.js
@@ -0,0 +1,92 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var markreadElement = null;
+var numberElement = null;
+
+var nntpServer = null;
+var args = null;
+
+document.addEventListener("dialogaccept", OkButtonCallback);
+document.addEventListener("dialogcancel", CancelButtonCallback);
+
+function OnLoad() {
+ let newsBundle = document.getElementById("bundle_news");
+
+ if ("arguments" in window && window.arguments[0]) {
+ args = window.arguments[0].QueryInterface(Ci.nsINewsDownloadDialogArgs);
+ /* by default, act like the user hit cancel */
+ args.hitOK = false;
+ /* by default, act like the user did not select download all */
+ args.downloadAll = false;
+
+ nntpServer = MailServices.accounts
+ .getIncomingServer(args.serverKey)
+ .QueryInterface(Ci.nsINntpIncomingServer);
+
+ document.title = newsBundle.getString("downloadHeadersTitlePrefix");
+
+ let infotext = newsBundle.getFormattedString("downloadHeadersInfoText", [
+ args.articleCount,
+ ]);
+ setText("info", infotext);
+ let okButtonText = newsBundle.getString("okButtonText");
+ let okbutton = document.querySelector("dialog").getButton("accept");
+ okbutton.setAttribute("label", okButtonText);
+ okbutton.focus();
+ setText("newsgroupLabel", args.groupName);
+ }
+
+ numberElement = document.getElementById("number");
+ numberElement.value = nntpServer.maxArticles;
+
+ markreadElement = document.getElementById("markread");
+ markreadElement.checked = nntpServer.markOldRead;
+
+ setupDownloadUI(true);
+
+ return true;
+}
+
+function setText(id, value) {
+ let element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+
+ while (element.lastChild) {
+ element.lastChild.remove();
+ }
+ let textNode = document.createTextNode(value);
+ element.appendChild(textNode);
+}
+
+function OkButtonCallback() {
+ nntpServer.maxArticles = numberElement.value;
+ nntpServer.markOldRead = markreadElement.checked;
+
+ let radio = document.getElementById("all");
+ if (radio) {
+ args.downloadAll = radio.selected;
+ }
+
+ args.hitOK = true;
+}
+
+function CancelButtonCallback() {
+ args.hitOK = false;
+}
+
+function setupDownloadUI(enable) {
+ let checkbox = document.getElementById("markread");
+ let numberFld = document.getElementById("number");
+
+ checkbox.disabled = !enable;
+ numberFld.disabled = !enable;
+ numberFld.select();
+}
diff --git a/comm/mailnews/news/content/downloadheaders.xhtml b/comm/mailnews/news/content/downloadheaders.xhtml
new file mode 100644
index 0000000000..b212d5d288
--- /dev/null
+++ b/comm/mailnews/news/content/downloadheaders.xhtml
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/downloadheaders.dtd">
+
+<dialog
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="max-width: 27em; padding: 10px"
+ buttonpack="center"
+ lightweightthemes="true"
+ onload="OnLoad();"
+>
+ <stringbundle
+ id="bundle_news"
+ src="chrome://messenger/locale/news.properties"
+ />
+ <script src="chrome://messenger/content/downloadheaders.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <label
+ id="newsgroupLabel"
+ control="downloadGroup"
+ class="header"
+ style="width: 25em; max-width: 25em; margin-inline-start: 6px"
+ />
+ <description
+ style="width: 25em; max-width: 25em"
+ id="info"
+ control="downloadGroup"
+ />
+ <separator class="thin" />
+ <vbox class="indent">
+ <radiogroup id="downloadGroup">
+ <radio
+ id="all"
+ label="&all.label;"
+ accesskey="&all.accesskey;"
+ oncommand="setupDownloadUI(false);"
+ value="all"
+ />
+ <separator class="thin" />
+ <hbox align="center">
+ <radio
+ id="some"
+ selected="true"
+ label="&download.label;"
+ accesskey="&download.accesskey;"
+ oncommand="setupDownloadUI(true);"
+ aria-labelledby="some number headers"
+ value="some"
+ />
+ <html:input
+ id="number"
+ type="number"
+ min="1"
+ aria-labelledby="some number headers"
+ />
+ <label
+ id="headers"
+ control="number"
+ value="&headers.label;"
+ accesskey="&headers.accesskey;"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox class="indent" align="start">
+ <checkbox
+ id="markread"
+ label="&mark.label;"
+ accesskey="&mark.accesskey;"
+ />
+ </hbox>
+ </vbox>
+</dialog>
diff --git a/comm/mailnews/news/moz.build b/comm/mailnews/news/moz.build
new file mode 100644
index 0000000000..a49689ab64
--- /dev/null
+++ b/comm/mailnews/news/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/news/public/moz.build b/comm/mailnews/news/public/moz.build
new file mode 100644
index 0000000000..c6c6b27c6f
--- /dev/null
+++ b/comm/mailnews/news/public/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIMsgNewsFolder.idl",
+ "nsIMsgOfflineNewsState.idl",
+ "nsINewsDownloadDialogArgs.idl",
+ "nsINntpIncomingServer.idl",
+ "nsINNTPNewsgroupPost.idl",
+ "nsINntpService.idl",
+ "nsINntpUrl.idl",
+]
+
+XPIDL_MODULE = "msgnews"
+
+EXPORTS += []
diff --git a/comm/mailnews/news/public/nsIMsgNewsFolder.idl b/comm/mailnews/news/public/nsIMsgNewsFolder.idl
new file mode 100644
index 0000000000..683c566595
--- /dev/null
+++ b/comm/mailnews/news/public/nsIMsgNewsFolder.idl
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgFolder.idl"
+
+%{C++
+#include "nsTArray.h"
+%}
+
+interface nsIMsgWindow;
+interface nsINntpIncomingServer;
+
+[scriptable, uuid(9a12c3a5-9de5-4c57-ace3-d51802b525a9)]
+interface nsIMsgNewsFolder : nsISupports {
+ readonly attribute AString unicodeName;
+ /**|rawName| is an 8-bit string to represent the name of a newsgroup used by
+ * a news server. It's offered for the convenience of callers so that they
+ * don't have to convert |unicodeName| to the server-side name when
+ * communicating with a news server. It's US-ASCII except for some
+ * 'stand-alone' Chinese news servers that use GB2312 for newsgroup names
+ * violating RFC 1036. For those servers, it's GB2312. However, it can be any
+ * other single and multibyte encoding in principle. The encoding of this
+ * string is stored in |nsINntpIncomingServer| because that's a server-wide
+ * property.
+ **/
+ readonly attribute ACString rawName;
+ readonly attribute nsINntpIncomingServer nntpServer;
+ attribute boolean saveArticleOffline;
+
+ /**
+ * @name Authentication methods
+ * NNTP authentication is slightly wonky, due to edge cases that are not seen
+ * in other protocols. Authentication is not necessary; if authentication is
+ * used, it could be configured on a per-group basis or even require only a
+ * username and not a password.
+ *
+ * Since passwords could be per-group, it is necessary to refer to passwords
+ * using the methods on this interface and not nsIMsgIncomingServer. Passwords
+ * for the server as a whole are found via the root folder. If the server is
+ * configured to use single sign-on (the default), asking any group for its
+ * password will result in the server's password, otherwise, each group stores
+ * its password individually.
+ *
+ * Due to this setup, most of the password management functions on
+ * nsIMsgIncomingServer do not correctly work. The only one that would affect
+ * the passwords stored on folders correctly is forgetPassword; using any
+ * other on a news server would result in inconsistent state.
+ *
+ * Before requesting either the username or password for authentication, it is
+ * first necessary to call getAuthenticationCredentials. If the method returns
+ * true, then groupUsername and groupPassword are appropriately set up for
+ * necessary authentication; if not, then authentication must be stopped.
+ */
+ /// @{
+
+ /**
+ * Gets the authentication credentials, returning if the results are valid.
+ *
+ * If mustPrompt is true, then the user will always be asked for the
+ * credentials. Otherwise, if mayPrompt is true, then the user will be asked
+ * for credentials if there are no saved credentials. If mayPrompt is false,
+ * then no prompt will be shown, even if there are no saved credentials.
+ *
+ * If this method returns true, then groupUsername and groupPassword will
+ * contain non-empty results that could be used for authentication. If this
+ * method returns false, then the values of groupUsername and groupPassword
+ * will be cleared if they had previously been set. This could happen if
+ * mustPrompt were true and the user decided to cancel the authentication
+ * prompt.
+ *
+ * Note that this method will be executed synchronously; if an async prompt
+ * is wanted, it is the responsibility of the caller to manage it explicitly
+ * with nsIMsgAsyncPrompter.
+ */
+ bool getAuthenticationCredentials(in nsIMsgWindow aMsgWindow,
+ in bool mayPrompt, in bool mustPrompt);
+
+ /// The username that should be used for this group
+ attribute ACString groupUsername;
+
+ /// The password that should be used for this group
+ attribute ACString groupPassword;
+
+ /// Forgets saved authentication credentials permanently.
+ void forgetAuthenticationCredentials();
+ /// @}
+
+ void reorderGroup(in nsIMsgFolder aNewsgroupToMove, in nsIMsgFolder aRefNewsgroup);
+
+ nsIMsgFolder addNewsgroup(in AUTF8String newsgroupName, in ACString setStr);
+
+ void setReadSetFromStr(in ACString setStr);
+
+ /// returns the server's default charset.
+ readonly attribute ACString charset;
+
+ readonly attribute AUTF8String newsrcLine;
+ readonly attribute ACString optionLines;
+ readonly attribute ACString unsubscribedNewsgroupLines;
+ void SetNewsrcHasChanged(in boolean newsrcHasChanged);
+ void updateSummaryFromNNTPInfo(in long oldest, in long youngest, in long total);
+ void removeMessage(in nsMsgKey key);
+ void removeMessages(in Array<nsMsgKey> keys);
+ void cancelComplete();
+ void cancelFailed();
+
+ ACString getMessageIdForKey(in nsMsgKey key);
+
+ void getNextNMessages(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Begin loading a message into the folder's offline store.
+ * @param key - the key of the message being loaded (the folder must
+ * already have the message in it's DB)
+ */
+ void notifyDownloadBegin(in nsMsgKey key);
+
+ /**
+ * Feed the next line of a message into the folder, to be invoked multiple
+ * times between notifyDownloadBegin() and notifyDownloadEnd().
+ *
+ * @param line - a single line of data to append to the message, including
+ * end-of-line terminator.
+ */
+ void notifyDownloadedLine(in ACString line);
+
+ /**
+ * Finish loading a message into the folder. If an error occurs, the
+ * folder hears about it via this function, and should abort the message.
+ *
+ * @param status - NS_OK if the operation was completed, an error code
+ * otherwise.
+ */
+ void notifyDownloadEnd(in nsresult status);
+
+ void notifyFinishedDownloadinghdrs();
+
+ /**
+ * Requests that a message be canceled.
+ *
+ * Note that, before sending the news cancel, this method will check to make
+ * sure that the user has proper permission to cancel the message.
+ *
+ * @param aMsgHdr The header of the message to be canceled.
+ * @param aMsgWindow The standard message window object, for error dialogs.
+ */
+ void cancelMessage(in nsIMsgDBHdr aMsgHdr, in nsIMsgWindow aMsgWindow);
+};
diff --git a/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl b/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl
new file mode 100644
index 0000000000..eef1e88db6
--- /dev/null
+++ b/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*
+ * offline news message state. Interface for old MSG_OfflineNewsArtState
+ */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(921AC210-96B5-11d2-B7EB-00805F05FFA5)]
+interface nsIMsgOfflineNewsState : nsISupports {
+
+ /* outputBuffer is actually
+ * a buffer to dump data into, but we normally pass it NET_Socket_Buffer,
+ * which is constant. The implementation should only allocate a new
+ * buffer if *outputBuffer is NULL.
+ */
+ long Process(out string outputBuffer, in long bufferSize);
+ long Interrupt();
+};
diff --git a/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl b/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl
new file mode 100644
index 0000000000..95f3cbb2bf
--- /dev/null
+++ b/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl
@@ -0,0 +1,51 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/* This object represents the stream of data which will be sent to an
+ NNTP server. You basically set up all the RFC850 required headers, etc,
+ then pass it to something that reads off the nsIInputStream interface.
+*/
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(9979a2cb-a4e6-45e6-bfeb-b08e704c5a2b)]
+interface nsINNTPNewsgroupPost : nsISupports {
+
+ /* from RFC850 */
+ /* section 2.1 - required headers */
+ attribute string relayVersion;
+ attribute string postingVersion;
+ attribute string from;
+ attribute string date;
+
+ void AddNewsgroup(in string newsgroupName);
+ readonly attribute string newsgroups;
+
+ attribute string subject;
+ attribute string path;
+
+ /* Section 2.2 - optional headers */
+ attribute string replyTo;
+ attribute string sender;
+ attribute string followupTo;
+ attribute string dateReceived;
+ attribute string expires;
+
+ readonly attribute string references;
+
+ attribute string control;
+ attribute string distribution;
+ attribute string organization;
+
+ /* the message itself */
+ attribute string body;
+
+ /* is this a control message? */
+ readonly attribute boolean isControl;
+
+ attribute nsIFile postMessageFile;
+};
diff --git a/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl b/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl
new file mode 100644
index 0000000000..8641a56034
--- /dev/null
+++ b/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(3634327c-392b-4686-adf5-576e6cef9196)]
+interface nsINewsDownloadDialogArgs: nsISupports {
+ attribute AString groupName;
+ attribute long articleCount;
+ attribute string serverKey;
+ attribute boolean hitOK;
+ attribute boolean downloadAll;
+};
+
+%{ C++
+#define DOWNLOAD_HEADERS_URL "chrome://messenger/content/downloadheaders.xhtml"
+%}
diff --git a/comm/mailnews/news/public/nsINntpIncomingServer.idl b/comm/mailnews/news/public/nsINntpIncomingServer.idl
new file mode 100644
index 0000000000..548b62da24
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpIncomingServer.idl
@@ -0,0 +1,136 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIMsgNewsFolder;
+interface nsIChannel;
+interface nsIURI;
+interface nsIMsgWindow;
+
+[scriptable, uuid(077620ed-c6c4-4d4d-bed5-4d041f924002)]
+interface nsINntpIncomingServer : nsISupports {
+ /* the on-disk path to the newsrc file for this server */
+ attribute nsIFile newsrcFilePath;
+
+ /* the newsrc root path (the directories all the newsrc files live) */
+ attribute nsIFile newsrcRootPath;
+
+ /* ask the user before downloading more than maxArticles? */
+ attribute boolean notifyOn;
+
+ /* the max articles to download */
+ attribute long maxArticles;
+
+ /* when we don't download all, do we mark the rest read? */
+ attribute boolean markOldRead;
+
+ /* abbreviate the newsgroup names in the folder pane? */
+ attribute boolean abbreviate;
+
+ /* do we use a single login per server or do we login per group */
+ attribute boolean singleSignon;
+
+ /** the server charset and it may be needed to display newsgroup folder
+ * names correctly
+ **/
+ attribute ACString charset;
+
+ /* the server keeps track of all the newsgroups we are subscribed to */
+ void addNewsgroup(in AString name);
+ void removeNewsgroup(in AString name);
+
+ void writeNewsrcFile();
+
+ attribute boolean newsrcHasChanged;
+
+ /**
+ * The maximum number of connections to make to the server.
+ *
+ * This preference (internally max_cached_connections) controls how many
+ * connections we can make. A negative connection count is treated as only
+ * one connection, while 0 (the default) loads the default number of
+ * connections, presently 2.
+ */
+ attribute long maximumConnectionsNumber;
+
+ void displaySubscribedGroup(in nsIMsgNewsFolder msgFolder,
+ in long firstMessage, in long lastMessage,
+ in long totalMessages);
+
+
+ /**
+ * Get a new NNTP channel to run the URI.
+ *
+ * If the server has used up all of its connections, this will place the URI
+ * in the queue to be run when one is freed.
+ *
+ * @param uri The URI to run.
+ * @param window The standard message window object.
+ */
+ nsIChannel getNntpChannel(in nsIURI uri, in nsIMsgWindow window);
+ /**
+ * Enqueues a URI to be run when we have a free connection.
+ *
+ * If there is one already free, it will be immediately started.
+ *
+ * @param uri The URI to run.
+ * @param window The standard message window object.
+ * @param consumer A listener for the response data.
+ */
+ void loadNewsUrl(in nsIURI uri, in nsIMsgWindow window,
+ in nsISupports consumer);
+
+ /**
+ * Returns whether or not the server has subscribed to the given newsgroup.
+ *
+ * Note that the name here is intended to be escaped; however, since `%' is
+ * not a legal newsgroup name, it is possibly safe to pass in an unescaped
+ * newsgroup name.
+ */
+ boolean containsNewsgroup(in AUTF8String escapedName);
+
+ void subscribeToNewsgroup(in AUTF8String name);
+
+ /* used for the subscribe dialog.
+ name is encoded in |charset| (attribute declared above) */
+ void addNewsgroupToList(in string name);
+
+ attribute boolean supportsExtensions;
+ void addExtension(in string extension);
+ boolean queryExtension(in string extension);
+
+ attribute boolean postingAllowed;
+ attribute boolean pushAuth;
+ attribute unsigned long lastUpdatedTime;
+
+ void addPropertyForGet(in string name, in string value);
+ string queryPropertyForGet(in string name);
+
+ void addSearchableGroup(in AString name);
+ boolean querySearchableGroup(in AString name);
+
+ void addSearchableHeader(in string headerName);
+ boolean querySearchableHeader(in string headerName);
+
+ /**
+ * Returns the folder corresponding to the given group.
+ *
+ * Note that this name is expected to be unescaped.
+ * @note If the group does not exist, a bogus news folder will be returned.
+ * DO NOT call this method unless you are sure that the newsgroup
+ * is subscribed to (e.g., by containsNewsgroup)
+ */
+ nsIMsgNewsFolder findGroup(in AUTF8String name);
+
+ readonly attribute AUTF8String firstGroupNeedingExtraInfo;
+ void setGroupNeedsExtraInfo(in AUTF8String name, in boolean needsExtraInfo);
+
+ void groupNotFound(in nsIMsgWindow window, in AString group,
+ in boolean opening);
+
+ void setPrettyNameForGroup(in AString name, in AString prettyName);
+};
diff --git a/comm/mailnews/news/public/nsINntpService.idl b/comm/mailnews/news/public/nsINntpService.idl
new file mode 100644
index 0000000000..ec1b0b3efe
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpService.idl
@@ -0,0 +1,50 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIUrlListener.idl"
+#include "nsINntpIncomingServer.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIMsgWindow;
+interface nsIMsgFolder;
+interface nsICacheStorage;
+
+[scriptable, uuid(dc5cadb0-966c-4ef1-a4c8-cc1e48d1ac61)]
+interface nsINntpService : nsISupports {
+
+ /* newsgroupsList is a comma separated list of newsgroups, which may be
+ * in news://host/group or group form
+ * "news://host/group1,news://host/group2" or "group1,group2"
+ *
+ * newsgroupsHeaderVal is a comma separated list of groups in the group form
+ * "group1,group2"
+ *
+ * newshostHeaderVal is a single hostname.
+ * "host"
+ */
+ void generateNewsHeaderValsForPosting(in ACString newsgroupsList, out string newsgroupsHeaderVal, out string newshostHeaderVal);
+
+ nsIURI postMessage(in nsIFile aFileToPost, in string newsgroupNames, in string aAccountKey, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ nsIURI getNewNews(in nsINntpIncomingServer nntpServer, in AUTF8String uri, in boolean getOld, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ nsIURI cancelMessage(in AUTF8String cancelURL, in AUTF8String messageURI, in nsISupports aConsumer, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ void getListOfGroupsOnServer(in nsINntpIncomingServer nntpServer, in nsIMsgWindow aMsgWindow, in boolean getOnlyNew);
+
+ nsIURI fetchMessage(in nsIMsgFolder newsFolder, in nsMsgKey key, in nsIMsgWindow aMsgWindow, in nsISupports aConsumer, in nsIUrlListener aUrlListener);
+
+ void downloadNewsgroupsForOffline(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+ /**
+ * can handle news-message:// and news://
+ */
+ void decomposeNewsURI(in AUTF8String uri, out nsIMsgFolder folder, out nsMsgKey key);
+
+ // handle to the cache session used by news....
+ readonly attribute nsICacheStorage cacheStorage;
+};
diff --git a/comm/mailnews/news/public/nsINntpUrl.idl b/comm/mailnews/news/public/nsINntpUrl.idl
new file mode 100644
index 0000000000..33b82b6687
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpUrl.idl
@@ -0,0 +1,102 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsINNTPNewsgroupPost;
+
+typedef long nsNewsAction;
+
+/**
+ * Represents specific attributes to a URL for news usage.
+ *
+ * Note that the urls represented by this interface can be one of five schemes:
+ * [s]news, nntp[s], or news-message. Any URI that is valid under RFC 5538 will
+ * be accepted. However, it is possible for some queries to be invalid. There
+ * are also a few important things to note:
+ *
+ * - Missing authorities in [s]news: URIs cause nsIMsgMailNewsUrl::server and
+ * nsIMsgMessageUrl::folder to be null.
+ * - nsIMsgMailNewsUrl::server and nsIMsgMessageUrl::folder will be null if the
+ * specified server does not actually exist. In addition, the folder is null
+ * if the group is not currently subscribed on that server.
+ * - Although news-message URIs are parsable, there is no protocol handler
+ * associated with this url. To run these, you should convert these to the
+ * corresponding [s]news or nntp URL, and set the original one in
+ * nsIMsgMessageUrl::uri and ::originalSpec.
+ * - A URI that results in an ActionUnknown will not be run.
+ * - Cancel URIs require the original spec to also be set, so it can find both
+ * the message ID and the group/key combination.
+ * * Some actions require either a group or a message id. Since actions can be
+ * set after the fact, these conditions are not verified.
+ */
+[scriptable, uuid(ef920ca3-9c46-48b8-9fa3-cb430d3681ea)]
+interface nsINntpUrl : nsISupports {
+ /// For ActionPostArticle URIs, the message to be posted.
+ attribute nsINNTPNewsgroupPost messageToPost;
+
+ /**
+ * The action that this URL will take when run.
+ *
+ * Most actions can be automatically determined from the URL spec as follows:
+ *
+ * 1. The query string is searched for the appropriate action.
+ *
+ * 2. A non-empty message ID or key is found (sets ActionFetchArticle).
+ *
+ * 3. A non-empty group is found (ActionGetNewNews or ActionListGroups).
+ */
+ attribute nsNewsAction newsAction;
+
+ /// For ActionGetNewNews URIs, whether or not to get older messages.
+ attribute boolean getOldMessages;
+
+ /**
+ * The group portion of the URI, if one is present.
+ *
+ * This group name is fully unescaped; if you need to construct news URLs with
+ * this value, be sure to escape it first.
+ */
+ readonly attribute ACString group;
+
+ /// The message ID portion of the URI, if one is present
+ readonly attribute ACString messageID;
+
+ /// The message key portion of the URI or nsMsgKey_None if not present
+ readonly attribute nsMsgKey key;
+
+ /// returns the server's default charset.
+ readonly attribute ACString charset;
+
+ /// The action of this news URI could not be determined
+ const nsNewsAction ActionUnknown = 0;
+ /// Fetch the contents of an article
+ const nsNewsAction ActionFetchArticle = 1;
+ /// Fetch the part of an article (requires ?part=)
+ const nsNewsAction ActionFetchPart = 2;
+ /// Save the contents of an article to disk
+ const nsNewsAction ActionSaveMessageToDisk = 3;
+ /// Cancel the article (requires ?cancel)
+ const nsNewsAction ActionCancelArticle = 4;
+ /// Post an article
+ const nsNewsAction ActionPostArticle = 5;
+ /// List the non-expired ids in the newsgroup (requires ?list-ids)
+ const nsNewsAction ActionListIds = 6;
+ /// Do an online newsgroup search (requires ?search)
+ const nsNewsAction ActionSearch = 7;
+ /// Retrieve new messages from the server
+ const nsNewsAction ActionGetNewNews = 8;
+ /// List groups for subscribe
+ const nsNewsAction ActionListGroups = 9;
+ /// List new groups for subscribe (requires ?new-groups)
+ const nsNewsAction ActionListNewGroups = 10;
+
+ /// Constant for the default NNTP over ssl port number
+ const int32_t DEFAULT_NNTP_PORT = 119;
+
+ /// Constant for the default NNTP over ssl port number
+ const int32_t DEFAULT_NNTPS_PORT = 563;
+};
diff --git a/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..88716e01bc
--- /dev/null
+++ b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
@@ -0,0 +1,134 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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 EXPORTED_SYMBOLS = ["NewsAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kACR = Ci.nsIAutoCompleteResult;
+var kSupportedTypes = new Set(["addr_newsgroups", "addr_followup"]);
+
+function NewsAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+}
+
+NewsAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ searchString: null,
+ searchResult: kACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt(aIndex) {
+ return "subscribed-news-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]),
+};
+
+function NewsAutoCompleteSearch() {}
+
+NewsAutoCompleteSearch.prototype = {
+ // For component registration
+ classDescription: "Newsgroup Autocomplete",
+
+ cachedAccountKey: "",
+ cachedServer: null,
+
+ /**
+ * Find the newsgroup server associated with the given accountKey.
+ *
+ * @param accountKey The key of the account.
+ * @returns The incoming news server (or null if one does not exist).
+ */
+ _findServer(accountKey) {
+ let account = MailServices.accounts.getAccount(accountKey);
+
+ if (account.incomingServer.type == "nntp") {
+ return account.incomingServer;
+ }
+ return null;
+ },
+
+ // nsIAutoCompleteSearch
+ startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+ let params = aSearchParam ? JSON.parse(aSearchParam) : {};
+ let result = new NewsAutoCompleteResult(aSearchString);
+ if (
+ !("type" in params) ||
+ !("accountKey" in params) ||
+ !kSupportedTypes.has(params.type)
+ ) {
+ result.searchResult = kACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ if ("accountKey" in params && params.accountKey != this.cachedAccountKey) {
+ this.cachedAccountKey = params.accountKey;
+ this.cachedServer = this._findServer(params.accountKey);
+ }
+
+ if (this.cachedServer) {
+ for (let curr of this.cachedServer.rootFolder.subFolders) {
+ if (curr.prettyName.includes(aSearchString)) {
+ result._searchResults.push({
+ value: curr.prettyName,
+ comment: this.cachedServer.prettyName,
+ });
+ }
+ }
+ }
+
+ if (result.matchCount) {
+ result.searchResult = kACR.RESULT_SUCCESS;
+ // If the user does not select anything, use the first entry:
+ result.defaultIndex = 0;
+ }
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch() {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+};
diff --git a/comm/mailnews/news/src/NewsDownloader.sys.mjs b/comm/mailnews/news/src/NewsDownloader.sys.mjs
new file mode 100644
index 0000000000..be94dfb96e
--- /dev/null
+++ b/comm/mailnews/news/src/NewsDownloader.sys.mjs
@@ -0,0 +1,158 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+/**
+ * Download articles in all subscribed newsgroups for offline use.
+ */
+export class NewsDownloader {
+ _logger = NntpUtils.logger;
+
+ /**
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ */
+ constructor(msgWindow, urlListener) {
+ this._msgWindow = msgWindow;
+ this._urlListener = urlListener;
+
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ }
+
+ /**
+ * Actually start the download process.
+ */
+ async start() {
+ this._logger.debug("Start downloading articles for offline use");
+ let servers = MailServices.accounts.allServers.filter(
+ x => x.type == "nntp"
+ );
+ // Download all servers concurrently.
+ await Promise.all(
+ servers.map(async server => {
+ let folders = server.rootFolder.descendants;
+ for (let folder of folders) {
+ if (folder.flags & Ci.nsMsgFolderFlags.Offline) {
+ // Download newsgroups set for offline use in a server one by one.
+ await this._downloadFolder(folder);
+ }
+ }
+ })
+ );
+
+ this._urlListener.OnStopRunningUrl(null, Cr.NS_OK);
+
+ this._logger.debug("Finished downloading articles for offline use");
+ this._msgWindow.statusFeedback.showStatusString("");
+ }
+
+ /**
+ * Download articles in a newsgroup one by one.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ */
+ async _downloadFolder(folder) {
+ this._logger.debug(`Start downloading ${folder.URI}`);
+
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline = true;
+ let keysToDownload = await this._getKeysToDownload(folder);
+
+ let i = 0;
+ let total = keysToDownload.size;
+ for (let key of keysToDownload) {
+ await new Promise(resolve => {
+ MailServices.nntp.fetchMessage(folder, key, this._msgWindow, null, {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl() {
+ resolve();
+ },
+ });
+ });
+ this._msgWindow.statusFeedback.showStatusString(
+ this._bundle.formatStringFromName("downloadingArticlesForOffline", [
+ ++i,
+ total,
+ folder.prettyName,
+ ])
+ );
+ }
+
+ folder.saveArticleOffline = false;
+ }
+
+ /**
+ * Use a search session to find articles that match the download settings
+ * and we don't already have.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ * @returns {Set<number>}
+ */
+ async _getKeysToDownload(folder) {
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let termValue = searchSession.createTerm().value;
+
+ let downloadSettings = folder.downloadSettings;
+ if (downloadSettings.downloadUnreadOnly) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Read;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+ }
+ if (downloadSettings.downloadByDate) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.AgeInDays;
+ termValue.age = downloadSettings.ageLimitOfMsgsToDownload;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.AgeInDays,
+ Ci.nsMsgSearchOp.IsLessThan,
+ termValue,
+ true,
+ null
+ );
+ }
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Offline;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+
+ let keysToDownload = new Set();
+ await new Promise(resolve => {
+ searchSession.registerListener(
+ {
+ onSearchHit(hdr, folder) {
+ if (!(hdr.flags & Ci.nsMsgMessageFlags.Offline)) {
+ // Only need to download articles we don't already have.
+ keysToDownload.add(hdr.messageKey);
+ }
+ },
+ onSearchDone: status => {
+ resolve();
+ },
+ },
+ Ci.nsIMsgSearchSession.allNotifications
+ );
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.localNews, folder);
+ searchSession.search(this._msgWindow);
+ });
+
+ return keysToDownload;
+ }
+}
diff --git a/comm/mailnews/news/src/NntpChannel.jsm b/comm/mailnews/news/src/NntpChannel.jsm
new file mode 100644
index 0000000000..4e20fca7bc
--- /dev/null
+++ b/comm/mailnews/news/src/NntpChannel.jsm
@@ -0,0 +1,402 @@
+/* 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 = ["NntpChannel"];
+
+const { MailChannel } = ChromeUtils.importESModule(
+ "resource:///modules/MailChannel.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A channel to interact with NNTP server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ * @implements {nsICacheEntryOpenCallback}
+ */
+class NntpChannel extends MailChannel {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMailChannel",
+ "nsIChannel",
+ "nsIRequest",
+ "nsICacheEntryOpenCallback",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+ _status = Cr.NS_OK;
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} [loadInfo] - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ super();
+ this._server = lazy.NntpUtils.findServer(uri.asciiHost);
+ if (!this._server) {
+ this._server = MailServices.accounts
+ .createIncomingServer("", uri.asciiHost, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ this._server.port = uri.port;
+ }
+
+ if (uri.port < 1) {
+ // Ensure the uri has a port so that memory cache works.
+ uri = uri.mutate().setPort(this._server.port).finalize();
+ }
+
+ // Two forms of the uri:
+ // - news://news.mozilla.org:119/mailman.30.1608649442.1056.accessibility%40lists.mozilla.org?group=mozilla.accessibility&key=378
+ // - news://news.mozilla.org:119/id@mozilla.org
+ let url = new URL(uri.spec);
+ this._groupName = url.searchParams.get("group");
+ if (this._groupName) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(
+ decodeURIComponent(url.searchParams.get("group"))
+ );
+ this._articleNumber = url.searchParams.get("key");
+ } else {
+ this._messageId = decodeURIComponent(url.pathname.slice(1));
+ if (!this._messageId.includes("@")) {
+ this._groupName = this._messageId;
+ this._messageId = null;
+ }
+ }
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.URI = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ this.loadInfo = loadInfo || {
+ QueryInterface: ChromeUtils.generateQI(["nsILoadInfo"]),
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ internalContentPolicy: Ci.nsIContentPolicy.TYPE_OTHER,
+ };
+ this.contentLength = 0;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {string}
+ */
+ get name() {
+ return this.URI?.spec;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {boolean}
+ */
+ isPending() {
+ return !!this._pending;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {nsresult}
+ */
+ get status() {
+ return this._status;
+ }
+
+ /**
+ * @see nsICacheEntryOpenCallback
+ */
+ onCacheEntryAvailable(entry, isNew, status) {
+ if (!Components.isSuccessCode(status)) {
+ // If memory cache doesn't work, read from the server.
+ this._readFromServer();
+ return;
+ }
+
+ if (isNew) {
+ if (Services.io.offline) {
+ this._status = Cr.NS_ERROR_OFFLINE;
+ return;
+ }
+ // It's a new entry, needs to read from the server.
+ let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ let outStream = entry.openOutputStream(0, -1);
+ // When the tee stream receives data from the server, it writes to both
+ // the original listener and outStream (memory cache).
+ tee.init(this._listener, outStream, null);
+ this._listener = tee;
+ this._cacheEntry = entry;
+ this._readFromServer();
+ return;
+ }
+
+ // It's an old entry, read from the memory cache.
+ this._readFromCacheStream(entry.openInputStream(0));
+ }
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ /**
+ * @see nsIChannel
+ */
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ `${this.constructor.name}.open not implemented`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug("asyncOpen", this.URI.spec);
+ let url = new URL(this.URI.spec);
+ this._listener = listener;
+ if (url.searchParams.has("list-ids")) {
+ // Triggered by newsError.js.
+ this._removeExpired(decodeURIComponent(url.pathname.slice(1)));
+ return;
+ }
+
+ if (this._groupName && !this._server.containsNewsgroup(this._groupName)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ let result = Services.prompt.confirm(
+ win,
+ null,
+ bundle.formatStringFromName("autoSubscribeText", [this._groupName])
+ );
+ if (!result) {
+ return;
+ }
+ this._server.subscribeToNewsgroup(this._groupName);
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ }
+
+ if (this._groupName && !this._articleNumber && !this._messageId) {
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ return;
+ }
+
+ if (url.searchParams.has("part")) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ this._listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ listener,
+ this
+ );
+ }
+ try {
+ // Attempt to get the message from the offline storage.
+ try {
+ if (this._readFromOfflineStorage()) {
+ return;
+ }
+ } catch (e) {
+ this._logger.warn(e);
+ }
+
+ let uri = this.URI;
+ if (url.search) {
+ // A full news url may look like
+ // news://<host>:119/<Msg-ID>?group=<name>&key=<key>&header=quotebody.
+ // Remove any query strings to keep the cache key stable.
+ uri = uri.mutate().setQuery("").finalize();
+ }
+
+ // Check if a memory cache is available for the current URI.
+ MailServices.nntp.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ this
+ );
+ } catch (e) {
+ this._logger.warn(e);
+ this._readFromServer();
+ }
+ if (this._status == Cr.NS_ERROR_OFFLINE) {
+ throw new Components.Exception(
+ "The requested action could not be completed in the offline state",
+ Cr.NS_ERROR_OFFLINE
+ );
+ }
+ }
+
+ /**
+ * Try to read the article from the offline storage.
+ *
+ * @returns {boolean} True if successfully read from the offline storage.
+ */
+ _readFromOfflineStorage() {
+ if (!this._newsFolder) {
+ return false;
+ }
+ if (!this._newsFolder.hasMsgOffline(this._articleNumber)) {
+ return false;
+ }
+ let hdr = this._newsFolder.GetMessageHeader(this._articleNumber);
+ let stream = this._newsFolder.getLocalMsgStream(hdr);
+ this._readFromCacheStream(stream);
+ return true;
+ }
+
+ /**
+ * Read the article from the a stream.
+ *
+ * @param {nsIInputStream} cacheStream - The input stream to read.
+ */
+ _readFromCacheStream(cacheStream) {
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this.contentLength = 0;
+ this._contentType = "";
+ pump.init(cacheStream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: () => {
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ },
+ onStopRequest: (request, status) => {
+ this._listener.onStopRequest(this, status);
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ this._pending = false;
+ },
+ onDataAvailable: (request, stream, offset, count) => {
+ this.contentLength += count;
+ this._listener.onDataAvailable(this, stream, offset, count);
+ try {
+ if (!cacheStream.available()) {
+ cacheStream.close();
+ }
+ } catch (e) {}
+ },
+ });
+ }
+
+ /**
+ * Retrieve the article from the server.
+ */
+ _readFromServer() {
+ this._logger.debug("Read from server");
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ if (this._newsFolder) {
+ this._newsFolder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline =
+ this._newsFolder.shouldStoreMsgOffline(this._articleNumber);
+ }
+
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ client.channel = this;
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ if (this._messageId) {
+ client.getArticleByMessageId(this._messageId);
+ } else {
+ client.getArticleByArticleNumber(
+ this._groupName,
+ this._articleNumber
+ );
+ }
+ };
+
+ client.onData = data => {
+ this.contentLength += data.length;
+ outputStream.write(data, data.length);
+ this._listener.onDataAvailable(this, inputStream, 0, data.length);
+ };
+
+ client.onDone = status => {
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ if (status != Cr.NS_OK) {
+ // Prevent marking a message as read.
+ this.URI.errorCode = status;
+ // Remove the invalid cache.
+ this._cacheEntry?.asyncDoom(null);
+ }
+ this._listener.onStopRequest(this, status);
+ this._newsFolder?.msgDatabase.commit(
+ Ci.nsMsgDBCommitType.kSessionCommit
+ );
+ this._pending = false;
+ };
+ });
+ }
+
+ /**
+ * Fetch all the article keys on the server, then remove expired keys from the
+ * local folder.
+ *
+ * @param {string} groupName - The group to check.
+ */
+ _removeExpired(groupName) {
+ this._logger.debug("_removeExpired", groupName);
+ let newsFolder = this._server.findGroup(groupName);
+ let allKeys = new Set(newsFolder.msgDatabase.listAllKeys());
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ client.listgroup(groupName);
+ };
+
+ client.onData = data => {
+ allKeys.delete(+data);
+ };
+
+ client.onDone = status => {
+ newsFolder.removeMessages([...allKeys]);
+ this._listener.onStopRequest(this, status);
+ this._pending = false;
+ };
+ });
+ }
+}
diff --git a/comm/mailnews/news/src/NntpClient.jsm b/comm/mailnews/news/src/NntpClient.jsm
new file mode 100644
index 0000000000..dfbd9fde10
--- /dev/null
+++ b/comm/mailnews/news/src/NntpClient.jsm
@@ -0,0 +1,981 @@
+/* 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 = ["NntpClient"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { LineReader } = ChromeUtils.import("resource:///modules/LineReader.jsm");
+var { NntpNewsGroup } = ChromeUtils.import(
+ "resource:///modules/NntpNewsGroup.jsm"
+);
+
+// Server response code.
+const AUTH_ACCEPTED = 281;
+const AUTH_PASSWORD_REQUIRED = 381;
+const AUTH_REQUIRED = 480;
+const AUTH_FAILED = 481;
+const SERVICE_UNAVAILABLE = 502;
+const NOT_SUPPORTED = 503;
+const XPAT_OK = 221;
+
+const NNTP_ERROR_MESSAGE = -304;
+
+/**
+ * A structure to represent a response received from the server. A response can
+ * be a single status line of a multi-line data block.
+ *
+ * @typedef {object} NntpResponse
+ * @property {number} status - The status code of the response.
+ * @property {string} statusText - The status line of the response excluding the
+ * status code.
+ * @property {string} data - The part of a multi-line data block excluding the
+ * status line.
+ */
+
+/**
+ * A class to interact with NNTP server.
+ */
+class NntpClient {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {string} uri - The server uri.
+ */
+ constructor(server) {
+ this._server = server;
+ this._lineReader = new LineReader();
+
+ this._reset();
+ this._logger = console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ });
+ }
+
+ /**
+ * @type {NntpAuthenticator} - An authentication helper.
+ */
+ get _authenticator() {
+ if (!this._nntpAuthenticator) {
+ var { NntpAuthenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+ );
+ this._nntpAuthenticator = new NntpAuthenticator(this._server);
+ }
+ return this._nntpAuthenticator;
+ }
+
+ /**
+ * Reset some internal states to be safely reused.
+ */
+ _reset() {
+ this.onOpen = () => {};
+ this.onError = () => {};
+ this.onData = () => {};
+ this.onDone = () => {};
+
+ this.runningUri = null;
+ this.urlListener = null;
+ this._msgWindow = null;
+ this._newsFolder = null;
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ this._done = false;
+ if (this._socket?.readyState == "open") {
+ // Reuse the connection.
+ this.onOpen();
+ } else {
+ // Start a new connection.
+ this._authenticated = false;
+ let hostname = this._server.hostName.toLowerCase();
+ let useSecureTransport = this._server.isSecure;
+ this._logger.debug(
+ `Connecting to ${useSecureTransport ? "snews" : "news"}://${hostname}:${
+ this._server.port
+ }`
+ );
+ this._socket = new TCPSocket(hostname, this._server.port, {
+ binaryType: "arraybuffer",
+ useSecureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTING_TO);
+ }
+ }
+
+ /**
+ * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when
+ * the current request is finished.
+ *
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIMsgMailNewsUrl} [runningUrl] - The url to run, if provided.
+ * @returns {nsIMsgMailNewsUrl}
+ */
+ startRunningUrl(urlListener, msgWindow, runningUri) {
+ this.urlListener = urlListener;
+ this._msgWindow = msgWindow;
+ this.runningUri = runningUri;
+ if (!this.runningUri) {
+ this.runningUri = Services.io
+ .newURI(`news://${this._server.hostName}:${this._server.port}`)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ }
+ if (msgWindow) {
+ this.runningUri.msgWindow = msgWindow;
+ }
+ this.urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+ this.runningUri.SetUrlState(true, Cr.NS_OK);
+ return this.runningUri;
+ }
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this._inReadingMode = false;
+ this._currentGroupName = null;
+ this._nextAction = ({ status }) => {
+ if ([200, 201].includes(status)) {
+ this._nextAction = null;
+ this.onOpen();
+ } else {
+ this.quit(Cr.NS_ERROR_FAILURE);
+ }
+ };
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTED_TO);
+ };
+
+ /**
+ * The data event handler.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = event => {
+ let stringPayload = CommonUtils.arrayBufferToByteString(
+ new Uint8Array(event.data)
+ );
+ this._logger.debug(`S: ${stringPayload}`);
+
+ let res = this._parse(stringPayload);
+ switch (res.status) {
+ case AUTH_REQUIRED:
+ this._currentGroupName = null;
+ this._actionAuthUser();
+ return;
+ case SERVICE_UNAVAILABLE:
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ default:
+ if (
+ res.status != AUTH_FAILED &&
+ res.status >= 400 &&
+ res.status < 500
+ ) {
+ if (this._messageId || this._articleNumber) {
+ let uri = `about:newserror?r=${res.statusText}`;
+
+ if (this._messageId) {
+ uri += `&m=${encodeURIComponent(this._messageId)}`;
+ } else {
+ let msgId = this._newsFolder?.getMessageIdForKey(
+ this._articleNumber
+ );
+ if (msgId) {
+ uri += `&m=${encodeURIComponent(msgId)}`;
+ }
+ uri += `&k=${this._articleNumber}`;
+ }
+ if (this._newsFolder) {
+ uri += `&f=${this._newsFolder.URI}`;
+ }
+ // Store the uri to display. The registered uriListener will get
+ // notified when we stop running the uri, and can act on this data.
+ this.runningUri.seeOtherURI = uri;
+ }
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ }
+ }
+
+ try {
+ this._nextAction?.(res);
+ } catch (e) {
+ this._logger.error(`Failed to process server response ${res}.`, e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = event => {
+ this._logger.error(event, event.name, event.message, event.errorCode);
+ let errorName;
+ let uri;
+ switch (event.errorCode) {
+ case Cr.NS_ERROR_UNKNOWN_HOST:
+ case Cr.NS_ERROR_UNKNOWN_PROXY_HOST:
+ errorName = "unknownHostError";
+ uri = "about:neterror?e=dnsNotFound";
+ break;
+ case Cr.NS_ERROR_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=connectionFailure";
+ break;
+ case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=proxyConnectFailure";
+ break;
+ case Cr.NS_ERROR_NET_TIMEOUT:
+ errorName = "netTimeoutError";
+ uri = "about:neterror?e=netTimeout";
+ break;
+ case Cr.NS_ERROR_NET_RESET:
+ errorName = "netResetError";
+ uri = "about:neterror?e=netReset";
+ break;
+ case Cr.NS_ERROR_NET_INTERRUPT:
+ errorName = "netInterruptError";
+ uri = "about:neterror?e=netInterrupt";
+ break;
+ }
+ if (errorName && uri) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let errorMessage = bundle.formatStringFromName(errorName, [
+ this._server.hostName,
+ ]);
+ MailServices.mailSession.alertUser(errorMessage, this.runningUri);
+
+ // If we were going to display an article, instead show an error page.
+ this.runningUri.seeOtherURI = uri;
+ }
+
+ this._msgWindow?.statusFeedback?.showStatusString("");
+ this.quit(event.errorCode);
+ };
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed.");
+ };
+
+ /**
+ * Parse the server response.
+ *
+ * @param {string} str - Response received from the server.
+ * @returns {NntpResponse}
+ */
+ _parse(str) {
+ if (this._lineReader.processingMultiLineResponse) {
+ // When processing multi-line response, no parsing should happen.
+ return { data: str };
+ }
+ let matches = /^(\d{3}) (.+)\r\n([^]*)/.exec(str);
+ if (matches) {
+ let [, status, statusText, data] = matches;
+ return { status: Number(status), statusText, data };
+ }
+ return { data: str };
+ }
+
+ /**
+ * Send a command to the socket.
+ *
+ * @param {string} str - The command string to send.
+ * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str.
+ */
+ _sendCommand(str, suppressLogging) {
+ if (this._socket.readyState != "open") {
+ if (str != "QUIT") {
+ this._logger.warn(
+ `Failed to send "${str}" because socket state is ${this._socket.readyState}`
+ );
+ }
+ return;
+ }
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this._logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ // Do not suppress for non-release builds, so that debugging auth problems
+ // is easier.
+ this._logger.debug(`C: ${str}`);
+ }
+ this.send(str + "\r\n");
+ }
+
+ /**
+ * Send a string to the socket.
+ *
+ * @param {string} str - The string to send.
+ */
+ send(str) {
+ this._socket.send(CommonUtils.byteStringToArrayBuffer(str).buffer);
+ }
+
+ /**
+ * Send a LIST or NEWGROUPS command to get groups in the current server.
+ *
+ * @param {boolean} getOnlyNew - List only new groups.
+ */
+ getListOfGroups(getOnlyNew) {
+ if (!getOnlyNew) {
+ this._actionModeReader(this._actionList);
+ } else {
+ this._actionModeReader(this._actionNewgroups);
+ }
+ this.urlListener = this._server.QueryInterface(Ci.nsIUrlListener);
+ }
+
+ /**
+ * Get new articles.
+ *
+ * @param {string} groupName - The group to get new articles.
+ * @param {boolean} getOld - Get old articles as well.
+ */
+ getNewNews(groupName, getOld) {
+ this._currentGroupName = null;
+ this._newsFolder = this._getNewsFolder(groupName);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._newsGroup.getOldMessages = getOld;
+ this._nextGroupName = this._newsFolder.rawName;
+ this.runningUri.updatingFolder = true;
+ this._firstGroupCommand = this._actionXOver;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by group name and article number.
+ *
+ * @param {string} groupName - The group name.
+ * @param {integer} articleNumber - The article number.
+ */
+ getArticleByArticleNumber(groupName, articleNumber) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(groupName);
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._articleNumber = articleNumber;
+ this._messageId = "";
+ this._firstGroupCommand = this._actionArticle;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by the message id.
+ *
+ * @param {string} messageId - The message id.
+ */
+ getArticleByMessageId(messageId) {
+ this._messageId = `<${messageId}>`;
+ this._articleNumber = 0;
+ this._actionModeReader(this._actionArticle);
+ }
+
+ /**
+ * Send a `Control: cancel <msg-id>` message to cancel an article, not every
+ * server supports it, see rfc5537.
+ *
+ * @param {string} groupName - The group name.
+ */
+ cancelArticle(groupName) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._firstGroupCommand = this.post;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Send a `XPAT <header> <message-id> <pattern>` message, not every server
+ * supports it, see rfc2980.
+ *
+ * @param {string} groupName - The group name.
+ * @param {string[]} xpatLines - An array of xpat lines to send.
+ */
+ search(groupName, xpatLines) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._xpatLines = xpatLines;
+ this._firstGroupCommand = this._actionXPat;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Load a news uri directly, see rfc5538 about supported news uri.
+ *
+ * @param {string} uri - The news uri to load.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIStreamListener} streamListener - The listener for the request.
+ */
+ loadNewsUrl(uri, msgWindow, streamListener) {
+ this._logger.debug(`Loading ${uri}`);
+ let url = new URL(uri);
+ let path = url.pathname.slice(1);
+ let action;
+ if (path == "*") {
+ action = () => this.getListOfGroups();
+ } else if (path.includes("@")) {
+ action = () => this.getArticleByMessageId(path);
+ } else {
+ this._newsFolder = this._getNewsFolder(path);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._nextGroupName = this._newsFolder.rawName;
+ action = () => this._actionModeReader(this._actionGroup);
+ }
+ if (!action) {
+ return;
+ }
+ this._msgWindow = msgWindow;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ this.onOpen = () => {
+ streamListener.onStartRequest(null, Cr.NS_OK);
+ action();
+ };
+ this.onData = data => {
+ outputStream.write(data, data.length);
+ streamListener.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ this.onDone = status => {
+ streamListener.onStopRequest(null, status);
+ };
+ }
+
+ /**
+ * Send LISTGROUP request to the server.
+ *
+ * @param {string} groupName - The group to request.
+ */
+ listgroup(groupName) {
+ this._actionModeReader(() => {
+ this._nextAction = this._actionListgroupResponse;
+ this._sendCommand(`LISTGROUP ${groupName}`);
+ });
+ }
+
+ /**
+ * Send `POST` request to the server.
+ */
+ post() {
+ let action = () => {
+ this._nextAction = this._actionHandlePost;
+ this._sendCommand("POST");
+ };
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = action;
+ this._actionAuthUser();
+ } else {
+ action();
+ }
+ }
+
+ /**
+ * Send `QUIT` request to the server.
+ */
+ quit(status = Cr.NS_OK) {
+ this._sendCommand("QUIT");
+ this._nextAction = this.close;
+ this.close();
+ this._actionDone(status);
+ }
+
+ /**
+ * Close the socket.
+ */
+ close() {
+ this._socket.close();
+ }
+
+ /**
+ * Get the news folder corresponding to a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {nsIMsgNewsFolder}
+ */
+ _getNewsFolder(groupName) {
+ return this._server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ /**
+ * Given a UTF-8 group name, return the underlying group name used by the server.
+ *
+ * @param {string} groupName - The UTF-8 group name.
+ * @returns {BinaryString} - The group name that can be sent to the server.
+ */
+ _getNextGroupName(groupName) {
+ return this._getNewsFolder(groupName).rawName;
+ }
+
+ /**
+ * Send `MODE READER` request to the server.
+ */
+ _actionModeReader(nextAction) {
+ if (this._inReadingMode) {
+ nextAction();
+ } else {
+ this._currentAction = () => {
+ this._inReadingMode = false;
+ this._actionModeReader(nextAction);
+ };
+ this._sendCommand("MODE READER");
+ this._inReadingMode = true;
+ this._nextAction = () => {
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = nextAction;
+ this._actionAuthUser();
+ } else {
+ nextAction();
+ }
+ };
+ }
+ }
+
+ /**
+ * Send `LIST` request to the server.
+ */
+ _actionList = () => {
+ this._sendCommand("LIST");
+ this._currentAction = this._actionList;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `NEWGROUPS` request to the server.
+ * @see rfc3977#section-7.3
+ */
+ _actionNewgroups = () => {
+ const days = Services.prefs.getIntPref("news.newgroups_for_num_days", 180);
+ const dateTime = new Date(Date.now() - 86400000 * days)
+ .toISOString()
+ .replace(
+ /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*/,
+ "$1$2$3 $4$5$6"
+ );
+ this._sendCommand("NEWGROUPS " + dateTime + " GMT");
+ this._currentAction = this._actionNewgroups;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `GROUP` request to the server.
+ */
+ _actionGroup = () => {
+ this._firstGroupCommand = this._firstGroupCommand || this._actionXOver;
+ if (this._nextGroupName == this._currentGroupName) {
+ this._firstGroupCommand();
+ } else {
+ this._sendCommand(`GROUP ${this._nextGroupName}`);
+ this._currentAction = this._actionGroup;
+ this._currentGroupName = this._nextGroupName;
+ this._nextAction = this._actionGroupResponse;
+ }
+ };
+
+ /**
+ * Handle GROUP response.
+ *
+ * @param {NntpResponse} res - GROUP response received from the server.
+ */
+ _actionGroupResponse = res => {
+ if (res.status == 411) {
+ this._server.groupNotFound(null, this._currentGroupName, true);
+ return;
+ }
+ this._firstGroupCommand(res);
+ };
+
+ /**
+ * Consume the status line of LISTGROUP response.
+ */
+ _actionListgroupResponse = res => {
+ this._nextAction = this._actionListgroupDataResponse;
+ if (res.data) {
+ this._actionListgroupDataResponse(res);
+ }
+ };
+
+ /**
+ * Consume the multi-line data of LISTGROUP response.
+ *
+ * @param {NntpResponse} res - The server response.
+ */
+ _actionListgroupDataResponse = ({ data }) => {
+ this._lineReader.read(
+ data,
+ line => {
+ this.onData(line);
+ },
+ () => {
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Send `XOVER` request to the server.
+ */
+ _actionXOver = res => {
+ let [count, low, high] = res.statusText.split(" ");
+ this._newsFolder.updateSummaryFromNNTPInfo(low, high, count);
+ let [start, end] = this._newsGroup.getArticlesRangeToFetch(
+ this._msgWindow,
+ Number(low),
+ Number(high)
+ );
+ if (start && end) {
+ this._startArticle = start;
+ this._endArticle = end;
+ this._nextAction = this._actionXOverResponse;
+ this._sendCommand(`XOVER ${start}-${end}`);
+ } else {
+ this._actionDone();
+ }
+ };
+
+ /**
+ * A transient action to consume the status line of XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXOverResponse(res) {
+ if (res.status == 224) {
+ this._nextAction = this._actionReadXOver;
+ this._newsGroup.addKnownArticles(this._startArticle, this._endArticle);
+ this._actionReadXOver(res);
+ } else {
+ // Somehow XOVER is not supported by the server, fallback to use HEAD to
+ // fetch one by one.
+ this._actionHead();
+ }
+ }
+
+ /**
+ * Handle XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadXOver({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXOverLine(line);
+ },
+ () => {
+ // Fetch extra headers used by filters, but not returned in XOVER response.
+ this._xhdrFields = this._newsGroup.getXHdrFields();
+ this._actionXHdr();
+ }
+ );
+ }
+
+ /**
+ * Send `XHDR` request to the server.
+ */
+ _actionXHdr = () => {
+ this._curXHdrHeader = this._xhdrFields.shift();
+ if (this._curXHdrHeader) {
+ this._nextAction = this._actionXHdrResponse;
+ this._sendCommand(
+ `XHDR ${this._curXHdrHeader} ${this._startArticle}-${this._endArticle}`
+ );
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle XHDR response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXHdrResponse({ status, data }) {
+ if (status == NOT_SUPPORTED) {
+ // Fallback to HEAD request.
+ this._actionHead();
+ return;
+ }
+
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXHdrLine(this._curXHdrHeader, line);
+ },
+ this._actionXHdr
+ );
+ }
+
+ /**
+ * Send `HEAD` request to the server.
+ */
+ _actionHead = () => {
+ if (this._startArticle <= this._endArticle) {
+ this._nextAction = this._actionReadHead;
+ this._sendCommand(`HEAD ${this._startArticle}`);
+ this._newsGroup.initHdr(this._startArticle);
+ this._startArticle++;
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle HEAD response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadHead({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processHeadLine(line);
+ },
+ () => {
+ this._newsGroup.initHdr(-1);
+ this._actionHead();
+ }
+ );
+ }
+
+ /**
+ * Send `ARTICLE` request to the server.
+ * @see {@link https://www.rfc-editor.org/rfc/rfc3977#section-6.2.1|RFC 3977 §6.2.1}
+ */
+ _actionArticle = () => {
+ this._sendCommand(`ARTICLE ${this._articleNumber || this._messageId}`);
+ this._nextAction = this._actionArticleResponse;
+ this._newsFolder?.notifyDownloadBegin(this._articleNumber);
+ this._downloadingToFolder = true;
+ };
+
+ /**
+ * Handle `ARTICLE` response.
+ *
+ * @param {NntpResponse} res - ARTICLE response received from the server.
+ */
+ _actionArticleResponse = ({ data }) => {
+ let lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ this._lineReader.read(
+ data,
+ line => {
+ // NewsFolder will decide whether to save it to the offline storage.
+ this._newsFolder?.notifyDownloadedLine(
+ line.slice(0, -2) + lineSeparator
+ );
+ this.onData(line);
+ },
+ () => {
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_OK);
+ this._downloadingToFolder = false;
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Handle multi-line data blocks response, e.g. ARTICLE/LIST response. Emit
+ * each line through onData.
+ *
+ * @param {NntpResponse} res - Response received from the server.
+ */
+ _actionReadData({ data }) {
+ this._lineReader.read(data, this.onData, this._actionDone);
+ }
+
+ /**
+ * Handle POST response.
+ *
+ * @param {NntpResponse} res - POST response received from the server.
+ */
+ _actionHandlePost({ status, statusText }) {
+ if (status == 340) {
+ this.onReadyToPost();
+ } else if (status == 240) {
+ this._actionDone();
+ } else {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ }
+ }
+
+ /**
+ * Send `AUTHINFO user <name>` to the server.
+ *
+ * @param {boolean} [forcePrompt=false] - Whether to force showing an auth prompt.
+ */
+ _actionAuthUser(forcePrompt = false) {
+ if (!this._newsFolder) {
+ this._newsFolder = this._server.rootFolder.QueryInterface(
+ Ci.nsIMsgNewsFolder
+ );
+ }
+ if (!this._newsFolder.groupUsername) {
+ let gotPassword = this._newsFolder.getAuthenticationCredentials(
+ this._msgWindow,
+ true,
+ forcePrompt
+ );
+ if (!gotPassword) {
+ this._actionDone(Cr.NS_ERROR_ABORT);
+ return;
+ }
+ }
+ this._sendCommand(`AUTHINFO user ${this._newsFolder.groupUsername}`, true);
+ this._nextAction = this._actionAuthResult;
+ this._authenticator.username = this._newsFolder.groupUsername;
+ }
+
+ /**
+ * Send `AUTHINFO pass <password>` to the server.
+ */
+ _actionAuthPassword() {
+ this._sendCommand(`AUTHINFO pass ${this._newsFolder.groupPassword}`, true);
+ this._nextAction = this._actionAuthResult;
+ }
+
+ /**
+ * Decide the next step according to the auth response.
+ *
+ * @param {NntpResponse} res - Auth response received from the server.
+ */
+ _actionAuthResult({ status }) {
+ switch (status) {
+ case AUTH_ACCEPTED:
+ this._authenticated = true;
+ this._currentAction?.();
+ return;
+ case AUTH_PASSWORD_REQUIRED:
+ this._actionAuthPassword();
+ return;
+ case AUTH_FAILED:
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this._actionDone();
+ return;
+ }
+ if (action == 2) {
+ // 'New password' button pressed.
+ this._newsFolder.forgetAuthenticationCredentials();
+ }
+ // Retry.
+ this._actionAuthUser();
+ }
+ }
+
+ /**
+ * Send `XPAT <header> <message-id> <pattern>` to the server.
+ */
+ _actionXPat = () => {
+ let xptLine = this._xpatLines.shift();
+ if (!xptLine) {
+ this._actionDone();
+ return;
+ }
+ this._sendCommand(xptLine);
+ this._nextAction = this._actionXPatResponse;
+ };
+
+ /**
+ * Handle XPAT response.
+ *
+ * @param {NntpResponse} res - XPAT response received from the server.
+ */
+ _actionXPatResponse({ status, statusText, data }) {
+ if (status && status != XPAT_OK) {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ return;
+ }
+ this._lineReader.read(data, this.onData, this._actionXPat);
+ }
+
+ /**
+ * Show network status in the status bar.
+ *
+ * @param {number} status - See NS_NET_STATUS_* in nsISocketTransport.idl.
+ */
+ _showNetworkStatus(status) {
+ let statusMessage = Services.strings.formatStatusMessage(
+ status,
+ this._server.hostName
+ );
+ this._msgWindow?.statusFeedback?.showStatusString(statusMessage);
+ }
+
+ /**
+ * Show an error prompt.
+ *
+ * @param {number} errorId - An error name corresponds to an entry of
+ * news.properties.
+ * @param {string} serverErrorMsg - Error message returned by the server.
+ */
+ _actionError(errorId, serverErrorMsg) {
+ this._logger.error(`Got an error id=${errorId}`);
+ let msgWindow = this._msgWindow;
+
+ if (!msgWindow) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let errorMsg = bundle.GetStringFromID(errorId);
+ if (serverErrorMsg) {
+ errorMsg += " " + serverErrorMsg;
+ }
+ Services.prompt.alert(msgWindow?.domWindow, null, errorMsg);
+
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+
+ /**
+ * Close the connection and do necessary cleanup.
+ */
+ _actionDone = (status = Cr.NS_OK) => {
+ if (this._done) {
+ return;
+ }
+ if (this._downloadingToFolder) {
+ // If we're in the middle of sending a message to the folder, make sure
+ // the folder knows we're aborting.
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_ERROR_FAILURE);
+ this._downloadingToFolder = false;
+ }
+ this._done = true;
+ this._logger.debug(`Done with status=${status}`);
+ this.onDone(status);
+ this._newsGroup?.cleanUp();
+ this._newsFolder?.OnStopRunningUrl?.(this.runningUri, status);
+ this.urlListener?.OnStopRunningUrl(this.runningUri, status);
+ this.runningUri.SetUrlState(false, status);
+ this._reset();
+ this.onIdle?.();
+ };
+}
diff --git a/comm/mailnews/news/src/NntpIncomingServer.jsm b/comm/mailnews/news/src/NntpIncomingServer.jsm
new file mode 100644
index 0000000000..3497cbae77
--- /dev/null
+++ b/comm/mailnews/news/src/NntpIncomingServer.jsm
@@ -0,0 +1,624 @@
+/* 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 = ["NntpIncomingServer"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MsgIncomingServer } = ChromeUtils.import(
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CommonUtils: "resource://services-common/utils.sys.mjs",
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpClient: "resource:///modules/NntpClient.jsm",
+});
+
+/**
+ * A class to represent a NNTP server.
+ *
+ * @implements {nsINntpIncomingServer}
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsISubscribableServer}
+ * @implements {nsITreeView}
+ * @implements {nsIUrlListener}
+ */
+class NntpIncomingServer extends MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsINntpIncomingServer",
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ "nsISubscribableServer",
+ "nsITreeView",
+ "nsIUrlListener",
+ ]);
+
+ constructor() {
+ super();
+
+ this._subscribed = new Set();
+ this._groups = [];
+
+ // @type {NntpClient[]} - An array of connections can be used.
+ this._idleConnections = [];
+ // @type {NntpClient[]} - An array of connections in use.
+ this._busyConnections = [];
+ // @type {Function[]} - An array of Promise.resolve functions.
+ this._connectionWaitingQueue = [];
+
+ Services.obs.addObserver(this, "profile-before-change");
+ // Update newsrc every 5 minutes.
+ this._newsrcTimer = lazy.setInterval(() => this.writeNewsrcFile(), 300000);
+
+ // nsIMsgIncomingServer attributes.
+ this.localStoreType = "news";
+ this.localDatabaseType = "news";
+ this.canSearchMessages = true;
+ this.canCompactFoldersOnServer = false;
+ this.sortOrder = 500000000;
+
+ Object.defineProperty(this, "defaultCopiesAndFoldersPrefsToServer", {
+ // No Draft/Sent folder on news servers, will point to "Local Folders".
+ get: () => false,
+ });
+ Object.defineProperty(this, "canCreateFoldersOnServer", {
+ // No folder creation on news servers.
+ get: () => false,
+ });
+ Object.defineProperty(this, "canFileMessagesOnServer", {
+ get: () => false,
+ });
+
+ // nsISubscribableServer attributes.
+ this.supportsSubscribeSearch = true;
+
+ // nsINntpIncomingServer attributes.
+ this.newsrcHasChanged = false;
+
+ // nsINntpIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Bool", "notifyOn", "notify.on"],
+ ["Bool", "markOldRead", "mark_old_read"],
+ ["Bool", "abbreviate", "abbreviate"],
+ ["Bool", "pushAuth", "always_authenticate"],
+ ["Bool", "singleSignon"],
+ ["Int", "maxArticles", "max_articles"],
+ ]);
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ lazy.clearInterval(this._newsrcTimer);
+ this.writeNewsrcFile();
+ }
+ }
+
+ /**
+ * Most of nsISubscribableServer interfaces are delegated to
+ * this._subscribable.
+ */
+ get _subscribable() {
+ if (!this._subscribableServer) {
+ this._subscribableServer = Cc[
+ "@mozilla.org/messenger/subscribableserver;1"
+ ].createInstance(Ci.nsISubscribableServer);
+ this._subscribableServer.setIncomingServer(this);
+ }
+ return this._subscribableServer;
+ }
+
+ /** @see nsISubscribableServer */
+ get folderView() {
+ return this._subscribable.folderView;
+ }
+
+ get subscribeListener() {
+ return this._subscribable.subscribeListener;
+ }
+
+ set subscribeListener(value) {
+ this._subscribable.subscribeListener = value;
+ }
+
+ subscribeCleanup() {
+ this._subscribableServer = null;
+ }
+
+ startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._startPopulating(msgWindow, forceToServer, getOnlyNew);
+ }
+
+ stopPopulating(msgWindow) {
+ this._subscribable.stopPopulating(msgWindow);
+ if (!this._hostInfoLoaded) {
+ this._saveHostInfo();
+ }
+ this.updateSubscribed();
+ }
+
+ addTo(name, addAsSubscribed, subscribale, changeIfExists) {
+ try {
+ this._subscribable.addTo(
+ name,
+ addAsSubscribed,
+ subscribale,
+ changeIfExists
+ );
+ this._groups.push(name);
+ } catch (e) {
+ // Group names with double dot, like alt.binaries.sounds..mp3.zappa are
+ // not working. Bug 1788572.
+ console.error(`Failed to add group ${name}. ${e}`);
+ }
+ }
+
+ subscribe(name) {
+ this.subscribeToNewsgroup(name);
+ }
+
+ unsubscribe(name) {
+ this.rootMsgFolder.propagateDelete(
+ this.rootMsgFolder.getChildNamed(name),
+ true // delete storage
+ );
+ this.newsrcHasChanged = true;
+ }
+
+ commitSubscribeChanges() {
+ this.newsrcHasChanged = true;
+ this.writeNewsrcFile();
+ }
+
+ setAsSubscribed(path) {
+ this._tmpSubscribed.add(path);
+ this._subscribable.setAsSubscribed(path);
+ }
+
+ updateSubscribed() {
+ this._tmpSubscribed = new Set();
+ this._subscribed.forEach(path => this.setAsSubscribed(path));
+ }
+
+ setState(path, state) {
+ let changed = this._subscribable.setState(path, state);
+ if (changed) {
+ if (state) {
+ this._tmpSubscribed.add(path);
+ } else {
+ this._tmpSubscribed.delete(path);
+ }
+ }
+ return changed;
+ }
+
+ hasChildren(path) {
+ return this._subscribable.hasChildren(path);
+ }
+
+ isSubscribed(path) {
+ return this._subscribable.isSubscribed(path);
+ }
+
+ isSubscribable(path) {
+ return this._subscribable.isSubscribable(path);
+ }
+
+ setSearchValue(value) {
+ this._tree?.beginUpdateBatch();
+ this._tree?.rowCountChanged(0, -this._searchResult.length);
+
+ let terms = value.toLowerCase().split(" ");
+ this._searchResult = this._groups
+ .filter(name => {
+ name = name.toLowerCase();
+ // The group name should contain all the search terms.
+ return terms.every(term => name.includes(term));
+ })
+ .sort();
+
+ this._tree?.rowCountChanged(0, this._searchResult.length);
+ this._tree?.endUpdateBatch();
+ }
+
+ getLeafName(path) {
+ return this._subscribable.getLeafName(path);
+ }
+
+ getFirstChildURI(path) {
+ return this._subscribable.getFirstChildURI(path);
+ }
+
+ getChildURIs(path) {
+ return this._subscribable.getChildURIs(path);
+ }
+
+ /** @see nsITreeView */
+ get rowCount() {
+ return this._searchResult.length;
+ }
+
+ isContainer(index) {
+ return false;
+ }
+
+ getCellProperties(row, col) {
+ if (
+ col.id == "subscribedColumn2" &&
+ this._tmpSubscribed.has(this._searchResult[row])
+ ) {
+ return "subscribed-true";
+ }
+ if (col.id == "nameColumn2") {
+ // Show the news folder icon in the search view.
+ return "serverType-nntp";
+ }
+ return "";
+ }
+
+ getCellValue(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ getCellText(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ setTree(tree) {
+ this._tree = tree;
+ }
+
+ /** @see nsIUrlListener */
+ OnStartRunningUrl() {}
+
+ OnStopRunningUrl() {
+ this.stopPopulating(this._msgWindow);
+ }
+
+ /** @see nsIMsgIncomingServer */
+ get serverRequiresPasswordForBiff() {
+ return false;
+ }
+
+ get filterScope() {
+ return Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ get searchScope() {
+ return Services.io.offline
+ ? Ci.nsMsgSearchScope.localNewsBody
+ : Ci.nsMsgSearchScope.news;
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ const OFFLINE_SUPPORT_LEVEL_EXTENDED = 20;
+ let level = this.getIntValue("offline_support_level");
+ return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? level
+ : OFFLINE_SUPPORT_LEVEL_EXTENDED;
+ }
+
+ performExpand(msgWindow) {
+ if (!Services.prefs.getBoolPref("news.update_unread_on_expand", false)) {
+ return;
+ }
+
+ for (let folder of this.rootFolder.subFolders) {
+ folder.getNewMessages(msgWindow, null);
+ }
+ }
+
+ performBiff(msgWindow) {
+ this.performExpand(msgWindow);
+ }
+
+ closeCachedConnections() {
+ for (let client of [...this._idleConnections, ...this._busyConnections]) {
+ client.quit();
+ }
+ this._idleConnections = [];
+ this._busyConnections = [];
+ }
+
+ /** @see nsINntpIncomingServer */
+ get charset() {
+ return this.getCharValue("charset") || "UTF-8";
+ }
+
+ set charset(value) {
+ this.setCharValue("charset", value);
+ }
+
+ get maximumConnectionsNumber() {
+ let maxConnections = this.getIntValue("max_cached_connections", 0);
+ if (maxConnections > 0) {
+ return maxConnections;
+ }
+ // The default is 2 connections, if the pref value is 0, we use the default.
+ // If it's negative, treat it as 1.
+ maxConnections = maxConnections == 0 ? 2 : 1;
+ this.maximumConnectionsNumber = maxConnections;
+ return maxConnections;
+ }
+
+ set maximumConnectionsNumber(value) {
+ this.setIntValue("max_cached_connections", value);
+ }
+
+ get newsrcRootPath() {
+ let file = this.getFileValue("mail.newsrc_root-rel", "mail.newsrc_root");
+ if (!file) {
+ file = Services.dirsvc.get("NewsD", Ci.nsIFile);
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", file);
+ }
+ return file;
+ }
+
+ set newsrcRootPath(value) {
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", value);
+ }
+
+ get newsrcFilePath() {
+ if (!this._newsrcFilePath) {
+ this._newsrcFilePath = this.getFileValue(
+ "newsrc.file-rel",
+ "newsrc.file"
+ );
+ }
+ if (!this._newsrcFilePath) {
+ let prefix = "newsrc-";
+ let suffix = "";
+ if (AppConstants.platform == "win") {
+ prefix = "";
+ suffix = ".rc";
+ }
+ this._newsrcFilePath = this.newsrcRootPath;
+ this._newsrcFilePath.append(`${prefix}${this.hostName}${suffix}`);
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ this.newsrcFilePath = this._newsrcFilePath;
+ }
+ return this._newsrcFilePath;
+ }
+
+ set newsrcFilePath(value) {
+ this._newsrcFilePath = value;
+ if (!this._newsrcFilePath.exists) {
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ }
+ this.setFileValue("newsrc.file-rel", "newsrc.file", this._newsrcFilePath);
+ }
+
+ addNewsgroupToList(name) {
+ name = new TextDecoder(this.charset).decode(
+ lazy.CommonUtils.byteStringToArrayBuffer(name)
+ );
+ this.addTo(name, false, true, true);
+ }
+
+ addNewsgroup(name) {
+ this._subscribed.add(name);
+ }
+
+ removeNewsgroup(name) {
+ this._subscribed.delete(name);
+ }
+
+ containsNewsgroup(name) {
+ // Get subFolders triggers populating _subscribed if it wasn't set already.
+ if (this._subscribed.size == 0) {
+ this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder).subFolders;
+ }
+ return this._subscribed.has(name);
+ }
+
+ subscribeToNewsgroup(name) {
+ if (this.containsNewsgroup(name)) {
+ return;
+ }
+ this.rootMsgFolder.createSubfolder(name, null);
+ }
+
+ writeNewsrcFile() {
+ if (!this.newsrcHasChanged) {
+ return;
+ }
+
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ let lines = [];
+ for (let folder of newsFolder.subFolders) {
+ folder = folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ if (folder.newsrcLine) {
+ lines.push(folder.newsrcLine);
+ }
+ }
+ IOUtils.writeUTF8(this.newsrcFilePath.path, lines.join(""));
+ }
+
+ findGroup(name) {
+ return this.rootMsgFolder
+ .findSubFolder(name)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ loadNewsUrl(uri, msgWindow, consumer) {
+ if (consumer instanceof Ci.nsIStreamListener) {
+ this.withClient(client => {
+ client.loadNewsUrl(uri.spec, msgWindow, consumer);
+ });
+ }
+ }
+
+ forgetPassword() {
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ // Clear password of root folder.
+ newsFolder.forgetAuthenticationCredentials();
+
+ // Clear password of all sub folders.
+ for (let folder of newsFolder.subFolders) {
+ folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ folder.forgetAuthenticationCredentials();
+ }
+ }
+
+ groupNotFound(msgWindow, groupName, opening) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirm(
+ msgWindow,
+ null,
+ bundle.formatStringFromName("autoUnsubscribeText", [
+ groupName,
+ this.hostName,
+ ])
+ );
+ if (result) {
+ this.unsubscribe(groupName);
+ }
+ }
+
+ _lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ /**
+ * startPopulating as an async function.
+ *
+ * @see startPopulating
+ */
+ async _startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._msgWindow = msgWindow;
+ this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew);
+ this._groups = [];
+
+ this._hostInfoLoaded = false;
+ if (!forceToServer) {
+ this._hostInfoLoaded = await this._loadHostInfo();
+ if (this._hostInfoLoaded) {
+ this.stopPopulating(msgWindow);
+ return;
+ }
+ }
+ this._hostInfoChanged = !getOnlyNew;
+ MailServices.nntp.getListOfGroupsOnServer(this, msgWindow, getOnlyNew);
+ }
+
+ /**
+ * Try to load groups from hostinfo.dat.
+ *
+ * @returns {boolean} Returns false if hostinfo.dat doesn't exist or doesn't
+ * contain any group.
+ */
+ async _loadHostInfo() {
+ this._hostInfoFile = this.localPath;
+ this._hostInfoFile.append("hostinfo.dat");
+ if (!this._hostInfoFile.exists()) {
+ return false;
+ }
+ let content = await IOUtils.readUTF8(this._hostInfoFile.path);
+ let groupLine = false;
+ for (let line of content.split(this._lineSeparator)) {
+ if (groupLine) {
+ this.addTo(line, false, true, true);
+ } else if (line == "begingroups") {
+ groupLine = true;
+ }
+ }
+ return this._groups.length;
+ }
+
+ /**
+ * Save this._groups to hostinfo.dat.
+ */
+ async _saveHostInfo() {
+ if (!this._hostInfoChanged) {
+ return;
+ }
+
+ let lines = [
+ "# News host information file.",
+ "# This is a generated file! Do not edit.",
+ "",
+ "version=2",
+ `newsrcname=${this.hostName}`,
+ `lastgroupdate=${Math.floor(Date.now() / 1000)}`,
+ "uniqueid=0",
+ "",
+ "begingroups",
+ ...this._groups,
+ ];
+ await IOUtils.writeUTF8(
+ this._hostInfoFile.path,
+ lines.join(this._lineSeparator) + this._lineSeparator
+ );
+ }
+
+ /**
+ * Get an idle connection that can be used.
+ *
+ * @returns {NntpClient}
+ */
+ async _getNextClient() {
+ // The newest connection is the least likely to have timed out.
+ let client = this._idleConnections.pop();
+ if (client) {
+ this._busyConnections.push(client);
+ return client;
+ }
+ if (
+ this._idleConnections.length + this._busyConnections.length <
+ this.maximumConnectionsNumber
+ ) {
+ // Create a new client if the pool is not full.
+ client = new lazy.NntpClient(this);
+ this._busyConnections.push(client);
+ return client;
+ }
+ // Wait until a connection is available.
+ await new Promise(resolve => this._connectionWaitingQueue.push(resolve));
+ return this._getNextClient();
+ }
+
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {Function} handler - A callback function to take a NntpClient
+ * instance, and do some actions.
+ */
+ async withClient(handler) {
+ let client = await this._getNextClient();
+ client.onIdle = () => {
+ this._busyConnections = this._busyConnections.filter(c => c != client);
+ this._idleConnections.push(client);
+ // Resovle the first waiting in queue.
+ this._connectionWaitingQueue.shift()?.();
+ };
+ handler(client);
+ client.connect();
+ }
+}
+
+NntpIncomingServer.prototype.classID = Components.ID(
+ "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}"
+);
diff --git a/comm/mailnews/news/src/NntpMessageService.jsm b/comm/mailnews/news/src/NntpMessageService.jsm
new file mode 100644
index 0000000000..dcfac7570a
--- /dev/null
+++ b/comm/mailnews/news/src/NntpMessageService.jsm
@@ -0,0 +1,272 @@
+/* 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 = ["NntpMessageService", "NewsMessageService"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpChannel: "resource:///modules/NntpChannel.jsm",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A message service for news-message://, mainly used for displaying messages.
+ *
+ * @implements {nsIMsgMessageService}
+ * @implements {nsIMsgMessageFetchPartService}
+ */
+class BaseMessageService {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgMessageService",
+ "nsIMsgMessageFetchPartService",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+
+ /** @see nsIMsgMessageService */
+ loadMessage(
+ messageURI,
+ displayConsumer,
+ msgWindow,
+ urlListener,
+ autodetectCharset
+ ) {
+ this._logger.debug("loadMessage", messageURI);
+
+ let uri = this.getUrlForUri(messageURI, msgWindow);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+ if (displayConsumer instanceof Ci.nsIDocShell) {
+ uri.loadURI(
+ displayConsumer.QueryInterface(Ci.nsIDocShell),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+ );
+ } else {
+ let streamListener = displayConsumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ }
+ }
+
+ /**
+ * @param {string} messageURI - Message URI.
+ * @param {?nsIMsgWindow} [msgWindow] - Message window.
+ * @returns {nsIURI}
+ */
+ getUrlForUri(messageURI, msgWindow) {
+ let uri = Services.io
+ .newURI(this._createMessageIdUrl(messageURI))
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgWindow = msgWindow;
+ uri.QueryInterface(Ci.nsIMsgMessageUrl).originalSpec = messageURI;
+ uri.QueryInterface(Ci.nsINntpUrl).newsAction =
+ Ci.nsINntpUrl.ActionFetchArticle;
+ return uri;
+ }
+
+ /**
+ * @param {string} uri - The message URI.
+ * @returns {?nsIMsgDBHdr} The message for the URI, or null.
+ */
+ messageURIToMsgHdr(uri) {
+ let [folder, key] = this._decomposeNewsMessageURI(uri);
+ return folder?.GetMessageHeader(key);
+ }
+
+ copyMessage(messageUri, copyListener, moveMessage, urlListener, msgWindow) {
+ this._logger.debug("copyMessage", messageUri);
+ this.loadMessage(messageUri, copyListener, msgWindow, urlListener, false);
+ }
+
+ SaveMessageToDisk(
+ messageUri,
+ file,
+ addDummyEnvelope,
+ urlListener,
+ outUrl,
+ canonicalLineEnding,
+ msgWindow
+ ) {
+ this._logger.debug("SaveMessageToDisk", messageUri);
+ let url = this.getUrlForUri(messageUri, msgWindow);
+ if (urlListener) {
+ url.RegisterListener(urlListener);
+ }
+ url.newsAction = Ci.nsINntpUrl.ActionSaveMessageToDisk;
+ url.AddDummyEnvelope = addDummyEnvelope;
+ url.canonicalLineEnding = canonicalLineEnding;
+
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+ if (folder && folder.QueryInterface(Ci.nsIMsgNewsFolder)) {
+ url.msgIsInLocalCache = folder.hasMsgOffline(key);
+ }
+
+ this.loadMessage(
+ messageUri,
+ url.getSaveAsListener(addDummyEnvelope, file),
+ msgWindow,
+ urlListener,
+ false
+ );
+ }
+
+ Search(searchSession, msgWindow, msgFolder, searchUri) {
+ let slashIndex = searchUri.indexOf("/");
+ let xpatLines = searchUri.slice(slashIndex + 1).split("/");
+ let server = msgFolder.server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(
+ searchSession.QueryInterface(Ci.nsIUrlListener),
+ msgWindow
+ );
+ client.onOpen = () => {
+ client.search(msgFolder.name, xpatLines);
+ };
+
+ client.onData = line => {
+ searchSession.runningAdapter.AddHit(line.split(" ")[0]);
+ };
+ });
+ }
+
+ streamMessage(
+ messageUri,
+ consumer,
+ msgWindow,
+ urlListener,
+ convertData,
+ additionalHeader
+ ) {
+ this._logger.debug("streamMessage", messageUri);
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+
+ let uri = this.getUrlForUri(messageUri, msgWindow);
+ if (additionalHeader) {
+ // NOTE: jsmimeemitter relies on this.
+ let url = new URL(uri.spec);
+ let params = new URLSearchParams(`?header=${additionalHeader}`);
+ for (let [key, value] of params.entries()) {
+ url.searchParams.set(key, value);
+ }
+ uri = uri.mutate().setQuery(url.search).finalize();
+ }
+
+ uri = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgIsInLocalCache = folder.hasMsgOffline(key);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+
+ let streamListener = consumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri.QueryInterface(Ci.nsINntpUrl));
+ let listener = streamListener;
+ if (convertData) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ streamListener,
+ channel
+ );
+ }
+ channel.asyncOpen(listener);
+ return uri;
+ }
+
+ /**
+ * Parse a message uri to folder and message key.
+ *
+ * @param {string} uri - The news-message:// url to parse.
+ * @returns {[nsIMsgFolder, string]} - The folder and message key.
+ */
+ _decomposeNewsMessageURI(uri) {
+ let host, groupName, key;
+ if (uri.startsWith("news-message://")) {
+ let matches = /news-message:\/\/([^:]+)\/(.+)#(\d+)/.exec(uri);
+ if (!matches) {
+ throw Components.Exception(
+ `Failed to parse message url: ${uri}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ [, host, groupName, key] = matches;
+ if (host.includes("@")) {
+ host = host.slice(host.indexOf("@") + 1);
+ }
+ } else {
+ let url = new URL(uri);
+ host = url.hostname;
+ groupName = url.searchParams.get("group");
+ key = url.searchParams.get("key");
+ }
+ groupName = groupName ? decodeURIComponent(groupName) : null;
+ let server = MailServices.accounts
+ .findServer("", host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let folder;
+ if (groupName) {
+ folder = server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+ return [folder, key];
+ }
+
+ /**
+ * Create a news:// url from a news-message:// url.
+ *
+ * @param {string} messageURI - The news-message:// url.
+ * @returns {string} The news:// url.
+ */
+ _createMessageIdUrl(messageURI) {
+ if (messageURI.startsWith("news://")) {
+ return messageURI;
+ }
+ let [folder, key] = this._decomposeNewsMessageURI(messageURI);
+ let host = folder.rootFolder.URI;
+ let messageId = folder.getMessageIdForKey(key);
+ let url = new URL(`${host}/${encodeURIComponent(messageId)}`);
+ url.searchParams.set("group", folder.name);
+ url.searchParams.set("key", key);
+ if (!url.port) {
+ url.port = folder.server.port;
+ }
+ return url.toString();
+ }
+
+ /** @see nsIMsgMessageFetchPartService */
+ fetchMimePart(uri, messageUri, displayConsumer, msgWindow, urlListener) {
+ this._logger.debug("fetchMimePart", uri.spec);
+ this.loadMessage(uri.spec, displayConsumer, msgWindow, urlListener, false);
+ }
+}
+
+/**
+ * A message service for news-message://, mainly for displaying messages.
+ */
+class NntpMessageService extends BaseMessageService {}
+
+NntpMessageService.prototype.classID = Components.ID(
+ "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}"
+);
+
+/**
+ * A message service for news://, mainly for handling attachments.
+ */
+class NewsMessageService extends BaseMessageService {}
+
+NewsMessageService.prototype.classID = Components.ID(
+ "{4cae5569-2c72-4910-9f3d-774f9e939df8}"
+);
diff --git a/comm/mailnews/news/src/NntpNewsGroup.jsm b/comm/mailnews/news/src/NntpNewsGroup.jsm
new file mode 100644
index 0000000000..e4df659802
--- /dev/null
+++ b/comm/mailnews/news/src/NntpNewsGroup.jsm
@@ -0,0 +1,420 @@
+/* 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 = ["NntpNewsGroup"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgKeySet } = ChromeUtils.import("resource:///modules/MsgKeySet.jsm");
+
+/**
+ * A helper class for NntpClient to deal with msg db and folders.
+ */
+class NntpNewsGroup {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {nsIMsgNewsFolder} folder - The associated news folder.
+ */
+ constructor(server, folder) {
+ this._server = server;
+ this._folder = folder;
+ this._db = this._folder.msgDatabase;
+ this._msgHdrs = [];
+ }
+
+ /**
+ * @type {boolean} value - Whether to fetch old messages.
+ */
+ set getOldMessages(value) {
+ this._getOldMessages = value;
+ }
+
+ /**
+ * Get the articles range to fetch, depending on server setting and user
+ * selection.
+ *
+ * @type {nsIMsgWindow} msgWindow - The associated msg window.
+ * @type {number} firstPossible - The first article that can be fetched.
+ * @type {number} lastPossible - The last article that can be fetched.
+ * @returns {[number, number]} A tuple of the first and last article to fetch.
+ */
+ getArticlesRangeToFetch(msgWindow, firstPossible, lastPossible) {
+ this._msgWindow = msgWindow;
+ if (!this._msgWindow) {
+ try {
+ this._msgWindow = MailServices.mailSession.topmostMsgWindow;
+ } catch (e) {}
+ }
+
+ this._folderFilterList = this._folder.getFilterList(this._msgWindow);
+ this._serverFilterList = this._server.getFilterList(this._msgWindow);
+ this._filterHeaders = new Set(
+ (
+ this._folderFilterList.arbitraryHeaders +
+ " " +
+ this._serverFilterList.arbitraryHeaders
+ )
+ .split(" ")
+ .filter(Boolean)
+ );
+
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ if (lastPossible < groupInfo.highWater) {
+ groupInfo.highWater = lastPossible;
+ }
+ this._knownKeySet = new MsgKeySet(groupInfo.knownArtsSet);
+ } else {
+ this._knownKeySet = new MsgKeySet();
+ this._knownKeySet.addRange(
+ this._db.lowWaterArticleNum,
+ this._db.highWaterArticleNum
+ );
+ }
+ if (this._knownKeySet.has(lastPossible)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let messengerBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ msgWindow?.statusFeedback.showStatusString(
+ messengerBundle.formatStringFromName("statusMessage", [
+ this._server.prettyName,
+ bundle.GetStringFromName("noNewMessages"),
+ ])
+ );
+ }
+
+ if (this._getOldMessages || !this._knownKeySet.has(lastPossible)) {
+ let [start, end] = this._knownKeySet.getLastMissingRange(
+ firstPossible,
+ lastPossible
+ );
+ if (this._getOldMessages) {
+ return [Math.max(start, end - this._server.maxArticles + 1), end];
+ }
+ if (
+ start &&
+ end - start > this._server.maxArticles &&
+ this._server.notifyOn
+ ) {
+ // Show a dialog to let user decide how many articles to download.
+ let args = Cc[
+ "@mozilla.org/messenger/newsdownloaddialogargs;1"
+ ].createInstance(Ci.nsINewsDownloadDialogArgs);
+ args.articleCount = end - start + 1;
+ args.groupName = this._folder.unicodeName;
+ args.serverKey = this._server.key;
+ this._msgWindow.domWindow.openDialog(
+ "chrome://messenger/content/downloadheaders.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ args
+ );
+ if (!args.hitOK) {
+ return [];
+ }
+ start = args.downloadAll ? start : end - this._server.maxArticles + 1;
+ if (this._server.markOldRead) {
+ this._readKeySet = new MsgKeySet(
+ this._folder.newsrcLine.split(":")[1].trim()
+ );
+ this._readKeySet.addRange(firstPossible, start - 1);
+ }
+ }
+ return [start, end];
+ }
+ return [];
+ }
+
+ /**
+ * Strip multiple localized Re: prefixes and set the subject and the hasRe
+ * flag. This emulates NS_MsgStripRE()
+ *
+ * @param {nsIMsgDBHdr} msgHdr - The nsIMsgDBHdr to update
+ * @param {string} subject - The unprocessed subject
+ */
+ setSubject(msgHdr, subject) {
+ let prefixes = Services.prefs
+ .getComplexValue("mailnews.localizedRe", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .filter(Boolean);
+ if (!prefixes.includes("Re")) {
+ prefixes.push("Re");
+ }
+ // Construct a regular expression like this: ^(Re: |Aw: )+
+ let newSubject = subject.replace(
+ new RegExp(`^(${prefixes.join(": |")}: )+`, "i"),
+ ""
+ );
+ msgHdr.subject = newSubject;
+ if (newSubject != subject) {
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.HasRe);
+ }
+ }
+
+ /**
+ * Parse an XOVER line to a msg hdr.
+ *
+ * @param {string} line - An XOVER response line.
+ */
+ processXOverLine(line) {
+ let parts = line.split("\t");
+ if (parts.length < 8) {
+ return;
+ }
+ let [
+ articleNumber,
+ subject,
+ from,
+ date,
+ messageId,
+ references,
+ bytes,
+ lines,
+ ] = parts;
+ let msgHdr = this._db.createNewHdr(articleNumber);
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ this.setSubject(msgHdr, subject);
+ msgHdr.author = from;
+ msgHdr.date = new Date(date).valueOf() * 1000;
+ msgHdr.messageId = messageId;
+ msgHdr.setReferences(references);
+ msgHdr.messageSize = bytes;
+ msgHdr.lineCount = lines;
+ this._msgHdrs.push(msgHdr);
+ }
+
+ /**
+ * Add a range (usually XOVER range) to the known key set.
+ */
+ addKnownArticles(start, end) {
+ this._knownKeySet.addRange(start, end);
+ }
+
+ /**
+ * Finish processing XOVER responses.
+ */
+ finishProcessingXOver() {
+ this._runFilters();
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ groupInfo.knownArtsSet = this._knownKeySet.toString();
+ }
+ }
+
+ /**
+ * Extra headers needed by filters, but not returned in XOVER response.
+ */
+ getXHdrFields() {
+ return [...this._filterHeaders].filter(
+ x => !["message-id", "references"].includes(x)
+ );
+ }
+
+ /**
+ * Update msgHdr according to XHDR line.
+ *
+ * @param {string} header - The requested header.
+ * @param {string} line - A XHDR response line.
+ */
+ processXHdrLine(header, line) {
+ let spaceIndex = line.indexOf(" ");
+ let articleNumber = line.slice(0, spaceIndex);
+ let value = line.slice(spaceIndex).trim();
+ let msgHdr = this._db.getMsgHdrForKey(articleNumber);
+ msgHdr.setStringProperty(header, value);
+ }
+
+ /**
+ * Init a msgHdr to prepare to take HEAD response.
+ *
+ * @param {number} articleNumber - The article number.
+ */
+ initHdr(articleNumber) {
+ if (this._msgHdr) {
+ this._msgHdrs.push(this._msgHdr);
+ }
+
+ if (articleNumber >= 0) {
+ this._msgHdr = this._db.createNewHdr(articleNumber);
+ }
+ }
+
+ /**
+ * Update msgHdr according to HEAD line.
+ *
+ * @param {string} line - A HEAD response line.
+ */
+ processHeadLine(line) {
+ let colonIndex = line.indexOf(":");
+ let name = line.slice(0, colonIndex);
+ let value = line.slice(colonIndex + 1).trim();
+ switch (name) {
+ case "from":
+ this._msgHdr.author = value;
+ break;
+ case "date":
+ this._msgHdr.date = new Date(value).valueOf() * 1000;
+ break;
+ case "subject":
+ this.setSubject(this._msgHdr, value);
+ this._msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ break;
+ case "message-id":
+ this._msgHdr.messageId = value;
+ break;
+ case "references":
+ this._msgHdr.setReferences(value);
+ break;
+ case "bytes":
+ this._msgHdr.messageSize = value;
+ break;
+ case "lines":
+ this._msgHdr.lineCount = value;
+ break;
+ default:
+ if (this._filterHeaders.has(name)) {
+ this._msgHdr.setStringProperty(name, value);
+ }
+ }
+ }
+
+ /**
+ * Run filters to all newly added msg hdrs.
+ */
+ _runFilters() {
+ let folderFilterCount = this._folderFilterList.filterCount;
+ let serverFilterCount = this._serverFilterList.filterCount;
+
+ for (let msgHdr of this._msgHdrs) {
+ this._filteringHdr = msgHdr;
+ this._addHdrToDB = true;
+ let headers = "";
+ if (folderFilterCount || serverFilterCount) {
+ let author = this._filteringHdr.author;
+ let subject = this._filteringHdr.subject;
+ if (author) {
+ headers += `From: ${author}\0`;
+ }
+ if (subject) {
+ headers += `Subject: ${subject}\0`;
+ }
+ }
+ if (folderFilterCount) {
+ this._folderFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (serverFilterCount) {
+ this._serverFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (this._addHdrToDB && !this._db.containsKey(msgHdr.messageKey)) {
+ this._db.addNewHdrToDB(msgHdr, true);
+ MailServices.mfn.notifyMsgAdded(msgHdr);
+ this._folder.orProcessingFlags(
+ msgHdr.messageKey,
+ Ci.nsMsgProcessingFlags.NotReportedClassified
+ );
+ }
+ }
+ }
+
+ /**
+ * Callback of nsIMsgFilterList.applyFiltersToHdr.
+ *
+ * @see nsIMsgFilterHitNotify
+ */
+ applyFilterHit(filter, msgWindow) {
+ let loggingEnabled = filter.filterList.loggingEnabled;
+ let applyMore = true;
+
+ for (let action of filter.sortedActionList) {
+ if (loggingEnabled) {
+ filter.logRuleHit(action, this._filteringHdr);
+ }
+ switch (action.type) {
+ case Ci.nsMsgFilterAction.Delete:
+ this._addHdrToDB = false;
+ break;
+ case Ci.nsMsgFilterAction.MarkRead:
+ this._db.markHdrRead(this._filteringHdr, true, null);
+ break;
+ case Ci.nsMsgFilterAction.MarkUnread:
+ this._db.markHdrRead(this._filteringHdr, false, null);
+ break;
+ case Ci.nsMsgFilterAction.KillThread:
+ this._filteringHdr.setUint32Property(
+ "ProtoThreadFlags",
+ Ci.nsMsgMessageFlags.Ignored
+ );
+ break;
+ case Ci.nsMsgFilterAction.KillSubthread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Ignored);
+ break;
+ case Ci.nsMsgFilterAction.WatchThread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Watched);
+ break;
+ case Ci.nsMsgFilterAction.MarkFlagged:
+ this._filteringHdr.markFlagged(true);
+ break;
+ case Ci.nsMsgFilterAction.ChangePriority:
+ this._filteringHdr.priority = action.priority;
+ break;
+ case Ci.nsMsgFilterAction.AddTag:
+ this._folder.addKeywordsToMessages(
+ [this._filteringHdr],
+ action.strValue
+ );
+ break;
+ case Ci.nsMsgFilterAction.StopExecution:
+ applyMore = false;
+ break;
+ case Ci.nsMsgFilterAction.Custom:
+ action.customAction.applyAction(
+ [this._filteringHdr],
+ action.strValue,
+ null,
+ Ci.nsMsgFilterType.NewsRule,
+ msgWindow
+ );
+ break;
+ default:
+ throw Components.Exception(
+ `Unexpected filter action type=${action.type}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ }
+ return applyMore;
+ }
+
+ /**
+ * Commit changes to msg db.
+ */
+ cleanUp() {
+ if (this._readKeySet) {
+ this._folder.setReadSetFromStr(this._readKeySet);
+ }
+ this._folder.notifyFinishedDownloadinghdrs();
+ this._db.commit(Ci.nsMsgDBCommitType.kSessionCommit);
+ this._db.close(true);
+ }
+}
diff --git a/comm/mailnews/news/src/NntpProtocolHandler.jsm b/comm/mailnews/news/src/NntpProtocolHandler.jsm
new file mode 100644
index 0000000000..00e5dc224b
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolHandler.jsm
@@ -0,0 +1,46 @@
+/* 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 EXPORTED_SYMBOLS = ["NewsProtocolHandler", "SnewsProtocolHandler"];
+
+var { NntpChannel } = ChromeUtils.import("resource:///modules/NntpChannel.jsm");
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class NewsProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "news";
+
+ newChannel(uri, loadInfo) {
+ let channel = new NntpChannel(uri, loadInfo);
+ let spec = uri.spec;
+ if (
+ spec.includes("part=") &&
+ !spec.includes("type=message/rfc822") &&
+ !spec.includes("type=application/x-message-display") &&
+ !spec.includes("type=application/pdf")
+ ) {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ } else {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE;
+ }
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return true;
+ }
+}
+NewsProtocolHandler.prototype.classID = Components.ID(
+ "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}"
+);
+
+class SnewsProtocolHandler extends NewsProtocolHandler {
+ scheme = "snews";
+}
+SnewsProtocolHandler.prototype.classID = Components.ID(
+ "{1895016d-5302-46a9-b3f5-9c47694d9eca}"
+);
diff --git a/comm/mailnews/news/src/NntpProtocolInfo.jsm b/comm/mailnews/news/src/NntpProtocolInfo.jsm
new file mode 100644
index 0000000000..3a1bfeb887
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolInfo.jsm
@@ -0,0 +1,44 @@
+/* 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 = ["NntpProtocolInfo"];
+
+var { MsgProtocolInfo } = ChromeUtils.importESModule(
+ "resource:///modules/MsgProtocolInfo.sys.mjs"
+);
+
+/**
+ * @implements {nsIMsgProtocolInfo}
+ */
+class NntpProtocolInfo extends MsgProtocolInfo {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]);
+
+ serverIID = Components.ID("{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}");
+
+ requiresUsername = false;
+ preflightPrettyNameWithEmailAddress = false;
+ canDelete = true;
+ canLoginAtStartUp = true;
+ canDuplicate = true;
+ canGetMessages = true;
+ canGetIncomingMessages = false;
+ defaultDoBiff = false;
+ showComposeMsgLink = false;
+ foldersCreatedAsync = false;
+
+ getDefaultServerPort(isSecure) {
+ return isSecure
+ ? Ci.nsINntpUrl.DEFAULT_NNTPS_PORT
+ : Ci.nsINntpUrl.DEFAULT_NNTP_PORT;
+ }
+
+ // @see MsgProtocolInfo.sys.mjs
+ RELATIVE_PREF = "mail.root.nntp-rel";
+ ABSOLUTE_PREF = "mail.root.nntp";
+ DIR_SERVICE_PROP = "NewsD";
+}
+
+NntpProtocolInfo.prototype.classID = Components.ID(
+ "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}"
+);
diff --git a/comm/mailnews/news/src/NntpService.jsm b/comm/mailnews/news/src/NntpService.jsm
new file mode 100644
index 0000000000..cae1cd9002
--- /dev/null
+++ b/comm/mailnews/news/src/NntpService.jsm
@@ -0,0 +1,250 @@
+/* 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 = ["NntpService"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * @implements {nsINntpService}
+ */
+class NntpService {
+ QueryInterface = ChromeUtils.generateQI(["nsINntpService"]);
+
+ get cacheStorage() {
+ if (!this._cacheStorage) {
+ this._cacheStorage = Services.cache2.memoryCacheStorage(
+ Services.loadContextInfo.custom(false, {})
+ );
+ }
+ return this._cacheStorage;
+ }
+
+ generateNewsHeaderValsForPosting(
+ newsgroupsList,
+ outNewsgroupsHeader,
+ outNewsHostHeader
+ ) {
+ let groups = newsgroupsList.split(",");
+ outNewsgroupsHeader.value = newsgroupsList;
+ let hosts = groups.map(name => this._findHostFromGroupName(name));
+ hosts = [...new Set(hosts)].filter(Boolean);
+ let host = hosts[0];
+ if (!host) {
+ outNewsHostHeader.value = "";
+ return;
+ }
+ if (hosts.length > 1) {
+ throw Components.Exception(
+ `Cross posting not allowed, hosts=${hosts.join(",")}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ outNewsHostHeader.value = host;
+ }
+
+ postMessage(messageFile, groupNames, accountKey, urlListener, msgWindow) {
+ let server = MailServices.accounts.getAccount(accountKey)?.incomingServer;
+ if (!server) {
+ // If no matching server, find the first news server and use it.
+ server = MailServices.accounts.findServer("", "", "nntp");
+ }
+ server = server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.post();
+ };
+
+ client.onReadyToPost = () => {
+ let fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ // PR_RDONLY
+ fstream.init(messageFile, 0x01, 0, 0);
+ let lineInputStream = fstream.QueryInterface(Ci.nsILineInputStream);
+ let hasMore;
+ do {
+ let outLine = {};
+ hasMore = lineInputStream.readLine(outLine);
+ let line = outLine.value;
+ if (line.startsWith(".")) {
+ // Dot stuffing, see rfc3977#section-3.1.1.
+ line = "." + line;
+ }
+ client.send(line + "\r\n");
+ } while (hasMore);
+ fstream.close();
+ client.send(".\r\n");
+ };
+ });
+ }
+
+ getNewNews(server, uri, getOld, urlListener, msgWindow) {
+ if (Services.io.offline) {
+ const NS_MSG_ERROR_OFFLINE = 0x80550014;
+ // @see nsMsgNewsFolder::UpdateFolder
+ throw Components.Exception(
+ "Cannot get news while offline",
+ NS_MSG_ERROR_OFFLINE
+ );
+ }
+ // The uri is in the form of news://news.mozilla.org/mozilla.accessibility
+ let matches = /.+:\/\/([^:]+):?(\d+)?\/(.+)?/.exec(uri);
+ let groupName = decodeURIComponent(matches[3]);
+
+ let runningUri = Services.io
+ .newURI(uri)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow, runningUri);
+ client.onOpen = () => {
+ client.getNewNews(groupName, getOld);
+ };
+ });
+
+ return runningUri;
+ }
+
+ getListOfGroupsOnServer(server, msgWindow, getOnlyNew) {
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(null, msgWindow);
+ client.onOpen = () => {
+ client.getListOfGroups(getOnlyNew);
+ };
+
+ client.onData = data => {
+ server.addNewsgroupToList(data.split(" ")[0]);
+ };
+ });
+ }
+
+ fetchMessage(folder, key, msgWindow, consumer, urlListener) {
+ let streamListener, inputStream, outputStream;
+ if (consumer instanceof Ci.nsIStreamListener) {
+ streamListener = consumer;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ inputStream = pipe.inputStream;
+ outputStream = pipe.outputStream;
+ }
+
+ let server = folder.server.QueryInterface(Ci.nsINntpIncomingServer);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.getArticleByArticleNumber(folder.name, key);
+ streamListener?.onStartRequest(null);
+ };
+ client.onData = data => {
+ outputStream?.write(data, data.length);
+ streamListener?.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ client.onDone = () => {
+ streamListener?.onStopRequest(null, Cr.NS_OK);
+ };
+ });
+ }
+
+ cancelMessage(cancelUrl, messageUri, consumer, urlListener, msgWindow) {
+ if (Services.prefs.getBoolPref("news.cancel.confirm")) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirmEx(
+ msgWindow?.domWindow,
+ null,
+ bundle.GetStringFromName("cancelConfirm"),
+ Ci.nsIPrompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ { value: false }
+ );
+ if (result != 0) {
+ // Cancelled.
+ return;
+ }
+ }
+ // The cancelUrl is in the form of "news://host/message-id?cancel"
+ let url = new URL(cancelUrl);
+ let messageId = "<" + decodeURIComponent(url.pathname.slice(1)) + ">";
+ let server = MailServices.accounts
+ .findServer("", url.host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let groupName = new URL(messageUri).pathname.slice(1);
+ let messageKey = messageUri.split("#")[1];
+ let newsFolder = server.findGroup(groupName);
+ let from = MailServices.accounts.getFirstIdentityForServer(server).email;
+ let bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+
+ server.wrappedJSObject.withClient(client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.msgWindow = msgWindow;
+
+ client.onOpen = () => {
+ client.cancelArticle(groupName);
+ };
+
+ client.onReadyToPost = () => {
+ let content = [
+ `From: ${from}`,
+ `Newsgroups: ${groupName}`,
+ `Subject: cancel ${messageId}`,
+ `References: ${messageId}`,
+ `Control: cancel ${messageId}`,
+ "MIME-Version: 1.0",
+ "Content-Type: text/plain",
+ "", // body separator
+ `This message was cancelled from within ${bundle.GetStringFromName(
+ "brandFullName"
+ )}`,
+ ];
+ client.send(content.join("\r\n"));
+ client.send("\r\n.\r\n");
+
+ newsFolder.removeMessage(messageKey);
+ newsFolder.cancelComplete();
+ };
+ });
+ }
+
+ downloadNewsgroupsForOffline(msgWindow, urlListener) {
+ let { NewsDownloader } = ChromeUtils.importESModule(
+ "resource:///modules/NewsDownloader.sys.mjs"
+ );
+ let downloader = new NewsDownloader(msgWindow, urlListener);
+ downloader.start();
+ }
+
+ /**
+ * Find the hostname of a NNTP server from a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {string} The corresponding server host.
+ */
+ _findHostFromGroupName(groupName) {
+ for (let server of MailServices.accounts.allServers) {
+ if (
+ server instanceof Ci.nsINntpIncomingServer &&
+ server.containsNewsgroup(groupName)
+ ) {
+ return server.hostName;
+ }
+ }
+ return "";
+ }
+}
+
+NntpService.prototype.classID = Components.ID(
+ "{b13db263-a219-4168-aeaf-8266f001087e}"
+);
diff --git a/comm/mailnews/news/src/NntpUtils.jsm b/comm/mailnews/news/src/NntpUtils.jsm
new file mode 100644
index 0000000000..40cc51b993
--- /dev/null
+++ b/comm/mailnews/news/src/NntpUtils.jsm
@@ -0,0 +1,63 @@
+/* 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 = ["NntpUtils"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Collection of helper functions for NNTP.
+ */
+var NntpUtils = {
+ logger: console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ }),
+
+ /**
+ * Find a server instance by its hostname.
+ *
+ * Sometimes we create a server instance to load a news url, this server is
+ * written to the prefs but not associated with any account. Different from
+ * nsIMsgAccountManager.findServer which can only find servers associated
+ * with accounts, this function looks for NNTP server in the mail.server.
+ * branch directly.
+ *
+ * @param {string} hostname - The hostname of the server.
+ * @returns {nsINntpIncomingServer|null}
+ */
+ findServer(hostname) {
+ let branch = Services.prefs.getBranch("mail.server.");
+
+ // Collect all the server keys.
+ let keySet = new Set();
+ for (let name of branch.getChildList("")) {
+ keySet.add(name.split(".")[0]);
+ }
+
+ // Find the NNTP server that matches the hostname.
+ hostname = hostname.toLowerCase();
+ for (let key of keySet) {
+ let type = branch.getCharPref(`${key}.type`, "");
+ let hostnameValue = branch
+ .getCharPref(`${key}.hostname`, "")
+ .toLowerCase();
+ if (type == "nntp" && hostnameValue == hostname) {
+ try {
+ return MailServices.accounts
+ .getIncomingServer(key)
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ } catch (e) {
+ // In some profiles, two servers have the same hostname, but only one
+ // can be loaded into AccountManager. Catch the error here and the
+ // already loaded server will be found.
+ }
+ }
+ }
+ return null;
+ },
+};
diff --git a/comm/mailnews/news/src/components.conf b/comm/mailnews/news/src/components.conf
new file mode 100644
index 0000000000..502e3cd271
--- /dev/null
+++ b/comm/mailnews/news/src/components.conf
@@ -0,0 +1,98 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{e9bb3330-ac7e-11de-8a39-0800200c9a66}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=news"],
+ "jsm": "resource:///modules/NewsAutoCompleteSearch.jsm",
+ "constructor": "NewsAutoCompleteSearch",
+ },
+ {
+ "cid": "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=nntp"],
+ "jsm": "resource:///modules/NntpIncomingServer.jsm",
+ "constructor": "NntpIncomingServer",
+ },
+ {
+ "cid": "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}",
+ "contract_ids": ["@mozilla.org/messenger/protocol/info;1?type=nntp"],
+ "jsm": "resource:///modules/NntpProtocolInfo.jsm",
+ "constructor": "NntpProtocolInfo",
+ },
+ {
+ "cid": "{b13db263-a219-4168-aeaf-8266f001087e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpservice;1"],
+ "jsm": "resource:///modules/NntpService.jsm",
+ "constructor": "NntpService",
+ },
+ {
+ "cid": "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news-message"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NntpMessageService",
+ },
+ {
+ "cid": "{4cae5569-2c72-4910-9f3d-774f9e939df8}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NewsMessageService",
+ },
+ {
+ "cid": "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=news"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "NewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "news",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 119,
+ },
+ },
+ {
+ "cid": "{1895016d-5302-46a9-b3f5-9c47694d9eca}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=snews"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "SnewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "snews",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 563,
+ },
+ },
+ {
+ "cid": "{196b4b30-e18c-11d2-806e-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpurl;1"],
+ "type": "nsNntpUrl",
+ "headers": ["/comm/mailnews/news/src/nsNntpUrl.h"],
+ },
+ {
+ "cid": "{4ace448a-f6d4-11d2-880d-004005263078}",
+ "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=news"],
+ "type": "nsMsgNewsFolder",
+ "headers": ["/comm/mailnews/news/src/nsNewsFolder.h"],
+ },
+ {
+ "cid": "{1540689e-1dd2-11b2-933d-f0d1e460ef4a}",
+ "contract_ids": ["@mozilla.org/messenger/newsdownloaddialogargs;1"],
+ "type": "nsNewsDownloadDialogArgs",
+ "headers": ["/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h"],
+ },
+]
diff --git a/comm/mailnews/news/src/moz.build b/comm/mailnews/news/src/moz.build
new file mode 100644
index 0000000000..a5f792b2cf
--- /dev/null
+++ b/comm/mailnews/news/src/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsNewsDownloadDialogArgs.cpp",
+ "nsNewsDownloader.cpp",
+ "nsNewsFolder.cpp",
+ "nsNewsUtils.cpp",
+ "nsNntpUrl.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "NewsAutoCompleteSearch.jsm",
+ "NewsDownloader.sys.mjs",
+ "NntpChannel.jsm",
+ "NntpClient.jsm",
+ "NntpIncomingServer.jsm",
+ "NntpMessageService.jsm",
+ "NntpNewsGroup.jsm",
+ "NntpProtocolHandler.jsm",
+ "NntpProtocolInfo.jsm",
+ "NntpService.jsm",
+ "NntpUtils.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
diff --git a/comm/mailnews/news/src/nntpCore.h b/comm/mailnews/news/src/nntpCore.h
new file mode 100644
index 0000000000..52230c6931
--- /dev/null
+++ b/comm/mailnews/news/src/nntpCore.h
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _NNTPCore_H__
+#define _NNTPCore_H__
+
+#define NEWS_MSGS_URL "chrome://messenger/locale/news.properties"
+
+// The following string constants are protocol strings. I'm defining them as
+// macros here so I don't have to sprinkle all of the strings throughout the
+// protocol.
+#define NNTP_CMD_LIST_EXTENSIONS "LIST EXTENSIONS" CRLF
+#define NNTP_CMD_MODE_READER "MODE READER" CRLF
+#define NNTP_CMD_LIST_SEARCHES "LIST SEARCHES" CRLF
+#define NNTP_CMD_LIST_SEARCH_FIELDS "LIST SRCHFIELDS" CRLF
+#define NNTP_CMD_GET_PROPERTIES "GET" CRLF
+#define NNTP_CMD_LIST_SUBSCRIPTIONS "LIST SUBSCRIPTIONS" CRLF
+#define NNTP_CMD_POST "POST" CRLF
+#define NNTP_CMD_QUIT "QUIT" CRLF
+
+// end of protocol strings
+
+#define MK_NNTP_RESPONSE_HELP 100
+
+#define MK_NNTP_RESPONSE_POSTING_ALLOWED 200
+#define MK_NNTP_RESPONSE_POSTING_DENIED 201
+
+#define MK_NNTP_RESPONSE_DISCONTINUED 400
+
+#define MK_NNTP_RESPONSE_COMMAND_UNKNOWN 500
+#define MK_NNTP_RESPONSE_SYNTAX_ERROR 501
+#define MK_NNTP_RESPONSE_PERMISSION_DENIED 502
+#define MK_NNTP_RESPONSE_SERVER_ERROR 503
+
+#define MK_NNTP_RESPONSE_ARTICLE_BOTH 220
+#define MK_NNTP_RESPONSE_ARTICLE_HEAD 221
+#define MK_NNTP_RESPONSE_ARTICLE_BODY 222
+#define MK_NNTP_RESPONSE_ARTICLE_NONE 223
+#define MK_NNTP_RESPONSE_ARTICLE_NO_GROUP 412
+#define MK_NNTP_RESPONSE_ARTICLE_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_ARTICLE_NONEXIST 423
+#define MK_NNTP_RESPONSE_ARTICLE_NOTFOUND 430
+
+#define MK_NNTP_RESPONSE_GROUP_SELECTED 211
+#define MK_NNTP_RESPONSE_GROUP_NO_GROUP 411
+
+#define MK_NNTP_RESPONSE_IHAVE_OK 235
+#define MK_NNTP_RESPONSE_IHAVE_ARTICLE 335
+#define MK_NNTP_RESPONSE_IHAVE_NOT_WANTED 435
+#define MK_NNTP_RESPONSE_IHAVE_FAILED 436
+#define MK_NNTP_RESPONSE_IHAVE_REJECTED 437
+
+#define MK_NNTP_RESPONSE_LAST_OK 223
+#define MK_NNTP_RESPONSE_LAST_NO_GROUP 412
+#define MK_NNTP_RESPONSE_LAST_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_LAST_NO_ARTICLE 422
+
+#define MK_NNTP_RESPONSE_LIST_OK 215
+
+#define MK_NNTP_RESPONSE_NEWGROUPS_OK 231
+
+#define MK_NNTP_RESPONSE_NEWNEWS_OK 230
+
+#define MK_NNTP_RESPONSE_NEXT_OK 223
+#define MK_NNTP_RESPONSE_NEXT_NO_GROUP 412
+#define MK_NNTP_RESPONSE_NEXT_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_NEXT_NO_ARTICLE 421
+
+#define MK_NNTP_RESPONSE_POST_OK 240
+#define MK_NNTP_RESPONSE_POST_SEND_NOW 340
+#define MK_NNTP_RESPONSE_POST_DENIED 440
+#define MK_NNTP_RESPONSE_POST_FAILED 441
+
+#define MK_NNTP_RESPONSE_QUIT_OK 205
+
+#define MK_NNTP_RESPONSE_SLAVE_OK 202
+
+#define MK_NNTP_RESPONSE_CHECK_NO_ARTICLE 238
+#define MK_NNTP_RESPONSE_CHECK_NO_ACCEPT 400
+#define MK_NNTP_RESPONSE_CHECK_LATER 431
+#define MK_NNTP_RESPONSE_CHECK_DONT_SEND 438
+#define MK_NNTP_RESPONSE_CHECK_DENIED 480
+#define MK_NNTP_RESPONSE_CHECK_ERROR 500
+
+#define MK_NNTP_RESPONSE_XHDR_OK 221
+#define MK_NNTP_RESPONSE_XHDR_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XHDR_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XHDR_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XHDR_DENIED 502
+
+#define MK_NNTP_RESPONSE_XOVER_OK 224
+#define MK_NNTP_RESPONSE_XOVER_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XOVER_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XOVER_DENIED 502
+
+#define MK_NNTP_RESPONSE_XPAT_OK 221
+#define MK_NNTP_RESPONSE_XPAT_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XPAT_DENIED 502
+
+#define MK_NNTP_RESPONSE_AUTHINFO_OK 281
+#define MK_NNTP_RESPONSE_AUTHINFO_CONT 381
+#define MK_NNTP_RESPONSE_AUTHINFO_REQUIRE 480
+#define MK_NNTP_RESPONSE_AUTHINFO_REJECT 482
+#define MK_NNTP_RESPONSE_AUTHINFO_DENIED 502
+
+#define MK_NNTP_RESPONSE_
+
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_OK 250
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_CONT 350
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REQUIRE 450
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REJECT 452
+
+#define MK_NNTP_RESPONSE_TYPE_INFO 1
+#define MK_NNTP_RESPONSE_TYPE_OK 2
+#define MK_NNTP_RESPONSE_TYPE_CONT 3
+#define MK_NNTP_RESPONSE_TYPE_CANNOT 4
+#define MK_NNTP_RESPONSE_TYPE_ERROR 5
+
+#define MK_NNTP_RESPONSE_TYPE(x) (x / 100)
+
+// the following used to be defined in allxpstr.h. Until we find a new values
+// for these, I'm defining them here because I don't want to link against
+// xplib.lib...(mscott)
+
+#define MK_DATA_LOADED 1
+#define MK_EMPTY_NEWS_LIST -227
+#define MK_INTERRUPTED -201
+#define MK_MALFORMED_URL_ERROR -209
+#define MK_NEWS_ERROR_FMT -430
+#define MK_NNTP_CANCEL_CONFIRM -426
+#define MK_NNTP_CANCEL_DISALLOWED -427
+#define MK_NNTP_NOT_CANCELLED -429
+#define MK_OUT_OF_MEMORY -207
+#define XP_CONFIRM_SAVE_NEWSGROUPS -1
+#define XP_HTML_ARTICLE_EXPIRED -1
+#define XP_HTML_NEWS_ERROR -1
+#define XP_PROGRESS_READ_NEWSGROUPINFO 1
+#define XP_PROGRESS_RECEIVE_ARTICLE 1
+#define XP_PROGRESS_RECEIVE_LISTARTICLES 1
+#define XP_PROGRESS_RECEIVE_NEWSGROUP 1
+#define XP_PROGRESS_SORT_ARTICLES 1
+#define XP_PROGRESS_READ_NEWSGROUP_COUNTS 1
+#define XP_THERMO_PERCENT_FORM 1
+#define XP_PROMPT_ENTER_USERNAME 1
+#define MK_NNTP_AUTH_FAILED -260
+#define MK_NNTP_ERROR_MESSAGE -304
+#define MK_NNTP_NEWSGROUP_SCAN_ERROR -305
+#define MK_NNTP_SERVER_ERROR -217
+#define MK_NNTP_SERVER_NOT_CONFIGURED -307
+#define MK_TCP_READ_ERROR -252
+#define MK_TCP_WRITE_ERROR -236
+#define MK_NNTP_CANCEL_ERROR -428
+#define XP_CONNECT_NEWS_HOST_CONTACTED_WAITING_FOR_REPLY 1
+#define XP_PLEASE_ENTER_A_PASSWORD_FOR_NEWS_SERVER_ACCESS 1
+#define XP_GARBAGE_COLLECTING 1
+#define XP_MESSAGE_SENT_WAITING_NEWS_REPLY 1
+#define MK_MSG_DELIV_NEWS 1
+#define MK_MSG_COLLABRA_DISABLED 1
+#define MK_MSG_EXPIRE_NEWS_ARTICLES 1
+#define MK_MSG_HTML_IMAP_NO_CACHED_BODY 1
+#define MK_MSG_CANT_MOVE_FOLDER 1
+
+#endif /* NNTPCore_H__ */
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
new file mode 100644
index 0000000000..3b91407598
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsNewsDownloadDialogArgs.h"
+
+nsNewsDownloadDialogArgs::nsNewsDownloadDialogArgs() {
+ mArticleCount = 0;
+ mServerKey = "";
+ mHitOK = false;
+ mDownloadAll = false;
+}
+
+nsNewsDownloadDialogArgs::~nsNewsDownloadDialogArgs() {}
+
+NS_IMPL_ISUPPORTS(nsNewsDownloadDialogArgs, nsINewsDownloadDialogArgs)
+
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetGroupName(nsAString& aGroupName) {
+ aGroupName = mGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetGroupName(
+ const nsAString& aGroupName) {
+ mGroupName = aGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetArticleCount(
+ int32_t* aArticleCount) {
+ NS_ENSURE_ARG_POINTER(aArticleCount);
+
+ *aArticleCount = mArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetArticleCount(int32_t aArticleCount) {
+ mArticleCount = aArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetServerKey(char** aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ *aServerKey = ToNewCString(mServerKey);
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetServerKey(const char* aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ mServerKey = aServerKey;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetHitOK(bool* aHitOK) {
+ NS_ENSURE_ARG_POINTER(aHitOK);
+
+ *aHitOK = mHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetHitOK(bool aHitOK) {
+ mHitOK = aHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetDownloadAll(bool* aDownloadAll) {
+ NS_ENSURE_ARG_POINTER(aDownloadAll);
+
+ *aDownloadAll = mDownloadAll;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetDownloadAll(bool aDownloadAll) {
+ mDownloadAll = aDownloadAll;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
new file mode 100644
index 0000000000..7a43523072
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsNewsDownloadDialogArgs_h__
+#define nsNewsDownloadDialogArgs_h__
+
+#include "nsINewsDownloadDialogArgs.h"
+#include "nsString.h"
+
+class nsNewsDownloadDialogArgs : public nsINewsDownloadDialogArgs {
+ public:
+ nsNewsDownloadDialogArgs();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINEWSDOWNLOADDIALOGARGS
+
+ private:
+ virtual ~nsNewsDownloadDialogArgs();
+
+ nsString mGroupName;
+ int32_t mArticleCount;
+ nsCString mServerKey;
+ bool mHitOK;
+ bool mDownloadAll;
+};
+
+#endif // nsNewsDownloadDialogArgs_h__
diff --git a/comm/mailnews/news/src/nsNewsDownloader.cpp b/comm/mailnews/news/src/nsNewsDownloader.cpp
new file mode 100644
index 0000000000..945e1bd084
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.cpp
@@ -0,0 +1,507 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nntpCore.h"
+#include "netCore.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsIStringBundle.h"
+#include "nsNewsDownloader.h"
+#include "nsINntpService.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgMailSession.h"
+#include "nsMsgMessageFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+
+// This file contains the news article download state machine.
+
+// if pIds is not null, download the articles whose id's are passed in.
+// Otherwise, which articles to download is determined by nsNewsDownloader
+// object, or subclasses thereof. News can download marked objects, for example.
+nsresult nsNewsDownloader::DownloadArticles(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pIds) {
+ if (pIds != nullptr)
+ m_keysToDownload.InsertElementsAt(0, pIds->Elements(), pIds->Length());
+
+ if (!m_keysToDownload.IsEmpty()) m_downloadFromKeys = true;
+
+ m_folder = folder;
+ m_window = window;
+ m_numwrote = 0;
+
+ bool headersToDownload = GetNextHdrToRetrieve();
+ // should we have a special error code for failure here?
+ return (headersToDownload) ? DownloadNext(true) : NS_ERROR_FAILURE;
+}
+
+/* Saving news messages
+ */
+
+NS_IMPL_ISUPPORTS(nsNewsDownloader, nsIUrlListener, nsIMsgSearchNotify)
+
+nsNewsDownloader::nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* msgDB,
+ nsIUrlListener* listener) {
+ m_numwrote = 0;
+ m_downloadFromKeys = false;
+ m_newsDB = msgDB;
+ m_abort = false;
+ m_listener = listener;
+ m_window = window;
+ m_lastPercent = -1;
+ m_lastProgressTime = 0;
+ // not the perfect place for this, but I think it will work.
+ if (m_window) m_window->SetStopped(false);
+}
+
+nsNewsDownloader::~nsNewsDownloader() {
+ if (m_listener)
+ m_listener->OnStopRunningUrl(/* don't have a url */ nullptr, m_status);
+ if (m_newsDB) {
+ m_newsDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ m_newsDB = nullptr;
+ }
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
+
+NS_IMETHODIMP nsNewsDownloader::OnStopRunningUrl(nsIURI* url,
+ nsresult exitCode) {
+ bool stopped = false;
+ if (m_window) m_window->GetStopped(&stopped);
+ if (stopped) exitCode = NS_BINDING_ABORTED;
+
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
+ rv = DownloadNext(false);
+
+ return rv;
+}
+
+nsresult nsNewsDownloader::DownloadNext(bool firstTimeP) {
+ nsresult rv;
+ if (!firstTimeP) {
+ bool moreHeaders = GetNextHdrToRetrieve();
+ if (!moreHeaders) {
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ return NS_OK;
+ }
+ }
+ StartDownload();
+ m_wroteAnyP = false;
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ return nntpService->FetchMessage(m_folder, m_keyToDownload, m_window, nullptr,
+ this, getter_AddRefs(uri));
+}
+
+bool DownloadNewsArticlesToOfflineStore::GetNextHdrToRetrieve() {
+ nsresult rv;
+
+ if (m_downloadFromKeys) return nsNewsDownloader::GetNextHdrToRetrieve();
+
+ if (m_headerEnumerator == nullptr)
+ rv = m_newsDB->EnumerateMessages(getter_AddRefs(m_headerEnumerator));
+
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(rv = m_headerEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ rv = m_headerEnumerator->GetNext(getter_AddRefs(m_newsHeader));
+ NS_ENSURE_SUCCESS(rv, false);
+ uint32_t hdrFlags;
+ m_newsHeader->GetFlags(&hdrFlags);
+ if (hdrFlags & nsMsgMessageFlags::Marked) {
+ m_newsHeader->GetMessageKey(&m_keyToDownload);
+ break;
+ } else {
+ m_newsHeader = nullptr;
+ }
+ }
+ return hasMore;
+}
+
+void nsNewsDownloader::Abort() {}
+void nsNewsDownloader::Complete() {}
+
+bool nsNewsDownloader::GetNextHdrToRetrieve() {
+ nsresult rv;
+ if (m_downloadFromKeys) {
+ if (m_numwrote >= (int32_t)m_keysToDownload.Length()) return false;
+
+ m_keyToDownload = m_keysToDownload[m_numwrote++];
+ int32_t percent;
+ percent = (100 * m_numwrote) / (int32_t)m_keysToDownload.Length();
+
+ int64_t nowMS = 0;
+ if (percent < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS - m_lastProgressTime < 750) return true;
+ }
+
+ m_lastProgressTime = nowMS;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, false);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString firstStr;
+ firstStr.AppendInt(m_numwrote);
+ nsAutoString totalStr;
+ totalStr.AppendInt(int(m_keysToDownload.Length()));
+ nsString prettyName;
+ nsString statusString;
+
+ m_folder->GetPrettyName(prettyName);
+
+ AutoTArray<nsString, 3> formatStrings = {firstStr, totalStr, prettyName};
+ rv = bundle->FormatStringFromName("downloadingArticlesForOffline",
+ formatStrings, statusString);
+ NS_ENSURE_SUCCESS(rv, false);
+ ShowProgress(statusString.get(), percent);
+ return true;
+ }
+ NS_ASSERTION(false, "shouldn't get here if we're not downloading from keys.");
+ return false; // shouldn't get here if we're not downloading from keys.
+}
+
+nsresult nsNewsDownloader::ShowProgress(const char16_t* progressString,
+ int32_t percent) {
+ if (!m_statusFeedback) {
+ if (m_window) m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+ }
+ if (m_statusFeedback) {
+ m_statusFeedback->ShowStatusString(nsDependentString(progressString));
+ if (percent != m_lastPercent) {
+ m_statusFeedback->ShowProgress(percent);
+ m_lastPercent = percent;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStartRunningUrl(
+ nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStopRunningUrl(
+ nsIURI* url, nsresult exitCode) {
+ m_status = exitCode;
+ if (m_newsHeader != nullptr) {
+#ifdef DEBUG_bienvenu
+ // XP_Trace("finished retrieving %ld\n", m_newsHeader->GetMessageKey());
+#endif
+ if (m_newsDB) {
+ nsMsgKey msgKey;
+ m_newsHeader->GetMessageKey(&msgKey);
+ m_newsDB->MarkMarked(msgKey, false, nullptr);
+ }
+ }
+ m_newsHeader = nullptr;
+ return nsNewsDownloader::OnStopRunningUrl(url, exitCode);
+}
+
+int DownloadNewsArticlesToOfflineStore::FinishDownload() { return 0; }
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchHit(nsIMsgDBHdr* header,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(header);
+
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ // only need to download articles we don't already have...
+ if (!(msgFlags & nsMsgMessageFlags::Offline)) {
+ nsMsgKey key;
+ header->GetMessageKey(&key);
+ m_keysToDownload.AppendElement(key);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchDone(nsresult status) {
+ if (m_keysToDownload.IsEmpty()) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ nsresult rv = DownloadArticles(
+ m_window, m_folder,
+ /* we've already set m_keysToDownload, so don't pass it in */ nullptr);
+ if (NS_FAILED(rv))
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, rv);
+
+ return rv;
+}
+NS_IMETHODIMP nsNewsDownloader::OnNewSearch() { return NS_OK; }
+
+int DownloadNewsArticlesToOfflineStore::StartDownload() {
+ m_newsDB->GetMsgHdrForKey(m_keyToDownload, getter_AddRefs(m_newsHeader));
+ return 0;
+}
+
+DownloadNewsArticlesToOfflineStore::DownloadNewsArticlesToOfflineStore(
+ nsIMsgWindow* window, nsIMsgDatabase* db, nsIUrlListener* listener)
+ : nsNewsDownloader(window, db, listener) {
+ m_newsDB = db;
+}
+
+DownloadNewsArticlesToOfflineStore::~DownloadNewsArticlesToOfflineStore() {}
+
+DownloadMatchingNewsArticlesToNewsDB::DownloadMatchingNewsArticlesToNewsDB(
+ nsIMsgWindow* window, nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener)
+ : DownloadNewsArticlesToOfflineStore(window, newsDB, listener) {
+ m_window = window;
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_downloadFromKeys = true; // search term matching means downloadFromKeys.
+}
+
+DownloadMatchingNewsArticlesToNewsDB::~DownloadMatchingNewsArticlesToNewsDB() {}
+
+NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups, nsIUrlListener)
+
+nsMsgDownloadAllNewsgroups::nsMsgDownloadAllNewsgroups(
+ nsIMsgWindow* window, nsIUrlListener* listener) {
+ m_window = window;
+ m_listener = listener;
+ m_downloaderForGroup =
+ new DownloadMatchingNewsArticlesToNewsDB(window, nullptr, nullptr, this);
+ m_downloadedHdrsForCurGroup = false;
+}
+
+nsMsgDownloadAllNewsgroups::~nsMsgDownloadAllNewsgroups() {}
+
+NS_IMETHODIMP nsMsgDownloadAllNewsgroups::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDownloadAllNewsgroups::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND) {
+ if (m_downloadedHdrsForCurGroup) {
+ bool savingArticlesOffline = false;
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder =
+ do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->GetSaveArticleOffline(&savingArticlesOffline);
+
+ m_downloadedHdrsForCurGroup = false;
+ if (savingArticlesOffline) // skip this group - we're saving to it
+ // already
+ rv = ProcessNextGroup();
+ else
+ rv = DownloadMsgsForCurrentGroup();
+ } else {
+ rv = ProcessNextGroup();
+ }
+ } else if (m_listener) // notify main observer.
+ m_listener->OnStopRunningUrl(url, exitCode);
+
+ return rv;
+}
+
+/**
+ * Leaves m_currentServer at the next nntp "server" that
+ * might have folders to download for offline use. If no more servers,
+ * m_currentServer will be left at nullptr and the function returns false.
+ * Also, sets up m_folderQueue to hold a (reversed) list of all the folders
+ * to consider for the current server.
+ * If no servers found, returns false.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextServer() {
+ nsresult rv;
+
+ if (m_allServers.IsEmpty()) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ASSERTION(accountManager && NS_SUCCEEDED(rv),
+ "couldn't get account mgr");
+ if (!accountManager || NS_FAILED(rv)) return false;
+
+ rv = accountManager->GetAllServers(m_allServers);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ size_t serverIndex = 0;
+ if (m_currentServer) {
+ serverIndex = m_allServers.IndexOf(m_currentServer);
+ if (serverIndex == m_allServers.NoIndex) {
+ serverIndex = 0;
+ } else {
+ ++serverIndex;
+ }
+ }
+ m_currentServer = nullptr;
+ uint32_t numServers = m_allServers.Length();
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ while (serverIndex < numServers) {
+ nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]);
+ serverIndex++;
+
+ nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server);
+ if (!newsServer) // we're only looking for news servers
+ continue;
+
+ if (server) {
+ m_currentServer = server;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ rv = rootFolder->GetDescendants(m_folderQueue);
+ if (NS_SUCCEEDED(rv)) {
+ if (!m_folderQueue.IsEmpty()) {
+ // We'll be popping folders from the end of the queue as we go.
+ m_folderQueue.Reverse();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Sets m_currentFolder to the next usable folder.
+ *
+ * @return False if no more folders found, otherwise true.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextGroup() {
+ nsresult rv = NS_OK;
+
+ if (m_currentFolder) {
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(false);
+
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) {
+ bool folderOpen;
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ session->IsFolderOpenInWindow(m_currentFolder, &folderOpen);
+ if (!folderOpen &&
+ !(folderFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
+ m_currentFolder->SetMsgDatabase(nullptr);
+ }
+ m_currentFolder = nullptr;
+ }
+
+ bool hasMore = false;
+ if (m_currentServer) {
+ hasMore = !m_folderQueue.IsEmpty();
+ }
+ if (!hasMore) {
+ hasMore = AdvanceToNextServer();
+ }
+
+ if (hasMore) {
+ m_currentFolder = m_folderQueue.PopLastElement();
+ }
+ return m_currentFolder;
+}
+
+nsresult DownloadMatchingNewsArticlesToNewsDB::RunSearch(
+ nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession) {
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_searchSession = searchSession;
+
+ m_keysToDownload.Clear();
+
+ NS_ENSURE_ARG(searchSession);
+ NS_ENSURE_ARG(folder);
+
+ searchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
+ nsresult rv =
+ searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return searchSession->Search(m_window);
+}
+
+nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup() {
+ bool done = false;
+
+ while (!done) {
+ done = !AdvanceToNextGroup();
+ if (!done && m_currentFolder) {
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Offline) break;
+ }
+ }
+ if (done) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ m_downloadedHdrsForCurGroup = true;
+ return m_currentFolder ? m_currentFolder->GetNewMessages(m_window, this)
+ : NS_ERROR_NOT_INITIALIZED;
+}
+
+nsresult nsMsgDownloadAllNewsgroups::DownloadMsgsForCurrentGroup() {
+ NS_ENSURE_TRUE(m_downloaderForGroup, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIMsgDownloadSettings> downloadSettings;
+ m_currentFolder->GetMsgDatabase(getter_AddRefs(db));
+ nsresult rv =
+ m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(true);
+
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool downloadByDate, downloadUnreadOnly;
+ uint32_t ageLimitOfMsgsToDownload;
+
+ downloadSettings->GetDownloadByDate(&downloadByDate);
+ downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ nsCOMPtr<nsIMsgSearchValue> value;
+
+ rv = searchSession->CreateTerm(getter_AddRefs(term));
+ NS_ENSURE_SUCCESS(rv, rv);
+ term->GetValue(getter_AddRefs(value));
+
+ if (downloadUnreadOnly) {
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Read);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value, true, nullptr);
+ }
+ if (downloadByDate) {
+ value->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ value->SetAge(ageLimitOfMsgsToDownload);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::AgeInDays,
+ nsMsgSearchOp::IsLessThan, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+ }
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Offline);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+
+ m_downloaderForGroup->RunSearch(m_currentFolder, db, searchSession);
+ return rv;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloader.h b/comm/mailnews/news/src/nsNewsDownloader.h
new file mode 100644
index 0000000000..c1e68eb77d
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsNewsDownloader_H_
+#define _nsNewsDownloader_H_
+
+#include "nsIMsgDatabase.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsTArray.h"
+
+// base class for downloading articles in a single newsgroup. Keys to download
+// are passed in to DownloadArticles method.
+class nsNewsDownloader : public nsIUrlListener, public nsIMsgSearchNotify {
+ public:
+ nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual nsresult DownloadArticles(nsIMsgWindow* window, nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pKeyArray);
+
+ bool ShouldAbort() const { return m_abort; }
+
+ protected:
+ virtual ~nsNewsDownloader();
+
+ virtual int32_t Write(const char* /*block*/, int32_t length) {
+ return length;
+ }
+ virtual void Abort();
+ virtual void Complete();
+ virtual bool GetNextHdrToRetrieve();
+ virtual nsresult DownloadNext(bool firstTimeP);
+ virtual int32_t FinishDownload() { return 0; }
+ virtual int32_t StartDownload() { return 0; }
+ virtual nsresult ShowProgress(const char16_t* progressString,
+ int32_t percent);
+
+ nsTArray<nsMsgKey> m_keysToDownload;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgDatabase> m_newsDB;
+ nsCOMPtr<nsIUrlListener> m_listener;
+ bool m_downloadFromKeys;
+ bool m_existedP;
+ bool m_wroteAnyP;
+ bool m_summaryValidP;
+ bool m_abort;
+ int32_t m_numwrote;
+ nsMsgKey m_keyToDownload;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+ nsresult m_status;
+};
+
+// class for downloading articles in a single newsgroup to the offline store.
+class DownloadNewsArticlesToOfflineStore : public nsNewsDownloader {
+ public:
+ DownloadNewsArticlesToOfflineStore(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+ virtual ~DownloadNewsArticlesToOfflineStore();
+
+ NS_IMETHOD OnStartRunningUrl(nsIURI* url);
+ NS_IMETHOD OnStopRunningUrl(nsIURI* url, nsresult exitCode);
+
+ protected:
+ virtual int32_t StartDownload();
+ virtual int32_t FinishDownload();
+ virtual bool GetNextHdrToRetrieve();
+
+ nsCOMPtr<nsIMsgEnumerator> m_headerEnumerator;
+ nsCOMPtr<nsIMsgDBHdr> m_newsHeader;
+};
+
+// class for downloading all the articles that match the passed in search
+// criteria for a single newsgroup.
+class DownloadMatchingNewsArticlesToNewsDB
+ : public DownloadNewsArticlesToOfflineStore {
+ public:
+ DownloadMatchingNewsArticlesToNewsDB(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener);
+ virtual ~DownloadMatchingNewsArticlesToNewsDB();
+ nsresult RunSearch(nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession);
+
+ protected:
+};
+
+// this class iterates all the news servers for each group on the server that's
+// configured for offline use, downloads the messages that meet the download
+// criteria for that newsgroup/server
+class nsMsgDownloadAllNewsgroups : public nsIUrlListener {
+ public:
+ nsMsgDownloadAllNewsgroups(nsIMsgWindow* window, nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ nsresult ProcessNextGroup();
+
+ protected:
+ virtual ~nsMsgDownloadAllNewsgroups();
+
+ bool AdvanceToNextServer();
+ bool AdvanceToNextGroup();
+ nsresult DownloadMsgsForCurrentGroup();
+
+ RefPtr<DownloadMatchingNewsArticlesToNewsDB> m_downloaderForGroup;
+
+ nsCOMPtr<nsIMsgFolder> m_currentFolder;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsTArray<RefPtr<nsIMsgIncomingServer>> m_allServers;
+ nsCOMPtr<nsIMsgIncomingServer> m_currentServer;
+ // Folders still to process for the current server.
+ nsTArray<RefPtr<nsIMsgFolder>> m_folderQueue;
+ nsCOMPtr<nsIUrlListener> m_listener;
+
+ bool m_downloadedHdrsForCurGroup;
+};
+
+#endif
diff --git a/comm/mailnews/news/src/nsNewsFolder.cpp b/comm/mailnews/news/src/nsNewsFolder.cpp
new file mode 100644
index 0000000000..f23b54e273
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.cpp
@@ -0,0 +1,1645 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "prlog.h"
+
+#include "msgCore.h" // precompiled header...
+#include "nntpCore.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsNewsFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "MailNewsTypes.h"
+#include "prprf.h"
+#include "prsystem.h"
+#include "nsTArray.h"
+#include "nsINntpService.h"
+#include "nsIMsgFilterService.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsNewsUtils.h"
+
+#include "nsIMsgIncomingServer.h"
+#include "nsINntpIncomingServer.h"
+#include "nsINewsDatabase.h"
+#include "nsILineInputStream.h"
+
+#include "nsIMsgWindow.h"
+#include "nsIWindowWatcher.h"
+
+#include "nsNetUtil.h"
+#include "nsIAuthPrompt.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsINntpUrl.h"
+
+#include "nsNewsDownloader.h"
+#include "nsIStringBundle.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+
+#include "nsIMsgFolderNotificationService.h"
+#include "nsILoginInfo.h"
+#include "nsILoginManager.h"
+#include "nsEmbedCID.h"
+#include "mozilla/Components.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsIInputStream.h"
+#include "nsMemory.h"
+#include "nsIURIMutator.h"
+
+#define kNewsSortOffset 9000
+
+#define NEWS_SCHEME "news:"
+#define SNEWS_SCHEME "snews:"
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsMsgNewsFolder::nsMsgNewsFolder(void)
+ : mExpungedBytes(0),
+ mGettingNews(false),
+ mInitialized(false),
+ m_downloadMessageForOfflineUse(false),
+ mReadSet(nullptr),
+ mSortOrder(kNewsSortOffset) {
+ mFolderSize = kSizeUnknown;
+}
+
+nsMsgNewsFolder::~nsMsgNewsFolder(void) {}
+
+NS_IMPL_ADDREF_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+NS_IMPL_RELEASE_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+
+NS_IMETHODIMP nsMsgNewsFolder::QueryInterface(REFNSIID aIID,
+ void** aInstancePtr) {
+ if (!aInstancePtr) return NS_ERROR_NULL_POINTER;
+ *aInstancePtr = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsIMsgNewsFolder)))
+ *aInstancePtr = static_cast<nsIMsgNewsFolder*>(this);
+ if (*aInstancePtr) {
+ AddRef();
+ return NS_OK;
+ }
+
+ return nsMsgDBFolder::QueryInterface(aIID, aInstancePtr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgNewsFolder::CreateSubFolders(nsIFile* path) {
+ nsresult rv;
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->GetNewsrcFilePath(getter_AddRefs(mNewsrcFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = LoadNewsrcFileAndCreateNewsgroups();
+ } else // is not a host, so it has no newsgroups. (what about categories??)
+ rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::AddNewsgroup(const nsACString& name, const nsACString& setStr,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+ // URI should use UTF-8
+ // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax)
+
+ // we are handling newsgroup names in UTF-8
+ NS_ConvertUTF8toUTF16 nameUtf16(name);
+
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(nameUtf16, escapedName);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = nntpServer->AddNewsgroup(nameUtf16);
+ if (NS_FAILED(rv)) return rv;
+
+ uri.Append(escapedName);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder(do_QueryInterface(folder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure any containing .sdb dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cache this for when we open the db
+ rv = newsFolder->SetReadSetFromStr(setStr);
+
+ rv = folder->SetParent(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this what shows up in the UI
+ rv = folder->SetName(nameUtf16);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->SetFlag(nsMsgFolderFlags::Newsgroup);
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t numExistingGroups = mSubFolders.Count();
+
+ // add kNewsSortOffset (9000) to prevent this problem: 1,10,11,2,3,4,5
+ // We use 9000 instead of 1000 so newsgroups will sort to bottom of flat
+ // folder views
+ rv = folder->SetSortOrder(numExistingGroups + kNewsSortOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mSubFolders.AppendObject(folder);
+ folder->SetParent(this);
+ folder.forget(child);
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::ParseFolder(nsIFile* path) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgNewsFolder::AddDirectorySeparator(nsIFile* path) {
+ // don't concat the full separator with .sbd
+ return (mURI.Equals(kNewsRootURI))
+ ? NS_OK
+ : nsMsgDBFolder::AddDirectorySeparator(path);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ if (!mInitialized) {
+ // do this first, so we make sure to do it, even on failure.
+ // see bug #70494
+ mInitialized = true;
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = CreateSubFolders(path);
+ if (NS_FAILED(rv)) return rv;
+
+ // force ourselves to get initialized from cache
+ // Don't care if it fails. this will fail the first time after
+ // migration, but we continue on. see #66018
+ (void)UpdateSummaryTotals(false);
+ }
+
+ return nsMsgDBFolder::GetSubFolders(folders);
+}
+
+// Makes sure the database is open and exists. If the database is valid then
+// returns NS_OK. Otherwise returns a failure error value.
+nsresult nsMsgNewsFolder::GetDatabase() {
+ nsresult rv;
+ if (!mDatabase) {
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the database, blowing it away if it's out of date.
+ rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase));
+ if (NS_FAILED(rv))
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mAddListener) rv = mDatabase->AddListener(this);
+
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = db->SetReadSet(mReadSet);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = UpdateSummaryTotals(true);
+ if (NS_FAILED(rv)) return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateFolder(nsIMsgWindow* aWindow) {
+ // Get news.get_messages_on_select pref
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool getMessagesOnSelect = true;
+ prefBranch->GetBoolPref("news.get_messages_on_select", &getMessagesOnSelect);
+
+ // Only if news.get_messages_on_select is true do we get new messages
+ // automatically
+ if (getMessagesOnSelect) {
+ rv = GetDatabase(); // want this cached...
+ if (NS_SUCCEEDED(rv)) {
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings;
+ nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings));
+ if (NS_SUCCEEDED(rv))
+ rv = mDatabase->ApplyRetentionSettings(retentionSettings, false);
+ }
+ rv = AutoCompact(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // GetNewMessages has to be the last rv set before we get to the next
+ // check, so that we'll have rv set to NS_MSG_ERROR_OFFLINE when offline
+ // and send a folder loaded notification to the front end.
+ rv = GetNewMessages(aWindow, nullptr);
+ }
+ if (rv != NS_MSG_ERROR_OFFLINE) return rv;
+ }
+ // We're not getting messages because either get_messages_on_select is
+ // false or we're offline. Send an immediate folder loaded notification.
+ NotifyFolderEvent(kFolderLoaded);
+ (void)RefreshSizeOnDisk();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ bool isNewsServer = false;
+ nsresult rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ // you can only subscribe to news servers, not news groups
+ *aResult = isNewsServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanFileMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // you can't file messages into a news server or news group
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't create subfolders on a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanRename(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't rename a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCompact(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't compact a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetFolderURL(nsACString& aUrl) {
+ nsCString hostName;
+ nsresult rv = GetHostname(hostName);
+ nsString groupName;
+ rv = GetName(groupName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = server->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+ const char* newsScheme =
+ (socketType == nsMsgSocketType::SSL) ? SNEWS_SCHEME : NEWS_SCHEME;
+ nsCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(groupName, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString tmpStr;
+ tmpStr.Adopt(PR_smprintf("%s//%s:%ld/%s", newsScheme, hostName.get(), port,
+ escapedName.get()));
+ aUrl.Assign(tmpStr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetNewsrcHasChanged(bool newsrcHasChanged) {
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+ return nntpServer->SetNewsrcHasChanged(newsrcHasChanged);
+}
+
+nsresult nsMsgNewsFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ nsMsgNewsFolder* newFolder = new nsMsgNewsFolder;
+ NS_ADDREF(*folder = newFolder);
+ newFolder->Init(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CreateSubfolder(const nsAString& newsgroupName,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (newsgroupName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsCOMPtr<nsIMsgFolder> child;
+ // Now let's create the actual new folder
+ rv = AddNewsgroup(NS_ConvertUTF16toUTF8(newsgroupName), EmptyCString(),
+ getter_AddRefs(child));
+
+ if (NS_SUCCEEDED(rv))
+ SetNewsrcHasChanged(true); // subscribe UI does this - but maybe we got
+ // here through auto-subscribe
+
+ if (NS_SUCCEEDED(rv) && child) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // Used to init some folder status of child.
+ rv = child->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NotifyFolderAdded(child);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(child);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DeleteStorage() {
+ nsresult rv = nsMsgDBFolder::DeleteStorage();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoString name;
+ rv = GetUnicodeName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->RemoveNewsgroup(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (void)RefreshSizeOnDisk();
+
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Rename(const nsAString& newName,
+ nsIMsgWindow* msgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) {
+ nsresult rv;
+
+ rv = nsMsgDBFolder::GetPrettyName(aAbbreviatedName);
+ if (NS_FAILED(rv)) return rv;
+
+ // only do this for newsgroup names, not for newsgroup hosts.
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool abbreviate = true;
+ rv = nntpServer->GetAbbreviate(&abbreviate);
+ if (NS_FAILED(rv)) return rv;
+
+ if (abbreviate)
+ rv = AbbreviatePrettyName(aAbbreviatedName, 1 /* hardcoded for now */);
+ }
+ return rv;
+}
+
+// original code from Oleg Rekutin
+// rekusha@asan.com
+// Public domain, created by Oleg Rekutin
+//
+// takes a newsgroup name, number of words from the end to leave unabberviated
+// the newsgroup name, will get reset to the following format:
+// x.x.x, where x is the first letter of each word and with the
+// exception of last 'fullwords' words, which are left intact.
+// If a word has a dash in it, it is abbreviated as a-b, where
+// 'a' is the first letter of the part of the word before the
+// dash and 'b' is the first letter of the part of the word after
+// the dash
+nsresult nsMsgNewsFolder::AbbreviatePrettyName(nsAString& prettyName,
+ int32_t fullwords) {
+ nsAutoString name(prettyName);
+ int32_t totalwords = 0; // total no. of words
+
+ // get the total no. of words
+ int32_t pos = 0;
+ while (1) {
+ pos = name.FindChar('.', pos);
+ if (pos == -1) {
+ totalwords++;
+ break;
+ } else {
+ totalwords++;
+ pos++;
+ }
+ }
+
+ // get the no. of words to abbreviate
+ int32_t abbrevnum = totalwords - fullwords;
+ if (abbrevnum < 1) return NS_OK; // nothing to abbreviate
+
+ // build the ellipsis
+ nsAutoString out;
+ out += name[0];
+
+ int32_t length = name.Length();
+ int32_t newword = 0; // == 2 if done with all abbreviated words
+
+ fullwords = 0;
+ char16_t currentChar;
+ for (int32_t i = 1; i < length; i++) {
+ // this temporary assignment is needed to fix an intel mac compiler bug.
+ // See Bug #327037 for details.
+ currentChar = name[i];
+ if (newword < 2) {
+ switch (currentChar) {
+ case '.':
+ fullwords++;
+ // check if done with all abbreviated words...
+ if (fullwords == abbrevnum)
+ newword = 2;
+ else
+ newword = 1;
+ break;
+ case '-':
+ newword = 1;
+ break;
+ default:
+ if (newword)
+ newword = 0;
+ else
+ continue;
+ }
+ }
+ out.Append(currentChar);
+ }
+ prettyName = out;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) {
+ NS_ENSURE_ARG_POINTER(folderInfo);
+ NS_ENSURE_ARG_POINTER(db);
+ nsresult openErr;
+ openErr = GetDatabase();
+ if (!mDatabase) {
+ *db = nullptr;
+ return openErr;
+ }
+
+ NS_ADDREF(*db = mDatabase);
+
+ if (NS_SUCCEEDED(openErr)) openErr = (*db)->GetDBFolderInfo(folderInfo);
+ return openErr;
+}
+
+/* this used to be MSG_FolderInfoNews::UpdateSummaryFromNNTPInfo() */
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateSummaryFromNNTPInfo(int32_t oldest, int32_t youngest,
+ int32_t total) {
+ NS_ENSURE_STATE(mReadSet);
+ /* First, mark all of the articles now known to be expired as read. */
+ if (oldest > 1) {
+ nsCString oldSet;
+ nsCString newSet;
+ mReadSet->Output(getter_Copies(oldSet));
+ mReadSet->AddRange(1, oldest - 1);
+ mReadSet->Output(getter_Copies(newSet));
+ }
+
+ /* Now search the newsrc line and figure out how many of these messages are
+ * marked as unread. */
+
+ /* make sure youngest is a least 1. MSNews seems to return a youngest of 0. */
+ if (youngest == 0) youngest = 1;
+
+ int32_t unread = mReadSet->CountMissingInRange(oldest, youngest);
+ NS_ASSERTION(unread >= 0, "CountMissingInRange reported unread < 0");
+ if (unread < 0)
+ // servers can send us stuff like "211 0 41 40 nz.netstatus"
+ // we should handle it gracefully.
+ unread = 0;
+
+ if (unread > total) {
+ /* This can happen when the newsrc file shows more unread than exist in the
+ * group (total is not necessarily `end - start'.) */
+ unread = total;
+ int32_t deltaInDB = mNumTotalMessages - mNumUnreadMessages;
+ // int32_t deltaInDB = m_totalInDB - m_unreadInDB;
+ /* if we know there are read messages in the db, subtract that from the
+ * unread total */
+ if (deltaInDB > 0) unread -= deltaInDB;
+ }
+
+ bool dbWasOpen = mDatabase != nullptr;
+ int32_t pendingUnreadDelta =
+ unread - mNumUnreadMessages - mNumPendingUnreadMessages;
+ int32_t pendingTotalDelta =
+ total - mNumTotalMessages - mNumPendingTotalMessages;
+ ChangeNumPendingUnread(pendingUnreadDelta);
+ ChangeNumPendingTotalMessages(pendingTotalDelta);
+ if (!dbWasOpen && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetExpungedBytesCount(int64_t* count) {
+ NS_ENSURE_ARG_POINTER(count);
+ *count = mExpungedBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+
+ *deletable = false;
+ // For legacy reasons, there can be Saved search folders under news accounts.
+ // Allow deleting those.
+ GetFlag(nsMsgFolderFlags::Virtual, deletable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RefreshSizeOnDisk() {
+ uint64_t oldFolderSize = mFolderSize;
+ // We set size to unknown to force it to get recalculated from disk.
+ mFolderSize = kSizeUnknown;
+ if (NS_SUCCEEDED(GetSizeOnDisk(&mFolderSize)))
+ NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ // If this is the rootFolder, return 0 as a safe value.
+ if (NS_FAILED(rv) || isServer) mFolderSize = 0;
+
+ // 0 is a valid folder size (meaning empty file with no offline messages),
+ // but 1 is not. So use -1 as a special value meaning no file size was fetched
+ // from disk yet.
+ if (mFolderSize == kSizeUnknown) {
+ nsCOMPtr<nsIFile> diskFile;
+ nsresult rv = GetFilePath(getter_AddRefs(diskFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there were no news messages downloaded for offline use, the folder
+ // file may not exist yet. In that case size is 0.
+ bool exists = false;
+ rv = diskFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ mFolderSize = 0;
+ } else {
+ int64_t fileSize;
+ rv = diskFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFolderSize = fileSize;
+ }
+ }
+
+ *size = mFolderSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHdrs,
+ nsIMsgWindow* aMsgWindow, bool deleteStorage,
+ bool isMove,
+ nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ if (!isMove) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableNotifications(allMessageCountNotifications, false);
+ if (NS_SUCCEEDED(rv)) {
+ for (auto msgHdr : msgHdrs) {
+ rv = mDatabase->DeleteHeader(msgHdr, nullptr, true, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+
+ if (!isMove)
+ NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+
+ (void)RefreshSizeOnDisk();
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelMessage(nsIMsgDBHdr* msgHdr,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ nsresult rv;
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // for cancel, we need to
+ // turn "newsmessage://sspitzer@news.mozilla.org/netscape.test#5428"
+ // into "news://sspitzer@news.mozilla.org/23423@netscape.com"
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverURI;
+ rv = server->GetServerURI(serverURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString messageID;
+ rv = msgHdr->GetMessageId(getter_Copies(messageID));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to escape the message ID,
+ // it might contain characters which will mess us up later, like #
+ // see bug #120502
+ nsCString escapedMessageID;
+ MsgEscapeString(messageID, nsINetUtil::ESCAPE_URL_PATH, escapedMessageID);
+
+ nsAutoCString cancelURL(serverURI.get());
+ cancelURL += '/';
+ cancelURL += escapedMessageID;
+ cancelURL += "?cancel";
+
+ nsCString messageURI;
+ rv = GetUriForMsg(msgHdr, messageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ return nntpService->CancelMessage(cancelURL, messageURI,
+ nullptr /* consumer */, nullptr, aMsgWindow,
+ getter_AddRefs(resultUri));
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNewMessages(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ return GetNewsMessages(aMsgWindow, false, aListener);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNextNMessages(nsIMsgWindow* aMsgWindow) {
+ return GetNewsMessages(aMsgWindow, true, nullptr);
+}
+
+nsresult nsMsgNewsFolder::GetNewsMessages(nsIMsgWindow* aMsgWindow,
+ bool aGetOld,
+ nsIUrlListener* aUrlListener) {
+ nsresult rv = NS_OK;
+
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isNewsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return server->PerformExpand(aMsgWindow);
+ }
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ rv = nntpService->GetNewNews(nntpServer, mURI, aGetOld, this, aMsgWindow,
+ getter_AddRefs(resultUri));
+ if (aUrlListener && NS_SUCCEEDED(rv) && resultUri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(resultUri));
+ if (msgUrl) msgUrl->RegisterListener(aUrlListener);
+ }
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::LoadNewsrcFileAndCreateNewsgroups() {
+ nsresult rv = NS_OK;
+ if (!mNewsrcFilePath) return NS_ERROR_FAILURE;
+
+ bool exists;
+ rv = mNewsrcFilePath->Exists(&exists);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!exists)
+ // it is ok for the newsrc file to not exist yet
+ return NS_OK;
+
+ nsCOMPtr<nsIInputStream> fileStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), mNewsrcFilePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(fileStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsCString line;
+
+ while (more && NS_SUCCEEDED(rv)) {
+ rv = lineInputStream->ReadLine(line, &more);
+ if (line.IsEmpty()) continue;
+ HandleNewsrcLine(line.get(), line.Length());
+ }
+
+ fileStream->Close();
+ return rv;
+}
+
+int32_t nsMsgNewsFolder::HandleNewsrcLine(const char* line,
+ uint32_t line_size) {
+ nsresult rv;
+
+ /* guard against blank line lossage */
+ if (line[0] == '#' || line[0] == '\r' || line[0] == '\n') return 0;
+
+ if ((line[0] == 'o' || line[0] == 'O') && !PL_strncasecmp(line, "options", 7))
+ return RememberLine(nsDependentCString(line));
+
+ const char* s = nullptr;
+ const char* setStr = nullptr;
+ const char* end = line + line_size;
+
+ for (s = line; s < end; s++)
+ if ((*s == ':') || (*s == '!')) break;
+
+ if (*s == 0) /* What is this?? Well, don't just throw it away... */
+ return RememberLine(nsDependentCString(line));
+
+ bool subscribed = (*s == ':');
+ setStr = s + 1;
+
+ if (*line == '\0') return 0;
+
+ // previous versions of Communicator polluted the
+ // newsrc files with articles
+ // (this would happen when you clicked on a link like
+ // news://news.mozilla.org/3746EF3F.6080309@netscape.com)
+ //
+ // legal newsgroup names can't contain @ or %
+ //
+ // News group names are structured into parts separated by dots,
+ // for example "netscape.public.mozilla.mail-news".
+ // Each part may be up to 14 characters long, and should consist
+ // only of letters, digits, "+" and "-", with at least one letter
+ //
+ // @ indicates an article and %40 is @ escaped.
+ // previous versions of Communicator also dumped
+ // the escaped version into the newsrc file
+ //
+ // So lines like this in a newsrc file should be ignored:
+ // 3746EF3F.6080309@netscape.com:
+ // 3746EF3F.6080309%40netscape.com:
+ if (PL_strchr(line, '@') || PL_strstr(line, "%40"))
+ // skipping, it contains @ or %40
+ subscribed = false;
+
+ if (subscribed) {
+ // we're subscribed, so add it
+ nsCOMPtr<nsIMsgFolder> child;
+
+ rv = AddNewsgroup(Substring(line, s), nsDependentCString(setStr),
+ getter_AddRefs(child));
+ if (NS_FAILED(rv)) return -1;
+ } else {
+ rv = RememberUnsubscribedGroup(nsDependentCString(line),
+ nsDependentCString(setStr));
+ if (NS_FAILED(rv)) return -1;
+ }
+
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr) {
+ mUnsubscribedNewsgroupLines.Append(newsgroup);
+ mUnsubscribedNewsgroupLines.AppendLiteral("! ");
+ if (!setStr.IsEmpty())
+ mUnsubscribedNewsgroupLines.Append(setStr);
+ else
+ mUnsubscribedNewsgroupLines.Append(MSG_LINEBREAK);
+ return NS_OK;
+}
+
+int32_t nsMsgNewsFolder::RememberLine(const nsACString& line) {
+ mOptionLines = line;
+ mOptionLines.Append(MSG_LINEBREAK);
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::ForgetLine() {
+ mOptionLines.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupUsername(nsACString& aGroupUsername) {
+ aGroupUsername = mGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupUsername(
+ const nsACString& aGroupUsername) {
+ mGroupUsername = aGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupPassword(nsACString& aGroupPassword) {
+ aGroupPassword = mGroupPassword;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupPassword(
+ const nsACString& aGroupPassword) {
+ mGroupPassword = aGroupPassword;
+ return NS_OK;
+}
+
+nsresult nsMsgNewsFolder::CreateNewsgroupUrlForSignon(const char* ref,
+ nsAString& result) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool singleSignon = true;
+ rv = nntpServer->GetSingleSignon(&singleSignon);
+
+ nsCOMPtr<nsIURL> url;
+ if (singleSignon) {
+ // Do not include username in the url when interacting with LoginManager.
+ nsCString serverURI = "news://"_ns;
+ nsCString hostName;
+ rv = server->GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ serverURI.Append(hostName);
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(serverURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int32_t port = 0;
+ rv = url->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (port <= 0) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ nsresult rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only set this for ssl newsgroups as for non-ssl connections, we don't
+ // need to specify the port as it is the default for the protocol and
+ // password manager "blanks" those out.
+ if (socketType == nsMsgSocketType::SSL) {
+ rv = NS_MutateURI(url)
+ .SetPort(nsINntpUrl::DEFAULT_NNTPS_PORT)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCString rawResult;
+ if (ref) {
+ rv = NS_MutateURI(url).SetRef(nsDependentCString(ref)).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = url->GetSpec(rawResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // If the url doesn't have a path, make sure we don't get a '/' on the end
+ // as that will confuse searching in password manager.
+ nsCString spec;
+ rv = url->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!spec.IsEmpty() && spec[spec.Length() - 1] == '/')
+ rawResult = StringHead(spec, spec.Length() - 1);
+ else
+ rawResult = spec;
+ }
+ result = NS_ConvertASCIItoUTF16(rawResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetAuthenticationCredentials(nsIMsgWindow* aMsgWindow,
+ bool mayPrompt, bool mustPrompt,
+ bool* validCredentials) {
+ // Not strictly necessary, but it would help consumers to realize that this is
+ // a rather nonsensical combination.
+ NS_ENSURE_FALSE(mustPrompt && !mayPrompt, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_ARG_POINTER(validCredentials);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString signonUrl;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we don't have a username or password, try to load it via the login mgr.
+ // Do this even if mustPrompt is true, to prefill the dialog.
+ if (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()) {
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (logins.Length() > 0) {
+ nsString uniUsername, uniPassword;
+ logins[0]->GetUsername(uniUsername);
+ logins[0]->GetPassword(uniPassword);
+ mGroupUsername = NS_LossyConvertUTF16toASCII(uniUsername);
+ mGroupPassword = NS_LossyConvertUTF16toASCII(uniPassword);
+
+ *validCredentials = true;
+ }
+ }
+
+ // Show the prompt if we need to
+ if (mustPrompt ||
+ (mayPrompt && (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()))) {
+ nsCOMPtr<nsIAuthPrompt> authPrompt =
+ do_GetService("@mozilla.org/messenger/msgAuthPrompt;1");
+ if (!authPrompt) {
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch) wwatch->GetNewAuthPrompter(0, getter_AddRefs(authPrompt));
+ if (!authPrompt) return NS_ERROR_FAILURE;
+ }
+
+ if (authPrompt) {
+ // Format the prompt text strings
+ nsString promptTitle, promptText;
+ bundle->GetStringFromName("enterUserPassTitle", promptTitle);
+
+ nsString serverName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ server->GetPrettyName(serverName);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool singleSignon = true;
+ nntpServer->GetSingleSignon(&singleSignon);
+
+ if (singleSignon) {
+ AutoTArray<nsString, 1> params = {serverName};
+ bundle->FormatStringFromName("enterUserPassServer", params, promptText);
+ } else {
+ AutoTArray<nsString, 2> params = {mName, serverName};
+ bundle->FormatStringFromName("enterUserPassGroup", params, promptText);
+ }
+
+ // Fill the signon url for the dialog
+ nsString signonURL;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Prefill saved username/password
+ char16_t* uniGroupUsername =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupUsername));
+ char16_t* uniGroupPassword =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupPassword));
+
+ // Prompt for the dialog
+ rv = authPrompt->PromptUsernameAndPassword(
+ promptTitle.get(), promptText.get(), signonURL.get(),
+ nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, &uniGroupUsername,
+ &uniGroupPassword, validCredentials);
+
+ nsAutoString uniPasswordAdopted, uniUsernameAdopted;
+ uniPasswordAdopted.Adopt(uniGroupPassword);
+ uniUsernameAdopted.Adopt(uniGroupUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only use the username/password if the user didn't cancel.
+ if (*validCredentials) {
+ SetGroupUsername(NS_LossyConvertUTF16toASCII(uniUsernameAdopted));
+ SetGroupPassword(NS_LossyConvertUTF16toASCII(uniPasswordAdopted));
+ } else {
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+ }
+ }
+ }
+
+ *validCredentials = !(mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::ForgetAuthenticationCredentials() {
+ nsString signonUrl;
+ nsresult rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // There should only be one-login stored for this url, however just in case
+ // there isn't.
+ for (uint32_t i = 0; i < logins.Length(); ++i)
+ loginMgr->RemoveLogin(logins[i]);
+
+ // Clear out the saved passwords for anyone else who tries to call.
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+
+ return NS_OK;
+}
+
+// change order of subfolders (newsgroups)
+NS_IMETHODIMP nsMsgNewsFolder::ReorderGroup(nsIMsgFolder* aNewsgroupToMove,
+ nsIMsgFolder* aRefNewsgroup) {
+ // if folders are identical do nothing
+ if (aNewsgroupToMove == aRefNewsgroup) return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ // get index for aNewsgroupToMove
+ int32_t indexNewsgroupToMove = mSubFolders.IndexOf(aNewsgroupToMove);
+ if (indexNewsgroupToMove == -1)
+ // aNewsgroupToMove is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // get index for aRefNewsgroup
+ int32_t indexRefNewsgroup = mSubFolders.IndexOf(aRefNewsgroup);
+ if (indexRefNewsgroup == -1)
+ // aRefNewsgroup is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // Move NewsgroupToMove to new index and set new sort order.
+
+ nsCOMPtr<nsIMsgFolder> newsgroup = mSubFolders[indexNewsgroupToMove];
+
+ mSubFolders.RemoveObjectAt(indexNewsgroupToMove);
+ mSubFolders.InsertObjectAt(newsgroup, indexRefNewsgroup);
+
+ for (uint32_t i = 0; i < mSubFolders.Length(); i++) {
+ mSubFolders[i]->SetSortOrder(kNewsSortOffset + i);
+ nsAutoString name;
+ mSubFolders[i]->GetName(name);
+ NotifyFolderRemoved(mSubFolders[i]);
+ NotifyFolderAdded(mSubFolders[i]);
+ }
+
+ // write changes back to file
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->SetNewsrcHasChanged(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->WriteNewsrcFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ return nsCreateNewsBaseMessageURI(aURI, mBaseMessageURI);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNewsrcLine(nsACString& newsrcLine) {
+ nsresult rv;
+ nsString newsgroupNameUtf16;
+ rv = GetName(newsgroupNameUtf16);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF16toUTF8 newsgroupName(newsgroupNameUtf16);
+
+ newsrcLine = newsgroupName;
+ newsrcLine.Append(':');
+
+ if (mReadSet) {
+ nsCString setStr;
+ mReadSet->Output(getter_Copies(setStr));
+ if (NS_SUCCEEDED(rv)) {
+ newsrcLine.Append(' ');
+ newsrcLine.Append(setStr);
+ newsrcLine.AppendLiteral(MSG_LINEBREAK);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetReadSetFromStr(const nsACString& newsrcLine) {
+ mReadSet = nsMsgKeySet::Create(PromiseFlatCString(newsrcLine).get());
+ NS_ENSURE_TRUE(mReadSet, NS_ERROR_OUT_OF_MEMORY);
+
+ // Now that mReadSet is recreated, make sure it's stored in the db as well.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) // it's ok not to have a db here.
+ db->SetReadSet(mReadSet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnsubscribedNewsgroupLines(
+ nsACString& aUnsubscribedNewsgroupLines) {
+ aUnsubscribedNewsgroupLines = mUnsubscribedNewsgroupLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetOptionLines(nsACString& optionLines) {
+ optionLines = mOptionLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::OnReadChanged(nsIDBChangeListener* aInstigator) {
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnicodeName(nsAString& aName) { return GetName(aName); }
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetRawName(nsACString& aRawName) {
+ nsresult rv;
+ if (mRawName.IsEmpty()) {
+ nsString name;
+ rv = GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // convert to the server-side encoding
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString dataCharset;
+ rv = nntpServer->GetCharset(dataCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsMsgI18NConvertFromUnicode(dataCharset, name, mRawName);
+
+ if (NS_FAILED(rv)) LossyCopyUTF16toASCII(name, mRawName);
+ }
+ aRawName = mRawName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNntpServer(nsINntpIncomingServer** result) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return rv;
+ nntpServer.forget(result);
+ return NS_OK;
+}
+
+// this gets called after the message actually gets cancelled
+// it removes the cancelled message from the db
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessage(nsMsgKey key) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a delete for a single message
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted({msgHdr.get()});
+ }
+ return mDatabase->DeleteMessage(key, nullptr, false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessages(
+ const nsTArray<nsMsgKey>& aMsgKeys) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a multiple message delete
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+
+ if (notifier) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrs;
+ rv = MsgGetHeadersFromKeys(mDatabase, aMsgKeys, msgHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ return mDatabase->DeleteMessages(aMsgKeys, nullptr);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelComplete() {
+ NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelFailed() {
+ NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSaveArticleOffline(bool* aBool) {
+ NS_ENSURE_ARG(aBool);
+ *aBool = m_downloadMessageForOfflineUse;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSaveArticleOffline(bool aBool) {
+ m_downloadMessageForOfflineUse = aBool;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ nsTArray<nsMsgKey> srcKeyArray;
+ SetSaveArticleOffline(true);
+ nsresult rv = NS_OK;
+
+ // build up message keys.
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = mDatabase->EnumerateMessages(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = enumerator->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ bool shouldStoreMsgOffline = false;
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ MsgFitsDownloadCriteria(msgKey, &shouldStoreMsgOffline);
+ if (shouldStoreMsgOffline) srcKeyArray.AppendElement(msgKey);
+ }
+ }
+ }
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(msgWindow, mDatabase, this);
+ rv = downloadState->DownloadArticles(msgWindow, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow* window) {
+ nsresult rv;
+ SetSaveArticleOffline(
+ true); // ### TODO need to clear this when we've finished
+ // build up message keys.
+ nsTArray<nsMsgKey> srcKeyArray(messages.Length());
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsMsgKey key;
+ rv = hdr->GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) srcKeyArray.AppendElement(key);
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(window, mDatabase, this);
+
+ rv = downloadState->DownloadArticles(window, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ nsMsgKey key;
+ hdr->GetMessageKey(&key);
+
+ uint64_t offset = 0;
+ uint32_t size = 0;
+ nsCOMPtr<nsIInputStream> rawStream;
+ nsresult rv =
+ GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(rawStream.forget(), offset,
+ uint64_t(size));
+ slicedStream.forget(stream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadBegin(nsMsgKey key) {
+ if (!m_downloadMessageForOfflineUse) {
+ return NS_OK;
+ }
+ nsresult rv = GetMessageHeader(key, getter_AddRefs(m_offlineHeader));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return StartNewOfflineMessage(); // Sets up m_tempMessageStream et al.
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadedLine(nsACString const& line) {
+ nsresult rv = NS_OK;
+ if (m_tempMessageStream) {
+ m_numOfflineMsgLines++;
+ uint32_t count = 0;
+ rv = m_tempMessageStream->Write(line.BeginReading(), line.Length(), &count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += count;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadEnd(nsresult status) {
+ if (m_tempMessageStream) {
+ return EndNewOfflineMessage(status);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyFinishedDownloadinghdrs() {
+ bool wasCached = !!mDatabase;
+ ChangeNumPendingTotalMessages(-mNumPendingTotalMessages);
+ ChangeNumPendingUnread(-mNumPendingUnreadMessages);
+ bool filtersRun;
+ // run the bayesian spam filters, if enabled.
+ CallFilterPlugins(nullptr, &filtersRun);
+
+ // If the DB was not open before, close our reference to it now.
+ if (!wasCached && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ // This also clears all of the cached headers that may have been added while
+ // we were downloading messages (and those clearing refcount cycles in the
+ // database).
+ mDatabase->ClearCachedHdrs();
+ mDatabase = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ rv = GetDatabase();
+ if (mDatabase) ApplyRetentionSettings();
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::ApplyRetentionSettings() {
+ return nsMsgDBFolder::ApplyRetentionSettings(false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetMessageIdForKey(nsMsgKey key,
+ nsACString& result) {
+ nsresult rv = GetDatabase();
+ if (!mDatabase) return rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString id;
+ rv = hdr->GetMessageId(getter_Copies(id));
+ result.Assign(id);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSortOrder(int32_t order) {
+ int32_t oldOrder = mSortOrder;
+ mSortOrder = order;
+
+ NotifyIntPropertyChanged(kSortOrder, oldOrder, order);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSortOrder(int32_t* order) {
+ NS_ENSURE_ARG_POINTER(order);
+ *order = mSortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Shutdown(bool shutdownChildren) {
+ if (mFilterList) {
+ // close the filter log stream
+ nsresult rv = mFilterList->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFilterList = nullptr;
+ }
+
+ mInitialized = false;
+ if (mReadSet) {
+ // the nsINewsDatabase holds a weak ref to the readset,
+ // and we outlive the db, so it's safe to delete it here.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) db->SetReadSet(nullptr);
+ mReadSet = nullptr;
+ }
+ return nsMsgDBFolder::Shutdown(shutdownChildren);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetFilterList(nsIMsgFilterList* aFilterList) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetFilterList(aFilterList);
+ }
+
+ mFilterList = aFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetFilterList(aMsgWindow, aResult);
+ }
+
+ if (!mFilterList) {
+ nsCOMPtr<nsIFile> thisFolder;
+ nsresult rv = GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> filterFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ;
+ rv = filterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // in 4.x, the news filter file was
+ // C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.dat where the
+ // summary file was C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.snm we make
+ // the rules file ".dat" in mozilla, so that migration works.
+
+ // NOTE:
+ // we don't we need to call NS_MsgHashIfNecessary()
+ // it's already been hashed, if necessary
+ nsCString filterFileName;
+ rv = filterFile->GetNativeLeafName(filterFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterFileName.AppendLiteral(".dat");
+
+ rv = filterFile->SetNativeLeafName(filterFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterService->OpenFilterList(filterFile, this, aMsgWindow,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ // We don't support pluggable filter list types for news.
+ return GetFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) {
+ return SetFilterList(aFilterList);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetIncomingServerType(nsACString& serverType) {
+ serverType.AssignLiteral("nntp");
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsFolder.h b/comm/mailnews/news/src/nsNewsFolder.h
new file mode 100644
index 0000000000..e18a768375
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ Interface for representing News folders.
+*/
+
+#ifndef nsMsgNewsFolder_h__
+#define nsMsgNewsFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBFolder.h"
+#include "nsIFile.h"
+#include "nsNewsUtils.h"
+#include "nsMsgKeySet.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFilterList.h"
+
+class nsMsgNewsFolder : public nsMsgDBFolder, public nsIMsgNewsFolder {
+ public:
+ nsMsgNewsFolder(void);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGNEWSFOLDER
+
+ // nsIMsgFolder methods:
+ NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override;
+
+ NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override;
+
+ NS_IMETHOD CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD DeleteStorage() override;
+ NS_IMETHOD Rename(const nsAString& newName, nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD GetAbbreviatedName(nsAString& aAbbreviatedName) override;
+
+ NS_IMETHOD GetFolderURL(nsACString& url) override;
+
+ NS_IMETHOD GetExpungedBytesCount(int64_t* count);
+ NS_IMETHOD GetDeletable(bool* deletable) override;
+
+ NS_IMETHOD RefreshSizeOnDisk();
+
+ NS_IMETHOD GetSizeOnDisk(int64_t* size) override;
+
+ NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) override;
+
+ NS_IMETHOD DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) override;
+ NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) override;
+
+ NS_IMETHOD GetCanSubscribe(bool* aResult) override;
+ NS_IMETHOD GetCanFileMessages(bool* aResult) override;
+ NS_IMETHOD GetCanCreateSubfolders(bool* aResult) override;
+ NS_IMETHOD GetCanRename(bool* aResult) override;
+ NS_IMETHOD GetCanCompact(bool* aResult) override;
+ NS_IMETHOD OnReadChanged(nsIDBChangeListener* aInstigator) override;
+
+ NS_IMETHOD DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* window) override;
+ NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) override;
+ NS_IMETHOD Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD GetSortOrder(int32_t* order) override;
+ NS_IMETHOD SetSortOrder(int32_t order) override;
+
+ NS_IMETHOD Shutdown(bool shutdownChildren) override;
+
+ NS_IMETHOD GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD SetFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD SetEditableFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD ApplyRetentionSettings() override;
+ NS_IMETHOD GetIncomingServerType(nsACString& serverType) override;
+
+ protected:
+ virtual ~nsMsgNewsFolder();
+ // helper routine to parse the URI and update member variables
+ nsresult AbbreviatePrettyName(nsAString& prettyName, int32_t fullwords);
+ nsresult ParseFolder(nsIFile* path);
+ nsresult CreateSubFolders(nsIFile* path);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult GetDatabase() override;
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+
+ nsresult LoadNewsrcFileAndCreateNewsgroups();
+ int32_t RememberLine(const nsACString& line);
+ nsresult RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr);
+ nsresult ForgetLine(void);
+ nsresult GetNewsMessages(nsIMsgWindow* aMsgWindow, bool getOld,
+ nsIUrlListener* aListener);
+
+ int32_t HandleNewsrcLine(const char* line, uint32_t line_size);
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override;
+
+ protected:
+ int64_t mExpungedBytes;
+ bool mGettingNews;
+ bool mInitialized;
+ bool m_downloadMessageForOfflineUse;
+
+ nsCString mOptionLines;
+ nsCString mUnsubscribedNewsgroupLines;
+ RefPtr<nsMsgKeySet> mReadSet;
+
+ nsCOMPtr<nsIFile> mNewsrcFilePath;
+
+ // used for auth news
+ nsCString mGroupUsername;
+ nsCString mGroupPassword;
+
+ // the name of the newsgroup.
+ nsCString mRawName;
+ int32_t mSortOrder;
+
+ private:
+ /**
+ * Constructs a signon url for use in login manager.
+ *
+ * @param ref The URI ref (should be null unless working with legacy).
+ * @param result The result of the string
+ */
+ nsresult CreateNewsgroupUrlForSignon(const char* ref, nsAString& result);
+ nsCOMPtr<nsIMsgFilterList> mFilterList;
+};
+
+#endif // nsMsgNewsFolder_h__
diff --git a/comm/mailnews/news/src/nsNewsUtils.cpp b/comm/mailnews/news/src/nsNewsUtils.cpp
new file mode 100644
index 0000000000..b9afed9c95
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nntpCore.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+/* parses NewsMessageURI */
+nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+
+ const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri);
+ int32_t keySeparator = uriStr.FindChar('#');
+ if (keySeparator != -1) {
+ int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "?&", keySeparator);
+
+ // Grab between the last '/' and the '#' for the key
+ group = StringHead(uriStr, keySeparator);
+ int32_t groupSeparator = group.RFind("/");
+ if (groupSeparator == -1) return NS_ERROR_FAILURE;
+
+ // Our string APIs don't let us unescape into the same buffer from earlier,
+ // so escape into a temporary
+ nsAutoCString unescapedGroup;
+ MsgUnescapeString(Substring(group, groupSeparator + 1), 0, unescapedGroup);
+ group = unescapedGroup;
+
+ nsAutoCString keyStr;
+ if (keyEndSeparator != -1)
+ keyStr = Substring(uriStr, keySeparator + 1,
+ keyEndSeparator - (keySeparator + 1));
+ else
+ keyStr = Substring(uriStr, keySeparator + 1);
+ nsresult errorCode;
+ *key = keyStr.ToInteger(&errorCode);
+
+ return errorCode;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI) {
+ nsAutoCString tailURI(baseURI);
+
+ // chop off news:/
+ if (tailURI.Find(kNewsRootURI) == 0) tailURI.Cut(0, PL_strlen(kNewsRootURI));
+
+ baseMessageURI = kNewsMessageRootURI;
+ baseMessageURI += tailURI;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsUtils.h b/comm/mailnews/news/src/nsNewsUtils.h
new file mode 100644
index 0000000000..601858de3f
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef NS_NEWSUTILS_H
+#define NS_NEWSUTILS_H
+
+#include "nsString.h"
+#include "MailNewsTypes2.h"
+
+#define kNewsRootURI "news:/"
+#define kNntpRootURI "nntp:/"
+#define kNewsMessageRootURI "news-message:/"
+#define kNewsURIGroupQuery "?group="
+#define kNewsURIKeyQuery "&key="
+
+#define kNewsRootURILen 6
+#define kNntpRootURILen 6
+#define kNewsMessageRootURILen 14
+#define kNewsURIGroupQueryLen 7
+#define kNewsURIKeyQueryLen 5
+
+extern nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key);
+
+extern nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI);
+
+#endif // NS_NEWSUTILS_H
diff --git a/comm/mailnews/news/src/nsNntpUrl.cpp b/comm/mailnews/news/src/nsNntpUrl.cpp
new file mode 100644
index 0000000000..ab7dcc08ab
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.cpp
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsNntpUrl.h"
+
+#include "nsString.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+#include "nntpCore.h"
+
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsINntpService.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsServiceManagerUtils.h"
+
+nsNntpUrl::nsNntpUrl() {
+ m_newsgroupPost = nullptr;
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ m_addDummyEnvelope = false;
+ m_canonicalLineEnding = false;
+ m_filePath = nullptr;
+ m_getOldMessages = false;
+ m_key = nsMsgKey_None;
+ mAutodetectCharset = false;
+}
+
+nsNntpUrl::~nsNntpUrl() {}
+
+NS_IMPL_ADDREF_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+NS_IMPL_RELEASE_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+
+NS_INTERFACE_MAP_BEGIN(nsNntpUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl)
+
+////////////////////////////////////////////////////////////////////////////////
+// Begin nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+/* News URI parsing explanation:
+ * We support 3 different news URI schemes, essentially boiling down to 8
+ * different formats:
+ * news://host/group
+ * news://host/message
+ * news://host/
+ * news:group
+ * news:message
+ * nntp://host/group
+ * nntp://host/group/key
+ * news-message://host/group#key
+ *
+ * In addition, we use queries on the news URIs with authorities for internal
+ * NNTP processing. The most important one is ?group=group&key=key, for cache
+ * canonicalization.
+ */
+
+nsresult nsNntpUrl::SetSpecInternal(const nsACString& aSpec) {
+ // For [s]news: URIs, we need to munge the spec if it is no authority, because
+ // the URI parser guesses the wrong thing otherwise
+ nsCString parseSpec(aSpec);
+ int32_t colon = parseSpec.Find(":");
+
+ // Our smallest scheme is 4 characters long, so colon must be at least 4
+ if (colon < 4 || colon + 1 == (int32_t)parseSpec.Length())
+ return NS_ERROR_MALFORMED_URI;
+
+ if (Substring(parseSpec, colon - 4, 4).EqualsLiteral("news") &&
+ parseSpec[colon + 1] != '/') {
+ // To make this parse properly, we add in three slashes, which convinces the
+ // parser that the authority component is empty.
+ parseSpec = Substring(aSpec, 0, colon + 1);
+ parseSpec.AppendLiteral("///");
+ parseSpec += Substring(aSpec, colon + 1);
+ }
+
+ nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(parseSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ rv = GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews"))
+ rv = ParseNewsURL();
+ else if (scheme.EqualsLiteral("nntp") || scheme.EqualsLiteral("nntps"))
+ rv = ParseNntpURL();
+ else if (scheme.EqualsLiteral("news-message")) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsParseNewsMessageURI(spec, m_group, &m_key);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ } else
+ return NS_ERROR_MALFORMED_URI;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = DetermineNewsAction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsNntpUrl::ParseNewsURL() {
+ // The path here is the group/msgid portion
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Drop the potential beginning from the path
+ if (path.Length() && path[0] == '/') path = Substring(path, 1);
+
+ // The presence of an `@' is a sign we have a msgid
+ if (path.Find("@") != -1 || path.Find("%40") != -1) {
+ MsgUnescapeString(path, 0, m_messageID);
+
+ // Set group, key for ?group=foo&key=123 uris
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t groupPos = spec.Find(kNewsURIGroupQuery); // find ?group=
+ int32_t keyPos = spec.Find(kNewsURIKeyQuery); // find &key=
+ if (groupPos != kNotFound && keyPos != kNotFound) {
+ // get group name and message key
+ m_group = Substring(spec, groupPos + kNewsURIGroupQueryLen,
+ keyPos - groupPos - kNewsURIGroupQueryLen);
+ nsCString keyStr(Substring(spec, keyPos + kNewsURIKeyQueryLen));
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ }
+ } else
+ MsgUnescapeString(path, 0, m_group);
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::ParseNntpURL() {
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (path.Length() > 0 && path[0] == '/') path = Substring(path, 1);
+
+ if (path.IsEmpty()) return NS_ERROR_MALFORMED_URI;
+
+ int32_t slash = path.FindChar('/');
+ if (slash == -1) {
+ m_group = path;
+ m_key = nsMsgKey_None;
+ } else {
+ m_group = Substring(path, 0, slash);
+ nsAutoCString keyStr;
+ keyStr = Substring(path, slash + 1);
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ // Keys must be at least one
+ if (m_key == 0) return NS_ERROR_MALFORMED_URI;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::DetermineNewsAction() {
+ nsAutoCString path;
+ nsresult rv = nsMsgMailNewsUrl::GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString query;
+ rv = GetQuery(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (query.EqualsLiteral("cancel")) {
+ m_newsAction = nsINntpUrl::ActionCancelArticle;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("list-ids")) {
+ m_newsAction = nsINntpUrl::ActionListIds;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("newgroups")) {
+ m_newsAction = nsINntpUrl::ActionListNewGroups;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "search"_ns)) {
+ m_newsAction = nsINntpUrl::ActionSearch;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "part="_ns) || query.Find("&part=") > 0) {
+ // news://news.mozilla.org:119/3B98D201.3020100%40cs.com?part=1
+ // news://news.mozilla.org:119/b58dme%24aia2%40ripley.netscape.com?header=print&part=1.2&type=image/jpeg&filename=Pole.jpg
+ m_newsAction = nsINntpUrl::ActionFetchPart;
+ return NS_OK;
+ }
+
+ if (!m_messageID.IsEmpty() || m_key != nsMsgKey_None) {
+ m_newsAction = nsINntpUrl::ActionFetchArticle;
+ return NS_OK;
+ }
+
+ if (m_group.Find("*") >= 0) {
+ // If the group is a wildmat, list groups instead of grabbing a group.
+ m_newsAction = nsINntpUrl::ActionListGroups;
+ return NS_OK;
+ }
+ if (!m_group.IsEmpty()) {
+ m_newsAction = nsINntpUrl::ActionGetNewNews;
+ return NS_OK;
+ }
+
+ // At this point, we have a URI that contains neither a query, a group, nor a
+ // message ID. Ergo, we don't know what it is.
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetGetOldMessages(bool aGetOldMessages) {
+ m_getOldMessages = aGetOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGetOldMessages(bool* aGetOldMessages) {
+ NS_ENSURE_ARG(aGetOldMessages);
+ *aGetOldMessages = m_getOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNewsAction(nsNewsAction* aNewsAction) {
+ if (aNewsAction) *aNewsAction = m_newsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetNewsAction(nsNewsAction aNewsAction) {
+ m_newsAction = aNewsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGroup(nsACString& group) {
+ group = m_group;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageID(nsACString& messageID) {
+ messageID = m_messageID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetKey(nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+ *key = m_key;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetUri(const nsACString& aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+// from nsIMsgMessageUrl
+NS_IMETHODIMP nsNntpUrl::GetUri(nsACString& aURI) {
+ nsresult rv = NS_OK;
+
+ // if we have been given a uri to associate with this url, then use it
+ // otherwise try to reconstruct a URI on the fly....
+ if (mURI.IsEmpty()) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mURI = spec;
+ }
+
+ aURI = mURI;
+ return rv;
+}
+
+NS_IMPL_GETSET(nsNntpUrl, AddDummyEnvelope, bool, m_addDummyEnvelope)
+NS_IMPL_GETSET(nsNntpUrl, CanonicalLineEnding, bool, m_canonicalLineEnding)
+
+NS_IMETHODIMP nsNntpUrl::SetMessageFile(nsIFile* aFile) {
+ m_messageFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageFile(nsIFile** aFile) {
+ if (aFile) NS_IF_ADDREF(*aFile = m_messageFile);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// End nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsNntpUrl::SetMessageToPost(nsINNTPNewsgroupPost* post) {
+ m_newsgroupPost = post;
+ if (post) SetNewsAction(nsINntpUrl::ActionPostArticle);
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::GetMessageToPost(nsINNTPNewsgroupPost** aPost) {
+ NS_ENSURE_ARG_POINTER(aPost);
+ NS_IF_ADDREF(*aPost = m_newsgroupPost);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService("@mozilla.org/messenger/messageservice;1?type=news", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec(mOriginalSpec);
+ if (spec.IsEmpty()) {
+ // Handle the case where necko directly runs an internal news:// URL,
+ // one that looks like news://host/message-id?group=mozilla.announce&key=15
+ // Other sorts of URLs -- e.g. news://host/message-id -- will not succeed.
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return msgService->MessageURIToMsgHdr(spec, aMsgHdr);
+}
+
+NS_IMETHODIMP nsNntpUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+
+ switch (type) {
+ case nsIMsgMailNewsUrl::eDisplay:
+ *isType = (m_newsAction == nsINntpUrl::ActionFetchArticle);
+ break;
+ default:
+ *isType = false;
+ };
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetOriginalSpec(nsACString& aSpec) {
+ aSpec = mOriginalSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::SetOriginalSpec(const nsACString& aSpec) {
+ mOriginalSpec = aSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetServer(nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsresult rv;
+ nsAutoCString scheme, user, host;
+
+ GetScheme(scheme);
+ GetUsername(user);
+ GetHost(host);
+
+ // No authority -> no server
+ if (host.IsEmpty()) {
+ *aServer = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aServer = nullptr;
+ accountManager->FindServer(user, host, "nntp"_ns, 0, aServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetFolder(nsIMsgFolder** msgFolder) {
+ NS_ENSURE_ARG_POINTER(msgFolder);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Need a server and a group to get the folder
+ if (!server || m_group.IsEmpty()) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ // Find the group on the server
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasGroup = false;
+ rv = nntpServer->ContainsNewsgroup(m_group, &hasGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!hasGroup) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder;
+ rv = nntpServer->FindGroup(m_group, getter_AddRefs(newsFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsFolder->QueryInterface(NS_GET_IID(nsIMsgFolder),
+ (void**)msgFolder);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetAutodetectCharset(bool* aAutodetectCharset) {
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetAutodetectCharset(bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::Clone(nsIURI** _retval) {
+ nsresult rv;
+ rv = nsMsgMailNewsUrl::Clone(_retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageUrl> newsurl = do_QueryInterface(*_retval, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsurl->SetUri(mURI);
+}
+
+nsresult nsNntpUrl::NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> nntpUri =
+ do_CreateInstance("@mozilla.org/messenger/nntpurl;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ aBaseURI->Resolve(aSpec, newSpec);
+ rv = nntpUri->SetSpecInternal(newSpec);
+ // XXX Consider: rv = NS_MutateURI(new
+ // nsNntpUrl::Mutator()).SetSpec(newSpec).Finalize(nntpUri);
+ } else {
+ rv = nntpUri->SetSpecInternal(aSpec);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nntpUri.forget(_retval);
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNntpUrl.h b/comm/mailnews/news/src/nsNntpUrl.h
new file mode 100644
index 0000000000..eab52cb1c5
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef nsNntpUrl_h__
+#define nsNntpUrl_h__
+
+#include "nsINntpUrl.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsINNTPNewsgroupPost.h"
+#include "nsIFile.h"
+
+class nsNntpUrl : public nsINntpUrl,
+ public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public nsIMsgI18NUrl {
+ public:
+ NS_DECL_NSINNTPURL
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_NSIMSGI18NURL
+
+ // nsMsgMailNewsUrl overrides
+ nsresult SetSpecInternal(const nsACString& aSpec) override;
+ nsresult Clone(nsIURI** _retval) override;
+
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+
+ // nsIMsgMailNewsUrl overrides
+ NS_IMETHOD GetServer(nsIMsgIncomingServer** server) override;
+ NS_IMETHOD GetFolder(nsIMsgFolder** msgFolder) override;
+
+ // nsNntpUrl
+ nsNntpUrl();
+ static nsresult NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ private:
+ virtual ~nsNntpUrl();
+ nsresult DetermineNewsAction();
+ nsresult ParseNewsURL();
+ nsresult ParseNntpURL();
+
+ nsCOMPtr<nsINNTPNewsgroupPost> m_newsgroupPost;
+ nsNewsAction m_newsAction; // the action this url represents...parse mailbox,
+ // display messages, etc.
+
+ nsCString mURI; // the RDF URI associated with this url.
+ bool mAutodetectCharset; // used by nsIMsgI18NUrl...
+
+ nsCString mOriginalSpec;
+ nsCOMPtr<nsIFile> m_filePath;
+
+ // used by save message to disk
+ nsCOMPtr<nsIFile> m_messageFile;
+
+ bool m_addDummyEnvelope;
+ bool m_canonicalLineEnding;
+ bool m_getOldMessages;
+
+ nsCString m_group;
+ nsCString m_messageID;
+ nsMsgKey m_key;
+};
+
+#endif // nsNntpUrl_h__
diff --git a/comm/mailnews/news/test/moz.build b/comm/mailnews/news/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/news/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/news/test/unit/head_server_setup.js b/comm/mailnews/news/test/unit/head_server_setup.js
new file mode 100644
index 0000000000..6a5bcbda9f
--- /dev/null
+++ b/comm/mailnews/news/test/unit/head_server_setup.js
@@ -0,0 +1,244 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var test = null;
+
+// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo.
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+// Import the servers
+var { fsDebugAll, gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var {
+ NewsArticle,
+ NNTP_Giganews_handler,
+ NNTP_RFC2980_handler,
+ NNTP_RFC4643_extension,
+ NNTP_RFC977_handler,
+ NntpDaemon,
+} = ChromeUtils.import("resource://testing-common/mailnews/Nntpd.jsm");
+
+var kSimpleNewsArticle =
+ "From: John Doe <john.doe@example.com>\n" +
+ "Date: Sat, 24 Mar 1990 10:59:24 -0500\n" +
+ "Newsgroups: test.subscribe.simple\n" +
+ "Subject: H2G2 -- What does it mean?\n" +
+ "Message-ID: <TSS1@nntp.invalid>\n" +
+ "\n" +
+ "What does the acronym H2G2 stand for? I've seen it before...\n";
+
+// The groups to set up on the fake server.
+// It is an array of tuples, where the first element is the group name and the
+// second element is whether or not we should subscribe to it.
+var groups = [
+ ["misc.test", false],
+ ["test.empty", false],
+ ["test.subscribe.empty", true],
+ ["test.subscribe.simple", true],
+ ["test.filter", true],
+];
+// Sets up the NNTP daemon object for use in fake server
+function setupNNTPDaemon() {
+ var daemon = new NntpDaemon();
+
+ groups.forEach(function (element) {
+ daemon.addGroup(element[0]);
+ });
+
+ var auto_add = do_get_file("postings/auto-add/");
+ var files = [...auto_add.directoryEntries];
+
+ files.sort(function (a, b) {
+ if (a.leafName == b.leafName) {
+ return 0;
+ }
+ return a.leafName < b.leafName ? -1 : 1;
+ });
+
+ files.forEach(function (file) {
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ sstream.init(fstream);
+
+ var post = "";
+ let part = sstream.read(4096);
+ while (part.length > 0) {
+ post += part;
+ part = sstream.read(4096);
+ }
+ sstream.close();
+ fstream.close();
+ daemon.addArticle(new NewsArticle(post));
+ });
+
+ var article = new NewsArticle(kSimpleNewsArticle);
+ daemon.addArticleToGroup(article, "test.subscribe.simple", 1);
+
+ return daemon;
+}
+
+function makeServer(handler, daemon) {
+ function createHandler(d) {
+ return new handler(d);
+ }
+ return new nsMailServer(createHandler, daemon);
+}
+
+// Enable strict threading
+Services.prefs.setBoolPref("mail.strict_threading", true);
+
+// Make sure we don't try to use a protected port. I like adding 1024 to the
+// default port when doing so...
+var NNTP_PORT = 1024 + 119;
+
+var _server = null;
+var _account = null;
+
+function subscribeServer(incomingServer) {
+ // Subscribe to newsgroups
+ incomingServer.QueryInterface(Ci.nsINntpIncomingServer);
+ groups.forEach(function (element) {
+ if (element[1]) {
+ incomingServer.subscribeToNewsgroup(element[0]);
+ }
+ });
+ // Only allow one connection
+ incomingServer.maximumConnectionsNumber = 1;
+}
+
+// Sets up the client-side portion of fakeserver
+function setupLocalServer(port, host = "localhost") {
+ if (_server != null) {
+ return _server;
+ }
+ let serverAndAccount = localAccountUtils.create_incoming_server_and_account(
+ "nntp",
+ port,
+ null,
+ null,
+ host
+ );
+ let server = serverAndAccount.server;
+ subscribeServer(server);
+
+ _server = server;
+ _account = serverAndAccount.account;
+
+ return server;
+}
+
+// Sets up a protocol object and prepares to run the test for the news url
+function setupProtocolTest(port, newsUrl, incomingServer) {
+ var url;
+ if (newsUrl instanceof Ci.nsIMsgMailNewsUrl) {
+ url = newsUrl;
+ } else {
+ url = Services.io.newURI(newsUrl);
+ }
+
+ var newsServer = incomingServer;
+ if (!newsServer) {
+ newsServer = setupLocalServer(port);
+ }
+
+ var listener = {
+ onStartRequest() {},
+ onStopRequest() {
+ if (!this.called) {
+ this.called = true;
+ newsServer.closeCachedConnections();
+ this.called = false;
+ }
+ },
+ onDataAvailable() {},
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ };
+ listener.called = false;
+ newsServer.loadNewsUrl(url, null, listener);
+}
+
+function create_post(baseURL, file) {
+ var url = Services.io.newURI(baseURL);
+ url.QueryInterface(Ci.nsINntpUrl);
+
+ var post = Cc["@mozilla.org/messenger/nntpnewsgrouppost;1"].createInstance(
+ Ci.nsINNTPNewsgroupPost
+ );
+ post.postMessageFile = do_get_file(file);
+ url.messageToPost = post;
+ return url;
+}
+
+function resetFolder(folder) {
+ var headers = [...folder.messages];
+
+ var db = folder.msgDatabase;
+ db.dBFolderInfo.knownArtsSet = "";
+ for (var header of headers) {
+ db.deleteHeader(header, null, true, false);
+ }
+ dump("resetting folder\n");
+ folder.msgDatabase = null;
+}
+
+function do_check_transaction(real, expected) {
+ if (Array.isArray(real)) {
+ real = real.at(-1);
+ }
+
+ // real.them may have an extra QUIT on the end, where the stream is only
+ // closed after we have a chance to process it and not them. We therefore
+ // excise this from the list
+ if (real.them[real.them.length - 1] == "QUIT") {
+ real.them.pop();
+ }
+
+ Assert.equal(real.them.join(","), expected.join(","));
+ dump("Passed test " + test + "\n");
+}
+
+function make_article(file) {
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ sstream.init(fstream);
+
+ var post = "";
+ let part = sstream.read(4096);
+ while (part.length > 0) {
+ post += part;
+ part = sstream.read(4096);
+ }
+ sstream.close();
+ fstream.close();
+ return new NewsArticle(post);
+}
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post1.eml b/comm/mailnews/news/test/unit/postings/auto-add/post1.eml
new file mode 100644
index 0000000000..9e1f3e7265
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post1.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: First post!
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <1@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:1
+
+This is the first body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post2.eml b/comm/mailnews/news/test/unit/postings/auto-add/post2.eml
new file mode 100644
index 0000000000..d5a4591bb3
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post2.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Odd Subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <2@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:2
+
+This is the second body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post3.eml b/comm/mailnews/news/test/unit/postings/auto-add/post3.eml
new file mode 100644
index 0000000000..fdfc2d69c1
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post3.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Odd Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <3@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:3
+
+This is the third body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post4.eml b/comm/mailnews/news/test/unit/postings/auto-add/post4.eml
new file mode 100644
index 0000000000..11a53b7035
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post4.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Sat, 1 Jan 2000 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <4@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:4
+
+This is the fourth body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post5.eml b/comm/mailnews/news/test/unit/postings/auto-add/post5.eml
new file mode 100644
index 0000000000..55ba89c022
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post5.eml
@@ -0,0 +1,50 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <5@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:5
+Bytes: 2057
+
+This is the fifth body post, with extra special padding to make sure
+that no one else has the same length as it. You see, we have to ensure
+that we have at least two KB of data because search is stupid and
+searches in terms of KB. If we didn't, we'd be stuck with a lot of
+messages merely matching 1, so we use this to pad the size.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+.
+
+But that wasn't all :-)
+
+--
+A signature for the heck of it.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post6.eml b/comm/mailnews/news/test/unit/postings/auto-add/post6.eml
new file mode 100644
index 0000000000..fe9d536203
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post6.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <6.odd@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:6
+
+This is the sixth body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post7.eml b/comm/mailnews/news/test/unit/postings/auto-add/post7.eml
new file mode 100644
index 0000000000..f12b708a9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post7.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Odd/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <7@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:7
+
+This is the seventh body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post8.eml b/comm/mailnews/news/test/unit/postings/auto-add/post8.eml
new file mode 100644
index 0000000000..265b1f5a4a
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post8.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Odd/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <8.unread@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:8
+
+This is the eight body post.
diff --git a/comm/mailnews/news/test/unit/postings/bug403242.eml b/comm/mailnews/news/test/unit/postings/bug403242.eml
new file mode 100644
index 0000000000..741ccf8624
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/bug403242.eml
@@ -0,0 +1,25 @@
+Message-ID: <89550684-f0df-4e9a-aea6-07645e238dd2>
+Subject: Attachment test
+From: =?ISO-8859-1?Q?Marcin_Miko=3Fajczak?= <marcin.mikolajczak@hotmail.invalid>
+Date: 2007-11-09 18:37:09
+Newsgroups: test1
+Content-Type: multipart/mixed; boundary="------------060805070701070202060104"
+MIME-Version: 1.0
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.6) Gecko/20070728 Thunderbird/2.0.0.6 Mnenhy/0.7.5.666
+
+This is a multi-part message in MIME format.
+--------------060805070701070202060104
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+This message has an attachment.
+
+--------------060805070701070202060104
+Content-Type: text/plain;
+ name="Test.txt"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="Test.txt"
+
+VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudC4=
+--------------060805070701070202060104--
diff --git a/comm/mailnews/news/test/unit/postings/bug670935.eml b/comm/mailnews/news/test/unit/postings/bug670935.eml
new file mode 100644
index 0000000000..7762586a2f
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/bug670935.eml
@@ -0,0 +1,7 @@
+From: Robert Kagan <Robert.Kagan@test.invalid>
+Date: Fri, 24 Mar 1989 00:04:18 -0900
+Newsgroups: test.malformed&name
+Subject: I think we have a problem
+
+This is an automated report. There may be some structural damage in
+sectors 10 and 9. Please respond to confirm.
diff --git a/comm/mailnews/news/test/unit/postings/post1.eml b/comm/mailnews/news/test/unit/postings/post1.eml
new file mode 100644
index 0000000000..b767aca453
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post1.eml
@@ -0,0 +1,7 @@
+From: Robert Kagan <Robert.Kagan@test.invalid>
+Date: Fri, 24 Mar 1989 00:04:18 -0900
+Newsgroups: test.news.problems test.news.fictional
+Subject: I think we have a problem
+
+This is an automated report. There may be some structural damage in
+sectors 10 and 9. Please respond to confirm.
diff --git a/comm/mailnews/news/test/unit/postings/post2.eml b/comm/mailnews/news/test/unit/postings/post2.eml
new file mode 100644
index 0000000000..c64818c6df
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post2.eml
@@ -0,0 +1,6 @@
+From: "Demo User" <nobody@example.net>
+Newsgroups: misc.test
+Subject: I am just a test article
+Organization: An Example Net
+
+This is just a test article.
diff --git a/comm/mailnews/news/test/unit/postings/post3.eml b/comm/mailnews/news/test/unit/postings/post3.eml
new file mode 100644
index 0000000000..7769fbf3d7
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post3.eml
@@ -0,0 +1,10 @@
+From: "Demo User" <nobody@example.net>
+Message-ID: <2@dot.invalid>
+Newsgroups: dot.test
+Subject: Bug 170727: Test article with dots
+
+. 1 dot
+.. 2 dots
+ .indented dot
+.
+no - that wasn't the end of the message!
diff --git a/comm/mailnews/news/test/unit/test_NntpChannel.js b/comm/mailnews/news/test/unit/test_NntpChannel.js
new file mode 100644
index 0000000000..0da7963c78
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_NntpChannel.js
@@ -0,0 +1,74 @@
+/* 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 { NntpChannel } = ChromeUtils.import("resource:///modules/NntpChannel.jsm");
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+let server;
+
+add_setup(function setup() {
+ let daemon = setupNNTPDaemon();
+ server = new nsMailServer(() => {
+ let handler = new NNTP_RFC977_handler(daemon);
+ // Test NntpClient works with 201 response.
+ handler.onStartup = () => {
+ return "201 posting prohibited";
+ };
+ return handler;
+ }, daemon);
+ server.start(NNTP_PORT);
+ registerCleanupFunction(() => {
+ server.stop();
+ });
+
+ setupLocalServer(NNTP_PORT);
+});
+
+/**
+ * Test a ?list-ids news url will trigger LISTGROUP request.
+ */
+add_task(async function test_listIds() {
+ // Init the uri and streamListener.
+ let uri = Services.io.newURI(
+ `news://localhost:${NNTP_PORT}/test.filter?list-ids`
+ );
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Run the uri with NntpChannel.
+ let channel = new NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ await streamListener.promise;
+
+ // Test LISTGROUP request was sent correctly.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER", "LISTGROUP test.filter"]);
+});
+
+/**
+ * Test a ?group=name&key=x news url will trigger ARTICLE request.
+ */
+add_task(async function test_fetchArticle() {
+ _server.closeCachedConnections();
+
+ // Init the uri and streamListener.
+ let uri = Services.io.newURI(
+ `news://localhost:${NNTP_PORT}?group=test.filter&key=1`
+ );
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Run the uri with NntpChannel.
+ let channel = new NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ await streamListener.promise;
+
+ // Test ARTICLE request was sent correctly.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "ARTICLE 1",
+ ]);
+});
diff --git a/comm/mailnews/news/test/unit/test_biff.js b/comm/mailnews/news/test/unit/test_biff.js
new file mode 100644
index 0000000000..ba6266ef25
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_biff.js
@@ -0,0 +1,57 @@
+// This tests that we can execute biff properly, specifically that filters are
+// run during biff, producing correct counts.
+
+/* import-globals-from ../../../test/resources/filterTestUtils.js */
+load("../../../resources/filterTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Set up the server and add in filters
+ let daemon = setupNNTPDaemon();
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ // Remove all but the test.filter folder
+ let rootFolder = localserver.rootFolder;
+ for (let folder of rootFolder.subFolders) {
+ if (folder.name != "test.filter") {
+ rootFolder.propagateDelete(folder, true);
+ }
+ }
+
+ // Create a filter to mark one message read.
+ let filters = localserver.getFilterList(null);
+ filters.loggingEnabled = true;
+ createFilter(filters, "subject", "Odd", "read");
+ localserver.setFilterList(filters);
+
+ // This is a bit hackish, but we don't have any really functional callbacks
+ // for biff. Instead, we use the notifier to look for all 7 messages to be
+ // added and take that as our sign that the download is finished.
+ let expectCount = 7,
+ seen = 0;
+ let listener = {
+ msgAdded() {
+ if (++seen == expectCount) {
+ localserver.closeCachedConnections();
+ }
+ },
+ };
+ MailServices.mfn.addListener(
+ listener,
+ Ci.nsIMsgFolderNotificationService.msgAdded
+ );
+ localserver.performBiff(null);
+ server.performTest();
+ MailServices.mfn.removeListener(listener);
+
+ // We marked, via our filters, one of the messages read. So if we do not
+ // have 1 read message, either we're not running the filters on biff, or the
+ // filters aren't working. This is disambiguated by the test_filter.js test.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), folder.getNumUnread(false) + 1);
+ server.stop();
+}
diff --git a/comm/mailnews/news/test/unit/test_bug170727.js b/comm/mailnews/news/test/unit/test_bug170727.js
new file mode 100644
index 0000000000..50bda7b096
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug170727.js
@@ -0,0 +1,61 @@
+// Bug 170727 - Remove the escaped dot from body lines before saving in the offline store.
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_task(async function testloadMessage() {
+ let daemon = setupNNTPDaemon();
+ daemon.addGroup("dot.test");
+ daemon.addArticle(make_article(do_get_file("postings/post3.eml")));
+
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ localserver.subscribeToNewsgroup("dot.test");
+
+ let folder = localserver.rootFolder.getChildNamed("dot.test");
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ let uri = folder.generateMessageURI(1);
+ let msgService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=news"
+ ].getService(Ci.nsIMsgMessageService);
+
+ // Pretend to display the message: During the first run, the article is downloaded,
+ // displayed directly and simultaneously saved in the offline storage.
+ {
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<2@dot.invalid>").fullText
+ );
+ }
+
+ // In the second run, the offline store serves as the source of the article.
+ {
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<2@dot.invalid>").fullText
+ );
+ }
+
+ server.stop();
+});
diff --git a/comm/mailnews/news/test/unit/test_bug37465.js b/comm/mailnews/news/test/unit/test_bug37465.js
new file mode 100644
index 0000000000..e3d61c9dbe
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug37465.js
@@ -0,0 +1,49 @@
+// Bug 37465 -- assertions with no accounts
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(async function textChannelAsync() {
+ let daemon = setupNNTPDaemon();
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+
+ // Correct URI?
+ let uri = Services.io.newURI(
+ "news://localhost:" + server.port + "/1@regular.invalid"
+ );
+ let newsUri = uri
+ .QueryInterface(Ci.nsINntpUrl)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ Assert.equal(uri.port, server.port);
+ Assert.equal(newsUri.server, null);
+ Assert.equal(newsUri.messageID, "1@regular.invalid");
+ Assert.equal(newsUri.folder, null);
+
+ // Run the URI and make sure we get the message
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ channel.asyncOpen(listener, null);
+ let msgText = await listener.promise;
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<1@regular.invalid>").fullText
+ );
+
+ // Shut down connections
+ MailServices.accounts.closeCachedConnections();
+ server.stop();
+});
diff --git a/comm/mailnews/news/test/unit/test_bug403242.js b/comm/mailnews/news/test/unit/test_bug403242.js
new file mode 100644
index 0000000000..54a385da78
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug403242.js
@@ -0,0 +1,55 @@
+// Bug 403242 stems from invalid message ids
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_task(async function test403242() {
+ let daemon = setupNNTPDaemon();
+ daemon.addGroup("test1");
+ daemon.addArticle(make_article(do_get_file("postings/bug403242.eml")));
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ localserver.subscribeToNewsgroup("test1");
+
+ let folder = localserver.rootFolder.getChildNamed("test1");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ // Fetch the message
+ let uri = folder.generateMessageURI(1);
+ let msgService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=news"
+ ].getService(Ci.nsIMsgMessageService);
+
+ // Does the URL lie to us?
+ let neckoUrl = msgService.getUrlForUri(uri).QueryInterface(Ci.nsINntpUrl);
+ Assert.equal(neckoUrl.newsAction, Ci.nsINntpUrl.ActionFetchArticle);
+
+ // Pretend to display the message
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+ server.stop();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getGroup("test1")[1].fullText
+ );
+
+ // No illegal commands?
+ test = "bug 403242";
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction[transaction.length - 1], [
+ "MODE READER",
+ "GROUP test1",
+ "ARTICLE 1",
+ ]);
+});
diff --git a/comm/mailnews/news/test/unit/test_bug540288.js b/comm/mailnews/news/test/unit/test_bug540288.js
new file mode 100644
index 0000000000..9ffe8a7616
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug540288.js
@@ -0,0 +1,114 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Tests that an empty cache entry doesn't return an empty message for news. */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+var streamListener = {
+ _data: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ // nsIRequestObserver
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ Assert.equal(aStatusCode, 0);
+
+ // Reduce any \r\n to just \n so we can do a good comparison on any
+ // platform.
+ var reduced = this._data.replace(/\r\n/g, "\n");
+ Assert.equal(reduced, kSimpleNewsArticle);
+
+ // We must finish closing connections and tidying up after a timeout
+ // so that the thread has time to unwrap itself.
+ do_timeout(0, doTestFinished);
+ },
+
+ // nsIStreamListener
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ let scriptStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+
+ scriptStream.init(aInputStream);
+
+ this._data += scriptStream.read(aCount);
+ },
+};
+
+function doTestFinished() {
+ localserver.closeCachedConnections();
+
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+ var uri = Services.io.newURI(
+ "news://localhost:" + server.port + "/TSS1%40nntp.test"
+ );
+
+ try {
+ // Add an empty message to the cache
+ MailServices.nntp.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ {
+ onCacheEntryAvailable(cacheEntry, isNew, status) {
+ Assert.equal(status, Cr.NS_OK);
+
+ cacheEntry.markValid();
+
+ // Get the folder and new mail
+ var folder = localserver.rootFolder.getChildNamed(
+ "test.subscribe.simple"
+ );
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ var message = folder.firstNewMessage;
+
+ var messageUri = folder.getUriForMsg(message);
+
+ Cc["@mozilla.org/messenger/messageservice;1?type=news"]
+ .getService(Ci.nsIMsgMessageService)
+ .loadMessage(messageUri, streamListener, null, null, false);
+
+ // Get the server to run
+ server.performTest();
+ },
+ }
+ );
+
+ do_test_pending();
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_bug695309.js b/comm/mailnews/news/test/unit/test_bug695309.js
new file mode 100644
index 0000000000..d51587ba08
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug695309.js
@@ -0,0 +1,121 @@
+/* Tests the connection mayhem found by bug 695309 */
+
+// The full bug requires several things to fall into place:
+// 1. Cause the connections to timeout, while keeping them in the cache.
+// 2. Enqueue enough requests to cause things to be placed in the pending queue.
+// 3. Commands try to run but die instead.
+// 4. Enqueue more requests to open up new connections.
+// 5. When loading, the connection ends up pulling somebody from the queue and
+// ends up treating the response for the prior command as the current
+// response.
+// 6. This causes, in particular, GROUP to read the logon string as the response
+// (where sprintf clears everything to 0), and AUTHINFO to think credentials
+// are wrong. The bug's description is then caused by the next read seeing
+// a large number of (not really) new messages.
+// For the purposes of this test, we read enough to see if the group command is
+// being misread or not, as it is complicated enough.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+
+var daemon, localserver, server;
+var highWater = 0;
+
+add_setup(async function () {
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ // Bug 1050840:
+ // Check if invalid value of the max_cached_connections pref
+ // is properly folded into a sane value.
+ localserver.maximumConnectionsNumber = -5;
+ Assert.equal(localserver.maximumConnectionsNumber, 1);
+
+ localserver.maximumConnectionsNumber = 0;
+ Assert.equal(localserver.maximumConnectionsNumber, 2);
+
+ localserver.maximumConnectionsNumber = 2;
+});
+
+add_task(async function test_newMsgs() {
+ // Start by initializing the folder, and mark some messages as read.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), 0);
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ // Do another folder to use up both connections
+ localserver.rootFolder
+ .getChildNamed("test.subscribe.simple")
+ .getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).setReadSetFromStr("1-3");
+ Assert.equal(folder.getTotalMessages(false) - folder.getNumUnread(false), 3);
+ highWater = folder.getTotalMessages(false);
+ Assert.equal(folder.msgDatabase.dBFolderInfo.highWater, highWater);
+});
+
+add_task(async function trigger_bug() {
+ // Kill the connection and start it up again.
+ server.stop();
+ server.start();
+
+ // Get new messages for all folders. Once we've seen one folder, trigger a
+ // load of the folder in question. This second load should, if the bug is
+ // present, be overwritten with one from the load queue that causes the
+ // confusion. It then loads it again, and should (before the patch that fixes
+ // this) read the 200 logon instead of the 211 group.
+ let testFolder = localserver.rootFolder.getChildNamed("test.filter");
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ let promiseFolderEvent = function (folder, event) {
+ return new Promise((resolve, reject) => {
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderEvent(aEventFolder, aEvent) {
+ if (
+ aEvent == "FolderLoaded" &&
+ aEventFolder.prettyName == "test.subscribe.simple"
+ ) {
+ aEventFolder.getNewMessages(null, asyncUrlListener);
+ return;
+ }
+
+ if (folder === aEventFolder && event == aEvent) {
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ resolve();
+ }
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+ });
+ };
+ let folderLoadedPromise = promiseFolderEvent(testFolder, "FolderLoaded");
+
+ localserver.performExpand(null);
+
+ // Wait for test.subscribe.simple to load. That will trigger getNewMessages.
+ await folderLoadedPromise;
+ // Wait for the new messages to be loaded.
+ await asyncUrlListener.promise;
+
+ Assert.equal(testFolder.msgDatabase.dBFolderInfo.highWater, highWater);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ localserver.closeCachedConnections();
+});
diff --git a/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js b/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js
new file mode 100644
index 0000000000..5212614b3a
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js
@@ -0,0 +1,59 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+let daemon = setupNNTPDaemon();
+let server = makeServer(NNTP_RFC4643_extension, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Test connection should be closed after canceling the password dialog.
+ */
+add_task(async function cancelPasswordDialog() {
+ // Mock the password prompt.
+ registerAlertTestUtils();
+
+ // Enforce server auth and trigger a list group request.
+ let incomingServer = setupLocalServer(server.port);
+ incomingServer.pushAuth = true;
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ incomingServer.loadNewsUrl(
+ Services.io.newURI(`news://localhost:${server.port}/*`),
+ null,
+ listener
+ );
+
+ // The request should be aborted.
+ try {
+ await listener.promise;
+ } catch (e) {
+ equal(e, Cr.NS_ERROR_ABORT);
+ }
+
+ // Should send nothing after canceling the password dialog.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER"]);
+});
+
+function promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ // Cancel the password dialog.
+ return false;
+}
diff --git a/comm/mailnews/news/test/unit/test_filter.js b/comm/mailnews/news/test/unit/test_filter.js
new file mode 100644
index 0000000000..9e450499e7
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_filter.js
@@ -0,0 +1,179 @@
+// Tests for the filtering code of NNTP. The same tests are run for each of the
+// different NNTP setups, to test code in a variety of cases.
+//
+// Different suites:
+// * Perfect 3977 compliance (not tested)
+// * Perfect 2980 compliance (XOVER and XHDR work)
+// * Giganews compliance (XHDR doesn't work for practical purposes)
+// * Only 977 compliance (no XOVER support)
+// Basic operations:
+// * Test that the following headers trigger:
+// - Subject
+// - From
+// - Date
+// - Size
+// - Message-ID (header retrievable by XOVER)
+// - User-Agent (header not retrievable by XHDR)
+// * Test all actions
+
+/* import-globals-from ../../../test/resources/filterTestUtils.js */
+load("../../../resources/filterTestUtils.js");
+
+// These are the expected results for testing filter triggers
+var attribResults = {
+ "1@regular.invalid": ["isRead", false],
+ "2@regular.invalid": ["isRead", true],
+ "3@regular.invalid": ["isRead", true],
+ "4@regular.invalid": ["isRead", true],
+ "5@regular.invalid": ["isRead", true],
+ "6.odd@regular.invalid": ["isRead", true],
+ "7@regular.invalid": ["isRead", true],
+ "8.unread@regular.invalid": ["isRead", true],
+};
+function testAttrib(handler, daemon, localserver) {
+ var server = makeServer(handler, daemon);
+ server.setDebugLevel(fsDebugAll);
+ server.start();
+ localserver.port = server.port;
+
+ // Get the folder and force filters to run
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ var headers = [...folder.messages];
+
+ try {
+ Assert.equal(headers.length, 8);
+ for (var header of headers) {
+ var id = header.messageId;
+ dump("Testing message " + id + "\n");
+ Assert.equal(header[attribResults[id][0]], attribResults[id][1]);
+ }
+ } catch (e) {
+ print(server.playTransaction().them);
+ throw e;
+ } finally {
+ server.stop();
+ }
+
+ resetFolder(folder);
+}
+
+// These are the results for testing actual actions
+var actionResults = {
+ "1@regular.invalid": ["priority", 6],
+ // "2@regular.invalid" should not be in database
+ "3@regular.invalid": function (header, folder) {
+ var flags = folder.msgDatabase.getThreadContainingMsgHdr(header).flags;
+ var ignored = Ci.nsMsgMessageFlags.Ignored;
+ // This is checking the thread's kill flag
+ return (flags & ignored) == ignored;
+ },
+ "4@regular.invalid": function (header, folder) {
+ var flags = folder.msgDatabase.getThreadContainingMsgHdr(header).flags;
+ var watched = Ci.nsMsgMessageFlags.Watched;
+ // This is checking the thread's watch flag
+ return (flags & watched) == watched;
+ },
+ "5@regular.invalid": ["isFlagged", true],
+ "6.odd@regular.invalid": ["isRead", false],
+ "7@regular.invalid": function (header, folder) {
+ return header.getStringProperty("keywords") == "tag";
+ },
+ "8.unread@regular.invalid": ["isRead", false],
+};
+function testAction(handler, daemon, localserver) {
+ var server = makeServer(handler, daemon);
+ server.start();
+ localserver.port = server.port;
+
+ // Get the folder and force filters to run
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ let headers = [...folder.messages];
+
+ try {
+ Assert.equal(headers.length, 7);
+ for (var header of headers) {
+ var id = header.messageId;
+ dump("Testing message " + id + "\n");
+ if (actionResults[id] instanceof Array) {
+ Assert.equal(header[actionResults[id][0]], actionResults[id][1]);
+ } else {
+ Assert.ok(actionResults[id](header, folder));
+ }
+ }
+ } catch (e) {
+ print(server.playTransaction().them);
+ throw e;
+ } finally {
+ server.stop();
+ }
+
+ resetFolder(folder);
+}
+
+// These are the various server handlers
+var handlers = [
+ NNTP_RFC977_handler,
+ NNTP_Giganews_handler,
+ NNTP_RFC2980_handler,
+];
+function run_test() {
+ // Set up the server and add in filters
+ var daemon = setupNNTPDaemon();
+ var localserver = setupLocalServer(NNTP_PORT);
+ var serverFilters = localserver.getFilterList(null);
+
+ createFilter(serverFilters, "subject", "Odd", "read");
+ createFilter(serverFilters, "from", "Odd Person", "read");
+ // A PRTime is the time in μs, but a JS date is time in ms.
+ createFilter(serverFilters, "date", new Date(2000, 0, 1) * 1000, "read");
+ createFilter(serverFilters, "size", 2, "read");
+ createFilter(serverFilters, "message-id", "odd", "read");
+ createFilter(serverFilters, "user-agent", "Odd/1.0", "read");
+ createFilter(serverFilters, "message-id", "8.unread", "read");
+ localserver.setFilterList(serverFilters);
+
+ handlers.forEach(function (handler) {
+ testAttrib(handler, daemon, localserver);
+ });
+
+ // Now we test folder-filters... and actions
+ // Clear out the server filters
+ while (serverFilters.filterCount > 0) {
+ serverFilters.removeFilterAt(0);
+ }
+ localserver.setFilterList(serverFilters);
+
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ var folderFilters = folder.getFilterList(null);
+ createFilter(folderFilters, "subject", "First", "priority");
+ createFilter(folderFilters, "subject", "Odd", "delete");
+ createFilter(folderFilters, "from", "Odd Person", "kill");
+ createFilter(folderFilters, "date", new Date(2000, 0, 1) * 1000, "watch");
+ createFilter(folderFilters, "size", 2, "flag");
+ createFilter(folderFilters, "message-id", "odd", "stop");
+ // This shouldn't be hit, because of the previous filter
+ createFilter(folderFilters, "message-id", "6.odd", "read");
+ createFilter(folderFilters, "user-agent", "Odd/1.0", "tag");
+ createFilter(folderFilters, "message-id", "8.unread", "read");
+ createFilter(folderFilters, "message-id", "8.unread", "unread");
+ folderFilters.loggingEnabled = true;
+ folder.setFilterList(folderFilters);
+
+ handlers.forEach(function (handler) {
+ testAction(handler, daemon, localserver);
+ });
+}
diff --git a/comm/mailnews/news/test/unit/test_getNewsMessage.js b/comm/mailnews/news/test/unit/test_getNewsMessage.js
new file mode 100644
index 0000000000..94fa82aece
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_getNewsMessage.js
@@ -0,0 +1,101 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests:
+ * - getNewMessages for a newsgroup folder (single message).
+ * - loadMessage for a newsgroup message
+ * - Downloading a single message and checking content in stream is correct.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+var streamListener = {
+ _data: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ // nsIRequestObserver
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ Assert.equal(aStatusCode, 0);
+
+ // Reduce any \r\n to just \n so we can do a good comparison on any
+ // platform.
+ var reduced = this._data.replace(/\r\n/g, "\n");
+ Assert.equal(reduced, kSimpleNewsArticle);
+
+ // We must finish closing connections and tidying up after a timeout
+ // so that the thread has time to unwrap itself.
+ do_timeout(0, doTestFinished);
+ },
+
+ // nsIStreamListener
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ let scriptStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+
+ scriptStream.init(aInputStream);
+
+ this._data += scriptStream.read(aCount);
+ },
+};
+
+function doTestFinished() {
+ localserver.closeCachedConnections();
+
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ try {
+ // Get the folder and new mail
+ var folder = localserver.rootFolder.getChildNamed("test.subscribe.simple");
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ var message = folder.firstNewMessage;
+
+ var messageUri = folder.getUriForMsg(message);
+
+ do_test_pending();
+
+ Cc["@mozilla.org/messenger/messageservice;1?type=news"]
+ .getService(Ci.nsIMsgMessageService)
+ .loadMessage(messageUri, streamListener, null, null, false);
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_internalUris.js b/comm/mailnews/news/test/unit/test_internalUris.js
new file mode 100644
index 0000000000..4be0402f0e
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_internalUris.js
@@ -0,0 +1,305 @@
+/* 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/. */
+
+/* Tests internal URIs generated by various methods in the code base.
+ * If you manually generate a news URI somewhere, please add it to this test.
+ */
+
+Cu.importGlobalProperties(["crypto"]);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+var daemon, localserver, server;
+
+var kCancelArticle =
+ "From: fake@acme.invalid\n" +
+ "Newsgroups: test.filter\n" +
+ "Subject: cancel <4@regular.invalid>\n" +
+ "References: <4@regular.invalid>\n" +
+ "Control: cancel <4@regular.invalid>\n" +
+ "MIME-Version: 1.0\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "This message was cancelled from within ";
+
+var dummyMsgWindow;
+
+add_setup(function setupTest() {
+ registerAlertTestUtils();
+
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ // Set up an identity for posting.
+ let identity = MailServices.accounts.createIdentity();
+ identity.fullName = "Normal Person";
+ identity.email = "fake@acme.invalid";
+ MailServices.accounts.FindAccountForServer(localserver).addIdentity(identity);
+
+ dummyMsgWindow = new DummyMsgWindow();
+});
+
+add_task(async function test_newMsgs() {
+ // This tests nsMsgNewsFolder::GetNewsMessages via getNewMessages.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), 0);
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, urlListener);
+ await urlListener.promise;
+ Assert.equal(folder.getTotalMessages(false), 8);
+});
+
+add_task(async function test_cancel() {
+ // This tests nsMsgNewsFolder::CancelMessage.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ let db = folder.msgDatabase;
+ let hdr = db.getMsgHdrForKey(4);
+
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).cancelMessage(hdr, dummyMsgWindow);
+ await dummyMsgWindow.promise;
+
+ // Reset promise state.
+ dummyMsgWindow.deferPromise();
+
+ Assert.equal(folder.getTotalMessages(false), 7);
+
+ // Check the content of the CancelMessage itself.
+ let article = daemon.getGroup("test.filter")[9];
+ // Since the cancel message includes the brand name (Daily, Thunderbird), we
+ // only check the beginning of the string.
+ Assert.ok(article.fullText.startsWith(kCancelArticle));
+});
+
+function generateLongArticle() {
+ // After converting to base64, the message body will be 65536 * 4 = 256KB.
+ let arr = new Uint8Array(65536);
+ crypto.getRandomValues(arr);
+ return `Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+MIME-Version: 1.0
+Subject: Odd Subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <2@regular.invalid>
+
+${btoa(arr)}
+${btoa(arr)}
+${btoa(arr)}
+`;
+}
+
+add_task(async function test_fetchMessage() {
+ // Replace the second article with a large message.
+ daemon.addArticleToGroup(
+ new NewsArticle(generateLongArticle()),
+ "test.filter",
+ 2
+ );
+
+ // Tests nsNntpService::CreateMessageIDURL via FetchMessage.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ MailServices.nntp.fetchMessage(folder, 2, null, streamListener, urlListener);
+ await urlListener.promise;
+ let data = await streamListener.promise;
+ // To point out that the streamListener Promise shouldn't reject.
+ Assert.ok(data);
+});
+
+add_task(async function test_fetchMessageNoStreamListener() {
+ // Tests nsNntpService::CreateMessageIDURL via FetchMessage.
+ let streamListener = null;
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ MailServices.nntp.fetchMessage(folder, 2, null, streamListener, urlListener);
+ await urlListener.promise;
+});
+
+add_task(async function test_search() {
+ // This tests nsNntpService::Search.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.news, folder);
+
+ let searchTerm = searchSession.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.Subject;
+ let value = searchTerm.value;
+ value.str = "First";
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ searchTerm.booleanAnd = false;
+ searchSession.appendTerm(searchTerm);
+
+ let hitCount;
+ let searchListener = new PromiseTestUtils.PromiseSearchNotify(searchSession, {
+ onSearchHit() {
+ hitCount++;
+ },
+ onNewSearch() {
+ hitCount = 0;
+ },
+ });
+
+ searchSession.search(null);
+ await searchListener.promise;
+
+ Assert.equal(hitCount, 1);
+});
+
+add_task(async function test_grouplist() {
+ // This tests nsNntpService::GetListOfGroupsOnServer.
+ let subserver = localserver.QueryInterface(Ci.nsISubscribableServer);
+ let subscribablePromise = PromiseUtils.defer();
+ let subscribeListener = {
+ OnDonePopulating() {
+ subscribablePromise.resolve();
+ },
+ };
+ subserver.subscribeListener = subscribeListener;
+
+ function enumGroups(rootUri) {
+ let hierarchy = subserver.getChildURIs(rootUri);
+ let groups = [];
+ for (let name of hierarchy) {
+ if (subserver.isSubscribable(name)) {
+ groups.push(name);
+ }
+ if (subserver.hasChildren(name)) {
+ groups = groups.concat(enumGroups(name));
+ }
+ }
+ return groups;
+ }
+
+ MailServices.nntp.getListOfGroupsOnServer(localserver, null, false);
+ await subscribablePromise.promise;
+
+ let groups = enumGroups("");
+ Assert.equal(groups.length, Object.keys(daemon._groups).length);
+ for (let group in daemon._groups) {
+ Assert.ok(groups.includes(group));
+ }
+
+ // First node in the group list, even though it is not subscribable,
+ // parent of "test.empty" group.
+ Assert.equal(subserver.getFirstChildURI(""), "test");
+
+ // Release reference, somehow impedes GC of 'subserver'.
+ subserver.subscribeListener = null;
+});
+
+add_task(async function test_postMessage() {
+ // This tests nsNntpService::SetUpNntpUrlForPosting via PostMessage.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.postMessage(
+ do_get_file("postings/post2.eml"),
+ "misc.test",
+ localserver.key,
+ urlListener,
+ null
+ );
+ await urlListener.promise;
+ Assert.equal(daemon.getGroup("misc.test").keys.length, 1);
+});
+
+// Not tested because it requires UI, and this is insufficient, I think.
+// function test_forwardInline() {
+// // This tests mime_parse_stream_complete via forwarding inline
+// let folder = localserver.rootFolder.getChildNamed("test.filter");
+// let hdr = folder.msgDatabase.getMsgHdrForKey(1);
+// MailServices.compose.forwardMessage("a@b.invalid", hdr, null,
+// localserver, Ci.nsIMsgComposeService.kForwardInline);
+// }
+
+add_task(async function test_escapedName() {
+ // This does a few tests to make sure our internal URIs work for newsgroups
+ // with names that need escaping.
+ let evilName = "test.malformed&name";
+ daemon.addGroup(evilName);
+ daemon.addArticle(make_article(do_get_file("postings/bug670935.eml")));
+ localserver.subscribeToNewsgroup(evilName);
+
+ // Can we access it?
+ let folder = localserver.rootFolder.getChildNamed(evilName);
+ let newMessageUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, newMessageUrlListener);
+ await newMessageUrlListener.promise;
+
+ // If we get here, we didn't crash--newsgroups unescape properly.
+ // Load a message, to test news-message: URI unescaping.
+ let streamlistener = new PromiseTestUtils.PromiseStreamListener();
+ let fetchMessageUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.fetchMessage(
+ folder,
+ 1,
+ null,
+ streamlistener,
+ fetchMessageUrlListener
+ );
+ await fetchMessageUrlListener.promise;
+ let data = await streamlistener.promise;
+ // To point out that the streamListener Promise shouldn't reject.
+ Assert.ok(data);
+});
+
+add_task(function cleanUp() {
+ localserver.closeCachedConnections();
+});
+
+class DummyMsgWindow {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgWindow",
+ "nsISupportsWeakReference",
+ ]);
+
+ constructor() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ get statusFeedback() {
+ let scopedThis = this;
+ return {
+ startMeteors() {},
+ stopMeteors() {
+ scopedThis._deferredPromise.resolve(true);
+ },
+ showProgress() {},
+ };
+ }
+
+ get promptDialog() {
+ return alertUtilsPrompts;
+ }
+
+ deferPromise() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ get promise() {
+ return this._deferredPromise.promise;
+ }
+}
+
+/* exported alert, confirmEx */
+// Prompts for cancel.
+function alertPS(parent, title, text) {}
+function confirmExPS(parent, title, text, flags) {
+ return 0;
+}
diff --git a/comm/mailnews/news/test/unit/test_newsAutocomplete.js b/comm/mailnews/news/test/unit/test_newsAutocomplete.js
new file mode 100644
index 0000000000..29fecb3d9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_newsAutocomplete.js
@@ -0,0 +1,107 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function acObserver() {}
+
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ },
+};
+
+function run_test() {
+ setupLocalServer(119);
+
+ // create identity
+ let identity = MailServices.accounts.createIdentity();
+ _account.addIdentity(identity);
+
+ let acs = Cc["@mozilla.org/autocomplete/search;1?name=news"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+ let obs;
+
+ let paramsN = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_newsgroups",
+ });
+ let paramsF = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_followup",
+ });
+ let paramsMail = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_to",
+ });
+
+ // misc.test is not subscribed
+ obs = new acObserver();
+ acs.startSearch("misc", paramsN, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ obs = new acObserver();
+ acs.startSearch("misc", paramsF, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ obs = new acObserver();
+ acs.startSearch("misc", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.filter is subscribed
+ obs = new acObserver();
+ acs.startSearch("filter", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ obs = new acObserver();
+ acs.startSearch("filter", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ // ... but no auto-complete should occur for addr_to
+ obs = new acObserver();
+ acs.startSearch("filter", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.subscribe.empty and test.subscribe.simple are subscribed
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 2);
+
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 2);
+
+ // ... but no auto-complete should occur for addr_to
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.subscribe.empty is subscribed, test.empty is not
+ obs = new acObserver();
+ acs.startSearch("empty", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ obs = new acObserver();
+ acs.startSearch("empty", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpContentLength.js b/comm/mailnews/news/test/unit/test_nntpContentLength.js
new file mode 100644
index 0000000000..f9f780011d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpContentLength.js
@@ -0,0 +1,80 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test content length for the news protocol. This focuses on necko URLs
+ * that are run externally.
+ */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ try {
+ // Get the folder and new mail
+ let folder = localserver.rootFolder.getChildNamed("test.subscribe.simple");
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ // Get the message URI
+ let msgHdr = folder.firstNewMessage;
+ let messageUri = folder.getUriForMsg(msgHdr);
+ // Convert this to a URI that necko can run
+ let messageService = MailServices.messageServiceFromURI(messageUri);
+ let neckoURL = messageService.getUrlForUri(messageUri);
+ // Don't use the necko URL directly. Instead, get the spec and create a new
+ // URL using the IO service
+ let urlToRun = Services.io.newURI(neckoURL.spec);
+
+ // Get a channel from this URI, and check its content length
+ let channel = Services.io.newChannelFromURI(
+ urlToRun,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ Assert.equal(channel.contentLength, kSimpleNewsArticle.length);
+
+ // Now try an attachment. &part=1.2
+ // XXX the message doesn't really have an attachment
+ let attachmentURL = Services.io.newURI(neckoURL.spec + "&part=1.2");
+ Services.io.newChannelFromURI(
+ attachmentURL,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ // Currently attachments have their content length set to the length of the
+ // entire message
+ Assert.equal(channel.contentLength, kSimpleNewsArticle.length);
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpGroupPassword.js b/comm/mailnews/news/test/unit/test_nntpGroupPassword.js
new file mode 100644
index 0000000000..6bf9be53cc
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpGroupPassword.js
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643).
+ */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ await Services.logins.initializationPromise;
+
+ daemon.groupCredentials = {
+ "test.subscribe.empty": ["group1", "pass1"],
+ "test.filter": ["group2", "pass2"],
+ };
+
+ var server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+ var localserver = setupLocalServer(server.port);
+ localserver.singleSignon = false;
+
+ // Add passwords to the manager
+ var serverURI = "news://localhost/";
+ var loginInfo1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo1.init(
+ serverURI + "test.subscribe.empty",
+ null,
+ serverURI + "test.subscribe.empty",
+ "group1",
+ "pass1",
+ "",
+ ""
+ );
+ Services.logins.addLogin(loginInfo1);
+ var loginInfo2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo2.init(
+ serverURI + "test.filter",
+ null,
+ serverURI + "test.filter",
+ "group2",
+ "pass2",
+ "",
+ ""
+ );
+ Services.logins.addLogin(loginInfo2);
+ try {
+ var prefix = "news://localhost:" + server.port + "/";
+ var transaction;
+
+ test = "per-group password part 1";
+ setupProtocolTest(
+ server.port,
+ prefix + "test.subscribe.empty",
+ localserver
+ );
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.subscribe.empty",
+ "AUTHINFO user group1",
+ "AUTHINFO pass pass1",
+ "GROUP test.subscribe.empty",
+ ]);
+
+ test = "per-group password part 2";
+ server.resetTest();
+ setupProtocolTest(server.port, prefix + "test.filter", localserver);
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "AUTHINFO user group2",
+ "AUTHINFO pass pass2",
+ "GROUP test.filter",
+ "XOVER 1-8",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + uneval(trans) + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword.js b/comm/mailnews/news/test/unit/test_nntpPassword.js
new file mode 100644
index 0000000000..dbccf01f74
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword.js
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643).
+ *
+ * Note: Logins for newsgroup servers for 1.8 were stored with either the
+ * default port or the SSL default port. Nothing else!
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ var server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+
+ try {
+ var prefix = "news://localhost:" + server.port + "/";
+ var transaction;
+
+ // Test - group subscribe listing
+ test = "news:*";
+ setupProtocolTest(server.port, prefix + "*");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "LIST",
+ "AUTHINFO user testnews",
+ "AUTHINFO pass newstest",
+ "LIST",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword2.js b/comm/mailnews/news/test/unit/test_nntpPassword2.js
new file mode 100644
index 0000000000..8fcf7d7e0b
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword2.js
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643) - checks for servers whose
+ * details have changed (e.g. realhostname is different from hostname).
+ *
+ * Note: Logins for newsgroup servers for 1.8 were stored with either the
+ * default port or the SSL default port. Nothing else!
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ let server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+
+ // These preferences set up a local news server that has had its hostname
+ // and username changed from the original settings. We can't do this by
+ // function calls for this test as they would cause the password to be
+ // forgotten when changing the hostname/username and this breaks the test.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id1");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2"
+ );
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account2");
+ Services.prefs.setCharPref("mail.identity.id1.fullName", "testnntp");
+ Services.prefs.setCharPref(
+ "mail.identity.id1.useremail",
+ "testnntp@localhost"
+ );
+ Services.prefs.setBoolPref("mail.identity.id1.valid", true);
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.name", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "invalid");
+ Services.prefs.setCharPref(
+ "mail.server.server2.name",
+ "testnntp on localhost"
+ );
+ Services.prefs.setIntPref("mail.server.server2.port", server.port);
+ Services.prefs.setCharPref("mail.server.server2.realhostname", "localhost");
+ Services.prefs.setCharPref("mail.server.server2.type", "nntp");
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-alt.json");
+
+ try {
+ // Note, the uri is for hostname "invalid" which is the original uri. See
+ // setupProtocolTest parameters.
+ var prefix = "news://invalid:" + server.port + "/";
+
+ // Test - group subscribe listing
+ test = "news:*";
+
+ // Get the existing incoming server
+ MailServices.accounts.loadAccounts();
+
+ // Create the incoming server with "original" details.
+ var incomingServer = MailServices.accounts.getIncomingServer("server2");
+
+ subscribeServer(incomingServer);
+
+ // Now set up and run the tests
+ setupProtocolTest(server.port, prefix + "*", incomingServer);
+ server.performTest();
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "LIST",
+ "AUTHINFO user testnews",
+ "AUTHINFO pass newstest",
+ "LIST",
+ ]);
+ incomingServer.QueryInterface(Ci.nsISubscribableServer).subscribeCleanup();
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword3.js b/comm/mailnews/news/test/unit/test_nntpPassword3.js
new file mode 100644
index 0000000000..a4c5e0ac76
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword3.js
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Extra tests for forgetting newsgroup usernames and passwords.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var kUsername = "testnews";
+var kPassword = "newstest";
+var kProtocol = "nntp";
+var kHostname = "localhost";
+var kServerUrl = "news://" + kHostname;
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ // Set up the basic accounts and folders.
+ localAccountUtils.loadLocalMailAccount();
+
+ var incomingServer = MailServices.accounts.createIncomingServer(
+ null,
+ kHostname,
+ kProtocol
+ );
+
+ // Test - Check there is a password to begin with...
+ var logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUsername);
+ Assert.equal(logins[0].password, kPassword);
+
+ // Test - Remove the news password login via the incoming server
+ incomingServer.forgetPassword();
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // should be no passwords left...
+ Assert.equal(logins.length, 0);
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js b/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js
new file mode 100644
index 0000000000..994a701c8e
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js
@@ -0,0 +1,196 @@
+/* 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/. */
+
+/**
+ * This test checks to see if the nntp password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var server;
+var daemon;
+var incomingServer;
+var folder;
+var attempt = 0;
+var logins;
+
+var kUserName = "testnews";
+var kInvalidPassword = "newstest";
+var kValidPassword = "notallama";
+
+add_setup(function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref("signon.debug", true);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Set up the server
+ daemon = setupNNTPDaemon();
+ function createHandler(d) {
+ var handler = new NNTP_RFC4643_extension(d);
+ handler.expectedPassword = kValidPassword;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+ incomingServer = setupLocalServer(server.port);
+ folder = incomingServer.rootFolder.getChildNamed("test.subscribe.simple");
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(folder.getTotalMessages(false), 0);
+});
+
+add_task(async function getMail1() {
+ // Now get mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(url, result) {
+ // On the last attempt, we should have successfully got one mail.
+ Assert.equal(folder.getTotalMessages(false), attempt == 4 ? 1 : 0);
+
+ // If we've just cancelled, expect failure rather than success
+ // because the server dropped the connection.
+ dump("in onStopRunning, result = " + result + "\n");
+ // do_check_eq(result, attempt == 2 ? Cr.NS_ERROR_FAILURE : 0);
+ },
+ });
+ folder.getNewMessages(gDummyMsgWindow, urlListener);
+ await urlListener.promise;
+
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ logins = Services.logins.findLogins(
+ "news://localhost",
+ null,
+ "news://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+});
+
+add_task(async function getMail2() {
+ let urlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(url, result) {
+ // On the last attempt, we should have successfully got one mail.
+ Assert.equal(folder.getTotalMessages(false), attempt == 4 ? 1 : 0);
+
+ // If we've just cancelled, expect failure rather than success
+ // because the server dropped the connection.
+ dump("in onStopRunning, result = " + result + "\n");
+ // do_check_eq(result, attempt == 2 ? Cr.NS_ERROR_FAILURE : 0);
+ },
+ });
+ folder.getNewMessages(gDummyMsgWindow, urlListener);
+ await urlListener.promise;
+ // Now check the new one has been saved.
+ logins = Services.logins.findLogins(
+ "news://localhost",
+ null,
+ "news://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+});
+
+add_task(function endTest() {
+ // Clean up nicely the test.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+/* exported alert, confirmEx, promptUsernameAndPasswordPS */
+function alertPS(parent, aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ dump("Alert Title: " + aDialogText + "\nAlert Text: " + aText + "\n");
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting retry\n");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ dump("\nCancelling login attempt\n");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ dump("\nAttempting Retry\n");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ dump("\nEnter new password\n");
+ return 2;
+ default:
+ throw new Error("unexpected attempt number " + attempt);
+ }
+}
+
+function promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aUsername.value = kUserName;
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpPost.js b/comm/mailnews/news/test/unit/test_nntpPost.js
new file mode 100644
index 0000000000..e49aa81d92
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPost.js
@@ -0,0 +1,37 @@
+// Tests that the news can correctly post messages
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/**
+ * Test dot is stuffed correctly when posting an article.
+ */
+add_task(async function test_nntpPost() {
+ // Setup test server.
+ let daemon = setupNNTPDaemon();
+ let handler = new NNTP_RFC977_handler(daemon);
+ let server = new nsMailServer(() => handler, daemon);
+ server.start();
+ registerCleanupFunction(() => server.stop());
+
+ // Send post3.eml to the server.
+ let localServer = setupLocalServer(server.port);
+ let testFile = do_get_file("postings/post3.eml");
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.postMessage(
+ testFile,
+ "test.empty",
+ localServer.key,
+ urlListener,
+ null
+ );
+ await urlListener.promise;
+
+ // Because Nntpd.jsm undone the dot-stuffing, handler.post should be the same
+ // as the original post.
+ equal(handler.post, await IOUtils.readUTF8(testFile.path));
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpProtocols.js b/comm/mailnews/news/test/unit/test_nntpProtocols.js
new file mode 100644
index 0000000000..48f13095fd
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpProtocols.js
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for getting news urls via the protocol handler.
+ */
+
+var defaultProtocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS |
+ Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC;
+
+var protocols = [
+ {
+ protocol: "news",
+ urlSpec: "news://user@localhost/",
+ defaultPort: Ci.nsINntpUrl.DEFAULT_NNTP_PORT,
+ },
+ // XXX News secure protocol not working yet.
+ /* { protocol: "snews",
+ urlSpec: "snews://user@localhost/",
+ defaultPort: Ci.nsINntpUrl.DEFAULT_NNTPS_PORT
+} */
+];
+
+function run_test() {
+ for (var part = 0; part < protocols.length; ++part) {
+ print("protocol: " + protocols[part].protocol);
+
+ var pH = Cc[
+ "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol
+ ].createInstance(Ci.nsIProtocolHandler);
+
+ Assert.equal(pH.scheme, protocols[part].protocol);
+ Assert.equal(
+ Services.io.getDefaultPort(pH.scheme),
+ protocols[part].defaultPort
+ );
+ Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags);
+
+ // Whip through some of the ports to check we get the right results.
+ // NEWS allows connecting to any port.
+ for (let i = 0; i < 1024; ++i) {
+ Assert.ok(pH.allowPort(i, ""));
+ }
+
+ // Check we get a URI when we ask for one
+ var uri = Services.io.newURI(protocols[part].urlSpec);
+
+ uri.QueryInterface(Ci.nsINntpUrl);
+
+ Assert.equal(uri.spec, protocols[part].urlSpec);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpProxy.js b/comm/mailnews/news/test/unit/test_nntpProxy.js
new file mode 100644
index 0000000000..826a057c9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpProxy.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Tests that NNTP over a SOCKS proxy works.
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const PORT = 119;
+
+var daemon, localserver, server;
+
+add_setup(async function () {
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ NetworkTestUtils.configureProxy("news.tinderbox.invalid", PORT, server.port);
+ localserver = setupLocalServer(PORT, "news.tinderbox.invalid");
+});
+
+add_task(async function findMessages() {
+ // This is a trivial check that makes sure that we actually do some network
+ // traffic without caring about the exact network traffic.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ equal(folder.getTotalMessages(false), 0);
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ equal(folder.getTotalMessages(false), 8);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ localserver.closeCachedConnections();
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpUrl.js b/comm/mailnews/news/test/unit/test_nntpUrl.js
new file mode 100644
index 0000000000..93c51334bd
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpUrl.js
@@ -0,0 +1,30 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function getMessageHeaderFromUrl(aUrl) {
+ let msgUrl = Services.io.newURI(aUrl).QueryInterface(Ci.nsIMsgMessageUrl);
+ return msgUrl.messageHeader;
+}
+
+function run_test() {
+ // This is crash test for Bug 392729
+ try {
+ // msgkey is invalid for news:// protocol
+ getMessageHeaderFromUrl(
+ "news://localhost:119" +
+ "/123@example.invalid?group=test.subscribe.simple&key=abcdefghijk"
+ );
+ Assert.ok(false);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_server.js b/comm/mailnews/news/test/unit/test_server.js
new file mode 100644
index 0000000000..f0954add1c
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_server.js
@@ -0,0 +1,179 @@
+// Protocol tests for NNTP. These actually aren't too important, but their main
+// purpose is to make sure that maild is working properly and to provide
+// examples for how using maild. They also help make sure that I coded Nntpd.jsm
+// right, both logically and for RFC compliance.
+// TODO:
+// * We need to hook up mochitest,
+// * TLS negotiation.
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+// NNTP SERVER TESTS
+// -----------------
+// Functions in order as defined in Nntpd.jsm. Each function tests the URLs
+// that are located over the implementation of nsNNTPProtocol::LoadURL and
+// added in bug 400331. Furthermore, they are tested in rough order as they
+// would be expected to be used in a session. If more URL types are modified,
+// please add a corresponding type to the following tests.
+// When adding new servers, only test the commands that become different for
+// each specified server, to keep down redudant tests.
+
+function testRFC977() {
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+
+ try {
+ var prefix = "news://localhost:" + NNTP_PORT + "/";
+ var transaction;
+
+ // Test - group subscribe listing
+ test = "news:*";
+ setupProtocolTest(NNTP_PORT, prefix + "*");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER", "LIST"]);
+
+ // Test - getting group headers
+ test = "news:test.subscribe.empty";
+ server.resetTest();
+ setupProtocolTest(NNTP_PORT, prefix + "test.subscribe.empty");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.subscribe.empty",
+ ]);
+
+ // Test - getting an article
+ test = "news:MESSAGE_ID";
+ server.resetTest();
+ setupProtocolTest(NNTP_PORT, prefix + "TSS1@nntp.invalid");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "ARTICLE <TSS1@nntp.invalid>",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
+
+function testConnectionLimit() {
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+ // 1 is the default, but other tests do change it, so let's be explicit.
+ _server.maximumConnectionsNumber = 1;
+
+ var prefix = "news://localhost:" + NNTP_PORT + "/";
+
+ // To test make connections limit, we run two URIs simultaneously.
+ var url = Services.io.newURI(prefix + "*");
+ _server.loadNewsUrl(url, null, null);
+ setupProtocolTest(NNTP_PORT, prefix + "TSS1@nntp.invalid");
+ server.performTest();
+ // We should have length one... which means this must be a transaction object,
+ // containing only us and them
+ // (playTransactions() returns an array of transaction objects if there is
+ // more than one of them, so this assert will fail in that case).
+ Assert.ok("us" in server.playTransaction());
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
+
+function testReentrantClose() {
+ // What we are testing is that a CloseConnection that spins the event loop
+ // does not cause a crash.
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+
+ var listener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, rv) {
+ // Spin the event loop (entering nsNNTPProtocol::ProcessProtocolState)
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ },
+ };
+ // Nice multi-step command--we can close while executing this URL if we are
+ // careful.
+ var url = Services.io.newURI(
+ "news://localhost:" + NNTP_PORT + "/test.filter"
+ );
+ url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ url.RegisterListener(listener);
+
+ _server.loadNewsUrl(url, null, {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ onStartRequest() {},
+ onStopRequest() {},
+ });
+ server.performTest("GROUP");
+ dump("Stopping server\n");
+ gThreadManager.currentThread.dispatch(
+ {
+ run() {
+ _server.closeCachedConnections();
+ },
+ },
+ Ci.nsIEventTarget.DISPATCH_NORMAL
+ );
+ server.performTest();
+ server.stop();
+
+ // Break refcnt loops
+ listener = url = null;
+}
+
+function testManyConnections() {
+ // Start up 2 connections at once and make sure that they don't conflict
+ var server = makeServer(NNTP_RFC2980_handler, daemon);
+ setupLocalServer(NNTP_PORT);
+ server.start(NNTP_PORT);
+ _server.maximumConnectionsNumber = 3;
+ var listener = {
+ ran: 0,
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, rv) {
+ if (--this.ran == 0) {
+ _server.closeCachedConnections();
+ }
+ },
+ };
+ for (let group of _server.rootFolder.subFolders) {
+ group.getNewMessages(null, listener);
+ listener.ran++;
+ }
+ server.performTest();
+ // The last one that is processed is test.filter, so make sure that
+ // test.subscribed.simple is not retrieving the data meant for test.filter
+ let folder = _server.rootFolder.getChildNamed("test.subscribe.simple");
+ Assert.equal(folder.getTotalMessages(false), 1);
+}
+
+function run_test() {
+ testRFC977();
+ testConnectionLimit();
+ testReentrantClose();
+ testManyConnections();
+}
diff --git a/comm/mailnews/news/test/unit/test_uriParser.js b/comm/mailnews/news/test/unit/test_uriParser.js
new file mode 100644
index 0000000000..a152076333
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_uriParser.js
@@ -0,0 +1,221 @@
+// Tests nsINntpUrl parsing.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var localserver;
+var tests = [
+ // news://host/-based URIs
+ {
+ uri: "news://localhost/?newgroups",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListNewGroups,
+ },
+ // news://host/group-based
+ {
+ uri: "news://news.server.example/example.group.this",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "example.group.this",
+ },
+ {
+ uri: "news://news.server.example/*",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListGroups,
+ },
+ {
+ uri: "news://news.server.example/news.*",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListGroups,
+ },
+ {
+ uri: "news://localhost/test.filter?list-ids",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionListIds,
+ group: "test.filter",
+ },
+ {
+ uri: "news://localhost/some.group?search/XPAT From 1-5 [Ww][Hh][Oo]",
+ get server() {
+ return localserver;
+ },
+ newsAction: Ci.nsINntpUrl.ActionSearch,
+ group: "some.group",
+ },
+
+ // news://host/message-based URIs
+ {
+ uri: "news://localhost/message-id@some-host.invalid",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "",
+ key: 0xffffffff,
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?part=1.4",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchPart,
+ messageID: "message-id@some-host.invalid",
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?cancel",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionCancelArticle,
+ messageID: "message-id@some-host.invalid",
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?group=foo&key=123",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "foo",
+ key: 123,
+ },
+
+ // No-authority uris
+ {
+ uri: "news:rec.games.pinball",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "rec.games.pinball",
+ host: "",
+ },
+ {
+ uri: "news:message-id@some-host.invalid",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "",
+ key: 0xffffffff,
+ },
+
+ // news-message://host/group#key
+ {
+ uri: "news-message://localhost/test.simple.subscribe#1",
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "test.simple.subscribe",
+ key: 1,
+ },
+
+ // nntp://host/group
+ {
+ uri: "nntp://localhost/test.filter",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "test.filter",
+ },
+ {
+ uri: "nntp://localhost/i.dont.exist",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "i.dont.exist",
+ },
+ {
+ uri: "nntp://news.example.invalid/i.dont.exist",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "i.dont.exist",
+ },
+
+ // nntp://host/group/key
+ {
+ uri: "nntp://localhost/test.filter/123",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "test.filter",
+ key: 123,
+ },
+ {
+ uri: "nntp://localhost/i.dont.exist/123",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "i.dont.exist",
+ key: 123,
+ },
+];
+
+var invalid_uris = [
+ "news-message://localhost/test.simple.subscribe#hello",
+ "nntp://localhost/",
+ "nntp://localhost/a.group/hello",
+ "nntp://localhost/a.group/0",
+ "nntp:a.group",
+];
+
+function run_test() {
+ // We're not running the server, just setting it up
+ localserver = setupLocalServer(119);
+ for (let test of tests) {
+ dump("Checking URL " + test.uri + "\n");
+ let url = Services.io.newURI(test.uri);
+ url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ url.QueryInterface(Ci.nsINntpUrl);
+ for (let prop in test) {
+ if (prop == "uri") {
+ continue;
+ }
+ Assert.equal(url[prop], test[prop]);
+ }
+ }
+
+ for (let fail of invalid_uris) {
+ try {
+ dump("Checking URL " + fail + " for failure\n");
+ Services.io.newURI(fail);
+ Assert.ok(false);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI);
+ }
+ }
+
+ // The password migration is async, so trigger an event to prevent the logon
+ // manager from trying to migrate after shutdown has started.
+ let thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_xover.js b/comm/mailnews/news/test/unit/test_xover.js
new file mode 100644
index 0000000000..48cc6f3e50
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_xover.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+let daemon = setupNNTPDaemon();
+let server = makeServer(NNTP_RFC2980_handler, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+let incomingServer = setupLocalServer(server.port);
+
+/**
+ * Test nsIDBFolderInfo.knownArtsSet is correctly updated after XOVER response.
+ * knownArtsSet depends on the XOVER range requested, it doesn't matter if
+ * articles in that range don't exist on the server.
+ */
+add_task(function test_updateKnownKeySetAfterXOver() {
+ // setupNNTPDaemon inited test.filter with 8 messages, delete the 5th, 6th here.
+ daemon.removeArticleFromGroup("test.filter", 5);
+ daemon.removeArticleFromGroup("test.filter", 6);
+
+ // Trigger a get new messages request.
+ let prefix = "news://localhost:" + server.port + "/";
+ setupProtocolTest(server.port, prefix + "test.filter", incomingServer);
+ server.performTest();
+ let transaction = server.playTransaction();
+
+ // Test XOVER was sent correctly.
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "XOVER 1-8",
+ ]);
+
+ // Test knownArtsSet was updated correctly.
+ let folder = incomingServer.rootFolder.getChildNamed("test.filter");
+ let groupInfo = folder.msgDatabase.dBFolderInfo;
+ // knownArtsSet should be "1-8", not "1-4,7-8".
+ equal(groupInfo.knownArtsSet, "1-8");
+});
diff --git a/comm/mailnews/news/test/unit/xpcshell.ini b/comm/mailnews/news/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..e997e00987
--- /dev/null
+++ b/comm/mailnews/news/test/unit/xpcshell.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+head = head_server_setup.js
+tail =
+support-files = postings/*
+
+[test_biff.js]
+[test_bug37465.js]
+[test_bug170727.js]
+run-sequentially = Restarts server twice--may work but dangerous
+[test_bug403242.js]
+[test_bug540288.js]
+[test_bug695309.js]
+[test_cancelPasswordDialog.js]
+[test_filter.js]
+[test_getNewsMessage.js]
+[test_internalUris.js]
+[test_newsAutocomplete.js]
+[test_NntpChannel.js]
+[test_nntpContentLength.js]
+# The server doesn't support returning sizes! (bug 782629)
+skip-if = true
+[test_nntpGroupPassword.js]
+[test_nntpPassword.js]
+[test_nntpPassword2.js]
+skip-if = true # realhostname and realuserName don't exist anymore
+[test_nntpPassword3.js]
+[test_nntpPasswordFailure.js]
+[test_nntpPost.js]
+[test_nntpProtocols.js]
+[test_nntpProxy.js]
+[test_nntpUrl.js]
+[test_server.js]
+run-sequentially = Uses fixed NNTP_PORT
+[test_uriParser.js]
+[test_xover.js]
diff --git a/comm/mailnews/nss-extra.symbols b/comm/mailnews/nss-extra.symbols
new file mode 100644
index 0000000000..27f5c497f2
--- /dev/null
+++ b/comm/mailnews/nss-extra.symbols
@@ -0,0 +1,58 @@
+CERT_GetCertNicknames
+CERT_VerifyCert
+NSSBase64Decoder_Create_Util
+NSSBase64Decoder_Destroy_Util
+NSSBase64Decoder_Update_Util
+NSSBase64Encoder_Create_Util
+NSSBase64Encoder_Destroy_Util
+NSSBase64Encoder_Update_Util
+NSS_CMSContentInfo_GetBulkKey
+NSS_CMSContentInfo_GetBulkKeySize
+NSS_CMSContentInfo_GetContentEncAlgTag
+NSS_CMSContentInfo_SetContent_Data
+NSS_CMSContentInfo_SetContent_EncryptedData
+NSS_CMSContentInfo_SetContent_EnvelopedData
+NSS_CMSDecoder_Cancel
+NSS_CMSDecoder_Finish
+NSS_CMSDecoder_Start
+NSS_CMSDecoder_Update
+NSS_CMSDEREncode
+NSS_CMSDigestContext_FinishMultiple
+NSS_CMSDigestContext_StartMultiple
+NSS_CMSDigestContext_Update
+NSS_CMSEncoder_Cancel
+NSS_CMSEncoder_Update
+NSS_CMSEncryptedData_Create
+NSS_CMSEncryptedData_GetContentInfo
+NSS_CMSEnvelopedData_AddRecipient
+NSS_CMSEnvelopedData_Create
+NSS_CMSEnvelopedData_GetContentInfo
+NSS_CMSMessage_ContentLevelCount
+NSS_CMSMessage_GetContent
+NSS_CMSMessage_IsEncrypted
+NSS_CMSRecipientInfo_Create
+NSS_CMSSignedData_AddCertChain
+NSS_CMSSignedData_AddCertList
+NSS_CMSSignedData_AddSignerInfo
+NSS_CMSSignedData_Create
+NSS_CMSSignedData_GetContentInfo
+NSS_CMSSignedData_GetDigestAlgs
+NSS_CMSSignedData_HasDigests
+NSS_CMSSignedData_ImportCerts
+NSS_CMSSignedData_SetDigests
+NSS_CMSSignedData_SetDigestValue
+NSS_CMSSignedData_VerifyCertsOnly
+NSS_CMSSignedData_VerifySignerInfo
+NSS_CMSSignerInfo_AddMSSMIMEEncKeyPrefs
+NSS_CMSSignerInfo_AddSigningTime
+NSS_CMSSignerInfo_AddSMIMECaps
+NSS_CMSSignerInfo_AddSMIMEEncKeyPrefs
+NSS_CMSSignerInfo_Create
+NSS_CMSSignerInfo_GetDigestAlgTag
+NSS_CMSSignerInfo_GetSignerCommonName
+NSS_CMSSignerInfo_GetSignerEmailAddress
+NSS_CMSSignerInfo_GetSigningTime
+NSS_CMSSignerInfo_GetVerificationStatus
+NSS_CMSSignerInfo_IncludeCerts
+NSS_CMSUtil_VerificationStatusToString
+PORT_ArenaGrow
diff --git a/comm/mailnews/search/content/CustomHeaders.js b/comm/mailnews/search/content/CustomHeaders.js
new file mode 100644
index 0000000000..4bfc4a3b78
--- /dev/null
+++ b/comm/mailnews/search/content/CustomHeaders.js
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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 gAddButton;
+var gRemoveButton;
+var gHeaderInputElement;
+var gArrayHdrs;
+var gHdrsList;
+var gContainer;
+var gFilterBundle = null;
+var gCustomBundle = null;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOk);
+document.addEventListener("dialogextra1", onAddHeader);
+document.addEventListener("dialogextra2", onRemoveHeader);
+
+function onLoad() {
+ let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
+ gHeaderInputElement = document.getElementById("headerInput");
+ gHeaderInputElement.focus();
+
+ gHdrsList = document.getElementById("headerList");
+ gArrayHdrs = [];
+ gAddButton = document.getElementById("addButton");
+ gRemoveButton = document.getElementById("removeButton");
+
+ initializeDialog(hdrs);
+ updateAddButton(true);
+ updateRemoveButton();
+}
+
+function initializeDialog(hdrs) {
+ if (hdrs) {
+ hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting
+ gArrayHdrs = hdrs.split(":");
+ for (var i = 0; i < gArrayHdrs.length; i++) {
+ if (!gArrayHdrs[i]) {
+ // Remove any null elements.
+ gArrayHdrs.splice(i, 1);
+ }
+ }
+ initializeRows();
+ }
+}
+
+function initializeRows() {
+ for (var i = 0; i < gArrayHdrs.length; i++) {
+ addRow(TrimString(gArrayHdrs[i]));
+ }
+}
+
+function onTextInput() {
+ // enable the add button if the user has started to type text
+ updateAddButton(gHeaderInputElement.value == "");
+}
+
+function onOk() {
+ if (gArrayHdrs.length) {
+ var hdrs;
+ if (gArrayHdrs.length == 1) {
+ hdrs = gArrayHdrs;
+ } else {
+ hdrs = gArrayHdrs.join(": ");
+ }
+ Services.prefs.setCharPref("mailnews.customHeaders", hdrs);
+ // flush prefs to disk, in case we crash, to avoid dataloss and problems with filters that use the custom headers
+ Services.prefs.savePrefFile(null);
+ } else {
+ Services.prefs.clearUserPref("mailnews.customHeaders"); // clear the pref, no custom headers
+ }
+
+ window.arguments[0].selectedVal = gHdrsList.selectedItem
+ ? gHdrsList.selectedItem.label
+ : null;
+}
+
+function customHeaderOverflow() {
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ if (
+ gArrayHdrs.length >=
+ nsMsgSearchAttrib.kNumMsgSearchAttributes -
+ nsMsgSearchAttrib.OtherHeader -
+ 1
+ ) {
+ if (!gFilterBundle) {
+ gFilterBundle = document.getElementById("bundle_filter");
+ }
+
+ var alertText = gFilterBundle.getString("customHeaderOverflow");
+ Services.prompt.alert(window, null, alertText);
+ return true;
+ }
+ return false;
+}
+
+function onAddHeader() {
+ var newHdr = TrimString(gHeaderInputElement.value);
+
+ if (!isRFC2822Header(newHdr)) {
+ // if user entered an invalid rfc822 header field name, bail out.
+ if (!gCustomBundle) {
+ gCustomBundle = document.getElementById("bundle_custom");
+ }
+
+ var alertText = gCustomBundle.getString("colonInHeaderName");
+ Services.prompt.alert(window, null, alertText);
+ return;
+ }
+
+ gHeaderInputElement.value = "";
+ if (!newHdr || customHeaderOverflow()) {
+ return;
+ }
+ if (!duplicateHdrExists(newHdr)) {
+ gArrayHdrs[gArrayHdrs.length] = newHdr;
+ var newItem = addRow(newHdr);
+ gHdrsList.selectItem(newItem); // make sure the new entry is selected in the tree
+ // now disable the add button
+ updateAddButton(true);
+ gHeaderInputElement.focus(); // refocus the input field for the next custom header
+ }
+}
+
+function isRFC2822Header(hdr) {
+ var charCode;
+ for (var i = 0; i < hdr.length; i++) {
+ charCode = hdr.charCodeAt(i);
+ // 58 is for colon and 33 and 126 are us-ascii bounds that should be used for header field name, as per rfc2822
+
+ if (charCode < 33 || charCode == 58 || charCode > 126) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function duplicateHdrExists(hdr) {
+ for (var i = 0; i < gArrayHdrs.length; i++) {
+ if (gArrayHdrs[i] == hdr) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function onRemoveHeader() {
+ var listitem = gHdrsList.selectedItems[0];
+ if (!listitem) {
+ return;
+ }
+ listitem.remove();
+ var selectedHdr = listitem.firstElementChild.getAttribute("value").trim();
+ for (let i = 0; i < gArrayHdrs.length; i++) {
+ if (gArrayHdrs[i] == selectedHdr) {
+ gArrayHdrs.splice(i, 1);
+ break;
+ }
+ }
+}
+
+function addRow(newHdr) {
+ return gHdrsList.appendItem(newHdr, "");
+}
+
+function updateAddButton(aDisable) {
+ // only update the button if the disabled state changed
+ if (aDisable == gAddButton.disabled) {
+ return;
+ }
+
+ gAddButton.disabled = aDisable;
+ document.querySelector("dialog").defaultButton = aDisable
+ ? "accept"
+ : "extra1";
+}
+
+function updateRemoveButton() {
+ var headerSelected = gHdrsList.selectedItems.length > 0;
+ gRemoveButton.disabled = !headerSelected;
+ if (gRemoveButton.disabled) {
+ gHeaderInputElement.focus();
+ }
+}
+
+// Remove whitespace from both ends of a string
+function TrimString(string) {
+ if (!string) {
+ return "";
+ }
+ return string.trim();
+}
diff --git a/comm/mailnews/search/content/CustomHeaders.xhtml b/comm/mailnews/search/content/CustomHeaders.xhtml
new file mode 100644
index 0000000000..9a2ac8468f
--- /dev/null
+++ b/comm/mailnews/search/content/CustomHeaders.xhtml
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+#ifdef MOZ_THUNDERBIRD
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+#else
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+#endif
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/CustomHeaders.dtd">
+<html id="customHeaderDialog" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="450" height="375"
+ persist="width height screenX screenY"
+ scrolling="false">
+<head>
+ <title>&window.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/CustomHeaders.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog buttons="accept,cancel,extra1,extra2">
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+ <stringbundle id="bundle_custom" src="chrome://messenger/locale/custom.properties"/>
+
+ <hbox flex="1">
+ <vbox flex="1">
+ <label id="headerInputLabel"
+ accesskey="&newMsgHeader.accesskey;"
+ control="headerInput"
+ value="&newMsgHeader.label;"/>
+ <html:input id="headerInput"
+ type="text"
+ aria-labelledby="headerInputLabel"
+ class="input-inline"
+ oninput="onTextInput();"/>
+ <richlistbox id="headerList"
+ class="theme-listbox"
+ flex="1"
+ onselect="updateRemoveButton();" />
+ </vbox>
+ <vbox>
+ <label value=""/>
+ <button id="addButton"
+ label="&addButton.label;"
+ accesskey="&addButton.accesskey;"
+ dlgtype="extra1"/>
+ <button id="removeButton"
+ label="&removeButton.label;"
+ accesskey="&removeButton.accesskey;"
+ dlgtype="extra2"/>
+ </vbox>
+ </hbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/search/content/FilterEditor.js b/comm/mailnews/search/content/FilterEditor.js
new file mode 100644
index 0000000000..3b93773192
--- /dev/null
+++ b/comm/mailnews/search/content/FilterEditor.js
@@ -0,0 +1,809 @@
+/* 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/. */
+
+/* import-globals-from searchTerm.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+// The actual filter that we're editing if it is a _saved_ filter or prefill;
+// void otherwise.
+var gFilter;
+// cache the key elements we need
+var gFilterList;
+// The filter name as it appears in the "Filter Name" field of dialog.
+var gFilterNameElement;
+var gFilterTypeSelector;
+var gFilterBundle;
+var gPreFillName;
+var gFilterActionList;
+var gCustomActions = null;
+var gFilterType;
+var gFilterPosition = 0;
+
+var gFilterActionStrings = [
+ "none",
+ "movemessage",
+ "setpriorityto",
+ "deletemessage",
+ "markasread",
+ "ignorethread",
+ "watchthread",
+ "markasflagged",
+ "label",
+ "replytomessage",
+ "forwardmessage",
+ "stopexecution",
+ "deletefrompopserver",
+ "leaveonpopserver",
+ "setjunkscore",
+ "fetchfrompopserver",
+ "copymessage",
+ "addtagtomessage",
+ "ignoresubthread",
+ "markasunread",
+];
+
+// A temporary filter with the current state of actions in the UI.
+var gTempFilter = null;
+// nsIMsgRuleAction[] - the currently defined actions in the order they will be run.
+var gActionListOrdered = null;
+
+var gFilterEditorMsgWindow = null;
+
+window.addEventListener("DOMContentLoaded", filterEditorOnLoad, { once: true });
+document.addEventListener("dialogaccept", onAccept);
+
+function filterEditorOnLoad() {
+ getCustomActions();
+ initializeSearchWidgets();
+ initializeFilterWidgets();
+
+ gFilterBundle = document.getElementById("bundle_filter");
+
+ if ("arguments" in window && window.arguments[0]) {
+ var args = window.arguments[0];
+
+ if ("filterList" in args) {
+ gFilterList = args.filterList;
+ // the postPlugin filters cannot be applied to servers that are
+ // deferred, (you must define them on the deferredTo server instead).
+ let server = gFilterList.folder.server;
+ if (server.rootFolder != server.rootMsgFolder) {
+ gFilterTypeSelector.disableDeferredAccount();
+ }
+ }
+
+ if ("filterPosition" in args) {
+ gFilterPosition = args.filterPosition;
+ }
+
+ if ("filter" in args) {
+ // editing a filter
+ gFilter = window.arguments[0].filter;
+ initializeDialog(gFilter);
+ } else {
+ if (gFilterList) {
+ setSearchScope(getScopeFromFilterList(gFilterList));
+ }
+ // if doing prefill filter create a new filter and populate it.
+ if ("filterName" in args) {
+ gPreFillName = args.filterName;
+
+ // Passing null as the parameter to createFilter to keep the name empty
+ // until later where we assign the name.
+ gFilter = gFilterList.createFilter(null);
+
+ var term = gFilter.createTerm();
+
+ term.attrib = Ci.nsMsgSearchAttrib.Default;
+ if ("fieldName" in args && args.fieldName) {
+ // fieldName should contain the name of the field in which to search,
+ // from nsMsgSearchTerm.cpp::SearchAttribEntryTable, e.g. "to" or "cc"
+ try {
+ term.attrib = term.getAttributeFromString(args.fieldName);
+ } catch (e) {
+ /* Invalid string is fine, just ignore it. */
+ }
+ }
+ if (term.attrib == Ci.nsMsgSearchAttrib.Default) {
+ term.attrib = Ci.nsMsgSearchAttrib.Sender;
+ }
+
+ term.op = Ci.nsMsgSearchOp.Is;
+ term.booleanAnd = gSearchBooleanRadiogroup.value == "and";
+
+ var termValue = term.value;
+ termValue.attrib = term.attrib;
+ termValue.str = gPreFillName;
+
+ term.value = termValue;
+
+ gFilter.appendTerm(term);
+
+ // the default action for news filters is Delete
+ // for everything else, it's MoveToFolder
+ var filterAction = gFilter.createAction();
+ filterAction.type =
+ getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter
+ ? Ci.nsMsgFilterAction.Delete
+ : Ci.nsMsgFilterAction.MoveToFolder;
+ gFilter.appendAction(filterAction);
+ initializeDialog(gFilter);
+ } else if ("copiedFilter" in args) {
+ // we are copying a filter
+ let copiedFilter = args.copiedFilter;
+ let copiedName = gFilterBundle.getFormattedString(
+ "copyToNewFilterName",
+ [copiedFilter.filterName]
+ );
+ let newFilter = gFilterList.createFilter(copiedName);
+
+ // copy the actions
+ for (let i = 0; i < copiedFilter.actionCount; i++) {
+ let filterAction = copiedFilter.getActionAt(i);
+ newFilter.appendAction(filterAction);
+ }
+
+ // copy the search terms
+ for (let searchTerm of copiedFilter.searchTerms) {
+ let newTerm = newFilter.createTerm();
+ newTerm.attrib = searchTerm.attrib;
+ newTerm.op = searchTerm.op;
+ newTerm.booleanAnd = searchTerm.booleanAnd;
+ newTerm.value = searchTerm.value;
+ newFilter.appendTerm(newTerm);
+ }
+
+ newFilter.filterType = copiedFilter.filterType;
+
+ gPreFillName = copiedName;
+ gFilter = newFilter;
+
+ initializeDialog(gFilter);
+
+ // We reset the filter name, because otherwise the saveFilter()
+ // function thinks we are editing a filter, and will thus skip the name
+ // uniqueness check.
+ gFilter.filterName = "";
+ } else {
+ // fake the first more button press
+ onMore(null);
+ }
+ }
+ }
+
+ if (!gFilter) {
+ // This is a new filter. Set to both Incoming and Manual contexts.
+ gFilterTypeSelector.setType(
+ Ci.nsMsgFilterType.Incoming | Ci.nsMsgFilterType.Manual
+ );
+ }
+
+ // in the case of a new filter, we may not have an action row yet.
+ ensureActionRow();
+ gFilterType = gFilterTypeSelector.getType();
+
+ gFilterNameElement.select();
+ // This call is required on mac and linux. It has no effect under win32. See bug 94800.
+ gFilterNameElement.focus();
+}
+
+function onEnterInSearchTerm(event) {
+ if (event.ctrlKey || (Services.appinfo.OS == "Darwin" && event.metaKey)) {
+ // If accel key (Ctrl on Win/Linux, Cmd on Mac) was held too, accept the dialog.
+ document.querySelector("dialog").acceptDialog();
+ } else {
+ // If only plain Enter was pressed, add a new rule line.
+ onMore(event);
+ }
+}
+
+function onAccept(event) {
+ try {
+ if (!saveFilter()) {
+ event.preventDefault();
+ return;
+ }
+ } catch (e) {
+ console.error(e);
+ event.preventDefault();
+ return;
+ }
+
+ // parent should refresh filter list..
+ // this should REALLY only happen when some criteria changes that
+ // are displayed in the filter dialog, like the filter name
+ window.arguments[0].refresh = true;
+ window.arguments[0].newFilter = gFilter;
+}
+
+function duplicateFilterNameExists(filterName) {
+ if (gFilterList) {
+ for (var i = 0; i < gFilterList.filterCount; i++) {
+ if (filterName == gFilterList.getFilterAt(i).filterName) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function getScopeFromFilterList(filterList) {
+ if (!filterList) {
+ dump("yikes, null filterList\n");
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+ return filterList.folder.server.filterScope;
+}
+
+function getScope(filter) {
+ return getScopeFromFilterList(filter.filterList);
+}
+
+function initializeFilterWidgets() {
+ gFilterNameElement = document.getElementById("filterName");
+ gFilterActionList = document.getElementById("filterActionList");
+ initializeFilterTypeSelector();
+}
+
+function initializeFilterTypeSelector() {
+ /**
+ * This object controls code interaction with the widget allowing specifying
+ * the filter type (event when the filter is run).
+ */
+ gFilterTypeSelector = {
+ checkBoxManual: document.getElementById("runManual"),
+ checkBoxIncoming: document.getElementById("runIncoming"),
+
+ menulistIncoming: document.getElementById("pluginsRunOrder"),
+
+ menuitemBeforePlugins: document.getElementById("runBeforePlugins"),
+ menuitemAfterPlugins: document.getElementById("runAfterPlugins"),
+
+ checkBoxArchive: document.getElementById("runArchive"),
+ checkBoxOutgoing: document.getElementById("runOutgoing"),
+ checkBoxPeriodic: document.getElementById("runPeriodic"),
+
+ /**
+ * Returns the currently set filter type (checkboxes) in terms
+ * of a Ci.Ci.nsMsgFilterType value.
+ */
+ getType() {
+ let type = Ci.nsMsgFilterType.None;
+
+ if (this.checkBoxManual.checked) {
+ type |= Ci.nsMsgFilterType.Manual;
+ }
+
+ if (this.checkBoxIncoming.checked) {
+ if (this.menulistIncoming.selectedItem == this.menuitemAfterPlugins) {
+ type |= Ci.nsMsgFilterType.PostPlugin;
+ } else if (
+ getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter
+ ) {
+ type |= Ci.nsMsgFilterType.NewsRule;
+ } else {
+ type |= Ci.nsMsgFilterType.InboxRule;
+ }
+ }
+
+ if (this.checkBoxArchive.checked) {
+ type |= Ci.nsMsgFilterType.Archive;
+ }
+
+ if (this.checkBoxOutgoing.checked) {
+ type |= Ci.nsMsgFilterType.PostOutgoing;
+ }
+
+ if (this.checkBoxPeriodic.checked) {
+ type |= Ci.nsMsgFilterType.Periodic;
+ }
+
+ return type;
+ },
+
+ /**
+ * Sets the checkboxes to represent the filter type passed in.
+ *
+ * @param aType the filter type to set in terms
+ * of Ci.Ci.nsMsgFilterType values.
+ */
+ setType(aType) {
+ // If there is no type (event) requested, force "when manually run"
+ if (aType == Ci.nsMsgFilterType.None) {
+ aType = Ci.nsMsgFilterType.Manual;
+ }
+
+ this.checkBoxManual.checked = aType & Ci.nsMsgFilterType.Manual;
+
+ this.checkBoxIncoming.checked =
+ aType & (Ci.nsMsgFilterType.PostPlugin | Ci.nsMsgFilterType.Incoming);
+
+ this.menulistIncoming.selectedItem =
+ aType & Ci.nsMsgFilterType.PostPlugin
+ ? this.menuitemAfterPlugins
+ : this.menuitemBeforePlugins;
+
+ this.checkBoxArchive.checked = aType & Ci.nsMsgFilterType.Archive;
+
+ this.checkBoxOutgoing.checked = aType & Ci.nsMsgFilterType.PostOutgoing;
+
+ this.checkBoxPeriodic.checked = aType & Ci.nsMsgFilterType.Periodic;
+ const periodMinutes = gFilterList.folder.server.getIntValue(
+ "periodicFilterRateMinutes"
+ );
+ document.getElementById("runPeriodic").label = PluralForm.get(
+ periodMinutes,
+ gFilterBundle.getString("contextPeriodic.label")
+ ).replace("#1", periodMinutes);
+
+ this.updateClassificationMenu();
+ },
+
+ /**
+ * Enable the "before/after classification" menulist depending on
+ * whether "run when incoming mail" is selected.
+ */
+ updateClassificationMenu() {
+ this.menulistIncoming.disabled = !this.checkBoxIncoming.checked;
+ updateFilterType();
+ },
+
+ /**
+ * Disable the options unsuitable for deferred accounts.
+ */
+ disableDeferredAccount() {
+ this.menuitemAfterPlugins.disabled = true;
+ this.checkBoxOutgoing.disabled = true;
+ },
+ };
+}
+
+function initializeDialog(filter) {
+ gFilterNameElement.value = filter.filterName;
+ gFilterTypeSelector.setType(filter.filterType);
+
+ let numActions = filter.actionCount;
+ for (let actionIndex = 0; actionIndex < numActions; actionIndex++) {
+ let filterAction = filter.getActionAt(actionIndex);
+
+ let newActionRow = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ newActionRow.setAttribute("initialActionIndex", actionIndex);
+ newActionRow.className = "ruleaction";
+ gFilterActionList.appendChild(newActionRow);
+ newActionRow.setAttribute(
+ "value",
+ filterAction.type == Ci.nsMsgFilterAction.Custom
+ ? filterAction.customId
+ : gFilterActionStrings[filterAction.type]
+ );
+ newActionRow.setAttribute("onfocus", "this.storeFocus();");
+ }
+
+ var gSearchScope = getFilterScope(
+ getScope(filter),
+ filter.filterType,
+ filter.filterList
+ );
+ initializeSearchRows(gSearchScope, filter.searchTerms);
+ setFilterScope(filter.filterType, filter.filterList);
+}
+
+function ensureActionRow() {
+ // make sure we have at least one action row visible to the user
+ if (!gFilterActionList.getRowCount()) {
+ let newActionRow = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ newActionRow.className = "ruleaction";
+ gFilterActionList.appendChild(newActionRow);
+ newActionRow.mRemoveButton.disabled = true;
+ }
+}
+
+// move to overlay
+function saveFilter() {
+ // See if at least one filter type (activation event) is selected.
+ if (gFilterType == Ci.nsMsgFilterType.None) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("mustHaveFilterTypeTitle"),
+ gFilterBundle.getString("mustHaveFilterTypeMessage")
+ );
+ return false;
+ }
+
+ let filterName = gFilterNameElement.value;
+ // If we think have a duplicate, then we need to check that if we
+ // have an original filter name (i.e. we are editing a filter), then
+ // we must check that the original is not the current as that is what
+ // the duplicateFilterNameExists function will have picked up.
+ if (
+ (!gFilter || gFilter.filterName != filterName) &&
+ duplicateFilterNameExists(filterName)
+ ) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("cannotHaveDuplicateFilterTitle"),
+ gFilterBundle.getString("cannotHaveDuplicateFilterMessage")
+ );
+ return false;
+ }
+
+ // Check that all of the search attributes and operators are valid.
+ function rule_desc(index, obj) {
+ return (
+ index +
+ 1 +
+ " (" +
+ obj.searchattribute.label +
+ ", " +
+ obj.searchoperator.label +
+ ")"
+ );
+ }
+
+ let invalidRule = false;
+ for (let index = 0; index < gSearchTerms.length; index++) {
+ let obj = gSearchTerms[index].obj;
+ // We don't need to check validity of matchAll terms
+ if (obj.matchAll) {
+ continue;
+ }
+
+ // the term might be an offscreen one that we haven't initialized yet
+ let searchTerm = obj.searchTerm;
+ if (!searchTerm && !gSearchTerms[index].initialized) {
+ continue;
+ }
+
+ if (isNaN(obj.searchattribute.value)) {
+ // is this a custom term?
+ let customTerm = MailServices.filters.getCustomTerm(
+ obj.searchattribute.value
+ );
+ if (!customTerm) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because custom search term '" +
+ obj.searchattribute.value +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not found"
+ );
+ } else if (
+ !customTerm.getAvailable(obj.searchScope, obj.searchattribute.value)
+ ) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because custom search term '" +
+ customTerm.name +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not available"
+ );
+ }
+ } else {
+ let otherHeader = Ci.nsMsgSearchAttrib.OtherHeader;
+ let attribValue =
+ obj.searchattribute.value > otherHeader
+ ? otherHeader
+ : obj.searchattribute.value;
+ if (
+ !obj.searchattribute.validityTable.getAvailable(
+ attribValue,
+ obj.searchoperator.value
+ )
+ ) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because standard search term '" +
+ attribValue +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not available in this context"
+ );
+ }
+ }
+
+ if (invalidRule) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("searchTermsInvalidTitle"),
+ gFilterBundle.getFormattedString("searchTermsInvalidRule", [
+ obj.searchattribute.label,
+ obj.searchoperator.label,
+ ])
+ );
+ return false;
+ }
+ }
+
+ // before we go any further, validate each specified filter action, abort the save
+ // if any of the actions is invalid...
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ var listItem = gFilterActionList.getItemAtIndex(index);
+ if (!listItem.validateAction()) {
+ return false;
+ }
+ }
+
+ // if we made it here, all of the actions are valid, so go ahead and save the filter
+ let isNewFilter;
+ if (!gFilter) {
+ // This is a new filter
+ gFilter = gFilterList.createFilter(filterName);
+ isNewFilter = true;
+ gFilter.enabled = true;
+ } else {
+ // We are working with an existing filter object,
+ // either editing or using prefill
+ gFilter.filterName = filterName;
+ // Prefilter is treated as a new filter.
+ if (gPreFillName) {
+ isNewFilter = true;
+ gFilter.enabled = true;
+ } else {
+ isNewFilter = false;
+ }
+
+ gFilter.clearActionList();
+ }
+
+ // add each filteraction to the filter
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ gFilterActionList.getItemAtIndex(index).saveToFilter(gFilter);
+ }
+
+ // If we do not have a filter name at this point, generate one.
+ if (!gFilter.filterName) {
+ AssignMeaningfulName();
+ }
+
+ gFilter.filterType = gFilterType;
+ gFilter.searchTerms = saveSearchTerms(gFilter.searchTerms, gFilter);
+
+ if (isNewFilter) {
+ // new filter - insert into gFilterList
+ gFilterList.insertFilterAt(gFilterPosition, gFilter);
+ }
+
+ // success!
+ return true;
+}
+
+/**
+ * Check if the list of actions the user created will be executed in a different order.
+ * Exposes a note to the user if that is the case.
+ */
+function checkActionsReorder() {
+ setTimeout(_checkActionsReorder, 0);
+}
+
+/**
+ * This should be called from setTimeout otherwise some of the elements calling
+ * may not be fully initialized yet (e.g. we get ".saveToFilter is not a function").
+ * It is OK to schedule multiple timeouts with this function.
+ */
+function _checkActionsReorder() {
+ // Create a temporary disposable filter and add current actions to it.
+ if (!gTempFilter) {
+ gTempFilter = gFilterList.createFilter("");
+ } else {
+ gTempFilter.clearActionList();
+ }
+
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ gFilterActionList.getItemAtIndex(index).saveToFilter(gTempFilter);
+ }
+
+ // Now get the actions out of the filter in the order they will be executed in.
+ gActionListOrdered = gTempFilter.sortedActionList;
+
+ // Compare the two lists.
+ let statusBar = document.getElementById("statusbar");
+ for (let index = 0; index < gActionListOrdered.length; index++) {
+ if (index != gTempFilter.getActionIndex(gActionListOrdered[index])) {
+ // If the lists are not the same unhide the status bar and show warning.
+ statusBar.style.visibility = "visible";
+ return;
+ }
+ }
+
+ statusBar.style.visibility = "hidden";
+}
+
+/**
+ * Show a dialog with the ordered list of actions.
+ * The fetching of action label and argument is separated from checkActionsReorder
+ * function to make that one more lightweight. The list is built only upon
+ * user request.
+ */
+function showActionsOrder() {
+ // Fetch the actions and arguments as a string.
+ let actionStrings = [];
+ for (let i = 0; i < gFilterActionList.itemCount; i++) {
+ let ruleAction = gFilterActionList.getItemAtIndex(i);
+ let actionTarget = ruleAction.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let actionItemLabel = actionItem && actionItem.children[0].label;
+
+ let actionString = {
+ label: ruleAction.mRuleActionType.label,
+ argument: "",
+ };
+ if (actionItem) {
+ if (actionItemLabel) {
+ actionString.argument = actionItemLabel;
+ } else {
+ actionString.argument = actionItem.children[0].value;
+ }
+ }
+ actionStrings.push(actionString);
+ }
+
+ // Present a nicely formatted list of action names and arguments.
+ let actionList = gFilterBundle.getString("filterActionOrderExplanation");
+ for (let i = 0; i < gActionListOrdered.length; i++) {
+ let actionIndex = gTempFilter.getActionIndex(gActionListOrdered[i]);
+ let action = actionStrings[actionIndex];
+ actionList += gFilterBundle.getFormattedString("filterActionItem", [
+ i + 1,
+ action.label,
+ action.argument,
+ ]);
+ }
+
+ Services.prompt.confirmEx(
+ window,
+ gFilterBundle.getString("filterActionOrderTitle"),
+ actionList,
+ Services.prompt.BUTTON_TITLE_OK,
+ null,
+ null,
+ null,
+ null,
+ { value: false }
+ );
+}
+
+function AssignMeaningfulName() {
+ // termRoot points to the first search object, which is the one we care about.
+ let termRoot = gSearchTerms[0].obj;
+ // stub is used as the base name for a filter.
+ let stub;
+
+ // If this is a Match All Messages Filter, we already know the name to assign.
+ if (termRoot.matchAll) {
+ stub = gFilterBundle.getString("matchAllFilterName");
+ } else {
+ // Assign a name based on the first search term.
+ let term = termRoot.searchattribute.label;
+ let operator = termRoot.searchoperator.label;
+ let value = termRoot.searchvalue.getReadableValue();
+ stub = gFilterBundle.getFormattedString("filterAutoNameStr", [
+ term,
+ operator,
+ value,
+ ]);
+ }
+
+ // Whatever name we have used, 'uniquify' it.
+ let tempName = stub;
+ let count = 1;
+ while (duplicateFilterNameExists(tempName)) {
+ count++;
+ tempName = `${stub} ${count}`;
+ }
+ gFilter.filterName = tempName;
+}
+
+function UpdateAfterCustomHeaderChange() {
+ updateSearchAttributes();
+}
+
+// if you use msgWindow, please make sure that destructor gets called when you close the "window"
+function GetFilterEditorMsgWindow() {
+ if (!gFilterEditorMsgWindow) {
+ var msgWindowContractID = "@mozilla.org/messenger/msgwindow;1";
+ var nsIMsgWindow = Ci.nsIMsgWindow;
+ gFilterEditorMsgWindow =
+ Cc[msgWindowContractID].createInstance(nsIMsgWindow);
+ gFilterEditorMsgWindow.domWindow = window;
+ gFilterEditorMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+ }
+ return gFilterEditorMsgWindow;
+}
+
+function SetBusyCursor(window, enable) {
+ // setCursor() is only available for chrome windows.
+ // However one of our frames is the start page which
+ // is a non-chrome window, so check if this window has a
+ // setCursor method
+ if ("setCursor" in window) {
+ if (enable) {
+ window.setCursor("wait");
+ } else {
+ window.setCursor("auto");
+ }
+ }
+}
+
+/* globals openHelp */
+// suite/components/helpviewer/content/contextHelp.js
+function doHelpButton() {
+ openHelp("mail-filters");
+}
+
+function getCustomActions() {
+ if (!gCustomActions) {
+ gCustomActions = MailServices.filters.getCustomActions();
+ }
+}
+
+function updateFilterType() {
+ gFilterType = gFilterTypeSelector.getType();
+ setFilterScope(gFilterType, gFilterList);
+
+ // set valid actions
+ var ruleActions = gFilterActionList.getElementsByAttribute(
+ "class",
+ "ruleaction"
+ );
+ for (var i = 0; i < ruleActions.length; i++) {
+ ruleActions[i].mRuleActionType.hideInvalidActions();
+ }
+}
+
+// Given a filter type, set the global search scope to the filter scope
+function setFilterScope(aFilterType, aFilterList) {
+ let filterScope = getFilterScope(
+ getScopeFromFilterList(aFilterList),
+ aFilterType,
+ aFilterList
+ );
+ setSearchScope(filterScope);
+}
+
+//
+// Given the base filter scope for a server, and the filter
+// type, return the scope used for filter. This assumes a
+// hierarchy of contexts, with incoming the most restrictive,
+// followed by manual and post-plugin.
+function getFilterScope(aServerFilterScope, aFilterType, aFilterList) {
+ if (aFilterType & Ci.nsMsgFilterType.Incoming) {
+ return aServerFilterScope;
+ }
+
+ // Manual or PostPlugin
+ // local mail allows body and junk types
+ if (aServerFilterScope == Ci.nsMsgSearchScope.offlineMailFilter) {
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+ // IMAP and NEWS online don't allow body
+ return Ci.nsMsgSearchScope.onlineManual;
+}
+
+/**
+ * Re-focus the action that was focused before focus was lost.
+ */
+function setLastActionFocus() {
+ let lastAction = gFilterActionList.getAttribute("focusedAction");
+ if (!lastAction || lastAction < 0) {
+ lastAction = 0;
+ }
+ if (lastAction >= gFilterActionList.itemCount) {
+ lastAction = gFilterActionList.itemCount - 1;
+ }
+
+ gFilterActionList.getItemAtIndex(lastAction).mRuleActionType.focus();
+}
diff --git a/comm/mailnews/search/content/FilterEditor.xhtml b/comm/mailnews/search/content/FilterEditor.xhtml
new file mode 100644
index 0000000000..00188392bc
--- /dev/null
+++ b/comm/mailnews/search/content/FilterEditor.xhtml
@@ -0,0 +1,136 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/filterEditor.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % filterEditorDTD SYSTEM "chrome://messenger/locale/FilterEditor.dtd">
+ %filterEditorDTD;
+ <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+ %searchTermDTD;
+]>
+<html id="FilterEditor" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:filtereditor"
+ lightweightthemes="true"
+ style="min-width: 900px; min-height: 600px;"
+ scrolling="false">
+<head>
+ <title>&window.title;</title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchWidgets.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailWindowOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchTerm.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dateFormat.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/FilterEditor.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog buttons="accept,cancel">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+
+ <commandset>
+ <command id="cmd_updateFilterType" oncommand="updateFilterType();"/>
+ <command id="cmd_updateClassificationMenu" oncommand="gFilterTypeSelector.updateClassificationMenu();"/>
+ </commandset>
+
+ <html:div id="filterNameBox" class="input-container">
+ <label id="filterNameLabel"
+ value="&filterName.label;"
+ accesskey="&filterName.accesskey;"
+ control="filterName"/>
+ <html:input id="filterName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="filterNameLabel"/>
+ </html:div>
+
+ <html:fieldset id="applyFiltersSettings">
+ <html:legend>&contextDesc.label;</html:legend>
+ <vbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runManual"
+ label="&contextManual.label;"
+ accesskey="&contextManual.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runIncoming"
+ label="&contextIncomingMail.label;"
+ accesskey="&contextIncomingMail.accesskey;"
+ command="cmd_updateClassificationMenu"/>
+ <menulist id="pluginsRunOrder"
+ command="cmd_updateFilterType">
+ <menupopup>
+ <menuitem id="runBeforePlugins"
+ label="&contextBeforeCls.label;"/>
+ <menuitem id="runAfterPlugins"
+ label="&contextAfterCls.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runArchive"
+ label="&contextArchive.label;"
+ accesskey="&contextArchive.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runOutgoing"
+ label="&contextOutgoing.label;"
+ accesskey="&contextOutgoing.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runPeriodic"
+ accesskey="&contextPeriodic.accesskey;"
+ command="cmd_updateFilterType"/>
+ <label id="periodLength"/>
+ </hbox>
+ </vbox>
+ </html:fieldset>
+
+
+ <vbox id="searchTermListBox">
+#include searchTerm.inc.xhtml
+
+ <splitter id="gray_horizontal_splitter" persist="state" orient="vertical"/>
+
+ <vbox id="filterActionsBox">
+ <label value="&filterActionDesc.label;"
+ accesskey="&filterActionDesc.accesskey;"
+ control="filterActionList"/>
+ <richlistbox id="filterActionList"
+ flex="1"
+ style="min-height: 100px;"
+ onfocus="setLastActionFocus();"
+ focusedAction="0">
+ </richlistbox>
+ </vbox>
+
+ <vbox id="statusbar" style="visibility: hidden;">
+ <hbox align="center">
+ <label>
+ &filterActionOrderWarning.label;
+ </label>
+ <label id="seeExecutionOrder" class="text-link"
+ onclick="showActionsOrder();">&filterActionOrder.label;</label>
+ </hbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/search/content/searchTerm.inc.xhtml b/comm/mailnews/search/content/searchTerm.inc.xhtml
new file mode 100644
index 0000000000..07e3407a4c
--- /dev/null
+++ b/comm/mailnews/search/content/searchTerm.inc.xhtml
@@ -0,0 +1,27 @@
+# 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/.
+
+ <radiogroup id="booleanAndGroup" orient="horizontal" value="and"
+ oncommand="booleanChanged(event);">
+ <radio value="and" label="&matchAll.label;"
+ accesskey="&matchAll.accesskey;" flex="1"/>
+ <radio value="or" label="&matchAny.label;"
+ accesskey="&matchAny.accesskey;" flex="1"/>
+ <radio value="matchAll" id="matchAllItem" label="&matchAllMsgs.label;"
+ accesskey="&matchAllMsgs.accesskey;" flex="1"/>
+ </radiogroup>
+
+ <hbox id="searchTermBox" style="flex: 1 1 0;">
+ <hbox id="searchterms" class="themeable-brighttext"/>
+ <richlistbox id="searchTermList" flex="1">
+ <treecols hidden="true">
+ <treecol style="flex: &searchTermListAttributesFlexValue; auto"/>
+ <treecol style="flex: &searchTermListOperatorsFlexValue; auto"/>
+ <treecol style="flex: &searchTermListValueFlexValue; auto"/>
+ <treecol class="filler"/>
+ </treecols>
+ </richlistbox>
+
+ </hbox>
+ </vbox>
diff --git a/comm/mailnews/search/content/searchTerm.js b/comm/mailnews/search/content/searchTerm.js
new file mode 100644
index 0000000000..10d085a5e0
--- /dev/null
+++ b/comm/mailnews/search/content/searchTerm.js
@@ -0,0 +1,568 @@
+/* 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/. */
+
+// abSearchDialog.js
+/* globals GetScopeForDirectoryURI */
+
+var gTotalSearchTerms = 0;
+var gSearchTermList;
+var gSearchTerms = [];
+var gSearchRemovedTerms = [];
+var gSearchScope;
+var gSearchBooleanRadiogroup;
+
+var gUniqueSearchTermCounter = 0; // gets bumped every time we add a search term so we can always
+// dynamically generate unique IDs for the terms.
+
+// cache these so we don't have to hit the string bundle for them
+var gMoreButtonTooltipText;
+var gLessButtonTooltipText;
+var gLoading = true;
+
+function searchTermContainer() {}
+
+searchTermContainer.prototype = {
+ internalSearchTerm: "",
+ internalBooleanAnd: "",
+
+ // this.searchTerm: the actual nsIMsgSearchTerm object
+ get searchTerm() {
+ return this.internalSearchTerm;
+ },
+ set searchTerm(val) {
+ this.internalSearchTerm = val;
+
+ var term = val;
+ // val is a nsIMsgSearchTerm
+ var searchAttribute = this.searchattribute;
+ var searchOperator = this.searchoperator;
+ var searchValue = this.searchvalue;
+
+ // now reflect all attributes of the searchterm into the widgets
+ if (searchAttribute) {
+ // for custom, the value is the custom id, not the integer attribute
+ if (term.attrib == Ci.nsMsgSearchAttrib.Custom) {
+ searchAttribute.value = term.customId;
+ } else {
+ searchAttribute.value = term.attrib;
+ }
+ }
+ if (searchOperator) {
+ searchOperator.value = val.op;
+ }
+ if (searchValue) {
+ searchValue.value = term.value;
+ }
+
+ this.booleanAnd = val.booleanAnd;
+ this.matchAll = val.matchAll;
+ },
+
+ // searchscope - just forward to the searchattribute
+ get searchScope() {
+ if (this.searchattribute) {
+ return this.searchattribute.searchScope;
+ }
+ return undefined;
+ },
+ set searchScope(val) {
+ var searchAttribute = this.searchattribute;
+ if (searchAttribute) {
+ searchAttribute.searchScope = val;
+ }
+ },
+
+ saveId(element, slot) {
+ this[slot] = element.id;
+ },
+
+ getElement(slot) {
+ return document.getElementById(this[slot]);
+ },
+
+ // three well-defined properties:
+ // searchattribute, searchoperator, searchvalue
+ // the trick going on here is that we're storing the Element's Id,
+ // not the element itself, because the XBL object may change out
+ // from underneath us
+ get searchattribute() {
+ return this.getElement("internalSearchAttributeId");
+ },
+ set searchattribute(val) {
+ this.saveId(val, "internalSearchAttributeId");
+ },
+ get searchoperator() {
+ return this.getElement("internalSearchOperatorId");
+ },
+ set searchoperator(val) {
+ this.saveId(val, "internalSearchOperatorId");
+ },
+ get searchvalue() {
+ return this.getElement("internalSearchValueId");
+ },
+ set searchvalue(val) {
+ this.saveId(val, "internalSearchValueId");
+ },
+
+ booleanNodes: null,
+ get booleanAnd() {
+ return this.internalBooleanAnd;
+ },
+ set booleanAnd(val) {
+ this.internalBooleanAnd = val;
+ },
+
+ save() {
+ var searchTerm = this.searchTerm;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+
+ if (isNaN(this.searchattribute.value)) {
+ // is this a custom term?
+ searchTerm.attrib = nsMsgSearchAttrib.Custom;
+ searchTerm.customId = this.searchattribute.value;
+ } else {
+ searchTerm.attrib = this.searchattribute.value;
+ }
+
+ if (
+ this.searchattribute.value > nsMsgSearchAttrib.OtherHeader &&
+ this.searchattribute.value < nsMsgSearchAttrib.kNumMsgSearchAttributes
+ ) {
+ searchTerm.arbitraryHeader = this.searchattribute.label;
+ }
+ searchTerm.op = this.searchoperator.value;
+ if (this.searchvalue.value) {
+ this.searchvalue.save();
+ } else {
+ this.searchvalue.saveTo(searchTerm.value);
+ }
+ searchTerm.value = this.searchvalue.value;
+ searchTerm.booleanAnd = this.booleanAnd;
+ searchTerm.matchAll = this.matchAll;
+ },
+ // if you have a search term element with no search term
+ saveTo(searchTerm) {
+ this.internalSearchTerm = searchTerm;
+ this.save();
+ },
+};
+
+function initializeSearchWidgets() {
+ gSearchBooleanRadiogroup = document.getElementById("booleanAndGroup");
+ gSearchTermList = document.getElementById("searchTermList");
+
+ // initialize some strings
+ var bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search.properties"
+ );
+ gMoreButtonTooltipText = bundle.GetStringFromName("moreButtonTooltipText");
+ gLessButtonTooltipText = bundle.GetStringFromName("lessButtonTooltipText");
+}
+
+function initializeBooleanWidgets() {
+ var booleanAnd = true;
+ var matchAll = false;
+ // get the boolean value from the first term
+ var firstTerm = gSearchTerms[0].searchTerm;
+ if (firstTerm) {
+ // If there is a second term, it should actually define whether we're
+ // using 'and' or not. Note that our UI is not as rich as the
+ // underlying search model, so there's the potential to lose here when
+ // grouping is involved.
+ booleanAnd =
+ gSearchTerms.length > 1
+ ? gSearchTerms[1].searchTerm.booleanAnd
+ : firstTerm.booleanAnd;
+ matchAll = firstTerm.matchAll;
+ }
+ // target radio items have value="and" or value="or" or "all"
+ if (matchAll) {
+ gSearchBooleanRadiogroup.value = "matchAll";
+ } else if (booleanAnd) {
+ gSearchBooleanRadiogroup.value = "and";
+ } else {
+ gSearchBooleanRadiogroup.value = "or";
+ }
+ var searchTerms = document.getElementById("searchTermList");
+ if (searchTerms) {
+ updateSearchTermsListbox(matchAll);
+ }
+}
+
+function initializeSearchRows(scope, searchTerms) {
+ for (let i = 0; i < searchTerms.length; i++) {
+ let searchTerm = searchTerms[i];
+ createSearchRow(i, scope, searchTerm, false);
+ gTotalSearchTerms++;
+ }
+ initializeBooleanWidgets();
+ updateRemoveRowButton();
+}
+
+/**
+ * Enables/disables all the visible elements inside the search terms listbox.
+ *
+ * @param matchAllValue boolean value from the first search term
+ */
+function updateSearchTermsListbox(matchAllValue) {
+ var searchTerms = document.getElementById("searchTermList");
+ searchTerms.setAttribute("disabled", matchAllValue);
+ var searchAttributeList =
+ searchTerms.getElementsByTagName("search-attribute");
+ var searchOperatorList = searchTerms.getElementsByTagName("search-operator");
+ var searchValueList = searchTerms.getElementsByTagName("search-value");
+ for (let i = 0; i < searchAttributeList.length; i++) {
+ searchAttributeList[i].setAttribute("disabled", matchAllValue);
+ searchOperatorList[i].setAttribute("disabled", matchAllValue);
+ searchValueList[i].setAttribute("disabled", matchAllValue);
+ if (!matchAllValue) {
+ searchValueList[i].removeAttribute("disabled");
+ }
+ }
+ var moreOrLessButtonsList = searchTerms.getElementsByTagName("button");
+ for (let i = 0; i < moreOrLessButtonsList.length; i++) {
+ moreOrLessButtonsList[i].setAttribute("disabled", matchAllValue);
+ }
+ if (!matchAllValue) {
+ updateRemoveRowButton();
+ }
+}
+
+// enables/disables the less button for the first row of search terms.
+function updateRemoveRowButton() {
+ var firstListItem = gSearchTermList.getItemAtIndex(0);
+ if (firstListItem) {
+ firstListItem.lastElementChild.lastElementChild.setAttribute(
+ "disabled",
+ gTotalSearchTerms == 1
+ );
+ }
+}
+
+// Returns the actual list item row index in the list of search rows
+// that contains the passed in element id.
+function getSearchRowIndexForElement(aElement) {
+ var listItem = aElement;
+
+ while (listItem && listItem.localName != "richlistitem") {
+ listItem = listItem.parentNode;
+ }
+
+ return gSearchTermList.getIndexOfItem(listItem);
+}
+
+function onMore(event) {
+ // if we have an event, extract the list row index and use that as the row number
+ // for our insertion point. If there is no event, append to the end....
+ var rowIndex;
+
+ if (event) {
+ rowIndex = getSearchRowIndexForElement(event.target) + 1;
+ } else {
+ rowIndex = gSearchTermList.getRowCount();
+ }
+
+ createSearchRow(rowIndex, gSearchScope, null, event != null);
+ gTotalSearchTerms++;
+ updateRemoveRowButton();
+
+ // the user just added a term, so scroll to it
+ gSearchTermList.ensureIndexIsVisible(rowIndex);
+}
+
+function onLess(event) {
+ if (event && gTotalSearchTerms > 1) {
+ removeSearchRow(getSearchRowIndexForElement(event.target));
+ --gTotalSearchTerms;
+ }
+
+ updateRemoveRowButton();
+}
+
+// set scope on all visible searchattribute tags
+function setSearchScope(scope) {
+ gSearchScope = scope;
+ for (var i = 0; i < gSearchTerms.length; i++) {
+ // don't set element attributes if XBL hasn't loaded
+ if (!(gSearchTerms[i].obj.searchattribute.searchScope === undefined)) {
+ gSearchTerms[i].obj.searchattribute.searchScope = scope;
+ // act like the user "selected" this, see bug #202848
+ gSearchTerms[i].obj.searchattribute.onSelect(null /* no event */);
+ }
+ gSearchTerms[i].scope = scope;
+ }
+}
+
+function updateSearchAttributes() {
+ for (var i = 0; i < gSearchTerms.length; i++) {
+ gSearchTerms[i].obj.searchattribute.refreshList();
+ }
+}
+
+function booleanChanged(event) {
+ // when boolean changes, we have to update all the attributes on the search terms
+ var newBoolValue = event.target.getAttribute("value") == "and";
+ var matchAllValue = event.target.getAttribute("value") == "matchAll";
+ if (document.getElementById("abPopup")) {
+ var selectedAB = document.getElementById("abPopup").selectedItem.value;
+ setSearchScope(GetScopeForDirectoryURI(selectedAB));
+ }
+ for (var i = 0; i < gSearchTerms.length; i++) {
+ let searchTerm = gSearchTerms[i].obj;
+ // If term is not yet initialized in the UI, change the original object.
+ if (!searchTerm || !gSearchTerms[i].initialized) {
+ searchTerm = gSearchTerms[i].searchTerm;
+ }
+
+ searchTerm.booleanAnd = newBoolValue;
+ searchTerm.matchAll = matchAllValue;
+ }
+ var searchTerms = document.getElementById("searchTermList");
+ if (searchTerms) {
+ if (!matchAllValue && searchTerms.hidden && !gTotalSearchTerms) {
+ // Fake to get empty row.
+ onMore(null);
+ }
+ updateSearchTermsListbox(matchAllValue);
+ }
+}
+
+/**
+ * Create a new search row with all the needed elements.
+ *
+ * @param index index of the position in the menulist where to add the row
+ * @param scope a nsMsgSearchScope constant indicating scope of this search rule
+ * @param searchTerm nsIMsgSearchTerm object to hold the search term
+ * @param aUserAdded boolean indicating if the row addition was initiated by the user
+ * (e.g. via the '+' button)
+ */
+function createSearchRow(index, scope, searchTerm, aUserAdded) {
+ var searchAttr = document.createXULElement("search-attribute");
+ var searchOp = document.createXULElement("search-operator");
+ var searchVal = document.createXULElement("search-value");
+
+ var moreButton = document.createXULElement("button");
+ var lessButton = document.createXULElement("button");
+ moreButton.setAttribute("class", "small-button");
+ moreButton.setAttribute("oncommand", "onMore(event);");
+ moreButton.setAttribute("label", "+");
+ moreButton.setAttribute("tooltiptext", gMoreButtonTooltipText);
+ lessButton.setAttribute("class", "small-button");
+ lessButton.setAttribute("oncommand", "onLess(event);");
+ lessButton.setAttribute("label", "\u2212");
+ lessButton.setAttribute("tooltiptext", gLessButtonTooltipText);
+
+ // now set up ids:
+ searchAttr.id = "searchAttr" + gUniqueSearchTermCounter;
+ searchOp.id = "searchOp" + gUniqueSearchTermCounter;
+ searchVal.id = "searchVal" + gUniqueSearchTermCounter;
+
+ searchAttr.setAttribute("for", searchOp.id + "," + searchVal.id);
+ searchOp.setAttribute("opfor", searchVal.id);
+
+ var rowdata = [searchAttr, searchOp, searchVal, [moreButton, lessButton]];
+ var searchrow = constructRow(rowdata);
+ searchrow.id = "searchRow" + gUniqueSearchTermCounter;
+
+ var searchTermObj = new searchTermContainer();
+ searchTermObj.searchattribute = searchAttr;
+ searchTermObj.searchoperator = searchOp;
+ searchTermObj.searchvalue = searchVal;
+
+ // now insert the new search term into our list of terms
+ gSearchTerms.splice(index, 0, {
+ obj: searchTermObj,
+ scope,
+ searchTerm,
+ initialized: false,
+ });
+
+ var editFilter = window.gFilter || null;
+ var editMailView = window.gMailView || null;
+
+ if (
+ (!editFilter && !editMailView) ||
+ (editFilter && index == gTotalSearchTerms) ||
+ (editMailView && index == gTotalSearchTerms)
+ ) {
+ gLoading = false;
+ }
+
+ // index is index of new row
+ // gTotalSearchTerms has not been updated yet
+ if (gLoading || index == gTotalSearchTerms) {
+ gSearchTermList.appendChild(searchrow);
+ } else {
+ var currentItem = gSearchTermList.getItemAtIndex(index);
+ gSearchTermList.insertBefore(searchrow, currentItem);
+ }
+
+ // If this row was added by user action, focus the value field.
+ if (aUserAdded) {
+ document.commandDispatcher.advanceFocusIntoSubtree(searchVal);
+ searchrow.setAttribute("highlight", "true");
+ }
+
+ // bump our unique search term counter
+ gUniqueSearchTermCounter++;
+}
+
+function initializeTermFromId(id) {
+ initializeTermFromIndex(
+ getSearchRowIndexForElement(document.getElementById(id))
+ );
+}
+
+function initializeTermFromIndex(index) {
+ var searchTermObj = gSearchTerms[index].obj;
+
+ searchTermObj.searchScope = gSearchTerms[index].scope;
+ // the search term will initialize the searchTerm element, including
+ // .booleanAnd
+ if (gSearchTerms[index].searchTerm) {
+ searchTermObj.searchTerm = gSearchTerms[index].searchTerm;
+ // here, we don't have a searchTerm, so it's probably a new element -
+ // we'll initialize the .booleanAnd from the existing setting in
+ // the UI
+ } else {
+ searchTermObj.booleanAnd = gSearchBooleanRadiogroup.value == "and";
+ if (index) {
+ // If we weren't pre-initialized with a searchTerm then steal the
+ // search attribute and operator from the previous row.
+ searchTermObj.searchattribute.value =
+ gSearchTerms[index - 1].obj.searchattribute.value;
+ searchTermObj.searchoperator.value =
+ gSearchTerms[index - 1].obj.searchoperator.value;
+ }
+ }
+
+ gSearchTerms[index].initialized = true;
+}
+
+/**
+ * Creates a <richlistitem> using the array children as the children
+ * of each listcell.
+ *
+ * @param aChildren An array of XUL elements to put into the listitem.
+ * Each array member is put into a separate listcell.
+ * If the member itself is an array of elements,
+ * all of them are put into the same listcell.
+ */
+function constructRow(aChildren) {
+ let cols = gSearchTermList.firstElementChild.children; // treecol elements
+ let listitem = document.createXULElement("richlistitem");
+ listitem.setAttribute("allowevents", "true");
+ for (let i = 0; i < aChildren.length; i++) {
+ let listcell = document.createXULElement("hbox");
+ if (cols[i].hasAttribute("style")) {
+ listcell.setAttribute("style", cols[i].getAttribute("style"));
+ }
+ let child = aChildren[i];
+
+ if (child instanceof Array) {
+ for (let j = 0; j < child.length; j++) {
+ listcell.appendChild(child[j]);
+ }
+ } else {
+ child.setAttribute("flex", "1");
+ listcell.appendChild(child);
+ }
+ listitem.appendChild(listcell);
+ }
+ return listitem;
+}
+
+function removeSearchRow(index) {
+ var searchTermObj = gSearchTerms[index].obj;
+ if (!searchTermObj) {
+ return;
+ }
+
+ // if it is an existing (but offscreen) term,
+ // make sure it is initialized before we remove it.
+ if (!gSearchTerms[index].searchTerm && !gSearchTerms[index].initialized) {
+ initializeTermFromIndex(index);
+ }
+
+ // need to remove row from list, so walk upwards from the
+ // searchattribute to find the first <listitem>
+ var listitem = searchTermObj.searchattribute;
+
+ while (listitem) {
+ if (listitem.localName == "richlistitem") {
+ break;
+ }
+ listitem = listitem.parentNode;
+ }
+
+ if (!listitem) {
+ dump("Error: couldn't find parent listitem!\n");
+ return;
+ }
+
+ if (searchTermObj.searchTerm) {
+ gSearchRemovedTerms.push(searchTermObj.searchTerm);
+ } else {
+ // dump("That wasn't real. ignoring \n");
+ }
+
+ listitem.remove();
+
+ // now remove the item from our list of terms
+ gSearchTerms.splice(index, 1);
+}
+
+/**
+ * Save the search terms from the UI back to the actual search terms.
+ *
+ * @param {nsIMsgSearchTerm[]} searchTerms - Array of terms
+ * @param {object} termOwner - Object which can contain and create the terms
+ * e.g. a nsIMsgSearchSession (will be unnecessary if we just make terms
+ * creatable via XPCOM).
+ * @returns {nsIMsgSearchTerm[]} The filtered searchTerms.
+ */
+function saveSearchTerms(searchTerms, termOwner) {
+ var matchAll = gSearchBooleanRadiogroup.value == "matchAll";
+ var i;
+
+ searchTerms = searchTerms.filter(t => !gSearchRemovedTerms.includes(t));
+
+ for (i = 0; i < gSearchTerms.length; i++) {
+ try {
+ gSearchTerms[i].obj.matchAll = matchAll;
+ var searchTerm = gSearchTerms[i].obj.searchTerm;
+ if (searchTerm) {
+ gSearchTerms[i].obj.save();
+ } else if (!gSearchTerms[i].initialized) {
+ // the term might be an offscreen one we haven't initialized yet
+ searchTerm = gSearchTerms[i].searchTerm;
+ } else {
+ // need to create a new searchTerm, and somehow save it to that
+ searchTerm = termOwner.createTerm();
+ gSearchTerms[i].obj.saveTo(searchTerm);
+ // this might not be the right place for the term,
+ // but we need to make the array longer anyway
+ termOwner.appendTerm(searchTerm);
+ }
+ searchTerms[i] = searchTerm;
+ } catch (ex) {
+ dump("** Error saving element " + i + ": " + ex + "\n");
+ }
+ }
+ return searchTerms;
+}
+
+function onReset(event) {
+ while (gTotalSearchTerms > 0) {
+ removeSearchRow(--gTotalSearchTerms);
+ }
+ onMore(null);
+}
+
+function hideMatchAllItem() {
+ var allItems = document.getElementById("matchAllItem");
+ if (allItems) {
+ allItems.hidden = true;
+ }
+}
diff --git a/comm/mailnews/search/content/searchWidgets.js b/comm/mailnews/search/content/searchWidgets.js
new file mode 100644
index 0000000000..add3ed29b8
--- /dev/null
+++ b/comm/mailnews/search/content/searchWidgets.js
@@ -0,0 +1,1779 @@
+/**
+ * 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/. */
+
+/* global MozElements MozXULElement */
+
+/* import-globals-from ../../base/content/dateFormat.js */
+// TODO: This is completely bogus. Only one use of this file also has FilterEditor.js.
+/* import-globals-from FilterEditor.js */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+ const updateParentNode = parentNode => {
+ if (parentNode.hasAttribute("initialActionIndex")) {
+ let actionIndex = parentNode.getAttribute("initialActionIndex");
+ let filterAction = gFilter.getActionAt(actionIndex);
+ parentNode.initWithAction(filterAction);
+ }
+ parentNode.updateRemoveButton();
+ };
+
+ class MozRuleactiontargetTag extends MozXULElement {
+ connectedCallback() {
+ const menulist = document.createXULElement("menulist");
+ const menuPopup = document.createXULElement("menupopup");
+
+ menulist.classList.add("ruleactionitem");
+ menulist.setAttribute("flex", "1");
+ menulist.appendChild(menuPopup);
+
+ for (let taginfo of MailServices.tags.getAllTags()) {
+ const newMenuItem = document.createXULElement("menuitem");
+ newMenuItem.setAttribute("label", taginfo.tag);
+ newMenuItem.setAttribute("value", taginfo.key);
+ if (taginfo.color) {
+ newMenuItem.setAttribute("style", `color: ${taginfo.color};`);
+ }
+ menuPopup.appendChild(newMenuItem);
+ }
+
+ this.appendChild(menulist);
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetPriority extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem" flex="1">
+ <menupopup>
+ <menuitem value="6" label="&highestPriorityCmd.label;"></menuitem>
+ <menuitem value="5" label="&highPriorityCmd.label;"></menuitem>
+ <menuitem value="4" label="&normalPriorityCmd.label;"></menuitem>
+ <menuitem value="3" label="&lowPriorityCmd.label;"></menuitem>
+ <menuitem value="2" label="&lowestPriorityCmd.label;"></menuitem>
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/FilterEditor.dtd"]
+ )
+ );
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetJunkscore extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem" flex="1">
+ <menupopup>
+ <menuitem value="100" label="&junk.label;"/>
+ <menuitem value="0" label="&notJunk.label;"/>
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/FilterEditor.dtd"]
+ )
+ );
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetReplyto extends MozXULElement {
+ connectedCallback() {
+ const menulist = document.createXULElement("menulist");
+ const menuPopup = document.createXULElement("menupopup");
+
+ menulist.classList.add("ruleactionitem");
+ menulist.setAttribute("flex", "1");
+ menulist.appendChild(menuPopup);
+
+ this.appendChild(menulist);
+
+ let ruleaction = this.closest(".ruleaction");
+ let raMenulist = ruleaction.querySelector(
+ '[is="ruleactiontype-menulist"]'
+ );
+ for (let { label, value } of raMenulist.findTemplates()) {
+ menulist.appendItem(label, value);
+ }
+ updateParentNode(ruleaction);
+ }
+ }
+
+ class MozRuleactiontargetForwardto extends MozXULElement {
+ connectedCallback() {
+ const input = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "input"
+ );
+ input.classList.add("ruleactionitem", "input-inline");
+
+ this.classList.add("input-container");
+ this.appendChild(input);
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetFolder extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem
+ folderMenuItem"
+ flex="1"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showRecent="true"
+ recentLabel="&recentFolders.label;"
+ showFileHereLabel="true">
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/messenger.dtd"]
+ )
+ );
+
+ this.menulist = this.querySelector("menulist");
+
+ this.menulist.addEventListener("command", event => {
+ this.setPicker(event);
+ });
+
+ updateParentNode(this.closest(".ruleaction"));
+
+ let folder = this.menulist.value
+ ? MailUtils.getOrCreateFolder(this.menulist.value)
+ : gFilterList.folder;
+
+ // An account folder is not a move/copy target; show "Choose Folder".
+ folder = folder.isServer ? null : folder;
+
+ this.menulist.menupopup.selectFolder(folder);
+ }
+
+ setPicker(event) {
+ this.menulist.menupopup.selectFolder(event.target._folder);
+ }
+ }
+
+ class MozRuleactiontargetWrapper extends MozXULElement {
+ static get observedAttributes() {
+ return ["type"];
+ }
+
+ get ruleactiontargetElement() {
+ return this.node;
+ }
+
+ connectedCallback() {
+ this._updateAttributes();
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _getChildNode(type) {
+ const elementMapping = {
+ movemessage: "ruleactiontarget-folder",
+ copymessage: "ruleactiontarget-folder",
+ setpriorityto: "ruleactiontarget-priority",
+ setjunkscore: "ruleactiontarget-junkscore",
+ forwardmessage: "ruleactiontarget-forwardto",
+ replytomessage: "ruleactiontarget-replyto",
+ addtagtomessage: "ruleactiontarget-tag",
+ };
+ const elementName = elementMapping[type];
+
+ return elementName ? document.createXULElement(elementName) : null;
+ }
+
+ _updateAttributes() {
+ if (!this.hasAttribute("type")) {
+ return;
+ }
+
+ const type = this.getAttribute("type");
+
+ while (this.lastChild) {
+ this.lastChild.remove();
+ }
+
+ if (type == null) {
+ return;
+ }
+
+ this.node = this._getChildNode(type);
+
+ if (this.node) {
+ this.node.setAttribute("flex", "1");
+ this.appendChild(this.node);
+ } else {
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+ }
+
+ customElements.define("ruleactiontarget-tag", MozRuleactiontargetTag);
+ customElements.define(
+ "ruleactiontarget-priority",
+ MozRuleactiontargetPriority
+ );
+ customElements.define(
+ "ruleactiontarget-junkscore",
+ MozRuleactiontargetJunkscore
+ );
+ customElements.define("ruleactiontarget-replyto", MozRuleactiontargetReplyto);
+ customElements.define(
+ "ruleactiontarget-forwardto",
+ MozRuleactiontargetForwardto
+ );
+ customElements.define("ruleactiontarget-folder", MozRuleactiontargetFolder);
+ customElements.define("ruleactiontarget-wrapper", MozRuleactiontargetWrapper);
+
+ /**
+ * This is an abstract class for search menulist general functionality.
+ *
+ * @abstract
+ * @augments MozXULElement
+ */
+ class MozSearchMenulistAbstract extends MozXULElement {
+ static get observedAttributes() {
+ return ["flex", "disabled"];
+ }
+
+ constructor() {
+ super();
+ this.internalScope = null;
+ this.internalValue = -1;
+ this.validityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+ ].getService(Ci.nsIMsgSearchValidityManager);
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ this.menulist = document.createXULElement("menulist");
+ this.menulist.classList.add("search-menulist");
+ this.menulist.addEventListener("command", this.onSelect.bind(this));
+ this.menupopup = document.createXULElement("menupopup");
+ this.menupopup.classList.add("search-menulist-popup");
+ this.menulist.appendChild(this.menupopup);
+ this.appendChild(this.menulist);
+ this._updateAttributes();
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _updateAttributes() {
+ if (!this.menulist) {
+ return;
+ }
+ if (this.hasAttribute("flex")) {
+ this.menulist.setAttribute("flex", this.getAttribute("flex"));
+ } else {
+ this.menulist.removeAttribute("flex");
+ }
+ if (this.hasAttribute("disabled")) {
+ this.menulist.setAttribute("disabled", this.getAttribute("disabled"));
+ } else {
+ this.menulist.removeAttribute("disabled");
+ }
+ }
+
+ set searchScope(val) {
+ // if scope isn't changing this is a noop
+ if (this.internalScope == val) {
+ return;
+ }
+ this.internalScope = val;
+ this.refreshList();
+ if (this.targets) {
+ this.targets.forEach(target => {
+ customElements.upgrade(target);
+ target.searchScope = val;
+ });
+ }
+ }
+
+ get searchScope() {
+ return this.internalScope;
+ }
+
+ get validityTable() {
+ return this.validityManager.getTable(this.searchScope);
+ }
+
+ get targets() {
+ const forAttrs = this.getAttribute("for");
+ if (!forAttrs) {
+ return null;
+ }
+ const targetIds = forAttrs.split(",");
+ if (targetIds.length == 0) {
+ return null;
+ }
+
+ return targetIds
+ .map(id => document.getElementById(id))
+ .filter(e => e != null);
+ }
+
+ get optargets() {
+ const forAttrs = this.getAttribute("opfor");
+ if (!forAttrs) {
+ return null;
+ }
+ const optargetIds = forAttrs.split(",");
+ if (optargetIds.length == 0) {
+ return null;
+ }
+
+ return optargetIds
+ .map(id => document.getElementById(id))
+ .filter(e => e != null);
+ }
+
+ set value(val) {
+ if (this.internalValue == val) {
+ return;
+ }
+ this.internalValue = val;
+ this.menulist.selectedItem = this.validMenuitem;
+ // now notify targets of new parent's value
+ if (this.targets) {
+ this.targets.forEach(target => {
+ customElements.upgrade(target);
+ target.parentValue = val;
+ });
+ }
+ // now notify optargets of new op parent's value
+ if (this.optargets) {
+ this.optargets.forEach(optarget => {
+ customElements.upgrade(optarget);
+ optarget.opParentValue = val;
+ });
+ }
+ }
+
+ get value() {
+ return this.internalValue;
+ }
+
+ /**
+ * Gets the label of the menulist's selected item.
+ */
+ get label() {
+ return this.menulist.selectedItem.getAttribute("label");
+ }
+
+ get validMenuitem() {
+ if (this.value == -1) {
+ // -1 means not initialized
+ return null;
+ }
+ let isCustom = isNaN(this.value);
+ let typedValue = isCustom ? this.value : parseInt(this.value);
+ // custom attribute to style the unavailable menulist item
+ this.menulist.setAttribute(
+ "unavailable",
+ !this.valueIds.includes(typedValue) ? "true" : null
+ );
+ // add a hidden menulist item if value is missing
+ let menuitem = this.menulist.querySelector(`[value="${this.value}"]`);
+ if (!menuitem) {
+ // need to add a hidden menuitem
+ menuitem = this.menulist.appendItem(this.valueLabel, this.value);
+ menuitem.hidden = true;
+ }
+ return menuitem;
+ }
+
+ refreshList(dontRestore) {
+ const menuItemIds = this.valueIds;
+ const menuItemStrings = this.valueStrings;
+ const popup = this.menupopup;
+ // save our old "value" so we can restore it later
+ let oldData;
+ if (!dontRestore) {
+ oldData = this.menulist.value;
+ }
+ // remove the old popup children
+ while (popup.hasChildNodes()) {
+ popup.lastChild.remove();
+ }
+ let newSelection;
+ let customizePos = -1;
+ for (let i = 0; i < menuItemIds.length; i++) {
+ // create the menuitem
+ if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString()) {
+ customizePos = i;
+ } else {
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[i]);
+ menuitem.setAttribute("value", menuItemIds[i]);
+ popup.appendChild(menuitem);
+ // try to restore the selection
+ if (!newSelection || oldData == menuItemIds[i].toString()) {
+ newSelection = menuitem;
+ }
+ }
+ }
+ if (customizePos != -1) {
+ const separator = document.createXULElement("menuseparator");
+ popup.appendChild(separator);
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[customizePos]);
+ menuitem.setAttribute("value", menuItemIds[customizePos]);
+ popup.appendChild(menuitem);
+ }
+
+ // If we are either uninitialized, or if we are called because
+ // of a change in our parent, update the value to the
+ // default stored in newSelection.
+ if ((this.value == -1 || dontRestore) && newSelection) {
+ this.value = newSelection.getAttribute("value");
+ }
+ this.menulist.selectedItem = this.validMenuitem;
+ }
+
+ onSelect(event) {
+ if (this.menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) {
+ // Customize menuitem selected.
+ let args = {};
+ window.openDialog(
+ "chrome://messenger/content/CustomHeaders.xhtml",
+ "",
+ "modal,centerscreen,resizable,titlebar,chrome",
+ args
+ );
+ // User may have removed the custom header currently selected
+ // in the menulist so temporarily set the selection to a safe value.
+ this.value = Ci.nsMsgSearchAttrib.OtherHeader;
+ // rebuild the menulist
+ UpdateAfterCustomHeaderChange();
+ // Find the created or chosen custom header and select it.
+ let menuitem = null;
+ if (args.selectedVal) {
+ menuitem = this.menulist.querySelector(
+ `[label="${args.selectedVal}"]`
+ );
+ }
+ if (menuitem) {
+ this.value = menuitem.value;
+ } else {
+ // Nothing was picked in the custom headers editor so just pick something
+ // instead of the current "Customize" menuitem.
+ this.value = this.menulist.getItemAtIndex(0).value;
+ }
+ } else {
+ this.value = this.menulist.value;
+ }
+ }
+ }
+
+ /**
+ * The MozSearchAttribute widget is typically used in the search and filter dialogs to show a list
+ * of possible message headers.
+ *
+ * @augments MozSearchMenulistAbstract
+ */
+ class MozSearchAttribute extends MozSearchMenulistAbstract {
+ constructor() {
+ super();
+
+ this.stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search-attributes.properties"
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ initializeTermFromId(this.id);
+ }
+
+ get valueLabel() {
+ if (isNaN(this.value)) {
+ // is this a custom term?
+ let customTerm = MailServices.filters.getCustomTerm(this.value);
+ if (customTerm) {
+ return customTerm.name;
+ }
+ // The custom term may be missing after the extension that added it
+ // was disabled or removed. We need to notify the user.
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.init(
+ "Missing custom search term " + this.value,
+ null,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.errorFlag,
+ "component javascript"
+ );
+ Services.console.logMessage(scriptError);
+ return this.stringBundle.GetStringFromName("MissingCustomTerm");
+ }
+ return this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(parseInt(this.value))
+ );
+ }
+
+ get valueIds() {
+ let result = this.validityTable.getAvailableAttributes();
+ // add any available custom search terms
+ for (let customTerm of MailServices.filters.getCustomTerms()) {
+ // For custom terms, the array element is a string with the custom id
+ // instead of the integer attribute
+ if (customTerm.getAvailable(this.searchScope, null)) {
+ result.push(customTerm.id);
+ }
+ }
+ return result;
+ }
+
+ get valueStrings() {
+ let strings = [];
+ let ids = this.valueIds;
+ let hdrsArray = null;
+ try {
+ let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
+ hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting
+ hdrsArray = hdrs.match(/[^:]+/g);
+ } catch (ex) {}
+ let j = 0;
+ for (let i = 0; i < ids.length; i++) {
+ if (isNaN(ids[i])) {
+ // Is this a custom search term?
+ let customTerm = MailServices.filters.getCustomTerm(ids[i]);
+ if (customTerm) {
+ strings[i] = customTerm.name;
+ } else {
+ strings[i] = "";
+ }
+ } else if (ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray) {
+ strings[i] = hdrsArray[j++];
+ } else {
+ strings[i] = this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(ids[i])
+ );
+ }
+ }
+ return strings;
+ }
+ }
+ customElements.define("search-attribute", MozSearchAttribute);
+
+ /**
+ * MozSearchOperator contains a list of operators that can be applied on search-attribute and
+ * search-value value.
+ *
+ * @augments MozSearchMenulistAbstract
+ */
+ class MozSearchOperator extends MozSearchMenulistAbstract {
+ constructor() {
+ super();
+
+ this.stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search-operators.properties"
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this.searchAttribute = Ci.nsMsgSearchAttrib.Default;
+ }
+
+ get valueLabel() {
+ return this.stringBundle.GetStringFromName(this.value);
+ }
+
+ get valueIds() {
+ let isCustom = isNaN(this.searchAttribute);
+ if (isCustom) {
+ let customTerm = MailServices.filters.getCustomTerm(
+ this.searchAttribute
+ );
+ if (customTerm) {
+ return customTerm.getAvailableOperators(this.searchScope);
+ }
+ return [Ci.nsMsgSearchOp.Contains];
+ }
+ return this.validityTable.getAvailableOperators(this.searchAttribute);
+ }
+
+ get valueStrings() {
+ let strings = [];
+ let ids = this.valueIds;
+ for (let i = 0; i < ids.length; i++) {
+ strings[i] = this.stringBundle.GetStringFromID(ids[i]);
+ }
+ return strings;
+ }
+
+ set parentValue(val) {
+ if (
+ this.searchAttribute == val &&
+ val != Ci.nsMsgSearchAttrib.OtherHeader
+ ) {
+ return;
+ }
+ this.searchAttribute = val;
+ this.refreshList(true); // don't restore the selection, since searchvalue nulls it
+ if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
+ // We want "Age in Days" to default to "is less than".
+ this.value = Ci.nsMsgSearchOp.IsLessThan;
+ }
+ }
+
+ get parentValue() {
+ return this.searchAttribute;
+ }
+ }
+ customElements.define("search-operator", MozSearchOperator);
+
+ /**
+ * MozSearchValue is a widget that allows selecting the value to search or filter on. It can be a
+ * text entry, priority, status, junk status, tags, hasAttachment status, and addressbook etc.
+ *
+ * @augments MozXULElement
+ */
+ class MozSearchValue extends MozXULElement {
+ static get observedAttributes() {
+ return ["disabled"];
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("keypress", event => {
+ if (event.keyCode != KeyEvent.DOM_VK_RETURN) {
+ return;
+ }
+ onEnterInSearchTerm(event);
+ });
+
+ this.internalOperator = null;
+ this.internalAttribute = null;
+ this.internalValue = null;
+
+ this.inputType = "none";
+ }
+
+ connectedCallback() {
+ this.classList.add("input-container");
+ }
+
+ static get stringBundle() {
+ if (!this._stringBundle) {
+ this._stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ }
+ return this._stringBundle;
+ }
+
+ /**
+ * Create a menulist to be used as the input.
+ *
+ * @param {object[]} itemDataList - An ordered list of items to add to the
+ * menulist. Each entry must have a 'value' property to be used as the
+ * item value. If the entry has a 'label' property, it will be used
+ * directly as the item label, otherwise it must identify a bundle string
+ * using the 'stringId' property.
+ *
+ * @returns {MozMenuList} - The newly created menulist.
+ */
+ static _createMenulist(itemDataList) {
+ let menulist = document.createXULElement("menulist");
+ menulist.classList.add("search-value-menulist");
+ let menupopup = document.createXULElement("menupopup");
+ menupopup.classList.add("search-value-popup");
+
+ let bundle = this.stringBundle;
+
+ for (let itemData of itemDataList) {
+ let item = document.createXULElement("menuitem");
+ item.classList.add("search-value-menuitem");
+ item.label =
+ itemData.label || bundle.GetStringFromName(itemData.stringId);
+ item.value = itemData.value;
+ menupopup.appendChild(item);
+ }
+ menulist.appendChild(menupopup);
+ return menulist;
+ }
+
+ /**
+ * Set the child input. The input will only be changed if the type changes.
+ *
+ * @param {string} type - The type of input to use.
+ * @param {string|number|undefined} value - A value to set on the input, or
+ * leave undefined to not change the value. See setInputValue.
+ */
+ setInput(type, value) {
+ if (type != this.inputType) {
+ this.inputType = type;
+ this.input?.remove();
+ let input;
+ switch (type) {
+ case "text":
+ input = document.createElement("input");
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "date":
+ input = document.createElement("input");
+ input.classList.add("input-inline", "search-value-input");
+ if (!value) {
+ // Newly created date input shows today's date.
+ // value is expected in microseconds since epoch.
+ value = Date.now() * 1000;
+ }
+ break;
+ case "size":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = 0;
+ input.max = 1000000000;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "age":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = -40000; // ~100 years.
+ input.max = 40000;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "percent":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = 0;
+ input.max = 100;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "priority":
+ input = this.constructor._createMenulist([
+ { stringId: "priorityHighest", value: Ci.nsMsgPriority.highest },
+ { stringId: "priorityHigh", value: Ci.nsMsgPriority.high },
+ { stringId: "priorityNormal", value: Ci.nsMsgPriority.normal },
+ { stringId: "priorityLow", value: Ci.nsMsgPriority.low },
+ { stringId: "priorityLowest", value: Ci.nsMsgPriority.lowest },
+ ]);
+ break;
+ case "status":
+ input = this.constructor._createMenulist([
+ { stringId: "replied", value: Ci.nsMsgMessageFlags.Replied },
+ { stringId: "read", value: Ci.nsMsgMessageFlags.Read },
+ { stringId: "new", value: Ci.nsMsgMessageFlags.New },
+ { stringId: "forwarded", value: Ci.nsMsgMessageFlags.Forwarded },
+ { stringId: "flagged", value: Ci.nsMsgMessageFlags.Marked },
+ ]);
+ break;
+ case "addressbook":
+ input = document.createXULElement("menulist", {
+ is: "menulist-addrbooks",
+ });
+ input.setAttribute("localonly", "true");
+ input.classList.add("search-value-menulist");
+ if (!value) {
+ // Select the personal addressbook by default.
+ value = "jsaddrbook://abook.sqlite";
+ }
+ break;
+ case "tags":
+ input = this.constructor._createMenulist(
+ MailServices.tags.getAllTags().map(taginfo => {
+ return { label: taginfo.tag, value: taginfo.key };
+ })
+ );
+ break;
+ case "junk-status":
+ // "Junk Status is/isn't/is empty/isn't empty 'Junk'".
+ input = this.constructor._createMenulist([
+ { stringId: "junk", value: Ci.nsIJunkMailPlugin.JUNK },
+ ]);
+ break;
+ case "attachment-status":
+ // "Attachment Status is/isn't 'Has Attachments'".
+ input = this.constructor._createMenulist([
+ { stringId: "hasAttachments", value: "0" },
+ ]);
+ break;
+ case "junk-origin":
+ input = this.constructor._createMenulist([
+ { stringId: "junkScoreOriginPlugin", value: "plugin" },
+ { stringId: "junkScoreOriginUser", value: "user" },
+ { stringId: "junkScoreOriginFilter", value: "filter" },
+ { stringId: "junkScoreOriginWhitelist", value: "whitelist" },
+ { stringId: "junkScoreOriginImapFlag", value: "imapflag" },
+ ]);
+ break;
+ case "none":
+ input = null;
+ break;
+ case "custom":
+ // Used by extensions.
+ // FIXME: We need a better way for extensions to set a custom input.
+ input = document.createXULElement("hbox");
+ input.setAttribute("flex", "1");
+ input.classList.add("search-value-custom");
+ break;
+ default:
+ throw new Error(`Unrecognised input type "${type}"`);
+ }
+
+ this.input = input;
+ if (input) {
+ this.appendChild(input);
+ }
+
+ this._updateAttributes();
+ }
+
+ this.setInputValue(value);
+ }
+
+ /**
+ * Set the child input to the given value.
+ *
+ * @param {string|number} value - The value to set on the input. For "date"
+ * inputs, this should be a number of microseconds since the epoch.
+ */
+ setInputValue(value) {
+ if (value === undefined) {
+ return;
+ }
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ this.input.value = value;
+ break;
+ case "date":
+ this.input.value = convertPRTimeToString(value);
+ break;
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ let item = this.input.querySelector(`menuitem[value="${value}"]`);
+ if (item) {
+ this.input.selectedItem = item;
+ }
+ break;
+ case "none":
+ // Silently ignore the value.
+ break;
+ case "custom":
+ this.input.setAttribute("value", value);
+ break;
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ /**
+ * Get the child input's value.
+ *
+ * @returns {string|number} - The value set in the input. For "date"
+ * inputs, this is the number of microseconds since the epoch.
+ */
+ getInputValue() {
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ return this.input.value;
+ case "date":
+ return convertStringToPRTime(this.input.value);
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ return this.input.selectedItem.value;
+ case "none":
+ return "";
+ case "custom":
+ return this.input.getAttribute("value");
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ /**
+ * Get the element's displayed value.
+ *
+ * @returns {string} - The value seen by the user.
+ */
+ getReadableValue() {
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ case "date":
+ return this.input.value;
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ return this.input.selectedItem.label;
+ case "none":
+ return "";
+ case "custom":
+ return this.input.getAttribute("value");
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _updateAttributes() {
+ if (!this.input) {
+ return;
+ }
+ if (this.hasAttribute("disabled")) {
+ this.input.setAttribute("disabled", this.getAttribute("disabled"));
+ } else {
+ this.input.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Update the displayed input according to the selected sibling attributes
+ * and operators.
+ *
+ * @param {nsIMsgSearchValue} [value] - A value to display in the input. Or
+ * leave unset to not change the value.
+ */
+ updateDisplay(value) {
+ let operator = Number(this.internalOperator);
+ switch (Number(this.internalAttribute)) {
+ // Use the index to hide/show the appropriate child.
+ case Ci.nsMsgSearchAttrib.Priority:
+ this.setInput("priority", value?.priority);
+ break;
+ case Ci.nsMsgSearchAttrib.MsgStatus:
+ this.setInput("status", value?.status);
+ break;
+ case Ci.nsMsgSearchAttrib.Date:
+ this.setInput("date", value?.date);
+ break;
+ case Ci.nsMsgSearchAttrib.Sender:
+ case Ci.nsMsgSearchAttrib.To:
+ case Ci.nsMsgSearchAttrib.ToOrCC:
+ case Ci.nsMsgSearchAttrib.AllAddresses:
+ case Ci.nsMsgSearchAttrib.CC:
+ if (
+ operator == Ci.nsMsgSearchOp.IsntInAB ||
+ operator == Ci.nsMsgSearchOp.IsInAB
+ ) {
+ this.setInput("addressbook", value?.str);
+ } else {
+ this.setInput("text", value?.str);
+ }
+ break;
+ case Ci.nsMsgSearchAttrib.Keywords:
+ this.setInput(
+ operator == Ci.nsMsgSearchOp.IsEmpty ||
+ operator == Ci.nsMsgSearchOp.IsntEmpty
+ ? "none"
+ : "tags",
+ value?.str
+ );
+ break;
+ case Ci.nsMsgSearchAttrib.JunkStatus:
+ this.setInput(
+ operator == Ci.nsMsgSearchOp.IsEmpty ||
+ operator == Ci.nsMsgSearchOp.IsntEmpty
+ ? "none"
+ : "junk-status",
+ value?.junkStatus
+ );
+ break;
+ case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
+ this.setInput("attachment-status", value?.hasAttachmentStatus);
+ break;
+ case Ci.nsMsgSearchAttrib.JunkScoreOrigin:
+ this.setInput("junk-origin", value?.str);
+ break;
+ case Ci.nsMsgSearchAttrib.AgeInDays:
+ this.setInput("age", value?.age);
+ break;
+ case Ci.nsMsgSearchAttrib.Size:
+ this.setInput("size", value?.size);
+ break;
+ case Ci.nsMsgSearchAttrib.JunkPercent:
+ this.setInput("percent", value?.junkPercent);
+ break;
+ default:
+ if (isNaN(this.internalAttribute)) {
+ // Custom attribute, the internalAttribute is a string.
+ // FIXME: We need a better way for extensions to set a custom input.
+ this.setInput("custom", value?.str);
+ this.input.setAttribute("searchAttribute", this.internalAttribute);
+ } else {
+ this.setInput("text", value?.str);
+ }
+ break;
+ }
+ }
+
+ /**
+ * The sibling operator type.
+ *
+ * @type {nsMsgSearchOpValue}
+ */
+ set opParentValue(val) {
+ if (this.internalOperator == val) {
+ return;
+ }
+ this.internalOperator = val;
+ this.updateDisplay();
+ }
+
+ get opParentValue() {
+ return this.internalOperator;
+ }
+
+ /**
+ * A duplicate of the searchAttribute property.
+ *
+ * @type {nsMsgSearchAttribValue}
+ */
+ set parentValue(val) {
+ this.searchAttribute = val;
+ }
+
+ get parentValue() {
+ return this.searchAttribute;
+ }
+
+ /**
+ * The sibling attribute type.
+ *
+ * @type {nsMsgSearchAttribValue}
+ */
+ set searchAttribute(val) {
+ if (this.internalAttribute == val) {
+ return;
+ }
+ this.internalAttribute = val;
+ this.updateDisplay();
+ }
+
+ get searchAttribute() {
+ return this.internalAttribute;
+ }
+
+ /**
+ * The stored value for this element.
+ *
+ * Note that the input value is *derived* from this object when it is set.
+ * But changes to the input value using the UI will not change the stored
+ * value until the save method is called.
+ *
+ * @type {nsIMsgSearchValue}
+ */
+ set value(val) {
+ // val is a nsIMsgSearchValue object
+ this.internalValue = val;
+ this.updateDisplay(val);
+ }
+
+ get value() {
+ return this.internalValue;
+ }
+
+ /**
+ * Updates the stored value for this element to reflect its current input
+ * value.
+ */
+ save() {
+ let searchValue = this.value;
+ let searchAttribute = this.searchAttribute;
+
+ searchValue.attrib = isNaN(searchAttribute)
+ ? Ci.nsMsgSearchAttrib.Custom
+ : searchAttribute;
+ switch (Number(searchAttribute)) {
+ case Ci.nsMsgSearchAttrib.Priority:
+ searchValue.priority = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.MsgStatus:
+ searchValue.status = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.AgeInDays:
+ searchValue.age = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.Date:
+ searchValue.date = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.JunkStatus:
+ searchValue.junkStatus = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
+ searchValue.status = Ci.nsMsgMessageFlags.Attachment;
+ break;
+ case Ci.nsMsgSearchAttrib.JunkPercent:
+ searchValue.junkPercent = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.Size:
+ searchValue.size = this.getInputValue();
+ break;
+ default:
+ searchValue.str = this.getInputValue();
+ break;
+ }
+ }
+
+ /**
+ * Stores the displayed value for this element in the given object.
+ *
+ * Note that after this call, the stored value will remain pointing to the
+ * given searchValue object.
+ *
+ * @param {nsIMsgSearchValue} searchValue - The object to store the
+ * displayed value in.
+ */
+ saveTo(searchValue) {
+ this.internalValue = searchValue;
+ this.save();
+ }
+ }
+ customElements.define("search-value", MozSearchValue);
+
+ // The menulist CE is defined lazily. Create one now to get menulist defined,
+ // allowing us to inherit from it.
+ if (!customElements.get("menulist")) {
+ delete document.createXULElement("menulist");
+ }
+ {
+ /**
+ * The MozRuleactiontypeMenulist is a widget that allows selecting the actions from the given menulist for
+ * the selected folder. It gets displayed in the message filter dialog box.
+ *
+ * @augments {MozMenuList}
+ */
+ class MozRuleactiontypeMenulist extends customElements.get("menulist") {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ this.setAttribute("is", "ruleactiontype-menulist");
+ this.addEventListener("command", event => {
+ this.parentNode.setAttribute("value", this.value);
+ checkActionsReorder();
+ });
+
+ this.addEventListener("popupshowing", event => {
+ let unavailableActions = this.usedActionsList();
+ for (let index = 0; index < this.menuitems.length; index++) {
+ let menu = this.menuitems[index];
+ menu.setAttribute("disabled", menu.value in unavailableActions);
+ }
+ });
+
+ this.menuitems = this.getElementsByTagNameNS(
+ this.namespaceURI,
+ "menuitem"
+ );
+
+ // Force initialization of the menulist custom element first.
+ customElements.upgrade(this);
+ this.addCustomActions();
+ this.hideInvalidActions();
+ // Differentiate between creating a new, next available action,
+ // and creating a row which will be initialized with an action.
+ if (!this.parentNode.hasAttribute("initialActionIndex")) {
+ let unavailableActions = this.usedActionsList();
+ // Select the first one that's not in the list.
+ for (let index = 0; index < this.menuitems.length; index++) {
+ let menu = this.menuitems[index];
+ if (!(menu.value in unavailableActions) && !menu.hidden) {
+ this.value = menu.value;
+ this.parentNode.setAttribute("value", menu.value);
+ break;
+ }
+ }
+ } else {
+ this.parentNode.mActionTypeInitialized = true;
+ this.parentNode.clearInitialActionIndex();
+ }
+ }
+
+ hideInvalidActions() {
+ let menupopup = this.menupopup;
+ let scope = getScopeFromFilterList(gFilterList);
+
+ // Walk through the list of filter actions and hide any actions which aren't valid
+ // for our given scope (news, imap, pop, etc) and context.
+ let elements;
+
+ // Disable / enable all elements in the "filteractionlist"
+ // based on the scope and the "enablefornews" attribute.
+ elements = menupopup.getElementsByAttribute("enablefornews", "true");
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].hidden = scope != Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ elements = menupopup.getElementsByAttribute("enablefornews", "false");
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].hidden = scope == Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ elements = menupopup.getElementsByAttribute("enableforpop3", "true");
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].hidden = !(
+ gFilterList.folder.server.type == "pop3" ||
+ gFilterList.folder.server.type == "none"
+ );
+ }
+
+ elements = menupopup.getElementsByAttribute("isCustom", "true");
+ // Note there might be an additional element here as a placeholder
+ // for a missing action, so we iterate over the known actions
+ // instead of the elements.
+ for (let i = 0; i < gCustomActions.length; i++) {
+ elements[i].hidden = !gCustomActions[i].isValidForType(
+ gFilterType,
+ scope
+ );
+ }
+
+ // Disable "Reply with Template" if there are no templates.
+ if (this.findTemplates().length == 0) {
+ elements = menupopup.getElementsByAttribute(
+ "value",
+ "replytomessage"
+ );
+ if (elements.length == 1) {
+ elements[0].hidden = true;
+ }
+ }
+ }
+
+ addCustomActions() {
+ let menupopup = this.menupopup;
+ for (let i = 0; i < gCustomActions.length; i++) {
+ let customAction = gCustomActions[i];
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", customAction.name);
+ menuitem.setAttribute("value", customAction.id);
+ menuitem.setAttribute("isCustom", "true");
+ menupopup.appendChild(menuitem);
+ }
+ }
+
+ /**
+ * Returns a hash containing all of the filter actions which are currently
+ * being used by other filteractionrows.
+ *
+ * @returns {object} - a hash containing all of the filter actions which are
+ * currently being used by other filteractionrows.
+ */
+ usedActionsList() {
+ let usedActions = {};
+ let currentFilterActionRow = this.parentNode;
+ let listBox = currentFilterActionRow.parentNode; // need to account for the list item.
+ // Now iterate over each list item in the list box.
+ for (let index = 0; index < listBox.getRowCount(); index++) {
+ let filterActionRow = listBox.getItemAtIndex(index);
+ if (filterActionRow != currentFilterActionRow) {
+ let actionValue = filterActionRow.getAttribute("value");
+
+ // Let custom actions decide if dups are allowed.
+ let isCustom = false;
+ for (let i = 0; i < gCustomActions.length; i++) {
+ if (gCustomActions[i].id == actionValue) {
+ isCustom = true;
+ if (!gCustomActions[i].allowDuplicates) {
+ usedActions[actionValue] = true;
+ }
+ break;
+ }
+ }
+
+ if (!isCustom) {
+ // The following actions can appear more than once in a single filter
+ // so do not set them as already used.
+ if (
+ actionValue != "addtagtomessage" &&
+ actionValue != "forwardmessage" &&
+ actionValue != "copymessage"
+ ) {
+ usedActions[actionValue] = true;
+ }
+ // If either Delete message or Move message exists, disable the other one.
+ // It does not make sense to apply both to the same message.
+ if (actionValue == "deletemessage") {
+ usedActions.movemessage = true;
+ } else if (actionValue == "movemessage") {
+ usedActions.deletemessage = true;
+ } else if (actionValue == "markasread") {
+ // The same with Mark as read/Mark as Unread.
+ usedActions.markasunread = true;
+ } else if (actionValue == "markasunread") {
+ usedActions.markasread = true;
+ }
+ }
+ }
+ }
+ return usedActions;
+ }
+
+ /**
+ * Check if there exist any templates in this account.
+ *
+ * @returns {object[]} - An array of template headers: each has a label and
+ * a value.
+ */
+ findTemplates() {
+ let identities = MailServices.accounts.getIdentitiesForServer(
+ gFilterList.folder.server
+ );
+ // Typically if this is Local Folders.
+ if (identities.length == 0) {
+ if (MailServices.accounts.defaultAccount) {
+ identities.push(
+ MailServices.accounts.defaultAccount.defaultIdentity
+ );
+ }
+ }
+
+ let templates = [];
+ let foldersScanned = [];
+
+ for (let identity of identities) {
+ let enumerator = null;
+ let msgFolder = MailUtils.getExistingFolder(
+ identity.stationeryFolder
+ );
+ // If we already processed this folder, do not set enumerator
+ // so that we skip this identity.
+ if (msgFolder && !foldersScanned.includes(msgFolder)) {
+ foldersScanned.push(msgFolder);
+ enumerator = msgFolder.msgDatabase.enumerateMessages();
+ }
+
+ if (!enumerator) {
+ continue;
+ }
+
+ for (let header of enumerator) {
+ let uri =
+ msgFolder.URI +
+ "?messageId=" +
+ header.messageId +
+ "&subject=" +
+ header.mime2DecodedSubject;
+ templates.push({ label: header.mime2DecodedSubject, value: uri });
+ }
+ }
+ return templates;
+ }
+ }
+
+ customElements.define(
+ "ruleactiontype-menulist",
+ MozRuleactiontypeMenulist,
+ { extends: "menulist" }
+ );
+ }
+
+ /**
+ * The MozRuleactionRichlistitem is a widget which gives the options to filter
+ * the messages with following elements: ruleactiontype-menulist, ruleactiontarget-wrapper
+ * and button to add or remove the MozRuleactionRichlistitem. It gets added in the
+ * filterActionList richlistbox in the Filter Editor dialog.
+ *
+ * @augments {MozElements.MozRichlistitem}
+ */
+ class MozRuleactionRichlistitem extends MozElements.MozRichlistitem {
+ static get inheritedAttributes() {
+ return { ".ruleactiontarget": "type=value" };
+ }
+
+ constructor() {
+ super();
+
+ this.mActionTypeInitialized = false;
+ this.mRuleActionTargetInitialized = false;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "ruleaction-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist is="ruleactiontype-menulist" style="flex: &filterActionTypeFlexValue;">
+ <menupopup>
+ <menuitem label="&moveMessage.label;"
+ value="movemessage"
+ enablefornews="false"></menuitem>
+ <menuitem label="&copyMessage.label;"
+ value="copymessage"></menuitem>
+ <menuseparator enablefornews="false"></menuseparator>
+ <menuitem label="&forwardTo.label;"
+ value="forwardmessage"
+ enablefornews="false"></menuitem>
+ <menuitem label="&replyWithTemplate.label;"
+ value="replytomessage"
+ enablefornews="false"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&markMessageRead.label;"
+ value="markasread"></menuitem>
+ <menuitem label="&markMessageUnread.label;"
+ value="markasunread"></menuitem>
+ <menuitem label="&markMessageStarred.label;"
+ value="markasflagged"></menuitem>
+ <menuitem label="&setPriority.label;"
+ value="setpriorityto"></menuitem>
+ <menuitem label="&addTag.label;"
+ value="addtagtomessage"></menuitem>
+ <menuitem label="&setJunkScore.label;"
+ value="setjunkscore"
+ enablefornews="false"></menuitem>
+ <menuseparator enableforpop3="true"></menuseparator>
+ <menuitem label="&deleteMessage.label;"
+ value="deletemessage"></menuitem>
+ <menuitem label="&deleteFromPOP.label;"
+ value="deletefrompopserver"
+ enableforpop3="true"></menuitem>
+ <menuitem label="&fetchFromPOP.label;"
+ value="fetchfrompopserver"
+ enableforpop3="true"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&ignoreThread.label;"
+ value="ignorethread"></menuitem>
+ <menuitem label="&ignoreSubthread.label;"
+ value="ignoresubthread"></menuitem>
+ <menuitem label="&watchThread.label;"
+ value="watchthread"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&stopExecution.label;"
+ value="stopexecution"></menuitem>
+ </menupopup>
+ </menulist>
+ <ruleactiontarget-wrapper class="ruleactiontarget"
+ style="flex: &filterActionTargetFlexValue;">
+ </ruleactiontarget-wrapper>
+ <hbox>
+ <button class="small-button"
+ label="+"
+ tooltiptext="&addAction.tooltip;"
+ oncommand="this.parentNode.parentNode.addRow();"></button>
+ <button class="small-button remove-small-button"
+ label="−"
+ tooltiptext="&removeAction.tooltip;"
+ oncommand="this.parentNode.parentNode.removeRow();"></button>
+ </hbox>
+ `,
+ [
+ "chrome://messenger/locale/messenger.dtd",
+ "chrome://messenger/locale/FilterEditor.dtd",
+ ]
+ )
+ );
+
+ this.mRuleActionType = this.querySelector("menulist");
+ this.mRemoveButton = this.querySelector(".remove-small-button");
+ this.mListBox = this.parentNode;
+ this.initializeAttributeInheritance();
+ }
+
+ set selected(val) {
+ // This provides a dummy selected property that the richlistbox expects to
+ // be able to call. See bug 202036.
+ }
+
+ get selected() {
+ return false;
+ }
+
+ _fireEvent(aName) {
+ // This provides a dummy _fireEvent function that the richlistbox expects to
+ // be able to call. See bug 202036.
+ }
+
+ /**
+ * We should only remove the initialActionIndex after we have been told that
+ * both the rule action type and the rule action target have both been built
+ * since they both need this piece of information. This complication arises
+ * because both of these child elements are getting bound asynchronously
+ * after the search row has been constructed.
+ */
+ clearInitialActionIndex() {
+ if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized) {
+ this.removeAttribute("initialActionIndex");
+ }
+ }
+
+ initWithAction(aFilterAction) {
+ let filterActionStr;
+ let actionTarget = this.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let nsMsgFilterAction = Ci.nsMsgFilterAction;
+ switch (aFilterAction.type) {
+ case nsMsgFilterAction.Custom:
+ filterActionStr = aFilterAction.customId;
+ if (actionItem) {
+ actionItem.children[0].value = aFilterAction.strValue;
+ }
+
+ // Make sure the custom action has been added. If not, it
+ // probably was from an extension that has been removed. We'll
+ // show a dummy menuitem to warn the user.
+ let needCustomLabel = true;
+ for (let i = 0; i < gCustomActions.length; i++) {
+ if (gCustomActions[i].id == filterActionStr) {
+ needCustomLabel = false;
+ break;
+ }
+ }
+ if (needCustomLabel) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ gFilterBundle.getString("filterMissingCustomAction")
+ );
+ menuitem.setAttribute("value", filterActionStr);
+ menuitem.disabled = true;
+ this.mRuleActionType.menupopup.appendChild(menuitem);
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.init(
+ "Missing custom action " + filterActionStr,
+ null,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.errorFlag,
+ "component javascript"
+ );
+ Services.console.logMessage(scriptError);
+ }
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ actionItem.children[0].value = aFilterAction.targetFolderUri;
+ break;
+ case nsMsgFilterAction.Reply:
+ case nsMsgFilterAction.Forward:
+ actionItem.children[0].value = aFilterAction.strValue;
+ break;
+ case nsMsgFilterAction.ChangePriority:
+ actionItem.children[0].value = aFilterAction.priority;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ actionItem.children[0].value = aFilterAction.junkScore;
+ break;
+ case nsMsgFilterAction.AddTag:
+ actionItem.children[0].value = aFilterAction.strValue;
+ break;
+ default:
+ break;
+ }
+ if (aFilterAction.type != nsMsgFilterAction.Custom) {
+ filterActionStr = gFilterActionStrings[aFilterAction.type];
+ }
+ this.mRuleActionType.value = filterActionStr;
+ this.mRuleActionTargetInitialized = true;
+ this.clearInitialActionIndex();
+ checkActionsReorder();
+ }
+
+ /**
+ * Function is used to check if the filter is valid or not. This routine
+ * also prompts the user.
+ *
+ * @returns {boolean} - true if this row represents a valid filter action.
+ */
+ validateAction() {
+ let filterActionString = this.getAttribute("value");
+ let actionTarget = this.children[1];
+ let actionTargetLabel =
+ actionTarget.ruleactiontargetElement &&
+ actionTarget.ruleactiontargetElement.children[0].value;
+ let errorString, customError;
+
+ switch (filterActionString) {
+ case "movemessage":
+ case "copymessage":
+ let msgFolder = actionTargetLabel
+ ? MailUtils.getOrCreateFolder(actionTargetLabel)
+ : null;
+ if (!msgFolder || !msgFolder.canFileMessages) {
+ errorString = "mustSelectFolder";
+ }
+ break;
+ case "forwardmessage":
+ if (
+ actionTargetLabel.length < 3 ||
+ actionTargetLabel.indexOf("@") < 1
+ ) {
+ errorString = "enterValidEmailAddress";
+ }
+ break;
+ case "replytomessage":
+ if (!actionTarget.ruleactiontargetElement.children[0].selectedItem) {
+ errorString = "pickTemplateToReplyWith";
+ }
+ break;
+ default:
+ // Locate the correct custom action, and check validity.
+ for (let i = 0; i < gCustomActions.length; i++) {
+ if (gCustomActions[i].id == filterActionString) {
+ customError = gCustomActions[i].validateActionValue(
+ actionTargetLabel,
+ gFilterList.folder,
+ gFilterType
+ );
+ break;
+ }
+ }
+ break;
+ }
+
+ errorString = errorString
+ ? gFilterBundle.getString(errorString)
+ : customError;
+ if (errorString) {
+ Services.prompt.alert(window, null, errorString);
+ }
+
+ return !errorString;
+ }
+
+ /**
+ * Create a new filter action, fill it in, and then append it to the filter.
+ *
+ * @param {object} aFilter - filter object to save.
+ */
+ saveToFilter(aFilter) {
+ let filterAction = aFilter.createAction();
+ let filterActionString = this.getAttribute("value");
+ filterAction.type = gFilterActionStrings.indexOf(filterActionString);
+ let actionTarget = this.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let nsMsgFilterAction = Ci.nsMsgFilterAction;
+ switch (filterAction.type) {
+ case nsMsgFilterAction.ChangePriority:
+ filterAction.priority = actionItem.children[0].getAttribute("value");
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ filterAction.targetFolderUri = actionItem.children[0].value;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ filterAction.junkScore = actionItem.children[0].value;
+ break;
+ case nsMsgFilterAction.Custom:
+ filterAction.customId = filterActionString;
+ // Fall through to set the value.
+ default:
+ if (actionItem && actionItem.children.length > 0) {
+ filterAction.strValue = actionItem.children[0].value;
+ }
+ break;
+ }
+ aFilter.appendAction(filterAction);
+ }
+
+ /**
+ * If we only have one row of actions, then disable the remove button for that row.
+ */
+ updateRemoveButton() {
+ this.mListBox.getItemAtIndex(0).mRemoveButton.disabled =
+ this.mListBox.getRowCount() == 1;
+ }
+
+ addRow() {
+ let listItem = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ listItem.classList.add("ruleaction");
+ listItem.setAttribute("onfocus", "this.storeFocus();");
+ this.mListBox.insertBefore(listItem, this.nextElementSibling);
+ this.mListBox.ensureElementIsVisible(listItem);
+
+ // Make sure the first remove button is enabled.
+ this.updateRemoveButton();
+ checkActionsReorder();
+ }
+
+ removeRow() {
+ // this.mListBox will fail after the row is removed, so save a reference.
+ let listBox = this.mListBox;
+ if (listBox.getRowCount() > 1) {
+ this.remove();
+ }
+ // Can't use 'this' as it is destroyed now.
+ listBox.getItemAtIndex(0).updateRemoveButton();
+ checkActionsReorder();
+ }
+
+ /**
+ * When this action row is focused, store its index in the parent richlistbox.
+ */
+ storeFocus() {
+ this.mListBox.setAttribute(
+ "focusedAction",
+ this.mListBox.getIndexOfItem(this)
+ );
+ }
+ }
+
+ customElements.define("ruleaction-richlistitem", MozRuleactionRichlistitem, {
+ extends: "richlistitem",
+ });
+}
diff --git a/comm/mailnews/search/content/viewLog.js b/comm/mailnews/search/content/viewLog.js
new file mode 100644
index 0000000000..6ac120b2cf
--- /dev/null
+++ b/comm/mailnews/search/content/viewLog.js
@@ -0,0 +1,38 @@
+/* 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 { MailE10SUtils } = ChromeUtils.import(
+ "resource:///modules/MailE10SUtils.jsm"
+);
+
+var gFilterList;
+var gLogFilters;
+var gLogView;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+function onLoad() {
+ gFilterList = window.arguments[0].filterList;
+
+ gLogFilters = document.getElementById("logFilters");
+ gLogFilters.checked = gFilterList.loggingEnabled;
+
+ gLogView = document.getElementById("logView");
+
+ // for security, disable JS
+ gLogView.browsingContext.allowJavascript = false;
+
+ MailE10SUtils.loadURI(gLogView, gFilterList.logURL);
+}
+
+function toggleLogFilters() {
+ gFilterList.loggingEnabled = gLogFilters.checked;
+}
+
+function clearLog() {
+ gFilterList.clearLog();
+
+ // reload the newly truncated file
+ gLogView.reload();
+}
diff --git a/comm/mailnews/search/content/viewLog.xhtml b/comm/mailnews/search/content/viewLog.xhtml
new file mode 100644
index 0000000000..66d2671df9
--- /dev/null
+++ b/comm/mailnews/search/content/viewLog.xhtml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/viewLog.dtd">
+
+<html
+ id="viewLogWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:filterlog"
+ width="600"
+ height="375"
+ persist="screenX screenY width height"
+ scrolling="false"
+>
+ <head>
+ <title>&viewLog.title;</title>
+ <script defer="defer" src="chrome://messenger/content/viewLog.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ >
+ <vbox flex="1">
+ <description>&viewLogInfo.text;</description>
+ <hbox>
+ <checkbox
+ id="logFilters"
+ label="&enableLog.label;"
+ accesskey="&enableLog.accesskey;"
+ oncommand="toggleLogFilters();"
+ />
+ <spacer flex="1" />
+ <button
+ label="&clearLog.label;"
+ accesskey="&clearLog.accesskey;"
+ oncommand="clearLog();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox flex="1">
+ <browser
+ id="logView"
+ class="inset"
+ type="content"
+ disablehistory="true"
+ disablesecurity="true"
+ src="about:blank"
+ autofind="false"
+ flex="1"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/search/public/moz.build b/comm/mailnews/search/public/moz.build
new file mode 100644
index 0000000000..39a41a1d2a
--- /dev/null
+++ b/comm/mailnews/search/public/moz.build
@@ -0,0 +1,37 @@
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIMsgFilter.idl",
+ "nsIMsgFilterCustomAction.idl",
+ "nsIMsgFilterHitNotify.idl",
+ "nsIMsgFilterList.idl",
+ "nsIMsgFilterPlugin.idl",
+ "nsIMsgFilterService.idl",
+ "nsIMsgOperationListener.idl",
+ "nsIMsgSearchAdapter.idl",
+ "nsIMsgSearchCustomTerm.idl",
+ "nsIMsgSearchNotify.idl",
+ "nsIMsgSearchScopeTerm.idl",
+ "nsIMsgSearchSession.idl",
+ "nsIMsgSearchTerm.idl",
+ "nsIMsgSearchValidityManager.idl",
+ "nsIMsgSearchValidityTable.idl",
+ "nsIMsgSearchValue.idl",
+ "nsIMsgTraitService.idl",
+ "nsMsgFilterCore.idl",
+ "nsMsgSearchCore.idl",
+]
+
+XPIDL_MODULE = "msgsearch"
+
+EXPORTS += [
+ "nsMsgBodyHandler.h",
+ "nsMsgResultElement.h",
+ "nsMsgSearchAdapter.h",
+ "nsMsgSearchBoolExpression.h",
+ "nsMsgSearchScopeTerm.h",
+ "nsMsgSearchTerm.h",
+]
diff --git a/comm/mailnews/search/public/nsIMsgFilter.idl b/comm/mailnews/search/public/nsIMsgFilter.idl
new file mode 100644
index 0000000000..6cf65c774e
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilter.idl
@@ -0,0 +1,124 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+#include "nsMsgFilterCore.idl"
+
+interface nsIOutputStream;
+interface nsIMsgFilterCustomAction;
+interface nsIMsgFilterList;
+interface nsIMsgSearchScopeTerm;
+interface nsIMsgSearchValue;
+interface nsIMsgSearchTerm;
+
+[scriptable, uuid(36d2748e-9246-44f3-bb74-46cbb0b8c23a)]
+interface nsIMsgRuleAction : nsISupports {
+
+ attribute nsMsgRuleActionType type;
+
+ // target priority.. throws an exception if the action is not priority
+ attribute nsMsgPriorityValue priority;
+
+ // target folder.. throws an exception if the action is not move to folder
+ attribute AUTF8String targetFolderUri;
+
+ attribute long junkScore;
+
+ attribute AUTF8String strValue;
+
+ // action id if type is Custom
+ attribute ACString customId;
+
+ // custom action associated with customId
+ // (which must be set prior to reading this attribute)
+ readonly attribute nsIMsgFilterCustomAction customAction;
+
+};
+
+[scriptable, uuid(d304fcfc-b588-11e4-981c-770e1e5d46b0)]
+interface nsIMsgFilter : nsISupports {
+ attribute nsMsgFilterTypeType filterType;
+ /**
+ * some filters are "temporary". For example, the filters we create when the user
+ * filters return receipts to the Sent folder.
+ * we don't show temporary filters in the UI
+ * and we don't write them to disk.
+ */
+ attribute boolean temporary;
+ attribute boolean enabled;
+ attribute AString filterName;
+ attribute ACString filterDesc;
+ attribute ACString unparsedBuffer; //holds the entire filter if we don't know how to handle it
+ attribute boolean unparseable; //whether we could parse the filter or not
+
+ attribute nsIMsgFilterList filterList; // owning filter list
+
+ void AddTerm(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op,
+ in nsIMsgSearchValue value,
+ in boolean BooleanAND,
+ in ACString arbitraryHeader);
+
+ void GetTerm(in long termIndex,
+ out nsMsgSearchAttribValue attrib,
+ out nsMsgSearchOpValue op,
+ out nsIMsgSearchValue value, // bad! using shared structure
+ out boolean BooleanAND,
+ out ACString arbitraryHeader);
+
+ void appendTerm(in nsIMsgSearchTerm term);
+
+ nsIMsgSearchTerm createTerm();
+
+ attribute Array<nsIMsgSearchTerm> searchTerms;
+
+ attribute nsIMsgSearchScopeTerm scope;
+
+ boolean MatchHdr(in nsIMsgDBHdr msgHdr, in nsIMsgFolder folder,
+ in nsIMsgDatabase db,
+ in ACString headers); // null-separated list of headers
+
+
+ /*
+ * Report that Rule was matched and executed when filter logging is enabled.
+ *
+ * @param aFilterAction The filter rule that was invoked.
+ * @param aHeader The header information of the message acted on by
+ * the filter.
+ */
+ void logRuleHit(in nsIMsgRuleAction aFilterAction,
+ in nsIMsgDBHdr aHeader);
+
+ /* Report that filtering failed for some reason when filter logging is enabled.
+ *
+ * @param aFilterAction Filter rule that was invoked.
+ * @param aHeader Header of the message acted on by the filter.
+ * @param aRcode Error code returned by low-level routine that
+ * led to the filter failure.
+ * @param aErrmsg Error message
+ */
+ void logRuleHitFail(in nsIMsgRuleAction aFilterAction,
+ in nsIMsgDBHdr aHeader,
+ in nsresult aRcode,
+ in AUTF8String aErrmsg);
+
+ nsIMsgRuleAction createAction();
+
+ nsIMsgRuleAction getActionAt(in unsigned long aIndex);
+
+ long getActionIndex(in nsIMsgRuleAction aAction);
+
+ void appendAction(in nsIMsgRuleAction action);
+
+ readonly attribute unsigned long actionCount;
+
+ void clearActionList();
+
+ // Returns the action list in the order it will be really executed in.
+ readonly attribute Array<nsIMsgRuleAction> sortedActionList;
+
+ void SaveToTextFile(in nsIOutputStream aStream);
+};
diff --git a/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl b/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl
new file mode 100644
index 0000000000..6e0f15cb09
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl
@@ -0,0 +1,88 @@
+/* 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/. */
+
+#include "nsMsgFilterCore.idl"
+
+interface nsIMsgCopyServiceListener;
+interface nsIMsgWindow;
+interface nsIMsgDBHdr;
+
+/**
+ * describes a custom action added to a message filter
+ */
+[scriptable,uuid(4699C41E-3671-436e-B6AE-4FD8106747E4)]
+interface nsIMsgFilterCustomAction : nsISupports
+{
+ /* globally unique string to identify this filter action.
+ * recommended form: ExtensionName@example.com#ActionName
+ */
+ readonly attribute ACString id;
+
+ /* action name to display in action list. This should be localized. */
+ readonly attribute AString name;
+
+ /**
+ * Is this custom action valid for a particular filter type?
+ *
+ * @param type the filter type
+ * @param scope the search scope
+ *
+ * @return true if valid
+ */
+ boolean isValidForType(in nsMsgFilterTypeType type, in nsMsgSearchScopeValue scope);
+
+ /**
+ * After the user inputs a particular action value for the action, determine
+ * if that value is valid.
+ *
+ * @param actionValue The value entered.
+ * @param actionFolder Folder in the filter list
+ * @param filterType Filter Type (Manual, OfflineMail, etc.)
+ *
+ * @return errorMessage A localized message to display if invalid
+ * Set to null if the actionValue is valid
+ */
+ AUTF8String validateActionValue(in AUTF8String actionValue,
+ in nsIMsgFolder actionFolder,
+ in nsMsgFilterTypeType filterType);
+
+ /* allow duplicate actions in the same filter list? Default No. */
+ attribute boolean allowDuplicates;
+
+ /*
+ * The custom action itself
+ *
+ * Generally for the applyAction method, folder-based methods give correct
+ * results and are preferred if available. Otherwise, be careful
+ * that the action does correct notifications to maintain counts, and correct
+ * manipulations of both IMAP and local non-database storage of message
+ * metadata.
+ */
+
+ /**
+ * Apply the custom action to an array of messages
+ *
+ * @param msgHdrs array of nsIMsgDBHdr objects of messages
+ * @param actionValue user-set value to use in the action
+ * @param copyListener calling method (filterType Manual only)
+ * @param filterType type of filter being applied
+ * @param msgWindow message window
+ */
+
+ void applyAction(in Array<nsIMsgDBHdr> msgHdrs,
+ in AUTF8String actionValue,
+ in nsIMsgCopyServiceListener copyListener,
+ in nsMsgFilterTypeType filterType,
+ in nsIMsgWindow msgWindow);
+
+ /* does this action start an async action? If so, a copy listener must
+ * be used to continue filter processing after the action. This only
+ * applies to after-the-fact (manual) filters. Call OnStopCopy when done
+ * using the copyListener to continue.
+ */
+ readonly attribute boolean isAsync;
+
+ /// Does this action need the message body?
+ readonly attribute boolean needsBody;
+};
diff --git a/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl b/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl
new file mode 100644
index 0000000000..c0bea47db1
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFilter;
+interface nsIMsgWindow;
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIMsgFilterHitNotify is an interface designed to make evaluating filters
+// easier. Clients typically open a filter list and ask the filter list to
+// evaluate the filters for a particular message, and pass in an
+// interface pointer to be notified of hits. The filter list will call the
+// ApplyFilterHit method on the interface pointer in case of hits, along with
+// the desired action and value.
+// return value is used to indicate whether the
+// filter list should continue trying to apply filters or not.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(c9f15174-1f3f-11d3-a51b-0060b0fc04b7)]
+interface nsIMsgFilterHitNotify : nsISupports {
+ boolean applyFilterHit(in nsIMsgFilter filter, in nsIMsgWindow msgWindow);
+};
diff --git a/comm/mailnews/search/public/nsIMsgFilterList.idl b/comm/mailnews/search/public/nsIMsgFilterList.idl
new file mode 100644
index 0000000000..fa294d5063
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilterList.idl
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgFilterHitNotify.idl"
+#include "nsMsgFilterCore.idl"
+
+interface nsIFile;
+interface nsIOutputStream;
+interface nsIMsgFilter;
+interface nsIMsgFolder;
+
+///////////////////////////////////////////////////////////////////////////////
+// The Msg Filter List is an interface designed to make accessing filter lists
+// easier. Clients typically open a filter list and either enumerate the filters,
+// or add new filters, or change the order around...
+//
+///////////////////////////////////////////////////////////////////////////////
+
+typedef long nsMsgFilterFileAttribValue;
+
+[scriptable, uuid(5d0ec03e-7e2f-49e9-b58a-b274c85f279e)]
+interface nsIMsgFilterList : nsISupports {
+
+ const nsMsgFilterFileAttribValue attribNone = 0;
+ const nsMsgFilterFileAttribValue attribVersion = 1;
+ const nsMsgFilterFileAttribValue attribLogging = 2;
+ const nsMsgFilterFileAttribValue attribName = 3;
+ const nsMsgFilterFileAttribValue attribEnabled = 4;
+ const nsMsgFilterFileAttribValue attribDescription = 5;
+ const nsMsgFilterFileAttribValue attribType = 6;
+ const nsMsgFilterFileAttribValue attribScriptFile = 7;
+ const nsMsgFilterFileAttribValue attribAction = 8;
+ const nsMsgFilterFileAttribValue attribActionValue = 9;
+ const nsMsgFilterFileAttribValue attribCondition = 10;
+ const nsMsgFilterFileAttribValue attribCustomId = 11;
+
+ /// Unique text identifier of this filter list.
+ readonly attribute ACString listId;
+ attribute nsIMsgFolder folder;
+ readonly attribute short version;
+ readonly attribute ACString arbitraryHeaders;
+ readonly attribute boolean shouldDownloadAllHeaders;
+ readonly attribute unsigned long filterCount;
+ nsIMsgFilter getFilterAt(in unsigned long filterIndex);
+ nsIMsgFilter getFilterNamed(in AString filterName);
+
+ void setFilterAt(in unsigned long filterIndex, in nsIMsgFilter filter);
+ void removeFilter(in nsIMsgFilter filter);
+ void removeFilterAt(in unsigned long filterIndex);
+
+ void moveFilterAt(in unsigned long filterIndex,
+ in nsMsgFilterMotionValue motion);
+ void moveFilter(in nsIMsgFilter filter,
+ in nsMsgFilterMotionValue motion);
+
+ void insertFilterAt(in unsigned long filterIndex, in nsIMsgFilter filter);
+
+ nsIMsgFilter createFilter(in AString name);
+
+ void saveToFile(in nsIOutputStream stream);
+
+ void parseCondition(in nsIMsgFilter aFilter, in string condition);
+ // this is temporary so that we can save the filterlist to disk
+ // without knowing where the filters were read from initially
+ // (such as the filter list dialog)
+ attribute nsIFile defaultFile;
+ void saveToDefaultFile();
+
+ void applyFiltersToHdr(in nsMsgFilterTypeType filterType,
+ in nsIMsgDBHdr msgHdr,
+ in nsIMsgFolder folder,
+ in nsIMsgDatabase db,
+ in ACString headers, // null-separated list of headers
+ in nsIMsgFilterHitNotify listener,
+ in nsIMsgWindow msgWindow);
+
+ // IO routines, used by filter object filing code.
+ void writeIntAttr(in nsMsgFilterFileAttribValue attrib, in long value, in nsIOutputStream stream);
+ void writeStrAttr(in nsMsgFilterFileAttribValue attrib, in string value, in nsIOutputStream stream);
+ void writeWstrAttr(in nsMsgFilterFileAttribValue attrib, in wstring value, in nsIOutputStream stream);
+ void writeBoolAttr(in nsMsgFilterFileAttribValue attrib, in boolean value, in nsIOutputStream stream);
+ boolean matchOrChangeFilterTarget(in AUTF8String oldUri, in AUTF8String newUri, in boolean caseInsensitive);
+
+ /**
+ * Turn filter logging on or off. Turning logging off will close any
+ * currently-open open logfile.
+ */
+ attribute boolean loggingEnabled;
+ /**
+ * The log will be written via logStream. This may be null if
+ * loggingEnabled is false or if there is some problem with the logging.
+ */
+ attribute nsIOutputStream logStream;
+ readonly attribute ACString logURL;
+ void clearLog();
+ void flushLogIfNecessary();
+ /**
+ * Push a message to the filter log file, adding a timestamp.
+ *
+ * @param message The message text to log.
+ * @param filter Optional filter object that reports the message.
+ */
+ void logFilterMessage(in AString message, [optional] in nsIMsgFilter filter);
+};
+
+
+/* these longs are all actually of type nsMsgFilterMotionValue */
+[scriptable, uuid(d067b528-304e-11d3-a0e1-00a0c900d445)]
+interface nsMsgFilterMotion : nsISupports {
+ const long up = 0;
+ const long down = 1;
+};
diff --git a/comm/mailnews/search/public/nsIMsgFilterPlugin.idl b/comm/mailnews/search/public/nsIMsgFilterPlugin.idl
new file mode 100644
index 0000000000..93934a364a
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilterPlugin.idl
@@ -0,0 +1,331 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgWindow;
+interface nsIFile;
+
+/**
+ * This interface is still very much under development, and is not yet stable.
+ */
+
+[scriptable, uuid(e2e56690-a676-11d6-80c9-00008646b737)]
+interface nsIMsgFilterPlugin : nsISupports
+{
+ /**
+ * Do any necessary cleanup: flush and close any open files, etc.
+ */
+ void shutdown();
+
+ /**
+ * Some protocols (ie IMAP) can, as an optimization, avoid
+ * downloading all message header lines. If your plugin doesn't need
+ * any more than the minimal set, it can return false for this attribute.
+ */
+ readonly attribute boolean shouldDownloadAllHeaders;
+
+};
+
+/*
+ * These interfaces typically implement a Bayesian classifier of messages.
+ *
+ * Two sets of interfaces may be used: the older junk-only interfaces, and
+ * the newer trait-oriented interfaces that treat junk classification as
+ * one of a set of classifications to accomplish.
+ */
+
+[scriptable, uuid(b15a0f9c-df07-4af0-9ba8-80dca68ac35d)]
+interface nsIJunkMailClassificationListener : nsISupports
+{
+ /**
+ * Inform a listener of a message's classification as junk. At the end
+ * of a batch of classifications, signify end of batch by calling with
+ * null aMsgURI (other parameters are don't care)
+ *
+ * @param aMsgURI URI of the message that was classified.
+ * @param aClassification classification of message as UNCLASSIFIED, GOOD,
+ * or JUNK.
+ * @param aJunkPercent indicator of degree of uncertainty, with 100 being
+ * probably junk, and 0 probably good
+ */
+ void onMessageClassified(in AUTF8String aMsgURI,
+ in nsMsgJunkStatus aClassification,
+ in uint32_t aJunkPercent);
+};
+
+[scriptable, uuid(AF247D07-72F0-482d-9EAB-5A786407AA4C)]
+interface nsIMsgTraitClassificationListener : nsISupports
+{
+ /**
+ * Inform a listener of a message's match to traits. The list
+ * of traits being matched is in aTraits. Corresponding
+ * indicator of match (percent) is in aPercents. At the end
+ * of a batch of classifications, signify end of batch by calling with
+ * null aMsgURI (other parameters are don't care)
+ *
+ * @param aMsgURI URI of the message that was classified
+ * @param aTraits array of matched trait ids
+ * @param aPercents array of percent match (0 is unmatched, 100 is fully
+ * matched) of the trait with the corresponding array
+ * index in aTraits
+ */
+ void onMessageTraitsClassified(in AUTF8String aMsgURI,
+ in Array<unsigned long> aTraits,
+ in Array<unsigned long> aPercents);
+};
+
+[scriptable, uuid(12667532-88D1-44a7-AD48-F73719BE5C92)]
+interface nsIMsgTraitDetailListener : nsISupports
+{
+ /**
+ * Inform a listener of details of a message's match to traits.
+ * This returns the tokens that were used in the calculation,
+ * the calculated percent probability that each token matches the trait,
+ * and a running estimate (starting with the strongest tokens) of the
+ * combined total probability that a message matches the trait, when
+ * only tokens stronger than the current token are used.
+ *
+ * @param aMsgURI URI of the message that was classified
+ * @param aProTrait trait id of pro trait for the calculation
+ * @param tokenStrings the string for a particular token
+ * @param tokenPercents calculated probability that a message with that token
+ * matches the trait
+ * @param runningPercents calculated probability that the message matches the
+ * trait, accounting for this token and all stronger tokens.
+ */
+ void onMessageTraitDetails(in AUTF8String aMsgUri,
+ in unsigned long aProTrait,
+ in Array<AString> tokenStrings,
+ in Array<unsigned long> tokenPercents,
+ in Array<unsigned long> runningPercents);
+};
+
+[scriptable, uuid(8EA5BBCA-F735-4d43-8541-D203D8E2FF2F)]
+interface nsIJunkMailPlugin : nsIMsgFilterPlugin
+{
+ /**
+ * Message classifications.
+ */
+ const nsMsgJunkStatus UNCLASSIFIED = 0;
+ const nsMsgJunkStatus GOOD = 1;
+ const nsMsgJunkStatus JUNK = 2;
+
+ /**
+ * Message junk score constants. Junkscore can only be one of these two
+ * values (or not set).
+ */
+ const nsMsgJunkScore IS_SPAM_SCORE = 100; // junk
+ const nsMsgJunkScore IS_HAM_SCORE = 0; // not junk
+
+ /**
+ * Trait ids for junk analysis. These values are fixed to ensure
+ * backwards compatibility with existing junk-oriented classification
+ * code.
+ */
+
+ const unsigned long GOOD_TRAIT = 1; // good
+ const unsigned long JUNK_TRAIT = 2; // junk
+
+ /**
+ * Given a message URI, determine what its current classification is
+ * according to the current training set.
+ */
+ void classifyMessage(in AUTF8String aMsgURI, in nsIMsgWindow aMsgWindow,
+ in nsIJunkMailClassificationListener aListener);
+
+ void classifyMessages(in Array<AUTF8String> aMsgURIs,
+ in nsIMsgWindow aMsgWindow,
+ in nsIJunkMailClassificationListener aListener);
+
+ /**
+ * Given a message URI, evaluate its relative match to a list of
+ * traits according to the current training set.
+ *
+ * @param aMsgURI URI of the message to be evaluated
+ * @param aProTraits array of trait ids for trained messages that
+ * match the tested trait (for example,
+ * JUNK_TRAIT if testing for junk)
+ * @param aAntiTraits array of trait ids for trained messages that
+ * do not match the tested trait (for example,
+ * GOOD_TRAIT if testing for junk)
+ * @param aTraitListener trait-oriented callback listener (may be null)
+ * @param aMsgWindow current message window (may be null)
+ * @param aJunkListener junk-oriented callback listener (may be null)
+ */
+
+ void classifyTraitsInMessage(
+ in AUTF8String aMsgURI,
+ in Array<unsigned long> aProTraits,
+ in Array<unsigned long> aAntiTraits,
+ in nsIMsgTraitClassificationListener aTraitListener,
+ [optional] in nsIMsgWindow aMsgWindow,
+ [optional] in nsIJunkMailClassificationListener aJunkListener);
+
+ /**
+ * Given an array of message URIs, evaluate their relative match to a
+ * list of traits according to the current training set.
+ *
+ * @param aMsgURIs array of URIs of the messages to be evaluated
+ * @param aProTraits array of trait ids for trained messages that
+ * match the tested trait (for example,
+ * JUNK_TRAIT if testing for junk)
+ * @param aAntiTraits array of trait ids for trained messages that
+ * do not match the tested trait (for example,
+ * GOOD_TRAIT if testing for junk)
+ * @param aTraitListener trait-oriented callback listener (may be null)
+ * @param aMsgWindow current message window (may be null)
+ * @param aJunkListener junk-oriented callback listener (may be null)
+ */
+
+ void classifyTraitsInMessages(
+ in Array<AUTF8String> aMsgURIs,
+ in Array<unsigned long> aProTraits,
+ in Array<unsigned long> aAntiTraits,
+ in nsIMsgTraitClassificationListener aTraitListener,
+ [optional] in nsIMsgWindow aMsgWindow,
+ [optional] in nsIJunkMailClassificationListener aJunkListener);
+
+ /**
+ * Called when a user forces the classification of a message. Should
+ * cause the training set to be updated appropriately.
+ *
+ * @arg aMsgURI URI of the message to be classified
+ * @arg aOldUserClassification Was it previous manually classified
+ * by the user? If so, how?
+ * @arg aNewClassification New manual classification.
+ * @arg aListener Callback (may be null)
+ */
+ void setMessageClassification(
+ in AUTF8String aMsgURI, in nsMsgJunkStatus aOldUserClassification,
+ in nsMsgJunkStatus aNewClassification,
+ in nsIMsgWindow aMsgWindow,
+ in nsIJunkMailClassificationListener aListener);
+
+ /**
+ * Called when a user forces a change in the classification of a message.
+ * Should cause the training set to be updated appropriately.
+ *
+ * @param aMsgURI URI of the message to be classified
+ * @param aOldTraits array of trait IDs of the old
+ * message classification(s), if any
+ * @param aNewTraits array of trait IDs of the new
+ * message classification(s), if any
+ * @param aTraitListener trait-oriented listener (may be null)
+ * @param aMsgWindow current message window (may be null)
+ * @param aJunkListener junk-oriented listener (may be null)
+ */
+ void setMsgTraitClassification(
+ in AUTF8String aMsgURI,
+ in Array<unsigned long> aOldTraits,
+ in Array<unsigned long> aNewTraits,
+ [optional] in nsIMsgTraitClassificationListener aTraitListener,
+ [optional] in nsIMsgWindow aMsgWindow,
+ [optional] in nsIJunkMailClassificationListener aJunkListener);
+
+ readonly attribute boolean userHasClassified;
+
+ /** Removes the training file and clears out any in memory training tokens.
+ User must retrain after doing this.
+ **/
+ void resetTrainingData();
+
+ /**
+ * Given a message URI, return a list of tokens and their contribution to
+ * the analysis of a message's match to a trait according to the
+ * current training set.
+ *
+ * @param aMsgURI URI of the message to be evaluated
+ * @param aProTrait trait id for trained messages that match the
+ * tested trait (for example, JUNK_TRAIT if testing
+ * for junk)
+ * @param aAntiTrait trait id for trained messages that do not match
+ * the tested trait (for example, GOOD_TRAIT
+ * if testing for junk)
+ * @param aListener callback listener for results
+ * @param aMsgWindow current message window (may be null)
+ */
+ void detailMessage(
+ in AUTF8String aMsgURI,
+ in unsigned long aProTrait,
+ in unsigned long aAntiTrait,
+ in nsIMsgTraitDetailListener aListener,
+ [optional] in nsIMsgWindow aMsgWindow);
+
+};
+
+/**
+ * The nsIMsgCorpus interface manages a corpus of mail data used for
+ * statistical analysis of messages.
+ */
+[scriptable, uuid(70BAD26F-DFD4-41bd-8FAB-4C09B9C1E845)]
+interface nsIMsgCorpus : nsISupports
+{
+ /**
+ * Clear the corpus data for a trait id.
+ *
+ * @param aTrait trait id
+ */
+ void clearTrait(in unsigned long aTrait);
+
+ /**
+ * Update corpus data from a file.
+ * Uses the parallel arrays aFromTraits and aToTraits. These arrays allow
+ * conversion of the trait id stored in the file (which may be originated
+ * externally) to the trait id used in the local corpus (which is defined
+ * locally using nsIMsgTraitService, and mapped by that interface to a
+ * globally unique trait id string).
+ *
+ * @param aFile the file with the data, in the format:
+ *
+ * Format of the trait file for version 1:
+ * [0xFCA93601] (the 01 is the version)
+ * for each trait to write:
+ * [id of trait to write] (0 means end of list)
+ * [number of messages per trait]
+ * for each token with non-zero count
+ * [count]
+ * [length of word]word
+ *
+ * @param aIsAdd should the data be added, or removed? True if
+ * adding, false if removing.
+ *
+ * @param aFromTraits array of trait ids used in aFile. If aFile contains
+ * trait ids that are not in this array, they are not
+ * remapped, but assumed to be local trait ids.
+ *
+ * @param aToTraits array of trait ids, corresponding to elements of
+ * aFromTraits, that represent the local trait ids to
+ * be used in storing data from aFile into the local corpus.
+ */
+ void updateData(in nsIFile aFile, in boolean aIsAdd,
+ [optional] in Array<unsigned long> aFromTraits,
+ [optional] in Array<unsigned long> aToTraits);
+
+ /**
+ * Get the corpus count for a token as a string.
+ *
+ * @param aWord string of characters representing the token
+ * @param aTrait trait id
+ *
+ * @return count of that token in the corpus
+ *
+ */
+ unsigned long getTokenCount(in AUTF8String aWord, in unsigned long aTrait);
+
+ /**
+ * Gives information on token and message count information in the
+ * training data corpus.
+ *
+ * @param aTrait trait id (may be null)
+ * @param aMessageCount count of messages that have been trained with aTrait
+ *
+ * @return token count for all traits
+ */
+
+ unsigned long corpusCounts(in unsigned long aTrait, out unsigned long aMessageCount);
+};
diff --git a/comm/mailnews/search/public/nsIMsgFilterService.idl b/comm/mailnews/search/public/nsIMsgFilterService.idl
new file mode 100644
index 0000000000..439dd40f93
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgFilterService.idl
@@ -0,0 +1,102 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+#include "nsISupports.idl"
+#include "nsMsgFilterCore.idl"
+
+interface nsIMsgFilterList;
+interface nsIMsgWindow;
+interface nsIMsgFilterCustomAction;
+interface nsIFile;
+interface nsIMsgFolder;
+interface nsIMsgSearchCustomTerm;
+interface nsIMsgOperationListener;
+
+[scriptable, uuid(78a74023-1692-4567-8d72-9ca58fbbd427)]
+interface nsIMsgFilterService : nsISupports {
+
+ nsIMsgFilterList OpenFilterList(in nsIFile filterFile, in nsIMsgFolder rootFolder, in nsIMsgWindow msgWindow);
+ void CloseFilterList(in nsIMsgFilterList filterList);
+
+ void SaveFilterList(in nsIMsgFilterList filterList,
+ in nsIFile filterFile);
+
+ void CancelFilterList(in nsIMsgFilterList filterList);
+ nsIMsgFilterList getTempFilterList(in nsIMsgFolder aFolder);
+ void applyFiltersToFolders(in nsIMsgFilterList aFilterList,
+ in Array<nsIMsgFolder> aFolders,
+ in nsIMsgWindow aMsgWindow,
+ [optional] in nsIMsgOperationListener aCallback);
+
+ /**
+ * Apply filters to a specific list of messages in a folder.
+ * @param aFilterType The type of filter to match against
+ * @param aMsgHdrList The list of message headers (nsIMsgDBHdr objects)
+ * @param aFolder The folder the messages belong to
+ * @param aMsgWindow A UI window for attaching progress/dialogs
+ * @param aCallback A listener that gets notified of any filtering error
+ */
+ void applyFilters(in nsMsgFilterTypeType aFilterType,
+ in Array<nsIMsgDBHdr> aMsgHdrList,
+ in nsIMsgFolder aFolder,
+ in nsIMsgWindow aMsgWindow,
+ [optional] in nsIMsgOperationListener aCallback);
+
+ /**
+ * Add a custom filter action.
+ *
+ * @param aAction the custom action to add
+ */
+ void addCustomAction(in nsIMsgFilterCustomAction aAction);
+
+ /**
+ * get the list of custom actions
+ *
+ * @return an array of nsIMsgFilterCustomAction objects
+ */
+ Array<nsIMsgFilterCustomAction> getCustomActions();
+
+ /**
+ * Lookup a custom action given its id.
+ *
+ * @param id unique identifier for a particular custom action
+ *
+ * @return the custom action, or null if not found
+ */
+ nsIMsgFilterCustomAction getCustomAction(in ACString id);
+
+ /**
+ * Add a custom search term.
+ *
+ * @param aTerm the custom term to add
+ */
+ void addCustomTerm(in nsIMsgSearchCustomTerm aTerm);
+
+ /**
+ * get the list of custom search terms
+ *
+ * @return an array of nsIMsgSearchCustomTerm objects
+ */
+ Array<nsIMsgSearchCustomTerm> getCustomTerms();
+
+ /**
+ * Lookup a custom search term given its id.
+ *
+ * @param id unique identifier for a particular custom search term
+ *
+ * @return the custom search term, or null if not found
+ */
+ nsIMsgSearchCustomTerm getCustomTerm(in ACString id);
+
+ /**
+ * Translate the filter type flag into human readable type names.
+ * In case of multiple flag they are delimited by '&'.
+ *
+ * @param filterType nsMsgFilterType flags of filter type
+ *
+ * @return A string describing the filter type.
+ */
+ ACString filterTypeName(in nsMsgFilterTypeType filterType);
+};
diff --git a/comm/mailnews/search/public/nsIMsgOperationListener.idl b/comm/mailnews/search/public/nsIMsgOperationListener.idl
new file mode 100644
index 0000000000..30751cd5c3
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgOperationListener.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+#include "nsISupports.idl"
+
+// Listener used to notify when an operation has completed.
+[scriptable, uuid(bdaef6ff-0909-435b-8fcd-76525dd2364c)]
+interface nsIMsgOperationListener : nsISupports {
+ /**
+ * Called when the operation stops (possibly with errors)
+ *
+ * @param aStatus Success or failure of the operation
+ */
+ void onStopOperation(in nsresult aStatus);
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchAdapter.idl b/comm/mailnews/search/public/nsIMsgSearchAdapter.idl
new file mode 100644
index 0000000000..9784fdda79
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchAdapter.idl
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+
+#include "nsISupports.idl"
+#include "nsIMsgSearchScopeTerm.idl"
+
+[ptr] native nsMsgResultElement(nsMsgResultElement);
+
+%{C++
+class nsMsgResultElement;
+%}
+
+[scriptable, uuid(0b09078b-e0cd-440a-afee-01f45808ee74)]
+interface nsIMsgSearchAdapter : nsISupports {
+ void ValidateTerms();
+ void Search(out boolean done);
+ void SendUrl();
+ void CurrentUrlDone(in nsresult exitCode);
+
+ void AddHit(in nsMsgKey key);
+ void AddResultElement(in nsIMsgDBHdr aHdr);
+
+ [noscript] void OpenResultElement(in nsMsgResultElement element);
+ [noscript] void ModifyResultElement(in nsMsgResultElement element,
+ in nsMsgSearchValue value);
+
+ readonly attribute string encoding;
+
+ [noscript] nsIMsgFolder FindTargetFolder([const] in nsMsgResultElement
+ element);
+ void Abort();
+ void getSearchCharsets(out AString srcCharset, out AString destCharset);
+ /*
+ * Clear the saved scope reference. This is used when deleting scope, which is not
+ * reference counted in nsMsgSearchSession
+ */
+ void clearScope();
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl b/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl
new file mode 100644
index 0000000000..b2e3c024cd
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl
@@ -0,0 +1,75 @@
+/* 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/. */
+
+#include "nsMsgSearchCore.idl"
+
+/**
+ * describes a custom term added to a message search or filter
+ */
+[scriptable,uuid(925DB5AA-21AF-494c-8652-984BC7BAD13A)]
+interface nsIMsgSearchCustomTerm : nsISupports
+{
+ /**
+ * globally unique string to identify this search term.
+ * recommended form: ExtensionName@example.com#TermName
+ * Commas and quotes are not allowed, the id must not
+ * parse to an integer, and names of standard search
+ * attributes in SearchAttribEntryTable in nsMsgSearchTerm.cpp
+ * are not allowed.
+ */
+ readonly attribute ACString id;
+
+ /// name to display in term list. This should be localized. */
+ readonly attribute AString name;
+
+ /// Does this term need the message body?
+ readonly attribute boolean needsBody;
+
+ /**
+ * Is this custom term enabled?
+ *
+ * @param scope search scope (nsMsgSearchScope)
+ * @param op search operator (nsMsgSearchOp). If null, determine
+ * if term is available for any operator.
+ *
+ * @return true if enabled
+ */
+ boolean getEnabled(in nsMsgSearchScopeValue scope,
+ in nsMsgSearchOpValue op);
+
+ /**
+ * Is this custom term available?
+ *
+ * @param scope search scope (nsMsgSearchScope)
+ * @param op search operator (nsMsgSearchOp). If null, determine
+ * if term is available for any operator.
+ *
+ * @return true if available
+ */
+ boolean getAvailable(in nsMsgSearchScopeValue scope,
+ in nsMsgSearchOpValue op);
+
+ /**
+ * List the valid operators for this term.
+ *
+ * @param scope search scope (nsMsgSearchScope)
+ *
+ * @return array of operators
+ */
+ Array<nsMsgSearchOpValue> getAvailableOperators(in nsMsgSearchScopeValue scope);
+
+ /**
+ * Apply the custom search term to a message
+ *
+ * @param msgHdr header database reference representing the message
+ * @param searchValue user-set value to use in the search
+ * @param searchOp search operator (Contains, IsHigherThan, etc.)
+ *
+ * @return true if the term matches the message, else false
+ */
+
+ boolean match(in nsIMsgDBHdr msgHdr,
+ in AUTF8String searchValue,
+ in nsMsgSearchOpValue searchOp);
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchNotify.idl b/comm/mailnews/search/public/nsIMsgSearchNotify.idl
new file mode 100644
index 0000000000..1e3493ad83
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchNotify.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgSearchSession;
+interface nsIMsgFolder;
+
+// when a search is run, this interface is passed in as a listener
+// on the search.
+[scriptable, uuid(ca37784d-352b-4c39-8ccb-0abc1a93f681)]
+interface nsIMsgSearchNotify : nsISupports
+{
+ void onSearchHit(in nsIMsgDBHdr header, in nsIMsgFolder folder);
+
+ // notification that a search has finished.
+ void onSearchDone(in nsresult status);
+ /*
+ * until we can encode searches with a URI, this will be an
+ * out-of-bound way to connect a set of search terms to a datasource
+ */
+
+ /*
+ * called when a new search begins
+ */
+ void onNewSearch();
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl b/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl
new file mode 100644
index 0000000000..63a130d9ea
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsIMsgSearchSession.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+interface nsILineInputStream;
+interface nsIInputStream;
+
+[scriptable, uuid(934672c3-9b8f-488a-935d-87b4023fa0be)]
+interface nsIMsgSearchScopeTerm : nsISupports {
+ nsIInputStream getInputStream(in nsIMsgDBHdr aHdr);
+ void closeInputStream();
+ readonly attribute nsIMsgFolder folder;
+ readonly attribute nsIMsgSearchSession searchSession;
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchSession.idl b/comm/mailnews/search/public/nsIMsgSearchSession.idl
new file mode 100644
index 0000000000..024ce829b2
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchSession.idl
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgSearchValue.idl"
+
+interface nsIMsgSearchAdapter;
+interface nsIMsgSearchTerm;
+interface nsIMsgSearchNotify;
+interface nsIMsgDatabase;
+interface nsIMsgWindow;
+
+//////////////////////////////////////////////////////////////////////////////
+// The Msg Search Session is an interface designed to make constructing
+// searches easier. Clients typically build up search terms, and then run
+// the search
+//////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(1ed69bbf-7983-4602-9a9b-2f2263a78878)]
+interface nsIMsgSearchSession : nsISupports {
+
+/**
+ * add a search term to the search session
+ *
+ * @param attrib search attribute (e.g. nsMsgSearchAttrib::Subject)
+ * @param op search operator (e.g. nsMsgSearchOp::Contains)
+ * @param value search value (e.g. "Dogbert", see nsIMsgSearchValue)
+ * @param BooleanAND set to true if associated boolean operator is AND
+ * @param customString if attrib > nsMsgSearchAttrib::OtherHeader,
+ * a user defined arbitrary header
+ * if attrib == nsMsgSearchAttrib::Custom, the custom id
+ * otherwise ignored
+ */
+ void addSearchTerm(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op,
+ in nsIMsgSearchValue value,
+ in boolean BooleanAND,
+ in string customString);
+
+ attribute Array<nsIMsgSearchTerm> searchTerms;
+
+ nsIMsgSearchTerm createTerm();
+ void appendTerm(in nsIMsgSearchTerm term);
+
+ /**
+ * @name Search notification flags
+ * These flags determine which notifications will be sent.
+ * @{
+ */
+ /// search started notification
+ const long onNewSearch = 0x1;
+
+ /// search finished notification
+ const long onSearchDone = 0x2;
+
+ /// search hit notification
+ const long onSearchHit = 0x4;
+
+ const long allNotifications = 0x7;
+ /** @} */
+
+ /**
+ * Add a listener to get notified of search starts, stops, and hits.
+ *
+ * @param aListener listener
+ * @param aNotifyFlags which notifications to send. Defaults to all
+ */
+ void registerListener(in nsIMsgSearchNotify aListener,
+ [optional] in long aNotifyFlags);
+ void unregisterListener(in nsIMsgSearchNotify listener);
+
+ readonly attribute unsigned long numSearchTerms;
+
+ readonly attribute nsIMsgSearchAdapter runningAdapter;
+
+ void getNthSearchTerm(in long whichTerm,
+ in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op,
+ in nsIMsgSearchValue value); // wrong, should be out
+
+ long countSearchScopes();
+
+ void getNthSearchScope(in long which,out nsMsgSearchScopeValue scopeId, out nsIMsgFolder folder);
+
+ /* add a scope (e.g. a mail folder) to the search */
+ void addScopeTerm(in nsMsgSearchScopeValue scope,
+ in nsIMsgFolder folder);
+
+ void addDirectoryScopeTerm(in nsMsgSearchScopeValue scope);
+
+ void clearScopes();
+
+ /* Call this function every time the scope changes! It informs the FE if
+ the current scope support custom header use. FEs should not display the
+ custom header dialog if custom headers are not supported */
+ [noscript] boolean ScopeUsesCustomHeaders(in nsMsgSearchScopeValue scope,
+ /* could be a folder or server based on scope */
+ in voidPtr selection,
+ in boolean forFilters);
+
+ /* use this to determine if your attribute is a string attrib */
+ boolean IsStringAttribute(in nsMsgSearchAttribValue attrib);
+
+ /* add all scopes of a given type to the search */
+ void AddAllScopes(in nsMsgSearchScopeValue attrib);
+
+ void search(in nsIMsgWindow aWindow);
+ void interruptSearch();
+
+ // these two methods are used when the search session is using
+ // a timer to do local search, and the search adapter needs
+ // to run a url (e.g., to reparse a local folder) and wants to
+ // pause the timer while running the url. This will fail if the
+ // current adapter is not using a timer.
+ void pauseSearch();
+ void resumeSearch();
+
+ boolean MatchHdr(in nsIMsgDBHdr aMsgHdr, in nsIMsgDatabase aDatabase);
+
+ void addSearchHit(in nsIMsgDBHdr header, in nsIMsgFolder folder);
+
+ readonly attribute long numResults;
+ attribute nsIMsgWindow window;
+
+ /* these longs are all actually of type nsMsgSearchBooleanOp */
+ const long BooleanOR=0;
+ const long BooleanAND=1;
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchTerm.idl b/comm/mailnews/search/public/nsIMsgSearchTerm.idl
new file mode 100644
index 0000000000..5724ba1c1a
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchTerm.idl
@@ -0,0 +1,153 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsMsgSearchCore.idl"
+#include "nsIMsgSearchValue.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgDatabase;
+interface nsIMsgSearchScopeTerm;
+
+[scriptable, uuid(705a2b5a-5efc-495c-897a-bef1161cd3c0)]
+interface nsIMsgSearchTerm : nsISupports {
+ attribute nsMsgSearchAttribValue attrib;
+ attribute nsMsgSearchOpValue op;
+ attribute nsIMsgSearchValue value;
+
+ attribute boolean booleanAnd;
+ attribute ACString arbitraryHeader;
+ /**
+ * Not to be confused with arbitraryHeader, which is a header in the
+ * rfc822 message. This is a property of the nsIMsgDBHdr, and may have
+ * nothing to do the message headers, e.g., gloda-id.
+ * value.str will be compared with nsIMsgHdr::GetProperty(hdrProperty).
+ */
+ attribute ACString hdrProperty;
+
+ /// identifier for a custom id used for this term, if any.
+ attribute ACString customId;
+
+ attribute boolean beginsGrouping;
+ attribute boolean endsGrouping;
+
+ /**
+ * Match the value against one of the emails found in the incoming
+ * 2047-encoded string.
+ */
+ boolean matchRfc822String(in ACString aString, in string charset);
+ /**
+ * Match the current header value against the incoming 2047-encoded string.
+ *
+ * This method will first apply the nsIMimeConverter decoding to the string
+ * (using the supplied parameters) and will then match the value against the
+ * decoded result.
+ */
+ boolean matchRfc2047String(in ACString aString, in string charset, in boolean charsetOverride);
+ boolean matchDate(in PRTime aTime);
+ boolean matchStatus(in unsigned long aStatus);
+ boolean matchPriority(in nsMsgPriorityValue priority);
+ boolean matchAge(in PRTime days);
+ boolean matchSize(in unsigned long size);
+ boolean matchJunkStatus(in string aJunkScore);
+ /*
+ * Test search term match for junkpercent
+ *
+ * @param aJunkPercent junkpercent for message (0-100, 100 is junk)
+ * @return true if matches
+ */
+ boolean matchJunkPercent(in unsigned long aJunkPercent);
+ /*
+ * Test search term match for junkscoreorigin
+ * @param aJunkScoreOrigin Who set junk score? Possible values:
+ * plugin filter imapflag user whitelist
+ * @return true if matches
+ */
+ boolean matchJunkScoreOrigin(in string aJunkScoreOrigin);
+
+ /**
+ * Test if the body of the passed in message matches "this" search term.
+ * @param aScopeTerm scope of search
+ * @param aOffset offset of message in message store.
+ * @param aLength length of message.
+ * @param aCharset folder charset.
+ * @param aMsg db msg hdr of message to match.
+ * @param aDB db containing msg header.
+ */
+ boolean matchBody(in nsIMsgSearchScopeTerm aScopeTerm,
+ in unsigned long long aOffset,
+ in unsigned long aLength,
+ in string aCharset,
+ in nsIMsgDBHdr aMsg,
+ in nsIMsgDatabase aDb);
+
+ /**
+ * Test if the arbitrary header specified by this search term
+ * matches the corresponding header in the passed in message.
+ *
+ * @param aScopeTerm scope of search
+ * @param aLength length of message
+ * @param aCharset The charset to apply to un-labeled non-UTF-8 data.
+ * @param aCharsetOverride If true, aCharset is used instead of any
+ * charset labeling other than UTF-8.
+ * @param aMsg The nsIMsgDBHdr of the message
+ * @param aDb The message database containing aMsg
+ * @param aHeaders A null-separated list of message headers.
+ * @param aForFilters Whether this is a filter or a search operation.
+ */
+ boolean matchArbitraryHeader(in nsIMsgSearchScopeTerm aScopeTerm,
+ in unsigned long aLength,
+ in string aCharset,
+ in boolean aCharsetOverride,
+ in nsIMsgDBHdr aMsg,
+ in nsIMsgDatabase aDb,
+ in ACString aHeaders,
+ in boolean aForFilters);
+
+ /**
+ * Compares value.str with nsIMsgHdr::GetProperty(hdrProperty).
+ * @param msg msg to match db hdr property of.
+ *
+ * @returns true if msg matches property, false otherwise.
+ */
+ boolean matchHdrProperty(in nsIMsgDBHdr msg);
+
+ /**
+ * Compares value.status with nsIMsgHdr::GetUint32Property(hdrProperty).
+ * @param msg msg to match db hdr property of.
+ *
+ * @returns true if msg matches property, false otherwise.
+ */
+ boolean matchUint32HdrProperty(in nsIMsgDBHdr msg);
+
+ /**
+ * Compares value.status with the folder flags of the msg's folder.
+ * @param msg msgHdr whose folder's flag we want to compare.
+ *
+ * @returns true if folder's flags match value.status, false otherwise.
+ */
+ boolean matchFolderFlag(in nsIMsgDBHdr msg);
+
+ readonly attribute boolean matchAllBeforeDeciding;
+
+ readonly attribute ACString termAsString;
+ boolean matchKeyword(in ACString keyword); // used for tag searches
+ attribute boolean matchAll;
+ /**
+ * Does the message match the custom search term?
+ *
+ * @param msg message database object representing the message
+ *
+ * @return true if message matches
+ */
+ boolean matchCustom(in nsIMsgDBHdr msg);
+
+ /**
+ * Returns a nsMsgSearchAttribValue value corresponding to a field string from
+ * the nsMsgSearchTerm.cpp::SearchAttribEntryTable table.
+ * Does not handle custom attributes yet.
+ */
+ nsMsgSearchAttribValue getAttributeFromString(in string aAttribName);
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl b/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl
new file mode 100644
index 0000000000..dbc7958bd8
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgSearchValidityTable.idl"
+
+typedef long nsMsgSearchValidityScope;
+
+[scriptable, uuid(6A352055-DE6E-49d2-A256-89E0B9EC405E)]
+interface nsIMsgSearchValidityManager : nsISupports {
+ nsIMsgSearchValidityTable getTable(in nsMsgSearchValidityScope scope);
+
+ /**
+ * Given a search attribute (which is an internal numerical id), return
+ * the string name that you can use as a key to look up the localized
+ * string in the search-attributes.properties file.
+ *
+ * @param aSearchAttribute attribute type from interface nsMsgSearchAttrib
+ *
+ * @return localization-friendly string representation
+ * of the attribute
+ */
+ AString getAttributeProperty(in nsMsgSearchAttribValue aSearchAttribute);
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl b/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl
new file mode 100644
index 0000000000..75fd4efd51
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "nsMsgSearchCore.idl"
+
+[scriptable, uuid(b07f1cb6-fae9-4d92-9edb-03f9ad249c66)]
+interface nsIMsgSearchValidityTable : nsISupports {
+
+ void setAvailable(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op, in boolean active);
+ void setEnabled(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op, in boolean enabled);
+ void setValidButNotShown(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op, in boolean valid);
+
+ boolean getAvailable(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op);
+ boolean getEnabled(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op);
+ boolean getValidButNotShown(in nsMsgSearchAttribValue attrib,
+ in nsMsgSearchOpValue op);
+
+ readonly attribute long numAvailAttribs;
+
+ Array<nsMsgSearchAttribValue> getAvailableAttributes();
+ Array<nsMsgSearchOpValue> getAvailableOperators(in nsMsgSearchAttribValue attrib);
+
+ void setDefaultAttrib(in nsMsgSearchAttribValue defaultAttrib);
+};
diff --git a/comm/mailnews/search/public/nsIMsgSearchValue.idl b/comm/mailnews/search/public/nsIMsgSearchValue.idl
new file mode 100644
index 0000000000..6a4cec0b28
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgSearchValue.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsMsgSearchCore.idl"
+
+interface nsIMsgFolder;
+
+[scriptable, uuid(783758a0-cdb5-11dc-95ff-0800200c9a66)]
+interface nsIMsgSearchValue : nsISupports {
+ // type of object
+ attribute nsMsgSearchAttribValue attrib;
+
+ // accessing these will throw an exception if the above
+ // attribute does not match the type!
+ attribute AString str;
+ attribute nsMsgPriorityValue priority;
+ attribute PRTime date;
+ // see nsMsgMessageFlags.idl and nsMsgFolderFlags.idl
+ attribute unsigned long status;
+ attribute unsigned long size;
+ attribute nsMsgKey msgKey;
+ attribute long age; // in days
+ attribute nsIMsgFolder folder;
+ attribute nsMsgJunkStatus junkStatus;
+ /*
+ * junkPercent is set by the message filter plugin, and is approximately
+ * proportional to the probability that a message is junk.
+ * (range 0-100, 100 is junk)
+ */
+ attribute unsigned long junkPercent;
+
+ AString toString();
+};
diff --git a/comm/mailnews/search/public/nsIMsgTraitService.idl b/comm/mailnews/search/public/nsIMsgTraitService.idl
new file mode 100644
index 0000000000..23ade21d8d
--- /dev/null
+++ b/comm/mailnews/search/public/nsIMsgTraitService.idl
@@ -0,0 +1,182 @@
+/* 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/. */
+
+ /**
+ * This interface provides management of traits that are used to categorize
+ * messages. A trait is some characteristic of a message, such as being "junk"
+ * or "personal", that may be discoverable by analysis of the message.
+ *
+ * Traits are described by a universal identifier "id" as a string, as well
+ * as a local integer identifier "index". One purpose of this service is to
+ * provide the mapping between those forms.
+ *
+ * Recommended (but not required) format for id:
+ * "extensionName@example.org#traitName"
+ */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(2CB15FB0-A912-40d3-8882-F2765C75655F)]
+interface nsIMsgTraitService : nsISupports
+{
+ /**
+ * the highest ever index for a registered trait. The first trait is 1,
+ * == 0 means no traits are defined
+ */
+ readonly attribute long lastIndex;
+
+ /**
+ * Register a trait. May be called multiple times, but subsequent
+ * calls do not register the trait
+ *
+ * @param id the trait universal identifier
+ *
+ * @return the internal index for the registered trait if newly
+ * registered, else 0
+ */
+ unsigned long registerTrait(in ACString id);
+
+ /**
+ * Unregister a trait.
+ *
+ * @param id the trait universal identifier
+ */
+ void unRegisterTrait(in ACString id);
+
+ /**
+ * is a trait registered?
+ *
+ * @param id the trait universal identifier
+ *
+ * @return true if registered
+ */
+ boolean isRegistered(in ACString id);
+
+ /**
+ * set the trait name, which is an optional short description of the trait
+ *
+ * @param id the trait universal identifier
+ * @param name description of the trait.
+ */
+ void setName(in ACString id, in ACString name);
+
+ /**
+ * get the trait name, which is an optional short description of the trait
+ *
+ * @param id the trait universal identifier
+ *
+ * @return description of the trait
+ */
+ ACString getName(in ACString id);
+
+ /**
+ * get the internal index number for the trait.
+ *
+ * @param id the trait universal identifier
+ *
+ * @return internal index number for the trait
+ */
+ unsigned long getIndex(in ACString id);
+
+ /**
+ * get the trait universal identifier for an internal trait index
+ *
+ * @param index the internal identifier for the trait
+ *
+ * @return trait universal identifier
+ */
+ ACString getId(in unsigned long index);
+
+ /**
+ * enable the trait for analysis. Each enabled trait will be analyzed by
+ * the bayesian code. The enabled trait is the "pro" trait that represents
+ * messages matching the trait. Each enabled trait also needs a corresponding
+ * anti trait defined, which represents messages that do not match the trait.
+ * The anti trait does not need to be enabled
+ *
+ * @param id the trait universal identifier
+ * @param enabled should this trait be processed by the bayesian analyzer?
+ */
+ void setEnabled(in ACString id, in boolean enabled);
+
+ /**
+ * Should this trait be processed by the bayes analyzer?
+ *
+ * @param id the trait universal identifier
+ *
+ * @return true if this is a "pro" trait to process
+ */
+ boolean getEnabled(in ACString id);
+
+ /**
+ * set the anti trait, which indicates messages that have been marked as
+ * NOT matching a particular trait.
+ *
+ * @param id the trait universal identifier
+ * @param antiId trait id for messages marked as not matching the trait
+ */
+ void setAntiId(in ACString id, in ACString antiId);
+
+ /**
+ * get the id of traits that do not match a particular trait
+ *
+ * @param id the trait universal identifier for a "pro" trait
+ *
+ * @return universal trait identifier for an "anti" trait that does not
+ * match the "pro" trait messages
+ */
+ ACString getAntiId(in ACString id);
+
+ /**
+ * Get an array of "pro" traits to be analyzed by the bayesian code. This is
+ * a "pro" trait of messages that match the trait.
+ * Only enabled traits are returned.
+ * This should return the same number of indices as the corresponding call to
+ * getEnabledAntiIndices().
+ *
+ * @return an array of trait internal indices for "pro" trait to analyze
+ */
+ Array<unsigned long> getEnabledProIndices();
+
+ /**
+ * Get an array of "anti" traits to be analyzed by the bayesian code. This is
+ * a "anti" trait of messages that do not match the trait.
+ * Only enabled traits are returned.
+ * This should return the same number of indices as the corresponding call to
+ * getEnabledProIndices().
+ *
+ * @return an array of trait internal indices for "anti" trait to analyze
+ */
+ Array<unsigned long> getEnabledAntiIndices();
+
+ /**
+ * Add a trait as an alias of another trait. An alias is a trait whose
+ * counts will be combined with the aliased trait. This allows multiple sets
+ * of corpus data to be used to provide information on a single message
+ * characteristic, while allowing each individual set of corpus data to
+ * retain its own identity.
+ *
+ * @param aTraitIndex the internal identifier for the aliased trait
+ * @param aTraitAlias the internal identifier for the alias to add
+ */
+ void addAlias(in unsigned long aTraitIndex, in unsigned long aTraitAlias);
+
+ /**
+ * Removes a trait as an alias of another trait.
+ *
+ * @param aTraitIndex the internal identifier for the aliased trait
+ * @param aTraitAlias the internal identifier for the alias to remove
+ */
+ void removeAlias(in unsigned long aTraitIndex, in unsigned long aTraitAlias);
+
+ /**
+ * Get an array of trait aliases for a trait index, if any
+ *
+ * @param aTraitIndex the internal identifier for the aliased trait
+ *
+ * @return an array of internal identifiers for aliases
+ */
+ Array<unsigned long> getAliases(in unsigned long aTraitIndex);
+
+};
diff --git a/comm/mailnews/search/public/nsMsgBodyHandler.h b/comm/mailnews/search/public/nsMsgBodyHandler.h
new file mode 100644
index 0000000000..1252e2c20b
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgBodyHandler.h
@@ -0,0 +1,110 @@
+/* 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/. */
+
+#ifndef __nsMsgBodyHandler_h
+#define __nsMsgBodyHandler_h
+
+#include "nsIMsgSearchScopeTerm.h"
+#include "nsILineInputStream.h"
+#include "nsIMsgDatabase.h"
+
+//---------------------------------------------------------------------------
+// nsMsgBodyHandler: used to retrieve lines from POP and IMAP offline messages.
+// This is a helper class used by nsMsgSearchTerm::MatchBody
+//---------------------------------------------------------------------------
+class nsMsgBodyHandler {
+ public:
+ nsMsgBodyHandler(nsIMsgSearchScopeTerm*, uint32_t length, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db);
+
+ // we can also create a body handler when doing arbitrary header
+ // filtering...we need the list of headers and the header size as well
+ // if we are doing filtering...if ForFilters is false, headers and
+ // headersSize is ignored!!!
+ nsMsgBodyHandler(nsIMsgSearchScopeTerm*, uint32_t length, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db,
+ const char* headers /* NULL terminated list of headers */,
+ uint32_t headersSize, bool ForFilters);
+
+ virtual ~nsMsgBodyHandler();
+
+ // Returns next message line in buf and the applicable charset, if found.
+ // The return value is the length of 'buf' or -1 for EOF.
+ int32_t GetNextLine(nsCString& buf, nsCString& charset);
+ bool IsQP() { return m_partIsQP; }
+
+ // Transformations
+ void SetStripHeaders(bool strip) { m_stripHeaders = strip; }
+
+ protected:
+ void Initialize(); // common initialization code
+
+ // filter related methods. For filtering we always use the headers
+ // list instead of the database...
+ bool m_Filtering;
+ int32_t GetNextFilterLine(nsCString& buf);
+ // pointer into the headers list in the original message hdr db...
+ const char* m_headers;
+ uint32_t m_headersSize;
+ uint32_t m_headerBytesRead;
+
+ // local / POP related methods
+ void OpenLocalFolder();
+
+ // goes through the mail folder
+ int32_t GetNextLocalLine(nsCString& buf);
+
+ nsIMsgSearchScopeTerm* m_scope;
+ nsCOMPtr<nsILineInputStream> m_fileLineStream;
+ nsCOMPtr<nsIFile> m_localFile;
+
+ /**
+ * The number of lines in the message. If |m_lineCountInBodyLines| then this
+ * is the number of body lines, otherwise this is the entire number of lines
+ * in the message. This is important so we know when to stop reading the file
+ * without accidentally reading part of the next message.
+ */
+ uint32_t m_numLocalLines;
+ /**
+ * When true, |m_numLocalLines| is the number of body lines in the message,
+ * when false it is the entire number of lines in the message.
+ *
+ * When a message is an offline IMAP or news message, then the number of lines
+ * will be the entire number of lines, so this should be false. When the
+ * message is a local message, the number of lines will be the number of body
+ * lines.
+ */
+ bool m_lineCountInBodyLines;
+
+ // Offline IMAP related methods & state
+
+ nsCOMPtr<nsIMsgDBHdr> m_msgHdr;
+ nsCOMPtr<nsIMsgDatabase> m_db;
+
+ // Transformations
+ // With the exception of m_isMultipart, these all apply to the various parts
+ bool m_stripHeaders; // true if we're supposed to strip of message headers
+ bool m_pastMsgHeaders; // true if we've already skipped over the message
+ // headers
+ bool m_pastPartHeaders; // true if we've already skipped over the part
+ // headers
+ bool m_partIsQP; // true if the Content-Transfer-Encoding header claims
+ // quoted-printable
+ bool m_partIsHtml; // true if the Content-type header claims text/html
+ bool m_base64part; // true if the current part is in base64
+ bool m_isMultipart; // true if the message is a multipart/* message
+ bool m_partIsText; // true if the current part is text/*
+ bool m_inMessageAttachment; // true if current part is message/*
+
+ nsTArray<nsCString> m_boundaries; // The boundary strings to look for
+ nsCString m_partCharset; // The charset found in the part
+
+ // See implementation for comments
+ int32_t ApplyTransformations(const nsCString& line, int32_t length,
+ bool& returnThisLine, nsCString& buf);
+ void SniffPossibleMIMEHeader(const nsCString& line);
+ static void StripHtml(nsCString& buf);
+ static void Base64Decode(nsCString& buf);
+};
+#endif
diff --git a/comm/mailnews/search/public/nsMsgFilterCore.idl b/comm/mailnews/search/public/nsMsgFilterCore.idl
new file mode 100644
index 0000000000..42adc5e730
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgFilterCore.idl
@@ -0,0 +1,62 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgSearchCore.idl"
+
+typedef long nsMsgFilterTypeType;
+
+[scriptable,uuid(b963a9c6-3a75-4d91-9f79-7186418d4d2d)]
+interface nsMsgFilterType : nsISupports {
+ /* these longs are all actually of type nsMsgFilterTypeType */
+ const long None = 0x00;
+ const long InboxRule = 0x01;
+ const long InboxJavaScript = 0x02;
+ const long Inbox = InboxRule | InboxJavaScript;
+ const long NewsRule = 0x04;
+ const long NewsJavaScript = 0x08;
+ const long News = NewsRule | NewsJavaScript;
+ const long Incoming = Inbox | News;
+ const long Manual = 0x10;
+ const long PostPlugin = 0x20; // After bayes filtering
+ const long PostOutgoing = 0x40; // After sending
+ const long Archive = 0x80; // Before archiving
+ const long Periodic = 0x100;// On a repeating timer
+ const long All = Incoming | Manual;
+};
+
+typedef long nsMsgFilterMotionValue;
+
+typedef long nsMsgFilterIndex;
+
+typedef long nsMsgRuleActionType;
+
+[scriptable, uuid(7726FE79-AFA3-4a39-8292-733AEE288737)]
+interface nsMsgFilterAction : nsISupports {
+
+ // Custom Action.
+ const long Custom=-1;
+ /* if you change these, you need to update filter.properties,
+ look for filterActionX */
+ /* these longs are all actually of type nsMsgFilterActionType */
+ const long None=0; /* uninitialized state */
+ const long MoveToFolder=1;
+ const long ChangePriority=2;
+ const long Delete=3;
+ const long MarkRead=4;
+ const long KillThread=5;
+ const long WatchThread=6;
+ const long MarkFlagged=7;
+ const long Reply=9;
+ const long Forward=10;
+ const long StopExecution=11;
+ const long DeleteFromPop3Server=12;
+ const long LeaveOnPop3Server=13;
+ const long JunkScore=14;
+ const long FetchBodyFromPop3Server=15;
+ const long CopyToFolder=16;
+ const long AddTag=17;
+ const long KillSubthread=18;
+ const long MarkUnread=19;
+};
diff --git a/comm/mailnews/search/public/nsMsgResultElement.h b/comm/mailnews/search/public/nsMsgResultElement.h
new file mode 100644
index 0000000000..104e6a3771
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgResultElement.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef __nsMsgResultElement_h
+#define __nsMsgResultElement_h
+
+#include "nsMsgSearchCore.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsTArray.h"
+
+// nsMsgResultElement specifies a single search hit.
+
+//---------------------------------------------------------------------------
+// nsMsgResultElement is a list of attribute/value pairs which are used to
+// represent a search hit without requiring a message header or server
+// connection
+//---------------------------------------------------------------------------
+
+class nsMsgResultElement {
+ public:
+ explicit nsMsgResultElement(nsIMsgSearchAdapter*);
+ virtual ~nsMsgResultElement();
+
+ static nsresult AssignValues(nsIMsgSearchValue* src, nsMsgSearchValue* dst);
+ nsresult GetValue(nsMsgSearchAttribValue, nsMsgSearchValue**) const;
+ nsresult AddValue(nsIMsgSearchValue*);
+ nsresult AddValue(nsMsgSearchValue*);
+
+ nsresult GetPrettyName(nsMsgSearchValue**);
+ nsresult Open(void* window);
+
+ nsTArray<nsCOMPtr<nsIMsgSearchValue> > m_valueList;
+ nsIMsgSearchAdapter* m_adapter;
+
+ protected:
+};
+
+#endif
diff --git a/comm/mailnews/search/public/nsMsgSearchAdapter.h b/comm/mailnews/search/public/nsMsgSearchAdapter.h
new file mode 100644
index 0000000000..fbfa5176e6
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgSearchAdapter.h
@@ -0,0 +1,242 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSearchAdapter_H_
+#define _nsMsgSearchAdapter_H_
+
+#include "nsMsgSearchCore.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsIMsgSearchValidityTable.h"
+#include "nsIMsgSearchValidityManager.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsINntpIncomingServer.h"
+
+class nsIMsgSearchScopeTerm;
+
+//-----------------------------------------------------------------------------
+// These Adapter classes contain the smarts to convert search criteria from
+// the canonical structures in msg_srch.h into whatever format is required
+// by their protocol.
+//
+// There is a separate Adapter class for area (pop, imap, nntp, ldap) to contain
+// the special smarts for that protocol.
+//-----------------------------------------------------------------------------
+
+class nsMsgSearchAdapter : public nsIMsgSearchAdapter {
+ public:
+ nsMsgSearchAdapter(nsIMsgSearchScopeTerm*,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const&);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSEARCHADAPTER
+
+ nsIMsgSearchScopeTerm* m_scope;
+ nsTArray<RefPtr<nsIMsgSearchTerm>>
+ m_searchTerms; /* linked list of criteria terms */
+
+ nsString m_defaultCharset = u"UTF-8"_ns;
+
+ static nsresult EncodeImap(
+ char** ppEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms,
+ const char16_t* srcCharset, const char16_t* destCharset,
+ bool reallyDredd = false);
+
+ static nsresult EncodeImapValue(char* encoding, const char* value,
+ bool useQuotes, bool reallyDredd);
+
+ static char* GetImapCharsetParam(const char16_t* destCharset);
+ static char16_t* EscapeSearchUrl(const char16_t* nntpCommand);
+ static char16_t* EscapeImapSearchProtocol(const char16_t* imapCommand);
+ static char16_t* EscapeQuoteImapSearchProtocol(const char16_t* imapCommand);
+ static char* UnEscapeSearchUrl(const char* commandSpecificData);
+ // This stuff lives in the base class because the IMAP search syntax
+ // is used by the Dredd SEARCH command as well as IMAP itself
+ static const char* m_kImapBefore;
+ static const char* m_kImapBody;
+ static const char* m_kImapCC;
+ static const char* m_kImapFrom;
+ static const char* m_kImapNot;
+ static const char* m_kImapOr;
+ static const char* m_kImapSince;
+ static const char* m_kImapSubject;
+ static const char* m_kImapTo;
+ static const char* m_kImapHeader;
+ static const char* m_kImapAnyText;
+ static const char* m_kImapKeyword;
+ static const char* m_kNntpKeywords;
+ static const char* m_kImapSentOn;
+ static const char* m_kImapSeen;
+ static const char* m_kImapAnswered;
+ static const char* m_kImapNotSeen;
+ static const char* m_kImapNotAnswered;
+ static const char* m_kImapCharset;
+ static const char* m_kImapUnDeleted;
+ static const char* m_kImapSizeSmaller;
+ static const char* m_kImapSizeLarger;
+ static const char* m_kImapNew;
+ static const char* m_kImapNotNew;
+ static const char* m_kImapFlagged;
+ static const char* m_kImapNotFlagged;
+
+ protected:
+ virtual ~nsMsgSearchAdapter();
+ typedef enum _msg_TransformType {
+ kOverwrite, /* "John Doe" -> "John*Doe", simple contains */
+ kInsert, /* "John Doe" -> "John* Doe", name completion */
+ kSurround /* "John Doe" -> "John* *Doe", advanced contains */
+ } msg_TransformType;
+
+ char* TransformSpacesToStars(const char*, msg_TransformType transformType);
+ nsresult OpenNewsResultInUnknownGroup(nsMsgResultElement*);
+
+ static nsresult EncodeImapTerm(nsIMsgSearchTerm*, bool reallyDredd,
+ const char16_t* srcCharset,
+ const char16_t* destCharset, char** ppOutTerm);
+};
+
+//-----------------------------------------------------------------------------
+// Validity checking for attrib/op pairs. We need to know what operations are
+// legal in three places:
+// 1. when the FE brings up the dialog box and needs to know how to build
+// the menus and enable their items
+// 2. when the FE fires off a search, we need to check their lists for
+// correctness
+// 3. for on-the-fly capability negotiation e.g. with XSEARCH-capable news
+// servers
+//-----------------------------------------------------------------------------
+
+class nsMsgSearchValidityTable final : public nsIMsgSearchValidityTable {
+ public:
+ nsMsgSearchValidityTable();
+ NS_DECL_NSIMSGSEARCHVALIDITYTABLE
+ NS_DECL_ISUPPORTS
+
+ protected:
+ int m_numAvailAttribs; // number of rows with at least one available operator
+ typedef struct vtBits {
+ uint16_t bitEnabled : 1;
+ uint16_t bitAvailable : 1;
+ uint16_t bitValidButNotShown : 1;
+ } vtBits;
+ vtBits m_table[nsMsgSearchAttrib::kNumMsgSearchAttributes]
+ [nsMsgSearchOp::kNumMsgSearchOperators];
+
+ private:
+ ~nsMsgSearchValidityTable() {}
+ nsMsgSearchAttribValue m_defaultAttrib;
+};
+
+// Using getters and setters seems a little nicer then dumping the 2-D array
+// syntax all over the code
+#define CHECK_AO \
+ if (a < 0 || a >= nsMsgSearchAttrib::kNumMsgSearchAttributes || o < 0 || \
+ o >= nsMsgSearchOp::kNumMsgSearchOperators) \
+ return NS_ERROR_ILLEGAL_VALUE;
+inline nsresult nsMsgSearchValidityTable::SetAvailable(int a, int o, bool b) {
+ CHECK_AO;
+ m_table[a][o].bitAvailable = b;
+ return NS_OK;
+}
+inline nsresult nsMsgSearchValidityTable::SetEnabled(int a, int o, bool b) {
+ CHECK_AO;
+ m_table[a][o].bitEnabled = b;
+ return NS_OK;
+}
+inline nsresult nsMsgSearchValidityTable::SetValidButNotShown(int a, int o,
+ bool b) {
+ CHECK_AO;
+ m_table[a][o].bitValidButNotShown = b;
+ return NS_OK;
+}
+
+inline nsresult nsMsgSearchValidityTable::GetAvailable(int a, int o,
+ bool* aResult) {
+ CHECK_AO;
+ *aResult = m_table[a][o].bitAvailable;
+ return NS_OK;
+}
+inline nsresult nsMsgSearchValidityTable::GetEnabled(int a, int o,
+ bool* aResult) {
+ CHECK_AO;
+ *aResult = m_table[a][o].bitEnabled;
+ return NS_OK;
+}
+inline nsresult nsMsgSearchValidityTable::GetValidButNotShown(int a, int o,
+ bool* aResult) {
+ CHECK_AO;
+ *aResult = m_table[a][o].bitValidButNotShown;
+ return NS_OK;
+}
+#undef CHECK_AO
+
+class nsMsgSearchValidityManager : public nsIMsgSearchValidityManager {
+ public:
+ nsMsgSearchValidityManager();
+
+ protected:
+ virtual ~nsMsgSearchValidityManager();
+
+ public:
+ NS_DECL_NSIMSGSEARCHVALIDITYMANAGER
+ NS_DECL_ISUPPORTS
+
+ nsresult GetTable(int, nsMsgSearchValidityTable**);
+
+ protected:
+ // There's one global validity manager that everyone uses. You *could* do
+ // this with static members of the adapter classes, but having a dedicated
+ // object makes cleanup of these tables (at shutdown-time) automagic.
+
+ nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailFilterTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailFilterTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_onlineManualFilterTable;
+
+ nsCOMPtr<nsIMsgSearchValidityTable> m_newsTable; // online news
+
+ // Local news tables, used for local news searching or offline.
+ nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsTable; // base table
+ nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsJunkTable; // base + junk
+ nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsBodyTable; // base + body
+ nsCOMPtr<nsIMsgSearchValidityTable>
+ m_localNewsJunkBodyTable; // base + junk + body
+ nsCOMPtr<nsIMsgSearchValidityTable> m_ldapTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_ldapAndTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_localABTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_localABAndTable;
+ nsCOMPtr<nsIMsgSearchValidityTable> m_newsFilterTable;
+
+ nsresult NewTable(nsIMsgSearchValidityTable**);
+
+ nsresult InitOfflineMailTable();
+ nsresult InitOfflineMailFilterTable();
+ nsresult InitOnlineMailTable();
+ nsresult InitOnlineMailFilterTable();
+ nsresult InitOnlineManualFilterTable();
+ nsresult InitNewsTable();
+ nsresult InitLocalNewsTable();
+ nsresult InitLocalNewsJunkTable();
+ nsresult InitLocalNewsBodyTable();
+ nsresult InitLocalNewsJunkBodyTable();
+ nsresult InitNewsFilterTable();
+
+ // set the custom headers in the table, changes whenever
+ // "mailnews.customHeaders" pref changes.
+ nsresult SetOtherHeadersInTable(nsIMsgSearchValidityTable* table,
+ const char* customHeaders);
+
+ nsresult InitLdapTable();
+ nsresult InitLdapAndTable();
+ nsresult InitLocalABTable();
+ nsresult InitLocalABAndTable();
+ nsresult SetUpABTable(nsIMsgSearchValidityTable* aTable, bool isOrTable);
+ nsresult EnableDirectoryAttribute(nsIMsgSearchValidityTable* table,
+ nsMsgSearchAttribValue aSearchAttrib);
+};
+
+#endif
diff --git a/comm/mailnews/search/public/nsMsgSearchBoolExpression.h b/comm/mailnews/search/public/nsMsgSearchBoolExpression.h
new file mode 100644
index 0000000000..1eefd81fcc
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgSearchBoolExpression.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsMsgSearchCore.h"
+
+#ifndef __nsMsgSearchBoolExpression_h
+# define __nsMsgSearchBoolExpression_h
+
+//-----------------------------------------------------------------------------
+// nsMsgSearchBoolExpression is a class added to provide AND/OR terms in search
+// queries.
+// A nsMsgSearchBoolExpression contains either a search term or two
+// nsMsgSearchBoolExpressions and
+// a boolean operator.
+// I (mscott) am placing it here for now....
+//-----------------------------------------------------------------------------
+
+/* CBoolExpression --> encapsulates one or more search terms by internally
+ representing the search terms and their boolean operators as a binary
+ expression tree. Each node in the tree consists of either
+ (1) a boolean operator and two nsMsgSearchBoolExpressions or
+ (2) if the node is a leaf node then it contains a search term.
+ With each search term that is part of the expression we may also keep
+ a character string. The character
+ string is used to store the IMAP/NNTP encoding of the search term. This
+ makes generating a search encoding (for online) easier.
+
+ For IMAP/NNTP: nsMsgSearchBoolExpression has/assumes knowledge about how
+ AND and OR search terms are combined according to IMAP4 and NNTP protocol.
+ That is the only piece of IMAP/NNTP knowledge it is aware of.
+
+ Order of Evaluation: Okay, the way in which the boolean expression tree
+ is put together directly effects the order of evaluation. We currently
+ support left to right evaluation.
+ Supporting other order of evaluations involves adding new internal add
+ term methods.
+ */
+
+class nsMsgSearchBoolExpression {
+ public:
+ // create a leaf node expression
+ explicit nsMsgSearchBoolExpression(nsIMsgSearchTerm* aNewTerm,
+ char* aEncodingString = NULL);
+
+ // create a non-leaf node expression containing 2 expressions
+ // and a boolean operator
+ nsMsgSearchBoolExpression(nsMsgSearchBoolExpression*,
+ nsMsgSearchBoolExpression*,
+ nsMsgSearchBooleanOperator boolOp);
+
+ nsMsgSearchBoolExpression();
+ ~nsMsgSearchBoolExpression(); // recursively destroys all sub
+ // expressions as well
+
+ // accessors
+
+ // Offline
+ static nsMsgSearchBoolExpression* AddSearchTerm(
+ nsMsgSearchBoolExpression* aOrigExpr, nsIMsgSearchTerm* aNewTerm,
+ char* aEncodingStr); // IMAP/NNTP
+ static nsMsgSearchBoolExpression* AddExpressionTree(
+ nsMsgSearchBoolExpression* aOrigExpr,
+ nsMsgSearchBoolExpression* aExpression, bool aBoolOp);
+
+ // parses the expression tree and all
+ // expressions underneath this node to
+ // determine if the end result is true or false.
+ bool OfflineEvaluate(nsIMsgDBHdr* msgToMatch, const char* defaultCharset,
+ nsIMsgSearchScopeTerm* scope, nsIMsgDatabase* db,
+ const nsACString& headers, bool Filtering);
+
+ // assuming the expression is for online
+ // searches, determine the length of the
+ // resulting IMAP/NNTP encoding string
+ int32_t CalcEncodeStrSize();
+
+ // fills pre-allocated
+ // memory in buffer with
+ // the IMAP/NNTP encoding for the expression
+ void GenerateEncodeStr(nsCString* buffer);
+
+ // if we are not a leaf node, then we have two other expressions
+ // and a boolean operator
+ nsMsgSearchBoolExpression* m_leftChild;
+ nsMsgSearchBoolExpression* m_rightChild;
+ nsMsgSearchBooleanOperator m_boolOp;
+
+ protected:
+ // if we are a leaf node, all we have is a search term
+
+ nsIMsgSearchTerm* m_term;
+
+ // store IMAP/NNTP encoding for the search term if applicable
+ nsCString m_encodingStr;
+
+ // internal methods
+
+ // the idea is to separate the public interface for adding terms to
+ // the expression tree from the order of evaluation which influences
+ // how we internally construct the tree. Right now, we are supporting
+ // left to right evaluation so the tree is constructed to represent
+ // that by calling leftToRightAddTerm. If future forms of evaluation
+ // need to be supported, add new methods here for proper tree construction.
+ nsMsgSearchBoolExpression* leftToRightAddTerm(nsIMsgSearchTerm* newTerm,
+ char* encodingStr);
+};
+
+#endif
diff --git a/comm/mailnews/search/public/nsMsgSearchCore.idl b/comm/mailnews/search/public/nsMsgSearchCore.idl
new file mode 100644
index 0000000000..77b18dfa57
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgSearchCore.idl
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+
+interface nsIMsgDatabase;
+interface nsIMsgDBHdr;
+
+[scriptable, uuid(6e893e59-af98-4f62-a326-0f00f32147cd)]
+
+interface nsMsgSearchScope : nsISupports {
+ const nsMsgSearchScopeValue offlineMail = 0;
+ const nsMsgSearchScopeValue offlineMailFilter = 1;
+ const nsMsgSearchScopeValue onlineMail = 2;
+ const nsMsgSearchScopeValue onlineMailFilter = 3;
+ /// offline news, base table, no body or junk
+ const nsMsgSearchScopeValue localNews = 4;
+ const nsMsgSearchScopeValue news = 5;
+ const nsMsgSearchScopeValue newsEx = 6;
+ const nsMsgSearchScopeValue LDAP = 7;
+ const nsMsgSearchScopeValue LocalAB = 8;
+ const nsMsgSearchScopeValue allSearchableGroups = 9;
+ const nsMsgSearchScopeValue newsFilter = 10;
+ const nsMsgSearchScopeValue LocalABAnd = 11;
+ const nsMsgSearchScopeValue LDAPAnd = 12;
+ // IMAP and NEWS, searched using local headers
+ const nsMsgSearchScopeValue onlineManual = 13;
+ /// local news + junk
+ const nsMsgSearchScopeValue localNewsJunk = 14;
+ /// local news + body
+ const nsMsgSearchScopeValue localNewsBody = 15;
+ /// local news + junk + body
+ const nsMsgSearchScopeValue localNewsJunkBody = 16;
+};
+
+typedef long nsMsgSearchAttribValue;
+
+/**
+ * Definitions of search attribute types. The numerical order
+ * from here will also be used to determine the order that the
+ * attributes display in the filter editor.
+ */
+[scriptable, uuid(a83ca7e8-4591-4111-8fb8-fd76ac73c866)]
+interface nsMsgSearchAttrib : nsISupports {
+ const nsMsgSearchAttribValue Custom = -2; /* a custom term, see nsIMsgSearchCustomTerm */
+ const nsMsgSearchAttribValue Default = -1;
+ const nsMsgSearchAttribValue Subject = 0; /* mail and news */
+ const nsMsgSearchAttribValue Sender = 1;
+ const nsMsgSearchAttribValue Body = 2;
+ const nsMsgSearchAttribValue Date = 3;
+
+ const nsMsgSearchAttribValue Priority = 4; /* mail only */
+ const nsMsgSearchAttribValue MsgStatus = 5;
+ const nsMsgSearchAttribValue To = 6;
+ const nsMsgSearchAttribValue CC = 7;
+ const nsMsgSearchAttribValue ToOrCC = 8;
+ const nsMsgSearchAttribValue AllAddresses = 9;
+
+ const nsMsgSearchAttribValue Location = 10; /* result list only */
+ const nsMsgSearchAttribValue MessageKey = 11; /* message result elems */
+ const nsMsgSearchAttribValue AgeInDays = 12;
+ const nsMsgSearchAttribValue FolderInfo = 13; /* for "view thread context" from result */
+ const nsMsgSearchAttribValue Size = 14;
+ const nsMsgSearchAttribValue AnyText = 15;
+ const nsMsgSearchAttribValue Keywords = 16; // keywords are the internal representation of tags.
+
+ const nsMsgSearchAttribValue Name = 17;
+ const nsMsgSearchAttribValue DisplayName = 18;
+ const nsMsgSearchAttribValue Nickname = 19;
+ const nsMsgSearchAttribValue ScreenName = 20;
+ const nsMsgSearchAttribValue Email = 21;
+ const nsMsgSearchAttribValue AdditionalEmail = 22;
+ const nsMsgSearchAttribValue PhoneNumber = 23;
+ const nsMsgSearchAttribValue WorkPhone = 24;
+ const nsMsgSearchAttribValue HomePhone = 25;
+ const nsMsgSearchAttribValue Fax = 26;
+ const nsMsgSearchAttribValue Pager = 27;
+ const nsMsgSearchAttribValue Mobile = 28;
+ const nsMsgSearchAttribValue City = 29;
+ const nsMsgSearchAttribValue Street = 30;
+ const nsMsgSearchAttribValue Title = 31;
+ const nsMsgSearchAttribValue Organization = 32;
+ const nsMsgSearchAttribValue Department = 33;
+
+ // 34 - 43, reserved for ab / LDAP;
+ const nsMsgSearchAttribValue HasAttachmentStatus = 44;
+ const nsMsgSearchAttribValue JunkStatus = 45;
+ const nsMsgSearchAttribValue JunkPercent = 46;
+ const nsMsgSearchAttribValue JunkScoreOrigin = 47;
+ const nsMsgSearchAttribValue HdrProperty = 49; // uses nsIMsgSearchTerm::hdrProperty
+ const nsMsgSearchAttribValue FolderFlag = 50; // uses nsIMsgSearchTerm::status
+ const nsMsgSearchAttribValue Uint32HdrProperty = 51; // uses nsIMsgSearchTerm::hdrProperty
+
+ // 52 is for showing customize - in ui headers start from 53 onwards up until 99.
+
+ /** OtherHeader MUST ALWAYS BE LAST attribute since
+ * we can have an arbitrary # of these. The number can be changed,
+ * however, because we never persist AttribValues as integers.
+ */
+ const nsMsgSearchAttribValue OtherHeader = 52;
+ // must be last attribute
+ const nsMsgSearchAttribValue kNumMsgSearchAttributes = 100;
+};
+
+typedef long nsMsgSearchOpValue;
+
+[scriptable, uuid(9160b196-6fcb-4eba-aaaf-6c806c4ee420)]
+interface nsMsgSearchOp : nsISupports {
+ const nsMsgSearchOpValue Contains = 0; /* for text attributes */
+ const nsMsgSearchOpValue DoesntContain = 1;
+ const nsMsgSearchOpValue Is = 2; /* is and isn't also apply to some non-text attrs */
+ const nsMsgSearchOpValue Isnt = 3;
+ const nsMsgSearchOpValue IsEmpty = 4;
+
+ const nsMsgSearchOpValue IsBefore = 5; /* for date attributes */
+ const nsMsgSearchOpValue IsAfter = 6;
+
+ const nsMsgSearchOpValue IsHigherThan = 7; /* for priority. Is also applies */
+ const nsMsgSearchOpValue IsLowerThan = 8;
+
+ const nsMsgSearchOpValue BeginsWith = 9;
+ const nsMsgSearchOpValue EndsWith = 10;
+
+ const nsMsgSearchOpValue SoundsLike = 11; /* for LDAP phoenetic matching */
+ const nsMsgSearchOpValue LdapDwim = 12; /* Do What I Mean for simple search */
+
+ const nsMsgSearchOpValue IsGreaterThan = 13;
+ const nsMsgSearchOpValue IsLessThan = 14;
+
+ const nsMsgSearchOpValue NameCompletion = 15; /* Name Completion operator...as the name implies =) */
+ const nsMsgSearchOpValue IsInAB = 16;
+ const nsMsgSearchOpValue IsntInAB = 17;
+ const nsMsgSearchOpValue IsntEmpty = 18; /* primarily for tags */
+ const nsMsgSearchOpValue Matches = 19; /* generic term for use by custom terms */
+ const nsMsgSearchOpValue DoesntMatch = 20; /* generic term for use by custom terms */
+ const nsMsgSearchOpValue kNumMsgSearchOperators = 21; /* must be last operator */
+};
+
+typedef long nsMsgSearchWidgetValue;
+
+/* FEs use this to help build the search dialog box */
+[scriptable,uuid(903dd2e8-304e-11d3-92e6-00a0c900d445)]
+interface nsMsgSearchWidget : nsISupports {
+ const nsMsgSearchWidgetValue Text = 0;
+ const nsMsgSearchWidgetValue Date = 1;
+ const nsMsgSearchWidgetValue Menu = 2;
+ const nsMsgSearchWidgetValue Int = 3; /* added to account for age in days which requires an integer field */
+ const nsMsgSearchWidgetValue None = 4;
+};
+
+typedef long nsMsgSearchBooleanOperator;
+
+[scriptable, uuid(a37f3f4a-304e-11d3-8f94-00a0c900d445)]
+interface nsMsgSearchBooleanOp : nsISupports {
+ const nsMsgSearchBooleanOperator BooleanOR = 0;
+ const nsMsgSearchBooleanOperator BooleanAND = 1;
+};
+
+/* Use this to specify the value of a search term */
+
+[ptr] native nsMsgSearchValue(nsMsgSearchValue);
+
+%{C++
+#include "nsString.h"
+typedef struct nsMsgSearchValue
+{
+ nsMsgSearchAttribValue attribute;
+ union
+ {
+ nsMsgPriorityValue priority;
+ PRTime date;
+ uint32_t msgStatus; /* see MSG_FLAG in msgcom.h */
+ uint32_t size;
+ nsMsgKey key;
+ int32_t age; /* in days */
+ nsIMsgFolder *folder;
+ uint32_t junkStatus;
+ uint32_t junkPercent;
+ } u;
+ // We keep two versions of the string to avoid conversion at "search time".
+ nsCString utf8String;
+ nsString utf16String;
+} nsMsgSearchValue;
+%}
+
+[ptr] native nsMsgSearchTerm(nsMsgSearchTerm);
+
+// Please note the ! at the start of this macro, which means the macro
+// needs to enumerate the non-string attributes.
+%{C++
+#define IS_STRING_ATTRIBUTE(_a) \
+(!(_a == nsMsgSearchAttrib::Priority || _a == nsMsgSearchAttrib::Date || \
+ _a == nsMsgSearchAttrib::MsgStatus || _a == nsMsgSearchAttrib::MessageKey || \
+ _a == nsMsgSearchAttrib::Size || _a == nsMsgSearchAttrib::AgeInDays || \
+ _a == nsMsgSearchAttrib::FolderInfo || _a == nsMsgSearchAttrib::Location || \
+ _a == nsMsgSearchAttrib::JunkStatus || \
+ _a == nsMsgSearchAttrib::FolderFlag || _a == nsMsgSearchAttrib::Uint32HdrProperty || \
+ _a == nsMsgSearchAttrib::JunkPercent || _a == nsMsgSearchAttrib::HasAttachmentStatus))
+%}
+
+[ptr] native nsSearchMenuItem(nsSearchMenuItem);
diff --git a/comm/mailnews/search/public/nsMsgSearchScopeTerm.h b/comm/mailnews/search/public/nsMsgSearchScopeTerm.h
new file mode 100644
index 0000000000..b76cc676dc
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgSearchScopeTerm.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsMsgSearchScopeTerm_h
+#define __nsMsgSearchScopeTerm_h
+
+#include "nsMsgSearchCore.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsIMsgSearchSession.h"
+#include "nsCOMPtr.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgSearchScopeTerm : public nsIMsgSearchScopeTerm {
+ public:
+ nsMsgSearchScopeTerm(nsIMsgSearchSession*, nsMsgSearchScopeValue,
+ nsIMsgFolder*);
+ nsMsgSearchScopeTerm();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSEARCHSCOPETERM
+
+ nsresult TimeSlice(bool* aDone);
+ nsresult InitializeAdapter(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList);
+
+ char* GetStatusBarName();
+
+ nsMsgSearchScopeValue m_attribute;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgSearchAdapter> m_adapter;
+ nsCOMPtr<nsIInputStream> m_inputStream; // for message bodies
+ nsWeakPtr m_searchSession;
+ bool m_searchServer;
+
+ private:
+ virtual ~nsMsgSearchScopeTerm();
+};
+
+#endif
diff --git a/comm/mailnews/search/public/nsMsgSearchTerm.h b/comm/mailnews/search/public/nsMsgSearchTerm.h
new file mode 100644
index 0000000000..2ab5618cad
--- /dev/null
+++ b/comm/mailnews/search/public/nsMsgSearchTerm.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef __nsMsgSearchTerm_h
+#define __nsMsgSearchTerm_h
+//---------------------------------------------------------------------------
+// nsMsgSearchTerm specifies one criterion, e.g. name contains phil
+//---------------------------------------------------------------------------
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchScopeTerm.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgSearchCustomTerm.h"
+
+// needed to search for addresses in address books
+#include "nsIAbDirectory.h"
+
+#define EMPTY_MESSAGE_LINE(buf) \
+ (buf[0] == '\r' || buf[0] == '\n' || buf[0] == '\0')
+
+class nsMsgSearchTerm : public nsIMsgSearchTerm {
+ public:
+ nsMsgSearchTerm();
+ nsMsgSearchTerm(nsMsgSearchAttribValue, nsMsgSearchOpValue,
+ nsIMsgSearchValue*, nsMsgSearchBooleanOperator,
+ const char* arbitraryHeader);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSEARCHTERM
+
+ nsresult DeStream(char*, int16_t length);
+ nsresult DeStreamNew(char*, int16_t length);
+
+ nsresult GetLocalTimes(PRTime, PRTime, PRExplodedTime&, PRExplodedTime&);
+
+ bool IsBooleanOpAND() {
+ return m_booleanOp == nsMsgSearchBooleanOp::BooleanAND ? true : false;
+ }
+ nsMsgSearchBooleanOperator GetBooleanOp() { return m_booleanOp; }
+ // maybe should return nsString & ??
+ const char* GetArbitraryHeader() { return m_arbitraryHeader.get(); }
+
+ static char* EscapeQuotesInStr(const char* str);
+
+ nsMsgSearchAttribValue m_attribute;
+ nsMsgSearchOpValue m_operator;
+ nsMsgSearchValue m_value;
+
+ // boolean operator to be applied to this search term and the search term
+ // which precedes it.
+ nsMsgSearchBooleanOperator m_booleanOp;
+
+ // user specified string for the name of the arbitrary header to be used in
+ // the search only has a value when m_attribute = OtherHeader!!!!
+ nsCString m_arbitraryHeader;
+
+ // db hdr property name to use - used when m_attribute = HdrProperty.
+ nsCString m_hdrProperty;
+ bool m_matchAll; // does this term match all headers?
+ nsCString m_customId; // id of custom search term
+
+ protected:
+ virtual ~nsMsgSearchTerm();
+
+ nsresult MatchString(const nsACString& stringToMatch, const char* charset,
+ bool* pResult);
+ nsresult MatchString(const nsAString& stringToMatch, bool* pResult);
+ nsresult OutputValue(nsCString& outputStr);
+ nsresult ParseAttribute(char* inStream, nsMsgSearchAttribValue* attrib);
+ nsresult ParseOperator(char* inStream, nsMsgSearchOpValue* value);
+ nsresult ParseValue(char* inStream);
+ /**
+ * Switch a string to lower case, except for special database rows
+ * that are not headers, but could be headers
+ *
+ * @param aValue the string to switch
+ */
+ void ToLowerCaseExceptSpecials(nsACString& aValue);
+ nsresult InitializeAddressBook();
+ nsresult MatchInAddressBook(const nsAString& aAddress, bool* pResult);
+ // fields used by search in address book
+ nsCOMPtr<nsIAbDirectory> mDirectory;
+
+ bool mBeginsGrouping;
+ bool mEndsGrouping;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/Bogofilter.sfd b/comm/mailnews/search/src/Bogofilter.sfd
new file mode 100644
index 0000000000..a29a818e69
--- /dev/null
+++ b/comm/mailnews/search/src/Bogofilter.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="BogofilterYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"X-Bogosity\",begins with,Spam) OR (\"X-Bogosity\",begins with,Y)"
+name="BogofilterNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-Bogosity\",begins with,Ham) OR (\"X-Bogosity\",begins with,N)"
diff --git a/comm/mailnews/search/src/DSPAM.sfd b/comm/mailnews/search/src/DSPAM.sfd
new file mode 100644
index 0000000000..40bc00df78
--- /dev/null
+++ b/comm/mailnews/search/src/DSPAM.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="DSPAMYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"X-DSPAM-Result\",begins with,Spam)"
+name="DSPAMNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-DSPAM-Result\",begins with,Innocent)"
diff --git a/comm/mailnews/search/src/Habeas.sfd b/comm/mailnews/search/src/Habeas.sfd
new file mode 100644
index 0000000000..ceffff16e5
--- /dev/null
+++ b/comm/mailnews/search/src/Habeas.sfd
@@ -0,0 +1,8 @@
+version="8"
+logging="yes"
+name="HabeasNo"
+enabled="yes"
+type="1"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-Habeas-SWE-3\",is,\"like Habeas SWE (tm)\")"
diff --git a/comm/mailnews/search/src/MsgTraitService.jsm b/comm/mailnews/search/src/MsgTraitService.jsm
new file mode 100644
index 0000000000..c44b015e38
--- /dev/null
+++ b/comm/mailnews/search/src/MsgTraitService.jsm
@@ -0,0 +1,199 @@
+/* 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 EXPORTED_SYMBOLS = ["MsgTraitService"];
+
+// local static variables
+
+var _lastIndex = 0; // the first index will be one
+var _traits = {};
+
+var traitsBranch = Services.prefs.getBranch("mailnews.traits.");
+
+function _registerTrait(aId, aIndex) {
+ var trait = {};
+ trait.enabled = false;
+ trait.name = "";
+ trait.antiId = "";
+ trait.index = aIndex;
+ _traits[aId] = trait;
+}
+
+function MsgTraitService() {}
+
+MsgTraitService.prototype = {
+ // Component setup
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgTraitService"]),
+
+ // nsIMsgTraitService implementation
+
+ get lastIndex() {
+ return _lastIndex;
+ },
+
+ registerTrait(aId) {
+ if (_traits[aId]) {
+ // Meaning already registered.
+ return 0;
+ }
+ _registerTrait(aId, ++_lastIndex);
+ traitsBranch.setBoolPref("enabled." + _lastIndex, false);
+ traitsBranch.setCharPref("id." + _lastIndex, aId);
+ return _lastIndex;
+ },
+
+ unRegisterTrait(aId) {
+ if (_traits[aId]) {
+ var index = _traits[aId].index;
+ _traits[aId] = null;
+ traitsBranch.clearUserPref("id." + index);
+ traitsBranch.clearUserPref("enabled." + index);
+ traitsBranch.clearUserPref("antiId." + index);
+ traitsBranch.clearUserPref("name." + index);
+ }
+ },
+
+ isRegistered(aId) {
+ return !!_traits[aId];
+ },
+
+ setName(aId, aName) {
+ traitsBranch.setCharPref("name." + _traits[aId].index, aName);
+ _traits[aId].name = aName;
+ },
+
+ getName(aId) {
+ return _traits[aId].name;
+ },
+
+ getIndex(aId) {
+ return _traits[aId].index;
+ },
+
+ getId(aIndex) {
+ for (let id in _traits) {
+ if (_traits[id].index == aIndex) {
+ return id;
+ }
+ }
+ return null;
+ },
+
+ setEnabled(aId, aEnabled) {
+ traitsBranch.setBoolPref("enabled." + _traits[aId].index, aEnabled);
+ _traits[aId].enabled = aEnabled;
+ },
+
+ getEnabled(aId) {
+ return _traits[aId].enabled;
+ },
+
+ setAntiId(aId, aAntiId) {
+ traitsBranch.setCharPref("antiId." + _traits[aId].index, aAntiId);
+ _traits[aId].antiId = aAntiId;
+ },
+
+ getAntiId(aId) {
+ return _traits[aId].antiId;
+ },
+
+ getEnabledProIndices() {
+ let proIndices = [];
+ for (let id in _traits) {
+ if (_traits[id].enabled) {
+ proIndices.push(_traits[id].index);
+ }
+ }
+ return proIndices;
+ },
+
+ getEnabledAntiIndices() {
+ let antiIndices = [];
+ for (let id in _traits) {
+ if (_traits[id].enabled) {
+ antiIndices.push(_traits[_traits[id].antiId].index);
+ }
+ }
+ return antiIndices;
+ },
+
+ addAlias(aTraitIndex, aTraitAliasIndex) {
+ let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, "");
+ let aliases;
+ if (aliasesString.length) {
+ aliases = aliasesString.split(",");
+ } else {
+ aliases = [];
+ }
+ if (!aliases.includes(aTraitAliasIndex.toString())) {
+ aliases.push(aTraitAliasIndex);
+ traitsBranch.setCharPref("aliases." + aTraitIndex, aliases.join());
+ }
+ },
+
+ removeAlias(aTraitIndex, aTraitAliasIndex) {
+ let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, "");
+ let aliases;
+ if (aliasesString.length) {
+ aliases = aliasesString.split(",");
+ } else {
+ aliases = [];
+ }
+ let location = aliases.indexOf(aTraitAliasIndex.toString());
+ if (location != -1) {
+ aliases.splice(location, 1);
+ traitsBranch.setCharPref("aliases." + aTraitIndex, aliases.join());
+ }
+ },
+
+ getAliases(aTraitIndex) {
+ let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, "");
+ let aliases;
+ if (aliasesString.length) {
+ aliases = aliasesString.split(",");
+ } else {
+ aliases = [];
+ }
+ return aliases;
+ },
+};
+
+// initialization
+
+_init();
+
+function _init() {
+ // get existing traits
+ var idBranch = Services.prefs.getBranch("mailnews.traits.id.");
+ var nameBranch = Services.prefs.getBranch("mailnews.traits.name.");
+ var enabledBranch = Services.prefs.getBranch("mailnews.traits.enabled.");
+ var antiIdBranch = Services.prefs.getBranch("mailnews.traits.antiId.");
+ _lastIndex = Services.prefs
+ .getBranch("mailnews.traits.")
+ .getIntPref("lastIndex");
+ var ids = idBranch.getChildList("");
+ for (let i = 0; i < ids.length; i++) {
+ var id = idBranch.getCharPref(ids[i]);
+ var index = parseInt(ids[i]);
+ _registerTrait(id, index, false);
+
+ if (nameBranch.getPrefType(ids[i]) == Services.prefs.PREF_STRING) {
+ _traits[id].name = nameBranch.getCharPref(ids[i]);
+ }
+ if (enabledBranch.getPrefType(ids[i]) == Services.prefs.PREF_BOOL) {
+ _traits[id].enabled = enabledBranch.getBoolPref(ids[i]);
+ }
+ if (antiIdBranch.getPrefType(ids[i]) == Services.prefs.PREF_STRING) {
+ _traits[id].antiId = antiIdBranch.getCharPref(ids[i]);
+ }
+
+ if (_lastIndex < index) {
+ _lastIndex = index;
+ }
+ }
+
+ // for (traitId in _traits)
+ // dump("\nindex of " + traitId + " is " + _traits[traitId].index);
+ // dump("\n");
+}
diff --git a/comm/mailnews/search/src/POPFile.sfd b/comm/mailnews/search/src/POPFile.sfd
new file mode 100644
index 0000000000..a791705aa4
--- /dev/null
+++ b/comm/mailnews/search/src/POPFile.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="POPFileYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"X-Text-Classification\",begins with,spam)"
+name="POPFileNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-Text-Classification\",begins with,inbox) OR (\"X-Text-Classification\",begins with,allowed)"
diff --git a/comm/mailnews/search/src/PeriodicFilterManager.jsm b/comm/mailnews/search/src/PeriodicFilterManager.jsm
new file mode 100644
index 0000000000..3d6f52c504
--- /dev/null
+++ b/comm/mailnews/search/src/PeriodicFilterManager.jsm
@@ -0,0 +1,202 @@
+/* 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/. */
+
+/*
+ * Execute periodic filters at the correct rate.
+ *
+ * The only external call required for this is setupFiltering(). This should be
+ * called before the mail-startup-done notification.
+ */
+
+const EXPORTED_SYMBOLS = ["PeriodicFilterManager"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const log = console.createInstance({
+ prefix: "mail.periodicfilters",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.periodicfilters.loglevel",
+});
+
+var PeriodicFilterManager = {
+ _timer: null,
+ _checkRateMilliseconds: 60000, // How often do we check if servers are ready to run?
+ _defaultFilterRateMinutes: Services.prefs
+ .getDefaultBranch("")
+ .getIntPref("mail.server.default.periodicFilterRateMinutes"),
+ _initialized: false, // Has this been initialized?
+ _running: false, // Are we executing filters already?
+
+ // Initial call to begin startup.
+ setupFiltering() {
+ if (this._initialized) {
+ return;
+ }
+
+ this._initialized = true;
+ Services.obs.addObserver(this, "mail-startup-done");
+ },
+
+ // Main call to start the periodic filter process
+ init() {
+ log.info("PeriodicFilterManager init()");
+ // set the next filter time
+ for (let server of MailServices.accounts.allServers) {
+ let nowTime = parseInt(Date.now() / 60000);
+ // Make sure that the last filter time of all servers was in the past.
+ let lastFilterTime = server.getIntValue("lastFilterTime");
+ // Schedule next filter run.
+ let nextFilterTime =
+ lastFilterTime < nowTime
+ ? lastFilterTime + this.getServerPeriod(server)
+ : nowTime;
+ server.setIntValue("nextFilterTime", nextFilterTime);
+ }
+
+ // kickoff the timer to run periodic filters
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(
+ this,
+ this._checkRateMilliseconds,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ Services.obs.addObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * Periodic callback to check if any periodic filters need to be run.
+ *
+ * The periodic filter manager does not guarantee that filters will be run
+ * precisely at the specified interval.
+ * The server may be busy (e.g. downloading messages) or another filter run
+ * is still ongoing, in which cases running periodic filter of any server
+ * may be postponed.
+ */
+ notify(timer) {
+ log.debug("PeriodicFilterManager timer callback");
+ if (this._running) {
+ log.debug("PeriodicFilterManager Previous filter run still executing");
+ return;
+ }
+ this._running = true;
+ let nowTime = parseInt(Date.now() / 60000);
+ for (let server of MailServices.accounts.allServers) {
+ if (!server.canHaveFilters) {
+ continue;
+ }
+ if (server.getIntValue("nextFilterTime") > nowTime) {
+ continue;
+ }
+ if (server.serverBusy) {
+ continue;
+ }
+
+ // Schedule next time this account's filters should be run.
+ server.setIntValue(
+ "nextFilterTime",
+ nowTime + this.getServerPeriod(server)
+ );
+ server.setIntValue("lastFilterTime", nowTime);
+
+ // Build a temporary list of periodic filters.
+ // XXX TODO: make applyFiltersToFolders() take a filterType instead (bug 1551043).
+ let curFilterList = server.getFilterList(null);
+ let tempFilterList = MailServices.filters.getTempFilterList(
+ server.rootFolder
+ );
+ let numFilters = curFilterList.filterCount;
+ tempFilterList.loggingEnabled = curFilterList.loggingEnabled;
+ tempFilterList.logStream = curFilterList.logStream;
+ let newFilterIndex = 0;
+ for (let i = 0; i < numFilters; i++) {
+ let curFilter = curFilterList.getFilterAt(i);
+ // Only add enabled, UI visible filters that are of the Periodic type.
+ if (
+ curFilter.enabled &&
+ !curFilter.temporary &&
+ curFilter.filterType & Ci.nsMsgFilterType.Periodic
+ ) {
+ tempFilterList.insertFilterAt(newFilterIndex, curFilter);
+ newFilterIndex++;
+ }
+ }
+ if (newFilterIndex == 0) {
+ continue;
+ }
+ let foldersToFilter = server.rootFolder.getFoldersWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ if (foldersToFilter.length == 0) {
+ continue;
+ }
+
+ log.debug(
+ "PeriodicFilterManager apply periodic filters to server " +
+ server.prettyName
+ );
+ MailServices.filters.applyFiltersToFolders(
+ tempFilterList,
+ foldersToFilter,
+ null
+ );
+ }
+ this._running = false;
+ },
+
+ /**
+ * Gets the periodic filter interval for the given server.
+ * If the server's interval is not sane, clean it up.
+ *
+ * @param {nsIMsgIncomingServer} server - The server to return interval for.
+ */
+ getServerPeriod(server) {
+ const minimumPeriodMinutes = 1;
+ let serverRateMinutes = server.getIntValue("periodicFilterRateMinutes");
+ // Check if period is too short.
+ if (serverRateMinutes < minimumPeriodMinutes) {
+ // If the server.default pref is too low, clear that one first.
+ if (
+ Services.prefs.getIntPref(
+ "mail.server.default.periodicFilterRateMinutes"
+ ) == serverRateMinutes
+ ) {
+ Services.prefs.clearUserPref(
+ "mail.server.default.periodicFilterRateMinutes"
+ );
+ }
+ // If the server still has its own specific value and it is still too low, sanitize it.
+ if (
+ server.getIntValue("periodicFilterRateMinutes") < minimumPeriodMinutes
+ ) {
+ server.setIntValue(
+ "periodicFilterRateMinutes",
+ this._defaultFilterRateMinutes
+ );
+ }
+
+ return this._defaultFilterRateMinutes;
+ }
+
+ return serverRateMinutes;
+ },
+
+ observe(subject, topic, data) {
+ Services.obs.removeObserver(this, topic);
+ if (topic == "mail-startup-done") {
+ this.init();
+ } else if (topic == "quit-application-granted") {
+ this.shutdown();
+ }
+ },
+
+ shutdown() {
+ log.info("PeriodicFilterManager shutdown");
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ },
+};
diff --git a/comm/mailnews/search/src/SpamAssassin.sfd b/comm/mailnews/search/src/SpamAssassin.sfd
new file mode 100644
index 0000000000..d8d0ecdb10
--- /dev/null
+++ b/comm/mailnews/search/src/SpamAssassin.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="SpamAssassinYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"X-Spam-Status\",begins with,Yes) OR (\"X-Spam-Flag\",begins with,YES) OR (subject,begins with,***SPAM***)"
+name="SpamAssassinNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-Spam-Status\",begins with,No)"
diff --git a/comm/mailnews/search/src/SpamCatcher.sfd b/comm/mailnews/search/src/SpamCatcher.sfd
new file mode 100644
index 0000000000..d16cd80a28
--- /dev/null
+++ b/comm/mailnews/search/src/SpamCatcher.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="SpamCatcherNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"x-SpamCatcher\",begins with,No)"
+name="SpamCatcherYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"x-SpamCatcher\",begins with,Yes)"
diff --git a/comm/mailnews/search/src/SpamPal.sfd b/comm/mailnews/search/src/SpamPal.sfd
new file mode 100644
index 0000000000..830b1937b5
--- /dev/null
+++ b/comm/mailnews/search/src/SpamPal.sfd
@@ -0,0 +1,14 @@
+version="9"
+logging="yes"
+name="SpamPalNo"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="0"
+condition="OR (\"X-SpamPal\",begins with,PASS)"
+name="SpamPalYes"
+enabled="yes"
+type="17"
+action="JunkScore"
+actionValue="100"
+condition="OR (\"X-SpamPal\",begins with,SPAM)"
diff --git a/comm/mailnews/search/src/components.conf b/comm/mailnews/search/src/components.conf
new file mode 100644
index 0000000000..943b3fb542
--- /dev/null
+++ b/comm/mailnews/search/src/components.conf
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ "cid": "{a2e95f4f-da72-4a41-9493-661ad353c00a}",
+ "contract_ids": ["@mozilla.org/msg-trait-service;1"],
+ "jsm": "resource:///modules/MsgTraitService.jsm",
+ "constructor": "MsgTraitService",
+ },
+ {
+ "cid": "{5cbb0700-04bc-11d3-a50a-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/services/filters;1"],
+ "type": "nsMsgFilterService",
+ "headers": ["/comm/mailnews/search/src/nsMsgFilterService.h"],
+ "name": "Filter",
+ "interfaces": ["nsIMsgFilterService"],
+ },
+ {
+ "cid": "{e9a7cd70-0303-11d3-a50a-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/searchSession;1"],
+ "type": "nsMsgSearchSession",
+ "headers": ["/comm/mailnews/search/src/nsMsgSearchSession.h"],
+ },
+ {
+ "cid": "{e1da397d-fdc5-4b23-a6fe-d46a034d80b3}",
+ "contract_ids": ["@mozilla.org/messenger/searchTerm;1"],
+ "type": "nsMsgSearchTerm",
+ "headers": ["/comm/mailnews/search/public/nsMsgSearchTerm.h"],
+ },
+ {
+ "cid": "{1510faee-ad1a-4194-8039-33de32d5a882}",
+ "contract_ids": ["@mozilla.org/mail/search/validityManager;1"],
+ "type": "nsMsgSearchValidityManager",
+ "headers": ["/comm/mailnews/search/public/nsMsgSearchAdapter.h"],
+ },
+]
diff --git a/comm/mailnews/search/src/moz.build b/comm/mailnews/search/src/moz.build
new file mode 100644
index 0000000000..b8f879aa15
--- /dev/null
+++ b/comm/mailnews/search/src/moz.build
@@ -0,0 +1,37 @@
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "nsMsgBodyHandler.cpp",
+ "nsMsgFilter.cpp",
+ "nsMsgFilterList.cpp",
+ "nsMsgFilterService.cpp",
+ "nsMsgImapSearch.cpp",
+ "nsMsgLocalSearch.cpp",
+ "nsMsgSearchAdapter.cpp",
+ "nsMsgSearchNews.cpp",
+ "nsMsgSearchSession.cpp",
+ "nsMsgSearchTerm.cpp",
+ "nsMsgSearchValue.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "MsgTraitService.jsm",
+ "PeriodicFilterManager.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
+
+FINAL_TARGET_FILES.isp += [
+ "Bogofilter.sfd",
+ "DSPAM.sfd",
+ "POPFile.sfd",
+ "SpamAssassin.sfd",
+ "SpamPal.sfd",
+]
diff --git a/comm/mailnews/search/src/nsMsgBodyHandler.cpp b/comm/mailnews/search/src/nsMsgBodyHandler.cpp
new file mode 100644
index 0000000000..5b77750f63
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgBodyHandler.cpp
@@ -0,0 +1,464 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgSearchCore.h"
+#include "nsMsgUtils.h"
+#include "nsMsgBodyHandler.h"
+#include "nsMsgSearchTerm.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgMessageFlags.h"
+#include "nsISeekableStream.h"
+#include "nsIInputStream.h"
+#include "nsIFile.h"
+#include "plbase64.h"
+#include "prmem.h"
+#include "nsMimeTypes.h"
+
+nsMsgBodyHandler::nsMsgBodyHandler(nsIMsgSearchScopeTerm* scope,
+ uint32_t numLines, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db) {
+ m_scope = scope;
+ m_numLocalLines = numLines;
+ uint32_t flags;
+ m_lineCountInBodyLines = NS_SUCCEEDED(msg->GetFlags(&flags))
+ ? !(flags & nsMsgMessageFlags::Offline)
+ : true;
+ // account for added x-mozilla-status lines, and envelope line.
+ if (!m_lineCountInBodyLines) m_numLocalLines += 3;
+ m_msgHdr = msg;
+ m_db = db;
+
+ // the following are variables used when the body handler is handling stuff
+ // from filters....through this constructor, that is not the case so we set
+ // them to NULL.
+ m_headers = nullptr;
+ m_headersSize = 0;
+ m_Filtering = false; // make sure we set this before we call initialize...
+
+ Initialize(); // common initialization stuff
+ OpenLocalFolder();
+}
+
+nsMsgBodyHandler::nsMsgBodyHandler(nsIMsgSearchScopeTerm* scope,
+ uint32_t numLines, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db, const char* headers,
+ uint32_t headersSize, bool Filtering) {
+ m_scope = scope;
+ m_numLocalLines = numLines;
+ uint32_t flags;
+ m_lineCountInBodyLines = NS_SUCCEEDED(msg->GetFlags(&flags))
+ ? !(flags & nsMsgMessageFlags::Offline)
+ : true;
+ // account for added x-mozilla-status lines, and envelope line.
+ if (!m_lineCountInBodyLines) m_numLocalLines += 3;
+ m_msgHdr = msg;
+ m_db = db;
+ m_headers = nullptr;
+ m_headersSize = 0;
+ m_Filtering = Filtering;
+
+ Initialize();
+
+ if (m_Filtering) {
+ m_headers = headers;
+ m_headersSize = headersSize;
+ } else {
+ OpenLocalFolder();
+ }
+}
+
+void nsMsgBodyHandler::Initialize()
+// common initialization code regardless of what body type we are handling...
+{
+ // Default transformations for local message search and MAPI access
+ m_stripHeaders = true;
+ m_partIsHtml = false;
+ m_base64part = false;
+ m_partIsQP = false;
+ m_isMultipart = false;
+ m_partIsText = true; // Default is text/plain, maybe proven otherwise later.
+ m_pastMsgHeaders = false;
+ m_pastPartHeaders = false;
+ m_inMessageAttachment = false;
+ m_headerBytesRead = 0;
+}
+
+nsMsgBodyHandler::~nsMsgBodyHandler() {}
+
+int32_t nsMsgBodyHandler::GetNextLine(nsCString& buf, nsCString& charset) {
+ int32_t length = -1; // length of incoming line or -1 eof
+ int32_t outLength = -1; // length of outgoing line or -1 eof
+ bool eatThisLine = true;
+ nsAutoCString nextLine;
+
+ while (eatThisLine) {
+ // first, handle the filtering case...this is easy....
+ if (m_Filtering) {
+ length = GetNextFilterLine(nextLine);
+ } else {
+ // 3 cases: Offline IMAP, POP, or we are dealing with a news message....
+ // Offline cases should be same as local mail cases, since we're going
+ // to store offline messages in berkeley format folders.
+ if (m_db) {
+ length = GetNextLocalLine(nextLine); // (2) POP
+ }
+ }
+
+ if (length < 0) break; // eof in
+
+ outLength = ApplyTransformations(nextLine, length, eatThisLine, buf);
+ }
+
+ if (outLength < 0) return -1; // eof out
+
+ // For non-multipart messages, the entire message minus headers is encoded
+ // ApplyTransformations can only decode a part
+ if (!m_isMultipart && m_base64part) {
+ Base64Decode(buf);
+ m_base64part = false;
+ // And reapply our transformations...
+ outLength = ApplyTransformations(buf, buf.Length(), eatThisLine, buf);
+ }
+
+ // Process aggregated HTML.
+ if (!m_isMultipart && m_partIsHtml) {
+ StripHtml(buf);
+ outLength = buf.Length();
+ }
+
+ charset = m_partCharset;
+ return outLength;
+}
+
+void nsMsgBodyHandler::OpenLocalFolder() {
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = m_scope->GetInputStream(m_msgHdr, getter_AddRefs(inputStream));
+ // Warn and return if GetInputStream fails
+ NS_ENSURE_SUCCESS_VOID(rv);
+ m_fileLineStream = do_QueryInterface(inputStream);
+}
+
+int32_t nsMsgBodyHandler::GetNextFilterLine(nsCString& buf) {
+ // m_nextHdr always points to the next header in the list....the list is NULL
+ // terminated...
+ uint32_t numBytesCopied = 0;
+ if (m_headersSize > 0) {
+ // #mscott. Ugly hack! filter headers list have CRs & LFs inside the NULL
+ // delimited list of header strings. It is possible to have: To NULL CR LF
+ // From. We want to skip over these CR/LFs if they start at the beginning of
+ // what we think is another header.
+
+ while (m_headersSize > 0 && (m_headers[0] == '\r' || m_headers[0] == '\n' ||
+ m_headers[0] == ' ' || m_headers[0] == '\0')) {
+ m_headers++; // skip over these chars...
+ m_headersSize--;
+ }
+
+ if (m_headersSize > 0) {
+ numBytesCopied = strlen(m_headers) + 1;
+ buf.Assign(m_headers);
+ m_headers += numBytesCopied;
+ // be careful...m_headersSize is unsigned. Don't let it go negative or we
+ // overflow to 2^32....*yikes*
+ if (m_headersSize < numBytesCopied)
+ m_headersSize = 0;
+ else
+ m_headersSize -= numBytesCopied; // update # bytes we have read from
+ // the headers list
+
+ return (int32_t)numBytesCopied;
+ }
+ } else if (m_headersSize == 0) {
+ buf.Truncate();
+ }
+ return -1;
+}
+
+// return -1 if no more local lines, length of next line otherwise.
+
+int32_t nsMsgBodyHandler::GetNextLocalLine(nsCString& buf)
+// returns number of bytes copied
+{
+ if (m_numLocalLines) {
+ // I the line count is in body lines, only decrement once we have
+ // processed all the headers. Otherwise the line is not in body
+ // lines and we want to decrement for every line.
+ if (m_pastMsgHeaders || !m_lineCountInBodyLines) m_numLocalLines--;
+ // do we need to check the return value here?
+ if (m_fileLineStream) {
+ bool more = false;
+ nsresult rv = m_fileLineStream->ReadLine(buf, &more);
+ if (NS_SUCCEEDED(rv)) return buf.Length();
+ }
+ }
+
+ return -1;
+}
+
+/**
+ * This method applies a sequence of transformations to the line.
+ *
+ * It applies the following sequences in order
+ * * Removes headers if the searcher doesn't want them
+ * (sets m_past*Headers)
+ * * Determines the current MIME type.
+ * (via SniffPossibleMIMEHeader)
+ * * Strips any HTML if the searcher doesn't want it
+ * * Strips non-text parts
+ * * Decodes any base64 part
+ * (resetting part variables: m_base64part, m_pastPartHeaders, m_partIsHtml,
+ * m_partIsText)
+ *
+ * @param line (in) the current line
+ * @param length (in) the length of said line
+ * @param eatThisLine (out) whether or not to ignore this line
+ * @param buf (inout) if m_base64part, the current part as needed for
+ * decoding; else, it is treated as an out param (a
+ * redundant version of line).
+ * @return the length of the line after applying transformations
+ */
+int32_t nsMsgBodyHandler::ApplyTransformations(const nsCString& line,
+ int32_t length,
+ bool& eatThisLine,
+ nsCString& buf) {
+ eatThisLine = false;
+
+ if (!m_pastPartHeaders) // line is a line from the part headers
+ {
+ if (m_stripHeaders) eatThisLine = true;
+
+ // We have already grabbed all worthwhile information from the headers,
+ // so there is no need to keep track of the current lines
+ buf.Assign(line);
+
+ SniffPossibleMIMEHeader(buf);
+
+ if (buf.IsEmpty() || buf.First() == '\r' || buf.First() == '\n') {
+ if (!m_inMessageAttachment) {
+ m_pastPartHeaders = true;
+ } else {
+ // We're in a message attachment and have just read past the
+ // part header for the attached message. We now need to read
+ // the message headers and any part headers.
+ // We can now forget about the special handling of attached messages.
+ m_inMessageAttachment = false;
+ }
+ }
+
+ // We set m_pastMsgHeaders to 'true' only once.
+ if (m_pastPartHeaders) m_pastMsgHeaders = true;
+
+ return length;
+ }
+
+ // Check to see if this is one of our boundary strings.
+ bool matchedBoundary = false;
+ if (m_isMultipart && m_boundaries.Length() > 0) {
+ for (int32_t i = (int32_t)m_boundaries.Length() - 1; i >= 0; i--) {
+ if (StringBeginsWith(line, m_boundaries[i])) {
+ matchedBoundary = true;
+ // If we matched a boundary, we won't need the nested/later ones any
+ // more.
+ m_boundaries.SetLength(i + 1);
+ break;
+ }
+ }
+ }
+ if (matchedBoundary) {
+ if (m_base64part && m_partIsText) {
+ Base64Decode(buf);
+ // Work on the parsed string
+ if (!buf.Length()) {
+ NS_WARNING("Trying to transform an empty buffer");
+ eatThisLine = true;
+ } else {
+ // It is wrong to call ApplyTransformations() here since this will
+ // lead to the buffer being doubled-up at |buf.Append(line);|
+ // below. ApplyTransformations(buf, buf.Length(), eatThisLine, buf);
+ // Avoid spurious failures
+ eatThisLine = false;
+ }
+ } else if (!m_partIsHtml) {
+ buf.Truncate();
+ eatThisLine = true; // We have no content...
+ }
+
+ if (m_partIsHtml) {
+ StripHtml(buf);
+ }
+
+ // Reset all assumed headers
+ m_base64part = false;
+ // Get ready to sniff new part headers, but do not reset m_pastMsgHeaders
+ // since it will screw the body line count.
+ m_pastPartHeaders = false;
+ m_partIsHtml = false;
+ // If we ever see a multipart message, each part needs to set
+ // 'm_partIsText', so no more defaulting to 'true' when the part is done.
+ m_partIsText = false;
+
+ // Note: we cannot reset 'm_partIsQP' yet since we still need it to process
+ // the last buffer returned here. Parsing the next part will set a new
+ // value.
+ return buf.Length();
+ }
+
+ if (!m_partIsText) {
+ // Ignore non-text parts
+ buf.Truncate();
+ eatThisLine = true;
+ return 0;
+ }
+
+ // Accumulate base64 parts and HTML parts for later decoding or tag stripping.
+ if (m_base64part || m_partIsHtml) {
+ if (m_partIsHtml && !m_base64part) {
+ size_t bufLength = buf.Length();
+ if (!m_partIsQP || bufLength == 0 || !StringEndsWith(buf, "="_ns)) {
+ // Replace newline in HTML with a space.
+ buf.Append(' ');
+ } else {
+ // Strip the soft line break.
+ buf.SetLength(bufLength - 1);
+ }
+ }
+ buf.Append(line);
+ eatThisLine = true;
+ return buf.Length();
+ }
+
+ buf.Assign(line);
+ return buf.Length();
+}
+
+void nsMsgBodyHandler::StripHtml(nsCString& pBufInOut) {
+ char* pBuf = (char*)PR_Malloc(pBufInOut.Length() + 1);
+ if (pBuf) {
+ char* pWalk = pBuf;
+
+ char* pWalkInOut = (char*)pBufInOut.get();
+ bool inTag = false;
+ while (*pWalkInOut) // throw away everything inside < >
+ {
+ if (!inTag) {
+ if (*pWalkInOut == '<')
+ inTag = true;
+ else
+ *pWalk++ = *pWalkInOut;
+ } else {
+ if (*pWalkInOut == '>') inTag = false;
+ }
+ pWalkInOut++;
+ }
+ *pWalk = 0; // null terminator
+
+ pBufInOut.Adopt(pBuf);
+ }
+}
+
+/**
+ * Determines the MIME type, if present, from the current line.
+ *
+ * m_partIsHtml, m_isMultipart, m_partIsText, m_base64part, and boundary are
+ * all set by this method at various points in time.
+ *
+ * @param line (in) a header line that may contain a MIME header
+ */
+void nsMsgBodyHandler::SniffPossibleMIMEHeader(const nsCString& line) {
+ // Some parts of MIME are case-sensitive and other parts are case-insensitive;
+ // specifically, the headers are all case-insensitive and the values we care
+ // about are also case-insensitive, with the sole exception of the boundary
+ // string, so we can't just take the input line and make it lower case.
+ nsCString lowerCaseLine(line);
+ ToLowerCase(lowerCaseLine);
+
+ if (StringBeginsWith(lowerCaseLine, "content-transfer-encoding:"_ns))
+ m_partIsQP = lowerCaseLine.Find("quoted-printable") != kNotFound;
+
+ if (StringBeginsWith(lowerCaseLine, "content-type:"_ns)) {
+ if (lowerCaseLine.LowerCaseFindASCII("text/html") != kNotFound) {
+ m_partIsText = true;
+ m_partIsHtml = true;
+ } else if (lowerCaseLine.Find("multipart/") != kNotFound) {
+ if (m_isMultipart) {
+ // Nested multipart, get ready for new headers.
+ m_base64part = false;
+ m_partIsQP = false;
+ m_pastPartHeaders = false;
+ m_partIsHtml = false;
+ m_partIsText = false;
+ }
+ m_isMultipart = true;
+ m_partCharset.Truncate();
+ } else if (lowerCaseLine.Find("message/") != kNotFound) {
+ // Initialise again.
+ m_base64part = false;
+ m_partIsQP = false;
+ m_pastPartHeaders = false;
+ m_partIsHtml = false;
+ m_partIsText =
+ true; // Default is text/plain, maybe proven otherwise later.
+ m_inMessageAttachment = true;
+ } else if (lowerCaseLine.Find("text/") != kNotFound)
+ m_partIsText = true;
+ else if (lowerCaseLine.Find("text/") == kNotFound)
+ m_partIsText = false; // We have disproven our assumption.
+ }
+
+ int32_t start;
+ if (m_isMultipart && (start = lowerCaseLine.Find("boundary=")) != kNotFound) {
+ start += 9; // strlen("boundary=")
+ if (line[start] == '\"') start++;
+ int32_t end = line.RFindChar('\"');
+ if (end == -1) end = line.Length();
+
+ // Collect all boundaries. Since we only react to crossing a boundary,
+ // we can simply collect the boundaries instead of forming a tree
+ // structure from the message. Keep it simple ;-)
+ nsCString boundary;
+ boundary.AssignLiteral("--");
+ boundary.Append(Substring(line, start, end - start));
+ if (!m_boundaries.Contains(boundary)) m_boundaries.AppendElement(boundary);
+ }
+
+ if (m_isMultipart && (start = lowerCaseLine.Find("charset=")) != kNotFound) {
+ start += 8; // strlen("charset=")
+ bool foundQuote = false;
+ if (line[start] == '\"') {
+ start++;
+ foundQuote = true;
+ }
+ int32_t end = line.FindChar(foundQuote ? '\"' : ';', start);
+ if (end == -1) end = line.Length();
+
+ m_partCharset.Assign(Substring(line, start, end - start));
+ }
+
+ if (StringBeginsWith(lowerCaseLine, "content-transfer-encoding:"_ns) &&
+ lowerCaseLine.LowerCaseFindASCII(ENCODING_BASE64) != kNotFound)
+ m_base64part = true;
+}
+
+/**
+ * Decodes the given base64 string.
+ *
+ * It returns its decoded string in its input.
+ *
+ * @param pBufInOut (inout) a buffer of the string
+ */
+void nsMsgBodyHandler::Base64Decode(nsCString& pBufInOut) {
+ char* decodedBody =
+ PL_Base64Decode(pBufInOut.get(), pBufInOut.Length(), nullptr);
+ if (decodedBody) {
+ // Replace CR LF with spaces.
+ char* q = decodedBody;
+ while (*q) {
+ if (*q == '\n' || *q == '\r') *q = ' ';
+ q++;
+ }
+ pBufInOut.Adopt(decodedBody);
+ }
+}
diff --git a/comm/mailnews/search/src/nsMsgFilter.cpp b/comm/mailnews/search/src/nsMsgFilter.cpp
new file mode 100644
index 0000000000..273b74aa07
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilter.cpp
@@ -0,0 +1,864 @@
+/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// this file implements the nsMsgFilter interface
+
+#include "msgCore.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgFilterList.h" // for kFileVersion
+#include "nsMsgFilter.h"
+#include "nsMsgUtils.h"
+#include "nsMsgLocalSearch.h"
+#include "nsMsgSearchTerm.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsMsgSearchValue.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIOutputStream.h"
+#include "nsIStringBundle.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgNewsFolder.h"
+#include "prmem.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+static const char* kImapPrefix = "//imap:";
+static const char* kWhitespace = "\b\t\r\n ";
+
+nsMsgRuleAction::nsMsgRuleAction()
+ : m_type(nsMsgFilterAction::None),
+ m_priority(nsMsgPriority::notSet),
+ m_junkScore(0) {}
+
+nsMsgRuleAction::~nsMsgRuleAction() {}
+
+NS_IMPL_ISUPPORTS(nsMsgRuleAction, nsIMsgRuleAction)
+
+NS_IMPL_GETSET(nsMsgRuleAction, Type, nsMsgRuleActionType, m_type)
+
+NS_IMETHODIMP nsMsgRuleAction::SetPriority(nsMsgPriorityValue aPriority) {
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::ChangePriority,
+ NS_ERROR_ILLEGAL_VALUE);
+ m_priority = aPriority;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::GetPriority(nsMsgPriorityValue* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::ChangePriority,
+ NS_ERROR_ILLEGAL_VALUE);
+ *aResult = m_priority;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::SetTargetFolderUri(const nsACString& aUri) {
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::MoveToFolder ||
+ m_type == nsMsgFilterAction::CopyToFolder,
+ NS_ERROR_ILLEGAL_VALUE);
+ m_folderUri = aUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::GetTargetFolderUri(nsACString& aResult) {
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::MoveToFolder ||
+ m_type == nsMsgFilterAction::CopyToFolder,
+ NS_ERROR_ILLEGAL_VALUE);
+ aResult = m_folderUri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::SetJunkScore(int32_t aJunkScore) {
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::JunkScore && aJunkScore >= 0 &&
+ aJunkScore <= 100,
+ NS_ERROR_ILLEGAL_VALUE);
+ m_junkScore = aJunkScore;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::GetJunkScore(int32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_TRUE(m_type == nsMsgFilterAction::JunkScore,
+ NS_ERROR_ILLEGAL_VALUE);
+ *aResult = m_junkScore;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::SetStrValue(const nsACString& aStrValue) {
+ m_strValue = aStrValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgRuleAction::GetStrValue(nsACString& aStrValue) {
+ aStrValue = m_strValue;
+ return NS_OK;
+}
+
+/* attribute ACString customId; */
+NS_IMETHODIMP nsMsgRuleAction::GetCustomId(nsACString& aCustomId) {
+ aCustomId = m_customId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgRuleAction::SetCustomId(const nsACString& aCustomId) {
+ m_customId = aCustomId;
+ return NS_OK;
+}
+
+// this can only be called after the customId is set
+NS_IMETHODIMP nsMsgRuleAction::GetCustomAction(
+ nsIMsgFilterCustomAction** aCustomAction) {
+ NS_ENSURE_ARG_POINTER(aCustomAction);
+ if (!m_customAction) {
+ if (m_customId.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterService->GetCustomAction(m_customId,
+ getter_AddRefs(m_customAction));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // found the correct custom action
+ NS_ADDREF(*aCustomAction = m_customAction);
+ return NS_OK;
+}
+
+nsMsgFilter::nsMsgFilter()
+ : m_type(nsMsgFilterType::InboxRule | nsMsgFilterType::Manual),
+ m_enabled(false),
+ m_temporary(false),
+ m_unparseable(false),
+ m_filterList(nullptr),
+ m_expressionTree(nullptr) {}
+
+nsMsgFilter::~nsMsgFilter() { delete m_expressionTree; }
+
+NS_IMPL_ISUPPORTS(nsMsgFilter, nsIMsgFilter)
+
+NS_IMPL_GETSET(nsMsgFilter, FilterType, nsMsgFilterTypeType, m_type)
+NS_IMPL_GETSET(nsMsgFilter, Enabled, bool, m_enabled)
+NS_IMPL_GETSET(nsMsgFilter, Temporary, bool, m_temporary)
+NS_IMPL_GETSET(nsMsgFilter, Unparseable, bool, m_unparseable)
+
+NS_IMETHODIMP nsMsgFilter::GetFilterName(nsAString& name) {
+ name = m_filterName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::SetFilterName(const nsAString& name) {
+ m_filterName.Assign(name);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::GetFilterDesc(nsACString& description) {
+ description = m_description;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::SetFilterDesc(const nsACString& description) {
+ m_description.Assign(description);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::GetUnparsedBuffer(nsACString& unparsedBuffer) {
+ unparsedBuffer = m_unparsedBuffer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::SetUnparsedBuffer(const nsACString& unparsedBuffer) {
+ m_unparsedBuffer.Assign(unparsedBuffer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::AddTerm(
+ nsMsgSearchAttribValue attrib, /* attribute for this term */
+ nsMsgSearchOpValue op, /* operator e.g. opContains */
+ nsIMsgSearchValue* value, /* value e.g. "Dogbert" */
+ bool BooleanAND, /* true if AND is the boolean operator.
+ false if OR is the boolean operators */
+ const nsACString& arbitraryHeader) /* arbitrary header specified by user.
+ ignored unless attrib = attribOtherHeader */
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::AppendTerm(nsIMsgSearchTerm* aTerm) {
+ NS_ENSURE_TRUE(aTerm, NS_ERROR_NULL_POINTER);
+ // invalidate expression tree if we're changing the terms
+ delete m_expressionTree;
+ m_expressionTree = nullptr;
+ m_termList.AppendElement(aTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::CreateTerm(nsIMsgSearchTerm** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ADDREF(*aResult = new nsMsgSearchTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::CreateAction(nsIMsgRuleAction** aAction) {
+ NS_ENSURE_ARG_POINTER(aAction);
+ NS_ADDREF(*aAction = new nsMsgRuleAction);
+ return NS_OK;
+}
+
+// All the rules' actions form a unit, with no real order imposed.
+// But certain actions like MoveToFolder or StopExecution would make us drop
+// consecutive actions, while actions like AddTag implicitly care about the
+// order of invocation. Hence we do as little reordering as possible, keeping
+// the user-defined order as much as possible.
+// We explicitly don't allow for filters which do "tag message as Important,
+// copy it to another folder, tag it as To Do also, copy this different state
+// elsewhere" in one go. You need to define separate filters for that.
+//
+// The order of actions returned by this method:
+// index action(s)
+// ------- ---------
+// 0 FetchBodyFromPop3Server
+// 1..n all other 'normal' actions, in their original order
+// n+1..m CopyToFolder
+// m+1 MoveToFolder or Delete
+// m+2 StopExecution
+NS_IMETHODIMP
+nsMsgFilter::GetSortedActionList(
+ nsTArray<RefPtr<nsIMsgRuleAction>>& aActionList) {
+ aActionList.Clear();
+ aActionList.SetCapacity(m_actionList.Length());
+
+ // hold separate pointers into the action list
+ uint32_t nextIndexForNormal = 0, nextIndexForCopy = 0, nextIndexForMove = 0;
+ for (auto action : m_actionList) {
+ if (!action) continue;
+
+ nsMsgRuleActionType actionType;
+ action->GetType(&actionType);
+ switch (actionType) {
+ case nsMsgFilterAction::FetchBodyFromPop3Server: {
+ // always insert in front
+ aActionList.InsertElementAt(0, action);
+ ++nextIndexForNormal;
+ ++nextIndexForCopy;
+ ++nextIndexForMove;
+ break;
+ }
+
+ case nsMsgFilterAction::CopyToFolder: {
+ // insert into copy actions block, in order of appearance
+ aActionList.InsertElementAt(nextIndexForCopy, action);
+ ++nextIndexForCopy;
+ ++nextIndexForMove;
+ break;
+ }
+
+ case nsMsgFilterAction::MoveToFolder:
+ case nsMsgFilterAction::Delete: {
+ // insert into move/delete action block
+ aActionList.InsertElementAt(nextIndexForMove, action);
+ ++nextIndexForMove;
+ break;
+ }
+
+ case nsMsgFilterAction::StopExecution: {
+ // insert into stop action block
+ aActionList.AppendElement(action);
+ break;
+ }
+
+ default: {
+ // insert into normal action block, in order of appearance
+ aActionList.InsertElementAt(nextIndexForNormal, action);
+ ++nextIndexForNormal;
+ ++nextIndexForCopy;
+ ++nextIndexForMove;
+ break;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::AppendAction(nsIMsgRuleAction* aAction) {
+ NS_ENSURE_ARG_POINTER(aAction);
+
+ m_actionList.AppendElement(aAction);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::GetActionAt(uint32_t aIndex, nsIMsgRuleAction** aAction) {
+ NS_ENSURE_ARG_POINTER(aAction);
+ NS_ENSURE_ARG(aIndex < m_actionList.Length());
+
+ NS_ENSURE_TRUE(m_actionList[aIndex], NS_ERROR_ILLEGAL_VALUE);
+ NS_IF_ADDREF(*aAction = m_actionList[aIndex]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::GetActionIndex(nsIMsgRuleAction* aAction, int32_t* aIndex) {
+ NS_ENSURE_ARG_POINTER(aIndex);
+
+ *aIndex = m_actionList.IndexOf(aAction);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::GetActionCount(uint32_t* aCount) {
+ NS_ENSURE_ARG_POINTER(aCount);
+
+ *aCount = m_actionList.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP // for editing a filter
+nsMsgFilter::ClearActionList() {
+ m_actionList.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::GetTerm(
+ int32_t termIndex,
+ nsMsgSearchAttribValue* attrib, /* attribute for this term */
+ nsMsgSearchOpValue* op, /* operator e.g. opContains */
+ nsIMsgSearchValue** value, /* value e.g. "Dogbert" */
+ bool* booleanAnd, /* true if AND is the boolean operator. false if OR is the
+ boolean operator */
+ nsACString& arbitraryHeader) /* arbitrary header specified by user.ignore
+ unless attrib = attribOtherHeader */
+{
+ if (termIndex >= (int32_t)m_termList.Length()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsIMsgSearchTerm* term = m_termList[termIndex];
+ if (attrib) term->GetAttrib(attrib);
+ if (op) term->GetOp(op);
+ if (value) term->GetValue(value);
+ if (booleanAnd) term->GetBooleanAnd(booleanAnd);
+ if (attrib && *attrib > nsMsgSearchAttrib::OtherHeader &&
+ *attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ term->GetArbitraryHeader(arbitraryHeader);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::GetSearchTerms(
+ nsTArray<RefPtr<nsIMsgSearchTerm>>& terms) {
+ delete m_expressionTree;
+ m_expressionTree = nullptr;
+ terms = m_termList.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::SetSearchTerms(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& terms) {
+ delete m_expressionTree;
+ m_expressionTree = nullptr;
+ m_termList = terms.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::SetScope(nsIMsgSearchScopeTerm* aResult) {
+ m_scope = aResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilter::GetScope(nsIMsgSearchScopeTerm** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_IF_ADDREF(*aResult = m_scope);
+ return NS_OK;
+}
+
+// This function handles the logging both for success of filtering
+// (NS_SUCCEEDED(aRcode)), and for error reporting (NS_FAILED(aRcode)
+// when the filter action (such as file move/copy) failed.
+//
+// @param aRcode NS_OK for successful filtering
+// operation, otherwise, an error code for filtering failure.
+// @param aErrmsg Not used for success case (ignored), and a non-null
+// error message for failure case.
+//
+// CAUTION: Unless logging is enabled, no error/warning is shown.
+// So enable logging if you would like to see the error/warning.
+//
+// XXX The current code in this file does not report errors of minor
+// operations such as adding labels and so forth which may fail when
+// underlying file system for the message store experiences
+// failure. For now, most visible major errors such as message
+// move/copy failures are taken care of.
+//
+// XXX Possible Improvement: For error case reporting, someone might
+// want to implement a transient message that appears and stick until
+// the user clears in the message status bar, etc. For now, we log an
+// error in a similar form as a conventional successful filter event
+// with additional error information at the beginning.
+//
+nsresult nsMsgFilter::LogRuleHitGeneric(nsIMsgRuleAction* aFilterAction,
+ nsIMsgDBHdr* aMsgHdr, nsresult aRcode,
+ const nsACString& aErrmsg) {
+ NS_ENSURE_ARG_POINTER(aFilterAction);
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+
+ NS_ENSURE_TRUE(m_filterList, NS_OK);
+
+ PRTime date;
+ nsMsgRuleActionType actionType;
+
+ nsString authorValue;
+ nsString subjectValue;
+ nsString filterName;
+ nsString dateValue;
+
+ GetFilterName(filterName);
+ aFilterAction->GetType(&actionType);
+ (void)aMsgHdr->GetDate(&date);
+ PRExplodedTime exploded;
+ PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded);
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ (void)aMsgHdr->GetMime2DecodedAuthor(authorValue);
+ (void)aMsgHdr->GetMime2DecodedSubject(subjectValue);
+
+ nsString buffer;
+ // this is big enough to hold a log entry.
+ // do this so we avoid growing and copying as we append to the log.
+ buffer.SetCapacity(512);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If error, prefix with the error code and error message.
+ // A desired wording (without NEWLINEs):
+ // Filter Action Failed "Move failed" with error code=0x80004005
+ // while attempting: Applied filter "test" to message from
+ // Some Test <test@example.com> - send test 3 at 2/13/2015 11:32:53 AM
+ // moved message id = 54DE5165.7000907@example.com to
+ // mailbox://nobody@Local%20Folders/test
+ if (NS_FAILED(aRcode)) {
+ // Convert aErrmsg to UTF16 string, and
+ // convert aRcode to UTF16 string in advance.
+ char tcode[20];
+ PR_snprintf(tcode, sizeof(tcode), "0x%08x", aRcode);
+ NS_ConvertASCIItoUTF16 tcode16(tcode);
+
+ nsString tErrmsg;
+ if (actionType != nsMsgFilterAction::Custom) {
+ // If this is one of our internal actions, the passed string
+ // is an identifier to get from the bundle.
+ rv =
+ bundle->GetStringFromName(PromiseFlatCString(aErrmsg).get(), tErrmsg);
+ if (NS_FAILED(rv)) tErrmsg.Assign(NS_ConvertUTF8toUTF16(aErrmsg));
+ } else {
+ // The addon creating the custom action should have passed a localized
+ // string.
+ tErrmsg.Assign(NS_ConvertUTF8toUTF16(aErrmsg));
+ }
+ AutoTArray<nsString, 2> logErrorFormatStrings = {tErrmsg, tcode16};
+
+ nsString filterFailureWarningPrefix;
+ rv = bundle->FormatStringFromName("filterFailureWarningPrefix",
+ logErrorFormatStrings,
+ filterFailureWarningPrefix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ buffer += filterFailureWarningPrefix;
+ buffer.AppendLiteral("\n");
+ }
+
+ AutoTArray<nsString, 4> filterLogDetectFormatStrings = {
+ filterName, authorValue, subjectValue, dateValue};
+ nsString filterLogDetectStr;
+ rv = bundle->FormatStringFromName(
+ "filterLogDetectStr", filterLogDetectFormatStrings, filterLogDetectStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += filterLogDetectStr;
+ buffer.AppendLiteral("\n");
+
+ if (actionType == nsMsgFilterAction::MoveToFolder ||
+ actionType == nsMsgFilterAction::CopyToFolder) {
+ nsCString actionFolderUri;
+ aFilterAction->GetTargetFolderUri(actionFolderUri);
+
+ nsCString msgId;
+ aMsgHdr->GetMessageId(getter_Copies(msgId));
+
+ AutoTArray<nsString, 2> logMoveFormatStrings;
+ CopyUTF8toUTF16(msgId, *logMoveFormatStrings.AppendElement());
+ CopyUTF8toUTF16(actionFolderUri, *logMoveFormatStrings.AppendElement());
+ nsString logMoveStr;
+ rv = bundle->FormatStringFromName(
+ (actionType == nsMsgFilterAction::MoveToFolder) ? "logMoveStr"
+ : "logCopyStr",
+ logMoveFormatStrings, logMoveStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += logMoveStr;
+ } else if (actionType == nsMsgFilterAction::Custom) {
+ nsCOMPtr<nsIMsgFilterCustomAction> customAction;
+ nsAutoString filterActionName;
+ rv = aFilterAction->GetCustomAction(getter_AddRefs(customAction));
+ if (NS_SUCCEEDED(rv) && customAction)
+ customAction->GetName(filterActionName);
+ if (filterActionName.IsEmpty())
+ bundle->GetStringFromName("filterMissingCustomAction", filterActionName);
+ buffer += filterActionName;
+ } else {
+ nsString actionValue;
+ nsAutoCString filterActionID;
+ filterActionID = "filterAction"_ns;
+ filterActionID.AppendInt(actionType);
+ rv = bundle->GetStringFromName(filterActionID.get(), actionValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += actionValue;
+ }
+ buffer.AppendLiteral("\n");
+
+ return m_filterList->LogFilterMessage(buffer, nullptr);
+}
+
+NS_IMETHODIMP nsMsgFilter::LogRuleHit(nsIMsgRuleAction* aFilterAction,
+ nsIMsgDBHdr* aMsgHdr) {
+ return nsMsgFilter::LogRuleHitGeneric(aFilterAction, aMsgHdr, NS_OK,
+ EmptyCString());
+}
+
+NS_IMETHODIMP nsMsgFilter::LogRuleHitFail(nsIMsgRuleAction* aFilterAction,
+ nsIMsgDBHdr* aMsgHdr, nsresult aRcode,
+ const nsACString& aErrMsg) {
+ return nsMsgFilter::LogRuleHitGeneric(aFilterAction, aMsgHdr, aRcode,
+ aErrMsg);
+}
+
+NS_IMETHODIMP
+nsMsgFilter::MatchHdr(nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder,
+ nsIMsgDatabase* db, const nsACString& headers,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(folder);
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ nsCString folderCharset = "UTF-8"_ns;
+ nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(folder));
+ if (newsfolder) newsfolder->GetCharset(folderCharset);
+ return nsMsgSearchOfflineMail::MatchTermsForFilter(
+ msgHdr, m_termList, folderCharset.get(), m_scope, db, headers,
+ &m_expressionTree, pResult);
+}
+
+NS_IMETHODIMP
+nsMsgFilter::SetFilterList(nsIMsgFilterList* filterList) {
+ // doesn't hold a ref.
+ m_filterList = filterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilter::GetFilterList(nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_IF_ADDREF(*aResult = m_filterList);
+ return NS_OK;
+}
+
+void nsMsgFilter::SetFilterScript(nsCString* fileName) {
+ m_scriptFileName = *fileName;
+}
+
+nsresult nsMsgFilter::ConvertMoveOrCopyToFolderValue(
+ nsIMsgRuleAction* filterAction, nsCString& moveValue) {
+ NS_ENSURE_ARG_POINTER(filterAction);
+ int16_t filterVersion = kFileVersion;
+ if (m_filterList) m_filterList->GetVersion(&filterVersion);
+ if (filterVersion <= k60Beta1Version) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsCString folderUri;
+
+ m_filterList->GetFolder(getter_AddRefs(rootFolder));
+ // if relative path starts with kImap, this is a move to folder on the same
+ // server
+ if (moveValue.Find(kImapPrefix) == 0) {
+ int32_t prefixLen = PL_strlen(kImapPrefix);
+ nsAutoCString originalServerPath(Substring(moveValue, prefixLen));
+ if (filterVersion == k45Version) {
+ nsAutoString unicodeStr;
+ NS_CopyNativeToUnicode(originalServerPath, unicodeStr);
+
+ nsresult rv = CopyUTF16toMUTF7(unicodeStr, originalServerPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgFolder> destIFolder;
+ if (rootFolder) {
+ rootFolder->FindSubFolder(originalServerPath,
+ getter_AddRefs(destIFolder));
+ if (destIFolder) {
+ destIFolder->GetURI(folderUri);
+ filterAction->SetTargetFolderUri(folderUri);
+ moveValue.Assign(folderUri);
+ }
+ }
+ } else {
+ // start off leaving the value the same.
+ filterAction->SetTargetFolderUri(moveValue);
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgFolder> localMailRoot;
+ rootFolder->GetURI(folderUri);
+ // if the root folder is not imap, than the local mail root is the server
+ // root. otherwise, it's the migrated local folders.
+ if (!StringBeginsWith(folderUri, "imap:"_ns))
+ localMailRoot = rootFolder;
+ else {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server)
+ rv = server->GetRootFolder(getter_AddRefs(localMailRoot));
+ }
+ if (NS_SUCCEEDED(rv) && localMailRoot) {
+ nsCString localRootURI;
+ nsCOMPtr<nsIMsgFolder> destIMsgFolder;
+ localMailRoot->GetURI(localRootURI);
+ nsCString destFolderUri;
+ destFolderUri.Assign(localRootURI);
+ // need to remove ".sbd" from moveValue, and perhaps escape it.
+ int32_t offset = moveValue.Find(FOLDER_SUFFIX8 "/");
+ if (offset != -1) moveValue.Cut(offset, FOLDER_SUFFIX_LENGTH);
+
+#ifdef XP_MACOSX
+ nsCString unescapedMoveValue;
+ MsgUnescapeString(moveValue, 0, unescapedMoveValue);
+ moveValue = unescapedMoveValue;
+#endif
+ destFolderUri.Append('/');
+ if (filterVersion == k45Version) {
+ nsAutoString unicodeStr;
+ NS_CopyNativeToUnicode(moveValue, unicodeStr);
+ rv = NS_MsgEscapeEncodeURLPath(unicodeStr, moveValue);
+ }
+ destFolderUri.Append(moveValue);
+ localMailRoot->GetChildWithURI(destFolderUri, true,
+ false /*caseInsensitive*/,
+ getter_AddRefs(destIMsgFolder));
+
+ if (destIMsgFolder) {
+ destIMsgFolder->GetURI(folderUri);
+ filterAction->SetTargetFolderUri(folderUri);
+ moveValue.Assign(folderUri);
+ }
+ }
+ }
+ } else
+ filterAction->SetTargetFolderUri(moveValue);
+
+ return NS_OK;
+ // set m_action.m_value.m_folderUri
+}
+
+NS_IMETHODIMP
+nsMsgFilter::SaveToTextFile(nsIOutputStream* aStream) {
+ NS_ENSURE_ARG_POINTER(aStream);
+ if (m_unparseable) {
+ uint32_t bytesWritten;
+ // we need to trim leading whitespaces before filing out
+ m_unparsedBuffer.Trim(kWhitespace, true /*leadingCharacters*/,
+ false /*trailingCharacters*/);
+ return aStream->Write(m_unparsedBuffer.get(), m_unparsedBuffer.Length(),
+ &bytesWritten);
+ }
+ nsresult err = m_filterList->WriteWstrAttr(nsIMsgFilterList::attribName,
+ m_filterName.get(), aStream);
+ err = m_filterList->WriteBoolAttr(nsIMsgFilterList::attribEnabled, m_enabled,
+ aStream);
+ err = m_filterList->WriteStrAttr(nsIMsgFilterList::attribDescription,
+ m_description.get(), aStream);
+ err =
+ m_filterList->WriteIntAttr(nsIMsgFilterList::attribType, m_type, aStream);
+ if (IsScript())
+ err = m_filterList->WriteStrAttr(nsIMsgFilterList::attribScriptFile,
+ m_scriptFileName.get(), aStream);
+ else
+ err = SaveRule(aStream);
+ return err;
+}
+
+nsresult nsMsgFilter::SaveRule(nsIOutputStream* aStream) {
+ nsresult err = NS_OK;
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ GetFilterList(getter_AddRefs(filterList));
+ nsAutoCString actionFilingStr;
+
+ uint32_t numActions;
+ err = GetActionCount(&numActions);
+ NS_ENSURE_SUCCESS(err, err);
+
+ for (uint32_t index = 0; index < numActions; index++) {
+ nsCOMPtr<nsIMsgRuleAction> action;
+ err = GetActionAt(index, getter_AddRefs(action));
+ if (NS_FAILED(err) || !action) continue;
+
+ nsMsgRuleActionType actionType;
+ action->GetType(&actionType);
+ GetActionFilingStr(actionType, actionFilingStr);
+
+ err = filterList->WriteStrAttr(nsIMsgFilterList::attribAction,
+ actionFilingStr.get(), aStream);
+ NS_ENSURE_SUCCESS(err, err);
+
+ switch (actionType) {
+ case nsMsgFilterAction::MoveToFolder:
+ case nsMsgFilterAction::CopyToFolder: {
+ nsCString imapTargetString;
+ action->GetTargetFolderUri(imapTargetString);
+ err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue,
+ imapTargetString.get(), aStream);
+ } break;
+ case nsMsgFilterAction::ChangePriority: {
+ nsMsgPriorityValue priorityValue;
+ action->GetPriority(&priorityValue);
+ nsAutoCString priority;
+ NS_MsgGetUntranslatedPriorityName(priorityValue, priority);
+ err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue,
+ priority.get(), aStream);
+ } break;
+ case nsMsgFilterAction::JunkScore: {
+ int32_t junkScore;
+ action->GetJunkScore(&junkScore);
+ err = filterList->WriteIntAttr(nsIMsgFilterList::attribActionValue,
+ junkScore, aStream);
+ } break;
+ case nsMsgFilterAction::AddTag:
+ case nsMsgFilterAction::Reply:
+ case nsMsgFilterAction::Forward: {
+ nsCString strValue;
+ action->GetStrValue(strValue);
+ // strValue is e-mail address
+ err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue,
+ strValue.get(), aStream);
+ } break;
+ case nsMsgFilterAction::Custom: {
+ nsAutoCString id;
+ action->GetCustomId(id);
+ err = filterList->WriteStrAttr(nsIMsgFilterList::attribCustomId,
+ id.get(), aStream);
+ nsAutoCString strValue;
+ action->GetStrValue(strValue);
+ if (strValue.Length())
+ err = filterList->WriteWstrAttr(nsIMsgFilterList::attribActionValue,
+ NS_ConvertUTF8toUTF16(strValue).get(),
+ aStream);
+ } break;
+
+ default:
+ break;
+ }
+ NS_ENSURE_SUCCESS(err, err);
+ }
+ // and here the fun begins - file out term list...
+ nsAutoCString condition;
+ err = MsgTermListToString(m_termList, condition);
+ NS_ENSURE_SUCCESS(err, err);
+ return filterList->WriteStrAttr(nsIMsgFilterList::attribCondition,
+ condition.get(), aStream);
+}
+
+// for each action, this table encodes the filterTypes that support the action.
+struct RuleActionsTableEntry {
+ nsMsgRuleActionType action;
+ const char*
+ actionFilingStr; /* used for filing out filters, don't translate! */
+};
+
+static struct RuleActionsTableEntry ruleActionsTable[] = {
+ {nsMsgFilterAction::MoveToFolder, "Move to folder"},
+ {nsMsgFilterAction::CopyToFolder, "Copy to folder"},
+ {nsMsgFilterAction::ChangePriority, "Change priority"},
+ {nsMsgFilterAction::Delete, "Delete"},
+ {nsMsgFilterAction::MarkRead, "Mark read"},
+ {nsMsgFilterAction::KillThread, "Ignore thread"},
+ {nsMsgFilterAction::KillSubthread, "Ignore subthread"},
+ {nsMsgFilterAction::WatchThread, "Watch thread"},
+ {nsMsgFilterAction::MarkFlagged, "Mark flagged"},
+ {nsMsgFilterAction::Reply, "Reply"},
+ {nsMsgFilterAction::Forward, "Forward"},
+ {nsMsgFilterAction::StopExecution, "Stop execution"},
+ {nsMsgFilterAction::DeleteFromPop3Server, "Delete from Pop3 server"},
+ {nsMsgFilterAction::LeaveOnPop3Server, "Leave on Pop3 server"},
+ {nsMsgFilterAction::JunkScore, "JunkScore"},
+ {nsMsgFilterAction::FetchBodyFromPop3Server, "Fetch body from Pop3Server"},
+ {nsMsgFilterAction::AddTag, "AddTag"},
+ {nsMsgFilterAction::MarkUnread, "Mark unread"},
+ {nsMsgFilterAction::Custom, "Custom"},
+};
+
+static const unsigned int sNumActions = MOZ_ARRAY_LENGTH(ruleActionsTable);
+
+const char* nsMsgFilter::GetActionStr(nsMsgRuleActionType action) {
+ for (unsigned int i = 0; i < sNumActions; i++) {
+ if (action == ruleActionsTable[i].action)
+ return ruleActionsTable[i].actionFilingStr;
+ }
+ return "";
+}
+/*static */ nsresult nsMsgFilter::GetActionFilingStr(nsMsgRuleActionType action,
+ nsCString& actionStr) {
+ for (unsigned int i = 0; i < sNumActions; i++) {
+ if (action == ruleActionsTable[i].action) {
+ actionStr = ruleActionsTable[i].actionFilingStr;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_INVALID_ARG;
+}
+
+nsMsgRuleActionType nsMsgFilter::GetActionForFilingStr(nsCString& actionStr) {
+ for (unsigned int i = 0; i < sNumActions; i++) {
+ if (actionStr.Equals(ruleActionsTable[i].actionFilingStr))
+ return ruleActionsTable[i].action;
+ }
+ return nsMsgFilterAction::None;
+}
+
+int16_t nsMsgFilter::GetVersion() {
+ if (!m_filterList) return 0;
+ int16_t version;
+ m_filterList->GetVersion(&version);
+ return version;
+}
+
+#ifdef DEBUG
+void nsMsgFilter::Dump() {
+ nsAutoCString s;
+ LossyCopyUTF16toASCII(m_filterName, s);
+ printf("filter %s type = %c desc = %s\n", s.get(), m_type + '0',
+ m_description.get());
+}
+#endif
diff --git a/comm/mailnews/search/src/nsMsgFilter.h b/comm/mailnews/search/src/nsMsgFilter.h
new file mode 100644
index 0000000000..bf77f7a992
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilter.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgFilter_H_
+#define _nsMsgFilter_H_
+
+#include "nscore.h"
+#include "nsISupports.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgSearchScopeTerm.h"
+#include "nsMsgSearchBoolExpression.h"
+#include "nsIMsgFilterCustomAction.h"
+
+class nsMsgRuleAction : public nsIMsgRuleAction {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsMsgRuleAction();
+
+ NS_DECL_NSIMSGRULEACTION
+
+ private:
+ virtual ~nsMsgRuleAction();
+
+ nsMsgRuleActionType m_type;
+ // this used to be a union - why bother?
+ nsMsgPriorityValue m_priority; /* priority to set rule to */
+ nsCString m_folderUri;
+ int32_t m_junkScore; /* junk score (or arbitrary int value?) */
+ // arbitrary string value. Currently, email address to forward to
+ nsCString m_strValue;
+ nsCString m_customId;
+ nsCOMPtr<nsIMsgFilterCustomAction> m_customAction;
+};
+
+class nsMsgFilter : public nsIMsgFilter {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsMsgFilter();
+
+ NS_DECL_NSIMSGFILTER
+
+ nsMsgFilterTypeType GetType() { return m_type; }
+ void SetType(nsMsgFilterTypeType type) { m_type = type; }
+ bool GetEnabled() { return m_enabled; }
+ void SetFilterScript(nsCString* filterName);
+
+ bool IsScript() {
+ return (m_type & (nsMsgFilterType::InboxJavaScript |
+ nsMsgFilterType::NewsJavaScript)) != 0;
+ }
+
+ // filing routines.
+ nsresult SaveRule(nsIOutputStream* aStream);
+
+ int16_t GetVersion();
+#ifdef DEBUG
+ void Dump();
+#endif
+
+ nsresult ConvertMoveOrCopyToFolderValue(nsIMsgRuleAction* filterAction,
+ nsCString& relativePath);
+ static const char* GetActionStr(nsMsgRuleActionType action);
+ static nsresult GetActionFilingStr(nsMsgRuleActionType action,
+ nsCString& actionStr);
+ static nsMsgRuleActionType GetActionForFilingStr(nsCString& actionStr);
+
+ protected:
+ /*
+ * Reporting function for filtering success/failure.
+ * Logging has to be enabled for the message to appear.
+ */
+ nsresult LogRuleHitGeneric(nsIMsgRuleAction* aFilterAction,
+ nsIMsgDBHdr* aMsgHdr, nsresult aRcode,
+ const nsACString& aErrmsg);
+
+ virtual ~nsMsgFilter();
+
+ nsMsgFilterTypeType m_type;
+ nsString m_filterName;
+ nsCString m_scriptFileName; // iff this filter is a script.
+ nsCString m_description;
+ nsCString m_unparsedBuffer;
+
+ bool m_enabled;
+ bool m_temporary;
+ bool m_unparseable;
+ nsIMsgFilterList* m_filterList; /* owning filter list */
+ nsTArray<RefPtr<nsIMsgSearchTerm>> m_termList; /* criteria terms */
+ nsCOMPtr<nsIMsgSearchScopeTerm>
+ m_scope; /* default for mail rules is inbox, but news rules could
+ have a newsgroup - LDAP would be invalid */
+ nsTArray<nsCOMPtr<nsIMsgRuleAction>> m_actionList;
+ nsMsgSearchBoolExpression* m_expressionTree;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgFilterList.cpp b/comm/mailnews/search/src/nsMsgFilterList.cpp
new file mode 100644
index 0000000000..4a81eafe09
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilterList.cpp
@@ -0,0 +1,1207 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// this file implements the nsMsgFilterList interface
+
+#include "nsTextFormatter.h"
+
+#include "msgCore.h"
+#include "nsMsgFilterList.h"
+#include "nsMsgFilter.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFilterHitNotify.h"
+#include "nsMsgUtils.h"
+#include "nsMsgSearchTerm.h"
+#include "nsString.h"
+#include "nsIMsgFilterService.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsIStringBundle.h"
+#include "nsNetUtil.h"
+#include "nsIInputStream.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsMemory.h"
+#include "prmem.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/Logging.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+#include <ctype.h>
+
+// Marker for EOF or failure during read
+#define EOF_CHAR -1
+
+using namespace mozilla;
+
+extern LazyLogModule FILTERLOGMODULE;
+
+static uint32_t nextListId = 0;
+
+nsMsgFilterList::nsMsgFilterList() : m_fileVersion(0) {
+ m_loggingEnabled = false;
+ m_startWritingToBuffer = false;
+ m_temporaryList = false;
+ m_curFilter = nullptr;
+ m_listId.Assign("List");
+ m_listId.AppendInt(nextListId++);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Creating a new filter list with id=%s", m_listId.get()));
+}
+
+NS_IMPL_ADDREF(nsMsgFilterList)
+NS_IMPL_RELEASE(nsMsgFilterList)
+NS_IMPL_QUERY_INTERFACE(nsMsgFilterList, nsIMsgFilterList)
+
+NS_IMETHODIMP nsMsgFilterList::CreateFilter(const nsAString& name,
+ class nsIMsgFilter** aFilter) {
+ NS_ENSURE_ARG_POINTER(aFilter);
+
+ NS_ADDREF(*aFilter = new nsMsgFilter);
+
+ (*aFilter)->SetFilterName(name);
+ (*aFilter)->SetFilterList(this);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::SetLoggingEnabled(bool enabled) {
+ if (!enabled) {
+ // Disabling logging has side effect of closing logfile (if open).
+ SetLogStream(nullptr);
+ }
+ m_loggingEnabled = enabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::GetLoggingEnabled(bool* enabled) {
+ *enabled = m_loggingEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::GetListId(nsACString& aListId) {
+ aListId.Assign(m_listId);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::GetFolder(nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::SetFolder(nsIMsgFolder* aFolder) {
+ m_folder = aFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::SaveToFile(nsIOutputStream* stream) {
+ if (!stream) return NS_ERROR_NULL_POINTER;
+ return SaveTextFilters(stream);
+}
+
+#define LOG_HEADER \
+ "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style " \
+ "type=\"text/css\">body{font-family:Consolas,\"Lucida " \
+ "Console\",Monaco,\"Courier " \
+ "New\",Courier,monospace;font-size:small}</style>\n</head>\n<body>\n"
+#define LOG_HEADER_LEN (strlen(LOG_HEADER))
+
+nsresult nsMsgFilterList::EnsureLogFile(nsIFile* file) {
+ bool exists;
+ nsresult rv = file->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists) {
+ rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0666);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int64_t fileSize;
+ rv = file->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the header at the start
+ if (fileSize == 0) {
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = MsgGetFileStream(file, getter_AddRefs(outputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t writeCount;
+ rv = outputStream->Write(LOG_HEADER, LOG_HEADER_LEN, &writeCount);
+ NS_ASSERTION(writeCount == LOG_HEADER_LEN,
+ "failed to write out log header");
+ NS_ENSURE_SUCCESS(rv, rv);
+ outputStream->Close();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::ClearLog() {
+ bool loggingEnabled = m_loggingEnabled;
+
+ // disable logging while clearing (and close logStream if open).
+ SetLoggingEnabled(false);
+
+ nsCOMPtr<nsIFile> file;
+ if (NS_SUCCEEDED(GetLogFile(getter_AddRefs(file)))) {
+ file->Remove(false);
+ // Recreate the file, with just the html header.
+ EnsureLogFile(file);
+ }
+
+ SetLoggingEnabled(loggingEnabled);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::GetLogFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ // XXX todo
+ // the path to the log file won't change
+ // should we cache it?
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString type;
+ rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isServer = false;
+ rv = m_folder->GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // for news folders (not servers), the filter file is
+ // mcom.test.dat
+ // where the summary file is
+ // mcom.test.msf
+ // since the log is an html file we make it
+ // mcom.test.htm
+ if (type.EqualsLiteral("nntp") && !isServer) {
+ nsCOMPtr<nsIFile> thisFolder;
+ rv = m_folder->GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> filterLogFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = filterLogFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // NOTE:
+ // we don't we need to call NS_MsgHashIfNecessary()
+ // it's already been hashed, if necessary
+ nsAutoString filterLogName;
+ rv = filterLogFile->GetLeafName(filterLogName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterLogName.AppendLiteral(u".htm");
+
+ rv = filterLogFile->SetLeafName(filterLogName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterLogFile.forget(aFile);
+ } else {
+ rv = server->GetLocalPath(aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = (*aFile)->AppendNative("filterlog.html"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return EnsureLogFile(*aFile);
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::GetLogURL(nsACString& aLogURL) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetLogFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_GetURLSpecFromFile(file, aLogURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return !aLogURL.IsEmpty() ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::SetLogStream(nsIOutputStream* aLogStream) {
+ // if there is a log stream already, close it
+ if (m_logStream) {
+ m_logStream->Close(); // will flush
+ }
+
+ m_logStream = aLogStream;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::GetLogStream(nsIOutputStream** aLogStream) {
+ NS_ENSURE_ARG_POINTER(aLogStream);
+
+ if (!m_logStream && m_loggingEnabled) {
+ nsCOMPtr<nsIFile> logFile;
+ nsresult rv = GetLogFile(getter_AddRefs(logFile));
+ if (NS_SUCCEEDED(rv)) {
+ // Make sure it exists and has it's initial header.
+ rv = EnsureLogFile(logFile);
+ if (NS_SUCCEEDED(rv)) {
+ // append to the end of the log file
+ rv = MsgNewBufferedFileOutputStream(
+ getter_AddRefs(m_logStream), logFile,
+ PR_CREATE_FILE | PR_WRONLY | PR_APPEND, 0666);
+ }
+ }
+ if (NS_FAILED(rv)) {
+ m_logStream = nullptr;
+ }
+ }
+
+ // Always returns NS_OK. The stream can be null.
+ NS_IF_ADDREF(*aLogStream = m_logStream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::ApplyFiltersToHdr(nsMsgFilterTypeType filterType,
+ nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder,
+ nsIMsgDatabase* db,
+ const nsACString& headers,
+ nsIMsgFilterHitNotify* listener,
+ nsIMsgWindow* msgWindow) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Auto) nsMsgFilterList::ApplyFiltersToHdr"));
+ if (!msgHdr) {
+ // Sometimes we get here with no header, so let's not crash on that
+ // later on.
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Auto) Called with NULL message header, nothing to do"));
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsCOMPtr<nsIMsgFilter> filter;
+ uint32_t filterCount = 0;
+ nsresult rv = GetFilterCount(&filterCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsMsgSearchScopeTerm> scope =
+ new nsMsgSearchScopeTerm(nullptr, nsMsgSearchScope::offlineMail, folder);
+
+ nsString folderName;
+ folder->GetName(folderName);
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ nsCString typeName;
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ filterService->FilterTypeName(filterType, typeName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Filter run initiated, trigger=%s (%i)", typeName.get(),
+ filterType));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Running %" PRIu32
+ " filters from %s on message with key %" PRIu32 " in folder '%s'",
+ filterCount, m_listId.get(), msgKeyToInt(msgKey),
+ NS_ConvertUTF16toUTF8(folderName).get()));
+
+ for (uint32_t filterIndex = 0; filterIndex < filterCount; filterIndex++) {
+ if (NS_SUCCEEDED(GetFilterAt(filterIndex, getter_AddRefs(filter)))) {
+ bool isEnabled;
+ nsMsgFilterTypeType curFilterType;
+
+ filter->GetEnabled(&isEnabled);
+ if (!isEnabled) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Skipping disabled filter at index %" PRIu32,
+ filterIndex));
+ // clang-format on
+ continue;
+ }
+
+ nsString filterName;
+ filter->GetFilterName(filterName);
+ filter->GetFilterType(&curFilterType);
+ if (curFilterType & filterType) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Running filter %" PRIu32, filterIndex));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Auto) Filter name: %s",
+ NS_ConvertUTF16toUTF8(filterName).get()));
+
+ nsresult matchTermStatus = NS_OK;
+ bool result = false;
+
+ filter->SetScope(scope);
+ matchTermStatus =
+ filter->MatchHdr(msgHdr, folder, db, headers, &result);
+ filter->SetScope(nullptr);
+ if (NS_SUCCEEDED(matchTermStatus) && result && listener) {
+ nsCString msgId;
+ msgHdr->GetMessageId(getter_Copies(msgId));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Filter matched message with key %" PRIu32,
+ msgKeyToInt(msgKey)));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Auto) Matched message ID: %s", msgId.get()));
+
+ bool applyMore = true;
+ rv = listener->ApplyFilterHit(filter, msgWindow, &applyMore);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Auto) Applying filter actions failed"));
+ LogFilterMessage(u"Applying filter actions failed"_ns, filter);
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Applying filter actions succeeded"));
+ }
+ if (NS_FAILED(rv) || !applyMore) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Stopping further filter execution"
+ " on this message"));
+ break;
+ }
+ } else {
+ if (NS_FAILED(matchTermStatus)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Auto) Filter evaluation failed"));
+ LogFilterMessage(u"Filter evaluation failed"_ns, filter);
+ }
+ if (!result)
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Filter didn't match"));
+ }
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Auto) Skipping filter of non-matching type"
+ " at index %" PRIu32,
+ filterIndex));
+ }
+ }
+ }
+ if (NS_FAILED(rv)) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Auto) Filter run failed (%" PRIx32 ")", static_cast<uint32_t>(rv)));
+ // clang-format on
+ LogFilterMessage(u"Filter run failed"_ns, nullptr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::SetDefaultFile(nsIFile* aFile) {
+ m_defaultFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::GetDefaultFile(nsIFile** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ NS_IF_ADDREF(*aResult = m_defaultFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::SaveToDefaultFile() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return filterService->SaveFilterList(this, m_defaultFile);
+}
+
+typedef struct {
+ nsMsgFilterFileAttribValue attrib;
+ const char* attribName;
+} FilterFileAttribEntry;
+
+static FilterFileAttribEntry FilterFileAttribTable[] = {
+ {nsIMsgFilterList::attribNone, ""},
+ {nsIMsgFilterList::attribVersion, "version"},
+ {nsIMsgFilterList::attribLogging, "logging"},
+ {nsIMsgFilterList::attribName, "name"},
+ {nsIMsgFilterList::attribEnabled, "enabled"},
+ {nsIMsgFilterList::attribDescription, "description"},
+ {nsIMsgFilterList::attribType, "type"},
+ {nsIMsgFilterList::attribScriptFile, "scriptName"},
+ {nsIMsgFilterList::attribAction, "action"},
+ {nsIMsgFilterList::attribActionValue, "actionValue"},
+ {nsIMsgFilterList::attribCondition, "condition"},
+ {nsIMsgFilterList::attribCustomId, "customId"},
+};
+
+static const unsigned int sNumFilterFileAttribTable =
+ MOZ_ARRAY_LENGTH(FilterFileAttribTable);
+
+// If we want to buffer file IO, wrap it in here.
+int nsMsgFilterList::ReadChar(nsIInputStream* aStream) {
+ char newChar;
+ uint32_t bytesRead;
+ uint64_t bytesAvailable;
+ nsresult rv = aStream->Available(&bytesAvailable);
+ if (NS_FAILED(rv) || bytesAvailable == 0) return EOF_CHAR;
+
+ rv = aStream->Read(&newChar, 1, &bytesRead);
+ if (NS_FAILED(rv) || !bytesRead) return EOF_CHAR;
+
+ if (m_startWritingToBuffer) m_unparsedFilterBuffer.Append(newChar);
+ return (unsigned char)newChar; // Make sure the char is unsigned.
+}
+
+int nsMsgFilterList::SkipWhitespace(nsIInputStream* aStream) {
+ int ch;
+ do {
+ ch = ReadChar(aStream);
+ } while (!(ch & 0x80) &&
+ isspace(ch)); // isspace can crash with non-ascii input
+
+ return ch;
+}
+
+bool nsMsgFilterList::StrToBool(nsCString& str) {
+ return str.EqualsLiteral("yes");
+}
+
+int nsMsgFilterList::LoadAttrib(nsMsgFilterFileAttribValue& attrib,
+ nsIInputStream* aStream) {
+ char attribStr[100];
+ int curChar;
+ attrib = nsIMsgFilterList::attribNone;
+
+ curChar = SkipWhitespace(aStream);
+ int i;
+ for (i = 0; i + 1 < (int)(sizeof(attribStr));) {
+ if (curChar == EOF_CHAR || (!(curChar & 0x80) && isspace(curChar)) ||
+ curChar == '=')
+ break;
+ attribStr[i++] = curChar;
+ curChar = ReadChar(aStream);
+ }
+ attribStr[i] = '\0';
+ for (unsigned int tableIndex = 0; tableIndex < sNumFilterFileAttribTable;
+ tableIndex++) {
+ if (!PL_strcasecmp(attribStr,
+ FilterFileAttribTable[tableIndex].attribName)) {
+ attrib = FilterFileAttribTable[tableIndex].attrib;
+ break;
+ }
+ }
+ return curChar;
+}
+
+const char* nsMsgFilterList::GetStringForAttrib(
+ nsMsgFilterFileAttribValue attrib) {
+ for (unsigned int tableIndex = 0; tableIndex < sNumFilterFileAttribTable;
+ tableIndex++) {
+ if (attrib == FilterFileAttribTable[tableIndex].attrib)
+ return FilterFileAttribTable[tableIndex].attribName;
+ }
+ return nullptr;
+}
+
+nsresult nsMsgFilterList::LoadValue(nsCString& value, nsIInputStream* aStream) {
+ nsAutoCString valueStr;
+ int curChar;
+ value = "";
+ curChar = SkipWhitespace(aStream);
+ if (curChar != '"') {
+ NS_ASSERTION(false, "expecting quote as start of value");
+ return NS_MSG_FILTER_PARSE_ERROR;
+ }
+ curChar = ReadChar(aStream);
+ do {
+ if (curChar == '\\') {
+ int nextChar = ReadChar(aStream);
+ if (nextChar == '"')
+ curChar = '"';
+ else if (nextChar == '\\') // replace "\\" with "\"
+ {
+ valueStr += curChar;
+ curChar = ReadChar(aStream);
+ } else {
+ valueStr += curChar;
+ curChar = nextChar;
+ }
+ } else {
+ if (curChar == EOF_CHAR || curChar == '"' || curChar == '\n' ||
+ curChar == '\r') {
+ value += valueStr;
+ break;
+ }
+ }
+ valueStr += curChar;
+ curChar = ReadChar(aStream);
+ } while (curChar != EOF_CHAR);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::LoadTextFilters(
+ already_AddRefed<nsIInputStream> aStream) {
+ nsresult err = NS_OK;
+ uint64_t bytesAvailable;
+
+ nsCOMPtr<nsIInputStream> bufStream;
+ nsCOMPtr<nsIInputStream> stream = std::move(aStream);
+ err = NS_NewBufferedInputStream(getter_AddRefs(bufStream), stream.forget(),
+ FILE_IO_BUFFER_SIZE);
+ NS_ENSURE_SUCCESS(err, err);
+
+ nsMsgFilterFileAttribValue attrib;
+ nsCOMPtr<nsIMsgRuleAction> currentFilterAction;
+ // We'd really like to move lot's of these into the objects that they refer
+ // to.
+ do {
+ nsAutoCString value;
+ nsresult intToStringResult;
+
+ int curChar;
+ curChar = LoadAttrib(attrib, bufStream);
+ if (curChar == EOF_CHAR) // reached eof
+ break;
+ err = LoadValue(value, bufStream);
+ if (NS_FAILED(err)) break;
+
+ switch (attrib) {
+ case nsIMsgFilterList::attribNone:
+ if (m_curFilter) m_curFilter->SetUnparseable(true);
+ break;
+ case nsIMsgFilterList::attribVersion:
+ m_fileVersion = value.ToInteger(&intToStringResult);
+ if (NS_FAILED(intToStringResult)) {
+ attrib = nsIMsgFilterList::attribNone;
+ NS_ASSERTION(false, "error parsing filter file version");
+ }
+ break;
+ case nsIMsgFilterList::attribLogging:
+ m_loggingEnabled = StrToBool(value);
+ // We are going to buffer each filter as we read them.
+ // Make sure no garbage is there
+ m_unparsedFilterBuffer.Truncate();
+ m_startWritingToBuffer = true; // filters begin now
+ break;
+ case nsIMsgFilterList::attribName: // every filter starts w/ a name
+ {
+ if (m_curFilter) {
+ int32_t nextFilterStartPos = m_unparsedFilterBuffer.RFind("name");
+
+ nsAutoCString nextFilterPart;
+ nextFilterPart = Substring(m_unparsedFilterBuffer, nextFilterStartPos,
+ m_unparsedFilterBuffer.Length());
+ m_unparsedFilterBuffer.SetLength(nextFilterStartPos);
+
+ bool unparseableFilter;
+ m_curFilter->GetUnparseable(&unparseableFilter);
+ if (unparseableFilter) {
+ m_curFilter->SetUnparsedBuffer(m_unparsedFilterBuffer);
+ m_curFilter->SetEnabled(false); // disable the filter because we
+ // don't know how to apply it
+ }
+ m_unparsedFilterBuffer = nextFilterPart;
+ }
+ nsMsgFilter* filter = new nsMsgFilter;
+ if (filter == nullptr) {
+ err = NS_ERROR_OUT_OF_MEMORY;
+ break;
+ }
+ filter->SetFilterList(static_cast<nsIMsgFilterList*>(this));
+ nsAutoString unicodeStr;
+ if (m_fileVersion == k45Version) {
+ NS_CopyNativeToUnicode(value, unicodeStr);
+ filter->SetFilterName(unicodeStr);
+ } else {
+ CopyUTF8toUTF16(value, unicodeStr);
+ filter->SetFilterName(unicodeStr);
+ }
+ m_curFilter = filter;
+ m_filters.AppendElement(filter);
+ } break;
+ case nsIMsgFilterList::attribEnabled:
+ if (m_curFilter) m_curFilter->SetEnabled(StrToBool(value));
+ break;
+ case nsIMsgFilterList::attribDescription:
+ if (m_curFilter) m_curFilter->SetFilterDesc(value);
+ break;
+ case nsIMsgFilterList::attribType:
+ if (m_curFilter) {
+ // Older versions of filters didn't have the ability to turn on/off
+ // the manual filter context, so default manual to be on in that case
+ int32_t filterType = value.ToInteger(&intToStringResult);
+ if (m_fileVersion < kManualContextVersion)
+ filterType |= nsMsgFilterType::Manual;
+ m_curFilter->SetType((nsMsgFilterTypeType)filterType);
+ }
+ break;
+ case nsIMsgFilterList::attribScriptFile:
+ if (m_curFilter) m_curFilter->SetFilterScript(&value);
+ break;
+ case nsIMsgFilterList::attribAction:
+ if (m_curFilter) {
+ nsMsgRuleActionType actionType =
+ nsMsgFilter::GetActionForFilingStr(value);
+ if (actionType == nsMsgFilterAction::None)
+ m_curFilter->SetUnparseable(true);
+ else {
+ err =
+ m_curFilter->CreateAction(getter_AddRefs(currentFilterAction));
+ NS_ENSURE_SUCCESS(err, err);
+ currentFilterAction->SetType(actionType);
+ m_curFilter->AppendAction(currentFilterAction);
+ }
+ }
+ break;
+ case nsIMsgFilterList::attribActionValue:
+ if (m_curFilter && currentFilterAction) {
+ nsMsgRuleActionType type;
+ currentFilterAction->GetType(&type);
+ if (type == nsMsgFilterAction::MoveToFolder ||
+ type == nsMsgFilterAction::CopyToFolder)
+ err = m_curFilter->ConvertMoveOrCopyToFolderValue(
+ currentFilterAction, value);
+ else if (type == nsMsgFilterAction::ChangePriority) {
+ nsMsgPriorityValue outPriority;
+ nsresult res =
+ NS_MsgGetPriorityFromString(value.get(), outPriority);
+ if (NS_SUCCEEDED(res))
+ currentFilterAction->SetPriority(outPriority);
+ else
+ NS_ASSERTION(false, "invalid priority in filter file");
+ } else if (type == nsMsgFilterAction::JunkScore) {
+ nsresult res;
+ int32_t junkScore = value.ToInteger(&res);
+ if (NS_SUCCEEDED(res)) currentFilterAction->SetJunkScore(junkScore);
+ } else if (type == nsMsgFilterAction::Forward ||
+ type == nsMsgFilterAction::Reply ||
+ type == nsMsgFilterAction::AddTag ||
+ type == nsMsgFilterAction::Custom) {
+ currentFilterAction->SetStrValue(value);
+ }
+ }
+ break;
+ case nsIMsgFilterList::attribCondition:
+ if (m_curFilter) {
+ if (m_fileVersion == k45Version) {
+ nsAutoString unicodeStr;
+ NS_CopyNativeToUnicode(value, unicodeStr);
+ CopyUTF16toUTF8(unicodeStr, value);
+ }
+ err = ParseCondition(m_curFilter, value.get());
+ if (err == NS_ERROR_INVALID_ARG)
+ err = m_curFilter->SetUnparseable(true);
+ NS_ENSURE_SUCCESS(err, err);
+ }
+ break;
+ case nsIMsgFilterList::attribCustomId:
+ if (m_curFilter && currentFilterAction) {
+ err = currentFilterAction->SetCustomId(value);
+ NS_ENSURE_SUCCESS(err, err);
+ }
+ break;
+ }
+ } while (NS_SUCCEEDED(bufStream->Available(&bytesAvailable)));
+
+ if (m_curFilter) {
+ bool unparseableFilter;
+ m_curFilter->GetUnparseable(&unparseableFilter);
+ if (unparseableFilter) {
+ m_curFilter->SetUnparsedBuffer(m_unparsedFilterBuffer);
+ m_curFilter->SetEnabled(
+ false); // disable the filter because we don't know how to apply it
+ }
+ }
+
+ return err;
+}
+
+// parse condition like "(subject, contains, fred) AND (body, isn't, "foo)")"
+// values with close parens will be quoted.
+// what about values with close parens and quotes? e.g., (body, isn't, "foo")")
+// I guess interior quotes will need to be escaped - ("foo\")")
+// which will get written out as (\"foo\\")\") and read in as ("foo\")"
+// ALL means match all messages.
+NS_IMETHODIMP nsMsgFilterList::ParseCondition(nsIMsgFilter* aFilter,
+ const char* aCondition) {
+ NS_ENSURE_ARG_POINTER(aFilter);
+
+ bool done = false;
+ nsresult err = NS_OK;
+ const char* curPtr = aCondition;
+ if (!strcmp(aCondition, "ALL")) {
+ RefPtr<nsMsgSearchTerm> newTerm = new nsMsgSearchTerm;
+ newTerm->m_matchAll = true;
+ aFilter->AppendTerm(newTerm);
+ return NS_OK;
+ }
+
+ while (!done) {
+ // insert code to save the boolean operator if there is one for this search
+ // term....
+ const char* openParen = PL_strchr(curPtr, '(');
+ const char* orTermPos = PL_strchr(
+ curPtr, 'O'); // determine if an "OR" appears b4 the openParen...
+ bool ANDTerm = true;
+ if (orTermPos &&
+ orTermPos < openParen) // make sure OR term falls before the '('
+ ANDTerm = false;
+
+ char* termDup = nullptr;
+ if (openParen) {
+ bool foundEndTerm = false;
+ bool inQuote = false;
+ for (curPtr = openParen + 1; *curPtr; curPtr++) {
+ if (*curPtr == '\\' && *(curPtr + 1) == '"')
+ curPtr++;
+ else if (*curPtr == ')' && !inQuote) {
+ foundEndTerm = true;
+ break;
+ } else if (*curPtr == '"')
+ inQuote = !inQuote;
+ }
+ if (foundEndTerm) {
+ int termLen = curPtr - openParen - 1;
+ termDup = (char*)PR_Malloc(termLen + 1);
+ if (termDup) {
+ PL_strncpy(termDup, openParen + 1, termLen + 1);
+ termDup[termLen] = '\0';
+ } else {
+ err = NS_ERROR_OUT_OF_MEMORY;
+ break;
+ }
+ }
+ } else
+ break;
+ if (termDup) {
+ RefPtr<nsMsgSearchTerm> newTerm = new nsMsgSearchTerm;
+ // Invert nsMsgSearchTerm::EscapeQuotesInStr()
+ for (char *to = termDup, *from = termDup;;) {
+ if (*from == '\\' && from[1] == '"') from++;
+ if (!(*to++ = *from++)) break;
+ }
+ newTerm->m_booleanOp = (ANDTerm) ? nsMsgSearchBooleanOp::BooleanAND
+ : nsMsgSearchBooleanOp::BooleanOR;
+
+ err = newTerm->DeStreamNew(termDup, PL_strlen(termDup));
+ NS_ENSURE_SUCCESS(err, err);
+ aFilter->AppendTerm(newTerm);
+ PR_FREEIF(termDup);
+ } else
+ break;
+ }
+ return err;
+}
+
+nsresult nsMsgFilterList::WriteIntAttr(nsMsgFilterFileAttribValue attrib,
+ int value, nsIOutputStream* aStream) {
+ nsresult rv = NS_OK;
+ const char* attribStr = GetStringForAttrib(attrib);
+ if (attribStr) {
+ uint32_t bytesWritten;
+ nsAutoCString writeStr(attribStr);
+ writeStr.AppendLiteral("=\"");
+ writeStr.AppendInt(value);
+ writeStr.AppendLiteral("\"" MSG_LINEBREAK);
+ rv = aStream->Write(writeStr.get(), writeStr.Length(), &bytesWritten);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgFilterList::WriteStrAttr(nsMsgFilterFileAttribValue attrib,
+ const char* aStr, nsIOutputStream* aStream) {
+ nsresult rv = NS_OK;
+ if (aStr && *aStr &&
+ aStream) // only proceed if we actually have a string to write out.
+ {
+ char* escapedStr = nullptr;
+ if (PL_strchr(aStr, '"'))
+ escapedStr = nsMsgSearchTerm::EscapeQuotesInStr(aStr);
+
+ const char* attribStr = GetStringForAttrib(attrib);
+ if (attribStr) {
+ uint32_t bytesWritten;
+ nsAutoCString writeStr(attribStr);
+ writeStr.AppendLiteral("=\"");
+ writeStr.Append((escapedStr) ? escapedStr : aStr);
+ writeStr.AppendLiteral("\"" MSG_LINEBREAK);
+ rv = aStream->Write(writeStr.get(), writeStr.Length(), &bytesWritten);
+ }
+ PR_Free(escapedStr);
+ }
+ return rv;
+}
+
+nsresult nsMsgFilterList::WriteBoolAttr(nsMsgFilterFileAttribValue attrib,
+ bool boolVal,
+ nsIOutputStream* aStream) {
+ return WriteStrAttr(attrib, (boolVal) ? "yes" : "no", aStream);
+}
+
+nsresult nsMsgFilterList::WriteWstrAttr(nsMsgFilterFileAttribValue attrib,
+ const char16_t* aFilterName,
+ nsIOutputStream* aStream) {
+ WriteStrAttr(attrib, NS_ConvertUTF16toUTF8(aFilterName).get(), aStream);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::SaveTextFilters(nsIOutputStream* aStream) {
+ uint32_t filterCount = 0;
+ nsresult err = GetFilterCount(&filterCount);
+ NS_ENSURE_SUCCESS(err, err);
+
+ err = WriteIntAttr(nsIMsgFilterList::attribVersion, kFileVersion, aStream);
+ NS_ENSURE_SUCCESS(err, err);
+ err =
+ WriteBoolAttr(nsIMsgFilterList::attribLogging, m_loggingEnabled, aStream);
+ NS_ENSURE_SUCCESS(err, err);
+ for (uint32_t i = 0; i < filterCount; i++) {
+ nsCOMPtr<nsIMsgFilter> filter;
+ if (NS_SUCCEEDED(GetFilterAt(i, getter_AddRefs(filter))) && filter) {
+ filter->SetFilterList(this);
+
+ // if the filter is temporary, don't write it to disk
+ bool isTemporary;
+ err = filter->GetTemporary(&isTemporary);
+ if (NS_SUCCEEDED(err) && !isTemporary) {
+ err = filter->SaveToTextFile(aStream);
+ if (NS_FAILED(err)) break;
+ }
+ } else
+ break;
+ }
+ if (NS_SUCCEEDED(err)) m_arbitraryHeaders.Truncate();
+ return err;
+}
+
+nsMsgFilterList::~nsMsgFilterList() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Closing filter list %s", m_listId.get()));
+}
+
+nsresult nsMsgFilterList::Close() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+nsresult nsMsgFilterList::GetFilterCount(uint32_t* pCount) {
+ NS_ENSURE_ARG_POINTER(pCount);
+
+ *pCount = m_filters.Length();
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::GetFilterAt(uint32_t filterIndex,
+ nsIMsgFilter** filter) {
+ NS_ENSURE_ARG_POINTER(filter);
+
+ uint32_t filterCount = 0;
+ GetFilterCount(&filterCount);
+ NS_ENSURE_ARG(filterIndex < filterCount);
+
+ NS_IF_ADDREF(*filter = m_filters[filterIndex]);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::GetFilterNamed(const nsAString& aName,
+ nsIMsgFilter** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ uint32_t count = 0;
+ nsresult rv = GetFilterCount(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = nullptr;
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFilter> filter;
+ rv = GetFilterAt(i, getter_AddRefs(filter));
+ if (NS_FAILED(rv)) continue;
+
+ nsString filterName;
+ filter->GetFilterName(filterName);
+ if (filterName.Equals(aName)) {
+ filter.forget(aResult);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::SetFilterAt(uint32_t filterIndex,
+ nsIMsgFilter* filter) {
+ m_filters[filterIndex] = filter;
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::RemoveFilterAt(uint32_t filterIndex) {
+ m_filters.RemoveElementAt(filterIndex);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::RemoveFilter(nsIMsgFilter* aFilter) {
+ m_filters.RemoveElement(aFilter);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::InsertFilterAt(uint32_t filterIndex,
+ nsIMsgFilter* aFilter) {
+ if (!m_temporaryList) aFilter->SetFilterList(this);
+ m_filters.InsertElementAt(filterIndex, aFilter);
+
+ return NS_OK;
+}
+
+// Attempt to move the filter at index filterIndex in the specified direction.
+// If motion not possible in that direction, we still return success.
+// We could return an error if the FE's want to beep or something.
+nsresult nsMsgFilterList::MoveFilterAt(uint32_t filterIndex,
+ nsMsgFilterMotionValue motion) {
+ NS_ENSURE_ARG((motion == nsMsgFilterMotion::up) ||
+ (motion == nsMsgFilterMotion::down));
+
+ uint32_t filterCount = 0;
+ nsresult rv = GetFilterCount(&filterCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_ARG(filterIndex < filterCount);
+
+ uint32_t newIndex = filterIndex;
+
+ if (motion == nsMsgFilterMotion::up) {
+ // are we already at the top?
+ if (filterIndex == 0) return NS_OK;
+
+ newIndex = filterIndex - 1;
+ } else if (motion == nsMsgFilterMotion::down) {
+ // are we already at the bottom?
+ if (filterIndex == filterCount - 1) return NS_OK;
+
+ newIndex = filterIndex + 1;
+ }
+
+ nsCOMPtr<nsIMsgFilter> tempFilter1;
+ rv = GetFilterAt(newIndex, getter_AddRefs(tempFilter1));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilter> tempFilter2;
+ rv = GetFilterAt(filterIndex, getter_AddRefs(tempFilter2));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetFilterAt(newIndex, tempFilter2);
+ SetFilterAt(filterIndex, tempFilter1);
+
+ return NS_OK;
+}
+
+nsresult nsMsgFilterList::MoveFilter(nsIMsgFilter* aFilter,
+ nsMsgFilterMotionValue motion) {
+ size_t filterIndex = m_filters.IndexOf(aFilter, 0);
+ NS_ENSURE_ARG(filterIndex != m_filters.NoIndex);
+
+ return MoveFilterAt(filterIndex, motion);
+}
+
+nsresult nsMsgFilterList::GetVersion(int16_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_fileVersion;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::MatchOrChangeFilterTarget(
+ const nsACString& oldFolderUri, const nsACString& newFolderUri,
+ bool caseInsensitive, bool* found) {
+ NS_ENSURE_ARG_POINTER(found);
+
+ uint32_t numFilters = 0;
+ nsresult rv = GetFilterCount(&numFilters);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilter> filter;
+ nsCString folderUri;
+ *found = false;
+ for (uint32_t index = 0; index < numFilters; index++) {
+ rv = GetFilterAt(index, getter_AddRefs(filter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t numActions;
+ rv = filter->GetActionCount(&numActions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t actionIndex = 0; actionIndex < numActions; actionIndex++) {
+ nsCOMPtr<nsIMsgRuleAction> filterAction;
+ rv = filter->GetActionAt(actionIndex, getter_AddRefs(filterAction));
+ if (NS_FAILED(rv) || !filterAction) continue;
+
+ nsMsgRuleActionType actionType;
+ if (NS_FAILED(filterAction->GetType(&actionType))) continue;
+
+ if (actionType == nsMsgFilterAction::MoveToFolder ||
+ actionType == nsMsgFilterAction::CopyToFolder) {
+ rv = filterAction->GetTargetFolderUri(folderUri);
+ if (NS_SUCCEEDED(rv) && !folderUri.IsEmpty()) {
+ bool matchFound = false;
+ if (caseInsensitive) {
+ if (folderUri.Equals(oldFolderUri,
+ nsCaseInsensitiveCStringComparator)) // local
+ matchFound = true;
+ } else {
+ if (folderUri.Equals(oldFolderUri)) // imap
+ matchFound = true;
+ }
+ if (matchFound) {
+ *found = true;
+ // if we just want to match the uri's, newFolderUri will be null
+ if (!newFolderUri.IsEmpty()) {
+ rv = filterAction->SetTargetFolderUri(newFolderUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+// this would only return true if any filter was on "any header", which we
+// don't support in 6.x
+NS_IMETHODIMP nsMsgFilterList::GetShouldDownloadAllHeaders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+ return NS_OK;
+}
+
+// leaves m_arbitraryHeaders filed in with the arbitrary headers.
+nsresult nsMsgFilterList::ComputeArbitraryHeaders() {
+ NS_ENSURE_TRUE(m_arbitraryHeaders.IsEmpty(), NS_OK);
+
+ uint32_t numFilters = 0;
+ nsresult rv = GetFilterCount(&numFilters);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilter> filter;
+ nsMsgSearchAttribValue attrib;
+ nsCString arbitraryHeader;
+ for (uint32_t index = 0; index < numFilters; index++) {
+ rv = GetFilterAt(index, getter_AddRefs(filter));
+ if (!(NS_SUCCEEDED(rv) && filter)) continue;
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ filter->GetSearchTerms(searchTerms);
+ for (uint32_t i = 0; i < searchTerms.Length(); i++) {
+ filter->GetTerm(i, &attrib, nullptr, nullptr, nullptr, arbitraryHeader);
+ if (!arbitraryHeader.IsEmpty()) {
+ if (m_arbitraryHeaders.IsEmpty())
+ m_arbitraryHeaders.Assign(arbitraryHeader);
+ else if (!FindInReadable(arbitraryHeader, m_arbitraryHeaders,
+ nsCaseInsensitiveCStringComparator)) {
+ m_arbitraryHeaders.Append(' ');
+ m_arbitraryHeaders.Append(arbitraryHeader);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::GetArbitraryHeaders(nsACString& aResult) {
+ ComputeArbitraryHeaders();
+ aResult = m_arbitraryHeaders;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterList::FlushLogIfNecessary() {
+ // only flush the log if we are logging
+ if (m_loggingEnabled && m_logStream) {
+ nsresult rv = m_logStream->Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+#define LOG_ENTRY_START_TAG "<p>\n"
+#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG))
+#define LOG_ENTRY_END_TAG "</p>\n"
+#define LOG_ENTRY_END_TAG_LEN (strlen(LOG_ENTRY_END_TAG))
+
+NS_IMETHODIMP nsMsgFilterList::LogFilterMessage(const nsAString& message,
+ nsIMsgFilter* filter) {
+ if (!m_loggingEnabled) {
+ return NS_OK;
+ }
+ nsCOMPtr<nsIOutputStream> logStream;
+ GetLogStream(getter_AddRefs(logStream));
+ if (!logStream) {
+ // Logging is on, but we failed to access the filter logfile.
+ // For completeness, we'll return an error, but we don't expect anyone
+ // to ever check it - logging failures shouldn't stop anything else.
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString tempMessage(message);
+
+ if (filter) {
+ // If a filter was passed, prepend its name in the log message.
+ nsString filterName;
+ filter->GetFilterName(filterName);
+
+ AutoTArray<nsString, 2> logFormatStrings = {filterName, tempMessage};
+ nsString statusLogMessage;
+ rv = bundle->FormatStringFromName("filterMessage", logFormatStrings,
+ statusLogMessage);
+ if (NS_SUCCEEDED(rv)) tempMessage.Assign(statusLogMessage);
+ }
+
+ // Prepare timestamp
+ PRExplodedTime exploded;
+ nsString dateValue;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ // HTML-escape the log for security reasons.
+ // We don't want someone to send us a message with a subject with
+ // HTML tags, especially <script>.
+ nsCString escapedBuffer;
+ nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(tempMessage), escapedBuffer);
+
+ // Print timestamp and the message.
+ AutoTArray<nsString, 2> logFormatStrings = {dateValue};
+ CopyUTF8toUTF16(escapedBuffer, *logFormatStrings.AppendElement());
+ nsString filterLogMessage;
+ rv = bundle->FormatStringFromName("filterLogLine", logFormatStrings,
+ filterLogMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write message into log stream.
+ uint32_t writeCount;
+ rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN,
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN,
+ "failed to write out start log tag");
+
+ NS_ConvertUTF16toUTF8 buffer(filterLogMessage);
+ uint32_t escapedBufferLen = buffer.Length();
+ rv = logStream->Write(buffer.get(), escapedBufferLen, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == escapedBufferLen, "failed to write out log hit");
+
+ rv = logStream->Write(LOG_ENTRY_END_TAG, LOG_ENTRY_END_TAG_LEN, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_END_TAG_LEN,
+ "failed to write out end log tag");
+ return NS_OK;
+}
+// ------------ End FilterList methods ------------------
diff --git a/comm/mailnews/search/src/nsMsgFilterList.h b/comm/mailnews/search/src/nsMsgFilterList.h
new file mode 100644
index 0000000000..536156854e
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilterList.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgFilterList_H_
+#define _nsMsgFilterList_H_
+
+#include "nscore.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgFilterList.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+
+const int16_t kFileVersion = 9;
+const int16_t kManualContextVersion = 9;
+const int16_t k60Beta1Version = 7;
+const int16_t k45Version = 6;
+
+////////////////////////////////////////////////////////////////////////////////////////
+// The Msg Filter List is an interface designed to make accessing filter lists
+// easier. Clients typically open a filter list and either enumerate the
+// filters, or add new filters, or change the order around...
+//
+////////////////////////////////////////////////////////////////////////////////////////
+
+class nsIMsgFilter;
+class nsMsgFilter;
+
+class nsMsgFilterList : public nsIMsgFilterList {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFILTERLIST
+
+ nsMsgFilterList();
+
+ nsresult Close();
+ nsresult LoadTextFilters(already_AddRefed<nsIInputStream> aStream);
+
+ bool m_temporaryList;
+
+ protected:
+ virtual ~nsMsgFilterList();
+
+ nsresult ComputeArbitraryHeaders();
+ nsresult SaveTextFilters(nsIOutputStream* aStream);
+ // file streaming methods
+ int ReadChar(nsIInputStream* aStream);
+ int SkipWhitespace(nsIInputStream* aStream);
+ bool StrToBool(nsCString& str);
+ int LoadAttrib(nsMsgFilterFileAttribValue& attrib, nsIInputStream* aStream);
+ const char* GetStringForAttrib(nsMsgFilterFileAttribValue attrib);
+ nsresult LoadValue(nsCString& value, nsIInputStream* aStream);
+ int16_t m_fileVersion;
+ bool m_loggingEnabled;
+ bool m_startWritingToBuffer; // tells us when to start writing one whole
+ // filter to m_unparsedBuffer
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsMsgFilter* m_curFilter; // filter we're filing in or out(?)
+ nsCString m_listId;
+ nsTArray<nsCOMPtr<nsIMsgFilter> > m_filters;
+ nsCString m_arbitraryHeaders;
+ nsCOMPtr<nsIFile> m_defaultFile;
+ nsCString m_unparsedFilterBuffer; // holds one entire filter unparsed
+
+ private:
+ nsresult GetLogFile(nsIFile** aFile);
+ nsresult EnsureLogFile(nsIFile* file);
+ nsCOMPtr<nsIOutputStream> m_logStream;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgFilterService.cpp b/comm/mailnews/search/src/nsMsgFilterService.cpp
new file mode 100644
index 0000000000..1adfe7cee9
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilterService.cpp
@@ -0,0 +1,1374 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// this file implements the nsMsgFilterService interface
+
+#include "msgCore.h"
+#include "nsMsgFilterService.h"
+#include "nsMsgFilterList.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIPrompt.h"
+#include "nsIDocShell.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgCopyService.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsIMsgComposeService.h"
+#include "nsNetUtil.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgMailSession.h"
+#include "nsIFile.h"
+#include "nsIMsgFilterCustomAction.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSearchCustomTerm.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgThread.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgOperationListener.h"
+#include "mozilla/Components.h"
+#include "mozilla/Logging.h"
+
+using namespace mozilla;
+
+LazyLogModule FILTERLOGMODULE("Filters");
+
+#define BREAK_IF_FAILURE(_rv, _text) \
+ if (NS_FAILED(_rv)) { \
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \
+ ("(Post) Filter error: %s", _text)); \
+ m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ m_curFilter); \
+ NS_WARNING(_text); \
+ mFinalResult = _rv; \
+ break; \
+ }
+
+#define CONTINUE_IF_FAILURE(_rv, _text) \
+ if (NS_FAILED(_rv)) { \
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \
+ ("(Post) Filter problem: %s", _text)); \
+ m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ m_curFilter); \
+ NS_WARNING(_text); \
+ mFinalResult = _rv; \
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \
+ continue; \
+ }
+
+#define BREAK_IF_FALSE(_assertTrue, _text) \
+ if (MOZ_UNLIKELY(!(_assertTrue))) { \
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \
+ ("(Post) Filter error: %s", _text)); \
+ m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ m_curFilter); \
+ NS_WARNING(_text); \
+ mFinalResult = NS_ERROR_FAILURE; \
+ break; \
+ }
+
+#define CONTINUE_IF_FALSE(_assertTrue, _text) \
+ if (MOZ_UNLIKELY(!(_assertTrue))) { \
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \
+ ("(Post) Filter problem: %s", _text)); \
+ m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ m_curFilter); \
+ NS_WARNING(_text); \
+ mFinalResult = NS_ERROR_FAILURE; \
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \
+ continue; \
+ }
+
+#define BREAK_ACTION(_text) \
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \
+ ("(Post) Filter Error: %s", _text)); \
+ if (loggingEnabled) \
+ m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ m_curFilter); \
+ NS_WARNING(_text); \
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \
+ break;
+
+#define BREAK_ACTION_IF_FALSE(_assertTrue, _text) \
+ if (MOZ_UNLIKELY(!(_assertTrue))) { \
+ finalResult = NS_ERROR_FAILURE; \
+ BREAK_ACTION(_text); \
+ }
+
+#define BREAK_ACTION_IF_FAILURE(_rv, _text) \
+ if (NS_FAILED(_rv)) { \
+ finalResult = _rv; \
+ BREAK_ACTION(_text); \
+ }
+
+NS_IMPL_ISUPPORTS(nsMsgFilterService, nsIMsgFilterService)
+
+nsMsgFilterService::nsMsgFilterService() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("nsMsgFilterService"));
+}
+
+nsMsgFilterService::~nsMsgFilterService() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("~nsMsgFilterService"));
+}
+
+NS_IMETHODIMP nsMsgFilterService::OpenFilterList(
+ nsIFile* aFilterFile, nsIMsgFolder* rootFolder, nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** resultFilterList) {
+ NS_ENSURE_ARG_POINTER(aFilterFile);
+ NS_ENSURE_ARG_POINTER(resultFilterList);
+
+ nsresult rv;
+ if (rootFolder) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = rootFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString serverName;
+ server->GetPrettyName(serverName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Reading filter list for account '%s'",
+ NS_ConvertUTF16toUTF8(serverName).get()));
+ }
+
+ nsString fileName;
+ (void)aFilterFile->GetPath(fileName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("Reading filter list from file '%s'",
+ NS_ConvertUTF16toUTF8(fileName).get()));
+
+ bool exists = false;
+ rv = aFilterFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ rv = aFilterFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIInputStream> fileStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), aFilterFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(fileStream, NS_ERROR_OUT_OF_MEMORY);
+
+ RefPtr<nsMsgFilterList> filterList = new nsMsgFilterList();
+ filterList->SetFolder(rootFolder);
+
+ // temporarily tell the filter where its file path is
+ filterList->SetDefaultFile(aFilterFile);
+
+ int64_t size = 0;
+ rv = aFilterFile->GetFileSize(&size);
+ if (NS_SUCCEEDED(rv) && size > 0)
+ rv = filterList->LoadTextFilters(fileStream.forget());
+ if (NS_SUCCEEDED(rv)) {
+ int16_t version;
+ filterList->GetVersion(&version);
+ if (version != kFileVersion) SaveFilterList(filterList, aFilterFile);
+ } else {
+ if (rv == NS_MSG_FILTER_PARSE_ERROR && aMsgWindow) {
+ rv = BackUpFilterFile(aFilterFile, aMsgWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aFilterFile->SetFileSize(0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return OpenFilterList(aFilterFile, rootFolder, aMsgWindow,
+ resultFilterList);
+ } else if (rv == NS_MSG_CUSTOM_HEADERS_OVERFLOW && aMsgWindow)
+ ThrowAlertMsg("filterCustomHeaderOverflow", aMsgWindow);
+ else if (rv == NS_MSG_INVALID_CUSTOM_HEADER && aMsgWindow)
+ ThrowAlertMsg("invalidCustomHeader", aMsgWindow);
+ }
+
+ nsCString listId;
+ filterList->GetListId(listId);
+ uint32_t filterCount = 0;
+ (void)filterList->GetFilterCount(&filterCount);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Read %" PRIu32 " filters", filterCount));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Filter list stored as %s", listId.get()));
+
+ filterList.forget(resultFilterList);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgFilterService::CloseFilterList(
+ nsIMsgFilterList* filterList) {
+ // NS_ASSERTION(false,"CloseFilterList doesn't do anything yet");
+ return NS_OK;
+}
+
+/* save without deleting */
+NS_IMETHODIMP nsMsgFilterService::SaveFilterList(nsIMsgFilterList* filterList,
+ nsIFile* filterFile) {
+ NS_ENSURE_ARG_POINTER(filterFile);
+ NS_ENSURE_ARG_POINTER(filterList);
+
+ nsCString listId;
+ filterList->GetListId(listId);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Saving filter list %s", listId.get()));
+
+ nsCOMPtr<nsIOutputStream> strm;
+ nsresult rv = MsgNewSafeBufferedFileOutputStream(getter_AddRefs(strm),
+ filterFile, -1, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterList->SaveToFile(strm);
+
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(strm);
+ NS_ASSERTION(safeStream, "expected a safe output stream!");
+ if (safeStream) {
+ rv = safeStream->Finish();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to save filter file! possible data loss");
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, ("Save of list failed"));
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgFilterService::CancelFilterList(
+ nsIMsgFilterList* filterList) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgFilterService::BackUpFilterFile(nsIFile* aFilterFile,
+ nsIMsgWindow* aMsgWindow) {
+ AlertBackingUpFilterFile(aMsgWindow);
+
+ nsCOMPtr<nsIFile> localParentDir;
+ nsresult rv = aFilterFile->GetParent(getter_AddRefs(localParentDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if back-up file exists delete the back up file otherwise copy fails.
+ nsCOMPtr<nsIFile> backupFile;
+ rv = localParentDir->Clone(getter_AddRefs(backupFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ backupFile->AppendNative("rulesbackup.dat"_ns);
+ bool exists;
+ backupFile->Exists(&exists);
+ if (exists) backupFile->Remove(false);
+
+ return aFilterFile->CopyToNative(localParentDir, "rulesbackup.dat"_ns);
+}
+
+nsresult nsMsgFilterService::AlertBackingUpFilterFile(
+ nsIMsgWindow* aMsgWindow) {
+ return ThrowAlertMsg("filterListBackUpMsg", aMsgWindow);
+}
+
+// Do not use this routine if you have to call it very often because it creates
+// a new bundle each time.
+nsresult nsMsgFilterService::GetStringFromBundle(const char* aMsgName,
+ nsAString& aResult) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetFilterStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle)
+ rv = bundle->GetStringFromName(aMsgName, aResult);
+ return rv;
+}
+
+nsresult nsMsgFilterService::GetFilterStringBundle(nsIStringBundle** aBundle) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ if (bundleService)
+ bundleService->CreateBundle("chrome://messenger/locale/filter.properties",
+ getter_AddRefs(bundle));
+ bundle.forget(aBundle);
+ return NS_OK;
+}
+
+nsresult nsMsgFilterService::ThrowAlertMsg(const char* aMsgName,
+ nsIMsgWindow* aMsgWindow) {
+ nsString alertString;
+ nsresult rv = GetStringFromBundle(aMsgName, alertString);
+ nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow;
+ if (!msgWindow) {
+ nsCOMPtr<nsIMsgMailSession> mailSession(
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv));
+ if (NS_SUCCEEDED(rv))
+ rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow));
+ }
+
+ if (NS_SUCCEEDED(rv) && !alertString.IsEmpty() && msgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog && !alertString.IsEmpty())
+ dialog->Alert(nullptr, alertString.get());
+ }
+ }
+ return rv;
+}
+
+// this class is used to run filters after the fact, i.e., after new mail has
+// been downloaded from the server. It can do the following:
+// 1. Apply a single imap or pop3 filter on a single folder.
+// 2. Apply multiple filters on a single imap or pop3 folder.
+// 3. Apply a single filter on multiple imap or pop3 folders in the same
+// account.
+// 4. Apply multiple filters on multiple imap or pop3 folders in the same
+// account.
+// This will be called from the front end js code in the case of the
+// apply filters to folder menu code, and from the filter dialog js code with
+// the run filter now command.
+
+// this class holds the list of filters and folders, and applies them in turn,
+// first iterating over all the filters on one folder, and then advancing to the
+// next folder and repeating. For each filter,we take the filter criteria and
+// create a search term list. Then, we execute the search. We are a search
+// listener so that we can build up the list of search hits. Then, when the
+// search is done, we will apply the filter action(s) en-masse, so, for example,
+// if the action is a move, we calls one method to move all the messages to the
+// destination folder. Or, mark all the messages read. In the case of imap
+// operations, or imap/local moves, the action will be asynchronous, so we'll
+// need to be a url listener as well, and kick off the next filter when the
+// action completes.
+class nsMsgFilterAfterTheFact : public nsIUrlListener,
+ public nsIMsgSearchNotify,
+ public nsIMsgCopyServiceListener {
+ public:
+ nsMsgFilterAfterTheFact(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList* aFilterList,
+ const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList,
+ nsIMsgOperationListener* aCallback);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSEARCHNOTIFY
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ nsresult AdvanceToNextFolder(); // kicks off the process
+ protected:
+ virtual ~nsMsgFilterAfterTheFact();
+ virtual nsresult RunNextFilter();
+ /**
+ * apply filter actions to current search hits
+ */
+ nsresult ApplyFilter();
+ nsresult OnEndExecution(); // do what we have to do to cleanup.
+ bool ContinueExecutionPrompt();
+ nsresult DisplayConfirmationPrompt(nsIMsgWindow* msgWindow,
+ const char16_t* confirmString,
+ bool* confirmed);
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgFilterList> m_filters;
+ nsTArray<RefPtr<nsIMsgFolder>> m_folders;
+ nsCOMPtr<nsIMsgFolder> m_curFolder;
+ nsCOMPtr<nsIMsgDatabase> m_curFolderDB;
+ nsCOMPtr<nsIMsgFilter> m_curFilter;
+ uint32_t m_curFilterIndex;
+ uint32_t m_curFolderIndex;
+ uint32_t m_numFilters;
+ nsTArray<nsMsgKey> m_searchHits;
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_searchHitHdrs;
+ nsTArray<nsMsgKey> m_stopFiltering;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ nsCOMPtr<nsIMsgOperationListener> m_callback;
+ uint32_t m_nextAction; // next filter action to perform
+ nsresult mFinalResult; // report of overall success or failure
+ bool mNeedsRelease; // Did we need to release ourself?
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFilterAfterTheFact, nsIUrlListener, nsIMsgSearchNotify,
+ nsIMsgCopyServiceListener)
+
+nsMsgFilterAfterTheFact::nsMsgFilterAfterTheFact(
+ nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList,
+ const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList,
+ nsIMsgOperationListener* aCallback) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("(Post) nsMsgFilterAfterTheFact"));
+ m_curFilterIndex = m_curFolderIndex = m_nextAction = 0;
+ m_msgWindow = aMsgWindow;
+ m_filters = aFilterList;
+ m_folders = aFolderList.Clone();
+ m_filters->GetFilterCount(&m_numFilters);
+
+ NS_ADDREF_THIS(); // we own ourselves, and will release ourselves when
+ // execution is done.
+ mNeedsRelease = true;
+
+ m_callback = aCallback;
+ mFinalResult = NS_OK;
+}
+
+nsMsgFilterAfterTheFact::~nsMsgFilterAfterTheFact() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) ~nsMsgFilterAfterTheFact"));
+}
+
+// do what we have to do to cleanup.
+nsresult nsMsgFilterAfterTheFact::OnEndExecution() {
+ if (m_searchSession) m_searchSession->UnregisterListener(this);
+
+ if (m_filters) (void)m_filters->FlushLogIfNecessary();
+
+ if (m_callback) (void)m_callback->OnStopOperation(mFinalResult);
+
+ nsresult rv = mFinalResult;
+ // OnEndExecution() can be called a second time when a rule execution fails
+ // and the user is prompted whether he wants to continue.
+ if (mNeedsRelease) {
+ NS_RELEASE_THIS(); // release ourselves.
+ mNeedsRelease = false;
+ }
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("(Post) End executing filters"));
+ return rv;
+}
+
+nsresult nsMsgFilterAfterTheFact::RunNextFilter() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgFilterAfterTheFact::RunNextFilter"));
+ nsresult rv = NS_OK;
+ while (true) {
+ m_curFilter = nullptr;
+ if (m_curFilterIndex >= m_numFilters) break;
+
+ BREAK_IF_FALSE(m_filters, "Missing filters");
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Running filter %" PRIu32, m_curFilterIndex));
+
+ rv =
+ m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter));
+ CONTINUE_IF_FAILURE(rv, "Could not get filter at index");
+
+ nsString filterName;
+ m_curFilter->GetFilterName(filterName);
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get()));
+ // clang-format on
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = m_curFilter->GetSearchTerms(searchTerms);
+ CONTINUE_IF_FAILURE(rv, "Could not get searchTerms");
+
+ if (m_searchSession) m_searchSession->UnregisterListener(this);
+ m_searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ BREAK_IF_FAILURE(rv, "Failed to get search session");
+
+ nsMsgSearchScopeValue searchScope = nsMsgSearchScope::offlineMail;
+ for (nsIMsgSearchTerm* term : searchTerms) {
+ rv = m_searchSession->AppendTerm(term);
+ BREAK_IF_FAILURE(rv, "Could not append search term");
+ }
+ CONTINUE_IF_FAILURE(rv, "Failed to setup search terms");
+ m_searchSession->RegisterListener(this,
+ nsIMsgSearchSession::allNotifications);
+
+ rv = m_searchSession->AddScopeTerm(searchScope, m_curFolder);
+ CONTINUE_IF_FAILURE(rv, "Failed to add scope term");
+ m_nextAction = 0;
+ rv = m_searchSession->Search(m_msgWindow);
+ CONTINUE_IF_FAILURE(rv, "Search failed");
+ return NS_OK; // OnSearchDone will continue
+ }
+
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Post) Filter evaluation failed"));
+ m_filters->LogFilterMessage(u"Filter evaluation failed"_ns, m_curFilter);
+ }
+
+ m_curFilter = nullptr;
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Search failed");
+ return AdvanceToNextFolder();
+}
+
+nsresult nsMsgFilterAfterTheFact::AdvanceToNextFolder() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgFilterAfterTheFact::AdvanceToNextFolder"));
+ nsresult rv = NS_OK;
+ // Advance through folders, making sure m_curFolder is null on errors
+ while (true) {
+ m_stopFiltering.Clear();
+ m_curFolder = nullptr;
+ if (m_curFolderIndex >= m_folders.Length()) {
+ // final end of nsMsgFilterAfterTheFact object
+ return OnEndExecution();
+ }
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Entering folder %" PRIu32, m_curFolderIndex));
+
+ // reset the filter index to apply all filters to this new folder
+ m_curFilterIndex = 0;
+ m_nextAction = 0;
+ m_curFolder = m_folders[m_curFolderIndex++];
+
+ // Note: I got rv = NS_OK but null m_curFolder after deleting a folder
+ // outside of TB, when I select a single message and "run filter on message"
+ // and the filter is to move the message to the deleted folder.
+
+ // m_curFolder may be null when the folder is deleted externally.
+ CONTINUE_IF_FALSE(m_curFolder, "Next folder returned null");
+
+ nsString folderName;
+ (void)m_curFolder->GetName(folderName);
+ MOZ_LOG(
+ FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Folder name: %s", NS_ConvertUTF16toUTF8(folderName).get()));
+
+ nsCOMPtr<nsIFile> folderPath;
+ (void)m_curFolder->GetFilePath(getter_AddRefs(folderPath));
+ (void)folderPath->GetPath(folderName);
+ MOZ_LOG(
+ FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Folder path: %s", NS_ConvertUTF16toUTF8(folderName).get()));
+
+ rv = m_curFolder->GetMsgDatabase(getter_AddRefs(m_curFolderDB));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(m_curFolder, &rv);
+ if (NS_SUCCEEDED(rv) && localFolder)
+ // will continue with OnStopRunningUrl
+ return localFolder->ParseFolder(m_msgWindow, this);
+ }
+ CONTINUE_IF_FAILURE(rv, "Could not get folder db");
+
+ rv = RunNextFilter();
+ // RunNextFilter returns success when either filters are done, or an async
+ // process has started. It will call AdvanceToNextFolder itself if possible,
+ // so no need to call here.
+ BREAK_IF_FAILURE(rv, "Failed to run next filter");
+ break;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartRunningUrl(nsIURI* aUrl) {
+ return NS_OK;
+}
+
+// This is the return from a folder parse
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopRunningUrl(nsIURI* aUrl,
+ nsresult aExitCode) {
+ if (NS_SUCCEEDED(aExitCode)) return RunNextFilter();
+
+ mFinalResult = aExitCode;
+ // If m_msgWindow then we are in a context where the user can deal with
+ // errors. Put up a prompt, and exit if user wants.
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution();
+
+ // folder parse failed, so stop processing this folder.
+ return AdvanceToNextFolder();
+}
+
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchHit(nsIMsgDBHdr* header,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG_POINTER(header);
+
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+
+ nsCString msgId;
+ header->GetMessageId(getter_Copies(msgId));
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Filter matched message with key %" PRIu32,
+ msgKeyToInt(msgKey)));
+ // clang-format on
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Matched message ID: %s", msgId.get()));
+
+ // Under various previous actions (a move, delete, or stopExecution)
+ // we do not want to process filters on a per-message basis.
+ if (m_stopFiltering.Contains(msgKey)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Stopping further filter execution on this message"));
+ return NS_OK;
+ }
+
+ m_searchHits.AppendElement(msgKey);
+ m_searchHitHdrs.AppendElement(header);
+ return NS_OK;
+}
+
+// Continue after an async operation.
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchDone(nsresult status) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Done matching current filter"));
+ if (NS_SUCCEEDED(status))
+ return m_searchHits.IsEmpty() ? RunNextFilter() : ApplyFilter();
+
+ mFinalResult = status;
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution();
+
+ // The search failed, so move on to the next filter.
+ return RunNextFilter();
+}
+
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnNewSearch() {
+ m_searchHits.Clear();
+ m_searchHitHdrs.Clear();
+ return NS_OK;
+}
+
+// This method will apply filters. It will continue to advance though headers,
+// filters, and folders until done, unless it starts an async operation with
+// a callback. The callback should call ApplyFilter again. It only returns
+// an error if it is impossible to continue after attempting to continue the
+// next filter action, filter, or folder.
+nsresult nsMsgFilterAfterTheFact::ApplyFilter() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgFilterAfterTheFact::ApplyFilter"));
+ nsresult rv;
+ do {
+ // Error management block, break if unable to continue with filter.
+
+ if (!m_curFilter)
+ break; // Maybe not an error, we just need to call RunNextFilter();
+ if (!m_curFolder)
+ break; // Maybe not an error, we just need to call AdvanceToNextFolder();
+
+ // 'm_curFolder' can be reset asynchronously by the copy service
+ // calling OnStopCopy(). So take a local copy here and use it throughout the
+ // function.
+ nsCOMPtr<nsIMsgFolder> curFolder = m_curFolder;
+ nsCOMPtr<nsIMsgFilter> curFilter = m_curFilter;
+
+ // We're going to log the filter actions before firing them because some
+ // actions are async.
+ bool loggingEnabled = false;
+ if (m_filters) (void)m_filters->GetLoggingEnabled(&loggingEnabled);
+
+ nsTArray<RefPtr<nsIMsgRuleAction>> actionList;
+ rv = curFilter->GetSortedActionList(actionList);
+ BREAK_IF_FAILURE(rv, "Could not get action list for filter");
+
+ uint32_t numActions = actionList.Length();
+
+ if (m_nextAction == 0) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Applying %" PRIu32 " filter actions to %" PRIu32
+ " matched messages",
+ numActions, static_cast<uint32_t>(m_searchHits.Length())));
+ } else if (m_nextAction < numActions) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Applying remaining %" PRIu32
+ " filter actions to %" PRIu32 " matched messages",
+ numActions - m_nextAction,
+ static_cast<uint32_t>(m_searchHits.Length())));
+ }
+
+ // We start from m_nextAction to allow us to continue applying actions
+ // after the return from an async copy.
+ while (m_nextAction < numActions) {
+ nsresult finalResult = NS_OK;
+ nsCOMPtr<nsIMsgRuleAction> filterAction(actionList[m_nextAction]);
+ ++m_nextAction;
+
+ nsMsgRuleActionType actionType;
+ rv = filterAction->GetType(&actionType);
+ CONTINUE_IF_FAILURE(rv, "Could not get type for filter action");
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Running filter action at index %" PRIu32
+ ", action type = %i",
+ m_nextAction - 1, actionType));
+
+ nsCString actionTargetFolderUri;
+ if (actionType == nsMsgFilterAction::MoveToFolder ||
+ actionType == nsMsgFilterAction::CopyToFolder) {
+ rv = filterAction->GetTargetFolderUri(actionTargetFolderUri);
+ CONTINUE_IF_FAILURE(rv, "GetTargetFolderUri failed");
+ CONTINUE_IF_FALSE(!actionTargetFolderUri.IsEmpty(),
+ "actionTargetFolderUri is empty");
+ }
+
+ if (loggingEnabled) {
+ for (auto msgHdr : m_searchHitHdrs) {
+ (void)curFilter->LogRuleHit(filterAction, msgHdr);
+ }
+ }
+
+ // all actions that pass "this" as a listener in order to chain filter
+ // execution when the action is finished need to return before reaching
+ // the bottom of this routine, because we run the next filter at the end
+ // of this routine.
+ switch (actionType) {
+ case nsMsgFilterAction::Delete:
+ // we can't pass ourselves in as a copy service listener because the
+ // copy service listener won't get called in several situations (e.g.,
+ // the delete model is imap delete) and we rely on the listener
+ // getting called to continue the filter application. This means we're
+ // going to end up firing off the delete, and then subsequently
+ // issuing a search for the next filter, which will block until the
+ // delete finishes.
+ rv = curFolder->DeleteMessages(m_searchHitHdrs, m_msgWindow, false,
+ false, nullptr, false /*allow Undo*/);
+ BREAK_ACTION_IF_FAILURE(rv, "Deleting messages failed");
+
+ // don't allow any more filters on this message
+ m_stopFiltering.AppendElements(m_searchHits);
+ for (uint32_t i = 0; i < m_searchHits.Length(); i++)
+ curFolder->OrProcessingFlags(m_searchHits[i],
+ nsMsgProcessingFlags::FilterToMove);
+ // if we are deleting then we couldn't care less about applying
+ // remaining filter actions
+ m_nextAction = numActions;
+ break;
+
+ case nsMsgFilterAction::MoveToFolder:
+ // Even if move fails we will not run additional actions, as they
+ // would not have run if move succeeded.
+ m_nextAction = numActions;
+ // Fall through to the copy case.
+ [[fallthrough]];
+ case nsMsgFilterAction::CopyToFolder: {
+ nsCString uri;
+ curFolder->GetURI(uri);
+
+ if (uri.Equals(actionTargetFolderUri)) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Target folder is the same as source folder, "
+ "skipping"));
+ break;
+ }
+
+ nsCOMPtr<nsIMsgFolder> destIFolder;
+ rv = GetOrCreateFolder(actionTargetFolderUri,
+ getter_AddRefs(destIFolder));
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get action folder");
+
+ bool canFileMessages = true;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ destIFolder->GetParent(getter_AddRefs(parentFolder));
+ if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages);
+ if (!parentFolder || !canFileMessages) {
+ curFilter->SetEnabled(false);
+ destIFolder->ThrowAlertMsg("filterDisabled", m_msgWindow);
+ // we need to explicitly save the filter file.
+ m_filters->SaveToDefaultFile();
+ // In the case of applying multiple filters
+ // we might want to remove the filter from the list, but
+ // that's a bit evil since we really don't know that we own
+ // the list. Disabling it doesn't do a lot of good since
+ // we still apply disabled filters. Currently, we don't
+ // have any clients that apply filters to multiple folders,
+ // so this might be the edge case of an edge case.
+ m_nextAction = numActions;
+ BREAK_ACTION_IF_FALSE(false,
+ "No parent folder or folder can't file "
+ "messages, disabling the filter");
+ }
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get copy service");
+
+ if (actionType == nsMsgFilterAction::MoveToFolder) {
+ m_stopFiltering.AppendElements(m_searchHits);
+ for (uint32_t i = 0; i < m_searchHits.Length(); i++)
+ curFolder->OrProcessingFlags(m_searchHits[i],
+ nsMsgProcessingFlags::FilterToMove);
+ }
+
+ rv = copyService->CopyMessages(
+ curFolder, m_searchHitHdrs, destIFolder,
+ actionType == nsMsgFilterAction::MoveToFolder, this, m_msgWindow,
+ false);
+ BREAK_ACTION_IF_FAILURE(rv, "CopyMessages failed");
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Action execution continues async"));
+ return NS_OK; // OnStopCopy callback to continue;
+ } break;
+ case nsMsgFilterAction::MarkRead:
+ // crud, no listener support here - we'll probably just need to go on
+ // and apply the next filter, and, in the imap case, rely on multiple
+ // connection and url queueing to stay out of trouble
+ rv = curFolder->MarkMessagesRead(m_searchHitHdrs, true);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ break;
+ case nsMsgFilterAction::MarkUnread:
+ rv = curFolder->MarkMessagesRead(m_searchHitHdrs, false);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ break;
+ case nsMsgFilterAction::MarkFlagged:
+ rv = curFolder->MarkMessagesFlagged(m_searchHitHdrs, true);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ break;
+ case nsMsgFilterAction::KillThread:
+ case nsMsgFilterAction::WatchThread: {
+ for (auto msgHdr : m_searchHitHdrs) {
+ nsCOMPtr<nsIMsgThread> msgThread;
+ nsMsgKey threadKey;
+ m_curFolderDB->GetThreadContainingMsgHdr(msgHdr,
+ getter_AddRefs(msgThread));
+ BREAK_ACTION_IF_FALSE(msgThread, "Could not find msg thread");
+ msgThread->GetThreadKey(&threadKey);
+ if (actionType == nsMsgFilterAction::KillThread) {
+ rv = m_curFolderDB->MarkThreadIgnored(msgThread, threadKey, true,
+ nullptr);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ } else {
+ rv = m_curFolderDB->MarkThreadWatched(msgThread, threadKey, true,
+ nullptr);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ }
+ }
+ } break;
+ case nsMsgFilterAction::KillSubthread: {
+ for (auto msgHdr : m_searchHitHdrs) {
+ rv = m_curFolderDB->MarkHeaderKilled(msgHdr, true, nullptr);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ }
+ } break;
+ case nsMsgFilterAction::ChangePriority: {
+ nsMsgPriorityValue filterPriority;
+ filterAction->GetPriority(&filterPriority);
+ for (auto msgHdr : m_searchHitHdrs) {
+ rv = msgHdr->SetPriority(filterPriority);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ }
+ } break;
+ case nsMsgFilterAction::AddTag: {
+ nsCString keyword;
+ filterAction->GetStrValue(keyword);
+ rv = curFolder->AddKeywordsToMessages(m_searchHitHdrs, keyword);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ } break;
+ case nsMsgFilterAction::JunkScore: {
+ nsAutoCString junkScoreStr;
+ int32_t junkScore;
+ filterAction->GetJunkScore(&junkScore);
+ junkScoreStr.AppendInt(junkScore);
+ rv =
+ curFolder->SetJunkScoreForMessages(m_searchHitHdrs, junkScoreStr);
+ BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed");
+ } break;
+ case nsMsgFilterAction::Forward: {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = curFolder->GetServer(getter_AddRefs(server));
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get server");
+ nsCString forwardTo;
+ filterAction->GetStrValue(forwardTo);
+ BREAK_ACTION_IF_FALSE(!forwardTo.IsEmpty(), "blank forwardTo URI");
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service");
+
+ for (auto msgHdr : m_searchHitHdrs) {
+ rv = compService->ForwardMessage(
+ NS_ConvertASCIItoUTF16(forwardTo), msgHdr, m_msgWindow, server,
+ nsIMsgComposeService::kForwardAsDefault);
+ BREAK_ACTION_IF_FAILURE(rv, "Forward action failed");
+ }
+ } break;
+ case nsMsgFilterAction::Reply: {
+ nsCString replyTemplateUri;
+ filterAction->GetStrValue(replyTemplateUri);
+ BREAK_ACTION_IF_FALSE(!replyTemplateUri.IsEmpty(),
+ "Empty reply template URI");
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = curFolder->GetServer(getter_AddRefs(server));
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get server");
+
+ nsCOMPtr<nsIMsgComposeService> compService =
+ do_GetService("@mozilla.org/messengercompose;1", &rv);
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service");
+ for (auto msgHdr : m_searchHitHdrs) {
+ rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri,
+ m_msgWindow, server);
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_ABORT) {
+ (void)curFilter->LogRuleHitFail(
+ filterAction, msgHdr, rv,
+ "filterFailureSendingReplyAborted"_ns);
+ } else {
+ (void)curFilter->LogRuleHitFail(
+ filterAction, msgHdr, rv,
+ "filterFailureSendingReplyError"_ns);
+ }
+ }
+ BREAK_ACTION_IF_FAILURE(rv, "ReplyWithTemplate failed");
+ }
+ } break;
+ case nsMsgFilterAction::DeleteFromPop3Server: {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(curFolder, &rv);
+ BREAK_ACTION_IF_FAILURE(rv, "Current folder not a local folder");
+ BREAK_ACTION_IF_FALSE(localFolder,
+ "Current folder not a local folder");
+ // This action ignores the deleteMailLeftOnServer preference
+ rv = localFolder->MarkMsgsOnPop3Server(m_searchHitHdrs,
+ POP3_FORCE_DEL);
+ BREAK_ACTION_IF_FAILURE(rv, "MarkMsgsOnPop3Server failed");
+
+ // Delete the partial headers. They're useless now
+ // that the server copy is being deleted.
+ nsTArray<RefPtr<nsIMsgDBHdr>> partialMsgs;
+ for (uint32_t i = 0; i < m_searchHits.Length(); ++i) {
+ nsIMsgDBHdr* msgHdr = m_searchHitHdrs[i];
+ nsMsgKey msgKey = m_searchHits[i];
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) {
+ partialMsgs.AppendElement(msgHdr);
+ m_stopFiltering.AppendElement(msgKey);
+ curFolder->OrProcessingFlags(msgKey,
+ nsMsgProcessingFlags::FilterToMove);
+ }
+ }
+ if (!partialMsgs.IsEmpty()) {
+ rv = curFolder->DeleteMessages(partialMsgs, m_msgWindow, true,
+ false, nullptr, false);
+ BREAK_ACTION_IF_FAILURE(rv, "Delete messages failed");
+ }
+ } break;
+ case nsMsgFilterAction::FetchBodyFromPop3Server: {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(curFolder, &rv);
+ BREAK_ACTION_IF_FAILURE(rv, "current folder not local");
+ BREAK_ACTION_IF_FALSE(localFolder, "current folder not local");
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ for (nsIMsgDBHdr* msgHdr : m_searchHitHdrs) {
+ uint32_t flags = 0;
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial)
+ messages.AppendElement(msgHdr);
+ }
+ if (messages.Length() > 0) {
+ rv = curFolder->DownloadMessagesForOffline(messages, m_msgWindow);
+ BREAK_ACTION_IF_FAILURE(rv, "DownloadMessagesForOffline failed");
+ }
+ } break;
+
+ case nsMsgFilterAction::StopExecution: {
+ // don't apply any more filters
+ m_stopFiltering.AppendElements(m_searchHits);
+ m_nextAction = numActions;
+ } break;
+
+ case nsMsgFilterAction::Custom: {
+ nsMsgFilterTypeType filterType;
+ curFilter->GetFilterType(&filterType);
+ nsCOMPtr<nsIMsgFilterCustomAction> customAction;
+ rv = filterAction->GetCustomAction(getter_AddRefs(customAction));
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action");
+
+ nsAutoCString value;
+ rv = filterAction->GetStrValue(value);
+ BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action value");
+ bool isAsync = false;
+ customAction->GetIsAsync(&isAsync);
+ rv = customAction->ApplyAction(m_searchHitHdrs, value, this,
+ filterType, m_msgWindow);
+ BREAK_ACTION_IF_FAILURE(rv, "custom action failed to apply");
+ if (isAsync) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Action execution continues async"));
+ return NS_OK; // custom action should call ApplyFilter on callback
+ }
+ } break;
+
+ default:
+ NS_ERROR("unexpected filter action");
+ BREAK_ACTION_IF_FAILURE(NS_ERROR_UNEXPECTED,
+ "Unexpected filter action");
+ }
+ if (NS_FAILED(finalResult)) {
+ mFinalResult = finalResult;
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Post) Action execution failed with error: %" PRIx32,
+ static_cast<uint32_t>(mFinalResult)));
+ if (loggingEnabled && m_searchHitHdrs.Length() > 0) {
+ (void)curFilter->LogRuleHitFail(filterAction, m_searchHitHdrs[0],
+ mFinalResult,
+ "filterActionFailed"_ns);
+ }
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Action execution succeeded"));
+ }
+ }
+ } while (false); // end error management block
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Finished executing actions"));
+ return RunNextFilter();
+}
+
+NS_IMETHODIMP nsMsgFilterService::GetTempFilterList(
+ nsIMsgFolder* aFolder, nsIMsgFilterList** aFilterList) {
+ NS_ENSURE_ARG_POINTER(aFilterList);
+
+ nsMsgFilterList* filterList = new nsMsgFilterList;
+ filterList->SetFolder(aFolder);
+ filterList->m_temporaryList = true;
+ NS_ADDREF(*aFilterList = filterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterService::ApplyFiltersToFolders(
+ nsIMsgFilterList* aFilterList,
+ const nsTArray<RefPtr<nsIMsgFolder>>& aFolders, nsIMsgWindow* aMsgWindow,
+ nsIMsgOperationListener* aCallback) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgFilterService::ApplyFiltersToFolders"));
+ NS_ENSURE_ARG_POINTER(aFilterList);
+
+ uint32_t filterCount;
+ aFilterList->GetFilterCount(&filterCount);
+ nsCString listId;
+ aFilterList->GetListId(listId);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Manual filter run initiated"));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32 " folders",
+ filterCount, listId.get(), (int)aFolders.Length()));
+
+ RefPtr<nsMsgFilterAfterTheFact> filterExecutor =
+ new nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolders, aCallback);
+ if (filterExecutor)
+ return filterExecutor->AdvanceToNextFolder();
+ else
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsMsgFilterService::AddCustomAction(
+ nsIMsgFilterCustomAction* aAction) {
+ mCustomActions.AppendElement(aAction);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterService::GetCustomActions(
+ nsTArray<RefPtr<nsIMsgFilterCustomAction>>& actions) {
+ actions = mCustomActions.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterService::GetCustomAction(const nsACString& aId,
+ nsIMsgFilterCustomAction** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ for (nsIMsgFilterCustomAction* action : mCustomActions) {
+ nsAutoCString id;
+ nsresult rv = action->GetId(id);
+ if (NS_SUCCEEDED(rv) && aId.Equals(id)) {
+ NS_ADDREF(*aResult = action);
+ return NS_OK;
+ }
+ }
+ aResult = nullptr;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgFilterService::AddCustomTerm(nsIMsgSearchCustomTerm* aTerm) {
+ mCustomTerms.AppendElement(aTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterService::GetCustomTerms(
+ nsTArray<RefPtr<nsIMsgSearchCustomTerm>>& terms) {
+ terms = mCustomTerms.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFilterService::GetCustomTerm(const nsACString& aId,
+ nsIMsgSearchCustomTerm** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ for (nsIMsgSearchCustomTerm* term : mCustomTerms) {
+ nsAutoCString id;
+ nsresult rv = term->GetId(id);
+ if (NS_SUCCEEDED(rv) && aId.Equals(id)) {
+ NS_ADDREF(*aResult = term);
+ return NS_OK;
+ }
+ }
+ aResult = nullptr;
+ // we use a null result to indicate failure to find a term
+ return NS_OK;
+}
+
+/**
+ * Translate the filter type flag into human readable type names.
+ * In case of multiple flag they are delimited by '&'.
+ */
+NS_IMETHODIMP
+nsMsgFilterService::FilterTypeName(nsMsgFilterTypeType filterType,
+ nsACString& typeName) {
+ typeName.Truncate();
+ if (filterType == nsMsgFilterType::None) {
+ typeName.Assign("None");
+ return NS_OK;
+ }
+
+ if ((filterType & nsMsgFilterType::Incoming) == nsMsgFilterType::Incoming) {
+ typeName.Append("Incoming&");
+ } else {
+ if ((filterType & nsMsgFilterType::Inbox) == nsMsgFilterType::Inbox) {
+ typeName.Append("Inbox&");
+ } else {
+ if (filterType & nsMsgFilterType::InboxRule)
+ typeName.Append("InboxRule&");
+ if (filterType & nsMsgFilterType::InboxJavaScript)
+ typeName.Append("InboxJavaScript&");
+ }
+ if ((filterType & nsMsgFilterType::News) == nsMsgFilterType::News) {
+ typeName.Append("News&");
+ } else {
+ if (filterType & nsMsgFilterType::NewsRule) typeName.Append("NewsRule&");
+ if (filterType & nsMsgFilterType::NewsJavaScript)
+ typeName.Append("NewsJavaScript&");
+ }
+ }
+ if (filterType & nsMsgFilterType::Manual) typeName.Append("Manual&");
+ if (filterType & nsMsgFilterType::PostPlugin) typeName.Append("PostPlugin&");
+ if (filterType & nsMsgFilterType::PostOutgoing)
+ typeName.Append("PostOutgoing&");
+ if (filterType & nsMsgFilterType::Archive) typeName.Append("Archive&");
+ if (filterType & nsMsgFilterType::Periodic) typeName.Append("Periodic&");
+
+ if (typeName.IsEmpty()) {
+ typeName.Assign("UNKNOWN");
+ } else {
+ // Cut the trailing '&' character.
+ typeName.Truncate(typeName.Length() - 1);
+ }
+ return NS_OK;
+}
+
+// nsMsgApplyFiltersToMessages overrides nsMsgFilterAfterTheFact in order to
+// apply filters to a list of messages, rather than an entire folder
+class nsMsgApplyFiltersToMessages : public nsMsgFilterAfterTheFact {
+ public:
+ nsMsgApplyFiltersToMessages(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList* aFilterList,
+ const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList,
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList,
+ nsMsgFilterTypeType aFilterType,
+ nsIMsgOperationListener* aCallback);
+
+ protected:
+ virtual nsresult RunNextFilter();
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_msgHdrList;
+ nsMsgFilterTypeType m_filterType;
+};
+
+nsMsgApplyFiltersToMessages::nsMsgApplyFiltersToMessages(
+ nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList,
+ const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList,
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList,
+ nsMsgFilterTypeType aFilterType, nsIMsgOperationListener* aCallback)
+ : nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolderList, aCallback),
+ m_msgHdrList(aMsgHdrList.Clone()),
+ m_filterType(aFilterType) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgApplyFiltersToMessages"));
+}
+
+nsresult nsMsgApplyFiltersToMessages::RunNextFilter() {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgApplyFiltersToMessages::RunNextFilter"));
+ nsresult rv = NS_OK;
+ while (true) {
+ m_curFilter = nullptr; // we are done with the current filter
+ if (!m_curFolder || // Not an error, we just need to run
+ // AdvanceToNextFolder()
+ m_curFilterIndex >= m_numFilters)
+ break;
+
+ BREAK_IF_FALSE(m_filters, "No filters");
+ nsMsgFilterTypeType filterType;
+ bool isEnabled;
+ rv =
+ m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter));
+ CONTINUE_IF_FAILURE(rv, "Could not get filter");
+ rv = m_curFilter->GetFilterType(&filterType);
+ CONTINUE_IF_FAILURE(rv, "Could not get filter type");
+ if (!(filterType & m_filterType)) continue;
+ rv = m_curFilter->GetEnabled(&isEnabled);
+ CONTINUE_IF_FAILURE(rv, "Could not get isEnabled");
+ if (!isEnabled) continue;
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) Running filter %" PRIu32, m_curFilterIndex));
+ nsString filterName;
+ m_curFilter->GetFilterName(filterName);
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get()));
+ // clang-format on
+
+ nsCOMPtr<nsIMsgSearchScopeTerm> scope(new nsMsgSearchScopeTerm(
+ nullptr, nsMsgSearchScope::offlineMail, m_curFolder));
+ BREAK_IF_FALSE(scope, "Could not create scope, OOM?");
+ m_curFilter->SetScope(scope);
+ OnNewSearch();
+
+ for (auto msgHdr : m_msgHdrList) {
+ bool matched;
+ rv = m_curFilter->MatchHdr(msgHdr, m_curFolder, m_curFolderDB,
+ EmptyCString(), &matched);
+ if (NS_SUCCEEDED(rv) && matched) {
+ // In order to work with nsMsgFilterAfterTheFact::ApplyFilter we
+ // initialize nsMsgFilterAfterTheFact's information with a search hit
+ // now for the message that we're filtering.
+ OnSearchHit(msgHdr, m_curFolder);
+ }
+ }
+ m_curFilter->SetScope(nullptr);
+
+ if (m_searchHits.Length() > 0) {
+ m_nextAction = 0;
+ rv = ApplyFilter();
+ if (NS_SUCCEEDED(rv))
+ return NS_OK; // async callback will continue, or we are done.
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Post) Filter run failed (%" PRIx32 ")",
+ static_cast<uint32_t>(rv)));
+ // clang-format on
+ m_filters->LogFilterMessage(u"Filter run failed"_ns, m_curFilter);
+ NS_WARNING_ASSERTION(false, "Failed to run filters");
+ } else {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Filter run finished on the current folder"));
+ }
+
+ m_curFilter = nullptr;
+
+ // We expect the failure is already recorded through one of the macro
+ // expressions, that will have console logging added to them.
+ // So an additional console warning is not needed here.
+ return AdvanceToNextFolder();
+}
+
+NS_IMETHODIMP nsMsgFilterService::ApplyFilters(
+ nsMsgFilterTypeType aFilterType,
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, nsIMsgFolder* aFolder,
+ nsIMsgWindow* aMsgWindow, nsIMsgOperationListener* aCallback) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
+ ("(Post) nsMsgApplyFiltersToMessages::ApplyFilters"));
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ aFolder->GetFilterList(aMsgWindow, getter_AddRefs(filterList));
+ NS_ENSURE_STATE(filterList);
+
+ uint32_t filterCount;
+ filterList->GetFilterCount(&filterCount);
+ nsCString listId;
+ filterList->GetListId(listId);
+ nsString folderName;
+ aFolder->GetName(folderName);
+ nsCString typeName;
+ FilterTypeName(aFilterType, typeName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Filter run initiated, trigger=%s (%i)", typeName.get(),
+ aFilterType));
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32
+ " message(s) in folder '%s'",
+ filterCount, listId.get(), (uint32_t)aMsgHdrList.Length(),
+ NS_ConvertUTF16toUTF8(folderName).get()));
+
+ // Create our nsMsgApplyFiltersToMessages object which will be called when
+ // ApplyFiltersToHdr finds one or more filters that hit.
+ RefPtr<nsMsgApplyFiltersToMessages> filterExecutor =
+ new nsMsgApplyFiltersToMessages(aMsgWindow, filterList, {aFolder},
+ aMsgHdrList, aFilterType, aCallback);
+
+ if (filterExecutor) return filterExecutor->AdvanceToNextFolder();
+
+ return NS_ERROR_OUT_OF_MEMORY;
+}
+
+/* void OnStartCopy (); */
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartCopy() { return NS_OK; }
+
+/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+/* void SetMessageKey (in uint32_t aKey); */
+NS_IMETHODIMP nsMsgFilterAfterTheFact::SetMessageKey(nsMsgKey /* aKey */) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilterAfterTheFact::GetMessageId(nsACString& messageId) {
+ return NS_OK;
+}
+
+/* void OnStopCopy (in nsresult aStatus); */
+NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopCopy(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("(Post) Async message copy from filter action finished successfully"));
+ // clang-format on
+ return ApplyFilter();
+ }
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
+ ("(Post) Async message copy from filter action failed (%" PRIx32 ")",
+ static_cast<uint32_t>(aStatus)));
+
+ mFinalResult = aStatus;
+ if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution();
+
+ // Copy failed, so run the next filter
+ return RunNextFilter();
+}
+
+bool nsMsgFilterAfterTheFact::ContinueExecutionPrompt() {
+ if (!m_curFilter) return false;
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ if (!bundleService) return false;
+ bundleService->CreateBundle("chrome://messenger/locale/filter.properties",
+ getter_AddRefs(bundle));
+ if (!bundle) return false;
+ nsString filterName;
+ m_curFilter->GetFilterName(filterName);
+ nsString formatString;
+ nsString confirmText;
+ AutoTArray<nsString, 1> formatStrings = {filterName};
+ nsresult rv = bundle->FormatStringFromName("continueFilterExecution",
+ formatStrings, confirmText);
+ if (NS_FAILED(rv)) return false;
+ bool returnVal = false;
+ (void)DisplayConfirmationPrompt(m_msgWindow, confirmText.get(), &returnVal);
+ if (!returnVal) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
+ ("(Post) User aborted further filter execution on prompt"));
+ }
+ return returnVal;
+}
+
+nsresult nsMsgFilterAfterTheFact::DisplayConfirmationPrompt(
+ nsIMsgWindow* msgWindow, const char16_t* confirmString, bool* confirmed) {
+ if (msgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog && confirmString)
+ dialog->Confirm(nullptr, confirmString, confirmed);
+ }
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/search/src/nsMsgFilterService.h b/comm/mailnews/search/src/nsMsgFilterService.h
new file mode 100644
index 0000000000..7089c8362f
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgFilterService.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgFilterService_H_
+#define _nsMsgFilterService_H_
+
+#include "nsIMsgFilterService.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+
+class nsIMsgWindow;
+class nsIStringBundle;
+
+// The filter service is used to acquire and manipulate filter lists.
+
+class nsMsgFilterService : public nsIMsgFilterService {
+ public:
+ nsMsgFilterService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFILTERSERVICE
+ // clients call OpenFilterList to get a handle to a FilterList, of existing
+ // nsMsgFilter. These are manipulated by the front end as a result of user
+ // interaction with dialog boxes. To apply the new list call
+ // MSG_CloseFilterList.
+
+ nsresult BackUpFilterFile(nsIFile* aFilterFile, nsIMsgWindow* aMsgWindow);
+ nsresult AlertBackingUpFilterFile(nsIMsgWindow* aMsgWindow);
+ nsresult ThrowAlertMsg(const char* aMsgName, nsIMsgWindow* aMsgWindow);
+ nsresult GetStringFromBundle(const char* aMsgName, nsAString& aResult);
+ nsresult GetFilterStringBundle(nsIStringBundle** aBundle);
+
+ protected:
+ virtual ~nsMsgFilterService();
+
+ // defined custom action list
+ nsTArray<RefPtr<nsIMsgFilterCustomAction>> mCustomActions;
+ // defined custom term list
+ nsTArray<RefPtr<nsIMsgSearchCustomTerm>> mCustomTerms;
+};
+
+#endif // _nsMsgFilterService_H_
diff --git a/comm/mailnews/search/src/nsMsgImapSearch.cpp b/comm/mailnews/search/src/nsMsgImapSearch.cpp
new file mode 100644
index 0000000000..9ca0183d60
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgImapSearch.cpp
@@ -0,0 +1,991 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "msgCore.h"
+#include "nsMsgSearchAdapter.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsMsgResultElement.h"
+#include "nsMsgSearchTerm.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgSearchImap.h"
+#include "prmem.h"
+#include "nsIMsgImapMailFolder.h"
+// Implementation of search for IMAP mail folders
+
+nsMsgSearchOnlineMail::nsMsgSearchOnlineMail(
+ nsMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList)
+ : nsMsgSearchAdapter(scope, termList) {}
+
+nsMsgSearchOnlineMail::~nsMsgSearchOnlineMail() {}
+
+nsresult nsMsgSearchOnlineMail::ValidateTerms() {
+ nsresult err = nsMsgSearchAdapter::ValidateTerms();
+
+ if (NS_SUCCEEDED(err)) {
+ // ### mwelch Figure out the charsets to use
+ // for the search terms and targets.
+ nsAutoString srcCharset, dstCharset;
+ GetSearchCharsets(srcCharset, dstCharset);
+
+ // do IMAP specific validation
+ err = Encode(m_encoding, m_searchTerms, dstCharset.get(), m_scope);
+ NS_ASSERTION(NS_SUCCEEDED(err), "failed to encode imap search");
+ }
+
+ return err;
+}
+
+NS_IMETHODIMP nsMsgSearchOnlineMail::GetEncoding(char** result) {
+ *result = ToNewCString(m_encoding);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchOnlineMail::AddResultElement(nsIMsgDBHdr* pHeaders) {
+ nsresult err = NS_OK;
+
+ nsCOMPtr<nsIMsgSearchSession> searchSession;
+ m_scope->GetSearchSession(getter_AddRefs(searchSession));
+ if (searchSession) {
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+ err = m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ searchSession->AddSearchHit(pHeaders, scopeFolder);
+ }
+ // XXXX alecf do not checkin without fixing!
+ // m_scope->m_searchSession->AddResultElement (newResult);
+ return err;
+}
+
+nsresult nsMsgSearchOnlineMail::Search(bool* aDone) {
+ // we should never end up here for a purely online
+ // folder. We might for an offline IMAP folder.
+ nsresult err = NS_ERROR_NOT_IMPLEMENTED;
+
+ return err;
+}
+
+nsresult nsMsgSearchOnlineMail::Encode(
+ nsCString& pEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms,
+ const char16_t* destCharset, nsIMsgSearchScopeTerm* scope) {
+ nsCString imapTerms;
+
+ // check if searchTerms are ascii only
+ bool asciiOnly = true;
+ // ### what's this mean in the NWO?????
+
+ if (true) // !(srcCharset & CODESET_MASK == STATEFUL || srcCharset &
+ // CODESET_MASK == WIDECHAR) )
+ // assume all single/multiple bytes charset has ascii as subset
+ {
+ for (nsIMsgSearchTerm* pTerm : searchTerms) {
+ nsMsgSearchAttribValue attribute;
+ pTerm->GetAttrib(&attribute);
+ if (IS_STRING_ATTRIBUTE(attribute)) {
+ nsString pchar;
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+
+ nsresult rv = pTerm->GetValue(getter_AddRefs(searchValue));
+ if (NS_FAILED(rv) || !searchValue) continue;
+
+ rv = searchValue->GetStr(pchar);
+ if (NS_FAILED(rv) || pchar.IsEmpty()) continue;
+ asciiOnly = mozilla::IsAsciiNullTerminated(
+ static_cast<const char16_t*>(pchar.get()));
+ if (!asciiOnly) {
+ break;
+ }
+ }
+ }
+ }
+ // else
+ // asciiOnly = false; // TODO: enable this line when the condition is not a
+ // plain "true" in the if().
+
+ const char16_t* usAsciiCharSet = u"us-ascii";
+ // Get the optional CHARSET parameter, in case we need it.
+ char* csname = GetImapCharsetParam(asciiOnly ? usAsciiCharSet : destCharset);
+
+ // We do not need "srcCharset" since the search term in always unicode.
+ // I just pass destCharset for both src and dest charset instead of removing
+ // srcCharst from the argument.
+ nsresult err = nsMsgSearchAdapter::EncodeImap(
+ getter_Copies(imapTerms), searchTerms,
+ asciiOnly ? usAsciiCharSet : destCharset,
+ asciiOnly ? usAsciiCharSet : destCharset, false);
+ if (NS_SUCCEEDED(err)) {
+ pEncoding.AppendLiteral("SEARCH");
+ if (csname) {
+ // We have a "CHARSET <name>" string which is typically appended to
+ // "SEARCH". But don't append it if server has UTF8=ACCEPT enabled.
+ nsCOMPtr<nsIMsgFolder> folder;
+ err = scope->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(err, err);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ bool utf8AcceptEnabled = false;
+ imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled);
+ if (!utf8AcceptEnabled) pEncoding.Append(csname);
+ }
+ pEncoding.Append(imapTerms);
+ }
+ PR_FREEIF(csname);
+ return err;
+}
+
+// clang-format off
+nsresult
+nsMsgSearchValidityManager::InitOfflineMailTable()
+{
+ NS_ASSERTION(!m_offlineMailTable, "offline mail table already initted");
+ nsresult rv = NewTable(getter_AddRefs(m_offlineMailTable));
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ // m_offlineMailTable->SetValidButNotShown (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsHigherThan, 1);
+ // m_offlineMailTable->SetValidButNotShown (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLowerThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ return rv;
+}
+
+
+nsresult
+nsMsgSearchValidityManager::InitOnlineMailTable()
+{
+ NS_ASSERTION(!m_onlineMailTable, "Online mail table already initted!");
+ nsresult rv = NewTable(getter_AddRefs(m_onlineMailTable));
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+
+ return rv;
+}
+
+nsresult
+nsMsgSearchValidityManager::InitOnlineMailFilterTable()
+{
+ // Oh what a tangled web...
+ //
+ // IMAP filtering happens on the client, fundamentally using the same
+ // capabilities as POP filtering. However, since we don't yet have the
+ // IMAP message body, we can't filter on body attributes. So this table
+ // is supposed to be the same as offline mail, except that the body
+ // attribute is omitted
+ NS_ASSERTION(!m_onlineMailFilterTable, "online filter table already initted");
+ nsresult rv = NewTable(getter_AddRefs(m_onlineMailFilterTable));
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+
+ return rv;
+}
+
+nsresult
+nsMsgSearchValidityManager::InitOfflineMailFilterTable()
+{
+ NS_ASSERTION(!m_offlineMailFilterTable, "offline mail filter table already initted");
+ nsresult rv = NewTable(getter_AddRefs(m_offlineMailFilterTable));
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ // junk status and attachment status not available for offline mail (POP) filters
+ // because we won't know those until after the message has been analyzed.
+ // see bug #185937
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+
+ return rv;
+}
+
+// Online Manual is used for IMAP and NEWS, where at manual
+// filtering we have junk info, but cannot assure that the
+// body is available.
+nsresult
+nsMsgSearchValidityManager::InitOnlineManualFilterTable()
+{
+ NS_ASSERTION(!m_onlineManualFilterTable, "online manual filter table already initted");
+ nsresult rv = NewTable(getter_AddRefs(m_onlineManualFilterTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+
+ // HasAttachmentStatus does not work reliably until the user has opened a
+ // message to force it through MIME. We need a solution for this (bug 105169)
+ // but in the meantime, I'm doing the same thing here that we do in the
+ // offline mail table, as this does not really depend at the moment on
+ // whether we have downloaded the body for offline use.
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+
+ return rv;
+}
+// clang-format on
diff --git a/comm/mailnews/search/src/nsMsgLocalSearch.cpp b/comm/mailnews/search/src/nsMsgLocalSearch.cpp
new file mode 100644
index 0000000000..e43c0dd9d9
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgLocalSearch.cpp
@@ -0,0 +1,919 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// Implementation of db search for POP and offline IMAP mail folders
+
+#include "msgCore.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgSearchCore.h"
+#include "nsMsgLocalSearch.h"
+#include "nsIStreamListener.h"
+#include "nsMsgSearchBoolExpression.h"
+#include "nsMsgSearchTerm.h"
+#include "nsMsgResultElement.h"
+#include "nsIDBFolderInfo.h"
+#include "nsMsgSearchValue.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgFolder.h"
+
+extern "C" {
+extern int MK_MSG_SEARCH_STATUS;
+extern int MK_MSG_CANT_SEARCH_IF_NO_SUMMARY;
+extern int MK_MSG_SEARCH_HITS_NOT_IN_DB;
+}
+
+//----------------------------------------------------------------------------
+// Class definitions for the boolean expression structure....
+//----------------------------------------------------------------------------
+
+nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::AddSearchTerm(
+ nsMsgSearchBoolExpression* aOrigExpr, nsIMsgSearchTerm* aNewTerm,
+ char* aEncodingStr)
+// appropriately add the search term to the current expression and return a
+// pointer to the new expression. The encodingStr is the IMAP/NNTP encoding
+// string for newTerm.
+{
+ return aOrigExpr->leftToRightAddTerm(aNewTerm, aEncodingStr);
+}
+
+nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::AddExpressionTree(
+ nsMsgSearchBoolExpression* aOrigExpr,
+ nsMsgSearchBoolExpression* aExpression, bool aBoolOp) {
+ if (!aOrigExpr->m_term && !aOrigExpr->m_leftChild &&
+ !aOrigExpr->m_rightChild) {
+ // just use the original expression tree...
+ // delete the original since we have a new original to use
+ delete aOrigExpr;
+ return aExpression;
+ }
+
+ nsMsgSearchBoolExpression* newExpr =
+ new nsMsgSearchBoolExpression(aOrigExpr, aExpression, aBoolOp);
+ return (newExpr) ? newExpr : aOrigExpr;
+}
+
+nsMsgSearchBoolExpression::nsMsgSearchBoolExpression() {
+ m_term = nullptr;
+ m_boolOp = nsMsgSearchBooleanOp::BooleanAND;
+ m_leftChild = nullptr;
+ m_rightChild = nullptr;
+}
+
+nsMsgSearchBoolExpression::nsMsgSearchBoolExpression(nsIMsgSearchTerm* newTerm,
+ char* encodingStr)
+// we are creating an expression which contains a single search term (newTerm)
+// and the search term's IMAP or NNTP encoding value for online search
+// expressions AND a boolean evaluation value which is used for offline search
+// expressions.
+{
+ m_term = newTerm;
+ m_encodingStr = encodingStr;
+ m_boolOp = nsMsgSearchBooleanOp::BooleanAND;
+
+ // this expression does not contain sub expressions
+ m_leftChild = nullptr;
+ m_rightChild = nullptr;
+}
+
+nsMsgSearchBoolExpression::nsMsgSearchBoolExpression(
+ nsMsgSearchBoolExpression* expr1, nsMsgSearchBoolExpression* expr2,
+ nsMsgSearchBooleanOperator boolOp)
+// we are creating an expression which contains two sub expressions and a
+// boolean operator used to combine them.
+{
+ m_leftChild = expr1;
+ m_rightChild = expr2;
+ m_boolOp = boolOp;
+
+ m_term = nullptr;
+}
+
+nsMsgSearchBoolExpression::~nsMsgSearchBoolExpression() {
+ // we must recursively destroy all sub expressions before we destroy
+ // ourself.....We leave search terms alone!
+ delete m_leftChild;
+ delete m_rightChild;
+}
+
+nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::leftToRightAddTerm(
+ nsIMsgSearchTerm* newTerm, char* encodingStr) {
+ // we have a base case where this is the first term being added to the
+ // expression:
+ if (!m_term && !m_leftChild && !m_rightChild) {
+ m_term = newTerm;
+ m_encodingStr = encodingStr;
+ return this;
+ }
+
+ nsMsgSearchBoolExpression* tempExpr =
+ new nsMsgSearchBoolExpression(newTerm, encodingStr);
+ if (tempExpr) // make sure creation succeeded
+ {
+ bool booleanAnd;
+ newTerm->GetBooleanAnd(&booleanAnd);
+ nsMsgSearchBoolExpression* newExpr =
+ new nsMsgSearchBoolExpression(this, tempExpr, booleanAnd);
+ if (newExpr)
+ return newExpr;
+ else
+ delete tempExpr; // clean up memory allocation in case of failure
+ }
+ return this; // in case we failed to create a new expression, return self
+}
+
+// returns true or false depending on what the current expression evaluates to.
+bool nsMsgSearchBoolExpression::OfflineEvaluate(nsIMsgDBHdr* msgToMatch,
+ const char* defaultCharset,
+ nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db,
+ const nsACString& headers,
+ bool Filtering) {
+ bool result = true; // always default to false positives
+ bool isAnd;
+
+ if (m_term) // do we contain just a search term?
+ {
+ nsMsgSearchOfflineMail::ProcessSearchTerm(msgToMatch, m_term,
+ defaultCharset, scope, db,
+ headers, Filtering, &result);
+ return result;
+ }
+
+ // otherwise we must recursively determine the value of our sub expressions
+
+ isAnd = (m_boolOp == nsMsgSearchBooleanOp::BooleanAND);
+
+ if (m_leftChild) {
+ result = m_leftChild->OfflineEvaluate(msgToMatch, defaultCharset, scope, db,
+ headers, Filtering);
+ if ((result && !isAnd) || (!result && isAnd)) return result;
+ }
+
+ // If we got this far, either there was no leftChild (which is impossible)
+ // or we got (FALSE and OR) or (TRUE and AND) from the first result. That
+ // means the outcome depends entirely on the rightChild.
+ if (m_rightChild)
+ result = m_rightChild->OfflineEvaluate(msgToMatch, defaultCharset, scope,
+ db, headers, Filtering);
+
+ return result;
+}
+
+// ### Maybe we can get rid of these because of our use of nsString???
+// constants used for online searching with IMAP/NNTP encoded search terms.
+// the + 1 is to account for null terminators we add at each stage of assembling
+// the expression...
+const int sizeOfORTerm =
+ 6 + 1; // 6 bytes if we are combining two sub expressions with an OR term
+const int sizeOfANDTerm =
+ 1 + 1; // 1 byte if we are combining two sub expressions with an AND term
+
+int32_t nsMsgSearchBoolExpression::CalcEncodeStrSize()
+// recursively examine each sub expression and calculate a final size for the
+// entire IMAP/NNTP encoding
+{
+ if (!m_term && (!m_leftChild || !m_rightChild)) // is the expression empty?
+ return 0;
+ if (m_term) // are we a leaf node?
+ return m_encodingStr.Length();
+ if (m_boolOp == nsMsgSearchBooleanOp::BooleanOR)
+ return sizeOfORTerm + m_leftChild->CalcEncodeStrSize() +
+ m_rightChild->CalcEncodeStrSize();
+ if (m_boolOp == nsMsgSearchBooleanOp::BooleanAND)
+ return sizeOfANDTerm + m_leftChild->CalcEncodeStrSize() +
+ m_rightChild->CalcEncodeStrSize();
+ return 0;
+}
+
+void nsMsgSearchBoolExpression::GenerateEncodeStr(nsCString* buffer)
+// recursively combine sub expressions to form a single IMAP/NNTP encoded string
+{
+ if ((!m_term && (!m_leftChild || !m_rightChild))) // is expression empty?
+ return;
+
+ if (m_term) // are we a leaf expression?
+ {
+ *buffer += m_encodingStr;
+ return;
+ }
+
+ // add encode strings of each sub expression
+ if (m_boolOp == nsMsgSearchBooleanOp::BooleanOR) {
+ *buffer += " (OR";
+
+ m_leftChild->GenerateEncodeStr(
+ buffer); // insert left expression into the buffer
+ m_rightChild->GenerateEncodeStr(
+ buffer); // insert right expression into the buffer
+
+ // HACK ALERT!!! if last returned character in the buffer is now a ' ' then
+ // we need to remove it because we don't want a ' ' to preceded the closing
+ // paren in the OR encoding.
+ uint32_t lastCharPos = buffer->Length() - 1;
+ if (buffer->CharAt(lastCharPos) == ' ') {
+ buffer->SetLength(lastCharPos);
+ }
+
+ *buffer += ')';
+ } else if (m_boolOp == nsMsgSearchBooleanOp::BooleanAND) {
+ m_leftChild->GenerateEncodeStr(buffer); // insert left expression
+ m_rightChild->GenerateEncodeStr(buffer);
+ }
+ return;
+}
+
+//-----------------------------------------------------------------------------
+//---------------- Adapter class for searching offline folders ----------------
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgSearchOfflineMail, nsMsgSearchAdapter,
+ nsIUrlListener)
+
+nsMsgSearchOfflineMail::nsMsgSearchOfflineMail(
+ nsIMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList)
+ : nsMsgSearchAdapter(scope, termList) {}
+
+nsMsgSearchOfflineMail::~nsMsgSearchOfflineMail() {
+ // Database should have been closed when the scope term finished.
+ CleanUpScope();
+ NS_ASSERTION(!m_db, "db not closed");
+}
+
+nsresult nsMsgSearchOfflineMail::ValidateTerms() {
+ return nsMsgSearchAdapter::ValidateTerms();
+}
+
+nsresult nsMsgSearchOfflineMail::OpenSummaryFile() {
+ nsCOMPtr<nsIMsgDatabase> mailDB;
+
+ nsresult err = NS_OK;
+ // do password protection of local cache thing.
+#ifdef DOING_FOLDER_CACHE_PASSWORDS
+ if (m_scope->m_folder &&
+ m_scope->m_folder->UserNeedsToAuthenticateForFolder(false) &&
+ m_scope->m_folder->GetMaster()->PromptForHostPassword(
+ m_scope->m_frame->GetContext(), m_scope->m_folder) != 0) {
+ m_scope->m_frame->StopRunning();
+ return SearchError_ScopeDone;
+ }
+#endif
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+ err = m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ if (NS_SUCCEEDED(err) && scopeFolder) {
+ err = scopeFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(m_db));
+ } else
+ return err; // not sure why m_folder wouldn't be set.
+
+ if (NS_SUCCEEDED(err)) return NS_OK;
+
+ if ((err == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) ||
+ (err == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
+ do_QueryInterface(scopeFolder, &err);
+ if (NS_SUCCEEDED(err) && localFolder) {
+ nsCOMPtr<nsIMsgSearchSession> searchSession;
+ m_scope->GetSearchSession(getter_AddRefs(searchSession));
+ if (searchSession) {
+ nsCOMPtr<nsIMsgWindow> searchWindow;
+
+ searchSession->GetWindow(getter_AddRefs(searchWindow));
+ searchSession->PauseSearch();
+ localFolder->ParseFolder(searchWindow, this);
+ }
+ }
+ } else {
+ NS_ASSERTION(false, "unexpected error opening db");
+ }
+
+ return err;
+}
+
+nsresult nsMsgSearchOfflineMail::MatchTermsForFilter(
+ nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, const nsACString& headers,
+ nsMsgSearchBoolExpression** aExpressionTree, bool* pResult) {
+ return MatchTerms(msgToMatch, termList, defaultCharset, scope, db, headers,
+ true, aExpressionTree, pResult);
+}
+
+// static method which matches a header against a list of search terms.
+nsresult nsMsgSearchOfflineMail::MatchTermsForSearch(
+ nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, nsMsgSearchBoolExpression** aExpressionTree,
+ bool* pResult) {
+ return MatchTerms(msgToMatch, termList, defaultCharset, scope, db,
+ EmptyCString(), false, aExpressionTree, pResult);
+}
+
+nsresult nsMsgSearchOfflineMail::ConstructExpressionTree(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, uint32_t termCount,
+ uint32_t& aStartPosInList, nsMsgSearchBoolExpression** aExpressionTree) {
+ nsMsgSearchBoolExpression* finalExpression = *aExpressionTree;
+
+ if (!finalExpression) finalExpression = new nsMsgSearchBoolExpression();
+
+ while (aStartPosInList < termCount) {
+ nsIMsgSearchTerm* pTerm = termList[aStartPosInList];
+ NS_ASSERTION(pTerm, "couldn't get term to match");
+
+ bool beginsGrouping;
+ bool endsGrouping;
+ pTerm->GetBeginsGrouping(&beginsGrouping);
+ pTerm->GetEndsGrouping(&endsGrouping);
+
+ if (beginsGrouping) {
+ // temporarily turn off the grouping for our recursive call
+ pTerm->SetBeginsGrouping(false);
+ nsMsgSearchBoolExpression* innerExpression =
+ new nsMsgSearchBoolExpression();
+
+ // the first search term in the grouping is the one that holds the
+ // operator for how this search term should be joined with the expressions
+ // to it's left.
+ bool booleanAnd;
+ pTerm->GetBooleanAnd(&booleanAnd);
+
+ // now add this expression tree to our overall expression tree...
+ finalExpression = nsMsgSearchBoolExpression::AddExpressionTree(
+ finalExpression, innerExpression, booleanAnd);
+
+ // recursively process this inner expression
+ ConstructExpressionTree(termList, termCount, aStartPosInList,
+ &finalExpression->m_rightChild);
+
+ // undo our damage
+ pTerm->SetBeginsGrouping(true);
+
+ } else {
+ finalExpression = nsMsgSearchBoolExpression::AddSearchTerm(
+ finalExpression, pTerm,
+ nullptr); // add the term to the expression tree
+
+ if (endsGrouping) break;
+ }
+
+ aStartPosInList++;
+ } // while we still have terms to process in this group
+
+ *aExpressionTree = finalExpression;
+
+ return NS_OK;
+}
+
+nsresult nsMsgSearchOfflineMail::ProcessSearchTerm(
+ nsIMsgDBHdr* msgToMatch, nsIMsgSearchTerm* aTerm,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, const nsACString& headers, bool Filtering,
+ bool* pResult) {
+ nsresult err = NS_OK;
+ nsCString recipients;
+ nsCString ccList;
+ nsCString matchString;
+ nsCString msgCharset;
+ const char* charset;
+ bool charsetOverride = false; /* XXX BUG 68706 */
+ uint32_t msgFlags;
+ bool result;
+ bool matchAll;
+
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ aTerm->GetMatchAll(&matchAll);
+ if (matchAll) {
+ *pResult = true;
+ return NS_OK;
+ }
+ *pResult = false;
+
+ nsMsgSearchAttribValue attrib;
+ aTerm->GetAttrib(&attrib);
+ msgToMatch->GetCharset(getter_Copies(msgCharset));
+ charset = msgCharset.get();
+ if (!charset || !*charset) charset = (const char*)defaultCharset;
+ msgToMatch->GetFlags(&msgFlags);
+
+ switch (attrib) {
+ case nsMsgSearchAttrib::Sender:
+ msgToMatch->GetAuthor(getter_Copies(matchString));
+ err = aTerm->MatchRfc822String(matchString, charset, &result);
+ break;
+ case nsMsgSearchAttrib::Subject: {
+ msgToMatch->GetSubject(matchString /* , true */);
+ if (msgFlags & nsMsgMessageFlags::HasRe) {
+ // Make sure we pass along the "Re: " part of the subject if this is a
+ // reply.
+ nsCString reString;
+ reString.AssignLiteral("Re: ");
+ reString.Append(matchString);
+ err = aTerm->MatchRfc2047String(reString, charset, charsetOverride,
+ &result);
+ } else
+ err = aTerm->MatchRfc2047String(matchString, charset, charsetOverride,
+ &result);
+ break;
+ }
+ case nsMsgSearchAttrib::ToOrCC: {
+ bool boolKeepGoing;
+ aTerm->GetMatchAllBeforeDeciding(&boolKeepGoing);
+ msgToMatch->GetRecipients(getter_Copies(recipients));
+ err = aTerm->MatchRfc822String(recipients, charset, &result);
+ if (boolKeepGoing == result) {
+ msgToMatch->GetCcList(getter_Copies(ccList));
+ err = aTerm->MatchRfc822String(ccList, charset, &result);
+ }
+ break;
+ }
+ case nsMsgSearchAttrib::AllAddresses: {
+ bool boolKeepGoing;
+ aTerm->GetMatchAllBeforeDeciding(&boolKeepGoing);
+ msgToMatch->GetRecipients(getter_Copies(recipients));
+ err = aTerm->MatchRfc822String(recipients, charset, &result);
+ if (boolKeepGoing == result) {
+ msgToMatch->GetCcList(getter_Copies(ccList));
+ err = aTerm->MatchRfc822String(ccList, charset, &result);
+ }
+ if (boolKeepGoing == result) {
+ msgToMatch->GetAuthor(getter_Copies(matchString));
+ err = aTerm->MatchRfc822String(matchString, charset, &result);
+ }
+ if (boolKeepGoing == result) {
+ nsCString bccList;
+ msgToMatch->GetBccList(getter_Copies(bccList));
+ err = aTerm->MatchRfc822String(bccList, charset, &result);
+ }
+ break;
+ }
+ case nsMsgSearchAttrib::Body: {
+ uint64_t messageOffset;
+ uint32_t lineCount;
+ msgToMatch->GetMessageOffset(&messageOffset);
+ msgToMatch->GetLineCount(&lineCount);
+ err = aTerm->MatchBody(scope, messageOffset, lineCount, charset,
+ msgToMatch, db, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::Date: {
+ PRTime date;
+ msgToMatch->GetDate(&date);
+ err = aTerm->MatchDate(date, &result);
+
+ break;
+ }
+ case nsMsgSearchAttrib::HasAttachmentStatus:
+ case nsMsgSearchAttrib::MsgStatus:
+ err = aTerm->MatchStatus(msgFlags, &result);
+ break;
+ case nsMsgSearchAttrib::Priority: {
+ nsMsgPriorityValue msgPriority;
+ msgToMatch->GetPriority(&msgPriority);
+ err = aTerm->MatchPriority(msgPriority, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::Size: {
+ uint32_t messageSize;
+ msgToMatch->GetMessageSize(&messageSize);
+ err = aTerm->MatchSize(messageSize, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::To:
+ msgToMatch->GetRecipients(getter_Copies(recipients));
+ err = aTerm->MatchRfc822String(recipients, charset, &result);
+ break;
+ case nsMsgSearchAttrib::CC:
+ msgToMatch->GetCcList(getter_Copies(ccList));
+ err = aTerm->MatchRfc822String(ccList, charset, &result);
+ break;
+ case nsMsgSearchAttrib::AgeInDays: {
+ PRTime date;
+ msgToMatch->GetDate(&date);
+ err = aTerm->MatchAge(date, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::Keywords: {
+ nsCString keywords;
+ msgToMatch->GetStringProperty("keywords", keywords);
+ err = aTerm->MatchKeyword(keywords, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::JunkStatus: {
+ nsCString junkScoreStr;
+ msgToMatch->GetStringProperty("junkscore", junkScoreStr);
+ err = aTerm->MatchJunkStatus(junkScoreStr.get(), &result);
+ break;
+ }
+ case nsMsgSearchAttrib::JunkPercent: {
+ // When the junk status is set by the plugin, use junkpercent (if
+ // available) Otherwise, use the limits (0 or 100) depending on the
+ // junkscore.
+ uint32_t junkPercent;
+ nsresult rv;
+ nsCString junkScoreOriginStr;
+ nsCString junkPercentStr;
+ msgToMatch->GetStringProperty("junkscoreorigin", junkScoreOriginStr);
+ msgToMatch->GetStringProperty("junkpercent", junkPercentStr);
+ if (junkScoreOriginStr.EqualsLiteral("plugin") &&
+ !junkPercentStr.IsEmpty()) {
+ junkPercent = junkPercentStr.ToInteger(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCString junkScoreStr;
+ msgToMatch->GetStringProperty("junkscore", junkScoreStr);
+ // When junk status is not set (uncertain) we'll set the value to ham.
+ if (junkScoreStr.IsEmpty())
+ junkPercent = nsIJunkMailPlugin::IS_HAM_SCORE;
+ else {
+ junkPercent = junkScoreStr.ToInteger(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ err = aTerm->MatchJunkPercent(junkPercent, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::JunkScoreOrigin: {
+ nsCString junkScoreOriginStr;
+ msgToMatch->GetStringProperty("junkscoreorigin", junkScoreOriginStr);
+ err = aTerm->MatchJunkScoreOrigin(junkScoreOriginStr.get(), &result);
+ break;
+ }
+ case nsMsgSearchAttrib::HdrProperty: {
+ err = aTerm->MatchHdrProperty(msgToMatch, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::Uint32HdrProperty: {
+ err = aTerm->MatchUint32HdrProperty(msgToMatch, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::Custom: {
+ err = aTerm->MatchCustom(msgToMatch, &result);
+ break;
+ }
+ case nsMsgSearchAttrib::FolderFlag: {
+ err = aTerm->MatchFolderFlag(msgToMatch, &result);
+ break;
+ }
+ default:
+ // XXX todo
+ // for the temporary return receipts filters, we use a custom header for
+ // Content-Type but unlike the other custom headers, this one doesn't show
+ // up in the search / filter UI. we set the attrib to be
+ // nsMsgSearchAttrib::OtherHeader, where as for user defined custom
+ // headers start at nsMsgSearchAttrib::OtherHeader + 1 Not sure if there
+ // is a better way to do this yet. Maybe reserve the last custom header
+ // for ::Content-Type? But if we do, make sure that change doesn't cause
+ // nsMsgFilter::GetTerm() to change, and start making us ask IMAP servers
+ // for the Content-Type header on all messages.
+ if (attrib >= nsMsgSearchAttrib::OtherHeader &&
+ attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ uint32_t lineCount;
+ msgToMatch->GetLineCount(&lineCount);
+ uint64_t messageOffset;
+ msgToMatch->GetMessageOffset(&messageOffset);
+ err = aTerm->MatchArbitraryHeader(scope, lineCount, charset,
+ charsetOverride, msgToMatch, db,
+ headers, Filtering, &result);
+ } else {
+ err = NS_ERROR_INVALID_ARG; // ### was SearchError_InvalidAttribute
+ result = false;
+ }
+ }
+
+ *pResult = result;
+ return err;
+}
+
+nsresult nsMsgSearchOfflineMail::MatchTerms(
+ nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, const nsACString& headers, bool Filtering,
+ nsMsgSearchBoolExpression** aExpressionTree, bool* pResult) {
+ NS_ENSURE_ARG(aExpressionTree);
+ nsresult err;
+
+ if (!*aExpressionTree) {
+ uint32_t initialPos = 0;
+ uint32_t count = termList.Length();
+ err = ConstructExpressionTree(termList, count, initialPos, aExpressionTree);
+ if (NS_FAILED(err)) return err;
+ }
+
+ // evaluate the expression tree and return the result
+ *pResult = (*aExpressionTree)
+ ? (*aExpressionTree)
+ ->OfflineEvaluate(msgToMatch, defaultCharset, scope, db,
+ headers, Filtering)
+ : true; // vacuously true...
+
+ return NS_OK;
+}
+
+nsresult nsMsgSearchOfflineMail::Search(bool* aDone) {
+ nsresult err = NS_OK;
+
+ NS_ENSURE_ARG(aDone);
+ nsresult dbErr = NS_OK;
+ nsMsgSearchBoolExpression* expressionTree = nullptr;
+
+ const uint32_t kTimeSliceInMS = 200;
+
+ *aDone = false;
+ // Try to open the DB lazily. This will set up a parser if one is required
+ if (!m_db) err = OpenSummaryFile();
+ if (!m_db) // must be reparsing.
+ return err;
+
+ // Reparsing is unnecessary or completed
+ if (NS_SUCCEEDED(err)) {
+ if (!m_listContext)
+ dbErr = m_db->ReverseEnumerateMessages(getter_AddRefs(m_listContext));
+ if (NS_SUCCEEDED(dbErr) && m_listContext) {
+ PRIntervalTime startTime = PR_IntervalNow();
+ while (!*aDone) // we'll break out of the loop after kTimeSliceInMS
+ // milliseconds
+ {
+ nsCOMPtr<nsIMsgDBHdr> msgDBHdr;
+ dbErr = m_listContext->GetNext(getter_AddRefs(msgDBHdr));
+ if (NS_FAILED(dbErr))
+ *aDone = true; // ###phil dbErr is dropped on the floor. just note
+ // that we did have an error so we'll clean up later
+ else {
+ bool match = false;
+ nsAutoString nullCharset, folderCharset;
+ GetSearchCharsets(nullCharset, folderCharset);
+ NS_ConvertUTF16toUTF8 charset(folderCharset);
+ // Is this message a hit?
+ err = MatchTermsForSearch(msgDBHdr, m_searchTerms, charset.get(),
+ m_scope, m_db, &expressionTree, &match);
+ // Add search hits to the results list
+ if (NS_SUCCEEDED(err) && match) {
+ AddResultElement(msgDBHdr);
+ }
+ PRIntervalTime elapsedTime = PR_IntervalNow() - startTime;
+ // check if more than kTimeSliceInMS milliseconds have elapsed in this
+ // time slice started
+ if (PR_IntervalToMilliseconds(elapsedTime) > kTimeSliceInMS) break;
+ }
+ }
+ }
+ } else
+ *aDone = true; // we couldn't open up the DB. This is an unrecoverable
+ // error so mark the scope as done.
+
+ delete expressionTree;
+
+ // in the past an error here would cause an "infinite" search because the url
+ // would continue to run... i.e. if we couldn't open the database, it returns
+ // an error code but the caller of this function says, oh, we did not finish
+ // so continue...what we really want is to treat this current scope as done
+ if (*aDone) CleanUpScope(); // Do clean up for end-of-scope processing
+ return err;
+}
+
+void nsMsgSearchOfflineMail::CleanUpScope() {
+ // Let go of the DB when we're done with it so we don't kill the db cache
+ if (m_db) {
+ m_listContext = nullptr;
+ m_db->Close(false);
+ }
+ m_db = nullptr;
+
+ if (m_scope) m_scope->CloseInputStream();
+}
+
+NS_IMETHODIMP nsMsgSearchOfflineMail::AddResultElement(nsIMsgDBHdr* pHeaders) {
+ nsresult err = NS_OK;
+
+ nsCOMPtr<nsIMsgSearchSession> searchSession;
+ m_scope->GetSearchSession(getter_AddRefs(searchSession));
+ if (searchSession) {
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+ err = m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ searchSession->AddSearchHit(pHeaders, scopeFolder);
+ }
+ return err;
+}
+
+NS_IMETHODIMP
+nsMsgSearchOfflineMail::Abort() {
+ // Let go of the DB when we're done with it so we don't kill the db cache
+ if (m_db) m_db->Close(true /* commit in case we downloaded new headers */);
+ m_db = nullptr;
+ return nsMsgSearchAdapter::Abort();
+}
+
+/* void OnStartRunningUrl (in nsIURI url); */
+NS_IMETHODIMP nsMsgSearchOfflineMail::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+/* void OnStopRunningUrl (in nsIURI url, in nsresult aExitCode); */
+NS_IMETHODIMP nsMsgSearchOfflineMail::OnStopRunningUrl(nsIURI* url,
+ nsresult aExitCode) {
+ nsCOMPtr<nsIMsgSearchSession> searchSession;
+ if (m_scope) m_scope->GetSearchSession(getter_AddRefs(searchSession));
+ if (searchSession) searchSession->ResumeSearch();
+
+ return NS_OK;
+}
+
+nsMsgSearchOfflineNews::nsMsgSearchOfflineNews(
+ nsIMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList)
+ : nsMsgSearchOfflineMail(scope, termList) {}
+
+nsMsgSearchOfflineNews::~nsMsgSearchOfflineNews() {}
+
+nsresult nsMsgSearchOfflineNews::OpenSummaryFile() {
+ nsresult err = NS_OK;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+ err = m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ // code here used to check if offline store existed, which breaks offline
+ // news.
+ if (NS_SUCCEEDED(err) && scopeFolder)
+ err = scopeFolder->GetMsgDatabase(getter_AddRefs(m_db));
+ return err;
+}
+
+nsresult nsMsgSearchOfflineNews::ValidateTerms() {
+ return nsMsgSearchOfflineMail::ValidateTerms();
+}
+
+// local helper functions to set subsets of the validity table
+// clang-format off
+nsresult SetJunk(nsIMsgSearchValidityTable *aTable) {
+ NS_ENSURE_ARG_POINTER(aTable);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+
+ return NS_OK;
+}
+
+nsresult SetBody(nsIMsgSearchValidityTable* aTable) {
+ NS_ENSURE_ARG_POINTER(aTable);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+
+ return NS_OK;
+}
+
+// set the base validity table values for local news
+nsresult SetLocalNews(nsIMsgSearchValidityTable* aTable) {
+ NS_ENSURE_ARG_POINTER(aTable);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ return NS_OK;
+}
+// clang-format on
+
+nsresult nsMsgSearchValidityManager::InitLocalNewsTable() {
+ NS_ASSERTION(nullptr == m_localNewsTable,
+ "already have local news validity table");
+ nsresult rv = NewTable(getter_AddRefs(m_localNewsTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetLocalNews(m_localNewsTable);
+}
+
+nsresult nsMsgSearchValidityManager::InitLocalNewsBodyTable() {
+ NS_ASSERTION(nullptr == m_localNewsBodyTable,
+ "already have local news+body validity table");
+ nsresult rv = NewTable(getter_AddRefs(m_localNewsBodyTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetLocalNews(m_localNewsBodyTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetBody(m_localNewsBodyTable);
+}
+
+nsresult nsMsgSearchValidityManager::InitLocalNewsJunkTable() {
+ NS_ASSERTION(nullptr == m_localNewsJunkTable,
+ "already have local news+junk validity table");
+ nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetLocalNews(m_localNewsJunkTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetJunk(m_localNewsJunkTable);
+}
+
+nsresult nsMsgSearchValidityManager::InitLocalNewsJunkBodyTable() {
+ NS_ASSERTION(nullptr == m_localNewsJunkBodyTable,
+ "already have local news+junk+body validity table");
+ nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkBodyTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetLocalNews(m_localNewsJunkBodyTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetJunk(m_localNewsJunkBodyTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetBody(m_localNewsJunkBodyTable);
+}
diff --git a/comm/mailnews/search/src/nsMsgLocalSearch.h b/comm/mailnews/search/src/nsMsgLocalSearch.h
new file mode 100644
index 0000000000..4deb5b4065
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgLocalSearch.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgLocalSearch_H
+#define _nsMsgLocalSearch_H
+
+// inherit interface here
+#include "mozilla/Attributes.h"
+#include "nsIMsgSearchAdapter.h"
+#include "nsIUrlListener.h"
+
+// inherit base implementation
+#include "nsMsgSearchAdapter.h"
+
+class nsIMsgDBHdr;
+class nsIMsgSearchScopeTerm;
+class nsIMsgFolder;
+class nsMsgSearchBoolExpression;
+
+class nsMsgSearchOfflineMail : public nsMsgSearchAdapter,
+ public nsIUrlListener {
+ public:
+ nsMsgSearchOfflineMail(nsIMsgSearchScopeTerm*,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const&);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIURLLISTENER
+
+ NS_IMETHOD ValidateTerms() override;
+ NS_IMETHOD Search(bool* aDone) override;
+ NS_IMETHOD Abort() override;
+ NS_IMETHOD AddResultElement(nsIMsgDBHdr*) override;
+
+ static nsresult MatchTermsForFilter(
+ nsIMsgDBHdr* msgToMatch,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, const nsACString& headers,
+ nsMsgSearchBoolExpression** aExpressionTree, bool* pResult);
+
+ static nsresult MatchTermsForSearch(
+ nsIMsgDBHdr* msgTomatch,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset, nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db, nsMsgSearchBoolExpression** aExpressionTree,
+ bool* pResult);
+
+ virtual nsresult OpenSummaryFile();
+
+ static nsresult ProcessSearchTerm(nsIMsgDBHdr* msgToMatch,
+ nsIMsgSearchTerm* aTerm,
+ const char* defaultCharset,
+ nsIMsgSearchScopeTerm* scope,
+ nsIMsgDatabase* db,
+ const nsACString& headers, bool Filtering,
+ bool* pResult);
+
+ protected:
+ virtual ~nsMsgSearchOfflineMail();
+ static nsresult MatchTerms(nsIMsgDBHdr* msgToMatch,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList,
+ const char* defaultCharset,
+ nsIMsgSearchScopeTerm* scope, nsIMsgDatabase* db,
+ const nsACString& headers, bool ForFilters,
+ nsMsgSearchBoolExpression** aExpressionTree,
+ bool* pResult);
+
+ static nsresult ConstructExpressionTree(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, uint32_t termCount,
+ uint32_t& aStartPosInList, nsMsgSearchBoolExpression** aExpressionTree);
+
+ nsCOMPtr<nsIMsgDatabase> m_db;
+ nsCOMPtr<nsIMsgEnumerator> m_listContext;
+ void CleanUpScope();
+};
+
+class nsMsgSearchOfflineNews : public nsMsgSearchOfflineMail {
+ public:
+ nsMsgSearchOfflineNews(nsIMsgSearchScopeTerm*,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const&);
+ virtual ~nsMsgSearchOfflineNews();
+ NS_IMETHOD ValidateTerms() override;
+
+ virtual nsresult OpenSummaryFile() override;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgSearchAdapter.cpp b/comm/mailnews/search/src/nsMsgSearchAdapter.cpp
new file mode 100644
index 0000000000..0a2fa5d2ee
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchAdapter.cpp
@@ -0,0 +1,1109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsTextFormatter.h"
+#include "nsMsgSearchCore.h"
+#include "nsMsgSearchAdapter.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsMsgI18N.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsMsgSearchTerm.h"
+#include "nsMsgSearchBoolExpression.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "prprf.h"
+#include "mozilla/UniquePtr.h"
+#include "prmem.h"
+#include "MailNewsTypes.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "nsMsgMessageFlags.h"
+#include "mozilla/Attributes.h"
+#include "nsIMsgNewsFolder.h"
+
+// This stuff lives in the base class because the IMAP search syntax
+// is used by the Dredd SEARCH command as well as IMAP itself
+
+// km - the NOT and HEADER strings are not encoded with a trailing
+// <space> because they always precede a mnemonic that has a
+// preceding <space> and double <space> characters cause some
+// imap servers to return an error.
+const char* nsMsgSearchAdapter::m_kImapBefore = " SENTBEFORE ";
+const char* nsMsgSearchAdapter::m_kImapBody = " BODY ";
+const char* nsMsgSearchAdapter::m_kImapCC = " CC ";
+const char* nsMsgSearchAdapter::m_kImapFrom = " FROM ";
+const char* nsMsgSearchAdapter::m_kImapNot = " NOT";
+const char* nsMsgSearchAdapter::m_kImapUnDeleted = " UNDELETED";
+const char* nsMsgSearchAdapter::m_kImapOr = " OR";
+const char* nsMsgSearchAdapter::m_kImapSince = " SENTSINCE ";
+const char* nsMsgSearchAdapter::m_kImapSubject = " SUBJECT ";
+const char* nsMsgSearchAdapter::m_kImapTo = " TO ";
+const char* nsMsgSearchAdapter::m_kImapHeader = " HEADER";
+const char* nsMsgSearchAdapter::m_kImapAnyText = " TEXT ";
+const char* nsMsgSearchAdapter::m_kImapKeyword = " KEYWORD ";
+const char* nsMsgSearchAdapter::m_kNntpKeywords = " KEYWORDS "; // ggrrrr...
+const char* nsMsgSearchAdapter::m_kImapSentOn = " SENTON ";
+const char* nsMsgSearchAdapter::m_kImapSeen = " SEEN ";
+const char* nsMsgSearchAdapter::m_kImapAnswered = " ANSWERED ";
+const char* nsMsgSearchAdapter::m_kImapNotSeen = " UNSEEN ";
+const char* nsMsgSearchAdapter::m_kImapNotAnswered = " UNANSWERED ";
+const char* nsMsgSearchAdapter::m_kImapCharset = " CHARSET ";
+const char* nsMsgSearchAdapter::m_kImapSizeSmaller = " SMALLER ";
+const char* nsMsgSearchAdapter::m_kImapSizeLarger = " LARGER ";
+const char* nsMsgSearchAdapter::m_kImapNew = " NEW ";
+const char* nsMsgSearchAdapter::m_kImapNotNew = " OLD SEEN ";
+const char* nsMsgSearchAdapter::m_kImapFlagged = " FLAGGED ";
+const char* nsMsgSearchAdapter::m_kImapNotFlagged = " UNFLAGGED ";
+
+#define PREF_CUSTOM_HEADERS "mailnews.customHeaders"
+
+NS_IMETHODIMP nsMsgSearchAdapter::FindTargetFolder(const nsMsgResultElement*,
+ nsIMsgFolder**) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::ModifyResultElement(nsMsgResultElement*,
+ nsMsgSearchValue*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::OpenResultElement(nsMsgResultElement*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchAdapter, nsIMsgSearchAdapter)
+
+nsMsgSearchAdapter::nsMsgSearchAdapter(
+ nsIMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms)
+ : m_scope(scope), m_searchTerms(searchTerms.Clone()) {}
+
+nsMsgSearchAdapter::~nsMsgSearchAdapter() {}
+
+NS_IMETHODIMP nsMsgSearchAdapter::ClearScope() {
+ if (m_scope) {
+ m_scope->CloseInputStream();
+ m_scope = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::ValidateTerms() {
+ // all this used to do is check if the object had been deleted - we can skip
+ // that.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::Abort() { return NS_ERROR_NOT_IMPLEMENTED; }
+NS_IMETHODIMP nsMsgSearchAdapter::Search(bool* aDone) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgSearchAdapter::SendUrl() { return NS_OK; }
+
+/* void CurrentUrlDone (in nsresult exitCode); */
+NS_IMETHODIMP nsMsgSearchAdapter::CurrentUrlDone(nsresult exitCode) {
+ // base implementation doesn't need to do anything.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::GetEncoding(char** encoding) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgSearchAdapter::AddResultElement(nsIMsgDBHdr* pHeaders) {
+ NS_ASSERTION(false, "shouldn't call this base class impl");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgSearchAdapter::AddHit(nsMsgKey key) {
+ NS_ASSERTION(false, "shouldn't call this base class impl");
+ return NS_ERROR_FAILURE;
+}
+
+char* nsMsgSearchAdapter::GetImapCharsetParam(const char16_t* destCharset) {
+ char* result = nullptr;
+
+ // Specify a character set unless we happen to be US-ASCII.
+ if (NS_strcmp(destCharset, u"us-ascii"))
+ result = PR_smprintf("%s%s", nsMsgSearchAdapter::m_kImapCharset,
+ NS_ConvertUTF16toUTF8(destCharset).get());
+
+ return result;
+}
+
+/*
+ 09/21/2000 - taka@netscape.com
+ This method is bogus. Escape must be done against char * not char16_t *
+ should be rewritten later.
+ for now, just duplicate the string.
+*/
+char16_t* nsMsgSearchAdapter::EscapeSearchUrl(const char16_t* nntpCommand) {
+ return nntpCommand ? NS_xstrdup(nntpCommand) : nullptr;
+}
+
+/*
+ 09/21/2000 - taka@netscape.com
+ This method is bogus. Escape must be done against char * not char16_t *
+ should be rewritten later.
+ for now, just duplicate the string.
+*/
+char16_t* nsMsgSearchAdapter::EscapeImapSearchProtocol(
+ const char16_t* imapCommand) {
+ return imapCommand ? NS_xstrdup(imapCommand) : nullptr;
+}
+
+/*
+ 09/21/2000 - taka@netscape.com
+ This method is bogus. Escape must be done against char * not char16_t *
+ should be rewritten later.
+ for now, just duplicate the string.
+*/
+char16_t* nsMsgSearchAdapter::EscapeQuoteImapSearchProtocol(
+ const char16_t* imapCommand) {
+ return imapCommand ? NS_xstrdup(imapCommand) : nullptr;
+}
+
+char* nsMsgSearchAdapter::UnEscapeSearchUrl(const char* commandSpecificData) {
+ char* result = (char*)PR_Malloc(strlen(commandSpecificData) + 1);
+ if (result) {
+ char* resultPtr = result;
+ while (1) {
+ char ch = *commandSpecificData++;
+ if (!ch) break;
+ if (ch == '\\') {
+ char scratchBuf[3];
+ scratchBuf[0] = (char)*commandSpecificData++;
+ scratchBuf[1] = (char)*commandSpecificData++;
+ scratchBuf[2] = '\0';
+ unsigned int accum = 0;
+ sscanf(scratchBuf, "%X", &accum);
+ *resultPtr++ = (char)accum;
+ } else
+ *resultPtr++ = ch;
+ }
+ *resultPtr = '\0';
+ }
+ return result;
+}
+
+nsresult nsMsgSearchAdapter::GetSearchCharsets(nsAString& srcCharset,
+ nsAString& dstCharset) {
+ nsresult rv;
+ bool forceAsciiSearch = false;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ prefs->GetBoolPref("mailnews.force_ascii_search", &forceAsciiSearch);
+ }
+
+ srcCharset = m_defaultCharset;
+ dstCharset.Assign(srcCharset);
+
+ if (m_scope) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = m_scope->GetFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) {
+ nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(folder));
+ if (newsfolder) {
+ nsCString folderCharset;
+ rv = newsfolder->GetCharset(folderCharset);
+ if (NS_SUCCEEDED(rv))
+ dstCharset.Assign(NS_ConvertASCIItoUTF16(folderCharset));
+ }
+ }
+ }
+
+ if (forceAsciiSearch) {
+ // Special cases to use in order to force US-ASCII searching with Latin1
+ // or MacRoman text. Eurgh. This only has to happen because IMAP
+ // and Dredd servers currently (4/23/97) only support US-ASCII.
+ //
+ // If the dest csid is ISO Latin 1 or MacRoman, attempt to convert the
+ // source text to US-ASCII. (Not for now.)
+ // if ((dst_csid == CS_LATIN1) || (dst_csid == CS_MAC_ROMAN))
+ dstCharset.AssignLiteral("us-ascii");
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgSearchAdapter::EncodeImapTerm(nsIMsgSearchTerm* term,
+ bool reallyDredd,
+ const char16_t* srcCharset,
+ const char16_t* destCharset,
+ char** ppOutTerm) {
+ NS_ENSURE_ARG_POINTER(term);
+ NS_ENSURE_ARG_POINTER(ppOutTerm);
+
+ nsresult err = NS_OK;
+ bool useNot = false;
+ bool useQuotes = false;
+ bool ignoreValue = false;
+ nsAutoCString arbitraryHeader;
+ const char* whichMnemonic = nullptr;
+ const char* orHeaderMnemonic = nullptr;
+
+ *ppOutTerm = nullptr;
+
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+ nsresult rv = term->GetValue(getter_AddRefs(searchValue));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgSearchOpValue op;
+ term->GetOp(&op);
+
+ if (op == nsMsgSearchOp::DoesntContain || op == nsMsgSearchOp::Isnt)
+ useNot = true;
+
+ nsMsgSearchAttribValue attrib;
+ term->GetAttrib(&attrib);
+
+ switch (attrib) {
+ case nsMsgSearchAttrib::ToOrCC:
+ orHeaderMnemonic = m_kImapCC;
+ // fall through to case nsMsgSearchAttrib::To:
+ [[fallthrough]];
+ case nsMsgSearchAttrib::To:
+ whichMnemonic = m_kImapTo;
+ break;
+ case nsMsgSearchAttrib::CC:
+ whichMnemonic = m_kImapCC;
+ break;
+ case nsMsgSearchAttrib::Sender:
+ whichMnemonic = m_kImapFrom;
+ break;
+ case nsMsgSearchAttrib::Subject:
+ whichMnemonic = m_kImapSubject;
+ break;
+ case nsMsgSearchAttrib::Body:
+ whichMnemonic = m_kImapBody;
+ break;
+ case nsMsgSearchAttrib::AgeInDays: // added for searching online for age in
+ // days...
+ // for AgeInDays, we are actually going to perform a search by date, so
+ // convert the operations for age to the IMAP mnemonics that we would use
+ // for date!
+ {
+ // If we have a future date, the > and < are reversed.
+ // e.g. ageInDays > 2 means more than 2 days old ("date before X")
+ // whereas
+ // ageInDays > -2 should be more than 2 days in the future ("date
+ // after X")
+ int32_t ageInDays;
+ searchValue->GetAge(&ageInDays);
+ bool dateInFuture = (ageInDays < 0);
+ switch (op) {
+ case nsMsgSearchOp::IsGreaterThan:
+ whichMnemonic = (!dateInFuture) ? m_kImapBefore : m_kImapSince;
+ break;
+ case nsMsgSearchOp::IsLessThan:
+ whichMnemonic = (!dateInFuture) ? m_kImapSince : m_kImapBefore;
+ break;
+ case nsMsgSearchOp::Is:
+ whichMnemonic = m_kImapSentOn;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid search operator");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ break;
+ case nsMsgSearchAttrib::Size:
+ switch (op) {
+ case nsMsgSearchOp::IsGreaterThan:
+ whichMnemonic = m_kImapSizeLarger;
+ break;
+ case nsMsgSearchOp::IsLessThan:
+ whichMnemonic = m_kImapSizeSmaller;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid search operator");
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ case nsMsgSearchAttrib::Date:
+ switch (op) {
+ case nsMsgSearchOp::IsBefore:
+ whichMnemonic = m_kImapBefore;
+ break;
+ case nsMsgSearchOp::IsAfter:
+ whichMnemonic = m_kImapSince;
+ break;
+ case nsMsgSearchOp::Isnt: /* we've already added the "Not" so just
+ process it like it was a date is search */
+ case nsMsgSearchOp::Is:
+ whichMnemonic = m_kImapSentOn;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid search operator");
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ case nsMsgSearchAttrib::AnyText:
+ whichMnemonic = m_kImapAnyText;
+ break;
+ case nsMsgSearchAttrib::Keywords:
+ whichMnemonic = m_kImapKeyword;
+ break;
+ case nsMsgSearchAttrib::MsgStatus:
+ useNot = false; // bizarrely, NOT SEEN is wrong, but UNSEEN is right.
+ ignoreValue = true; // the mnemonic is all we need
+ uint32_t status;
+ searchValue->GetStatus(&status);
+
+ switch (status) {
+ case nsMsgMessageFlags::Read:
+ whichMnemonic =
+ op == nsMsgSearchOp::Is ? m_kImapSeen : m_kImapNotSeen;
+ break;
+ case nsMsgMessageFlags::Replied:
+ whichMnemonic =
+ op == nsMsgSearchOp::Is ? m_kImapAnswered : m_kImapNotAnswered;
+ break;
+ case nsMsgMessageFlags::New:
+ whichMnemonic = op == nsMsgSearchOp::Is ? m_kImapNew : m_kImapNotNew;
+ break;
+ case nsMsgMessageFlags::Marked:
+ whichMnemonic =
+ op == nsMsgSearchOp::Is ? m_kImapFlagged : m_kImapNotFlagged;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid search operator");
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ default:
+ if (attrib > nsMsgSearchAttrib::OtherHeader &&
+ attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ nsCString arbitraryHeaderTerm;
+ term->GetArbitraryHeader(arbitraryHeaderTerm);
+ if (!arbitraryHeaderTerm.IsEmpty()) {
+ arbitraryHeader.AssignLiteral(" \"");
+ arbitraryHeader.Append(arbitraryHeaderTerm);
+ arbitraryHeader.AppendLiteral("\" ");
+ whichMnemonic = arbitraryHeader.get();
+ } else
+ return NS_ERROR_FAILURE;
+ } else {
+ NS_ASSERTION(false, "invalid search operator");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ char* value = nullptr;
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+
+ bool valueWasAllocated = false;
+ if (attrib == nsMsgSearchAttrib::Date) {
+ // note that there used to be code here that encoded an RFC822 date for imap
+ // searches. The IMAP RFC 2060 is misleading to the point that it looks like
+ // it requires an RFC822 date but really it expects dd-mmm-yyyy, like dredd,
+ // and refers to the RFC822 date only in that the dd-mmm-yyyy date will
+ // match the RFC822 date within the message.
+
+ PRTime adjustedDate;
+ searchValue->GetDate(&adjustedDate);
+ if (whichMnemonic == m_kImapSince) {
+ // it looks like the IMAP server searches on Since includes the date in
+ // question... our UI presents Is, IsGreater and IsLessThan. For the
+ // IsGreater case (m_kImapSince) we need to adjust the date so we get
+ // greater than and not greater than or equal to which is what the IMAP
+ // server wants to search on won't work on Mac.
+ adjustedDate += PR_USEC_PER_DAY;
+ }
+
+ PRExplodedTime exploded;
+ PR_ExplodeTime(adjustedDate, PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded);
+ // strftime (dateBuf, sizeof(dateBuf), "%d-%b-%Y", localtime (/*
+ // &term->m_value.u.date */ &adjustedDate));
+ value = dateBuf;
+ } else {
+ if (attrib == nsMsgSearchAttrib::AgeInDays) {
+ // okay, take the current date, subtract off the age in days, then do an
+ // appropriate Date search on the resulting day.
+ int32_t ageInDays;
+
+ searchValue->GetAge(&ageInDays);
+
+ PRTime now = PR_Now();
+ PRTime matchDay = now - ageInDays * PR_USEC_PER_DAY;
+
+ PRExplodedTime exploded;
+ PR_ExplodeTime(matchDay, PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded);
+ // strftime (dateBuf, sizeof(dateBuf), "%d-%b-%Y", localtime
+ // (&matchDay));
+ value = dateBuf;
+ } else if (attrib == nsMsgSearchAttrib::Size) {
+ uint32_t sizeValue;
+ nsAutoCString searchTermValue;
+ searchValue->GetSize(&sizeValue);
+
+ // Multiply by 1024 to get into kb resolution
+ sizeValue *= 1024;
+
+ // Ensure that greater than is really greater than
+ // in kb resolution.
+ if (op == nsMsgSearchOp::IsGreaterThan) sizeValue += 1024;
+
+ searchTermValue.AppendInt(sizeValue);
+
+ value = ToNewCString(searchTermValue);
+ valueWasAllocated = true;
+ } else
+
+ if (IS_STRING_ATTRIBUTE(attrib)) {
+ char16_t*
+ convertedValue; // = reallyDredd ? MSG_EscapeSearchUrl
+ // (term->m_value.u.string) :
+ // msg_EscapeImapSearchProtocol(term->m_value.u.string);
+ nsString searchTermValue;
+ searchValue->GetStr(searchTermValue);
+ // Ugly switch for Korean mail/news charsets.
+ // We want to do this here because here is where
+ // we know what charset we want to use.
+#ifdef DOING_CHARSET
+ if (reallyDredd)
+ dest_csid = INTL_DefaultNewsCharSetID(dest_csid);
+ else
+ dest_csid = INTL_DefaultMailCharSetID(dest_csid);
+#endif
+
+ // do all sorts of crazy escaping
+ convertedValue = reallyDredd
+ ? EscapeSearchUrl(searchTermValue.get())
+ : EscapeImapSearchProtocol(searchTermValue.get());
+ useQuotes =
+ ((!reallyDredd ||
+ (nsDependentString(convertedValue).FindChar(char16_t(' ')) !=
+ -1)) &&
+ (attrib != nsMsgSearchAttrib::Keywords));
+ // now convert to char* and escape quoted_specials
+ nsAutoCString valueStr;
+ nsresult rv = nsMsgI18NConvertFromUnicode(
+ NS_LossyConvertUTF16toASCII(destCharset),
+ nsDependentString(convertedValue), valueStr);
+ if (NS_SUCCEEDED(rv)) {
+ const char* vptr = valueStr.get();
+ // max escaped length is one extra character for every character in the
+ // cmd.
+ mozilla::UniquePtr<char[]> newValue =
+ mozilla::MakeUnique<char[]>(2 * strlen(vptr) + 1);
+ if (newValue) {
+ char* p = newValue.get();
+ while (1) {
+ char ch = *vptr++;
+ if (!ch) break;
+ if ((useQuotes ? ch == '"' : 0) || ch == '\\') *p++ = '\\';
+ *p++ = ch;
+ }
+ *p = '\0';
+ value = strdup(newValue.get()); // realloc down to smaller size
+ }
+ } else
+ value = strdup("");
+ free(convertedValue);
+ valueWasAllocated = true;
+ }
+ }
+
+ // this should be rewritten to use nsCString
+ int subLen = (value ? strlen(value) : 0) + (useNot ? strlen(m_kImapNot) : 0) +
+ strlen(m_kImapHeader);
+ int len =
+ strlen(whichMnemonic) + subLen + (useQuotes ? 2 : 0) +
+ (orHeaderMnemonic
+ ? (subLen + strlen(m_kImapOr) + strlen(orHeaderMnemonic) + 2 /*""*/)
+ : 0) +
+ 10; // add slough for imap string literals
+ char* encoding = new char[len];
+ if (encoding) {
+ encoding[0] = '\0';
+ // Remember: if ToOrCC and useNot then the expression becomes NOT To AND Not
+ // CC as opposed to (NOT TO) || (NOT CC)
+ if (orHeaderMnemonic && !useNot) PL_strcat(encoding, m_kImapOr);
+ if (useNot) PL_strcat(encoding, m_kImapNot);
+ if (!arbitraryHeader.IsEmpty()) PL_strcat(encoding, m_kImapHeader);
+ PL_strcat(encoding, whichMnemonic);
+ if (!ignoreValue)
+ err = EncodeImapValue(encoding, value, useQuotes, reallyDredd);
+
+ if (orHeaderMnemonic) {
+ if (useNot) PL_strcat(encoding, m_kImapNot);
+
+ PL_strcat(encoding, m_kImapHeader);
+
+ PL_strcat(encoding, orHeaderMnemonic);
+ if (!ignoreValue)
+ err = EncodeImapValue(encoding, value, useQuotes, reallyDredd);
+ }
+
+ // kmcentee, don't let the encoding end with whitespace,
+ // this throws off later url STRCMP
+ if (*encoding && *(encoding + strlen(encoding) - 1) == ' ')
+ *(encoding + strlen(encoding) - 1) = '\0';
+ }
+
+ if (value && valueWasAllocated) free(value);
+
+ *ppOutTerm = encoding;
+
+ return err;
+}
+
+nsresult nsMsgSearchAdapter::EncodeImapValue(char* encoding, const char* value,
+ bool useQuotes, bool reallyDredd) {
+ // By NNTP RFC, SEARCH HEADER SUBJECT "" is legal and means 'find messages
+ // without a subject header'
+ if (!reallyDredd) {
+ // By IMAP RFC, SEARCH HEADER SUBJECT "" is illegal and will generate an
+ // error from the server
+ if (!value || !value[0]) return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!NS_IsAscii(value)) {
+ nsAutoCString lengthStr;
+ PL_strcat(encoding, "{");
+ lengthStr.AppendInt((int32_t)strlen(value));
+ PL_strcat(encoding, lengthStr.get());
+ PL_strcat(encoding, "}" CRLF);
+ PL_strcat(encoding, value);
+ return NS_OK;
+ }
+ if (useQuotes) PL_strcat(encoding, "\"");
+ PL_strcat(encoding, value);
+ if (useQuotes) PL_strcat(encoding, "\"");
+
+ return NS_OK;
+}
+
+nsresult nsMsgSearchAdapter::EncodeImap(
+ char** ppOutEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms,
+ const char16_t* srcCharset, const char16_t* destCharset, bool reallyDredd) {
+ // i've left the old code (before using CBoolExpression for debugging purposes
+ // to make sure that the new code generates the same encoding string as the
+ // old code.....
+
+ nsresult err = NS_OK;
+ *ppOutEncoding = nullptr;
+
+ // create our expression
+ nsMsgSearchBoolExpression* expression = new nsMsgSearchBoolExpression();
+ if (!expression) return NS_ERROR_OUT_OF_MEMORY;
+
+ for (nsIMsgSearchTerm* pTerm : searchTerms) {
+ bool matchAll;
+ pTerm->GetMatchAll(&matchAll);
+ if (matchAll) continue;
+ char* termEncoding;
+ err = EncodeImapTerm(pTerm, reallyDredd, srcCharset, destCharset,
+ &termEncoding);
+ if (NS_SUCCEEDED(err) && nullptr != termEncoding) {
+ expression = nsMsgSearchBoolExpression::AddSearchTerm(expression, pTerm,
+ termEncoding);
+ delete[] termEncoding;
+ } else {
+ break;
+ }
+ }
+
+ if (NS_SUCCEEDED(err)) {
+ // Catenate the intermediate encodings together into a big string
+ nsAutoCString encodingBuff;
+
+ if (!reallyDredd) encodingBuff.Append(m_kImapUnDeleted);
+
+ expression->GenerateEncodeStr(&encodingBuff);
+ *ppOutEncoding = ToNewCString(encodingBuff);
+ }
+
+ delete expression;
+
+ return err;
+}
+
+char* nsMsgSearchAdapter::TransformSpacesToStars(
+ const char* spaceString, msg_TransformType transformType) {
+ char* starString;
+
+ if (transformType == kOverwrite) {
+ if ((starString = strdup(spaceString)) != nullptr) {
+ char* star = starString;
+ while ((star = PL_strchr(star, ' ')) != nullptr) *star = '*';
+ }
+ } else {
+ int i, count;
+
+ for (i = 0, count = 0; spaceString[i];) {
+ if (spaceString[i++] == ' ') {
+ count++;
+ while (spaceString[i] && spaceString[i] == ' ') i++;
+ }
+ }
+
+ if (transformType == kSurround) count *= 2;
+
+ if (count > 0) {
+ if ((starString = (char*)PR_Malloc(i + count + 1)) != nullptr) {
+ int j;
+
+ for (i = 0, j = 0; spaceString[i];) {
+ if (spaceString[i] == ' ') {
+ starString[j++] = '*';
+ starString[j++] = ' ';
+ if (transformType == kSurround) starString[j++] = '*';
+
+ i++;
+ while (spaceString[i] && spaceString[i] == ' ') i++;
+ } else
+ starString[j++] = spaceString[i++];
+ }
+ starString[j] = 0;
+ }
+ } else
+ starString = strdup(spaceString);
+ }
+
+ return starString;
+}
+
+//-----------------------------------------------------------------------------
+//------------------- Validity checking for menu items etc. -------------------
+//-----------------------------------------------------------------------------
+
+nsMsgSearchValidityTable::nsMsgSearchValidityTable() {
+ // Set everything to be unavailable and disabled
+ for (int i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++)
+ for (int j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) {
+ SetAvailable(i, j, false);
+ SetEnabled(i, j, false);
+ SetValidButNotShown(i, j, false);
+ }
+ m_numAvailAttribs =
+ 0; // # of attributes marked with at least one available operator
+ // assume default is Subject, which it is for mail and news search
+ // it's not for LDAP, so we'll call SetDefaultAttrib()
+ m_defaultAttrib = nsMsgSearchAttrib::Subject;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchValidityTable, nsIMsgSearchValidityTable)
+
+nsresult nsMsgSearchValidityTable::GetNumAvailAttribs(int32_t* aResult) {
+ m_numAvailAttribs = 0;
+ for (int i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++)
+ for (int j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) {
+ bool available;
+ GetAvailable(i, j, &available);
+ if (available) {
+ m_numAvailAttribs++;
+ break;
+ }
+ }
+ *aResult = m_numAvailAttribs;
+ return NS_OK;
+}
+
+nsresult nsMsgSearchValidityTable::GetAvailableAttributes(
+ nsTArray<nsMsgSearchAttribValue>& aResult) {
+ aResult.Clear();
+ int32_t i, j;
+ for (i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++) {
+ for (j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) {
+ if (m_table[i][j].bitAvailable) {
+ aResult.AppendElement(static_cast<nsMsgSearchAttribValue>(i));
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSearchValidityTable::GetAvailableOperators(
+ nsMsgSearchAttribValue aAttribute, nsTArray<nsMsgSearchOpValue>& aResult) {
+ aResult.Clear();
+
+ nsMsgSearchAttribValue attr;
+ if (aAttribute == nsMsgSearchAttrib::Default)
+ attr = m_defaultAttrib;
+ else
+ attr = std::min(aAttribute,
+ (nsMsgSearchAttribValue)nsMsgSearchAttrib::OtherHeader);
+
+ int32_t i;
+ for (i = 0; i < nsMsgSearchOp::kNumMsgSearchOperators; i++) {
+ if (m_table[attr][i].bitAvailable) {
+ aResult.AppendElement(static_cast<nsMsgSearchOpValue>(i));
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchValidityTable::SetDefaultAttrib(nsMsgSearchAttribValue aAttribute) {
+ m_defaultAttrib = aAttribute;
+ return NS_OK;
+}
+
+nsMsgSearchValidityManager::nsMsgSearchValidityManager() {}
+
+nsMsgSearchValidityManager::~nsMsgSearchValidityManager() {
+ // tables released by nsCOMPtr
+}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchValidityManager, nsIMsgSearchValidityManager)
+
+//-----------------------------------------------------------------------------
+// Bottleneck accesses to the objects so we can allocate and initialize them
+// lazily. This way, there's no heap overhead for the validity tables until the
+// user actually searches that scope.
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP nsMsgSearchValidityManager::GetTable(
+ int whichTable, nsIMsgSearchValidityTable** ppOutTable) {
+ NS_ENSURE_ARG_POINTER(ppOutTable);
+
+ nsresult rv;
+ *ppOutTable = nullptr;
+
+ nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ nsCString customHeaders;
+ if (NS_SUCCEEDED(rv)) pref->GetCharPref(PREF_CUSTOM_HEADERS, customHeaders);
+
+ switch (whichTable) {
+ case nsMsgSearchScope::offlineMail:
+ if (!m_offlineMailTable) rv = InitOfflineMailTable();
+ if (m_offlineMailTable)
+ rv = SetOtherHeadersInTable(m_offlineMailTable, customHeaders.get());
+ *ppOutTable = m_offlineMailTable;
+ break;
+ case nsMsgSearchScope::offlineMailFilter:
+ if (!m_offlineMailFilterTable) rv = InitOfflineMailFilterTable();
+ if (m_offlineMailFilterTable)
+ rv = SetOtherHeadersInTable(m_offlineMailFilterTable,
+ customHeaders.get());
+ *ppOutTable = m_offlineMailFilterTable;
+ break;
+ case nsMsgSearchScope::onlineMail:
+ if (!m_onlineMailTable) rv = InitOnlineMailTable();
+ if (m_onlineMailTable)
+ rv = SetOtherHeadersInTable(m_onlineMailTable, customHeaders.get());
+ *ppOutTable = m_onlineMailTable;
+ break;
+ case nsMsgSearchScope::onlineMailFilter:
+ if (!m_onlineMailFilterTable) rv = InitOnlineMailFilterTable();
+ if (m_onlineMailFilterTable)
+ rv = SetOtherHeadersInTable(m_onlineMailFilterTable,
+ customHeaders.get());
+ *ppOutTable = m_onlineMailFilterTable;
+ break;
+ case nsMsgSearchScope::news:
+ if (!m_newsTable) rv = InitNewsTable();
+ if (m_newsTable)
+ rv = SetOtherHeadersInTable(m_newsTable, customHeaders.get());
+ *ppOutTable = m_newsTable;
+ break;
+ case nsMsgSearchScope::newsFilter:
+ if (!m_newsFilterTable) rv = InitNewsFilterTable();
+ if (m_newsFilterTable)
+ rv = SetOtherHeadersInTable(m_newsFilterTable, customHeaders.get());
+ *ppOutTable = m_newsFilterTable;
+ break;
+ case nsMsgSearchScope::localNews:
+ if (!m_localNewsTable) rv = InitLocalNewsTable();
+ if (m_localNewsTable)
+ rv = SetOtherHeadersInTable(m_localNewsTable, customHeaders.get());
+ *ppOutTable = m_localNewsTable;
+ break;
+ case nsMsgSearchScope::localNewsJunk:
+ if (!m_localNewsJunkTable) rv = InitLocalNewsJunkTable();
+ if (m_localNewsJunkTable)
+ rv = SetOtherHeadersInTable(m_localNewsJunkTable, customHeaders.get());
+ *ppOutTable = m_localNewsJunkTable;
+ break;
+ case nsMsgSearchScope::localNewsBody:
+ if (!m_localNewsBodyTable) rv = InitLocalNewsBodyTable();
+ if (m_localNewsBodyTable)
+ rv = SetOtherHeadersInTable(m_localNewsBodyTable, customHeaders.get());
+ *ppOutTable = m_localNewsBodyTable;
+ break;
+ case nsMsgSearchScope::localNewsJunkBody:
+ if (!m_localNewsJunkBodyTable) rv = InitLocalNewsJunkBodyTable();
+ if (m_localNewsJunkBodyTable)
+ rv = SetOtherHeadersInTable(m_localNewsJunkBodyTable,
+ customHeaders.get());
+ *ppOutTable = m_localNewsJunkBodyTable;
+ break;
+
+ case nsMsgSearchScope::onlineManual:
+ if (!m_onlineManualFilterTable) rv = InitOnlineManualFilterTable();
+ if (m_onlineManualFilterTable)
+ rv = SetOtherHeadersInTable(m_onlineManualFilterTable,
+ customHeaders.get());
+ *ppOutTable = m_onlineManualFilterTable;
+ break;
+ case nsMsgSearchScope::LDAP:
+ if (!m_ldapTable) rv = InitLdapTable();
+ *ppOutTable = m_ldapTable;
+ break;
+ case nsMsgSearchScope::LDAPAnd:
+ if (!m_ldapAndTable) rv = InitLdapAndTable();
+ *ppOutTable = m_ldapAndTable;
+ break;
+ case nsMsgSearchScope::LocalAB:
+ if (!m_localABTable) rv = InitLocalABTable();
+ *ppOutTable = m_localABTable;
+ break;
+ case nsMsgSearchScope::LocalABAnd:
+ if (!m_localABAndTable) rv = InitLocalABAndTable();
+ *ppOutTable = m_localABAndTable;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid table type");
+ rv = NS_MSG_ERROR_INVALID_SEARCH_TERM;
+ }
+
+ NS_IF_ADDREF(*ppOutTable); // Was populated from member variable.
+ return rv;
+}
+
+// mapping between ordered attribute values, and property strings
+// see search-attributes.properties
+static struct {
+ nsMsgSearchAttribValue id;
+ const char* property;
+} nsMsgSearchAttribMap[] = {
+ {nsMsgSearchAttrib::Subject, "Subject"},
+ {nsMsgSearchAttrib::Sender, "From"},
+ {nsMsgSearchAttrib::Body, "Body"},
+ {nsMsgSearchAttrib::Date, "Date"},
+ {nsMsgSearchAttrib::Priority, "Priority"},
+ {nsMsgSearchAttrib::MsgStatus, "Status"},
+ {nsMsgSearchAttrib::To, "To"},
+ {nsMsgSearchAttrib::CC, "Cc"},
+ {nsMsgSearchAttrib::ToOrCC, "ToOrCc"},
+ {nsMsgSearchAttrib::AgeInDays, "AgeInDays"},
+ {nsMsgSearchAttrib::Size, "SizeKB"},
+ {nsMsgSearchAttrib::Keywords, "Tags"},
+ {nsMsgSearchAttrib::Name, "AnyName"},
+ {nsMsgSearchAttrib::DisplayName, "DisplayName"},
+ {nsMsgSearchAttrib::Nickname, "Nickname"},
+ {nsMsgSearchAttrib::ScreenName, "ScreenName"},
+ {nsMsgSearchAttrib::Email, "Email"},
+ {nsMsgSearchAttrib::AdditionalEmail, "AdditionalEmail"},
+ {nsMsgSearchAttrib::PhoneNumber, "AnyNumber"},
+ {nsMsgSearchAttrib::WorkPhone, "WorkPhone"},
+ {nsMsgSearchAttrib::HomePhone, "HomePhone"},
+ {nsMsgSearchAttrib::Fax, "Fax"},
+ {nsMsgSearchAttrib::Pager, "Pager"},
+ {nsMsgSearchAttrib::Mobile, "Mobile"},
+ {nsMsgSearchAttrib::City, "City"},
+ {nsMsgSearchAttrib::Street, "Street"},
+ {nsMsgSearchAttrib::Title, "Title"},
+ {nsMsgSearchAttrib::Organization, "Organization"},
+ {nsMsgSearchAttrib::Department, "Department"},
+ {nsMsgSearchAttrib::AllAddresses, "FromToCcOrBcc"},
+ {nsMsgSearchAttrib::JunkScoreOrigin, "JunkScoreOrigin"},
+ {nsMsgSearchAttrib::JunkPercent, "JunkPercent"},
+ {nsMsgSearchAttrib::HasAttachmentStatus, "AttachmentStatus"},
+ {nsMsgSearchAttrib::JunkStatus, "JunkStatus"},
+ {nsMsgSearchAttrib::OtherHeader, "Customize"},
+ // the last id is -1 to denote end of table
+ {-1, ""}};
+
+NS_IMETHODIMP
+nsMsgSearchValidityManager::GetAttributeProperty(
+ nsMsgSearchAttribValue aSearchAttribute, nsAString& aProperty) {
+ for (int32_t i = 0; nsMsgSearchAttribMap[i].id >= 0; ++i) {
+ if (nsMsgSearchAttribMap[i].id == aSearchAttribute) {
+ aProperty.Assign(NS_ConvertUTF8toUTF16(nsMsgSearchAttribMap[i].property));
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgSearchValidityManager::NewTable(
+ nsIMsgSearchValidityTable** aTable) {
+ NS_ENSURE_ARG_POINTER(aTable);
+ NS_ADDREF(*aTable = new nsMsgSearchValidityTable);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchValidityManager::SetOtherHeadersInTable(
+ nsIMsgSearchValidityTable* aTable, const char* customHeaders) {
+ uint32_t customHeadersLength = strlen(customHeaders);
+ uint32_t numHeaders = 0;
+ if (customHeadersLength) {
+ nsAutoCString hdrStr(customHeaders);
+ hdrStr.StripWhitespace(); // remove whitespace before parsing
+ char* newStr = hdrStr.BeginWriting();
+ char* token = NS_strtok(":", &newStr);
+ while (token) {
+ numHeaders++;
+ token = NS_strtok(":", &newStr);
+ }
+ }
+
+ NS_ASSERTION(nsMsgSearchAttrib::OtherHeader + numHeaders <
+ nsMsgSearchAttrib::kNumMsgSearchAttributes,
+ "more headers than the table can hold");
+
+ uint32_t maxHdrs =
+ std::min(nsMsgSearchAttrib::OtherHeader + numHeaders + 1,
+ (uint32_t)nsMsgSearchAttrib::kNumMsgSearchAttributes);
+ for (uint32_t i = nsMsgSearchAttrib::OtherHeader + 1; i < maxHdrs; i++) {
+ // clang-format off
+ aTable->SetAvailable(i, nsMsgSearchOp::Contains, 1); // added for arbitrary headers
+ aTable->SetEnabled (i, nsMsgSearchOp::Contains, 1);
+ aTable->SetAvailable(i, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetEnabled (i, nsMsgSearchOp::DoesntContain, 1);
+ aTable->SetAvailable(i, nsMsgSearchOp::Is, 1);
+ aTable->SetEnabled (i, nsMsgSearchOp::Is, 1);
+ aTable->SetAvailable(i, nsMsgSearchOp::Isnt, 1);
+ aTable->SetEnabled (i, nsMsgSearchOp::Isnt, 1);
+ // clang-format on
+ }
+ // because custom headers can change; so reset the table for those which are
+ // no longer used.
+ for (uint32_t j = maxHdrs; j < nsMsgSearchAttrib::kNumMsgSearchAttributes;
+ j++) {
+ for (uint32_t k = 0; k < nsMsgSearchOp::kNumMsgSearchOperators; k++) {
+ aTable->SetAvailable(j, k, 0);
+ aTable->SetEnabled(j, k, 0);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSearchValidityManager::EnableDirectoryAttribute(
+ nsIMsgSearchValidityTable* table, nsMsgSearchAttribValue aSearchAttrib) {
+ // clang-format off
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Contains, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Contains, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::DoesntContain, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::DoesntContain, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Is, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Is, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Isnt, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Isnt, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::BeginsWith, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::BeginsWith, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::EndsWith, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::EndsWith, 1);
+ table->SetAvailable(aSearchAttrib, nsMsgSearchOp::SoundsLike, 1);
+ table->SetEnabled (aSearchAttrib, nsMsgSearchOp::SoundsLike, 1);
+ // clang-format on
+ return NS_OK;
+}
+
+nsresult nsMsgSearchValidityManager::InitLdapTable() {
+ NS_ASSERTION(!m_ldapTable, "don't call this twice!");
+
+ nsresult rv = NewTable(getter_AddRefs(m_ldapTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetUpABTable(m_ldapTable, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgSearchValidityManager::InitLdapAndTable() {
+ NS_ASSERTION(!m_ldapAndTable, "don't call this twice!");
+
+ nsresult rv = NewTable(getter_AddRefs(m_ldapAndTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetUpABTable(m_ldapAndTable, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgSearchValidityManager::InitLocalABTable() {
+ NS_ASSERTION(!m_localABTable, "don't call this twice!");
+
+ nsresult rv = NewTable(getter_AddRefs(m_localABTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetUpABTable(m_localABTable, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgSearchValidityManager::InitLocalABAndTable() {
+ NS_ASSERTION(!m_localABAndTable, "don't call this twice!");
+
+ nsresult rv = NewTable(getter_AddRefs(m_localABAndTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetUpABTable(m_localABAndTable, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsMsgSearchValidityManager::SetUpABTable(
+ nsIMsgSearchValidityTable* aTable, bool isOrTable) {
+ nsresult rv = aTable->SetDefaultAttrib(
+ isOrTable ? nsMsgSearchAttrib::Name : nsMsgSearchAttrib::DisplayName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isOrTable) {
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::PhoneNumber);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::DisplayName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Email);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::AdditionalEmail);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::ScreenName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Street);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::City);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Organization);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Department);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Nickname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::WorkPhone);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::HomePhone);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Fax);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Pager);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Mobile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
diff --git a/comm/mailnews/search/src/nsMsgSearchImap.h b/comm/mailnews/search/src/nsMsgSearchImap.h
new file mode 100644
index 0000000000..92dbed8dcc
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchImap.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSearchImap_h__
+# include "mozilla/Attributes.h"
+# include "nsMsgSearchAdapter.h"
+
+//-----------------------------------------------------------------------------
+//---------- Adapter class for searching online (IMAP) folders ----------------
+//-----------------------------------------------------------------------------
+
+class nsMsgSearchOnlineMail : public nsMsgSearchAdapter {
+ public:
+ nsMsgSearchOnlineMail(nsMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList);
+ virtual ~nsMsgSearchOnlineMail();
+
+ NS_IMETHOD ValidateTerms() override;
+ NS_IMETHOD Search(bool* aDone) override;
+ NS_IMETHOD GetEncoding(char** result) override;
+ NS_IMETHOD AddResultElement(nsIMsgDBHdr*) override;
+
+ static nsresult Encode(nsCString& ppEncoding,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms,
+ const char16_t* destCharset,
+ nsIMsgSearchScopeTerm* scope);
+
+ protected:
+ nsCString m_encoding;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgSearchNews.cpp b/comm/mailnews/search/src/nsMsgSearchNews.cpp
new file mode 100644
index 0000000000..022a80e79f
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchNews.cpp
@@ -0,0 +1,452 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "msgCore.h"
+#include "nsMsgSearchAdapter.h"
+#include "nsUnicharUtils.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsMsgResultElement.h"
+#include "nsMsgSearchTerm.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgSearchNews.h"
+#include "nsIDBFolderInfo.h"
+#include "prprf.h"
+#include "nsIMsgDatabase.h"
+#include "nsMemory.h"
+#include <ctype.h>
+
+// Implementation of search for IMAP mail folders
+
+// Implementation of search for newsgroups
+
+//-----------------------------------------------------------------------------
+//----------- Adapter class for searching XPAT-capable news servers -----------
+//-----------------------------------------------------------------------------
+
+const char* nsMsgSearchNews::m_kNntpFrom = "FROM ";
+const char* nsMsgSearchNews::m_kNntpSubject = "SUBJECT ";
+const char* nsMsgSearchNews::m_kTermSeparator = "/";
+
+nsMsgSearchNews::nsMsgSearchNews(
+ nsMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList)
+ : nsMsgSearchAdapter(scope, termList) {
+ m_searchType = ST_UNINITIALIZED;
+}
+
+nsMsgSearchNews::~nsMsgSearchNews() {}
+
+nsresult nsMsgSearchNews::ValidateTerms() {
+ nsresult err = nsMsgSearchAdapter::ValidateTerms();
+ if (NS_OK == err) {
+ err = Encode(&m_encoding);
+ }
+
+ return err;
+}
+
+nsresult nsMsgSearchNews::Search(bool* aDone) {
+ // the state machine runs in the news: handler
+ nsresult err = NS_ERROR_NOT_IMPLEMENTED;
+ return err;
+}
+
+char16_t* nsMsgSearchNews::EncodeToWildmat(const char16_t* value) {
+ // Here we take advantage of XPAT's use of the wildmat format, which allows
+ // a case-insensitive match by specifying each case possibility for each
+ // character So, "FooBar" is encoded as "[Ff][Oo][Bb][Aa][Rr]"
+
+ char16_t* caseInsensitiveValue =
+ (char16_t*)moz_xmalloc(sizeof(char16_t) * ((4 * NS_strlen(value)) + 1));
+ if (caseInsensitiveValue) {
+ char16_t* walkValue = caseInsensitiveValue;
+ while (*value) {
+ if (isalpha(*value)) {
+ *walkValue++ = (char16_t)'[';
+ *walkValue++ = ToUpperCase((char16_t)*value);
+ *walkValue++ = ToLowerCase((char16_t)*value);
+ *walkValue++ = (char16_t)']';
+ } else
+ *walkValue++ = *value;
+ value++;
+ }
+ *walkValue = 0;
+ }
+ return caseInsensitiveValue;
+}
+
+char* nsMsgSearchNews::EncodeTerm(nsIMsgSearchTerm* term) {
+ // Develop an XPAT-style encoding for the search term
+
+ NS_ASSERTION(term, "null term");
+ if (!term) return nullptr;
+
+ // Find a string to represent the attribute
+ const char* attribEncoding = nullptr;
+ nsMsgSearchAttribValue attrib;
+
+ term->GetAttrib(&attrib);
+
+ switch (attrib) {
+ case nsMsgSearchAttrib::Sender:
+ attribEncoding = m_kNntpFrom;
+ break;
+ case nsMsgSearchAttrib::Subject:
+ attribEncoding = m_kNntpSubject;
+ break;
+ default:
+ nsCString header;
+ term->GetArbitraryHeader(header);
+ if (header.IsEmpty()) {
+ NS_ASSERTION(false, "malformed search"); // malformed search term?
+ return nullptr;
+ }
+ attribEncoding = header.get();
+ }
+
+ // Build a string to represent the string pattern
+ bool leadingStar = false;
+ bool trailingStar = false;
+ nsMsgSearchOpValue op;
+ term->GetOp(&op);
+
+ switch (op) {
+ case nsMsgSearchOp::Contains:
+ leadingStar = true;
+ trailingStar = true;
+ break;
+ case nsMsgSearchOp::Is:
+ break;
+ case nsMsgSearchOp::BeginsWith:
+ trailingStar = true;
+ break;
+ case nsMsgSearchOp::EndsWith:
+ leadingStar = true;
+ break;
+ default:
+ NS_ASSERTION(false, "malformed search"); // malformed search term?
+ return nullptr;
+ }
+
+ // ### i18N problem Get the csid from FE, which is the correct csid for term
+ // int16 wincsid = INTL_GetCharSetID(INTL_DefaultTextWidgetCsidSel);
+
+ // Do INTL_FormatNNTPXPATInRFC1522Format trick for non-ASCII string
+ // unsigned char *intlNonRFC1522Value = INTL_FormatNNTPXPATInNonRFC1522Format
+ // (wincsid, (unsigned char*)term->m_value.u.string);
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+
+ nsresult rv = term->GetValue(getter_AddRefs(searchValue));
+ if (NS_FAILED(rv) || !searchValue) return nullptr;
+
+ nsString intlNonRFC1522Value;
+ rv = searchValue->GetStr(intlNonRFC1522Value);
+ if (NS_FAILED(rv) || intlNonRFC1522Value.IsEmpty()) return nullptr;
+
+ char16_t* caseInsensitiveValue = EncodeToWildmat(intlNonRFC1522Value.get());
+ if (!caseInsensitiveValue) return nullptr;
+
+ // TO DO: Do INTL_FormatNNTPXPATInRFC1522Format trick for non-ASCII string
+ // Unfortunately, we currently do not handle xxx or xxx search in XPAT
+ // Need to add the INTL_FormatNNTPXPATInRFC1522Format call after we can do
+ // that so we should search a string in either RFC1522 format and non-RFC1522
+ // format
+
+ char16_t* escapedValue = EscapeSearchUrl(caseInsensitiveValue);
+ free(caseInsensitiveValue);
+ if (!escapedValue) return nullptr;
+
+ nsAutoCString pattern;
+
+ if (leadingStar) pattern.Append('*');
+ pattern.Append(NS_ConvertUTF16toUTF8(escapedValue));
+ if (trailingStar) pattern.Append('*');
+
+ // Combine the XPAT command syntax with the attribute and the pattern to
+ // form the term encoding
+ const char xpatTemplate[] = "XPAT %s 1- %s";
+ int termLength = (sizeof(xpatTemplate) - 1) + strlen(attribEncoding) +
+ pattern.Length() + 1;
+ char* termEncoding = new char[termLength];
+ if (termEncoding)
+ PR_snprintf(termEncoding, termLength, xpatTemplate, attribEncoding,
+ pattern.get());
+
+ return termEncoding;
+}
+
+nsresult nsMsgSearchNews::GetEncoding(char** result) {
+ NS_ENSURE_ARG(result);
+ *result = ToNewCString(m_encoding);
+ return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult nsMsgSearchNews::Encode(nsCString* outEncoding) {
+ NS_ASSERTION(outEncoding, "no out encoding");
+ if (!outEncoding) return NS_ERROR_NULL_POINTER;
+
+ nsresult err = NS_OK;
+
+ uint32_t numTerms = m_searchTerms.Length();
+
+ char** intermediateEncodings = new char*[numTerms];
+ if (intermediateEncodings) {
+ // Build an XPAT command for each term
+ int encodingLength = 0;
+ for (uint32_t i = 0; i < numTerms; i++) {
+ nsIMsgSearchTerm* pTerm = m_searchTerms[i];
+ // set boolean OR term if any of the search terms are an OR...this only
+ // works if we are using homogeneous boolean operators.
+ bool isBooleanOpAnd;
+ pTerm->GetBooleanAnd(&isBooleanOpAnd);
+ m_searchType = isBooleanOpAnd ? ST_AND_SEARCH : ST_OR_SEARCH;
+
+ intermediateEncodings[i] = EncodeTerm(pTerm);
+ if (intermediateEncodings[i])
+ encodingLength +=
+ strlen(intermediateEncodings[i]) + strlen(m_kTermSeparator);
+ }
+ encodingLength += strlen("?search");
+ // Combine all the term encodings into one big encoding
+ char* encoding = new char[encodingLength + 1];
+ if (encoding) {
+ PL_strcpy(encoding, "?search");
+
+ for (uint32_t i = 0; i < numTerms; i++) {
+ if (intermediateEncodings[i]) {
+ PL_strcat(encoding, m_kTermSeparator);
+ PL_strcat(encoding, intermediateEncodings[i]);
+ delete[] intermediateEncodings[i];
+ }
+ }
+ *outEncoding = encoding;
+ } else
+ err = NS_ERROR_OUT_OF_MEMORY;
+ } else
+ err = NS_ERROR_OUT_OF_MEMORY;
+ delete[] intermediateEncodings;
+
+ return err;
+}
+
+NS_IMETHODIMP nsMsgSearchNews::AddHit(nsMsgKey key) {
+ m_candidateHits.AppendElement(key);
+ return NS_OK;
+}
+
+/* void CurrentUrlDone (in nsresult exitCode); */
+NS_IMETHODIMP nsMsgSearchNews::CurrentUrlDone(nsresult exitCode) {
+ CollateHits();
+ ReportHits();
+ return NS_OK;
+}
+
+#if 0 // need to switch this to a notify stop loading handler, I think.
+void nsMsgSearchNews::PreExitFunction (URL_Struct * /*url*/, int status, MWContext *context)
+{
+ MSG_SearchFrame *frame = MSG_SearchFrame::FromContext (context);
+ nsMsgSearchNews *adapter = (nsMsgSearchNews*) frame->GetRunningAdapter();
+ adapter->CollateHits();
+ adapter->ReportHits();
+
+ if (status == MK_INTERRUPTED)
+ {
+ adapter->Abort();
+ frame->EndCylonMode();
+ }
+ else
+ {
+ frame->m_idxRunningScope++;
+ if (frame->m_idxRunningScope >= frame->m_scopeList.Count())
+ frame->EndCylonMode();
+ }
+}
+#endif // 0
+
+void nsMsgSearchNews::CollateHits() {
+ // Since the XPAT commands are processed one at a time, the result set for the
+ // entire query is the intersection of results for each XPAT command if an AND
+ // search, otherwise we want the union of all the search hits (minus the
+ // duplicates of course).
+
+ uint32_t size = m_candidateHits.Length();
+ if (!size) return;
+
+ // Sort the article numbers first, so it's easy to tell how many hits
+ // on a given article we got
+ m_candidateHits.Sort();
+
+ // For an OR search we only need to count the first occurrence of a candidate.
+ uint32_t termCount = 1;
+ MOZ_ASSERT(m_searchType != ST_UNINITIALIZED,
+ "m_searchType accessed without being set");
+ if (m_searchType == ST_AND_SEARCH) {
+ // We have a traditional AND search which must be collated. In order to
+ // get promoted into the hits list, a candidate article number must appear
+ // in the results of each XPAT command. So if we fire 3 XPAT commands (one
+ // per search term), the article number must appear 3 times. If it appears
+ // fewer than 3 times, it matched some search terms, but not all.
+ termCount = m_searchTerms.Length();
+ }
+ uint32_t candidateCount = 0;
+ uint32_t candidate = m_candidateHits[0];
+ for (uint32_t index = 0; index < size; ++index) {
+ uint32_t possibleCandidate = m_candidateHits[index];
+ if (candidate == possibleCandidate) {
+ ++candidateCount;
+ } else {
+ candidateCount = 1;
+ candidate = possibleCandidate;
+ }
+ if (candidateCount == termCount) m_hits.AppendElement(candidate);
+ }
+}
+
+void nsMsgSearchNews::ReportHits() {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+
+ nsresult err = m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ if (NS_SUCCEEDED(err) && scopeFolder) {
+ err = scopeFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ }
+
+ if (db) {
+ uint32_t size = m_hits.Length();
+ for (uint32_t i = 0; i < size; ++i) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+
+ db->GetMsgHdrForKey(m_hits.ElementAt(i), getter_AddRefs(header));
+ if (header) ReportHit(header, scopeFolder);
+ }
+ }
+}
+
+// ### this should take an nsIMsgFolder instead of a string location.
+void nsMsgSearchNews::ReportHit(nsIMsgDBHdr* pHeaders, nsIMsgFolder* folder) {
+ // this is totally filched from msg_SearchOfflineMail until I decide whether
+ // the right thing is to get them from the db or from NNTP
+ nsCOMPtr<nsIMsgSearchSession> session;
+ nsCOMPtr<nsIMsgFolder> scopeFolder;
+ m_scope->GetFolder(getter_AddRefs(scopeFolder));
+ m_scope->GetSearchSession(getter_AddRefs(session));
+ if (session) session->AddSearchHit(pHeaders, scopeFolder);
+}
+
+nsresult nsMsgSearchValidityManager::InitNewsTable() {
+ NS_ASSERTION(nullptr == m_newsTable, "don't call this twice!");
+ nsresult rv = NewTable(getter_AddRefs(m_newsTable));
+
+ if (NS_SUCCEEDED(rv)) {
+ // clang-format off
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+#if 0
+ // Size should be handled after the fact...
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+#endif
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ // clang-format on
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchValidityManager::InitNewsFilterTable() {
+ NS_ASSERTION(nullptr == m_newsFilterTable,
+ "news filter table already initted");
+ nsresult rv = NewTable(getter_AddRefs(m_newsFilterTable));
+
+ if (NS_SUCCEEDED(rv)) {
+ // clang-format off
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1);
+
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+ m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+ // clang-format on
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/search/src/nsMsgSearchNews.h b/comm/mailnews/search/src/nsMsgSearchNews.h
new file mode 100644
index 0000000000..38045d978a
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchNews.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _nsMsgSearchNews_h__
+# include "nsMsgSearchAdapter.h"
+# include "MailNewsTypes.h"
+# include "nsTArray.h"
+
+typedef enum search_type {
+ ST_UNINITIALIZED,
+ ST_OR_SEARCH,
+ ST_AND_SEARCH
+} search_type;
+
+//-----------------------------------------------------------------------------
+//---------- Adapter class for searching online (news) folders ----------------
+//-----------------------------------------------------------------------------
+
+class nsMsgSearchNews : public nsMsgSearchAdapter {
+ public:
+ nsMsgSearchNews(nsMsgSearchScopeTerm* scope,
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList);
+ virtual ~nsMsgSearchNews();
+
+ NS_IMETHOD ValidateTerms() override;
+ NS_IMETHOD Search(bool* aDone) override;
+ NS_IMETHOD GetEncoding(char** result) override;
+ NS_IMETHOD AddHit(nsMsgKey key) override;
+ NS_IMETHOD CurrentUrlDone(nsresult exitCode) override;
+
+ virtual nsresult Encode(nsCString* outEncoding);
+ virtual char* EncodeTerm(nsIMsgSearchTerm*);
+ char16_t* EncodeToWildmat(const char16_t*);
+
+ void ReportHits();
+ void CollateHits();
+ void ReportHit(nsIMsgDBHdr* pHeaders, nsIMsgFolder* folder);
+
+ protected:
+ nsCString m_encoding;
+ search_type m_searchType;
+
+ nsTArray<nsMsgKey> m_candidateHits;
+ nsTArray<nsMsgKey> m_hits;
+
+ static const char* m_kNntpFrom;
+ static const char* m_kNntpSubject;
+ static const char* m_kTermSeparator;
+ static const char* m_kUrlPrefix;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgSearchSession.cpp b/comm/mailnews/search/src/nsMsgSearchSession.cpp
new file mode 100644
index 0000000000..51d5d363d2
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchSession.cpp
@@ -0,0 +1,576 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "nsMsgSearchCore.h"
+#include "nsMsgSearchAdapter.h"
+#include "nsMsgSearchBoolExpression.h"
+#include "nsMsgSearchSession.h"
+#include "nsMsgResultElement.h"
+#include "nsMsgSearchTerm.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgWindow.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgLocalSearch.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgSearchSession, nsIMsgSearchSession, nsIUrlListener,
+ nsISupportsWeakReference)
+
+nsMsgSearchSession::nsMsgSearchSession() {
+ m_sortAttribute = nsMsgSearchAttrib::Sender;
+ m_idxRunningScope = 0;
+ m_handlingError = false;
+ m_expressionTree = nullptr;
+ m_searchPaused = false;
+ m_iListener = -1;
+}
+
+nsMsgSearchSession::~nsMsgSearchSession() {
+ InterruptSearch();
+ delete m_expressionTree;
+ DestroyScopeList();
+ DestroyTermList();
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::AddSearchTerm(nsMsgSearchAttribValue attrib,
+ nsMsgSearchOpValue op,
+ nsIMsgSearchValue* value, bool BooleanANDp,
+ const char* customString) {
+ // stupid gcc
+ nsMsgSearchBooleanOperator boolOp;
+ if (BooleanANDp)
+ boolOp = (nsMsgSearchBooleanOperator)nsMsgSearchBooleanOp::BooleanAND;
+ else
+ boolOp = (nsMsgSearchBooleanOperator)nsMsgSearchBooleanOp::BooleanOR;
+ nsMsgSearchTerm* pTerm =
+ new nsMsgSearchTerm(attrib, op, value, boolOp, customString);
+ NS_ENSURE_TRUE(pTerm, NS_ERROR_OUT_OF_MEMORY);
+
+ m_termList.AppendElement(pTerm);
+ // force the expression tree to rebuild whenever we change the terms
+ delete m_expressionTree;
+ m_expressionTree = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::AppendTerm(nsIMsgSearchTerm* aTerm) {
+ NS_ENSURE_ARG_POINTER(aTerm);
+ delete m_expressionTree;
+ m_expressionTree = nullptr;
+ m_termList.AppendElement(aTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::GetSearchTerms(nsTArray<RefPtr<nsIMsgSearchTerm>>& terms) {
+ terms = m_termList.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::SetSearchTerms(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& terms) {
+ m_termList = terms.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::CreateTerm(nsIMsgSearchTerm** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ADDREF(*aResult = new nsMsgSearchTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::RegisterListener(
+ nsIMsgSearchNotify* aListener, int32_t aNotifyFlags) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ m_listenerList.AppendElement(aListener);
+ m_listenerFlagList.AppendElement(aNotifyFlags);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::UnregisterListener(
+ nsIMsgSearchNotify* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ size_t listenerIndex = m_listenerList.IndexOf(aListener);
+ if (listenerIndex != m_listenerList.NoIndex) {
+ m_listenerList.RemoveElementAt(listenerIndex);
+ m_listenerFlagList.RemoveElementAt(listenerIndex);
+
+ // Adjust our iterator if it is active.
+ // Removal of something at a higher index than the iterator does not affect
+ // it; we only care if the the index we were pointing at gets shifted down,
+ // in which case we also want to shift down.
+ if (m_iListener != -1 && (signed)listenerIndex <= m_iListener)
+ m_iListener--;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::GetNumSearchTerms(uint32_t* aNumSearchTerms) {
+ NS_ENSURE_ARG(aNumSearchTerms);
+ *aNumSearchTerms = m_termList.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::GetNthSearchTerm(int32_t whichTerm,
+ nsMsgSearchAttribValue attrib,
+ nsMsgSearchOpValue op,
+ nsIMsgSearchValue* value) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::CountSearchScopes(int32_t* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_scopeList.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::GetNthSearchScope(int32_t which,
+ nsMsgSearchScopeValue* scopeId,
+ nsIMsgFolder** folder) {
+ NS_ENSURE_ARG_POINTER(scopeId);
+ NS_ENSURE_ARG_POINTER(folder);
+
+ nsMsgSearchScopeTerm* scopeTerm = m_scopeList.SafeElementAt(which, nullptr);
+ NS_ENSURE_ARG(scopeTerm);
+
+ *scopeId = scopeTerm->m_attribute;
+ NS_IF_ADDREF(*folder = scopeTerm->m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::AddScopeTerm(nsMsgSearchScopeValue scope,
+ nsIMsgFolder* folder) {
+ if (scope != nsMsgSearchScope::allSearchableGroups) {
+ NS_ASSERTION(folder, "need folder if not searching all groups");
+ NS_ENSURE_TRUE(folder, NS_ERROR_NULL_POINTER);
+ }
+
+ nsMsgSearchScopeTerm* pScopeTerm =
+ new nsMsgSearchScopeTerm(this, scope, folder);
+ NS_ENSURE_TRUE(pScopeTerm, NS_ERROR_OUT_OF_MEMORY);
+
+ m_scopeList.AppendElement(pScopeTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::AddDirectoryScopeTerm(nsMsgSearchScopeValue scope) {
+ nsMsgSearchScopeTerm* pScopeTerm =
+ new nsMsgSearchScopeTerm(this, scope, nullptr);
+ NS_ENSURE_TRUE(pScopeTerm, NS_ERROR_OUT_OF_MEMORY);
+
+ m_scopeList.AppendElement(pScopeTerm);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::ClearScopes() {
+ DestroyScopeList();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::ScopeUsesCustomHeaders(nsMsgSearchScopeValue scope,
+ void* selection, bool forFilters,
+ bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::IsStringAttribute(nsMsgSearchAttribValue attrib,
+ bool* _retval) {
+ // Is this check needed?
+ NS_ENSURE_ARG(_retval);
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::AddAllScopes(nsMsgSearchScopeValue attrib) {
+ // don't think this is needed.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::Search(nsIMsgWindow* aWindow) {
+ nsresult rv = Initialize();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgSearchNotify> listener;
+ m_iListener = 0;
+ while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) {
+ listener = m_listenerList[m_iListener];
+ int32_t listenerFlags = m_listenerFlagList[m_iListener++];
+ if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onNewSearch))
+ listener->OnNewSearch();
+ }
+ m_iListener = -1;
+
+ m_msgWindowWeak = do_GetWeakReference(aWindow);
+
+ return BeginSearching();
+}
+
+NS_IMETHODIMP nsMsgSearchSession::InterruptSearch() {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) {
+ EnableFolderNotifications(true);
+ if (m_idxRunningScope < m_scopeList.Length()) msgWindow->StopUrls();
+
+ while (m_idxRunningScope < m_scopeList.Length()) {
+ ReleaseFolderDBRef();
+ m_idxRunningScope++;
+ }
+ // m_idxRunningScope = m_scopeList.Length() so it will make us not run
+ // another url
+ }
+ if (m_backgroundTimer) {
+ m_backgroundTimer->Cancel();
+ NotifyListenersDone(NS_MSG_SEARCH_INTERRUPTED);
+
+ m_backgroundTimer = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::PauseSearch() {
+ if (m_backgroundTimer) {
+ m_backgroundTimer->Cancel();
+ m_searchPaused = true;
+ return NS_OK;
+ } else
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::ResumeSearch() {
+ if (m_searchPaused) {
+ m_searchPaused = false;
+ return StartTimer();
+ } else
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::GetNumResults(int32_t* aNumResults) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::SetWindow(nsIMsgWindow* aWindow) {
+ m_msgWindowWeak = do_GetWeakReference(aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::GetWindow(nsIMsgWindow** aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+ *aWindow = nullptr;
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ msgWindow.forget(aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::OnStopRunningUrl(nsIURI* url,
+ nsresult aExitCode) {
+ nsCOMPtr<nsIMsgSearchAdapter> runningAdapter;
+
+ nsresult rv = GetRunningAdapter(getter_AddRefs(runningAdapter));
+ // tell the current adapter that the current url has run.
+ if (NS_SUCCEEDED(rv) && runningAdapter) {
+ runningAdapter->CurrentUrlDone(aExitCode);
+ EnableFolderNotifications(true);
+ ReleaseFolderDBRef();
+ }
+ if (++m_idxRunningScope < m_scopeList.Length())
+ DoNextSearch();
+ else
+ NotifyListenersDone(aExitCode);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchSession::Initialize() {
+ // Loop over scope terms, initializing an adapter per term. This
+ // architecture is necessitated by two things:
+ // 1. There might be more than one kind of adapter per if online
+ // *and* offline mail mail folders are selected, or if newsgroups
+ // belonging to Dredd *and* INN are selected
+ // 2. Most of the protocols are only capable of searching one scope at a
+ // time, so we'll do each scope in a separate adapter on the client
+
+ nsMsgSearchScopeTerm* scopeTerm = nullptr;
+ nsresult rv = NS_OK;
+
+ uint32_t numTerms = m_termList.Length();
+ // Ensure that the FE has added scopes and terms to this search
+ NS_ASSERTION(numTerms > 0, "no terms to search!");
+ if (numTerms == 0) return NS_MSG_ERROR_NO_SEARCH_VALUES;
+
+ // if we don't have any search scopes to search, return that code.
+ if (m_scopeList.Length() == 0) return NS_MSG_ERROR_INVALID_SEARCH_SCOPE;
+
+ m_runningUrl.Truncate(); // clear out old url, if any.
+ m_idxRunningScope = 0;
+
+ // If this term list (loosely specified here by the first term) should be
+ // scheduled in parallel, build up a list of scopes to do the round-robin
+ // scheduling
+ for (uint32_t i = 0; i < m_scopeList.Length() && NS_SUCCEEDED(rv); i++) {
+ scopeTerm = m_scopeList.ElementAt(i);
+ // NS_ASSERTION(scopeTerm->IsValid());
+
+ rv = scopeTerm->InitializeAdapter(m_termList);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchSession::BeginSearching() {
+ // Here's a sloppy way to start the URL, but I don't really have time to
+ // unify the scheduling mechanisms. If the first scope is a newsgroup, and
+ // it's not Dredd-capable, we build the URL queue. All other searches can be
+ // done with one URL
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) msgWindow->SetStopped(false);
+ return DoNextSearch();
+}
+
+nsresult nsMsgSearchSession::DoNextSearch() {
+ nsMsgSearchScopeTerm* scope = m_scopeList.ElementAt(m_idxRunningScope);
+ if (scope->m_attribute == nsMsgSearchScope::onlineMail ||
+ (scope->m_attribute == nsMsgSearchScope::news && scope->m_searchServer)) {
+ if (scope->m_adapter) {
+ m_runningUrl.Truncate();
+ scope->m_adapter->GetEncoding(getter_Copies(m_runningUrl));
+ }
+ NS_ENSURE_STATE(!m_runningUrl.IsEmpty());
+ return GetNextUrl();
+ } else {
+ return SearchWOUrls();
+ }
+}
+
+nsresult nsMsgSearchSession::GetNextUrl() {
+ nsCOMPtr<nsIMsgMessageService> msgService;
+
+ bool stopped = false;
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) msgWindow->GetStopped(&stopped);
+ if (stopped) return NS_OK;
+
+ nsMsgSearchScopeTerm* currentTerm = GetRunningScope();
+ NS_ENSURE_TRUE(currentTerm, NS_ERROR_NULL_POINTER);
+ EnableFolderNotifications(false);
+ nsCOMPtr<nsIMsgFolder> folder = currentTerm->m_folder;
+ if (folder) {
+ nsCString folderUri;
+ folder->GetURI(folderUri);
+ nsresult rv =
+ GetMessageServiceFromURI(folderUri, getter_AddRefs(msgService));
+
+ if (NS_SUCCEEDED(rv) && msgService && currentTerm)
+ msgService->Search(this, msgWindow, currentTerm->m_folder, m_runningUrl);
+ return rv;
+ }
+ return NS_OK;
+}
+
+/* static */
+void nsMsgSearchSession::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ NS_ENSURE_TRUE_VOID(aClosure);
+ nsMsgSearchSession* searchSession = (nsMsgSearchSession*)aClosure;
+ bool done;
+ bool stopped = false;
+
+ searchSession->TimeSlice(&done);
+ nsCOMPtr<nsIMsgWindow> msgWindow(
+ do_QueryReferent(searchSession->m_msgWindowWeak));
+ if (msgWindow) msgWindow->GetStopped(&stopped);
+
+ if (done || stopped) {
+ if (aTimer) aTimer->Cancel();
+ searchSession->m_backgroundTimer = nullptr;
+ if (searchSession->m_idxRunningScope < searchSession->m_scopeList.Length())
+ searchSession->DoNextSearch();
+ else
+ searchSession->NotifyListenersDone(NS_OK);
+ }
+}
+
+nsresult nsMsgSearchSession::StartTimer() {
+ nsresult rv;
+
+ m_backgroundTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_backgroundTimer->InitWithNamedFuncCallback(
+ TimerCallback, (void*)this, 0, nsITimer::TYPE_REPEATING_SLACK,
+ "nsMsgSearchSession::TimerCallback");
+ TimerCallback(m_backgroundTimer, this);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchSession::SearchWOUrls() {
+ EnableFolderNotifications(false);
+ return StartTimer();
+}
+
+NS_IMETHODIMP
+nsMsgSearchSession::GetRunningAdapter(nsIMsgSearchAdapter** aSearchAdapter) {
+ NS_ENSURE_ARG_POINTER(aSearchAdapter);
+ *aSearchAdapter = nullptr;
+ nsMsgSearchScopeTerm* scope = GetRunningScope();
+ if (scope) {
+ NS_IF_ADDREF(*aSearchAdapter = scope->m_adapter);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchSession::AddSearchHit(nsIMsgDBHdr* aHeader,
+ nsIMsgFolder* aFolder) {
+ nsCOMPtr<nsIMsgSearchNotify> listener;
+ m_iListener = 0;
+ while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) {
+ listener = m_listenerList[m_iListener];
+ int32_t listenerFlags = m_listenerFlagList[m_iListener++];
+ if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchHit))
+ listener->OnSearchHit(aHeader, aFolder);
+ }
+ m_iListener = -1;
+ return NS_OK;
+}
+
+nsresult nsMsgSearchSession::NotifyListenersDone(nsresult aStatus) {
+ // need to stabilize "this" in case one of the listeners releases the last
+ // reference to us.
+ RefPtr<nsIMsgSearchSession> kungFuDeathGrip(this);
+
+ nsCOMPtr<nsIMsgSearchNotify> listener;
+ m_iListener = 0;
+ while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) {
+ listener = m_listenerList[m_iListener];
+ int32_t listenerFlags = m_listenerFlagList[m_iListener++];
+ if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchDone))
+ listener->OnSearchDone(aStatus);
+ }
+ m_iListener = -1;
+ return NS_OK;
+}
+
+void nsMsgSearchSession::DestroyScopeList() {
+ nsMsgSearchScopeTerm* scope = nullptr;
+
+ for (int32_t i = m_scopeList.Length() - 1; i >= 0; i--) {
+ scope = m_scopeList.ElementAt(i);
+ // NS_ASSERTION (scope->IsValid(), "invalid search scope");
+ if (scope->m_adapter) scope->m_adapter->ClearScope();
+ }
+ m_scopeList.Clear();
+}
+
+void nsMsgSearchSession::DestroyTermList() { m_termList.Clear(); }
+
+nsMsgSearchScopeTerm* nsMsgSearchSession::GetRunningScope() {
+ return m_scopeList.SafeElementAt(m_idxRunningScope, nullptr);
+}
+
+nsresult nsMsgSearchSession::TimeSlice(bool* aDone) {
+ // we only do serial for now.
+ return TimeSliceSerial(aDone);
+}
+
+void nsMsgSearchSession::ReleaseFolderDBRef() {
+ nsMsgSearchScopeTerm* scope = GetRunningScope();
+ if (!scope) return;
+
+ bool isOpen = false;
+ uint32_t flags;
+ nsCOMPtr<nsIMsgFolder> folder;
+ scope->GetFolder(getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (!mailSession || !folder) return;
+
+ mailSession->IsFolderOpenInWindow(folder, &isOpen);
+ folder->GetFlags(&flags);
+
+ /*we don't null out the db reference for inbox because inbox is like the
+ "main" folder and performance outweighs footprint */
+ if (!isOpen && !(nsMsgFolderFlags::Inbox & flags))
+ folder->SetMsgDatabase(nullptr);
+}
+nsresult nsMsgSearchSession::TimeSliceSerial(bool* aDone) {
+ // This version of TimeSlice runs each scope term one at a time, and waits
+ // until one scope term is finished before starting another one. When we're
+ // searching the local disk, this is the fastest way to do it.
+
+ NS_ENSURE_ARG_POINTER(aDone);
+
+ nsMsgSearchScopeTerm* scope = GetRunningScope();
+ if (!scope) {
+ *aDone = true;
+ return NS_OK;
+ }
+
+ nsresult rv = scope->TimeSlice(aDone);
+ if (*aDone || NS_FAILED(rv)) {
+ EnableFolderNotifications(true);
+ ReleaseFolderDBRef();
+ m_idxRunningScope++;
+ EnableFolderNotifications(false);
+ // check if the next scope is an online search; if so,
+ // set *aDone to true so that we'll try to run the next
+ // search in TimerCallback.
+ scope = GetRunningScope();
+ if (scope && (scope->m_attribute == nsMsgSearchScope::onlineMail ||
+ (scope->m_attribute == nsMsgSearchScope::news &&
+ scope->m_searchServer))) {
+ *aDone = true;
+ return rv;
+ }
+ }
+ *aDone = false;
+ return rv;
+}
+
+void nsMsgSearchSession::EnableFolderNotifications(bool aEnable) {
+ nsMsgSearchScopeTerm* scope = GetRunningScope();
+ if (scope) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ scope->GetFolder(getter_AddRefs(folder));
+ if (folder) // enable msg count notifications
+ folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ aEnable);
+ }
+}
+
+// this method is used for adding new hdrs to quick search view
+NS_IMETHODIMP
+nsMsgSearchSession::MatchHdr(nsIMsgDBHdr* aMsgHdr, nsIMsgDatabase* aDatabase,
+ bool* aResult) {
+ nsMsgSearchScopeTerm* scope = m_scopeList.SafeElementAt(0, nullptr);
+ if (scope) {
+ if (!scope->m_adapter) scope->InitializeAdapter(m_termList);
+ if (scope->m_adapter) {
+ nsAutoString nullCharset, folderCharset;
+ scope->m_adapter->GetSearchCharsets(nullCharset, folderCharset);
+ NS_ConvertUTF16toUTF8 charset(folderCharset.get());
+ nsMsgSearchOfflineMail::MatchTermsForSearch(
+ aMsgHdr, m_termList, charset.get(), scope, aDatabase,
+ &m_expressionTree, aResult);
+ }
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/search/src/nsMsgSearchSession.h b/comm/mailnews/search/src/nsMsgSearchSession.h
new file mode 100644
index 0000000000..a5f311e1e5
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchSession.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsMsgSearchSession_h___
+#define nsMsgSearchSession_h___
+
+#include "nscore.h"
+#include "nsMsgSearchCore.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIUrlListener.h"
+#include "nsITimer.h"
+#include "nsWeakReference.h"
+
+class nsMsgSearchAdapter;
+class nsMsgSearchBoolExpression;
+class nsMsgSearchScopeTerm;
+
+class nsMsgSearchSession : public nsIMsgSearchSession,
+ public nsIUrlListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSEARCHSESSION
+ NS_DECL_NSIURLLISTENER
+
+ nsMsgSearchSession();
+
+ protected:
+ virtual ~nsMsgSearchSession();
+
+ nsWeakPtr m_msgWindowWeak;
+ nsresult Initialize();
+ nsresult StartTimer();
+ nsresult TimeSlice(bool* aDone);
+ nsMsgSearchScopeTerm* GetRunningScope();
+ void StopRunning();
+ nsresult BeginSearching();
+ nsresult DoNextSearch();
+ nsresult SearchWOUrls();
+ nsresult GetNextUrl();
+ nsresult NotifyListenersDone(nsresult status);
+ void EnableFolderNotifications(bool aEnable);
+ void ReleaseFolderDBRef();
+
+ nsTArray<RefPtr<nsMsgSearchScopeTerm>> m_scopeList;
+ nsTArray<RefPtr<nsIMsgSearchTerm>> m_termList;
+
+ nsTArray<nsCOMPtr<nsIMsgSearchNotify>> m_listenerList;
+ nsTArray<int32_t> m_listenerFlagList;
+ /**
+ * Iterator index for m_listenerList/m_listenerFlagList. We used to use an
+ * nsTObserverArray for m_listenerList but its auto-adjusting iterator was
+ * not helping us keep our m_listenerFlagList iterator correct.
+ *
+ * We are making the simplifying assumption that our notifications are
+ * non-reentrant. In the exceptional case that it turns out they are
+ * reentrant, we assume that this is the result of canceling a search while
+ * the session is active and initiating a new one. In that case, we assume
+ * the outer iteration can safely be abandoned.
+ *
+ * This value is defined to be the index of the next listener we will process.
+ * This allows us to use the sentinel value of -1 to convey that no iteration
+ * is in progress (and the iteration process to abort if the value transitions
+ * to -1, which we always set on conclusion of our loop).
+ */
+ int32_t m_iListener;
+
+ void DestroyTermList();
+ void DestroyScopeList();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+ // support for searching multiple scopes in serial
+ nsresult TimeSliceSerial(bool* aDone);
+ nsresult TimeSliceParallel();
+
+ nsMsgSearchAttribValue m_sortAttribute;
+ uint32_t m_idxRunningScope;
+ bool m_handlingError;
+ nsCString m_runningUrl; // The url for the current search
+ nsCOMPtr<nsITimer> m_backgroundTimer;
+ bool m_searchPaused;
+ nsMsgSearchBoolExpression* m_expressionTree;
+};
+
+#endif
diff --git a/comm/mailnews/search/src/nsMsgSearchTerm.cpp b/comm/mailnews/search/src/nsMsgSearchTerm.cpp
new file mode 100644
index 0000000000..7aad1628e5
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchTerm.cpp
@@ -0,0 +1,1797 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "msgCore.h"
+#include "prmem.h"
+#include "nsMsgSearchCore.h"
+#include "nsIMsgSearchSession.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgSearchTerm.h"
+#include "nsMsgSearchScopeTerm.h"
+#include "nsMsgBodyHandler.h"
+#include "nsMsgResultElement.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsMsgSearchImap.h"
+#include "nsMsgLocalSearch.h"
+#include "nsMsgSearchNews.h"
+#include "nsMsgSearchValue.h"
+#include "nsMsgI18N.h"
+#include "nsIMimeConverter.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIFile.h"
+#include "nsISeekableStream.h"
+#include "nsNetCID.h"
+#include "nsIFileStreams.h"
+#include "nsUnicharUtils.h"
+#include "nsIAbCard.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include <ctype.h>
+#include "nsIMsgTagService.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIAbManager.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/Utf8.h"
+
+using namespace mozilla::mailnews;
+
+//---------------------------------------------------------------------------
+// nsMsgSearchTerm specifies one criterion, e.g. name contains phil
+//---------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+//-------------------- Implementation of nsMsgSearchTerm -----------------------
+//-----------------------------------------------------------------------------
+#define MAILNEWS_CUSTOM_HEADERS "mailnews.customHeaders"
+
+typedef struct {
+ nsMsgSearchAttribValue attrib;
+ const char* attribName;
+} nsMsgSearchAttribEntry;
+
+nsMsgSearchAttribEntry SearchAttribEntryTable[] = {
+ {nsMsgSearchAttrib::Subject, "subject"},
+ {nsMsgSearchAttrib::Sender, "from"},
+ {nsMsgSearchAttrib::Body, "body"},
+ {nsMsgSearchAttrib::Date, "date"},
+ {nsMsgSearchAttrib::Priority, "priority"},
+ {nsMsgSearchAttrib::MsgStatus, "status"},
+ {nsMsgSearchAttrib::To, "to"},
+ {nsMsgSearchAttrib::CC, "cc"},
+ {nsMsgSearchAttrib::ToOrCC, "to or cc"},
+ {nsMsgSearchAttrib::AllAddresses, "all addresses"},
+ {nsMsgSearchAttrib::AgeInDays, "age in days"},
+ {nsMsgSearchAttrib::Keywords, "tag"},
+ {nsMsgSearchAttrib::Size, "size"},
+ // this used to be nsMsgSearchAttrib::SenderInAddressBook
+ // we used to have two Sender menuitems
+ // for backward compatibility, we can still parse
+ // the old style. see bug #179803
+ {nsMsgSearchAttrib::Sender, "from in ab"},
+ {nsMsgSearchAttrib::JunkStatus, "junk status"},
+ {nsMsgSearchAttrib::JunkPercent, "junk percent"},
+ {nsMsgSearchAttrib::JunkScoreOrigin, "junk score origin"},
+ {nsMsgSearchAttrib::HasAttachmentStatus, "has attachment status"},
+};
+
+static const unsigned int sNumSearchAttribEntryTable =
+ MOZ_ARRAY_LENGTH(SearchAttribEntryTable);
+
+// Take a string which starts off with an attribute
+// and return the matching attribute. If the string is not in the table, and it
+// begins with a quote, then we can conclude that it is an arbitrary header.
+// Otherwise if not in the table, it is the id for a custom search term.
+nsresult NS_MsgGetAttributeFromString(const char* string,
+ nsMsgSearchAttribValue* attrib,
+ nsACString& aCustomId) {
+ NS_ENSURE_ARG_POINTER(string);
+ NS_ENSURE_ARG_POINTER(attrib);
+
+ bool found = false;
+ bool isArbitraryHeader = false;
+ // arbitrary headers have a leading quote
+ if (*string != '"') {
+ for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable;
+ idxAttrib++) {
+ if (!PL_strcasecmp(string,
+ SearchAttribEntryTable[idxAttrib].attribName)) {
+ found = true;
+ *attrib = SearchAttribEntryTable[idxAttrib].attrib;
+ break;
+ }
+ }
+ } else // remove the leading quote
+ {
+ string++;
+ isArbitraryHeader = true;
+ }
+
+ if (!found && !isArbitraryHeader) {
+ // must be a custom attribute
+ *attrib = nsMsgSearchAttrib::Custom;
+ aCustomId.Assign(string);
+ return NS_OK;
+ }
+
+ if (!found) {
+ nsresult rv;
+ bool goodHdr;
+ IsRFC822HeaderFieldName(string, &goodHdr);
+ if (!goodHdr) return NS_MSG_INVALID_CUSTOM_HEADER;
+ // 49 is for showing customize... in ui, headers start from 50 onwards up
+ // until 99.
+ *attrib = nsMsgSearchAttrib::OtherHeader + 1;
+
+ nsCOMPtr<nsIPrefService> prefService =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefService->GetBranch(nullptr, getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString headers;
+ prefBranch->GetCharPref(MAILNEWS_CUSTOM_HEADERS, headers);
+
+ if (!headers.IsEmpty()) {
+ nsAutoCString hdrStr(headers);
+ hdrStr.StripWhitespace(); // remove whitespace before parsing
+
+ char* newStr = hdrStr.BeginWriting();
+ char* token = NS_strtok(":", &newStr);
+ uint32_t i = 0;
+ while (token) {
+ if (PL_strcasecmp(token, string) == 0) {
+ *attrib += i; // we found custom header in the pref
+ found = true;
+ break;
+ }
+ token = NS_strtok(":", &newStr);
+ i++;
+ }
+ }
+ }
+ // If we didn't find the header in MAILNEWS_CUSTOM_HEADERS, we're
+ // going to return NS_OK and an attrib of nsMsgSearchAttrib::OtherHeader+1.
+ // in case it's a client side spam filter description filter,
+ // which doesn't add its headers to mailnews.customMailHeaders.
+ // We've already checked that it's a valid header and returned
+ // an error if so.
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::GetAttributeFromString(
+ const char* aString, nsMsgSearchAttribValue* aAttrib) {
+ nsAutoCString customId;
+ return NS_MsgGetAttributeFromString(aString, aAttrib, customId);
+}
+
+nsresult NS_MsgGetStringForAttribute(int16_t attrib, const char** string) {
+ NS_ENSURE_ARG_POINTER(string);
+
+ bool found = false;
+ for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable;
+ idxAttrib++) {
+ // I'm using the idx's as aliases into MSG_SearchAttribute and
+ // MSG_SearchOperator enums which is legal because of the way the
+ // enums are defined (starts at 0, numItems at end)
+ if (attrib == SearchAttribEntryTable[idxAttrib].attrib) {
+ found = true;
+ *string = SearchAttribEntryTable[idxAttrib].attribName;
+ break;
+ }
+ }
+ if (!found) *string = ""; // don't leave the string uninitialized
+
+ // we no longer return invalid attribute. If we cannot find the string in the
+ // table, then it is an arbitrary header. Return success regardless if found
+ // or not
+ return NS_OK;
+}
+
+typedef struct {
+ nsMsgSearchOpValue op;
+ const char* opName;
+} nsMsgSearchOperatorEntry;
+
+nsMsgSearchOperatorEntry SearchOperatorEntryTable[] = {
+ {nsMsgSearchOp::Contains, "contains"},
+ {nsMsgSearchOp::DoesntContain, "doesn't contain"},
+ {nsMsgSearchOp::Is, "is"},
+ {nsMsgSearchOp::Isnt, "isn't"},
+ {nsMsgSearchOp::IsEmpty, "is empty"},
+ {nsMsgSearchOp::IsntEmpty, "isn't empty"},
+ {nsMsgSearchOp::IsBefore, "is before"},
+ {nsMsgSearchOp::IsAfter, "is after"},
+ {nsMsgSearchOp::IsHigherThan, "is higher than"},
+ {nsMsgSearchOp::IsLowerThan, "is lower than"},
+ {nsMsgSearchOp::BeginsWith, "begins with"},
+ {nsMsgSearchOp::EndsWith, "ends with"},
+ {nsMsgSearchOp::IsInAB, "is in ab"},
+ {nsMsgSearchOp::IsntInAB, "isn't in ab"},
+ {nsMsgSearchOp::IsGreaterThan, "is greater than"},
+ {nsMsgSearchOp::IsLessThan, "is less than"},
+ {nsMsgSearchOp::Matches, "matches"},
+ {nsMsgSearchOp::DoesntMatch, "doesn't match"}};
+
+static const unsigned int sNumSearchOperatorEntryTable =
+ MOZ_ARRAY_LENGTH(SearchOperatorEntryTable);
+
+nsresult NS_MsgGetOperatorFromString(const char* string, int16_t* op) {
+ NS_ENSURE_ARG_POINTER(string);
+ NS_ENSURE_ARG_POINTER(op);
+
+ bool found = false;
+ for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) {
+ // I'm using the idx's as aliases into MSG_SearchAttribute and
+ // MSG_SearchOperator enums which is legal because of the way the
+ // enums are defined (starts at 0, numItems at end)
+ if (!PL_strcasecmp(string, SearchOperatorEntryTable[idxOp].opName)) {
+ found = true;
+ *op = SearchOperatorEntryTable[idxOp].op;
+ break;
+ }
+ }
+ return (found) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+nsresult NS_MsgGetStringForOperator(int16_t op, const char** string) {
+ NS_ENSURE_ARG_POINTER(string);
+
+ bool found = false;
+ for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) {
+ // I'm using the idx's as aliases into MSG_SearchAttribute and
+ // MSG_SearchOperator enums which is legal because of the way the
+ // enums are defined (starts at 0, numItems at end)
+ if (op == SearchOperatorEntryTable[idxOp].op) {
+ found = true;
+ *string = SearchOperatorEntryTable[idxOp].opName;
+ break;
+ }
+ }
+
+ return (found) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+void NS_MsgGetUntranslatedStatusName(uint32_t s, nsCString* outName) {
+ const char* tmpOutName = NULL;
+#define MSG_STATUS_MASK \
+ (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied | \
+ nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::New | \
+ nsMsgMessageFlags::Marked)
+ uint32_t maskOut = (s & MSG_STATUS_MASK);
+
+ // diddle the flags to pay attention to the most important ones first, if
+ // multiple flags are set. Should remove this code from the winfe.
+ if (maskOut & nsMsgMessageFlags::New) maskOut = nsMsgMessageFlags::New;
+ if (maskOut & nsMsgMessageFlags::Replied &&
+ maskOut & nsMsgMessageFlags::Forwarded)
+ maskOut = nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded;
+ else if (maskOut & nsMsgMessageFlags::Forwarded)
+ maskOut = nsMsgMessageFlags::Forwarded;
+ else if (maskOut & nsMsgMessageFlags::Replied)
+ maskOut = nsMsgMessageFlags::Replied;
+
+ switch (maskOut) {
+ case nsMsgMessageFlags::Read:
+ tmpOutName = "read";
+ break;
+ case nsMsgMessageFlags::Replied:
+ tmpOutName = "replied";
+ break;
+ case nsMsgMessageFlags::Forwarded:
+ tmpOutName = "forwarded";
+ break;
+ case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied:
+ tmpOutName = "replied and forwarded";
+ break;
+ case nsMsgMessageFlags::New:
+ tmpOutName = "new";
+ break;
+ case nsMsgMessageFlags::Marked:
+ tmpOutName = "flagged";
+ break;
+ default:
+ // This is fine, status may be "unread" for example
+ break;
+ }
+
+ if (tmpOutName) *outName = tmpOutName;
+}
+
+int32_t NS_MsgGetStatusValueFromName(char* name) {
+ if (!strcmp("read", name)) return nsMsgMessageFlags::Read;
+ if (!strcmp("replied", name)) return nsMsgMessageFlags::Replied;
+ if (!strcmp("forwarded", name)) return nsMsgMessageFlags::Forwarded;
+ if (!strcmp("replied and forwarded", name))
+ return nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied;
+ if (!strcmp("new", name)) return nsMsgMessageFlags::New;
+ if (!strcmp("flagged", name)) return nsMsgMessageFlags::Marked;
+ return 0;
+}
+
+// Needed for DeStream method.
+nsMsgSearchTerm::nsMsgSearchTerm() {
+ // initialize this to zero
+ m_value.attribute = 0;
+ m_value.u.priority = 0;
+ m_attribute = nsMsgSearchAttrib::Default;
+ m_operator = nsMsgSearchOp::Contains;
+ mBeginsGrouping = false;
+ mEndsGrouping = false;
+ m_matchAll = false;
+
+ // valgrind warning during GC/java data check suggests
+ // m_booleanp needs to be initialized too.
+ m_booleanOp = nsMsgSearchBooleanOp::BooleanAND;
+}
+
+nsMsgSearchTerm::nsMsgSearchTerm(nsMsgSearchAttribValue attrib,
+ nsMsgSearchOpValue op, nsIMsgSearchValue* val,
+ nsMsgSearchBooleanOperator boolOp,
+ const char* aCustomString) {
+ m_operator = op;
+ m_attribute = attrib;
+ m_booleanOp = boolOp;
+ if (attrib > nsMsgSearchAttrib::OtherHeader &&
+ attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes && aCustomString) {
+ m_arbitraryHeader = aCustomString;
+ ToLowerCaseExceptSpecials(m_arbitraryHeader);
+ } else if (attrib == nsMsgSearchAttrib::Custom) {
+ m_customId = aCustomString;
+ }
+
+ nsMsgResultElement::AssignValues(val, &m_value);
+ m_matchAll = false;
+ mBeginsGrouping = false;
+ mEndsGrouping = false;
+}
+
+nsMsgSearchTerm::~nsMsgSearchTerm() {}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchTerm, nsIMsgSearchTerm)
+
+// Perhaps we could find a better place for this?
+// Caller needs to free.
+/* static */ char* nsMsgSearchTerm::EscapeQuotesInStr(const char* str) {
+ int numQuotes = 0;
+ for (const char* strPtr = str; *strPtr; strPtr++)
+ if (*strPtr == '"') numQuotes++;
+ int escapedStrLen = PL_strlen(str) + numQuotes;
+ char* escapedStr = (char*)PR_Malloc(escapedStrLen + 1);
+ if (escapedStr) {
+ char* destPtr;
+ for (destPtr = escapedStr; *str; str++) {
+ if (*str == '"') *destPtr++ = '\\';
+ *destPtr++ = *str;
+ }
+ *destPtr = '\0';
+ }
+ return escapedStr;
+}
+
+nsresult nsMsgSearchTerm::OutputValue(nsCString& outputStr) {
+ if (IS_STRING_ATTRIBUTE(m_attribute) && !m_value.utf8String.IsEmpty()) {
+ bool quoteVal = false;
+ // need to quote strings with ')' and strings starting with '"' or ' '
+ // filter code will escape quotes
+ if (m_value.utf8String.FindChar(')') != kNotFound ||
+ (m_value.utf8String.First() == ' ') ||
+ (m_value.utf8String.First() == '"')) {
+ quoteVal = true;
+ outputStr += "\"";
+ }
+ if (m_value.utf8String.FindChar('"') != kNotFound) {
+ char* escapedString =
+ nsMsgSearchTerm::EscapeQuotesInStr(m_value.utf8String.get());
+ if (escapedString) {
+ outputStr += escapedString;
+ PR_Free(escapedString);
+ }
+
+ } else {
+ outputStr += m_value.utf8String;
+ }
+ if (quoteVal) outputStr += "\"";
+ } else {
+ switch (m_attribute) {
+ case nsMsgSearchAttrib::Date: {
+ PRExplodedTime exploded;
+ PR_ExplodeTime(m_value.u.date, PR_LocalTimeParameters, &exploded);
+
+ // wow, so tm_mon is 0 based, tm_mday is 1 based.
+ char dateBuf[100];
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded);
+ outputStr += dateBuf;
+ break;
+ }
+ case nsMsgSearchAttrib::AgeInDays: {
+ outputStr.AppendInt(m_value.u.age);
+ break;
+ }
+ case nsMsgSearchAttrib::JunkStatus: {
+ outputStr.AppendInt(
+ m_value.u.junkStatus); // only if we write to disk, right?
+ break;
+ }
+ case nsMsgSearchAttrib::JunkPercent: {
+ outputStr.AppendInt(m_value.u.junkPercent);
+ break;
+ }
+ case nsMsgSearchAttrib::MsgStatus: {
+ nsAutoCString status;
+ NS_MsgGetUntranslatedStatusName(m_value.u.msgStatus, &status);
+ outputStr += status;
+ break;
+ }
+ case nsMsgSearchAttrib::Priority: {
+ nsAutoCString priority;
+ NS_MsgGetUntranslatedPriorityName(m_value.u.priority, priority);
+ outputStr += priority;
+ break;
+ }
+ case nsMsgSearchAttrib::HasAttachmentStatus: {
+ outputStr.AppendLiteral("true"); // don't need anything here, really
+ break;
+ }
+ case nsMsgSearchAttrib::Size: {
+ outputStr.AppendInt(m_value.u.size);
+ break;
+ }
+ case nsMsgSearchAttrib::Uint32HdrProperty: {
+ outputStr.AppendInt(m_value.u.msgStatus);
+ break;
+ }
+ default:
+ NS_ASSERTION(false, "trying to output invalid attribute");
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::GetTermAsString(nsACString& outStream) {
+ const char* operatorStr;
+ nsAutoCString outputStr;
+ nsresult rv;
+
+ if (m_matchAll) {
+ outStream = "ALL";
+ return NS_OK;
+ }
+
+ // if arbitrary header, use it instead!
+ if (m_attribute > nsMsgSearchAttrib::OtherHeader &&
+ m_attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ outputStr = "\"";
+ outputStr += m_arbitraryHeader;
+ outputStr += "\"";
+ } else if (m_attribute == nsMsgSearchAttrib::Custom) {
+ // use the custom id as the string
+ outputStr = m_customId;
+ }
+
+ else if (m_attribute == nsMsgSearchAttrib::Uint32HdrProperty) {
+ outputStr = m_hdrProperty;
+ } else {
+ const char* attrib;
+ rv = NS_MsgGetStringForAttribute(m_attribute, &attrib);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outputStr = attrib;
+ }
+
+ outputStr += ',';
+
+ rv = NS_MsgGetStringForOperator(m_operator, &operatorStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outputStr += operatorStr;
+ outputStr += ',';
+
+ OutputValue(outputStr);
+ outStream = outputStr;
+ return NS_OK;
+}
+
+// fill in m_value from the input stream.
+nsresult nsMsgSearchTerm::ParseValue(char* inStream) {
+ if (IS_STRING_ATTRIBUTE(m_attribute)) {
+ bool quoteVal = false;
+ while (isspace(*inStream)) inStream++;
+ // need to remove pair of '"', if present
+ if (*inStream == '"') {
+ quoteVal = true;
+ inStream++;
+ }
+ int valueLen = PL_strlen(inStream);
+ if (quoteVal && inStream[valueLen - 1] == '"') valueLen--;
+
+ m_value.utf8String.Assign(inStream, valueLen);
+ CopyUTF8toUTF16(m_value.utf8String, m_value.utf16String);
+ } else {
+ switch (m_attribute) {
+ case nsMsgSearchAttrib::Date:
+ PR_ParseTimeString(inStream, false, &m_value.u.date);
+ break;
+ case nsMsgSearchAttrib::MsgStatus:
+ m_value.u.msgStatus = NS_MsgGetStatusValueFromName(inStream);
+ break;
+ case nsMsgSearchAttrib::Priority:
+ NS_MsgGetPriorityFromString(inStream, m_value.u.priority);
+ break;
+ case nsMsgSearchAttrib::AgeInDays:
+ m_value.u.age = atoi(inStream);
+ break;
+ case nsMsgSearchAttrib::JunkStatus:
+ m_value.u.junkStatus =
+ atoi(inStream); // only if we read from disk, right?
+ break;
+ case nsMsgSearchAttrib::JunkPercent:
+ m_value.u.junkPercent = atoi(inStream);
+ break;
+ case nsMsgSearchAttrib::HasAttachmentStatus:
+ m_value.u.msgStatus = nsMsgMessageFlags::Attachment;
+ break; // this should always be true.
+ case nsMsgSearchAttrib::Size:
+ m_value.u.size = atoi(inStream);
+ break;
+ default:
+ NS_ASSERTION(false, "invalid attribute parsing search term value");
+ break;
+ }
+ }
+ m_value.attribute = m_attribute;
+ return NS_OK;
+}
+
+// find the operator code for this operator string.
+nsresult nsMsgSearchTerm::ParseOperator(char* inStream,
+ nsMsgSearchOpValue* value) {
+ NS_ENSURE_ARG_POINTER(value);
+ int16_t operatorVal;
+ while (isspace(*inStream)) inStream++;
+
+ char* commaSep = PL_strchr(inStream, ',');
+
+ if (commaSep) *commaSep = '\0';
+
+ nsresult rv = NS_MsgGetOperatorFromString(inStream, &operatorVal);
+ if (NS_SUCCEEDED(rv)) *value = (nsMsgSearchOpValue)operatorVal;
+ return rv;
+}
+
+// find the attribute code for this comma-delimited attribute.
+nsresult nsMsgSearchTerm::ParseAttribute(char* inStream,
+ nsMsgSearchAttribValue* attrib) {
+ while (isspace(*inStream)) inStream++;
+
+ // if we are dealing with an arbitrary header, it will be quoted....
+ // it seems like a kludge, but to distinguish arbitrary headers from
+ // standard headers with the same name, like "Date", we'll use the
+ // presence of the quote to recognize arbitrary headers. We leave the
+ // leading quote as a flag, but remove the trailing quote.
+ bool quoteVal = false;
+ if (*inStream == '"') quoteVal = true;
+
+ // arbitrary headers are quoted. Skip first character, which will be the
+ // first quote for arbitrary headers
+ char* separator = strchr(inStream + 1, quoteVal ? '"' : ',');
+
+ if (separator) *separator = '\0';
+
+ nsAutoCString customId;
+ nsresult rv = NS_MsgGetAttributeFromString(inStream, attrib, m_customId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*attrib > nsMsgSearchAttrib::OtherHeader &&
+ *attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ // We are dealing with an arbitrary header.
+ m_arbitraryHeader = inStream + 1; // remove the leading quote
+ ToLowerCaseExceptSpecials(m_arbitraryHeader);
+ }
+ return rv;
+}
+
+// De stream one search term. If the condition looks like
+// condition = "(to or cc, contains, r-thompson) AND (body, doesn't contain,
+// fred)" This routine should get called twice, the first time with "to or cc,
+// contains, r-thompson", the second time with "body, doesn't contain, fred"
+
+nsresult nsMsgSearchTerm::DeStreamNew(char* inStream, int16_t /*length*/) {
+ if (!strcmp(inStream, "ALL")) {
+ m_matchAll = true;
+ return NS_OK;
+ }
+ char* commaSep = PL_strchr(inStream, ',');
+ nsresult rv = ParseAttribute(
+ inStream,
+ &m_attribute); // will allocate space for arbitrary header if necessary
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!commaSep) return NS_ERROR_INVALID_ARG;
+ char* secondCommaSep = PL_strchr(commaSep + 1, ',');
+ if (commaSep) rv = ParseOperator(commaSep + 1, &m_operator);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // convert label filters and saved searches to keyword equivalents
+ if (secondCommaSep) ParseValue(secondCommaSep + 1);
+ return NS_OK;
+}
+
+// Looks in the MessageDB for the user specified arbitrary header, if it finds
+// the header, it then looks for a match against the value for the header.
+nsresult nsMsgSearchTerm::MatchArbitraryHeader(
+ nsIMsgSearchScopeTerm* scope, uint32_t length /* in lines*/,
+ const char* charset, bool charsetOverride, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db, const nsACString& headers, bool ForFiltering,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ *pResult = false;
+ nsresult rv = NS_OK;
+ bool matchExpected = m_operator == nsMsgSearchOp::Contains ||
+ m_operator == nsMsgSearchOp::Is ||
+ m_operator == nsMsgSearchOp::BeginsWith ||
+ m_operator == nsMsgSearchOp::EndsWith;
+ // Initialize result to what we want if we don't find the header at all.
+ bool result = !matchExpected;
+
+ nsCString dbHdrValue;
+ msg->GetStringProperty(m_arbitraryHeader.get(), dbHdrValue);
+ if (!dbHdrValue.IsEmpty()) {
+ // Match value with the other info. It doesn't check all header occurrences,
+ // so we use it only if we match and do line by line headers parsing
+ // otherwise.
+ rv = MatchRfc2047String(dbHdrValue, charset, charsetOverride, pResult);
+ if (matchExpected == *pResult) return rv;
+
+ // Preset result in case we don't have access to the headers, like for IMAP.
+ result = *pResult;
+ }
+
+ nsMsgBodyHandler* bodyHandler =
+ new nsMsgBodyHandler(scope, length, msg, db, headers.BeginReading(),
+ headers.Length(), ForFiltering);
+ bodyHandler->SetStripHeaders(false);
+
+ nsCString headerFullValue; // Contains matched header value accumulated over
+ // multiple lines.
+ nsAutoCString buf;
+ nsAutoCString curMsgHeader;
+ bool processingHeaders = true;
+
+ while (processingHeaders) {
+ nsCString charsetIgnored;
+ if (bodyHandler->GetNextLine(buf, charsetIgnored) < 0 ||
+ EMPTY_MESSAGE_LINE(buf))
+ processingHeaders =
+ false; // No more lines or empty line terminating headers.
+
+ bool isContinuationHeader =
+ processingHeaders ? NS_IsAsciiWhitespace(buf.CharAt(0)) : false;
+
+ // If we're not on a continuation header the header value is not empty,
+ // we have finished accumulating the header value by iterating over all
+ // header lines. Now we need to check whether the value is a match.
+ if (!isContinuationHeader && !headerFullValue.IsEmpty()) {
+ bool stringMatches;
+ // Match value with the other info.
+ rv = MatchRfc2047String(headerFullValue, charset, charsetOverride,
+ &stringMatches);
+ if (matchExpected == stringMatches) // if we found a match
+ {
+ // If we found a match, stop examining the headers.
+ processingHeaders = false;
+ result = stringMatches;
+ }
+ // Prepare for repeated header of the same type.
+ headerFullValue.Truncate();
+ }
+
+ // We got result or finished processing all lines.
+ if (!processingHeaders) break;
+
+ char* buf_end = (char*)(buf.get() + buf.Length());
+ int headerLength = m_arbitraryHeader.Length();
+
+ // If the line starts with whitespace, then we use the current header.
+ if (!isContinuationHeader) {
+ // Here we start a new header.
+ uint32_t colonPos = buf.FindChar(':');
+ curMsgHeader = StringHead(buf, colonPos);
+ }
+
+ if (curMsgHeader.Equals(m_arbitraryHeader,
+ nsCaseInsensitiveCStringComparator)) {
+ // Process the value:
+ // Value occurs after the header name or whitespace continuation char.
+ const char* headerValue =
+ buf.get() + (isContinuationHeader ? 1 : headerLength);
+ if (headerValue < buf_end &&
+ headerValue[0] ==
+ ':') // + 1 to account for the colon which is MANDATORY
+ headerValue++;
+
+ // Strip leading white space.
+ while (headerValue < buf_end && isspace(*headerValue)) headerValue++;
+
+ // Strip trailing white space.
+ char* end = buf_end - 1;
+ while (headerValue < end && isspace(*end)) {
+ *end = '\0';
+ end--;
+ }
+
+ // Any continuation whitespace is converted to a single space.
+ if (!headerFullValue.IsEmpty()) headerFullValue.Append(' ');
+ headerFullValue.Append(nsDependentCString(headerValue));
+ }
+ }
+
+ delete bodyHandler;
+ *pResult = result;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::MatchHdrProperty(nsIMsgDBHdr* aHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aHdr);
+
+ nsCString dbHdrValue;
+ aHdr->GetStringProperty(m_hdrProperty.get(), dbHdrValue);
+ return MatchString(dbHdrValue, nullptr, aResult);
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::MatchFolderFlag(nsIMsgDBHdr* aMsgToMatch,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aMsgToMatch);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ nsresult rv = aMsgToMatch->GetFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t folderFlags;
+ msgFolder->GetFlags(&folderFlags);
+ return MatchStatus(folderFlags, aResult);
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::MatchUint32HdrProperty(nsIMsgDBHdr* aHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aHdr);
+
+ nsresult rv = NS_OK;
+ uint32_t dbHdrValue;
+ aHdr->GetUint32Property(m_hdrProperty.get(), &dbHdrValue);
+
+ bool result = false;
+ switch (m_operator) {
+ case nsMsgSearchOp::IsGreaterThan:
+ if (dbHdrValue > m_value.u.msgStatus) result = true;
+ break;
+ case nsMsgSearchOp::IsLessThan:
+ if (dbHdrValue < m_value.u.msgStatus) result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (dbHdrValue == m_value.u.msgStatus) result = true;
+ break;
+ case nsMsgSearchOp::Isnt:
+ if (dbHdrValue != m_value.u.msgStatus) result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for uint");
+ }
+ *aResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchBody(nsIMsgSearchScopeTerm* scope,
+ uint64_t offset,
+ uint32_t length /*in lines*/,
+ const char* folderCharset, nsIMsgDBHdr* msg,
+ nsIMsgDatabase* db, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+
+ bool result = false;
+ *pResult = false;
+
+ // Small hack so we don't look all through a message when someone has
+ // specified "BODY IS foo". ### Since length is in lines, this is not quite
+ // right.
+ if ((length > 0) &&
+ (m_operator == nsMsgSearchOp::Is || m_operator == nsMsgSearchOp::Isnt))
+ length = m_value.utf8String.Length();
+
+ nsMsgBodyHandler* bodyHan = new nsMsgBodyHandler(scope, length, msg, db);
+ if (!bodyHan) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsAutoCString buf;
+ bool endOfFile = false; // if retValue == 0, we've hit the end of the file
+
+ // Change the sense of the loop so we don't bail out prematurely
+ // on negative terms. i.e. opDoesntContain must look at all lines
+ bool boolContinueLoop;
+ GetMatchAllBeforeDeciding(&boolContinueLoop);
+ result = boolContinueLoop;
+
+ nsCString compare;
+ nsCString charset;
+ while (!endOfFile && result == boolContinueLoop) {
+ if (bodyHan->GetNextLine(buf, charset) >= 0) {
+ bool softLineBreak = false;
+ // Do in-place decoding of quoted printable
+ if (bodyHan->IsQP()) {
+ softLineBreak = StringEndsWith(buf, "="_ns);
+ MsgStripQuotedPrintable(buf);
+ // If soft line break, chop off the last char as well.
+ size_t bufLength = buf.Length();
+ if ((bufLength > 0) && softLineBreak) buf.SetLength(bufLength - 1);
+ }
+ compare.Append(buf);
+ // If this line ends with a soft line break, loop around
+ // and get the next line before looking for the search string.
+ // This assumes the message can't end on a QP soft line break.
+ // That seems like a pretty safe assumption.
+ if (softLineBreak) continue;
+ if (!compare.IsEmpty()) {
+ char startChar = (char)compare.CharAt(0);
+ if (startChar != '\r' && startChar != '\n') {
+ rv = MatchString(compare,
+ charset.IsEmpty() ? folderCharset : charset.get(),
+ &result);
+ }
+ compare.Truncate();
+ }
+ } else
+ endOfFile = true;
+ }
+
+ delete bodyHan;
+ *pResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::InitializeAddressBook() {
+ // the search attribute value has the URI for the address book we need to
+ // load. we need both the database and the directory.
+ nsresult rv = NS_OK;
+
+ if (mDirectory) {
+ nsCString uri;
+ rv = mDirectory->GetURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!uri.Equals(m_value.utf8String))
+ // clear out the directory....we are no longer pointing to the right one
+ mDirectory = nullptr;
+ }
+ if (!mDirectory) {
+ nsCOMPtr<nsIAbManager> abManager =
+ do_GetService("@mozilla.org/abmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv =
+ abManager->GetDirectory(m_value.utf8String, getter_AddRefs(mDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgSearchTerm::MatchInAddressBook(const nsAString& aAddress,
+ bool* pResult) {
+ nsresult rv = InitializeAddressBook();
+ *pResult = false;
+
+ // Some junkmails have empty From: fields.
+ if (aAddress.IsEmpty()) return rv;
+
+ if (mDirectory) {
+ nsCOMPtr<nsIAbCard> cardForAddress = nullptr;
+ rv = mDirectory->CardForEmailAddress(NS_ConvertUTF16toUTF8(aAddress),
+ getter_AddRefs(cardForAddress));
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) return rv;
+ switch (m_operator) {
+ case nsMsgSearchOp::IsInAB:
+ if (cardForAddress) *pResult = true;
+ break;
+ case nsMsgSearchOp::IsntInAB:
+ if (!cardForAddress) *pResult = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for address book");
+ }
+ }
+
+ return rv;
+}
+
+// *pResult is false when strings don't match, true if they do.
+nsresult nsMsgSearchTerm::MatchRfc2047String(const nsACString& rfc2047string,
+ const char* charset,
+ bool charsetOverride,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv;
+ nsCOMPtr<nsIMimeConverter> mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString stringToMatch;
+ rv = mimeConverter->DecodeMimeHeader(PromiseFlatCString(rfc2047string).get(),
+ charset, charsetOverride, false,
+ stringToMatch);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_operator == nsMsgSearchOp::IsInAB ||
+ m_operator == nsMsgSearchOp::IsntInAB)
+ return MatchInAddressBook(stringToMatch, pResult);
+
+ return MatchString(stringToMatch, pResult);
+}
+
+// *pResult is false when strings don't match, true if they do.
+nsresult nsMsgSearchTerm::MatchString(const nsACString& stringToMatch,
+ const char* charset, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ bool result = false;
+
+ nsresult rv = NS_OK;
+
+ // Save some performance for opIsEmpty / opIsntEmpty
+ if (nsMsgSearchOp::IsEmpty == m_operator) {
+ if (stringToMatch.IsEmpty()) result = true;
+ } else if (nsMsgSearchOp::IsntEmpty == m_operator) {
+ if (!stringToMatch.IsEmpty()) result = true;
+ } else {
+ nsAutoString utf16StrToMatch;
+ rv = NS_ERROR_UNEXPECTED;
+ if (charset) {
+ rv = nsMsgI18NConvertToUnicode(nsDependentCString(charset), stringToMatch,
+ utf16StrToMatch);
+ }
+ if (NS_FAILED(rv)) {
+ // No charset or conversion failed, maybe due to a bad charset, try UTF-8.
+ if (mozilla::IsUtf8(stringToMatch)) {
+ CopyUTF8toUTF16(stringToMatch, utf16StrToMatch);
+ } else {
+ // Bad luck, let's assume ASCII/windows-1252 then.
+ CopyASCIItoUTF16(stringToMatch, utf16StrToMatch);
+ }
+ }
+
+ rv = MatchString(utf16StrToMatch, &result);
+ }
+
+ *pResult = result;
+ return rv;
+}
+
+// *pResult is false when strings don't match, true if they do.
+nsresult nsMsgSearchTerm::MatchString(const nsAString& utf16StrToMatch,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ bool result = false;
+
+ nsresult rv = NS_OK;
+ auto needle = m_value.utf16String;
+
+ switch (m_operator) {
+ case nsMsgSearchOp::Contains:
+ if (CaseInsensitiveFindInReadable(needle, utf16StrToMatch)) result = true;
+ break;
+ case nsMsgSearchOp::DoesntContain:
+ if (!CaseInsensitiveFindInReadable(needle, utf16StrToMatch))
+ result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator))
+ result = true;
+ break;
+ case nsMsgSearchOp::Isnt:
+ if (!needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator))
+ result = true;
+ break;
+ case nsMsgSearchOp::IsEmpty:
+ if (utf16StrToMatch.IsEmpty()) result = true;
+ break;
+ case nsMsgSearchOp::IsntEmpty:
+ if (!utf16StrToMatch.IsEmpty()) result = true;
+ break;
+ case nsMsgSearchOp::BeginsWith:
+ if (StringBeginsWith(utf16StrToMatch, needle,
+ nsCaseInsensitiveStringComparator))
+ result = true;
+ break;
+ case nsMsgSearchOp::EndsWith:
+ if (StringEndsWith(utf16StrToMatch, needle,
+ nsCaseInsensitiveStringComparator))
+ result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for matching search results");
+ }
+
+ *pResult = result;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::GetMatchAllBeforeDeciding(bool* aResult) {
+ *aResult = (m_operator == nsMsgSearchOp::DoesntContain ||
+ m_operator == nsMsgSearchOp::Isnt);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchTerm::MatchRfc822String(const nsACString& string,
+ const char* charset,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ *pResult = false;
+ bool result;
+
+ // Change the sense of the loop so we don't bail out prematurely
+ // on negative terms. i.e. opDoesntContain must look at all recipients
+ bool boolContinueLoop;
+ GetMatchAllBeforeDeciding(&boolContinueLoop);
+ result = boolContinueLoop;
+
+ // If the operator is Contains, then we can cheat and avoid having to parse
+ // addresses. This does open up potential spurious matches for punctuation
+ // (e.g., ; or <), but the likelihood of users intending to search for these
+ // and also being able to match them is rather low. This optimization is not
+ // applicable to any other search type.
+ if (m_operator == nsMsgSearchOp::Contains)
+ return MatchRfc2047String(string, charset, false, pResult);
+
+ nsTArray<nsString> names, addresses;
+ ExtractAllAddresses(EncodedHeader(string, charset), names, addresses);
+ uint32_t count = names.Length();
+
+ nsresult rv = NS_OK;
+ for (uint32_t i = 0; i < count && result == boolContinueLoop; i++) {
+ if (m_operator == nsMsgSearchOp::IsInAB ||
+ m_operator == nsMsgSearchOp::IsntInAB) {
+ rv = MatchInAddressBook(addresses[i], &result);
+ } else {
+ rv = MatchString(names[i], &result);
+ if (boolContinueLoop == result) rv = MatchString(addresses[i], &result);
+ }
+ }
+ *pResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::GetLocalTimes(PRTime a, PRTime b,
+ PRExplodedTime& aExploded,
+ PRExplodedTime& bExploded) {
+ PR_ExplodeTime(a, PR_LocalTimeParameters, &aExploded);
+ PR_ExplodeTime(b, PR_LocalTimeParameters, &bExploded);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchTerm::MatchDate(PRTime dateToMatch, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+ bool result = false;
+
+ PRExplodedTime tmToMatch, tmThis;
+ if (NS_SUCCEEDED(
+ GetLocalTimes(dateToMatch, m_value.u.date, tmToMatch, tmThis))) {
+ switch (m_operator) {
+ case nsMsgSearchOp::IsBefore:
+ if (tmToMatch.tm_year < tmThis.tm_year ||
+ (tmToMatch.tm_year == tmThis.tm_year &&
+ tmToMatch.tm_yday < tmThis.tm_yday))
+ result = true;
+ break;
+ case nsMsgSearchOp::IsAfter:
+ if (tmToMatch.tm_year > tmThis.tm_year ||
+ (tmToMatch.tm_year == tmThis.tm_year &&
+ tmToMatch.tm_yday > tmThis.tm_yday))
+ result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (tmThis.tm_year == tmToMatch.tm_year &&
+ tmThis.tm_month == tmToMatch.tm_month &&
+ tmThis.tm_mday == tmToMatch.tm_mday)
+ result = true;
+ break;
+ case nsMsgSearchOp::Isnt:
+ if (tmThis.tm_year != tmToMatch.tm_year ||
+ tmThis.tm_month != tmToMatch.tm_month ||
+ tmThis.tm_mday != tmToMatch.tm_mday)
+ result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for dates");
+ }
+ }
+ *pResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchAge(PRTime msgDate, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ bool result = false;
+ nsresult rv = NS_OK;
+
+ PRTime now = PR_Now();
+ PRTime cutOffDay = now - m_value.u.age * PR_USEC_PER_DAY;
+
+ bool cutOffDayInTheFuture = m_value.u.age < 0;
+
+ // So now cutOffDay is the PRTime cut-off point.
+ // Any msg with a time less than that will be past the age.
+
+ switch (m_operator) {
+ case nsMsgSearchOp::IsGreaterThan: // is older than, or more in the future
+ if ((!cutOffDayInTheFuture && msgDate < cutOffDay) ||
+ (cutOffDayInTheFuture && msgDate > cutOffDay))
+ result = true;
+ break;
+ case nsMsgSearchOp::IsLessThan: // is younger than, or less in the future
+ if ((!cutOffDayInTheFuture && msgDate > cutOffDay) ||
+ (cutOffDayInTheFuture && msgDate < cutOffDay))
+ result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ PRExplodedTime msgDateExploded;
+ PRExplodedTime cutOffDayExploded;
+ if (NS_SUCCEEDED(GetLocalTimes(msgDate, cutOffDay, msgDateExploded,
+ cutOffDayExploded))) {
+ if ((msgDateExploded.tm_mday == cutOffDayExploded.tm_mday) &&
+ (msgDateExploded.tm_month == cutOffDayExploded.tm_month) &&
+ (msgDateExploded.tm_year == cutOffDayExploded.tm_year))
+ result = true;
+ }
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for msg age");
+ }
+ *pResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchSize(uint32_t sizeToMatch, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+ bool result = false;
+ // We reduce the sizeToMatch rather than supplied size
+ // as then we can do an exact match on the displayed value
+ // which will be less confusing to the user.
+ uint32_t sizeToMatchKB = sizeToMatch;
+
+ if (sizeToMatchKB < 1024) sizeToMatchKB = 1024;
+
+ sizeToMatchKB /= 1024;
+
+ switch (m_operator) {
+ case nsMsgSearchOp::IsGreaterThan:
+ if (sizeToMatchKB > m_value.u.size) result = true;
+ break;
+ case nsMsgSearchOp::IsLessThan:
+ if (sizeToMatchKB < m_value.u.size) result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (sizeToMatchKB == m_value.u.size) result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for size to match");
+ }
+ *pResult = result;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchJunkStatus(const char* aJunkScore,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ if (m_operator == nsMsgSearchOp::IsEmpty) {
+ *pResult = !(aJunkScore && *aJunkScore);
+ return NS_OK;
+ }
+ if (m_operator == nsMsgSearchOp::IsntEmpty) {
+ *pResult = (aJunkScore && *aJunkScore);
+ return NS_OK;
+ }
+
+ nsMsgJunkStatus junkStatus;
+ if (aJunkScore && *aJunkScore) {
+ junkStatus = (atoi(aJunkScore) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ ? nsIJunkMailPlugin::JUNK
+ : nsIJunkMailPlugin::GOOD;
+ } else {
+ // the in UI, we only show "junk" or "not junk"
+ // unknown, or nsIJunkMailPlugin::UNCLASSIFIED is shown as not junk
+ // so for the search to work as expected, treat unknown as not junk
+ junkStatus = nsIJunkMailPlugin::GOOD;
+ }
+
+ nsresult rv = NS_OK;
+ bool matches = (junkStatus == m_value.u.junkStatus);
+
+ switch (m_operator) {
+ case nsMsgSearchOp::Is:
+ break;
+ case nsMsgSearchOp::Isnt:
+ matches = !matches;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ matches = false;
+ NS_ERROR("invalid compare op for junk status");
+ }
+
+ *pResult = matches;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchJunkScoreOrigin(const char* aJunkScoreOrigin,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ bool matches = false;
+ nsresult rv = NS_OK;
+
+ switch (m_operator) {
+ case nsMsgSearchOp::Is:
+ matches = aJunkScoreOrigin && m_value.utf8String.Equals(aJunkScoreOrigin);
+ break;
+ case nsMsgSearchOp::Isnt:
+ matches =
+ !aJunkScoreOrigin || !m_value.utf8String.Equals(aJunkScoreOrigin);
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for junk score origin");
+ }
+
+ *pResult = matches;
+ return rv;
+}
+
+nsresult nsMsgSearchTerm::MatchJunkPercent(uint32_t aJunkPercent,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+ bool result = false;
+ switch (m_operator) {
+ case nsMsgSearchOp::IsGreaterThan:
+ if (aJunkPercent > m_value.u.junkPercent) result = true;
+ break;
+ case nsMsgSearchOp::IsLessThan:
+ if (aJunkPercent < m_value.u.junkPercent) result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (aJunkPercent == m_value.u.junkPercent) result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for junk percent");
+ }
+ *pResult = result;
+ return rv;
+}
+
+// MatchStatus () is not only used for nsMsgMessageFlags but also for
+// nsMsgFolderFlags (both being 'unsigned long')
+nsresult nsMsgSearchTerm::MatchStatus(uint32_t statusToMatch, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+ bool matches = (statusToMatch & m_value.u.msgStatus);
+
+ // nsMsgSearchOp::Is and nsMsgSearchOp::Isnt are intentionally used as
+ // Contains and DoesntContain respectively, for legacy reasons.
+ switch (m_operator) {
+ case nsMsgSearchOp::Is:
+ break;
+ case nsMsgSearchOp::Isnt:
+ matches = !matches;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ matches = false;
+ NS_ERROR("invalid compare op for msg status");
+ }
+
+ *pResult = matches;
+ return rv;
+}
+
+/*
+ * MatchKeyword Logic table (*pResult: + is true, - is false)
+ *
+ * # Valid Tokens IsEmpty IsntEmpty Contains DoesntContain Is Isnt
+ * 0 + - - + - +
+ * Term found? N Y N Y N Y N Y
+ * 1 - + - + + - - + + -
+ * >1 - + - + + - - - + +
+ */
+// look up nsMsgSearchTerm::m_value in space-delimited keywordList
+nsresult nsMsgSearchTerm::MatchKeyword(const nsACString& keywordList,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ bool matches = false;
+
+ // special-case empty for performance reasons
+ if (keywordList.IsEmpty()) {
+ *pResult = m_operator != nsMsgSearchOp::Contains &&
+ m_operator != nsMsgSearchOp::Is &&
+ m_operator != nsMsgSearchOp::IsntEmpty;
+ return NS_OK;
+ }
+
+ // check if we can skip expensive valid keywordList test
+ if (m_operator == nsMsgSearchOp::DoesntContain ||
+ m_operator == nsMsgSearchOp::Contains) {
+ nsCString keywordString(keywordList);
+ const uint32_t kKeywordLen = m_value.utf8String.Length();
+ const char* matchStart =
+ PL_strstr(keywordString.get(), m_value.utf8String.get());
+ while (matchStart) {
+ // For a real match, matchStart must be the start of the keywordList or
+ // preceded by a space and matchEnd must point to a \0 or space.
+ const char* matchEnd = matchStart + kKeywordLen;
+ if ((matchStart == keywordString.get() || matchStart[-1] == ' ') &&
+ (!*matchEnd || *matchEnd == ' ')) {
+ // found the keyword
+ *pResult = m_operator == nsMsgSearchOp::Contains;
+ return NS_OK;
+ }
+ // no match yet, so search on
+ matchStart = PL_strstr(matchEnd, m_value.utf8String.get());
+ }
+ // keyword not found
+ *pResult = m_operator == nsMsgSearchOp::DoesntContain;
+ return NS_OK;
+ }
+
+ // Only accept valid keys in tokens.
+ nsresult rv = NS_OK;
+ nsTArray<nsCString> keywordArray;
+ ParseString(keywordList, ' ', keywordArray);
+ nsCOMPtr<nsIMsgTagService> tagService(
+ do_GetService("@mozilla.org/messenger/tagservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Loop through tokens in keywords
+ uint32_t count = keywordArray.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ // is this token a valid tag? Otherwise ignore it
+ bool isValid;
+ rv = tagService->IsValidKey(keywordArray[i], &isValid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isValid) {
+ // IsEmpty fails on any valid token
+ if (m_operator == nsMsgSearchOp::IsEmpty) {
+ *pResult = false;
+ return rv;
+ }
+
+ // IsntEmpty succeeds on any valid token
+ if (m_operator == nsMsgSearchOp::IsntEmpty) {
+ *pResult = true;
+ return rv;
+ }
+
+ // Does this valid tag key match our search term?
+ matches = keywordArray[i].Equals(m_value.utf8String);
+
+ // Is or Isn't partly determined on a single unmatched token
+ if (!matches) {
+ if (m_operator == nsMsgSearchOp::Is) {
+ *pResult = false;
+ return rv;
+ }
+ if (m_operator == nsMsgSearchOp::Isnt) {
+ *pResult = true;
+ return rv;
+ }
+ }
+ }
+ }
+
+ if (m_operator == nsMsgSearchOp::Is) {
+ *pResult = matches;
+ return NS_OK;
+ }
+
+ if (m_operator == nsMsgSearchOp::Isnt) {
+ *pResult = !matches;
+ return NS_OK;
+ }
+
+ if (m_operator == nsMsgSearchOp::IsEmpty) {
+ *pResult = true;
+ return NS_OK;
+ }
+
+ if (m_operator == nsMsgSearchOp::IsntEmpty) {
+ *pResult = false;
+ return NS_OK;
+ }
+
+ // no valid match operator found
+ *pResult = false;
+ NS_ERROR("invalid compare op for msg status");
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgSearchTerm::MatchPriority(nsMsgPriorityValue priorityToMatch,
+ bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv = NS_OK;
+ bool result = false;
+
+ // Use this ugly little hack to get around the fact that enums don't have
+ // integer compare operators
+ int p1 = (priorityToMatch == nsMsgPriority::none) ? (int)nsMsgPriority::normal
+ : (int)priorityToMatch;
+ int p2 = (int)m_value.u.priority;
+
+ switch (m_operator) {
+ case nsMsgSearchOp::IsHigherThan:
+ if (p1 > p2) result = true;
+ break;
+ case nsMsgSearchOp::IsLowerThan:
+ if (p1 < p2) result = true;
+ break;
+ case nsMsgSearchOp::Is:
+ if (p1 == p2) result = true;
+ break;
+ case nsMsgSearchOp::Isnt:
+ if (p1 != p2) result = true;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE;
+ NS_ERROR("invalid compare op for priority");
+ }
+ *pResult = result;
+ return rv;
+}
+
+// match a custom search term
+NS_IMETHODIMP nsMsgSearchTerm::MatchCustom(nsIMsgDBHdr* aHdr, bool* pResult) {
+ NS_ENSURE_ARG_POINTER(pResult);
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgSearchCustomTerm> customTerm;
+ rv = filterService->GetCustomTerm(m_customId, getter_AddRefs(customTerm));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (customTerm)
+ return customTerm->Match(aHdr, m_value.utf8String, m_operator, pResult);
+ *pResult = false; // default to no match if term is missing
+ return NS_ERROR_FAILURE; // missing custom term
+}
+
+// set the id of a custom search term
+NS_IMETHODIMP nsMsgSearchTerm::SetCustomId(const nsACString& aId) {
+ m_customId = aId;
+ return NS_OK;
+}
+
+// get the id of a custom search term
+NS_IMETHODIMP nsMsgSearchTerm::GetCustomId(nsACString& aResult) {
+ aResult = m_customId;
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgSearchTerm, Attrib, nsMsgSearchAttribValue, m_attribute)
+NS_IMPL_GETSET(nsMsgSearchTerm, Op, nsMsgSearchOpValue, m_operator)
+NS_IMPL_GETSET(nsMsgSearchTerm, MatchAll, bool, m_matchAll)
+
+NS_IMETHODIMP
+nsMsgSearchTerm::GetValue(nsIMsgSearchValue** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ADDREF(*aResult = new nsMsgSearchValueImpl(&m_value));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::SetValue(nsIMsgSearchValue* aValue) {
+ nsMsgResultElement::AssignValues(aValue, &m_value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::GetBooleanAnd(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = (m_booleanOp == nsMsgSearchBooleanOp::BooleanAND);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::SetBooleanAnd(bool aValue) {
+ if (aValue)
+ m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanAND);
+ else
+ m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanOR);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::GetArbitraryHeader(nsACString& aResult) {
+ aResult = m_arbitraryHeader;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::SetArbitraryHeader(const nsACString& aValue) {
+ m_arbitraryHeader = aValue;
+ ToLowerCaseExceptSpecials(m_arbitraryHeader);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::GetHdrProperty(nsACString& aResult) {
+ aResult = m_hdrProperty;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchTerm::SetHdrProperty(const nsACString& aValue) {
+ m_hdrProperty = aValue;
+ ToLowerCaseExceptSpecials(m_hdrProperty);
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgSearchTerm, BeginsGrouping, bool, mBeginsGrouping)
+NS_IMPL_GETSET(nsMsgSearchTerm, EndsGrouping, bool, mEndsGrouping)
+
+//
+// Certain possible standard values of a message database row also sometimes
+// appear as header values. To prevent a naming collision, we use all
+// lower case for the standard headers, and first capital when those
+// same strings are requested as arbitrary headers. This routine is used
+// when setting arbitrary headers.
+//
+void nsMsgSearchTerm::ToLowerCaseExceptSpecials(nsACString& aValue) {
+ if (aValue.LowerCaseEqualsLiteral("sender"))
+ aValue.AssignLiteral("Sender");
+ else if (aValue.LowerCaseEqualsLiteral("date"))
+ aValue.AssignLiteral("Date");
+ else if (aValue.LowerCaseEqualsLiteral("status"))
+ aValue.AssignLiteral("Status");
+ else
+ ToLowerCase(aValue);
+}
+
+//-----------------------------------------------------------------------------
+// nsMsgSearchScopeTerm implementation
+//-----------------------------------------------------------------------------
+nsMsgSearchScopeTerm::nsMsgSearchScopeTerm(nsIMsgSearchSession* session,
+ nsMsgSearchScopeValue attribute,
+ nsIMsgFolder* folder) {
+ m_attribute = attribute;
+ m_folder = folder;
+ m_searchServer = true;
+ m_searchSession = do_GetWeakReference(session);
+}
+
+nsMsgSearchScopeTerm::nsMsgSearchScopeTerm() { m_searchServer = true; }
+
+nsMsgSearchScopeTerm::~nsMsgSearchScopeTerm() {
+ if (m_inputStream) m_inputStream->Close();
+ m_inputStream = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchScopeTerm, nsIMsgSearchScopeTerm)
+
+NS_IMETHODIMP
+nsMsgSearchScopeTerm::GetFolder(nsIMsgFolder** aResult) {
+ NS_IF_ADDREF(*aResult = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchScopeTerm::GetSearchSession(nsIMsgSearchSession** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ searchSession.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchScopeTerm::GetInputStream(nsIMsgDBHdr* aMsgHdr,
+ nsIInputStream** aInputStream) {
+ NS_ENSURE_ARG_POINTER(aInputStream);
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_TRUE(m_folder, NS_ERROR_NULL_POINTER);
+ nsresult rv =
+ m_folder->GetMsgInputStream(aMsgHdr, getter_AddRefs(m_inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_IF_ADDREF(*aInputStream = m_inputStream);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgSearchScopeTerm::CloseInputStream() {
+ if (m_inputStream) {
+ m_inputStream->Close();
+ m_inputStream = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSearchScopeTerm::TimeSlice(bool* aDone) {
+ return m_adapter->Search(aDone);
+}
+
+nsresult nsMsgSearchScopeTerm::InitializeAdapter(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) {
+ if (m_adapter) return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ switch (m_attribute) {
+ case nsMsgSearchScope::onlineMail:
+ m_adapter = new nsMsgSearchOnlineMail(this, termList);
+ break;
+ case nsMsgSearchScope::offlineMail:
+ case nsMsgSearchScope::onlineManual:
+ m_adapter = new nsMsgSearchOfflineMail(this, termList);
+ break;
+ case nsMsgSearchScope::newsEx:
+ NS_ASSERTION(false, "not supporting newsEx yet");
+ break;
+ case nsMsgSearchScope::news:
+ m_adapter = new nsMsgSearchNews(this, termList);
+ break;
+ case nsMsgSearchScope::allSearchableGroups:
+ NS_ASSERTION(false, "not supporting allSearchableGroups yet");
+ break;
+ case nsMsgSearchScope::LDAP:
+ NS_ASSERTION(false, "not supporting LDAP yet");
+ break;
+ case nsMsgSearchScope::localNews:
+ case nsMsgSearchScope::localNewsJunk:
+ case nsMsgSearchScope::localNewsBody:
+ case nsMsgSearchScope::localNewsJunkBody:
+ m_adapter = new nsMsgSearchOfflineNews(this, termList);
+ break;
+ default:
+ NS_ASSERTION(false, "invalid scope");
+ rv = NS_ERROR_FAILURE;
+ }
+
+ if (m_adapter) rv = m_adapter->ValidateTerms();
+
+ return rv;
+}
+
+char* nsMsgSearchScopeTerm::GetStatusBarName() { return nullptr; }
+
+//-----------------------------------------------------------------------------
+// nsMsgResultElement implementation
+//-----------------------------------------------------------------------------
+
+nsMsgResultElement::nsMsgResultElement(nsIMsgSearchAdapter* adapter) {
+ m_adapter = adapter;
+}
+
+nsMsgResultElement::~nsMsgResultElement() {}
+
+nsresult nsMsgResultElement::AddValue(nsIMsgSearchValue* value) {
+ m_valueList.AppendElement(value);
+ return NS_OK;
+}
+
+nsresult nsMsgResultElement::AddValue(nsMsgSearchValue* value) {
+ nsMsgSearchValueImpl* valueImpl = new nsMsgSearchValueImpl(value);
+ delete value; // we keep the nsIMsgSearchValue, not
+ // the nsMsgSearchValue
+ return AddValue(valueImpl);
+}
+
+nsresult nsMsgResultElement::AssignValues(nsIMsgSearchValue* src,
+ nsMsgSearchValue* dst) {
+ NS_ENSURE_ARG_POINTER(src);
+ NS_ENSURE_ARG_POINTER(dst);
+ // Yes, this could be an operator overload, but nsMsgSearchValue is totally
+ // public, so I'd have to define a derived class with nothing by operator=,
+ // and that seems like a bit much
+ nsresult rv = NS_OK;
+ src->GetAttrib(&dst->attribute);
+ switch (dst->attribute) {
+ case nsMsgSearchAttrib::Priority:
+ rv = src->GetPriority(&dst->u.priority);
+ break;
+ case nsMsgSearchAttrib::Date:
+ rv = src->GetDate(&dst->u.date);
+ break;
+ case nsMsgSearchAttrib::HasAttachmentStatus:
+ case nsMsgSearchAttrib::MsgStatus:
+ case nsMsgSearchAttrib::FolderFlag:
+ case nsMsgSearchAttrib::Uint32HdrProperty:
+ rv = src->GetStatus(&dst->u.msgStatus);
+ break;
+ case nsMsgSearchAttrib::MessageKey:
+ rv = src->GetMsgKey(&dst->u.key);
+ break;
+ case nsMsgSearchAttrib::AgeInDays:
+ rv = src->GetAge(&dst->u.age);
+ break;
+ case nsMsgSearchAttrib::JunkStatus:
+ rv = src->GetJunkStatus(&dst->u.junkStatus);
+ break;
+ case nsMsgSearchAttrib::JunkPercent:
+ rv = src->GetJunkPercent(&dst->u.junkPercent);
+ break;
+ case nsMsgSearchAttrib::Size:
+ rv = src->GetSize(&dst->u.size);
+ break;
+ default:
+ if (dst->attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) {
+ NS_ASSERTION(IS_STRING_ATTRIBUTE(dst->attribute),
+ "assigning non-string result");
+ nsString unicodeString;
+ rv = src->GetStr(unicodeString);
+ CopyUTF16toUTF8(unicodeString, dst->utf8String);
+ dst->utf16String = unicodeString;
+ } else
+ rv = NS_ERROR_INVALID_ARG;
+ }
+ return rv;
+}
+
+nsresult nsMsgResultElement::GetValue(nsMsgSearchAttribValue attrib,
+ nsMsgSearchValue** outValue) const {
+ nsresult rv = NS_OK;
+ *outValue = NULL;
+
+ for (uint32_t i = 0; i < m_valueList.Length() && NS_FAILED(rv); i++) {
+ nsMsgSearchAttribValue valueAttribute;
+ m_valueList[i]->GetAttrib(&valueAttribute);
+ if (attrib == valueAttribute) {
+ *outValue = new nsMsgSearchValue;
+ if (*outValue) {
+ rv = AssignValues(m_valueList[i], *outValue);
+ // Now this is really strange! What is this assignment doing here?
+ rv = NS_OK;
+ } else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgResultElement::GetPrettyName(nsMsgSearchValue** value) {
+ return GetValue(nsMsgSearchAttrib::Location, value);
+}
+
+nsresult nsMsgResultElement::Open(void* window) {
+ return NS_ERROR_NULL_POINTER;
+}
diff --git a/comm/mailnews/search/src/nsMsgSearchValue.cpp b/comm/mailnews/search/src/nsMsgSearchValue.cpp
new file mode 100644
index 0000000000..55f88b5fd2
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchValue.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "MailNewsTypes.h"
+#include "nsMsgSearchValue.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgUtils.h"
+#include "nsString.h"
+
+nsMsgSearchValueImpl::nsMsgSearchValueImpl(nsMsgSearchValue* aInitialValue) {
+ mValue = *aInitialValue;
+}
+
+nsMsgSearchValueImpl::~nsMsgSearchValueImpl() {}
+
+NS_IMPL_ISUPPORTS(nsMsgSearchValueImpl, nsIMsgSearchValue)
+
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Priority, nsMsgPriorityValue,
+ mValue.u.priority)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Status, uint32_t, mValue.u.msgStatus)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Size, uint32_t, mValue.u.size)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, MsgKey, nsMsgKey, mValue.u.key)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Age, int32_t, mValue.u.age)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Date, PRTime, mValue.u.date)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, Attrib, nsMsgSearchAttribValue,
+ mValue.attribute)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, JunkStatus, uint32_t, mValue.u.junkStatus)
+NS_IMPL_GETSET(nsMsgSearchValueImpl, JunkPercent, uint32_t,
+ mValue.u.junkPercent)
+
+NS_IMETHODIMP
+nsMsgSearchValueImpl::GetFolder(nsIMsgFolder** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_TRUE(mValue.attribute == nsMsgSearchAttrib::FolderInfo,
+ NS_ERROR_ILLEGAL_VALUE);
+ NS_IF_ADDREF(*aResult = mValue.u.folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchValueImpl::SetFolder(nsIMsgFolder* aValue) {
+ NS_ENSURE_TRUE(mValue.attribute == nsMsgSearchAttrib::FolderInfo,
+ NS_ERROR_ILLEGAL_VALUE);
+ mValue.u.folder = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchValueImpl::GetStr(nsAString& aResult) {
+ NS_ENSURE_TRUE(IS_STRING_ATTRIBUTE(mValue.attribute), NS_ERROR_ILLEGAL_VALUE);
+ aResult = mValue.utf16String;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchValueImpl::SetStr(const nsAString& aValue) {
+ NS_ENSURE_TRUE(IS_STRING_ATTRIBUTE(mValue.attribute), NS_ERROR_ILLEGAL_VALUE);
+ CopyUTF16toUTF8(aValue, mValue.utf8String);
+ mValue.utf16String = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchValueImpl::ToString(nsAString& aResult) {
+ aResult.AssignLiteral("[nsIMsgSearchValue: ");
+ if (IS_STRING_ATTRIBUTE(mValue.attribute)) {
+ aResult.Append(mValue.utf16String);
+ return NS_OK;
+ }
+
+ switch (mValue.attribute) {
+ case nsMsgSearchAttrib::Priority:
+ case nsMsgSearchAttrib::Date:
+ case nsMsgSearchAttrib::MsgStatus:
+ case nsMsgSearchAttrib::MessageKey:
+ case nsMsgSearchAttrib::Size:
+ case nsMsgSearchAttrib::AgeInDays:
+ case nsMsgSearchAttrib::FolderInfo:
+ case nsMsgSearchAttrib::JunkStatus:
+ case nsMsgSearchAttrib::JunkPercent: {
+ nsAutoString tempInt;
+ tempInt.AppendInt(mValue.attribute);
+
+ aResult.AppendLiteral("type=");
+ aResult.Append(tempInt);
+ } break;
+ default:
+ NS_ERROR("Unknown search value type");
+ }
+
+ aResult.Append(']');
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/search/src/nsMsgSearchValue.h b/comm/mailnews/search/src/nsMsgSearchValue.h
new file mode 100644
index 0000000000..225ee64760
--- /dev/null
+++ b/comm/mailnews/search/src/nsMsgSearchValue.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#ifndef __nsMsgSearchValue_h
+#define __nsMsgSearchValue_h
+
+#include "nsIMsgSearchValue.h"
+#include "nsMsgSearchCore.h"
+
+class nsMsgSearchValueImpl : public nsIMsgSearchValue {
+ public:
+ explicit nsMsgSearchValueImpl(nsMsgSearchValue* aInitialValue);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSEARCHVALUE
+
+ private:
+ virtual ~nsMsgSearchValueImpl();
+
+ nsMsgSearchValue mValue;
+};
+
+#endif
diff --git a/comm/mailnews/search/test/moz.build b/comm/mailnews/search/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/search/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/search/test/unit/head_mailbase.js b/comm/mailnews/search/test/unit/head_mailbase.js
new file mode 100644
index 0000000000..9f37623291
--- /dev/null
+++ b/comm/mailnews/search/test/unit/head_mailbase.js
@@ -0,0 +1,23 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/search/test/unit/test_base64_decoding.js b/comm/mailnews/search/test/unit/test_base64_decoding.js
new file mode 100644
index 0000000000..e0f7ddd94d
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_base64_decoding.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+// This tests that we do not crash when loading the email bodySearchCrash,
+// which was fixed in bug 465805
+
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Contains = Ci.nsMsgSearchOp.Contains;
+var Body = Ci.nsMsgSearchAttrib.Body;
+
+var Files = [
+ "../../../data/bugmail1",
+ "../../../data/bodySearchCrash", // Test for bug 465805.
+ "../../../data/base64-with-whitespace.eml", // Test for bug 1487421.
+];
+
+var Tests = [
+ {
+ // this number appears in bugmail1
+ value: "432710",
+ attrib: Body,
+ op: Contains,
+ count: 1,
+ },
+ {
+ // this appears in base64-with-whitespace.eml
+ value: "abcdefghijklmnopqrstuvwxyz",
+ attrib: Body,
+ op: Contains,
+ count: 1,
+ },
+];
+
+function run_test() {
+ // Setup local mail accounts.
+ localAccountUtils.loadLocalMailAccount();
+
+ // Get a message into the local filestore. function testBodySearch() continues the testing after the copy.
+ do_test_pending();
+ copyListener.OnStopCopy(null);
+ return true;
+}
+
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ let fileName = Files.shift();
+ if (fileName) {
+ let file = do_get_file(fileName);
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ } else {
+ testBodySearch();
+ }
+ },
+};
+
+// Runs at completion of copy
+
+// process each test from queue, calls itself upon completion of each search
+function testBodySearch() {
+ print("Test Body Search");
+ var test = Tests.shift();
+ if (test) {
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.value,
+ test.attrib,
+ test.op,
+ test.count,
+ testBodySearch
+ );
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_bug366491.js b/comm/mailnews/search/test/unit/test_bug366491.js
new file mode 100644
index 0000000000..774e8932a3
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_bug366491.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+// tests return of junk percent from bayesian filter
+
+// main setup
+
+// only needed during debug
+// do_import_script("mailnews/extensions/bayesian-spam-filter/test/resources/trainingfile.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// local constants
+var kUnclassified = MailServices.junk.UNCLASSIFIED;
+var kJunk = MailServices.junk.JUNK;
+var kGood = MailServices.junk.GOOD;
+
+/*
+ * This test is not intended to check the spam calculations,
+ * but only that the junk percent is transmitted (particularly
+ * for intermediate values). The test
+ * junkPercent values below were calculated by the plugin,
+ * not indepedently verified.
+ */
+
+var tests = [
+ {
+ fileName: "ham2.eml",
+ junkPercent: 8,
+ },
+ {
+ fileName: "spam2.eml",
+ junkPercent: 81,
+ },
+];
+
+var emails = [
+ {
+ fileName: "ham1.eml",
+ classification: kGood,
+ },
+ {
+ fileName: "spam1.eml",
+ classification: kJunk,
+ },
+];
+
+// main test
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ do_test_pending();
+ doTestingListener.onMessageClassified(null, null, null);
+ return true;
+}
+
+var haveClassification = false;
+var doTestingListener = {
+ onMessageClassified(aMsgURI, aClassification, aJunkPercent) {
+ // Do we have more training emails? If so, train
+ var email = emails.shift();
+ if (email) {
+ MailServices.junk.setMessageClassification(
+ getSpec(email.fileName),
+ kUnclassified,
+ email.classification,
+ null,
+ doTestingListener
+ );
+ return;
+ }
+
+ if (!aMsgURI) {
+ // Ignore end of batch.
+ return;
+ }
+
+ // Have we completed a classification? If so, test
+ if (haveClassification) {
+ let test = tests.shift();
+ Assert.equal(getSpec(test.fileName), aMsgURI);
+ Assert.equal(test.junkPercent, aJunkPercent);
+ }
+
+ // Do we have more classifications to do? Then classify the first one.
+ if (tests.length) {
+ haveClassification = true;
+ MailServices.junk.classifyMessage(
+ getSpec(tests[0].fileName),
+ null,
+ doTestingListener
+ );
+ } else {
+ do_test_finished();
+ }
+ },
+};
+
+// helper functions
+
+function getSpec(aFileName) {
+ var file = do_get_file(
+ "../../../extensions/bayesian-spam-filter/test/unit/resources/" + aFileName
+ );
+ var uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIURL);
+ uri = uri.mutate().setQuery("type=application/x-message-display").finalize();
+ return uri.spec;
+}
diff --git a/comm/mailnews/search/test/unit/test_bug404489.js b/comm/mailnews/search/test/unit/test_bug404489.js
new file mode 100644
index 0000000000..93ae2aac3c
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_bug404489.js
@@ -0,0 +1,202 @@
+/* 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/. */
+
+// Tests that custom headers like "Sender" work (bug 404489)
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Contains = Ci.nsMsgSearchOp.Contains;
+var gArrayHdrs = ["X-Bugzilla-Who", "Sender"];
+var gFirstHeader = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+var fileName = "../../../data/SenderHeader";
+
+var Tests = [
+ /* test header:
+ X-Bugzilla-Who: bugmail@example.org
+
+ This just shows that normal custom headers work
+ */
+ {
+ testValue: "bugmail",
+ attrib: gFirstHeader,
+ op: Contains,
+ count: 1,
+ },
+ {
+ testValue: "ThisIsNotThere",
+ attrib: gFirstHeader,
+ op: Contains,
+ count: 0,
+ },
+ /* test header:
+ Sender: iamthesender@example.com
+
+ This is the main fix of bug 404489, that we can use Sender as a header
+ */
+ {
+ testValue: "iamthesender",
+ attrib: gFirstHeader + 1,
+ op: Contains,
+ count: 1,
+ },
+ /* test header:
+ From: bugzilla-daemon@mozilla.invalid
+
+ Here we show that the "From" header does not fire tests for the
+ "Sender" arbitrary headers, but does fire the standard test
+ for nsMsgSenderAttrib.Sender
+ */
+ {
+ testValue: "bugzilla",
+ attrib: gFirstHeader + 1,
+ op: Contains,
+ count: 0,
+ },
+ {
+ testValue: "bugzilla",
+ attrib: Ci.nsMsgSearchAttrib.Sender,
+ op: Contains,
+ count: 1,
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // add the custom headers into the preferences file, ":" delimited
+
+ var hdrs;
+ if (gArrayHdrs.length == 1) {
+ hdrs = gArrayHdrs;
+ } else {
+ hdrs = gArrayHdrs.join(": ");
+ }
+ Services.prefs.setCharPref("mailnews.customHeaders", hdrs);
+
+ // Get a message into the local filestore. function continue_test() continues the testing after the copy.
+ do_test_pending();
+ var file = do_get_file(fileName);
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ return true;
+}
+
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continue_test();
+ },
+};
+
+// Runs at completion of each copy
+// process each test from queue, calls itself upon completion of each search
+function continue_test() {
+ var test = Tests.shift();
+ if (test) {
+ new TestSearchx(
+ localAccountUtils.inboxFolder,
+ test.testValue,
+ test.attrib,
+ test.op,
+ test.count,
+ continue_test
+ );
+ } else {
+ do_test_finished();
+ }
+}
+
+/*
+ * TestSearchx: Class to test number of search hits
+ *
+ * @param aFolder: the folder to search
+ * @param aValue: value used for the search
+ * The interpretation of aValue depends on aAttrib. It
+ * defaults to string, but for certain attributes other
+ * types are used.
+ * WARNING: not all attributes have been tested.
+ *
+ * @param aAttrib: attribute for the search (Ci.nsMsgSearchAttrib.Size, etc.)
+ * @param aOp: operation for the search (Ci.nsMsgSearchOp.Contains, etc.)
+ * @param aHitCount: expected number of search hits
+ * @param onDone: function to call on completion of search
+ *
+ */
+
+function TestSearchx(aFolder, aValue, aAttrib, aOp, aHitCount, onDone) {
+ var searchListener = {
+ onSearchHit(dbHdr, folder) {
+ hitCount++;
+ },
+ onSearchDone(status) {
+ print("Finished search does " + aHitCount + " equal " + hitCount + "?");
+ searchSession = null;
+ Assert.equal(aHitCount, hitCount);
+ if (onDone) {
+ onDone();
+ }
+ },
+ onNewSearch() {
+ hitCount = 0;
+ },
+ };
+
+ // define and initiate the search session
+
+ var hitCount;
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder);
+ var searchTerm = searchSession.createTerm();
+ searchTerm.attrib = aAttrib;
+
+ var value = searchTerm.value;
+ // This is tricky - value.attrib must be set before actual values
+ value.attrib = aAttrib;
+ if (aAttrib == Ci.nsMsgSearchAttrib.JunkPercent) {
+ value.junkPercent = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Priority) {
+ value.priority = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Date) {
+ value.date = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.MsgStatus) {
+ value.status = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.MessageKey) {
+ value.msgKey = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Size) {
+ value.size = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.AgeInDays) {
+ value.age = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.JunkStatus) {
+ value.junkStatus = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
+ value.status = Ci.nsMsgMessageFlags.Attachment;
+ } else {
+ value.str = aValue;
+ }
+ searchTerm.value = value;
+ if (aAttrib > Ci.nsMsgSearchAttrib.OtherHeader) {
+ searchTerm.arbitraryHeader =
+ gArrayHdrs[aAttrib - 1 - Ci.nsMsgSearchAttrib.OtherHeader];
+ }
+ searchTerm.op = aOp;
+ searchTerm.booleanAnd = false;
+ searchSession.appendTerm(searchTerm);
+ searchSession.registerListener(searchListener);
+ searchSession.search(null);
+}
diff --git a/comm/mailnews/search/test/unit/test_copyThenMoveManual.js b/comm/mailnews/search/test/unit/test_copyThenMoveManual.js
new file mode 100644
index 0000000000..ef34aeb3ca
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_copyThenMoveManual.js
@@ -0,0 +1,116 @@
+/*
+ * This file tests copy followed by a move in a single filter.
+ * Tests fix from bug 448337.
+ *
+ * Original author: Kent James <kent@caspia.com>
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var gFiles = ["../../../data/bugmail1"];
+var gCopyFolder;
+var gMoveFolder;
+var gFilter; // the test filter
+var gFilterList;
+var gTestArray = [
+ function createFilters() {
+ // setup manual copy then move mail filters on the inbox
+ gFilterList = localAccountUtils.incomingServer.getFilterList(null);
+ gFilter = gFilterList.createFilter("copyThenMoveAll");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.matchAll = true;
+ gFilter.appendTerm(searchTerm);
+ let copyAction = gFilter.createAction();
+ copyAction.type = Ci.nsMsgFilterAction.CopyToFolder;
+ copyAction.targetFolderUri = gCopyFolder.URI;
+ gFilter.appendAction(copyAction);
+ let moveAction = gFilter.createAction();
+ moveAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ moveAction.targetFolderUri = gMoveFolder.URI;
+ gFilter.appendAction(moveAction);
+ gFilter.enabled = true;
+ gFilter.filterType = Ci.nsMsgFilterType.Manual;
+ gFilterList.insertFilterAt(0, gFilter);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages1() {
+ gPOP3Pump.files = gFiles;
+ await gPOP3Pump.run();
+ },
+ // test applying filters to a message header
+ async function applyFilters() {
+ let promiseFolderEvent = PromiseTestUtils.promiseFolderEvent(
+ localAccountUtils.inboxFolder,
+ "DeleteOrMoveMsgCompleted"
+ );
+ MailServices.filters.applyFilters(
+ Ci.nsMsgFilterType.Manual,
+ [localAccountUtils.inboxFolder.firstNewMessage],
+ localAccountUtils.inboxFolder,
+ null
+ );
+ await promiseFolderEvent;
+ },
+ function verifyFolders1() {
+ // Copy and Move should each now have 1 message in them.
+ Assert.equal(folderCount(gCopyFolder), 1);
+ Assert.equal(folderCount(gMoveFolder), 1);
+ // the local inbox folder should now be empty, since the second
+ // operation was a move
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages2() {
+ gPOP3Pump.files = gFiles;
+ await gPOP3Pump.run();
+ },
+ // use the alternate call into the filter service
+ async function applyFiltersToFolders() {
+ let folders = [localAccountUtils.inboxFolder];
+ let promiseFolderEvent = PromiseTestUtils.promiseFolderEvent(
+ localAccountUtils.inboxFolder,
+ "DeleteOrMoveMsgCompleted"
+ );
+ MailServices.filters.applyFiltersToFolders(gFilterList, folders, null);
+ await promiseFolderEvent;
+ },
+ function verifyFolders2() {
+ // Copy and Move should each now have 2 message in them.
+ Assert.equal(folderCount(gCopyFolder), 2);
+ Assert.equal(folderCount(gMoveFolder), 2);
+ // the local inbox folder should now be empty, since the second
+ // operation was a move
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+ },
+ function endTest() {
+ // Cleanup, null out everything, close all cached connections and stop the
+ // server
+ dump(" Exiting mail tests\n");
+ gPOP3Pump = null;
+ },
+];
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
+
+function run_test() {
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ gCopyFolder = localAccountUtils.rootFolder.createLocalSubfolder("CopyFolder");
+ gMoveFolder = localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder");
+
+ gTestArray.forEach(x => add_task(x));
+
+ run_next_test();
+}
diff --git a/comm/mailnews/search/test/unit/test_junkWhitelisting.js b/comm/mailnews/search/test/unit/test_junkWhitelisting.js
new file mode 100644
index 0000000000..9263de68a2
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_junkWhitelisting.js
@@ -0,0 +1,204 @@
+/* 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/. */
+
+/*
+ * Testing of junk whitelisting
+ */
+
+// add address book setup
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+// add fake POP3 server driver
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/*
+ * The address available in the test address book is "PrimaryEmail1@test.invalid"
+ * Test emails may also include the address "invalid@example.com"
+ *
+ * Map of test email contents: (P is "Prim...", I is "inva.." address)
+ *
+ * Index Bugmail# From
+ * 0 1 P
+ * 1 3 I
+ *
+ */
+
+// indices into hdrs[] of email by domain
+var kDomainTest = 0;
+var kDomainExample = 1;
+
+var Files = ["../../../data/bugmail1", "../../../data/bugmail3"];
+
+var hdrs = [];
+
+function run_test() {
+ loadABFile(
+ "../../../addrbook/test/unit/data/cardForEmail",
+ kPABData.fileName
+ );
+
+ do_test_pending();
+
+ // kick off copying
+ gPOP3Pump.files = Files;
+ gPOP3Pump.onDone = continueTest;
+ gPOP3Pump.run();
+}
+
+function continueTest() {
+ // get the message headers
+ for (let header of localAccountUtils.inboxFolder.messages) {
+ hdrs.push(header);
+ }
+
+ // check with spam properties set on the local server
+ doChecks(localAccountUtils.incomingServer);
+
+ // Free our globals
+ hdrs = null;
+ gPOP3Pump = null;
+ do_test_finished();
+}
+
+function doChecks(server) {
+ let spamSettings = server.spamSettings;
+
+ // default is to use the whitelist
+ Assert.ok(spamSettings.useWhiteList);
+
+ // check email with the address PrimaryEmail1@test.invalid
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // check email without the address
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainExample]));
+
+ //
+ // check changes in server-level settings. Although the spamSettings object
+ // has methods to set these, those methods are not persistent (which seems
+ // strange). You need to set the actual preference, and call initialize on
+ // spam settings, to get the settings to be saved persistently and stick, then
+ // be recalled into the program. So that's the way that I will test it.
+ //
+
+ // disable whitelisting
+ server.setBoolValue("useWhiteList", false);
+ spamSettings.initialize(server);
+
+ // check that the change was propagated to spamSettings
+ Assert.ok(!spamSettings.useWhiteList);
+
+ // and affects whitelisting calculationss
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // re-enable whitelisting
+ server.setBoolValue("useWhiteList", true);
+ spamSettings.initialize(server);
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // Set an empty white list.
+ // To really empty this, I have to change the default value as well
+ Services.prefs.setCharPref("mail.server.default.whiteListAbURI", "");
+ server.setCharValue("whiteListAbURI", "");
+ spamSettings.initialize(server);
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // add a trusted domain. This is a global preference
+ Services.prefs.setCharPref("mail.trusteddomains", "example.com");
+ spamSettings.initialize(server);
+
+ // check email with the address invalid@example.com, a trusted domain
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainExample]));
+
+ // check email without the address
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // disable the trusted domain
+ Services.prefs.setCharPref("mail.trusteddomains", "");
+ spamSettings.initialize(server);
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainExample]));
+
+ // add back the Personal Address Book
+ server.setCharValue("whiteListAbURI", kPABData.URI);
+ spamSettings.initialize(server);
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ /*
+ * tests of whitelist suppression by identity
+ */
+
+ // setup
+ let account = MailServices.accounts.FindAccountForServer(server);
+ let identity = MailServices.accounts.createIdentity();
+ // start with an email that does not match
+ identity.email = "iAmNotTheSender@test.invalid";
+ account.addIdentity(identity);
+
+ // setup account and identify for the deferred-from fake server
+ let fakeAccount = MailServices.accounts.createAccount();
+ fakeAccount.incomingServer = gPOP3Pump.fakeServer;
+ let fakeIdentity = MailServices.accounts.createIdentity();
+ // start with an email that does not match
+ fakeIdentity.email = "iAmNotTheSender@wrong.invalid";
+ fakeAccount.addIdentity(fakeIdentity);
+
+ // gPOP3Pump delivers messages to the local inbox regardless of other
+ // settings. But because we are testing here one of those other settings,
+ // let's just pretend that it works like the real POP3 stuff, and set
+ // the correct setting for deferring.
+ gPOP3Pump.fakeServer.setCharValue("deferred_to_account", "account1");
+
+ // suppress whitelisting for sender
+ server.setBoolValue("inhibitWhiteListingIdentityUser", true);
+ spamSettings.initialize(server);
+ // (email does not match yet though)
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // add a matching email (mixing case)
+ identity.email = "PrimaryEMAIL1@test.INVALID";
+ spamSettings.initialize(server);
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // remove the matching email
+ identity.email = "iAmNotTheSender@test.invalid";
+ spamSettings.initialize(server);
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // add the email to the deferred-from server
+ fakeIdentity.email = "PrimaryEMAIL1@test.INVALID";
+ spamSettings.initialize(server);
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // stop suppressing identity users
+ server.setBoolValue("inhibitWhiteListingIdentityUser", false);
+ spamSettings.initialize(server);
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // remove the matching email from the fake identity
+ fakeIdentity.email = "iAmNotTheSender@wrong.invalid";
+
+ // add a fully non-matching domain to the identity
+ identity.email = "PrimaryEmail1@wrong.invalid";
+
+ // suppress whitelist by matching domain
+ server.setBoolValue("inhibitWhiteListingIdentityDomain", true);
+ spamSettings.initialize(server);
+ // but domain still does not match
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // add a matching email to the identity, in the domain (mixing case)
+ identity.email = "iAmNotTheSender@TEST.invalid";
+ spamSettings.initialize(server);
+ Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest]));
+
+ // stop suppressing whitelist by domain
+ server.setBoolValue("inhibitWhiteListingIdentityDomain", false);
+ spamSettings.initialize(server);
+ Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest]));
+}
diff --git a/comm/mailnews/search/test/unit/test_quarantineFilterMove.js b/comm/mailnews/search/test/unit/test_quarantineFilterMove.js
new file mode 100644
index 0000000000..853a3980f4
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_quarantineFilterMove.js
@@ -0,0 +1,181 @@
+/* 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/. */
+
+/*
+ * tests message moves with filter and quarantine enabled per bug 582918.
+ * It then tests that subsequent moves of the filtered messages work.
+ *
+ * adapted from test_copyThenMoveManual.js
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+var gFiles = ["../../../data/bugmail1", "../../../data/bugmail10"];
+
+var gMoveFolder, gMoveFolder2;
+var gFilter; // the test filter
+var gFilterList;
+var gTestArray = [
+ function createFilters() {
+ gFilterList = gPOP3Pump.fakeServer.getFilterList(null);
+ gFilter = gFilterList.createFilter("MoveAll");
+ let searchTerm = gFilter.createTerm();
+ searchTerm.matchAll = true;
+ gFilter.appendTerm(searchTerm);
+ let moveAction = gFilter.createAction();
+ moveAction.type = Ci.nsMsgFilterAction.MoveToFolder;
+ moveAction.targetFolderUri = gMoveFolder.URI;
+ gFilter.appendAction(moveAction);
+ gFilter.enabled = true;
+ gFilter.filterType = Ci.nsMsgFilterType.InboxRule;
+ gFilterList.insertFilterAt(0, gFilter);
+ },
+ // just get a message into the local folder
+ async function getLocalMessages1() {
+ gPOP3Pump.files = gFiles;
+ let promise1 = PromiseTestUtils.promiseFolderNotification(
+ gMoveFolder,
+ "msgsClassified"
+ );
+ let promise2 = gPOP3Pump.run();
+ await Promise.all([promise1, promise2]);
+ },
+ async function verifyFolders1() {
+ Assert.equal(folderCount(gMoveFolder), 2);
+ // the local inbox folder should now be empty, since the second
+ // operation was a move
+ Assert.equal(folderCount(localAccountUtils.inboxFolder), 0);
+
+ let msgs = [...gMoveFolder.msgDatabase.enumerateMessages()];
+ let firstMsgHdr = msgs[0];
+ let secondMsgHdr = msgs[1];
+ // Check that the messages have content
+ let messageContent = await getContentFromMessage(firstMsgHdr);
+ Assert.ok(
+ messageContent.includes("Some User <bugmail@example.org> changed")
+ );
+ messageContent = await getContentFromMessage(secondMsgHdr);
+ Assert.ok(
+ messageContent.includes(
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=436880"
+ )
+ );
+ },
+ async function copyMovedMessages() {
+ let msgs = [...gMoveFolder.msgDatabase.enumerateMessages()];
+ let firstMsgHdr = msgs[0];
+ let secondMsgHdr = msgs[1];
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ gMoveFolder,
+ [firstMsgHdr, secondMsgHdr],
+ gMoveFolder2,
+ false,
+ promiseCopyListener,
+ null,
+ false
+ );
+ let promiseMoveMsg = PromiseTestUtils.promiseFolderEvent(
+ gMoveFolder,
+ "DeleteOrMoveMsgCompleted"
+ );
+ await Promise.all([promiseCopyListener.promise, promiseMoveMsg]);
+ },
+ async function verifyFolders2() {
+ Assert.equal(folderCount(gMoveFolder2), 2);
+
+ let msgs = [...gMoveFolder2.msgDatabase.enumerateMessages()];
+ let firstMsgHdr = msgs[0];
+ let secondMsgHdr = msgs[1];
+ // Check that the messages have content
+ let messageContent = await getContentFromMessage(firstMsgHdr);
+ Assert.ok(
+ messageContent.includes("Some User <bugmail@example.org> changed")
+ );
+ messageContent = await getContentFromMessage(secondMsgHdr);
+ Assert.ok(
+ messageContent.includes(
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=436880"
+ )
+ );
+ },
+ function endTest() {
+ dump("Exiting mail tests\n");
+ gPOP3Pump = null;
+ },
+];
+
+function folderCount(folder) {
+ return [...folder.msgDatabase.enumerateMessages()].length;
+}
+
+function run_test() {
+ /* may not work in Linux */
+ // if ("@mozilla.org/gnome-gconf-service;1" in Cc)
+ // return;
+ /**/
+ // quarantine messages
+ Services.prefs.setBoolPref("mailnews.downloadToTempFile", true);
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ gMoveFolder = localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder");
+ gMoveFolder2 =
+ localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder2");
+
+ gTestArray.forEach(x => add_task(x));
+ run_next_test();
+}
+
+/**
+ * Get the full message content.
+ *
+ * @param aMsgHdr - nsIMsgDBHdr object whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ });
+}
diff --git a/comm/mailnews/search/test/unit/test_search.js b/comm/mailnews/search/test/unit/test_search.js
new file mode 100644
index 0000000000..98e8874fd3
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_search.js
@@ -0,0 +1,623 @@
+/* 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/. */
+
+/*
+ * Testing of general mail search features.
+ *
+ * This tests some search attributes not tested by other specific tests,
+ * e.g., test_searchTag.js or test_searchJunk.js
+ */
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var Is = Ci.nsMsgSearchOp.Is;
+var Contains = Ci.nsMsgSearchOp.Contains;
+var DoesntContain = Ci.nsMsgSearchOp.DoesntContain;
+var BeginsWith = Ci.nsMsgSearchOp.BeginsWith;
+var EndsWith = Ci.nsMsgSearchOp.EndsWith;
+var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled
+var IsAfter = Ci.nsMsgSearchOp.IsAfter;
+var IsHigherThan = Ci.nsMsgSearchOp.IsHigherThan;
+var IsLowerThan = Ci.nsMsgSearchOp.IsLowerThan;
+
+var OtherHeader = Ci.nsMsgSearchAttrib.OtherHeader;
+var From = Ci.nsMsgSearchAttrib.Sender;
+var Subject = Ci.nsMsgSearchAttrib.Subject;
+var Priority = Ci.nsMsgSearchAttrib.Priority;
+var SDate = Ci.nsMsgSearchAttrib.Date;
+
+var Tests = [
+ // test the To: header
+ {
+ testString: "PrimaryEmail1@test.invalid",
+ testAttribute: From,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: "PrimaryEmail1@test.invalid",
+ testAttribute: From,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ testString: "PrimaryEmail",
+ testAttribute: From,
+ op: BeginsWith,
+ count: 1,
+ },
+ {
+ testString: "invalid",
+ testAttribute: From,
+ op: BeginsWith,
+ count: 0,
+ },
+ {
+ testString: "invalid",
+ testAttribute: From,
+ op: EndsWith,
+ count: 1,
+ },
+ {
+ testString: "Primary",
+ testAttribute: From,
+ op: EndsWith,
+ count: 0,
+ },
+ {
+ testString: "QAContact",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ count: 1,
+ },
+ {
+ testString: "filters",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ count: 0,
+ },
+ {
+ testString: "mail.bugs",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ count: 1,
+ },
+ {
+ testString: "QAContact",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ count: 0,
+ },
+ {
+ testString: "QAcontact filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: "filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Is,
+ count: 0,
+ },
+ {
+ testString: "QAcontact filters@mail.bugs",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ testString: "QAcontact",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ testString: "filters",
+ testAttribute: OtherHeader,
+ op: Contains,
+ count: 1,
+ },
+ {
+ testString: "foobar",
+ testAttribute: OtherHeader,
+ op: Contains,
+ count: 0,
+ },
+ // test header with multiple occurrences
+ {
+ testString: "one value",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "second",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "third value for test purposes",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "multiline value that needs to be handled.",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "one value",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "second",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "third value for test purposes",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "multiline value that needs to be handled.",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "one",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "second",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "purposes",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "value",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "that needs to be",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "fifth",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "is the end my",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "the end",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "handled.",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "one value",
+ testAttribute: OtherHeader,
+ op: EndsWith,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "third",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "This is",
+ testAttribute: OtherHeader,
+ op: BeginsWith,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+
+ {
+ testString: "nothing",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header",
+ count: 0,
+ },
+ {
+ testString: "nothing",
+ testAttribute: OtherHeader,
+ op: DoesntContain,
+ customHeader: "X-Duplicated-Header",
+ count: 1,
+ },
+ {
+ testString: "this header tests DB string properties",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 1,
+ },
+ {
+ testString: "which can be handled",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 1,
+ },
+ {
+ testString: "differently than X-Duplicated-Header, so better test it",
+ testAttribute: OtherHeader,
+ op: Is,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 1,
+ },
+ {
+ testString: "this header tests DB string properties",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 0,
+ },
+ {
+ testString: "which can be handled",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 0,
+ },
+ {
+ testString: "differently than X-Duplicated-Header, so better test it",
+ testAttribute: OtherHeader,
+ op: Isnt,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 0,
+ },
+ {
+ testString: "than X-Duplicated-Header,",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 1,
+ },
+ {
+ testString: "than X-Duplicated-Header, so",
+ testAttribute: OtherHeader,
+ op: DoesntContain,
+ customHeader: "X-Duplicated-Header-DB",
+ count: 0,
+ },
+ // test accumulation of received header
+ {
+ // only in first received
+ testString: "caspiaco",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "Received",
+ count: 1,
+ },
+ {
+ // only in second
+ testString: "webapp01.sj.mozilla.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "received",
+ count: 1,
+ },
+ {
+ // in neither
+ testString: "not there",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "received",
+ count: 0,
+ },
+ {
+ // not on first line of received
+ testString: "m47LtAFJ007547",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "received",
+ count: 1,
+ },
+ // test multiple line arbitrary headers
+ {
+ // in the first line
+ testString: "SpamAssassin 3.2.3",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ {
+ // in the second line
+ testString: "host29.example.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ {
+ // spans two lines with space
+ testString: "on host29.example.com",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "X-Spam-Checker-Version",
+ count: 1,
+ },
+ // subject spanning several lines
+ {
+ // on the first line
+ testString: "A filter will",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ {
+ testString: "I do not exist",
+ testAttribute: Subject,
+ op: Contains,
+ count: 0,
+ },
+ {
+ // on the second line
+ testString: "this message",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ {
+ // spanning second and third line
+ testString: "over many",
+ testAttribute: Subject,
+ op: Contains,
+ count: 1,
+ },
+ // tests of custom headers db values
+ {
+ testString: "a one line header",
+ dbHeader: "oneliner",
+ },
+ {
+ testString: "a two line header",
+ dbHeader: "twoliner",
+ },
+ {
+ testString: "a three line header with lotsa space and tabs",
+ dbHeader: "threeliner",
+ },
+ {
+ testString: "I have no space",
+ dbHeader: "nospace",
+ },
+ {
+ testString: "too much space",
+ dbHeader: "withspace",
+ },
+ // tests of custom db headers in a search
+ {
+ testString: "one line",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "oneliner",
+ count: 1,
+ },
+ {
+ testString: "two line header",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "twoliner",
+ count: 1,
+ },
+ {
+ testString: "three line header with lotsa",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "threeliner",
+ count: 1,
+ },
+ {
+ testString: "I have no space",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "nospace",
+ count: 1,
+ },
+ {
+ testString: "too much space",
+ testAttribute: OtherHeader,
+ op: Contains,
+ customHeader: "withspace",
+ count: 1,
+ },
+ // test for priority
+ {
+ testString: Ci.nsMsgPriority.lowest,
+ testAttribute: Priority,
+ op: IsHigherThan,
+ count: 1,
+ },
+ {
+ testString: Ci.nsMsgPriority.low,
+ testAttribute: Priority,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: Ci.nsMsgPriority.normal,
+ testAttribute: Priority,
+ op: IsLowerThan,
+ count: 1,
+ },
+ {
+ testString: Ci.nsMsgPriority.lowest,
+ testAttribute: Priority,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ testString: Ci.nsMsgPriority.low,
+ testAttribute: Priority,
+ op: Isnt,
+ count: 0,
+ },
+
+ // tests of Date header
+ // The internal value of date in the search is PRTime (nanoseconds since Epoch).
+ // Date().getTime() returns milliseconds since Epoch.
+ // The dates used here are tailored for the ../../../data/bugmail12 message.
+ {
+ testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: Is,
+ count: 1,
+ },
+ {
+ testString: new Date("Thu, 8 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: IsBefore,
+ count: 1,
+ },
+ {
+ testString: new Date("Tue, 6 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: IsAfter,
+ count: 1,
+ },
+ {
+ testString: new Date("Tue, 6 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ // check bug 248808
+ testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: IsBefore,
+ count: 0,
+ },
+ {
+ testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000,
+ testAttribute: SDate,
+ op: IsAfter,
+ count: 0,
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ testSearch();
+ },
+ };
+
+ // set value of headers we want parsed into the db
+ Services.prefs.setCharPref(
+ "mailnews.customDBHeaders",
+ "oneLiner twoLiner threeLiner noSpace withSpace X-Duplicated-Header-DB"
+ );
+ // Get a message into the local filestore. function testSearch() continues
+ // the testing after the copy.
+ var bugmail12 = do_get_file("../../../data/bugmail12");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ bugmail12,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+// process each test from queue, calls itself upon completion of each search
+function testSearch() {
+ var test = Tests.shift();
+ if (test && test.dbHeader) {
+ // test of a custom db header
+ dump("testing dbHeader " + test.dbHeader + "\n");
+ let customValue = mailTestUtils
+ .firstMsgHdr(localAccountUtils.inboxFolder)
+ .getStringProperty(test.dbHeader);
+ Assert.equal(customValue, test.testString);
+ do_timeout(0, testSearch);
+ } else if (test) {
+ dump("testing for string '" + test.testString + "'\n");
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.testString,
+ test.testAttribute,
+ test.op,
+ test.count,
+ testSearch,
+ null,
+ test.customHeader ? test.customHeader : "X-Bugzilla-Watch-Reason"
+ );
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchAddressInAb.js b/comm/mailnews/search/test/unit/test_searchAddressInAb.js
new file mode 100644
index 0000000000..a93f405047
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchAddressInAb.js
@@ -0,0 +1,337 @@
+/* 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/. */
+
+// Testing of to, cc, toorcc in addressbook search features added in bug 187768
+// Added testing of AllAddresses from bug 310359
+
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+// add address book setup
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var ABUri = kPABData.URI;
+
+var IsntInAB = Ci.nsMsgSearchOp.IsntInAB;
+var IsInAB = Ci.nsMsgSearchOp.IsInAB;
+var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry that is not enabled
+var Is = Ci.nsMsgSearchOp.Is;
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+
+var offlineMail = Ci.nsMsgSearchScope.offlineMail;
+var onlineMail = Ci.nsMsgSearchScope.onlineMail;
+var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter;
+var onlineMailFilter = Ci.nsMsgSearchScope.onlineMailFilter;
+var news = Ci.nsMsgSearchScope.news; // control entry that is not enabled
+
+var Sender = Ci.nsMsgSearchAttrib.Sender;
+var To = Ci.nsMsgSearchAttrib.To;
+var CCopy = Ci.nsMsgSearchAttrib.CC;
+var ToOrCC = Ci.nsMsgSearchAttrib.ToOrCC;
+var AllAddresses = Ci.nsMsgSearchAttrib.AllAddresses;
+var Keywords = Ci.nsMsgSearchAttrib.Keywords; // control entry that is not enabled
+
+/*
+ * The address available in the test address book is "PrimaryEmail1@test.invalid"
+ * Test emails may also include the address "invalid@example.com"
+ *
+ *
+ * Map of test email contents: (P is "Prim...", I is "inva.." address, N is none)
+ *
+ *
+ * Email From To CC BCC
+ * 1 P I I N
+ * 2 P P P N
+ * 3 I P I N
+ * 4 I I P N
+ * 5 P I P N
+ * 6 I I,P P,I N
+ * 7 I I I P
+ * 8 I P P N
+ *
+ */
+
+var Tests = [
+ {
+ value: ABUri,
+ attrib: Sender,
+ op: IsInAB,
+ count: 3,
+ },
+ {
+ value: ABUri,
+ attrib: To,
+ op: IsInAB,
+ count: 4,
+ },
+ {
+ value: ABUri,
+ attrib: ToOrCC,
+ op: IsInAB,
+ count: 6,
+ },
+ {
+ value: ABUri,
+ attrib: AllAddresses,
+ op: IsInAB,
+ count: 8,
+ },
+ {
+ value: ABUri,
+ attrib: CCopy,
+ op: IsInAB,
+ count: 5,
+ },
+ {
+ value: ABUri,
+ attrib: Sender,
+ op: IsntInAB,
+ count: 5,
+ },
+ {
+ value: ABUri,
+ attrib: To,
+ op: IsntInAB,
+ count: 5,
+ },
+ {
+ value: ABUri,
+ attrib: ToOrCC,
+ op: IsntInAB,
+ count: 6,
+ },
+ {
+ value: ABUri,
+ attrib: AllAddresses,
+ op: IsntInAB,
+ count: 7,
+ },
+ {
+ value: ABUri,
+ attrib: CCopy,
+ op: IsntInAB,
+ count: 4,
+ },
+ {
+ value: "PrimaryEmail1@test.invalid",
+ attrib: AllAddresses,
+ op: Is,
+ count: 8,
+ },
+ {
+ value: "PrimaryEmail1@test.invalid",
+ attrib: AllAddresses,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ value: "invalid@example.com",
+ attrib: AllAddresses,
+ op: Is,
+ count: 7,
+ },
+ {
+ value: "invalid@example.com",
+ attrib: AllAddresses,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ value: "PrimaryEmail1@test.invalid",
+ attrib: ToOrCC,
+ op: Is,
+ count: 6,
+ },
+ {
+ value: "PrimaryEmail1@test.invalid",
+ attrib: ToOrCC,
+ op: Isnt,
+ count: 2,
+ },
+ {
+ value: "invalid@example.com",
+ attrib: ToOrCC,
+ op: Is,
+ count: 6,
+ },
+ {
+ value: "invalid@example.com",
+ attrib: ToOrCC,
+ op: Isnt,
+ count: 2,
+ },
+];
+
+var Files = [
+ "../../../data/bugmail1",
+ "../../../data/bugmail2",
+ "../../../data/bugmail3",
+ "../../../data/bugmail4",
+ "../../../data/bugmail5",
+ "../../../data/bugmail6",
+ "../../../data/bugmail7",
+ "../../../data/bugmail8",
+];
+
+function run_test() {
+ // Setup local mail accounts.
+ localAccountUtils.loadLocalMailAccount();
+
+ loadABFile(
+ "../../../addrbook/test/unit/data/cardForEmail",
+ kPABData.fileName
+ );
+
+ // test that validity table terms are valid
+
+ // offline mail table
+ testValidityTable(offlineMail, IsInAB, Sender, true);
+ testValidityTable(offlineMail, IsInAB, To, true);
+ testValidityTable(offlineMail, IsInAB, ToOrCC, true);
+ testValidityTable(offlineMail, IsInAB, AllAddresses, true);
+ testValidityTable(offlineMail, IsInAB, CCopy, true);
+ testValidityTable(offlineMail, IsInAB, Keywords, false);
+ testValidityTable(offlineMail, IsntInAB, Sender, true);
+ testValidityTable(offlineMail, IsntInAB, To, true);
+ testValidityTable(offlineMail, IsntInAB, ToOrCC, true);
+ testValidityTable(offlineMail, IsntInAB, AllAddresses, true);
+ testValidityTable(offlineMail, IsntInAB, CCopy, true);
+ testValidityTable(offlineMail, IsntInAB, Keywords, false);
+ testValidityTable(offlineMail, IsBefore, Sender, false);
+ testValidityTable(offlineMail, IsBefore, To, false);
+ testValidityTable(offlineMail, IsBefore, ToOrCC, false);
+ testValidityTable(offlineMail, IsBefore, AllAddresses, false);
+ testValidityTable(offlineMail, IsBefore, CCopy, false);
+ testValidityTable(offlineMail, IsBefore, Keywords, false);
+ testValidityTable(offlineMail, Is, AllAddresses, true);
+ testValidityTable(offlineMail, Isnt, AllAddresses, true);
+
+ // offline mail filter table
+ testValidityTable(offlineMailFilter, IsInAB, Sender, true);
+ testValidityTable(offlineMailFilter, IsInAB, To, true);
+ testValidityTable(offlineMailFilter, IsInAB, ToOrCC, true);
+ testValidityTable(offlineMailFilter, IsInAB, AllAddresses, true);
+ testValidityTable(offlineMailFilter, IsInAB, CCopy, true);
+ testValidityTable(offlineMailFilter, IsInAB, Keywords, false);
+ testValidityTable(offlineMailFilter, IsntInAB, Sender, true);
+ testValidityTable(offlineMailFilter, IsntInAB, To, true);
+ testValidityTable(offlineMailFilter, IsntInAB, AllAddresses, true);
+ testValidityTable(offlineMailFilter, IsntInAB, ToOrCC, true);
+ testValidityTable(offlineMailFilter, IsntInAB, CCopy, true);
+ testValidityTable(offlineMailFilter, IsntInAB, Keywords, false);
+ testValidityTable(offlineMailFilter, IsBefore, Sender, false);
+ testValidityTable(offlineMailFilter, IsBefore, To, false);
+ testValidityTable(offlineMailFilter, IsBefore, ToOrCC, false);
+ testValidityTable(offlineMailFilter, IsBefore, AllAddresses, false);
+ testValidityTable(offlineMailFilter, IsBefore, CCopy, false);
+ testValidityTable(offlineMailFilter, IsBefore, Keywords, false);
+ testValidityTable(offlineMailFilter, Is, AllAddresses, true);
+ testValidityTable(offlineMailFilter, Isnt, AllAddresses, true);
+
+ // online mail
+ testValidityTable(onlineMail, IsInAB, Sender, false);
+ testValidityTable(onlineMail, IsInAB, To, false);
+ testValidityTable(onlineMail, IsInAB, ToOrCC, false);
+ testValidityTable(onlineMail, IsInAB, CCopy, false);
+ testValidityTable(onlineMail, IsInAB, Keywords, false);
+ testValidityTable(onlineMail, IsntInAB, Sender, false);
+ testValidityTable(onlineMail, IsntInAB, To, false);
+ testValidityTable(onlineMail, IsntInAB, ToOrCC, false);
+ testValidityTable(onlineMail, IsntInAB, CCopy, false);
+ testValidityTable(onlineMail, IsntInAB, Keywords, false);
+ testValidityTable(onlineMail, IsBefore, Sender, false);
+ testValidityTable(onlineMail, IsBefore, To, false);
+ testValidityTable(onlineMail, IsBefore, ToOrCC, false);
+ testValidityTable(onlineMail, IsBefore, CCopy, false);
+ testValidityTable(onlineMail, IsBefore, Keywords, false);
+
+ // online mail filter
+ testValidityTable(onlineMailFilter, IsInAB, Sender, true);
+ testValidityTable(onlineMailFilter, IsInAB, To, true);
+ testValidityTable(onlineMailFilter, IsInAB, ToOrCC, true);
+ testValidityTable(onlineMailFilter, IsInAB, CCopy, true);
+ testValidityTable(onlineMailFilter, IsInAB, Keywords, false);
+ testValidityTable(onlineMailFilter, IsntInAB, Sender, true);
+ testValidityTable(onlineMailFilter, IsntInAB, To, true);
+ testValidityTable(onlineMailFilter, IsntInAB, ToOrCC, true);
+ testValidityTable(onlineMailFilter, IsntInAB, CCopy, true);
+ testValidityTable(onlineMailFilter, IsntInAB, Keywords, false);
+ testValidityTable(onlineMailFilter, IsBefore, Sender, false);
+ testValidityTable(onlineMailFilter, IsBefore, To, false);
+ testValidityTable(onlineMailFilter, IsBefore, ToOrCC, false);
+ testValidityTable(onlineMailFilter, IsBefore, CCopy, false);
+ testValidityTable(onlineMailFilter, IsBefore, Keywords, false);
+
+ // news
+ testValidityTable(news, IsInAB, Sender, false);
+ testValidityTable(news, IsInAB, To, false);
+ testValidityTable(news, IsInAB, ToOrCC, false);
+ testValidityTable(news, IsInAB, CCopy, false);
+ testValidityTable(news, IsInAB, Keywords, false);
+ testValidityTable(news, IsntInAB, Sender, false);
+ testValidityTable(news, IsntInAB, To, false);
+ testValidityTable(news, IsntInAB, ToOrCC, false);
+ testValidityTable(news, IsntInAB, CCopy, false);
+ testValidityTable(news, IsntInAB, Keywords, false);
+ testValidityTable(news, IsBefore, Sender, false);
+ testValidityTable(news, IsBefore, To, false);
+ testValidityTable(news, IsBefore, ToOrCC, false);
+ testValidityTable(news, IsBefore, CCopy, false);
+ testValidityTable(news, IsBefore, Keywords, false);
+
+ // Get a message into the local filestore. function testAbSearch() continues the testing after the copy.
+ do_test_pending();
+ copyListener.OnStopCopy(null);
+ return true;
+}
+
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ var fileName = Files.shift();
+ if (fileName) {
+ var file = do_get_file(fileName);
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ } else {
+ testAbSearch();
+ }
+ },
+};
+
+// Runs at completion of copy
+
+// process each test from queue, calls itself upon completion of each search
+function testAbSearch() {
+ print("Test AbSearch");
+ var test = Tests.shift();
+ if (test) {
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.value,
+ test.attrib,
+ test.op,
+ test.count,
+ testAbSearch
+ );
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchBody.js b/comm/mailnews/search/test/unit/test_searchBody.js
new file mode 100644
index 0000000000..c2ec73c115
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchBody.js
@@ -0,0 +1,294 @@
+/* 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/. */
+
+/*
+ * This tests various body search criteria.
+ */
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var Is = Ci.nsMsgSearchOp.Is;
+var IsEmpty = Ci.nsMsgSearchOp.IsEmpty;
+var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty;
+var Contains = Ci.nsMsgSearchOp.Contains;
+var DoesntContain = Ci.nsMsgSearchOp.DoesntContain;
+var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled
+
+var offlineMail = Ci.nsMsgSearchScope.offlineMail;
+var onlineMail = Ci.nsMsgSearchScope.onlineMail;
+var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter;
+var news = Ci.nsMsgSearchScope.news; // control entry not enabled
+
+var Body = Ci.nsMsgSearchAttrib.Body;
+
+var Files = [
+ "../../../data/base64-1",
+ "../../../data/basic1",
+ "../../../data/multipart-base64-2",
+ "../../../data/bug132340",
+ "../../../data/bad-charset.eml",
+ "../../../data/HTML-with-split-tag1.eml",
+ "../../../data/HTML-with-split-tag2.eml",
+
+ // Base64 encoded bodies.
+ "../../../data/01-plaintext.eml",
+ "../../../data/02-plaintext+attachment.eml",
+ "../../../data/03-HTML.eml",
+ "../../../data/04-HTML+attachment.eml",
+ "../../../data/05-HTML+embedded-image.eml",
+ "../../../data/06-plaintext+HMTL.eml",
+ "../../../data/07-plaintext+(HTML+embedded-image).eml",
+ "../../../data/08-plaintext+HTML+attachment.eml",
+ "../../../data/09-(HTML+embedded-image)+attachment.eml",
+ "../../../data/10-plaintext+(HTML+embedded-image)+attachment.eml",
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets.
+ "../../../data/11-plaintext.eml",
+ "../../../data/12-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/13-HTML.eml",
+ "../../../data/14-HTML+attachment.eml",
+ "../../../data/15-HTML+embedded-image.eml",
+ "../../../data/16-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/17-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/18-plaintext+HTML+attachment.eml",
+ "../../../data/19-(HTML+embedded-image)+attachment.eml",
+ "../../../data/20-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets, all encoded with quoted printable.
+ "../../../data/21-plaintext.eml",
+ "../../../data/22-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/23-HTML.eml",
+ "../../../data/24-HTML+attachment.eml",
+ "../../../data/25-HTML+embedded-image.eml",
+ "../../../data/26-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/27-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/28-plaintext+HTML+attachment.eml",
+ "../../../data/29-(HTML+embedded-image)+attachment.eml",
+ "../../../data/30-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+
+ // Messages with message attachments, Content-Type: message/rfc822.
+ "../../../data/multipart-message-1.eml", // plaintext, has "bodyOfAttachedMessagePlain"
+ "../../../data/multipart-message-2.eml", // plaintext, base64, non-ASCII, has "bodyOfAttachedMessagePläin"
+ "../../../data/multipart-message-3.eml", // plaintext+HTML, non-ASCII in plaintext, has "bodyOfAttachedMessagePläin"
+ "../../../data/multipart-message-4.eml", // plaintext+HTML, non-ASCII in HTML, has "bodyOfAttachedMessägeHTML"
+
+ // Message using ISO-2022-JP and CTE: quoted-printable.
+ "../../../data/iso-2022-jp-qp.eml", // plaintext, has 日本 (Japan), we shouldn't find =1B$BF|K.
+
+ // Message using ISO-2022-JP and 7bit, but containing something that looks like quoted-printable.
+ // (bug 314637).
+ "../../../data/iso-2022-jp-not-qp.eml", // plaintext, has ç¾æ³ which contains =67.
+];
+var Tests = [
+ /* Translate Base64 messages */
+ // "World!" is contained in three messages, but in bug132340 it's not in a text
+ // part and should not be found.
+ { value: "World!", op: Contains, count: 2 },
+ /* Don't match the base64 text */
+ { value: "DQp", op: Contains, count: 0 },
+ /* Nested multipart/mixed, don't match */
+ { value: "PGh", op: Contains, count: 0 },
+ /* An encoded base-64 text/plain match */
+ { value: "base 64 text", op: Contains, count: 1 },
+
+ // From the message with the bad charset.
+ { value: "Mätterhorn", op: Contains, count: 1 },
+ { value: "Matterhörn", op: Contains, count: 1 },
+
+ // Comprehensive test of various MIME structures, messages 01 to 10.
+ // Messages 01 to 10 contain "huhu" once.
+ { value: "huhu", op: Contains, count: 10 },
+
+ // Messages 06, 07, 08, 10 contain "hihi" in the plaintext part.
+ { value: "hihi", op: Contains, count: 4 },
+
+ // The base64 of embedded images and attachments contains "iVBORw" and we don't
+ // want to find that.
+ { value: "iVBORw", op: Contains, count: 0 },
+
+ // The base64 of attachments contains "wMA005J0z" and we don't want to find that.
+ { value: "wMA005J0z", op: Contains, count: 0 },
+
+ // The base64 of the plaintext and HTML parts contains "U2VhcmNoIGZ"
+ // and we don't want to find that.
+ { value: "U2VhcmNoIGZ", op: Contains, count: 0 },
+
+ // Messages 11 and 13 to 20 contain "hühü" once.
+ { value: "hühü", op: Contains, count: 9 },
+ // Message 12 contains ΚαλησπέÏα (good evening in Greek).
+ { value: "ΚαλησπέÏα", op: Contains, count: 1 },
+
+ // Messages 16, 17, 18, 20 contain "hïhï" in the plaintext part.
+ { value: "hïhï", op: Contains, count: 4 },
+
+ // Messages 21 and 23 to 30 contain "höhö" once.
+ { value: "höhö", op: Contains, count: 9 },
+ // Message 22 contains ΚαλημέÏα (good morning in Greek).
+ { value: "ΚαλημέÏα", op: Contains, count: 1 },
+
+ // Messages 21, 23 and 24 contain "softbreak" broken by a soft line break.
+ { value: "softbreak", op: Contains, count: 3 },
+
+ // Messages 16, 17, 18, 20 contain "hähä" in the plaintext part.
+ { value: "hähä", op: Contains, count: 4 },
+
+ // The four messages with message/rfc822 attachment contain "bodyOfAttachedMessagePlain"
+ // or "bodyOfAttachedMessagePläin" in the plaintext part and "bodyOfAttachedMessageHTML"
+ // or "bodyOfAttachedMessägeHTML" in the HTML part.
+ { value: "bodyOfAttachedMessagePlain", op: Contains, count: 2 },
+ { value: "bodyOfAttachedMessagePläin", op: Contains, count: 2 },
+ { value: "bodyOfAttachedMessageHTML", op: Contains, count: 1 },
+ { value: "bodyOfAttachedMessägeHTML", op: Contains, count: 1 },
+
+ // Test that we don't find anything in HTML tags.
+ { value: "ShouldNotFindThis", op: Contains, count: 0 },
+ { value: "ShouldntFindThisEither", op: Contains, count: 0 },
+ { value: "ShouldntFindHref", op: Contains, count: 0 },
+ { value: "ShouldNotFindAcrossLines", op: Contains, count: 0 },
+ { value: "ShouldFindThisAgain", op: Contains, count: 2 },
+ { value: "ShouldFind AcrossLines", op: Contains, count: 2 },
+
+ // Test for ISO-2022-JP and CTE: quoted-printable, also 7bit looking like quoted-printable.
+ { value: "日本", op: Contains, count: 1 },
+ { value: "=1B$BF|K", op: Contains, count: 0 },
+ { value: "ç¾æ³", op: Contains, count: 1 },
+];
+
+function fixFile(file) {
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sstream.init(fstream);
+
+ var str = sstream.read(4096);
+ if (str.startsWith("From ")) {
+ sstream.close();
+ fstream.close();
+ return file;
+ }
+ var data = "From - Tue Oct 02 00:26:47 2007\r\n";
+ do {
+ data += str;
+ str = sstream.read(4096);
+ } while (str.length > 0);
+
+ sstream.close();
+ fstream.close();
+
+ let targetFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ targetFile.initWithFile(do_get_profile());
+ targetFile.append(file.leafName);
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(targetFile, -1, -1, 0);
+ ostream.write(data, data.length);
+ ostream.close();
+ return targetFile;
+}
+
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ var fileName = Files.shift();
+ if (fileName) {
+ var file = fixFile(do_get_file(fileName));
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ } else {
+ testBodySearch();
+ }
+ },
+};
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // test that validity table terms are valid
+
+ // offline mail table
+ testValidityTable(offlineMail, Contains, Body, true);
+ testValidityTable(offlineMail, DoesntContain, Body, true);
+ testValidityTable(offlineMail, Is, Body, true);
+ testValidityTable(offlineMail, Isnt, Body, true);
+ testValidityTable(offlineMail, IsEmpty, Body, false);
+ testValidityTable(offlineMail, IsntEmpty, Body, false);
+ testValidityTable(offlineMail, IsBefore, Body, false);
+
+ // offline mail filter table
+ testValidityTable(offlineMailFilter, Contains, Body, true);
+ testValidityTable(offlineMailFilter, DoesntContain, Body, true);
+ testValidityTable(offlineMailFilter, Is, Body, true);
+ testValidityTable(offlineMailFilter, Isnt, Body, true);
+ testValidityTable(offlineMailFilter, IsEmpty, Body, false);
+ testValidityTable(offlineMailFilter, IsntEmpty, Body, false);
+ testValidityTable(offlineMailFilter, IsBefore, Body, false);
+
+ // online mail
+ testValidityTable(onlineMail, Contains, Body, true);
+ testValidityTable(onlineMail, DoesntContain, Body, true);
+ testValidityTable(onlineMail, Is, Body, false);
+ testValidityTable(onlineMail, Isnt, Body, false);
+ testValidityTable(onlineMail, IsEmpty, Body, false);
+ testValidityTable(onlineMail, IsntEmpty, Body, false);
+ testValidityTable(onlineMail, IsBefore, Body, false);
+
+ // online mail filter
+ /* testValidityTable(onlineMailFilter, Contains, Body, true);
+ testValidityTable(onlineMailFilter, DoesntContain, Body, true);
+ testValidityTable(onlineMailFilter, Is, Body, false);
+ testValidityTable(onlineMailFilter, Isnt, Body, false);
+ testValidityTable(onlineMailFilter, IsEmpty, Body, false);
+ testValidityTable(onlineMailFilter, IsntEmpty, Body, false);
+ testValidityTable(onlineMailFilter, IsBefore, Body, false);*/
+
+ // News does not support body tests
+ testValidityTable(news, Contains, Body, false);
+ testValidityTable(news, DoesntContain, Body, false);
+ testValidityTable(news, Is, Body, false);
+ testValidityTable(news, Isnt, Body, false);
+ testValidityTable(news, IsEmpty, Body, false);
+ testValidityTable(news, IsntEmpty, Body, false);
+ testValidityTable(news, IsBefore, Body, false);
+
+ do_test_pending();
+ copyListener.OnStopCopy(null);
+}
+
+// process each test from queue, calls itself upon completion of each search
+function testBodySearch() {
+ var test = Tests.shift();
+ if (test) {
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.value,
+ Body,
+ test.op,
+ test.count,
+ testBodySearch
+ );
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchBoolean.js b/comm/mailnews/search/test/unit/test_searchBoolean.js
new file mode 100644
index 0000000000..132acad4a3
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchBoolean.js
@@ -0,0 +1,239 @@
+/* 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/. */
+
+/*
+ * Demonstrates and tests the use of grouped boolean expressions in search terms
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gSearchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+].createInstance(Ci.nsIMsgSearchSession);
+
+var gHdr; // the message header for the one mailbox message
+
+var Tests = [
+ {
+ A: false,
+ B: false,
+ C: false,
+ D: false,
+ matches: false,
+ },
+ {
+ A: false,
+ B: false,
+ C: false,
+ D: true,
+ matches: false,
+ },
+ {
+ A: false,
+ B: false,
+ C: true,
+ D: false,
+ matches: false,
+ },
+ {
+ A: false,
+ B: false,
+ C: true,
+ D: true,
+ matches: false,
+ },
+ {
+ A: false,
+ B: true,
+ C: false,
+ D: false,
+ matches: false,
+ },
+ {
+ A: false,
+ B: true,
+ C: false,
+ D: true,
+ matches: true,
+ },
+ {
+ A: false,
+ B: true,
+ C: true,
+ D: false,
+ matches: true,
+ },
+ {
+ A: false,
+ B: true,
+ C: true,
+ D: true,
+ matches: true,
+ },
+ {
+ A: true,
+ B: false,
+ C: false,
+ D: false,
+ matches: false,
+ },
+ {
+ A: true,
+ B: false,
+ C: false,
+ D: true,
+ matches: true,
+ },
+ {
+ A: true,
+ B: false,
+ C: true,
+ D: false,
+ matches: true,
+ },
+ {
+ A: true,
+ B: false,
+ C: true,
+ D: true,
+ matches: true,
+ },
+ {
+ A: true,
+ B: true,
+ C: false,
+ D: false,
+ matches: false,
+ },
+ {
+ A: true,
+ B: true,
+ C: false,
+ D: true,
+ matches: true,
+ },
+ {
+ A: true,
+ B: true,
+ C: true,
+ D: false,
+ matches: true,
+ },
+ {
+ A: true,
+ B: true,
+ C: true,
+ D: true,
+ matches: true,
+ },
+];
+
+var gHitCount = 0;
+var searchListener = {
+ onSearchHit(dbHdr, folder) {
+ gHitCount++;
+ },
+ onSearchDone(status) {
+ testSearch();
+ },
+ onNewSearch() {
+ gHitCount = 0;
+ },
+};
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ /*
+ * I want to create and test a search term that uses this expression:
+ * (A || B ) && (C || D)
+ *
+ * The logical expressions A, B, C, and D will be represented by header
+ * string properties with values of T (true) or F (false).
+ *
+ */
+
+ // add a search term HdrProperty (some string) Is "T", with grouping
+ function addSearchTerm(aHdrProperty, aBeginGrouping, aEndGrouping, aBoolAnd) {
+ let searchTerm = gSearchSession.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.HdrProperty;
+
+ let value = searchTerm.value;
+ // This is tricky - value.attrib must be set before actual values
+ value.attrib = Ci.nsMsgSearchAttrib.HdrProperty;
+ value.str = "T";
+ searchTerm.value = value;
+
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ searchTerm.booleanAnd = aBoolAnd;
+ searchTerm.beginsGrouping = aBeginGrouping;
+ searchTerm.endsGrouping = aEndGrouping;
+ searchTerm.hdrProperty = aHdrProperty;
+ gSearchSession.appendTerm(searchTerm);
+ }
+
+ gSearchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ localAccountUtils.inboxFolder
+ );
+ gSearchSession.registerListener(searchListener);
+ // I tried using capital "A" but something makes it lower case internally, so it failed
+ addSearchTerm("a", true, false, true); // "(A"
+ addSearchTerm("b", false, true, false); // " || B)"
+ addSearchTerm("c", true, false, true); // " && (C"
+ addSearchTerm("d", false, true, false); // " || D)"
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ testSearch();
+ },
+ };
+
+ // Get a message into the local filestore. function testSearch() continues
+ // the testing after the copy.
+ var bugmail1 = do_get_file("../../../data/bugmail1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+var gTest = null;
+// process each test from queue, calls itself upon completion of each search
+function testSearch() {
+ // tests the previous search
+ if (gTest) {
+ Assert.equal(gHitCount, gTest.matches ? 1 : 0);
+ }
+ gTest = Tests.shift();
+ if (gTest) {
+ gHdr.setStringProperty("a", gTest.A ? "T" : "F");
+ gHdr.setStringProperty("b", gTest.B ? "T" : "F");
+ gHdr.setStringProperty("c", gTest.C ? "T" : "F");
+ gHdr.setStringProperty("d", gTest.D ? "T" : "F");
+ try {
+ gSearchSession.search(null);
+ } catch (e) {
+ dump(e);
+ }
+ } else {
+ gSearchSession.unregisterListener(searchListener);
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchChaining.js b/comm/mailnews/search/test/unit/test_searchChaining.js
new file mode 100644
index 0000000000..4bdbebab57
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchChaining.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+// Test of chaining of search scopes in a search session. In particular,
+// Bug 541969 made us not search an imap folder if the search scope before it
+// there was an empty local folder.
+
+// main test
+
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+async function setupFolder() {
+ // add a single message to the imap inbox.
+ let messages = [];
+ let messageGenerator = new MessageGenerator();
+ messages = messages.concat(messageGenerator.makeMessage());
+ let synthMessage = messages[0];
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(synthMessage.toMessageString())
+ );
+ let message = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []);
+ IMAPPump.mailbox.addMessage(message);
+
+ // update folder to download header.
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+}
+
+async function searchTest() {
+ // Get the IMAP inbox...
+ var emptyLocal1 =
+ localAccountUtils.rootFolder.createLocalSubfolder("empty 1");
+
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+
+ let searchTerm = searchSession.createTerm();
+ searchTerm.matchAll = true;
+ searchSession.appendTerm(searchTerm);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, emptyLocal1);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.onlineMail, IMAPPump.inbox);
+ let listener = new PromiseTestUtils.PromiseSearchNotify(
+ searchSession,
+ searchListener
+ );
+ searchSession.search(null);
+ await listener.promise;
+
+ // After the search completes, there still seem to be active URLs, so we
+ // have to wait before we are done and clear.
+ await PromiseTestUtils.promiseDelay(1000);
+}
+
+// nsIMsgSearchNotify implementation
+var searchListener = {
+ numTotalMessages: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSearchNotify"]),
+ onNewSearch() {
+ this.numTotalMessages = 0;
+ },
+ onSearchHit(dbHdr, folder) {
+ this.numTotalMessages++;
+ },
+ onSearchDone(status) {
+ Assert.equal(this.numTotalMessages, 1);
+ return true;
+ },
+};
+
+var tests = [setupIMAPPump, setupFolder, searchTest, teardownIMAPPump];
+
+function run_test() {
+ tests.forEach(x => add_task(x));
+ run_next_test();
+}
diff --git a/comm/mailnews/search/test/unit/test_searchCustomTerm.js b/comm/mailnews/search/test/unit/test_searchCustomTerm.js
new file mode 100644
index 0000000000..baea69767b
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchCustomTerm.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+/*
+ * Testing of custom search features.
+ *
+ */
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kCustomId = "xpcomtest@mozilla.org#test";
+var gHdr;
+
+var Tests = [
+ {
+ setValue: "iamgood",
+ testValue: "iamnotgood",
+ op: Ci.nsMsgSearchOp.Is,
+ count: 0,
+ },
+ {
+ setValue: "iamgood",
+ testValue: "iamgood",
+ op: Ci.nsMsgSearchOp.Is,
+ count: 1,
+ },
+];
+
+// nsIMsgSearchCustomTerm object
+var customTerm = {
+ id: kCustomId,
+ name: "term name",
+ getEnabled(scope, op) {
+ return (
+ scope == Ci.nsMsgSearchScope.offlineMail && op == Ci.nsMsgSearchOp.Is
+ );
+ },
+ getAvailable(scope, op) {
+ return (
+ scope == Ci.nsMsgSearchScope.offlineMail && op == Ci.nsMsgSearchOp.Is
+ );
+ },
+ getAvailableOperators(scope) {
+ return [Ci.nsMsgSearchOp.Is];
+ },
+ match(msgHdr, searchValue, searchOp) {
+ switch (searchOp) {
+ case Ci.nsMsgSearchOp.Is:
+ if (msgHdr.getStringProperty("theTestProperty") == searchValue) {
+ return true;
+ }
+ }
+ return false;
+ },
+};
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ MailServices.filters.addCustomTerm(customTerm);
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ doTest();
+ },
+ };
+
+ // Get a message into the local filestore.
+ // function testSearch() continues the testing after the copy.
+ let bugmail1 = do_get_file("../../../data/bugmail1");
+ do_test_pending();
+
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function doTest() {
+ let test = Tests.shift();
+ if (test) {
+ gHdr.setStringProperty("theTestProperty", test.setValue);
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.testValue,
+ Ci.nsMsgSearchAttrib.Custom,
+ test.op,
+ test.count,
+ doTest,
+ kCustomId
+ );
+ } else {
+ gHdr = null;
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchJunk.js b/comm/mailnews/search/test/unit/test_searchJunk.js
new file mode 100644
index 0000000000..fdffd96165
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchJunk.js
@@ -0,0 +1,322 @@
+/* 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/. */
+
+// Testing of search by junk percent and junk score origin
+
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var IsGreaterThan = Ci.nsMsgSearchOp.IsGreaterThan;
+var IsLessThan = Ci.nsMsgSearchOp.IsLessThan;
+var Is = Ci.nsMsgSearchOp.Is;
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var IsEmpty = Ci.nsMsgSearchOp.IsEmpty;
+var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty;
+
+var offlineMail = Ci.nsMsgSearchScope.offlineMail;
+
+var JunkScoreOrigin = Ci.nsMsgSearchAttrib.JunkScoreOrigin;
+var JunkPercent = Ci.nsMsgSearchAttrib.JunkPercent;
+var JunkStatus = Ci.nsMsgSearchAttrib.JunkStatus;
+
+var fileName = "../../../data/bugmail1";
+
+/*
+ * The search for junkpercent is defined as the effective value,
+ * while the "junkpercent" database term is always the result
+ * from the bayes filter. This is optimized to make views that
+ * rely on junk percent search work with the best value for junk
+ * percent, while allowing junk percent from the bayes filter
+ * to be saved for analysis.
+ *
+ * This means that the search for "junk percent" only uses the
+ * database junkpercent value if junkscoreorigin is "plugin".
+ * Otherwise, it uses junkstatus (if set) or defaults to 0
+ * (not junk) if the message is unclassified.
+ */
+
+var Tests = [
+ {
+ // test empty junk status
+ junkScore: false,
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsEmpty,
+ count: 1,
+ },
+ {
+ junkScore: false,
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsntEmpty,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsntEmpty,
+ count: 1,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ junkScore: "100",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsntEmpty,
+ count: 1,
+ },
+ {
+ junkScore: "100",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkStatus,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ // Use junkpercent from database
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkPercent,
+ op: IsGreaterThan,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkPercent,
+ op: IsLessThan,
+ count: 1,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 90,
+ attrib: JunkPercent,
+ op: Is,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "90",
+ testValue: 10,
+ attrib: JunkPercent,
+ op: IsGreaterThan,
+ count: 1,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "90",
+ testValue: 10,
+ attrib: JunkPercent,
+ op: IsLessThan,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "10",
+ testValue: 10,
+ attrib: JunkPercent,
+ op: Is,
+ count: 1,
+ },
+ {
+ // values set by user, use junkscore not junkpercent
+ junkScore: "0",
+ junkScoreOrigin: "user",
+ junkPercent: "90",
+ testValue: 50,
+ attrib: JunkPercent,
+ op: IsGreaterThan,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "user",
+ junkPercent: "90",
+ testValue: 50,
+ attrib: JunkPercent,
+ op: IsLessThan,
+ count: 1,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "user",
+ junkPercent: "90",
+ testValue: 50,
+ attrib: JunkPercent,
+ op: Is,
+ count: 0,
+ },
+ {
+ junkScore: "100",
+ junkScoreOrigin: "user",
+ junkPercent: "10",
+ testValue: 50,
+ attrib: JunkPercent,
+ op: IsGreaterThan,
+ count: 1,
+ },
+ {
+ junkScore: "100",
+ junkScoreOrigin: "user",
+ junkPercent: "10",
+ testValue: 50,
+ attrib: JunkPercent,
+ op: IsLessThan,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "user",
+ junkPercent: "90",
+ testValue: 0,
+ attrib: JunkPercent,
+ op: Is,
+ count: 1,
+ },
+ {
+ // default to 0 when nothing set
+ junkScore: "",
+ junkScoreOrigin: "",
+ junkPercent: "",
+ testValue: 0,
+ attrib: JunkPercent,
+ op: Is,
+ count: 1,
+ },
+ {
+ // junkscoreorigin search tests
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "50",
+ testValue: "plugin",
+ attrib: JunkScoreOrigin,
+ op: Is,
+ count: 1,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "plugin",
+ junkPercent: "50",
+ testValue: "plugin",
+ attrib: JunkScoreOrigin,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "filter",
+ junkPercent: "50",
+ testValue: "plugin",
+ attrib: JunkScoreOrigin,
+ op: Is,
+ count: 0,
+ },
+ {
+ junkScore: "0",
+ junkScoreOrigin: "filter",
+ junkPercent: "50",
+ testValue: "plugin",
+ attrib: JunkScoreOrigin,
+ op: Isnt,
+ count: 1,
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // test that validity table terms are valid
+
+ // offline mail table
+ testValidityTable(offlineMail, Is, JunkPercent, true);
+ testValidityTable(offlineMail, Isnt, JunkPercent, false);
+ testValidityTable(offlineMail, IsGreaterThan, JunkPercent, true);
+ testValidityTable(offlineMail, IsLessThan, JunkPercent, true);
+
+ testValidityTable(offlineMail, Is, JunkScoreOrigin, true);
+ testValidityTable(offlineMail, Isnt, JunkScoreOrigin, true);
+ testValidityTable(offlineMail, IsGreaterThan, JunkScoreOrigin, false);
+ testValidityTable(offlineMail, IsLessThan, JunkScoreOrigin, false);
+
+ // Get a message into the local filestore. function testJunkSearch() continues the testing after the copy.
+ do_test_pending();
+ var file = do_get_file(fileName);
+ MailServices.copy.copyFileMessage(
+ file,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ return true;
+}
+
+var hdr;
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ testJunkSearch();
+ },
+};
+
+// Runs at completion of each copy
+// process each test from queue, calls itself upon completion of each search
+function testJunkSearch() {
+ var test = Tests.shift();
+ if (test) {
+ if (test.junkScore) {
+ hdr.setStringProperty("junkpercent", test.junkPercent);
+ hdr.setStringProperty("junkscoreorigin", test.junkScoreOrigin);
+ hdr.setStringProperty("junkscore", test.junkScore);
+ }
+
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.testValue,
+ test.attrib,
+ test.op,
+ test.count,
+ testJunkSearch
+ );
+ } else {
+ hdr = null;
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js b/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js
new file mode 100644
index 0000000000..9de9b7eb0e
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+// tests that localization strings added in bug 484147 are defined in preferences
+
+var gValidityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+].getService(Ci.nsIMsgSearchValidityManager);
+
+var gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search-attributes.properties"
+);
+
+// The following table of valid table scopes matches the allowable table
+// scopes in nsMsgSearchValidityManager::GetTable
+var kValidScopes = [
+ Ci.nsMsgSearchScope.offlineMail,
+ Ci.nsMsgSearchScope.offlineMailFilter,
+ Ci.nsMsgSearchScope.onlineMail,
+ Ci.nsMsgSearchScope.onlineMailFilter,
+ Ci.nsMsgSearchScope.news,
+ Ci.nsMsgSearchScope.newsFilter,
+ Ci.nsMsgSearchScope.localNews,
+ Ci.nsMsgSearchScope.LDAP,
+ Ci.nsMsgSearchScope.LDAPAnd,
+ Ci.nsMsgSearchScope.LocalAB,
+ Ci.nsMsgSearchScope.LocalABAnd,
+];
+
+function run_test() {
+ for (var index = 0; index < kValidScopes.length; ++index) {
+ let scope = kValidScopes[index];
+ let table = gValidityManager.getTable(scope);
+ let attributes = table.getAvailableAttributes();
+ let attribute;
+ while ((attribute = attributes.pop()) && attribute) {
+ let property = gValidityManager.getAttributeProperty(attribute);
+ let valid = false;
+ let localizedString;
+ try {
+ localizedString = gStringBundle.GetStringFromName(property);
+ valid = true;
+ } catch (e) {
+ dump("\n" + e);
+ }
+ valid = valid && localizedString && localizedString.length > 0;
+ if (!valid) {
+ dump(
+ "\nNo valid property for scope = " +
+ scope +
+ " attribute = " +
+ attribute +
+ " property = " +
+ property
+ );
+ }
+ Assert.ok(valid);
+ }
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchTag.js b/comm/mailnews/search/test/unit/test_searchTag.js
new file mode 100644
index 0000000000..321c22a6cf
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchTag.js
@@ -0,0 +1,490 @@
+/* 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/. */
+
+/*
+ * Testing of tag search features.
+ *
+ * Specifically tests changes implemented in bug 217034
+ * Does not do comprehensive testing.
+ *
+ */
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var Is = Ci.nsMsgSearchOp.Is;
+var IsEmpty = Ci.nsMsgSearchOp.IsEmpty;
+var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty;
+var Contains = Ci.nsMsgSearchOp.Contains;
+var DoesntContain = Ci.nsMsgSearchOp.DoesntContain;
+var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled
+
+var offlineMail = Ci.nsMsgSearchScope.offlineMail;
+var onlineMail = Ci.nsMsgSearchScope.onlineMail;
+var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter;
+var onlineMailFilter = Ci.nsMsgSearchScope.onlineMailFilter;
+var news = Ci.nsMsgSearchScope.news; // control entry not enabled
+
+var Keywords = Ci.nsMsgSearchAttrib.Keywords;
+
+// test tags
+var Tag1 = "istag";
+var Tag2 = "notistag";
+var Tag3 = "istagnot";
+var Tag4 = "istagtoo";
+var Tag1Tag4 = Tag1 + " " + Tag4;
+var Tag1Tag3 = Tag1 + " " + Tag3;
+var Tag1Tag1 = Tag1 + " " + Tag1;
+
+var Tests = [
+ // Message has a single valid tag
+ // test the valid tag
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: Is,
+ count: 1,
+ },
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: Contains,
+ count: 1,
+ },
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: DoesntContain,
+ count: 0,
+ },
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1,
+ testTag: Tag1,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // test an invalid tag, should act like empty
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: Contains,
+ count: 0,
+ },
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: DoesntContain,
+ count: 1,
+ },
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: IsEmpty,
+ count: 1,
+ },
+ {
+ msgTag: Tag2,
+ testTag: Tag1,
+ op: IsntEmpty,
+ count: 0,
+ },
+ // Message has two valid tags
+ // test first tag
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: Contains,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: DoesntContain,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag1,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // test second tag
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: Contains,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: DoesntContain,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag4,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // test tag not in message
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: Contains,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: DoesntContain,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag4,
+ testTag: Tag2,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // empty message
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: Contains,
+ count: 0,
+ },
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: DoesntContain,
+ count: 1,
+ },
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: IsEmpty,
+ count: 1,
+ },
+ {
+ msgTag: "",
+ testTag: Tag2,
+ op: IsntEmpty,
+ count: 0,
+ },
+ // message with two tags, only one is valid
+ // test with the single valid tag
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: Is,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: Contains,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: DoesntContain,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag1,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // test with a tag not in the message
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: Is,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: Isnt,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: Contains,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: DoesntContain,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag3,
+ testTag: Tag2,
+ op: IsntEmpty,
+ count: 1,
+ },
+ // Message has a duplicated tag
+ // test the tag
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: Is,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: Isnt,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: Contains,
+ count: 1,
+ },
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: DoesntContain,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: IsEmpty,
+ count: 0,
+ },
+ {
+ msgTag: Tag1Tag1,
+ testTag: Tag1,
+ op: IsntEmpty,
+ count: 1,
+ },
+];
+
+var hdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // test that validity table terms are valid
+
+ // offline mail table
+ testValidityTable(offlineMail, Contains, Keywords, true);
+ testValidityTable(offlineMail, DoesntContain, Keywords, true);
+ testValidityTable(offlineMail, Is, Keywords, true);
+ testValidityTable(offlineMail, Isnt, Keywords, true);
+ testValidityTable(offlineMail, IsEmpty, Keywords, true);
+ testValidityTable(offlineMail, IsntEmpty, Keywords, true);
+ testValidityTable(offlineMail, IsBefore, Keywords, false);
+
+ // offline mail filter table
+ testValidityTable(offlineMailFilter, Contains, Keywords, true);
+ testValidityTable(offlineMailFilter, DoesntContain, Keywords, true);
+ testValidityTable(offlineMailFilter, Is, Keywords, true);
+ testValidityTable(offlineMailFilter, Isnt, Keywords, true);
+ testValidityTable(offlineMailFilter, IsEmpty, Keywords, true);
+ testValidityTable(offlineMailFilter, IsntEmpty, Keywords, true);
+ testValidityTable(offlineMailFilter, IsBefore, Keywords, false);
+
+ // online mail
+ testValidityTable(onlineMail, Contains, Keywords, true);
+ testValidityTable(onlineMail, DoesntContain, Keywords, true);
+ testValidityTable(onlineMail, Is, Keywords, false);
+ testValidityTable(onlineMail, Isnt, Keywords, false);
+ testValidityTable(onlineMail, IsEmpty, Keywords, false);
+ testValidityTable(onlineMail, IsntEmpty, Keywords, false);
+ testValidityTable(onlineMail, IsBefore, Keywords, false);
+
+ // online mail filter
+ testValidityTable(onlineMailFilter, Contains, Keywords, true);
+ testValidityTable(onlineMailFilter, DoesntContain, Keywords, true);
+ testValidityTable(onlineMailFilter, Is, Keywords, true);
+ testValidityTable(onlineMailFilter, Isnt, Keywords, true);
+ testValidityTable(onlineMailFilter, IsEmpty, Keywords, true);
+ testValidityTable(onlineMailFilter, IsntEmpty, Keywords, true);
+ testValidityTable(onlineMailFilter, IsBefore, Keywords, false);
+
+ // news
+ testValidityTable(news, Contains, Keywords, false);
+ testValidityTable(news, DoesntContain, Keywords, false);
+ testValidityTable(news, Is, Keywords, false);
+ testValidityTable(news, Isnt, Keywords, false);
+ testValidityTable(news, IsEmpty, Keywords, false);
+ testValidityTable(news, IsntEmpty, Keywords, false);
+ testValidityTable(news, IsBefore, Keywords, false);
+
+ // delete any existing tags
+ let tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; i++) {
+ MailServices.tags.deleteKey(tagArray[i].key);
+ }
+
+ // add as valid tags Tag1 and Tag4
+ MailServices.tags.addTagForKey(Tag1, Tag1, null, null);
+ MailServices.tags.addTagForKey(Tag4, Tag4, null, null);
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ testKeywordSearch();
+ },
+ };
+
+ // Get a message into the local filestore. function testKeywordSearch() continues the testing after the copy.
+ var bugmail1 = do_get_file("../../../data/bugmail1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+// process each test from queue, calls itself upon completion of each search
+function testKeywordSearch() {
+ var test = Tests.shift();
+ if (test) {
+ hdr.setStringProperty("keywords", test.msgTag);
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.testTag,
+ Ci.nsMsgSearchAttrib.Keywords,
+ test.op,
+ test.count,
+ testKeywordSearch
+ );
+ } else {
+ hdr = null;
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js b/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js
new file mode 100644
index 0000000000..e31f4db7d2
--- /dev/null
+++ b/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+/*
+ * Testing of Uint32HdrProperty search attribute. Adapted from test_search.js
+ */
+
+/* import-globals-from ../../../test/resources/searchTestUtils.js */
+load("../../../resources/searchTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var Isnt = Ci.nsMsgSearchOp.Isnt;
+var Is = Ci.nsMsgSearchOp.Is;
+var IsGreaterThan = Ci.nsMsgSearchOp.IsGreaterThan;
+var IsLessThan = Ci.nsMsgSearchOp.IsLessThan;
+
+var Tests = [
+ // test a property that does not exist
+ {
+ hdrProperty: "idonotexist",
+ op: Is,
+ value: 1,
+ count: 0,
+ },
+ {
+ hdrProperty: "idonotexist",
+ op: Isnt,
+ value: 1,
+ count: 1,
+ },
+ // add a property and test its value
+ {
+ setup: function setupProperty() {
+ for (let msgHdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) {
+ msgHdr.setUint32Property("iam23", 23);
+ }
+ },
+ hdrProperty: "iam23",
+ op: Is,
+ value: 23,
+ count: 1,
+ },
+ {
+ hdrProperty: "iam23",
+ op: Isnt,
+ value: 23,
+ count: 0,
+ },
+ {
+ hdrProperty: "iam23",
+ op: Is,
+ value: 17,
+ count: 0,
+ },
+ {
+ hdrProperty: "iam23",
+ op: Isnt,
+ value: 17,
+ count: 1,
+ },
+ {
+ hdrProperty: "iam23",
+ op: IsGreaterThan,
+ value: 25,
+ count: 0,
+ },
+ {
+ hdrProperty: "iam23",
+ op: IsLessThan,
+ value: 25,
+ count: 1,
+ },
+ {
+ hdrProperty: "iam23",
+ op: IsGreaterThan,
+ value: 17,
+ count: 1,
+ },
+ {
+ hdrProperty: "iam23",
+ op: IsLessThan,
+ value: 17,
+ count: 0,
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ testSearch();
+ },
+ };
+
+ // Get a message into the local filestore. function testSearch() continues
+ // the testing after the copy.
+ var bugmail1 = do_get_file("../../../data/bugmail1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+// process each test from queue, calls itself upon completion of each search
+function testSearch() {
+ var test = Tests.shift();
+ if (test) {
+ if (test.setup) {
+ test.setup();
+ }
+ new TestSearch(
+ localAccountUtils.inboxFolder,
+ test.value,
+ Ci.nsMsgSearchAttrib.Uint32HdrProperty,
+ test.op,
+ test.count,
+ testSearch,
+ null,
+ null,
+ test.hdrProperty
+ );
+ } else {
+ do_test_finished();
+ }
+}
diff --git a/comm/mailnews/search/test/unit/xpcshell.ini b/comm/mailnews/search/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..3157b90aea
--- /dev/null
+++ b/comm/mailnews/search/test/unit/xpcshell.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = head_mailbase.js
+tail =
+
+[test_base64_decoding.js]
+[test_bug366491.js]
+[test_bug404489.js]
+[test_copyThenMoveManual.js]
+[test_junkWhitelisting.js]
+[test_quarantineFilterMove.js]
+[test_search.js]
+[test_searchAddressInAb.js]
+[test_searchBody.js]
+[test_searchBoolean.js]
+[test_searchChaining.js]
+[test_searchCustomTerm.js]
+[test_searchJunk.js]
+[test_searchLocalizationStrings.js]
+[test_searchTag.js]
+[test_searchUint32HdrProperty.js]
diff --git a/comm/mailnews/test/data/01-plaintext.eml b/comm/mailnews/test/data/01-plaintext.eml
new file mode 100644
index 0000000000..5aa8b3046e
--- /dev/null
+++ b/comm/mailnews/test/data/01-plaintext.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: 1 plaintext
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+Content-Language: en-GB
+
+U2VhcmNoIGZvciBodWh1
+
diff --git a/comm/mailnews/test/data/02-plaintext+attachment.eml b/comm/mailnews/test/data/02-plaintext+attachment.eml
new file mode 100644
index 0000000000..864861e1f0
--- /dev/null
+++ b/comm/mailnews/test/data/02-plaintext+attachment.eml
@@ -0,0 +1,32 @@
+To: test@example.com
+From: test@example.com
+Subject: 2 plaintext + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBodWh1
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/03-HTML.eml b/comm/mailnews/test/data/03-HTML.eml
new file mode 100644
index 0000000000..dd407575cc
--- /dev/null
+++ b/comm/mailnews/test/data/03-HTML.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: 3 HTML
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+Content-Language: en-GB
+
+PGJvZHk+U2VhcmNoIGZvciA8Yj5odWh1PC9iPjwvYm9keT4=
+
diff --git a/comm/mailnews/test/data/04-HTML+attachment.eml b/comm/mailnews/test/data/04-HTML+attachment.eml
new file mode 100644
index 0000000000..5e6ae10eb3
--- /dev/null
+++ b/comm/mailnews/test/data/04-HTML+attachment.eml
@@ -0,0 +1,32 @@
+To: test@example.com
+From: test@example.com
+Subject: 4 HTML + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/html; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+PGJvZHk+U2VhcmNoIGZvciA8Yj5odWh1PC9iPjwvYm9keT4=
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/05-HTML+embedded-image.eml b/comm/mailnews/test/data/05-HTML+embedded-image.eml
new file mode 100644
index 0000000000..de72415237
--- /dev/null
+++ b/comm/mailnews/test/data/05-HTML+embedded-image.eml
@@ -0,0 +1,38 @@
+To: test@example.com
+From: test@example.com
+Subject: 5 HTML + embedded image
+Message-ID: <c1ddcd5d-71c1-9c9d-1b81-e3b9abb99030@example.com>
+Date: Sat, 30 Dec 2017 19:26:23 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/related;
+ boundary="------------B2BBD36A919AB2B2F84E2469"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBl
+IiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9d2luZG93cy0xMjUyIj4NCiAgPC9oZWFk
+Pg0KICA8Ym9keSB0ZXh0PSIjMDAwMDAwIiBiZ2NvbG9yPSIjRkZGRkZGIj4NCiAgICA8cD48
+dHQ+U2VhcmNoIGZvciBodWh1PC90dD48L3A+DQogICAgPHA+PGltZyBzcmM9ImNpZDpwYXJ0
+MS44QzVFNkE4MS5EMEMxQjkxQUBqb3Jnay5jb20iIGFsdD0iIj48L3A+DQogIDwvYm9keT4N
+CjwvaHRtbD4=
+
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@jorgk.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------B2BBD36A919AB2B2F84E2469--
diff --git a/comm/mailnews/test/data/06-plaintext+HMTL.eml b/comm/mailnews/test/data/06-plaintext+HMTL.eml
new file mode 100644
index 0000000000..e028ecc485
--- /dev/null
+++ b/comm/mailnews/test/data/06-plaintext+HMTL.eml
@@ -0,0 +1,27 @@
+To: test@example.com
+From: test@example.com
+Subject: 6 plaintext + HMTL
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+Content-Language: de-DE
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBoaWhp
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGJvZHk+U2VhcmNoIGZvciA8Yj5odWh1PC9iPjwvYm9keT4=
+
+--------------FAB286B8794CC63C0A0FD1BB--
diff --git a/comm/mailnews/test/data/07-plaintext+(HTML+embedded-image).eml b/comm/mailnews/test/data/07-plaintext+(HTML+embedded-image).eml
new file mode 100644
index 0000000000..23b0900584
--- /dev/null
+++ b/comm/mailnews/test/data/07-plaintext+(HTML+embedded-image).eml
@@ -0,0 +1,52 @@
+To: test@example.com
+From: test@example.com
+Subject: 7 plaintext + (HTML + embedded image)
+Message-ID: <fd7a5d4a-6a3a-8b9b-b3e4-9e9391c3c703@example.com>
+Date: Sat, 30 Dec 2017 19:36:00 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------77E82F0826A0A90EABD21FC3"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBoaWhp
+
+
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: multipart/related;
+ boundary="------------D719681335F2A7D71D3761B1"
+
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBl
+IiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9d2luZG93cy0xMjUyIj4NCiAgPC9oZWFk
+Pg0KICA8Ym9keSB0ZXh0PSIjMDAwMDAwIiBiZ2NvbG9yPSIjRkZGRkZGIj4NCiAgICA8cD48
+dHQ+U2VhcmNoIGZvciBodWh1PC90dD48L3A+DQogICAgPHA+PGltZyBzcmM9ImNpZDpwYXJ0
+MS44QzVFNkE4MS5EMEMxQjkxQUBqb3Jnay5jb20iIGFsdD0iIj48L3A+DQogIDwvYm9keT4N
+CjwvaHRtbD4=
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@jorgk.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D719681335F2A7D71D3761B1--
+
+--------------77E82F0826A0A90EABD21FC3--
diff --git a/comm/mailnews/test/data/08-plaintext+HTML+attachment.eml b/comm/mailnews/test/data/08-plaintext+HTML+attachment.eml
new file mode 100644
index 0000000000..3daaeec967
--- /dev/null
+++ b/comm/mailnews/test/data/08-plaintext+HTML+attachment.eml
@@ -0,0 +1,46 @@
+To: test@example.com
+From: test@example.com
+Subject: 8 plaintext + HTML + attachment
+Message-ID: <b09c8682-a485-98ee-8f8e-edb89a1deec3@example.com>
+Date: Sat, 30 Dec 2017 19:58:40 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------A1EC8071C6B86B871C9CB87F"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: multipart/alternative;
+ boundary="------------9EC5D7C387C9839604A227BB"
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBoaWhp
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGJvZHk+U2VhcmNoIGZvciA8Yj5odWh1PC9iPjwvYm9keT4=
+
+--------------9EC5D7C387C9839604A227BB--
+
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------A1EC8071C6B86B871C9CB87F--
diff --git a/comm/mailnews/test/data/09-(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/09-(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..fc7a802423
--- /dev/null
+++ b/comm/mailnews/test/data/09-(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,55 @@
+To: test@example.com
+From: test@example.com
+Subject: 9 (HTML + embedded image) + attachment
+Message-ID: <cdf5bf44-03a4-4be1-c9c7-88cb4a5838ed@example.com>
+Date: Sat, 30 Dec 2017 20:19:46 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------F5CEBCED9FC06ACB07B3D485"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: multipart/related;
+ boundary="------------1722706F2C203820A6CAA06F"
+
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBl
+IiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9d2luZG93cy0xMjUyIj4NCiAgPC9oZWFk
+Pg0KICA8Ym9keSB0ZXh0PSIjMDAwMDAwIiBiZ2NvbG9yPSIjRkZGRkZGIj4NCiAgICA8cD48
+dHQ+U2VhcmNoIGZvciBodWh1PC90dD48L3A+DQogICAgPHA+PGltZyBzcmM9ImNpZDpwYXJ0
+MS44QzVFNkE4MS5EMEMxQjkxQUBqb3Jnay5jb20iIGFsdD0iIj48L3A+DQogIDwvYm9keT4N
+CjwvaHRtbD4=
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@jorgk.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------1722706F2C203820A6CAA06F--
+
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------F5CEBCED9FC06ACB07B3D485--
diff --git a/comm/mailnews/test/data/10-plaintext+(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/10-plaintext+(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..1df9853dce
--- /dev/null
+++ b/comm/mailnews/test/data/10-plaintext+(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,69 @@
+To: test@example.com
+From: test@example.com
+Subject: 10 plaintext + (HTML + embedded image) + attachment
+Message-ID: <1e58c8f2-3a15-96e7-76b7-046cf6e1ce1e@example.com>
+Date: Sat, 30 Dec 2017 20:50:01 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------B94553864BC0A4472640622E"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B94553864BC0A4472640622E
+Content-Type: multipart/alternative;
+ boundary="------------B24EA868A72E5E6144485481"
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBoaWhp
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: multipart/related;
+ boundary="------------D1360749D11EBC0C64444B6C"
+
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBl
+IiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9d2luZG93cy0xMjUyIj4NCiAgPC9oZWFk
+Pg0KICA8Ym9keSB0ZXh0PSIjMDAwMDAwIiBiZ2NvbG9yPSIjRkZGRkZGIj4NCiAgICA8cD48
+dHQ+U2VhcmNoIGZvciBodWh1PC90dD48L3A+DQogICAgPHA+PGltZyBzcmM9ImNpZDpwYXJ0
+MS44QzVFNkE4MS5EMEMxQjkxQUBqb3Jnay5jb20iIGFsdD0iIj48L3A+DQogIDwvYm9keT4N
+CjwvaHRtbD4=
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@jorgk.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D1360749D11EBC0C64444B6C--
+
+--------------B24EA868A72E5E6144485481--
+
+--------------B94553864BC0A4472640622E
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------B94553864BC0A4472640622E--
diff --git a/comm/mailnews/test/data/11-plaintext.eml b/comm/mailnews/test/data/11-plaintext.eml
new file mode 100644
index 0000000000..e3ee23db36
--- /dev/null
+++ b/comm/mailnews/test/data/11-plaintext.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: 11 plaintext
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+Content-Language: en-GB
+
+Search for hühü
+
diff --git a/comm/mailnews/test/data/12-plaintext+attachment.eml b/comm/mailnews/test/data/12-plaintext+attachment.eml
new file mode 100644
index 0000000000..d6d1f7aa3f
--- /dev/null
+++ b/comm/mailnews/test/data/12-plaintext+attachment.eml
@@ -0,0 +1,32 @@
+To: test@example.com
+From: test@example.com
+Subject: 12 plaintext + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/plain; charset=ISO-8859-7; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Search for Greek text ÊáëçóðÝñá
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/13-HTML.eml b/comm/mailnews/test/data/13-HTML.eml
new file mode 100644
index 0000000000..861f21d361
--- /dev/null
+++ b/comm/mailnews/test/data/13-HTML.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: 13 HTML
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+Content-Language: en-GB
+
+<body>Search for <b>hühü</b></body>
+
diff --git a/comm/mailnews/test/data/14-HTML+attachment.eml b/comm/mailnews/test/data/14-HTML+attachment.eml
new file mode 100644
index 0000000000..95cdedc104
--- /dev/null
+++ b/comm/mailnews/test/data/14-HTML+attachment.eml
@@ -0,0 +1,32 @@
+To: test@example.com
+From: test@example.com
+Subject: 14 HTML + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+<body>Search for <b>hühü</b></body>
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/15-HTML+embedded-image.eml b/comm/mailnews/test/data/15-HTML+embedded-image.eml
new file mode 100644
index 0000000000..6b91ae2035
--- /dev/null
+++ b/comm/mailnews/test/data/15-HTML+embedded-image.eml
@@ -0,0 +1,42 @@
+To: test@example.com
+From: test@example.com
+Subject: 15 HTML + embedded image
+Message-ID: <c1ddcd5d-71c1-9c9d-1b81-e3b9abb99030@example.com>
+Date: Sat, 30 Dec 2017 19:26:23 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/related;
+ boundary="------------B2BBD36A919AB2B2F84E2469"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for hühü</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------B2BBD36A919AB2B2F84E2469--
diff --git a/comm/mailnews/test/data/16-plaintext+HMTL.eml b/comm/mailnews/test/data/16-plaintext+HMTL.eml
new file mode 100644
index 0000000000..e810d7ac2d
--- /dev/null
+++ b/comm/mailnews/test/data/16-plaintext+HMTL.eml
@@ -0,0 +1,27 @@
+To: test@example.com
+From: test@example.com
+Subject: 16 plaintext + HMTL
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+Content-Language: de-DE
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: base64
+
+U2VhcmNoIGZvciBow69ow68=
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<body>Search for <b>hühü</b></body>
+
+--------------FAB286B8794CC63C0A0FD1BB--
diff --git a/comm/mailnews/test/data/17-plaintext+(HTML+embedded-image).eml b/comm/mailnews/test/data/17-plaintext+(HTML+embedded-image).eml
new file mode 100644
index 0000000000..9b68e3421b
--- /dev/null
+++ b/comm/mailnews/test/data/17-plaintext+(HTML+embedded-image).eml
@@ -0,0 +1,52 @@
+To: test@example.com
+From: test@example.com
+Subject: 17 plaintext + (HTML + embedded image)
+Message-ID: <fd7a5d4a-6a3a-8b9b-b3e4-9e9391c3c703@example.com>
+Date: Sat, 30 Dec 2017 19:36:00 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------77E82F0826A0A90EABD21FC3"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Search for hïhï
+
+
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: multipart/related;
+ boundary="------------D719681335F2A7D71D3761B1"
+
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQogIDxoZWFkPg0KDQogICAgPG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBl
+IiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiPg0KICA8L2hlYWQ+DQogIDxi
+b2R5IHRleHQ9IiMwMDAwMDAiIGJnY29sb3I9IiNGRkZGRkYiPg0KICAgIDxwPjx0dD5TZWFy
+Y2ggZm9yIGjDvGjDvDwvdHQ+PC9wPg0KICAgIDxwPjxpbWcgc3JjPSJjaWQ6cGFydDEuOEM1
+RTZBODEuRDBDMUI5MUFAZXhhbXBsZS5jb20iIGFsdD0iIj48L3A+DQogIDwvYm9keT4NCjwv
+aHRtbD4=
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D719681335F2A7D71D3761B1--
+
+--------------77E82F0826A0A90EABD21FC3--
diff --git a/comm/mailnews/test/data/18-plaintext+HTML+attachment.eml b/comm/mailnews/test/data/18-plaintext+HTML+attachment.eml
new file mode 100644
index 0000000000..047a85c211
--- /dev/null
+++ b/comm/mailnews/test/data/18-plaintext+HTML+attachment.eml
@@ -0,0 +1,46 @@
+To: test@example.com
+From: test@example.com
+Subject: 18 plaintext + HTML + attachment
+Message-ID: <b09c8682-a485-98ee-8f8e-edb89a1deec3@example.com>
+Date: Sat, 30 Dec 2017 19:58:40 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------A1EC8071C6B86B871C9CB87F"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: multipart/alternative;
+ boundary="------------9EC5D7C387C9839604A227BB"
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Search for hïhï
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<body>Search for <b>hühü</b></body>
+
+--------------9EC5D7C387C9839604A227BB--
+
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------A1EC8071C6B86B871C9CB87F--
diff --git a/comm/mailnews/test/data/19-(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/19-(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..4d222db968
--- /dev/null
+++ b/comm/mailnews/test/data/19-(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,59 @@
+To: test@example.com
+From: test@example.com
+Subject: 19 (HTML + embedded image) + attachment
+Message-ID: <cdf5bf44-03a4-4be1-c9c7-88cb4a5838ed@example.com>
+Date: Sat, 30 Dec 2017 20:19:46 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------F5CEBCED9FC06ACB07B3D485"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: multipart/related;
+ boundary="------------1722706F2C203820A6CAA06F"
+
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for hühü</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------1722706F2C203820A6CAA06F--
+
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------F5CEBCED9FC06ACB07B3D485--
diff --git a/comm/mailnews/test/data/20-plaintext+(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/20-plaintext+(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..754e407be0
--- /dev/null
+++ b/comm/mailnews/test/data/20-plaintext+(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,73 @@
+To: test@example.com
+From: test@example.com
+Subject: 20 plaintext + (HTML + embedded image) + attachment
+Message-ID: <1e58c8f2-3a15-96e7-76b7-046cf6e1ce1e@example.com>
+Date: Sat, 30 Dec 2017 20:50:01 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------B94553864BC0A4472640622E"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B94553864BC0A4472640622E
+Content-Type: multipart/alternative;
+ boundary="------------B24EA868A72E5E6144485481"
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Search for hïhï
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: multipart/related;
+ boundary="------------D1360749D11EBC0C64444B6C"
+
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for hühü</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D1360749D11EBC0C64444B6C--
+
+--------------B24EA868A72E5E6144485481--
+
+--------------B94553864BC0A4472640622E
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------B94553864BC0A4472640622E--
diff --git a/comm/mailnews/test/data/21-plaintext.eml b/comm/mailnews/test/data/21-plaintext.eml
new file mode 100644
index 0000000000..bc2ea408ab
--- /dev/null
+++ b/comm/mailnews/test/data/21-plaintext.eml
@@ -0,0 +1,16 @@
+To: test@example.com
+From: test@example.com
+Subject: 21 plaintext
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-GB
+
+Search for h=C3=B6h=C3=B6
+Test that we ignore a soft=
+break correctly.
+
diff --git a/comm/mailnews/test/data/22-plaintext+attachment.eml b/comm/mailnews/test/data/22-plaintext+attachment.eml
new file mode 100644
index 0000000000..1b447b2dc5
--- /dev/null
+++ b/comm/mailnews/test/data/22-plaintext+attachment.eml
@@ -0,0 +1,32 @@
+To: test@example.com
+From: test@example.com
+Subject: 22 plaintext + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/plain; charset=ISO-8859-7; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Search for Greek text =CA=E1=EB=E7=EC=DD=F1=E1
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/23-HTML.eml b/comm/mailnews/test/data/23-HTML.eml
new file mode 100644
index 0000000000..19f8539621
--- /dev/null
+++ b/comm/mailnews/test/data/23-HTML.eml
@@ -0,0 +1,18 @@
+To: test@example.com
+From: test@example.com
+Subject: 23 HTML
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-GB
+
+<body>Search for <b>h=C3=B6h=C3=B6</b>
+Test that we assemble HTML bodies correctly when there is a
+<a href=3D"https://www.example.com">soft=
+break</a> involved.
+</body>
+
diff --git a/comm/mailnews/test/data/24-HTML+attachment.eml b/comm/mailnews/test/data/24-HTML+attachment.eml
new file mode 100644
index 0000000000..704e37f7f1
--- /dev/null
+++ b/comm/mailnews/test/data/24-HTML+attachment.eml
@@ -0,0 +1,35 @@
+To: test@example.com
+From: test@example.com
+Subject: 24 HTML + attachment
+Message-ID: <9ec4f4cb-b14b-aed6-a042-58897d12e4a9@example.com>
+Date: Sat, 30 Dec 2017 19:15:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------BC006DD22051247571F398E0"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------BC006DD22051247571F398E0
+Content-Type: text/html; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+<body>Search for <b>h=C3=B6h=C3=B6</b>
+Test that we ignore a soft=
+break correctly.
+</body>
+
+--------------BC006DD22051247571F398E0
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------BC006DD22051247571F398E0--
diff --git a/comm/mailnews/test/data/25-HTML+embedded-image.eml b/comm/mailnews/test/data/25-HTML+embedded-image.eml
new file mode 100644
index 0000000000..8559e10a45
--- /dev/null
+++ b/comm/mailnews/test/data/25-HTML+embedded-image.eml
@@ -0,0 +1,42 @@
+To: test@example.com
+From: test@example.com
+Subject: 25 HTML + embedded image
+Message-ID: <c1ddcd5d-71c1-9c9d-1b81-e3b9abb99030@example.com>
+Date: Sat, 30 Dec 2017 19:26:23 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/related;
+ boundary="------------B2BBD36A919AB2B2F84E2469"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for h=C3=B6h=C3=B6</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------B2BBD36A919AB2B2F84E2469
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------B2BBD36A919AB2B2F84E2469--
diff --git a/comm/mailnews/test/data/26-plaintext+HMTL.eml b/comm/mailnews/test/data/26-plaintext+HMTL.eml
new file mode 100644
index 0000000000..87229fe590
--- /dev/null
+++ b/comm/mailnews/test/data/26-plaintext+HMTL.eml
@@ -0,0 +1,27 @@
+To: test@example.com
+From: test@example.com
+Subject: 26 plaintext + HMTL
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+Content-Language: de-DE
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Search for h=C3=A4h=C3=A4
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<body>Search for <b>h=C3=B6h=C3=B6</b></body>
+
+--------------FAB286B8794CC63C0A0FD1BB--
diff --git a/comm/mailnews/test/data/27-plaintext+(HTML+embedded-image).eml b/comm/mailnews/test/data/27-plaintext+(HTML+embedded-image).eml
new file mode 100644
index 0000000000..b2945aea2c
--- /dev/null
+++ b/comm/mailnews/test/data/27-plaintext+(HTML+embedded-image).eml
@@ -0,0 +1,56 @@
+To: test@example.com
+From: test@example.com
+Subject: 27 plaintext + (HTML + embedded image)
+Message-ID: <fd7a5d4a-6a3a-8b9b-b3e4-9e9391c3c703@example.com>
+Date: Sat, 30 Dec 2017 19:36:00 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------77E82F0826A0A90EABD21FC3"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: text/plain; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Search for h=C3=A4h=C3=A4
+
+
+--------------77E82F0826A0A90EABD21FC3
+Content-Type: multipart/related;
+ boundary="------------D719681335F2A7D71D3761B1"
+
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for h=C3=B6h=C3=B6</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------D719681335F2A7D71D3761B1
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D719681335F2A7D71D3761B1--
+
+--------------77E82F0826A0A90EABD21FC3--
diff --git a/comm/mailnews/test/data/28-plaintext+HTML+attachment.eml b/comm/mailnews/test/data/28-plaintext+HTML+attachment.eml
new file mode 100644
index 0000000000..32ea5dc0a5
--- /dev/null
+++ b/comm/mailnews/test/data/28-plaintext+HTML+attachment.eml
@@ -0,0 +1,46 @@
+To: test@example.com
+From: test@example.com
+Subject: 28 plaintext + HTML + attachment
+Message-ID: <b09c8682-a485-98ee-8f8e-edb89a1deec3@example.com>
+Date: Sat, 30 Dec 2017 19:58:40 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------A1EC8071C6B86B871C9CB87F"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: multipart/alternative;
+ boundary="------------9EC5D7C387C9839604A227BB"
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/plain; charset="utf-8"; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Search for h=C3=A4h=C3=A4
+
+
+--------------9EC5D7C387C9839604A227BB
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<body>Search for <b>h=C3=B6h=C3=B6</b></body>
+
+--------------9EC5D7C387C9839604A227BB--
+
+--------------A1EC8071C6B86B871C9CB87F
+Content-Type: image/png;
+ name="attach.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------A1EC8071C6B86B871C9CB87F--
diff --git a/comm/mailnews/test/data/29-(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/29-(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..07c89faf59
--- /dev/null
+++ b/comm/mailnews/test/data/29-(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,59 @@
+To: test@example.com
+From: test@example.com
+Subject: 29 (HTML + embedded image) + attachment
+Message-ID: <cdf5bf44-03a4-4be1-c9c7-88cb4a5838ed@example.com>
+Date: Sat, 30 Dec 2017 20:19:46 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------F5CEBCED9FC06ACB07B3D485"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: multipart/related;
+ boundary="------------1722706F2C203820A6CAA06F"
+
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for h=C3=B6h=C3=B6</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------1722706F2C203820A6CAA06F
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------1722706F2C203820A6CAA06F--
+
+--------------F5CEBCED9FC06ACB07B3D485
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------F5CEBCED9FC06ACB07B3D485--
diff --git a/comm/mailnews/test/data/30-plaintext+(HTML+embedded-image)+attachment.eml b/comm/mailnews/test/data/30-plaintext+(HTML+embedded-image)+attachment.eml
new file mode 100644
index 0000000000..8c0777d72b
--- /dev/null
+++ b/comm/mailnews/test/data/30-plaintext+(HTML+embedded-image)+attachment.eml
@@ -0,0 +1,73 @@
+To: test@example.com
+From: test@example.com
+Subject: 30 plaintext + (HTML + embedded image) + attachment
+Message-ID: <1e58c8f2-3a15-96e7-76b7-046cf6e1ce1e@example.com>
+Date: Sat, 30 Dec 2017 20:50:01 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------B94553864BC0A4472640622E"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------B94553864BC0A4472640622E
+Content-Type: multipart/alternative;
+ boundary="------------B24EA868A72E5E6144485481"
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Search for h=E4h=E4
+
+
+--------------B24EA868A72E5E6144485481
+Content-Type: multipart/related;
+ boundary="------------D1360749D11EBC0C64444B6C"
+
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ </head>
+ <body text="#000000" bgcolor="#FFFFFF">
+ <p><tt>Search for h=F6h=F6</tt></p>
+ <p><img src="cid:part1.8C5E6A81.D0C1B91A@example.com" alt=""></p>
+ </body>
+</html>
+
+--------------D1360749D11EBC0C64444B6C
+Content-Type: image/png;
+ name="kigaaldcbanejcbi.png"
+Content-Transfer-Encoding: base64
+Content-ID: <part1.8C5E6A81.D0C1B91A@example.com>
+Content-Disposition: inline;
+ filename="kigaaldcbanejcbi.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAABpSURBVDhP3dA7EoAgDEXR7Ew+bgdx/018BEYyiICtb27FcCig3Z7Im6gK3ZxN
+/RcQkb6aK8DjtuRMzMEAiNGvlFpgtyOdEjFz14xA10wA1pg5wLRZAthtVgEm5vGtA4DhvILa
+O8A+AuYLy0U5xUUpL8kAAAAASUVORK5CYII=
+--------------D1360749D11EBC0C64444B6C--
+
+--------------B24EA868A72E5E6144485481--
+
+--------------B94553864BC0A4472640622E
+Content-Type: image/png;
+ name="attach2.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attach2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAASdEVYdFNvZnR3YXJlAEdyZWVuc2hv
+dF5VCAUAAAAVSURBVChTY/hPNBhVOqqUaKX//wMA005J0zvV0VsAAAAASUVORK5CYII=
+--------------B94553864BC0A4472640622E--
diff --git a/comm/mailnews/test/data/HTML-with-split-tag1.eml b/comm/mailnews/test/data/HTML-with-split-tag1.eml
new file mode 100644
index 0000000000..573670470c
--- /dev/null
+++ b/comm/mailnews/test/data/HTML-with-split-tag1.eml
@@ -0,0 +1,26 @@
+To: test@example.com
+From: test@example.com
+Subject: HTML with split tag
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+Content-Language: en-GB
+
+<body>
+HTML part. Now comes the tag
+
+<tag attr1=ShouldNotFindThis
+attr2="ShouldntFindThisEither">
+
+ShouldFindThisAgain
+
+The following should *not* be found: ShouldNotFind
+AcrossLines
+
+<a href="ShouldntFindHref">ShouldFind
+AcrossLines</a>
+</body>
diff --git a/comm/mailnews/test/data/HTML-with-split-tag2.eml b/comm/mailnews/test/data/HTML-with-split-tag2.eml
new file mode 100644
index 0000000000..38581d0810
--- /dev/null
+++ b/comm/mailnews/test/data/HTML-with-split-tag2.eml
@@ -0,0 +1,41 @@
+To: test@example.com
+From: test@example.com
+Subject: HTML with split tag
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+Text part
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+<body>
+HTML part. Now comes the tag
+
+<tag attr1=ShouldNotFindThis
+attr2="ShouldntFindThisEither">
+
+ShouldFindThisAgain
+
+The following should *not* be found: ShouldNotFind
+AcrossLines
+
+<a href="ShouldntFindHref">ShouldFind
+AcrossLines</a>
+</body>
+
+--------------FAB286B8794CC63C0A0FD1BB--
+
+
diff --git a/comm/mailnews/test/data/SenderHeader b/comm/mailnews/test/data/SenderHeader
new file mode 100644
index 0000000000..d37f5c04b9
--- /dev/null
+++ b/comm/mailnews/test/data/SenderHeader
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from caspiaco by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for kent@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([63.245.208.146] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for kent@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <kent@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+From: bugzilla-daemon@mozilla.org
+Sender: iamthesender@example.com
+To: kent@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::63.245.208.146:host29.hostmonster.com::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/SpamAssassinYes b/comm/mailnews/test/data/SpamAssassinYes
new file mode 100644
index 0000000000..ac0f76b193
--- /dev/null
+++ b/comm/mailnews/test/data/SpamAssassinYes
@@ -0,0 +1,12 @@
+X-Spam-Status: Yes, score=10.0 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail7.m47LtAEf007543@mrapp51.mozilla.org>
+From: invalid@example.com
+To: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
diff --git a/comm/mailnews/test/data/abLists1.sql b/comm/mailnews/test/data/abLists1.sql
new file mode 100644
index 0000000000..77963da29e
--- /dev/null
+++ b/comm/mailnews/test/data/abLists1.sql
@@ -0,0 +1,62 @@
+-- Address book data for use in various tests.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 1),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 2),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 3),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 4),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 5);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'PrimaryEmail', 'test1@foo.invalid'),
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'LowercasePrimaryEmail', 'test1@foo.invalid'),
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'PreferMailFormat', '0'),
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'PopularityIndex', '0'),
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'AllowRemoteContent', '0'),
+ ('0a64d642-7b51-4a84-be67-59b27ff2b528', 'LastModifiedDate', '0'),
+
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'PrimaryEmail', 'test2@foo.invalid'),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'LowercasePrimaryEmail', 'test2@foo.invalid'),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'PreferMailFormat', '0'),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'PopularityIndex', '0'),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'AllowRemoteContent', '0'),
+ ('ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd', 'LastModifiedDate', '0'),
+
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'PrimaryEmail', 'test3@foo.invalid'),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'LowercasePrimaryEmail', 'test3@foo.invalid'),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'PreferMailFormat', '0'),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'PopularityIndex', '0'),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'AllowRemoteContent', '0'),
+ ('caaadb6c-425d-40e3-8f19-72546f6b01d8', 'LastModifiedDate', '0'),
+
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'PrimaryEmail', 'test4@foo.invalid'),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'LowercasePrimaryEmail', 'test4@foo.invalid'),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'LastModifiedDate', '1200685646'),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'PreferMailFormat', '1'),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'PopularityIndex', '0'),
+ ('23acb230-f0d9-4348-a7be-1242cd579631', 'AllowRemoteContent', '0'),
+
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'PrimaryEmail', 'test5@foo.invalid'),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'LowercasePrimaryEmail', 'test5@foo.invalid'),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'LastModifiedDate', '1200685651'),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'PreferMailFormat', '2'),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'PopularityIndex', '0'),
+ ('02cf43d5-e5b8-48b4-9546-1bb509cd998f', 'AllowRemoteContent', '0');
+
+INSERT INTO lists (uid, localId, name, nickName, description) VALUES
+ ('98636844-ed9c-4ac1-98ac-de7989a93615', 1, 'TestList1', '', ''),
+ ('31c44c28-450f-44d6-ba39-71cae90fac21', 2, 'TestList2', '', ''),
+ ('46cf4cbf-5945-43e4-a822-30c2f2969db9', 3, 'TestList3', '', '');
+
+INSERT INTO list_cards (list, card) VALUES
+ ('98636844-ed9c-4ac1-98ac-de7989a93615', '0a64d642-7b51-4a84-be67-59b27ff2b528'),
+ ('98636844-ed9c-4ac1-98ac-de7989a93615', 'ce1bd5ad-17e7-4a1b-a51e-fbce76556ebd'),
+ ('98636844-ed9c-4ac1-98ac-de7989a93615', 'caaadb6c-425d-40e3-8f19-72546f6b01d8'),
+ ('31c44c28-450f-44d6-ba39-71cae90fac21', '23acb230-f0d9-4348-a7be-1242cd579631'),
+ ('46cf4cbf-5945-43e4-a822-30c2f2969db9', '02cf43d5-e5b8-48b4-9546-1bb509cd998f');
diff --git a/comm/mailnews/test/data/abLists2.sql b/comm/mailnews/test/data/abLists2.sql
new file mode 100644
index 0000000000..f37d1a1cda
--- /dev/null
+++ b/comm/mailnews/test/data/abLists2.sql
@@ -0,0 +1,62 @@
+-- Address book data for use in various tests.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 1),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 2),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 3),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 4),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 5);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'PrimaryEmail', 'test1@com.invalid'),
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'LowercasePrimaryEmail', 'test1@com.invalid'),
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'PreferMailFormat', '0'),
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'PopularityIndex', '0'),
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'AllowRemoteContent', '0'),
+ ('420a2534-7e35-45e3-88b1-104e92608faa', 'LastModifiedDate', '0'),
+
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'PrimaryEmail', 'test2@com.invalid'),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'LowercasePrimaryEmail', 'test2@com.invalid'),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'PreferMailFormat', '0'),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'PopularityIndex', '0'),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'AllowRemoteContent', '0'),
+ ('3291e9a7-cbd9-4146-9c4e-e1afe5e25085', 'LastModifiedDate', '0'),
+
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'PrimaryEmail', 'test3@com.invalid'),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'LowercasePrimaryEmail', 'test3@com.invalid'),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'PreferMailFormat', '0'),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'PopularityIndex', '0'),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'AllowRemoteContent', '0'),
+ ('fcc46367-7081-487d-bbd3-f8f8e03e5262', 'LastModifiedDate', '0'),
+
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'PrimaryEmail', 'test4@com.invalid'),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'LowercasePrimaryEmail', 'test4@com.invalid'),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'PreferMailFormat', '1'),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'PopularityIndex', '0'),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'AllowRemoteContent', '0'),
+ ('e62f6ec2-8248-478e-8f6d-e31cdbeda4b8', 'LastModifiedDate', '0'),
+
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'PrimaryEmail', 'test5@com.invalid'),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'LowercasePrimaryEmail', 'test5@com.invalid'),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'PreferMailFormat', '2'),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'PopularityIndex', '0'),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'AllowRemoteContent', '0'),
+ ('4bc4f8c2-66d4-4421-a7b4-4be9d8be8614', 'LastModifiedDate', '0');
+
+INSERT INTO lists (uid, localId, name, nickName, description) VALUES
+ ('df79d3c0-5976-4279-851c-a8814f17ef30', 1, 'ListTest1', '', ''),
+ ('cad38149-925a-4159-8c34-20ac74ae7a17', 2, 'ListTest2', '', ''),
+ ('c069dd7a-408f-4440-9fc0-67643fbe5777', 3, 'ListTest3', '', '');
+
+INSERT INTO list_cards (list, card) VALUES
+ ('df79d3c0-5976-4279-851c-a8814f17ef30', '420a2534-7e35-45e3-88b1-104e92608faa'),
+ ('df79d3c0-5976-4279-851c-a8814f17ef30', '3291e9a7-cbd9-4146-9c4e-e1afe5e25085'),
+ ('df79d3c0-5976-4279-851c-a8814f17ef30', 'fcc46367-7081-487d-bbd3-f8f8e03e5262'),
+ ('cad38149-925a-4159-8c34-20ac74ae7a17', 'e62f6ec2-8248-478e-8f6d-e31cdbeda4b8'),
+ ('c069dd7a-408f-4440-9fc0-67643fbe5777', '4bc4f8c2-66d4-4421-a7b4-4be9d8be8614');
diff --git a/comm/mailnews/test/data/alias-1.json b/comm/mailnews/test/data/alias-1.json
new file mode 100644
index 0000000000..96c575936b
--- /dev/null
+++ b/comm/mailnews/test/data/alias-1.json
@@ -0,0 +1,12 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example",
+ "keys": [
+ { "id": "FBFCC82A015E7330" },
+ { "fingerprint": "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F" }
+ ]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-10.json b/comm/mailnews/test/data/alias-10.json
new file mode 100644
index 0000000000..3324b1a127
--- /dev/null
+++ b/comm/mailnews/test/data/alias-10.json
@@ -0,0 +1,8 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example"
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-11.json b/comm/mailnews/test/data/alias-11.json
new file mode 100644
index 0000000000..5a47244204
--- /dev/null
+++ b/comm/mailnews/test/data/alias-11.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example",
+ "keys": []
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-12.json b/comm/mailnews/test/data/alias-12.json
new file mode 100644
index 0000000000..26f934fceb
--- /dev/null
+++ b/comm/mailnews/test/data/alias-12.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example",
+ "keys": "bad type, not an array"
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-13.json b/comm/mailnews/test/data/alias-13.json
new file mode 100644
index 0000000000..aeca978e36
--- /dev/null
+++ b/comm/mailnews/test/data/alias-13.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "name@domain.net",
+ "keys": [{ "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-14.json b/comm/mailnews/test/data/alias-14.json
new file mode 100644
index 0000000000..3396d74f0c
--- /dev/null
+++ b/comm/mailnews/test/data/alias-14.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "NAME@DOMAIN.NET",
+ "keys": [{ "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-15.json b/comm/mailnews/test/data/alias-15.json
new file mode 100644
index 0000000000..c67494be2f
--- /dev/null
+++ b/comm/mailnews/test/data/alias-15.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "NAME@domain.net",
+ "keys": [{ "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-2.json b/comm/mailnews/test/data/alias-2.json
new file mode 100644
index 0000000000..0876eb643b
--- /dev/null
+++ b/comm/mailnews/test/data/alias-2.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example",
+ "keys": [{ "fingerprint": "0123456789012345678901234567890123456789" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-3.json b/comm/mailnews/test/data/alias-3.json
new file mode 100644
index 0000000000..e7e3bffc62
--- /dev/null
+++ b/comm/mailnews/test/data/alias-3.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "alice@openpgp.example",
+ "keys": [{ "fingerprint": "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-4.json b/comm/mailnews/test/data/alias-4.json
new file mode 100644
index 0000000000..8da21ee3cd
--- /dev/null
+++ b/comm/mailnews/test/data/alias-4.json
@@ -0,0 +1,13 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "nobody@example.com",
+ "keys": [
+ { "fingerprint": "EB85BB5FA33A75E15E944E63F231550C4F47E38E" },
+ { "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" },
+ { "fingerprint": "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F" }
+ ]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-5.json b/comm/mailnews/test/data/alias-5.json
new file mode 100644
index 0000000000..51379b8692
--- /dev/null
+++ b/comm/mailnews/test/data/alias-5.json
@@ -0,0 +1,14 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "nobody@example.com",
+ "keys": [
+ { "fingerprint": "EB85BB5FA33A75E15E944E63F231550C4F47E38E" },
+ { "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" },
+ { "fingerprint": "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F" },
+ { "fingerprint": "0123456789012345678901234567890123456789" }
+ ]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-6.json b/comm/mailnews/test/data/alias-6.json
new file mode 100644
index 0000000000..1b0da71f75
--- /dev/null
+++ b/comm/mailnews/test/data/alias-6.json
@@ -0,0 +1,9 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "domain": "example.com",
+ "keys": [{ "fingerprint": "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-7.json b/comm/mailnews/test/data/alias-7.json
new file mode 100644
index 0000000000..b2d71ead48
--- /dev/null
+++ b/comm/mailnews/test/data/alias-7.json
@@ -0,0 +1,20 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "domain": "domain1.example.com",
+ "keys": [{ "fingerprint": "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }]
+ },
+ {
+ "domain": "domain2.example.com",
+ "keys": [{ "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" }]
+ },
+ {
+ "email": "contact@domain1.example.com",
+ "keys": [
+ { "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" },
+ { "id": "F231550C4F47E38E" }
+ ]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-8.json b/comm/mailnews/test/data/alias-8.json
new file mode 100644
index 0000000000..0bc111b3d5
--- /dev/null
+++ b/comm/mailnews/test/data/alias-8.json
@@ -0,0 +1,19 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ "email": "c@mixED.exAMPLE",
+ "keys": [{ "fingerprint": "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F" }]
+ },
+
+ {
+ "domain": "upperdom.example",
+ "keys": [{ "fingerprint": "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }]
+ },
+
+ {
+ "domain": "LOWERDOM.EXAMPLE",
+ "keys": [{ "fingerprint": "D1A66E1A23B182C9980F788CFBFCC82A015E7330" }]
+ }
+ ]
+}
diff --git a/comm/mailnews/test/data/alias-9.json b/comm/mailnews/test/data/alias-9.json
new file mode 100644
index 0000000000..8dff904c94
--- /dev/null
+++ b/comm/mailnews/test/data/alias-9.json
@@ -0,0 +1,7 @@
+{
+ "description": "OpenPGP alias rules",
+ "rules": [
+ {
+ },
+ ]
+}
diff --git a/comm/mailnews/test/data/bad-charset.eml b/comm/mailnews/test/data/bad-charset.eml
new file mode 100644
index 0000000000..417e268999
--- /dev/null
+++ b/comm/mailnews/test/data/bad-charset.eml
@@ -0,0 +1,29 @@
+To: test@example.com
+From: test@example.com
+Subject: plaintext + HMTL, both with a bad charset
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset="bad-charset"
+Content-Transfer-Encoding: quoted-printable
+
+Despite the bad charset we can find the search term Mätterhorn.
+Note that the umlaut is encoded in windows-1252.
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset="bad-charset"
+Content-Transfer-Encoding: quoted-printable
+
+<body>
+Despite the bad charset we can find the search term Matterhörn.
+Note that the umlaut is encoded in UTF-8.</body>
+
+--------------FAB286B8794CC63C0A0FD1BB--
diff --git a/comm/mailnews/test/data/badly-folded-headers.eml b/comm/mailnews/test/data/badly-folded-headers.eml
new file mode 100644
index 0000000000..03908d78a7
--- /dev/null
+++ b/comm/mailnews/test/data/badly-folded-headers.eml
@@ -0,0 +1,16 @@
+From - Sat Apr 14 14:01:30 2018
+Date: Sat, 14 Apr 2018 14:01:30 +0200
+Subject: Badly
+ folded
+ headers,
+ one
+
+
+ line
+ with space between To and From
+
+To: "Recipient with spaces" <recipient@example.com>
+
+From: sender@example.com
+
+Here's our body
diff --git a/comm/mailnews/test/data/base64-1 b/comm/mailnews/test/data/base64-1
new file mode 100644
index 0000000000..18e089a5b4
--- /dev/null
+++ b/comm/mailnews/test/data/base64-1
@@ -0,0 +1,7 @@
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: base64
+
+DQpIZWxsbywgd29ybGQhIChBZ2Fpbi4uLikNCg0KTGV0J3Mgc2VlIGhvdyB3ZWxsIGJhc2U2NCB0ZXh
+0IGlzIGhhbmRsZWQuICAgICAgICAgICAgICAgICAgICAgICAgICAgIFlheSwgbG90cyBvZiBzcGFjZX
+MhIFRoZXJlJ3MgZXZlbiBhIENSTEYgYXQgdGhlIGVuZCBhbmQgb25lIGF0IHRoZSBiZWdpbm5pbmcsI
+GJ1dCB0aGUgb3V0cHV0IHNob3VsZG4ndCBoYXZlIGl0Lg0K
diff --git a/comm/mailnews/test/data/base64-1.out b/comm/mailnews/test/data/base64-1.out
new file mode 100644
index 0000000000..4da219eadf
--- /dev/null
+++ b/comm/mailnews/test/data/base64-1.out
@@ -0,0 +1,3 @@
+Hello, world! (Again...)
+
+Let's see how well base64 text is handled. Yay, lots of spaces! There's even a CRLF at the end and one at the beginning, but the output shouldn't have it. \ No newline at end of file
diff --git a/comm/mailnews/test/data/base64-2 b/comm/mailnews/test/data/base64-2
new file mode 100644
index 0000000000..bb469edcde
--- /dev/null
+++ b/comm/mailnews/test/data/base64-2
@@ -0,0 +1,6 @@
+Content-Type: text/html; encoding=iso-8859-1
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGJvZHk+VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBIVE1MIHRleHQsIGFuZCB0aGUgdGFncyB
+zaG91bGRuJ3QgYmUgc3RyaXBwZWQuDQo8Yj5Cb2xkIHRleHQgaXMgYm9sZCE8L2I+PC9ib2R5PjwvaH
+RtbD4NCg==
diff --git a/comm/mailnews/test/data/base64-2.out b/comm/mailnews/test/data/base64-2.out
new file mode 100644
index 0000000000..49f4996c30
--- /dev/null
+++ b/comm/mailnews/test/data/base64-2.out
@@ -0,0 +1,2 @@
+<html><body>This is base64 encoded HTML text, and the tags shouldn't be stripped.
+<b>Bold text is bold!</b></body></html> \ No newline at end of file
diff --git a/comm/mailnews/test/data/base64-3 b/comm/mailnews/test/data/base64-3
new file mode 100644
index 0000000000..4905fe4d94
--- /dev/null
+++ b/comm/mailnews/test/data/base64-3
@@ -0,0 +1,4 @@
+Content-Type: text/html
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+VGhlIHRhZ3MgPGI+c2hvdWxkPC9iPiBiZSA8aT4gc3RyaXBwZWQgPC9pPiBpbiB0aGlzIGNhc2UhDQpObywgPHU+c2VyaW91c2x5LjwvdT48L2hlYWQ+PC9odG1sPg==
diff --git a/comm/mailnews/test/data/base64-3.out b/comm/mailnews/test/data/base64-3.out
new file mode 100644
index 0000000000..20224f8297
--- /dev/null
+++ b/comm/mailnews/test/data/base64-3.out
@@ -0,0 +1 @@
+The tags should be stripped in this case! No, seriously. \ No newline at end of file
diff --git a/comm/mailnews/test/data/base64-with-whitespace.eml b/comm/mailnews/test/data/base64-with-whitespace.eml
new file mode 100644
index 0000000000..f610203558
--- /dev/null
+++ b/comm/mailnews/test/data/base64-with-whitespace.eml
@@ -0,0 +1,46 @@
+Date: Tue, 31 Aug 2018 16:33:00 +0200
+From: From <from@example.com>
+To: To <to@example.com>
+Subject: Bug 1487421 - BASE64 MIME body and attachment with empty lines in between
+MIME-Version: 1.0
+Message-ID: <1dcZe4@example.com>
+Content-Type: multipart/mixed;
+ boundary="------------DA562B250842CC7332F16476"
+
+This is a multi-part message in MIME format.
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+YWJj
+
+ZG
+
+V
+
+mZ2hpamtsbW
+
+5vcHFyc3R1dnd4
+
+eXo=
+
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="test.txt"
+
+YWJj
+
+ZG
+
+V
+
+mZ2hpamtsbW
+
+5vcHFyc3R1dnd4
+
+eXo=
+
+--------------DA562B250842CC7332F16476--
+
diff --git a/comm/mailnews/test/data/basic1 b/comm/mailnews/test/data/basic1
new file mode 100644
index 0000000000..f23da3ea93
--- /dev/null
+++ b/comm/mailnews/test/data/basic1
@@ -0,0 +1,3 @@
+Content-Type: text/plain; charset=iso-8859-1
+
+Hello, world!
diff --git a/comm/mailnews/test/data/basic1.out b/comm/mailnews/test/data/basic1.out
new file mode 100644
index 0000000000..5dd01c177f
--- /dev/null
+++ b/comm/mailnews/test/data/basic1.out
@@ -0,0 +1 @@
+Hello, world! \ No newline at end of file
diff --git a/comm/mailnews/test/data/basic2 b/comm/mailnews/test/data/basic2
new file mode 100644
index 0000000000..d22e8f3821
--- /dev/null
+++ b/comm/mailnews/test/data/basic2
@@ -0,0 +1,3 @@
+Content-Type: text/html; charset=iso-8859-1
+
+<html><body><h1>The HTML tags here should remain intact.</h1></body></html>
diff --git a/comm/mailnews/test/data/basic2.out b/comm/mailnews/test/data/basic2.out
new file mode 100644
index 0000000000..89f45e2392
--- /dev/null
+++ b/comm/mailnews/test/data/basic2.out
@@ -0,0 +1 @@
+<html><body><h1>The HTML tags here should remain intact.</h1></body></html> \ No newline at end of file
diff --git a/comm/mailnews/test/data/basic3 b/comm/mailnews/test/data/basic3
new file mode 100644
index 0000000000..aff580f6d5
--- /dev/null
+++ b/comm/mailnews/test/data/basic3
@@ -0,0 +1,3 @@
+Content-Type: text/html; charset=iso-8859-1
+
+<html><body><h1>The HTML tags here <b>are</b> supposed to be stripped out.</h1></body></html>
diff --git a/comm/mailnews/test/data/basic3.out b/comm/mailnews/test/data/basic3.out
new file mode 100644
index 0000000000..e94f891f5e
--- /dev/null
+++ b/comm/mailnews/test/data/basic3.out
@@ -0,0 +1 @@
+The HTML tags here are supposed to be stripped out. \ No newline at end of file
diff --git a/comm/mailnews/test/data/basic4 b/comm/mailnews/test/data/basic4
new file mode 100644
index 0000000000..99933f4fe7
--- /dev/null
+++ b/comm/mailnews/test/data/basic4
@@ -0,0 +1,2 @@
+
+This message has no content type. This should be assumed to be text/plain.
diff --git a/comm/mailnews/test/data/basic4.out b/comm/mailnews/test/data/basic4.out
new file mode 100644
index 0000000000..190e5de6a6
--- /dev/null
+++ b/comm/mailnews/test/data/basic4.out
@@ -0,0 +1 @@
+This message has no content type. This should be assumed to be text/plain. \ No newline at end of file
diff --git a/comm/mailnews/test/data/basic5 b/comm/mailnews/test/data/basic5
new file mode 100644
index 0000000000..47925f60e6
--- /dev/null
+++ b/comm/mailnews/test/data/basic5
@@ -0,0 +1,4 @@
+
+<html><body>Since this message has no content type, it should be assumed to be
+<b>text/plain</b>. This means that asking to strip HTML tags should not have a
+difference.</body></html>
diff --git a/comm/mailnews/test/data/basic5.out b/comm/mailnews/test/data/basic5.out
new file mode 100644
index 0000000000..2e0f4f7abd
--- /dev/null
+++ b/comm/mailnews/test/data/basic5.out
@@ -0,0 +1,3 @@
+<html><body>Since this message has no content type, it should be assumed to be
+<b>text/plain</b>. This means that asking to strip HTML tags should not have a
+difference.</body></html> \ No newline at end of file
diff --git a/comm/mailnews/test/data/bodySearchCrash b/comm/mailnews/test/data/bodySearchCrash
new file mode 100644
index 0000000000..c82fcbcfc4
--- /dev/null
+++ b/comm/mailnews/test/data/bodySearchCrash
@@ -0,0 +1,52 @@
+From - Tue Oct 02 00:26:47 2007
+To: person@example.com
+Subject: Crashme
+From: <noreply@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary = bfe44553e2faf6bebbd51288ac31d80ec
+Date: Tue, 17 Apr 2007 18:00:44 +0300
+Message-Id: <1176822044.0@ecom>
+
+This is a MIME encoded message.
+
+--bfe44553e2faf6bebbd51288ac31d80ec
+Content-Type: text/plain
+Content-Transfer-Encoding: base64
+
+SGVsbG8sCgpKdXN0IHNpeCB3ZWVrcyB1bnRpbCBVUyBNVU0gaW4gT3JsYW5kbywgRkwhIApNYXkg
+MzFzdCAtIEp1bmUgMXN0LCAyMDA3IChUaHVyc2RheSBhbmQgRnJpZGF5KQoKaHR0cDovL211bS5t
+aWtyb3Rpay5jb20vMjAwNy9VUy9pbmZvCgpNYWtlIHN1cmUgdG8gcmVnaXN0ZXIgbm93IGFuZCBi
+ZSB0aGUgRmlyc3QgdG8gc2VlIHRoZSBORVcgUm91dGVyQk9BUkQgaGlnaCBwZXJmb3JtYW5jZSBk
+ZXZpY2VzIC0gUm91dGVyQk9BUkQgMzMzIEFORCBSb3V0ZXJCT0FSRCAxMDAwIC0gLCBhbmQgdmll
+dyB0aGUgUm91dGVyQk9BUkQgcm9hZG1hcCBmb3IgdGhpcyB5ZWFyLgoKJDg5L25pZ2h0IGF0IHRo
+ZSBPcmxhbmRvIEFpcnBvcnQgTWFycmlvdHQgLS0gbWVudGlvbiBjb2RlIG10c210c2EKClRoZSBN
+VU0gaXMgdGhlIG9ubHkgcGxhY2UgdG8gbWVldCB3aXRoIG90aGVyIE1pa3JvVGlrIHByb2Zlc3Np
+b25hbHMsIHNoYXJlIGV4cGVyaWVuY2VzLCBsZWFybiBtb3JlIGFib3V0IG5ldHdvcmtpbmcgaW4g
+Z2VuZXJhbCBhbmQgZmluYWxseSBtZWV0IHRoZSBNaWtyb1RpayBzdGFmZi4gQXNrIHF1ZXN0aW9u
+cywgbGlzdGVuIHRvIHByZXNlbnRhdGlvbnMsIHRhbGsgd2l0aCBzcGVjaWFsaXN0cyBhbmQgc2Vl
+IGludGVyZXN0aW5nIHRlY2hub2xvZ3kgZGVtb3MsIGFsbCBhdCB0aGUgTVVNIQoKKiBNaWtyb1Rp
+ayBVc2VyIE1lZXRpbmcgICAtIE1heSAzMXN0IGFuZCBKdW5lIDFzdCAoVGh1cnNkYXkgYW5kIEZy
+aWRheSkKKiBNaWtyb1RpayBTYWxlcyBNZWV0aW5ncyAtIGJ5IGFwcG9pbnRtZW50IGZyb20gTWF5
+IDMwdGggLSBKdW5lIDFzdCAoY29udGFjdCBzYWxlc0BtaWtyb3Rpay5jb20pIAoKUHJlc2VudGF0
+aW9ucyBhbmQgRGVtb3MgYnkgTWlrcm9UaWsgYW5kIEd1ZXN0czoKCi0gSG90U3BvdCBuZXR3b3Jr
+cyBvbiBhIG1vdmluZyB0cmFpbiB3aXRoIE1pa3JvVGlrIC0tIHJlYWwgZXhwZXJpZW5jZXMKLSBM
+b25nIFJhbmdlIGFuZCBIaWdoIHNwZWVkIHdpcmVsZXNzICg2MCBtaWxlcyBoaWdoIHNwZWVkISkg
+d2l0aCBNaWtyb3RpayBQcm9kdWN0cwotIFRoZSBEdWRlIG5ldHdvcmsgbWFuYWdlbWVudCBzdWl0
+ZS4gRmVhdHVyZSBwcmVzZW50YXRpb24gYW5kIGRpc2N1c3Npb24KLSBJbnRyb2R1Y3Rpb24gb2Yg
+ZXhjaXRpbmcgbmV3IHByb2R1Y3RzCiAgICh0d28gbmV3IHBvd2VyZnVsIGJvYXJkcyBvbiBkaXNw
+bGF5IC0tIGluY2x1ZGluZyBhIGJvYXJkIHdpdGgKZ2lnYWJpdCBFdGhlcm5ldCkKLSBUaGUgTWlr
+cm90aWsgVXNlciBNYW5hZ2VyIGJ1aWx0IGludG8gUm91dGVyT1MKLSBOZXcgcHJvZHVjdCBhbm5v
+dW5jZW1lbnRzIGFuZCBkZW1vcywgcm9hZG1hcAoKRWFjaCBwYXJ0aWNpcGFudCB3aWxsIHJlY2Vp
+dmUgYSBmcmVlIEw0IGxpY2Vuc2UgYW5kIGEgVC1TaGlydC4KClNlZSBNVU0gcGFnZSBmb3IgZGV0
+YWlsczogaHR0cDovL211bS5taWtyb3Rpay5jb20vMjAwNy9VUy9pbmZvCgpMb2NhdGlvbjogICAg
+ICAgICAgIE9ybGFuZG8sICBGbG9yaWRhLCBVU0EgLSBBaXBvcnQgTWFycmlvdHQKUmVnaXN0cmF0
+aW9uIERlc2s6ICBNYXkgMzB0aCAgNC02UE0sIE1heSAzMXN0IDgtMTBBTQpQcmUtbWVldGluZyBk
+cmlua3M6IE1heSAzMHRoICA2LThQTSAoZnJlZSBiZWVyKQpNZWV0aW5nIFN0YXJ0OiAgICAgIE1h
+eSAzMXN0ICA5QU0KTWVldGluZyBFbmQ6ICAgICAgICBKdW5lIDFzdCA1OjMwUE0KCkJlc3QgUmVn
+YXJkcywKVGhlIE1pa3JvVGlrIFRlYW0=
+
+--bfe44553e2faf6bebbd51288ac31d80ec--
+
+
+
+
diff --git a/comm/mailnews/test/data/bodystructurebug244741 b/comm/mailnews/test/data/bodystructurebug244741
new file mode 100644
index 0000000000..c7e23ac980
--- /dev/null
+++ b/comm/mailnews/test/data/bodystructurebug244741
@@ -0,0 +1,72 @@
+Date: Sun, 7 Feb 2010 00:01:00 -0600
+From: from@invalid.invalid
+To: to@invalid.invalid
+Subject: bug244741
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="-------bug244741partN"
+
+This is a multi-part message in MIME format.
+
+---------bug244741partN
+Content-Type: multipart/related;
+ type="multipart/alternative";
+ boundary="-------bug244741part1.N"
+
+
+---------bug244741part1.N
+Content-Type: multipart/alternative;
+ boundary="-------bug244741part1.1.N"
+
+
+---------bug244741part1.1.N
+Content-Type: text/plain;
+ charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+thisplaintextneedstodisplaytopasstest
+plaintextshouldnotbefetchedtopasstest
+
+---------bug244741part1.1.N
+Content-Type: text/html;
+ charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<body><pre>
+thishtmltextneedstodisplaytopasstest
+htmltextshouldnotbefetchedtopasstest
+</pre></body></html>
+
+---------bug244741part1.1.N--
+
+---------bug244741part1.N
+Content-Type: image/jpeg;
+ name="image001.jpg"
+Content-Transfer-Encoding: base64
+Content-ID: <image001.jpg@01CA0BAD.274DAC50>
+Content-Description: image001.jpg
+Content-Location: image001.jpg
+
+
+---------bug244741part1.N--
+
+---------bug244741partN
+Content-Type: application/octet-stream;
+ name=".pdf"
+Content-Transfer-Encoding: base64
+Content-Description: .pdf
+Content-Disposition: attachment;
+ filename=".pdf"
+
+
+---------bug244741partN
+Content-Type: application/octet-stream;
+ name=".pdf"
+Content-Transfer-Encoding: base64
+Content-Description: .pdf
+Content-Disposition: attachment;
+ filename=".pdf"
+
+
+---------bug244741partN--
diff --git a/comm/mailnews/test/data/bodystructurebug246415 b/comm/mailnews/test/data/bodystructurebug246415
new file mode 100644
index 0000000000..da132f36f8
--- /dev/null
+++ b/comm/mailnews/test/data/bodystructurebug246415
@@ -0,0 +1,30 @@
+Date: Sun, 7 Feb 2010 00:01:00 -0600
+From: from@invalid.invalid
+To: to@invalid.invalid
+Subject: bug246415
+MIME-Version: 1.0
+Content-Type: multipart/related; boundary="-------bug246415partN"
+
+---------bug246415partN
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<body>
+<pre>
+Only one text content-type so this html needs
+to display in both tests, plain and html
+
+thishtmltextneedstodisplaytopasstest
+thisplaintextneedstodisplaytopasstest
+</pre>
+</body></html>
+
+---------bug246415partN
+Content-Type: image/gif
+Content-Transfer-Encoding: base64
+Content-Disposition: inline; filename="solid.gif"
+Content-ID: <8aa65aaf56c805fc71ef7c76fb22614b>
+
+R0lGODlhDAABAIAAAM7P2AAAACH5BAAAAAAALAAAAAAMAAEAAAIDhI9WADs=
+---------bug246415partN--
diff --git a/comm/mailnews/test/data/bodystructuretest1 b/comm/mailnews/test/data/bodystructuretest1
new file mode 100644
index 0000000000..7f8bf8a76f
--- /dev/null
+++ b/comm/mailnews/test/data/bodystructuretest1
@@ -0,0 +1,57 @@
+Date: Fri, 01 Jan 2010 12:00 -0500
+To: x@x.x
+From: y@y.y
+Subject: download on demand problem
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----Part0+"
+
+This is a multi-part message in MIME format.
+
+------Part0+
+Content-Type: multipart/related;
+ type="multipart/alternative";
+ boundary="----Part1.n"
+
+
+------Part1.n
+Content-Type: multipart/alternative;
+ boundary="----Part1.1.n"
+
+
+------Part1.1.n
+Content-Type: text/plain;
+
+thisplaintextneedstodisplaytopasstest
+plaintextshouldnotbefetchedtopasstest
+
+------Part1.1.n
+Content-Type: text/html;
+
+<html>
+thishtmltextneedstodisplaytopasstest
+htmltextshouldnotbefetchedtopasstest
+</html>
+
+------Part1.1.n--
+
+------Part1.n
+Content-Type: image/jpeg;
+ name="someimage.jpg"
+
+This is someimage.jpg
+
+------Part1.n--
+
+------Part0+
+Content-Type: application/zip;
+ name="somename.zip"
+
+This is somename.zip
+
+------Part0+
+Content-Type: text/html;
+
+This is a text part that should not be displayed
+it is not inline and is not an attachment
+------Part0+--
diff --git a/comm/mailnews/test/data/bodystructuretest2 b/comm/mailnews/test/data/bodystructuretest2
new file mode 100644
index 0000000000..65bedc9760
--- /dev/null
+++ b/comm/mailnews/test/data/bodystructuretest2
@@ -0,0 +1,31 @@
+Date: Sun, 7 Feb 2010 00:01:00 -0600
+From: from@invalid.invalid
+To: to@invalid.invalid
+Subject: bug246415modified
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="-------toppart"
+
+---------toppart
+Content-Type: multipart/alternative; boundary="-------bug246415partN"
+
+---------bug246415partN
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+thisplaintextneedstodisplaytopasstest
+plaintextshouldnotbefetchedtopasstest
+
+---------bug246415partN
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<body>
+<pre>
+thishtmltextneedstodisplaytopasstest
+htmltextshouldnotbefetchedtopasstest
+</pre>
+</body></html>
+
+---------bug246415partN--
+---------toppart-- \ No newline at end of file
diff --git a/comm/mailnews/test/data/bodystructuretest3 b/comm/mailnews/test/data/bodystructuretest3
new file mode 100644
index 0000000000..0e381a883f
--- /dev/null
+++ b/comm/mailnews/test/data/bodystructuretest3
@@ -0,0 +1,56 @@
+Date: Sun, 7 Feb 2010 00:01:00 -0600
+From: from@invalid.invalid
+To: to@invalid.invalid
+Subject: bug246415
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="-------bodystructure3partN"
+
+---------bodystructure3partN
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<body>
+<pre>
+Only one text content-type so this html needs
+to display in both tests, plain and html
+
+thishtmltextneedstodisplaytopasstest
+thisplaintextneedstodisplaytopasstest
+</pre>
+</body></html>
+
+---------bodystructure3partN
+Content-Type: text/plain
+Content-Disposition: inline; filename="inline.txt"
+
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+line line line line line line line line line line line line line line line line
+---------bodystructure3partN--
diff --git a/comm/mailnews/test/data/bug132340 b/comm/mailnews/test/data/bug132340
new file mode 100644
index 0000000000..e01b250a67
--- /dev/null
+++ b/comm/mailnews/test/data/bug132340
@@ -0,0 +1,26 @@
+From - Sat Jun 11 17:26:54 2009
+Message-ID: <foo.12345@bar>
+Date: Sun, 19 Oct 2008 08:00:00 +0000
+From: Me <me@example.net>
+To: You <you@example.org>
+Content-Type: multipart/alternative; boundary=pleasekillMIME
+Subject: Test message
+
+Testing that we do body search according to bug 132340 correctly.
+
+--pleasekillMIME
+Content-Type: text/plain
+
+This is a plain-text part to match on.
+
+--pleasekillMIME
+Content-Type: application/x-javascript
+
+dump("Hello, World!");
+
+--pleasekillMIME
+Content-Type: text/plain
+Content-Transfer-Encoding: BASE64
+
+U2VtaSBjYXNlLWluc2Vuc2l0aXZpdHkgc3Vja3MuIE1hdGNoIG9uICJiYXNlIDY0IHRleHQi
+--pleasekillMIME--
diff --git a/comm/mailnews/test/data/bug460636 b/comm/mailnews/test/data/bug460636
new file mode 100644
index 0000000000..9b60413488
--- /dev/null
+++ b/comm/mailnews/test/data/bug460636
@@ -0,0 +1,126 @@
+Message-ID: <foo.12345@example>
+Date: Sun, 19 Oct 2008 08:00:00 +0000
+From: Me <me@example.net>
+To: You <you@example.org>
+Subject: Test message
+
+When this message is stored on an IMAP server and subsequently saved
+in Thunderbird as a .eml file, a spurious new line will appear after
+line 110 (caused by an erroneously inserted LF character).
+
+005 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+006 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+007 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+008 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+009 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+010 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+011 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+012 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+013 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+014 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+015 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+016 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+017 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+018 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+019 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+020 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+021 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+022 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+023 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+024 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+025 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+026 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+027 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+028 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+029 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+030 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+031 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+032 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+033 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+034 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+035 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+036 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+037 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+038 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+039 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+040 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+041 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+042 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+043 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+044 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+045 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+046 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+047 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+048 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+049 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+051 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+052 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+053 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+054 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+055 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+056 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+057 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+058 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+059 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+060 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+061 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+062 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+063 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+064 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+065 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+066 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+067 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+068 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+069 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+070 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+071 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+072 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+073 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+074 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+075 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+076 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+077 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+078 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+079 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+080 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+081 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+082 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+083 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+084 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+085 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+086 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+087 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+088 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+089 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+090 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+091 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+092 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+093 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+094 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+095 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+096 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+097 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+098 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+099 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+101 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+102 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+103 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+104 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+105 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+106 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+107 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+108 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+109 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+110 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+111 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+112 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+113 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+114 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+115 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+116 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+117 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+118 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+119 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+120 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
diff --git a/comm/mailnews/test/data/bug505221 b/comm/mailnews/test/data/bug505221
new file mode 100644
index 0000000000..c2e292156a
--- /dev/null
+++ b/comm/mailnews/test/data/bug505221
@@ -0,0 +1,75 @@
+From - Mon Jan 1 00:00:00 1965
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 10000000
+From: <aaa bbb>
+To: <aaa@bb.invalid>
+Subject: xxx
+Date: Tue, 9 Dec 2008 16:49:02 +0200
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_36B5_01C9DB8C.9514C300"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_36B5_01C9DB8C.9514C300
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; =
+charset=3Dus-ascii">
+
+
+<META content=3D"MSHTML 6.00.6000.16735" name=3DGENERATOR></HEAD>
+<BODY> bbb
+</BODY></HTML>
+------=_NextPart_000_36B5_01C9DB8C.9514C300
+Content-Type: message/rfc822
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+
+From: <sys admin>
+To: <dd@ee.invalid>
+Subject: yyy
+Date: Sun, 7 Dec 2008 17:53:47 +0200
+MIME-Version: 1.0
+Content-Type: message/rfc822
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+From: <aa@b>
+To: <aa@b>
+Subject: ccc
+Date: Sat, 23 May 2009 09:55:19 +0200
+MIME-Version: 1.0
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2180
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
+<HTML>
+<HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; =
+charset=3Diso-8859-1">
+<META NAME=3D"Generator" CONTENT=3D"MS Exchange Server version =
+08.00.0681.000">
+<TITLE>ccc</TITLE>
+</HEAD>
+<BODY>
+<!-- Converted from text/plain format -->
+
+</BODY>
+</HTML>
+------=_NextPart_000_36B5_01C9DB8C.9514C300--
+
diff --git a/comm/mailnews/test/data/bug513543 b/comm/mailnews/test/data/bug513543
new file mode 100644
index 0000000000..b2fa6f2cee
--- /dev/null
+++ b/comm/mailnews/test/data/bug513543
@@ -0,0 +1,58 @@
+From - Tue Feb 24 09:09:10 2009
+X-Account-Key: account4
+X-UIDL: 1235429919.7600.xx
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 10000000
+X-Mozilla-Keys:
+To: abc@xyc.com
+Subject: zzz
+From: def@a.org
+Reply-To: zyx@foo.com
+X-Mailer: Simple Mailer
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="PHP-mixed-45bdaa586a68b372a68536239277dcd8"
+Message-Id: <20090223142458.B361A1347C@60xx.ttt.net>
+Date: Mon, 23 Feb 2009 15:24:58 +0100 (CET)
+
+
+If you can read this then you should upgrade to a MIME complaint email reader - try Thunderbird.
+
+--PHP-mixed-45bdaa586a68b372a68536239277dcd8
+Content-Type: multipart/related;
+ boundary="PHP-rel-45bdaa586a68b372a68536239277dcd8"
+
+--PHP-rel-45bdaa586a68b372a68536239277dcd8
+Content-Type: multipart/alternative;
+ boundary="PHP-alt-45bdaa586a68b372a68536239277dcd8"
+
+--PHP-alt-45bdaa586a68b372a68536239277dcd8
+Content-type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+--PHP-alt-45bdaa586a68b372a68536239277dcd8
+Content-type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<HTML><head></head>
+</body>
+<br /><br />
+</HTML>
+
+--PHP-alt-45bdaa586a68b372a68536239277dcd8--
+
+--PHP-rel-45bdaa586a68b372a68536239277dcd8
+Content-type: text/plain; name="foo.txt"
+Content-ID: <logoufe.01030301.08030105@hhh.org>
+Content-Disposition: inline
+
+abc
+
+--PHP-rel-45bdaa586a68b372a68536239277dcd8--
+
+--PHP-mixed-45bdaa586a68b372a68536239277dcd8
+--PHP-mixed-45bdaa586a68b372a68536239277dcd8--
+
diff --git a/comm/mailnews/test/data/bug92111 b/comm/mailnews/test/data/bug92111
new file mode 100644
index 0000000000..22b45433ac
--- /dev/null
+++ b/comm/mailnews/test/data/bug92111
@@ -0,0 +1,103 @@
+Message-ID: <foo.12345@bar>
+Date: Sun, 19 Oct 2008 08:00:00 +0000
+From: Me <me@example.net>
+To: You <you@example.org>
+Subject: Test message
+
+This message tests several conditions at the same time when
+used by test_imapChunks.js, which downloads 1000 byte chunks:
+
+1) download of a message in several chunks, when the server
+ gives an incorrect value for RFC822.SIZE - specifically when
+ the message is an exact number of chunks long
+2) Handling of CR/LF split across a chunk boundary (line 1950)
+3) CR/LF immediately after chunk boundary (line 2950)
+4) CR/LF immediately before chunk boundary (every other chunk)
+
+At the end of this padding we have 800 octets (including CRLF endings).
+Each subsequent line is 50 octets except for the CRLF boundary test spots.
+PADPADPADPADPAD
+0800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1150 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1200 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1250 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1300 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1350 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1400 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1450 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2001 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2150 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2200 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2250 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2300 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2350 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2400 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2450 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+2950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3002 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3150 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3200 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3250 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3300 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3350 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3400 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3450 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+3950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4150 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4200 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4250 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4300 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4350 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4400 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4450 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+4950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
diff --git a/comm/mailnews/test/data/bug92111b b/comm/mailnews/test/data/bug92111b
new file mode 100644
index 0000000000..7fbe33ad76
--- /dev/null
+++ b/comm/mailnews/test/data/bug92111b
@@ -0,0 +1,44 @@
+Message-ID: <foo.12345@bar>
+Date: Sun, 19 Oct 2008 08:00:00 +0000
+From: Me <me@example.net>
+To: You <you@example.org>
+Subject: Test message
+
+This message tests the case where only the final LF
+of the CRLF pair at the end of the message falls into
+a new chunk.
+Used by test_imapChunks.js, which downloads 1000 byte chunks:
+
+At the end of this padding we have 500 octets (including CRLF endings).
+Each subsequent line is 50 octets except for the CRLF boundary test spots.
+PADPADPADPADPAD
+0500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+0950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1050 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1100 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1150 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1200 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1250 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1300 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1350 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1400 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1450 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1500 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1550 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1600 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1650 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1700 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1750 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1800 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1850 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1900 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1950 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
diff --git a/comm/mailnews/test/data/bugmail-1 b/comm/mailnews/test/data/bugmail-1
new file mode 100644
index 0000000000..e9afd1cbd1
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail-1
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from caspiaco by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for kent@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([63.245.208.146] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for kent@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <kent@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::63.245.208.146:host29.hostmonster.com::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail-1.msf b/comm/mailnews/test/data/bugmail-1.msf
new file mode 100644
index 0000000000..bda7be4436
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail-1.msf
@@ -0,0 +1 @@
+// <!-- <mdb:mork:z v="1.4"/> --> < <(a=c)> // (f=iso-8859-1) (B8=account)(B9=keywords)(BA=sortType)(BB=sortOrder)(BC=viewFlags) (BD=viewType)(BE=MRUTime)(80=ns:msg:db:row:scope:msgs:all)(81=subject) (82=sender)(83=message-id)(84=references)(85=recipients)(86=date) (87=size)(88=flags)(89=priority)(8A=label)(8B=statusOfset)(8C=numLines) (8D=ccList)(8E=msgThreadId)(8F=threadId)(90=threadFlags) (91=threadNewestMsgDate)(92=children)(93=unreadChildren) (94=threadSubject)(95=numRefs)(96=msgCharSet) (97=ns:msg:db:table:kind:msgs)(98=ns:msg:db:table:kind:thread) (99=ns:msg:db:table:kind:allthreads) (9A=ns:msg:db:row:scope:threads:all)(9B=threadParent)(9C=threadRoot) (9D=msgOffset)(9E=offlineMsgSize) (9F=ns:msg:db:row:scope:dbfolderinfo:all) (A0=ns:msg:db:table:kind:dbfolderinfo)(A1=numMsgs)(A2=numNewMsgs) (A3=folderSize)(A4=expungedBytes)(A5=folderDate)(A6=highWaterKey) (A7=mailboxName)(A8=UIDValidity)(A9=totPendingMsgs) (AA=unreadPendingMsgs)(AB=expiredMark)(AC=version) (AD=fixedBadRefThreading)(AE=folderName)(AF=charSet)(B0=retainBy) (B1=daysToKeepHdrs)(B2=numHdrsToKeep)(B3=daysToKeepBodies) (B4=keepUnreadOnly)(B5=useServerDefaults)(B6=cleanupBodies) (B7=LastPurgeTime)> <(82=0)(8A=4822253e)(80=1)>[1:m(^9C=0)(^8F=0)(^91^8A)(^92=1)] <(83=54)(84=PrimaryEmail1@test.invalid)(85=invalid@example.com)(86 =[Bug 397009] A filter will let me tag, but not untag)(87 =200805072155.m47LtAEf007542@mrapp51.mozilla.org)(88=account2)(89 =<bug-397009-254728@https.bugzilla.mozilla.org/>)(8B=\$label4)(8C =UTF-8)(8D=a77)(8E=13)(8F=ffffffff)> {0:^80 {(k^98:c)(s=9)1:m } [0(^88=1)(^8A=0)(^8B=54)(^82^84)(^85^85)(^8D^85) (^81^86)(^83^87)(^B8^88)(^95=1)(^84^89)(^86^8A)(^89=1)(^B9^8B)(^96^8C) (^87^8D)(^8C=13)(^9B^8F)(^8E=0)]} {1:^80 {(k^97:c)(s=9)} 0 } {FFFFFFFD:^9A {(k^99:c)(s=9)} [0(^94^86)]} <(81=Wed Feb 03 14:39:24 2010)(90=4b01c51b)(91=1265236764)> {1:^9F {(k^A0:c)(s=9)} [1(^AC=1)(^AD=1)(^B7^81)(^A4=0)(^A1=1)(^A3^8D)(^A5^90)(^BE^91)]} @$${2{@ < <(a=c)> // (f=iso-8859-1) (BF=customSortCol)(C0=current-view-tag)(C1=current-view)(C2=imageSize) (C3=junkscore)> <(92=12)>[-1:^9F(^AC=1)(^AD=1)(^B7^81)(^A4=0)(^A1=1)(^A3^8D)(^A5^90) (^BE^91)(^BA=12)(^BB=1)(^BC=0)(^BD=0)] @$$}2}@ \ No newline at end of file
diff --git a/comm/mailnews/test/data/bugmail1 b/comm/mailnews/test/data/bugmail1
new file mode 100644
index 0000000000..0a31c0dad6
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail1
@@ -0,0 +1,75 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from caspiaco by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for kent@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([63.245.208.146] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for kent@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <kent@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+sUbJ: Now since Bug 1615064 don't see this as a subject since header name abbreviated
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+: Leading colon caused a MOZ_RELEASE_ASSERT crash
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::63.245.208.146:host29.hostmonster.com::::::
+DomainKey-Status: no signature
+Subject: Only one Subject header allowed, don't store this one (but appears in all header view).
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail10 b/comm/mailnews/test/data/bugmail10
new file mode 100644
index 0000000000..37bb4dc602
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail10
@@ -0,0 +1,61 @@
+From - Fri May 30 16:47:38 2008
+X-Account-Key: account3
+X-UIDL: UID26-1106642608
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Delivered-To: example@example.com
+Received: by 10.110.49.5 with SMTP id w5cs244663tiw;
+ Fri, 6 Jun 2008 10:06:34 -0700 (PDT)
+Received: by 10.114.170.1 with SMTP id s1mr433517wae.133.1212771993914;
+ Fri, 06 Jun 2008 10:06:33 -0700 (PDT)
+Return-Path: <bugzilla-daemon@mozilla.org>
+Received: from webapp-out.mozilla.org (webapp01.sj.mozilla.com [63.245.208.146])
+ by mx.google.com with ESMTP id i13si5593575wxd.5.2008.06.06.10.06.29;
+ Fri, 06 Jun 2008 10:06:33 -0700 (PDT)
+Received-SPF: neutral (google.com: 63.245.208.146 is neither permitted nor denied by domain of bugzilla-daemon@mozilla.org) client-ip=63.245.208.146;
+Authentication-Results: mx.google.com; spf=neutral (google.com: 63.245.208.146 is neither permitted nor denied by domain of bugzilla-daemon@mozilla.org) smtp.mail=bugzilla-daemon@mozilla.org
+Received: from mrapp54.mozilla.org (unused-10-2-11-236.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m56H6SWk004936
+ for <example@example.com>; Fri, 6 Jun 2008 10:06:28 -0700
+Received: (from root@localhost)
+ by mrapp54.mozilla.org (8.13.8/8.13.8/Submit) id m56H6RWT004933;
+ Fri, 6 Jun 2008 10:06:27 -0700
+Date: Fri, 6 Jun 2008 10:06:27 -0700
+Message-Id: <200806061706.m56H6RWT004933@mrapp54.mozilla.org>
+From: bugzilla-daemon@mozilla.org
+To: example@example.com
+Subject: [Bug 436880] IMAP itemDeleted and itemMoveCopyCompleted
+ notifications quite broken
+Cc: invalid@example.com
+X-Bugzilla-Reason: AssignedTo Reporter
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: None
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Backend
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: normal
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: ASSIGNED
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: example@example.com
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields:
+In-Reply-To: <bug-436880-306754@https.bugzilla.mozilla.org/>
+References: <bug-436880-306754@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=436880
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are the assignee for the bug.
+You reported the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail11 b/comm/mailnews/test/data/bugmail11
new file mode 100644
index 0000000000..cfc03cc70e
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail11
@@ -0,0 +1,47 @@
+From - Mon Jun 02 19:00:00 2008
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Return-path: <example@example.com>
+Delivered-To: bugmail@example.org
+Received: by 10.114.166.12 with SMTP id o12cs163262wae;
+ Fri, 11 Apr 2008 07:17:31 -0700 (PDT)
+Received: by 10.115.60.1 with SMTP id n1mr214763wak.181.1207923450166;
+ Fri, 11 Apr 2008 07:17:30 -0700 (PDT)
+Return-Path: <bugzilla-daemon@mozilla.org>
+Received: from webapp-out.mozilla.org (webapp01.sj.mozilla.com [63.245.208.146])
+ by mx.google.com with ESMTP id n38si6807242wag.2.2008.04.11.07.17.29;
+ Fri, 11 Apr 2008 07:17:30 -0700 (PDT)
+Received-SPF: neutral (google.com: 63.245.208.146 is neither permitted nor denied by best guess record for domain of bugzilla-daemon@mozilla.org) client-ip=63.245.208.146;
+Authentication-Results: mx.google.com; spf=neutral (google.com: 63.245.208.146 is neither permitted nor denied by best guess record for domain of bugzilla-daemon@mozilla.org) smtp.mail=bugzilla-daemon@mozilla.org
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m3BEHTGU030132
+ for <bugmail@example.org>; Fri, 11 Apr 2008 07:17:29 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m3BEHTk4030129;
+ Fri, 11 Apr 2008 07:17:29 -0700
+Date: Fri, 11 Apr 2008 07:17:29 -0700
+Message-Id: <200804111417.m3BEHTk4030129@mrapp51.mozilla.org>
+From: bugzilla-daemon@mozilla.org
+To: bugmail@example.org
+Subject: Bugzilla: confirm account creation
+X-Bugzilla-Type: admin
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Bugzilla has received a request to create a user account
+using your email address (example@example.org).
+
+To confirm that you want to create an account using that email address,
+visit the following link:
+
+https://bugzilla.mozilla.org/token.cgi?t=xxxxxxxxxx&a=request_new_account
+
+If you are not the person who made this request, or you wish to cancel
+this request, visit the following link:
+
+https://bugzilla.mozilla.org/token.cgi?t=xxxxxxxxxx&a=cancel_new_account
+
+If you do nothing, the request will lapse after 3 days
+(on April 14th, 2008 at 07:17 PDT).
+
diff --git a/comm/mailnews/test/data/bugmail12 b/comm/mailnews/test/data/bugmail12
new file mode 100644
index 0000000000..e47a410d1a
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail12
@@ -0,0 +1,101 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from caspiaco by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for kent@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+oneLiner: a one line header
+twoLiner: a two line
+ header
+threeLiner: a three line
+ header
+ with lotsa space and tabs
+noSpace:I have no space
+withSpace: too much space
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([63.245.208.146] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for kent@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <kent@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag, but
+ this message has a continued subject over
+ many lines
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+X-Duplicated-Header: one value
+X-Duplicated-Header: second
+X-Duplicated-Header: third value for test purposes
+X-Duplicated-Header: multiline value
+ that needs to be
+ handled.
+X-Duplicated-Header: here comes a continuation line
+ that contains the word 'fifth' we're looking for
+ and then another one
+X-Duplicated-Header: This is
+ the end
+ my friend
+X-Duplicated-Header-DB: this header
+ tests DB
+ string properties
+X-Duplicated-Header-DB: which can be handled
+X-Duplicated-Header-DB: differently than
+ X-Duplicated-Header, so better test it
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+X-Priority: 4
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::63.245.208.146:host29.hostmonster.com::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail19 b/comm/mailnews/test/data/bugmail19
new file mode 100644
index 0000000000..7010ab9a2f
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail19
@@ -0,0 +1,61 @@
+From - Tue Oct 02 00:26:47 2007
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <200805072155.m47LtAEf007542@mrapp51.mozilla.org>
+To: invalid@example.com
+From: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 655578] list-id filter broken
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+ <4DC2493C.4060403@gmx.invalid>
+ <BANLkTin2w8LJxYGHV3_5NpFbsiBhrP96XA@mail.gmail.invalid>
+ <175221688.20110506234025@my_localhost>
+ <201105072315.25120@thufir.ingo-kloecker.invalid>
+ <BANLkTinacQCd+mZ7fL1THLK55X2+u9g5-w@mail.gmail.invalid>
+ <05433510.20110507224940@my_localhost>
+ <4DC5C015.7050800@sixdemonbag.invalid>
+ <BANLkTinv7NvPG9gE1Fha+X+6ZkHzdXdRdg@mail.gmail.invalid>
+ <BANLkTi=6zDTsYymc+bUTwPOM2AohJD2wfA@mail.gmail.invalid>
+Reply-To: FOO <bar@foo.invalid>
+List-Id: Help and discussion among users of GnuPG <gnupg-users.gnupg.org>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail2 b/comm/mailnews/test/data/bugmail2
new file mode 100644
index 0000000000..1591900e78
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail2
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail2.m47LtAEf007542@mrapp51.mozilla.org>
+From: PrimaryEmail1@test.invalid
+To: PrimaryEmail1@test.invalid
+Cc: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail3 b/comm/mailnews/test/data/bugmail3
new file mode 100644
index 0000000000..932cbbc590
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail3
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail3.m47LtAEf007542@mrapp51.mozilla.org>
+From: invalid@example.com
+To: PrimaryEmail1@test.invalid
+Cc: invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail4 b/comm/mailnews/test/data/bugmail4
new file mode 100644
index 0000000000..c256e42456
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail4
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail4.m47LtAEf007542@mrapp51.mozilla.org>
+From: invalid@example.com
+To: invalid@example.com
+Cc: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail5 b/comm/mailnews/test/data/bugmail5
new file mode 100644
index 0000000000..4705fba33a
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail5
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail5.m47LtAEf007542@mrapp51.mozilla.org>
+From: PrimaryEmail1@test.invalid
+To: invalid@example.com
+Cc: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail6 b/comm/mailnews/test/data/bugmail6
new file mode 100644
index 0000000000..c977e16c04
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail6
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail6.m47LtAEf007542@mrapp51.mozilla.org>
+From: invalid@example.com
+To: invalid@example.com, PrimaryEmail1@test.invalid
+Cc: PrimaryEmail1@test.invalid, invalid@example.com
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail7 b/comm/mailnews/test/data/bugmail7
new file mode 100644
index 0000000000..e3e6d59064
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail7
@@ -0,0 +1,73 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail7.m47LtAEf007542@mrapp51.mozilla.org>
+From: invalid@example.com
+To: invalid@example.com
+Cc: invalid@example.com
+Bcc: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/bugmail8 b/comm/mailnews/test/data/bugmail8
new file mode 100644
index 0000000000..ef874ec5e5
--- /dev/null
+++ b/comm/mailnews/test/data/bugmail8
@@ -0,0 +1,72 @@
+From - Tue Oct 02 00:26:47 2007
+X-Account-Key: account2
+X-UIDL: UID18345-1161858178
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys: $label4
+Received: from example by host29.example.com with local-bsmtp (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001Kc-Nf
+ for invalid@example.com; Wed, 07 May 2008 15:55:21 -0600
+X-Spam-Checker-Version: SpamAssassin 3.2.3 (2007-08-08) on
+ host29.example.com
+X-Spam-Level:
+X-Spam-Status: No, score=-2.4 required=5.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.2.3
+Received: from webapp01.sj.mozilla.com ([10.3.4.5] helo=webapp-out.mozilla.org)
+ by host29.example.com with esmtps (TLSv1:AES256-SHA:256)
+ (Exim 4.68)
+ (envelope-from <bugzilla-daemon@mozilla.org>)
+ id 1JtrbR-0001KT-FP
+ for invalid@example.com; Wed, 07 May 2008 15:55:09 -0600
+Received: from mrapp51.mozilla.org (mrapp51.mozilla.org [127.0.0.1])
+ by webapp-out.mozilla.org (8.13.8/8.13.8) with ESMTP id m47LtAFJ007547
+ for <invalid@example.com>; Wed, 7 May 2008 14:55:10 -0700
+Received: (from root@localhost)
+ by mrapp51.mozilla.org (8.13.8/8.13.8/Submit) id m47LtAEf007542;
+ Wed, 7 May 2008 14:55:10 -0700
+Date: Wed, 7 May 2008 14:55:10 -0700
+Message-Id: <bugmail7.m47LtAEf007542@mrapp51.mozilla.org>
+From: invalid@example.com
+To: PrimaryEmail1@test.invalid
+Cc: PrimaryEmail1@test.invalid
+Subject: [Bug 397009] A filter will let me tag, but not untag
+X-Bugzilla-Reason: None
+X-Bugzilla-Type: newchanged
+X-Bugzilla-Watch-Reason: QAcontact filters@mail.bugs
+X-Bugzilla-Product: Core
+X-Bugzilla-Component: MailNews: Filters
+X-Bugzilla-Keywords:
+X-Bugzilla-Severity: enhancement
+X-Bugzilla-Who: bugmail@example.org
+X-Bugzilla-Status: NEW
+X-Bugzilla-Priority: --
+X-Bugzilla-Assigned-To: nobody@mozilla.org
+X-Bugzilla-Target-Milestone: ---
+X-Bugzilla-Changed-Fields: Blocks
+In-Reply-To: <bug-397009-254728@https.bugzilla.mozilla.org/>
+References: <bug-397009-254728@https.bugzilla.mozilla.org/>
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+X-user: ::::10.0.1.2:host29.example.net::::::
+DomainKey-Status: no signature
+
+Do not reply to this email. You can add comments to this bug at
+https://bugzilla.mozilla.org/show_bug.cgi?id=397009
+
+
+Some User <bugmail@example.org> changed:
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+ Blocks| |432710
+
+
+
+
+--
+Configure bugmail: https://bugzilla.mozilla.org/userprefs.cgi?tab=email
+------- You are receiving this mail because: -------
+You are watching the QA contact of the bug.
+
+
diff --git a/comm/mailnews/test/data/db-tinderbox-invalid/cert9.db b/comm/mailnews/test/data/db-tinderbox-invalid/cert9.db
new file mode 100644
index 0000000000..162e60adb7
--- /dev/null
+++ b/comm/mailnews/test/data/db-tinderbox-invalid/cert9.db
Binary files differ
diff --git a/comm/mailnews/test/data/db-tinderbox-invalid/key4.db b/comm/mailnews/test/data/db-tinderbox-invalid/key4.db
new file mode 100644
index 0000000000..1b588f0f2d
--- /dev/null
+++ b/comm/mailnews/test/data/db-tinderbox-invalid/key4.db
Binary files differ
diff --git a/comm/mailnews/test/data/draft1 b/comm/mailnews/test/data/draft1
new file mode 100644
index 0000000000..7ccd5dc806
--- /dev/null
+++ b/comm/mailnews/test/data/draft1
@@ -0,0 +1,36 @@
+From - Sat Jun 07 04:23:42 2008
+X-Mozilla-Status: 0000
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+FCC: mailbox://nobody@Local%20Folders/Sent
+BCC: Some User <u1@example.com>, Another Person <u2@example.com>
+X-Identity-Key: id2
+Message-ID: <4849BF7B.2030800@example.com>
+Date: Sat, 07 Jun 2008 04:23:42 +0530
+From: Some User <example@example.com>
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0
+User-Agent: Thunderbird 3.0a2pre (Windows/2008060416)
+MIME-Version: 1.0
+To: bugmail@example.org
+Subject: Hello, did you receive my bugmail?
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta http-equiv="content-type"
+ content="text/html; charset=ISO-8859-1">
+</head>
+<body text="#000000" bgcolor="#ffffff">
+This is an HTML message. Did you receive my bugmail?<br>
+<br>
+Thanks, and goodbye.<br>
+<br>
+--<br>
+Some User<br>
+<br>
+</body>
+</html>
+
+
diff --git a/comm/mailnews/test/data/external-attach-test b/comm/mailnews/test/data/external-attach-test
new file mode 100644
index 0000000000..f7513963d5
--- /dev/null
+++ b/comm/mailnews/test/data/external-attach-test
@@ -0,0 +1,63 @@
+From - Fri Aug 26 16:47:38 2008
+X-Identity-Key: id2
+Message-ID: <876TY.5030709@example.com>
+Date: Tue, 26 Aug 2009 17:18:59 -0700
+From: Foo Bar <xxx@example.com>
+Reply-To: xxx@example.com
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.3pre) Gecko/20090817 Shredder/3.0b4pre
+MIME-Version: 1.0
+To: asfd@asdf
+Subject: external attach test
+Content-Type: multipart/mixed;
+ boundary="------------080903080202020605050108"
+
+This is a multi-part message in MIME format.
+--------------080903080202020605050108
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+
+<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+ <title>image attach test</title>
+</head>
+<body bgcolor="#ffffff" text="#000000">
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890<br>
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+</body>
+</html>
+
+--------------080903080202020605050108
+Content-Type: application/pdf;
+ name="check.exe"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="check.pdf"
+
+R0lGODlhDQANAIMAAAAAAICAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAACH5BAAAAP8ALAAAAAANAA0AAAQnMMhJqbg4C6k77wJwfRogbsF1huaYsuz6mXRG
+1varejqI+piKcBIBADs=
+--------------080903080202020605050108--
diff --git a/comm/mailnews/test/data/image-attach-test b/comm/mailnews/test/data/image-attach-test
new file mode 100644
index 0000000000..297948ff2f
--- /dev/null
+++ b/comm/mailnews/test/data/image-attach-test
@@ -0,0 +1,62 @@
+From - Fri May 20 16:47:38 2008
+Message-ID: <4A947F73.5030709@example.com>
+Date: Tue, 25 Aug 2009 17:18:59 -0700
+From: Foo Bar <xxx@example.com>
+Reply-To: xxx@example.com
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.3pre) Gecko/20090817 Shredder/3.0b4pre
+MIME-Version: 1.0
+To: asfd@asdf
+Subject: image attach test
+Content-Type: multipart/mixed;
+ boundary="------------080903080202020605050108"
+
+This is a multi-part message in MIME format.
+--------------080903080202020605050108
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+
+<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+ <title>image attach test</title>
+</head>
+<body bgcolor="#ffffff" text="#000000">
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890<br>
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+</body>
+</html>
+
+--------------080903080202020605050108
+Content-Type: image/gif;
+ name="check.gif"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="check.gif"
+
+R0lGODlhDQANAIMAAAAAAICAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAACH5BAAAAP8ALAAAAAANAA0AAAQnMMhJqbg4C6k77wJwfRogbsF1huaYsuz6mXRG
+1varejqI+piKcBIBADs=
+--------------080903080202020605050108--
diff --git a/comm/mailnews/test/data/iso-2022-jp-not-qp.eml b/comm/mailnews/test/data/iso-2022-jp-not-qp.eml
new file mode 100644
index 0000000000..ea8fce9fff
--- /dev/null
+++ b/comm/mailnews/test/data/iso-2022-jp-not-qp.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: ISO-2022-JP and 7bit containing =67 and hence looking like quoted-printable
+Message-ID: <10a2aa17-e92f-417c-864e-575d4e371702@example.com>
+Date: Tue, 3 Apr 2018 19:09:16 +0900
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
+ Thunderbird/52.6.0
+MIME-Version: 1.0
+Content-Type: text/plain; charset=ISO-2022-JP; format=flowed
+Content-Language: ja-JP
+Content-Transfer-Encoding: 7bit
+
+$B8=67(B
+
diff --git a/comm/mailnews/test/data/iso-2022-jp-qp.eml b/comm/mailnews/test/data/iso-2022-jp-qp.eml
new file mode 100644
index 0000000000..22e11ae922
--- /dev/null
+++ b/comm/mailnews/test/data/iso-2022-jp-qp.eml
@@ -0,0 +1,14 @@
+To: test@example.com
+From: test@example.com
+Subject: ISO-2022-JP and quoted-printable
+Message-ID: <10a2aa17-e92f-417c-864e-575d4e371702@example.com>
+Date: Tue, 3 Apr 2018 19:09:16 +0900
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
+ Thunderbird/52.6.0
+MIME-Version: 1.0
+Content-Type: text/plain; charset=ISO-2022-JP; format=flowed
+Content-Language: ja-JP
+Content-Transfer-Encoding: quoted-printable
+
+=1B$BF|K\8l$NK\J8$,=1B(BQuoted Printable=1B$B$K$J$C$F$$$k$b$N=1B(B
+
diff --git a/comm/mailnews/test/data/key4.db b/comm/mailnews/test/data/key4.db
new file mode 100644
index 0000000000..6b71ee5e71
--- /dev/null
+++ b/comm/mailnews/test/data/key4.db
Binary files differ
diff --git a/comm/mailnews/test/data/mail-without-from b/comm/mailnews/test/data/mail-without-from
new file mode 100644
index 0000000000..292b239787
--- /dev/null
+++ b/comm/mailnews/test/data/mail-without-from
@@ -0,0 +1,8 @@
+From - Sun Apr 26 21:30:59 2015
+Subject: mail with no from
+To: from@foo.invalid
+Content-Type: text/plain; charset="UTF-8"
+MIME-Version: 1.0
+
+This mail has no from address.
+
diff --git a/comm/mailnews/test/data/mbox_mboxrd b/comm/mailnews/test/data/mbox_mboxrd
new file mode 100644
index 0000000000..4cbef917b0
--- /dev/null
+++ b/comm/mailnews/test/data/mbox_mboxrd
@@ -0,0 +1,15 @@
+From MAILER-DAEMON Fri Jul 8 12:08:34 2011
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+
+This is the body.
+>From (should be escaped).
+There are 3 lines.
+
+From MAILER-DAEMON Fri Jul 8 12:08:34 2011
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+
+This is the second body.
diff --git a/comm/mailnews/test/data/mbox_modern b/comm/mailnews/test/data/mbox_modern
new file mode 100644
index 0000000000..2d61279a15
--- /dev/null
+++ b/comm/mailnews/test/data/mbox_modern
@@ -0,0 +1,20 @@
+From - Fri Aug 24 11:55:47 2018
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+Date: Fri, 24 Aug 2018 11:55:47 +0000
+
+Later versions of Thunderbird quote things by prefixing
+a space,like this:
+ From
+ From - Fri Aug 24 11:55:47 2018
+This could cause problems, if a reader decides to split the message
+here.
+
+From - Thu Aug 23 09:10:23 2018
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+Date: Thu, 23 Aug 2018 09:10:23 +0000
+
+This is the second body.
diff --git a/comm/mailnews/test/data/mbox_unquoted b/comm/mailnews/test/data/mbox_unquoted
new file mode 100644
index 0000000000..1c8de551aa
--- /dev/null
+++ b/comm/mailnews/test/data/mbox_unquoted
@@ -0,0 +1,18 @@
+From
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+Date: Wed, 22 Aug 2005 17:20:08 +0000
+
+Earlier versions of Thunderbird don't seem to quote things like this:
+From
+This could cause problems, if a reader decides to split the message
+here.
+
+From
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+Date: Thu, 23 Aug 2005 11:17:43 +0000
+
+This is the second body.
diff --git a/comm/mailnews/test/data/mime-torture b/comm/mailnews/test/data/mime-torture
new file mode 100644
index 0000000000..ad540ae0e8
--- /dev/null
+++ b/comm/mailnews/test/data/mime-torture
@@ -0,0 +1,25875 @@
+Date: Mon, 6 Feb 1993 02:53:47 -0800 (PST)
+From: Mark Crispin <mrc@CAC.Washington.EDU>
+Subject: Multi-media mail demonstration
+To: MRC@CAC.Washington.EDU
+Mime-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=owatagusiam
+Message-Id: <CMM.0.88.687168092.mrc@akbar.cac.washington.edu>
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+--owatagusiam
+Content-Type: TEXT/PLAIN
+Content-Description: Explanation
+
+This is a demonstration of multi-part mail with encapsulated messages. This
+is a very complex message whose purpose it is to exercise software using the
+new multi-part message standard.
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Rich Text demo
+
+Received: from dimacs.rutgers.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA21889; Tue, 24 Dec 91 05:52:04 -0800
+Received: by dimacs.rutgers.edu (5.59/SMI4.0/RU1.4/3.08)
+ id AA09350; Tue, 24 Dec 91 08:14:38 EST
+Received: from thumper.bellcore.com by dimacs.rutgers.edu (5.59/SMI4.0/RU1.4/3.08)
+ id AA09346; Tue, 24 Dec 91 08:14:33 EST
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA28328> for ietf-822@dimacs.rutgers.edu; Tue, 24 Dec 91 08:14:30 EST
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA22222> for ietf-822@dimacs.rutgers.edu; Tue, 24 Dec 91 08:14:29 EST
+Received: from Messages.8.0.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.41
+ via MS.5.6.greenbush.galaxy.sun4_41;
+ Tue, 24 Dec 1991 08:14:27 -0500 (EST)
+Message-Id: <sdJn_nq0M2YtNKaFtO@thumper.bellcore.com>
+Date: Tue, 24 Dec 1991 08:14:27 -0500 (EST)
+From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+X-Andrew-Message-Size: 747+0
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk="
+To: ietf-822@dimacs.rutgers.edu
+Subject: Re: a MIME-Version misfeature
+
+This message has been composed in the 'multipart' format for extended
+mail bodies. If you are reading this prefix, your mail reader probably has
+not yet been modified to understand the new format, and some of what follows
+may look strange. You may wish to look into upgrading your mail reader.
+
+However, the first part of this message (immediately after the funny-looking
+boundary line) is the text-only version of the message. If you read that part
+and skip the rest, you will probably understand the gist of the message.
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following comment:
+
+Excerpts from internet.ietf-822: 23-Dec-91 a MIME-Version misfeature
+Mark Crispin@cac.washing (429*)
+
+> All in all, it's starting to look like MIME-Version is not really all that
+> well thought out and is in danger of being superfluous baggage.
+
+Some kind of version number is, in my opinion, totally vital -- getting
+rid of MIME-Version without some similar versioning mechanism to replace
+it would be, for me, a real show-stopper.
+
+By the way, this message is being sent by an experimental version of
+Andrew I'm playing with. I'd be interested in any comments you have on
+its formatting, etc. -- Nathaniel
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+Content-Type: multipart/mixed;
+ boundary="Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA"
+
+--Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA
+Content-type: text/richtext
+Content-Transfer-Encoding: quoted-printable
+
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following
+comment:<nl><nl><excerptedcaption>Excerpts from internet.ietf-822: 23-Dec-91 a
+MIME-Version misfeature Mark Crispin@cac.washing
+(429*)</excerptedcaption><nl><nl><quotation>All in all, it's starting to look
+like MIME-Version is not really all that<nl></quotation><quotation>well
+thought out and is in danger of being superfluous
+baggage.<nl></quotation><nl>Some kind of version number is, in my opinion,
+totally vital -- getting rid of MIME-Version without some similar versioning
+mechanism to replace it would be, for me, a real show-stopper.<nl><nl>By the
+way, this message is being sent by an experimental version of Andrew I'm
+playing with. I'd be interested in any comments you have on its formatting,
+etc. -- Nathaniel<nl>\
+
+--Alternative_Boundary_8dJn:mu0M2Yt5KaFZ8AdJn:mu0M2Yt1KaFdA--
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=
+Content-type: application/andrew-inset
+
+\begindata{text,1672608}
+\textdsversion{12}
+\template{messages}
+I have no problem with changing the BNF to permit the period, as Mark
+suggests. However, I disagree violently with the following comment:
+
+
+\excerptedcaption{Excerpts from internet.ietf-822: 23-Dec-91 a MIME-Version
+misfeature Mark Crispin@cac.washing (429*)}
+
+
+\quotation{All in all, it's starting to look like MIME-Version is not really
+all that
+
+}\quotation{well thought out and is in danger of being superfluous baggage.
+
+}
+Some kind of version number is, in my opinion, totally vital -- getting rid of
+MIME-Version without some similar versioning mechanism to replace it would be,
+for me, a real show-stopper.
+
+
+By the way, this message is being sent by an experimental version of Andrew
+I'm playing with. I'd be interested in any comments you have on its
+formatting, etc. -- Nathaniel
+
+\enddata{text,1672608}
+
+--Interpart_Boundary_AdJn:mu0M2YtJKaFh9AdJn:mu0M2YtJKaFk=--
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Voice Mail demo
+
+Received: from Tomobiki-Cho.CAC.Washington. (Tomobiki-Cho.CAC.Washington.EDU) by Ikkoku-Kan.Panda.COM
+ (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 2.22 ) id AA12299; Tue, 8 Oct 91 07:29:39 PDT
+Return-Path: <nsb@thumper.bellcore.com>
+Received: from thumper.bellcore.com by Tomobiki-Cho.CAC.Washington.EDU
+ (NeXT-1.0 (From Sendmail 5.52)/UW-NDC Revision: 1.60.MRC ) id AA27545; Tue, 8 Oct 91 07:28:25 PDT
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA08355> for mrc@panda.com; Tue, 8 Oct 91 10:25:41 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA00616> for mrc@panda.com; Tue, 8 Oct 91 10:25:36 EDT
+Date: Tue, 8 Oct 91 10:25:36 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110081425.AA00616@greenbush.bellcore.com>
+To: mrc@panda.com
+Subject: Re: multipart mail
+MIME-Version: 1.0
+Content-Type: audio/basic
+Content-Transfer-Encoding: base64
+Content-Description: Hi Mark
+
++Pv/d3RydWlnbGtnYWJiaWxqaWxucHrx7erk7X709Pfx6eXo5uXe3Ojw+Px1cXZra2dsbGdr
+Z2t5bG1wbWxtf+/v8vPq7O3ue3B19efk8vz3fXl1c3RqcHFsb3x6dnZ5anf4+vb+8nt2+vL4
+8ej5+vN7/vH2dPzweG3++23y6vn6eGlneP37fHB+dm1yevt7ev39d2518evs+Pzw/Hr67np1
+fn7+evp8fP3ycmxucXxtaW58/m94/HNtfOji6e/y8vD67+Xm7P/x+G5naXBwcnl4dHx/c3Bx
+eXt3bmJfY2/29v3v6+rj4e3t5eno6/b+8vBxc3JubnNzeWxpaGRnZGZz9vbt6e/8+fr4+vfr
+6/bw7PXw+fj3/Pl3a2x5cXFwfnhra3r5em98/Pv0/Xt4fHn/7env7+vt9/748Ozq7ntpZmln
+ZWl9fvn7enVu+fH4/3R2fHZv9+v5dnB8//76/fr+fn/4+n3u6u59fHlxem5t/vl3dvl89+/q
+5u7n7Ph1aGFjZ25wam13/nt++374e35/+/Lt73lsZnB+/fP38OHj8PL0+nX17vr4b2tnX2dx
+dW5x9PB7c/z9fPTvf/9+dP39ePXn9nN6fHV4e3hydv799f9veXtxdOrk8O/i6/vr6u/49nvx
+92l1925lZWdoaW5sb3FtcXX9/XF1fXd3eHh47+Xt7ejp9fHu7uvx8vH2+vjv63l98/D4/HRl
+YWFhX29+/Xhvb3Bu/+7s8Pn3+Xhs9+bl6vJ+7Ob++Ozv9PDk7G5veWNla2lxcmtqd25ueXx8
+8/J79O7s5u38enN8/HpsbXz87fDr4+jr7u/6+Hj27Hl6fv52cG5ueGxrfff+cf/3eHFvffZ6
+e/Tv8/v7/Pnx+3X/8ft+9ejs7uvx8+/z/Hl3dW14/HVzffv5/v56/PH8b3Fxa2prcXl5/fx2
+8+zs7uzt8fPt93BtcXlwd/708n7q4vLx7Phwcn/y6/Z5efh9efj8+nVzam10dP7+/vjv7fhv
+a2xxfPL9eHJ58vlucO7p8vz9+v379/T5/fn3d2hw/fTz7fP6/fr6enny7O3s+P3/dnXu9n50
+cndtb3l8fP/09PHu8X1sbGpvdnx6/fHv7uzyfPz2d29++X5vaWJv8Ojn4+92/O3yfPr7eHx9
+dm1vefn4ePPu7O3x/Gxtffr+a3H++XFq//X6/n54eff3dW94/vv9/Xb66+599/T57Or6eHv2
+7PR2df9ubG5wd/f+cX31+np5evh9dXRvbnB6ffr39O7s7/Xr5N7h7/5ue/n9c29w++97bWtn
+Z2tqbmxoZmduaWr76eTn5uXx/ffy+nZ29vP57/119u7v7/r15+nr8Hd4e3p7cGlta2tycG95
+fHb4+3hxa2dlbP72fv15anN9bm/68fXt7/Xs5Onx7+jr7eTw+/X+ffz+b25ua290dW5qZWJn
+cHz3/fv++O3u+//0/Pv6fW5oaXX78u7r5+/s6uv8b3ZufH95dXX++O3ufG5ubGty/nh2bW1v
+bWpqZ2187uPo7/Lq4OLq7v10eH57fvt+fnx79O3wd3h4a3H5/XlyamNibXVvcm50+Pf5fH/6
++vz//PTq6+/6d/3u6/Lw6e/48+r1cn16aGx4b29rbX74/H31/2xueHRucnl1cnt4df198uzq
+6ufk6O3u7vp6d/98bGZxefrz7X1oZW37dm1oZ2t5fvz3f/vt6uXn5eno8fd+d/r3/HFzdm9x
++3ptanN2bm97fnZ+efjz+n5zb29vePPs/33r5uXe3uj09/l9cXFzb3Bxc3Z0a3F1cHV2en15
+dnZtbG91+nx5fX7v6+ji4ent7/bs73x0fXl4e3lsb/90cvfyfHt+bGZv93tpbHv18nz9fH/0
+8/P48u328/b07+73fHVzdXv+bmZqe/x+8vL0+HtmZG5we/z8dnv08XZ4+/v99/Dv7vDv7u/4
+9fP6+/r6cG7+/W9namlyfHJ+fnduevP+c335fvn6+u/x8vj5/vn1+P57++3t93lyamtsam52
+9fj6fHzy9Hd5eXpubnpxePx/c3j37evo8/7x7fP2+fPy7+/8cHN//3h3d2tmYWZrbPd9f3r5
+8Pv5fPvv7fz87vDr5+78dnx9cmlmam1jZnV67+vr9Pz37u37/v91fPr4+/Xw7vv49+7q+3Fw
+/nxwb2hsbHX3d29z/nh1/Xx8//f+b3r19X396e7v9nhvcX72/fh7e3d1bmhr9Pb38vx9/vf+
++/fu7X13fvfxdm968uzx9X93/37+cm1ufPl3cm9xa3R6fn58+vj29P379Pj79PH5eG1tb3h+
+bn/9/efs7OvyeH79em9ycmpx9/H29P//+O3z9/Jzb317dX78d3Vxd/rt+2/68u3v8O/6+/f7
+e3d5eXFvcX56cnJrZGt9//b47urydG92+unr7O718fT3fG5+8vPw+Pz9dXVxcmZlcGtiaHZ+
++vLk5urp5uPr6uzv9Pl2dHt0eXhybG5rZ21wfXx9eHx8eW9yfPv68er0dHf2+fPl5ers+fX0
+fHvu7Pj2/HRsb/v9cnBtaWpqdfXt7HZrdX3u7X56fvT5dGr77vD5eG5tanTz8Pbx7fbyfv3y
+/nh3+fvz7vl3eOru/Xl6+3x7amRqb3l0cnZ5/+/1+fr89P14e/h9eOzo9v/49XF5fP9xbHx3
+cHT89+zx6vdy/vH0fX768+/u7PZ+/nlwbWpve/R+bnZ5+PX2eX7z+npxeX12cXB/9e339u33
++fru7Ozo6/D+bW1sbm9tcnZwdWtt9/Hw8/L7/O/w7+35/v7u9/j6c3J5b254fP/+8vH6+vZ2
+cvTq6/1ta2VpbWx1/fvt6vb47/P6+PL3eHd4bH7p6efn6vh/dWtz+/10c3L48P91bHT5//x0
+cvfu+m1tbG1wdfj+8Ojyemp29PLx8/vy7+3xf/fu7XtveHV4bm55+uzs6/xpc3P+7Pvv6Ov2
++v1ybm30831+e2xjbnhzXmRrZ3F49vh19Obl6+vk6OHk4uzx8e/vev55d3Nwa2hsb21zdv/7
+e/1wdn54eW92bGZpdPT39Pj39PLv7Pdx8ufq+f18f3r58fjv+Hl1b/rq/nd5+/r6+3J29/n/
+/HVlZnFua2pwbG1ra3L88eru+evg3eTt7Onq8nlsbHv67vj6/37+cnFncH3+fHR6b3JpbX56
+evz2/fLq7Pv0/HZ8dWlseXv0ff3v5u339PN2evL+c3J+dG5sdHT/8fj07ejr6+7z7/v5fvnv
+/XJzbmt+eG1tZWpvc2pjd/vy7+3r+vr2fPn78urx8e/t73Fvffx1b3lw+uhva3V0bu7s+XN5
+/Pn0ant8//bu7nr79Pn3635oZF9ZbvTm6vXr7ez/3djy73pyWFZeaWZbafj07Ovn8evc8Hll
+bv54fert397k7+ro+3xxeW36c/VxZWds72Jia2tgZG/7/fvv3+Dh5+br7/L37+7vc3d4YWZn
+eXprdndjZXz//nl1+ur2ffvl7vXk7eXx3t3h83ptbPnxce/yXWJma2Zq9P//a/98aHdt6vJ6
+++jndOvtav/q6vv48ulvae/67/NybXb7ev5oc3Xt6/l0c/R5dmBj+e91+vR0eu/s8n5q7+97
+dOnc6e7t/Xbq7W5obHL4f/Z4efLq82JkYWBlaHr8fX177Xp98e3u7+v55exz5fLtbGd0d/fy
+5/12+f9yamJlZmtfe/b9+291b3V67fHh2Nz529/k4G9oYWFqbe/xX29qYmRiaGhsau3ffeXh
+8Xdx7//6aPfv7+Pk39/vfHp0Y11tZF9le/pt+nT3eXfi+G5sd+Xt7t/h3+p2bn5wbe7xZXFt
+c2pcd3xuevz56+v06vRqcvDpaWFl/f/02OL/dvFsWVxoeP9sfurx8+zw4+/5/mVia3rucm3v
+3+f+cnno7H3ofGtja/f4dXPv9HFzY3F2Z3b2/mp57vRveX3w/3h0bGRedN7g9Xfo5/jt8Pp7
+9PX4+Xd+7uz+aGru83Fob2xgb/7zeG58aWRjbfdxb3Ti4e755+Pq9u3n+Xl2cWRhdfjn83l7
+cvbu9e90d2l4b2Np/elpcOrofWv+fvxubvDy39/x7+59fHVraGz46vly9PNzbvX/b3Z3/nr4
+c2tva2757v396+d4bX7v9W98ev9ya+/q7/Lm5vp6fHZzfWxrenByc/h2bG956fP59H77bPvu
+c2JfeHV33+bn6ezn6PX86vl8f3l1bnNvbmdld3Zw7vtveP53fPx/c2l+8vj6bGps+37v3uTt
+c/Po6+/x+vl+a/J5Y3P2eHP+7e1wX2zzdnbk+Gpm/fRod+/v7fTxf/btfHX08mn6emto/Ob5
+ZvXm7/P17fnwfPPueG1u6PBu+nRxanp5bWtp7uxgavPue3Dr9/By/+5/emjv73Pw3ulv7t3g
+7vh/83Jf9+P8anT4dm9zfft1aXJrbmhfa/59eu/p73j25e3n6PHqemlq8HZu+Org+X7tdnBq
+be77bnXs6XJv5/dr5+X5ZmzraF198Oz4/eLw8e92/Wxp9X1sbH3mcl1sd3RyZ3bsfO7n+Hrw
+6vfu6/Zn/fdq8PHr4+n6cnn7c/j0fXZ+/Hb99fRzc2Zj/vZmau3md2h5dGb2/OTl/ff482v+
+7ejx/+Htb294emt5c2pudPt5cm9t7O5zbvzq9Xh85u537ufh7ufa7GZ39/dvau7x/n1pZ15o
++f1x+fdqbHdyZ29la+/n/Gvy5/p829h89/zu8H3z9uX5efD69G1p/m9pa+v1cPj+8Xvw6ft5
+bfTnd2lo+vhleHN1/nHs5Pr+7+17dvtpZnb+a3Hs7PHq/Or5fOtz/nTu82NxY2vucHxxe35p
+b3ns4ujv5+Dz+fJ+fvv1+2tr8vxvdOrufXFxb2r7dWzwb29wdHhnb/Rz/u7w6uzu+ez9Z3z1
+7vrw4OX5aGv8a2n75WtreGhseHzv9W7m2979f+7zfnzr9GljeX1dYnL5aWnu8nB8+nNtff3o
++F7+4fJv6OPl8Pfd82x3/e90a3ri4nVvc/lsX196fFps8f5tbezl+3bi3PD44O/1835sam/6
+9G5rfG9ob3H6dWv99ert6+bj/Gt9fmxoeft46/Ts53tw7OloaftuZ3H59f3q/G364uL6++J4
+/e70/2t8fnd58H5tanN2Z3JtbHp+7P1v8u7ke/3l6vx4e/t5aXZ9bW939e/17uvm5/X3/fj7
+eXt+bHR5a/59Z/Tq9m1w731y9/X+8fP49n929+b/a/Dv93p8e2d67Xl4f/x2fvB2+/bz9Px5
+fWtxe/b2dnnu9WJpfG14fHx+cujh5vZ18Xl58fH37Ozt+n1+a2dq+/n77vdq/fDr/Wd7+XBq
+amVnbWt39/r0dmt4d2n36PDr6d7q9+bs8uvuePjxfWNmam12bGhxdG9xbWdqcGx+dP3s6/T8
+4uLp4N7j7357b29oendjafz6+HhuZWFrbWxyfm7293v1+eHl/+rj6/lv+3p2aGv6+/xucO7w
+eeXx9exybHv+cvHrfvH2bnJ8fm52cXF9fez6b2/t73FrYnl4ZnTt9Xp7/HNq7+Tu//n+391v
+dOTq+Xju7m149XpoZnz29WNn8npnZujrbXb/9nZndOpzXmn983555+p2++fqffD27OL2/+n7
+dvdx8vpsY2j4b2f67XpobnV0am/q/m76fXL8fW9ye/jvff3h6nz15fb37Onpfnzm6Plnc/dn
+YW1uZGvl6GlrbflyXGz0bGl+7W/v3Pnx5+zd4+vx5u1rc/T8fG52729ycnr1X1txbXd6eHJ0
+9e/s3+t0fe78c3r35Xpp8+jxZml8b3Fw+nhrcfji6fbn4eZ69O52+nV2/XN4fWps/m5sc3T4
++mZs/W5pZ25mff326O/q2Nbn7Onq++/3cm9mZ3n7eGBt5/drbeXoYWT0eWBifntqZ+7m83T7
+6W9obW70fvzr7PHq6errcW7p6n129u7/+vHz7ebyb2hgbvz7al5xdHJtb31uc/59/3lvenBo
+d/Dydv7r6/p7/ebffX7h5np14eB7aWvj6mdnY29zanT5dWpqaGzu5Ot99+ry/HTz8XN5bWr7
+93B6/nL8f3L15vBy7+V4am/3+XB3fPft//fzZX3q9O16/nttd2lmb2lt++/1ZG/3b3D/5uLp
+7erh8vn+7Onq6X5oYmVnbP95eP92b35oa3xtfPZ7cu/m/n/d2/N1++vr+W3+7HtiY3l6aHt9
+Y2Nx+nVrdG599u/e5e37efDv5u978ez8enNudW9uaHh1d3n77n5vd+z1ZG/v8Ht07v18b3ng
+7W5rfP5ubvXn7/TpfGtv9O1lXWdy+3x97vXi5fLu7O35dWxvdWtvc27t7n3w4t/rd2/z/HBt
+e+9tZnrveXLr6ezvbmhqZGJoenZs/Or3/u3x9Ojv/PL8/f3y8+ni7H579X54eW9x/mxl/en7
+cnL88W17+3l1a3jufGNo9ez58PHq7Orq7OXt6vlwdG9peP14fX/y93pu+Xtobm1udvbybGty
+b3hzbnB17Or//O/u6u3t4ebp7e3q9/rt/Xd1fHFrZmN3/m9tbPv3Zmf+93Vu/uv5bPbo92t1
+5uPp/u/r6Of4+PlyY2Bscmtvb/bybPXld2lz/fV8/3vt6Xp3/fD08u3q+P745+t5a3Xq+29r
+/vF6cW91ePf3cWxy9uztd3Hw925kaXp5b2x57PR++Ovz9vTu+vz+dHn96e/39O3o/m779P9z
+cnBr9Hpuc255dXN8fXxxbHN4//7t6nX87vf1+/ry7+7t9PZ0bm1scfj2e33u6e76eHv4fnB3
+bmZw9PLx7+7ucmRnb3FycGdv7e/v7uvl93fxfHBzef3x+fjy8PRz+PP0eGdpaWdqeO74cHrv
+eHZ2c3327nx5e3Tx6Ozs5d/n8353bXP6fnl1bm59+fr47vb4+XRwbHJuZGdfXmZrdHrx9H7z
+83x7/PDp5/L46/D68+3s8vb48e33fvPp7n389f14fv12d21pbXFqXl9qbndtcPft8/5+fHx6
+fvj28P/z63pmdfr9dXjq6vf27ufr9fXt6/H4fv/5fXvw9Hl6fPR7aW5+9nh5fX5waWtrZmlv
+cXRvbvPl6e/07vZ9/v707nb/93d7fP38+XZ19+98bvro5er0+Pr8dGv473J3+vt99flvdH//
+/3h99vr4e3h2cnV2bGp0fXV6+nx1+ers8/T07eXo+nV8fnd3e3hucXhta294ff338O/q5Ot4
+aGlucGt59e/s+Pby9+7u6Ofq6fV3c/zz//r0dmpqa2tsam52dGZpeHB2efjr7Pf++Ozy8/Dz
+6ur0eff+cmVkem1qcu3o/v7u8f9tZG/v9H76+HP06vPw9u3qfXFycXFrdP7+/3trZW357+x+
+evv9dnP79/r78PB9dn7y7fPs9O7yenN5/X13bG51+HpscHd7cmxxcWtr/PL07Ovv+Pf//Pv4
++/339/t///jp7PT9fnt6eW568/v49Xt6/vTw+H/19nxvaWhua25zcvnu8Hz37fD07e92enn+
+7uno73p6d/rz/nZsaGZscmZt//18fHtveu72fenq9/jz9n/v4+bm6vZ5dGhjbnF7+3Fqb31x
+anBvd/Lt73t0fu/y+vX3e3v48vPt6e16d/x/cHp5df707PTx8fp+dGtsfflubnxzbm91+Pt4
+d/798+3q5/xvc/x+cHVta3X87+3s6Or8/vDzcGRqcXfy9Hxx+/P++/jv7/T5//Tz/X/9fHX5
+/Hx9bXj9fHb7fm9saWtucfj8amtxffR/+/Pt9e3k5efi5vn+9fhzam1ycXJ6fXx//P17a2d0
+cnH//vb58fp9+vX6dHFpaXzu+frs/fr1+nxvdPXv/nvy7/r67+rv9+17bHJ5b2Zrd/19eHJs
+Z2Nren95+u74+unl6/T++fP28ezy+vDv9e7v9u/+dnf/eG9tdnpxe3JsYl9kaW5rcHdrb/19
+++rm6O3r5+3w6+z7eGxqd/r16+js7e9+/f90evv/a2hraWpsbGlteXx4cnF88Pf88e3t6+v5
+/e/v8ff17fDxemtucXt6en359vf3d3JzcGxoa3D7fHP+/Hjw6evq6+739/Lz7/D08+rwfXFr
+bW92e21pamt0++33+e7r5ejs+Pj3+v5wamlsd/v6cG93d21udWhs/O/x7e72+/729Ors7eft
+9fb07/75+f10c3t9cmlrb3NpbHV4amz5+v59+X577/T7cnN6d313efrq6+nm7/Do6vD27+n8
+c3Jydm9teHZvdnJoYWlvbGlpdnt1//57/e/p7PZ97Of+bm92+uji6Ovm5Oz39vLw8nt5ffR5
+amdfanFsc3FxdGxrbnZ6cG//dmhtfXv+8Obk5Onr7/3+/vf7+Ovn8Pf2/O/weGVueXp2cG5v
+aWx4+Ovr7vh9bWhqb3dvcP59eHh7ff92b/nu9Ojo7/L59PR+fnr/8+/2eX1w+u19/vv38u56
+cXBvamFrcGtmZW1t+/h89uvu9PJ5fe/w8Ors9Ht4+fPu8/X9ffr+8vtvcfR8bW1ndPt+dHP+
+++/t+vn8+/f+/Hh5enttbHf79HtobXh5+/Hu7+bq9Xlscmx58vh/8Hx16ufr9Ozq6OLqfmt0
++m5eXGRtc2VZX3L253xmcPrk2edZVF753Ojt3NDM0N7g29zo7l9Yd/H8XFRdbVxVRUFV+eDu
+aGDe1t57VV/l2dnb2MrCxMjR9Wtq/u5kSUlobuNNOD5R5tfvVHfb3t1mUVf51tpz98/CvsDL
+1t7g5GNe+PxOS01MRDk+T+DP0t/u3+vq82ZRSVD22MnJy8jBvL7WaGpw1tLdaENAQTowN0jV
+wMvj9eXd3GNYTklU793FwsjHycO8xXlnaNrEyutMPjYyLDFUx7vA2evX4vxUS05LWtx8z8PD
+w8zOwMDo8urMvcfnVTkxLSgz/ry3vt3jzuZfRj5ER0/e18e/wcfNzL/B6djHu7XCVz00Ligk
+Ns61s8Dw3MzpWz04P0BK1sy6t7/Gz9DEyfXHvLW12D8+NigeJFG0q7nXb8vI7kIzOUlDSuHO
+sbS+zuvavsPnv7Wrr9dDMioeHCvYrq6618rB2lI5Mz5URj9cwq+vv+pf+MjIyruuqKvEPS4o
+HBcp26umtc/OxeZYNS4/b1hOTuayr7bIVE3bysKzraipvT4xKB0UHVGrn6u+z7rNZDQpL1Nm
+bWNMurOyv1Y+XtHDr6ukpK9QOCofExU1tJ+ntcq1vmw2JStH2t/VVb20uMFXQVHWz7Kqo6Op
+0TosIRcQI9Cjn6q+vLhfOyUjMmrmzNG8sLK9YEFCcN+8qqGhpbVDLSEXDxxbqJ2nr7Ouy0Am
+ICpI8d7fwq+uuf5DP1FgyK2in6GrbS8kGQ8UMrOeoaqvrr9KKh8mOVXf0sWurLLNSjtKWNmv
+o56fpsoyJxwQECbAn5+qr663cC8fIS9L5PXHsK2wylQ8P0Z8tqWdnJ+2OSweEg4bYqOdpaqu
+sM0zIB4qStlu2ruxq6/TPy81VLmooJ+iqso2IhQNFTiqnaOqq6m4PyIdJT394N3Bsq60wGQ5
+NTzPq5+fo6qzzS8bDQweuJ2dp6+pqWAtHh0uS+zOwLavrbbXOi85a6+in6OvtqmyIxALDkOn
+oaiusaKpPCMaHz7C6Eu7q6Ssczo4OETBrKGhqayqp7wbCQkYuJyfsrmtqLMtGiIwSkc5wqSf
+rMs8OjwzUq+fnqrLvaqjpDoKAA7Ik5S6Rr2pr/0gGyQxT9bHqp+tw0A1SFLZr6Wor7+3pJ6o
+WQ8CDCyglqfoxLTH1S0jKy04UsinprK+0nFNOkG5pqWzy7ypoKW6NBAEDz6ima5sy8RERy0s
+NTFBuLu4sb6srus6OU6wp7C0tK6oq8JSOi4PCia6m6M/L3BYOzwmL23FveBXtayst1lHVOG3
+pqertsu6tstJQ+7cGg4ix6ewLipCNy47OsyqwlZMVLKsusHW9sO7r6WotdtN6tt04c28uE4a
+FCh5sUYfHzNhP1nApKZlKy/OrLDDuq2tt7ivrLxGOUpazLi/tbtbZDgbHSEmPDEvTu9P2r2s
+rVsuL0+xp6ikpa7aSlHLw+tZYcq9xd/Jw8tMMyUZHixS3U1Abtc5U83IwU1Ku6yjpbTFa0dK
+z7+ys8vYaFnVx8DcOi4uNEE4Kyw7XeZONjQvObenqLHFxcjP5cfAyNLPta+0yU5ubVdRPjdM
+anPc8+BOLiowOUU8PkTxv6+wvdZFQ1rWvLu+uLi5wcvebk9JSkVf2NPfVf7Szko1NTI5R09e
+Xc3IxF87U8rLf3rHtrzOeMvBy19JZu7O39bjb05GTEzMwt5JXVNVRkbZ4zUsP9K82O3Du8pH
+P2zEzOrb1dHZ5s65vlw8QGjO2N3X2/LX0dJdPTo7Pzk8ZtLN0+PYbU5IX8zK3W/8yMnP1si/
+wmBS09912cnL01Bc/Eg6PU9qQDRF+9TZVEdWV2vVxrzA4HHxZ9LUyL3Dz+LJy8/5bnnf7FY6
+R0k/STgwPWRnz+Rj7X7uz93l282+vutuybi3w9vVy9p659HrX1w3LDk/QT4vL0lZ4sDEyNpx
+z9Tr0b65uNTbuLjCz+PazF/v0svLNCs1Ojk1MDtIQE7OwLzPWt/jzr++ub3Uybe6zfvVwcXR
+wsY9KSsyPTgvPPlFPGHawMdVUdPLxb64r7vbzL26vcTQ1NG/wDYiJzjkVCsnR/7y3d++s+I7
+SdCxr7fG1eXLv8G8ub7Q39MvHCA5xMYzKUZsT+u7ub1SOGa6r6yrt89Zb7uyvcvlarxGHBwo
+UsNGKjno2M7hu7jsPD3frqmtrbnU7dC/t7m7v8A2FBYwusw1LT5oOzzNqqvXPEnGr6utrLTG
+Xt64sbCwsj4NDC61sFMkPas6Jl2ypLUpK7yppKmsqrZSTb6vp6q5HQYRs6PLKyTIqSwiSqug
+ySArrp6graulr0RFt6WdpiEEDN+isjMp260qHTS2pcAnK7ukoqmnoarvS7qpn6IlBQ2+o7g2
+LrCiJRotUbK2LC65qaesrKSpVkC9p5yeHQMSsKjEOEqfqRwbOmLRVTHqra2trKShsT1Ot6yf
+xAsIVaDHLC6mlU8UJr3uOTFEraq6r6KgqtRGvayqtxMHIaC1OTezmbsXHcdrKS5jq6nOv56c
+p7rbvq2tsiEIEqerMzmwmqYdF0rBIRteoKfoXKaZqV7FrLHJuEwPDFefzzTeoJw7GzLNKRkr
+pZ687queqXHerKvAvu0VCiijyTTNoJxZHTPAJxUlp53CdqGYqUZorKzHycweDR6vyDbPoZ7O
+JjzDJREfrZ+37KaZpFltra/GwGsdDh+x2zDRop/wJ96vKREbvabEu5yZp9zhr79LuL0eDh2y
+0i7Lnp59J+urLRMc3KvI152XpL7Fsr5BXGEfECG9PTGvnaRUL8WuJhUk5L/ouZmXqravr9o/
+5NcfDh+0PCa+nKBMLL6oKBIjz8dNvJqXpbCqq+I85XcdDiOyLyOymaNDMbmsJRUn4nM7t5ia
+qKmjq+w8WVgcDiW2LCKymaRFNrawIBYs0VA2vJmZqamepdVC7esjDhyzPB7hm6FSMMOpLRQj
+02wvXp2Zp62fn7lU2MwpEBbH6B84npzGNGip5RkaPck7M6+ana+uoajNesxJGxAovzImxJ+o
+00DJrzkbIUbyTu2unp2psKyttLppKhgaOj4oMbqrsL1vz8o1JiYwWbu1va2hoqy8ua2tXSAY
+IEFALiw/vauqv1pOVUc1MDzNtba0sbCxsa6vttc5JyUzPzQpKjVqtrbCyf5Z4s/O0u9r6t7u
+6MvCwr60rrfIytlINy0oJiguP1FIVcq7trW6wMvdfNXIxsm/sa2wus9TPjgvKiYlLUdVTmPh
+0sW/v835aOjRz9DHua6ppqq+VktPQTIqJiYrNU1uWV9r28jJzt3qbV3ty7u1rqqssLe/b1F4
+VDwuKSouNjs9P0hRZNPIz9vaz8vHvrS3x8C0sLS5wtv0YU8/LyooLDE0Nj1NTlTcv7m8vbm8
+0eHMx8q9sa2vu8PL129URzIpJCYsLy8zQvTNvrSxu8rL0u75ybi6wL2zrrK8xs/Y/U00JyIh
+Ji45P13NysrCvb/Ix7/A0drIyczGurKvs7rDaElHPzApKSkrMDtCTGTgzL22tbW5w9jq5tfR
+08i9u7q3u8fiW19lSDUtKScnLjpFWN/Lwbu5uLa4v87mY1/95OPZxrextLzG3/7jVDcsKScp
+Lzk/T+/Qxbu3t7q/ze5eXXnj9OfMwbq0tbq/zODhfEMxLSsqLTY+QE7jzMO+u7q8y/tdVE1W
+aGzfx7u0r6+2vMji6+JLMy0rKSw2P0dn2dXMvby/wMruUlNeX2Jv4MW6tbK0uL3L3NvfTzcu
+KygrND4/TeLT08K4t77L5FxfcF9SWc3Cva+qq7/2z3E8PEQ+MywnKz3dura/4VrnyMHPTj06
+RWzU1OzJr6ehn6OxWycYFh4sOUVPXr6knJ6tTysnLz5FQztDy6+suNpOdraopqipr9I1Gw8U
+Ij3fvLS0qZ2dq0wnHyM1U1RSe8Kuq7dUPlDEraWjpam6PRsODx5Gv7Ctrqqem6o8HxsgN9PM
+YX6/r62+Py46yquioJ+ksGYgDgsWO7mtqquso5ugTx0ZHzbGuutFfretukEtNcyqoqCfn6rD
+LxIJDCmtp6moqaqgnbkfFRsvzrjHRUDGqqx5Ly5OsaKen6CjsVQeDAgR55+hqaioqqOlRRgS
+H+u0vl9Iabamr0ApMcuso5+enqKsYBkIBhW7naCop6mqpa8sExAlu63FVVzNrqWyNyU0uqah
+npydoq5MFwYEFa2Zn6mko6qtuioRDyeqpts2Tr2uqrU8JC63oZ+fnZuiujoWBwQTqpSdq6Sf
+qbrRKxMPJqSezC5BvbOxuksoLbygn6WhnJ2tPBoKBQ3Uk5arr6Kkt1IuGhAbtpmmNy3SrrbX
+WDwzTqyeoaafnKY/HA4IDCuak6W/qp2pTCkeGBg5n53ILEWsqc8+Q0dIyKWeoqagocMjEAoL
+HKmWnrq1oJ+8LR8aGiiynao+McGotj8zQlfUq52do6elsTEVCwsW35ycq7GknqpKIhkWHt6f
+oX0vbKmqXjA878OvoJ2gqamtWBsLCBFHnp2tsKGbo34mGRQaP6KexjBTp6XqLTJlw6+fm5+q
+rKvHHgsHDz6fnayuoZuhyCsZEhYypZ68NVyno88sLU/Er6KcnaSqrfMaCQcUxJ2ktqmbmaZl
+JhcQF0Oio9s6vqGnSycuW8Oun5qbo6mtVBUGBhmtna24oJaZqPwmFA4XbqGsSFSnna45LDx8
+ya+fmpygpbEoCwMLOJ+m0rWakpq0NxwQDx+6qdo9s5ue1Ss02cvRsp6Zm56iyhQDBRqrpWJc
+n4+Pn2AiFhEZOL9fOLqambcsM8G9TUytnJucm58uCAENYas5Ka6Oi5a5Lh8XFh8zNS7Jmpas
+MTuwrFUzyKOgo5yXrxEDCSXBMR5PkomOplczJhoYHiMmVZ6Xp0t+qKlKMMGirMWkkpgkBwcb
+TigVIp2KjJ27vccrGRYYHTOonKrVuKCoQC+6n7Yz2paNpBYIDiMvGxUvmIuSq7mlpy4UEx83
+TOnBr6ensltXtqe3OTysmJquKw8MGF/jHBe9jI3BJLuYuxQQP5+/Hyijlq4oLKycsTEztJ+k
+sLlHGRAcTDsaHa+SnT06pJ1HGyawqzglXKKnUC/dqq30PWi2rrK1vEkgHC3aOx0ivJ6zNUym
+pEMkN7CxPy7/rro+N+O5ykVIx7K0vr7DXzg5X+Q/M0u/vmpWxbteLixB4FtCUsnB313ev9lE
+SdzCxMG5tsdSSHLXVz1F387m9Me3wT8tMk9pQTpRyMtZTte/zko+ar66wsrEwc94Ym1ZTU5l
+fl19w7nNPjM+W1FAQWrQ23Tdxcb5SErpw8DGysnM3XNcUkxNUlxSTu6/utFJQEhMREBKavn4
+08G+zmBPVvXazcO/wszV5XRYSkdGTFJh4MzHzu1USklKSEdMWuPLxcveXlBacu3ezL++x9LV
+1vJOQkNOV1Zd68vG2FVMUFZOR0tY7c7Kzd9dVGJ//nfbxr/H0dLO31JESV90WlN5zMvuUFZu
+Yk1IU2/k2dHS5F1XeexvWGbYyczW2tPgVUZKX21dW37X1/JiaHplWFRf7t3V09Xle+3d8FpV
+Z9/T0Nje2uBnUFBeb2ZaXnjtdltZX2xlXmPw4uPpfW/44t/pe2Vu5dPO1ePj3e9cU2Hn6FtU
+bN7mXU9Zev5fXvvp8G9qd+fsc33k3+fi19HW6fro4WlWYfLwXlRn5eZkVVtoZVxm69/+YWN8
+8m5pc+vj4t/a2N7q7eXh7nBz6up1Y2h3eWpeX2Rma/vp7/9wbv9+Z2Fpc3/06+7y7Ovs5eTy
+bnDv4upvZW93al5cYfTf6Pv06+v3d33+e/v5fH9+8+zz9fry73tqeOTa4G1ievRmV1dj/vNr
+XnXk5HtnbnPv5+zyf/bj3+3+6tbZ9mBfeuftbWv7fmtcVWH54u93+O3zfW9v8v/37v71bm1+
+fm9sbltoeuje7vLt7+Xs9vL+6+bua2vk1tr6bHz8/ltPVGX7b3R07O1/Z196eHRqYmlrbnfw
+7e3c2t/j7eLd6PJibOzh6OLven76Y1taXmZy+Pd3Yl5uc2Ry//ntfW3y6e/e3evraV/56uf0
+5eLv62peZvbr+PbvcXJ7efpmUklW0MTWY2Pn2e1sZ1tOR0/e0N778tjS2uHh/ndsWmB06+/1
+9e/v8PDp92jo09X4WlBd5P5MR1/Rze9XZN/sb2hfbGpsdPp2bPLs6+nm7fni2tnk6vtxb2Vk
+YV9t9d7W2t/d5ndpdft+f+Xk7ntzbmBXUVJVVFRVVlRYWltdY33r2c/KxMjKw7/BytDg1NDr
+6N3halJAPUU9MzQ/QUFKXN/IwMXIvrzH3drX9ldLVO3pY3nIvcHFurG1xMzPSDI0NConLC8u
+OlfYwbSusK+vtMLP6UU5OTk3PE1wxbSvsKuoq7a8wUIoJy0gGh8sLjZnvKyko6iloK3dXk4u
+JCYpKzJE5rWop6iinqOstr9uLhwaIh8WGSxEQtWqnZucoqajrkYrLCgeHiYuPc62qp+doKCe
+pK+70zooHhgbHxkXJ/7ox6idmpufqquyPyMhIB8iJjDdrqupnpudpamrscFbNywnGxYdIxsd
+P8PAraOem5ypt7TRLh8dISorLFyrpaupn52ksbq2vlpDRjclGR0wIBckzN92vaudnai7q6jQ
+LCcrLS0oNMKssbWooKSytbKy1URN3H41JxweLCQaIFXZ3sesnpyotqyt1C4pND8yLkq6sb24
+qqSsvLy3vlY/SONuRjEeHzQsGyBF287Uv6efrfq+qbc8NE3PXjpIua69ybWpr9xkysloPThY
+1mj4Mxww0SYbNO7gzzhVqajg7biutGpA5rnPXeq5sLvXyLjITD9s3G0/RcfB604vK1RAJytD
+SFw+L2W0w1/JuK204Oi1s8vNwrq83Ejz201ATmN97l7f0cz451A8QkQ6MTY1NjU2SO3Kv7u4
+sLK6v7y/wcr36+NtSEpJ+PhWa9XPzu1y29nY6GxPVUY5MjU8PD5BaMm/08y6vs7OxMTG2Wro
+7VNSWFb/6Vluz9Luy8jb0NbpcFReTjw8UktNWFL51Ob+3ex95eNb6ODr39luY9njamtp+Njn
+8dnPz+Tez191dUxVUlBMYGjw8d3c2c/fWnHeXnN2+WpxZlpbak5S6ubsd+XT0uPW2PLw9lBT
+VmlUTWHW2t7M2szDd1zP205R+3JYUlVaXFNNXupiWerW2dLP1czI3G30X0xYXlNmW2Tl32bb
+1NvX++vxYufqalpeXfxPSWZ7VVtq7cnM29zCzf9l7l9OUFVgc2Rp7e3xaPnf4ebp6ezf1Ofx
+3XJeWlpMVGxfY3fyb+fT1u7f1G1dclxa+/hb/Nfxct1veOjde9rT3N3ffmBmWV9dV1tdZ1pv
+7Gxk/tPm/+Pj8OrcamzU22By9G5v6nNhb+RsYunW9O/p9F9dflxdbmVxbG1q9u5r7dHPc2rX
+3XfiatfqcHLz+WZXXm5x62tjYNnwX2v94PRUaPxubFzp7Xty+efs5+Xr53zl9X3W8drcdfXe
+al9o63NmempvbF9k+etZX25+aGx+cvp7eWjz9X1eb+7x53nZz+h41tnb5e1fdt37ZO3weOx8
+Z1x6YF1ga1xm7mTd+uxv7ltd5uheXOzi2Httc9Xu5lnk0Ol+6dzY3WBn/XprUktn2FdSYt3k
+9+Pi7NZyUOnVXE3ubuzY4mnh5d53at7p5G3o8u5oZfFqVfFjWmRS2GjecuDP7HxKz93mU+rl
+7mVNeXvtWlrm0Vx8dMzZbm/a0nxgbd3pV15t72pV3eD3Xfbx33Ve5O3ucV9reHBjWVnX0WRX
+5MzbbnHa0dNtX+nka2RbXWn+W1NcaPpST17gXV5v/n7p2eLd3s3LzsvIzNbT7c/J39zN0Eo8
+OTs7NzEyP0xYYt7LvLzCx8a9vcr+49XW7+3ezsbCxMe9v9hEMzI5MSopLTpKXu/GuK+vusfK
+ztP/T1Nm79bt0r+9t7m8vbe8ejstLS8sJyUrPHDnz72wqquvvMruWE0+PENKWerdxriysrK1
+tLjLUzUrLC4pJiguP/nTxLeuqau1wct2U0Q7PEBJWmvVvbWzr7GztLrHUzgsKysqKCgrOm7P
+wLyzrKy0vctcTkU+Pj9FW9vOvbaxrq+ztLjE7j8uKiwqKSkqMEjuz8G7sayutsHcY19NPz4/
+S3zazLy1sa6ytrm/y2s/LystLCspKy9BbN/JvbKusLnBzu90VkhCQ07nzMbBuLKxtry/wMv4
+QjIvMTIuLS0zQVVe5Mq8s7W+w8fN0mpLSE5f6dzWxb65ucDBw8TL505AOjo6NC8uMDdDRlHs
+y765u768vcPO52/7dG1+e9jGwsbOys3GzdPreE8+Ojk5NTMzNz5GW//dy7++u7u9wMnPzNz+
+eWFr+Ojr1OjV2u/Y1srf3VFGRUhAOTg3PEJNVWf1zsG+vr/AwsfT0upwb11UXl30ZN/44c/W
+yM3K3uxcTU5IQD08PUBGSVPrzb+7vcDLytLyWEtPTk9GT13j0s3KzcTHxcvKyc3WcVRNTEM9
+Ojs+Rk5bbt/MxsbO3fF2X01KTlJgZ2nczMfCwsjCxMPJysrO3XBXTEpDPTg6PUZNVmj31M3O
+3fVhaGRWUFRZb+rf183GxMbJxMXDxsjKy9p1T01NRj88PUBITU5SX+vZ3XpmX2pdWVVaaunj
+4dXLwb69vr7AwMTGy9psVE5JPzo5Oj1ARUhOXevd5vZ1ZWdiX1hf/eLWz8nCvbu6u7y7vb3B
+yu5MQ0FAODExNjxCREdOb9bP1ON7bmhrY1tb+dTNyse9ube4uLu7vb7E4kw7ODg1Li0wOEBM
+UVzpysPFzNn4eWZZT09d38/Kxr+6trW2uLu5vMHXTjgzNjMuLC41QU1ZaN/Hvr/I2mlbV1ZR
+SlXyy8TBvre1s7K4ubm6v9FNMy4uLywqKzFDY9/Yz7+4ucDdUU1MSkVGT9/Evr28s66ysLO6
+usDCfT4sKi0uLCksNU7Xzc7HubO3xG1HR0VHPz9P2MC9vLmvrKywubu6vcxzMyYkLC8rJys+
+3cXFyb+yr7jbRD1GRT05P3XBvL69tKmorbS0s7jK2lIuISAoLCkmLUnIvb6+t6+xwk44OkNE
+P0Fqwbu+wrmtqKqxt7e4wONtZTEfHSQrKycsUr23u726sbXMPTJAVlJES8y2tr7Etquqr7y6
+tLnPYGLVaSYbHysxKSMyxbO8z8eyrr4/Mkvd3kVA0La3wsu5rKuywcG5u9FqcM/GzjAcIy4w
+KB8txLjeW9+vr89DTce76z9My7e/xsC1rrK/y8C7xfR12tLjbNk7HyUsLi0mL9vC0NLSvL/N
+eHjQyudgfc+6v7+/ubS6v8HGz9p06Pbo7lz94TckJy46Miw407/Lc1HMvsdaTNe7xO1oxLO0
+vsq/ur7kbvLU111b82VkU0xf8UQxNjtGPTc+T11MQEPWyuD+0L25wsvFuLW+y9nIxNxXY+bi
+a1Nx3tvqYF/k2/5NSEdAOjY0NDxGSU1d4s3O0crCv8PEv72+yuHU0ehrVmr929Lc5s/M3u7/
+/3taS0I/S0o+PEhZYFJKUmZkVGDt0s7LzczKydDq3dbQ2dTNzNTg283jYXvl62RNTl9vXFRS
+XF9QR0hOT11o9Obt9tz1Z29rduT+atzT1NHU1tXW3OHl3N/3WGP7ZlhZ9Ovu+Ozc7V9eW19e
+Z19dbF1UVlpZWW/Y2eDh29rZ19rZ2eZtb2JWZ2Vabul4cvzo5+Plenzv3+z07nF2aFhYXVlb
+YV1i/9zj4t/b5vbwb1da+HJ0697i4uju7P1vfevd5vDs3u1qX11ZWV1mYGBpbvf07eju+N/u
+Zv7zZ2Lf32lp3uT67/J9+ub/bXPr7e3t6XViYl1bX21gb+3j7f7z6u7xeWt1a2lv+fTq393o
+6OZpXml3aXB+bXzi3/x97OZyaHzx++z3e/nzY2RpZGRtb2xzcP30ce3n937v6Ots89jtZfn7
+/OPf9/js529ifm1ka2pm/HJdb3L9bWJsd+fm7+vg7Wj63Ol39+d+b+3rfvf7TlzSXVDaz/R+
+eXvxflln9+NsXHvib2/f5fJz+3nsbmJobuz6ae/db3bd5u/rbXjmbGZ1dPD8dP3ud29qbnts
+Z3dveff/3+dvZ+nv9fD36d/r7+ba2XBj5t1fW2JuWk5PYv1dV17u6Wpg8el/aXTl5+976dne
+49PKxs7dz8rN1OPUydJkT1xrPi0vOTYxNkXr1+TOv8HL0szDzOTWycjJyL65vcO+u7u9v7q9
+KhozOBwcLmq4RD+moc1GYL3MKy7Dv1BNx6uwWcipsMnJta65yrixOxgfPB8YKGu3y0iqnbJP
+9OJCLS1H91Hmu7a7ycCws8O8r7K9xLWuyiUYKDkaGj+5vO/ToJ7ORsLJNR4ozEwtda2uvd6u
+pr3Kra21x8msrk8lGiYzGhpduNDqyaSi4Ei81i8nKTxNOlOvrLa8tKy1xLKtt7m9ta/EMBwd
+MSQYLL3CXWmtnrc817s+KCkyUz46u6axvbCqrs2/q7LSv7OzvT8fHS0lGCbTxWLrsKCvS+XB
+PyouMztFR8OqsrusrrrFvq2y07+utsJyJxwlKBwiScbM17qkqOZnzlIvLTI/P0TOtLa6s7K5
+xr6vr7+3rbi+yiscHycfHi/Hve/Dp6TFTOviNSsxSV1Lf7iyuru1tr2/tK65v7Cvu78+Hhwj
+Ix0mfLjM1q+ntlJP7EAuNktRS+C+t7m7tbfHxrewtbuyr7q85SYbHicfHjy0uuW5p6riPnP4
+NC9AX1BWz727vru7xse7s7K4ta6yubw5HRojKR4nw67O1K2mtkJEz00vN1VZSFXMt7zLu7zM
+x7evs7qyrbe81yQYHywfH1Kvu/+/pqleOt3ZNjBFXExW7r2ywMS6xcy9trG0uLCyuMYtGhso
+KB4wtbDh2a2mwDlPzUMvOlBYXmrDsbvGu7/Hvbiusbuzr7a+Rx0XISsfItKsvFW+pa1FOtLZ
+NS5Cb1tdzrOxyMm9w8u9rq65t6+1udoiFh0tJB9IrK785amm/TFlwkQuMVO/VkW3rMD1yL26
+zrmosb62r7PHKhUaLSYePayr0EuxosUvSMRfMS9AwctDy6yy/f/Dt7zBrKu2ubOuvy8WFiks
+Hiyvpr5Hw6OxMTjM3TcqMLq6QMerr89R3rK5x6qqtbi2r7kxFhQoKxwrsaS9Q8qjrjE5zdM4
+KTS5ukrLrLDOUdOyvL+rq7O3uLO1NxQULCwcKrCkujjhoa8vNM/TNSU7rL9Eu6qy4UrGr83D
+p6u0ubSuvC4UFS0pGy2rpsQ9y6C3LjzJ5y0kTKnLP7WouFNJua7pv6aptbyuq8YjExsuHxtI
+p6v4Sa+i9y5fyD8oKeWsWVCrrNRY2rW4XbOkrriyq61tHRUhLhwezqayUW+pqzs3z9EzIy24
+tj++p6/fVcex1neqp7C7saizORoYKSgZJLSrwU3BpbozTb9UKyc9udNOr6i8/NW7uN6/qay3
+t66tvjQbGikjGSm7r8pWtaS/OuzERiooRrRbWaqpwvDHs7xTuamyvLmtr9QvHR0qHxovw7/b
+7q6n1EfFzDssLWPCSNaqrcnYubDD26+rtry4rrnxMx4eKR8aL9jK7OKtqdJevtI+Li983T7J
+rrjIw7Oxx8Csrru9tbC+ZD0kHykiHSxW7Fdes6vI17zMSzU29Vg22rXC3sexsci6qq68vLG0
+zeluLB8pKB4lPFxLP8Wuwt+7u9ZMQ+t0OErK3W/Qt7G8uKuruLyzuM737j8pJSwpIyw/RDlC
+x7rOyrq5xd3TyVw7TGFOV823tr2xq7O8urzPYFnwSi8sNTQsLz09MzZUzebou7G1u7u90kVA
+U05IdczJxr23uL7AvsTO0dPoTz9DQzYxMzEtLTZKXeDGuLK2vL/OTUJKTUxO98nGwrm4ur6+
+vL7Ky8jWe1RDOi8sLSwrMD9k6s+6try+wNFoTU1RSklt083Fvru7v725vMDBxMbL5k47MS0t
+LC0xOkZkz8fFwMHI093lXE5OU13s2tXMyMS+vb2+v7+9vL7Kf089NC8uMzY2O05lbO3Y0dnW
+ycn1UlNXWFdo393fz8jFwL67uLi5u77E1ltFPjw2Mjg4NzpBTlRa89XT19TW4nRWV11UXvDc
+zcbCvbu8u7i8vL/ExMjTcEM7Ny8tLi8xOEFb7t/LwsXEyNbjT0hRR0pYatDEvba4ubW6u7zB
+vsLL1l5NPTMzLiwxMjQ8RV/Z0cW/yMjL3eRVUVlJUnx+2cjCvLy6sbe5tb2/ws3SU0E6MC4s
+KS0wLzlJa87GurS6vLzS3lVFVkNAWVh5zcG3s7Susbq4vsTCzeFPPzUsKyolKC8vOE7Ru7ey
+rrC4vtLqSTRBRThNedvExLavubavubq7xMDE51hMMyopJyUlLDY9VMGxr6+usLfNVU9GLzRO
+QEfdxbm+uq2vurW1u73LxLvUSE06JiYkIyQmME1vxK2pq660t8o/NT05LTxsX8vCt622ua+1
+vb/FvLvLw7TPPDwpIyMeHyguP9C4q6aoq7LB7T4uLjIyOGzIvba0r7G6vr7FzMvCuri5tr1Z
+Ri8eHh8gISg+vrCtpqKmsOxGPzIqKDFN0MzAr6yyv8/JxNRv076zsbKvrrxMMiIbHB0fJjfO
+rqakpKesxTsxLSwtMD/eu66vt7m7z1ZBRN/GxLyuqKipr79KLiEYFxohLELEq56eoau8bzUt
+JycuQN66tbaus81hQzxJUPTBs62oqKipr75KKSAbFhshLli7rKOfpKzAQTIrLC80TNe4rq++
+z3dFRTw6U9O6rquqqayvssJkPiomIBseJTPiua+qqa2wxEs2LC02Q/jNx766vcnrRD5GTtjC
+urCtrKyvub7HdU86LSUdHiMvUs+7s66rrrj1PC8sMjxcy8S+vLq9y+9NSkxe08m7sq+tr7a8
+xtLrUDwyJR4fIi9Jzrm1s7GyuN1ALSsvOubFu7e6vcDF2mdNQkxvxbKura61uL3M2edLPC4j
+Hh4iLUvJtq6vr7G82EMvLC4737qxsrfB0OhOSEJBTuG+r6qqrLO/0Op16FE7LCAcHSU17byz
+sLGxs7vURjItMkLVubO0vtRuTk1MSlVwy7eurK2yu83lfdrQekQrHhscJDXhtq2tr6+yu89H
+MiwtOX3BtLO8ye9VWF792s7BubSwsbi9xM3S2l9KMCAbGh0pSrqrqKussLvKVDkuKi0967mv
+sLjE2/5kZP7cyr+4tbO1ub7Fztb7SDUkHBocJDnJraanq6+8z1c6Mi0uOV2+r62xutBlWFJd
+9tnHu7Swsrm+yM/Q1mo9Kh4bGx8ubLSpqKywusnsSzszLjRJz7eusLjFe1tVUWH40L61sK+2
+v83e6NvaaD4rIB0dIjBuuKyrrrW9yNdbRTo2O0zcvre2usbfZlhaaOLLvbe0uMDP7HR739HN
++zYlHRseKUq6rKqtsrvAyM/ySz03OEfiwLq8xtl7bvbfzsjBvLu8wczmfnLt0svNVzAkHh0i
+Lm61rq6zvsnJyszmTUI9RfrNv73DzN7649XMx8rIxcnLz9vs9X3q3u5dRDktKCgpM0nZvbq8
+wcbHxs7e7F9eafze3NXU2dHNysjN1drg5ODc1tDU3nJcU0tEPzw6PT9ETFNeZvzh3dLKyc7Z
+/WZkcurc1NHT2tfUz8vT3XtgZffYy8bJ1m1ORUJCREhMTldw/3F3enFlYml85ujxbFtefdvO
+zM7V293Wz87Zb1VNUXjSyMbN7lNJR0ZLVFpgbn3i2d/g9W90ee/u/3h4aWv559/c2+Dr8PDw
+9WxnZV1cZP3f29/lfWhZUlVVYH3g29nb4Op4ev768f12a23z7Ovp5eDe3N3j7m9hYWBhZmlv
+cW1ufP1rZV9XU1hfb/Tn3Nja4u97a3bs39zf4OPt/u3f39zd4fVdVVFSV1hbZHvw5d/vaWdv
+//Z+fHl88+fi39/h7GxgZXD+8e/1b3L27d/d2drneWddWlVPVl1u7d7c6XpnX2Bn/ertb3Lu
+6uXi5vL27eno73pwen51f+Xb4+fi5+7r6f1nWE5OUFdq7N/g7HJfWFtleO7y9O7v6+nm5/rv
+4t3d2+Dv/P39+vLl3t/sfXdua2VdV1RSVlxod+rk8HdtdvP48ejl5uno9nx7+v5ubXnr6+vt
+7vd99uri4+Dn8XFobGllZGZsbnJ++3Ftd/13/Ofn7v52amhpZmt2fvf37+nk3d/o7u/z7urt
+739vbWhoYF9rcHFueXZ3fn9+eXN68erk3t/q6ur0+n17d3ZyZ2hyd3Z0d/7w6uv+/npram5t
+bvLxeH3x/v3q5PL7+G9ybWpreP11c21uaGh4fHv2++/w7N3b3d/b3uzx6+51dW9tYV9lYG1s
+aW/9e3t1amdlbHBkZXn2dW386+jn5uPpfu/s8Ovk5Ozt+HpwaGVmbP15fvX6efr9cmx37vP1
+6vVqbntudWpocXD3/O/ve2ttcnp6efLzfe/u+P159+x4bH9+fW9rb3xtaGlzd/fl5u7t6Ofj
+5n78+np1dXFnau/+ePzz83t3f/xsX2V1b21scnR79e7u7vttbXlqdHV18+7r+m/25enx8On0
+8OTudm5tbmp0eX3yenP36ur+//726/j1e29veXtmaXFybWtvb21+fW9sdfv2d+3t+e3s5ev0
+fHb0fXFwcfn8cO/v7evs8fn0fHh1evl7aG98bGtqbnn6/P7q7fnu6W9oanRsbHhubfn+bX5+
+/O/r5X799e/t7vDx8/d47vxtfXZt//H1+npwdHdyenZ8bv1rYmx/7+3u9vDv7+339/nw7O71
+e2/+8XFvcHBtZWz1bWlrc2948+7s+uzp6/Lu8O/s8vPz7nV8fHFvbft8dG1ydu7y/ejueXJ3
+cWVqbWBif/7n6uzn+P7o6ursbPl4b3tufW9zb/71b2px+fv67+/+bv76e3J+7fr/5e5+9/Lt
+e/f79/x2++77aW9tbW5sb2hebXd1d359/vPz7uDm3+v+9u3z7uz09210+f387vxtb2t2enBy
+bmpuanB2Zmr4ffzz7/B/eH33+OHk7/L9+vTp+3Z3dnhvb3zxdP/79nb35/H+9eb8bfP6ZmBp
+Z19x+Xz8fvHm4uTw+Pn2/nF6+29r7/d08/N0cnZvcW9vcmxkevz28Xv+8/H+8fbw6+339Pru
+4u/s5O3y+Hlxb2tibHFyf3VvbXZ0b3F6fvvt/3h88fr46vd77vl+6/h88nf6eP7n8Xzyf/7z
+fnRpanZqcHZ5dvb5f+3n/Hb58m/76vb+dHXv+np5bXjz5eXq7/T1eXZucPV9f3n4/f3+/Gxi
+bG9oe/P7+Or+8eLx9err83d8+XdmaX12cXb3d3p6cW92+e7wbvb07Oju6/fy/Xvv7vx27npm
+b/VvZnR5bmtvfvZxbnf08Oz27Px37fnr7fnvevlvZ21rYGvw8/7n5vXw7vHr7e75d2dkb292
+bfR+ZnZz8O/tdmt/cnl5/nz/4ubp5+Xj7fRua3prd/1wbnj6+vl+/Wz7/XF2+3Fr9fxsaXdv
+a3rw/vn36e93fHb4fPz16evw6+z1cG56c3b78vp8e/70c2129nJlb3NveHfv7Hn85+nq6en7
+eHZ+9nxwcmlgbXJsdnD7eX3s7e7y8vr4+3p6/W16+vbt5u/s7fl4eX19ff9tbfv2/HB+fPx0
+c3lybGlucPH57/Dv/fjt8Pt58+n5fvf99/N0bm1zcHJ9/vT9en729O/o8/f76/N2b3lnb/v2
+92xkZXb+dHX28/r48Hx+9vJ+efzw+HH39Pf2+u/ufPjpemz2fXtxamlx/vj+a2pndv54bfjr
+9vzz9fbr+33q9/bm6fv+9e7u6+77dWhrbGpuaXR6aGNrdm1zfv10dfvt6+/p6e/s5ubsfPzv
+/fH++PHw82p6enRqa2ppfvz0dHj47e3ky8hYTmtmSlRu5tzhaezQ8VFRXVXl/1Ba2tbv3sTH
+38nUeHH7SU1bVGne7WPl1ujp4nZuZWhpYl1qaWpmanvf51lT7+rs99HO82h97W5MVNbO1m/U
+xtVJVm5dU1tUadTacvvZ3W9ya3pcZmth4O9h89t0dN7o7txg8OdZS1zt9mB46ttpbeLzbGvt
+19h+29HYY2J852FXVGbtYmbh6337bm1kW3N7YGfi9Hp97eXm6ubjanba5Gtw7edja+HfZ2p7
++H1zZWp2a2/s3HJleWZgaGd6/Gx46vT24uv2d/l7ZGt+b+/g4+7m5/3u5Xxob+zjdGz95H5f
+de5qYWFoXWF24+L/bPn9aGRu5Xn+59zi9efte19de3JeZ+Lrd3fp5Pth7t3ubHbj73h09/5s
+Y2/d1vNt7uhyalxed296bO5sYXrwbm/1ceje5u3s+Ojq6nlnbH9hX2tw9Of9Zv7u7n1q8vL+
+/+fr9WtueHFoaW9ndGvr+3T17er27N7k//jv8e77dvj2+v5ianJlX2Rv8Xdt9+r4Zmn9/mb9
+7uNwduji+mJ7+Xpeaufrbmjp8u3s7ePkeWxy8Ol0euryfGRf+/NcaOjwaWb0fG1lc+z9a/fu
+8XX/8u18bvPv7n3j3eVnYG79Zl76+mhf9OTu+PHm919k+/J0bnfv6vh66uvz9/HsdGn4fml+
++PDrb2J76nlbWWx2Yl9z/HH78Obn+Xrh4WRb79x8Y/be6HF83+pnb/f5cmrv6Ph7e+xybV9q
+aGhucn75/350cer09/xrdvPy+X117eRtcnpkYmdv7e5+/vb06+/9+uj4aXL0cnv47+fm5uXv
+bWx/7v1saWxvZWR16vthbvjtd3f9dm1sbebrZXn9/X1sZvH4a2r53e396uP3++/zbHvj8/p/
+cPLd7WZ69e58Z2hpbV9fbfj7bXXo9mPv3vF2ffNyannve2997Ov0++ft7vF26fh0e3Lv62li
+a2V8/nH8dGt08/x2b/zlevXr7PTs7Pzr+nP25up3b3Ztce72cG9xdvfw9vp6aGt1b2tt/nny
+7e3vdf75fHJv9/T+bnJ+bXNs/unw7OXr93t66ep27ePwe/Ll/m598+3wem1zbmdeZ3pya295
+amx36/R3bWzu5vX98eXp7+Xl5ntv+3VocfrveGtvcXf4dm/u73V3fH349PP5en7k4vZ2/fP7
+cm10dXV4dG99+Ht7b3X29/jwfv7v+Pnr5/z27e3zdnp9/W1kcfb5bWhsfe3xfHFpaf7q+Pfr
+4uN1cunvffX56+Tz8+bsfmtoeH5sbG93bWFp+HZsdmh47+7vcHP8fG917+Tl7Oro6eLl5e15
+e3x9bWhw9f1ra3H/dmVjb3B3fXXr4fp5fnj67unh4+r29/hvb/f0fWls/XttcP/8dmtraG12
++ubj7/787+v9b+/s9m/8/Gr99+/jf+7o9353/P9xbn5waG9vbHv8ePvu/Pnv8/Lt8/jo4OPv
+8Pp/9nFqbG5vbXV0c3zxemtxcmhobH7v8/54fX7u6+ns+e7n6/Tr5u70+3v2fXb57e32/HJp
+YGJubGtzfvX+evf05uvufHb17O39e3VzaGppcu9+6uP3fvn0+Wts/fr79fn+8uzu+Pfs8ndu
+aGhrcW9v9/D7e35+fO/l6PBv++v7efz1+/Lq7fh7eXh9/3J1cW5vb/ns7urzd/HvfGlncf/9
+cWtt+3Nzcnb6+/Ds8vLu+Pv4fPDm7/zu6fX/8+ro+3h2cG1pb3dwZnn1eHx7e3d2bWxzeHB0
+fPN4cury6+nx9fPzc3p5d/pwdPDn5evr7vh8/vl6dX75bG10e/V6dHf99Pb6eXR7dnhxanj9
+fnj76ens6+f2fnX6+XF0dXhtamptem50/fn9ffp0bvrt5+jw6N/t8vX38fx0aWxvd/x1dHv/
+enZubXz+dnB3+PXy6fL17+3t8fHu7ftvZmBfam9paWt57u36e3359/d3bm939vDv9PTw+v7y
+5+bxe3j1+vnt+ndy+vtvaWhta2pxcG1oZm5ub3v/dXjz5ujs8enh6/Lz7fv//vx0cPrufHP+
+dnF1cWt99vv/cXZ7ff15bnbt8nt2+PR0dO3t9O3vfm9393ttb/338fB9bXr99+3vbmZxfnVm
+anN9+Pb6fXr28Pj+/vV8cf/s7O/s8v728/Pw8fT//3Rsev10aWZlam58/29udvz6dGp07O/1
+7Ovr5t3j+f51cG1ranl8bG9+9/Px9vf3+v52cW9vcf19fffv9fbs8npzfXFxeXZyfPTv8/1+
+8ez2/X9+bmlub3Jsbv/19Hh/7vF8/Hdy9vZ5d3R78vZ+fPf7/Xt6/fv39vt9c37r8fvq4ep9
+b2tvb2x1a2JocnBv+u/w9Pby9HtpZmp0/fv4dXN67ePm6Ovm6vhvcXB19vB6cn17b3NxbG9t
+ZWd77+/u929u9Ozx+Pjs6On/cnpvenhtc3z9c2hnZ2hpa3F0ff/57e338u3r5+rq4eHt6+3z
+8vj48HpucWxkZmhhYmhpa3JuaXzx7uru7u/89/d/9+/y/nx3+3ht//Lt6+fp+HT3fmZnaWx3
+/3x4dPz6e3Fz/evw/nz76+/8/PXy+P5/e252/G9mb3/9dn12bHJ57Obl7fZ8bG568+fm5un+
+a2ZsbW/77XpeW1xdY333eX58/uzm6ent7fTx3uHs7eTf2t3m7nRoamFdaHP5d11WUlBWX3/g
+2trg821pbXv9/uvo8nJmbHv77d3p+PTq4+v4e3bz6uDY7Gb382nq4378aWhyXkxHS1jl2Nvi
+eHFtbG9rbeff721fVl3jz87T3d7f5uHk7Orb0dHrWU1SamdXTURCTGXi2tjU1NvzW1FXbN3X
+3epsYFha8+HUzNXZ33zo1s3L097kfGVeVEpHRUBDTWzTyMvV5mJbYG78+/Ps9v9sVFjy4ODl
+fnzh1Mi/vb3F2mlPTE5LSEI/PkBLYuLRyMvQ3XFqb3vf19z4ZmBkXWjt39Tb6dzWyL2+xdJx
+ZWRPTEg7NjM2SuLFvcXS9lVTY33bz87P4V9RT1N0zsrM12Vp2sK6vcXO6PlpTEQ/NjEvNlDQ
+vbrC1GtMSlJn1MrM0O1YU1tWds3Ky+xTbNC9tLa+0GljaExEPzYxLjNM0b68w89oR0VPYdfO
+xsbsc1JKX9nBv811W2vNuLK0u9ViZVFEPTkyLCozUsS3ub/OZkxKS13by8bH6ldOR1fdycPP
+bFhk2byyr7G+2npYRD06MisoLkfGtra8zGVMSUxi3MvAw21IQz5R3s3H0HJiatu8tK+uus50
+Wk8/PTkrKC09yra2u8dfR0A+THHPwL7UUD44S9nBustaTlHuv7SsqrC+6E5EPTw3LScrON63
+t7vE7FBIRUlV7r+8zWE+OUbgv7nFeVJMaMe2rKquu9VVRDo1MysmKjfkt7O5w/5NSUdW9dG8
+vMhpPTY/5r63xmNHQVnHtKyqrbvaWD42MS8tKSs55rq1usLcU0pKW+HNwr/OYkU5P+rCuL9a
+QkFWwK+qqK2821NCNzEvKSUrPM20tr3KbE9KRE79yrq6yWU/N0PcvrfFWUI/V7+vp6auvt5N
+Oi4uLiknL0fHtbm/z2RTTUZS5Mi7vcxmPzlPzb64z1dKRXi7rqmqtMHaTzouKygmKDVqvra8
+y91rUlFOYM7Cu77mTj07asi9u9pVTErdt62oqrbKWEY2LS0oJCo37ba3vcL0a2xLT2PmwLvC
+1Us9Ql/IvMPkUkdN57usp6u2y39INispJiMtSMu3uMHI3WdiTVbgzb28y/5KPkvjx73NXEpE
+Uc+4q6est9FXRDErKyUkLknBs73EzXVtXlBh6su8v8l8RT9Szry93k0/P1vEsKmnrbzUej4v
+KygmJjBOxrq/x8/hcVtWfNXIwcbPZEVBWcu9v99MP0L7va6oqrC+4F49LionIyYzYrq2vsXX
+83dZT2bcxb3Bz2dFRu3Fu8JiQjs/57itqKu1wNZfOSspKCYrOfu7ub7G3nRnW2h9483Mz9pX
+Q03fw7vHYkU8SNG3q6ittsbfazwvLSgmKjJYwr67wtvjZUxUV3vNysfPX0dL3b+7yGBAO0jT
+tauorLTC51Y4LS0sKystQc+8t7vM315SZFxs6N3Lzl5DSuK/usdlPztG07asqauyw9fgQzMv
+KykoKTZqxbW3vstgT1pRW97Sw8hwS0vfvbjGXT03Q9y3rKmssL3dVkE3MC4qJiYvU7+ysrfE
+81hRSk1o28TE3k1P3MC5w2lAOD70u66qq664zPJOOzMtKSYlKTzdurG1vdJWT1JNcNrOws5f
+XtnGvMVzSTo9Y7+vq6yutcfeX0U4LywmIiQwWr2wsbjJa05OTFXr1sjJ3l94z8S/0FE9PE7O
+tq6srrC5x+RMPTErJiEhKULBrq2xvdthU1NUX3/WzM/eYuLLx8xgRT1E9byxrq2vsrzQe0My
+LSgjIiQy4LiurrfG4FZiY1hgXvLSz9rd2M3I31JEQVfLurOvr6+zvMtYOy8qJB8gKkfEs6+y
+ucPN1vhWS0dKVvHm/dzMxcfcXkxP7cW7uLSxsLW+0U86LykjISUvTsu8t7i5uru/y+VSRkVI
+TlFVaNzLys7o+dvNw8HCvry8v8ncVkE8Ni4sLzlEUl5qcODIvru7vcXO3fBrU0tMU1pWTk1Y
+99fPzcnDvbu9xc7a4/BiTEJCS0tEPzw7P0pZfOrc0c3Kyc/d7fHq8WtZU1vu2dfZ1MrGw8PG
+ysvN1udaSkpKQj07OjtAS1JWZ+LOyMbHzNXe29nqYVNYaO/e3N3b0s3Nz9LOy8rO23VfXlZH
+PTo5PEVNUVl83M3HxszkcvTzeHRdVWri083P0dDR0tPY2dLT0tr0aGhmWktBPz4+REpMU2Xc
+zMvQ2unu3dzl/GVf8dfUz8/Pzs7P0dne2tvlcl5ZXWJVS0Q/QkdIR0tX7tHNzdrz8uXd4/lx
+c3Ds2NXPzMvJx8vP1dve7HleUk9XVUlEQkFDRklMU23YzcvP2tzZ293sbF9jeOTY29bOy8nG
+xMnO0dnuX1BNUExGQ0A/QERGTFdo4dTT09TW0dHS2ex2d/7v5N3b1tXPyMbIyszT4m1OSkxJ
+RUVCP0NGR0xYbOHU19LS1NLP1t3e7P9uafvm3tjRzcbCyMvP2+VnT0tKQ0FDREdJSk1ZbOjW
+1dHP1t7g5+rk82xsbHLz5t3e287GxMXHzNXqYVRNQT5AQkVKSUlSbunc2dLOz9bk/W/07fJ7
+e/nj2dXOzs3Jys3Oz+FnWE9JQ0NDQkZMTk9aanjk2NfRztTd4uHc3uj79O307N/U0M/S0dLZ
+3e54Xk9KRkRDR0tNUl1oce3i3drV09PX2tzi8Hhv/+7o5N3U0dfb2N7n9HRlXFdOSUdJTU5O
+WWh+6eXi2tjY193f4uDe5P555+js6+Hd3Nzg3t3o9m5dWFdSTUpJTVFWXGvv5eDd3t7d2tjd
+5+bn3+Xu9PDy9+jm6ODZ2+f5bGReWFhVUk5PUVVYYXf+9uPd3t7j59/Z3d7b2t7m6vpyduzs
+/fDm5ubuf21mZF5cWVZTUFFWWmBsbXXx4d7e4N/c2tnd6u7l5uz19ejm6eTt8enn8XRpY15b
+XlxaVlZaWl9sbGl6fnjq39/l39rb4eTtevvw8ujh4+Lj7/v1fnBqY15dXFhcZWlna2xpa3rn
+4ufp4+Ho7PD7+376/Pf0+PPy8erk7Ovr/25rb2pkZGRlaF9ebPr/dvvu9vru5+jt5ubveXZx
+bHFyfv727fTz7Orn5Ov9e3ZsY2RrbGBjb21rc/r5+unp7u3y7evw+3d0cG9zc3J2eezp6efk
+3+Xu7vJ9d/5xZGdqYV1iZmltbWt3+fXy7/n+9Ovp7/Py6Ojs5t/f5ejs9Pb5eW1dW11cWlpc
+ZGNka3d9/uri5Ofl5+fj6Onq8P797vH77ufl3t/n8XBqdX58Zl9dWVlaWlpdX2RlaHfw5ODe
+3t/j6+Xk6evm4ufs6ubo7vLu8XRqY2JoYF9dWFlcX11fa+/y6uPe29ze4+j5+PP+e+3k6+Pg
+4d3d4/dqYmNfXFhVVlhZXmhx+nZtbf748Ojr8vLk4ubq5OXn5ubf3N3h5O3v6e12ZF1hXl9e
+XWBoaGVmYV5lfHhy+e3v+/v88ens7Onk3uPf3d7Z2+X6eG9tZV1aW11dXF5gaHH39Prz7ezz
++nV+ff3s6eTj3drX09PY4uv9aVxYU1RaWlhZW1tedfH08uTl8f5+cHN59Orl4d/b3NvX1tje
+6393aF5bWVlYWl9gXmB2fW5xfX797fF0eunf397e3NnY2Nrc4ux7amdjX1tYUVFXW15eZG55
+/e3r9O/n7u7o39jZ3Nzb2dfa3Onu73liWlJPUU9PVFxganf97u3w8X10+Ozo6ebi3NjX1dHQ
+09PW4vLzbVpXVE1NTkxRWl1eZG54eH3v+Xh78uzr3NbV0s/O0dLP0Nbc7n5tWlRXTkhNT1NW
+XGVmYWxta3n0/e7a4uri2tnU0M7R0tDT3Nzg++xjV1hYTExLS05QVmNcXGxo+e3o4tjn5d7c
+2tnZ09XRysrOz9Pf23pYT09LSEtISklTUVRXbF1v6/B3393f2tnT0s3Ly83Fys3Ozd7f9FlN
+SklBR0BFP01JUk5nZ2jv5OTez9TRz8rMy8jGycPDycvL3u1uZU1DSD0+PEA9QUpLVFP9Wvjs
+29zOzMvLx8bHx8HGwcHBzsXO+NlYTD5FOT03Ojs9RkNMT3JX1erW0cPJxMjAyMLCwcS+wL/F
+xNR490s/O0cwPTM9NEA/REdYc27P1srGv8DCw7+/wLy/vr3Dv8jMYOVJRDk8MTQyNDQ5RD5Y
+T9dnxs2/w7y8v8G/wsXAv8G7vcLB2mxSQjo4Ni4zLzYwPT5HU+LN1L2/uL21ur2+v8nGwcHE
+vcXKaG9OOjk1Nyo3LTcwSUBO9czAx7S6t7azvMG8ytnfxtXRx8zlRdw/OzU/LC41MDU38z/z
+y7jcs6+4uK+3wry96/fN2mjK2Fc//0EvND0sKUIvODzLP867tMirqriwrbnMvspXXNteSO5n
+PztZMi4yPikzOzs37NP+v664uamsta+uvsbG0kp46VhN10EyPzkqLjMtLDg6QUnF276wrrep
+qq+vsrnBytBgT2pWV+1DM04xKC0vJSo1MzxmvNa4qa6ypKexqq26xcPjR1BsSUzaPC9QLiUo
+MSIpLjc2b77ItKerrqOprqyut77FzVtP8FlcXzA6PSUkLiQgLCwxPs7Luamlq6OjrKustr3G
+zdxo5uR35zgwSyggLCkdKisvNfPYvq6lqqWhq62vuMHL5c7s3MnJzEE0UiggKyocKy0uOebc
+vK6mqaahqq+vvtXX62ni2MXIzTc8RCUgLyceLy8xQMjOuaukq6SjrLW1y+j/6PNyzbzE7TJO
+NB4fMh4iNTY4a8C2sqqiqqmmsL+8zOxZ28/cyLLAPTRnIxsnLRopPDk/urWuraKkrKqsytHT
+b1llzL7Iua3FLz1JGxkrIhkuRUFnsKmqqp6lsKyyXmrva1vMu7i4rK88K2gfFCArGiZH3FS1
+qKeuo6S3vLlYPlFgY86yrq2rqLYsKTsXEiMmHDPIt8Kpn6ezpKvQ4tk8N0nRzbmqpqeqrE0d
+LB8PFiwhLd+oqa+knbO9rss/PkM6POy0sKijn6aswB8ZJREOIC8txamcpKifp17M3T41N0BV
+ZbSop6KhpbHPRBcTHhIOK2FqsZ6YormosDE4VT82Psi1xKqepKekq8w8KRITHBMXUMGvqJ6b
+qPbG0i8sRNZTWrGpt6uhpq+xuFg1LBQTJhkXfrG1q6WeqTtRwjIsUr/F+a+muK6jqrG7zl8/
+ORsPJSQVLa+6rq+hoVk1vdUvOL67X7uqsK6qqqq8cmBlSSoRGTUZGNq1tbCrnKo9PtJuLjC6
+uNa3q6epsaiq6kVn604mEB43FRrDsK6vqJurMDXNOyYwrqvJs5+muburrkY198ZDHw8iOxUd
+r6elsamYryMmVEYjM5+ivq6kqcBer64/NH3DviwPG1EYF9Kjn7Sul6koIi9bMCqrnqyvr6yy
+a76u2T0616q9GA0zLg8hrJ2esaWaxyQdIMtAJaSYqLPSs6lHU67JPzrEo7UdDic2FBq3mZe1
+t5quHxUguEQlppWhvFOvqjk2uL1ANr+cph0NIjYUGLyVlLW8nKsjFBrLvyyzlZy9SMquTTLW
+wdJexJyZRwwNLR0QL5qOmcisoTMYEx+11U6fmqC3QMG0PTlg1ru7p5mbPAkKJRgVVZeKkc7X
+ryoQDx+1rbqdlp7FNDtPNjRdvKahpZ2fzRMDDCIhXJyNiZ8pMycXFRrpm5yen6m+KRwt6s28
+rJ6aqczFvk4SAg/Dz8+llIykGRwvKiIksJScvt9tNyEhyKCjrKmmsT4rXrO+NxkRITssX6Oa
+m7MyLywsPWK0q7VbMi442q2moqS6OyctWcuwn5meLgkHHDI7q5SLkzwcJCEfJmafo+ZKPFXO
+b7CkukgxLD/BqJqZnaU/FAYFIqmelZGSnh4MEiBDt6SVlvIeGRs12LidmaVnJSy9qqWkqqy3
+OhsLDG2gp6atrbEjFiherKu/srIvGRYivp2Xl5usLhkcP6ufnp+eoas8EQYJKK2kpKOcoS8U
+FybLtL2soK8tGhvmnJiYnrouGBYss5uRkpipSywXBQMTqpSZqKSe5RcOG+umpqefosEkGB/B
+nJyhqtMtHiA/rJqUmKpBJi4sDwgTsZWcvcuosSoVGDulnKOrq7A7GhYmppSbsUQ0LCIm052S
+lapZOTYyGQwPK56YpLi2vDMZFC+lmZqhqr4tGRUd3JuXna1EJyEkNrKcl526SljLRxoNDiui
+qbu8q6OyJxwvu56hts9kLx4bI76alpyxPCQdHCfDm5OYo7XgPSARDg8kn5mbnKKvWRgPHEqf
+mqGsr1olHBxDpJufuz4rIB0rupmSm6z6ODU0IxsZHq+bpK7DYUMlGi67n5ulv2szHhwhe52X
+nro4IR0dJtWelJSfx0YyLi4sLCMhxKissVAzTTstXsmuprVQNSorOk+yn5+tQyEbHSnlp5mT
+l6ZMJSIv7ra5SB4dPkhGSV2un7w5MS1N90VbvrisuGBPSE9LMzBgvLK1vrOstuBOarSoqr8u
+Gw8TLm6voqSnrDkkJSY/vbKsqbW78zo5P1DR4FL63trN18exsbi4wr2+XDYmHRYcZLClo7C7
+vTInLDLEqq+vscfXPSw2U2rK02tuVlRo+82zr66uuM5WOzEyLx4f4LSqrXVkumo9OTHYr7nF
+2lfdUDg+WF/Yztzd1fhVS2C8sLC2wc3P7VU+MSAfRMqyrsLFs+I7NS9fucfMdFLT9UBET2XL
+0crKdEpGRdS7x8jIzcDCwK68NiEbJ9S9uLGvqrI8Kio06txf1r+8v1U4Q2XSyebyydBROTRO
+zMnBtq6w0js73LC7LB8t07jYP+6urMk2KzZSSjxNuqeryj47bs9MNjzMs8BJO1e8t95LTGfg
+ZFDqv7Wvt8HNRCIfKzrFtLqytPZAODRTz8m9u7vITjk5PE3u58/Au79nOz1MWWVbzLGxvdpI
+Pz41O/m4q67HXEMrJzRIxLS6tLDHTzoyQEpCXM69t8haT0pXUkBI3by0vOtZX1JIRlLVxNF8
+Wk9hcFz0zca/zWZeUkZIS1F+dmB65tTO5WFVSkxl383HycfI2G1OTFpeUE5m0cTHz+xrTTw+
+Sm7QzM3Fx9llS0JDTGnQxcHG2WRPSUhV79bQy8rO511PSUpKSFfez9Pf+PjqZlxd9s7PfVxd
+89fb187O22FJQklbdNzIvsHVTj4+RFJj39DKx9LxXk9QWVtq6djMzuJoWVRYY2nez83M3Wh0
++G1ZVG7c4mRST1lkX2fh0MzUb1pVVllXcNzb29PS0dtdVFldeevv2c/Y3npcXFJScez35ePp
+3ex9cGBdW2Dq2uH+am71dlpRXeLT3vd9+vZrWVly5N3Z19jqWE5TYe3X0dPW3OdrV1hjcuTa
+4d/ldV1PTVRh8OXh3N7ofWBYXF9w4dba3/F0dG1pX1xp9uHc3uDi4/NoaGhucvPf4/BoYmBm
+bX3+9+Xd3vBtY2Fka3j46/xnYGVu/uvv8Ofe3uvz6+71937+9/z+6ebxf2tdYXL3/Hh+/nFs
+aWV2dmhv/nX87fp+/v7u7evj4NfX5/p2aW1xaXHs5uZ9aXBtYmVpZW/+/vF2cO7q8/j27+js
+9nprfXlvamv85t7sdm11fG518/Ht4u5sa29wbWZk+t/c4/p/+fz+b2Fea3f+fX7p53lv+vj9
+/v39//Xv9+73/uvn6ujzamp1fHpwa2praGVkann17e95ePt2dXZ67+7p4N/o8nx2dnx+cGlr
+//b0cWt8/H5ybHjr7Pp89/Z7+/d5fPl7+uje3eDrb2JcV1dcZnzq6fl89vj8/XNy+e7y+PHv
+7urg4+7z9P59enH+9fxxbW1vef14b3RuZ21ud/n7ff17/f317vDp5eDrc21qbnFvZWp8ePnp
+6Onr7npu/vb1fW9sfvH27e/5/Ozt/n14a2ludXt6dXd+eXR4enV4/u3t+Xr97vDv73739/Xv
+f3V9/X54bnD97/Du6u75enNxbG58dW1x+v/9enNyfvXz8vLz7Or2dXBzb2p0/H7+/3j9eGz5
+6Ojt6+5vb/vz9O7t9fN9dHZ0cW91fnBu8/J3bXF6a2ZnZ258+u77cfXq8/Tr5uXm6u73em19
+/n389+r5b3b8fX73fXZvb2plbGx4e/z0+fL2+vT/dW9se3Rwfn13a2f++fLj4eXp6ejt9+/v
+9fLv5efufm1pZmFhYmdkX2lxanf7eX1ubX739vfv+H3u5+rt4N7g4Oft8PL9eHx2bWxvbGxi
+XmVnbnn2dmdpb3Bv++rm6uvq6+vn6e35/X56cXB7//T59Hxzdv30+Pt+/fn6e3v8fHpwampy
+fXt9eXv07fJ7f/d3dnl3eXX9d2Rld/z4+Ovj4OPp6fD37Obs+v5zam5udfT2/Xh5fHVtbWZk
+aWRkZGFu9/Xz7uvt9ube6+3d3vxub3r+eXF8fn12fm508/bo7f14bWls+uvo7fX6fW9sb21r
+b35ua2397Pj37/Pu8vN+cX3s7Xxwevl87/N1fHx6cnt+d371f337fHhtbfz5//n0+3739Hxr
+bHj39vPu7ubq+ff/e31+8vfx8X1sb/ru9nv8eW5rdW9naWhtdnHw7Pl0dPnt7n77/P757+ns
+8fPv8Hl4fv///HZlZnD9d2xv++/16On1/Pbu9PX/cXr7eHn7f318+e36eP3+dG9qZ2Vne35z
+cHR9/P/48PHy5+bt6uXk4+bs5en5bWFfYGBfZWpxb2xsa33y7u75efbp9Hn8+PLq6uzt9u/u
+eW789nx3cnFyfu3wd/v09fd0anB6cWpvfHl1dHRu+unh4/Lz/Xd6d/p+cHZ5d37x7fdub/75
+//18+PH+ffTw/X59+Xt1+/R+dPvn5+zs+XdvcH92a298fHJ98u57b3V4cf3n5/V/+XdnaXf+
+dG5ueXZ1f/Tu9fHt8P569PHz/vbi5vX78ff1/fb+amlsaWdodndrbXP6+3J+8/h79+v4cXvt
+7e3r7Ofr8f14f3R8/29tbmZpbXjq6fN2fu7v7/b1c2729Hl1+/Z7dXD+dG16e25p8ujs8PX7
+8uLt+/j7cWtzaF9teP58/O/6e3n49/jz7u78+e3u8Ovj6Pdvb3t6eHl1Z2Nkb3Z0d/Ls+Hh3
+f3t+/HVz9+7v9P777vj69v51bW59/vl4/fp6++vj7vb39/1vfP1rbG9wcPrueW5zfe7p7fp6
+cGxue/x8++/2+np48/F7fu7q8/328nlsbmllav3s9n51+vt3cmluf/Du6u749/v18/Lo5+32
+89/n8Ovr+Xp5cGlfXV1gX1xicGtoefX06d/ue/fy8fHx8evl6ufe3N7e4fd6d3NtZV1lbWJe
+XWZsaXN+/vXj3u17+/D1d/zzeG1v/+3u7ujv/vfv7vn9d3hvaWVpb299+vb9fPX6eG/85d7n
+9P12b/7k5unl6O94b3J7d29nZ2RhamZgbfp6dnl2+vHz8+7q6/L3f/jr6uXj8u7h6ffv5uvu
+/3ZsaGlpZGJpbnVpanD2/Hr1dHR2bGtna3bv6evt6N/i4+Hc2d3kf2doa2hjX15nb3327+zv
+fP90Zmhsdv397+h9d+vl6+Pe5/5tcWpqbW78fnRv7+vr9vr19vV2cGhgY2x3bmx68u7o393c
+2dvd6PT9bGFeX11bWVxmb/79/PXw9/x0a21z9+/s6+Hb2trc3NnZ3/luX15bWVtaXF1bXmlx
+fvV4bW58/3Fzce3k6OXg3NbRz9PR1PD+cGZdXGBeW1pcXFdqeHD8fmV+Y19vYV1jfOze2dDO
+zcvMztTlfGtaT0xNTk9TWF5q/ezp7ff98f1rc3x97uTa1dPRzs7Pz9Pa6P1vXE9LSkxOT1JX
+Xm77+3JkZWpramhv9+Db2tfUz87Nzs3Nz9Tc6vxgUUxNTExOUVFRVl5eXmBoaml0d+jZ2Nva
+0c/OzcvLycbIztbm/mJVTkhGRkZHSk5TVllaWlpbXV5k/N3b39fU1c/My8rHxMTGyc/Y4O5v
+XFRQTEtJRkdKSkxNTlVVUFFZWlTW3dzN4F3Kxce+vL2+vsLKxdPp9FRVSD8+Oj48PD8/RUxO
+V2T969/f2c/JysrFx8bEw8LJxMbJx8/a5PR4XEhLQzs8PDo7Pj9BSFZdYt/a5tTN0NDLyMzL
+xsfMxb/Hw8LKxdPY7FppUD5JQDY8PTc7PkNITGTsaNvM4M3Czs3HzsvJxcfKx8C/w8K/08/X
+WH5POEE/MTY5MTU8PENUXtnc3cTG0cG+zcS/zM3Kx8PGvL7Gv7/NyFZL4D0uRTopMzosMUE9
+QejY18vCubnEvLXEzrzI38i/yci+wr7A1dBlQFU6LDU3KSo4LzBDSlbLx7+2trezu767xNXW
+3+rSy8u/u729ucFtRlc/KSswKCQpLjI7QGS6ur+0q62ztrW4xf7y3OV10sG7v762uMlPPVg1
+JCYqJiQnLDxXReiurLGyqaetvca/yEY6S87Y4se0rq61tbdROj0rIyIeHyUkKT/90rOqqqim
+qaywv95fRTc5QV/Ovbetqaeprto6PC4gHh0bICEjN+y/raWlpKKjqa/RS0U3Li81SMe5saqk
+oqKnujoxKSAdGBYaISc3aLynnZ2fn6Korso8MCspKzA817Onop+enqGuRywiGxoXExUbIzrQ
+sKScmpucn6m3bTMrJCElLUHGrKGdnJmanKhNKR4YFxQSExcfOr+qn5uZmpqeqLpHKiQgICQs
+PsaqnpqZmZqcp1UoGhUVExMUGB86u6OcmJmanJ+pukInHxweJzRPvaqemZaWmZ6yPCQZFRQS
+ExUZJE2yn5qZmZudoazCPScfHB4nOd+2qJ+bmJmbosM0IBgXFRYYGhwnSbKgm5ucn6Opr8NG
+LSIfIitDv62no6CdnJ6oyzcjHBwcHB0eHyg7w6mhn6OorbK4w2w5KygqNVzCtK2qp6Wkp6y+
+WzYqJyUkJSYmKjNHzriwr7K6v8bQ+Ew9Ojk+UN7FubGtqqqrrK+3x2FANjEtLSwtLzM5QFHz
+0svU9V9VTUtLS09j48+/t6+tq6ytrrO5wt5XRDo1MjM1ODk5PUJNWVxUTUxLS0hHS1jw1ce9
+t7Ovr7CxtLm+y+RkT0pFQkBBQUFGS1BOS0U+PT0/QUNKTlhv1sS8trS1tri5u8DP9F9TTkpL
+TVZfb+fd1NbhaE9EPTs4Nzk7P0hb3se8urm5ubu9ws/4WE5KSk1SYPHZzcrIx8fL22tPRD48
+PDw8PkZPbtrLxMPDxMbL1u1jVE5MTlZeaubWz8rFwb/BxMvfa1ZNSURDQ0VKUFtu8OTc4fR1
+bmdaU05NUldi7NvWzsbBwcPHys/V2+X4b2ldYGRv6vptZ2JYUUtJSUdGSU1OT1dp+9/W0c3O
+09ja3NfQ0tTPzc7KxsrMz9rzaFtMRkNAPj89P0lMSlb+cvXV1tPO3e/g7//T023JsbnKvrCv
+v1Vw0Uo2Ly4yMSwtQFRMX9jLycPDxr/D1dLM6HzLw7+4s6+vr7XNRlVHKCcqJSMmKC1BP0m5
+r8e1oq64qrK/wHdLwdQ9vavDyaqvTErGPiIpKh8eISUtNDTqu8Cxp6eqpaetsbzK2vNi28O+
+vb2wyDLdyR4jSx0XLiocNlw05664raCmoqCvrKvfaL5FPbvKX7u06DnYyiEpVh0ZLykbM1Yu
+SbOys6SgoKSloLPEuNQ7Ut5J5cPPvvkusDkYwywTOzMXNNAmQbG2vqqcpqyeosi4tTo8Tjg5
+XV7bvrKsVFGhNh2sMBJHLxQvOh479ma0r6GeqZ6durOrWk7UPj5NR+PZvatFO54uG6csFXQr
+FjIzIjdTv8Kxm6avmqPWqLVA3l03QEZFZeC6tbCtS8CuIzHNGx41GxwtKCk+vLzDnp2wnp27
+rKlf4ds9RkM98XC/tL+pr3K+4DEvLh8gIB0fIyszPr+1r6GjqKCnsa242tVUTExJ7ufJsre8
+rLXG0n87Ky4kHiAfISAsOzbdrryuoaqspqqzu7W/T97RTtzFwcC8ucDDwW1jQCwzKh0qJR4t
+LS1Xac23u6+rsayts6+5xMLO287SwsLPvsDk0V9SWzk/PSwvLysuLzE5N0NiYMi8wLezuLay
+tLu3tr++zd7HePDY6/ZuaVdGSD02PzQvPz45Qj5aWz/f1GjNwMzNwsK8wLS0vbi928nbYP1Q
+SlJFPkxKUFFydFrbdkxNQDgxOks1SM1rY763w7art7+0u19nz0Q6YUg97ujSz8e90W7I7URf
+SzE0PTc1Mn5KQr2+67q4x8nMzd3gz93/yGJQ7cvR4sW84c/mWPXYX+xxSDg8UC8yQ0c/PGzL
+2Mq/xsPd1b9VeMRTWMrm3tTIu8q8tsrOz3XvY05cQTZBOy8wNEdMPVy/cWzFy8Xe2MTtWsHP
+3cfVvcDIuL7CyuvJ5kvwaVtFMU9EKkJnMC9LWD4+xsZHwbflzsTKysPMy8m+xMO7zdK+u9nT
+xNxeSEw/LTI9Lys+RDZES05PxcVyvr3k0L7Jvbi+xLS5/c2509fEysbP4MpWLy8vLC0sQUAz
+R2ZH/PhOy8rDwMa3u8S/uLm7wba/RM+4dvm92dDHPzM7NCMrMik0RT9I+tfNzbnG3Lu7zryw
+usC1sLbPVsG7dkh2yEpNukIvQi0nKSY3OTRORta73sC1vOPVu7TBxK+ts8fFvMtozszRTmLM
+SkU/MSwlJC88PW3mUU9nVePEvLy7xsm5tLK6rKzFQNi2SD7R2szTQVw7KykqKig4Rj++5Te9
+uF2/t8+/6NGwv7ymqrbK9r7ZSNDhVG1HNC87NycjLj1HSN7T29hOaLS6c7q/0MPBuK6qrMJN
+vcc6W7/Czt1CLyQcKTpFOS7fxjo8vrW+y/TS+eu4r66tt7Wu2D9fxM7OvdrOxS4lKSYsLjZP
+elY+Vb68R1G4y1vetq2zvK6vx9F83tZe0bbOZsZGJSoqJEBEMspGLtLL6r/N2vlD2bO6tayr
+q8M96LrLW82vttM3JSc/Oh8qRjhb0mO82FFnQXXG37uzzLirsLbD4c1sS8a3vskyLzw1OC0t
+QmhDPFTGw2FP3tFx6MiwwGC1qrfk08ffQ9mtsUIkKUE+Nzk1SdA7Sbe+3lw/dsTZybGzx8u7
+s8710GBE97+56yUoYTwlJD+sszVCv7nISuK/3Vfot6y7wLK8911HfbS7Ty4mLTg4LC1Rxbvi
+SNe8ws/i2MTDxbmttcK7wtrea/2/vzobHjQuTjsp+8dI2si9tMpYz7a4urWvsL7UtKzGYsK+
+1jIZFyg9Tkg3Oj0yT8G8tstHYb6zrKmpsb7Dv62sz82/vbsgDBParMwsKUk6KDuro8IsMs2w
+rqqlprtQ0a+jq9+3pLQfBw26obkyJUc0HjmintwfJsutraukpMM+1qigruO+pKApBAq3lqAm
+GUlVHiqplq4aHGynpa+qoLczNaybqW1JqZksAAenkJ4nGHP1Giqhla8ZGsmjpbGtnrYuOa6c
+otPZoqAPABOVkJ8fGF8qGcaVljkQIKyeoLSsoGMnR6OYplW/nEEAAqWOk00RMckXJJ2RqhYU
+ZJyaq8mprTcuvpudxbyd0QAAzo6OthMhyBscopCgHA8tm5SjwLO2TTzFoJ6usp1XAAC7j421
+DyDLGx+nk50bDSqckZ7Lt7dDPMCenKqpnhEADZyKjSYNLTkeX5+XuBEQSJWPplfB1DhCrJmf
+rqAwAAPLkYekDhcuJMCen6ofDyKokJS0TmdeUMGln6KceAAAN5eJlxUXKRpVm5ukJxAhr5eV
+p77RSUfAoZujoEIAADuVipsTFiwcSp2dpSYPIaiUla7xyGdLvKObnKIaAAe7jomsERgdHqua
+ob4ZEzakkpOuST881Kabm5/WBgAbnImNLBIZFzmbm6MuDxnKmo+Zx003M8CimZWqDAAMrYuK
+txobFR6nl5nXExMxo4+Uqb0xKFasmZGhDQAKyouJrR8cFR63nperGxIeto+Qo7Y9Kka6nI+m
+CQANvIuLtScdEx60nJq9HRkltpORm7QuKTq7l5AzAgMbnoyZTzsjGSbPm5jCHRYnppSUma81
+KDCyl5wQAAxYj4+8LCslKDe+oaK/Ixo0ppaRmrdKKjGqmcwHAhSrjJPlJx8dOL22rKy/MiUt
+tJiQlqVOLT6woCIGCRqpjpi7Lx0eM1/MuKmwbjgt7p+VlJ3EN0G4sh0NDhZYoZ6gtjYsLi03
+WLaqrcxBWLSfnKW/T1HEuD8gGxkfTa6otUszNUBZxLi+0U5B77uvrb1IODtXvre6y0Y1QMiu
+rc8vKDRYyNNDNDNAzbCvu+VCRV9q3b26xl8+SNDBzFQ2OU3nvMFUNTFNva+yyVNDVe/1V+/T
+42pf3s7VW0E8RFDpwMNqPTdYuq632ExF+8/yWldXb/l1083qXFBb3dXW0edNOzlF0Li830c+
+UNnhYFZW8crK0NpfTlT2zMXL1OhfTUZEX8rC0VRKXfbxbmBVUenLz2hLSVTmzcrJz+lnXF5m
+79nT4mtrenZVTlJPTmPdz95WTE50z8nK0d9mWWbu3+Dj8NzOz+1WTlhdX/vq6XdeUlFs1srQ
+fmNbWV1ia2ZbXdzIyexMSVVp6ODn7fb6fOnV0NHZ6nVeX3vva1BNYNjGxtxbS0hX5t9wXV52
++vnr3+xsd/bo3eDh5GRVaNnJyudRSU5bYmhmZGpqfNrV42tdbPXl3t/d3OxmZ+zV1+5lVFhn
+amhfXVpn59zb4nRkZW/q6Ht839Xa8Xbu3u5lZXzm5W5dYmdveX71cFtVW/XhdVdb+9/b3d/f
+3eJ8aXvf2exOa9BW8+the1tF58xw3etp7+ju1d7wflxNUk1pv25V2PBXWWTdz2ZfVUlq1evc
+4tfI32Dv2dvh7vrd4PZZYG9ZX3T7b2NQZPz2ffR5fV9WaGl+6eTv3ejj2Ovv6vtp5+x8dGFm
+YF9u5t3xZnbu8Ojl2tf4au3s9GxfaVlVau3y8m5w2t3i2u3+5fpu+m5+719ZX11cYl5cWlZc
+7t3b1NTOw725ucjMvr/mS0Q+Ni4sKywtLjhMY9u8tLCtrKuop6mrq66wrLgxGygpEBYgHyMk
+NKu64qGepqSknqC5rKe8vK+yyh4ZTxsOHyYfJibXrD65n6uqpaOdrbWgs8Wrr7G0KRs9Ig4d
+JB0kJj60Ucmiq6umpZ+ruqOu0q+vtq/aIyJCGhMlIyAmKvHJPrOmr6qmo6O2rKC5wKyvsLPt
+LCMzHBUgHx8nK07ZSLGosamnp6e0rKW7tau1sLPQMCE7JRQgJyQmKlnOO8OptK+sqai2taWv
+vK+vsrTBQCctMBkdLCMlLTLcTz20r7ivrqaosqims7Wyuru+5TolKzQeIDIpKi8vfVY2wLvH
+r6+tqLW0rr67wPjA7WrMQkh4RVlPOkY/Ojw5Ozs5R1dj29vQwMXEwsW/xdDOycvIw8bEx87V
+3HFPQjo6NjA2OTdGX2Z8X/LPdtrCysO8u7i7vrjAyLzE8XxYRz07Pjs5OD0+OTxMTVXta9TD
+3sy7zb2817e0v729vMHe8Gp4Rzg5NTQzLS89PztE1s3OxLyvuNa0sci+18S62srN185ORmpD
+MzEuNzktOltMTEzTtsrIsrW2wta4wFnS6MvFaNPA1lRLReNFMjs1NzwxQO5VU1XEtczDt7i2
+z2+4v0baeNfHS8fJ6V8/flo7ODY4ODc8XPlYTHu4xd+4t7S3a8i2UuTLT8TY28Pe10tNbD43
+NzMyNC89al5lXMKz3sixvLvG2brGYtLD6MPCy7ptZ0tRRS0xNzIyMj3sblpi4LrIVbq1wb3h
+wbdf3MTMxcHBvMvqRlBbMy4vNDMwM0blaGHezLjIc7e2xsPfyrxx88rLvLvJu79bT1I9My0t
+LS8yOFX+8tLfzLbK4bi8wL/mxb33zNO9tMO4vsrsSj84LisqLTAyOk/m2szY2L/G3se+vr/a
+zb/VzcG9srK7xMFqREEwLC4qKjI3P/fhzb/MzsPDxs7Nv8hyduPSzta/tLO4wMfOTEI3LC0u
+LS80PnLt0L/Czsa8ydDJxMbzU+fjetPBuLGyuMTJckQ4LS0sLDAzOlBp3MbFzs28vM/OxMnb
+WF3Z+ObIvbexs7vA1nFINS4uLS8xNj5MdtTOysfKxsbS2tbmYlZZ+9nPwrmyr7G3v9lmTzwy
+MTIzNDY9SVhw3tDU1svM5uvee25bWvbayb64sq+xtr/eaU06MjExMzU4PkdPf8/MztDLyuBn
+entwaVzy08q9trKvsLW7z3JPPTQxLy80Nzo9Rlrj1dbZzcbb/u3/4/Fk79TIvLaxra6yusx/
+TjwyMS8uMjQ5P0de3trX2d/O1F1dXGPzYXrQybyzrqursLjJ7l49MjEvLjE0OkFGWuXa1dbp
+0MxuXlpe/Vdc1cq+sq6rq6+4zVhOPy8tLjAyMzlIVP7Pz8/Q8OvL1GpOT+5rUeTGurGvq6iv
+u91CPjctLC0wPD1DY93GvcPK2UY8TVdLRUnPymHfvq+npqOkr79KKCYpJSMmMVf1zLm1srC7
+0Fc0Ky4+SUxM2LrGxLSpn56foq/2NR8bHyAiJzDqvLisrK2tue43JSErPEx737uwvr6xpp2c
+nqe6UCoZFx0fJSs5yrWtp6uvr8BVLx8eL1/Qxt22rry6tKacm56s9jghFhkdHikxWLm0qqar
+r73mQCcdITTaur/Hubu+u6+inJqermcsGxYcHyQsN86wrqyqrrTGTjAjHSNBwrC6xLu+v7qt
+oZyan7JKJhcVHB8oMD/Crq2qq7K6zUouHxwoXbStvci/vri0qqGbmqK6OR4UFh0hKy9Lt6yq
+qa23vdk/KR4dK+WwrLrIyb+3r6egm5ulujEaEhcfJzEwT7Spp6iyx8v5PiccHS7Bqaq609W/
+squln5ydpsQrFhEZIS43NFu0qKOnuuHoWjomHB40uKamuuTwyK+nop+doavaJRQQGCM6QTz3
+tqegpLppSUE2Ix0gPLCipbjnVc2vpZ+en6awTSASEhknRUlL2baln6W7Vjw5LyUfJEqwo6e7
+cE3RrqKenaCrvDkdExQbKkZT3cWwpqGovk43LighIC3jq6OtvmhmvauhnZ2irtosGBMWHS9C
+VMu6q6Ghq8xAOC0lISE2wqmksMta67Wmn5yeqLdDIRUSGCI1S37Dr6Sfoq9nOzIqIx8nTLSl
+qLfaXsetpJ+epK3IMxwSExkpPFDVu6qfnqW8PzUtJyMgLd2spKm53d64p6Cen6m2XScYEhQd
+LUBvxrKlnp6p1zsvJyMfJTjDqqetwdvKrqSfnqOsvj8fFRIWIDFI5b6toJyerHc5LSciHyc+
+uaqpr8PXvaqinp6nstsyHBQTGCIwQ+C5qp6cn65jOS0nICEqT7errLfHwrGmoJ6fp6/iLxsV
+FBkjLDZOv6udnJ+u2Ek1KiMhKEHFsLC8ysKvo5+eoqqxzjgeGBcaIyouOGK1oZ6fqrzWTzsu
+Ky08X9PO9WLRuq2oqa61u8XcTDQvMTc4My8wOU3ZzMnQzsnGyNDU0t1cSD06P1jNvbm6urm5
+vMDFwsDF1VI9Nzk5OjY1Nz1DSlBTf9TK0HdJTF3lzcnKz8i+u7q5trGusbvRWk1NST0yLi8y
+Nzk8Pklk3ORlT01e3c/V6m3y2MrEv7qxraywub/Fy9J0TkA5NjIwMjU6Pj9BQkhUc9ve92Ne
+buzb1M3BuLGxtbq8vLu9xtH6Y15aT0M9Ozg2NTQyNjxHVmVjXmHq2NTOycO+vb/Bw7+9vcLI
+zszLy87Y6n5pVEY7NjU3Ozw4Nzc8R1f42M/Evb3Ax8vNzczO2d3XzcS/vr6/wcfTd1dTUUtB
+OjQxMTQ3Oj1MetXIx8nJv8DH0+lpXWT31s3JxMC/vsHCw8PI1GNJQT8/PDg1Nzk8PUJKXdjE
+v8PLz8jP3uryavng29DQzcrHx8nP2ODe4OVtW1NWUEY/PT4+RkxPUF/ay8/Y2Nza4N/rZEnG
+w/PV3mhpYOO8ydPGyMfI7HFVTk9IPjk4O0ZNUWDr0cjFz87Yz8ruX11XWGFQSD3rwdBiya+3
+wLezt7tSOjs7My4nKTNH3cvHua6xtLzaX1NGOzkzPlJMZdTKvrq5srG1s7nAvtBIMSssLS0q
+LDvywbq4tq+vuMxuUUE4MS4yOkJT6su5sLCvrquqr7O7v8FKJx8kJigoKDnFsa6sraytv1s5
+MDItKSs5TvfFvbGtrrCxtq+qsbu8xe1EJR0jJSUrMEK2qqyqqrG1zzcwMi8tLjBHzMG8t7Gu
+r7q+ubOvusTA02w9JR8pKSYtMUy2r7Osqq+21DYxODAtMjRLvsbAsK+urrvKwL+8usbKy9tJ
+KyUsKyksL0S/tLWyr7G0xUc7Ozc0MzZF2sfIvbi3s7nDxb++uLvP8+lQMi0yLi00NTrsxsa6
+trq4uM9fYks9Pj47RF1r5sq/wMC+v8TCwcW/wOJVbVo+Oz02MzpBSFVq68zGzc/P2N/zaWBt
+6djX6+HY4V5QVF9lX23dyMC/wcC9v9JsVUpGRD46PUNHSEhOaPPt39fPyMXM1dje6mJNTVda
+Vl7608K+wMC+vcHVbFxRR0I9PD9CQURLWfXh3tDIx8XK2+fpa1FKS09VVFBi49LKw7+9vL7C
+ztzibktBQj89PT0/Slde7drUycjO09be62dWXGVaU1RWbdrUzMK9u7u+wcbS911JPz89OTk8
+PkdPWfjYz8zM09LO0/BkWldYVlNXet3Uy8G9ubi6vcHK31hIPzs5ODc5P0VNW2ru1s3Ozs3N
+ztduW1tQTk9PYdrQy8K/vLi5ur3CzG5JRUE7OTc1NztASVFd7NHLyMfKy9Dmb2NYVVdZXnXe
+ycC/vLm6vLy9xNhVSkc9ODUyMjU6QkhPaOnXycLAv8PM2/ddV1pgZWp839LJw7+9vby8vsbU
+91JLRDw2MjI0NztFT2Hp1MrEwcHFy9Xrb2JgY2xqa2ry0srHxL66ury9wMx5TUY+ODUyMTI1
+PkpRb9bMysjIyMnMz+JtZmJmaGBpbubMxcC9u7m5urzF1GVEPjw4MzIyMzQ6R1Bs18/LxsDC
+yMjK1u1vY11qfmNg5dDKw7y4uLi6vMLO/kc7Ozk1MjAwMDU/R0z5z8rFv77Ew8DK2upnVFNc
+Y1/11s7EvLm2tba5ur7LXD07OjUwLi4tLzc+QVXazMbCvsHCwcrV3P5ZWGFha+LPy8S7uLe0
+tLi8v8f8QTo7OjIuLiwsMDk9SO7Lxb+8v8TCxsvP3GxVWFpm3dHOx7+7trO0t7q8xe1EOTo4
+Mi8uLCsuNz1H8s3Iv7y+w8TDxcrdZFlbXnjdzsjCu7aysLG2u7/RTDg2ODYwLiwqKzA3PlbZ
+ysC8vMHEv8DK1v9kXGPf0s7Hwby4tbKzubzA1kc3OTo3MzAsKSoxOURu08jAvsDHycPDxs9v
+WF1y3NHNv7iysK6srbG30jUkICEkKTA4OD9vx7qwrq+5y1E1LSw3SvfKxL22sq+vrq2tqqms
+tNE/JRURFR03u6inr7i5t7W9fjYjICUvS72tra2vuL/Gzsm9sqyqqKuywEwiEQ4SHlKnn6a3
+z8WxrLpEJhwdKD7Er6ytrqustL3KxbWrqKquueBIJxENER1Qpp6ov9zFsKeyQCUcHixMyr62
+r6uprLvc2LipoqKor8D6PR0ODRMkwaGgr+54vKmoxS4fHitI0MrXxLCoqLPaV8uqnpygq8Fb
+SSkRDBAdTqafrlJNvqqjsjAdGypdv7nQ07aopK3SQku1oJ2eqLtqSCsSDBEh1qSfsD49vqej
+ti4bGytZz9d9za2fn69jPlawnpyjr77LZiIOCxQurZ2hzy4+rp6j5SAXHTnZ3WFTxqecn7xK
+TsGnnqCtwcnLMxMMESW9oKG8LzK7oqPALB0eMEtEQE2/qJ+iuezCrKGgqbre0r45EgsULbGg
+ossrMbWjqmcpIio9QDMvRrSjoa3XzKqcmp+t3jxEOhoNECa8o6CsSzF8sLk+KSw/ZU84LjnA
+qae19OSunJaapLdcQCoTDBEpvaqorL/MsKv0IBonT+1USVDPsamyTzrVp5uZnKOyvPMeDQwa
+Rbq0tLm7qp+oNhobLT4yLj+8qKOr2Dc+t6CdoKWrr7gyEw4aP8VoV8a0qZ+jXh8gPEMmHy/M
+rKSnvkBVrqGkqauuvFMjFBUpvLVfQdiupqe6MCApVV4tKEG7r7W+0lzRq5+fpaqyaiUUEh4/
+xs3WuamhpLU/Jyk2MyUmXqqjrcpuY9i9ta6nn6G2LhQNFzzBTDLRoJicrEkmJ0n6Kx0ovKCh
+r8lo7bisr7mvoqRqGg0PIU9YMjyumZaeySojNNxEJyrDoqW41sjDv7awr66npLkmDw0cODQh
+J7KZl565OSs87j8tOrKgqMlQ1Le6vr+4rKakqkwWCxIsTCoaLKWWmrk9UcG8Sy48v6emuH5u
+vrnVW82ro6myxysTEh4uMyQmX6+su8q4tbvMWVjuvayuxfbMvcjdxLWtqamuyy8XEB00MR4c
+RKeq7UG7qK/IZ+K4rq2+RlC4rsdF56ymr8R2RTUqHx4pMC4qLmS9x9vLtK++zsa3rbLLV1jf
+yc9p4r+5t8FbODVKWUI3LzVNWj4uM+23u29Yx7Ozzz9qtbh2WF1NzLfJaElAx75AM/K1w1JD
+SU9LT1FDRem7zUbnv/5H7VtGxMRLYMPpSeZZPcXBPdutwT9usb06Ms26Uzk7xbw/Pk9XVk/u
+WtDKxVhPv2s7R/ba09LP67W/P1S5vV8+NL6xPzRLfMdJP1p2uMs3TMfoRDvg0OdPT8Wzzzjs
+t97n2tbazdJcQ/69OkjyUtdJPsD9RXpN8kPe20rY3c3J/VTf1MO5Sk+0XEm/TUfcfd88aN1a
+eHJETsBBP+5c69E+17fFaVjYxORs1tjNzHdPvkh52D9R0OpPQmDUXDpNwlZE/3PS2G7TY7xx
+PPHPzPre2Ly/PcTXVt8+789LWEJN2klM73d90FVOwM/MQFq6X1pO0udbwOZfytbHZGDAXF74
+Tcw+Q09lZz/M+kTL89lpTr9vS9jPTkzN4XPK6WDPvehd491w+19jUm3VSGF1WnpKyeI/w8xB
+ys9CVMnhR+3LSODHddJj9XvO60Lt5VDM+0DOaVPbW/3QRN3EaO1L+8tjXOTaWXvJ6091zFLf
+a2tWP7u8QUG1X1jOSOph3M1XeGlRV/vMVT1sv3NJXM9o3MxU3Flhydxa39Hk//TdalfTW9zb
+TWRpeWnVSFTha2lg7EzX8XXxR83gd8DcYd/J2tpTbMjRWWRpdfBDalVO2VJLZ+JmSt7OR1zc
+V8vPa+/C69i+X0XUuF5Wannv5Fhf+ERrzUlg/Evv90rR/D7R1VbZXdzCUvLF6f3f7c/oRNTN
+Vm5iUXnu3edGWOJtY2NmSdTVTMxqUMnUy89HXfPfwE1V2vvo2tBbTGvt1+9OUVlibF1s0llN
+2sfsXGvY11x22nZVfs3abGPi7mPcde73TGnN5WlbTujvc9llXGR42tbvYl5e595kZWrt0NVs
+Yul+3dR/dFZm7n14an5fc/D2bVlrWlvt7vLtbe/a8PHsbuxset9+f+b2annu2utp8e7b8mrn
+7u7mfW1kUGt9XnJ9eW5dfedkZnDrcG3u5t3z/XV36/n62fVk4uv8dm7l39vd1/pz6XN1/Hdt
+YldYUE5dXl1kZ299fu7hfOro/e3v5dra1MvMycjMzszZ28vV2vFfSjMsKysvOUrn0t/Lvrq2
+uL3Dy8nFy9Tt7tjSzMXFyMO+v/UzJyMhJCw5TvLZw7m2s7O6xcvO1tHU52RNX9fIvru7u769
+ub71MyMfHh4mNUrWvrewrq6vtMV7XlJPau1qWFV4y7qwrq+1ur7D2kYtIB0dHiY1Usa1sa2r
+rKyvvfg+MjI5Q1j85NLEt6ypq663xc3WfkUtIB0dHiU3Zr+zrqurrKyvve4/LyorM0btwbez
+sa6rq66zusPQ+UcxJR0dHiErQOq/ta+rqqussL9dOy8rKzBC3byxr66urq2usba8yf9EMCYe
+HR4hKDJG3ruwqqeoq6+5zUo0KygqMkrHs62rq6ysrK6wucXnRzkuJx8dHR4kLkvFsqqmp6ms
+r7fIVDgtKSw0SNG5sK2tra2tr7O7yelWQTYrIR0cHyUuRc66s66sq6usrrfNSjYvLzZCZtDE
+vbiyrqytsLe/ydZtRjMnHhwdICk1TM+/ubKtqqmqrK+73kk4MC8xOkzqxbq0r66vs7m+xNBl
+PzEoIh8hJiw2RP3LvbOtqaamp6y30kUzLS0wNz9Mdc2+t7Gvsbi/yM7fWkQ1KyYkJis0QV3Y
+w7myraqpqq22ylU7MS4wNz5LcNTJvrm3uLq8wcjS6VVBNi4rKiwyOkNQ+8q9t7Gvr7K6xuZW
+SERFSU9VWlxu3szDw8TFxcfHydd+UUE6NDEzNjo/RlJv59PKw727uru+xc7pXlJKSEVFSlnp
+zsbCv7+/v8LGytNrRDgyLy80OT5FTFZx2ci9uLa2ub/O7llMRkE/QUlZ5cm/vby8vL2+wsTJ
+4FI9NC8uLzQ5Oz0/Q1Dgxru1srS5v8znXlZTUFBOTld+1cjBvry6uru7v8rtSTcuLCwuMTU3
+Oj1Ha8u7s7Cvs7nAy99rXlNMRUBBTHHMv7u6ubi6vb/H3VY9MSwqKy40ODo8QlTZvbGura6w
+uMTS9VtMQTw6OkFX07+4tra4uLvAwM5bPjErKSotMDU6PkZezbuxrausr7fAz3RMPzs5Nzc+
+Utu/t7S1tbW2u8LI5Ec0KiYnKi80NjpAW8u5r6upq660vM5pSTw0MDE2PlPVvraysLCxs7a6
+xPdGMygjIiUqLzM4QWjDsquop6eqr7vPVz40LiwtMj1b0L22r6urrbC2vMPUVDcqIx8gJCov
+Nz9nwa+npKOlqrC93Uw5LiknKjFCfs7At66ppqersbrDz2k/LSEdHB8pMTpDY8OupaGhpKmx
+wmlANCwmIiUtQ9S/ubOspqOlqrC7xNBgPy4iHBodJjI+R1XGrKKen6Oqs8tLNy4pJCAkM+K2
+sLa2raajqK+6w83Xb0YxIhsZHSk6REVbvqmfnZ+nsMNiQDcuJR8fK1S2rbC4tKulpau1x9rU
+ztxJKx0YGyMyPTw++7KinJyhrsteTUEzJx4eJ0K2qaqwuLGppaq64l3dvrzcMx8YGB8xQjs1
+SLagm5qfrvA/QEY1Jh0dKuiqoqevubSrp6zAT0L9vbnVMB0WGCQ5TD86WrKfmZmivzkxOT0v
+Ix4gNrOenaW2v7OpqbHfOzhZvrlcJRgWHS9PWkxYxaqcmJ2yPSstMzApISAtwp+anqu6u7Ct
+sMZALzj5v9otGxUaKlbP1tK9qp2Ym61DKSUoKykhHyy+nZebpbO6ta6vyz0vNk72TSoZFBor
+bcO/vLGkmpedtjgmISImJiIiM7acl5mgrrq1rbDQPjQ4PkY3HxUUHS5zurCuqp6XmKHELyAd
+HyIhHyj7ppuYm6SvsKqpteM/Nzk+OiIVEhgjOcewramfmJidrUklHh8iHx4lQ66dmp2mr7Cr
+qKy8Xj48PjklFxIVHjPJsKynnpmYnaxPKB8gISIiJDe2n5uep7K5raaptdZJQD80IBYTFh0y
+w7Cqo52am56vOyQfHyAjKDDtq5+eoau3ua6qrbjWSj06KxwWFxkhPcKupp6bm52n2i4iHRwf
+Ji93raCen6e1vLWvr7bGZEI5LB4aGhsgMnW4qJ+dnJ2mwzonHhwdISparKGdnaOts7KztrvT
+S0E4KR4bGxwkOWG/q6OenJykvEMqHx0dHidNsqGbnKKqrrCwsL1bOTItJB0bGx0oP9Cxp6Kf
+nZ6nuUsoHhwcHy3drqCcnJ+kqq6zutVFMywkHRscHSMyTcatpJ+enaKuyDskHRoaITu+pp2b
+nZ+jp6qvxE8zKiQeGxsdIS1B2LaqpaGfoKew1DAfGhcbKFGxop2cm5ydn6a08ToqIRsXGBwf
+K0Pfu6ukoJ+hqbLARyoeGBcdLs6qn52dnJqZm6O6QSkeGRUUFxslPs+wpqKgn5+nr71KLCAb
+Gh4pSbemn52cm5qanafEMh4WERAUGCAwVLajnJubnqm0wk4wJRwaHShFuamloZ6cmZiaobc+
+IhYQDxAVHitIt6ScmJebpLPpOS0iHBsdJT66qKCenp2amJmer0IfFA8PERYdKD+4opqVlZqi
+tk0vJR4bGh0nQrilnZybm5uam6K4MhsRDw8UGyEtWbCelpOUmqfIOikfHBsbHyxcr5+bmpqa
+m5yhsD0eFA8PFBogLUa+pZqVlJifs1ovJB4cGx4nPrymnZqZmpueqL80HxgTExYaIzRzsaKc
+mZmco7HtMyYgHh4iLES+qJ+cm5yfqL4/KB4aGRkcISxIvKifnJyfp6/CWjgqIh8hKj/Jr6ei
+n5+hqsQ9KiEeHx8hJi5PuKihn6GnrrzXTjkvKykqMUTRs6mmpKevykkzKiYlIyQpL0fGr6ag
+oKWsuNJOPjYwLi82RuK8rqmnqK7JPy0lISIkJy03ScqxqaSho6ivv2Q9MS0sLjVH1LmtqKem
+qrpQMCUeHR4fJS5Bxqyjn56fpauyyU81KSUmKzlzv7CppqSlrck9Kh8dHR0eIys+u6aem5yf
+pau2zE4zKSQmKzzjuKyopaSot1o0JR8fHh4fJC1QtqaenJ2hpau5z0YvKSgqNEfnu62oo6Wv
+2DssJSMhHh4iKDzBrKShoaOkp66+ZjctLS84RV7Lt62npKzMPS0nJiYiHyEnNs2vqqakpqam
+rLxvOi8vNDlCUt25q6alrMRCMy0oJiEeHyc37LevrKikoqOqvGxBOjk2MzQ/6reqpqeruWU9
+MyojHx0eJzNTv7SuqaOhpKq31U49ODIvMTxmwa+pqKmtvkw4LiciHx0gKjratq6ppaKjp6/B
+bEM5MS4uNUzMt62qqqqx0kEzKyUhHh4kL0vDtK2noqGkq7jOXD41LSwvPfrAs6ypqay5WTov
+KSQfHR8qNmDBtqujn6Gnr7zLZj0wLCwzRt6/sqqoqa7HSToxKSIfHiYvPF/JuKqjo6issrrJ
+VjozMDI5SPHBsayrrrXIVj8yKSMhJCsyOljIt6ynqKuss7vHVj47ODc8SujEt7GxtrrIVj82
+LSknJyw2PFHVwLStrrCwtbzD4lFNSERHTvzIvb27vsHFb0E9NzAvLi43PUNh28q7t7u4uL2+
+xNrua05PXmDcz9XGxL/HzWxTUEU8Nzc0PD09Q0xk0srOxL29u73HzdHjf1JVZGz36tvIw8vL
+18/k309GPTw5Ojo4P0pUXufSwLy+wMbExtd/U1dZXVrkdsPDvri/v8jR9WVGPDczNDQ1OT9L
+YunPwby8w8zI6thPWlhZaejRxb+9u7y5w7/tYUdIOjg1NTo7PT9LVerV1dnLz8rL+Odn2u/V
+18+9w7zKv8rD1tvtXllESEBDQ0FAQ0NERklLVWZjb+nPy8rJysDGv8bCysve29543/Ltaext
+22FmV1NKQz06PTw+RElPZOLbzsK8u7u/wcjc6+1y7Ofl5Nnn1NXa9edm+FZHQD9APz88QEhc
+Xmfq3MjIz8/Nzc/O4dXTy9TQ2dPO09r2aFlfSlVNV2ZvXFpgZfxdTk1TUlJPTlB5393Xysa/
+xc7SzNn5WmBeZVhZXVzf38/gy97J73FJUktMQ0FDSl5e/G7RzMbR39XW1O1sa2V47/5y2czG
+1M/i2fbzWlpmbmRMSVBaYVZWT3z8dlxeaufv/Gty293b9tjWztbo6eXX3+9me/dp5v3l2c/n
+cVJtW1tIRkhTWldYXObX2N7n1s7T7GZ9fG9mVHrs2t/k4dnP2M7Xy87NZ1tRV0xHPT5JWGBW
+XO3Uz9d3bG/X6O5Z5+3nX+jmzc3Tx9jEysDKycnJ9EdAPkM4NzM8Sl9OXHTSw8bUeG/i3d9j
+ftrP237U2rrHv86+v8DKyMXUVj83OTYwLS01RGvr28/CvL3I3FZfV2BWY/zb183Jxb26uLy4
+urq+vsr7PS8sKyooKS05WNPGvbi1s7zT7ks/P0RO7tfMv728ubu6tbm/vb2/w9dHMSoqKyom
+KC9I0r+6t7CwtMTvSz44OjxO6c7Dvbu4tLe1tri5t7q7xV40JyQmKCcmLUHLuLKzsrG0vGNH
+OjkvLjh9wry+vLKvsbm8vLa4uby+zkwuIyImKCcpMFS9srG0trW2xlE8ODc1OT/evbi6urmz
+srm/wLy5t7u+zU0vJSMlKCcqL0zDtbG2uLe3v/VEOTk6PUVezL26uri3tLe9w8G+vr7DyvFB
+LScnKSwsLjhev7e1u726usFtQzs+QEJFVNjAu7u6urq8ws7Sz87R1c3X9UM0MC8yMjM3QvvK
+w8nIwLu7yHBLSUpLS01f383Gx8TFytPd1c7P2tPLxb/G3Eg8PT49Nzc6QU1acOXYzcnHzNvu
+dGVdX2NoX2v6+vzt3tPLxL/Aw7+8u73D205ERUU8NC4vMzxKWmnlyr+9xNDs6u9rUkpKUFdg
+d+HOyMK/vLu5vLu4trrKbUI4NTMvLCwwO0ho0cvFwL/Byt9bTElJTE1KSlvPw8HCv7y6u7y8
+vr23uMDfRzQsLC0uLzM6UM68ur3FzM3O3k89OT9OY3ZgYN7Lwr/CwL28u7u6vMC9usJnOisl
+JiswNjxM1LqvsLzPdlxVSDw1OE3PwcTN39vIw8bO1cm+uru9vbq2t8VHLCEfJC05QlfQt62t
+ts5XTEtIPTY4Scy3t8bmbuXU2d/r28S6ury9vbi2vG4xJB8iKz9Y+Ni/s6+zw2lJSUdFPjtE
+5r61u+9GQ0965unhz8C6ury6trO1vlUvJB8lM1nb09LGuLO4ylhBP0A+OTlF27u1v2A/P1DZ
+ycfNz8S7tba4tre5wlQzKCAjLlnPy+Tlwrm2xlo6Nz1HRkVcyrexvGM+OkrYys7b3c29tbO2
+ubvAy3o+LychJz7XydR4z7m0vO06MDhJVlli2b20t9BANjxmx8PM29O/tbC0vMLGx9JROi4l
+Ii1Wy8Xdbc+8uslOMzI+ZOXg7dO9uL9eOzhI18XK3+rNvLS0ur7Aw8XOVTosISU8zb7KUE7V
+vr3QQDM6TufyZ2fLurrQRDY7XsrAyNjRw7mztbq+wsPF4kYwIx8rXMPEW0FSyb2/Zzg1RP/j
+4fbavbS8eDw1RNfDw9L/1ryysbS5vr6/0kwwIR0oYLq6XDxKyri7cTQvPF1yZlfpu6+21jwy
+PVHOvcre07+xq6yyur/Izlw0IRwjUbS28zU5eru51zUtOGvb8V1tv7Gyzj0wOtu4uMtcbryt
+qq23vsTH3z8sHRon4bKzWTVJyLS2WS8vRdXVbE//tay0/y0qPcu3vHxQ1LOqq7C4vr/A3UAq
+Gxsuw666QDROwbO9Qy0yT9nnVEnbsauyTikrWLavwFFUxrGtsru/v7/F3EAkGR08tK3JODrZ
+s7DLNy074M9hPj/IrKrFNikx1rS4005fva+usbm8vL3JeTQdGCBWrrDiOkrCsbhVLzFQ1elG
+Ply1qq3lLykv6La2xG3/vK+trrO5vsvk8zweFx5NrrFdMkO8rbRNLC9dyO09O/GxqrlGLzBS
+w77BzdbIv7eurK++yttoWysZGizBrMQ+QMiurtsyLEPQ9T84S7ers+A8NztP28a4t8bQybOq
+q7jO3OneSyMXGzazr2c6YbGqu0AvO9LPQzQ8y66wxU4/Pzw+Xr2utOVOy6ulrMpvyL3cOh8X
+H025xkRAvaiueTc3Td5MNDt6vLbH29vnRDQ71K+uzU/hsqmuwMvAub5NLyQdIjnsbk/quKy2
+Zz0/SFRGPEnQyMvGwMJ6Ni9HwbbC79m4rbC8v7q2u8xMOjMkHiY+1XBlx66tzDo5SH1hQD9Y
+z8HFw8hhQTtF18fe+M63rq+5vrazvvNKQkEwIiAsTfxM1bGruUw4SfNWOzlMzb7Iysn0S0xi
+5uxRTM24t7q7ubCwv+ZfV1VFMyklKjxWT8izscFbR27xPjM5Y7671XJu3MzNak5PS1rNvrm1
+s7OussdaSufjRjInIiw9PkTHtrO/dfnUSC4xT8a+1lnhxcLO3GdNSUdazby2r66trbvpXfnW
++DwvJyMqNjU4zbKvuMnY2D8sNfLOaWTcw7W+2s/oSkdGS868ubGsrbTG3dbOc0xUOCghJS4u
+LlyurLe/vslHLTJPTzdptbe7vb283DxG5Vtov7Svsri3tcpSUHxdac85JCkpIik3cbSzxLSz
+bDs3NFXLP0yvseDJvcDNQT3KxFPetK+1uru4ykRKVzZxrNFZvC0WKjseKOa9rb43vaw9LtO+
+wchOy7JaP7m2UlTQw7/W4re06G2+v9PTybu+SD3DzjEYG9o3Gy+stts/3qp7K86saUDAscJU
+W7ixTEC5tevvvbjBY8mww1HJt+Q4N1r9OzdBIiBaNCM9vtrMTFeuzTfPrsHSxL29+0/BsM7+
+u7jT89HBvuLfv8XdbkxHVDgzTl1DKidBPicvy8rrTc2zyUbXuszdzrzLUnu7utTKtrnqatPH
+yNfCvNH/11VDTT85SlpbPC0wNDEvPVzLXX7By2tTzMfE2sW/x+ryv7/EvbjAyNHUw8va09pp
+12I/UutRRmhbMy0vKis7QUVbcc7MeNC/ydbGvcTe7cnDw8W9ucPXxsbY1tbQyuBd09xJPnXi
+RkNFOi4qLTI4QUlpxs1iz8nQzsa9u8PPysXN18rBxMjKzMfS/dTE5Utt50xP9Xb1+Us4Lysu
+Liw749xwys/N0vfXusbZvr7J2tLKwdjjw7/P3cfD0W7qyuBMWNt9VWdrXj8sKzAsKzdP+uzf
+xMjU2srExMu/usnWy8bQ3NfN09jIy8/Oz874V+XuSVzuceTbTzYsLCwrLzznzXbSusxmz7/A
+xs7JvdZezr7L2tbOzubny8PK1d7My1hY0NROTNjwMSMsOiwpOc+/Yku/teJSzLi96ti4u/71
+vrzaZdjEz1p6wMHW28/ObUZQ5HZc0/QvJy8vKio37b/8Xriz3lbJuLrGzLy78lvMvtV05MjG
+9lzLv8zl1L/UREjg3G/pOSIuPSMhOGXLyli5q8pMyrzEz+PEuc5bxbjLdPbQyuJX38PF0NPI
+xORj2+xb+zkeKUAlIDlsyLj5v6e9PtzI7c7v/7a3b8e2y9zXX+nKYFnRz8/Gy8rH+XLc+Ghe
+Jh02NB4lQ9O0vOSspMU/8cjO2VHlsrphz8HP1W5M08hTVdjMxsrh0czvaXTvz1UlITYxJSo6
+6bS2ybKqt+VXWc3IUVLGv83fbNTB6U3jydXq5tjK1GBc/Pxda9LFxOA5LC81Li0xP9a8yti+
+trvCydTGv99cdGty4lRI7M/Uz9fayMx7XltgdFxX787R8mb7dEY7Pk5cTkdDS2Dx/HDcyMPE
+yc/U3PP1ZlVVXfDe39XJzNxzX23o4ejc1eFqYlpPVGN79H1jY21dTUhFS2vi9Hfy69/f4+Xv
+9G5t6N/o7H/y3N/6/dvNys/S1eF1XlVUXWNk7+De4XJeZnVubWBbV09LTFFPTFRr+Ozr3s/N
+2urp+vbf3d7c19PX63J8e2tt8Obg39ze3tva2uLwdmtoWk5KTUxKS0tOVVZWZevp69/u8+bk
+4NfU1tXW2Nrj8evg4+/w6NnX3+7s4OHq9nd4alxbXlxYVlFSWlVQUVNVUVVo6+n58+zq5OHc
+19XS1dfY2tjU1t7j5OLk6u18YF1iZGlkX2Bmam50YlpecXNiXl5fXVhUV2JsZ2dw8eXh39rb
+3dXZ3trV1dfV2Nbb7Pt5aG7x8XFjbX1kX1tXWmBubWJeXF1dWVlZWl5n//Lw7evf3+Dh6enf
+2NbV2Nvc3uDi5uzv/25ycl9gYF5eX2JobWtxff19bmJdZW5oZHZ+fvb9/f/9+v717evn6Pfx
+7+vn4t/e5eri5/BtanBoX2VoYmZscn306ejg6u/sfGllZWdmZF9mcHl79ezv6+/7a234+378
+/v3y9XJ0fHr67PJ9+PDw8u7o5uvq4url5Ont/nh2fnhra19bXV5cWmJgXWhvcXv0+//78+nl
+5Ofl3t/g4+zv6unu/ndueff07e71dm1ua2xscnx0dGtrY19ta2x0e/f3/XVvb3Tv7Ovu8e/u
+6e/q4N3f7/v7fPF1aG/7fW5tZmd+8HttbXl+8X1taG1sV1d65P9n7tzW6Wnk1uD2cXZsc2Bc
+bmz65uLu69zq7+vm8+zsd/zvdnPxdmv783Jtb2pjYlxQTFBWUE9TXH7o597QzMvLzc3P2N3e
+3dPP1dnPycnN093zVTsvLi4uLjA6V8u/ubGtrrG6yd5YQDYzNz5HTvzIvbi4uru8ws/f6t/i
+9+jOz2tRRDIsLS0sLzdIzb26sayutL3WXEo7NDY5Qlj21MG4tba6vL3B0Obe3eLd0M3Q5VdB
+MisqKystN0zPvLexra20wutYSD43NjtFW/TVwrq3ub2/v8TM1Nba2NTS0dPdZEYzKyosLS0y
+RNG8uLOvrrG81mJVQzg1OUFPXHXUv7m5u76+v8bQ09bZ1trW0dbuVT8xLCwtLS43TtXCu7aw
+r7W/0ehdSDw5PEJITl/bxLu5urq6ur/L1eZyam5w9e71Z08/ODc4NzU4Pk5t5dPMxcLBwsXI
+0eRlUkxKR0pX/drMycbDwcLHys/P0Nfg3t3o+2ddTUM+PT49PT5ARlBhe9zPycTFytDa6m9a
+UE1TXWrk1NDMycnJx8jJyMvMztbe4XtbVVFLRkRFREE/Q0ZLT1JcYvnb1tXa3eDb3/P37uDg
+3Nre3NjPzcvIxcbKzdHW3Oj5YVpUT0xHRUJBPj5AR0tPXW7i0M7P09TPz9bj9vh0bG5scuvY
+09LRzsnIy9HU1trb3+Di7m5aTUVDQD09PkFFS1Ff7tnT0M/OztHZ8V5ZXmZpYmvn2M/Kx8jF
+wsPGy8vMz9fe63ZhVUxCPjw6OTk9Q0ZMWPfWzMnLzM7Y3eV8ZmNtamJdf9jOysjFwcDCxcvR
+09HU2uhpVU5MSUQ/PDs6PD9DS1d039LMzM3O0NPc9WpmbmddZO/XzsvIyMbAvsHIzNHY5fNz
+bm9eTUZGQz88PDw/RkpNU2nq2tLOzdDR0t71cW5ka+/j3trPyMXEw8LGys7S1t/6cGtfXWBd
+TUVIS0Q+PkFGSk5QVV332tnb1dLV2eh8a2z/6NzX1NLNyMbFxMfJyc3Z4fRvb2hcWF5maVZH
+SFBNQT4/Q0hJSUpRZe7j39PNzM3Q2dzX19XRz83Mzc7P0c7P1dnb4+5xYV5cW1pbXWRpWUxM
+V1lOSUdJSktLS05c+OTd1s/MzMzMztDOz9PT1dTV2/BtzMjwfNHZ6/xiZ2RWTlFYWVRQUE9a
+XVdTU1lbXl5cX3b78uTa1tzf3djS0trf3Nnb5uHb49/e7O/q7/P4dXJo/fZpbGxhXVtWVltd
+ZGBZWmNqXfvR5n7s3ezx+HpsZPfu5+bl9d7b6dzl4OPq6eT+XHTwaWBv7u58d29sc3VubXVq
+ZF5fffD15/L/8+x+fnZofP7t/ffvemh05O31+Gv67m/u5ntrbnf18Ozc+Wt+anzx+ObqZnrX
+XV/laX5yZV1vYXXhZ+7y2dDb9N3eamplb1Ra9n90fuzg091mZmlWTlHfaExpytPs9ODc6Gxa
+7W9hZWb82+3+3d7Z39/u3+r4X1hiemxhY2rr42lq1ttq4uR5cmhqbGFfYmZdcelqZG717HZp
+aevu8vPk3+763tLf3nV33vJ3d+v16nx0X2Z+aWhpeG9ZYO33a2r3b/5jcOX87H3o5u3gffju
+/+r2aGd5dnH533ns+2Dsb2vq3Xhq7PT96n1y4eftdWv8d3xacGn5bmLqcut89u11+3x3YWDr
++2vx497z7+Xj7nXx5eT6bffmbF5nd2Fhbu77amj03vvr+3rp5vJmb/Do7P305n52X2Z6Y2p7
+7nbw/Gf47+5safr08/945uT48+jk8+7i/PpxeGlp/V7+e2V7dmRh6u548t3ueHpvdOjpb93d
+cG917+7ra2D23/lmZuvodV5deHZcX2nvfXB17u7u3t/t3tv15t/o5G9vZPNtXlpq7W/7+25s
+5d5j6+DvZmR88OF8bm/xf3RiaXdtf/byc2rz9HD5433w8mp45Ovw633q6Xbt9Phzbf7g9vDj
+b2v263tub+1uY2JmZmbg6XVteWVp82xtdvry9Xpk8t/jeuzs7uf46998aeTj92lf4tZtbfHk
+929gdvhva2ltYXplZPj2+Wp9a2xg+996/Hrl4O57697m6O7o+2zs3N5yaON6a2L763xybXlq
+bF1lXGNt6f9cXl50fm1nau/Z3ezn09zb3ejo9OTc5u995fV0d2JnYGVlX2h0ZGXxZnbodP73
+alpqd3V5dujteG/g6ezx79vm+n7u+ndwaPb+e+jy+3Vq/3ltbHZrcOby9/N4cn3e6fDl3fFv
+dG18YmBm+WBfaXt99/Fw++Do7+v1/W966Ozo7XP54vv8+/Z27PZ7/WjtaV9q5Hxu5+re9Wxo
+cvlxbH15bWRvbvPsafjm4vh2afvxd3Tn3uja9f336+1s/vLrZ21kXGNubXfyfW985e723ul0
+Z/7/++Hh6e3o8WFgdXn2/WVffnBk3fX8fuP+dvF77X5mb3Noc/72b/ru5+j4eH507PLocH70
++et26+7k82n49v1fZPpzaV9fa3R+8Hh4fWfr5u923d7u+1/ZXmjU6ORueGH4SFbL7W3m3evd
+fWnt6G1x6lBQ6934atzczdhq9W5fY3tOTlvl22585Obf1Wfo6Vt4cldPb/Py2fJg2dT9ZO/+
+5eNdZubwYfT6U2vsZ+HP6d3N2nxsZldpY1BWa2xy8mdm7ntiXWhvX2Jf5M/W1svJxMrWzMfE
+zszKzd88Kio3OTQzMTzcubW1trm5tbzyRjk4P0hQYtrKv73Av73AxMHGzsnIzF8zJyo2Ny8t
+MDvft7C1uLe3tbrXPzIzPUdJWd7Jv729wsLCxcjJycjHx8dXLikzPzguLC481raws7y8tbO8
+9DwxM0FUU1Fmz728vcHP1c3HxMjKxb/C6jcqLjw/MSosNlTEsrG5vrevschHNjU9SVNVUWPO
+vbe8zM/GwMHL19THv8tNMCswPzwtKC0/4L+2srS5tK603UA6PkI+PERa38i9uLvEw76+x9HP
+yMPJ3z4rKDM+NSooMlDMua+tsbeyrbTfOi8zNzlAU1plyrWvtsbMvrq/zdDJyNNVNSorNDov
+KSs5872xrKyvtLGvvE0vKy0xOUhi/dO5ra24xsa8uLzFyc7nTDQrKy8xLCgrOW+9rqqprK+x
+tMo9KygoLDhczMfDs6mps8TEvLzDxsLMXToqJCcuMComLFO6railpquwtsRGKR8fJzh7x763
+r6mnrLvX18G7vsnU4E0uIyIoLCkmLE25qqOfoamzvt06Jh0dJTjNtLCwq6ensMlsYt3LxcLG
+x9g6Ix0gKisnKUO0pJ+eoKq83E0zJR0dKV6xrKyrqKeuwmhMSVbVxL67trZPIBgcKy8lI0Oq
+nZucoK7dSj0pHBkhQrqtqaWjpq/GZFBNQz5Rv6yqrbZGHBMaLi4dH8KZlpyforZGMyocFh09
+urStop2grs5MR1ZPODThqqOprrM9Fg4aNCkaJ6SSlp2fqe0wKx8XGjPHy7yhmqCwvcxNQ04+
+LTmyoKSqp6w6FQ4WJScgMqaTkpqlu0ovJBsZJEfQwq2fnaWwyk0/SEIvLE6upKSho7M5HBIS
+GB8rT6mYlJeiu0ApIB4fLFPY1bOgnqrEy8dyPDIvMkq8qaGeoa/ZLhcOER4tQLeclZeesjwl
+IiMjJz+/vb+toaW61sfJUTMqLD/ErqWenabDPh8QDRcoR76jmJidrUkpJScoKDjHs73PtaWp
+w+LByD0pKTjasKafnZ+2RiwYDQ4eVLisnpibp+opISUqKzPOrazB8r6urrzPz+k7Kys/t6Wf
+np+r/zYjEgwSMLarqJ6an7kwICItMzRPs6m1SEO3qbbZzspJLCcxyaifnp6kyTksGg8OHb2f
+o6ehn6w9Ih8qP1FevauvTzFEvK+xu85TOS0rSK2fnZ+ltvo+Hg4LGM+mp6ihnqhLIh8rQ0pN
+vqmtTykvu6auv8DIRisnP6yenp+ltPE7IREMFEmmpKmmoKlPIx8tUXBiwqqo1yciRKefrtPY
+azEkLrScm5+osr9AIxMKDz2lpKqnoqflIx0t+9tvyKynzyclbqWjsct3SC4lM7CdnJ+mtvM7
+JRILEkemo6eopqn0Ix8x+9vlv6uo5h8f9qSistDpTC0jN6mbnZ+mt3g5IxAKFcWlpqeopqpP
+Hx8732Vfya2pSxwmq5+qsMRWPysl+p6cn5+oy0gvGg0NHL2jpaqop7U5IiZC1m9hv6yxNB0v
+pZ6su9lHNCgttZydoJ+p00UkEQwPJ66jpqejp84tJC5T90/+sqtdHyuvpairvWI3JCXcop6e
+nqW76TIXDQwY7KuopaChsz4qLUJTQkvFscssMbisq6WtyD4kIkSvpp+bnqzUQywPBw8uvbOr
+o52fyysuRlA8NU67aTFcu7CnpamxViUiM8+soJycosxM4B0HCB3JvMCtnJenNyxI4D4sMDFe
+s1k+rpudqsJiPigfNaueoamqpanvGwgFE86r2cucj503KVXEPyQcLKSnODunmJ+/z7HGJBss
+sqWxr6Ohp+UoGgsMJUM90aGZn8BZzMFIHx7GrU883qujrK6gn8gnICs6VMOxo5ujUjctEw8a
+ITHVs6ekruPWvUUy38f7fmBZybWvo52js1MnHytC/bamqbzHxz0fFRkuJiHerLbTzrmtrMHm
+v80+QU9Ox62qpZ+m5i0qKStCv7zOt6vNLSQtQTYhJca8OTl0tqu+z7Gt1TpK22pFbbGorbO1
+yDwsL0J+WV6xtUEzPWpSLy9g/D9LRVLEzMe6wsfKfU9SRE7MzMKxrbfFWTY1Rlrc0dreTllG
+N0FNbsDndsnWRDbpyHXOvMbkXkNAPz5iyLa+w7r1PDxNcca79Li+Pj5OOjfNa3qz2EvFSTzD
+vtLRzddCSEI/YdfH1MC5QjBKRi5dus3ZtsRAeX8yQsrJXsXByrpIdbvnvb0+wUREPjRw5b5d
+1rZNOHJBOFxQxNvEYcx3Nbs+TsrKTkurZ0m1cdl4vVPCRE/ONDm7OdW1Sty8SjLcQkXNa8DK
+3U7Uy0HN80S8yDrHeMlfT8PE3WDHRUc4UTnZyD212utlUjPESDux+sS/S0yvUV/gW8FaT75D
+a9s/wcPVR+bJMzpoQljlUte42EjYa1DdScvK1f3nw+9e+M9x9tVOX0DDL8fBQMLPyy+8OEpW
+fEjHy3a9TcFbX2DF6lXFU+Vn00fXxV1tbMxC6D9OzkntwHzn1kD45VQ8v1lwxUrYzMY7wepN
+4VTXUMlOyFfJWdVc73NCwi61RerWwkvpujlNwDhNvVBezM5XwV1gzUjPRtljXthOzF3aacVh
+Zt3xQlTLTcRDv2pc3k5lV9tJyk3c02DQ6dFFxEbAQuHPVr9FyEnK6nVfSMpUWF3YScRKXdze
+V0/MRsVO1WPJX+nUTMhgSsdf6tZd5NT/Rr9aUupnS71AUMXfXXH4195NWclB29pAzcxTdcll
+UtFI4/5a0U/ETclOy+dA4M5V2l9ezF5rZMRhd3p5Ze9SzlDRZM5I5ODtRNvJOs/fXUnV3199
+7/PNTcNYd8lTzNz+W8ZWT+fwPsTVNr16SVq2OFvBQN9QdmDa4F1s29rhR8/RUcFOabpWSOrC
+WlXtftLjPMNLvT5i0WLwSebs1kTa1lpZx0bP4D7MdcdAUsN+60nOyU7OR9zFSdVNwW1JuURc
+4VtkSL03wXVEykXDTs5t2ljc6FLQ1k/L2kzFVH73WfHf+kLM1kpV+sY7zFZayVNV/79Ecm/a
+y0zt0r9cT9HT60Te1FjUPfG3PmdbwetG5N29QevkdL9Hb9DuR1DnRW48PshGQWZ41lXjz8rG
+Y8vMxNXfvMHNy7zSy8PzwLx2KSpyMh8oSeNFNMqowTjsrso6TLa4R022ruJZs6/G2MS3w2C8
+stcoJuQtGSBYXC85vKTEPsC5zjlAuMNEfrW83cm5tcHKsrPFy7q0xz4mLzkeHTVONTrSr7Zq
+x7HkMUi9SDPYt9pXybO64Lmqsb+yrsLMvMAzITQ3HR9BSy48xrfHZb62Vzlg0z4628Xg8rex
+wbevsLe4srnHxLzNNi86LiAlODErOuju1cG9yOHuak9CTllM0rq/uLKztLa1s7a+wcPCw009
+QzAoKSwqLC41SvjNvL/Ew8hjX1I6UuHtvrK4uLS7uLvGu7jGzc3L0UlAPy8nLC0oLDI+Y8/I
+vL3Gvb/0XGFKWNrbv7vAvrjAwr7FxsLK0M/U3fVWQDMtLCwrLjA6W9TFvLq8vLm+3mNRRk1Z
+YNfM08W+ys/FwcPHysPL6tjN6kM5ODEuLjQ3Ok3ZxMHCvbzDys9jR0VLWFtr2cvKzMjKzcnH
+xMXLzM/U2OZVQDs2NDY3OUBV7M3Mxr/IzNHdalRQU1FPX+ze2c7L1NfOysXEwcDH0NHR6VtG
+QUM7PD9ARkxVbOj77tfY4eLyWE1KS01VcNzRzc7KxMXEwL+/w8bHy8TAzm5LPjk0MjI2PUpZ
+deDc0MzP3HlXTk1PT1r+4NPKycrKycLBwLy8vcDExMXQWkE6NTIyMTI6Q1Ju+9/VzMvO0+xe
+T09PU1z/2cvEwsTDv7+9u7q7vsnN0tp8STw3My8vLzE3RE9m3dPNy83Oz9t3YGBdYfbe0s3I
+xsjIxr+9vLy+wsrS5fVrTz43NjMwMjY5P09139bOycbJysnWdWBeVVBb+N7QzMrJycW/v8LE
+xszW6Pzu9VZBPDs3NDU6PUpr5tnOycfGyM3XfFNMST9G3NP36Ma/x9PHvL3Jz8jC0X3bydlL
+Qko+NDI2PT9AUtLN1s7Jz97uX0xHR0xa+d/Vdv+5tczqu7S/4s28vtx9xrnmNzpJOSstN0BA
+PVHCvtjayc3vX01IR0ZKYdrQy8bAwcTGx8TDw8PAvr/DycTDVjQzOy0mKzc/R070u7fHzL/S
+WE9HPD5PVmbNxMO/xMjAwc/IvL29ury9v8jG2TcrLjEnJS47TWn7yLG1zNnQZ09MPjlJ6uzd
+xb68vszXycnSx76+u7e4uLu+xEorJiopISY0VtPFxLivuNliT0JDRDk4Xb+/wr25ub/rUPzQ
+zsi9uLKvtL3Bv8k3Hx8pKCMqO924s7u5tLziTTw3Q1Y+Ona9uLe7x8fD21pj3ci6tLWyr7O5
+vcdfLx8dJCUkLD/huK2tsbjD7k8+MjM8QEfXu7i2tLzO3nVZbNjJvbWxsK+zvMrT6z0nHyAi
+Ji48R2TEsqyts8PpUUY/PDs6QWzIurW3vsvOy8XEx8vHvr7G1WpUVl1fW04/Ozo8PTw7Ojo/
+TVz+3NrPyMTGysfIztvlc1xWUE9g3szDvru6u7/N2dbZ3d/Z0s/lUUI7ODY1NDEyOD5GVPnZ
+0snCwMLDxcvQ1+V7feLZ1NHOx8PGztfWzs3MzM7Z4eRxTkE9Ozk3OTo7Oz5KWvHb1tLOy83U
+1tbjfXjj08vJysvKyszO0tTRzsvN19rfbV9iXlVLRUJHSUNAPz5ASVNXXGd0ePTd19DPzcnI
+ysvNztXa2tfX1dLT1tfS1uZpX11TUllebXJmYV9dWVZSS0dEQUNIU2Ft797TzszKzdfg4NzX
+z8/PztLUz9HfbmRcVldcYGN/5t7U0tjg8m5dVUxEQUFBREtVYXf77+Tf29fU1NXY2djW0c/W
+3ef5+vn/+3169+be397b5PL48n52X1RTV1ldYFZQT09SVFhaWmFvfPHm3drd3d/g3uTr4drX
+09LW2dre6urk5uLi8Hj37O7q6vN7bWRlXFJOSkhFSEtMTVVfaPfk3t3e4d/e29fT0dTY2dPV
+2tzi7PHt5+nu6uDf5Ojl5uXn8W1dVE5JRUVERkpNU1llbG/z4Nvb297e2M/S1NTV19nY1dna
+3Obr6uvu/WtpcPrs5+9zZ11aWFVQS0hFREdOWVpfafzi2NHP0dPRz9PW1tXZ3t7k5eLl6vDq
+4N7j5ubq5ur9bWhgWlZPTUtLS0lJSk9VW2Rt9+vh2tjW1djX087NztTa3eXo5d/f3d3l7vLu
+8vx5fnJqaXJoYV1ZVE5MSkZER0hKX3lu6ODa0sjIzc/P1eZXW93T3OfOysHGzMrCvr28vLzK
+OR4aHSUtPvrUuqWeoKrBOiotMS0rLkyyoqGtyl5ZdeJ7VG+/sK2vt769tbk1Fw8SHC/Gs7Kp
+nJqgtzMeHCo9SXfKvaujq8dIOz/n0VA/S9O5rKurqam3+FY0HBMVGyNNraeknp6qv0EkHCAw
+Tb6rq66vs89HPzg4S+9747yxrKenrLfOPjQ4KhkTGilarKSprKanuE8qHh86wrizr6+1vHI1
+MUTm2OhPQ2O4q6ioqKy4VTA2QCgWFB4yv6elrq6lrc4vHxsnv6moqq+72lQ3LTRNzMjbVk/C
+raamqa278jc3bUMgFxklPrWutrSoprVmKB0iTbGura+6vNIzKjtr18HKQj7cuq6npqyvukgs
+N+w0HBkfLXetrrmwqa7GPyQeLMStra+50tTnPDFPxsPG8zo487etp6esttA8LkTNNRwZIjBY
+tba9sqitzDwlHy/FsK6ts8TCxzwsO97RzuQ9Os2vqqSkrbvSOytBxDweGh8pQbu1uK+orcJB
+JB0p466npq7EzNRMNzo+QF76VVTEraWfpLLcQjEtUNEzHx0oMVK9v76zqa7OOCIfL8etqqmw
+t7jWOCwwOETg3/vSuaukoKm9XzkuNsvGMR4dJS1Yv766rqmwzjMfIDa7qKWrvMveRDg2Nz3u
+wtPd1cewo6CsvV00MVe/SygdHSY428q8r6ursv4qHyQ3w6mlqrTAfT41NDhC7cfKzcq7rKKj
+rcRGMjNtyDwlHR8mPM/Ku6+qrLRaJx8mPb2opay0wVM8PTgyPf7WysDGuqmhpa3GPjA46HYy
+IBwgLGq7tbKwrbC/QSUfJUC2pKGqtc1OOjMyLzvgvba7wb2upaeuzTsuOt1fNSEdIC1yvbOy
+s6ysuEgmHiE6uqWhp7biTkA5MSwzXrmts7m8sampsNw6LzzacTQgHB4q+7etrK2trbhCJh0e
+LsSknqGsyVBDNy0oLUe5qauvt7Wtq67LOy02WU0vIR0cKuexqaesr668PycdHSvGpZ2erNHo
+YzgtKCg4u6upqbC3sK642UMwMjg1KyQgITDfsaalqa631jwqISEsbqyfnaW0zUozKScpNset
+pqWprrG1wWs5LSsrKyorKSs90rOopqyzwFE3LCYmL+qwop6lsclFLiopKz7HsKahpKmuueBL
+OisjISElLTlK2bywqaitu182KyorL0TPsqeipK3FQC0pKjRQw66noqKlq7piOSsfHBweJjfx
+v7aurKusstU9LScoLjpXwa6ln6Gt0DgpKC082LWrpqGhpq7IPikcGBkdJTfUu6+oqaqst/48
+LygoLDJGzbSnn6CpuU8vKiouRMexp6Gho6m44jMbExQZID6/tqukpKSltU0yKSMmLC857rKl
+np6ovGA6MTIyN2O4qaKhpq2xvT0cEREVHkS8taujoqChsUovKSMlLCsufK6lnp6qvMlaOjg1
+MUm4q6emqrCytVsdEBQYHUO+2Lain6CgrEkwNCskKScoTq2jpaasu72/TjIxOVa6rK22tbCy
+tLwyFxEZIjLfxMm0pJ6jr848MDk3KiQpRL+tqKu1trO67T04PGG8srm/wb28vsT4OyUbHSkw
+OUjevLWsqLHNe2pMOTxURj9rxry2sbTH9vt2a3HtysLGxMTcTktbV01n3kApKTtKPDQ9YuHN
+vLjKUHa2sc1OWtPIwr3JTjs/7sreaNy6r7jKz1M6PkxMRUhoztROPUJGPDU2P1dv9N7QyMnJ
+w726u7+/xmQ/O0Fs5lrav7zAv8VnSlVXTkxKSU9aaf1fTktKSk9VVVrz0cva9NfGw8fWflpS
+TUdGR0/uxr7Bx8nM1dnedmZnZ+7c/FpUVVJLREhOVnHe297h59vMxsneXVRRUFJMRUdT2r++
+x9DY2+Pr8WZYY+nb1tXqWUxJS0ZCSFvXyMnZ9evXzM3Y8mhmXFFVVVFae9vOz9DY5vh8em5t
+/u3z6eDi5V9RSz8+Uupy/tbOz87N0+z/3df5W15gX2fj+GXq9Hjc1fNbZv3r7V5benlpb87Z
+SUF921Jd537g2uDg3e956WJv8nxg8tzg7OzxcXFu4NhpS9nBVUjKyU1fzlpIblhFYnlZ7dTv
+7d7x79bT8+vm7vTsZm1dStbDZFnKzFFs215XYlFN1dVXX+RhYudfVXZoUd/UYeTS9v3S2/Te
+8HR52uly3N1e3PVYduJadu1Xc+ViXOliXvVwaGJlXG1pU2Pp3V3n0fLg3N774/n32vnb1fD4
+4G9fcd/0ZXNsYWjd6OjtZv5oVlJeXmRsZXHodG7v7uvo5fHudWBi53Lq3uXc1vVy5Ontbvj5
++2Pv83F48PBfYWBgXVtxZF7k62vk1vff2/z83n5r8uJpaOj1ZV/tcPpyaXFme3f63dp8e+L8
+/WZSWW1hfPPp7uPv2+t6fu5qeN1rZndnY+t27enwfvf3a15jb2r42Nnj3tXbdHBrWmL4YFp0
+8/T64eNxdntmX2tjamBs+vP65eDg5On6aGv1+Wxx9Ozn5+Xn/X5tYV1eZ2Vmanvq7t/f5+zn
++nrq6/r8/W13+Pr77+92/nFlZF1eYmz24+Hf6HhsY1xcY2hw+ubm5ODp93d88etyYfvg4OT2
+3+D4a3JtZGp1/G5raHnt4uDi6XpfWF9dWGL95O583Nxz+PpqbmtmcnJv6+3v4tra6nnr7mtm
+9XJlbft8/OLz8uh1amxpZWlqZ/707+vr7v55a3J2cmxr7+/n3N/o8nZtd3/ud3BzaWNxdnX2
+fXz8+HNtfnD45e7z7ff0eGR88evq7O/7e/tvaHpuaW1z//7wf33w7ers6+x7bv/1c21sb291
+/3Vv++p8dX5rdO/ta3P2dHd9f3n3f33p7uzr+P9te/jy6+vn9f92+255e21senVoenh2fff0
+6uz4/mxreHl9/X/8emdzbmppbvv77e/y8e3t6uzq6Oz4fnlsevP5eHJ0ePH1+vZuZGxzbfHq
++Pbz+3l2d3FudXl7fPn6++/s8+/xfXd0cXh0fPn9/fN7a296env3/nf57Ofy/vP59+jk7vlv
+YWR3/e30ePjufHfw+Hr9+X5uY2FwdW1qbnn3/Hv9fHb47u7x7urq6enl9vv1e2xseHh4dv71
+8/j78PD9fP97/vV0bm5tanl5bmZeXWRrZ2p6++3u7u/x7Ovr6+Tr7Ofq6erl4uff3uHj3+Pd
+3N/odltOTU5JRENFRkxRWXbj29XOzc3S1Nzu4uDm3uPu5+Lf3dbMys7Oz9nZ3OTd411LQj88
+ODU3PENIVO7OyMbAvb3CzvNobl5NTlhdbHb01MrGw8G+vsHGx83e2tFoRT45NDEuLjM6Pkt1
+yby6t7Oyt73K6VhEPz89P0ZPa9nNwLm4trS0usHI0NjiVTw2MiwrLCsuNjxS0b+3sK+trbG4
+wNxOPTg4NDQ6Q1Lyzb21srCurrK5vsbUflQ+MS0sKSgoKzA3Qu/BurKtq6qssrbBakU+ODEu
+MjtBSHTEurWvrKuus7O5x95kRDYtKigmJSctMjxX0r2zrqupqq2vuc5WRz0yLi81NjlK3cS8
+s6yrrKytsrzEymY9MywoJiQkJyovPlHYvbStqqmpq663xeNUPDQyMC8yOUZe2L6zr6yqq62x
+t7zOUD8zKicmIyMmKi86TM68ta2qqamqrLK9yvFGOjYzLy81PUVxxbu0rqytrrCzus9eSTQs
+KycjJCgrLzhL4Ma1rayqqaqts7zK9Uw+ODIwMjc9R/zJv7evrq2trrO/yug9MC4pJCQmKCow
+P1LmvbGurKmqq66yus9uTzw1NDQ0OD9SdM+8trOura+zuL3tPzoxKSYmJiYqMTtC6ry2r6qo
+qqqrsLrA1E4+OjYxMjk8P1nVyL21sLCwsbe+4U5BNSspKSYnKzA3QurFu7Crq6yprLS5vdtT
+S0A4NDY4OUNd7M++t7Oxr7O4vtlbRjctKykoKSwwNj9e2MC3sK6tq62ytLrO9FxHPTs8OztG
+VmPWwr66trK1ubzPYk49MS4sKyosLzQ9UfHKvLaxrq2usrW5x9ftT0NAPzw+SE5W4MnFvLe1
+uLq9z2tQPjMvLiwsLTA0PEph1L+4tLCvr7O0t8PN2F9OSkU+P0lOVOfNy766u7u8v9JjWEQ0
+MS8rKy0wMztJWOG/urexr6+ysrW/xcpzTk1LQ0NNUVD60tPIvb2/vcDXZ1NANTEwLSwvMTQ7
+R1Pxxbu5tbCxsrG0vcfL6ldVTUJDTE5OaNfRzL6+v7y7xexfSzk0NC4sLjAwNT9KWNG/vbix
+sbOzs7zExNdfXlVHRUtOTmXf3c+/vb+8vcjjbU09NzQvLC4vMDc/S1jawb25sbG0sbS7vsnc
+f11RSEdLSkxcZejPyMHBvr7ByuNfTD43MC4tLjE1OUFPbM7Bu7e0srK0uLvC1O9oT0tJRkhL
+WmZ+1srEvLq7vb/G71hLOjQzLy0uLzI2P01X1L+9uLS0tLa3vcfL2G9fV0xJTFRcbN/PysS+
+vby+w8nrVkc6NTEuLS8wMjlDT2XPv724tLO0tbm+yNPpYFROS0lMTlZr6NLKwry8vLzDyNdR
+ST41My8uLy80OTxIXeHDvbq1tba2ubvBzNb9V1BOTkxQX2/u1svIwL7AwMDL0X1KQDczMS4u
+MDA1O0JV+su8uraxsrK2u77K1+hjU01ISUtRXHvbz8vGwsC/vsXN11ZKQTY1My4vMDM5Pk1q
+48a8ura1t7a4vMPQ4GtVTUtLTVNdcOzazsbDv7/Cw8jT2l1JQjg1MS4wMTM6PUn/2sO6ure0
+tbe8wcjZ6v1eVVJMS05VZ+fUysfEvr/Dw8nP3lFEPjc1MS4vMDU8QlHu0L+5uLOytbi9wMrd
+7l5NSkZER0tWc+bVysW/vLy/v8fO2lFBPDMwLy4vMDU8QE7pzLy3trKwsrW5wM3fa1ZJQUJC
+RkxQaN/Qxb+/u7q7ucHR2Ew7PTItLiwsLTE6QEzVxLyxr6+ur7K3v8vdVkhFOzo8PUVPZ93O
+wrq7ubS4u7vJ2Gw9OjgrKiwqKy82PUnsv7iwqqusrK62v8xsQz86MTI1NTtIXNfEvLOys7Cy
+uLu81FtVPDAyKyYqKSowN0ndybatrammqqyvus9WQDcuLS0rLjg/YMi9tK6sqqyvsbvF0G1J
+PDkvKCopJyktMztPz7mxrKemp6mss8Z1PzAtKicqLS8+X8y2rqunp6mqr7nD21lGODE0LyUp
+LicrOD9N172vrKympauvtspUOy4qJiYoKTJHcbyvrKakpqaqs73PXEY+OjMwNzAoLzYtMkNa
+4Mm5rKyuq6uzwNtNOi8pKSopLTxL3rmvqqampqiwuL/tTUI8Ojg5PkM2Ljk4LzU/WPPTvK2t
+sK2tuMxpPzUtJyktLDZl07qtq6emq6ytvtl6Rzs6O0NPSErnTzQ6Oy8xOEXuzMGvq6+ws7zX
+RjEtKycpLzhFzbSuqKanp6y4wtRLPjs6PkRM3cDN7NdKMS8sKy41P9C4sKqnqa+97j8vJyQl
+Ki4927mqpqelpq+80k9DPDQ2Q1Nnzbu3tLrLXjMnIyQmKjrVsaqmo6Oqvk80KSMgIio7/byt
+o6Clqq+/Xj41Mjk/RVrMu7ezr7G4xkwzKiAeIyoxT7ipoqOnrLpXLyYiIyYuSMKup6amqK/E
+XEA5Nzc6Us3Cvru3t7zBxM/xUTwvKiUlLDM/2LatrKyyu9tCMi0tMTxRzrWtra+7ztRYPD9I
+T33f7cm5vMnIwsPDz9/Q3Uk7LyclKSwyTcezq6qttMBfOS8uMDtS172vrbC7zltYRjo+Ve3W
+zs29trzHy8bGyczVz+RFNSwjHyctNFu8rqinr7rJSjEsLTFH1r+0rKy2ylxGSUJASunJwsTK
+wLq7xtHRys3u++XzWDcoIyInLTZTvKunqK66z0w1LS44T9G9s66uuN1FQUU/RFPvxLzFyr+6
+vMXT3dvqfGz6ztg+LighIywxRryrp6asudNFLyorMErLurKtrbLPPDU7Q0pi0762t8POwr3E
+5lhPb+xm3L262DwrIx8kKjL0r6ejprDCYTgrKCw59r61r6yvv1E5OERbXejJv72+zM6/vsz5
+VFTp5P7IuLzZOicgHyMqON6upaSottNFMSoqMErIs62srLPOPjI4Q1/g0MG5vMfN0MTC2VtO
+Tlhe9MW1tPAvJyEeIyo1y6umpam1zU40LC46abyxra2xw1k9MTdQcdLEw7+8ydnNx8LVTkFL
+XG/Kta+9Si0jHBsjLUy0p6KgqLvsOSwqLj3VtquprLjYTTQvN0vOvbu/vcPMfmzf0OFYS0h+
+z8O2r8JCKh0ZHCUv0ayhnJ6qxUgtKSktPsSuqKeuvd5CLiw2Tsi6v8K8xdzqZ+TL2k9HTt7D
+u7Kvwj0oHhocJjXMqqCen6rLPyslJy5GvauoqK/FTzkwMz1M0rq9wMTT2M3j3sroUk9U8MO4
+rq7KPCYeGhwmOcSonp6grd85KSUnMVS5qqapss0/Mi4tNEvDtbW5v8fL1H3cz9huTUznv7Wu
+rcgyIx8bGyg4wqSen6OwVDopJCo74bGoqau0djguLDI/3bu1t7/N7Gpp3c/Hz19QVv7Gtq+v
+xTUlIB0dKTzDpp6hprVONionKz3es6qrrrxVOTIuM0vZv7K2vsTdbtvW2MTPZG1s5sG2sa/H
+OCIcGhspPb+lnZ6iskQvJiUqPdWupqisu1E1Li0zSsq4srrGz+P049PIvsdnTE/qw7extdMy
+JB4aHStJuKSfoaa6Qy8nJi5Ey66prK/DTzsyMDhF27azucHR29Ha4snD0mpMS9O4tbW48jEj
+GhYeL2+soZ+eo8U8KSInM0zAq6iortQ8MCwtQ9y/srXBzN5f7M/IubvdT0lL47+5sbRnLiMc
+GSAuVK6joKCozkEuJys0TMKtq6yy1Ek6MTNN1r+2vcvP6l3a0Ma8zV9JQ0zVvrOvvV8wIx0b
+Hy/qraKgo6vFRC0oKzVWvq+trrXdQTMvOV7NvbvBxtH5edrMv73J51RGTfPOuq+3zVgsHxwb
+JD3ErKKiprBrNSspLj/4va+us77yPjY7TPrLw8PDy9zSx767w9xoTkVJXMy3tbi/fUo9KBkY
+Ijy/rq6sp67dOy0uQ//Ovbi3ucxNREhKb+Pdxst/eezXvbq+wNJYRTs9ZcC1sbrqWUs1MTs5
+JyU3XL287N29xNf+SGLJy9b4VGfbb+zDvb3C5FZYTlHvy7y4vcjXXEQ/RmnHw8/5Y0pAPjpJ
+1b23bCUlKS5LWd2upq63+DxDPjtKzLmvtcbIy99tXF7Y0WxUUnTLxczS23VdUE5ZU0dNZtzO
+3tTK4+rtPy4sLTlPYOG/tr9jRURQ1Nb11MbAwNLjxLq4u89wXk9LV+DGvsbhX09HRUpPTVRe
+WmJn6+L3WVBqZFdPTUlLSVLb2djZfGfubV5uevng1tPIvr2/zNx7ZG1taXfr3tTfcGVofe39
+Y1NNXHJdTU9q3d91cufZ51dTWFZbTUtc69nX5ejoZlJIT/vb0MjHytPxYm1uXV1v18bH1+Db
+2d5tXWhpa3n/5t3zXVZaYvzyZ1ZNSkpNTVf50cbJ1/prX1NGRlPozs3P2/NoY27p2tzZ0c/Y
++W9w9Ov569va5v1tdPl7bvfi3vZdU1ZcVk9RXv/u6uHgfV5aU1hgW1ht697V2tnZ3+fm5enw
+dHD87N/d2trpdGlvbWxu/vDo5e34dHZtYWh67+9zamZ27uvu6vD2bVdUVVNabXF95+Ho9H1s
+aHnx9+zj3tbW3u3t7fT+fn10fO/4dPDg4en0b2ZhW1ldZXvo6PH+/vjtc15bYm387+nm7vT7
+a2RpZF5aXnfp3NfV2d7l7G5iX2Nx7ujr6Obm6XtyaV5eZnPu5ufn5el+bXJ+/ndqZWNvcnzz
++Pbq8WtiX2Jocfbs5uDi8nFoaG1xc/rq5+ru6e7r5u53b2xmbvzu5uLo6Ofr+Hp0d3V6/355
+b2Rja2plZ2pqefHu+Pbs9/zy+ff09fn6ffHu7fVucP76+fP9c3Jxdfrq7O3s9vHx/25ueG9w
++/n2+H5rbv//+/j09vV5dnN4fXv77+rl6f5ybnBnZ3J8e/Xj3t3f5/TyfmhgZ21sbXRub3Ju
++/h+cW54+Ors6+no73L8fHBubm187+35fvn++Pjy8/NubHBz/ezn4d7m+XJvbWtqb3vs6O7t
+8nx8a2huZWJdXmp3+fH8/3n+8PJ8+ujq8evk6PT7fnN9/vzl5fL17fD26fVzcmZeXmhscv16
+efXx8fXr6/X5env77+3+evx7aF5jYWFkZ2/79Ovt+ubl9vZz+OTp5uPj5/Du9Xl0b3Z2eHBv
+fn11aWd1+v1++P3+9/t8/P96a2Nmbfbr9/X07e3v8/P9dmtpbn//dPPs8ezp7+9+d/L2fG98
+eP55dvh/dXxtbWx76/V7/HF48/z38Pn27fH7+fb8dHZraX32++fm8nVycWdocWpeZm177P50
+9vDq7P/08Pnn7eni6Oru+H34+ezo8nxnZ3f2dXJsZF5YXWlvePfn4+Tr8O7v7+z1fHVucf7w
+fvx9++91bnn9cW135uLo7np5/XVsefl5en12fPhubvbx8O757en7e3V+9WxrbXZ5ZmtqbO3k
+6ebq7vl++Ht7+35yZml3fvN0a/Xn6vTx9nn1/Wz8+2x97H7r4X1vcG1vaW7s7fl2d3psaWVo
+ZmRqcXv68PHt4uHk6nN88e36fOvo/WdgcPj68e7s6XtvdHBud/lzZXf8e+rrffnx9unq+3J4
+c3tqaXFqbmxpcO7t+/99fX93dvjwcnXv9fL86+Hn4/l2+X16bWn26fr79+rtamdqcv1tav16
+cG1oc/T8++39cerofO/i7Pl+c29ybvvreX5xcnpobnF8+PXv7+98fXZu7+v7ffbq6/Nvcez8
+f357dWZ5+fr2cfTg7WFg/XlvZGxrdPZvbu7u8ud08eL37t/t6Xtn7PVu+2Vn7GhaXmR87O7l
+9Pvm7/L39+t8Y3L5cvh7bu3m9XZldvlxcV5daG9s9vbj23Fu9u/z+fvo4+V7bOXi8u778etw
+Y2xoYWJhdv7/d23082h4a2jr7/v78ent6/14735k6et0fGlu7/hv+fHw92l47PJ1bvLqdHF3
+9f18/Hd7/PNpfN/4avt8evFhW3R2a3fz3OXsfOrc7HJseezuY2H7+Phxeebtb2b76mpmafjn
+cGFv+fp+afvxcGxu/eHfb2fo7GRs9Xvl4f/55uv3cvtt9edreO35YGvh19t7a+rofGxhd21b
+XF9db/x8+G1kde99cnf/6v1kb+Dk5uXu3tn3d+/08+/wcfrp7/nodWNucm/x/Vhi/G1ncm5p
+7epnfm1s5O5ud3b249748+L5fv1dZO59be9x/+X4dfBwb+TrbG11bXZiY+zo9npoXFxs//Tn
+5ebc9Gh+4djV7W716f333OLj6ldWYF1fXWTz6m5ha/tnYlxh+/7r3N99821gb2p98erc1dbX
+zMnX9fvs5Nrg5dnia1ZJTVdXVmvh7WJXVVNXV2R96eZrWvPtan3r6djX5tfL13ze2NTbX2Ln
+719aaOjm9/t3YmNtaXHm4ePW2ONzcW54e2lsXFpdW191b3hxeXNYYP51fGZj6P5lbvfvXE5b
+7d3c2Nff7Oz14NfX4G9s73t2+fXX3l5dX3NeVGhubWZfa+zn+V9hamJt8vzv5vZ77f9z+nl1
++HFs+XBn9uXt7fBufPb29Wl96+t1ft7n5O7u29/pd3Lw92VdfHFaWFto83dq/P7vdmB54OHr
+7ene5PXp/2tvbfbj5e3v395vbfFtdWthbmRmbv3r5+v5fmlpe/nv9f/zfvLu5uPr5u7t+O/q
+7+jpc2lucnv1dmN/+m9qX2ZfW1tlam588+jq7vHf3eDg49zS2eTd6vjvenz6ZF9uY1xgX2B5
+b1tmfn/88eXj6/Ty9G1eVltgYmhsend7dfjq8H3y3tfV2t3p8edxa+z0eX1v++br6+P5/XFj
+a3vr5u73fvtzW1VZVFRhZmzu5vl0cW5wa11e/+7v6ebc3unse+/e3d/d5PTu6+fo5eXq7/9j
+X2ptZV5daWpdXmh1fPbs7Ox8cPnk5vFrbnZ0/vHg3Nzf5OTi7Hp3bGJga/30fGVhaHby5+3p
+5fl0bXj0/3BxfHX85ufq6Orr5fX/7PNzb29sffhqaWlscG1sb3J3dWxpduzs7fz87ePl6+fq
+9H9zfPtobXl9/3JqbGVdWFhiXlxfanlubPvh3t/d3t3Z3/Lq6n5vdOz2bGZuevZybu3xdnr+
+bWhyam90c/vtfGNp/vBnYXZ8dndu/OXo/Pnr7u30/Xl18/1td3r44+//+f7s92pjZvz0dWFu
+c2xpb3r95OPw9ubq8Xxy7n199PHl59zd5uTyeG52fmtpfuv5fPT5b2pqZXJuW15nY2798PX8
+d2VgaXByfvvy7fT+dv368+fo5ep8cvhv8Ojp3OLy9+Dl9Ozh3uv1+Obtem9qb2hfYWxpZWts
+ZnJkaXppZWvx6vdsb3bs5nHv2d7ufnDs6nVtbvD9Z2Ry/XD//n389n7y7WJt3+h8bHHl2eTo
+5enf+2d24+d7evf7/n9odv5nWmRqW15ecftuXXfvdGpfY2/+avvs5eb28+zh3OLi5ur8e/H7
+/mNo+fn8/+rn5/N49+5tbf7w7vDk5fNxbv7t+29maGxlbG5z/31zb2lf+vducmdu+XZpbHJz
+dGtwfHDu7ujf6Ozk4ux+fPDvb2pqbnzy/vn9enf/7fl7/Op4bvHt5uR7b3t0aF5eZ25vdnv5
+7e9uZ+33a3Fta2xbVnP5Yn3xdXRlZO/tY2z8dvp6a3h3aWx9dvnm8/rr6vX18fLl6evj3t3s
+5ubw9Oz5+e13+Pz0+nhwcnduc19ebm9kaGJr7vj75+bu5vbw7HRoZF1hZV9eanhvdX3p4vp3
+7d7c7O/f3uLh3eLo4+Tj4u7+5ed6amRrbnNtbXJvcnB38vHu6ejm6fd4e3Nu/vt2dG5fYWNi
+YHjx/Xtyb3L07e3m5uv0e399bnl2aW5tfHzy8/nk4uPe5ern+P3v6erwcv7w7+z/bm/3/3l5
+e/H4ZGZvbnV1a2duff736u72b21zcHpyb3dxZGVufn50ev7y59/f5OXs/f/xd21sZm17fvX3
+eXr38e/27OLm4O34/v70/PDr+/j3cmtqbnBqZm1+/W9wbmlqYWh1b3t1bnF5b2ludXf77e7r
+6fXy6unv8fD8eG94/f5y+3lxbGp98/H9+H3/7uvt+PH4ev9ybnv+8PT3/HZ7cn779O/6ef15
+eHFlX19fY25vb2xxdfn3+u7v8XZpcXd6+fn+d/l5dPbo5unf39/e3+jt7/x2f3lqcnVzeXN3
+b2ZfYmzx4fdx/XRqbWhibfv9+PHr7vjz+e7yeXz8/fzp7XhycnZ2c25tcu/ye/Ly9frze3j3
++Ozr6uno6u/u9f3w7vd0bmxkZmxvb2ppbnV59vd4ev72fH74/3xwc3N69vL/b252/Xh6fP75
+7u97fHz48Ozp7vL9cH3u7OXj6vvz9vLt+H1ud3l3b2RqbWhqbGxvbWhwb331f3VxeXt99Pd6
+e/T5+PXz4uLt6+ft7+32cXR8/f11ePV4am1vbn705+L0d+7q7Orz+/Zyam988PX28vx3evpy
+cG5vd3n3eGt+9PD4+Hh+dWp0eGlja236+v3v9vz9bfzr9O/yfPrt9nl8dnjs6ers7e/z9e7w
+ffX7c21tbnF0bnn+cWt5cWxxcXv5/vvr8fH0/3307/f39Pr4cXN4c294/3h7cv36/fd8fH12
+bGlnbHN88O/u8Pl+/Pb7+fV1cPvr5Ofm6Ofo8npxZ2VraG31/XZ0fXb46O77/e/o6e/8fH73
+9H7z7n58e2xpa2tqa3NvdG5neO3r7Orx9PX09nR69Pbz9PHr6urv9+ztfvz4em10fXJubW9+
+fnz38/54dn368+bl6er4+/5ufX12b2hsb21vbWxvf+33/vfz9/P7/vl4bnt/dnr18/Ho6u3x
+9ntxcG91dHb7fWtobHr89PDv6/j8929pbXl5fPbx8evv/nr38fbz9vP19Hp193hsZ2lnYWVn
+a21ydXJ2d/nv9vf17vb+7ur9enxy++3y9vX0/PP2e3F5eG5qbXd4b2VoaWpz9Ozy59/o8ff6
+fX36++3r/nzw9Pv1b2ZhZ2lwa2x4ffb7fu/x/v5vZXX19Gxq7+/59Pr+8evu+vl4e3x2bW71
+7vP17O3s6ujr8e/u6/Vw/u59cm9raHn3e3V0cnV++vf7eXlvanF57uzy/fHw7PhvfO3q7u74
+/nX+9Xd1+/H6eXp2f37y/Pt8dfX8cWxpbfz59e7r5ODh4eLr7ut8bHB0cWdeWl5na2138/L0
+6Ov48O7z+PZ2enl4/fT1cHBucWtrbnJ7d3Fy/PTw6uzr6ubs+u7u7O/v9XZ8d3Bvanp2eG5k
+aGxsfe18aW5vdHBlaGRt8+78cHV99urm3tfV19ba4etvZGdlX1pXW15hX1haXWJlZG/z4Nzc
+7f3u+P/78+ja29/j5t7Y0tLa9mtwd/xtYV1cWE9JQTw/U3LVyc7My9J/Z1pXbfnl3Nzb2+tn
+XmpwcHts/vHt5vD38+zo7OXe4eXV3ODf+PP4aVRVTUM/Pk5z3dHe39jeb1xPTV7263hu+dPN
+z9fc2NTY7PTq1srIyMzY4fRZUlhOOy8rMEjRvsG/v7/KbEc9SGPd09LLx8bYUEVFSlj+2MzG
+yM7U3t/e3+fu7Ozb1tfMy9t9X0g4KyUuQue8u7y6vdBdRDxN/tzMysvL3FlPSk/jysfJzt3g
+6fzk08nGytb9XftcUtbP1vE5JyQqONC5u7W0usVdOjc+Vc7CxcC/0llBOT1gycDAxtHP2HVj
+7s/JzNz/e+DS19DFzthRLSMfKka/srW1urzaQjY0QfDBvb2+xuBOPThG38a/wsrN1Hxke9HF
+xMvZ6OrV4XPQ2dp/NiofHi51tq6vtba9YD40NU3LwL6/x8z3Rjo9Wcu9v8zW4vRzfN3HwMPN
+9l9m9NfPxL7MTy8kHR8verCrrrC1zUs7MTdN0r23vMbZSzw7QFfJurzA2V9dZ+vKvLm9yehn
+ZFlv1MrG1UQtIxwfNs+vq6+2udBLPTM6fcS7usTbdkg/QE/SvLW9zPFTUVjvy7y6v877XFVW
+d9jHw85RMSUcHi/ur6uvtbrMVkE5PXbEu7zM5l9GQD5K0L24vM/8ZFtj1MO6ucr+WktV99nH
+vcDIfDQkHB0t6LKtsri7yFQ/Nztqwru+y+RvSkI+P+C/ubvN8nByfs++t7XB2VtMUmZ94snJ
+ydg3JBwcLGm0ra+1u8hWPTU5WsS5usHT2etMPkFcxr3E0HJeXu/Vx7y7wM34WVV43szByc7X
+RiwhGx412q6rr7e6wGlIOTxsx8XG0tbI01hERWDKxc7f83393tfKwcLJ4VlOZdHEwL2/z9pC
+Kx8ZHjLLrKuyuLjE+kI1O/nFwsfZzsPPUDw+YsjEyNDUzM/Oy8fCxNRbSk3nysG6vsnYTjMk
+HRoiPMisrLCyutFPPTdK3ci/vr67v/JKPT1g1srFy83V2tvYz8rJ31BCSHbNvrm5v9JaNyYc
+GSI8vKeprrG5zVk7MT5ty768wb/HdkU2NkvRwr7J1Nbe3NnPzc/bYFV+zLy5vsHJ2vs1IhoY
+JUqxp6msrrTSRTAsOWrKvry+u8daOjA5cb+4vcfL3Gtga9vKzOVgVufCure3vMTNXjQjGhYd
+NMWnpqmrsslTOjA7bcq7ur/C0kg1LzVuvre2vcna7Obg2tDN3FlRZ+LOwLu4uMB8NyEXFSBA
+r6Omqq+8/T0tLD/Ks66zu8TuPi8uOt27ucDR2u7l2M/BvcTZVExq2se9u7vA+kQtHhcYJ1ur
+oqeqscFmPC0vR8q3sra6vn46MDFEybq5wtbd7f/pz8K+x/lOTGLY09G/vMTWPioeFx4xyqak
+p6u13j8wKzRXx7ezubq/eT8wNFq/t7vTYFxcWWfbx7/G2eTazb+9vre2w1gwHxUVHzyuo6ep
+rbvqQi8uPuC8s7e8vdBTNi44csC/1GFm7NzTyL67vcz8X+/Jv7u3u8HLQykfFxcmQrWmqauu
+vXpGNjE/37+1ucTF4kg3NU3KuLnL+u/o2c3KwL/G7FJNUPPNxb68v89CKx0XHS7GpqSorLXP
+SzYtMUfSvLrBw8d7RzI32Lqxtsv03dzo1dTKwtpMQUJYzL28ubzLVy8iGxsoRremp6isvndA
+MS85TdfEwcbHz15CO0PUt7W82mby5N3SysK+yO9XWt7Jx769y9tFLB8WGCZDrqWoqq662E04
+NkfYvrW4vb3YSjsyLkbIwbzPUXLV3NDO18zK4W/jzLuztbe71UYsHhUYJ0iso6eqsMZYOy0s
+PHbCtbi+wuBNPjc87r+5vczd3s/GxcTFzNldSk/fwLi5v8TWPiwhGRklPLmlp6quvnhGNDJA
+dcW2tbvDeUU9Oz5Xyr68xORgad3QxsLGzeRhfc3JwLq7vs4+Kh4WGilTqaGmqbLLVjksL0De
+ua+zvMpeQjw0PdC+uL1pVVxu4dDLxb/LXEtL9L+1s7e/XzQlGhcfMr2io6aruN5GMCsxUMCv
+rra920Q5MjBBxbOwudpdX/vZ1MrDvspcRErhx726v9JELB4XGSdSrKGkqK+/ajouLjvUta2u
+u8lkPzcxOVDLuLnG4V9j3s7Kx8TB1WVYVt69tLS20zkoHRccLfiooKaqttpJNCssO+u4rrO9
+zlVAOTY/+sG1usnsWPTOycnIxcXtUUhLz7qwrr5OLyUeGx4v0aefpK29a0I4LzE+1rausLzP
+XUU8OTtK5r65wM1pXfDYzMnKy+Brb3HVwri0uu4vIhsXHzPQp6Omp7DMSjMsMEXLta2vucNR
+OTU0QuLLv73L3G9efdvMycrP3/zv1cm9t7W5dC8mHhkcKUysn6OotNJNPC8uOFy9ra24x1o9
+Ojg/5L20tcDkU1Zp2crNztTfc2Bd6Lu0trxgNCogGx0pVa2ipKq43FI8MTE937atrrvSUzw3
+Nz95w7u2vdb9UGHj08/T2dXlbPXevrW1u903KCAbHi1hraOmqrbpRjUuMEHQtq6uudNKNTQ4
+RXzHubq/02NVZefPzNPb39fZ3dXDuLe6xkMrIhscKUm1pKass9BNOS4uO+25rq661lQ7NTY3
+S8m4srnO9/p94c/KxcbN42v96c68uLzLSC8nHhshNsWopKmwxlc9MzA6Yb2vrbPJXD02Nz1U
+0cLCvcDY6lte18zLz+Po4+Hb1b22ur9gOS0kHx4nRbampKy86UI6NTVA37qwr7ztSDs5PUn3
+zMa6usfPcGXZzMnL4uHV3N/n3cfDxNRNOS0oIiAoSbqmpa+8+UVAPTtJ7b2xsr12Qj4/SW5u
+6b+7vsZgU3fjx8fU3eDd3u3d0MjDy/NQOC0sJyYnO8qtqK++9FNNW1Ba78a4t8H2SEFHT2np
+xry+yPVSS1Fi0sjEwcnaYl/21cnJ0+ZNMzAtKy8qOMmzqa3IaU5LaltY/9G7usbjR0FOVWH8
+fM69w8rhTFdz5MnExsXQ5+Lu3tPMxvRKODEwLi0oL+a3qqzA4XpfZVBETeW+vMbbU09ZVlRW
+bdnGw8bL4/394cvJzNDY7d7d3tfb293+SjouKi0qLUrRta60vcbdfllNTljTyMfE1vL3WVFP
+T3HMys3Q/2Rja9rMx8vN0tvmW2Tnzr7G4kcvKykkKT3lsKuusbvQbkc7PUfvzMLDzdnvWEtI
+TVVNXGvJvcrLzsjBx+l349TP1NXSxb7JUzgtKiwqJTD6tqquu77H21o+OkR2zMrJzszJ71BK
+PkTq0MvHzMzH0OLV0M/R1tXS0tvZ2dbPUjwxKCglKDzZt62usrXC7kg7PERh38vAwcHKa1JR
+TU1RedbKyMzT09TY6Gxv6NLR29rQzMLB10s3LyghJTBet6+vra60yU02MjlHWd3IvLrAzOpe
+ST08Q17OxMC/wcHK3GVWXHndz8rFv8THxHc9MCcfHis+zLSvq6mtvmg7Njc5P1jOvLe6wcra
+aUs9PlPdzs3Qyb6/zmZVXWhfatvYyMDJxsl4TDEoIiAtO9W5saqoq7XORzs0NzhD9MK6ubzE
+1WxJOjs+Tn7Vx765vMXealZTUl7fzr+7vLnKWU40KiAdKknJuLSup6m1+DkzNDc2O2y+s7K7
+wcXWTzgzOEju2sa4sbTA425TSkZHeMS8vLu+wMhMLSYeHSw6aLqvqKOot9BGNy8tLTvsvrOw
+tLW85kc3MDdETty9s66zwdhpSkVCR27HvL26uL/LRywjHR4rO+67rqeiqLLKSTUtLC08fsK0
+sbCwuM9ENC8uOUr+urCusLnL/Eg/PERkzb67s7a70kAuJB0eKjjpva+ooqaxyUc2LisrN0/L
+ta+urbPJWzkvMTU9asm3rq60vc9WRT09VNrSvre6vtVLOCkeHSk0T8q6qaCjrLjvSDUrJyw6
+Wsi7sKqttcdIOzczMzthw7Szs7S7y19DRk1MWdjIwL3Lb0MwJiQpLTpNzbGopqesuMtONSwr
+LTVDZL2uq621vdFOOzIyPUv8zb+6ubzH1+Hh+frt2djbeE48LigsMTM6TNG3rKysrrK84z82
+MTEzNT7lvrmzs7S5zF1IRENHSVXoz8jCyMjEzNbS1+9jV0o+NSsqMTY5P07cubCwra2vtMRp
+SDw3NDM4RVniyL68uLvGzdHc4+5oamdZXFlSUk1Pae3u3tz39fpdVFdbWFRTU1txf3f76d7b
+43NcW19lferg1cvMz9nc4/Zybfzp5t/d3uZpXFlVV2j25dnW1tLV2d/rZVNPS0ZHTVRZYG9p
+ZWZhX2FiYWny3c/JyszT5Onz7ufm4N7b2tbU2+l0XVtfaHrt397f5/D1b1xQTEZFSkxNVmvy
+7Pl4bWdnXlxi8djNyMnJy87W3ux4c/nx7ufh2934YF5cWVlj8erp2dTW2upfVU1FQ0RHS09Y
+c+be3OPj6fr+efPb0dTVz9DU3OZ/enx3fvPn393e3efq6GVVV1tcXWJhanb/eWtfV1NPTVJZ
+X2jv3t/e4OPh3uHf29vd29jY3OT9Z19hX2Vy++XZ1Nja3ef6dHJfWVdTUlZcXl9eX2RmbGpp
+bnz34Nra1tfd7nd0aGlrbezh4N/h4OHq+mxhY2Ntdf15c/H39PH0eWtsbnj+/O3u6ufre2Nh
+Xl5hbXl5d3V1ePf8d3d2+fbr3t3n8vZ3bnN2bWptfO7n3tzf6e3t7vZxZ2RdYmNgbPn1/vf+
+9Xp38uvk73Z6e3h1cHj+8uzs93hvdnhsdP1ya29udPjs63z+8vHo4+9sa2hkZ3H/+fl8+PPz
+6ut7e/Ln4+Df5fdscHr9/fP0d2pramZocG5ubGt+/fP8cG9//mhjZWdu/fj17Ofr+PDl5u/1
+fvPq7urq5+nv9Xj+e3Zwa2pseXn47vd3ZW1xcXV1bWRpb313/3x4dW599ufm5unr7+7zcnv/
+c3759/fs5+707er2efx2bW949fpwaGhobHZ+eGxka21ufu7t9/z77d/c4OPn7+3m4ujt+W9j
+Y2plX2Bqbm/76+vze3Zvb3R6b3NzcHvx83t6dG5x/PH39/r79ujg5uXm6flwcnd9fHn+8fD2
+fHNwa2Rna3R1dvz9/fnu8fl3dnBz+Pt99+73/O/w7X1ramppaW14+2xwb235729vc/Pd3+Dn
+8trY9+nqeWxnbWf983FzZl5XW/fv9O7s397h4+Lc4nR2eHH89vt3bnV5eWxeZGpkbXhyaG15
+Xl5vdG16/fz06+jp6fHq3+Xv7OHg4uPh6nz/7O718/LxcmtnbG94/PN4bG1oaG59enNuY2h5
+bmxnZl9hevL7ee76fHZ+5+vm3+Pk4+Lp+G5oamdxbWxzcXz57e307+r8bX13aWRlYVpfbm5z
+8Ofq7ejf3Nzf5evt7Onq6vx4dHBzaGdqaGJpdXRw+H59+nBsePp6bGhkX2tubG5ucfn9/u7p
+6Ojp5OTu5+Ld29vf8nl6/fnz829ma2pu+/1zeG9maf7u5+Po7Or4//f3fnR3e3FvcGlsc25y
+fvv4/HhsaW93/n9yfP5udv7z9n75+37s39/p9ez1cPvs9f7/eGt2evvu8/Pr7fjv6u18b3p/
+eW1ramxy+O70/fLy9P93/PX/9vZ5fX5vaW1vbm1udW5mampx+nt7/np1/fTu8Ozq7/f6fX7+
+9vLn5OPh4OXp6/Z47+fwdmloc/vv/WxlX2BfXF9qZ2Fobm947u79bWx2+PDy++7t7eXl6O7t
+6uvo5+jveGBdXV5obWtsaXbz9fLs6efk4+Tk7fr5+3hvdXZlXmRoam9++Pb/b3r+cXJ0a238
+5ujx/vl5/O7r7fJ5df91fvf9eW1qam9xdm9yb2dt/ejo6+55anbv9fry8u7y8PR4/fP89/Hv
+6+n0dXNybnBze29oZGdjZG5tbGxwcWtsffb47ev4/PX3dnv6fHBvb2p1eHj66+/s4eXi4ez5
+9O7v9/vy7/bz93n6+Hh1d29xemtmbnV1c29xeXNuY2ZscP74+3z57Pn38PDyffbu9P59+XN2
+fn17cnV4+fPt7evh6O3s7u7n7nlwaGxsbP75a2RqbXL7/3z9/Pv97e7zdWxtbf399/50bnj5
+8+vp6Ovw8P5vdnFycGhlb3NyfOrn7e39e314d3h1dXx6cHj7/ff39/jv+HR4+vP5+3v+d250
+en1+7vV8/fR2X19laGlkeHFrb3jg5OXi3d3r+33+b2tgXmJ1fvH269vh5ur9fXl+dWhpdfv1
+83P97/1xaWxsaXh2aWz7+vnx+fH3fnhsevXr4d3g39vkd11ZYmptfPzq2dXZ4nNpZltUUVRb
+bX/v4Nnc/F5UVF1rcv7g1c/S2+j+fG9pY11f8NnX19fc6vJwWlNZYl5ke+/f0s/X4v5tYV5e
+XWBy6+Xo72taVFBPUFJd8eDb1dPW3uj8bnhvd+/j2NLS1dne4X9dXFxdaW/53NPY7WJVT1Zk
+dn196t/e6FRFQ0z+z83Y3uHd33dXTE5d6ODtbmzj2d/d1tfPytHpbV1ifPN99evk3NbZfkg6
+PUFP99/k3NLTzddqT05Td97t/Xzm2NbzXl1i5dXb5/R839LeZlxe8czFxtJdSkE6PD9N7cjB
+wMf2bFhMTVBXftLMytDvXlVY+djOztv0cm/0293sb1tWX3Ju9trX29PiQzAwNlDDu7rD12Xv
+/lxNS17cx8fQaFBY3crGzW9WVmTf0drxb9zJvru+zUUqICQt5K+qrb/2TVNQST4/X8Oys8hK
+OTpK3szR2OXWztLhaWvaxsTLaERJ4rqvte8qHh8q2a6ssMTuY/RPPzI1Tr+sq7hZNTI+e8LG
+1N/Xx8LH4Ftc2svGxMjM21A6Jh8jL72npKzNPjpHXOhaVn3Gt7jKSDY3TsW6vutDP0/RvrzA
+yMXBwsvN20MxIhsmO7KfprJMMTNK+9TrUt/Ft7jZQzY6/L+4vmdAP1bKtrK0ub/R5eLLz0Yr
+GRckSqCbo8AtJy9iw75xWNi/sL1cOTFDwbGyzDguMla2qqestb/hTFDyTjkeFR0yqpabqjQe
+IDbGr7tabOq4sto8LDHZraiyTSsrPbmmpKi2zllS8MzOPh8QEyS/lJSd9B8dKfe3tmFezLas
+wjwrKlmtpqp4LCgyzaiipKzCaUVdwdI9GwwTJ6uNkZxKGhkr4K+yRPbPsKvuLSIrup+fry4f
+IUKpnJymu01DTr68OBsLDyytjJCfPRoZLruvqz9UyrmudSYjL7yanq4uHB88q5uZoK1MMTV2
+wPAtDwwdWpSOnM8eGSW8r6fLOmlow2EsLFysmJ7IJxkeOqublpymzDEvNz4rIBUWLrCXlqU+
+IB4uvq6rt83SSzQoIjutmpaoNx0aJ/SpnZueqNc9SD02KBoSGkKnlZqvNyMmN//LuLmstU8r
+HB9Un5OWtCkaGy/JqJyanafPSDkwLB4PECevkJGkOx4eLtHWyMa0qbJCHhsnrpWToT0eHSpS
+sqWdm6O6XTUtJBsUDyaslo+gRyclLVJPWLmtqa87IiMss5ybpVkoJTJWtqyooqqvuT0gHRYN
+GU6hkJiySy4qOzk3zbWqptgsKCVIqJ+eqUcoKC+9pqSgq7zCSCAcGQ8WRKmXl6vKTS4sKyrc
+p6Wr6SstNTy8qaOgszgrK0qrop6esd82HBoWDhdXp5iar7i+Oy4kIE+pp6vIND86NM6vpqK1
+Qjk88rKqoJ2nzi4dGhQNEzaklZemr7ZJLCEeNLq2u77Hw9g5P7+uq7VvT+LTvauhnaJmJyId
+EAwUK62anJ2bpdYrHB40UE3auLCuxF/Atb/RV1vGv8m5ppydsDsqIxUMDxo5raOdlZek0ywf
+JCknLny3r6+6tquwyN5VR0pT26+fnJyoTSYZDgwTGi29p5aOk5urRigfGxkeLD+6paGdnqiu
+vE06NDAzT66enaS6LRgUExIZJD2wnZqWlZynvzwsJyEdIC9fuayvqqCosb88LSooN+LCsq61
+wOlJPTw+QEZOVF3q1tTybWtaTD01OEBO8ci3rauwxFw/OjpC57ywrK+5yGhLQT5AQ0FAPTo9
+SGDU0e1YSERFQT5H/sCzr7K6wdlUSE7hxsC9v8viT0NEUdrHyuhLP0U/M0XWXFBHPEtOSExH
+wLHHvLG8U0dAXLO8yKmqt7fEXjsuKS8vISIpLDlbwKWdoqWjrshqPC0rJyMqMDZP0L+upp+b
+nKClrcVEKx8aGRoZHCUvTLupnJeZnJ+v4UkrHh4eHicyWLKrqZ+dnp6fo6zLRjcpHBgcHRgc
+MVFdv6WcnaCeo7xcMyglHiAsLzvBsKqhnp2dn6GluGJKLBsZHRoWGSg4Nuaon6Gdmp+ut9k3
+KCImJSIvXd68qaOfnpyeo6q0zTolHh0ZExceHidUu6+jm5mcnZ6u1E8vJSMgJCctUci4p6Cf
+nZ+ipbPN8S4jIBoZFxgeIylfua2enJqYnaKp3EQ1JyYlJy44XL24q6Sno6OssL1LOysfHhsa
+HB0kMES/q6aenJ2dpK66ZTwxKywtMkt8zrOsq6mqrLtvcTkmIiEeHR8mLDX4vLSooaKfoKat
+udZEMi4uLTdR98Oxr6qqq6/KZkAsJCEfHiAlKy9Dz76yqaWlpKKor7PHUj00LiwyO0nZuLGs
+qaqtuNN0OComIB8eISYrNlPYu66qp6SipKmsss5dQjItLTI4Q9y/t6ypra65y2E1LSYhHx8i
+Ji09a8q2raelpaWlrK645lI9My8vNT1G1by1rKqusL/wTjIrJSAfHyEnLT32xLGqpaOio6Wr
+s7vnSzsxLS40O0rpwLivqq2vss5fRywnIh8dHyMpMk7FuKmhoJ+eoqevvNY9NC4pKiwxPV7N
+t66qqKmrsMRPPishHx4cHiQqOHG6rKSenZ2eo6y02UM0KCcmJSszROW9r6qnoqOpq7ZbOC4h
+HBscGx4oNUy/qaGem5udoai47josJSAgIyYtRNS8rKOhoJ6fp6+9UCwlHxkYGhsdKDv4sqSc
+m5mZnaKqwz0tJRwcHR4lMk3BrKSenJycnqWqu0M1JhoaGBUZHSMveLumnZuXmJyfqr5LKhwe
+GxYeKSxJr6mim5qbnJ+jr8brOCckHBkcFxwlJTHFr66dmpyanaW44TkhGxwcGyQ6Xbuqn52e
+nZ2hq667UkQ6LCUfHx8dGyU4LUatpaefm5+krcRPMyIcIiMfL9i7raShoKKpq6y+zb/4R2E/
+LykiIyAcHiszOcapo6CfnqKuzkk4Jx4gLC0y0a+qp6emp7DBuLv628TfWN38NSIkKRwZIzI2
+TrimnZ6loKG5PDkzIx8nMj5pwa+mqrOvr77QwsjDuL/Qyr9JIx0jHxgaJjvSt6qblpujpq15
+LyUgIiMjMsu9uKuoqqy74s7H2tvCtq+5x7vPLRwbHRoZHi7HqKKdl5adqb9DLSQcGiAnMGu8
+rqWkqauvxdrT1czBt7G0vcfoMhwYHBsbIC/XpZycmpedqcM0IyAeGhwoNdCtqaahpKuyx09Y
+7OfOv7evr7zIXy0cGRsbHiU1y6Sbm5uco6zZLB8dHBwjK0K0pJ+fo6mttutBPkNuzMG9trW9
+vu0uHRscHSAoNtCmnZubn6iy3DMiHBwfKjRls6Wfn6SsuM9UPDk8TOK/sa2wuLvjMiAbGhwg
+KDjVraCcm56ouF4yJh8eHyk53a+moqSorrnZRDg5QF7KuK6rq620wEsmGhgXGR8qPMCnnZqZ
+nqi4UC4mIh8hKTJqsKekpKevuslpRz09UNO8s7C0t7jATyodGRgaICs9yaugm5mcoKzPOSok
+ISAgJzber6aipqyxt7zG3WZd6szBvLq7wNVILB4bGBkeJzfPraOdm5udpbVXMCcfHh4jLUu+
+q6SipKirsLrH2fRWSlBs0cC7xG00JB4bGhwjLUO/rKCbmZufqLtOMCQeGxwgLVS4qaKgn6Ko
+rbK82E46MjQ8WMi8u8NaLyIeHR4jKjRTwK6jnZucn6nBPiceHBwiLk6/raaioKOmq7PDcT8z
+Ly82Q2HRwb/Gx8DDTCwhGxoeJzzOtK2opaWjpq28TjApJiozT9e/uLm1sK2ttL/Vb1xaVEpD
+Pjo7P0xaU09Zb+nUy8bCzl5EOC8vMTU/Tlvq0s2/uLW2ubzD0uvq3NHLy8nGy9DiVklGRURH
+SUpPa9zNyc3e2dza32NSRD5ATFROVmVaaHt3ZlpffON5dvdsbeLS0tzm2urrzcTDzdnl39XQ
+y8TF221qTUpZX01GTlFZZV5fYXXzdm9damtga+NqZGpXVk1MYfxfYuz04uTYz9HR08HI2tr6
+7mFYWtvx7tbc1dRaXObebE9k7uFVTV/cfklgcUxSZlVNTl/t1szNx87Y4fHoX1NGTXdsdFd5
+1s/O1srI4fdoX1pbUU1iXm101tV80M306mlbYOhXTvjh52bY3n5udXzsWVfs43ln2+be8+Zm
++OpOUe/xVlNe+P7v8Hn3/u3q9u75Zt/c6tzb/dzb3F/7zs7x+t5cVV1579/b71pcSUFKVlFQ
+ZHr35e3e1OPf6u1WYetv+/fW1dfVz9vZ0vhbb2Vldmzo197g1tjZYVNWWE5HTUpITvfS2NPY
+0eNbc+BlT11bWWrs3tzOzdPR5Xj9c2vubm/vYOrX4GpvXFx5dW9aXl5UXnx94s/T+Ord/P39
+7+9rYlxfWmrudu3199jX6OPf5vrocm31bV9fX1NPX/Dp7ODe9fXp5/leZ21sZWpkafTz7eHf
+4ePi5ujp6/z83uB5duvxbWRsX19eZGZsa2N55/Lx4uDnb/59Z2Vp9WhkfXNz7uLf4Nflfvt8
+9W/05fZrZ2Nvamr3/Pvs2ef3fvZ8aWxtZV1odX/59/798efpdHh+eGtn+9/n7ODi7O9vdf1f
+Z3F6dmlfbHp66evy+HFy/HRoYWdhY/fo8O/p7vF74d17/Ofk6+78eW1vcmtsaXn8d/f26elt
+anJ0dGt6cHJp+u118PPzfe/x++ttYm3w+3n8+vr1+f5yeXtqcPTycXn8bffm+ejkfm94+/tq
+afD4e93d933y/n97ZP3f9GFr7u9tZ+rrfnVtaW5oa2r6e3t3fvz7dHX0afnq+23y7lxr43//
+3d/e5dzm4O5s8uR+ZO5qbvJe+25fXfJmXWtlXVzr3+x5cnPmZ+LSaWdvd+za4PPt6upueeHj
+Z3t4a+j4dexoWuTga11YV2/feGVw6F1X291mVO7P7FnSwF5L6c75W1hs7F5Z7dz7X2/Y/uxo
+4tlcWXbecHTu4njxcXd1blhRWHPbdv/j+encdvXR3GPj32pjV2xocvv95vtfY2leV2rb6O76
+9f5w4u7p/Xjw5/BZWl/o6mV55eNnW/rW7Pre0t/l+W/5Ylhg7mJQXmX44XH71dpnWuPiZ1hp
+6PteYNnYb3N5fvZt8+x8d3Jo9t3o+Nzd3/lcaffvc/f7aHX5cXNu8ejv5/NgaHxlX+92ZmBb
++WVx5e7edfzj6+Hs3djn3eLo4G34YFxtaXNYbV5VYVpnXF9zWV92bXr03tLWz9XW09bHy8jK
+2dDb29HOyNNdOiwpJSYtPM+5raqutctfRDw+QE/uy76+u73GyM/Ny8S8u7u9ye08KyIeHyQv
+VbysqKitvXM+NjAxOEZ7x7q1tbm/yc3LwLi1sbK2vu48KR8cHSIuV7ippaivwFQ7NjU4OkBP
+6Me+uLe6vcPGvrm0sbG0ushTMyYeHR8nN9yzqqmtvPBDOjg5OTk+TG/KvLSxtrvBw723sa+v
+srfCWzIlHhweJznataytts5ZQj0+REc9P1Hsxbmxsba7wcC+ubSys7S5v/I4Jx4bHCM15LWv
+r7vZXk9YYFVJOzhBbcSzrKywusTNy8G6s7OytLrNPScdGhsiM/i6srfGf1pt1s3qSjgyO+y7
+rquttMHP08u/urWxs7S0vF0sHhkaHixXxLu/zdvRwbu//DwwLjrPtK2tsbvHzsvJyL+6tbKx
+r7DENh8ZGR0oPe/U3dPBuLO0vlY0LjNEzbOxtri5u7/Hz9/XvrWvrrCxt2sqHRkbHyk5V+DK
+ua+vtcVgPDM6T+HDury+uLa5xdrn+sy3rqyusbO8SiYcGhsfKTtrz7+zrrG7y11BPEdv/OLM
+y8G0r7XKb/nYv7Gtr7W2tLtLKBwZGx8oOVzQva+rrbrOfEw/QUlDQvK9sKyut9RQbMy+t7O0
+urasrNMnGRcbHyc3WtK7q6SputlkSz08Pzw6Z7WrrLbGZUhlw7y+vrmysayqvysZFxwiJi5L
+zLmso6W4fmJjRzk6OzhPuKqtwuX6af/JvsPKva+trq21QB8YGh8lKTzRuq+opa3HbWJLOTY6
+PEHbsq660NHN08/EwMnCtq2ur7PSLBwaHiQmLUzHua6oqLPR5+RNOTU4NTjqta+4vr7G0MzE
+w8e+s7G3t7TcKx4cHyQnM1/NwbSpqLG/yuBFNTEyMTjqsqyyv8zOz8m9v87Nu7Kxr7DZLB8e
+IyYnLTtP0rOmpa25xudFNTEvLTFZuK60vcHHxLu3wt3Wv7i2trjOPCsoJygnKS47Z7utq660
+u8ppQTgxLzJA0r28v8K/ura1vdLd1snHxb++z0s3Li0sLjA2Pk3uyL27vMDO7ltQT1dhaO/x
+bGjxzsPCx87c08jBvbu7v85kS0VEQTs4NjY3OT9KUlVd9dLIwb/Ez9zpbl9j79bW3NvUysa/
+u73AwsDCzd7oXkY6NTEuLjI3Oj1EW93GvLu+ydXe4e/y5tza4t7XzsnIx8fGwbu3t8DQ3mdN
+QTgwLSssLjE5RF7XycHAv8PJz9PS1Nnm6trW0czT1M7Hvru3trm+zellUkQ4LisoKSswOkNQ
+6crBvbm5vsbMz9Tk/Wr66uDV0M/MxL68u7m5ub7a/1Q+Ny4sKSgrLzlFVd/Ivrm5ur/M3O7p
+5On2ZW7l3dXW1MzGv7y5tra2uM5LQTsvKissLC42QlnnzL+8vL/Fydbtbmjz+Hft6+7239zd
+zL+6t7a0tLS3y0QyKyoqKSsuN0/QxMbHw768w+NTSkxXU1bz3Nzc0c3OysK8t7OytbW3vLvF
+OSMhJigkIy1OvbW4t7GvueRFPURKQT5Py8C+w9PIvtJRacG3ubq6s661xsLMMCIkIiAkKDXI
+srm6ra/F5FdIRkE3Pt3HyMS4tMdhTlftZ2jGsa2ys66uuL3OLiAgHR4lKji9rbe2rbjcXkVC
+Tkc6UsDGw7y6uM9BP1VPQ/24rq+ysa+wvM/KSyUgIx8mLDPbrrPGtbTVWEZHZl5BRca908fA
+ycpMMD51Ul2+r6uqsrevtszQyTwmLSQbKDUxZbS5vLLH5chSPWfrREnf2cvFzdrP5zo/b23e
+v7GrqrS6tLrL3eJlLycnHyc1M0O9sL3Cwc/USjpRfElFXNW+xNbCyGpCO1Td6NO1q6ywvLy7
+yOx6wMIvKjUkIzEuNb6267+8bvNTOk7gRD/dx8C/vbjcT1E9P2XWxLGtrqu4zb29296/vzoo
+KB8hKyk5vrbKw8Pf108/bt1fZNa+ucXMvtFBNTdFddbArKeqsL3ExdnhzM7PPykpHx0uNy37
+rb7Jv8zCzDxEv85GYcW+xnXV0DouSWhRxrGqqbC4uslc5768xN5JPi4bGy40Lke/rq7XTLy7
+Qjxa39RPR8K27FjIeDk7T9q7trOsrbbDz9DMyMS9v+I8LCQaHCw0Pti2ra/XWsXjQUBZzMTy
+aMK50Gja50s1Nce0w7aoqrfdVsjI5MW3u10vJx8XHjI6bLWsrLVpV89YQVnezMfracm+03Ld
++0g7QXe+sbGvrrfRc+3Xx767vVYwLyATGjpMT7+poqxCOcfLOzn2tLLvRsq5+Uxsbmc6M9+5
+urGsr7bNVNfL1bqwvnI6LSMUFjRiR8Wno6pZLlzHPzfTr67HQ1+9cDlO3XVJO1K2t7utrLXK
+VE7+dcyur9dHOC0fFBk7weC/pZ+tQS1G1kE31quqykBQ1k87Q9fA7T9Mu7LAuK2uwEs9WNxs
+u6u20TssIhQULcnBs6SfqE8nMVVERMisqLhFOFVZPULcu8VPS8a4vLu2s75XPEFOZb2qrL5P
+LiseERlisLuuo6KsNCA41UZFu6inwDc6d0cxPM68zlD/vLbCyrmywUg+Ze15v62suzwrKxsT
+JOzOsaSjpLYuLUY5MlG7ra7OTGdLLy473b7LyrKwwMzOv7jTSuHaTFnFsa2/e9ktEhQnP0XK
+ppmcyzVFPSckNryrtsi8vGw0LkPbWlu8sLnDv7Wz0FHZ2EE517S4zN2/vjsaESC7NSepmJ+2
+TErELxssvsTU1bmovi890U47S8GssMu4rb9Z/MzRQzjXulo52L1o6PknHTk2IzHLs629zayx
+PzpvYz4uOcLEZNOyrbrhzbvMWW7T09jPxsxfSVPpUDpUzE078cXLyk0zTlApL0lKb+pXy7xq
+Sv/QxM3XvsJ+cXtkaX7LvMfZxL3F2mnx4VQ/SfLiX1jx1N5PQ0lRR0BR5dXV0tDsTT8/RERS
+3szH1eJuTUdEX9LExMq+v9vle1re1GdVxsLK3cu/4U1TUUdDP1zR3tm8vdxQSD02Mjx0Z+i/
+xe5LNzY/P1nFu7W40NP1TOvL0sjGx8dt9cHE1MvG3URDTj5CYMrHf1f0VjlDTFN2fG3C3T9R
+Ois+2FZiuq6/UtvIaklbvLfNyrzLbWzdzt3e12VERERa5GrFyU5PPzhIXX/M5WFdWTw5RfZi
+c8DCcOzHd0/fxtDQw73G3dLH3UdQWkVET+rOc+rA1k9x2XFVT+FgQE5LREdDSebbcuvZyNVD
+679hYsPI49jLv8v74stiRmTOV1DP0ubw/vxUUONaTVFNWEg6U8vzW83ZPDx/Qz/IxsO45fDE
+XEzPwsPCycPSTVH1WH7W0spiSF5uU3zh08tOTVxANz1NQj3ev9tHa8JLOd3BaPG7xtjb08rU
+1L/J2MnhV1h71OhkzcpMP1tYP0/GxvhUe2A/Rkw+Rz063M3x7M7I+D9I4dPuybe3ysvD1Wfz
+z83Xd/LL2Gbi/URc7UpBf85UUFvnTT5zfDk/WGZ2QDtaXkncwcK7xs/P8GHlzL7Axb7D9G/d
+3+HZ3kZA2ts/arXaNkrSWTIv/Mk5S7vOOjBBSzdEtK/KybtvPT7ZwdC3rLrh0sLMYl/gXT49
+TUlatbLJxH08NS4350xJyVYyO09FRtC3xOjL1VNV2L+9wLm7ytrSztLfWeXQQT9oS03Ry8nh
+PDU7LjNM8n5NWt1CRszJxra+xtNW+8vHt7S4v9vyW0LRuVJFzOM7MUe7SSlG1DQrPHY+OeDO
+4sK6u8HRzdhY3ru6vbS0vszX7PlYQlTe1P5CQlQ5Jyo5PTk/bMrNT++6v9W7s7zf3r/A1May
+tdHx1dtdRVu7utM6JR4lKSIwta/v17nKPTvStcDFrKu+dcOxu8u4udZb6PZCabe9LBYlORoZ
++aarzcKpxSQsvrzovqWl3E61sM3QuLbZQWjB0M+x2hkZLB8ZLqqeq8KtuyojNOm/xq6fr1bM
+tsZk6Li3ZkHGr7/FuSoXHR4cJ9qhm6q1uTcdJDjwuK2kpb7oxNJa7b23xuPFr6+0uR4UJh8W
+Ka+dm62yrC4VHUJq76+bnctGvb5ANtitvErJpKetvh4YJBkVMa2dmaesqTEVFiVkzsadmKxZ
+YexPMDa0rM7Nq6GhvxwTICEYJqqVlqavqkEVEBxEw76jl5/ZQV9NLCraq7DKr52dsB8NFywa
+Ga+Pj566rboZDRU3u8qtlpm8R1pOKx82q6vArJybpiULECsdFtKPjJi8sKsfDBAqt7rFmpKv
+OUxNMiIosKC6tp6aoDgODiUfFC6XjJOrtKkuDg4d3LHIo5KeUDxBNSYh35+quKOanV4UDhsh
+GCKijY+frqlvFQ0YNL+6rZiXtzw+MiUiM62ir6mcnLIeDxUeGh3PlY2Yp6a1IxASIUvDsZ+X
+nsk5MSgjK/Cqp6ignag3GBMXGx4sqpKSnKCpTx0RFClfwqqcmJ/MLysoJi/YqaKjoqGwLhgT
+FxsgNaeUk5mfr0ofExQkTtCvnJaezS4rLCcp5qSfo6OfrDsaEBUcHyqylpGWn6vCKBUTGytK
+wqCVmbFLNzAoIjaso6qknKDMJRUTGRocQZ+VlJidp3MgGBYXIEW4oJqdqLhKLywtP76uqJ+f
+r9ctFxQZHCVFsZ2WmqOouDwiGBkqRj3NpaGovGbJyjg6uKemrLjB1y4ZFyIuMTjgq6GnsLW6
+2jwuLjlo8kZVytNlX+nFvsCxpaaxx1Q3LisnKTlt6lFTyrvSQjxiw8XQwLm+0D8uOVA/Qdm6
+rq+5sa/PSVhcXe19T0Y/PDQtLTI5RWnKsqmrtb9hPz4zLkTJysW4ubvB28ayt72ys81XNCIe
+ISIhKku3qqipqKe1SjQvKSw7R2y0rLfBvL3Cw7+vp6y8v888KR0YHikrLlytpquxt7e6Vy8z
+PzY4W+nfv7vDuq+ztrOzsbPB9WlENTAlHyw8Mis6xbi/vbOtq7lUQ0cvJCo7T8eup6Kkr73B
+2Ug9SW3Sx8e93SshKCYdHS7MtK+ooJ6lwTouLykfK8mvrqmhorBsQkJEQkXQrqmxxb3WJhoe
+Hx0eK+6xqaWjpKWv+zo5Py4lNb61vrqtrMBl68vLeHvBuLvEzMToKBseIx4bIk6zq6Wempui
+tl05MyoeIES5wsyrn6rbWNXVRTlZvrq9vrm+Nx0bICAcHzy0qqaempqgrss/LCYjHyQ/v8PO
+rqGp122/xEw/2Lm9zcS7w0YmHSEpJR8qxaamq6Odn6/kSjsuKiclLErfXO6yp63Dvq+3fEzY
+yl1DWca+y1o6NDk0Jh8pWcHOxamdn66/w+wyJCAiJjFYyLSnoqm5w8ZvNzd0xdTPr6OltNpM
+LR8dHh0eKlfCvKyem6GutLd3LSMlKSkrO+i+sq2srq+yv+Pnysp77Lqus8PcYTchGx4hHx8t
+x6yrqJ6anq69yFAtIB8jKTBF1Lmsp6etsrG67kFCX/JSUsmysMLs9UcnGx0lJyIr06moq6Kc
+navC0GYzJSMoLCw0XMC2r6yrrLS+zv9WRz9CUenEuLO2vc1QLyIfIiQjJ0K1qqqnn5+mt8bV
+SC4oKSoqLDlS4MOzrKurqq++3v13Sjs+UPTRwrm2vuRBLSMfICAjK024qqSem5yirsZKLiEe
+HiAlLk3EsaegoKWrrrjkQjo4NzpJbtnEubnLYEk6LCMjKS8yOX61q6qqqKisuNlNPDIsKSku
+O1bYva2mpKirrbjqPTMuLS0xPFfGsq6ztLLHOSgnKicgJDnPvbmsop+kq7G/azwwKiUlKzlL
+ZsSup6esr7C55jwzMzMyNkTZuq+sq62zw0wtIyEjIiAnP8W4tKmenqWvucJULygmJyktN1XF
+s6upqqyvtclNOTY3ODlDbMm5sq+usLvYPislJCQiIipEzb2zp5+fpKuvvF0yKSYkJCk0Sdq4
+q6eoqamtv1g+OjYvM0Fgzbuwr7O2vOk3KSYoJSAiLlvKwbKln6GprK67WDMrKScmKjJA+MCx
+q6ioqa6802RGOTY8SFP6ybm2vcjN8jsrKSsqJSUyYdLOuaihpq2tr71QNjAtKSktNkBsu66r
+rKqprr/a61lAODpASWjVzcq/vdc+MTMyKSImM0ZKZbqppqmqqay1zk46LysqKy84S824r6up
+qay0v9ZUQjw5NzxO7tXS1dLWSzMvMzEqKTh12tm7qqarr66uvGtDOzMtLC0zP3XHvLKrqay0
+usDnRz4+PT9HYu7w0MbiRT08Ni0qLThASu68r62trKyvucxeQjgxLS01Pk7lv7Kurq+ytr3O
+/lZKQ0RRaGFe39hNODU5Ni4rMUFMTfm8r6+wrqyvvNdbSDkvLjM4PlDYvLOvrq6vtMDWeFJB
+Oz9OXl5gfNjzPDI6OywpNktBRcu1tLSurK+4wtdlPzMyMzM5R3PIu7SvrayuuMLSYUQ6NThA
+Pz1NzMTySUXw2DcqOUw7Nz/qv8PMu66vvcq+wV49Ok5iOznXucnfva6zysm+yk89SFc9OEtu
+U2/Iw8viSEBSPCwuPzwvN2/M2c67r666wbm601Vv2GJETu57YPLHvMjZvb1fX81bSVxEOkvw
+QD7ZzlXawltD7lE1Oz82OkQ+S8/Rz7mwt7u1tb3U1M1PTk5LSO9cV8zMccq76ujR3Ul9S0VK
+UkRNbPTp8Orf/VNPVkg/RUJLT2Ff1svDx8S+x8jJ3OV7SnNrZl9izNLS3Lve0ddfb19TP9xD
+Xj/YTt5O8exd6UjaRtpJWnXubVa/9szdztXV527lTvlLalB152Pa39DT2tVn2tlWYON4Wmnm
+d2neUdBd51Zs4EbeRs9J6Fnd31vJbsVd0+/zamBs6lb0Ud9Mz1DZV9xw5PVWyUfJRb5DvUy+
+T8pO5XBX4D7CPcU4u0LCWtPZ7sdJv0zFQs5PX1Rd2UnTUc9W3Fjn9VbjV/JZ5GPp6vrf2vPl
+/Pps5W18W3tfeGL43O7V8MhmymjSWOZmWOFO90zZWv5a6Pz8YO9tfG1t43DkX+b98X5l/l5z
++Gxd4W9zZO162vbf0NrV69Jz31TqXW1O8VbgXfpi5uls5nPr8WR8etxT0F3SZv/tZtlS3FPW
+U19pW2Rj8Gjid9r00/vX6+P1fHtdZl5kZuPp6efw2Ovo4ujna/tgc11mam7xauhy3lTrXWpf
+XWBrbWD77uzn19fQ5d3641xtX2N4Zvl58mHyZdl+6Ofo92j3XHZrbnXtb9710e7abdlf615g
+W15fVuBV3m/VXthn7WzsdGb3XOVW5mfcZ9zu2e7j6OjfdOxvcGb2YX7qZeNm5VjoVORRZXFf
+72Dcbdl35N/03Ojd/e5mXXtT3Vntc9xi6PbaeOBq32TqaHLvdW7u+/l293j9XHpl62HyZeh7
+duxu317uaud2Zf5q6W55dnp88v777e/83/bf9//u5/75/+dvbPT1aHtma2Vuant8e2d0+9ze
+5uf15HRp4HNuXPlsfWx+aOld/1/9efrr+Hf6Zu5833z37nTf8dd75Hh3dmvxYOhi4V/mbPf5
+c3Rs+FziXepg7XRt3fDke+X+33PmeO9cfmZudfJg21/kW9dU11DTTNJI00zSUttjz3jZ5uTd
+/d5h1mDbTtVM3VLcVuRbbett6mDcWe1nd13edHF24/Pp3OPf8OlwZvdo6lpzZut5/9jy7fdv
+f2n4WWdmX2Jpc+3jY+je4X7r9e3fbelv4Wp3YvhvX21u22T4ZNLp3vvd53Z5buZwaFv9cHVs
+6+3i9fhp3u9oVFtcW1RUWnZtX37d1Nfa1c7Z5+7ibWpaX3FuXmT76ebd2tPV3tvm6vxlUVJK
+SUdHTVp2e9fUy8/R1tHi6GliXFtRV1led+nb19TQ19zY2up9eGdfV1ddaWFhZnRu+P3pfmtj
+aWt+bXLzf3Zu+Wxwb/D27PLd4Orn29fP0djp5/FkUVFUVE5NWPrs2dXP1d36d3puUk5YWVZa
+fuLZ3tjY2uzo+n1eXV5jY27x+OXd1dfQ2NrscmNdXWdqaHhvc35qbWpqY15iXFpcV1d+3+je
+0tja3ejf0+b1+PXv/Gz79Gtr7+jta2Rsd2RebvXvfm/96/5oYnX9al1offH28/jr83D029t+
+cPHm7nx1+HV9e+z1+WxqdfDy9/Xp83N38PV2b3lxbW5qZW/3e3fx9/zt4+DofXj483Zva2Fh
+YHDu6/bu5+nu8Pn8+XP//m9se/n6/uvg7e7t7vTy9nd3eG1mY2Bobm5senB28O/2+3p7+Pnz
+6+jh/Gf/7fj87/V7dHvw7n1md+/vfPj2/WRfanVvbfPnfG/9fXxvenx8dX7s/vrv8PPt6u/r
+8ff3fe7l6fD5/G9mYm9mYWFpdu3t+vh+enR3al9eZWdv6tva3+bm8Xp5+PZ2bu7o5vHr5PB4
+a21ra2FjbG5t79/i9XR6cGFs7Wxh7OV6e3j99Wdh7ON58OXv+2946/BsaXZ7bmt89XVrd/N+
+9unk7f5rb/H3/vT4d33x+Xz19XZubG95eHdtcXlybnNvb2ps7t/h5t7ob217e2prePHv/PPh
+4v7y+mtgX2t8cGX03XZd5dh5Yv7o5X504+VoXnFtVU9ldG733dHZ7urg62RhaGZbXOfa+X7V
+3Wnn3Px0c2huYlzu2+pye+X3aH7k/15oe2xaVXDqZF7s43r93934a/jjfmN36O/15t7fdG7u
+4+/z4N/w7uZ4TkptW0FQ2Of+08zP52rm21hMaGhNT/nfff3Y0ed629l5buLd9eXR1ezr09Zz
+5+JGQFA/OlJ8XuDNy8XN9eVvSElVVldw283N08zN6mJva1Nb5evuz8zU2d/qb1dWXFpcbOr9
+++H6e1NGX1c8TNtgXs3IzNLe1d9VUHVeTGrb5NrR1NLaddrhUF7gbGLe2eTb59zOcGLR7VZT
+SEA/Pz5MbP7Yx8PJztPkbVhOVF1gbNzQ1M7Q3upvXF9mY+bT3NvP1uXi8G5pYfzc5uLaVkdB
+O0BBP1vQ3tG/xs7V6GtXTlNcW2bl19vY0Nni7G37+W7b0d/Z2uvkflzi0V/myN1lVkw8Nj1A
+QlLUy8nCw8nTd11VSktXZl/o1NbS3dnbXW7hbG3W0dfP0czaZdLMY9/I9FtNPzg0NzxFU9nD
+v8LGy9txW1FJSVr259fJyNbs9XFaT/HQ4tvCwtLMxt5t6urxYlxbSD02NDs8PF3Gysa5u8nU
+8lVJQUZPVnbPysvIz9fcXF/b7eHFy8/Dxc7O3OjSZE9wRzgyLzY3Nk/Kz8S2ucTI12lOQERL
+SlHYztnIx9rX7Vby23HRwMXEw8bEzeXVz2VIRkIyKjI5Mz/PwcO6trvMeV5QPz5MTE7vy8rO
+zdbd/Vz84dXJw76+xMTD0ObPzmpQTEc2LCw0ODNNvsDGtrTF3XZURTxEV01ZzMfbzsfX6nTo
+3+7OwMLAv8jHy/HZ1WZ4X0A+OisqNzc1Z7q9vbK0wt9kWkM7Q09IVczL2dLO12Fb2+L9xb3D
+vrzBxM7j2e3oakFESC4oNTkvPsW8v7evts30d0k4O0ZFSOfGztPIxOxS7d5d7b/AyL64vc7M
+ze9qbURESDMsLjo3Nme5v8Kwssjd7FBBPEBIR1POx9zSvs1LcM9gU8y8w8a5tr/Lzs/6VkxD
+QDotKzk4Mk+8wMO1tb/Q6WVKPUBMSEjsy9HUy8bZW+bQe+zDwcO9vr7D2NzPX0dKQTgtLTMz
+NUzMw7qxsrnF22ZKP0NFQ0lk4trY2s7WWGLa+/zHw8G9u7zAzdzW6WBSR0c5LS04NzhfxsC9
+t7W+3O5mQjxAQ0JN7s/W3c/L31ni0OnUv73AvLu8xt3Mz1RLSTs1LSgwOjJGvLy/sa+6yOBh
+Sjs9RUJH+M/U1czK6V565Pjjyr/Av7q6xNDM2WZTRUdCLyowNzM6zrm/u6+zyNfrTj07QUZE
+T9vQ4NbI0mJc3dxq2Ly9wbm3vs7a1uxVT0Y/Oy8rLzs4Q8i4u7izucpzWUw/PEFIR1nX1eva
+x9lW38zu4MLAxL67ucLa0dFXTFFBPDIrLjY2PtO+u7a0uL7XaE8/PEBDRljf1ejhxdNMdc5y
+7cO/v726uMDYztFRTVJCOTgvKjI9PUrAtbi2s7nSXVBNQT1HT01lzc5+3slfS/7s8NnIu7m+
+u7XG6NBvTVZKPjw1LS42O0nnv7e4ubnA311NREJARU9Tec/c7tHPWlTf3d3Jvbu8vLm9493I
+blFPOz88Kio0OTlevrS1ubS52VhXST8/RE9PWs/Mbe3Te0xizs7Txra4v7u6zXvX6VZPQD0+
+LiovOz5Gy7O0vbi3y1hOTEY+QFhiVuTL4vXN401f49rNxbq1vL62wHLe111TQjg/NigpOEI9
+X7euuLy4vftGSU9DO0zta2rOzfxg5vVKX83Exb23t7u8vtZ++V9bTTw/PCwoLkVAQ8Cttr+5
+us1MP05WPkH77FrlzeFbWvFRT+DJwr+6uLi+xszc4OtcYVQ7OzwrKzo8PtG7ubO8wMF7QEpO
+RElOZej12s5pW+JrT3rLwsHCurXAysHVZX5nf2pEPDsvJys8RE/Esq+1vsLIWT5FTUpKWdze
+79bqXepYRGXS0MO5uLm9v7/YVePdVWr9Rzs4LCguOUjXu7CutcXM20xAQ0pMS1Xi29bSblpd
+WklP1cG+vri0u8XGz2df8e3ba0E9OSwkLktN7LqtrrrZ09ZANkBVU01c1MrV7PftZElJ2sK/
+vrm0ucnNwdtSZ+rvakw/Oy8mJTNLVM+yq7C/2tPvQDxMa15Z4sXE0XRaX1lFSdnAv723tr7U
+1s9lTWvPzk9DTjgoJC4/RmS8q626ycrWSDlBXFxRYs/Hzdf+VVJKQVLGvLy7uLnH7NrRaVz6
+18zkTD02LCQpP13Wua+uttlbfks9Q1N45e7QwsngXkxKSkhbyrq0trq9yPxr+lln0s3N6kpB
+NykkKj/x1bysqrjbW2JROzlO9mhc3cTH31VFSlFCWb6zsrW5vMhiatj+Zd7Ov8dUPTcrIiM0
+9tTBr6qw2UZPWTw2RuLO2+DEvMpjSEFET1PSsq+0u8TO/E9a/33VyMjR4Us0LSUiLlPbvq6r
+r8NSTVA9OkNi0s/TwLzOcUM5RUlI2revsbrCxm1IW/t+0MLBvsVONi4nIShA7L6xra25XkFG
+Pzo8S9fL28u+xdxPOTtJTei4rayywszZT0pg7NjHwr7BXDUvKSAlO2fGtq6rtO9HST85O0nX
+xs3FvMTaUjg7SUj7vbKutMbK1U5NeufQx8O7wO48MiohIjFsyLatqrHZRUJBOjlF38jNzb7A
+6009PEFNcb6vrrG7yO5SSlrm3ci/ur5pOTMuIx8u4tDCsqmqxT5BWD02Ptu7xNnCus1NODdN
+SkjLsayyyNDNWT9M2MrKy7y3yUMyMCYfKUzKuLCsq7tNQEZAPDxWw8PSyMPQZj02PkNM47uu
+rbnK0nxLTejPxry7v8xaNi0nISo8Wr+vray4cElEOjk+T8/N0MO/z2xJPT8/UMq5sa+1vspp
+Tljr39rCvMHNajktKSAkOFLRta2rseVLSz85PEzRxtnIvcntUkBBQ0jevbKvtLzMfFJQaOHP
+w7/BxN8+NCsiJC8++76wq67GXk5EOzpE48vPyb7C1FtERUpETcW1s7W6v89cUHzs6M3Fvslf
+RTouJSUuP2LItaytvO9fTz05PmbP19vKx9JlREpRQ07Qu7W2vL7I5ffl7d/OxMfQ1kUwLSUm
+MjxYvbKursLe80U8Oz1WfnDNwsjOdU1TSERg1by2t7W3wtLg4+1w3MzO1VM6NSwkJjRFZcq2
+rK7A0tlfQTo/XHFf5c7GyllWXU5MWtW8uLu5u77E3uvU093j6+lINTIrKi4zPuPEuLC2u8Tx
+UUU+R0pP++vfzONr5lZVcvnMwr+6uby8wtXR19vVdl9KOTcuKS0yOVLZvrGzubnG6FdBQUhG
+TVZV3dlj39tk63XvxMPDvLu7vczPzeLg62pPRDwxLi8vNz9M0L27t7i8v9hVTkhFSEJEV2Nj
+9N/Uy8vJwL6/wMLK321US0xPWHfi3dna5G1PRUFCREhPZfb36+bt9X1tc/L7ePvt8WteXF5w
+++fTzcfDw8TJ0NnueuTa2Nfj62pPSUVAP0FCTFJRXfft73lt9XFma3nt5PDy5nhpe/Pg183E
+wL/ByczN1d7n4dfed3NaSkM8Ozw9P0dNXH324N3k4e94fnZ/7Pj/9fTx6eje1c7KxsPCxMrO
+1tvd4N3g+l5STEdFPz0/Pj9DSlt46drZ19TV3uXj3uZxamNfZmZv7d7TzMfBv8HHys7Z3+rt
++2phVExIRT8+QURJSk9cdOng2NXZ3N3h6fJyZl1bWl9ufOnd1s7Iwr/Dx8jO1tnd3+lvZVxR
+S0VAQUA+Q0lOWW979uTd19bZ2uju9mFYVlVXX2zr29PMxsTAv8DCxMjN1uPsbVpQTEU/Pj5A
+QUNJUFtz5tzc2tjY2NvudGpeWFJXXl9s6drPysbBwsTFyMnL0tvmd2xcTktIRURDREZITVVa
+XX7u8+Db2dTW3OT0fmlVUlddaXTk19XPycTFxsfKzMzR2+T+X1dQSUZHR0VER0tNUl1sev3s
+5tjY2Nfd5PN3XVZVXWZq897Wz8/KxsXGyMrKztnj+2tdVE9MSkdGRERJTk9VY3Z7fOnb3Nvd
+6fH193hnaHnu6vh469za09HOz8/Nzc3O1+Xm6nloW1JPTUpISktMTE5WXG/25Nre5+z3fX1t
+Z2Vree/o4tjT09ne29fX29zb2tLX4O7+/3ZrZmJdWE9LTE9OTlRaX2h78PXs5+/s6Oj3fX56
+6+zz7ebm6Ozm3d3b29jS1tXV29/n6XdpZ2RcVk9NT1JVWVlaW1xaW11jYGr58+/o4ebi3dzb
+1tba3uPd3drV1tjd4Ofk6ebl/Pn7aWhkX1pST09QUlRSUlddXmJseH78+PHt7d/b2uDp3tzf
+4eTo39/b2d7f4N/l5Orn5evue31+dm5gWFNQT09NTlFVV1pOR0/03+TPwsPLysjQ3t34ZFhS
+T1lhb3bby87PxsXP2dfxVktFQEFDQ0VLVGHg08/PzczP1tzo+2ZbXWFXVV977OPSzMrIysrM
+093k+FtTTEdFRUZFR0tXa+Xa1dLSzcrM1eD//mteWVZVWGBqbu7bz8vOysfJzdPd91tPTUZD
+RERFR0xVbebZz8nGyMnM1+P4bF5WVVZTT09UW3Tg0czKyMTEyMzY/15SSUZGQ0NESE5SbeHZ
+z83KyMjMztbg92leVFBVWFpibfzh2dHMysvQ1+JsWlFMSEdHRUVITFht8tzUzcvJyMrLztPc
+73RkWllZWmNw9eHW0M/Q0dnpfV9TTktJS0lISktNVmd85dfSzs3Nzc7P1eF3dnxtbXF5c+7c
+2NLOzdLa4flkVEtJRUNGSUtNVFphb+7e1M7O09ba5e59+vr47OTd2dTQzc3LyszU6W9aTUdF
+QkNERktQVl998+zf39rb2dXZ2t3k7Hdoav/v6uTd2dbNyMnLztj0XE1HQ0FBREhJTVdibHfh
+2tbS0NHV297m6+7m73Rqdejc1tPU08/Pzs7T3vtjUUY/PT0+RUtLTlhpe+/h29fPysnKzM7U
+3OZ+al9fYmz34djQysjJys3S3nlaTEI9Ojk7PUFLVmjq2NPPysjIysrJztzkfmJaVVRZW2H+
+5NfQzMjGxsjN1uduWks/Ozk3Nzs/SVjv1czIxcHBw8TKztbi/mFaU09PUlhh59fPzcjCwsPH
+y9X6XUxBPDk2NTY5Pkpc6s/Fv76+vr/Eyc/X7WldVlFPUVRaX/fZzcfEwMDDx87daVFEPTk1
+MzQ2OT9NbdrJv7y5uby+wsbK1OtdUExJR0hKUWLiz8fCwL++v8bM2mRIPDYxLi4wNDxKdtHF
+vbm1tbe5vMPM2HtXSkM/P0JIVHLYy8S+vby6ur3H2Fo/Ni8sKysuMztIYdPDurOvrq6vtrzK
+c01BPDs8PT9IWuDLv7m0s7W3usPQdkY3LSglJScsNUR4yLu2sq6trK2xuspeQDk4OTtAR1Fq
+2cW7s6+vsLS5v89kQTIqJCAhJCkyQ3vHuLCsqKioq7LEZ0E3MzM1Oj9GUPfJurCrq62vtbvG
+1mJALyUfHR4hKjdWy7y1rqmlo6SquN8/Mi8vNDg8P0VV1bquqqmqrrW8wMjSZjkpHxwbHiUv
+PmzJvLCnoZ+fprTxPDIwMzU2NTc9TdC2rKmqrLC1trW4x2M8LCEeHh8iJy44VcOwp6Cfoqix
+yF1FOzUwLy82RHDNvbWysa+vsbGyt8HqTTstJR8eHiAnL0HZt6ynoqCkqrLGYUA6NDExNT5T
+387Eu7i2s7Cxtbe4vttRPS4mIR8fISgyRtO3rKmmpaest8pZPjk2ODs/SFV03Mm8tbKytbq8
+uba5x3g/LSQhISIlLDVDcMCvqqenqbDD9E1APkFGRkZMXfjTwry8vbu8vLq1s7nF5kcyKSQi
+IyUqMT1Uz7murKqrsL3SbldNSU1QT1Bm5tPJyMfJyMnHwb+8vcLM7k08Mi4tLi4wNj1IXNjD
+vLq6vcbNz9HX4PtmW1haXGZ76+TVy8O9uLW2u77J5ltLPjc0Mi8wMzg7QElWaurUzMvJxsPE
+xMbL09fY3d7a1dPNxsO/v8HEyc7X61ZDPDk0Ly8yNTg8Q0pTdtXLysnGw8XHx8jN09HR0tTP
+y8rGxMHDxsfM1eR3WUhAPDczMzU3ODtASFJv4tbPy8fJzMvKzdHQzMzMycXEw8DAwcXJ0OLv
+clZIQDs1MDA1ODo+SE9Xd9bNzMnGyc/QzMzSzcjKycO/wMC+v8HHzNhxVUo/Ozo3Mi8xNTg+
+S1pw4M/LysrFxszR19fW1c3LzMnEwb+9u7u9wszfXUk9ODQwLi8xNTo/TmXm1snExsjIytLX
+0dDU1tDR18/IxcC+vLy+wsjTbU0/ODMuLC0vNDk/TF/qy8C/wcHDydDTztHTzcvLy8fFwb69
+vb/HztXuU0E6My4rKy0vNT1LW/fPwLy8vL7FzM/O0tXOzM3LxcPBvry6vMXM2GRKPjk0Liws
+LS80OkRY7s7Dv768vL/ExMbJy8zQ1c/Jx8bAvLu+xcnUa0s9NzAsKyssLTI6Q1Tjxr++u7m7
+v8LCxMrKyMzNy8rKwrq4ur/Bx+BRQjoyLSoqKisuNT1IZtnLx7+7vb6/w8fHxsjDvbu4trOw
+r7G7x2s6LSglIB4jKTBE5MCzqqivvMfbSDUyNT9PUOe5q6mpop+foKjHTVgjEBMcGRQfT7qp
+pqGZmaxP7XQjGiIvOj/XrJ+mr6aeprOto6xNOUkjFBkcGh8pP7SnrqabpM5XVC8hISpCVmWv
+oqeuqaSstLCoqds69ToWFiQdHClDuayyq5uk0dthMiEfKjg+Ta2iq6ukpKuuq6eryURsORcV
+IRsZJEO4s7Kimaa/vN4yISAoMDBIramtp6Klqquop6y3Xz47GhIcGxgdOsO3raKao6+z3jkp
+JCkvLUqws7Klpqmnp6SmrKy1PC8jEhYZFhkqVNCtpZufqq3KTzglKTMwP8O1taqnpaWmoKOr
+rrXkMh8UFxsQFi9CO7SdnqSoqbFOKkFDJi7OzE6/rK2vp56ipKKtuW5IORIPKBwKH6srNp+a
+r7ujpT41uFYsRcdXO9qwxL+ioKefnqy6ukooGxgeFRAqMib7p7O5qau00rq1XN++ZFHiWlnI
+ta+rpaClraarXCcYKxkIHi0XJqrN/J6ht7WitlqyujxGz0o3V7rFu6akpqejqLrHLhoyGAwl
+HxYuwUS5pK2rqqi4vK/JQ89fN0hQ78q4r6akqaSor8JIICUiDxwmFiTPMU6lubaoq7q6rbhQ
+wL05TOJf6s69raysqKistsxlQB8gLBgcLiMi+E0zt7XivKy5vbG0vrzD7MzSZNi3v7upr7Sw
+vdhVPjMuKiopKDEvLDxMNjzfWmLEwb2ysrSyub29xcO8vLe2sb7Sy04+Ojo2MDlGOz1OOjw/
+OjVASDtXzdfKu7u9ur6/uLi5tLG3w7rMTUdCNjc6OEFLe1lKX1M/PkI5RUVO98rT2LrG2cK7
+x7mxuLa5wNpkSjs6QDQ8WT1i01Fj3ERGU0c6TV9W1dPM0MjQ5LvDx7ayvbu50VlUQDc3PDpA
+S1BdZFlsZUReT0Rfb9vd0c7F5NnE4c/Eu8G8vMLL21xVQDtAOTxVTUN29ExcXktRTWPcb/TM
+zXTN1vja1srGwr2+u7/Z3e1FP0xDOUJNO0ttRV7lXnDdaNbS2u/OZnfoTuPufc7Axr26yb/G
+Svd0QD9GQjpIUz5WX1pbftZvzPHU2tfeXc5a99nvzcbDz77G2c1rc1NNSktMQ1FOPlBORlRJ
+eVD239vP2cnN1t3P1tbRytDEzNDW0F9veWFSWldJXkVGSklETklYWFX28Nr3z83S2M3S49DN
+zsvM19TM6nndUVlnTFRmSk5mQlJVTE9c/W3m2XTb7nLk3+Lh39vO19/IzePL5t98ZHtRXVhP
+TmRSUmhZUWRgXul4dtxt9efb6/Pe4d3T0s3T3dLbcNlubGtdWVlaRltSUVtYVWVbaXZ96Hnf
+6tji6NF609LayNbQ1uPb/+r37GhdfFRVW0xNVU9OW1BXWV1oaGfm8fTg3e3U0dzP0dfQ0djY
+1eXk3nLZ5G3X63jY7mlyXUNBRjM7PjY+SkZV29/MvMG9t7y7ur/ExMrOycvMy89bVU8vMDsp
+LT0uO+JHfMN5z77Zwr3Mwb/Kwr/IvrzDvLu/vc9OWDcrOCwnNy8vYk1Lwtbmw8TPw73Pwb7X
+x7/PvbrBubq+udFaWjAsNCYnNC02b1jiv9HOyeDRy9HJys3K08zHyb65ubW0uL/bXzstMCwn
+MDMzTmleyszuzdL81tTrzczWy9PfysfCubi2s7W902o+LC8vJzA5MU3oWMnKa8/bW9Le/s3k
+etnk7cvIxre1tq+xubrUVTwtMy8pNTkzVftPzc9c0+Ra3v1m4Oh36vNs18nFuLO2sLG4tchx
+ZDIuPCorQTE42FZnylht31Pp5HLh+OPhbd7b0r+8t7Czs7K6usZOTzsqNTQpNT0zSndLW+pc
+WuPV4NrO6+7Z/dzMyL25tbO1tbi+vsPMaElELi47Kyw8MzVQS0NZc1zyyc/axM3ryMnPwby9
+vLa3vbq/zcHI1M10Q0M4LTMzLDA4NjxES01Q2czawLzMwL3Fvby/vL29u8DEw87Uz9r59PFb
+SDw7MywvMC0zOzxBSlz75ce/v7m1uLi4ury+vr/EysjH0dXadmBmVlBLQz85NDMxMDM2Oj5G
+UF7l0Mi+u7m3uLq7ub3Dv8TIydLb3X5ZVlNTTkpRSz8+REFAQEdJRkZQWk9o2c3Hw76/xcPF
+yc/l6drZ7fx49XhabHNfWl5XX2tNT2FgSFP+3uZp3MjR7N3U0l1S8ehVUnfdaU9xz/loZs/Y
+VlDy501DbM5UT+nAz/vNv8Lc7sLOUFLd9UtJX9tRQFziWj5U3NlMT9nZSkfp1VxUbsfOZna/
+vt9tv77qadXJ41xd3O5LSfljSkJcaE9IVNtpUF7gelle9u1x59vO1M/Oxs7izM7d+eTa9VVg
+b11bTFJhUl1OV2BdXmlsZ2xd9Nvc+uTQz9zZ1dLS7ODa2f755X1vWVlTT01NWFdbX3X1ZGr5
+6Hro29DX3dre5Ovg4uzd3N7n293pbmN1XVVMUVhNUF1cWV1ncfdu/dzP2ezZ0dbu4dTQ+mrd
+2epgcO9uXFxp6F5bbHtzXl9/alBWZGlgc/fp3NzZztDd4uDd63h1fPxoV2ZzZl55dGhmXGZ3
+bF9i/3Zr+/Z/dunk3djd19jU29vl8Pbr9W1yYl5eYGNiXlNdamFaYWZlal969/xj5+Te5eDY
+3ffs1dfj4ubZ6HNx4eplZVtqZ2RYWmtoWFhh7mxhbO/aeffv5OB6dn7d3O358Nz4bfTn6Gls
++d7vY/zf9WdhbW9zVVhk7GVcZfZxaWr86d/s+fXy7Hx66eHg4dvq3d/f+3r//3BkeGtrXWZ8
+929fcPPsX1x29nVabPfqdfbv4uXr+PTW5P9h/P59eHDwb3hiavng/GL/49/5cHro+WVz5uPp
+8v326HNu6d9kWV1taV5eYPP0b2rr4nd8/nv75uf85t7e7+rg6nNz+fp9a3N0eWlzb25na2Vi
+Ymlsd3Nr7u54bufc6Prz6O3s7ezk3+Ld1dbZ3Njc3uTd6ObnaUo8ODs9OzxGW/HsfN3KytPW
+0cvGztzUysjHyMnCxMvNxsPEy1Q1LS8yLy40SOTW3M3Avsh8SktebGFg5sa+x9fSyMHDxcK9
+vL/FxMLPQSomKy8vLzZNy7/Cv7y9xNJWRUhOUlhi28XDztfKwLu8v7+/vsPFxMPlNigpLS8v
+LzZMz8O9vb/Ax9lfRDxATl/eyL++v8TAvr/Dw8XIytDQxb/GTy8pKy4vMDM8T9jEvr6/w8rY
+dVVLTFly1sbGyMrIw8TM1Nba2NXSzsnGydtNPDw9OTQ1OTs9RVBw18zJx8fKz9XY1+ptbvDe
+1M3Q2NbUzcrJxsTFytLW1+9LPDg3Nzk6OTtIaeDUzc3Ny83W4OLmbmF54t3d4uHc1tfa1s7K
+ys3NyMfIy+xKPz89Ozs8Oz1CTVr02M7Mzc7W397U2ux7ZWNqd+vh6OPVzMjFwL/DxMXHzNZd
+Qj08Ojc4OTk7RE9k3s/Nzc/Pz9LX4XFdYWxxd/3q3NLLw768vb7Aw8rV2uZqTj85ODo6Ojw9
+PD9IU23azs3Ny8zP2uDl6+7r4t/WzcnIycfExszS1+Du7XNdV1phWkpAP0A/Pj9DREdRaPDa
+z8zMztDT0NHX2t7i497b3d7Y197m49za29rY3+7u9W9jX15PRERKS0ZGTExKTmFxcu7e1M7O
+zs3LzM/T3nxjYGtx8t7U0dXT1tne4+Xwd2NiYFxdX15eWFhSTk9QT1JXW2BldvXu6O318OLm
+7OHd5eff2tjX1NLR1tbV2ePf4u97al9aW1ZTU1VZXV9dYWx5dW1zdm9obW1oamhqaXrs3dfb
+3uDh4N/c3N3j83l4bGdnX11eZWhhZnD9+/jr39ve3+HudXJ9f294++7s8fPwfHFzbWZgYm17
+cWxzfG5vaWJhZmxsZWj85N3c1dLZ2t3i6OjvfHttZF5fX2d+7/Z4f/X9bWhubXHx9HRsa3Vv
+YWNjaHB47uTh4Obl6f797Ojs7ufq7/73/W1tb29rdHh4fHj77/doZHh7bnF7eG798fH7eHj+
++/337vL98url6Orq9/z0+3duam52cGx0/HN09+7y9vZ+fHNz/PZ7fvf7dGpsa2hse/57dHbx
+7Ovp7vDp5e3v5+bm3+ft7/L5c3lzcHZ0dHdvcXRvZ2RjZF9fYl5fYmhsdnH77enk4uXn4OXo
+6Onq6OLf4uTj5vDv7X5wcGhhXV1jZ2RfZWdfX2RnYmFzffLo4+He4OHf4eb19fT9/nn79PLq
+7vdwcX15cGtvaWZoaW1nY2h2d3P35ODe3d/k6+fsfW59/P94a21w+/5xePrveXlpYHD48/f9
+bW1vbHR9eXv273Rw9ft88e3r5efs7/V0bHr+dXh6e3x7e/3v7efp7PN9+3lua3B1ZWBncGxs
+/v5vdXb+9+/r6efn4eXj5ubl7vl2ampoYGFpem5pc3V0/vt3bW159355eHh2evDo7fL09vb8
+7+jud3B1+/Tt7n5tbfb3b2pz/vnt7/Dr8PTf5/j49/1tbG1ta3Frb2lfZWxfWmvu929q/fdr
+Zurb6OLd4ePuW2jT1N/e3vHv6Onr/W9sb2ddW15UWX1oXHTo9WxrZWV16en8XmnYzsrPbWPn
+7Wt1a1pt6tvkXmvxd+/b8F5eXXJfceNyYX7a73bf+lxv4+1gWPrwY3Pj2dn+ft7c5GpccG9k
+ce5vfuP1XFvt1+hy3Nb6VE1Xa/fl2d7pf2x6X23g51xt2v5sY2Dp1Obo1+VyamRVW2f97uLq
+Zmds+3fw7e/tem5saGbv7Whv6OTm5O/6+/Ti4vJ86OxiWFhfbOzf6mtv+Pf5aXZmdPp7dWxs
+Y+jY2dvd5nRz8Ov6/3ZmXGXv4v1hYnnt93t7bWFhb3t+eerqdn3m3N96X1pcXm/u+3H24NvU
+1Nnd2dDU4Xtib9/V1NvvWk9IPzgwLzU/VerNv7/DxcTGz9fWz83PzcvKxsG/wcTDxMTEwME7
+GhIYJC9Tt6qtvruqrWswMj9X0ri0zU/ZtbS+xsLJ3s++wNvTvbKv2R4PDhgpbq+lprG4qKKw
+SisiJTi8rLZ/eL62tbW94FfeurvV5sm9sqmuKw8LEyNEtKKhrrSknahkKx4ZHTuyrrmwqKmw
+uL9RN0q9t9B5z7+2qJ+4GgoLFSX9pp2jraacnrQ+IxgVHkyvq6ukoKavxkIvNmnGx8rFx7up
+np7gEwgJESW2nJqfpqGepL4yGxIWKNCtpqOhoqazTSsrO9y9usHb2relnZ9PEQcJEiytmpuj
+p5+dpMQsGhUbMMuvqaSioKO1OyYoO9O7vdtfy6yfnKM6DwgKFjapnJ6mpZ2cp9wmGBUdO72u
+qqikoajJNCktSr22zEtbvKmenKgqDgoNGUGmnaCinp2hsjscFRknWrKrrK2npbBXMCov2a+4
+cEn7uaadnbEjDwwPG0uon5+enZ6nwC0bFxsre66pra6pqrlVMSs5yLTAYVjOr6Kdn7YmEQ0P
+GzusoJ2cnaOv+yobFxsq5Kylp6qtscNIMCw+wrW//WXOsKOen68tFw8RGSu8p5+cnKGwaikb
+GR0u06ykpauwvdNNNzNDyb3SW1jQrp+dpLM3GhIRFh9gqJ6ZmaC3SCgbGR4r3aien6q1z05H
+QkFP08x6VVnYrp+eprhEIBkWFhopvKGZlp2wRSkfHB8qSbCgnqa6ZUFAUXTj5ObvXm7Uu6mj
+pq7IOCAZFBQbP6ialpmnZSwjHh4oO7+lnZ+w3D82P+zO1dHMZ07pxLKppaq8YC4dFREVKLOb
+lZeg0S4jHh4kNMmlnJ2sUjAuOta+vcDF1UtGb8Oup6etwz8lGhMSGk+ilpObtDYmIB8mNX2u
+n56ozTIrMXK5ubzE3k8/RXG4p6SnsNMyHxYQEiitmJKXpj8nIyEmLUG8o5yguzYlKT67r7S5
+xnhAOD1rsaSip7VbKx0UEBc5opaTm7guJCYnLDdat6KdptYsICz1rquzuNJNNSw21qien6W2
+PSMcFA8Z7p6VlJ3SKSYpKS49c7OioK9AKCk7vaqsuMljOCsvSbafnJ+qwjYeFxERH7OZlZim
+TikqLCssOsurn6PPLCcy4a6or8DTSi0nL+Cnm5qgr1gnGxMOEi2kl5SbrjssLysoLD+7p6Gs
+SCwvYrKqq7zYVjUqKUGxnZqdpsEzHhUPDxrGm5WYosI3MS8mJCzoq6GkxTAqRbasrLPE50Uu
+Ji7NpJqan61pJhgRDhEnp5eWm6jaOzcqHx8xv6iksEs7X7yztLfG21s4KyxSrZ6bnKa+LhwU
+Dg8a0J6ZmZ+s2kQvIB4nULOrrsTdy7u3vsHK3l09MS5BvaeenqCsSB8YEA8WLqyempufrdU7
+JR4iL2LAuLW2s6+1vcfN3lVBNjM867Gknp6nyCgZEw8UH9qonZmaoLJeLB8fKDZKd7isqqqu
+tsLI0FpCOTc6UsOuoJyfsjEdFA8SGzXLpJmXmqK2PiklIyQnMc+vpaeqrrW3yN9PPTg1O0i/
+pJ2dpHQhFRITFyM5tZ6Wl52nvU8xKCEcHjPQrquoqqyprrvcVD40MjVJuqWdn7E0HhoYFxsh
+OLqfm52fp6687jMfHCMvU866tqukpaqxvvhGOS0sPMetpqa4PykkHx0fIy5Ltaqqqaenqa/a
+MiwvMjY7Rlm9rKiqrbrPVUM3Mz1ywLOvtuU/My0pJyorNELy3L6yrKuutc/fVU0/PENRy7qv
+s7rLZkQ5NjM+X8G3s7S4vtZSODAtLi8yNj1b0b27ub7CydpmT1fwyL24ub3H6009NTI1P2nI
+vLezsre9z1s+ODQyNTc7Pk1jz8nK13pjVWnjy8W9uLa3w+JMPjc1Nz9Z0L64tbm9wsjWbk5A
+PDk6PUpebHRYWVFORUVNb8u9t7e6v8TPYEM6Oj1MbtPIv7u8wcvbcWtjW1NQT1rv2OdcUUQ9
+ODc5P1Xdxbu4ubzG3lpOS0xPX+DPwsC/xs/hdl5MSkhRXd3Kw7/FzG1JOTQ0NTk+TnHNv73A
+y9Pmd1tda9zNxsHFytHW6WRMQz9ETWTVyb+/vsPPYkhFP0A+P0FIXfTjc21pZ29+3dHGwb/A
+xMnS3mNORUJARk1y2M3Hx8fKz939bG9fWFlTT05QTUVCQkZNXHrg0sjAv7/Ey9LlbVFLSEpY
+dN7TzMzNztPT19rf3trb3O5kUkg+ODc4Oz5FUGTbyL++v8DHy9DafmhuaWtqePTr6+Xc29zY
+1NLMyMTJzdxxSjw3NjY0Nzo+SGPRyMO/vLy+v8XL1uR5W1FNTk9XWmby2crDwL/AxsrO2fJf
+U0o+ODg5NjM3PklYdM/Hv7u2tru9wc58W1FIQkFFR01l1sa+uri5u73DyM/ob1pLPC8sLS4t
+KzA8VNfAtq+sqqquucXcTjs1MzQ3P0xl0b+4t7e3uLu/yNPh5+d9U0E0KykrLCosOmHLvrGr
+qKiqrr3kU0Y3LiwxPEZS5cW7t7e5uri3vcvU19bY3fJVRjUpJyosKSo3ccS7rqmpq62zyVlK
+PjMuMDg6QOrGwb2xrbK5ubvJ2+RpWnrQz+5dZ0gsIycrJyMtVMa8r6ajpaaqt/BIPC4nJy44
+PEXPtq+ura6zucDgTUVJS0teyrq8yNPyOiMfJSgjJT28rqqinZ+lq7pQLyonISEpOlrNuK2p
+qKqvt8DaSzo6P0VIWtG/uLO3zWNeNx8aICwpJkWsoaOhnqCqvFQtIyMkICRBurGvp6CjrL/e
+b0MzLzU+SU1kzr66ubm7vL34OTQ2Kx8hMUhATrSlpausr75gOywoKzQ4O32vp6qtrK27b0Ex
+Ljo7ND7QvMrewrq9w8TV1r/BUEL9YS4hIy82LjLZrqmrq6qrt2gzLjEzLzRhurOzsrC1wWg5
+MTs8Mz7Iur64sLXDztxSS15UTNa/zW11cC8dITc1KTuuoqeppqm0/i8lJywrLEe1qKipqq24
+YjEsMjYzQM65t7e4wtz8UkFCUGHtyr26tbTHSzwqGx0uMypLp56kpqivyUIpIScvMTvDqKKk
+p669YjMmJCs4QeGyqauxvsj4OS0zRVRgzLWvsLS6wcs+JBobLTEoOaicn6qqrLs7Ih4nMi83
+vqKdoamwvWArHyMvPUzUr6aptsbeSjIrL0ffzr6uq6+6yeFhQi0hHyk6PULGp6Orur3AbS4l
+JzM+Rv+ypaOqt8pkOiomLD91y7WqqbDHcUc2LjRHcs67r66zvMzk5U4tJCEoNzw9z6eiq7q9
+yVEtJic0RlfNrqSkrb3ZTjUqJy1G3sGyq6mvxF5DNy8zQFjPvLOvsrzR6V9ONiooJCs7S1i4
+p6attb/XRS4oKzlDWsSup6iwxGVDMywsOF7GurGtr75sQzgyND1Uz7mxr7O60FlMTUU3KiYq
+Pk9J6a2mrLvI0m01KCkzS/rIs6ejq791RTYrKS5D1Ly3rqywy047NjY5QHG+sbCztr3eUEdA
+Pi4gJkBPQW2zpqa01ObtOykmL0rdzbyro6m+W0M6MCsrPce2s6yqrr5YNzAyNT3zu6+ur7O7
+2U9ENiMcIzY4P8qsoqKuwshwLyUmLj5d0beopKu51k84LCctRta+r6morsJPPTcwMj30u7Kx
+rq+61FEvIR4gKS480qyjoqistNM2JyUoLjlSv62nqKy2z0g0LC41ROy+sq2uu8vbRjY4PEBY
+1L2ytLy+0UIuJiMqMzhNvq6qqq60u9s9MjAxLzNXx721sLG0xlRGQzo2O1DJvby6uLzOak1C
+PT1FXNLKyL/G2eJPPD5KT0c6StnuVX/OyM9sctbtTkZKWmdw1sO7u8LNzdxTSExb6+Hp08fK
+2OT2ZlNNWP7p5dnR2ltNU09GOjdFV01Nat7N1u3bys71WVr7YVFpz8bKycLBzOf85vlYU2vW
+0NbTztF7U1ZhXFRWaenqaVdYW09FSFpZSEhMT1JRWuzb4NrMzdXg4dvX1c3Fw8jNztHcclxY
+VVdk7vji2ut9Y1ZYXVxeavjg815bY1tVUFFYY2tmXGX88N/k393qfPt5bXVqftvc19LQ1tjm
+e2l3eevj3Obi3vtsX1dcVFNdbW7h593c1+11ZF1ZUVhf+9/c3dvX5nV0XE1NT05ZaG740tXW
+2dr8fGptd3zo3tfR1d7d7/txZmJ4/XLk2Nnr8G5XXldQTl5iZu/f29vZ1+VzX1RRTU9cfW3p
+2Nvm42xia1xi/fLs19XY2tfl8d3Y52vt6HDv337s32ZbVFlYWWRl/P74eXR9Z2VgZ19i7mz8
+4m134+jb3nDq62h64+Dma+/VenZkdF1eaPX66uTo5N7scPtsZmhi7n/n8/B67fF0aGv5cGhp
+6ex/8W53aGlkant1X2n15d7f7ObteGVfWFxm/PN729r68en/9Pnq5Ojy69/f4uDk4ndscW5r
+cGxnXmZgX2drZF9ifX1ubXLt7vD34uTh7mp5/3L59+nr8ubi6/v1dV19aW3153V37d3o4+nq
+7ffx6W5lfGb78mZoZV5saVhebl9v6m53+evp49/wbvr5furj493Z4Obv/vN+d29sb2Zkb2Je
+Z2lwZ3Fybfv46Hz3b/71/PLi397n6uzq+f389PL28n1nfmdqaHdr8/P39PN6enR5amBmZGJp
+b/nt7uXl/e58dev2bP334t549uX7fX3z7vhj7n5teufh72j16XBeX21iYWB1fup7duv47ufn
+8O1w/OV/8O599nVpbn/+/23u7ul6e2j8fl13c2J5efLu9ezv6fZtZ2RrXXvs7uPm7PTp9Ob4
+cnlrbfh+enJz8OTlfXX2dXNhcuzvd2dv6eN+e+ZyXmR1+HV67/J+X1547XF67Ozzff789ndu
+aHnk5e/54eHv7eDg7ndka+/5bXvk9nPz6nReXV9fXF5eZnZ0fv1/+P3s5OHo7urs6Hxobvzm
+3+Hj3uP78Orj9GxtZVxYW2RoZF9jbXzy8+r0eXx4d3fr93H79fPx5uPr49/e5O93cXRua2x3
+dm1ubHL6fXFuaWZre31zfPz2/3z8d/v28O7p5ert8+/393V6+X14em9vbmdoaWppaHvv7uzm
+8/d+9+7r8PD3/f1+9n5xevrx+3lqbW5vcn3v93Vva3X79O/v8P78cXh6eX/66+jn8O/r83Rw
+cnZuc31ubHN3eft/8vR8/fL4dmprbnrz5ubs9f39+f5veuzx9+34eXFt+Pl++PH4enJvc25m
+ffpxe/t7/fnt7ff37+rzfHt+fPjv8fDv7Ox8b29yfP77f3lzaGx5bmp19fPz/nl1cnH98e/y
+9vH19O7m7Ori6f90bW9xXl9se3Vz9n5yfPHy7+7s933+fvN1ef/z9XNyfH94dnFtZ2x7ffTu
+7vHy5/N0dPz9dG1vf3x2fu7k5e/s6+99eP33fGhobmZfYWVnZG/5eH368v3z5uPi8Obg7fr/
++fT5ffbw/3j6+X72dHN9b2ZnaWZfXm52aW19Z3H+9/Dv6+7x6+Pk6/h6+PZ77u749+/s8HD7
++Xrz+Hb0+WtxeWRfbnR+bmhzeHhrdunybfzn//Pt8uzpff7ua2Fq+nZ9fPnv73psfOf5c/Xq
+63NqfvL1/vTp8nr3fW99eG73eGh0dfV/YGzc63R39+h2XW/tZWf49G5sa/XwaGX03/Fkftzn
+bfva4O124+X1efzo6l9f9HdfeHBzfmJt5H1l8vhvbmhe//5l7t3gfHXz2upj++Dvb3rk6XNo
+8fP5aG12/G5x7OnvaXRtZWt2fv/q8/vr9G14+X5ke9/09O5rbHNpaPt89OHm+G375+3j4X19
+82tgdv/849/q/Gtfb/thX39sX2p8aGjt9G5t8Xzk3Xfp3e/n8Gp24u97bWb172Fu325s4v1p
+5/lr5Ptid+X6Y/58aut4Xfvda2399/rqd3Dk82pmefh0Zujp/O5tY+D6Xf7mev/t3+Pk7Gl6
+7WFX93Z/325e5+1f+91vXnZ59Xdd9939cfDj8PJ55Xli8+jq+Xv97e54ZPPuZF1nW2n3bXLy
+6+vn7Hhxdm597H556vP+e/1vZW/uZmng5PTu7Xf3b2Lv72no2/Ntef/f71nu4mNlcnP6b2Zp
+b19fbuV9Xubc+XTseWb0/HvY52Xj2uvw4fZu7nd55mpp+m1ZXnl8bGh2fP7v7fnrcvx+aXTu
+4ulrduXkblxtefZ2ZezoaWf1c15t+fR67fvv2vR13uLq9uh89d9tbuxed/5dcnFtdWhx7GBu
+3mdq52547G9k8uVrdNpsZOn0/dpvZ/DtenHy9W9obHbf5Wj33vn9/GtqdXZ87Otq7nhf7uZt
+79hmZfRtaW/v4nNe/X14b2Dm0Ohm7ODq/GdiePpqbm1oXmn2b2bu6vHuaHHe5Xvf3Xlq+v1k
+aHno4Oh57tPxZO7mcX5lXHNlXG3kcmhs8PtqdvTwdHlv+Xlv+9/g9u7k+nx35Nx0bH378mlY
++/xWW3P2fWJp1d1a7tF1YeLyaO959uBrYePbc1/r5+5zbNn1bW1662tkcfN7aPpwbPJxX232
+ZGrt9/fwbfZ2be/v/nXu6/x6+e/b32163evs/mH4enRjX+/9Y3xueuNz/uZoZP3v+mBs6fXt
+73v25/ry6ev0b/Zvavf0cPfnXmD3bejza+7iXF3pdntxYOHfaPbndmJ93/b54en19/bx5Xle
+3O1UY/tfXG3t3/lm2tZ4XGff5l5d79t1XXbt/WV8eOPnU3PdamZteerzW+fTXVba2Wv73+7p
++2H+5+t4Y+LjVmfcf2rp621gdO//aGlseXhfdeB7a/Xd5XN48uLyb+7ra3xtYfF4ceLya2fw
+22Rn3+d9bXDc5lxifm9faezxaPntbnFtdtnoW/fvavrx7OHhY+/Vb/vc5urxbP50W216a3pq
+amtgZGxpc3lkbftvd/J64tffdd/X8uXjanheXfV7bXX+7+1rfdbhc+76XPztZ/5qXOzkZGzn
+7PFqbN3rb2x+7V9e+F9ieGvi5GDo0u9o/uX6dmdg3OFfb9vq6+h4ft3jbWj4fnVgVuDdUl30
+YmJqZu/tc/nj8W56697/fdjraeh79+JpbN/uYubgYW/9ZPLkaV5acnhlcGZ13/Rd6dD9aObp
+6vNsaep5VnHb/P5t+3lie33w8GNu3/do6PBp9urseW3q4Otl8Np6/+Py9WZXc21e73lqc1tf
+5m9t6OvufPL2aW316+Hx7dnj7nl0dG53dPn4feznam/w82dv9fBsXf7tfnvj33tZZt9gVXPk
+b2by8Ov7XXPU+l/s6+3r4fdkZ2tu+Prk3dvs5/5162dt831sam5uamBr7Gdp9PltZnhybnxm
+3txs79zq/Xfr4+JsduXiafXmb/hxbeToXWttemtfbHpy+3ZpdmDu8XTl5+bo8ervZv3vb/ns
+ZWv7a215+nPs+nTr8nh5b3ziX1rX2mzs7Xje6mry4ntiZnZze25v+mJYfndq7u/v+nh3+2xu
+bvPr7uvm7uzueOfq8uTke/PuW2b3bWJqfOz//Ph1dW9f+uZfdO9obXduc+DwZe7bb2Tz8fXv
+afzf8+n+e+vp/vDf9Wv17V5u/fTv93T1fmppbnBYXezpXW3Z5ltp3/h3f37p3n114fVcXvb7
+/d7n++/6b/XoanblfWv7dml7fPpnYerxbvPpaHzx8+PlbnXzcW5s4ux67fJrcnH35XFobe57
+WnbdYV7u9O3ubPbuZm/06vRn+ud2+/j59fDx+f1udud3XvzgdGno8Xn+7e/y7f1oeeh1a+35
+aHTzZGTr9F1iYGr3+vbk82l56u5t/ejo/nLq5W12e/zudvTr6PRy/999bvHx7ndhbe9paXZx
++3dx83hz7Xtt7ultaHj78+ruc+5vZ3795OJ9eX779nht8vdpcG9iYfbo4+L77ur4bGx49vf1
+cWpw8fZpeu3q8XVub/hza3fs4vxq++t+eHHv/md56+zt7nxz6nZbaW9taGVufPHo4+ftfXjs
+7nh8+Pj8b2pscG9janPu6/3s5fT3dujgb3bj5/F4a/7zcG3w7Px3c2/1cmByfWppbHr1cmx2
+8Ppo9ed1d+bs/nl17uhv7tzl/vjl7vtxdPH6cmdxfvFsc3ltdPT98e/++3V29en9f/bu7Hf6
+7HdubGpxY2VtcHp9/25u7unx6nt763347O/w+fvv7ejq5eL6+2xmeG1iY2l7+fl49/h98u79
+ZGNkd/x98+DnbXj17fxtcu73dG365PR/7+Dlc337fvR2cnFva213dnp8fnl0c/5zdf11dG1q
+/e7r/Xt7+Pp29N7e8Pfy+v3r5nxy+XZ2eXp9/XdtbXN2cX19cGllaGxtfPj9evHu7+zj4Pdv
++vd5/Xh193ptfP53f/z5cXF1b//18fb/ev7r+nT+7ntyaHjzfvty7vD1fern+W9t/vP+c/v1
++HhjaHT18+zr7PF0bHz6aV908XFqanbwfWlv9flyd/T6/vb5fPny8O3yfm53+n7z7O3r6f55
+++7v/Xx+/G9r++77b2/6/nJv/fn9fm5obW5kbHX89+7rfv7z/nf88PF/dvvw8vPq6/H/bW1v
+aWhtcWtxd3n17Ojj5ezk5/Hv6u51ZnB/b3J+8vh6dv19bGVta2dqZ254cHp6b3Z2ePv3+fD7
+eOzf5ebm5u729f95dn/9fn/+fHx7bW57e/vt4/B4eGpjY2tsaW1tbHR78+j2efn9/f12/vH1
+/X758Pny731tdPbr6+75ffL7f/d2eXd7dG1ybnFxcHr8+u7s6PRyePX7+O7w/W5jYWhve2xu
+dHz08PLz8/t4/Pb39fbs7fV7ePXq6vz19fv6/np5efnwd3Z6cnBubG/8f3psaGtua2v98e7t
+6uf08+/9+fLv/vvu6ezp6fd2cm5nZ2dsd2xubG57bXl+/H1+9evs7uzr7vLr5+vq7Orq6v5v
+dnJzcXlxaW1tbm97+/p5evl2bXNvenhuefd+//V9/vTm5uLl6+12cG909u/s7PF4cv/99/lx
+bHFubPt+efx/cnzy7Oz5ePp7b25rb/jr6OXn+XR2bGt0emtscn7y83p08+/v8/t5bXrz+m93
+7/p8ffnw7Ozo6/B9dW9qdfr+cnn9//Ty8enn83dqZGV88PptYmh5eP/1fHX69fr7+/r3e37n
+3+736vLx73hubWxreXFnZHDn5unj6f959/52/fL6cmdrfPLo7X1ubnj8fvpvY2ltbfXq/Xp4
+a2lqeeXq/fvv6+ru7+3w/X3z8fnw7vT/aGt0c/HvfHBsaWlreX359WxnaWdqefttaG50++7v
+7+bn8Xn9+uri5fh3cWx37+ri4fdiXGV3e/Z3bXv6bXF8a3Lv3tvpbO/R095+XV94bXPyWkU9
+R3TRzM/X9F1MTmHZysvZbU5LU2/NwsnfZ1NVb9rFvsbc5HRe7OBlTz0vLTvlvLS7zl1FOj9p
+xLa3y1A/PEVry769y1s/O0Ne0sG/xNdNRWfa1s3Oz83dZFhd/9nP5dnVMx0s0L+0scfP1C4r
+Qcq1rL1dSjYvOu+4q7HeRDs+UM66r7THXkhN6czIt781IRwhX6ysq6m/PCgiMbysrK/ASzQq
+KkS4rK6+Szc3OEq9ra242E9DOj/Grqy40tjjQC4oJyQvuqy0sbb0Py0nPru1s6+61kItKTlp
+v7W3ucJTPUhg0L62sLxSP03iwbu9uu8pHhobNa2mop6rai0eIUbDtKWirfQqHyg5U8e0ra3E
+PELVyM/MvbC03E5g1c7P1+NULR0XHUK6raOdo74tHyQyR7ymoanNNSorKzHrraexv8DPRj1x
+tq69zcPOTEbvvuUoOWIhGylJvqy4qaG/LS0yOTdJqKO7ZXRJOi4wza+40LyvuXhkxLrH2sbA
+0Es+Q1XvOiIdJjMvPLGfpLTIxn8yLEDZxcrHtMI8Ps7JVEjPuMBs17ezu762ue09PT8wNG3N
+zrzPHxksKihmqp6fu8W+OSYsQM/O1K6qwFBW2ms+SMa4vMK9tLvMx7zMR0s2Lzw7TcC6vy4c
+LSodMLenorC5r1IpMPm+y8atrcxQXXZfVNm/v768vbrUWNJKQ1AvTLpITMpiMh8fMTMtTq+o
+uMKwuE1F683Wase2zvW5ssdPT95nRuS5s7fAfUw/Mz93VFVa30cvNjosLTU6Zfzjtq63t7zE
+3ktd0Nzfvbi7xNbT7VdZ+czFz9rsWk48OOXXTFFrV2Q5L1FOLj7dU01ly7m/x7m711VVdO9u
+3MrNzsTF6Fv0aFtu0MC/ytLdZT43RmNQU+DXX0FHUUg+R/R4Q1HJw9/pw790TFtrY1/SvMDS
+y8f4SVPb1uPNv77ZYWhTQD1LTVRcVWleXN7uW2V5cXle2sjR2db1WFRMSEhUadDL19rQf1lb
+U3ne3srCyc/bZV1JTGfb1dPL6mJbWl5kY3zqcFh09257c9/bV0lOVU1a8dXS5+TcaF3p32t/
+2dvbb+feVE5eX2xu39rMYOLK4efU2N9efetqa2xqXk1IUF9W7eR44XBpX2zncml87ufu5NrZ
++3xuam5fdO3Xzc3U3X13XVH+dPL/7+9fVu9SSVlSWNrf/9zYdWB4/fV7c2/r4G7t6/7meu/v
+bnnf6unk6OPm+GTr2e/mYP5yTFVTWF/2fnHqbWloa2Re7fT75+Lo+W/e7XHc5N3X2vfp2vlc
+52FRXWnj6+bf0/VYXl5cUFVkZ+/sbt3cWnv3eeddfNdxdmvs3l934+rc3d7e8d7lYW9y/H1d
+/t7s+1plYk1PXGpvbPvX3eD08+dkXWpi7v5o191x++DY2Obb4t16+mrxek5Ob25bYPHZY+7n
+3V9bYFpe4v7673thZOb17NnX0dXyb/BnWGhfU1x+9t/c5d/e7Ovd82vx9VtgZV5WaXJiaOP5
+/ebv+uLiaefpa2J99mpo7exr9fV049/e2d766OpmY2VYXnNaXvvq8t/f4OXddFpvXVVedf98
+auncdXnf3+fj6udubf1/Z+P1bN3yeGBs5ndoZn5092dp425bb3Fqem94d357697a3+zf62J+
+523j4/j2b2lnb29pdG/77+3s6ef1+F9bXmdbX23raWHl4vduf9/edurr6fNoffb+aOXc5ODz
+3dvs7PF0YlNVVl1lZ2ns4N/obezrbWVt43Vbberq//fe4HPk3OX0a279Y1t18flraGrnYmD+
++Gpt++l67vDx6ftf7OFk3vP96+N/cOzf63jy9Plse/R7/nRqYHlnV2Z89m9/5/B2dnvy8mx9
+4u796vFuZO/vfubh8PLpa2Ru/nDh53P57/77dmT26+T1799sZ25qZXJrYW5taGX87e199ePo
+715sbXtmeeDy9HDs6vvp3dvb2ePt9WRpb23+YXb5Y2ZzfnZ+/fXsd/dnW29sdml+92le/fp4
+4N/h7ejoemT54PP5597zcWry5/zz++l2dWlWb35baPt1XV9tcvT96urq2ud2cO3j/vt3evL1
+7+vi6n7v7/tkZ2hjY3JrY278/v76eP51/f749vX1/Ovz8u3t63Tp4WleZ3Bzf+Lo4+ly8+1r
+/25u/WVebWJo6/fz6vf3+n11d/3n9XfsfGRmfWxl9e5xcOvq+/Lo8PDt7e3tfmZvbmxq9+h7
+eubu+uDs3eZ9eG5rZWthaXN3+29xfm7l5F3v5/l2a/5xdXZraG11ePfm4ujrafXc7fvj3u91
+av/7fvPq7fF7c+/2/117fmZmaW99b3Lr7u51bHtoYm9ucvPt6ebj5O308eLyc3h1bWtlYmxx
+6d/p8vPo6ufr4eX+d29wY2ViamxmaWl18/J/eOnq/Xrs+nnyeXlweXx8fmhteHdtc/j+8ezs
+8evj5ebq6e3u73Ft+Hx+/nJvb3N3a2RhXV9dZmdoZmRtZm348Ork4eXc1dbTzc7Pzc7U7VxZ
+WUtERkxNSEJIV15h+tzT0dPRzcjKz9DLyszO0M/eTklPRD08P0lLSFvl/WDu2dvc1czL09PL
+y9nMxMjHwcj0P0BQNS85REZARc7KY2DKyOxgz8XS07+2t7+9ub3C3D0sMzEkJzpQVFHCrbl8
+1b7lO0HOymXIray2tq6wwd48JygpHSI1XNfLr6WvztnSRC0zXGVavqinrKuor8RYKyAnHxso
+RtnQuqSkuN3dXS0qPU9OzqujqKijqLvrNSIjIBoiNFTNuaemsMHdVi4qOUJHyqylpqWhqLjK
+NCAkIRohNVfquqSnucXQVywqPT89zqmmqqOfqLzaMiUmHh4sOEzTr6autsH8PSouODVLvaup
+pqChr75YJickGiY5PVLAp6q2tchPLyw7MzbLr66roJ6qtsQuKCgbHjA8Q+2sqrWwvX46Lzoy
+M/63sq+loaqtuTYsMx0cLzo3Ua+tvrGw0D82RDEucr++sqejrK2vPDE+Hh82MzNPtLbMr7HZ
+T0VJMzN9y8i0qamtrL89STcdKD0rL9m5zsauu17i1UI7deZjwLCzua6xSkTPKiBGOihBv9/y
+srNkbcdYPeXFeM+1ub22tGE6wTshV08oPr9RRby9TWPCXD/Iv2/Is7rKvL1JO+M9JlXqLFG5
+S067zkxczFxFyL7Su7S4vL2+TjdvOiZPci9euk5cvfFFT2tKQtXI07myt7m3wFg/TTAoU0cw
+yr5ZwLnhZ15MPT9jac63trW3trxbR1AtK0k2NNPY77q8z9jcVT9GWkxyv7q8u7O+XuJZLTJJ
+LjXfV/+8vsvMw9xXaW1YYMy+y8a3xF/mVDA6STE3VkdayMjNzsTL+tXP/ujOydfKvMvi3lI5
+Q0k2PklAVOjm6OXNzeHOytvWztDd0MTO29tQPUlIO0ZPRlb8/+X618/gzMnZzsjU2MnFytDU
+X0dKRj5BQkRQU05YU2je4tTL0MzGzc7JxMfMznFPSkJAP0RKUGJfX2V939/Syc3Oyc/QzMrD
+ytZ3SkhBPD0+SFBWamFYZe/cz8vJxcbJzc7KxMPL31hGRj88PURNTlFbVll45tDPzsXIzc7P
+z8jDw8v1UUc+PDtAS05VVlRdcurTzsvHyczX3dDLx7++xvNNPjk9P0BIU1ZPUFpi9NvNycvO
+1tva0s3HxcjEymVKR0FAQEZLSEtQXmt12tbW1tve2c/NyMbFw8TDz2BMRTw7OzxBR1d+/PPo
+3+bl3NPOysbKzs3Pz8zLz/xUS0E8PD9DTmBnffxubGzu3NXLycvJy83O0tLR1djzU0tFQUFE
+SE9bXFZVVltr3dPNy8rHydDX1dnV1NrY4PhsWk1ISUhGSU9PWGxtd97V1tHR19zf3Nzn4NvW
+2NzheF1QTEtMTldkaXPz7/D9++zi3NfW2Nrc3t3j6ufxaVxXU1Nba37u7+/zdXNfV1heZmz1
+4tjQztHQ0+NsXFlUVFJWau7n5uPtb2BiaWlmdfrf1s/O0d3v+WtbVVVUV2Nt6t/f9GxubWZc
+X2lrdOPX2d7c2trk9XZyePnu+HlybmlkZWJhZ290YG70dHjr8/rn7Pf08/j4fvzv8v94bXf5
+7vZ8bmhtcXh+7PDn3e35/HlqZW1uaW1vXVLz09/55e59/fXr397v7O5nWlVYZH3w59jX5vh+
+d3BsXGJmXWl87ezv9O/t7fX27/bv7fPu93lyffx9enRvZ2ZjX3x1c/Pt9n98e/j58efj7Ht6
+fvv7+e71+3j7+3z4en16++/q7Pp+fmtmbWprffp8+n9ucnl9fm/28vbp7vbu6evn7Hx2/vV8
+/vp7c/fu9v9vW1tnanf5735+7ufw7Obl6P5rX3H07+/p7fj18fJ2aHB4aWxsa21yamJubPDe
+5+/g3u7j3+Tp8vx6fHljYm1iYGd1bGNtbHP27ubk6ezo6O7x6ujt9HtudXBz/nJ2enb/dm9t
+bHz/dPf4dHz/8PHr5Obt7ep5cHlqamr4fHhubnn5ePPn+3BlX1tdZv3i3N7g29re5fxsamln
+YV1bYGlx7uvl39vf7fT8bGRhXWJrcPLq393f5P/w6X1uZF9canv97O34/OP8Z29zdHB3dPnr
+fnf37/T+8e306/T67v9v+e/z+H98a39xbXh5b2t3bm59a2348ff69PX76ezq4/Dw7PX7d3J+
+cGtne+/u/fl+Z3f2+3J7fvTzc2/9/331cnjr6/X5/vzq6uz68+r2aWlpZHjx6/fq7Xt0bGxr
+7O55cnd0cWlw9fZ893Nu/Xj77P1yd3zq/unu9eh+9O78b218cm7t53z37nt5/G5vbmZpc311
+8Oj0bnF1aG12eHN4b2/78PHh3+bu9v7zdW55b3FtfP9vevXv/vDq8XJvb/76d/v4f+rk3upu
+ffhvbWV693l0XmpzZ3X0d2t79/71/e3t7+Lf5/F4725kbvt5//92dXRvdvD1+P54ffL37/Z5
+8npe389fZtnhe2x282piUXzQZFfmeep+YujR5Xp+3+Jna/5vaWpr+/d2em16cG9nbG5Ybtpq
+ZtHP5v107nxiXN7dVVjb2W9g6dr7Y3zk3vFffOBkZm138GliefdmYl9fc2t43tze3OXf0tPS
+ysbM1+pfUEE3MDAyNj5Q0b65trm7vcbOz8m+ubmzrq60Sh8fLhsUIkXVv7yfmq3Wu8FBKSv0
+1kDZramxzbalrsPBeT4hGCMlGynZuqmtrJ+tSks7LyspW7S+r6SlqLO7rbPVXzYrHxsnIRs0
+xcywrqifsErYTy4uLl63ybSlqq+7w7XLUcTIPjgjHjMjGjTL2rexp52tZ9tWMyopUsDPtqiq
+sM/bt85IzbG43jggIigaGTnMwK6lnZyv7d8+Jh4kQtDGqp+muHtQWT89w6qotMs8IyAfGR4+
+zrKloJyeu1s8Jh4eKG+6r6Ohr95CQURIyqukoaq3ThwXHRgXL8Gon56amrg5MiceHCT6sbSq
+o6rFPTxOUsyspKCpu9UkExgeGh9sp5yeoZyiTSgjIh4eMbSpq6imr04vO+jNuKaen6zDRhsQ
+FhoXJbeempubmqotHyAfGx9Iq66vpKW2TDVV2W6yoJ+iqrr1Gw4XHRkhvp2Ynp+drCkcHiMf
+IUOtp6yspa1IMEB01LynnJ6orcUyFg8aHxonrJuan6OjvR8bIScmKt+lqK+trLtCLUvO1ryn
+oKKsrbBHHRMZHhodwJ2coKCksyscJC0mKUmvpbG/rrFRMTzMusK0pKSvs62xTiQZGx4bHE2m
+o6upp7E6JCcsKytDtquysausv1JSzt5U1rivr6+opLFOKRsYGBgcOrappaGkrU0rLCspNGu5
+qqysprJRPD9JR03Gra60r66tr8RYJhcYHBgcSqyioqSfpkooMDUsLD+4p6uxqKvpP0Y/NS41
+1ry5p52coLE7HhEPFRgfy56ZmZyfqTkdICcnM76gmZ6pr+cqHh0lM0S+p6CenqOrvkYuHRET
+HiUs0KObnaesskwoKDlUTm2ypq/2We08Ki1KxsLFtKuyw7uyv9pWLyMcHSctNL6jn6Cmrbo9
+JykuNkFmuquuwsjLTjg0O0dDTcm9vLavrrW90DomISMqLjm8pKSnqrTWNigsNTlJzbKss8LN
+6Uc2MzhBSmnFu77DwLizvMG81TInKi0sMvmvqa+3uMc+LjE7Pj1Jy7a7w7u2wU41NDo3N1LB
+trS2sq+2vrW7RDExLSstMlHEy8u4tr3VUFZbOzU9R/3Lx7q3xvBQQEFFQVXIvLe2ube6vrO7
+NC08LSYpKD/KXsOoqK24e29eLys0O1fPzLOqsLzJa04+MDVJWfLKvLCvvdT/RTgzMjpNd9TL
+x8DPST8/P1R90Liwt8DaXlpJPERYZvRveNPbYWRaWmlr9tTW53Bo7dzc2N5+bmllYWr41sfF
+z+hfST48P1Ds1ci/xs3fYFJIPj9HUebLycnN2vZgW2zsfmby2tLMy8nAxNpZQjs7OzxN4se8
+urzB2VVHPTg9Q01s2cnEyM3R42FZVldodvDUysjExtDlV0dCPjxDU/7YzMK+x9Teak9MTU5b
+bv7b0NXV2OhyWExNT1Nl5dLIxcbI1WlVS0JARExt29bPy8vY9WlnZFlYYHzr6OXh3eF6bndu
+b2pgcPjt3Nzg3ej28WFVWVZWXWr14OPj4PVvaVxZZvvr5d7f2+P8+f9ze3x17uvv7/Z7a1tR
+Vl5gcOzp5ujq6u7s7vdwYl1ifPTf1tPV1+l1YVRUW2fv2tLU2+lvWktGSUxUbt3OysrN0+lj
+VlBOTlh33tHMys7b6GZWUE9RWmP729PP0+H2a15bWVdcfd3Z2dja5XtkWllaX3H45+Db1dfq
+al1ZVFhfa3L15uXl5el2Yl5l9uPh3OH09X1gYGZnbXfu5+Lc2Nrh73RfXWFeaHF79+/r9XBj
+a/9zfOrm39zb5HtqZmJia3ns39/t/m9mZ2VhYmtu+e99fPhzZ2dpbnf27Obe3d/e3+Tq8f18
+fG5iYWJgaHn77O93dXVudnt9+nj38Pr5em92em1sdP3y6+nf4enp7vprZGFlZXXxfG98+vXv
++vPu/XNnbX36+u7j5e3u/mVaWmJu9O/m6/Hq5+n1/XVqZnLr6+3r921rZmZyff/27/L8d3h7
+b2Nha3ZtbX3t6N/c3+Xt+n1vZWl59H737O/08P1vcf35d3l9dG12+vL1+Pl6eHZ0e/Xw+X38
+/XVubnt1aG50b3L783BxfP7y//j0fv726d7f7PF8ePjr6uzw+3RvcW9rZGdsdnZ2dm1lYGNt
+8+rr7fPz6OPd3u9vZ2BgbXv58erm6OHj8WxfYmVkbP/t7e7zffj+dm5vd3n5fnlvdfv9dWz/
+ffz5+Pbu4/NsYmd37OTt+3BpZ37r4d7j92ZdX2589ezxff73+HRpaGp/7ero7X17dm1wevfz
++nZvbWptdPrs5tzZ3uxuXFZaafXj2t7s/XFrampvd/Tv7uz7dXFye//p6HJWVFJRZtrHytXi
+XE9SX+/W1OT5bGD529jc5HRbW2T739rb6GRPS09WXvre2tbV4H51XFBXXWfey8rR51xOTVBc
+7t3l6uni18zO7VNISFrUw8DH5E5GSE9+0MvQ3G5XWFpgXlZPTlVo4dDN1fxaVVdbbff45uTg
+1tHX63leUVzjzsG9xeBPPzxHcc/FxdJrVE9Y99rb5WNIPD1BSXLJv77C1lhJRUhZ2ci/wNFg
+SUNIaNDCv8fXW0pKV+vRzM3T/llb9M7IyedCLikrOc+up6m1XDEsLjzmurS4vt5KPjs7QV/J
+u7a5yl1EP0/Tvbm8z1xGRW7Cur52MiQgJju4paGnvjspJy1EwK+tsb7mSz46ODtI3LuxsLrd
+Rz0/YcW3tr7aV0z7xsXZQCceHylwqJ2eq34qISUy2K+prLXQSj88Nzc6RN63rq610kM7P1vF
+uLe7y/T03drgRysfHR80tZ6anbA4IB0iO7emoqi7UTgzNTs/SufAta+wvm9FPkbkwLm6xN9x
++uH8SC4fGx4wuJyWmqw7HxshObmjoKi8SzEuMTY9SvvDtrG1wepVWvDOw7/ExcXL1OlPLR4a
+HSy/nJWYpkIeGh8zvKajqrlYODQzMTZH78C1uMHPePXGubi8z1NN48bDxE8iFxcePqKVlZy+
+JRseLcamo6u8VTY1Ojk3Qf7Ju7zVYWHbuaurscVMPk/lyLraJBYVGjOllpadyiEaHjC6n5+o
+ukgvNDg2ScvCvMNGNz1VuqShqLRZNDlQ6sTcKRkYHTeomZmgzyUdITC/pKGntkotKyw9t6Wk
+r0IgHCfanpSWotgsJCo4STYhHCE+q5ucrjceHSzDpJ+nwkY1MDhEb7yspqm/MiEgL7edmJup
+5S4kJSYeHCE1tZyYnbkqGRklaaacnq5LJx8nRK6dnalUJR4qy6Ocn67Kakk6LR4VFSO6mJGa
+xx8VGTarnJ2uTS0oKjNIx6uho7c4IyE8qJiXn7VALS0rIxkVHVCdkZWqKRUTIMmemJyvPSUc
+Hza1m5ae4x4VG1uckpSfwTwvKyYYDhElp4yKlfMVCw8qrJaSnLY3HBggP6OTlqsuFxYvpZSR
+m7swIB8lHBceOauVladCHBUfVK6enaa2PR0YHkWdj5SnLRUXMqmXkperLxoUDw8d1JyPk7Ql
+FhUotKGepb7fUicfKlWhlZqvMhocO6uZkpWkQB0TDAsX4JiMjqkmEw8beKSYlqC2MBYVIsiZ
+kZy7Lx0myaSalZzDKRkPDBU+oJGRpi4XDxQst5uQlKI8Ew4cxZaOmL0sHiFqo5iSmbslFQ0J
+Ek2ZjIudLxUNDx9cpJWTmrYdERczo5SVotAqJkKwn5mezisZDQwWTJuNjZxHGQ8SHj+qm5qe
+wSYgJjizn56lwzI/sZ+an2kjGA8OFzumko6YtikWExwtxKSfoLU9Oj89RvrDrKm7w7Wnmp3D
+LRsPDBEns5aOkqFKHBMXIk6rnp64PT9ETtXOy7zL77OknZqm2S8ZDg4ZNaybmJupTB8aHSpU
+t6apydb7RWDPzLu+bMSon52kv0clFBAYKFqpmpibrisZGR4v06umrbLIPzk/RNu8vratrqed
+nqwsEg4PHDK5l4+UpU0kGxgXIcOfm5yp40MwJzBZuaamr6ukrcQsFxcdHB9dnpGPm7xWKRQP
+FSm4nJmamqwrGxwn462on5uht/ohEBQcIC+/n5WUm6avOxkSFB06xqeXkpu2OCIeHy+wnp2e
+pb8iExYcHiVJqJqZnJ2guikaHCAfKsWinZ+nseAqITB32Lumn6s7HR4pIR86r5+hrK6ruz4v
+OUU+NTpqwrKuvU04LywvP9isoaavuuE4Kik0aL+9uq+uvks0Ly8uLjVN18jExsO9w/JKQk7R
+xMa7sLPFVTw7PDxIzre3w+VfZk86MTE3PUlkzry4vs/y+epzYu7Hu73Dy8zaTT49P1DzafDc
+aFBGOj9GPVLXy8u7rt5St9k/YcTG3cS9wsbeQDxKNCoxOzw9RcvC18K6vL/KvbS/zs7UTjY8
+b+nTt62uu18tKCYdHy04Sty4rbCup6zC0MrCc0Hx0Ovd2c/abdG+7dTNNzctHyIlJjBOtqmn
+o6WqsLm/eUVKTlfvzL3AzMTSTkcsKTInJi4xOUFYy8e2qqimpaervuZtPjc8StvBtK252F0q
+KCgcHyotNELNusi2sLOurqussK60zM9nQT9DUmhpwuE8+DAgJyUiKDbszLSjqq2psbu9xL7F
+vrfIzcvZ/ENJSiouPycrPjMuMkE+OsOxvamhqayruPNe5VRVwb7Hv8LWRS46LyAsLyguQU9J
+0rC6u6mtuq+tvr+zwFz8+Edc1O1o9z8+NCcrKiotNkZT3cC/t62tr66tsre70HvoX03q41nY
+3Tw/Ry4rLy0nMEE7XMK+ubGttLOtt76zuM3N2E1LT0tHbmU/X0swODgrLDY1OF/QyrmtsLas
+ssK4vdDWyt5Z4fFRVl8+OV4/NUE+NDNFOjdlY/a5travrLC1tcPQ3vNWV+JZZf5eST1LPS88
+PTA6Szw/635iwrrEuK61tq65xb/PYF1lTUlUZVVJW0w5OjozMzw/PGjW/Mm4vruxtr23vM7F
+yOLy411OXFdUS01uRTxCNzA0ODY/anzdure+s6+6urXDz8bTY/j3VV/v9VVSZVM9Pj0wMDc1
+Nkxv9Ma1ubevtr24vNXOymlb8l9UX99/UGNbPzo+Ni82ODVCX/fVt7K5rq68vLnN8MrkT/jk
+XnDR3lxVXU43OjstLjYzNkt36sSztbSsr7u3vejl4lZQ9d19z8HRX2tmOS84LyoyOjE+5urc
+ubG8sKu3vLTHatlxR1Pc7W3Ev3Vz1E0xMjcpKTY0NF3N1cKusrutrsG7t91b31VEVtxt+7rC
+YO3gQS4zNCcpNjE138DIuKmutquuyMO+WktpT0BQzt/Tt7r2bdxAKSwwIyU2NjTVtry1pqm2
+ravI5MZaOkdYREjFxs+ys9Nve0EpJSwmHy4/Ot6urq+no663r71PWFU5NUNRTty4ubWtu+RV
+QSwfIyYgJT1R4K+lpaejpbfCwEwyOToxNU7f0rWrrKqtw0s+Lx4bIR8eLWfIsqOdn6Whqc5b
+SzApLDA0Ove5saunpaWtyTksKBoVHSAfML+ro52ZmqOtsfAtKSgnJyxC4sywo6OkpaWnxyoi
+JRkQFiQoLsCempualpy6UkYrHBsmLy5Dtqusq6Cfp6+urtsfGCQdDxUvSEu8nJGZopydzCUg
+Jh4YIGG9xbSin6y5p6K04bu6MRkaJxsSH1+4sqqZkJywrLIvGRojIiA2taaqrains72vrLW/
+uscqGhwhGRgp4a6poZiUn7PBRiYZGB4nMd6wpqGnr7a3sKyzuLK1ZiYZGh8aGizKqZ+dm5ie
+uUQtIRsYHCtQt6ymn6Kxzsq0rba+uLHALhoZHx4cJkyrnJyfnZ+wSSYeHhscKFavpaSjo67K
+68axrbC2tLdcIRYZHh0iMMigmZ2foq3NLh8dHRwjPb+poqKkqbvf1berqq60srs7HBMYHCAp
+N72fmZyfqrxZLh8cHB4sYLmrpKChqbzVybWrq7S/v8M8HBQWHSYxPMqlm5ugr9xAMSgfHB4t
+2LCoo6CiprPpWMmvqa7IzsRkJxcVGiIwP1a7o52dqc1EPDIqJiImP7yppKirqa7Dcmy6qaqz
+wsnXMR0WFhsmNkrGrqOeoKu+Xzo0MSokJzy+rqyrq6yxucC7ramuve9OPScbFxkfLEB9xLWq
+o6KpuuhQQDUsJiYuU7ysqKioqa2ws7O3yFlCPT4xJiAfJy47P0RUyLCpp6y1xnZJOCskJC9k
+sqaio6ess7zE2FxOUOu/v1guJSEhISIkKTfNqZ6cn6evxFAzJh8gLFewpKGlrra7v8bS3sa5
+tLG9QygcGBgaHSMuUa6fmZaaoa7YPi0jHh4lP7anpaiutbu9u7e2trrCyes4JRoVFxwiLDtq
+sZ+alpifqsFMNikhHyAqSbmpqK20t7evrK62ylxXZEUsHxkYHCApM0bLrJ+ZlZierM1NMyUd
+Gx0rVbKnqKyvsq6rrrfJW1b15UssHhkYGyIrNU29pJmUlJmjt1YzJR0aGh8wy6qjqK60s62r
+r7vM2MjDz0onHBkYHSYsNl29pJuYmZ6rvOs9LSMcHCM1zrKyubuzqaSlq7jN4nZrcD8uJiEj
+IykuLDZOxKmfn6ClrbPKPSwhHiYwTMvBurGspqSps85KQUNDT2PPvOY7LyQgIyMnM0fEqqOf
+o6y2xWZINy4yP27Gws/IvLezvelSTnfKxdV21MXDxFwtJSQkLTczO1jQt7HB0dvNurW4vMTN
+zdfvcmJu/v9wbOfPwbu6wsvX5u53+ttdLikrJycoJy1BV9y9tq2oqKmutbu/zvlRREhRZPht
+8+nu4c7JxsjSyb7A2FRDOywpKykqKi45VNnDurOura2wtry/ydtXT1lq71FBSu7NzdS9tbnM
+6W57d0QxLTpLRTo4RV9eREBW09Dc0NjIwL6/ytfOvsvpZGVj7Oj31c7CddvhbzxFTUpKT0Lw
+wtPj9MFu+UXtRkhJRGrX62HdfG1RWtXW2s3Iv7zVaVZgWz88Q2971enOyL/Ov8LGv2JGTk5K
+P0NLY3dFUFBK4FNt3ta5Y719vsFKZ0FWT0BFR9901/XDvrnUytvC4kNXSlhM30bUUW9YT2Z0
+XnlP3dXAw8zD68lCXT1PSkRhRMlhvOPB5sZgTFxa/VLcctL9x0fjSmVAVE7Vb9fTysHfxkjD
+RXFCaX/V6+HX2WnY7floamdJdl7N49p33lJdT1ZS70hZ7Mb4b9JX00p4XdVv3NTN2sTbW9Na
+3lFaSu3uT9395cpfWvlbYVnmWM9h6Fn5699W2ldfUPhyz/Hk4M7h42zVeHD+Y91lXmxhw0z2
+U89U1U3dX+JudGt83EpxTdtaW1j83t3p0ufZ8G7v8N338lvZaOh6497f3vpt2OTwemvxXlpp
+WmpOZlFmW3HefHns7V1sbXrw7WjQe9Tz5NX8z2rTXsxb0VfXZfruSdJBxzzbX1DSUMhPzV36
+fUzmQ8dGymPtzUi+Prc8t0vT0mnBRLg7tjO/Ocs9ZuxAuTm0OLU/x0flek7NQsE+wT7IUe3e
+R7s8tT6+Wc/NS79Auzi9PMhD0lNZwju4PLk+vlNkzj+7NLw/0FZLyzq7OMJIz+xrvUm3QrtA
+x1FSfkbPObg7wFDKaGLCPbQ6tT2/T2/XQ8I5vDq/P9P0S789tTq4PMFL7u9EwTm9OL8+yF9f
+xUi7PbtCxGN91WfOSs1Px0fISMxT3VRi3k7JRMpEyUPMRthMz1LVW2/bWc1Ov0vBTsNJyk3O
+U9hs1PPl62z1am1Pz0LPStNP2Gtg10vJRsNAxUTJS85addRY1FrlXtxi+eXlfNFjx0/FQshF
+zUPVSf/eTtJIvjy/P8JNz+Vh0VDEP71CwkTMUN3zUMlIxUHERMhX2fVo407aTuFJ01roV91g
+eW3wfdJmy1vSeObiXM9Sz1XPUdZm417iYV/fT9VG11rlXV7ZTctN2FDbXHjrZ+T+3GfSfNl4
+1tlazVLMT9Jd/X1c203aT91L5l9ja9nnXnB71k14btXmXOzvz2P14PfXXuFw935t7WHZVtpX
+9VLYRnrvzWJj2dbfSOty2FX8Y9zoeGfw4t/hW9x201jYXGvvZVf7deZZ7GV731bNStRPy1np
+52rZ6n3f799l3+li21TeXF1r+mB2cGXv++Fa3l/hbfJY8Pt5W9xt/dj/a+Ln52nhatx9XNxT
+6V/u+XbbXdVU3GH4a2V2YfBa42Lv6+Tu8vRw5Fvq8/X3deRu6F3cXunjX2xkf1t4aF/t+1vg
+ZeXz7e392XHfaed7cX5l+m1ve+777+318uBn/Hn2+XB2/mZ4emxrYXxm4Hb6bur+/e189+Z+
+8nTn+3Xrbmrx6Wn38XHybOVi/mX78HxyXnf+altd4eBp4M7Y+/3ZY1pt4vNe+nTveXHd+eJ8
++WH7WWxdc1xu6nBuaetv5Hnqbent4nj44vz2/Nt7/3Xw+fvuaelrcXpy+mtoePVp7l5d1l/z
+1X3l4mvj8mVpbe3r/mDYV2XIY1L91WBXX+77X13scVh+YuHbV9fef+Fk6N77de3g9+rd29PZ
+2tbe3NDOzs7iUT01MDE3P0tX8dnIvb2/ws7b+G/3cHBq69jWz87LyMjJycfBw9xGMSspLC87
+Rlhy18O6tLW3wNVxUVFWUFNaXO7czsjGw7+9vb2/wMTkQC4pJycrNEJl39fCubGvsbfCd0xA
+Pz4/R1TpzsO8vL29vby/v72+xmU1JiAiJSs7YcnEv7ewrK20v186MC8yO07qwrm0s7S8xMzO
+x8C9v77JUS8jHh8mMUrRvLmzrqytssRPNSooLDdPxrSurK+6xd1m99PDvLu8vMJTLR8bHSU3
+z7axrq2sq7DJRS4oKS493r+1rrC2w15KXti/tLS3ubi3wzofFhQcL8ysp6ipqau3TyceIC3l
+sa2tsbi4xFk9OD/RuLe6wb+xqqq/LRgRFiE/w6+qpKCksUIkHSM8vqytur+9xNlOPEbWwL/P
+XWDLtaqoq7w/KBoUFx44tqCdnaS07DUnJS1Ovq6wwtHd6O1cTm/PztZaRmW7q6WlrMY6JBcQ
+EyD8o5qbn6rDQiwiIi/YsKqxzWZnTkVJWce3vu5JR+i1qqaosMc+JRgPEiW2npqdp627SCke
+Hy+9qaix+Do5Pz9ay7ivt/0+OELCqqOiq8hAKx8WEx1Vpp2do6u4TCkfHy3Oq6mxzkk3PElf
+vq+uuGIvKjThrJ+gqbnrPy4eExMjvZ+cn6mtvTkiHSNCtaitvvhLOUB0yrWuum44Kis/vaad
+n6q4z0ArHRQUJbyfnqCrr8YxHhwnW62lq7vVXz0yP3q5rrdbNy0uSrqmnp+rt8VGKyAZFB/l
+p6GhqbPJMh8eK2qtp6690GJALjPgsKmv7TYvLjfPrJ+do7PVRDEnHxkZKsClpai2xlIuIyhG
+uKustsjWVzsuQLaqrc47MDMzPcKmnJyn2TMuMS8sIR4r3bS3usTAxkwzN/6/u8TO2M3XXj5e
+r6y7TS8qLzRFs5+cn65vPTxCQjkgFhw1wbKtrqqs9y0nMErEvbu5trrKRDnSv+tANjNDW2y0
+op6ir188PkxJOigYGSnWr6qoqqnNLiEmO8Ovr62wuNU8KTBf19ffU0liUditoqOqwEY+TeBP
+Mh4WHj+zrKSlqLczHx41yK+ur7K31jspJj3Eur/JfltPRs+qo6auzT82QHn/Nx8ZIlOxrqyt
+sdEvJSplt6+ztLbCTC8mKWuvr7zJXUM7OdWpoKWzXzk7f8HlMRsYJWa2taysq8I0Jy7mu7e9
+v8PZPzItN8Grr7/gPTU0PryjoKq4cUJVzr9+KBcWI1+zsKijpcItIClnvb3Gvbq9VDUuOr+s
+sdBWODI4R7ujn6ezcD5J2clPKhkWIWCvraajpr8sHyhPwb+/tK+1UzAsNcqutdtfQzk2O82l
+nqWx3UA+YehMLx4ZI16ysKuoq78uIytixcnLurK6TTIwO82zuM7eTTczPMeln6avxlI/S+h0
+OCIZHTa8s66mqLE9Jic8eGptwrKzykU9OkzJusDNaTo2P9asoKOruWI6NVHFei4dGyVdu8Ov
+p6nGNSkuTUs9c6+oq8FHNTVG7s7QznZDQFDAq6SnrsdANkjLx08sHR0uVlzXraSnvzcuODgt
+McinoarLPzQ6REdVzsdvRUJqtainqq7DRj1Jar/KKxwcJjE5XqubnrRKMSwnIiu/oZ6ltmhC
+PTEuQdjS42fft62vr6212kM7Ozlxra0wGx4lJC5jqpueul9MLiQmN8Cnp7S5tMs7Kys8b/XX
+ta2vtbu7vc9cVU89YK+osx8OGS0oL7Gdl59JN3A0ISdAv62wuqmsXTw7OkJIRtG0tbu2r7G9
+705MPTzFs7m3VhgUJSgkP7Ojna5pvr4vJC1Rvba7r6i7REREOj1nvLK+y7e20U9txb/O49Fc
+Qj5ISi4bJDstLdawq65m17ZtMz68rL9nvbTOQjhPvshyxrvUTV7Vzci+trXSQDxCNTZOyNws
+KTo0LjpG3bzVvqu32MvL3WlJT9LJ19HEvsTP+FVLUtjEvry9wcpVPkE/OD1GRdLJTkdGOC4u
+MT/NtrG3uL3qSUZX0cTIysfDyN7z9Nzv39Rru8fH1mXbRkI6Nzh6TNrWYdpnRT9OUEbfzczj
+bF5VTFXZzcbPbcjcd1XWwHztzl7D8MHWV8NLWl1OUnB+bVReSljNaOlk41/f81HZV9BTTllk
+cHb37m1x2kze3drO19VvwEfdX1DWP8lS7NBKzD3U3vbCTMHgYOJH7mRjeUW5P8XdWMxqWXJj
+Tldl2uPa4d5dX0ddYWhP3/nqy0LdXlngatrJ29/R32nfUufT7l3DVMZP4t1R00zTRORgU8VU
+bFXYR1dMSlzfW2a0P8JhVU3QTt+8YcbKTMxcR9fbUsXXUctMzE7MR+3aRe5H3cZU7M3xRk1p
+PdJNz9zIdXTY50Dk5uDY781twk92eW//aGjc6e/Rfs1h51BeQlBO+XvfVMFPXd44uTbU4uu/
+WrxOvj3IQdbcVL9+yWhAwe85v2ftx15OxEZI1j7ITz7AXk3dxUa+Vz69U1VPvkv2zEG5R1PL
+U/js4le4aEjP1Wr1YbhO231HyENQ5u9k3kzny0TlXMpRZ1Dl2VB8adFJWdFmac9L1sQ/xH7N
+Y+5rzG7z/cvaSsZDxj/VTPvTPcRDzXxExlZmYNBOzF3iZNJN79tPvj9s1/1dfcdGxlpvxEvT
+TsVQ991S0mZM5PVi6W9Hxl9MyVTi5FjNTNruSsvXWGLNXm/dWN3c5EK5XVPJSM1V2zzF3Dm+
+PMPfTMzr5F1uWXHSTeXVdmDvblTNTV3KcW//xlR462f5Sbo/e71H62a+NMO/L7rtP86+QlO5
+Oky5P1nHRctaaOb5TNPoRs/LTNfW/V3hYMzzTMB+Yc1eXMNST+TuUl5bZ89KauNxeknZZ9rQ
+PsHURMhPY9ngP8bLVtblZezXVWff6Et1x1Jo1W3aZ3HfWdVS1krjzUDh0H5d5fBX1mFKx3lO
+39JRYMhO59B78mPPSurcWGvq7VTr7Vvs0FFtx05n3WVcdelkb9h/Wu7d7X/r+/LVbPXl9N3x
+b2XbeFhczmRPbN56R+LVW0/j6lXy+edv83Tk6nRubddhd+ns5Ojv59Z/6vjmbG/za+lealv4
+9lpqcGX/8V5oduVfZOz03m7o1eL+/utkbfVyc2/37nfr5mn57fDtdOlq93V8a2/yXO5bePln
+9nzt9Xr26m5qdvLgc+nk6+146nB59nbpZ3T8XHZ7c2lv+WH8ZnxmX2xbb3Pnfejc/dzn1Nnc
+2tzW5+bf7Of77PL+bWNWTVVLSEpGSk1QWW5q7tzk19TP0M/O287e2s/b0tPR2N3Y3t/d6ePp
+5u1XTkY/Pzw6PT5ERl5od9XazM7Nzc/Q29jc29ra2drU3NPNzdTPydHS0djo395rT0VAPjs5
+OTk6PkZQa9/Uz8nJzdDW3Ojk3ePi393Y1NPNyMnFwsTFyMzU3unk8VFEPjk2NzU0OT5IVG7d
+083KxMnJy9TY9PLx++/m5e/azs3Kx8TFyMnM0tTb3NxuTD48Ojc3Mzc8Pk1h8NvNysfFzM3S
+3eZx9nx+9fnm29bMxsO/vr6/wcfL0d7h8U8/OzczMTAyNztDUWTt1s3GycvL0NjZ3t7Z3t3c
+3NTQzcnFwb+/v8DHz9bb3uFaQjs3MzAvMTc7P09o7NnMxcXLy83e5ubo5urr4NrY0svIxsC+
+v8DCwsrS19ndY0Y8NzUyLzAyNTtJXnrezsbGxsLGy9DV2+Tu5eDn4NLNy8O/v7+/v8XM0Nrf
+9lhDOzkzLy4uMTM5SV/+2MjEwb++wsrKzNLb2Nvn7+jc3dTJw8LBwMPHyczV3OhZRT46NC8u
+MDI1PUpUadzKxsW/v8bMzdHX19bX4uPj3tTPysjEv76+v8PHzNpfRz04My8tLi8yOEBKWt3I
+wcG/v8PIys3R19jY3t7b2tbQzMfBv769vsHHy9xYRD05Mi4sLi4xOT5GVuPMyMO/v8PFyM3O
+zM3T1NHU1M7NzcnCwMC/v7/G0N9SQD86LywtLS4xNzxKatzNw77Aw8HCyM3OztDR0c/Ly8vI
+xsnHv8DEv8DR7PFLOjc0LisuLy40PENU3srCvr28v8bEyNnSztzk09LXzcjGxcK9vcC/w87p
+dU09ODQvKy4uLzQ6RU/rycXEvbzFx8LKzs3N2OPQ0NfOxMPDvb/EwsDH19dpRz03NSwsLi0w
+N0VKW9fKyMS8v8fCxM7Zztbv2M/Tz8G/xb69vr++wdThZE87NzUrLS0uMTVER1vZyMjCu8LE
+wsbU3M/e7dzS0s7DwL+7uru6vMPLfEs/NDQsLC0sLzQ7Ql3fyMPDu8PCvsjOztHe3drU1c7D
+xr+7vLm6vcLO/U49NS8rKyorLjM6RWDjw7+5try6vMTFydTf3urr29DHxb68u7q9vMtyXj85
+LysrJyosMDVJbN6+wre4ubK7wb/E29HS6tTRy8nGv7+9xcTPU29BODIuLCctLDE4QGVnxcW5
+uLmyvLu8v8vIxt7Ix8W/vbq9u8bMd0hGLy4rJyYoLSs5PlXfyry8tLqytb63v8jKxdHSwci7
+ube0uLm/5lY+LyomJCAjJyovOU34xL+1srWtr7a1uMnFwtPMxMK6tLW1tLnifVgwKyomHyMm
+JSwxPkn8zb20uaystK+vvMm6xs+8uri2tbi2w2nXRiwrLCQfKikoMjxCS3vVvb66rbS6srPD
+wbnAv7exr7a3sMpCf0UmJjMnHysvLDJATlttzba6v6+uu7y3v86/u7q4srC1sr9ZVjwqKC0l
+HyotLTVEVOXMxra0trGus7u6wc/PycG6t7q3tdJdYDssKislJCksMjc9XsjCv7Kur7GvsbrB
+ycrM1su+wL/C811cOC0wLigpLjE6PUN1x8PFt7CysrGzt7zIxsDI1MrG9EQ/RjcsLC4tLTA5
+TGr0xbeys7Kvr7K4v8PJ2v7v2d3rYkI4PjkrKiwrLC8zQt7KwrWtq6qrq6qstLy+y9jsZlRU
+TzUrMTMoJScnKi4uN2TJvK+qqKOipqalq6+yvs3P+kY/OCspLSgjJSQkKSwtO2DVva+rqKKj
+paWmqquwube6zfBUNCosJyEjIh8jKisvPUzsvLGspqOjoaOoqaqzur7K3Wg8LS8sJiYkIiYq
+KjE9P1rAt7GrqqilpqiprbCxuczhVDYwMywoKSYmKy4wO0RN2b67sKytq6irrq21t7fB02Y+
+MDEtKSknJSguMz9XW9W7s62qqqqprK+vtbu8xNX5RjEtKigqKSYnKy02S1/iwrivqKWlpair
+rK+4u8PUfUc1LSkmJiYkJCgsNURP9cW6s6unpaWnqautrq+3wNJONy8qJScnJSUnKjFBSm7R
+w7asqqmoq6ytr7Gxt7zLUjoyLCkpJyUnKi04R13Mw8C1raqoqa2usbi3uL3A0kc1LyooKyko
+KCktO1Lhwr25sa6qp6irrbS6t7q8wOc+LyslJScnKCkqLz9szLi3s66tqqipq66zu7q6ur3b
+Py4oIyIlJSgqKi89bcW0r7Cur62qq62vtbq6uLm2vmk4KyUgJCUmKCgsNkrVuq+ura6wrayv
+sbi+vry6uLfMQjAoIyQoKCorKzRJ3ryvrq+usLKusbe7xc7Ivru4vWw4LCYkKCopKywuO+q8
+sKysrq+vsbC1wM3V5828vL3ETzErKScqLisqLTA+zbWwrq6wsa6us7rG6V1p39PMy95bQzc0
+NzY4Ozk2NjxN6MzCw8zV1L2ws73DwcXKy8TCyMTA1ExBRz41Ly0sKiktOElx29jMvLKtq62u
+s7q9vbu8v8vU4mRENzAvMC4rKisuOERzx76+ureyr7C0ur/Ew76+w8nKytFyRzcyMS8tKysq
+LDNE7MG5trW1s6+ur7S5vsnW19fOysjNXj80MC4tLCooJik0Tcm4s7Cwr6yqqay0vt5gaP7d
+zczR2f5LOjQwLCknJycqNUjdvbSvraupqaqttL/ab2FXX+vY1NLZcUk4Mi0qKigoKSw2S8mz
+rKioqaqrrK+5ymdHPz9FTmPk0s/Vdkc8MSwsKikrLThPyLStqaiqqqyvsb3ZVD87Oj1KV+PF
+vrvAcEQ4LiwrKSkoKzdQv6+rp6ipqqyvtcV1QzUzMjZCXcq6tK6xvOhCMisnJCUmKTBD27qt
+qKWlp6yvuMbuRTYuLS47Wsi0rKqprK67LSIuIhokJSAwUsimnZ+dn6qy2jstJSIjJzBMv6qg
+nJiYnKK8KhYPDQ4VHjG3pZqSj5CWpGcmGBQUFBooV6abmJeeqq+6tK23xlwuIRkUFhkhPsal
+nJmXmaGyRiIbFxgeKD62p56doqm9X17fuquqq6y88ioXEA8UIVqqm5mZm5+s1yoaFRMaLOar
+n56fpK6940hK3LesqayvtNMzHQ8NDxlLqJqWmJygqu0tGxIUGzKzop2fqa+5wNlXRUntu6un
+p6iqt0oeDwwNFTaqmpWYm5+vdCoYExUeV6idmp6prsNYPC8vOmrDtbOxq6KcnapNGQsHChMx
+p5qVlZqdqlAkGRMaK8CemZmcp7hbLSIfIS7ptqqqrqqknp6sPh0TDQwSH0SlmJWTmKCxRSQd
+GxwtbKucnJylxDYmHyEqN+a9uq+zsKyrqrXcPS4vLiUbGiA4qZqXm6rMPS8oIyEiMsejmZme
+rVYqHRkZHzHCopuboK7D0srPeTwzU7+6Sx0WFx/RpZ6hsOP72k49KCIxxp+YnKxDJiAiIiUs
+Qa+empyr2T5D2bSvuLm1r7E9EQkJDTeaj46VqLzNNiUZERo6o5GSnc0pHyYqKjZLsp6anKpf
+Li43SeVb1rGkm5yzHAgEChutk5CUnKy8+ScZExY6nY6Nlr0mHBoeIyQ8sJyUl6lKKCAqPlbJ
+u66fnJ6qPhQIBg0ko4+Okp7AQC0fGhoku5iPkaI6HBcaJDdWxa+loKW5Py4vSL6xsrvEvK2l
+p7kuFQoKFzqfkZOapLj7Ox8YGB/anZmdtDUoLz9WTDo76bKmpbPc37+vq707KCc7taGepuUf
+DgsQHr6YkpOarzQiGRkhOq+bmJuo5yoiISQtSbaim52q6CsmKzhc7VhW37amoqSowzAbDQoR
+K6OOjJKkPyAfICAoOb2dlpmoPR0ZHy7rtKypqrDNOSopOcutqrdVNjvLq6OhqcMvGA0OG0md
+kZSctDQkIB4hMNSlmZmhxCkbGyI5tKSfo7dILCYrUbepp7ZrPztUt6qop7NOKBcOER/fmpCU
+n98hGRsgNb+mnJidsi8ZFRsyspyanq9AJiAlNr+qo6W0XDQsMv6uo6CktEYhEgwQI7aUjpao
+Qx4dIypJuaqgnqjKLh4dK/mtoqm8XDUvNDzfr6moskkqJSpZqJyboLRNNCMVDg8ftJGMkqsp
+FhglQr6ysKmiqLwvGxgj66KZna5LKycsOuq1qKSqzjEmKEetn6Ct1j9EUzIaEBUls5eVn9En
+Hy/WuLncV8Wxsco1JClvrKGnyjQpLUXKwb60rau0SSYeIkWpnJ6ou9vdeCsQCg8io42Ml7km
+HCg4Ozw77aOZnLQpFxo1s5+ivT83OkxoSuizp6W3Lx0cKbmblpqnz0xBLBcLChi5joiNpyoV
+FiM4UsqwoJiatCUUEySwm5qmZC8uMjxK7rWloa09HhkgW5+VlqDINC0uKRwPEiylj42aTB4Y
+H0m5t7avqaOtOx0ZH9+em6PKLyszPUFDUbqloa4+Hx0sv5+bobROMjdJYj4fFBxFqZmcvTgu
+L92yzkZP77eqxTIpLFGpo6/XODJBSTtAWb2lo7JDJB8s366mqrW6u8bR6E82HREYLrycmqrJ
+aDtOcC8oNuKroKx1OTRAy9VEP1TEr7hZQ1HArbRfMi02fr+9x8q8rq/LPzdFa0QiFR06sJue
+st/tR/JMKytMt6SkwDovMj3zXk7PuLG03zc9Y8rGcklxx8XhOjJKtqanu0Y+5rWyQxcNGT6m
+mqPEyrvJxjMeJFOtnqXeNzU4TnA9T72wrrpCMj9Ib8rFt6y9OiYjNrKipK7PWsi1vUwcDhMw
+r5yfuMCwtsg8Hx40v6mow0hk83JYNTbauLK4Wz9ZXWbRyr+zwz0sKj21p6mvv9O8ts45HREW
+Lr+op7Cvpq3+Lh8hPb2ytMHHuLpjNCovbrazu87d0/ZKP0rQt7XOPjI49rq0tLS3trjLOyIY
+FCBLuKilpaKn4i4kIjD7zMK4r6qt4C8pLULIv8W+u7zDZjs8W8i3v1E7PVLFuLe2tri9zj8o
+HBYaMsyup6ejobFFLSUoNj5ZvKuiorg/Ly82PTo+17Ksrr5NQEha2d1XSEvivbW3usG7vMxI
+KhwWHjj3ua6nnp60SC8oKy0tOsGpoqa5fEo8NTAuPsy4srK4v9NMRlRpaEU+e7iwsr3JvrrQ
+Nx8XGyk84L2rnpyowkQxLiklLFS2qamvtLxkOC0tO1Ne17murbfqTV5jTEFBacG4t7W0trpZ
+KxwYHikvVLKjnJ6ss8U/Kh8fLVrMu7KnoKfUNS0uMSwuW7Goq7W8udI7LjBGzMPFt62psek1
+Ix8fICEteqyfoaSlqLZLKCEnLDM8U72moqauwW9ALCQqOOK+vLGpqrXnPUBTTk9d07avt+Is
+HSIrJyox4aagq6qmqrVCJyotKywsQrKprKytrrNlMS4wNDtDfLqwtLe5ubnSUlhQVFRHRTgm
+JzAtNkzls6qysauus807PEE0LiwyXcXBurOuqrXeZ1tEODE2XNLXz8a3rq+8wL/TTzQrJSAj
+Kiw0arepp6ysqrC/bz48PTUuLjdU5NvHtKustb/Axu1GOTpCRUVV0Lu2uLi0tr9fMCUjJCIg
+JjTpt62moqChq7rJaD0uKSsvLzI/b7+4uLCsrbC8zNF5SD48PUlUa8/Fvr3UQzo9OS8qKjI+
+Slbcu6yprK6vtb79Rj86NDMyOERm0cbFvrq8vL6/wcjX61xMST49RU5ffGhYZl9ORD48Pj5F
+Uv3Pwry4tri9xMzqUEY+PURNZ93NxsbJzNjt6e3u3tnS2NbkZFZMSElST1BXVlNMSUxOR0ZN
+beHPyMTAv7/Ezd3mb1NJSE1TatrNycXK2WNYWF1nbtbMyc7P3upmVUxFSUlIQkhNT1FaXV1y
+2MvM09HO1NHM1N7p62lWTWJeaufZzc3P4OtxXGFZVffhzs/X29zqX0xGRENAQEdPYXnj2dDO
+z9Tf59/ybn7n3uTc1tns62hpaHD49Nje1PjseXlrZvb62ufT5Ndvd1tPSENHSU9OWWnr3dLY
+2uLtbl5kYG9+4dnNzsnP293e7GRhWnhie+/h5+bc6eFz6PHu925dVldNTEtPWF1nfObl4+7u
+7OXta2t+dWNod+PZ183QytjW9PZ5VmRaennr7djV1dns5+3ra1tSW1hYVEtRT2Jgamvt5+zp
+6N/p9F9oXlxuZOTc1NDMytnc3eJfWVNmYP3r3szQztrrdlhcVFtZXGBoYWVeYFxsZmfu8914
++3l5b3xhXltw9e7j3c/OzdfZde14aGph7HHe79Xb5N3uZVNUVVhdam19bHNlX2tedGFsf//p
+9vh2e3p+bXRt7OLZ3d/f3tjp7PV7cXd0d3vs4+Lj5/pqZFpWV1deb3x1++/u7Pl88XJvbmv8
+eP388n91/3Vvfvbh3dra4uLzaGtqYGBpbPfu5Nzm4+ToaGRcWl9eZW10e97f+314bGhiaHl+
+9ffx8/N/dXx79Pr57Ont6OLq+Pl1dnx+/m9ZYdbM12tafdzuVUtVd+P4dPvrblhr5N57WGjf
+09nkfXn6/nt4Z2N2fXz87nhnZ+rY7G3+2dDfdnD77XVgWGVuYlpk9PRgXPre5HZo9dzdbWRz
+6/h6ZHT1/mdw8+/q4d7i1tfb62l372JadHhsbl9ZVmXm6F5r5dXce3rY1XtqXmpjWE9m+2hr
+8OLzdfvg6Wp85+tsfuPS1vHq3NHX3+rc193k7eXqZlJNSEI9PD0+Q0tT9tLRzsnFwsrWzMXP
+3tza1+1kZ93O1dTSx7y9yMbG3lk6LiwsKSctN0540Lyvra+0ub7RVkQ/Oj0/RFffzMC7uba2
+t7q+v8C/wdFURDYsJyUmKS44U827sq2tr7fE304+NjQ3Pktrz8K7urq8u7u+wsDBwr67vcxt
+SzUpJCIlJys3a8C1rq2tsbvWTz08OzlAUNjDvsG+vsTM39nNysW/vriys7xrQzYoIR8jKTA+
+47qxrK2wusp9UT02Oj5FXNHDvL7Bxtp07dzZyr+5t7ayr7PHVDwtIyAgJSw1S8+4sK6zucLc
+Wkk9PEFQctfHxsDJ3PleXW/ZzMO6s6+vsLC1x005LiUgHyQsM0PaurOvsbm/021TRT5ATPro
+3dHJydTsaune08rBvLe0trq8vsLlSkU3KSUlKC80PGnDua+vt7vG4FxFOzxFUnFld8/JzNDX
+zsjIx8XAvr/J0NPZ3994ZlhEODMvMTY2O0hs2MbDxcPEx8nP1+Xr6XRcTktLRkZPaOvPxcK9
+vL2/xsnJy9PZ1drrYUo/PTo4NTY7QElYZH7XysO/wcbQ5HVZSUFETFNf7dLKxb+9vb/AxcbF
+yc7P1ub7XUs/OTg5NTQ4PUVT+9jNysTAxc3fX1pZVlFOUnPf39rY08zFw8K/v76/xMrU3+9h
+VlBMRT89Pj48OzxDUGNsbXbo187O1dfb2dXY7l9UW3FsVkvmvcXHtbK5vMDMztjk+GRQSDwu
+KisyODtEZcm6trm8xMXK7EhAPT9JVFRYds7Bw8vNxr++y9fe1M7O3tjFvcHaXUw7LCYlKjU7
+TtW5sKywusLPbk09OTxEWObp+NjEyd/t2MTBz9TIv77Dz9DFwsbhS0k+KyMjKDQ/SteyrKuw
+vsrNXz45Oj5Pefvmy7/D4Vhi3NDY5Mq5tbm/wLu7yPRPSk43JCAmLTY4Q8Ssq660ubzNRDQ6
+Qz8+UP3Nv8jb6eXd13Rpy7u2t72/vL7GzuReTkMwJSMnLTM4Vrusr6+ws73uPjpAPjo/TVzN
+xtDRztHP1PXcxLy5ur++vsXL0OBpRTkvJSQqLCw82rqvra+trr1rTkI8ODk+SHPg2dLOzcvO
+0s/Jwr+9vr7DyM/a3N97WjsvMyskLDg1Ssa+tayytLLIVFZHOjw6N0hoY93LycC+xL+6ury9
+yMrJ3l9UTU1OSktbYkU4NTI7PzdCzr/HxMK8u81o8/ddTElUet/s4NXPzNDRy8rR2drV2PdX
+Tk9KR0hNWWJnb+jd3NXQ0dPQ2e9tZFtTTUpLTExLVW/s5uXm5+bj3tXT1NHZ29La+mBVU11b
+WGT869rX29jU0dPX5/xxZ2ZkX2BdVE9NT1Zgav7o3eHo7vLs8fd3a2pmX2Fia2v04+3q2dPR
+z9Lc3eTv7GRebWthbnlqd25ZXmFdY2967uXo4OPs7epuY3JlWltcXWhsbX3r6unh3dzZ2d7s
++vx2b3r5e3f6fXFza2NqeG9xfXj8+Xd+f3lwa2hnbHRwfPnv7//w39zc3N/k5fx+e/v/eP7+
+enp4amRXUldZXHLz7e/t7n5vbGtrePTu7ern4err6e7l4uvm3uDk7/x7b3d+eXHy7XZlXl5c
+WFxfX2N48ezo5uz5fXRyb290fPjr6fV5bW5+8GZgyMxuXt7QalXW2GBRbN1vU/TR91ZSTtDU
+Tn3Kz2Fc9etmTU1n2vdZbWpT18tmWerO4l9e29V4XG/X1XFd/dvc63p8+3ZlYGFsenFq/ep7
+ZnXk6l5d+H9gXXPm+mr429rheHLj3+Te6XL68+PI0zMsy6hrJDmopkkqV6y6Mi3stWgwRbOx
+SzXossA9OmHM3VtVSWPKw8zQz8nVak5ebmRKRlLp6XRf79LY9/z/Vk1VYPLxZe/bys5pYurj
+5l5ZXuTR51VrzdVoVuPXX1ZlfnhYWG/i+k151OHf19ze2+ze+ExO7uJORPDJYEzfwMtLStbH
+bkRVeNXdX/3TzPh8eGf6Ylv53e5YVvba83Tm3dnqXGJuWE7f1lNPfczaVVffzWdSWt7TcUvj
+xN9WZNfPaUtd3NhlV+zK2F5u1NZeS1jv6FJV5dnlbmr+5F9gc+9uXFl829nj7/vg2PJUU+LS
+aWh079pzXH3jfer1Ymbbe2RXY93Ybk9Y1c9rUnfl8/1ZcdTP32Rb19tbV/NkXlJa9+3x++T2
+8d1+WPrYy8xkTV7Q4U9H79Dhcm3nzdpcVn7YaEtP/+HuYv3X8GVo6+JkXVz63+twZODO4F1a
+X3Xd5HFreWz72vZq3cvNdVlx29feXnPX/lFPWO5lT+5oSENJ/Nh1Ymdh83Nc+dfOytf65cm/
+wc/X0Nbvf+rm1MzYdczEdTImJzE+Q01o7NniXXjezMPO8NfEv7u+wby+ydLi4srKz87Xyr2/
+VB8bKDpCPTrkuMtDNT/Twdps3MK2t7q8v8XG1vHjy7q5ws3Syb/BVyAVHj753E9mrq/bRTMy
+TdDS1dTAtLjE0NrX2GlPfL+yrbHCzsa+wEMdFRwyz8Z20rKxx0Y1PF9aRkrlvLW1vcXL0NpZ
+S3O9r6yvur+6s7oyGBMaL8rA58+7tb5PNTpS6Fs+O1W6q622ws3N41FdyLOsrrm8uba6OBoV
+HC/e01Blw7q7zkg7P1bqVT4+brywrrG6yPlQU9+8r7C4u7i0tvcmGh0pNjk1Pti5sbPEYk9P
+VFZJP0BTyLOsrLHHWkZL4sK+wL64sa+z3S4hHyEkKC5GyratrLTJdE1NSkA9P07Ot66rrr30
+RkNb3c7Du7Swsra9XS4gHB0iLDxtxbevr7S6v8z9Qzo8QlHqybu1tb7aXGDWxcPJ1NfOwbi7
+cDAnKCwvNTk+RFbZvrSvsr3L0s7ZXEVDTu/S3Xxz4MzEyeBiXv7UysjIze1XTU9RTUU6MzU+
+S1ZkdtvHwcXM0NXo7N/yZWRkdeHU0Nbo38zIysvJxcbXd3jrcU9CODMzNjo+R0/51szJzs/L
+zM7V5v7o3uHl5ufv8eTVzMnJysnEw8nS4WNLPjUyNDg+REdIS1d/0MfFxczV0s/a5dzo6uXv
+9/Lp2MrDvr2+vb3MeVhWTkQ9NzQ2Ojo4OD5O9NTKysa/ws3X19ja7l5bY+LOycS/u7q+w8rJ
+w8n4U1FOQzkvKysvNTo/TPDIvr3Cx8fJ09TR3+3f2tLLys3T3NzPycPAwr+9v9FWQj03MC0r
+LDI+S1deaOjRx8LFxsHBxMTIz9Ta62taVV/p0ca/v76+wMPG1lM/PTw7OTY0NDk9QEVMXtzH
+v76/wMDAv8PVXE9PUFplbPvcy8K/v8DBxcjJ0XNUTUpDPDYxMTY8QENHT3LMvbq6vL/HzdHc
+Z1RUUlRZZH3fzsjFxsfFxMTGyNH3WFFPRz44MzM3PEFJUWvXyMfGycnJztPbdVteZWtub37t
+4djOysnHw8DBwMHJ2/9eTUY9ODc3ODk8P0dZ6tDLzc7Kyc3R1eHv8O3ve/ns5t7Yz87OzMbE
+ys7P2OPxaFdMRkM/Pj0+Pj5AR01c9tzSzMjIx8jJzNXg8W9ucX/n2NLX2tze29nW0c/Rz8/Y
+7GxZSkA8ODg5PUFIT1hs6dXV1c7LysvO19bR0dXb6fj+fOvf29jUz87MzM/Y6HtrYVxPR0RB
+QUA/QkRGS01TYuXSysjLysrN0trh6vB9fvzw6uLc2tTS0NHU1NLU09Xf/V5RTEhCQEBBQ0hO
+U1hbYHfs39rb3NjV0tnd5Obg5ujm3tfTz9HU19nd5OPm73Bpdn52Y1hPSkdFRUZJTVRgdfXi
+2+Dj5ufg3tzb2tzZ19bU2Njd6O70+vPm6+7q5+Pm8fH/ZV1aUExKRUNGSkxRV1df69TOzMrL
+zc3Q1N3y+untbHHn29jOyca+vcLCvsXvQSshIiYnKjZ2sKSiqKywvlYzKio0PUTRr6qrssjw
+XkA3PWLCtLKxrKuvvkgmGBUYHSc7uJ6XmZ+t4y8hHR4sbq+noqWqslcuLDM6S+q/r662ubGy
+t7rIRC0jGRccJz7Dq6CcoLJSNCooLDNqr6Slrb3OdD4vMUri1+7OurCxsayss8pYNysjFhId
+Os64qJ6bplsqKispLuuqn6CsvFUvLDE2Vbi0u8LmW+fNu6qmrrnFXjIlGhIbPs/Ur6Cdpkon
+LTgtL9+on6S51kosKDTuv7rH53BBNE2voZ6irbvSRisnIRgaNd/Pt6mkp+YpKjMvMtWon6e/
+fFIuIi2/r7zO1dNcNkatn6GptcluQDMyMx4YJ0NO/ryqoK44LDw8Nka9p6e8T006KzO7pq7N
+UE08LjO7np2msbzWPi8xPCcZHzvd1s21o6lHKi87PkjQq6a1ZkY+NjLLoqjPRDkzMjXPn5qj
+ss1hSzQvQjAdHjJwzufMq6jdLi88TFbQsqy17UlAOUC1o6zVOyspLTu8npqgr9hIOzQ4VjEd
+IDjay9O+qalkKiw5RVHltay5XT9EQUy2pKzlOCooME+1npuhsPA4OUVTTikbHjHTxcKsoq0+
+JiczQk/FqKa2aEE+Pj+/pq5dNS0sOtqunp2qy0o2M0DN9CQdLDxLw7mtqL8uKTU7RNSzqq58
+Oz88Pe6wp6zgNS0vOt+toaKuxlo8QO26VBwYJjzWurKjot8nJjFGXcusp7dELzA9T9OoorRP
+LykuRsOmnqa31kY2PN3ALhseK1q5u6+gqkAnKDjXzb+ssNw8MTFGzLKorMo/Lis15bSnpbDI
+9Es8SMj8Jh0hLdWzuKqjvDMpK0TEvbmzwlk9MDRauKqtwnE+LSxCuqajrrzHWTk7bdAwHh8r
+Ybi6r6WuRCsrOs67u7m++zwuMUnCray9204xLT7HqqSsuMFlOTde0TUhIi1JxsO1p6xXLiw4
+2Ly9vbrDPiwuPb+str+6XzAwPM2qp62vvkw1NlL4OCgnMFjfdr2rr944LzzNvcTAvcVJLio1
+vq22u7p6ODA6xamrr6y5RzQ8Wj8qJS9O3Wp2uq6/Rjk888LEysK/ZzIqM82yvLy41zwyOc6r
+rLKur8s8L0lHLS0uLm26Z9Wwul9MNznKu83As8dBLS1SvMfJt79KMS9brqy3rqq7RzQ8TzAk
+KULd0dzMtbVbMzdKzsTZwa24Piw44szkzrm9TDNCxrvCuaupvj4/engrICY0SGXrv6uubjQy
+OmXfXr2ksTxC5WlcV+25xjo628PLzLuustd3SjrOwyofKjE6T+awo65PNzo8Oz3Sray8zMTU
+WEhbxsxJPlbW0My8trvMeEE/T1u/sS8PHqs0IaWWo9QoK1csIrqan9VUudgtM7mqvTk09uNE
+2qyru2hEZE4sVKmqMg8buTUguZmbzyIoU0g1zJ6bvzdqzFVD0a2xRjBL2FJMvK3OQN3JPzfr
+uK5LFxxbNSPrnaDuN0v/ST2/pKnoZcXmR0fKsbx63NZIR3jh5d9ZRMa9TPC95C4eJDMvM9qo
+p8RHW85YTb+rrMlXfupRXb63vc/o1GpBQU5HRdK4ucneRSciLjIuN9O4vsW8uM1S3L29z9bE
+z1puxsbNx8fQXl7k9WJKRlRzbFZ16T8xODY3QERP2djMvb7Cvr3DztHOzu5f8NjncNLMbH7t
+Xm71cepiP0ZVSkhW5OJPSE9NSFH62/Lzzc7Rw7y/z+rf6lhQZvBtX19bWXbU1tbM11ZFRVBy
+afLU2WlYdN/wX3PebE1b8tfX1MzVbk5Vb3hnaHZmT0xs0cjK0+ZXRkVVctzLydRsWWvj2Nzg
+291pVmz4/+rX22tPSFbveFpXWldOTF7WyMrP4mVUW37i2dPP3GRWfNLR2tzdfl1jZmxvYF5j
+XFJd6dzialRWT0pT7M7Hx8/nY1Rf4+Df1dv7ZV9pbnr16er7dXttXl9odG9peuTm6+14aV9Y
+WWnf1NLQ3HJcWnPi6H3p/VtVVWBocO/b2OL2c2NkdX7t6ubn4u15dvD7cWdnb2x97eju8OTs
+/nBrYmNfWmBu7vh97+rs+u3f3/L87Ot7+ubm+mdfa21lYmhpbvvu5ebt5uP1bWtsbWhn/+vt
+6+De6fn8dHN/e3jq5/V3cv5vXl5oa2RbXmz+6d/f8Pd2aWhueHvy/XJx8OTl3NXW4f713t3f
+3tzvXlVZXFhYYWlrY1tkZmBt6+v6b2xz7+Xf3ePv7uPe3NjX2+l+b2x+5+Z8X1RQT09bY2R3
++vXub33v9/Pm3uN/c/v56+Pn6/f04d7n6ODp9nJoX2x0c2teXmFeaf5qbe/y7WdWd85pYdrm
+/fJx5ub15+t/bnBu72jx2Wrd7Vj6fV3t6ml8eHZ8eHf4cHz+9+rv+mVsa2xuW01y1mjT0v7f
+ZV3hS1LNYePP9dfYbNXq7vRG/M9G9+pc31hl02XjamToeul7aXxk8+1+eHXodPl+cfvwb3xy
+b+Zl79pJ5u1M2f1p2t53e+v37eB6a173/F3r5+7Y5e5+/WZdbWNu/ONtfm/qbndkVdteYOfz
+3d7k0HTq7Wh2aWNmZ2xiavVt73z05WznfnjzfOn9/fpo62594fj14HN4cl1qcf96Ye9taN92
+/3lz6XrW6OPN5dHX99j9XmJVTEk/REI9TU1KfuLeycXEv72+wb/Ez8vG2dLM1NpTTlA2MTgw
+LTM5Ok7d7Mm7v7+6u8TJy+Tw3/dp5t/hy7+/v7e6ysfnQjYxLCsrKy83RFvbx7u6tLe5usPL
+zd/z7Gdic+/Zvr26s7zI3E43LSwrKSsyNkb2zL+4tra5t8TV1l1QWE1KXG7gxr+2r6+ttL3X
+SDoqJiknJS82PenDvbSvsrm6v15uXkFCS0RJadrJurGtraysu8diNi0nIyYoKTY/YMW4s6+2
+tsbS7EJAQz1IYl7dx7y2sK2rra2wy9c7KywiHicrKT541Lyur6+2t9xGVjsvP0U9/cfLvrKu
+raypra+wy1M9KyckISMrLD5yyrqxsa+5xNBDPz8wOFVL5Ly8ubGvrq+trrS4vVZCMCkkIyIp
+Ljhez7y3sbS3zeFPOTg+MUHb48OytLGvrq+vsbC5vtJKOSooJCIjLzI7+8C/t7K3wfDtSTc+
+STZew82+sLK0tK+xt7SxvcLdQDUpKCUlJjEzPubGwLu5vMX66U04Qlo68Lu/u7Gytrmys7q4
+tb7EekM8KCgoJCQwOD5Zyby+uri/YPZfQDnoTlm7trq2s7W9vri8x727yNlWQi0rKCYnKjU7
+StO5vre8vc/5X2I4StlOyrS5u7m8vdHOusPLvLm+dmxIKysnJiQsM0VLybe4uba3y+dw3zpM
+y/DZtrzCzM7FdXjEwMC4tbDAbUkvKCEhIyYuRWvFsa+utLW+3E75PTnc5XDBvcPL4NHdcMm8
+ubWyra/iVzYkHx0eISk5zr6uqKess7zOSjlPOzjvyezGvMHOaN/MX9e6trWxr67NQjUgHRwe
+Ii5OuqyooaWsvMxfPC83PDo+yL3PwbzE8VV30fXct6+ytK+x/jYxJB0eIi45c6+mq6eqs9dE
+PjwvMEZPS2G4udvMvtdYUP7M29G2sbe5vbjMODgwIiQnMUNzvqmttrO/cj83PD8zP+DdXtC2
+tMzeyO9GPkrp1tm9ra60sayuUjw7IR8hJTZKVLCsvLW95005OFE8O97Ayse3r8RGTVc9OELV
+uLexqKasr6+z2TArKRwcICw6SMusrby9wstKOUnbTUPdur9jzLLKPUhoak5JvqyzuK6rsMnG
+tl8rLiodHiAqT0zTqKe5uL7Oajs56U46+L61usPJudcvNFJYSGC8qay6s6y46U/RXystMiIk
+Jir30mCuqLu8zGvKSjNo70j0xbmww0x94zctQs/K0sKtq7rOvbnYTGq80zMxLSUmJC/rzr+r
+rri9dUpRODleVujCwLS6Zn7qMy45SOHNvqqnsLO6wMlneb7cQFM7IyYrKTlARryy3cG92dVh
+Qc/JR/nCzMVdPu5gLzf+x7y7sKatwcPHy9lax7XNT0k7KB4eJC81PcKpq7a3tLlfNj39Qzpa
+w7nDYFjJ+zY8zbe9w7aqr87awL/fS3LE1kk2LCwiHCNDRF64qqarw8S+TDE6TVpWT72vvexi
+4W83NNm6wsS5rbLX4b/NUkfpyepOUT8zJR8nLj1Oy6+lrrm9wXw+MkrrRk3HvLzTU2ZLPUZF
+zbW6urGxtMdr9+xTUPPN2k9EPiogJis4TfyzqK+8u8HaSThAeUVBbsC8zlvSxVg8Ur65ycq1
+sMhf+NDyREfLvdFo3fk7KCIpMi031LCttbuus+s/P0JANDjPucjWxrfEPz7NxeVlyrS65NO8
+xl5T48zoUmZeRDouKi0uOU94wa6zu7jBz2E5PEs9QG/Kv87sw8dcTvTPyefivbzPzcvJz2Rq
+zNfnzNhQPi8pKi01QljJtLa7vL7MYUBJVkhFWO7O417YyP1g58vByNXFws/V1NPLyc7OzszI
+60k9OTAqLDZCR03dwcHKzMfE03ReX1xPTVFMT2N608W/uLa6vsXV3/FcT01TWllf9eHh6N/Z
+2+b9b2VJPTw8Ojk8SF537+Hb08zMzdXa1dbv3s7O2ORydvfh0szPx8PM0M/Q19/r5uLwXE9K
+Pzc0NTc4O0Vb49jZ0s/U4N/c5efa09Ta3dTX7Xjq3tHOzMO+vsLKysjN3OxrXE9HQT04MzM1
+NzpCWOzZz83Nz9Xf1954cdnP19nQz9ns7+fa1c/Mx8LEys7Pz8/b7PlvVElBOjc2NjY5PEZX
+7NLLy8nKz93l6mtgbv378dza293XzcnFwsLDw8bJz9jY3u91XVhQSEI8ODc3ODs9RFb34dfP
+y8zV4OTxc23o4Onk1M/Oz87Jx8XDxsrLztLU1dzj6e5kVU5FPzw5Nzc6PT9FT2nhz8vIyM3U
+3m5cYXV6dfHa0s7MyMXExMHBxMjM0NPU4O3udVpORUE+Ozs7Oz1AQ0hSY+/d1M/P1NfZ73F9
+5ed899nPzszLxsbJysrLy87W1dfi5+11XlFNSUM+Pj08PUBER05f69jSztDU1Nv9X2Z4bGL5
+2dbTzsrJxsPGycrN0dTU2d7j531fVk5JPz0+PTw+QERKVmf23dLQ0NHW5nf3+Wdmbuvb1MzI
+xsPBwsTDxsvU2dvk7/puWU9JRUA9Ojs7PD5BSldn9+Pb1M/N0dvo29jtanzk3dfT38rEyMG9
+uru/xcbO521rXkY8NzU1NDA1P0pSWeDLw8rOztXk5/9TUmF+YlJmzsrPx764u8HAvcDN3d3O
+2l5acPxTNi4uMy8sLkHm2NO/srC6yszO30o6PVRZTFzOvsfkzby7xtLGvMDW7NfJ4lFcfV1L
+Qk1ULiowMz9DSMG0vr/Dvr1+Q0ZJU0pE/MvMy9PLx9/x08rFzNHCw8/i3tDZYE5YVktGS0tN
+OTFDTUdJasjDfG/R0lxCS+HiZebLwM951MfP59zEvcvTyMLKcljt/E9GS2BfUUxOXVRDQ09d
+UFBkfHJeYO56W2H4bmt03MvP2c7JytLczsfM0Nvf42dq83JiWFZsYmVvYF5ZUV5lXF7t3flW
+WHt6W1NddGNTT23Y2d/c1NTkf+jj8frw6X1u6u3x737k3/D6f9/Y6Obb3OR6et7f/m5hXVpP
+UVhVUVFZYV1geO/vbGfs6vzu6+XscnDt5e/x5tnY5OTX0tXa3Nve8PLtfG5lW1tZUlJZWllV
+WmFfaWt7/Pjr4Ojp3+bl6vPs7vTw7+3f3+Xm6OHjfmttcXp5dHr3+XVvcWZobG1jXWJkXWVo
+evV37u397OXp6ff+9PXy+f3v6ezu9Pbs5+PnfG367uft7v9rbHNoWlteZmhtbXJ1b/18a2Rk
+ZW386uXm4+nu7fDv5d7d3+3r4uPo7/P5dnJsamZgYGBja2psdXlyefnu9mxnamZgXmBoffz2
+497l5uPj5+vd3ODo+PTt9X789/Ht+W9ja3FqZ2FhbHhsa21vdG9tcXh2a2tqZHnv93p86u71
+6OLf3dzb3d/l7erqdmv+83hlaXVxbmtgYGdrbGxvdHBx9/53fvPtenz5//14/enq8/Hs7fHy
++n55dPTf5Pz793Jrann0em159/977/5oamds+O1+cnZvZGNx7u/v7Px68vDq5e/r7urueu7q
+5ubr7e75cmVeZnVnaXNrfHRqbm1lYGz+cHVzeez/ePLq4+f0+eXl5uTj4On+dPPs+Pnv7/L/
+bGtuZFpfbHdbXHr6b2Ft9H1t/uvm6+jf7Hhodfbs7PXt9Pbn3+rs6fFqa2t1bmbx3up3Y2z2
+aF1662xgZf1zXFt863hu/ev8a//f6/Tp3eR47tvo5N7p8ef2eWtndu91ZHfucV5qb2BqdmRc
+XWl9cmlv7uXu39/49N/ucXB8+O7w7u3u7PPy8Pn8cnD5fezt/f7v9PFxbHNpY/36aXJ1bW1j
+Zm9sYmFv73966t3uePbv6HNq6uro3fvp4nd73/Nv7eXzbnj8/Gttc/H3bHD5aVtcd3xcYO9+
+Xl13fW128t3h/PDv7+996d1+7djm7/j69n9jYHZuYHPu+3P/9+b0a/XldWlse21kavT5amv4
+cWj79v789m149/N49+vl325y4ux7ferwaHh5d2xq+m1ndXnv/Gv++H50d+36anvl4/vp7Gz9
+f3X6e3n9ZV1t8ff55uR+au3oY1j05HRdbeZnXuzr8PLg4/VmauN0Wnbh+Wz57fnv/G1lXWxv
+df90d2//5vDu6Ofh83T38/NobG918H/66/34eO57ZXtv8O9v/O95a2N47XNoaXpfWmxyc3f5
+8vtp9OTf4ODv3+doeurv9n567PN0fvR0b2hte25s+HVranf5aWNp6+dv7+np72ht5Gpl7P1o
+Xujl8vb82+FfZubuZPX5b/lr5ulbX+rtZF/23u9naeDjbWre2mVo7fJ8f+7u/n5xdv1haONn
+Wnb8+HJtZ2z0dXH89+Dpf3r47u1x+Nfb9mfw7mp5ZmntZGLs4G5rfvl6Zv34cnVybnL0/O3s
+/+jobmlt7OPxbPbZ6Xb3c2xjYnb++m/963pda/RuYH/meGj27OZq+eRxZHHl9mx86eH+8d7/
+bvzx/P16au3l+nZ9bfL2XWzsanDvYXLtb2xv8e5fcH5s93L06mVc5tz76+TmeWh39H5sfNz/
+b/DqfXzg9G1r7u1jXPng/F5h+H1lauje83Fy9+9+Z27vdG/98vn8eXXsfm7p7mL132hffHF1
++/vf5+z57+v05up9X3P2Xlrt+mJ4au/pXXHmZG/qdPJtZXnk3uHt4+f44ft54/bxam9uXf/s
+bft5X/j/ZHhlcW9nbHr8dX76Zufi7edi8+vu7v7t6PJ16drq7/Te6V1o6m1dW19rWmh7anfq
+4ezscf3wZ2T9/vHi4+3/4uLj7ffvfG1bWXNsdm5o+fZga+x0Xu7dfOje7Hx37+798+vr+W7i
+6/tuc/FuZl7z7nxhZ+VrZ+r68+75e/xub2Zo+uzr5/fx6Wz29nrubWLy92B77urz9vbu91x5
+8WZpaXXx6Onu83jm7Wbu63N9dXBl9fthbvLjbn7l7PV89v5+/vbt6O13fu5pZn1jeN/t92xv
+aV9ibfz+dXBoa3N07dzmZPLm/urs5Nroe+nif2/n621oeHJoY3L3735pdvdtaHR7aXbhfWt8
+c/Lz5+r333xm+/bt+3Vq62pq6m5ydWh942dmdX/36+3p6frq+Hjs3vFq7npva2nyem9+bWxg
+WfL3X23i73Xp8fHp8vHl7Hby4OV6//Hrc2RvfH1se+1hVXfsemT24+9++ur3fO/l+mtme/Rv
+bvXtbWFx+Gdy9ebq8/Lv3HVm8en0am9+c3Vs6OX+eunjb2h9clxhdW76+vDv73dy7fL2dfTp
++Gj24+/5//vp7vP/eu5ybnZhWmxwZ2l6/HX6bW7uefjh8Hzt8/H37u396uT98urp8/788vd6
+fWJgaF9tamnq5nf+cm36+fHw+vp9/vj77fHm8XBscWxo9fFue/Nyam/s7Of5Znf7e2x4+fTu
+8PLn8u/k5nJy9vh7bvnv8G1se3Npce51ZWZ38WxhbXliY+x5aP7y9Xd8593o8uvqfe7l83jy
+6/rr8PTx+HNqePVzbmlsfHJx/v5gY/ptX15pdPL1/eTo8/ft73vu4uH7evB8ennv5/J1d/xz
+/vX9eGt0/WxpbP72b2t1eG18f3RtbHB6bXPt6/j9925y/vHt7eXi5O1y+O/8f/vy9Pbw8H5+
++fN4Z2NjaXBoYm1oZ3Nvam1/8fF9fPLr6+7x7Ozs5vN19evt9fh6+Ptud3l2eP13b3h0ev18
+bm/+cmpreXpvc29teP18e3d77vl6e3j15uLl6uvp7O/7bXP8fXF3efp2cvn5+31+fXv/dnl0
+ZWl2dm5rb/r/efj0emxvdXP98+bh8m91d3F87ezt7PF+dX368evm5ObzcWhneHVpY2/3ff53
+b25u9vltbfp/amhqc3X37PTy9O7tfP95+/j3+/7t6Or+df947+zyfXf9dmlt+O/v7fF6a2xt
+ZmJo/e7u8vH4enL6fnZ6fO/t8fLy+nFpde/0dnx7bmhscW9vdv5wa297eXf9+/X47+br7enq
+7/b39ez6/fbs6O76cWlmbW1laHX++3t39Ovx9Pp8fn76+XVreHx8dGdmZmRhbPr57+3u/H/4
+9vT07u3t6+7x7/L6ff/88/P2e29/+u/0enf99Pbv8HpxcnNmaGpscXf89fP9/nV8f3/0/nz9
+/2lncnn/+fJ6fuzs6ujq8Ovp/mhncP3u9H789np89/z293n/f3p2fHRw/3tvbHBvbPPo82tu
+6O57dfPq+G1z93Z58vp8cnD2/WtmdPv++u7s6fNoc/X77Ofn6e/7bm9+cXz4bW9sYWJv+Xv3
+4+t+a2Nv/Xn56Xxvcnvv+/7f2+5s/+xvdup6bvt//H52cGxoZml7e2Zpcnrv7O/2+frv4Ort
+7fDzc2Zq9ux1Zm1pY2ht+/rz7vJ0ZXTw7+jm7vPu8np+cW19eHJz/e7y5t37bvf8deDZ59zd
+Z2ReU1VneV9UXmZSTF3n4Pfr1NltY/7r5+re2OF7feDd5ebg4PPu19DX18/O0uJvXVBFPzo2
+NEB07M/Gx9RtVmj5/uLUztLi7+Hp5+Pu8mZdXl5i3tTV2d/jc2Be+OPf3t7Z4f5s79tv5MnP
+7k9QQyUr81Z0v7a5zD5F5FdLXc7C2l7e29bZ4dPcW09WZ9TNyL/G0eNuZ2/l0crMy8nQZiwg
+Jy5BbcCtqb5RR0hEOz/Pvc7h38THYUzz5kQ+RdW+xL62u81dTk5ESOzIv72+wtBoV158WT86
+NisnMljbzbqxttlOT2dfTl/W0ubv1sbM2NnnVkNCUO3MwL69wtltU1dj/tPLxMPEvr/WVDQu
+KyQmNVrXv7WvuND86F1GPkdj/nnVv77I2m9vUkdPddbGvru8wsjP5GtYbN/p3svNz9JTPTwv
+JSkzNjpbwLS1vru4wORdae9cSUdNaG171MjGycrK03BeZmxlafTb1dzf2Nvt7N/Y0tnu6utf
+UE1KPzo5PD9FTWHr2dDRz9DX3ejs4+Lh299+fPfw493X087Pz83R1tPS1dPPzM3T6n1uT0E9
+Pj45ODw/QkZHTmjn2czFxsvPz87R3ODh5v1fY/ne083Jw8PEx83a4OPe3+Tf5PFfT0tGQT06
+OTs9P0NJUGHi19DKw8PIy8/U4PR2c15YZfDh1s7QzMnJx8XDxcrO1eR3YFldW05FQkI+Nzg9
+Pz9BTF5y993NyMjJysvS3O7v397e29XX3OLc2NDKxcHDx8zW+G9wY1xiW0xEQT45Nzk8PEJP
+YvvYy8nL0NbV197m5uHf5OLa19bX1tDPzsvFxsjJys7X33doX1VKQj89Ozo7Pj8/RU1VYO7a
+zsnIys7O0d3q5+30697Y0s3Ix8jKzc/S2t7b29vY3OpwWUlDPzo3ODo6PkhRXe/c1c/NzM/R
+0tff7unk4+ro4t7c5vtnw7i9t7C1wMXNbEQ7NDEuLi40O0BP+8vFwcnIxMfqXFBSTEVKcPpn
+4svHycnEurq+vbi7vsG/vcRlQTYpICMpKSlBw7W3sauqu2VYakMwLTx1Y1/AsLbP6MzRTUjJ
+tLW3rquwwsbGRyomIh0fJixBu7Ctq6y0w0s8Q0s9OWPDw8S9tr1aQko/OT/Msa6rpqOptcTZ
+RiQZGBwfJja0o6WurrXVMCs6bE8/1qqx89u3uT4wS3A9Nl6xr7aro6i4x8HDWDgtIhgaHytI
+wqifp7zNUTwvMU7Nzse9r8j6xsBLOTZDSEL4sKamqKervvtZYVc0JxsYKCcsbaqmqNPWwkcu
+OV6/z0/KrchB2bfKPzVJYj9Bv6Wkqaalrt5Pbi4lKBobNTM7tK+qrFpH2zg1PVW9wEjLrN1L
+x7a5UDZRYTY+u6GiqqSkv1s5KSkjFxxMPj62rq6wPj/PPzVJcMHiScW+ybu5s7biTEA1Pd6z
+pqajn6pJKSAiIBghzcJexLWvyjE1zt5DR+/Jdjs7wa6zubW0yT867bewsqulqbxLLCMfGhw9
+y/HCs7rCSDjvylNc5e5cMi1SvrmxsLLDOjBNu6qmopyexisgHhkVH+mvsa6rr+IsKD5qZtO+
+uNoxMW2+u7u5uW4uM9ayraidmqpsLiIZEBcwzremoqOxOCkxNztM37m9ZGDNvbe7u7l+Oj5r
+xbmrn6OzRSUbFRkhNdOpoaGqzT8zLS03WtfQtrG+w8fIxs9rxLnEv7SrrL7Mvz0aDhpLJh/G
+npyrS8irSyEnM0JbVrmfn7W9uclIOV6yq7q9r7TG0VMxKBwYHCIpPbyrqK65v8dGKy9fy8++
+ramvv7+7yNa/r7LcTl/15dHHQyssKh8eJzZMZ+W6r7zfzsLJzse9ucTh3M/P1827uLm+50lH
+UU5Pd8e+az9CMigoKzJGTVu/u9XOv769w8e/wsvc9+tdZ9fOyt94e/P9U13rzs7dX0hEOjU5
+SFzW1dXYalVbb+vRxLy+xtHqT0FBTWb/2sa/xttQTk1JTFxYTk9LVu/dzMXJ22nk12hf1MjK
+xMncflpCOz9EUfnSxcHO+VpOSEFETmBjYH7h2NjVz8/M0+nZ1dfPxcHH0m9MPzw8P0dc3czJ
+zOxZS0dJTFVu3uppY+PPz87Kxs3c++7b1dfUzNXzX05GQkRMX3hmd+duVUxRWl5aauzt3dXT
+ysjMycTJ3PdiXm9xceje/lpRTktOU1xu8+jwbV1ZV1pfY19ad/Lr5NPGw8LCx9b7/Htja33p
+2+VdTkpIR0dMWPPp3d7y9G1sZGBha2tx6dfMycTKy9HpZWJYXvjq1M7YeVlKRD89R1Zv69PN
+2epxdGxkYfXa29bNzM7N2ej0YlNVXV9c/tnW1OlqVE5ISEpSat7Uzc/uaP13bGl82M7RzszT
+5eZtUE5WTVRabPzh4N3yaV5XWFRj8+rO0d7u73lkX2Lm29LP0s/b6N52ZF9dVmFZVVx2YmRi
+W25sa2bx6eTY3ud4dW5cXe7v2c/O19bZ3N7vaX39anNvaWd2WU9UWU1cXmrk6uTd7ObvaFhg
+afXr//XUy9fs2Nvg6e9iZuvlZ2vu+21fbGNRT1vt3H3v6OV1aXBgWF5xbfjc2t/j6ebq8Pxp
+aXJpYWdsdOvf5/Hw9XdudX707enf5fXu6fP+ZWL9f3r9/nh0ef10a2diX1lbaf3o5ubp8n5x
+amFsfOzf39vU1dXZ4vR1Y15qa3T07Ph4enBcVFJcX2NtdvTo5en17e78//7y6eTh393f6O/5
+b25kZGtwent/eftxYmFfWVpmde7g3tnb6/J2YmJ5fO3t7N7d5H3/emZeZGhtcnrv5+fv+3Vx
+ef9ye+7q6/t8cvvycF5n+/Pxf/XY0vtMS+DM/UlO3c/+V3jP1mhVXvB+XWLr293r+uTb7lpi
+9G1gb+rc3+3t6X7u615Ybet4XGDg6Flm5XpZZ9PXbGrY0upfYn5sTFLn4Wr408nQVEjSxUk5
+973dQU/FxVtK9snfRlPV6ldh39Pfbe3P0vtn7+9eVFph+nx8afDg725zdWBOXuTjaGDr0Nl3
+9t7ja1tr3O5x7/Zv1uNTatbdXFTkw+tMaXdxZU5a3+5ZVfXZ7GLv3ebs79jW0s/f2tHh9vRh
+UEc/PTk6Pj5Hd9jOyMS/vr7Bxb+9wcXEwL++vru/NyQtLB4gKjX+aVavp7O9xs/JZj9K5c/b
+f8y5uLu6sayutLOx/SQeJSMdHilXxvfIraqx0j9S70I8SNe1tsvFurm3vLqtrK6ur8MqGx4i
+HBwmTrS1v62mrsBSNjY8ODxWyLKzxc/Jxb25tayqq6urvisZGyMgHyM7uq+7uK+vtMw/NDlI
+UkthyLm3xvDdwrOrq6yrq6yuZSIaHSYnISQ4zbe5xsS8vMdONDM/UVtf7cGxsrvCu62oqK61
+uLq+6TIlISYvLywwPU/n5/Xe3M/Ezm5dbG1cXX3c2dDRzbuysbW9xb+9wL/D1fxhTjwvKyop
+KCowQHve7Mu9urvOce7SyMfMy8W+urzCw8G9uLi6vMp1Sjw0LikmJiguO0NKUXDKvr6/w8rJ
+wL3Azc/Fvbu8v8G9ubm+w7/JYkQ7NjEqJSQoLTc9QlHfwbi1usHCwMHFz97bzcfGyMnEvrq3
+ur6/vsXyRTgvLCgmJigsNkNLX869t7S5vr28ws3W3dfQzczS1MzBu7i4ub7ExdVJODMwLCgn
+Ki4yOkBP3ca8t7W1t7m8ytrh6v1hYPXXx727urm6vsXKz18/OjYyLysoKy41P1Hhw7qzr7O5
+wtTvVkxOVWvb0MjDwL2+v76/w8bGxNFYPzc3LSYpKy0/XHbKvruzusbN9vt3TVFj7c7a5cjD
+y8TDs6+0srO3u1IuJyMoJiIoNUbGwcGvra+vz2/WPjc7NEh4SP3IybS0ua6tra27xsvWxVst
+KCQpLSMjNk7Pws2ypquzyVzUQSstMUPeWfm5tLG0v7avtbe9vrS7v8o7KiUiIB4gLEjkv7Kq
+pqm0yWlYOCgqNU/b2M61rrW5vbuzuLm2tbKwtsJGKR8hHx0eKDnqw72tqKqwwllMPysqNUva
+zM22rK+0uLivr7e6urSyvMRjLyUhIR8fISs7Ws68rqqssb/sTjktLjlK38u9r6uqrLO0tLvA
+y9PLytPVWzouKSopJiYqMUP0zb+4tLK3yPRQTVRVY+7Ovru7vcfNz+Z4/9nCvL2+wb++10xB
+PjkvKyosLzY8QU95187IxMDCw8XJzMrKzNLX2OTy5NjKv728urq4ub7OWUA4LyspKCosMDlE
+VPfKvri5vb/BwcjO1Nnc3uF3c+XXzsrFwby6ury9vclmPzgyLSopKi0zOj9O4ca9u7y+vr/E
+ztrc5XhkX2R/3dXKwb26uLe4ury+zltBOTItKSkqLTM7Q1ffyMC9vb/DyM/W3vb+/e/j5d7R
+y8a/u7i3tri9w83aWD01MzAtLC0wNT5GTmbhzMPBxcnIxMfUdGd2eW5969jIwb26t7S1ur/L
+5H1kRzo4Ojc0MjEzOT9BSFvcycTDxMTDxtRsV2T8+2553cvCvry7uri7wtHg4t5vRj8+OjQy
+MDE1PD9FVOzJvr2/wsTHzuZdXmvw7X3v2cjAvb6+vL/Hz9nb2NtcQT48NTAuLjI7QEVS78u/
+vL3Bw8PJ1m5dY2l7++zczcfGxsfDvr7BxsjIzOZMPzw1MC8uLzU9RlBv28rDwcXMy8/V3nxy
+/Ovk4d7VzMrHw8C9vLy+xcjK2VhAOjYwLi0uMDpCTGTfzcW/vsTGyM/a+nNlZHb66+DPyMS/
+vLq6vL3Bx87mUz86NTAvLi8zOkJLXOvUyMC9wMbK0918Z2hnZWx4693Pxb+8uri4ub2/xNJj
+Qzo1MC4uLzE2PUZPZ9rJwL7Ey8/Z3vFeUWzf8e7Xyr+6urq3trW4vL/J+kg3Ly0rKSksMDhF
+Y9HBvLq6ucDT3HFWTk1OWnb71MjDvbm1tLOztLi9w9tPPTEuKCUoKSovPk3dv7+4tLe2wc7X
+Xk1WUURa73PMyMa8uLa0sbO1tr3E4FI4MC0iKCojKzk4Qr/Dx661ubjGyvJaYldJYuhL1sfj
+v7u7tLGysbK4vMt9TzAwLCEqKCQtOzdMwdK8rre0ucDGaWVuTUxdVlfQ29a+v7u3tbSzsri8
+xthMNjkoIiwkIjA0MnnB0rWuuLK2wMzg6lpOWFRQavj7zcfGurq5s7S2uL3K2VE0NykkKyQl
+MTQ45cbQtK+4sbi+xt54V05PT09ZbPnPxb+6ubSytLO2u8/dWy8zLCEnJyMsPDlcvMO3rbGz
+u73NaX1VRExeTV3X786/wbu3tLOxsbi8x+JBNDIjJCkhJjExPMrEx62vuK+4yMzgSlJYRlxo
+YNrQyMG8ubaysbK2ur/haz4uLiUiKCQlNDs9yLrCrq62sbnG3XpRS0xPV1p83t7Fwb62t7Sx
+srW5vc94PS4vIyIpIyQ2Oj3CusWurLezuMfvd1xJTE9SUWTh6Mi/vre0s7Kyt7q9x3ZKMi8q
+ISknIy5CNnW3yLqrtrq1ydJyUkxNSFpeT9TP1b26u7Ows7Gzuby/70U+LSsnIycoKDJBRs67
+u7Sutbe8y9tWTU5LSV92Z9jLyL+7t7S0tLW2vcPKVD86LSooJicrLTRKYs25t7WxtrvBzf5Z
+TUpWTk/65NzLwby4ubi1u8C+0OniTkE+My4yLy44NztSYN7Ix8q/vsjI0uzsXFNYU1VsdW3c
+0svCwLy6urm8v8XM0d5tST49ODQxMTU3PEBHU3PazsvKzMzU5Xdnb3/s6eXa0crFw7+9vLy+
+wMTHzM7XeU5EPTkzMDEyNTk/SVb12M/HycvJz9nj/njw8u/a19DKyMbDwsPCw8PFx8nHy+5S
+RD05My8uLzE3PUFRe9nLxMfGxszP1uTh2tnU1NbVz87LyMO/vr/AwcbJzNZ2T0Q9NzAtLS4v
+NDxHUG3d0MjCxsTDyczLzdLT09bZ3tzXz8rGwcDAvrzAxcTK4VhDOzUvLSwsLTM5QEtZ9szB
+v8DCwcTFxcnR3mxa9MzH0N3Nvbi8w8G+vb/Fzf9IOjAtLS0rLC42SGB438u+ubm+yc7S1dne
+82xwf+nZzczMyL+8vLy+vsDIzcvPXDwvKystLSwsMT5Z2Mi/vbu6vcTM2vJqZ2pu/vnt4NDH
+v7y7uru8vL/HzMzM029DNS4qKSkqLC84Q1/Nvbm3ubu9x9jtdGZkYl5q79zOxsC8ubm4uLzA
+xs7X2d7mX0AxKicoKSouNj9P5sa6tLOztr7O8mhiXl1bUFJg9NXFvLe1tre4u8HKz9rq6+9W
+PTEqJyYoKy86RGPSxLy3srK2vtFsU09VWFhbbPvmz7+3srGytbm9xc3X63/3bEg2LCgkJCcq
+LzpKdsy9t7Cur7K3x+pbUVNXVlJVV2rZw7izs7O0ub7G0er0fXRtTDktJyMkKCw0QlndysG6
+s6+ur7jKdU1GSk5TWlRQVPrJuK+urrK4vsjX4ubo5W9EMikkIiQoLDVCXdjFvbWura2vuMxr
+SkFDSEpLTFJe38O3r62usbe+y9fh4+XjfEs2KiMhIyYrMkBW2cK8ta6rq621w/VIPTs+QUVJ
+TmXOvrWurKyus7zL3uru+vdlQjAnIB8hJis2SvXJvLavq6mqrrjVTj05Oz9ERUhOas28sqyr
+rK+2vMbX7372/VY6KiEeHiEoLz5dzb21rqmnp6uyxVo9NjU5PD9CR1Ldv7Otq6uts7rAytLb
+5PRgQC4kHh0eIyo1S9m+sqympKSpscNcPDQzNTc7PURc0byxrKurrbK5vsfV6OrvZEUxJh4c
+HB8nL0TbvK+poqChp7DJTTkwLzI2O0JS5cC2r6yrrK+0ub7J3O/j2uNKLyQcGRoeJjJO1rus
+pJ+eoKq7XDYtLC42P0le0r+2sK+vsbS3ubzCzNjQxsPQRyweGBcaHys/3Lqtpp+cnaW6Sy4n
+Jio2SW3SvbOur7K4vsfKxcDDyszBubSyvEUkGRMUGR8vabyvpp6am6CySikgHyQvTcy4rqqp
+q7C+6U9Ob87Bv766sq2qrc4tGhEPExwsdLetpZ6bmp6vQiQbGyEvbbesp6WlqbXpPjIyP+q+
+tK+tq6mnqLE/HhENDhUjT6+mop6cm56tRCEYFh0t1qmfn5+jrL5FLikqNPq1qaWlpaeoq7Fr
+IxQNDBAeQK2fnp+go6eyRyQZFxwuv6Kbm5+ot+Q7KyUmL2axpZ+foqaqr79iLBsUDxEaLdKm
+nqCjqLC+TiwfHB8v0aecnJ6ovlg2KiYoMWC0pZ+en6auuNxNOiUaFRIXJUytn5+msMRxQTMp
+IyUy1amdnaGtykQ3LywuNla7qaKfn6astshgPCofGhUXHy++pJ+jrMdRPTIsKSoyda+in6Ks
+yE45MDI0OEzJr6eioaOpscLhTi8iHBUVHi2/op6kr985NDAtLjE70q2lpKi11V9HPTw5ND/h
+uqumo6OmrLjKUTYnHRcTGynOo5yerNgxKywsMThHxq2kpqu7Zl1OTEc8NTdZwKykoqKmrrvQ
+WUApHRcRGSfDnpqdr14tJiwtNj5RvKukqbTFUV/f3ftBMy9Cya6ioaOnr7zXd0opHhYPGCi7
+m5ectT8mICovOUr9u6mkrbzeP13QzNBGMy9CwqygoKSqt8vUz2QyIRYOFie4mJWctTMhHykx
+PVnbu6umr8xjP2e+wMtHLy0/vaefn6auv9ro2OYzIxgPFym5mJacui4gHio5TN3OvLCqs2dN
+P1+4uchMMC5BvKaen6i47lbexbxPJhcOFCG1l5SZuC4fHio9XOvYwLOsuGJCP/m4tMZILy1C
+u6Sen6m8bU7bvrb+KBQMESKpko+ZyyIaHi1pxc3p3Lu2wmdRWsWwt904KS5ZsJ+doa7KVE/L
+u7fTJhMMESSoj5CZ0R4YHS/Xsr3q3NLL00ZS1LqssnkwKCtzq56corRkQErJr63OLRcMDyC3
+k4+ZvCYYGy1wsq/IbeDY70xH2birrug0JilPrJ2cobVQPEDHray7NRwNDhxKmJCWqy4aGClW
+sqq+aVBadEtux7eqruAzJiY/rp6boLZKN0DGraq3PyISDRgso5GUnk4eFiFCuaawbD9AVk1e
+yrysqsA7JiEvuJ+Zna9LMzzTramyTSccERYnUJ2YnbI1Hx0yXq+qyU9BQj9cubisqchBKCAq
+06Wcm6njPDhvtKqvVigfFhcrQKmboLFEKB8xZb6txU49Pj5DuLKtpb8/KiAnbaiem6bMTD57
+tKms2SsdFxUkQ6ybn64/KSErWsqyvFpHREQ52bCtoq1RLSAkQ6qenKPFUkXwta2v3i4hHRce
+NNShn6nuMiUoUdC3tPBKUFNCTbyyp6bLNCMhL7egnqC5WEpjuq+wyTYmIRwcKkSuoKS4Pyok
+NW6/tc9bbFtQP1+7raWySykiKmuon56qyWhSyLSvsGgvJRwXHi/CoJ+tWy4kLVLVvsb938l1
+PTvnsqWmxzElJzq4paCjsMPm38LBv81JNSUaGiI9rKOqvUk1NklIU1xSyrnJTDlAvqqotkYs
+KzNUtqmkp6270tH37sPxPzMfGh8v16mqvM5YQEVCNjpIfLi2+z8/3bOstOo+NTc9dLaooqWt
+v9PvUf5xVkwvHhshLcasr7TE50tMNS82PsextcxfT8q0u8RmQT44OFi3pqCirLvhQT5ay9JI
+NyIcJClHv7yxr7PPZTQrMjVlv767u8bEvdrgWz4+Nz7itaiho6ux0k0/OlH7Tz0oHyUpNU7o
+v7Csr7ZbNi4qN0/gvr21rK++5EE1NzdB0rmqpaepsshsSUVXbU5CLiEjJSkxPGi+rKWmrsw/
+LSwvNkdywq6prbjlQz8+RFPgvLGrqq61v8/Pzu9QPS0lJSMjJis76bOopqSsvV83MS8xNkbh
+wbm8wMbGxMbOy8fFvL7H0d7g28vLxsfWajsuJyIgIyo2dbyuq6+1vsLDwcfTXEE8NzU6QmnL
+vLe2tba4vcfZXlNTYtXBu7zLVjsyLi0tLzhIbd5qS0RL7cK5uL3GztfuVUtOYejXzczKxsXE
+y87Pzsa9uLa5w+xMOS8tLC0wOUNGQD49RF3Yxr6+v7/Ey9nl+vv/7N3Z09HNyMrMzcrBu7i5
+u7/Qc0k6NDQ1NTU1NDM3Oz9JddTKxMPDwcLHys/W297j3tvc3OHa18/EwLy5uLq+xt9QRT89
+Ojc1MzIyMjM3Pklp0srDvr6+v8DBx87Y39/t7PBx8uLXzsjAvry+v8bN2PteT0pDPTo2MjAx
+MTM4P056z8jBvr7AwsPIzMzN1NzqaF5q7tzUy8a/vb6+v8TL0dtoU0xCPTo1MTAwLzM5P076
+0MrEv7/CxMTHycrP2uDu7+nj3dvTzMS/v7/CyM3P19zta1ZHPzkzLywtLjE3QU95z8jAvr2+
+vr7CyM3Q09bY4uz17+XbzsbCwsLAxMfO2N7wZE5EOzUwLi4tLzM6Qk9y1ce+ubi5ur3CxsrM
+z9xrVlZaaPraysK+vLy/yMrMztb3Wkg/OjQwLy4vMTU5QE5v1ca8urq7vb2/wcnT4nZhVlZV
+ZeTVyMC9vL2+wMTIy9l5UkQ+OTMvLi4vMTY7RlncysO+vby9vr7Ax8/c82hcWWVv49LJwcDB
+v72+wMTJzdd1UEE5NTAtLCwtMDU8SWLUxL27ubq6u73EzdTh8mdeZG3x3dHLxb+9vb6/wsXK
+ztRnSj44Mi0rKystMjpCUejMwb65tbq9vb/L1uXn5WRXbevr18i/vru5u7y+v8LF5ExENiwq
+KSYmKi8yPlnnxLmzrq2vsLfAydxtWUlMTUhPW/7RxLy6uLe5vL7Eyc75Tj82LSoqKCcrLTY+
+VdfEuLGurq+wt73G221OTEdCR0tbfdbDvbm2tLe5ur/D019KOzErKSclKCsvOETszb21r66v
+r7W7wMvYXVRLQ0lMT1zmysK8uLW1tre7wc3UUTk2KSsoIygnLTI/XeS+ua+ur660tb3J0Whd
+TkdMTFJk387EvbiztLO1ub7O7kg7LykoIiIlJSw0QG/NurOurKyusbO8w9BxbUpJUU1detzK
+wry4t7e1t73J6086MCwnJSUkJiswPU7TvLivrK6ur7W5v8fdZ2RPTE9XaOnJwr+4tLS0tb3R
+4U02MCslIyQjJysyP0/Vu7aura6tr7a6wM/dcmFeUVhrdNjIw7m0s7C0ucPYXTouKyUjIyIl
+Ki88U9a9uLGtrq+xtrrAz9r5WVlaXHrUyMG9t7KzsLS/xfA9MywlIiMiJCkwPE7bvbizra+y
+trrC0NPhYmR8Y2nZycG8trKvrq+5vNBAPTIkISQfHyktL0jTx7qwrq6wtLjE2NXmTVNmT2rN
+x723sa2sra+zv1pINygjIh8eIygsN1zPvK+tq6yusbfF1exXSk9QS2PYycC1rqusra66a04/
+KSAkHxwhKCs0aMe5raupqa2ztMLv+l4/QlRMWNPBurCrra6utvVLPiwiIyIeICctNU/Quq2s
+q6irs7O40m9dSkdMTXLLwbmtq7Gxr89AQDEiHyAeHyUqNmzTv6uorKmmrbK3zOZlQ0BUTVLL
+u7euqqyusMlJPy4hHh4cHSAlLkvWvq2np6Wkqq60x993RTxGTFzIu7Ssqaytsc5aQiwhHx0c
+HR8kLkRgw6+sqqanqquxvsjdUlNbW9O+urKsrq+30ldELyUjIB8hJCcwRVbVvLaxrq+2s7a/
+xsvVzsrIvLWxrq2vtsdXPzMoIiEgIicqL0Bsz725tK+xuLzAz9t/YvvRwrivrKmoqq+/aT4x
+KCEfHh8kKS89X8q3s6+urrG2vc71V09ZddO8sqynpqmtvXBCLyUfHh0fIygxRG/GuLKsq66x
+s7rG2VlITmnYvrWuqqmpr8JgPS4nIh8fISUrNUJvyb61rayurrS8w9RsU1Vrzby0rqytssH3
+RTUsKSQiJSkuOUdc0ca+uba1trvEztzzcGV8zr21r62us8L4RzUvLSknKCs0Ql3cycW+ube1
+t8DS/U9GSlJi5ce5sa6vtclfRDk0LisqLC85V9PCu7u8ube2uL/aW0g/QkZO98y/t7KzuMxN
+OTAuLSsrLTE+98G2r66xr7C0t73TVDsyMzg/X8y/t7CwtcFjPzQsKSgpKy05WsS0rKusrKyv
+s7jNX0MyLjE2Qe3GurKwsbneQzkuKSgnKCs0Rde3rampq6usr7S9e0M3Li40PE/PvLaysbS+
+YzwyLCkpKiouOVjFr6moqq6yub/OWUE9OjY5PENm0Ma/uLGwtLnKX0Q6NzIvLi0vNT5T2sS+
+vby6uLe4vcfZfV1VW2Fm/9rIvrm5wd5OPDc1MS8tLS40Q3PMwbu6urq2s7S5wthjT0xOVWnf
+yLy3srG2xWQ+Mi4sKyknKCs1SN/Buri3trSwr7K8zmdOSklPctXHvbawr7G4yVU5Ly0sKicn
+KCsxRdvDu7i1tbSwr7bC31xPTFNy2s7Fvbawr7K3xWQ8My4qKCYmJiguPGTKvbe0srKvr7S8
+yd9gVFdj99vLwbqzrq6yucdaOS4pJSQiIiMnLj17v7SvraurrK6yusx1UkdERU1a5Mi8s62s
+ra+54T0uKCMhICAiJCs4WsKzq6mpqaqsr7XC31RDPj0/SWvLvbavrKytsr5yOiskHx4eHyIl
+KzZVv66no6KkpquvtL7ZUz85OTo+SGHUwriwraytsb1wNyojHx0eHyInLj3puKujn5+foaet
+uMhkQjYuKywuNUR3wLGrqaqppqy+Yy4bFhoZGB8rM2GxqJ2Xl5iapLpTLyQfHh8lLkbJrqig
+nqWoq7zL6UdDPC8pHhcXGR49vamcnJyanaGo2C4eFhgcJ1+3qaSmp6qxvm06Ly0vPHPCta2q
+qKq14SoZFBMYKdSonJmZmZ2nsz0gGBIUHC2/pJ6epa6+2k08My4vPOy3q6Wlp6eqrrtYKhgR
+EBQlz6ebmp+krb9TLh4ZGRwvvJ+Wl5ytSS4oKTE4UMu3rKWmqrPK5PbRw8xgOiYZFBUXIkyy
+n5qbnaKuyDwnIB8mPr+poaKrw0QwLC5EzLSopqq2Uy8pKDBNw7Osrra88TsfFhQVI8Sek5GY
+o7w4KSQfHiIoV66emJymxzYoJikuOUPkvLKuscH4RkfdvLCxt7q8urrMPh4QDxIfs5qSk567
+UzEvMykiISVDqp2ZnrozIiUx1bCttMPYzL7GzUs0MzlJyb3AvMO9sK2ywkMiFBEXH86knZuk
+tsfPVVs1KCkuVauenaG7Pi4qMz9GQUA/Xr61sLvMWEhNSFNQR1DYvKuora/IRzkuHxgcHjSr
+nZaYo84+KCUsKC06T7WkoKSyPygiJDLntauorbS/Xj4yLDI/7by0trq/wb/DzOJMU2Fp2DUd
+GRohzaCamaPbNSomMz0+a9G2p6KntE4qKCs6zrWwsbrKzGBCNy4uOVnNvcfc1ce5s7jEZj4/
+6bqxwC8YFBgupJWSmrMrHx8jNj5GzralnZ2sTSQaHSviqZ+hq7lnQDYtLjE9dr+2tre9yNBc
+RDUyPOy1qaWps9QjExAUJKuWkZSmNyEdHSw8W7Won5yftjUfGR41vKKfqb9FMTM/RV/y5MrI
+1HdNSvTDt7S+7EpARFT2yby8vMMvGhoYKq6dl52zLSgoLdHVzcLItq6vyUYxKz/Ir6m5aDcx
+O32+v7/fRD46PFTbwbKvtsdILiszXa+kpK5nKBcUGii5npmaptEwKycsPmS5rKmqtGIvKSg0
+zq+psMlOOz1LftjIzP5lRz4/SXPJuLS2wWhHRFrPxMLQT0RGSn7SRSgjIC+/qaCrwzcwPGa1
+t7/WSk1kz8zK2V7bzcS8y31KPz5P7NbI2ltSR0hmy7qytclYODA74rqwvz4gGR8stp+epMc/
+LT5i0b9eWfjMur3hOC81WbGpq7pELzNSv7S2zkk7OUJQaN/Swb/E1Ug3MDl6uKusuVw0Ky06
+a7u5SC8pLfiyp6y7QjNG6Lq2xmRGTuW+vNFcQUP1xL3GYz07R1Tazs3N53tXTklR38a5ub3T
+Zl5u1VU5IRokMa+cm6LKMB8pO96vtru/zdtgRDEyRdSuqq/CSDY3TNy/vtH5T0ZEQ0ddyLix
+ueo7MDA+2LivtMNROzc4SG7K1jcxMDnMs6y3zD86X9a5uMxfR0hT1tDq2+DVwcjcV0A/V9nM
+xt1oe3FnXk5O68y+tbfA0EkwKyEeKES2oJ6owDsoKjtvvbe8u7vD2UgzLzpus6equU4uKzNf
+vbO4zWNDPz9CX+XKyc3fXl1SXHrZzc7bZVNSV93Evr3LPiIhIzW4pZ+ntT4vMTb8yr+8xMre
+eUU8P0nUvLvA11JDU1/91NvW0tbm83Rt2M3Kx9DU2U9BNyYfKDq9pKKrxTsqL0P7v7q+vsLQ
+/Es3NkPVsqmsvU0wLTldxbm6y/pVQD4+TNjBu8HiSj1CV8q7u7/cVj85P07SvLm82TAfHyVE
+rZ+fqME2LzE6Xs/IwLq8y2w7MThMwbKyvWxHPkt118nLxcnZa01S78m9u7jMRTUpHxwrTq2d
+n6rwMSIoPPOyrK60v3Q9MiwwWbysp63FQzEsNk/RvLm/0GNCOjtOzLi0u+JDPUNb0cfEx9Jt
+UEM+U8++trlQIh0eKr6knqa1RjI5PVnd4M+9t7i/ZjkxN0vFubu/0G5YVFNe6dPIyu1MQ07b
+v7u6vsxYOyscGidJp5qbqWkqHyk+2bOtr7O40z4uKS1atKekrdw3Li04Ysa5t7zXSDUvN2y4
+rK6+UzYxOlrGtrG3yF1CPEBa2FMoJis8tKeovk8uL+jAubzbZ9vAyNhONj3mwLvIUj5D/7+2
+vM50TkxLS01kyLWvtMZOOzY2LiAiLu2mnKC6OiQjPcSyscP868XKcz4uNOayqKzLOC8zR8G5
+vcfmXlVLPTxF67mvt89LOj5hzL+/yNTX2M1YIBkeK7CbmafHLR8tOlfMv7esp7ZVLCAnT7Cn
+q8BJPj9Nc3XTwba2xVU6OUbauLK0vtZfPy8eGB0xr5uYpM4uIyo7Y8e5s6ysvT4pIilqsKeq
+uvJHPjk/Uc20rrbRQDI1Qe+/ubvJ3+1rTUZKX85rLSgtNuWvqbK8WT5KTkhRW0tp29fIwcbF
+zWZSR0daz8G2sbjHVTgwNkfQvr/Cz/tMRUVNY2nayc7O2UIsJzBVu7G6w+dMPT5JXMq+w81c
+SFvLubW96FFOW/Z8Z/rOw77NTj9EU9zO23NOT0xe1s7W529IPzw4RevY09dmfOJUUVdZ287f
+3Nfw2tLWx769v9RKPj9E8L22t8VdR0I+R1JPaeDOzNFmQD49TtjKxtV9Uz88RVzfyMPLyelK
+RUNM7sjAvsHMdU5NSlvUx8DA5k1HSV7i19njeOTgZ0xDS/bSztDifmFbVFRaXmlz+3VldW18
+9uHa395hTEhNY9DAwsvbdFxWT1Bg3szHxsveX1JRV1hYZv3e0crfVUtFTWJq/trT0/JXU09Q
+Yd7SysrT8VpZYWdlXmju397n8H56++Dd82lbX+/k63dxdvTt6epiUVJg3c3V6/Lk6W9YUVlg
+b/Tq5uTb2tjfb1dNTlBSWu/Tztfq9XpgVVps6+Xl3tLQ0drs+WlWWWF06+nn4enu8nh2Z1ha
+ZPfa2uP5ZVxdZvHn+PX5aW1nWFZZa9/V2O1lXmFhct7a2trf6e7/bm91/OHg7vZycXzt3+Pu
++/xmZF1j9ev3dXNnYV5fa2VnZGBo+Ofk4On+9+np6vVsaHHy7err7u3z6+Tj92Rnbnzu5up8
+bm99cGtpbPz58vx4b2ZjXl9q7N3e3t/tfGNcZO/q8/NyaG/u7/357Ovu7/Dv9Hpz+/j2fGtn
+aHvl4Ojp8Pd7amFfX15kbnL97Obm6/Xv5ubo6O77f3Z6cGdzfGpobXvq4uX7bWtkan/3+Pf9
+dvns8/H39Ojm929nYWVy8/b3/Pjy/e3p7fX1eG13b3J3cnf6em9oa3/z7PX97u3m5PVvYmJp
+dHFyeHZ6b3R1dPfs59/i5On29url6O/1fXNvZmJiZGdrbG51dH13d+7k4eX2dn3y7fl2dHV9
+fm9yevru7efpfmlnbnV79/V2aGNoa2r57enk3eX9+/dzfu/38+vr++vj7H1oY2JkZmppZGZ1
+8fZ8f/F8e+/t7PLzfm5zdnj17vDj3uvv7vl9cnT19PN5amdobnFxcnjz83pvcH53eHRxdWxq
+9e9zdevm5uXm9HV7+/H68OTj6Pj57vV+e/b9eHtsbG9ubm1uaGZ0/XF4/Wtsc3ZpZmp17uji
+4OLr7+nh4+3r63t68P9zd/vz8HtvdnR6cHZ4dXv09nn+end6b3BxcHJuevr8dHhzaXPx7fDp
+5u728ffx8PLu/f3v7O3x+vj19np0efx8bnt+cHF3dGNodW5rbGttfPDt8evq6+vy/vPn5vR8
+f/nv9vfy/np+/X198vhqZmVnaGh0ffjx7O/09nz67urv9PttZmp1+vDy+Hh5/X/57uzl5vZ7
+eXhzef98eu7xfXJuc3z9fHX7eWlubG3/9/v+9eru7fZvc3htcPrw9vn48/f19ff++Pnz8npv
+c375fXf67ufs8e9vbHB8dGtwfX59c2hnbW1qcHx4cXlub/7s5uzp5u7o5Obn6Obw7vh9+/N2
+aW95bWpoaW96fXZ6cmpubmZod+9+c3d1c/fq5OTl3+Xu8O3s6vL18v55cWxjX2dz/nFoa33u
+/W5sdG5v/nH+6efo8vT19vT6+ft9dfrt/n7w7/1y/evo+GxufGlpdXt1cH17b3/z7vD9e/fw
+dG9sdWlu9vh4e/T08+/18fz37/HyeXzz9vLt8Hh3/Pvw9HVz/vtsZXBnYW1xbmv96e348vXw
++Ht5dPrt4OL+bvjr9m1sdPv6b219cm7z+/bq6+7q6ur2+/VucX53bv5ubHZqbWRydHhxaWtp
+aP3k6u7y6uXt6+jn6Hz26uzq5fX9+3N683tvcmtdXmNmc215fXZ3cnft9/787uXu8/fv7Ppo
+bH36dm5ve3L7/Prz73197/L3+Pf68eTr4+hxb3l5anH19XpuaHZ8dm/97m9udWhla/fu8X/+
+9/768u32fuzycWx19/B7cnR8+vzu8vFz/vH+7ezh5P337PHw7npsam1nYmFz7vdqX3Xy8/j4
+bGl1e3xwe+zq93x9+fTp5+3u6d/q+vv4cHf2bmr7+G5raGtveX5y/nb88m9mb/jv7fP89/X6
++vx359/3evB67O1ybHb9dnRoam54/nnt6vPr8HBpbHb48m9t+PprZXTu639t/evnfmd67fX1
+7OPk6e/m5X51eff5cnhuZ3lna2NteWv7dWpia/xsZnJ6e33x7ejl6eHZ4Hxx/ebubn3u9m1z
++u93Znjs82t+8XBv/vz6fGt8+PHx8fl8a2htdGxebvt3c3ly+f797n/2/fTx+Op/ce3p83lu
+++Tk9e3g7HViY29/cn7q5+zt631qanZqaWJl+XFoa33x9nF08n37+fv/ZGVv+et76enz+HZy
+7u/m7+3o7nNz7+rr7+zm7nZ9+/J4Z2h5fG9y/fN/Z2R6amBqem9ub2109Ozp6Pfn7Pbm5nxz
+cu3ybfrm6evv/Obl73VmbP1bYGRaYPpsZ3Fr/O3o5+Tv6u34fHD08+5+/+Xs5ery7fL49fhm
+W2vu9vxs6+hyY2xoa3d+eGZgYXNzdXPt6/3/9+bp/f31fP30evjo3+Pe53h0evh1dHf96PRy
+/XNsb25nbO98aW9wbnRpZvfq/mt4cunpfOjse3p+6+js9/nmbG3s4uvv8nvuamP49mpgYWxs
+aXfw9nR9//jrfv344+1q8vvr4/dtdXhocnJ39fH77eb6cv/v63pqa2dicXl0b3r07eT5+unt
+9Pfp/mNpaHLwcfbj9G948PHs9G///nRtcf5+aV9u/v/v6P37eG5v+vDx6u769+v0evDubWRq
+eHlqavfwdWtv7H3q393md3b1dmn34uz48nZ1aHvqfm9pfHBla2Zsc3129ujg7fzw/mVfafPu
+c2/97e1/7OPg5Ht48fF6aXXpf3Frc3t5+Xt7cHPr9n12+Pb7+O7ua2r68v11Z3X4enx1/H7m
+6PX0b+vvc27573h+d3h8bGx6bP9z8+Pv5/Xr8XJseP9vdH5mc+b7+OrscP36eHl79vn6bXDv
+7Xl1+/L4fe/f6vZ5dnBpeO32fXZ6729w/ez4dvP6dn38ePd+ffnseG16a2BeeXX65+7t+v10
+dPb39ebl6+3v6/707fzy8nlr+flsaWhnZm13cf3rfWVp9ujq6ff9+mdl/Xxv/e309e36dO/8
+d/Ti7nj2cPf1dWj+6PZ0b/7+Z2f683N4fu/teP76d2dkYe/xaWz75uLq5ufz9+vp/f1rcHNm
+Z3zt93p97/H99Pd1c/NsZXV7f3t++uzvd+3r9v97/P/0e358e/35fXtzaWJsem16+X7q5/Hq
+4u3t8evv7+Lw73xkZW5uZ2tze3Bqde77ZWbz8XD+7fdta/zx7/x+6Oft+fv48vf3enp67e35
+fvn1/nx59/55b3T6f/js9//u7vp0eXx1ZmBnbmpqevj+cXfx6OPf4/D5e21qcvrx6fdvfXN4
+9Ovo6fv473Jxd3z4b2p4/nF08fd8/W1vcnL97vbv6vTu7PD39P5/fnt4f3VxbXXw/G1od39y
+evTz8fl+8PLv8/X5eHL++W1//Hh0a3T7e3z3+3Rsen56+n347/bu5+ny7/l3b/ry8+3wdm52
+cXd2amp+93h77fF0eHJwbmpmYGVufPXsfHHw4Obq6urt+/3z5er4/nN79/v4/Pr9aWl9dG1x
+Zmpwa23+8PlsZvPp7PJ+/Pny8ent7vd5e3vz7/xzc3n493t5eP//fHx9dX55aWNnfO7p7vjz
+cm53+er5+O/2/unu7+l9fn54ev15fXB47/l9f2tlaGz+8/f5fPvu9PHvfnRrbXp/+Ojs7+/5
+7unv+fZ/d/vudGhz+3pyenR0fW1laHH8+n5tfPPv7/jw7+/z/HVxdv/0fHT26u11anJ59OHn
+6/Z0bmx6eXv5fG5wc3r49/nw83x/+f537+1+dPPuenBva2twdHBvc37t6eXp7fJ5ev3z+ffu
+8XFvfG9xd3318P16e2trb2lpdXv9/X716ubl+nVzampy+fn/fP5+/Ono9P/97vP6+vHu83lt
+bGptd3FwbP7v9Px+eHp7d358e/j3cWtvff71+P/v6Ojm5O5+dXh5efv6fG5qcHpxbG1vc//w
+7vn49v15eP9+9Pr99e/r7fh9/nf97O/r8PxybnFzeXZzcGxsfn78dm9wc/73+f317+/t6Ozu
+7O98fH16cWlreHn9+/x7e3d/8u/t8P1ta213+/v8+Hx3cv/t9vL0fG98fXl8cH7v8vd+//18
+/vX57fd8eWtre+79bW1xb3D88u/3/npvfO7u9e3u/vTs7v18dXR3fvV98vJ0/PHy8fhvbXtv
+bHn7aWlvbGtqc3x6dH339/Tv7vH57OTi7Pr07+/v+X53b29yb3ru7+/7dHBtcmxpd/l0cX38
+/3vu9Pny8/5vbfru7+nv/XV68npnbfZ7b3V3dnzr3+f1fnVvdXRsdXB0/vDwevrq5+zw7/N8
+ff75+3P9/mlkaW989O/4emtqfXp2dHt5c3p6/H727PFwa3p+/H766ufp5/Dv7fHt6ezs7vb9
+cP769/Z+b2dhY2pnY2hrZWRucHZ7du/p6e32fnVxf+/9+ezs7vH37Ons7vjzfnD7/Xj37fP0
+9v5zamZkZ3X+fn13bnf1+Pn6dXZ8bGpsbXj98uvm4+Xn6erw9Ph7fX16c290fH1+9ft3fP3+
+bWhtb3j09u7zdmxqbHFvcXl99PD7+3519Ozn5Ofm7P9++XRz7Ox/e/73/P1wbG92/fj8b2pt
+bnB6+vbvfm/99m5ue3X66+b4ePr2+Hz29+/s+3R8fHjx6/589Pfy9vXz+Pn28/x1ffd5b3Bu
+ZWFqZ2pta2psc/DxeXd68+rt9/787ebl4d/f5/D9fXvy7O/t4d3g3drd4ujvemxkW1dPSkhF
+QkFDSFV22czHxMXGx83X4vRqXltbXWBs+Orf2dHNysrLzNLc4ePr+H9zXEtAOjQwMTQ8Sm7L
+vbq4ubu/xc7b7GVUS0RAP0ROZ9zKv729vb/Gys7Y7nJoYmBlaWxhT0I7NTExNDtI8se9uru9
+v8TIy9HjcVlJPTs8QE7wzL65uLi6vL/Izth8WE5UWV1pZVVBNi8tLS87T9e+uLe5u77AwsPJ
+1G9IOjMyNT1S2sS8t7a2t7i7vsnhXkxDQkpY9dfP3Vs+Mi0tLjZGatPFwL69u7i1trvKXz4z
+LzA2P1XgyL66tbKxsra+0VxHQEJIVv7f0tHhYEk4LiwsLjZIb86+ubWxsLK2vdFROzIvLzU9
+SfHGurKura6wuMfzUklDQkVMXeDMxcXWVTksJygsM0Jn18S5sa2rrLPGWTwwLi4uMjxO2Lqu
+q6qrrrW+z1pDPz09SfTNv7i6yXxIMSckJigtPXHJt6ypqamtu+1GNSspKiwySc67sKuqq62x
+v95XQjs8Q0tnyry4trXETDQoHx4kKzJNwbOqo6GmrbnpPC8rJSQpM0rEr6uqqamstcVaPDg6
+P0RX2cW5sK60xU81KB8cHiQtSL6spaGgo6q37DgqJCMjKjhpva2npqirscVqST48QlBh78e7
+trKxuM1FLyYfHB0kMk7Aq6Kfn6OtxEkxKCMhJCxExbKppaiss8B8SEJEQ0zoyL65tbW3uL3f
+Pi8pIRwbIzVWxayhn6Glr+E5LichIis82Lasqaiqts9hSz8/S2Xdx7y7vby7v8vR6EUyLy8o
+Hh4teM7GrKCgp67FRDIuKSUoONW3s7KurLTfRUNLWG/kz763ub/Cw8rY4NnvSzkxLiYcHTXS
+88ymnaKuts09Ly0nJTPWvLuwqq/B7Uk7Pk5Zfb+zucK8usXe6t3NzPVLPDkwIxwfON1eyaSc
+oq++3D0tKCUpPcu5ta2qsMpNOjc/TVPWt6+5vrm5y3Zu1sHHa1lSOy8lGxsv2k1gpZqitL3L
+Py0nJCtIybyyqqq53048OEBLXL6usb+5s8FaT2zgyMTr+9g+LSofGSJd7EWzm56tucRJMCol
+Jz7Bu7WqqLb5UkI3NkL2u66yvbixyUdK5dnUx87ayngtKCofHCdK8c2qn6Oqt+Y+MSsoLkvH
+t66ssL/kTTw7P1bLt7S7ubXBX1R06OPbzcbG5DsxMSUcHy5L5r+poKOvyHM+LSksNFG+srCv
+s8hvSjw9Sue9tbi6tLfOY2np3tbayLrQRTszKx8dJS060rKoo6Wuv2w0KiorLk64r6+sr77T
+VT08T9XGvrq0sbvT39zcbVFU0r1lPD0wIh4iJitZt66moaWuwEIuLSwmLfu4tq+qrLbRSj9I
+VFrZua+zt7e90HVSTUpLXetGNjUlHSUtKz+3qqajqK20cC4pLCkrQsa0qaetsrxbOjk+VcvA
+uayrtcTMz2E/NTpKOS8uKigtLjP8tq+sqayxu180LSsoKzvct6yoqa6830tCSmLQurGvrq63
+zdZeNS85LiYqKygqLz7ewbispqmvvMlkOi4qKy89Xcm2q6uvusfL12h+y728wL24u8nW7kY3
+MyojIiQnKjFauq6qp6aosMxaRDQsKi44Rl3OuLCxtri3usLHwb7G1NnZ3eTY2mBRTzgtJx8g
+KSgtX7mtpqSlpq7CaEA2LCktNjxN1b+3uLq0sbW1tbW4xetfTEBDTElJYXlIOzYtKysrMEH4
+wLKsqqqut8TnTTs0NTo7PEdo5NrSwbm2sq+trrfIekk5MTM8QkhxzcbH1GpJQjw4ODs/Slrz
+39POzc3U0cvHyc7R3WFNSUVFUefIu7a2t7zH7VJIQj9KYvXsz8TL6fhxTENFQj9ESUtKPz9F
+SU5e2sW8u7u9xM/pXVFYaP/c0cnKyc/b9WZeW2d22s7Iz9J26WFaSUlMUE1NTktGQkNDRlD6
+18zJxMDI0dp+ZFlcbOTl3dLU5Ph8aO9p7tbIw8G/w83Y5FZMSk5KSkhISEZGSEtNT1x76+XX
+09zf7Oj3eOzw2d3X29z3/fzk9OPjzc/LzcbMztTe5GtuXF5NTENFQkxGSlBaamdfdOtwbWt0
+++Lm2t3a39rl4frj5uLf6tPZz9HS1NTT0N/a4t9sX1dRS0ZKSVFQV1VaU1FPUVlZfXfi5tDQ
+0c3U0tzm7N/u6frfft/p6ujX29jV08zb3e51TVFGSktOREtUT15WXPz1feLa3dvZ7N3j3ev2
+YPnl+uvz3und5cva09nP0dbQ3M178VJMP0Y9Pj9ARk5hYNfTyc3M0M/l8vxjWFxbVmX939XR
+z8nIy8jHyMbGyNpmYUg9NzY0Nzo8SGPUyb27ubu9x9hcR0Q+Pj9GTV7q0Ma/ure3uby8vcZc
+SEA4MC8tLjg+WtLHvbKxtrvL4GRFPTs5PUhX58zEu7e+xMva3ex+1sxaSEw7OD86PE1T2b7C
+wbrExspeSkM6P01KX8/Iv7zGy9JwdFlMVnPfv7nHZFc9NTkzMj5T3ry5ube6w8phQz05PktP
+cNLLvrvBzntQTlRSZNjIurK0zUw6LSstLTRJ2berrK+51FtFNS8wN1PJu7e3ur3KZkU6OkVb
+0r+6t7O0tsc8LigjKjI82bqwqau510QzMzY5SvnGtbCzvN5MQTs7P0vdvraztbzJ09HuOy0s
+Ki9K4MG3trW4z0w4LzJAY8y+vbq6v8ttQjg3QWLXysK/wMTM2uDPxNBPPDIsL0Viybq6vMDZ
+UD05OkR0xr28v8zXfEc6NzxM3MG8vcDIzM/PycPOck06LSs2Tc64tb7KekdAPDpG2r6zs8B/
+Sz03Oz9Jb8m6uL7SbG/Ww7i1u81XNychKDVvsamttcxJPjo0O27Asay220MyLzY/V8y7urzK
+X0xZzriur7fLQy4eGiM1w6Odp7VpLiwyMUTArqenslIwKCczV8a2sLfFdDw5UcCtp6m20T8v
+JBUYKmGhlpuuUygeKThFuqekoatYKiAfLdKvq6y7VT43N1m6p6Cks+4/NjcuGBMiOqaUmaxG
+Ih4sXcS2rKmor3UoICIwtKaqu0kvMD9cvayloqnEOzI4ZbrfGRAcKKmSmKlaISE4WtzJvaqg
+p70sHB0uvaOmyjwuL03Jtamkpa5sLy08u6irMg0NGTaYjpqtOR0lNDdVt6mcnLQwGhQe0KCa
+odguKCxCvaqgn6rRNS1Fv66r0hoMEh7GlZSfsy8fKDA5v6ehnqw2HhkbQKGZmqswHRwmZKec
+mp6zPy4yVr21uUMUDRgnqpKWpMQmHSgvT6ufn6TjIRwcKquYmaU4GRcgV6KXl52xNyYnO7iq
+pqwrDgwVK56Pk57jHhoeKHqkm52oPiAcHEOhmpy3IxgcL6+al5qnTCglL8CnqapBDwwTIaiQ
+kp28IhkeI0CnmpeeeSEYFyymmJekLxsZINyfmJaf6C0lLr+nqLkjDA4efpiQnL0uHB4tNuSm
+m5mjQBkRGE+ZjpOtJRQUH8eakJShTyIiMEu2rSoVGyNtnZ6v1C4kO/Rqv76/qq5EJR8pwZ2a
+nrQuHh8tv5+bnaO2Mx4fKSksPkZF0bixscc9Ly85zaqnsk8pIzm6qqm9RkBUzrautbi4x8rI
+USwxPhwXL96vnKg0Kyovsaa4vc9DTlEvL1DBp6CyRy0lMbWem52qwkksISQqGBdOrp+Xpjkq
+Ix4yzrifm6fPKRoeO62dnbBBJx8tuJ6bnarCZDQnKCcVFUG3pJqpTVI4IytA8aSbqOApHinu
+raShq9ovIydfpp6dorJSLiAfKRkVRbqpmaN0XzUcJU7Ln5qozS0iKC9fqp2dqTscIEqrnZ2i
+q+QrHhwqHxMncsKcmqyvyiIdJynGm5yeqy4kJyE9qqSmrkA1XOS+rqWdouYmFxslFRc/xKqd
+pKys3iklJTS8sa+sr7DDNSo6dMnA6863sq+zr6ap2zYqJSgaDhg/sKSfn6WvVCclOl9a68e9
+q61sQ82zwUsxMF65rKahn6i+NycgIB0SFzLTvKyenJ+tPisvLyosOc2trLm1qKi+PC85Xs++
+tKifprLYLicoFQ8eKSJJqqGZmKWwr9YuIyEhJzzgvqugo6mw5UNi2dvOxsLrOC0zQfXORTxU
+Oy0zPU3Iu8a9tLvMalJe2NduY+LRaUVCV87G1M2+u8hIPEvex8LJyb3ITz0+SVpNPENsUT85
+NUReWXHCt7vNTT5KeNrLu7CttdJdT1Nsd+3X1ehq/9bJzWJFSlM/NC8tMT1I+cG9wMTT/mZO
+P1evrb2utk5NTj5O6unVxcPCyc/OfVxMSVRHPDQuLjQ4RtfDwLq/yshvXtDHytHbyt5FTNnD
+u7m3rau1vcBLLSwnISQkJy4uOMu2r6ein6GwxcNELS0sLTIxPV9vvaupqaeko6uvsck/MSMc
+Hx0YICknN+68qaejnJ+nqLPNOigwLiQrOT1O3Lyxr6efo6alp67LUTYhHh0XFxweJTN9sayn
+npyeoqWry0FJMycsMi8uN1rY0rKnp6Wjoqe6ycosHiUdFhofJCo1ybSyo5yfoZ+lr77STDQv
+LisqLjc/V8Oyrqqmpamvtsk4KyoeGh4gISk1X8i6p6KkoKCqr7O+7kY+Ni0tLy81S87CuKup
+qqqutMVAMiwfHiAgIyk2Uuu+rKypoqKmqay2y/JNNC0sKywyQGXOt6yqqaqut8o9Ni4iIiMi
+JSkyRk3Br62ppaOlqaqvx+FKMi4tLS82RVzUu7GwrKqwtblaOjcpIyEjJSQqOUNhuq6ppqOg
+payrteNOOjArKy4uNlToyLWvq6qrrbW8fDo3JiIkICElKjREd7atqKKgoaSqsb9TPjErKCkt
+MD1lxriuqqenqq2ywlg3MiceIx8fJCswRte2q6efn6CjqLfNSzMsJygpLDVI1ryvq6inpqev
+tchVMC0qGx0hHR8sNk/BrqOin5yfp6m8Tz4uJiUmKi48Zsa3rKinp6apsLjNXTIlKR4ZHiAe
+KTxkua2hnJ6enqezvj4tLSIgKSswUs26raqnpampqra9yU02JyMkGxgmJx89vsSwoJ6en6Sm
+suE1LC8hHzE5MfW1urGpqKerrqm05MvcMCImKBwZJCsnMsqvrKifnKGqqbVIMycqKSAtW05r
+sK6zra2srLa3rcJQ2WUpICgiGxwjLzlFuaKjpJ6dpbPBVTcqHyYvKzLQu7qvr62rtrmttci/
+wd9PNygoKh4aJy8oOLqvqqGgn6CuytA+JycqKzI/VMGwtLevr7e4ubi0ucTadFguHiYqGhwv
+MznBsaqdnqmmpcE9OCsoKicu5MjWtq20ur6/urnFu7G7yMjpMyYoJRwcJS071LaqoJ6ip627
+3DwqJiosLTvjwrqztLi7xca+vr+2sLe8u9Y6JyEhHBwgJzjBrqeenJ6irb9hOComIyIsPlfV
+uq+trrW/w8G9u7y5tbK40k01JB4dGhwiLEa1qKGbmp6iq8s/LiQfHiEpN2a7sK2rq6+5wb65
+ur68tLW+1EgrHhwcGh0kL/mupJ2Zm56hrto6Jx0cHiEqPs6zqqipqq+7vry9wL69v8PM9Dsm
+HRwcHCApN82qn5uZm56ms/kyJB4cHCEsRcGuqaWlq7G3xNPOztPU1cfB0186JR8dGx4kLEyz
+pJyZmp2irs09Jx0aGR0qRsSspKGgo6qzv9loWE5KU+LLysrXOSYjHh0hJi5ps6admp2gprTo
+NiMcGxwlOdWwqKSho6qyw/ZTSkdIUvDFuLW+3zknIB0cICcvYrOmn52fpKq47zQkHh0hLEbD
+r6mmpaetvNZcTlFZXWnVwbi0trteKx8eHR8kKTfgs6ijpKivub9YNi8uM0XtyL27u7u9yN1p
+YO7XzcrHwLu8vsLUaFJMV0kqHh8jJi4xN/u5raqstbi3tbfG2utrbFJCQkZM/tDJv7mxrrG3
+v8/xUj42MjE4QlRodF5ZTzszNzc5QUvzxL68uri4v8nR525aSkdKTvXLurKvsbW9ydNmTzw7
+OTk9Tmpe329HRn5qPz5R8cnMYUhLaHxZTElTdtvb3svAtrS3vc3d5/FabeJeW+jU0eRjXH5X
+T0RDTU48Tv3tzM584sDdRE5T+1xmSU5cyNH60eTI1+hfa+3Ybv7PynXK3k/kQfJWXExO5+vc
+2se/V8fq5mM9fETeVk3P1stv1PNlXF1aV0rYWeXYesLJ2WXdRms+TF1CWeBd38/RxsrlycNJ
+22TazUvk31XZXd1N7Vxh0UHaYO1X3GHJ4EzMQtY92UR0V0/bVcBOvE/Dz/rSV+t6y1rMY9vZ
+XsxbbEHtX1HrWchoX/Z02/NPQcM8yDfTUU/JUL1Hvd7J1W7N+N1V3FXXUXVX1WrR1lfIS8hC
+b0x4Y0nNVctS51xYbVBeS9xjeOrSWsHken3NxWHcbdrYVlj769le5/3EWtfr82NnTvtPUuFU
+WXp9S9RL72VJ11rYX+nq2PHSW71Kzell0lrt5OLh9ezWW8x64nHm2V/dRdpFaENI2UnhTdZn
+vlTU8ObXStVN61h6Y/vJRcBOyOZy3Nvb9W/W+dNS2Wv57F9N6l9t6UvaUmNm/tpfeH7Maftf
+6fhZTl9eX2Buau7eWb5hzfnL1u/T39d+3GH+aU/4S2tV5GPtXfxe3Fr+Vtle0FZ4Zd7bUstI
+ylTjXdt85WPpeN/Lac5d0nH3alfpTPNS/lhsa97ve9pu1GTb6Fvtc/Vwd2Xu32TaUs1f4OBe
+9WPPX+NS/m/XW1rhYHRN2k/tX+H78dd029/841bqbm5Z4fBv1nnk7ubc7F3YVNZV1l/n7X3p
+fGlU+F5Rcl72W/lo4N7/y1vZd+xi4mdZz0nRT99bzfLf8lnQ7mnZY2Xu52tj7mDbTfZd3lrn
+aGfc/91Y2Gbaat9s99xX3FnnYuFg2v5q/nBl/+xlbGvfe+Ze61/ZXmHdbelr9Xbs7WDY9HRl
+dO5e5lzZW2bcX/d+7GzP/ert63Lu3lXman1o+GRpaP3tYuBu/2hd4/nk2uJ25Vnn5FhfXuhv
+fPLtdOvf83To3vl6XfvzWW3+Zmpv+F/8V+5ffHZw4+vm7Obn7uPu5G3u8eZycPn/cnbg+Oj5
+5eNxdmr8dmtycGNddWRtX29senLu7PHrad7p8Xh9/G/vc/T+6+ju5nT2bOtsdm19bfR0+Hd7
+b//q83/r7mVl7vxp+n3vZPNfbPd/ZfbybtvY9fj07Gnk+WfhZV5162D63e7d+XNqamN8X3Be
+6+Nz+upmdXFf8F1Y33db2+vYzN7Pz+7o23vt32x4XlFJRURCR1ReavP739fa09fa29nc1t7i
+393V5tDY5NzSycbKw81qPzMvLS4yOEJO4sC2sbGzvMPeW0o/Ozw/Sl/g0MvHwsHAxsvHyMvI
+yMXI70gzKicoKzE9UNa/ta2qq6661k45NDIwMz1N4MC8ubq9vsLFw8nJxsO+usH1PCohISQp
+MT9zwbStqKitt9ZHNzIzNjlDc8u7t7q+yNLQzszGxsXCu7W3x0wsHx0dICxE17esp6OmrsJH
+My0tNURQ2r60r7PGX0ZBW9vPx766s7Gxr7hOLyIYGBwlQLqpn56lrMs3LCgpOFjIr62vs8Bz
+SDg4RFDbysjFvru1sre1tLlcLSEZGB4qa66noJ+puE4sJygrRcq5q6ivv209Ozk4SGHcxL+8
+trWvra+1vMNpLyEZFR0u662mpqOqvUgqIig126+trKu5XTouMz9L78nHx8zTwLauqamvvNP2
+OyQaFBgqTrWnpaChsU8qHyQ26LCqra2xXzEqL07e1s7SyMHNxrSsp6m31VBJajskGhYeONm6
+q6Sfo8IvIiEu/7uvrK+zwzYoLEjDvc3m1czsTX2ypaOrykpLcc9KKR0ZJDxG3K6inqVuKCIm
+NuPEs6msuO4xKTfQucBqS1lZQ0PPqJ2hsWc+TF/Pz0ErHRskKTuzoJ2fuzEnJClByq6io7Nz
+QTEzWs3hTUFDT1FXwKieobNdOj1K3rq5fy0ZFBsp5KKanKTXKB4fKXKsn52kxEQ4Kio6UGXq
+7u/X4dKxpJ+oyTovOFS/q6a4KBIPGC29pZ2cp18mHB81uKOdn6rdMyskJT+9uLvFd+fj5rSk
+o65NKixKvauips0lDwsVLb+fmJugviMaHzW2n56jqu4qKCUm8Kmvw/w7P1dss5+gtT8pLeqw
+p6CqYy4VCQ4rw56Um6W0JxcdLcWdmqCozCkiHyFspai54jw9TlS7o6O1TC811bGso6HEKBkM
+Ch7BppaUorM9GBcsyqKYnaq/LB0eJFOgn7pcOS45Q9OmnqnTOjRVuKynpb0uHhILFU+qm5ef
+tWYiGSfcqJydrNUzIR8r4KagtkU0Li49w6Obo8s5NEPGsKmlyiYhGg0VTaydmanT7SodLMSm
+m5+8TTQoKDF3q6O5OiwrNey3p56kxzs0OOGvqKSrNhwbEw8ntKSbnLpTOyIm6ayjn63mRi8m
+KUC9qa1OLjM7TcaypqGxRDM8YrWop6rZIx0cERZIqJ+bp99cNiIq2K6jobVXRC8mLUu8qrNK
+Oz9CX72spKXEOjlVyrerqsMrIh8TEi2zpp2ht8JNJCNFuKeirt1qRConMcmorl89QkNLbr2m
+n65YO0Fex7Cps0QrIBUQHuisn52pt2gnITLMrKSqvMhWKSEvx6ywzlJQRTtMu6afqeE9QEZV
+vKiuNyotFw8eSrqfnausty0kM0XEqa29t8sxKzVVvrvrU19EPV69qqCozV1bPkS/rrBKLTAa
+DxxEzKaep6esNSMuOmyvrrmttT8vOkJYyuBScFFF3720qKi6zt48PMC0wkw7Nh0SGzdxsqOk
+o6lNKi83Ps+9u66v40dOP0FXRELu39+/t7OrrcDJz09dvLroPzs7JxcZLUzuuKumpbZJP0I7
+PlnXt7DI0sFcQEs6Olpe4q+surWxxNvuSGe9z1ZlNzq5ThwfOi0oO1+vnqrErq9JMzc3a8pl
+y7PCV0s/SFxNy660v72+xcx91re8YUZHOjU5TrrKHho5MiE5uaicpse3vi8qPkhZxr+6ssZY
+YO7j52Hkv77K1tLBur/Ne0VEOy0vTbu55s7fIh40Li+/t6+kvkPL7DhM73/HxurdzdPJwc/U
+2mH1zM7NwL/J515JOTs9SmLuetZ9UE1oWkvQ1k1MQjZLdFVs/OjJXzlnvLa3yse0zjxG5dS/
+u/S9/to6Pck6Vt5ibdZMZd18SmnZSltEylfi22vH/W4/P0xZQHa+0c+8SeTcRlnFcsfk/tdK
+Zt1aXM9+1M5Py9nRZ9Vtb25T+9bV3/xd3kRQQXZsYuNo715bSN9MembczdJm2mL6SeZd6Phe
+y8/721nR2mhT0+jTz8LQzdNLYkg/VU3Y3c3c1HZKR0FPTk3Zz83O0HvcW1JO10vf1trQzNBd
+01Jcat5Kzcf/w/Bb7l5BSlVO3/DL2NheXEdeSvhH1sFav87xW74+2/bm4s57zlTn7FXvXl7O
+V+NPw1L21U74Tto72kTwSL9U7dze7FneRchIzd3M38neV/1NWGLMU8Xced7kae7XT8ZK3VTc
+VMpJxkPQTFBiTFj7b2PIYOjrSu9O5kzH2+rO5c9R1WtP4990zm3eznXkdlrZXVvt10vW9Gtg
+TVtHXE9xec7m6tLY1+NY3WVQ6eP7cNf/WPb3Wdn0Z+3N6/HQWe/kVudJ0WL/5WhczUvRUPBf
+X+9wb3V25v1q3mnIV3Jy6EvfZ2vNaODf7t3ia23lXlLN/1nQ9GvPVNtV6ETUQMV13/3h71lj
+X2zYb+Hc3nDtWl/oUmB0yVjTZdNq3FvUV/BO1Vzu6n5sx1DeUdhG33ZaylnIUtNNYFToW9Vn
+x1jbcWP9+uNodvds4eXycNtMzk3jYOda0mTq/eBeWv9saGxe03XUVtH7TctKyVj87XDYcnTi
+bNBY5PRr+E984mVp+PRXy0tt0m5s1VFv31hczHj64VXZ+F121VPTdmvb4Fxp/mp7WVjpb358
+6/ri4nPYbW7mauBobfd76/PZfOz+bOPt6e5pVlhXYk5defDpb9vdz9/g4eN2+WXmdmdc7+5e
+5Xvy5GdiXWJTTVFT7fPfz8rLy9nR5epUWE5PWU1v2+/e48nd2PfX1XlvZu9vXVROSU9NUV7f
+1NfX2cnY6Ghwb1lPVHjvfvfd1dF89vB9XWFu6uj439Hj8e5s3/9gZ3R4altkbG5kbXlkYltb
+Y2r739fPzt946ulTT1v+fvz2z8jX4e3dbFdUX2VUYXl8c33SaeLa2ePr5t9rSUpwW1pl3MrY
+4tjP7FJSWWVWVOre0uTg0tBtWVxfX1xe49fk8t3maFJWcmpq+9rO2ePj3vJdWmJsZmlz+X1u
+8/f5anny7N36eN/rb2Z19/p3fubu//P06mlqa2ZdYvpkftTq9+Jy+3pee+fs/ODf4epq92xi
+XFtfam/u3t3z9238Z2RoavZ27ezk4uruenv49uz68+fn7/T8ZlpbY21oa/Xk8fhuZGhrbfjt
+6eLZ3ef49PltX2hvY3ni3e3x4fpdX3B5b2xx4upsZ/jsaWz+9v5w9N7jb3fy83VmdPN0eevs
+8m9+9nts+fLq/u7oeGtqdGx1/XJv5e9q6OBlamj+7HBv6/dxfPft+Xjq5u7q5/J0ZV9janFn
+euLd7Xt1dG1hc/Ry/NzY5nvz+mZv6n5ydl1mbV514OHh5OrhdVtfa2ZiYHre5Pfi4vXv3Nhp
+TF7kT0/QxsbObXl4QT5c+Xjez8nWXVtvWE5o3/P929nvX23deU9n1+tn7M7SZ+jP+lZ36fdv
+UExNS11k58nJ0t7uZVdWaurq5NvW6lxZW2R+6+Xm9OrgXU1rfVVi1cvM1tndXk1Yb+TQz8jQ
+RThAQTxZw7q9ydPqRztEa9vPycjOYUlMXVZR0cbVzczcWkZPZ+3JwcjIyPg9KS1OPkO6rK/F
+aXBHMDRuvry+t7xROjc8S1fLu8DeX1xQS2XPwsHN1dFtT3PMv9EyJjxCK0GtqLbNy8w5KjnT
+xMq7r7pJO0I+OEXLwNHCvtnyZ1VjXuC+w97JwcvGVDAqIS5IP7uiqLvTSjwtKki7tLCwt9k6
+MDY+P3DGxbzLb/vf72vgxbvF3cy+xtnVSy4eHUg9NqqdqbvvTzokJ2e3tK+treYuLjUxNXG6
+rrS+vM5USFjZyMfBvbi71uM+KhoXSUssqpehuepNPyIfTbKvraqlvi4qLSorSrqqq7e5xUs5
+Sce7u7awtcO/Oh8bFyc6LrOXnLC61DsjHji/xLKhoLQ/NzMmIjLbta+yrLVlTc+/wb65tbvP
+cVQwEw9KPhm9kJq0srH9HxgsxdXYo5enP0hDJRwnaMm/q6Kvz8q8v8vCuLfTQt/FHQwluhgd
+mpGstqWqPhkcTkUrvZedzdOx9yAhP0YvSaypu7alpbnBxXRLPVBGGhFbXxEvlqPboJyuRiQo
+Oycfy6Ozrp+v2V4+LigtO/HPsp+fpqWqvlstJjQhDyGyJB6jnd+zoK7GRS89OB8m4c69pqCm
+rb3uOCcoNj1jsKKen6CrvEcqKRcNIiwYPpqot5ygt7xSMjYpITRCPrmlqaeosLnfOjc0MErW
+1bOjpay94swuFxkmHxkxrrS9p6Ktusfr2kUtPk88SMK5trCysLXH7VY+NTpAXc68t7fG9l0+
+NDQuMVBHQL3CPvPsOEraaM+/y7u54OrZa2J84M3L1HlUT0Q9Scy9u6+ww9pOOTg1NlLXzb+4
+vtxNPz4+PURj2tnOyedST1ZSXdLN19HhYElETlxjyre4v77KYUc/QUtQe7+9w8LTT0VAPUNR
+WPfP2eHX5VtXb/f34ePvfF9RTk5Pbs/BwcXG0WFOS0hLWt/KxMbR3/tPRURESFZWWtDI2t3P
+0ebv29f8d+hhSklgf/zbzs7V2u5iUU9gduHW3tPW9mBbST5DSkhL/tDPyMPI0NPT3X5dYF5N
+UHzi7+DP1/fm4F9dbGVkbnr+5OHq7/5dT1JNRklf7NzMyMnP1tHdaFpdVU5We+Pw6tbV6H3v
+cVpeXl947t/W3Pjv9l5SXOZ/TV7R5VfbyfxZ9ePudmBZXGJvau/Qz9jd2dt0XFteVVJp4tfU
+1Np4YGBbUU1XdeDg7uHf6m5aXGNmdPju3N50feZu7NfY2d3c2flSW9biUE3autI1P7q/Ojbj
+vV43UsHcRErazG1N8srO73fXzuFbX+zT1e/cytdqXGlvT07f3GFPT+vZXVvnc0pPzMF+RUpv
+znpAVcHEWEjGt9JMV9DI/klZ2uhcT2V5c/nzz83iYnvtVEtX/lVV5+Zq8nDd2mlO5exaYvHX
+2mjrz85OacrJR07c2XNL89PPZVXcx3FNSc16Plxn8OVqaMDi51H7y2dK6vDX7lxr4u5i4ljY
+4VZ4dtrtS17OX+9GzsziP/nMUXVVWc3TU1LDx2hByeNlXslP5+pU3thqTc5YaG3sVufOXFbv
+ZN1tWdbjzlxd02r4XlrdYGh/62PdY1/eeWH6zGFY4VrP6FFv1dxrWPPJ6U5bxc3fSeV9zl5G
+Xu/9T1he2XFd7tLaXFTL3Wli5nvv4ORa59NQYVnW/Fxn5eHhVO7J73D901/nXG1VeGds3GvZ
+X/1X1d/oZFzmX19MzmXlXXTj/t9b4nbjaWvje9Xa2mTd1fN0VHJ99Et8fNRoUfXZ7lDoXNJO
+WmT9cPFt2Pve2mrk5dFk7Wvc83pt7Ph0fGd9bn9S7GnpX2T96etn3m7ebFxbdd7e6e3e83z7
+d2v7+2BwZXDrcVvt6Odr+9jXaGbd62pg/OV7+Vvj7/D0ZmF6YGXva/P6ZOx28nV71+zr6Xrc
+7fdyZeptdmp+4mVtXWZgZWPudPZ86t7fffj59/n79fRy3n7W7OPe7W77Z2tfaGtvbVtsaWtp
+Y2pl/n708nJw9Pr86PPa/eDj2+l16Xbl7vLy2+Rr7eX+3Pjw2Nzc49XsVk5WSkBBRUtOTExo
+4+v028/MzNLO08/a3drn2N/j4eneft5t7PLi89fz5N3Z8lZRTFM+Pz9EREJLWdrf19HMzM3b
+1tXd3W/k7Onn8ebq3O3Z0tjT3tnS2+je6t7q+mtWU1ZRQkA/QUFDSFRq7eHSycjJ2tTM6tZW
+X/th/1LieNZu187ZydnNzdjX2N7V/trvYWxOWlpdTUhMRkxESEtYY2Tm39rb2Gls72r1b3b+
+4vPZ09fIzc3LztHLx9Po8udbb2hfX1xlVmhSTUhMSExPUVlmcG53fenq7PHx7u386+Pn4dvX
+3t7b19PX09La2eHk5v186+7n9+v5fmx2WlJOTExKS0pKTFBSaG379fv3aPzs4uPg3Nra19HN
+zdjc29PV3eLr7OXh/u3i3dzh4uV5c2VbUkhHRUNAQklKT1RbZGz96tza4NfUz9LW0tXU1NjU
+19rW2djb19vg4d3keXP9a2JfW1pVTk5LSkpISUpMT1piYm7y3tvY3dzb3Nva29XRz8/W29vh
+3dTW1t7d5u3yeHRvb2tpcm1eYWljW1RiWEpNTU1NTlJaZWpu7d/m39rQz9DW1NLY3+3f3mPX
+yObf2Nl0SVTDcVjTx+Zh9+DYaVtsdV5JRkhFQ0FARk9OatrZzsPGxsXYZGReSFJZbNLIzb68
+yc3V1cjUdtvR4tXHw8LQY049LiooJiYrMj1lx7OppqinqbPFXzYvLyopNkdXzbmuqKqsqa28
+ydBrT0VKSz5ERCghLyceKzY9xLO0oJ2qqqjDSD8qIScpKT/Nvayoqaeosra7ytZzYs3M48tp
+P0wtGh8oGRozU3avpZyXnq2nsS8kJyEhIi7Cs7amnqayu8TSPjjbxWfIrq+1t8BQNyEWGx4X
+G0W6taKal5mktbtIHxsfHyMu76aiqaGfsmhGPDk0P8exr6qlpqy04y8mHxQQGyIgOamdm5qb
+m6hELC0hGx8uRce2qp6kwMHJMigtNlPKsqGfp6erweZVOSsiISQbGTBMMuCenqWmqq/eKSU6
+LyY/tre5sbe5zzI3VDk5yLOwrKuprsf1fUlFcFtGQzs5IxkjNikpsZ6nqaKhtD0qNjcmK864
+z8W6xtdINF3NVMOrrrGzvLzWSmTh7MvE30c8NycXGS0yLdeamKKppq42Hh8vLipPqqi0vb7X
+SDE50sXDs6uwu8zh4lRIV97PyL7OQjc1HxYfPEBKqpeaqry+Qx8aJUNR6ayfpLrkSTExMkC7
+rrCursZcT0NLaHf839LCsr1GTz8fFx42R96rmpquXUYyHhwrz6+spaCq5C4qJypH4LCip7S/
+Yzg4O0XMvcTHwbyysMZJOSscFR5L772gmqG8PCwrJSVLrKarq7FpLycmL0G7qKqss9s3LTRK
+5ci1rbLP3bu2uLbgMigeFho7w62em6C3Mx8gIyhGrJ+hqbpMKR8oOnK2paKy2Uc4Nz3msq+6
+x9RqTHS/r6yyzDQiGhAazLWunZqlVh4dKS46tJyYp908KR4cLK+lp6Gn4S0jKUjHsqiquks1
+PVDYs6qorMAwHhkQGMGsq5uaq0ccGCg3W6mbmac+KCQfIT2mnaSuxD8oIy7DqKeqs+MzKTTa
+sqmmpKzbJhoXDxuwp6aanrU1GBgvW76gmpyvMSEgISrbop6ms+o1Kys7tqepr8pCMi9Evayn
+qa29NR4aFA8vn6efm6fOJxQbTcSsnZqevyQcICgxwJ6dpsBCNCsrRK+kqb37SDEwXrirq7i2
+yjEgGhIXsZ+yn5yvPRoTKcbCp5mapjsdHiUpP6ibnqvuOiwlK3OrpKzDeD0uN/q2p6mvtVgo
+HhYPL56uq5ukxSgSGlzGtJ6YncEkHSIoL76fnaC8PDAnJTe9pKCwzV40Mj3zrKGrt8MwHRIM
+J6CxrJibrS8TGUBK+qSYlqosIicmJDusn6Cu0kkvJitjraSruchKMjBAuamqr8wvIxkOHK2r
+rp2dpfkaGDBKS7afmJ5RKSwpJS5LrZ2mwMVaNC0u26quuri9/Dg137O1u9s+LxcQKMjIs6Gc
+nu8fIzQ1NmWpnKbeR0k4KiM8q6i2trHCPSszzL3Itqu0+T9L18jCw8lbHhAdRDhRrJ2aqTYu
+OyomL82lp7W6wFk4KzDMwM22rrbsPEnx6826tLpxQ0xDPE7Szvn6ynVAKyJI4DNJxMXISUK2
+r8XDx9JOKy09Rf2+qqKsusBUNCwtUb++s6quy0E7Nzc8Rs3HfOTV2M/HaDU4NCcnLDrduq+k
+pbPPRD02Lz/VuK+yr7X9RUNATH3VwcTW21pIWvj4ftlQVEk9UVPMytDCyE8/Pi4+SE/Pur/e
+TD8/PVL8vLa4tLniUT9EXVXDvby/79BIQ05tV9O/QHHYQD5oztTMeNddOj5AOXu547G6TmVQ
+NT9PSbfBw713WmYzSdZBxrnayN1PZuvo12zL8kZ3SkjKzlO3y1LNSTlJOEfG+MKyw3ppQDw9
+ZFnNsuK/0us2Xj9MyvLdu2Rd5U/mRlvvbmrNU8pk6NHGWNndSmxMVGjd1Wu729FIRnE7Tutp
+wMPRX8zuOdZYVdHe6uLVXX155l81uUhHwjq9w1POwFZSZ0TJTj7Fzk6/VE7HPUXuyGruxrvM
+WWz03T9T0t3L723H10Y+zz9PWlv22WPG5s9TVW5RRnVg6ODjxdLfT89OQtJqWLlDucrs8OBb
+7Dpz0Nxb4sFg2WH5f0o+RvTNO8HJS79R0eNWPcpQT3jDVc7KWM9cakjMW1DEadjZ69lf6mxp
+Xl3RTtltXctX3WtOw0XnZGhZ28lMzfZfaXxh2lfZd13XbndN31D3anzMVcjNVuvjRspK1dff
+attZzNI/0PhZaVtf1UvSUelwVE/BTml43NvpUsjpa9FWTtdIeevV2/rNv1HY3T/KP+5vZmTO
+X9j9aGps8VNW3lNm3mXRdNtPwWZkdc5W60rHS9VgXM5mWF3Qbe/63dBd2HLh7eBd9WF09vHv
+5lfaT/1ifE/qW1pp4l1h1FzLU+LQVeTw9ctHyV/NbHrj89ZjctpgVF3z/U/wVs56UNJaYOhc
+3UzNVc5azmFh29pTxlTpbudi/u3oXORG1+BiU9JqZGru29pO4tH+X1jLVNRZ1F3UStxjdE9q
+VdR99ebdWNdWUM/zTdZh11bT8tX3b+Bu9WTi3OVfz23NWmfjXmhUX115Yl/PV9ZM4VlfVOLu
+5Ol+2d1m7mxf72jtz/Dl4ufhYvBZ3FP0WfLeX29v3u/ye+xS3lzSXNBe3uHoa+l7d19eYXpZ
+fPr7bmnyc3Js62rh7PHl6Gb2fOZ02VnYdWXs7Ot55nf4X21+al/u+27uWd587Gn25WjdW93+
++F3Vfehy6XxvZm1k+2L7XOl19/5h4nlm8+ll3WPd79tn1mn2+O5f6GzrbW3d71nzXO5zb2H0
+ZOL7Z+V8dWfnaO97den7bvLm+fZk3fHTZmfpbflt72b9al/y/nJo/f5lc+ls5eVu9Nz3dur1
+6u15bO1rYnxq62Vzdmn2dO5m7vth7ulydvv/Yfna7vft3fB97/h7bHZ3bHlh+n5993llduts
+d/Ppfv1u+/ln7fzx5/Z6cHd4eHDv6u/x/P7u+nbwbnVx+2jxavVub+p9f/JnafppZevm5fDf
+7+zqenvr+Plv5HhXcl5ecmxb8edpbvbod/nr5fXn9Wru7WBt9t/offLl53947e3jX/V3cm9z
+/V1XbeP+ePTcdf3naXRs8Proe+5o+f1XZuXja2fQ1nBsbNtvd/jp5W9tZOplYltyXXJ1e957
+6Nvp7fX79exjf3lcd/J04953+W3i3vFe1nn07vz15GtX8G9tZ2p531174n5m9lpobW52b2bg
+/2zt3/HTzd7P0M7r8HTe/O5dXE9MQT08PTxDSVvdzsa+u7u+vsbGy83KzsrFxMG5s7s2IiUt
+KCIeI0K8vriupqGnv+JYRDYtLDv1zsi/samprq+urrO8wtQ1HxobHB4eIji3pp+fn56iuEUp
+ICIjJzbPsKalqKiqs7i+vru8xNE7Jx0bGhwgJj+5o56bnZ+nwy8fGxwiLE20pqChqa6zxdXG
+vbi8vbC1OB8YFxkeIyvhp5ycnqStyzcgGx4tRc+0p6CkrsfZztTNv6+rrrCrqz8ZDxIdJCYt
+y6CZnq7L8ko0Ih0qzK+xurKlqMVIXb+7zt66q6qusau9KBUPFiItNVKxoJ+u7z5BRTYrLlG0
+rb3JuKqsv3XVvMBrVsSsqa62rapQGg0OHjM/QNCpnabIOzRGXzs3SOe4vMm6qqetzktYVkZC
+366jo6qvqa8sEwsOITZM1bmnnqzbPjRNzU08Qk66sbu5r6yzUi81RNrAtqefoaq7xrk2FAsN
+HUu7vraroqjTNi5DycxDQknYur2+s6+5ZzA1Zb61ta6mpa/PXMvUKRQNFTG7rrvMs6ev1zg2
+zbbUOC8zVcDBua6vulI0Qsu5vMm7qqq1zNe1uSwUDhYzv7fIvaefq+ozNPbTPCYmNN6wq6im
+r9k4LDjyz8m+sKeptr3Cta87GA8QJbyqq62vq7U/Kik1YPA5OVHMrqqvsspAPDE19sKzrK6t
+r7mzt7q44CgWEBtHsqm3yMHETTguOtDHXzY1Pl69tK+tve9JOU/Iv7u+uKyrq6uutcYtGxYX
+KErKsbe8xVA6Pj5VaTs2SVpd0rippLBtPzk+U+m9s6+rq6mpscfhWioZExkzx7TM6NnJ005D
+T9PSTi4vS9evqaypu0Y7NEbNy766uq6rq6ewwdE9KB0VGio2yMfTtLm8v1ZGSzYwMT/Eu7Cp
+qqvCPjk2PlNsybetpqWmp7LFQCogFxceLlG+vbe0xslqTU1BPDk+58G6sK2rsHk5NDpMX+fA
+sqimpqetu8lBJhsWGSMxTcW5ra+7xHlZTz42MDnlwrmwrqmzXzYxPEph3L6tpaOkqK222z8m
+GRUXJTvsv72vr7bKTkBBRzszN1m9sK2tr71MMjE8WMy/taumpKass7zOQyUZFhsrPEn207my
+usXpW2liQjQ0R8i1sLK4v2xCPkNW38e6sKupqauutc49LCEcHSQvPkVM3ry2tr7V4OdjSjU1
+TOG8usPEy+h2ZV3dyL+3sq+trrK2vtVXOCYeISouMS8xT8O2sbjBw8XPUzo5RVn519DMyMnM
+ycbFxcW/vbi5wcnf5flXTkQ8NDM2MzM2OkRg29Xh4M7Iwb7FzM/bcU5BQEhVcWb8z8C5uLi4
+ub7G3OvbzcPKbkQ8Pz43MjAyODo9R13Tw8XJ09ze9VdVYO/a/2/41MK8vb/Bv72/v7++vMHU
+a1BFPT4+Ozo5NC8xN0Rc/vH11cnM0dLQzM3rVk5d28zKysfFyMfCvbi3u8je7tvT61lPTEM4
+MC0tLzU6PEBOdNLFv7/Bydbr6trRz978bG/22si9uLa3vL/Dys3Qz8zWWkQ5MzEvLi0tLzY+
+SV7bxbu6vcDDxcrO1t/4Y1dUXuLIvbq6uru9v8TJycvP12hHPzs2MC0qKi0xOkpp18rEvbq6
+ur7L5GJTUlVp6NjRz8vIwb26u7y9v8TKy87V5lA+NjIvLSwtLzdBUF/kzsjAvsLGzNrobmh4
+7/R6/vXw1L+3s7O2ur/Eyc3S193xVj4yLCkpKy43Pkpd6tTHvLm6v8vX719VVl985uTh3dLG
+vry7uru9wsnNztDPztRpQzYuKikqLS82P1Lt0Me/u7m4vcfad2BcWltgZ3fk08a8trO0uLzB
+ytbf4NrRztxNNy0qKCgpLDI8SV7byL22s7K4v890U05YaXf36+jZyr+4tbW1t73G1ePo6+Pd
++Ew4LionJigsMT5X48zAu7aztLe9yuVbTEtPVl5sfeTNwby4tbO0ub/N4vDu6+LoWj8wKicl
+JisyO0/jy7+7uLWztbnH6VhJRUpOUl5659HEu7WysbK3vsvg+nfz3dlqRDQsJyUnKjA6SGjV
+xLy2srGyt8HoTUI+QEdOXHLr1Mi9ta+vsLa9x8/c3+Pk43xPOy8pJSUmKzI+VODHvbi0srG2
+vc9dRz8+RU9cc9/Rxbuzr66usrvI2/N18+ni6lk9LyomIyUoLTVCZ8++t6+srK62x2dHPz5A
+SVRp/eXPwbiwra2wucTW/Wp3593b+006LSckJCYqLztN68S3r6yrrLC94Eg8OTxBSlNv3c/C
+urOura+0vs3ncGl74tnZYEEzKyYkJScsMz5fz7yxrKqrrrbFckU7OTo+RlBo3cq9trCurrC3
+wM7rZGzo4NzcX0AvKCQhJCctN0rbwbWuqqiprbnQTTkyMTU8RlJz1sW6sq2srbC5xdxrYnPj
+1s/WXz4vKCMhJCguOE3XvbGsqKirscB+QDYwMTY8SF7fyr62r6yrrbG6yN9kWF/l1M7eUDks
+JiEhIykxQHTItq6qp6irtMVcOzAuLzQ+TWzWxLqzrqurrrS+z/9dWmzczc3mTDUqJCAhJCo0
+ReTEta2qp6msuM9MOC8uMThDVuzLvrixraurrbO/1WxYV2bk0MzfSjMoIh8gIyozRte+sq2p
+p6iststOODAuMTdBV/LOvravrKqqrbXC41pOT1384N9tRzUqJCEiJSs3TNjAtK6rqaqststX
+PDMvMTc/TnDQwrqxrausr7a/zvZbUFJcf+D1TDouKSUlJyw1QWjMvbavrayts77lRjo1MzY8
+RlL+z8C2r6yrrbO8yOReV1xfY2puWUc7Mi0rKy0xOEFU7M2/ura1trvC0WpORUE/Q0xe5MzB
+u7a0tbi8wsnW+WhkX2BjYFxLPzs3Nzk7PUBFSk9WYHXm0crJy83V3N7e4+vl5d7a083Hw8LB
+wcHDxMjO1OPscF9YTEU+PDw8PD09PDw9PkRLV3rVy8XDxsnKyszMy8zO0NXX1c3Kx8fGys7P
+1dvh63tiX1ZNSUdEQT89PTw6Ojo9RlRw18nFxs3Ny8vOzcvOz9TZ29vSzMfHxsfKzM/Z397i
+3+9kV09LRj8+PDk1NDY4PEFNZ9zMyMXGx8bGyMvLz9TZ5urb0s/LysjFw8THy9PW3ersbFZM
+R0E8OjY0NDM1OT9LZ9nKwb/CxsjL0NXX3d/f6Oro3tTMxsPCwMDDxcjMzdDZ43BURj88ODQy
+MjEzNjo/S2TZx8HBxMXLzc3R1NLZ3t7g4NTJxcPDwcHAwcPIzc/R2OlgTkQ9OTQzMS8wMjY7
+Rlrlz8S+vsDCxsjM1d7j3+Df3+HZzsnDwMHCxcfKz9PV1NTcak1FPjg0MjExMzc6P01u3c3F
+wb/Cx8zR2d7p6evq3tnX0szHxMHAwMHGy83PztLZ41lFPzw3NDIwLzI3O0NX6M/Gwb++wMjM
+09/r9nd9dfjr3s/Iw7+/v7/AwsXJzc7U32tQRz45NDEvLzE1OT5KYuTNxsO+vsHIzdfl9HFo
+YXHs2s/PyL26vL+8u7y/w8fFzGZAOTAtKykoKi41PUv3yLm1s7Kzt7zPZ1lLQUBCR1dx6Mq6
+tbSysrO1u8HCy93fcEg8Ni4pKCgnKi0zQW/YwrWzsK+zur/P6W9NQ0VJSVrr0cO7trCws7W3
+vMHEzdrZWjs4LyYmKCQlLC82T9rGtbGyra63ub/XdE5HQz8/TFv4y724sq6vr7G2ur7Ezdta
+PjctJyglICcrKjdZXMqws7Grr7e3wdlzSklIPkRRT37FwLyyr7Cvr7W5vcbM3klDOisoKSQg
+KSkpO1xewK+1r6uvtbjBzWlMUUA8TEtF3sfMuK+zr62ytrm+x8vfTEE0KSknICUqJi5GQGq3
+uLaqrbSvuMnMZk9UQEFNQE/U38m2uLStsLOyuL6+wvxVSi4rKyQgJiclMj5A67q7taqts6+5
+y8xbTU5ARVFEV87cw7O3s62xsbK5vMHLYUw9LSsqISAoJCY6O0LBt72uqrGxrr7Sy11LTkZA
+SUxf18y+t7SxsLKzt7y9w87uVj0vLSgjIyckKTY2R86/va6ssa6uvsbFUUtYQD5RS1LQyMC1
+srKvr7a5uL/Mx2JGQi4sKyQjKCUoNTVG0Ma7r62urq65wsD6TlhEPE1LRt7IzLmytrGvt7m6
+v8bB3ExRNystKR8nKSMvPjdbvMa5qq+yrLPJwdNJVlk6P1hDWcPIvq+zta60vLi+ycPLZ1M/
+LS0pIiQnJSk6OkjOwr6vrbKurr3Dw1ZMXkI+VUpSysvJtLO3r7G7ubzIx8npVVI5LS4pIyYq
+Ji08OlDHxL6urrOuscLGzE5MVz9AWEhSyszFs7S1r7K6t7zIyMn3VFQ6Li8pJScqJyw6OVLK
+wr2vrrOvssXNzEZFWT1AfU9bwcfEsLK2rrO7ub/Ozc/pT1M+LTErJCgrJSw9NUvIyr+vr7Su
+sMHLyktCVEA9ZFRNyMPHsbC3r7C5ubzKzdPsX0dBMi0uKCYrKik4PD/eyMq3r7Wzs7vLyPZH
+V1A+Uu5R2b/KvLG5t6+6vrvJ29LkaFRKPC8wLCgrLCkuPj1Ux8S+srG2s7TC0c5XRVpFPFhU
+TcvCyLWxuLO0vr2+zdPX715PRjgvMispLS0sNkI+dcXIvrO1ubW5zNrRUEhdRD9fXVjJxMS1
+s7eytL2+wszS3OdkSkc3LzItKS0vKzhHQHbHyb2zt7i4u9Te30hGWUdG72lvwsHAtbS3tLe+
+wsbO2t3dWU1LNi8zLSovLyw5Q0Bgysy/tba7ubvT499ORVpOSHzr9si+vriztri5vsfKzNbl
+62hMSD00MTAuLTAwMj9HTODGx7y0ubu7yOTxW0tPUE1acd/Lw7+6uLe3ur7Ezt3udf76aU1J
+QDc1NTEvNDQ5P0ha3s/FvLy8vMDM419UUVBXYm3w1snDv7u5urq9xszZ9HVwZXP1Z1JIPjo5
+NjQ1Nzk8RFFm3c7Jwb/Dx83leWtkYnzx7t/X0MvDv729vsHEzNTc5O/683zzblpPRz47OTYz
+MzU4PUdTa97QycPDxMfL0tXb7PXv6+HVzcnDwMDCwsbKzdHa4OPu+/P0bFtPRj46NTIzNDc7
+P0ZPaOPRzMnGxcjLztXa3d/f2Nvd0s7Lx8PCwsbJy9DY2t/j4d3ceVpORT05NjIzNTY5PUBL
+Yu7WyMLAv8HIzNDW4ezr7Ond1tHMyMbEwsPFyc/X6Pz38O/1alxbTEE+OjU0NjU3PEBKXuzU
+xr+/vr/IzdPd7nd5ffHl2dDMxsPAvr7Bx8zT3ef3fG1oX1ZMSEE9OzYzNDY3PENJWd/Nx7++
+v7/EzM/X5fJ7bXrv5djPx8HAv7/Dx8rT4O36fHF3929YUEk/PDk1NDQ0OT5FTmnhz8S/v8HF
+zNDX4/1tb/7p3NPLxL+/wcDAw8jP2u7x7O34+v5eUEpCPTo3MjQ2NztASVXt1cvBv8DAxM3Y
+2+V6d3j969zOycfFw8PDxMjL0dvj6ubp6uPvZlZLQT06NzU2Njc6PkNQdeLNwr/Bw8XM1dze
+4urk3ePf1c7Kx8XCwsTHzdfj7e/29fvwelxTTUdBPjo3ODg4O0BET/jZzMO/vr/Cyc7U2Njb
+827+5t7d29rOw8PI19LL1OH+z87c+W7iXkI/Pjg1Nzg4NTpGREJYzcS+vL27wc3M0dXr8G9k
+cHR6b+PJwL/FxsTFyszP3OP4+nZPRkU+PDo4NjY5PEFETWFz18vHxL69w8vV1NTncGttanP2
+6NzRysPAwMPIzc/X4Ot0Wk5MRT06Ojo6Ozs+Q0lQX+7e2tPJxMPEzd3e293q/Wv25dvSzcnI
+xMPDxMjMzs/X1uJ4alZIPzw7OTc4PEBESlRu5NXPzMnIy8zP1NrsaW1taHjp5N7UyMXFxsjG
+yM/S091+al1PSUQ/PTs6OzxAR09cZe3Y0M7Ix8vO09fZ4uPi5eDa2+Hd1dDLy8rJyMfKztLT
+2t/i/VpKQD07Ojg3ODo+Rk5j5tjXzsfHy83P1+DpfXnz5N7b29bOysnKys3NzM7X2Nvi621c
+UUc/PTs6ODg7PUFIUHTaz8rFw8jMzM3U2d3r8Hj2+33r2dTTz8rMy8zLztHP0NHc9G1dT0U+
+PDo4OTs9P0hPYPPZysfHxsvO0tjk7X5jbnNvf+/e2drWz87NysvQ1dfZ19nb3fdgU0lCPjw6
+Ojs9P0hSY+rPx8O/vsHGy87W4/VtYmNmZ2z+7N/d2dDOzcvLzc7P09LX5HpmWEtFPzw6ODk6
+PUFKV2vr2c3Hw8HDyMvO1+f6d21ucW3y4t7a1s/LycfJz9bY3N7l+GlcU0pCPjw6OTk5Oz9H
+T1r41crDwL+/wcXJ093l6vV/dmly7+vo3NPRz8rMz8/V3uTi5Ol+a1xPRj89Ozk5PD0/RExa
+bd3OyMTCw8jKztjf5u757+3q5t3a19PRzsrJys3O0dXR1Nvl629aTEE9Ozg2Njg7P0hVXfjS
+ysW/vr/BxMrV3t/t9vb67uvh2tTPzMjGx8rMztHY3+d7bVxORj86ODg2Njg8P0ZUcePRyMXC
+v7/CxsvT2d3o7e7s7Oje2dLPy8rJyszMzdDZ3el7X1NJPzs4NjQ0Nzo9Q01bdtvNxcG/v8TF
+xczX3tzZ3ufh19LT0s3LysvNycnLzM7S3GtNRT85My8uLi8wNj1IXtPDwLu4ubq6vcPK0O5t
+c11bb2/22c/NyMLCv76/wsbK0ORlTkY9NTIvKywuLTM6QVfhyL+7t7S3urm/ysvga2piXV9t
+c+TSycHAvru9vr/J0N/xYj86Oi4tLSkrLi43Pkzky721s7Gvtrm7xs3aaWROTlJPWunUyL+8
+u7i5urzAydbrV0E9NiwsKiYqLC42PlDexLq1srCxs7a9ws30clxOVVlj9NrOxL66tre3trvD
+xtxOPjozKCkqIyYrKzA+VufFuLKvrq2utbe5yeTccktSVE9b6dHJwrm2trOzvMO+5EJCOiko
+KSMiJiktM0Hmyryyrq6tra+6ur7a9u5ORlBfYvnNw8C5sbOzsbi+vuw/PDcoIiUjHyQrLDJJ
+3sm6r6ysrKustbi5z2puTkBGUE9Z08bEurOwrq+7uLhJN0cvHiEqHhwpLik218zIr6uqqqqp
+q7i6utdUXE4+P1NYW9TEv7ivrq+0tbdyNUk4HR8tHxolMikt78nOsaurp6isq667wsN7QEVM
+QkVi+9vEurawrrC3tLZcNEM0HR4rIRsjMi0vbsrJsK2tpqeur6uyzs3MSkBSTEZU7NXDuLay
+q7G+tL8yL0IoHCIoHh0qMy9Fz8u1qK2tpqaxta24/On3QkldSUvXydC9sLCvr7q4uzsrOC4e
+HSUkHiMvM0DUzLqop62rpai0s7bP82ZFRVhWSvy/wcOzrq6yurbINC02KB4eICAiKC01bsDH
+taalq6qnq7W2vWhcZz86SmJTccK8uq6qrLS6tcswKzIoHh4fHiIrLjBau7q1qaWnqKittLfA
+XUhKPzxBTVvdwLm0rKiprbi8xToqLCceHR4eHykxOFe6s6+opaeoqay3v8RfPj89OTtHWd/B
+ubWtp6WosbfAPyosJx0cHh0dJS85Vr2yraajpaeoq7O9y1I9OzU0Oj9P2L+6tqympKattr1F
+KCknHRocHR0jLjpaua2qo5+ipKasucXeQTU0Ly01PkvWu7SvqaWioaazvmsqIiYeGBocGx4r
+O1+4qaijnZ6io6i1ynQ/Ly4tKCs1P2e9sK2ppKKgoKq3wjohISIZFxsbGyMxRMaopKOcm6Cg
+oa3C5z4tKikkJS43RsGvrKegoqCeoq/DTykeHhwWGBwaHi5C1qqgn5yanKChqb/yQCwnJyMj
+KjJAzbKrpqGhoqGhprbtPigcGxsXGR4eIDRfx6ien52bnqWlrM5MPiwjJigmLT9S1bStraqn
+q6+sqq+6yVI5KyQkIh4fIh8mN0XlsKmrpaCmqKauvspVNTAvKiswNkLXvLOrqKimpqaosMhT
+MyYhIR4dHx4fKDRFvqupp6Gjp6Sntr/UPTIzLSsyNjhXyr2uqauppqiqqrbVYzckIiQeHiEf
+HikxPsKvrqqmp6emqa+5zEs9OzU1OTY5S+nDtK+tq6urq62yveo/LigkISAgHx8kKDJaxbWr
+qKinpqeprrbIaUc8NjUzMzpFY8a2r62sra2srrO87zktKCIgIB8fICMpO/q9rqmnpqWlpKmt
+ttVNPDQwMTE0OUbiv7Ssq6yrqqutsL4/LCokHBwfHR0hJDPXsaqkn6Cio6SqsLxONS0oKC02
+R+zaxq+opqWpqqutrLR7Mh4VExYaHiInNFOynpmWl56nuV89LCIdHB8rU7mooJ+fnp+ip7d0
+PjhI1dlPNh8XGBceLi5I1b2mnJmZn707KB4gJCQrNFWsn5uZn6288HDbW0hEPl60pJ2fsVAd
+EQ8NExsjRbahlo+PlKBaIhgUFhogOb6flpSWnrZQLy00PEjoxLGknpucsUMeDQoLDBs0vpyW
+k4+SnK4wGBAPER47rpuUkpWeuzsmICQvTL2so56dm52myioZDgkKDhpypZeRk5mer00mGRMU
+HDKvnZWVmaTINiIeHypFs6Sdmpyeoqu9QyIZFhUTFh4pvqObmJ2qyEksKyUjKjjQqZ+eo7hm
+LyopLT/Sr6WfnqGosMDzTj4uJyoyLiEcGR89sp2cobttPTk7LCoqPLqgm56vPisoLkbYva+r
+q6qvu8bQ19bb3mHdz8tYGg8NEiusl5qbufxsPD4nIiREqJuXn/stJCtB08fNu76zr7Svr7G4
+zk9ITs+urVoYCgkOLqKTlaG+P2NaPy0iK8+flpmpOR8dLVzAuczMxLi1rqmsq7dGNTRzqqao
+0hoJBwwcqJaTmqzdP1gxKCYuxZyUmKM+HhwdLGDGs6+srK+wtq+yw0ItK0SmmJerKxMFBQ0e
+ppOQl5+/PC0gJSpqopqXndQmGhgfP7epn6etuVxg/8e+00A4SbeakpauIA4FAgseqY+Mjpiz
+LB4YHCzOn5aXofMiGRkkS6uhpKvURDxDzK+qrb5OQeavnp22MxwODAwVOKSPjpOmMRoXHzG2
+n5ybo78vHxodM8yhnKe1Pi443a+kpLVvMSw7yaahq8gnGhEMEiW1k46TpUQdHSg2v66noqS1
+OSAbH0Sqn5+zPSwuTbKlpazLNywqNs+tn56rajIZDgsPK6WMjJKuJRYYIzu1pp2co80kGBsn
+waGiqdU3N1HJsqyvuV8uKStFr56Zm684Jh0SEBEftpKMkqkgFBYmy6ekqaqvwTYhICZYpqGn
+wjMsPcyxqbDCYjYwOF24pZ+gq0wqIhsVFB1BoZOVoUgfHSdJv7Gxrautzy4jHy28q6m2SjpP
+yLetuth0Qj9DS82soJ+jwC8lHBQPFyuuko+ZtiobHy8+1762pp+oeCgcHTu5q6m9VFzFvLO8
+VktDPlXfybGmo6e5OS0lGRIRG06bjo+cVB4ZHSU01q2dlpmtKxYTHEypnZ+twGhNPDo5Tb2y
+rrjYeMatpajINyQaEw8ZNaeTj5m7KxkZHy34qp2YmKc0GxMYMrOdnKK0bEAwOD/msqmotGYy
+NsWqo6tdKhoTEBkqtpmVmKlIJB4hKEO/p5ybo90nGhooWauhpKu//kVDRV++sau02T49vKaj
+rkEfFhIUHjbSo5udnrI+KiMgK1e8pp6jrustJSMpP72qpqWvvudBQlHVuLK4xse9tbLOMx8W
+EBYiO7Gen56jutg+KiUoLmOspJ+kvFgvJSctO86tpqCktuc3Lz/curK1t7i5vOIyIRYRFx40
+raGem6aut1g3KyEhLUqyn6CjrksyKSYtP8+toqKksHI/Mzx1y7WvsLS/WTMjGRUVGCjHqZqZ
+nqKyyFU1KiEfJTfEp56eordKLyYsOWW2rauts7/da09W2b60sbhdLx8XFRgeLdGuo56fo6eu
+vvw7KSMhIy5Mvamioqi04kI9P1nLwLy8y+JvWHrGtq6rr9I3HxYTExgkSrWjnJyen6arstU6
+KR8cHSEub7Kkn6Cmr8Ln5+TUy9xaRz08S+e8r6urstgwHxkWFxwnQMGspJ+fnp+iqblZMCQe
+HR4jLUrAraalp6qtrayutslPNy0qKzNHzbavssNTMiUhHyEpMUfOvbKqqKakqK642EY6MCws
+LTA6SmvWw7qwqaWlqK687T8yLS41RXvSyNFZQDMsLCssNDtM0L21r62vsre9v8rcZ0Y8Nzc5
+PD0+SHLEsKmnp6u0x21GPj09RVN83d9pSjsvKyopLDRBace6s6+ztbe6vLu+xthdSD84MzEx
+Nkbgu66qqKqvuchzSUJDSlVcYm51YUs7LyooKCwzQPXCura2t7i2tLSztbnC7UM1LSkqLTla
+xLKrqamssr3L2m5gU0xKSU5YZVlFOS4rKSovOkhy2MvFwb26tbCtqqywvWY4LCgnLDNH172y
+raysrrS6v8zzUEQ/P0JOXGJTPzQtKysuOD5PcuXPyb+5sKypqKuwvlc5LSkqLTQ/XtC9tK6r
+qquut8lzTEI/P0ZMUEtANi4sLC4yOj9JWGrgyryzramoqay2y086Ly0tLjI8UNS8sKyqqqyx
+vdFhTUtGRUNCQ0NEQTszLi0uMDpCS1z20MC0rKimqK69eEM5My8vLzU/bL+vqaiprbK4vctr
+RjcwNDgzNT9KccjMZkk8MjI7Ojk8Oz/iuK+qqrC1uMHXZUE4Oj5T1snEvru7trW7xtpoWVlN
+Qj46PEpt7+PXysT+NSooKCswMTRB9burqKyur6+vttBJPD1Pb1xZX9i9trrJ1M7IxdNOPj5D
+SkpHRVB89N/V1NjVXi8oKywuNDY78cHCt6+vrKuvt7zJ4nBPSEtNTmnm38y/u7m7ws/lX05J
+Pjg5PD5DTV/v6WZeTTw4Ni4wPD5J4NDAtLKwra2vs7vH3lRGQ0ZMVG3Rwb25u8XO0+tZRTg4
+Oz5BPz9YcUxLR0FDRENGR1Dv2trPxL26vLy4uL/M197tXU5SedjQ19fQ0c7M3HdhVUlEQT5G
+Rz9NaX3+ZGVwWUA/S0xJSE/oy87Ovrm6vb++v8nb3utqdG5v7fVhXl1aXV1aaV1OTU5QU09U
+Z+Xk3+Rsb2dncvdeVlxdYfLj39jU0tPb19bb3+dtZ2ZibWzn7O3f29zf3nVeWVZUWFtm6+jx
+9u7s8XFpYWJaU1dTXnLw49HM1Nnb397e7nxkWFVcZ291YWRrdHrv59/g6fx49f5yfvvx6+js
+6OTrfHtqaWlfX2Rqeujc3NfW5+jb3uh2Y1hVW1hUX19deOv7/Phxal1veHL28urn3tzV1djd
+7H9vb2d1b2pleHBg++LZ427o2f5z4vVfanhxYWNeWmNeZvb3aXzsd/p0burx9vb429nZ2uF+
+Z3ZoXGdiWl5eXHXf4t3W2NfeeO3hd3t0aGj/bmxybnpu+/v6b/v0c/7/bmp3bXP3c2Bi+Op+
+bG1sZ2xvffjt79zY293Z3ePc4ORvZ15eZ2Rqbmly+fLo73v99HZ2bW5nYGp38ezr6Phydmpl
+bH1xamz5+HXz6u3m6+vu6Pjtf3d3aGVscfzf7fx47vn6fGtwbfvq3OLi3N3f6nZtal5bVVJV
+VlpdYG7c2+jl6N/a3uTf3+xgXFVSWnDp4N343tj3+e5+b21fZWxma/L07e/s7v/77Od67fJ6
+dHD8bnJ+/Xr8bF9uT2DX1lxf1uBpbnx4bWZiaW9gau7yfHbn91ZK2sha4sLO29rk8eVqa2BV
+TFjw5HZI7r3XVvvS1P1OUP9eT1FJUdjyaO/b0tXvZ+h7b2ZeeOt9bd3h6Oj19Pjr9OrucP/o
++Prl3Njd7Ojd3t3e6ehuW1NDOj0/PURLUuLLysG9v8DD12RYVk1ISFFq6d/bzcO/v7++vr7A
+zM/X7Vk9NC4qKSstLzxZyrixrqyssLrJbkg7MzExN0Vg2MK5tLO3vL3BzeDxfurf4NLNzMTQ
+QzArJyYmJio6bMO0ramkpau0wnpDNS0rLDE7SurBta6srrG0u8xqTExWWV1v2cXFx8JfMiom
+IyQlJzNlw7GqqKWjqK+9dUM3LCgqLTZHecKxrKurrrK6z1xMQ0BDRlfh19DKxMXIz0ksJygn
+KSsuRca4s66sqaqxvd9OQDUtLC87W+bGs62rrbS7vcp8VUQ/QkBDTmncy8vLytp8d/PpSS4q
+LS8zNDQ/6si9ubmzsLS6zWRgWEpMUm/OysvEwMDBydXgbVJOR0lJSVFf9d/S0M7PdVpbXlhL
+R09jX0xFSEpMTEtVet/e49jOy8rR1M/T09je3NPY397c3NvV08/e1tH0XlBYXlZOTllfYlZP
+WWRfWWhx3N1scPju6VtSVFBPVlZUd3dq/XHez83Pz8zKzc7S1+zh0uH3enPt4fx8bV9rZFNZ
+6H12amBxWlxXWn9qa3fr+mlYVlp+5n505N57X2xnYXlmat3h6u/b2tjQ4NTice/sa1di/W5r
+5+bc0+nd/fL+cHFdXWNfbm36b/92dOddbub3ZntwYGpcW11qX2Zu6Pb+2tn/ee/t3PtdX/P7
+6fj25N7x+dLc4+3c9+9wZNt67+3lb/tpUl9hZl5bZvpnZndcbuhrXPpmcXhZcGjmZuj459zm
+59bP4fH+2+pq7/xoZdvm7/v639n3dGbkXVVPdVtRZlhk5tXh2+3e7WJoVFheWlRf9eTa49PP
+1958127fYHhmfWBieXvtWeF+3f/n42xzXPNk43zraOricfLr5/HkaXRqa2BbWPp9ZeXf733o
++nXwa35s/mVman9+7ev3+NLh3fnf6m9oYWB0eGrhffr26P328ebhaHNvX19pdvdd49xmY3jv
+3eX05etlXlNw/Gpa5N7x3n7X7nxeZlzlaV3qbvDv2uvn4vV+fuXte2r0bnxc89htYPH67fjs
+8mp462hlbHlrcWdp7u9+/evu/G3v+ebxefX06+3z//To6v59ZnBnbPF0Z3jn+ntlb+zt8Xvz
+7Ot9fGJteHdjdfno3ur7bHF47evcZ+PqZGT+8Gzs7XBtdV97cHNn29z27Ptva/5z73bs8HP9
+eH9o/3z6emZ7+Hl3amZt/evp5fLtfnr9ffH6+PJ8+XZn8u3zdNnp6/RsbP59Z2Vk7W1nXH7x
+ZWf34eTrb97+8Hvte/lpb297c2bv4+1t5ex3d1x94un7bHjm8nTo3tx9eHZ+emJpaWV5ZnXo
+82Nl8/tqWnRzZW9p7uHf7f7l2Obi6vNx8e94/ez9bOp+fvVn62v2am5taGV1e238YHRvc2Vg
+anhv8t7e5Obs6N3t6f7d8u954XRnc3d+c31gc2d1evjudXH3cG9t9fd85+3/Y2Tz33T4ePP2
++GZs6Hvtetvx8GvmeXf5cG5742bq3+h2cnp8al92/PNs9m5ndfT0a3j0/mjofe/+a2t27PTv
+7NhzfN7xaWP/bvf1ZWrl9f5r8u7uaH7qZ2hwfPd47Ovy7vh6Xuf9Xm5l8e/3ffj+/25y5Pf8
+ee3/dXvr5n5se27j/vpv+3nv3/V0d/9nYGXwae3+6Obi6fbteWxa8ltbVOxubnbv2NnTYu96
+1Ft98ORtaXRd1+rkatbj1/7/7mddT11Y6m1tYmFdWFdZZlpdXWVr9uLRy83KxcrQzMbIzMzK
+ycrJxdVWNykpLS0sLjRWvbKvsba1tcDyRzk1Oz5MWu7HvLq5ury8vL2+v77AwL7kOisiHyIn
+KC9By6+oqaquuchnPC4uMDtK6MO8ubq8wsrY3dnVxru2tK6tsNM2Jx8dHSQpOnS4qqaqsb9a
+SDk1MTxI586/tbO4ws12XktUdc68s66trK+0v0cqHhoaHys57b+vqKixyUo7Pj5AQk3bubi5
+t7vB3ks9PkBZy7uzrq6ytrq+xGIyKB8dICo+/MG7sbC5x1lDP0lNXV3kyri5xdLy+Uo8Nz1W
+ybixr7K2urzCv8LFXDUtIx8kL1HZxce7vMbeXUhQ+v3gZfbYwcLaWU1KPzs+Zcazr66us7rD
+yc7NxsLPRzMmHR4pQW/f27+3tb/cUUxnWE9DSt68uMJxT1ZTSUNdxLSvsra7wM//XvTJvba6
+5T8lGhwlPWZ017mvr7v2RD5NVU9IWsqzr7vfTEc+OjtUw7Ows7i5vdpi/cm4tbzG1E8wHRYb
+LOzL1dC4rq/CTz5G6NLcc+rEtrrTSDs7Oj5LzLavr7a5v9NcaNTCv8TGvrzxMh4XGyhK0drJ
+sqqsvFlFSmTpVT5F+7+5yGhaXFJJRWq9srG2u77G2OTaz87Y39vDwD8nHBsqPlBxzrOnq8BL
+OUJmY0k6PdS4sbjKYU5JPD5OxbCusbi+xN1ZTlBffuvPvK+3PikdHSs1PEXcr6euy1VR0s5T
+PTdCyLu/wszR5Uc7QG69tba2vcHPb2P45dnazsPIwds2KR8gLDRBZcavrbfM/WvuVT8+RGu/
+vcG+x9dhQj5K5b+7ubrByNXoe2Nf5s7GvsO8zD0uIx8nLT1lxrKtsr7Q6mxJPj4/T9zAu7m+
+3FtGP0dky7+8vb/DzdHX+mx31svHwL+++zovJCAoMEhp1by0tL7ZbV5XWEtIT9/HxMXWem51
+aW/50MXEyc/LxsjU5OfXzMzMxMTMXjkvKScvOEBFX8u7vMbMzczYYUlETV3i71tcaNfQ0dHJ
+wMTO1dvMyMnO2drY2dzQztLbc0xGQD09Ojg1NTxGUF3rzsrN3OHWysXL2Ovp6/f5bf7g09jo
+1svDw8/me/32eHjezs3N0NDT42JLQUI9ODg9QkdFQ0FHWerPzcrFwL/I23xfZ1xMU3TVxsLB
+wsLGy83Q0tXW19nX19naa0lCPTw7OjxBRkM9Oz9NXnns1cS9vL7DzNnf71NKVu3X1NTMx8LC
+y87Pz9Lj6djPz+D6Z1ZMSEJBSEg/PD08PkFAR09u08vGwcLDx9T2cHh2denX0tHQysfK0dXV
+0s7S08/N0OpmVk9XUEpNTk9ORj07PTs6PENW7NnQysS/xM3W5+ra3Xtu4dTU18/MycjN0s/L
+y87U1t1qWVNSX/d0XV1dUUU8Nzg6Oz0/R2Lj18/LxsXKz9rf1tLZ3d7T1d7k2s3Mzc/LxMTJ
+1uXyZVdWYWz86N9sT0xNRTo2OD4+PkNObd/m7+/XzcvR1czIy9vi39/f4ODVyMbIxcnHvtNi
+6u9pW2/r3utrc3pPPTo/ODQ2Nz9IU1tw283Ky87JytxjadXP62vWxcXQzsG8wczJxdD7XnLk
+6Gpv2c9+RlPP5Vo0JCc3LyoucLPAT8uusXFB07nDT1q7schey7fATUvGvOxM373HU1DOzFNE
+XORPO1XZS0VQ49tUVsVAJTdHOTM1ybZVSL+1vlXZsbtfX8W5zF++sr5ibMPGXU/Nx3dPa93t
+Y2htS0xOR05MTlRQSV1PNTo4Oz48QWRv+dfCtry+v8PIzt3Kv8fMzL++yM3IyO5h+fFlYnXy
+b1xaR0VNPj9GSEZPTkk+PEI/PUPz083Zy8PLzs7Cw9LHvsnLysvI52bf4O/67dja8trdXV9o
+U2tuT2JMRXtQO0pWPjxJbnx5Zs/VT1Hr1t/bw73V6dbI1lzlytV30svXbXTR3Ors7/JdWvZe
+UHVeaVlIWP9MSkZGWUhN8NTU19vsZmnY2evWysjeedzS3eHSzd5s3NNuU1z+7ldMZ25SW+rq
+WmDfZFlc5ttPVfxYUUz902j6zt5dVenXdO7IzN/Z0ehl9NXTemnV03x2z9NiYuxZTE5SV1Bg
+7Vht1d5sXfZ6SERUe29Q99Xebn7a5WHh1Nzt6cnV7tHM2X/Uyull3d9gWezmV1vl3GFGX3JM
+SmjvYVZgcVBKXdtaSu3RbWfl319p2d/j19DT1+Xj4+3j0MzS0dbxbmVndmNnePn1Zm1d9PpW
+bUpJTExKZNn2Wml+TFFv3OLu4trdaF/mz9rj1dHg3d3n3Ozaz9hrY+ztX+nN4O3r+GlOSFtv
+TU7e1VFb0XRP8FRLTk5SS1zedeft49FhWNvR4dzN0t7Sys/Z0dbf2uj03err5lpf3GhHUltP
+XujtXFJOSkxLY9f2cVlLT2xTXNzQ2+Xd2N/x0c7U7N/P1/ney83PzM/b+F9xbFDrUkviS07N
+6Wd5TlE/OmJYU3Pw4E1CWmtVZdbEz/naz+X008vO49jL1ebTycfL0tHsaGT+dOxlTH1dPU7W
+/1c/TUkxOVVXXnzh03tYZV5gzsPOz9LZ4fncx8HN1dDR3NrAxM/R7OhORVFez99a7u1FODA4
+PDZK92/7dmprXWHk3NPPysrLysbFy87P3fHRyMvIxMDOWVzoTDVavWdG2M8/Ly03OCw60uxR
+07y/Z2TBzGzhx7zJ1ry3w8vBvupT7M3Vbc2580NW9s9HQD8tKSovMTrjyt/9zcdtWdbHv8bA
+vMLRy7q4v8m/x+Jz29n22NLO0GphxtgaGl8tIS64p80vyLBJL0ewuUvNq7pu67Ou52rBvl5J
+y7bHY8S8y/3cxz8VF2ouHjmon8UxvbQyK0G0t0/HqLpX0LCt40zHzklPy7W20MW/2XnTyikQ
+G1UpIPment8+srYqHUSpzzqwn7M+dqmsQjfDuz43v6m02bavz2XISxUOJUsjKqqUnzs/rcwe
+GzuouEStna1HXK2uPS5svX1Fvaquxb+3vbw5Ew8hLyAvpJOdaU+zbR4aMLm//q6fq25ptLBJ
+NFHmZnS7qKq2uLa4Uh8UEh8uJV+dl5/BXMU7HyA0ybvIraKxd2G9uE01WdNZermpqba5r8cq
+FxIaKyw0q5eaq8zfVCYdKUvDurarqL5Z3MDLRjvowt7PtKeqt729NBUPGystMrOYmazb0E4o
+HivSubmvqqzCTVvIz01DYsPCz72vrbG5zSwZFxwkLkC1nJ6tvc9IKCAx2My7rKmxcU3MzXt9
+2L/J7L+xtby9srspGRofIyYzuaClsby77SsiMP3QzK+kq8lsy8NeP2rAvszDsa6zt7/PMhsY
+HCcsMt6joK7GyNU0JSpNxM29qam45+TF7D0/07u5uLGrrra/9yodGh0kKjTOpqSsub70NCUr
+QerSvqqps8PDxOs9OmvGv7uvqau1u9cqHRkbIyks26Sip7K6wjsiJjZY5M+soqy+wsbaPi9E
+zMW9r6qorba7MR0aGh0fKEinoKKlqLNOJSEsNkFVsqGksre2yT8rMl/V27yqpamvr/YmHRkY
+GiEvxaOenp+nvjYlIigxOHytn6Spq7njMigxR1Dltqijp6uuUyceFhUaHyy+ppyYnKW2PSYi
+ICQySrmfn6Okrc43Jyk3Pku9qaOlpai/Mh8WEhYZH0CunJWWm6K0OyMcHCMrN7efnp6hrMI/
+KCkvNEbKr6akpKSySyIXFRYWGyvBn5mYmJifujcoIiEfHi7EtrWppqeryeTIUjg5P/DI0b21
+xWIyJiotKy03TMu8vLSur7CzvcPOWEI8PUVEPlHRyMziV1NNQkNW3cnCxsnIy8rExsnbWElG
+QkJJUfjb5GFORT4/SU9i5M/Aw9HfalZdWmjX0NXS19XPyr67vb7J1/FQQ0lVUUhAPzs4ODk+
+UOvczsbCvr/S3M/Z69t+UldQSU1bTl27uc24r7u6sLi/wuE6KCImKSIkMV7Au7+0p6eutri5
+zT0uLC4xLi5Mt7a/r6aptsnWx947OVNdT/u7sLXMPyolJB4cITjDrqegnJ2mvlxDMichKDz8
+zb+xp6i65N/qPS0vRX7gx7asrLa8uMBYOisfHB8mKCtYq6Cfn5+hq9E2KiknIyY8xba0tLCv
+vldFUUw5NlPKx8e+ta+1wsW9wm1GQjQkHiApNDlMtKOfpKqstHwvJiUqLzU/0a+qsLy7uMlL
+O0Jk9FveurjNWF/qaElK8cW/ycm8x0EwLCUjKi8zUrWpqqunqrncRzcvLzAxPtK2sa+trbTZ
+RT42LzE4RfXLvrm4trzN7Hnv+O3W0dztTjMoKDEvKTHPrqyuqKWsw1I5LisqKzNeuK6vrKmu
+xE47My8uLj3LuLm1rau10OrpUEBEZcrNYF5YMSIjLi4oNLunqamioa3YPzEpJCYrNGqzqaip
+qK3CTjw4MzAxPl7GubzCu7jJUlHd5F3cubPCWU9GKh4jLy8sQ62ip6upq8FFNC4pKC89Tsis
+pamvs7jcOzEzNjxP7NC+tr7T1NJYP0R51vLnxbexut9KPCwhICouLz+5paWrqqmzWzUwLSkq
+OGfAr6ekqbC71D8uKywvOVTPvri1ucb0UElBP0hf387Du7Svsr9eNychJSsrLVCtoqappqe7
+OiopJyQoONq2qqOiqLDBTC4mJikuPtm2rKmqr7rWSzo1NDdCaMq4r66ytrzxNCQdHygsLTu+
+paGorKy0dDIsLS0wQNi7r6ilqrfJbzssKSwyPWnDt7Kvsb7kU0Y7NThFXd7DuLO0t7nJTDAl
+HyYvNDRSrqGlrq+wyDssKywsNl7Btq2np6/F7Ew1Kyw0P1XMuLOys7jJX0g/OTc9UfTbzL63
+tba71TomIiwyKSU5tKitr6motWM+OC8tNEZc1LWqqrK5u88/Ly4zOD5X1cK4s7W/2GJNPzk5
+Qlb/zr63tLe6ur8+Hx0uQCkfNK2irLGop7dfQDMoKDZLQ0+2pKezurfFSDc0NDdEft/Tuq6z
+zl5zXz42PFB03ce+vr29yHQ8KyctNzMvQL+urrCur772UUE3ND5Vbti9srS8v8pgQDs8PkRW
+3M/Kv7q/3lZSTUVES1vu1cvGw8DD02ZQSDgvNUFBPEfcwL2+vcTZ5OhbQkFX+H7gx76/yM/d
+dGBYUVNn29fm28nF0VxMUldORUhm39vf3NDM1HdPRkdHPTlDan1o28O/xs7S3vxuXE9SYuPW
+zsrKztPdbVdXXF9l7N7l3M3M319QTU5NSUpZ59bb283L129YUU5PTk5UaeTd4uDY1t78aWhu
+bmRfbeTR0NTW5vH9b2VjY2f549je69vPz+VoXFxXTk1XcuTe6N3X2uxeTUlKTU9SWe7X19/h
+1tPZ4ej6eHNsa3zd0tbc3+bwdGdhWlhkd/74+/v05OpvZmRiXFpf8d7d29vd3udpVU9SWVpa
+X3jh2d/s49ze7H1uav3t9m51597f5uv4cm5lWVRbanBvduzc19rj7PD2dWFdYnDw8vno29vf
+82tmenleVlRfbm5nY3bp6f5z9ujp6vTu6Orx7evt5+b4b3n98+7s6/Pw8vP2b2l0c2hkYV9k
+d313ffrxel9caHfy8e3k4eDo8H//fG1hYmlucn3u3tvf5+7zfvv9eHR7+/Z5bn776+Xl5ubu
+9O78a2VkaG5kZnBrZWJgY219+/zt5+Ho7+ro937/9e/n7W5panjw+PXv+HBqZ2Nq/u7u7unk
+5Ov/c2168/99fvTo5urn5vZwa2ZdXl9laW305N/k6unuf3Zva2t0fX5vcPfy/vf7/fR+bm93
+f+/q6/Hs5+bs9fp8dGtscW1te/r88+7z+W1ueHFub3BxdXd3enhvcXX19O7v6ur27/Lu7fV7
+f/Ty7/R3bm5vcG19/Hv76+317vd6bW17d29rb/x+eGxpZWxtc+3r6u3r4d/f4+54/nRubmdo
+dPp4fPPx8+vyef9va2tpdX999fV8bHr3fnpte394ffbq5uHl6/T/cW1sYWFtbm9veuzt7+3y
+6vJ1cHlucf14eP71e3bz6+zr9vn0c3r/c/78amp4c2pyfXN6eHz9+Xz87/T28O9+d29/9Pfu
+6e7s73l1eW1sc2txdHVyb397fv3z+3zw9HtzenR7/H18ffX4dXd4eX57e339+O76fvP5dvv4
+/u/s6Onp7PDx/W1rcGxmX2Blb31wdX//d/Hu/n756/T4df7u6OLf4+jm6fP8921nZmFnamF4
+7ft59Ppwa2Jlbnpu+Onv9e3t9Pj25un8e/57/Pz/+n7y8PN3b3Pz83j38/T+cnJ8b2v8+m50
+8vVyanv9dHrz7fH69u90bXj5+v777+7v7PtxfvX07/t3+/J4b3x+eWx7+3j9evl//PL8+vrz
+9HVxfG9vc3V0+vvy+3D8ffz8/W1t7u9zdnZ37+3k4ez5+PF8bG39eHLzdXz5fXlq+3Z5du3q
+am1+cW789H/7a2/9eW15/PLn7O34evTl8Ov7/e57cWt0dGt3dHd/d21qd2xsfXD58+rt8Pxt
+dnvn5vvz9fP8/u3x9/PwemptbGpr+3t3+fFydvlqbG1v8/Pt8n7u8/J0bHl3bP37bm99efDu
+5eDu9Xh1cXJsZW1udPF+9vrw/f7q/PbRylBOeHdNX9j0f/nm4drpfu3PzE5hcV9KVfpo6F7t
+wtBHT937SV7tfmb7xNVcVexeU3LV2fTUyc9qXl5fW9vKy19N+WhYUuxaS09h7+todN3P3Pbq
+729ZYfXc4N7c5Hb96+zsbHnp6Ob5ZV9fXmxxfGjo+/5tX2FWY3Vzd+7U0dfy+O/s6+FvaOne
+ZlZ4d25TZu3rfl/+8ejd2e9ta21kXX5v/vbZ8O7nbP/v6WRs7+R6Z3rneWX43O5c6/N+Zmxb
+aFxa5vfvb9re6Pvy3uxyZ/rsf2luf+Fzd/ro5nb2XWV0bnfy7nv+5uB0/uvc+F9lZ+ZjZ3t4
+b15reXn1f+1waff08e3j8u7h+mZ862lr/unvcevc7WN7/+35a+P353ju5HtqWnFxZ2hqaV9t
+Y/5rdnjk3+zxduD8bmz32efq99zsc21q6Wxi/uV9a2hyfPppYv71eG979vtu9Njc8fp+aVNq
+9fR8b+Zx6/f9d2vkbnB8+vN47P7+anPrffZs6OlnafXzeHnz3fZxcO1zX2vt2HNp+2ZramH+
+9Gpl5udr/fDc7XTq7958/vL5fWR8Zmxxaurr+XPp5mJfePN7cn/t/mVmZuPqaX/j2Gpv3eVv
+Wmbi+mxk6dj0bnne3/1fbu5iZuXsdF9pefZuX37ldW147fP96Pdt7ux2Znbr4PPv3ulwZWrt
+Zln13+JvbOPga1d7721fXfXv7mvy3nb83uZeY+3ocX7/3dvr/mne13Nm79n3TmXfelhMXuZg
+U31xadtkXt/a5ezq/ODZen/vePrg6HLb22ZfdfNiX2/m32Fa9eH7XmH55d9kWOnaeldm3eVr
+Xnz8X2t86un9aXnY5WNm69v1bXj74e9hZ3dy/PVoYunbd3P34NLhXWrX5WFederuYFbx23Nb
+a3l0X1hf5Hlc3uD88X7s4Gxf++bud37a7GTn393uZ+rt6upsbPX3ZmJ0bXxqbm1g/ebqYlj4
+62Rfbm1qeG5i4eLi2dfa29Z2duLebF5r+/piWGh+9OX8cV9h/G1lX27w+/Jv6uFv6e34emTe
+3X9r8e958W1ueGx43Od78fj26Wlf8OjZZlFu3uNcbd7tcV5eb3bs7G1ic/r1cFZr3O71Y2XZ
+0G9e9v7a1OpdfupmdPBx8fpfXvx5YO7l5Ov04OfudFlu2epmXWDu33lba+/p+2Zo+/tiZPf0
++nr62thtVn3cemVqcNvYW2fT1e9eW23e71ZWeejxZl356Xb66Pdz/drX9FVf3OljX37e7HBq
+7t7tbXDp4uhraujr8/N+efN4b2VtcHhuavTvXVZ+7/N8Xmze/W14Y+nd/H3mee7W5+NsYOv7
+7eFmau3s7vlyXWvr8l1j7fZsXWnu4mdY89jjaHP06ehqZ/fg8G3n6XD+8Op0b3b373hmXefU
+91xy5+Z2ZGJo9HFqdf7xafvncGVo93jya2fl4PRi++bk3+7p7Xt4dnxucHNd/XpeaO/aenpt
+fdnfdVxq6mxt4X1reePrduX8c+nibXBmZvF2XnLo/Ov0Xn7cc2ZwfvJteW37+WVo695oXmvZ
+1Wtg9ebm6F9m1tz1cmzvfG5oYv/y8WR932lZau/pbmXq4eL78uTuel525WZaX3fi7nxu6uPt
+/mdwZ3l0Z/T03dt5cXru3u59fv317O1bUWDi3W11+e/9b/lyaXH5737weWXy6Oj1Zv37Ymdx
+Z1/o2/lpZt/V52xw5OjpfmV87H1s7eVkXvntfXXyfFla3NpbV1/r2G9aXXDn6Hv+8nv75/h0
+8ujl8evgd/Hj+enp8XB29G5eZ/358W5navfe5l1d5e1eXfn5a2Fh7epnbPLp8P5qbvf6fufo
+7+zq6efb8Wz59/b6+vjv+3N8fHP8/e7vYl5ec/lpaG7u5uh3eHJmcXN3fWhdYvXh8G7r2Nnm
+6/by7PDwfF9ma3x+eu7xcWlubHt8YWbo6mFZYnn0/e/j6O7o3N3wb/Tq729od/Z+aWV3/fP2
++v54dWZpdnFka+/j5mti7dzkdHLu+HV0+25pcPPt6uXr+PDv9HhlXGf0fv3w9fvz7fhvanb7
+/nZrc/f68uj+eu13aW9+e3xxeebr/fT7bm55/Ofi/Pv07W9eX3bt929v6Oh6cf706/H/7evx
+9fh+dPz47Pt4eG1saWNpanH093FqaWz+4t/l7Oj3eXhpZ2l28e75d+7g73hxcvny9HhkX2r9
+5un9dn/l3uHvcWFkeHz073dlau/s9nl4d27/7X9gX27w8356cnf18HNnaWh77evv9vHt6+rf
+5/Tp/Hx+/v39b3N19nlmbX59aWl/+/1+dmVib/Tu/Pvx7e3x+W//8nxsc/Ln6vLz5eXr8fZ3
+bGhkbm9waGtwb3z59P1xbG18+HlrbnZ5+e/l4N7k5ebg3/N78u98a2ZkY2V4+XNvd//8+3dh
+Ymxve3Rzd3Rxbmh16+rt7uvk3uTt5+Pk5OXr9P766vJ5bmRma25jXl9jZGRwc3h9b3T88PT1
+7/jt4+js6ev/fX39ev/ydnl4fW5tfHJ2/PL08fb4+Xp27+3y9uvp7+3u+H5xcG1sZ2hwa2dv
+bnfw7urs7vl9cW52e/jv+fLr8O3t9vzu6O36c2lmYl9jaHjv7fl1dvbu5unx7/L2em9wc3/w
+6OXp9nFraGdmamlrZm179/H56+nv7OXi3t7k63V3+3psb3NpcPnt/mxsb2tgaGprcW5vdXV4
+ffjp5+rl4erk4et6fvt5bnZ7eP12fX5xb3ZzaWl3+Xx2cW56+Pfs9H307uzu6ub7eHRy+ezu
+9/L7ffb0e29pa/58d377fXf++21t+PT9fn/47/D+dGdnbG5vcG95cXFvc/3y7/fw6+3z7Ph0
+bGVpcXr4+nt0+eLj6+zt7uvu7u3w+X94e3Ruem9w9Pjt83h4cHd6eXn++/zz+/zv6+75dXZz
+aWpqZWNobnJ1cnV3dff1e/35+vn5/vvv7/Z6fvX39+zyentuaWluevf3fvf+eHj07v315eLg
+4ObveXR//nt9c3n+em9v+Pb3+vv29/H0fG1ueHJpcnBnbG9yfG9xent9d3x0cnf+9PHm5+v1
+/v19dXN1c3hybGxtevf+d//u6ev1fnzy+nl/9+/u8vj09fb2+3157eX1dHx7cGp0dWtzfGtq
+d/7v8f31+v55fPt4/3t3b29yfPN6aXDz7u/0fvr58ufm4NvU0dPW2udvYl5XUE1LSEZGSExU
+ZfPa0tDPzc3NzM/U2uH4fvDn2c/LzM/R2eJ4XVBFOjQwLzI6SHXSxL26uLa3ub7OY0c7OTo8
+Qk9t2s7Gv7u3tre9ydHc+3x1YVBGOzAtLi81PUhW58i8s6+wtr3RWUU+Ozs8PkROddC/ube0
+srOztLi+yvBPPjcwKygmJyovPFLOuq+qqKirs73bTDoxLi4wNj9Y1cO6tLGvrayutLvF2PxX
+PjIrJiIkJysvPFbKs6uop6issbfITTcuLC0yNzpEX825rqysq6yur7O7yPBWQC8oIyAgJCgr
+M0rPtqumpqanq7LGTDk0Ly4uLjNB7ca6tLKuqairrbG6w9VfQzQpIiIhHyEmLDvuvrOrpqSj
+pay6zOpCMy8uLzg/RFLOvLOsq6upqq+1u83uWT8uIx8iIB8gJi0/5sm4q6Wkpamtr7nkS0c+
+Nzc5PEZZ78u7sq6trbCwt8rPaU0/OCsjKCsnIScyQEdOzLKsr7GurK2wv2LMy01CelZHQ0bl
+/E5fwsPJxMDHytDvXV1gSURFQ0E/Q0tVU09f7er7693b4fn56uHc6v3/9+5iW2Tu0crMyMLC
+wMXKztHeblJLQUBBQUVIS1BVWWNve+nX19vTzczT1d9kUk9KSUpJS1VbaeHPx8bEvb7Fx8vY
+6nRcVE9MSU1MT1hZXe7e1tvv0Nrc2nXo8WJPTVNKSltXW/zwzdPQys7Mzsra9PtrW09IUE1D
+TFFTc/z+5NHNzMfKx8bM2+NtWFJPRkhZTVZZ+ex03Nbb3dnk3fV4/PZzU1RaTk9kZlP28+Ts
+2NrOy9DK1OHZ21lr6k1SXF1OXWN06mDS7ezt/9zzbVr7bHVTbmFt1kzZfH3ZX83Z39ht7d/c
+V3HgaGNV6WNR3WR52/ZT49N6WuBu4O5Mz15a2Uvr815g6NhZ/c1p8djp6t1w999Ra1fN3DzP
+z0nawFL8wkZg0OdVUNRmXlr/W0/sXl994v973XPb6WXR2uNmzfne623w8NtN0+1R2/v2Wcpy
+Qs3cRe9e6FN9XE7HQl5s+P5N3VTa1U3R297e59Ta2vvZxkzgzGpT1G1Vz1pK089DWclnTmzd
+UmteSuD0RO/KWE7fy2f69XnAYFTIzWNJ0tteX1rWzkxx29/JP+/D0zrTvj3y7lDMSkrp6V9M
+1vHaT33Ca0i61UTK9XrcUkW8Ri7HtDVPtm7ObGK/00fnYM3eOmnW91VPVr/FLN64V2w+07tf
+PsnbaMhEPbB+Nc5+3HRfTNPE4EbjucQ3cbzoU2hK3b9ANc7CQktW3Mtdb9bfy+xYV8paU9lC
+z+xKVMFL6chJacPEWljAvT/t0vFWXExd6kFV1E9O2Hzj31vV0mbtX9vvWmHe3FdN2NhdcPJJ
+vL5JSLy5QEXMyURi9UJO2XBAXM7e5m1qV8nLUm3Sz9NC3M5R6nJkV8dOSuFr+Gdw3Ndo5d/S
+XVndzWZF199ybltu1/dy4Vxa19rdX+7XVu/tTVRtdWbheE/x8e1jZefO3ktkz9BzW3fjcm5e
+9W9jbu7M5Gtv38zZ79Pg7d3e4uLpX9/QbG5PSU1KPjs5Njg9TV1d3MfEtbK5uLK3t7bGz9DP
+X13/W+/ZXjovNTMtMDEuMD1M+crHv7i5s7Gzt73CzdfO+VJcauXRwsrEur++vnw8LSclKCkn
+KC06T8q3tLCurK6srrjE4HVUSkc+SG/Vx764uLi3tLrqPS4nJiUlJSgrMEbNt7Ctq6yur7W8
+yulXRkFCSFf1x7u1r66usbS4x08tIx8fHx8jKCw3abuuqaipqq6zu8p+V0xGSEdezr+2sq+u
+rq+xtb3tOSgeHB0eHyMqLz/StaulpKaqr7zM7VdIQkJCSnnIurOvra2srK6xt9g8Kh4bGhse
+JCszR9i4q6Oho6izyl9HPz09PkVR4sS1raqpqq2vtLm9y04uIRsZGh0kLDlO276up6Ohpa7B
+Vj03ODo/TnHfzbyzraqqrK61ubzD1kctIRwaGx8nLztUz7mspqKjp67FTTkxMTQ5QU5pzryy
+raurq66vtLvD0kswJR4cHR8mLjtWzrqtqKSkp66/VjgvLS4zPUx5y7yyrauqq66zuL3F1Vo6
+KyIfHyInLjlK6MW3rqyqq6++/j80Ly81PlXhyby0r62trbG4vcLI03pKOC0qKCktLzM4P1Pc
+wrq2tbi/01tIQD5CS13lx726trS0t73DxsfEytHfWkU6NTIxMjQ1Njk+TG3dycLBxMvd7vH+
+9OfZ0svGwcLIztXY08rCvL2/w8rnV01FOzYyLi0uMTY8RFTv1szFwsLBwcfLysbEx8nP2uPd
+z8jBvbu6u77F129URDkyLSopKiwuNz9O7cu/u7m6u77Dx8fIztPb4u7t28zFvrq4uby+xddu
+VUE3MCwoKCotMTtHXtLEvbq4uby/x8zP19/g63nt3tXIv7y6uru9vsTRe049NC8sKCgsLjU/
+TnrOwLy4t7u+xszX2dba2+Hq39TOxr++vLy+vr7D0nRKOzItKygqLC82QVPrxr27ubq8v8PM
+2Nvc4uPi39/d1cnDvru6uru+x9VfQzgxLCkoKi0xPE1o08G8u7q6vcLK5uba4e7a2uDMysrC
+vbq2tre7yFxCOzUtKCUlKC43RGrNvLOvsLa6v8r/UUdFUW/z18nAurWyr62tr7jMbT4sKCcj
+HyAjKDBA/byvraurrLC2w25PRDs8RVTfxL65sK6tqqqsscDmPykjIx8cHiIlLDppu62sqaSn
+sbm//VRHPD5JR1XDubatqqqpqKy2vuovJSQeGRwgISUzS9G2r6mjpq2usthMUEA5PkVQ38S4
+rKmppaOqs7nIOSgmHxkaHh8iLkbsva+ppqeoq7TJW0g7NztBR3nDubCqpaOkpqq0yFAuJSAb
+GRsdHig5Scqtqaijo6uvtdRSQjg2OjxM2cO2rKimoaKorrPZLismGhccGxolMTnjr6qoo6Cn
+ra+9XEQ+NDM8Q1TMua+qpqKip62vyjEvKhoYHhoYJi8taK6uq5+fqKmru+VlRDY0Oz5C3723
+rqalpKaqrcY8NSobGh0ZGSMrLFO3saygoailqLrL10E2Ojk4Q+7Huq2mpaOjqqzAOjkqGhse
+FxklJipvubmroKSnpKm6xM1MOz09OELmz76tqKijpquv1D87JRodHRcdKCYvyr65p6Kop6Wv
+vLzeQkREOj5X+tO5rauopamssGU9OiEbHxwXHygkOsTHtKOjqKOlsbq9b0JGQjxEXvnLuK+r
+p6qpqr5MSjEdHR8YGCUiJVjHzqygqKifp7eyvE1LXzo5V1Vcwra1q6WurqjCPWE1Gx8kFxoo
+ISTzyNOroKionqe4sblKS2o5OVxNTsS6uKynrK+rvERMNh4eJBkaJiQkUMrerqKnqJ+ntrC3
+VlLoPTpZTUzLvLiuqqytrL1vTzUjHyAbGyAjJT3s3bGnqKehpa6ust5a7D43TkhF0cPAsqut
+rau1xmJCLiAgHxsdJSMrVfzHrKippaKpsq++WXNYN0BTPU/MzLyurK2rrbjOWDwpIiIdGyAk
+JDVk3raoqKikpa2xtd9X/D83Sj8/7M3Ita2urayzv+JQMyYkIRweJSQrR2DKr6qpp6Wpr7G+
+am1SOz9IP1DZz7+yr7Ctr7nH8kguKCUhHiIlJzJH68Cwq6qpp6y0tslgWkc7QEFEXdzLvbWy
+sK+zucVsRjcpJyciJCosMkvkybatrq2rr7y+yl1UU0RCTlJb2cXEvLO2trK6ytlTPDItKign
+LC4yRFzpvrW2tLO3vMHRcFBIREhQV2nWycW8t7i0sre7v9tRQDgvLS0sLjM6P1Tkz8O8vL6+
+ydzuVEZGSU1c7NPKwLm4trO2uLi+yt5YRDo1MC8vLzQ4P0lf3svEv8HIzudlVUxKU2D008vE
+vry6ubm5uru8ws79T0A5NDEwLzI2OkBMX+fSzcvN1uPsYVZXX2zsz8jCvry8vLu8vb28wMXM
++U5AOjYzMTEyNjk9R05h4tPPztLj4/Z37djTz8fFyMXAwb68u7q6vL/BzHVOPzYxLywtLjA1
+O0VR69DNzMTKzsvQ2dXNzszJyMrIwcO/u7y9vL7ExddsTTszLy0qLC0uMjtGU+DMyMK9wMO/
+xs3Ny8/NyMnKwb/Cvr2/v72/ys9lTDw0Li0rKiwuMjdFVPnLv7y7ub6/v8nPzs7V0dHPzsbG
+wb69vby7wsnfV0c3MS8sKSssLTM8SV/NwLy4tbu+vcnY0NXk28/X3c3JyL66vLy6vcTKbVNB
+MDAuJygtLC46Slfdvby+t7S/vbrP38/fYe/W6eXIycy+ury5tr7Cz05JPC0tLicoLi0vPlJe
+zbm1ubKvvcK61GXX7VFt3fjnyMXHvre5urm8ymNORjIrLyskKjEtMlNtdb2ztbWuscHHvuZM
+5uJJUNt3Zsm+vrqysbe5ucVRQkIvJSkrISMwMC9ZxMq7rauwsa+50dPWTkJRX0tZz8nLvLKy
+s6+wuL3LRTo5KR8lJx4hLjI1c763rqyqqa64t8NcU1RGQUxabtfEvbiyr66sr7u7vz8tMioc
+HSMgISo0TMm9sqWlq6qqtsTbUElCOTlKVFTYvLezr6yqqqy2v8FLKykoHhodHyIqMEa/r62m
+oqOmq7jE0EU5OTk3PUpmyrqzsKuqqampr7/OSSkhHxwaGxsfKzZKvaqnoZ+go6exweBBODYy
+MDlJX868sa+tq6mpqq2+1UgpHx0cGRobHyk2Wrqpo56cnp+kr8DhPC8vLCwxPEjYvrWuq6in
+p6iptdlPLh8dHBgXGxwgL1W+qZ+dm5ucn6m52EYwLCkpKi88Vsm5r6yopqSlpqu+5j0nHhsY
+FRgaHCY46q6gnJmYmZygrMFVMiknJSQnLTp7v7KrpqSjoqOlrL5nOSUcGRYUFxkcJz3Lqp2Z
+l5aZnJ+rv0suJSEfHyMqNVPGsqmjoZ+foKKntOU9JhsYFhQVGRsjN9qsnZmWlZebn6i7XDEk
+Hx0cHyYuQdS3qqOfnZ2en6StxU4vHhkVExQXGx8uTbmjm5eVlpqdprHLPyoiHhwdICg0VsKv
+pZ+dnJ2foqez2kEpHBcVFBcbHiU1VrqlnJmXmZygqLbSRy8mIR4fIyk0TsevqKSioqOkpqy5
+2j8tIx8dHR0fICYuPtO0qqSfoKKjqK65zk06MCwrLS84Q1zZwbixrq+ytrzAz9xqRzkxLi4v
+MDE1Oj9O8c2+t7W2ub/K0tz4YFpXUU9RV2X138/Ix8fFwsK/v8PGzNX8V0tDPjo3NjY4PD1C
+SU9deejg3t/b083Jw8HExsrP0tXb3t/h3trT0s7JxcDBw8fWfFFHPzs3MjAvMTY7P0RPXXza
+y8C8uLe6vcXM0N37YVRUWGHs0srGwb28u7m6v8zlXEk9NzIvLSwtLzM7RVjpz8S9uLa2t73F
+z+VoWFJQVFZe+9vOx767ubq9wcfR5XFXRT05NDMzNDc7PkZa/dbHwb69vcDI0+tlU0tKTE9W
+beDSycK/vr/Bw8bL1uJ+W09LSUtLSkxOS0lKTFJebf18dnxnYWNiYmFgYGr+59zUzMrIxsfI
+yMvO1d7te2xmYGtvb/3v73hpWlVVUk1KSEZGSk5TWFtmd+/l3/3TytvV1dHa2dzY1d3a2dPV
+3Nrf39/p6Ofq6vP1allWVlFLSUdGRUlOU1dg9O1/5+fu5/14fn3v8PHkzczk0ci9y+LEv9Ps
+z8rN1vLX1FRIRkM7NDU3ODg6Q1di+82/wsbFwcDGxsjM2ur0dm1u7t/f2s7GwsC+xcrE7UBM
+Sy8uNy4qLzg0PVpt076/vLGxt7Kyvb++z2liVElNT0pV7dXGvb3DubtUX903LDItJikuLjM7
+SN/SwbS0ta2ss6+vvL7F0elWVldNTFptb9LE2sq6S07WNi05LygtLy40Okd+Z8e0u7asrrWt
+rru5u8PWXu9aQlVlVPLLydDRvP887kEqMjQoKS4vMDRJa1DLtL64rK6zrq21vLi70N7Mfkpt
+cU1zy8/PzcHrO29BKjIyJycsLi4yS2pSwrC9t6uvta+tt7+7v+rr1VVM+vVV68C/xsC4yz5f
+XiksNSYkLC0sLkhtSceuu7uprbmvrbnDvMZTXe5IQV15UNm6vLu3sblS2s8tLTomICkqJik8
+Tz/Jrb65p623rKu6xrvNR09cPDtQWEzlubm5r6ux3cjDLiw8JB4oKCMpO0k+y629tKSrtKms
+vL+73UFLSDM4TEdH0rm3tK2psdXDvy0rPiIdJyYgKDtGQL6ru6+fqq+kqrvAvWM6Rj8uNUk+
+Qsi5u7SrqbjWv9knLTgeHiklIS9LTF+uq7Sln6yrpbDDxMk/ND40KzhEPF25tLOtqKnG8bo7
+IDIpGh8pISQ991m9pKmqnaCsp6q73uZWLi04LC1FVU/IrauxqKK5UMnfHyAxGxgnJx8wx83O
+o56qn5qorKa4/kpIMSYvMSk47W7JrKaqrKGqY0vYKhglIBQbKiEozbG6ppmdoJqdrq+0Vy8u
+LiIjLy4z3ru1qaCgpqKlyDk8LhgXHxYVIyknYqurpJiXm5ycpr/NTigiJiAeKjc+yK+ooZ2b
+n6SlvDEpKRgPGRcRHC0vcaadnJaSlpyepdE5MiMaHB8dJ0HqtqWenJuYnKeovCsfHhYOEBgT
+GDFPx6GWlpSPlJqhrmcoIR0VFx4fKuG3qp2amZiZm6e3vi4ZGBcNDRYYFzG+taCTkJKRkZuq
+uUUeGhsTEx4lL8CmoZuWlpiZm6jb7jIVEhcOCxUdGCuvq6KSjpKSkZuu0TseFRkXEh4uNL2g
+nZqVlZmcnahlSDsXEBgQCxQfGieyqaaUjpOVkpu06kUeFBkXEx4zPLqenJqVlJmcnapoRDYY
+DxgSCxMhGyaxqaeVj5SWlJy1Zj8fFhsYFSA1QLafnJqWlpmdnqpJRTYTEBsPCxghGC2srKWT
+j5aWk569yT0cGRsXGCU1TrGenJqVlpudoL1NRx8RFRYNDh4dHdmprZuPlJaVmqrKYioaGhoX
+HCs9z6ecmpiVl5ufrehLJxQVFg4OGRwcSLCsnZGTl5OXqrnJLBwdGxYbKC9cqp+cl5WYmpyq
+zGYmFhUUDQ0YGxs+sK2dkJKWk5anvckuHB0bFhsoL0+soJ2YlJaZnKrDTh8VFxILDxoUHs+2
+rJaPlJKQmam13iUcHhkVHSkscaegnJWTl5meq8UtGhcTDAwUEhcy57eckpGQjpGdp7Q0Hx4Z
+FBkfJja6pp+Yk5OXm5+uNB4dFQsMEw4RLD5en5SVkY2QmJ6m5SYiHBQXHR8pfrGkmpWTlpqc
+qzMnJxILExELFy4qaZ2amZCOk5mbo/QzKxwWGh4dKua8q5uWmJiXntw1PxsNFhULEyUfM6ij
+npSRlZeZnrNuQigdHR8dJDhXvKSdnJuZnsJV0R8QHRgMFSMbKrGtppiWl5mcnqvYXTskIici
+JjhX0ayjpJ+dpMFbzCgWHx0PFyceKsK3q52bmpifo6XBSEQuJSsrLD//yq6nqaWjqcI9STMX
+GiMXFSUqK+m1qp6dnZqeqqm1Tzs0Ky0tL0ze1bClqKinqrM8KjohFBseGR0nLc61uJ+YnqGb
+oK6300g4KyszMThuxraqqKalrLHCMSUpHRYYGRwmLDS5qKifm5yenqm5xHE6LiwtMjhH3buv
+qqenqK653TMjHxwaGhoeJzJRtaqnnpyfoKCtvsZvPjk2MThJb9LBv724ubvQWFA4KyoqKCsz
+ODxKdtLKu7C3wr2+ycO/yMa+wcTBxdh7XlRJPj9CQUxpZd/EyN3Tz2FQWEk5ODo3NztDUe3O
+xr+7uLi5ub3EzeZlVk9PTFNzcWTcys/Pw81oXVtJRUQ7Ojw+QE5818zCvcHHydH9amFPSk5e
+Z3Lh3enSw8fNys/9/tz2a+huUlleVlx96+bf3OprbFlKRUA8Oz4/QU3zzsjBu7u+v7/I2t7i
+al9iXvXZ08nAv8HDydZ3U0U7NTMwLzAzNz5OZ+DKwL27ubq9w8nU8WdhdPjw2MzHwL27vL7E
+zN5dST44MzIyMjM4PUNPetrMxcPFx8vS3ODw9uHl693OyMK/v72/xs7gaVZLQ0FCPz9CRkxY
+X2x59N/f7Hp9e2ZdXF9dXmrk2tfLxMLDxMrP2vdyaFlQTU1PT1pmbHbr3dbSz9Lc5OpxV05M
+S0tOU1RaavTl6OPg5uzv+nRtaWx87e3m3trUzs7Qz9nY1tze8HzvfF5gZW15b/1eUUtKSkpJ
+SExOUFJXb2td2tTpVmm5usrMwsDDxsrI2XjwfNza7dLQ6mhNPjo1NDg2OT5DVfHc0M7MzszG
+yMTFzNHUzsvNzcrHw7+9vL6/wMXtQS0jIB4jKi07SG+6q6aipq255lFAOTk2NkFW07q0sbCw
+sK+yuMLl69z+TTQlHhscISo2R3a/r6SenqGqu1o3MS0sLi84T9O2ra2srK6usbzC1/fq5f09
+Kh8bHB8nMDtD5ruqn52doq3AVTo2LywtLjVJ6cG2tLGur6+zvMXS1szO3EQuJB4eIictNjpU
+w62hnZ6hqrnOWEQ5LiwsLjdFas/JvrSvrq6zub2/vL7GajYoIB8iKSotMThhuaqlo6astLjF
+9VA9NTQ3O0NFTW3Uxr21tLe4t7m3uMTMXTwxLCoqLSsrLzhI6Lu1sK+wtLe8xtR/V01KQUNC
+QkdObVVOvbrFs7a3s7i9vcpuTTs1Ly0uLjI3O0NSb8vAvLq6ur3DzNHxXVNKR0lIVmBv3NS+
+ubm8vL3GxtDb7lNCOzs5Oj08PDs9Q0533c3NzcvKx8nR2/103trZ3NXb29DRz99pU93mauHk
+2/Db29PS9GhTSEE+Pj0/QENLUmPw2s3NyMbGxcXIy9Pjc11aTkxSVF7d3dfMz87Ox8nN0dpv
+U0xMTk9MSkdHRkZOVlxjfNzPyMXFyc/Z8WVPSUhIT13619HOzcfFxMbKzNzwb2BXVE9NTU5Q
+UFFTWWL77+Hb3t3i8HlkWFlQSUc9ZcJc3MXMxsnLy8vPzNjhc1tiZFxaUk5FRuX4Xl3ZytHK
+0d15Wk1KRkZLT1pRWHfz1c/N0Mrd18t70G3m4WnX9d7ee9ti0HDM5tLTbNo/SD06PDo6OkhY
+0cnIvr66uL3Fz+b4XVZKSEpPVWHn1dnT0tvZ5dXQ1tfl29bJ2NxQOTczNzY0Nj1Q3MC8trGw
+sLbBzm9XQzozMzk9TFfiz766trW6vMLGytDZ3uvcVkY1LS4uLi0vNU7Uvbayr6ysr7nM8UxC
+NjEuMztEYN/GuLGxs7S5u8bf/l5iavBTSTcvLi0uLTE0RvjDubGtq6qvtb/YXEk4MC4tNTlC
+X8y6sK+vr7O2vNTfc+12ZVVCQjIwLC0uLjQ5Ttu+ta+trayvt8DcTz40Li0vNDhJdse2rq2t
+ra+0xMdi11dbUUNMOTgsKywuMDM9SN6+ubCtq6uutr/RW0M2MC8vMjlFb8K4srCtrbG4xcr2
++1tMUEdHPTYxMDI0NTY7SGXQxb21sq6us7rD1lQ/ODU1NzxCW9XFvLa4ube/wczR619QT0dQ
+SkVLPkdETU5JS0laVnnh4dLMw8TK1t9jW1NKTlJVb+DZzszIxsXJys7P1OLnXlVMSUtMVWrm
+49/X1N3ie2FZTEhFQ0VHSkdJTlp57tvQysjGwsTGx8vM1Nfa4P9iXVpZWF1iaW356t3Z2tje
+4OX6ZFZNSURAP0BER0xVau/d1M3Ozs3MycvO0tff6/h+dHBram1w9Ofe2tvc3OHo9WphWVpS
+T05OTk9VWWNydOrk4t7a2drd4uTt9Pt1b21iYl1eY19pbG/3593d3NnW193f4+fq/mdoZmz/
++3pqbG1wcn9wb29ra19jY21uZmprb2ptc25+9Ozy7e738+zl3+Do8Hzz6+bh4+fn+H53Z2tq
+a2Jlb3R5bm9xe3l2+nv+e25qb29+fnF0++fg3+Tv+3d8eXx7/vD8cm1uev37+nRubnP9eXVy
+a3F4+e95cXX78/b2+319/Onk6uff4/Du5unvfm5haHN8+/72c2xsZmVqbmllaWxpbn54b3nt
+6O39bGp7b2/88+fk49/f4+Hj7O3r6e/6+Pj+/Pl+fnx3cWZjZGltZWdkZWpsfH99e/3x8vx4
+enFtaW738fHq4+rq5+zq8/Lr7Xtv/PH/c/v17Ovo8W9rcHBvc29qbG99+fj8ef37d25pZmJj
+bnZ69vPn4+bk3uTi5O5+fHprb2xpcHt+ef7z8u7s8nh2d3lpXmVvfv378/bz7vpxd3p68358
+d3z07/B/e+/o6/L8e250dHL86+726urp7fL6b2NiaW1qZW1zaWz7+/ns8X/5e3vw7/Pz8+zr
+7/Hy+3398/j17Ozv+Pn7+/78/vv+cHNtYV1dXVteYW52dvDp6ebk4eTm6+no8f/8/nX77+3w
+8eri5e76+XpvcXh6aWVlYU87zrw+4npRyOlfyd1L8tBMWV97wkdV68NYZmK6VDSwfElb6cLa
+5szB3kxbbPo+dvvh4Fri1uZQzNtN2EZrX+5D0ml0YETBR8tvZtTLQc7NTdbMRsD/Uenu31xx
+/8dc2e3jZtJT9WPVUU7aUFTXeN/cStDd/k7KSNBOyk2+bOt1Ztv56Erc1lD5ccxJ6m3GS8g7
+xV3fT1/HVHVO2dxmb2TLTEjQ5lfkXM3XUd7az0/KTsNK5XBi0Vx51FxeyvBJvz7j2kdsy0tj
+61vJakza1k/q+sdlSvnKVUzPyUddcsVzVubD2EJzwE472Ndkel7RvTd0v+pedVbKZT/sxVpe
+X8XsV//CXETWzUhG3MnaP3e/2UPv2cpHR9nTSFHbzdJJ9cz2Tl3O10pfzt5uUeTKfVDvzutg
+XeLmXVHb0E1c2kpY/GLP4FzG3Wz7WeJXYdHnUs3ST9rXb/lJb+5KXW3ozFhe1+5V5F7a3FZ3
+U1nZWGbK1dbWY2r2VXHk3dJebs1WVs9Y7NtW2XFJ6G5O4Vx7z0VS1vJXe83GXVvM0FtLVstn
+TPLQ3N9f9NriYdDYU1TwUknmfOr83ddx5OjfzVtc9HRYSVTk9ml6+tHbcW/dXUlUcV3y2+79
+0dDfZNjV6OnfyuRozMthUG9NOTzh2uzd0cXdO0njTkl2xN9ASdZSTMXBw2NJw9BEwLnczd3u
+9UxUzMZ46mZZTTk61s5DTNxjRjc+yWxY1k7h00ncuMDH08u7+1y8usNt78BWRtDbe1E57kwv
+PkBLUDg7ZFVPT1XBzenDz9rCzc2+v8O9vbnNfclePvHIUc/AT1o+LTEuLT0/P2Jp29ZfyL2/
+vODkxd/exr63t7evyFfKVz7tyMb/NkdEIR4sPDIvS8G51mq4rsRdwbHFaMe2ucbMtKu+UMe2
+2EJdvc8sGhkkIB0yx66rvMO630t1zri1v7ayvL24r6y1z+TExExcsq0mCRU9Gxgto5O2LbWj
+7yMtqKNJTq2osVG8nqnYU1rI1MisqboSByQlEiTUl49zK6mwMSQtpqU+W7KprVy1oLdWUFvG
+u7qpo7oVBxcpGSLLl42oLPWxPxwgtZ+0TnWqqGJks6+9UkfTrqanpLQQBRQgHDK8lYqdOz04
+MSEZSp6gr2fKqbw9SL+uvUj7qp2cokMJAhckIsuhjoieKy0nGxgZPpuWpb/AuM48LVSsteLD
+qp2XnCgCARoyN8KhjIWbKSQeFhYXMJiPnLbVcNY9Ijitqrq9qJyUnhUAAxtKx7ifioeaLBkU
+FhseNZuOmK89L+DhKS3Cq6irqJyUrA4ABRj6qaOZjYuZOxUOESFAcaWUl6dWKDbcOjRUw6ee
+oJ2Zxw8EBRJOppuSj5CbZRkODR3Zq6GcnaW+LykyO2HJw66jnpycTw0FBhF+npeSkJObzRwO
+DBnLn5uep664RCcmM8OttLSspZyZTgwDAhKwl5GSlpqexx4OCxi2lpSfwV3GyjkpK/mqqa+4
+sZ6ZzBADAQ/KlY6Smp6itS8TCxE+mo+Ztz1Sws49KjW9pqGqrqSjcxgIAgssnY2MlaGz4Tgd
+EBAhs5WSntA9THplODJtr6Sjop6jVhgJBAshqI+MkZut+TcfExEbRKCUmKjLSkE/MzJqr6em
+o52duh8LAwgZuJCLj5mnyU4sGREVI7uYlJyq3TgxKy56r6ajo5+erywOBAUPR5SKjJan1Uw2
+HxUUGzihlZedsjsrJipfrqahoaKiq0IVBwQKH52KiY6fWTExJx4ZGB/TnJOTntUqHyNFsaSf
+n6SnrNgfDAYHENKPiYmSry8nIR8hHh4uuZ6UlqdFIh0sxqacmqCsulgnFQsKDR+ljomLl74t
+HxsdHyApTK6cl5y1NiIjPbKfmZuktU8oGQ8NDxhFnY6Ljpy9LRwaGx0hLs+lmpqlzi8mLfWq
+nZqdp8IyHRIPDxQjwZyPjZCctTEdGRcZHi+4npiaqOkwKjXNqZ6cnqnXKhkQDxIYL7ackY+S
+m64yHhcTFRsytJyWmqXNNC872a2hnqCq5ikaERASGi+5nJKPkJeoPR0UDw8XLrSbk5efuDwt
+LkG7o52dptApGRAPEhouvZ6Tjo+UoWAgFA4OEya7mpCSmqtGKSYv1KWbm6C+KhcPDhAaLcSf
+k4+Pk57EKhYODBAd1JuQj5SkaisiKl+rnZqerzUYDg0OFCjJnpKPkJSeujUcEA0PGTqfko+Q
+nsg0JihHuKOam6fcHw8MCw4ea6GSjpCVnbFMIxUPDxQprZiQkZyzSSwrP8mqnZufszIXDQsM
+Ey6smY+OlJmj0TEcExIUHnykmZabqcg/NkHfsqWfnqfWKRUMCw4XN6qakZCWm6f+KhwYGR4z
+vqugn621wk7ru7mro6eqr2QoGRAOEhksvKKal5qfq+0vJyUmLkfgw7S1vr7F0ruwrqimq7TL
+QzIrIBsdHhwqWteqnaesq+47aUI2b+1I58pc+cLJwK2pq6eqx0c6LzHwvOZHNR8aHiMr67K0
+rKq+zsRyTt/Z7tfV4trKw7qvrrS9z1dKZ9zezsnqUTstKCUkKTZNzcTU7Hxx0b68u7m+w7/B
+ys3X3dLJvr2/xmtCPjk6TF3qx8PL3Uk0LSorLzlN7NTMx7+7u7e6xsPDz8S9wb3F8Eg/QT0+
+V2NuwsTNz1w8ODQxOkFN5MbAx83uX1xr0MG6tbvO4GJNSE1MS1Je58i/xMjaSj05Nzk7Pk72
+x7a1usPwYFRO0by8ubvYUUA9Oz1BQ09718G8yc7PRjw8MzI8SUvLtbm6u8puYWdkyLezu7zB
+6Ew8LzM+P13GyMrJ2WtNRTw4OUFM7M2/usLY2FxNXerUyb+6vL3Nb0M2ODtBXdDNxbrH8FpF
+Ojs+PlbFv8PEyt1OVVBKZsnFybS+181yQz5NQkFh7vLe2P9fY1ZRS0Zd4NjFxsDJ6lhQTT5M
+ZOfIvbrBx9xSRUZaVvzV0M9iR0tIPT9OYWrV0s7EzvDi1O9Wdmdk1c7bx8jb9ltXRUxP683N
+yd/ge00+OzxFUHnTxM3b3P9YUGfq3OnPy8rEy9Xd3VdNU0hKY+Pn3tHcXlpSTUdJUlrVydrb
+1lZKW19ufd7LyNLKx9N47e9XXmZPVPtgYnj4W1RQfG1ed+jW2eBr8PNUUV3j08vO0svU63Bb
+W3ReYWro6m1gXVdbW1FabV57e3Ds4/TzW2ruX1/RytbFytvR2lJt7WBebubielppXU5ZXFRW
+aedZVNvZ7WP823lY/M/s3OD03s70Yc/pVe/b6mFh629WU2htcHZS9fNnYl7Q2PNd79TgWW3M
+2VZR7NjSWmPS0FRI1N9QV2Rs6l1g4dDrVF7p5uR1aOndbVXg1fBgd93ifGpj+25ee+Ld+nbc
+Z2p85F1n12Bp5Ohi++xYa9b4X9veXG3xXlJf1NflX2rg51hs4fb95vR27FpZbOr8XNzc7fBo
+V2BoWXPj3Xr63t972NxzaOvlbmpf79fqftraaF5edWVj+Pbsb2Xq3lxWam1tX3/mbF5gXldY
+XV5SUWnk0srIyMzOy87JxcXFx8W+ubm/TCYaGR8tO0b4wbaspKCqy0E6Ozs+S1pry7Sus727
+sbG1s7C5QCAZFxsgLD5vybKln5+nuVEvJykvO0Tgs6iqsb3Fxbuvr7S4trxDIRgVGB4sU76z
+qqGen6nHOSomJis4asK1rauvvs/KwLy1sLGys78+IhgVGSM27764rqWfoq36LygoLS81SM+6
+rqqstMLDu7i3tLW4tbPENx4WFx0sRO7KvK+opKm8QCwoLDE0NkjHsauqrre+vbe3u7u6urax
+u1EpGxcbJTRJds27rqWlr941LC43PjkyPc6wqKittLu6usTZ3c+/sKqv7SwcGBsjKzA6Wbuo
+n6CrxT4vNDg0MTA+yKujpq+9x8nJ31dS27quqKe4QSUaGBsfJy9Jvquhn6Wvykk8PDcyMTZJ
+xa6prbO8yNT8U1Lrv7Gtq6y5USscGBkdJTFPxbGooqOpsstRQzw0Ly81Trysrra+xcC9v8vf
+6c6/ubS22jgoHx4hKC0wOlHFrqWipq2702tOOy8rLTnwvru5tK+us8VeR0/szcjHx8ndVzwt
+JycpLDRD6r6wq6qttsxdQjk3ODxEX9DGv7y9xNRfUVzlzMS/vr28vMTfSTczNzw/PDk5PEhg
+59rY0cnCv8DK715kaXNuX1dVWmhvamvmyby1r6+ytrzG4E8+Ni8uLi0uMThGbM/Kzs7P083H
+xcbJzdxpV1BLSU5m1sO6tbS4vL28u7/O91BAOjQuKiotNDtDTV7u0MG9vb6/xczV8GBYYu7g
+4uro2s/IxL+8uri4usHcUD83MzIxMTM3PENGSU1XctXLxsPBv72+xc79VlFXaXRv3szCvLq8
+wc3c3+98bGhmZGhZSDkxLi81Oj9IXdrDvLu9x9Tf7HX+b2dz59bPzM3P1uDs5NfPx7++wMPI
+zN1fTT83Mi8vNDk9RVBf3tHOy87NycvN0dbO2OTmeGFfW1xv6s/Ewb25uLi6vsnjUUQ/OzUy
+MTM3Oj1DR09p4NbOzcS+v8LI1OtrVlRPTlx+2Ma/vLm5uru/xcjO1epeTUI8NzMvLi8zNz1D
+UvjXy8S+vbu+zOx6ef9zeurg2NLPzsjEv76/wsTDwsbO3WFLPjo0Ly4uLzQ7QUxf5NDKxr+/
+wb/Cztna4XddXV9jb+zb08rCvr69vsDEy8/fe1tJPjs2MjMzMzg8P0la3snCvru7vb/I1/xk
+VU1MUV5w3tPNycO/v8HCwsPFyc3fb1VHPjkzLzAwNDs/S17ozsXAvb3Bw8fR2OVlWFdaXGNl
+ed/OxsG+vby9vsHIzudqWkpAPTkzMjMyNDg8RlbxzcPBvru/zLvVfvxmXmz8a+Hh2c3Lx8fG
+vr/AwMfKz+JoT0Y9OjQxLy80OkFIUWni0cjBvr6+vsPK2PJyXVVWWltr6dbKx8G+vr/BxszP
+3m5dU0c/Ozg1NTQ1OD5IWfLUxb69vL3AxsvZfGJZV1NWXWJw7eDb0crHxMPCxMnN0dz3Z1pO
+RT89Ozo6Oz9GS1p479TKycjHyczP1Oh4Y1pbVFlhZ+nZ1M3JxsXFx8jKztDY4/pnW05IQz48
+OTc5PEBKWPLVysbDxcnLzdHedWZeX1dXZW7439jWzsjFw8PGx83U2eLudllNR0I+PDo6Oz5F
+TFx53tDMyMbHyszS3vZlXVlbV15peO3d083Lx8bHxsfKzNfn9G9nVUtIQz08PD0+Q0pSYvPZ
+zsjFxcbKztbsYE9QT1BTX3N9623qvbvBvrm8w8zR0d/6XEc6NzQ1NzY6QFV80MrGwL6+xs3h
+eGFZT0dCRExXZXne0cvK2cy6ur6+vcDJ2dXX6WdYRDcwLzExNTpJ8sq9u7i4t73Jf05EPT06
+PD5GUeLKxb69u7u9xMXIz9Hf593R0NXg+2hGNy8wMjE2O03myL66ube3vct2VEc+PTo6PkdT
+5c3Cu7q5u77EyNDb3vHy8d7Z1N35bEw7MzEzNTc9TunHvru5uLm/znFSRz49PT1CSUhgzcK7
+uru7vcjM0N/f3uXi1NDMztrkVTw0MC8yMzdAV9nFvbu5ur3H31tORz8/Pz9FT3XOwru4uLq/
+x8zQ3fTz4ef30srO2tjfSTUuLi8vMDhQzr24t7Wzt8JySkM9ODY4PUlTZM+7tbO0uLy/zflu
+eerd3NHEvsTK2nlINCsoKiwuNUXbvLKysbO1v+ZEPTs3Nzg+SGnZwrm3srG0u8TY5Hhic+ja
+0MfDvsPO9UUxKScmKCswP+K8sq6srbG93VJCOjMwMztIc82/trCvsLa6wMvdYlpy/+rSxL/B
+yuJXPS0oJiQmLDZQyLmwrKutsr3gTTs0MjEyN0P2x7uzrqyutb7HzuNbUl9t7M/JxL2/1lE4
+KycmIyYsN2G9tK6pqayzxGRGNi4sLTFBdtO9sa2srbS5vMbfZ15gcGbuy8PI0XxOOSsmJicn
+LjxpwLSvqqmutb71RTkwLi4zP2LPvbKvrrG2t7rH2uZya2xy68/N2e5XPzEqJygpKzNOzLiv
+rKurr7zRUDs0Ly0vOUriwrivra6xt73C0e9wam3x4tjIydVqRTcrJSYnJyw99ruurKmmrLfD
+cEE2LywsMT1b0760rq6wtbi8xtfxcf3p39XOycfWUT8yJyMlJygzTciwq6qoqK+93kM1Lisr
+LzhO0L21r66vs7u9wc3Y3e7e3eLSyMbNbj4vJyMjJSkyS8Svqqemqa+9Zj0xKikrLzznwbiv
+rq+xusbIzNHS2djLzM3MzMPNSjcqISEiIixBfLKnpqOiqrPEQy8rJictM0bEuLOtr7S2xdnP
+1NnLy8vCxczHwuNDMyYhIyIiLkbOrqimoqOrt+U6MC0pKi83XsC8tq+zuL/e28rOzMPCvLi+
+w8BqNSwiHiElJzb0vqylpqWlr8VhNywsKy06Stm6t7u7wc/W/uzFvbq2trOzv+4/LCIfHyEo
+NlG9raqmp6y0yk07NTEyOEbsybu3ub3PXkpHSmjKv7aur7K310AvJCAkJis6/L2tq62tssdc
+PzQ0Oz1Nzry1s7m8wNtOPjo+UeXKvLCvtr3KTzAoJCYqLz7duK6tr7S/20s5MjU9RWvGurOx
+t7/F50s/PD9a1cW6tLW+y/pINyooKy84S9a6rq+0ucZvQjcwMTlDWsy6tLGzusfdU0M+PUdw
+zL24tLG30V1KNyonKi89VeS9r6+0ustxTDkyMzY/ZNbMvLO1usPZdFhHQ0hb4M/Iwr25t7/m
+V0w9MCwtND5NbMm5tri+zu9cRjs4PERSY+HLwLy8xNDT1OFlUVru497VzMbBw8PD50lGPC8u
+MTZCW33Kubm9v851WkY6Oj9IYdjKwLq8wcTP8V9NSU9dZ+3PzsnEx8nJy872Qj0/NzQ6P03i
+09bIwMfO2V9SV09IT1hr3tzfz83Y2Nfn69zj+PDw8ujg5dzS2eXf4HxgU01LSEZFRUtf7dnN
+zszGy9rzXFBNS0tPXv7j2tLNycvV2Nje4+vt1svO3XRaUUo+Ojs+Qk972cW9vr/Cy9xkSj8/
+QURNZdjHv7/AyNf3YVJMV2Bn5dTX08/W4XFXTk1JSE5YZ+nX1tfa4+70aV1eXVtlenVv+vbv
+6vLr4uLj3dvc1tbe6PdqZ3NhW3Pt6OTq+XNeT01OT1VebfDVzcvLz9nqblZQT1BWXWt859jS
+09bb3upyZ2JbV1VdaW368Ori6uvt9P1udH387+Tl5+Dg5u98c2dfXVxgYWru6+/17Obt9314
+c2tnZWleXGBpZWzo4eTg3uPn7+vq9P11efr06uPt/3VtbW5mZW958ePe5PZzaV5bYGZqbnNz
+eHZ67eni6Ovv7ebr7erq+/3ycWdkaGhve3d6/n76fnr97erx+fp3a21gYWpqc+nl6eLo+P/9
+dm1rb33n5efl6+30fW1tdnBqamhtdXZ3e/rv9W5pY1xham5sc/Dm3t3d29/k5ujxfvrw8+7v
+/f5uYlxZXWZtbG557uXi7Ht+/G9kZGNhZ25ze+vk39/g4eLteWRiaWlub3b+7+rm7fX07+57
+c/zv/vvt7O7u8v90bnh+cWtvfXZvd3VubGxxb29z9+7z9+7v+vrw7vl/8+3t7+/z/frz9Pb8
+dGlgXFxlY2hz/fPq6OXl8Pvt5fB2bnN0+u/9evn39vz7dXvx+3r07359fn17dm9sZWZrdnZo
+aHR1fOzh6fbp4uju+vjw/m1w/fD28/d+dXv27/r/9fv0eXV6bmpoZ2Zmbv/+fvry+XX+7Ort
+8fn7cnjt8n/38/l0cGxqZ2hu/e3w6+rr6vTx7/d1cWtqdnV89+7t5uju+fT1dnR1dG13fPfy
+f/z+d3F0/npua2dhYWp5+/Dq6enp6+/w9Pn6dHJ1d/Tt7+zq6er7e3lzdnNzaWJpbGtud/To
+6+vr8/ry7e34ffn5d290cnF1fm94eH32+Hz8fnJ7bmx3/X1/+vbx7+/67+/q7P/99u7q9Xp7
+dXp4/n5ye/n59e35e3l1fnp4cnNsbv55dXBx+/T9cHf7fv769+7t9/L8a25+/v5+/fH5evrs
+9PPp6Pf9fXzz6/L4fnZpZGhpcXl6dWx1d3xzfPD5/PbxeHL+9vf7ev9+bm5w7up/ffD2+vV8
+c3x5+fP38e30fHd+6/t7fX57dvv1f3FwfHxsbXBoYGFla2597OLm7vLs6+/w5e51dHz4/v78
+emxv+/T08n1ua2xreP1ucvn+dfTo8frk3t/o/n30+fL4e3JscXVtaXFzcG9tbWxvdf5+cXX7
+9frv5uTx9+3u8+/k6vX58O3v/W52eXN1fHNvdmxhaG51e3dxcX329/ru9Pnt6+3r6+/u6/d6
+/nhubWxpaf/08XxudvLq7evwdXV6e3t++H92bm//9vR7bnZ8+/n8dG95+vt9efXo6+/u6u73
+9vF4fvP0e3JzbnP9fHBvYmRsanD67+30eXL6+nd2fHt+8u32+fbx8/X7+u/w8vn08fXx/G5p
+d/X5fPrw/21ubnb9+v91bXj5enFvbnh6/vHu9P/2+fr6+fDze3328vR99/F7dvr59/r8fnZw
+ePH2ffh8enN0fHtrZ3P7+/Tt8v1xdPD1fvX1/v767+jr+3Jtb3t9b1xSWWj76ufe2djW1dzr
+7vhvY19iYWFgbH1xd/Hq7+708n5ufPL+amlxffj1/nltdPHo6/zv9nFx7+9wb3Fza3N59vZ2
+fPh8fPDx7e9+euvn7vTn9Hh783hvdnN0anby6fdyd29rbv77e2trb3X/e/vz/nl69fl5efpx
+b/x9+/l6efP7+Pr7b3jv7e7n6O73fn3w7vt++n59fH5ub21taGZsbm1sePnw9/x0bm9wavjt
+8vDw9mv4/ent6urt/fH89/fv9XR1a/52/XR3fHv5anBeY254e395+Hzu9ezs7/Ly8Xt3dex+
+end+fPR9+HtxfXN6ZnNvcnz4+Ovf5Xhr8/D56+10fGxpb+3z7urz+21xb/1u9mtuefh+cX19
+8mjw/e5xcWx0ffnyb3x9fHpveOTq+2l38fno+ez4dmrv8f975vp6/e/3fHVqdm5lYGRvbnLq
+4+Pl7vZ8/2x9/fd4eHzt8vd1Zn18dXD/e31w/Xx5be/ve3JpbP/x/fXr7/v97+576+vq/HJ9
+e/9qZ3Lwc/5ya2Jv+Xd5+err9Hn57/p75vZw+3d6cG587ff47OB7aG1uZG937/V3Zmf3/u7u
+7nXp6eF4d+9t/HT2cfdnbmdta/Fs+eft63n7+Ovv7nvy8Ph2fO/yfXv6aPL5/HR5fHJ8Zmpy
+/2787enq73dzb/Rwbf5o8/j37+/9dnn06nh3cnnufmfz5/7u9f7f+Pnqc25sbXJ09vfubnT4
+cPx89m1scH9+d35ydv/6fnb77X3z9ursePp3d2997Pn+9vTu9fXrfnVtam5sa3TnfHnwd/d4
+8/j3cGbv+nxocHFsdv/q/fn76vJ7/+z473728PLp+fzv/vp3fXhraGRmXm54/fzu/HHv7vf4
++vd8fP/49n1t/O/t/vLh7vH76PV9/u56fe/5cXJ1a3h++HhuZHBiaf/6a3B1aW7w6/Lq7eR9
+d+3w6/rx8+d95/trbHdwb2h55nd39e7t8efq6XV9+W1oa/Vzdm/9/fP5an1ram3z+2dpbHv8
+9vXn7eLr7/Tu9X1/eux3cP7++XJh9uj3fHf4e3n7bmlvf3Z/+fP24+p2bXj67Pjx+Pn68vJw
++3NzcGx1bHxm7/Lu9e3n7O3q9vb2cnVmdmtm+nT0be5zfH3+eOztbmtrb2d+/Pbp9+b18urf
+7/r2+Op6cPt4++/3fG90cXt+bG/q7nZod3btdv1wfXN4+/zw/H9ufvj+cHzo7O7td3x1fe/4
+7nL7ZWFlbPpnd+rm5un4+e75fO3q8+jx7vf8/nv97PVzanl4b2Rlb/BxZ3pq/3b89PFxcvX1
++m/u6H538evr9vfp6ep3fX5q9Hh87frz/m5ubF9wa290am/17+3p7urzff7q5fD4fuh+anXv
+9fb17Oxwbfbqd2dkc3ZkYntuZ/xz8+18a/VsY3/z8unpauZv7+7ffvHo59fl7O/obG7q7Wli
+d2hr+e9RZ2hj1+376/Nzb2d6alxuafLOzeng4XxdcF9ud+9kamp+YnLrbODz3m1j/3Ro8mvm
+3+BeY3xwUXbVzcvb4N3dWVxa+mt2b/hsc3htcXtuY25QVnJgZNHm299s//5QbN345tHX3+vl
+3mdTWGJ43uXcy7/Fw8PL0t5qRCwnKi0vNjxTwLezsra4trvAydLKxNDTxr+5t7i3u0ggGxob
+ICYtY66ln6GorsA/LignLkbXtqyqqa21ta+sq7K6uMcuGRAPFh8qQbegmZidqsY2JRwbHjK2
+npman63NPDZMvKqmqK3ZLR4WEhIYK7+impeanqtSJRgUFyBKp5qWmaPKMyct9auem5mfWB8U
+DQ4RGUGhlY+Qmq08HBQSFiHeoJeXnKvjMCgqP7KfmZeZsSoWDg0PFzOmlI6PmLAxGRITGSZu
+oZeYn7g/LS0yWrGgmZaYpTMZDgoNGSmzlo+PlK8sGxQXIDHjqp2dpcU6MDM+37OknJiYn08b
+EAsNGCe0lo+PlK4qGRIWIj+2pqOlr3gzLC9Nv66knpiXoGIeEQwPFyiumJGQmLcrGRUaKPeu
+qqSlt2IzLDFNv66lnpqdsDUcDw0UH2melZSWokggFxYfP7atrquuu9g7N1LLt62noaW1TiAS
+DxYhy5+amJqlzCwbGSA7vq6npq/HVDk7Zb6wqKSqs+omFg8RHVCkmpicpLw6JBweK1qvn5yh
+r3szMUTRtKuprLRYIRIOEyHbopybnaS3RiUcHihCsJ2Zm6O6Sjk4T761v8vdNx8VEhkutKCe
+oqy5yU0vJiAkQ6qcm52lrrXCzsn7OTUyJiAdGyVZtaimscz2TDw3MSkqTq6hnqGrrq2vrKq2
+VjMjGxgXHS/Lq6KircVPODAvKiUx1auenaSssbm5rq21yT8gFxUXIEi1p6Ops8dWOzMrJCs/
+ya2in6GkqrCzt8PfPSUcGBgeLt+uo6Clrcg+LyojJzdZvKynop+gpqistso/IxgUFRolOdav
+pZ6cn6zSMiUkKS8/6Lqonp6mr7/Gvb5vMSQeHiEkJik0ZrOloaOrtsTnVUdDT8uwqau6VTMt
+Lz1+wbq8y3ZALyspKzRI1722srK78z0yMj/SsamprsB+TUxZ2sTAws5kRTkvKyotNlLKvLm+
+1VlEOzpF772vra+1vcze73hs7NrQy9VZPTUyNTxGT1ZcaGtfTkdGTWbXyMG9u7m3uLvB0eXn
+2trnZEtDQD8/Pz49P0NOZO1xU0xPZ+DPy8e/uLKwtb3N6G1sb2RcUUlEQkE+Pj5BS1dcVVBT
+XXZxcPLYxry3tri8v8TKzdfuZlVQTUtIREBAQkVISUdKVW/tdGVk99XHxs/Z0se/vL7DzNHW
+2d3pbFxUTkhBPT0/RU1PTU5aa/z0b2lt79nNycnGwsLAvr/Fy9LX325USkdEQ0JAPj9FSUtN
+VF/15+Pe3tvX1M/LyMPBxcnMzM3P1NztaFdOSUVGRkVHR0dHSlJg+/Dv/33y4dXPysjHxsbF
+x8zY+V9XWmBx6OLlb1RIQkBDSk9bYWNeXF9n/eXYzsnFwcDH1HRaVFvt1MvJzdbsX09JRUZL
+VWd+e2FQSkhHSlFu2s3L0OxscfHZzcnFwr++v8jZcVtWWl9neHBcT0Y/P0JHS0xPUlJQT1Jc
+bubTy8O9u7y9wsrP0tXX193k7XVeVlRPTUlISUhIRkZHR0lMU2Dx2s/MzMvKx8TCwsbM0dXW
+0tno1fVRQUnuWEpMUUxKQTxETU8+RcrKdMm7v8PD2P3Kv9DiycDV4M/abnzpUUZPWEU8PkJE
+Pjs8R2BnZNrAusDKwbW52Om/t+l02v36+EdT7kxCRl9MPj9VUD9O7FdAb89mcsjI4tzKzNbN
+ysrHysvKzNXe6m5aU1FTUU5PS0pOT09WXGFld+rt6e5zc+3t6N3a1djZ19vc4eff3u72dHv9
+aWpoYlxdZV5ZVVZYV11rdvvm29nY3eHi3d3k3tzb6O3v9fHo6HZueGVYUFFRT1BTWl9t7PF0
+7OTq5+Dr8Ong2NjU0tfb1djh4u3yZ15bVlJYXFhYXltYW2Bqcfj7e3/w7Ozf3d3e3Nzd393c
+5/Xo+G1nXmBoZFpfaV5ZXWBiZ2du/ezr8vTm5e3m3N3e2tnc29zo+//5bWVjamZgYl9aV1la
+WVtlfu35f/vs3+Hq8ufn6ubk4OLg2dXZ3uDpcnF5XVVXWVpZWlldZmtsanFzbv7w5ejn63Rh
+ydRSysvo4Od45v1fanFZSM3XPX3LUFXtYWZoWmt0dm3/3ufu5tzdz3nh3Ftf3NPz5d34+Plb
+Z1paVVZCPdpo1lxdt27f2Ml84v/o6FdHdslR8FC/4UjU4vpfeF902U1pTdZPTnDPZGzYWs9r
+YeHUUU/T8rtD7sHcy0fm3MxKQVxfXzzJOezZR8i723y7YNxRRd9O20fJW9BKxfHNzUzDXV5i
+Ss9M10Pc6UTQSclf++/QdEzbPMBTWHXUU9Hkfb5n3uZjx0nXYf5sdVpfzExO0+xZ1l342FVc
+x0p45ErBWUzLbVfORM3kW89nzMpfd8dB0Vk/4eRF3vfV8mP76mlbbnnedVtmy99U5uH06E73
+xFpZzWDYUFHQ6E17ae7MSNS+ZPxXaP5eSNrv32BN089kePZu6U9b6P3W7l7jaupi9erO1N/j
++mJPS15m2t/e4thkWmJjbm3c5NNRUOZa9VDay+n3YPFu6+Rv3eRrXflrWF3463VhW+/fXHXb
+29tdXNbiXu72y8tq7eDn421j2t9rSkVKUGtl3dJ5a1lKT1Vd7Hfz0n7o1+vh0trN4e/P0M7N
+6dnOcnzme9VeVfNm/E9S2Ek8RDk5Mi08W3DEurq1wdzVeOLJxby9w7/EyMjG0NdmWFM9S+5s
+z7u7QygkHyUuMk29uL3C017gzsO9u7e6w9HPw7+9xMXEzmNXeFlg4lj5ur3AzxoOGyc4yrSk
+nK8wJy5Tv7uxpqq/RTpQwbC0uMHdT0Jezr++xtRZP1/F1xoOHy1Hubmvnas9MiwtR72rpKiv
+yUlAVsO6vM7kW0VMZMGwrrbIXD05W80jDhMs+K2stqekxDwsKT+9tbW4u7+8us7O7U5LP0Rb
+1MzAtK+vucPbTDUsQVIgFh81xaqys6euyEgpJjfYr6auv85dUD0wPdm5uO4+QnDBtbSwr71a
+QEBUuquzMhcRGi/JqKKhorFBIxwhQa2jpa22xjslIDDHrKvLPDpZ08G4rqq07Ts4TMawp6au
+TSIYERMhaqaZmqXILx8eIz2qm5qjxS8kIiYzyaqlqr1KOD3WsqutudhGODpXwK+nqr8wGRUR
+FS+wnJSZrFcjGRwn/5+Wl5/zJRwcJk6xoJ2p0zYsNFbLuLK2xFg6N1DBraquvk8oHBoXHGqo
+nZqkzT8qICk6vZ+boLYyICAmNNevpqW2Ry0rQb6rqbDPPi8wP8quqKmzzksuISImIDC4rKOj
+v0Q3KCgwP7qko6vKNCwuMkDdvq2mrs49LjjNtbK5300+MzdSwayps8DS3dg4JiMcHku9r6aw
+yM47KS872quqr7XuPzwzOP23rK7aOissRuC6q6y11DUqLT2/qqmvv/xdbs7rPT49JR4pNc6r
+ra+tx0U4Ky9kuayszUA5Nz1T6cGytcNGLDFH2rStrq7BPDEzPsWvrq+7z9H9as1cLCYdFiBP
+waegqq27NioqLm+1r6uuyF8/Lzl4wrGw0mFHNnTAzb+7yt8+Ki086rWsqqu5aT83O1FwTD8v
+ISc7RtOvqKSpXS4rLT7NwLSssb5VNDE/bsSzr7XFSygpPE6/sLGtuzouLS9Zx7WmqbnMV0ZU
+SDo0Jx8tS03Fraairz8uLi83Ql+2qauvvexINCwvRcuwrbK32TEtLzf2xb+4vnhPTVDZwb66
+vs7Iu7rISjEmGh4vOseinqCpUCwrJiAsXbKjo6uvxDcsKitOvbmzrbvZdDguOkRbyr27ub/t
+Xl9VZd3Lwbm1srLNPi8gFxsvRdSpn6Kq3y8tKyIpW7SnpKeorlcuLS81Q2fAr7bFv8tfSD46
+PkvsysG/wclzSklnz760s7rFRy4sHRg70S/hnqCssNZYViccL8vjzqmgoqzcSksvISc+W9a3
+ray0bk/9PC078dzNwb65v3F1z+Pdwr7B5z5ILRgoWCMpqqS0qKe0vzkfK0ouLLqkqqilrb9N
+LSktKipYu72vqa+92UE+QTIxUPDjvbW2tra5u8djQzwzHRpCNh1Fn6q3paOtzywoPCwgOrK4
+uKajqLV+UUgqIi06P3u1qKeutbxcNjI0NDlOzb+5rauut8xdPCgaGi8nHE+irrOfnaW7SkQ+
+JSA2Uz1qrKerrauuyDktLygmNlvJt66srb7Sy2Y5OUlMTOfDubfDy8pFJygzJR8422DFqqap
+sLe1zjQxOzIrOGHbxLmuqa67vdc7LzA0OUBjxre2urq8xdlqXG77WV3Mz0s+Mi8xKCpAYF68
+q6uvsrjKTTg3NC41W+7dvLK4vbi4v9r+3lc4ND4/PVXKvLi0sLC6ytVuPjk9OjUzNkg+Lzxv
+R0TCsbSwrK235EI7LigsNj5dva+trK61vNJPR0M/REdGTmXn39LHvLe5uLW4xls9NisgJC4o
+Kl+/yLSoqayzvMJ4OjpBNC45Pz9ZzL2zsLGsrsnZ00c4OTo6PUFY2M/Et7a4uLrBzFxAOSol
+LCkjL1le0aympqWmq7PaOjEtJCYuMTv5xryvrK6ur7nE2lI+PDg1O0hUbNfLwbm2t7jD3mAz
+KjcuIitAPTvhu7Osra6oqbjcaUYzLSsuNT1Ccr+9vLeztLrAzOtbTUU/Q0xRYOjUysnOz9Hz
+W0g+QUNAQ0td6uLd2NbX3dva4/P+al9YT01PW2/mzcG+vb3AyuFdTkhBQ0lRXnTh1M7Lyc3T
+1uNmWldPS0tJSlRbZ+HW3NvY3uj5ZFpVTU1XWltqe/Tg3dTMy8zKydPc4XpaT09SWF5839jP
+zc/R3PhjWlZOTFFbX2JhaXvt4+fo3dro+XxhWlZSUlNcavnt4tTR0M3R3Obk83dzal9ZWFhi
+b3Xq3tjT0tPV2+1sZV9WVFtbW1xeX3Ds4d7c293e9W5iV1FQUFl76d/Yz9PZ19vsbF5fX1tZ
+WmBgZXPv4OHd2Njb2tfd8P56bGdfXFxhan7m3drTz9btb2NTUFVUUVBQVmV07dzV19nY4v9l
+X2ZsaXT2dW999fZ98ODb29na2Nzh4ut3Z2lmXl5mbnz06+bseW9sbmVeYFlUWFtbZH3r5enk
+3uT17+/+/vXu/Orc3uLi6Ojp9Pf2dmt7/3NpaWZjbv78/fj9+P90b2dmaGZlbG138O7s7Onm
+8X/t5ujp6Ov2fv50X2V+fPru7vn6+fl7Y2NrbGp0c//+furl6+/r93draG1xbW38+/3v6ufn
+5/L8fHN2bm5/7+nt7+3y+Xl3Z2ViX2VnaW12+/nu9fd5aXD39fDq7PDu7e3w8vTs6e/t7e/6
+8/v+/3VoX1tZYWdkbGts7uXo6vF9evr9eHN4+3pvdvbr6eTi5eXu9X51dXj7fnN7+X1vbHP2
+8/Hw/m1rbm9jYmtwbm5ud3n65d/l6uXl5fh0+vv3enb2/216+n798eTqdW5ya2lt/Xlwe/bt
+8/b6+/319vD9amhobW139v599u7y9/Hv9Hf37vd7c/99fXtoU+vcYmnh6nz5+NzoZt7oXurp
+ZGFhXmjufG91/ubs6e56cWdz+3xnXGT54uLp5Ob44tzf9+/q7fNnXVdaYmluanL3eH316OL1
+eHz1fW52em575+fr+3/k3/f7dGVpamJfaGlncvP29/Hp5e59+evt9vn19fbt6ujufn3r6nVq
+b3X5/mxjX2h8eGlvb2/36+/9/vbt8vn7bvzq+fns7+/o6Oz5eXN1a2hmYWpvd3V1dm18f/Xw
+8v97+vHwb3Bvdvz58u36eu/w/f76fP55fvjw6+ft/nFobG9vffn07e7u/HRqbX17bGloamdr
+amx48+jt+Xv99Ph98vr7/fDl5+ji5u39+vd0e25mYGZrbvv7dnl7bmhsdnJmZvnwffDw7O7x
+5u/79Orzb291+e71eWxpc/719HJucP39fv1vcHL55Ofz+/Z+eH13a2p8+H1+evXw9Pb5d2xt
+cXZzbWx0+vj56vB4+O/3/Pnt7fry731wfHxtaW93/PltaWhrdHv7/3T24+Do7O3u7/P2e3z9
+/Xt1am96dGZocHFudfZ8cXV2dG92/P1yb/z39+vk6O/w9Pr4/Hr/fntydXVwdHJ6+XFvdXd8
+8ero7PP7d3z59Ph4b21rbWdndXv88+no7vN8dXV29frx7n1xb3d3+n1xdv/y9nz78nZ2/HFt
+cXRyd3J6+vvx8ff1+PDr6+z7/P/79Xt4cG51/vr29Ph/fm1laGx97u789f74/W929PTy6Ot9
+bnBvbnd9+f1zbm97c2508Pp66ePp7PPx8/f3+n54eX57cXX1/mxvbGZteP16fHp+8/T18fP7
+7+/t/W589Xt7dPnwfHJ6fG9sfet0Z2lu/3h3ffbz+ezr8vju73X/6eXu8/b//nl2bWVhbP19
+aHT7d3R/+//26erw7n9sc/h6e3prbndycXB1e/l0be3sd3nu8H7/+O3o7P326+73f/94e/p2
+a2prcXFsffx0eHX4d2zz73v59fn17Oz0+ezk6nRfau7pdl9t5fd4e374bnf77/F7cnRuanHv
+8nLx6u/w8nj/7vlzdGt1eGR97/J6b/j8+Wxh++t/fuz09HZi7uDv8+fv9+vy8fX1/3V9b2xt
+fHlra2xkYnFvamZu8PDd72F08d/c6vLf3nv9d//+a/Xj8XJsbvHvc11bbnNpZm178vZyd/Bw
+ZXt57eP6++t7dP9ofeV5cfn47fDv9n/r3+lw9ufq5e9v7u/z+WVpZ11cXFtz7XBgZfDje2R3
+7+z6c3Hs6f/17+ff5/V8cP18amzs9Hfs5eTn/mx4+Xhob/Nzc/lv7+N+b3H5eXJvXGtuWl54
+6eZxWnDb61xn7drb921v5dzg5e/v3PBobnBydu/zfnB18/lpXF1kbPl9aWhld97i/nrn+Wlk
+ZW3v4+nZ19zd7Wdy/XxzdXn1/GFg+e318Wlda2tnc2p76On4ePt8bm9z8fN39ebnfWj14O7z
+dGp97Ot5a2z9eWr7/n/1/W165unm5uz5d/1tdvxhaejrdXRrbOr6aXn9cn5+e3Jtdn3t7fLv
+7/5ucOfe7mJl7urqbXXtdfDmeGP23u53a250c3Rtc3b+63hp+O9tam92+uzs9uTs+e99feve
+6vT7b3P5fGdx7Pl6c33ne19w3exzbWT66mtmZ/vj9mZ093dscfv093V17eXrc3Hm43Z4///3
+eHn0aWP36vJmaPnq4OptfOr3fHBs7OJ0/nFndnp2aHr6bvP2a//n62774PtuXmft7vv+dWN4
+8Pv0fm3m3n5kbOf6eWxlanTg6Gdibuzb3Op5/P1193xpZPrz7vFgY/nt9XprafPj/m1pau74
+amFv7vv2fHdubPPn8e/z7ebm/H3t7efqcWJz/WZfduzvbGVr8vR0cW1o/+Hybvz/8ej+bW/r
+7PTwZ2Vz7Od3ZWVx8ul9bHX76OZuXHvk8Hdv8eXh7P14b29u/fn9cm757/ZyaWJfa//68XV4
+7Oz3+Pj17/Du9Hn16/T9eH729PX9cWpmanN6fnVre/B+b2tmeO58burp+PD7ffTk6ujne3H2
+73p/b2Ts53n7/mVrc2FgfvV0bnR7dnPj3/5uePv592pr+X3x7ffx8fvr5vXo4ervb2lvd3Jp
+bG5scmNea/75em9mbeXm5ObxfnjveGds9vHw8ft4enp6cXX+7+Poemr06Hl+4uh9ZmJx8vlu
+ee77dfx/aWx+bnFxZWVoZmb76Pj36vHo3+52f37u5/H9++7y8Hlucnh0fHhra2p2dXVve+n3
+9/b29//7dG76/XPn2+Pu7/Lx+XRla31sYW37cm1obHj7eWt1d3n9dHPv7nv75uHo7fHw93z2
+5vR0a2ttevJ/fP7t8HR+e/vv83BmanF1dfTz8vZvb21veGducnJsbmtq/Ozj6evy9e3h5+7r
+9u/+bm/36+76cG1rbHJ1Zl9qbWlycHvu6urs7/Px8/328/386/NyeP517O17/nZsam9hXGVz
+f/d0aXz7d2x18uno5+j09Ort7On29fDx/fv3fXRpZmxw+nlsdXb7eHBtc/11b3vv6+/v8vby
+7+jte35qY2Robm5tdX59/fP+8+989uzo6vf/d3V+dX57cXD59Pj59/rz6ur0fX1+fHFse/pt
+bnv6eGJkc3hwbmduef7zdXb18Ovg6vn09f757O3t4ODy/3h9+vn4/2Vfcv9vYWdxbXx+bv70
+dmtqb/nw/3T6f/vq5evt5O367u70dWtzanF9e3L8/HZ9dP/5f2ppe3dvePX6e3p0dPTs6/P+
+9e/v7Ort9n5yfHxqa355bWv88vp2ZWlwcmx5+W9kan718/Du4OPw7ezr9nhz8/n883RtfXll
+aH72829qe/n59PR2bvrv9H5z+e3t6un79u/zem52/nx7cW12e3hvdHxtaW947vh3dHF+/W9p
+/O12ePfp4+fu7fL49ern6/l6/vb8cXJ4+vh+dnBzdG5ucnR9d3Jzb3F79+3r8PDw9vP/7/58
+b2NibHp3fnz76/R3fvz68Onr9fp+dW9oZWd49fn+/P3x7O7u6+Xf5/n8ff7++/54cW9sa3Fy
+bG53bnNpaX5ubHh8eG9ufu7q7vD3f3Z5/vPo5uvr7fHv5+rz7fTw83VwbnNteHdqaGxva3Pz
+9Xt6b3/+b25vePD58Obp6+z1cWxu/O3p6vr29XZyc21z/Hxxev5ucHZt9ujv9vXwfHf5/Xf3
++f7zenB7bm52d292+fl9b25z/H1wcH33+vXt7evp5OXk5O7s7f9uY2h99u95YV5iZWVsb2lq
+bW53efX47OLj5eft9Ovv9/Dp6fLq6/Tt/29vb3FqaWtrb3Jta311//l7dGptfH1ydH7u7PL+
+/nZ2+fXu6+Ll6en2/PT7+/Pv/mx1e3x7+Pv9fHp9b2hoeXNpcfz2+PPq93l9eGx4fGhpeHr9
+6u/+9+fi6/t9/f1xfH1+fvPn7vPu9/xwZ2pxcXR8c3J5f/Ls8Xh0d3Z4eHd87fhy+HVy9/1z
+eW5u/3d39e7v7ers9PDxeXp5fvrt5vt2e/jv8/X7fvT8bGZnaWprbG14dm59/vz9/vh6++3t
+7+/p7f77+H5+/ffu93B2+Xt++m94+vnv9v98cWttdnR5dnJwbHR59f35+e7n6vPy8nx8/PNz
+amltbGhy9PH4+fT2fn31/Pvt7/Ly9+nu+uvwdHFydv98d3Z2/HNqbm9vcHJuZl5pcm908Ojo
+5ujm6O/56+388u7+e/v/d3p+b21vbm5wd3r+/Htwcfj7+/f38/P78u708u3xe23++nv3+vhq
+YGRvfWxs9Ofq7nZ3eHZ2fvLz6fN6+u3zenZ1bmZfZmtyfHjt6uvw8u73+fPu9m989fXx7vV1
+dHVuaWZpbXp5enxufPN8ePbu6OPv+v54fH14fPx8en54bXD8entvbHt7fn75enV3c2118fd5
+fP58e3r+6ujm6Obo8fP39vvzfWpoZWNncXN2+n10bm92dGds+fju+Xt9+/Tx+u7s7Ojo7Px4
+a2p49+/v7Ojr8nRvdXhvZ2RnZmltdXf68/Ps7ezt+nx3cWxxenV6dHh8fnd67+vn+HF4+315
++PXx6+br+P3/e3d4eHV79vd9b2tveHBze3R8d3t8cG138/Lv6+33fP39d3R2evr6/n3z8O3t
+7/j7+vh9dnp5bG/v6Ot4cXt2c3Fxbmx1//99fXd7fHn3++3m6/l7dnF+cG12+vP/eXf78efg
+4unu7Oju/377e25uZl9ganl0cnR+fXBzffv89/D4/v3v8fXv9e3s8fr5enn29+r27+54c3J7
+eG92bW1xdnt4b2x5ef35/X58dXn09vfv6evq7vHz8fH8dnp+f/ru/XFub3Fud3Nyf351b3/x
+6vJ88+7z9/Ps83t3+PxrbWpgY2dzfnx3cWt1+XpzefH39O7u8e3m5OPk4ubv8f389/z2+XRq
+bWpmZWdvb3l1c29scGttbnvq4OT09+ja231QRUhf183Lz+FtYF9f8tzT1PBhT0pNXeDYfVhh
++3lv6tjNytPleWxoWVdn7t94UUdGTm3d19nVy8nJy8vO3m5eW1NYYFxQS1Voe2lfaXH8b3Bz
+aGz64ubq6f1jXHHf2d9oTk1OXO3d3NLQ19je/F5bWllfd+fp9uXPys3W63RramprdvP2+et0
+YWlvYlhdanx9c/3u2M3J0P5bUE5QWV5eZm/99HpjY2NfXVpaXF1jbW7r2dXU2dfT1t/6ZlZS
+U1dn9ubg3dve5d7W1tba29re2+hxaGBgX1xXUFJZYnhvZGRo8uDZ1Njc6nhvbW9va2NfXllb
+Z2tpbWlu+Obh6O77/Pf5d2Ribfnu8O7zZlxifuHZ1djrbWx8e3x2aXF9++/u93R69/3v6+vz
++evc2uD+XVZaantrZ11daGt+9e3l5ez6/PV/f3373tzm+2tcWmRyaWxpZmZq8+fr/3v46efq
+5e1x++5+fW9ha/3y6+/x7+7v7OLf5v1iW2Bo9u31fXP56Oh3YlxaXGBqcm1ub37p3tvkd2Vl
+aW7+8nt68ebd3N3f5evv8PdsX19kc/zv7vl4eGtmev1+c2htcvjt+P15/XF57ezi3t/j5u30
+/2xpb3J9dWx1fvvu6u16fHprY1tcXlxdXFhZaPn3dXf47eru6N/d29zf5vT36ePi5efo7n5z
+bHF6ev77cn7wcXv9e/t/cWNdWmBt9ev3b3Z0dPLw8uvi2tnf6Or4dn73+ndrbmlfXVpYWmJ2
+6vV08u3ybWh38u3k5/f46+Ph3NfPzc/U2+pjWFtgX1hYT0tJQD9OcNHBx9HcaVtgXWrp3c3K
+0+tnV197+e3o9Wv/fX3m593W2d7p+W95/enedF1gXF92am1rXl5jSUFOW+DIyc7dblhdWVZs
+cOjb7f/z49zW3N/e/Pfj4tnNyMjM32pkXmdsSz4/OjU7SXm/ur2/1lRNS058183FytjrWU5P
+VW3d3npcVmbf0srJztd9VVBXc9XKys3X63VfV1Ja5NzuSy4sNkPGtLe7wuFkUz0+UOS8uMTa
+VkZVZFNy3dzK0WtlVmbLwMTMfU9XX+TNzc3K0O1OMSYiKUO+q6qvv2A+NTU6Xb6vrLXYRTg5
+QV3YyMbHxNVqVU1d08K/y11JUWzOwsDEzt5NRlJMSjwuKS1Cx66vucxlX1lLQkfuvrS500o7
+QmzNyNzn2dvifVtd2MW/xmBKS1TlzcjHycvWe05BRD8+NyosPdKzrLfP+ExRX0pO6sW3t8tS
+Pz1U1dLX533f0dbe3t/OxcvvTEJLdczDv8PO4GRZX008NCwkKUPJrKmyxWY+PkVCYMa6sbXM
+TDg3R+bFvsPad1xUXu7OxcXO70w+P07Rvbq8xeFidNP6PDMuJSU347GmrLXDSTg4Nz3swbay
+w20/NjpO3cK9xc3abnD+5cvIyc1uR0JO3r+3usXgVEpg5E47My0kKT/Tr6itt89EOz49Tc++
+s7XMTTozPd/Jvb7fdX5dZnxxzb/HzGw/P07dvbm+x31Q69LWVzQsJB4rY7mlpLC9aDc4ODdh
+wbSstvA/NjRKz8i8xezcfFxfYOfAvtF5QDpFY8K0tr7N+F/Rz086LispJzXetaqrt85MODo/
+SOC/t7bHVT86PGzGv7zRZ3RVVHLtzr/ByeFDO0VyvbO2vcr88d1kRS8pJyAseLqoprHCXzY1
+OzpYybuwt9lGNTQ/2by5vtXlXlJdX+vEu73MTjs/T8+0sbrJ5fDb2VYyKSYfJVW+qqOuveQ5
+Mjc0Rsi4rK7JTjcwPF/LvLzN1dxfZV5mzb28yGY+PEjaubG4wtZdZOd4Ri8rJx8sXL+op7O/
+dDg3OTddw7Wss9BLOTQ++sq7vdDP5V5dXe7Hv8PQSjk/Usu2trvH+mfd6VU8KyomJDzOs6er
+ustMNTg4POS+sq68X0E5OFXPw7nE8GxaXvnmz76+ye0+OEJfwbOzutBdUm/sUzguLCgmMF27
+qqivvlg6NDY8ZsCzrbPNUjw5Q2HMvsDfZVVQYmzix8DD3UU7PkzKtrW4wNfm/GVNPjcwLCcs
+Psytqa267D83NzhH1rqusb9ZOzo7VcfAv878aHFy89XLv7/XTTo4ROm+trO6yuBvXUo7Ly8s
+JyxFxq6orbzkQDg5O0zNurCxv1g8ODxY08a+xuldWlx508m/wtxPPztAer+1sba/4UpCSV9O
+PzUpJSw80K+sr7XLVkI3NUPtu6+0v+VNRkBLdOPR0dXT2Nzh39rP0XxMPj9P3buzsrfHbEg+
+Pjo6PDQuMEDQta+0wNlZTkhGSl/MvbvF8Ec+SV7WytDieOnT0trn39TP1f5MQEjsv7i2ucz/
+VEhdT0Q8LSYpN1q2rbGzvt5lQjg6RvG9tbvGakQ/QlT/1c/RyszR2/V67NPX3nVLSFzZwbu5
+u8xeTU1JSTovKiYvUsGvrrnB0mZSRzo/V8+5tb3VWUpUdfTm+V7/2c/O3Hp53djcX0hGTOfD
+vLu8xc/bYEZESD03LCYtQ8muqq+5zFpPQzc5P12+tLe/50hIVl92Y1Vwz8O+xNr5cfXdc05J
+SmfMvbm5v8zkaG1aPS4lICc41bCtr7S7xNZKNC4wP9G3srbA0dzddVBBPEBjw7i4v8zqaVlQ
+SkRFVdnBurq8wMx1TUhDOzQsJSs7/7Srra+5zN5ZOjIxMkTRu7KwusHI8VVDNjQ8Uci3s7S9
+z2dIPTw/S9/CubS1u8XkXFZHOzMrJScuQsewq6qtuMllPzYwLS87bbmqqKqxy088My8zPFnB
+sayttc5LOzc7RlrTvri0tbzOX0s/NS8pIyYsOdGvqaapsb3aSDozLiwvOmW1qKKhq79MMCss
+MD5oy7mvra+8djwzMjtZybm2usDM3eZcQzcpISEmL2+0qqSlq7G94046LSknKTTys6Wfn6e4
+TzItLTQ/VdjCu7e4vs1nSD9FV97Hxc3iYE9NRz0yLCkpLUHKsKilp6y3x/pIOTEuLC45WL6t
+p6eqttpGNzI2PU3+3M/Ly8vM0Nne3tnQzdHfWD82LywsLS84TdC0qaWlqbHDZ0A6NjAuLzI6
+U8q2raqqrrrdSjw4OkFNX3r03dHKxcDAyMvN2O1WPS8pJCQpM03Br6qnpqirsb/mRDUuKyos
+LzhI476xrKqqrrfIaEM6Nzc8SWXdysLAv7y/yd1SPjYvLCsrLDND57yuqaeoq6+5yHxFNy8s
+KywxOkzZvrSurKyutL7UXUQ7ODk9S3PQxL+/wsrUbkU6MS0tLjI7TO/IurKura6zvM77WEtF
+Pzs4Nzc7SGnPvreyr6+zusbkUUI7ODo/SVbu0srFxcvyTDw1MC8xNz9U2cC3sK6vsre/1G9S
+SENEREJBQkNIWuXJv7u5ubq9xM3jWUlAPT1BSlZi89zQy8vYX0M4MjEzOkNd0sG5tbKxtbrC
+029SSUNAQERJTllu3s7Fvby8vsXQ6GJTTEhCQEFIUmLm1c7P0NtgST84NDM2Pk/exLmyr7Cz
+ucLTbVBFPj0+QkdTc97TysK/vry9xtZ2V0xEQD9BRE1f69TOz9bjX0pAOzg3Oj9Mb8y9t7Gv
+sba+zXtQRT8/PkBGUWTt183KxsK/v8LI0eZiT0dEQUFJWu/Tys/qVkM7NjMzNz1M68O4sK6u
+sLnF3VpHPjs6PEBJVXjdz8W/vbq3t7q9yeNcRTw6Oj1HWHvb1d5rTD00Ly4xOk/Quq+srK6z
+vMt/T0ZAPj4/Pz9BSFfixLevra2vt8f7Rz06Oz9IWvXd2utPPTUuLC4zPWfEt7CvsbO3vL/E
+zuhVPTMtKysvPu66rKalp623yV1KSUdKUE9PV1pmbk05MCkjJSs34bGqpaOor7bF3+lZPzku
+JyYoLki+raWhpKqwv9bf/mj3WEdISVDq2mQ4Jx4cHCY+waefoqSnrq+vvek7Jh4dICpJv7Gt
+rK6tqquutddFPz5M0crKxNBsaEEoHhsYGypctqCcn56eqa62QCchGxojLkG+rKuloaerr8xN
+S0dTycLHwsbMwsJjNSEWExgfMrqsp56bm5mduDsjGhsgJSxDYsapn6KjrM5oV0NM/O/Fsa2v
+sbvRYDsqIBcQFR0qxqWfm5aYm6C9Lh8dGx0lKzvEq6GepbXWT0dMU1X5vq+ppqqyvNlKLiEd
+FxIZIyzrqaCalJefrc8wJiMdHCQwWrGpqaiuvdRbPjpAYcCspqWmq7G+60UuIRsVEBckM9+r
+oZyVlp+rxzUnIx4dJjlztaeoqqu421g9NUDvvq2mpqaruc7tPiYcGBETHSg6u6SclpSbpbNK
+KyIeHSMvO9+tpaSlrs5WQDc7W8izqaSlqq297UYrHhgSEhkfKk+tnpaTl5yjt0EpHxscHiQ5
+vqujnqGqtd9LUlhez7i0r62xur99LyEaFRcaHCQ+wKicmZmZnKa5SiogHBsfKjrIrKWjpauu
+r7jGysrT1d5fbmhCODEmHyInKCsxPt+2rauopamts8VnUkc7ODtCYc/f/+Pd29fJwb6+xMnJ
+0u1kTkZDRkZBPT9DQkVIR0VLU2Dq0MjBvL7DytXjbllJPz9HW9nLx7+5t7e5vcLO/FdUVlhk
+eFxPTUU9Ozg3Oj1ASlru08fFyc3P1eVcTEdIVvnazsW/vbm4u7/F0uzl4NnU331rW09NRz49
+PTw9QkNGTmD04t7o4dnlalhVWnLt4drTy8W/vr/Gz9rf3NTV29zX0tj6WUtCP0FESE5UVFhm
+a3B9a2FeWFhYVldfZ3jf2dTMx8jMz9fk6OPk3c/MzMzR3OdlTkdEQkdPX+7XzcvN0ehnWU9M
+SEVFRUdMVGRz59XP0tDV3urs6N3Z0s/MysrM1/xmX2FeW23s2c/M0NTa6XZWSkNBQD9AREtS
+X/jf29nY3e99d2xnbHTe0M/Nzs/U3eDl4fDP0Nl+1cXT7PDRa19PS09MSkRKSktNUVdadHZ5
+evv57vH05Gpc38/d3tDR0unYw8Lc1MbGy9PGwL3L1PFZPy4pKiwpLDE/5by4s6uprbO/ZUtE
+NC8zOkBW8c65sbGyr66xucXMxspePDo6LSIfJCksLTrasKmopqSlrLtZMiwqKCYsQdK7ubKq
+p6uzsrK4wNPXz85oOzMrHx4hIiEqPeizqKein6SuuNc9LSYjKjI4UryvrKutrqyzw77Ayc7Q
+xcXYTDwpGxwgHh4pPciqo6SenKOwwk80LCUgKDtMW76tq6uwubi2wunLvsvbx7u91k00JB0f
+Hx0hL028q6ajnp+pus5VOC4nIys9TF/Fr6ywtrm5ucDZz7y8w7+8v8bkOCgfHR8fHiQ7xLCr
+pJ+fo6zEdk43KiQjLT1GTs+xqq2ysq6wusfGvr3I08zK0GE8Kh8dICIfJDq/rqumn5+krcJX
+PDMqISMvQEhiwq6prK+vrq+2wMjExdzu2vpZUTklHh8mJiIqWLawr6mjpay30kg9OzIqKTzp
+5vbIsKuxvby5uLvDzMi+u77ObGBYMx8aHScoIilyramrqaWlq7jfRDs6NiwqNujH7ny6q668
+w7y1tbvFx721udhOVk0uHhoeJiUiL9+wqqmmpKesttxBODc0Kyk0XO1c7bmtrbO4t7Kvtb7H
+vra64kNGSjEfGh0mKCUscLCpqKalqa61z0EzNDMsJy5VzNDKt6uqsLm5trW5w87NvbnQR0BF
+NSEZHCcqJSx/raiqp6Sor7zOSTEuMi4qMmfDyMezqquyt7a2uLzF0c/AvfI8OjwsHRofKisr
+Pbysq6qnqa+6v947LjE6ODI83b7Exbmvr7i8u7m4usDHxb/F8Eo9MSYeHSEnKi9Mva6rqKeq
+rrS+dDszNDUxMT30y8nAtrCwt7q6urq9xMvGv8FuPTw+LR4bIy4uKze/q6utqqituL/NRy8t
+Njo0N1jFwMO7sK61ure1u8LFx8nP325MOi8oIiAlLC8zP8uurLGxraywwfVPQDk2NjY8Uu3b
+y7yyr7G2t7S4wtX19t72Tj88PDUqJio0OjQ1Wry2vLyyrK66y9TkTTs2Nzo/R1TZvri4u7y7
+vL7E0eDp6+5wV0tJS0E3MDU+Pzs8R2Taz8/Ivrq9xMzW3mtMR0xOSkxo18vMy8O9vcDDxsfK
+ztnwX1hVSkA8PD49OztBS0pMZODXy8fNzsjL3mhfYWllXHLXz9Xc2c3Ix8nKycfGydLe6f1c
+VVFJPz08Ozk5PUFIWfPf08zLysbJ0dXX3vZpZ3fu7Hx76tvX1c7JxsTFyMvP2/VoW1FIPzo6
+PkE9O0BR/e3k29XLw8jX3drZ6V5TW3n/YWX12dnd1snAw8fHw8PM5f7q7WtSRj02NTc7Pj4/
+TejPzdPOvrm+z+Pp7WNKQUVVYF1y3s/KxsS/vsLJyczX08/X82dhVD8wLC85OjQ4WcO6vb63
+sLPA2GhPPjY1Nzo+UtLEwL24t77FvbnBz87BvMLcVVpIMSUfICs4OEe+rKmorba+0WRLOC80
+O0jpz8m7u8TU/VddYO7EtrKytba0vO1IOi0mHhsgMUvou6yoqK253khDSD48R15v0MLDy83c
+UkNGVGvWvbKsqq24wMjZVz0vKiggHSQ7+Mu5r62us8hMQUtaWmXwz8jP5mtZW1hGQ1rVwbm1
+sq+vs8Hd5dPPeT8xLCkfHSU6ZcSxsK+vuNZVTkxX4tjaz9BvXGZRTE9PXNjAu7axsrm+wd5e
+bt3Gwc9TNSYbGB80bL6tqqusuftHRElQ89/n0dH2fPNTRVFVU9rFvLOutb3F3Xv76d3Et77j
+Ry4fFxkmQ72uqaqsscpaQjtJ3MfT73xeaeFqRkRUXfnSybmtrbfG817t0NfavrW9YTcpHhkc
+K/uyq6mtsr7rSDs8St/O6l9ccN3X+VZXZ3rdyr61rrG93FJPfdPT1cm8tLxGKB4YGSvbs6yn
+rLG7TzQ1PE/TzGpX5c7Ewd1IQEtPXNnEtKutu+JLQ0/k2dnOvLCvvj8nHhkcMcivp6iyw146
+MzpK3cbHdE5l3cjEdUI+RVbZvrWtq7PUQTg8TtLBwb21rrDAQCoiGRgn/q+kpbLFWDo4PVDc
+x8PgT1ncwrvKSDY0P+q6r6+vuN5EOTdF1bq0usO9tLTKRSseGRovvqmjq8J5PzU6P1fFvsto
+SFXNurzePzI3Uci0rq+1x1E5OD94vra0u8vKurW86C8dFRcyuKehr8jsPC4vNV61r75fPkrJ
+usFrQkVTfdDFurO5zEo6P1Hry8G8t7rFxcK7tvQnGxMcxqyoqcdu/TMqM0y2qLRaPDxrv8j3
+7tTO2kQ/esK2uMzr3exYSEvaurK40ODEt7dhKSAaGkS9va2zv79GLThew7O+aG506NFsbczC
+zFw/O07WyL25vcxcPTpQ08G5trO8yMXI5DgpHxkm1r+uq7jBbDMvPvLAvszKzt54QEXYx8rW
+ZUxj4t/Twrq8y1NAR2bhy7q0tb/V00suKCAfNb25tbTB5VQ4M0/HxcnIz9LXVkJWz83R4G3/
+81BO3b+7yOFwZu1fV9S/uLrP/M9eLyglKEbN1MCwt95cQUDf31Pyxcrc3mv+1e5YdtPccVpQ
+bdzn39TP2Otva+3d2dro18PCwPYwKy4wNkXTubO6ysrL70ZAUFJPUGfCu8Xc6dziWERJYP5r
+e8rFzszQ0MtfP0pbT1nLu7e0vD4nMTYoK1G8srG6ubHBQDtGOjM9+MG6w8i5u+lKT1lPQUFh
+y83OuK+9Z0pHQTs88Le1v/V1ejIlLT5BQ+24rrPCwbrPOjM/S0xO3r69zNLEwdFnYmVNPT1T
+79nOxcPO5+flX05PZN7a6vrxdU4/Pk5eV2L7c/jXztrr4tLL2WRXUkhCU93MxL66urzEztpW
+QDw5NjU6PkVO8MW7trO3vtFRPz4/Rk1RWm3t3dPT0srHyMzY7WFOR0dKTE9b7dHMy8vS3npe
+YG1nWFJTVlFT7tDKyc/X4FxRXnnwZVNRXnNrZWbv39zY1tfX3+v7Y1plaWJ86un+aWdzePLe
+0s7Mz+L9Wk5TVExHREJETGja0NPPzcrN19jZ5v1x/e53ZmNYVlpUX+re1tPX2+hxZ2Zw5NTP
+1dbc7/9oan9jWFdKSEpKTU1KTVtccNfP0NLMyNbi2t3e9G/z6fLo4NrV4eX8WFVbZ+XVzcfL
+0d5pWlNLR0A+P0BDTl937PXh1tTPzMnJy8/V2/P6allWU1FRWvfa087O1eD7aXrp2s/MzM3R
++GleSkRCQkE9QE1SXXbw3tza0M7Pzc/R1N3a2ubn/2liXFpfZPjZ29nZ3tzZ29bR19zf/19S
+SkZBP0A/QUhNVGvk1c7KxcHDxcfN2O13cVtVWFxeZ2Z36Ovp4eHb2NbPzs/M0eDo+11SSEFA
+Pj4/QUZLUmVteOTUzcrHwsHHys7V4vj7alxdZmFidP7q7ObX19PP0c/R2eD3bl5PR0I+Pj08
+P0VJVWV95NvRzcvJxcXGxsnO1+Lp8WxqaV5cXF9fae/m4N/a0tLWz9He4PVgWE1FREI9PT5B
+R0tQXnPt1c/Mx8XCw8fIy9Pa3+x3cXZwaWZucmdt9erh2dPPztja2OxsXVZLPz5CPDo+Q0RG
+TV545c7Ew8bAvMDJyMrY5+79aV5qfWZj/3Zga+/o593PztPNzNXb5GNOTUM7PkA7Oz8/QUhU
+ZuHQysLAwL2+xcvJzufv5Hxhb3xkYXT2cm/q3d3h187U1M7R2uhlVE5IOzc+PTY7RkRAUe/8
+5ci+v8G9u8DJxsXU8uDgY1ds815XfvBhZeTb393RztfZ1tvc9FpNTUc4Nz89NDlKSz9O09Xq
+ybq9xby4v8rFxM/i2txrWVxeV1ZbX21sb93T29/OydXh0dNcTlNNQjo3Ozo2N0NXUFTXxs3O
+vrm9v7y9wcnP0Nj+XV1mV0tOWVlVctfZ29DNztHSz9LZ52JNR0w/MjY8NzI7U09HdMbFy8K7
+ury9vbzB0N3R2VtNYWtMSV1wXFzk09rUzszJy9DS0eNeTENEPjIxOzsyNElbSlTKvsLBura5
+vL2+wMjd9fVgTUxaWExPa/Xy3s/LycvIxMfP3998T0M9PTsvLjY4MzVFWl3zx7y6ubiysri+
+vr/K8mltV0pJUlJOVnbr39bSzMfK1NvOzftaVkNDPjI3PDQwOU1JQGLKycq/u7e5vby7vs3g
+1NZdVWNoXV7t5d3c9tvP2H114u1ZVmtUSlVVSUpZ92BES+BmPUNoWUZKanpccNrTycvOwr3D
+zcW9yOfTxtZdbXBp9VRFec1QSc7SRdvLPVfBUDr/40VPX0ZXzU04074+PMvISz/ext5qesy+
+zl3TusH4YcG2UD/JyU4/Wc/eS07WwlxC57zaOk27bDBMwe06Q9DVSFp63MVEScG/Tk3TwmNF
+2eNk6lxb1/ptZG/a3NzV8HW60zzMtmE63L1YQVZs1FpESsVjTEvMxlZf+cR+TE7dwkNV4nLm
+SUvwv1I7zsJyTEjEwkp83dzQa0/RyUZmvllEzsNNS8jiTFHGW21h5V3y1F1d28pO3tp+/Nlq
+al5y4EXt2UlQ1E9U0/A5z787TbVkPbzaS8FzX9/Ke1DOztY6usc69b1ZNMDHPk68VD282jra
+rzU7q0Iuu7wsXLFEObRXM63EKtOoOzPKvmE+zFFfvT5HxcFHTc7CaFLC21DO52fSVXnGTEi/
+TU27OubKTEjY0j9wyk5V1OZCy9FLdNlwdOlJfM73RNbMbnLg0mzJ6kK52Dbfsk85xsVDTVa7
+Qz2+/VppSmKtLUKwwzRvw37TOtDMvjRMrPMuzbZLP8xv98NBPrTQLeuqOTKsaja8X0HK0zfP
+xU9LcrE8P7H/N7FVNaxlM7fSPtVPwts0xcs90k5Csvstt7oxzcw1sE84tlo9u01SvD7VvTlu
+sjpPsztuvUZFuGM8w3pLymJJ4MRYOrhsUHlr2VldvT3ovT/P6XJf3dJMYNvEM9e9RT+xOkqw
+PUXFyTzM2VDJTM1P2NVIyNw9vT7KS1rTTbw+TMC+Me/MvD9Dskhjwjrmrzg5vbg0OK/GLc26
+OM3HNtq1Rzy9uzlOvtY/4H3x31RPyuNSS8rCNtLNSWPNTtvMP+rU7FNgd8PmQ2Cz8zHEwGY7
++b1Jbc49wb01Pq7LL2W/bULXU0u6ZzfCtTlgvXptVNnPSlrHR33DO9rN8T/NwUM/sU00r0FK
+wWI4xbo3V7ROU3jLbP3q4+/fzED5ymBIzU7FSUjNzj7GekK9TltexNg42q83PK9HRrpNO7jG
+Okq6xThOtE9PzT9Zv1s3zLw/bNBYu0Vvulpdxl5my0p24HjcQdjPPGnVUONC5b8+XrxKY9NI
+zNVWWv3FVmLpwUdywlPZ/Va/R2VFat7I8kbB10D2zlDyRMzYTOB47MdPV8TbW17LUOBgVOdi
+1UPkzVJO59R0UcxLzdRCYb5gTFXfyEzcTtXHXkPJvl5QWcRb/kHLvjlNxOVAwU1Ju1Y/ztft
+SH3DTM9HzNVSXW7PWdNJ97g67749zcw7W71jO8fWQrtPMq3HKby6K8a2MUusXDS3vjLbszhA
+tto12LpS/NzJTL/8Qs/I50zs2ttnWl7XU0hCOVFANztWYk/W78y0vOW2s8LDvsDLuuvZxNbV
+47vCNCU57SUdKjs0LzJEtqvE1Kejtry7ycDLPjdS1ExtwbKzrqyuq6+6OCgpIxsYGR4mLzdV
+rKKioqOdobHKYtw8KCksNj9Dx7GkoaOfoKWtfCoiHBkXEhUfKzV+rp2ZmJqdnqXBNygkJB4d
+JjzYt6qhmpeanZ+ksT0gGxYRERIRGy9IwaKXlJKSmqOr8SobFhcaHyQys56cmpaTlJqiqrc+
+HRYUDw0PFBgo6bOhlZCRkZahtUomGhMTFx0oW7Cfl5SWlpeao7C9VCAWGRIOERMXKFbEqpiT
+lJSXobDRKhoXFxgcKUS1npmZmJaanqOwzOU7HRcaFhMVFBw47sOom5aWl52ns1grHRgaHSQw
+X66em5ubnJ6jr7/PzkciHB8XFRgXHCtUzq6dmJiXmqOrvy8fHRoaHy1AvaSfnZqdoaKpucrA
+zy4hJxwRFhgYHzNKwJ6ZmpaVm6SvSyEdGhYZJDd3rqGfm5mepKWsvMTMTSYiJBYSFhkdJTvX
+qpmXl5SVnKnJKxwaFxUZJ0PCqKCcmJqfpqixw9ldPyohIBgUGBsfJz2+ppmXl5WWnrFZJxoY
+FBQbK1i7ppyZl5mfpai4+UtHPCYfJRsUGiEhKFW9p5mYmpaWobpWJRkYFBMcLU65oJqYlZig
+pKrHUUI6NS0iIiIdHyEfLUtus6Kdm5ibpa7KKh0cFxYfKjq6pqCbmJueoq3GbUU4OjgyMioh
+JCEdJS0ydLCqopycoKOtXS8oHxkdJCg+wLGknJ2en6awus9LSmVPRj0rKCYgHh4lLzhlt6ui
+n6KlqLPPOy0nISUnLUnXu6unpqOmq6yzws3e4v1hQC0wMSQjJCUuLyw80MG4sa2nqbW7usPi
+emld3txjbFM+QUE5aVs/vbbZv6u3xrS52NDpOTI5LyovNDM/WGF2xsTHvr/Lycl3W11KSEQ/
+SGLu4Mq/u7SysK6vtLzMeUo+OTIzODw9P0pWWFhZX/xfU1VWWV9bWVplb15aZuvc3c/Aure1
+tba5v89pTklGRUtTfNjLyMvIyutRSUI9OjIvOTs6Q0VJdGxW2svKw9y7rc3DtV9J09BD7MpW
+1bm/ua23yM7FxVs82MctJkY1JCkiKFQwKNqyuq6uq52mw76/YD0qKDo8L0a5rqqrq6SowtzH
+9D0tLj45Lzjnv8k5JEJRICU7SddrPbWn4fO1t7K/6bWu3VPSzedTX7uzxcKxsr7P4XpHMTEz
+LDU1OMrGd9U7MlcsHzA5MT9G1q2vuaehqK20uL5dPkdXR0favbW4u7C44k5OQjI0PDtCRjlB
+31IyMjk2KiUvVW9btaanrK+srMNkzcTeSk7Hv33dvb3I08C+Z0ZPOj47MDpOQjlFMy8+MS5Y
+YlW7u72xuL62u8K7wsvSZ37O/uHIxcXb8NzwT0heY17tRT5BNzAtLTphSV/X187ZdsrEzsK3
+uL29vL3T1MHG61rgcEFOSVu+y0dOxfw4Lzs/MzU8ec1LPtTWQEPdvcLLvLC4zsW6vr66wMb8
+T+reTj9rvz0w59VUPzNQVCEqX0I+/cK3yTtYvMtW0a6wz9S4ssdguq/ITU7j6TY+t7xLQbvI
+NDc/My8wLT7nS1XKb1fR12Taw8XJzL+3wsy8vsnka91UWdLOwtfNustY63k5NTArPC8qUfVW
+2+xVblJK08LPur7Lu7/azbvM3szH0mh7zr3Ebsa3zUdO7UIwL0s3JC5dPzE85LjJSc6xvkxG
+yLnfY8CyutvWwMZtcc3D38+9vcnUv9A9OUEsLT09Oz5t5TsvOD5OSV21rb7szsDPX9K2sr/L
+v77QW9y7wFtvz9dN/c1cTFtCSfYsJjssIzlMX73PS7u192/Rv7fF1721x+vSxMHNz766ynXa
+1NjX7tDVU2tOb1wUDjpWJECnnp9iJs7FJSW8o6y9zLWzQDm9r89NVdzSTWqyqKu2wmxFOCxJ
+z97CuKwzDRQ4JiraqZmfLiu+/ikpXKKfzmausEs8VMO1XjzKt8bBsquux1poRy47V3m7rr0V
+DCE6LS+xm5m6Jz26Rio5va27zca4utTZ2FdPXU1c0riurqyvxlhDOj1BPr+qsrIfBxM9NDu3
+m5amJiDoxC8rvaSpvi5DrLx26sa73TY6xLGur62sv040OWBOV724vLM1CQgnVt+0npeaShsq
+8/I7/aiepkYqPnXk6My3t946Qta1rq+uvVlDPEblyrqxx87FIggJKcimpKCZnzwdJjj0zcGn
+oa1hKStF5t7gvrjIPzzBqaiwyFlQQkFgx7OvvOxTQikSDh3+rqOkqqW8LikvPOG5rqy370Qt
+LGW5tsFfYm9LbLGlp7pGOkNDQ2++rKvVRGBaKBEPIsCqp6ulptkrJCxLtauutdFNQCssxqmu
+yEg0PmDMr6mqs3gzLTNPx7qxq633Mk1KGhIbO7ajo66mui4lKjvIrrW5u089PTJRr6/LTzo9
++cy7ramwbzAsN13Fuq+stddBOsS6Hw8VL7mjpq6or00nJC1tsq+xvNToQC89zLzXTkJqvb7E
+vLbGVj45YsTHw7u7vs9KX7q4SxsOHFW6qKmnp7woHiYwwKqrr73NSDEsOM2+vsDCwM7wUHjN
+v7jC3mrWyfdvysG/vtlUVVEtExUvwKerucnKPyg0Tb6srr7T6DUsPcWvsbnCyN88NEy9rq21
+y9t3RD9E/cK88UtuV+fW1ugmGSM5/snDv7C0QDE6UMjDcVm3tPpPWMuzuNfjx9T8WE/RurfG
+2tXsXUU6PuvF0GNZduDlUCcfLztAS0XbsrJ9WdDv1M/j3MG4zuXm2bq2u83ldU1IQVfJvL3P
+8FhKRjtBT0tKQUZWzLu/yHpMPi0pNXTDvMDd5NtdSVjLvLrGefjU19fMxL/M715VUFFdaevU
+zNdfRkNJQkRi3crG21pFPz1GVfXGvsDOdlVKTEtU3Ma8v8xnR0hMaNDKztPdd09QWmb119He
+bVRHSWbdw73Ez91cPjxFS1/T0dzY2+xqWVZce//UxMXH1VxPT0xU79/Y3VVJSExX9tbZ53hf
+XHx4687Nx83fZUtHTXTNys7h6f1tbPnga1hSVuPV0djqaFRRVn7k3Ohw7e5gWuttT0tb/ODd
+68/N+F5ufeL3U2jk7OXsan7+e95/4dr+XV9r7Mrc5t9lWWFhauDgeOlsT1JOTmjaycvT721Z
+UUxP3NXe4fx+/uhr7NxmWGRhXfve7NrW9Xp44G9hefzW2vD53eRpfmVTTVJY7dXf187rUlZm
+ZXn96915VFNiaujm3+v76u3689bi43RWZWz+/X5+5n1cbWds4tnp3N1eXmJgW//ebGnwXFjs
+597r7f9q+9vra+DZaVHe0ktPwH5O7d3S637a2Uvf6Uv1xVRE18r9Pk3GSS/OvT8817h8Q87B
+4V5f1MJjQ+PGbEnpzu5b6Nfkanfc62nr5GxeXe3sXmp6Z2daV2lub/v7d21rc/Pq49Hfbf7o
+eWt9fPL48+5lWnLr6uPd2e9menNtb/BqXW90bGtnaXVn9uN7d/NvfOzi4Ors5e5z9n9reep3
+cu7u6+9ze3Vn8+9r8+5vb2hzdWNpbXBgYXFr+OXm7PLm5WZ64/Lk29ve/nP7e/Tz8nV77WRi
+aWt5fXpvbm3272ZrfW9+eH338fb79u72fH1sYWJ66u/u7/N9cO3i7u7q59/n8OtxZ3N1fnVt
+eHloanh1bHTu7nF38u9zdurscXvzdm97c2Jfbuzv5t7id2n8/nzy5vZ1b2psbn7z7fT6dWt3
+9P3z3dvh93x1dmhvfW9uam9qYGf/dmppbG979G987Ovy7OHtdXb5+fvr3+Tv7er6bnV7cW/6
+7n56/W5oaWZnZGZkZm14enX28vHq6vP59PZ+dPTf5O3t7+7s7nt2dm5ybm5uen5tb//7bm19
+fG1ybGpwcPDu/v718O7z8fDwfHr+b2dsc3r2/fjxfW/48Hx4eHlxbW5zd3N88PV/ffPr8PPq
+6Pb9+vX2dfXvdWx0em9rbXv++u3+bGlpam5xcn39fXR9fVPazErO7Wv8TFvDxVd7v9pkXlvq
+TF5r92R0aVzadu3NUV3d7V5ay1fiaFrYWdxjzdTg7vHdZWTVTvfuU+JgZmHldV7rb+ZX9V7w
+atVlbNtX2ljaXuja7dbq3eN53frrau7hXtlW6ltwflXSXFj3Zmrmal547WZS1GVmz03d/O7Y
+XdJo1Ovu6dZgyWLY4FrMRcdMW+tNclBWT1NRUFx0Wf5q/Nfm19rV5MjrzdXgzOTR2drN2dTc
+3cr+y194RDkxLzgvNzhD5crBubOysbrEz/1GPzg5PEFIWtzKu7y6vL/Gytz1XFVfXnN+09XF
+vdDeXDkvLiwsLzM7X86/tK+ura+6w/ZKPjYyMTQ4RvvPvbWxrrG5vNRtT0ZFTVtZeNfLwbu9
+wkw+MiksKikuOUfVvLiuq62us8POYD87NTAzNjxR1MS5srOwtLvF2F9OR0JKTld35dTEx8nF
+yc7VSjAwMC4uLS8/Xu3Fvbeur7S3wddzRz4/PkFJUXrNwL67vcC/ytdyWU1PSENYXGjU6uHF
+5N/ecu9bTU9nZGRFPkdHSkdJW9/m4OTYztPb5O/87dzh4tTd08/azd3M5Nbb5td0eOpw92bz
+fW7sR/FPbF1ba179Tl1YX2hd/2FfXl9ccmh8cu5t/Xhz+G/d49PZzc/wzezd017ddOtb4Oti
+cWhg33F43+zcdtxX3u5k3VnmXG5RZmRbX1jfWGv9a2L+UnFYb2li1XXN7NjY3c5s0nXvX2T7
+Xlt8X2ziedPf0urU5e9gaW9lcV1iam9n7Hnr5v3xdXL+6PpeXmNYYVxj4Fzy6eXu99xwZHp/
++3JrXen06OX83X3ieOTv3O1ye9/nbvBv+Ohpfe1sZWx3bG/t8t/rfeRsdl1scPxXXV1fXFlt
+X+tqdPze6+Ll4+fq2urgbNjf6uHl/ObY5959/3Z3X1xdXF1oc/h37Ofo8v37aGZaWlNdX2lb
+aXzo4tza2ubb5XlwavZ7eW/v8d3z5tzv/efqevlz/HJseGxsb2fqfWXi8nVt42N3bmtdanRx
+cv19/+/r4N/vbfXsZF9ranhtb/fq7uf+//N3eP30YHL5f/nu+Nrl3njz7Gr8+3xmdGzreWfr
+7+Zt423u8t7r/Wx0X2hdaGR6aGvmZFx4ZGzteH7e7m32b+jg9uDc9XXq9O7lavx4a3tkent2
+4X7429/m73Dwc/xda1pfa21lbG559X5l6ffwb+7/YOR17N/u+f7n6eF87njf/W93b/5ecGf9
+dnd05mr+ZHJv+tx07uj472Bz6m586/FufXtb5nZqfPVf9n9eed9+d+Fv6uR0ZebjbPTh2//f
+8ej3/WFqalp6XWT5V/hlY17m7Pxy+uvn7mHe9ORz6edz4ftwdfHn9Whv7n7qbXbj8Gru/O5t
+aHFibfX+bfTranf/6P9ofmpt7HVlbn3xbvx9+ufq7Pni+3399O7oZd1iZu593+ft4ejtYO9i
+c2xmcup6YGh8Zv3u9N7xaW35Y2jwYffrfF98XeP1ddzlduvX9Pj4cOB24PPp9HJd517mV938
+7vlofWbPXu5e32Fq+1tnd1lo4Ptr6ubve+vu6fn9+H3i+GPreXB87mb++/ni//zp713ha2R5
+aGvtfHxj9u5c2mXZbdj9cNpl7mLk9Wj+4llzfWnaad9efmXrZm1/W+Zt6/b3X+3vcHp+9vHf
+e2948nlpdWfkW+J37OfudW3peN3tXt7j32BPzW7ezu7O32/tc2V3SV1jSmNWXUvs4O3S5NNj
+VmRYVeNy9Nzo19vU2N3U6+7OafT4S9dX9t1qX/d8dPlmYV9eVf5cZWJcfWFj5W395Pv97+fb
+6/v55eLe2tba2M3i6cvVdO3PXHpgRj86OjpGTF5u7dnXysPBvsPN22xXT0tNT1Vdb+fb1s/M
+zMrOxc3Ozd3Q3+NnTjw0MC4xN0JR5NDGvLmzsrS6yPRIPTo2OTo/SlPlyr25uLi8v8PL0dvk
+4+Pb62ZPPDAtKiwyPlbXxr24tbSys7vGaUA4NDQ2O0FOZtzJu7SwsLW7yM3lcW9ibPzucVk/
+NC0qKS04U8+8uLWzs7Ozt8HxQzYwMTQ8SFR918i+uba4u8LM193f3+bg0MfDyeU+KyQhISg1
+cbqvrK2tr7O4wvFANC0sNET2ysbHycjEwsPIz93o7tzMycjIxsG8w+09IxsZGyZTraKeoqmw
+vMlfOSslJTBkt6inr9JANDY/VN/Kvrm1uL7P5OTXy8PCyNBSOSobGBslXaadnJ+ux145LSon
+LEy6p6KnuVAuJyowRsq0q6mtudZMRVzQxcjRzsKzrcEsFA0QH76blpuir7/MOyIaGyyznJul
+5DMwNzUxNkW5pKGpxj88asrC1ExH5Lisqa1LIBUOFSjCn5ean6TIOSUaGirRo5qgtEkwMDMv
+LT++qaOryFJKa8zJ6W9qesq4r6urfioaDQ8jxZ6Vm6Wp1TQkGho0r5yYpO0vJy08OTjxr6ur
+xUhP68K8v+dwY1nlwbewqrM0HxAMHPKjmZioqrsvIRwcOqabmqVLKiYpP1NXzrCvutk9UM23
+r7PMRTk1RMesp6qt0CgYDQ8orJuWm6mzOh4ZHS+pmJqj3ikiJDBWx7murclGPEXIsK2xxD0t
+MEe9qaWmrMI/Hw4KFWGck5ais14iGRsrsJiZpNMtJSMoSb+sqLD5NDBCva2orME8KSpJuqqm
+qKmz5ioYDAshpJWUnLdkMx0ZJmehmJ67PCkjJz/Eq6e8SDEuSbWrp6i9PiglObyuqqenq8NH
+KRcNDCudk5eizkc2HhwsxJ6ZpXAwKict6rWprFYuLTbPq6enre0uIydTrKWlqK+9fEAuHA0M
+LZqVmqhqSkAfGy6/n5mmUDQsKC/ZtqqxOSowV7OmqKy4RCgjL72lpKuwudBxSS8aDQ9fmZic
+rFVGMxscObKdmqpGNComM8irqcMrKEHEraerr8QwISdYqp+mtr3ISFTNOyobDhyrnp6hyzVP
+LB4qz6ianMUuKSYr1LK1tVEzVMvCr7C+3zwsNc2vqK7I2dBXW7jCOB8QEUOhnZ2rU0c5Hx9D
+rp2arDUmJSdLtrazzj1T1uS+srzGdjc43bqytMjYzGVSrqxIKxQLHqmenJ/XO1QnGiy7oZid
+TSMnJjS7s7ayyUlBQvOxrLnoSkFa08vIxcXI10vKpbctGw4RRZ2do6p/QDkhHT6onZyrNCMo
+L0nGtLKxwToxTryztL7a1dlORF/Kt7TJb7uqwCweFRAlrp+fpck5OyskN7iooaXPLSkuOdy0
+tLa7QjFKxrautMvW70E+WdO5sLzNu7b7Kh0XEyatnp+lzTM4MSo7xK+mpL0xKysu27Kvr8E8
+MkXpuqyuusP5OzU/4rOss7e6zjMjHRAXvqCenq8vL0EtNdnNu6KkXzAuKDS7rrS2zT0/VV/E
+rKuxvkUuNEbyvrGztMUyJly7KBchPM2lp8tj2lVS00k437W/0947LWavu8rTU27F907Btb/E
+w9plXEg/XNfX1uRSVM/Ju7gzGhwkL9evrKipwEE1LzBIxrSvtddGQ1D/z8C6t7jTSUBEYMe+
+vsHQW0k/PEBM4M3o9W5STEpGSti6veZJPDMwOEFgu6+2wNZPP0NSctXAubzI401IWvTY1exx
+2s3W2Nrn5+bf51ZGRD9CWNjFwMlkQDo9R1TlzMbH1l5OXODW33RfT1T229bW3vt3ZV5s6dbL
+ytXzWVvj09prSURT7tDIx8/vTj06QWHSwr/J61RKSlN529ngc2Vw6tvT0eZfWl9fbeTc4uz1
+8tvZfU9CPURk0cW/xtthSD0+S/rFubnC02NHQEVV9NbNyczkWU1MU2js29Xd7XdaT1Nj6tXS
+411KQkZc08G9wNZOPjs/T/DJvr/N+VVNTl16993Oy83Q61lUYGZr7OPo6XtVT1vy19LhWktG
+R1nt08rM311OSUVIZdTFwsrgXlJZ7NXW2+X9euzsePvt9W5t9+bc5HFgW1xfZWpmX11dZezd
+3OZ7bGVhXmVlcOfc2Nv8Xl5v7uDZ3Pt3cvve2tzobV5bXm546+V9fOzqeGtfWl984t7e521k
+Yl9eYW5++Pt3feni497h73FlX2j9+u/e3ebufGVudHTz7Ozr6Pjs5uxpXV5gdfHr5+r6alxY
+XGvq3+P8amZm+ufq6u3+bHPs397e3/ZpZmNkanJ4/e/s/m9qZGBue29y+e97dvr38PHx7/jv
+5ebz/Pj17OnueGJjY2j07Ovt+21oZWVtbnBwbXn6+nZv/PTv7+/u/m9sa3B+9ujj6e3w/3Js
+b3v08u/xaGNxe2568urw+Hdud/nr7P74em5uZm735+zuc2RmZV9eX2D/6/x67Of689zb3+Ln
+5OZrYHP/9fb2/Hh5+XhlZfrwcG/v7fd/cmxiY3Xsem9qfuzx9vv/bfFwcOjpfHH359/r8OXv
+6P5q9t/m9vrt4X1z/fVxX2JnX2z7cP5jXFdfWVtkeOfl5uzwfOHY19jb2eHp6+3l5HNmbF9X
+X2hgbn77+Ovx7O7+/vbq6fR+b3V6fXB7/Gr6fm549nZ6+Hv673lk6uRlZv17c+99fPh2cHX0
+8vvu7erv9/z49Pfs9Xv6f/ttdPZ/+v9zZWpqdPj76O97d3F4+n707PT7fnXz6/jr7+zvdm5r
+dnn48u3+b2xmZG33+fNvcW739W9xefl9cfZ+7+ju8/Ht//p++u7s7+7v+/T18vR593ltcmt8
+dWxsamxuanB4d2tt/35+8/B7c29re+zm4eT48/X69errd/x8dn/9+fDs+W5oZWNlbm33e25+
+e/1+8/jy8PT5fHV+fnX5e3zv8+38fnJvcm5sZG5ycHdxeuXx7+nu6unq7ejo5Ovh19vf3+xu
+X1NLREZHSU5VZvjf3NXT1tba29zf5Ovy7fLn28/JyMnLyMbIzdldRj44KycrMj1P4b2tq661
+v9BfPTMyOEZc8cq4srS8z2pQSD09SfjNxcTAvMHReu3V1/nlwr7HWzkvJSAqLC1IzrClqa2q
+tc5ILSwzNDtW0bOsrrO70ls+NDc+U9zJv7q6xddrXPTg7NzNx8LKUT40JR8rLjdnx62kqK2u
+wWU3KSsyNT/1vaysr7fE9EEzNTtH3M2/uLi9zGZlfWLp4NzMxb++0GI8LyAbKS40dr6pn6Ww
+rb9ONCYpMzI+yrOoqK+1vk82Ly46Rl/Kt7Kzu8ziV0NDTVrs1sC5urq71TstHhsnKy3ytKWf
+p66qvkUwKC0zMD7Ita2ttLjLRjcwMj9J+MK2sra+xttYSkBOXnDUwb++vsTMTkw9KyEgLjg9
+9bKnqLK+vd45LzQ/SEfuvLS3ur/L5UM8RVhnacy5t7/O0dBjRkdfc1ho0sPK8c3NXFpd4843
+HiM7KitEuqSszriu3jgyTsxbR8evt7/AvcRaRFN2UU1r0MnV0sC/3E5ERz86QebGzsnG3llH
+Wuxy3MXC/Uo2Ki4wMUhrvrS7x8/kWkpL2MPBu7i7xmxm8Vhtct/X33Xfz83IzsXKX0VKPj5F
+Q9HPTnXYbHBNTuJKOn7d38ZVSUY8NzZP32bcu8nFyNS6vMzBwtrkbOrS6uvN2dvPX39uT3Bi
+VFpTUFFZbXZ81OBabGVSRUJZXE/+zN3v6ks9MjA/WH3CubS0vcnSfF37etfQzsfTz8vJ0elZ
+SFRPRX3Wyszc2l1RTD5ATUtNVG1n9M3Lv8rV20MxNjUyM0PEury6tsRuTUxeV3DHub7Ev77J
+823e0m1adWVUWWbez9Tx73lOPjk8SlRayr/O4OZeYl1GUVg+QGhMPEvVw8zd2tJVQEfsyMK9
+uLPE1dJvS01+2NTbzcXSc3zn815TaF5OWU5KWWdpXeB7T01lUUhlTE5XVVxYZ2FrTUdJSF3i
+3Ma8u73HwMTS+8/I4tXLydPi0c/vbV9UVExLXFxU/O5dTEdESkRK1dHj4PTlU0Fc31FR2GRb
+TzxDS0Nzvb+7uMra3m/q08/Dws7Q197T1c/L1mZOUFBHUtfPb1lgVEE8UeFm6OZm30hAWlV3
+1m50YEA9RF3c5dTS3O9NYdDHwsS/wN1adPXXycS8wetfWVFdZ23OyN9+XlJYRkF4205RYUxR
+Tl/ZX9fmSVdHP09zZ1JTS1h+TWrQ0s3LycXAysvGy83X3eFp+9jUyszN0ef1XU1KTVZ5X1vg
+8kRIZltNTd52UD9L4dxfQ2NJMzpMXunky7a82dLJy8/ezb/MztHfzeZnzdHezdDg9V1ZZGFt
++lpLVfFXQ+rSTlRPS1ROTm0/N0s8N0dc5OvnxcHN0s/QyM7VwsfOycrIys/Pytbo39bM2dnO
+9GlrUUlSREVeTnlUSldnTUs9Kj89LzzowsLL3LvGT0/hytTWv7rEzMnBxdv41dv9YN3I0sXC
+xMx3V11GNU//Tk1r2drQzyofUiwiSMm2scrNrNo1YV9gUE6+utjPvbrEZezD/Eda4Mfa5bm1
+w8/X1F89SHpST9zHvjkcJzwoKE2zqa/UurBmNTx22FtfxL3fTujBz1BwvsT8XuLN6FzbwMPK
+w8PQ615t0t5PYt06IB4sMC5Iuaels8C2zTozOlFtXtm5tcNZSlxLQ1rpzsLTy7zH09Dk7Nlg
+XMnMzr3Gvb1SOi0nIiIsOma8rauttM57SjY2PlnTx727vsbaTj1KT0rmyr+7v8nK21daWmHf
+1tHGwszGvOE1LS8oJicx47u5samuv3tBPj05P9m9ubvCwc5KNz5eWWXOu7bA4/LaXEthzb/J
+5M/Axs7OQzc5KCMlKkS+v7WkqbXKRzw/NjRiwbqzusvOYTowNFLp7MCysr3fVPbwSVDavrvP
+48e9x9xbPy4nHx8xRHu2qaaosW5KOS0xQG29srm4uGFLRzU2T9jPu7q6vOJQV1xTbdvDvMnU
+xb++xEgzLyYcHS9YwrKpoqK0Rjo2Ly40X7Stt7i6zmo9MThe2NHBubG52FdYXE1N+8m+xdXF
+u8LPcDUtJxsaLUpwsKagnq1NQjwsKzFJt6y2trS7zUYwM01OTdu7sLTM5c5/RUJP0cPa3b+6
+v8DIPysnGxopOE+zpqGep9ZaPSsqLTjNs7awrbe93Tc0OTxDV9G1rrjCx9N+SkBO5djh2L+3
+ubzKOCsiGBwqLkivpZ2bp7rBQionJy1Ie9iyqqqque1SRTIuND7nwLuyra+5zldMTT07T93F
+uLa65T0pGx8nIy72uaWepaemtNtLLikrKSg0W7+pqKyoqrvrPC42OTNDzry0tsO/wl5IQj5M
+UExZXlZnVkBIW05OVUxU3+Hgxb6+vsx0UklIUVpt2MzHxc3Z52tdUk9aanPt5N7X4nP1enTn
+e2p1aVxhau7SzcvL2XxVR0hVU05dWEtKRUVbfXDh1tXLz+HW2N3R0dLP1+Tm/lpjXlVofN7U
+297vaWRrbfrXzs7LzuNmSj9JTz47QkNJW2H1y8PJzNLe2+lfZmVk6uhmYm/24urz29bbzcvb
+1c53TlDs0eLawb7Ey87M1WlCLCQxPiooQc26r7Czrq2z5z4+PDMxLzJQy8nFvLKtsMHc6vpQ
+PDlDWvTp0ruytLzDwsPRYkc7NiwhIS87Mj3Lsayrq661u8VSNS8xMzEyQsm3tbOxrq+931hI
+Pjs4OkZm1sa8tbO4wcrK0lE7NzQpIChHSzhMtqqssrCutsfcTjYvMjgzMUfDu8O/tK+4y+f0
+V0I9P0RL/M3Hv7WutMLCu8o+MDMtIB0qSEE536ukqq+sqrpoTD8vKCo4PjpNu62xuravtNBU
+UU0/Oj5KUlrcwry6uLm9wcXaSDcyKyAfK0JDPX+toaWtrKu13z82LigpM0BHab2tq7O7uLnP
+SkBGRjw8T+/8/cm4t77Ewb6+z0s3MCkhISw4OT7Rq6KkqaqsuHE6MSwmJi5GdNS7rKitt7u9
+00s8PT88PE3r4+rNurO5xsi/v8xTOS8nHyErNTg92qyfoaiqq7TeOy8rJiUsP2/Uv6+nqLG9
+v8pdPTg5OTtGYfxx2byytL7Nyr/A2k06LSQgJzM3NUDAp5+jqauuvVg5MSokJS9N2c7Cr6io
+ssjW3Fc9NzY5QVTq2tbGt6+yvcnIw8TQVzcpIiEnMTc4R8SqoaOpr7nFYzwvKCMnN3fKyb6u
+pqevxvlgTzw0NDpDVPDNw7y3tba8yuDk3ORdPi4kHyU2SEM+c66goKiyvcl9RTQqIyQwar68
+vLGopazCUkdDOjc7QUZKdMG3ub2/vb3J4/x4X11RPiwfHi1QXD5BvqOfpa66yWVAOTEpJSpI
+uq6yt6+qqrhVNjI0OUBKTVTjvLG1w9rUxMTYXlhl6+ZpQCsfHy9YVz9Lt6Kgp6+850E4OTQr
+Ji7Vraqvt7Swt9g+LywyRuTX7d7AtbS9311l7dzX2u3i08zcTTEhHSY+Y1ZYwKihpq28VzYw
+NTkwLDjNraaorrnO/nNKMCgrRcK5vsC/wcbM6kY7RNW/wszEuLfA20UnGRkpTlVKyamfoqaq
+yy8mLTgzLDXes6mioq5jOj9INCorOeK2rK2829zPbj86RfzKvLW2u727wWk3JxsXID/ZzLWn
+oqWprtcrICQrMT7auK6noaW2VzYuLjI7RVHMsKqvwOxLOzc9THLZyLuzsLa/ydhQNysiGxwu
+3rqroaStsrjnMiclJStNu6+sqamuvO09LSktOXa8s7a6u8LqTD0zN1XEuru8v8LBv8TYakQ1
+MC8nHydG1LCgpLS/zUo4MismLFS3qaGjrr/jPjMxLi463rWurbbWWVFIPj5ATtS8tre9ytvX
+0NbncVREODEoHypA5a2jq7vKTDU3NS0vR8OtoKCrv1AxLTE0OU/RvK6qr8VOOTQ6S1/73cy9
+ub3I4F5Z88zO5X1eRz0wHx8yRruiprjMSjY8QDc5W8OvpKSx1z8tKzREbMW5trK0y04/NTE/
+b+DFvsbIxsvW2XZUVWn53cvAv9ZKNiEaJzjjp6Suttc/PD06OEnZvq6or8dVNDA/asu7u83b
+5WNSTEtMTe/JycveX3bn383P4/X24tHDvcHTSTIrHhwtP7ygoqmxVTExLzJG2r6wq6y11EQ0
+MzxQ2MC+xsvO0eVWXFtNW15MTVXyxru3ucf8VEpO7s3CxN5YPS8lHSQ31qSepbFLKCguPsm1
+sq2us7v2Oi0tOPK2rq+72U1ER0NOY13d409JTmnJuLO40UQ4OELnwbq4vM5YRj0pHB4tW6ec
+n6xdKScxSsa0t7y5vc1mPzY8ZsGxsr/uPzpEWOTW4uv9bllGP0T9u6+yxEs1MDztuq+vutNT
+OS4sKCIr/rajn6/mNScrRcy0rbW/yupoT0VHWNG9ur3RT0FATvHY0GFHSU9h9W7bxMPI9EA6
+OkbOubS5ylZEQEVX+dLKy0EoKi08uKussM89OUBP08LKyMfOz/9KSFjVvrvJXj85P17OvLzL
+Sjg5PEnWv7u6v9ZgRz5L+Mu8uLvFTTUsHh431aedpr4/KSg5a8y6vcG7v8rtSD5J2cC6vexD
+Oz9qxbzA3kY3NjtM0762tcL+QTY2P2vIv7/I3V9XWldjeGnx3eTTy9VwNiUoMV2up6u130FA
+TVPpz8u+vsx2ST9HYtzHx95oUE1RYOxoTv7P29ZbP0hf2L68yNbpdu/5X2b80cDFUikeJz22
+o6i8TDI1W8jHyNnaxMvZVj5J78C6wOtCPD5P1cjFxc3jeU89ODxlvbS52UM6PlvLwMbbX1VP
+TVVj3cbBzOxLPkpv2thMLi5A7rSttsduR0pk99/P0MzTZ01MWN/GyORaSElj3tTT4GddSUBO
+WPDN2tPG0/ppUFTn3tLI1dzjc+HyPzQ0LjzJu7S521BRSkv3eta/wMLPV0ZLY9fDzXhURUpa
+b9/OyMvRXj87QWXIvcLTbktISU1r2MrFxtxYSkRKV2vZz8/M0v5bTkhV/OXP0uNiUF3o9EZI
+XPbGx9fvX2TTx9PmYExOVFl32s7IzelbTUdMW2vXzczM3VhAOD5WeMnAy9PqanHz7tbLzcjR
++efU2FI+QUA3P1Zc08nNyMTfbmRITm581dDa3dzY19tsVkxMaN7U0Nr8X19YW2pcYGFw39bZ
+eFtUW/3Zzc7V4vp2aGp69+z3enRub+3d2tThY1RLS1///e1qVWNxX21scdzO0+ZoVVlqc/p+
+b+rZz8vUfFBHTu3OysjO3n1tfOro5Ofx9+3azs5UPTYrNVtpzsXJv7vZXVlGVulkcerexsHO
+2+hmZ1hFSFbsy8fR3+TzW09NTV7v4t3Z1NnvV0tMVfvZ19rk73pjYG/h2dvuYVhceOj3fe/4
+dF1fYmdxfPD7em1obmps5Nfe7PBybW1lYmBv4dXT1t7ne2BjZ3Po4uDf5N3Q1tnW7Ug4MjhG
+Tlzy0sG9xtPrdfhuUU1SadfP0s/W295tXlxYYGhr897c3+Df3+9dWFhXYXBx+v7//nf35tna
+6XdeV1to++7m3t/p6PtoZFpXXF9bXGX1297q8G9s/Xt28uvf2Nvf3+z68O3o5u/5e3d7/+zp
+49/g8mdjY2r14dzV7VlaVkxKS0tSVVFbaPvY09fR2NrV4Ozp7+jn8nl3bXD5cmZ65NfQ1Nrn
+9Hx2cW/37vZrXl9fZGFbYHfl4erw6eDk7XNgW1hcZV1bXF5qemxpavzk3NXQ09nf6fLv6+Pn
+c3r1enNzd/js7+95ampqevLx6/Jya25oZ2tq9ud9YW71bFxXVVRaXmNoc+Lc4+Dd29rZ3+38
+9Ofl5Ort6e3o5uTe3uXs7Xl0cWFgZm5/+/37cGpzb2psdXf07XZeWmddT1RUT1FSV1teeejr
+597n5uXf3NvW0tXc3uDj6e30b3D67+nh3tze7O7z/3JtZ2pw/+/y7+72fnFrYVhbYFRRWFRP
+T09UW23w7/Tn4+bn4N7g4tzd5uPf3dvc4OPo8fh3e/Lp5ufv+vx0+/t2/3r89X5sbf13Z2Jd
+XV1fX2FkZl1cXVtbXmNqce3e5enl5OTr7eri3N7l4t7Z19/t9///cGxxfX58/nVvcHX99/p1
+dPX5/n5+bG92Z2BeYV5gY2BhYWJmYmdocvvq3+Xm39/h3dve3Nze5u7n5+vv9/z8fnx3bmNg
+ZWxqaXj6+X95dGxkaWxqbGtsbmhkcXBsc/b48O75e3P29u/r6urn6vX36N3f3Nne6Orx/nh8
+dnt9e3p4dHV4a2lu/Xh6/np3ampnY2Voa21vbGtnZ2dodHVwb2tsd+7l4Obv6ero4uLv8eno
+5efk5Ofq7fJ9b2pqa2xuaWp1bnd2dP32/m9obHh9eGxnZGZpbWxqcvXw7fVzeX718nxydXL7
+8PTp4+Pm7ex/cXp+d3358e7v5uru/v7vfG1xbmltcGpkaW169vrw7/p+dm9iW11nb2xud/51
+dH199vfz7X1zb3B49e/p4eLi39/f5d/i7Pj7+3hvbHd1amt4dmdoamBNWNlsb93t8PBtbHht
+avvy7Ot8evT38e3rd3r+9Xtv8+x1cfNuevPw/ffvfev7cXFx8P3v6nz77XT3+Xz5+3ZdcPn3
+63lwd3BveP55aW15fmdv9Gzmenb4b+Zu/PXp5Pnl+HPtfm54d2h7/m17ZnH/deH36uPu6vTj
+8On7731tfmrsZux0/etj9G1gam5ta2fybW5392xvfGzu/u91f/b/5n7472/h+fn0/vN56vdq
+41rk6Hjo+fP37nT8ZltnX2NtaWt7bv7dcPF57H9v7+zhdc9f53N96mH9X2tmbXjubN9p3/p5
+5vrjbOZm6WjrZvB+fV3tav/vdtle3HP942HvZ23uYtxp89xd53lq6VrvX3Fp92nobe5f517i
+eeRj4/Rp5/nv53nw5Wnf72ffYn3491Hu72/Uftne591aZ2tZRlpdTuB03tfZ2tfjbelkZn1e
+Vvpe+fBj3mzaYuL56eVe8WRvdvrx8W7nc+z1eOpj4F335WL/7GFk8/Jv7O9m2XJc3+Rd6mhU
+5/xn4nB55Hlz5Pdm4F9b4G9361r65V5+f+nu/mR93nDpZFjf+mDsZ+vk7Wrm3eDtXv56/m1m
+XOrm9e/p8+h4YHNlZ3VqanhnffNra+Pw8f1memxw63785n9m+Hf87frz3t7j3+715+vq5n7z
+/W93amNbW1NLT0xOYmXu/d/Y3NPe4t7r7efq4+Hd3tnZ29Xd2c/O0dPV1PFOQDgwLi4xOkzx
+yLizsbK5wtlcTUM/QUdV99HHw8XK0OJtY1ZYbnDr6NzY297d3+LX2+H67uvn1ug0JSswN1NZ
+Vr+wrqyyzXtgTEc9NTlPzb26v8jLzdlpTkdMbt3Z297f1M3Q4uzez8vQ71hDLiYpMz95xr2x
+rK61y0c3NDM3P07cvbSzuMXtUUY/PkJez720uL7PX0pBPUFV3MW8u8LHxcPKWjEfHCY2TMi+
+uamkqbVNLCkvP2bs48CxrKy5Zjw1Nzs7Q3a/srK70VhHRkNBSmfWwLm8vru4vlwvHhokOv66
+trKnoqq/OSYnL0fi3tK7r6yzbDQyOU18V0zevLi73E1UetjiWVFp0sLG1NHAt7K/QyodGidD
+br6xrKOjslstJCk0R2vPuq2qrsVEMzpGUF5c/r+1vt1VTWvZfktDSnDQysfBubKyuNc8LyUZ
+GzNiuaiqqKOsfS8iIi5rvrezra633TMqOExfzcnHtrf3RkhS289dQ0Rd4vDv17yvrrjF2V9L
+PyYUGmi4sau3r6CsOiQgJ1O5vsO2rrPLOyw0T+DWzsS7vfNESeTN13hcX/JdQEPftausutLe
+9VxEODQfFjewy7y0uaWlPyQoLEa+ztOyq7LZPTRJ4mNW4r22xko5RtLPa/bTzt1HNj7SurKv
+s77SXkJITT4/LBozrWZVuLaopjojNURHbFHXrarATEhN12U0PMe0vm9JXcfHRjtixM9KO0HL
+t8LDtbG+WEJS1r7KQCgaLMc3NL6poKVGK0M/LjJBwquvzeLD2kg3NFO9u8vKvsJ1QjpB9Nde
+XNfVz9Ns1cndaVp2zsK9sq+9RB4VIDk666idnalAKy8pIypHrZ+msrm/TS0kKmK6trazsr1I
+LzRFUW/Zz76730lLZ+DnZ3DLvsHL1MvFTiYeLFdc3bWqqrw/ND44LDTVraeuv8PJQiwrN+a7
+vb+3ufE7OUhRTk7ivbrOWFnw+FtR7sXG09rTz9jYxMg5KTRFOTNHxbC00/fWTzUyR8G1vMXD
+wuI8NkPfyc/Lu7rmRkJCTmRi3L++01JDRE1OTeDDzON0dtXWaOjHwcp+VmJdMyY20dvZwsK9
+zD45TVxqy725u9ZLTFpOSVHgv7vE2Hp+Y0I+TFh+2d/q3eRlWF3kztRxVWTXdFvWx87nXltb
+T0xk0dHq7tTJznU6KzluSU3FuLS9VUZaS0FayLu6xd/k8Uo+Q2DSysrKzN1TRktf7+ff1s/j
+VUxNUlBSa9vS32thbHh/elNK6Mt3Z9DK0WJHVdbcbt7JxNhectvPz/VlYzs3SkxRzsjLxNxR
+Uko/RmnOxMPL3O1bSktk1MnM1N95Y1hQV2/ezcnaVkVGSlB72M3FyvNVT1VeX2vUyNDqXlhd
+Xm/czsfJ32ZRSk1g5NbU419IPj9CTO/Nwb3B0mZLRUtb5Mi/wsrpU01NUGb79drb7uLtXVxc
+WXDse3f659bc8u7c2PhDRG559tbazsp2TEtNU2vx2c3M1GxVUlBNUtjIXnDF71TZ4mTT1WT1
+3v5mX2FgXl5ueGZy7evf2eP5bV1bWVZj9eDa3/R1amdve/Lo3NbX3nhZUlZi++Tl6eb7YWN6
+4uDr6t/rY1tXWWv16t/W3WlVU1RWXG/k2Nbb7nb3fGVu/fDq5u1//ez3aGx1bG14bXL06+73
+f3drZG1vZml8/Xr97ujqeWl35t/b1tjX2/VhXVxYXWRoZGRgWV1qd/3s397j4+HsfmtlaW1z
+7+bh3Nvj59/d4u79bV9UUVVca/1+cvp4Y19fXnLj2tjY3ep8YV9qdvPj3uft7vf+/vj+enlp
+ZGVoaGl4+fv19n12bGh0+e/m4ebk6flvb3N5cWt88+39c2977O/9+vZ9e3Bna2t5/Hf47ut4
+Y2RkanBubnV8e/nu5uL0+355dnd9+eXf5OLe5nFlYWRz/e/v8vpvY15gZXL28enueGJfav7u
+fPnu8/T47+rt6ePn6+72dG5sb2xnZWFjX2VxbWz+5uHg4eHl7+/z9/n4dHNxb/7+8O/3/vr7
+d3VrZ21sb25naGNnYGBx+vDv5uPm5uLg5Ofp7vH57vJycWlqaWpyamlpbu7k5Op+bGljYmVl
+andycvfp6u/1fXb26ezs5enwfnFwaG96bW98//Z8Z2V47+fo7vT+eW1wc3vr5/T7fvv7/W9z
+fHX4+f5ydnZvc3n7dGp0/XV47fJxZ19jaXX//+vk5OXf4Obtd2pnb338fHl7/v7++Pf68+zr
++f3zeWVlaGhoaW78fHR4/vP17e3o6urr6uvv/nl5dW5taWdtbmpqd/91cHZ4/n769uvq6eXs
+eHR8/fP9/u/u+vp7cv/u7n/0fG1zbXF6//n19fb1c2dqcWtncvv4ffX3ffbw9f5+fvfu6+z0
++XVy+n96fP949O7y7Orq9ndtaHV4b3RyfPbz+PPx7fl8+fv7fvduYmhtb3N2//Hp7Pj3+HRq
+amv++/v0bWh19XtvePXx9f58+One4uzu7Obw9/x2eGxpZmVt++z+/3l6en3+dXR0dm1yZmL7
+8+3p6uv0/fn3dvvueu/k7+rxbmpoaG5qZ2Rmc2x+7+/l6en4d/d68vb+9fZzb3t+8PN8enbz
+7f74+vP7eGhdYWlocOLo5+3x/m7v+vLt/vp5eXnw6Pn38HZpbF9gbmNn+P1tbfL66Oj//e3t
+6On9fPP48m9u8/90+en0+PHu7XlobnRuZ2ZobGxzb2xxamhta3Hv+33s4Ojy4Oft5+rk6O5/
+bHH29HR89m9vc2Vxcv9+emtlZGNpcHt5ffp3ceztbnP6/XJyfO7w4erz7nzu8u7vfPj5+Ovp
+9PL7cv7++nxlYGJrcmpeYmlteW9v++r67+348Pf2d2/47vHl6efm9vDx8uv1dGdudGhtbW5u
+bG51dG/8fH3v8Ozo7u7r+Pn38Hpw8vv8/37993NjX2Flb3T583747X92++vs9vxyb/br6ePo
+8Ofk/GVkcW5tfPlvaWh5dGT/8H57efnx+H9z9+3m3+fj73P//2pnbW/78251cXfx7Xpq/O9+
+b21ia3Fpbfru7Ov6fe3e3er0ff75ZWPx7nxxbXf+dHD1eHH88O1pX//n6uji+2nf3fh6XGDy
+b1lZXFxpbG1+69zaem7f297mdvji6u94bXv4cePfY17+fGRhZvZ0Xmv15Odyfvh26PVv6PBt
+dn1t+H1p8eR7cGVadenv/n599u/3fvfq3t/w9/Z1d+jo+2VecnNob29faP7t7m746O37W1vj
+7f3k79/b+0tIWubLzdnfVEvp7NrM0+3yaEtWfe/e52NZV1Rd7ene3P1sYW/z7Ofv7fV9/Oje
+297pdm5sbP7s+G9xY11gbn92bG96bmltbnRvU1bLzfbxaHPc32Xy3PTi9Vpm6+bg4Pb9fWRl
+Z2xkVOHM3XhaV2rofnLj5fpeUlv33eXua11sZ2/s+Ofe62566evb4HllZvvxfWzv3uR2YGXn
+2Plvd2J7725xempqaFhWX2pv/Xz97vLz7ezv6url6O38eXlxe97kVmD+bertbP7Y3t3ceWtp
+Y2D57fvvc2JiZV9o8ubra2lkafLf5PbrbXjzZXXc3eTf915aV1d77f3v4e5uaGX+4t7ramRh
+/e7r5eXpc2x1/e3o7P5vamdxbn3qaWloYvLm8/vteW3x9eve6ftya2Jhe+rr9X9mXmjy6Xlt
+aGF3fmpq9uv2+WVm4Obr0tP8Y1pT995obt3f8XVYWOba5PxwZm3xeWz+d3J6Z1lba2n84uVv
+6tt2Y3jueu33ct3g8ejq93Z9cP7u6e90Zl5fZGdlY29wcH7w5vXv4OxydG723WxWYXNx89/f
+39t8Xnbv9e3lbWdzcX7n7Xzv7XNpZ2ZkXl5k7exvbWdYZN3a4eLw89/a3+Lk+P3u8WpfZmNg
+Z19ebOzk3+Dk3NvleU5BSFvrz83mZWhaVv/X29vZelxaWmLl2Nrf9WFbaPnl19PY4W5WUU9i
+3tbZ715ZY29s++Hf3t/oe29dWGtvXmd6fPjq7Xb/6d/Y09/072lNRUlPdszG0m5WTlT26OTU
+0dnnb1ZRa9fNztpnUVBYbenc3eduXVVXd+na199+ZmRbXG3kz8zXblVRU2Xu7n35dnnl3uTf
+19re4PlaRzs8Se/EvcToVEpNd9rX2tXW6l5PSk540svN4FxTWHLj29rd3/BcTU1a6NHN319X
+WFxmdvfk09Dde1VMUF3739nc4ub7dHL03tjV2GZBO0JT3cG+zW9TTUxad+jVzM7fZU9LU3fX
+zdHjbV1Yaunf3d3m92RVVFr13N/l+GpeWVte+dvV0N13WUxOXvfd0dDS1+V3aXbo3Nvi/l5P
+TlFSWPnY1uR1V01QYe3f3Njc5vVyYV302tLV3/1eXF9w7+Pf4elvXFVd6uDZ2u5gVV5u+NzU
+2eLvdltSUVhs39nkdGVhZHjq6e3weHfv53lXWGP46vPy+/fx5N/vbGzr6PLuemdy6dzc5O18
+cHl1bWRi/u30cWdeZnh1bmBfbO7o6v5t/npv//v759zV1+J0X15pffLr7vb8fXd0bWz25eHo
+/nZuZ3jo+mJfb3ZzY2Fs+eXc3u1tZWZr8ujq8HdrbmZfb/Pk3t7tcWpx7+Tn7/f88fZ3bG55
++/v2/2xqaGlubW10eP7x8PVzaWZt9+73+O/p5ufk6Ozp6PJ5ZVhVZuXvYF3p1tvmc1JGSW7a
+1978X1/02tve5Ojj5OXq9vv08Gtpb1xVXGxu9N7jem75fGZjfung3+prWVtgb//48/l/fHdw
+d+vh6O/5cm/29Gdifejt9W9daOjVzdHtVkxSYfri5e/27+bh5ebp5efqb15191FJWGhm9Nfd
+e2x+dV5t5ubr+2VXWHDu5dzV3fF2Xl565N7jeVRZ2MTOT0pu39jU7Vlk3N5mTUhY08TOaUtI
+XtPJzupqavPqbFRNT3Tb+2nw4Ot8e1pQ8tDd5/BcWPDe7+fh/Gz8/GJhVkzpxsjXbVpf89ze
+7PHd2eBYPz5exr/LZ0hN8dPT09ZfOTZKYMu5wHVMS0xm19rMydpPRkxezr/D21tJS1/k6U5O
+yc176mN5y81v89n4/fxMP0JO4MbM6fzq6eza6UM3P2+/uctiTVJXcNri287cZVpQTd7J2Nn8
+UE5cWFlPaM3LyNX05+x+593v0cvc70A2RuTEwupp73pfWk02N+PExMR4T1lXTGPPz8vK41Na
+7d3p4snW2mtHR0o+WM/JvcbjW1FZ1srP1eZ1Z31YS09Sb8vK2d9KLipE2s+0u87TTjg9Zc27
+usjgaU1EWMS97tDRQ0c5ONTQ58TEystsP1HcyLvNZHNYWGH82dl36NtHLCM2wMa5vc7Bzz0x
+QGC+r7vReEtER1rCs81HSENFR0/PurjC2VxLTV/jyby6wmpLWUlLa2lqQzIsL0zYvbS5v89U
+PDpBXMm7vMTMZ0FHTV7g3s3J3nNeXdzIx8TOd2l5/OHHyXjrxc86MjorKjtL2K6uu7vQPDc6
+OE3DvLu4x15LPzpL3d3aztLRzvNu2dzYz97fzsfGy9Tex79UMDArIipC67ysrK+xzT44NTVI
+18q+tr3U/EQ6QUBDbd3Pwb/Jyczh5+rozsTFyM3Nv7rcMy0lHic4PtCuqqinuUw8Mi44R07N
+s7Czt89HPjw6QU9a1b26vMHaaP3qfuvNx8jJycbHbTssIR8oMkrBsa6qqbC/8jovMzg8Tv3a
+v7W3u8DYUUpFPkVW+Mq7ur3F2fPRwsLK035OQTo3LioqLjlbyL24tLO1trnLXEU5NDlASVvk
+zb61trvF709JR0xTWFZl2sm9tbS4vMHcSjw2LSclKC9Fzby3s7S2t7i8ws1ePDQvLjRAWdu/
+uLWytbzE1GNOST8+RE/wxrq2tLS4wNhMNCkiHyEnNmC+r6ytr7GztLS60k03LCkpLDVNzrqv
+ra6vtbzC0mBHOzU1PE/Qu7Kvs7nMTDUsJiQnLDVK28C3sa+urq6wtL7sQjIrKSksNEffvrOt
+ra6wtr3L7U4+OTk+Te7Lv77CzG1BNy8uLzM4RVzmy7+9ubOwr7O7z1E7Mi4vMjpHZtXDu7e1
+tLW3vcjjU0dDQklWa+HP1vlgSz45NjU3Oj5GU/DMvreyr6+zuspaPzg0Njo/Rk9q3szCvLi2
+trrC2V9QTE9UW2nw6OzxblJEPjk0MzM3P072yru0sK+xtrzKfUxAOzk6PD5CSVXkyb23tra3
+vMTN7VlNSkpRZOLR09p5SDw2Ly4wMjlGXc+8sq2srbG6yGtGPjw6Ozs7PUFQ6cW6tbK0t7q9
+x9pkUEtHSFB13uv3XD84NjIxNTY5RWXMuK6srK20v9NaQDs3NDQ4PEdr1sa7trOws7m+zfBb
+Uk1NU1db4t5qXUU3NDIvMDQ1Pm3DtKyrrK2vusd/PzUxLi40OkBkzL60r6+vsrnB1F1LSEdJ
+Tljt0dfyXEY5NjMvLzQ4RuXGua+trKyvu8phPTcyLS0yOEbqxrqwr6+usrvF31RNS0hNVnPS
+31plUjs3NC0uMzU/fs6+sK6vra+7wttIOzcvLjQ5Qm/NvLCtr6+yvMPcWE1JSExYcs7QbPNf
+OjIzLi0wLzde1sq2r66tr7q+yVpAOTQyNDc9WNLAtq+ura+0vMjdYE1HRUVFSlVSV1xDOz0/
+PTs8P1Bs/NnLyMXJ0M/O0NPT1NXSz8/Q1NzsfV9ZWFheZGRod+zf2t7m7PT2bmtu+Obd2uTt
+e2BVTEdFRENCQUNIUGvo3NPNycTExsbIzdXe/W5ybXzv5trSz87P1dvocmhkWVhdWlhbVk1L
+SkdFREhNT1Vieenc1tHOz9PQzs/P0tzq7nhsa3Ps39fV0M3Nzs7W6ntfV1dSTUxPUlhfXFxX
+VVdWU1RaYGdteu3b2dvb2tfU1djh9HVvcnT38PXu3NjZ2dve8X91bGpjX19fZG344u3u9Phr
+Xl1hbGhtfX1ua2dmYV9mamxpaV5ZXGFjaf3v5tzY1dPU1dTW3OPp7urq6erp7/t5al9aXl1b
+WVhZW19hZGl19+7m5ufg3+v0+/52dHhwcG5x/nVpZGRnbvbx/PPu6+Li3tzh5eb2+Ptpan35
+eH79fXl0fPft5uXvc3dwZmZlZ2Rrb2ts+vtvcXV5fH19c3F2+u7r6uTt6u55cm9ycHhtdHn0
+7O/t497c2NbZ4vtnXVxfXl5gY2x5+vl9dv57aWVpa33o4fJ/8O37cHJrbXr3fGhocG5naX3u
+6ePh5OPd2tzj7vz7/mtkaHZ4/fZwbmRpa2NrcXJoZ213d3X//f18fX/zfXfp4ux1e/tub/Dm
+5+7r4t3n7erz/3xxaWdnanX8fHv2dXF2dHd8eWlzc2/w73Zwffz/eH5+dXtxbHNpY3B7fu3n
+5OXf3uno4ebt7PZyZ2VdWmZqbHX+cv7s6+/+//37bGl97/v07e369PxzcmlmdfRwbX7t5eLo
+7/R8dv3+am19b2dvbm169u3p9HF1/nFrbv/3+/bt8fZ4ffHs6ejvd3J0Zl9u7Ojv6+Hl6O94
+dGxue3dtbWRhZ2hmbH5tc/34+fzs7+rp6t/m7+jq+Xv9+H32f318bGxnY19kb3jr7u/t+nt0
+dW52+3h5bm9vdXt+8O/t9Pb3fXn0/vfn6efp9nn+7/b+eW9nbm9la2xxe/bw8O/y9HNqaXb7
+f3N9cmlsXm9tbXBWXMbjYdrT6+rf5Nn1a3P2ePJubntsbGJqZWprcnp0e+9++/zzc2dpe3Bt
+eGFc0dB6X9LieG/nzfJXS8/bS0bCvz9YzUxI+1La11zRzO/edXfO307k3FNbaflndWvubFJZ
+ZmNUXVVgc2dhec/W2tbL2t7P083QztHLysXLztB5PzU4Ny4qMT4/Qnu+uby8s7TB4N3TUDxH
+cVpLYci/wryvrbK0rq3FRkouIB4eHB8mKEW9s6qfn6Kmrb1KMjIuJi09SlHMtKyrraalrK+w
+t9ZDMCIeHxscJi441rSsoqOnpqzOSTosIyctL0XLtq+sqaiqsK+wt76+vd1IPCkiJx8cKDEs
+Qr23rKmrqavVVlowJSw5M0HAsrGvrKmtu7awvL+1tsBIRUwjHSceGikuLeW4uKWjrqer2l5Q
+Kyo1MTnzw7ivsa2tuLu1ury0t7rESDw3IB8pHRw0NS/Isbaopa2rtGz5RiktOzI61MS9s7Cu
+sbu2tsC9s7/HvkE9ViUeNCoZLfkrQK20taisrrBmW+0tK0g5MnbD0L2xsre8t7rOx7q+yLq4
+bExxLiEvKBwrPys9uMK9rK2xtcPH7Dk4RTo5Xc/ZzLy8ysy+xcy8ury2s7S20PLdLSQ1Jhos
+NiE6t2/Mpq67rLPX5VE9Pjo7VE5Iz9BhxrvGvK+0s66ws6+14mXsKiEwJBoqOSM5s+HeparK
+r6z+W81DMz5ANz5SV3TOvbq5sq+zsq+2uri+zfREOy4mKyYjKjUzPMrHyrSuubyxv/7TXTs/
+RTk9V0xkzsy+t7azr7W2uMzc5EtFXEo8T0o0PD8wMkA7PG3je8W7xcW8xd7R6UtMUkVCWmRu
+0MXDwsHFxs/ZzdTdy8nTycXW4fhTQDw5NDQ3OTxDS1Rq8tbLy8K+wsXJ3GtaSkJEREladdXF
+vLq4tbm8vcTa4PpZVl9lVE9OPzs5NzQ1Oj1GV9/Nxb28wcTMfE1JRD4/SFt/1b+8vLe0t7m6
+vsfN4GJYT0hGRkRJT05MSUVCRklGS1JPU15jYXbi2NTRzdXg4Ox4eN/b08jCwsDCysvM1+lt
+VFBPS05UUVNnffPVz91vXVBGQkE+PkRMTFNt997OycrMysvU4+3t797c1tPX1tbZ4Nzf6+fu
+b2rv6/jj5O7q6n5ua11PSUhLS0ZHSUhMWFtYa/Hv2MzN1tLT2tnV2dze4t/m5tzW1tTV3t3d
+6H13dmxveWpsem5oZmVhaGxraV1ZWFlVU1NQUVNWWVxibHV56N3g2NTV1dDP087N09ra2Nvf
+49/f4t/nb2NhWFVaV1lha2JcX15fZWFeXl9iZ19bWVxgav3t6+3x7uXe3NrY1NXV0tnb3+9d
+W9DI4kpH3MXUXmFtZ1RWbl5LTFlcYGNjZv3k4u309/zr29fa3e3v6fD6e3n89X138ebafmL3
+eWdfYl1lW1pjaXRzZGh+9N/o/ujc2eTo3PN76fNw+2xjYlhf8eLq5+nj4Onf3fVtdWdYWVhU
+VV9cXuTjeu7X4Hne0+5e4t9aVXb/X1pm4tzd59ra6nr40tx55/hu7HRfWlVpa1tcYWJvaGTs
+7G9t3eN6aWb7b3Vu7uD1evLW3m7a4HP97Hjt2vblf2l77u1ubnFxff1zZFpdd2ZeaGdfdnd7
+6v33cnJ9e3VxbG7r5uPj19jd3OHg7m/1+F1ZZ2Jca2NcZm9hbuz0/ObrfPPt5vh1eW1v/vzv
+5+377eZ+ffD9eXfq+XJz/GNjY2Bma25iZn9yeO7q5OTt+PPw/Hp2/Hl743724ufu6+Pwdmps
+ZFl8aWhqZHl6+fx8evn5+3Z48u/3a/1xfeHdbWjmbE5Y1PFb7NHmeepv1uZcbc7eU2nqY2Ne
+WtB7at7UaFX/bW1kWlxtXFXf2+rt3dvpbml9YGRtbl9k1dd55s7N2O7m0dR758jOb/XOzX5C
+MC05PC80X8TAwb+7tbt9TFteSD5D3L/K1sO6wdjjyb3C18+8vsbJwsozHBolKB0jdqqmqaqj
+oLQ6Lzs3KCg31rGvs66quVZNV05MWuzBt7u8tbK6vbVlKSMXEh4pJz+hm5+gprLWOR8eLjo6
+7aukpae561M4LC9Abci1raussb7CxXNIT149LiEXHjQvML+fnqWuuslDJx8s78G6sKiltUoz
+LzIzNEm2q7C0tbW7z11cbF7/yLm/3EgdEB06LS6vmpmfuExIMB4ePK+oqqywuv8nHShGe860
+qaSr00pm3W5e79PAvL28u8BKHg8UNU03u5qVmq0xJSojHjCvoaCltmU5LCQkMsutqaqtts4+
+Mj3qx8jDvLq+wr++2zYmFhEnx2XJnZeerEEgIi4uNLmkp6iwSDAxKyo+zrmrqbXJaUI+WOTy
+2cnYbuPQyLu0uuM5HhIaZL6/pZyfqLwpHCMtME6yq6agrz4uLSktSceyqKm8XktAPkA+TMm7
+wczNyr+6ur29y0QkFhw6Tl22qqqork4tLi4vTry9u7C938z2PkzPz3tPPj5hy87Nw8XIwdRR
+T1JY3sm8ta2pyigZGCM33rm3squpsdg7Kyk0Ys3JznTzuay030E5Qn1PODpcwbOtsL/VWUZI
+S0hR3L+vqquz6yobGx4jO8O7s6qloqW7NiknJi5ASlu/sKymprXYRi8sLS42VsW1raqstM1G
+NTA2PmjJwLqwrLHZLyEeHyc1TMuyqqWlqrHESS0lJCgyT8u1q6mrra+/WzwwLC41OkjRubG0
+v87Sy8TNUzw4OUXewb+/y11EOy4qLDFB1LarpKOosuc2KygnLDz2vrOxs7a6vcDNUzguLC46
+UtTAure6xN1WUV5y5NPNyb+9zV4/My80OEBSYPzf2cu9tbCzvNpIODAvNj9dzsK9u7u/x83Y
+Z0k9ODpGX+ba2NnPxr+/xtDtZHXOv77NTjctKCkvP3fGvLq5t7S0u85TPDQyNz1LetTMxsC/
+vbzB1HROPz5CRUtYZHfayb64tri8wc77V0U6My4rLTM+Xs/Auraxr7G5xulKOjIuLjI7Td2/
+ta6srbPAZz84NDM3O0j7yLq0tLe9w8jQ2+B3TD01Ly4vLzE3PlbCrqeio6q56j0vKygpLThP
+y7SqqKmuu9hNPjc0MzQ4RG3Lu7a3uLi7wcnMz99aPzEpJygrMTxL2bmspqKlrLjWQzQtJyUn
+LT7TuK2pqKqss8ZQNy0rLjM7TebEt6+usLe9xtXe4Fo/MSgmKSsvOUbvu6ympKetucdfOy4o
+JCcwP+S7saunp6uzzEw9NzIxMzQ9XM+/uLW1tLe9yNd4UUM4KyQoLS86RlTEramnp6y2u8pE
+LyklKDJBVc25sauorLa/4ks+NzI0OkRUedXGu7e3ub2+xdT0UkA2KSUsLiw2RF64qaqrq660
+uM08Li0qKzU5PtG3sq2ssLS4ylpGPTg4OzxAYdnOw728t7K3wMzvVEo0JScwKyg1PUa9rbGt
+pqqusMBNPj84LzA2OkbszMq+tbW5wM7Y22xLPz1HU0xIVfHZ1s/Mz9TT0dPcbV1v+WJPSklM
+T05NTFFcX19gYmrv393e2s3Hx8zMztDP1fhhX1RPUlRdbW7x2MvGx83a531lYFtXXGFpXVVQ
+TUlISUpNUF795+Dl6vtvYW7z7NzSzc3JxsnIydTqa1xWWWVrbvTq6d7Z2uDi3+toWltkc2Rc
+VU1NUU5JS05VW2Ro+t7f8HBiWmHx3dXPy8fDwcPH0OZvXFxWT1JXYXbq5OLa1dPT2eLr83Zv
+Y1hPTEdHRENHTVNj7OHb1Nff8GlcW2Nt+t7RzcnGxsvO1eV5Y1xaVFVbaW1s9ODc2Njn7uHh
+5OLvY2VmVkxLSkhJTlVZbefc3N/mfHFmXF5dXmT74dnPzcvLzc/V3unl7vn0+Xx6fv57eX76
+8+99f+be3uf7aFdNSUZDREZFSVJhbvTp5t3d3uTj3tva2tXPzM7U3t7f4N7b3Obj4ePi4e1/
+dG1rZWFeX21vZ2ZhWE9LSkZFSkxOWnf36d/Y09HV2d3f4N/d4OHj5ubo+Pbv+O7c1dra2NjX
+2N7o5fh0c3Z4/O78YVNSTUVBRENBRkpNVm7u2tDNzczLzc/S2N3f7nhmYl9haX3o39/Z1tXR
+09PU19zf4+bv/O/6b2ZYTkpGQD4/P0JJTldp79zQzczLzdHV1t7s7f1wZmz4+n3s39vW0M7N
+0tLQ0tfa3eHub2xrYltTS0VISEVEQUJGSk9dbPnc08/NzM3Ozs/X3uj6amp3bWZu9ubj3Nvd
+29TO0NLV1tTW2uXs7/ptX1RIRUdFPjw+QkZMWGZq5tTNy8nJzc7P1eN+YlxfYFlbe97Tzc3T
+087O0+Dq5eLf39nZ39XacVdHOTQ7Pjg6R1b+zsXDwbu7x9ToXVVQRkBETEdO5+XPwsXDvr/G
+z+FsWlZSXO3YzcW/v8fO1mBHOi8lKD8uK2W7uriyq6qzf1DqSjAuOlJXWNy5tL3JxcLsRUta
+T0hL9cvS7trAxN3bxcXY1eDn+klLQC0kO1UmNry+yb6xrLJtTcNXLjNHYEVHx6273rS0z1hN
+WGI/O9bMUXnGyNLi2cnhZM7Hzvx74G1AMCovRigqxsN7vKqrssXWwU4tNUI6N0rFuLq/sK7N
+9ttgPTxKTVFb3cLL4MjG5Ove4t520sLnauDxOiUy+SQly8hIwKytrb/ItVwuOUYwLknOy8O2
+rrHBysVpPT5HPT1U79DSyrzJ0MTJ29rN5drVZeP4TDQpPTohMstJR7Ovta64ubxHPkg2MDxB
+WsvNubK8u7zT5HZJRkVET05O1s/jx73IzMnFx9Hg0dxMe1AtKz4yITnnOle3srSvsLK8alxH
+NTY7P0Jp0My9u7u+w8nlZmZPSEZOXU9b6tfNycS/vMTJv85s6GtCMSw/Lx49VS5Us8S8qrGy
+uMbJRTpCODU7T1Rfy8S9vry6xMPN2eNmVUtNS09ja/bbyMDDwb7NycpKPjM0MiAvOyk6x9DW
+rK20rbK4zn18Pj09PUVDVVXpztLIwLzMv7zX0t1lV09KTlRa6+DLw9O+wW3AUzJLRCMvVScw
+cExMwLW+ubG3x7+9euzdaV5MbFJCVlJEXttf0cLVxb/DytDN4F9uelBsfljv33nl2mNJQ2gz
+L2szNXVVRuvH8dfFxMzNucjZu8jfy9BdW3tJR1hKTnnq6t3Jy97KyePY0Hb+3O9jbHhYYmFo
+Wk9VR0tMREtLTU1SWU1dX1t66uDXycPBvry/xcXR3e33YlpuWVdpX1Rr6mR91d/p3NPgfO9+
+WVdPT1NLT15QXXxgb957a3ReVk1UT0xZXWjc1M3IyMXEycnIy87P7Wl1V11bU1RUYGNjb+Tb
+3OHb73n/bVpUV1BYY1xedfbg7fzwdW1aXFVOWFVjYmn189rf2NLPz8zP0Nja3+59bnJmcHN7
+/+nl7+fb4W3sa1ZWXFpSXG9jYvrre+3h7fh6YV9VVlNTWVZbWGhp6ebX2dbO09LV2N/h7n1r
+dGh38+3k7ODc3+Lg3eJvbn5fWmxcVVtfWldbXVleZV1eem909e58cPvr7ezd4+na4+Pg7OPr
+7vB+b2Fkb/fp8O7x5Nvk5uj7+G5iaWRnant0b2x4e31tbGt8fXD9aGlvZmVsduz0e+Xu7/fv
+92l+evj/7OTr697i7+tzb3J8fevs5OHj4eju7Pb37XNfX2NgYm9nXWNhaWFqamz9b3556+j6
+7eXu6+Xv9Hz44ex7a/j27Obo3+rr5+pu83JzaG5zeHBt9276+vDy73vxfn5ud2plXV1aX2du
+eXx7eOfq/Hnv7/D49+fu3+Lk6+rw+Pd99mlfb3b44u3+cG74/vp3b2FqZGVzbX717vDm6Xb1
+bmxpcXHy7Hz7fP5hbWr8bmltZXZu6+/o8+/x8Ojm7P7ye3xx9n17d2H5fvXw8Ofn5vLz8v9r
+Zm9tbGhqeXF7d3v+7efk2+Tx6/lvamNeVlRcUk5RVldefOjg3NbWz87OzM3U2ODq5GpiZ1xc
+XVtcX2Z97Onk3tnX3d/g4unq5VpQ9EY+UT89SUpHUWlxd9/MyMy/vMfEw83T1uFcWGZLSVVS
+S1n6WmvU1tbOytDWzdbp4t/scnZmSERWQjQ+Sjc6WlRG4sHRyLW4v7m4xc3K1GZNT0o9PkVB
+P01UWf7az8nFwsDAxcfL1evr9WV09l5id0I9bz4tPlAwMOz1PfO5xs+4tMC9ucTNzeFgX08+
+RFNDR1tXXt/V18nDx8fI1Ojf52NqcGD83OXq50o/dEcuNUU5MEN3Xd3Fw7u0ub+2ttXo0HxJ
+R0pCQ0I+TfBxfsi/xcK9w8jM33JzXU1TX1/93eHezOU/SWc4Ljg8OD1MTuO/wsK2sr3Fwc3n
+6lxGQ0dEQ0tNVtzOzsC5u76+xc7X9lpOTExNVlzwzsrNy85hPz08MC0wMzZCVGDPuLe3sLC2
+ur7RemRNPzw8PT1CS3LMxL+6t7m9wNF9YE5IRkRHVP3czcK/wMbPYkQ4MzAsLC82QmfTw7au
+rq+wt7/K50o8Ojc2ODtAVtPAu7i0sbS7ws9tS0M9Oz1FT3nPyb+5u7/I2Vk+Ni4qLCwtNUFg
+ybexrqysr7fC4U5AOjY2NzpJZNvAt7OwsLa+yudRQz06PEBHT/nNwcLDvb7I2ldGPTYwLzAx
+MzxJX9G/ubW0tbvC0PdZTURAS09f2MrDvbu8vcPP32xRTkpGRkxXW2Xr2dLP1N/v+2pbWlBL
+UVZVRj1AQj9BSFHu0s7LxsbFydTa1d1tb+Pl29fUyMTIy83S5f5dVVZST01QWWN9dvLZ0dZ+
+bvBWR0tNW2lt29TY3nlOREI9OTk8SGXt28e/v7/AwsHGzdDP09rV2uPe6vbg2NXW9VdTV09K
+XO906Hxb91RAQkhMZPBt087T0+/yXjw1NDM6Rk/bu7m6uLrAyNHa1szO08rJ0t3ua1Zf5X9j
+83Nk519PamtQSlBJREtJZN3u+tHH2O5ra+ZPNzM6PT1EacC1uL6/v8vpdefT0NDMyMnK0vDr
+0NxfWVlWW0hH69PZZk1NUkw8Qlrn2mnly8nORC8/Xjw9b8/IzHzpxdBa48jMyMPNy8Xk/MzM
+09PwYmpNR13X1/b038/dTkhNTEVBVN/o1c/pXzoqMEE8Ssq6tbnN3N9YQ0pf0cbSx73I1dDM
+zs/ncuHo8+fbz8x2UmxQSE1KbO5PYdlkTe7T5XxNOi4uNztSyrqztcXl42JLVW7OxNTl7PJv
+/97Oxs3e3+J0XmTYwb/I1PlYRDtEVFTvzs7Yz85bTl9FNDI3NjpTyru5vcPK+0dDU+jZ08zL
+1f1aV/f8W+TNz8/Y2s/N2ePZ2nNffftcZtjb2szO119NeUg7XE04OjEpbLXwzrO63VA7PG91
+Ts+6vs/Z/lv/b2nTws1g2srU1N7XyNNaUVlSV+XLw8nM22dsSkJ91kdCYUAiHMqxQsWsrrpS
+KS7UXjvNrq670kdNzVVF3Lu8y+FX8nk+WbvC2exVTEtDT76zvc3tWkc+VdDKw8heRzUYGbS3
+Va2jr8U6HyrIcEGypK29YzE69kZIvrC6xls9TnBQbMTXz8hCOlrYyb7Gy8V/RUpc0bu+SmjL
+NjpUKxcon7VYr7TNyy8ePq3Bz6ysu9E5JzvD1My2tMPWPDBP2dHHxc/rPzxOY8/AvsTiSk3e
+09fNyMjlSEpQWW1YLRkkq7pDwa62xTojOq66Wryuu8dLKjy4uszCv8DeOS9BzcTKw8TnRTg6
+Ss+5vMPJVkJb/W7Ov7/dRz9Sb0AyPzseLai35LO/T/w8KVaut7quunZROjBetra7vdBl8zwt
+UL3AxNZAPlM/OeG3s7xsQk/6XvvXzcF/OjlhwlpMcDkkJMy0ybu8wMFSLTPNu7+4ucbWWTk7
+zrm7vsnebUAxQMzAx87fVUc+PlrFusDmY1xVVF9+zsLlRujbPjxDPjYnMa+ru8DB03xELTy6
+sru+yursVDdFwre/evHsXkU2RtPIzdHjY1NJSvjGv8HbXGL372TsyONc21Q7QmpNOSYmw6y8
+wr/Az1YvLuu7urm+zsvRPTVTw7q9zXXtUDk9Te3NwcHfZE5HUGjZwLrE4t59TUdOVXXL008/
+PEs6IifOsbKwwdO/3y8uSs63rbfNxsxHOD5hwbe/1uZOPUVIQnjFxMfuR0VUT07Qu7m8zWNV
+TUpdeNjAz0o/SjgkJkfSt624v7nFPzY2OGi8u8G+uMDSaUpOXW5famhgz8bcW1VealRISmXQ
+z8/LycfO719cVFv282zp6FpKQTQsMT5T1MfFvrm6v8vxT09i+2Fl6OHMwMPM1d59Xl1kcWxP
+R01aWVVx2M3P7l1YWlNQWF/tz9Di+e3x8+Hf3dfzV1ZXUl3ve2l3aV1cUlV9397pc37d2N79
+bfv65dXRz9v/a21uX2Pw19T1aG5vZ2d9fXLg5lpOTVBc797j2t9uY2FaWm/o59zZ539eVlZc
+df/u393a3eP7b3Jv79/W2fV1aF1ZXHPf2d/s7nFbXWl1693f5/JpWFNceu7i3+r3bFtWVmTz
+4d/l4X9cV1hcY2l169fP2Nzkemp67Ojl4ufweXR49eju73xyaFtZXm3u6/JuZmddWWB069nX
+3ud8X1xkannj4eXe52ZaXWZocHd349fd4eTqd2dhYGptffH5Z2vx7e/zd37q7n1vbu/f5Ptu
+cnlpYGRs/OXg6+75cW1tZGRudHn57/R7bG1sYl9kbvHm4Nve6v5wbW12++rj6e/r+mhjZ255
+7+v5ff7z8W927OxxZ2px+evu8Xtvef50ZWZzd3717u3s73pfYWp68+ze3fbj5WVjWmN85uf0
+9O3u+WZgdHh7+f9ueejh6vbr6e35aWVqevXt9Pzv4/FlXWR6YWPc2ezx6OZ6XVVdZ27v5ezx
+7PBya256+X11dunq+/Pn6fLu/25oZWpucXDt5n3/7OLl5+Xvfnttand8bnP19urm3Nvo2tX2
+UEZERkNDSlTwz8zJxMHDydLvaFxNRkpPV19oaXTaz9zaz9PT3Pb86v9cauTm8+Paz8nKzM3l
+VTovNzctMEFV38S7ta2tur6/6EE6NjY7Pj9cv72/uLW5v+BZUEk9NztITFPqyb++wsXFz2lY
+X1xWVWrg08vX7nA6PFUvLElUQ17Bvbe0v7qxyFPt+zw4P0BHVVF8vMLzvrjdXd35UE1FTGZL
+UNDM1su/wczX2s7fUtPNQ1T6NC5IOys8VkJpycu5sr68ssVsz99FRk1HS09Q6c/dy7zE5NfM
+eVJXT0tJSlRteeXOysrGxcrK0Pjh01ZTb0Q0N0gvLEdGPmjJxL23t7S5yL/LUU1XRj5MUk5i
+/dzKyMfCxc3O1mBPV01GR05WWWPo08zKx8fK09PNXU74Ti44eCoqf0E0dsXWwLS1tLu7tM1m
+2mc/QFZDQVxwdfLLxc/OxMjq9+JYSEpMR0dXaX3czsrPysHP3cXfSdZlL0BYKjFeODfw3evA
+u7a0vLa1z9jNVj9JRzxDTkxQ+c3Vy8DFyMrV6WpXUkxGTVpXXena0MzJxsnPz9B0X1o7Pkov
+M0I2OldlZci9u7i6tbfDvsdcT15IPEVEP0xfavDQysTFycTK3d5+VVFRUk5NXPjq4dHOz8nG
+ytzpaT8+Qy8zPTQ6SE9qzcO9t7i0tb68xu/rYUQ/R0BBSlH1dtjAyMy/xNjQ2lhWVk1QVl1b
+Y/L/4dfX1dTW5eBlMT/MJzK/LjPDc0W+udG5uLnAzrbTTsxwPkpzPT93UVvoy8/RvsTY1tBn
+TW1TRFpqWF7m8u3P2OTc5WltXVFOSkpDREhLVlhc7+De0s3WycLNy83Y2tvq+P53/Gzz6vPW
+29/b8GBnb1FTXVpaX2xcYOju7N/b7njhbVZja1VWW1ZXW2dzbmze2fXk2fLg3X7n5Gz/42n+
+2/vX2ezO2NvU4uzo+F5gV09XVVJaVlr1dmfu3+7j2ON5afxqWmtuXWlmXnDs+OzZ39/c5+/v
+cmpuXlxkWVpqavTe3trY2trZ1dr4+PJhXV1bXV5odG//9P3e4nnl5313dGJZXV9fX215Znf1
+bn7i4+fv8O9nY2hmaXT1c3br6d/h4drW1dbX5unobG5pXlxcYF1dZnV5fPD3fXhuYlxmXF52
+aGJr/fXx39jc29ve4Ofr9vlybm1fXF1hZWt7ePHn7unm4uj37vhtd/tyamx57u/w7fR+dnV/
+bHD4e/R0XmNoXWN2bXT97+jv6Obu/Hb5fvp7bnp8b2xqZ21669/t+P3t4ubj4eXu8f1rZmRm
+X2Nra3dwbHV4dXX9+XluenJgY2py+Pfv5+jq6ezu6evq7Pb69uvxbm/7eGlteWxrevv5bGZt
+YGJub31+cXn98e717Ont7Oj3fPDv83n+eXV4c3h3//3+9Xd4fft7cnv/9HZudv9+fPr9b2lv
+fXpx//b3dGlrcnl9//jt5+fz/Xr76+/x7ebo9Pf29P5+8P1sbvXte212/WlhdW1hYmZqbHB6
++Wdsd3zm6ePe5+3w6u/17+nuYmZ4aXXp6OLnf+7ufHj79nlxZXNvYGJhd/nv8/T7ePTl6Pp1
+8/RtanN79OXtc3v9amp7//P0+vJzffb7/3X5f3VuZ2Zibnl97vX77evx6+zq5fRsa2hr9vv0
+4u/87X/8/PPt8Pf8eGpjaW94bW1mX2htcWxv8/J1bu/7bf3s7fXs6vf7++70+/B39O9v/u3t
+/Pj3cl9faWr35Xdz8urj7P7l3+r7endtbW5nYWhvcWliZP3ye2/0/mp2cXF6bWbv8Gn35uzm
+3+vu9Xb9+X7z5+91/OrufPLz8OP+aXB6aWRtZHFuZmxjY2R46XJv7OXzdu3t7W1x6/j49vRu
+c3t47nr+6npk6uL9eGJz6/1x6+36cGR9f3B+7Pxma256dGj07Xd17Nvi7uHzanX5+HP06vP3
+dW9r+mljfW5ma+TwbXHudV58/PTsfGl/+G/p8Gxue/DwYXnr6uj4ffTx7fX56ejv5OV8/Wz5
+8HV4eWNYZ/VpZGvy7W727337b/Tdfnz0cGr1a1/29G7p/md4cGvm3m/vb25yXfnsft7S52rd
+623c8Pvxamt+b3X2cmVdaW5oZXLu4/VgaOZ8ZWv67Hfl2e91bHDu+/Ls63Rr9fl7+uvv6+ru
+c2/t6X/7eWFdXGl3dez6++7o7mJ89v7xfXjq7Hnp6W7h911++/l8+/FuW1vxd3zzaX7q+fjr
+4vvu3uXr8nbtaWJ0/ut7Yufub2hoa3R8bfNq8t/7du/jemhzfPJ9+u/n7HHxc3XyZmlieO/m
++3R7am5gZmtuZXLj8e3sa2P8/WFg+fDf8Hr3d/j06+Dg7uDU6Ovva/PsbXn+Y2r0f2lfYWhg
+ZmZldfV6bvt0YWJfZ3F7avjl5+Ls7O/m4dza5uDd7WBebVxUa2xmfPLf3ft95d59/d5xcWlg
+bXB0dOLf5uPr6ffyaWN3XFdhYV5fam14+X7t8+n26dvp9v31dWpsX3b7ae3r7uTf7O3j5u5+
+72RoeXpvY25ne+HvZ+bR52vq7G5iaWFeZV3r6XJz/vVv9+j17O1s7/VkX2ZxdHL+8uff9PDq
+X2LveG10enZ5fXj49Obs5d3e9n3rcGv48Xb6521x/H7t7PZpZl9dZHT1/nF9aW1oZ3pr7uLi
+6uzq7PZ4b15idPNxYP3o7PT8/Pjq/fzlaHB4c2Rddnj/8Ozl3dbb7f57+mJpbGTtcWhvcnlx
+ePLvZ2Xv7mh27mteU1l0X27c5Ob47dzX3fzj3HNldW5sa2ptae/wdO9q6+V4d33/bGxwZGdu
+fG/p62Le3Onp6N/uZ2BweGtkcvT9aG/u7vLobXj5X3h9Y314b/j9bGf+6eXl5uHzffZue+1h
+ae1zbX38cvp5cnluZmfvcWPo6Xnv7Ovk5nR96OV/anJ8al70em3g83Hqa2dqYv13+PN58H32
+82zt+21s+/ZuePHm7/L98PJvdP/s82n/7/50/+Ld7/vyZ1xfX21vZWv78PDo5N/oefvi4vBz
+em92a2Rxfmx+63P06O3idW/0bnJmZW9ncHf67e/o6+1+7+v9a25sZGx+bWTv8O7n3d/g53zs
+73FeY29pbWB34e79++p4fOr073tueHJlXmxwb2hifOz07eje2+hubHdsaHBv/fFpburt9/Pl
+5ex6bXthWGv09Glu9/p5eeTe4Ont9PlmW2Bsbl1fdOjs++nl6f1u8PBsZm54amJn6+x2fu/c
+6fv67vNvaWBvdmlo9O7vfujh7unt3/Jr8HBfW1xdZv14b2/66+zu6uXc3fRqaWdiZGt07/xs
+9O/77+Xq8eb38vlcbGltdWVibO3+e/bn4Hxx5+l4fXNte2Ffef7xaO7f6N/d2+p+aPn8XmJt
+bmllavf08f578ORvef50b3ByXm38bH7f3OTj4PPseXVxdPJoXmloaHd06+b7297v8+zd/W9q
+ZX5mYHDiZWBybepwbfV3bHL6a3FwbG905e3u3eDo9HX45fP5dWp5Z2zs+ft5aW1tfO7pd/rj
+4m9eXmDs6nX27fzt5+5vY2ZsYl1t7+3r6Ontc23+/XX7efLvZXHsamBqdfp8/vnr53H37Plt
+bOHoZmn97u/0b2Xy6vJ79vXu9Wh1bWBhdO33aXLc4Xfl63N7XV/q82507etjWnHyd//97enx
+duvnfnb692x0bmr8aGr9f+rn7fDd4mVgcP5ue/ny6XZv+ejub3L+cmty9Gtm9uz1amj15uz+
+8u3x7/B1bWpnam5zafp1Z//4cm51d+7m6u/37Ozu9W9q+evy5uLu6uTl+v5uZHj6aWp+Z2vy
+fl9eX2Bwc2N45/d1dnT24+/r5d7i7eP28//86ezg6HJubG1tbXhuav99939vb3L6cWl77vZt
+8Ot4cXf5/3jy7ON9XmX17W9vffL/eO7v7ufq5uT3fHR89flx+un3fnpsZmr/b2drbHh+9fN7
+cm55ffbp7vLr7n5783119n3y/Hn66OHv9XRpb3Z9d354fvz98P5tcff09vT57X50+nVtZm5v
+a2dfbv326u59fvju6uvp3u7u6e7l73lvbGlqc3r8/PXy83VoanR4Z2ZpfHVvfmt68uzo9Hx9
+7uXleWvr7PX3dW5ydnj57fx3bmppZGlubW598vJ+8evy7nxvfPj3/Onwc/Ht9ft3e/Tx/nn8
+9O3yent4a2hsb2lrbG9ubnh4bnj4+/Ln7vv97d/h6/n49WppdXRtb3FubnH99+/v9/n+/nBw
+//r7+fN/dv56dfjs5/B8+unn5urz8vj6+v1rY2ZoYltfb2ZpfPn2e/rx9fl7c3duc/Py7+jn
+7u3r6Ort5+Pk7n9rZ2xmZmdreu/u9vjr4+14bm/16Oz2+3dobHN+e3V0a2Vqd3597enu9fp8
+b3N4+n5ubHT6dm1rbvjv6+/y9O/x6+js7O/v+H7x9Hv+en17fHh8fHZ4d2Vjb/fzeW9ue31z
+bW1ta3T7dn3+eXj/6OLh5Onn6PX97+vx+nxrX19ka2lpamz9+fr0/Xzx5+r3d/nx8+zv+3p4
+enh+fvbz/PL07fp0a2tpY2ViX19fYmp1e/Pp7/717u/j5ejs7Ojt+vPo6Oz69+jm7Pp8dGdj
+Y19dYmVjYmZse/T+ffx7/vPq8nZ1+fDt7PDu8P1/f3t9fnhtdHp2eXhrbXR4fHd/fnrt6ezv
+6unv+ft+e/748fh1cXH9/3l+amJpbHRqZmt18fV//fr2/ff49O/z8Pd6endxb3jy8PTw7vz3
+9nx6fvPn9XVqanZ0fPj4fvju/mxnbHd1b3n78/p4d/Xz+PF8bHj9dXZ5fvX3dG1x9H366PT4
++vTucWhsbWxrbf3x+/vq6Onq+Hx3b3f++fp3dXNscvbr7/rw8fT++Xttc3p2bWpjZmdkb/36
++O/r7O3o4OTr7e/0dHN9fnRubmRlb3l+eXJ7+e/x/nxydPn0/vvv7ezwfHz47vRyb/1tanN5
+enr29H1ub/f5fH56cnN2cHV4/u3q8uzl7H379v5xbF9dX2Fvev939uXf5OLf5ut/b2ttbWhq
+aG14/Xdod/7/8vX87ur2dmhs//bxdWp49n13bm9wd/Xm6Ozn6fp89fTx+XVqaWRjaW1vc35/
+8/Ds6ux3bvv9+3x2a2pydG50e+/o8v3z8O5+b3FsbX71b21yefxvcvry+nvw8fDn5+389u7s
+8nxtbP5vcPf1dm9vdHP77/p/cnp+bW1tbndtb3t7ePP5fHZt8/L/+PTs7PX5/H/05uj3/flt
+aGlp/PTwfnF4dH3wfnR7/PR6bG7w6fj67ejr6OXweXB5cmt3bWNdXF9leHhvc3b36vL2+vbm
+5OXq6u/x9f3z8/r89vf37ufs+XtzeGtod/32fm1sYl9we3FxdHV8fvv18+ro6e35+n78eX3v
+6u3ye3l3fPf07fj9/WxlaHZza291e2xrefj28u3v+Xt8+/fy5uLd3OLw+u3+fP15dHB6bmp+
+/vxtbG5teGpkYF9oa3Bva3r19/Dr6N/c3uTo6ejk4+Pq6ex5b2tqZF9fX19gbG9qbHfw7/D7
+8+97+uzn6/Lu8PT2+vx5bnrv7vn98O3u7Pz+ev12bGddXWFeXmVr+/Hj3uTvc33x7OLg5Ort
+6/B+f313+u37fHb96Ofs+XdvaFpQU11kaGJhaHLx5OHs6N7f7PX4fu/q6+zr5unt8PPq6ep3
+bHBtaWlnYWdz9/R2bmx1cXhvb/15/3dsZm/t8PXt7fH+b213bnH6+fXv6fD9f+/s9/j17ev2
+9u7q6uzm3+TzemhfX19dW1dTWV5k/erk6ejufnFudPHu69/j8Hp2b2xtcfr2/Ozi3t/f4Ojm
+5urm6fPu8XdpWUk+O0Fb387LzdPdd1JOUF/czs/X3ev27/fv5uft7/P79Pr06ez0dF1SUE9T
+YHTr4+d6Zl9hbuzd29ra3uj6ffHl5+zramJq7uN9XlRWXWh3c2NleW9hXV9y//zq5ufj3N/t
+8P3t3drc4uDe49vY3eXr8Hh98/lnTj42MzpO1r6+w8ziXk1KSlzXxsDI315bYnLj4vHv7u9/
+cGx56OLs/2xbYXb57fp+6+bxcGJmfePZ2+P58enh5O33SDAvOkrJuLrDz2FMU0lN4Mi/v85d
+SkxOat3b2OB8YGVvfdvLxsncWUxSYOXR0dnj5vNgWllvfVtfXU86LjU/3rm1u81pSU9aTFnn
+zb67xv1YWmfk8Hb6dG7y6vDf3NrU2mZNS0ti19HP0tPY62xYXuLc72dXam05LC83+7azvtRk
+Um5rSUlc1Lq2vulSTVJt+nhuZ3zZ1Nrb1dLQ21tLSVXfzcvP3et7bVtZeebV2l5NVlY4LCw1
+6bOvuNBVS19jR0NUybOvu+JNR1Xo2OPw6uXb8GJt5szF0Fg/PUh719DQ293T31pUd9bCvtVO
+SFpONSsrNdy0sLzWXVdyTTw8Vb6urrtlQkFY3dba6OjOx9dbTmXLw9VbR0ZU7d3s8OjY12dN
+SlPYv7vDYkdHVTolLDjZrKu012JDP0w5P9G6r6+/Uz09QfjK1etmatXH0N7T0NPgUUVLWujP
+z83O0dPfblxr2svTSiwgJjrLrKyyx1w7Mzk/a8O8usPsUUhR3L+7yHlNTGD/7dzMwr/E3Ew9
+O0vTvbvMXUhIVWJ94M6/wMn9SENGXz0jKDBMrKisx0c0MlFJTMy9raq3WzUyPNi6wtNfTeLG
+ydlmXdvF100+P3K9u8hiRE7cysrR3NHZTzQkHyxXsaesvlJCOTpCPVXAtK66Zjw/9sS5xFxM
+U+vL2U9O1725w1Q5NULXwMTuV13h0PFTTl7IvL7KV01NVzkgJC1Oq6WqwFg6NEk/Q866rKm2
+UzIyPtm8x9v1bsvG5VlOXs++zkk6N1XAvcdnSVTaytDR3tHG+T0oHiY/uaWlrtlFOzdAPUrJ
+ua+zzkI6RWnGxN76aF91ak9Q3MK6vnxEOj9uysTVZmHuz+FhXXzCvL3IVUY/YV8lISw0uKep
+ufdJNEVCNFPJrqSqwjovMknCw8rL2sjA30s9Q3+8udNRPULn0M9vTFvNxdXbc1/Ux811Pywh
+JDJtrqirt3s+MzQ4P9W4rKu210M8PUzPxMbN3fHo715g28nC1Us9PU9j897ZwsLPbEVKfMe6
+yOFaRUo6KSIqPsKqq7XJb0I7NzJBxaykp7pHMjI/2snDxsnByPVBOD7vwMHRVkdITlhXYd/F
+v853W2Xnyr/Ya009MyYnNv2vqq3BWUc5Ozs+5rytq7bcPTU6U83KzdDZychrSElmzMnTXU1b
+XWVYZNnKv8TWb1Re08zTaklFQi8jKTPorKmuxGw+OkA2POq3qKeveDUxOWPKxMbOycLTTTs8
+W8K6w+xMRkZMT1nfwbq+12JKU9TLzd1cRTsuIyg51a2pr8lgQDo+OUDbtquqs3I3LzdbxsDJ
+zsjDymFCRmnNxMruVk9LTE9p3MrEze1STXPaztFrU0g3JycwRrKqrbnqTT1COjVRx6ylq75E
+NDRB5M7M09bEwdROP03mzMjfV0lJTE1VctDDwM3vZVt+0snRWEhBNykqOF6yrK28d0g8Qjo6
+XMSuqq7GQzMxROHQyNLTx8XaS0FCYszFzOVyUkxQW+HLwL/P719dfubP3elfSD4pJS0+uq2s
+tstnODc0M1G8q6itxUAzMz1zzcnIw76+2Ec/RmfNwspvT0VGT2bTxsG/zOhZT2Td09Xfb0cy
+JiYvTbisrLjKXj03MTZQvKyprL9GNDM+ZMzGxsG/v9tGPD5az8HI5GRJRElQ6Me9vtFwSkhQ
+ZMzDx8x8OygiKDXLramrtdQ9MC0uP8Wup6i2YzcvNUrUxb+8vL3NTT07Q3fJxNV6TEJKVe7Q
+xsTK21ZGRlTbwbzD1l02JiQpN8Krpaiy7TQtLDFeu6ymq75NNC40S9nDu7q8x2pHPDpI4cTD
+01A/Qkt+zcTAxc3tTkdLcMa7vsxzPSgkKC/nsaeorb8/MSssQM6xqaq03z8yMTtUzry6ub3X
+Tz89RGbNx8jeUkxLU37VxsTM51ZLSV7Ow7/K00opJCUtdbWpqay6UzorKDRrtamosMxINzY7
+RF7Mu7Kxv1k7Njxb1tXW197eb05JV9e/vcZ1R0VX18bF1GpKLScpLVe8rKuuuFw+MCszT72s
+qK3BXz45PEJL98S4r7jbRDk6RXHl3tbi3f9RSkt1z8XD03FbadrKyNX8Sy4oKi9Ov6+srrbv
+QjEqL0q/ramtvfBFPDw8P1bPuq+0zks7O0FYe9zMzM3bWk1OYdnMytbu/XPi19LT7UQrJysx
+7bqwrrG53EsxKS9Gvquorb3nRT48NzhJybOrr8dUPDo9SVJp0cvI1VdKSl/h1MvP29vX1djc
+6G85KCktPsS1r7Oyu91NLSgvSLysqrDA205CODEzQsyzrLLE8khAP0BIaczFxc9tUU9ZZ+vZ
+2c7Nzs3V1eQzJSgpPMCzr7Gwu8xvLygsOMmuq6+4wONVPjAvN1a8rq64xuBMPjc1PnPDvsLP
+7u72Y1Zb+9e/vsnc7lAsJykrTMW1r7KywctZLywtOtm1ra6vus90QTIuNUfNtLK1u8ntSTYx
+N1XQxMHL0dTff1VPZ9PCv8LNPSopKi9M0byzrrG7xks0MC84U8e4sa2vuMtNNi40PVbLvLW0
+uMB/PTEwOlXRxcPHxsviYEpNYNjHy8vePywrLTRM07y1rq22xE81LzE6T827tbCwtsZdOzAy
+N0d81L23tbrOVz06PUJf3MzEv7/Hzdfp2OJNOy0oLDdPzry1trK3yH4+MjE1PlPMu7Wvr7nK
+aUI7Oz0/V9K+t7W7yW9DOjk8RVXtz8fCxM3Z7/xoXFxw2s98NSwuL0Dfxby1sbe9zUc5Oz9M
+79rdyr26ur7oQj0/Q0tVWXLNvLrC3F5abvLpbld308jCyfpLOC0uMThJ9MvBuLa6usLhWEA2
+LzM9T867tK6vtb/RWEA8OTo/TuvOwry+wcvtWktDPTxEVevOxsPBv77Dze9ZTTotLC0vQPPL
+wbmzs7O72VNHQ0ZRTkdQe9PFwszl3NHO0HRMSE5v4+Xy9NPGv8HP6X3r3PRTQzgyNz0+RUpW
+btTBvbq4vL7H80Q6NjM3QVjXv7azsbS7wtJjSkI/QEZKT2X439LT2ulzd3ru9GpnZ+3V0c7O
+1N9fRDw4Njo+R1Ft18rAu7y/ytHX19Da5vhhWVJLQkBJWOjRysbEv7/F0nJaWWV0bF1VV2Jx
+cGNXTE9bX1ZMRUFDT1/12c3Gv73D0OFnXWhsb/vl4tvT2eF+W1RRVVlk9+HXz87T3/1mVVJb
+ZWp09fLf1tDR1dnf5HlUSEM/P0BBR1BccNrM0tXT19fOy9La3uDd4exsbHj76+zv9eXb4Hhe
+WV775ePi8Xl883547urq7vX/ZllOSUdGSUlNWGfr19nk8ff7+vXx59vRycbIyc3W5/htYFhU
+X27x5OLj635kXlxZXWpze+PZ2uLu8fPxeW1mW1FNTUpJTE9SWl9h/uTd2NTS09PT1tnX2Nzf
+2tba5uzu+/j2fm5tc/f2b2NhWlhbXGZxd/Hd29zc4elvXFRNSUpLTVdib/nq5N/d4fP15uPk
+2Nbc3Nvb3t3j7f797ujm7XxzdWx/9m1jYWp58efn6fRvY2h3dXZ1bWpmX1pSUlRWWVxdZHF2
+/ene4une2NjW1dne4uLk7Wxhcvnu6ufr7eLh5e79+O7r7+zi5ev2+nhrZltOTE1NTk5NTlFY
+W2Z48ubm3Nzd293g6+ni3d3a2+Pi3d/h29vb29ze5ezpfWtnaGJgaWtsZmp3bnV9Z1pST0xK
+TUxNVFtdbOrd3tnU19jV09XY3d/a3ePc5f5/ev11bnZybHL46uDf4eTh3+Dk73Zub2pfV1BO
+TE1NTVBVWl9mZW346+jc2Nvg3tvZ2djc4ODj4uPvb3js9f3v6Obn5+Pu/nb+eWhtfHp8f/x6
+bmVdXlRLTE1MT1NYXGh979/b3tzV0dTU1Nvg3t3v+vt6en35+v/27+3h5fT7dmxufv17dXt7
+fvh+a2VhYmdkZWlua19bWVtaWlpZZXzn3t/l4NjT2Nvb4eLh5+fp7PHv/nh3e/3+5+t97u7x
+9fd+b2dlZF9rdWhofG5namFmZ11eXV9gXl9fX2V7+uve3Nrb3Nzb293d3t/f3uTxfn14ev7+
+fHJ1c3BucnP78fZ3enxubmllaF9iYVxcX2ZiZG98a2doaW5tbG786+Pg497f3dne6u7i3t7g
+5eru6ef3+/Xx8W9ta2xlYWhlZ3Fydvv7e/D9Yl9gZGNhX15ja3V1a2/1+P96+vl2/+3i4+Pm
+5uDm6uXp7O/t7e7z6+Tv9vzqf25wampgZ2JkZ2Jt9/dsaWht/O3r9/zw7v5ra3Bzb2Joamhm
+bnd0fnh2/e3r5+Xp6+jg4+vm6ffp4+p8/Xh99vf++uzz9fZ3ZGJtZl5dYWBjYmhyem1vbmZi
+aXJrbv3v8+zi4uPk397g5urs7Ovs6+jw/Prt7vl9fHR0cW9+d2x7d2NfX2BfXltofHl+fv91
+aW1ta3bt6u/p4Nzd4ufs6fF2dHt2f3/+dG9yd3ZkX2dtb/fy/v7v5fDz4uTl7Pl6/f57aF1f
+Z3V9d3Zye3x/cXFtamxvbnP18Onsevrr7Ovq8f59eH19fnf37PXt7vh9+u7r8m1qcnB4fHv7
+enR6cW7/8u/6dHV5fHByc3X6enFxa3Xv7Hl2+/P6/vd2dndzbnh4+uvvfPbs5uLn5Ojv8+32
+eHVrZmVm/f35dXju+Hhwa250c/p7aW5zdm9sfnt9+O33de3o7fp9+/X1+/n+8vZ9e21obnt+
+fnF+/mho/+z5d3p+/Hb68n/46+Pn7/Xt6/H08u97cv53aWpxcHpxaHF+enZzbm5783tsbWVi
+Y3N9+fb89+nf3+fl6v15eX5xcf77/+zp5+Xy+Pv47e38bWdv/3Vv/vbu9WtscW108HhnZ2pp
+anD56eXsenr69/jy6u7x7u91bHV99fn9+nRna3b9emx6+Pvw8P3//vn37vDu7/d6dnn87Ono
+6P9v/u3w+nlva2NiZmx2eHBoZnN/8vR3a3D8fP3+/vnq5fDq5uv1+ezn7fV3Z2lnbnZvbHLv
+5+35fnt49v91dXR9b3F8eHzw7/f2+Xxtand0dfh/dvzu7+/t9u7q7u7x+3txZ2dvcW5wc3B6
+/H188/H+fHn8f/34+nd09Pv9eXf07+z5dn/y7e/o6f15fntpZHH/eGxufXZ29/tzdndvbm1y
+d292/Org4eXo7fb37uv1end1b2ZqfPj8+H979e/r9XNwanh8dvl0al9v+3tubnh4e3f0/mhy
+d3r08enm4+Df6e/u8Pd3eGptd3f+8PD58u7s7f5va2hobXJyZ2RnaWVrff/6+f1++fTp8f9+
++evo6+7r6urn8XpqZ2lqa2x7/m1ucnl9/Xz2f/zv8v737Onu+Xp+7/L6fXVxfvtzam50bnNu
+ZmtzbGRmdPfy9v1wfPDo6ezo6OPm6Onm6e3v+W969HxqbnFvcW91amloZGdvd3FvaWv+6+Tk
+4+3u6u3s9Hxucf/17/Ls9mxmc/T+e3t7e3Jyb2198u/88u719Plyb3f+/nx7cWp0/Pj27+76
++PP6//z7/v5/c3V2dXN+fnZwfnxzdGxx/Ojg7Xz+/X54fvz9dW15fPLu8vfw9ezm6fN0evv+
+aGdpaXX8e29vfPb+bWFibW95/nz48ezi3uTr7vZ0/+/6bmx5//70/ndxdHJtdnd4em9lY297
+efv19Ovp5ujx8u3n+W9zfnFqdXBscXZ1bnR87+z7+fr8+Xl7+vh+c3Z0efr6fXBvbG1wc318
+e2/+e3j99Pj88u7s+nb88X//8+rz/H5vamVrbXBtfPf4fXF0d3z97u74+H3/c2xvd/rs7n35
++PPr6vP5e25veXv59n12fPH2fP35enp3ef79/vx0bvnx9H1+fX7/dnVsaGxybW978uzu9PTv
+7/P+eGpndfn4/fr08+rn8Ozv/nN1f3Jxdn5tbHnw8fr5fXdvc3Vuand9ffvv6ezv7PB9+/v+
++vd9cW9vbnn5/XZ0enNvcX54cHv5+XxvcX379Orq8fl4ef9/9fLxfm96fnN2931tdft8/3pv
+bf348u/zfXL08X787ez8cGdrd3t6/HFsfPT+cn/5/f79fH58cG/8e3vy+mxz7/L78Ons7fPv
+9H3z9/H5b21udHFveHt7fvz1/3349nFqbW1wa2z7+Xv39PLt7fT5+337/HVvd+/xfHl3cHJ9
+8Pl4cf/3e3zz9X178evu7O9/fe7u8vF5a2tmYV9ocW5saWhufPf2fvHo5eft7+vq6+jq8vj8
+e2hlbXj5d3b58/dxb3x4d3t1bmtsaWlmanL87/Hr5evu+fvu6Onyfnn47ev1dnn9+X99ent9
++/xzc21qZV9fZm5va2x2/nv07PXq397f6Ozp6/J+fXh7fm9sdHJ1+3tz+PD2fv/s6fpwbWZk
+a2xnZHnv+n5zcf7zem5ud/l+fnvx6OHl8evq6+/9dWtqb3BuaGl5fvry8O/9cHF4fHd2bW93
+evj0+Pnu6+zu/vXu6+39d29nZF9ham91fPft7vP4dn369ezz+HR/7fJ5b3p7dXVuc/12b3t3
+fvbtfmz58vnv6ev28O/u6+vq8PtybGtua2RmbXRubm1rZmhlZ2Fgc/p0cvDm5d/i6Ojf3ODn
+6enn5fF5/f9vdW9mZGNteG9tdnxwa3R9dnB3cGlrdvRy/fDr5/N9d/T6+u3u+m1vcnN5dO/t
+6+3v6n1+8PLz7+rs7OrvfnFtbW1uY19qZ2Nud3hqampsdn7393n66OXk7e7o4uHj6fj9dHFv
+bnhydXFsdfbvfW5pZmVrem1z7+7s8O7o4+Xs+HZsbG9wZWhwampsZ2lwb33v6eXk5/T+ffft
+6+7x/Pp8e39tamdoaW90/v14d/Hq7Ojv9HtwdHx+/fv57n79/HZ0bHNvbnBqbnN4dHf6/e7u
++urv/O/qfXd6c2pn+ft29+vx9evr+WtjbXR7+n1//fx0e355b3Fua3r87+58fvH3+fP+eHL7
+7/Z3ev5yc2xncHv48Px3/u7s9f789H57ePzs7fB+/XJ09u52a29sbHB5ent+fPz0en31f3b6
+9Xt7cXr5+fry9/vt5uzwdnl1bW9wdmtqcfrx9vr5fW90fnx0dO/t8vl7+O/v6uvw9n7//nx/
+/ntta25sbXZ5dm54fn7/bG/08fj28fp7ePrv9fv4eGlpa3r68+rp8/x++/78++719u7q9GVk
+Z29z+efw+H57dmxwd/1+fvzv7vb5ef5va25rdWxpamn47eXt/fDk5u7n6Hh5+/z+bHH07/X7
+enZzcn5zeHNqbGpvdHbz9ffs7fZ9dnr5+3x7bXrv8/l893388fh3+PF6dXd1ePn1/v7z+nhv
+evp6cHJ0fPTu6vv78efk7/P7b3t1bHlpZmx0dG11+XN3eXnv7/Pp6+/3+O/z5+X6eWxy+Pv8
+aWRmaW16+3x1+3xx+Ozvent5fvnv9/16e/x3/vvw5ePrf/d99+t3bWlse3Rwc330+Pf9fXx6
+bm1ufn5scHd96N/tcnV/evR8d3tv//708+rk7f9samhqY2j77+bt7PB8//f5b2hz9Pv1eGVo
+bv7xeGtz+Pf18+3o9f3k5/12eP56bGp3cWxxfXp1+ebvdnh99vHo7Xt4dXx1/v/49/5xc+rp
+7/lvamlu9PNvaW55c3h++3j58vh7bu7r7/h+bG/v/Pby++zy/PdzbG9sb/Dt+HF17PV4+/38
+fHt++/3r7/11cvvw7e3/dW1mb3N7cmhycW/9+v7+d/359fP/evl49+jw7+38bWlsef367+/z
+9Hj++Hz08fh3cnbw9nR98uh+ffd78Pt2/X9z7+/+d2l2b2NfanJzcmxya27u6+/4+uni7G9t
+8ubq7/Pn5+He6PdubnxzaWxvZWptcXn+/HVqbGpp/3z6+vvy6u/r83zz9v/37e3s+397dXF7
+bGhsbv98e3hy9+Xg6v3y7/BtZ2995vZ19Ptqc/5+f3Vu7/h9729xePjt7eri7v5+bnLs+nj3
+bW5qYW9uavb2bG9ue/xuZ3Tu6+ns5Ofu5ef3cXl5f/Tr5vNpZ2lx7/P6fXh7enFte+7i9XFu
+dG9qb3R9a3vq4+v39PPu7nJsb3X393Fmcft7bmp3eX33fnJp/Ovi6+5+ZGxu6ub59Xvz6en6
+cX/98vf8a3T6eW9ocHf/9Ppybnv76u9wbHTu8fl8enz9735pamFhbF9tcW94/379/3z36OHi
+5Ojq8eHd4ebq6ul6YW93b3ZoYWFs+XJhXGBtam1sfmxz+n3s/f7+/Pzn3+jvcHrs9fR1/PNs
+dPf3+Xx3+vx68Px6fHr+9+3u83V4/XR3dmlpeP18dG9rY2ZtZXnx7ubl7err+vt0ff11+/t+
+7+39+P306ntvb3t1X2JjaH7t6PTy+Onn/H5+++55fff+7/d2dHr37+9qaW9pefzv+XRtcvv4
+/2pv+fXj53T98fbv8H5v/v7q7O/8bunqd212cG5sZmxqcvLp7H1+5eTw7fVyaW725vFz+fT3
+9HhtYV9x9PljXmn+6/rv7OXm5ep5bHTw7OP0bm1p79zmcmp66/JqZW1rbvf2Z15m9/R4fG90
+9u3ufXZ09Ovp8Xno3t/5dG1s7/ZtbXB99PLv/Hl2bW56bGh0eXhwbW50d/re3uPn7fN99fp9
+dfxvcftxcf318fb37X1vbW3//nN2aGTw8Wlvdffr+e92ZvLf4O1lZ/L/8en+fPJ9++n5/nVl
+/e//7HVhcWt/emhzaGP+6n1z/O7n7v3v4uXm8n3k9Gp4dv37cWt0Xl1vb2VnaFxr+O7mePjo
+6uPj3ex7fXD79uTi73hwbm197fl7/HJeaXdnZnT1+P/59fz+cHr5b2j46fV8ZVxk7eHl+nzr
+5uzu7/B9b3bxfnp9aXj++fFyann58vj57vz+b3Xrfvvldmpxc3z76eJ0ZW1uff5obHl+8PHv
+8nXx8HBua2717vftdWlpdX3p4O7k6uv5den/duDjeWxpaGZldG1ma2r8/v/9fXdtfnl5+Xv9
+9/F/c/fm5ebr7vrp4ODuc/75/Hj3fmp1dm9t/31xbGluamFeXm/y+m9t7+3y7unq5uDo+n/y
+/e/6fP57bv3p9u7we3JzbnN3/vv7/3n8+/n9e35uYmJw/PLv9uzy8+vt8u/o3+x/aWZtc3L7
+8fX5bWVjZ3H7/vn8fHz78PDt9fZ3cP71+fv8dft+e/lsa3R69+7z7+fv/fr++fbz8PLs+f9y
+ePN8c3BtZmRrevv39e/y/ffz/W5tbWx2bnNtavz77Ojp8O/t+Oz4f+73/fF1ffNxe3p++f97
+/Xh2b29nbPzu4uzq7nd78H939v74eHz4/vl3bWx3+X1vb3j3em988vl8/Hv7/fDr9u/06+t4
+fXZyem76fWpxeHN/e3z493596+z18XZ0+nx8+/r//X19eX7y+HpsbHR5fv7xfmz8dWFmcnp7
+/Ojg5O3yfH359nxub212eHJ8eH1vaHzt7Xp8eG51evbm7Hp2/u/t9PN5cvTq/nJtdnxranVu
+bXVvfX1qanb98Oro+nTp5/Py7/14f/Dq6PL/a2NqePHw7e3t5+b7Y1xTWGJmb/rs6vpoZV9q
+euvf4Obr9HX+6drZ3drX19vk6/Ph3N3oXExISEU/OjxK3bu0t8lWPjpG37+7vtVeTExUZenc
+297c62ZWT1Jv2c7M2nlVUlx72dHW2tv3YGd73dDO1OZnWVI4Kiwy76+mqLpDKygy6LOrr81E
+NzU+eMG3ucHpSkJM27yztLvTWVzVwMo/HxQYKK+VkJrMHhMZL6qamqdUKSMrPc68tri7w2Q9
+LCswXa+ioq5UKyYu/q2kprL/OjZJvqut1yYPDhc7mo6QojAUFB/enpmetDopKzVa0MK8u7nO
+SjIuO8CooqnPMCUqR7SmqK27y+5PJQ4OEiyYiYiQ7g4ICyGfjYuUvx4VFSFEs6iprL9bNDA4
+1a2mp7s+KixEtKOhqLxaNDElEBEXKp2MipDFEAgKG6eNiY+uHxEQHmmmnqOv6kI1OlLHta2v
+wlI1PuSzqquyzzovIRAPFyefjYqOqxkMCxbGlY2PnzgaFBos16qhpa7XMisvWK2gn65IKCYz
+waiiobFKJhELDxnHj4mKlT0RDA4grJeSl6w/Jx8hJjjVrJ+jskcpKEKvn5+uQycnNLafnaRf
+GQwJDiqejIqPrCMTEx5Qqp6fqr1pPzMpIic+r5ycp00gHjGumZWe8iIbI+WinKNJEwoKEWOU
+i4uXYRsTFynBpZ+hq7ryNygfICvZopuerT0kJj2vnpymvzkrNF7Qws0nFxYTILKckpKlQyMb
+Hz3Br6mttbt2NCkmKUazpqKq1D00PcCooKKwUTAwQNbNPR4SEBg9npGQmsMmHB4vy6+tsLm8
+u8lFKSMnPLOhoKnTMCsyfKqdnaO6NissN005HBITGV+Yj4+bSB0bIjm3r7q8wbmwu1UtJCIv
+2q+jpKy46URJ2LqtqrHLWjsuKxwTExYrqJOOkaE3HRoeNMy1sK+ztrTLRzgsLTdD3byvqqan
+rLTLWEdFS1rR4EEkGBIRHEqhko+WpfIsISIkLEPJqqCepLhJKyYoMEvWt66sqqqrr7jHajwr
+IR8hJi45P0ZJ37iqpKSrv0cuKSw39bepqK25ZjkwLTE8WMWxqqinqrPK7WJJQDcwKyMhIB8o
+OeevpaOkpqy91T8tKSoxV7itqq261WdQTlNeec26uLe6wcTP+Us5LScjIyMlLTpfuqymoqKn
+rbpmPjIsLDA9XMm/v728uba4v8/p6tPFxcxoRD9DXXRPOysiICYvQ9TEvbm0rqqprrjPTkVH
+XW9ZRz07PlTKurSzuL/Fy87TaUxGRUlc6WlENSwqLDVDY9jT0M3JxcG8ubm6vcXT6VlHQj1A
+T2rMvry9v8PGyNvraFhOaNPQ3lQ+NC4sMDpEUmzr6Oje1c3Du7e2vMrpU0U6PFjbxb6+xs9y
+UGzX0snHytDa4NrU+VNHPDk6Oz5JTlVXVU9PWGjVy8fAws7mXk5MWfbOwL7AzOFx99/QysXI
+yc/oW0ZFRD9CSE9f5NXQ13tYTEdISk5QT1Rb+9zT1OpuZ+/Qw8DG0ehmYPXUycbCxc3eYU5J
+SE1TaPd7dGFfbeng3uVjUExMTlVaVVFQS0tZ/NTLxMbO3XZfa3fp0tHX2d93ZGludvfo4OLj
+3d7U0NTV1c/X6GVZT0tMSUZEREZJUl1peGxeWVtk7tnQzMrL0+huYXLu49zW0dXhatvA2+7W
+yc/LzM/N5mxZUUxFPz8+Pj9FR0pUVVxt7dzQz9ja5fr4bU1D6NN41sLHxrq7vb7Bw8bGyM7d
+6N54TjsuKikqLzU7QVrLubCvr7W9y3xKOzIuMDtYz8C9urezs7i+x8nBvLi7wcv6RTsuIx4d
+HSY66LeqpaKfoqq4TS0mIiAiKTXtsKagoaivvuRPQjxG+sSvq6+zvmozHxgUFRsoWq+hnJmX
+m6K4OSMcGxwfLEi3o5ybn6nBTDguLTFDyq2ko6asu1MpGBIQEx02vqSbmJWWnKxNIhkYGh4s
+ULmjm5qdp8g6KyUnKzfbrqKcm6GqvTofEg0NEiBcqZuVlJSYos4qGhUXHChGvaidmpuhtz4o
+IB8kLUq4o5uZmp+uzjomGQ4NEBg0rZ6YlZeZnbI5HxcVHCc+vKuknZygrGkrIR8jL1q9qJ6b
+m56nv0AsIRsUDxIcMrGem5qanqKtSiMaGBwtYbmqpaKfo7NRKx8fKTfos6minp6iqrxJMy0q
+KB4XGB4q06qnoqCkparMMCQeIC9UzretqaWnuE4uJSYwSs+3raeioKWuxlE5NT87KBwWGCA1
+xK6qpaKhoKreLiEfJjdczLmwrKitxUMsKC08eLytp6Sgo6y8fkE4ODEhFhQZIEevrKafoqGf
+rk4sHx0oPPu8tLmxq7TMRCwoMEXOsauooaClrLpZPTYrIRgTFh0vv6umn56fnqbMMSQeIS9H
+27y4s6utvOs5LC88aL2wraikpqqw0Es/LyMaExUaJV2yqJ+dnZygtj0mHh4oN03Qu7Gppau7
+Ty8sM0HpwretpaKjqLX0QC4fFhMUFyNStKObmpqan7dEJBobHyk9z7qsoZ+jrdc0KyovQt+8
+raSfnqKuzjkkGhMRExkmWqyemZeYm6O+NSEaGR0oPsivpp+fpK3VNyspLj7qvK2moZ+lr8o8
+IxkTEhUcKmOtn5qXmJujuTojHBodJjVvu6yjoKKrvk41MDZEfMCyrKinq7THTCgaFxUVHCs/
+uKKempeaoa5cKB8dHCEsOOqupaCfpbPQTT0/T/fIuLKwr7TBWzMhGxkZHCY1abOmnpuanaa6
+QikiHyAlKzbls6ehoqq2zGpdbXjdxru0sbTA+T0qHxwbHCErPMqupp+dnaGpvz4sJiMlKSoy
+Wr+tpaarsr7Jx8LGyMbFvri5yk0zJR0cHB4kLUDIrqSfnZ2irMBELiglJigrM0/Crqemqa+7
+w8O/wMHCxcbCyl89LyYfHh4fJi4/xa2noJ6fpKu4XjctJyUmKS9Gy7OrqKmtsLW6urq9v8DH
+32pINy4kHh0dHig2aLerpaGfoKWtw0YvKigmJys1TMayrKmqra+0t7e4vMLExuVQQjUqJB8d
+HSAoN922qqOgoKKnss5GMSonJSgvPfW6r6yrrbCxs7O0uL7Cw9FcRDgsJSEdHR8kLVS/r6Wg
+oKGkrLvqOywoJSYsOVjCtK6tra+wsbK1trq/v8PTWT8wJh8dHB4jKTfouKujoKGkqrbaQjAq
+Ky0yQ3jRv7m2tLKztbW1uLm6wMTG31E9LSQeHR0gKDFIyrOqo6GiqK/EVjsyLzA3Pktg4M3B
+u7i1s7KztLa4vL/F2GlKNi0nHx4fIicxROW3q6ejoqWsuF0wKikrNEvlwrm3tbGvr6+xtbnB
+7lBOQDk/S05aRy8qJyQpMTc/WPnItKupqrHOT05UX9rbdGBiadjDv8LCv720r7S7x19CQEA/
+SE5ISz0pJCclKjtBQ/nWyrOqrbCwusa8vc/WYkBAS09p0s/MvLOvrK241VlEODI1NjlNW1ha
+OionJikwPUlU+c/AsKmop6mts7nB2lU+NDM5Re2+t7Sysra5vt5NST46PDo1LigiHyEoL0LN
+vLSsqaelpamtsr3aaks7NTQ3RefCu7m7wL/BysvnRTAmHx4fJS07/cG3rqyqqqussLW8x9hZ
+Pzs8R+zFvL7L31tOWlRHOi0pKCkuO07dxrmxrauttL/TdVxSVFBOWGbexLewsLa+3VJKSEQ9
+MCciISMpOFjMubGuq6mqrbC4wMrZY0c7MjI8Wcu7tLe+y9z4fXNSRTsvKCQiJS0/8b60sK6s
+rK2trrW7wttXRj06QF3by8HH71dPVWFYSDkvLCknKi41Q/e/squoqamqrrO2vsngWUY+R1vx
+zsfQ4+h1aWBJOCwkISAjKjFA+ce5r6qmpqanqq2zvdlOPjo/Unncy8nP0NntaE06LSgkHx8i
+KC9G0LerpqWkpKaqrbK+1XJLP0dj3srAxcbC0HVPOCkgHRwdISkzS8y4rKSgn6CkqrC5xuVP
+Pjk8RlTiw7q4uLvGWDQnHxwdHiMrN1PBraWgnZ2hqK+6zl5EOzk+PkJY28C0sr5XNisjHx8g
+ISgwRsaup6Genp+fpK270kw5Nzk4OUBPft9MODQvKywvLi4yOkzJs6yppaWmpqmxvMl8VFxT
+Qz9ESDwtKCopKi81ODtK+8CtqKimp6qrrLLE/01DS25xUk1JPzMqKCYkJiouNkNpxK+moJ6c
+nZ+jqbfoRjkzNDk5Nzk3LSkoJyYmKSsuOEzZt6iinpuanJ2fqLLEb0tJSkU7NiwkIB8fHx8f
+IycvP922qqKenJubnZ+mrbnRXVhs/WxSOy8pIh8eHRwdICcyUMKwqKCdm5ucoKevv+FbTFDp
+0/BPRDkuKigkICAhIigwPmi8q6Sem5yfo6u2xNxXR0tXYVhJQjwwKyonJScpKzFAZMeyqKOh
+oKSpr7nOY0xFQUdV+9nU3d/1RzUtKCIhIyYsOGC+raOfnp+iqbC710o6MCwtMTxhw7i1sa6w
+v1UzJh8eHyIpM0jVtKiioJ+jqa+83EMzKiYlKDFLxLCpop+gpKq7RSwkHhwcHR4lMVC7qJ+d
+nZ2iqrf8NSgiHx8iJy9GxayinJmYmZuhr+UvHxsYFhYXGh4sTrqnnpubm52krss6KCIfHyMq
+NEjMsKaem5mZm56ovkUpHRgVFBQXGyEuVrinnpqZmpygqr5FLCQhISQqM0fQtKignZubm5+q
+wT4mHBYUExQXHCU20q6inZmYmZugrMY+KSAeHh8mLz/Qr6WenJubm56nu0YrHRgVFBMVGh8t
+cbCknJmYmZufq8dBKh8dHh8lL0bPr6Odm5mZmp2lu0ApHRcTEhETGB8sXLGjnJeVlpmdqLxN
+LSAcGx4jLEDXtaeenJuanJ+ovEcrHhgVFBQVGiI00aygm5eVlpqfqsNCKx8bGhwfKTlsvayj
+n52dnp+krco9Kh8aGRkZGyEuU7yqop2ampyhrMZBLiUdGhwfKDZcv66knpycnZ+iqLdjNykf
+GxsaGh4nM1K+rqihnp6jqrXYRTEmIB8kKzdP07qtpqOhoqSlpam16UAxIhwcGxocJCw72rWr
+pJ6doKWpt+1DLyUiJiktOlDbvK6ppqSkpaaprb9MNi8mHh0fHyAoMDdTvK+spqKlqq21xlg9
+NzUyMTY7Pkrfv7i1trKur7e4uL/N7EYzMzo1KyktMDExOFPPyce6srO3t7S4wtV+Wkk+NzY5
+PEVty7+5r6qqq62vuclePjIrJiQjJCgtN0J0xby2sq+urrK8yNxZSUQ9Oj1GS1vbxbuyra2t
+rbC7yvhGMywpJycoLDA5RFzWxL27trS3u8LO4G5PREFDRUtd682/ure0sbGytLvJb0M4ODQt
+LDI2MjdJVlXnxsnLv7/R2thjTVlbSk3+3dvKwb22tLi5t7q/xNBzWko1LS8xLS87Pj9s1Pje
+wsPJwMfh2N9JP0xKSWvn69LBtLO6sq2xsrS6u+BGQTEnJCEiJCQqPVrHr6qmoqOqr7rmQC4l
+JCcpLTt4uKumo6KhpKmzv8PLSzEsLCofGyAoJixD77alpKWhoKm5z0QuLCgfIS07Tc63raSh
+qa+ur7zN7ei/uv4+TlkuHR4jICUsLlyrpqmjnqOvv0UtLSsiICo4Tci4sqiiqLG0ub/BytXG
+s66+eGdbOR4WGyEeIS9qrJ6fo5+erOA6KiYoJyUrSMS2r66sp6q78H3Yz8vTzrGkp75w200g
+ExUaGx8uU62bmZ2en63iNSIeIyknLVm3q6ertK+w1z9CXsu2s7SonqCw9T4tGxAQFhslSLmj
+mJWaoKvdLyUdGyAvQuO0qKKirMR5VkA1NUrBq6Skop2fs0guIxgODxgeLcKonZaWnqzGMSAe
+HB0s9LapoqOmqr86LSwtNEbasaKen6Cip7FTJx8dEw8YISvHpqSdmJ+11zUhIyclMcKrpqCn
+ucdrLiQmLDzMs6uhnJ+mq7XAzD4oJx4REh4pNb2rp5yZp8lkMSUpKilJrqilpbhZTzYkIyw5
+zaunpZ6fqbTC9trLRTNGORoTHSIoa8jCo5qjs786KTU2LD+6rqajukZENScpLjbKqaamoqaw
+u9BZ2b3F1/Y/JBURGCIyebemm5ies04vLC0sLki3p6SszUAzKygrOPuvo6Chpa/D2WNa0r3E
+3dNNHxIRFyE53rmimJierk8sLC4tME+9raiv2zksKzI9TMuvpaCjrshtYvvcxLm1t849IBEO
+FR8zyKygmJWbrGArJCcpLDvWs6enukk2MzdDTWy7q6Wmrsbl1MzKu7nG1Wo8IxQPFSEzxayl
+mpWapswtISYtLDVvu6ijslY+OztFRD5xtKmmqbjIw7/Hyc3xQjczJBcUHCtKtKmonpqeq9Ys
+IykvMD/vwK2rvl1JPT9XXF7LtKuoqrXGztHKzPs/MS4qHRcYIT68p6Sjnp2luUAnIys1PmzE
+tKyvx086NDxMYNzAsqmnrsLh9d/MyvdLPzgtHhgYIDvAqqWinp2kuD8nIyo0PVfLtq2vwFY7
+Nj9PYePEta2qsMpncdO/vMbqW1I4IRgWHC3ysqmknpyerU4pISYvOj9av62pseo8NjpMZlx6
+wa+prLx6W9e/ub3M3+pmOiQaFhssT8CuqJ+bnatxLyYnLjQ3Q9eyqKq+Uz8+RUtAQOq4ra22
+ztzIv77Czut2/0ovIhsaIzRYvKylnpygsHoxJygrLC8/2rSqrbnMXEZEQUFU1b2zr7O8x9Tc
+ycnX5lhEOiwfHB8pO8asp6Gen6m7PygjJSgtPFvBq6asudw/PERMU/LOvrGtts9bTWjFvMPT
+2OpJLh4aGyM+uqqjnp6fpr8yIR0eJjA9/rSln6OwaTc1OUFPVe+9r66210lP0Luysb9dWGsx
+HRgaHza7q6ihoKOntTkjHyEnNkxsvKmlqK7PPz9MTmF9UmTEvcree3DFsKytsspOPSsdGBke
+L8SrpZ+gpKi0SyskIyg1R1bOtq6tsb/6Y25bVk9BQmHX0M7Gv7etqqyxxUg+OyYaGhwiPbqv
+rKaoqamyXTcwKy46OTZLzr6zrrK4t8J0Tj4yMz1K47qyr62sra61ykk6LyUfHRsgMmS5q6mq
+qKituuE1KCgqLDZGasCspaWptN5MPjUyMTRCzrOur7K1tLK4w/JCPTotIx0bHy1KxbStqaOf
+pK/cOCooKSsvOWC6q6eorbjLZUQ4MjE2RerEura0srCvs7fCbEw6KB4bGx8rSMqxp6Oio6i1
+2UAuKCgrLz1dyrWsq6uvu9BcRDs4ODpCc8i6sq+vr66xuM1DLyYeGxweJjjPsKmko6Slq7h7
+NysnKCsvOVTHs6qnqK22xW9IOjAuMz5iyLixrqyrrbPGUzksIh4cHSIuSsGxq6eko6SqulMz
+KicnKiwyTcGvpqKlqq+97EMyKigsOVjEtq+rqaqstM1POiwhHBocIi5KzLispJ6dnqW3WjQq
+JSMiJCxDxK2kn6GkqrbXPi0nJys1Tcy5r6qpqqyzwek+KR4bGhwhLDzftaiempmcpbdPMCcg
+Hh4jLk66qqKfn6Orunk5LCgoLTZH47+2r6uqq620zjomHBkaHCErO9OsnpmXmJ2qxEAsIx4c
+HSMuTrqpop+foqizz0IwLCssMT5VzrmvqqiprbtOKh4bGhseJzNqsqKbmZmep7XsOSsiHh4i
+KzhwuKukn6Ckq7npQTUuLC0xPF+/sq2srK66Zy4gHh0dISgvTLuonpubn6aux0w1KCIgJCoz
+S8+2qaOhpKq1y1w/NC4tMDpPy7mzr6+wu2MvJCAfHyQqL0m9qqCdnaGmrLxtOiokJCgqLjtg
+wK2lpKeqsL3RTzcuLTA8WNS/uLOxsLh0LyYkIiMmKC1Fv6ykoKCjpKm4+jssJicpKSszRsmu
+p6epqq61wF03LzA2P05tz721sK+67zcsKScjJCYsQMmwqqWioaGntuc+LysqKSYnL0rKtK2s
+qqiqsL91RDw5OTg9U9XBuri5vtw9LywmIyUoLT/OuKyloKCiqLbVUDsuKycmKTFCecCyrKim
+qbG901VDPTc0PVXhzca+vML1PDIuKicpKzBA372xqaSio6m2xntBNC0pJyozPEzSua6pp6uv
+tb3SYkM4OD9OVl/22szQTDg4NS0sLS42T9vHtayop6etuL3NUDszLS0xNTdC+Ma5sK+yr7C5
+wtVaRUNER09TXOfX+E1EQjw0Ly4vNj9Kc8W4r6ysr7Gyucn/TUE+OjQyNj9Nd9bKwLmzs7e9
+xtHqYFNPT1Ra9NnkVEVDPDczLy8zO0RT3cO5sq6tr7O5vsfVaEg9OTk8PT5DT/3PxL++vb7A
+xM3oXlteZ2323drhbVZPSUA6NjU2O0FKXdzJwr69vb29wMnY9GZeWVBNUV5w9dzQz87S2d/t
+eGVqYGt14NzX1Nrh3ndbTUtJQ0JCRkpTXF1259vS0Nbf6+zu8/Py7vbi3d7Z19XU2ePn7Pdq
+bPt+5tjZ3eTt73dqYldUWFlYW1tq6uHtbm1mYFtVT09PU1lfY2vk19XV0M3Oz9re6uXp+fz5
+6eHV2N3e4/78X1NST09SUlZu4tzUy8zLzdPiemBSSkRCRkdISlBedePd39jZ39/l6n/q3NfY
+0s7Tztfc1uJ6a1xXW11QYmvu6OHe1NLY1t7u+nNbV1RNS0lPU1JZXFVXYF5fYGR2buff2NDQ
+zM/N1M/T1eXb63xoY/B+6GTi7Nvo59/wb2p1XWBWWFBWY2X86+nraWNdXFBTS1FRVF/yfuLd
+29jc09na2+nnctvz39/Z1ezQ3NLX291p5WBeUVVXU29UcWnvb2/9/Od2cGN7WlxmaWNaaFVj
+Ymdebu5m8H7r79ze69Pj0OLN4NbP5t5r+W5k/VdyYHVvatxY0V3e23PYVX3wXPZ3YvRpb/D+
+WV9PUlhKS1dQVlnzftnVy9XS0NXc197w2uxz7fT94WrXXvnu2XTV4V3Wa99W21L1a2DbR89c
+XNpK8FRRUVtNTGVM+F56fO/y2d7e3O/p5ep0y+L7xtva5NDzzevoadN/9nvtWulRXv1fculH
+0Edf51N5WvRaaXVgTGtWb1nsSdFc99VQznvsxlrJcM/u8shb189c3dxlYdnmbtZv2t9b2/hW
+3HZO2XpP0fpc2lhseU5cTUlRQkxQRf/4Wcvg4sbP0NPa1N9b3t1g29xwbdnjX9rYXN7c/Wz8
+3e711+9t3c55atB8c9/uUUpHQkU8PkVBTFdg8NrHy8nF1M3L4NrtXnXbXWvY4ePa2unPyN7a
+7tjaXu52aGxsa3P319bya35YSzw7RDo6QUJPbM7Nyb/CurzMzu7qUk5NQk9OVWtt08jDxcLA
+xsjT2ONbcmFbbnjc1dTX3uNkTzguNz4wLjpKeNHLvbW2s7C5zt94UEQ7ODs+TGv75sO4uri3
+vb/Ncl1QS01cWG7f0MTJ0dlxQy0qLzUuLDlTzry1rKyurq+55Us+OTMtLjE6VNbAubOurrC3
+xORRRkJCQkNR38K4tbvGz18xIyQuLyosP8y0raimqq6vuOE2Li4sKywyPWC9r6usra2yvdtO
+PTc5P0tZ7si1raywwmRDLB0cIScpMV24q6WfnqWvutM8KiQlJik1VMy5rKWlq6+76kU5MzEy
+PFnPvLayrq2stNc9LCIaGSErL0q4pZ+fn6Ct0ks1KCAhKTE/ya6qqqmqsMhWPjIvMztBUMy3
+sbKztru7v8pPMy4oGxgmPkjrtKaio6Clxzs3MCklKjlRy6ylqrC5x3xANzEvOmDSycC8ubu/
+wdF728jFzu5XRS0cGic5PVu8rKahnqfUOzUuKyotOE63paWstMdWPjkyLTNewLq4t7m+xMxt
+TVnk08S8vsn+OiAWGy03QcSup56cn7w6LysoKi41WLKhoaq13T8zLy4uONOzrq2xvc30Tz06
+U97Pu7Gytr52LBYPHTM8z62lnZibsDQpJSIoLzX1q56fqbdqMi0pJy5JwK2pqa6+11A6ODtS
+z8K1rK2vtNwyHQ8PIUR8s6OdmZqoPyQjIiQvPeyrnp6ouf00KywpMOm5sK2sr7/vRjY5Rk/i
+xLmtqq63yTsfFA8WMcuwo56amaLoJx8hIy1A6q6fn6m7ZDcrKikv6rOvr7C0veZFNzpSX2/I
+uLCsqq/DPCYbEhEffrGknZycn7osHx8hKT/OsKCep7jzOiwoJy7+sq6ysrG5zE86OkhdX9a+
+tKyrr71MKR0WDxlasqmcm52fry8dHiMmOsKwoZykvVU5LCUmLF+vrLGzs7nEaj07S2di+sa3
+rqqutdAtHxoREyu5rJ+anKGqSx4dJCYt3q+lnZ+1XEYwJSIoPLeprbaxr7xnPDpES0hPy7Gt
+rq2uuk4oGxMQH8m3q5uZnaXWJR4kJSU9tKWenq3ZZT4oISYz3LKztqyps9hIP0g/OD/PsK+v
+q6y3dzEiGRIVLb2zp5uanq5DJSUoIydMsaahp7a8wT8nJSw/z8XJsaWouu1eWkQxLUe8tLiw
+qau1TiYcFRIbPde1npibobU/LSkgHivetKqjp6qrxjIpKCkzRmW7pqOrs7vQRjArNV7UybKp
+qq/EPCkaDxQuREKtmZebo8FBOycZHTVtxa6no5+n4TUzMyonMe21rq6rqa7RPDU7PTlIvKyw
+t7nNPh4RFywtKsKdmpyfrLXALhwfLS4z3K+noaa0w9U/KyozO0jww6+prr3DzmtKPUnPwMTM
+yslCHxYgNCQnvqChpKSlp7suIy4xIiZbt66srKutvkw7Pj02O1zRwry7t7vM3tTV29TLzldG
+SiwaHDMsIk6noqSipKSqSicuMiIfMs6/uK6pqbPQY1M/MzVHYGnPubK5xL+8ztnDvs5PR0It
+Hh0lKigvyailpqGepL9BODAlHyU4Zsq4qqWqrrjPVjsyNDtAUs+6tba0t72/wsnXV0A8Khob
+LiYcN7OtqaGdmZ2/Td42HBslKStKvKmgoqikpdE4PzcpKTRE2b21q6qusrS/Xj05Lx8YHSkf
+IlG0rqiem5mfsr7dKxwdIh8iOMmtpaKenKO40GQwIyIqMzxftqemqaent14+LSAeHBsfJy9W
+taihnJueoanBOighHxscJDL7t6ufmpyipq7fMygmKCktPsOuqKeopa9ILysgHR4cIDJDbq+g
+n5+dn6ex6DstIR0eICg3VLqln5+en6i1zEYyLCkqMj9UxLSur7rOTTMpJiUkKC85Wr2uqaSi
+paiuvmo6LSUjJiouSsG2raaio6euu8xVNjE1NDVBWNLBxMbaSTk1LCktLjE+YtC7sK6rqa20
+ucdLODcyLC46SmbRva+srayrrrvNekc8NzY7PDlg5D1xzz5ATTg0UD4x8MZf3ra3vLbAwLzi
+UmpaQEpYTe/X2cW+xcS5vcrK1WdSSz4/QkJGT3rz1tNeYN1TPD0+ODg8QVPfzsW5try9vcHH
+z+f16W1ba3pWXWxga3fv6djS3tjP2u5uVFJeVk9k4ufz5uL/W0pESUdFTF3q3+HWy9Dl5OH+
+W1ZWWXD8dPvl4N7W09HOy8fFxsvW42dWU05LTVZbYejd5fBrXU1ISExWY+jWz8zJy9joc1JP
+T0lIT1ZTZufy7Nrd39jX2dnT1uh1a2RfXXfk3NDPysfO5PZkT0tLS1VYYufW09HW1/FeT01O
+S0pOXFrz5Pb82dva3tb9dHZbTV1WZmvO3vHT/L3Q1MvV2E1ST2RRR+Lr6fjJy83b/d9iXkpF
+WmJCS/bW3+XSz8x3WOZ3UUhU8l9bXPPV5WDlzehbdenrX1VdXFtUX9TVzMjDvsvK6d7kT1FP
+ak9l3l/N0vXZ4NxSVGZJTlRTZFhGTV5KP05VU1heeNzN5N/Dw8vLwMTLx9bZz/pcZuxuW3rs
+08/S0czI2HbebVxQRzovOTwvMDtERlFl1Ly8wbu0t8LJ1v5pSz9FU1Vd38/Fvr69u73JzMne
+X1ZSVFZRUH3a6E01MT88Kys4QkRJYcCvsryxqrC/0/BbSDQvO0Y/QeW9uLe2sa21ydTO7kQ8
+PUdPRkzSv8LMXz06Oi4oKzE2PEnfta2uramqrr16Rz81LCswNz5ixrWtrK6trrvUbE0/OTQ5
+T/Dtzruzsb1aOzIuJyAjLDU9Vr+rpaapqKmy3T0yLywoKDJK3b+xqqaorra+10c4NDU4OkXc
+vbW0r6ywzDwrJiQfHCAvR9+5qqCdn6mut+80KCQkJyozZbutp6SkqK/HWj82Ly82QXHMv7Cq
+q6+zx0cyJB0eHx4mPtaxpqKfnqOxxFYvKCQhJjFE0rOqpaOosLzcRDYwLzZEVdK4r66usrm/
+4jwtKiUfHyQuQ9u7rKOhpqy0yUwxKSgtND9rw6+qrK6yu89MOTQ4Oz1M2L22tbe4t73VXUo/
+Ni8pJCcvN0PuwbKrq6+1vNdNOi8xOkNU1r2zrbG5vcfnTz88QkxOWdrCvLu9wMHRdFxNPzky
+KiUoLzxUz7uuqKmvuclrQzYuLztQ5Ma6tLG0wNpkT0U+PEZu2c7IxL+9vsTO3eruaFlXTDUl
+IiwzN0V6vamlq7G5zV4/LioyR1/Ru7Gsrr7YaVFIPDU7VdzHwr+9ur/fWV94+fhpbtnTazMg
+Ii84PEzqtKOirLfC60w4KSk4Uty+uLKts9NWSj89OzlJz768ur3CxdpSSU1Z59nRvre88i0d
+HystNUjtr56eqK/AVT0uJCUzSc61sa2qsdFVQTg4OTtUxbq2tb3M1WNDQEti3M7CuLe8+jMi
+HiEqNEHXr6KeoavATTctKCcsO9m1rayttMP/RjczOEFkx7q1tLrL900/RFrmz8jCvr/K5zwi
+HCArPHXMt6WeoazLOy8wLSktPsytqaywuc9tSjg0O0Zry8S/ur3N3FxJUnB42cTM3MzJ3T8l
+HCIxStjJu6mfo67NPTE0MSwwQtGxq661vutQSjo1O0f3w72+vL3H12dFRl7s4tvPysLAzUMl
+HSMuQubTvamhpK7NPDMzLy0zQN6zq62xvN5aTTw2OD5jw72+u77JznBEP0pV+M/JysK9wvUx
+ISAqOVLey7Smoqm4WjUyNTEvNUnDr62yu81yb0w5NztNz72+vbq+xtlMPUJWeNbO1MS7xe0+
+KSAnMkL4ybuspqiyzz80NjYyNkPguK+zu8LpV1xHOz1IdMe9wL68xtZcQEBY6NrLx8S9wc5W
+LSAhLDlR4M21qKWsu1c4ODk0MjlNx7OxuLzI5uZbPTxFWc2/w8O/xdH5SkFOcODOxsnGvcNT
+KR4iLD5ZZs+xpaSqvFE8OzgwLzRHx7WytLrDzuZXQDxAUNrGw8HCxs/mUUNGXdvHx8zDu7hg
+IhwgKz9dWOCsn6Cnu0k5OzErLTFLuq2ur7S+xfA+NzY5RXXOv7a3vcbkVExMT3Xd1Mq9trlB
+Hh0lLUBPSM+nn6KpwEhBPi0pKzBWt6+urbG6vd84LzMzPnDbwrOyur3STktLR1V8XefBvbq7
+Oh4hLDFGTULEo6CkqcpCTEUvLC4vVrSyt7W7vrjUNzAyNErf/M64trm9+URZ7WFsaW3LvLq8
+UyUgKjA7Pz9mrqKkqbhzU1M3KyssOsu3t7W1t7S6Vzc1Nz5baO3Ft7W5zU9P+PZubVte1MbG
+ykIlJS81PklF7q6lpqu7bl9kOy0tLjzMu726t7ezuVg3NztFWllT2by3usTvY+LeZU5LTurK
+zMldKyk1NjtHRFqzqKmrtd1k9kAvLi42a8XCurS1s7lzPT4+PUZJTNi6t7vG7GrZ0X1jZmXp
+y8jgOiosMTpERU7PsKmprsDtZ1M7MC4uOnjFvLm3tbS+/kc/P0VQWmnTwby/zOpt7N7md2JW
+ZtbYRCsqLjlXYlPzu6ypqrt0T09GOzUvNFDJvLm5vLq92E8/PD9NY2rnyr+9v8zo79zb5urq
+49RMKygqLj9mXF/Br6qmrcpQSUE7OC4tO968tLS8u7e810k2Mz1SaO/e1MG3u8vsW2Df2Xhe
+WVn06DstLzM//eNq2bqxra7FVE1RTEI3Lzdcyby8xse/vstvSUNPe+Lj7/rYxL6/w8bH1kw0
+KScqMT1IYseyqqirtsr+Tj0zLS00S8y9ubWztrvE90M5Nzg8RFPly7+6u8DL1nRZUUtQY/3e
+09XY31c6Njc4RVtf78q6trO51WJaUktHPzxGauDPy9DOxsPK1eJpXW397fPt28zGx8bfPDMy
+LzU8PUFazLuvrbTAyN9iVUU4MjhFb83DxcW9u77KbUhGS0tIR01d2sW/wsjO1dvwWU1GRk9t
+7fv67m1MSU1KVGpiW1/fy8K/y+fo7PX6b1NIT1tefu3gzsjKzM3a4NjT2+nn39zV7kU2MjA3
+Rk5XbdbEu7W4xNLoaVtUTEZESk9b/ePTzMjIyMzX53thWVFPT09YceLWzMrJy9PoaFRJSExU
+WV9x8t/oYVhbUE1UVVVp3dXLxMfMzdHe+G5fXFxcWmDt3tPP1tvX0M3MztTY22hDNC8uMz1M
+WurMv7ezs7vL5F1PTEU+PkJNddbP0s7LycXHz9zwalpWWFdUVFtr3M3JycrP2u1fT0lGSUlH
+QUFJS1L53dvPzMrDv8TP3PllbWdVSkxVXenQz9HMzc7P1Nnl/nFt+9/c5VxCOTU1OT5ETFzZ
+wbavsLfB0OpeTUU9OTxEVf7YzcfDw8bN3fxoXVlWV11jae3c19DNzMvO2uXueV9ENzIvMTpH
+U+zKvLawrrK7xd9SRT04NTY6QlzRxb+8u7u8vsXVe1pMSktTbeLaZkk/Ojk+Q0dPW3DUvra2
+uL3N3/BeTkc/PD5JXffVzcvKxsLFzdxyU05OTVBXaeHRysbHyMzV1eFJODIuLjU8QlLix7iu
+rK2vt8XfWz83Mi8vND5X17+5uLa2uLvG21xKQ0JFTW/SycreUkI8Ojs/QUhW9s28trS3vMvx
+WktFQD09PT9IZNPHxMfHxcTAxdT5UklGS1x5283LyMG/vr3EYDovKyotNDpEWd6+r6qprLPE
+7E8+NzQwLi84SNm8sq+vs7i5vcnwTkA8PkRT78zBwtVQPjg2Njo+PkFU3MC1r7G3vs19WUtA
+PDo3Okd+zMK/vr+9u77I21lHQUBGWuLOx8TFxcC9vsxFLysnJy42PlfeyLarpqeqtd9IOjEv
+Ly4uM0L6vK2qq662vsjZaU8/OTc9Tt6/ubi7ylI3Ly0sMDY4QGLKtqypqq63ymJHOjQzMC82
+RG7Huba1tbm+w91RRj89Pklj08K+vru7ubq/2TsqJSAiLDhF4sO7saeipKm3VzYuKysuLzA7
+Xcewp6aqr7/ZdVRHPjg0N0J3yLexs7S+/j0uKCUmKjA/77+wqaamqK+/ZD0wLCwtLjdRyriv
+ra+0usjiT0E8ODY8S3XIuri5uLu9v8zpTS8kISAjMEvgv7Stp6GgqLZ1LyknKC00OkXiu62m
+pau2zU4/Pzw9PDs+UNO9sq+ytr3JZTkrIh8iKTn2v7KrqKWlqrd9NiknKCw0P1/Jt62rrbC+
+6E5BOzs8Pkhpyru0sra6vsbQ5F5HNCcgICQuT8q7s6+rpqSquVUuJycrMj5Nasq0rKiosMZj
+Pjk9Pj5ASVjZv7q3t7q+v83qWj0sIx8iKjzZvbOtq6inq7lnNSspLDA7S+/BsqytrrrbVklA
+PT4+QVTVv7i3vsPGyszbXEtEOzApJSctQ8+7sa6tq6msu1ozKiktOERU9ca0rq2vu+VRRT49
+QEZR+NLIv7+/v76/xthbSUU6LSUiJi9Oybmxrqyqq7PMQzAsLTI7Sn7Dta+vs7vI6k9BOjg+
+TuvMxMHCvrm7xd1SSEpLPi8lIic1c8G8uLCqp6ey7DkwLy8yNTtevrGutby+wcn3Qzc4RXbV
+1dzYx7u3u8vvaGV+X0IzJyMnLjxnzb2vqKapr8ZePjYvLjI8XMe7ury7usDNbUQ+QEld6eLd
+yL69vL/N09f1ZVRANSwnKS89Ws28s62sr7jIXj83MzI4Q2bNwsC+ubm+zGpKR0pLU3ngzcTE
+x8XBw8fQeVtbRDYsJyovOUn6w7WurK+1vs1uQzMvMjlJ78/Mv7a4vcTadWNTRkZTYuPMy8a9
+u7y+zd3nVj4xKigtMTY+TN67r66vs7e/zlw7NDc7P01efcy+u7m4u8LN61JMT01OTk9acOjh
+0srR3NnkbVxSSkA9P0NFSFX/28rCxMvS3N/qaFhYbubp+Xt33s7R619ebufc3uLd1c/R297d
+3eLo6uPkaExDPj0+Pz1AS17v3NrXz9Ha5O7r39fY3efe1NHZ7mpfa+3p39XPzMnLzc/S1+V6
+aF1aVUpCPjs6Oz1BTFr92MzIxcbO2+j9c3ZybXrt5eHe3+bl5ODm6ufe08/OzdDV0s/T3OX1
+Xk5JQTw7Ozo7PkhY+NLKxcLHzdXld2xsa29/8unj29bU19zf3t/g3Nzb3NfW2+Hk5ODd6mZV
+S0U/PDk5PUFMXuvSyMPDwsbL1/JsXVhZXF1meubj3NfZ3NjV1dbZ2Nvm/Pfq2tnZ3ex2YltP
+Rj8+PT0/RUxZ79bMyMjM1Nrc4+zp7erl3Nzf4ejg5PN3amhy7+Xo7uTd29jT0NPX2+hfTUhD
+Pz9AQUVMV2jx39HNzc/X5Ov07fN5dvfh4N/d3eLi4vJ5dvjo5O5+7d7Uz8/Q0tbc3/FcTUhD
+Pz9BREZMWWru3dze3dvc3ub19Ofc3+fh3tjZ3uxua294+XVudvzm3Nva2dnY2tnd8m5gWlFO
+T01MT1JQWGNod+fd7vfq7uzr6e72/Ozm7vPu+2hgam5scPjo3tbT1tPPz83O1t/yb2trYFJL
+R0lLTlFOTFNdbtzT0tXY3NzW2+Li7GpdXV1kbG5rbPzr4eTp4N7d3ODv5+Pt9Pnt5uTi7mha
+VFBMTVBSU1laXnvh1M/O0tfY3ePse29kW1lfaWpfYWpy7N/e4tvW3OHj6/v67Pjt5+bpdlxX
+WVNUVVFRUFdebOrZ2NfR0tfc5u3v9/5pXlxfZG14dvz8/Pb17+ni4OTn5+zs6Ojk4OPm729h
+XFhQTU5OU1xiZ2t3/e3c29vZ2+fw7PV6eXxtbPfx9O/p4N3b3NfV2eDu8vTu+GZYT1BOTU1L
+S1Jeav35/Ojc1NPS0NDS1t3o+21ma2ldXV5dXV5eYmZ+6N7a3N7c2tXS0tnd3+5+d2tcV1RO
+S0tNTlVbW2N1+uvf4N3c3N3f4e39e2xt9+3s5ePm5ubf3trW19jc3NjZ3+9uYldOS0ZER0xO
+VV9sdX379O3m3NzZ1dXY2N/ze3j7fGppamJhY29xfezs6N/b1tDS19fa4OXp9W9jWE9MS0tK
+SkxPV2Bv+/Dm3NvZ1tfW2t7j6/n3dmdia/r17Ovr5N3c2uDn3drd6fHq6ObsallTTkxKSUpN
+T1dYXXrq6Ovr7u/p8Orj6Ojm7e7r93x28+Hf3NfW1dLR1NTW3eHq7vDz/HhvbWxeWFFNSEZG
+RkpPVVtke+Xb2NjX3N/i3+Hl4+bu+PXt9vZ+dn5weO3o497c293f3Nrc2tjb3uLp/mRaUE1M
+TEtJSUxVY37m3NXY29zg5enxdmlganJze3z05+nr7Orj4ujq7PPr5unt7+7+9/Ht6PD0dVxR
+S0dFREZNWGnp1szKx8fKz9nc8W5fWFZXWlteYXL35trW1dPT193j6+v4+fn98eXd3d/j8V1L
+Qj05ODk8QlfjzMG9vL3AyNDialxWTk5OT1decuLSy8XBv8DHzNbncGNeZ2x56eDe3uL5W0U6
+NC8vMTdEbMq8trS1ub7J3mdVTEVAPTw9RVXfxbq1tLe9x9j+XVVVV19+59zTzcvO3Vo/My0q
+Ky86Vse4sK6vs7rD0P5WRz03MzI3P1/Gt6+usbe+zvVaTk1OUllk8dbJwb2/ym88LCUiIys4
+a72xrq2vsrS4vtZOOC4rLDA+472xra+zuL/Kz+FhTUA9QFDewri2t7zNWj0uJiQjJi9G0rau
+rKurrrXCez0wKyotN0vOubOwsra4vcjeWkY+PUJP3MO9u7q9vcLjSTQnISEkLkzHt7Cvrq2t
+sL1vOSwpKzJI4Me8ureysbW+3UxBQEZWdeTazcK9t7a8xWw7LikiIykwS8u8tK6tra+55EE0
+LS00PVHaxbqzsLG2wNhfUlBVXVla786/t7e+ydh0Xks5LSYhJi9Ezby5trSwsLjNRzYxMz1M
+W+nNw7q2ucHR/WT08mRYTlH7zMXDwMLJys3f9ls/NS0mJy896ry6uLW0s7jGTzgyMjhGUWPS
+x765u7/Hztt6Wk9MU2Xiz83Nx8HAwsfS9VZDNy0lJCw5XsO+u7SvrbDBUjg1OD1FR0r7xry3
+uL7Jzdl/W01KU3bk2tnXyr25u8TZb2dYQzUqIiQuP/vGwbyxra+41kU7PT8+Pz1G3L65ub3G
+y8fObVFKTu3W29vczr+4uL/P53NuVj4wKCMnMD5b0cW5rqyuu+BNQ0M+Ojk6Stm/u7y+wcDF
+1GFLS1Jl6OXn0MO9ur3Hztze22hENSwoKC47S+DGvLWytbrD5VNHPjo6PUhezr+9vcHKxsni
+XU5LVXTr49vPyMPDyM/U2+fk7VtCNSwqLz5Pe93hybeztbrLbFdRSEI9PUdc2MfIy8nKyczt
+XF1h/uN6XGndzMXEy9XU1tzo93hONSosMTlNaVBfxrqzrrfK1+JsXE0+O0FPd9fY28/IwMDL
+53JoXmJmYFx83dnPzMzKyMzV7Uw4MDAyNTxGTWbOvbi0t73G0+x4XUk/PkFIUmjs2c/KxMTG
+ytLnal9eX15jafncz87O0OH78m9URT49RE1dcmZdauvi2+ZjXvDQyszhZlxmd3RzYVlabu7f
+3eHZ1dDQz9Pc29TR1d12Y3P0c1tNSU1UXF9aT0tLTE5TXGd83crEw8jS53ZsZV9TTU1Xa/Dd
+4Ovo3tjW19rd3NXR09noeXRvZmBkbH77dXJoXVpTSklLT1dg8uPf3ebw//fr7H5fYHvo5/tp
+X2F44tva3NXOy8vM1OpqY2x5b2BbVlhebWxaUlFSVFpsb2166ujf09Xb6XlpbHBnZ2NjaHF+
+fPbi2dja2NTR1NrpeGhmcndwcXBteOzk7mlWTUpMUFlcW19fdN/W0NXj/HPt2tbY4/5jZ3V5
+e3ru5d7b2NbZ2dztcHBwb29iV1FMSk9eZ2ReWVhw2s/LzdXc4OLo5uP2al5TUVdcXFtkcffm
+2tXPy87T2t/i7PZ4Z15fbXNwee/t93JcT0pGRktVaXBueO/c0cvM0dnoe2ZfYmNlbHr26trT
+0c/Q09TZ3ut5bWFbV1JPS0hJTE9YZWpveeve2tHQ0dXc6PF8cm1hW1dbY2JeZnfu4d/f4t7e
+3Nze3drY293b3+jzfW5mX1BIRENHTVBQV2V57+fe29TS1tna3unp+GpraGhscW7y39rT0c/P
+0tjd4/F+b1tPTUpHSUtNVWP95uTv6t3b2dfV3/J7cPr3+P5zZGRvcG1vdezh5eLf3tjZ3d7l
+4+Hi4u3n625eVE5OTEpNTE5UX2Vq9eDb29rW2dfV2+HofV9dW1xp+uvq5dzW09DP0dLU2uLq
+7vTzdmlaTUpHRkZITVhlbPTn4N7f4uHf4Obp5vNzZl1fYmZrfe3g2dzf3NbS0NDW2tbU2N3e
+3+Po5nZYTUdCQUNHSk5YXWb34+Lm4NvY2tzmc2ljXlVRW2hy6d7c1M/Oz8/Nzs/T197d2+Ds
+6uLnZU9HPzw9P0NJV2/15NjU0s/OzczO0N1xW1JNTE9fe+XTzMzNztTb3N3kfmxiWlpidO7h
+4uh8V0g/Ozk7P0pd5M/JxMPEw8LFxsrXeFBEPz9FTV/q2M3IxsfKzM3P2ul0XVRSV2Tu1c3N
+0eNbQzkyLzM6Q1rdysLAv769vL3D1FpDOzg4PEVa5c3FxcW/vb7ByNLrYlZUXf/Yy8fIyszP
+8kw8LyspKzND8ce8vL29u7m7wttQPTUzN0FX2cfBvry6ubq9xdleS0dLU3jXzsvGxsXByPZA
+LSQiJSw9bsi/v7y2r62uts1CMi8yPExe9eTUv7ewr7bG/kpITFRhWlFa8ci6tri+1FM9MCom
+JikvQuO+tbCurq60v99INzExNTtHZdTHvbe2uL/N5GlfWVFSV2HhzMW+vsDFz/lKNyslJSkz
+SdXGv7myrq2yw10/ODY5PT1DWNW8srK4wdLh4+9hUE1NWODLyMbFx8jHz1k8LyglKTA6TPHO
+vLGtrrK92FlJPTk5OTxP18C4trm9v8bX+1pPV3nr5dXMx8G9vsPSVjwxKiYoLTRBX9a/ta6u
+srrLZUw/ODc4O07Xw7y3tbW3vM1tXFpcamFf6c3Cv7/AxtBpPzMtKCguMj1a27+zrq2vucbx
+T0A3Njg6SH7Uw7y6uLq9xNji9lZRU2jXy8bCw8LAynFBNS0nKS4vOk//xbWvrrC1vdZeRTo5
+OjpFXObJvry5ubzDzdLca1lUTlnr1snDxcXNdEo7LyoqLjA0PlHWu7Kvr7K5ym5NQDs3OD1I
+ZNXHvrm5uby/xMrW625oX2z458/IzN5eSj82Li4wLzE5Ql3Mvbi2tba8yNlfSUJDQkRTedrI
+vry7vL7Dy9HZ+FxQUltj89fQ5GZgTTsyNjgyMjpATuLHvru2t73Byd9dUkpDRk5Zat7MxL+9
+vL6/xczR2e5sXVtiaGhub15MQTw5NzQ1NzpATvrPxby6vL/Cxc/e8mZcXF5o6NfNysnIys7X
+3OxoZ19ZWV1iYGZwd2lnZFFHR01MQkVPTUxe18/e2M/a39nb8vvh7mZ329XX0M7R0s/R3uHe
+4+3o7PxzdmdcXFZVVE9NU1lWVFNST09ZXFVbbGxhbezm3tnR0M3HxsjNz9TX2eLr5+R/Z2x8
+7+3rem5lY2RcV1daW1hdYGttZWlpYm15a2hiYGlpY2Vx7u7q4tjR1dvk8vDm3+fh4d7Z293d
+4+rt7flta21gW15wcWdoamFpb2tqbvxyYl1gYmvx8PXt4ePn5/NkW1xqaGZyevbr393b2N3p
++Pd/bm1y/vLn4d/k4uPr/mtqZV9fX1lcZmz+5N/l6u/4+O/0/m1paWlubWZt9+rr+m1qbmdg
+Ymh1+O/x/e/i3Nvc3uTm+HR0bWlqbG7v5unq7/Dw8fZ1Z2BfaWpse3xudmppevj3cHFvanT8
+7fHs6+zv7+vt9/j6e3X+9nhma31tcXj8/fv6+n357e/v+/fw8OXk6fr77/txYWRlX2FncPfu
+6+/t5+DqeHFramxtd3j99nloZWt3eXVxde3n4fB1TUC7t2FESsbHY0rvwPhXac/+S19xeVtp
+2G9ZaPnpYVZd2n523eRnXePd91pmcFRK2M/W4tzN2N5q1/BiW09MSFDcWk3fztnfy8nS9VDb
+z/5QTvBgTVb2/Utb1uzg2uPx4Hh92HhbWFBSVV5fYtzQy9fb5Mzc/Ox9eGdqU09cY0xc6uXy
+7tjT2ubv693tXlllXl9fYPvtam/r1dpx7+14ZGJv+GNXYvNsaG3v6Ovt5+Pm5ut4aGdfevFp
+/vD06uvi5OV8bPf1ZF55fm5ic9zbdWvo1Nd+YX3pdFtt6m1uYFxeb3JbV15lXF92bF999t/W
+3d7d3Nze3ubf29na4Ofl2NnYzcjR4trb719sXkc7MTA4Ozs9SWNr+t/Kv7/Cxr++xtbr39rk
+4NfIxMbLzsfEyNTd4+TuWE5UZFUwIyk1MzI3QmjLyNzItbfI2dPKxNBZVM/CzNbJvr2/zObS
+yeBab9vf4u936c/O1tZMLScrLi0vNTxWzsO/t7CzusXlZGZgTkpSYNnLysm/ubzBx8zQ0+Fc
+UWT06d/e2dnhe0U0MS8vNDc4PEd5y8G8u7q5vMDLeE1NWVZTWG3lz83Z2tTPzs/T3Nrd5dzg
+4uPl4nBZZ21SRkNHSkhGREBGUVdeaH3v4NbV0c3O0c/Q197n49zkbmP43OTs3tnRz9fp6uT6
+dHB18uHZ7lVIRktKREFCRkxRWGTh0s7PzsrN1uRtXGhuWlhp7t7a2d3e1NPY2uDc2t3U3NrO
+zcvK1Oz9Tzo1NTI0Oz1BWeHQv7i5ubvC1HpOQD4+PUVbe9rKw8G/xM3X7GNdaWzz1sq/urm9
+vcVbPTAkISYqNFPZxbWtrKywv3xMPzg4OjxL58u9uby/x9T5V01KT27k18XAwLy9xMfK23NO
+OCwkHyUxU7+2tbSwr7PBWTo0OT9JVnPWw7y8v8z0Z19YUlBd3cnBv7/GyczNycvo99r0Wz8t
+Ih8mMuy3srS2t7m93D8xLzhM283QyL66u8jzUE1WcO707NTEv7/H3v7s3d/d29rHvL3PSy4f
+HSIv+bWwtbi+x812PjQ0OlDIu7/DxcW+xf9JP0Jox7/Dx87Px8vrWEpFVdXGvrq/w7/aRi0d
+GR4uy6ijrb/V39HMVjcvMkq8rrC/bkthyshcOjM57bavtshhWt7X5VpHRvu9trW8zt7KwGsv
+HBYaLLufnqvLS1TIuss8Kig2w6urvUU1Qcu0t+80Lj3Jrau5eT88UdLL6lBO47qvsLvTYuXa
+QCkcGB07rp6dqtlCR9u+x0IrJy5psauwyVBGa8bDdDswMlC6rKy4dT9AW8q9wuFf88m1r7XF
+Uy0hHR0kPMauqKu2v8nU2XtALywuPdC1r7G6ydriY0U7ODpVw7WwuNFXVGXZyc7taF9u0sK7
+trxYLR8aHSlurKSos83nyri1zjgmIihCu6mpr77Z59veUjszMz/QtK2uu+ZKP0Vb39Ta9WD0
+w7Wvs/IrHBgbKG+rpKevwsW5sLHGOycfJDHkr6mqsMX7U0xOVlBMUmjZw7y+y2tFQU7avri5
+v87e1M1mNyUdGyAzxqukpq66w7+5vNg9KyQlLle6rauvvdJkWW3d2eVqTkdLV3TazszLycPA
+wL/Hz9rpVDssIx8iLEm+rqyutLi3tbW+bDYqJicuQtm7tLW4vMDDxcXNdk9BPD5GVe3Qx8G9
+uri3ub/L3l0+LSMeHSEuVLyurK2urKurrsFDLCMhJS5F1ry3uLe1s6+vt8pRPDQzNz5KX+HN
+v7iyr6+1vstpOiogHR0hLUnGtK6tqqakpKq8SS0lIiUtN0jxzcS5r6yrrbXHX0M6Nzg5O0JP
+7MO1rq+yucTKytdHLSEdHSMzZsK7urivpqCgp709KiUnLDI4ODtMz7KopqmvvtPj+VpCNS4u
+N1DKuLS3u7u2sLLHPicdHSAqOEZIU82voZyboLJoOTIyMCslIyg407Cqq66wr66vufY5Li0x
+Pk9YWGrPvLKusrm+y04wKCIhJy43PkvZuKienaGrvW9EOzEpJCMoMU3NvbeuqaemrLnXUj86
+ODY1OUrfxLm2tbO3xHw7KyYlJyksMTtcva2mo6KlqrPITDUtKigpLDI++r2xrKmoqq63zks8
+NzY3Oj1FV9LBu7OwtcdMNCwsLSwrLTNBdMKyq6aipay4xnU/MiomKC00PUnqu62qqamssr7m
+Sj06Ozk7Rl7cxr65ucHVRjEvMC4tLjY+Tte/tKuoqq6xt8tZOy4tLi8vMDxfyrqyrqysrrnB
+zWxIPz5AR0tSfs7DwsnMXTcxNDIvMDg7QWnOv7StrK6usr3ZUzw1NTMuLztEU8q3srGurrK0
+uc9cTkc9PktKSWba2NPOVzI0PjQvND0+Rt3JvrKwtbKtssXWe0Q7OjYwNz87RNXBwbqvsLSz
+uszR401EU1FET298dXFmSDo5OzgzNDo/TG/Pvbe0tbGus7zK7VZGPDY1OTo8S2vcy765t7S2
+u77F2GheWkxLT1lhXFJUTj04Ozs2NTxDTHLUwrq2trSxtb7K2n1OQTs7PT4/Rlfv59fHvru7
+vL/Aw87d7WlUVl5dW1NTT0A5Oz05ODw/RE9u2si+u7q1tbrAxMznZk9JSEdEQkpVVFz518nB
+wMHBwcXN0t1iWmFeWFFNTU1FP0NFPz5ARElTZuvQxcLGw7/Bx8rQ4+1+XVJWW1VTXmf93tPO
+ysbIysrN2O1fUVFRU1JVYmZdWldRUFVMRkdLTU5cXmHi0tbXy8jO0NHPz9Ph8eTg6Xz88Pfx
+7eXk2N/v7PR2W19nX1pYbfPs8unk6/ZwX1VNSklLS0xPVV9ha/Lh18/Pz83Ky9Db2N3o9W36
+8+vs8Off3t/p8np1XV1taF9kevH69e/q6u9uY2ZfWlFNTEtMTk9XXGZz8d7Wz9HOz9jb3tbc
+4t7b3Nzc393qc3piX2RkXF1kZm557OPg3d/f63h4cHpfWFpcVVJRV1VVXGF17Od++ujp7Ovq
+5N/i7evh5Ovl5uPz/ebt9PX/fe3f397s4N/n5nro/G5xXWpiYVtZXltgWl1jV1xiX15n/Gt6
+cGl2du537vfq4+bs+OPk4d/V19Tc4OTl3tbY3uJ88X56b2V5am1vY2FeX1pdX11eXl1cW2Ff
+YWdy/HF8bGh2+3lpa3b86eTg3t3f39rd3NjU197b3+Xe3Oz5+mxdYVxYW11gX2d2cHl5dnj5
+fWZoaWtqaGZqaWloaXH++vn+e3bu6OPf4+De5/fx4+Xv8n568nxnaGhma/fv7uzq7fZ+fPTx
+8vH7//n4/HFlZ3FhXmZkZWViZW91/Ht4eH9//e9zbHZ2e/PxfvTt7PPv39/k6Ovt5eTv6u3u
+7PlvaGprbXdqaGpta2RkZGhrb2doam1ubG9vbnt5dvnt4uPo7vD27+vq6e7q6n356unm7X3+
+eHBuZmpvdHRuaWdqaGRq+ezp8/9zdvHp8HpzfHd19/r0+3f5dW98dm1oa3b/fvf1/O3u/u7l
+6vP27/L58OnteXR+e3v7eGpvdHpvZnFua2tz+e3s7/t9+n1+/PH7fXJqYWJxeHt2/vzu5ubx
+fHr+/fz19vvy8vz97ebs7ev5/3Zta2l+8fX9emxqbGpqdPrw73lnb25qbG97en15/evo8P7y
+7vR+/Hhyc37y7uzu+Hd3/PTy7+/4cG94/e5+/vn8dGtsd31ub3Bpanz5eGxz9/J7dX1tcX7i
+5vvs7u7+fOzu+3Bydnl5f3plZnN5dG568u7v6err5ul5amZu//B4b3VrY23193v28vpvbvPz
+/Xv7aOPjeef35Ofs9fZ4cGZmcWpw9PlrYnrsbG549H9pX+nedHV9cHr09nfi33H88+Xw/vN9
+amxran/wem1+9XN77/j7/fb08uvyee9/aGr7fGZs+e306OT3cuz5cd/X1c/W2Nvf33hYVVFH
+Q0VDP0VJSU9i/t3OzMrEwsXIyM7Z2t18a/15ZGr78eLa2c/Oz8jIzt1aSUk+NTc6NjQ6QUNM
+b9rIw8K7t7q8urzDytPqYU5HR0M+QElKSFBw6NvNxb/Bv7q8vr6+wctlR0RCMy41MCwvNTxD
+TurHv7+6r7K5uLW7yc/c/0s+RUQ6OkVIRU/12tHMxL2+v727vcK/wsrfXko+PjItNTAtMTc7
+QFrvyr2+t7G0uLe1vMfP4HNMQkRDOzlDR0FKa+Dez8K+vb67usLGvr/O4eNZPDw+Ly8zLi8z
+Nz1QbO3FvLq0sLK2tbe8wtXnfEs+Pj47OD5CP0li4tbMwr26u726vMXExtb7bFtFOj05LzMz
+MjU4PElv69G8ubu3tLa6urzDz3xvWEM+QUM8QE1PVWrg0cvGwb2/w7/Cx83P09t5YVQ+Oj82
+MDQzMzY8Q1J6+su9vry2tLm7ur/Hz/RzWkdKS0FCS1NVXujXzczKxcTGycbJ1trZ6GldWkc8
+PTw2Nzs5OT5ETGrs2sS9vry4ur++v8/e5GhZUUlKT1NUcN3z4cvGysnGydDb3+NzW1hYTk5X
+VU5ESUk+QEVHRUpSVWrs4c7Dx8zDwcrKys3Z7u92Z2No+HRu69PV3dPS3ufwb1dSUExMT1pW
+bejt79jU3N7R9Gj8YVBPVEpJTktPWFpcf+L82s/a28/P29Da2NPf3urw3e7v82R88V1e+H5j
+cebo9/Xf3tzm7d3dbGJyX1VWW1hPTk9TU1RaYfxcYOxv4dzc5PXg3tvr59jj3/7mc+vj/drm
+3N7k4nLu3Pjd6+PifnJlW2VVVlxXWVpWWGdgbHfq92rt8vrq+ezh8ev8/m366On4a2xaaHtr
+cft6cPju9+HZ4uDV3Nzk6uPv9npkcXhaXl9dbG5z6vJo7XBt8WVseGpeWmf4+Gl6d2326P9t
+8fhw4PZp7Oju6OTs7t/d6Pvr4uTifvJxZnxjbWheY2luX2l1dP3u5+/w73ZjbmlubF5tcWpx
+ffjr4uTt6+bg9P7l7G1h6XBqc/fm7v9q6/Rmb2x3Z/h5aOvo3eTo63z47vPwbWl5629hcGJf
+X2BkX/5uYml1/fnx8eff5d/w7nf95ebs8+Bvb+Xi8evz9ex7dX5cZmTu6mlpaub+W2T5a3Nv
+bm5+aWX7fnp649r6bWrt8fTpfOt8fndrfmb+Z3v0+/Htf25zet/p6efs7uPuceb0dnn0b3V3
+avJf63Rqc/17aGhlVFlgbGL+0NTT7PPob/3s++5lZGx+bP/3e+7r3/T4b3Rf8nXma/tuXPNw
+9XXwcWjoenteYv5m7PHw2vTl8Ozh4fF/3mlnemHudfRvXehqdXnl8OP0ZPFkZ1NiW1tmYXlu
+2GXa9ePZ3dRq6XpvemTceedh9+li+nrz8nDdZPnuYvFk31t+bFZ3duJd5PpmznFrc3Z1WeBe
+6OJv7vfP7n7Zatl8fWll6E/r92tnWvpl3vdf6F9rWm3tX9327d/y6n/b7vbq8e/sfGNu/2Rn
+Z+vq+3Zg7vB+9e/0bvT1e+557ex3anBrZFxcVVlbWlxnbvvq3+zn3d7c3tnW1dba1dfi3tDY
+4+bm39neeFVWRDs+Ozk8P0NK+93Pwr+9vLvDy9HqUkQ/P0RJSVH9z8vHv768wMbJz9Lc5+rz
+3+DfZFFHOTQzMjI1OT9Od82/urS0tbi8xdlrTT87Ojs+RU9r0cO/v7y7vcDFz9/tb2FZXn7l
+2+BqXEg5NTY3Nzg8Q07zy8S9uri4usHK1PpYS0VDREdKU1v51cjCwMLGx8rQ435sW1pZYmtn
+fuvr7H5gU01KSEdGQ0RITlpdaOnZ0MrLzc7V2eP1en3z/Ord3dvc2Nnc2dzmd29mXl9cYmz+
+9nd4+97i+XZ07PZ8Z1xbVVdWWVlWWVpeX2x6dH9kYPzc6/Xe3N7g4dvY3t7W1dvc3N3rZ/vn
+8HNgX2hrXGfn7/5tfu15anPm921jZ3RpYF1qaFxcV19oXF1ne/jy8t/a7uzf3tnl7d7f9XL7
+7fBsbXt1d/vh2trf4t/k9HRxeW5nX1lbWlRVXXLy/vrh73v+9et0X11ncl5favTt79/d3/Hz
+6t/jff3p5PxvfO3j6vPu7+zr7PXxeWNsaWNdW2lwbGFre3T/d3p9cGxscPH5b/nf2+Le3Od0
+bn1+cWhhaG9pX2n38XJse/Tn6OXe4+np6/x4cXT18fR7dPXzc2z57vn+8+37cGzx7nJgXWtn
+VVRfY2dlfefj93vl3uDo5uLl+H3s6u758uPl8u3g4+v/d/lxYGFoa2VkYXJ9/Pf9+nt5fnlt
+aGpub2588+vm5Ov87+/0/m9z+fl2e/x+fnJ+8vZ+ef79bGn67fl49uvt3+Du6unl9f5va3Fi
+ZXD/d3v8bWb35+3+9PrsemZqbmlfefpvdPn07GZtcGp3aHR1b/nk7m7x5fzv/fLi4+Tg9vPi
+7PRuae99bHv65P14/nt29utgXmVoe2dt83Zt+nhvafbs5nJhdWF45HFz7uzm63/83XVt3fll
+Z2vv7OLi+3ju6fl89nnw/G9sZF9rbX3xam11bW7z9+ft7/X+dV9j++zm8W5t7N3ubnX1+H32
+7XRp8OjyeHlnXmju8WppbPvve2tqc/nf6vfj6vH+ZFpi/uXb3evq6ebf5u55bXL56+5rXl1c
+ZF1QTEpQXF9jc+3h2NfX3uXb1tzm7e/n3+Pf29nU19XOys7V2Nzd6fP+aUIqICMuRmby1Ma9
+tK6tt9JiXlhFOThAVtjBubi9wL6/z2FOTlNRV2b72srAvsLP2M7XRCsfHSAsRNLBvLKrp6mx
+wXBGPjo3NTQ6Wca5tbe5u8LSZElES3PW1tvRwru5u77Hzu5VST8yJx8eJTbhvbexr66sr71s
+Pjg3NjY5Sdm+ubm+xcXL2mNNTl/czs/X2M/JxsjP2NHMzczUZkg7LiMeIC9mwbawsK+ur7h9
+NS0uMztCZsG0r6+6zeJzY1JNUlnz0MnR6+nVz87O0M7Hwr7GdkM0KB0aIjvJs6ypqKiprtUw
+JSUsOlrLvbaurLPMSzcwNkNf3869uL3L+l1i8dvg8tzJw7+6ubvOOyMYFiBAu62rqaimpbI+
+JR8lM07SzMayqaq4ZjsyMzg6SM61ra64zVtLTEc/QmDKvru8vbq0ssguGA8WLb2oqK2spZ+m
+VCMcHy9vx9bcs6OirmIvKS4/SkBNxK6qrsVNQk5TS0hU2cC6usLPxb7BxkciGBgoerassbiv
+qazRMCYmNPbJ19+9rauy4zQtNk/vaVRpvq+vvPpLSElIPDtPyrq6v7+8uLnMakIjFhoqXK6m
+qrSvqK/vMiMiMOHCy8W2rau0SCkoMUrZzNTGs62vxkw2LzY8P23CuLOztbS0vlY2OksvHx0l
+S6+jq7++s7PNMyIiNse5v8fAtK6+OicqQc++v8vKvba/TTMvOlLl1c+/tbCzvMLMb1BHRtNh
+IRkkRLmnr+jYr63KOSYjPL2+2ubMt627Oyw2Ws7G0tvFvstpQTtL++jj1MvAur3Mz8W/yNnd
+5es9IBkfOtGytcK1qavIOSoqPP5hTPm5ra29SjpFXFtPWdu/vMTM2erfdklBTmXqysPDv7y9
+xM/oVVtBHxgiPMuvtsm1p6vMQS8tQFU/Qte4r66/TEp3ZEM/S9W6vc3IxM3eSDU3RVjovra4
+trS7xvA+PT8pHh4oVryxuLitrLTVPTI4QkFFYc25srrVW1hoX0lDWdHOz8nKys1vRD4/SffI
+vLm5ury+zFxOTTAjHyEx3r64sq2ssLxQODk4NThI3r+1t77H5F9tTj9FTm/NwMHKytV+VkE9
+S9nDvr29uLK4x87uMCMcGypH1L6wqKeptl48NS8uMTpkvLOztLnN+lpFQEA9Ss26uLnA1N/l
+Sjg7T9fCwL+4srO/zeo0JRwZJThTxbKnoaKsylc6LCotMELPvLKsrr3VYEg/OjY7Y7+6u7zA
+xc5aQkNR89TOx7y0sLG/Si4iHB4kLEbEraahpKu02zcoJyktO1TNsqmpr7vI7kg3MzdG6s/G
+ura6xupnaVxYW2vez8O8vMhNMCMiKCkvQcuwqKepqa/ASjArKy8zOE/Crqywt7u90FE9PENK
+TlHiysjO28/BvMPb5enl3GpEPTkrKjA3PmTLuayrsba4xlY8Mi83OztG+MO4uL7CwcjS5mNY
+VFNWW2l+anvezL67vMTBvsvoRDMoIykqLjlPxK2mqamqscNjOy8vLi0xPWbIvbq0tLnBytXd
+7V5NTFZjYFRU+8y9ubm7vLvJTzkvJyQlJSo2SNeyqKakpau1yUc1MC0rLTZEZs2+uLOytbi9
+xNLtXk9HRENCREhd0L66t7S1vNtFLispJiYpLTrnva+opaWnq7fLWzkuLCstMjlO3MrCuLSx
+sri9wcPSelZMSEI/PkhbdN/Tz9DsWVFGOzUzMTU5PlfPvrevra6vtsHdVkVCPz0+R1BefN7N
+x8HDxMLDxszS43xcTkA9Pj9BSFFf79/e2+VyWkpAPj9BRk1tzL66ubi6vcLO5W1cUVFVU1df
+eO/i3eDb3f3v3urv5nJiXFBLSkxKTGHl0MrFxMfO5mxVRz48PkJLV2Tt1c/Q2Njb5O51fnrp
+4NrW2tTSz9ba3Obb3OHy+nppXlNMTFFYYerc2dDKzdPlb1dKR0ZFRUhLV2vv6+bl3dzram5r
+af145djWzM3PztHY493a3uDmb2ptZVBOUU9UX3zd2dbOzMjY7GpPTEtIS1BSXnZk+dbc3Oln
+aGNfcXl593d4b33q8+/y5dbP0c3S4eDp719cU1RvZ/Xa1c/O0Nh4aFZWT0xLTk9ZVGf5e9/f
+9+Xc5Opnc21ueGdhX2lea3/619bT4NrR6OtoZWBjW251+dzm2NfU2eDld/BgZ1JaTlZTUfVs
+6nzg5+rg8fBt9WpvXGdgXmde+vHr3N/b2+Ls+mpob15eWWBke+v039za1dvY5d9ubmFca2Rr
+bnN0fWzvZnnqaXxfYVpaWmRe/3Do6vT68eDa4d7v4urzc2xnbnrzfnzt6d709nh2cm1uYGdf
+ZGl89Pbo+ubs6X73bGVlYGtmZWZve3P6fO3v+Ozv9X3+dXZ0Z2RnaHR4/+zv6eHh5eTr9f99
+dHJ1+Pj7d//+b/14eXZ8+Hl8e/n/+/H4d2VnX1ljae/u7Xdwbm79bnhv+v36a3p39/F+8v32
+8X967vTf3+zgd/Ln8+b08vH39mtlX29YX1tcbmd7ZnJ6XO1y7dXn8d1sfOxn53l88W3h337n
+7XDl82Xtd11qcGjs+vHoZe3qbe7sY/fmXmHrbWDzZ2lzbfPwbPLvevlmb27+7PPq6O/g5ffr
+/H15bGxtbHJuanrs8vXu/nP6fXJ7cH33+XZ5/vnv7u3q/Xvw+fHu/fl6anRxb3x4bGx0fnxr
+ePjx7ufr7PB9dWx4f3b4+nt0dnn4+/fv/XFzbGVtc/309/Xs6vHo5u38/Pn1+m5rbnp89+39
+c37/fvz57/b4dmlmefv/+Xd09PHt7/7r7fZ8bWtqY2h2cHP0/Xf58O7v7+vs7+zu+vjy9Ph8
+bnf6/P3xfHJ7fXN2b2tvc25oZmltb/7y8+3q7ezs9fr7+/Hx8X93/X1++PX9+3hzeH97++vy
+fPj58v5wbm547+7y+P769vV4b2dtaGBve/j5+Hp773/t7vXq6Pt4bGzy5fbr+fXi6ufv9/bl
+8mllal9fWlxmXG/o7Pl/7+n7//Zvb3t+397h4Obd3er29WRaZu9eXXt1c/t67vToeVl5fHrg
+3t/f2d3+e/JlXmNhW19kZ3D75uvm5/R9eP5oaf5yeeXh7uzl3+Lm+nbs6+vo39jc2dXc3uVp
+VEI7PD0/SE9c8cy+vLy+xc7afVNJREE/SV352MrDwsLDytnn/2pjXGngz8rIx8zlTjcrKCwx
+O0diy7WrqKqxwHVKPTItLTNF07y1sa+vtMB3PTIxNjxIXNrAt7S3vcnY6lpGQUdLYdXL4jAo
+NDY8aUxNwa2nq7O/UU1XNi0vNUjPuri+u7a9yOhEPD9GTlNf69K/u7/CwsTJz+ZbSzosJSUt
+OlzLwLWrqKu220I2NTIvM0HeurGwtLrA0E85MjQ6RvnNwrm3u8PXW0hITVZ6z8jEv8PBwM5v
+NR4eKy894sy5qKGlt9ZPNDAwKSk617q0rq2xtb9JNjU3Pkxxy761sbvJ1nllZFFNU2T379PO
+WTsuKCw3Pkf/wrKsq6++1lxCNjAxN0Lovbe0s7q/yO5JPjw7P1rTysTBxdT39W1cWlRQcczD
+xcvMx8LGQyIeKi82S1jcsaOgqrnFWj88LSUpOGbIubCwr660zVU/OztBT1FY28fBvb/M1drd
+8V9hXk1COC0qMkRc7dDJvbKus8DYVkNAPjo7RV3Twb3Aw768x+FbRD5BR0lOYOTWysfLzMzS
+2/teXmb23Onsz8jyNCswNjpUbVfduqyrsLbLT09POjAuLzdPxru+u7S2uLvWRjs+RkdIS0zy
+vLKztr3FzNPeUjktJyQpM0Bp0cm8r6mnrLnYRDs8OTQwLzdPy7m0tbOztLfGXz83Njk7QU1i
+2se9uLm7vs72b19YTkZJTz82NTM1Q+XHwcbGxb+2t8TsRjg5P0tKQUZR7cG3t7m9xMTM2XxO
+RT89R2Tcwru9vr++vctaOSsmKCktOUvjxr22sq2qrLTEVTw4Njc5ODc4QfLCt6+vs7W6v8vn
+UD83MzI1QVzSvre0tbi9xczVVTYsJyQoMD5mx7m0sq+usLK2w2k/My0sLzY7Q1fdwrWurK2v
+tb/pSjozNDlCVd/Ivru5ubzLUDgtJycrLzxZ0r64tbOzsrK2vdRMPDg2ODs9QEdY5cq9uLi3
+t7q9wdhPPzs6PUVOYd3Mx8LAws/8VTwuLCssNUrYwbu1s7Oys7i8yetdRzs2MTE1PEpx08e/
+urOys7a+02NJPTk5PUhk1srHw8PJ2l9CNzEvMDhHY9zJvbm3tre7vsfYXkQ8Ojk6PURLX9nG
+vbe0tbm8yNx4UEE+P0ZQaOXf3M/N0+hRPTQxMDM8S2XRwLq2tLO1ubzG3ldBOTU1ODxGWOPI
+v7y6ubq9wMvsXE1CQklUZXvk4dzU095YPzk0MTU7RHHOwbu4t7a4vMbZfks+OjY2O0NPedTF
+u7i3uLq+yd5bRD0+QU582MzBwMLEyNZWPTIrJyksM0vVvbCtra2tr7fF9z8vKykqMD1eybu2
+sa+usLa91048MzE2PVHVw7y4tri7vspZNigiHyEqPNy2rqyqqaiprbpfMiciISYvQ9i/urKu
+q6iqsshNNy4uND1LcNzKvbWurK+820cwJyIgISg0VcCvqaWio6etwkYtJCIjKDFF372xq6io
+q7faQTEtLzU/VPjWxru1sbG1wfFFOTg9QTovLC0zVr61sLG3uba2vnk7LCgtOk7Wyc3HurGu
+ssJNNC8yPWfm7+LYyLu1uMXgX1NVVktKTV3QZTk3Mi9G183Bv765s666bEMxLjpBRVBc272v
+rbfMYD07SUpHSUtkzby5wtTsZuXXXkY/Q1zNvbq9yUMxNDM0QEdL9L2vra600ldPPDc4NjtP
+zby5t7rDw8Z1Rzw3O03u2eTm6NPAv839TkhX4MjAv8xZPTY2OT5CQ0/Ru7CtsL/iUUA6NzU2
+PlzSvra0tba801E+NzlEU11k7M/Fv8PR+VVGQ0dNaNzX09TOy87Q6lVMSktVbOXgblpdWFZc
+WFdo6dzc3djSysXL415RUVlocXF6dHHf1dTW3/V78NvPysjkRzs5ODxESU7nv7i2ub/N3XhQ
+Pzo5PEdYfNTAuri6wNFrUEc/PT9FT2Xj0cW9vcXXZE9NTUhDRk1f08fLy8/Sz9h7U01MS1Bf
+aurT09LP1uP3ZlZUWl9aYXJ069nS09v3ZF5dWlxpaFhZZ2702tfa0s7S3ePo7ujqZFNPT09W
+Z3B56d7U0Nbc629eVldXUVFUV2Ds1c3LzNPb5HVjW05OWF1fburY0NDV2tzY1drf6vhpV1FU
+V1VUUk5UYXH87+Xl39nd+nB6+Onl3uDf2tza33JeVlBOUFlfbeve2tXU1trl821ZUVVbafLp
+6OPh4+LrcWdlbnb+9Px9+uvh3+bzdWllZ2lrbG9yfH1saXRwbP73/u7p7fv56ufvb1xYXnnp
+4t3f39za3efyfG9vdnb+f/Lh3+n6a15aWl1ZWFpj/fb39ujremdmcXvs7O7s5t3c3un1+/H1
+/P5uZmtycW9oZ2Zqb/bl4uDd2dXR193pbFtWVlRTVlthZnPv7Pl5bmdrdHr9/fTm4uTf3t7n
+9G9tdnhxbnJy/ert+fju8Xn8+ebf3eDo8PX1/HJraGZob21rZ2NhWlNQVVhdYGNz693b2trd
+4+Pt6+rs5uns5uXm5Ofs+PTt+vju6u3j3t3f6fhpXVlVT0xLTE1SWFxfYGh3/Ozd29zd29va
+2Nze5ezxcm5z+Ozn5uPe3N3j5Ofo7Pnx7PPz6e97Z15dU05NTExPUlZWV19rb3vr4d3d3trY
+19na4uvi3+T2bmx49ujj6e7w6ePi3t7l+/zx6uXm8HtuZl9VTkpISUtOVFtpdn3s5N/a2djZ
+3d/k5evx9Pp0bm989v99fn3w6ebk4ODj3trZ2NXZ3OP0/WlbVE5LSktKSkxPVl586tzZ2t7n
+4t7f4+nv7vHw8/Hx9fX48O3s5uXp6+LZ2Nfa3uLk4Ofq925kXFlTT01LSUdKT1ddZXzk3NfX
+2t7e3d7h4+jl5+3zc/z5fX95eXvz6+Ha1NTZ29nZ2NjY2ebzblhOTElEQkNGSUxPV1/83t7V
+zM7Pz87P3fV1YVlYU1BXX27q2c7HxMbIyszS2t3d5+3r49ndYUs9Njk6NjY7SGrVw724tLi/
+y+RgTUA8Oz9KTl3czMPByMjEyNV5X19cX2du4szHx8bBv8TK3VVEOC0rLi8wOU3av7azs7C1
+wt5ZRTw4NjlFXuzNv7q4ur/M1ehMQklLTVJe6sq/wsO9vszZ319KPzctLTU1N0b3yby2tbOx
+u9hfTkE6OTtDXm/uxru+wMPRfmFVSExdVlfv2M3Hw8TDwMrc5PxpWEM7NjM2NjZAXdrKwLm2
+t77O2nhKPjs9QElc6My/v8O/w83Y62RYVlRTYXx369HMysnM2n/o6GJeUEI6NTY2OENOeM7A
+ube5vsTK6UtDQT8/RFB638/Cvbu/ys7U8F5STk9OTVrt3NTKyM3MzNjh4OlaQDg0NjMyO01c
+68O3tri5u8HVV0hDPjs9R1Jd4MO9vb6+vcXdb21bSkZMU1lj3cnKzMjH0NnT3WRDNzM3NTA6
+TlNdy7m3uLW3vs9nT0lAOz1ITE3+ysnHvr3ByczP415MR0lKTV/h1tTKw8XHycfLWEFBOjQw
+NDw7P2PNwb63sre9w835SkBBPzw8R1de28S/v769wMvR3mdVSUZKTFP81tTNwb7Cxcz+TD47
+NDA3OTtDWdrMv7i4uru+yN5nVUg+Oz5ESE/81NDMwb3AxsbJ3GJeX1JPXWtt7tTNzMzZ6mJP
+SDw6PTs8QE9l/tLGw8C/vsTO2eR7WE1KSEZGTllf69DIxcTAwcnP1eJ8bm1gWVxgX2X56vdj
+XVxHQEhIQ0FMXFZf2s3Oy8HBzMzJz/Bqc1pKSlBSUFv57uDPysfHy83T2+doWFJPT09TXm/r
+3d3q/d3yTVJiTUFLfVpM+tHqbM/H3fnW0n5t3+haVXV5XWrtfN7T09rb0dbe7ntfUU9PT05X
+cPvp2dDQ1trf+WVdWVFNUVhVWGl2evvp4ujs4ur39ev5d/J7dfXo39nT09rb2eH+bWJWT09S
+U1leb+vf2trV193g7HloYVpYVVBRWF1mbfjn7+re3eDn6OxubHb9fPnr9urk4N7g5ezudGJZ
+W11YWV9odunk6uLa19jZ3/BzbWNZXF5eXmJqcOzg5+ni3+ju91xVZ3pnWGfxeGVw5N7d5vbw
+fmlmbP5zbnb28u3f3tza3+bi5ef1bGBjampdW2V2cGjx3tvkfPfudV9jXldaXF1gc+vn6PPr
+3Obw6vJ8dGhq9Obm5+Dc4Ojm3N78bmdsXVtbWWNraGx26d/59+vl5nZ4YWxrcfNbe/h8993p
+2t/w63h39Vt0ZWdpU/lr6OVb299q6N/j7ep972DgfFzpdPL6efvi9fxi79lg5WpcafZaXHd2
+bG937eva8nfgfW15/Hn25Xts+fvueGjd/n707Xxf+2r/X/VwffTs92xx597x+e/s9HL2bXL5
+Z21x9m/z8/7+6PhkdWNoaWl0d2585+d35/3n2e/9bHxrbXT67+l67m3m93Rx//p+6PNsZ31o
+bv756mxua3j99vj8cX77Z/j143by5+7feu5+fX9tdX7te/9ode79efXmb2vseeX36vRce15t
+cn79/Pbn7+lreWr4+3P7cOhf/W/v5Oji7PXx+Ofu4XjmdHFva2l8dGlmZmz7cGhtemr6//Xv
+eezv6f3m3u3uaOP7999p+19wcXdqeGvucnjrevh0aPFmZ29ycH1z7t/j3enr7H717O/p8H1t
+ZmptfW9sbWhxbnXs7/f8f3Bycf53b3x89fv49vP9fe7v6evyc3J57+n3/3d17e/3+/5yc21k
+aG93+Hp4+ubodnR59/jzcnBtdnRucHP68vZ6fu3p6PHv+/Tz9Ov+8Ox+e2hyfnhubW9oZmRu
+eXl87uft5OPi53359O/5+Xlqa3B5+Hhv8G5+fnh+dXRvc319/fp2bGhzeXj3+nX2+Ovh6/P3
+cXF9/enqcm56b3F7+fTwe378/fDufGtqcXRucf39+fv78Ozq7fl5cnd3bGllZW/+8vDt7Pxw
+b2948fF+cW98fXn+//Tz7vDt83Ju/ndxf/Hye3JydvD1+fh3c3Zwb3JubWVlanj79/R4+fB6
+9fn67e7u7uzk3+bu9fj1/v9uaF5gbm5qbG9pcXFtbHJ4bWdodnv/e2p47vX47ODd4Obo9n17
+dHZ2/ebg6vV2dHNpbWplbXVub2tpcHpybG12/nr89XFy9PTw6PR3evjp7O3s9nVsb3d9+Pf4
+fX709vTv/21qbnp9b3Jtbfj4fW5zevf9c/Xu9nRsam148erp7fR9+/H29uzufH16fvTz/vf+
+/fF+cG5zcWllbHZ2bmxscf727fdz//Pz++/w93f48XBpePbt7/5++vp6eHX88u9l59V+4uzz
+6d5yYWRob1NH7udNX+bi/s7fXNHicOnq7ndw9WZ+3XN16910Yup8ZG7semnl3v7/4/BscG5q
+ZmhfXmheXffuaHfe7nH1+m9renZ86HB46vDu4t7n6OT0dufj6ubg6tvT49rT1tzh2OJtXEpD
+RDg2PTs3PUtSatfKx8G9u7u+wL7F2N7vX1paWFhmfurWzsnCwMHEzdleRDw1MC4uLi8zOUJY
+5sW8tbOys7W2u77H3XNfTURCQUJFSU9f6M/JycC9vb28vsvN0GNAOjoyLSwvLy82RE9fzby4
+t7Gtr7S1t8PW4FdBPj08Oz1AR05r2MzGvry7vb29v8bM2l9HPjw4Ly82MjA7Qktc18XAvrmz
+t7q4vMfP3WNQRkRGP0BKTU1YdunRyMG/wL29wL/CydHdaUtAPzovLzQxMjo/Rk1+zcnHvbi6
+uri6vb/E0vxnWk5JR0RARExOV2/n3M7Hw7+/v73Bx8jS8lVEPjg1MjIyMzc6P0xr1Me+ure0
+srO3ur7M1t9tTUJCPzw9P0JHUGXj1Mm+vby4uby9wMbQc11JOjkzLy8uLzU3Pkxm5se8ubSy
+sLK3ur/M3OxbS0U+Pj4+RU1VZ9rPzMO/v7/Av8TN1OH6ZFZQQz09OzY1ODk4P0lOXN3Jwb67
+t7i5t7m+xc3gWk1LREBCQEBIT1h43dHKxsPCwsTEyNPX1/NqZExBP0A7NDk7NzpCSU1t0MrE
+vLe2ubq7v8rR1PdZVU1GQUZKSU5Za+7ezsjHxcTHyMrV4uN7WFJQTEQ+QD85Oj5CQEhjdfLV
+x7++vbu8vsHHz995XVFOSkdJTVBbcufVzcrJysvLz9fuX1FMS0lISU1OVF5nd+nf6ufh3+Xy
+8v5lYGNjXV1mamZfYnjq5t7a19fV0M/Q0tTS1tvp/mhZWFlVUU9RV1haYGv44dfX19XY1NTd
+7HJoW1hWVE5PU1RVWV1bXGh96dzZ1NPV1tTOztTX3Ojz/v9/bWBfX1tfbWllbnz17OXl6ODc
+6e/7e/xxZl1cXVpYVlVXXGx/8uno5OHg393d4unm6uTm7f11dXF1/ftydnVvbnnw6OTn6+rl
+6/Hr+Xr9fXVtbGphXF9ma3Rta2xuePLr9fXt6ezx7+vq7fp7/X5+/m9ma//69f55/vrw6+jl
+6PT++v9+fnhpZGp3dHH9+nJ9eH34/ufe5PPx5ut5a2hrZ2Vsbm987u738O7r+f1uZ19dYV5i
+bvvz6eXi393d3dva3ufs+WhnbWxvcm1re31tbGtobW5pa3BuZWFkZG/8/frq7vfs5ubj393e
+3tvd5e/6e/L2f3h3bWdtbm1wbWpoX15nbnBze3p7fvr08Orr7eji5OLr/Hb69HZtc29wdP/x
+fnrw9G9ucnV5bGZqcHh89+3l5+Xf4uXp7XlsbHV8+e7q9m5z+nhwd3trYlxLX9xq7+3p4ujq
+6vBsWHLV9/l89ujt5OPyZ19PVdjma1Fe0tjV5d7Z4nVjdWZgX2ZibXj5eWlgXO7y+/H65Ozo
+dP/r6/lubHf7fHBzef3z8Gxt8+rldmdvfvZ8cHD47O3w7Ojq4/dlXW33fGtv7+nn9HFpZ3L4
+em9ofu7+eHNxdv9zbn99d3VvbHb06+Lb19vi6vDv5O12amFma3n3fu3sfWxaTUZDRElSYd/P
+ycXHxsjO4mdWTEpNT2Tb0MrHy9LW2eF1am5ubvjdz8vK1GA9LyspKzVKyravrrC0ur/NZUI2
+Ly83Ss+5sa+1wNleRz48OTg8SGbHubOzuMXjV0hHTE9Zbu3VxL25usdEKyMeIC1LuammqKyz
+vsx+PzErKCs71K+lpKq26j42MjM3PEZmybqwr7fHY0VET3Pay8jExNH3Si4lIiErS72qpKas
+ssH+RzYtKywzXLqsp6iwwnxCOTcxMThJzrOtrrrzPzk9VNfM1PJq7Mi5sbC9VTEhHB8mPrms
+p6aqsrrWPC4mIyk52q+mqKuyxmpGMiwsLj7FrqensM9IOTlEUV1649LBvLq6wc7WYT4tHx0i
+Ld6sp6est8fZTjYuKSw82basq7K8yN9pRDQvMUPCsayxyVxLTFh9al1YXtXHx83Qz9XQ1VdE
+Pz04KSUuPbump6y+SDg+QUNDOT1dw7Gtr7vJX0E+PEJtz8jE0Prr1M3I1VxLRk/jycPM4WZR
+W3H+6t93Tkg/Pkk/OT9M9r+6vcxZPTs9RvvHu7a4wNDwWV1uaXh18NjT2ed3+ObZzt1tU09a
+Z/Dn39/u7/fc61ZGQEFGXGniyMjbTD89P0NFVnrVzs7PzsbDxtN4U09YcNLEv8PPeVNKTn7Q
+yMfL2mVUT0tLVfnS0uVtXlFVT0xp4910X1hPVk9HPjY4R+rDuLi+xd5XSkpU4Ma7t7nEe05M
+UnXZzszW2uBfXVxs5GtNTE9OTExb58nFzXxORUpaTl9lWEs8OD5c0Lm2u8ZfPzk/acW7ur2/
+xNhcTmLSw8DOZUtNXuPKxMluQzw8R1ZXas/KwshTQEFAREZHWtvaXEtPWvDkaG3SydbpeN7G
+wMHDw8rcaVRedG3p083IyeFbUFdqbl5PSk5SVVhe+tbNzdVvQzU8S1nPzfBZPzhF7c3Fx8/Z
+31tT3L+5uL3K605CTOXFvsXV519YWlZd9uJsbmBJP0Rsz8O9zGVENy0rOlvfysS+v+w/Pk57
+2MnDv7/O9+vUycfQ3Nh1TExb18G8v85gQDxGWV7nxsxhVG996NT7RS8pLTxS0L23t8VaQ01u
+5enc1t7f4t3KxNDk4ufyal9639XNxMDH3mBTTkxOXnzVzd9edtLeTzgxLzM+S9q9usbnXFFW
+XmvrysHL3/5qX1pQ+cnCxtluW2zq0MLBwc1lSEtt3szEytdkTExcUkJGPTMvND1gxsDAws5s
+WFta78vIy87U4XdXUl7u6HV+1NDb2t7Y0959Z1xOU+bGvL7H2mZc7t9USEtCOC4uOnfGwL29
+wt5IOz1P9s6/vMTZWUE/S2jWxsPK4F1aYPLPxcbUb1JPWHvZyb6/zHZZd87CyV86LCYoMlC7
+rq2yxFo5MjM+dMC3uL7iST06P2vIvbnA+EpHTevEvb2/z1A+PUrcwL2/v8HlR1HNyfxHNSso
+LTVZt62utspGNzU5Ss+8vL/NW0dCRl7IvsDI0n5NSVPcxMLF0vRVRkJM3L+6vcjbZVd9x7zM
+QC8nICk+zrGpqrrmOS0uPWTFtLfC11RAQVPbwLy/z25ORktlzL+/yutOPjxBaMe6uL3JZ0hP
+zbi72kU2KB8mPcGsp6271zstLTtuvrO7yt5QPT9S1b68zGtTTEpU5MW/w91OPjtBXcu7tLO8
+30pH9MG7w+FOOCYeJkW4qaivv+c5Kys497qzvtLeXkRBWNHAvcpeTE9Ta82/vsb1Sj8+SG3F
+u7q7y1pGTe/HvMdgQzcoHylQs6enr8juPi0sOHG8tMLf2ONMQU10ybzIXU5o8N3OycXH60Q4
+OkzTvby8usFvRk7bvb9POjkvIiM03qylqrnLWzcwMT3ftrbJ1dttTkxP+sG/21dUZN7Ny8vH
+0k89OT9twrm5u8LgSkRpxr9rOjkyJic3arSoqrnIZTs3OT1eu7S+zvtZVl9ZWNbI3F1UX9jC
+vsPTb0k9OTpMzLi3vMXbZU9W2tTpfFc9LiksO86vrK+3x1Y7MzE6c7+7vsvke2BPTFn02tDP
+0tPRz87dfmheU0hIX8u+v8PCws1sSD49SVM+Li06Tsm5ury9wOBTQjs+XtXW0tba1t5gTExZ
+3crIzMrHzdb7WldsblhWYN/KxcfIvr/WUjw4Pj81LS5A8L23vcLAweBOPTg7P/u/vLy9wd5P
+R0JLbNvMv7q8xt5TSEpNTlrk0svJzMzIxdVQPTo+REg7MzhCWd3OysO8t7nHXzw5QEVKWd7F
+u7a6yNthTUZJV+DFwcPM32lRS0tPXPXn49LMzNxeTktQWFdVVFFNS0pKT2rcz87P1t3obFNK
+T3bSycbEwsTQcE9GSVnd0NPV2+dhUE5PW2r59Obb3N96aHbs93v1+nRfUVFVXXDf1Nja3Opr
+WExISExXdNrOy83W5WRXVVzw1c7M0OFwW1VWYe3WztHZ3+trW1RVX33q5vdlX2JnZ2lra/3l
+3t7qaldPT1NdZezWzcrO7FdMSVJlfeXTzs/ZdVdPW+zV0NHU2tzmal9ibPby/2tfX2xvffV7
+fHJtcGphX2htZ15bVl/z2tjg6nxhWVxdZfLY0NDcemJaXHTi3Nva19vd7GxobO/q8mhVUF7+
+5+bn4eLtfmhXUllhY11eZW7s3Nvc5/x+c2pnanrn3+Hn9GtiY2p2fu7e2NnidmRleu73b2do
+Y2Nrcu/j3dze4eX7bGZobmpfXGt1f3X4+XVnXVxYXmvr3N3c5Ovy/P97/PLh3N/pfXJvcPjs
++nF0bmtmcfnu8f5+c2tmbv/37vDw9HNoa2x+6OHn/Wxtbmtran/r9G9nYGRye/Xy7+z1bWZo
++uzu6evx/fXo6d/a1djh6PNqXl9paGlrZ2VmZGpmXmRpbGtkX2l09e/g5PNleNTj19bU2nRb
+eOxzTkzb2fbs4uHh7Vpm6PB1aWdfUlj77Nfb1dje61ReW1ZZVFlba/h+4d/d2urt/P57ZnB6
+8OLo7fr7+u3d3d/j6/D36+/35eTo4vtjanRbST8+QT9ES1bo0MjBvbu9w83ibFxOQz9BQ0pa
+/trLwb2+w8vR3XFcUlRXVV/y2dDMx8HC0VA7MTIxKy42RWrOu7Grq7C2ucVbPTYyMzU4Q/fH
+wLuzs7a7xddsSjs5PT9CS27Owry7urzCz+ppUTotLzUuLTlT2sS7tK2rsr2/0Eg4MzAxNjpI
+18K+uLCyur7F0PtMPz9CQUdX7NDHv7u7vb/B0k0+NS0tLCovPEtywbWvrKyvtLvXSj85MS8w
+N0FR3MK4tLKytbi/z31OQTs5Oz5IWu7Lvru7u73EzvpOSj0sLTouLTxKV9q+uK6ttbOyxF5a
+SDc0NDc/R1DVwb64sbS5vsfbVUE+PTs6QlZw1MC6uLi4vM36RjQzLygrMDQ7WMy7s6+tq665
+w85UPTk0MjQ3QFb8yru2s7O1ucHdXUs/Ojg7PUFP6cu+uri4vMLK1GE+MzgzKjA5OD9c0MC4
+trCut729y1dHRz46Oz5HTFPYxcrCvL/Fxs/ie1tRTElLXW3/0MXHyMLD1mVUQjw3LzM3NTtN
+d9TAubWztbe7xd5hUUM+QD9CSE5f+tDGwr/BwsjV5HxXTU1OT09WcOPZz8vJxsvQ031RQEI+
+LjU9ODtO4tzLwLq4vr68w9r581dMSkpMS1J+d+/Ry9DRxsrZ6/5xW11+92712tng3Nbc9Xdu
+Tz8+RDo0PkVFT3HRxsXCvb/JyszlbWleXmNndvB25Nz23NHZ4+XmcmRveGFUaPlvfOnf29PS
+1Nt1+fJcUkxCRkE6RktGU+7p69LNzMvO0c/T39/fen37bPV4a+fh5OPh293d3+r2cGxlZ/fs
+9+/s7fTzcXv7WFxiUUdGS0tESFpYW+jX29PNysvQ09bfb2x3YV5y8vDo3tjX3ODf+XL+aml6
+c3Dv5eTe29vZ2eLqeFhYU0xNTU1KS1tUS156Znri3eLg3+LzdO3d3ezh2N7d09LY29fc7v56
+emxlbfv78+Hf7fHi62hhaFlQVE5MTlRdW15y6+3s5+n2/f5vYGNueHZ68Ofa19POztLQ0N7o
+7mddV1NVXWh3//nr5uTp5uPpfnFrYmRbVFpeZGtrb2178efg6u/tcl5jcGpncX1sc+jk6+nf
+2N3m6vL+eGxkXWBud//z5eXh3dzj5uPr+mxeYGJfaW5w/O7p5+rvf2tlXFhYVVVVWmFv+erk
+2tDR1tve4/F7cWhhX2VrbnH24d/k6N7e7vjzeW5vdnNnZ3BqaWl49u3s6P1lfndnZGFfXV5o
+bnfx6uzv6d/k5eHm6ev09/B2cHtxbXb3fnd8fnVtffl7d/75dW56e25z9+/z/vbt8vnv6/1u
+b3Rsbnp57+Xm8nh2e/v+9/xydHh6dW1scG1r+enq6e//ePj5fXlv++/sf298+37y6evz9vp8
+b253+PDzfHJ5cnn1d/nx+G5qa2x2bG388fDt8Xdwfff1+nZ77e3w6eDg5+fn833+/HV3fXZ0
+amppa2lvamFoaXR6dnR2fv55cPv08uzw9ezo7e3t9u7q5eX18vV8dX16bnJucXNuc3d48eTr
+9nxtdnpyeH9uZm9rZ2x0/Ozq9/Ly6+v9eXP7+Pv8ev3u6+/37fB0b2xndPH7d3z87ev2d37z
++P52a2Rla2tu/vLs6/Pu6ujs9/5xcXBzfXJ8+XlwbXD47357e3F1dPrw8u7t8nt0+OTpdW18
+eHd+cGpu+/D0/P3t7fR+c3l2c3VzcHB1/fh2cnj7fffw/XZ3fnx+ff32d3zu8O709u7u+nV9
+fH3+/nlvcHx9bXF5+/X6enny+O7u7e3w7O/9/XttZmBnbXNucv33d3z1fPjm5vJ4b3Z4cXz7
+/vt+/X16fv358u30+PLy93nw9Pz17Oz19H7+fG19bmlpa3Jx//11cHNsbv35/fX8a296/ft/
++/Dr8/b2+O7u7fDw7+96c3Z+8fJ+dHl/9fj7c25vbmlkcP37+e72bnz183598fJ7c3Nrb3h6
+bGv6/3Z5eH707uzr5ubk7XtvdXn06/x4/fr8b291dm9scW9yZmdscXz2+u7m5+Hk7vX37vN+
+cmlpY2JfYGVtdvbx+Ovt/Hl9e/16d3h7fu/v8vh79e/r8Xlv/fpva2tqbHN4df72fnpvd/X8
+cnJ2dHv283Rs/vL5++rs8PP3+W9vd/p9cXz7bmloanRwcW5qbG94cmhr/u/u73zv+fv9ffPw
+6ODo9Pvv8fx4d3Rtampsb252cG1xePT5fW5tendtcm9kb/15++/6+fT5fvR4bXX77e3t/fb7
+9u/9bXB/fXZrbHx0eOvz9+3v8vH49ft1aF9oYmRiZWxvanj8eP7v7+jr6OTw8fv07ebn7fX0
+9fh9emtpbm1uaWdobmx6e3v07n1vefns8fx2/fv/dXrx6uxzb2ptfXp5cfbo4+ju7Ptxb3Fs
+Ymh7f3R3dXl2/X/z6vDs8/n7/f94bnrv8Xxucnv/9u/99/P0+mVr/u/z9v77/HN1b3X48PX5
+/np8fXNy9u3+cv/38PT5d3n9cW9qdXl1e3z97unt9vTr7u/y7/l8fnz2+XdubnN++Px4bmlo
+Zmhse/H19vh/cP3z8uzt6/x7dHh9dHrz5uPn6vH3/Pj99u75dm9pa/V+b29tb3J3b3V6dvr2
+d3J88PR0eurn7/N3c/vu9Or3df37cXF6+PF3b290cPjp+vX08fD2en9+dnZvcWxqeHFt9PHp
+3evwbXf8c3j3d23+7/f69P9wfe/x8vP6+21qamNlZ25scv7w7efm6efq5+7v9/n+7vB9/3f7
+/X51dHf/bWVnZmlia21sfnJ4efrv7On3793d4ubqen3573ZqdmtoY2Zrdfr5e3vs9vl4d/x6
+9/X66ujm6fDs5uv++/1vbnRrY2BlaXL/e//2b3FwdHn57+/7ee/u7fnz9ffs7Hx4dW9xbXj3
+7+56am5ve/f2/e7293xrdX3z9vvv6/H36vf3fG56fmxpa2Rta/31eG1tb3z4d/p1aHFyb27+
+7O55fO/q6ur+d/369fL3+fft6vl+7u/2eW5sa2dtbXBqb/T9/3f89vDueXtwe/H8ev/3+P39
++/t8bG93aXFxanV6/nj+dW717+/26e717nh4dfby+Xr3+e7uc2hx9/T083lyeHzzeGtscW91
+/H3ye212dfX1efr7f3j793p+e3R7fWxvbXB7f+/u6uXt/GpwdnR+7Pd5+XV9fnnyeXr9fvR7
+c3j3bmh/8u7s7X319O/v+vn0c2l2b/59fG9veP14/fd3dmJpcm5qcnpodXF49uvh5ePc297k
+6/Z99fT89/xybHBtc29tZF9fZm12cHj7+/t/e25vd/T18u3s6O9ycPN2fHt+d3ft6fhwcPn4
+dvny8/dxdXf/+vx9b3J+9Pt2fPX8+39ubG92/Hdrc/Xr6/P5+/rr6+rt8/r+d2htcGtocnx2
+enx8fH/+7e5+/fL3an309O77+/x/f3l8cXF+9f3+7+3teG5z6eby93v67Pvu+XV7cW1ua2xv
+Z2xs/nZ67n3z8u348H715eXn7evw7/z9cnp+fPx1cWlsc2xsd3RqZV9kZm/u5/L76+Xv+uHd
+4+jp8PPt83pqZG5uam9tcPz8emxoZGd5/376/O/p7OXo8Xxv+f3w+/vs+nv+6uz0fvZ0Zmds
+f/r+/f1va250+fLu7fT8/nlyb33+73p09n//+PJzbGz9fG1xbvXs6O3t7/fz7+r39uv2+H7y
+8Hxvb2t7+/97enRtZWdmZnn3fv73d/n79fjx9PfqfG9y9Orq6uPm6u/7+3ZxbG5tZ2z18375
+9u3zef9/fvz4+npu8Xp3bWp3b3RxbWxocnlwc3Tp7Prt8efo6erl7ffp/HNsb3p6cXl7fHr+
+f3t9+f11amT+eG77e3d2aG7u9+7u8Pfz697j6uvt+G95eGxpZmxlYGZodG1ke/B5efl+9vzy
+++/tffr/9Xz36u716ubm7+3x925oZmhsefP+e27683ZvbW92/nVwdnr5+vz38/z+amZman7v
+7fn8dHj4d3V96eV/+Pbtcnj3fvzw6Pn7/fjxfn15e3hnbHx47/f7++Tfbm50cmL3/3doY230
+4u5/bnhuaGxmZm9+bvD33uH39+3raevr5PL41svafmtcT0pUXV5j/Nva1drh8eFxXFxUYl1h
+avTl6+7v3vL37+nocP7xeGl5eH169vXy+W9/eHRpbe32dG198nH6/Ozzdmh992hv+/t7c2v7
+c29q/9zd7np5/HZs+3D7+274dWpcZuv57G/65+Z2dHp5eWh+/3D+Yf/q9Pr13uXu3dxjZ11a
+X1x0b/bg+m/9cvzr8Ormenvl7fhgeulsXVhobWr4e/H4fnh4/Wl+7O5+9ub06/Lx4ensdXJ3
+c213ePv+dWxvZmd6eXfx9Xbf/v3va3lsaGB0aHFubvLs8OLp7etlaHt4ZGPd6fbv6u98c2xv
+Z3V8fuppb253d3P9YGZp29lq2dlr3+pt+PxaXmZ18XLm4vTo93RmbmteZmFkbG12++94ZWZs
+bXrZzXpj6uLraF/wWsWzRdrY11xZVPnyQ0xN71de4s/o8en94131YO52XfT17mPz8OV9fPjw
+7Gb5cnp8cnDl6d/b7+Pv+GxqYmz99W9f+Wzldvfy8n1w7fPyeXtrXVxk9vRzffPv8PNxee3s
+bXZ0b/rs9O76bvB59vbidmVv/Hp66Hns9O3x9fr5fG13cnZt/15ob258bXr9cnvwcm/q8Pv8
+/nHpePri523v2+Pmc+987nvn53VtW3JcZl9ob2Nsc/78ePd9eGhq4ePj5Nniefz87nxs+Ofu
+fv/nb2d5bur9Y3v9bl1aaX9zZHR072hv6u/e9+zm3Pbr4Hj66+Hu5+117npqaXxuX1t08G54
+dfNwYmn38XL7bG56eW5qbm58+m5/6enh2+d/6f79eW/9fuvi8v/yfW/9/u74b2x5cu3oc3pg
+an55a/vx+vVqYGRzeu7s/HX5+3969O5iZnV2cP/x6+3r3OPy9OzvcHRv+XJ68vV2dv516+5t
+cfH2fHz/Z2R3de38+fR++Xj293hoZ2lrdWxt8/R5/vrv497xb3v57P7t5+Xs5eP5aGJrdndm
+Z/p9X2hw8uxx+XJmZnH07vjy4udzd+r47/l48+rv9+nu9XF1e3t7e3ptYlxneXZub3j8fG5w
+fXlsd+ng4ePl6Ojv7/Xw/W1pbvZ/cHf5+/t1fu/3bXF7Z2v87fZwbnvq73hra21y9HN+8Pj9
+dP/76+XtdGRve3x8/fX86+7x6+73ffl17/Do+G9wa3Pu82t6eGtnX2JoZmpqdPf68/jz8fPu
+59/o7vF4cmRs9+fp8er+/XlvfPz8aPzt9nZ4fv778OPq/WtiYmpubnZqbG58enrt9PV7en7t
+5/LzfHL9fv1vbHJ77ezd3ejq9337eHB6dW5zfvt0bXTzaV9tcG1ucm719G769Wph/ejz7Onk
+3ePq8urud3Zqanp3eXry6+17bWhtfWlxaWZ58nxoa3zq7vrg2+t1/ezs93z99Wpobnzvb2hw
++29t7u/tbmvp4/Nvc21ubW/5fvnx7+3p5fDo7Pb+ZWFqZmry9vvu6+/76fj+/nv9bGhxb2Vt
+/Ozvd/r4+ev4eXZ09fR89O3u7+96+vT3dWt2bG1v+355c3p3cfvs3+59/n78/vf2dfvu6u93
+7eZ4bmtlZ25vb2hq+ur3enJtfu3u/nr9+fr9d/Lt7+3z5+rr5O37fnNzc3Rub3x6b/Ds/PVt
+b25t/vxvZ294eHF96Of/evt5ePj99XVy+fXr73559O54ePr1/Hv/++v3/npobnD89fz58P1u
+aW51eXv89fTy8PptbHhtb+v0+ezxbnb9ePf0c/Xt7/du+evs7vl+7vB48+9tbP52bW1mb/1t
+bnJ68PJ/aWv293NzcWx7ef3xcXzu5ebr9O/qevb2+up6b/p49uvtd251d29haH5/c3Bub25v
+7vV6f/Lu/3R66/f+8/96b3t9c/f5+fJsbe/q/Xd9en96fn78fW76eGlseO37b3fu+nv37/Hv
+6vD8//58fnhueHxxb3rv73d993lrcX9yY15r/nfy6ers+f17e/zy6fducf1vanP34+v88PT0
++Xzy8mx2dWlkbPb7/P5x8+vu+vhyfvB3d29y+/P7e/16+/pwfvDq7HRlZGtqbnT+9Px9eG5s
+eunf5uzs5ej17Ony9/x+eWxhZm1gXm96bGxxfX158uzw7/P89Pz96u3+bH/2fPLv8vv+9fV+
+aG7x+3t3fv92+/Pz7/X7dm1pcX90fvt//ftzf/txeHRudP78/3H85/F0dvry7Ptv+/f+9fFy
+bvTu7HZ36vVwamz09ntrd3VnaHX1ePz/9fFzfu7w+fnx7fL19v54/vz37vltbm1qam17+/pz
+bXL89vL+df16e/HueXzz7fd59fL7eXV+9/H9cnR0cX72evn5+npybnf9d2tvdHj8//r5+OTl
+7ezv7Hh4e297eGt0dHX07/Hz7fNva3Rydndvam969vt1enr+9urq+fz4/f71+fTv/f7x7/t1
+cm5oZmpu+vn5+Htvc3J59f11e/7/8/336evy9O3v83r97P9vcXVvaGx5/XVsb3h3df739fr0
+8/z97efm6+/49O/48vF5cXl5bGlpa19fbmppa2VuenZ19Ojq6uTq6d/c3d/z9/N8b25rYWNw
+f/v+9v17eW1la2pmdXZ7//jn8fvz8uzp6uzv/HNubGh193txc3Bsdf77fHzv7fL9ffH0fvjv
+7/H09/p9dX3/8/lwbW1rbWtpamx1+/d5b3j2fPru7urm5ufq5+Tm9W9pam5sbWxlZm5vcnN2
+/nJtffjv9X199/Xn5ufp8/Z9eHVue359cmpqeHxz+3Z+9ntua/3t7e327/P7eXh+b3Tv63xy
++/l+cHj/dG92fnF8/Pl0Z3b47/f++X7y7fbp6e3p6fH79/v/dm1pZ2dsdGtoaHL89ff2/XV5
+/Pr/cHP39Ovq7e3v7/Pq7PL1eGpqamp3d3RxbXH99PH/dHd2fH3z7Onv+/n09Xn6935w+u33
+/H7+bWtubnJ3fH1yb/18dHBscX339Ozo6u7z6env7vZza25ze337+v75+PT2+3JydHV1d/58
+b3F8fPr17vJwbXNpaW1xbWVz7e/q6/L16+jn6356/vb+/vPw+Hn99fXs6Or2/m9vdnV1dW5o
+aGVlZ2hia29++fvw9ff29PLv6ebs+/vx+PPt73V0fn35eX37+Pbr7P50aV9iZGt2dXt2ffj7
+++7u8fZ2b2929evq+Pju5+fs8PL2enVzcm5qbnVyfvr6bmt3f/h/eXlvbXr4/Pfw7e/w7fL3
++HxraGxrfe3w9vH0+379f3tycPvv+P99fnV77ez3/fD9+XVoc/j39/N8dm90b2l29vPw+nZs
+aG596+nl7PP4fnv7+np3amZtdXJ5fXBs+ers7O74ef35enL/8frx8vTq6vl2b2hteHNzb2tw
+dn//fXx4dnl6eO/t8v788e7q7/r38/z47+vv9v11a2drdXlrcf56fHJ1b2xtevjv5+r1bnB+
+7+v4+/vz8P58eW569fL+fHP5eGp2/PDo5fBxbHL++Xx++3j/e252dfj0e3r97vh1eX317+/8
+9X51fHp6bnv19v3u7n94dHBzeXhzc29zb29vam7+/X397uzr6unr9ezl6vHs8nNuc3j+eXJu
+bXFrbHV7fH57/f538+jxfv13bXZxb3n87/B9d3FzfPv8+PR9a2Zsb3f78Xp96eHg4unr6fj5
+f3R1e3ZsaXL39vpzb3R3fXR5eHr2+Xl0b3J4eXt99319+fr99vL0+fx/9Pj87ujufmxucm94
++/1vdP3/e3Zvdm9vd/rs8fnz/Hnw6/F+fvf193z37uzv+ff3bmp/dGhxeG5yffX8fHlv//L/
+dHBtbv7vfHT+/XVvbnft5uDm6fF87ev5ff79fn/5/Hx+/3p3fHd4dXlzcGpsc37v7PN7e/r6
+evr8cf79fHJu/vr+eXb++3x+8+zz7ev3/P34/3Nrbvz27+98/H5wcm50/+7v/Hp9fHNzeP/w
+9315a2p97Or/dP3+ffz4/P759X51cn33eXl7bWtxbXZ4bXV3bnZ48+zu6ebk5uvt6u/2/f59
+eP96a2pva3R8fXxzb212dm5ybW54eXF2c3F9/fny8vT//fTx6+Tl9nj+/Xj79vjz9/H3/X79
++Pfv73xxeX1tamttcXR8dnX7/v1tanB4fXd1e/p4bW9zdPXr6evu6u/s8/vv93h9+vB+d/t2
+cP3u9PtqZWxpaW38+PDn7/X57OP4cHj+9/l+dG9mcX1qaWxwdXlu+e7x9vHs7e368/x2fe7v
++Pt+fn1+7OLn7PV7al9gZl5bZ/d1bnr48P149O/u6ePh7Ojg4+Hc2Nfe7Xxzc3ZmX1hWWlRO
+V1hQbOz78ujo5/Jve/tpavPr6t3Sz87NzM/Z3e7v/V1dWkpCSk9HUfbi3dTOzdPr83JSTlFO
+TlFn4t7czs3d19Pf3trY09bb19fqZV1LPUJHQElf893NycXL3OVrTkdHSklMX+He1cjFz9DN
+2Ovp1dnSzMvJy83kSjo3NS83QE55z7+7vcLF2VVNREBERFPt2Mi8vL+/y933aHbx3tTSzMbJ
+Zj43Mi8uNEBU8cW3tbe5vs9XQz89Oz5M+tLIvLm/x8rUf1153t/Pwb6/we1dPScsMSkuTnbQ
+trGuscDG0EE1Ozk0PW7Hxruvs8XLyPpQYtvKxL26wdJrPCcpMCUrTWnkvLGtrcHKyEAwPDgu
+PtXLvravr7/Oydti2ce+vtLEwUouJi0uISx47ve2rKyuvL7POC43LyxD6s23sq6uvMa+y9DC
+w83NydxEKyQvKyA23FvXsKqrsb68ZzAyLyoxRV7HubGqr72wrrm+u755a0csJCUtJiZJ9n69
+rKurtL7ATDMuLi4wO+vDv7Grr6+pqrKyuczgNSEfJiEdL21nza6mqK66ucc7LS8vLS9MzNS/
+rq2up6erqK++2i8eHyMcHS9La8OtpKStt7XUMy4xMC0yX8nKv66oqq+spq26wkQpJSMbHSUs
+POC5pp+nqarCPzcxKiovOVPQuaypqqanrrC/PS4wKh8iKi01QOm0rrWuqbC/zd1XPjU5P0Jb
+/vvJv723uMDFxd9HOzcxLTM7P1jYx7++v7u+3f3ebFFn3d7gz8bBy/5fW01BQktSVEtJWXBX
+SE9+YF/gycDCwbi2vcLExsvhRTtFPDAvNDg7OkDNwODKsbLBxcC/zFFfw89my7u8xOlTbEMq
+LTosJzE/UGtPzLK+x6+uu7evr7jJubPTeNo+MTYqKC8pKD9LP+LN0L7d2LXF/bqvt7y0q627
+vMZJPjQoLC0mLT4/XtXOusDfvsFo2b/DzL6xsrq71UJMOCgvMSkySUzUwr2zxdi801nLvsDE
+t623v71NOEAqJDItKjxKUc/Lu7vWv79qzbe6u7Ktr7e66Dg5LiMrLyovRFTp17+5z8+/3OK/
+uby6r7C5trxEP00qKTguLD1ASG7Vx+DXu9l2trXGvrC1v7e+Qz1HKic1Liw/Y37aubHGzLbT
+Wb6+1MO0s7+8u0c5SiwjMi4oOFRNX8O0xdO1w3e4s8K7r7C8u7tINVMvITEzJjVdQUzHucnV
+trxsvbLGxLKzvbq36DhPPyIrPSgpUlE+5Li/2b6339e1vdC6sLu9sLpIReMtIzszIzNbOUHG
+vs3Ourv9wLLAw7Oywbu0xzxDYCkjPjIjOeA9S729zcu/webWwL/Iv6+xvLGufjvcPSAsPiUm
+S0k65sDDxsq+uttevrXNxa2xv7SyVz1nNiUuNykrP0xV7da9utXYvMpVzre/x7Wusba5zEA9
+OSgmLSsqNkFPzMXGuLnTz8DK1Ma8t7S3s66zwls7OjQoJiosLzQ8/MTYz7++w8fGw7+9t7m7
+s7G4u7/nOTQ4LikpKSw2OkBd6s29vL+3tru7ub2/urW6u7a5xWs8NC8tKyckKTA6SfXTybqz
+s7S4wMPBxcnMxLi1trK1yE0+NiwqKCQlKi477cfGvre0r7C9x8vb0cO/vbWxsrO1w002LCcm
+JycnJys788G2tLi5tK+0uLvG3t3Mxrqzt7y8vtZIOS0nJiYlJSkuOmLDtq+tra+ytLi/xs/1
+XezKwbqzs7rB9z0wKyclJCQmKjNKybSsq6utsbe7vsXO6VRKSlrQvLWxtLrIXTsvKyYjJCcq
+MD9sw7Kppqerr7jAyuVXRj47PUjqvrKtrK20xU8xJyIgIiUqLzlTyLWsqKeorbjG/E9KSkVD
+REJK576wqqeqrrjoOCojHyAjKi83Q33GtKqnpqiuvdxOPj4+PT09PUXru62loqSpr8JHLSQf
+Hh8kKS43SdO3qqShoqevx003MC8wLzE1PE3Ns6efnZ6jqa/EQykfGhcZHSIsPf27rKWfnZ6i
+rcBONCsmJCQmKzJMvayknpycnZ6jrLnzMR4VEA8SGSAwbbuqnpiVlZmitUQoHx0cHSAoNtSu
+op2dnqCkpaaqt2gzJx4aFhUWGCA316ygnZqYmZ2lvDkmHhsbHyQtSLunnZucn6e0xvZJRUpR
+ZvPc4EspHBwbHiwzOmW9qpuVmJ+tTy4uLCYmJig8uammp7G9uLi+2joqKzVWu66tq6mnrPcf
+ExMUHS44Srqhl4+Pm7VIKiUlHRcaJUupnaCstLi3u1UqJCk30be2r6WdmZqgujEZDQwRFB88
+z6aVj5CUn/QuJBoVFhokz6Kdm5yltc88JyUqMXOyqaakpaqqr77LVjMoHhQNFCUuz6qjm5KU
+obVVJh8eGBcnzqydmqGkorA6KiUgLWHcw66uvLu/z7m0xMK3uMPkLRMNGiUjN+WwmY+Xqa3A
+MygfGR75treln6eruC4dJiwsQ8e5p52jsrjRRFJyTunDznXoYyIOEiwuPrGlm5KVrs7RKBkY
+GBxOq6+kmp2rtPUnJCwqOLyurqapxGY+LjNxzNKypaCks2ojDg8dGhs/p5qWmamrqTsbHCEm
+PcG3p5qdsLm9NyYoKTLhydCwqb5YV079u73OsqisvFo1IQ4OKCUe4Z2YmZmipaU8Gx4rJSM9
+zqucn6+vsTgiJyknMVbAq6WvuK21+1/O39/F1NW5vj0fEx4uHSTEpaehnJ2ftDEqLiAXH0DE
+taqinp+3Ojw9KCEtTde7raimrsTEut5AcMfN3md4PBkUKyYaMbGtqZybnJ6+NToqGBgoOD+6
+op2cobS/zC0eJy8uOsuvqqirray34NvfRkBHQjooGiA9JSFsr7Sqn5+fpsdeOx8aJDIuOLWj
+oaGorrhJKiwvKi9MyLSurKmqssXNzfFOREJJMhwbMCYZNbS9tJ6bnZ6uv9ooGh8pIinMq6qm
+oaGrzkVBMSQrP0ZwvrGur7Gxt7+8v+1dRiwbFyQeFi6/xq+alpibpa7dIxkdHRklUbmooJyb
+obO75y0lKi0uPPy+trOuq66yr7jq1z0fGRoeFhc5zcqolpOXm52kVyAcGxYXIzvPrJ+ZmaGn
+q30tKykjJzJE5b2tqKmoqa6zx0wvHhcdHBMeQEfFnpeVlZqbpUMkIRoSFx8nR7Wim5ucnKS8
+bjwqJCcrMEzHt6yop6aqu2w5IRsfGRYiKi7Pp5+ZlpiaobZULB0YFxkdKj+5o56Zl5yhqc82
+LCYiJy43Xbuvq6iutOUsLi8bHSwmJ0q/tKmhnZykqavFMSooHRsfKS1LtKegnp2ep7bKRy8r
+KywuQtK+ubOvZzfdMh4uNSUsWeDFt66lp66rrMtRWDYqKCcqLC9Zv7mqoKKmpqy+ZkU6Mi42
+WltLzL0/Odw2Jjc7Ljps1L63squxu7K8Vk9mPC83OTQ3P2Hcy7itrK2sr7vMeU5CP0JEWl5R
+5GA3PUsvLT44NFbNy72zrq61uLvTVklDPDI2Pzs/XOfPvrmzsbOzucrcak9KTU1RZExMWDk2
+STsvPEo/XMrAu7m2s7a/xMxaR0A6OzY3T01U0cfGvbm8vL7Cy+N+YFxYfs7UcVpUPzU5OS8z
+PUBO3MW6t7Svsrq/y2VGPjs2MzNHTD7Dt8i4rra8vcrWW0pjTD9U3vjfw2dE2kIvPzoyPEZL
+4czFs7W6tLfDzNhPQDozNjY1Smrnv7e3tbO7wcr+V0xCQEhKT+HSzcHCV0/PPzNHPTM9UVjh
+z8S2u7+3utDh5Uc6OTk7PEPszcy8s7m7ucTab05FQj1DY+zVw9ZLy94xR1UzOk5MXc/Ou7bA
+u7fH7NpgPj09PEA9Stra2L23vLy5wddgTUY8OUJIQ1/Oz8i9vb/J4GpNPTs/ODZHTEnwyMS9
+ur29xtvuWEdKUU5e2cu7s7nBvsdEMzg0KSkzO0fouq2trq2uvv9QPjAtMDQ3Qu3Aurawr7a/
+zfZCNzc1MzhDYc2/ubOxtLzH1kw6OTk1N0BU7s7At7W+x8tuRz87Njo/QFPfzsS6uLy+xtj9
+VEdERERLXO3Xy8O/wsfH0vJvXUk3N0Y7Nk7e3Mq/vbq8zd7fUD4/Pz5FU/nPycK9v8jWXE5S
+S0lV9t/Uxb++wsfL31pSRSwqQDYyZtbFtrm/tLdfR1ZEOz1DVtDLybu6xtdnUlZNRlrcz8jD
+xb++ycPBVC8tLikrNkjaurKvrLDG/0k2MC8zQta7s62ssLzfPTEvLTFBa8SwrrS4wmpJPzk+
+V+rTvLO0srvWfCUXKC4mQc63n56vtrBWLyslKEd8YK+mrKy6Tko+Kis8ReSzsLKst/hrRjhE
+TkvJtri3yTIiICgvPfm8pqCptsxONSwqLT/LtK6vsrjjOi8tMD5YxrCtrrjRU0I9PERxvbO1
+srC0ul8pHRcYLVrFqaShn6xLLysmLDVBxqikqrPSRjkvLDNO0rqwtLm++UlJRkzlyse+ury5
+ubjJOikgGRoxbLajqquntz0vKyg7a9qyqq2xv0w3NS82X97ItLXAzGdJV/Tr19zSwsHAvLu7
+tcM6JhwSGD7WrKCrqKS6PjEnKE/byrCvtbPFPDM0LkfS+r+1wsTPSVPX8tnIefbLz8e4sa2r
+vzEfEg4iTbecpKmfqWw2JR0v2sStrLOutj8yLiY81/29t8zAxlBmzsq9vd1gZFvUvLOqprU8
+JBYNGDJunp2ro6fISi4dJlnJram3ur9JPTIoNdrEs67K0tNMYOL1yrzJ295V78C2qqi7Nycc
+ERQlQKiaoaWqzUs6JiM357KmrLrKW0U6Mi1GzbyrttjhTlfNyNXGyvnjWU/TuKumrzwnIBgW
+HS3FnZykqMFIRDQpL0Pcr6q3x9JQSE02PevnurXP6XReyrrBw8xVUGRf6MG1rKe/NScZExcn
+UKicoqWs0Uo2Jyc5a7qssbfAcFw/NjhF3bq1wM/zYtLFxcjTfl5kZ+K/ua6ouj4pHRgXHTS8
+oJ2iq75aPTQtL0LNsKyuvWhaPzlBO0jHt7e5zk5p4dzJ3Fhg9uDPxr+vrLxPLSMfHRwmTq+d
+nae16T89Oi8yRdSwqK7HST80OE44Xbi6rrHzTHde1b1+XNx42MTHv7OywFksHhsXHDzDpZ2j
+q7HcPDUrKj/Staqstc1DOTI1OUK+tLCwxGlPVV7TzPjV097OzMW0rrteLh8dGxwvbK6eoKiv
+0EM6MS45U8uxrbG9WDU5OzxT3sK2tcHXc1Lz1tnZ//TVzc29s7O3zzspHxoYIkG4n56orb1p
+SzgpLDp3sKqutsw/NUI4OG3xv6+5ydBuZ8vWZF5KY8W/urOvsb5QLSIcGR0v3amfpKq03Eo8
+LSs1ScKurbK8ajxJPzxWSH26ubm91nLT0OPgTUXzyr63tbSzv0ErIBgXITW+oqGlqLLG2z4q
+KS4+wK6trrjTTj07MjZDT761urzGzs7UU0dFROm/t6+vsrS8TSwiGhcfMMimo6enrLnHRiom
+KTL+uLGusLzqUks3Nzs+2b69ubq/wcDlU0g8S9G+ta6wtbzyOioeGBojOLqnpqWnrbC/Pism
+JzXkvrq4v8S8v99QOzhJdOXU09fCubu/0Fha5dDKycvIwtVcRS0hHiAqQsS1r66vrq6530E1
+NkJVWk5DR//Et7a9yM/T32tJPD1J88a/wsO/vLq8yNhsSj03MSsoKjNI07y5vL2+v7/H3G9r
+Z25hRjk2OUnjyL66ubm6v9JbRkZLWHjm18rAvr2+x9xeRj46MS0vN0FVb/Tt59nNyMfGwL6+
+wcvlTz47PkRPYe3Txr6/xdR5Y2h67/n/7+DZz8vT8VxPTFBXWFZadPl3W0xGRUdKVW/ezsjF
+x87c8WdZVl539Onc2Nnd43Vla/fazc3Pz9LPztXmXk9LTVRcbHd0dPh1VUlBP0FGTl596djO
+z9TY3eTk5ujg3dvd3eDg5/lvb/Ha0NLR0M/Qz9PjYlRQTU9bX19eW1NST0dDRUpUZvTm4tfS
+1Nbc39/e3+Xn+29yfu/r4+Di39jPz9ff4ePe2t7ifGNfZWZnYlhTUk5OTElIS1Jbcf17fP3o
+5N3b19DP1djg8Pj28O7t6+Hf3tza3N3m7ure4Ov6bm586udyXFRMSEhGRUhOV2fm3eLn9Wr8
+4N/h4eXe1dXc5evq4+br6uTl6OLf2dfb3+Hp8XhsaGdu7vRmWk9KSUlGRktPV2rp3d/b2tnU
+0NHe7fTp5e3vfHB/6ufs8//05t3c2djX2N/n7XtrbG1yb2ZZT01MTU9PT1BSWGBrbvLc09DR
+1dvi+3Z4dHzu597a2tjW3+bh3+Tm4d/h4OLh7fZuXVdQUExKSkpNVFdaXV5mcnR97uTc0s/X
+2dXc7PJ+a3nx5OHh3dra4OLd3eDl5Obl4uTn/mlmX1RMSklISUtPVl1keHhr+uno393a1dTY
+3uLg5+zt6ufm397n597c19jc3uPl497g6fhpWk9LR0ZFQ0ZLT1Vfc/rt5N/h4uLh4t/c29fX
+297b3NvZ2dzd2tjY2dnb4ens9v5mXVxZU0xJR0VGSElMT1lr9ebg393c29vf4N7e3t7c19fb
+3uDf3Nza19TT0dHZ3uL0aV9aU05MSEVHSUpOT09SV19t/vvq3tzc3uTh3dnW29zW19nY2tbR
+0dHS1dfa4ex3ZV9fW1BMSklISElMTlFWXWNu7Oju6vn36O/k3tza2dnZ1tnRz8/P0NHX19LT
+0tff/l5VTUtHRkZERUhKT1JSXGRsdPfz8Obf3d/n5OHb297W087OzMvMycjKz9DR1N3zaVlS
+TkpGQj8/P0NIS05WXl9jbHZ77+fh39vZ2tnW0M7MzMrJysrKyszLzdLZ5HZeVU1IRkdEQUFD
+RkpNUlVXXWZzdPzo397f39vZ19XU1tXOy8vLysnIy8/O1N3j+GVWT0lEQkJDREVHSU1QVFxx
+eXh5/vHu7+vn7HpbOT60sb+xrK+wtr2wuGA/PDcvLSclLTEuMT5MzrrFuaustre2vcR0Q0hX
+RzxP08O/wbivsb/QbkQ4LygkJygpLjhPzLy5r6ipra6zvMbsSkZBO0DswsC4raqrr8ZFOTIl
+HR0eICUoMOazr6uin6Gkrba67TkxMjA6WmvBqqanp6/fUkwpHR0dHB8kKlm7vK2foKOhqrKx
+0zc3Ni8/bmG4p6uurdBDbC4cHiIdHygrTrS5sqGfpKOqtrPGOjY+Pkjvx6+qtMZgT00sHh4j
+IiImMtOztrSlnqOrra+yykVJ0f5Mybe+yFovOEIkHCQnJS0xPbOqvq6eoqeptrWu3EbHzP+/
+y0hONyQpKx0eKSYpPkvgrausoZ6ioqavsa++0b+7101BLCEiIBscHyIsOz5durGvqKWkoaOo
+pKOsrqq0z9I9IR4fGxcZHCMwMzy3p6uooqGgpK2pp7O4rr9+1zcgJScdHCAfKjw3UbGurqiq
+p6Cnrqutrq69UllLIR0kHxwhHyZcT0G1p6ihoqWdoLK0rrW/4DxBOR8dIh4bICIpUm/OqqWl
+np+ioKeysb1gZk49TDQhJjAqIyYmOWY1R7Ksr62tpaGzw7G9XFNc4c3lLi5VQiolKT5jMjbA
+r7TBwqunvvvFvu9YQt6+REfF1kE+STs/NS9FTT9Hy86/v9e3tsXCyN7N1u3hc8b561NTX0RB
+P0M8Q0RYZU1Yctzo5tvKt9C6ur7Cw8DdzFDnXEw6Pkg4REZHWUo7SVRc+ubW3cDHvLu1usfA
+087SZ1BXUEM8Q0pBP0ZcZk1K3eLvfd7DxMrJycrV1/bgbVzpb+pvVEFOR0xJRWn/U1jP1cHM
+ybrH3/bvaFNWTmr7e01m31xYSl13V1JZ39ff68nEzM/EzOvrT1lfU0pcU01+ZG7442N7WVTd
+6Gpx3uXo3Obj421c6dlxzctbfehe4f1ITU9KWGZuXmzo3/7f3Nvv8NJy3cvn4tDe3uRm81Nz
+blNcW3dcYlRPYFhea+dqVul1/eLTz9jS4s/Y4vdoam55enHm51f8Y1JeT0vy4m9/+dLQ5eV1
+c+lqYmjvXFppbN/c3ezh6mZ37F9pcWza3Hrv3OHlYFldbV5eZX5r4+5n2t343edkfV5WbOjf
+3Wz73fbvbHZqWlX17P15aXDw2N3e09t+bGFpembo6VdjeFxqX+vsbW1aX3B499/o3txw3ePu
+69rVfVn05n5jVHFq/17/9HVbXVhm6WHp1ebg2OTrztfaXFTaZldcaHl8anXjZ1tefehnT2Bk
+Yvls5dx+2tbe0ur/2c7Z8fv8cmtkYV1XaF9oT1Nwcm1Z8m7/3+Pi0OPn3tzefV9b7V1aeXLx
+cvTh517zfX7qed/g53Xb729ganJdaF1kcmJpaOfeenXl2m5sV+TrdH7d2Gje/enj+HFXZX9b
+XnlhfmZn+vLt8HB34uHr62d/2Wjd2W/uZmrp+mZpWnFyWFd4d/h6ff7u6WD55N32bm3q5H55
+bex9aubj7O5zfXbi/OTq2+5wZWrj7PxbaFpwYFBOaFdKUVhjWVlh3Nfd1cnFyMbFxr/Dy83P
+61VLSUI8ODc7PD1ATGLr7OfGv8LKx8DH3d/OyMfVysLBxsrPYF5XQDs5NjY3NDU9R0tZ99TI
+xMS/vL/Av8HFyM7Kw8XMwcPFz1hGT0o1Ly4xNTIvOFpkXOXGu7m/yLu3xNXPzM/cac66ws/D
+u73LUUFYUDIrLjExLy03/OJX27q1tbvFurLEbd/V5mJJWL+6z865tLnOTE32PyopLy8tLC9G
+035nv6+yuL2+tbjqTe3kTUJE7bq8z72trbTEfu3gOCYoLSonKCw8fllfu62vtbm5s7rxXNbe
+TkdR5sXCxrmur7W2vvxPQi8pKCckJyotO1Fhzre1tK+yt7e8ytHXcGd0YnTTwr26uLSytb3N
+XkU7LiclJyYnKi88V+3QvbOxtLa4ur3H1NPP2e7p0cbAv723s7a8vshYPDguJyUlIycsLjld
+59O7s7Oxsri2tLzIx8fP1uNv3MfHzce/vr7G12hHOzUtKCcoKSouNUJx08O2sLGwsLS2t7vC
+xcvZ5PNjatfT2szFysvN4F5HPDo0LCkqLC4wNUTvy8W8sq+xsrO2t7rEzNLlbVtVW/Xf8O7f
+4tjS9H1yTz4/PzYwMjM0PEJI/8fFv7i4urm7xMPD3W7pb1xuamXf0+971N3j39/p6e5XS01L
+Pjs8PD9GSUpl2tfWzc7SzMvQ1NDW1dHU29fY7ejc5GReaF976tTPzc3V4fHsXE1DQD4+Ozo8
+Q0lLWHTf1s7MzMW+vsHBxtDha1ZQVldqaOHWz8bBvb28wcjic1RAOTUzMTIxMjg/Rk/x3MvC
+vr+8u73ByNLp2etaXvXa59PMxb/DwMK/ydXsWEdCPTYzMzQ1ODo+R1VfetjOycXFxL+7vcTO
+0cnL52vv2d3r39Xh2N/r6HlsXVZNSUxLRz9BRUdGRUdOXvPq69bKx8bEwr/CzNLQ3NXtX1pS
+W1laZGtff+Td2NXPz9Pf721vVEhDQUJAQ0NGUFhieN/XycTIzc3MzuLv7f13V1NhfHnw6tzN
+xcXNysjJz8/b72NTSkNBPjs8PT5BR0xXftnTzczN0dXX4tro7f5r6+HU49bQy8vNx9fJzcrL
+093k7E9IQkQ/Ojk6Oz5CRk1a8N3Qz87JzM3UzszY0Nnc1tvb1tjV3tbV2OX00ufe/mt5WVhP
+TEhLS0RCQkVITVFaZObXzc7QzcnO3Nng2+n68H7f/9n929bdz+rR5dfi+uZea2Njak5OTU9M
+R0dHTFFSX2nm4tXR19fXzNXe5vrkaXRq6+bp4OzZ2dfU8tfZ6MzS4Ofu2nFoSE9LRkI9Pz9P
+UUxXcdnYz9DVyszM3N17cn3xamfa2eLm6tzPxc7Yzs3X2OXkbO1NQkRDPztDPz1LTVNV29nS
+xMbMycXO3dPs5e52cFnqcu/j28zTwtXOz87L5tBvZ049Pzk+NTM6NkRLXW3ewMS5uby6wb3Q
+0dJpbVVWS0tcTmZ82cvNyMPCwMXT1e1bQTU6OjMzMjM4RFZW6sq+t7S0t7W3vcfT6lhNST4+
+RUpSXOLMw728urrDycrnTDwyNDQuLi4vN0RV8s6/tbGurrO2ur/J4lNFPz1AQkVKTu7KwcHB
+uri5v8vTcU46MC8vLS0vMTdGXdjGvLi0r66vsrjAzd5gST89Pj9ETVJ3z8XBvLu+vr/FznVV
+RTYvMTEuLC40Okddzr69ubGtra+0vMfTfFhJPzw9RElOVuHQx7y8v72+xcfJ2GJMRDguLjAx
+Ly81Pk1h18G7uLSwsLC3v8bUfFVLQz4/QkdPV2/Rx8XAv7+9wsrKydxdTUc8MS8yNTY2OD5K
+XN3Bubi3t7e1tb3O9VxQR0E+PkNJTlvxysTLy8C/w8HDyt9lY2hhT0I7NjM1O0BAP0dd48y/
+ube5vsPBxMzdWklFQERLTFNu7d7OxcPDyMrGyth+4d5rXllYWU9FPTw8QUhMS0pRZu7cy8TD
+yNHV1tbS23RbWl5p9d7X1M7Q1tLS2e9vXl9//VxX997Z1d3i/1hJRkVERERDQUROYOrZzsnJ
+zMzMy8zW6Pz6+Htubvzv+nNw/v576t3a0M3PysrQz8zQ3mlMQTs4Ojw9PT9ETFd32crCwMPK
+0dLP1eJ+aGFfXGN88Oje2t7c1c7NzMrMzc3R1dXcc2FVRz45Nzc6PT9DSFFv4dXKwb/Eyc3Y
+297h8mlmbW508enb2tva2tnQyMrM0dTQ1ef75+1bSj86NjY4Oz5BRkxWaeLLwb28v8jMztLW
+3fB4Xllhdu/o4N/p39HRz8vM0dbZ1dPa53pdSz86NzY5PD0/REpZ99nMxL++vsLJzdHe7O9v
+Xl1iYW/44dTV2NPNycjOzMnR2dffd1tNPzgzMzc7PD1CR09l487Cu7u9wcbJy8/b82tjYV9j
+b+zi3tTPzc7R0M3Lzc7W9GVYTUM7NDI0Nzs+Q0lVctvKwby6ubzEyczR2+xlUk9OT13r2dXS
+1c7Ev8LEy9bU2uxqWUo7My8vMzc5PD9LY97Nwru2trm9v8XO33ZfUkxIRUxg4NPOysO9vL3B
+w8bN1d98WkY5Ly0tMDU3NztDTnbMu7Sytbq7vLzDzuZaS0I/Q0xaW17sx7q1tbe6vcDEx9T9
+TDcrJygrLS4uMDhGd8a2rq2usK+tr7jMYExEPTs8Pj9ARmrCtrCvsLOztLi+z083KyYkJiYn
+JyovP//Bs66rqaemp6y3yN9bRTgxLy4uNUbux7y2r6qoqauvuctPNywkHx4fHyInLkLYvK+p
+pKGgoKKotM1eRDguKikqLDA+bcS3rqmkpKWorLXGVDQmHh0dHRwdJC9H1bWooZ6dnJ2gqbXH
+VzQpJCIiJCctOVe8q6alop+foqmyvn42JBwaGxkYGx8rPtqypZ6cmZibnqStxz4tJyEeHh8k
+Kzh2tqqkn52cnqGossVOLR8ZGRkWFRoiL0nCq5+bmJeZnJ+ltVgyKSIeHB0gKDRbwa6knp2c
+nJ+mrr1dLx8ZGBgWFBggLEXDq5+ZlZWYm56mvUsxJyAeHB0hKjxqvqqgnp2cnaGotNo9KRwY
+FxYVFhslN9WvopuWlJaanqOzUzMnHx0dHiEqO9e2qaCenZydoaez+DcnHBcXFxUXHCc5x6yh
+m5aVlpuirMU8KSEdHR8jKjrrtqifnZ2dnaGpsMRCKh0YFRYXFxoiM9ytoZyYlZaZn63LOCYf
+HBocIis9y66knZqZm56iq7nkPCcbFRQVFhgcJTu+p52YlZSWmqO7SiodGRgZHCEuWbSknZmX
+l5mco6/HTi4fFxERFBcZHytYrJ2YlpWVl5yq6C0dFxYXGR4pQbqmnpmWlpeZnqq8aTQkGREQ
+EhUaHytHtqGZlJOWmp6qyDgfFxYYGyMtP8KnnpqZm5ydoau7XjIkGxYVFhgcIi5gr6CbmJiZ
+m6Cv0zEfGxobICs3a7uroJybnJ+lrbbKSC4gGRYXGB0lLUDFrZ+ZmJqdpLDFRyoiHhwfKDJ+
+uKyln5+enqSuvGw8LiQcGRcZHSY1Yr2tpJ2bm52jr8s/KyQfHyQrNmC+rqSfnp+iqK69czoq
+IBwZGRofKDVgu62knpydnqStv0ovKCMiJSszS9G4rKSfnp6jrLvhPi0iHBkZGh4lLkq+raSf
+np6fpKq4dTksJiMlKjJA/MCwqKKfnqGptt47LCMdGxkaHSQuS8KupJ+dnZ+kq7jePS0nJCUp
+MEBvwbCooZ+foqu43jssIBwaGRseJS9Nwq2ln52dnqKqun47LCclJiowPXS+rqWfnZ6gp7Pl
+OSceGhcXGR0mNWm6qqGdm5yeoqu9UTAnIiAiKC9B17mqoZ2bnJ+mtOI1IxsXFRUZHSc45LKm
+n5uam5yhrMBILSUgHyEnLj7ftqefnJudoKax4TIgGhYUFhofKz3Vsaafm5qbnaOuyT8tJSEh
+IyguPPG2qKGenZ6gqbdqLyEaFxYZHCMuScKto56cnJ2hq7trNywmJCUnKzZSxq+no5+fpKiv
+wEsrHxsZGRwfKTdfuquinpydn6WwykcxKiYmJysvPfa6q6ShoKKlqrPMOyYcGBcZHSMtPtay
+pp6bm52hqbxZNysnJSUnKzZRxa+kn56foaart2YqGxUTFRoeJC9Mt6CZlZWZn6q39zYlHRob
+HyxGzLWqoZyamZuhq7tVKxsTDw8SGB4oSrOelI+Pk5mhr9guHRYTFRsmOdOxpJyYlpebn6ey
+2zQeEw8PEhcbHi3EopaQkZOWmp+yPiAWEhQXHSc5wKadmZiZmZufqr1MLR4VERITFhsfLr6k
+mpSTlZaZn69DJBoVFRcbIzXTrKCdmpiYmp2lsc49JRgTFRQSFRon3KqhmpWSkpWcqcU5IBgV
+FhkeJDXGqZ+bmZiXmZ+ru2kyHxYUFBMTFyA4v62kmpOSlJidqL01HhkZGBkcHzPLsqedmJaX
+mp+jrMw2IRkXFhMTGR8qQ8eqnJiXlpabpK/pMyofGRodHyc2XbWkn52amp6kqrpoLx8dHRkW
+GR8lLDzSraGfnZqbn6auvWI0KiopJCUsOVvJu6+npaqrqq/HSDk3LiMfJCYkKTdNfc+8rqqr
+q6qssbm9xmpAPkZHQUpt7c+/xsvCyWFHSUI3Mzc0Mjc+SVp53cfG0MjByse+xcO/w8bGzc7D
+xsvMyMjT32NKOzQ2Ny8uMzU1Plrr3MzEvry/w8zp283e5tHU2M/Nx8DBv7q5vsXI1mZPQDo1
+LisuLy0xP0lV38q/ubu+vMTaz9Rm89Dj7tPTzcTAu7i7v8DJ3+b2Sj87MC4vLy4wOT9JX9TG
+wsHAvsfPys70e9va39PNx7+9vcHExcfLztTyVkc/NzIxLy4yNzxO79THvr2/v8Tcc2xfWmb1
+4dHGwL++vLu/w8XJyc7gaUw/OzQuLi4uMzpEWt/Mwr2/wL7J3eB0WFZVatvRysC+v7y7vr6+
+wsXG1nNOPjkyLi0vLzE5RFX9z8G+wsLAytve6nBuZvXh2s7LysvGwsLBwsPBwsnV+05BPDUx
+MDEzNjtDS1/dzcfHx8bJzdDU1djb19nY1dfX2dnQzcnIzMvIzNn9XE1FPjs5ODk7PkJGT2Pz
+5N3X0c7NysnLzM3O09ra3uv45tvV1M7JyMbFx83afmFLPjo3NjY2OTw+RE9s5tTLxcG/wMPI
+ztHU1tXd6/Du49/c0cvGxMTIzNPrWktCOzc2NjY3Oz9LX/LbzsjIysjHx8rM0NXa3ef6fv/q
+2dHMxcO/v8TK0elYRj03MzIzNDg+Rk5j59fMx8bGx8jJzNDW2+Ho933z5dnTzcnEv7+/w8rW
+81JCPDk3NTY4Oj1GTlz84NDIx8jIyMrP0tbf5vR5d3Xv5NjNy8bExMfKztTfd1xNRT89Ojk7
+Pj9FS1Zs6drTz8/O0NHT3d/f3+np4OXj2tTPzMrKysrMzM3V3uxdTEQ+Ozk6Oz0/Rk1Yb/Lh
+19DOzc3MzdLY2t3i4eLu8Obg39rUzcnHxcXJy8/iWUdBPTk4ODg7P0hNXvDhz8jIycjJzNLb
+5O9vZmBganTu39jOyMfFwsLCxsvT7VlKQz46Nzg7PD9GTmDw4drPzM7N0dXc3uD09/Ly7OPb
+2dPNy8nHx8fEx8rQ3npWSkVBPDk5Ozw9QktSXHvo28/Nzc7Q0tPU19zg4Ovt5eLd2NHNysbG
+xMTGyc/fX0xCPzozODo4OkBKUF3r0c3LxsPBydLNz+d2bF1aYWhea9bUyL67uLm4ub7N41w+
+NzAtLCsuMTY9Te3TxLu3uLa2vL/D4F9TSENGSEtVX+rOyb65uLS1uLm7ydlePzYvLCoqKiwx
+OEdt08C5tbKzs7S9xcljTEw+PUJBSVpxzsO9tLKyr7G2usDRWj44LiopJycqLTI/WdS/u7Ow
+sLC1tb7O0l9GSz88QkBKfurMvry1srOvtLq7yOVWPDcvKigoKCouNkJO4r+5s6+vr7S1vc/Y
+YEZEPj0/QUthfM+/vbi0s7C1t7nH0+9GODAtKCcpKSwzPkzoxby2sa6vs7O3x9RvSUQ9Oj4/
+P09p5Me/ubSzsLG1trzL4VM8Ni8sKCgqKSw2QFHawLm0sK2us7O8y89fR0I6OTo6Q1Re0MO+
+tbKxrrK1tb7N3U08Mi0rJygsKi06QVTPv7ezr6yvs7K8ztNeQj86OTs7Q0xV08a/trKzsLK1
+uMDM5k0+NS8uKygsLS43Qk/cw7mzsq6utLe6yuZzTT88Ozo8QUxe6cm+vbi0tbS0usLJ2GNM
+PzgxLzAtLDM2N0NY6Mu/uLi5tLa+wcLgW15PRURLSEZW7+7Vwr/Avby/xsbI3upyUE5LQj06
+PDs0O0E+R1hm39LGv8K9vsXH183YWHpcRVJQSlVb/ODly8jPx8TLy8vJz9nU5fD2WUxEQj83
+Ojw3PEVHUm7Qycu/vsbDxNLL3G7oVVBkV1ptZmf/3NLZ2M7U1MvJy83EydvX415TSEU9NDs6
+NTw+P01c4tDKvb3DwMDLzs/e5mZeZFZUVVRWX/7u5t7TzMjExMC/wsfM0NjuXFFFPzY0OTIy
+Ojs+TGrU0su+v7+6vsLD1dHbYf9oU2JWS1ZaXX3y2NPYx8bJwsPEyMvK1eDuUEZDPTU2Ny8z
+OTpBT+3R08S9vru4vL/Fycvvb3lOT1VKS0tYaFzf0NXJwsHCxMPEzsvN+HlrTUM9PjsvNjsw
+N0NETWjOxci8uL69u77Hz8zfYnBfUkxRXE9PcH552MzO0MrI0c7J0OPn5n1meXBUTEdIRTY9
+QzU+SURWZdnN28K9ycK9wMfOzOH081x3XE58XVj0aPrm/uXj5NLY18/Y2dfe3vxz+mhjUUhP
+PThJPjlERk1WZc7NzL27wcC+w8zS0u1cdHJPUmFbVlpnY2Tt5W1/2dbe3NXieu/p/m7z3N9c
+V9lbQGFRPEVPUU9U399yzsXTzsXI29/P22Jz6ltSXWZvS1HXUE7S4XzZ0N/+19Df7NjV/enO
+3v7S1FXy3T9LXT1BSUNLVlZu3fTRydnOytTZ19nt9P55aV5qXVNdY1li6Xz409jTzNDOy87P
+z9ji1Ntmd2tNSkZBQD9BSE1MUG/k5tvO0NXSz8/W29ji+frp4Hxp/Hdqdu16XnXvX2zp+vXp
+5/F/7+bm7fTm4Obx+HR39fn+bm1dVltZV1tbW1pbYGJncXL16OTe293a19jX3+nr6uT9e/1m
+/H997vvp4/r+cGJx8fr79vj1c3NwXlxnamZwfmhlbGloaW5yb29++3Pp3+bm393i4N3f3+Tf
+4fz3/mtpaWRfYGZeYG91eHx4bGx28en6/vh++OLi7ero7XlvbWxpZGprbX7z8O7l39/f7Pnt
+63tpXl1iYnJxam11c21taP7r7vDr4eLl4O9+9nz+d2p37PL67//68312dnh/6+32//7z9XJz
++3RubmxiXmZnYWFpcXVycXT56/Ht6unj3t3e29ja4uru8/txZ2/6eXz8dnF2cGphX2FmaGVr
+b2tpa29vfu/s6vR/9/Dv6+Ph6uru9mxcavjdy/Ph3FJP9OhH591s6M7f48ZcZsjgRlR7SVJO
+Rk/sXEnk0Wr209ndzdrtzNFo/NHyXOlpW/ZlU3fdW1rocGvf62Tw52Rw4nVl/flv6fdj+ufy
+9eLm6eR1Y2prWFxsZWhycW99///m3ufm3+fh525kdnZjbvlya3/7c/H58uv1dXvw+3Ztd3B3
+enLr8Hf27vf27+7y8O77/vh2/nNvdGz//XF3/WpncHR5eHx8++7q7e7p+nFuYmhsaWdv7Ph+
+9+/07+r//O/98uDsbvXn+XFubmtueHn27eze1tvg29rs9PhoXEY8R0xBR2ruz8PLzcTMb19f
+UVhVVGDXyc3Kx8rM3VxNTUlJT1Zv3tLT2t3hc1pNSk1SXHzb0s7Lzd3i/F1TT05UcuXe0s3X
+5u1dUFVZXGn13NbT1Nze6mJaWVpdZWz35t7f3+tmYm1rWlxk/uTl497h6e/0fm9x+e198+bt
+/vdyYfnranH5dm1wbWr07/t6bfp+aGVvamv3bHTzfGro4fHg4np/9GFk7+nh2d3i3ut8fPX1
+9Pr/fnhuZmReSz9JWl3t1NXPy9pkWVZWX2Jb/dnTz9fi4N3m9fj/39HV3d3X1dbd7X5pZE4z
+Jy5N6sy/vru2yUc9QkdZ3NnQvr3J4F1MT11aWGplZtjY/vPpffLpZF9vaGBvfd/EwNNvc9vV
+4+/j3udrXDUnOd/izsHNzb75OTtITfTG0eHFyu99VUVK38fW1MjN1ttlaMa9xtXW0dPkZlFM
+RT4uITTIz8a6xNzA4jc8YGbevcjvyM1PT3tnbc3mR11kUsfB6s++zmdjTEnYxs3My9XS0vTu
+6WVjWTgmHirXyMC0r7W43jU1SmD0ysjQvb1jSmZb/czrQUbd2unSxsC9z0RBXHLx08jAu7nI
+VklDOzEoITW+v8W8uL2/3jw+YFZGWe7av7/kdtFrQmzUUFDO0Nm+v8jAx08/TlTrwr68ucH8
+Vk03KywuJyvGt8m+s7zLykY2Wc9aUsvKzrzGXnTfTUx4RzzhvczMu77Jy2RBVOtV/725vsHO
+6HY3JiUjJ1K+ycCsq7rHdkBGXj42UM/MyMHIytZPTks7P+jH0sOysLnWS0Ng3mte47+yt81Y
+QDYuKyMhPrvBx7evt7vVP0BwUzpB/9DDv83hzb7PPThS2f1a7b+xsr/v+9t+YFpsybzH7Fs/
+NzYpHCjN0FLGrbG0s9FR2MtFNUNMUtjIfWm6s+I9TtTuT01xxbq+zsvBvsV3SFPU13RvUkZD
+NSQgN1pFT76xsa+1zuXK2j04RFhp5udO8La8bXvMzOB3XFzWxMzc1MrHz35k28TG31E/OjQr
+JCo8SE7bu7WzsrfC0c/kS0JJSkRNWV3azOTqyMHM1MvLzMfP5drMy9pmU2DX0vpaT05NPC4u
+Nzg3PVXayLq1ur6+wthiaGhZX+bxY+3nXl7z/ltv0M/RycXFyNLzWlleWFZq2crP41tLS0tF
+Ozk/TFNUZ+TZ1tztcOnS1+rdzMrT3/tm69xoU17k19HV18i/wcra6+vlel9t5u1cSkJDR0I7
+O0FJUVpeZe3b2dze2c7N4fPZ0d7l3efn5eL7fdzb18/JxcrKzdzu9uRzYV9YVVJIQkJDP0FI
+SUxVaff46uPWzs7R1dja0dd0buvc4e/y++3h3u3k1c3LysjO0M3V8mBhX1NNR0RDRUI/QkhN
+Vl9kYvni39fOzM7O1Nvb3O/23t7q49jc7ODc597Y3OPVzNDSz9z3eW5fUk5HQkA/P0FHS01W
+XGBr8ubf2NPT08/T19zj39nS19jW3ezq3N3c2tjT1tTS1Nzg4vZmVU9LR0RBPz5BQ0ZMTllf
+bu3n28/Nzs/QztTW2N/j29re4+Lm8Ovn5ODc1c7NzdLW2uj+b2BVTkdGQj0+P0FCSVBSW/Dg
+2s/LzdHU3OXo6vvz4djQ0tPR0NHY3d3i3tzh3Nvf4uZ9al9bVU1MUFlWVVZPTk1LSk1UXGJx
+e/Xt7e/k3uLe19XVz8vMzczNztHY4O7x8Hx+7uzn7HxuYGReVlZWVFNZXF1eW1pbWVdPUVtc
+Wmj+d/rs7Ofh4eDc2trY09LU1dfbYkf+2dbozsnEv87d49pqUk9LTk5LSktVWWFebPzxeWpr
+Y2ZoXVRPTUxPVFtf9+nb0MnHy8fKxsvR0tTX9ejw6u99aWp4XVl2dVpYYGxlal9ra19jXlxa
+ZF1e/m5xaV9TWmZub2F44N3r5dzX2tnW2NbX2d/f3+X79enl+e9v9fBjYfPoWlheb2lbVf13
+bm3v7fDvYWpsfVpk+Gjb6efv5mlfaXXx/mxr5Wpi7uv14+d87+nk897b1uTp3eb4eOll+nJ9
+7O5q/3Zdbf1mX2hqbl1ab2Nybv1gYWZtbGt46t7k5uzt8W3p7vPv8/P06nbu7tzhfGRt52d0
+dd33but4bnboeH117vBp63b1ZF5UbmhkeF9pYn5sd2xsYHjle/H5+fvg5Nve6eP07u7f5O35
+6mRu5mxw7fFlb2Bq7vHwdnxsZXBjXu37d2xe7+3fdHzw7HBc5/FpZuzp/O/z9uXnfnzxbHt0
+ZnjiX2re7Xnq/Vjm+Wxx3eBtfnrpamFo4etocdbe5uti+Xz+bmpubPhkb2zd8Gh5ZWBmZmvo
+b+zj6vfn8nL8YXl9bnfu5ez5ft7x4Gl2d+fw7u7v3W986nXv/mxnc31uaHZXXG5cWF3tYXF2
++OL07G3l6/DubOve3vff3+B96vHq42Z1cnds6W1u7l5bXW1f8G/u5fTpfPZ0b2pob2pzb/vu
++Wjv6uJ5++fp73bvcHX8f/1x8Pp0ZW3o63194+Xubm3y9XPu4u78aGxq8XX7/3x2cGNncXts
++/NvdPj+7fnu6vL38PDf2OVvc+z4amv/9/toe3R7cvzf+nvsfnhyeHfrbmnqdXRgb+r0fnvp
+d/hq8mj7del7ZOno6PLxaO13/HV/3/HnbvV37n37bPxve3Rs+GnyY2v48Ox+/3V2a2/weHDu
++HTv9vvv5u776HT67Gls+PlrbfXxfPZzdvJyfndx/+1n8955aPDy9vl7+W5ofWtvaHx3Yl7/
++G72+Ozt2v7z/Orr+u9x5G75Z+9uaGz6emznePp73293+ed0ce7n8mtudW77fG/59mhr7W9l
+7vB//fdf++ht9Prk5+/r6XNs8XFocOLvbmfq+Hdnb/968fTv8Wtya2pp7X7883l93Pnu7PX0
+8ulRfXlpdO7x99p16+3vbfXk5X9f5Xn7VvJ4YWlgbmf8X+vf5WFx2OTf+3fz8ml76WZqfO1l
+aG5j5+ds7dt9/m1mY379Yn5tePfmbuXn+vvr9/Rx6ONq7/duYV5oe3lu3/pzdX17b/pc+2/1
+aelod9p6ed/gZvXl/3ty8Xv3b3ZuY3Tr8GJia+f1fnDk1+Tt7edx9Fpe6G5fZHxr5PLz1Nrd
+6uRqdnns2/t+X+5uXVVMV09PTVdQTmJg6+3SzsnDyMvLwsjI0MjHzcnGx8bEYDYqLC0qKSou
+PdrCt66rqKaptchVOjItKiovO1bMvLOrp6aoq6+2v9DpSTEqJB4dISQnM0vIraSin5+mrbtP
+LyklIiYsMVG/squnqKuqrre7xNHHvcdSODAnIR4cHiMtQcSvpJ2cnaOuyz8sJCMhIy1HzLSq
+qKeorbjE3m/33M+8tMB1RCseGxscHyg316yfmpmbn6q7SisfHh8iLUfLs6mlpqqwwN5wVVNh
+2r2ytsRfLB4cHB0fJzH1rJ+amZufqr5EKx8dHyQtTb6up6SnrLTJX05MTubFubK2v3cvHxsb
+HR8pOP2wn5qZmqCuzz0rIB0eISxYt6ynpqmsr73qT01W07q0tbzORSofGxsdICo8zKyfmpmb
+oa7QOikfHR4iLEy+r6mmp6qut8XNz9LHvry9yF8wHxsaHB8pNUi+ppyZmJynul0xJB4dHSEv
+Vb6tp6emp6uxsrjBwcDIz9tLMSAaGRodKDlVxKygmpeYnq3aMCIfHR0gKTZ7sqiko6Wpra2v
+s7a8y+xPOSofGxkZHCQ2+7ipoZyZmZyjt0EnHRscISw6XMK0raekpaeqrK+ztrzKVzIiGxgX
+GR4nMVS7qZ+amJico7NQKh4bGx4oNl7EubGsqKWkpKaqrrO800QsHxkVFRgdKDrhtaiemZaW
+mqG0SyofHBseJjZXybiyrqqnpKOlqa650kgwJB0ZFxgbHyxGw62jnZqYmJylu0EqIR4fJCs6
+Vs28ta+rqqqqq620xVY0Jx8dHB0fJCw6Y72tpp+dnJ2grMc+LCcnKi84R+fGvLe0srGxs7rK
+WzksJiQlKS00O0NPbtG9sqypqKmts7/eTz88PUJMad3Qzc3My8vN31A+NS8tLjQ9T+rOy9Do
+Zlxj4si+u7q7vb6/xs3X7HVoXF506d7d525YTEQ9ODc5Oj9MadvOzdf9XVlm8t/WzcbDwL69
+vb7Ez/ZdWVpedOnm9G9dUk1IQz89PD5ETmD02dLa+l5YWWrh19TLw7+8uru8wMvX729jYWFV
+TElJTFBVU1NQSUVGS1Jm6+5sYVdNSUlMVvzPxr+7uLa3t7m9xdD1U0ZAPj0+QkZMW+/e3dzb
+3uPj5/RlU0g/Ozg5O0JS6su+uri1tLW3u8PVcFFHQD49P0RNXHbs3tnU0M7Ny8vW7VxLQT09
+PkBHVGze1MrHxsPBxMrR5WtXUE9QT05UYHzg3NjX1tPPztTX3nhTSUVER0lOWWj46uDc19PO
+zc/S1NrzZ11dXF5iXV5p/e/56uXl4OTs+WxdWldVWGN56ujn7PDp4t/a1dXV2eX7b2ZjY2Ng
+XFtdWlpfZHTq5ODf6X1pY19eYHHx59/f5e3v7evp3dvf5ez58u36cl5aW1tZXWJu+e7k397h
+6fR9dXt5a2hpb2tqaGl79/L37+zk19jc3tzf+mlcVFFQVFlfae7l4uLl29vl6/Nwa29udG5p
+b338eP35cfvt6+Ti6et+cnZtbGZeX2VpZ3bx7+no6Ofl4t7f6393/vt7+X5vbGxwb2xtdnVx
+f+nuf3h0bGFeY2Nka2tvdurj397c2t7d5up2fO/v6vL+b11V4Pph+vLscP58/nhvampqX11g
+W2ZobPH44t7f3N7tU2HT0OLf3N7p8uPqdlxYW1BPZlJd09TL3mr49MjfSz5JbF5NWtHf2Vvn
+x8nJz/Dd4WDqbV5eUl59Vl95WO7nY+nj7+n5bWltXl5dYnNpd3rq6ejb5tp75fDh0vnvbXJ8
+7u/w/f93cXh4cOptX15f5l9Va1va3l/4cO7dXePxYNTxeNtbU95599jx7tBd4N5o82vq7mVp
+fFroZX97YXP+bmP4Y3Hi81/gedjq+dpkY+9aX+h+5XZz1/7o4mDv2nDt3/reXm5sXl1rbHhk
+YnVg7uDm5Obo3eb7emhoaPDmaH58ZnVoX/p+8OTi/HTuX/H0/fb77W1jdn556vjq5Ozq+H7c
+/XRnWPtgbnJdbV1Rdf3j0+PU53X+bevu9XroaW1ZZNx0ffFibuVj4ujl+fnfdP5kWV5lVnbu
+dm/27eTs2tz55ufn6Gfy6GJiX1xX+G5+6+3i3nzl7l7hb19oZvdvWt7fWNzifu3vbmzveXZr
+9OlmenV1bfN879/kfl/kfe3k53nq53Z9+f9ienJfXWNddmxw7eP65tzi6fTe3X5gdmxvaW3+
+7HZp61/s3l/9bmbsat93deHh8O/Ybu3V1uLn4fl26mVuWVFMRUhDQkZLVWvfzs/KxsLAwsPH
+ytDS1+R338/Sy8bE2Uk6LSgpLDJGV2Tf3M7EvrezubvBz9zvaV5nWXfp3c/EvsG9v77H2j8x
+KSQlKC46TW3Lxb61sa+vtL7L/FtTTEpLTk950766tbi3ub7GeEIuKiUmKCw0PUx3x7uwrKuu
+tsTpWk5LSEhITVN107+7ua6trq+0wmM1JiEeHiImLTlOzrCnoqCkrb3bSj45NDM0N0jivbCs
+qamnqauuv2AzIhwZGRsfJC1MvKedm5uepbDIPy4mISQpM0/JuKymoqCjp6uvuddFLR4ZGBkb
+HiQv7q2gm5ubnaGrxjgjHh4fKjRFxLCqpKOlpaesrrbjPjgqHBoaGh4jKD3Gr6KcnJydoq7P
+NyUiISAqNULBr62no6Wmqa+wss1JOikcGxwbHR8jNcy1qqCdm5ufq7reNSooIiQvNEDTvbKo
+p6mmpqiruN9dOSMeHRkZHB8pPF3CqZ6bmpudoarCOC4qISAkJy9DfL2spaGenqGnr73sMx8c
+GhgYGR0oNEi+p5+cmZiZnqi2azsrHx4gICIsPc+uqaSenqCkqa/GPikjHxwZGR4kKDH7s6qk
+npubnqGotts5KiklISMqL0DUvK6opqamp6y63z8zLCUfICQkJC0+Tti3q6Wjo6OnrbfI7UQ0
+MTIyNDpEW9/Bt7i3r7DGftr3NSw0LycoLjc6PXK8ur2vqrC0r7jBxelaUz4+TkpN9dDLyL+9
+w9L41nk0Lz0xKC45NzxM3r/Cw7SywLyxw9PE1V9mWFJcTVXPztC+urq6wNbe+TkuMi0lKC8w
+N0RqxL2+sq21tq+6y77KX1hLQUZIS/HQy8G6tba8v8xePDQwLCkoKS0zPlXTv7myrrGwr7jC
+vshxXVVKSExXaufQxb68urzJ2+VGMTEtJygsLTM8StbBvrKtsrWvtse+vt9tb2JiXGfm4N3L
+wcPBwMr5W0Y1Ly0oJyssLz5V7cK3sq6vsa61xcC/7VpyXlZYa+LZ0cS8vLu9yNVaPzcuKiko
+JysuMkBn1buxr62srbG3usDXcWFaTU5bZvfYzMS/vL3G1W9GOzctKispKS4zOU3bwriyrqus
+rrC2wc3T+1JJSE1TYd7RzMS+vsbR2lQ9Oi8qLC0qLTU4Q+/LvLCvr6yttbW4x9XuVlFPSE5i
+YeTJyce/xc7N+z9BPywqLywrMDZAXm/Nt7SzrauusrW5vdJdXlxIQ1BYU2Xp0M3Q0s1+PU1K
+Ki09MCs0PkFeXti2tLevqq60tLq+y2xmdEdESldVT1521tnu183gZ00/TTwrMkI5MjpQb2Zg
+zbW2vrWts8DAvsrkVWT0WURMeF9TXtzQ22ttz9dWU2JSQ0JJRUdHRk5QSE7x59bPy8i+wMjG
+xcrP325y61VQYmBnam7q8mFvcGxlfHre1N3n2eReZ1JSTk9MTlVMT13l+nDZ5NbU4NfS2HnX
+3/p1/ellfVpqz17V0MjQ2dfe/l1mTExVSGNWTdxKcNJd7+br+PVs3GD+6lv4Xfv2delt2s7W
+39fR5f1sX/NPWV1S3Uzy5H7fbNrd7dvy1fTy8W58YXdoY1/6+unx4u3lfPP3XHJPU1NOUU5Z
+/nPw1crRzdPVzt7b0uPo7u5fXmdfbltpZmX4/tnn8d7+XltWVl1XVFtjZW7f4OPa1tfa2eLS
+4Ph4bGhcWVdXX2lfbf36bWhdZv7e5uvuanx5/2p89Pfh6uHc3tTX2drb3Nfce2dlUlNVVFZO
+U1xdVU9XbFxhaunpceHc2dTU1dfX2enq8XB539rj3+DjdWxnWl5qbXpuY11fX1dZaVxXaPvu
+8nny3vPr3Njl+N7b/O3l4OHi4X5mZWly+XH+9HRzaGXl6ePvcvZ1b/78eXfp4/318/toe/F8
+cGF0cW7z9vR1cfl/93Nv/Hd69fv1+nZ1a29qafLq8nl27d/a1tzb2eLg5fX3c2x0bnJrX1tb
+XF1eXm9vdGZqdf/o4+fp4+Xl6OPrfHFsb310d3J8/fjt9PV4fGpkaGdqc3307u7x/n52bmlw
+eu/s4+fs6Ofq7O/5fvF9b2188Xd6cXNrbf7ybGlmZWZka2ttdPv67d/c3+Tn7Hd4bmVmXV9o
+a29wbHJubXPz7Ofh3drc5O3y9fv6/Xd5/XpweHN1bW5nZmt39+vtfvh+am5paWxtcHB8e/zp
+3dnc3N/h6Ovz7fpubHlscXVjYWNra2lfYnJzcnvu4Obq4+Xo7ezzdP309P5waF1t0v5f293t
+421h6G5eXF5cW2F5PFm8XFzKyWzJ2ObJeWJsa19bSVBcZUnZzffUxd/eetngT2xQamlCTv1a
+WW9pYdzR3t/FzezV4nrOb0/x+FhaY/v+805W2u52W/na52N43NrZV0phUFVT8tvL29nW1dxv
+7trV9Vtr7FdTTeJuWudm3Wb8Xmd+Xm9wf2jr/urofOrn2+jb7f185HLb5e110GntY1nu8Vts
+6Ofn8HPn7fpY5vdtdGhd5VRNVVpNSVVNb2NR+OHT7tDJzcXTxcnKzczY0tnY9c7kztDv3047
+QEU1LjU4OD1ETtC8ysG0sbq/wsXN80xSVkhGTk36z9LKvrq7v7/Dy91PPzs3MS8uMDM5Pk3j
+yby2tLO2uLvI3+VjUkpISEtSWmTl2dnNzM3Jz9XV1tzwXl1SSkRCQD8+PkJLWGHs2dLOzMjI
+ys3T3eTucGVfXVpaX2rv39/a08vKz9LP2N3uZF9WU1BMR0VJSk1PWV1e/uTc2tjW2t7i7OPt
+cGZjaXzv+Ozg3uDY29fSztPd2t3e62tvXVlTUk5LSkxNT09abXFvfu7o3t7e5ebs//j3dvvy
++evdz83Oz8/NztDT2uX8c2NcWVRQTEpJSUtNTk9QVVts+fbv3Nra19bW193f3drY2dzg5/jv
+9/vy8vT+bnF+5OPo8mxjX1xUT09TT1NZX2l779/d2dbV3uTv/Hdxb2ZqbHB6897e3d7m5e3e
+3+d2bHNraHBtcf/v9vrr7fL3d2dlYV5aVFZYVFdfZmhsZ3Ry9d7b3d7Z2NfY2N7j4+jm3dzh
+7ejq8+jg5f98/fV+bmJeWlFPTUpMUVNUWF1nfPf27N/i3drd3t7d3uDg3t3r++nn5t/d5fTs
+3+nu6ejj6Ovrd298bl9XUlBRUlBPVVxcZG57fnzt/n/t5N7h6eLd3N/g4urx5+Lu9Ozp7e/t
+3Nna2tnX2drd72paVlNMSklLTExNUVtlcP3/+vPn5fTt4N7d2NXQ0tjc3Nt9TtrC5ljky83R
+1dPS3fhmXlRFQURDQkJERUZMWG1sf9bQ2dfKyM7Pzc/W3efr7en3dfjy/efY2NHO1M/HydTW
+23tNQkc+MjM6ODQ6TVpT9MvGwby7vLy8xs3L3WBfbVdKTl9cW/DZ1s7EwcPDwcTP315HPj00
+MDY3MzdDS0plysXHvba5vru7wM3d63pVS01OS0tZ8eLZy769wL67xNbbfkk7OTIvMTAuND9D
+S+XJyr+3tre2trm+xc7zY1NIRUZGSFN34dTKwb29vLm7ytTaTj09MiwvLysuOz4/XMvEvrWx
+s7Kyub2/zO9bTEU/PT1DSU5c6M/GvLa4uLS3xczXUT86Ni4sLy4sMz1BTt3Evrq0sbOztrq/
+zNh6TkZCPj0/Q0tZ9drNwbq4t7a3usLP/Es7NzErKy0rLjg/SGnKvbexrq6vsbW8xtJ5TkE/
+PTc6QURHV9/NyL+4t7m4tbnJ0N9BOTovKSsuKiw4QUvgwLayr6ysr7K0vtZ8WkY7OTo4OD5M
+UmHXwby7s66ztbS5w95XPjQvKycnKyssN0hV1LuyrqyrrK2zusDcUEE9My40OTY8WdrLvrOv
+r6+tr7m9vtJLRjsqKisnJSkuMjxbyrqyramoq66uu9hsSjcvLy0sMDtFa8u7sa2tq6uvs7a/
+1/ZaQDUwKycpKiUsPz1GwbKyrampq665wdRDNzkuKCw1NDVZv764ramqra6vt8XT3l1APj82
+LSssLCwqMldlarusrK2rq6+8z2NJNiwuLysuP0NJxrOxrqqqrK+5vcHjXm5dTEY/NjEuKSoy
+LTFkzMq4rKysrLS8v2Q3NzMsLCwvQlRNzK6sr6uoq7G7wsHQVVBpWEU/ODMwLCkrMjU+8r+1
+rq2srK++2mtDMS4xLSw1QUrkvbavq6utrrK7xM3d+2lbUk8+MS8vKygtNDtN1LuvrKyur7XG
++0o5Mi8tLTNESFrEtrSxr66usbvAv8tuXfN6T0Y7My4sKyotOUVczLavra2us7vKX0A3MS8t
+MT5HUte+uLOvr6+vtL3DyulqZ01JRT06My8yNTU7R2TZxL27t7W6wMnoV01EPz9APUBKT1r7
+0cS9u7m4uLq8v8vfa1RMSEhJSUtLTE5RVFFQV1tbXnbu7O/v8HhvZ1xTT1VebfHf1czGwL28
+vsDCxtDkc19RSkpMTlFTVFhXVVNPUFZZWl50/fvp5vH+a1hPVl1cbOvf2M7NysfGx8fIzNHY
+6GlkX1hYXl9cXnfv739fXWlcTU1UTUpPUE1OUlRbcern08zOy8nMy8nN2NfT2efp6Ox0XFtZ
+WVpicm1ven/9697f5+x6ZF5bVFFPTk5OT05UXm7+5NbS1tTP1dvb1NPY2+Dm+W12+3ht9ubp
+4uHd2+D1/PRwZmRmW1lcW1RRUE5SV1937/f76Of08uXl5N/g6O78/Xzw8fjn3t3d19ni393l
+f3R+//P29351aF9eWVhYWFZaYV9kfPP2fHV+a1pMT8TAT2HM1dZYT83AXj/1t9A5W7bbQc/J
+ZFTb7Uffyz4+vPZA3dZCWthL9XpAeN1MRey16EK7vmxazlRPvlI6y75IYMHgdMX7Tm7PYTpo
+z/pITerAUzjA1E1+fE7ny19HWr7vPdnE5lBc4u9j2OBhx8xS38Zi09RUTdPDQEa/6z9/7lBw
+WXh9UPHfRG3EXFDS01tO6tPuWuDZfeHib2Ha127l39p4ZH7eb9p1TvXKUUnS21taVfvTWUzu
+3U9V2+BV887pUl3b3E920vHVe1nP1l1YzdtN3b9QUsbcT2bcXVZbW2ntYW75XWnSdkXfyGdU
+bt3acGza4F9v3t5c+upZX+vk2+5fc8/NWVbb0OxdVHbSWk7c71fx4+7qZnZzdGlu62NYbdrt
+UVzS3FZr1u7t2GtY1s1dSfHG505m1t9vX+/cZF7b32do7eFRVs/OV03p0ehcX/je6lxb8m1a
+/eNuaPjr4OPlb2fj7Hb3fGp74u9ZX+/f52T54eTi5GtTcN5xZGppbnV1bH7r/v/j3vVy/eXv
+YWjn7WlubWv06nD26uzs4u9fZu/2YWn35/pr7O55fXH75+Pj92R4fu3o5uX3fO7e1OtVUllr
+Wk5OS0pUa31kcOPYz87Ny8/OztLvde1wduDb6OPQ0NfPzMvJ7TosLDQ8OTM1Ss++u7a0tbi5
+ur7gQjc6RktFRVjcysK7ur/M0MnF1VhKU+3UzM9xOiwrMD09NzdG0Lm0tLO1usDBwt1FNjQ7
+SVVTUWTRvra2ur/HyMTL32dXXO7OzHg8LCcrNTw4NT3vu66srK+3vcDF2Eo0LjE6Q0ZIUt7D
+ubGwtr7IzNHca1RUbN/Ny9doPzAqKi84Ozk+Y76wrayusbe+y/9JOjEvMzs/R1Hbvrawr7G3
+wtv6ZlVMR0xu1szJ0fFINCwrMDs9Oj9+vLCtrq+xt8PnTz0yLi41P0hMXsm0rayutL3O62RW
+SD49SPDEu73H0144KygqMDg7P1+/r6qpqq22yGJDOTIuLC43SvLNvbOtrK2zv91OQ0A/PTtG
+4L60sra9ylAtIyQtODg1Psutp6eprLLHVD83LyklKTVgx724r6qqrbO/bkE8PT08O0Hru7Cu
+rrK93j0oHh4lLTM5XLWno6Olq7f3OzEuLCgoMFa9sK2rrK2xus5KNjI3PkBASHfAsq6tr7bG
+Zz8rHRofLkdt0LSmoKOor8VDLCgrLi4vPM2uqaiqr75rSElFOTI6Y8vN2Mu/vb26tbjIXk9W
+PSQaHTFt9ea7qaeusa+7PiYlNEo+Ol+3ra2urrh0OTY6OzpE88O9v8XN2t3MwL3CycjGze9D
+JhgZKF3JzMGvqqqpqLoxICQ6Xk5N07qzrair1S8rOlRKPU7Hu7y5uMxGPne/v9HXzcfAvcZA
+HBEZOMPEvK6prKuiqD0cHC7pycS4u8q6qKnzKSMtQ9y+t7/dzLOy4Tg2R9m+t7zkVsyxt20t
+GBAbXqypqquwu7Wu2SUbI06wpqayTzlgvck9LC1Avamqw0hG9cG90kk8Tca6y1VM4rqvtvYq
+FBAju6moqa/M57i2RCcnNsWpoatPLDFTx7/RRjdesq/NOzQ+4LKswj41PVDbxL29uK2rvjwf
+EREkvqegn6jPTdrgOi0xSsCrp7ZHLy8857y84UVI2cTaSDc5yKmqvUUtKTXEq6qtsbnByl42
+HA4TM66dlZizMC4+OjlFQD/EqKi8TTMqMM+tr8dNOjdB+c3FubG2zkUuJy7ZrKOiq9BBP0VI
+PiITGkaqm5adTx8jNVXFvlk/zKyqukMpJS/Cp6e+OSouVbisrbvaZEs7Ly9Bwqqipbo9MTxP
+0L8xFRY0sp+Yn0siKUPhwc09Nsiop7VJJh4rvqOivS8rPM2uqbNiQU5PQjo2QcCoo6vQMis5
+ya+zMhQTJsafl53oKCo4W8XNP0C6p6m3RSQeK8aln603KDrPtauvYDg+Qz0/S3+8qae0YTMx
+ULmxxikVFiq6npifbygmL0vHwOXTsqyuxDciHy3HpJ2m3i8tPOq8udFIP0VMTmrLvLKvvFg7
+Pte0r7wyFxIeXKaZmq45KSs46MLP176ztcFHKSEq9qecn8kpJC5Rtqq1Uj1FTVvp3urHsq63
+2lBOXWo6HhggP6+bmKdDJyUu7rG2ztrQz8bRPCkqQrein64+JSMwzquotNk/OD1Mcc+8srG6
+ytpVPTssHBwuzamdnbE6KiowWbu5ycvDzOJgPS8zUsKwrLhIMTVD17Sttc5NOTVC3ryvrbTC
+1GJENiIXHDW+opmdwi4pKjLivczavLa9xGgyKjFawbSwvV5MV2TgxsDFwsb8QTxJ27WopbBR
+LB4UFSbXqJuYoeEuJSMt+7W1urrAxsHWOigmNNCspKrKPjY9YsC2usbMzedWTFTUt6yw1ywY
+ERYlyZ2WmaXLNCYmMEvaxr29ws3+TURMbM+/u7zNXkZCUc+5sbTFX0A7RGbY13BSWP1YQ0U7
+Li46WtDAu7/L0sq/v81TPTc4RG7b2Me4r6ystO02LC06/by1usbW3OZfTD01N0brx7/C0l8/
+OjtARkZJVnzYxr/E03ZZWObKwcTVaFl91MS9xHxIQUJO7srAwcjX/FtIQEJIW9PEyf9KQURW
+eG5VSkVP2L67wdNWRElrz8nO51hLT2bez8vM219KQkRfzL24u8hiRUBBSFb62dHNzuJaSUVI
+T3TSzMzQ2OtpU0xRcdPFxNJZQD1EWtPCw8/3VkhHUu3Nx8XL22dSTU1SW3zdz8vM4VJCP0dm
+y76+xttdS0hNV3rUysjM3lpIREps08jI1mRLSE1WX/vwcG/+49/h3/5kbe7l4fdcUlBb/tLG
+xMfS+1VOUVv40MXFzvhRR0VLXvXf4O9oYmpxdm1jVVJc/d/V0tzweHJsb377f3T35d7e4ndh
+ZXvh29rb6O7s+mpbV1dZc97X2N97Xltia25rXFxp59HLy9DvW09NUFVfb/fi1tbhdlxRTlJd
+ft7QzM7R2/hpXmBkcuff3tvX3O1+aldOTU9WZenW0tXd5XxiXFpZYnzu5N3b4O1vXFlgbnvx
+5+nn5OTqeGZdXmJu9ejs7+Xh4+twWVBRWnrd09DY5fT89O71/Xdubvfs6fxpYmBiZnvr5ejv
+831teO3ya11bWl925dvd5/J3bWxqaGRk+9/c3u5oTFHPzez219ni6vHtbVhYafv6e+7i7O/3
++m9eXGJnbH30+fns7OzzbGJo+u37fnJjde3l6+7ufXJ06efz/v15bW7x625ne+Te+GtrZ2pv
+7Ofve/vxcW726v52dXXx7u//bGRqdH3v9fXt6Ofr93Zxamhqamdo/fbr6Ojj7vH5cG1sYGpw
+ev3x8nx6bnzu6/Tj5nJ05d/v+mZfXmd+b3Pw6uDl/vd0XmJr/Xvu7ejv7+Tq/W53anhnX3js
+8vjz7/Vwc2Zod2ZaZuzv49vj/Gttc378/enf3t/k5ufpfGtqcunrfG1ycW5walpbX2l5dHN6
++Ojj6v1oanR4bHXo6PHu6PNxaWZwbWv8+Xzz9nl2aGZ27ODc2tnY19XT29/d3dbR1uDf1s/Y
+Z0MyLS0yODo8RWDOvbOur7a8wcjjTz86Oz1HZNfPzcS6tba7w83Z+WpiW1dbaVxDMisqLjU3
+OkNjzbquqquvub2+yl0+NC4uNUVddOjLurCvsLW9zNzqdFlLR1Hgys5iPSwmKC86NzM85bas
+qKeqsbq7vNg6KyksMTc/TVzcuKqnrLa+xM3jXUg9PUvbxcbW+1Y9LigpLS8wO+27sKyopqqy
+vMLvOSsoKy0uNVHNvbKqpqmvt7/cS0BCQT9FXNvKv7u/7zkqJCYrMDQ5Ub2rpqWnrLW/00sw
+JiQpLzhAbL2uqKWmq7bF3FQ9NTc8QU3kwLi1tr3iPiwjHyQrLjZftKekpKWpsMRfOSkkJiwz
+OkjNsKilqK21v9dNOTM3PEVc2si+t7G0vdZMMSUeHykyN0y6pqGjpam13UEzKSIkLDlH1bOs
+qqipsL7PdT4wMj5JSl/IvLu3sbTA7lhDLSAdIS89Pm+voqCorbTGTzUrJygvRmTXuaqnrru6
+wGM7OT4/Qlbt4sy8uL7Gv7zG6FVNPi8nIig8RzlMsaOpurmxvU00LzA1RFRN5LSttb69xv1f
+Sjs+WfBWWMe91e3BtbvTzr3FbkY9PC4gIDlYLzC4oazJtaeyTzs9OTM0OT5quLG8va+txD06
+T1A6N1LKyc7Fura7wcPJ2PZlT0hNRDMnJDNBLChirK6+rqGnveRrSzYtKy9FzsvBrqitwtfP
+SjAxOj9Jbcy9t7a9xL3DaUxs3FVN2t8+JyE8TSAixqu91aidq83N5Ew3KCYyWX1MwqaosrW4
+yFg6NzxFU0tXzsHE08zAx+Z218nTfGZHOC4lKDQwLU+1r7Sto6a3zfZLOCsoLz9TXs6vqrK7
+trruPTxCOzU7U9PN2MSzsLm/wcPLYkVMRTQtJSc/Oic6srHAs6Wks8rO7Uw0KC5CPTVOt665
+t6+0vNxITlA7N0BTWVzXv7y6vMK+vdxc41YzLy4mKzguM8y0vLSopq26wMlpOi0sMTMuN+vJ
+wre1s6uuysq93kA7PT47OkzYxb+7s7C5x9RwOyklLTAkJkfT9cazqqartrGvxEs+QDswLC46
+VFZNy7CyurWyt8BnS15NPEVYWmP97dTL5fHQ3FlNS09IOz1TU0dU6N7X1djQzN1mb+Z4XWT+
+2snGyMjIy9XyZ25dTU5SUVBUXm997tzc39LLzc/P2+L2U0lMTUpHSVZoa2xo9+B9XF9tbGVf
+bOPT0tDLzM7S39zZ83vs6OxwZWxqZWdpbfTw69TS3ebd4mNQT1BKR0tRW2ZgWmn6eXb+6dzd
+7t7Nztve297d4Whx80tR2O/axM3Xz/d54VpTXGb/1+LkzdfubEc8Qz8xM0FGSGXcwbK1vbWu
+ucnM3mRHNzU5NDQ6Q+TGybuusLe4vMXbTENLQztI+X/zzsnU+j85XDkmMU0+PebEsK24s6mv
+3Hv4TTswLz1JNznJuM/CsK+3x9TBzj08Z1M+RWDKw9zPub9TPjU1NicmO046Wbatq6ytqKrL
+TFNALCotNDxDWsewr7mxrLvg8V1IOzRCc01Pxru7vcW+yE4/MioyLiU22Vpvs62urba5s9o5
+PkQvLTpFVubMvLOzvL++zFxLSkc/QE545s6+vb+8vtbsbks8MywyOisx0c1bva6ytrzCvNc7
+P084MURcW+LMwLm7xb++1nB4XUtLT1zs6tvFw8zHw913/ko9OCwvRS4q2MFI0q2yvrq7vMdL
+Rm1DLz5zTEnWv8nHu7/Dw83W21ZOZFFLbOft1cvMztPT4lZOQzEsQDknQ75IQbSuxr2wusrl
+XGRONzpuWj7yvt3Zt7zSxr/V/3pTTk1JWH7858vJ2MvI3d/qUTsxPUQpLsxZM9KtxeKvr9To
+yOJJQkFHTUZPzc5sv7fVz7nHZdbZS0ZYWk5a4tXW08rH1uvpaUk6OUQ5LUPeQ0G/t+vPsLtl
+2b9qQlpkSUlUXfPx9snD0MnAydjc23teYl5WXXRvdu3o39ro8+n5Sz5MRTQ8Z1E9Zr/S7r+7
+z9zJzW9YXWVdU17l6X3fzsvR1cjM7v7uXk5SWFhWXGdket7e4NLN3uvX9kxGTUo/P1BcT13b
+ztLQzM3X8f51X1hbW2Tx4dzTzszP2tzf/mtqY11cXGV7/+jZ1NfX2ODob1xgZFZMTk9KRklQ
+WFz539zZ29zc2t3m7u7ud/Pk9/ze3Oz27fpmX3rq8n7t5ff02tnd39vhfW9ua2BpfnFnZWNa
+UU5QUVBZZXH26Nzc3d/c3+fp8Ht38nZnbHt+a3Xe3erp3Nnd2tzl7+vm6vb+fnbu5Oz7dnN3
+92pVUVFNSkxMTldlfebc2NPQ0tbX3Ox5dWxlbHR+8+re3d/d29/t/nhxbWRqa3Hw5NrY3t7e
+3+n0blNKSElFRk5WX/fd3dfS1dPX3e1uY1xZWlphffno19Ta2tTT3Ox+/Wtcd3j95tna2NPX
+09ft92lQSkI+PkBETF963MzN0NHY2t/tZFxUUWJbXVriz+bN283P1tvh2uX57WxcY/nr39Dc
+2dzf5u9zXFJMQDs8QkVM+9bOycnO0tXf4+xlU1BUU1t+38/Nz9Ha5v1xc3p48uzve2x59ejc
+2NPW2tzf6u3m3dztXUM4Nzg8R2fWy8XGzNTd4ubybFRNTVFb8s/KyMzS3/VnWWd6/e7o6evn
+5d/b1Nfb2Nzk5ePg5vRrUT01NTk8SubKwr/Dzdrp7vxtWk5LSlNv1cfBxczgbFtVV11u8e7q
+5enq4tzb2tXZ3NjZ1tHR1/FcTTwxMDU7St3CvL3BzOJjV1dcW1NNS1J7z8O9vsjfYFJQWWzq
+3+5/++zg2tTY3Nvm8Ofb2tzZ1NbrXU89MC8xOETswr28wczdc2ZiY19ZUVBj3cq+vcPP81dM
+Tltodu7r7vLt39jc3dvb3d3Z2dze3NbU3HhUPCsqLjtpwre5wN1nXGzi3HFQRkZS2cK9vMLZ
+a11VTlNg8N3Uz9R6UlJe8M/JzNbg7+Xc2M3DxM7pTDcnJSw627avt8ddSFBaYnBaTVBr2ca+
+wcTI0t73VERATdu/u8LZWUdIUW7f2dPOzs/V6mdk38i+v+A9KB4lOdSuqrLLbEhMaVNMTk5f
+1s7e4trSv7vE70c7PVjUwLzG2uh4UkhHT+DAubzKZ0hHZsm6ucNbOi0gIzVZva2uvtVPPUVP
+T/za5N/Y8/XMxb+8ymNKRUNNbNS/u77K8EI4O0nZvLnA2FxOTWDWxLu5wls8Lh4fOtq0qa7N
++E87SE9AVNDOzcjlXM7CwL7STUdLSFT+2sS8vctkPTQ7X8O4ucHbbVRNVm7LubS9XTYpHSBD
+zrasscvVYTo9QDxywcXNz+b3w77FzO9PU2hNR1rewLi91Fc8OER2ybu5wMvsTEdNccW5u9NI
+NCIdMWfRsay7xMdCOEA6Q8vJ2s3O38q/093fYGrfXUBO4s/BwNHkX0NDU3XHurq/yOVVT2HV
+xMPNUzwqHSpcXcivuMG35jQ6PTvsw/NvxsPHu8ld8Nxz5ulBRdjXz8HH2ulaP0Nsz8G6vsrN
+3FBg0trkbkI2LSAt1+bctLK9t8w2M0RBS9rmWse2wMfG7mDja0pQeuTe4eXOxs5rTUdJaM7I
+ycPAw85z7NTa60s6NiYfRcdM16yvt6/TMTlJNTZu4nG5sMjHudJLUEU7bcZ3Xs/HyMrfT1RZ
+RkrszcW9vcjOzcjNYEY9PTklJFLKTtyvsLW00js/TzYwSvF7wLbBvrHBSERMP0JXTV3HvsPF
+wNVXUk1Ld87Mx76/yMPD5EE/TTciHjbfPkixqK2yvu52XDEpNVtNUcW5tq6xzGniUzk8VFZq
+y83Mu79pZ99RSvDd+c7CzMG//mDWXjEnKTg3Kzq2rbu5r6+78zs0PDkuON3Ex7uvr7vN7VJO
+TkRGbeNh48HAys/W2M/YWmTO10w7Q8p6MDjP1zAu6MJPTMS6wdDp3cloOUvJWzz1wdDf187C
+x2FXzs5dV3n5bV9fbN3S2tzU1NnXc0pOb1ZJV3ri2Oluee9uTUtPSkJJXk9K6dpe7srO0sjO
+18vRbGns+fvr3NbQ0t9x/N7mcH7b0tHdevl3UkpNTlJdbmlu6W5h4+Zfc3JOTFtSR01eXmL/
+/eXV1eXr2NDb4t3X1M/Kyc/X3P1iXlZafube3eL6ZWthUlFbWlxtYVVt5e3o3dvqa1tNSlBa
+V1Rn3dfd3drY4PZ293pcX/Lk69rS09jub2hgX3bb1N7k3OZ5X1xsbVtXWmZrZ2/23d7n5vpq
+ZFpVXGVkZ27v3N3r8/Rya21kXGF67O3u3dvb33ptef/9++fd2t3y+PH1c2xzdmpfbH5zY2jn
+2+xz//9oWlpgX1xj7+j5/+7h7W5183FjaHJ18+br7+nl83N6/vDf2d3f5312bGz49fbo5Op8
+am1waHDy9nlwbWZoYVtaX2VfZHF69e7q3+x7e/ru9/Ti3ebs5uPu8fL77ur/bmdjaXz+ePDp
+7O7u/HVvaWhnaWx2+H53+n5zdm5oWllfZvrf2t7d4O/1/vbo8Xfy6fJufOryaW3z+29qampq
+cu7m7Ofj8np1cmtv9+n3+ezp/2tqb2dRX+hpV/zX3+vj5Ol8ZWNydmBe+u1t9Nzc6ebg5ej2
+9+vza19neV5j6u9jaPTu8/R55t1wauftZV5obGhy7+719/Tu5v5s6u1oZ/RsZG//eXp+c3bv
+/nT/7PF++u3o/vfub/fw/n92b3rz/f3t9fTv83NxdHz6bPrtdnB0dW1manD5bmh7d2717ezo
+7fz8+XVkb/5udvR/+uzr8vj16u77+/78bXz6enNu/3t3ePn8+v1x+u99cm5xaG72+fN+6+ns
+6ujqdXn+aGlrdXFpZ15s/vr46e5wa21zc3jz7fbv6OXl6ufi5u35cWprfnp883p8+WxnaWpv
+cXdtbv35+ff993N29fv//f/19vT07ejw+Hzv7fL5+fD+dnFpaW54/Hh08vnxfPv/bXJ4bWZu
+eXJ2+O7q4OLq6Ozvd3R0aXD7+H7q+er49f/wdHn7c3t76/N7bm9oZW9sc3FnanD/9O/u9Pv4
+7PPz7+rt8e/p5fh8dGxkbX52anL56unvbmVrfn11+v9+fe7+d+/u7uzu/nBvb21iaXzu+/jv
+8nh29nR9/353bG1rdXr77uft9/n59fHt7vP9/Pv9cm1tcHJve/X8evPwc3j0fH54bW75fXF/
+9Pb67O//7ujm5+jq6+z1eXD1emtnXlhZXFVRV1lWWmnx5uDh3NTQz9LS2N3e3eh9fXN6+ejb
+3Nzf393f7G5lTEA8Ojs7Oz5NfdLEvry4t7vBytlnT0Y/QURFSlrt2M3IxsXGydLuZltVU1Na
+aP7f1M3Lzc3P1uw8MT88LjI+QVzFx7+urbi6u85tWz0zOjw5PlJ3z7u6vbi4ws7ZXU9RRkNV
+bGjdysm+vPBBQEI3Ly81QW7dzbqvr7O4w8/zRTc0Njc6P0rgwLu4tbW4vcn5VEs/Ozw+Rl/j
+1sa/wcPEzdzc4l5SWVlDMzU/OjtIS3q/vsW5tb7Dxu1UV0k9QEhJZd3b0cXHzs3W3972Wldc
+WWL879PEwMjKyMvRTzYzNzUvLzpP1sfAtq6ut7/L3W1BMzI5P0FIYMy5tri6ur7MbklCQz46
+PU1t4dvWyL/F13r78V9VVWHfz9vfzsvK1U46QUY2NT1AVtPWzrm1v8PG1Nj7Rj5HSUxbZePF
+wsvJytXV7lZcbVpZfOjVy87Py83bYkM2NDYzNDtN4cW9ubOxt7/VZk5AOjg8Qk750MG6t7e7
+wtB5Sjw6Oz0+SF3cxsDAwsfTbk9LSUlMT13ezs3Iw8bIz9x5Vk9SUE1NUlBFRFBOS1/u2sbA
+xcLBzNnoXVNTTkpPVlr26f/Zy9DRzdvc1vFu9HVp7fZy5NHOz/JNPzk3NTQ5S+vLvbezsra+
+zfJNPjs4OkFNZ9TDvby+xMrWck1DQUBDR01q28/KxMXK0PhjZlNOVV1x287NzMzW4mtPS0pE
+RExTV2f13M/Lzc7N0N5uU0VDRkhLU2zWyMS/vsDGzN1qVktISEtPWWdw3M3T393d63VhWmV4
+aWju2M7NysTAympHOTY5NDE6TffJu7eyr7bAzX1NPjY0OkFLfMzBure8wcjXa05EQkZMTVre
+0s3Gytnc81FKSURIVFdj3dPV0M3P0uJsbP5+fX1ubfP6XUdFVEtFTVt3zsjLv73Dyszee1NA
+SE5FTm9ieM/OzcXT8uRvXGVZUWnvbd/Nz8rHzs/UeV5KNjQ4MzdASG/HycW5vcK8wc7P8lBR
+TENNXFh73N/Rxc3RztjzcVVIS0xPae/o1M7RztHi4N55XVtUWGNZVWJXTFNZT1FeW2j6cnbq
+8ubZ2c/M09nZ6XJ79+zj6erZz9PY293kfWhpfH1oXl1lcnR0aWReVE5QV11hZn3r/Hl9ZGr9
+WlJaXGh3a/3U0trT1tfP0+fe1NXa3unk6W9pYW18Z2Fmauv+WWDuenB8bW5qXmRxZl1wa2pr
+cOzt3+l+bFlYXVZWX+7j3c/Qzc/a3ed4bX1nXGz1fe7u8OL17unrfnJ1aV57+vTzZ1xo4XZ4
+6eju7X1ra11jY2pna/Bvfnf8emFtfWtu5uZxYXd3f3Vd6+Pl4NPR19vk1eF7buV5Y/Tzfn32
+XGFhYFJVWVr9X2bu4njy4f77d2j+4nX86uHf6/Hp2u/k4+vs/3lbal1hYltcZnVu8n3m4ORq
+df5k9HDzfuzh3eXy4OPt6OZw7+3za2ltbW5g/GZe/vr0+P5tf3ZmY3NwafJ5eHL1eXlw8+/x
+4+p06vP73+jl6fd7/Gvo5/T953xpbGJhZnhbZvp1enFubXRhdef1/vH06tzr7en89Glpaex4
+f3j05H7+aXlh/WRz+vvv7N5zcm/9aO57Z/nj6ure23106/llaWlmWWd8++r4bv5nfm5tb/ns
+6nT6fnP4+Ont6t7i6e5+d218/mFjY/JrYXfg7Pf59W1sfmd39P1r8uX4+uHneu51+Ozv6+13
+9nhjXmV2dfjq8e7z9/1ia+/6dH7/a2j5fOtzdGzydnJ09uLv8ejofPl0am1+d2ny5e37ffRx
+cnvp7vzp3uz79nf+/nReaHxubW/wdW9i9Pd8bvXg53Vz6nnscPBubWlndV597/N+3vj8a/zp
+/e7l3PHu4OPv72j8+25pdmZi9P5ma3FyaV97/f3w6en4d+rufvX0cHfxbXjx8XF1d+9obXfs
+a/fa6u94cG/rb3Xs7PPr221tavR1e2NneGf4efD55vvy+2dqX/Z19vf7dOTqd+p633n89On0
+avHu72R0Zntvb175e27hZu39d2bo6Hv7aet9d2vo9vvt9ebx/mjq8uZz9HJ7al/ucXRo3+jq
+/fbxc+pkdnP9X/n4c/J15XLqXXn39Phk9Wz0VOrs5/v93WvoYNpv7fRt32/uWN7q32np5XX5
+WfBY+F9o6fhkbdJ0+Xx7bO5+XuN2b3vrauXraOze+2rfc2/9X/fvbVn092t+em7h53Tt3nde
+2f5q7uF5cvNZ4Gxtcn1oYuBddnV5X3X/Zd/99HzaeeHf9/HjflvYbGXs/mle6mvxbWtv8Wxo
+4mh98Oh77vB05t91aOf3aGrqZGRravXpb1r5415t3+tu4+N98uBxctz9aN18YuDsYP3pYGl6
+dnDra13d+1z37l9cdmjwdGB62e9e4td+897z9Ox1edp+aNneZfn4aWrxVurdUVzoZ1Pa/ljc
+6mrn4WXu11d712Fb33Rk1Ota4dhg5tlX/9xkX2ZtW+t9YNftZ/7nf3zv9/vg5vfl/vbr4PV0
+dGj/d1xo8l9c9HFva3R3buhuduvu7np87fXu7/b4/e3799bi79/2am/eY2Tjdv7u92fh/WTd
+dWTs6md95WZ1bldWV1RXV09ZbVpd5uLm1NLUzs3RyMrRz8/h7+Z4bHzw7efrdnxiU0VERDg4
+PT49RVhr28vDu7q9vb3BzdDdXlBPSkJFS1Zv+9nMysvMyM3a2NjpYGHo6F1t4GlDQk88Mjk/
+Pj5OdtTMyry0u8C7vtZndllEPUFOR0Zoz9XUvbrCx8HF1nVk+WdOWvN9WnPPaDs/WTktO1A+
+O2HKx8jAt7O+zsHAXz9OVD03RWlYTNy8vce+tLnOz8TTUE5oa09Q79HgYd/MTjM9XzMoOl06
+Nue9xsi8tLK/3Mu+bTpEZ0M3R9vkX9W7usbFu7zOZXPcYkRN4/JX+s3U8djfTT0/RzYvOkxD
+P/zBwcO+ube90eLWbUI+S00/RF/u7dXHyMbEyszM1On+e1xYb2th9eD47tbT3OV6Y15PREBA
+Pz9ETE5T9dbY2c/LztTX2+Lo4/lqaX71//fk1MvQ18/O1+Xt/3FxbW109+3q5PF5a19PS0pE
+Pz5ESUtTaPnp1szMycfJ0NnqaW1uY19o++7k3tnW1NfZ3ens9u/6dXNucXF7/ejh5OHr/l5P
+UFBJQkRISk5Zb/zn2tfU1NTV1N/v9398fe7r5dve4d/e3trX3uPe4vl8+3F0//Dt5u726+1r
+UE5OSUI/RklITV5octfJy87Mzc/O1Of3eGdiZWVp6N/n4NfY3mxe0M152MLOedrNzs9uTnbo
+RDU0OT88Nz5z19nKwb24u87dzdtPR0VFS05JU9rP1cjAx8TAze/t719fbv/j1MzHwsDIaU5q
+SSojMUYtK224vr60sq+xz0tdazsvND9U8d3aw7W3x8m9xvZbT09cXVJsx77LzL+/xsh6PkBG
+LiQoNT05Rca1s7S3urnEWEBAQz89RFrRwsHFwL7K6PV0W2JxX2nk39/WysTFx8fKxtNIRls7
+KyksMjs+TM23s7e6u7zLXUQ/QUNITmbOv77EyM/a7Vlc+OXc0tni3NrYzsvS29fNyd9MRko2
+JyUvPD9Oz7qwrrS/ydJsSD89Pkpt8tjEvr3F21lOUFh0487EwcfP2uXaz9Pi7N3Nx8zqTUIu
+HyIzNTX8ubWuqrDAy+1EPD46N0jdyb+5usjY3mlJSmzozb6/x8nN3tzcemre0NPR1N1iRjYi
+HCg2Ol+5tLCpqbnN/kI6Qkg8P2LTxb22vt1zT0BFXGvcvri8xNJmZtjX9PLe18bBzeRbQS4e
+HiszTrmvs62pr77PRDEzOzxFc+jQwbi0wOxLQz9CYeXSvLa6vsz57dztcO3WzcnFz2RIOSUd
+Iyo1za+urqyvu77YPjQxMDdYzMK9uri8xe9IPj1O7s29urq9yN7q1tj9Y19ked3PbUU5KR8j
+KzPhsKurq667w9FIOjQuLzxixrays7vI7U9MS01j/NPBvbq6v8/e4Hxr+V1MXe5cTD8tISQn
+LF+1q6iorbnG40pFPTQyMjpev7GsrrzlT0NGYN3W0NDPwrq4u8ThW1RccmdWSj0zLScmKzBF
+xrKrqq2zvdVbRz04ODw/T93CtrS3vdRhT09cfN/Z19DIvr2+xNhvX1xdXEo9Mi0qJy04Scq1
+raussr3SWUI+PT1ARU/vzMa/vMPM1XxiX2X+5+fr1sW+vLzG1vZaV1dSSUM3KiQlKTXWr6mm
+qbLC5lBKSUI7PD0/Vs+6sbG4zlA/Pkhf4MzLz9LPx7+8vsjfXE1PYmtaSTYoISInNs6tpaSq
+ts1jTU9PQzs5OkbevLCttMxXPjtHdM/Kzd1w/t3KvLu+xdXvbm/x8Uw5LCAeJCxMtKekp66+
+3F9HPz86Nz5S3Luwsb3sRTxEa8/DxdlyZm3Tv76+wcrPzsvZZFc+LykiICczarSopqqwxGpS
+RDw8PT1Ozry2uMdSOjhBbr+1tbzPWk5TZ+DOx8K8u7y/z1tDNi0oIiEnNNqtpaSrvGdAPz4/
+Q0peyrextcFNLCozRrysrK+75ElERENwxry0sbjD12JJPzQsJx8eLFO4pqKpt9JHNz09Ok/O
+vLKuuGQ9NDA4RmXIta6us8pFNzhE2ru1tri8wcvrWjwtKiUgIi9Vuaejq7n3OjA3QVfEtbS1
+u9tDOjc2PWnDvLe3v8jcWklESmHGuLa4v8jZ9+9TNysoJSAqW7urpqq+Yj0wNlLewrW0v9Fv
+Pzg9R05fzb66u8HSZlRMS1ravbS0vd1kYejLwM07KikmIidCxLGprL3yTkFE6NXdyMTT3dpj
+SVdYPUjPzL++1dPVa0xT3su5tL/fV1D2vra5xUwsIx8fJEHIuq+tsLrFVTtAT2jaycvPxstb
+REdYX1p+zs7NxNRs18bHxMpnUHd0fsa6trW8UiwnJR4gLUjGraaprLTrOjU2OEvMvrm3wP5L
+R0Q+QW3Gx8zCxMrEx9be709Q7uLYv7e4urvWNignHhkfPcuuoJ+nrL81Ki8wONW2trCxzkZG
+Qy4z8N3Ovbi1ub/+XW1OSlzLwsXAvri5vdg3KCMgHRw0v66ppKavu0UrLz1JVsS6vbi+aU1Y
+OyxUx13Kr7HCvM5K4t1KXr7Cfs2/xby63jIpKCIgHinJsq+so6ey1jQwP0c+RtDAvLzMzs/f
+SzhAWe3r18O8tb3Nzs/VVk9m58S+v8PPUzAnIyQhIT6/tauioqy15Dc0Ni0sP+7TubO4tK/C
+Pz5BOjxHfb+trri1s8RNPkNMcNttTHvXOikqKCUtS13Fp6CorK/ITUQ0Ki49Pj/ZubOtrbnK
+0FM2NT1DaL2ztLCvucnX/D8vMjYyMDg8Mi89SlHeuq+tq7G+x9BNODg5PVBsds64try+ws7e
+a0hBVunay766ur3YSz01MTQ1MzU8P0VKTlnfxr63srG1usXvUEQ+QEVPbdPHycfDwMDI1nVZ
+T0pKVPXazs3Y4PRYRDw5ODo8Pk5l/vPf3O/q0sTAvry/xdJpUk5PVW3m5d7Wzs7U1tvscmBU
+U2Hw5t/e0s7eWkhDPjs6Oz9Lfs7R6t3X7mNp583Ly8fFytj09+ns8OLb2tnb4PHf2/xXVFhZ
+V1RWbeLn3+NiSURMTklLUVxjd+/3ZmFfYG3v1c3Mx8DAxc7X29zp+PLr3tzh6u95aFlPT1BQ
+UVt8497k7H5tXlBMS01OU1ZcZ2tqXFpjdfbj1M3HxMXH0tzh8mdmdXH139XOzNPb43lWTUlN
+U2P3/+fb2epiV1NPSklPVm3m5uHs/W9paF1m+ezk29TZ5+ftal9se+/a0srHyc/Z4H9pX1xZ
+XnDs4+7o6nNfUkxLSk5VWGbu4/f3+Wxpa2pse+7k3N3p9XFtbW3w5N/b1NLY2t/ycPvxeHH6
+5tzd5ud1XFZQUFVXVF1x7env6/Xt6O98a2hqevjs929xbnJrY3ru9O/r8Pr1eGprcGl+39rW
+09TZ3eL4Z11fZ253++7yeG5sZF9oamZrcvj07/T7cmpdXmZt+/33/Pfzfm5maWdqePzk2tXR
+19vc4/Z7cWRlcHzu4+Hm5/p0cGdfYWtqa217cWZoZGRjb3Rsa25uc/978O15evxx+eTi5OLh
+4d7f8nBvdnP57Xn33+Xn39ze3+Hu9XZ4bGdgXV5aWlpWVVRUWFpcX2h+5uLj6O7o5enl4t3U
+09vZ2Nvm5d3v7Ojg3dvd6eHue3txYVhPTE1JR0pKTVBWXWZ5+OLb2tjRz9zY0+Xv6evg19vf
+0tLc6ePtZW357tzc49jQ2vT4W0hDPjo4Ojs9RlBf3MbCv7y8vr/Dz+H3W0lGRUVJTl7qzsfD
+vbu9wMTKztTm29heTEs6MDEuKisvMT1s2r6uq6upqa60wXJPPS4sLi4uNkV4xbqxq6irrbC5
+xN9LRENBPjc2OjIsLC8zNzxJzrm1sKuprLG7x9hFMjEvLDA5P2fDvLOsrK6vtsHNbU1JSElU
+ZtzSVU9JMissKikyN0HHtrWrpqqsscrvTTEtLyssOEFTv7SvqaqtrbLA015EQD4+S2XnysHS
+blc4LCsqKS0xPda9u6+pqq20xNNaNS4xLi83RO7AuLGrrK+xucPUWURCPz9KWurJxcXN31Y6
+Mi8sLC4yQe7TwbKusbW8ws5POzw5NDc9WdLDurGurrK4vsjfWUxISEVJW+rWzs7a9kw9OTUx
+LzI3P01qzby5uLi7vcXdYlVGPz9DV/TdzL66ubu9v8XXeGVfX1FPW21rcfpy7v5LRElDOzk6
+P0ZFSWLq2s7Jwby9xMbK1edzaHFqc3Vw9Ot+d97b49/b1tTQ0tXR0dTV2+TzaVBJQz08Ojo9
+Pj5CTFho797QycrLyczNzM3Rz9DX297e3NbU0M7OzM7Q09TX19fd9l1TRz07ODU0Njk8QEhW
+dejVzMjBwMTExMnO0Nbd7Xx+e/Pk393X0c/Pz87Pz9TV0c/S4mZTTEI6NTQ2ODo8P0hUbPPb
+y8bExMbGxszW3t7d4u7w4d/e2trTzc3O0NfWz9bc2t/s7X1eT0dAPTo6PD09P0RNWWPw2tHN
+ysrLzM/U1drb1tPb393c3+Hd2tfW0s7Nzs/V3Nzb4vJsV09LQ0A+Ozw9PT5CSExWWmXe08/K
+ycnHyc3Nzs7N1NrY2Nzc3+DZ2Nza1NbZ3OTm92NsdmVXVFFLR0RBQD88PUFDSVNce9TPzcjH
+xsTFyMnMz9fk6+76fn548+Hj3dPOz8/V4t7f7Ptwa19gXE5MR0NCPzw9Q0RETVVn3tLNycnG
+xMnQ09PZ5X3w59/g7OnYzc7NysvQ19zr/ftxXlpaWVlUUE9NSUI/Q0I9RE1MUW3u1cfHyMLC
+ycvO1Nzk4uvv6+nx7d/c29rZ2dne8f9wamhiWldYVVlcXmZwaFhTV1RKRkpQV1te9N/S1N3Z
+09Xb3+vo3uH889rO1NjRy8rP1NbY3XtlX11cV1RUWFZTV11fWVVbXVtYWF5qaGNfYW50duze
+3d/k4dzY3uvl2NLY3tzX2d3f6Oftb23973xjZ3VrZ2djaGxjXVxeW1tfYFxdaGl18frw3t3k
+7Onl8fj3+PF7am389/599ufo7erp5OHn5+zxenZyanBoY2tramJoef119enj3OPx6ud7c3Rt
+bGxoZmRgam1qam59ff30/X/29+rr9e/xfH70+Hj/e3v3e2188u/q4OHg3t7f6e95d3BvbGpp
+X2FlX19obnL/e252dm9ubnb36+r0cP33f/t1aGn4+frr4eHj4ePs8+vv9fV++fr8dmZlam1t
+cHl3eHZtbGNkb2psd3d9+Ofj6Ont7fz+/nZ3eHZ69Pd09Ovu+e7t9/Dx+H7+dnBqZ2thZGtx
+eHN0eHRx+/L//O/z+ebf7vXp5efs7+/r7vB8dm9wbmpqY2Vobvl3bXd/dnj+dn/5/Pr78vd0
+bnhxeOro7+/m5ejt7e7x/v97dHd3c3Fwb3z5/Xh1bm5raXf9/fPx/XJ2dHNvb/9vbPV4dnl3
+8vHy8e7u6+fi4+Xm8377+XR8/nR5eGdsfHhrb3VpZWxwbmxobfj4d2188Xh29e9+bnN+9PHt
+5uHo8+/0/W567O99ff7v7/nv7unu9vl3Z2RvanV4bnv9b2779e93bnluam1lX2poc+3y+fTq
+6vPx5+bo4OX1/HV66O927fF77n557Pd4bmtvcmZscXNua//w+m35+GhxePr7d27s5u7x9/r+
+eWlz731z8u377/B9+e/6dvLubHj9bXr07vz68H92dG926vF0cWlrcmtv6up8avrteHP35vNz
+d+jj72xu93pzbv94fvh4Z3LtbWBn/Pzq7/Pe5+zt7+lvZWpoYl5m6Oz96+fs7Ov9/vl2dXJv
++vtsbP/v8/Pq6v5nYG56ZF967u/8efv77vj89PX6++1zcfr3/O3sdXny/2Jx8Xp3e3T89W96
+83598Wtsb2zu73Rq9ur16O1u7+xp8OZ4cv3zb2Bke/92av7a6GJ34O1yY2jueGZz5N/7+97b
+4fx15+diWW7tb2hpbvl+aGv6al1u6/pu+uri9Xb65OtrbOrj8F5j4ehna+vm9Hl08/lveePk
+9/vvdGp7b3fj8mp9bWBpfvby/v1waPr6bmvr7HJu/vp56+7+3+D77/xr+PFpZHRn9+9pdnr1
+bm5kdfN28ur4aP7o8Oh3bOjtYG/l7W5k/+3ydefc8WVn5npkZ3n0Y2F64vxp4+T3cX/z9Gxq
+8O37aWpvaF1fe/R6+e3u8Wd253ttfvf4a3Tf6Obwbt3ea3rodmttbvDsb/rb5nr+/H5hWWVo
+XmhbXfZred/t+Nvof/ttffN+7+jl3+Dk8uzzZXdwYmJfZG9xYW37b296b/Dxam9xbm7t5urr
+59Pfduvp8/VxYfr6YOznaWz2/O/lfHztf3N1XmzvYmn4XmL+bW3x6uvxd/j6bv5/9ebq9Xzz
+395vb+z3a11i/nxcX/nf7/588Odkafbu9ffx4eR8833+5Ov1+W9v/X9taGxu+nxtfvhseW9g
+a2NX7u5i+ePd1+5v3dRtb+l6/ft4en59Z3F5XmHr7mV06u/s63z/4uv8+Xj23nxh6eVhY292
+/XJvfHZiaHf5dfnvZ3rg8XnmfHrobe/sa2/06e/r++vp/GVh7e59cm187F5c6udwbOzt9Wx2
+73pgd97eaXXq9nhlbPPvaGb893d7d+jhc+3pcXJ4a31tZunve97b4/Ry6eVlYGdne2li8+5o
+Yfrv935pc/ppaP3v5vD169/t/O7l3u/57utmYWtrXHf3afN5cerqXWF6cWdibvXvfu3c3uDa
+6Wv/6Wtt7+3m5/zs5W1heWVeZFpfX1/y9XD19Pfu/vDn6uvtbPvi73ns6ev5aW73eHj4+mj+
+fmphX3H8/P708fXo8H72+u3u8Hb76PtsdX56aGV4cnjwZm/9bmJuc//sfO7id33j7/3i5Xnv
++Wp982tx/X58am5ub/1vb+zraHPr/vb2+evu9fPqfPn3fHlkZX59dPtyb/XqfW1vffr3Zl9z
+enj3fP3k7n7p5/Tt7PL3fvPseGRn9+t0cnN+/mZg+vlmdffv9nl79/pu/2586n5rdvPz39zo
++PPyb3hrZm1ma+x+b+f9b+/tb/P0aHX1fHr2ev/r73P25P7+enJvbHd2dWl3+fxyef3x5vvq
+6fT9Zmf6+WT07WXv52995/357ffudXNpZ2x0bXvufvPy++3sdX79dvRoYmr+73Bv/n7z73jt
+4P558ftna/7v7/t57/P9efvv5u/28vrqe2f763Nvc2l0emdmavb6dW9iXnD47+rf7fvvafbf
+dmR9/Hnv+erpe/Z8aHDw9Xb0+vf4cWl68HDxdWbs/nvvdWf+5+3zduLlY2/k9Wjt4eh8ZmF+
+elpf7Oxs9e59bGf+7vj37PV4dPP/bW1+7OTp6+fp+Wt0fGtuZm18Ymt6eXX3/3jv/nv77v5z
+99/e+/r38e18a3zo9m/26O79+ejtaF9qZV1cZ3dvbGp2+mZabOvs7/Hh3uns3N377+L3ZGd+
+eHH6++jhf2/i6mpsa2ZoZ2L862976PxeZW16/H7z933v+ffsfmv23+bsfvfrfF9kX2jyePXf
+5Ov5+Ormemb49WdoaGBo+292+PTtbmZ5emF87evf7fDk5ezufXF69fXu3+D9/G1pZF9ybl9o
+dWVmfnp973ZlcH327ufu7N/t9+98aPTrePrm3+XueHD4bl5mcH5vcvNtZXJ0/u99een1d2pq
++3hv7ej6d+L1cvtv/Ov3aXXn9m7l5m9ybmt4bGnr5+zq7u/1amNx7PllcXZrbm9ufvpv/Or3
+8OT59fF57/H593Fu9/d26/F09Gxg6/12/nj+dGxsamR5ePHs9t7f8fHvcXtnae91eft6+vRt
+fu316Ox2+vhvdGttdPz4fWdxfG75cGJiffXq5url7+bm+vrz8H1wa/n+bnt0fXtoduvzb/p8
+d/JtZ35qXGb8cm5x/ODq6N3g+m/x73Fu+Pjz/W5t7u3u6fvyfG1ma3xlX3jpbvv8Z/59ce/k
+6Oz9fHv7enN45+996uLs7OpvX2JfX15eY3vs9nd6/GtmdXH+6PDv3Nrf5+TveXN18nFo+/Jo
+a+v8be33++n7cvnveWZjY2lkYX3r7nb57HZuevf4c2/w/nz96f5t7/jr6e7x6fFv9XFndG5y
+5uf39Or7amh+/Xh/+flra3n0/3br//rq/v7q+WlqZl1bZ/5+f+zd4/Xz7Pbz7Hn/6uvr8Xnx
+7X54eWxvfXV2eHl+8/d3fXJzdGllcWxzfXTx7PPo7G/+5epwbuvj73p2/3Jpfvf89HNq8/Z3
+5vZt+O9+ZWNwev3o7Pfr5+v2dGZdZWZn+Pdq8Nzi5+z87Ovy6Hp3eGZtamRmfPTr9O3mcWNv
+b2NudPPr+Pzq7/3p7+js8/D98PdnZGRm+/F8eO/p7Pn9bGVrc3hs/3X74fJtePd5be/k6+rs
++Obf8m1//WVr925pbG5oaWBdbnz+7e/q5f5x92to+O7o4uXj6d/b6fDvfX19a2l7a15lb2xh
+a/B9fe7tdmd1/W5faPHz8vH66/dvcvjvfWps//p+73pv9u3w7Ofp3uHo6ubqZWZlYGVn/PJx
+bX5vbOj3aHF3b2v9eGl5+X5sYnDv8u/s4ePr5+r+dnZz7+vqfPT/cHp0b2poYV5iaWhu9+rq
+6enq4uju9u/z/Xx8/n148/hventmYmRkZ2xqeeZzZufg6ebh9e7ydm9s//d8df3n5fX+/XJu
+dmhlZWd0cG5ufvl77+717PDy5eTp6fDv93BydW1y7+9+fnBnYF9ne3JfZ2Rpd3Z+7efq6d7h
+5+318vTr7/Bwa3FxbXT07XtmevZqZHj/evbv6e96fu/8//zz6/D5fG94bHb7a213a2ZtaGVt
+cf/v8u3k3+v99O3zdu/x+u7u9fP9cvp2dPV8d3Jqb2ldZHh7+//v4OHm4utqaW1wbG39+nb+
+6+j7ffF0b3v7cvl8a/16bHF0dXr88+ji6O/x7X968/Pr73tvcm5ubG7+b2Nxc2prdOzu/fnu
+6+3x7fJxbv99fn9qcO/1/n18+ndtef1ydvZ3cXdvfu9+eO/m7Obl4up+cHr1fG5sbnZ6+ezx
+e25qaWReX/nrdHR5+Onw7uvv7vP/+HRvcnh4fH5+8vb7e+3qdnRyZml3fPr1/39/7+nu6vPw
+7v9ucGxofvn3fXFucWlmcXt6ePTv+Pn9+Onk6uzs+XJvbnh7fPd9dX3/e/Hw9XxucXF4aG18
+fPHv/nBucHzu5/N2c3Vtcf97e/rt83R59fPt8vTx+3x6/nFtdv3x+fjy+npwefp/fnh2Z2h7
+cmRmef73ff706uv16+vx7ery7+3y+3d+e3d++/j6/X5ucW9pb3JkYm5vb3f//H/88fPw8PHm
+6fzv8Pj18f5saXjvfnJud3J8fHJ6fvX/9+707+v7bnV7c3N9+fF9b3NweP369/766e15/fT3
++PN4a3BsbXVy//jt9G1xend59/Z8+/n6/vzw7u/9fv75e/Tv/3p//mpkcWtmbXN3/f50b/nv
+8Ovr7e/x+vTu/Pb1eXdz/359+/p5/u54a3p2evzv9fbwfnt9/P7v8O91aXx7c3fz/Hp2d3Fu
+dXX3+/36a2tteHFrc/Tz+H7z7ezv5+Xt7O7q8vXz+fNzeXz6dWtua21rbPxiX2dla295+vp3
++O3f5uzk5vp47fHu9fHyeP/xfn3t73n8fm1yal5kXm5yde9+am919nRpd/j9ef3x9+vv5OP6
+feft4+n35en8/ex2dOjp+W1qdWxrZ3vyb15ibmJm7+9tav7r/nd58G1xbP14bOPn6+Pg7Nfk
+buXs7/Z6ZGZtd3n+Zf9/ZvHo8Hrs5vx2WOv/Ymfu8vhkfn/5dmr7bm79bW/vY23p9nTe3OHs
+4vT8b2z4d3Vu32hl++ls7uf3/HZ0bm1vdHpuZV/0Z2lv9vj5+fTa+v7l5udv6unjev19am9s
+e+vr62v96Gt1cPZoZGxadHVx9OXl6uJobutmdf1ybGV3Znn74eTr7/HoffHjbX53Z/VxYmp3
+5Hvhb9znavPmeXBw9e1uanhxY3P57O9sbPx2f2tscG9ofvVydfR9d+bb7HX67Xz6+fXc7XPt
+8/b37HZ2fmdj/vJoWXxzX2f9935v9+9883Xn7H589Hfr531s5HRr5X//ZXlq+Xtm4v91ZV99
+6Xd58uTpcuvz/fDua/zm9vh67/FlW3llc/Jy9fFpf21uem14/+r+8/Z76et+8fT39Xn0/nV+
+5nVkeWRk9nJmb2Nw7vj56O759uvv7+f17Xjve2Pv/mb25uxkeOz4Yn3lbW/6evT4Y2FscXb5
+/3bxa2ps79/19OXpZH59aXtv9vxv7+9u+uJn+97n73z+6/Zmd/X+b29y/ml27XPz9H17XnVu
+b3ts7fT93evv/uvtcXj07HRf7fJobW14ZWxfbHnp8Hf7aWF39m1t8uHk4eDr5/Xq5fz94+n4
+629nbW5dYHVvXHZqbm12bu599eT84OXt9mfsbW354G/65Hz+6evzbnNv+WhoWmx0bvxue3h1
+cv3t7+jo/uX7auh7fW52ambp6eVfdH52cmNg7e5uY9f27N7v2/3v9+zsaet1/W9p/Xll/mNc
+ZOp7ZfHs/HP3bOxxYXrs8Xrp3e9s/v99aO3rbf3y8XT5e2v7fm30fXBtcfrzbmvl7/tsevtt
+7uFxdl9gYnFr8+/n6dvi3u7z9XTm/fH7+PdraOnha19vaGRuXnVsY2Zi/G5w5fVmd+Nv5t5g
+8fjw9t/k6+Tl9Hx2/Gj/dGFtYujsaOrrd+/7bm5ubf1pa/R6+uD5eXR7499fW+hoXnfj19h+
+X/f9YmXo0d9obu1yUmV+aHVgamjp+vXb5vTya29laGXn6N57cebi8uLXd3T+YGxxa2jd4fLy
+429i3PtSYV1o+99eXdPfT+bP8WRxXlfke27r3e9n6/RZX3NOW9Ro3MvOzvFh22dOTk/Xz2JZ
+0spbU3vT2extcv5fYlVn3d91WPbpbGZn6tpcXuzd9lht+2pobO3p6ev678/LzMvFys3W09t4
+WUM3NTo2LzI9QUFL5MW5t7GtrK+ztbq9wcTBxMjlRzMoKyoeHiYtMThKtKSnqaSmqrxLRE43
+LjRE1sfFrJ+foqKnujseGhoSDxYdLEjYpJWWmZmcpcEtKCojICg6v7CrnJWUmKW9WxsNDg0L
+Dxgrrp6XjIqOkp/JLxkPEhUbLWapmZeTj5CVneQlGAsICQkOIDyok42KiY6Xpy4WDgwOFCDO
+n5aSkpGTmaO5XCUSDQ0ODhQovKKbko2OmKm5OBcODxcdKruZkZaampyr0dPOUiMYHR0XFx88
+wbysmpacpbO5PxcUHyMkRqaWmaikn7RXbrm2yTknKiAWFx8tQtyumpieoqq5OxkWICQlUaOY
+m6mpqMs/0ay0wcg+LB8ZGBwiL+avoJubn6u/RB8XHSgsRKqcnKevrrpkzqmtv7lAKB0WGBwf
+L8WpnZqbnKjKRB8XHCEoQbKfnaasrbrJwK2stKxHISAZFhkeNLyzo5mbn6vI4yUWHCUrOMGf
+mqezrrTM4K2kvNdSLyEWGSAjLFOwn56jnqG4WCgcHyAkNr+koKiqqrXLz6+nwsPMLB8aGh4d
+Jlm1qKCenJ+1zzgeHSAlL1+toaOqq6+90LyqttHLNSggGh0fJDncs6Sgn5+qt1okHyQiKDu8
+p6itqqy5yr2qtMizWzEkHCAdGypBx6ypnpqirbc8KSQfJy0+vK6tra+vtb+vqrSuuT4xIR0d
+GR0sPM+1qZ2dpam0Ty8oJigrOe3Au7iwra+tp6WnqrlNLh8cGRcbJDNuvKicnKClrMBMLykp
+KSw4UNzEtrKvrauqrq6w7D8xKiYhIS02PGTIta60tbfFbD42ODY4P03vyMS/uLavrK2rrbbF
+XEU4Li0tLjQ3PU5s5XxYZU4/PDlGbvzMurSzt7u0s7WxsbC0yPRmQjQuLC4vLzQ/VfpXTVpM
+Pzs/XtbMvrWztLq9ubq3s7aysLvQdFM+LyotLy4uNEJPQz5CR0hHT9m+ubq7uLe+ysO6ub25
+sK+zv8/ZUzkwLzEzMC8zNzo2Nj5KXuPJura7w8bEzXv3yL69urStrLC7w8xqQDYxMjItKywu
+MDA2TdjKwby2utHv3tx1YtbAureyraytr7jEzPVGNzEyLSclKCwvM0Phxb69vr7F7lhm6et1
+1b21srCsqqyxuL3Jb0Y6MS0qJSMoLTM5S8/CwsjKxs9qXe3P1NnIvLe3ta+tra+yt7zKWzwv
+KygjIicsMThE5s3Ozc3Mzetr48/O0Mm9urm2sK2srK6yucZnOi0qKSYkKC00O0Bc1tXq5dPQ
+3vzmzMrU0cS5t7mxrKurrrO6y1Q7LisqJyYqLzc7P1X073Z16N7j7t3OzNLUw7m0sa6pqKmr
+sMbbdjYnKSsnIiUwQDk5er7UU9+8yE9U1NNMScm3urSnn6Gmoai/RTQwIRodJiQkLta7z7yo
+rsd0z900LT4+NzvfubqxoZ2foJ2dr1U8LR8XFx8eHSpyvr6zoqa4vLz2NCw3Mys458/KsKOf
+n52bnqe7XiwhHBgaHh8oNt21s6ynrbbLXEUtKzQvM0rdv7msoZ6dnJ6eqtY7KiIZFx0eHSpL
+xbqvpam3t8ZMOS8vMS07VHXCsaqjnpydnp+u0TMpIhcYHxwfMVrIvKulsriy4Uc/MjAvNEVE
+8rmzrKKenp+dpLnlNSceGBweGyY8Scyzq62zsbxtXUcxNTo2PWrQw7Goo5+enaCpsO4wKh8Z
+HR4dJjZI37uvs7W2wd9hRjk7PTpAY+LIsqymoJ+en6iv3DgtHxwfHR0pMTdjv7u7sbbFyN5G
+PEI/OEZpW8uwrqifnp+fo6/LQy8hHR8cHCYtLk/Izr2vuca8zE9KTj85TFlMy7W1qaCgoaCi
+rLnpNiwgHyAdHyoqNPfvzre5xMDFd09ZTEBTYVzKvLitpqWjoqSqr7tINywgIiEeIyoqOFdP
+7sDA0MTEW3/STlzVbebBvLmtqKmloqmqrsVbPy0kJCMeISwqLkpJSM7A38u85efD6l7K0/W/
+t7uvqaqopamtsL1vPTEoJCUjISgsLDNDQ0/P1d7BxObLxfPZwcvEtrSwq6mrqaqwtLzmTTwv
+KyknJSUnKiwyOkBJ/c3Rx77IycDGx7u5ubGusK2ssLGyu8XH1k8+OC8rKiYjJSgpLTQ8Se/L
+xr+7vLu5urq2s7W0tLe3tLm+vb/FxMniUkI4MSwnIyIjJiovOkVtzcS+uraztLOysbGxtrm6
+vsDAxMnFwcXGyeRJOzIrJyMiIiYpLjdBYM6+uLWzsK+wsrO0uLq+xszOzMvIw8C/v8bN5Ug1
+Ly0oJCQkJiovN0Vpybuzrq6trK6ytbrAyNHl7ObVzMnHw7/Ax8zWYT8yLSkmJCQnKi46S+G/
+s66rqqytsLi/xtXta19bZefbzcTBvr3ByMzfTzoxLCglJCYqLjtQ2b+1r62rrrCzvMjR3nJo
+YVdabO7by8TBvbu+wMLWTzoxLSonJigsMT5V5Ma6tbCxtLa3vcfO1un7fmBd6dLHvry9vb3B
+x87+RTkzLispKSsvNkBNdc/Bure5u7m7w8rN2eLb3+fXzMnDwMLAwMXJydHzTT45My4rLC0v
+NT5GVN7Jv7u8vru+yczK09nT1+LYzcrCvby9vL7AwMTeTkE6MiwqKSotMTk9TurNwb2+wL3A
+zM/Nz9XNztXNxMG/u7q6ubq8vsfjUkA5MSwpKSorLjU6Q1/ZycC/wr29xMTBxMnHys7LxMC9
+uru7urvBxMv4TEA6MSwpKSkrLjU7Q1vYyMG/v729w8O+wcTEx8vIv728urm6vcPGytduRzs1
+LyspKSorLjQ6QlrcycC9vLm6vby7vr++wcbCvr/Avby+wMTJztZpRDs1LyspKSkrLjI4Qljf
+x728u7i4u7q4u72+wcTDwMDAvr2/wsXIztxdQjkzLiopKCkqLTA2P1bbv7e1tbS2urq4ur7A
+w8fHxcTBvr6/wMLH0/5NPTYxLSopKSkrLTI5R/PJvLa0tra3ubm5u77Cx83NysTBv729v8PI
+2WRJODc0JygtKigsLi81Qvu7r6ytsbS7v76/wsrU6uTOvrOtqKeoq7LcNiMZFRQVGSM6zK+n
+oqKjpamuvFkuIx8jLVK6rainqq+xr6yrrK+5yv4+KyIaFRQWGylXsKKdnZ+mq6+52zwpHx0k
+M86spKKnsL/Oxraura60v9hNOSsfGRYVGSM/sJ+dnqOstb3F004vIx8lOMWmnp+ovmFRcMGx
+ra63wsnbSjEiGBISFiFTqpyYmp+ruMfRaDkmHRwlQq+dmp2q2z08X7qsqq67z/lbRy8fFxEQ
+FifAnZWUmaO01/heRi8fGhsm8aOZl56xTzlDyq6oqrTeSktQQi0fFg8QGC+rmJGTm6rPTk1Y
+QSoeGh0su52XmKHAPzhOvKuprslLRUpdSi0dEw8RHFCgk5CVn7lPQEhLOiccGR44rJmTl6PL
+OTZYuKurt/E+PFBoSS8eFA8SHnCek5GXpMZCOkFHNiYdHCJOppeTmKbVOjpoua2vwEo4N0bl
+WzMfFREUH+uek5KYpc08NTo8MCQdHinjo5iVmqnNQz/xvLK0xV5IRFNfPyscFRMXJsyflpWa
+p79RQD0yJx4cHzK3nZaWna56RlDLtbG+UzcxPGjQXC8eFRMXJsCelpWbqcdYTkQ4KR8dITi3
+npeYn7PdVm2/trrORTg1PmFONiYaFBQcNq6blpedq73ZVDwtIh0fKuGmmpidqsD86L+wssFF
+LigqNkU/MyMZFhkl16GXlZmktdtIPTYtJyYrRbejnZ6lr7/Gvra2x0swKScuPz4yJRoWGB9H
+p5iUlp2ty0g8OjMtKyw36q+kn6Gor7i5uLrISzUtKi06Oi0jGxcZIDutm5eYnqzKTENKTkpC
+OjpI1rWrp6aprrS7y189MS4yPU1NLyAZFBcfN6+dmZqgsMvjzLm0utk9Li462rGnpamwwedO
+RUNIWmNbSywfGhUVHCn2qqCen6eqq6qlpq2/QisnKTjRr6inrcBrQT9NV0UzJxwYGBoiOM2s
+op+fnp+foaaruv06LSsrLjdH5L64t7vXRjEnIyEgJy5BybOrpqanqq+3u8nO1W3+ZFxfVV5W
+SkY6NDExNz1Lb29lZGZ92cS8trm/ydXKzcPR3XZYdF7z1MjP83NOSElEREhCOzw+QmTYzby+
+wMPZV1Jt717xx8xPaN1iVVN9997Hw7u4vMLLz0w+Pjo2Njk7Su/IwsvC20dLPkNQUnbZ4NDE
+yMO/wsPGxsPEwcrL3XdXSj02MS0xOjtR1tm/sbx2ctdSPET63NvNzcC9ytHLxs/h1cvY5d1h
+UVRHPj08ODY5PUd0y7+1tsPP3FtJTWzaz8rHx8vQ2djc7etuYltQU15eX19PRD46Oj5M7s3E
+vr7DzeN+4dvi1M7Kztnd7mpu8vhoU1FNRkFFbGpY7WJNTEdGTvTPyMC/xMrLztXQztTk82pi
+Z2Bt5eFzWE9OS0VLUFhhZWhwZk9NTlNf/9jHwsjGxsnIzM/X4u5kU23va2Rlal5YU05MT05V
+W1xy6m1kYFdaZPLe1szKztHW2NbY1tvY8O/ma2L2+l9ZSkZCQFnbWj7jydPS1cvO1uhuYWZk
+7vNyb2jh1cvU19xnYlpYTEtPWFBOWHLf2ODi1dLh9Ovf6PRjbm9oeOnY0+7r197peuxoalpU
+Tk5V/eB1cXr87O3s5ej89W1v8+zf39zvcWb2/mxsZG9u5t304XhpXFh1a/b9Zm9cYmV+a2jm
+2efs9uns3Nnbz9z2WvpmbWNj9t3g8+v1a3JuTlhfWFVoYXJ0W97c8vLW9uzact3a4+njX2jn
+Y/r3cfznd+XaZ17nYU5lZVhXXlzl7P7Z1e/b4uXv4eHj1/jv3+nzW1l2Z11bW3By/m369WVo
+WF1ZUFpu63bZ59PR19/c0drb2dja2PRx6V9YWlhSU1JWWF5ea1VS4dftbef+4+Tk79v3+/LW
+2fXX0Njc5tzuXl1YWU9VWlNdVmVoaW1443rZ9eXh6mjl2F/k7N776uf+8Wfd93ZV1t1i8vvl
+XGJff1RS8eBnb9Vr9m30bmpZ69dgZtXZX3Ll62Vk/+t6XubjenPrcGdganlxavTafHrh6Wzs
+33LubX3ic/z7a2jtfWhy9mZv6u3f7evxcnztZ17yaV9tZV9caWx+Y+/Z6t/Y7HzjbXjpc23d
+Znze9O/o53/Z7/H0dmJoaVtdX3BbXmxqWGvaZOPp6+7n6uPbddbg/d7maX3oYfvufvBgU11Y
+TFFSUGFdZuXu9dDQ1sXJx8HExcfJy8jbZldHOzQ2MS8xNjk8R1nfzr+5trSxtLe3vMLHx8/V
+0MnNzt1OWD4uMzMoKzAuMjtGVGvJuL22rrO0sre9wcbF0MvCy8nAwsnOVEw5Ky0sIyYsKy88
+SF7pv7W0rqqur662vcbM1PHaz93SwsTFyWRVOy0vLCQoLSswOkNa88W4urOssLCutr3Bytrl
+2MvMyr+/v8LTZUg2Ly4pJikqKzM8QVjUyb22srGvr7W7wcnY287NyL27uri4u8r6VDktLSkj
+JykoLjo+TNrIwbqzsra1tb7Fw8na08fDwLezt7a0vNVpRjYsKyklJCktLzdJZ+3LvLm6ubm9
+wcbJy87PycO8uLazsrW3usfmSzw3LyspKCkrLTM6QFb738/JxcPCxcTEwsC/vLq3tbO0tbe5
+ur3Cz2FEOzMtKygmJigrLjZBUXbUx8G9urq6ube3t7i4uLi4ubm6vLy+xtLqWEM6My0pJyYl
+JyotMz1P7su/u7aysrOysbO0t7q8vb/AwsXHyMjK1eFrRzw1LisoJycnKi40PlLgxLu4tLGw
+srO0trq7vcXIyc7PztDPysnN0+NYQzs1LispKSkrLjU+Ut/Jvba0sa+vsrW3u8DIzuN67O9y
+++ji2NDQ2ONtUEE7NS8tLS0tMjlBUd7Fu7Ovr6+wtbvByt34fF9ZX2pq/uHV0s3IzM/ReExB
+OTAuLi0tMzpCWdLDvLWytbe3vcrR1+xqfW5ebfLz59HMycTAxMvO7k5CPDUwMDExMzpASV/Z
+zMS8uru7vcPJzdTf5+33eu3m5NrSzMjGxMPGyttbTEI6NTMxMDI2OD1O9tbHv76+v8HIzc/V
+2dve6OXX1s/KxsO/vb2/xtDc2ng2LTMyLC0wNDpATG/XzcbAwMPHx8jK19rU19jZzcbDvru3
+s7W5u8DnST41KykoJycrLjM/W+LIubW1srO3vcPJ2O7wd19v4NjLwL67tbO3ub3NY1A7Kyoq
+IyIoKCg0RUzOtrWxq6uurrW/v8/561hRaF9i1svIuba6uLW/4HVMLyssIyEmJiYvO0TjvrWv
+rKmrrq62vcHa9m1QVVVQaOvcxr69ubq9yfNtPS4xKiImJyUrMzxPzrqxrqqprKyuubrD6m9U
+S0pMVWF02MbAu7m6vc/gUzUzLiUlKCYoLzdAesO3sayorK2stry91f5gTEtKS1dm3sjFvbi6
+vMTU6T80MiklJyYmLDI7Us27s66pqq2ssby9zXRiTkhISU9c98zBvri3ubvJ3GU6My8oJigm
+KC42RXfFtLGtqa2urrm+w+VnTkVKREZbaufGv7y2t7m+0/1LNzItJycoKCwyPFLbvbGxrauu
+rrG7v9P2XUlJS0RPcfXOwLu6ube/z9JbOzMvKSYoKCkuOUZax7SzrqqtsK+2vsvgb01KUUZL
+7+fWv726t7m8yOhePjMxKyYoKCovN0Nty7ixsK2sr7K2vc7ufFRIVllNZ9rayL68vLu+z2tb
+PzEwLigqLCwwOUj5z7yzsq+tsLW1usnT2l5NXF9OXd3j28fBxsnJ2llSRDY0NC0tLzA2Pkdg
+2Mu8trizsbe5usTM0u50b3tz+uPa2dnR2vX5bldMSj88Pjs4Ojk7QUhPa+LVx8LBvL3Avb/J
+ysjS08/U2t3h7Pl0cXdwb2NZU1BLSUdBPz49Pz9BSE9aftrQy8PBwL+/wsTEyc3O0tvb3OXh
+4+76em95a19fT0hHRD49Pjw8QEVIU3nk18vFw7+/vr7FyMfL1tfU6vTyeWVp+eXo4uLteG5l
+TktFPTs8Ozs9QEhX+tjMx8O/vb6/vsbMytXx3/Fdef5s3Nno0dDT1Nrh/FRIQzo0NTUzNjw/
+SW3QyL+6uLi5urrAzM7cZmZfUFz9bubP0M7DxcrN23BYPzg0Li0vLzI7QU/ezcG3trSyt7m6
+xMrL+2Z8W1p09ujdz8bKx7/O2uJKOzkxLCwtLS81P0pfy767tK+xs7G2vr/F5Wn+YVN15Xzb
+ycjKxcTWcVxGOTQxLCosLi42RU7wv7q5sa6xsrC1v8LJ5+/sXFd97u3XzszOys32b1M8ODUt
+LCwtLzQ8SmTRvrqzr7Gysri9wM3b5PRrYXHn38/Fx8vEx/hlWTs0NC0qKywsMTtJY827uLOt
+rrOys73Cxdb7/XJdZOnd3MrFzsvI7F5jPjMxLSkqLC0uOUdT0rq3s62trrGwtsLEzmlfYFVX
+deHgz8PExMHMfFpJODAuKygpKy0yQFjiv7a1r6yur7W5vc/c709PVlJo3tHJwbq8v7zC7V1B
+Mi0qKCcnKiw0RmDOuLCuq6uur7W9yelgUUdJTUxh28zCvLW1t7a/32M/LywoJCMlKSwzSvHF
+saysqaitsbbD2WhMQzw+Q0VY38q9trCxsLC9ze0/LiomIiEkJys0TNy9rqqopqeqsbjHWEo/
+NjY5Oj5O4ca5sK6ura61weFGNSkkIx8gJSkvQ9u8rqikpaaor7zJWD45MzI1Nz1M8cS3r6yq
+qqyvtc5MOi0jIB8eHyYsN1e/r6qkoqSnq7TIaEE0Ly0uMjtL68O2rquoqKmssLrbPy8qIB0e
+Hh8mLj/YuaumpKKiqbC/XkEzKy4uLDhJWMSxraqnpqerrbDKej8qJyAcHh8fKDRDxrKrpKSl
+pau3zEY1MSoqMjM6dsu8rqyop6mqrLS/x2Q2KigjHB4jIyg4a8KyqqKip6isv2o/Mi4rKjM+
+Qeu9t6+qqamrrq64y9B+NysqJh4eIykrNGe8s6ylo6itsLt0OjMwLSwzSFjmvbGvrq2trbG2
+ucLM2UUvKygjHyEnKzBC1LWsqaalqrG710EyLi8wMT56z8K2sK+ur7Gztru+xdluRC4oJiIg
+IicuOk7NtKupp6iss773Pjc2NTM6S3HRxLu2tLO1tbS2ub/Lzd9CLykmJSMiJi46Tc+2ramn
+qKyxu9RMOzk4NzpBTnDTyL65uLi4uLq9v8PL12M+MS0qKSkqLTM9WM+8s6+vrq+0vMnrUEVB
+OzpAQkJKX+7Uxb+6tLOzs7O3vcn2RzkxLSsqKywuNT9Z3Mi8uLWztrzByd9dUExHR0tOWO3Q
+xry3tLKytLq/zWhDODQuLCwsLjI6Qk/yzMO9uLi6vMDI2XFcT0xQV2D52s/Iv727ubq9wsjQ
+a0pAOjUyMC8wNDlAS1/hzMK+vb29wMvbfV1ZWWBo79bOx8C9u7q6vL/Izt5bRD48NjEyMzEz
+OD1AS2jdzcK9vb6+ws/g731z+ufXzsrGxb++v8HCydPe8ltKQz88OTY0MzQ3O0BKWu7QxL6+
+wMPGytDe397q5dzb1cvIx8TAwsnN097f4WVPTEY+OTY0MTM3Oj9MXurKvr29vsPFyNDd5+fw
+9+rj3dLNzMnFxsfEw8bLzt9dTUU8NTIwLi8zOD1IXuHLwL28vL7Cx83X6Pd2dPXs69zT0MvF
+wr++vb/AxdNtTkM8NC8uLS4yNjxEUubMwr28vL3ByNHf7PR3d3X47d7PzcfCv729vb/DyM7x
+VkY+ODEuLS0vMzg+Smrayr+9vr+/wsbN1dzl8uzg2c/MysbBv7++vb67vtVPRT82LioqKywu
+N0BRfcu8trO2u8HGzO5aV1ZVWmbu08bEvba3uLW2urzH+Eg4LSooJiYpLTVE7L+0rq2srbG6
+0k0+PDgxOEROadPDurKws7Cws7i7wMzZaDwtKikkHyMpLjVHyrGrq6ilqLO910IzMC4tND1A
+WMi6ta6sraytsrS3vs5rUDsnIygiHSEtMDVvt6yopqSiqbvI4zYrLSwoLkBHb7+2r6usq6ms
+trW2xd/mVi0kKyYcHissKkK/saunpKGms7vAPSouLygqPUtO0Lmwra6rqKuysrK+0NdXLyYp
+Jh0dJyopOMu2r6qkoaevtbxNLS82Kik5XVLkt62urqqorLKzsr/p69w7IiUsHRklLykwzLOu
+qaWho6+/vHQpKjkqJTrvV9a0rausrKiqtrevvVvf7CwjLSYbHiwqK0bGtKyqpaCpuri/OCkv
+LygrP2X7yK+pq66qqa+3t7rKaVhMLCIpJRwfLi0xbbqtp6elo6zBzP4vJiwuKjBP5s67sKqp
+raypr7m5vOVT4DsfJS8dGis3L0bBr6WmqqSlumHvQCclLC4vOnHCurqxq66yrq+5ubrE3/JZ
+LiMnJR4fKzhH2LeooKSoqa7KRTkuKCktM0LtyruytLSxtbi4ubq4vse/4y8kJSMdHSYzTNe6
+qJ6hqaqvzD4uKSgsMDpiwbq4srK3uru+vb29t7W+zcxGJB4iHhshL0q9rqifnaWuvWk8LiQi
+LUhO77atrrO9x7/A2trJvK+uvMi6YiAaHx0bHyxVrqWln52jrss8LiwlIStPx7uxrK2ywujp
+2uDYxbuup6vBUjwkGRkaHSc7166gnp6hrcNWNCkmKCw9xrCura2zv/VGSmnjzryyqqesvW8w
+HBcZGh4sS7qlnp+fpLToQC4nJis1Z7mxraywvtlLPkpf58e1rKilrMhEJhgWGRojONmtn52f
+oqzCSzIsKSsyRMWuq62xvc9rQztBWsu5sKqlp7VcMB4WFxkeLEq6p5+en6e46z0vLCstOuCy
+q6quucbpTD4/TW3JuK2opqvEQCsaFRgbJTv5s6Sgn6Grvk4wLCwtN0jMr6qprsPxU0dHT13p
+xLWrpKOs5TAfGBcZHSk50q2jnp6krshALywqLDdkuK2nqbO+ekxLSE1Y6cGxqaSnukQqGxcZ
+GyQ0SL6rpJ6fqLLiNSwqKzI95ritqKy3xm1cXVZmed/ItKqlp7lHJRkYGh0nM0fIrqKcnKKv
+7TIpJycsOGG8rKmqrbzP61ZUUU15ybisqa6/PCIcGRofJzjovKuhnp2frdg2JyMjKTr2vK6s
+qquyu8djRkRJ+Mi7r662zzwnHRoaHiY09LeqoZ6eoavFPCggICYyc7quqaiprbK72E0+PUrn
+vrOxvV01Jh4eHiApMUy7q6GdnZ+qv0IrIyElL0/AsaupqaywuMhiR0NHX8m8vc1MMSUfHx8l
+LDZku62jnp6gqbxKLickJiw6fL2vqqiqrrS9z31kWmHa0NjmTTUqIyAhJSozR9G0qqShoqat
+vmI8MC0uMTxUzru0tLi+zNXb3t7XzczMzuJgRTkwLSsrLjY/Ut7Hvbm4ubzAxsvQ0dPY1tre
++1JHPzw9R2vOvbe0tLe6wtVpRzw4Nzc6PkZNT09NTE1SXP/r2czKxsTEydDmWEpIS1vgyb67
+uLi5vMHNe01DP0JIT1pfW1FIPzo5Oz1BSVzlzcXBvr/J22ZNR0pc3si+u7u6u7/G03VPRUFI
+XN7Nx8vXbEpBPDo4ODo/Sl/j0s3P3WxUTUxSatjGvbq4uLm8wcvdYk1GRk5p2MvKy9X9XlFN
+SkQ/PD1CSlFZXFVNSElOWHvVycC8urm6vL/K311MR0VLXOvTysfKzNLb3uh1XFBLSUtOTkpF
+Pz4+P0ZOXfXbz8rEwcHGztz9aVxebuzZ0M3Lys/W1M/O0Nfj721pZVtUTkdBPT0/QEhOV19k
+afzh3Nrb4+js5+Ha0s7Mzs/R0dDPzs7Oz9Pc4+r5bmNbU1BPTUxLS0tJR0hJSktPWWH539XQ
+z9HW2NrZ2dbVz8vLzMzN0drj7G9pa2lrcfjq5exvXVFNR0I/P0BESlFebe/e3d7c29/d2NXT
+zsvl173N287P8mBg0NLm1svO1t73ZU9IQj8+Ojg6PUFHVmB61c/Vy8jIwr29vsPS5flmWFxe
+auvf08nBvbq4vdBgQjAqKCcoKy02T8y7rqmoqKqzy2dJOjU1MzpLbNW/uLOtrLG3ub3BzmJO
+PjAkHR0fJis4W72qoqCjqK+82jonJCo0T9C/r6eor7jDy8niXNzIxru+YEAuHxwdHSArR76o
+n5+hpbDsOSwkJCw8x66opKStvu1EQkpMWMauqqusuOk+HxIUGBon/LqlmZqgpr83LyofJDNN
+sKKkpqi6VT0uL1LbzrSqp6Souug0IBgSFR0pWa2jnpugsdI6LS4tKznSt6yqr7i+YDo6QFnK
+v7+upqq0v9FKKx4bGR0sOtirpqakrdlPPzU1My5HvLe0rrK5wko2P1V+zb+2qqizyN5CNzgl
+HB4pOFDFtqmlq8dUQ0BFPjM6vrS8vrq8veg1PF3WycLEubLH9WZMPEpcNykiLEJDTcWwq6vA
+XWPaakU3PL+3y9K8vMByOEDq0/Xl1b62yX/MxXBNS0cuHSNASE68tLGquUhJfkVCRkbCssDO
+vMTTZj0++c/pzMfHvsDPzcbPekhLLRcdPEZRt7CupLU8OEs+R2n7uKy23d5cVltJSti8xcnL
+zsrBwMfS5dVIOjcbFzD2VLqqtK2tTS5GVUN+yb+1s+pFU3x83MrQw8dkSF7Nwbu7v8K/Yz07
+GxEmfl28p6+sp2wrOk1B7sXFu7DNQFPQy8bBfWnlSz5Pzruur7u+w0s4PxwOHm3rvKSrrKTc
+KS9GPtu6w7+tvUBMz8nLvv5MYExEW8a3rq6/1cj6OTUcDRrMwsunoaWjySQlP0Z6vLqzqbhA
+T7u1xOVAOUZBQOKxrrS8xN5EStO+KgkL0KlMrpyfn7ccFjC7uq+npaOvLibLq7xwR0JKPTRj
+rqasyz5AWDQuy7AjDx/hTlSvqKasMB9VsNnEqqytsUM3uq/nZd9SWVk3Pbux22vP2FhP91Ms
+HiAtODpJuKiy5khKz7y8uLG4vr3K2MbD4HFqRTw9Xbu9YEXdxEk85s1JKiIpO0Y+97W62eJ+
+6rm0vLe62ebOxreutWw4OD81L/TBzsTNPnW2VTIyLSssO8u2vn5TVmDYvrOvs8diVEhpu7G0
+wtFRPDo9SUZXyOBexsHSSTY3MjE/XXVi+tDPbVbHuLzL7cq+119dyLa81VBYflRFR0Y7Ql1p
+2b27vM1DMz1MTkjs0NzQUUNd2+Pb0d/W5V7639TD11hYwsPc50NeUDxAPMSxvcxbXVpJT+zG
+zOjfSlNMSmRQ19xe2/FVTWDebtPUSfLKxNhFQEFOXlXEtr/S1+pGPVjJxdHVzsxfQkv1zdxV
+W09Valxq3OLh7mx9ZVxbUVRKSFvay8i+yNVnREZZ3t3Pwc9lZV1fXnB4eNTpXF9q9PxzaurV
+319ZWmdjVVvv1d/ezMzfV1JVVGbYx8xtWltoXEtZ3tx3WvjV3/753HdXZXX68V1efO1raOXj
+3t/Z2v19dO/c5+XdelZMWGxdbuvZ2/149+PnfXlzXV1u6f5tZl1wbmV84s7abl913+5ebH3m
+ZFR27nNn6tnpb+v5dfdx3M/aa1tt3tbgfW5oYWdaZN/yWF7oXFzb7F5e6910ZnVp4dT25NTb
+ZHPr6lVR/+BlYdzX8Flc/WhaaO/p9HFgWV9weO7m2tra2NvqcujjeWX25/ff4UhF0s1OTNbK
+Xj9Wz1tEeszlXXrycGxh28nYfc/M515rclxVXnJsZmR6887GclPZ105Me959Wlvd5GFn9eFy
+etzwe3Za6tz6eN/f/ltv/l1e59jiXlPoy+BDTc/xR2fjzd9a7tLoYlhu32dd9N96cuDg5/jv
+5Ox7/N5rVXzuX2ZscXni+2jf5F1x23Rc69x6XnHocVZg09xOXtDmUVzW1uR56t5uUFRfcm9f
+79Pf7+HY7Ghu83R48fv1/nBp9/Lo4nvq3d56ZuvX81Ro7GFPUVxrUEpm7m9yfm/nc1Ff2n5Z
+2MrS3tTT1dLg4M/S5tvQ2NvU2OhzYHzg295iPjI0OzQzRFtPT1BZzcbjzLu+xcXJz8rGyMnM
+z83N2tbU7GhuY0tIT0tGYeFbSkVGSkxIR0lHS1ZlYPbc2dPOy8rHy9TTzsfHz9rd29zn/uvd
+3dXW5OXf9mFm/+tTPj9MPzU4PDxATU9Z7Ojp0MnMycLGycbExcjS5N7b3NjZ3NfW2tza3/xu
+fPHz9mZHP0Q9NTY7OTtHTVX97e3PxsnFwMTGxsfKytPo5uDe2Nnh29DMzNDa3+R/aHN9ZUk+
+P0E8ODk4OT9LTVns49zPx8TAwMLCxMfFy93s4NzU097c087N0Nrr8HddW2ZfVVBCOzw9Ojg7
+Oz9MV2Lt3tjJw8G/vsDBxMnMztLb5Ofm5OLi29nX2Nje7enn82NdX2BVQzo8Pzg2PT89S2pz
+7dfPysLAwMDEx8fLztXi6+He3tPS3N/f39vW4/39cWxfWldaXk5BQEZAPUBCQk1eW17u29PN
+ysrGxsnJzdXX2+Pn3uLe1trc2M/O0tvqem9zY15bVFBPTEVERkVDQkhJTlVZYPTf2czGxsbE
+x8rL0Nvc4+7p5uDe2uHp49/h7Pp9a11bV1FOTk5SUlRVV11qY2n4/Wdu8vHx6t/k4tzb4ODj
+6ebj5u70fWtv4tzZ0tfd5uzv6f5zbVtWWVxXYGFjefNvcef7fersc2tfXWdpY2NrZ2d4f3d4
+7ebr6d3g7t3d6+Th8+vm9/Xt5uzq6ez67u1/amBhdvVnZmdXXGhfXnNoW1pebnp7a3b27+La
+3OTl6enf3u91/P5ubPn67+7o6f3s+ujq/vfo+/pnXnJpZWdpa2hja3Nrbm1qdvnm7Xz16vRm
+bnZsevru6NzmfOfp5+nl6N/d4/n6fW9pZGNcW15gWVthcnNu/mttePHr3uTs5ufr9N7c4d7i
+7vfs+OblcGX19Gd0fWtkZ2lrYl9qX2JveHRpaGJlbXf573xy7urf2ufi2uj/6dzs6+/s7v3u
+5Pr372lpfG1faGZ6bFtldGlefnlmaWdlcH9icO/m7env6eHy6Of27Ovo4+Dj6ODednV6bXr2
+bl1iYktQ0N9gZ95faGvx5Gteaed8/3nw9m/8f+br5+vy7Oz2b3199v7t9nN77vdnb+nu+urp
+bXD9cG/9e2nuaFpiXlhj4vZ46uDo5/Vw6uDl6OT4/unmferc/k3uzWpb39thdF51+2VUXnFa
+Xl9maulnX/X8/Xvr43ru2uJw39ri3nzs2uPm5X5oaOzkZ2X99uvkbGlrU1Ht8VhpeFxV9f5g
+av7re/Bo/Gnf3Nra2fH+4nLt4OZv395w4fz4anxW9+JgW+/pUVlqbWJwXG1uYPp54d/2Y+XW
+42lja3Pz8n9o83/p3dv98tngbfvsb3fzZlv4fWNn7l5X5Nxv7N9mYHFiXH/x7nxnbH717fp3
+723m09zycuTt9+v8XWpwZV1v/HB1Xmnk2PRoc+vpbWdtbVVa38/f8nd73udndN9+WV1ubWV0
+4N3q9Hvq8XVudO7o8Xp8eHH7emNlfXx47OJ4+N3pcfPqaV5//nJnZnBqZG7o5vdvd3tmXGBn
++Ovp3t7l6enr6Ozt/ffu73F1e3t9cXH34vJsbPR4ZG50ZWv6dnfw+fl77uTn6+rf5OHo/nRp
+W1JPSEM/P0ZMXOXPysK/v8HFxszR197j+ODY1czHwb29zUkyKSMgJC06WMe5trWzsK+xt8Vb
+PTUzNkFm0sK7uru5trS0trm/zGQ6LCQfHh8mMUzOurCtq6imp66/VTQqJys1StbEw8S+uLGt
+q660vs/nUTouJiAeHiMtRs22ramnpqapsNA8KyQkKzzevrq6vby2rqurrrrPYVhcTDkrIBwb
+HilBw7GsqqqppqWqukQpIB8mO8m0srnDx7uuqamuvvVMU9XKbjQiGhcZITXbvbevrKafnJ6r
+aiofHSU507zBzt3LtamlqK/DaFJpybm8SiMZFhgdKkfe582uoJqYmaDBLiIiJiw5T2b1ybix
+r62sr7e9yOBdbce8ZygaFhgeKkTxaF66oJmXmqPCMSUmKiwwPV3Ww7m0s7Csqq/BcEtIWsez
+tzobFBkhLD1YSTzWoJaXnam9Qy0sLSgmMOq9vr+6t6+mpK5lODxczr+/xmktHRwlKigoLjtb
+sp2Znaiss/s0LCspLUXe2M+6rq2trbHGYV15VkpY5si6uUEeGiMvKiIpQMmtn5+qs62v2jQw
+O0BDVHTyyrGssru7vvZGT+TffvPkz7irtSoWGioqHR86v7avp6Soqqiv4T9KZkk2NU3IubzK
+1sW2uM9NSWjVysvXzbmuvyAVHzUkGSFtu8Szp6epo6e+3r7COy4/dU1H0bu7u7a+6F3tzczL
+3FRdzL2/TyYbIC4pHiFBvb67rampqauyvL/LVT9BTVNryru+ysrK0tnV1M7PUTtF3eQ5KSkz
+MScoO97S1cOyrK+4ubKutsfW2lY4Mz1qycHIyMLLblzj1l5ESEU9PDo4ODs9PkzVxcjJyNLb
+v7e6xL20utxGPT5FR0ttz8O/x9zYvr5PMzg9MzE/Vt/lYWrPxsjT9eDbX0b6wcLNxr7G2E5B
+QkxNRlDUx8S6utJt8GRANTQ9VXFLSsm50l/LwtZdVXfVXUBnxr7F41voz0I3VFlIesnPwbne
+5MXhOD9ZOTxi0lrXxNLgvr37fdFvQ15TVF/W2OTlaVFJbnBNR//d62vn1MbG7WZe2/lifvrn
+32VV6+Tf2uRmU1dP6G1hf+7eYvT+4FlhVGlWX+vi3Ovu89TV3N/PzNlqTUpYem9fWWrZ2H75
+6WPz3VtNY+1p+dbT315UbGZaYuDnfvzs3dbk2tLZ4HJWTExSXVVa8d7T2t/f4OhuYmR1ZnXp
++Xzl2/plY2dufl5Tb9nv4tHZ5Oj/6uBeUVdRVWD52dbd6efrfG999Ortf3Zub/d+ZWn6fG1v
+dnd46/h07eb29G1ZYPrtfWRs5tnb1uLl5eV5amltb2FcZG1qcHl0bnTw6vxqX2FvamR+6ODd
+6PDn3tje6uPl5ubt7vfs+nZqYl5aX1xaZGt59PZzZnZubnZ6/X5yZGvp4+Pl6N7Y1dnZ3u7s
+6eru+nBfW1dWUlRcX3Dq8Ofd9G7z9P5sbXZ2cGFlb/R7+ujb2d3e5ODe6vVw/ux8bWFbWF9n
+ampnbnN66eXs+/j4eGhkev78cnpybP3t497e3t3e5+7reWVma2tpYF9nb/ju+G9qbPz2bHX8
+/e/s7vXt7/96bGZgYGZ09u3x7uPe5erufnJveX3++/D9fPnz7/f5+PHw8/D6amhucfbr6/pu
+amhfXV1eYmd9/PL973Fx+PXt3+Lm3uvm7+jo737v8PBu/OXv9+30eGJi9/JeXmxgWGFoZW5i
+Y/L3cPbk7+Xy6Oz2+3h46eDl3OTs4vP98+32eu5lffJkZHlmXVRr32Jb+fxu+nNr6nZq4+vd
+7Xbh2V1Na+ZV6s/n2cz89Nb4aeBmcnVeUmRUXmb5Wmv/YXT53/ba/NvudmTUeV7t63Ra+mvi
+/uNV3d952ePc5d18XlFUbXpLadJGe9Nybc5uU8jV7fNb2dxPTtbdTl5+4N/4T9fLal7x1v5v
+SvPOYUv+2WbmWm/F6Exowl9V32nP6E1fv0xT3190z1BJwP5KbdRR6HtW2M5SUczkaF9/8tH4
+a+/c9ujO1c3Y1tLF1N3Y+f1nTTg5MS4zNDRAbs6+urawsr2+v8rP0dTKv8HCvLe2umYxJx8e
+ICElO8azqqKgoai58TwtJSElMEbRta6sqaqvtLi8yM7E2EA4Ny4mIyUtPEVsuKqpq7C6yGc+
+NzY6SGbs2sa9xt/veXzaxr66t7a8yd5NNysmJS48Qna5r6+xu9BnSTUyPEdV0L67t7fF/11L
+Rktp28O4tbOzuM9LNy0nIiY0SO28srCvuN9LQTo5PEfQurWysrvKez83Oj9K5srAubGxtbrJ
+TzIqJyUlLUngv7Ctr7K/YUY/Oz1HVdi5tbvB0mNNQz5CVd7OzMXEzdDR4fzx6mxNPzcyMDZI
+7Mq5sbK3v9xYSD8+Rk9hzsLFx8jdUkc9PUVNXuHW0sjDxszLycnNzdH6TzwzLzE7UNjBube7
+xOFKPjw+Sn7Sx7u7yutRPTg8RGLUxLm3vcTO6GNRTVPszcvHyNZROC8sLj7uxry1tb3YSTg1
+O0hn1cS9ur7VU0E+SXvRxby8yuF9T0lLTl/YysTAvb3QSTguKSs3SNq6s7W4wHBHQ0dQ7MvG
+v77I31ZCPkZTXFhLXMvJ083Q2c3L3m3kz8jMYUM3LiwzQvW7r66wudFRQz08P0750sbH0O1V
+RkVPUkplzc/LwcnTysXMy8fPzsxsPDQvKSw9Ycy2r7K1u/4+Ojk7S3ThxbvByNdLPT47O1TP
+yryytrq7xNzuX1Jfdl5JQjorKz9k1ry5wcDFXz8+P0zVwcvKvsfYdEE3O0NN1768uLa8yM3Z
+8fns18/U/EMyJh8oO13EtLOzsr5eQjw9V8vI0sK9x8pwPDU4RWzVxL27u77Aw8TI53nn62U/
+NCsfIjRNzLKvsa+09z89OUrGu8fEusjmUzYtMENh1b25t7a8x8nGzNvg3dXIzEEsIB0nPVzE
+r6yrrL5CMzE4VsrJxba4wdk8LDFBQ1PMvrSts766ucPPZ07tybzKMiEdHik7T8qupqSsyzsx
+NkFOX9bCsq+8Wzo+Rz48SeK6sLO3s6+2wmBASf3NaTUlHR4qMkPBraaiqcFLOjM1PEVyvbe3
+sbnMcEAyMjpNxLCtrq6us79lPT5VZEctHh0lLjvpv7Onpa/JTDU4Q0RM7s2yqbHByms+NS4z
+5bOurq2trrHBUUE7NjEjGRwqNFq+vrGjpbPROjVXTz9DUNK5tbi4tLfMQzIzS8O4tbCsrLTI
+UzM3USoaGR0pTMbIt6ijp6/cQT08MSwuQLyqqKmqrbhzNCwzVcy8tK+qqrTdSkk5IhYTHClK
+v7OspKOptno+NTMvLTbYtq2qq62uu99OQ0NGRV7Mu62rrbbCbzAfFxQZIC1YtKWdnaCrv0k3
+LicpOG2/s6+tq663xd78cFNBPETpwLe4tq+y3CsbGBodICtItqOenqKpsLtlNSssOEhV6Me4
+srS5u77F3Ek4NDQ+WWvgwrexssNHNzk4NDI1PmPV19bc6ebxZ2Rx4tHOz9XOyszV2tTT3l5M
+TVhx0ci/ubKyusp8STs4NjQ1OT1CS01NVVlVXW1f8c/Lwr/Bv7/I0+B1ZFxdcPXt5dnEu7m6
+vcLN41VAODc7PT9BSU9RTkZBQUNHSFXvyrq1tri6wttcTU1YZG1/3cq+urm7v8nY/VhKRkVG
+RkdMTUpJR0VEQD06Oz5L7cS6trOzt73M7VdJRkdNXufNwr29w9Da4OduW1heb/91a2Nsb1dF
+PDc0NjpCWdXCvLy+vb7K/U9HRUhMVnXWzMrJzM7S2dzj+ePT1NDNzcvP6VlKPz9BPz9GTldj
+bHT/6Ozwc11ZWmV/7uTcy9DTy8/g+G554NnPyMTBv8bU62pWTEhHSk9TU0xIR0ZKS0lNXHB4
+49jV0c/W08/YZ07vxH5jxsfU0dTBvtXb2mZTcmFt4XlZUT89Pzs1OTtAWm3pyMK/ur3GxMDH
+ze5cW19QTVJZ+9PNyMLDv8HR1c/R/lpDMSwsKCgrLTl6xLaqp6Slq7LDTz86Ly40PFbZ3cm4
+tLOyuLeyu8rYTkJDLyEgIiMnKi5VubCppaShpLC/aDYvLSksO0rVvLmxq62usbq8vcva1U48
+QTAkIyQjJigtT8S3raikoaawvO86MCooMDpB37yzrKywrq+6w8bHxs7sTUU7KiQkIiIjKDVf
+wbGoo6GiqLC+ZDUrKCctNz/et6+sq6yrrbe/xsrK3FtJRTkoIiYlISMqNmfBtqujoaSpr77s
+OysoKCoxPmHAr62sqquutb3JzMnS+HNMPjEoJyglJCkwQ+W/sKejpamtt85GMSwqLC81P+K+
+uLGura2vtbu+w8TJz9xfPCwoKichIio0Qmy9rKalp6irtMxIMi0sKiotOFLTxbmtqqqur6+z
+vsPDyNFpPS4qKiYhICcvPFTDraajpKapsMBUNCwpJyktMj1dzLuwrKysrK61vcfLztDW/U8/
+MCooJyUlKjZN1bmspqOlqa68eTsuKikrLzU8T866tK+tra+1ur/L1tjQy8vVXjwvLCkkIyYu
+O1DJr6ejpKituc9MNi4rLTE3PVDTvru6t7e5v8XCwcXEvru7v+0/MCsnIyEkLDpVyLGopaWp
+r7rOUTowLi82PUVQ/MzDw8O/vb29vLq4uLm6vMlYOCsoJyMhJjNJYsmxqaeprLG80Vc8MzQ4
+PD5EUGjczs7OycG/wLy2tbe3tbvSWTgqKSgiIio8T2S/rKipqq24zGtBNDE2Oj1HYnreysXG
+ycrMy8S/v724trvJ50kxLCwnIyk1P0zJr6ysq621yWRFPDg0NT5c6fDVwb3BydLb1M/e3se8
+u7y9xnU7MTMsJSk0PT96uK+vrq+0v/VBOj04MDts1OLVu7jDys7b6PFnYNG/v724usV3OzM2
+LCQqOj087LOvsa6utstUPTg6NjNF09Hfxrm6x9nZ1PVMSmTc2dG/t7i7w/09OTQpJS45N0PE
+s7GurbG6zFI9OjczPGVwccW5v8nCzHR6ZEZL+/lzy729urW621NBMyooLDE1P965s7OvrrXL
+YUtAODY8SFN0zcPBxMfC0FZl5lxPfNbZyr++u7e7zlg5MC4mJCw4OUq+sLCtq6+4xPFKPjc2
+PEFIZtPLxcbJw83t3/dWXvTr2sa/vry7wNxFMS0rJSUwO0DPsK2sqKmvvNpPPDEuMTc6StXI
+wbq6vsXV7WJQUl1728zCvbm5u8xROSwqJyMqO0J4saiqqKatvedFNi4qLDM8TMy6t7WytsjX
+3U5CTVha6sjAvr27u8ZbPi4oKiYkN1tXvqenq6erv+tONy4tLTRAUc+4uLq1vNbS2kxKWVJc
+29DJvL29uc1WSCwnLyUiQXFEu6WrrqWsxddNNjEsLTg/Sda8v72zu8vJ2GRpYFfv1NTEu7/F
+w9ZOLygwKB0x7zpgqKiwpaS3ydg+MS8sMD4/VcG9vra4xMnaXnNcSnzM1cm5u8K+xlRDLCQv
+JR47fjnFoqqvo6i/yfU5Ly8wMDhP5tbCub3CvMLn7N5dWNfN0cC6u7zG2eA+JScwHh5NZzm4
+oayuo6vFz184Ly8wMDxRcc69u7+7vM/Y0vNdd9PJxr+4ur++z1JAJyAuIBw89Tq/oaqtoqm/
+xXU6MjAzMTlX8tvAu7++vc3b2Xhl6tfLxMK8uMHGxFA7LCApJRwv20POo6SspKe7xec7MS4u
+LjJCXOjEvLu4vMPDy+fb1eLLv8jBub/Lw+U8LB8lJxon0kZWpaGspKKxv9s+NC4sLDA9T9HA
+vbazur7A0+jZ19vNyc7AusnHvFQ/OB0fLhwe1s5Dqp2pqKOwxes4MTIpKjs8Psi7x7avv8G7
+z93N1trIx8q+ub/Jwtc/MCEdKR8bR75JtZ2jqqSsxO47Ly4rLDRDUtG9u7a2vcLG0NXRzsfD
+x8S8wMfE1GNFLx4dLB4bVrxQr5yip6aywugzLjcsJztRTMS6u7K8zb7JX87B0MW8vbq4vsPH
+6UU0KR0eJx4j177Lp56mp6zC2UwtLjQqL0lOzbrAtbPKzL/e+8LAwbm6urW8y8XvOjUnGx4m
+HiTjv76poqSlrsLIUi8xOC4zSVvKvsm4tdnfxN1vxbu8trO2tbjK3mo2Jx8cHyIfLsO+uaSg
+pqiyxM8/LzY1Lz1V4LzCzrvGUWTa+tS9trGvsK+xusniPyoiGxohICROt7OqoqOmsc7+TDEu
+Nzk9V9O4t9LJyVBNZV/Yv7mvq6ysrbfcX0QoHRsdHx8ncLOvqqKgp7bN80UwLTQ7SGPMr7LL
+w9FFPj5F9cW7raeoqay5y2YvIRsZHR8hOLyvq6Sho6/P/U8zKzE4PlbVsay6vL37RDw7TubP
+s6ioqKiuvdkzHxoaHBsfOsWwq6WeoLHP4ksvKi44S1njrqi0ur3iWT41T9LWu6upqKmwttsq
+HRoaHBwhSruyqqKforHUckYtJyoyR2XFqqetr73cWzs3S/XMs6uqp6u5vl4nGxkcHBwoW7Ws
+qqOeo7njXT4tJSczSGG9q6anr8HMZDw8TuS+sa6oqK26TjIiGBocHCM6z6+opaGfrMVpPzAp
+JSs5T8u2raWnr7jIblBITtW8ta6ssLnIQSIaHBwZHi1nr6ypnpymttRLNicgJi4/bc6vpait
+r7a91EtW0cnEubSwuOFGLR8gHhofLTzPtaqfn6artdk7KiUmKTI+Zbmsq6uqrK+81NbO19rI
+xcfMZDsrKigfHiYuPWnJrqOlqauvvE4xLSwtLzNJy7y0r6ypq7O5vcPN7GJWVVc+Li01Likq
+LDlWWN22rautsrK31Us9NjQxMj9W3sW9t7G1u77Czdnuc2757+jZ3WtURjszLzExNj1HaNDH
+vrq5u8DO+1NJQ0VNX93Lv7u6ubi7vb7IzNTtX01JQzw3MjAvMDE2P09+08S9ube7wMfP7FdH
+R1vt5dK/uLe4ubq+xc/qWkpCQD46ODo5ODY2OT5KWHjRwby7u7u9xdPuY1ZUWmbizsW/vLm3
+t7vF0f9MPTc2NDM0Njk8PUBJT2L52szCv7+9u7zB0OprWlRUX+PMwb26uLa5v835TT01Ly8u
+LjI2PEZVdtzRysfHxsjIx8jHyM7c73VfYXnt38/Fvbu7u7/J408+NS8uLS0vNTxKatvLwr+/
+v7/Cx8jMzMrMz9Ld7P7t5d/b0svIyMvQ5VpJPjcwLy8vMzg/TfzQxr++vb2+wcXJzM3Ozs/R
+09HP1NzZ2d7j6uzvblpRSUA8NzU2OTs/R1V1283IxMPAwMLFys7Q1drd3NbY2tnb3d/n3+Tp
+6PhiVk5JQj07Oz0/QklWYvXbzcnEwMLFx8rO1uByWvfGxfVmzsXlYtrPfFBe7VlFQ0pJPjw/
+RUNGTmDs1svExcfFxcnQ09PkdPHsaWTv1tbl4M7P3+Lb3OtvZ19USEA+Pj5ARUhOXX3d0M3O
+z87M1evm2+Ht7u3Z2/7u19tqb9bL2u/Ox9V1bGpXTUhDRUZDQ0hMT2Rx7dbOzdDPy83u9s3P
+XVjg2mpQ/dfnaN7J2GzZy99n29xUT19SPz9MTEZHTl1nYfrT0NPOys/U1eHc1+heaN3qW3rb
+5NXY2tHN093V/Vx2cERCVEs9RExEQkxgd3LjzczOzsvI1uva4F5fcPfw7/jhzM7M2uzLxt5X
+28h0S2dmSUVFPD1EQkBFYuHq7s+/wdDTwcbmdN3ZW1pt/vF179vU19bI0tfT2dbV5U9JUUw8
+NThAPjtEdtXb08O7u8DDwsXT6/J5W01QUl/yZF7GxNrTxMHHyevpcEY8PjsxMzk6O0BX2MzG
+u7W1u7u7wNX99l1JQ01RVlpk08zOyMPBvsLPzc1QOj09MS0vNTk5PFPTx762sa+xtrvAz3ZY
+S0VBQUJGXezdzL+7ubi7wL/BYjw7PzMoKC42Ly4/z8XNva6rrbO1srnRXFZPQTk5QUpNWN2/
+ubu5s7S6wtFaPDc2LCUoLi4sMlbLzMGwqamtrq6zvt1TRz84MjU7REtfzby4tbGur7W8x2A6
+MzIrJCUqLC0yRdnFvLCqqaqrrrK7zlxHPTYxMTY7QlnTwLiyrq2vsLS/dkA2LyslIycrKy47
+98C8tKunqKutr7XFeUg+OTQvMDc+R1vNu7SvraytsbrHXjswLigjIygqKi5D2sS7rqinqaqr
+r7nG/Ek9Ny8tMTc7Qv3Cu7Wuq6yur7XGXz40LighISYoKS0958e/sKilp6iqrbPD9kxCOjMv
+MTY5PlbOvrmzrautr7K72Uo5MSwlIiUpKSo0T9fIuq2oqaqprLG6xOVWQjkyMzc5O0JpzMS/
+tK2usbK0vNhNPTQtJyQnKiosOE7vz7uvrK6tq662vL/TYU5EPjw8PD1HXujRxbmztLa1tbzO
+W0c6LyopKyopLTlDTPHAtrazrqywuLe3wuH2d04/PkBAP0dc793Ju7m6uLa6zmtcSTguLy8s
+Ki45PD9VzsPBurKvs7ays77Ozc9nR0VIQj5BS1FZ78e+vrq1uL/O2XlENjI0LystNDc4QmfW
+zcW6tba2tbK3vsPI5E9KTEY/QEhPXHfRxcO/ubm9wsvcYEM4NjMtLC8zNDlFXuPRxbm3uLaz
+tLm+w83mal9WTEhITVJe8tzPxsO/vb/DzedsUT42NjAtLS81Oz5K6c/Lwbi3tbW3uLzEytDr
+YVhOTk9QVVxr4NjNxcPAv8LK2XRfSzo1NC8tLjE2Oj9T1MrFu7W2t7W3ub3GyNHyZ15QS01P
+Vl9r6tnVysfIxcXJ0WhQUT00NTQvLzI3PkRQ2crIvre0tre4u7/HzNNzWVZOSkpQWV5j383N
+xr6/xMbJz3xQTEQ3MTMwLi82PUZR68a+vrq1t7u5ur/I1+VvT0pOTkdKUlpn4MzHx8C9v7/A
+yt9cS0Q5MC8xLy82P0hO/8m/vbq1tLm6vMDK3fZjT0lJS0hJT15t6NDLysG/wb/BxtFjTkk8
+MC8zMC81PkVMbsm9vLu1tbq8vL/K2/B3VElLTUlIUlteetbNzcnBwcG/ws3mWEpCNjAyMS4v
+OUBHXNHCvry3sbS4ubvCz+L9WktFRENCR09cZubQy8S/v7+9vMXea1dGMy4zMCstOT4/T9PA
+vbqzrrG3tra+0d/7Tj8/QT48QEpNVOjJyMS7uLm5ub7QX1JLNiwvMSspMj08ROK/vLuyra60
+tbS6ydrlWkA7PTs4O0ZOVP7NxcK5tLe3tLnH5FZKOy0rLiwnKzs/PFbAuLu1rKuwsrC2xd/y
+XEI6Ozw4Nz1KUWHSv7y7s6+0tre93U5HPC0pLC0oKDNCQk3Lt7e1raqtsrO2v9ZqU0Q7ODk7
+OzxEWnrWwrq2tLKxtLjA1VJCPi8nKS0qJy9DRUXRtLG1rqqtsra3vtVeS0I8Nzc7Pz5EZNvP
+w7u3tLOztbnC31NEPC8pKSwqKC09SU7aubK1r6usr7S4vc5qTEQ/Ozk8QUZHV93Mx761sbW4
+t7nG+0tEPS4oKSwqJy5CSUnWtbG0r6qrr7S3vtZkS0I/Ojc5P0REUtXGxLyzsba4trrJfkxB
+Oy4pKy4qKC9FSkfft7K1r6qrsLW4vs9tTkM+OjY3PUFDT9vJx7+1sbW2tLjF5FA/OjAqKSwt
+Ki07TE/9vK+wsKyqrbO4vMbfTT8+OzY0O0E/RnfKxcO5sbK2tre9z1lDPDUrJyosKys1S2N+
+xbKvr66rrK+3vsnfUz88PDk0N0JGRFbPv766sa+xtLS4xHdJPTYtJiYqKyktPVZh27qvrq6s
+q62xusDK6kc8Pz43NDtGSU3wyMC9trKytLe6wM9bPTU1LCUlLC0pL0n17cqyrK2trKyutcDN
+021AOjs8NTU+Sk5d18K7uLWzsrW5vMXeTTkzMSokJywsKjJR4djAsKysq6urrbS/zd1POjc6
+OTU2PkdMYNHCu7aysbG0ubzJ5Uw3MS8qJCYsLSszVtXOva+rq6usrq+4ytzoTjk2Ojk1OEBM
+YN7IvbizsbCxtrq9zl1DNC4tKiUmLC0uNlXVx7mvq6qrra6vudDj7Uw5Njg3NTpEUnvTw7m0
+srGxsre+yt5WPzEtLSolJCsuLzVPyr23r6qoqq2urrbO8mdHODQ2Nzg6P07908a6tLKxsbO4
+v8jdVEU4LiwsKSYoLzQ3RdzDvLWuq6usr7G2xuloTTw4ODc4PEVOadTDvbm1srK1ub7L2mhI
+OzQvLSwrKi41O0Bazb+7trCur7K1uMDc815EPDw8PEBGTmfcy8C5t7m4t7vCytl3WUE5NzUw
+LzIzNjxCR1zPysq9tre6uru/zNrxX1JKRkZOUVJo3tHNwr++vMHAzMzlWFtTSUA+PTw5PD09
+PktNXPLj18vDycnCw8/Ny9Xmd3BfZ2pmc9zY5dDKycrNztPWcWxcX1VMTVVNSUlJR0tIRE9U
+T01XbH7l39PNysvNyczZ18/i6Oni4/ng387X1tjX7n9uamVfXm5sdWT9cHxfVlhKSkVHRUND
+Sk1PW+zb1czNzM3KzdHP1MnW19bc0XrU9d9qX+tZ3Wln4vrk/9zl2nneXF9LTEk9RD1HPkVJ
+T11j+NLOyMnHyMrJ0tDV2Nrl3vvbcu103fXq9N3d2OXj6d707m7rWlVTUU9JT0FIQUpGTUxc
+YPLf28/KyMzMytjQ597d5N/99Gt49ejb2tjY4NXt8HJ2cHhjWl9eZmZfXmFVY1hZVk9VXFxd
+aXFq+/r95+vi5d/i3t7g2PrX5NTZ3NvT19Xh5uru/Xf2dGZiWWdfWVdUaVtpWWtlamRrbmte
+V15canb7duvmf+bk29ja0trR2urm3/Lt9OP64vf3ce9tZmxlbmd+an5rZmtv72ZrXl5fcGVj
+aGRYYWJwbX7p693p2OLW3+Xufu13/XDyevh06fTddO1v3/Z74Wrva254aNtc3Wbma/HwWnFf
+amtlallsY2Zq6vtt329xaf5hbtxd7edy7F3c4+Xb29P94X7e1Fz47dhsbudk1Wn173b9TmdX
+dVZZT1tjWVZkYNpx/vnt0fb2edDy6OX/1e5w7O7ceO5q7mZ9+/F96+vz7+dq1FzV8m39W2Ze
+bFNTb0xoVVl+Wdle097p12HeXtf0XOTucFnvY/zRdtTh1dZeyN7n4tn5cc39fONjbmdjR05T
+Sz9IS01QZV1x19fVzsrYzNbv6dzyXn78YP7jX97Z5H7P0+bY3OzTadR759J+WvHzWUdKVT9H
+PkJFW0xQ6tHTz8XHyMXOeNLfZl5jZln2eVnp3t3b19nTy+Pg2NN539Ly6Pb7WFRaREBRPjk/
+R0JEZvHfysLIvrjDzcjP9mxZSkdPUkhW/n3618zPxsPKyMTV2tLc4O1meW5KPz1EPjM1PkA/
+SFPiwMXEvLa1v8bFztdiR0pORUBDTF5l7tHFvL/Cvr7Ez9HP+HBcT1BRQDc6RDwuNEVBQlFe
+zLy/wLmvtb/BwszkYUdESkA7QExQV+3PxcC8u7m3vsnJye1QUVxRPTk3NDwzKzVLRD1S07+9
+vLizr7TAxb/NXEtHQ0A7OkBNVF7cxr+9uri3tr3Dw8tpUVJNRjw2LzE8Mis1UUk9VMy7ury4
+sa61w8m/x1JHR0A9OTlCT09V38S9vru3tba9wcLH8k9bXkI4ODUwODQrME1IO1DMu7q6ta+v
+srvHw8hYQEFBOzc3PEpPU/fGvby7trO3vL6/yH5TVVpKPDs4MjQ6Lys6TT4+88K5uLixrayy
+vr++00xAPzw3MzY7P0ZR38q/urm3tre7vsHK61loWUZBPzs0Mjw3LTRIQj5gzr64t7WvrbG7
+wL7RT0E+Ozg1NDtAR1zlz762tra0s7a9xszmW1FLSkY/PTw1Mz84LTVGQUfvzr+4trSvrrK/
+w8ToTD89PTg1OEJMUl7Rvrq5t7W0tr7IzN1aS0tIREZEQUU8ND5DMDA/Q0Vf9M+7tre1r7C8
+wsLXYVJDPT49Pj9BTPbbzr+4tre4ubzH1/dXSElEPklMSERIRD5EPDZASz9FaubLvr+7tLS5
+vr/H31tKQ0JDPzxHZWNl0b+6uLq7ub3N4H5WRUdHQktMR05YTUZOVUU6PkRGVEtaycLMwrq4
+uL3Dx8vzS0hPQz1IT2Lp2M3AvL7Cx8XNa19STUs/PkhJTE1p72r1y9tL6tBSSlpNT21PSeDN
+dXPMydvd6PjT11h1z9323NPVzcDj3r3sV+pcUmBNSlBlUkly319c1O3Z5Odq4N5HUGBQTEtG
+TVpeX3Lb2Pzg3+HM3tzY1tXd0tHK3M7Y2dxoe2Z8eEx3TFZqTuPtZ9lc1ON63mfzUttIYeZH
+akzpT2RjV9x5Z1rv31/v3OfN6+rfz9hfz+TbZe1h4t1dZuZyenRk2m3a3OXg42xtZ2RdTXVd
+V+lTflrbUOhl/m74Yept1m56ylXfb/lZeHxr6Oxg01/Ueud33d/r/eHZ6+bv2WDiWF5q9FT6
+Xfls+mH5d2XaTdNP1VLUUM5V4Opb2vNqZfRjfl/2WuZZ6ubr1vTccs1o2WvYdnvSVNRVdE7V
+VeZU017NYuTs+OFLyErOWeBvbnFda/RU+kxuZHFVen5g5eji79zt5NHhW9Bi1k/ebWXoXnx1
+2/l31fzZae7vcu9rdmriXmhX3/xndvdV3E7aU9NM9k/WVvveaNxm8fnvXN1u3917Z9td5+5t
+ZNz7b9zq6mjaXOzm8k/ne0veZm5ZyEzW7nniX8xO0uhU3WhsY2h4WepTYOla33Ju8+fwZ+Nn
+7+LsX+7e/WvY41jNaVnQX1TXaV7z5GXy0Vzy215rd95Pd/Na4OtZ8d5aUdp4SdvjT+rNVFzI
+7F/b69Vr/tpPy1tKdsxJ79NSY8pnWNXJT17IaUzWcV5X2Vhf79ZsScL4UctmYcxObfXj52FY
+2NFV6mZny0p2yEjRbWLcbmX+1Vpa+dz7V9boUNveZlPf1lxoaNvv83R14+9gb+Lfa3rY7ONR
+2W1daN5bWNfdSeXTWerO81LY2E/n0VtB2NFLXeTSWerbcWbc81/p22pX3O5PyF9R089bTNry
+b+Jcd9f2W3DaZ2N6Z2t+eupZbOPrXlv/4uR54dF2a9ZobdRuWdfWUGfq51tc32dwbOHhcuht
+9WlufGZ4Zuh5Zu962e1SZNnbTPLPelbe31znfF597mFlWNn4X+zs52DW+HLn0vhl2N1h7mNi
+0d9RatVnWXxkdmN5elpWb9p2XeHf62rybWba3Fx8395q791g2//kZ+DiWGrqam3wWFnV9l39
+e25i0OZl9uHqfHdt8/Bub1Dv72zx9HjV32ps2uVfbv9nY1145F9o2+ZX9N/o3Ol8997scPR+
+7GRr3O5wZ23zYlZ95vRfYNvcXH3w3dxsY2/3VnLkbP3oeGLu1/5f4fV81e777np34/VebXV2
+dmpt9vXsZ1n34l5q3OdxXej4a1lY7N3vaeTteuXz3ev61vf27O5vWvx8b+lZ+eRtXefaeO1j
+VOzkZl5Wan3qXXjnZ+r052lk5dj27v/28W9y+9DoePDpd+XZXmzrfF5reVpkYvjiXV5q4flp
+9393bGHl7e9979nr4P128Pjv4WZs22pN9NFvWfrkXF7z82fm8Wvk6Hnh63breF/v3HNdcevi
+62tca9nhZV72cVlvb2lqcm/v+Xno6/Xw6OX6aXj27ft19t/teens9n7xcWJoZGD772Ve8Nvm
+93zr9Hpua3tjd2pedXRvePni321r7OXn7f5jeff35erd1tPc5eT07HdmWk5LTE9MS09XZWtq
+bvDb19jWz83T3djV2+zs29nc39fT2d3Y2er7aVJCPT8/PDg6RlNZXuDIv76/vr7E1OflaVJM
+TE9OUlv/2dTSzMjLzM3O0djc2d3d3+Tj7Fc9OTw9Ny4vP1ZMTNq6tr27srG80NjTbkM6PD49
+PURa3tHMvri3uby8v8jY/WhhXFhRRj02Nzs2LzE8Tk5O2Lm0t7iwrrbHz872Qzk5ODQ1Oz9L
+68jBu7Svsba3ucPZdGxfT0pNQzw4NDo5Ly43REhEZL23vLuvrLO/vr7RTkBDPjU0PUFCUtnJ
+wrq2tbe4usLO1ehYTk5SSj8/PTY6PjAvOUY/Pl/DvcC7sa6zubi5yflZVEU6OT9BPERy3d7Q
+v7m7vr68wc/d5fNeV2ZaSUVBPD07LzQ9PDc9WOXb0b2zsbazsLfGzs/sTkNBRkA7Qk9MUufO
+ysi/urzCwMHK2ePnclRLTks+OD4+MTU7Ojk9T+bc0b20tbi3tbjAxs3lX1RMQ0BFRklMU2/f
+1MvDv76+w8fGzdzwb1lPSkI/Ojw3NDo4Oj9JWHvayMC/vbq4vL/Cx8/adl5kVlRYWFtaYuze
+3NfPysvNzc/Y4PP6Y01MR0A/PDs7PD1CSU5f99nNyMK/v8DDxcrS2ODyfGdlaF1aaefg493W
+1NHQ1dDQ4ODe+2hlV01DQkA7PT49QUFNU2P23NHKxMLAw8nLytDR2t7ofn118fDq5+bn7/nz
+5eDe5uPj73Z1bl5cVU1MSUFEQENDSldXXGni29DPycfJyszV2trc4O3e4+Xj4eHs5uDi5+jq
+7+7k3N3j5Ov4Zl5TTExEP0I/QkJITVNf/+Pg0c3JysvKys7O1Nzc49nf29nc8W5vdv957njy
+5uXg4tje5HVmXlJOSkRDQkFCQklLVF1+3tfQycjKyszO0tLX3NzX1+Pg3ujs6+71+ezl63/s
+3+bh735iXl5TTExFPkNCQ0JGUVhj9trYzsrGyMfIzNDY2d7j4d/o5t7m7ezvenru4uz13+fl
+6ufufm5mW0tMRz9BP0E/RU1NV2ri2MzJxcrIxsrNzM3W2dXT5Ort+nf+9HV08unv7uro5OTj
+7nt2WktHST8/QD9AP0lLVGXd08zJx8fJxcjL083P2Nna4PXi6/L58/P+8PD6b+rf4OPh5/z9
+WllRQUM/PD09REJIVGBu29LMzMPAw8PFyNPV293n4+H2+/nx+/Ls6ujf2d3d1dnY3u9qV1RE
+QT47OTo9PkNLWmrw1szKxsLGxsXEys3L0dfZ1+Py7fD6fO3p5+708ebl2dnk/GNoUUM+Pjk5
+Oz4/Q1JgfObVzcfFwMbFwsfLzs3V2Nfa5u/n7H578vbv8d/f4trc3+5oZVtHQj87OTk8PEBM
+W2/m0M3MyMPFyMTGy87N1ejn3/Lv6Or19+Dg39zVz9DO0eRvWl9JPT47NzY4PDxCWXDp1cnH
+xsHAxsXEyMzP1ODt3+D8/uro+e7d2drV0tfQ2Nv8U1ZMPj07Nzc3Pj1CT2D108nGxsC+wsDC
+x87V1Nvr7/pkZGn75ebUzs3IyMjKz99sVEk9ODgxMTI2Oz1LXujSxL+9vLu9vb7CyNDW7n5n
+X1xXYWd77NjOy8jEw8fT3WtTRDo5MzAyMzc4P1Ft2sm/vLq3uLy7vsPL1uZoXV1SS1FbYGnj
+zsvHwb7By9LdX0Q7OzUwLzE0NjxKWefKv727tra6ubq/ytHZb1dVTkdLVmNp6MvOx729v8jT
+2ls9OzkxLi8zMjQ+TVrhx7y6uLG0t7e7v8rW51lOTUdCR1BcX9nIyMC6vMLO0uFCOjoyLS0w
+NDM8U3Tmyr26u7azt7i3u8fX4GJJSUpCRU9aYefNxb69vMXO1GtEOjgvLi0vNDU+UvbVw7m2
+tbKztbm8wM3be11NSUlIS1Vh6dLMwb29w9bfbT81NjEtLS81NzxU4djGubW1srGztru+zOBv
+U0lCREVKWGnax8K9ubq+z+zgQC8zMCspLDM3OEzOxcC0r7Cxs7K3wsbOeU9LST8+Q1RgaMy9
+vrq1t73ZdPo8LC8uKCgqMzo7U8S9vLGrrK6wrrLG1t5aPjs/PTo+XOt8xbe4tLO1ut1RVjgn
+KismJSkzPT9furGyramorLSytdVPUEk4Mjg9Oz59zci4sK2tr7K65kdEMiUlJyQkJzA+Tt61
+qquqp6Wqtrm89T05OS8rLjc9RuK/ta6sqaisr7fYPj0vHyAkIiEnM0VrzbKmpaemoqa2v8F0
+NSwvLSkqMkBM6buuqammpaiuus1BLy0iHB4hISMtQ93AsaWfoKOko6vFdFQ5KSUoKSgrOFjZ
+v6+noqWnpaawxuM+KyYiHBwfIyQuSc+3rKSenp+kpqrCSTo0KR8hJigqMl+/tqykn6Ckpaar
+wmE+KyIhHBodIiUqOsyvqqWem52hpqm0TzEuKiIeISotL0TBr6ynoJ6fpaeqtPg7LiUfHhsZ
+HSMoLEa6q6egm5qdoqevw0UsJyciHiArMjpZuaqmo5+dn6arrrlkNSslHx0cGRwjKy5Guaik
+oZyanaOqsshFLSckIiAhJjBF8L+so6Cgn56jq7W80jwqJSEeHBocISkuPMmtpqOem5ygpqu8
+XzwtJiIiIyMoN2fLvKuhn6Khn6SvvMDiOSkkIB4dGxwiKzE+y6ykop+cnJ+psbvpPCslIyIk
+JCg1Ycu8q6CfoaKgo627wc5FKyYkHhwcHB4jLDpbu6qin56dnaGqssNQNyoiHyEiIik+1r+z
+pp6eoKCfpLDAyXQ3KiYhHhwcHB4kLT16uqmgnp2cnaCqtcVPMCkiHx4fJyw4erWqpZ+cnJ+j
+p62+a0I3KyMfHB0dHR4jMEJfwaugnp+dnJ6ns8H8NSckIiAfISk6YsqwpJ6enp6fprDA2Uw0
+KyYlIBweIiMiJz3mzryqn56fn56hrL7YTzMmISIiICQuQvG/rKKfn56eo6y4xn49LywoJSAd
+HyYmIypNyMS7qJ6epKSfo7LXYk4xJCEjJCImNE/Suqujn5+foKatu9VSOC4sKCYjISMmKSow
+RtS/t6ukoqSlpqm1y15GNCklJScnKS5Fx7ivp5+foqanrL19SkE2LisrLComKS4vLzZR1c7A
+sKmoqquqrLjNbEw5LioqKystOFHNurCqpaOmq66zxV5IQjwzLzM4NiwrOD8yLD3Q1lLesKmu
+ubGqrsTn2uo9LSsvMy0tPNG/xLaopKiuray22lRiWz00PEM3LisvNy8qL0JmVVq/rq2xsqup
+ssfa2lk2LS0yMS0wRdzEv7Wqpqqur6+521lhZkc5PUI+OzIuOT8zLC9Pf0RGy7Gyvbisq7XO
+zsxfOS80OTYyPeDEx7+xq6yxs7G3y3ZtbEo+Pz9APDAyOTsvLDtJPj5ZzLy8vLOsrre9vsVs
+RD5APDU1Q2ds7MC0sLK1s7G4w87T5VRDQEE9ODM1NTQvLTM5Pj9D0Lm6vLStq6+8vbvPTEA/
+PTk2O1J278u6s7O0srK3wdPZ+E1AP0A9Nzc4LzdHLi5HTUVRXtO3vs64rra8v8TBz0xIXEs+
+QExs3d7Mube7u7u7w9l+cFNAP0RHQkZHUFlEPkRBPEE7OkdQUGTZyLy7u7WxusLIzuVUSklO
+TEhS483Nx8C8v8nIztz+UEtJR0NHU1ROavpm3tt5VmdMRD83Oj4+PEhh7NLJvre2uLi4u8fR
+4WpUSElKTVFm6dnIycTCy9D29lNWWUhGV01HU1dOZGpV8+ru997k+fdhaFpYSVFWTU9UXG7j
+8NfQ0MrMysvGzsrQ3M3g9n98bF1wZ3T1Y3Jq+lxpX29sZW1veWprYnBebVxp8Fv1ZG5fZFpa
+eVxrZHZyf+nm7Pdy9d/86N3W09/T4tPc6djv9flr/ul37ubo/erue9p27+9naVhmUl1TWXVV
+Z1tqWXV1dmBvXvtmYGf+eO7q6evj++Tf6O/s5Ofp7PHn3eLV1NrX1trc4ux7ZWJcU1JWXFxd
+Z2997H1x6XtkXmdhXGJfXmZgXWt+ffby5+Tj6/Ln+fB+7tzg4OLZ293c4drg4+fg6fXvc2ls
+a15fW1daWWNcaWp3am14anZsb29gYGVub3Tv5enf3eHa29zh3t3k3+x9am5tbWprbnDs5+/t
+9f7vbGtubW5ncn5+fPPq7ffsev32YmJiZF9kbnls9ebx5d/i7t/e5/JvZWxrW2zvcGVre3Xz
+9/365+fs5N3m7erd4+Hq53j2aGZjW1paWmN5eG54dXns4t3g6+vzb2BoX2FkeW5zZHHv9fT8
+8erp4+3x6uru9e5n/G3/9+Z15e/vbH384ujtbX1u6mn2/u5UYF1pYF9ifnhhanD1bGJsdd9/
+5PXl8er33drg7ejvdvXvbfP8+XxseHL6YvR89uH38vpqYlteXVhmWflr9e3Z2uvU5+hz63H1
+aH/seWdeeGFwYGVfX2hp8ffr8eXf3ejq7uze7X1w+m1rc/b++fF1bf13XWJfY3Xv+/To8+3q
+6+327XRxZ2FnZG1rd3z48fjn3d7f5/J6a2pqcGlvbWZoYGRdYm5sc+7m6uzt7Ori3dzf3+rt
+6uxxcWtfX2RnaG1zcPz07Ovn/XZ1fXh1fP3++3VwamlqbGZjaPd9bGdx+/vn6+Tk6eXyeHt4
+enp5ffXy+uz3+/Du8Ozv9f3+cWxsampnam9oZGh1cXl+fnv//vP7/fHm3t7g5+78cHL3d3dz
+dGtra3j7dHl8+fz/fnr9dG90+vp8d3f+eP339PX/9355d/nq6+ri6OXg4ub5d29maGtnZGZl
+YWdpd3r/8PD3+Ovq6+78e3n39H9zbXZ8+vr0+3v5+ff8+e7o8Pfu6ej+b3NraXz1enH9+Hx1
+/u/z+X59enRveHx7+u709/52eHV0evv2d3Bwb2939flxcv317uXk6eXq9vr5f/r4e3j97+vr
+WXnUa1555ml5e3rxdHJvZmRmb/H1+nVzYlRET727ztnQ29bbWE9MSFVxSz0+X9DL0M3Dw8bP
+zNH4U01PUE1JRUXVzXzj2eXczd/x8Vtbfuzpa1pg9t/p+Vpl7fb9b3Ts3+v883RnePd89+5t
+b+/z4tHLztXU1tbpU0Y/PDw7PEBHVOHJvru7vcDByNxTQ0FFR0pMVejKwL6+v76+wMnXdWn/
+cWBLPTcvKi00MTRAWcizrayrq660wGE8NDAuMTM5SHnHt7Ozsba8w9pURkA/RlBy08S8t7W0
+vv5CNCcfICYmKz37taaioqGkq7bZOSomJCUpMUHctKqnpqiss8NYOjIvLzQ/Xcq6tK6rrrna
+SjYrIx0fJyw2X7uqoZ+ipau57zkqJSQmKTFGzrGopqiprLXIUjs0MTE0PV7Ju7Wyr7G4v9pL
+OC0qJh8jLzI94b6upqeqqay73UczLi0rLTdG7b20sa6tsLfGa05FPDw+RWHYxr68u7u+xcbX
+U0tCLikkIywwLjnZuK6srKimrLnQXD8zLCstM0Zk37yvrK2wtLfC+0xGR0M/R1zhzMzGvb7N
+Z1FfWT0uKi01MSwvRd/Kxryvq622u7/I4kw+Oz9DRFTaxb64s7S5v8/E+UVcOTlHRzhGcm9a
+VcLwP7/POd7OS1PpVWzZWXnhbXJfefhseXl6bXV9/+7mzM7Uzs7L0NnX4GtobFhcaVVUc3tu
+5fbs2dze83D2f1lTUlhqX1pldOzp7v3z4OtubGFiaFxYXF52++7X1dbZ39/nefjudXP9am1w
+fXRy3trW09jd3t73aGNpY1lcWl54/m927erj9Wl1Y1lbT1NpWmN5dufY1tXd4tvk8vZraGNa
+aG525enf0tXV1tfX6n5rZm5hWVtdafxsZ2xveGdcZmdfbHD76+vn6+Xf4+5sZHBqX2pmcu3u
+5t/b2+To72xoZl1pdmtx9/Pk3O3q5+bscWlkXVdXWVxz493b0s3R1+V3XltVS0lKUVZc/Ofd
+18/R2NbefGhqZlxdYGZ09ujn39zk3trc6vl0eWxfX1tbX2dqfe/g2Nrf5/BqWlNSVFlaXXP3
+9+Hc3d3a2d7u/nJiZmZiZ2Vu9fXw6efo7Xxxbm5xbXd2d/js49/a3N7ub3NsaGhoZ2BdYGVl
+bfn17/B7eX51enl8fG1tdHJw7uHg3uDk9Xz+eXV//vvz9fx4eG9qb3FzcHZ8dHNrbnFz/3Z+
++fvr397i4+1xa2RqZmNqZGpudfXz+Hz473pqaHNvb33x8O7t6ebr5env/Pv+e3RnZ2Joe3X8
+9Pjs5ez8dXT9bmpvamVmZmNmbfTq6+rs7O/5/v7zfv7+fXZ29XdtaGx0fu/p6Pj58Xl7effv
+7uXo7/PzeXJrZ2dmamVbYGplYWhvcHvy6ert59/e4uLveXrw6vvw7O7n4/F0/nhoaG9rY2xu
+bGdnb21qaWNXUM/SP7/d+/nScvrdWehnaVX3WuxT3Xrf71x14cnudNzY1mbe0V59Vn1420db
+4U5SVN/+a1Fl2HJt9tjk8G3Z1PpaWGXOeXxN/c1v2du/9vr363NOS1NzTURc9uXl9snE7O14
+0/VTZNfyWVp18etKetx/bVfY9+JP1tTve/Hf5179e/1+ZF1tZfzxYN/p/d1j217cT2vdTmdU
+0VB3aOHnfezZzl3Z9Nr16tdu2VPTVtVtYXhp5EvWTd9SX+xq3lzh7fH6aefk5ln+cn35bOln
+zlrp72/kXuHpfGJ45OzwZeLy32TefN/6WNdf3VTUeP7dXMx8a1hrbVReW3NTV2H1eWRo2tZW
+2d7P39/l3slZ1tDX2Nnb28pd3815+el95dJEN21IMjg5OkA7OPnNYNa8uLa6uq+yxMq/yX5P
+SmpfRk3b0uPRyr+/03tHSkg1LCwyLi8vOm/v8sSxrrCxsrK1yuTVcElAR1BeXFvPzM3MzsjL
+2vXc42xsTUxUT0M/Q0JBPT1DT1Zc+dTIxsXIxMTKzNbdfe7sfPdydu/Z3dvf29vZ1dnX3t/m
+6uByYFdUSUZCPz9AQkJJVWDw287MyMfIy9DT2+VtZlpYXGV5eN/Z1dDOyczKy8zLzc7Y3/hi
+V09IRD47Pj9AQ0ZJUF1r29XS1Nbc6uXv8Ofd1tfW1szLzMjHxcjLztrf4mtnXVpVUlZOTUtL
+T0lISEpNTlBQXWz9f+nU0NPb1NHW1dXU2NjW0tLa4eng4vF79O/+b2dsdW9memplZVhdXltT
+UVFTWllZX2hqfvXt5fL8+m1v9eXj3trY2dnW09LY2+Hn7vtwamdq8PPp4t/d4OHv9nBcT0lF
+RENFRkdMVl1y6N3V19fW1NXV2dzc4t/e29vf3d3j7N7b3N3e3uTr4t3n9WRWVFJJPz9BQD9E
+SE1cX27ezczNx8jM0Nrb3Ot/4t3u9+fj6Ozg1dbPysrLxsrX1O5OR0c9MTI0MTA1PUZZ6cq6
+tri3tLi+v8PgW09JRkVFQ01de9DEvLm2s7W4ur/bRzw4LCUmKScnLjxKasq3rqysqKiutba/
+fEpAOTUxMTg+Ql/GurWuq6yrr7rNTjsxJyAjIx8jLjY/1betqaajoaWrrrTTTUQ3LSssLC83
+ROrCtq2pqKapsLfOOzcuHh4jHhwoMTJburGooqGenqWqrL9TSjcqKCkoKjA7V8y5ramopKKr
+trV1LDArGhshHRsrNTrGsaqgnp6bnqiqrupBOiwmJSUmKTBA8MKupqWkn6Kts7pCLC0kGBse
+GhwqND+/raaenZyanqaos10+MyYhIiEkKTJJ0Liqo6OfnqKqsL5AKikhFxgdGhooOD6/qKKd
+mpqanaWqu0o3LCIfHx8jKzZYwa6kn56dnaCns85GKB4gGRIZHBkfPVHKpZ6cmJiZm6SvvEss
+JyIdHSAkKj3Vt6mfnZydnZ+puOk4JRwbFxEXGxsgP9S7oZqZlpaanKS66zomHx4cHCAoMmK5
+q6Kcm5ucnaSuxEQrHxkYFhEXHR4p4barnJeXlpedoq1eMyoeGhscHSUzULqon5yZmZudoam/
+QTMiGRgXExMcHyRCtamfl5WWl5uirN0uJx8YGR0cIjprvqadnJmYm56iq7w9LCQZFRcVEhoj
+J0CxpZ6XlZWXm6Sv8SsgHhkWHCIkN8KupZyZmZibnqay0jcmHhgVFRQVHSUt3amim5SUlpid
+rMBCIR0bFRceISl+s6mdmJiXmJ2jq79CKx8aFhUUFBsiKkqtoZyVk5SXm6e+TCYaGxgTGSQp
+OrOmn5mWl5iboqq5PykiGRQWFRIaJSpJqp+clJGUl5upxkYlGhoXFRojK0mxp56YmJeYnKSs
+vTsoHxgVFRMUHSQr46eempOSlZedrdQ5HxgaFxQcKzBkqqGcmJiYmp+or9QvJB0XFRYVFh8q
+OL6inJeTk5ebpcRALBoXGxgZJDdLtqSfmpmbnJ2otL5HKh8ZGRkUFR8pLf+onpmVlZaYor5P
+Mh0XGhoaIjNctqegm5mdnp+puc5PLiAaGhwYFh0sNlKvn5qXl5iZn7s/LyEXGBwcJjh4tKOf
+nZqdoKStvt5LLyIbGx0bGB0sPOizopqXmZucorc+Kh8ZGhsdKkfHraCenJufpam01Ew6LiMb
+Gh0eHB8sR7yro52YmJufq8E9KR0YGx8jMFu+qZ6enp2hqa7CWUg9LiUeHB0fICMrPMiroZ6c
+m5ufq8w3KR8bHSAmOsy2qaCgn56lrbfOU0s/MiojHh4fICUrNFK3p6CdnJ6hp7ddMiEcHyQq
+OmXGraSioaGnrrfOW1NOOjEsJCEhICMqMDdfu62knp6fo6u9azopIyMkKjpfxK6qqaenq7C8
+4E9GPjw+PTgzLywtLzE2P05+ybuzrqurrrfD6VBHQ0RMWG73eHb56H9iVUxUX2fnz8O6tra5
+v9NZPzQvLSssLzdDac+/ura0s7a7xM7eZFZPRz8/Pz9FWevNv7u3s7Kzs7fE7kk7MSwqKisu
+Nj5R4se8ubi2uLzE1HFXTUxPUE9PUVzu1MnAvb29vLy/yt5kT0Q+PDo7Pz9DSlVv+nvu7vn1
++311cWRdYGnv3tfV0cvIw8DAwcLI1eRpVUtFR0ZLXvHa1tve625eUUhCQUFDR0dKUVxv3szI
+xcC/wMDAwMPK4WVQRj8+QUVNYeXOxcK/wMjO2O9ZS0ZCPz9BRENDSVBectrLxL++vb3DzeFd
+SkM/P0JLXubPyMK/v8HGzNv7Y1tWVldVUExJSEdGR01RWWnj1dHOzs7afV9UUlBb/uXYz8nH
+xsbJzdbn8WxdZvfh3tze4e1iU0lDPz9CREdMT1pqcW157Ofq4tTNycfGxcjP2Nvld3b47+3q
+3dzW0Nja2Nz4ZE0/PDo6OTo8PkZNXN/Iv728vr/ByNHY33dgXWFsZnHr7fzNwsjMyMjO2d7c
+bEE2MS8vMDI2OT9N2761r66wtrvBydh+XE1HR0xWa9zX3ufn0sPH82rZ0d/0bllKQD09PTw7
+Oz5FWeLNxsG/v8PIycrO4nd87+Xf7G9eYGVy6uTe3NvSzsnEw8rpVkpBOjc3NjU0OD9S8tbK
+wL7FysC4ucrb0MnM2vxeTUlR/Nvi7drKv7m5vsb+PDM1MiwpKCotOWLBubexraupq6++8E5E
+QD05NTQ6SffJvbm1sq6tr7O8yGw+LygjICEhIygwRsuxqaOhoKKlrLfaQzErKSgqLTVF3L2v
+qqakpaessrvG90MyJx4dHB0fICUvT7qnn52cnZ6iq79LLiUgICIlKzhpuqymoZ+goqatutJf
+SzsvKSEdHh4fIiUtSb2roJ6dnZ2gqbxGLygiHx8hKTdaw7CqpKCfoaatt8XqUEM7My8qIyIi
+ISMmKznUtqijoZ6eoKey9zwwKSQjIigvPFzCsqmjoqOlqKyzw+9RRj41LykiISEhIiUrO9S5
+rKahnp2fqLLRSzkrJSQjJy41Rsu2qqOjo6Kkqa/C8FhFOjItKCMhISAjJSs8a8Svp6GenqGl
+rbzZRTErJyUkKC42U8azqaSjoKKnrLjM9k0/ODEtKSckJScoKCwzS8W0rKekoqGlqrLC7EIx
+KiYlKSotPGHEr6qmoqKkp667x/RANzAuLisoJiYpLC0xOlPOuK+qpqOkp6y1v9xINSwoKSwt
+MDxfwbGsp6Snq665yfs/OjUvLzI0ODk5PT9ARUx6zsS+u7u5u8DOdlVMR0RDRUpd69rLwb25
+ub6/w8zQ6lpPTElIR0ZMT1FUT1vm6Xbq3drX3PZiVEtGQz8/RUxVXnrcz8rHxsXCv76+v8HI
+zdnoelxPTEtNUFdmeejc3t3h82paTEVAQENAP0JHT2H77OLX0s7OzMjGw8PExMfM0t/1+mZY
+VE1PWF1p9NvLx8fHztjlXk9IPz49Ozw+QEdMWGzv3NbPzc3KysrJy87T3+/o6f55dHb36uLg
+2dTSzcvN1Nzqb2FXTklGRUNAP0FFSU5VXGb+6t3W09LS193b2tXU2tzY1dLQ0tPQ0tTX4fPw
+9v9xY2ZtbmdnYV5iW09KRkVGR0ZJTlheYW177t7Y1tra08/Q0c/O0dHPz8/Q09TY3+zvdWJf
+ZG/75+15a2FcWFBOT1NPS0pLSU1UW2Ft//Ls59/a3N3d3NjZ29bV1dXV19vf4dze6eDe3+bj
+3uP6aGJZVldVV1paYGhmYF5cWllXV1FOUlZdYm/26+Pe2NTW1dLS1dLOz9LT09TY2+f6b19a
+VVRVV1xha2959ujo6ubsb2ZgWVNSU09PVlxhbPPp39nY19bW2t7q+21nZ2FfbPDn4ebr3t7c
+19jb293k+XdsamZgX11dW1teYmRlbndydnxza2tyeW5saW54bGlqeXx69/Dn39nb2dnc3uPu
+fP/o5+np7vp5cm1sWkJWuFk9wslN9c5q22ZIWltGX09F4F3j3HrbztvO3dfbW9pNbWl65Fje
+V+PFU9PL7MfLy/W/vWrOzk0+RDswMzAuNz49U9HMurWysa6xu7vI9nhLOj09ODxOTXO/072y
+s7W1usTF0EczMi4tKCMoLzc7QOW5r66tqaeorrzG0m1ANTEzNTQ2QG3Pwriyr6ytsrO4w9NU
+Ny0pJyMhISUqMz5mwK6mpaSio6aruM1lPTIuKSctLzVF78OzrKqpp6itsbnQWD8uJiMhHx4f
+ISo4TdKwpqCenZ+hpa2+Xj0vJyUkICUuOE3FtaujoqKgo6iuutpCOC0iHR0cHB4fJTNfw66l
+n5yanJ+kq7lmNCkjISEfISs5X7+vqJ+dnZ+ip6253kAyKiEcGxsbHB8kL07BrqSem5qbnaOr
+uPs3KSMgHx4hKDRXw66ln52dn6GnrrvaRDAoIBwbGxscHyYuT7+upJ2ampqdo6u3WTAoIh8e
+HiAoNlzArqSenJyeoKevvP0+LygeGhoaGhweJTF5t6qhnJmYmZ2lrrtMLiUfHh4eIis+1rWp
+op6dnZ6kq7XFVjYpHxsaGhkaHiMtTb2soZyamJmcoqy+TzAmHx0dHyMrOmy7q6KenZ2eoaiw
+vvk9LSMcGhoaGx0gKT3HraKdmpmZm6CsvU0vJyAeHR4iKjvpuaqinpycnaKqsL53PSwgGxkZ
+GRsdIS1NvKifm5mZmp2jsN08KyMfHx8gJzFOwa+oop+enqCmrrrPUjYpHxsZGhsdHycz+a+j
+nZubm5yfqsBGLychHyAiJS0+2bSppKGfn6GkqrbKaD4vJR0aGhwdISYuSrqon5ybnJ6gqLhb
+MygjICEjKCw3bruspaGhoaGkqrLC7kY0KB4bGxsdICYtQcCqn5ybnJ6jqrh9MycjICIlKS88
+6birpaGho6SorLTFc0QzKR8cHB0eIigvRMGqoJ2cnZ+lrr9YMiciICInLTdNy7WqpKOkpKer
+r7nOYUMyKCAdHR8iJyw1UbuqoZ6en6OptNVCLiQhISQqM0flvq+ppKKkpqqvt8XcVz0vJh4e
+HyInLjVBzq+ln56gpqy1zkozJyEgIik3Udi6rqmlo6WqrrbE1XZIPDUrIyIlJys2PkrOtauj
+oKSqsL3yRzgqJCMmKjl1xbatqqelqK2zvdJtXko7NzEqJyssLjlET9K5sKunqK+1wWY/OS8p
+KCouO23GuK+srKqrsLnGdE1NTU1HPjo1MjU6Ozw/Rl/KurGtrrS7w9pYQTUuLS81QGDPwLq0
+sbGwtL3PbEtEQ0tZ6+hhXFNGQEQ+OTo8P1vLvbi1ub/Cx9xhSjw2Oj9KadzVzMS/vbq9xM3d
+c2RgVUpJTV/Pv8LXa0k7Oz48Ojo6O0rgxry4ur6+v8G/yHJKQT0+SE9SV2bpz8K+vb/H1N/m
+cVxUTUhHTVr2z8bQblNDOz0+PDw/RFTQvbWvrrO4vcrlW0E2MzQ2PEpe38e+u7i4u77F0v9X
+SD07Ozo8P0FGVu7Ov7m3t7e8zvRVQDo2NDM4P0/fxLy5tbW4u8DO/k9FPz08PT9CSFj92crD
+wsLDwb69wdJhRDg1NTU3OjxDYc+9s66vs7e/z+tXRj47Ojk9Qkpb9+DPyMjIy9Pe4uPn8WhZ
+VlhbZ+7j5ujo3dbNys3Zd1xVU1RTU05NUFljZmZiW1hWV1tn9uTe4OXaz87O0Nrj5uXh3t/e
+2MzGxsTFy9R2STo0MTAxNjtFXOLLvbe1tLe9yNpqT0U/PTw+QUhWedjNx8PAv8HEyM7a6mdW
+T01LTFNcZvTc0czIytf1VEI7ODY3O0FKXd/Mv7m1tba5wM7oXE5KR0VFREVKVHXazMbCwsXL
+z9PZ4uzzb2NXRz04Njc6PkROZdzLwLq2tLS4vcfYaVBGPzw5ODk6P072zL64tbO1uLvAydT5
+VElEQD9CQT89Ojk5PEFMX97Lwr25trW3ur/M311LREA+PT5CR1Fz1cjCvr2+wcbL0d3qfmdf
+XVxeWk9MRz46OTk7P0dTfNXGvLe1tLS3vMbbXEg+OTc3ODk/SVbhx7u2tLO3ur7EzNt6XlVO
+TExLSUhBPDc1Njg+Rk5r08O6sq+ur7W/02RLPzk0MjIzOUVsybu1sbCwsra6xNdjSUE+PkFN
+Xmp+bVNDPj06OTo7PEhoz7uzsrKzt7y/yOZLPDYwMDY9R2DZx7y1sK+wtr3I4GdVTEZBQ0xl
+3dryTzkyNDMzNjY3QXjCsKqrra2vtLnGTDEtKiouNDlEc8azqqeprLS7v8rrSzw3OUFMU195
+aFVFOjc0Ly8xND/6wratq6qpqq631UE3LyorLC01SN27rqurqaqtsL3iTT45OTo5P01e+dno
+Pi82NS8zNDA86sK0q6ysqKuzvOA+NzUsLC8xP9jBt66sq6irtL7XT0Q+NjQ1NDVH7+nvVDtG
+ZEA9Pjg7Vm94xb67r7K6u77L1/5EPUA8QlNg0r+9vLq5u7zE32hOREVBPT9ISlvi7mJv1d9M
+Pj09PTw2O1BZfMnKxbe2uba7w8DKZWJfWHFcT1zf0NDP3dnN1vNrWFNZUklMT1BgZ2Ju7uvu
+2k88Qz8+QTw+T+7u0MfDt7a8vsLGzNtfU1xaZ2Zabt3Pzs7P18/O5GlcWFVPSERMSUpPVltX
+WV5senno3Njk9e7o3t7ucF1iY2BfXnjyfPX45tLPzszO19PM1drV3nxpb2BcV1dYW1ZVWVxf
+bG9udvDi8f729HdkYWJnaFxUV2R88Ozw6tfR19rc6d/l/334fX5yd3d45+Lp9ejn83JrZl5e
+YW53fvP5d3F86uff5vX8Y2hrYl5jZ3zn7eTm6v1xa2dfXVxhX2Pr6O3f3dza3+zo7vJvZmNp
+cXr09O7n2NHS1OP7bWdqV1hXVVtjZG7w/fD0+/twa2VuaWpfa/z45vr47+jv7eTmeXVvbnz9
++ufn/uTd1tvm+e35cGxlXFhdZ3xt/+bi7nJ9e29wbWx6/XV95env7Op9aF1YVFNZW2l5dn73
+6N/S0dXT1t3f5fN2bmFmZmdr+fDs5u71/WhhZGRiX19eYm98cv767+fs73t+cGhwcnBue+Xl
+4eHd3N7h6u1ybWxdXF1nbWxtc35z+O3p5vF+dGpkaG778Onn8vLu7e59dX58+Xtob/5v+vp5
+eHF4eXp6dm1qa3J4evr58v589PHr5Ono5enu7XZpbG58fHVtbGtrdHpyfPb2+nNmbffx7Obt
+fn5zbm1pbnNvcXv87uvw7uzt6ObyeXj8cG9vcm9t9Ons5+j57v1tbvXvfmZeX2ZtbW1panvn
+4/Lu7/z98vj/+/1++/T4+vb09PXv9fj8eHJsenx0enZwc29rbvbz9/Hu8vHo7vLr7vTz8XR+
+/GlpcGtpefltZGludPHr5+Xt8fDx7OXi6fn/cW39fW5wcXVxcff6b3zs6uzt/W1nZGNhXmFq
+cnP26Obx+O/p4t7e3+Tq6/l0cXB/9HJrbG52/nJoam96+ndtcvbt/nZ+8f76fnJx/Ozt9nx4
+/fd9d3V8//58dXnz9/v89/Hp6fn/+n559fB/b294e3d1dXd2e3l9/Xb793x4e/l9df7z/Hj5
+7/T2fnx4c3h6c29+fvzw7e79fnRz/fN4an779u7u/Xh2/ezt7u3v+3Jtbm9yf/R3bm93+P51
+bmxra3P9emtv+vPy6uZ3YdXZbmT36ujlZl302HNn4ttmX+VxX2ns725fV2FkUExyxs5LQ7mz
+RDq9r1o46LZ4MWO4dDVSvOU+Vr/KRGG/1EhfxflKXtZrRVzXYUhmz+lf/tjYcV7b0/1Y49Zq
+W2rT91ju2OFt7O7k/Hv5eXT7b2v+W1jrfFhs4+xmZuLhbfrtem/58+7t9+Xk8mdy6vF5c+Hl
+83ju5mtx8HtwaVlqdVVi9FxhZFdtblRk6mtb+9LgfNrM2t/KzNjY1tLa3dbX4erZ3NrT4UJd
+3C0xSC4qODkvQWXk4cawtLOqrbOvsr/M1PVRSlBHQ2Bsa9PC00XOzi00TSokMy8qN01fbL6v
+ubGnrbStsb7J1GpDSFA/ReD7ZsK8wLzBw+VJSC8qKyYkKi0vQVfKu7WrqqqprLC1vtdaTEk+
+PEZKR3XPyrqysry6vV5CQC8lJyUjJiwyOVfGvreqqaypqa2zs77qblg/PkVEQFLf2ci5usO2
+zk9UOi0pKyUkKS4wO/vPvbCrqqmmqq6utsza9Uk9Pj07Pkxj8ca8vr62xmLlRi4rLCUkKi4v
+OvnUwq+srKmnqa6ut8zaXkY9Pzo4QEVM5cXDuLi+utRkWzotLSsmKS0vNknfy7qvrayrqq60
+tb/bZlY+PUA9P0hPYNHGvre7vrzZXlA4Ly4rKSsuMTtR4cq5sa+trq+yubzI2O9aS0hDQkdL
+V3rYzMS9vcnBxFBKRjQuLiwsLzc8Ru7Nx7q0tbOvtru8ws3f6G5VVlFITllf59bNyL/C08XT
+TUw/NDIwLi4xNz1D8tjWwLy6uLa2uLu8wcrL331rVVJUT1ly5s3JwL7VxMlKUEk0Li8sKi0v
+NDtY1s69tLWzr7Cztri/zM7dXFlgSUhYW1j2y87EuczVu2o9TjkrLS4oKzA2PUvOx8KysLSu
+rbK1tr7M1t9aR01EPUhRU2XQy8e5u9zGvkY7SDQpLS8pLDg7Q+rFwravrq+vrrO7vMjleFtF
+QEI/Qk1gdtrGwru3wtnF2zs2PC4mKy8sLj1OZs28ta+urq2utLu/z35bR0BBPz5GVWnx28rB
+wcfAx9xoV0E5NzQyNDc7Q1H+zcG8ubW3ur3E1exuTkhJRkdNVV5359jR0s7Mzc3Oztbi6epu
+WVZUUFJSTEtNUVtcXF9iaWdkXllfbXzx6Obc09LQzs/P1dna3uPf2uL57Ofh6vDteXBrX2Be
+XmljVFBTTktLS0xSWF5y+/Dd19nY1NjZ2dva3OHq4tzc29ja19Tb3OH3eX5oXVdVU05OTUxN
+TE9YXGb35t/c2t3f3Nfb5ufv/u3c3dzb3+Dl6u91cnRoZWlmZml0bmJfXWJfVVlgXWT/7uvl
+39ja2tjg4t/m+H52a2d1cmptcXRtbXH06/916Or+/nduYmp4a2VmaWx88ung4ODe4N3d6Ptv
+ZGRoaWhka3BpaGlscW726O7p5Ofp7vp8c2tnZWVjaXh7fuvj7O/u7/X59H747n1zf/X28PJ7
+ePjp5/R8/PP3bGJiY2JfZmtsZW5vdfDs4uLq9f1ybG50amrz6urq39vc2dnh5+z9bGNjX2Je
+Wl5iYU9PzthLbsfcXXrT63Ne8v5kXV9zamZP58bV6tXN29ne5+p3WVtdY2ZlX2JjXWBhaHR2
+dXF8fnL55O5sa+fof+/g7vnr4dzc5ebm4Oj9bWhqYmVWSFXd1W9ba+Tb3t3rXVFWX3ztd/zi
+39/Qzs7P1NTec1JJQT5BREhPWW3f087Jx8vN0uZ2Z3r6aGVsam9/593VzMvGw8TEyMvaWD00
+LywtMTdCUf3Lvbaxr7K4wd1VRD8+Pj9ASU90zb+5uLu+xc3SzcrJy8/WdU46LisnJysuOlPU
+u6+rqaiqr7nTSDcvLi8yNjxGWtC7sKurrrK6wsvZ8HxqXlRDNCsmJCQoLzpXy7etp6Oho6my
+y0gyKykqLC4zPErovrCrqKqtsbvH1vBlY2VeU0AyKiUiIykvPXm/sKmin5+iqbTeOiwmJSYp
+LTA6S9u4raejpKitt8bZYVBPUVdOPjYrIx8jJCo9UMeyrKWfoKGkr8VHLicmJigsLjQ/acCt
+paCfo6qxv9xbRT05OkFNVko6MSwnJysuO2DTua+uqaaoqq6+30czLispKSwvPH6+rqejoaSp
+r7/1STw4NjY5P01q08xdQTcrKSwuN0lMac7AtKypqKmwwHFANC8xMjc+SfW+saqmp6qvvM5q
+Sj43Mi4uMTlK8c/CvsTlW0c2ODg0OTw7R+rQvbe6uLe8vL3G0/hSSktX8MrBwcHGycnL0+Jc
+R0I9PUBGV19y3tzQysTD2kk5ListLzM8P0Nn0b6vq6qrr7q+xdfb/0tDPz1GX+DOx8bLzMfF
+xMnabFFHREI+Pj5ATVd02dx6UUc/OTxIRkpOS1ThxbevsLW6vcDEzO1RRUFJWHDk187Iv728
+v8vkXk9JSEE7OTo9RmLZydxWRDs6PkNGRj5AVuPFt7a2tLe3trnC0WhNS0tNU1df6M/Evr2+
+xM7X81hEOzk3O0NTWUw/OjxCSlFNPzxEWc+9uru8vbiysLO8z2xTVFVXT0pNW97DvLu9ydbd
+blNHPjs9REpIPTUxNDtGTU1NXObIvbu8vb26tbK3wddeWV1WUEtKUOvGvr/Iz87P1d1dQTs8
+QEtUTj41MTE4PkRDQUdny7mxsrW3trOytLrJe1NNSkpKSlB21MjDx8rLztPmWEQ8OTs/Pzw3
+MjI0OkJKTV3eyr63tLW2trW1trrC0fheW1tPR0pZe9XJxsjLys/c+FtIQD89OC8tLC0xOT5D
+TFvQu7GurrG0tbW2u8hwTUpMTVVXVF32zsG9vr7Ey9Hc/lRJRkc+MS0qKSsvNDtFT9+/s66r
+q66xtbe8xelSQz49P0NETGfUxbu2tbe8wcvV/1tUSDctKSYmKS0wNTtM2LitqKeoq6+xs7nJ
+ZEE4NTc4Oj5DT+7HurOvsbS5vMDJ2HVNOS0oJSQlJyosMj77vK2npKSmqauvt8dvQTUvLi4x
+OEFU6Ma3sK2srK+0u8bS+E88LygiICIkJysuN1LFsKeioaKlqa2wu9BKNi0pKSotND1M2L6z
+rKinqayxt77L/0IzKiQhISEjJiovP+O7raejoqKlp6uxv+1CNC0qKiorLzdCbsGzq6moqaut
+sbnEe0M1KyYiISEiJSkvPvq9raejoKChpaqxv/VBNCwoJygqLTQ/ZcW0rKmoqKqsr7fB4Ec1
+LCgkIiEjJyowPm3BsKmloqKkp6uyv+BJNy4rKikqLTM+Wc+8sq2pqKiqrbS+2FQ/NCwnIyEi
+JSovOkzYva+ppaOkp6uwustvRzoyLSorLTI9T/bOvravrKqrrbG7yONVQTctJyUkJSktND9b
+0r2zrKmnp6mttL3PcUo8NS8tLS82P1Ply7+6s6+tra+0vMngVkM6Mi0pJygrLzc/T/DLu7Ou
+rKusr7S8yOJfSz44NDM0OD5LcdPGv7y6trOzt7zF2XZRRj86NC8uLS81PERU8dLIvbaysbC0
+u8XT7GRTSUI+PDw/SVj71czHwr68urq8vsfV72FVTEU9ODQxMTM3PEROYOvPw726ubi6vsTK
+09/6Y1RMSUhLU1xq7t7Z0szIxsPDxsvO1N57YlVJPzo4NzY5PUFHTVz83M/Jwr+/wMPFy8/R
+2PleWllXW2JvcXLu3dnUy8fIx8jM0Nvh72lRRT46Nzc4ODo9QUtaaebPycO9u7y+vsHEytTa
+429fW1RPT1RWXGzw3NPPysfFxcfKz+JtWEtBPTg2MzM1OT5ETmLoz8e/vLq4ubu+wsnN1ON8
+ZlRMTExOUV138+DTysXDwsTGy9DaeU9COzYzMTI1ODxCTV/w1Mi/vLq5u72/wsbM1eH4YVdR
+T09PUVhdbOnXzcrFw8LCxcvV7VdFPTk1NDU4Oj5DS1v72MvGw8G+vr/AxcrO2ODrfXVvYFdV
+WFtidPTf2tPOysrLy8/aeVBFPjs5ODo9P0ZNWG/g0svIxMLBwsXJzM/Z3+f2f/t4Y11fXFte
+am785uPe1tHS1drpaVhNSUdDQkNER01ZYnTk1tDMy8zMz8/S1NbZ2eH7fPd0Z2tsbG36+3Zy
+e3p1+PTq4t7c4vxnXVdVU09PUlNRUVhdY2366+Xc2Nfb2dTU1Nna3Nzc4O75dm1ydmdla25q
+cPjv4dza2+P6bWZeV1FQTk5QT1NYXF9jbXvl29jZ1dLW29rX2tjT1trh9HBsY2Nua2NjZ15p
+fXV6b2lnaW1naW54cGx4b2tkam1qbf7r6ufn6Orx8uvp6+He4+36+Xhz/H5zcXBub3Jyefx9
+em//9/Ds7/R7bWltamVpa2RlbXjy5ODl5ent7PP49/56b2xpZGlscHF++/Dp7Ozu6+Tj6err
++nhybW1xa2diX15iZGlvcn7+d/fu7OLe4eju8/r57Onk5eXq7/d5/fj+c2xqaWNdW2NmZGVp
+b2767fL06Ov07evv9/Z6efLs9vvw8Ozh3uLj5+rxfGtua2JmYlxaXGNnanj6f/Tz+vfw9frp
+5uzq7fB2cPDv9/fx8fj4+fj2+PTv/vv2fXRuf/x4dG5x8/t1d/50cHtyamRobG1qaWhs9/v6
+6ufq9u7z7d/d4evv+XVveHt4dXp5eGdob3X66eXp7fb1/fd4cXVraGhtaWFlamdla3X57u3t
+6urp4eLs+Pnz/nr++Ht4fnv26+vp7HpueHpwaGp0dG1vdXT+eG5tcnZ5eHJ4/ff8+e7s7+bf
+4ej09HtubGxsbXJvbmNhaG5xfu/t5+br7vH3+/Du9PP7+nRwcm12dnVzcm5qeevo7uno7u/3
+e2tnbGxoZmtxc29scvLm4+X3effx6OXl7fLu+29tdHNlYWRlZGdqam397+32/uzo6ubm5erl
+6vt+8/Hs6nxpZmhqcnxvZmx9bXD59vb49P37/GtjZm1zfPv7+/f79fX06d7h8fHve/jr9m5p
+fPn5+nxtbHlue/t9ffTx+3F6fXd+8u3+dW1jXFxhavPj39vc4uHb29/m925nXVhaXV5hY2Fm
+eOrr7ufr9fb16eXq7vd4b3B0/HNwe/9+dHL48Pbu5+bs7fp6dv/v+nVtbWlreHJtbm54+fb8
++/Ds+mxwf3FpbWtsd/bo5+zp6uvv8fZ7eHl9cm92eG1ucH/t6/R8+Pt3e/fs5ej2/vtvb3Np
+Y2luZ2Voa3P57eLe4Ofte25we/n8dXRraXR8/fT0/P/29/bv+XlufPp3b/r7d/nv9n50dnZu
+efz6fHD97fF88vD8enVyfHVtbnl+dfz0+n397v346+708fl1bGtvfP1zc3nx8PLt8nZu/vf+
+Z2FobHJ4/vD4fv357efp7e3u7Ovs7PPu9G5na2xscG1nZ214+31udH7y9P/6/P3n5e/y7Oru
+73dnaG9ubnVxa3ryd/jh4ebofmtnX1xaYGZqbnN7+fLl3N3d29vh7Ontfvvv/mtqeXhscn5q
+ZWVeZnL/fu3m6+bc3N/p7m1cVVFKRURIT1/u187KyMbGyMvN1OVxW09MSUhLT1hn69zWzcjF
+xsrP2epuXVdXVFFOTE1PWmhqYmNlXl1gZ3Xz7//67N/T0tfb3unxfGxlbvjy6uDg2M7KycvL
+z9jjdmBXT0tJSUdJTFRhf+Dd5nBjXFhXVE9NTU1WX2Xv2NPNycjJycrKzM3S2+Pz9u3f5enk
+6vhzamBcW1lVT09QU1dZYHPw7/d3aFxQTEtMTk9TWV9l++LWzcbCxMLDxsrLztnm8XRgW11s
+/e/s6uzs7PVyaWBcWE5LS05XXWJgVk5OT1RWYXNpaGpt/d3SzMjGx8rMztHV1tje+Xhxa27r
+2dPa4+9vY2RlWlROT1FSUlVbWlhUUU5OUVheau/q4t7c3NfPzs3LzNDV3Orw5dzb2trZ2+Hl
+3t7f7HpfUE9UUk1PUlNbaF5WUk9RUVFQUVdcY3jn29fPy8nKztLV3OTi3t7c2dXS0NPY3dvV
+1tnrZFZUT0xNTk1NUVJLRkRFRUhOVVxlbfDa08zGxsfJzNHT09fZ3N3i4+Tm3dPQ0tbX3PBn
+W1thX1VQTkpLT09LSkpIRUZGSlV15dvW0cvIxcTEyM3V3+xtbHV3fvvr39bRz8zM09re6nZt
+ZVxZXF5USkI+PDw8PUJLUmTr2s7JxMC/wMTJ0N5wY2RcXmpxfubaz8jBwMHFytHjaFdWVVFN
+SUI8OTc2Nzs/SV3m1cvEwb+9vL2/x9PoYU9IRUdMV3Td0MjCv726u7/FzN9pVE1JQz46NzQ0
+NDU4PUpn2s3Dvru5ubm7vsTO7lVIQj8/Q0hQb9PFvrq3t7i7vsXN3mJMQTs1MS8uLjE1PEZZ
+4czCu7e1tLW4vMPN6FRHQj48PkBIWt/Lv7m2tbW3vMDGz+tYRTszLy0sLC0xOkVc08G7t7Kx
+srO2vMPSbE0/Ozk3ODtBT/XLvreyr66vs7jAze1PPjUuKyopKi0xO0z3yLu1sK+vsLW6wc7y
+VUQ8OTU1OT9KZ8+/t7Gvrq+xtrzG5lBCOC8rKCcnKi43RW3JubGtrKyusLW7x+xUQzs2MzEy
+OD5O3sW6s66sra6yucDSX0U4LysoJiYoKzE9WM67sq6rq6utr7a/0mdJPDUxLzAzOUFb1L+1
+r6yrq62yu8fhUj41LikmJSYoLDRBeca4r6urqqutsbnD2lxFOjMvLS4xNz9bzbyyraurq62x
+uL7WUz80LCgnJiUoLDNA9sO4r6upqaqtsbnG51A+NjEuLS0vNT9Y0760rauqqqyvtb3RWz80
+LCckJCUpLjdK2L2zraqoqKqutL3QXkM3Ly0tLC4xOUd0ybqwrKmoqaywuMTnSDctKCUjIyYr
+MT5rxbWtqaenqKuwustnQzgxLiwsLTA4QmXKurCsqqmprK61wehHNiwnJCIjJiszQvLAs6yo
+pqWnq7C6zlw/NzAtKystMDlJcsm6sa2rqqutsbnG9UU2LSgkJCYpLjlN2r2yrKmoqKqttcHf
+Tz42Ly4uLzE2PlHgxbqzsK+ur7W7v818TDwxLCopKi0zPE/Xv7ewraysrbC3wt9RPzg0MzIz
+NTo/TfHKvre0s7O4u8LO6GBYT0Q7NjIvMDQ7SG/Pwbu4tbS1tbq/zO9PQz06OTg6PkhOX+vV
+yr+7uru8v8nZ+F9YUU1KRT8/Pz9DTVzy2c/My8rGxsjJy9TjaE5MS0tLTU5OU17+3tLLyMfH
+y83O1t3f6nZdTUM8Ojo8Qktb7tHIwby7urq9yNduT0hDPz8/Q0dPX37k0srGwcLI1etxYVtU
+T0tISEhMVWrh0MvKycrMy83W4fRtXVlVVFZXWVtgXVlZV11x7Nva2tbZ3+Tl7nZpX1pWWmJs
+amz8497b1NLR0dPc7HdjX19dXF5nX11bW15lfO3m7vZ6cXH77/V0bG5xcXH+8e/q6ez4+/rv
+6ePc19Xc4uru6ufreWFaVlJNTE9VW15pcfHp4dve3djX3ujyeWhhX2Jrbm93/fXo3djZ3uDk
++GxnYF9eW2JraGdzfPbr4+LwdWpna3Z+9e3xevvt7+jg3ef98vV1YmFfXV5nb2djZ3jy59vb
+4unrenBwamptbHNxef717uvq7+nm4Ox79/5ubW5qYV9kaGtrefP17O3z6ODn8fD1e3lvamlt
+d2xscnL3+fXy8vH17er0//t5b2ZjZGlv/vLl5OXk73f57ft7entxdvz5/nf0+/v5dX18bGhq
+dXv5+3h6+/r98+z07en5dXJ9+3dsZmJma212cXTy4uDa2d/k6en2dWpmX19mZmNmb3P77Ong
+3uHn8XtvbWtvd3JxZmxtZWluamX65+bl5urs6d/e5Oz3e3Zxa25tZmllZ29udvHr7ufj6Pp4
+cmpoZGVgXV9qanf97NzZ2d3h3+Tq8nttYmlnYnr0/XhvcX7+bGx0b3R78vDy5eLo6vt3bWNb
+W11fcune39zW0tPW2+TyfGxmZF1aW1JLTExNUVxiYm7o3NTKx8nJzNXe5/1tbW9zal5dZGhu
+eG9tb3H+6OXc19/r7/p4fGlYV1pcXmp27ej5c1pRVF1famtkbnltcvHt4tfY29vd3drb2dXV
+19na2dfU1tzo9G9eU1BPTE1OT09XXF5ra3Hh193mc1dPS0pMVV1fYmVndODPyMK+vsDFy8/S
+19nn+21fW1VbbOjh6O9qVlBOSktOVVxcWVhYXW7z59nW2+hqWVZUUFBRUVddY+/Wy8fCwsTE
+xcbL0tvm+XZmXV9z59/c3ur5b1xVT09OS0pKSUlITFhk+OLc29vb4+j0bmNdWVRTVl5t9N/Z
+1MzJxsLCwcHHzdTh7mxgY2ZeWVxhYmZwcWVdWlhOS01SVFVZXF5t+e/u6eTo+WFXVVNSVV5l
+c+vg39jRzMjGyMjIys/b4Ov99/Tt+/zl4+r0+PtpW1dZV09OTUpKTVRbXWnz7fz/a11WU1NS
+Vl1u9O3s6eTYzsnGwsHDxcnP1trn7u78dmhlbHR49+p5al1RT09RU1tjY2NeYVxWUE5OS0xO
+UVhodnfy49rUz8rGw8LGzNPc5uvr6ujc2d/k4eTg2dnc3ul1XVFMS05PUVhXT0tFPj1AREpT
+aPTm3tfTzsbAvr6/xMzS3Op+bmpkYlxh/enYzs3P1NXY4ntpX1NOT09KRUA8PD0+Q0pXet7P
+yMfIx8XGyMvO1uPzcGJfYmvz3NTQzMvJyM3Z5/JvX1lVUFNcXldMQzw5OTo8QUxg59XNx8K+
+vLu8v8fV8F9PS0lKTVp83c/Jwr69vsHIz91oW1hQUlJJPzgyLy8xNj1Ld9HDvbu5t7i3ub7F
+029OQz8/Q0ZPa+HNwr26uLe5vsbYbFZLR0Q/PDYwLSsrLjdCYsy8tbKwr66ws7jC3VpEOjUz
+NDg/Te7NwLizr66vsba/1GlPQzw1MC0pJycqLTRD9ci6sK2rq6utsbnJb0Q4Mi8vMTY9TuvH
+ubCsq6qrrrW+zmxLPjYvKyclIyQnLDhO0Lmvqqimpqmssr3XTDkuKigpLDA8V865r6unpqap
+rLO90Vw/Ni4pJSIhISMpL0HZua6opKKipKmvu9VJNSsmJCUnLDVJ1bqtqKSioaOorbjNWz4z
+LSgjHx4eHyUtO+u5rKWgnp+hpKq21UUxKSMgISMoL0PZuKyloJ+foKWsudNLOS8rKCMfHR0e
+Iis83raqo5+dnZ6hqLLLRy8mIB4eICYtO/q7rKWfnp6fpay40E89Ni4rJyMfHh4gJzNVwK6n
+op+enqClrb9QMichHx8iJyw1Tcewpp+dnJ6krbzfTT87ODQvLCYhHyAkKzpgxLOrpaCenqCn
+stw9LSUhIiMmKi45V8Cto56dnqOrudNYRT48PTs2MC8tKCcoKS47W8WwqKKfnqGnrr9RNCok
+ISEjJy05VcGtpZ+en6WtutdQQjw5OTs9P0RDOi0oJyYqNlvEsKiin56fpa7GPywkIB8gJis0
+TMmypp+en6Otwm9GOjY2NjlFbsvDxN46JyEgIis/3LytpJ6cnJ+szzclHh0fICUsOuywo52c
+naSvv/FENi8tLjhVyLavrrPFUy4fGxsdJjvGraOcmpqdpb44IxwZGh4nNeWuop6cnaSvwVY0
+LSoqLTzmvK+rrK6us8hAKh4YFx0qPsaso56amZ6uUigcGhwhJzbWr6KcnKKvxlI6My4pKTRr
+uq2qrri8vLy9yE8zKSYmJCUtPmq+rKWmqKu2ZzYtKCcsOkviua6srK2430k+NzQ7SV/axL2/
+vbW0usHGxs/cdkIvJyEgJy86Sde3q6WiqLhtPzcwLy8yPH67sbCvr7fJZ0EzLzlHUGPPvraw
+rK64wsPF12xHNC0pIR8nLTVFy7GsqaWos8XgTTUtLjAyQtG+vbatrbrO3lg/PD5BRWbFu7eu
+q664urzTTzwuJiAfISgsM0XOs6qlo6Wpr79XNi0qKSkuO1rBr6qqrq+wvVw8Ojo7QVbdvrCt
+sbWzt8tPPDAqJR8gJykrNvS2rKWgn6Opr8dFLygjHyMuPFm/raaioaartMtINC4vMjlH+Mm8
+tLCvsLXEUDctJB8fISImMW+7rqSdnZ+kqrlaNSsjICMnKjjVt6ylo6SnqrHJb003LC01ODpY
+wbq3s7G1yj4uLygdGyIsLjnFqKOjn5yeprHEUC8nIyIlKi40UrqtqqekpKmwvtRXPjYzOD4/
+TM/F7Vz9RTIyMCssMTg9Yr63tK6pqqyusb39R0A3Ly4wMy872u/ts6uys6ystsDD019TT0hA
+Pzw8PTUzPTsvNEE9PE7Zyci/ta2tsK6uuNN6bkEzMzg3MTNL+1Hrure7t6+ts7y7us1pYlpE
+Nzc5Mi84PTYzOD5BP0nYzNHAs7Cysq+xvcfF7VNIQUhCO0RFQUdMXdvMwbq4t7W2uL/N31pE
+Pj47NTg5NjY2ODs7P1dv6c+9t7e5trW4vcPFy/d75XRRTE9OR0lKUVZt1szGv7q8xczR705M
+TEZAPz49Ojs9PT1ATFxz383Hv7++vr7DyMfK2OHe7XhmXVtPUFdcburPyMO/vcDJz+BcS0dD
+PTw8PD09QUhNTV157+Hb1NPX0czU09HNydDPyMza4/NsW1RZWFxv5NLKyL+/xsrP4l1OTUY9
+PT4+PT5DSEZMX37+ft3U3eTU0NXT0MrKzMbDyM/Z2+taWmlcWmZ+8+bZz9TWztLh+2xfT0pO
+SkA9PEFCQEdPUl3z2tra0MzOz9DQy83Mys/X2Njf6vj0bWRv++/f2tLS083U29zwcV5MSkk+
+Ojw7PDxCTk9Y79XPzsrEx8rIys7b2tTl7uDg7u/f2/Jw8fZ77d3Rz9HJx83Q2N1YSUpAODU4
+NjU3PkZJXdjNycC8u7/Av8bS1dt9bnV2YmPl2+Pd197v6t3a4djLysvSz9xOT0o5NTU0LzA5
+QEJM3s3Lwry5u7y4usXGyd7+aGBXS1ZlVWPn4NzbzcfPzMPK2NTVYU9PPzc1NDIwMztAQ1fb
+0cm+u7u8urm+wsHN2dvrZ1dbWlVbbe7t5c3LzsS/yNTO50lMRjMwNTAtMDc9RUrnyc6/tbm7
+tra9wr/J3OP6Y09PY1paed/g5czHzcnBxM7P1VJGSjYuNDIrLjY1Ok/z0se+trS4trG4w73B
++G/4VEpNT09daenU2cu+wMa+vMnY0F4/QjgrLDEsKjc9Ok/PzMO6trWysra3vsjK3FZOTEZD
+SEtPW+nVz8W+vby6u8DGymtCQzssKi8sKC8+Oj7cw8O8s7GysrO3vMbP2XxNR0pFP0VVVlrc
+ysfAurm7ubrFzt1NPDoxKSotKio0PkBZysC8s6+wr7C2u73H/l9cSUFGSkdMWmjmzsfAvLq7
+u7vAzNhePzk0LCksLCouPERM38O9t7Gxr6+zur7E2GBXTkNCRkZLVltv1cfGw7y5u7y7v9Ds
+Tzw5MiopLC0rLz1IWdXAubKvsa+wt7zAyN1aSUVFQ0FIVWl37szDxL+7uru8vsjbZUs8NzMs
+KSwuLS89S2bQwbm0sbOzsra9yNLnV0hHSkpLUFz669/NxcHAv7+/xsvP3WhPPjo4MCwuMzEy
+PU9m38m+uLa5ube5wMrO3VxPTk9MS1Jl7eDZzcrGyMrCxc/a1eFoUUdFRj03Nzw7NjtJU1p0
+1cTAw8O+v8nOys7rXWl+cGBkau/o4dvMy8vOzMvQz93r829TT0tMP0JAQT0/RUdITFjv4N3T
+zMjPzcrO19/i7/Xwc+Tq29vW19XRy9TP3dD7amNvWlpMfVvwUeB0bFVZWltMTkdNS0lLTmpu
++PLd1d7r4dXU19fP1M/jzNTH1s3K1dZ63/53amH8XmdaaWdtbe1z+FpeTEhHREZFSEpNVVZ6
+/NnU1MjOzNbOz9TU3trS3uzj2ufn6PTo593o3enf6Obo7G52XWtZU09TT0tLS05NSk5OU1dr
+dunZ3dPOztPZ1Nna393k59/j2+Te3Nbc4N7c5PPwbnRcYlZYVFtYXl1kXlhfempeYHNrbG5s
+Y19qXf3p7H7i4Nvk3ufe3OTk2tjZ2NnX3N/r/nhiWltUWVdZWlxsZ2Zv+ex5637d5Or38m5v
+ZWdsdGpk6Ptw/nrvfP9v6/R7fH1s+Ph8avNx9uPh5+jk5+3r/eZ63ObhaXNeZGz7+m1rZl9f
+ZGL0dPZ/6vb3+vvx/vx6cvxsaWh8bGxt8vj/e9zteP727fL+bOvr7Prteul0debs+mvn3ers
+7OTibGN5eVxZXmVtWlprZmdge3Jvb/jn6OXt5vbj9e393nvv/eP23uPm4fjhc+lz/m1k8l5o
+V29pYmFcc+1fa17hV3hi8npzeNLy4XvZdtNt3+D+9uTm5OvnXuJrbvx93XPfWGjbVGRa81Bh
+X1NqXFJ49/Nl0ubh5N7ffORl2/Vu/dvcZ9PeeNxv42PaZHbfWvzl9mbd3FfpW1VnTk9NbUdU
+81Vp5eTnz8/4wuPs289mZtNP6W1bfd5yY9Xdac/8z97X6NvlcN97au9PWU9PPUdLPklJWmVz
+79bB18zCys3b1t70aGLyVFdqd1/30+je09HP3NfW2Nfn2eHcb/BpRklMQDc/Rjg9Sk9b587H
+vcTAusTVz9BiWF5OUEpMbHZ03MrOy8PIysnIztjd3eBodutbPz9POy87QjU1UFlP3sW/wb66
+uMLPytxTUlpIRFJOT3Pi1srFx8K/yMXH0tLS7XvrdGxYT0s/Pjs1ODg2PFVOVsjEzL+5vcK+
+xdXnY2FRSk9VT1P4293PxcPHxMTNzM3r5eFxau/vUVhTPj8/ODs9OTtQUlXLx9TDusXJvcnr
+2txaUFRWT09f7t7eysnRzMnN1c7Q4OLe+Xrd5H3tbk1ITT85Pzw2PU1JSeDZ9Mm8xMm8xOXW
+1Whj8WZZZmdh5tfa1szMzs3M1Nrc4PLz+2lpamJoYVVMSkE9Pj07PUhLUOzZ1Ma9v8TAxM7V
+1uRwe21gXWZqZ3z25dnX1NDT2tzc3+Xf3uz5/PdzbmZPSklDPTs8PT9MV2LqzsrHwcPGx8jR
+6+x+YF5nXVxzdnjg1dHOy87U0s/S3N/m73lsZW1lYmhUR0VGPjo+Pz5IXmj6zsjKwb/GysnQ
+5+nmeF1dXVxkd3Xq2djRzM/T0NLY2+Dx8fZ2/fxudm5aS0pHPjs+Pz9IV1tk2szLx8TL0szS
+7uvlZV79f2zn3N/Xz8/Qzs/V09Tk8+z6e3xwZmloWVRNRUNEPz0/RUROaX/mzsjJxsTKz8/W
+7nl5amV69vPm3+Dd2tjSz87S1tXc4e1xal5cWlpXVk5JRkNCQURGSE5c/ODVz8zJxsnO0dvl
+6e7q8vbs4ujo3NrZ1tja19fd3d3i+GpmXldXWldUV1RRT0pJSEdJSkxUZXnr2dPQzsvN0tLV
+29zb3+bp7ePd29rX19zc3d3j7vVyZFtYVlJPUVRTWV1iZmRfZWxoZmhmX2JoaWVv9Ovg2t/f
+2uTq3dvk5+Xq5drY3Nza2trc5e93cWdcVlJTVFpdXl1hcHpyeP/07fxoX2Vv/f5tZ2z05+74
+7vN7eHd6d+vf5+nj4d7c29zc4+31e2pjZmJeZGhkaHFvcv3z8u7seHr3/HFpcPh6cf/4c256
+fHRvffh+b259+v/67/f7fHhxffr06vdrZGptb/bt8evp5uXp5+Xm6vD8eW1scnFtaW95a2xs
+aG5tbmlqfHj9fW9/+f56fvPq5+30/HxtaW9pYWz98vn49fjx5uXo5OXo5e74+P5vaGZia21q
+c31vfO3r6/F8fHRyen1saXB++ndqZmVqcmpna2x96ejn5+vq5d/j7vLp6f9veGxsdX7z8Pfz
+6/P/dW5maGxrbXBsZm18bXL2fnv463xyeH13df70+fLs7ebm6ur1cGpudmz59/v4d3v97+7u
++3V5+n9yb29wb29vbnDt6Orn6ff/b2ludP5taG1vbn7v/vzx7u/07vr/7unm7flyXkzkxEh5
+1txfamhMw0xSz+jyVuDr319QY79OZf/Ibk/eYc9H8NnTSdJ7+lftzF/qW+zOU2vqzEvtTshd
+b11ya/JlWk2+THVqysxF2PvNSl3celRbXcZcTcrIZVG/e19ads5oOtvN9zvU39FKT8XYzznJ
+5cg/asBH0UXEdk/JVeJhyl9Zw03W3kPK91FK0XlR10nF811c3NVM6mDe2FTi6tlmUnrm3E9G
+3sxpT1jE1FZL3cTsUF3JzE1Q0MpXTP6/b0xpyNBHUuPPTkZ309hPSdzD+0VoztBcRGfD2UpR
+zNRyVufM011SbdxaS1Bs23pc6NbI8V/fyO9PYP71T1P3y9703tnScVVY8mxOUF/k62Bt29rv
++t7gd1pldnJibNzj8evv2djYZW1iZF5Va3VzbHzr7d3u629sfmNdXmt4evf64eLp1+Tz7Ovs
+5Gtmb216YnNn+Pb87uDocG1+/l5jcvNs/utvb+Defu7xY2pdWmNdZezT39zZ2u1++u1zWWFo
+a1x/6t/g49vb7GVYX29iX/3m6Xt46ODudnFv4/VkXVpof3j69unx6+nd9mz7bmdu63Zr/X50
+aGty4Nfr/erj7OtdXfXsa3nu/9bsXFv54HzuYWr67npu6u/nclhXXmZv6NfY6V5WYf3r/W98
+3djpaFts5N3e7vVqYm5r/9jP1NHZ8mxpZ23h5vD7cl1iY1RNVVRWT0M+QExScNvTx8PEycrR
+2dXb29XO0c/IyMrN29zd8WRldtnAPR4hKi47X09ss62xs8xDTNzf5c/r6r+8wsXL6NPIzNXc
+81762ce7uMEvGhskKDRVWdOqo6eos3tOTzgsLzhMwrOxs7fB08/O2edsWfrIwLy3r65IGhQa
+HixYc2qwoZ2boMc0LSwqKygqTa+joae2zsrGy/1FOT7bua6qqK9LHxgZGyArOFC4pJyZm6jW
+OSojIiMmN7ynoqKrub/CzOdINDZHzbGopq74KxwaHCAnLTjrq52YmJ+xcDgqIyAfJjrAqqam
+rLO3u8xcPTM6V8axqqq3UCYdHSAkKjE7zqecmJmguE41KyUjISIv3LCopquxs7jF3EY3PFLS
+uq+zwEgpIiEiJiwxPcapnpqbo7blPi4qJR8fK0TErKanqay2wtFaSkxPXc+9u75eLCMjJSox
+NTvfsaOcnaSxzkQ2LygjIyk0a7Wqp6ist72/ytRtT01ozsC/ViwlIiMpMzk/27eroZ+kqbTh
+PjArJiYpLT7Kr6mmq7a9vcbJzO5aY9zJyFAuJiIgJi40QOG8raShoaSrv00xKSYnKi06YL+v
+qamtsLa9v8LO2d3f6WE+LScjICIpLTnvuq2kn6CiqLfoPC0mJScrNE3JtK2srq+yt7q9x9PX
+2N11PywlIh8hJis1Xb2soZ2dn6WwzUcyKiYkJiw937uzsLO0sbGztr3Gy8vN3EktIx8dHiQr
+NVHDr6SenZ6hrMJUOC0oJicqNEvOu7a3t7Owrq2vuMDK0d5SMSQeHBwgKTJMyrWpn52dn6e5
+6UEyKygnJy06TtrAurmyrq2rrK+3vs7rWDYmHx0cHycuO+u7raKdnZ6ircJiPi8pJiUnLTlK
+5se9tKyqqKissrrAzndBLCAeHR0iKjFB17epoJ2dn6exwX1BMikkJCYtOEr4xbivqaalqK2z
+vMjWXjopHx0eHyUrND94uqqhnp6gp667zlY3KiYkJSoyQGPLuK2opaaprbXA02dHOS0mISIn
+Ky8zOUzXu62npaittLq+ymM+NjQ2Oj0/RE5w3nhvtKituMG9trXHSkJEOy8pKCwzNzU4QmrL
+v7ivq6uwusPL1G5JPjw9PT5DTv7Ov7m2tbe2uLzF/D80LiopKy0yODs+aL+0ra2ur6+zvcrm
+U0A5NDY7QUtV+cy8trS0tbm+yNRrRjYuKiotMDE1PErsxrmvrKutr7e/zPFMPjk5OTo+RmDT
+x7qysLG0usDL22dENCwoJSYqLjdEY8/At66op6istsTuT0E5Nzg4Oj5Jbc2/t7KvrrG5ws7k
+TzwvKygnJyouOEztybuyraqqq662wdxUPzk4NjU6P0de08C2sK+vr7a+y+dNOi8rJyYoKy85
+R2jJurGtq6ytr7W90FtFOzY1Njo+RlbdyLyzsK+vtLq+zGNDMywpJygqLDI9S+3DurKtq6us
+r7fA309CPTs5ODk+RVnbxbiwr6+wtbm+0lc8MCsnJykqLjU8Tt3GurCtrKutsbnK/E9BPTw5
+ODo9R1/Xv7ayr66vs7a9020/JyQsLCcqLi8+XtW4rrCvqqyyt8HhZUo8Ozo2O0ZO68zFubKw
+r7K0uMftQTAsKiYmKCgtO07Hsq6rqKmpqrTHbEI0Li4tLTQ6S8q7ua+srKmqr7bE3N1mNykj
+IyUlJictQM+3rqiloaClrbzsQTQsJSAlLTU/V86vqKampqiprbzZYk9PPywjIykqJiQnNGDE
+urCmn5+iqq+31z4vKSQkKCwtOF+/r6uqp6Okqq+7xc57RDkwKCQnJyEiKjVJ28KzpaCio6Wq
+ss4/NS8lJCopKjdJ3Lexrqajp6mqrrLBZU9JMyooJiYjICYwOUHRtqulpaShprC9z1k5KiYr
+KSUsPVjWwLOppainpamvucvuUzkuKCUkIx8hLTE1WL6wqaakn6Cqr7jQRzcsKislJS84PXK9
+sKqoqKSkrbK2x/dSPzMpIicnHB8vLy9QxLWqqaWeoa6srsxRPy8qKyYjLTQ5XcG4raenpKOq
+rq++6mBCMysiIykfHCouKjvIua2npZ6epqiot3BENCgnJyElLjE90LmupaOjoaasrbbdV0c1
+KyUgIyMcIDIuL/20r6iin52gqKuz/D82KyQjJicpM1HHua+ooqSnpaq3wNBnQjYwKyMjKSIf
+LDUyR8azqaWmn6Csr6/VPzguKSkoLDg8Tb+0tKypq62yubnD331lTz84LCYtKB4nNy820Lqt
+pqqkn6m5t8Y8MjItLS0uRFdKz7Cvsq6ur7bFwsDlffpv60c4MCsqJyElMTQ52Laspqamoqq+
+z949Ly8vLzEyRd7rybKur66wsbbM28vvUnRrYVxBNSwrKyQiLTo8Z7qtpqeqpae54/RFMC0s
+LzczOujHxrWtra2yuLjE7vdsVFJWVG9VPzYsLy4lJjhFSNS3q6SpraetzGBQOTAtLDQ9OkrO
+xLqxsa6tuL/D5VhQR05vX1/p/083LTUzJik5R2jJvqujrbCss8pcPTY4MSwzPkdd3cCxrbKz
+sba/4FpaWEVAUGTu5l5gQzI0NS4uNj1lysW0q66xtbzF30s6PDo1Oz1FaNzPvrWzsbW+vsd5
+VVNKSkpDTvllX1tJP0RBOj5GTXvb1MvDw8TDztrW3u50WE9RT09UWm/o2dLNy8vOz8/a7fHj
+7/ru7+7b2Of/Wk1KSEE9PkJIT1Jh6uDX1dbT19rb91xoeGvv29HOzc3MzM7Nz9zp//vu8Ozg
+2d3q7W5PRkZHPz0+P0NKT1hn7tzY1tjV1NfV2+Db3ubc2NnW1dnc4N3b4erj3eTn2dPV1dfb
+4mNPTUtDPjw9Pz9DSk1SY+rXzszIxsfJzNLW3O94df5xe+/z+fTq3NbW19XU0NLZ19je4eZ1
+ZFlKQj06Ojo8P0NJVnvc08vGxsbHx8nO1tvl+21s+PF8eOjW1djT1NXS1tve3t7sb2pvaFxR
+SUE9PD0/QUZOVlp53NHMyMXHys7V1+D4f3d97Onq3N3c1dbZ1dnk6efs/Ofu9e31/vlsVVZT
+TUpHRkZISUhLU11dduPg3NbQzM3SzszPz9DX1trf4vF7b3J2c/z9/O3s6uHo6+bucHVyZGFn
+Yl9bUktIR0dHSU1SWGX65NvRz8/T2tnT1NnZ2t/k3Nzf3drW197f3ud9/3ZubWhqXl5nYmJr
+aV9XT01LSktMUlpga/je19fa2dbX2+Lf537x5+ju7eTl6+rh393Z3fFybXL4cWh37O7s5e5+
+bW5wXVZST01NT1NVV2BxeP7o39vb3+Dl729OUcba38rMxM7c3dbvaldeYXdKTtLg8PTQ0NXb
+ZHzfWj5DRDs8PDtFaF39xb68t7vEu8DWf2NOTUo7P05LT3fbyL+/w72+xMjS2NDZ8ujuUToz
+NjMsKi02P0xew6+rrKyqq7DA7FZBNy4tLi84PlDVvreyrq2vs7zM3WhLQURDRUtOST88PTw2
+NDpDTnLdybiys7S0trzH6FdKPzs3NTtGTFzOv7q2ubq7wMvbfVxQT0lGTU9OS0c/REpBP0ZQ
+WnHe183Dv72/wcfL1vdiVFNPTlNe/uzk5d3Y29/k5PtrYGp8bP3u6tzh8P3z7v15aHRiV1BK
+SEVKT1BPW/l8/9/c19TT09LNzMzNz9LX2ub+a2VfXW5qb3z99+rp5t/o7fh5cmFebF9VTUpK
+SkhITlVaYGvl1d3h1c/U1NXX2OXi5+304uLr4tza297T0dzX1tve82RqXk9RT05PT09SUVhn
+Xl1pZ2bw7fbr8vbq92hqZWZxZGr+8eHb187LzcrJzdLW3+Tpb2hlW1tiX1teZWhdWGhuYm7z
+fnFrYmluZWpgYmBdX2doZP1+8+r04Nzg39jY19ff3+Xp39/u7+78bG5ra3FkZmheW1xdYGVv
+7+vz+Pnw5+325+n6d3doZ3j98/V+cnjx/XF47fpybmNlaG9ve+/27vTs6nz26uz18vXr5e/p
+5ubh5vp4em5wfHRnW11hX2/v7efo7u3n73dhX2NdXmRt++7y7vT17/z4/29u//T3em/v3uPj
+3N3p7/D+/XxvZGZkXF5qfPzm4ebk7fx8/mtgZmtte/Tx7ufn7fl9dmhfX2Rub2/7/Xj56+Tt
++Ovo7/Ps/PP6aXj19+3i6H98+PRuZ3B7cHTx6+/u6eb4cfDtaF5pa15ec/hqaHx7b3nw7+/v
+6ujz+/h3df35d3P68XNjd/L65+tT99bu2t7S3dp78u5iWl1xX05LRVzN51R5yMvQ6WzU11RO
+alRQVlVN79vk49DJz9Z6b93ZYFh7cWBvYVny4mhnfund72Nq3eNrXl5kXF9fcefk6nft5+z/
+/eTj72VZb+fyaH7h3OVzfe/3YFxv9ndoaW7v7vXm29fl82xi599jUmD4aFto+f766+rt3eDx
+7/VzY2JeY2pjXnvc2fJvfWdcXmVw8fRu+OHl6uPsdN7b7err7/j19mvw2+jl82536fF76PFf
+WVpUTkpGRE1OSE9o6+ni3s7FzNPN0NTR2uHSztna29zS0tvX3v3m33tub2VcXEotLDo3MzZI
+1LzGd8G2xmxV+9LdVFbIwMzSyL3C1PHWxtlmZNnJ1f390tX6al3733g4ISlGMCcy6rq33Pms
+q/VGX+55TT9buL3exbq9zW9b08HxWdPEx9/p0MbSXnzT3mFUKx0sNycoP8Kvss+3o7JPTeNy
+OjE5+cLZ0rmxvedq2sTUX+nEv9Di1cPE3eXbz85kRC4fJC4qKDbQs667uKmu109SUUI0M07J
+2uK/s7fUUeq7v3Htv7rF8G7Gw/t42MzI20wyJCEqKygvbbWvtrOoqr9ZSUo/MS4+3NnNu7Wz
+w1tcz833/NC9veJ0yL/Q69rJxutKPC8mJSsrLT3Rt6+wsKyyz11KPzgzOFXQ1se5ub/fb8/J
+7GnUycfS+c/Ez+XZzc7OdEE+LyQpLisyVsSzrbauq779U0I7OjE2Y9brzbu5vtrmwcDo7tTN
+zWxf1MvS18zGvslYTEcuIyktLTI/2rWvtq6sts9UQD88MTZU6uTjzby6yM7BvcLYYvLeXlBb
+3s3KzMi8v9TlYz8tKiwsMDVG2sC+urS0u8jXalVHPzxASkpLWdzHwsC7trnHz9TbZk1SW2B5
+4+nhz87d8mNJQUNFQT9FTlhWU1t54t/d2M/Ozc7Y63BeU1ZdaurY2dzd3M/R297a0tjh3dbZ
+497Y19zs9nZaVE1BOzw9Ozw/Sl7+7tfJx8fJyszU2t7m5ed3X1tr6+Pb08zNztDOztja2eh6
+/vHr+2RYTEdDPDk7PkBJTU5j29HSz8jEw8fMztDc82dcW2Z5Z27f1NXY1dHMy8/T09nh5uvz
+eWpcUEdBQkVBPUBFRklNWmvj09HJw8LGyc7a8WRdXWRrcXbj4l/XxtfKwL/Jzc/L0Oth/fZf
+TUhKPDg8OjM2PUZZXei9t7q9vMDB3UtGR0RARUvzzNfPu7i8vcLBw+tOXVtMTlNw0tbZwr/K
+1n0+Li4uKCguPmDayrCprbW2ucN1PTlAOzU9U83Av7q1trvG5djoTUFGTVZVTuHCxcrDvr7L
+42U+KiswKCgvRO7DvrCmq7m5u9RQOTE8PzE818jBvbywsMvlx+dLRENPWUVOzs3UyMG7vtbW
+z35EMCovNSopPXXPy8Supq6/uLfXRDc4QjswPs3HzsG4r7fa3cv3REFLZVZHZ8nQ6s7Dv8XX
+ystuRzItPDMmLUpbfNK+qqi4ua66WD45PTwvMU/e7ty9s7bAysPPaEtJVlFITvr09tfMw8HH
+xb7E9ks6NTstJzFDRE3Vu62wvbOvw11RR0A6MDpTTFXQvbe6wL+7yV1hZ1VIQk1rW1LfyMnL
+zcW/w9RKP0E9Lis4QkBAab+1ur+0r77i9n9SPTU8TkRCa8vBxcrAu8Pc3NXfXEpOXFNKU3zm
+497SxsLO5n1vUT84O0E+Pk3x1tHMxL7Cys7V321QTE9OTVZeZufa29zW2t3W2d3d6X1xaGVu
+f+3e087NysrP3X1YS0VBPjs8P0RKUHHe1s7Nzs/S3ez1cHri5ezf4t/X2NvZ1dzn7fXv6vB0
+b2xv8N/Z19rb3/ZYS0lGQD4/REpPVmnc1dDJy8/S2+p+bm7v4ujr5Obj297g397g5OTl4er2
++vB5a3P38Pfv6fZuXE9LSUNCR0pKTllt4NTSzsvMz9DT2dnX297i7nRubWdeaX58//Hq393f
+3Nvf829ud3737+fwZFlVTkhEREZJT1pedd7T1dPNz8/Q2eDg4N/h6v52/m9lb/b69O/s5N3Y
+29na4/Lu73RjaW9vaFpQT09LSUpOUlZdYW3r29bQztXd3N/q7enu7ev8+e7s7vP0/PDk3NjX
+19rd3OtqaGZbWl5eXFxfZWhqbGRYUlNWWFlfZ2ppevv36t7e6OXh5d7Y2dfY3t/b2Nvf6O/3
+/vV5eXhmZV9dXF5iXWJvcW1xfHv29/52//T8eX5tZGpuaWpud/5+fP7z+/nu6ezt6vt++Hxv
+eM7if9Pp5OHqcnBtaGdlYWdjZGhrfnZ7+nl3+/H7/n706+nk7ft8enRtbmt38/RzbGptbnf1
+7fp77u7x7vJ9dW9zc2ttb/727eDe4eTs7er+bm90aGRsbWtqdH1wefX0+/t1/vV/9ern7HVt
+aGl6/fp+/HNsbW1yenJ0ffHo5uXn4d/g4Or1ff72dGhv/25lZGZkZml2fHf+/3lzd/p8e3l4
+cWl0fPn6+OXe4Onu7+nn7e/0/H56ZF9kbnp5b3T3+vn//e9+/fB+//t3c/327unv8vn9fvv9
+dmxoaWNpbHd1fvV9en3t8nlubXB87ubk4uLo7O/t7/P3/nl+cGpnXmdve/rx6/B++3l48Obx
+93lraGRpa21yfHz/cW17eWp28ezo5/H96OPm7fZ///3/eG10b3P/bW7+7+7v+G9vdG94+vH6
+endvdHt3b3zu7vDs7fzu7H/7emNqcWxuevF7dP14c3F6WVTG2kdczs9SQfew7TVAsLEyLsGp
+VS/drc4yQrrCOz3QxEk95sVtRvfJ11xj2l9m4GVc09hdae3bcFZO+9NzUm7V1ezj0WRl8WBv
+X09QamnO5+jNyOlQTFbY505VzMjjatvM40tN5PlITt7ccWjfzth45X1gX19XSFBm+N/fYt3E
+6F7cyd9hcNfaVE3311pMbdpvVHzY3nt6z83ud+f+UFJmXFFk5/Zz4NnhdF/g6VJa53ZaZ+jp
+4u3Z221d+/NsZl5d3ude/9Xldd3obnvs5+F3YPLnZ1li591oUuvWdl9d69prW/nueXpkZejs
+Y3H/avN0V2Tq9Gnq3vPd2t/f4Pj06+1/XVxqaVxdbOn+X/fi8/bk62/s4O1maWpt6l5R6dRr
+XNvY7u9p59xfWvvlZVlq/+p7c/3e3/7+bPXsX1Ro+nJqeuLY3Gz85ORqWnvj8Gdj7d7v/Pr+
+fX1sbvz04/1oYnN9ZVlmfP53a+PkcvTd3u35fXx5bnT+729p9/107N19eOp5ePd9b/XvYmXu
+fF1u7OPndFxq3u5t5uHl3PZRUvpnWWZmbOVuUV3h3unp9PDf3+/85+Tn82/97+7u5dzWz9Pa
+4+nf2NTee+jw7m9HLig2OjQ6Ss+0sL3Cub3aWU1PXvd5ednKxMLHz8/Jy9ba5e7e0trhzcPK
+Oh4dLCwnLUC/p6Svsquvv3E3LTU8NjtaxbOtsby/xc/xTkJH/8vGxr+6tra/0D4fGR4lIic7
+wqadoamoqr1LLiMjLDU+37arpKaz01hIPjk5P3S+ubu5uLe2vd9MPS4kHh8mLjzYsKehoaWs
+u2U0KSUmKzd1uKyrrLHFXz83Nj1R58e6tLG2vMLL1N1oRTs4KB8lLTRJy7mso6SqschCMCwo
+KDFIzbKssLq7w11CPj9cy8TFwL7Av8PaWnnU9llOPC0lJSovOVHIr6Wip669Zz4yKycrOV/B
+ta+usr3M90pIV2No7NHJwsPN1M/Kys3oVF1NKx8hKi86X8WuoZ+mrb5VOzMpIyg2WL6ura2u
+uNJcQTpCWmfyysPDvsDO29XU0tRcRUg6KCIoLjhkxLmro6SqteA9NS8oKC8/3Laurq+zvuVh
+UkJHVVdm39bW0NPu4MzJycXGzt9LLCAkKi45U9i2pqKor7vtSz0tJikyQ9O6trGsrLbE1l1E
+QD08RVlm78/KxLq3ubrD5Es7LCAfKC43WsW2qaGlrLPJTDwzKikvOUvKubaxsLi/xNdNRERA
+S19cVfrNw7u3tra4xltDMSMfJCouP+a/raSjp6u330s5LCcpKzJQyby0ra2xs7vnTktCP0ZD
+QlPfzL61sbGxutRbOSUeHyMkLUXfs6OfoaKpu99KLyYmJyk3a8y5raqrrbG91FxDPDs5NztJ
+bsm5s7Cur7XBbjUkHR0gJS4+8bWmoKChqLXF9jsrKCYnLj5cy7auqqapr7rNV0A6Mi0uNUXb
+vbOtqquuuuI9Kh8dICMoMkXWsKWioaOpsLrVPS0nJCQqMj/kuKulpKeqr77mQzMsKy0zP/vA
+tK2srK+4yUYpHh8gIScsNGyyp6KenqOmq8NBLSQfHyQpNWW8raSfoqarufZDMikoKy47bsm8
+r6uqq7DKPiojIyIhIyk14bCnoJ6en6StyzwqIR8fISgzW7qqoqCipqy20kUyKyosMTtM48Cy
+q6mqrbphLyYiHx0eIitJuamhnZydnqa5Ui8kHh4fIy1Az7KopKKhpq242UM1LyspLjZI1r2w
+qqiqtOwxKCYfHR4iK1W1qqKenp6fqL5UNygiJCIkLT1mu6yopaKnrbDBTjkwKikuND7ivbKq
+qa21yjkoKiQeHyQqP72vqqCeoKCkstpNMCUlJCEnMT7vt6yooqOqrrXYSTwvKi41OUzNvbKr
+rLG43DIqLCUfIyYqQsO4rqShoZ+kschpNSkoJiIoMDhQwLGrpqWpq66870U1Li80NTpP0by1
+t73B/jw9OSknKy00T+zQtaqop6ars7vLRTIyListMTRC2MfFubKyr6+6yc7oUUlGQD9KXVdM
+UnjnelJJWPFVRVVxXFBq4Hhp2MnVbfva3eDf087b5dPP7GNgaWVaZeze3NrX1Njd6mhXVFNQ
+UFNg7drPy8jHzdjlZU9JRT89PkFDRktW7c/My8nKzNLY52tp/Hn72c7JxMLExsrP3WRRTEhG
+R0tPWXXe0MnKztLdblFHPz09PkBESVX52M/OzcrM0dvqaWb4+vDf1MzHxcXFxszT6l5YUUxJ
+R0dLVV924NLNzM3P2epuV0tFQkJFSEtSX3Dr29XW3ObueXn26eDZ0M7NzM3Ny8/f92tbWFRR
+UFZkcu/h3dnV1trh+WhfXVpZXGRnaG5tdG9oXltZT05PTk9WafXm3NLPzMzP0djf6flnXmlv
+e+bW0M3JzdDP3v5xWk1KSkE7astKXcDL1cXW5stsRExGPEBEQEhXaObd2s/EwsrQ3N/hb1le
+YWna2+3Zzs7P2enh33BaVFZebF9+3M/NzszNztHuV1A/Ojw6NTc8P0/46cu8vcC8vMHBx9/p
+7ltMSkZCS0xIVfPa1dHMxb/Ays/KzNx5bXByaVpf7N/felhIQUk9MjQ5PD5FSPK+vb63sbG1
+vcvT3FdAPT09QUBBWenYz8i+ubq+wcfO4VpMS01KT2jl0M7V2epKQUA2LzAyNj9NYMm5trGu
+r7S4w9p4VkE6OTg6P0VP+dXEvby6uLvAx9h9X1BLSU5ZYfTa0M3Q3FZDST0vLzMzOEFH7L68
+u7Ovs7S7zNbbUz9APz5GSEhd393Rw768vMDHyc3mYFdTUU9OU2fe3Pbm2WBIRj86NzY3PUVJ
+as3Dvbi3ubq+x87Y/V5US0lPT01OV/za3OHTztTa2NXV2+r9+PV+fnHc1Njf3dv4UUtLQTw5
+OTxDRk1819bNxMXEw8fLztPe5e5vdH5saf728/5sfOzn8fvu7Ojo93H97+/v8eHa5eLf3+Zy
+VE9OSEA+P0FHS01Vb93R0s3IxMXJyMnMztXe4+dwbWVhXmFaV19pZGZtbG/09e7h5PTr2d/d
+3ef37/tmZ15UVVpWTUxOT1FSUldmbnjm3NfQzs/QzczP1dPX197sdmtmaF9cXmJlZ2Zs/vD6
+/u3s/fLv9/rqfm7z/Xz4emVjalxTT05NTk1MV2Zr+t7c3dfT2djOzs/Q1NPY4+np731tX2Rm
+Z2hta/vm+H3l7/xqb1tjcWlzb/n78+9sbnJoY1xWTlRRTk5SXGd28+/d0s/T087Oz8/X3N/q
+829veuzm4uXg6fX6b2NgamVeYHdyefN0dH5xfXNtXFdbWVlWWl5cXGlnZW7/7+ji3eHc3t7d
+3N7c29/k4N7g6eXk4en85ebm7vJxaV1dZFtfaV9fXVlcXF5eZ2xrbm5laWZqZ19p+Ovg7+bX
+19ba1tze3d7h7X7s5n52bOzt9vfsfnJqbWJpamplYWxsamNfaW1nbGhvYWllbGppcPzu9fl8
+fuvp597j5tfc5dzf3+Xs7u758315eG5u+2l6dHn8dG1qbWFaXVheZHh9e/d35/Xu9HZvev5v
+ZWx8/vH78eji5Ozr5eTr4fLre/lzeu317nf7/vTw/nz+Z25uZWFjaHJq73Ru/f5vc/93/e78
+bGl0dHFnau328fz87u/v6+bs8Ojz8Pp99ezteO37eX9t+W995uvqbHj3939ka2ptZ2JydnFq
+ZmZiZmxoaW7t6Prt8uPo5+Hm4eDb4+3n7fX4ef5t73x3fe9vdfz/aG5oefZrYVtbXVxVVl5u
+7Ovk39ra5efh3Nnoeu/v+f39c2xubHJ97nzx5eno5Oft6ed8cv/tc2RqZVlPTUxNTk1UX23f
+0MvKyMnNz9vz+vxlXFtVUFNZXmP75d3Y19bb3eH093lvb/729ujb3eDg2dfnW09TTkQ+PkRL
+UFpz2MvHycnJzdTpbWFiXlNSWlxgfuPa0s3P0M7P09nm8e9rW1pdaHbw5NvZ3OVqXVVNRz8/
+QEVITVv+3tTPzsnHyMvQ1d7o+mtfX25xaXr6+/P39/Pl39/j7uro4+bl3+De3dzd2drj+mNY
+TUY/P0A/QUVKVWTt3tTOzMvN09rZ2trZ3+jn4uz07Ork5ePe393e3+bp8O/r/Pjv7+vs/Gxn
+XU5NS0A+QkJCSE9Uad/YzsrNzsvP19Xc6ung3+Dg39zm5dzc3t/d6O5+Y2BoaGdubH3u6N/f
+5envXE1MRj8/QkFGU1321tLRzMrMztTc3unx8Ptuce358uPj39fZ4+Db5vH6c2t67ff5d3X1
+6ODf5elyUExQRT0+RENIV1tk4dXQyMfLycvY3N5+anZycP3z8uji5NjV2drc3+jv+nJnZGht
+dW917uHm7+jvW05SS0BAR0dHUV1t49PPzcrM0NLZ5O/6bGJz9PD68eTg3eDe3HZe3uFr6tHN
+0tTUzMvS2dr9W0g0OUAyMzlETXbs1be7w768xND3SUtXQD1OX2Nz3NHBwM/HwcbR2/d1dU5N
+XWFq6+PSx9bqzsrmXVNKODI8NzM8RVjqzsm5tcHAvct1WExITkhEXuPj1MbBwb/JzcvuW1lT
+U09MUnh+/tjRy8nVz8XXb2BTRTIzOzMzPk5f18m+sba/vb7XU05KREdBSnPyfdLCxcTHzczS
+dV5vWk1UZvPs+NnIydPRzM3qdvNURTc2PjIvPE9PYMrAubzGv7zPV1dWRUJFT/Tv9c2+w8zG
+xsvbfXf+YU1TZGBddNTO0NTRzMva5+JeRDg4QTMuPE5LTtnCur3Eu7jKZXJtS0FDTF5ZVuPK
+0NDGyMnN2drZdVJfaFtm9NnOzszGxc3P2VtOSjUyQjQtPklDU9rNvLnFvbjPW/hsRERMTF9x
+Yt3Gycu/vsXKzdrz/VleZVVXae3/5NHS0dLc6WhYTDw7PjU0PUNCTt/OwL7BvLvC1tTVW1JS
+UVlbZO3Y1NPOz8/V2drg5m9iZV9eV1xmZPXj3N7a2OXw+mVSTExHQkFHTU1Y9uPf2NTY29Xf
+9efo+u3h49/f3N7f4ufg4uPh4vL67O7p39ve5drc5t/c4ev/al9NRERDPz0/Q0dOZXVs5NPU
+1c3P29bV3eDY1tvV097d3OTk4uLz8u349/v+e/f9/uLk+fb28e3v+3NoUEpLRz4+RkZDVu3n
+4s7L1tDM1eDd3/zr4ffx3d/n393d3t/f3+jq6O96eHd2/e/o5+Pm6errf25sXk9ISEc/PkRM
+Tlfz5N3PzNDV09nb29/g6uvq8eze5e/h29/j3N308uTqfX7w+XV2fvvq5d/l7X5iX1lMSEdE
+QURJS09o7OPUy8rLzM/d5d/l9vPxen7p9Xrn2t7k39zc3d3s/XRye21oa/nn3t3e5OjuaVpQ
+SkZDQkJDSVFfavrb0s/Pz9DU2dzl+fns7v766uXf3N/i3Nvb19rf5fF4cHV4cHNwcffl5fhu
+XkxZ+0M8RkhIVVJSdu/dzdDY09HR0972f3386fP06Ojp4tza0tLb4eLl6+z3a21va296fnzr
+6+z5/ntaUFJNQUFHR0dPYm7s3drb2M/Q2Nnb4Ofh4uzo3Nff4tvc4N3a3ebh3O53/Whdandp
+ZnFpYGRoWlldWlNTVVJRWl9cY3H7fvvq4eDh397d29rd3Nve4eTl7uji4eh67uzv83huZWBk
+X2BmZ3R8dXP5/3X9+Pj8/Hpub2tiaGZeafn9eHZ8b21sbWpu/vx9cHvr5OPl4d3c2d7g6O3c
+7W3q7WlxeHDv/WtsfnRqZWZtY2NqbWtpaG1+eHL76uPu9v92e3VzbXZ/+Ox6b/7w9vbs6ez7
++nxx9vL2eXBxeOrp7O/r9vzy/HV89/5zbHX38O74ff37fGtiYWl0eWhlbmZgYGl1ePTs6vHs
+3uL68eXq6Ol+cHV8e2p57+rj6/7v7Pp6bnF8+3Ztbmpx/n1wdPf2/Pp9dHl5bnF4bm9za2dh
+VX/P4W5j/d7gfGB23+5ub2307+307+9e5NLd4n365tjua3t7e2xmXl9qZ19ZYvT5ZV5nenfr
+53Vvae7gbU9V4NXb7W/u2trn7/bv59fXeE9V6t3sZ2V26+v1d2p39HZnYmZhX2Zja+7r7uft
+8PDtcmty7+Xs6+7x8en1d21o/vR4aG55bmtrc/vf3ev+bmh46/Rv+/L4fn5+cfbp9+bk7Phn
+aHD7aW5oY2lv+nf7a3n37XBm++fe7/P773liZPPse3du9OXk3eLi5+jc4/plW1VdXmJdWGH1
+29jW4Pj04+NjW1pya2lqa2hk/nLy+37o3+rr5eLm5epye9/c+WdZXnftc2h5+efe3eh3bmNd
+YGRdWV1kfNzX7mZvfOrhcF9m/+3a2uB1bfvs2997anh48/n7ZmdrYGB9f/Dj+G167Gxnd/Rw
+du7kcl5s7/bu9X15bn5qbnr+++30eOXzYWre3PVoXmvp4e7sd2f96fpjaGtuX1z82tjo/Xvq
+5O3k3m9XWmZpZllUcNjY5m9u6dfkbmFZW3bk9mpxePrf3NvncWv85N3ef11bYPbtcWxfWvvZ
+3/l1Z/rqbXToe1tq+vptfujzeHNy7unvcWBt8uro+W999fxtYWHw3uttbezo7uXr/n7/dWd8
+dG5obfbt5vluZWF44uJvaGtodOvd4+Tn/f72+255dHB59nh27eny7fVwbXR4a15cWlts7v1u
+bm1+6+rs4extbPDp+nV+7uni3N3d4+zr6uf0b29uZ2VwfG5ramtsaGNra1xbW1pXWGF76fd9
+493Y1tPS09Xa4OHc2tvb3M/JztbUy85oPjAqKCwxNjxXvq2qrK2usr7hRjQtLTA4QmLJurKv
+sbe/zeRURkZJT17nyr24u8DQQicfJS8uKTHHqKWopqSrvm9ALSMjLDk9R8SrqK61trnKXkg/
+PERy2t/MubG2vsTVTi0eHCUvLy9ZrKCgo6WqvFE6LCEeJjpk0rutp6mwv+tOR0A7O0zWw8XG
+v7u7ws7X3WpKOioeIDdcPjrAop6osrjAWTovJyEqVMbRyLGqr8TtW09TXlNIYsC4w9rb1dTU
+0dv13s3VVjYkHitNPzFQqp+msLa+/kI2KSQtcsHMwa+rsMDhTUhRW0lCXMW+zebc2d7b1s/O
+zcrUYjogGy3sQy5ppJ6lrrrbS0IzJCIyy73Ct66uuNBHOUBdbVRixba3yHBkfmZaad3PycHK
++FAxHh88VjM4tqOlrrvXYVc/LCg22b3EvrSvttJLPkhzfGBl0b68zlZQa/Xs7NnFv8zY1l08
+Kx4gPFg4Ra+hpKy661VSOSkpPdK/wbqyr7frPj1LUElP5cO8vsvc6OR3WmrNwM3TyNVYQS0c
+Hz9NN0mvpaarvF5qazkpLD/hwL6+ubK4Xz5MaFlX5M/Fv8vtdGpbV/vWyr/Bz87TVj8wHx43
+aDw/uailp7T2dNlEKyo1R+HMzMK1ssZ259pmUVZb3s7V5N3PzdHY0svL121d8HRINyUhOGQ3
+NOmzrKu2zsO3zDkvMjpJV0xouK60vsPAye9OP0NXWVPvy8O+wMjLz/JdT05NQ0E1KC5SSDRG
+0L6wrre7r6/LSTswLzg3M0vBt7Svr7a8xlY8Ojs8P07lwbq8vbq+0OLnWEI+MyovQjYuPlzn
+ubG6taurtcZmPDc4LyszR1fNtrKyra69zthdSEhGREpRUFNVUVlsdvrVyMzR0dd3UkY8O0FK
+S1F30MTH22vr2ONpau3WysnQ1s/P3vtqaW/t7m5vZ1pWTUtSZvTczMfJys3iWk1GQEBERkpV
+aOfW2nBSXeva1NfWzMTK7mhiYHD5am3m087W6PH2b21kYvnh2tTP0Nnkc1dQTEFASEpLTUpN
+XGVZV3bYysG+vsHFzNv4Xk1HRUhUYfnazMjKz9zf3tvY2ODl09He8O9xWFpRRDoxLzY9PURc
+17uuq62us73WSzQrKy41QljavLCtr7a9yOVRQTs9R1V53M3Bu73HztDZ9k86MCgnLjg5R8y4
+q6SmrbO+eD0vKCUpMkzcvrOsqau0x+pNPzk4O0Zt0srEvLu9wMnU6WJFNC4pJSw8Q1XEt62n
+qK+6zU45MS0rLj3txrixsbGzvt1aRT4+QUlW3se/v8HBwMzm72hPSD0zKycsPEdW0cK4rKmt
+tcPwTD84MTA1PFXPvrm1tLW7w9BcR0JCRk9g+tfKv7y/x9Hb6WBIOi8pKC42PE300LywrKyt
+sr3N71E/NzAvMjpHcsq9uLWxsLS8z15NR0JDQ0ZOaOba1tna3fFeU01KSUpJR0hMU1dm6NPO
+zs7OzMzR2vNhX2386+Hf29PO0NzteGhiaWZYV15fYn15cuzY1tzd5u7vcV5WVFhaWVtdXltS
+T1VdbPVxYm/u3dbW3vHe08/O0tjf3NnW1trd4uxvaF9ZW1teXlpibG98f29pfXhlZGlcUlBQ
+Vlxkbm1qcuzb1dTQ1dnc3eL8/3764drb1tbX2uHyfWxhW1dWWF5mXl5q/uvf2Nvf5vtmXlxd
+WVBOTE5YX3zj3NnZ2+Pp5ejt8X388+7p4NjU1NXV2uDp/2thXmReYGpv7eXh4OT7a2hjXFlY
+U09PUVVaW15dZW7t3tvg7ePj59/h3dfa29nU09jZ6HJucW9oaXF/8ezt73xtX1xbXWp2/Ht6
+9vd7cWxnXllUUlNVWV5dX2dw+erb1c/Lzc/X4e54ZF1gbfjq5trUz83O2ev/aGJjZ2ZkY2Nr
+en5vZWBeW1xbWldVUEtLTk9aaP3f1tLQzs/S1dzl6u3t7uLf39nV1drd4fN0eW5pZ2NdX2Rq
+bHR0bHr18+frb2VcWlVWU09PVFZacPjw6+jq9+3o7fjs5d3b3NzZ1tTS1Nje5O52amNhaH34
+bXr3f+ztemtsb2tvenFnXFpZXWBeX1tZVlRYXGFjav7w8Orf3t3Y0M7NzdHX3eDi6Ozq7+zv
+fPx6bWlnZF5eX2hjY2Fna2dw8eTz/PHydG1uXVRUV1teY2FibH7o3tze3t3h5ujn5+Le3Nzb
+3N/e3+Ts6PJ1bWlkXFxjYV9oaW599+zt+X50bmlob3hyampxb2VjXVdVWmNtZHbl5Obi3d/j
+5OXj3tzg397f3dvd4eXm6npnZGVpamdpa2llbG1mbX15fO3s/WhYWmJgYmJkY15cZGj992lp
+fff47Ofaz83P0dDX2+bq83Rxb21kYGdsdPfs4+nr5d7Z29zfd11cXGNyd2tWS0A5NzY4QVfd
+xb25uLq7vMDM6FlHPjs6PUVb3Me/u7i5vL/H2PhcTUdGSE1Z/djNycvN22pLOC8rKSsvO2LF
+t66sra+yusXcUj44MzAyOkjqw7iwr7C1vMncZk1CPTs7P0dZ3Mi/vLy+xM3V3V0/MywnJics
+OWi8rainqKuyvdNRPjQuLC0yPWPHtq6sra+4xNpeST47Ojs+SFzbxLu3t7u+yulOPDErJiUm
+LDdYwK+qp6eqrrfH/UU1LisrLDNH2bqtqqmrsbvNaEk+Ojg2OD5Jas29ubi4vMLL6E48Lyon
+JikvP964raimp6uvvNNZPTEsKSksNUjQtq2rq66zu8xvTD87OTg6PkvjxLu0sra6wM9sQzMr
+JiMlKjRRxbKqp6eprbS+1005LyooKi46YsKzraurrbS9zWdJPjk2NjpBY9TCu7q6u77J2U48
+MyooKiszSO2/sq+urrK4vMrsVD83MzI0PU/jxLu3tbW3u8PV/k9DPz0+RE1XbN7OysbIztfm
+b1hNS0tKS09TUU9NT1NWXm/z5N/a3NzV2tfOzszJy9DW4O7/b3B2e/7z/m5oa2NjbHzu5eXo
+5u9xXFJST05UXmRob/NxbO33bm5taG5tYmtu/d/p4tva2dvc4eTl4N/o5+p2bnpmYHv+dOzr
+8O9zdvj1+3Fvb3dx+3p1c3BiYl9bXV5mbHf87fX4/HFycfvxdXx3dP/s7eHW3OPe3ux8e29t
+Z25vevnw+u3q/ejo9fj4fmtuaWhraWZu9+no5OLo4ef3bWJaV1VWVlhfaHvn5/Xr4Of79fdt
+cnX49Ofh39jX2tjb5d/b3uvu/GhlXFpbXVtfe3t8+ubwen1pXltZVlRXU1Zp+eDa1dXU1dXZ
+6vN+fG9rbWhmanv+5ODq2t735/L+eHf6dWNoaGdpdO716OHi63poYl9ZVltdXWRrbXjl5Nzb
+7u30feztdvrx9+7z+/LpfvbmevTmeG5+aGR1b/nl6fT083z1fHlka3JuaGh8Z2bu9HTq3d/r
+9P5nbm5iaWZ873pz/+ru5O5s4uVhXmhve3T++P79+Ovx6ujf3+zr+25vdG1ndG5+7vfs6ens
+7XdjXlpfY2NiYGdub2/57Orq6erz9W9s+u7o3dzo6ePg3+ft73/zdW5pZWNlbW5qZ2lnZ216
++35+bGdhXWt3evzt7+7s5N/f3eDg5fX8+Xt4//x9/Pf6dnlucXfyeG1ybF9U3+tg/ePr9e36
+4edrb/lzbV9jb2Boanh1e+7r5e3v4uP17u98eXN6b3h+dXX/7+vi5P3r7nt5dHJqa3Tv7Wxt
+8uz78u5saWplZGZrePltcH59+Px3+O/x7u59d/v9e3T65ero5Onr6uzt+Pf2fHtucXV8b2dw
+bW7+8v5pbmRodnD87/H8/err83tw+vry7fZ8fvb5ee79/e3/em51aGp2YGBtfO7s5+ru7/np
++vzu7vjy9HTtfXl5dHr89/f/bGphYWFq/nFu+vL+8+96d+3x/X76fHbx//v39n35/Xl7cPz3
+dv/u7vP89Ov193Zudn11a3Budn7wa2RscP74f/nz7+t8aW5+d/F3Z3N28nj19Xjz+/l2enhq
+eXju4OXqdnl7fPHv6nt+83Z77fLy+Pf5bm1ye2psbmV0a3h0aWp29/f58HxvcXV5efLu597h
+6enp73tl5Nly9O13ZGtjduxybXhtZ2dlZ2lqdfvy8+jl6Ozf4e/wauXsbHn59nzfemFkXVVt
+cmD2++/b0/v65m1qXmheWmh6+NzU09PU7f3e4/1kXmFYV1NffOzj5ef+6ej3fGtlZVtdX2Rl
+Y3h8+/Te3O7q6Obi5+bu6Od17+zw/Pnr/ufk7m5u+W1neP94/Gpofe17ZWViXV1aWlxla2Rf
+aHFu8err3tzc2Nfa3tzh6uPh4u5zeP7t+3z36vJvdvDy/fd5ZW53ZmRaTkRHTktJS1Nh6drQ
+ycXGycrN0NXldmJWTU5QVVlk7NzUz9DMy9Xi4N/3amlpZV5jaWtv9/J0WUpITUxDP0NKVWXz
+3M/Jw7+/wsrQ2ehuXlxfX15fdd/X2NjX1dbe7vl5aV9eXVdWX2ly/PD5ZlhOTElGR0pOV2X8
+38/LycXDwsnP1+XseF1RTU5RV2Ny89/XzcrKzdHX5GlebW5bWWBucHL74d9mUEtKSklFQ0ZL
+VFz918zIxsTBwcfP2ul5ZltVT05OV2T95NrT1NHNz9fc5vptcmpfYWhz8eDf4uVpT0hGR0RA
+P0BHUl/mz8nEwcC/wMbR5fRtWVJQTk5SY/fj3dzXzsvNz9rv/G5eV1RXXGXz4N3a19vkZk9L
+SkdBPj9ETFr32MrExcTExMbO3e52ZldOTExLUmbr2tLOysXGyMzW53VkW1dWV1pdcufd3fVa
+SUE/QUI/PD1JYNzJwb27uru7vsndZFhPS0dEQ0RJVunSzMjHxcPFy9TiaVROTU1OUl3r1srE
+z2dCPEBGPTQzPFLmzsS8t7a3trnC5lVNS0M9Ojs/RFHhx8PAvbm3vMnfe1xNSElKSk9s08nH
+zNbqVT85OTo4NjdF4svGvreys7i9xN1VRD49Ojg8S1xx3sm8t7i6vL7K5V5PRT4+RE5YYd3G
+vr28wnw+Nzk6MyssOWXTy76yra2usrvUUEQ/OS8tMj9NUGrFtLCytra6xtxqTD47PkZOVvHL
+vru6usZzQTQvLi0rLTdTyLy3rqqqrLS+3Uc4Mi4sLTM/Yc+/tK6srLC7x9lYPjU1OTxCUOrJ
+v7q3t7i9xNc/KictLyomLWS5uLetp6aqssJSOTUxKyksPO3Kwreuq6yvu9RrWkU2LzE9UWLn
+xrq2tLS5xM/vPS0nJSkuLzBHxLCrqqqoqrDFSzQuKyssLj3bvrevq6qsssVaRDw2MDA3RFzg
+xr28u7m8w9f4X01NVVhBKyg9WzkxQci0tbSwtry5wk80MDo/Oj5Yz7mvr7a7vMZvTEE7PEVJ
+SE7dwb/BwMbHv8hZTWFHNDQ0LSw3T15sxa+srrS4wdtcQTMuMTtGWdC5sK6wtr7PXUI7NzQ7
+TF/nyb++vsLFzW5QT09NT1/u18nEzkcsLD09MThNzrSrqrC8v8dfPC4rLz5ozsS7sq+wutZU
+QDo5OkBP9ce4t77EydDeYE1OTklV7WhFQTwvMUNKSPDGvLCts7/R900+ODExP+bFvLWztbi/
+7kE7OjxCS1zgyb++xNJ6X2xlTUdQa9rNy8rGw8vbcFA/LSQqNz/nvb+6ra2zu/84MDY7P1H9
+2rqur7S/ZT8+QEBDS0xbxre6wtJubuLqZVth59TPz8/NfjwxKCYuPErWurWuqqy2yE40LS4x
+Ok/cv7Gqqqy240E4NDQ2O0jtv7e4vs15TUI/P0ZX4s/Hwb/By91wVE5QT1BZa1w/Oz08SvLa
+0MjKzMXGz9PkWVFfff3c1+/p2N7z7nBhWFJg6M/Iyc/Z39nc6mJSUFZsZWR3/+HoTTs0MTVB
+ccm8uLm9wcnXZUg9Oz5Ke8m8uLe8yOlaSkFAQ0lg1sbDx8rZZ1ROTFBj+Oze19HLxsvV315P
+T1JJODIvLjlYxrSusrm/0m1dTUE+PkRXzbu0tLrI+E9CPT1CVPfay8bGydDqaltTVVhe8dva
+2djY29nb9GVSST0yLi4xPnjAtK6vtb7VVklEPz9GU+PGvLi2uMLiTT07Pkdd3c/Ly8/b63hc
+T0xOWfvWzMnM1N/2bV5XTktOTko/Ojc2PVHOurGwtr7OcllTUlRZXF133MzDwMLP+FdJRkdQ
+ceDb2eHw93Nsdfrs5t3a3N/k7np1b2hcVFNbY1lLPTc0Nz9Zybiwr7W/1WxRTEtNVGB+4dDF
+vry+yOVWRT4/RE/329LR1Nvf7G5wbmFpZlxmfX578PF0eHpvYUc5NjAyPVfItKysrrXG9kxA
+P0BGSlj4z765ubzF7ks/PDs/SFry29XP09ja3uL8a2phanz06d7Z3t7ifF9UUUxBPDk2OUNi
+ybixsbS90G1NR0VMVmnh2c/IxcPGz2tNRD4/R1rr0szLzt75Y1lZWVpt7dzPzM3V3/dxaWRl
+aVE7NjEuN0bdvbCvsLXE4VpGPz5AQk7rzLu1tbnA2kw/OztDTnbaz83Lzdv0ZFpYVltjd+bf
+2t/i5nxpYmBfZGFTRD06NjxJesa3s7a6yN58VU5SUlRf69vLwb/Dyt1aTEZHT1x76+Ho7u3y
++2RbYGVv/unn5OP5e2RZX2hucG9eT0I7PT9O58m9u7u/w87peVlNS1Fj7s/JxcXM225bVVFZ
+WVdaX259597g6vz9dG1sb2leW1xeXWBcTk1YVWji39zb5Xf57eLR0tnZ625ubm/r3NnT0tPW
+3vBtamBdYWBmbnduamxseXtval5bWlZYY/Tp5/FfVVZYafDe2drg6O7x9H1ubnv04tra19La
+3eD7aGloZ33l4uLucHBsYltdXFxhZnnz7/F8d21nZmRmbHfw8/rs7HpvcWxx++3n6Obg5e3+
+enP88vbs7e99cXdtZWl0++vm3+Hu8fR1ZWJlXlxibHPs39/h6vlrYV1cX3nw9Oz18fFyamZn
+b37m6e3g29/t+m9ud3h1bG11+fj99eri4+7x83/z9PtuXltbX2Z09/Xu6+3o7fp1dWtxfnz7
+++/u7XVudm91bnRqdOns7397a2pvdf3v6ern7X759PPs6Onn5uru9fdtYmBeXmRve/Hp5OHr
+/H1sY2RlaW1v/u7p4+r7a2JkaGpu9eXg6O3r93x9dnFyfe7z+fh7c//v8f7+6u7y5/P19Pt0
+bHr+c/ns7vx3fGtmYWRramZrcG5wd3ZveHd99PDn5OPi5en0+nJ09vjn3d/l5Onv7npqamlr
+cHBsbGFhX19jZWlrbXL27PP++/Dx8ens5+Xo6ezs7ers7fP0fGpqbWx69H5sZmlmZmptd/f8
+cW52ef3y7+3u6/N7/O3p7/z7dHvu7e3u+PT3b2poZGv783t3+PD7dXh4fHp4bmt3/n3+/ezn
+6/d6cXP+bGp3cHF5f3x1+u9xanX59+/t6ur69PPz5+Hi397reHFtbGlnanZ9bmlmYmdnbW1n
+/vD1+PN9een6dHv+7Ovu5uTr+nJ09f1ufO3v93xsa3f+f/948Pd4+nxzdm13end5eHB8fvzu
+6u/3dnd3amd58fz28O3k7Hp9+3n79HTx6PJ3cm9wbm138fdtcm1udGVq/Obr8erv8fJ77u33
+9vVxevLz/fz+cHF2fntyc3h4d3l2b3h9/vr0fu3h7Ojq9vzn7WRof2x7+2dnb3Bzb2dpemhr
+7nvp5+zv9Oj67O/k3u90bf7v7XVu8vZ6b3L3fHxucPduYVxfd21s/vbu//n3+PHv5+Pl6ex7
+/O95aHv+8/pu+et3Y2Zic+9yc/X8/vh5bm14/O/p6/lmavbw9vTx5/hmZ213+Ov5/Pn96HBu
+e3vu6+n/d27++G/6ePF7bfVvdOrtbG95fP9z93z7/W/7cGzn5O7t7+93eWtfevt1b315/PFu
+9/p1933w6fD/9+ny+2xr8nX66O31d/Hn5/J+6ux3ZGF1bFxbbPt4bn3g5PRuZXTx7vP9+/Zw
+fPJqZnrue3J8+O3q7ezp8Xl87f109ud1c3hy+nR+8X5vbnxuZ3JpYnV7ePbvfe/j7Pv5+P93
+9n1s9nL8+2fx4XtscW5u9en7evPu+Xp1ZnPs725u++nneX3z/nhv93Vu7fVtbGto8Xdg//Vu
+8OHrfPvr4uh89ndlfelpZfrr8v15+ezf+Gt2Zf3uaWRwZ/LmbV9u6eT3amh2dm3+8e7s7+j+
+dun1bGxvZ3vvbvrz++fh5upucO/r8Xpv831q+nJcZ/v2/21t8ubo73N96OpycnF1Zl/xeXT7
+dfP9ZmVq7+RfZOnr6erw6eB9dd/1ZPtycvb+/Pdz9+ZxaefmZm38+OJ1XWr0emhqdnB+925u
+ZGRueOnw8Onr6+bx6en39XVrfnxtfnB56ehya3Z2/XB+7fDnfG79aG7yb2r27PX1b273b3h3
+ZHB+d/p5Znn5+vp4cHjn7f7i5P5+a3ru7u/rfn3tfXR0eXV58nlyamvv9mty8u72amFv+flf
+YO//dnl67O989evy9/vu4evv5+zxfm9y+u9tamtvfP1+a2Fn8eplaPJv/u516+Zra+vz7N7w
+a2xueevveHp5cOrwWmLx6er5aP7g+GViePl/dXZ6dvT3+uzd7nt9cGlv8e3+enV07/1odHR5
+bWBz5m9f+t3o9fl8+/Px7u/w/ennf3P1dv7ua19/c279/vPvc2/6a2FhamxoamL33X1w8Pfl
+3vJ45uN45N52+N3fc2x47exrXVz9/V1eaGf5a2Nt831y9mxze+nd733z797ffP/m6+98a+Xq
+eW50///7/Hn1dGFtb2Vjfn59eGRo4exvfu7j7mp83elvcPt9X2B5dHRs+dz29d/0++5ya/Vm
+X/t+bXPz9fb28Ozr73/65u3873Zs+3xtev14fnZqa213a3Dd9mj14+vo6XHz73teXnP8Z15v
++Oz6cel+aP3ubm59duvh+Pjg7P3z6Ob/++94ZvTt+efuZHT3bP3+Xmf6Z2pja+/y6vl7a3B9
+7/9o/3ptb3L9fW/35On49e3q7/Tq7u3i9P3n7P3+8fhv7Hlh7HFY/XRcXWVmaV9cdfX+Y27u
++Pru4ODj3d7t9efd2+Lv7/n0eWBtbGBpY2XvY1luZ2BmcP7s/md43eH43/hj6+l76/Fx6eht
+e/1q7N5vaG/v6vFja9vha2/0d/Loe2j99njmbV5vdWpoXFho+3pqb3j8fGlw3uh83uZt7+3+
+2t115tfi5Op87d3vW1l6eVxYY/hsZWxkXvN/cG5oaW3reGT57vXg5PL35eR6duzq8Xl/8PDk
+7PPkeGzn521cX3/rZFh+3Xtid+7ecF7/+11c+mxo7/P773t+6tvf729cc/H49P557t7kePfs
+53Jl/nxx92pm8u708m5kdun3WGfc5mJr5uL3bfDjaldt6PBqYvXc6Gr843r07W3s7m76cmt2
+cnD15nP+7Hx6b2r0fmRlbe/kdmN3dG9rae7j+3bs6+z87tzi4Orp5e5ub/H1eWdncHlmamlo
++nxsaWJq9u/27uhydO38b393dvxwb+zn82xf9+JfWe3p8ubo3dv/euXr/m529+1jWnV0b3Fo
+9d53/eVvZur7Yn10Ze57XWZ7+vxyeuDld/ru821t93hpfvPv5Wxn5O7r7+/e+WT/8XJw7u74
+fW52+2hm92dv925zbF1v63Z6693f/v/veX1xZm5rdvF7env07n37d+fmZnvq+v5mY+3sb2fw
+3O7/9/bp4ntx5PtfafhtaG1dbuZzbu9+/el2ae3vb+t8auLxaH3q7nBjZWxvfnZ57Op2eOrm
+7vx16e5nefN06uRycOJ9Xv3zZ2H+5f9lbu3d433s1+Jren1qbGBcaXdgad7rbWz273Z2ZW7v
+eG/x/ujodPXj5P15++/3cnvza29++nly8PduZPzwa3D37/P7bXze6X7k7WluaGZwaV9063xt
+9d/k+ujndfr6b/j7a3jueW/0fPryde19ZvP6aHD6fvrq7vX9+fxvbGthZfPpfnHk5vv0bGj8
+8vp1a3jt6/9zf3z6/W3z5+z58uju7fT4/GllaWZva2Z37t7e/Xrl8nXvfm52d19ke3FnbfTx
+fnNydPv7ePbw/u3z/+Tq+vDt7O3z+/fk7XJ0+vNsamtpa2txaGhsdXBt739p+Ot7c+vwbm7u
+7G/z6fb28PTz9nV5+nlr7uX77PH16+9vbfp8amVxcnx+/vtyenl4+/T/dnRx9fL5enf78Xv7
+5+/9eGpp8/lrbXn5ffrw/ObuaPnrdmjy6n7y7fd39H9+6/J1/fpjaHtva3vv7fj7cWpvc3t6
+c3H27H1sfv1w+fR+8/57fvjl6Ono9X7w/G/48Hz98GdieH1wa2tt/P9zdXxxdfX1fG5t9vdv
+9uzs6PJ97Ob8fe12aWxv+/r9fnJucm/17np99/L3eG9tb3h7+O95cvf2eWxmbXn66+zo4Ov+
+/vH1dfrp7X56b2ptd3rz82tqc3JpZGt2f21udnF9efno7/nk4u3z8v3u5+nn8f3+8fJubHB2
+bWhte/78cGV3+29x9/d8+vh3ffH19vp0bnduePtzc3zx9Xzy6urq8Pbt7PTy6e1ycv78cGRp
+a2tpbW9tenJwefn+/354/u3p9ffyfvjz/fb7/e7o6+fp+nRz/fh6bXN0aGRkaXj3/v73endz
+c3h3dXd6/vD4e/pxanr08vL19+rt//7u7PTs4fB8fXdudHN69PD2dnNtbnh8cnF1bm1vbGhk
+cHx2c3J98vd4c/Lt9u/37+zm6/zr5eXh6O/s6vT+fG5iYm1xbXFwbWhvfW5qZGFoeff2/nd0
+d3p8+PPs5+no5eTvcHH+/e3m4ej9+O/7dHr39Xh2dnJyd3VqYWJpb21pbW1wefj38PTx7PDy
+6+fr+/Lu8/bt6fr3em9++vz08nhucHt4dv18dnh9/X17/u/t7vjx+XNzdHFvfXtuZ2pwdu/r
++fr8fm53/Hd+7Pl37u/18vDq6Ovt7/JxanRxZ2lxcnh88/Dt6/h3fn93c3n48/52bm99enFy
+dm589H7/9f588PH+e3h6d2x08+7y9fl8d3vp6HVv+/d4bHLu82lkbXVqafbj6e7q7f/7b23q
+5Pd2fff8d3l8+ux5ZWVua2dqfvX9bW57dHj77u/u7O/w8PH3fXx+9OnteWpsfPH1e2lqcW50
+/f5oa/76fXbu8Ph4au3k8evsfnZ4b259fW1z+X9ye/v8/Pz76ePx+Pb3+/tvbHRxcG13fv/4
+7Ojt7eHm+3F5bGx+cmVjZWJjbXb36v587/R4bHr5f/z87ubn6vHs7392cnbr4+rp2dLY6ubl
+em9+YFteVlFOVlVHSnPsdeHR09LddOjdb2T77+jmbmvd3fZ0/+zsfHHq293k5dnV4Ojl7XJp
+cWFsZFpbVEg7OFjuTF3Gv8vXcOrRbE1q2vx3fHnazdneztTrbmfw2d7x4s/Q5/z26nxQXOxw
+695ILC9QSDlbvbi6xtnAwkg7U3xbWF7qyMne591qSURS5tLNysK/y/NfX15ZXfPa1NDPz9np
+Z1hfWD8sLEBTO0m/trnC49XA+T9N6O9lYnvHwdvh0/FkV0liz9zlycTN32NdblRMZNjPzM3L
+xsnpZkAuMC4rOVD3v7a+vrnRVVhPTF9kXtPGz83Jz95oTVLydvXPx8jL2WxiWlFTZmv41s/N
+zM/Nzf9QRTAjKEFCRcSurrHC9c7nPTlPYFVaa9C+xPlk1tdPRW7HyNnPvbvOV1bh9k5P38DG
++9PE3ElLPS4tKiw95NrHsLW8xdRVWUpBTt3m2cjCwcl+Udf4P0jezMnP28C/XUdeYlNOUvrK
+z9jIxczL0vDqUC4uMSYrXddvtq23usdTZt89OVzj/M/O076831hjSkhhWu69u8a/xndVRj1N
+++nPwb65vs/N9zw2LywpIzW+4FW0qLfFbU/jYjQ8y8r23tfFus5a6thZSVrYw8rdzMDPUk5d
+XFdW78bEzM7Ex/BaPzQ3NSQj3sRE26ytt8JN3MFHLkzcYFnqzri57HvO4EtFTPbM5urAvNRc
+V15iUUrhv8LJw8DE2E0+NzQyKCNdwUf3rq25ulllzk8uQ/VVTu7RvbnO2c3UXUtOa9P55cW7
+xtrg4+hYSW7J0OXZycxsRT4+PTQvKzbc7knMsbO8xuHFyUs7T3JTUGL60MfKysS/zvNq52FT
+Tmfd5nvXx8zX5NznalJW/HZTSlhnTzw5SFdFP03h1+nl0MfGyM3O0dd+W1Nbavnr3s/Kzt7q
+5+R+YmVv+vlsb/Hi5ePt5tbb5Ojj4+pfUUlKSkE8PERKR0lg2MzKxcPFytXyaVxYbfj+59fP
+1NjZ0NHjdu/n8fbq2NDP2/7r3ed9dvp0aFtLPDY7Qjk3RWtf+dnMwb3Ay8XL13xbTFdoV1x8
+2tna3MzFy9XX1drraHHu7P7u4N3wbuPS3V5dX0k1Ljc8NTVH383Av7ivr7zK12hHOjY3Q0tM
+b8W+wL27vsbTdl5TTEtXb/LXy8TEyM7MzPVbU0g/PDkvLj1EOUTRwLy3urSwvnhoX0I5NztI
+eG7bvre6wcfEymVFSFhOR1Pey8nLwbzH8fvT5U0/QUs9LCtLSzdByr27u7+0scVPe2VFOjo+
+W3JP4r67xcTHw8tdTmt1R0xq19DQzb28zNzV019FPkI7NzItOvhCRcK3wr27vrnLSEzsQzc8
+SFrmadC3ucbIwcHMX0/0805N8NHN0dHFws/x3XhEPD07NCwsP0c7Tr22ube3sbTOS2pdPzY7
+SWpwWM24usXHwsLdTlRqVUdOX2z04s7AwMrMys34RENGNCswOTQ6T+K/t7ezrbPG0PFNPzg1
+PkhIYsi+vr27vcTdXnhYQkRPTVVeas7F0NfDw87T1dh4QjY4PDQuN0/+58u5sLK8vr7NWkI9
+PT06Pl/X2s6+urvAzM/ZX0lITEpLTVfv1dPLwcPKzcvN60U+SjwxNj4+R1FtyLy+vba6xNXt
+XFJEPkJKS09s4czIy8rHytTj7eR1UVBfamFreu3h4OTb2u5hZHJSTE1PTUxRXO7s5tvV193c
+3vF1Z19lberk3dnb3Nze3uZ0alxXW15fZnjv39rX0dLU2+Pz/mZVTUxLSEhLVmnz5d/a0tPW
+2uj27vxmZ3X9/PLl3tzd3+Pzb2xwbG9zamrp4+/08PH19vru8XR8cGlmXmJkW1NWXV9eZX71
+eXvz5Nzh8+3l6ujs8eTc4+je2t3h397X1tva2v1WUk9LSkpKS0tNV2r88OHX1dbV1NXZ4u/3
+7WRQW21kafbo2NPU19nf9mxkZFhXXGZtdO/k2trk6OfzZ1tYU1RebnBvbnvv7fP+9nr/fnj7
+8erp8/f0/fPn5ejl5N/i6O90amNcWFlZWWBz8efj3+d9/nhvZVpgZl1eZ2t87e7u4dnU08/N
+0dTY3ODn9HNiWlVRUFFST1BSV1xcX21vePLi3dvV0dTb3eXw9P38bXF9bWpjY29+7eHf4+ff
+3d3e5uvxdWxqbWpmbnh4anl5aWNbTk1SUlFWWmBrfefe19jb293h397d3t/f4Nja3d/c2dnY
+2dvd53dZUk5JSEZGR0tPVl1u8+Pc2dTS09TY3+rq6PT/+XtqaGxmYWJv/XHy5N/c2tze2trm
+6/D57eji3d/sfnNfUE5NSEVHSElPXWT53NvY0s/Q0NHW2dfa5/huZmh09/Xv9vf47d/Y1dPR
+1dvh/WNWS0dFQ0VHSk5XYnLp2tbQzdHW1tnf4uPveG9rZmlv+XZsbHR//O3n3tjX19fa2tzf
+5OTo7m1bUk1LSkhEQ0RIUVxn89rTz87NztHS29rc5OLc4fb9enh4/HlpZWhqcfPp4t7m493e
+29zk9HBbUk5KRkZFR0xSXGlz7t3Uzs7Oz9TZ293m6e338vl2aGhsffz4+erd29rY3N/d2dXV
+2eP4dVxQTEhBPz8+P0ZMUVx94NTKycrJys/Pz9fa3efp5/R1ef5sZ218+ezm4+Tf29rd4uTi
+4eh8Y1lQTEdDQUJER0tPWm3s2tXQzsvMzdDR0trn8fb8fXNsa3zz+O3s5NrW0tTW2dnd4uDp
+7vV0XFJMRkA/P0BER0pPXvff18/OzcvJy87W19Tb4/Nyb25rZW13e3zu6efe2dna1tbX1dTX
+3eH6YVFLR0A8PD4/P0ZOWXfZz8zLysjJy83W3dnje3d7f312aWBi8OHs893OysvMzMvIx8zV
+5XJOPTczLy4vMjdCaM3BubKvrq+2v8zwSjw5NzQ1OkFY28u+s6+0t7m/215XVE9MWN/X1tfV
+1E0tLDktJCxJXnnArqinrbKuukg3OjMrLTZL997Dr6yytrW830xCQTw3P2zj1b+2tLrIz9lC
+LScqKyUpPN7JvKykpa2ztMZFMzEvKyw5XdrItKuttLS50k5FPzo5P09v1r62t7i4vtRlSTQq
+KCkoKjNK076zqqersLXBVjs1MS8vNkzbyruwr7S3uslcRUU+NzxRZXHLube7ubXA2OpJOTMp
+Jy4sKjrl2cOvq6qttLe+WDw9Ny4wO0pa3L2ytLm1t8j1ZU89O0RSUmLFvL++trjI4/ZYNysp
+KigmL0RS3LesqaqqqrDJaU45LSwwNTlF2726tq6uuL7B3khDQz47RG3g38W4vMK+xW9CNy0r
+KycsOD5Wva+tqaaprrfGZj0wLS0sLjlIbsa1rq6ur7TA2mRIOjk+QkhizL2+urrCy1s3MjMn
+JC81LkDAubWrp6mtsbbHSzs6LyouNjdDzMK/r62vr7K+zPBIPz48P0dQ78/NycvRTDpIOCov
+OjAzasvKuK6sra+wtsloUkAyMjczNEdkaMm5uLe0t73E0fZlUElMTU9g7GzfzVlATFAzL0A+
+MT3a3Pe/sri6tLO+z87iSDxDQjk+WU1L0cbOw7m+ycPCzdbW4l9ceVlNXltPVko8S0cvOWc/
+NHHGWW65u9G/s7zPxsN8Tm1lSkpWWk5V6+r83MrJyL7Ay83N2WZcVkVBQ0RDSl5ST/dhTFn0
+U0lrb1Bi19Xh0MbK0s3K2OfY2vr+/VpYb2dm6dvc08vP3+Hb7GlfVUxJSk5WYGpy5dzi39/g
+7ltWY1lLU11PVXBzdeba3dzT1drZ4+7t9P325t/c29jX19nb6W1fWlZNT1RWUV73dfvr4uDk
+293r8OjqZ19oX1xreF9p821gXF/m61Zh2/Jb5tNoW8PDXdu911Pn2lZKXWFPTmvo/njh2d7Z
+09flcGVeVVhdXGBpaGR24t7m6ftraG9tV1Rka23r3NnX1tTd5Od8bGhaVWFoZ/jk4+Dh29ja
+3uB8X11bWk1Lc9lYU9bPdHDSzfNb7t1oUl7+Zl/v2uLs4+f1/GtmYlxaW2RkafPm5eTc1dvj
+3uP7bWtvbGRdYPnp+Ora4u7i5XdqcmtdWl1n+H/07WlWXtbNa09t1+xUXeb0WVrp2+X899zd
+8nXr4fF2bm/5+Wx84+Ty6OHf5PpmY25uZm/0bF1p62xZavd1ZGj7fl5ca/drXnra2uve1tno
+3tnneu/vaFhf+GhaZHvv6mxw5+t5ff3x5fljX+Lf/vtyZXJnX/Lr7v743m1e395vc2Vt7FVh
++GPmdn7k6P9q6O9W+NZu5edPf894XGRe6uZkfc/SbWnv5t5nWWrSW07XYnHmXO5u3WVZ6OD1
+UnLdZ27seHr1/2TZbGrZ+PtyX9j0dHb+/WbpX2vz+W9izvNqd+zea2rdX356V+/YbV3U7WFv
++tFpXe7p8mZtbnz9YVXs52jgaGDn3ldt42pa6udf/NPi++xn33702Gv3c9lccOLkb2TifnFv
+Y3v3aVRh3/JX+310anRu7dxvWOfb+HLk9+B2325z1nhe3ONe8Pt473FmVePjV1zXbW3d9Xrg
+aWbseGRp3Gxkbm7rePXq6+3d5vxv6uV0WGzqdGL4d2H+cHl9du5s9HX1/nx1cenrcG/l/PR7
+9+h84fVt6etxYG/pbmBw/HV86/hfZXz9afTu+3PmcXzmffn9d/p2aOrl5u7s6/5r/G9fcnPv
+5mxq7Hpk5vN4fe5mcuphYO/67fHncfneeG1m7fBk+fJj/vprafJn7+94enD57nT3cuno8ezl
+53nn7Hj4b29vd29h+XXudWTu4WJo/GJ+b11l+vh+7u3o5/jv4/f+fvb27P18/Xdpa/zs6OP5
+bu3qYnd7Z2xp+Pv7e2Jw/fVtaHXs+G1wb3f5b3fq8vbo/Oje6Ofnc+vm9up9eG14eG5ucW37
+fHFoW2ppbnJq7/t7bvt66+tx8OTq6PRv7eLp6unpe/x4dmJtY21wcHf3eHx+6/V9+ej3+X99
+e3J3/Whje/5y8PRue/57+nx5+X/n/Hp+7u3s5+/8cnVud2l4/fHpaW73+Pd88mx3fmdtdmXz
+/X3tfefu6urt6en6bGhxe3dqZmt3c3Psdnvr7fz18fv9bvTj/njxeXF4bWdxbGx7/f31b3vx
+9fF1dOv++fJ27OXh+Onr/njv7vVv9O9wbnZpanFtc3N5bvBvePNvbGlraGlpbHf99uPh7u7n
+4ufi7Ojnf3z2fm98YG/vdWdsdHH58/r8fvXu9nl8eH7y4u9rc3r7/fF3dHxmc35haHBvc3v0
+cPX3eejz6vPt7/Hn3+rt6eP5bGdrcWx4fXP+aHN+aW9udWxodPj+bOTg+Pjf8Hj7fH54c31/
+eG56f/L17err9XFraGh3b2hoa2198ebh6OHjfu/mfW5tfnT+7vtybnFxcnr2anZmZ/t8ePfs
+dfPse330cf32fHr1cnX/du/t+u7s7W97e3Rr/XFx9m5qe3J17O736fpqcP5ycfTo+Pjr5/l8
+9/D+6PRnanBwX2R9bX7o9nJ69HJucmxucePne2757uvr5+X48fJzcHry+mFmb3Dr6+Z47u12
+fvL5c/tsbvz9/nFpfG9uc21obXdtbm128/157fXv5OPs5+zi5Ono8/j7f3b6/nhza3FybnR4
+cGhjYFtsdft2em987vvo3+P98O79ff/7/erm6+nm+2hscm328Xlobnhtcv59b/t+bf56fX10
+c3717u/29PL27+rn7f56dXVvevxyb2hvb25+9HBubGv27fLs3uHm6+zs9+z/cH50bHdkZ2pr
+bW36fHjv8Hlx+/52a2hrfX/17/Xq5uzl5/nv6m9mcP566+T+cf709nZsaGp2b2t4dmxla/Pu
+8+rr+O/u6un6f/F2dH14dPj5a235fXp9e2tqbWZsefh7fffy7u3q7Or1cf3u/3X0fG739v55
+eXBtevrz4OT09n5sbHN2cnx58fj37v5wdW9xffL1cHJzdnN0dXp0bnB+7ufj5O/u6vL063Zh
+Z25jcXR3/vju7/Lv7/d5ePLwcnVwbXJxbnpy7+308ffx7Ob5bWtue/no7PH7/3tybnJkY2xq
+Ynb7/nd7cnbr5u3v7/b77vj++f39/fv3b/n7cHp9dXN8dfr2+/bn6nt28vry7vX1cGpucGtt
++35za2/9/Xr3+W9rdHx9fXf0+e7p7PL59fp/+u/y+PZ9fPb/dnz9a29vb3n++HxtYml5+nzq
++3L87vDw+ert7e9+7erxf3t7e3twdm5ue3t5eWtvbnV9b3tufePt8PXn7Hp+/HD/8enx+v5z
+ev1vdPt5amZye3jy8Xdy5/bo4+/t8Wxtcn5kbXdzeuz57W75+/p3fPfr9/b8/H/86PBx9vv9
+9PV8835ufnRvcmRmamp+dnjvd2p+6vD87+19fvnw8ero/Ozt+/179nR2aXp5d3n893V4ZW1n
+bWltbfr9dvvn6+/e3u7b5np8fHxtfHlvcn1+/vp1//RrZXBqXmN1aXb95O578/5sfXF8c/Dl
+6/rh+f/07+r1/Xdw+3RhbXju+3vq8Ht2cfLv/PD/ZWv6fmt5cH1z9Hx3Z/7t6vb19PLw+XB8
+bXp7dm5tcfDy6+rq+/Tu6efr/vd3anz8bGRmamxdbG1qbnNvdfvv7+/5duTf4uHf5+7sffhz
+92xy7Gdu+PFsZ3dvam99fH1renNobH/o6/f05u7l+ut66vd+/f5+bmVhX2J3/Pr+/fzydevz
+6+Xp63ft9/h5c35ufX78anT78v75b/1xbm13/nL89/n6cf/rd/D2ePz1+Xzt7vP6/Xn9bGtp
+aX5rcGh3ZHnk6u7n8Pnq7vn3+P/q7+rv5+z0d29yb25nZ2xvbXNsbG37d3Jt/X558O/5+Ht3
+6evm6ujv9ufl+/f79WtwfnBddG53Zmp3ffx7cXL8evF28/vp6uX27e7te3V/+376d3ttbH72
+e3JqdX31d3H/7vrt737u7vluenf+/vH+d3Bvdmxz7/X9/Pz5+3h2fXR6d+f28ezv8PHs7ufv
++m1lYW9ub216bnx4eHj4bezv5nzlef9rfWzx/Ox38v3w9/T77u/0fXdreWpnaXJ7d2537vf6
+8u9/7OzxePt16u74b/1z+f7r+/r45Hl1a3Zoc3V1am9z8O17a/vt8evt+XZ7/W17bnhze3v6
+//5rdvnt9/Z693Dt7vF07P3mdXdq93v2/ed77u/x+O/8/W5rbHFuYGtmcm33++945ujm7Oz7
+7XB5bfB09+nv9XT+fHZwc3ZsbXBtbHdo6H7tcuXq7eTf53XsZndlfW75Zfhn+2pwZ3tu7+vs
+/u397vzyd+t78Wl+bvd97+rj7vDrdPtl+Fx8aGlpaH1m8mbuef195Xfj9Ozlc+Jd5F/naO1m
+8/b2dl/uZuT6+m9e7nZy62Bt+OHT9urfd+3521lgTFlsXH7w7vHf3N7s8HNrbG9eXmho8vfm
+7Hri59787HB6attw/O1sc3bi+X3iY/vrauNUeGZpYvFnaGbrfHrwef74/ODsc+Ff9exY3m7f
+eezW699nae1t9/pe72j/5Wpqc2D3b2r2aGz+bvj3dfrn9PV+buV67OZ2+ft+4t3v625u5XVp
+c2lpZmx5bX5qbm/1bWLvbnB5bPbv5uHv8ePu7+js7vl+bWn47vv7ePj38e59+vru7m54e3D6
+dmZeU1hWVlNWZF1tffLa2NHOz8/Z2tv6ffj1/Pji39zUzczIycfL5FY4KyksLzFCZfDGta2s
+ra+702BKPjY0Njk+TuDHv7q3ub3Dzu1RS0dHTlnz2crAvry7v9dKLyQkKS0zSPjXvK2oqKux
+wmhCOTMvMDQ7TtC8tbKxsre/z2hLQ0FDSFbozcfAvr69w/05KB8jKy457sfBr6WkqKy5XjMt
+Li4uNkdc0LqzsbK0u8jvUEpIR05VWPzOw7+8vcTHy+pELyEdIi48abq2tqujpKu5VC8pKzA2
+PEtvzrqvr7e7xexZU0tCQUxfcuHPycW+urzHzdZtWlAzIR4oNDt/t7m+raOlr8VMMSsuNjU0
+P/3Es6qsuMjQ41pRTD88RWLd0MnEw8C+wtTq3t/j1/MzHx4sOThVxNvYrJ+jrcFNNTI4OjIt
+NVS+raiuv8zJytdfPzU3SN3FwsTKzcnDyN14d3Tp3V4yISEvOzVC2tTAp56jr8JuSUI8MCon
+LEe+r660vby1tcBdOTI4T9zS723iyr/Bz29aW2jk2utMNjA2Pj08P0dcyrWxt7y7ur3NUDs2
+OD9TbGdq6Mu/urrIa1Rr2NHjWk1QZeLgaFlm/ezc1Nnj4nFNRUlTVE5KREZRaeHPy9L8W2Ht
+z8nJ0+r13NDP0dr3a3bo53NdUk5OYuzzbG366dzW2u7z3djvW1FNT2BzVkVET/vY2nZbYOHN
+yMXO7l1i4M7Rf1NLTF7f2upraf7YztTxZGRfX3/rbmjnzMbGxcx8S0VHSUU/PDxEVevNw8HF
+yMXDy9n9U0Q+P0ZNUldm38vCv7/Ey9HW4XRWTVBcbebXzcjGwr2+cTIpLDI1OD1CTdSzp6Wr
+us31UUA6NC4sM0zIt7O4u7q7wtZXPjk9Rlzo5/Pbxru5vMTQ1crBYCoeIiw2TsW/yb2so6Sx
+XTEqJyozPkZYx6+opam37EA4NTMzOEbkv7OvtsXad19o/nJcZMu1r/AoHyMpMu67v8y4qKSq
+uk8uJSUvRV/fw7ewra237zszMzY/We7Twbq5vMrwVlBX/tficd/Csa5ZIh0gJje8rLS+t6yq
+r8FDKSElNe+8sbC4v728yE43Ly44Xr+4u8XR29/pZU9JSmDUysvLxLmxtj0gHiIrWK+rs7q4
+tLS76DYnIypDwa+ttcTOy9FgPzYyOFLDtLS9zex6/XNUR0NIWtrIw8C7t7jPLyAhJzPPray0
+t7i9w9RHLyosOd61rrXG4H5tXU1BPT5O0ry4vdNeVF1093pfVFrxzsK+vsXO1GgyJyowP8iw
+sbq+zPJ8X0M4NztIz7i3wNL4XFtydF1XYeDNxsfUcV9dY3xuXlxaWGrTxsXIysrSPyooLTX/
+sa60ucXpdHVOPjk6Qfq7tLvLa0xMWWlkZm723MrGzeBuW1ln/vT9Zl1m59DLzdTb39/3Pi0s
+MDdXvbKytb7YYFNKQkBCSnrIv729xNlpVEpIT19kbt/X19DP2eb9XF1n69TFubO9QyojHyZD
+uaunqrjTb1FAOjYzNk7Cr6qrt2E2Li899cG8vcfka2ZcV1FJTHLSysbNbEtHSVrZxL/By99m
+XGJnVTwxMjhMxLKwuM9IOTlCWtzMycrKyMrbXEc+PD9S1Luwr7fKWj89Q1fMuLCvukklHR0h
+NLymoqWswllCPDY0NDhIzLKpqK7POywpLkTJt7a5wM/l8VxIQ0BEaMvBv8jtTEZPc9PIy+Zf
+YO/Mvrm8QyQfIiltqaGlr9s3MDtO8s3PeXzRwry900Y1MzpSxLW0u8xaREVOW+zTzsnEydXe
+1sfCfzIkHh8s4qqgoKi/QTAvOEhhaF1t0ruvrrfrNykoNPO1rK6/ZUdFV9jM3GJWWm/azNZx
+ZPjKtayyRSMbGR46sJ+dn63ePjY3Ojw4NDpfu6qio69RKiEhLFi3rKuyxfhaXF5VTUlMXtS/
+uLOytLnSOCIbGx8zuaKdn6rLOy4uNUFNTEhR3ryuqq7DQy4sMkzCs7bC311Y4cjJ3GNOR05t
+49rKuq6rr9QsGhUWH0eom5meq9g7MzM1NTc4Qdyzp6SntkcqJCUvXrivsbjIcGX/4edlTkpU
+7s/EvrqysLl7LBwYGiJKqJyanq3tNi4vNDtBQkveu6+rrbxOMy4vPd68uL3Le1Rb4szHzuZd
+T01PX9q9rqquxy8bFhceO6ycmZylvUIyLi0vMzY/3baqpai2XTQrKS9Ky7u3usjb2tjV2f9W
+UF7s08jCurKzvV0qGxcZIUSqnZqdp8FENTAvMTExOVq9rKWlr88+LiswQvDEu7vBx8jN1+Bl
+UE5XaOfOvra2uso7IxsZGyjkp52anae9UzYtKywsLjlcvKqjo6q6UDIsLDFF27+6uLu+wsnY
+ZFBKR0xbz766t7t+LiAbGh4vxqacm56pumo7MS0pKCo0VramoKGqvEowLC00Q3jOxL+/v8DE
+z+xeUEtS7c7IwL3PQi0jHh8nPb6on56gqLXVRjQsJyUnLkTDraalqLDHUTw1Mzc/TnzPwr26
+u77G1HFaWl5q49fySjYrJSMnMFa5qqWjp622w+hJOC0pKCs3bLyuqquvucbWdFNJPjs7P0v3
+yL68vsXN2unu7u1fRTkvKSgqLz/hu66qqaqtsrzOWz0zLiwuN0jsxby5ubu9vsHJ3GNMREBB
+R1fv2NPW2+nw4dXQ1udXRTs1NTc+S27Sx7+9u7y/xdJxT0Q/RU1e28zKzNHW3Ofz92ZfXl9c
+aO3g29rf92VdZvLXy8XHze5PQTs4OT5GTFRp6NvNx8jM2PFgVFRi7dXLxsTJztTd5fFyaF5V
+Vl5lcu/g53NfWFRafN7Tzs7U42lQSklKTU9SWFxt7+bd3u9jVk9PXH7bzcfDxMjP3OPr+3Fm
+cfjq3dnY33ZZT0tJTFv53c7LzNPkaVRNSUlKT1dfdX3q3uP5X1NPVmro08vHyMrR4nhjXlhc
+a/Lg2M7Pz9Tc7l9PS01PXPLa0M7T3Ol4XlROTUxNVFxnd/lrX1dVWWbu2s7Kx8nM0N94YVNP
+U1hp6t3VzcvLz9t2WlVTU1FZZ/3i2drY1N39Y1xYWl5eXV5aWVlSTVBXX3zd0dHR1dvd3+d8
+aV9han7o3M/LysvP2vhvZFpaWlpfbnN+9HxvbmhgYF9haG1xbl5VT05QWGT76NzW0M3Mz9jl
+eF9bXGNrfuLVz87O1+Xxdm12dWxfYPhybere8X9/b2RdWllbXFdSUE5RWFxlduLl/NHJ0uLW
+1eJsXf91Zmfhz8zP09Xe5HhoXFdZbenZz83LysvP2fxSPjczMzU2NztKbMy8t7OxsLK4xONU
+Qzw6Nzc5Pk1vz8C7t7W2u8LRfFROTUxUauXSy8bJ0l47NC8uLi8zPVfTuq+urK2vtL7bUz43
+Mi4vMzpFZ8u8s6+urrO8yvpKPz09Rk5tz8a/vcXZSTgxLiwrLTJF5b2wrKqpq7C6zVk+My0s
+LS85R3PKvLSurq+zu8fiUkM9PD5JV9/DvLu8w+c9MS4sKSotMkfXva+rqaiqr7vPTjwyLSwt
+Mj1O6MO3r66vsrjB1VpGPz09RU9p0b21uLu9yE81LSknJygrNlDIs6uopqaqrrzrQzcuLCwt
+ND9W1r2zrq+xtrzH51hGPj5ASFBxz722uL2/yUYwLSgkJigsO2/ErqinpaSprrr6PzYtKiss
+LztO6r+0r66usri8zmJKPTo+QUVX3Ma9vsC9xVY0Ky4vKyswOl69tq+rqqqpsMXwRzg0Lywu
+Nz1Q59K/s7G0tLi9w9lUSUZDRURGXN7Zz9XUxMPUYUc4NTY1Mi81SezPxLq0r6+2vMPP8VRC
+Ozo9QUZNbc/BvLq5urvA1GZORkM+Pj9EWHHr3dTMzcvU5vr3cVNKQ0BDR0VCR1Vr4NfSycTE
+ys/RzszV6/39ePL5b2313d3Z5PDu/2RQUE9TX/V8+dzY2+fzev9wbWBbXl1bWlpcduHuav/n
++2JfVl1hXGV67eTOzM7Q0NTc3f5rXWNlXmJYXGXo6NzY3tzWz+Px+GlcVVFPTFhlfvTi7eTm
+7fJuXWFrWFtfaGz66tzZ2dPY5+7+eXVxXWVqbGv9fvjX39TX29/fdWpoXGlqYF1pZmL+/vjx
+6/7vdGtebvFvfHJ+/330amVrc2pj7G/17v/v6erh3eLb2NfgfW1lVlRcXl168uf12tfg3Ozx
++mtgbF5iafLw7d7k6n1vZ1lZXVRRXVlZbP/56NjSzs/T0dvn7n1dUVhYV1x17uDd2Nbc293y
+aHppWFlcXGlv8+Df29/i5vVvX1tYT1FXXmtw/+nb3eDa3+Lh629rXl1ka2zs3d3f4OLc6vJo
+YmxrbmphYnbl4u3j3uXxblllaV9fZ2/w/e7j4d7t9e5xeGdpZWlfYmxwc2z47ujv5ev4/Ov0
+bfHt9O/w7eXp6+jt83N7dWJiaWJefmZlcf567tzl5+nudmd7+3RnZ2hsaGlr8uHq5/Dq7f9s
+9Pn8e33q5fzz7XV9c3lr9/ljaPtzbHl+dHf3ffHkfnbl7Xtof/b8cnBtbG/9eeno5+fv7e/v
+9f50bmt4d2BeZXxzdvzs6u708+/yb/7/ZXJvdHHt733o4unx/Pb7ev9nZXnveHP/aWz0c3Vt
+d/3v5OnuefLs++ru937s7vXv/mZtb3FtaGFod3F8fffv9vr3+e94cf3yfvB08Hhx8XxtY3H/
+f3x59vdtauz0a37w5+jd6Ozk3+Hm8/TufXFvaGRtc2xoaf5qZ2x0furybmhy/HNuefft7+/+
+6uLs6+v6/nB6eW1zefr7am3s9H378fN6dmZfeXl+/3L3+P3o7OLe6Obe3Ob+fWtob2h5al9g
+bm1qemZy9Hv8dm5r++758un5ffDsfvtvdXT3+nzz//z4fXzv6fX17fF++Ox1evF2aPv0cfxx
+X2b8+mp0dnJ96fx5d276bG5p+vPr7OPn6PHu7HXt5+d593Fvb2hmaG52cmtzaW18eP336/Pm
+8O/y/vh8+e/39Pl28fr3bX12Y3P/eWpva2r6bnF3eOv1duzs7/H18+7q9HHu7XB3a3T+9Prw
+8GZpYm5vcnX67fr56e7+8unw6+/6+Gz+/f1rbfx7df359ftsanH9bPPzbmxicmT88Pdt++D3
+9fzu/ubvdvTu5fDw7PHo4eN6/G5qZ2NpY29tZWpzevXqe2VveW1sf3F09G5o+2998ubn6+Tp
+5ebu5t/n+fP+bWpuXmZ293V6e21scnzk+355YmlobmVtaWp19/rr6Ont6uDh3/j86/fs7eh0
+amxpbXBpa39vZ3JuYG/s93b69Px1c33v/Pvi4Ojq7e32eHH/ff9vdnpmfvv4Zm5we/p++Hp5
+ZXN3/3xtdvxwb+jz+O/o5O3r6uv8af1vaHT+/mn1cGho/n11dmv5au/v63168/Tq7/Pr7Glx
+/e1z92xfb+7+dvV0fnj0fun6ZmduaWrt/Pf7fH5v7ubr7+/z9/f3bm1+b3B/7e/5eHR2fvj+
+7fz2fPpwb3JxcWZ86e59e3x2eu56f/R9dP709+fr/Xl9bGj56/Z87Wxpd/fv9n78+fx++fNx
+en38a3B/9nlwentp++/28vb25ef98Xx0dnb6ffZzeG5kYvh6fflr8Hr26u91bfjo+nv5/n5o
+Znzz8e7j73768vNrb255dPfvbn9//vnq6OPudn9scHJtb/94/HBybnN8//lrdvjt7vl9eH/5
+7P12ePzv9/f47nVyfvr7+O/2+Hp+fm5tfnR9+n735/Z0fnB2dG5rcWlpdHt7cvrv+3rk4e/1
+8nVp9nv5/vnv8u/07+zv+ex9amr7dm5tfXV+921793NpbnZ5fHFtcn387eHi7Hn56vP68PZ4
+enJscntxa3N88e7r+//+7/H6eXb7dXJvampyem1tbXRr/eXrcXLl4ebv7+bg6n359n58bHJu
+cvt68nh7dPx+/XlsamVhX2Z6a/r26+rp7Hv8+OPo9fD6eP359u/38e7r9H36a3tx/HRxdm58
+fHZscPR8b3pzZWdpb3Bz//Pw6ejo5ePr+/x9cHR+dG50/Hh4fOrs7ev5/nf9fnFmZW5qbGly
+e3j68/N+/3VtdG179/t+/ezz6eHf3+Dh6ftydH7+/XN8f3RwZmJoZWRpZmtycnNrbHj+/vTt
+7e3q6uzx8+zs+/rt7fP8fW9sbG9wZ2t9fHt98/D68ejo8fb6+nt56+bud29wcWpwf3lycnV2
+bWttaW559Pv68uzr5+nr7vd+fnz8ev/0+XVrdnVub3pvbXn/+nR4f/79+X11/vL1fH79+fn7
+9/bv6/l/+fPv9/P7bm1uZV9o/PL48O769fz6cXr89Pt3eW/7eXZwcnx1b3V9/Pz19nVvevn0
++35+/e35eHH47O/z+fz79/Hw7fr3/m94efxtbXp3dHF+bW12dXlxb2509vH28PD9/+7u+O7m
+8Pjt7/D7b2lmZm14dHXv9Xd4/PDu7Oj2eHhucnJ0ef39+n95c3Bta2hmZ25sce/w9v57/ezn
+6ufm6uzr7/Pv7Phuc3r8+fp5cnx1b3ZtZmv6+H94bnB7/35ubnR3cmprfe/u7+3r6vP8+Xh1
+/PLw9/n2fmly8e3x+nxubG96fnd+/nR4fn7//PP79/b17fL8dnZ9c25qa2//8/h+fn11dv72
+8PLs9O/t8u7r7vfv7v1mW15iYmdrcHF1ev739vfy7317+fz+8uPl6ebs7e/u7vP29ft2bGVp
+bGhqaWxvb3N//HX48350ePr06enx+n18fvb3fnd9cG17/PTs7Pj5/Hz1fXh8en15+e/7bWVo
+bHz4+P90//18fP788e3s9vDp7vDw+/D0fH96c252fXJteu/09vZ2b2lkZ2pr+PDx7Oz6d/v8
+8uvp/mv/6+z07/psbGpxc3b8+vT17vZ7dXd9dffs7fP9b2hven717/R5eHd8ffvz//92enFx
+c2/58ez0c3Fxbm9+/nl3evv++vnx/3l8cm91+/Tv8Hz99evs8fL68e3q/m1sdH17dnZ7em9t
+ZWFod/3+fH307+rz7/Vwcvnt+fbucG15+u/49e3+cvrx8vp8/X16df76dnNxfHx2dGxrZmT6
+eXJ9Yj58uFlUzdNUWXb6xtBj2+1SXWJfauHnWmLp3udn3c/vYVhqeGRhbeDtWv3o6+JpfOzv
+Z3HoeO/vdujya2v9/P7vcWz4fufydPV7amRvbPn5bG1kXnP26OHn637x9vHt83/+7eX5eXxn
+bV9c+Xr37fx4aX7u4vlt9O1t/fju6GR75+lle93t9Wzv8V1ie+p4eu7vbFpk6uXo+Xvdc1Vc
+9Obq9GxqV1lu/u7d295pVfzsVOrM/+TY5ulaWndkZtvb9OlaadZmeNztW05ceV5g3+NpWV1v
+927n0fBf9u/tffXQ2/re32VeaGhp8frp2mNc5ftYfudp5Hxk8+laTXPka/HM1mtjVl7+X9zN
+fmdeX11Lccp+98fhZ3pg+Vdj0tne4tH9R1TqZlrXyPlW6e9QTWrmcF3j5FFTbHJt7NbS3PL3
+bmlp5N1k2dlT7d1e7Mvr+9NyeOtXVPB9WeDdXVJcWU1Xa+D4ZON8Wlt2ZV9q69jp2crc4NXe
+187e2s7t/dj0ZvDq615s7FZQflhJ310+Q1I8M0xNRWHPz9fKv8HJv77Lz83a6d7a2+PVz9LV
+4ub5WURe40hb2nPWTC/pPyQ9Vi9Bxu7Hubu6x9HK7lZp+Xp32MfN08HG3dDO6vrg5PNj3tZc
+dMlu7Lt7LjxOHyNIMCzYucezq6yzvLvTPkRJODxQ/vDNvcHLvL3l2ctcVNfrVfDU6/rSxt7e
+5DkxOSgmNzY157y9r6utsLjFYkY8MzI4P0vrzsG9vbm5wsS/zN3Z5l1e6+/sysXO1Pc9MTYo
+JDIvLlHCybSrra+0utNHPjkvLzxATMq+vbWwtbm4vczS3FVLWE9U7eXSytHgWTw5LiUtLiw/
+3NG7ra2ur7TDbU09MzM3PEZ+zMW4tLa2t7/KzuRdUEtJT2XqzcDMzco9OD8nKDItL17MzrSt
+rrKyut9eTTcyNzk9UtHPwLa3uLa5wsrP+E1PT0hZfPjNxOPL5zQ/NCYxNi1C1Wy/r7O1sLbL
+6vRHNz48N0FYata+uri1s7i9w89jTk9GS1lU3sbN38JpNEg3Ji44LDfmadW0sbiys8Tv6E47
+P0I9RGj90764uLe0u8HF1FxUS0JLTlfWytHPxk8+TS8sNi8uRV5QxLe9u7W8y8nTS0hJPT1H
+Tlbax7+7tre6vMHZeG9PTFhZadHKzcvMTURKLi06Ly1MV0PKusi8sr7Lw8tQTls+PUtITd7N
+x7y5ubq9xNPgaE5OV1Zn1s/R0s18QEw/LDg+Ljn3R1C7xM22t8zHwWZOXkU9Q0xLYczJw7q5
+vL7AyNvvb1JOWlxr2s3X085ISVUwL0AxLU5NPc6/2by0xMe/02NjTkZHTVpl28rBvr26vcPE
+yvFnZk9OWF9p2s7i2d1LRUg3Ljk1L0JPSdzAxL65vMHE0+5eSEdNSkz86N7FwcK8vMXFyNzu
+6mNZ+fTe09XY3fhNREQ3LzY4MztHTnzVyr+9v8TFzexiZlhPVmH96NrKxcS/wMfIy93u9mJk
+/G9r6ebe3/xeTUM/PTk4PD5FUFnv08/Pys3WztDl7vhgaef699XU2MvLz87N1NbT2t3Y3OHd
+19/o5H5pWktEPjs5Ojk6PkVOaeHczsjJyMrO0tXi7u/66Nza2tXP0M7N0dXS1tva29/a2uHe
+3ut9Zk5BPTs5ODo5PENLXnjh0c3KyMjJzM/U2t3d293d2dve3dXQ0M7O0dPY293p+/vzdHtx
+WUxFPjw7Ozw/QURMXfnd0czJxsbKzc/U2OPt8P3u4+rt4N3Z0M/PztHV19re2t/o6e9+XVNM
+RD08Ozk8PkBHUVx92M7Lx8jKycrN1eP47ujv8+Xg3drSz8/Pz87U2dzd3ufw8O5+Z1tRSEE+
+Ozk7Pj5DSk5d7NrRzMnHx8jKztXY3+rx7u/p3dzY08/Nzc/P1NjZ3eLsdGtuXlhdXU5EPzs4
+ODk6PklTZd3OyMC/wcHCytbjeWVjZ3rl2tfSz83Kx8jL0dnd4u7u9G13cGBaVkpBPDg1Njk7
+PkZObdjLwr++vr/DyNHa6WxgYF5i+N/b0svIxsjLz9jd5vZwa2tmamtpXlRNRj87Ozk5PD9G
+Tl7nzsO9vL2+wsfM2HldWFRUXm/p2M/My8jIycrP2uTubWdoaW54c2lYTEU+Ojg4OTs/Rk9d
+5szAvby7vL/FzuNqV01MUl1r6tnPycPCwsPIztTfeGJgYmJoXlhXT0lDPjs4ODo8P0hWd9bG
+vru5u77Bx9DnXk5MTFFba+LSy8jEwL+/xczX63poYVxVVU9OT0lDQDs1Nzk7PkhTb9PFvrq5
+ubu9w9DhZk9LSkhOYu/XzsnEwMDCxsrS3/NzbGNhXFdTTUlFQDs1Nzs6PEVPX9vJv7y4tri8
+v8rccU9FQ0NDS1v+1snDwL6+vsHFz+hwZV5dWVNOTkxGQj86ODs6OT9MV23UxL24tre6vMXV
++ldKRERCRE5l6dHGv727u77ByNf3blpRWF9dVE1OTURBOzU5OzY5RVBY8Mq/ubS2ubi8yd1s
+U0hDQ0ZOXu7VysO/vby+wMTR6XNdVFBSVUxJUk0/QUQ3NT87NUNiV2jIvLu2s7e4uMLkfGNI
+Pj8/Qk9pdt/Jw8O+vL/Cx9Ld62pcXF9aUFRbS0NFPDQ8PzU5TFVT7ce/vbi6vLq9zuPzWklE
+RkVKXv7oz8bEv7y9v8bP3e70Xk9WXE5MV1FFSlE/OkBEOztGTVB9zsnBu7u+vb3G0tlwT09P
+TEtSWmPo0MvIxsfHyNDc2t/waV5iX15hY11UVVZKPj5DPzw/RktZ89fOx7+9v7/CyM7ZeFtb
+W1JPW2np1MzLzMvKzdHc82lhXVVTWGFhaWtnaGFcUUpFQEFCQ0hNWXHdzcbCv8DBw8rX6nte
+VlZbYnP249bPz87NztHf82dhXV9dVlRfZmh0amBfZVxRTktHRkZKT1pm79fOy8nIycnO1uD9
+/X94eHJ/9ejb2NjY19nZ4Wtkbm9hXmxxZF5eZW1wa3F0bGVaUU1LS0xOUFNfavTe3dTR0M/O
+0dna3+z37u/v4eHg29jc59zY7nru92xqXl9dW213de7p+v3x9WNUT0tIRERFR05ZbfHd0MzI
+xsfLztnm83FrZ3j48urf29fY2dbc6Ol9Wltuc2Ru3tTd4drc7/puVElCPjw8PDw/R1X708jF
+wr/AwMPL0eB8ZGNfXF5n/Ore083Q2Nrd3d3xaXHu397h393h+WZYTEE8Ozk2NjlAS1/cyr+7
+ubm6u7/K22lOSUhFREhPXPHUzMjDv7/Cw8jP2OtlYGpvZFxcXFdNQTc4PDg1OkNLW9rIwbq2
+t7q7v87ccU9FREJASFhv68zEwr69v8LDytv6ZFdTVltaVFpdT05LPjxAPzo8S09P/szGwLy6
+vL2/zt/sZU1KSEZNXn/sz8XGxMHCxcjP3vF1X1RTWmJcV1lZVFE/OUJDOjpLWk1Z1sjFv7y9
+vb/M3tzsVkxPT01TZnXfzsrHwb/FyszS4n1uY15cUk9TVlZPT0xBP0RCPT9MTVN21svHwL+/
+v8LKz9vxX1hYU1BYYWvs1M/NysjM0NDX3+txaGVjZWFmYmFiWU5KSEdBP0NHSExaa+zYzszH
+xMbKztPc7vj0dV9ibn7q3NjU0c/LzdLR1N97bWldWFpdXmpnXlxdVktKR0FAREhHTF5859XL
+ycjFyMzO0dvv/Hhrfe7u49jV1dDO0tvX2+v5eGZdXFhUWmNobG1nXlpWTUZEQ0FFTE9UcdvU
+zsjGxsfJz9jb62xsfG1p7+fk1M7U1dHX3+DieV9dXlxcX2Fu+vt2a2VbVE1GRERCQ0tRVWjd
+1M/Hw8jIyM3X3N/6ZmtoZXvl3dva2tnb3uDq93Fob25jXV9sevfj5fd3YlhPSEVGRUNGTVNc
+7dbTzsfFyszQ4uvvbWJnamJn+uLb083Oz87R2Nvi7fVyaGhzeG54fHttamNXT0lAP0FAQElP
+UV3h19DFwsfKyMvZ3uF3Y2loaf3k3tjT1djY19jc4/1qb29jZG18+Ori5Op/XlJNRkBBQz8/
+SU9UedfQycPEycrN2+55cGJnfHNy6eHh2M/Q0tLT2N3j9/x5amlveXd5/fh7bGlcUklFQ0BA
+RUhMWnzl28/KysvLztbb4/J0d29n+uXp5dnV2dfT1tvY3e94bWdgbG5sbHhvanFxamZcT0tJ
+RUBBREdPbeHb0MnHyMbFy87P2+33b15aWVtcVVnZys3c7Obd3up3XllhaGlw+Orl1s7U3vlc
+SkZFPjo8P0VT6c7Gvr3AxMrZeWRXTEpLTE9g6dTNysnKys3W3vhoXl5fX2h3ffTc1M/Q3Of5
+dFxMPTg6OztASlTlx768u7zDzNZ7TkRAQkZNWGTmzcbAvb/K0d37aWJZUVFa/tvNxsXHyMnO
+5l1MPzcwLi8zOUNb2sC4tbO0usTaWEU+PT0/SVziyb25ubzAydb+Wk5LSkxVdNvQzcvHw8bK
+z/FNQTouKi0yNkrWyb2zsbO0us5iRTo3ODxATGXeyr66ubu/ytb3WlZWU1di/dvLx8fGyMnL
+2G9VRzswLCsuN0V4yb+7t7e4u8XgV0M6ODxCT3TXzMa/vb6/xM/qX09OVFpj/d/SysbHyMnM
+z9j7UUM3LSkqLjdNz7+4srK1uL3K90k6NDM4Q2LRx8LAvr+/v8XR7VpNS1Ns7N7Z0s7Ny8fF
+ys/eWEU8NCwpKy84WMi6tLGztrnA0l5IOzU2PU7iyL+9vcDDx8zQ319MSUpQZOTQzMrKzMvJ
+zM7Q6VFFPDEqKSwwPfrBurWxs7i7xN9UQzo1Nz5Q1sK9u7u+wcLGzutUSUdITWPp3dfPy8rJ
+ysvM0XlPRDswKystM0LhwLqzsbW5vcntTz42NDhCW9LDvru7vb6/xc97VExITVx26N3Yz8vJ
+yMnQ2+9VR0A3LSkqLjdPy7u2srK1t7zJeUs8NjY8TPbOxsTAvby9wczuVUtKTFRncXbo08rH
+xsfL09jZbkpBNisnKzA7Xce9u7aysrW6y1pAOjc5QFF52crEvru6wMzgZ1RRUE5PUFhv2cfB
+w8jKzMvM1mpMQTguKiwzOkrXxb65s7K2vMh8Rj05ODxGWeTKwr67u77EzeVgUExKTFRn7t7P
+x8XFx8vS3uhpTUM6LyosMzxP1sK+urOwtLrHf0U9Ozk7RFBn1cO9u7m8xtPrXVBNSktSX33a
+ysHCw8PJ1eHl/1NGOy8rLTU9SXTWzb+1sbS7yO5SRz88Oz5JZNPCu7u8vsPL225VTEpMU119
+3c7GwsTIy8/V4vVdRzw0LSwwNzxJ882/t7GzusDK4lNEOzc7SGLlzL+9vLq8xtTpYU9NTE1T
+Xnjh0MjIycnP09vrflVCPDUtLjU7P0zkyr62srW8wsv1TEE9PD9MXu/Qw7y6vL/Hz+NpVEpL
+T1tjcd/RzcnGx83Z3fFTRj43Li83OztCa87CurW4vr/D2lRGQD0+SE9a3cO9vb2+v8LL3Wxa
+WFlYWmjt2dDNy8vR2u9dSz86NjEwNjo9RmTPv7q4uby+w9RiTEVDRkpOX9jGwL++v8HH0eth
+WFNRU1de/dzX0czMz9jpZkxBPTgyMjg6PU7nzse+uLm9w8zeXU1HREdOXHjTxr++vr/DydHf
+ZFVaYV5gavni2Nfd5u5hUEhAPDc2OTo6QFrezcS7t7i7vsXQflJKR0dMWGf108jFx8bDx87d
+92RXUldcavjn4OXp7XZXTEpDPDo8PT1BT3DfzcO/vr7Axs3b9WpYUlZhbfDazsnEwMPKztXp
+bldGQklNTFZxc2tu/OHrcmhcTUxMQTtDVkhG9s/f1cK/xcbFx8rQ43X3fGRo4tfXzsvO1Nnj
++mRYVFNQUVZaY2Jfb29eXF1VS0hOTklHS0xNWm7o2s/Mx8XDw8XJ0NXb4ens4d7Z0c/Q1tbY
+5XhdVE9NSkxNTU9VUlJYV1ZZYHh1bvzycmt9cGl6/W535eHo39nT1Nja19fe3tjZ293b2+v6
+8fptYl1ZWFpcWmZqXFxhdXlvaGReXGloY3H27/Hu6fl+7ufu+uTm7O93cWpiZnH57d/c397b
+3OLt/XBjZGVkZml9/n7p5+zs9W5ob3loaHJ3c3D48v549vD48+7t9HRqZF9kdXhlY3rz8e7t
+8fR+/Xx3cmxpe+3h39zb4+r5fXJ1+fp1c3Zsb/bu+Pjx9XB4dHFvanBybGNobXV5+ezv9vn0
++m5v/3xrZGZlYm/w6+rm5efk4uHt9vn59n1yfe7s9fPu7n989fv67+x8Y2JlZGdqa2tuefn7
++f318ndpa3ZycHhycXrr4OHn6ury9v3y7Px7enr78/X5+fbq5ezv8vx6bmdka2tsa2dsefz9
+fnh7+/Py+/50cG1qbvbv8d7e5OLf5fZ/dWxgafxueHlmVlr4W13a3Prb1fr183P8+Wt94PFu
+fO3q4d/42u9ld+f8aetscV5UXGJdVFv+bGZ35OZt+ejp5d7Z5u/o/Ori8297XnfsZWvY6W3a
+8nprcnzx9Wru71ts729fe3v6emR97V9e5N1vX+vkYWrffF5fcujndnPZ4n/j2+h339vbb27e
+eVtz415Uc21nb2Ze+vdc7+Fdft5vddvpZOZ2Yt3uXf/bYV3Z9lvb613m32D51ln/2WVj33hd
+7uJX8+Bn6uBiZNt1W/ttW2l9XXXs/uLj/3zubuzkdHzYdfd+9eztbv/p/njgcFp2bGb6fmDv
+8lz582v3dm98f39v72xl/Hxv7u/r7+zu8tps9dT0VOvlbW3sbHXuaWXqeVfv4Ghv4mxv//r/
+6d9u8+trbN1vYN7uXO34XPPnafbocnj6bW/7ZGbkcWrg63Py/2ppcejr+2Lm23xv3+xf8d3h
+Xnze5F9a8OtoYuZwbHL293ZsZm1oemJo4t9qadvcbefe+HLe42dx6ntsffZpavr3bWzc43pj
+fO52Ymz0c2Z353xy8Ph+53Zf++ro+ffs6u57e3Rv+vpsa/Xsb3nm6PDs5eTr8/Pl8HxvZmBd
+XFRYV1NYWlZZdX7/6+Lc1c7OysvOzs7W3+Hf6nh6b3Lr6urm9W5aTUhHPzs6PT9ARFH52czB
+vby7ur3Ey9Tma1ZOTEdLT1Bbb+XX0c7MzMzNzMzS2d/m/mZdUEU9PTw5ODtBRktW/tPIwLu4
+uby9wsva/19UTElLSk1UaXnv2M7LycjKzM/U19rqe2ZbVk9IQT8/PTw+QkhNXezVysTAvry9
+wMbO42BWTkhGR0pPV1/w18/MxcPDwcTJz9rn/WdbUlBQTUtHQkA/Pz9DSEtQXPrYy8fDwMDC
+w8bM09/2YldUUVJUXnV76tzX09DOztLW2uDudW5lXV9fXFtVTEdDQD8/REtOU2D02svDv7/C
+w8XKz9PceGJaUE5RWF1hfujd1NHPzszNz9fj8v54Zl1bWFNQTkpGQj8/QUZJT1p439PMyMO/
+v7/CytPd8GxbV1VTVltp+d/X08/S09DR1dzvb2NcWVZYWVhfX1RLRkI/P0FITVd449XNxsPB
+wMHCxszW43RbVFFQVlxr8fDm2tXT0NLX4O79c3NoZmZfXltaWVNMSERAQUVKVF/53tjMxsTD
+xMXIzdTe8GxkX1pXVltoe/Lh2dfV0NDU19re6XBjXlhVVllWUU5LRkNDRUpNUl772s/JxcXG
+x8jLz9DX5nxqXlpWVlhdb+/c19nUz9DV1tri9nJpYl5XVVpaVFBNR0VFRkdKTlx34NTOycbG
+xsjKztHU2+P/aVpUVFZbavfo3t3a2dfb3t7s+/jz/nBsYFxfW1VOSkVDQ0VITVRcfN/UzMfG
+xMPFyczP2eH3a1tWVFNZZXzo3dnU0NLT2Nvg7vhrYV9ZU1NWWVZPTEhFQ0RFR01WaeTQycTC
+wb+/xMjN0dzqfl1UT05PVFxq/Obc2dnc3+Hf3+3+dmVdXFxgaGxtaFtPTElGRkhKTllu5dbM
+ycfGycnLzc/Y3eh8XlZVU1pldn3o29TU2Nfa3+54aWNdXFxcX2Zybl9WTUhISEdHSlNh69jO
+yMXEw8THyc7S4PFqV05JSkxRXHjj2NLSz9HU1dHW3e5rX1tXUlhcXWVlVkxHQT8/RExXdN7Q
+y8bEwL/AwcXK1fNiUkpJSktPWnTr49zX1dHOzs3S3u9xY11dZ2tpaWNSRT48Ojs/S1tr59XM
+xr65t7m7v8ztV0tGQkBBRUhLWP7Zy8K9vb/Fzdbh825oYV9mYFpXWVJIPzw7OjxDTVv81sjA
+vrq4ubzBy+JZSUM/Pj9ARUxa5svBvLq7vMDHztrzZlhQTExRW2Rsa08/Ozw8PkFITFNq18W8
+uLa3u8PO3HBYTkc/PT5ASFflysG+vby9v8HG1G9SSkhKUmb27/9tWkg+PT07Oj1ES1Hux726
+t7a3vMPK12RMRD87OjxCTmvYx767ubm6vcXQ62JQTU9NTFNhcvpzTj06Ojo5O0FLU2rKubW1
+tbS4wcrVZ0Y8Ojo6Oj9RdNbCubW1tre8xs/tWklDRElNT1Zma1NBOjk8PDs9RlJs0r21tLe2
+tLe/0ehYQDk4Ojs9R2rUx764tbe5ur7Nc1NNR0NFSExQXH1nSDo5PT48PEBQ+9K/tbK1t7W1
+vtH5U0I6Nzg8QEpqz8fFvbe2u7/FzehdV1BJRkpQVVJUTUE6OT0/Pj9LcdjKvrays7a4vsnh
+WEk+ODY6QEtV7szDvrq4t7vCytPtXVJTW1VTVlxdSTs3Ojs4NThGX+7QwLexsbO2vcrlYE0/
+Ojg7P0da1sK9u7m5ub3G0utjU09PT09UX/D5SDExT04xLDvTy0lKvq6wvbyyt9RhbWBDOT9J
+RUNexrzBw7q4v9Ll9VpGREpNS0xa8ePf19z+4PNHPT48NDdCT1Bfy7u7wr21uMvr3NhhSUlT
+XWj+3tHLx8nP29zlb1lVWlpSVFxqdnjg2NnZ7ko/R0U5MztQVURI3cHBxr+4t73Gys3lXlRT
+Vl5hbevd18/NzcrKzuF2ZlVNS0tJS1BWWl1saFpOSElLSUNHUFxp7NPJxMLAvr7CydHm8nFc
+XGFeX3Pg1s/OzczP3/FzWU5LS0pKSUtbXlZcbfnu+3d4fWVcbXtlY3/o4d3a1dbn8Onx+HV8
+dfTt6dzY2dPMz9bS3m1zZ1hWXVNTYlxVWWRga3hqW11rZmBeaWf+7/rm4unr5vDh4fbt9m13
+d+vY5/Xm1+J+a+7fdFln6NzxbOTm+2thbWRYVV5tXl5defZt9+Xn29zn7e15ZPx6ZW72/mT6
+4ubv/fT7/vpsX15lfPr4bnPn8vrj73f99nn9b3nm7PPr3+Hn5Ofo7+3t/3R2YFRUY3pkX278
+fnRsZ3P/efp3fXF08+/t7+jo9ufi8395fn/4em3x6efn3tzv8e/ueXR8ZGdpYl50+e/2/Xxw
+ZWVtbH37dPj3fuzs8ufydG1ta21rWWDx7u32793e5+Xg6+zl4elycmxkYGdkbmxvd/t6+OHh
+7u3r73jte2x09n5q/Hh5dGxicfVmX2Rram7+eObZ4e3c4XByd25qfW1tdXLu6+Xn6Onc5nN2
+9P5pbWxlX2hoafXr8efed1Tz41tn7Xr13///+Pt53+pfY215Z39x+OXe6Oz38+zt6vlucXhi
+YWZobGb6f/nf4d/v8/j0f3VwZWtuam5sa/Lt5+zv737m7GNZV2zf72Nq8+rd3uzo7PH7dXt1
+bGJy/u7m7fz1bWt4aVpVZf/ybGBq/uPX2t/k8PDy7PF0cXd4c2tfZG9ocPft6+Xo/Hl27OT6
+cPt/eXJoa2xsb2ppbvft83dv/ufj7Ojp6ur0fm9sd3xxfXludG5rb25oa3NubnN+7e5+evbp
+5+vu9vj99PT69/Hq8fn4+fx0b3F7+/H3e3F6+3ptZWtzb3B7b15bXFpebHz1+/jz7Obf3ujg
+3eLj397i4t/b2dna397d4ePmeltMQTw4Nzk+SVnq0svHxsPCw8bM1eT6aWRfX3Xt3dDMy83O
+z9fmZEo/Ozg3OT5JWuzQxb++wMjO2/Rxamtpc3Nvcvvr3tDKyMfL0+1gUEU+Ojc4Oj5JWezO
+wb28vcLL1uJqXFlbYWl88eLXzcnHxsTHzd5yWEg+Ojc1NjpCTGPZysXCxMjM0N72bWZka3zo
+3tfPzsvIxcXIzNLlZks+OTQyNTpATWjWyMO/vsDI0OFlVFNSWF5x3tLLyMXDwcLGzNjoXkc7
+NDAvMTc/U+THvLm4ub3G1G9OSEVFSVN92s7HwL28vb/FzdtvSzowLSwtMTtS0r20sbK3vMnt
+VEY+PkFKXNzIv7y7u72+w8nZcFZCMywpKSsxR9K6r6yus7zKb0g+Ojg8R2TSv7i1tri8v8PF
+ydV2TDgrJSUnLD3Vtq6srLG/11w/NzM0OEThvrWxsbjCzMzIwb6/y249LCQiJCs9zLCrqqqv
+vNhPOS4sLjZMyLSusLjF3/rVwrq4usPuPisiICMpOc2up6WnrLvvPi8pKS076LitrLG+3Vxh
+zry0sra+3T0oHx4gJzy8qKOiprDORjMpJikzXLqrqa6+cUdHdb+zr6+2wWQvIB0dIC7Lp5+f
+o63NQDEpJSg1cbapp67FTz4+W8a3sbG1ucY8IhsbHizJo5yepbNwNiwnIyg6ya2lprLbQztC
+dsO6t7i2tLtBHxgZHzavnJufrMJJMiskISlItaajq8dDO0Zj18jEwLqvrLkuGBUbKsSemZ6r
+us1MNSogHy+9pqSrxEM7UtfR4vjqx7Coq08cExgp0aOboLG+vMlNLyIeLMGnp7HjPDz8v8b5
+XXrHsKmrZx0TGC3KqJ6jtsC4vVAvJiM3t6esv1M4OVfO3GrkyrywrbRcIRYZLcyuoqCrurzP
+OyspLVWvpqy/bD45Q15dW9W+t6+uukggFRcp6K+in6m5v+QyJygw16miq73fSD5IV05L676y
+rKy5SB8UFyrls6Whq7e82zIlJS7NqKStvdRYSkxORkVizLarq7dHHhQZLs2wpaKstrvqMSYn
+ObypqrW+xtxVSkdESVfqv6+tt0QdFBs0zrSnpq20u1wuKC1dsqyyvLy7yl9LTFpvZ33Esq/B
+LxkXI0/BtKyssLXMOCgoPb6xtry8uLvWUFXXzPNPYsGytGgkFxw3xb23rq2uu04sJzXMt73E
+v7q6y1BO08DWVFfQt7LMKxoaKte7vLOrqbNcLSUxzri/xr26vM5MS8271kRJ0LawvzgdGCRR
+yM22p6Sr0i0jM8u+zMe8ubnPR06+t+o5OVa8rbhBIhwlP2pXx6ukqcUzKT7N0V/fvLK21Ex0
+uLVuMC9Kva+7RCUgMFFGPc+uqazLOTnTxllFZsK3vOlfwa+3WC8rNeG7yUYtLUnxPzdzt7C6
+8UvPtL1PPk7Kub/Z0bevu08zLzdOZ0k2LkPH0kRE4MXIYj9fsay+XEpP6s7T0r2vscZPOzc4
+PTs1LzfVuMTz3dz4VTs51K2suuJUU2vp1b+1srvpSkhDPjkvKytBvrjEx8LbUzwvO8KvtL3N
+Y1RUUt+2rrfSYVVaZUw1KycrT7i0uri830MyLkC+s7e7xOJjS0jZs6641kxGX+hJMCgkLGC7
+uLawudxDMC5Nvbq+vcTO3FBH37ezvftGSP3ZVTYqJS1Tw7u0r7bMSjIvUcDAx8jPycHcVty9
+u8PpTEv60V45KyUrSczEurG2xV43LkPHws3Q08m8xH5+xr3E6U1L58vqQTAnKT7azL+0s7jE
+TzE3fdDk7+bPurjSYtjGx9RcSWHKyl05KiQuUdjNvLW2uc1AMz1aY3jRx7y4wefoy8rO4lZV
+1sPSXz8uKC07TNW5s7S4x2Y+MzQ+bb+0t7/J1ePq+Wht4M7Jyc3ZWDsqJCo90baus7m7yUYt
+KTJgua+zvsXN6WFPS2HMwcTKysfKWS8iISpCv62qrLDFSzQrKzvTtq2uutFlSEFLaNfGv8G/
+vcLbSzMnIyc03K6oq7XOTDo0LzVZv7KutMhhRTw/T+7FtrK4v9T1Zkk0KCUqOOy3rKuvwFg9
+Ojw/UOHGurS61Uk5O1nGube9z3hfb+LW7kEsJCQrQbyppqu47kE8Pj9AT+rHubK3yFpBQlTd
+zsvGxMTHz9d2SzgrJigvSr6rqKu37jwzMzhBYsm8uLjC8ktCRlrVxL26u8fpVElS2cbPTDIq
+Jys9ya+qrbvyS0hOUUU6OEF2v7SzucjzWFRp3c/P297RwLi3vNQ8KSAeICxdsaajqLPaQzg1
+Nzs+RVTmw7Osq6/CVz08QE/tzMLAwb++vsL5NSQeHB4pTrenoaKptchcSEA5MjE3Tsm1rq6y
+vc9fSkVDRUlPds6/ubW6xWg5KSEfICc157Gkn5+krL39RTgyLi4vN0zKta2rrrnUSjgyMjY/
+Z8e5s7O2u73GWzElHx0hLVq3p6Ggo6q1zVg+MSspKzBB2b22tLi+ytTlfW9pbGdlYWD418i+
+ubm910EuJSAgJTBUu6ulo6WrtMV4RjcvLS0wOlDZxL+/v767uLa5v9VSPjo8P01w18vEvr/K
+WzwtJyYpMEPNtq6rrK+1vMbZZUo9NjIxNDtN3L61r66usbnKWz40LzE5Su7Jvbq8v8jjSTgu
+KSgrNEjPuK+rqqutsrzhQzUuLC40PlLcwrm0sLK2v9ZXQjs4ODxJa9LEu7e5v85uRjgwLi40
+PlrQvri1tbi/znVRS0hDQEJESVNo5tTIv7u4ub3H2V9MRT8/QUhY2cG6t7e9yeNMOC4pJygs
+O+q6rqytsbjB02xLOzU0OUTrv7SvsrnH4lVIQkBBRU1f4c3GwsHEytLe825kUEc+Ny8uMjte
+w7SvsLfG51hIRENGSlfny7+8v85wTUNDSl3118vIyMrQ3OxrXmN36tbOzMnJzd1hQy8nJigw
+V7msqKy51FA/PkRISVFo28O7urzLYUpHR1nn2tLOy8fEzNxyUkhIS09ae+vYzMrGw8tqQjMr
+KCs2XL2vra+4yOxZS0ZEQ0RP6Mm9ubvG4VxQUlpp5uLm3+Xt+mdcWVdf7tTMxcXL0vtTSUQ8
+NzY3OkF4yLq1trzMaEhAQkhVbOLQy8bCwsXN1m9TSkhMU23k2djZ1t74+mtgYGVy6NPOy83c
+e1xNQz47NjU5Stu9tra7ym1OSUpNUFhq38q+u73F3FRGQkNOb9jLxsTI1e1WSEJET/fNw7+/
+wcbT6WdPQDYsJioyUbqrqa258kU9PEFIRkdS68e5tLe+0lpGP0ZW+NfOyszO2GxRTE1W987F
+v7/F0nlUTVJdcXNKOCwpLjrmt66utstZTEdGTExKUfrJubCzu9FNPjs+SmHh18/Kx8bN3V9L
+RUhY6dDLys3b7efx7tfZeEw7LyoqMUzAr6yvu9pTR0JGS0xNYNK+trS6yWVJREZMW37r4dLM
+zM7dWklDRVbjzsnK1OLr28vI0e1SPTUxLzE9Vsu2sbW92Us/Pz9JWHDcy8C8u77M71VNUF58
+4t7w/OHb2eBmVEtOYt3Qz9h+Z2nx0MzR3m5OPTczMDdAZsi6uLvE32xeVlRTTVBl28S6ubzF
+311TTUtKSkpObNXHwsjZaVNTXnPt4+1nb+TUysfL2mRKPzs5NjU5QlvVw7u6vL/G1m9USUFE
+TnXWycK/v8PJ2V9JQT9BTHDSyMXJztbs+nRjVE9RW/DZ0MvKzdDfXUU8My8wNDxO68vBvLq6
+vcHL4GhWTk1RYuzY0tDP0dff8m527d3d3eVlVU9PTlhsdXT06dvRzs7P1N9vXE1EQEFAQUhK
+S1Rm7uHY1dDPzs3Mzc7Nz9nh4uV/a2hcV1tja/fh3t7h6vTp4/F6c2FaX3jr29fT093xc19S
+SkNAQEVKUl1dXGdqa37l3dTS0c7Oy8XFyMzU3vtlXFpja2z/9P759nloamleWVVVXvjd0M3N
+ztTZ5mxfV01HRkhMTlRUTk1RWmN55NzY1tbV1NTOztDQ1djW3ufr6fRxcHFwdfnu8m1iWlVV
+XXPq2dPR2OhwZV5ZXF5aWlZQT1BOS0xNT1lidPLg2NbW087LysnKztHY5u7v9vTt8/by7vN9
+aV9YUVFYZvXf2tnb4ev9eWxhXVtWUVJTT01OS0pNUFdo897X0tHT08/MzMvLzNHX3Of7cvx1
+YmNubHB7fHNubV9eX2Z+7Ofp5Obr+np2dn53cWtbVE9JRkdKTFFYXnPs4trSzcjGxsnLzdDa
+5/V1bGRiX19qeWhld/ru9XV3fPbv7fR/+/Ts+nl4fffn3+bz7vF3W0lBQD8/Q0hMUGHx1cnB
+vby+v8bO2e1zZllWWlpcY3zh1tDP0993XVlWTk1RT1du6tvWz83Nz9PZ3+h2XlNMRkE9Ozs9
+QU1x0sO/vr+/wsfN2O9XS0tOWfXQy8rLz9Pc5fhlUktLSUpQZuXZ2dza4+rre3ZwbXzt7d7U
+1t7valZGPTk3OT5Mes/DvLm5vMDK3G1SSUVESlbw0MfCxMnO3e5pU09MSU1add7Y2Nnj/Gdf
+YWBcW19p5tHLyc3beFtRTEdAPTo7QU/hxr26u77DydPb8FlKR0dOaN3PysjL097yaltQTUxN
+UmPz4tnX3dvi+XZiYVxXYH3j1c/OzdDjemJURDo0MzY+Xsy9urq7vb/Fz3dMQj0+SGTWysXE
+xcfN129TSUBBSFFv3NTS0M/P0dr3XEtJTFRh8dvSzszKy9X4VEhDPTo4ODxJbM29uLi6vcPM
+2fBZS0RFS13gz8vKzNDd9GlXTklJT1p44djQzdDY3vxdVU9QXHPt2s/OzMnL0ulmSTYuLi82
+R+3FvLm1s7a5wOBSQz4/R05l6tHGv72/zXRUSEZISk9OU3LazMXEy9h1W1pVTk1OVnLUxcDG
+z+pXTktCODIyNUBfzb+7ubi3uLzH51BFQUVOXWr5287HwsLK5VtMRUJITE9aXevPy8nM2nxh
+WFBVXGvq2dHLx8bL21g8MS8wNj9Obc+/uLGvsrvMbExGRENCQkZY38a+vcHI0u5kVExGRUdO
+ZeTTzMbEyMzS5WhYSkNKVlxibHH083xzVklDQkNHU1587NvOxL+/wMbN0tzn+WhbW1xaZfbn
+3NHT3N/v+XxpXFdWVV1x6+Dm/P7+bGZfWU9PV19rdW9scXNmX2FlZ19bX/Hb1dLP1NLNzs/T
+3v1vaGz/9/15eu3j4d3oaV5fXVxdXGJx/+zj6+rzbGVdWVxZVVZZX2dt7e3/8/V93uD9+Gpr
+fu3s3dbb3t/a29zh4fdoYXJrY/789fn/ff1vcHtjYGVne/Z1dPt8fnNkXl1hY2trcPnv8vPn
+6Ozr72x8dHH/bG93cv7p5+Xi3/Hu7fT95exzdXR2fnxpd/vq6PPvenxzfHl8dWZcWlhiaGNt
+/X138+7r7unmdmt5aW5w9ubt7uvp6vD48vf07Ojj4eXq9HxsX1lbYGl19fx6fXd3/e7u+317
+f/399vpxa2RhZmJiaXf7++307e7ufHd6cG748vf+7+5ycOvg397b3Ovp+29kYF9ubG72bnRt
++W1ubHpqYnn89O//a/p9aHd1bG9xbnF09u/n6uv35fB87u52cfhzbWpwb37w7uXm8u7n7+zo
+931xbm5xam12/O/3+mVkZWxua299/Xx1c2x1/f7s7On0e3Nz9+vy9fvv+PPq9v9+bm1tamZq
+en17efV//3zs7O7p73FqfG948e3v8PhpdW58b+ns6PlybmJrb/T/d3X+bmd3ZW1+7/Pq735y
+bO727nZ493lteHz3dn7683fv7e7xfXh0endz+W979+p9/n7+b2/+/Op8+3pzaW958O38enJt
+e+jq8333+fx5ZWlycXrx9XP68G5mbvz7eP7v9XZ48O/39Oz0dnl5fPb5+nV99fPx7vH48Ory
+e3dvaGtze3Rua3F4bm59/fr1fXd3fX17bm96//Ty7u/s6uz2cW16c3N7ffn28vVwc3p3+urt
+7+/v7/b+9Ovuffn3fnlvbWhobmtkY256cHJ+/Pp+/fD0fPT093tycn34/fnz9X1z/vPs5unt
++fXt7vxzfXdvbHZ0bWlrbGRt9+vs8Xp3+npxeXRucX37eX/9/Pfv7Ozw8vJ5dnB7eXJ7dnzz
++HL99W9vd/7t7+35bmxudPv9++z38+rl7P75d3JqZmlobW9qa3D29Pj3/Hd0/ffw7/R+/vX0
+7+31+/h8bG5zdXRwbXN99vP4b2/6+fT26+r+8e339fL+/Xx6bmxvd3VxfHny7Phva2pobf5r
+7ddt6uDtcHdu8PZ69XdraPBiWFPd1f3j1+He91pf/1/o/ff27t7t6338dvxvaVxn7+zt7Pv6
+7HBsbmlobGpmaGNp83z14N/e3N7n6XRzeH76fPL48Onl5+Dc2tna6X9oZF9QSEA8PUNGSlrq
+y727vLy/xcvaWklDQ0dKT17p0cfCw8bMz9npZ1ddYfXl3dLO5EIxLi8xNzo/bL+zrKuusrfC
+7EU0LS4yOEd2y7uysbK3xNP7TUE/P0hd6Mq+u7e3vNs+KiIkJys0Q9+xpqKiqLPAejssJSMp
+Nk3LurSsqKyywl1GQTo3OT1Uz7+8uru6vMbgXkk1KCImLDhSz7uuqKepsMlZPTEtKyw2Ts65
+s7Oxtb3K+0xDQUVQZe/Y0M/IxcTDyMvO2VY0JCIoLTpW4byqpaWpufdPOC0pJy1A2Ly0sq+v
+tL3XUEVCRUlPXerPx8PEx8PAzNnkTTcqISYuNlHOv66np6u1e0A7MC0tMD/NuLCus7i7yf5P
+Qz5AR0522c3GxcjKzs3M7HduSz0tJiw0P3bMx7KsrK69XEY9NTM1OEjUv7e2ubu/zOBiTUVF
+SVJr49bRz83Hwb3Cyc9rTTooJCktPmfcwq+sqau86kw5MzQvNET/xLe4t7i9xtJ6TkZFR01X
+etrLwr+9vLy/y3JFMyUjJis5V9O8ramkp7HNTTgxLSwuOFLPu7Wztba8yutOQT9AR1Fq3s7I
+w768ur3BzV45JyQnKjRGYcayrKWlrbzfPzYvKiktOVbHubKvr7C1vNFbQz49PkBNZd3Hvbiz
+tbrDZTMoJCMnLTZHybSpoqOnrbxsPy8qJycsN0/QurGtrK2xusxeRT46OTxBTujIvLWztLjM
+QjIsKCosLjdJ47mtqqmrsLrNTzwxLS0wN0Z0zr63sa+wuMHN5l1LQj0/SFrt1c3Iw8TYT0A5
+NTU2Njo/Uc68tbKzuLzI3GNIPTw+Qk1adOHNxMDDx8jHyc/jZ1hSXF9cWFhf++pwVE5IREVE
+Q0VKVuTLwr6/yczP2etkV1ZZY+zh6unb2t3p+eLd3dnf4+Dq5O93bVxcWFtXUU1IRERGRkpU
+ZODMx8DDycvT3fZ6X1xu8uvt8uvd3uT4c2dq9t/d4N/n6N3q/WdnbHr3YlFHQj8/QENObdnL
+xsTGycrQ4m9kYWvx8O/9/u3u7H9uaGn04efY193b3OPu9P3p+f1tXE5KRkE/QEhSaebU0NDU
+19bc3vZz9uTb2N7n8Onp7vR+/vTh3trX3tvk9nl0a3F7ZFtYWE5NSEdESlFZafXf29TU0dLW
+0tXa3uPl7O76+3Nobm5reurq397Y2eLm8fLp5Hfw7nRkXFFMSEdJSEtRXG1+79/h4djV1dPW
+3N7n7X76/n139Ozz3+Tb4t/h39/c3/XsePP/8mNgX1hPT0pJSktOUVNbZvHj2tbPzs/S19/v
+6evw9fDu++f25unb4eDo6Ojv7evs597j6X3vb1VNRUNAQUdGSU1f7djOysnJysvO1OJzdmRp
+Z296+uXb1tfZ3ubp8ez4/XL36ujp8urt9G1PSURDQkFCRU1d7NnTzs7Mzs7P2O9vbWNqbG5z
+7dnQ0dHT1tXb3+z6dWf08er4fff8d3VbS0dGRURGRElPYuna19fPzMzQ2eXw8fh9c2547N7Z
+1dbT1NXX2d7q7n7r3t7g6nxweG1YSkdJRUBAP0FIUWTr2dbMx8jLzdbj8PLx/nJs9urf2Nre
+3tnY19rk6ePf39/o/PTt7v5uW0pGSkU+Pj5ASVdi9t3VzcjIycvV3N3e6u37anvu6ODe4+jn
+397h6+zp49/e7+3p7uDh821hVkhFSEI+Q0ZFT2du9+Da1c3Oz8/V2NTW29zm7+bl4+Tr8+ng
+5unn4+js6+btb3F8evn1/m5nYlhOTkxEQUVGSVZfXG/j2s/IyczLz9HP1+Pv9H725Ofvd3fs
+5e3s5ejm6Onr/X/+8/Hv5+3+f/h0XU5MTEQ/QUNCSVJefN3TysfGx8fLzdHe5ux9cnpwbO/p
+8fX57Ovv8O3s8n57YVj07uPPzMnKzt9uTTszODY1PUNR28bAu7q+v8bX7FlHRkxKTV9n8s3H
+xMPJz9biXlxTTFNUV27w7tXMzs7R3dzX2dLO2fZOOy8wNTI4P07VvLq2try+wd5PQz0+SFBb
+5s3EvLzAxcreelpFQkZGSlNi7dHJxsXO3eR3XF5lftfQ1NT1SDkxNjk5QEr4w7i3ubzFytlR
+Q0BBS2X24sq/vLzE2PJgTkhDPkhfX3Tb1M/KytHZ7W3s63/q2M3Fxs3dW0U9MSotNDdFbte8
+tLa4u8rX6Es/QkVS4NPLvru+wc9qZFxKSkhIXd7VzszMyc7xXk9IQ0JLVmrayb+9wMfN3F5M
+PjMuLTA6QFLXvbWys7m9xdphTUVET1tp7c/DwcLI0uJvV0tERE5aa3bo0c/P3m5ubWpudHbl
+z8vM1N/X3UQvLjM0O0FK2bu1tLa+xMXpSz89RFlqatrFvLi+z9feZ1FJP0RZX2T46djLzNje
++WhrWE1Zfvbf1NHNzdvnbj0uNzo1P0lZwba7urrBwMZYRUhDRlJPWMq/w7/M3M3Qak9KRU5g
+Ulrj0s3Iytrh62xdTUVPZ2/+7ebSy9ff2Uc1PTozP0lOzb7DurW+v8VdUV9MSVNPWc7NzsHF
+zMPPW1FNQ0lORlD+4NDIycvP9mRfTUlPVVdeberb3N7a6G1rWEE+RkZNZPvUwsHCwMXO0dxg
+UU1QXWZg/dnXzcrQ09Xnal1RTllbZPXazsvLy8vYTj86NDY4OT9X2cS6ubm5wtVdR0E+P0BK
+XdzDvby9vsLM3FxPTkxOUFr6187MzdPW5V5PRUJFRkpc9tzNys3S09j8WExHSE5OT1BVafL5
++N7X08/X19TX3unucPns7Pt3f+fa1tDMzdDU2dze61tJQjw5Ozw/THvYycC/vb/J22lNSEZC
+Q0hOZ93LxcLEy8/ia1xVT09SWG/g18/Mz9TT2+b2Z1tcZXZzbvv99erufGtqXlVQTk5RVVZY
+Wl105d3a19XX2uPv9f14b37m3drb19TS0tfd5uHqe2NeaGFGPURDRU5PXNnR0sjHzMjO+Wle
+VFhWTVJlaezX2NPQ1c3P3d3kcWNVS09cY3zl6t/c3uvv9GpgVlFTUFBYU1FfeuTZ2NfPztHZ
+5ez2bl9ibv7h2+fq4eHj7W5faGVhbm9s9e14/H/38PP+cGdkbvh6bmVhbHBpbHZubG1panX+
++XdreO7p8vTs49vd6+vi4tzW2tzd5vN2Y1xcW15iY2f+6u34bW1ybWxmZm1hX19odPLn6fB6
+9PJ+eH1wb3Fz8OPk4uTr7Obf6X1udm1rbG717v7++/7++X54eHFpa29vcG92dXry7Ovq7vz2
+7/t8a2ZfZn1+7Obq7vB3cH39+nz9/Xh6dm91d3p2/vx8+3prZ3Ty6uLe5+7r7vp4b291eHp5
+cW9uam766uXs7/N9dnp8/XhubWdnZWVrcX7v6eXs7/J9dW5sbG1873twePj6+vDu6/L07urs
+8+vv9ndycmxvcPLu9/t/d2lub3BsZGdjYml2fvHq6enz7/Do7ft+dvt8fvz/b2x4/fP7eHx8
+9v33fXT+/Px8ePz9/vDm7Ht/8Ov1b292cW1qaGZtb3l9cnp+b21ucnz47Ozx9/X5+fj9f35+
+/fzr6PF/9e74/fv/+O/6cvf+bG1nZmVzdHZybXL47+74/Pbt6vdqaXT5+vv29/Hv7/9rb25w
+c25ten587ery/vP9dXr5/Xh4cvz4/3h7/v759/Xv8fD5dHducH1vbWx0fvj57u306/h4/O/x
+/Px29/H5fnZuZWt7dXV5fnd0cnF7/vTq63Zxdmpre/Ly8u3z6ury/fvv9/fu7fT8bmtva2po
+aHZ3dGxtd/35dvnzdm92f31+9/bu7358+PDu/Hl4/fz39/z5fvz6+nl4/v13d3tudPP9cHN+
+/Xz56ef4bmtuefp4dXp29e338vf/9e/3/f17dHFvb3Z3e/t7dvzx+XF0/nN08vX6dmlna3V9
+8vnz7O/+cnT99/Xm4ez78+v2fXz19H13fGxsb3H483Vuevh2ePz+/v37dX18+/T78evn8P98
+enZubGlrbGlrbGtudX74+Prx7fHv8vL19PTu6Ofh5+77/3t3e3dzb2lpbm90eXNqbG12fnZv
+eHl58vn/9fry7O3o6+zw+PXw8/t5d397cn37d2xsZF9ia3t0ePTo8f31+v/z8354cnV/+vz7
+9evs8/l6cnTy7fP7d3p+cGhtbXR4e35+7+37evbz7v1tant3bn/9/XZ99fv/8Or2cW52/Pbu
+5+z6d3ZzfPv29fj3//z4dnpzeHNv8vj8+nNrZ2756O7/fnR/dnl2b2tqc2xteHz/+PT37O72
+6+fr6ezt7ezq7+/x9/j6b2ludnJzcm5mY2hoamlpb3NtbG1z+/Lu7Ozo7PTx7/Hp6u3m5OTt
+7+zq6fh3bGhmbft3aGVnZmpoaW5oZGZqaW199/z88ejg4+Ph5+3u8/r9efXx/f/39vx8dnJ5
+eHd7bmptcXF29/T7eXhxb/75+fX28Pv8/HRwcfnz7/lqZ2dmbn7+8erq9PXw8/r7//56d315
+ent5ffXv6+j1/vr2+X1zb29rb3B3+fx2fP7+9O/n7P78d25ud3v78e/t7e/6dmpuevx+fXx5
+fn13dPT0/3xua2hrbGFlaG3+9+3u7+ji4+nr5OHq+O7s7ff9d2x0dP9vZmVpZF9odXBz+n14
+enb47evq5uX2fHV7d3v4e/12fPh+/PTx+P56c3hydH73f3d2/+748e9+dXT5/m9ub297ffP0
+dnJxaGZxd3308vn37fP08fb3+O/t93j77fd0dv/8/vr7+/Xx9fp5bWxqamttcHV6d3358+7v
+9u/08vX+fnpvaWdu9vR4eX16fX13dnFvf/Lw8e/o6fDz7Ozx6vt1bnBwbm9ydXBvb3Z3e3Jq
+cfz57+rx8e7u8P58ffX1e3VzbXXv83h5ffn6eHR1bGpsc3n/+P11/vbs6urn6erv8fR1Y2Fo
+efn7enx+eH57/vl7cnx4fPTr/X18ffv57v999vtxd/vz8/91bGtobG9vcnR6fXr7cm537Ojp
+4uXq8/Xt9e/1+v52dW/x+X18dnN+fXVscG9rZ2t0fXJqcHR4cX39eXd39vn08vTo3+Ds+Hh6
++fd+9urr7373+Hdxfn9panN3d29pa2dnbX738/l5bm93/O30fuzq8/Pr6O338u32cG52fWts
+evpyaGttbmpx/fD47ePp6OvvfWtvbv99evr/d3h2c3zu7Pd9/n5yfG5sd/n7eH73/nZ6b2t2
+7+3t7352/fX7fnj/8P9ycHr+/P727uzu8Pb8+/V2bGtpcG1sevPz7u5+e/P7dXt+bmpwb3F1
+cvjye3f97vF8eX5+fn5zb3v89/t+ff717e/47O7+9fD8d//v8/J6/PJuc/h8Yl1ocn10eft4
+fffz7/r+/nR0bHN7eHNze/5+d3N0+/Lu6ezz6u9ua315b+7vd35+5+Txdm9vc3x49XZue3Zv
+bnx++nJ1+np9b2lobvz8dH/+cHZ4+fHr5+777er4dHn29Prw6vpucndtanN3fPn+bWVqb31/
+fHV5fnv6fvXn6Ons9/z6+XRkYWVtZGR3f3n68+/p9/Lt6+bo73P29H5183tudnt2fnhu++3q
+9293cWppanVtbH7093t79O/v+Ovo8P57//7x+X11dvL3eG1ubnN2+vtobHN87+3w8+99bmxw
+bm/z9Xr37O/9fHD883F8/nd77uDl9/37fXR5b3FzcXV0cW18+/xvc3ZudHRwdfn5/Pt89PP8
+e+7o6uz6+/pxcv17+Px59PF5ann99P1tcnFnbvz69O3q/XL27urz8u37eHhybXB1cff3eG5u
+9/bz//Z+aW1vX2Jv/+nv8ensfPzy7fvv5/P49vD/fH5z9PD5/Pz0fmt08/f/cG5tZm56b29w
+b/7p83pz/vN8e/v4/vf49v52/u/p5e779vv0/Hh1//1xbW1oYmpsbXh/8n/26PV8/Ofs8v9x
++nhxfX757fHw8+/89fxxfG9vamRmZnp7cXd+/HT27vR8cPf7b//w6+zu5fJ99v5+fvNwbnJq
+dOz4e3j06fn6/HV17+P2ZmZpcXt9emZqd3B1bmt77P9od+/z7/x89PHl3uDr4+H1d/n6em9k
+YGJgbG9sbGx7bmtz/unp9O7x9ers8+z79/p7e3L+7OXu/fx8/u7za27+dmlramJhZ3rz7O/q
+6e59eHxyanD29O34an34dv18/fzv/n/+fn9nZ3J2++fq6t7j/f95ef3v/mhwbW17enJ5/ft+
+cnZ2eOjr6vB7/3R0a2d1fHR38/D57+vr9vXu/3hwdHJhafvt83d+dvzz8ufu+nh56vR8cnB+
+bXT5emx88XX+e/3u73Fy+mp4cXFrde9+8/Lp6+/57/P58f5t/v/y7Xhqc3FvbGxtaHd0dXj4
+7evu9nh9ef7v+3ny6vn3+PJ/b+77eHxxenr5cXZ1bG5tb/x9evbt+W5nc+/s6Obn5+33+fz/
+cHPs7nl5fG1jYWv9eHhsa/t8eHJ3evz5cHrz/n18+/zx6+Pn6eju8fD99Ovu+nlqbm9pZGpt
+bn50fHBlfu39bn3l6/T09nt98Xd+8/7o7nLp+m98a2FkbWz2+HTt7fL8bfvv/nr07+71//31
+7vTse2due3FkZ3Ry9f337PP98/5vdvzk5nFrcv75+3z97e30eHB5empvf/r47fl6d3B3eP5t
+YWt8/m9udvbq8vXq5u3v5/Pt7PTz/G1pam1/a3nt+nv+cfjwb3N+b/Dg/2xobv5+eP337+t8
+b2l5f/T5b352fXBj/PX56+Hh7+/5+31sbvTofnzv+nR1cmZ8emx1e3/4fmdk9P9zfnlpb+z6
+fvvs4vtw8PHz7uns7+79+P1ze3l7c2Zrb3x7enl2enP3eG12dvnt7On0/fbu/W99eG1zcG5t
+aGz16vFscHzycW18++v4+O7u8O3j7uzu/e3x+PPu9nFmYmpoX3BucPf9/fF7bGpu/vbv+P3v
+eV9xc2l2dWtu+O7p9/Pe7e/p+X796vtsb3j/b/rs93l/9/726O7r/GppaWlnbW5w+XJtbGtw
+d3Zpb/vw7+r3+ent7Ozv7+Pf6XpqamJjfXdvd259/vR+9XptdHjw/X36f/f9+vH6+fV+evf0
+fvl6f/du7vpv/m16fmtobnZyfHZ35+bv8ntwb3L5fHZventxdXjv7Xxwd+Hc6+n3efvvc3Z/
+aXL8cHF0Zv56ZP7r8vZ1dHB8amVuaP58d2xy6u7w93X7+uzm6ezr6fh59/B6cXN2a2d2+vl2
+a3fx9W9wfXFvfPru83d49+nv+XxsbWl5+Wxvdm9veHNyfO/o7fV7f+7u7uzu9/Tu9mxjcnt6
+6u3573JpeHxra/v5dXNpdH167nzzfnj9bHxtYWr883Hx5eDf5+vm7Xpsb3Xq7fx7dXls7+9p
+ff5tZGRjZnr+8+xxavv39HJtfvf99e77+H50a/n/a+fsc/bw7PLw9vrt63pu//L7d/l5+/39
+fHl1Y372ZHFuanNx/nX+92h5+XRyfevxfPvv6+5x/ezm7PTr7vpvevNz/e339m7/7/loYWlv
+eurtcXJ1dWRmb3t3bGBoe3tw+er1+nvq7O7p7OPt9Obs7W928u/o9P3+c/htamxrbF5hY19p
+/fR9+Pnz9X/4+Hz5fO/m6OXv8u3u/nF7eG59a2RpZWlu/G1t6uLf9nr+b/tvanV3cnltbH7y
+8u/37ODte+vq/O77d/hva3Nsa3d8d3/teG1/9O/96vxs9/X/82xs9PN0a21u+H5n+u72eXn2
+7vb56/dvcHNucGp46/h2/G185+zv8O7u6nR65ndr/Ph3//ttamVpeHn69Hr5+X90cf9ybmNt
+fvX/+fbs7/7i6vL9+flt/vH893Zsdfh5dGps7+n++e5+/v1sa2p37ff9aW/y/nJ7ePjq82Vg
++PXz8/fi7PP9bG5obHP/bGj/8/Vtfuv3/vj2fnZxd3x7+Xn25evu7vL3+/PzfPH3bGVqavdu
+bPNw+u3y9mtoanJzcP3y9vn2dPnm9W5u9+n6f/B+9nxu8HJp8Hr+93f2+P51++j9fn5/7Wpl
+eXZ/e3j3e3Hs5/Tp3+337fz9dfL5bGNtZW97Z3F58XJlcvVvcvdu8vp2d3zufe7d5evnf//3
+bXhocPdtcenj+3twdP77+vfu7vdxanD8fXH68OlxcfF5bHT57nJt8H9pbnz5eG/u+fbm6O/5
+efl0aml28Xlxb/3te3x0enNobXT86ut8d3Nv4+f8/PB4aG5+fff7bvz5/fB87fDx7nXxfmVu
+93xmbXfp9Xb8duh7bfLq5nt36udoX3B2ZWb+fej2ce50aXX682l17vTr7XZz8PH0eHl2afpv
+e/h+9u/p4u998ur9dXV/fWrw/GJqe/vtd3fr8XRvbH3ufX3xfXV8dfdyd/Nud3px6W9v/vNz
+eObn7Gxv/PZtbev+/fV98vD2bm7sbHXreXr99Xbs6PVq6uZweGl3+31vZntzcnh79XB4d//q
+cnt5b25s7fpyf+3lfX/46+327/by9/VpZvFocu1peHZ2fvP8a/3/f/l7Zf7qbv7r+H7p7Xh2
+e+vo+Pf26/JyZnzxcfJtaXRobGdnYG7p8f9q5t7v7O7t/vTsenFtdf7m8m/0/Xlt9+/7dlxg
+/WVub2r9e/P99P3u3uXk7Wb26H3u4vtu7+7t835uZXdubHFjWGN4bnhxc+9qb/Zx8eL6eOTf
+73rt5+TweOnn+u3+Z3r++O5zdm9faWhq/mVte/lodfr+7HNz6nZk++jt+f1u9+52enhx/Wtw
++ePr/+Pt9fVvfnzv+/r2duz+dPd6cv7p/WZmcG9tZmNsbXJx797h+HDp4mhgaPlxW3vm7+jo
+3Nv9bXV5d3D+e/Ds7+Xz/nlycG5ma2pca2hjbv3u5Pjn3Pzz7/z+/Wth8/v7e//t6Ot5cnxv
+73Jj8fvk+mFvcfF5b/r0eHH++Ppu6utt9un1/P1/fe3y9vp8a2praW3s9m/u+fnvfO/uZmZ1
+e15abH74em/r3PVv8uXi6vT+7Ob69fPr7HF773JnZWpvb29tbvp0ZHTz6nb88/L9aXZ7Z/zk
+9O7u5+H37fBtbHhvaHb3+Htw+fb37/v59PtpZ21paWFib3f3+/nk4X185+jp/Pvy/P795t/p
+/fPr9mtrbXBmZ2ZgbnR2X2T+cX717PJ4fe7h9Pbt6+//e/Pj+Hf07+b3Y2xvZWVhbPN2cnT0
+bG5z6+h5/nvs9Ph38+r98u/j7nd4fu/0bG/v6HRdavb4b3X96Xtrb/X+bnvx7vHu+fHxf3Vv
+a3V+dG99+PhtZ3Z+dXbo6/HqeXD9+vTu9Px6aXP/8Ony6Or9/P3zcGRta211b275921sefd7
+bXV6cn71++zq9n319W778fZ6+/Hv7O/wfPrr6nhseG5dX29veXL9/XN7efvt937y7v1w/PD0
+eGxo8+z7/PLw9fvt73hydnN68e3v6uzv/fT3+3lgaWxmYmZscntxeu/teWd3+Xbr6erm6+7x
+6+vt7O7vfGltbHV8ef577e93e3xvZGt5dnF3bGpyefx++fh2+n726+7v+3lrb/707P17fvP2
+fXR77PV88Ovr6+vv/npxc3RtZGh8b3FnZGVfbHT88e3v+3j86Ony7ejo5+369np99fDt73xs
+Y15eYWlu//x5+/vr6Ofq8evw+Hx8c3Z+//j3ePp9cnj9e25udm1maG1++HZ0/nvz7+7u6urr
+7O3t7+Pl9e/s9Hh1e25pbHh+amlxc21ocXX//mxtbXV0fn52cHjs5eLq6OPh3enq7vl3aGhs
+en95cmts/394d2lka/t9b3J5dHR8//Pq6+/17u79fHd6+e3z+e73+Pv9c2h36eX2fn5zb2xz
+/nt8dHJvdX7//P708vR9bGplaHr59fv78/d+ffL18u/z/fz8/P7/cX7p63x6fG9xfXxzcHx4
+fHz99PDz+/57dnV67+32/vny+Pz0fGtyenz89vN/eXt5fvh9cm//dW3+fnh2+vH+eHJ3ffPv
+9PT17/D9fHJ48O/+dv56f/71+XpybHVveXz4dnTu6unp6/P+/fb8+PVvaWxqa3d+eW158Ov3
+dHhvbXP19vfs5Obr8O7u7fH7/HJwc2xzfG92em1pbGxqa3r1+Pj0+3R+8unh4ujr6fP78nhp
+bHFseHR1dv58ffd7d//y7O/7cXJ/+/1ua2d1fWpscn358OvyePnu8fPq6uXp+3p/f//38Ply
+bXFzb29pZGVoffD2cm96d319+/j38O/v+v3u7PD29O3v9+/w/X36eWZlffV6bGt1ff10cXFv
+d/L1f/52bnZ3cn7x8/L58vj17e3s6uLn+n3392ttb3h0ZWhxc2x3f3ZtbXVybW16/Pv07/Hv
+6eju//nv7u/8+Hx08unn7vP2+3JvbmpsZmBkaWhvfvPp6+zv/HZqcnl2/HltdPHn6+/y+PH4
+/3dxc3j57O////749u/t/nZ2eH76dXd+fn14dH57bGlrbGt9eXL8+O34e/z27uvp6fD09v9u
+aW1tefp2evL1+fv27fP49Pb69ndzb2p1+3dpcfx2bHr+c21sbXX/++/1dvfs83x6fXdzc3p4
+evry5ujt5uLm6uft/XV1aGNjZWppampzdXNtamltdnp3ePHv8erl6+7m5ezw6+3x9/308/n/
+emlpc3dxbmtmYmdtdHx5d/708Ozs7vt5/nt4fPLsfn76fPbo6fX+8PtsamNo/vPy8vv7+Pzx
+7PL07e19cnz6fXn5dm17dndxbnZ7fHhvaW1vevf3/315c3r26uDm7vbs6fb9+/H4+/1xa2lr
+YWFvb3Zze/Pz//bp6O7z7evx/H75e3r+/vl/eW9ucnt8bmhnbnt6enn+7vH1+f5++/f9fHZ/
+fXn+9f11bWZe/tH55OZmVVHb4W/k4+xsUWTZZ1hozszk69PMUkfhYE554GXe32vo3nZvcXNO
+V2xPXnbVaMzW6v7p5kvf5V7v8Vpx2mr0ddr33vFr3nZuXP5oXuxh3OzoafBned5eXtzjZnPf
+5uv9XfPf9Vtn4l9v6FLm8mti7+193XN04W9j9Oj1/29nfe7r3eZz7edl6vNZcvxgXWbr/N92
+V/NsZm5ybXfkbW3n4+Pr7Ovd293X297f7O7f32hx8ldVXllKR0M7ODc5RVzcxbq4urvAx8vQ
+19jX1tLW3dXHvr2+xsTGw8soExUdHi/bvqebn6yrsvRMOCQjLTNQuK+rpau+0GNJVF9c0r+8
+tbK0srdMIxkQEB8za6qfnZqcqsJBJh8gHiNPqqGgpbO/2D4xPWjIuLzDtqqmo6jPJxcSDxUf
+PbSgmpyfp8U0KSYiK0TqtKSjq7TQQz5CQFfZ2tfIvLWrpqWoySUcFw8RHz25nJicn6rmLykm
+Jjm/sa2rr8thVD5DeeHs0st+5cW6sKuqtHU3KB0VEho8sJ6Znai2ejIpJSlAtqipss5sVElO
+ZdzAyE88PEzGramqq7HNQTIoHRUSHsWgmJiiv1Q4KicrNdarpa7MS0VGTeXKxMlbNi45362f
+naOt2DEqLi4iGRghvp2anKxfNDIvLjBDvqqnr+I6O0NIZ8XE2FE0L0y1pZ2dqsZINC8/djke
+EhQvrJ6bobpMRDgrKzfQqqKryzsvMzhSxba2zj0vOsirn56lvzsuNVe9s8geDQ4gv6CanKm8
+Xy8iIC3aqqCktEcuLTNLvqytyDgtOdasn52n0jEqNv25seYeERIfy6Wfn6a0Vy8lIzO8qKOl
+uDcpKzBVsaWr1DMoL9Won6CrzUQ5OEvRx0gnHBkfPLWsq6qtun86MDZNybS7TUJKSFTGraet
+7CwlL9WspaesteA6OWPHbzckGRspQ8i2qaSkqcU0KCk5TFjYu7a7wMfBtrPLNCgpNtevpqSm
+sNs9LzNavlIlHRoeK9CioJ+eqecvKSUoNme5sbC0u8LGu8JcNystONq0q6SlrNc2Lj7EX0tB
+ISUoJ0fOtq6wuN1JPjw+TM+8uL/Q0MW7t7jHTDYvOFHFtK2rsc1GOkVJP1TV1DwdHEFUMEuu
+p7tGOEu4scTHubpfOz7erqewbzs7OC89vq6xutJIWOTvTEpFNC0rMTY4Vu3Yvbm/0Mi+wsnY
+3uHV2fDDs62vyz8zMzg/VuDEvb+/zHtSNSIjN0o4PMa3x1dPwa+02FnPwOBFU8G+ycm+usDO
+9GlKNjM+aFdOw7O7304/Ni8vMDpoxMXSyb/G1eDPzvZPXtHI1/3Pwsrf/tHG1lJRT0NOTUrc
+zdzR11hBPUI+P0tJZ8PEy8zEwdt1bGb5Tk3KvMb66MnTTUTZwdxbXVFQTUVO4srNzs/WbEVH
+SD07TtTP4NjV7FBHaOZ6/d3Ly9vf1tx4dNTYZPfH0f9k29xi6+vbzOtLR/ZQOFjaTWjN21lX
+TVBfTfptUmNs89HZ693h5mrPz83Kv77nUstNOMrN5NPkc2tGWFJEbNvcUlTYWEHmvjrj5TfS
+dUnG4XDFPt/ASc+60ui/RFX7ReLF2eXL4WVeVFZW5Gtq0lZf7undWs9MRt8+YV//x13v4Mxc
+wNtowkpnQmTWR2i812nSXVTNcEvhyExZ3elV7GDZbGbcRmxiVljZ1u7NYMVh3sxL1VVPcWtT
+1v/Pcs5bTs1E3Pvg8+LLS8NZW9px3VrkTU1dWt5N1fPl207T3+7tXfBj9Gjm7N/cfcRgRc9h
+U2XY3FHN8Fjf2EnY1k9Q3E9YS75LashadlreY9bW23jNZ2prbWRoy2Pq+PZq0nVT1Wxd8nph
+X9ZPXNhI7WpX3/bMTHXbZkvAYfrB5d1iz0zMa0TC7ULPxTvn2VZHx2s/w/FBycQ8c70/T8VL
+ULtRR7/pU93lZdTaY+TeV+Jfffxj31jkak3YZW5b3uVecODJ8lTI1lr9WmVXXU/O2UTc1mlg
+69fs01BnzUnjV1/haWpu1k3Y7VjP10nF7ldkuUpVu1pJ1XBHxtYzvcw0ZLg9QrlxPL9mSr9U
+Qb/FOeS8R1PHUevQVPnGUmnEW+3a63teWWtrR+fYRHPmXmT84/Zp5OXufdJo2NJW2+hgcV/u
+Yenib/7272lm49vfeFzufk1dzk5IznxW1tdh0+pf0t9P3NxPaelrXHRicu5iaNboZNjeWfXO
+Z/XVfGHaXWnlflrc6FpqeWtS7u5SWGdbTn1dU+Tfa9nY7NbN18/C3MzH3svI39bB2GdPRkgz
+LDc2LS9CRkfl0by5vLOttL21u9bT5VhNS0xa7V7cvcPoy8lUPT02Ly8tLTY9O1XIwr2xra+z
+s7W9y+BtSz4+RkVIdM3Fwbu1vc7STTozLissLSswP1DnvrSurayrrre8xHFLPTo4OTtEb9TJ
+uLKyra29x9I4LC0mHyMnJi1AXsWvq6ijpqqrtMprSjkzMTA3P0vVu7awrKqsrrXD3zoqJyIe
+HR8mKjJMu6yopJ+epauvu3Y7MS0sLCw2SG3Ht62pqqusr7e8VEA9JyAlHB4mJitFyb6qoJ+f
+n6Ost/o7NSskJywwO1HVtq2uqaerrrLC5d5ROjUpIiUhHycuNlfBtqifoKChqLLESS8tKiYp
+LjVO1cm4ra2trLC1uMr85n5HOzQrJScmJi44R8u3r6eipKerus9ONi0qKSsyOkfTwbexsbGw
+sbm+yOrk4O39UEE3LSkpKSszPVvItq6rqKqut8doPzUuLS8zPVHnyLy5uLO1tbe/xMvKz87K
+21xFMCknJSgrLTNL1LyuqqiprbK70E07NC8xOD5La9bIu7S3sqyytbvCx9dlTEA2LSclJCUo
+LjlSy7itqKWkqK63y1U9MCwtMDhGbNK+uLOvr7G3vM13bFxYUU5IPTIsKygqLzNB8MKyq6em
+p6y3yFY8MS0tLzZBac+/ure0tbe7wtDncGrp2dveeEMyLCcmKi04Tte7rKelpKqywms9NCwp
+LDA9WNnBvrizuLm7xcvP3djOy8fK1lo4LCYhIykuP9+9rqajoqavw1U4LSomKC8+47yzr7C1
+uL3Hz9r/9trLvru9wtk/LCQeHiIpNXe6rKKfoaWvyEYwKCQkKTnjuq2qq662x/hSS1j42si+
+tq6vtck8JhwZGh0jL+Suop2cnqSv1TgoIB4jLUm8q6Sip7DEWD47PEdm2MW4r6ysscY4IhsX
+FxsjNcKmnZmZnaW0SywhHR4jLlywpJ+fpq/KRTMvMT7+xrivq6eqtt8wIRkUFhsmPrikm5ia
+nqi9QCkfHB4kMO+uop6epLPkPC0sLjhewbOsp6WnsNI8JhsXFhgjNOGpnZqZnam6aSsfHh4i
+Lkm8pJ6foqzISjIqLTVCz7aup6Smq7pPLyIZFhccKD3ApZyam5+qu04qHx0fJC1MtKKeoKOs
+x0MvKy84R8myq6alp6y9SSwfGRYXHSo9waWdm5ufqrZYKh8eHiErR7Sin5+gqLtYNi4wMz3Y
+ua+ppqiuvkkrHxoYGh4lNr+qopybnaKu3jkqHx0gKjjYrqeioau6yFA3NzpH1L63rquvuMpI
+LCIeHB0kLDy/q6einqKqss8/MCkjJy40XLm1r6qwvL3cTWzk5Ma9vbe3xNPgRCsmJSAgKDJF
+0bqup6aqrK+9X0I6Ly4wM0fda9q2ucnBv8S+vb64uMDAweVHQj8vJSUqKiozTt7EuLGrqa6x
+tMJiSTszNDI2SkpE2r7Hv7Oys7G1t7fD2Nl1Rj8+MyspKyoqLjhH4Mi8ramrrKuzxtxKNjQw
+LDQ9O1XDwLuvra2usre3xO9vVUI/PjUyMysnLS8sNFXkyLOtqaaorrK+Zj84MS0sLzs/Tsa5
+tK6srK6yt73QWkpIQEBGPjc3MCkrLSstQPLLtaumpKWqr7nXQzcuKCYpMTZExrOvq6eoqq+5
+vtBKQEM/QEtHOz89KSUuLCUwe9a/raijoKerq7lMOTQpIyQpMjpOva2qqKOkrLO7zVY+OTo7
+PEVdRTY/NSgsLiw3TVrCrKuooKGqr7fyPDAoJScmKjlXyrCpp6SkqK64zFo/NjQ0NjxJT0VB
+QjQvOC8tRFhQw66tqqWprq68WEU6LCkrKS49QnKzrKympKmttMnfVDgxMzEvN0dAPk9MPkVH
+PU1oWfPCu7u0sLS4u8reekM3ODg1OEdb1ry0sK6wt8DSWkA7MzEyODxDadbKxcHCxMrS0uBm
+YFdOTFJSU1pVU1VXWl906d3WzMbDwMLIzNX/WU9HRERHSlR44dXPzsvJycrO1tjedVhUVk1M
+TUlGQ0RGSlZs6trOzM3Ozs/O0+dyaVpRTU9YWl5u59TOzMbDxMvR5d7M9urY4PHxblVOSz86
+Pj03O0lNVeDSyL68wcG/z+VuXUtBQENLSk/0zsrGv72+v8nJxdff0dZ6cdl3WVxNRz0zMzk2
+MDVIUVfWvbe0tLa0uc1pX0o5Njk6QktN4L++vbSztri8xMzdW1tZRkdPUlxZQ0tMMTE+OC47
+UU/8yb+0rbe8srfV/PRLPTg2OT9BTdnAu7ewrbC4vcbcUz88Ozk8R1d8997NUDdITS4tRUc8
+TNO9srO5rqq44M3ORzU0ODk1OV/Ox720ra+5vsDcTEdGP0FETt7V3sPJUkU8ODctLDtHPEfJ
+trOzsKytuc/b5UU0Mjk9ODt+ycvCtbCzusHEzGJKT09GRE586eHMzldNQzEzOC0tPUZF6rmx
+r66srK+60/NZOy8yODYzPv3XzLuysLO6vsPdVlJUTUhLXt7Z1svTWkk9My8tLTAzQXzKu6+u
+rKyusr3RUjw2MS8xNDpMedC8tLCvsbW5yOdrU0hGRUlk5vThztVMPUMyLDguK0R0UtW1rq+x
+ra+3v2xCQjguLjQ8PkvRvLavr7CutsTP1WBCP0VJTlVn1stvR09RMic2PSotT+PWv7asqqyy
+tLLCRj1AMy4yNThP3M69sbC0srG7y9V7T0U+PkRKU13cy15EcPM1KDVcLipfyd/Dua+qr7u7
+ts0+PUk3MTk8TNnPxrKvtri2uMbhYkxAOjg7QkVIYdTJyMjE5UM8SzImOzYx4lNKrqy+sKmu
+vcXSVUtCMDlPPD7xy8W/u7i1ucbO0GRBP0VEPkNcbfng2cjJfuXUPTRMOis0PTtKX16/sr28
+sLW9v83f2l1FT2FNTurPysXGxb/J2fB9bElARU9CP09bYHpWXOnWaHba4utWU2NOSU1SXWpc
+XtfR7dvJzdTX2tLU3d/Q0trX3tLW6XzW419TbF9FUlBOWW9WdOjy6GPj5nN5YOxkYFZX42ZX
+TXpvT/Jg5t316vHM3HrO0c/d09zf3e7q/fn6XmlhWl9a6GBp5Gbp5fvg2Ht4/W9lVGhgTmNn
+WXD7dn3m697v7nfm7lRtX15vY+/82N7W19Xr3OzuePBnXutveWj47/Dq8dnr6fV4b3JRWF5Y
+XmTs5XRv3ezp529nZF9qZ3hmZn3lZOXs5/TxdeTm+f7g+fb2cff25ebx3Nbx/n15WFlpXnBh
+bnbr4+bb5fV6ZF5dXXp07vv52/vlfPJialtYU1VZV21l+W7W19LRz9HR1efh+XFdYmRiX233
++vbc2dzh+HlkZ1hWUVhdV19y+tzZ7NzX5nBuY15aUV514+Dc2NnV4tnf6X1iWmVfUVpZZW1u
+5d7a4Nvq6d7x8P7ub29qaPBq7+zf/u1rbXBnXFZoYHBm7W9+/ufj7e7l63Jp+m9q9X/14PTb
+39777vJp8l9ta15fX2RzcOjr9vbj9e/za290dH3y4vjm7+Dz5XXucP1daGlubFxgW/l19+/q
+3/H28+TsdnDs9PT55OD08PPrbmpqb3JjdXD+aexc+XX3b/zscft4/2D+b/1m6vL37fDj5ujr
+4d7w8/LlfGduYWtrZm9me350bHpw/nN16u74/OXc7G5teHZqa3B8fmtmfPVwe/Hq/uDs7Xvv
+8//u8uludPFx63P7/OlqZ2Vi/mdubX7+buz16Pztdu1+dvBzfW/wf+fw7Pz1d/fnb2/wanBr
+cHP5bnZ9fXl7/m3/fvnw9fB4/fFs+f3r6uLh5P3rb2xzemhpamp3Y2h0b2R8/uznfe7n4vL/
+5+TvfXNxZF9rZnFrfPH1ePvk9/b87fD16vDv7f/q9Pz5bnX9bW5rdnxyZ3zyafh5fm9pc/du
+cGVoe/rvfe7/7eTt3t7g7vbo7/z87edz+vJmZW5+X3PsVG/vanHpYud9anz5a2R87+17fn3t
+9njn6ejs5fX853hpf2dc6fr+7vlia3pjcvhrcev76e3mbvnref3ybv17/npyfW3f7eP74fZ2
+aWbt7PZb619vZ2/rZ+5w6277XXJ5eOt7e3Xqbvd07ujweX3++exz93t0cOzn/d7h7u/3bvZ1
+c2N6Z1lv72JkaXzp7m563O1lcPPe7v73+fZoZ+/9aHn8am1udXj66+rx8uzjfvn19O32+3n6
+bmj17PX17vl5bGhveGxfe3Vi9Ojn7uzq9n7+9/p5/eptZ2hsbWBdXGBfWVZVW1df7+PZz8/J
+xsvKysvU5O3+bmZgYGRp8t7d9mpUSEpDOjo/PT5FTGjWzMO6ub28vL/G0+n9XExMUE1MTFFj
+eOrSy87LxsfHytrf22RIQ0Q9NzQ3PT09SXPd08i9uru+vLvDzt3saFNMTU5HS1pt6NbKw8DA
+v77BzNLSekY+OzUvLzQ2NThDZN3Rv7e2t7a0uL/IzeRYTkpGQUFIS05Z3szIxLy7v72+w8/6
+U0c+ODMyNDAyOD1HXtjHv724tLa5ur3CzN3tWEdCQ0FDSl/+69fJv76+vLzCy9ZnRzw2MjEw
+MTI2PEVQ7sm+ubWztLW5vMHL3/VdS0lHRkZITVZu28zEwL68vr/G1vtTQTs1MTIwLzM4Pk71
+zMC8uLO1tbS4vMPWelhLR0VDRUdKV2T/2M3Evr27vL/I32dYQTg0Mi8vMDY9RFbaysO9t7O1
+tre6xM7jX01FQ0RCREtRXGzbzMa+urq6vL7G41xPPjQxMC8vMDc9QVDaysG7tbO1t7a6xszd
+YE9JRkZAQUlOW+7PyMK/vr6+vcHM52JKPTUyMy8uMDg9RlLbxr+7tLS2tri7w8zUdVJKQkBB
+Q0pPVGvg1MzGv7u8vr7G1m1PRDszMjMvLzQ8RE/xyMG+uLS0t7m6vs/a4lhLSEdHQ0VRUlbv
+08vGwr6+wb/Dzt5YSUI4MDMvLjM5P0xd1sK/u7e0tbi7vMXW2ftPSUhGQ0JKUFNn3M7MycG9
+vr3Awcrpak08NzMvMjEyOT5Ha93Jvry3tbe6u7/I3O5+TkhIQkJHSlZjYN3My8S+vby9v8LJ
+33tYQTk3MS8xMTY8RFni0cK8uri4uru+yM7lXFJOSkhIRkxOVW/hz8fEwL/Bvb7GwtB0WEA6
+OS8tMjAyOT9T6ti+trm1sLS3u8PH21VTTUA/P0FFQ0xrf9/Kwr+7urq5wsbF509GOzQxLS0x
+LzM/TGTOwbmztbGvtLi8ytlhRkhBOTo9PUVPaNfKwbq5uri4ub7IzNtXQjk1LywrLzEwOkxq
+18K4sLCxr7G7v8l9T0U7OTo2OUJGVdnLwbm1trS2uLq/xdJjUEU2MDMuKy0vNDk/XMvCu6+t
+rrCwtb3M9U5BOjQ0Njc8S2jbx724tbOztrm8x9PoYEpAPDUxMS8xMjM8S05uxbq5tK+wtLe8
+xt1WQjo6OTI3REpL9MO9vLSwsre8vsbcZmhYRUE/PDUxNDk0Mj9STVPMuru6s7G2vsXL5FJB
+PD08NzhJXmTZv7i4trO0ub7I1OBiTExPSUBDQTs3ODk4OkFLT3zMw766t7m8vsjiZVhJPDxA
+P0FS6tLHvrm3trm+wMfYd2JbVE1OU05IRD88Ozs5Oz9GT2PdycC9u7y+vsTR7mVSQ0FHR0xe
+4s/Iwby6ur7GytLsal9ZVVdZVExHQjs4Nzk5O0NOXejLv7u4uLm7v8jZc1ZHQENCRU5n3c7F
+vr28vcLJztfsfG9gXF9aTkxHPzk2NzY3O0FPedDEvbq4uLm6wczdaVFDPj9DR1fz18rCvr69
+v8bLz977ZFtaXmhoU0lCOzg4ODY5Pkdb2cjAura3ury/y+RjT0U/QEBDS1/nz8O9vby7vcPM
+1uxzb2xraGxuYE49NjU2NDU5PUJa1cW8t7e4urvBzuJkS0JBQUFGU/3Yx768uru9wszZ82Vh
+WlZec/1xXEs+OTg4Njg8P0Zc0cC7t7a4urzCz/hXSEFBQEBIW/fTxL++vLy/yM/Z9mVcWl1f
+cu3wbFM/ODc2NDY6PUZizsK8tbO2ubvC1PlYRj48PT9KX+PLv7y7u7u/xs/nXVJZWVNacO3u
+9VtBODc3NTU5Pkdozb+6tLK2ubzE31lLQTw8OzxIYtzFvLm4tbW7xdD0VlNXVlVd9d/ib008
+NTQzMTA2PU7dxry2s7G0uL/QdVBGPjk4PERV38i+ubWztbnAzu5aS0dKTVZ84d/qZE9ANzMy
+MTI5Rl/bw7q1srK2vcbWYkk/Ojk7P01l07+5tLK1ur7G2llHQUNMW2r8283XaEk6NDAvLzE6
+T+XFuLOvrrC4xNheRT05Njg6QmjTwrexr7C2vMbWY0c+QEVS7t/aztb3Szc0My4uMjhH38C6
+tK+vsrnE5VVEPTg3Oz9R5M2+tK+vtLrAz/tNQT9AR01d08PBwtdTPzYvLCsuND1azryyra2v
+tb3NZkM5NTQ3PEFXybu4sa2vtbzOaVJKPjxEVGrPwb+9wtpKNzIuKSouM0LxxrmvrK6zu8z3
+Tj02Mzg8QFHUvrevrq+yt8LdW0g/P0FJXtfIwr7A1VdBMi0sKywwO07YvbWwra60v89tRz05
+Nzg9THjMvrawsLW7wMxtSUFCRklQatPDv77By25DNS4rKiwuN03TwLewrq60vs3tTz86OTk+
+TH7PxLq0tLe7v87zU0hHSEpWe9TFv8PIz11GOzEsKiwvN0Npy7yzsbK0ucHUaFBHQz8+R1n3
+18a+vLu9v8fS611TT1JaZOjPysvO1nlMPzkyLS81OD9N6M/Au7m3uLvFz95kVExGRUxebe7X
+zMPAwsXIzNbmfWtncu3m3+j5bltNRkA9Ojg5PEBIUXvaz8vFw8PBxcnP2e79dWBfbXRx5uDj
+2tTS0NLX3N/h3dvZ2+p5aF5WTkhFQUBAP0JJTk9b//Lj1c/Qz83P0dHX3N3b3+zq9Ozp5+Te
+2dvW1NfUz83Oz9ToblxTSkQ/PT09PkNKTE1edXjh1dbWzs/X0s/W1M7T3t/l9O7f4+ne3eDg
+3d3Z09PQztPe62tWTUc+Ozw9PEBHSlR09fPXzs7OztLWz9fd3N7p5d7v7+Pf4OTh3tfX2tjT
+0dPQ1dztd2RUSD48PDs7PEBFUHvp6NXLzMvLzc/Pz9TW4eTi8P/67ODZ297e4OLg3Nzg39nc
+7XNvaVhMR0I/Qj8+QUlPWXDz38/LzMzLz8/N0Nre5fLi5vXp3Nzc2Nzc2Nvm9Pn7+HxuZ2Fd
+X2JQRUFFSEZFRk5hfuzk2dDOztPY3Nvc4ujv8fHh3uLc1s/R1dbb4u/t9nRs/nhkZ2tlYVxd
+ZWRdU09PT0tKT1FRXWxqb/Tk49/Z1NPSz8/T09LU19rb3N3g5+rubW96dX327HxvbmNaU1hZ
+Xl9eYFtYWlxXVFxdXmFbWmb6e+/e2NXOyszRz8/Y3+fl4uLt/v1se+fi7O3u/nj+bV9cWE9R
+VFJZX2hqcHt29On5bGNlZ2xpa2n46u3n3dna3uTi6OTp6eXu7Ojm7OPi4+ff3+t/Y15ZVFpe
+ZGh3fn5xdHFsZGJkXVxcX195em7559vk3tzu5t7i+vvy8+nz7urn6+jl7/Vzb3BsbWd8enZ9
++P9zcH1xYmdpYl9udn5xfXru5vTg3N/v7Xp6bXduaGx7anF1eHTzdXJuc3RjeXnx+vPi29vq
+4t/f6+//a252bm36/3x/8/fw63ZubWdeXmJfZGp87uzu7Ov7a3Z8b3F+dfp6/PPt3uHveu77
+/Hl+bGj7/fl57vT0++7j73/u4n1tcX5vb311eW158XJqe3Jf+2Zr7+d26/t+eebzeG5obm5m
+Wm95YXbr4ufe6uTk3OfY6Nvx8+7pb2F+b35gVl1WVfNhbH3n5uj08u3r6nR+a375fevy82/w
+b2N1/fB1+Wx7cPRpbnjk6nfy9+vs5/vh/ux6Y3Nbc3T6e/Lhcvn76tzq+3n5eVxbaHv4avlx
+d+lzZHJ0de773ubv+eZ8fvh17231bHl/fWp57OnwceXt83ZxbHvk/HP35XXqcXT09mlb5PJ2
+ZGP/dPZpeOrjbXL1/f9xb3/r93597ez3bW7o9H95dPd6anH17ufg3+Pt8PHx9W9sbHBwZWRl
+Ymxvb3Z65uNzZmlqXVpZW19eZG3t3dzZ0tDNzs/Q0drd3t7d2NPV0tjW0+hSOS0uMS0rM0n+
+yr62ramstrq/20k7NTQ3OT1N1cC8uLW1uMDXdl5PRklRZN/Oxb/E20w2LSwqKCs3UtK8sKuo
+qa65yGg/My4uMTVAbsu9s66ur7fBz/VRRkNDS2ndzr+7w851OikmKSgpL0jIsqyqpqWsvetG
+Ni4rKzFBZtfFt6+zubq9zGxKRk9SSU7exb6/wbu7z1xDMScjKCwvP8awrKqrrK/ASTQwLi4x
+OlHHuLe3uby9zGFcaF1QVGL86+vdy8TGxsPG3FVNPi8oKS82PVa9r62xt7m+/Tw0OD4/Rl3K
+uLi+wL3E5UM/8vlJVtbN1N9l2sPN3srCzt9uTEVLOCYoPko9aLiws7rL0sZgODpLTU9v6ci3
+uc3Vz2hIR0tX2c/fz8XL2dHP3dzb3tLM1+z/fmdCLCIrQDs1bLWwt8DLvr5jPEnuZ05Secm8
+xt7Qy/VKQ1Pb2+rWxcDI3fvk3HBd7s/Jytl2dWRCLCEoPT84ZLOtr7zPy8N8PT5RV1tddci5
+u8jV5V1RW1ha08bMzcjL0XlPT2FfVWbSw8PWaWNqPigkLzw/TMqzrLHE1s/SYEM/RVr4duDI
+vLzLa1Z5alFg2MfByNne6FtIRk504dnIvL3Q9mFKOSkjKz1MXsCwr7S/0trhVERCSFfz5NnI
+vsHcV13laFH6y8PG3Gz56k9CTPjPz9HIv8DN/ExJPiohK0JOaMK1r6++4N7gWElCQlTs79nE
+vr7NYmbnXk9c6M7L4Hjk2fVdWWba0NrdzcnScllRRC4mL0FGVc67tLK9zdPdb1pHPkZga3/V
+ysLH5mjm+WByftvM0+/n2uZvZWrk3N3XzMvSeU9USjYsLjlCU2/Qvra2usbW191gRj5CUmhk
+XfTRyc3OzNHQ0dftZ2FvdWhjYWju4+7v8fLo72dr7e1hVVlcWFJOSElWX1pZedjLyM3b3trg
+92RTU3Ta1tjWzsrP81xYX3xuWl7m1dfc3d3b33lodutuT01UUEpHRUFGUl1l4s/Nx8XIz9vj
+72tWT1dy4Nva09HR1/JpeOt+a3Tk2NTW3ebo7P11fHpYS1BTSEJGRD5IVVv219PMxMHGztvr
+Z1JIR1Bx3M3Hwb/Cxc/jb2heW2VsbO/b29nX5/nqZjwwNTk3PUdMesS9u7i7vb/OUUlLPz9J
+SEhsz8a/vLy7v9RtXFBJTVJXetHKxcHCwMTYVEA8OCokLjk/9cS9sq2yu8bmVk0+NztJXN7N
+z8e8vcjP09babVVVWFxdafPb1NPQzMfJzc3+R0I9LCUtPUFixr21rrTBzepRSkY+PUpo6c7J
+zMXE09vY6evp/O3e3eHd3d/c5G5ecuZ6XF1lXk5bTzpERz5JTk750tfYz8/OyMvpXVpdd/f4
+3dPOycrT2tTZ3uD1cO7o+fH8bmxsWVVjdHhuXlZgf3p3bko4OUA/QVPcycLDyszR29rje/Dh
+4O7r3NLOzM3T1NLZ529dWV5fXl9dXV1kZmnu72pjWVdcWFdkevHl4fVkXmhlWU5MVGR5+W//
+19DX2drWzMrMzs/O0d75bV9kc3z8/HZxaVteXFtmaV1dY15hXl1iXl906+Lm9nxra3Z2dmt4
++vjx9O7n397Z1tjd7vd8enJw8t7s/OJ+YG9nXGrte3tt+d/r/+99aWthZ/x4ZmZueW9reXtu
++/Hk6Xn/fnxlbHBqbm/0eHXi4Pvz9u5naHxx4+Tn3djZ3dfcfHJrYXP2bWFsb3Pj8GBeaGRb
+YmNjZGl86X1qevHp93Xq3uv59v1vdvd3eXrt6X598+Xg6unm+29pbHF48vzn4+zq497o+PF5
+W1tbXWVs+Hr89ebmeXFfXmVYWWVq7+Xh6fnxeO/6+fPy8O7g3uLs4er8dHvx6ujt+vh9evpq
+W1xodHH17f9wfXn85X5veWdYYGhteXh+8+ftePLt+XP0+v7s9evc7XF8+e5/9fVxau92Vl7+
+fn395t7k6PhuffN0fPdt9ent633q9WR4fG9982pq+nZleXBaXWhta+zq7vjq7n54b3f08Hb9
+/u3m5XFp8el5Zl516PHz393n7/z+9fp8+HN/73Jt/Xd2fV9jdG1qcPLt/HBrb2xfYXny9/t3
+b/D2a3jv7e3l6nno9Hfq4OP07vPt6/xrfO1zafbsfm1pY2hzaW58b/73fP5zd3t4c/D1ZG3r
++XH46u3n6vDseml/6XR18/h8dG57c2dmfu7q/H7p6vXy/nNpbWpmfvx6+O/z6un/+fL7dnlo
+aXZ87fP4fXZ7fXRmef5/dnj7cP7w7n1ud3/5ef3yd3/47+35cm95c3Bvb3X56efufX3o4Ov4
+5eb3+e76ZFpUUVFTS0xbdfPo4drQzdDS1Nvg9fL3c3fz7enk4NzRzMzOztPd6002KygsMDM/
++b+wq6utrbPFWjkvLzI4P1vKt7O4vL3E2VxFPkBISU1k2svCvb6/wcXH0WtMOSskJywvPu68
+rqmqrrG530AzLi4zO0blu6+vs7e9xt1OP0FJS1BcedXIx8nFwcnV0e9PQzQmJC83OWq7r6mo
+r7i41zowLi00Q0Vluq+vsrzT9GpDOD5V4M3N1cm/x9r0+vl+WlLmzc7W90MsKzY1NErQu66t
+s7e41kY8NS80PULourSzsrnKcT43PkJFVO/IurnCxsnafFJLVvDk3cS9y1kzJywxKy9oua2q
+rbGvumA6MS8vOD1Svq+utLrC1kszMDxISnjGubO60+vcbU5MV9bL2tC7tclELCMnKSgyz66p
+p6qur7xKLysqLTdC4bWsrK+1wv5DMjU+Q1/Pvba3xvZjVElBQ17WzcS/u7fKPC0mJSgtOdit
+qKiprrfMRDArLTA8Wsu1rq+yu9BMOzg8RU9yzbu6xtzvY01CQ1nUycjHwr3D3Vw9KSEqLy9L
+uqumqLO+vmo0LS0yQF/lva6tsLnOXEM7Oj9GY8vCxtnVzVhHTU9a/OXZxcLHw8jQ1l42KiYs
+NDZHvaqprbe6u9s5LTA7Q0VWx6+stMLR4lE6Mzxe+P/Nw8/X4VBFS1Va7dTIvb7Fx8nPzdo+
+LCguMy08ya6rr7iyr803LjA1NzhGxKyrr7a5wE4wLjQ9Qk3auLC6z+TeVD47Q2jWx8O+uLnH
+71pZOyclMzk6frmtqrDBvsRDLi8zO0pU0bGqrrrF3U00LjM+Vd/HurK1ymdbTT87PVvMxcW+
+u8DK51Rf62xKKyhHPC1Dvre0t8a4smQ1PD84OUNtuLC8v7m97UA5P09HReS6uMXIxNhKOT5S
+YWDdvrvF2fbp4Nv6YuFJLSswMjVPzLmur7a4vlc5ODc2PFXJtrCzs7bDXT44NzhAWM28ub7L
+zn1DPUFJWW1w1MLI0tXg69/db1t5dTgsQ044QtjBu7zRyLvvPUdbV1Fl3MG7xcO/x2A+P0lO
+RUXowsp03cLG+01b4/dLTs/E1+7SvrzMRzAuNy0pPMm7vLq0rrLYR0ZANjM7V83AvbWvtsbf
+X0c6MzpOYmrOuLnH2tbZVj89TFtOW9PDxs/NyMbK515fWTooLkYxNeG7uLa7vbPEPj9dSjo/
+7b64wL61vPs9OT9APD/gvry+vrzHdUpFREZHT9/Px7q60GxCLC8yKjTfv76yrq+xxU9FQDEt
+NkvoxLy1rbC/1vRIODQ3Pk1hz7i2wMjD3EhCQT9CRVTaycnDvLq+zvhkXT0sLT85M1LDur3C
+vre/TEZfUEA/VtLL1c+7vM9zXFpUSkBQ2Nlp2729zdvMxdpOS3X/S0/gekg7OEI8NUDh0t7J
+ura7ys/MYjo3QERFT9u6srm8trrhSENGPTY8W9PTy725weDtdkk9PUVRXvjPwL3AwL/K+kou
+LTssKkfP0sK1sa624OvdPjE6RUlQVMWywsW0udZoVkpMQj5X7PXUv76/v8PJ3l9KOzk+PDs9
+Uc/c6MnD2llUU09KTHPX3N3Iyuf9altcdWvuzMnFw8zO1upoYGRca/ng2uHs+fZgVldSUE9W
+XWzt9NrZ6d74T0xOPj1LRkhf+tzJyci+xdXT1+hnXW33dvLMydTR1ftiZ19cYGXx4/Tl1tx/
+dWhYUFBQTFNcVGXm5NnfaXNZS05ZXE1Ye/bj2M3Izt/d4ndz49je3NLT3N3q/PZrXnfqWlJu
+82xoeebqaGH7b11ddO9t+dzc2uVlcFxLS05MTlhZ7uLdyszb3OJxWlFeX1731c7Pz87Q43fv
+5f9hdulnY/Pi5O7z5OtjXmZcUlpdXGj05+t/+n1tb2tdWGFWXH5u7ufc2Obn2+ljavHvd/Hf
+3Or3/vD3Xmzm7O/h2trb7Xx1YWlnW2/zZ2B462xk5u5mXlxbWVZgfGxw9u3v6OXd2t7j4OR8
+b/Pt9e3n73Frbn5pX3l6ZWJdeuj57djU6/72bl1ddW5t7OLp9f3r7fl0Y2RaVFdf+/Po3+/7
+6Hxdandmb/Xs8Ojc39/l49rd6ux+fnZjZ3VnbW5iYm53bf57f/xrZm5iXW/76OPx5uJ4de77
+eHv2729t6ux/fPnycG7u92Vs8Xtx/ejY3vfq5m1td2dxZ2ztdHPz+HD+8nrzfWX+cFxqeG/y
+6Pl5fnZ6/O3e4uzr731tdfT2/W1ucmps/Xxpeev3bvT2c+7t7N/p+eTo8vH7eHpzdfd4c+z4
+aXF7Yl1nY2VjY/32+e3o7O7z+unu+/Lr3/f+6/n9enZ9/2t97Pl8ffT8aWBudWn33+Pp6O3u
+d2l6fHNpdHdsbWz++3Bkb/tqbfjzbG/v8Ors8ObudvXh3Ojk2d/z/vl8Z19oc2xkb/xvY2Zs
+ZWZvfGhlbvvv6/Lv5+787d7m5+ns63Ju7O1tfHx7bWJub2Rh9Ot1cnXx+m3s6P947unzaWV+
+f2916O5/d/ft/G5+82phbPv8fevc5Wt382thau/0aXL2dGVs5ux7+vDvcnDm5O3u7PN8+3h9
+dHHq53ZvbW1jW2VraXry+XBvdPDj3uX6+nFucn72b3rv9v/5am/w7e/57nhucXRtdO/r4+z+
+eW5scXtydXn77/b9+/3w7evn82tqaWJia/7/ffh5a2pw9vL7f/18e+3h73r5+fD07et7fu/v
+/HxrYW52aGv+eHn9+/1vb/r7em97end+7Oju8vx88n5/+Hdyfev6d3J7fPzu5uHudnRzbHJ4
+9fdubXN9/vX0eGts/vZuaG76en72fXr+/v/y+e7n93p8/PDt6urv8Pf7ffb/d/fteml3c2pp
+ZmpndXH78m9tf+rk5efu7/L9fv73/f78enPy9nv5cXB4/f5vb25maX77+vN+b297ffz1/nn9
+8O31fXl0e/n+8vX++/H2fPTq6fb6+vz+eHlqbGplaWt0ePn1e3p1/fDy8vh6fvHv7/Z29vX+
++fz/c21tcnN3/G5qa3d79fd4/3368O7u7e75fHj47ez7e/d3bGRsfHpwcXp5fP75eWx37+zz
+935we3R57Ovv7PP08X19emtsenb993xybnN5eXZ9ffb4/+/s7/P1fGx+8unq+fj4dW/98Pds
+Z3FsbXn49XL8eXBxdH58fXb+7evy9vLy/Pjs8f1uaW91fu3v/nRyen55+vF8bfXw/31z/fX2
++/X6/vr46/B/fX5+c2tobnFpbHx4fO3u//zv93F1+e3v9u/s+nn+7er2+O31eXZvaGVkbXH+
+8fj5eHJv9fR29e71fH799eru+PZ2bXJ9eGx2e3Rxdm9sc3Bsbvj59u/v7urf3+l9dH767e17
+fvHw7n51/m9kZm9xcXv9eXBsbHn1/nr8fGto/fj+9PT+/Hzv6unr7/f5e29wamZw+vH39vl9
+/Pp7+3dtefL4++vk6nJpdvn38+76dnz9935ub25vam149uXs9n5/bmpubml57ezy7enycHDx
++P31e2tpc/Xs+3d/+3pz/vtwePx1bm147Of2+/jt7u3r8ntvb3B0/3d5bm1ree/z9nh4eP7x
+//98evz7ffzw+nt/cWhqbXRxbvv6//X5cnB9+Ono/nZwev746vD90+1w83fs+uz9bGJnaGpl
+bGr+7H58a3Z5bP/67vV79fb/+e/p+P3z7Ovv7vl5cHX/9nhsdfV8bXr09X38e21tbW97dHZ9
+6vH6/PT5e/nz8vL1eXB3df3z7/X69m9v//Pt6ft4/vz3939vcv18Zmh6dG1vaXf38vH5cGpw
+e3ZwbnZ+8uLl5+fm6Ofm7vD6fvj07vhvb3Nzc3VvcfhtaHZ7c3b+b2l0b2hpc/Py8vHv7e35
+evju7u/49/1/ev/9bP7u8fnw83JtcHV7b3r68uvw7+77ef17fnz+9nR0/XZueff+fXd7fv3+
+e3lwc3z6+W9x9uzv7er8dnv6dm5tb/b28+ro7fHs8Hpzem5qamVncm7+6ev0fvl8b3ztfnJ1
+e3r8//3r7H93/fzt6O9+dHVqa29rb3V4/O7r8v716fp5/n12dXx2c3p8/Xt0c3r4dW/+/vXt
+7urr+3z/+/x4e3lubvz9bGxzfnR+8u3r+vn2/m9y9/V0b359c3f9fXr68ujp+312bGpt9fL6
+6+jufvj47+/+eG9nZ3Z1dnx2fH16e3l2cm50eXZ2/fx1cezq/e3q6O/17/v16uz5e3Fv/vbt
+9fT9c3FrbGpqa//vdnz6eWdr/nB4fH11b3J7ePnr7erp6uzu9318//z7fXP7839+9Pjy5+n/
+df93eHB8b3X7fH57dW5uc2988HVrbnB1+fn28fTu7/fu+Xr+e314dfLv/XZ/+Ph8f+34/PHu
+8fLw93ZtcHNsb3Frbmpse375d/To7Pb67e99cG58/fnz/XN38+/+fHt+dXn16+78fXtza3j7
++m1y9/v56+9+fv3993V39/b9+/Xv8O/0cW99/Pf7e2lpbmR87fZzam1tcf77bGpzev53ePXt
+6OTi3+j79vP3fe/p7PX+fHNsZ3ru83J1dWlqaW5vcfx7efn8cHN/e/zy/X15dHf76+18+evp
+7O70/W1oaWt2dv7xfm5xe/v6/vHv+HVw/v3//fn3evXs8f16eXr97uvvfG96/Pt7d3FsbnF0
+dm5mYm9wbX19dXp9f3Nz//rv9nJz8+nl4ufm493e6vvw7H50ZmVrbGtoZGNu9/p3bnX/ffd+
+++33/O3v9PN4cW1qbHF5cW9tc3X56ejt8+75/Xjz73t2+/T8fnz7e29udm9xd3l7ffDy8u/1
+eHzq6uzr/XN57PB9+vB9bWpyeGxqdXd4cnJ6bmhlZGRuff7z7uvr5+fr9Pfx/nV4++7s7nJp
+bnz37vTu7P90dm5pcPfxeHr4fnBubvns7vhtZ2t3+vl+ePbp9PZ+dW1oZWdueujpfHN29O7r
+5t/k7fh8fvX3fnlzcv33fft3dXFvfHh3aWVsamz69Hpud/f09Ozl6uzr5Ofu9PZ2e3hv/vf8
+eHh0bG97d3J+/3Zsb/b2fGtpa2l48e7v8vT28/f7//58f/f18vn57vr15+z28fTr7f78eW1n
+Zmx3bW14b2xw8vD+/H798/LzfHN2dnn+eP3t+H3+/359c3d5/nH55vX0735vb3jv8X39fn73
+fv3y/mtv9/f48fb4+Xz5/Ht5e3pqce7u/v/4+n5++fN7b2pteHf6/n57ffl2bXf46+93/fnz
+9u/8bGx1/Pn3b2hmZ3JwaXj49ubf5u3r9Xl68ezx+PX0f3t19/f//3lubGxtZ2pqa3FyeXdz
+fHt3/PT49O7u9/Lm6vD88/B1c3drZmBgaXvu8+73fvn27e/s6fN+/P16efvp5/z69353b21v
+enh+eW9sbnl6d3zz/HJ6dnh3dX7v6+zw7/L57+n3eHducW1zdm91+OzwcG397f95fHl0ePfw
+93V8fPrx6uLp8vr18ezo6/FsZWhnY2n6/HdveH7/dGp3d3T/fvT3fPn8/2907/dyeft+ePzr
+7vD18vLt6eLn+fX6+/nz8Pt4fvD0bmprZWhpdn54bmlweXF9+XlsdPbu7PD5/Xd2++/08Onp
+6Ovs8Xt3fu/t+XJ2+nxpYGVud/Xo7fp7cnd1a2t0dXZxcXh89fPr7ejt9vTt+n7v/nn+/Xv3
+9vV8/vj/fnj88P9+cGxrb3F7eHR5b25veHV4+O7z9Px69e/zfn71fP3w7/T67ufvd3V6fHp6
+fP/58vl4dHNtcfx3b/r7dmhpcHj38O3xfHj49/ny+n57fXlqb356cXz99evo5efwe21ocW9+
++/n3+/Hq8/zr6vd7ePf+aGRmaGFnbHZtav7s8fz16Ofq6ex7a239+Pnr5urq8PV6/X5yfH7/
+d3Nrbm9oZW1sbHz093Z5//b47+z06e9vf+3tfXD5dG93+f51fvfz8/nw6u35fn55a291bnV/
+eHf7+uzp+G1493dtfP5yeXvx8vr57+3s7uvp9G5vb2hscG5ubGVrcvz19/V++/f7fP7z9/L8
+fX96/vz5cHFxff7/7OXi9f7/fX1yfP75f3n1+Xx2fX99eXhsZGdrdXpwc3zw9PX8eH75fm98
++3x9e37/fvD5e/z7efzz9/Dt7O18en16b3749f3x7X5yd/t5dX/+b2hoaG90dXRxa2hqfP59
+b/zq8X1y/PTy+/X19vP28fXs6Ozu7+/37/x4/Xt3aGpuZGRmcnv+9fl2dHt+eHl6efv4fXr+
+7Ovu7Pf8e3RrcPX68/Tv73Rz+fNxbG1rb2toc3N76+Pt7vDq7v5zbvvu7vft8nlyb2xkZW93
+bW9+fP/06ePr9vf4/npzdXz9dH73+e3s8/11fvT5c2ppZWFpand9evn79PLw8vPw7uz48eLo
+9/Pr7f9xeHZvb3J3/25s+H9xfvb9bW10bmxuefX8fv328PD47vD9+/50a2969/H69u3u9X39
+/3b+92tu8/T9+/Du+Hh8/v51cHtvZmhxcGn97PXv7/r07O7t6/B2bHx8evH5dW9xfXtvbWx7
+/371/Hb67e7m6+/0f3p5/nt3fXRwc2958/N5dnZ0eHxxfHltbm57/PDl5erv9P5+eXV7+Ozq
+6vZ4en13b/fxfHh8+f59bnj7bWdsdG9z+PLv//19e3dzffj4/uzt9e/o6+np7/52bW57f3t2
+cGpxcPjs9357/HVxb2ttamJob3fw5ezs6+Tk+fLx9nN2+PX08fX4fnj19f9+emxqbm5xeXlr
+a3v5fPfxeWx5e29xcn7/fHb16Ozt6enu7fp2/nt4bml38+3n5vh38/V37/dsbnx8bW1xbWRq
+ffLvf3Ntbm//f2xocf3+ePv8dH7u6+jm7OTm7+7m6fD9/PX29Pr+bmRudnFvbnBybGtuaWx3
+8/f//Xf99/Pt5+X1/3xqaHHz7/rx7vpxbm579Pf49vHv+f51evn8/X1ucvT46ertfXXu6f9q
+ZmBfan9uZGRse/p/7uHq9O3o7+vf5fL99Pf0eXz17Op+cWhpY2NmaWtscHf+eHT99Pfv9f57
+fOjq8Oz0eHN5/PV+c2xu+/z6/Hj07Ofp7uz0fv17d390bXz+fG1rf/P6fHRucm9ufm5od/17
+evr8fHj46+vn7vX08O7s5uj0+39/+X1vbWtgZW1vdnz9c2l0+vr+9PlubfTwfnX/8u7v9Ofq
+7u70/H7+fXB0cG1/d3P8+3VmbP52a250amd09+7u7erm7PLp6e/y+f59ffx1cGtq+O9vanv7
+cmxwb25z93twfP/39Xr77Ojq7/Dxevbn5Ors7vj8e3dwbGhtdWReY2lvb/ftenvt5/N8b3Z6
++vbu6vH37e/1fPj++fN5amhtanV8dPro3/F/9/v6cXr8eHZ6d3x8//dzb/bwdnBzbmhvf/zv
+7u/v+HxtbXx8dXF1cnN+dHB0evPj4vDv7vTv9PP9/PX27efq9X50eP1+aWJraF9gYmdpb337
+9/z2/n346+fx9+7q7OvvfnP+8vf69nVy+n1ve/j+e3h8dHn+fndxefX2c2lobfnx9PF/9uvv
+93lqamtsbXN++/Ly8fr77OTrfXl8c3F9eHt6/Xx9/33v7vPy8v37dGhqbm1pbfn8bnj6/3zu
++mxz//p/e3P7/nJ66+bs8351fPL3/PP19/bw7fnz+HRtdXBrbm5waWpycnX76un3fXv7+u3w
+fPn/dnv+bmlx+evs5uP9dnRwbWn/8Pj/+fL5/3JtbG9saHN5fe/x8/n27e/69+/qdW58d3J4
++/96fPX2cmxvevr27uz1+3j97/Xy7vH18/Z7bWZkcnFlaHFxbfvzcm37+HB4dXh6/fXu6/Tw
+5ujr7evzeP3+fXZzcnZ3dPvx7n358n5xb3hybWprd/t3c3RscH77a238/nZ2eXx/6+vu4+31
+6ef6/vL5cGpycm3/6ujr9Pnx+Hz/fXJmY210+vT9e3dtZWhtbHHy+Xz47uvu7+/u6/HyfXJ0
+/vhzcnh5cnJ3/Q==
+
+--owatagusiam
+Content-Type: audio/basic
+Content-Description: Flint phone
+Content-Transfer-Encoding: base64
+
+/31+/35/fv7+/v3//v///v59/f5+/X19/f5+//7/ff/8ff/+///+/Xx8//9/+Px7fPNyf/38fnZ+
+9vr7//79/Pz/ff1+/v98ff3//37/fv79fv9+/fz//nv+/P/9fX5+/v3/fvr9fH7/f379/P17/ft+
+/P99//37fX77f379f/5+fvz+fv39//1+fP3++vx//f39//39fvx++33/+379/n99f/v//P77ff3/
+//5+/fz7fn74fv39ff1+/f7++////v5+/vz7fH37fvl/ePz//vz+f3/9/v/+fX79/n///n39/3/+
+fvx8/P3++3z7fXz//n/8ff77/H7//339//3+f/p9/v5+/377/3/9/H7+f359/vz/fv349HN3fnb+
+///9//Z+fn98eXrv9/38+vz+/f7+fn79/H59/P7/fH58/n1+ff/8ff7//nr9/33+/v/+/3p+fv7+
+/H/+/H///f58/f79/Hz6fv79/n7//n79fv7+f3/7fX37/f5+/vz//3/+e/v8/v7//fx+/X7//Px9
+//39//7+fH77/f5+ffn++358/f///v/+/v7/f////X9+/vt9////f/7+fvV8ePh9/H99/n//f3/9
++33//37+/fz///78/3//fX78/v39fv/+/f9+fv/9/v58/P3+f31+/vx+/v/7/n38/3x+/P///n37
+//t9ff39f35+/fz+/f18e/v8/nz+/H77e3r+/Pt9/H5++/7+//3+/v9/fv78fn18fvt++33//X7+
+fH98+f5++3r//f1/f35+/f7//n39/X3/fH/8/33//f7+fnz//vz+/n5+/n79fX7+/v7+ff79/H39
+fXz8/P95+/1+/n9/ff79/X5/ff79fv1+fvx/fn78f///f/59+399f/7//X5+fH/7/39//vt+/v77
+c/37fv1+/Xv8f35+/P9+/v7+/33//n5+/f7//3/9//x9ff7+f379/n7+f/99/f7/fv/9fv9+/31/
++/19fv7/fv99fv78/35//P3+f/59ffp8fv79fn/+/nt/+n3+ff1///17/X38/3x+/v7//319fv1+
+/H5/+n3//nz+/f5+/n7+/X7+fX38/X5+/v3/f31/ff7+ff/9/f///Xx8+nz9/n1++/5+/33/f/79
+fn78fv7/fX/9/X1//H39/359/v1+/n3+/X78fn59+/17f/z+fv3/fn///v9+/f5//n19fv38/359
+/X/9/359/Px6/f37ef7+fP7//f18/fx+f31+fv7//P58/v3//n3+//58+vn/b//7//x+/f1+/v3+
+/n3///77ffx+ffr+//1+e/r+f//+/v7+/n19+v9+/v7/fv//ff/9/f59/v1//X3+ff79/fh8d/t5
+dPr/evt2+f179/z8fXt+8f9983z6fHd+evzzfvv++v1//Xx9/379/n39/37/fXt+/v58/f1/+nz8
+fnn7/P9+/f/+/X77ffr9fvz+/vz9fn///f3+/n77/n77ev/+/v1//vz/f/x//v7+/n78/X7+/n5+
+/Px++39+/X///37//H//ff39/f1+f/r7d379fvt9/v9+/P79/Hz9/P///v58+n79//79/v3//377
+/3/9/3/7fn7//X39+3x/fvn//X5+/H/9/v5//v7+fn38/X5//v1+/v9+//79/f58+n7///99/v3/
+/n/8/v//fn7+/n59/fn+fn5/fP5++/x8/f3+/35+ff39/v/+///+f3/+/v3//f/8/n/+/nn7/f7/
+fvv+/v/9fv77fv39/P99/X9/fv39fn78//59fnz9/P7/ff39/n3//379/398/vv+fft8evv+ff79
+fv5+fn39/X37fH7+/379/378/37+/vv//v5+fvr//v59+v9/fP18/P1+/Xv7/v3/ff9++v5/fv38
+/f18fPz+e/z+/H5//n7/ffv9/Xz9/H37fX39/v//fv79/v99/378/f19/Px7/H3+ffv+fn18+/1+
+/P54/fx+fv39/nv+ff9/fvz//P7+/nz//X75fn5//f5+/33//v77ff/9//3/fX/9//3+/v3+ff7+
+efr+fv19+/7//n97/f/8/338/v79fX79/f5+fvt9/v/+fH/8//5+fv39/f98//3++nv8//7/fX//
+/fz9fH75fPx9fn7//H7+ff77//9/fv/9ff7+/P9+fX79/P3+f/x+/f5+/37+//z6d/3+/vl4/f9/
+//5///z///59f/3///7+/X9/fn7//Px+/376/33+e////vt8ffl+/H15/P/9/X7+/vz+fH59/v38
+/3z9/P/9fX7+/f5///x/+35+/H///H7//H7+/v99/v3+/Xr7/H5+/37+/v7/fP3+/v59//79/Xz9
+/f5+//9//n79+397+339/3//+37+f//9/f56+37+/H5+/n9//H3+ff79//7+/X79///+fft9/f9+
++n79fn5++35+//z9ffx8fv79/P97/fp5+X57/f79f//9/v9+/n5++/5+//z///5+/n37/H1/fft/
+/v39ePl+//59+P9+fP59/f7//v7+//7+fX/9/vt/ff76fPt+e/t9//19/P5+/f19fvt8/f78fv/9
+e35++f9+//77/f9/fnz7/f99/v79/Hx+/vt9fv7+/Hz9fXx+//z/f/37/v7+ff/8/fx+/P7+//5+
+ff7/fn/8/nz//33+fv1+/Hz+/P/+//9///1+//1+/X99f37+f//9/3/+fX7//v39/Xr9/v/9+v12
+fXX+/n71fHd6+/j7+v39+v/8/n5+/v7+/H5+/X7/fv9//vt8/n39/n78fX7+/v59/fz9fP1/fX/9
+/Hz//v3//v59//t+/v39fX7+/X7///v+e/z///1+ff7+/n79fft//X59/H/9fX/9/vt/ff1+/v18
+/vt///58/v79ff79ffv//v5+fv3+//7+/v7+/n99/X9/+3v9/P59fv5+/fx9/vz+/H/9fvz///19
++v/9/n19/ft6/Hr8/H7+fP58/f7+fnv7fv19f3/+/v9+/vx+/n5+/v39fn5+f//+ff99/vt+f3z8
+/3799Hlvfnr2//z9/f///v/7/n78/f/+/3/+/n77/31//P///X1+/X9+fv/+//19fX/8fn7+/H5+
+/n19/v7+/37//n7+fv98/Hz9ff79//98/v78fH9/f/l+/n5+fH/8/X19+3/8fn5+fv1/ffz+/H5+
+/nz9/v59fvz9/nz+/3z6ff7///5+fv59/vx+//7+fv5+fn7+/f5+/f5///7/fH7+/vx8/vx/f37/
+ff3/fv1//P7+ff////1+ff3+//99/339f/1+fvz+f37/fP3+/n59/H////98//3+/337f3z9fn3+
+/P7/f/v9/v7+fX77//5//n3+/H39ffr8/337fv7+fX58+v19/379//1/ff7//n1+/v1///59f/79
++vn5+f3+/X18/X78/339ff9/ff7+fn5+/v3+ffx9f/78/n78/P39/35///r+//7+/f3////8/f7+
++/z9ffn9//77/f/9+/t//X79//v8/f39/P77+/79/fz7//37/f77/v37/fx++vz/+/7+//z8/P/8
+/P/8/P3+/vv8/f38/fz9f/78/P3+//r9/vx+/H76+/39/vv+/P99+fx9/P38/P39+3t/+v/9/Pr/
+/P1+f/79+vx//P79/f9+/f39+37/+X5+/f1//P39/P38+/5//n78+/t9fvz++/9+fX74fv3++/3/
+/Xx++/z+/v/9/P39fP5++v3//v37//n+/v38+//7/fv9/f1//vf+ffv8/fz///z++vz7/n74ff3+
+/vr++/39/vv9/H///fz+/P/7/P/7//79/fv9/v76/P3+/H/9//v+f/r+//t+/vz//P/9+v79/P1+
+/vv7/n/8+/7+/X3/+/z+fvr8fvz+fv75/f//+v1++v7//vv9/P/9/f37/v/9/fv8/vz8/H77fvz9
+//p/+/3+/n7+/f39+X5++vz6fv/9/f38/H/6/f/7/X38/Px+/vv9//3/ff/9+////f/+/v1+//37
+fv/+/P7+/n78//t//vr/+v9//X7++v3+f/v6fvz9//r9//39/f38/v/8/fn+/fv+/P1++/n4eXh3
+d/z+ef15e3779f32+Pr9+/z+/Pv+//36/v//fP7/fv7////8fn///n99/n///vv//nx9/v7+ff79
+/n9//33+/f3////9/n5+/X39fv5+fv1+/X59/f1//P59+33+fX5//vr+f/79/Px9/P19+H3//v1/
+/Xx/fn77ff99/P3//n7+e/v+/n7//P7+f/9+/P58/X7+/nz+ff7+/v5+/P/+fv5+f/t9/fx7//v/
+/f5+/f/+/fz7fv5/ff///n5//f/+fv1+/v7+/X1++/5//v96/f57/379/v5/f37/f/79fvv9/nz+
+fP/7//t+/vv9/f/+ffv8fv77fP7/ff9+/P7+fv7/fX//fH76fX1+fvz+/X/+ff78fH37fv7//35+
+/f7+f/v7/n5/fX79/P5/fv36/fz9fv38fP39/v78/31+fv7+fP/8fvx+fX3+/v1+ffz+ff97fn/+
+fn9+/f59/Hz+/n77/378/X7+fn7//X/+f/37/vz+/X37/n58/H38fn3+fvz9evx9/v/9e3v/fvp8
+/H7/+3z+/3x+/n5+fv1//X5+/v79f//9/n///nv++3n6fXz8/v79/v39/fx+/f78/X1+f//+/v5+
+/f3//319fvx//X7+/P//fn58f//9fv/+f/x7fv7+/Hz9//p+f35+//38/n7/+/v7f35//f/+/v79
+fP5+/n/+fn7+//v+/n5+f//8ff1+fvr//f58fv3+fP/+/v5//n39/f/9e/r//v9+fn/+/v5+//1+
++379/n76fn3++v///3z+//59//z//H7/fn3+fvt9//x9/n7+fn78fP59+35+/3t+/P7//X37/f79
+en39/fv+/v77/v79fXz9/n7++37+fX7+fv1+/n1+/v1+/3r+/f39ff/8/v/+fn3+fv9+/v7+/31/
+fv1//f/8/H38fX39fv79/339+/76/378/v78//79fn5+ff7//n5+//37e3n8fv39ff/7/P/+e/59
+//x/fv78//1/ffv9/n7++/99+39+//79/H3//ft+//37/f/7fP78//x8fXz5fX5+//5+/Xx+/n9+
++317+/z7ffh/ePp9fn39fv7+fv/9/P9+/v/+/35/fv///n5+fvr9fv79+/39/H7+/H7//nv//fp8
+fP77ff7+ff/+/f7+fvz9//5/fH37fH18/P7/fn7//P19fv78//1/ff78/P3//n78//v7efp//f9/
++3z8fP9+/v5/fH79/v57/n1++v7+fn76/v5+/v/9/X59/v5+//98f/z+/v79/37+/Xz9ffz+/378
+/P7+/v39/v59//d+fv59fv3+/nv+/H3/fP59//v9f379/37+/3r9fn7+efx/fv18/f33/nd/evr/
+e3P79/74/nz9evb4+fz9+/r7fPx/fP96fv3+f3x9/v18fn1+fPz9f/97/P///X1+/v/7e/79fvx+
+fP78/v5+/P37fn3/fv3+/n59/vz//P58+f/9/v79//79fn7+fvx8/vx/+v94/v79//t7//p8/n18
+ff79fn1++/9/fn5++/3//v78/H59/37/+X5+/fl9/Px//X39/v5+/P79fHx+/v/8fH77//19fn1+
+/f5+fvz6/v3+/n39/n9+/H7/fn1/ff/8fv38fv7/fP7+/f1+ff/7/v39/v78/f5+//t+/35+f//+
+/n/9/f79/3x//vz9/////v99/3x9/P5+f3/+/35+ff3+/v/+/P9/ff59f/r9///7/fz8fP19/f59
+fvz9/37//n79/f58/H/+/nv//v39/X19+/3+/nz//v//ff7//35+fH/6/n3++39//f//fP3+/H58
+/Pv8fv38/n1pXF9a/tjfvLjJ9Vo8Oi8xRklg67qusbGssrm/QDguJzMsLTU4w7Ovr7yxqrbvQjw7
+NywtLkq9ua+4sqmttVVBPzkvKSw0Q1W+uLqzrK25/Uw+PC4pMDQ7T+K4rq+rqq+zzkJBKiovKzc1
+R7Ctra6yrKzEQzszNiwpKyxqu7Svu6+qrsJEQTw3LCotPOLOtbi3rK2uwERCNjktKDg/Rdi+t7iw
+q623wFJCOykxNjA9Ps6vsbGvr622TT8wLTQoKys0vraxsLiqqLPpSEE9MiosLUzPurG6rqqutU09
+OjYvKC03SGHBs7GyrKqz121APjEnMzc3R1m9rrKvrrKvwj9DLi41KjAvQbe0sLK1q6y+UEE6OS4q
+LC5sw7q1va+rsLpIPjw5MiouQmTLt7q1rqytxE1HOzoqLDo7RvfEs7OzrK+4vEU9NCcyLi43Osqw
+sKystcPPX1JLRUVFRUdNUl1w9NvX0s7R09vi7u96cWttb214bnt8eH92dHp2d31zaW1qZnt2fuz3
+6dze4Ors6ff7dXV1cnZubG7+9fX+f3N1cG1lamVldH39/vP0+/L2/XX5enr8ePLp6eTp6+vu7+5+
+b29rc/vzffz9/e/0c3Vybvlzbft29uj+fXV8eHxtbXF1+ff++H7z7PR+dm10/nZ8+n338/L8+PR0
+f3VueHb97Pfr7fzt7vn+bWxybnB7+f/s7+7s+vh2cHhtfXR78Prt93X693v/bGZ4dH33/fHv+PT0
+9Hhz/nRzdHJ+8O/t7nvz93x9b3N2+f16fPjt8u33/n54eXBydHd8eO/68Xh593T+9Xz2eX387X7v
+93z59m9+fW51/f/59PLrd3BtfPJ2/fF69O777ev29PR6eHVuemlt92z1+fvz/3Zv+nlpdW1seXNs
+2Lu721VMSEU3NjZLubizt7mrsfBOOzU3KS0tOr26tre4qq3YZEA2OysvLjm5tbGxs6qs0Vk+MDor
+LS8ywri5tLasrc5QQTE7Ly0wM8i4urKwrKzIWkgvOjIuNjXItbqysK2uzkhBLjkzLTU4zLu+s66t
+rt5eRCs8MC85Pbu1ua+ur7JXSzonOC8tOD6+uLqtrK+ycnI7KTwwND1Mura2ra61uFNSNic1LjA8
+Sr22tqyttLhgVTknMzAvPlO8tLWrq7O4W0k5JTAxLz9bu7ezqau1vVtLNycxMzFD7ry5tKqtucFN
+QzglMDg0T9u9vLCprbzWVkg2KDM4OVbKuLmuqa7FakU8LyQuNTdYx7m3q6etx19QPi8oMTg/7r21
+tKupr9dKRzotKC44P3i9uLSrqrDaSkg7LSkvO0beurizq6uv5EVGOzApLjtH4bu8tausr9xFSTsx
+Ki46TN+6vrWrrq7UQEI6NCkrO1PVvMO0qq2u4khLPDAqLThdzri8s6msr/o+PzguKCo2Vc+5vrOo
+q6/0SUc6LiotN/zHtry1qay1ZUVBOS0qLDV/ybe7uaqrtHxHRTouKiwz48K2ubqqq7V7Q0A5Lior
+Mtm/tri8q6u1eUNDOi8rLDHWvri4v6yrtm5BQTszKysxzr+7usKsq7pmSEM+MS0uMsm8ubfBrau7
+WEI9PDIrLC/MvLq3v6upv2hMPD8uLS4xwbm4tr+trMdSRDg9LSwuL8a5ubS9q6rJeEw1Qi4uMi/H
+t7mzva6qyFhOMT0vKzIt2rS6srmtqb5bVjA6NCozLm20urO1rqq7VVExNjgrLzBmuby3sa6rvVJb
+LzQ4LDYy+rK5s6+urLxITC0uOiszNGG3u7Strqy9U20sLzktOjn2srazra+wwURLLCo3LDM7Ybe4
+s6yvsbxYay8sOi85P++1tbOrsLi/S08uJzUtMj1ft7Syqq23vFVSMyc1MDJCa7m2tKqsub5WSjgm
+MjUyQ2++u7WqrrvFXU04KTE3NknevLm0qq26zFBANiYtNzVM1r26rqiruuhZQzEoLjU5V8a1tKyn
+rMVVRTktJSw0OVvEu7esqa7JXFRAMSsxO0XqvLi0q6qw4UlGOi0oLTg/Zr25tKuprtVJSDsuKC03
+RuG5uLKpqq7XREQ6LygsOk3Vubuxqayv9kJEOi8pLDhS2Lm9taqtr98+QTkzKSs5Xc25wLaprK7w
+Q0s8LyotN3fJtby3qaywbj4/OC4oKjJtyLe6uKmqsO9FRzouKiwz5cK1ubqpqrVkQ0M6LSorMOPD
+trm8qqq2ZEdFOy4rLDHUv7e5vqurt2VERDwwKywx0L+4ub+rq7phR0M8LissMsq9uLjArKu7WEI/
+PTErKzHHvrq4vaqqvWBKPT4tLS0yv7q3tr2rrMVPQDc7LCstMb+6t7W5qazMbUY3Py0vMDW+ube1
+vK2t1VpDMj4uLjIzxre5tLmurM1oSi88MC00MMqzt7G2rqzNUUctOTAsMjDMs7ixsq2ryVlMLDgy
+LDYyz7S6s7CvrcNKSi01Nyw2ONu6vbOurq7KWFYsNTguPDzNtLuyrbGx1EFHKS43LTo80ra7r6uw
+sc5WTCoyNy8+R8S0ua6rtbjuRD4mLTMuO0rFtLatqbW72U5FKS02MD1WwrW3raq3welERCkrNzE9
+W8O0tqyossbrRj8qKTQyPGTAt7arqbLKc0M+Kyc5Nj/awbu1qqu051lHOysqNzlI0Lq4samrtWpJ
+PTcqJTI6SMm8uq6pqrdVUUU4LCw+TUBTvq2ztLy1rslEPC4zLygtLFK1ubCzrae05e02OjosNi9S
+s7izs7KruU1TLi41KS8tQLa2sa+uqrFtZDItOSsxMUW1tbOurqyzY1I1KzstLzZCvbu5rq6vtV1Y
+Nio6LzI8TLq2t62tsrVZRzgnNjIvPk++u7arrLK5ZFc2KDUxM0JwuLOxqau5wEpBMSQuLzBBb7y5
+saiuvMNfWDcqNjc6UNC4t7GqrsbmTEEzJy40NknWubewqKu+6lZENSctNzlRyLi3rqmsvl9OPzYn
+LDk7V8a8ua2qrcBNTj81KSw3QF/Auretq62/SEU8NSkpOEXtvr+5q6ytxUVMPjQrLTdM5rq6uKqs
+rs8/QTkwKCo0TeC7vbipq67QRko8MSstNF3Ot7m4qauw30JEOS8pKzBbzbi5u6qrseJDRjswKiwx
+fMW3t72rqrDTQUU7NCsrLmHEu7fCrqqwz0VIPjQrLC1Wv7y2wq+qsM9CQTs3LCstZb29tsGuqLLc
+S0E+MystLWi5urS+sKm07EY9OzQrLSxOur20vLKptedXPTw5LTMvS7m8tru3rLXxVzw3Oy0yMD29
+ure3t6yx5WE8MjwtMTI7u7W2s7Str+dTPS48Li4xNb+2uLGwrK3YW0EsOi4tNTbCtrqwr7Cu2EpG
+Kzk0Ljk8yry+sa6vr91bSCs4NjA9QsO2ua+tsrT3Qz4nLzUsOkPJt7mtqrK13VNDKTE2MT9Xv7W3
+rKq3vm1CPSYsNC89VMK1tquptsXiS0MqKzYzPmO/tbesqbPNe0U/Kyg0NT1qwLa2q6mwzGNKPy0o
+MTc+d8C5t6yqsNBQRT0yJy89P9vAvrSrq6/eSks8LyowPUvTurqxqqyx70A+Ny4oLTtN0729sayt
+tOtMQzUuOk9W3NfMzM7Q2Nnd3+r3eGtgW1pYWFxcYWdre/fz8ujm4ubo5O3p8nz5/vzx9uzs7vJ+
+9Htxc2txbGtvdHlyfn99/vz+e356fnl5fHv7+fv5+Pjz+vj9/vn9/Pj7/P77+v77/v1+fvp+e/t+
+fHx7fvh6cnJ28X76+Pf+8vz3fv14/Hx4/nt7c/78/3z8e3///f/9/vt3d/f9ef1++Pn5+PXz+/p8
+/Xz093z3/f39e354ef59enx//Xt8fv3+fn18dPf4/fl+fvf3+/t8/357fXp+fPX3//x7+f/7f319
+//16fPz+/Pf9/Hx+9376+H75+31+enx5eXx9/3z5/X17/vx/+nl//3b/en39fvf39fX18vz9+v38
+fXh3e///+319/fn8+f15/Xl1b3B3dH78/fX07Pb78fv5fXl3cnd6/Hv++vj1+vv4935+/XBtb2Vd
+U9G2vutkR0hDNTk3Tbq0sbi9r7HCTDk4Ni8qLDBL4byxtK6qq7HpRD46MSktOEBZyravsq6rsr/Q
+OzkuJDAvMDxDvq6wrqytrLlMRS8xNCkvLT64ta+zuaust2Q9PDczKiotUsy8tb2tqq20V0Q+Oi8q
+LjtLeL21tbKsrLnvTzk5LCc1NTlM47avsq2tsrDcSEMrMzItNzNlsbOvtressNZHPDY4LCssL+W+
+s7G5raquvUY/OzgtKSw5V9e2srKtqqy5VUI3NyslMjg9bce0srCqq7S4Xj07Ji0yLTk45K+vr6+u
+rK/fPzkwNiwqKy/Mu7ayu6ypr85JRz42KywtQt/Bs7qyq66w6Tw8NjQpKjc+Tsy4r7GsqK7D1UU/
+MyYvMjI+SMKusK+usq+8RUYvLTYqLy85urGvr7isq7htQDw4MCorLUjMu7O3r6yvuGw+ODE1NzxF
+VXzczsjFw8PFyMzR2vdmW1RRT09TV1tfZGtwf/X48/Tz9Pb68vDt7Ojm5OPh5Onv+3huamRlZmRo
+bWx1fnt+eX7+/vr8e3z+//r18fHv7O3s7eru7/X3+Pz39vz79X17eXZ8cnBxcnFtcnF2dXB2c3t3
+dX10e3d6/Pry/Pf4+fb59vr09/Ty+Pj39vb2/f/7//z9eXl9ff///fv6e/3y/vz9e3l9fHp+fHt7
+fvx8e/V4/vx5e3p9eHt6d3t6+////Pj8/Pz7/Pn3fn57e/j+9fp/+fn9fv58/f7++vz9+vr//nx9
+/P7+fHt/fPj+/Pp8fX17eXZ4dnj9f3n5fvr7+fX5//v6fn55fnz//v/5fvf5f/5//nh9enZ9/f35
++vz7/fp+fv7+f3h8enx+fvv3+fL2+fj8fXl7e3Z8+Hl8+376fHx6e/v2+OffWT9IZNzPurO20UZI
+PjctLz5a3Lu2tKqqrtFFRTkuKCoyRHC8urepqq3LREY6LygqMUvluLS0p6irw0FGOi8oKi9K8by4
+u6qrrcJARTs1KikvTta8ur+sqq7DRUo/NiwsL0/Lura/rqquv0FBOzcsKSxNx763xK+orsZIST40
+KywtS7+6s7+yqa/KREE8NSsqK0a+vbS+tKivy1JGQDgtLi1Dvbu1vLmrr85OQDw4LC0tOcC6tLe6
+q6zFW0Q7Oy0sLTPDuLW1vKyrv1hDOTwvKy0uy7i5tLqsqr5fSTY9LywvLs+2uLO5rqu6VEY1OTUr
+Li19uLy0tKypul9XMTc1KzQvcLO4s7Svq71QTC4yNisyL1i2urOvrqu7X1wuMDcsNjRms7WwrrCu
+vFBWLSw1KzM1T7W0sq2vr7lYWzErNi4yOlK6tLKtrrG6Z0o2Ky8xQ2zhyMjDxsrM0dfd5PZvYVpX
+VlRUV1tgaG189Ovp5+Tk4eLm6u3z8/j59vTt6+vv7/X+cWllYmFkZmttdv59ffb3/v15+37+9/n5
+8/Tz8vHv9PTu+Pf8+3x7/n31+v76fXt8dnt8eX12eHl2d3l4f/7/fv/8e33+fH1//n19/Pv9+Pj/
+fv79f/v7/vb8/fh8/vr8+/769/x9/P57/Px6fn38fnn7fn59+vx/e/3+//19fP9//3p9f3z+fHt1
+fn58eXz3+ff9+n789/r9en9+fv36fHL8/fn7/Ph9/P79/n3+/v79fvx++3j9+n79+/v9/Hj+fnh8
+fHt4/X19+37+/fr3+n/8+3p+fnt8d373/X74+/57e3v///9+fvv1+/X2+vn4/n79+P9+/Xp4fn5+
+fXh+/nv+fHt8f/r9f/n9+/58/3n//Xn//fr3+vj47+TdTjtP7eG8r7zW71E7LC44Qt6+vq+orLtS
+T0ExKyorW7y+t72tqsTjSS49Li42QL6+tKmtvMZbRywpMzVJ0Mi6rKyx3FRSOy8sLkjCv7i9sKq4
+41E5OjQsMTnmy7+srbfB+UkvLDIzQdq+uKyprMBQTjwvKCk9zsa8wbCotM9pOjwzLjMzzLm9rayw
+uGdNLicvLDRU1MKxqq2/cnpHMy4uPtK+tr2zqK/LXz88MysuLFq6wrSvrq/SYzsoMi4vROnAuaup
+tNdtUDcrKzVPz7vCtKmsvVlOQzUsLCxUusO2sKysxFpEKjQvLD1UyMGtqa/L9Fw8LCs2Rty+xLWp
+rbhZT0c1LCssT7zCuLisqsTtSC05Li04TMHAsKivwddTOyorMTdYxcCyqKq1a15LNiwrLGO9vbe7
+q6rC50gvOywtMDq/v7eqrbXEV0cqKjEwRNnHuauqr9VaXT4wKy5Pxb+6vq2pufFNOjwuLC40xcS+
+rK2uulFOKyk2LT5+zb2sqa3JXGg+MCwvSsy8usCtqbfpTEA8LiwsMMnDxLGvrLd7cy8sNiw4Usm8
+s6erve1xRzArLTlYyrvCr6mwy09LPi8rLC7Pu760saqu13Y6LDgrLzxrv7yrqLTM8E82KS02Q9fB
+xa+rrrpMUkY4LSsv2sDDubaqrMx8Qi48LC42Tr7Er6iwu95JPiYqNDZlysuvqau3WG5MNi0tL9m7
+ure7qavIYT8yOissLjvBxrepr7TFY0opLTIxSs2/t6mpsOlXUTktKy1KyLy4wK2ot+RPPDwuKy4v
+y77Arq2ts2tdMSg1LTVT0sCyqKu8ZXJHMiosPue/ucKvqK7ISEg9NCsqLHbAyLOtqq3YdzgpNyww
+RNy+uaqostZrTDUqKjVG2sDKsqmtvldXRTUtLS7lub60tq2rzulALDosLTdLwcCvqLLK3VQ7Kisy
+PHS/wLGoq7VfVUo4LSssUbu9trutqbztTzA5LysxOMrEvKutt8BXTi4oNTZE0869q6uuzlhlPzEt
+LkXAvbe+sKiz4Es6OTEpLDHxycWtrLC5dFkvKzQvPebBuqynrMVXWT8vKiw/1MS9x7CptttYPz8x
+LjAyyby/r6+utndrMSo1LTVM0b+2qau+8m1JMissOWDGusGwqK3DT01BNCssLXm9w7ayq67UfT0r
+Ny0vPmnAvq2psct2TT0rKTdF6MDNtqqtuFxaTTouLS1Pub+3uK2qvV9KLTUxKjQ70cm6qa2+y3hH
+LiszOVPGwrmpq7DmUE47LiorPr+/t7yvp7ncZDA5MisyNMy9vausur5vTy8oMTA788a+rqmsxF1o
+QzErLDvJv7e/taivz1k/PDcrLS5ev8uxrK+04141KDMuNV3Mv7KoqrlbW0g2Kio92cW8yLKnr81Y
+Rj8yLC4t07rBsK6ssPlfNSc1Ky9G5MK7qaq56/tONSwtOV/IucCxp63DU01BMystLGm6wbW1rKzQ
+7UEpNy0tO1a9va6mr8vfUzwqKDI9br/DtqirtGNVTTktLCxIur+3uq6pvGhRMTcyKzM82868qq27
+ympMLys0OU3Iv7mqqq3MSk08MCorPcfFu76xqLXcYzY4NCwzNNO+vqystbxlUDEnMTA59szBrqqt
+xV59RjMtLTrJvra9t6iw1287OjcqMCxLu8e1rbCx0WhAKDEvL0jav7mrqbLbZFY8LSoxVs28v76q
+q7xsTUE5LS0sPrzEua+trL5STSouNSw+WM7CrqmvzXViPi8tNUvVvLy4q6y37EtDOC8tLj3Wx722
+srTG1145Mjg5RPrVwbOvt+FQTDsvLS5Hx7y3u6+otNNmOjwzLTEx1by+r66zuO5hMSgxLjVWzcCy
+qay8Yl1INSssOtnCucG2qK7KYUU+NywvLV68ybOvr67cWT4nMjIwT/nMuqurtnVwTjcsLTnowLa+
+taeswE9GPjUqKitOvsq2r62tzu49KjYuMUbkv7urqbbiZE44Kys0S9S9xLaorcFjT0U3LS8tW7jB
+tbOtrM1+Qik1Li07VMC+rqexzuVWOioqM0HpvMO1qKu3WFJJOS0sLFG6xLe2q6rE+0orNy4rOUjE
+xLCor8PiTkArJzY9bMPQtairtlxkUDkuLi78uL61t6uqyWlDKzksKzVCx8iwqLPF1VU7KS00Peu/
+wq6nrLtWYEczLCsu0by7t7uorNPdOy46Ki4xTbzCr6i0vdhTOyctMzZlyMSvqau4W2hLNy0sMNq+
+urm6qavGakE1Oy0tLkTMz7SqsLbRXD0oLzI2XMm/r6iqs19RRzcrKDHkxb29u6isxe5DOTotMC9M
+u8S0rLG00VU+KC8xMUvgybisrLj+cE83Li006sG5urupq8LuQjo5Ky0sPbvAtq2vrsNmRCgtLy0/
+6cC5qqev0WJVOiwoL1DNu727qaq6fEtAOSwtLD28xLmurq2+V0wqLTUtPlnRwa6qr85nZD0vLDNP
+zru8uqqqtnZIQTctKis7w8a6sa6svulNLTMxLj1exL2uqbHOcFI7LSsyQu+/wbmqrLr9VEg5Li4u
+Rby+t7SwrL53UCwzMiw4Rsq/tKitw91gQC0pLzxawr+7qqqw3k9OOzArKz2+v7m1ram581ItMjAq
+NUDLwbWorbzUUkItJjA7WcfOuqqrsedcVjsvLi9Nu7y2t66rvX1ILy8tNFNQ29rTz9fa4+jw8ft4
+bm1wcHb36+Pj3t7f4enp7/Pu8/by+f53bGdjYWBiZmpuc3V1fnhyeHT+fP338+/w7O7u6urt7+76
+9Pr4fv36/P59eXh5eHh4d3l3c3l3dXx6dnh6+Pn//H//fvf7fvnyfnx+fPr/+vfv+fj4//r+en5/
+fHt1eP18fP36f3/3/vv6/fz9/n19f317fHx++f9//P7+9vz+fX/8ent4ePx9/P1+fn59e/55e/V+
+/fn3fHt7///+/Pr2+fP7/X53fHz+/f78/Pr+/fh+/vd/+v38/nx7eHl5/359e3p+fn5//f74+vv8
+e3P983f//v/9+f729v79/v59fXt5dnZ7fHv3+n399Pr6+PZ+fXt6fXz7fPz8+vf3+Pz7+vx6enp5
+dXd8ffr+9vn6+f78fX51cnN6fXb7+Pz4f//68+/29Pfr33ZEPkzeyci4trzBTkg1KTUzNEfjtK+u
+p6mvt1BBMCEtLSw9XLy4rqWos79PRi0jLi4ySs6ysqukqrrMR0AsIy8vNE7Oubmsp67G41FELik0
+Nj1sv7a2q6iw21JFOywmLzU8a762s6qnr9lRSTosJy84Qtu5tLCpqK/0SEI5LSYuO0TXurmxqaqw
+c0dGOSwnLjtJ0rm8r6mssGJBPzgvJiw/V8e5v66oq69RRUE3LCgtPW/Cs7usp6uwTD07MyslKjpq
+wrW+rKaqtEtJPzUrKSw91b2yvK2mq7dIPzswKCYoOdW8sLmupam4TEQ9MikoKTnIvLC6sqaqukxE
+PTYrKSo2yb2yubeoqrtVRj84LCorN8W+tru6qqy9U0I9OjArKzjDv7i7uamswFlHPjosLSw5vbu2
+ubiqrslLPzk4LSwtO8W+uLe0rLHKYkI3NThIT3Pq3NLV1tnZ2tve4+/4dGZfXV1cXl9la3F5+PLu
+6ujq7Orv7/x+f333+fLu7O3q7/R+cW5qaWpqbnF4enz79/v9/3r/e3n/e3n+fnj69P719Pv3/PH2
+/vrz+Xv4/Pn+evr8e312fH55/Pl7/Ht7fHt6e33+fn31/Hx+/358+/z9/f79/v7//n59/np9/v5+
+fv59/vr6/v38/P7+/nx+ffz3f3d7+vP6+3v7fv7/ev97en14/33+/n1//P7+fn79+X78fP1/fnt9
++3t+fP/9e/z9/375+/74+/t8+3//fn5+/f7//Hr7/fv6/f///P17fXp7eXl/env8/H79/X5+fX16
+evb+/P18+P35+H39/Pt+e3z+e/99fPr38vn9/Xt7eXRzdnp8/Xz++fr3/H7/+Pv5/fv6+Pp+/Pz/
+/P57fP95fHh8fnp+e312dnVoXlfovrrBX0xOPzUwM1G8ubS4sKm69FcvNjAsMzjNwLurrr7IYEsu
+KjM0RM/Buauprs5bXz4vLCw+wr+3v7OotuxTODo0Ki4w48HGrqyyuHNcMSg0MDvuxr2tqau8U1xE
+NSoqPsvEusSzp7HTXD49MywvLti8xq+tr7VeVDImMy0zUdbCs6irvnj0SzUtLjzZwbjAtKivy1ZD
+PjQsLi1au8O0sK+vzv8+KjQuL0Pvv7usqbXbaU85LCozTNW7v7iprLpgTUU3LS0sRbvBuLSuq75i
+TywyNSw7Ss/Esaqux/ZiQC8rM0J3v7+8qquw20lMOzIsKznFx764sKq142oyMDcsNj/Uv7qqq7zP
+Z0YxKC84R8/IwKyrr9BTWz4xLS07vry3uLOps+FwMjE2KjI1fr++rau6xehNMykvND/hxL2uq67B
+XldBNy8vPNrAura2vs3ZZVhPTExLTE1OUVddZnrv6eTk5efm4+Pj39ze3uHp7Pn/+Xp3+ff5fn52
+bW1nZGlnaW9tdX55fv5//Xz+fXp7f3v49/n28u728vD19f/28nr7fH76e318e/p+ef99fn57fH59
+e3h/9Pf2+Xp7e3z/ff18/v5+fnx6+H79/X19fXt8/nt8fvn+c3j8/Pr+ee/19/148P128P34/vz9
+/v16/n96fXz+e/79e//+/3x8fH19fXV9fv39ffr++/r5+f96+3t+f3t8/37/e/z7/fp/+X74fn75
+//n+/X76fv75ffb+/Pt9/n19eH5+fHt9/nz9/Xx6fX19ff/+/P36fv3+fvt8fPv69/z9+fn7/f71
+9Hx8e3d3ef13eXn/fHh9fP/7/Pn9/vj7+/78f3/6fv35+vf+/n3+fHx8f3x9/f76fvx+/P16e/58
++vfs4fNQQ0FS28u2rrfbWE44LSstWsXGta6rs9D1MC43Mk7TzbKqr8dPTzwuLCszwMK4qa+zxOY9
+KjE1Rs7Nya2sv3hKPzotLzBcvcCpqLq/0U4vKzJG5MjYvqu23185OzgsOEfOw62ntc7WWTQsLD/M
+y8G+rq3Ld0AtPS80VXfEr6u03nheNy4tMsrBwrWvq7ruYSwvNi9R89Owq7HXVls6Li4sRrvIsaqu
+s83vNSozNEnP2r+qr8ddRj4yLDA0zL65pq2+wuZBLCw2S9HH2LSrvuhPNT4vLjlWwb2pqLjP2FEz
+KixK1M7Iw6yv3eQ4Lj0sOl3NvKyost9hUzcsKDTN0ce3rqzB5k4rNzM19da+rKuz61ZRNS0sLW/A
+zK6tr7ncWCwtNTj2zNCwqbTZTkQ5Li0uP73Eraa1vcdlNCswPXvIz8arsdz9Ojc6LDQ+zsCypa/G
+zf06LSs26c7FybWswO5PLzs0MEr9yLaprsTtez0uKy1twsm6s6yz4mcyLDowSPbttq2vw1drQjIv
+Ljm+v7utra7A8EUqLzQ6+WzTrq6+ek9DOC8zMnq5vqqotbzYTy8rMjxrz9+7q7jYXTg8Mi82Sb69
+rKa0x89OMSssPe/Lycmur9PgPTM+LjdKz7+xqLDJ3Gs5LSs12c7GwrWsv+tXLTY1L03lxbOqrcNb
+aD4uKytRwdC4r6yy2/AzLDkwR9nWtquuvVRbQjAtLDTFxr6rrrHC3UQrMjU/29XOrq26eU1FOS4v
+Lk28xaypt73NVTEsMj9+yNbCrLPWbjw5Ny42Pcm+s6auxM38Oy0sOHDUys+0rMXpTjE/MTBF+MK4
+qKy/4OpAListWsrNwbmstOd2NS4+LkF32rqsrLpueUwyLiw6wMm+sq6txX5HKjQ0Nvd+yK6tuG9e
+TzYvLy/dvcesrLC64lotLTY5bdfetKy33FBGPC8vMEK9xK6ntL3KaDcsLzlZzM7Lrq7L+UM5PC4z
+OuLCuqisvs7cRi8sNFnWyc+9rLnheTU5OS09WMi9q6m23XlOMysqRM3Ww7ytrtHiPC09Lzx8zryt
+qrTiXU82Lisx0M7JtrGtwOBWLTY2NnjWx66rs9pTVjouLSxLvs6zrbG4zfgzLjc4W8vXuquyzlVG
+PTEuLzXHv7inr7zE8D0sLzlRz83QsK3C9Uk8Pi8xOW/Evqmrvc3ZSDAsMVPTyc2+rLbmZTg3PC49
+UtnErau55eJYNS8tQ8PLv7yvrcxwRS08MjVdbcmxrbjrc1k3MC4zy8DEtK+su+hgLTE4Mlvq1LCs
+t+NUUzouLy1Husexq7C1zu41LDY2TdPhv6yxy1dJPzQuMTbMwLuorrnA6EYtLjlK1dLasq3C8Es7
+PS4wOHHCvamqu8rnSi8pM1neztS6q7rnbzI6OC1BZMK7qam55/dLMiorSM/Zxr6tst/bOi8+Lj7w
+ybusqrnwb040LSsyyc7HtK+tx9tJKzYyN+3Owayqte1XTzUsLStWu8murbC31PgvLDU2XczZt6qy
+zk9KPzEtLjbFxrelrrnC6D4qLjdOzMzRrqu/cUU6PC0uOWHLvairvs/YRC8sMmbLwsi6qrXsXzQ2
+OSw8T9jCrKu9499MMi4tScDHvbutrdfePCw8Ljdf4sKuq7j2cE8zLiwvyL7AsrCsu9xjLDE1MVbY
+zLCqstVSUzotLSs9vMa0qq6wyOc5KTM1RtDdvqutv1xKPzIsLjHVv7umqre/6kosKjlK39P4tKu+
+7E04Pi8vOV++vainuM3gTDEpL07fysrN0Xi8s8plSDw+LzA5ZcK5pqi6w85BLSovUNnKzrmquutX
+LjUxKzxcwLmop7rN2kcvLCtDxMm8u66u1ts8LDsuOFzUv66qt997UTUtKy7YxMazr6y632stLzkz
+W9zSsaywz1JdPC8uLT+8yrOprrHUbTonMjlI1HXHrK/FXUpBOC4zN8++uaaru8TtRSwrNkjfz+G4
+q7/nVjU+Mi88Y7+8qam7z9tKLywtSNHLxcOtsd/aNjA8LDtdyLyrqLjY7E8yLCo0yMzAu7GsxttO
+KzkzNGfYwa+rstlcXDovLC1Zv8+0rq622/MxLTgzUdHatauwx0xUPzMtLTvLzbapsLjI9TwsMjpU
+y8/Gra7DYEU+OS0uNvrJuqeuwsbfPi4uOnDLw82zq8PtTDE+Ly8+a8S7qK3H0OI/Li0udcnIv7ys
+uO7pLzM6LUN7yLurrL/x8kcxLSs7wMy+t6+uzOJGKzs0O+Lnv62uuGdaTjUuLS/avsetrK+69Fws
+LDo5b+N6tKy221BLPjEvMUi9xK6ns7vRXzcqMT9a1ezRrrPX8z48PC85QsnBs6evx9FyPC0sOXfb
+yNO3rMnbTi8+MTJK27+1qK7H3W08LiwtZMfKvruttt/hMjE7L0Xoy7mrrb1oaUgyLSs3w8y/sa+v
+x95EKzc0O9/axa2tt3dNTjkvLS9bxsytq7O6110vLTY89cnbuKqz1k1EPjMtMT/NyrGntcTKYzgt
+MEHkyMnIra7R8z01PS01Q9vJtqizzdNzOi4tNdHJwsO3q8LqWy06NS5I68W3qa7G6f0+LiwsT8DM
+u7atsdzhNi09METi2bqsrr5aZkYxLiw0w8a+ra+uwflNKS85PN761a+uu3VPSToxMjVkvr+sq7W9
+4k4yLj5MXt7c1drg5/j3/PTy9310aWZkZ3T97OLi4eXn7fl9ev7++f58fHdzbmpqbG14dnr///j8
+ef37fHv6/fz67/j27/Tw7u73+v99d3h6eft5ev98/vt7fHt5end4dXd9ffz/e3t//Pz7/Pn6/P/8
+fvv6fv59e/f9/v/9/f38fn1++H77/n37+fl9/n5++3x+ff3+fn1+ev78fvz9/vp8/Xt8f37+e37+
+/H18fnx8ffz8ef75+vj9+/36fn59/n55/nr+//v4fvz++/f7+3t5fnl+ffz6/vz6/n73fX58/f9+
+e3z+d3x5+P1/+Pz6/ft7fHd+fXz7/f/9/f//fv17fH94fXt7/vfx+Pj7/Px9fH97/v59/n78+/38
+/Xp++3h6enx6+Pj5+/33+v79+/z8+///e3j+fHh5en94en57+////X75/Pv9e319eHBnWvy8v/T2
+RUlANjw/ybu8ra21vFlKLyc0MTzs3sGura/UUl8/Mi4vTL+9uMCvqbfrTDs+MSwuM9HKwa6vsr15
+Wy0uNzFF3sO8raqw0lZXPC8rL07Owb7Frau+31E+Py8vMDm9v7yvsrC9ZFgsLTQtO1vHvq+or8xx
+Yz4uKy9I2b28v6uqtuZPST4vLS03v8W+sq+svWRbLC82LDtPysKzqq/Fa1tDMSoySvvCx8GsrLbn
+VU8+MS8uPr3AurWvq7tmUS0vMyo3RM/GtamuxtxrQS8sNEBvwMG7qqy2+lVOOy8uLUC9wbm6squ9
+7WAvNjUtNz/Ow7qrrb/OZ0gwKjE5SM7Fvqyrr8tXXT4xLC07v764urOps95bNjU2KjA6fcu/ray5
+w2xMMCozNkTPxburqq3KTVE9MysrQMbFu72wqrfXWTo5My40O9rCu6+wuMlUQzw6PD5GT15439XP
+zMvKy8zO1trn7/ltaGFpYl9dW1tdW11iaW9xfvn6/Pv9+PXy7ufi4+Hh4eXp7vv8d3VzdG5sdHN4
+dHJ0dHZvbXJxb3Nzdnb++/rx8+/3fu/u8fD38PT2+vb8+X77/nz4fv58fn7+dnN8e3d3eHV8/nl9
+d33w+fr3fXx8f3h7fXp+/H77f337/f75+/z8+vr6f/n7fv78evx8fft8fvz9fvz7/n3+/Hz9/n36
+fn5/d338fv7+eX35f31+eH769vp/fn/6fP38/fb7dXZ7/Hj8+nV4/vv5+f33+/j7f/v7/fv/fX7+
+eXv+/f77+/3//P99fXl/enp5d3x8/P7++/n4/fv8+fh8fXp4e33/+336+fj19/t9e3l7eHt6//j7
++fXy+Pf2enp5eW90fHh8fP/9/fP3+/h9/nh3fHh+f/z7c2VfzLrUXmlYRzY0PHjJurq9ra205jw9
+Ni0oKy9Wybe1u6qorspHSz0zKywuTcm5tL2tqK7JREY7MCkqK0jHvLS/r6mvx0VGPTUrKy1WwL21
+w6+psMxKSj82LC0tU769tsOzqrLPRkI9OCwrLFW9v7bDsKiy3UxBPjUrLi1mt7uzv7Gpt29JOzw0
+Ky4sY7m8tL2wqbh9Vzo9NiwzLm+2u7S8s6u8XlU2OjcsMy5Ptrq0ubWquF9fNTY6KzMuR7O3s7W0
+q7JkUzgwPCsvLz24uLWysKuw+lw6LTwsLzI7u7a3sa+tr/5LPCw8Ly41Pb+6uq+trq/uYD0qOy8w
+OkC8tbiurbCzXEk4JzUuLThDvra2rKywtHpeOyk3MTA9T7qztKystrpZTDcmMi8vPE68s7Sqq7i8
+Y0w3JzAxMkNvu7Syq6y5xGJHOSsvNTdI7r60sbjCyuJ4XFVQTk5OT1FUV11neO3i3dra2tve3uHm
+6vPzfXl2bnFyd3Vsb/z6c21oZGNiaGtxevnv7u3s7u3u9fX6+vv79Pf69fL19PT0+f59enV1em93
+dnN8dnp6e3p9entxdfr1fn57enx4fXx+ev/9fvb8+PX7/f33+/t8fv7/+Pt+fn14/X59fPr8e3n/
++n19e33+/v9+/X59f/5+enn+ff3+//r+/3l+/P9+fX1+fnn/fX57en15eP36+/f++33+/356e/t8
+/vz9fHv+/339+P39/3h7+vv8+/x//359/Ht8/n77d3j8ePz+fv5+e/58//37+3/+d339fnt/fHx7
+dXZ8/3/7/f349vn4/Hh8e3x8eHh9fP7++/z8/fv8+f3+enn+ffn+/fj69ff8/Xx7eHVxcnp2d/5/
+/Pv1+PX6+fh5enpxcnh5eH57+fLt39ToSDxFX2/MxMjAubi/XEI7OTMrN0BDbcu0rbCurLG0yzo7
+Kyw0KzAxSLa0sbOzq67PTD41OiwsLjPJubOzvK6sss5BQTs1LCsvRPbFtLiyrKyx3UVAOzQqLTg9
+TNa8srWvrLK/ykE+MSYzMTA8P72tr66urau8S0MuMjAoLitKtrWvt7eqrrlUPTw3LygpMujJt7m6
+q6yuvkVDPDktKzNATtW6trevq6/B7UY5NycsNjE+TsSvsa6qra26RkosKjQpMC89tLGvsLarq8dM
+PjY4KykrLeO+s7G6rKmuw0RCPDgrKi0+Zs2ztrOtq67JR0E5OCkqOjlF8MKytLGqrre7R0I0JTMv
+Ljk5wa+yr7Ctq7dOPzMzNCkrKkW6u7C6tKiruVNIQTouKis0/861ubmqra7CPj44NiooMz9Ozriy
+s62orcPbRz41JjE1M0FLwq+0sa6zr78/SS0tNyoyMD22s7Cxuqusv1VFOz0vKywtecG3s72uq6+7
+Qz87OS4oLj1Y27i2tq6rrb5RRzs6Kyo5OUFmybW1ta2ut7hNPTkoNTQuODjOs7aysa2qtlpMNjc2
+KS8sQrq7sru7qq67TT49OC8pKzN+zrW2t6mqrL1FRTw5Kyo0PkrevLO0sauuxtVKPDomLTYwPT/l
+r6+wra+usU9KNSo5Ki0wML+vsK65ramz6kA8OTUpKipAyb6yvbKprbFjPz85MiksN01vvLW3says
+tOhPPDkxJTI8O1jYubW3rauzs9U+RCktNiw4NU+wsK+xs6yu00I5LzYqKCst1Lmzr7msp67JTUg/
+OCwsLj/rxrS6tKyuseU/Pjk0KSw2PkzTurGyrqqvv81EPjMmLzEwPUTArq+ura6tukhBLy81KS0t
+PLm3sbW2qqy7VkU9Oy4qKy/vxre3vaysr71BPzo5LigvRVnKt7q0rKutyU9HPDcoLjo4SXi/sLSx
+rLK3vz0/LSc2LDA1O7mwsa+0rKu/XUs0PS8rLy1euriwvLarr8FIPzw4LSotNV7Yt7W2rKqsu05F
+PTksKDE7Qvm+srOvqay6wko5NyMsMy06Q8uwsq+rrau2SkovLjcpMC49traxtbqrrblPPTs5MCkq
+Le7Euba+q6mtvUVIPTosKi8/WNG2t7Wtq67KTUE4NycrNzRDXsSwsa+qsLW7Q00vKDctMTk5vq6x
+r7Wvq7tORTM2MygtKz+8ua+3t6mstl5APzovKSsvVNy5tLmtq620Vz86NzEnLT5AeMK4t7KqrLfI
+YD4+KSw5MDxD27Gys66ur7NQPjQrOCssLjG/trOwuquptv1LPz4xKy0tWMa7tcGxq7G7RUA+Oi4q
+LjtW6bq2tq+qrL5XSz48LCo3OD9cy7Sxs6yuuLhQPzkmMC8sNzbRrrCusK6qtGNHNjQ0KS0rPb66
+sri7q621/D8/OzYrKjBc0bu4va2srrZOQz47LyoxPkn5v7e2tKytu81aOj0qKTgwOUT3tLCyra6u
+sV1KNyo3LC4zNMKys7C8sKu4Z0U7OzQqLSxBxbyxurWqrbVlREI8MystNVB+vra5s62ttW9IPTs0
+KDA8PE/evLK2r6uzuMw+QSsqNi01OUm1sLGwsK2ux0U+MjkwKy0t7Lq6sr2wqa/CS0g/Oi0sLTzd
+yLW9uKyvscw+Pzk3Kys1Q1PNubS1rauvx3BIPjIoMDQ4RE3Fr6+wrrOzvURELys2KzAyOryysLC3
+rau7YUk6PDArLi1Pw7evubKrr7dVPDs2LygrNUt4vLS0rquste5KPDMtMTtFX2HRvq+329dOVlg5
+QDpOuLe1vMqztMpDNzc1LigrLk7ZvLC3rqmssPBFQj42Ky89SWXDtrG0rqy0yOc6Ny4lLzIxPk/A
+s7Swra6uvkpGLzE1LDMySLmzsLS4rK25Wj46NzEqKS1Xxrq1va6qrbhNQz46LiouOlDnurW2r6us
+u1lGODksJzU2O1bYtK6wq6u0s+9EQScuMiw4NVyvsK+ytKyv40c7MzorKywv0bqzsbytqq/FQkE8
+NyspLDx3yrO4squsrcdBPjc3Kyg4PkjTvbW3rqquu8ZGQDYmMTMxPkHEr7Kxrq6ut0o+MS42Kiws
+N764sbK4q6q060tDPTEqLC1Ozru1va6rrrdKPjw3LikuOkt+vLSzr6qquvtVPz0rKTY1O07etbCz
+ra61s10/PCcyMCs3MPOvsq60saqv2Ek9NzgqKys0yr60t7ysrbHLPz86OSwqL0vuwba8sKytr+hF
+Qzs1Ki87PlLPurO2r6u0vM86PS0nMy4xOkW5rrCurayrxUtELTYvKjAtXbe4sLu4q6/HS0I+Oy4s
+LTTxzLe2uqysr8BERD07LiswPk3mu7S1squsu+JVOTwrJzYzOEh7ta6xrKyvr+I/PSgvLyo0L2Sy
+ta+2s6uvyUU+OTovKyszy7+3t76sq6/GREY9OS0rL0Fpy7W5ta2tr8tEPzg3Kis4PEnfv7S0sKuv
+usBIRTYoNDIxPT7Jr7OwsrKuu0xHMjE2KS8uOr64srS7rKu4a0VCPjUrLS5N0b2yu7GrrrFuPTw4
+NikrOEVTx7e0tK2qsMnuPjwyJTE2NERVva+zr6yvr70+Py4tOSsvLzy5trKys6qrvFFEOTwuKy0t
+dMC5s76wq6+9RD46OC8qLjpe1Li2tK2srr5VQjg2Nzo/SFdr49fOzMrJyszP1uHzZ1xXU1JSU1da
+X2NodHj78Pf09Pn07+/r6+ni4d/d3eHl7fb+cW9raWdqaWpubm5vd3hzdHJyc3V0dX779/j48u/0
+8/H08fLz9vr68+/5fv55enl2eHp+e316eHZ6e3t5d3t4eXd7enr/en97ffz7/vn4cfz9ffj39fn8
+/Pt9+fV8+vr//3l+fXx7fHl9fXz8/vv9//59fX59fvv+fn56/f5+fP59fHx4eHl/f/t8/fx7e3t+
+e//+eXj7/X1+fv/++/1+/f78/vz8e//6+X58/vn9/f79fn36+nx8fHt6/nn//Xv7/n5+/X53eHdy
+eXR3eXX7/fj4/Pb6/Pt9/3l5fnZ++vD3/fj893/1em18/3n8/nT+/P/5/ff3+/56eXV2b253c3j9
+9Pf67/P3+Pv8fnl3c25zeH378unf0dhJPUpOT23Pvby+t7jMz0k5OiguODI/QOqtrKytr6uu3ko5
+LjcqKiwuzri0sL2wrLPPPj04NSspLD7ZwLG4sKmsrspDPzk1Kis0QFHMuLO0rauvv9ZEODMlLDcz
+P0/ItLWxrK6uuE1ILyw2KzAyPLqxsLK4ray+Tz43OC4pKyxev7ixvK6orbxTSUE8LisuPF/Yt7e3
+r62vwklCOjgsKTU6P13NtrGzra24uvdAPSktMy04OGqvr66urqyv3EM4LzYqKiovx7iysbirqrDY
+Q0E6MyorLUnXvbS9r6uusWQ+PDg0KSw9SG++uLizq6u22WE+Py0pOTQ7Sv21sLWvr7OyZj08KDQv
+KzQv57G0rrawqbDbUD47OSsuLTzHvbO6u6yut2k+Pzs0KiwyTHK/tLawrKyy6kU/OTIpLjk9UtG7
+srOvrbO800E7LiozOkdh5M/JxsbHyMrN0tjpeGNaVFJTUVZdZWp0/vf39fb4/fz7+/f18evp5+Ph
+3uDi6evxeW9raWhoam1uc3l4e3h5cXR2b210cnF1dHL//P349/X39vT99ff99vz3+Pj3+/78+/z6
+/353/X14f356ev95/3p++nl5enZ7+n36+338fnr9//3+/n19+vt8/vp8ff56e31/fnl1e/z9/H99
+/X77/X36fHz7/f79/P74enb+ffx+eX3+fX77fH18//x+f35+fHx9dX30ff57e/58e359evt+f316
+9/76/f38+/v9f/z5+f78fHp9fX58ef37fn9/eXz+enx5eX55enh8eH5/+/z+/v1++/t4eP95fX3+
+//z49/r39vb8+f3+fHl3ff/8+v7/+Pf6/395eXh3dXFydHJ4/Pv38PH38vj7+nt7dXF9eXR4/vr3
+9+/n39tbQj9DesPGw767tsNKQDY2Oi42NUu1sq+0uqytuFU8OzgyKSovZs25t7+sq667QUA7OS0q
+MD5S2beys6+qrL1zSTc6KCg2MT1M1bCvsKuusrNRTjkoOC4uODTMsLWwubSst15FOTk3Ki0sO8C7
+r7e5qqyy9j9AOjQpKzBN/b6zua+rrbJ9Qj04NygtPj5Yzbq2t62rs7zRPUYtKDovNz5Mt6+0sLCv
+rso/PS00MSovLFi3uK+6s6itwU9HPzwtLCw12Ma0uLurrbDLPT45NysqMEFW0ri0s66qrcRiSj06
+KSw3NkBWzLKxs62vtrZNPzknMy8tODPRrrGutK+rsv9GOjU3KSwrN8G7srW8rKyy1z0/OTcrKS9U
+2ry1vK2qrbBeQD85LykvOT9RyLausa2qs7/YODgsJC8uLztKu6+vrKusrMBSRS8xLywzNmG9ubO0
+ub3I2G9VTEZDQkNESU1SZfje0s3MzMzO0NXZ3+ft9np8dG5ubm1pamhjYF1bXFpaXmBpdfvy6+bl
+5ebp7O3s7/D59fXz7+/v7+7y9/z6/np6b25tbnh0cnJ0dnh8dXV6fXh+/P38+fn17/D39Pf59ff3
++v76+/T5/P97/np4e3N4/Xl2fnp6eXd0dnt//n/8ffr/ePX2fff69/5893r9/vz8/fx8ev7//Hv+
+fnv79359/fr9//59/f37f3/9/Hx7fnnz/vbyfH59fP19en39/3h9/f37/n54/H36+v77/vn7e/1+
+fvx8e3l3fH59en7/+318/Pn+/X78+3r3+Pz8/Pr9+fj//317e358fn16//b99/x+/nl4d3l7en58
+9Pn++PP29f15/Ht+enN3/n///fv9+/T9//f+f/56e3l6/vn7fvv8fv36ffjr3+1HQk5Z1NHKtrO3
+2EFBPTUtLjdYy7e2t6mprstDRTswKSouS9a8uL6sqq/JQkU8MyorLkvNurO8rqmtvktIPjYsKy1A
+zL+2wbWrr71NRj86LystPsbBuMC5qq6+U0lCOi4tLTvAvra8vKuuv09CPTsvKyw4wL64u7uqrcdf
+Rj08LS8uOr27t7m8rK/QUkA4PS4uLje+vLi3u6yt029ENkEuMjI3vbe3tbqur+NYPy88Li8zNMC1
+t7K2rq3QWEUuOzAuNTTGtLmysq6uzU9GLTg0LTU2zbe6sa6urs5USSs1NS45Os21urGtsLDMREYq
+LjgtOkLSur2vq7Czz1FMKjA4Lz1LxrW4rqu2u+dBQCcsNi48T8i5uKyqtr/iTkMqLjg0RP68tbWq
+qbnQYkI8KCozMT99vra1qqm34WBGPCoqNjhF2bu2s6qqte9VQzwsKTY6RtG9urKqrLdcTUc4LCo0
+Pk3Ju7yuq624TUY+OSwoNEZhv729rKutukhKPzYsKzJJfb24vKyrrrxDQToyKikuQ/O/ub6sqq28
+S0xANywsL0nVvba/rqquv0ZGPTQrKy1C0b61v7Gprr9KRz81KyssQca8tL61qa2+TUU/OS0rLDzC
+vrW9uamtv1BHQDktLCw7v763vb2rrsBORD87MCwsOcG/ubq6q63CXEc9PS4uLjm/uri4uqyuyE4+
+NzouLC00wrq4tberrc1sQzM/LjAyNry2uLS3ra7eUT4tOy4uNDPBs7ews66t0V5HKzoxLjg0yrK4
+sbCxr8tOTCsyNi03ONu2uLCtr7DGVkwsLzQuOTrhsrWyrK+xv0ZHLCs3LDQ9/bq7samusr9UWS0r
+OS85RNmztrCpr7jFP0IrJTQtM0Psurquqa67xF1VLys4MztSzLW3rqmvv9ZFQSwmMjA0SNe3tq6n
+rMDZUEQvJzAzOVbHtratp67Id0tALycvNDhNybWyrKarvW5LPTAlKzM3Tsm5t62prLtjUkE3KSw4
+PFfGu7esqq28Tkk9MikqNDxZwrq2q6qsvElFPDMoKTVAbL26tKmqq8FFSTwwKSs1Rf67uraqq63J
+P0U7MCkrM0n3uru4qautyEJLPDIqLDFP2rm4uqmrrss/RjowKSsvSOK7tLuqqa29QUQ7MyopLUPY
+vbW+ramtuUdHPjksKi1D0L+1wrCprrlHRD84LCosO8vAtr+2qq22T0BAOS8qKzrGwba+uqmsuFZH
+RDsuLC01y7+3ur6rrLpUQ0A6LyssMcu+uLi/q6u5ZklCPS8sLjDNu7i1wq6qumFHPT0wKy4t4rm6
+s8GvqblwSzw+NCsvLGK3vLO+sqm180w/PTksLyxOub61vbOptPdSPDs5KzAtTLe+tbuyqrNvSjs5
+PCwvLUi4vre2rqmyZ1w4NjwrNS9Ktbu2t7Kst1ZPNTA7KzEvP7e6tbGvqrNoaDUvPCw1M0K1tbWv
+sa21VV4zKzorMTM+tre2rq+ttVdmNCs7LDI3Q7a0tq6urrNVVDgpPC4vOEC7t7iura+yXFk5Jzgu
+LzpFu7a3rKuvslxHOSUzMC48TL66tqqqr7ZeUzknNTExQV25t7WqrLa8SkI2JDAxL0Jgu7myqKu4
+v1xQNyk2NTdM2rm4sqmuws9LQzMmMDM1SNu5uLCorMHYUUU0JzA1OE7Nt7evqay/6U9ANyctNjhO
+zrm4sKmsvGtPQDYoLDc4T8q6ua+qrb5WTD03KCw7PGPCvrmsq63BSk8/NSouOUJsvrm3rKuuwEJF
+OjMoKThBbb6+uKurrcdJT0A0LC45TOq7urerra/OP0Q6MSkrNUf/vLu5qquuz0dMPTErLTVV17m7
+uqqssNVBRTwzKiwyT9u7ubyrrK7FQ0k8NCssME3WvLm+rayvwUNGPTctKzBUzr67xK+ssMVISz82
+LS0vVci8uMKvrLHIQkQ8NywrLlvDvrnGr6qy0UpNQDUtLy9rv7u3xLGrteVFQz01LC0tab6+tsex
+qbbgT0ZDNS0wLnS6vLXEtKq5c0k9PjMsLy1oubu0wLOpt+5OPj82LC8tari8tMCzqrd+TD4+Oi0v
+LVa5vra9squ3eFE7PDktMi5ct7y2vbOst19KOjo9LS8uUrm/t7iwqrhqXzY6Oi03MGG1vLa5s63A
+T08xODksNTB2t721tK+twl9bLjk4Ljs11rK5s7Ozr8xOTiw0Ni05NOa0ubOwsq7GVFcsMzkuOjnb
+tLmzr7GvxUxRLS87Ljg75re8tK2xsshOUiwvOC86P9C2u7KssrTNRkgrLTsvO0jQu7yvq7K40E1M
+Ky45MT1Ox7W5r6u2vt9DQykrNy88Ucu5uq2qtMHcT0ctLjk1QGfBtritqrbOc0Y9Kyo0Mzxew7a3
+rKq20WpJPSsrNjdB6r22taurs9ReRz0sKTY4QeW/ubWrq7PlUUk8LCo1OkTYvbq0q620fktCOy4n
+ND9Lyr29r6ustFhNSDktKzQ/VcW5uq6qrbhJQT00KigwPlbDurusqqy7TE1BNiwsM0j8vbe7rKut
+v0NIPTQqKy9DbsC3vqyqrb5DSj41KisuRt68tL2sqa26Q0Q9NispLULXvrS/rqmtukZIPjYrKi0/
+z762wbCqrrpFQj45LiksQ8W/t8SzqK29SUtBOCwsLELBvrTBt6muvkVBPjgtKis7w7+1vrqorMBX
+S0M7LS0sO769tbu9qq3FTkA6OissLDW+u7S3vamryV5FOz0sLS0zvri1tb+sq8ZYRDg9LiwuLsi4
+uLS9raq/X0s3PzAsLy7St7m0va+rvFpKNzw4LS4u77m+tritqrteVTM7NSszLuCzurK1rqu+S0Mv
+NjQqLy7utrqyr62qvl1cLTg2LDgx27G4srGvrcdISiswNSs0MWq0uLCurq2+ZFwtMDYtNzjos7ax
+rbGwxEpOKys1LDY5a7K1sauur71MViwqNyw0O3+0trGqrrG9R08tKDktNEHtt7mvqK22xUdNKyg4
+LjdI0LS2rqiuuck/RSskNS82TtC5uqynrb3TTEctKDUzO1vDs7Wrp6/I/kE8KyQvMThdxbi2qqez
+z25OQy0qNDlB4ru0tKqose9MQzkrJi41PPe8tbKpp6/fT0g8LCcuOELkureyqamu4khFOi4mLjtF
+1rq7sKmrr25DRTktJy47TM23u66oq69dPj42LSUrPl7Dub+tqKuxTEdCNywpLj78wrS9raistUZA
+PDUrJys878W2wK6nrLZKS0I4LCstPdK+sr2wp6y6R0M9NSopKjjRv7K7sqeruU1FPzcrKis4yr2y
+u7eoq7hOQkA5LSkrNMm/tbu7qau5VUVDOi4rLDLGvba5v6qruFhAPzsyKiowyb65uL+qqbteST49
+ListMMK6tra+q6q/TkI4Oy0rLC7Gubm0vKqpwWdLNj8uLTAuxrW4s7ytqsdWSjA8LysxLdmzubG5
+rqm/WlQvOTIqMy3msbiwtq+qvVFPLzk3KjItW7O6srKuqbtZWS40NiozL22zuLGur6u7Sk8tMDkr
+MjNlub20rK2svlNbLDA6LTk73LS4sq2vscZGRiwtNS01Pu66tK+3vsXb7mVZVFNTUlJUV1hfbfzq
+5d3b3Nve3uHp6/R8/mxrbWJlaGltfHdxc3dya21sbHR3+vLr5+bk5ujq7PH2+Pf9fvl+/n34/v77
+fXl0cndtcXR0dHF4d3Z5e3n9fn18fv369/5/+/r49/j28/r39/36/Pv0/X78f3t6fv78+v31/n7+
+fnx3/X3//n3/fXt9+Xv+///8fv5+/X7/fHv+/v78eX78dv95/n5/fn74/Pr6/v/3/3/9fPl5/vP2
++vz8/vv+/v79/Xx9/336/Xz/fv71/vv+fX99fn57eX94/3t/e3f49/j7/Xt9fH15fP5+ff58+f3+
++3r59/v7/n58/Hz9fv37evz/+ft8/Pr6+f50+P/9e33/eX18e+/5e/J/ffr7/3/9fHx4fn5+eHt9
+evr9/vv7+/r8fv/8/v98/f7++f7/fX98/v13+nx9/Xj/fv76fv5//Pr8ff78+vz+/nx9fX34/P36
+/Pf//P/+fnl4enp6/n36/nt7fXx8/v/6f337+vn6//x++v3+/vz7//39fP7/f/5+//z7fXx+/nt+
+fnv2+/x9/f99+n38ff/9e/38fnx++33//f/+/n3//fz7fHp7f/58fH57fv/7fv/5+/v//H3+/f3+
++vp++/38e3v6/ft+/f9+/fx9f/57enp/ef9+eX56/v97//z5//59f//++/r8fff4/f5+/vv/+n99
+/X36fn1+evz7fPz//n5+fHz//H57/Pz7//59fP78/n38+/r9ff7//Px/fH38fP5/eXr++n37f3z9
+enLy7nT9f3r5ffz9/f1+9X38+3j2/H/7e3v9/f73/XX9/f18//59ff/+fv//fn5+//3++f79/f9+
+e3x+//v+/v7+///7/fv9+/7+/n19e/96evv/ff38+X5+fX1+/vz7/f39+vr/+vp9//x+e35+d3h8
+/n1+9/z6+338/n98ev/9fXp5eX19/nx+///7/f76/Pt+/fj9+v37+f37/3t8e37+/v7//v5+e3x4
+/v54+338fv79/f56fn15fv3/ffr7e378+/19/H3//f55e/v8fnl+//v/ff3++/35+PP9+n79fvz+
+/fp8f31+/X/6ff1/fXp8/Xt4d/97enx7eXz5fvn7+/n8+fr/f/v7fv7//f99ffr+/Pl9fXx5+/1/
+//z8fnx4/v//+vv4fv76/P38f/z8ff56//53/Xx+fnr/fP/4+/78fn5/f35/fn7+fP1+eH78/Pj9
+///7+fl++31+/X37fn79fX56ffv//P18ffz9fX79fP5+/vr+/n7/fv19/n/693z9/P56f/x09fj/
++3z6ff52fv53eH3+fH94ev59/fn9+fz6+3z/9fT9937++X7+/v19ff/9ev16fHt3d3j9evt+/vd+
+/33+fX74/fj++/r99fl4d/12fn79e3r79/n8+3r8//79fPn+/v/9/n98/vl+fXv//ff+ffz9fv58
+e/t9+n/+/n/9fP59eHv/fn15evz+f/j//vv+/3p+fn5/f/t9/fr7f/v6+/79/f76+/t+ev35fX1/
+/339/H3/ffx7e3t4fX5+fPx+/Xz+//z7/vr8/v7/f/5+f/99+/l9+vT9/fp4fv5+/nj9/n7+e//+
+ffl//vz6+v78fvv9ef19/fl4fv59fXz8ff59fvt9+356/f79+////P3+fXp//n78fvx+eXz+fn39
++/37+f77+3z7fv78f/7++P7+fv//ff78/v7+fnx9eH78fXz++/1+//98+/z7+/35ff/8enx3ff56
+/f///n1+/P/4+fr6+vd/+3t6fP3++P1++/7//H94ff19fv7/fH//fX7+/f/8/v7+/f59f33++nv9
++/39fv/7fnv8fX3//n19fH19+vr++vz9f/z9fn77/v76ev39/f5//f1///59f/56eHn7/vn6/Pd/
+fvx3fH1+/H56ff5+/n18e/79/f59/f7/+/589/x++Hv7+/v6/X5+fvv6fH36f3x9e3j+/3x7/f1+
++/x+fn5+/X3+/378e/z/ffx+/nz9/H7+enz6/n1/fv78/Pz+/v3//3x89ft++Hr/ff/8ffz9/P77
+fP59fXl4/v/8/Xr+fH78/Xv++fz//n18fv39/np/+/z+ff3/f/d+/3/+/f/7fv19/f3//nz9ff58
+fvr6+v79/H38fXp9//p+fn7/fXx+fP/9/3z//f76/vt+f//8/Hx+/vj/fv9+e3/+fnx+f/99/P18
++Pt6fvn9fX7//Pn99/n7/fx+fn58fX5+fv59/X57fH19/338//j/efr+en/9fvP/fvl++fZ7ev59
+ff1+/v59/n1+/vr6/v34//x9fnx++vx7/vv+/n7+/333fP57fP57/P7+ff5+f3/++/75/n59/H56
+/3r9fv97eHx+/fn+/fz+/n/9/Pj8+/n79/58/Xz6fv79/X58/Ht8fPr+f/x9/357fnx5fv99fv77
+/Px+ff9+/Pl9+35+/Xv/d3v9/vv8/Pz4/v3+ff37/v79/Pv+/H79+v36+3/++nx9fv97fv59fP99
+fn18fHx9/v17/v3+/vz8ffz+/fv9+/37e3j9fP5+fPx7/vx+/X37f/f8/vl7eH73ffr7/Pt+/vt+
+/fx+ff5/fH76/X5+e3v+fnx8/Pr7/339ff3+//18/P/9f3x9ePt++v9+/Pz/fH16/P37+fz2/P1/
+e/99f357ff3+ff98/X3//P399/78fnt9fH5+e378/vt7e3x//fv9+vn7/v19e/x+fXt+/v19fHt8
+f/z+ff7//f5+fvnxdnj1dH/593h6/f7z//j+fPl+ff97evx//n3//Xv6fXz/fP1+fH1//v//fn3/
+/f1/ffv+/fp/+Pf5/v78+fz//n19/fv8/nl6fXl8/np+fP19e/r+/vz+/nz++378+/9+fv//fvf5
+fn54//z9/fx7dvl+/v76/vz8fX58fn78+v36/P5+/f/7+f1//n7+f35+fv3/fHv8fnx7fX15/n56
+/vx5/fv9/fr9+X7//P9/f359fPn8/Pn2+f/+/nv++Ht8/H38efp7/n59/H3/e/t7f/19ff59fn/6
+/vz7+/v9+v/+fP77/n5+/3z89f7//3t7fX5+fP9/fP99+vx+/X1+/f/8/vz8/Pt/+/78/fz8fnz9
+/379fP38e/5+/P1+/X16e///e35//X79/vr+ffz9/v78/f37ev/7//79fP1+fvz+/fz9fnv9/n7+
+eXx5e/36fn75+fz8+3/8+/98fPh6fP5+f/7+efx7//18ff5+fPt+/f5++3x9/nx8/Pr9fP59/P79
+9/33/fz++ft+/X5+fH7+fX7//v59/n59/3p9/v76//9+enz9/H3++/r7/Xx6d/79fP79/X/9/f98
+//19fn77+/5+///6+/v8//f8/f55fv///X18+/t+/n7+evv+fn9+/X3+f31+/X99fv3+fH5+fH/+
++/7+/fz5fv7/e/x///97//1+/n56+vv8/vv9+/76/337ffx8enp+f/33ffv//fp8/np8fH58/319
+//34fv5+fnz+/n/4ff76/H39eH77/f59/v57/H56+ft9/H/7/vz++n9+/fv9f/5+/v99/v33fnt7
+ePz/fvR9/v38/Xx9/nr//Xx+/vv9+/35f/1/e318/nl8/v18/vx7e39//f77+Pz8/n/9+/r6+v9+
+93r8+3z8/P1+e35/fP58env+fHz+/P19/nx9/339/35//v39fnx+/fv9fv37//79///+/P78//7/
+/f9+f3/8/v1+/P5+/f////t7ff35f/58/n1793v8ff3+f/58/n3+/f1+fvp//35+ff38fn1//X3+
+/33+/vz8fvz9fv19fn9+/P78ff7+/P18//38/Xz++n78fn5/fv3//X79/vt7e/t+/Pt8//t9f3t+
+/31++/59/P39/v99f/v9ff7+/f/+fnr9/Pz9/v38/v3//X1+/H3//f7/fv9+f/79/339/n/9fn3/
+/f7/fn/5ffx+/X58/H/+fPx+/v99/v39/37+/n7//n7+/H38/X/+/vr/fv///f79f/3+/v59/nz9
+fH7+ffr+fPx6//z//Hz9//5+fH98/f3+fP78fv59///7f378/v5+/v7+fv77+339+/7//v1+/f/+
+fPr9e/3//331/Hl5cf77/375eHd+9/j9/Pp++v/+/P/+fX37fv1+fn3+/n59/v7//nx/e/5/fvh7
+ePh//Px///79/v16+X9+/nz/fv3/e/z9/X7//v/+/fp7fvp//X59/379/nz+/X/+f35+/Pz+/335
+ff39ff79/fz7ff38//t/fn/9/n9++v/+/39+//5+/Hr8/Hz9fn7+//79fP/6fn/8fX38fn98/f5+
+/n1+//37/v7+/X5+//5+//v+/X78/P/8ff39fvl9fvr//H97fP1+fn/9/X37fP19fvv+/H79/f56
+f33/+37/fv38f35+/v78/vh5/vt9/H3+fv3+//3//P38/Xx//H3+f//8ff59/n39/n7+/v7//37+
+fP78fv59/ft9//5+/n1+/3/9ff58fnz/+/15fvv//X1/fv7++/18/vv8fv1+/ft+/H79/X7//3z/
+/n19/f38fX7+fv5//v59/P18fX9+ff3+/n7+/v7+f378+///fn/6///8fX/+/fz9ffv8/P19ff39
+/n7//f9/fn1+/fv+fXz5fX79fn3+/P9+ff77/X7//n9+/357/P79/31+ffv+fv3+/f5/fn5+/vz6
+fX33fv7+fv/9/P99//1+//9+fn7/fP3+//7//n5+/P7+f379/H1+fX3//n5/fvx+/v59fP3+//t9
++vz/ff79fPr9/v19/P77fXz9//19f/3+/n7+fXz9f////v1+/X96//v+fn5+/f7+/X3//f5/ff39
+fP9+/X/7//5+/P9+/X//ffv+/X/+/vt+/v5+/f3+//79ff76eHv8fv59/vz/+X97/f/8/X7//v9+
+/n18//7+fP7//f39/nv9/f79fvz+fv1//nv8+vz+//p8/fx9//9/f378fn77fX99/vt/fX/7//79
+fH79/H3/fn77f/78/3r+/nz9/X7+fn99/f/8fv/8f/59/n7//f78ffz7/Px9/P/9/v17/vp9fX5+
+/33+fn35fX39fP59/P3//378fv5+fX5++35+//1/+n3+/f/9fv7//P7+ff5+/fr8/nv8/v1//n5+
+/f59+/79/Xz+ff1//f9+/P1+fv98/v3//X///f3+fn3//35/ff5//v5+fX78fv19+v9//X7//n79
+/H7+/fz+/v7+/Pj+enn+/XF8ff/+fX98+Pn+/f==
+
+--owatagusiam
+Content-Type: image/pbm
+Content-Description: MTR's photo
+Content-Transfer-Encoding: base64
+
+UDQKMTA0IDEwMArve3/+3//////////w3b/b73//////////sLbt/7v3/v++//////B732/+3/+5
+/9/////w3nf7d//9/9/////78Lfc392//m++7/f///Dtt/b//7/d7//////w3bs//////v93////
+8Hbu7b/99//9/////+Du2+///9///77////wu3c7f/v9//6Nf///8O212////2+60z////Bvvd3/
+/7+ym3bP///w9s5n//+/tt253//38J3zv//+/9+3/mf///B7Pf//9v/f///r/+/g7u1v/90/////
++///8Lbvf//n7aO/6eX/3/C3O9//vu5s7/91//9wbdr//fszW2/983/98G3W///pkkN//Lv3//C2
+d7/+zEyU3f9t//+ws7n/9iAAEpf/c///4M2ef/EiIAJPbJ////Dd5//5AAAASf217/+wtnn/zBAA
+AAb29///8Gbef8CAAAAj2t////DZp/8yAAAAAfs/777Q33n+SBAAAAX83/378GbefskAAAAB9m/f
+/+B6zf4kQAAAAPs/9+9wm3d9kgQAAAL7t/2/0O20/JiAAAABbP/v//Dtn/ygIAAAAWx/+9twm2n9
+JggAAADzf/7/0HZu+UCAAAAAnb/XtvBnt/5QIAAAAP3/X/+wubX5hAIAAABXf2/b4L5t+SEAAAAC
+Xf+z//DNz/5ISEAAAGb/v7dwc3P5kgAIYAA7f3/94L51++SRvyQAP/5237Bmzf/9RPeSABf+7/bw
+W7v3///52SAf/u974Ns25//7DAEAD/n/23Dc1dv/rmcggA/o/v9wZ9tb/+SeyAAf4H+30Lsqxt/S
+PgBAW+A39vDa77f/3j/gT/vwPd/gbtWz/9oe/DuH/D95sG23j9/CHmQwBYA/7/Dbakn/4i8AIBYA
+N79w222Xf8EGyCAHAD330G23N//BEJIACUA/ffBtmsm/wIAQAAYQN+9wm26Yp4CCACAGAH3/0PNt
+pl2AACAAAAB/dvBtsyVfIEkAIAAAd9/wbbbxLkAEAAAAAP270Jtt3r6AEkKAAIDu7/DzWZ/+gAZa
+AAAA/v3wbdtgT8GBgAAAAfefcL222k/gASAAAAP98/CW7Zky6ADIAAAD73/wdtnlHuQAYAAAR/vd
+0Nt2fNuSADAAAgfe73C7bxo2wgAYAAAP/7vwbbnqbaAADAABH/v+8M227JskACQAAD3+77C22zY+
+2QAkAAAf97vgdtubD/lT8AAAHf/+8Fts+V/gAJgAAD97b7DNt2xHkgQIAAA///vgdLWeNPAQIAAA
+G+++8LeZ05ZMgCAAAD/777Crbn0TAAAIAAB/v//QamenbeIAAAAAd+/28Fu5uyUoAEAAAH3//uDd
+rtubyQAAAABf9vuwptts0nSAAAAAf9/v8DrZt2WSAAAAAM9/ftDbbt95IAAAAAC3+//wzW3ZmWAA
+AAAAt//t8Gebb+yIAAAAANv9v7C0//72gAAAAAFb//vwn///NkQAAAABbf7f0P////sgAAAAA2b/
+9vD////eiQAAAAKa/7+w/////8wAAAQDu3/t4P///73SQAAABmd//3D////v0xIEAA2Zv9vQ////
++/yAIAANtv/28P///97spABAGmb//bD////PIwEBADObP+9g////zfJIEAA9u//7cP///8fYkIAA
+Zm2/9ND////HbIQgANtN//+w////yeZhAADZ93/7YP///4DZEAAB5jd//dD///+yexIAAzu5/+3Q
+AAAAAAAAAAAAAAAAAA==
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Star Trek Party
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29543; Thu, 3 Oct 91 13:04:09 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19372; Thu, 3 Oct 91 13:03:25 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12199> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:12 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08947> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:09 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:03:08 -0400 (EDT)
+Resent-Message-Id: <IcurRw60M2Yt4Ta1h1@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:03:08 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <nsb>
+Date: Thu, 19 Sep 91 12:41:43 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9109191641.AA12840@greenbush.bellcore.com>
+To: abel@thumper.bellcore.com, bianchi@thumper.bellcore.com,
+ braun@thumper.bellcore.com, cameron@thumper.bellcore.com,
+ carmen@thumper.bellcore.com, jfp@thumper.bellcore.com,
+ jxr@thumper.bellcore.com, kraut@thumper.bellcore.com,
+ lamb@thumper.bellcore.com, lowery@thumper.bellcore.com,
+ lynn@thumper.bellcore.com, mlittman@thumper.bellcore.com,
+ nancyg@thumper.bellcore.com, sau@thumper.bellcore.com,
+ shoshi@thumper.bellcore.com, slr@thumper.bellcore.com,
+ stornett@flash.bellcore.com, tkl@thumper.bellcore.com
+Cc: nsb@thumper.bellcore.com, trina@flash.bellcore.com
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=Outermost_Trek
+
+--Outermost_Trek
+Content-type: MULTIPART/MIXED;boundary=Where_No_One_Has_Gone_Before
+
+--Where_No_One_Has_Gone_Before
+
+You are invited to a
+
+*** STAR TREK 25TH ANNIVERSARY PARTY ***
+
+When: September 28, 1991, 4:30 PM until whenever
+What: For those interested, we'll have a rerun of last year's season-ending
+ cliffhanger, and then we'll tune in the season opener and the 25th
+ anniversary TV special. Prior to that, if you bring some food
+ we'll have a pot-luck meal, and general merriment before and after.
+Who: You and your family, including kids of course.
+Where: 25 Washington Avenue, Morristown.
+ (See Nathaniel if you need directions.)
+RSVP: 993-8586
+
+What follows is some Star Trek related multimedia mail, the last of which will give you a chance to RSVP on line.
+
+Live Long and Prosper! -- Nathaniel & Trina
+--Where_No_One_Has_Gone_Before
+Content-type: audio/x-sun
+Content-transfer-encoding: base64
+Content-Description: He's dead, Jim
+
+LnNuZAAAACAAAFmUAAAAAQAAH0AAAAABAAAAAAAAAAD////////37+/r7e/z9/f7//93b29z
+//////v37evv7+vr7+/z9/97b2NdVU9OT09RWmFr9+Pd3d3e4+vt/29la21ra2lra3vn3dzn
+9+//YVtaV1tp9/PjzsjHxsbGy87P2ef3c2dz/+vr59/Z09HR1dLR2d7n/2lXTUU+OzY1NTQ1
+OD5ETmXf2dLP1dfb3O9jYWNhX19pb+/ZzsjIzMzP43dfVU1RX2/bybu6u7q7wcjN0+1ra1VR
+Y3fv7+fZzsS/v8LEztXW62FPRj03Mi8sLCwsLTVFVffLwL6+v8XFy9b/W1tdV1RVWVtr2srG
+ytXj/09LRT4/SVtd2MC0r66trrG2uL3I0/dOSE5WWVxn99nGvby7vsfR4VdKRT03MCwoJSYn
+JyowPURb0cfHz9lrZ2tZRktdd+HazcrGv7iwrrC4usHR5W9VTl1pd8i7tLSztbm/xcrb91dF
+Pj9DREhXb+vQv7y7uby/wsr3XUQ7My8pJiYnJiYsOEFK38G9vL7Fzc/R72f/1dXe69/v99/M
+w87v919NSUU+PUVMTme8s66urKyvsrO2vcTNb1JVWVNLTmfr2szKx8HLzs3XY0o9ODQtKCYm
+KCYoLjpHacnIx8fI1W9dU0A9RU5nZf/Zxru2rKuusrK3xM/cb1pvd2PXu7Ovr7O2vtVlS0M9
+ODM2Oj1ATVfr1cq/vbu6ur7DyutSPzoxKiUjJCUkJS48T+29trSxt7zCyNNYS1FYb+/t2c/H
+vrq6x9HbW0E9PDg6QE1dxbGtqaenqayxtbnC3FFNS1NeZ//c18/SzM7Pz/ddTktAOTUvLCgl
+JScmKCw3QV3bvru2trzCyMjrT0hTe+fXy767urCsq7G4vcn3WU4/P0NJTs+6tbS1uL3Oa0Y/
+PDcxMjg+SlBb3si8urq4uru+yNnvW0c9OTApJycmIyQpLzdFY8i8uLS2urzC71b/697j2sfC
+vLivr7W9xM1bRT88ODxHSnu8rqyqqquvtsDXd1NGRERKUVphX+HTz9LV2t/3b1tNRDs2MC8p
+JSUlJSUsND1I2MC3srK1urm7xldHRUtLTF7Vyr61rKutsLa8zmdKPjpDRlHPt6+ur7K7x+FZ
+Rj86NzU3Pk9cb+/Nvry9wb6/w8jV/1pFOzgzLSssKygoLTQ9SNrHvbi3uLq8xNtMTE9eZWfn
+x8G6r6yttLa9ymdGODQ1PUBHzrWtrauutLrC2VlNPj1BRE1be+/jzMLJzNPjXVtKQDo2NDEu
+KygoKignKjA4QGvIv7u4uLq8v83hY2lvc+/Ovrm2r6utr7O7yetURzs5PUdbyrevrq+zusfP
+91lEPjs5OT5FTFBd98zIzcvKy93ja1lPQzs6Mi4sLCsqKzRFT9a5s7O3ubm9vcPnY1dMS1NN
+XdfMw7y9x8rI0V1VT1M+PUdvz76vq6+vr7zCy9VZTkpKTERhc1lr0d3Sy83azs5rXU5EODMu
+KykmJSQkIygyOkTOuLWzrrG/xbu41s7I1+u9ubmyrKytrbS7uuFCOjY0ODlAc8O+tbbAyM3v
+X11LRkZERV7f69PPz9vZyszKx8DYy8vrUT83Li8pJioqKCs0Oz13u7W0srXHwsXcUVQ9PEZE
+QE/XzsC6uL28xcHhRz9HQD5axbavq6mssbC+z+PTRzI8Ojg700ZFd9dN47nDz7+47cjM7ztD
+Qy4qLS0jJygqMDNP38vPu7m8vbq2vsfG19fXzsO1tLOvrbPfu77VO0xFNS9CQEXCzL+3rcvY
+4108Qz0wO0jj/93awr7VwLW83bS6wtTDWzowMzMoIyYpIyorLkJd19W6r7K/vLH/zrz3Pc3M
+P2O/zky5s9HIr+dMPkIvNkpM0LinpJ+fo6qvvU5LSzYuMjk4SdDP3sO9z8vFTzZHQi49U0Rd
+zMnN59zrbzA3PDsuLi0xKTAuKz3rxb6urqWgsbe4n6Sqt6rGLTI2Kibrs8XBq8F7MDIfJCEn
+Ljtfr6ain5+foqK1w0IvHyAoIzBByc+trKepq7HGZTErHx8fHx8fHyAnMkLIr6ifn5+fn5+f
+o6Cx78HhLzRTPylHMysrOiwmTEU4P7Pew7P33atJSa6nc76sr0i2rMNevbs438hIJVlQLyx3
+OCgtNCslMTwuNOPQyLKtqqerrK7GzVvTNS0uRSwmRdM3T63FrqzM377eOT1fKy5XREjJt7Oq
+sLKtqv++wUfXwUss3UYvxNg8c6zLvrLDv75eS2suNy0iLjMfJjsyPa62yq2ipsyrxUq+zT5O
+20e5PTK8yz+8sV/VZ7NFS09NMi9UTTxetFzIu96yvt+7r1ezsMXMa1tO0z05KT0rIkAuMEn3
+QLfnabRWSkfJTDo3yk9DLN+qv1/RyUbrU0lFd0W5tr3Bv7xPZ9EwLsbEPFuvvVO+q7Cup7K4
+Y080HyUgHyMmP0BXraOroammta3Jv0tLOygpKT5EL+mst8q7q6zv0cu1UUFZP003LjMzXS00
+RkYq4z0wz0M4QrNFyrNIvsS3T7HG07DDsrasv7m/XbprOzFILS48KD4wRT3XO0TEUEjJy99M
+3bPQY925RbbEa3OvQzrJOsTXTOvJ+9e/68FMr21aRMbDQcdCxMy92a9OwNXzX0bYOlVR33dO
+rcDjw7V31Uc8d0w/OzxTUz4/VdDz99/Ore3G78xCPjxBNi8/Ly85OTMywT+3b83Gzu3Vx0lD
+70nIzFHIt669966wU8RMUelET0DLNWdMa01ttmfBP824Ok9JPeEw6Va/O2e8ULZS07XHU7rK
+uljVw1vXa/tfQ088STlLSmPnzGO/W7/Wx//TxDxNXm8x/85D30nP5bJMw+PNN9Y9V1bBUc8/
+7bG94cO+zmvHZ0zLRz9CQjhZSztAc0TOXjhO38FTyM1XzXNH5z1JNFMqT0RfTdeyXa/pwcXI
+Y1fS/87bX9nKzGf/vuHHz/e4T8DzuUpX73vFOs/Nzcr/yOf33kC/d9tcYchP671PxNDUSsL3
+uL3VxLXF+8tMS009KjM4LS45OkTjMtBZ3Ei6wtFKb0rBb0zIxcg6u2HP10vM1O8z2M81QzbO
+R8ZYtMzPU8vPz1HKU8tFYWvM90PdVMZY607HTkbcP8c9zuVFVDrINcw/QzzzZ0q9yeu0uLnJ
+u8NXb9rV+06+Srbf80/Zx8/USctaREndOE04XGFvwsW367fR3L7ezzzeOfdEyEhfw0vBd8xD
+xmvOWlNj5+c+Uzk+ODgvQDM+NUE/WVX/yMnAx3fA/7BlvGm+Ub5Rxsq+xdW988XhVmNGVzlL
+OFpjRMLTtc68w9PMy9vT7VnY8+//593aXc3Wz8/I28vv4UHHUVlMUDxANUU9OktJa9fFy8jK
+w9zM49nKTM5Zd1lR90/cXdTdz9vX3l5jUT5VTW9P2b++vbq/wdJ3Y1dKPkBGPUM+TFlvWs/F
+083Hw9HVzu1nRUg+QTU2ODQ2MzpNTGfcw8nLw8nhx9bO3cnVyMrAxrjFwL3Cu9vC92dcQ005
+PD5BS1Prv8TVx87I1Gfe/9NOa3dvT1X3d3NvzM7vz8jI02drXVM8OTk6NDQ2MTw+UljN0MC7
+v77SynfMZ1djUWtOY3PX0+3ry9Xn3ddlWm9LW0pIR/vdyb6+usPB2tTNZUxJUERLSllXa1nv
+x9bP2cfHx8rK1/9NVkM+NzM3NzM4PFxl18fFv8DFy8nMzszfxsXHzcTBwcjEyc/b299fR0M/
+PTs/TvfUyb64usHJ1W9hU01VV1JNUE1TVG9l49PcyNHN193r729OQ0Y1NjQyMTA2NUJKY8rE
+wsfCxtnZ69Tr39nj7+/OztXdysbP5//vW0NDPT9ARf/OvrivsLK1vsPP0P9eU0peWVFUW2v/
+a9nZzdXO1P93d29IPz0+NTIxMC4tNjtFSf/TvLu+ubu5vb3Nztfez9LT19jLxczb/8f3UkRJ
+Qz08QEpV28u/wcS/zdNvVE1fUD9DS0hMW13zzsfCur+3u77Jzd1PSUc/ODk2NTAvNzs/PmXj
+z9bJzd7N1MzP29fMydPIxsbAvr7B0cjTy2fnSkJBQ1lRzce+t7u9xMbTd3tXTEdHSU1Tb9nb
+6c7SzcjEyc/lWWFKOzk4Ly0uLSsuMkNPVM3Dvr28vL+8u73IzcTFzNDGy83X291rX1FGPDo+
+Ojo5P0Jb28C/vb3Ix9HOzNFzVE1VT1lYXWNn59jNx727vb68ydlfU0s/NDAyNS8vNkVGT2fP
+zM3JzN/v1t1nb9/KzNPOzGGryd/Lw06zzMn3XNHb3TfG12dfsfPCVVfvsllvQkfDP8c64+NB
+Ub9Ew9xc0+U8OL9K2FoyLjstOi89NOsuSz22ScPOrsixY8K/+04/47/e373FzcNQuMrTa2Vd
+Y0AsP0M4z7a7q666y6v3R1XhPdZLNMjHQN3Id9m349m3wTTFRMI80zE+RTNfWVwsWka6UVlG
+sk3nv8tR9zYwwq9HONTJa008Zaavus6tu1cxLS8sIjDlvNfJpqemtr61tjYrNiciIB8nPz1Y
+q6Kon6GjpajBYfcvIx8iIx8nHys7vbemrZ+rs03rMy03Vc2trK+rr9c8QDo0PeW2tc6zpKW+
+yMXOTyYkJygjKy9GwaijoJ+fpKqryS8/LB8iIh8gKT0+Pb+wraOgpK9r4UY1LjXJwKu1tNVX
+KikhHyErN0vIuMasoa6/zME6LyYuM1VDY6ynp6Ofn6WyxunfNR8fKB8fIyYp2zU5ta9GwKOf
+xdGtrsZb2rGgpK2sqrc/LSwpHx8jMTQySc64rMfBs7c6PU44M2PZyq+ioqGfp62rtd3dPScu
+Kx8jLiQr2z8uY6jRO7Gfw0ZfwMJPS8WiprW1r18tICQiIR8q08x3tKiuqq3S5VsnHyIoHy89
+X66foaSjqL5ZSUQ1MSk3KyknO0w7R8GzoqlvvqilTe+3vltS466hq7SutjkjJyYfHyAtPt9d
+rau1r6y43U0sNTctNMjVZ6afq6yrvddKLC1FNjM3PS46RitnscU9rqZra6ejXVGzsr/Z072k
+rNDOyDUfHyIfHyAwyb7DtJ+rw7W7dywoKy4rL1C+r6qfoamptNNvTz8/Qi45TCchLln3PDj3
+59/E+2Oon6/rqaHDP9G6p6/KuLdVIyInJiIlSry/ya6tvLnhz1Q1Kjo/LTe5sK6mn6Kps99v
+Pi0kOD4vK0cyLEQ34cG+Ta2rrFU3UKeuNe+wxjI2R7atxdjB3SkfICsiISjntsLNqKOuX8Sx
+xy4kN1xENc6qqKqmrbbHOTs3Ki93Oj7HUTk8Zy85zKpESK6t5+t7p6CqS6yoyDhPxcW+Tklh
+Nh8iKiciLty7tr2vpalLSMPILC9fXE3z27+yq6motVhpYyotLixM5TQ+tOM2O+/7vDQ7vai3
+udA1XbSsVEG31zwzOruvs0/d0zghHyMnKitZq6mpt8GrsTQz7eMtM8/JtM5jwKWjrLOww08x
+Mt1nND20uzUvSW05P08+zae8bbmhs8Y/PrGt3yu/r9UyV7atyTdT9zkfISs0KTTBqrG/taev
+Rjvrxz0wX8LOb//BuK2ssrXYQTA0LixQ1c7JW+9CNSw7SzswP0Jcy6Owtq6zyz0rWa/nLT26
+ezwzS7GtzTFBUCkfJDFHRMKtn6evra/HPzE1NisuRqzXPGOyrq+2wrS4vtfDSDxNX0tGMi84
+Tj4wOrq30dqmp846wKqtRCc4vksvWq3JL0U3OTkuObGvTzbVUzYlL2vEvbSfn6Sps7zAVywn
+Mjw6P0jbwUw7x6/M22dDX0gpKD85Nzw/TmlMOUiyxeO/qqbDWdW5zEzrrVsvNUUzva+v6TpR
+MjQ6Q9exvP84MCwpKTN3uqyfn5+fn6KtvD0pKSsfHy42L0e+xrymuP9P8zgwQELv09c1Lyk5
+NS04tqaosbGvrcw9Pau0TCy+v+tALzTcwikqa8YqKTBNQj1Dt5+qurmzYy8oL9O6wa2fn6Os
+r7bPLyAqLiIgLC0yTT1DvbHL1VlXXVUyNs/GRlG9urvD47iuwsqvqq+80ruqXzT3wzw5Pv/H
+P1tfOSkxQDJBt7nVQEA4W09Uu6irzcvfbzAtQ72/uqafpqyzvVEuIB8fHx8gJCo3QLympbmr
+ornLytvK1jo/+/83b763wK2oqa7L78/RRT0/dz4xLTg5N0BZSFH/T2d3b9RLzcbbd2/bSUnr
+/1V7wcHEyr22sLm2trC4vcZ3Ri4pLjMrKjdDOz5Nb9B3Qz1ZUUI4TsvK1bmmpaqqpKesr7W1
+tM1f71UvNT89NTc3Ny8uNkU7NDg1LzE3Pmm/ta+yuLGuvr+4vMXMVUnZzVFjylZFQzk1OTQr
+MDk4P93Pv73Bxs7zSlVv693EwMbCwr+9u8zR01tJPkNQ28vCu8Lfd15JPT5Oz8nfzr24ymdr
+zk8/Pk5PUT5D2dNBP2fX3evPvrrRSklXRTlB/148SnfF0e/jy8pXUt3aZ+O+w1ta52nn72Fv
+yc/3/8jB2/fGvcrvZ01GPzs4Oj88Slr/WGvOz9nEvsPlW2tINjU/U17Z083Jz93v3efrzsft
+d2tIOj1EU2HZx7u1vMDAurzC2tO/y0lBTkQzLC82SURNz7u4vr+7t73BvbzEzs3O0dLzW/tb
+SEpNSUxVZevrTz9KPTI6OTc8R1FOTkxTXV9VXffr187SycXNycnt2cnL0bmzubO3vr/DXD86
+Ozs7PkZEODU0OD5PZ8i6u7q6vt/dysjvadnHyv9U08zZa8jEUWM3PD0uLDU+S0ZMVFdES2HL
+tbKzs7Czs7W9z9/Z1mNGS2vf/2Np2Ug5NDw7NDc8Zelnb9fLzM6+vLm9wr7Ed0lN4/9GQkJB
+Ozk8RlNNYcbHzMvbWe3W68zEy83MbU1KTz5CTU9AQz9ASVFTY9zT1c/DvLm6v7vAz8vL1Wvp
+7//ryNZ3/+lVUVlbb+fd287L2V/e1eNzX0pKRjk0Okhb5c/KytnV3OPzX1ljZ1NGQUNZ59vB
+s7W8wMr3b3dfT05MXVtfY8a8xNPRzuNIOjg7PTk4RfdzW3fb3dbS07+8v7+8v8PEzN/n81VG
+PTMxNDU1OD5AREtFSFv/3M7CxMXI0tXJxMfKuLi7xOvnztfXzdRPP2NfN3f318jYyMbrU0xV
+5/Nf1cC+yMvf6917e+fba09VTUlEP0FVV0hBPzw+PkNES1Nr7czZ39nIwL68vMnJ7Xfv+1v3
+08vT2uv7/11PTldJPTw7Okbr2My/wMjN2VdaWktFWe9hXevjy8G+vru3xM/VaVNXSj9MV05M
+91FJSlJX99ZvbWFP89X3V83K0W9zSE1NUf/Bws3LydTjyr3IyMj7UfdPUW/U78i+yuFvT0z/
+a1FOXFVcT0A9R1dQW+/z2+drd8rB09TK2U1LPTQ1NjVHW1tSysXS78bIw7q+0v9PR01PU9/J
+xMnD309Y3/db691VR0U/SFvj0cDCy9vtNELKL9xWPcRD1dNZvNdrubjf0dfp18nPzNPY/1BI
+PTg6MT7CPGPIxL5HRePFxtvjSt9KP2FZe83Jv9fj7U5PXlHl2XfvY1ZPS1nTyMC+2GV3Xmvd
+98Y3vrA9PD88uuPRr3vLzEh33l/n3cXDylFPMzE6LjY9TENVa/fny8G9tLfC1N1zOlc+Y9Pa
+uMPNx3Pj42Pr3lNXRj53Szaz6zi+P7m6NsRBQbc+XctXVkvczLfFusXd1VxGymHLZ9Xrad4z
+Oj00TCphxy5S1y/PRFm/39bLZ0jZY9+9U7nP1k+/OEPFN8HNSc33Mki/P89N2rnTx99Kx1Xr
+Os1fyUk6uz1by0mzc/fE9zx33Efc58Tzx0nlwTjKWzpX0MM199lHW7DptOe/30vPd1ZExtTV
+ucA7uz/JwN9lutVDSEtDU9/SU7tBzdRAvE//1z7GRllMO88+uU1ruW+zwUvrb0zEQ8ZLS05W
+PD08R91Gb0ha11XRRcJrb7TXVrpLQ8df17xnylPvvUXKukmxPlS7Ob9KQ7hD3NnIwTjA1Vuy
+Se++81Tbb0Ln31nhzcfUvEbGPM7Pu1S+tjy4Pjm3O8zIUzbE5yzDTVO9vTu1uz62UD27VU+9
+Z3fMzjm+bze+Osq+RDW+7z68OWG8XF9I2zjRNvdc2U1E2Tu+dz64W+W3UVHKvjXF50S/2TDL
+NzfPVTu7SEfITvPn0E292UvKXbhB29W8zle067/3R1awRb/Jzu9t3DhP1zXAW8rYrtA8x7tL
+Rq9Xd+9ETdlXQkXAPbzZRNpZv03Ax8tLZzrjey5Ga8Ezw1s30/suz7bdSk7bWN9TPbtMVdFl
+2TP/TsvFPLtZQ84z1Ek42s7nSbl7V8V7V8NvxVE/1VxRc2drwW9B2rNKT8y9ytXRw8FUQsTR
+PURbVjrJPj5GXLFzTrW4zTvMvlLr90S7e0hDtU1GssnVukfY50s843dEONFTzOfMt2td6dHl
+PzTjPys8Z0BMXdm2ulm5u+/nuMdO3N1PNkhVLlVELzJNWzHZxeO5uv9Z1ckyMlHtuzfWpbNd
+16++RjFR0TUrPbzvLNuyVDSworJCr6a6RSlCsc0mMb3NMy3Nrq/DrKKnwWnntDYqQ0g4LTy2
+vse5srbeZT03LCcrMy4yPMO5t6ymoaWvsMZWQi8lJS4qLC03S764/7mpvDzrvy1ES7rWRUA/
+rkM0zavDSa6828XBPjE9QzD/bS7jt2/zzz1nuk4rQctBvUMxsq8+K/fKNSguurbPOUarrktG
+rKGpvby0rN83Q984JSUuT8LHT66frdwww709JUzSUCsu662xSu+soMMrQK/BLCvvs7Q+L1u+
+ySoqwbIyMMdKSHdKy7CqX8Ct1d3tyMDNxkwxRTAqLO/dOzZdVUgvM1fRw907z7DBMjrPrLZ3
+26up1FHMvus4OExLMi1E49vnwbOusWHMsbBLNsm2Nf9ZLza8MSjfrDouxq3KY+k7Qcc+LNOv
+38quvju7p61rLTq8qUclOLRnJyzXpK4zK8qtQSc3q7dKLVa4tFkqPbyqazzn685ZOzXDuj0q
+OU0+LkG9r7U6OLGkySkv6aOvMi65o7s8JWmosDMsuKvZKTTDqtErKVOs0yw1rrMvLduotNgy
+WKu2LSnAq9U2xbmxxTAytK82N7VrSrz3LsusVjWyyzHerzguxP8oMbG+NkW03TxHWEy3tz06
+ualON83eNkbAQ/Opu109PDU8S93fwL2/Uyouv0Ex26WrTURAPM29OTGzrkUtyaq+ODk7ratR
+K0yyOSY76biw1SdErdEuSv9YrK5ML82nYzNH2XfB1TTzralBM2e090EsUautPzHXres2LVOz
+sb+7V1e6QicnOGW7q9XjrsApKsvTRW9lyqzLJzi+vT8xN8SnxjQvx9dFNEXNwO/rvttTS0Y2
+PK+2PE/APWWyNzO+sTtcvddvvkMxX8//QUq00tjEucVNLylQwrxOMryo/0W/u+fRby1Lrbkt
+U7zN2zw4uqe0Xc7HSigu3aas3F33z1k2K1+qqMo2O0P7bzUyR7S4v9NDTMi/4+OzxEztZ0Zf
+WzE6uPM0OfutrcwoLbiq2TnTzsQvKDi02TvfwSopy62qwSgiVapWLDG3oao/Jy1Ctq3NRV62
+1WVLPz13rr1HSFlD0cBMN1nX18tGOcymwk/LsbpHMig2wbLWW8Oup9EsLOmvv0UvwK7KLTLN
+sK3IOEXHxzwpK+2svG82c7SuVjlWZ29lzlEtSbrKP8O5QS5vw0/Jsj9IvqzdLiEoxKm0NCw1
+O9y+R1OrqNs2JS/BsbTvWee/w005O0e7rLhISbi8Py0+58vGa1PQyc7XU0BJa//Hs7nd/9XO
+zWtCPkvbx8JnQsSzx03nvcXvYUo3M0xb69H398e6y0Q/W9O5v1hC0Md3REY/X8nhXFFbX+ta
+TsW6vcfGb07XzUxE5+9PRT4/Ozc228BnPUA5U8hfR2W/zFVRP0fIvszvtsT7W+lb48LB3s7B
+RjIvUOtPW9VSO0FvVVzRxe/X0/dG97zLyLjDy72270Y/S1bnymVF/7vC3drJzffdZTQuOV1z
+41tAU7e7Z+HDe8u360dOe0k/Ni9Ou8/rXz1G2cXXw7/OwsRdRFF3vMo8NlVvV048Kk7BVztJ
+RDvdwVNPycztz8PKv722sLi+02FCSffRWEM5QVdGPmfUX088OkvnvP89PcS9zsa72NPByGO8
+tr3tX1tV61h33VvfTzw+WktJzspJPVNYVV1vybu9yHfv/1Rdzru7vca/YTxnzsfezM5jd1dv
+XWPX619j+0NLVDw+TUs/Sz82Nu/rXdLZY07r1OdXTlvbwcPfXFdU68jD1dTW1/vfa09Vz9dR
+Rv9WQDdNzrWsvlJO08zR1crEvLvETjk7S93N41tnSjw5RWfMxVw9U/PjXf/v5d3n0dF7d+vL
+wL/FTUvXymvvUE//u8FDOTxFUffvTU5hb2VRW/9d+9nGysr3R0nKxMDMVV1Y51Vj6+lMRkZd
+829nSU/Kw2dET1Hc3crQ3e33/0pKXPfvysLJ2V0+PDxP0c7ad1dIRWPT08q/u793P0ZKYcDD
+Y1vnX0BHTs64uMtj3s27uLu4ucPN70NCSFN370tPTEBBOj1Zys/nTU9XTUZpv77H70lHZ/vO
+11/vx7/N90xGPEbjy8heQz5EQU1NSWvVY0c7RVPnwsjJy87nXXfDtri6yGdJa+9Tae3r5cnT
+TTk8TNnAw15FW95HUdm/ytRdSV/nT0Rr1c9lPzgyP1H3wbm5ub3DzdTdw8fvVz05Oz09Rk5b
+/29rb1rvyr+3s7i+0Vk/PUzMv9FdTEpLU/PLxcHHyvddSEVWe3dTSkpJRDo/78G/xdNrZVdB
+TNfnY01FRT04Nj/NvL/O4+tvUlnXxru/yVtFPkJt59HFy9/pa1Nvyrq4vcTra1FNWuvNzMvd
+a15PX9LJz8vr/2dPRU9r3tvdb0U8PD9Y187Wd1FNSkpV183Cv7zFaU5c993QXUA7NzY2P1/d
+4d3ra29ZV823try/w8fj49fLxsvnTkA9PVHfysfJ111NPzxDX29fU0xJSlRjzsK8ycrjUUZV
+XWve2eNTRkRGTd7JysnMYUpGRlnRx8nVd11KSUVLb8nAwczT5djCvri7vs7Zd0dEZ+PR019R
+TVFNXtfHzNXn/2NMR0JU993pTEE+Q1Pn92NNT09XTlP/ycPP3F9tR0VJZ9nZ1+NJQUVVXc/F
+zVtISUlh68nCwLy/xdPd48y8x9hvU0dEQ0h3zMrU3/9jTlD3ycrnTkVAQ0RI5cbBxc5lRkRc
+y7/AyntNQD5CVtXIy9P7Xlv/49/EvsvaTT48QUdZZWNZc2NVe8a5tLW+ydNdSEdLb9jvYVxf
+WV9ryr27v8d3SURDTf/zW0tDPD9CTWnPz+Prd0xIS0//09leV0ZAQEvvyMrX3t9v7+fp0cnf
+X1lGQExrzL68wMbFy9Lb08G8xe9OQ0JGV//XzfNNTEVET2PZytfvX1dPU2fevri802tZSk5N
+Tuvj/1xIRUZZa93Jz+9ZSUlOX+/PzOdPRkE/Rk1f0tHb4e//59XEuLe8y+lZSkZJc8/P+09T
+VV37387ExM/hWUlGS1zv4e1bT0xHSU1l499dTUhGVXfXxcDE3Onna/fj2NPX41lTTUtUZ+Xh
+33tjX1133tHFvL/EyczIx8nN095zWVZPTlVfc/NjTkhJSkhNUld3d19dXXPp08nAwc/j82FR
+TExNWWNZU1NQV2Vt99v3V1RZXWd38+vvY1FQT0tTa/fa3+/p1dfLx8PAwMfZ/11aZXf73d/z
+5/NrX2Nfa9vZ921jVFlfaefd52ttb19WTk9TV01MU1Bfd+vTx8jNztXz493l39XfY2lfU0lC
+SE5dZ11n7efn3M+/vb6/ws7/Z1dv721TWU5MTlFSWf979+9tU1FOW//nb/Pj7fP3/+/f5WNf
+T0FESE1X7+fj2/dfWFVb3dXb1dt3aWNhb+/pd3NhWWFjd8/DxsfGyM7R0c/K0eP7b1RNTVzj
+yszn4953YV1d99X3a2taT1FWZ9jQ2+t3a05HSkxf93dfWVNUXWPdz+Xv/15bV1Nj2dLd+2tX
+UEtQZ9HS2c7W397Wy7+7wMjVb1VPT1Xz4/drW05LTVRt0+ldV1JHREZT39HT1917Y19v49nf
+Y1RTSkZKV+HFyc/T71tRUWfRzdfva1RNSU7/3dxnXF5fVl3fxr29wcXM193f18zO52NVTEtL
+XuPM2P93XktFSFPr22tbV1BTUWXVys7vWkxJQT9IY+PpZ2dnY1tn3cvK429ZVVNRV93N0mta
+W1lPTunMwsfN0dXX3s/EvsPO3GNWTEtZ18/Xb2FbUU5P5c/L3W9fUk1JUN3Iyc7nb19YUV7b
+3/tZTktLSlfPw7/L2/djVU9b18zT61hJRkNFXNPV7WdYUFFd38W7u8LO2+tjc9nDx9l3WkxJ
+SU3pzNN3U0dDP0NV69brXlVOTlNf0cPK1ltMRD9AS+3X3W9jU01LXNPJy9n/XlFMTV3azNbj
+Y1VKR1bVwL7AxszX5evQwb7I1/tlTk1f28zM329VS0ZFV9fMzttzVUtFVd7Mxcvd711JRE5n
+9/9WTEU+PEFrz8nM1/NjTk7ry8XFzm9rTD9HWefa2+9eUUlL98e+vsTXbVVNVtXDv8LXd1dL
+SmHn09V3VUc+PUV30c3R5V1dUE/dxsPGzV9TRD5G99rT1d1vVUtMXNnIy873Z0pGTnfX0dj3
+V01ESe/Lv7y+x87X79fMxsPK12dTSUz/0snK2FtLRT5Eb+Pb33ddTUZKX9TFxMbdWkhDSVtj
+Z29UST49Qlfr1c7R+15NT+fbycXG0OtOQ0hf697X22NORkvjz8rHx9VvU0dY59XNytLfZ1Zb
+383NzM53UEJATO/W083db1lKVdHKxsfOWUs9PEln3NnT22NJQkZz09XV13tOQj9O39TT2P9b
+ST1Ia8vGw8XM3W/v0cK9wcbdXklHUtvMy9HhV0g+O0lc6+//XUxBQUrvycTJy9VtTlNp2dnn
+Z0xDPTtD+9bLzd1vU0hQ98bCxcnha09LVdrLyM3nXU5DS9/Iw8LJ62tMSFvhysbH1/9bTVLX
+xcHCyfddTkdX1c7Ky+1fSUVLb87GydNvSTs6PlNz5+tzU0M/RHvOy8vNe1RCQVXj1M/W6VND
+QErvysG+wtJrU1nVxL++xutPRD5K28zIyedYQTw/TfPVztVdRjw+TOvOycbYZ0xLVdjIx8zb
+VUc8PUxv0MnO22NLRkrrysTFzW1LQURd0MnJzON3TUZZ08S/wMrZV0ZHVd7NycznWk5JWM/E
+wcTWa1FGR2nbz87X/09DQ0vny8bK1V9IPjtGZ+/t61ZGQT5E+9TIytHvWktJT9nJycrnWUpH
+Rm3Iw8HF229bVmvKwL/H3WNMREdazcPCxdRnT0hJ/87LzeNVS0JEWdvJxcrnY09OXd/GydX/
+TUE/P0rjzMvaa1FMRk3jx7/E1ftRSUNFc8vL0ndSS0xP98O7vcrfZVVLV9PGyONhU0tITV/M
+v8znV0tFRU73zczfZ1VNSlfvw7zC2W9bSkNCSf/dXE5JREFCS+fHyeddVU5NU/PNv813Z1hP
+VV3jwbzJ1d/v//ffw7zA221XS0dKYdfAzOfnd1lSVePHx91fU0dCQE3byc7/d19XVFfjzch7
+TElDQkdMa8vP92tnVlRb5cS+ym9bVEpHSFfdzHtRUUxLTl3PwsfZ6/dnX2PlysPVa11cV1t3
+28XE3HtnVlFTXuvW5VtTU1hl68y/vcXf721OSUpRaW9RS0hISUxf3c7Q91pbWVxj3c/Jz/9v
+d2/z1czEv8zZ4+Hv49fMycnXX1dRTk9YZd3cZ11dVVll69bP31xRSkhMWm/TzeN773d773N7
+615FQkNESlBd4czV7+vt7+Pf1c3Pa1FOTEtMTlfnbU9NWGfz59nJw83f5+Pe3ePZy8jf8+fr
+5d/f08rX/19fV1dPT2n3Z1FXc/fv7dPIzOdlYVVPTUta92FRTlZdW1VZ7+F3XG//d19d99va
+b2/p3ePr3cvEy9LPz9XvZ2/d41dOVVVPSlRp2913d+//Y15r599fV1thWU9f18rM3dbR32NT
+VlpvVUlKTEtITGPUz9vp3ed3XXPbz9n/e29jUU9d89fj79/n/23318bDytfR6W9de9PKytXU
+1ON3Z3fYz+dvZ1BIQkNNXWdbd+t7e//by8fN42lXSkJDS23vc21vY1tPVu/Y3O9vWU1LTmPd
+0+X3/21rW//Xw7/JzdPjXVVr2djhd15RUUpJd9zT3f9rXldQb93S3fNzW1lPU+XKxMbN4WtV
+SUpdaWtYSkdGQUhezMfN0+13X1hr0MnO2G9WVU9Md8rFydPr3fdd6czBydn/V1FJSVvOxsnW
+92VYTVTZzM3bYUxLQkJW6c/U43dhW1NZ2cnL12lNR0I/TOfT0uddVk5FRV/Z0dxjVUxEQkrn
+z9PnY1lVVmPIuLW6v8vZXU5d3czK2OtnWExLd8nEydn/W0pHTPPPzuVfV05ETN3JwMPS32dI
+PkJc39rhb1VMQkVvzcjI2m9fSkBP28zIzuttV0NFWdPExcrZ51dR98jBxM97T0U9QGXayMfV
+62dGQ03bycbR71dHPkBc0cjJ0etfTkhd1MvJ21FJPTg+TO/Pzt1hTj8+TenNzdJrVEVASuvK
+xMXQ72tQT9nDubm9w89jTlXdy8jI0/dVSEdrzsfGyutZS0NId+HT0u9XST5DT9/Kw8TU/0lC
+Rln/3dx3VExCQ2fby8vS81lDPkpd59nV71RGP0Jj18nGyNXpYVfdwr6+wdhtSD9GX9zNyMnf
+W0M/T9/QysnXZ0tFSfPOx8XD1u1RS2PXzMzN2FlDOTpIb93W0XdORD5K+9PPzd9dSz9I79HJ
+xsrYY0pLd8a8vL3D2FdGTevZ19Xcb1dGRHfPwcPF2PtKQkln0M7P22dJPj9Y1czM0/9TQjs/
+WPvj3/9fTD9KZ9HLzNF7Uz48RG/b3N93VEQ9QGfSysbL191dW8+/urvAy+lMP0Nn08vKz+Nc
+S0NW1cvLzfdVSD9J48zEwMjT51NSb8vGyc3/Tj89QV7Xzs7ZX04+PUlr6+f3V0s+PERb08nF
+zetVSkvnw7y7vMjfWktd2c7O1+1fSD5AVc+/wcLPd0pIT93MzM3bX0Q+P1PXzMvPa1A9OTxI
+Z+/l+1lKRkzrxr+/wdddR0VL683Nz+NWRz5Fd9PFwcfP/09OX8a7vb/J50tBRm/NyMfN62NI
+QU/TysrM32dMQENP1cfGxdHpW1NZ28bEx9VZSz87R1zf2NfdVEM9PUdr5+9vSz86OT9j2dLM
+2HdfVmPLvrm5vMHPa1lnz8vR1XtMRT8+T9XJyc/vWVNMV9jLyc3b/1RKTVvRx8rP71VDPjxK
+/+Pf71lQSkxf1cK/yuN3VElNX9PJzNlhSkRCSO3Iw8nX92ldZ9PDvcHU61FGQENdz8nN32VT
+TU1fzsHE1mtRS0ZM783Gy9z3Y1ZRW9fFy99TR0JARFXVy9NrT0dBP0Zf3+VdS0VBP0RN1sXS
+6WdeZ2fZv7S0vMTL0+v/28vCz19OTElNU//Iwdp7W1JOUVnjysxvWlNTWGPdybzF43daTEVD
+TtnO71pfVFRXY87Axf9WT0pNTF/bxt9XTklHRkhdzszna2938/fQvLnG52dTTExMb8jG529r
+X2Nbb8rC129SSkZHSvfGxNh3Y11jXmvdyeNMRD9BRUdezszvZ1NNTk5P88zVa05LSkpNa93K
+0/tpb//r3cW5ub3Fys/V3tnOzN9ZS0lJTVX308vjY11dXF9r49Hhd1VVVV/73czExuFfWUxJ
+SExf/29XU1Fca2/h0+VjTEtLTlRb8+fvWU5PU1tp7dHKz9vb18/MycbGx9xfUU9TU1nz2+dh
+XV1hb+/hzs3rZ1dXY2//48nI13fv49vj7+vjX0tGSE1bXGHn2f9fYWf/92/t0dNpTlFUXVpb
+d9vvXF7/3NXZzMDAxs7V19Xbb+/Z41dRXGtvXV/r3P9bXWd7X1FY/3deUVr/3Xdr2M3VaV1V
+UUlDSF/va1tdZ/djX/PX52tVYWdfTExdb1tOU1dfW2fjyMLHyMbBw8rOy8bO811dXU5IS237
+Y1ldZ15XXf/Mzt3r7d/rd2vRxsza39fa/11d9/tVSktOTkdHT//ta2dv/11VUf/j71hTUk9L
+TmPj29/Z1NPW19HGwcTK0d7vXVJb8+tvaXdnWlNb693d7/P3d1pMTmX3ZVxdYV1RVuPNzdnh
+b19NSVL/3djd5ef/Xl1v2tHd6/9bS0VETWl7Z15fV05V7c6+vb29wMfP09PIyNnpZ1FGSVPv
+3N3j/1VMSU5n6dXa529dUVVf28jFys7XbVpVXvf3c11ORkJAR1lz2+Pra1RNTVbf1Nnj/1dL
+R0x3187P09jfd/PRxr++xdl3UkdFTnfd3+d3XVBPX9nMzNHfd1xMS1dv5d93Y11VTVHhy8nL
+3WtPQUBK/87N0ePzaVVY69HM1e9hTkA8PU138/tnV0tHTHfJv728v8PL09LIwb/J52VMPz9H
+c9PW3WtQREJFX+PZ22tTTklDVeXCv8DGz99bU2vY1d9zT0Y/PURX39jfc1dNRUNSe9PV72NV
+SEhO787Ex9Hjd2lv1cK8u73O91tOTmvjztd3Y1hOV3fOxcPI1ndUUVt729feY1FOTFjr0cTC
+02NRQTxDTF3p63daT0tP98/Hx9BrTUQ9RFVv3Nl7W01FTv/SxL+/ytXr3s/Hv77E2l1HQ0ZY
+/9/bb1RKP0Jb/97b42lOQkBFZ9fJxMXO32tz18vKz95fRj89TP/f0s/f+09ER1f34+fzVUc8
+OkJj2M/N1N3/W2/Lvbu7vcnR/2P/1crHzNp3TURKa9PLzM/nX0tGUe3Uzt5fXUhATG3Ry87r
+X0o8O0NU9+97WVpJSFrRw8DDz29XQ0FRZ+Pnb1lMRUBH38jAw8jT32Nj28fBwsnrU0g/SHPc
+zM/nZ1JFRlPdzsvN32NOSkzny8fFydPna13jzcrN2l9TRT5KX9XOzttfTUREV+3V1elTRz87
+P1n/2NXh92dZa8q+ubm7wtpjVFzbzMnK129QSUdn28/N011PQUFMY9/X3PNZSUFDZ83Gys9z
+TUA+Rl/f2dxfT0dBTO/Xy8vZbU1AQEr3z83VZ05BP0nvysK+w87pZW/Rwr6/x+lfR0BJXdnM
+zdpfT0ZHZ87Gx8rlXU1JT9nKwsLK3W1OTW/b0Nn/WUo+PkNpzsjIzntPRURO69nd91FJPzxD
+UOfc4/9bTklTd8S7urvC12tZXePIxMreW05HSFrpy8fL1WNLRUZb4dHX71dNSUlWzsLAxddX
+SkNBW9zL0N/3WU1KTefIyM3rTkQ/PEjfz83XZ1RLR03rwr2+wszl/+3Zwry8w91RSkRCVdnO
+zdlbS0VGTvfGwMnWY0tGRU7XxL/G129cVVFrzcvcX0dCPj9IZ83I1OlXSERET+PO1mNNRUNC
+R2vWytpdT0pGSWfRwL7L1mdjX13nw73D2WtTU1NfzsK/z+9rVE1OUu/N0fdUTkpLUe/Ev8bd
+XUlFREvvzcfa/2dUTlhrzsLM51pKQ0FBTt3M1mdaU0tPXdG/usTO1efr3dTEvMffW0xHSEhV
+0857T0dCQUZP/8XE3nNXT1Vde8a8xM3Zd2FdVXfPzmFNRkJESlH3ys9nWUxJS01e1c/lV01H
+R09Z3cjM72NbWmP/1cW7v8/b7213b97Ew8zrbV9j7+PNxsfab2teW1he39HnV1FLTldb78/T
+6VNKRkhMWeXOyd9rY15nc+vTzuNnVk1NTk9d5+NbTE1KTlb/z8fI1dvd2c3PzsfK91lTTU5R
+W//Z1XNOSklLVmf309tjWVtn79nOxL/Iz9//b2tXW+vnV0pLTlFcY2vh61hPSExjX2Xn195b
+U1Vj9//v19PrZ3Pz1czLxb6+zd/3/+//a+vW2nddZ2/j92/r1fdbV11jXFNV9+tlWVtv7/9h
+b+9vTUlLU2Nj79nL0vd3d+97ZWfn5/9YVVpbUUlPX1xJSVNbXFt328bGzMvGxM3a39ndZ1hb
+be/3d+PT1GtjaW9zY2/n1etdY//n83vnzsvZ5f9vYU5JVf/3Y3vv429bWXvf/2VjbWdXUFvp
+329pd/tvV1n/29vv7eHR3ePfy8jP2+f/WUxHVe3d7/fj2+9dWW3b3e//e19PSU1z3ePn3en3
+d1xj6/9bUlVPTk1Qa9vc6//vY1dQUXvZ3+vrY09NSVF36+/7d2dlWW/ax8LDw8TJ2/P31tHV
+3v9va1lSa9vP3elnW09IUF7f5+vvb19QTlvcx8fN2fdjSkVMXeXj3ud3XVVP/9HT2edeTkhH
+S2nf63NZUExDRlnn09HV2+/7d+fHvcDGz/9RR0ln2M7O1+tvVElN59fU1913VUlFT93Ny83Z
+82tXWe/R0O9fT0pCP0ph18/T2ftjTUxX18vP1f9hS0dM787Jy9Pn/1ZV886/v8PK0PdXVvfQ
+zt1tU05HQU3nzcvV72NVSEz/2s7Z/19URkVO28fExs7nVkc/S2/r4/dtaU9LV9/Ix83j91lD
+QUpr+2dXTEc/PUFl1c3V6/9vV1nbxLm5vsbTX0xP3svHydlzXUlFT9zMzNZ3W05AQlnjz87Y
+5f9aVWfOxsnQY0o/OzxN3s3K0O//UUdX183HzO9jSz9EVenMzNT3YUxLZc7Cv8PR53dWWdrO
+ydFnU0g+QE//0snP42dTSEzlzMjJ02lPRUBL4c7LzudrTUFFTF3v5+tfV0xNb83Bvr/K3ldJ
+S1v/7/9tT0pAPUtl2c/N1ftbUVrTwry7vcnhY05l2c7IxtZ3V0hN79fNz9dvU0FASXfc2dnl
+b11ST9vEwcXK81hBOj5O59XV3W9SREdX3dvb42FNPz1Ea9fV0tl7W0xT18nCwcXM315VY9TK
+y9P/U0Q/QVXr2dned11RTV/b0c/Z72lVS05r1szO0edlTEdOd97d72FTTUhM38W/w8nW51VN
+Z9PJy9xvTUA6O0/v19ffX1dLRl/Hv7u+ydPvU1Nz0cfIz9zvVkpZ1cfL03dUSD5BY9zRz+1b
+V0lGV+HIx87rUkU7PEr709PeY1RJR07jzs3XY0xGP0Fc5c/R32NUSEpay767vMbN5W9j2sfD
+x9dXSD89RHvVytD3XU1CRVLVx8nN71tKRk3Xv7u9xtxzSkdX387R52dPRkNN/8rBwsrvVUhF
+U9jJx9D/S0M8PFHv1d1vWE1FRlHNvr7B0XtTTlHjyMTG1G9XUk3vxMDF0WFORD9DV9vKzdxf
+TUdFV87Gxs9fRT47O1DhzM3XaVlNS1nbxcXP41VJQ0JXz8TH02VKR0VL28S8vcXN3fdt98y/
+wtdvS0M+P0/Xys3fV0pHRk3ey8XP719RTVNzw7q6vs13U0pFUt/X71NIQENEU9DDwtNvUUhC
+Rk7TyNHvU0A9PT5SzsjO91ZPTlr3zbq3wM5nT0pMWM7Cx99jTk9OU9m/vsxvUUxGRk3/x8Pb
+d1VPTlf/x73B900/PT1AWMa9x+dpYVtb986+vdllTklJS1rPwMV3TURDSFHpyrvC2etnX2dv
+18DB6VlLRkdMX87Dy2dQS0xOVdzHweFRTUtRWWnMvL/R+1VOTUhL79z3SkVCRk1X3cfA111O
+S09SUufJ1WdMREZITWfHwMrrb2dn++PPvbzPd1lPT1NZ2cfRd1VLTE9Vb8i/zm9fXF9rd9nA
+wNLp/2X39+XJwMlfSEA/QkNR183hW1FPWWN33cbD3GVLSE1NWt3N019OS0xca9bIv8v/Z15n
+Y//fzNVvU01PV1/pzsnQb1JRV2Fv0szMe05JSEtVX9/MytdfV1ddVV3/3eVZVFlb8+PQy8XR
+b11bY3f/59/nZ0pER0xZbd3Oy93r79vRzsvGw833Z11bb3P/2dfnWlNbY2t33c/X92Fjb/vr
+79jK1+tr99vX4evX611HQ0NDRUlXb2VPTlNf+//vzsjcb2Nnd29p99TW91dfb+n/d9PK029c
+YXdzV1V361pPU2Xn523dzdNrWmv33f/r287tVVVl7+1z/8/L3Ptvc3dVSlPv811c/9/a/+3W
+ydT3Z+fdc1lTd/9dSkpPVE1KWefa7+XTy8vZ1srAyd/j63dSSk5v4/9cZ21fSUhd59737eXb
+e1NV68zR2tPQz/Npa9/3VUpHSEY/Q1vv+2Nrb/dhUVPdzdbb293vX1j/0tDb3+Pta1VR3cnI
+0dXZ61JHSGHrd29v82tdW+PJyM3R1dv/WWHpz9nj4/NvXFlf1MnZ5+ttTkVASmn/a+/j72db
+We/LzNvb3/dcUVPn2ev/Z1NKQkFV3tnf1dvd93fWx7/Hz9f/TkA/TO/b3+N3YVBHTf/n3+f3
+ZVFGSFXTyMbGydb/XF3d0+t3XU5APT1M69PY3ftvU0ZLY9XO0NXrX01MWNXJys/db1NMTenC
+vb2/x85rT1Fv3t9nWE1GP0BS08PDy9j/XUxN+9XN3mddVEdJ98zExtLnWUQ8PEv/3+n3bVtR
+TV/MwcHJ33dbS0xe2czP22lTRDs+X9na3GtdU1Fb18G9wM73Y0o9QlPb0M3X+3dSUnfQxsTP
+61pMSEf3y8jFzc7T82/by8PL42NLPzo6TO3My/NVTEdGW8/H1e9lX1dKTdnNyMXd3+1MTFvZ
+v7m7w91jXlvf2dXea05HPj5Lb9rMzdDvTTo6Tffb3f9PRENFZ83CvcHL419OVGdr5WtdW01G
+RErdx8TE229JQ0z3z8vI1mtLQ0BN79nU0WtOQ0NZ2crAwL/I3VFHTWvXy8vR51tMXdvJxMnZ
+Y05MW2vT0N/O0vdcSEbau8XI1UxOQjhET0tXYUlb7UdabdvDwsbdVzxPY8e/bVnjZUtVPT3N
+yr21z9drX2/OzdvJz+9bODc9UePOzVvT1udrRC4qLkqyrK++1uNMZ1tJW0vav6u0t71F3VdC
+PzU1OVJrzsZjX+fKurnM39nj42VGZ2NBY0xOSURANLSlo6VPKh8nSq+fn6Gq50YtJCQrLz69
+u7S998+9v797Ni4xKzpvXbWwrq3H32NI1r/D1vdnP0g9KDlCM/tAMTUvOb+vublGOfdOv6+r
+qbC+SkUuLT9OvLzd1UZPzysuTbypt0ElHyy/p6Gfrba+OicfISnPtbOvvbm3sLe8xTkzKiIl
+KCtFva6qqK+tuGPN28O8yE9jOzlrSfPbR0A4NTp716+tt79vV9rPx7i/0c8yKyslLjUpO6ys
+qLkpHyYwtKu+q662sckvPWNXt9nnx8C1qKq0tV09MyMfHyQvO1NFT8zMvbKxr7DId0E0Li43
+Su/XzdLr1c7Fua+vr7zbzse5ra6uq7HLOTAlKjM0Ql1va+M6KjFbycTeOkL397ywuba3vLvK
+UUppy7W62Uk4MzpP78XI5VM4LCYmLT9Ye//hy8fHyMS+vcnbb1Bf18q3tbzH61FVSkNZ59r7
+ST49PUNj0cC+ztPVXkxHQWPN09tpRkVPd8q5vO1dT01RPjpV3MG/xc7Myb++vsf/92VOPjo+
+U99nPTk8SePOzsTD0eljXV1b3b21t8vnc3fbz8e7vclrQDQyMzZK38jGyMzTSEy5REVZ3ExH
+PkdI3b+4v9VfUl5NPTXD0kRBPUhNd86tvM9lSnvrY8K5zsffUDld7V9GSMldT0hOR1w6OstX
+Y1tnVrC5wqy+wb9A20VKzvNDrrr33TlHudDOs0fbTzpBOz9fW9PaO1Tf12XG1XdIXThvQ0JX
+v66suEJETeXl0Ve2vmk9Ly8tP1PNusqvu82/1b7C2UNMNjRbSjU6LzPKuLOqrKi12zk/NTg+
+S0vdwjxAQyMyRS3HxDbQybztNTpIvKWkuMq6srVvOiUutKaswCofJSw5u8a2oafGQC8rc9/V
+tbCno6WzyT4uLyonJSsqNjhGurW1uMrTy+PDtke+rFPMtEu7vjNfPi7vQS334TTD2tuptl3p
+Y7irt8g5MC4kZdH/PiwrQjkpMC/Koaauu1w+TzIvvLCmqNEz2N9JbSooTkg6LjrPprE7QsjG
+vuM5uKq+ubbPsco8u1QsJCI4sqtBNzX3u0Et86ygo+vHrcOtr8a96yUlLShZqK21UiEfISk9
+rqmjn7NfUzQ0VVTVpaeur0I6STMqLzs+21kvRdK0smE3PETVu6+sqLRTNSMtPzVjuffpPic9
+Wb3IzL24SiwpNLKqqKCpuEEnID08zq65sMpCQTIjNbfGvjQjNVVDyKupn6pNQDg221M1trK4
+v0I9vb5bTzU7X0MvP1q+uMrCvr7Me2fExj7G2b+460TBsUozJifZRnvN0cFILWs9QLmyd6u6
+SVYuQ+3de9NvRy8jNberr98uMTU7P/exoKS0UT0vLSQzU/+9tL2xu2dKw+97XTM7NTIyPWm4
+va+pxazKzLGtv7lIVT05NztVyPM9LjtZvs2zqcTGTyzX0b+trMm4QycqKDmuvLezZV88KCxC
+2bO4xLfDPlQuPnfX2b/X/+NXWNXHxrpcW1Q+PzcyP0pTz8LPwbK2t+/O619IKjEtPDdJN9E+
+wttJ07zLvb3MwtHH3Xfvwm9d4zxLPkXnzs7n48Jady5j0//DT9rN71xXOvdP1uNj2etK48rG
+wWfIT1s//9O0ta69w3s9MTA7Y8Xd4c1GRT1Ib8bhzNRfTURFyL64v+NnXUFHZ8nIyndJRTc7
+Vdq6t75vQzU5PkzczcjpRTw8PEhYW9Pd520+PjxJ0cy9vuO1zL67u7a5ws5fTzszOj08Vk9V
+Wj9DUsLMw85vzu9ZU+nfzcrTx8Ld17++vL3K0VtKPEdJ4+PX2W1va2XPxsHG0Fc+PC84Rltv
+zMvZ0UxKNk4+X07PydnHwbTHw8rt9088QEGws29V/9xZ/+/fzf/f4UlBMTpJTtR7181TST9b
+zW/AykvhTUdERj/Dy8Lpykb3Q01C2Gu9XD8+P8d7y8/LuMvtu97Vyr65tblVXEA6aUZv5Vvn
+QmtZxWXv0dpGv2vD58u0Z8M93Dj7P7r3usrf00dCRkLPY2nCTUw/Umfv41Hd/81PPldPZ/Pb
+S8s7Vjlv78i6wrzr50DnTNTfv9hf60fORlTFUbg/7zxRQP/UT7tpWUdEVzu6293G1sVJ2z7A
+wsfTslXWSVPXOsFOtFW5RWVMyEpGvUPISL49ylPO1+NYuFPN6d61xMZnwzfOTU/hvU7fa0Zc
+NVLrvt1H93dNWDXbTLBPUss+vkFXR8FJvl9bW0HMSV9Jx0TZQ2tXQ7ZPtD57N1NOMck4u8tJ
+WEhnSfdIs1fFT2/fU9w/ulnFv1F3PzrOd1e40XfRLkXfOtfA/8iuTVW6Pce+RLXEV8PrPbrH
+97zIPcY2K807Z8Nb7/+8P//fQq/3VcRIU0g3xP/LzVlZ/1VI2M/Gw7pWRdFHTHvdU3dTZ2NZ
+PjdRacjr0ffN1kBtRP/VPk09PEVBy8HFusnR401PUs/K18xHSkJKW02+v76/TE7ZUstZzcrJ
+ZUhHOEVVZ8e640Vvt7XM2uPJtLXF4ffpz+dcVTo9ODo1Nj/P6dvOb+vF1sy7vca8XfNhSENr
+b2XjWVD3UF/Ku7i/yFNLMzQ5NlBDSkJGRUfr1Lu1uLy91sra38Bt49trRDgtPELvuMdfSTlB
+OVtJU19Fd0JGPz5Kvrqusq+ytru9r8G63TkzJSYiKiw4S0vZybuzq6ukp7CxyW9QQUhJ005A
+ODs/Ql/Kt7CvvN3Ma1U5RsO+a0UtLC41V+vVREQxP0lTR9HNwrq+u83Hu7GqoaSpqa+9y/9L
+OykjIR8fHyAiMTxU08m7wrOusauuusnKY1PfTV3vY/9tZ9PJy7+2sbzL6VVJSjtFSThIQko5
+NzIzQOHLY1tAPkjOvLOtrbm4s6+pqaWqrLe72Vk/NDEuMCgoHx8gICcrNFFQzb+zrKekoZ+i
+qq66ytlr+0tTPzIuMjE3RG3NztDR2/vnztXNxl/VQT85OTU9Rk0+Ozo4SExvyry5ubq3vre5
+t7GyucPT22tlV0E+NC8rKywpLi42PEdHV9fOvL23sq+ytbu2srS0sri8zOf3b2PcX2f7UUJH
+QD5JOT4/Pjw1OzlNTO/ey83LxMG8v7vAuMXdZ0pIS0tLWm9jV1lUX1dvZ97e/1VnWV/Ta8XJ
+61lMS01YTE9fd29349/Z3eXb1NNdae/ZzdPHxr3Jz9Hn72NUSFhPRENHSFtZTuvl/2tlY2Nf
+S05ZWVhMWenb0dXMytvf+9Xj6effy8fV18/Kz8/XzMzfe/fn49Xz5dHdV1lcUVJISU1vTklO
+T2FnWl/33G9hY19eW01Za/dfZ+n382Xd0t3rXf//929z38/V4/vtb1hPSFl3Tk1MT09JR2Pz
+3f/3d/NpTWndys3RzcjT1OfVzcrd891391tW5+HtZ+Vv61RMXWNjWFtYb29WY9/b2+Pf2WtL
+SVB36eXX3ON3d2vSzMzP2dvnX1Jd29vn9/dnXldf4dvc6d/l329v68/V43tdWUhGS1rr42tr
+a1dIR1Fp6+//42NJR01319TX0s7be13t0tXb6+vzX1Ra2c7K09LX7V9VXefZe2dVUUk/QlP/
+9+93b2dPTlnTxr/Cv8HV71nt3dPa3/d7WVhd59DT3+HvWk1JTv/7b2ttXV1SV+nZz93/ZVRB
+P0lV/+vf2d1zb+vMwMLGzNfnXVRj2dHj81lOSURLY29tb1lXWU5Z79PN1etVS0NBPdtzR7Zd
+SsxUxd9KubHv91gw105H1ufQxs9lb+P/z8TU19tXSEDj5dG8d72+XMPrTP9hW8/fO1ZCPthL
+X9VNXGdOSv/3c8a/Z7rAz7nAvthV50lIPUI5P1FG1mdVPUpP77vR3co741Jdurm9wrHnM08q
+MMI3vLs7tzAu60tPOq6/tLAzRD02z3exoq28OiwlK0A0v7dLtt5EvVPEws9pZ9c6zEQ1yELj
+t9+wVFa4SLq4TrnN/y7KtLqsVy0sJTytpqCgyTsqIjXF47i57bS/QkIvNNm1rLjVMSw5Pr6z
+ycvzV0lWV2+9s7KzUylbt7mvSywlHylZr6mpvywqHy1D17yqqrOtPDEvJ1e6sq3OPVM8XcZU
+uuFzx2lNQm9fsK+9Wiv/ubW0MykiJtyto6jDMigiOXu9rdXM3ry93s4+RcjDzcTtPNJBSesy
+YUhnuLPUzr93qqtJPr3Jq8YuKh8o0a6kqksrJyBE48S061NPxMzfZzNPxsK9VDdBPUp3S09K
+R1XazsfFuLy6wkKuvrWuQTwjJtOspqtfJSQjO7+/t1w1QNPKyvcuREpzwONfY07pyczlVVtM
+48rBvbqs0zi5wqmv3dcpO821oK/TMicvP8u7bTw+aePNazpEPmPB381ZQlNAT1djy9dOSjbW
+uLZQu620p287KSrNq6OmzjMkI0f3vb9K6b3EvDwqKDjKsb5OLSg1T7e0xt9IO0RpzrFz/7HC
+qPc1SyzKtq6fuO8yKDZfu7rDS1NnzL3L30dHVU9HOC8yP9vCyrxIODgv07q+w6mzpbw0Lizr
+rKWfs0IoIytItKqxtldN5z/XzrSrtXcvJh8qNme6wLlMMyooUU7RrKSoskMpLT20rKu0NzUt
+LFrItL3NOV5N78Jrv77JyE00MC0wU0/CtPdWKyg+NkPbv6+ouzwmKE6tn5+r0zEsL1S3q6jT
+zUtLyWe2t6620Fc1Mi85RHe9vsE8KjEtPF/Gy6nJv0ErQzDCp62q3UEuK0S+sry467uxwLpv
+z8PNSz8uKi4rP2POustMOy8yOT/Hta7KPCgqLNy4prK50TNbWtumta2247C/v7rJzNlKRjor
+LykpOkHfykVILyxfPEzTOLew37RPO1g+r62ssU1NP++tv7y/17a/wsfJuMfZXT03MC4pNDJF
+3mfeRzI2NGPI2bzTvbS0uFNMQE24sauyzG85b8K4ucxeVcK2r8xIOTNBOzM7LTEvN+Pd91M0
+ODk6RUzr1+PIzq2qrb89PzjBqqqor8jlZ0lJQzrMz8HDZVFAPEpLQWc0R89IzGsvM2s381Y7
+777VzK5luLDTqefHs8nHu7G1uMdGXHtX3chDOF00azo090FQa026O09MYc08TznERUfLSLJT
+sLZetEnzssSrZ6++vbZVuGO8VN86OdssSTYwQDIrSzlVPm0v4Uk6zDjaaz/Qv7G+vtq2Q7pG
+3dDK4fu2V7J7xFW9QL9nZ1Q9wD3GNOc1a0JrRFVfQ8VX0EB720xvvtDHvM7O7enr0mfK07/I
+a8Xe48bV1XNtPd84zj/ZRVb3T2tvS+dPXE3pPctHX0xB/+PV40q3SbZKzEPIyT/LN8tU92Nd
+yG1La+FPwVW23Ne4XcNZwTrHP9BdRlRETUPvWm0740e/b0HjXMxbzMBKuN/FSMJvxMZKy/PV
+UsFDe9HtOXfZa99KsljEzO2/493r0NnATbdD0jbzPt3jPuVEtlq6P70/uTxT60zaQL1PxErn
+OOXlMtM0zlB3R0pZPnc32kvlTNFFxvdVxVddV10z3k3v69NjWsJP88lQxDjBO81X6/vnv1LR
+5c02xDPPXnfJ77blvj67Qs89yfv3Wul3Yds6z+vYe+NXVdNYytzMP89MwMtBvsvKxMg3xzVb
+TXdLXVxpY0nEMME/xsZV6dm8T7dIv03AXUjRP85CwVK8y1LXVGn/XMftS8s9zd86Ous8vknb
+Ot4z7XM6wzPBTNtOO7pGs07SyEvOX+FOve/vSk/ZQWdjYbZvwkfnzVzMXc7Mukm+U0jbbT7r
+1N3lQbVNvNPFb7mwQLvIWLlE0bs4yjRt3DbJO0fANs3LL9dGPb9TXE7jc3e8RrrOQq86ybU6
+s01OtTy6SjxNX0tvbW/P20tLvzO+b0W/Qla+L0q/S17ASl3FVP+63cK9Sc7zOdQzPN49c2NF
+RldCXMdRWu9BY8ZFzVpZwdbVx03n2sXVvm+/xGvUSUvlzd3YysjfxUNC7We+usbjyj1XSEpS
+Rk9F607/0eu21b/Azc7rS+NlZdlFT0VMyHfF1eG728tMTUBXZ+v3QUU4Pz9TR19Nzr7Lv0M8
+OmHEsa+94TQ2MkVOT1VFSOv7w7+5v77T1PNKPjU+Tnfze2dv7829sK+urrrIa0pvQ0VOQUM9
+OD42VWtdys/N0ddXzsC/uL7L31NLb0rMa1/PU+NcTlVV23PRXktRQUZdzrvDwO9QSU3lwb29
+xVM9MTdAU9fZzslcb1VZx8zH09XvTE9ITFrf385rX0E/P0nn1c/OX0tPPlpp3dzO1VdJRElL
+42/f/1tJVUta1dvPxc/KzWfrd9O8v7+/3ttMTv/c2ettSFVDSu3nu72zvb7JY9N31evcW0RC
+Mz88a2H/505GPD40Tl931tnv70v737y4trbAxu1XXF3nzOfEZU9BP05h1VvRTV1HPz9AZ1zP
+a/NEPj4/U9nMzcne72/71b7DwMHZ/0pFQVnv88x371NFV0fp08rL2VpRQU//z8rIzuNcTk9Y
+3czL3edbRkBLV+vGyMjR72dbb83Hxb3L2W9NR2ff38/vV0hBPkr/zsLGw9PfWVXj1cvO03tN
+Qz1FV//K3+9RRT47Q2fv39NZa0nfd8i/w8DnaUJERFPj18/e911KSVxzwcHJd0pHUvPT0V9L
+TEn/y7/Fzv9YS05dU1dN2cS3sri/50w+RFXl6+dJOz4+RlbMx8TGyNLP7e/fy8HIzfdpRUVH
+T3dv61BMQz86SG/ZzMrM3M9z59XHz97Wa1pRTV3PzcrVTUk8OVfjw7u/1+9vT1ffxcDD305D
+Qj9AU9labT85OTs7W8i/vc9jRUI/Ud++sbW4wM9aT09czNX/d1xET1ddxLy8zsnOT11OTM7P
+Y18/OTg/RtjHzNDj2tVj08S/urTMz9FrTc7F68BLM089MUJH4bvKT2dXSkhtU9fR21/HVVVn
+5dfOVkc+ST9S6+PK00hV3Ur/1e3DytpO+2k/U0xjwMvpytte71nhu8C93VxDQU09aczj3mdX
+VW9N29PS11RKT0c+Rk7vbef//1vP7XfAxsbKTc7tUdH/zL7Oz9Fve2NTb8rz81tHQTxKU8m9
+7+N3RU5BWdXNwcnJyu/jTE7NycjOWXdAO0dT18FvVFVDRjxSWO/VTUfzPHdbP7zP0MJFXc9E
+3vff2j9VTl9v69u86+ndREJNQ/fAxczvRl9RW3fz389n3/vRVctjxbjAwMZRVUxO2MnBv+dh
+Y0lrTvNd7/NVZ01fTEpS51tr7+PczOPL29DOTkM+REhIbd3M2FdpUFfX98fAxMnj729T32vJ
+2uPZVltWU+fRv87ca1JTVV3V2d9RPj89Q0JPTsvz5V9zQmdMY97Vy+9RWU7f3ci5zs1YVETZ
+///A1+XdPVhARdFvv8/dXEBDRUfr3rzO2fdYTlRfVcfJysv/ymtvzcS6wdHvS05Qd93RzGtM
+TEhIS1dnY1dJT1dHV0hn5+Nf2W1n2fvZw9TOSk5dS1tr0s3Fd1PjTOlpRtJGY15D1f9rym/N
+2FNX62fY3WnOVeFVR+NPb2Vf729JR0tS62/nxefH0+nOb1nnSN3vVtvr2tFPb2vR29v3zFVN
+VT7Rb1DP98nNXl1d///nZ8Npd+dNzNPNy83B3dRK2mNP3D3a52/PX7/KxdHYyEdVSU7LXNnn
+R2s8O11N81lMSkI/Q0hd0+XN52/GXNe907vve/9HUURzysTH52tDY0JMW2/K9//dRVlRb8rK
+ydx7b1xbb+XT0M5n0/ff3t3Ra8xpT3dMRVd7xtfJ69ldYXdK0NPpXndl2f/zzNfeUUNHd1fv
+d3fXUltRSGtTe8DOudnzd2HjU8vjxtlrW1tr8+PYz29UP0lPd+vPzs/KSttLV/890mf31UNj
+Xz9HREvcWdhZQWU8aV1j++v/c2d7bdHCvL7M9+ll/13329vJWFFvS2dX0+vS6Wd7UWtU5+PM
+ztDbydPr0NXR2tnU3Hfbb//a995lX0g9TUbP1tTXX1dMTEj/3e/PZ9lNZ0VjzdHMzuFOS0Nb
+c83O51NEPz1MVdDHx8rd72lPUv/rz8/O1llMRExf43v/b0tPUVvHv8fG+2djXWfZ18Hn71lN
+W1NVY9Tj2GNIQkJCVtfpzmNeTWP/58u+xr7b3/9LZ03Tyr6//19HR1d33czM129ITFZNz9Zv
+a2tX5U370N/B0f/TW1djd8jF299TWlNJQT9P89vf+0xJP1jhz2dPQlPd19/f2czJymNVSU9c
+/+NfW1VZSkxFSczMw83nZUZdd3fGy+POT1tZRmnv2dbO5fdHW2frwrzFwsvVTlNX7+Xpymtn
+Q0tP3MXf2Fd7XklHPUZv2e97UUlGS9nLw73O2l1OTmft0czN0Vv3TGdj38nO1ftfUE5TXsrP
+119BQE5OWd/SxcXPUUk/XdDHusfXXU8+Pk1SxsrLSkVAO2X3a1/Vd/dTPkRU4cPN33da/15c
+/8/M22VTW09ATP/Evcbba2NbW/vQ0cvNZ2NGVG3rwtXV011RX1fby7y+0dhz90pTY9z30d/3
+Szw+RtnPzednW0tHRe3Zy97XT0dDRm/Rv8bPe91LQU5P19vV309KQUNA38nIwtP3UUdO283Q
+b+ffV05HRHu/wr/Za1tDY+PAucLJWVtOTk1e59PH5d9IP0Bd2MZp329XTDo8X83Hy+ddY1xT
+1MnAw9vZY09DTnfBvcbbb1BFSErXy77Gc0I7SkxYY9XG0dtYU1l717y7vM1rVlpXa/fRy19v
+QklCSPvWw9xDQ0lEXWVj3P/ZTEg/Qk3pzdXIxuNVSz9N58zBe0tPPlFPb9HFyMTrVVtr3b/E
+w99VWU9KQ2/Xw8jO41db7+fUyL3D209GP0XfztPZ5WtbVkZP48nP505NSD5Md9nZ62VfXVlM
+Zcy8vdVeTElh49fBw8bZVEpJS1tvXl/37WtRQUNLY9Tj71lNTV//zMfMvb+/20ld7ce+d09P
+V1lFRFTXx9ZVQ0dPUVtt/+f/41tGT2nXw8Pb7+1OTkNEW8/L2HdTSkhf987Mxcn3X1/fycDA
+zNvL129ORV/YwsPaZ1FPVd7b5+Xr0NdRPT5N99nW2d3lT0xW/83F1c9fSUBCXHfb1uP380tJ
+XVHW19zaW09FT3PZzMzR7+NaTk5DT9vnZ1JMRUNOX9fGzNX/911j/8q8ubrQ2efv7//rzthn
+SUZITlFNY9t7XVNJSUVQ39XO71Xv62ldd9zBx93/UldfaW//5+Xt/2FfX2nZx8v/+1FdaV/7
+79XI/09PRuXP0c7P69pvVVFKW2fbzNtbWUth/3Pr69n3a2ddd+9v2vdTUU9PVVdt69/la1tb
+UWdjb/Pje2P/Z2/3593S2ePl73tz/2/n53tbUk9TX+ff3uvv6+Pv0dnPysXL13Nne+3n6dvf
+7XdpXF1dZ2tvWVlYV2FXY2n/6ef/d2/v39/d3eP3X2NaW2l3/+//b+9v9+dvZ+/p5ettb//n
+/3Nrb3d3729nbf/Zzt3r+93naVtYd+ttW11dZ2tv8+/j82drYV9jZ//j/29nd3N37/Pb3/93
+c3NrY2d77+dz93d7///73+H393trc/f/+/f/7f9tY2N77+/j6/fz9/v3997Z3e///29hZWv7
+9/f7d/97d3d3/3d3a2tjX11n++/t9/9zb3dvd/Pv93tvb21v/+/r93f/////d3f37/f3d3t3
+b3t3+/f/9/f3///38/f7/+/z/3t3//v/e3Nvd3d3d/////f7//97d///9///////////+/f3
+/////////////////y8=
+
+--Where_No_One_Has_Gone_Before--
+--Outermost_Trek
+Content-type: MULTIPART/MIXED;boundary=Where_No_Man_Has_Gone_Before
+
+--Where_No_Man_Has_Gone_Before
+Content-type: image/gif
+Content-transfer-encoding: base64
+Content-description: Kirk/Spock/McCoy
+
+R0lGODdhQAHIAKMAAAAAAP+2bQAAACQAAAAASEgAAAAkSEgkJG0kJJEkAABIkZFIJCRttrZt
+SNptSP+RbSwAAAAAQAHIAAAE/hCISecQt+qtJf9gKA4EpmVXZqasZREHp1ozKlJsig38yv9A
+0uAAGx54hUHSSBzCns3jj3hYKBQHae/n2+m4wVx4PO6Syd5zazuTVdq4D/xGl/uEBOEvT6IS
+YDt8ZzhbaTyAYF0rhD00YkCAMTxZBlkHCgsGlZaWlZt+WQQFfqMwDAEMDpWCamyuhq1BWrFT
+sLS0bjV1OCUhbSq6KF8mK39UnkRFlFTJMZKHMU+FLjoYodRzJ8JAXoFESzDLC+MMDQsN5gwL
+6uuoqJkIluHzkwfu6t93Yc9Btq1efKQU8IcEyKwxMQbegvMrzq4Rb0A03EENAwxNoDhp3MiM
+06o+/mmItXC0rdvIRBdYTVLWaZ25KuNinkM3Dp0DdOUwfdqU5wilBuUM6FtjRMKwMEMRCkgS
+g+CPAgCmRFXjQyHVN9lEVsw2MeIGG4RIIjqwKgsTshyzLFCrdu04tvKOOCRaQ1cuNWiRtZU5
+DsECv35jBjZXs8HNcpnWoj2SR0Anqi2M6uPjKCkXipwcD5G1g1/Cpf4OTB0Ci9hX0w8hWmzY
+lcbinhgrOSY7IcvstGzdGlBchRMiioVsNKq29egeSgYaKGjQ+23veAigR58e+C/fmUCzqytL
+GqPjkCiBi1RiqNuiIQCcbULmyUh5bl8QGkU/iE7W0/edYI3YqLPjEpo4/vaJM6K5V5RrZ+G2
+FyXcjEeDVnaYQRpy5VSoWAJZxJPFKAdAVwB0HU63ll8w1TSThbYRmJ4EMRiA0jBmuCDhF6Lp
+RZaNnZi1mSgF9bgPeGd4cAKEqUUo5JAOGSOAUOyhtaQFBD44HxvqKWiJc0gJh5oLQjaoQ14G
+pIJYhxoVYOaZSZx5wCgcZtihdXC5dU5QZXnnomsPAoNaMAQt5kklFWrnVhW7fXKGT87cwU8Z
+Rfri1Vd2zSDadzWSpUCT82khgGRZImHNZohactsBOJn1BxhsVJRLGjcqwMAp5WxkphK0ApGm
+Emta0qYlJDbXGzo6eQTqd3meZ1KeU3i0jCcu/lV4EzrQQvtsAwOaZa1j6QmQh0IFujLJpjfc
+R2SjGojWS22aZNIJBYmWJ0M1pI2i2a6c2EStJfAZK+O+smjyqgOw8ubTU7XOeiutabK55iga
+cqLYw+ouJtqkFtyJ6pbw1qMJFhznpF120JYTbQANkHwYwOMU6ueEmhFrSLcSHBmuRKe5wa5Q
+k9K25BB6/cninl2V9INRcQ2hoTWkorLTQf3tGweVYcIasGcYLGHVIwlvdmvCGzqs0XZNyeZi
+u3qmGlY9rn6cinYAowLw24bZRPLcJU87pzmbMCEgerZ4cJnMNG/pBuBvGAAAkwJmG8ORlRx+
+waTiKiXVJFFtJHAW/yW/hIkflMGI1SE8u4qKyJuk51PVnqZ+8BIIK4HmwgubmavXOVIas3hE
+rpHFcrCaHIAD7gQg/O/AOzAtOiQbBny054yznBVvKabXDqwTRcGRDJE7AhUsGh4VRpVD6Ti2
+/DLyLa2SnC4JDr1dmQWLOBFGVsO4Q2rRjWKmslbpTUmi0NXmsVVBljAwhKkJTQlaxoq2go1F
+dIZUpxie8fIXso9JK27Hi5YGm1cYaKVsf99aH2mGQziHaEtwKExhCry3ovQgYyq1AcCU/Ca+
+8zRlM8PyySa80Jb2OWMANQEYTNbTGBmVbQC7edU97mUbHgLnarTYGutGUbCD4XBCy0CWqv7m
+sru6AcxCGhRZyda2G+xo8IxoROOYPmGCZ0xJe49ymggKJCD27FAS82ljVLhEwy1Ya2Dpy2Ky
+LjcqmlwpYuZ5Wh+UKDXeOG6EtmIEQapnq9UVrHVMcU8WE0UcRaKFd6/ioEzEmIrgIWY3pGIe
+YfiSRuXFbXMpIp8spnKkEtbBLuA6wYry0iK0LNAaLIpKCXOAqNmEg2W16VZtzlEJR7YFlZlQ
+WRGJMwUF3COU4HuGFi/WnzBYzUdbI5g3cZgjimmJGmRp1k2ix8p2CC+CiFnOJUokkwSMIwGA
+AUxhjicTZVmjM5KxZYTIFzgZ6Kw2BPAEBcaGAQ/IcAQp+KFGWP4WKlbARFmLSRlcQoEXf0mN
+Oe1yyjQs0wpKgtN15OnRqGSTCEUdIFCJ8WFMqoCT4Z0CC+7bi2CoQ50OPoudAsMZ0VxIDId+
+QDIw6xKSZACgFMlmYooTKAcINDHNcOSfZ/knRiUWMUpEUz294NmNgIKK5XinW6gCS9nGsxAr
+dqpWBskLtkaKLgiaQ54PO6RyPFiqprDIYdaJCT7ZFJ0OvkQjGhJKDgRU1AnIbA4lnFSA+EXM
+Fh11mBKJaG2ueBYeEet9DWXLgKqFG6Hw0IUvzQkl5vqg83ClWOMcQ/VM2o995AhnsrRGJUAW
+z0H5kFR5kWETa8GJ6tjzLwgwU3IRgP9P3sxLdviahSFuZx9s+dJFD0UhWm/kjD3WLJeO0qo8
+crimHkBlQgEx3XokRqjRcjdRE4sasLJYmpbWjJpRFOBCGnQbhT7IT17NBHME1l9LmM58wMlV
+AeyZzw+haVeoe528SKOIDoAXcDfSxGStSzjhulChQpVjHId0GUqNkIezpE/OeCkqQmmkWtn0
+F8CUo0AKm/NprdXTjPbLYxZUiQEMIlBMeUNaOnK4S0cI59WUGx184jO5BtvQJOA6K6zSZwfU
+rQOggKwzGT5Uxzn7ky9tWct3xRA96fvOCNNcG5dtcmzu3ciW1RWmMSrnhabJrr50PC66nOGb
+uDiKgOYp5rT+gY2lkLpAl2ohwG8mDDAOfvCZqpYGqxFwXBUo8wbQQichUawCn+buemLIAakS
+dACPLdD6bqhZipAvRaVNC1AYwLFZX2FdrnZg9iDyiEH8g1G/+CsWmMU7GhuYNVoZ1lOk2Gg2
+PRg+UBTnmiPyUCEBTgXd+xO29Iwt0773TyHegMwei2ojrkDVP2tzdyn16omxN9a7e2f88HHd
+1vBnoPAJzmW81Q+2Nk0bn4QeTDcXoE/zp9dwRWnrUippYOcbCZ1AtIWvJ+7rNc6h38NBMDNs
+OMNp3NQF1aNjG/rqVyNVmN8pspzfvY6PcnnYZAFLeCUJjIyJRw2AnkYnZSBcTIj/DmzL8Osc
+MibtM6GUtv7mL7+hawWDgwvkoDac01VAFgnE5n3iKrXN9kU0Ne+rKf98Kmmb6avLmY4ss0YF
+tZpOiXRPhJquvYP5OjX3fOmrA19KDyPZoRHJvEu2y164o8vw1pQSjKZ3Ro1RRy5QGXZcqdc7
+nKWGnd2sjzy8SESznuHLbp1JVqxem2lQLUfWzeE0NpdPTb74DVeS5MCkrobR0MtdBVgxZz0e
+RlJwTKJkHyX8FrMKgplOxOWKb2rxMctypj8ApuQMW8TkyoA5jwRVismVUjgCLDpK1Js7siUn
+XfUEt0Ne88/F59eAf9G/77ukWTuyhZzyFpBiO0X91vYp/muCshQ5tMaPUxfq4LUBGiZ5BnAF
+HjdQyydV5UYfj+MBjcN5YnVHo9JPIONBS1MvmAA97GE7OpYf4GEI4VRSbkV4DqQN31JnlAdV
+uYVJwEEGJlV/9ddo04FPsQNXaiFmmlZtjBeAqUdqO4NbgNI4pgVHhLBqwSR9nbdSESgxzLFX
+I7NB+wNrzQF0wcWDEGVCQ+EFAxGC4pQEIrV+CAJ2WRAr7+NlkJM6BrRwRSd4loQEaJIA9nRc
+CRBpBrMEo2JtjgU4i7eDHeABlUKAw6ZYROgCTIIeefAz3VUlLEZazoMdIkM3wgMs0pNTVgAU
+X1Vw2LML2fN6wXd4mGQmlYYG/51UDx6xJNHkPZGXJ1kjaQbjhlT2iq7zhqwUhwxmdG44G4h2
+O172dLn0fyHAcbrlS0TYgFUFdp/iboOWfYyICWcEPMMjPMUTAGxnOUlDa18FLkYGffbxCICU
+SbOjMC74eycxCnfDAOulGAu0Dd50JvoXiwTTia74YH7hSuiQAA1gj/UYHVaENI2zfMbXi7zI
+ARrmJKL2UE4HaploZMqXZmQRDjD2Yu4DPeZQgcTzOxb5AA/wO6vEEWS1HeI3KQD4XeZTQOjj
+NQ3DNLTVb7jSDq+iDmcndZWnhQakXHM4WHPYcAkjj002jxlpPM+SQfcEj0jwYaKxi7qoi5fn
+iwi5JP8BEm7bJmKc8pT+USWfVSflRI1DNJEQZDyZ44wY+YzQmB3RRSj3AG4++I8PIQYDYTnR
+UyawMILepBY/RxuLIUP0F4sLVpMLpo8YworKZVg34QAZKUGBKS3PUotCOSFehpTZhYcBCDge
+5yQ8Mza7OHRxUJR5d2ZGgAzhcJWfAFSpJEZyE40YOZhgmTw4VS8LMDq+1APjN4gsQEBM0Ja/
+xQ8heDCzwBTqxQmmky1VsRQzCYd76WRwuABOxlzMZU+CCTxfeZqRSDyF+ZP3OFis40SM15j/
+l2XKJyQ+QxuU6XeZRXKY8VeKYyWA1ZYDZiJnNDfM2ZzO+Q5BhjnWxGU4I4j/SwVHfrQUtONb
++AKDPZImC2Ma7iZ+HGabSyGPcMhcxnlG9rigyiOYpvmew/OgPmkTcDidyeVNMWSUx0dxSllC
+2uZtfoVpczEFlJEinakgQCUTNNFKyFM3F+mezrk5o1J7ZjmENzaIW8EU+4kza/GOCASgojJV
+LUQg/YhDsvOGxTkTgemTTuqTpZmREfqeXsSVN4GP9iicdugDybeYXXqdAamDFTOQFzeEPsga
+VeUaOJOicfIra7FKzdOiaEQ3pBkApWmnEuoAiKQRVvCAe1SUW0dxmvifCUQ7DIcr0MWjutZQ
+ZlikpgNxsUOTC3aP05kADmCpT2o8USqlEgqNJPMA/8sjLXB4qQm6pQVRlIyJcXiYfMy3G5fi
+WCWGaoTzOJvSC1RVqCvKoobVSsnjOzEapZ36TrHkER75JID6XWAYLluwhWlxdqyGSYiye2WT
+M86aHlrzhnmZpZZqqYb5pJuKpxIKqtEYN9FyqYahpWklFEa5rl+6i0kJLhpmgDckqPsBFgyJ
+RW16HerZjMrDnr7jjMZjp5t6pxJKhbHRck7SoWk5M8LnRIk4G2o2QMBJK5Smc1tglzH0gI7h
+bDfZZHC4rfd4qZnqpN/6lSZLsBV6PFlKqvdoqlQEqBzKqozJi4yTWgfFQKbhY9oyIQnSK/o6
+kUBbrlDqpBZZtF45sMAqof/38lfNFBSfl0KLKjjAV14hpBmgQk73Jwa440fuJjFn0rHDWY/c
+OrIkC6GcKjxSeqdD66TlaqE3KQyJ46XHF7M0q3yTSWsHGTiGKAWI0pkQUyJplKmgiqdTCpZI
+O7DBilNM6T4D+Vj12kD1FYriOAuqNhv1AJzR9mdSwCUSYw2s2GTGubIUSrJIS7hIu5ziCp1p
+9LFemAIFoAlyK6ZeinyX1yrb8ZpcwYBa1ZnRQU8xMZHY4a2HOzymmbaEK7AYabadCmQlgCjh
+Z5fCAS9yFL20IGVTlj6gMlHCd6pTMATOcW67qStmcpM1+bEfe6UjW7pnu6nLCbDE04x6Op12
+OAX/6rqYc7uudQteSOQv63CANCQHNSebxVV2HRS8mlqynTqYJ4u8pSuhlfA47gNz+LEGtiC5
+gwdI0dW92Ys0Y4koPHolyaMO3bBVn+uxozqqgluayqvAJ6vC0Cmd0NKg5vq25lWGs9uh1ZbD
+WaZhBXiKCytR4nVIviunFAqqiBusnMrCAwuhnYoFawYXh5MnlPspqPJHoXI55sm32Ys5liA/
+ZzQTp7kAWoiLnMCOSmq+aEy6DXy8pyuuFapB08K658FCs9ulR6mwAGCrhXJx79qDwNEYY3EW
+1cFKNnHA3xqs76nADHzIzukA6tMJMIc912tVj9wcELMAAQuF/MmnEZk0/yWDyO/JZVssK0qq
+j9SBxvYIpYccoW38pORKqfiIycI5HKNQxzp8w41prBsDu2QWgJwhwEIsJ7s6tM1ZuKD8jHeq
+woz8jE81aB7hXd27EZQCuIQBvNJimtHIlYaRPNs8Ms54zFRqG6NcxgvGjsl1kw+Wxqoso8QL
+rMlrxM7IVxpUi2Zjy/eLw/jbbXqxQDL7rgZBXAkizL+7T5rKxCYLzqdpvOxLsGDJHNKXF32q
+gtN8Ysmifbv6oMaMyAH7zQhtU+pCikHXIWgCZej8uedLssTbzi38ztpMGA1qRuhKDbbMrvbr
+rpsSG7zsrka1h+ejPjUaDwJtRsR8vB2d0KZ7uP8yei/fIWy8vCkRlSDDwi65EadCi7gsnNDt
+WdSniQWVs1Ubon/lPJx9gaAofMBEjcDFYzcHEIf3tErRMRyMOtP3nMM4HKIZILv5DAac90ck
+ElgDXchmm8xajcxpu9BGPKV1AkoR4zdYhLWL0rUwMY+AbdYqjbZXPdhgqQ5YYF0tQc5kTaH3
+CNbJiamoa9jwzM2HlSGj3aD2FHx8NNN1PNdLkmFOt4cKiyDkEQ2holN/XcQBm9FFrciFrcxT
+yhvXdCMelixQPWV8gwI+8buAfcTGq9WfPDd000+bpYgbYnTjm8oHXKHGSdIfi9GH/d2qaw7P
+dsYfiwAwAtsxG9tWx8P/Q3iUXzqSEsWWAh20T2rZmG24SE3cBHsvexfRRWpVRpAgWKXFu03Q
+h73Awc1PqKQXONHMUrjdOamchgyhXBneJuzdqFvQ4K2XwSdFo70A8xt57p3i6JKMq/qui9a6
+JMmnutq2XimYAtvfhL3I7yyu7nkO76RatLF5E0VVV2UQtxFEgS3cx6zCNIHFbfHJ5bBL8CW+
+Z7y24vqgobvaHu7GzNOXOdk+OTnaoBgHKe7euSR1Awim+GyCLXYW3pvfVD2yN47jDHzUKlzQ
+pglKTiuM1sWzWlAvHsQJ03YlDwquho7IoEoYvWtg7oY87xQAjTPOkfq1zDW6TX5cpPpk6iyY
+/y1qnJLWXB6kj187WP9W5mb+PVVXcfTNi8z9OLJwnvsqtOaN4+58uB9u0OQQhbg3coL+5128
+rzPVXfhqE+yM6Ff6FxvC62rh6M/I1YBkBE726eMtP5F6oaR6ziftk/dU0h/iZB607eObnGPu
+N6Zuzw6oYU9C37lMXT1gLVB9JYM8E0Jd0HWu1cP93yn8AIPygAOoWQRy4IdE1aIkzhHVxcRe
+2IfeznpKg5OmU8wOlpvNtxqhlzXZ4ei8rOHOuh47tBjqYBRPg5bwsX+RreydeuUe2wsVc6ru
+oTq4WDsSVwQA1CzKV+fQpMuZ8Ahd60jdvmY9iQoUEZnxPgE/0KJ0NP+UI1yIZ8jFLrCEwSEa
+Qsga9IzlYF1amAXrjcbL1Y5WYxaULt6kfZjcbnTYYPUJylxj7n8nX98JtfJ2XN/RPBttMpvC
+HLjKM7hL39E6f7qqLI2M249QokkpMptvck/hPYdY1wgbYZhLj5HmoOnX4RdOBhMPjwppagTI
+Xr5oTIfDpxhNjlx0ONoVKpxgrdds4PRxyN5tkPY33Fo72K6t73rPOs17Iadxnu/9rcR67618
+38VmtXHhi8GWjFzT0YmlIdWkss6Gq6fGufyCNem5At1jBGSOgDkjgsrrvZPYsexxIxijTtpi
+y+1QRpAJY74jn/qqz6EXUQI7rdMcOpLRXOH+19FKNs/GOJ/A/83Sgrv7Y5gTfi/7uz3AEHBk
+GdWOIqilZ8gkccbnCc7TaZakSRZ4QahMsG1PWhqeCQzBIVg4NAINBCK0dC2dDcfDOD1BHYFF
+oaBsjUYuF2sJ05UP2oMzUbnZAG94XC4XAOoCQx7ovtvj/Tabi4qcIIEPiYMYGJ5GRxUvr4Ar
+k4BKFMzMkk1OzqjIyJIFg8QGBgaFAwM7oUQJRESz10Haig2LxASEHZJMFZEXFhm0D4xD3LQd
+IwZSAMKWo4OkJcimEJ6WRuWjyaMsLS4SKRVsJscdmARF3UWKm4G6Ofn5NtXVPrs7/L/84w5D
+D4iClEHnyKAXKJP+riysVCLTQ06WOj0g8QmUKFcKAjAYxQqAhIGJiLhydShgrWK2UooM0QuF
+ih3BxMyQMeFVDkTKTh1wlmvKBCdfnjDJ1ogbjwVnClBDqEaNQaQrlEndcMjNPKx08Ny7+iae
+V6/6+ln9ECSWkFhJV6y1cpBHqIUSHVrSdGLi3YqgxCUtdeoeTxwkcwgGifOkIlKDA4o8s8tl
+CkgOXoRIknDUAgaJ+JY6kbmVgF1Ghqnx4vSLZCsjqgQAurRL6V/AromAyhZplkBfs9K7oUef
+bj/5wI4VxBOtELOKF02FkvrtRYYON8m9W50iRYt6pbjC/ANwSMLhB/JVMEoRxxWJSWr+QdPl
+0iQri3Ql3NiAIAP7Enhs/PExyIsrkNgiqNKugSKbSFbjAQQuWhLKhS8amQ0hR45AKqA2dqMn
+rDwUUGAPfuL5Src22Cjmpla4Y6QRhMhpEa67qLNuIouu0267RBRoQAGv9iBEMAQSSYI70TZK
+hRT+1NqMsQJgiOKSyFoQwckrUMCviBOm4mUjUkJaIMAZZiBwhAlvDDA/p14DJhIWnyPHKCNg
+msEOeIDT8A8bDFDgFAUG6IcOOEbsx8TPYClMkUXcBKW50yqyzi4TZizhSTPnWnLHPN45iztF
+FNkFhpewiOGAzk5QZIX1tpAGirngE0qoLy/xoUr8OqMvs1j/oDkizAYjgy01cSi60AnXHIRQ
+r0UpVMHCFRCo06o7scJhT7+62sePq367gKSBwEP0rRSwUxZYRyU1V9xQKKIrS2kkICVT5MIr
+41NtLMTEKGaNYHeCJCp7LLJ0yERtP/hM8QUF+5KDZgX2inXQIgS9mJScydKkTUq4TnOOTW1m
+MNFODUXM8xS/CAA5uEBzO0YxAlD8QIYVI1OITQe+BPa66aSTTiIZJ0ZXnEzyS2qBVHh6oyzB
+hk5UGzL0xfchLBicYakGrnsJlIclNghqWrtMIyH7PgDnCYmRzWsFmbLQhahjzZbsl8i0QTsD
+C04OuTdqOQIR22tDhAeDHxPZFFFl/iRRLVmzcdY5Up551jnx9zYaekdVANgjRU47tU2qFaGG
+OqkEwKEptUoaBeM1iT0PNRoQoKlZpC0aFMFtSimWaoliyaU9QgolDJ09NqKV1gCSmQECZRHl
++U0Dk2IZ6TjCFWXU9GrI7YSuSLPX2RK3Iz+BlKQya8Yqeb9tjnOjqlS9WWmUCBP17H2F+0uJ
+vcfkEvXAdoVXotJ90sY3SSkESwkH1nZHM98NkAZdEV5Y8pQ3VfABZYASzjFWJrjPqIg5bXrT
+I6Z3kemgYHFz+VkooHYl/XzoDcbBoJAm17vnXA9q9vmU+8T0Gulk7HTpUh0mPLOwzUyNgOYo
+WwklVA4C/haQBAfxX4vKQSwi1KCByjMAZnayN+UBYlCBi0UsvHSqzdVGGzV7hDiigL3qQCo7
+Jjxhji7HiuQIKUiFww4jimi/SRCOMrFTAunsUr/eza5VPcxPW+aojt85rG154dhbcKcLLmAD
+KdmAH7IO5BQ0SHGKfAtCHsSXqWwFSpR8Q0YLW3GiTy3HNgXRUlRYVD+7oNETetFdJfQlOVUY
+Jx8sBBIMEGAFYdHvf4SMwQv6FbvZ/QtoERJmRHroGQkVQQJCnAYR3fbBb0CymmmCUDKdmEAo
+zsluWeHDVjz0LsBIaw7HONFZvJgTwi1naJ2SZww4Jz8bzdKScauGKvNjOa7A/8MwckwlqyYV
+ETx6QxhLmEYSeKEaEQJtZiDsoZVY4IJf4gcGDZMdNbxJsy+IiY9js9hHeyeZNG3BHePcjQAI
+oAcPES0PH5ngiHJjC5eWjyTqWJJO45koXtwMhAhkSz3naQrztMIZQfgIkFL50EFW9AHpuChl
++qWE1GFCohOtX0JRkAoW7MIHASiPUkhqIIxND5g1a1hbSUrSV47LGmoAhyY3KSJVkOxD9iBn
+yix4IsUgzT8+9Wm/gPrBNUYoKr686pA0VwTzNINOA0EEOOhZuK3G8nNVbUJjfynI90xEIiBk
+nOo4koYD+MAHF2JPR83xQYfYhwZauEBbp/YCtvjqiP5pUoIWWHo3T5LMHkbTIrbGYgucDC4R
+4PEWYQmzi4LKT2Py9GwHklYSOmGOJDQJRgy9irDlMFSbWH2MCJ3JVcU9kzNj9cEEzpom7VTB
+Y7eY7RaUEQMn8s6RKd1CnTYJljqQ4hRG4is/AFwiXBSDCIHLIBCUCp6eSqCpy8WcPacSt9sw
+FkWLScS7NtMlHAx2u+0Lgc1Au67ISSEdcw3r+3AIEetQSrQn5IvB+IMqs67tKdI1ATbOgIGG
+hQ5AHcwXoxxR0qX4lkTRAsRWNuKXCB7PTtoiBEDaCZIQf4CmRNJSZJf7I/DQqy3MJIMrzkKA
+IKU5wkol7nrS7D5GJHNnzv4M5lzHoM1rlFeEk2jVQT+RMxpXUV9jxTF7ShpXYXFum7PZYIXE
+aGe6esxaBvBTA/NEPOEu11qcJEshBiOQlmVwy5pR2g50VGYsx8tLYm5LVIJ0AAJU2cpn0Ix9
+8qMKeGQ3zCrtFzWayThgfwEdJSZ2r/P8JKgddHF//rP9fmjj+uSHBu57SsRgqI0nxOTRjxZg
+N3UBPAbetZOpPQVf8oCWCf7JJCsDyAVXSFOBEiapSUGnejxCEnt68EIYdMbf/lacb7krB3YA
+wqZ45dCqdnXOdSypVSMZrGQPM37K7kYmUPgC/qz21mNzLT7FKIapGMiV5zBITGaDO9rm+r9i
+Af8oCtXTSf8O5x0mMQxlb4AWb4EnMYn5iHoA6x+vRQ9YvvwyOtGSYM6QxCOBe3PTqeFdvDQB
+pU4Qg47dE0JfSEJ7flYX1IqWlKDxBchDbBAYEGcQmfgOKjsoQm0EiNbfWWDlw2EFKfbaj0Lh
+g8qHOZHOEwHHLulBPbkcfEly2YqmkQspr47FGxwckkOE2gidQqdXEPHmaV6VbMHyRBPsvNBv
+PwzrQWMVsulSo7gwCxWuUK2FyszxJBTrm22SG3OkNIW1F9XkJ/+dBdM9xRsAVMIpMgS0wrJF
+wHlaIIfPKUgEyk6jdThzJBm8HWs3DM2whsKyQITT0PkbObbvza9V+MT+7uuUqi9axg+BiRVO
+X/G4LIQK+HEwqQiN47EncZunKQo5Won22Xk0Vqq6McANq/gtddIHwfAIQwiectKUDBgo7RK4
+euAlpUKnW5oneSmc0UKCUjCKntoBFNCPWzo8B5uwOWoX8Vs0qMOOMCA2SKoYans4SVCIt1gL
+M8Ke+IMoRwg69moEV9CCAfqdPOumt/E8bROG2xDAU0EHAiwxJKgbBFQevxoA4pGwpio+KRQZ
+TbkyxTC8D2gZNhiMncsJTLCwRSC1hKCUpuEGhBGNzlkQgsGCwvMKzZimFZyGhCub0uCcFotB
+HeMf2ugnRog2w+kGdYkLg/AMHOCPKrg1pQj+HSGCwvORDSesmCFyglcDo/uSJ6pLCjYIN+DD
+lgPQkRSBo6P7jgpaGWRQLsLYlDUzgFuCEyckg8mhD3L4lhQoGKaxp4eAxAhqLhDAQ2N7qEVJ
+BzU4pmTkqJagjaVhlk9AxIVAiHwpj6bqDtXCMfeKnSDrNktkgSRLufyrL1ewtQHEHSVIinCb
+whDJhypiRJoriwoYi5TZFg4jtTTklhRZERYhPSxgmk6BLIiKAvTZNqa5JRRIqoBkhf15taZz
+qGsQJmFDG2GIPSG8KtqaNqIIK/uSCjfphkMsOb5wPBsDQtgJx4tMuPSbmnFMpJS8ruUISCgc
+n99buZFJES9qwO//CD5BkMekQcNRKZ982483yZc0XBETkL8w2hi5QUgUKLwI0i6+6Jd22QXc
+kki0gUEZECI+kjQOGJAWmy9OdJNNCJBlOYfhe8cnu5DNcCtDa4l04MqLLLGLYoQuI4lVWgdE
+YYb/okLw07Tlm7kDDIsqExsN2hwNxDeh2w9He0QliR5K+ITNqZ5zKMSHUBF3CbNpSooh0bzb
+m6TTMabYm5rYqwVDUyn28AAiwL21mihe3DfCK4IfTBW3hMK41KZiUoOq88QQWMyAXATJMq6+
+BAsrNCV59LdQDIS/WTB+KSiRRDV62hxoJD10gCw6qhIiUx+l5EDPIYygyxzOfLOFup1P/zwm
+IZoAlZitlny11ayC6zycL2CWockMx0stW0G1cawvA0HGT2k4TbS6FWm7BdEBtZgpuhM3AGs8
+6KkKdgrFLVJNfOPH2qAnVWiaRluNFPDIDHuLSCHIjzwc7oEoz1mSMhu8FMTDzIPImgG9yTBP
+1EwJ9ayvLTDJXzLLuDLLowDIBbCcXDKF1UpMs5LRokBGJAMeGgAcIcwG5iA1aQrO4WyyO/g+
+WEjOdXsWTdHJD0A4D+qgC0GVyUQYBbmvtbgwfQIFyKhBkHwIqMylCVNBaRia8auYySBAz2JJ
+JI0it6pRqOi6MpqZtRuaVBA+gzGFAQ3C2hzSlHLJI9XP/azM3//8u+Fcpzj4AFLwwrrZFpUp
+JRfiR9VgzQBxQ1AF0zdcJQ39n4lxCRjBjorLBHW8w8OThansTMOqGCKFpAE5JjxtGKXQgaoB
+SbhoDsxSiG5YLMRoBgnIRlRZkob5ytDANrp8pK6MgW0i1SpKjKZxUgPry3owgAgUCGQwiclC
+sMW4vGa9wQT51HsRVYQ0SBbxs2nsqnLpuqARuDVVTE8hOvETAz/cyoaCLvZsn62MGXmtEp9p
+v4oYVkc1D5oCAIPRKKnAoPoCMpETBoo9om2zvRpTC814l0j1y6Op1MFoHgmINeSg0gb9yZhh
+lCPgwVB1SgVZ2SJrV1O9mnzyhFV9w+//DMgRI4hRmSOYCYqZMCyYadYXYaNV3UHFmj1HKIOe
++wgfZUudHbv0TMfbuSioUCzGfE5GwEtX6FhJ/ctTUjAMiDWfFIQuPJRpukpFGchfrCjnOB8X
+MaMzegnFoTjPaYB6fdV5IYg8LCa4UQEtRayGiEaG0Kpd5LOJxMUc5Rw2ZUgfqDFpMrMjHbs7
+Ezl2/dMxoNAlWQWvFc6UgRdDEZt/mMdMRToicM64EtY0dds4scEOSlX1MS8Zm5RfvALqk77w
+4MxFeMiqiwTrM5NznZlhdcMdtI2zXDsJ+4uPeDJUW5L0rC/XslwOogL5JEBZ6LC+etJ8WL5W
+GIlbQBqz7UnA/2pIC9OLw60oUIVPc20iPBIX7Bgk+XOFvOVbRPEsROqj52CB7oFPZBle3DsK
+Mjsf1hxQywmxY0UhM2jLQRDSssMtMKiN6l2oNFAHpetcPEEeZ0gMMGPOQniWOkFOCXwqm3mO
+1Y3dHuLfanCRfDJhrXpfZ5K/yEqMgQguoiHRN+VXOFVCL6AS7TCbXSSHRzxLDB0BfozZkQyx
+OrC/05IXIAMcW6gr9HstR9OGTqFgUzpQCw4RmNs+bxWsDzYRBkvRYloB82XhtiXeSWEiuY0q
+us2OmoUCL+PRxFAtBOaLnpVVGWgB+rmoAzJTAT6Txf3fghQj5GAqx/WB+T2JyV1P3f9Mm7Yb
+FVtELeBRuSz22JsbKOBJLn3wYJwyuEUgI2NMPTB1WZAsy9ojnYlRHTd2lC/AMjwwjjxoWM+w
+Y4Lw11mF4wvLLwTihk+lgsZkTIX6uFYNFIFaS5IQ3Sf+3toKMmnoTZ7S3EhWVroJJWlhR1EC
+DpMgBev6sbDFObIozA1zqFTSY5tJEKc5Y31JSmGLGc7TMzWCDnGxj4ITAqOjY6TSIE8B2Hud
+JOsjYwH2oEAW5JjFvaE8h/GZ5yAYMEbsXg5YYCfegLay4p2KTiBdKZ5sKQuuA0oNLMMoC7K4
+Up/kjhqSp+ANVc9hiHXGLEAS0WChHRcsDB7dK7XEj6/T2cb/Ck8LawlgRZsx3RoFAebLtY9G
+WwuuEISmykbzGAKUoAX6yk883dWJFgmcCr5KvpvDo5ubaCcw3pZWFOOCShTzZb8fPlOYyDd4
+RVXVAKCMEZZ3ObdyQ4wnu5LNgNOrIrHQUEJntFo9XsoBrpCArswNQoc9yA1yS4+wVYmlri2H
+XlZj2GYr9i1EWKoNsWaM3lZQnADK+rEBINtvLtl79FmjeiWH6NT0hT9Q8CVF0acOzQv3zQ5U
+yQNKJZnLyEbPSGBpUBWqtMpqQg05nY2ebkML+ZapMEogDGweKOpNtr8d4YtDUOZLvSnxpQUN
+eF4jxQXMKOS+qepsfdUjvYkF+2Iw//5CAoWuMQ4YiUxKw9XON9nhXJ5ZcXEVRrKkUaA0e+CP
+avVRsqLlhqxrdtBDXqjVNeHPTcTbCBPBXjzlJQzUTC3s8ugSukGJj74pCD9ATraBkTAFnKsp
+z9XW4uqNw2sn5mxQTD0OmgNYo2rvaBzWM3pfg0WI3zW9mekx3ZFvSvMTAXuymUaFy7hDziRG
+cVbR3pZTG5oawmq0qWjCghg+4vgAmi6JxPa3CFeZA1we5cwJ+kun5Eme4KDsr8U7n1NN62qZ
+6B4E8R7a/jyslVZVP4MP60s0Mr4IVwGbtaWl/CDZj7BnWVRoG1ZB3Ma8wI3LapLLBYo38eap
+TrRLMY0Bzv91IIaUKYU9iVqgcCuV8otusifGX/okjpQxLi7PVk5CJ7EdWXf44n84urNAOE+0
+SxNKSpulCKUhl4JQPBwk4hlnk/kmAFzHA/wujz0xkgbX2cyrSscyn5gYTbdi6lBEmrOopyUk
+HDYlLjzAgVg2Vn9gpwj/4Cmn9HWy8LlMLbzdyd8YpSyWoGy+6jv9mEtV9g2TVpihWPzqHhdW
+Ach0cV+SUE4lIyNrpFcjgAxomV6HMh/VmxBkjDRDLYcaUyFabNGBajjrRE5ZkXwr0IXtpFQY
+t3MzDmPI9n/Ddimvye3lOAeWrEDgJHrUXrHgyXes1If+5oYWQ1g7dXEOK6r6Eon/ZLZ4Z0t+
+dEIRrsUVxfcxK2epyANcz2ADiOvwqY8q0inM28yOqQogQ/WZR8aYcc9kXUy2s7VVMBo7qBZe
+MtZ+uwBKn3IrrSA8ydIGYYGFBPcMyW6Tr8Jc2okrs3au9oBQC8M3Je90lInZqXnWJkha1rAU
+hXppvWstqRkXRwJS4Gwmp2MYkO35jaIhUQd68SUZ1dKCXhoBtpdaPEhUCwtycwNYpqkx38Lw
+DftraW7osiqQ+/ajKX11k6C586vt1QMJSAUxrAHkrLLjKCXxHNo5pXlkWWF555SWCbVQu3t+
+FVq7NDLgTwpcjzWSLfq1/BCDKZrwgHwVDNxm4cpqijPD/geqWC8qP/ztMvP8nWBAXaITxCb9
+sBcOTnrin00GQ1dy9s+NjParm6N9z8DUpe5oF7oqCFhpobXabW657t0DYsdykCaapghrURYW
+x9eGIQeB54RgNAyQQfIBFwaVqZA8FG4TUiMaZRWq1srEJZW6uhhY1Bu2oAAHs0FxBrABArZA
+MJjP5fRBPK9369/5udVAQUKFhEQCiYVBm9xfXx7cW9skZSUjnBufwIHB0RknzkGjHV7BwMHp
+6eYmqkkLBQuWocuHR0jHQxRKSQlSqEnGlswFx9YGGcHOXI5CAINzg4IRQ4PnkSeTIFPSjdYY
+lVWTFWECYWwsU0vFFrFMw8H7/vviIgB9ZOYfHumdPr7f/z9A4sZhQWTwSZkzdfDoe2TpIcR7
+muKAYvNJlCA7pOSI6riJgBJXFdS96OIgyoYPIQKwbMmyQQB2vXZpQNlh2IwwNSgkG9CTwKkF
+QYqYiIYthTYlp5okCvYFgbkqCMJRrSKoChMx7nDCMGLm6xpGDTUxLHunD5+0cQCxKDfobbkS
+5Z70itSoIVpIEfdiwpSH0zU/nxbeAakq1KlWKM6NlFAoQQ0PGnA9cBnTsrAvkzVYvslFJwcL
+N5KhAnrKAMtnDHodkXatVcirTKR25QLrHNMkVk0puVHI6TBiwuuusccozkRH+xjm1SOwSVtx
+U8tB/yXUC5GJThgXjkKe6TtfvpDypDljx0QqOufptEqv+ECsF7+BS7EFwvLLmDD16xd22WUH
+LJ1EQzslALUDDwOYAYQzqxlABiiepGLKNlc1AQYX1s3lwgQTxKdVZjmFodkFJtQTliXOPcLc
+KHjlY6E4CQwyFzlQHXDQjYCpAYB6eOEFXngReccHJ9UYwFEPJpyn0VKbpIKeCoyV5E0wtVTm
+wH8twSRFf/WdhCV+LgHnwGg5xAGUJ6oV8eBRnDyZHoyDtBNGW/PVRt9nwmRQ4J511XPcQwB5
+54iLaz0XDlRTKVqFjOQg4kR2RijE4nh9BSnkJAAdwMCRdbTykT6lsJeYkv/vwUdSF6HZVIsH
+AYCZ35aXbWmMq/tl2ZJTFyBAmgAHDnDND6q9MxMoq6CyG29zwEflC+RgGKIxwpwU4nAjfOLG
+kX5QIomgzfVxR1WLzmgOBeWcgIhVgMHB3SPaSnIpppFsKkSnym7CIz9yFJDDm+1t49srs3j5
+ZU0fyBorwiNyydnC+OkZypk7AKYmEL6gAQjGgjw3nw3WcVhbfZsZM+20CnelCBvz6JXiPe1y
+S1acVIED36OQYjcVYPRst1almcIrnh9pCIHvYfoaegcqS0GJxDnNFthBMShNdpKs/C2cJ5at
+xgqgyT8haEDFPzjjDLEoHGmKxkln3ERcX4jm8bP+NhXsqqs3eWZyF+aZt8d37qbVN0B+Hc1b
+VVLdIBJ2KFDYSaaU/qPWz4GCR5GDR3ckCpMeJUYqDk+mg6oFkXFAMOn9Wb0f6lvUXXBNUWi5
+BXo9Cb3mAQ1S40uxqoBLh1QFMPsxfXaDECCWCJfstgsmFrfHu+9e0nzf+KhlFuFWbIO72itv
+5JDfkQsZtFH7dEQ0R0Qn7QupIQW85wfRSuY6/FtrGVPWdLNeU5dRGJBMDmALm+bYGCANFRyB
+QvrKmMak0rZUeWNqBltJ1Walpy+cwDvZ6p7zLAWkDW4rOc5JoFW4o55tGaBHPbuE94R0CgHu
+bSFKuoMb+mWqpXHjFW3+S4kGCCSt0/FHS9OaH7SodYRTEEABQdBODoTSoGqgjz3LARd0DDGD
+KhmsfrMKU5fyVoIjfeKC0uvgkFCIwpdlilDqGdTR8GCJTe2Mb49LIctiaEROWA5zGDGUQjiC
+PsX1ZlmFgNrdttK6K8YPdcXzT4iyqKRk+G8mRIQHg9Rgto4owRSkQBu40nEndxjMYVh8yRjI
+MJNOkJJvgeJWBjnYl1R6sFsrqwTYqsE9DcKxEn+pWL3qwAqe9ahUxlIBhVZQEgxAzW1B/CEo
+88cOaMWtREc6AhB21UIUNGiAUJoQP7LJO2AwcE6u+2SYYIe8ohyRjsjRCwbPGcafsdJ787r/
+oxpf9spaHocIC4ih+MxwxhEmhhUo4FwN10GD1nkziBJ8UAO7aRsMEeM1FcNcGtMADWvApj3a
+zCZW1reVyoDTYTKZiVCGogDtfDGVzUOLKukZnk0FoBO6NKVKU7QJIHTqiTvL5vgUk75KCtMb
+OyERnmCXqkQ0BT672AV9cuaJBG0kMUZczT8xmSxMniWTJEmAMakWTi7lb6i8GMpQOvFKd6Gy
+Bz3AFgFSGtNANYNTC1ADSrXFTsnFsJEx7OUuGaI5ZQH0eiHpzW9kAlTk8YKbJzjqYbEHDD1p
+Jzs11SV7SCCEFHAEbcnKl1mUZieF2ep1V/soUecF1iCMdA3tlCcc/o7Ev7SudaVBCJYsf7RO
+8ezlDTn4waTypTk58OEwfPVFSBZDAdBM0QUsUGx2FLu55GrFCHEg5XYIg7QjSKpzltUYdvNl
+KJ52g4FcVeZCk0dBE4x2tIzzG/OOlJZ5rHZ/rYVIDyjWIFSMp53eQ+WvxJpGpDW1RzJcAh9F
+klASHQ4pKbjGEfyJvdDpooF0LBYX8XmXw4Din3zFGFUhm8ZdQMULcwJvceGR2PKWd6SsfZ4m
++LAIs9Zjf/vj33szdYRgCYFBk81LvHym44lop4T2wik8xacv3AVXfcwaEQUsnJgcpKCwwGjK
+qj4ajAQ/+F5M4hlHqPvCF2a3yy265BKu/urhPDVYxMM6AYnT7F61kGdIyOkEI0m52hOvdROz
+41QUnpGd+sIxg7at2F31SgrM+TezFF5CcAUs0MwQzJj0CWq0QpkIKajhmVbu7ZUBwaPkKskO
+172uhqWbaNB5GB4lOvOTS0DjNIN1pGpk83PNamk5w/nFL05GTHvQiadSIw1io+ne7KtWW2KL
+U4bq73pa0VukDYbI6gssJ/EX5ZANElrHGwEDzWlljeTj2MqyxgF5t7uzrAiKIkGHk0Ga6pmw
+mtUK4ME5J6Lis9Laxfxz8a1rmSRo0rgTqzZSI+BLS/gKgFMuGtVZlLRspJlBwUsp8qlSJa3W
+ke5uX7o2yQYU/15eSMoNLQxVQ0SorI5kxNPixqzL+IGVRCPBySowA0zarWYe+EMPK1YZI3Nu
+7zgTAM50DlLPSek/0hYp5kBgIoroKuzn7k9QzOEHoUXIS5IrRinb6A2I7ndtrRePfnq60wlc
+cN543uVbaRwFly+qTW5j+ZIc0Qb22rQISMo8ze9WULz/4UV6zPnePfe734EudGgOxQhD/3UQ
+CE5sHQetH3YpCzyP9WX2bJpzAkFF0wq69cwIh7Dd9LDL7cjLQpXd29tR27izOeHumJFJc4jw
+Rb5iBki2pO7lLaE6gzaPFvMcAH2/t9D7/j2hTywIQtGO/sDagNoOu56ofPwpiGav0v7v0186
+hfv6xlTtz5wseb1o2nwaUwK9sOilbZ+wXjOidtU//YzK0aYlK3IvFCDe9iUGSsvskS20zvnW
+tw76751S0A0eaQHBSJGSJIFVjn2RLcXa5ECOkOkOLyEJRvyTCgxAfAQWpHEfYRXYCYDfUJXB
+sZWey3QbcxSA0aReVaEfyo2eepQKEsjAM6SG/bUavL2ReqXV7s2D//2dzuGb8JHV4OmIERWh
+ApSWNRABEJyW4KgSeeyfX9Tc2r1UqPAIRVAS6i0GtOFEnnxefPiC+IlGCdyAB4VKCRLK0TzR
++lFhWZRdVTEbFMQN7VFMDYZVPEXhM/1J8OXc7/3f/7VXnP/tYRocYRFyCnRpx2sFiX29QX69
+mvMAxBtKoBlNX3swWUVlH5/kBAOpQ4F9XwbKx+GcoSien3StoXaFHPu54cjBw8PAgGosUR2K
+FLyxWR76XrYAH+DxIa35Xh8OXq0RohGOlJ1dAyEywPfU1nNBFOP5A8g90eQhnEX9VthJ3HBQ
+Iyge12E1hjZi4w220vlxzyReVIaZ4QQShirSxNfFwDPEHDTEYljhn1ylTD3kog62187tHB/a
+4+DF2REeADAegUWQkgEKnPZYSq+oFxqtUkBIVxup4chhU0VN4+cVVzedgx+VxEhgI32NIo4x
+DxqaYqi5nvsRRkg8mla8ohKuWg3/4h4+WFqv8I+u6Rzg2Rog6qMv2hoCCo0wyh4nCJAC0FY8
+HgfrxaNcrQiToOBQQh5F6E5krdvnaUFFKpQWGdUNBE7ueZC8iSJITuDk7cNalGQDSYGpCcWv
+BcAB9po7spDvaUK2PBPP5SNN+qAfDuBN1lpODuIRAqRF1INPEiQrldUbGeQqkYUa7heheQTq
+raJEWiMXhpIgGdcYmgiftQuLNGF9YZnakaBIYpaRTckIjJMUqAawKKE7Nt1L2tbuuUE92mPf
+zaX/1aXQAaMBCtB5tQFeEmQUntQJRc9gupITmZ8feMpyWF+qNcWYhcj7cF6SHY5dgKNDSM9l
+uszCOWQa/z5dH2VkFqga8oRmnr0G4aWliQkKtqjXH9YkEPYibMamAPnkEaolRdRDe8bLWGUl
+bz4f4CSHsuDLB92F3qQHHrmCcWbdqtTC/YzBYblISUWneC6odNacCbbfkFGlfHhfPJzAZwJb
+Y4kWeO6PT2QCD9zcPJanD/6dnAFfeiLgehaiEemlJAyicXQQrP0FggqmTAnlt5jQhAWaTSkY
+UYHIwbCK8YgSCuRdKxXpI8JaVhalN4baymVg9hHVmeFWhBiehqblrpBVyrilXOKivdnlifpi
+e/pkio7Ueu3aTyqeFGbHWKCWBikpZaqet+0OJl0OYonSqSHSwXwdhODAGfrD4/9FZ266Ubw5
+KNt5WkZ9olelW4R4QqWRV1qSVtN5qFjVwxu4l4jiI05+KQK2J6fmpXlkwq69aG6yGRRAVRgd
+aZtOzpvCqRuOnvggVRwGFdUcTwgOaUE6Z4oByUlBp6qyzH5RhR9VgHGKWOhhgyR9wqMa392N
+x4fyHq7tISBq6k0C4z9WWoS9AScY0Sld5ilIaWCsU1DWqCs1lXKwoCjsy5KpgJ2GpdR8XdjZ
+qijaaLuUlJ8SJYopZD5QCGBxk6ImFihM1rF6XLKSFlD4wd/x4mq1mB/eY10eYMPKphrQZoKt
+WMoc4Y4V5V9sChN5QsD9pWCSkW+uAvUtB/o5SZ1yXDD/gEG1nBmx8KqbEuoo/M3Ltin0AOYH
+8cZRFZYiKEKEcMIWJcRMDawhwlsyvKRq4iKJZipdSquZBuM/foIkZOtPumnurcIFtAm8DuYD
+bm3VnlP5mV0vKYslSmQorWu2jVJzqpPMspnvzee98mqqUu0f4OyBUWmF9RhgJAS2qKQ7qsGB
+pJV5Iq2JMi2YUqs/EuI1/EmLWiyWtqSdmYFQ3K2BodPHDmp9lpvUGZp5AEUwtZzOmtq79gJr
+yBZ6VW3NFSyRWq5JwW3NOujTORaCQVeFlcFXUI7QckrBti3g9iLPdSnhxmZe/qPQpVcaAAqs
+rYEJuAanIYEujeoyCs5+fhAq/6qi7igBNijq535ucnkkfUrEPyRITzAgb0IOsXVLGTKJRUyp
+2bAJJ4BFb1UpeHZO0fogL5ZnD/7u4AVvP46Up64Yt+za2mpKQA7B7D5ZCqCN6n6syxql9LYh
+Z14h7ohucR6YafXKZM6SoNxgpbgZA6KX91buijAC8s4eGlyaHsBvWsKZT8DYlgruD+JvGsRw
+qPIvYNTXIJYu35wBlQLkRaBA4iQBb43vAuPrfpZfZjXVpyBX3Nms+WptcvAATDIeyN5nzcqT
+FYOjgtQutnpcKw3dwKrBCr9kD5qn78LwDLPJruVMPAKwWvgvGnRRcoGFL2BHJaFu66KW2oZs
+GxZmVf8pD9YusUdaZYIG8JyNRfTMlr3C6PONaz4QiV0kh8DerolBsQ7O4zz2Lr6Z8V3yLw0P
+b6Zka9fuW/ryMFgA1EzISDiYnwKn07hKl1eunbHg7R8TnzfO6wVPBILMIpu+ba/eJ/QyshsB
+5znx2sC+1d+25ky+ZSbDMP/WpUcOol2AaBs4lvuea1PmyPVcXT9cJR4vsIpclB0UrL1cS9Ek
+F+Gxb4pdZr0+DlG+5Jwl8Aadln2qM8wysHLwQUjd7q258+4GYhlr8tIqVew+IgCPh6XdLYsa
+i2yoq+L4VRWobkH+MjA/6Jm0y7XQKUvRizAGG0e2zCKv8Ds3B/l+cBwFaij/l+A2u8HtHtEx
+r6VYxaU/L620yjTxefLL3PCbXYvOzJ3HmWzhKMHNIEqiqbLxuhHM1rKhkSBQqEDMNdZHGGkg
+DyrG8jNMJqSfcdAiRuEYkZXZjYc/rrQAHfOIJq2XAvTgHmIp7Z/uGeMW795Frwu+4Gy6rJxR
+SQVV/tVGytvfSHQtS+fUccTYrm+P8V1fd62fZiWvhLQFQ2BJ75iugtEYIWjQgDULrRZV/6A/
+m7FMJ63C6uquVaoeMk7DsQHUwchshMOpxMJAQAdSrKluHrKCxqhRPsm99WwNv6coavAsNeBl
+r5ZP9Mpek1EKKamukqPNUXZ45txBbmkm07QmL6xL/35HJ7A1D86DiWCauP20UCsKzdQJd1sF
+5VaxbE90uYZcSLQGvcBTWnV0INdcc1IyaXgN//zIYyuiGNHsLE03MSfrJAdu8NWac/8uXb4w
+UQKw0LVYWJiHCwlAVVxdJQ0CfIjDqRDCIFABetD3LaMR8/S1EWfsp/BMHsVrn2LQ0fYKSP9e
+3xHpwOnbhlOEF+vzEaDnAAb4c8f07kHPZ8/aVeJTHQDrT08F20AHazMGd6syceddCRZ2+2Wx
+Jii4bo+iBjdvb/IBile53z1n+UqEXwryrlF2WPPujNP4c9Pki+HYZ8uadmwb+lmWdiOKogD5
+OZhDC5xK9rjZBtNrHlNmUv/2CD5pcJTn9ob7zFpY+Q7EuImOdTrDqMABSt4JpJe7GhQDt98B
++DKPObTem0Hqmqz1zH6eayCc9mozinSwDZFXAHSgw0YK6qgKap5/LbeZRaFyZEU7p/a4sw7k
+sk2S6FjjmopzraCjl5l6OW1W+TIDoM9pajOnp1x+sK49UympOWYyeMy0ueFARbXHeVvQ+YVz
+86BAIFejNOsxBLz9tj48eR9Y9pPjecQQOnRn9h9iOTKm2ADuN1i/24fCGJkfe6WPOdKqNSO8
+NKVq+OhVz8q1+aK8+ZvLeaJkJDa+1Dqb7kmrSDkOGuqa5hn+tp6jKiOINcfrwD1Gqz7y+m5i
+6f//drmwG2AulvV/I/sZyzBs4jtX66WlFYoDU3vvyExUTAc6ePdtUMHhCDOSezB5h1yWAS6t
+QzGVW7A7QzUzWvkfXsOI7ruXwpjG5+q8h6mw9/eLhaiu26WYT+vLzxnNMo5+8VbuhcpUFbzN
+34B0wLnPE/kYYiMm8VlhB72eE/0KY7werPdpwndV49NIUzmKe3zgMvc+VnoUN6FqbmqJrecX
+k/mzqrwZO+xN7jpst5jE6/G0N2nhoDKpI/x01Ih3M4aS2bK3yDp/IljSlOC4nwm5H0iDsnNv
+3zoOxOWL1f7hL+2Al/nzXH0xEjPW83fKF7smUz7lrzyuBTyoIuTp81La/lM7kF8BOKg26MeC
+wu88zqCeiHNz2jrHM/3Au50m61c0FIO0OtmxoPPPrffhree7TDItTNIltapo8D8+TALhiX59
+bLp8XSa/cTwhBAg5aRVDlFF4979AQg8RObEUk5RlD+SFj0PDrFsA8kq/D+PAECoOBIyhQiBI
+lILmYKlc5gbQpkTXk265XamBYACPyeKwGH1Gr9cKt3solCvkdfsd7/6m2eDwvy9QTIGN0MAw
+TSprEUBs5yKnp2eiagPksiBhhCQzpCQT5XOlBSHhYKFkRqbgoLIKZydrYtJiAMgggCFXAYpp
+SScqWFgADKrnp9VIckfKymvLzyx6TU3QevAN/o6OgW4b7xt8yI+rrPza2hCR+o8AwD1ywnFx
+9majSuODpkNE3ySU5F8LVAgGLnjxwpSMGTRcveJBQZIsCgN+NBCSi0GRK1ecODGCwYhHKR0P
+ECJwYNY7Hc6MeBmj5CW7PebOocmmLc4QOOF4zpGWiI2aaufSDRLETqWsRmJoPapVCR8rDvpA
+mBjhj4WnEqNKbe1KKsaMqWIbXpjkTqJESgYWXNQlpAjIKRNCbunYkmMQlCFT+npGDhpMQHsA
+1Tx0c6e2nT0Zc2tSE2ZQQeooG6WGFksPJEj6PuqB4Z4He5c+cdAUyh+ogKO4EiQFI5XYhWTL
+2ugc60KQXLt1geno/9H3FhteRCITfuOv4HF8fpIZWhNxt+jcGvN0I62wYcOV1QVVu0OeUnqU
+7N2zhGkfpxNZt2pdwVpgDBTyYYRYqJAVRYcQKRB4yxsjA2zpzyO6lOgFrysA+EFAKBySJbmX
+YopMuZm0iw7DBbyhrro8lotMu2sQIaS7oNpRCR40lomIPKjOQ++DFE7Y5KpPvHoNx7BSeU22
+VmaboRJaKBLCIt6G0KgYJu7Cy6+RlGikCAlsw+0vjbqYELsQD8MwG+q6yWnDDrkxSSjtQKTJ
+Jsv6eBLFeMJr06nQLJkzKk6s8uQqPEnRs6vWYisNNoV+nC1QGAxaCADQGGigAQB1yQUIvv98
+EakLuUaCZCL+fGGpwislPHM7DLd8Q8Nt6GjrSzHHnIZCLdHpo0QwWMxhDbQwS0tKOTl4ET0Z
+88SKvRRaoyEFYvlZRZVkSVmA2QYWKJKBQ2d49r//MGIgUi7ucgJBSg+8wraVsHgEL047Haew
+59C57o1D3CVRgVK/lG5MVa+zUF1qtIx1HVsrMOMdARwY2IGnajAPxjzTQ0GrGnHUip8YWNGH
+2PVKYLZZRi2CNhdnm1200UcdhXSuSYVxklu7+DsLnuEiHGymfANhV01SMfRS1TrahcZVNN01
+k5Z3anVnYKILngiqDl7kdUaF/6lRz6tGsU+ssWhQlqCMNea40a7+GcX42ZBHfosbjUBaklK5
+RFqbB0aadPBlmKexRmZ1SkKs3jnmrW5nUHuG7qh2cCNmaIGPfmq00PBhWr0ZseLTYUBjvI+F
+BLTeercAtv56c0bHDsDIBoLz1skEn3kIHqWaSO4Ll9D0O8002b1pzC69Weybm8yQ+Zoy8x1x
+TTgJZwotggs2fuDEd2W88aYZhjwgGqdi2KvLNdZ8t5A7d3bRsDkWudoABYxyCQc9skJJ4lI6
+C0LSXZJJQn2bi32yd92Ql8NT8c5dOkNadZV3kAnYuNiQFKNJIHku2pVoEmYnp1HPRk3bxMUs
+573NOUBz2+uc90YGOow4ShxFAMDJgkH/qQIJpmSxgNPq/hINdsxvHYVog+xKsiX84eQN3EAA
+ELpUr/7dy0KBCGD9rsEyWg0NAAND4NESmDg6MU+C0osc9Rr2jwo6S4NZ1OL1HgW+zIHOWkJY
+gAFGKAEkJAh9V9qCuG7lGRQBhjAoBNEZhhirEXEpXqYqSQNsp5PaXWd3Pxmiz0IkOBT1ATPG
+m0ACD7a8BqYHII7bR2qedzEsbs8BW9QigDInMjCCUBdkCoMArERC1o0nYKpjHbqqAbsQkcgm
+XIrDTSzCDQN4STG6Y9XfLoTIRmhmeCr6jpSa+MQFRgWKkKSiVQKiAoJgsgGZ1GTnsFckD3bx
+k9jEpvgo0ogT/qGMdZfKjFPgEc6YBHKQsPqZ/Q5Rkhpm44YKCJlOcqlLF0qGl2Yi41KCKUwU
+TYKRLhoNMh+pTElKLgUWnOZCw9hJD17zk50M4Sij0SQWqo8e81jEKrNDGPm5Kh01tB8etxEg
+2+mOX/l0lTt+oYNApDJOTVTaQHk1UILGyE7LzITlsKjQzWVQk57EHkSzCUZtdpGPTDnj2ozA
+SlOWTDzjcl8LzZHOnxWFZiMlAu3coBsiceiP2eCDuqzas8j0E4nDFKgCCVpTpVWleXfqyrMS
+cMmFYo5RDQ2fFyNa1A/ChTO+GQeCqMqRWZ1FETJR7Fids69XjYqktdRbDhHDHHyq7PQcI4RJ
+I9D6T6S5YnHlWdxbl1ZQuTrzWd5L7V2158lHea5rRNXmF7u4KEL8wAmOKMJmiLFYdP1mPFRq
+Ifzm50qiGIU7Wp0XHXgDr7ACMn4whMxH6UbHdgjCXxVoCGiNOVrSFrRxCRBvXetqve0t6j/W
+9KsuuMZX95LNqGjoLfysG05Y9EezcGyVpwQ5MzWpk6Q/vJYt7xadQFLXspcF4HKw69kLNCS0
+NYhwd8FLmvcsgLwaa5ZPPVfN2VZTc67t6zYdei2LiAEZZ2xHFFj5A4zC4hnevOc5BpnS2BU4
+l7Vry4B9aM+yKjifMosAADs=
+
+--Where_No_Man_Has_Gone_Before
+Content-type: image/gif
+Content-transfer-encoding: base64
+Content-description: Star Trek Next Generation
+
+R0lGODdhQAHIAKcAACQkJNsAAAAAALZJJP+SbbZtJLYAANuSSZJJANttSf+2bZIkAJIAAEkk
+AElJSbZtSZJJJG1JSduSbW0kAElJJLaSbZJtSZJtbdu2krYkAP+2kv/bbdtJJG1tSf9tSf/b
+tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAQAHIAEAI/gABCBxIsKDBgwgTKlzIsKHD
+hxAjSpxIsaLFixgzCgCw0WDHgh8zchRJsqTJkxYdoCSokmHLgS8hqpzZMCbCkANxrtzJs6dP
+mD8HIoAwFABRCBIpQIxQ0CZCm0wNOlWoM2jCBQiwah2AoMCABAXAik1wgGwCAmStqqU4NeLQ
+t0QfFJB7FK5dog3y6t3LN2/BqA9VAhbYFsBUwC8Lr0WIYIDWrAgadx1QIKzZsmXREjiwuTPn
+z55Dg+a8mKTiiRLmqpabWi7rAxJgy35wgLbrt31z90XQgPdQvRDy8tabwHdvookJUph6WKDS
+g6d9WqAc+TFWyQO+FsA8WrRoBQTA/osPT15B9+5kL5Te2bZA67kIaLeWHTu27bkH8PZe4LUy
+WO7egUYZdUflVUBdZcmFGVm1HThgcFwF10BT65FUQVjZVQfZAlz1p10CIF523ncKiFdieeFx
+Rl6AaB1wAQYVrnSfa/TN1uBr9TU4VmYqjucjij/+SMCQGBYwVGXaHaAkaAlA0ORbAzSAlEPR
+xQiABRX8pySSXjkGmWT+MchieUsOqcBZn5XllQURtGkBBWz2p+JmIJpHwHYZVSnRaRFQ4Jp7
+q6mW2qC1FfqnXCAquVmQQDbKaJBpkhXlcLlRCpdeT12kp0lYVnBBZJQpWRxkA3rFGXgHLAAA
+nAMQMOCG/tmFtWBZCmRHGZJnhcbgAUWiaaSVgRlmUHzxuXdjoIeqpuWijgLJWZEZVqboZZdt
+l+KQunqlW17jEQDsQpuKVRkCCXTYX3X9bbfdiGVlZ92X/1G7JJp2rshZteV+Ve6mbIErLFAN
+DQbXgXdFCpaH4oJoJoqahSepAhhEjEEEFlR8QK1fPbrikAaHZWBxoRaAEXMm2VQYArx6WRxW
+91aXXaJobnZAhlx5eR2sAyh6b1lgyVwZx7OCWGRS3z5kZAM5j6ZzZwzf6+NnGkfdrJ2f/RcW
+kmiB5yphRfekQGOaYWxdrNSZRa6+CeecqwKyitejiAuOdXDXVAIMUdLMaiwz/gEa8K1A391e
+y3HUcyqNr9DlFuBX3VwzTndCORPQ2Lta9fyszbB2RXnC2yF+tViq2p0pQn1KVHqeFFkG4MXX
+Rkqrw1J3yzqjumJWJIYI+Ps4Qs9N1DtBA7DN1cyOlftlrBgiDzN3JVLdYqItiqx7RfwCq/p5
+A2SwwATZJ8CB9wR4EL73ZJ2pJOHOi8ozktPyKv23g/0rEMWoJ4dQAZtRByp2/Hvof5JmkRlY
+9AUgEG3kgCNBoAITyMAFbiQABmBABCfIgN0dhFwBDGCPGqWBE+mNY88LEc3WtTCOXa0AoVtP
+/KSykBUqRCm/awkCJ7C9rNRsMlpaGtSudTAj7UhE/thSn8gc2MAiGiAAFSSiEou4xKrE6EOf
++5y8TqihrZQqZqAhUwm3lAAaInAtNFnI70g3EMQQpIlMbCAF4xUabHVQa+dxW4uyAkElHjGN
+aMwjHvfoRCuRK4o8w9f1PAMpXrkvO3pcokjGOL2C9C6RfIykAy2gno6wyQIA24gBNhlBBkjQ
+AAswAAMlqEQJBuCUqEylKle5yiMe8ZScnOARGaBKV0IQlRUsSWMguUCUdW51q9ui+3YpyWJ+
+MYEFWGBEFMNLYzqzmXkMZS03CcFXutKT1IQlK1/JylZ2c5qfrKY2bcnNWT4EmgDwyhJzJ4Dt
+ASBm1FpUvlBoTMmg855F/rzJM/fZRFkGYIGx5OQtU7lJbNbyoNwk6Ctp6cpsDhShqExoKyUI
+pwtUgGIHUCg1rSlQbRL0lrnMCT/xCLIFqAQD3GOnhxCgRHri86UjPSZMl0jLb2bTodXc6EMD
+ulGdTrSmNn0oLG0p1G06VKIEhYACNkCBClBAAktd6gEMQLGCCrSTQP3oUEU505h6VaYjEUhX
+x/pVjoDUk9vDJlK9CctwdjKW27zlVQ/60XB2k6jVzKpR7frNvt51kwtw605zCkoP1WxcHeJK
+Y2hGs8kYyVbY4RCYbAXZc93whpalYpcUuFjHTCCsIxXrM8+1KgBM0JjS5KkEV0vBCW5TrwxF
+/qIn1YrTac6VnEUdp18V+tfZ5jaofZ1gWlkbW07uz0iTAdPkHsu/mn2lsTXjEGOJctguNbd/
+7rIVf7rS2cnZkLuXpQwkRdtEyzq2VAM4XWkjSMPZMoCG8A2sVSMq1/ri9a5ylSZD3yvf+9I3
+oPYNMH49+l8I6jWnat0tfSdKQxye5SzwjSAKEysW6lQGudDFzoAuK5nIdIk6L6MMAB5gUYtG
+4AF+AkB4OxQZ8Gbou/9jLmbRy1I8ftiwkiWbYdH7q4a8l6Gd3J6Q3TuBhgYYt0iu5gTIcieQ
+nZAyPI0yXJNM5QIjOYKBxeZshQxfLUtZp/715nZABTLHTobDaL4u/maV+9iwxMeiC3jPAyJw
+0Q3U5royphlW0EthPZPqf2RLbH+sGGMe85h/C+mjaC3I6EaXpnqLMVlC2oKTSoMEtPpUtKM3
+/ZMq2a8nm/p045wz6tJoetGcTnUjKwSB1dQoR8miy6Xq4hAXslAhkmYKpPVZoQ1lTnm/1CCL
+2KVqKwlqNsZ6j5wBJZ/V3GUvv5GQlKB9HDcT5dbQmYioeXfpoHDX1zkeULxmxa5hm5tJFSg2
+SybdkFbTiEb1QRazVaOfbeVlAg3Ad2/2DW2C1aUut3lAtCm1FwqNetffWkC+qni8W8FsTHIs
+d+GYFkTR8Ew9YPwJBG5ULPm82lD4sQ8g/oU9talV7cL15ousH9CkAfz7N5g6CcItkqXtgJjh
+kvVfFNm4w/S1MXb2eh4G0q3ugzAlAoIy1qGOFe/5pGbcMSs50J9mHp5dykj/dnOFsR5zXKu6
+Ap4SV2fGtT/HXLojDnBABBojpoOdbzMYUpNnuVwZOHYGRMGz+QHYve5+EcTW2VaIwO8D66TP
+SN46m7rUm0XxSCnO3nnRj9dlIr/FNBl/WJlAYyzTYsocbLHjVtHMwG1Dtc2LRwvazBt7JKoK
+Z6dWLjHNSSjQ6rjQ+0DOBgvLWT7sQl7Ndl5x1fsGQoE+sb1zJQThzh7PlztNZu9cmzlP9KSu
+428Pf6Pij+cV/tU06yqWcl5ZnlkUVqLzzApJbHuf9OWnp979LioxpFBhhIOAOw0aABdQAAUu
+sJE25Z9cdKJ4AtgonhE3V9MirVIZKcR+Bqdud1IqC8Ah4OEVMAZFO+YY0VV2KCMmrUNIZdJ6
+UTR8y1R5CaFrLYQSL8Fki+c8jtJzK9iCFrdz41J1m5E7jzaCJhE8roJz+zMzF4ZYZvZ9fxZF
+AnR+CWODMlch0SFsswMkXMIfUnQ+PaN45pZBB0MdE7I7oZZtCPc5VTQu2qdznCMuthM0meE5
+ldEQjNR3pPYQgFcavxd1FkcWXpEBB5AB2ZOHeLiHeUgv2EI7TLI+wSc0A7BpTrFC/s0xOoH3
+Tu2COdfhfDl2HZ4nJmoSNOWCemZRiG/Yhn8HausxSDsjL6J4Ki+YPraTL3jDJD0WI4Bnggex
+HHwnOgqheTnXPxcWSKlHiN1lKh1zhs+TKmX1QP/kVbtDQExoHuMBOPaCiafYYdL1M01odydH
+TxlnRuv3iqXGSzW0WNKCRXkTdDwTIHZyGQjAAIl0SsEYWoyWZuGVLuGHfDu0MFQzHmW2Rz3h
+ig6xhh6Rjs1kVQywHZCCLSnCFdsTQQ2ERPyYQKZUTgaAQLMEUcq0FhF5QP6hOqOIRVoDjrZC
+VkS0gEshEZvEkZtURPHFVenoXihpSg6UYMGlYGG2U62F/ldJlkqnlhDl9Vl5NAA4uS4XKY9K
+MhkjxR/NJJSRJBEcmZBEhJKD9VFrtVVI9FB8tVYy6VdHRAE25U8cVV8EBks2GYzFEVYOkG65
+o1KsM3abpypoNCBImZCwuJZNRFdDRVgaNWW/9VZNWWC8lVB36ZRbSVAIMCgUMHRDFwEvcmIF
+QFgN9ZAGFlch6ZYMRIEDUF6F2FXk5ZhHqUempGALZmA39UnWZFtyVVsTtZdamVcuGUG/tU1W
+uVuf9F45ZWWEJQAe1hVIkligAlnad1w3x2He51gOxBXOVJmVCVbNNJzFhIT4hF/kZABddpc1
+9VZI5E/KSZe61UqoOWARlZl7/iWamnmVoOSZUSZXLeZmzlVSGKhmONRd0QVZL0ZZ5oVm+qNw
+lPGM0SJdkhGG7LRPxrlO1mVdBDFbEwCgBdmaDJBaoQlXLllftBRbQFagrNVR1glmRwZcm1lX
+CIVV3Imd3yRBw9VanskhyfNcjjFh2seOItpczxiEf+R9F2ABagZi+hMtZyaJGlZoLpVG+6li
+/tN5hpZYC+FJXZaS8TUBg0WanslXqhRYBhagEKpb4VllUgmby6mdcuVlVMZJDypl9QWPc+Rc
+4acdN7RY6MmeyoWBtvJYGWNnYHdRBQB2SHcAFbBUFIimzhWm9Mlc/3OFRrKjHNGbgJZzhmaj
+GQIR/ljFX8SFoUQloS9ZSxMQFm4jaLHSM3QIFl92WxNKZWg1pP0VZb5VnXhJoah0m4KGouw4
+ptx4ppuXONmBNM3VAC6aWKWagdx1LlnxWO+CXTqmP+gCgbGyP8k1n4uVY5IhUvuIahZRk0ZZ
+dMq6rN9iacYKWsh6EFURrcy6rNd4g9XKa9m6rTj4ONumiGwIrmpBrdxarqtWrfoYe47jd2fU
+lVRhrvC6GHYBb/Imay9XElXiQptoEPu6acdzXl4Rr+zKEzgCG8lmsLFGbwQnbXqBhG64iPOj
+HKVmdBCRrjFCOV66cz2JHgLbrROrEEfhajkysoCibMcicC/3cl23F/Fz/q3fyoARQa4Y4V2P
+MVlhEjTnlrMdKxII2yAm63Q+a7ByBnOQF3nCEXm4BxYMexcNuK6y2GjO+GsOF2zBJHE5myY7
+CxHJMigh92pK1yB1UbS7wRcMCxxjS7ZvMX1Nu7YmEQHa5Wu2CHU6dLV0GylZmxDzBm8nSx/M
+RrR8UbPt2FhfgrIDIxq3EbIpyxs1oW2siCQ8OFk8B3GlCELKVzsJQHSeqBYvwbVxUSwjayPK
+5iDvmHplaXIVJzTHMRz/Bk8HMK8OknK7RjLXShEXsnDgBqnipxlWu7uE1IGu06Kosx4m85eA
+0mqfK3Lz4XHvsSNZNICywzaJA5mNkRd0iLKi/qKyiott4to1dFabNNtw7/hD7QOO5rZ4ljt0
+I8MQFrtuCBcfhoKwX6u3TacsQBSNkxuQk8pdEOByUTh+bqYtC5CFmdsTWGI1vQm3Mya3HCiN
+d9e7JaKMZ7IrslMW6GsVLfEcp0EyB4EfseazXdts97E8G+S8JjeOwyQhqjsZR+Fy4AVzsaiu
+FVLAJwQtKPRdXDGGFrk0KaIwfLMgSNInfZJ4lXsvdcIrXVEyLzyCR1cQfyJnINfE9WoZTEPC
+sbNBAsR8kOcbASzAMLwnbJsnETB0tSst9td5C3B0qfpgaHFDq7J2E6YvHBMiHIMBB1B8GxGY
+BfA0aJE4dAh9sbhr/vg4eUm8KoN3bAl7yFI8wqX4LCFSLbwCFnDEwGRBcJViIIX4xSnxtBAb
+EWCRFVz2MkbsYdqxGfzhpjDEFB8gcLcKjb94FhY5xYH4ZGShAZcsEtsWP9ZIgoXxEijLwVGc
+LMuiyHo8yZglgwkjx84yh8WRG0zGFd5irpeXODQkbi5zRefTMYN2u8bYIqKyKGmiRSC4cLCn
+yeFKgh/bOLPLxEdir8VCXTiHPDwijysCj5jRKhXgAB0wEh3wAFmSAODRKgEZywP0MWrjH+Rc
+bPU3QGCKPxRYq6NclqyXL5JFerRphP58LyxSLf0BFh7piYCXiA2Rui/HM+5yQajCIEJi/r+P
+IsymG4piQZt7iiGa+xNGDIVC5s9Gch2m4od2AoSOiDM8Yj59HBbKZ4YYUi5dHCwvARj9Ws4Q
+ISFJU2Gi0qY6iD9y5M9NQ8VS9ywGWC6a4c8i5mgItwB7TJAI0Kjh8WK0OW4+fauO6Hm4Az2s
+wyt398homMSxW0Yl+LARMSlvF3HfKI16zNJUfB6cs9N3ciYd6xRloSETwDHuUoEI48qb1zmJ
+s4Fjt0XDtj7iIoKCfM5OrSkXAYAqncwj7EGCM8WTW3HLlzY+VDXwCmkXUy5uLRlCQ8O3AiDh
+gT/3ci5OBnVW08mL+9nY2hDxLHX+IXqfEYHc80v2stJUozPz/hIvVyghFsy4bDi7z0EnPKgV
+M/MfXEGLoMIlvwQ1eNeNZaguAzTAGYHBGSHCDgxCk0pyVQzdhrMk0DIqnObeoG3OsuhCdOIV
+1oFC4Tep0OVwUITDds05nciv7peE/Z0Q6/sQP9R4dW0Wb3S/f0iAgXjbCF7LzFo9klZaABAV
+N9y6X9JigCo3U2s5AdLVdJgp/Qp/6bweocfaD8iHe3jWC8ABA/A937OHpJiRAV2Et7PHASvW
+eL22+SoQC3AmyIUVnfM1pee45qIwnOF5ytwqy9fRKNHUm+wTD9cdT7ajhwXJ5ts6cYN3WO4z
+1KhuYG5G5WwT6/JilGPDGoJDMrin/hepJkT0tDKrrLKCJqSr2s57Ma4zfhnCw9GNdw5bIYPR
+loDB3/k4EQrH0GmWLmMoKf3Jema4KwaUkKfkSeNlQVSLkeXxN6oOOH+zMTKjPrc9OaDsk6/e
+ZMC5GLZWfAOBSQ7A3xM+Ee1Ei+0Yheoj0F8Beo5cv/X8VaN+lHTzR+LLJBeOia6X55FKcVkN
+QlfDaOsXRnotQ0wkZOE2FuI4Oy6txt44JNSyT6d0mfbIaDH2vYB7XomSRfKcNcuoFfkELN7u
+SCQurg5Q6gf0Y6BX3vFtit2xjOVSZGh0RO6OThZU2Tis4Ol+70wmPIgUnCUB5l7HHA/PQLHE
+APXOLKvn/oFuoySOIVsOZJAfLwAsr5+7c8yfbkhRh4woYkiaA/OKxO//0vI4Cu4OGVAir0WR
+QaSknkDDyJEQxU3g+ZpMCVKYthOQFIfcQW6GzkMbmZD7WWOdpkBcVpCVGl8ztZCh9GUKhEls
+kkavVERICpct6U0MaVWKCZu4hBEOlEzOxE6NDEw8DTTc5VW5SUSvkk6TiaMYsUkBb5mmBfaK
+n0f2lWB3SZqe+vRD5VafiVu4RK5Tr5N7FBkI9GBmaO6xgpbFBIHEWEzclmmN75jFVZpwf/mj
+HpWMWaFuf5WC5aS0zxAw5UVFVIhYsRFYlIv3l0hZsfrj5RQ+31WxpVAs2ZeL/iqXuHRUs9+X
+q0RRbPJXPuVTrl9N+0hWAs5AhchO5xOBr+JAm1cqyZ+OmWT8o8RRiflfPFWX0kn7/mVLmQmh
+UVpOd8UASoViYIcBAHGhAgULBzYYCGBAYcKEChEyRPgwwMSIACwKAIBRY0aOGz12BOlxAAIE
+GhcgGBmyY4EBKj9+vOhS5suZNWnGpJnTpsuJFCVK9BlxYc+GDiEyJEqRAYOhSYlGTIqQQU+g
+TotStZoQgQQFGyhUqGDhwoYCESJYiKDUoVCFU51CVahT7s6PLAcUQImAZYG5fen+vQkYJE7B
+crNancq2J1OjVdfCRWrVANPDFCNfvSx5cuasCilE/kDQ1LJUhwwWTJAcuS3HBQkKsCwJckHN
+vCNta0Q5MrZf3oV99wbue8LppZsrRxXqNgBj0VEhLqz6dHnzt0+pv518/fhxhwsYY4XscKRd
+va/zwl6AV/3d2ncHjK+dG+97kvPdk7d9Pi8ABK4HbCQssOAGDImlCAj8qLHnTJtgKc4WQ8rB
+ttY6jC3ouNssOqyIoqzC7DTcrrLSvnusqO7cq808+FaM7z3y5suLJNvYg28v+vKbEb/X3rtP
+LxTdW+Ck2RD8zcaUAvyLOeaM6myi4qIbqrillvRQwdEeZDJE5bBjassQQaRKIe+eLO25BXhM
+r70dT2pRTfNg1E2v8848/m9HHFtkDyXYfMTxTCHtU4/Ivlykb6/9LJpyyrkmaA6yL4tbDino
+lvKOws4oDO+o6n5yTqlNJ5MwRMuOm4q4JSk8E7YXSZJTzzbHUzG++vTLLwEaX5UPpTRllJNH
+Xe8EtoDfCuyxvvtutSjZqUxLdLjhupzQuUwdBQ+i03xKiIEGqbxUwWkr3LBaL5N7UtQHnzot
+XWcTnUzGuwpwzTX0ZgXWRzhrO7PQG3ecT6ALLGgAWH0HSJNFBOgk1EYYjQRO4bvYTNi98dxL
+tmJEtTU1UXW9e67Eb8F8aIG2lhszyysfu/Db6q482UHpQNUOucSmQxmlBG4+4GZ4BzigPzpZ
+/pKXT13xw/NeX2vctwGD/MVAILMKOCBW+/Kbj05jjTzv2HjvG3KlG48leGKHx2bPYrMvZjDj
+Zq8d6mNNFxsK5wQomzLlo1B2W0NOv2V2wUSlBPVUvO92Dt4ECCBUPh6NRBjXehfmcSTXNlBA
+ArAikOC1CCrAvKvKGz/6RjYNNfQ9eW8k6Wd9ITf0YLGPJZvoZCk4u+K0Ge3Qwm2D8pbwaV9z
+LWcCDiCg+Jzh3RnvwfOuFqq/iXNW3eymbIjKTLd7KIE/bz3aXcdflfjW0y2goAEEHijgAQQg
+IKgB87sXWLd8AaXvYKLvXC//kYS0n//09j9Y/1oXn9oVMDGB+xu0/qZ0LUn5zoEJGUDwDqAA
+AijgePGK1fByZivlYcqBa1nKcFCmpLtNr3lfSg3CdhaxOBnteznq0elciD+JvRBiqAvg/2LH
+HoVlbYc/7FUQj2Q2mHCkgEY8YhKVuEQmNtGJT4RiFKV4NgdM0Yq1q+IVtXhE2hERibXDSEy0
+GMYtltGMZ0RjGtWIxShmMVlVdKMWI5CsOQKgixYjIxkJs0Y+9tGPfwRkIOMYyALCkZCHRGQi
+FblIKA5SiY7sI4C+iEQ9TrGSjMRkJjMJyYpBkpOF1CQULznJUYbSlKfc5BLrOMVPFvCOTVzl
+2UqJR1mi0pa3BGQrLaZLi3VxkLwEAC9f/hnFWYKRlkcsJi6V2UZb/vKUnoQiBeI4y2Qu05qa
+BOYWz5e+AkjgAJlDXzjPtz4EHNGZa8zmG9NYzWu20538gQA3H/DNbh4gnPIkJ/ogQE7z/dGT
+w3zlJ6sYgWF68Z0HRWgTBxDObzbUm+Dk5j4lSs59us+iDdgnKltZ0GSxE5P+yxHyMOiahB40
+nvqkaEonKtGIqvSiL3WfFh3JUTqa7TOf2SUylZnDANZppCJNwAGIN9ThlbSM6VxiPuW51Hs2
+NX32ZKpKEfDSYFaMpr00myGzykUmIjWR/nOd4uIVVLIKj6hnHV5ajSpIs21zng91aEPvCU56
+zlOcK10fTDE6/lW9TtScOX1kEmPZztTxNGtjzVliD4dWxqb1rGvtYxy7+VRvvhWqc61rXfEp
+0b1C4KWeNZ/7QLvXBPSnAHiFgDot4sbBsrGRt9SVYcVmOMUmtrG3dWxuIbvGyi4Vrr+NK10h
+ylm9FteioC2AaKea1wbAqz8UFWwwA6raAtbxqh8lmGzlQ9uyLja3uAXv8BKw2yUC06l2nSxw
+H8pNcFLUuO8VLXyJ6z6pbnW1ZnSkR89ogRjxz03BK2t4BfzdtAZVkV5dY1PlWs/M9RZ9Dk4f
+P+E74Yoad77FTS15mRgB+WnXdNwVnncHPGINJqAC5EUwUyl7WRZDVH0VLu7GJsDT/gVYlE3x
+xOs9WYpavlb0bK0FpTV9RWMcRRBqQSUx8SpIVOMRGK1BvYA/Tali9D44uJOd52ht7CrEkpUA
+hwvqrVCrYtsOj8fEpa5rAXvLC9gFrI0DnpfDW0E6U9DOdR5xzjin4SRupaUKBm6Vv8nXvXCX
+wMa785cVvSPzjfmeQiVAVCca3wZU9b5qtmYFnJtdkG63zHMuXvEsSMFDkxq3UEYwIVNdMR2v
+j73BtadmIYpBxaa1zre+c6KHGjQYxyexUBsAaoPN13420Y3QtO8fOawisPrwyJAWapKdnNsm
+PzlqaFHmqi1yWVdbdr0Qtuw9u2zrXOPa3HbeNY3eU1wE/kRNfQQgZ0uommxtK/ICQeN0TwlF
+aznbetoC1rWT9Qxk8vb2pLB+66vhir5xK7ncDz83niNtK7zEc2yHg1q7pRrTID+x3lq8N42a
+vW+gRq22CbDg8ND9b3ILeIP/SuLHEZleyjI41gxduGblBm2I9zzi6I4axddnZAmK17n77Fmj
+K51TrR7UAoYjD42vJlKqy43lZ/V5uR2b2ApgYIsy32I8G0zzyrbYofVk+LOHmvWf/5x4QT3t
+0FmKQR7jxaKWNjbek03IzUEdh/4tMtCe3e8lO9nU5daAAiBe4AOAhZlSJHjezTZHK8/1qZgt
+e8LRV+sJtp3tPv/yBpm73JUG/nvY+xwJaKlY0r4juYb5viHwBq/BtdNZ0UrGvcrFe4C7fCYC
+e+l8yqFWAQyq8bpWdKQ8b658u54d3GoXteel//AJCjXMFaevRZfb6LxMeulqBvverRgBDABV
+hoVVYc5yHW1qKz7Sule0zlxDULSYBS2JvYuSIw3pSLfkj760mMizI0xTPhfDssuDKgMEMN37
+vOmzvcIbquMZgNCysJQSLb1LswyUovCzmAq4N6CKNPTIjxyyI4IyQT0xKw3imQlSPOSxEZ6L
+OA16jS+zizMSwMc7otNCwAdjrx48L8MhNwdsQK07q+OZsB7jq0sjLwwAC5FKNxyhnc8oJ4so
+qIGK/iDX0IvDqSC4CyoFgBqzoB0MIL8DoID32Lodkb80Eqia2iIdFK7Lujwq46agYkAhxLW3
+Uyzq+y7egy8LxEG2aqKuu7fWMLL+859C4z9Ssw3x8q/XUD+4U7IHqAAFMMGvKICUi0CdgZcN
+GiJM+0MNrB1pwsCKwbGa+8EfLDo7TDku1JmgIyt5gRdEezK7gy9TCyxt40AlIgBn8Y/yMMSp
+QzIEIKiM+IDPIL4JgD3T6S7hATbZiz5Se8WdicXXSKTp6qqzmSjNk0NxA0KHazujQxoMyq1+
+qz2Vy0RbMa7O+zI+swicOZh0KQ9bCRvTyZmRCLUBMEEA+IALgD3XiaBM/jw5s1o/xnPBCPJC
+j5uiwdqoJLqjuDsvt4qwn4I2x1q8nNmueNEL3tuL0jqYkQo+CCTIBNCrAkAclDiA+wKmXDyj
+C3qNIJkAwcsu05I9oNE/oWpJqSuPgESy0FssUrM9LlSRSFuk68oimhoklhKnF4swCFgoaZSX
+T3tGPCs66+O9lrCADgCADrAAi/m0gTxHlrgonhFLauSjpvMj+SOUl5SY7XHEnpS4m2y2tuQ8
+gVQADQg1Czq5oNEZUDq28jKnOUqnT4qllHqxfNKTn5JIt1y/mxG1MGNBC7jKrITM4rmLnqvJ
+AsNI9zmdqCGA2Vgz5EukAfBCeTQPnXmPk3AP/iRjv5qURz8hsitULMakSibDxF8TuqjxP1YC
+ReuqmMi7QawSRXiSKmiTF1spyQGoIJ7xsjtkQAQYTRSsINgQqgYssNBDngm8Gcm5mY5DKAW4
+mfTYNJvMrtSsvUSLlX6cn51kRcXDxHGMzfzjvSkERU8MLCW0CMBcvay6quWawCtEw9lAC4JQ
+API5SQraROY8t9sawiYrKyfUyC+7i/n0o758rYpBgC9LlQQYDue0ldFRzjpMOdRBz/o4Mp4M
+OpIESTB7T+CRz8+8oqJESauyTyU6PkLDOJawIwwQQwrogA4gKAxwj9C7wwUl0iIESqixSQi9
+xoOClyBpjYOZgMXa/p/xkDNMLK0RFJJ8y0KVU7xXtEk6BMhohI2vi6YKzUBHstECg5O7QDJd
+I56AU0XpO8MNckHrM1DPtCazdFGLUACeiZ7hQDksNBYAC7E+yaEO+5/YZInW6DyTC9MTZQmV
+ZMgZlak1S64IYjlzC0JTA9E49UoZvJmtgRcKMjl3WrULhcvTkNKw8ZGREh19Y5N3HA4Pnbbi
+WSw6ddVKFabfRKMsIgktHELatD1hJVLmJCr5Eyn4NJzxMqU1XNKKiZpFRYAZi068CCvBe0Hi
+mR9ptBlI28T2ZCxIA8qtqU8qdKJUk1SLWEHqLM9baz9VbKxoxCC7QLkt7KRUEr9rjKNI/ouR
+zrNWP4kYR4wa8LRN0zFH1wi18BrXnTkjpALAe6XQLUpEz3u7O7PL9Us5eN3DaITU/sQZYcEl
+DtRTAEDRf9U3PiHJ11BZWAzVNqXBwym3DVLNhS0tJeLVe8XZUQzAPZU8i/lHb7TMqNE6LUys
+LVzQcTTRVuRWjmNHKzIe9Asr2yjNHhE8TmRT87Af92DGnUOeaM3PXtVZ0ESqD23XclvUrh0e
+GRlVhGVXcD3DjxU6z3qniM3ZfGVDAOAZBYitkfOPoOEVfUvNkqPKZaw6kJ28mANFdMVATjq+
+JNrEnQTan0RYnXnXYhW42ZSh08JTU13cTyTZ4UFUnuragQXc/ra0Vaz9ny4zOaozXDOlz1uC
+TciNvnGUG+/sVMsEvWpTVJj0zHRtVnMNzleaI+vbW566QnDMUnuRxsKlNaiputysqo9zgKsK
+zgltojhLwWFlPDokVTuMyk+lyoIMGt89I1E8J+401xw8UdHNWuCpIRtJzMRctHlVJQltosY9
+opuFonaDvurburIigItFnAwYAAIOsO+NQSMNuncJuhbFJPLF2euKgPS4GagFWHt5l61BzDC1
+zpHaTkrFO968W7ut1Lrl2SNi0yplrAJmYQJ2YRZ2yY25xCXDXTxESyRzrnfS3/u0KYKr3jna
+mXY7zXkRq+y0FUIVsY2Nl7dj1h+z/sb0TSQINi2BPNawcVKCSQAOIAAt9gAC6GIPmM7I1UNb
+tc0dAVUHPjC/5OFz5SIgc076OIniBCAMnpEFPJ43Lbxo5MkmPpuCmqMdTqj820njobgXzoAs
+RmSanT4Fhju8ANMv69BmQlzxG1mcuq9YuqMbhdoJSh34cJeptJWJtD7jtGHtOdw1BgBAPir0
+NaP+kF08FNH2ANOjDdNQNdhjlceQ7Tj9hSQBRM5XBSvei0va+lLaAp5aU000Bk0QXuU+EuTt
+hUrvgtfaHGVgk5zLFKq7mIDfveTexCoZHWEpQk4stOI46TAAij3Rkce8IFSqbAmdqBh4pqRJ
+Sqidm81N/tXYY8XVfWHACeLCzY3iAiKf2lklXUo1lPDOK3Uc2Akf9RjkpA1IYREUl4gIebam
+wYNKXKvLunS72hzX06TH2fVnssLRRWqtVXpYvvw6VsHIwOva7hJVdZtOMu6u27PIid6IbBkW
+2D2577KzjU68xONUfRbTTj5eb6xO4nEugFYkS05lKE7cE0am0zjZifFgzutg1Nm5Vnzl48Fp
+J8Fpes6k2O3Kj1w5OdvdvXURkhRWilRqednmQ/LjELapDUxpWnKWqI3H2QMzc7QgDIIV1X27
+7U0AQZmIYfGLU3JEqtNJzmzQXjSsjfQ3d71MnTmNRCpoMg1MJMlrODvq23Nr/rwsTowebP8t
+KwJZjbCeC9iSvdoqyHkBvNRM4upbuY+EZJccjAfuXBMmaGOaCwZxF+xFK8qG5BJlrFVMLAYQ
+jABA7OZeJqmDmKKRbVH+XkhDN47smiLioz8GwIGCaidSyKd2gNUOCaqmk6mkyApyv//16epT
+LJGhi8NW7Z3GpflhaNmTX6xz13pNaF35C1WrqfM1aN/+b46YElih7jpTvCIkVjw8GAOoCeae
+77DGJfxmbJp9Zep7U7v4D5lAEv3STcGsrsWd8JAoDTsOtQCutmlzPdzhCeUu8fm2pSy88Lwk
+PA3POBztjQ9v2K8d2RFvbtT+kFnW3uG2IIJZgIR4/onlCHKZgBQn4QmK8HBbukIk1snJTjTx
+cpHsFgweFwBldt0x6qgmT+yOmB6HmGH322jQhVIm94jJIHOQmBmo4J3vUA5OmQhJ+iO/aA0Q
+W0bZZU557HAKlwmmntSv/eYCivEud4AoC6Ms0ogPGZG6WRaQkPCZ8BJzeZkwoZLcIRwoj6TC
+kEu0JjyRDsH3WPQA4QuxDkXIi+c4L3HymyMB2MrxzghIN/DJWACNSIyXiBnsSQ06pxnoeIjr
+6QkQTyLc2ObgKKdQxeobN56emQ+cbt/7yKNVDwxWDts3Mom4+A0x6YuNmZnbERmI+JsRgo6M
+YAgBkG85/3UU0hvVwJSm/jB25rainBiAZd8JZMQInuHrMhtWaS8nAvkfutCTd66JT3QAb49x
+A9B3ZzGARQcOhoCWUUkNYHcMeVeQO//0w5aiudBmmzgJ1oDoKi08veDy3jgJAvHy3EbIMZd4
+WP8NjpGIOaeZcFkZ7sCSMJkQqcAWYc9zVq+l3mAJucj3jNj1aFtGb6Q4hPeLY5H5Mv/uXYr5
+qneJUMl4K6F4egcXTReR0hANu7F4CFciIsGLnOCLg8kIaKutCeKXlJ8JlIh6mC9w6sJ1q5/v
+bdkSYQd6aWkIy/gOlukUr2+SnreOzAAK5Vb0IOdIjaiiChCAtJ+NBRDlxPqf3Yh7rpn7POry
+/sXH+zg/oLqRjk2/FCfpmGrJ+UcxCyz5EJ5XDedQfFoK60W9dUf/jw5HzibTIbj/cqj/fM7f
+8aHffDL3IOt4cpx/G+vweddHfot3/rdgALPgHM0I/Ksw9op49RL372DiR5TAiCvdC97X8eEv
+8+Ag8N/PCOXJFgvpHeN/977/+cNvfsTvDAaAgAOIAH/hHAvoimIPfEsBiAAGBAYgWFCgAQAK
+AQhg6LAhxIcSI1J8mAABgoYAFiAYsKDAgAILJjoMWfEkSZQqU5JUuBLlQgcvWc6cycDgQQM6
+EQ4sOFAnUII9D/o0wGAn0aRCkwa9WZToUKg4ox5kAOHAgwMRKmDg/lqhQgQLWHUe7Rk0wM2B
+TnNWpem25sMBGOGafNvSLlyYeCUujJB370qEbKPuPJsTaNCfSgUyKLtYKlW1ZpkuVuxzsdUD
+EhRs7RqhQIQLYCFI/lkY6FqpPgGzrggSAYACCWTLRgCywN+ZLlvz1i2zN+vKQy3zLH4ZsWni
+UBvjVKp4uOnHwoc/vrqB84UIFK4/wG4huWCyqQcjZAA8t8OOHUMOOO92N3q98QFQ+D1/pvSq
+BpH6dBwZsVD8OcdcfpdJNV5+hlEFlQEIaAeBAhtoRpYBFHxnVHkLILiUafe556F8H74HHHwi
+QlSgUmsZVlZhTAGYmHRGLUgZdM0lyN+M/kQxEAGKZKHFwARLHXeYURANMEACIXU0UkMf2QZb
+kwgscORtT8p1pZVyNWCiQyWWWNGXXIYIokMTNCZgj2alhZaM1DH4nI0HHlUgcWqhyKabd954
+EwMa4sghYnIVsF6Sg1I55XoigYQlSBcdieWVR2Y56G1KQgrliBGFqRGJwHW0aW+NAakhgXeu
+5diPADoHaI5soRrjcqZiaKCeMWJopn9T8SSopQgkGalIGFFqZaIFDHupoSFN+WilhqoHrKTG
+/toeXAt1Gt9toOa2QGJGjfqqrf3tdBS4NBYXmYtrPvYfiuTWWiuG5CL3XGEdXeTsr4nq++iz
+SD56Kb+SPkkp/nsA21awr0pWeiRDUt62AMQLkZlblQNQIOKacP70LZo6EiTqjyy2Ch66zs1a
+ma4b6shcq+/mxOeZ88475QKOqmesXOoNTCyVxvK8pLOLSkplr8gqaduvH1mas6IeOTmAh9qS
+dHDFUr8Uc8vHeasyUltjPfKL5po8clJzoixqxy4zdROuMtO75GyMvoYRRgYTHLDO/R5rG84G
+8+qRsYN2tGjeDnP06EVisoQw4RYrJFBr3Moor8hpF3Rm2R8DOXmL08E45IyGqfbyuiFbrnZ/
+AbSNWMY6CTs4pEjS7azRe1+57N/P9v0s74JipLRHVBIrpZS5C8rkXlYzVDHViS6U/qoBcE3Q
+osZFISevx2Zt3Ke8orv4OYcoZ33ZhmrFjHqMbJNaOWKI8tv4kbj3PnDReSdLe8L4Ayzw0ev5
+nrP75peziZWkSsJiT9W0AwCvAYkBDZnAAiD4QG7ZCE7v4tN+zLe60yEnfBYUm/jQIr7JuSxr
+6psei9zVIF+9ZlEfCaD+bla/271vaPkbWgTsFj8XJut/HJlfs2TnHgT2L4iEK4C1AICWCDYm
+ghFsk7c0Ni+eoKl6BojgYBooMlvByIqwohXoEGQakL3LhAaAIBrXJyNESSskfXPSegToxhi+
+TnhXwpmhRBOBBgBRX3X8V++IaERlsWRTRqQb0RZGNQBE/sBa3mJin9KIQh+Rp3oJ4hAFyecn
+8EGGeh6MEwiPUyqtkfFOyiEPBJEkyTMxoHh8mw0s39i4QM6ShgobAAQGV6iOiEaPfERWouSH
+pbpRjYj7ew1djPjDRBZMkElciFEgKKompjFXoPvkKVWDvWieL3RU9GI22QLGpZiNlJgzJY6A
+wrtY3iYBB5iAz4oVOEAOToD7utmS3qeeJDVAM72swAWy84ACDHR/kfLhARXJKPbMplKLu1/B
+3CdIZiHQNox8pkKiCclpjkqagKIc56yZH8mZb4vUSY4nvbiqcT4nk4dBWzjH+LUpBgBJB5DN
+AQ7gzkLxKnA+u10teVdLgR3s/ki5dKMEMHAAgAIUAwEFgAQ0Q1D//WyYieqVEWnTUARGhHHG
+ZONEB2kojCaxMdXkaCSxeNJPVmZDHr0i1rA5RXCCEJwyMmf35jVTmX3SVwkgwAEIkCQZVipu
++PSbHBlVVF8dQEJfAQtovhIBzTR2A0c9LM/ACtGe8XShR8RqRYNXTLEK8lNkTaKZHilNtD4x
+PNgM3+XO+BGdKiCwBzjSXFkVNpXC9rU6wVV5mohWmLotpSiTzWANW6XlHg6xfhuawmCnAAlc
+QAENkMBUH3CB7ihAAQ9oQOwUC0Bm8mqXt3Ol8JjXrN/1NKxhfdZpn/mtAF3xrE7ZrW8/ZhTA
+8rcA/jq9KSxxW1y+0rVO4SPXt6jpRDSiBood/GCCZkM82QFLqPZE7HL/RRtjgaUB3iVoASwQ
+Aex+V382NNQPFzbHJRXPvYpc2DJdLGPaxVe+Cw7pZM4yYLmS5SMKAKw7qeROwP5XqwPuYn6l
+KKoF35h1dz2XJ9F5EEQ5bIaIvLA9MzyoABeAAntsAB/BDOY93tO5Kb6qR+oJQOJddZCifY1c
+OCJnkRCtX0+Db42TyBiO4pijBrKkkKpyxtnklABalU2vhpxTYx05yfTt6CYbTC8KXXN0wmGK
+K3ep0Lo5N8vGRDTzMGzlS6HXhmmWM6eDSN5lHq5SBwTcsuBcZTrjOc8LSrkvm4YLaTSy6Zp0
+JQqSdgrYH8fyVwX4sWBve2QpUojPblvVGHMc6NJB23edtSF6sfzcheFR2xSNIaTkHDvSHpJo
+0CK3jI33rIAAADs=
+
+--Where_No_Man_Has_Gone_Before
+Content-Type: APPLICATION/X-BE2;version=12
+
+\begindata{text, 269602880}
+\textdsversion{12}
+\template{messages}
+
+Where no man has gone before...
+
+Click on the "death star" icon to start the animation:
+
+\begindata{fad,270222644}
+$N icon12
+$C 30
+$T 30
+$L andy12
+$P 0,0,20000,256
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$A 29,24 -1,76
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$S 216,264
+"Fire!"
+$V 260,260 417,242
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 321,186 250,158
+$V 319,193 265,172
+$V 316,199 290,187
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 321,186 187,122
+$V 319,193 215,141
+$V 316,199 232,155
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,102 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,102
+$V 198,102 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 241,145 179,117
+$V 265,163 179,117
+$V 316,199 180,127
+$V 169,120 163,101
+$V 184,111 188,89
+$V 163,101 172,115
+$V 188,89 190,110
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,108 136,162
+$V 136,162 137,176
+$V 136,162 131,162
+$V 131,162 130,177
+$V 130,177 137,176
+$V 131,162 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,108
+$V 198,108 199,118
+$V 199,118 138,171
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 246,158 180,127
+$V 169,120 163,82
+$V 185,109 204,80
+$V 163,82 172,115
+$V 204,80 190,110
+$V 175,133 173,162
+$V 173,162 181,132
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 198,108 141,158
+$V 141,158 149,161
+$V 141,158 130,154
+$V 130,154 137,171
+$V 137,171 149,161
+$V 130,154 157,133
+$V 162,129 162,129
+$V 162,129 195,96
+$V 195,96 198,108
+$V 198,108 199,118
+$V 199,118 149,161
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 164,61
+$V 185,109 219,66
+$V 164,61 172,115
+$V 219,66 190,110
+$V 177,134 201,167
+$V 173,142 201,167
+$V 188,128 256,143
+$V 256,143 195,120
+$V 175,147 147,192
+$V 147,192 173,142
+$V 168,127 113,103
+$V 113,103 171,136
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 146,98
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 171,109
+$V 171,109 177,130
+$V 171,109 142,135
+$V 142,135 183,120
+$V 183,120 177,130
+$V 142,135 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 132,80
+$V 185,109 195,52
+$V 132,80 175,116
+$V 195,52 190,110
+$V 177,134 192,201
+$V 173,142 192,201
+$V 188,128 241,106
+$V 241,106 195,120
+$V 175,147 234,175
+$V 234,175 173,142
+$V 167,130 113,143
+$V 113,143 171,136
+$V 167,130 160,153
+$V 177,130 210,140
+$V 190,110 208,94
+$V 172,105 172,64
+$V 154,122 120,115
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 142,135
+$V 142,135 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 146,66
+$V 146,66 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 171,109
+$V 171,109 177,130
+$V 171,109 142,135
+$V 142,135 183,120
+$V 183,120 177,130
+$V 142,135 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 146,66
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 139,55
+$V 130,154 175,116
+$V 139,55 190,110
+$V 177,134 219,150
+$V 173,142 219,150
+$V 188,128 193,72
+$V 193,72 195,120
+$V 175,147 226,80
+$V 226,80 173,142
+$V 167,130 144,191
+$V 144,191 171,136
+$V 175,147 173,202
+$V 155,138 130,167
+$V 157,128 121,89
+$V 171,109 165,69
+$V 191,115 205,70
+$V 184,116 247,130
+$V 177,130 191,172
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 156,170
+$V 156,170 177,130
+$V 156,170 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 179,200
+$V 130,154 175,116
+$V 179,200 190,110
+$V 177,134 221,72
+$V 173,142 221,72
+$V 188,128 123,82
+$V 123,82 195,120
+$V 175,147 120,89
+$V 120,89 173,142
+$V 167,130 276,111
+$V 276,111 171,136
+$V 172,131 164,203
+$V 153,136 74,143
+$V 152,114 128,56
+$V 179,113 182,44
+$V 199,118 301,141
+$V 194,137 215,183
+$V 151,150 97,225
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 204,106 216,100
+$V 215,110 221,109
+$V 231,98 236,95
+$V 241,102 241,102
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 214,99 214,99
+$V 215,110 221,109
+$V 242,84 236,95
+$V 246,91 246,91
+$V 221,147 221,147
+$V 207,133 207,133
+$V 168,166 168,166
+$V 150,161 150,161
+$V 175,92 175,92
+$V 192,90 192,90
+$F
+$V 428,132 377,142
+$V 377,142 327,179
+$V 327,179 306,219
+$V 306,219 304,271
+$V 304,271 318,309
+$V 318,309 350,344
+$V 428,132 479,134
+$V 479,134 520,154
+$V 520,154 549,192
+$V 549,192 562,230
+$V 562,230 564,265
+$V 564,265 553,296
+$V 553,296 531,322
+$V 531,322 511,340
+$V 301,290 318,321
+$V 318,321 338,343
+$V 301,290 299,253
+$V 299,253 306,219
+$V 527,162 563,144
+$V 544,181 570,152
+$V 520,154 590,101
+$V 549,192 592,157
+$V 578,109 569,68
+$V 590,101 581,61
+$V 506,90 503,105
+$V 503,105 510,108
+$V 510,108 521,89
+$V 521,89 507,86
+$V 507,86 594,23
+$V 521,89 593,35
+$V 510,108 591,50
+$V 418,224 410,232
+$V 410,232 409,248
+$V 409,248 420,258
+$V 420,258 432,257
+$V 432,257 446,246
+$V 446,246 457,219
+$V 457,219 447,211
+$V 447,211 418,224
+$V 425,233 421,238
+$V 421,238 427,243
+$V 427,243 434,239
+$V 434,239 425,233
+$V 507,147 548,197
+$V 344,235 358,232
+$V 358,232 346,243
+$V 346,243 359,237
+$V 365,248 357,243
+$V 357,243 349,247
+$V 349,247 354,257
+$V 371,259 367,253
+$V 367,253 354,257
+$V 354,257 364,267
+$V 381,274 374,281
+$V 387,277 391,281
+$V 391,281 381,289
+$V 397,284 402,288
+$V 402,288 392,297
+$V 392,297 389,292
+$V 389,292 397,284
+$V 407,291 399,300
+$V 76,56 51,65
+$V 51,65 34,80
+$V 34,80 21,108
+$V 21,108 21,129
+$V 21,129 31,153
+$V 31,153 54,167
+$V 54,167 88,169
+$V 88,169 123,153
+$V 123,153 139,129
+$V 139,129 146,105
+$V 146,105 139,76
+$V 139,76 118,58
+$V 118,58 93,53
+$V 93,53 76,56
+$V 34,157 47,167
+$V 47,167 82,175
+$V 82,175 115,163
+$V 115,163 130,154
+$V 130,154 146,131
+$V 146,131 146,115
+$V 146,115 131,123
+$V 71,102 63,110
+$V 63,110 65,120
+$V 65,120 73,124
+$V 73,124 83,123
+$V 83,123 97,103
+$V 97,103 87,93
+$V 87,93 71,102
+$V 73,109 71,113
+$V 71,113 71,113
+$V 75,115 73,109
+$V 73,109 75,115
+$V 73,109 79,112
+$V 71,113 75,115
+$V 75,115 79,112
+$V 39,105 39,105
+$V 40,111 40,111
+$V 40,111 53,115
+$V 47,118 47,118
+$V 44,123 44,123
+$V 44,123 44,123
+$V 44,123 48,127
+$V 52,130 52,130
+$V 52,130 50,135
+$V 55,134 55,134
+$V 59,136 59,136
+$V 59,141 59,141
+$V 66,142 66,142
+$V 70,141 70,141
+$V 70,141 74,143
+$V 74,143 74,143
+$V 93,40 112,42
+$V 112,42 142,65
+$V 142,65 151,89
+$V 93,40 80,53
+$V 151,89 146,105
+$V 147,109 157,90
+$V 157,90 155,65
+$V 155,65 122,39
+$V 122,39 93,40
+$V 93,40 86,54
+$V 146,105 163,134
+$V 146,119 157,133
+$V 167,130 163,134
+$V 163,134 177,130
+$V 163,134 146,131
+$V 146,131 183,120
+$V 183,120 177,130
+$V 146,131 157,133
+$V 162,129 162,129
+$V 162,129 175,116
+$V 175,116 167,130
+$V 167,130 199,118
+$V 199,118 177,130
+$V 112,42 105,48
+$V 105,48 136,67
+$V 136,67 142,65
+$V 191,115 185,109
+$V 178,104 185,109
+$V 183,125 183,125
+$V 169,120 130,154
+$V 185,109 177,125
+$V 130,154 175,116
+$V 177,125 190,110
+$V 177,134 162,129
+$V 173,142 162,129
+$V 188,128 173,142
+$V 173,142 195,120
+$V 175,147 163,145
+$V 163,145 173,142
+$V 167,130 163,134
+$V 163,134 171,136
+$V 220,75 220,75
+$V 215,110 221,109
+$V 242,84 236,95
+$V 278,93 278,93
+$V 248,151 248,151
+$V 211,172 211,172
+$V 176,196 176,196
+$V 137,186 137,186
+$V 178,70 178,70
+$V 198,64 198,64
+$V 169,100 169,100
+$V 192,97 192,97
+$V 204,137 204,137
+$V 151,150 151,150
+$$
+\enddata{fad,270222644}
+\view{fadview,270222644,2,0,349}
+
+... by Curt Galloway
+\enddata{text,269602880}
+--Where_No_Man_Has_Gone_Before
+Content-Type: application/atomicmail;version="1.12"
+
+;
+;
+;
+;
+; This message contains a ATOMICMAIL program. If you are reading
+; this now, that probably means that your mail reader does not know
+; how to handle ATOMICMAIL programs.
+;
+; If you were reading this with a mailer that had been extended to understand
+; the ATOMICMAIL language, this mail message would automatically interact
+; with you and take certain actions based on your responses. However,
+; the language is designed in such a way that ATOMICMAIL programs can
+; NEVER do you serious harm.
+;
+; If your computer has a ATOMICMAIL interpreter but it has not been linked
+; into your mail system, you can run this program by piping the mail
+; through the ATOMICMAIL interpreter. (In Berkeley mail, for example, you simply type
+; "pipe <message number> atomicmail".) Otherwise, you can simply write the mail
+; out to a file and then type "atomicmail that-file-name".
+;
+; If your computer doesn't have any ATOMICMAIL software at all, you
+; should probably reply to the sender of this message to tell
+; him or her that you were unable to run this program.
+;
+
+(&checkversion 1 12)
+
+(defun init-ctrs ()
+ (progn
+ (setq newline "
+")
+ (setq summarizer "mmsurveyor@thumper.bellcore.com")
+ (setq global-survey-qid-ctr 0)
+ (setq global-nesting-level nil)
+ (setq this-level-ctr 0)
+ (setq total-questions 0))
+)
+
+(defun nextctr ()
+ (progn
+ (setq this-level-ctr (plus this-level-ctr 1))
+ (setq global-survey-qid-ctr (plus global-survey-qid-ctr 1))))
+
+(defun pushnesting (txt)
+ (progn
+ (setq global-nesting-level
+ (cons (list this-level-ctr txt) global-nesting-level))
+ (setq this-level-ctr 0)))
+
+(defun popnesting ()
+ (progn
+ (setq this-level-ctr (caar global-nesting-level))
+ (setq global-nesting-level (cdr global-nesting-level))))
+
+(defunq onelevel (i)
+ (strcat (int-to-str (car i)) (cadr i) "."))
+
+(defun apply (f l)
+ (magiceval (cons f l)))
+
+(defun getstring-oneline (prompt def)
+ (newlines-to-spaces (strip-newline (getstring prompt def))))
+
+(defun getstring-notrailers (prompt def)
+ (strip-newline (getstring prompt def)))
+
+(defun newlines-to-spaces (s)
+ (let* ((l (strdecompose newline s)))
+ (cond
+ ((null l) s)
+ (t (strcat (car l) " " (newlines-to-spaces (car (cdr (cdr l)))))))))
+
+(defun strip-newline (s)
+ (do*((len (strlen s) (- len 1)))
+ ((or (lessp len 1)
+ (not (equal newline (substring s (- len 1) 1))))
+ (substring s 0 len))))
+
+(defun cadr (lis) (car (cdr lis)))
+
+(defun cadar (lis) (car (cdr (car lis))))
+
+(defun caddr (lis) (car (cdr (cdr lis))))
+
+(defun cdddr (lis) (cdr (cdr (cdr lis))))
+
+(defun cadddr (lis) (car (cdr (cdr (cdr lis)))))
+
+(defun cddddr (lis) (cdr (cdr (cdr (cdr lis)))))
+
+(defun caar (lis) (car (car lis)))
+
+(defun cddr (lis) (cdr (cdr lis)))
+
+(defun caddar (lis) (car (cdr (cdr (car lis)))))
+
+(defun > (a b) (and (not (lessp a b)) (not (equal a b))))
+
+(defun mapcar (func args)
+ (cond ((null args) NIL)
+ (T
+ (append
+ (list (magiceval (list func (car args))))
+ (mapcar func (cdr args))))))
+
+(defun thislabel ()
+ (strcat
+ (cond
+ ((null global-nesting-level) "")
+ (t (apply (quote strcat) (mapcar (quote onelevel) (revlist global-nesting-level)))))
+ (int-to-str this-level-ctr)))
+
+(defun revlist (l) ; like common lisp REVERSE
+ (cond ((null l) nil)
+ (t (append (revlist (cdr l)) (list (car l))))))
+
+(defun informative (p)
+ (strcat
+ "#"
+ (int-to-str global-survey-qid-ctr)
+ " (of at most "
+ (int-to-str total-questions)
+ "): "
+ p))
+
+(defun survey-multiple-choice (prompt choices)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (car
+ (car
+ (select (cons (list "" (informative prompt) NIL NIL) (cons (list "" "" NIL NIL) choices)))))
+ newline)))
+
+; USAGE:
+;(SURVEY-BRANCH "What is your favorite color?"
+; (quote (
+; ("red" "red" (branch-question-set "red"
+; (quote ((SURVEY-SHORT-ANSWER "Why do you like red?")))))
+; ("green" "green" (branch-question-set "green"
+; (quote ((SURVEY-BOOLEAN-ANSWER "Are you green with envy?"))))))))
+
+(defun survey-branch (prompt choices)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (let* ((ans
+ (select (cons (list "" (informative prompt) NIL NIL)
+ (cons (list "" "" NIL NIL) choices)))))
+ (strcat
+ (caar ans)
+ newline
+ (cadar ans))))))
+
+(defun branch-question-set (branch set)
+ (progn
+ (pushnesting (strcat "/" branch))
+ (let* ((ans (ask-question-set set)))
+ (progn
+ (popnesting)
+ ans))))
+
+; USAGE: (survey-short-answer "How are you? ")
+
+(defun survey-short-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (getstring (informative prompt) "")
+ newline)))
+
+; USAGE: (survey-integer-answer "How old are you? ")
+
+(defun survey-integer-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (int-to-str (getinteger (informative prompt)))
+ newline)))
+
+; USAGE: (survey-boolean-answer "Do you think I am sexy? ")
+
+(defun survey-boolean-answer (prompt)
+ (progn
+ (nextctr)
+ (strcat
+ (thislabel)
+ " ("
+ prompt
+ "): "
+ (cond ((getboolean (informative prompt)) "Yes")
+ (T "No"))
+ newline)))
+
+(defunq surv-pkg2 (q)
+ (progn
+ (nextctr)
+ (list (strcat (thislabel)
+ " ("
+ (car q)
+ "): ")
+ (informative (car q)) "" (car (cdr q)))))
+
+(defun formatfillinlist (lis)
+ (cond
+ ((null lis) "")
+ (T (strcat
+ (car (car lis))
+ (cond
+ ((equal (cadar lis) t) "Yes")
+ ((equal (cadar lis) nil) "No")
+ (t (sexp-to-str (car (cdr (car lis))))))
+ newline
+ (formatfillinlist (cdr lis))))))
+
+(defun survey-complex-form (preface qlist)
+ (progn
+ (setq this-level-ctr (+ 1 this-level-ctr))
+ (pushnesting "")
+ (let* ((ans
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar (quote surv-pkg2) qlist))))))
+ (progn
+ (popnesting)
+ ans))))
+
+(defun ask-question-set (qlist)
+ (cond
+ ((null qlist) "")
+ (T (strcat
+ (magiceval (car qlist))
+ (ask-question-set (cdr qlist))))))
+
+(defun qcount (l)
+ (cond
+ ((null l) 0)
+ ((equal (quote survey-branch) (caar l))
+ (plus (branchcount (caddar l) 0)
+ (qcount (cdr l))))
+ ((equal (quote survey-complex-form) (car (car l)))
+ (plus (dcount (magiceval (car (cdr (cdr (car l))))))
+ (qcount (cdr l))))
+ (T (plus 1 (qcount (cdr l))))))
+
+(defun branchcount (l prevmax)
+ (cond
+ ((null l) prevmax)
+ (t (let* ((this (plus 1 (qcount (magiceval (caddr (caddar (magiceval l))))))))
+ (cond
+ ((> this prevmax) this)
+ (t prevmax))))))
+
+(defun dcount (l)
+ (cond
+ ((null l) 0)
+ (T (plus 1 (dcount (cdr l))))))
+
+(defun handle-survey (to summarize subject id qlist)
+ (progn
+ (init-ctrs)
+ (setq total-questions (qcount qlist))
+ (sendmessage
+ (cond
+ (summarize (strcat "\"" to "\" <" summarizer ">"))
+ (t to))
+ nil
+ subject
+ (strcat
+ (cond
+ (summarize (strcat id newline to newline))
+ (t ""))
+ (ask-question-set qlist))
+ NIL
+ 0
+ T)))
+
+(defun maybe-displaytext (tx)
+ (cond
+ ((equal tx NIL) NIL)
+ ((equal tx "") NIL)
+ (T (displaytext tx))))
+
+; THIS IS THE END OF BOILERPLATE CODE FOR THE RECIPIENTS
+
+; user-generated part begins here
+
+(maybe-displaytext
+ "")
+(handle-survey "nsb@greenbush.bellcore.com" T "RSVP NOW!" "nsb.greenbush.bellcore.com.1991.8.17.15.18.4" (quote((SURVEY-BRANCH
+ "So, can you come to the party?"
+ (QUOTE
+ (("Yes, I can come"
+ "Yes, I can come"
+ (BRANCH-QUESTION-SET
+ "Yes, I can come"
+ (QUOTE
+ ((SURVEY-INTEGER-ANSWER
+ "That's great! How many of you do you think will be coming (including yourself)?")
+ (SURVEY-SHORT-ANSWER
+ "What kind of *vegetarian* food would you like to bring, if you have any idea?")
+ )
+ )
+ )
+ )
+ ("No, I can't come."
+ "No, I can't come."
+ (BRANCH-QUESTION-SET
+ "No, I can't come."
+ (QUOTE
+ ((SURVEY-MULTIPLE-CHOICE
+ "Aw, that's too bad. Why not?"
+ (QUOTE
+ ("I'm busy that day."
+ "I hate Star Trek."
+ "I hate you."
+ "None of the above.")
+ )
+ )
+ )
+ )
+ )
+ )
+ ("I really don't know."
+ "I really don't know."
+ (BRANCH-QUESTION-SET
+ "I really don't know."
+ (QUOTE
+ ((SURVEY-BOOLEAN-ANSWER
+ "Well, please don't forget to RSVP when you decide, OK?")
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ ))
+
+
+--Where_No_Man_Has_Gone_Before--
+--Outermost_Trek
+Content-type: audio/x-sun
+Content-transfer-encoding: base64
+Content-Description: Distress calls
+
+LnNuZAAAACAAAIguAAAAAQAAH0AAAAABAAAAAAAAAAD///////93////////////////////
+//////f/9/////////f39/f37+/37///9/fr6+vr5+/r5+Pn4+fv6+/r6+ff2dnT1dXZ29vd
+5+fj6+//9//v///3//dv/2dnXV1dXV1fXVtVVVNVTk5MTU5PTU5XWV1bXV9r//f3/+////f/
+6+///3dfa/93b/d3d/f/5+fr49nZ3d3X3+Pn/29n///3//////f/d2dZUVFPVVVZX1lXWVtX
+X11bW1trX2fv49/b19HO0dHNzMzLx8bGwcbDzc3R193X49nf59vn52/r48/T3c/d119jZ9t3
+79/nz99ZT1/jY29X/+tdRm//Z0lLT01PSkNGW0o9SkRrS05FV0A9Q0tVWUlK3/9fV87Zx8/N
+2bzd68zBwMv3y8Ldzvf3xsrPyd3HyV3VzF3NTtf3zN/jSt93Tmtf71XrZ11VS+s/b3dLTmfn
+91tG3U1N511na+NLym9v19PKwsvZ29P//2PZxs5f78RLQG//TdlTuc9O6//nd13KY1v/2VtC
+vUHNO8VKv0dbw0b/SM9T11NIzlvZWb/n1VvK1WNXuv9FvzlPvzrB2UHHXUvEQ/fZRtlPytXO
+RWf3P0pZUdvTd8Q9RslGSrg378Q+vb7b91/A1V3/1VvFUUTAW0W+3z+4TU7AzUPPU+93/9s/
+T8K7PEZHRd+4vm/AWUc80znrumvMSWf3vknGvufZtkO5/0fLwj27zz/Asz/C20DV/0FLxS+5
+RkLC20jPwz2//0e8x0LCd0jNzzvT6+vLQUPd1zjjuFc/uD85wU8868A3Y74927s/91ldSFfd
+Se9fb3fd1z/f39dI08pHd85AWb3jQcHTT8fOPcnCT0XDz0xj2dFfZ9/F199ryuNfZ0TP/0x3
+PFnr/0lH2/fR0c/I3//jwFfX1ePf0//Z0cbLa8fbvGdjyGtX3+fdd2Pva9ffTufjTu/nY1nn
+1Vdr3UvT31Pd/0ld12d391dn61tKb+NPTu9MROtPVUVrQ0lVS0g/b0JI71VbSvdR2U9Z4+/L
+22PRyG//3dXfyszM68/Rxu/r293rZ+PLa9tfSOvK12f/Wd3j787bW+9r7//f5/9R60/TydfT
+zNfd3crJ2evr387H22Pv2W9X79Vnb2NTZ1dnV91K9+v/513X92drY2d3//9r42P////jb09d
+UVdXWVtNTFVMQU5NS0ZRSE5KTUxZV1X373dv39vR1+vf09fP49vf29vvzuNZb2fv2ev3b2ff
+5/fR21dn293R63fn59fj619VZ29r487X293Lw9f3Y/dRWXfZ72PKxMPBv8bJxcTLb2d3X1Hr
+W1tZ39/d9+ffVWtrU01fY1VP2d/Z1d/v901VSU9XTUk1ODQ8NTxKPDE1QUdCU9/TV2PJ4+tX
+V04/StvRz8e3srCvra20uLm731dFPzQ5Oz05PUzjzMXDubq3ubrEyc7rTEdPTzxGS0pD4+9j
+P/fTyDVP97hLVf+30W/PrLPO17e4XffP9z9Cb/88SF3I/0zR1dk/T189MTY3LCcrLjIvOE9n
+Y8m0rrOura2urq2zsbO2wcK/z993W008Oz04LzU5MjE7QD85VfdfUVPT311r0crj68vG0Xdn
+519RQkY8Pjw7ODxJPD9F/99398Syvd22q6y1sqywr6+vsbe1wbvCvdPNy7vJ0ci/v9vLzMXr
+21tTS0M8OTcxLC0xLSkqMi4rLTUyMDA5PTY3Pk0/Rkj/X3ffy8TBxL23vLy5t7u+v7y8vr2/
+v73AxL++vLy9vr2/vsO9wcfGxcfIy8jd53dVSkI+PDg3NDg1NjQ4ODw/Q0lPTl9n43fd2dXV
+08vV09/j/2tbT0dBRD06OT09PT9ARk1VZ+PZ68vGvr+7uri3tbKwra6tra2ur7O2tr7Fx9Pn
+509PTkVAPDw5NzUzOTc3NTg4OTw6PTw8O0I9Pz49Q0Q/P0NLQU5RRP9Xd1XHX87JzcS+vsC/
+uLu9trG0srGvr662rLuytLXLvsPZzyrGrT8zw18zY808X3dVLz9IKD13PjhJPTQ9RzI9SjU4
+PkQ3Rk4/RFFXY1P/93fd58TRv8zExMXBwe+9xb7Iw8nCvMzCucO5vMq7xNu/z+P3a2dFY0tj
+Pd82QjpNOlVJQldDTznJP9tBwEjB53fO1TzfvFnVxV9Ny+NVvtFPu8pN379J/0PLTUX3PFN3
+QTzNNE85XUs/Z0k859M+X8lbS8zTScfOv8m1zc68vsC6yr24v/+wU1O00V27XbzVstHO2cw4
+we/n17bP1/drWU5HPjs5LCxBMS01Pzw9Qk1Tzl9jw9VX/9dvQ1k+QFNKQd3M6+O6vMPHv8Vv
+2+vP913NWdfP38zVwre9uK6zr62vsre6vHfOPEdHIyY0LyMlRSovPm80QmtOP9HI69HFzffJ
+Z0r/RD45Tj5DSudb0dW8vsqusNdIb+M3L0vfQefBsK+wra2tra2tra2tra2yvNPMyF85RM8t
+OjM0Ki4qLyg9NSwmO1NfLTnN3TxB09P3TUxIW0Y4MkJANjU/59lryrLvvq2rb0+9zC84X29j
+17OvrKysrKysrKysrK23tbiyxGfCw9dDT0o1NDcuLio+KicvNzMtNFc3Ql9LPTxP4zk2UedH
+TVNdP0hnWTxB69dNTdPIw8XEub++w2NPV8fO68q0sravra2tra2tra2tra+vtb7Ly9VfPz81
+MjA1Ni4sLi8yMDQ4Nj5DO0JKV2dKTllNWV1OSUdOUVNbb+tnXVFPT0dMTkxZXV3/38jBwry5
+srW1s7G0sbGyt7m5vMK/x8jJ09HT3dXV1ePd09Xj3+P/W1lRT0dFPzs2NzUyMDAzMjA0Nzg4
+Oj4+P05VY/fT083Iwb/AwL2+vsHBw8PBwsDCx8PHy9HT0+f319nT08vLy8bCwL6+u7+/v7/I
+zc3T629bV1NKRz9KPkBDPkFHRklVT0xGRT8+PEA9OzhHMj03PTo0RjxGPltbd83nweu+wru8
+ura5u8W7tLzFzLvj09PG/13n/+Ndd19jRUpdX1FO69PbycW3vr+4srq5urW/xsjC1W9nW0dD
+PkM5PT07OT09PERGRU1XSExZa01FY01JQkg8Nzk3OTQ8OkM96/d30726vLKtsrWsrrOzsb2+
+wcvbW/dDTUdJPU5BPj5VRU3/5+/ZxrvNvcW8x8rBx9nV3/dPTl9RSl1jb2P3b2dRTkg+Pzs3
+NDI0Mjw6Qj9bU3fj2cnIu8DFvbq6yL++xcXIx9fd2edv53fK11/v481n1cPO1dvTzvf351lX
+TWNVU1lbX1dX//9bb2POV2tv70lGR1E/UVNMQE9XZ1VfX2dj1+vT2c/PydO/77/Cv9l3X99f
+90pITkE6TVU9Rv/rUW+/uL65scS2trrBzsW4d/9vTmdIS0YvLy83LS8vNTtMTkNCyL3Hyb21
+sryztbSwtrXB08G6xb531edjb0w7RkJJOT9JSFtf50TNd07PUWc67706NddPTzxrTj8vb+dL
+Kziw5zA70cHbxbG+w62070jdwDgsQf9DRO++ycC0yf/d/0UxNUs8OFPjwL6zrVWts2M6189E
+Lt/ITreusa6trcbGutE9U0hIRUNdb8rjZ1fZZ0k5TVM4P904NcnfMD3FW0xOSE08QzI9UzYt
+W0gvOUbOPz/Kz0jrxc5OyLa857u1wdPAt8e8s7S2sK+5vrm3yuPM0U/Xs87Rua/A39XXPjg6
+MTM4NS87QjYvODYuKyoqKy4qKDhIOztvv93Au7eyvrnC18K6w7vItK23ta2trrvFrK5X77a4
+xWvBsb/Cys3C0UE+Nzk3Ki05Oz85StXfWU1ISzouKy0vLCoxPD9FXV3Prb43yrVPRWvfxufJ
+us6svbyssd+z0dtfPUM9St1bTbPEz6+7ubXKuszDz9nGz9XOyMXGb9fZ/9s7R2M/MDU1MD0z
+NUM+TElFY1NHRkZJRkdX/9fM68bGv8XLv8vIx8nHxMzOwcHJxsHGz8vO99/M33fT/2/fZ1Nb
+VVFKR0tDPUU4ODs4MzU7OTlCRUddXWvV2dHKx8K/wry8x768ysfCz9HV2+/v929VU1tIS1dN
+SFfvZ3fr/+/d71Vn/29VW93j3c7MysfJyM3O0Wt3b1lOTE5VU11Z/+f/d9nX593b19HTyc7J
+vcHKv8bO29V3VV1MUVtXV3dd79fvz87O0cbGy8PHzMrH39H3b2NHSEpAPDs8PDk5OTg4OTU3
+Ojg7PD4+QUNGS0pVW2//49vNz9XO3f/V4//R3dXJxs2/wMjHs+PftrzHxMrFur3Rxr+9xMG8
+y8O5wcXGxcXGxcfdwLpT57lLLbRPLDxdNS42PTAtMjY5LjE7OTQ6SEFASN/3Z9PJzcnIxsbI
+xb/Hysi9w8y/v762tbm4ur7Bu8rKyNHbb+/dT09rP0VFOT9MMzpCPj5GQkZNY0BHZ0RHXURA
+WUtIT01BU1U9SGNFWU53V07b1ePGvr26ub+3u7y1vbi4vbvBwb/EwL/O68rTz8/jzdHMzt/r
+1f/n509ITk89OTo5OTg9Ozw/QkNHQU5VRj5MRkdXa1/nzdnPvtFj28jI7//FzP/Tu85MzLvE
+X8LDXcq+x1vBvM9DQEdL39HX08rA011DLy8wLCgqLTVVy7u6sa2xtcs+NDUvLTE3RuO0try2
+s7W1vcfMyclv58nJxLi4sMK+r7vbyt1n1+Nfd0dbTDlVSU1jZ8vN1cb/TONdPzg6VUXfzz0x
+SUpPXVtTR1dbOy4uKi42OURTxK6tr7G8x8zvTklPa8zAxc3vwbe8vcDIwr7A3V1PY93vS0/H
+v723ubnTX81HKSs0Ly9XRUHdt89LRFVOVU9RR1NNRExLU+fHwl2+v1X/zk5KTm9r2b61vLy5
+1WtbX+v3v73Dv87ZyL3Dxec4LC0817yvsMPRPzIxNDxBW/9ry7S0srfHY1NKOjpEPVnvY19I
+PzpERUtANjzZSlFvPDZjR0fL37/HX8HjXbG9vLPVssHTuMjfucnEtruvusjZPiwuLDhJY9HP
+U01XvLu7wFEpJCw0za+4vsLDuLXPNCMlMMSsq6u7d0E8QEpd99XJys9dUUk/TlVOU1s4PUpF
+R0xVUUxXd8Kzy81Xz05nrK+6u0tLsbu2rMBPPTIuPWdX2Vk5RV1vQTcyLS1N60RLPjA6P0rn
+yetZy7q4ra20w626xLm/vrmura2trb1fRT9N2c/P1f9XSUE5MzU4PlVvT1E/Ni8vNjtGRj80
+M0FGSltrQUtLTFdI5+fLwV/v28C1trCzwb6ztb28ur3BubW4srW9z2dfd9Pj28LBv7rH2WNH
+1cbbvcNMLCYpKDK4s7vCNiQiIis4Z9FrXUg1OTs4Pz9RUT1JOzxASGdv72NEQ9s8Wb68/7ay
+4721vMStrbG3rbi8r7W9sK+yu8DjWcC/xb6/72/Nw9Hv199OSj0/PUJOVf9nM0z/Q0/VTDs4
+QDYsL0lNY9X3Vzs8SDY1OT1Pb+9JNkhjb9drQzc5Sv/369X/X+PDvte+ts7Ps7rEvdXXta+t
+s7/I38m0t+NvT0/ny8bO38jLwbrL1+NNV29jV1tFPVH3Tj83Ly44RE7Xt7rTWzYvN1XCvbm0
+xcvXW05GQFFPSl1ZST86PD9LVUlBQVXj2+NnQUBO68DZY11f18rN4+fn3+fF02/Gvr7R/8zP
+w7y80cXCyb/HwcPAvc1n/3fnx9XT09fBa0ZBQv++uNFJW8hjPz02Lj1RWz8/a8VfOj0yKC4+
+MyY2yNNCR2/jY2/ra2/Nwr/f28O2tMTHvrCxrrfPtbW+u7O4yMy8vtnEvMTIxMdvR05VRklN
+Sjo6PkEzMTs+Ny8vOzYrKiwuLTM2Ny86Sk89PUvRye//ysW/ubq+u7Ovr66xta6tra6wr62v
+tby9uLO3vb3By9Pb605fb2ddY1lKS1lfSUdKRU1IST1AOz4/OjYyNDo4Ojk7QEU6QEQ/S0g+
+Q0RJSkxVV09O511NW1dbb1vO2c3Ky7/Dwbq/wcDKwr2/vb62vcC2v7vFwry/vb3Rv8Xnv933
+2f/va/9vT2dZSltITl1CXT9ETD1DQD5OPUJNPkJBNk84QE09QVlLR3dIXXdny2v/y9VvxdXn
+wcrHxcbMvsbIt8nDu8XHw8zryt3d79/ZV8tbWWNTU29NX1dOb1v3Z13bX+Pn3e/O18fL2cXj
+b8jM/9PPXczVWd/vV2vnUVFXT0FTVUBPWUdNSEVCRD08PDk+PTo9Pzg6Pjs6Pz5ATk5ATFFN
+UWdba9vb3dHJx8K/vru3ubWzsbGurq2tra2vr66vr7Cvs7e4u72+w8jM1d3jd1tPS0ZBQT8/
+Pj46NzU0NDIwMDAvLzEuLzEyNTQ4OTk7Pj1AP01TV2Nn99/f19HOy8TFxMfFxsDAwcC9v76+
+v7+/xMC7vLy+vr3DvsbFx8/TzdPd3dn3b2NvXWNTTlFIR0REQ0Q/QUNJS0pdWVtnb+/n///f
+d+/r39nn2+vv92dnXVFXWU9XV29dW2dnb+dr99nV29fZ2ePv/29vY//vb//Z793b0dHP1c/T
+zdPZ21Xd1SqtrDwjrqwvLa2sNDK4uEkx77rRMWe8VzDTuj4xybtGPN3PY0vn0d93z8jV583H
+62fV3VFMS8NdPMjdRtFNz+vrSdm060qxzWutUVWtwziurS6+rHdHsVM/tl9Eb7o+vLsxvb5I
+ML3rQD9v7z5H0T8uP7czNEXXPjw5XVEtR10tR+9VPTXv9znZdzeuWzbOu293xe/ZvMHPvky/
+rbxF562t6zStr0i+rsbOta3vzbews+tJurTJyELOtF0409dE71tALU68NShCuzUqMuM9Lz0+
+LTZdQyksRWcyKS/X5ysv004ySt06Q8q2Vz7DrsFD3bmv3cy3t823ssNfwK22Z8W0r9v3vLdr
+0bvFW8i8vFd3urnZUb2832PN2Vdbzuc8Qs3jPDVG31kuQu9HOktXNTx3WT49T9NZNFVvTmdv
+VffO0+9vzsTL3dHCx8nNwL3GxbrFzr6/083HwMpva8lvV2NNTGNXPj5LV05CTmfvSz5Db0s4
+OktCOjg8Ozg+STc8X99ZR1fCx0nPssvZvLvA472vv/+/ucbrb9nDV+vFzcm8u7i6trK4yMHB
+z0s/Tk43O0I3RXdvW2/Kw8jd28/ja1s9OD09PTUuQD80OV9LTkv3yf9nytffy8bGxsu/vs7N
+wr/Dxu/f0dlrWdfJ71Nj9/9MR1VFNzxLRTE4TVc8P2/PUWPI0WPvxsf3Y7i51/e/usTVxL/H
+xbm7v7+/vMnOwcXj1d3r70xVZ01XUU1ZU0FPSzo8T0k2NUtvQzRATEk+TW9MRVtdTElPV9vP
+62v3u77ZybzFzsPDvri5t7S+xbi568e93UVPvtk6SsDrT0TMu01CyE4vNk8/MThjVTo9X9tA
+PURPNzRESTo/WWdHP0rXTkFFystMa8fXT83H11/Zsr7XurG5xLe1vt+8tr/BtLrVwbeyv9u6
+tttZxtlTQd13OD1jTTE3SkgtL0k4MThHPj0/21VI3brX/8fAxN3Xw0k4Tu9KSv+4u99OU2NK
+My9LTUVd/1n/287d3b28zc7j/9tVP+fFw2fNu79vx7rAwsvFxMzjwc3vyL/C09vNzNPbXWfv
+Q0dCPD8/Q0ZRU+9VRGvv51VV9+9FS1dHSVX302NX9z01PEU8PkJZ51lDY1tKTd/b487d6+/N
+x77HvLi5vr++v7y9z8nEvMfByb7FzczL111KU2NDV9lKTOfvSE9NTv/nY01Z23dV3+fvxMhn
+Z0RG3WNHX1fja0RGV0k6S09XQz7JWz5T/0VJRj9ZSV3RX0pbb3dTSs9348HOY8O/vMVTy7bT
+v8PXvrvJ6/fZv8zn09W0tsbNus/GxmdVY+vvQkdvdzs2OTw3Nzs8Nz4/NjZTP0FnSmvnV9Pb
+0+tn28ZKW8fna+fPzk//wNfj/9m9vsrKub/Gu7q8vL23uOPIvsfj48bBzN/L1efMTErI1UhT
+710/PU1OQVlrTzs4PkQvPFFAPUJBQ0A0QFdB//9rRllv1VFNX9XM1c3Nwc7GxtXZysnv67y5
+yFfO393O009vy/9v5+vn3dNNSdPHTU/300o+Y29NZ8znVc7L61PR22fd49vJt8zj78G9yc25
+vdHIvs1rY89v486/10rn60xJ3e9JTk1MOzlAOjw2PFFbOD1AQk02Nj5CVWddV1lbZ2NP92/G
+zePGvbzK1cvK2efHyMXLw8rbyr/Nd+/nyHd323fT2993/3fb1UxZb9tPT1vT3Vdrb1dT9+dr
+X2vTTVPT90ZJR0VTR1NMX9fvV1VZSUpvd1//z8PXTOvN79vN0cLA6+//Y29na2vLxMfV09v/
+d/fPzMnV0ePr319KV93P91vb7/dnTGNjd+/f58rG119T705dV0lN30o9SllMRk5ORT1n60Vd
+9106S19bSVvV09PT23fPx8rC3c3Ly9fJb+u/29vHxs7My/9Nb9dfa2fd18rZ/2tv3XfXX2/Z
+TmfnV1ffV/dvS1lVT05ARW9NZ29NzNlOSlFd301HW1lb/8//WdnVa1tr92d3/99j/8jfb99d
+/83P38/Z093/0ef319Pfy8Z368/nWUdNd2f/6+dda/f/909Pd9/ja+Pn19POb+ffa+tXX3ff
+a//X61Fjd2tXSktbWU53d1VZ987b49X3/83Za3fVwb7vZ+fd591ja+Pn629dP11dT0n3d0pO
+Z2tKb813W+trb9Pf/+Pd12/j31frzNX/z9HbY13r4/dTWfdvTm9ZSFld2/dbXXdbUWdd/1XL
+xltX1+9v79fH0d/O/9ffb9X/SnfdXev/28//6+fn2+/rz29f1c13/+9r39Hv2eP/42fnWVNZ
+Y2NZW1vr1d3/b99nWf9d99HLysXK1efV31Xj12tX2d3/Z2dXSE5ZTE1FR1FXTU9HTElTY0hG
+Ue9bWU5dd193/9nf19Hnb/fv99HZ79PV2dXXb+/j58nL08rV18PH08rG173JzsnO28vCxcvZ
+ztfvT2df7+tMSEJHW1NLSUxZVU9JRUtVTU5RX2P/VUdf31tf71tbb29VWVFMd1FKS11bSEtZ
+X1djU1Pn/1tr52f309n30e/v0dfryc/VzsnH093P2dnMy87OysHHwb7Cvb6/ysTGzMvd59/d
+3dn/X91fa1tVU1dvZ3ddT1dRT0JAP0tPRj9ETVdOTUpISltTSEVET1dbU19XSl9ZSkhFTUlb
+WVn36+fTze/XxM7b18rIwr/GwcK/vrvEz8nNy2//W1VjV19ja1tTW1Nfa+//b/f3//9vb19V
+Y9vf3//v59nj49Xf28/P4+vX3d/fa/f359nj/1tjX1tbUVf349nr19fV19X///9db2tXVU9H
+P0E+RUVKS0lMUU5dTEj3d3fr3etrW2/nXVdd29nja+/n5+vn38932dPXzs7N287f1dvPyNHM
+09Xr92dj4+fv4/d39193XVtdX2/f/19v5+drZ/9v599j9+f/z8zOx8rDycvOx8nLyMnHytPr
+5+tnVV9jWVNZW09KQT9FRzw+Pj5ERUpOTVlLUV9JQ0pNSUlLTVVbTlln//93Y19ZY+ddZ+/b
+y9XV08rH19/b59XKyMLEzsXKy8jNzNfn193R5+vRy8fEw8TDwcbDxNHj393Vzszb/29ZV2Nd
+UVlNVU9TVU1NSUtKSkVIRkRHR1dbXVVOR0tfSUhNPkJRU01NU19rd/fv/1dv3dfOyMjLy8vf
+293j59XR09vf2d3Z083OyM3PxM3My87Jzs7Nz9nf9+/j0etja2NjZ11ZUVdJT0pVU0pOSERD
+P0NHSUZCSUpNSUZLS1drZ+vn783MzdPPzszN0dPJ0+fO3W/r09fV3+fPyM7b2/fV1dVv19PP
+zutZV2d3Z2tra2drV1tfW1lTUWNv39ndTln3W1lXSVFjY93Z7//FyM7n9+/V52N3a+vRzMvf
+29XV1eN32dXP2dP349/j43dTXV9bQ05GQ0tMV09RT11bR0JMTkxbT05MW/fb2/dv5+fO29/d
+zM3PzM/R1+dj39Hj1+dbV3dfWVVn69/r52ddT13X0+Nn59vZztXrX9vR291v3+9v09Hfa1/v
+42dV2ePfY/9ZW+PR0+tb3dfn499jV+vvb3dnd+N3b9fZ71VET09da1lOUUtXT0xNSFtbX1lb
+Y2f/X//NwcjMz2/fybzI0dXPz8rjVV1r485PR0tVXVtJRU9jZ+t3W+/n411jb+vZ1+NrZ9vO
+011MTmfr2c/Tzcvb32/j3+fV0cnFzdlv41dX91tKWWv/409JU1t33+tNT0RGUVtX/+tKSERH
+SVtRWV9La89fU8/H1dvX39/rxLm958C6v7W90f/RwsLfW9nNydH352/DzF1nSlXTa0JNSERH
+Pjw2O/9XSDtO29NTPEFHV93/V0Nb91NbS0JRXcfCy8Xn38rv577N09fPzc7LZ93Rx8HKvb7L
+39nn18bK/+dvd9/N3VdRWUhDUTs4d0xNu1syMjBVzlVJWd/GwNk/MjxOwbxMS0/T07rHb7C/
+69lVPtGuvciwwk6+9z5ESOPIXzRRvv9vSzw+47+/zltFx81bw9lJT79RMT9fRmtTU9VZMdfb
+PES5az/F51m7vM7byOvGvVXjv2fXzTj32UfdTUU/Rt9DOV09Y+NMvj882///Oj3HVdfZS01b
+wbnjd8RbvbzB00mssuuvT0+us7/bQsy7vF3bz0xNtmMxyL430esy68frWWc9PlVTMVfKK10/
+MEhrNjLvNTjLSzV3wDdXzWdjwlvGMa1bvuM/vt/IzetXvs3HwkC32V3V1z9X177LyddMd99j
+ylNrPMnd7/9rZ1XIxUlrY7tbv+c8tcvPY9nJT8a8PUi5U8q9XW//zuPHPkxTvEo4uTk40U9N
+P0Q7W7cyM2ddSf9vMzm6z0tOQC21sDxjyUjHusw8Ubi3ur03Nq3DUUhO3bC6ySVA9+vF5z7J
+vkz/zT5fW7xVRMNIPcxrOsDE3UxZRk9VV0pb10hKZ2vXQ1PIyLi8PFu3x3exzFe7xL7GycFT
+y7Tdzbo3QKzNML3MMj9KZ76vvzNn5yMnQC48trzfvr5NTOf/yr5EO85fz77BXVu0zVFLJz6v
+3y7T22vKti8luq1ET7Ur1a33Kz63rTw+RStLvNs3R99fd71FObGuW7/rMretvD08vbdd6+/P
+r8ROYzpOYz5RvrisrLO5999fNEzvRMEyR7g7X73bLy1A2yN3rc05P7W2NyoyNrOuv8vZtbLN
+Ozg+b9dvPjr/tb/rZ8XKyb7GRU7Nxzorx8ZRQVlCOkjJ4ztX389rSC5RtLfDvb69xOfOx8zF
+LjzDubtjTz4qLT9VPV2urbmvu1Pjz++8dz7f0b671W/Mw7rZNzc0NDs3Ki1HZ99jPEVdTEdd
+Rl29s73PTV24trrHx7y0sdfVV1PL2UosOuPJzkQrO7RXOTZLuqyuua+4tbfDyLzZ98XNyMvI
+41tBOjkzNC4zMCstMjg3OT5DSkI6ODhHwL7C51drzdPIvsW0wri7wLq8zv9fSbq/07ndUXdj
+WbO3y7W6ta+str7BXdfXX+fd2//VO09fSVc9LS82Li0uKzA2LiosLzc+S1XrzevAy8e7ybe3
+u766trOxw8PN49nVWV9rUe9ORknf3VfPV0/Pyc+2ub6zwbm/v8e7vtfZUVNHTU8zRTo1Pj47
+N0A1MSswOjQxLjE4QVVn3by2rqysrKysrKysrKyur6++xc/jTF08MTEpKiktLygsMi89P1c/
+19HIvbyxs7mvvb/Husu/zedfUV9ITUVOQkk+Qz85Pj41RUVGREM/TWPdwb+2rq6tra2tra2t
+rbKyu73DyF9PQTg5LCwrLCwtKiowLTQ7PkZbW9nFu7itra2trbK0sbjBzP9JPT43Oj0/PDY+
+OUBDRUA6PTQ5NTE+PDw+QUlvwrmtq6ysraysrKysrKyytriysrS/70w/ODYuLSwnJCQmLDE2
+QEDdXfe/uq+trrevsLOvuLC7w8/dTz5CNTIuKiYnKCkrKiovMTM9U2PLxcG6uK6tra2tubm0
+uLOwtLa4vbq3uLW2ur7Fvbe5wru+w8xbY1P3V000MCwrKikoIyMjIyQrMD1NTmfZxLWurK2s
+ubm5uLOzv8PO1ePN29PvVzkxLi8xMjIuMjU9Rv/bxry2raysrK2tra2trrG2ur7R51tFPjkv
+KCkpKCYnKycqLTY+TN3r07uvuLKtrq2tsLCurbG0ucHDd01KQDYyLiwzLi81NzU6P0JPU0pR
+V11d493PxsfGybyxtrq/w8PHzMzO39fEa7+0v1nn1cnNwL3AxcO9td9vd9fPXU9C3W9DNDA0
+KyMjKDI3OFHFwrm7sa++y8a/tr7CvLKusK+ytsfbTDstIyIiIiIiIycvOm/Py9vDtq2trq+t
+ra+xubu/tri5zOvN50Y7KigvLCwxODpE58Wxuresq7K8u864xN22s7y3vc+1vPdJPC8sKCQu
+PjA6TkVCQ0rrTTMvLjsvOUtnzri1sbbAvLHC513fwN3/ydfBvrq3xru2v7u6tcHfX1dVWUJL
+RTZAY/80MkdINS4jKCowLCpCZ7vBrbaurbS3t77rw8O7vrmzra+ttb66ylc9LiojKSclKy0q
+Nkb/vK+sq6usrK6wwN/J70VRQe/r/1NrTGc3OSszNikkJC42PTlRwK6svq+svqyuvsy/u7TI
+0bTfu8JJTFFLYzguOCwzOCg6SDJAP0bb693FyK24u768s7PE0d3Xz/fFuLjVtLnHrbi/urS6
+28/Ryf/rTEvCzDk3LzM7KSMvYyszSSw/40Qv98pbUTf3WdPKTcKzu7OyvbW9x7/nzUdKPuc4
+MWPrX1m9ucF3wj9M3SlBOydFxVPjZ7z/xq3VvaznS61DzUGxRi6tuzS5X8RZ374o3UEryy8z
+xlPARdnFStG0M7XLO8JfQLjvX7o/z7stu0g1vzs33zRBWyu9NbhHT641rkwvrkd3tC6xwePf
+sz9vrCesKzmsM7yxNqw2Ub++PMvKNawurj/FSLY0tOc51U02v0I81TXJQ+suvi7B0zPbYy++
+Tz+tONW9QNe6Mm+xL61nN60sb602tK4+tWdBrS+/xi3MySyuNmc/QD3jN7Uqryxf4zW9TTuv
+LcrBP607xa4yrev/rz66zkfj2z/EyU3Ru8rT68zKRMPOSc///z/rW0xKOcs6W+s7V9NOZ8Y2
+a9E9b0VPV13JyUy/U/fTxsi5PK09u8PJua1dsNXKvOfCPve/Mbc7/1n3Sdk21180xTczvyxn
+1yq/My/ZMdVKM8MyN78tV1szxDJbuS2/VT3dw8ay77TLxa7Hxq13rcC9sLdbrT+t2V2yY7yy
+/7xfw9vjPMRZRVHF00PEd1HNS85bTevMOLg4Sc8w9zA6UzlENk88QEkzUz9nNMlH389K20bH
+d120R8K7NbfTP67nVbQvTrQvsUDXV7FAtV/PvNVHsDbLy0nFPMRVY9tHxTg/vy3KPD/rxDa2
+MN9ZZ0S4QblGu9fKXVvTSdvj1T65RuPGb0u6OrxPd3fnX/dV5022W9W9QbRPxWOvQLrAwL7O
+y8i8PL1LtEPXV7k7xDjCO3fO4zq+PlnRREPXO88vQ0jrNmc9Y009RUkvuTRHdzpLPMY5TVnN
+VT21U9XXyGu34768xneyXcLIv0a6P7nV18JXukTvtj/MS89BuFtLv1PZW9tR091CyldDvj3B
+O9XPS9vKOsc7xz1jzTjTUePdVc8/skG7Mt3vb9k/uzq0U9/FQ9tB2c80uzu3OrtLzO/XVdlL
+PMVE107HSl9r3WM9W2dJyzGwLr7ZRtF360++SuPf40PjX0rD3z26QcNIvz+9Qe+7O8LvVbk0
+tfddyNndb0C9VbtVxW/TX09TVUhJ70VXY009a1lH/0rTTfe/yPfD47tnxb+/z7fA2bLTXbVX
+u0m/ScDOY9XOV1fNPms+azTPPTpfOT9FMFs9zzdJQu8+90VfWfc32TjRWW9vV9dv/2u/TblN
+yEK+90K6PLhNxM5DuetVvefbTLjvw+O7zve6v0fDVchOtD+/013CM68wska9T8+/QM5HSc1R
+711GzULKNboqzjvjTi+5Mt1vX0FK40ZX68pfXbtE28fKTsfdZ1XLVePN0zvX91M6wkxOPbg5
+RMI/zWO9SrnDxNXvvlG02f+tWbhPvd/fs0i80bpGw79Iv0vXQE9nMMs8QDZbPUlGMV9ESjnr
+TUVKWzz/70dv28NMzNtFvl/Z49fFXchvWefRa1XFVdlXulXTvc/Rzs3jwc7RyMzIwcTNv8jd
+yMm/19Pf6+dnS2/nTGtPQFlFRz9DPjg6PjxCQEJJSmNM30Z3a2dM7+ffR9/X21m81dPLz83I
+vVnTxM9O3cpTxV9nUbxvU9PvSXdVX0bfR+vZZ8M/vVPNTb8+698+/1e/POvJ28PEuc23612+
+SGM2TD1L9zPFycK/w6zjrLGu17RBz0dVIy0vLykzMDk/Sjc9Zz48Ok1OZ87NraysrKysrK2t
+usNROSYnIiIiIiMpLjVGu6+srK2tra2tr8PXXz0xMS8/SUbfv763vK/Gv8ZGL1NELytBOE9V
+38mxr7q6rb3Bzrj/t8bHtbOzu7CzvMDXT0Y0JiIkIyMjIyMjJis5WcK9rKusrKyssrK0wsnn
+38bbwevEuN9nusrXwkdTzM45Pci+Qbq7tbi1zsW541lO10ZL5+vDvLq9t7W+y9tRNCwnIyIi
+IiIiIiMkKzRLa7Szra2tra2tra+zvv9n31tRTVv/d//n12PXxc/Da8zLv9X31b/G18zByb7K
+xMK/w7/Gv8TGzt/O0Wd33VVrSk0/PTsxLSwqKCkpLCosLzEyPVXXyLazr6ysrayusrjBz85T
+Qz44OTc2NzdHRFNd3f/PvcTn08C9vcG0r7Cysbi0ubnI519ZOjo1OzxHTVn/b13vRU9FPS8u
+Ly4rMDA5QFdb/768tbezrbKvs7Kzt7m8v83Pzd3//1lIR0M/NDIyNCwvMTYzNz1GWU9j1b66
+vbGzrrK1tbq9vdfnY2tOSUlBRklBREFOPEg/TEJMTEljd9XPvbm3t6+1trW5u7zBxsLD1b/J
+vsvH5/fnT0lJOjcvKykoJyUnKCsrLzc7OUz3/09v0cnPu7q2trW2tLy1ub3Cw87N/+tv6+vd
+59HjysLOyb61ysvAv+/3zMzZx8nHwcnFys7Bwcy9x7/J09Nra04+Pzo1NC8uLSkoKSYjJywp
+KTA6OjZT2dV3u7extqysrK6tsbNFvshvZ+/rX+vIzMzEwcTOwc3IzHfdvtXv1c/KZ9e/07+5
+u8DEzNNXV11fS1dn51lTXV88PjwwLSwuKSgpKyskLio0NUZV983Nx825tba8sqystK2vrLO4
+xcPO90xIUUJFV0hn39HTzsXCzdnEU9W/3+drzOtP08DAt7Kys7y9u9l36+vfT2fC0V9r3Ugz
+OTwuKjAsJigtKiMsNjA9StPvW8+7xc3Auq+/r66vsrnAxW9RQzo2OTg9PUNZ5+fKyMHF09vj
+TT7V31E+U+tPZ8m7t66sraysrLm3r661sK2tsK6vvc3TVzcvNSwlIickIyMqKC47REJLY8xT
+0e/AxcPBtbi1wr/dXz88NDQuNDA2OEZVU9HNxtXRzv9R281JQf/XXee3uryysbm6sba2sq2u
+rq2tra2vsLzLZ04/NDEvKSYmKCIrLjgqPEFLPVnO3dO/urO9s7u9ys1jRj0/PDg9P0FDTExj
+TvdTTU1DRztPPktMUd3jy8W6s720uru4urK0rrGtra2tra2vtL3L3Vs+OzsvLyouKjcsPjlE
+PktbRkbv3dvjtLm8ubG7xtv/QDswMi8tLjQ1NkBFTD9LPjw5OzkzO0s/R9fIxL+usrGurbKx
+sK+xrK2srKysrKyzt7jMSm9INi80KyosLywwP0M+S9dVR2/v/9fGvLq0tLW7vtPbTUU7PDM1
+NDU0PTpGPkxISz0+PT41QUdFRc3XyL2vt7Wvr7aysra4sLWztK61r7m3zM9OSDUwLiwrKist
+LjA3QUBXTtXI0d/Fv8m/t6+zrayttLa82U9JPjMxNTQ3NUU/TUxXR05EPTs7MzpERUlf08zN
+ubnAvLS5xrO3u7qvr7GzsbG4wr7NWzU1NTUmJzA1KzI7Ty493/9BRr3rd8qusruura28uLTM
+T013RDNEU0g/V91TR0dXOTI5MzAyPjZB3eNNb7rZzbq3vMO2vMW8uLq2rbK1ubG6x9FZSjIz
+LDE0LDQ5PDJNRVNOwsDvu7u4xrKsxrfIrMPGtrvDyV9bMzQtMCktLTgwOU/nX8zrzkxXQ0M/
+PTpJREjKtnf3v7nv17Szvr21usK7uMC1rbTIuL7Rd01JMi4tKjEqKj0yOT0u32NB48S+wL+t
+u8awr7fKvLPHY73B3V9r7zw2QDgzMTc/PEDr38/DwdVj3e88QUlAPDw0VbxrW86vvdG/rF+u
+vbivwa25ra25vMDrxkItyy8pVzw0KS9HNyZEPC8yukPGz7evSrmw289Zvchb27lnudPrdzVJ
+NSs0NzJBT/fP57i93dtXRzg3OTw1S19T666sw7Wsrry4tb63srG1rKyysLG2y1dPSDsuLC4w
+Ky8+LSg2NzUuOltBRsHT67muu8HBs8LOws7Nyr6+yL6+y29VSjs8PDk8REZn3dvX4+drUUU+
+OTo6Oz1BQUlj19PbwcS/tra4tK+ur7Czub6/yf9nW0A+PjgwMS8uKy8rLTE2Nj5DT+PT08e9
+vb6/vL68tbW0r6+wr661ubi8ydHP3+/bb1tbWU9KTUxDRkhKSEpNS0pNRkFMSkVJTkpJW2/3
+48rHxMDGycbIztfXd1tXSz06OjIuLy8sLi4vLzY7P07v18S7srGwra2tra2tra6ur7O4ur/L
+1e9LQkE7NDI0NTQ4OjtBTFdXX/9fX19KVVFCQ0lGRGPd2cm8uLu1sri4tbm+v73Izc7VW1VR
+PTo8OzM1MzIuMTU0QUh30cS7s7Cvrq2vsK6vtrW1vsfGz+dnX0w/OjYtKyooKSkpLC41O0BT
+Z9fOy77Fvbm9x8fK39fRz9PGv8XFwcvKxcPTycjJzsvK693bZ01ZV0VAQjsvNDMuMT47SVnv
+1cy8urWzsa6ura2vtbW3u73Cw9vvXUM5NC8uLC8tLjQ8PUJVY93PzcjRvsLR0+9jTF9bTevR
+y9HJy9nLzcvZzcrTycvR5+NvT0pIPz86NjAvLi0sLS41PUhP78e8uLSvr62tra2tra2tr7O3
+vsHLY089NjMsLCwsLS4zOD1ESuv/09PFytPN79lH72/32dXFu7+8vrvCv8a8wMHCvcvV09tP
+U3c/PEA6MisuKyorLTE4UVXvzbi8tq+trKysrKysrq+0t7zI4/dbPjUzLysuLi0tODc6Ql1X
+WePJz9vT71lrb0gwxb1B2f//tcHbu8+6tcGxvLmvvcnTz8vZZ1E3Pz0uJyQkJi8xKzJHY2/B
+zbWtrKysrKurq6ysurO6xGdTPTg9NzEwNTU4Oz00N0BAN2dZRT1KTVPvxtHKtrb/99Vn9+/3
+27Gur62urLC4v9VHyT8vPDxILygpJC9CRiw0U0H/TDhfra2urbmtra2tt7TBx8VnNTpLX1tV
+PUJrz0hCPjdDQDzn/0dLY2fd583BvrrfPT1BY+/398ivsK+1trm8tu/v3/dbPkFGPCsvKzXV
+PztVN0BOMUqytcG0t7y1r6y8y7z/b2tdQkdf109FY0pnTzM4Nzg6TlVdQ0FE/7/Bxbm6zutv
+b+fXtrjIuK+tsrq9vt9fZ0pJW0Y5Ni87My9nOiw9Y1M7PsO7wsm9v761rq26ur9rP0pZT1VV
+42/bzspdS0E1ODg6OkZRTEpMZ2fPvrvAy2tb68nDu7Ozs7Ctra6yurzKXV1bVUAxLiooLi8t
+MS8wMjM7R0tv0cvCvLi0uLe4wsXI0efTd11NSUdDRUFDU1/3TmPr3ePX029vWd/f0dPT92Nr
+XdnPzs7L1dPGwbq4tLW1t7a3ubrB1WNKPUJANzUrLSwrKisyNzs3O03/zMC1s7a3tLS0tbq8
+xtNjV1NOSEY8Ojo6RE1PUVlnWUhGTVtjX1dOV1Pfzce/vbu3t7azr62ws7i7vb7IzutfQDky
+LS4uKS4tKTAwNEhFY9nGvLyzra2tra2ur7G5usrbV0U8NC8vLi8tLS8vMzg6R0tT/2dv2+PV
+ysfOxcfNwr++urq5t7a1sK+urrC0try9v9XZX0Y9ODAtLCoqKissMDI5Rknr1cC7tLK0r6+x
+sbK8vsLZY19HOzoyLy4tLi4xMTc5P0pba9fIw8PHys3Nys7JyNnTysvNv8PBuru6t7a1srS2
+ub2/xMnVa2NLPTg0MTAtLi0uLzI3PUROW9nKvrm1sbK1srS4urrI28NIQD4+NTczMTEwMjM7
+PkVPXe/ZzMS/ur3CvcHMx8bXzcnT1dPV29PGxcXFx8PEv8TBwMPHzdfrX01JPjg3MTAuLi0u
+Li8vMzk9Q0xd287Evbm7ubi6urq6vb7D1ed3V0hKR0dJR0ZMV13n1czCv7u5t7e5u7y9w8fX
+2/djSktJSz/3JNW4TEkqTLO8/+fPysnRzN/f61lLPD5NPDw/PDs1Ozo4Mjk+RUhd3c/Hv7y4
+tLCsrLG+yce7u7nD11NJQD9IRUE/PTY3SVnZ3dnbycm/wsPPW1dXUUxBOz1AR2fHv7/Bwb++
+vr7GyWtn52ddT0VDSllbR0Q8Oz5AP0JFVWtNW+vEvby7urq4usPJyMrVSz06Sd3Kz+d3a/9j
+W1VJQDs8RlN35+//08vAv8jTXUs9S1tPPjk5R9W7s7G0uLe4ur7OX1lOVV9bU1djZ1lbWU9H
+RD4+PT1BPT1IW1lVXdPHuri1usn/18jH1Ug8Ru/Dwszr/9/n52dORTo0Nz9TX2Nba82+vL/O
+519FQFNnSzsvTN+6s66vrq2srrjCzdv/VUxPRkVZX0w/QkpTTUk9NThATUs6PVN3y9fTxrq3
+ub3Fz9/r0eNLPUld99nIy9vf2e9ZR0Q+ODQ1OkRRX2/Rx8HDxdHfYz9HX1s9OD13wrm0srCt
+ra28yMrM3VVHQ0ZKT05APD1LS09IPDdMWUE2O03RzGNZY8+1srrLyMDD5/fJx+9V793NycfH
+09vb31lERj88Oj9DSk1b59fn2dlr/1tITW9HP0hfz7y2sLC0sq+3vr6/wc/Z419XW11AODg7
+PDw8OjM0PTw3OkRGR09n59HAt7vAwsHAu8XZ2d3VzMfFwMLMzs/nWVdJQT8+PDs9P1FZXWvb
+729nX1lKPUffY0dHXdvOxrq3vbmvr7e4uLzAx8/Z729j71NJSFVMQT1BPTo5PDo8PT9RWVVO
+58/T0czN3fdf0cxfU+Pn4+PT193359/3T09RSz8/REZHVWvr79HJxcjNysbN0cvV08TKzdfL
+x8rIv76+wL++v8LHzdHd2ddvV0lLR0I/NzY0MzQ1NDY6P0VLUVtfd+/d19PTz8rExsXFw8O/
+xMnHy87Tztff619PTEdKTUxKS05VVVVPVVVZU11rX2/33dnP68rRyMnOz9PX083Jz83T19nO
+0dvj929nX2NXUU9LT0xMTUtPUVNPT09NUUlKTUtTT2dd/9nOycvGwb68vL2/wMTByMzR23dn
+TkVJRT8/QEA+PkFAQEdLV2v/1czFxb/Cv7/Ew8PFx8TIztXX19vd9+//7293X11nZ2tnW1VV
+T0hFRUlDPz8/P0E+PkA/P0ZKV2dv4+vR0c7PzcjBwcC+vr29v77AvsbNztvjd11dT09HTEhM
+SEZJTU1LTlFVVWfd19HPzcTCv76+wcTEy8zOzOvf42fjX2NTV1VVT09NTUpMRkxGSE1JT1FV
+UVFPVVdOTkpMTUxLTlFbXW9rd3fn59vT0c/RycXBwcHBv77BxcHBx8jOyNHV29vd/29ZVUpK
+R0VDREhHS05TXW/n09HOysbIxcjJy8/Tz9XT/2f/V2NXTldXS1dPT05NT01IR0dGRkdLQ0ZD
+Q0dJS09bXWN379fPzcbIxcbCv8PCw7+/wcDEwsHIyszP0fdvZ3dfS01KSkVKSERCRkZGSk5T
+TlNNT1ldb/fbzszIzMjIxMjFw8fIy83Z2eNjUU9TTU5MSUpHSElCSkpHV2d3d+/n6+fj5+/j
+/+//d//j39fb0cvNz8rJx8LCxMfGxcnGys/Ozd3f4+NjWV1RR0dGRj9EPjw+Pzw5PDs8P0RG
+TFddU193///n39nRz87OxsfGysfM19nduTQ9rL0lPqw6P63OQcJrRcTnOee6PWe16z+6ukhr
+uchK3cpn99VZV9vZV3fD91nKyu/Iz3fX1Wfn13dv0+dX1cxF38dnQWtVPkZXPj9dRzxFRzxM
+STk8Q0o9P1tHTs/r78TBwLvCwLi+xsbC32e/yk3CxUe9vUfd1d/b38rTb9m/RVtbxU41Qm9N
+NUs4a2M72URGvtdRyNXPvlvfxNf3zuvLzcvN/87I29lb3evbVUpX701GS0dTU0RAQ05PRUxb
+a+/X0dXOxr3X473BTcNO28TvRs7GTsHbyL+7utnCv9nVWWdBP04wNTQ+LTo7PNs230fjW1Xv
+QWdNz01ZzE7E68G+wrzFvb2/zdPA58nJ113n1UVTRjs7Pjo3PDtFSkpX3cO+xLm5uLS5xbq7
+//9Za+PZzNW+sr68tse7w7/Fd29PU0M6QUZVMURNPDE2TjswRDs9QVM+Ud1TXc7Za8HF09vH
+z87O92/j/9NDX09d/0FnPVk/PEY7TjrvXc69z7rByLa51f/PW1/fTU5d67rNRbDFtsC82bjA
+N7gyrkU0rS65vz+8ONFCSzs9XzjbPk7vV9VPz8n/uMvCvcLJ48/Rd9NKb1HbT0r/V3dXS1dL
+WzpFRUhHXz5n929jX8VvZ7hLY///OtNON79Eb75NvTe1rS+sPUu3Sbw7sV1v2edNW8I1Vc07
+Y01JUzLMMkfRP+9PyVPIS2/VTcBjSb4971u/5zm2SLs8tTeu2d3F3bjV48V3zcLAyNXJa7RO
+u1O808nPXbZGu0O7QUe4RDW4L8tHZ18yzUk3Y/85ty+5Ns2/NMvPLbY0zzpMTDvbvjfr5zvj
+Z09VWedTTcU8yjWtMrvFP7hLd7/du75dtUTFPNm9S1XDO7itLa0rTbUvrDG7ObzEO79BPLYu
+rjbJazu2PlW7Lq850bsurjS00UC8Tj+5OLZIV8I1w0XnTz3AN8LPMK4vRb5Lu9dRyUPPZ9td
+RfdJzkhJ43c/uji0OFm0LrAvtzS878tdU8RvUbsvrje3vj+0OK5IxcJCskS3/z2yU83B3U/f
+vf9byj23P8rLQspOd0pjP2s5zU5GzT/NP91K30xGzDjr1TTP529G21PnU2NHR+dCvNtHxj6u
+McFfW8nVzMz3utlT18pXyMpPxT5vwjmxR0+9P+vDS7/XXbtO/7hfz64urDg2rFnjsTuvb0+u
+U3fTRdV3NP9TP7pBVd0860tZ69s4vj0yvi22TU7XTkrLQkK9Ql/EOlu9ObxNR68vXbhM2blI
+ulu+1+/Zxv/M2dVZvVPP485dOsF3ObpTV0PPUz7vW0n/Re9Ld+tb/8E/zsJj3993zF9r33dK
+51l3/z3ZyTPd10RvTtFbTt9NV1tf59d3d9nKd9XVy83Zv8vTwMDnwc++w2vEzNPj0+vn72vX
+a2/nY+fnT89Ma/db/1Nf/0VfX1dbSN9XT1FjVU1vSmtOX1lrW1tZWU1ZTFtOUVtPSlNPV01b
+VVtXWXdb71Nr6/f373fdY3fR/9HX18fZzNPR1c7b09fjz9fbytPGz83Pzt/L39fZ59X/zt3V
+y9fK08vR08rXz9HV2+Pd6+fva2ddXVtMWUlMSkhJQEtEP0pDSUpFU1VLY0v3Uf9OZ1lRb09b
+/1vr/9fv1+fd3+/d5+vZ79vj29nV29nd7+fd99l35+dv5/fr92v/Y+9f/2tn41vn929r713/
+d293W+93d+v34/fZd9v/69nv59X/0fff3dnj39nr3edr33fj9+dvY2tra2//9+fb5+vf3+Pj
+293b09nd59/jb/9rb2dX91NfV01PSUhPSk5JUUlOUURXRVNRV11VY29rd+/r6+Pv6//392f/
+393Z2dHZzt/N19HX0dfZ2c/b18/Pzs/O19PZ49tr72db51n3W+9jd2dbXVdfVV9Xa1N3WW9v
+d2v/7+tvb/dj62v/b/f/39/d2dHRys/O09PO68/Xa9XM21Fv0dfvZ9/3VVln3VlG511VU01f
+U1FRY1NNUV1vQ0xOTUtR/09ZWV9ra/fv52dv783b9/fb19Xn1dnv0dPNb93Dy2tRzu9n4/9v
+Sl3d31nv229ra19bTe9vZ9VTSVnf1WPZ79/fzdnI2cnJzdu4tlNN2b7CWevn31u7ykFJyLd3
+SV9Bvb9IOUZXzdfPQzLNs0JHbztbvko4PU3V7+c4OufBVz5L51t3zDlOZ19PzOc0Y8vO6+tf
+xMfCb87BwL1rx8Ovul/Zv7iwvUpTvsvE01M3PN/Pazs6O0XGVTMuOG9dOC45R01NRTw6WdFd
+WVnr2dHMw9vf38avw//fua2w2Wu7sL13zsnHx79XWdvKz87rd1nZysn/Y+/bz/9ZVUlI79/3
+R0tO59XvU0pXb1NBODtCPzw6PUVOUXdNVWPVW1dMPkVGRjpBQUld11Xfu7y+wte+tLLI69fH
+zGNFQ1nX09v/zsvT09dZ2XdbY0c7TM/n59vTua+z2cO9tq+2vrm0v8bC1/9JRkpZSz5H07/D
+62tXUzkrJCMjIyMkKTM/W8+/vLmsrMw0MUfIzEo097Kutse7tq+z0Wdb62PnUzhIXdfJWU/V
+vbvDTnfVXetbRUn3vrm+xL+/w+9HT+fbY1lLQjo0MTAvKzI3O0v/ybCssba6v81ZOjAyMjI2
+WVtbsKutvLyyrKy+Ttu/vVVfa8u5sLTGxr7CyFUzLzg5MjAwPVNJTj083+9N6zkvQD1KZ05O
+w7u1rLq/sbr3zcdJQUhJd8TIzL3AxNtnRjAwLzg7QvfZvry3tsTG609VOikmKjNLy7Wxr7G5
+y2M8Oj9G2c7JuK+0vr3Zz7XTNjw9Sus8WcFfr706PVPj4/85LCxEura5vMKxr8w6LDEsOjIt
+Qe+1rKyvsLW4yz0qKSsuMzY6W7K3rL3Ar8HrSj490brPvbG0vMG60c3G09nO0U1rX1NvSi4v
+vGMzRDY231MyMkRXPjdFPsy9XdlTza25yc0+Y7q6W0pL08vJy9m0trO5Y0xOOzIvMUV3xbiu
+tLezr8FvV0EzOjYtQHdn79VvR/d3PTxNUUVRVS9My1PXzNHI519XTUdLTEhFVTs2TF9Dyb6+
+3a6svK2tvMbEubatrbiurbbBuNEpOU42Qkk3RcbAxcdfR0U+KiIiIiIkKCtE71dXP0xRY1E/
+P2ddu7G0r6ysrKyyt7Kssba1tL/Dwc9vuLGxv2++/8pONElBV8vVK0BNPDQyMiwx7009/zwy
+Ny0nLSgoMS43PefTtLy7vs/Oyms+PtW8urSusq2svrq6t62trq2tra2tsrCtvL++v9W972Nb
+OkE4PjY2LC8mKTAqOS1ELjs+MC8uLC00MkhKTF/Nyb3Xysn3V9e+X83nwNu4u1m9zr69vv/R
+ur+yt7e9u7rOvtHP979fs8W+xd3IQudPQlXvQEc7OT8v/yrRP78/VetEyEvHXbjDtd+zd7W8
+00TRa0//X8swxTlZPl87YzFfPj28V1FVZ2/fyUpJd7pOzcpbyLvRuFevV7p3tj2uvb9vz8JM
+zF+9M8VE2z3/O28v1S09M+swRDxNNkJOW01Pd0/jWVG6S8+3L6wvuFe3b1esRqw3rUCvrj+s
+M7rfxFdVu1GuNbU3uEW6OcYub0NZNT1KN9sw2Tw8SUw6Ty/OML9NPdFM39lFvTjH0cjMzLrB
+wbbEuL3XrUyt67atv7nCvrzbt1e2UbpPwMzZU933SEhKvDfvN0JAQixGI04qPCdCJElMJFkj
+Uyw+MjE2RT88XUhHxj+vU7m3v7usa6zMrLutrK2srK6suazLrLmuv66turO1tOO5ys/FU81M
+aztKPzdAKUsiOSkrLykqLCorLihHI04oPzI+QDPPOl0+znfnw3fCzLr3rf+zuq69tK23rcet
+rrmsw6y+razHrMuzw8y6b71L51V3Tl8+TTfrMGMyNMslXTAvPj42Qjc1QTFGPzrVOOM/V1tZ
+02tOv8vTtlW2U69BsUZRt1G2d8lfu8jJV8Bv17xEvdHTxlm4S8nGSrpKtc1nuUK3XcFjRrhj
+d87ZyffZzUnGT83jb1lRxmNv2VFnU2tRWU9JYzxrSENJPUo3STc/Qj5MOlE8RERGTU5bW2Nf
+d+NV2V3Z/9Xv2dXNy83JxcfGwcq+xL+/xbvJvsnJycjN38rZyt/Ib8/r0+fb3+/jd2/nV+9n
+a9332dfv0+f/11PbX1tdR908bz1HTEFRQltAUUhbS1FKSkxHT0RJQkZHPko9ST5ORkpMS1lL
+61nb49/P0crbxc/AyMHHv73Dur+4vbe3ubK3sbK2sruzu7m7vbvDwMTIytvb43drT1lGSD47
+PDk1NTIxLy4tLC0rLSwtLS4vMzE1Nzk8PUJHTFFVV19v39PJyL+/urq2tLOysrGvsK+vr6+v
+r7Gyr7Wytba2uri7u7+9xcfL0dfvd2dXU0tHQz89PTg3MzQyMDAuLi4tLi4sLi0vLzM0NDk8
+Q0ZOWV3vb+Pf1cnMw7+8uLixs7CysLGzs7W2uLi6ube3urW4ure9xMbX2eP3U1VMSkpHTkRO
+RkJFP0E+Pz04NzY8QUNCPDg8NT47Pz8+QkBJTedRTuvDW+9MTP/Ru7S3vsPDvL66s7a3tra3
+tri8vMXIysfX51VTV0lPW11dVVNMSEU/QkZFSkNAP0hGdzFAQFFvTTUrR0m+u0s7191Z02c3
+SOutray5wE/fz7m+uMK90761s7m3srm9wctPQE5TU2drRkhITFc9MDMxLjE2LiwuLzM5ODg+
+OkI+PkQ8QlNvZ+PXy8S5tbO4tbOvra2tra+vsq6zsLa0tre5vL7FzOP/X1dOQE1NVUt3OkVL
+RFU+Rjg4OD01OTUzOD9ER0I/OzxFQ01NRmNrX9Hn2d3Oyr+9w8TMwsa/vru/xM/Ryc3Tb2tX
+d8zMzF9CPT1K9/9XSj4/P01nZ2dPa013b8zZ59/j0c7KxtHr09HGxL7CztvV09nXzNv/X1dR
+R0pKWVVfW0s/QEM+QERNUU9OU2tnz8vM0d/P48rCxMnPy9vDz76/ycXPz+PPZ99bVWNO03fr
+b+vjX+PJ42Nv63dTd8pHPkZZT0vV70I+RD44P29BPkU8Oz1KTkZDVz89Y0FrX29b/9nXvMm1
+x8nFxbS8ure4vLXHurW7t8OuZ8W+79fLuP//290yRUI7S1VjSTdCQj5ZSkBROVs/V91R39NT
+72t3yte7xu/TTE5v/2PT0dHv68hX49vbd1nKTFFfdz1MzndPX8hCTUfjd0jIwzhdy8vLwr9n
+OjI5Ljzd58K62ctryr/AvbxfTEIvNjZFY3fLyGfH4//O3cLDzLvDxba9urvBzmvX51dj61Fr
+XV9fRudLRllEPj48ODUySER30e//a01jXc3Fx7TBur/J18fjxcW/v8LGzu/PZ2NfPjYxLy41
+OknfzMPJwsvR31NKODI1MTtOY8m7tbq5ubm6v73Oz+9f2dPHvLvCwutjRz9HOjxLPD9BPUpZ
+/+Pr998/TEk//0/fY1dXU2/O/8y+077Gv8a+ybrGxs5Z21VVw8jJusvK1Vf/X0lrU1tHVV1T
+/8XIwL3Pyk9MPzY6PTU8Pzg6PEY9SO9TU28+PTo2RkBNye/n505XX+Pnzb/Hy7/Py7y+t7K6
+tbq+u7i5trC4urO70bvNx768wsnBy9PFymfOW1dGOTQpLSoqMC8tOS0pLyUqLS04LjM0LTw5
+RcPIuLS+tsPVwNPbucy8z7q+wa6ut6yvvLW9triyrbG4tMnHyNHDyczEd9vnU05IRUo+Tkk3
+SjtDSUNJPz5DNjJCMTxKOEU+PEk9WWdTXVVIRD0+UUPXzM2/yN3dZ9/R28S9zsLE1+vNwMe7
+s7m3uLm+vsnKz1lrR0T/a9PHu8vXa0Q2MjQuNjg6PkBIX1Vny9P/zttLS+NOT9/ZW2tv53dT
+udHvusVn03fPy76tsbCsv8lnQD02Q933wLLBvsrTd0RIRC41NCsvMTc9OllGPEc5MUc7SdHJ
+uLi0r7i1ubO6vL7KZ1dPNlNbT7/P0cXf491rWff/3ePLydG9487Ka9vV/3fX/2vvTk1OT0Hr
+TlPXTUxvQjlZP0RGUTlFTjZGRFFr787BzO/HU0Rr79/HucC1sLS2tLO8v8PO29nR19vH2d9d
+TUY+PkpDRvdHRO9FS048RTc4MzU4O0NFWWdfVev3SXdna+d3zd3NvMfGub7FuLzAw8O9w729
+xM3b2V9r38jFs6+3srnDzt1PRE1BS01LXVNMT0hJR0JKTT5fSExIPkg+QUdLV0//W0xLQDo1
+NS4vMTA0PkX/X1/vVV/3783Xx72+ubK5sLWys7SwtbKztbi/vr+9wc7TxlvLwN3H1dFvV189
+RmtNR01FPlNFSdNK0989Pjk4MEtIW8PFv8fXVUQ2Ly8sLjlFTtXJ1+9KPjo2PExC0bzLtbjF
+vL6+vbu4wMHM3dl3373IwbvRXWffTtvOx7/KvVvn519vXf9RSEZMRk5r6+vZ3V9LSz46TjhK
+90fKX1P3T1lZ40v331vPycjIyePr10tj3U7ZwdfdyF9fzt/b2+tbTU48TzxNd0/r9/dV60RH
+RD9NQt/jy8fByrzXv85vylnNzM3BzL7F68lnUdtMz9lbxd/Gvby7wsrvPFFKOHdja8/X29vv
+Z1NEODMvLDA3O2vLy7vB2VM8Ny80NEFdV8vVd+f3Rmt3Quv3Z//J1b2zurCvtbSxuL2wubyv
+uri2wMfZz01fV0BTPEI9R0pIVU1DPDoyND42Rk5DZ0FVSkRRS/dVZ1tL/0xr29O+vrm7w8nv
+WVHj69vJwtnM52NNS0xIUVdO21vO09XKwsfRwdvj2dNvyef/zNXK/9nZd09jSEVXSetOa3dK
+PUBGPlP3d8vMXedJR9V3xcDJvmPVyGfJw8XJusDNxcHJ0dPrT0tZUU9VW2dORF02NUEvMzk7
+PT5KPj49OkdBTc1n38Hr38rj983J2dW+zLy6uLC7sLe9vMHK0cd30/ff4+vVV9Vba2NHa0FJ
+Tmf/zc/IyddrWW9jb9Vdb+NRSE8+PD5KOD8+O01ASV9fV91rY8bX0b/Z58LJ47/b17xnzcpT
+3/dZZ+Nd2+9X411j/9fZy8L349NLTldKXV1TTVtZPWdKQsj/3evbW9vb38/O38nNd8zrzs/R
+0+/PX1HdVW9fU1VXRExrTV3/T1lbTEhKTD9RPkldT1FV/0vd6+/PyMDEw9PLz9PEvry2t7e/
+xs/ZZ2Pn5+Pfz93b3+t3V+t33dfX2WPjTmNFRz9CSzo8Qzk+PTpKSDxIRG9f0cfJusjLw19n
+791vxcfJxdfT129jUWdVa+NR0U9V3UxV39NZ/1tPZ1/r3+PR4+fr/87J4727xMXI0dvOV+/X
+SmdrVUhjSj1jTU9Vd0Z3d13X28vGztfO1Wfd213R02fMd99fd/9XY0JbSEFPRlFMZ19NUUFI
+P0pCRVNJd89db+fPzt/CysHMwr/Gx9PIU/9FXVtP0UfrV11nWVnbz9vIwse9xMPGycrJxc7J
+02vn993vyWfT42/nW1lZ30/nTkx3VVFbT0RPX0J3UU3jb3fR/+vP41/vW29ZV1VKVVs/UT9E
+Pj5AQj9FY0Y+SENNRXdf0+PAvt3E28/r28LOucW4uLm4vr7Gyetv31f3Y1NnTutfd11bW1N3
+39nZz8bNv8bIvr2+ubzJxsF3zW9b60lrZ0Y9RDwxPTMvPDg5OUU9O0hIY0ZM1+Nn181va+dn
+6/ffx8zdwMzjvsjEvsfGvd3TydfJwsx3x2dVy0bn52/Pze/nxO/HyNfIzNPbxmNXX0lXSUZO
+O0Y+OjM5OjA+Njo2P0s9TUpTa2/r3dPPyc/fyNnKwMfAv7zFvr7CwMTAwsXDw8PrzMLbytPV
+Y9n/WWNrze9T/11VY1t36/9TZ3dPWUZFXT9EP09CP1FITklXTlvnX/fdz8njw+fr191fb/9T
+719n719Ga0lMVU9db29da1Hn/2Pjz1dj1/fnzd/Gz8/ExMe9vrm9t7rIu7++wMfGzdnZ593n
+W1dJPEM+Pj05PDwvMi41NzY6M0FKOj9PSVdP51v/b9nC08jFvr6/ur2+wc67vdO+xMa5wL3I
+xMbEycfK0cjI283L18TE1cvGY9XXZ8tba11TRkhMQ0VAPjo5NjNDNDQ5Mi4xLi1DNTE+OTU9
+PD1dQ13VW9vRxufEvse6vLaytrWvtbuyu766ssK4v727vrrDwc7PzcTH09PL20tvW1lZb1NZ
+UUhTQUdKRUdMTj9VS0pKSENCUUlnUf9LRktTSk9rY9tbVU9IW1lvb9/Za9fn2+PR39Hf1dnn
+z9/T0crVzc3Ozdfd7+tdY+9v919XUV9OX2dbY+NXTndNY1dXU09PU1tvW2v3TmddV2v/1Wvb
+7+/O09++y8m9xsu+0cjE2cPT79Xb/19Xa1fOb2ffX+PT1efvV9NRT/dI3WffU0nVQ2tP91lv
+d07vUWNOQddL699Db1tHz0rN2UvLZ9XRS87L3bpjycdfvW/v00vMUdFj4/f/V2fdP2NnQONH
+U9U9bzw/Tzo9U0I6RTxEQ0ZTTUrKS8vT3cHb0cnJxsS908C9ybS4wLTEurTGsbfGsr6/u8DN
+tcHNusvNw0zDbz3dQ0FnQTJOLzU+LTk0KT8sKz0mPTYzQy87PTlMVU7KZ9PPyd/AxdW5yr+5
+xLm8urfHssTNutO9vne14/e478O7/77dSbdJ0chN41tE70RXSD5nPVVbPllJSFtO7+M80TU7
+3zDn4zbnO0xfQNFPTctJV8VEyF1nzFPGvlvB09e9yrSy16zfvbLXtbnPu99bvlPHzk3LRznE
+M9H/NtEw/1843TxDX0BXTkBRPUBdP1VXQe9FRP9GV+tCY0dRWT9jY03PY2/D68XI47rHwba/
+urfBtrrFr+O3w8+/0du5Z8fTWb9f3c8+6007Zz4+XzdIPjNHNjlGPEE+QUdAS09OVWtX12Pb
+yufHyf++yr6839HK68fA38/302vIxsDv3VdRz9nja193T2PP/2tNTVVn4+NTTEtLVU9LTkVn
+XUzjSV9ZWevf5+f/779d09fv1+fNyeO+42POW9PGy85n3c/rz/9P219Ty1vXZ9VnX19dY11j
+U+Pd3efnV1/fV9vfzXdfZ11f20ZO2U3db05ZTV1X/29Na0xRV1lb/3d3Z/fX29PX2/fZ99XL
+19HEzsLVz9PX383J1czb3dtXd01j90ZMQkBPREA/P0ZRRUxFTFVBW0tb71Nvb07f29vRX9nb
+zcD32c93zevR08zG0ci/zcnT58/Lx8DFxdvV2dXV09fX49fn51ljXVHdZ9vbU/dVZ3djWVNM
+UUZMQzpCPj1HQEpJREhCT01ESE9N711VX0dVXXfd293P79vbysbAv7u+vr28v8LIzcHBv8jM
+2d/j13ff2///Y+NjZ2dbXW9LT19OV1tNT1VVVU5NSkJORktTSkZMTEhKRUdJR0lRTlNbW2v/
+49/VyMfFw8PAv72+vb+8vr7CxcHFwMHJxs3RzePj//f3a2tRUU5DR0ZCSkNCPT09O0BCRVVI
+TVNRd1//193V0czJysvP08/Z1cnn2+Pv1etnX2tvY29ZW2vn//d3a2djW01NTElOWUtMRkxK
+TE5LS1Vv6+PXz8jEzMnEyb2/wMHEx77EycPGy8rJ29fb/9f//3ddb2v/X1lZW1VZUVdXd1tb
+TFVVTU1bSk1NU1VPX05fd1dOSk5NVV1ba2d34+/r6+PVz8jHx8vHzcnT19vj3evf5/9dV1FT
+UVtTTldVTU9LS09PW1dfX1dfZ2/n5+vr3ePb3efX29PT08/b39nn39PbzM3NzMzRyM/XzM7P
+29fd09fZ3+Pva1tVTk1MT0pGSUE7Pz1KS09RVV9nV2N379/f49nZ3dPOy87M0dPV19nP2//r
+Y11fb//v6+dva/f35+9v4+ff4+fvb2tnXVlTZ1tdb2N3a1tjY19rW1tdZ2tdb+/v32tra2v/
+/2dfXW9b92tjVVdVT1lXY2//3efn59PNy8vLzcnJxcjIxcjOzc7N2+v37+dZVWNfZ2NdWV1b
+U0xOVVVZUe//d+vZzdPOz8zM08vNzs/b0dvf62drd+9rV1NbV01JS0xVTUZLTEpMSUdKQD49
+Q0lDR0VIU05OUVtb/+vf19vT19fR0c3Nz8jIysfJx8rV39XXz8/KyMjCx8nJ1c/Nys7Xy8rJ
+y87Ry9fR0dvdd2tnY11dV11bVUpEQUdEPzw+QkA9QENGSU1VTldrd9nVys3NyMXDxMXFyM3N
+z9vP2d/j9+/vb/9fZ2djV1VZU09XWVFTTk1NTUxOTU5NS0pKTE9ZX//d3ePdztfT29fR0c3X
+0+d3693r79/d2+d3/+f379/r9+vv5/f34+N3W2NfV1t369/r6+vj2dnf29nT09nj29PR0dvd
+29PR2dvXzM3O2dPX2d3V2+vvd1tXWVlNR0lJTEZBQ0lOTk9OV1tVVVVjZ2t3d9XZ09XZ0d/n
+9+v/b1lRS0xLR0pLS0tKSE1VU1NOV2NjY2/r69/Z0cnNz9fRztfd6+v3b//v59nX19PRzdHV
+09vX2dHO087O0c/LzMfJx8rKzc/b3dnT19fVz87b29vj629fW1VTW1FfU09RTExLSERAQT5D
+QD9DRUBDP0pMTU9NXWtjY1tnX2drb/9vd+Pfz83Kzs3IxsPBwsTGycPJyMvV1d/f62dvXVdZ
+VVNOT09PU1tdX2fv7+vf929nY/9nZ2dna19na2936+vd49/r7///7+/f39PT0dXX1c/P2dfj
+5///73f3b/d36+fZ0dXP0c/V2dHZ39tra11RUUtLS0lJTUlETEpOT0xPT1VnX2/r6+9r/+vn
+Z2dnX2NXW3d3//9v//9nY19jZ05XXV1fU1trWVdXU1dbV1FVVWdTd9/b2+/X0cvOycnKyMvR
+zcvFxMG/vL++vL29vry6vr7Ex8vP0dvf5+trX1ldWVNPT0pBRURHSkxLRklLS01MTUxNTU5X
+V1dfVVlRV1NRV0xKR0tESEdOUU9TU1tXX11nd3dr7+vb2+fd6+PvZ3ff593V2efvd29nZ3dv
+993Z29fX1dfTy8nDxsXHxsTHycXFxMbHyMfGys7P2dvn72/v/+P35/9vWVVVU1dVV1tZX19b
+WVNRUUpMS0pOTU5MTElHRkdIS09RT1dZWVlXWV9fX1tfb3fj3dXX0dPV09HLzM3Nzs7Nz8vN
+09fb19Pf3efv/2NjX1tZVVdTVVVXVVdPT01OU1ddY2dra2v/9/9r93d36+/f9//na+vj2dHI
+y8fJxsTExMbGx8rLz9HV29XX29XV29vjb2dvY2tnXWNfX11fV1FNTExLTUxISkpHSUhJSUVJ
+SUpITEpHTU9XY2Njb2vv5//v6/f/7+Pb0dPXztnV1d/j5+vn//9v6+PZ2dPKycfGxcXJxMfJ
+yMzKzs7T0dfb39vf69vvd+93Z1NbT09RUVFNSkpLS0tRUU5PTEpMSUpPXWNn9//v///3393d
+39vd69/d3efn929v5/f34//db2NnZ+93d93V3dvd4+vj99/32ePZz9HOz9nZ429v///n7+//
+//9dWVFVU09VT1VPTFdTU05RW19XT09RXVlrd3f/b//f693n6+fd7+f/9+vvb+Pf3dnf0dXT
+1c3T0c3OxtHLzNPb5+vv79/33+fV4+/373dvb2fn7+/rZ+fj/29v52/3Y3dfV1lNX1FRTFNP
+T0xJS01LTkpRTElHSEtIR0RISUxMV1lv9+/n18/TzcjFys3RysjMysjCyszLz83Mzs3KyMjK
+zMzNy8zJy83TztnZ1+Pn793f/2tnZ1dTT1VPSUpJTU9PVU5LS0hFSENERUNHSklLTExDP0RF
+S008SEZMS0dGSUtPTUpMSUhCSVFVT1V3b//3/2/v1dHHyr++v73EzcnKwMXLzMrNz8fJxMbH
+xMDDvcfFy9nrX2dnb11TXVtOT2Nf7/dva1/n42NfZ1lZZ09NTkFKS05dXWdfVf9PWVtbUU1G
+TkhDTUxITldf/2Nb7/fj49XnzM7MzdPRzdPXy/fO69nXy8/Vy87P1c/L28/Xztf/0e/f1dPN
+999rb1tbXV1XX0tTTk5MP1E7Pzo+Qz9LSEhPU09vb1vb39PMzsPEv8O/v768u7vAvsO9x8zV
+3WNvWetnWVtVTUxbVVdXVWdX42/XZ1lPSU1LR0ZLR01DSkZEPkdKTldr49/J59fTzdXZz8jX
+53fZV19j71Ndd1lvb/d3Y2fd9+PXz8nHyb3BvL+/v7/Av768wb/DyM/b32NFTkE8Pjo6ODM0
+NzY1NTU4PTo6QUpHRl9db3fj59nKzMLGwMDDvsbBxM3Kxb/Izc2/wMrJvr/AybzMwci9wr3A
+3cPTyNnv/19fTldNVVtMSU9JRUg9R1c/SDtFPzo8OTo3Nzo3PTo9Pj88Q0pMU1drd+fd39PZ
+59fNx8O+v769t7rBu77AvMPGw7/BxMPMzNXf7+/va/dnWU9NV2djX2dbXU5VQ01PU11XUVVR
+TD9VVUxbVXfr1czRysTO09XK0czFyc/Z2f93z3fT63dra1tfV+9n9+/j4+vvZ/f/3//r4+dn
+Z1dbUVFPVUhDRkNBODtAP0hHRFNVX1tvb2vv283Fxr++vru9vb24v73Av8rM29Xf7/9nU0tK
+RkxGS1NKV0pGTUlMSUdCP0dKR11r49Pd0dV31d/ZzMnPxMS7v7zAub+7wsTPxM/n91tbW1Fb
+Z19d/3dvZ1NNS0tNU1NZT1NKSkxRTU9NSlNPTldTT1FdV1VOUVVbX2NbXf/359vbz9PrXedb
+53d319XO08XJ0dtba2dr/+fPzM3NzMjJzcvTzdvM2dfbzt3N38vF0dHfz8vPz9HVd9dr6+tn
+a11bR0hHTlFdWVlNR01LU1dRX1tfR0tKQUI/PTo7Ojg7PD4+SUJBSUBLSFVZ3+vXzsvIyMq8
+vr69vr67tra2srCxr6+xrrKztbm7wMbO12tdX1tVVUhOR0xBSUE/Pj5APEM9QDs/ODY5OTk1
+Ojk+PEA/RT8+QE9jV+Nnx8PEwMO+wMPKxMvL18/Zz9fbz+PZ5+9n6+fv987Ry8XTzMjJwsDL
+1dXDwcfJzszHwOdXa2f/d05JSlvnX0pDPk1TUVdNU0VbVUk9PkRXU19Oa//jVVldS1VMX1XL
+vrvvUz5dv7rHwrrVzefZ19HK6+fn1cbbPTAuzr2ttNtIQDw0ODhNb8G9vFdnX/9dTl3dwut3
+z8RFTGNvPd/328O9/0bdXUc/WU7/T2dZTzw1NVnZSz41LzRF3dlrW9/Ty7+9vtusrKw9LzT/
+rKysrK+vtO9GOz7Er6640+vba0c+OEHjwWsvQzovLzEqKUDnQuPMXzxNzGfru7/BucDVa8HP
+x8bP1VlnWbu4zz85L1ljxVFK213OuCQpr6yyNSMoKKyrrMqwrOffNigpPbeszLzDsLTTST0+
+u7mu32P321U6MiswQfdnb1fIztdTPUNfuruyt7S9uNPZ18HJ1dXvVUlfRTY7N0I6Q0BbXdH3
+Wzk2Oj08Sc3fx2eyrSNPra2vUyo2Ma2ttNOtrchTKykqScvNN9u4rc0+ODnXur3VX9u3vkwx
+LDlKY2NHd7uwtt9nY8C4tbu5sq64yklZ59PRV1Nn30s9MDY6QDw6RF3FTkFCTT5LV1tfuLBG
+K66srDolLTe/rLrBra2wUSoqNUHfX0TZra3AOzhT429MTNG4s8o7LzhDPTc7Ss2yrbbXz8rG
+1cjGta2tts5j2e9nSElr529EOjQyMjQuMTxLRD9jQzkySG/nUdHnwLvnsOs8raysQSvNv66t
+rqysrLM0LUxVOTNXya2srMHjw7lRNzlT193vTz9DPS8mJis0PkxjyL282V1LV2PTwLW0t8nj
+a1U+NTg+TU9MSkNGQTw5SOvf39fOwsjvb93ZzdPBure9uLW1uVXbvrPOW9+trKxAd8a9Slf/
+s665TDQ4RSgjIy1FS13jwLm84zs5R1E5Nz/ZzNn3W93nXUFD2cfKxbu5tL3My7+5wM3Py9f3
+WUs/Pzw6ODk1NDY+Pzo/T29j38zNzL/IzNPOytPRx7m8w7aztcnMwsrTvclnzK65SzdTUTo0
+Ok7JyVE1PUs2JictNDQ4Qte9u7u7s7bB09nKzONn3cO/ys7Ju7u9vLzFw9Hj393b3d/3X1FM
+SDw2MTAwLy4wNjc5QUdJT2/j0cfBw8G9vsLLy8G7vby2t7q6usrLxcvTxsv/29PnU0Zv51k+
+RkpEPjwzNDg5MzdBREFFUVNRb+fr39HLyL+8urq6u7q/wsPIzs7O3f9vd19fWVNOTltbV13/
+b2NZW1lTU01LTVdfW1tf///36+/n087N29PV1c/T3dHPysrO1+Pr72dTU1VbT05FQUJCP0FB
+RkxVVVVZY19dWVNMUVFLT1NXb+fd29HIyMXBwb+9vL69vb3CwL28v8LFwL7DztnR3/9ZT09O
+Sj89Pj4/PD1AREdJR0VJTUtPXevf2efd49tnV1dfa19dWWNdT0xLU19nb+fb0c/P19PN0c7M
+y8rDysfJzM7Pzc3R09PR1d/n6+v3a2NfY1lVT09PU05NTk5VWV1fY2/v72/37+vj9+//5+f3
+92NvXWNXVVVdZ11jVVVXUU9LTktPT01NTE1GSktMU2fd0crGxL+8uru8vLq6vLq/vb7Ayc/O
+09vj7/9v7/9nXV9Xa05NTEdTV09OSlFRSE1IUUxZUU5NWV9TU1FXT1dVVU5bVU9OT1FRT09X
+W1lna/fn39XL08nKzc/Oz83NyMrHwMHCwMPEycrJy9XT0dPd3+/na19TSEhPSkhPW1VTWVtV
+TVFZXWNjd2/d3d3r2dXb6+fj/+/rd19bZ2NdW2NrZ/9jY1FZU0tPTE1TS1lVV1tTVWNjd2Pr
+9+fr99/j59/fzs3NzdPVztPj29XO19vMw8vKz8/LztXX387L1dfd2ePb3/d3a+dra2drWVVT
+T0pNSkVGSklFREtLTUZBSEdEQ0ZDSk9TTFNnY/9n49nj2dHTztHM1dXTzNPVzdXX1c3X1dHM
+ysrGx8XHwL3Fxb6+xcbJzMfJ0d3Z2f9Za2tZWXdjTmtTOmNMODlTRDk+Pj46PUI+PkJFRz1T
+90VCVdldP1/LRES77zpZvU1O28XIX9W418a528y8ysrZvbzfQsex1zi8v29HwVnTzNfTxme5
+y0XBtlPvxf+972fMSP/FTUNNX1c3OUdbOFVGQFtOTks4b/83Rk9LU1fr2VfZwNX30dnL91/r
+VVfvS1Pj39HT0ca5vb7Dvb3Ezd/TzN/j2d/Z2e/v187d/2djUUpBQUFNVUpLWXdrX19XZ+v3
+d1NV919JWe/vUU/31VFb99VO78vRTd/r/03v3V0+2fdDSV/nZ9X/2z1Z3ddDOlu1zndR9+/M
+vcH/07C1xOfXzt1vX0BC2b2+y763tLy+zV1LUUU5MztDTl9nX+fMw8fb72v3Y1dMTW/371tv
+Z29rVT89RkRFRUlDQ01MQ0RTSz5NZ1lNd93jycTAwr/DyL21xMG/xdu6xO9nys7RxL5nT09n
+b1l33T1300lMSU5vT0hfa0ZT687V/3dOS2fZz9/Jwb7IxN1ZV1lTR0FXY11ra1NNTldnSlFV
+UU9Zb/drZ9tXTld3d+ffydnKwL7JxcjM6//f32/d093X18rO293r59fX329ZTldTRltZT0NJ
+V+fVzNlLR9HByNtbPTtO00lIUVdf78zNV0hdVztK12NBPzk4O9vCz+/Hxs7MyN0/RGdTSN/L
+x720r7G6uMDNd91nTk7n2dvIvcnOy87nX2tbSk9fZ//f1d////dTUU1ITk9XVU9VXVd3b1lV
+WV9OS2P372N3W0lOX1lvb2ddY+fv/+dnTFFRU1fvU05rzMLAy1tLV1n/02dn/0RL08zZWet3
+Su+5usXj905fv7rMa9vHyL67ushf/9tv68j3RU7XT07XXT06RPdCTvdKP2fr/2tV72dVxsLn
+59nL0c2+v+PZztvj49vvTFnda2fva19bW1lfZ1tr6//jW29fa2NLS13r911b99lnUV9MVdfn
+V0ZId3fT38dbLEu1tetTOCkvPbutuLPFUz1O0f8uKjEtQ8asrLK6uMvEvbpRNzQ6P3e8tsLf
+187O08bLXURMY83AtbW7xMbI2etjX0M9QU5dd+//Q0JHSEVLS01JT//3Z2PvXVVfb2v338//
+Y2v3V1tdW09MX2NTW+vb6+PMzetZZ1tEXdnZ793Z1edf39dd08bX6/9ZUefNwb53SFFOTtPM
+y9VrW/fvxsLdU0xbXd3HyN9PW13v5+t3W0pXWWtZa2/v59/n62/31dv36+tbWWvv2/dj0ddj
+X83jd03f1d131XdKTnfR6+PV/1NEV/ffy8XZY/9bW1tj6/9TV3dbT1XIzGNHPj08P2PKy9fb
+/1NJTD88PD5Gd9fFvsLFwMXGycvnb1dbW//v7/9n7+vb29PV28rb1+Pf2dHOzcrP1dvX09/f
+73drW1dTWVdfU1NOUWPn3dvP31Vfxcv//9XrP013/1n/2/9M//dvSe/Hb01EZ2tX58PCyN9r
+SEtEPjo6PDs6P9/Czd/j19nV53d3WVlbU1Ff19nb3ePRy8jCzc3Oxsa/vry9vb2+zNvX619P
+SkY9PT5APkZHX1lr79nOzs7X3eN3629va3dZ3W/Z79/j9//n3c/dz9XT28/b0dfVd11OXU1M
+SU5CQEZVS0dPX0tTZ91379njWWdr72vv5+9ZU2P/a+/n5+vX0crMzs3X72v/W09RTkxGS1tr
+X3fb5+/V09fV2c/Z29nTz9fX529nW19TUVlfV0/r29PVxcfOyNHN08zDTMzN72/LZ2NFy2ND
+wkhbTUdMOWdZTlH3XWvN5+vT2dfr411fWVtba/f/d9vvX9/j4+fd1+f34+//6//n71tZX01T
+TVFRTFtbV1tRW1lf6/fr1dnV0czV29vX/3f3Z+f/529r92v///d3/+fv5+/r7+fd5+/v32P3
+a+f/7+P35+Pf3+Pd52/v393Zzs7Xz9fNytHP0dVr5/dTU0Tf0z9OXz88PmtVTmPXU0t3129f
+a2NHR0hdT0NTzFFd2dPGPm/GX+vbyt9L69NbT+vrW/fNytHTzcfKzr/Dzc3T29n379drV1dd
+UUxRU0tESk1PVXfr7+fZ19XP08nN1c/Z4//v52t329/j99vv//ff7+vf1+fn7293X2f/W09n
+WWNfXWddWWNfVVtTU11TW1dXX1ln/2vn4+vZ1dnO1czPys/M19Pv7/9fV11fZ2dr/2//Y3dn
+Y29jV11dY1dnW2NfZ19bV1dVT09MVVFNW1tjZ293a+/n7+/b2d3f39HX19fX19vf0+/n3dXV
+2c3PzsfNyMnHxcTIyMfIyc/R09XZ3dvn5+/r92dnWVtdV1VXUUtKS0VFRURES0pJTEtLS01J
+T0tOTlVRV1NXT09OWVVXXWN3b/fr2dfZ2dnT59Xf2+fX9+Pf49/T5+Pd39vV19vT3dXV18/O
+0c7R1dXO183/3dHZ3+vf73fr3+v/73f3Y3drY2NnX2ddWWNbd1tra2djXVlbWVtZW11jb/f/
+72//d+vr/+9vd2v3X/9n52fv6+vv53dvd29v7/ffY/9v/2dv71/37/fv73fr/13d/3f34//v
+//drX1t3WVlvX1dbXV1bXWtnZ2dja/9fb3dn9+t3a3f/b+vv59vd39vb3+vja2Pd/2d3//9r
+2+P/387r1dnP0dPZ2dXf793X59nP987Ta83Oz9fr6+9n62NfXV9VV1tOTlNVTE5OSllRWU1n
+Z2tr3+9399/v5/fn59Pb3+Pv2+Prd+fv3/9j91lfW1NXX1FfV1lfWVtZU/9dW+db2e/329fZ
+3dPT19Pf0dvR19/T3///d9v///9Z93dra2tdU19RV01ZT1VXV2dra1tnd/9369/n2efn1dnV
+19nj793jd9Vnb+fj6//fa2//7+d33Xd34/d3Z+9r3d/V0dXr39Xf3dtv69/jX2vn5133d2f/
+b/drd2dnX2NPW1lZV1NnU09RU0xPVU1VXU1XXU5VXU9rWVnv5+Nd3d/n/9Xf593v79l33ePb
+3dPTz9PT1dHZ1c/P78/b4/fn711nW11r/1tv62//Wevva93rd+/P29/b39vb39XZ19XT59vn
+a3dvW05bV1lRS0pKRklFTk1NTFn/d/drXdNrXefV79nR28rL18hnx9XvxUc/vfdBv8A8wtc9
+U8xdRtXRwi+7PWu7OMa1QUq5zji2OErXYzmwPzy2ON9b50xT389VSsRL42/fSknr71lGVb5K
+33fr69nn1VvZydfn28fPytnT293Ozevn2+Pd/1FTUf9vTVNv/+/dV+PZ0ddVVWfKZ2tv78pb
+QFVrd1VCP87May9bvsLTd0hRzs3/RE3IwNtfb//T109N29lVRNPG5zs4Pm/ZwcjIv7a9WT4+
+Pzc8Oz1MvrO0vrmvtMlVRD45Ojs+W7/Da01TSzo1O0z/0c7Iure2v9fVuMvOxc9d50vLU9HR
+a0DNSVdOvtlL57XBLi+urccpJkE+wsnO262tyTYsNSwkJSguR6+tt7utrcTnTD03SNnvTr+2
+vetbV05Z289v78W7wsXCv7m3vstfR0xGPkBfd/drb1NBR009SE7vY1PVwXc/Nz333z5frqzL
+Rcm760pXut/Iuss9tKywMy9Hb++709m+t8kvJy06Kio9Q125rLxN2bbMR0tdX9O+utXRu7nv
+U9vVb8jCwd/Mvr/Z59HfX0pBOC4zNzo6Pj9FSD8+Nzk6Pzpd38nbz7y821XLra62zF27rq2+
+W9m2wL6/viv3razDLSlbTsvTzWe0rro0LC01LjEwMTu6rblJT7/E1VtZRG+8uNVT37/FV09f
+787AzdXdxclvRUZNS2fv91tdV0g6NzU3Njg7REpNU2NnY03rxsnGwcK9w7+/vlnZtK2vss1j
+ObyvrVE4vK13NEe6xc/J2Tk5RVNATOs8KzdLPjVbxl9DT189NU/XSkvOtLu+t7TAvb6818/E
+v+PXzsT/U05LRFNv/0tNY2dRVUtBO0zrazs6P1FVb0k+SGtZPj/PyNl3y8VrWcC5ubG3wFdb
+yLe5ub7ZO0fMr733T10+NzzZydvRbysszq9nLCYuPe+2t+9jv8Y3LjVIP07O02vCra/Jzr/d
+3cu9zc69u91RUVE/SltbPUprd11rW1lVb1tXT3fO01c+TU5nT+PrzMrH0/9f/9fR31/TybjB
+Pzhrr7PBRT7rt7LROUHJvj41O++2rsk0JyxTz99ES+/D11szOE1rZ1lPd8XJU0NASufOzm9b
+2bq4w8fGxsnK51VR48HIWVFMUVNJRklP6+9b9+vfy8zPzdvX2cvN6+v/Z3fnW9nrzONdSlFZ
+xMZbO0zP20NLZ105O85rPUDDTVtR2etX2bo117PBLzBbrsvBwsVVybe7Qk53d0VVV0ZA09dn
+PVHba13/UUI+VeNXRk9fVVlv42/v18rT0869wMPJzdXOwsHZ09HXZ2vv419bY0pHU1lDS0xf
+b1/v70Zj38nM68zAz8Nra8PHsrlny9fH01XIvlFvS9/jzclJLTnVzGc2Liw5a9dPMjtNPTlD
+zkZRXVE2PFvM/9u+vb28usjb073H083L08jCvsvZ1dFfTU5PW1dTW1NAXVlvT0prX2fPWVdf
+zbrBd9vPz81RRFnbvLXdU1XV5+NPWV/V72M4S9/nVV1X2T9Fw81TWXfDvk5jxcd3vlVMSsy5
+zOvn2V/jW+/dw+vdRUtvb0rb183jWUpFOz1nUWtVRktVT3d3WV1TV8vNa/9n3+dfTk1K/2v/
+12tnyt1Tb1nvvsjO1+/Z690/48fB1XdEUWNXv80+z9tvxFHj2e+7wz5ITOu4wf9dW8+497vR
+/3dd/8F3vb1XzcxXd8e/yD9ARj9K93dnTmdPOjg+ODs4OUBGXc7ZY2d3ysTBys/Pz9HPzcnJ
+ycbI59/Mb9Nba+9jW+/Z33f/XU5ZREhTS0VGP05KRFFEO1NnWV3fzN3DvMtJWb++29Oyw0NZ
+ycK2vudLTve1XUHOucTnQUNTXcO6w8fE31FDOz4+Ql9X99vd599339njyctRVWNnXUVGU0hN
+X0tETnfbV05VW+PVz9vvz8HRWWf/73dM/+tPWeNbQU1nd3dfUevra8e93U7vxL/rRNe5w9fK
+//e9xeNnV13Hzl/LPjbTt2tFOjpn42PJ20zV401GPUxXU09KXW/Nv87Pyb/Gy9tn0cLI02f3
+193v6+93a9fT///3d9HT493b39nvVU9nb1tNRkBGWUpDPUdPTENHRkpHX2dVTOPZUVm7ylXV
+sdtv97fnz765d//Tz2/P/2//xdlORszF6z/bZz4+T0k/P01LRkNKS1Nfa11399Pd2dfT2c7N
+0dPdy8rGxcrMysvMzs7O0cbJ3d/r09/f3/f3b+tjX2djZ29rX1NdZ2tvXW9379nvX13r61tZ
+U11RVUxHSUtOSExFSElLTEBKSl9bW2tvY/f/4+9d53fj3evf39fR09vTzM3R09/d1cvd1c3X
+1cvZ49vT4+fn49Xj3efR3e/d2ffvd2Pn//fvb29v/11jU1VVV1NdUWv//1n/a+tn/2tf5+PZ
+5/ff5/9v9/9dXXddY29rY/93X2vv/2tna/djY1tn63f/d9v/b+9n72//X/fr4//v73fj//f/
+/2vn/+Nf42/d/+P35+ff//93Y+93/2df7//dWV1n6/9rXVvrd+t3593j73fvY2v/X1dv/1VV
+X+9jZ/9vb//rb29v2+vf2c/r3c73593j79XVXefA7+PDyt/E41vAdz/b02dT019I22NFztdn
+a11r92dPX/dN611T/1fnY2NM10/vTUDBOk9fRlPTPl3nOsrFP1Hda+P3791Jz8FdXd/Kz9X/
+/9PD/1v32ePr9+/TzdHV69nM2ffVy9P/09fb1///3+/j/19j1+9MTdnnVdvjW0ffX01R3dFN
+V29JTstRS+vIdzpV0T9Pd91TVcm+S0q9xks+WbzJVUndz9NfSMC9w3fr0c9ROj5J7z09RM/T
+XVvby9vI1VFr2fdHVWPvSVvRXU1d2c1v49XN68vI5+fFv9/VxcXT0c/M513Tb19b929RTmNK
+QFNGTVlRU05F62tNUWN3XWvbSVPOU09T2c1jS2PXy0fKu79fSsy/d//V0azRSGe8wWNDz8HP
+y1nZzdNdR2e/S1Pf70tfV09Cb11BSO9dQERvRj5TTk5KTF9LT13v29/v1ev3VVf3b2/j529Z
+z2P3z8pX78TVWdu+3+vf0cDM/92+zv9Pz9HnU8nL61vXx+fdQkvfW0FI3czZ3/fvY11da09n
+RXfP30df/9lR4+9vX+//SVljW0//a1Pfxe/vzNHG0dHfzdfb2c/37+N342tv/+NXVetjV1FT
+W01GUUljX1lT7/dvTevX21tdzc7j683N09fP1+Pf905bT1lIU//3Tl/Vz01v0e/3VVvIzEhb
+93dTXedrWbzTTNvB30j3w8Rnv8fZ1clrb9vG/0nGykpDa0hBT04+X/9NRlfvb2tV62ddSEl3
+Y0tj3dfV1dHbyt/r09PPxN/XyL/L2cu+71PO0eNbz8znd9dd3ddfW+fdV1FfVUZjVUtO/08/
+Ve9HT11VW9XvV2fMxetHUd/VQj9b3UxIR1XTd1Vb49f/Rf/P41Vj2d/jXffP0ev35+t3d1tj
+z+v/28rK49nTa8C/7/e/ws7Gxt3Jxl1jxM9V38xVb+9PTOtPR0ljU09XWU5nY0lf711T93dV
+XV1OXVljWWPb/2fn2dXn68zN3dvb/+ffZ2vn52tf72tRW3dbW11Vd2tOW/dTWXd3W+Pv7+vP
+9+Pj53fj419n4+N3Z+/j9/fr6//nb1vv6+Nv79/TZ//j7//n6+v3z+P/59nrb9fr79/f4+/d
+4+vf2+vv5+tnZ3dvZ2tnX19nZ133XVlTVVldZ2NnW113/2tv72f37+d349nn3d/V09vb3/fr
+a2Nra1lZa19nX19TU19PT1VLTVVbUVFVWVtZX1dvd3dr3d3V08jPzdnP2dfR19fT09fb2czX
+3czX2c3d39nf7/fj5+/d2+t333dvY1NXU11PV19bVVNrV2tbWVlvX3dd6//v63fv9/9f71/f
+a+//3eP/92fd399v79nv9+vra2//a19TY09ZV1ddT1FOVVdZX1/vXf9fb/dv2f/d09nf09fX
+3dfZ1+PR09PR09ff3dPj1evb19v36+/v4+fZ3e/va/9jZ1VbWVVPW1lXWV1bZ2NfX1VbS1lN
+U0lJWVVNXXdfb3f3991r2e/P1dfX2dPj29PV09vT3dPbz9/Kz8/dzs7f1+fj3+PrZ3fj7/93
+Z1tjUV9LVVVPUU1KS01LTUhNVUxTUVlTW2dZd2tvZ2Nf7+dv39vjz83O0cjNztXI2czTx9PK
+ytPL1dHbzc/T19fn7/93X2djX19XV1VbT1FXVU1NTldRTFFOU1VZY1NXb2NrW2Nbb2vr/3fn
+Z///7/fj59/b4+fr6+fr5//P29XN1cvjz9vZ2+/P2d3d29XR39n/7+vn72P3b29db19RZ2NP
+Y19dX1VdZ1lZVWtfd1lbY3fvd+93X/f3Y/9v//9vWd3dd//jb2trXVtfV11fY1n/WU1vWV1b
+d2fv493j1dvR1czdzs7PzM7HzMnb3ePv39X/z9nf29/33efjd2fnZ2tfd19fV3drd1dna2dP
+VV1dV1lZZ2NrX/9dTedV/2dX51lnX2f3Z+P30/fn9+9f591rb2PvVV33a/9r7+/ra3ffb+f/
+d+P/79nr3czVd83Ry+vT2+Pd39fv5+vn2ff/429n999f/193Z+/rb/dvb+939/9RWV13TEtr
+U05NU1NXTENTWU1MSUxdd1NZ9/fr/93V19PN08zP2c3j18/X48jN3czZ0dnR1Xfr7//n5+v3
+62vr53ff4+vj3+dv91lrZ/9nW2trZ0pdTldRd11ZTlNnUWtbZ11nW+//W1dZU/9P93fv33fr
+42tf2evPb+fv5+dn42ff39vnzdfj/3e6Vb1r39XZ1dPnzWPHa+dj2ePfSe9Na1v/THdZY2P/
+411rY2dPVevvZ+v/b0vV30TTY3f3/2fbV+Pnd2/rV9FdY1t3Tk93X05b61dnd//nb9/O1dPJ
+ysLJycfL383F1dfP1+Pb59X/0//j51vZZ2tjX11ZX1dMU0NJRkdbTVdKd2NjVVFOSVdTY01r
+Z0/jY+Pv0+vfyevf79XfVclv41X/W0/vVe/va+fj02vvzkrNy1vf58tR1cJvzcrv38zNzMzK
+0b/b6//Xb/fE3cX/08pJwP/b51lbTe87Ttc5/0NExi652y+0Lsn3N8tLVzi9PTXEP2/nQ11b
+W81Ld8VB18w2x0TA2U9r0dXT9/fbxE7f72PFWc1Jz8vT79nD3cDDZ8S668P/XbvGTcPdv13/
+x9Pnx+tXy2vRTzr31ztM7z5vWUdTQ0zbPDjOU0HrTlNNz1VdTffvWW9N7+P/R2fd2U/OTV3Z
+W0dL31FH68tZ0d/P2cr30f/M/2PGy8rC48i7ycBfu71jy9tjumvXzP/Ld1fZW1XdS0/nUU9X
+X01bQ9FTPf9Fb1tCb0lX2T1X0VHbV0jnU81KP75RY8rrZ8LZ7/dZwWtOv/drxv9bu0y5wky6
+TffXPtnf/+PMUW/Z//dCWVnfW1NDUc9v4+dVv9dVZ1Vv2UZN6+/VU2Nf/+t3S1dv72dGXcx3
+a//j2//3Z3fjzlNO61Pb70xr41ffb0/ba2vfd+PK1c3CztnX19nX0cbRz8fVxcPbydfZ083j
+3c/jW1vf49/P1ePf1+d372dbS01LSj9MSEVHSkRKTERESEpDXUFLTUlHRk9ZVU1nRU9bVWNb
+3/fv2ev349v/0+dr49PTys/RzsvXzdvJx9nH2cjLzc/Rx8vKycvIzMXOyMPIxcjPzdHb2evX
+02/jZ3f3VWNrWWtdSE5LS0tCR0dESUpISEhHSUBIRkhJRElMSkpNSFFRTE5KVVFRU2Nv2e/v
+zdvO1cnLxcfAxMPCw8jEyNXG2c7ZztvVzdXT0dHPz9PZ19Hj09fb3dnf3+//d29TTldOSkVF
+Q0dHR0hCSkhMS0lVTUtOSU1OWUxfU2v3V//Z29HZysnIycHIvb2+v77FwMXJz9PX6/dbb1tX
+WVtVV2dnUWtfWfddXWf/6+dn7+fv5+9n32tnWVNZVU9NT09OT0dZTWtZU1tTXVVRWVNTV1FV
+XV9jWVl3b///b2d3a+vb2cnXzsvOzMzKzcvFzsXDysTHw8rKxMzK0dHT39Xd59vr3+P32+Pn
+32fdb9/r69vv79/v493v7+9db1tjVU5XS0tKSkVJRUQ/QD89QTw/PDs9OzxAPD5BQ0RLS1FR
+W2dd79nVyMPCv7y+vby7vLq9ury4vb25vb7AxL/DyM7MytXZ2+vj/1tvV11ZWWNdb29TW09f
+SltNTFtjVVNTSlFKRExISkhNSE5JS0xLS05MV1NfV2tfX1VbZ2v3/+fV39Prys/d09Xf39HV
+3d3KycjT183f0+fj29fT3ePR4+Pj29/P59vv3+/r59vr1d9r9+//d99Zd1dRVU5dTndnd2Pr
+Z+tjY1lTU01rUU5ORktPRUpNS1lPU1lMb1lbVV1vU2tv72fn2evZ0dPNzcnPzcnKxca/xMC+
+vr6/xca/wczIzM7P3dPf/29v92dna1VMU05DSFFRQUtBQT8+PEJAP0E/QURGSUlNU1VdZ1ld
+99939+PPd9Pd3/fP38/b1dvXzsvPzNfP48vj1czZxcfIy9PAxcbKxs3Rz8jH2dXT49tf/+db
+d3dnY13rTWtRWVlDW09MTUhJQUJIS0ZJSU1ISEtKT1tPXUxfTVdXXV33Y2v33f/X2+v/39t3
+zd/X3eff1d3Z3dPv6+fv3dvZ18jHy8LIx8jHw8jFxsfNzuPf23fZ3fdr/+9fWVdVU0pRS0hP
+R0pBS0tEVUtTT1lITE5RT2NTX29nb+v32efTz9HIysvRxsjLzcvJys7Mz9XT1dfd3dnj28/d
+3e/f9+tbXVVTW0hPREtIRlFHT0xPSklJQ05GSVFPTl1RW1tfV+tvU3dv1eNr99/r19nZ0dXb
+ys7VzMnKysrMzcvR09nX0+Pd1c/T0dvj593Rb+PZ6+fj5//rW2/vZ/9jZ2tjb2Nj73d3a2tr
+d2dbb3dnWWdV73dbd1FrUVlMWU9fVV1dW1n/Z/9fa19VWWtbb19V19dd4//XzG/j60fE29nJ
+VdXDVczZVdvF/93bW9tOY9dPb2dJV1FOX0RX/29rXW/v5/f3593Z0//XzufRztPnY89n5+tr
+3e9v3+Nvd1vfb2/dY2Pf3e9rW3dvWetrVWdvY13rZ2tr2Xfb4+/V791j7/fv3f/rd/fnd9nr
+a+td//9Vb1dn429fZ3fra3dnd/djW1XrW/93X293Wffjd2PnVXdnd1f/a11j92ffXXf3791v
+5//n32P3a+Nj01fO/93P99fb2+fT2d3n7//d5+fr5+Pf3czZ2dPT19HN3dnX6+v/a//3Y3db
+V2ddXWtXd2drb1lRWU5bYz9IW1VXd0j/W1VvVVvbTmtOY1F3WU5XW19vV11fV19bV/9rb9vX
+69Pd29fV39HZzs7NzcrM1dfG29PM29PZ38jT68fN38hZ68lN01HRT2vnWdVX91NZZ1N3b1tv
+T2NMX1H/UVVbX2tVZ2NPVVdjWV1OWVVjT1VZWV9Zd2Nf7/dv49lrzslnv9fFv9nr38nvyt3f
+zmfOXd1d//fX4113V3dZ91f/71ljV1dXZ3drY19nV09XW19ZY1tVWW9bWWNnY29j42vj1+vj
+zMrOzGfna8JVvlfZ3+93601vd//Td29jd1FdX+/352Pr//fv7+/Z2f/j6+/v/2v/b3dVWe9f
+a2NjX3dvX+N3d9vZY9XXWcdZxshT3WN31ddEyU9O7/9ZX0RZXWt3V1tPVU9bTmtdY3dba3dr
+79nf29/d5+P/a2dr62fr53ff6+vr3d33z9nR19Pb2c7bz83P593v2d932/9vY03n12dN70jv
+//9O70ffRtlX/01bZ05ZX2NJX0pOX2NVXVdTZ2Nv72P35+fr1dvZ2dPR08vR19PM49XT59//
+4+vnb+vn/+fv52/ZW+Pvb/d3a11na+9nb+dnZ+//a29rX2NZa1tZY2Nfb2f/a2/vX2v/91//
+//f/4+Pr6+/v4+fj7+vdd/9vb29rd2/3d3dX3dnvX19P49n/0WNr32dv/1nv3etvb/fr29vX
+49/d1+/v3efj92tvZ3dvb3d39/fv5+/r7//j6/9db2NRZ09nXV9ZV11rWVljb193Z2tfb2Nv
+a2tnX2N3Z+vj79/r3d/Z1c/R087bzdvZ2+Pr28/d3d3v7/f/92Nra1tXXWtXWV1ZZ3dfZ29r
+Y2dbW///929rb2/rd1trb29X/1ddXVnvX29j71/ra+fn/2933+9v29HT30+/zdfZ3d/Lzr/f
+0c5ra9nV1+Pj3Q==
+
+--Outermost_Trek--
+
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Digitizer test
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29550; Thu, 3 Oct 91 13:04:23 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19374; Thu, 3 Oct 91 13:04:04 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12278> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:01 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08969> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:03:59 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:03:59 -0400 (EDT)
+Resent-Message-Id: <YcurSj60M2Yt8Ta24=@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:03:59 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <sau@sleepy.bellcore.com>
+Date: Fri, 24 May 91 10:40:25 EDT
+From: sau@sleepy.bellcore.com (Stephen A Uhler)
+Message-Id: <9105241440.AA08935@sleepy.bellcore.com>
+To: nsb@sleepy.bellcore.com
+Subject: A cheap digitizer test
+Cc: sau@sleepy.bellcore.com
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary="mail.sleepy.sau.144.8891"
+
+--mail.sleepy.sau.144.8891
+
+Well, here's a sample
+--mail.sleepy.sau.144.8891
+Content-type: image/pgm
+Content-transfer-encoding: base64
+Content-Description: Bellcore mug
+Comments: 256 x Image wrapped by /usr/sau/bin/fetch_video
+Date: Fri May 24 10:35:57 EDT 1991
+
+UDUKMjU2IDI0NAoyNTUKAHaPj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5uCaWl2dml2dnZ2gnZ2goJvfHx8aXx8aWl2Y1dKMTEx
+MT5KSkpKV1dKSkpKSkpKSkpKSldpdnZ2Y1dXV1dKV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKSko+Sj4+Pko+Pj4+PkpKPj4+Pj4+PkpKSko+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTExMTExMTExAAB2iIiIiIiIiIiIiIiVlZWV
+laGVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurq6urq6uqePdnZpaXZ2aWl8fG9vgm9vgnZpfHxpaXZ2Y2NjRDExMTE+Pj5KSkpKSkpKV0pKSkpKSldXaXZ2Y2NjV0pKSkpK
+SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj5KSj5KSj5KSko+Pj4+Sko+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMQAAdo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6en
+p6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShlXxjY3ZpaXx8aXaCb298fG98fG9vfHxpaXZjUEQxJTExPj5KSkpKSkpKSkpKSkpKSkpKXW9vb29dXV1dSkpXV0pKSkpKSldXSldKSkpKSkpKSkpKSkpKSkpKSkpKSkpK
+SkpKSkpKSkpKSj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTE+MTExMTExMTExMTExMTExMTEAAHaIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u
+rq6urrq6rq66urq6urquro98fGNjdnZpdnZ2dnaCb298fG+CgmlpfG9jY1c+MSUxMT4+SkpKSldKSkpXSkpKSkpKV1djb29jV1dKSkpKSkpXSkpKSkpKSkpKSko+SkpKSkpKSkpKSkpKSj5KSkpKSko+PkpKPkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+PjExPj4+MTExMTExMTE+PjExMTExMTExAAB2iIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6rqGVgmlpdnZpdnZ2aXx8aXaC
+dnZ2dmlpfG9vb29KSjcrNzc3RERERERERFBQUFBQUFBQUFBdaWlXY1dXV1dXSldXV0pKSkpKSkpKSkpKSko+SkpKSkpKPj5KSj5KSj4+Sko+Pj5KSkpKSkpKSkpKSkpKSj4+Pj4+Pj4+MT4+Pj4+PjExPj4+PjExMTExMTExMTExMTExMTExMTExMTExMQAA
+doiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrqurrq6urq6uq6hj29vb2NjfHxvb3xpdnZ2dnZ2aWl8fGl2dmNjUDc3JTExPj5KSkpKSkpKSkpKSkpK
+SkpXY2NjY1dXV1dKSldXV1dXSkpKSkpKSkpKSko+SkpKSkpKSkpKSkpKSkpKSj4+Sko+PkpXSldXV1dXV0pKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMTExMTExMTExMTEAAHaIiIiIiIiIiIiIiIiVlZWVlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhoa6urqGurqGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqulZV8Y2Nvb29vfG9vfHxpdnZ2doKCaXZ2dmlpaUo+MSU3NzdKSkpKSkpKSldKSkpKSkpXV2NjY1dXV1dXV0pKV1dKSkpKPkpXSkpKSkpKSko+
+Pj4+Sko+PkpKPkpKPkpKSko+SkpKSldjV1dXV1dXV1dXV1dKV0pKSkpKSj4+Pj4+Pj4+Pj4+Pj4+MTExMT4xMTExMTExJTExMSUxMSU3AAB2iIiIiIiIiIiIiIiIiIiVlZWVlZWVoZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urq6urq6urq6urq6urq6urq6urq66urq6urqurqGIb29vY298b298fGl2dnZ2dnZpaXxvY3Z2Y2NXNzclMTE+PkpXSkpKSkpKSkpKSkpKSkpjY2NjV0pKSkpKV1dXV1dKSkpKSkpKSkpKSkpKSkpKSj5KSkpKSkpKSko+Pj5KSj4+SkpKXV1dXV1dXV1d
+XV1dXV1QUFBQUFBQUFBQUFBQPko+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMQAAdoiIiIiIiIiIiIiIiIiVlZWVlZWVoZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urrqurq6urq6urq6urq6uurq6urq6uq6b
+j29jY3Z2aXZ2aWl8fG98fG9vgnZpdnZ2aWlpSjc3JT4+PkpKSkpKSkpKSkpKSkpKSldXY2NjY2NQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERERERERERERERERERERERERERERERERFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERERERE
+REREN0Q3Nzc3NzclMTEAAHaIiIiIiIiIiIiVlZWVlZWVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6uq66uq6uoYhvb29jb3xvb29vb298b298fGl2dnZjdnZjY0o3NyUx
+MT4+SldKSkpKSkpKSkpKSkpKV2NjY2NXV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKSkpKSkpKSkpKSko+Sko+Pj4+Pj5KSkpKSj5KSkpKSkpKSkpXV1dXV1dKV1dKSkpKSkpKSkpKPkpKSkpKSkpKSj4+Pj4+Pj4+Pj4xAAB2iIiIiIiIiIiIiIiIiIiV
+lZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6urq6rpuPdmNjdmlpdoJvb3x8aXx8aXaCdml2dmlpaVdERCUxMT4+Sko+SkpKSkpKSkpKSkpKSkpjY2NjY0pKSldKSkpK
+SkpKSkpXSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPko+Pj4+Pj4+Pj4+Pj4+Pj4+SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSj4+SkpKSkpKSj4+Pj4+PgAAdoiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGh
+oaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6hgm9vb2NvfGlpdnZjdoJvb3x8aXZ2aWl2dmNjSjc3JTE+Pj5KSkpKSkpKSldKSkpKSkpXY2NjY1BQUFBQUFBQUFBQRERQUFBQUFBQUFBQUFBQUFBERERERERERERERERE
+RERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N0REREREREREREREREREREREREQAAHaCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pj4+hoaGhlaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u
+rq6urq6urq6uurq6urqum492Y2N2dml8fGl2dnZpfHxpdnZ2aXZ2Y29vXUo3JSUxMT5KSkpKSkpKV1dKSkpKSldKSmNjY1dXV0pXV0pKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSko+SkpKSkpKPkpKPj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTE+
+MTExMTExMTExMTExMTExPj4+Pj4+Pj4+Pj4+Pj4+AABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGCdnZpaXZ2aWl2dmN2dml2
+gnZ2dnZpaXxvV2NKNzclMTE+PkpKSkpKSkpKV0pKSkpKSldjY2NXSkpKSkpKSkpKV0pKSldKSldKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj5KPj4+Pj4+Pj4+Pj4+Pj4xMTE+Pj4+PjE+Pj4xMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMQAA
+doiIiIiIiIiVlZWIiIiIiJWVlZWVlZWVlZWVoZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6uurq6urq6rq66rq66uq6uuqebiHZdaXZpaXx8Y29vb298fGl2gm9vb29jb29XSjcrKys3N0pKSkpKSkpKSkpKSkpK
+SkpXY2NjV1dKSldXSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTE+Pj4xMTExMTExMTExMTExMTExJTExMSUxMSUxMTElJTExMTEAAHaIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhrq6hrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urquoYJvb29jdnZpdnZ2aXx8aWl8b29vfGlpfGlpXUoxMSUlNzc3SkpKSkpKSkpXSkpKSkpKV2NjY1dXV0pKSkpKSkpXSkpKSkpKV0pKSko+SkpK
+SkpKPkpKSko+Sj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMTExMTExMTExMTExJTExMTExMSUxMTExMSUlAABpiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6uoa6urq6urq6urq6urq6u
+rq6urq6urq6urq6urq6urq6urq6urq6urq66urq6rpuPb2Nvb29vfG9jdnZpaXx8aXZ2aWl2dmNvb1dKMSUlMTExSkpKSkpKSldKSkpKSkpKSkpjY1dXV1dXV1dKSldKSkpXSkpKSkpKV0pKSkpKSko+SkpKPj5KSkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+Pj4+MT4+MTExMTExMTExMTExMTElMTExMTExJTExJTElJSUlJSUlJQAAdoiIiIiIiIiIiIiIlZWVlZWVlaGVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurq6urqurq6urq6h
+fG9vb298fGlpfGlpfHxvb4J2aXZ2aXZ2aWldSjElJTExPj5KSkpKSkpKSkpKSkpKSkpdXV1dXVBQUFBQUFBQUFBQUFBQUFBQUERERERERERERERERERERERERERERERERERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyUxMTEx
+MTExJTExJSUlJSUxJSUAAHaIiIiIiIiIiIiIiIiIiJWVlZWVlaGVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqhj4J2aWl8fG9vb29vfHxpdoJvb3x8aWl8aWlpSjc3JSUx
+MTFKSkpKSkpKSldKSkpKSkpKV2NjY1dXV0pXV0pKSkpKSkpKSkpXSkpKSkpKSkpKSko+Sko+SkpKPj5KSko+Sj5KPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMSU3Nzc3NzcrKysrKysrKysrKysrKysrAAB2goKCgoKCgoKPj4+Pj4+P
+j4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6d8aXZ2Y298b298fGNvfG9vfHxvb3xvb29vXV0+MSUlJTc3N0RERERQUFBQUFBQUFBQUF1dXV1QUFBQUFBQUFBQ
+UFBQUFBQUFBQPkpKSkpKSj4+SkpKPj4+Sko+SkpKNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyU3NysrKysrKysrKysrKysrKwAAdoiIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq66urq6rq6urq6urq6PgnZjY3xvb298b298b298fGl2dnZpdnZpaWlQPjEfHzExMT5KSkpXSkpKSkpKSkpKSkpXY2NjV1dXSkpKSkpKSkpKSkpKSkpKSkpKPkpKPkpKSkpKSko+Pj4+Pko+Pj4+
+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTElJTExJSUxMTExMSUlJTElJTExJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6e0tKenp6enp6enp7S0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSnm3ZpaXZpdoJpaXZ2aXZ2dml8fG98fG9vb29dUDclJSUlNzdERFBQUFBQUFBQUFBQUEREV2NXV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj4+SkpKSj4+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTEx
+MTExMTExMTExMTExJTExJSUxMSUxMSUlJSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+Pm5uPm5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oY+CdmNjfG9vfHxjb3xvb298
+b2+CdnZ2dmlpaUo+MSUlMTE+SkpKSkpKSkpKSkpKSkpKSldjY1dXV0pKSkpKSkpKSkpKSj5KSkpKSj5KSko+Sko+PkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMSU3NyUxMTExMTElMTExMTExJTExMSUxMSUlJSUlJQAA
+doKCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeVdmlpaWl2dmlpfHxpdnZ2doKCb3x8aWl8aV1QPiUlJSU3N0pKSkpKSkpKSkpKSkpK
+SkpXV1dXV0pKV0pKSkpKSkpKSkpKSkpKSkpKSkpKSko+Sko+PkpKPkpKPj5KSj4+Pj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMTExMTExMSUxMTExMTElMTExMSUxJSUxJSUlJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubj6GhoaGh
+oaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6uq6urq6uurq6urq6urqhj4J2Y298b29vb2NjfHxpdnZ2doh2dnZ2aWlpSjc3JSUxMTFERERERERERERERERERFBQUF1dXVBQUFBQUFBQUFBQUFBERFBQUFBQUFBQUD5K
+SkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMSUxMSUlMTExMTElJTExJSUlJSUlAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp7S0tLS0tLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5V2aXZ2aXx8aXZ2aWl2dnZ2iHZpfHxpdnZpXVA3NyUlMTE+Pj5KPj5KSkpKSkpKSkpKSldXV1dXSkpXV1dXV0pKSkpKSkpKSkpKSldKSkpKSkpKSkpKSko+Sko+PkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+MTE+MTExMTExMTExMTExMTExMTExMSUxMTExMSUxMSUlJTExMTElJSUlJQAAaYKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGI
+fG9jb3xvb3x8aXaCb298fG9vgm9vfHxpaWlKNzcfHx8xPj4+PkpKPkpKSkpKSkpKSkpXV1dKSkpKSkpKSkpKSkpKSkpKSkpKV0pKSkpKSkpKSkpKSkpKSj4+Sko3N0RENzdENzc3NzdERDdERDc3Nzc3Nzc3Nzc3Nzc3Nys3Nzc3Nzc3NzclMTElMTExMSUx
+MSUlMSUxMSUlJSUlJSUAAHaIiIiIiIiIiIiIiIiIiIiVlZWVlZWhlZWVlaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6urq6urq6uq6hlW9vb29vb29jdnZ2dnZ2aXaCdnZ2gml2dmlXVzclJSUl
+MTExPko+PkpKSkpKSkpKSkpXV1dXV1dKSldKSldKSkpKSkpKSkpKSkpKSkpKSko+SkpKSkpKSj5KSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMT4+MTE+MTExMTExMTExMTElMTElJTExMTExMTElMTExMSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+P
+j4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oYiIb2N2dnZ2dnZpfHxpaXx8b3x8b298fGlpaUo3JSUlMTE+Pj4+PkpKSkpKSkpKSkpKSldXV0pXV0pKV1dKV1dK
+SkpKSkpKSkpKSkpKSko+Sko+SkpKSkpKSj5KPj4+Pj4+Pj4+Pko+Pj4+MT4+Pj4+PjExPjExMTExMTExMTExMTExJSU3NyUxMTExMTElMTElJSUlJSUxMSUlMSUlJQAAaYKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6en
+p6enp6e0tKenp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6ePb29vY2N8fGl2dmlpfHxpdoJ2aXx8aWl2Y1dXNyUlJTExPj4+Pj4+SkpKSkpKSkpKSkpXV1dXSkpKV0pKV1dKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPkpKPj4+
+Sj4+Pj4+Pj4+Pj4+Pj4+MTE+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMSUlJTElJTElJTExJSUlJSUAAGmCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSbiIhpaXZ2aXZ2dml2dnZ2dnZ2dnZpaXx8aXZjRDcfHx8rKzdENzc3NzdEV0pKV0pKSkpKSldXSkpXSldXV1dXSkpKSkpKSkpKSkpKSkpKSkpKSj5KSkpKSkpKSko+Pj5KPkpKPj5KSjc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3
+Nzc3Nzc3Nzc3KysrKysrKysrKysrKysrKysrHx8fAAB2iIiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6rq6urq6uurq6urqnp4hvY3Z2aXaCaWl8fGl2dnZ2
+dnZpdnZ2dnZpUD4xHx8fKz4+Pj4+Pj4+SkpKSkpKSkpKSldXV1dXSkpXV1dXV0pKSkpKSkpKSldKSkpKV1dXSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Sj4+Sko+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTExMTExMTExMTExMSUlMSUlJTElJTExJTElJQAA
+aYiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoZWhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urrq6uq6urq6urq6urrq6rq6urq66urq6rpWCgm9jb3xvb29vY298fGl2gm9vfG9vb3xpaV1KMSUlJTE+Pj4+Pj4+PkpKSkpKV0pK
+SkpXV1dKSkpXV1dXV1dXV0pKSkpKV0pKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MT4+MTExMTExMTExMTExMTExMTExMSUxMSUxMSUlJSUlMSUlJSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWhoaGhoaGh
+oaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urq6rrq6urq6uqenj29jdnZjdnZpaXx8aXZ2dnaCdml2dnZ2dmlQRDEfHysrNzc3Nzc3NzdKSldXV1dKSkpKV1dKV0pKV1dXV1dXV1dXV0pKSkpKV1dKV1dXV1dK
+SkpKSkpKSkpKSj5KSkpKSko+Sko+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+Pj4+MTE+PjExMTExMTExMTExMTExMTExJSUxJTExMSUlMTExMSUlAABpgoKCgoKCgoKCj4+Pj4+Pj5uPj4+bm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0lYJ2dl1vfGlpdnZpdnZ2dnaCaXaCb298fGlpXT4lJSUlNzc3Nzc3Nzc3RFBQUFBQUERQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQRERERERQUERERERERERERERERERENzdERDc3
+Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nys3Nzc3NzclMTElMTExJSUxMSUlJSUlJSUlJQAAaYiIiIiIiIiIiIiIiIiIiJWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq66urq6uq6urq6urq6uurqurrq6p6eI
+b29vb29vfGlpfG9vfHxpaXx8aXZ2aWl8aVdEMR8fHzExPj4+Pj4+Pj5KV2NXV1dKSkpXV0pKSkpXV0pXV1dKV1dXV1dXSkpKSkpKV0pXV0pKSkpKSkpKSkpKSkpKSkpKSj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4+Pj4xMT4+MTExMTExMTExMTExMTEl
+JTExMTElJSUlJSUlJSUAAGmCgoKCgoKCgoKPj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSPgnZpaWl8b298fGl2gnZpfHxpdnZ2aXx8aWlXPjElJTEx
+MTE+PjExMT5KV1dXY1dKSkpKSldXSkpKSkpKV1dXV1dKV1dXSkpKSkpXSldXV1dXV0pKSkpKSkpKSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4+Pj4+PjExPj4xMTExMTExMTExMTExMSUxMTExMTExMTElJSUlJTExJSUlAABpgoKCgoKPj4+Pj4+Pj4+P
+j4+Pj5ubm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShoYhpaXZ2Y3Z2aXaCdml8fGl2gnZpfHxpdnZjV0QxHx8fKz4+Pj4+MTE+PkpXY1dXV0pKSkpKSkpKSkpXV1dXV1dX
+V1dKV1dKSkpKSkpXSkpKSkpXSkpKSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTElNzclMTExJSUxJTExJSUlMSUlJSUlJSUlJSUlJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6en
+p6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tJWCgmlpdnZpaYJ2aXaCb298fGl2dmlpfHxpXV0xJRgYGCUxMT4xMTExMUREV1dXV0pKSkpKV0pKSkpXSldXV1dXV1dXV1dXV1dKSldKSldXSldXSkpKSkpKSkpKSkpKSkpK
+SkpKPj5KSj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+MTExPjExMTExMTExMTExMTExJTExJTExMSUxMSUlJSUlJSUAAGmCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tLSbiGlpdnZpfHxpdoJvb3x8aXZ2dml8fHx8fGlpSkpKNzc3Nzc3Nzc3NzdEREREREREREREREREREREUFBQUF1dXV1dXV1dXUpKV0pKV0pXV1dXSkpXSkpKSj5KSkpKSkpKSkpKSkpKSkpKSkpKSko+Pj4+Pj4+Pj4+Pj4xMT4+MTEx
+MTExMTExMTExMTExMTExMTExJTExMSUlJSUlJSUlAABpgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnlYKCaWl2dml2dnZpdnZ2do+b
+m6enp7S0tLS0tLS0p6enp6enp6eVlZWIiIiIfHxpXV1KSkpKPj4+SkpKV1dXV2NjY2NXSkpXSkpXSkpKV1dKV1dKSkpKSkpKSkpKSj5KSkpKSj4+Pko+SkpKSkpKPj5KSj4+Sj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTElMTElMTExJSUlJSUlJSUlJQAA
+aYKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp7S0tLS0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6eIaXZ2aWl8fGl2iIihtMfHx8e6urq6x8fHx9PT09PT09Pf39PT09PT08fHx8fHurqn
+p5uPgoJvY1dXV0pKSkpKV1dXV1dXV1dXSkpXV0pKV0pKSkpKSkpKSkpKPkpKPj5KSj5KSkpKSkpKSkpKPkpKPj4+Sj4+Pj4+Pj4+MTExMTExMTExMTExMTExMTExMTExMTElMTExJSUlJSUlJSUAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ub
+m5ubm5ubm6enp6enp6enp6enp6enp6e0tLSntLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSPgnZpaXZ2dnZ2j6fAwMDAtLS0p6e0tLS0wMDAwM3Nzc3Nzc3Nzc3Nzc3Z2dnZ2dnZ2dnZzc3NwMC0tKGVgm9vY1dXV1dKSldKV1dXSkpXV0pK
+SkpKSkpKSkpKSko+Pj5KSkpKPj4+Pj5KSkpKSko+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMSUlMTElMSUlJSUlJSUlJSUlAAB2iIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urq6urq6uurq6rq6urq6urq66urq6urq6uq6hlYJpaXx8aXZ2obS0oaGhlaGhoaGhoaGhrq6uurq6usfHx8fHx8fH08fH08fT09PT09PT09PT0+b4//////jfx7ShlYJvY1dKSkpKSkpKSkpKSkpKSkpKSkpKSko+SkpKSj5KSkpKPj4+SkpKSkpKSko+
+Pj4+Pj4+PjE+PjExMTExMTExMTExMTExMTExMTExMSUxMSUlJSUlJSUlJSUlJQAAaYKCgoKCgoKCgoKCj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p492
+dnZjb3xvb7rNupuPj4+Pj4+Pj5ubm5uurq6uurq6use6usfHx8fHx8fHx8fHx8fT09PT09PT7P//////+Pjs7NnZx66hlYJvV0pKSkpKSkpXSkpKSkpKSkpKSko+SkpKPkpKPj4+Pj5KSj5KSj4+PkpKPj4+Pj4+MTExMTExMTExMTElMTExMTExMTExMTEx
+MSUxMSUxMSUlMSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6urq6urq6urq6rqGVgmNjdnZpdoi62dm6oY+Cj4+Pj4+Pj6GhoaGurq6urq6u
+wMDAwMDAwMDAwMDAwMDAzc3Nzc3NzdnZ2ebm2ebm5ubm5ubZ2dnHx66PgmlQUERERERQUFBQUEREREREREREREREREREREREREREREREREREREQ3N0RENzc3Nzc3Nzc3Nzc3NzcrNzcrNzc3JTc3Kzc3NysrKysrKysrKysrAABpgoKCgoKCgo+Pj4+Pj4+P
+j4+Pm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnj3Z2dmN2gml2rs3m8tnAoYiIiIiIlZWVlaGhoaGurq6uurq6urq6x8fHx8fHx8fHx8fHx8fHx8fZ2dnZ2dnZ2ebm
+2ebm2dnZ5ubm5s26p49vXUpKSkpKPj5KSkpKSkpKSj4+Pko+Pj4+Pj4+Pj5KPj4+Sj4+Sj4+Pj4+Pj4+MTExMTExMTExMTElMTElMTElJTExJTExMTExMSUxMSUlJQAAaYiIiIiIiIiIiIiIiIiIlZWVlZWVlZWVlaGVoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+oaGhoa6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urrq6urqurrquoY98Y2N2dml2gqfAwN/s7OzTupWViIiIiJubm6enp6enp6e6urrHx8e6x8fHx8fHx8fHx8fHx8fH09PT09PT09PT39/f39PT09PT39/f39/Tx6eIaUo+PkpKPkpKPj5KSj5K
+SkpKSj4+Pj4+Pj4+PkpKPj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMTExJTExMTExJTc3KysrKysrKx8AAGmCgoKCgoKCgoKPj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6entLS0tLSntLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0tKeIb29vY3Z2aXauurrHx9nm8vLy07ShlYiIoaGhoaGurq6urq66urrHx8fHx8fHx8fHx8fHx8fHx9PT09PT09PT09PT09PT09PT09PT09/f39/f07SVb1A+Sko+Pj5KPj4+Pj5KSkpKSj4+Sko+PkpKPkpKPkpKPj4+Pj4+Pj4+PjEx
+MTExMTExMTExMTExMSUxMTExMTExMTExJTElJSUlAABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKenj4JjY3ZpaXaCp7TAwMDAwNPT
+5vLy8vLZwKebm5ubp6enp7S0tLTAwMDAwMDAwMDAwMDAwM3Nzc3Nzc3Nzc3Z2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZx66CYz4+Pj4+Pko+Pj4+Pj5KSj5KSj5KSj4+Pj4+Pko+Pj5KPj4+Pj4+PjExMTExMTExMTExMTExMTExMTExMSUxMTExMSUlJSUlJQAA
+aYiIiIiIiIiIiIiIiIiIiJWVlZWVlZWhoZWhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6urq6urq6urrq6uq6urq66urq6urq6oYh2dmlpdnZpfK66urrHx8fHx8fH09Ps7Pj/8t/Huqenp6enp7q6urq6urrHx8fHx8fH
+x8fHx8fH09PT09PT09PT09PT09PT09PT09PT09PT09PT09PHrohXREREREQ3Nzc3RERERERERERERERERERERERERERERERENzc3Nzc3Nzc3Nzc3Nzc3Kzc3JTExJTExJSUxMTExMSUlJTElJSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pm5uPm5ubm5ubm5ub
+m5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p6ePfF1vb29vb4Knuse6x8fHurq6urrHx8fZ2eb4+Pj4+ObTx7SntLS0tLS0wMDAwMDAwMDNzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ
+2dnZx7Shb0pKPj5KSj4+Pj4+PkpKPj4+Pj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMSUxMSUxMTExMTExMSUxJTExMSUlJSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp7S0tLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShgnZ2aWl2dmmCrrq6urrHurq6urq6urrHx8fT09Pf3+z4////////5tPAwMCurrq6usfHx8fHx8fT08fT09PT09PT09PT09PT09PT09PT09PT09PT08e0oXxXSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExMTExMTElMTElJTExJTExJSUlJSUlJQAAaYKCgoKCgoKCgoKPj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp7S0tKe0tLS0tKe0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oZV8
+Y298aWl2iK7AwMDAwMDAwMDAwMDAwMDAwMDAwMDNzc3Z2ez////////y8ubT09PHx8fHx8fHx8fH09PT09PT09PT09PT09PT09PT09PT09PT08e0tI9vSkpKPkpKPj5KPj4+Pj4+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMTExMTExMSUlMSUl
+JTElJTExJTExJSUlJSUAAGmIiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6urq6urq66urq6urq6urq6urqurq6urpuCdnZjY3Z2Y4i0wMDAwMDAwMDAwLS0tMDAwMDAwMDAwMDA
+wMDAwMDNzc3N2dnm5vLy8vLy8vLm5ubm09PT09PT08fT09PT09PT09PT09PT09PTx7S0oYh2aUpKSko+Pj4+Pj4+Pj5KSkpKSko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMSUlMTElMTExMTElJSUlJSUlAABpgoKCgoKCj4+Pj4+Pj4+P
+j4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeViHxjY3Z2aWmhtMfHx8fHx8fHx7q6urq6x8fHx8fHx8e6urq6urq6urq6x8fHx8fHx8fH09PT09/f39/s7Ozs7N/f
+7N/f7Ozf39/T09PHurquoaGPgnZ2aVdERERERERENzdEREREREREV0o+Sj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMTExMTExMTExJTElJSUxMSUlMTElMTExJSUlJSUlJQAAaYKCgoKCgoKCj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6en
+p6enp6enp6e0tLS0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oYJpdnZjb3xplbrHx8fHx8fHx8fHx7q6urq6x7q6x8fHurq6urq6urq6uq6urq6urrq6urq6urq6urq6urq6urq6uq66uq6uurqnp6ebm5uIiIiIdnZ2dmNXSkpKSko+Pj4+
+PkpKSkpXV1dXSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTExMT4xMTExMTExMTExMSUlMSUxMTElJSUlJSUAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0p5WIdmNjfGlpdqG0x8fHx8fHx8fHx8e6urq6usfHx8fHx7q6urq6urq6urq6rq6urq6urq6urq6urq6urq6hoaGhoaGhoaGhusfHrpWViIiIfIh8fHx8b29vV0pKSkpKSj5KSkpKSkpKSkpXSkpKSj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+
+PjExMTExMTElMTElMTExMTExMTExMSUxMSUlJSUlAABpgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp7S0p7S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLShfGl2dmNvfG+husfHx8fHx8fH
+x8e6urq6usfHx8fHx8e6urq6urq6urqurq6urq6urq6urq6urq6urqGhoaGVlZWVlZWVlae0oY+Pj3x8iIh2dnZ2dnZ2Y1dXSkpKSj5KSkpKSkpKSkpKSkpKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+MTE+MTExPjExMTExMTElJTExMTExJSUlMSUlJSUxMQAA
+aYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnj492Y2N2aWl2obrHx8fHx8fHx8e6urq6x8fHx8fHx8fHx7q6urq6urq6rq6urq6urq6u
+rq6uoaGhoaGhoaGVlZWVlZWIiIiIiIiIiIiIiHaCgnZ2doJvb29XSkpKSkpKPj5KSkpKV0pKSko+Sko+Pj4+Pj4+Pj4+Pj4+Sko+Pj4+Pj4+Pj4+MTExPjExMTExMSUxMTElJTExJTExJSUlMSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ub
+m5ubm5ubm5ubp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0p5t8b29vb298b6G6x8fHx8fHx8fHx8fHx8fHx8fHx8fHx7q6urq6urq6rq6urq6urq6urq6urqGhoaGhoZWVlZWVlYiIiIiIiIiIiIiIfHx8fHx8
+fG9vb29jV0pKSkpKSkpKSkpKSj5KSkpKSko+Pj4+Pj4+Pj4+Pj5KSj5KSj4+Pj4+Pj4xMTExMTExMTExMTExMSUlMTElMTElJTExJSUlAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6entLS0
+tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKGViHZjb3xpaXynusfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6hoaGhoaGhlZWhj4+Pj4+Pj4+Pj4+CgoKCdoKCdnZ2dnZ2Y1BQUERERERERERERERERERERERERERERERE
+RERERERERERERERERERERERERDc3Nzc3Nzc3Nys3NyUxMSUlMTExMTExJTExJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6enp6enp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSnm3Z2
+dnZjdnZpp8DAwMDAwNPHx8fHx8fHx8fHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6urqGhoaGVlZWVlZWVlYiIiIiIiIiIiIh8fHx8fHx8b29vb2NXSj5KSkpKSko+Pko+PkpKPko+Pj4+Pj4+Sko+Sko+PkpKPkpKPkpKPj4+Pj4+Pj4xMTExMTE+MTEx
+MTExMSUlJSUlMTElJTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pm5ubj5ubm5ubm5ubm5ubm5ubm5ubp6enp6enp6enp6enp6enp6e0tLS0tLSnp6entLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0oY+PdmNvfGlpgqe6x8fHx8fHx8fHx8fHx8fHx8fHx8fHx7q6
+urq6urqurq6urq6urq6urq6urqGhoaGhoaGVlZWVlZWViIiIiIiIiHx8fHx8fHx8fHx8b29vSkpKSj5KSj4+Sko+Pko+PkpKPj4+Pj5KSj5KSj4+SkpKSkpKSj4+Pj4+Pj4+Pj4+Pj4xMT4xMTExMTExMTExMSUxMSUlJSUlAABpgoKCgoKCgoKCj4+Pj4+P
+j4+Pj4+Pm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6e0tLS0tLSntLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKeVdnZ2dml8fHynx8fHx8fH08fHx8fHx8fHx8fHx8fHx8fHx7q6urq6uq6urq6urq6urq6urqGhoaGhoaGVoZWIlZWI
+iIiIiIiIiIiIiHx8fHx8fHx8fG9vY0pKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+PkpKSkpKSj5KSj4+Sko+Sko+SkpKPkpKNzc3Nzc3Nzc3Nzc3NzcrKysrKysrKysrKysrKwAAaYKCgoKCgoKPj4+Pj4+Pj4+Pj4+Pj4+PoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uurqurq6hlYhvY3Z2Y2OIrsDAwMDNzc3Nzc3NzcDAwMDAwMDAwMDAwMDAtLS0wMC0tLS0p6enp6enp6enp6enp6ebm5ubm4+Pj4+Pj4+Pj4+PgoKCgoKCgm98fHxpaWlKPko+Pj4+Pj4+
+Pj4+Pj4+SkpKSkpKSkpKSko+Pj4+Pj5KSj4+Sko+Pj4+Pj4+Pj4+MTE+MTExMTExMTExJTElJTElJSUxMSUAAGmCgoKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pm5ubm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLS0tLS0
+tLS0tLS0tLS0tLS0p492aXZpaXx8fK7Hx8fHx9PTx8fHx8fHx8fHx7rHx8fHx8fHurq6urq6uq6urq6urq6urq6urq6hoaGhlZWhlZWVlYiIiIiIiIiIiIh8fHx8fHx8fG98fG9jSj4+Pj4+Pj4+Pj4+Pj4+Pj5KSkpKV1dKSko3Nzc3Nzc3NzdERERERERE
+REREREQ3Nzc3Nzc3Nzc3Nzc3NysrKysrKysrKysrAABpiIiIiIiIiIiIiIiIiIiVlZWVlZWVlZWVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6uuq6urq6urq6urq66urq6urq6urqurpuIfHxpaXx8aY+6x8fHx9PT09PT
+x8fHx8fHx8e6urrHx8fHurq6urq6urqurq6hoaGhoa6uoa6uoaGhoZWhj4+Pj4+Pj4+Pj4+Pj4+CgoKCdoKCdnZ2dmlpXUREREREREQ3N0RERDc3NzdERFBQUFBQUFA+Pj4+MTE+Pj5KSj4+Pj4+Pj4+PkpKPj4+Pj4+PjExMTExMTExMTExMTExMTExJQAA
+aYKCgoKCj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj5ubm5ubm5ubm5ubm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6e0tLS0tLS0tLS0tLTH09Pf7Ozs39/T08fHx66bj29jb29jb2+PtMfHx9PT09PHx8fHx8fHx8fHx8fHx8fHx8e6usfHurq6urq6rq6urq6u
+rq6urqGhoaGVlZWVlZWVlYiIiIiIiIiIiIh8fHx8fHx8fHxvb1c+Pj4+Pj4xPj4+Pj4+Pj4+PkpXV1dXY1BQPj4xPj4+Pj4+MT5KPj4+SkpKSj4+Pj4+Pj4+Pj4+MTE+PjExMTExMTElMTElJTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj4+PoaGhlaGhoaGh
+oaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6ursfZ7Pj4+Pj///j4+Pj47OzZx7Shj29vb29jj8DA08fH09PT09PHx8fHx8fH08fHx8fHx8fHurq6urq6urq6uq6urq6urq6urq6hoaGVlaGVlZWViIiIiIiIiIiIiHx8fHx8fHx8
+fHx8b2NjPj4+Pj4+Pj4+Pj4+Pj4+Pj5KSkpXV1dXSko+Pj4xPj4+Pj4+Pj5KSj5KPj4+Pj4+Pj4+Pj4xMT4+PjExMTExMTExMTExMSUlAABpgoKCgo+Pj4+Pj4+Pj4+Pj4+Pj6GhlZWVlaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6u
+rq6urq6urq6urtPs+P/////////////////////4+ObNroh8fI/A09PT09PT09PHx8fHx8fHx8fHx8fHx8fHx8e6x7q6urq6urq6rq6urq6urq6urqGhoaGhoaGVlZWVlZWIiIiIiIiIiHx8fHx8fHxvb29vUD4+MTE+PjE+Pj4+Pj4+Pj4+PkpKSl1dUFBE
+RDExPj4+Pj4+Pj4+Pj4+PkpKPj4+Pj4+Pj4+Pj4+PjExMTExMTExMTExMTExMQAAaYKCgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5ubm5ubm5ubm5ubm5ubm6enp6enp6enp6enp6entLS0p7S0tLS0tLS0tLS0tLSnutPs+Pj47Pj47Pj/////////////////
+//jmx6e62dnZ2dnZ2c3Nzc3Nzc3Nzc3Nzc3Nzc3NzcDAwMDAwMDAwMC0tLS0tLS0tKenp6enp6enm5ubm5uPj4+Pj4+Pj4+PfHyIfHx8fHx8fHxpaVc+MT4+Pj4+Pj4+Pj4+Pj4+Pj4+SldXV1dKPj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4x
+MTExMTExMTExMTExJSUAAGmIiIiIiIiIiIiIiIiIiIiIiJWVlZWVlZWhoaGhoaGVoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq6urq6uutPm8vLm5s3Nzc3Nzc3Nzdnm5vLy///////////s7N/T09PT09PT08fTx8fHx8fHx8fHx8fHx8fH
+urrHurq6urq6urq6urqurrqurq6uoaGhoZWVoZWVlZWVlYiIiIiIfIiIfHyIfHx8fG9vb2NKPj4xMTExMTExMTE+PjExMTE+Pj4+SldKSko+Pj4+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+MTExMTExPj4xMTExMTExMTExJTExMTExAABpiIiIiIiIiIiIiIiIiIiI
+lZWVlZWVlZWVlZWVoaGViJWhoaGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6urq6ursfm5ubZ2cfHx7q6urq6urrHx8fT09Pf7Oz4//////Lm09PH09PT09PT09PT09PHx8fHx8fHx8fHx8e0tLS0tLTAtLS0tLS0tLS0p6enp6enm5ubm5ubj4+P
+j4+Pj4+Pj4+CgoKCdoKCdnZ2dnZpSjExMTExMT4xMTExMTExMTE+Pj4+Pj4+Sko+Sko+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+Pj4+MTE+PjExMTExMTExMTExMQAAaXyIiIiIiIiIiIiIiIiIiIiVlZWVlZWVoaGhoaGhj3yPoaGhoaGhoaGhoaGhoaGh
+rq6urq6urq6urq6urq6urq6urq6urrrZ2ebTx8e6uq6urq6urq6urq6uusfH09PT3+zs+Pj45tPT09PT09PT09PT09PT09PTx8fHx8fHx8e6urq6urq6urq6rq66uq6urq6urq6hoaGhoaGhlZWViIiVlYiIlYiIiIh8fHx8fHx8b29vY0o+Pis3Nzc3Nzc3
+Nzc3Kys3Nzc3Nzc3Nzc3Sj4+Pj4+PkpKPj4+MTExMTE+Pj4xPj4+Pj4xMTExMTExMTExMTExMTExMTExMTEAAGmCgoKCgo+Pj4+Pj4+Pj4+Pj4+bm5uPm5ubm5ubgoJvfI+hoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq7H2dnZx8e0p6en
+p6enp6entLSnp6e0wMDNzdnZ2ezs2dnNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NzcDAwMDAtLTAwMC0tLS0tLS0tLS0tKenp6enp6eVlaGVlZWVlZWVlYiIiHx8fHx8fHx8fHx8aWlKMTExMTExMTExMTExMTExMTExMTExMTE+Pj5KSj4+Pj4+Pj4+Pj4+Pj4+
+Pj4+Pj4+PjExMTExMTExMTExMTExMTExMTElMTExAABpgoKCgoKCgo+Pj4+Pj4+Pj4+Pj4+Pm5ubm5uPj3xjV2+Pm5ubp6enp6enp6enp6enp6enp6enp6enp7S0tLS0tLS0tLS009PTx7q6rqGhoa6urq6urq6urq6urq66x8fH09PT09PTx9PT09PT08fT
+09PT09PT09PTx8fHx8fHx7q6urq6urq6urq6rrqurq6urq6uoaGhoaGhoaGVlZWViJWVlZWViIiIiHx8fHx8fHxvb29jSjc3JTExMTExMTExMTExMTElMTExMTExMT4+Pj4+Pj4+Sj4+Pj4+MTExMT4+MTExMTExPjExMTExMTExMTExJTExMTExMTExMQAA
+aYiIiIiIiIiIiIiIiIiVlZWVlZWVlZWhlZWIfG9XV2Njgpubm5ubm5unp6enp6enp6enp6enp7S0p6enp6entLS0tLS0wNPTx7q6rqGhoa6urq6urq6urq6urq6urq7AwM3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NwMDAwMDAwMDAwLS0tLS0p6en
+p6enp6enp5ubm5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgnZ2dnZ2aUoxMTExMTExMTExMTExMTExMTExMTExMTExMT4+Pj4+Pj4+PjE+Pj4+Pj4+Pj4+MTExMTExPjExMTExMTElMTExMTExMSUxJSUAAG+IiIiIiIiIiIiIiIiIiIiIlZWVlZWhlYh8b11KV1dK
+Y4ibm5ubm5unp6enp6enp6enp6enp6enp6enp6enp6entLS0tMfHx7q6p6ebp6enp7S0tLS0tLS0tLSnp6enusfH09PT08fHx9PT09PT09PT09PT09PT09PHx8fHrq6Pb2+Cgpu0tLS0wLS0tKenp6enp6enp6enp5WhoY+Pm4+Pj4+Pj4+Pj4+PgoKCgnZ2
+dnZ2dlc+PjElMTExMTExMTExMTExMTExMTExMTExPj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+MTExMTExMTExMSUxMTElMTElJTExJTExJSUlAABpiIiIiIiIiIiIiIiIiJWVlZWVlZWVlYKCaVdXV0pKXV12laGhoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6u
+rq6urrrNzc20tKGhoaGurq6urq66urq6uq6urqGVp7TAwM3Nzc3Nzc3Nzc3Z2dnZ2dnZ2c3Nzc3Nzc20lWk3NzcrHzE+SnantLS0tLSnm5unp6enp6enp6enm5ubm4+Pj4+Pj4+Pj4+Pgo+PfHx8fHx8fGldPjExMTExMTExMTE+MTExMTExMTExMTExMT4+
+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4xMT4+MTExMTExMSUxMTExMTElNzc3NzclMTElJQAAaYKCgoKCgoKPj4+Pj4+Pj4+Pm5ubj49vXV1dSkpXV0pXfI+hoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq6urq66x8fHtKenlaGhrq6urq6urq6urq6urqGViJWn
+usfHx8fHx8fHx8fT09PT09PT09PT09PT08ehfD4lPjESJT4fHzE+PnantLS0tKGurq6urq6hoaGhoaGhlZWVlYiVlYiIlZWIiIh8fIh8fHx8fHxpVzc3NysrKysrKzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N0RERERENzc3Nzc3Nzc3Nzc3NzcrKysrKzc3Nzc3
+Nzc3NyUxMSUlJSUlJSUAAGmIiIiIiIiIiIiIiJWVlZWVlZWVlXxjV1dKSldXRERdUGOIm5ubm6enp6enp6enp6enp6enp6enp6enp6enp6enp6enwM3NwLShoZWhrq6urq6urq6urq66uq6uj3aClafAwMDAwMDNzc3N2dnZ2dnZ2dnZ2dnZzc2uYys+Ph8x
+V0o+SjcYGDExY6G0tLS0tLS0tKenp6enp6eVlaGVlZWVlZWVlYiIiIiIiIh8fHx8fHx8aVc+MTExMTExMTExMTExMTExMTExMTExMTE+Pj4+Pj4+Pj4xMT4+Pj4+MTExMTExMSUxMTElMTExMTExMTExJSUxMSUlMSUlJSUlAABpiIiIiIiIiIiIiIiIiIiI
+lZWViHZjV0o+V1dKSldXSldpgpWhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6ursfHx7quoZWVoaGurq6urq6urq6uurquoZWCaXyhtMfHx8fHx8fT09PT09PT09PT09PT09OnXT4+MVeVp6e0tKGIdlcYJURXj7S0tLS0p6enp6enp6enp5ubm5uP
+j4+Pj4+Pj4+Pj4KCgoJ2dnZ2dnZXNzc3KysrKysrNzc3Nzc3Nzc3Kzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3JSUlJSUlMSUlJSUlJTElMTExJSUxMSUlJSUlJQAAaYiIiIiIiIiIiJWVlZWVlZWVfGlpV0REV0pKV1dKV1dXV4KVoaGhoaGhoaGhoaGh
+oaGhoaGhoa6urq6urq6urq6urq7Hx8e0tJubm6enp6e0tKe0tLS0tLS0tKePdnZ2gqfAwMDAwMDT09PT09PT09PT09PT09O6dlclPo+6urrHurq6uqeVdjcfMWmVrrqurq6urqGurqGhoaGhoaGVlZWVlZWVlZWViIiIiHx8fHx8fG9vVz4xMTExMTExMTEx
+MTExMTExMTExMT4+MTExMTExPjExPjExMTExMTElJTExMTExJSUlJSUlMSUlJSUxJSUlJTElJSUlJSUlJSUAAGmIiIiIiIiIiIiIiIiIlaGVfGldRERXSj5XV0pKV1dKV2N2j6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq6ux8fHtKebj4+np7S0
+tLS0tLS0tLS0tLShlYJjY4KhtLS0wMDAwM3Nzc3Nzc3Nzc3N2dnHm1AfXZu0x8fHtI+CdqG6uqGCUBgxaaGhrq6urq6urqGhoaGhoZWhoZWVlZWVlZWViIiIiIiIfHx8fHxvb1c+Pj4xMTExMTExJTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlAABpiIiIiIiIiIiIlZWVlZWIfGlXSldKPkpXSkpdUD5QXUpKb4ibp6enp6enp6enp6enp6enp6enp6enp7S0p7S0tLS0tMfHurqhlZWVp6e0tLS0tLS0tLS0tLS0p4h2dmlpj66urrrHx9PT09PT09PT
+09PT09PTumM+V4i008e6x7pjMTGCp7q6oW9KKyuCrq6urq6urq6uoaGhoaGhlZWVlZWVlZWVlZWIiIiIfHx8fHx8fGlXPjExMTExMTExMTE+MT4+PjExMTExMTExMTElNzc3Nzc3Kys3Nzc3NysrKysrKysfMSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJQAA
+aYiIiIiIiIiIiIiIiIiIdl1EV0o+SldKPldXSkpdXUpdXV2Im5ubm6enp6enp6enp6enp6enp6enp6enp6enp7S0p7rHx8eum5uIm6enuq6urq6urq6urq6urqGPgmNjdoKhoa7AwMDNzc3Z2dnZ2dnZ2dnZzY9jN2m007SIb1dEREQlaaGurq6uaSs3So+u
+rq6urq6urq6hoaGVlZWVlZWVlZWVlZWIiIiIiIh8fHx8fHxpVzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NyUxMTElJTElJTExMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJSUlJSUYGCUAAGmIiIiIiIiIiIiVlYiIaVdXV0REV0o+V1dKSldXRFdX
+SkpjgpWhoaGhoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq66x8e6rpubiKGhtLS0tLS0tLS0tLS0p7SniHxvb2N2j5uuurrH09PT09PT09PT09PT09OCN12nwM2nVyU3Nx8fPj4+b5uurqFjJT6Coa6urq6urqGhoaGhoaGVlaGVlZWVlZWVlYiIiIh8fHx8
+fHxvY1A+MTExMTExPjExPj4+Pj4+MT4+PjExMTExMSUxMSUxMSUlMTExMTExJSUxJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUYAABpiIiIiIiIiIiIiIh8XVBQPj5QUDdQUFBEV1dKV1dKSl1dUHaPoaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6u
+rq6uusfHuqeVlYihrq6urq6urq6urq6urq6uoY+CY298aYKhrrrHx9PT09PT09PT09PT09OnY0p2p8fTgkpKMR8+PhglNx8xiK6hglclSpuurq6urqGhoaGhoaGhoaGVlaGVlZWVlYiIiIiIiHZ2goJvb29KMTExMTExMTExMTExPj4+Pj4+MTExMTExJTEx
+MSUxJSUlMTExMTElMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJQAAaYiIiIiIiIiViIhpSkpKPkpKSj5XV0REV1dKV1dKSl1dSl18j6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urrrHx7ShlYiIoaGurq6urq6urq6urq6urqGIb29v
+Y2+Cj7TAwM3Nzc3Nzc3N2dnZ2dnHj1BQocfTtHZKJUppUDExMR8fN3yhrqFKJVePp6e0p6enp6enp5ubm5uPj6GVlaGVlZWVlYiIiHx8fHx8fHxjREQxJTExMTExMTExMTExMTExMTExMTExMTExMSUxMSUlJSUxMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRglJRgYJRgAAGmCgoKCj4+Pj29QUFBQRFdXPkpKPj5QUD5QY0pKXV1KSl1daY+hoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6urq66x7q6oZWIiKGurq6urq6urq6urq6urq6ViHxpaXZ2aYi0tMfT09PT09PT09PT09PT028xfLTH08BjK2mh
+rrq6oXxjPh92rq6bdj4ldqenp6enp6enp6enp5ubm5ubm5uPj4+Pj4+Pj4KCgnZ2gm9vYz4xMTExMTExJTExMTExMTExMTExMTElMTExJTExJSUxMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYAABpiIiIiIiIiGNQUD4+Sko+
+SkpKPkpXRERdXUpXV0pKV1dKXW+ClaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq6uwMDArqGViIihrq6urq6urq6urq6urq6uoYhvb29jb3x8p7rHx9PT09PT09PT09PT07pvRHa009OhY0Rpp8DAwMDAfDc3dqG0wHw+PmmVp6enp6enp6enm5ubm4+b
+m5uPm5uIiIiIiIiIfHx8fHx8fGNENysrKysrKysrKysrKzc3Nzc3Nzc3NzcrNyUlMTExJSUxMTExJSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYJRgYGBgYGBgYGBgYGAAAb4iIiIiIfF1KSj5QUD4+Sko3SkpKSldKSkpXSkpXV0pXV0pdiJWhoaGhoaGhoaGh
+oaGhoaGhoaGhrq6urq6urq6urrq6uq6hlYiIp6e0tLS0tLS0tLS0tLS0p5WIfGNjdnZpgrS0x9PT09PT09PT09PT09PHVz6VwM3NrkoxiLTHx8fHx5VQH2mnp7ShVx9XoaGurq6hoaGhoaGhlZWhlZWhlZWVlZWIiIiIiIh8fHx8b29jPjExMTExMTElMTEl
+MTExMTExMTExMSUxMTElMTEfHzElJSUxJTExJSUlJSUlJSUYGCUlJRgYGCUlJSUlJSUlJSUlGBgYGBgYGBgAAGmCgo+CaV1KN0pKPj5KSj5KV0REV0o+Sl1KSl1dSldjV0pdXXaPoaGhoaGhoaGhoaGhoaGurq6urq6urq6urq6urq66x8e0oY+CgqGurq6u
+rq6urq6urq6urq6hiG9vb2N2doKnusfT09PT09PT09PT09PTtGlQj8fTx49dRIK0x8fHx8eISj52p7q6m1c+Soinp6enp6enp6enp6enlZWhlZWVlYiIlYiIiIh8fHx8fHxvXT4xMSU3NysrKysrKysrKzcrKysrKzc3NysrKysrKysrKysrKysrKysrHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fAABpj498aVc+PkpKPkpKPkpKSj5QUEREV0pKV1dKSl1dSldjV1d8j6GhoaGhoaGhoa6urq6urq6urq6urq6urq6urq6uwMDAwKGPgoKnp6e0tLS0tLSntLS0tLSnlYiIaWl2dmmItMDA09PT09PT09PT
+09PT08BKSqHH08eVPj6busfHx7q6oUoldq6urq5vJUSPoaGurqGhoaGhoaGVlaGPj4+Pj4+Pj4+Pj4KCgoJ2goJvb2M+MTExMTExJSUxMSUlMTElJTExMTExMTExMTElJSUlMSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAA
+Sl1pXUREREQ3Sko+PkpKPkpXSkpXSj5KV0pKV1dKV2NKSl1dXYihoaGhoaGhoaGhoaGhoa6urq6urq6urq6urq6ursDAwLShj4KCoa6urq6urq6urq6urq6urqGCdnZ2Y3x8iK7Hx8fT09PT09PT09PT09OuY0qVx9m6aVdXlcDAwMDAwIhKSm+hurqhb0o+
+gqenp6enp6enp5ubm5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgm9dNzcrKysrKysrKysrKysrPjExMTExMTExMTExMTElMTExMSUlJSUlJSUlJSUlJSUlGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgAAFdXREREREREREREREQ3SldKPldXRERXSkpXV0pXY1dK
+XV1KV2mClaGhoaGhoaGhoaGhoaGhrq6urq6urq6urq6urq7AwMC0oY+CgqGhrq6urq6urq6urq6urq6ViHxjb3xvb4+6x8fH09PT09PT09PT09PTx1dKocehfFclY67Hx8fHx8ehSiVvp7S0tHYlRIihoa6uoaGhoaGhoZWVoZWVlZWIiJWViIiIiIiIfHyI
+dnZ2Yz4xMTElMTExMTExJTExMTExMTExMTExMTExMTExMTElJSUxJSUlJSUlJSUlJSUlJSUlGBglJSUlJRgYGBgYGBgYGBgYGBgYGBgYAABXSkpKPkpXSj5KVz4+Sko+Sko+SldKPlBQUFBQUFBQY0pKXV1ddo+PoaGhoaGhoaGhoaGhoaGhrq6urq6urq6u
+rq6ursfHuqGVgoKnp7Snp6e0p7S0tLS0tLSnp3xvb29jdnaItMfH09PT09PT09PT09PT37pvSojAiD4lSoiux8fHx8fHj1A+Y6G6uqd2Sj6Cp6enp6enp6enp5ubm5ubm4+Pj4+Pj4+CgoKCgoKCdnZ2dlc+MSUxMTElJTElJTcrKys3Nzc3Nzc3Nzc3Nzcr
+NzcrKysrKysrKysrHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAAN0pKSkpKN0pKSj5KSjdKV0o+V1dKSldKSldXRFdXV0pXY0pXY2+Cm5ubm5unp6enp6enp6enp6enp6enp6enp6enp8DAwMCnlXyIp6enp6entLS0tLS0tLS0oY+PdmNv
+fG9vj7THx9PT09PT09PT09PT09PHYz6PrmlQUGOhwMDAwMDAwK5jJVeIp7q6byVEj6entKenp6enp6enlZWVlZWVlYiIiIiIiIiIiIh8fHx8b29dNysrKysrKysrKysrKysrKys3Nzc3Nzc3Nzc3NzclMTExJSUxJSUlJSUlJSUlJSUlJSUlJSUlGCUYGBgY
+GBgYGBgYGBgYGBgYGBgAADc3UFA3SldXSkpKPkpKSj5QUD5KV0o+V1dKSldXSl1dSldjV1dpgo+bm5ubp6enp6enp6enp6enp6enp6enp6enp6e0x8e6rpWCgqGurq6urq6urq6urq6urq6bgm9vb29vb4+0x8fT09PT09PT09PT09PTx49Kb7R8NzdXV2OI
+p6e0x7qniF0xPoiuoW9KSoinp6enp6enp6ebm5ubm5ubj4+Pj4+Pj4+Pj4KCgoJ2dnZpVzc3JTExMSUxMSUxMTElMTElMTExMTExMTExMTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYJSUYGBgYGCUYGBgYGBgYGBgYGBgYAAA+UFA3UGlpdmNQRERXRERX
+SkpKV0pKV1dKSl1KSldXSldjV1djY2+Im5ubp6enp6enp6enp6enp6enp6enp6enp6enwMDAwKePfIihrq6urq6urq6urq6urq6hj3x8Y2N8fGmhwMDNzc3Z2dnZ2dnZ2dnZ2c18PnyhSjdKJRg+PiU+XV1dgnY+MUqCoZVXJWmhrq6urqGhoaGhoaGhlZWV
+lZWViIiIiIiIiIiIiIh2goJ2aUoxJSUxMSUxMSUlMSUlMTElMTExMTExMTExJSUxMSUxMSUlMTElJSUlJSUlJSUlJSUlJSUYJSUYGCUYGBgYGBgYGBgYGBgYGBgYGAAAREREV2l2dnZXSldKPkpKPkpXSj5QUEREXVBQUGNQUGNjSl1dUF1vgo+hoaGhoaGh
+oaGhoaGhoaGurq6urq6urqGhobrHx7Snm4KCoaGurq6urq6urq6urq6uoaF8aXZ2aWl8obTH09PT09PT09Pf39/f39/NlVdXiHxQMT4+Hx8+PhgxMR8fPisriK52Nzd8oa6urq6urqGhoaGVoaGVlZWVlZWViIiIiIiIiHx8fHx8fGlKMTExMTExJTExJTEx
+MTExMTExMTExMTExJTExJTElMSUxMSUlMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBgYGBglJSUlJRgYGBgAAD5XSld8fHx8aUo+Sko+SkpKPkpKSkpXV0RXV0pKXV1QUGNXV2NjV3aPj6GhoaGhoaGhoaGhoaGhoa6urq6urq6urq66x8fHtJuIiKGurq6u
+rq6urq6urq6urqGPgoJpaXx8b6e6x9PT09PT09PT09Pf39/f36dKV4iuuq6PfHxXHx8+JRg+MRglSoiIaT4xgq6urq6urq6urqGhoZWVoZWVlZWIiIiIiIiIiIiIiHx8fG9jSjElJTExMTExJTExJSUlJSUxMTElMTElMTElJTElJSUxJSUlJSUlJSUlJSUl
+JSUlJSUlGBglJSUlJSUlJRgYGCUYGCUlGBgYGBgYAAA+PkpKaWl2dmNKSko+Sko+SldKPldXRFBQUFBQY0pKY1dXY2NKXWlpdo+hoaGhoaGhoaGhrq6urq6urq6urq6urq6uusfHurqhiHybp6enp7S0tLS0tLS0tKenlXxpdnZpdoKnusfT09PT09Pf39/f
+39/T09PHdjdptMfHx9O0fF0xMWNjNzc3JTePjzclY5Wnp7Snp6enp6enp6enm5uPj4+Pj4+Pj4+CgoKCgoKCgnZ2aUo3NzcrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAA
+PlBQPml2dnZ2Vz5QUD5KV0o+UFBERF1dSldXSkpjV0pXY1BQY2NQY3aIlaGhoaGhoaGhoaGhrq6urq6urq6urq6urq7Hx8e0oY98la6urq6urq6urq6urq6uoYiIdmNjfHx8rsDA09PT09PT09PT39/T09PT049dSojHx8fT05U+Pm+hrrqnj4+hj11KN2Ob
+tLS0tLSnp6enm5ubm5ubm4+bj4+Pj4+Cj4+CgoKCgoKCgmNEMSUxMSUxMSUlJTElJTElJSUxMTExMSUxMTElJTElJSUlJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglJSUYGBgYGBgYGBgYGBgAAERERFBddnZ2dl1dRERERERQUFA+UFA+SldKSldXSkpj
+V0pjY0pdaV1dfI+bm6enp6enp6enp6enp6enp6enp6enp6e0wMDAwK6bgoKhoa6urq6urq6urq6urqGPdml2dml2gqe609PT09PT39/f39/f39/f09O0Vz52p8fT08CniF18tMe6use6rpVXJUqbp6e0tKenp6enp6enm5ubm5ubj4+Pj4+Pj4KCj498iIh8
+fHxpSjExMTExMTExMTExMTElMTElJSUxJTExMTExJSUlJSUlMTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgYGBgYAAA+Sko+Y29XV3ZdRERXRERQUD5XV0pKV1dEV1dKSmNXSmNjV1djY1BjY2N8m5unp6enp6enp6enp6enp6enp6en
+p6enp8DAwMC0oYiIp6entKentLS0tLS0tLSbiIh2Y2N8b2+uwMDT09PT09PT09Pf39/f09PTx6FKMXy0x8fTx8fHx8fHx8fHx6FXJT5vm7S0tKe0tKenp6enp6ebm5ubj5uPj4+Pj4KCgoKCgoKCgoJ2dkoxJTExJSUlJSUlJSUlJSUlMTElJTElJTExJSUl
+JSUlJSUlJSUlJSUlJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglGBgYGAAAREREV0pdNzdjY1dKSldKSkpXSkpdUD5QXUpKV1dEV2NXSmNjUGNjY1dvfIibp6enp6enp6enp6enp6enp6enp6enp6e6x8fHuqeViKGhoa6urq6urq6urq6hoY98b29v
+b3yIrsDNzc3N2dnZ2dnZ2dnZ2dnZ2dmub1A+grrHx8fTx8fHx8fHx6djSjFKj7S0tLS0p6enp6enp6enp5ubm5uPj4+Pj4+Pj4+PgoKCgoKCgm9KNzclMTExMTExJSUxMSUlJTElMTElJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJRgYGBgAAD5XSkpXaTc3aWlKV1c+SldERERXSkpXSj5XV0RXY1dKY2NKV2NjV2NjV2mPoaGhoaGhoaGhoaGhoaGurq6urq6urq6uusfHx8e6oYiVoaGurq6urq6urq6urpuIfHxpdoJ2gq66x9PT09PT09PT09Pf39/f09PT06dKSkppp8fH
+x8fHx8fHuo9vShhKiKe0tLS0tLS0oa6uoaGhoZWhoY+Pm4+Pj4+Pj4+PgoKCgoKCgnZ2Sj4xMTExMTExMTElJSUlJSUlJSUlMSUlJTExJTElJSUxJSUlJSUlJSUlJSUlJRglJSUlJSUlJSUlJSUlJSUlJSUYGBgYGBglGBgYAABERFBQPmlXH0ppV0pKSko+
+V1dKSldXRFdXSkpjV0pdXVBQaWlQY2NXV2Njb4+hoaGhoaGhrq6urq6urq6urq6urq6uoa7Hx9PHuq6VlaGhrq6urq6urq66rqGhj3Z2dnZ2go+nwNPT09PT09PT09PT09/f09PT09PHlUoxSmmPus3Nzc3Np49jMR9KdqG0tLS0tLS0p7Snp6enp6enm5ub
+m5ubm4+Pj4+Cgo+CgoKCgoKCb1dERDc3Nzc3Nzc3Nzc3KysrKysrKysrKysrKysrKysrKysrHx8fHx8fKysrKysrKx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwAAPlBQUFBjYytKaVdXSj5KV0pKV1c+UFBQUFBQRFBQUFBjY1dXaV1daWlQY3yIlaen
+p6enp6enp6enp6e0tKenp6enp6enwMDT08e6p5Whrq6urq6urq6urq6um4+Cb29vgm+ItMfH2dnZx9PT09PT39/f39/T09PT07qhXSUxSldpj492aWkxHzFKaaG6uq6urq6urq6urq6hoaGhoaGhj4+Pj4+Pj4+Pj4+CgoKCgoKCdnZjV1dKSkpKSj4+Pj4+
+PjExMTExMTExMTExMTExMTExJTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGBglJSUlJSUlJSUYGBgAAEo+SldKY2MxV2lpSj5KV0pKV0o+V1dKSldXSldjV1djY0pjY1dXY2NKY2Njgpubm6enp6enp6enp6enp6enp6enp6enp7rH09PHx7ShoaGh
+rq6urq6urq6uoaGVfGl2gnaCobrH09PTx5uux9PT39/f39/f09PT09PTunZdSiUlPjEYMT4fHz5XiK66urq6uq6urq6urq6uoaGhoaGhlaGhlZWVlYiIiIiIiIiIfHx8fHx8fG9vb2NjY2NXV1dKSkpKSkpKPj4+Pj4+PjE+PjExMTExMTExMTElJSUlJSUl
+JSUlJSUlJSUlJSUYGCUlJSUlJSUlJSUlJSUYGCUlAABEV1dKSl1pRGN2XVBQUERXSj5KV0pKV1c+UGNKSl1dSldjV1djY1djb11daYKPoaGhoaGhoaGhrq6urq6urq6urq6urq6ux8fT09PHtKGhoa6urq6urq6urq6hiIh8b2+IiJW6x9PT3643N2Obx9nZ
+2dnZ2dnZ2dnZ2dnHoWNjShglPiUYPldvobq6urq6urq6uq6urq6urqGhoaGhoaGhj5ubiIiIiIiIiIiIiIh8fHx8fHx8iHx8fHxvb29jY2NjY2NjY2NXV1dXSkpKSkpKSj4+PjExMTExMTExJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRglJSUlJSUYGAAA
+REREV0pKdldXb29QRFdXPkpXSkpKV0RQXV1KV1dKV2NXV2NjV1dvY0pjY1d2j6Ghoa6uoaGurq6urq6urq6urq6urq6ursDA09PT08e6p6enp6entLSntLShoZWIiIiIiIinusfZ2dmnMQwAEmO62dnZ2dnZ2dnZ2dnZzc3NoXxvUD5ddoinwMC0wMDAwMC0
+tLS0tKe0tKGhoaGhoaGVlaGPj4+Pj4+PgoKCgoKCgoKCgoKVlZWVlYKCj4+CgoJ2goJvb3x8b29vb2NjY2NXV1dXV0pKSko+Pj4+Pj4xMTExMTExMSUlJSUlJRglJSUlJSUlGBgYGBglJSUlJSUAAEpXSkpXSmlpaXZjV1dKSldKPldXRERXV0pKY0pKY2NQ
+XWlXV2lpV2NjV1dpdoihoaGhoaGhoaGhoa6urq6urq6urq6urq7Azc3Z2dnZ2bqurq6urq6urq6urq6hoZWIiJWVobrN2dnmmyUlDAAAfMfZ2dnZ2dnZ2dnZ2dnZzc3NwMDAtLTHx7rHx8e6urq6urq6rq6urq6urq6hoaGhoZWVlZWIiIiIiIiIiIiIiHx8
+iIiIiJWVlZWVlYiVlYiIlYiIiIiIiIh8fHx8b29vb29vY2NjY2NXV1dKSkpKSj4+Pj4+MTExMTExMTExJSUlJSUlJSUYGCUlJSUlGBglAABKPlBQUERpfHx8fFdKV0pKSldKSldXPlBdSkpdXUpXY1dKY2NXY29dXV1dSmmIm5unp6enp6enp6enp6enp6en
+p6enp6enusfT09/f39/TwLS0p6enp7S0tLS0tKGhoaGVobS0x9nZ2Y8xdnZEAEq609Pf39/f38e609PH2dnZ2dnHx8fHx8fHx8fHx7q6urqurrqurq6urqGhoaGhoaGhoY+Pj4+Pj4+CgoKCgoKCgoKCgo+Pj5uPj4+Pj4+Pj4+Pj4+Pj4+Cgo+Pgo+CgoKC
+gnaCdnZ2dmNjY2NjY1dXV1dXSkpKSko3Nzc3Nzc3NzcrKysrKysrKx8fHx8fHwAASldKSldKV3Z2dnZdXUREV0o+V1dKSl1QUFBdSkpjV0pdXV1daWlQY29XV2NjfJubp6e0tLS0tLS0p6enp7S0p6enp6enp7TAwNPf3+zs39/Nuq6urq6urq66rq6urqGh
+rq6ux9Pf39+PN5vHoR8xp9Pf39/f39NvN4ihlbrT09PT09PHx8fHx8fHx8e6urq6urqurq6urq6hoaGhoaGhlZWhlZWVlYiIiIiIiIiIfIiIiIiIiIiIiIiIiIiIiIiViIiIlYiIlZWIiIiIiIiIiIiIfHx8fHx8fG9vb29vY2NjY2NjV1dXV1dXSkpKSkpK
+Pj4+PjExMTElMTElJSUAAEpKV1dKSld2dnaIY0pXSj5QUFBQUFBEV1dKSldXSldjV1djY1djY2NXaWlKY3yIm666usfHx7q6urq6urq6urqurq6urq6ursDA2ebm5vLy5tnHtLS0tLS0tLS0p6e0tKG0tMDT39/fiDGIx64lMafT09PT09/NVwZKMQBdtM3N
+zc3Nzc3AwMDAwMDAtLS0tLS0tLS0tKenp6enp6enlaGVlZWVlYiIiIiIiIiIiIiIfHx8iHx8iIh2iIh8fIiIfIiIiIiIiHyIiIiIiIh8iIiIfIiIfIiIfHx8fHx8fHxvb29vb29jY2NjY2NjY2NjV1dXV0pKSkpKPj4+PjExAAA+UFBEXV0+b3x8fG9dSkpX
+Sj5XV0pKV1dKV1dKV2NXV1djV1dpaVdpaVBQXV1diKG0x9PT09PT09PTx8fHx8fHurq6uq6urq66usfZ5vLy8vLy5tPHx7SntLS0tLS0p6e0tLTH2dnZ2YgxSnZ2GEq0x6GVp8fZzVcMSiUAV67Hx8fHx8fHx8e6x8e6use6urq6rq6urq6urqGhoaGVoaGV
+laGPj4+Pj4+Pj4KCgoKCgoKCgoKCgoKCgoJ2iIh2goKCdoiIfHyIiHx8iHaCgoKCgoKCgoKCgoKCdoKCgnaCgnZ2dnZ2dnZ2dnZ2dnZ2Y2NjY2NXY2NjV1dXSkpKSgAARERQUD5KSmN8fGlXSldKPldXRERXSj5XV0pKY1dXV2lXV2NjV2NjV1djY0pXaYKh
+wNPT39/f7Ozf39/f39/f09PTx8fHx8fHx8fH09Pm8vLy8vLy8ubZzc3Nzc26usfHx9PTx9/f39+CHwAAAABptGkfBhhdtMBKElclAGO0x9PTx8fHx8fHx8fHx8e6urq6urq6rq6urq6urqGhoaGVoaGPj4+Pj4+Pj4+Pj4KCgoKCgoKCgoKCgnaIiHx8iHx8
+fIh2doh8fHx8fHx8fGl8fHx8fHx8fHx8fHx8b3x8fHx8fG98fHx8fHx8fHx8fHxvb29vb29vY2NjY2NjV1cAAERXSkpXV0RpgmlKV1dEV1dKSldXRFdjSkpXV0pXY1dXY2NQUGNjV2NjSkpXSleIp8DZ2ebm5ubm5ubm8vLm5ubm5tnZ2dnZ2dnHx8fT09/s
++P/4///////4+Pj4+Pj////47Ozf39/faSUlEgAYj6ExBgYGBm+uRBJKJQBjrsfHx8fHx8fTx8fHx7q6urq6urqurq6urq6urqGhoaGhoZWVlZWVlZWIiIiIfIiIiIiIiIh8fIh8fIiIfHyIiHaCgoJ2goKCgoJ2dnZ2dnZ2dnZ2gm98fHxvb3xvb3xvb298
+b298fHx8fG9vb29vfHxvb29vb29vb29vb2NjY2NjAABERERXRERdXV1dUEREV1dKV1dKSldXSldXSldXV0pdXV1dXV1dXWlXV1dKPl18lbTNzdnm5ubm5ubm5ubm5ubm5vLy8ubm5ubm5s3NwNPT3+zs//////////////////////Lm5tnZ2WM+j49EEm9v
+DDFvVwwxiDESVyUGdrS0j4+Pp8DNzc3Nurq6urq6x7q6urqurq6urq6uoaGhoaGhoZWVlYiIiIiIiIiIiIiIiIh8iIh8fI+CdoiIfHyIfHx8fHx8iHx8fHx8fHx8b298b298fG9vfGlpdnZpdnZpaXZ2Y3Z2dml2dmNvb29vb29jb29vY3Z2aWlpaWlpaQAA
+RFdXSkpXSkpKSj5KV0pKV1dEV1dKSldXSldjV0pjY1djY1dXY2NXV1dKPl12iK7H09/f39/f39/f39/f39/f7Ozs7Pjs7Ozs7Ozf08DAzc3f7Pj4///////////////////y8ubZ2ccxY7rTfB9KKx92rpUlN2kYJUoSEo+CPgwABjGIusfHtLSnp6e6urq6
+urqurq6urq6hoaGhoaGhlZWVlZWVlZWIiIiIiIiIiIiIiIh8fIh8fIiIfHyIdnaIiHZ2iHx8iIh2doJ2dnZ2dnZ2dnZ2dmlpdnZpdnZpaXZpaWlpaWlpaWlpaWlpaWldaWlpaWlpXWlpaWlpaWkAAEpKSldKSldXSkpXSj5XV0REV1dKV1dKSl1dRFdjSldj
+Y1djY1dXY1c+PldpiK7A09Pf39/f39/f39/f39/f39/y8vLy5ubm5ubm5ubTx8fH09Pf7Oz4////////////////8ubm2dnHPmO02ZUlMRgMJVBjJSVEEiVKEh9vPgwADAAAJYi6p2kxGBhKdq7Ap4+hrpWVrq6bm5ubm5ubm5ubj4+bj4+Pj4+Cgo+CgoKC
+goKCj4KCj4+CgoKCgoKCgoKCgnaCgm98iHx8fHxvb3x8b3x8b298fGl2dmlpfHxpaWlpaWlpaWlpaWlpaVdpaV1daWldXV1dXV1pV2NjAABKV0o+UFBQUFBQPlBQUFBQUERXV0pKXV1KV1dXSmNjUF1dXV1dXURERERjiKe609PT39/f7Ozf3+zs7Ozs7Ozs
+7Ozs39PT09PT3+zs7NnHx8fH09PT3+zs7Pj4///4+Pj45ubm5tnZx0pKlbp8JTESAAAAAAASJQwlShIlShgAN2ldGABdm1cSAAAAABJpoWMfN0olSoiIYz4+XY+PoaGVlZWVlZWVlYiIiIiIiIh8fIiIiIiVlYiIiIiIiIiIiIiIfHyIdnaIfG+CgnZ2goJv
+fHx8fHx8fHx8b29vb29vb29vb29vb29vY2NjY2NjY1djY1dXY2NXV2NXV1dXVwAASkpKV0pKV1dERFdKPldXSkpdSkpXV0pXV1dKXV1KV2lpV2NjSkpXPj5jiJu0x9PT5ubm5ubm5ubm5ubm5vLy8vLy2cfHx7q6usfT09PT08fHx8fH2dnZ2dnZ2dnZ2dnZ
+2dnZ2dnZ2cBKEhIxGAZKHx9jYzcfHzEAH0oSJSUGEny0tFASPlAYAAwlJQwAH4hKDAwMADFdMQAAAAA3iJubm5uPj4+Pj4+Pj4KCgoKCgoKCgpWhlZWhlYiVlZWIiIiIiIiIiIiIfHyIfHx8fG98fHx8fHxvb3xvb29vb298fG9vb29vb29jb29vY2NjY2Nj
+Y2NjY1djY1dXY1dXV1cAAD5XV0pXV0pXY0pKXV1KSl1QUFBQUFBjV0pXV0pKY1dXY2NQUFBQN0ppfKG6x9PT39/f39/s7Ozs7Ozs7Ozs7Pjs39PHurq6urq6usfHx9PHx7rHx8fHx9PT09PT09PT09PT09/f09O6aTEGBgASdj4Ygrq6p49jEjdKEh8fACWP
+usehdmM+BgZjoaFpGAZXNxIAABIlJQAAEhIADF2VoZWVlZWIiJWIiIiIiIiIiIiIiJWhoaGhoaGhoZWhoZWVlZWVlZWIiIiIfIiIiHx8fHx8fHx8fHxvfHxvb29vb29vb29vb2Nvb29jY2NjY2NjY2NjY2NjY2NjY1dXY2NXAABKSldXSkpXV0RQUFBQUFBQ
+UFBQRF1dPldjV0pdXUpXY1dXY1c+PldXfKG0x9PT09/f3+zs7Ozs7Ozs7Ozs+Pjs7N/T09PTx9PTx8fHx8fHx8fHx8fHx8fHx9PT09PT09PT09Pf39PT08CIUB8MV6F2JVehlXyVdhg+ShIlJQAxlcDNzc20SgYlj7S0jyUANx8GBkp8aR8AMW9vJQY+iIib
+m4+Pj4+Pj4+Pj4+CgoKCgo+Pp6enp6enp6enm5ubm5ubm5uPoZWIiIiIiIiIiIiIiHyIiHZ2gnZ2dnZpdnZ2aXZpaWlpaWlpaWlpaWlpaWldXWlpV2NjV1djV1dXVwAAPldKSldXSldXV0pXV0pKXV1KXV1KSl1dSldXSldXV0pXV0pKV0pdgqG0x8fZ2ebm
+5ubm5ubm5uby8vLy8vLy8vLm5tnZ2ebm2ebZ2dnZ2dnZ2cfHx8fHx8fHx9PT09PT09PT09PT09PT08eursDTrlAMDAwASkoSPj4SMTEGMZvAzcDAp0oMPqHAwKExDCUlACuCp3YYGEp2dj4MMYihoZWhlZWVlZWIiIiIiIiIiIiIlaGhrq6hoaGhoaGhoaGh
+oaGhoZWhoZWVlZWVlZWViIiIiIiIiHx8fHx8fHxvb29vb29vY2NvY2NjY2NjY2NjY2NjY2NjV2NjV2NjV1cAAEREV1dKV2NXSldXSkpdXUpXV0pKXV1KV2NKSl1dSldjSkpXSj5jiKGux8fT09Pf7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs
+7N/f39/T09PHx8fHx9PT09PT09PT09PT09PT09PT39+PMQYAJYhdDD4+DDdXGBiCwLSCdnY+DD6htMehMQYxGAA+j6FjGAAADAAAACt8j6Ghj4+bj4+Pj4KCj4+CgoKCj5ubp6enp6ebp6enp6enp6enm5ubm5ubm5ubm5uPj4+Pj4+Pgo+PfHyIiHaCgnZ2
+dnZ2aXZpaWlpaWlpaV1paVdjY2NXY2NXV1dXV1dXAAA+V0pKXV1KV1dKSkpXSkpjSkpXV0RXV0pKXV1KV2NXSldXPkppgqG0x8fT09Pf39/f39/f3+zs7Ozs7Ozs7Ozf39/s7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7N/s7NnNzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ
+2aFvY4+6dh9KShI+djESUIh2JQBKNwwlj7S0gjEAMRgAPo+nYwwAAAAAAAAxfJWVlZWViIiViIiIiIiIiIiIiJWhoaGhoaGhoaGhoaGhoaGhoaGhlaGhlZWhoZWVoZWVlZWVlZWViIiIiIiIiHyIiHZ2gnZ2dnZpaXZpaWlpaWlpaVdjY1dXY1dXV1dXVwAA
+RERXV0pXY1dKV1dKSl1QRFdXRERdUFBQXUpKY1dKV1c+PldpfKG6x8fT09Pf39/f39/s7Ozs7Ozs7Ozs7Ozs39/f39/f7Ozs7Ozf39/f7Ozs7Ozs7Ozs7Ozs7N/Tx7rHusfT09PT09Pf39/f39/f39/f39PT06dvb1cfSpVjGAAMAAAYb2MYGGOhoVcSEkof
+AESVoWkYGF18fGlpgpubm5uPm5uPj4+Pj4+Pj3yIiIiVoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhlaGhoZWhoZWVlZWVlZWIiJWViIiIiIiIiHZ2iHx8fHx8fHxvY29vY2NjY2NjY1dXY1dXV1cAAERXSkpdXUpdXUpKV1dKXV1KSl1dSldjUFBdXURXV0RE
+RFdXgqG0x8fT09PT39/f3+zs7Ozs7Ozs7Ozs7Ozf39/f39/f3+zs7Ozs2dnZ2dnZ5ubm5ubm5vLy8vLy5tnHurq609PT09PT39/f39/f39/f39/f39/f09PAoaG6p10YAAAGUJWVPgYSPjESADFjJQBKlad2GBhdiIhpXYKVoZWVlZWVlZWIiIiIiIiIiIiI
+oaGhoa6uoa6uoaGhoaGhoaGhoaGVlaGVlaGhlaGhlZWVlZWVlZWVlZWIlZWVlZWViIiIiIiIiIiIiHZ2dnZ2dnZpaWlpaWlpXV1dXV1dAABKSkpdXUpdXUpKV0pKV1dKSldKSldXSkpXV0RXVz4+Sldpj6e6x8fT09PT39/f39/f3+zs7Ozs7Ozs7Ozf39/f
+39/f39/s7Ozf39/f39PT09/f39/T3+zs7Ozs39/s38fHx9nZ2dnZ2dnm5ubm5ubm5tnm5tnZ2dnZ2dnZ2c2udldXiLTHrmklAAAAACV2aRIAaaGhfCUMK0orACV8m5ubm4+Pj4+Pj4+Pj4+PgoKPj6e6urq6rq6urq6urq6urq6urq6hoaGVlaGhlaGhj4+b
+m4+hlZWVlZWVlZWVlZWViIiIiIiViIiIiIiIiIh8fHx8fHx8aWlpaWlpaV1dXQAARFdXSldXSkpXVz5KV0pKV1dEV1dKSldXRFBQRERERERvlae6x8fT09PT09Pf39/f39/f3+zs7Ozs7Ozs7N/f39/f39/f3+zZx9PT09PT09PT5ubZ2dnZ5ubZ2dnZ2dnm
+5tnm09PT09/f39/f39/f39/f39/f39/f39PT39PT08fHx8fHx8e0fD4fEjd2oW8fH2+hoaFQEgAAAAxXiJWhlZWVlZWVlZWCj4+PfIiIiJWux8fHx8fHx8fHurq6urq6urqurq6urq6hoaGhlZWVlZWVlZWVlZWIiIiIlZWIiIiIiIiIiIiIiIiIiIiIiHx8
+fHx8fHx8fG9vb29vY2MAAEpKV1dKSl1dSkpdSkpdXT5QXUpKV1c+SldKPlBQN1d8j6e6x8fH09PT09PT39/f39/f39/f3+zs7Ozs7Ozs7Ozs7Ozs7N/f08fHx9PHx8fHx9PT09PT39/f39/f39/f3+zs39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT
+09PHx8fHx8e6p6e0tLSnj3yhtLSnm1cfDAxXiJubm5ubj4+Pj4+Pj4+CgoKCgo+brtPT09PT08fH08fHx8fHx8fHx8fHurq6uq6urqGhlaGVlZWVlZWVlYiIlZWVlYiIiIiIiIiIiIiIiIiIiIh8fHx8fHx8fHx8b3x8b29vAAA+V0pKV1dKSmNXSldXSkpd
+XURXV0REV0o+SkoxPmOCm7THx8fHx9PT09PT39/f39/f39/f3+zs7Ozs3+zs7Ozs7Ozs7OzZ2cfHx8fHx8e6usfHx8fT09PT09PT09PT09PT39/f09Pf39/f39/f39/f39/f39/f39/f39/f09PT08fT08fHx8fHx8fHurq6urq6uq6urqGhlYiIoaGVlaGh
+lZWViIiViIiIiIiIiIiIm7rT09PT09PT09PT08fT08fH09PHx8fHx8e6urqurq6hoaGVlZWVlZWVlYiIiIiIiIiIiIiIiIiIiIiIiIiIfHx8fHx8fHx8fHx8fHxvbwAASkpKXUpKV1c+UF1KSldXSldXSkpXSjdKSjc3Sm+Cobq6x8fHx9PT09PT09Pf39/f
+39/f39/f39/f39/f3+zs7Ozs7Ozf08fHx8fHx8fHusfHx8fHx8fHx8fHx8fH08fHx9nZ2dnZ2dnZ2dnm5ubm5ubm5ubm5ubZ2ebZ2dnZ2c3Nzc3Nzc3Nzc3AwMDAwMC0tLS0tLS0tKGhoaGhoaGVlZWVlZWViIiIiIiIiIiIlaGu09PT09PH09PTx8fTx8fT
+08fH09PHx8fHx8fHurq6urqurq6hoaGhoaGVlZWViIiIiIiIiIiIiIiIiIiIiIh8fHx8fHx8fHx8fHxvfHwAAD5XV0pXV0pXY1BQUFBQUFBQUFBQPj5KPis+SmmPrq7AwMDNzc3NzdnZ2dnZ2dnZ2dnZ2ebm5ubm5ubm5ubm5ubm5ubm5ubZ2c3Nzc3Nzc3N
+zc3Nzc3Nzc3NwMDAwMDAwMDZ5ubZ2dnZ2dnZ2dnm5ubm5ubm5tnZ5ubZ2dnZ2dnZzc3Nzc3Nzc3Nzbq6urq6urq6urq6p6enp6enp5ubm5uPoY+Pj4+Pj4+PfHyIiIihtMfHx8fHx9PHx8fHx8fHx8fT08fH08fHx8fHx8fHx8fHurq6urqurq6hoaGhlZWV
+lYiIiIiIiIiIiIh8fIh8fHx8fHx8fHx8fHx8fHx8AABKSldXSkpjV0pXV0pKXV1EV1dEREREMTE+V3ahrrrHx8fT09PT09PT09PT39/f39/f39/f39/f39/f39/f39/f3+zs7Ozs7Ozs39/f39/T09PT08fH09PHx8fHx8fT09PT09/f39/f39/f39/f39/f
+39/f39/f39/f39/f39PT09PH09PTx8fHx8fHx7q6urq6urqurq6urq6uoaGhoaGhoY+Pj4+Pj4+Pj4+PfIiIla7Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8e6usfHurq6uq6urq6hoaGhlZWhlZWVlYiIiIiIfHx8fHx8fHx8fHx8fG9vfAAA
+SldKSldXSldXSkpXV0RQXUo+V0oxPj4+SoKnusfHx8fT09PT09PT09PT09PT39/f39/f39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7N/f39PT09PT09PT0+bm5tns7NnZ2dnm5ubm5ubm5ubm5ubm2dnm5tnZ2dnZ2dnZ2dnHx9PHx8fHx8fHx7q6
+urq6uq6urq6hoaGhoaGhj4+Pj4+Pj4+Pj4+CgoKPj6GuwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM3AwMDAwMDAtLS0tLSntLSnp6enp5ubj4+Pj4+Pj3x8fHx8fHx8b298b28AAEREV1dKSldXRFdXSkpdSj5QUD4+Pj4xV4inusfHx9PT
+09PT09PT09Pf39/f39/f39/f39PHx9nZ5ubZ2dns39/s7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozf39/f3+zs7Ozs7Ozf39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT09PHx8fHx8fHurrHurq6urq6uqenp6enp6enlZWVlZWVlYiIiIiIiIiIiJWV
+p7q6urq6urrHx7q6urq6x8e6use6urq6urq6urrHx7rHx7rHx8fHx8e6urq6urq6urq6uq6urq6urqGhoaGhlZWVlYiIiIh8fHx8fG9vAABEV0pKV1dKXV1KSl1dSkpXSjdKPjE+XYKnwMDAzc3N2dnN2dnZ2dnZ2dnZ2dnZ2dnZ2dnHx8fT09/fzc3Nzc3Z
+2dnZ5tnm5ubm5ubm5ubm5uby8vLy8vLy8ubm5ubm8vLm5ubm5ubm5ubm5ubm5ubm5ubZ7N/f39/f39/f39/f09PT08fT08fHx8fHx8e6urq6urq6uq6urqGhoaGhoaGVlZWViIiIiIiIiIiIiIiVoaGurq6urrq6rrq6urq6urq6urq6urq6use6urq6urq6
+urq6urq6x7q6urq6urq6urq6urq6uq6urq6urq6urqGhoaGhlZWVlYiIiHxvbwAARERXV0pXV1dKV1dKSldKPlBQMTFEY4i0wMDAwNPT09PT09PT09PT39/f39/f39/f08fHx8fHx8fT09PHx8fHx9PT09PT09Pf39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7N/f
+7Ozs39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT08fHx8fHx8fHx8e6urq6urq6rq6urqGhoaGVlZWVlZWViIiIiIiIiIiIiIiIoaGhoa6urq6urq6urq66urq6urq6urq6urqurrq6urq6urq6urq6urq6urq6urq6urq6rrq6rq6urq6urq6u
+rq6hoaGhoaGhoY98b28AAEpXSkpjV0pXV0pKV1dEV0o3Nzc3V4+0wMDAzdnZ2dnZ2dnZ2dnZ2dnZ2dnZ5ubm09PHurrHx8fHx9PHx8fHx8fH09PHx8fHx9PT09Pf39/f7Ozs7Ozs7Ozf39/f39/f39/f39/f39/s7Ozs7Ozs39/f39/f39/f39/f39/f39/f
+09PTx9PTx8fHx8fHx8fHx7q6urq6rq6urq6hoaGhoZWVlZWVlZWIiIiIiIiIiIiIiIiIiIibm5uPoaGhoaGhoa6urq6urq6uurquurq6urq6urq6urq6urq6uq66urq6urq6urq6rrq6rq6urq6urq6urqGhoaGhoaGPdmNjAABKSkpdUFBdXUpKXUpKV0ox
+Pj4+aZu0x8fHx8fT09PT09PT09/f39/f39/f39/f08fHx8fHx8fHx8fTx8fHx8fHx8fHx8fHx9PT08fHx8fT09Pf39/f39/f09PT09PTwMDA2dnZ5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubZ5ubT09PT09PTx8fHx8fHx7q6urq6urqurq6urq6urqGhoZWV
+lZWVlZWIiIiIiIiIiIiIiHx8fHx8iHx8iIiIiIiIiJWhoaGhrq6urq6urq6urq6urrq6urq6urq6urq6urq6urq6urq6urq6rq6urq6urq6urq6urq6hoaGVfGNXVwAASl1dSldXSkpXV0pXV0o+SjExb6G6x8fH09PT09PT09PT09Pf39/f39/f39/f08e6
+usfHx7q6usfHx8fHx8fHx8fHx8fHx9PT08fHx8fT08fT09PT08fHx8fHx8fTx7q6utPT39/f39/f7Ozs7Ozs7Ozs3+zs39/f39/f39/f09PT09PT09PHx8fHx8fHurq6urq6urqnp6enp6enp5ubm4+bm4iIiIiIiIiIiIiIiHxvb29vb29vb29vb3x8fHx8
+fIiIiJunp6enp6entLS0tLS0tLS0tLS0tLS0tLS0wLS0tLS0tLS0tLS0tLS0p6e0tKenp6enp6ebj29jV0oAAEpKV1dKSldXSkpXRERERCU3b6G0x8fH09PT09PT09PT09PT09/f39/f39/f07q6urrHx8fHx8e6x8fHx9Pf08fHx8fHx8fT09PHx8fHx8fH
+tLS0tLS0tLS0tLS0wMDAwMDN2dnZ2ebm5ubm5ubm8vLm5ubm5ubm5ubm5tnZ2dnZ2dnNzc3Nzc3NwMDAwMDAwLS0tLS0tKenp6enp6enlaGhj4+Pj4+Pj4+Pj4+Pj492dmlpaWlpaWl2dmNvb29vb29vb3yIlZWVlaGhoaGhoaGurq6urq6urq6urrq6urq6
+urq6urq6urqurq6urq6urq6uoaGhoaGhoXZjUF1dAAA+V1dKV2NKSldKPkpKNzc3b6G6x8fH09PT09PT09PT39/f39/f39/f39/fzc3AwMDAwMDAwMDAzc3N2dnZ2dnNzbrHx8fHx8fHx8fH09PHtLShoaGViIiIiJWhoaGhtMC0x9PT39/f39/f7Ozs7Ozs
+7Ozf39/f39/f39/f39/f39PT09PT09PHx8fHx8fHx8fHtLS0tLS0p6enp6eVoaGVlZWVlZWViIiIiIiIlYiIfG9vb29vb29vb29vY2Njb29vb29vfIh8fIiIlaGhlaGhoaGhoa6urq6urq6urrq6urq6uq6urq6uurqurq6urq6urq6uoa6uoYhjV2NjUAAA
+SkpXV0pKV1c+Sko+Pj4+aaG6x8fH09PT09PT09PT09PT09/f39/f3+zf08fHx8fH09/T08fT0+bm5ubm2dnNzdnZ2c3Nzc3Nzc3Nzc3NurquoaGViIh8fHx8fHx8fIiIobrT39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f09PT09PT08fHx8fHx8e6
+urq6urq6rq6urqGhoaGVlaGVlZWVlZWViIiIiIiIiG9vb29vb29vY2NjY2Nvb29vb29vb29vb29vgo+Pj4+Pj4+PoaGurqGhrq6urq6urq6urrq6rq6urq6urq6urq6urq6urq6uoaFvXV1KXWkAAERXSkpXY0pKVz4+Sj4xaafAwM3Nzc3NzdnZ2dnZ2dnZ
+2dnZ5ubm5ubZ2bq6usfHx8fZ2cfH09PT39/f8vLy8vLy8vLm2dnZ2dnZ2cfH08e6urquoZWIiHx8fG9vb29vb4Ku09/f39/f39/f3+zs39/s39/f39/f7N/f39/f39/f09PT09PT08fHx8fHx8e6urq6urq6p7S0p6enp6enp5WVlZWVlZWViIiIiIiIiHZ2
+dmNjb29vb29vb29vb29vb29vb2NjY2NjdnaCgoKCgoKCgpWhoaGhoaGurq6urq6urrqurrq6rq66uq6urq6urq6urq6urqGIY0pXaV1dAABKSldXSkpXVz5KSjFKdqHAwMDNzc3Nzc3NzdnZ2dnZ2dnZ2dnm5ubZzbrHx8fHx8fHx8fHx8fT09PTx8fT09PT
+7Ozs7Ozs7Ozs7Ozf39PT08fT07qurqGhj4+PgoJ2dnZ2rs3f39/f39/f39/f39/f39/f39/f39/f39/f39/T09PT09PTx9PTx8fHx8fHx8e6urqurrqurq6urq6uoaGhoZWVoZWVlZWVlZWViIh8aWlpaWlpaWlpaWlpaXZ2dmlpaWlpaWlpaWl2goKCj4+P
+j4+Pm5ubm5unp6entLS0tLS0tLS0tLS0tLS0tLS0tKenp6enp6eVaVdjY1djYwAARFdKSl1dRERERDdKfKHAwMDNzc3Nzc3NzdnZ2dnZ2ebm5ubm5ubm08DAwMDA08fHx8fZ2dnZx8fTx8fH09/f09/f09PH2dnZ5vLy8vLy8vLy8ubZ2cfHx7Snp6eVlaGP
+j67T39/f39/f39/f7Ozs7Ozf39/f39/f39/f39/f39/T09PH09PTx9PHx8fHx8fHx7q6urqurq6urq6uoaGhlZWhoZWVlZWVlZWIiIh8fHxpaWlpaWlpaWlpaWlpaWlpaXZ2aXZ2aWl2dnZ2goKCj4+Pj4+PoaGhoaGurq6urq6urq6urq6urq6urq6urq6u
+rq6urqGhiGNXSl1dXV0AAEREV1dERFc+Pj5XfKfAwMDAzc3Nzc3N2dnZ2dnZ2dnZ2dnZ2ezfx8e6usfHx8fHx8e6x9nZ2cfHx8fHx9PT39PT09PH09PT39/T09PT7Ozs7Oz4+Pj47OzZ2c3NwMDAtKG0zdnZ2dnm5ubm5ubm5ubm5ubm5ubm2ebm2dnZ2dnZ
+2dnZ2c3Nzc3Nzc3NusfHx7q6uq66uq6urq6hrq6hoaGhlZWhlZWVlZWVlYiIiHZpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXZ2dnZ2dnaCgoKCgoKClZWVoaGhoaGhoa66p6e0tLS0tLS0tLS0tLSnp7S0p6enm29QUGldXV1dAABKV0REV0o+SkpKgrTHx8fH
+09PT09PT09PT09Pf39/f39/f3+zZx8fHusfTx8fHx9PTx8fH09PT08fT09Pf39/fx8fZx9/f39/f39/NzdnZzd/f3+zs7Oz4+Pj4+Pj47OzZ2cfH09Pf39/f39/f39/f39/f39/f39/f39/f09PT09PT08fT08fH08fHx8e6urq6urq6rq6urq6urq6hoaGh
+oaGhlZWhj4+Pj4+PgnZ2dmlpaWlpaWlpaWlpaWlpaWlpaWldaWlpaWlpaXZ2dnZ2doKCgoKPm5ubm5ubp6enp6e0tLS0tLS0tLS0tLSntLShrq6uoXxdXV1dXV1dXQAAPkpXVz4+SjFKiLTHx8fHx8fT09PT09PT09PT09Pf39/f39/fx7S0x8fT08DAwMDA
+wNPTx9PT38fH09PT0+bm2dnZ5ubZ2dnZ2ezs7NPT09Ps7N/f3+zs2dnZ2ez4//j///jNwMDNzdnZ5ubm5ubm5ubm5tnZ2dnZ2dnZ2dnZ2dnZzc3Nzc3Nzc3NzcDAwMDAwMDAtLS0tLS0p6enp6enp6ebm5uPj5ubiIiIiHx8fGlpaWlpaWlpaWlpaWlpaWlp
+aWlpaVdjY2NjY2NjY29vb3x8fHyIiIiVoZWVp6enp6entLS0tLS0tLS0tLS0tLS0tLSnp5tpV1djY1dXaWkAAERXSj5KSjFQlbrHx8fHx8fH09PT09PT09/f39/f39/f39/fx7rH2dnZ5tO0tMDAtMfHx9Pfx8fH09PH2dnZ2dnZ2dnZ2dnZ7Ozs39/f39/f
++N/f39/f39PT09/f8vLf8vLmwKenusfT09/f39/f39/f39/f39/f39/T09/f09PT09PTx8fHx8fHx8fHx8e6use0tLS0tLS0p6enp6enp6enp6eVlZWViIiIiIh8fGlpaWlpaWlpaWlpaWlpaWlpaWlpaV1paV1dXV1dXWlpaWl2dnaCgoKPj4+bm5ubp6en
+p7Snp7S0p6e0tLS0tKentLSntJtvV0pjY1dXaWlQAABKPkpKNzdjm7rHx8fHx8fT09PT09PT09/f39/f39/f39/TwM3Nzc26x9PTx8fT39/f39/fx8fH09PT09PT09Pm8ubT09/f09Pf7Ozs7N/f39/f7Ozs7Ozf7Oz4+Ozf39/f38ehlaGuusfT09/f39/f
+39/f39/f39/f39/f09PT09PT09PTx8fHx8fHx8fHx8e6urq6uq6urq6urq6uoaGhoaGVlZWVlZWIiIiIdnZpaWlpaWlpaWlpaWlpaWlpaWlpXWlpaWlpaWlpaWlpaWlpdnZ2doiIiIiVlaGhoaGhrq6urq6urrq6rq6urq6urq6urq6IV1dpXV1paVdXYwAA
+N1BEN0pjlcDAwMDAwM3Nzc3Nzc3Z2dnZ2dnZ5ubZ2ebTx8fHx9PTx7rH39/T09PHx9nZ2dnZ2ebm2c3N2dnm+Ozs39PT09PH2dnm5ubm2dnZ5vLy8vLy8ubm5ubm2ezs7Nnmx6GVlaGursDA09PT09/f39/f39/f39/f39PT09PH09PTx9PTx8fHx8fHx7q6
+urq6urqurq6urq6uoaGhoaGhoZWVlZWIiIiIfHxvY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Njb29vb29vfHx8iIiVlaGhoaGhoaGhtLS0tLSntLS0tLS0tLS0p6eVaVdXV2lpV1dpXVAAADc3N0pjm8DAwMDAwNPH09PT09PT09PT09Pf39/f39/T
+09PHx8e6usfH09Pf09Pf08fH39/Tx8fT09PT09/f7Pjm2dnN3+zf09/f3/js09Pf3+zs7N/s+Ozf3+zs7N/f39/f39/frpubm5uurq66x8fT09PT09/f39/f39/f39PT09PT09PHx9PHx8fHusfHx8e6uq6urq6urq6urq6um5ubm5ubj4+Pj4+Pj3x8b2NX
+V2NjV2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nvb29vb298fHx8iIiIiJWhoaGhoaGurq6urq6urq6urq6urq6urq6ufFBQY2NXV2lpV2NjAAAxRERpp8fHx8fHx8fH09PH09PT09PT09Pf39/f39/fzc3m5vLy5tnZ2dnZ2ebZzc3Z2dnZ2cfHx9nZ2dnZ2dns
+2c3Z2ebm+Ozs7N/f39/f39/f39/f39/s7Ozs7Pjm2dnm5ubZ2ezTtJWVp6enp6e0wMDAwNPT09/f39/f39/f09PT09PT08fHx8fHx8fHurq6urq6uq6urq6urq6hoaGhoaGhj4+Pj4+Pj3x8fGNKSkpKSkpKSkpXV1djY2NjY2NjY2NjY2NjY3Z2aXZ2dnZ2
+dnaIiIiIlaGhoaGhoaGhoa6urq6urq6uurq6uq6urrquj2NKY2NXaWlXV2ldUAAAMTFpp7rHx8fHx8fHx8fH09PT09PT09PT09Pf39/NrqGuwMDZ2dm6use6usfZ2ebm2c3Nzc3N2c3Nzc3Nzc3Z2c3Z2c3f39/Tx8fH2fLm5vLy5ubm2dnZ5ubm5ubm5vLy
+8v/s7P//7OzZuqenp5WVoaG0tLS0x8fH09/f39/f09PT09PT08fHx8fHx8fHx8fHurq6urq6rq6urq6uoaGhoaGVlZWViIiIiIiIdmlXV0pKSkpKSkpKSkpKSkpKSkpXY2NjY2NjY29vb29vb3x8fIiIiIiVlZWhoaGhoa6urq6urq6urq66uq6urq6uuq6u
+rm9XV1dpaVdXb11dXV0AADdvp8fHx8fHx8fHx8fHx9PT09PT09PT09/f39/Hp6e6x9PTx7q6x9PTx8fHx8fHx8fHx8fZ5uby8ubm5s3N5vLZ2dnZ5vLZ2c3f39/f3+zf39/H3/j4+P/y5vL/7Ozs7Ozs7Pj////y8vLm07SVlZWhoaGhoaGuwMDAwNPT09PT
+09PT09PTx8fH08fHx8fHx7q6urq6rq6urq6uoaGhoaGVoaGPj4+Pj4+PdnZXPj4+SkpKSkpKSkpKV1dKSldXV2NjY2NjY2NjY2NjdnZ2dnaIiIiIiKGhoaGhrq6urq6urq6urrq6rq66urq6rq6uro9XRF1vXV1dXV1daVdXAABpp7q6urrHx8fHx8fT09PT
+09PT09PT39/f39/Ap7TH08e6oaG6urrH09PT09PHx8fH09/Tx8fT5vLy8vLT08ff7Oz4+OzZ2dnZ8ubZ2dns7NPT3+zs7NPm5uby8v//////7Ozs+Pjs7Ozs7Pj4+N+6rq6urqGhoaGurq6uurq6x8fHx8fHx9PTx9PTx8fHx8fHx7q6urqurq6uoaGhoaGh
+oZWVlZWViIh2dmNKNzc3N0pXSkpKSkpKV1dXV1dXV2NjY2Nvb29vb29vfHxvfHx8fHx8j6GhoaGurq6urq6urq6uurqurrqurrq6rq66uqFjPmNjV2lpV2NjY1BjYwAAiLq6urrHx8fHx8fHx8fHx9PT09PT09PT09O6rsDAwNO6urq6usfH08euurrZ2dnN
+zdnZ2ebZx8fHx8fZ7Nnm5sfT7NnNzeby8v//7OzZ2fLy39/y8ubT5ubT5tnN5vLy5vL/////8vLy8vLy///s7Ozfx7qhiIihj4+hoaGurq6urq6urrq6usfHusfHx8fHx7q6urq6urqnp6enp6enlaGhj4+CgmlpXV1ERERERERERERERFBQUFBdaWlpaWlp
+dnaCgoKCdnZ2dnaCgoKCgoKCm5ubrq6urrq6rq6urq66urq6urq6urqurrqurq6IUFBQaWldXW9jV2lpV1cAAJW6urrHx8fHx8fHx8fH09PT09PT09Pf39/f39/Tx8fHrq6ursfT09/fx8e0wMDAzdnZ2dnZx9Py39Pf3/LZzc3Nzc3N39/f39PHx8fH0+bm
+5uby5tnZ2eb4+Ozs7NnZ+P/s7Ozs09Py///////4+P////////Ly066CgnaIiJWhoaGhoaGhoaGhoaGVlaGhoaGhrq6urqGVoZWVlZWIiIh8aWlpXV1dXUpKSkpKSkpKV1dXV1djY2NjY2N2gm98fHx8iIh2doJ2doKCgoKPj4KCj6Gurq6urq6urrq6rq7A
+tLTAwLS0tLS0tLS0tMChXTdXb11daWlXV2lXV2NjAACPurrHx8fHx8fHx8fH09PT09PT09PT0+bZx9Pf39/f38fZ2ebm2dnAwMDf08fH39PT08fHx8fH5ubZ7Ozs39/T09PT08fZ2dnZ2dnZ5ubm5tPfzd/4+P//7Ozs+Pjm8v//8vLy/+zf8vLy39/s7P//
+///////////////42bqhj4KCgoKCgoKPj4+Pj4+Pj4+Pj4+Pj4KCdnZ2gnZ2dmlpaWldXV1dSj5KSkpjY2Njb29vfHx8fIh8fHx8iIiIiIiIiHx8iIiIiJWVlZWViIiVlaGurq6uurq6urq6urq6urq6urq6urq6uq6uurq6fEpdXV1paVdXb2NXaWlQXQAA
+obq6usfHx8fHx8fH09PT09PT09PT09PAm5ubrs3Nzc3N2cC0tLTAwM3m5uby5ubmzc26x7TAzdnZ2ebm2dnHus3N2ebm+ObZ8t/f3+zs39Pm8tPm5ubm09P42dns7Pj4+ObZ2ezs7Ozs7N/f39//+Obm5ubZ7P//////////////+Nm6oXZpaVdXY2Nvb298
+fHyIfHxvb29vb29jY2NjY1dKSko+PkpXV1dvb29vgoKClaGhoaGhoaGurq6hoaGhoY+Pm4+Pj5ubj4+Pj5ubm67AwMDAwLS0wMC0tMDAwMDAtLTAtLS0tLS0tLS0iEpdXV1paVdjY2NXaWlQY2MAAKHAwMDAwMDAzc3Nzc3Nzc3Nzc3N39Ouoa7Hx8fZx8e0
+tMff38euwNPHx7S0tLS0tMfZ2fLy8vLy39/T7NnNzcDNzebm5ubm2cfT09PT09O609Pm5tnZx9nZ5vLy5tnm0+bm///4+P/s3/j47Oz///Ly2dnZ8vLy8vLy//jm5v//////////////8seuurqViIh8fG9XV1dXV1dXV0pXY2NjY29vb4KClaGhoaGurq6h
+oZWVoZWVoaGhoa6urrrHtLTHurq6uq6urq6hoaGhoaG6usfHurrHx7q6x8fHx8e6urq6urq6urq6urq6p2lKSmlpXV1vb1dpaVdjb11QAAChurq6usfHx8fHx8fHx9PT09PT39O6x8fT09O6uq6urtnNzc3Z2c3Nzc26zdnZ2dnZx9nm5ub45sDAwMDf7P//
+//Lf3+zTx9//5tnZ2c3Z2eby8t/f8t/T5vLm5tPf09//+Pj47Nn47N/f8v////Ly39/y8vLy/+zs7N/f7Oz4+N/f39/4///////////////////y8v/47N/N2ebZ2dnZ2cfHx8fHx8e6rq6ursfHrq6urq6urqGhrq6urq66urqnp7q6usfHx8fHx8fHx7q6
+usfHx8fHx8fHx8fHx7q6x8e6urq6urq6urq6unw+V29dXXZpV2lpXV1paVdjYwAAobS0wMDAwMDAwM3Nzc3Nzc3Z2dnNzcDAtLS0tLS0tMDAwNPTx9PAwMDAwMDT7N/f38fHx7q6zd/s7Nnm5ubm08fH09/f8v/y8t/Tx8fZ2ebm5s3Nzc3Z2fLm5ubAwN/s
+2dnZ2dnm5uby8vLf39/f3+zs+P//8ubm2dns//j439PT+Pjf7Ozf39/f39PT8vLy///////4+Pjs7Ozf09/fzc3Z2c3N39/f383Nzc3NtMfHtLS0tLS0x8fHuq6hrrq6uq6urq6urq6uuq6uurq6zc3Nzc3Nzc3Nzc3NwMDAwMDAwMC0wMC0tLS0tKFdUFBQ
+aWlXY3ZpV2lpV1dpXV0AAKHAwMDAwMDAwMDAwNPHx9PT09PT09PT09PTx9PT08fHx8e0p7TTx8fZ2dnZ2dnHx8fHusfHx9PT5ubZ2dnHx8fT39/f7NPHx8fH2ezs7Ozs7NPTx8ff7Ozs39P47Ozs7Ozf09Pf39/s7Nnm2dnZ7Ozs7Oz4+OzT5vLy/////9nZ
+2dns7OzZwNPT0/js2dnZ2dnm09Py8vLy///////47OzfzdnH09PT09PT08fH08fT08fHurrHusfHusfHtLS0tLS0p7qursDAtLS0tKe0x8fH08fHx8fHx8fHx8fHx8fHx7q6urq6urq6x7p2Pkpvb11db2NQaWlXV2lpUGNjAACburrHusfHx8fHx8fH09PT
+09PTx9PT09PT08DT09PT5tnZ2dnZ2dnZ2c3Nurq6urrHx9Pm5tnZ2dnNzbq6x9PT39/f39/Hx8fZ7Ozf7Ozszd/T3/Ly8vLTx+zs7Ozs7Ozs7N/f3+z4+Ozs2dnZ8vLm5vLy/9PH3///7OzZ2dnZ5vLy8vLy8vLy8vLT0/Ly8vLfx8fT7N/TwMDA09Pf7Pj4
++Pj47Ozs39/fx8fH08fHx8eursC0tMe6rq6urq7Huq6uurq6x66urrq6p6e6x8fTx8fT08fHx8fHx8fHx8fHurq6urq6uq66urqVSkppVz5XY1dpaV1daWlQXWlXVwAAlbq6usfHx8fHx8fH08fHx8fH09PT09PTx7TAwNPHurq6urq6x8fH2ezs3+zs39/s
+39/f383NzcC0tN/T09Pf3+zZ2ezsx+by8t/f3/LTx9nm5ubZ2dns09Ps7Ozs7N/f08fZ//Ly8v/m09PT0+by8ubZ2dnZ2ezs7OzZzc3Nzc3f39/f39PH2fLm8vLy8vLm2dnZ2ebm09PT09PT39PTx8fT5vLy5vLy8vLy8ubm2dnHx7q6usfHtLS0tLTHx8fT
+08e6uq6urrq6urqntMfHx9PTx8fHx8fHx8fHx8e6urq6use6urq6urq6Y0pXVz4xPl1vY2Njb1djb2NXY2MAAJW6x8fHx8fHx8fHx8fT09PT09PH09PTx7S0p6e6oaG0tLTH08fHx9PTx67A09PT09/s7Ozs7Ozs7Ozs7N/f39PTx8fT39/s08DA09PT09Pm
+5tnZ2dnZ2fLy5ubm08fT09Py///y5uby8ubm5uby5ubm09//+NnZ2dnZx8fH3+zs7Ozs2dnZ5vj45ubZ2dnAwNnZ5vLy2dnAwM3Z2cfHuq7A09PTx8e6urq6rsfZ2ebm5ubm5tnZ2ce6urq6urq6rq6ursDArq66uq6urrrHx9PHx9PHx8fHx8fHx8fHx8fH
+urq6urq6urrHiEpKY0orSmNjV2NjY2NvY1BjY1dXAACPwMDAwMDAwM3Nzc3Nzc3Nzc3N2dnZwLS0tLS0tLS0tLTTx8fHx8e6urq6x8e0tMDA09PHurrH09Pm8vLy8vLy8vLm5ubTx8fHx9PT0+by39PAwNnZ7Ozf39/T3/Lf0+by8vLf07rH5vLy///4+ObZ
+2dns7Oz/7NnZ5tPm8v/m8vLy5ub45tnZ2ebm09/y8tnZ2dnHx8e60/Ly8vLy2cfHx8fH08DAwMDAwMDT09PHtMfHus3Z2dnZ5ubZ2dnNzc26rq66uq6uuqe0tMfT09PTx8fHx8fHx8fHx8fHx8fHx7q6urq6urq6oUpKb103RGNjY29jV2lpV2NvXV1paQAA
+j7TAwMDAwMDAwNPHx9PT09PT09PT09PT08fHx7quwNO6urq6usfHurq6x7q6x8fH2dm0tMfHx9nZ2dnZx8fHx8fT3+zs+Pj47Ozs3+z45tPTwNnZ2dnZ2ezZzdnZ7Ozfx+by2dnm+Ozs7NPT09Pf8vL///j/+Ozs39////////Ly8tnZx9nZ2dnZ2dnZx8fH
+09PTx8fH2dnZ2fLZx9PT09/f09/f08fHx8fH09O6urq6rrrNwMC0tKfAwMDT09PT39/T08fHx8e0p6fHx8fT08fHx9PHx8fHx8fHx8fHurrHx7q6urq6unZEUFBQN0RXdmlXaWldXXZjV2lpV1cAAI+6urrHx8fHx8fHx9PT09PT09PT09PT09PT39/f7Ozf
+39/f09PTx7rHusfHx8fH09PHx8fHx8fH08fHx8fTx8fT08fT09Pf08ff3+zs7P//////+ObZ2cfH0+bm5tnZ2c3f7Ozs39/s2dnZ2dnZ7Ozf8vLT39PT5ub4+P////////////////j439/Nzc3m2dns7MfH08fm2c20tNPf09/f09Pmzc3Nzc3Z2dnZx7q6
+rsDAwMDAwMDAwMDAtLSnp8fHx8fH09PH09PTx8fTx8fT08fHx8fHx8e6usfHx8e6urq6uq66x5U3RGNXHzFjY1djY1dXaWlQY2NXV2NjAAChurrHx8fHx8fHx8fH09PT09PT09PT09PT09Pf39/f39/f3+zf3+zs7Ozs7Ozs7N/f383Nzc26x9PHx9Pf383N
+zcDA08fZ5tnZ2dnHx8fT5ubm8vL////////4+ObT3/Lm5ubm5uby8tPT39/f7Ozs383Nzd/f3+zs7Ozfx8fH09Pm8vLy///4////////8vLf08fT8ubm5tPT08fHx9PTuse6usfHx7rNzc26uq7A39/Tx9nArq66urq6urq6rq7Hx7Snp6enp7rHx8fTx8fT
+x8fHx8fHx8fHx9PHx8fHtLTAwLS0x6FXPmlpMTFjY1dpaV1daWlQY2NXV2lXSgAAocDAwMDAwMDAzc3Nzc3Nzc3NzdnZ2dnZ2dnZ2dnZ5tnZ5ubm5ubm5ubm5ubm5uby8vLy8vLy8vLy5ubZ7Pj4+Pj47Ozf08fH09PT09PH09/f09PTx9ny5ub4+Pj/////
+//jm5tnZzc3N2dnZ5vjs383Z+Ozs7Ozs7Pjfx9Pf3/Ly39/f39/sx8fT7Oz4+P////////j47N/f7OzZ2cfT09PTx9PAwMDT08fHx7q6utPHx8fTx8fH09PT09/s2dnZ2bq6uq6ursfHx9PHx9PTx8fHx8fHx8fHx8fHx8e6urq6urq6urpjN11dRDdKV2lp
+V1dpaVdpaV1daWlXV2MAAIKhoa6urrq6x8fHx9PT09PT09PT09PT09Pf39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Oz4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj///j439/s08fT09/s7Ozf39/f09PT39PT5vLy///////////47OzZzc3m5vj47N/s+N+0tMDAwMDZ
+2ezZ2dnZzdnZ5ubZ5tnNzdnZ7Oz4+Pj///////Ly8se0wMDAwNPT09PTx7S0wMDfx66htLS0x7ShoaGhobrHx8fZ2dnZ2dnZ2dnH09PHx9PHx8fHx8fHx8fHx7q6x8e6urq6useVSkpXV0pKY29jY2NjV2NjY1BpaVBQaV1QAABpiIiIiJWVlZWVoaGhrrq6
+usfT09PT39/f39/f39/f39/f39/f7Ozs7Ozs7Ozs7Ozs7Ozs7Oz4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4//j/+Ozs7NPTx9PT08fH09PT09PT08fHx9/s7Oz4////////////7NnZ2c3Z7Ozf+Pjm8v/s09Pf39/y5tnZ2cff+N/f7N/f38C0x8ff7Oz4+Pj/
+//j4//j47N/Hurq6utPT09/fzbTA08fHx7qhtMfHurq6oaGhrq7A09PT09PT09PHx8fHx8fHx8fHx8fHx7q6urq6use6VzFjYz4+Y2NXb29XY2NjV2lpV1djY0pdXQAAV2lpaXx8iIiVlZWVlZWVlZWVlZWVoaGurq7AwNPT09Pm5ubm5ubm5ubm8vLy8vLy
+8vLy8vLy8vLy8vLy8vLy8vLy///y8v/y8v/4+Pj4+Pj///j///j47N/f7N/f39/f09PH2ezs7NnZ2c3N5ubm+Pj4////////////+Obm09Pm09/T3+zf39/Hx8fZ2ebm5tnNzbrs39/T09PHusfH09Pm8vLy8vLy8vLy8vLf09O6p7THusfHx7S0tLTAwLSn
+p6e0tLSnp8fZ2cfH08fH08fHx8fHx8fHx8fHurq6urq6x7q6gj5XYz4xRFdpdmNjb29XY2NXV2lpUFBpXVAAAFdpdnZjY3Z2aXx8fHyIiIiVlZWhoaGhoaGhoaGhoaGhoa6ursDAwM3N2dnm5ubm5vLy8vLy8vLy8vLy8vLy8vLy8vLy///////y8v//////
+///////////////4+PjZ2dnZ2dnZ2dnZ2cfT7Ozs2dnZ2fLm09Pf3+z/8vL//////////+zf08fHx9nZ2dnm09PT08fT7Ozf38fH7OzT0+bZ2ce6urrN383Z7Ozs+Pjs7Ozs39/fx8e6urrHx9PT5tO0tLTHx9nZx9PT09PTx8fHx8fHx8fHx8fHx8fHurq6
+urrHrko+Y29KN2N2aWlpdmNjdmlXb29XV2NjUGNjAABXgnZ2iIh2goKCdoiIdnaCdnZ2gm98fHx8j4+PoaGhobS0tLS0tLS0tLS0tLTAwMDNzc3N39/f8vLy8vLy8vLy8vLy8vLy8vLy8vLy////////8v/////////////////////4+Pjf39/f09/y8t/T
+383Nzd/s7N/f7N/Hx8fZ2dns+Pj4///////4+Ozfx8ff+Pjs7Ozs383Nzc3N2dnZx8fH09PT39/f08eux9nNzdnZ5ubm5vLy5ubm2dnArqGuurrH09PT09PTx9PTx8fT09PT08fH08fHx8e6x8fHx8fHumkrV2lEJUpjY2N2Y2N2aV1paWldb29XY2NQUAAA
+V2N8fG9viIh2iIh8fI+Pdo+PgoKViHyPj3x8iHZpdnZ2aWldXW9vfHyPj66uusfHx8fHx8fHx8fH09PT5vLy8vLy8vL////////////////////////////////////////////4///////s39Ps39PT09PT5tnZ2dnZx+zs09Pf09PHx9Pf3+zs////////
++Pj45ubTx7q62dnZ2dnHx8fH0+zZ2bS0tMDAwNPT08fHx7qurq6609PT3+zf3+zs39/f09PT09PT09PH09PTx8fHx8fHx8fHx8e6use6x8e6x5s3N11KN0pXY3ZjV2lpV2lpaVdpaVdjb2NXb28AAFeCb2+CgmmCgnZ2iIhvgo98fI+PdoiViHyVlYKCoY+P
+j2lQUFBQUFBQUFBQUFBQUGl8j6G0x9PT08fHx7q6usfH2eby8vLy8vL////////////////////////////////////////4////////////////8vLy5tnN2dnZ5ubm2dnZ2dnZ7NnZ2dnHx9PT8vLm+Pj4+P//+Pj439/s2dnN2ebm2dnAwMDA09/fzc3N
+urq6x8fT08e6urq6ocfTx9PT09Pf39PT09PT09PH09PT09PTx8fHx8fHx7q6x8fHx7RdMUpvVzdKaWlXaWlXV2ldUGNjV1dpaVBjY1dKAABXaXx8aXyIfG+IiHaCj4J2j498fJWIdo+PgoKViIiVlWlKV0pXaWlpaV1ddpWViIiIb11dXV1pfI+hrsDA09PH
+x8fHurq6x9Pf3/Ly8vL///j///j////4///4+P//+Pj///j///j////4///////////////////////////y8tPHx9/fzebm5ubm5ubm8t/T09PHx9nZ5vLy8v//////8vLm2c3Nzc3N2dnHx7qurq7AwNPT07TT5sChusfTx7q6usfZ2dnZ2dnZ2dnZ2c3N
+zc3Nzc3NwM3Nzc3NwMDAwMB2JUpvUCU+Y1dpdmNjb29daWlXV2lpSldjUFBjMQAAV3xpaYKCaYiVfHyPj3aIlXx8j492j5uCgpubfI+hiGldSjdKY2NXY2NXfKGVlaengoKCb3ybm4iIiHx8fIibrrq6x8fHurqnp6enp6fAwNPm+Pj4//j////4///4+P/4
++P////////j///j4///4////////+P//+P/////////////4+PjZ2ezZzc3f39/T08fHx8fZ2dnHx9nHx9Ps7Oz4+Pj4+Ozs39PAtLTH5ubm2dnZzdnHrqHAzdnZx7qurrrN2dnZ2dnZ2dnZ2c3Nzc3Nzc3Nzc3NwMDAwMDAwMChV0pdSjdKV2l2Y1dpaVdj
+b2NXaWlQXWlpUGNjPgwAAGNjdoh2aYiIb3yIfG+IiHZ2lYh8j498fJuPfI+bgmlXPj5KY1dXaWlKdqGPj6GhgoKCaYKhlYihoY+bm4+PoaGVlaGhobTAwM3NzcDArq6uoaGhtMfZ7Ozs+P/4////+P////////////j/////////////+P//////////////
+///////4////+Pj47Ozfzc3m5sfH08fH2dnmzdnZ2dnNzc3Z2dns+Ozs///s7OzZzcCurq66x9PT08fZx66uwNPT09/f09Pf09PT09PT08fT08fHx9PHx8fHx8fHuseuSkpjY0pKV2lpXV1vY1djb11daWlXY2NXV2NjPhIAAABXgoJpgoJvb4iIb4iIdnaP
+j3aIiHx8m498j6GIiHxXPkpXV1djY1BQfIiIoaGIiIhvb4+bfIihoY+np3yIp6eVp6d8Y2Njb3yIiKGhusfH2c3NzcDArqGurrrH2ezs+P/4//////////////j///j4///4////+P//+P////////j4//j4//j4+P//////+P////Ly383Nzc3NzdnH09Pm
+x8fHx8fHuq66x8fT5ubm+Pj4+Pjm5ubTwLS0tLTH09PH39/f39/f39/f09PTx9PT09PT08fH08fHx8fHx8fHaT5Xb0QlV3xvY3Z2XWlpaV1vb1djb2NXaWlXYyUAAAAAY2N2gm9viIhvgo+Cgo+PdoiVgoKPj3aIm4h8lYhKSko+SmNjV1djY3ybm4iVoXxp
+doiIfJWViJuniIiVlYihrpWCaVdKXV1dXWldXYKnp7TAwMDT09PT39/Tx8e6usfH09Pm8vLy8v////////////j/////////////+P//+Pj/////////////////////8v////////j/+Ozf09/Tx8fH09PT09Ps7NPTx8fHx9nZ2dnZ7Ozs7Ozs7Ozf3826
+utPT39/f39/T09PT09PTx9PT08fT08fH08fHx8fTrjdXb1c3SmlpaXZjY3x8Y29vXV1paVdjY2NXaVcYAAAAAFd8fG+Cgm98iHxviIh2gpWIfIiVgoKViHaPm4JjUD4+V2NXV1dXV4Khj4+hlW98fHx8j498m6ePj4+Pb3yViIiIY0pKV1dXdnZjY4iVlaen
+m5uPj3yVrq7A09PT7Ozf39/H08fH09PT3+zs7Pj4+P////////j////4////////+P//+Pj/+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj///j4//js7N/Hx9PAwNPTx8fH2dnNzc3NzcC0wNPf39/f39/f39/f39/f39/f39/T09/T09PT09PT09PTx8fHx8fHx11E
+XV1KSldpdmNjdnZjb29vXW9vV2NjY1djY1cxAAAAAABXY3aCb2+IiGmCgoJ2lZV2iJWCgpWVgoKbiIiIY0pKV1dXY2NKV4KCdpWVgoKCb2+Pj3aIoY+PoZV2goJ2doKCSkpXSkpjY2NjY298obSbm66VgoKCgoKhoY+hoaGhtMfH09PT09PT39/f09PT09/f
+3+zs+Pj///j////////4+P//+Pj4+Pj/+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4//js7OzT5ubHx8ffx8fHx7rT5ubm5ubm5ubZ2ebm2dnZ2dnZ2dnZ2dnZ2dnN2dnNzc3Nzc3Nzc3N2ac+SnZjRGN8b2Nvb2Njdmldb29jY29jV2lpSl1KBgAAAAAA
+V3ZpaYKCb3yIiHaPj3aClYh8j498iJuPgpWVfHyPgm+IiGlpdnZ2iJWIiJV8Y2+IiHyVoYihro+ClXxpiIh2Y1dXRFdvb2Nvb1eCp5uPrq6IiIh8iKGhj6G0oaGhlYihrq6uroiIobTH09/f39/s39/f09PT09Ps7Oz4//j////////4+P/4+Pj/+Pj/+Pj4
++Pj4//j4+Pj4+Pj4+Pj4+Pj4+Oz4+Pjs+Pjs7Ozf39/ArrrN39/s7N/s7Ozf39/f39/f39/f09Pf39PT09PT09PT09PHx8fHx8fHx8djRGNQPlBjY29vY2Nvb11vb2Njb29XY2NXV2NXGAAAAAAAAFdvgoJpdoh8b4iIdnaPj3aPj3x8j498j6GCgqGVgqGh
+j4+hlXyPp4+Pp6d8iJWIiKGVfI+hiIiVlXZ2iHxpdlBQUFBQXW9vY2OCm4+np5WVlZVpj6F8iK6biKGhiIiurpWurnZXV2Njb3x8iIihtMfT09PT09PT09PT09PT09Pf7Ozs+Pj4+Pj4+P//+P//+Pj4+Pj4//j4//j4+Pj4+Pj4+Pj4+Pj4+Ozs7Ozs7Ozs
+7Pjs7Ozs7Ozs7N/f7N/f39/f39/T09/f09PT09PT09PT09PT09PH08fHx9OnV1djgoJpdnZpaXZ2XWl2Y2Nvb2NjdmNXaWlXPgAAAAAAAABXfG9vgoJpfI98fI+Pb4KPfHyPj3yPj4+ClZWCj6GIiKGhiIinlYihoYiIp5WIoa6VlaebiJubdoKCgm98iFdX
+V0pKaXZpaWlpfKG0oaG0m4KCj4+Pp6ePobShiJWVfJWuoYhpXUpjdmlpdmlpiKGhoa6uoa6ux9Pf39/f39/f08fT08fT09PT3+zs7P////////////Ly//Ly///y///y8vLy8vLy8vLy8vLy8ubm8vLy8vLm5ubm5ubm5ubm5tnm5tnZ2dnZ2dnZ2dnZ2dnZ
+2dnZ2cfH08fH09PTaUppfGlpfG9jb3xjY3ZpXW9vXV1vY1dpaVdXVxgAAAAAAAAASml8fG98j3xviIh2do+CdoiIdnaPj3aPj3x8lZWClaGCgqGVfJWhiIihoYiVoY+Pp6eIoaGPj6GhiJubiHxpV0pKaWlddnZXdqGVla6uiIihiIinlYKbrqGVp6d8fJWC
+do9pRERjY2N2dmlpj6GVrq6hoYh2aZWnm6e6uq7H09Pf39/f39/f08fHx9PT0+bm8v////////////Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLm5ubm5vLm5ubm5ubm5tnm5tnZ2dnZ2dnZ2dnZzdnZzc3Nzc3Nzc3Np1dpaWmCdmN2dmlpdmlXaXZjY3Z2
+V2NjV1dpaT4MAAAAAAAAAERjV1djY2NjdnZ2j498iKGCgpWVfI+biHyVlYKPm4iIm5uClaePj6GhiJWnlYinp4iVp5WIoaGPj6enj6enfHyIdmlpdmNjdoJ2j6eVlaGPdnahgo+np4ihoXx8iIh8fJVvV2NjY3Z2dml2doKhuqGVro9vfKGhiKGhlaGhoZWh
+tLS0x8fH0+bm8vLm5tnZzc3N2dnm5vj4+Pj4+Pj4+Pj4+Pjs+Pjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozf7Ozf3+zs39/s2dnm2dnZ2dnZ2dnZ2dnZ2dnZx9PTx8fT02lKY3x8aWl8b2N2dmNvfGldb29jY3ZpV2lpV1clAAAAAAAAAABpiIiIfHx8SiU3NzdQaWlp
+doh2iJuPj5ubgo+hlYKbm3yIoY98lZWCj6GVgqGhiIihlZWhoY+PrqGPoa6Vla6ulaGulYiViHaVp5ubrqGCgoKCj6Ghlae6p4+hlXaPm4KCaVdKV3x8aXx8Y4Kurpu0tI+PgoKPoZWIoa6hoaGhj6G0p6enj4+hurrH09Pf39/s7N/f09PT09Pf39/y8vLy
+8vLy8vLy8vLy8vLy8vLy5uby5ubm5ubm5ubm5ubm5ubm2ebm2dnZ2dnZ2dnZ2dnN2dnNzc3Nzc3NzadKXXZpaYJ2aXZ2aWl8fGNvb2Njb29XaWldXWk+BgAAAAAAAAAAgqGhoaGuiB8AAAAMGCUlMTExSmNXY3x8fIibj4+hoYiIp4+PoZWCj5uIfJWVfI+h
+j4+np4ibrqGPp6ePoa6hj6enlZW0p5WurpWVtKeIoaGIm66hlae0m5ubm2+Pm3x8b1dKV29vb3x8Y4KuoaGuro+Pj2+IoaGIoa6hj6eVgpu0oaG0j2OIp6enp6entLTHx9Pf7Ozs7Ozf39/T09PT09/s7Ozs+Pjs7Pjs7Ozs7Ozs7Ozs7Ozs7N/f7OzZ5ubm
+2ebm5ubm2dnZ2dnZ2dnZ2dnNzc3Nzc3NzcBdUHZ2doJvb3x8Y2N2dmN8fGNjdmldaWlXY29QGAAAAAAAAAAAAIKhoaGhtIgYAAAAAAYYDAwMGBgYJSUlPlBQUGNvY3aPj4KVoY+bp5uIoaGPj6GVgpWhgoKhoYihrpWVrqGIoa6Vla6ulaG0oZW0tI+ntKGh
+tLSVrq6Vla6hfIiIiHaVgldKSldjfHxpaXyPj66um5ubgm+VroiIrq6brq6IiIiIiKG0lWmIoZWnp6enp6ebm4h2m7rHx8fH09PT39/f39/f09PH09PT09/f7Ozs7Ozs7Ozs7Ozs7Ozs7N/f39/f39/f39/f39/f39/f39PT09PT09PT09PH09ObSl1viHZp
+dnZpaXx8Y3Z2aWl8fGlpdmNjb2NXMQYAAAAAAAAAAACCoaGhobR2GAAAAAASEhISEhISEhISEhISEhISHys3N0pXSldvb2+Cm4iIoaGIm6ePj6GhgpWhiIihoY+htKGVrq6VobShla6ulaG0p5WurpuntLSVrq6VobS0la6uj3xpXV1pfGlpfG9vlaGVobqh
+doKVlZWhoY+hrqGPoY9piJWCgoKCgqG0oZWurpWntKGPoaGVp6ebm6e6usfHx8fH09Pf39/f09PT08fT09Pf7Ozs7Ozs7Ozs39/s7N/s7NnZ5ubZ2dnZ2ebZ2dnZ2dnZ2c3Z2cfH09PHXURvfG98iHZ2goJpdoJpaXZ2Y2N2Y1d2dmNvYxgAAAAAAAAAAAAA
+dqenp6e6bwwAAAAADAwMDBgYGBgYGBgYGBgYGBgMGBgMDBgYGCU3NzdXY1dpfHx8j5uIlaenj6enj5uuoZWnp5WVrqGPoa6Vla6ulae0oaG0tI+uuqGhuq6VrrqhobShiJWhiHyIiGl2m4+Pp7SViIhvfKGhiKG0oaGum3x8iIh2j49jgq6ulae0laG0oY+h
+rqGhrqGPoaGPm66urq66urq6usfH09PT39/f09PHx8fHx9PT39/f3+zs39/s7N/f39/f39/f39/f39/f09PT09PT09PT08fToURdaWmIfGl8fGlpgnZjdnZjY3ZpXW9vY2Nvbz4SAAAAAAAAAAAAAIKhoaGhtF0SAAAABhgYGBgYGBgYDBgYGBgYGBgYGBgY
+GBgYGBgYDBgYGBgYGBglPj5KY2NjfI+CgqGhiKGuoY+np4+hrpuPp6ePoa6hla6ulaG0oZW0tJWnuqeVrrqbp7quobq6m5uurpW0tJunp4+Cla6bj6enj6e0j4+PgoKCj49piKGPp7Sbm66uj6GIb4inp4+hrqGVrq6Poa6hla6uoa6uoa7Hx8fHx8fT09PT
+08fHx8fHx9PT09/f39/f39/f39/f39/f39/f39/T09PT09PT09PTx2k+Y4KCaXx8b2+CgmN2gm9jdnZjb29jY3ZpaWklAAAAAAAAAAAAAAB8oaGhrq5KBgAAAAAMDBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUlPkpKV29vb4iI
+iJunp4+np5WVrqGPp6eVoa6ula66m5u0tJW0tKGhuq6Prrqhobq6obTAoaG0tJWuuqenurqhrrqbm5uPb4KVdoKCb3ybtKGVrq6Vp7SPY3yhj6Gum4+np4+brqGPp6ePoa6hj4+hlae0tLS0x8fHx8fHx8fT08fHx8fHx9PT09PT39/f39/f39/f39PT09PT
+09PT09PT06E+V3Z2doJvb3x8Y298b2N2dmNjdmlddnZjY3ZQEgAAAAAAAAAAAAAAgqGhoa6hMQAAAAAAGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAwYGBgYJTExMUpdXWmIfHyhoY+hrqGVrq6PobShla6ulae0oZW0tJuu
+x66hurqhobqhobS0lafAp5W0tJunuq6btLSPobSnj6Gudnanp5WurpubtLSVlZWVlbquiJWnlZWuro+hrpubrq6Poa6bm6enj6GhlZWurq66urrHx8fHx9PT09PHx8e6x8fH09PT09Pf39Pf39PT09PT09PT09N2PldviIhpfHxpaXx8Y298Y2N8b2N2dmNj
+b29dMQwAAAAAAAAAAAAAAIKnp6e0jyUAAAAAABISEhISEhISEhISEhISEhISEh8fHx8fHx8fHx8fHx8fHx8SHx8fHx8fHx8fHx8fHxISEhISEhISEjE+PlBjY2OIiIiIrqGVp6ePoa6hj6GhiJWuoY+urpuntLSVrrqhobqula6uoaG0tI+uupubtLSVrrqh
+oaGhla66oaG0tJWhlWlpoaGPoZVvfKGhj6GulZWnp4+hrqGVp6eIoa6hj6eniJWniHyPp6enurq6urq6x9PT08fHx8e6urrHx8fT09PT09PT09PT09OuSj52dnZ2iG9vgnZpdoJpaXx8Y2N2Y2N2dl1vVxgAAAAAAAAAAAAAAACCoaGhtHwYAAAAAAwYGBgY
+GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJRgYGBgYGBglJTFKV1dpgnZ2m5uIoaGPj5uPfJWhiIibj3yVp5WVtLSPp7ShobS0lae6p5u0tJWnuqebtLSbp7qum7S0m6e0lW9vlZWhtKGCj6GPm7Shj6enj4+n
+m4ihoY+PtKePoa6VlaeViJWhiIihoY+hoZWhoaGuurrH09PH09PHuse6urrHx8fZ2dnZ2dnZgkpdaXyIdml8fGl2iHZpfHxpaXxvY3Z2XV12djcSAAAAAAAAAAAAAAAAdqGhoaFdEgAAAAAGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIf
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fEhIfHx8rKytEXV1pgoKCj6GPj6GVfHyVfG+Pj3yIoY+Poa6VobSnla66m5u0tI+0tKGhtLSVrq6hobS0laenlaG0tI+ntKGhrqGPoa6bj6enj4+PgoKnp4ibp4+PoaGIm6ePj6GhiJWnlZWnm4iVoZWV
+oaGhobrHx8fHx8fHurq6urrHx8fZtFc+Y4iIb4KCdnaCdml2dnZpfHxjdnZpaXZ2Y2MlAAAAAAAAAAAAAAAAAIinp49vJQYGBgYGEhISEhISEh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fPkpKV2lpaYihiIihlXyPj3x8iIhviJWIiKGhiKG0oZWuro+htKentLSVp7S0lbS0m6e0p5uuuqGhtLSVrq6Voa6hj6GhfIihoYihoZWVp6eIm6ePj6GhiJunj4+bm3ybm4iIm5uCj498iHx8obTAwMDAwMDAtLS0p2k3XYh8b4KCaXyI
+dnZ2gmNvfG9jdnZjY3xvY28+DAAAAAAAAAAAAAAAAACIlYJdNxgGBgYGBhISEhISEhISEhISEhISEhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHxISEhISEjElMURXV1d2doKVlYiI
+lYh2j498fJuIdo+biIihlYihrpubtLSVrrqhobS0j6e0oaG0p5WntKGVrq6Poa6bj6enj6GhlYinp4+hoY+Cm5uIlaePj6GhiIihj4KVlYKPoY+PoY9XY4KCdo+Poa66x8fHrnY+SmlpgoJvb4J2aXaCaWl8fGNvfGlpdnZdaXxjJQYAAAAAAAAAAAAAAAAA
+dnZKKysSAAAMAAASEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx83NzdjY2OClYiIoaGIm6eIiJWIdoiIiHybm4ihrqGhtLSbp7Sn
+m7q6lae0oaGuro+hrqGPp6eVlaeniKGhiIinm4iIiHyPp6eIoaGPj6eniKGhiIihlXyPoYh2aYKCj5uCgoKCgo+hlXZdSld2iHZpgoJpdoh2aXx8Y298aWl2dmNjfGldShIAAAAAAAAAAAAAAAAAAEo3JRgYDAAAAAAMDBgYGBgYGBgYGBgYGBgYGBgYGBgY
+GBgYGBgYGBgYGBgYGCUlJRgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUYGCUlJSUlGBgYGBgYGBgYGBgYGBgYGBgYDBglJSVEV1djfHx8laGPm6enj6GhfHyPfG+PoYiVrqGVrrqhobS0la6uoaGuoY+hrpWVp6eIoaGPj6GhiJuniHaV
+oYibp4+PoaGClaGPj6GVgo+hj4+PY1d8lYh8lZWCj4+Cdoh8b4KPgm+IiG98iHxpgoJpaXx8Y3Z2Y2N2aVdvYyUGAAAAAAAAAAAAAAAAAAAfHwwYKwwAAAAAAAwYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglGBgYGBgYGBgYGBgYGBgYGBgYGBglJRgY
+GBgYGBgYGBgYGBgYGBgYJSUlJRgYGCUlJSUYGBgYGBgYGBglJRgYGBgYJSUlJSUlJSUlGBgYGBgYJSU3V2NjfJuPj7SniIiVfHyPj3aIoYiIp6ePoa6hlbS0j6e0oY+np4+bp5uPp6eIlaebiKGhj4+nlYihoYiIp5WCoaGIiKeVgpubb1B8iHaPj3yIlZV8
+iJWCgo+Pb4KPdnaIiGl8iG9vgoJjdoJpaXx8Y2N2Y0oYAAAAAAAAAAAAAAAAAAAAEhIlJRgMDAAMDAwYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglJSUYGBgYGCUlJSUlJSUYGBgYJSUlJSUlJSUlJSUlJSUYGCUlJSUlGBglJSUlJSUlJSUY
+GCUlJSUlJSUlJSUlGCUlJSUYJSUlJSUYAAAAAAAAGBglSmNjdo+Pj6enj5unj4KVlXaIm4iIoaGPj66hlaenj4+uoYihoY+Pp5uIoaGPj6GViJuniIihlYKVoYiIoZV8j6GIiGlpdoiViHyVlXyIm4KCj492do92aYKCaXaCdmmCgml2gm9jdnZjY3xvY28+
+EgAAAAAAAAAAAAAAAAAAABIrGBgYBgYGBgYGEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHwwMDAwMAAAM
+AAAMDBglN1djY4+Pj6e6oaG0p4ibm4KCj4+ClaGPj6eniKGum4+hoYiVp5WIoaGIlaePj6Ghgo+hj4+bm4iIoY+ClYhKaZWIfI+PfIiVgnaIiHZ2j49viIhvb4h8aXx8b2+Cdml2dmNjfG9jb29XJQwAAAAAAAAAAAAAAAAAAAAYGCUSEhIGBgYGBhISEhIS
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8MDAwMDAwMDAwMAAAAAAAADBgYJUpKV3x8iJWnp5Wnp4iIoYh8lZV8
+iKGPj6GhiJWnlYihoYiIp5WIoaGIiKGPgpWhiIihj4KVoYhpaYKCj6GCgpubfI+PfHyPj2+IiHZ2iIhvfIh2doKCaXaCb298fGNjdmlpUBgGBgAAAAAAAAAAAAAAAAAAGBgAABIAAAAAABISEhISEhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fDAwMDAAMDAwADAwMAAwMDAwMAAAAAAwMHx8xPldpaXaPj4+hoY+PoZV8j5uCgqGVgpWhj4+np4iVoY+PoaGCj6GIiJubgo+h
+iIiIaWmCm4h8j498iIiIfI+PdoKPgnaIiG98iHZpfHxpdoJ2aYKCaXZ2dmN2djESAAAAAAAAAAAAAAAAAAAAAAwMABglDAAADAwMGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBglJSUlGBgYGBgYGBgYGCUlJRgYGBgYGBglJRgYJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwAAAAADAwMDAwMDAwMDAAMDAwAAAAAAAAAABIfHz5XY2OCgoKVoY+PoZV8iJuIfJWVgo+hj4+bm4KPoY+Cm5uIiKGPgpWVfG+VlYKVlYKClYh8iJV8fI+Pb4iIfHyPgml8iG9v
+fHxjdoJvb3x8aWl8b1AlBgYGAAAAAAAAAAAAAAAAAAAGBh9XPgwADAwMDBgYGBgYGBglJSUlJRgYGCUlJSUlJSUlGBgYGBgYJSUlJSUYGBgYGBgYGBglJSUlJSUlJSUlJRgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRgMDAwMAAAMDAwMDAwADAwMDAwMDAwMDAwMDAwMDAwMDAwAAAwYGCU3SldjfHx8j5uIiKGPfIiVgoKPj3yPoYiIoaGCj6GIiJWVfI+biIibm4KPj498lZV8fJWCdo+PdoKVgnaIiG98iHxpgoJpdoJ2aXZ2aWkxEgYGBgYAAAAAAAAAAAAAAAAA
+DDFpiD4SEgYGBhISHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYG
+BgYGBgYGBgYGBgYGBgYGBgYGEhIlRERXaXZ2iJuIiJWVdoibiHyPj4KPoY+Cm5uIiKGIfJWVgoKbj3yPj3x8j4J2j4+Cgo+PdoiIfHyIiG98fGlpfHxpfHxpaXxXHwwAAAAAAAAAAAAAAAAAAAAAADdpfHw+EgYGBgYSEhISEh8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fKysfHx8fHx8fHwwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAwMDAwMDAwMDAwYJTE+V29v
+b4+Pdo+PgoKViHyPoYiIoaGCj6GIiJuPgo+bgoKVlXaIiHx8j492go98fIiIb2+Idml8fGN2gm9vfHxjRB8MDAAAAAAAAAAAAAAAAAAAAABjfHxvMQwMDAwMGBgYGBgYGBgYGBgYGBgYGBgYGBgYJSUlJSUYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJRgMDAwMDAwAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAADAwMJT4+V29vb4KCdoKbiIibm4KPoY98lZV8iJWCgo+PfHyV
+iHaIiHx8j4JviIh2doiIb4iIb2+CgmN2dmlpaSUSBgYGBgYAAAAAAAAAAAAGBgAAY29jYzEMAAwMDBgYGBgYGBgYJSUlJSUYGCUlJSUlJSUlJRgYJSUlJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwMAAwYJTE+SmlpaXyIdoKVlYKVoYiIm498j498fJWIdoiVgoKPgm+IiHx8j49vfIh2doKCaXx8b298fEofDAwM
+AAAAAAAAAAAAAAAAAAAAAFdXV1clEgYGBgYYGBglJSUYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMJTExRFdjV2mCdnaIiHaIlYJ2j49vfI98fI+PdoKPgm+IiG98j3xviIhpdoh2aXx8aWk3GAwMDAAAAAAAAAAAAAAAAAAAAABKY2NXJRIAABISEh8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHxISEhISBgYGBgYGBgYGBgYGBgYGBgYSEhISEhISEhISEgYGBgYGEhIS
+EhISEhISEgYGBgYGBhISEhISEhISEhISBgYGBgYGGCUxMUpdXV12dnaCgnZ2iIhvgoJvb4h8b4iIfHyPj2+CgnZ2goJpfHxvb4JXJRgMDAwAAAAAAAAAAAAAAAAAAAAASldXVyUMAAASEhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHysrHx8fDAwADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwA
+AAAMDB8rK0pdXV1paV12dnZ2dnZjb3xvb4iIdnaPfHyIiG9vgnZpfHxpPhgGEhISBgYGBgAAAAAAAAYGBgYAAERXV0olDAAAAAwYGBgYGBgYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBglJTdKV0pjY1djb29jb29jb4KCb4iI
+dnaCgmmCgml2aSUMDAwMAAAAAAAAAAAAAAAAAAAAAABEV1dXJQwMDAAMGBgYJSUlGBgYGCUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMTElJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJRgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAABglJTFKSj5XV1dXb29XaXxvb4iIb3yIb298fEoYDBgMDAwAAAAAAAAAAAAAAAAAAAAA
+RFBQUCUMDAwMDBgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlMTElJSUxMSUlJSUlJSUlJSUlMSUYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAwfMTE+SkpKY2NXY3x8b4KCb2+Cgl03DAwMDAwMAAAAAAAAAAAAAAAAAAAAAERXV1cYBgYGBgYYGBgYGBgYJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUlJSUlJSUlJSUlJSUxMTElJTElJSUlJTExMSUlJSUlJSUlJSUlJSUlJSUlJSUlEhISEhISEhISEhISEhIGBgYGBgYGBgYSEhISEhISEhISEhISEhISEhISEhISEgYGEhIGEhIGBhIS
+EgYGBgYSEhISEgYGEhIGBhISEhISBgYGEhISEhISEhISBgYGBgYYGCU3NzdEXV1daYJvb4KCaXZjJQYGBhISAAAAAAAAAAAAAAwMDAAAAABEUFBQJQwADAwMHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHysrKysrKysrKysrKysr
+KysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrHxISEhISEhISEhISEhISEhIGBhISEhISEhISEhISEhISEhISEhISEhISEhISEhISBgYSEhISEhISEhIGBgYGBhISEhISBhISEhISEhISEhISEhISEhISEhIS
+EhISEhISBgYGBhIlMTE+UFBQb3xvb4JvShgAAAwMDAwAAAAAAAAAAAAAAAwMDAAARFdXVyUMAAAAEh8fHx8fHx8fHx8fHx8fHx8rKysrKysrKysrHysrKysrKx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHzElJSUlJSUlJSUlJSUlJSUlJSUlJTEl
+JSUlJSUlJSUlJSUlMSUxJSUlJSUlJSUSEhISEhISEhISEgYSEgYGEhISEhISEhISEhISEhISEgYGEhISEhISBgYGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEgYSEhISEhISBhISEhISEhISEhISEhISEhIGBgYYJSU3SldXdmNjYz4SAAASEhIG
+BgYGBgYAAAAGBgYGBgYAAERXV1crDAAMDAwYGBgYGCUlJSUlJSUlJSUlJRglJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxJTExMSUlJSUlMTElMSUlJTExJTElJSUlMSUxMSUxMTElJSUlJTExJSUlJSUlMTElMSUlJSUlJSUlEhISEhISEhIS
+EhISBgYGBgYGBhISEhISEhISBhISEhISEhISEhISEhISEhISEhISEhISEhISBgYGBgYSEhISEhISEhISEhISEhISEhISEgYGEhISEhISEhISAAwMDAwMDAwMDAwMDAwYGBg3Nzc3Sj4fBgYGBgYGBgYGBgYAAAAAAAAMDAwMAABEV1dXMRISAAwYJSUlJSUl
+JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlMSUxMTElJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUlJSUlJSUlJTExMTExMSUlJSUlJSUlJSUlJRISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISBgYGBgYGEhISEhIfHx8fDAAAAAwMDAAAAAAAAAAAAAAMDAwMDAAASldXVz4YDAwMGCUlJSUlJSUlJSUlJSUlJSUlJSUxJSUlJSUlJSUlJSUlJSUlJSUl
+JSUlJSUlJTElMSUlJSUlMTExJTExJSUxJSUlMSUlMTElJTElMSUxMTExMTExJSUlMTExJSUlJSUlJTExMTExJSUxJTExMR8SEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhIGEhISEgYSBgYGEhISEhISEhISEhISEhISEhISEhISEhISEgYGAAAMDAwMAAAAAAAAAAAADAwMDAwAAERXV1c+GAwMDBgYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUlJSUlJSUlJSUlJSUxJTElJSUl
+MSUlJSUlJTExJSUlJTExJSUxJSUxMTExMTElJSUlJTExJSUlJSUlEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEgYSEhISEhISEhISEhISEhISEhIS
+EhISBgYGEhISAAAADAwMDAwMDAAAAAAAAAwMDAwMAAA+V1dXRBgGBhIfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysr
+KysrKysrHx8MDAwMDAwMDAwMDAwMDAwMDAwMDBgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwYGAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgGBgYSEhISBgYABgYGBgYGBgYGBgYGBgYGBgYGBgAA
+PlBQXUoYDAwMGBglJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJTElJSUxJSUlJSUxMTElMTElMTElMSUlJSUxJTExMTExMSUlJTElMTExMSUxJTExJTExJSUlJSUlJSUlMTExMR8SEhISEhISEhISEhISEhISEhISEhISEhIS
+EhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIGEhIGEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhIGEgYGBhISBhgMDAwYGAwAAAAMDAwMAAAAAAAAAAAAAAAMDAwAAEpXV2NEGAwMDB8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f
+Hx8fHx8fHx8xJSUlJSUlMTExJTElMSUlJTElJTExJSUlJSUlJSUlJSUlJSUlJSUxJTExJSUxJSUlJTElMTExMTElMSUxJSUlMSUxMSUlJTExMSUlMSUYGBgMDAwMDAwMDAwMDAwYGAwMDAwMDAwMDAwMDBgYGAwMDAwMGAwMGAwMDAwMDAwMDAwMDAwMDAwM
+GAwMDAwMDAwMDAwMDAwMDAwMDAwMGBgYDAwMDBgYDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgGBgYGEhIGBgYGBgYGBgYGBgYGBgYGAABKV2NjSh8SEhIlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUxMSUxJSUxJSUlJSUlJSUlJSUl
+MTExMSUlJSUxMTElMTElJSUxJSUxMTExMSUlMSUlMSUlMTElJTExMTExJSUxMTExMTExJSUxGAwMDAwMDBgYGBgYGBgYGBgMDAwYGBgMGBgYDBgYGBgYGAwMDAwMDAwMDAwMDAwMDAwMDAwYGAwMDAwMDAwMGAwMDAwYGBgMDBgYDAwYDAwMDAwMDAwMDAwM
+DAwMDAwMDAwMDAwMDAwMDAwMDAwMDBgYBgYABgYSEgYGBgYGBgYGBgYGBgYGBgAASmNjY1AfDAwYJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJTElJSUlJSUlMSUxMSUxMTElJTExJTElJSUxMSUlJSUlJSUlMSUlMTExJTElJSUxJSUlJSUx
+MTExMTElJTEYAAAAAAAAACsrKysrKx8SEhISEhISEhISEhISEhISEgYGBgYGBhISEhISEhISEhISEhISEhISEhIGBhISEhISEhISEhISEhISEhISEhIGBhISBgYGBgYGBgYGBgYGEhISEhISEhISEhISBgYSEgYSEhISEgYGBgYGBgYGBgYSEgAAAAAMDAwA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHx9PT39/s7Pj4////////
+////////////////AAAAAAAADAwYGCUlMTE+PkpKV1djY29vfHyIiJWVoaGurrq6x8fT09/f7Oz4+P///////////////////////wAAAAAAAAwMGBglJTExPj5KSldXY2Nvb3x8iIiVlaGhrq66usfH09Pf3+zs+Pj/////AA==
+--mail.sleepy.sau.144.8891
+
+
+The digitizer does 64 gray levels at any of:
+ 256 x 244 (included)
+ 128 x 122
+ 66 x 61
+
+At 19.2k baud (it uses an rs232 interface) it takes up to 32 seconds to
+fetch the image (with no compression). The lower resolutions take
+8 and 2 seconds respectively.
+--mail.sleepy.sau.144.8891--
+
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: More Imagery
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA29595; Thu, 3 Oct 91 13:05:05 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19384; Thu, 3 Oct 91 13:04:49 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA12304> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:44 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA08989> for MRC@CAC.Washington.EDU; Thu, 3 Oct 91 16:04:43 EDT
+Received: from Messages.7.14.N.CUILIB.3.45.SNAP.NOT.LINKED.greenbush.galaxy.sun4.40
+ via MS.5.6.greenbush.galaxy.sun4_40;
+ Thu, 3 Oct 1991 16:04:42 -0400 (EDT)
+Resent-Message-Id: <EcurTOC0M2YtQTa2QW@thumper.bellcore.com>
+Resent-Date: Thu, 3 Oct 1991 16:04:42 -0400 (EDT)
+Resent-From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+If-Type-Unsupported: send
+Resent-To: Mark Crispin <MRC@CAC.Washington.EDU>
+Return-Path: <sau@sleepy.bellcore.com>
+Date: Fri, 7 Jun 91 09:09:05 EDT
+From: sau@sleepy.bellcore.com (Stephen A Uhler)
+Message-Id: <9106071309.AA00574@sleepy.bellcore.com>
+To: nsb@sleepy.bellcore.com
+Subject: meta-mail
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary="mail.sleepy.sau.158.532"
+
+--mail.sleepy.sau.158.532
+
+2 Questions:
+
+1) MM_QUIET doesn't seem to work in the 212 version of metamail.
+
+2) I've been pondering how I am going to send you this 40mb mail message.
+ I was going to split it up using the "parts" mechanism, but then it occurred to me.
+ My software has no business worrying about message size. Its up to the mail
+ delivery agent to do that form me. I should be able to compose a very
+ long message, and pass it off to sendmail (well, a pre-processor to sendmail).
+ if the message is too long, based upon the src/dst etc, then sendmail should
+ break it into multiple parts for me, and send it as multiple messages. As the
+ user (sender) I should be unaware this is happening.
+
+ Similarly, on the receiving end, the separate parts should be assembled as they
+ arrive; I shouldn't have do deal with them at all as multiple messages.
+
+ I guess what I am proposing are two utility programs:
+
+ msplit <max_lines> <max_bytes>
+
+ that takes 1 mail message and splits it into N messages and
+
+ mcombine <parts...>
+ mcombine <partially_assembled_thingy> <parts...>
+
+ that any mail system can call to achieve a uniform mechanism of splitting
+ and recombining messages, Kind of like:
+--mail.sleepy.sau.158.532
+Content-type: image/pbm
+Content-transfer-encoding: base64
+Content-Description: Mail architecture slide
+Comments: Image wrapped by /usr/sau/bin/fetch_image
+
+UDQKNzk4IDUzMgoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAANwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAB////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAP///////////AAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////wAAAD////gAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///gAAAAAAH///gAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//wAAAAAAAAA///AAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAAAAAAAAAAf//AAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//wAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAA
+AAAAf/4AAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AP/gAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAfAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABwAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAeAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAHgC4AAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAABcAuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAA/4
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAXATgHgdB0DwDwB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAF4E4GMIwjBGB
+HxjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAABOCODBmEYQwwwwwYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAA
+AAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAATwjggZgGAMMMMIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAEcQ4f+eB4ADDDH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAABHkOGAD4PgHwwxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAQ6DhgAPA8HMGIYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAEPg4YAA4DjDA8GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAABBwOHAUGQZgwQBwEAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAQcDg4JBkGYMMAOCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAECA4P8YRhHPz/j/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAHwg/g8F4Xg+Yf8PAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAA/wAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAABgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAQEAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAfwAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADADAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAwBwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAHQRwME/v4HgJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAACM9/DPDAwGMe8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAABhDgwwwwMDBhyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAYAwOMMMDAgYYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAHgMBjDDAwf+GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAA+DAYwwwMGABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAADwwGMMMDBgAYAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAOMBjDDAwYAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAPwAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAABBjAwwwwMHARgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAQYwMMMMDA4IY
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAGEOGDDDIyP8GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAABeDeD/8cHA8H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAD8AAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAIkAAAAAAAAA////////////4AAAAAD////////////gAAAAAB////////////wAAAAAAAAAAAAAAB+
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAjAAAAAAAAAf////////////AAAAAB////////////8AAAAAA////////////+AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAEAA
+AAAAAAAH////////////wAAAAAf////////////AAAAAAP////////////gAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAEUAAAAAAAAB////////////8AAAAAH////////////wAAAAAD////////////4
+AAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAPwAAAAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAA
+AADwAAAAAAAAAAB4AAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAB8AAAAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAH
+gAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAAA+AAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAIkAAAAAAAAB4AA
+AAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAA
+AAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAA
+AAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAA
+AAAAeAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAfgAAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAA
+A8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAQAAAAAAAAAeAAAAAAAAAAAPA
+AAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAA+AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAjAAAAAAA
+AAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAA
+AAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AHwAAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAA
+AAAAAAAAAHgAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAB8AAAABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAA
+AAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAARQAAAAAAAAHgAAAAAAA
+AAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAAAfAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAEAA
+AAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4
+AAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAA8AAAAIwAAAAAAAAHgAAAAAAAAAADwADwAAeAAAAAAAAAAAPAAB4AAPAAAAAAAAAAAHgAAAAAAeAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAABAAAAAAAAAB4AAAAAAAAAAA8AB/AAHgAAAAAAAAAADwAA/
+gADwAAAAAAAAAAB4AAAAAAP4AAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAABFAAAAAAAAAeAAAAAAAAAAAPAAf+AB4AAAAAAAAAAA8AAP/AA8AAAAAAAAAAAeAAAAAAD/wAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAfAAAAiQAAAAAAAAHgAAAAAAAAAADwAH/4AeAAAAAAAAAAAPAAD/8APAAAAAAAAAAAHgAAAAAA//AAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAACMAAAAAAAAB4AAAAAAAAAAA8AA//wH
+gAAAAAAAAAADwAAf/4DwAAAAAAAAAAB4AAAAAAH/+AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAQAAAAAAAAAeAAAAAAAAAAAPAAD//B4AAAAAAAAAAA8AAB//g8AAAAAAAAAAAeAAAAAAAf/4B4AAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAARQAAAAAAAAHgAAAAAAAAAADwAAP/+eAAAAAAAAAAAPAAAH//PAAAAAAAAAAAHgAAAAAAB//w+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAIkAAAAAAAAB4AA
+AAAAAAAAA8AAAf//gAAAAAAAAAADwAAAP//wAAAAAAAAAAB4AAAAAAAD//PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAjAAAAAAAAAeAAAAAAAAAAAP//////4AAAAAAAAAAA///////8AAAAAAAAAAAf///////////
+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAEAAAAAAAAAHgAAAAAAAAAAD//////+AAAAAAAAAAAP///////AAAAAAAAAAAH///////////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeA
+AAEUAAAAAAAAB4AAAAAAAAAAA///////gAAAAAAAAAAD///////wAAAAAAAAAAB///////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAACJAAAAAAAAAeAAAAAAAAAAAP//////4AAAAAAAAAAA///////8AAAAAAA
+AAAAf///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAIwAAAAAAAAHgAAAAAAAAAADwAAA//+AAAAAAAAAAAPAAAAf//AAAAAAAAAAAHgAAAAAAAH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAPAAABAAAAAAAAAB4AAAAAAAAAAA8AAB///gAAAAAAAAAADwAAA///wAAAAAAAAAAB4AAAAAAAP//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AABFAAAAAAAAAeAAAAAAAAAAAPAAD//x4AAAAAAAAAA
+A8AAB//48AAAAAAAAAAAeAAAAAAAf/+eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAiQAAAAAAAAHgAAAAAAAAAADwAD//geAAAAAAAAAAAPAAB//wPAAAAAAAAAAAHgAAAAAAf/8HgAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAACMAAAAAAAAB4AAAAAAAAAAA8AB//gHgAAAAAAAAAADwAA//wDwAAAAAAAAAAB4AAAAAAP/8D4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAQAAAAAAAAAeAAAAAAAAAAAPA
+Af/AB4AAAAAAAAAAA8AAP/gA8AAAAAAAAAAAeAAAAAAD/4A8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAARQAAAAAAAAHgAAAAAAAAAADwAH+AAeAAAAAAAAAAAPAAD/AAPAAAAAAAAAAAHgAAAAAA/wAPAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAIkAAAAAAAAB4AAAAAAAAAAA8AA8AAHgAAAAAAAAAADwAAeAADwAAAAAAAAAAB4AAAAAAHgAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAjAAAAAAA
+AAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAA
+AAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAPAACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAA
+AAAAAAAAAHgAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAHgABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAA
+AAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AARQAAAAAAAAHgAAAAAAA
+AAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAIkAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAB8AAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAEAA
+AAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4
+AAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8ACJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAPAAIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAA
+AADwAAAAAAAAAAB4AAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+ABFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAHgAiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4ACMAAAAAAAAB4AAAAAAAAAAA8AAAAAH
+gAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAQAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAD4AAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwARQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AIkAAAAAAAAB4AA
+AAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAjAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAADwA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAEAAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
++AEUAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgCJAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAA
+AAAAeAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIwAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAeABAAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgBFAAAAAAAAAeAAAAAAAAAAAPAAAAAB4AAAAAAAAAA
+A8AAAAAA8AAAAAAAAAAAeAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AiQAAAAAAAAHgAAAAAAAAAADwAAAAAeAAAAAAAAAAAPAAAAAAPAAAAAAAAAAAHgAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPACMAAAAAAAAB4AAAAAAAAAAA8AAAAAHgAAAAAAAAAADwAAAAADwAAAAAAAAAAB4AAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAQAAAAAAAAAeAAAAAAAAAAAPA
+AAAAB4AAAAAAAAAAA8AAAAAA8AAAAAAAAAAAeAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ARQAAAAAAAAH////////////wAAAAAf////////////AAAAAAP////////////gAAAAAAAHwAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgIkAAAAAAAAB////////////8AAAAAH////////////wAAAAAD////////////4AAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AjAAAAAAA
+AAf////////////AAAAAB////////////8AAAAAA////////////+AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAEAAAAAAAAAD////////////gAAAAAP///////////+AAAAAAH////////////AAAAAA
+AAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAB4CJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAA8BFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwCMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4IkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAwQAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEAA
+AAA/gfAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPkAAAAADwAAAAAM8AAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EUAAAADgBAAAAAAAACAAAAAAAAQAAAAAAAAAAAAAAAAAAAAOHAAAAAAMAAAAAA
+DAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeCJAAAAA4AQAAAAAAABwAAAAAAAEAAAAAAAAAAAAAAAAAAAADAwAAAAADAAAAAAAwAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAHgIwAAAAOAEAAAAAAAAcAAAAAAADAAAAAAAAAAAAAAAAAAAABwEAAAAAAwAAAAAAMAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BAAAAADgBAAAAAAAALgAAAAAABwAAAAAAAAAAAAAAAAAAA
+AcBAAAAAAMAAAAAADAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeBFAAAAA4AQOgPATAAC4APAHgTw/gAAAAAAAAAAAAAAAAAAAHgAHgTwB7BOHAPAQwAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAHwiQAAAAOAEEYMY94ABHAEfGM9+DAAAAAAAAAAAAAAAAAAAAA+AGM9+Bhz334EY8MAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8CMAAAADgBDCGDDkAARwDDDBjhwwAAA
+AAAAAAAAAAAAAAAAAH4DBjhwwMOPHDDDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAQAAAAA4AQwBAwwAAIOAwwgYwMMAAAAAAAAAAAAAAAAAAAAAfggYwMMDDBgwwwwwAAAAADwAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwRQAAAAOAEPA/8MAACDgMMf+MDDAAAAAAAAAAAAAAAAAAAAAB8f+MDGAwwYMAMMMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8IkAAAADgBB8MAD
+AAB/8DDGADAwwAAAAAAAAAAAAAAAAAAAAAHmADAxgMMGDAfDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAjAAAAA4AQHjAAwAAQHAYhgAwMMAAAAAAAAAAAAAAAAAAAAAA5gAwMYDDBgwcwwwAAAAADwAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwEAAAAAOAMAcwAMAAIA4DwYAMDDAAAAAAAAAAAAAAAAAAAABAOYAMDGAwwYMMMMMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+A8EUAAAABwCCDOAjAACAOBAHATAwwAAAAAAAAAAAAAAAAAAAAYDnATAxwMMGDGDDDAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPCJAAAAAcBggxwQwABABwwA4IwMMAAAAAAAAAAAAAAAAAAAAGAw4IwMMHD
+BgxgwwwAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwIwAAAADxwMIf4MAAQAcP+P8MDDIAAAAAAAAAAAAAAAAAAAA4cP8MDD+8wYMc/MMAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAA8BAAAAAAPwC8B4PwAfAfx/w8Pz4cAAAAAAAAAAAAAAAAAAAAL8A8Pz4PI/fvj5v/wAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPhFAAAAAAAAAAAAAAAAAAwMAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAYDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAGAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAABwwAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAHiJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4IwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAHhFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAB/gA/wAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAH4ADwAA
+AAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAA/AAYAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAH4AGAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAA
+AAAAAAAAAB/ABgAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAfwAYAAAAQAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAB4EAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAG+AGAAAAEAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAeEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAeAAAAAAAAAAAAAAAAABnwBgAAADAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAHiJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAY+AYAAABwAAAAAAAAAADgAAAAAAAAAAAA
+AAAAAAAAAAAB4IwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGHwGAD4A8AAAAAB+ABjg4AAAAAAAAAAAAAAAAAAAAAAAAeBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABh8BgDjg/9+P4fhx4H58OH8AAAAAAAAAAAAAAAAAAAAAAHhFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAYPgYBgcBwPB4Bw4HA
+OnDgcAAAAAAAAAAAAAAAAAAAAAAB4iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGB8GAYDAcBwOAYcB4Dxg4MAAAAAAAAAAAAAAAAAAAAAAAeCMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABgPhgMA4HAeDwEHAOA8AOGAAAAAAAAAAAAAAAAAAAAAAAHgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAY
+B8YDAOBwDgcDDgDwOADjAAAAAAAAAAAAAAAAAAAAAAAB4RQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAGAfGB//gcA4HAg4AcDgA5gAAAAAAAAAAAAAAAAAAAAAAAeIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAABgD5gcAAHAPD4YOAHA4AOwAAAAAAAAAAAAAAAAAAAAAAAHgjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAA
+AAAAAAAAAAAAAAAYAfYHAABwBwuEDgBwOAD8AAAAAAAAAAAAAAAAAAAAAAAD4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAD+BwAAcAcbjA4AcDgA/gAAAAAAAAAAAAAAAAAAAAAA
+A8EUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAABgAfgcAAHAHk8gOAHA4AO8AAAAAAAAAAAAAAAAAAAAAAAPCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAADwAAAAAAAAAAAAAAAAAYAH4HAABwA7HYDgBwOADngAAAAAAAAAAAAAAAAAAAAAADwIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAA+B4AgcAOh0A8AcDgA48AAAAAA
+AAAAAAAAAAAAAAAAA8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAABgAHgOAIHAD4fAHAOA4AOHgAAAAAAAAAAAAAAAAAAAAAAPBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAYAA4DwEBwgcDgB4DgOADg8AAAAAAAAAAAAAAAAAAAAAADwiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAGAAGAfHAeIHA
+4AOBwDgA4HgAAAAAAAAAAAAAAAAAAAAAA8CMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAADwABgD/gD8BgMAB44B8AfA8AAAAAAAAAAAAAAAAAAAAAAPAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAD/AAIAPgAeAIBAAH4B/wf8/wAAAAAAAAAAAAAAAAAAAAADwRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8IkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4EUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAeCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAeBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4CMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4CJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAeAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4BFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAeAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgCMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+eAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgCJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAA+ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAA8ACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAB4ABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAADwAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAPgAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAeAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAIkAAAAAAAAA//////////////////8AAAAAAAAAP//////////////////AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAjAAAAAAA
+AAf//////////////////gAAAAAAAAH//////////////////4AAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAEAAAAAAAAAH//////////////////4AAAAAAAAB//////////////////+AAAAAAAAA
+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAEUAAAAAAAAB//////////////////+AAAAAAAAAf//////////////////gAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AADwAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAHgAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAA
+AAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAH4AAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAA
+AAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAB8AAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAA
+AAAAAAAAAAAHgAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAD4AAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+AAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAB8AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAIkAAAAAAAAB4AA
+AAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAA
+AAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAA
+AAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAA
+B4AAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAB+AAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHg
+AAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAQAAAAAAAAAeAAAAAAAAAAAAA
+AAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAH4AAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAjAAAAAAA
+AAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAA
+AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/
+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAHgAAAAAeAAAAAAAAAAAAAAAAAHgAADwAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAD8AAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAH8AAAAAHgAAAAAAAAAAAAAAAAB4AAD+AAAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AP/AA
+AAAB4AAAAAAAAAAAAAAAAAeAAH/gAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAP/wAAAAAeAAAAAAAAAAAAAAAAAHgAH/4AAAAAAAAAAAAAAfAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgf/4AAAAAHgAAAAAAAAAAAAAAAAB4AP/8AAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4f/4AAAAAB4AAAAAAAAAAAAAAAAAeAP/8AAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAe//4AAAAAAeAAAAAAAAAAAAAAAAAHgf/8AAAAAAAAAAAAAAAH
+4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAH//wAAAAAAHgAAAAAAAAAAAAAAAAB4f/4AAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB//////////4AAAAAAAAAAAAAAAAAf////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAf/////////+AAAAAAAAAAAAAAAAAH////
+////////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAH//////////gAAAAAAAAAAAAAAAAB/////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAD8AAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB//////////4AAAAAAAAAAAAAAAAAf///////////////////9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAf/8AAAAAAAeAAAAAA
+AAAAAAAAAAAHn/+AAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAH//wAAAAAAHgAAAAAAAAAAAAAAAAB4f/4AAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAfgAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB7//gAAAAAB4AAAAAAAAAAAAAAAAAeB//wAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+H/+AAAAAAeAAAAAAAAAAAAAAAAAHgD//AAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgf/4AAAAAHgAAAAAAAAAAAAAAAAB4AP/8AAAAAAAAAAAAAAAAPwAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4A//AAAAAB4AAAAAAAAAAAAAAAAAeAAf/gAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAIkAAAAAAAAB4AA
+AAAAAAAAAAAAAAAeAD/wAAAAAeAAAAAAAAAAAAAAAAAHgAB/4AAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAH8AAAAAHgAAAAAAAAAAAAAAAAB4AAD+AAAAAAAAAA
+AAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAeAAAAAB4AAAAAAAAAAAAAAAAAeAAAPAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAA
+AAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAA
+B4AAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAB+AAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHg
+AAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAA
+AAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAARQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAB+A
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAjAAAAAAA
+AAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAEAAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAA
+AAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAA
+AAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAA
+AAAAAAeAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAD+AAAAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAA
+AAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAARQAAAAAAAAHgAAAAAAA
+AAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAIkAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAA
+AAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAjAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAEAA
+AAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAEUAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAA
+AAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAACJAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAA
+AAAAAAAAAAAAAIwAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAABAAAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAeAAAAAA
+AAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAABFAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAA
+AAAAAAAAAAD/gAAAAAAAAAAAAAAAAiQAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAB4AAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAACMAAAAAAAAB4AAAAAAAAAAAAAAAAAe
+AAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAQAAAAAAAAAeAAAAAAAAAAAAAAAAAHgAAAAAAAAHgAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAD/g
+AAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAARQAAAAAAAAH//////////////////4AAAAAAAAB//////////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAIkAAAAAAAAB///
+///////////////+AAAAAAAAAf//////////////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAjAAAAAAAAAf//////////////////gAAAAAAAAH//////////////////4AAAAAAAAAAAAAA
+AAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAEAAAAAAAAAD//////////////////wAAAAAAAAA//////////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAB/4A
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAA
+AAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAH/4AAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAAAAAAAAAAAAD//AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAB//wAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAAAAAAAB//8AAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//8AAAAAAAAAP//wAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///gAAAAAAH///gAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH////AAAAP///+AA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4APAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAPADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/////////+AAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwBcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAP///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuAXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAALgJwDwOg6B4B4A8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAD+B8AAAAAAAAgAAAAAAAAAAAAAAAAAAAC8CcDGEYRgjAj4xgAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAOAEAAAAAAAAIAAAAAAABAAAAAAAAAAAAnBHBgzCMIYYYYYMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAADgBAAAAAAAAHAAAAAAAAQAAAAAAAAAAAJ4RwQMwDAGGGGEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAA4AQAAAAAAABwAAAAAAAMAAAAAAAAAAACOIcP/PA8ABhhj/wAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAOAEAAAAAAAAuAAAAAAAHAAAAAAAAAAAAjyHDAB8HwD4YYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAADgBA6A8BMAALgA8AeBPD+AAAAAAAAAAAIdBwwAHgeDmDEMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAA4AQRgxj3gAEcAR8Yz34MAAAAAAAAAAACHwcM
+AAcBxhgeDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAOAEMIYMOQABHAMMMGOHDAAAAAAAAAAAAg4HDgKDIMwYIA4CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAADgBDAEDDAAAg4DDCBjAwwAAAAAAAAAAAIOBwcEgyDMGGAHBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAA4AQ8D/wwAAIOAwx/4wMM
+AAAAAAAAAAACBAcH+MIwjn5/x/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAOAEHwwAMAAH/wMMYAMDDAAAAAAAAAAAD4QfweC8LwfMP+HgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAADgBAeMADAABAcBiGADAwwAAAAAAAAAAAAAAAAAAAAAAGBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAA4AwB
+zAAwAAgDgPBgAwMMAAAAAAAAAAAAAAAAAAAAAADAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAHAIIM4CMAAIA4EAcBMDDAAAAAAAAAAAAAAAAAAAAAAAwEAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAABwGCDHBDAAEAHDADgjAwwAAAAAAAAAAAAAAAAAAAAAAOGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAPHAwh/gwABABw/4/wwMMgAAAAAAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAA/ALwHg/AB8B/H/Dw/PhwAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAADAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAABgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAYCAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAHDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAAAAAAAAAAAAAAAAAABDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAfwPgAAAAAAADwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAgAAAAAAAAMAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIAAAAAAAADABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACAAAAAAAAAwAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAgAAAAAAAAMAcHAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIE8AAB0EcDBP7+B4CYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACPfgAAjPfwzwwMBjHvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAg4cAAYQ4MM
+MMDAwYcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIMDAAGAMDjDDAwIGGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHACDAwAB4DAYwwwMH/hgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAABwAgwMf+PgwGMMMDBgAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAIMDH/g8MBjDDAwYAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAGDAwAADjAYwwwMGABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAA4BAwMAAQYwMMMMDBwEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAwMDAAEGMDDDDAwOCGAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB44DAwABhDhgwwyMj/BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4D8+AAXg3g//HBwPB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAACJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACM
+--mail.sleepy.sau.158.532--
+
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: PostScript demo
+
+Received: from hanna.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA19503; Mon, 7 Oct 91 09:15:36 -0700
+Received: from thumper.bellcore.com by hanna.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA28214; Mon, 7 Oct 91 09:14:12 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA13347> for mrc@cac.washington.edu; Mon, 7 Oct 91 12:13:58 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA10867> for mrc@cac.washington.edu; Mon, 7 Oct 91 12:13:55 EDT
+Date: Mon, 7 Oct 91 12:13:55 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110071613.AA10867@greenbush.bellcore.com>
+To: mrc@cac.washington.edu
+Subject: An image that went gif->ppm->ps
+MIME-Version: 1.0
+Content-Type: application/postscript
+Content-Description: Captain Picard
+
+%!PS-Adobe-2.0 EPSF-2.)
+%%Creator: ppmtops
+%%Title: noname.ps
+%%Pages: 1
+%%BoundingBox: 147 304 454 496
+%%EndComments
+%%EndProlog
+%%Page 1 1
+/picstr 384 string def
+gsave
+147 304 translate
+1 1 scale
+307 192 scale
+320 200 8
+[ 320 0 0 -200 0 200 ]
+{ currentfile picstr readhexstring pop }
+false 3
+colorimage
+9148489148486d4848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d4848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d48486d48486d48486d48486d24246d48486d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d2424916d6db69191da9191dab6b6dab6b6da9191b69191914848
+6d48484824246d24244824246d24246d24246d24246d24246d24246d2424
+4824246d24244824244824244824246d24246d2424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+6d24246d24246d24244824244824244824246d24246d2424482424482424
+6d24244824246d24244824246d24246d24246d2424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824246d2424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48489148486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d4848914848914848916d6d914848
+9148489148486d48486d48486d48486d24246d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d2424914848b66d6db69191dab6b6da9191b69191b66d6d914848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824244824246d24246d24246d2424482424
+4824244824244824244824244824244824246d24246d24246d2424482424
+4824244824246d24246d24244824246d2424482424482424482424482424
+4824244824246d24246d24244824246d24246d24244824246d2424482424
+6d24246d24244824246d24246d24244824246d24244824244824246d2424
+4824244824244824246d24246d24244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d24246d24246d4848
+6d24246d48486d48486d48486d24246d48486d24246d48486d24246d4848
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d48489148489148489148486d48486d2424
+6d48486d48486d24246d24246d24246d48486d48486d48486d24246d2424
+6d24244824246d2424914848916d6db66d6db66d6db66d6d9148486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d24244824246d2424482424
+4824246d24244824244824244824246d24246d24246d24246d2424482424
+4824246d24244824244824246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824244824244824244824246d2424
+6d24246d24244824246d24244824246d24246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+9148489148489148489148489148489148489148489148486d48486d4848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148486d48489148489148486d48489148489148489148489148486d4848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d24246d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d4848914848
+916d6d916d6d916d6d914848916d6db69191b69191b69191b66d6db66d6d
+b66d6d9148489148486d48486d48486d48486d48486d48486d24246d4848
+6d48486d24246d24246d24246d48489148489148486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d2424482424
+4824244824246d24244824244824244824246d2424482424482424482424
+4824246d24246d24244824244824244824246d2424482424482424482424
+6d24244824246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24244824246d24244824244824246d2424482424482424
+6d24246d24244824246d24244824244824246d24246d2424482424482424
+6d24244824244824244824244824246d24246d24244824246d2424482424
+6d24244824244824246d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d48486d24246d24246d48486d4848
+6d48486d24246d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d
+b66d6db66d6d914848916d6d916d6db66d6db69191b66d6db66d6db69191
+b69191b66d6d916d6d916d6d916d6d9148489148486d24246d48486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824244824244824244824246d24244824246d2424
+6d24244824246d24246d24244824246d24244824246d24246d24246d2424
+4824246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24246d24246d24244824246d2424
+4824244824244824244824244824244824244824246d24246d2424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148486d4848914848914848914848914848914848
+9148489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d24246d2424
+6d24246d24246d24246d24246d48486d24246d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d48486d48486d48486d48486d48486d2424
+914848914848914848914848916d6db66d6dda9191b69191b66d6db66d6d
+9148489148486d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848914848b66d6db69191b69191b66d6d916d6d914848914848
+6d48489148486d48489148486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824246d24244824246d24246d24246d24246d2424
+6d24244824246d24244824244824246d24244824244824244824246d2424
+6d24246d24246d24244824244824246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d2424482424482424
+4824244824246d24246d24244824246d24244824246d24246d2424482424
+6d24244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848914848914848914848914848914848914848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d48486d48486d48486d24246d48486d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d24246d48486d24246d48486d48486d24246d24246d24246d48486d4848
+916d6db66d6db69191b66d6d914848916d6d916d6d6d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6d916d6d6d48486d48486d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24244824246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d2424482424482424482424482424482424
+6d24244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824246d2424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148486d4848914848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d4848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d2424914848914848916d6d
+b66d6dda9191b66d6d916d6d6d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d4848914848914848916d6d916d6d916d6d916d6d
+b69191b69191da9191da9191da9191b69191b66d6d9148489148486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24244824244824246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d24246d24246d2424
+6d24244824246d24246d24244824244824246d24244824244824246d2424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d4848914848914848b66d6d916d6d916d6d
+6d48486d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d4848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6db69191b69191da9191b66d6d916d6db66d6d
+9148489148486d48486d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+4824246d24246d24244824244824244824246d24246d24246d48486d4848
+6d24246d24246d24244824244824244824244824244824246d24246d2424
+4824244824244824244824244824244824246d24244824244824246d2424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+b66d6d914848916d6d916d6d914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+9148489148486d4848914848916d6d916d6db66d6d916d6d9148486d4848
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db69191da9191
+dab6b6dab6b6b69191916d6d9148486d24246d48486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24244824246d24246d24244824244824246d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824244824246d24244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+b69191914848916d6d916d6d916d6d914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48489148489148489148489148489148486d4848914848914848914848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148489148489148486d4848
+6d4848914848916d6d916d6db66d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48489148486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b69191dab6b6da9191b69191b691919148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d2424482424482424482424
+4824244824246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+da9191916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d4848914848914848914848914848914848
+916d6db66d6db69191b66d6d9148486d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d48486d4848
+6d4848914848914848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db69191dab6b6da9191b69191b66d6d9148486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d2424482424482424
+6d24244824244824244824246d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824246d24244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+da9191916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848916d6d914848914848914848916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848916d6d916d6d916d6d914848
+914848916d6d914848914848916d6d916d6d916d6d914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848914848914848916d6db66d6d
+b69191da91919148486d48484824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d48489148489148489148486d4848914848
+914848914848916d6d914848916d6d916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6dda9191dab6b6b69191916d6d
+6d24246d48484824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824246d24244824246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824246d24244824244824244824244824244824246d24246d2424
+4824244824244824244824246d24244824246d2424482424482424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffb6b6916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848916d6d916d6d914848
+914848916d6d916d6d914848916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48486d48486d48486d48489148486d4848914848914848
+6d48486d48486d48489148486d48486d48486d48486d4848914848914848
+914848914848914848914848914848914848914848b66d6db69191b69191
+b691919148486d24244824244824244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d48486d4848
+6d24246d48486d48486d48489148486d4848914848914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+b66d6d916d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6d916d6db66d6db69191dab6b6dab6b6
+b69191916d6d916d6d6d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24244824246d24244824246d24246d24246d24246d24246d2424
+4824246d24246d24246d24244824246d2424482424482424482424482424
+6d24244824244824244824246d24246d2424482424482424482424482424
+4824244824246d24246d24246d48486d48486d48486d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d2424482424482424482424482424
+6d24244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffdadab66d6db66d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848916d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48489148489148486d4848914848
+9148486d48486d48489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+9148486d48489148486d4848914848914848916d6db69191da9191b66d6d
+6d48486d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d4848914848914848914848914848916d6d916d6d916d6d
+914848914848916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6db69191
+dab6b6dab6b6b69191b66d6d6d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48489148486d4848
+6d48486d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24244824244824246d24244824244824246d24246d2424482424482424
+6d24244824244824244824244824244824244824246d24246d2424482424
+4824244824244824244824246d24244824244824244824246d24246d2424
+4824246d24246d4848914848916d6d916d6d9148486d48486d24246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdab66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48489148486d48486d4848914848
+9148489148486d48489148489148486d48486d48486d4848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848916d6db66d6db691919148486d4848
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6d916d6d916d6db66d6db66d6d
+da9191dab6b6ffb6b6dab6b6b66d6d916d6d6d48486d48486d24246d2424
+6d24246d24246d24246d24244824246d24246d4848916d6d916d6d916d6d
+9148486d24244824246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24244824244824246d2424482424
+4824246d24244824246d24246d24244824244824244824246d24246d2424
+6d24244824246d24244824246d24244824246d24246d2424482424482424
+4824246d2424914848914848914848916d6d916d6d9148486d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d24246d2424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+ffffdab66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48489148489148486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d4848914848914848b66d6db66d6d916d6d6d48486d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d24246d24246d24246d48486d48486d4848
+6d48486d4848914848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191dab6b6ffb6b6916d6d916d6d6d48486d2424
+6d24244824244824244824246d24246d2424914848914848914848914848
+9148486d48486d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d2424482424482424482424
+6d24246d24246d24246d24246d24246d24246d24244824246d2424482424
+4824244824246d24246d24246d2424482424482424482424482424482424
+6d24244824246d24246d24244824246d2424482424482424482424482424
+4824246d24246d48489148489148489148489148486d48486d2424482424
+4824244824244824244824246d24244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824246d2424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdab66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848b66d6db69191b69191916d6d6d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6db66d6d916d6d
+916d6d916d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6ddab6b6ffb6b6da9191da9191916d6d914848
+6d24246d24244824246d24244824246d24246d4848914848914848914848
+9148486d48484824244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24244824246d24246d24246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824246d24246d48489148489148489148489148486d48486d2424482424
+4824244824244824244824246d24244824246d2424482424482424482424
+4824244824246d24244824244824244824246d24244824244824246d2424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+ffffdab69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6d916d6d
+b66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848916d6d914848914848914848914848914848914848
+6d4848916d6db69191b69191916d6d6d48486d24246d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d24246d24246d48486d48486d2424
+6d4848914848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6d916d6d916d6d916d6dda9191dab6b6dab6b6dab6b6916d6d
+6d48486d24244824246d24244824246d24246d24249148489148486d4848
+6d48486d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d24246d2424482424482424
+4824244824246d24244824244824244824244824246d24244824246d2424
+4824246d24246d24246d24246d24244824246d24244824246d2424482424
+6d24246d24246d24246d24246d24246d48486d24246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824244824244824246d24246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d2424482424482424482424482424
+ffffdada9191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d916d6d916d6d914848914848914848916d6d916d6db66d6d
+916d6db66d6db69191916d6d6d48486d24246d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b66d6db66d6d916d6d916d6d916d6db69191ffb6b6ffdadadab6b6
+b66d6d9148486d48489148489148486d48486d24246d4848916d6d6d2424
+4824246d24244824244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824246d24246d24246d24246d24246d2424482424482424
+6d24244824246d24244824244824244824244824244824244824246d2424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24244824244824244824244824246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d2424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdada9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848914848914848914848914848916d6d916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6db66d6d916d6d914848914848914848b69191b69191da9191
+b69191b69191916d6d6d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+916d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6db66d6d916d6d916d6d914848b69191dab6b6ffb6b6
+ffdadada9191b66d6db66d6db69191b66d6d916d6d916d6d6d4848482424
+4824244800006d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824244824246d24246d24246d24246d24246d2424
+4824244824244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24244824244824246d24246d2424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824244824246d2424482424
+4824244824244824244824246d24244824246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+ffffdada9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d916d6d916d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848916d6d914848914848914848914848914848914848914848
+916d6db69191b66d6db66d6d916d6d916d6db69191dab6b6dab6b6ffb6b6
+ffb6b69148489148486d24249148486d24246d48486d48486d48486d4848
+6d48486d48486d48486d24246d24246d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6db69191b69191
+ffb6b6ffdadada9191b66d6db69191da9191da9191b66d6d482424482424
+4824246d24244824246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24244824244824246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24244824246d2424482424482424
+4824246d24244824244824246d24246d24246d24246d24246d2424482424
+4824244824244824246d24244824246d24246d24246d2424482424482424
+4824244824246d24244824246d24246d24246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824244824244824246d2424
+ffffdadab6b6dab6b6dab6b6dab6b6da9191b69191b69191b69191da9191
+da9191b69191b69191da9191b69191da9191da9191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191
+b69191b66d6db66d6db66d6db69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6db66d6d916d6d
+914848916d6d914848916d6d914848914848914848916d6d914848914848
+914848914848916d6db66d6d916d6d916d6db69191ffb6b6dab6b6dab6b6
+b66d6d6d48484824246d48486d24249148486d24246d48486d48486d4848
+6d24246d48486d24246d48486d24246d24246d48486d24246d48486d4848
+6d48486d48486d48486d24246d24246d24246d48486d48486d4848914848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6d916d6db66d6d916d6d914848914848
+da9191ffb6b6ffb6b6dab6b6dab6b6dab6b6b66d6d916d6d482424482424
+4824244824244824246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48489148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d2424482424
+4824246d24244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24249148489148489148484824244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdadab6b6dab6b6dab6b6dab6b6da9191da9191b69191b69191da9191
+da9191da9191b69191b69191b69191da9191da9191da9191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d916d6d914848914848916d6db66d6db66d6d916d6d
+916d6d914848914848914848914848916d6d916d6d916d6d916d6d916d6d
+914848914848916d6d914848916d6db69191da9191dab6b6dab6b6b69191
+6d48486d24246d48486d48486d48486d24246d48486d48486d48486d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d4848914848
+914848914848914848916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6ddab6b6ffb6b6ffdadaffb6b6ffb6b6da9191916d6d482424
+6d24244824246d24244824246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824244824244824246d2424482424
+6d24246d4848914848916d6d916d6d916d6d6d48486d48484824246d2424
+4824246d24244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191dab6b6dab6b6b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191b66d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d914848916d6db66d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6db69191dab6b6ffb6b6b691916d4848
+6d24246d24249148486d24246d48486d48486d48486d48486d24246d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d48486d4848
+6d24246d24246d24246d24246d48486d48486d48486d48486d4848914848
+914848916d6d914848914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+914848914848916d6db69191ffb6b6ffdadaffdadaffdadadab6b6914848
+4824244800004824244824244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824244824246d24246d2424482424482424482424482424
+4824246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d2424482424482424
+6d24246d48489148489148489148489148489148486d48486d2424482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+ffffdaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191b69191b69191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d914848914848914848
+916d6d914848916d6db66d6db69191da9191ffb6b6da91916d4848914848
+4824249148486d24246d48486d24246d48486d48486d48486d24246d2424
+6d24246d48486d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d24246d48486d48486d4848914848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+916d6d914848914848916d6ddab6b6dab6b6ffdadaffdadaffb6b6b69191
+6d48486d24244824246d24244824246d24244824246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824246d24246d24246d4848916d6d916d6d916d6d9148486d2424
+6d24244824246d24244824246d24244824244824244824246d2424482424
+6d24246d24246d48489148489148489148489148486d4848482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdaffdadaffdadaffb6b6ffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191dab6b6da9191da9191b69191b69191b69191b69191da9191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848916d6d914848914848914848914848b66d6d
+b69191b66d6db69191b69191dab6b6dab6b6dab6b6916d6d4824246d2424
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+da9191da9191b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6d916d6d914848914848b66d6db69191dab6b6ffb6b6ffdadadab6b6
+b69191b66d6d9148486d24244824244824244824244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824246d24244824246d24244824246d2424482424482424
+6d24244824244824246d24244824246d2424482424482424482424482424
+4824244824246d2424914848b66d6dda9191b69191b69191b66d6d914848
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d24246d48489148489148489148489148486d2424482424482424
+4824244824244824244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+ffffdaffffdaffdadaffdadaffdadaffffdaffdadaffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191
+dab6b6dab6b6da9191da9191b69191b66d6db69191b69191b69191b69191
+da9191b69191da9191b69191da9191da9191da9191da9191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6db69191b66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d914848914848914848914848914848916d6d
+b66d6db69191dab6b6b69191ffb6b6da9191916d6d6d48486d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d48486d48486d48486d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b66d6db69191b66d6db69191b66d6db69191b69191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d914848914848b69191da9191ffb6b6ffb6b6
+dab6b6dab6b6da9191916d6d6d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24244824246d24244824246d2424
+6d24246d24246d24246d24244824246d24244824244824246d2424482424
+4824244824246d4848916d6db69191da9191dab6b6da9191b69191914848
+6d24244824244824246d24244824244824246d24244824244824246d2424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824246d24246d24244824244824244824244824244824246d2424
+ffdadaffffdaffffdaffdadaffffdaffffdaffdadaffb6b6ffb6b6ffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191b69191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+914848916d6d916d6d916d6d916d6d914848914848914848914848914848
+916d6db66d6dda9191dab6b6da9191b66d6d6d24246d24246d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d48486d24246d4848
+6d24246d24246d48486d48486d4848914848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6dda9191da9191
+da9191b69191b66d6d9148486d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24244824246d24246d2424482424
+4824244824246d4848b66d6dda9191da9191da9191da9191b66d6d914848
+6d24244824244824244824244824246d2424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d24244824244824244824246d2424482424482424482424482424
+6d24244824244824244824246d48486d48486d48486d2424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191da9191da9191da9191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d916d6db66d6d916d6d916d6d
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d914848914848b66d6d
+b69191da9191dab6b6dab6b6b66d6d9148486d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d24246d4848
+6d24246d48486d48486d4848914848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848b66d6ddab6b6
+ffb6b6da9191b691919148489148484824249148484824246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824244824244824246d2424482424
+4824244824246d2424914848b66d6db69191b69191b69191b66d6d6d4848
+6d24244824244824244824246d24244824246d2424482424482424482424
+6d24244824244824246d24246d24246d24246d24244824244824246d2424
+6d24244824246d24244824244824244824246d2424482424482424482424
+4824246d24246d24246d4848916d6d916d6d916d6d9148486d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffdadaffdadaffdadaffdadaffdadadab6b6ffb6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191b69191da9191da9191b69191b69191da9191
+b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848b66d6d
+dab6b6dab6b6ffdadab691919148484824249148486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d4848914848914848914848914848916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b66d6db69191b66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d914848914848914848914848
+ffb6b6ffb6b6ffdadada9191916d6d6d24246d24244824246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24244824246d24246d24246d2424482424482424482424482424482424
+4824244824244824246d24246d4848916d6d916d6d9148486d48486d2424
+6d24246d24244824246d24244824246d24244824246d2424482424482424
+4824246d24246d24244824246d24246d2424482424482424482424482424
+6d24244824246d24244824244824246d24246d24244824246d2424482424
+6d24246d48486d4848914848914848914848916d6d9148486d24246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191dab6b6da9191
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6db69191da9191
+ffb6b6ffb6b6dab6b69148486d48486d48486d24249148486d2424914848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d914848914848
+b69191da9191ffdadaffb6b6b691916d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824244824244824244824246d24246d24244824246d2424482424
+4824244824244824246d24246d24244824246d24246d24246d2424482424
+6d24246d24244824244824244824246d24244824244824246d2424482424
+6d24246d24246d24244824244824244824244824246d2424482424482424
+4824246d4848914848914848914848916d6d9148489148486d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6db69191b66d6d916d6d916d6db66d6d916d6d
+916d6d914848916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848916d6dda9191dab6b6ffb6b6
+dab6b6ffb6b6da91919148486d24246d48486d48489148489148486d4848
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191b69191b69191b69191
+b69191da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6db66d6dffb6b6ffb6b6916d6d4824244824244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24244824246d24246d24244824244824246d2424
+4824246d24244824244824244824244824244824246d2424482424482424
+4824246d24244824244824246d24246d24246d24244824244824246d2424
+4824246d24244824246d24244824246d24246d24244824246d2424482424
+6d24246d2424482424482424482424482424482424482424482424482424
+4824246d24249148489148489148489148489148486d48486d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffdada
+ffdadaffdadaffdadaffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191
+dab6b6da9191da9191da9191b69191b69191da9191b69191b69191da9191
+da9191b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db69191b66d6d916d6db66d6d916d6db66d6d
+916d6db66d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848b69191da9191ffb6b6ffb6b6
+ffffdada9191da91919148489148486d48489148486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d4848
+914848914848914848914848914848914848916d6d916d6d916d6d916d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191b69191da9191b69191b69191da9191
+da9191da9191da9191b69191b69191b66d6db66d6db69191b66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+914848916d6ddab6b6ffdadada91919148486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24244824244824246d24244824246d24244824246d2424482424
+6d24244824244824246d24244824246d24246d24246d2424482424482424
+4824246d24244824244824246d24244824244824246d24246d24246d2424
+6d24244824246d24246d48486d48486d48486d24246d24246d2424482424
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191da9191b69191b66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848da9191ffdadaffdada
+ffb6b6da9191b66d6d9148486d48489148486d48486d48486d48486d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d48486d48486d24246d48486d24246d48486d48486d4848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d
+914848914848b69191dab6b6ffdadab691919148486d24246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24244824244824246d24246d24244824244824246d2424482424
+4824244824246d24244824244824246d24246d24244824246d2424482424
+4824244824246d24244824244824246d24244824244824244824246d2424
+6d24246d2424482424482424482424482424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191
+b69191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db66d6db69191b66d6db66d6d916d6db66d6db66d6d916d6d
+b66d6d916d6d914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6d914848b66d6dffb6b6ffffdaffdada
+da9191b66d6d9148486d48486d24246d48486d24246d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d24246d24246d24246d48486d48486d48486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+da9191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d914848b66d6dda9191ffb6b6ffdadadab6b69148486d2424482424
+6d48484824246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d2424482424
+4824246d24246d24246d24246d24244824244824246d24246d2424482424
+4824244824246d24246d24246d24246d24244824244824244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6ffdadaffb6b6dab6b6dab6b6da9191
+dab6b6da9191dab6b6da9191da9191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6db66d6dda9191ffdadaffdadadab6b6
+da91919148489148486d24249148486d24246d48486d24246d48486d4848
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d48486d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b69191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+916d6d914848916d6db69191dab6b6ffdadaffdadab691919148486d2424
+4824244824246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d48486d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24244824244824244824246d24246d24246d24246d2424
+6d24244824246d24246d24244824244824244824244824246d2424482424
+4824246d24244824244824246d24244824246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191
+da9191dab6b6b69191da9191da9191da9191da9191b69191b69191da9191
+da9191b69191da9191b69191b66d6db66d6db66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191dab6b6ffb6b6ffdadaffdadada9191
+b66d6d916d6d6d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d48486d48486d4848914848914848
+916d6d914848916d6d916d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6ffdadaffb6b6b66d6d6d2424
+4824246d48484824246d48484824246d48484824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24246d24244824246d2424
+4824246d24246d24246d24244824246d24244824244824246d24246d2424
+4824244824246d24246d24246d24246d24244824246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6dab6b6ffb6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191b69191da9191da9191da9191da9191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6d916d6db66d6dda9191ffdadaffdadaffdadadab6b6b66d6d
+9148489148486d48486d48486d48486d48486d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d4848914848916d6d
+b66d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db69191dab6b6ffb6b6ffb6b6dab6b6916d6d914848
+4824246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24246d24246d24246d24246d24246d24246d2424482424482424
+4824244824244824246d24246d24244824244824244824244824246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffffdaffdadaffb6b6ffdadaffb6b6
+ffb6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffdadadab6b6
+da9191da9191da9191b69191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b69191b66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6ddab6b6ffdadaffdadaffb6b6da9191916d6d
+916d6d6d48489148489148489148486d48486d48486d24246d48486d2424
+6d48486d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d4848914848914848916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6dffb6b6ffdadaffdadada91919148486d2424
+9148486d24246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d48486d24246d48486d2424
+6d24246d48486d48486d24246d48486d24246d24246d48486d24246d2424
+6d48486d24246d24246d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24244824244824246d2424
+6d24246d24246d24246d24246d24244824244824246d24246d24246d2424
+6d24244824244824246d24246d24246d2424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6
+dab6b6da9191b69191da9191da9191b69191da9191da9191da9191da9191
+da9191b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d914848b66d6ddab6b6ffdadaffdadadab6b6b69191914848
+9148489148489148489148489148486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d4848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191da9191da9191b69191da9191da9191b69191b69191da9191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6ddab6b6ffb6b6ffdadadab6b6b66d6d914848
+9148486d48489148486d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d48486d24246d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824244824246d24244824244824246d24244824246d24246d2424
+6d24244824246d24246d24246d2424482424482424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6da9191b69191b69191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b66d6db66d6db69191b69191b69191b66d6d
+b66d6d916d6db69191b69191b66d6db66d6db69191b66d6db66d6db69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6d916d6d914848b69191ffb6b6ffdadaffb6b6b69191b66d6d916d6d
+916d6d916d6d9148489148486d48489148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191b69191b69191
+da9191da9191b69191b69191da9191da9191b69191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db69191ffb6b6ffdadaffb6b6b69191914848
+914848916d6d6d48486d48486d24246d48486d24246d48486d24246d2424
+6d24246d24246d24246d48486d48486d48486d48486d48486d24246d4848
+6d24246d48486d24246d24246d24246d24246d48486d24246d48486d4848
+6d24246d48486d48486d48486d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24244824244824246d2424
+4824246d24246d24246d24246d24246d24246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffdaffb6b6
+ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191b69191da9191da9191da9191b69191da9191
+da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d914848914848b69191ffb6b6ffdadadab6b6b69191916d6d916d6d
+914848916d6d9148489148486d48486d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d24246d48486d4848914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+da9191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6dda9191ffb6b6ffdadab66d6d916d6d
+6d48489148486d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d48486d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824244824244824246d24244824244824246d2424
+4824246d24244824244824244824244824244824246d24246d2424482424
+ffffdaffffdaffffdaffffffffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffdaffdada
+ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191da9191dab6b6da9191da9191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6d916d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6d916d6ddab6b6ffb6b6ffdadadab6b6b66d6db66d6d914848
+916d6d6d48489148486d48489148486d48486d48486d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d48486d2424
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6d916d6db69191ffdadaffdadada9191b66d6d
+9148486d48486d48486d24246d48486d24246d48486d24246d24246d4848
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d48486d24246d48486d24246d48486d4848
+6d48486d48486d48486d48486d24246d24246d48486d24246d48486d4848
+6d48486d48486d48486d48486d24246d48486d24246d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24244824244824246d24246d24244824244824246d2424
+6d24246d24246d24246d24244824244824244824244824246d2424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadadab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d916d6dda9191ffffdaffb6b6da9191b66d6d916d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d48486d24246d48486d48486d48486d48486d48486d4848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6d916d6db69191dab6b6ffb6b6ffb6b6b69191
+9148486d24246d24244824246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d48486d24246d24246d48486d24246d24246d2424
+6d48486d24246d24246d48486d48486d48486d48486d48486d24246d4848
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24244824244824244824244824246d2424
+6d24244824246d24246d24244824246d24246d2424482424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffdadaffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191b69191da9191b69191b69191da9191
+b69191da9191b69191b69191b69191b66d6db66d6db69191b66d6db66d6d
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6db69191ffb6b6ffb6b6ffb6b6b66d6db69191916d6d916d6d
+9148489148489148486d48486d48486d48486d48486d24246d48486d4848
+6d24246d48486d24246d24246d48486d24246d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191da9191da9191da9191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6db66d6db69191ffb6b6ffb6b6ffdadadab6b6
+916d6d4824246d48486d24246d48486d24246d48486d48486d48486d2424
+6d24246d24246d48486d48486d48486d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d48486d48486d24246d24246d24246d24246d24246d48486d24246d4848
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffdadaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6db66d6dda9191ffdadaffdadadab6b6b69191916d6db66d6d914848
+9148486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848916d6d914848914848916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191da9191
+da9191dab6b6da9191b69191b69191b66d6db69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db69191dab6b6ffdadaffb6b6b69191
+9148486d48486d24246d48486d24246d48486d24246d24246d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24244824246d24244824244824246d2424482424482424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6
+da9191dab6b6da9191da9191da9191da9191b69191b69191dab6b6da9191
+b69191b69191b69191da9191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b69191b66d6d916d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6ddab6b6ffdadaffdadab69191b69191b66d6db66d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848914848914848914848916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6dda9191ffb6b6ffffdaffb6b6b69191
+6d48486d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d48486d24246d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d48486d24246d4848
+6d24246d24246d48486d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d48486d24246d24246d24246d48486d24246d2424
+6d24246d48486d48486d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824246d24244824244824246d2424
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6dda9191ffdadaffb6b6da9191b66d6db69191b66d6d916d6d
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d2424
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191da9191da9191
+da9191da9191dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191da9191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6dda9191dab6b6ffdadaffdadab66d6d
+916d6d6d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d24246d48486d48486d24246d48486d48486d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d48486d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+6d24246d24246d24244824246d24246d24244824246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffb6b6ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6dab6b6
+da9191b69191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6ddab6b6ffdadaffb6b6b69191b69191b66d6db66d6d916d6d
+9148486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848914848914848916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191b69191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191b69191b69191da9191da9191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6d916d6db66d6ddab6b6ffdadaffdadada9191
+b691916d24249148486d24246d48486d24246d48486d48486d48486d4848
+6d24246d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d24246d24246d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24244824246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d2424482424
+ffffdaffffdaffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadaffdadaffdadaffb6b6da9191dab6b6dab6b6da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+916d6db66d6dda9191ffdadadab6b6b69191b66d6db66d6db69191916d6d
+6d48486d48486d24246d24246d48486d24246d48486d24246d48486d2424
+6d24246d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191da9191da9191b69191b66d6db69191b66d6db69191b69191
+b69191da9191b69191b69191da9191da9191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6da9191dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191b69191da9191da9191
+da9191da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6dffb6b6ffb6b6ffb6b6dab6b6
+b66d6d9148486d24246d48486d24246d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d48486d48486d24246d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6da9191
+dab6b6da9191da9191dab6b6da9191b69191b69191b69191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d916d6d
+916d6db66d6ddab6b6ffb6b6ffb6b6b69191b69191b66d6db66d6d916d6d
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848914848914848914848916d6d916d6d916d6db66d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b66d6db69191b69191
+da9191da9191b69191da9191da9191da9191b69191b69191da9191da9191
+da9191da9191da9191da9191da9191dab6b6da9191dab6b6dab6b6dab6b6
+dab6b6da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6ddab6b6ffdadaffb6b6da9191
+9148489148486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffb6b6ffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6da9191
+da9191b69191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+916d6d916d6db69191ffb6b6dab6b6b69191b66d6db69191916d6d916d6d
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+914848914848916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+da9191dab6b6da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6ddab6b6ffdadaffb6b6914848
+6d48486d24246d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191da9191dab6b6da9191b69191b66d6dda9191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db69191da9191da9191b69191b69191b66d6db66d6db66d6d
+916d6d916d6d9148486d48486d48489148486d48486d48486d24246d4848
+6d48486d48486d24246d24246d24246d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d2424914848914848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6da9191dab6b6dab6b6b69191b66d6db69191b69191b69191b69191
+da9191dab6b6dab6b6b69191b69191b69191b69191b69191da9191da9191
+da9191da9191da9191dab6b6dab6b6da9191da9191b69191b66d6db66d6d
+b66d6d916d6d914848914848914848914848916d6d914848914848914848
+916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191da9191ffdadaffdadab69191
+9148484824249148486d24246d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6db69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6ddab6b6da9191da9191b69191b69191b66d6db66d6d
+916d6d9148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d4848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6dab6b6dab6b6ffb6b6da9191b66d6db69191b69191b69191da9191
+dab6b6dab6b6dab6b6b69191b69191b69191b69191da9191da9191da9191
+dab6b6da9191da9191da9191b69191b69191b69191916d6d916d6d916d6d
+9148489148489148489148489148489148489148489148486d4848914848
+916d6db66d6db66d6db66d6db69191b69191b69191da9191b69191b69191
+b69191b69191b66d6db66d6db69191b69191da9191ffdadaffdadadab6b6
+916d6d9148486d24249148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6da9191
+b69191dab6b6dab6b6da9191dab6b6da9191b69191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191dab6b6ffb6b6dab6b6da9191da9191b69191b69191b66d6d
+916d6d916d6db66d6d9148489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6ffb6b6ffb6b6ffb6b6da9191b69191b66d6db69191b66d6dda9191
+dab6b6dab6b6dab6b6b69191b66d6db69191b69191da9191da9191da9191
+da9191b69191b66d6db66d6d916d6d916d6d916d6d9148489148486d4848
+6d48486d4848914848914848914848914848916d6d916d6d914848914848
+914848916d6d916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6dda9191da9191ffdadaffb6b6ffb6b6
+b69191916d6d9148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6dda9191ffb6b6ffb6b6dab6b6b69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6d916d6d916d6d914848914848914848914848
+9148486d48486d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d24246d48486d4848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191da9191da9191
+dab6b6ffb6b6ffb6b6dab6b6da9191b66d6db66d6db69191b69191dab6b6
+ffb6b6ffb6b6dab6b6da9191b66d6db69191b69191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6d916d6d916d6d914848914848
+914848914848916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db66d6db69191da9191ffb6b6ffb6b6ffb6b6da9191
+da9191916d6d916d6d6d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d24246d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824246d24244824246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191da9191da9191b69191b66d6db69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db69191ffb6b6ffb6b6da9191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b69191b69191916d6d914848914848916d6d
+9148486d48486d48486d48486d48486d48486d24246d24246d24246d4848
+6d24246d48486d48486d48486d4848914848914848914848914848914848
+914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+da9191dab6b6ffb6b6dab6b6da9191b66d6d916d6db66d6db69191da9191
+dab6b6dab6b6dab6b6b69191b69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d914848
+914848916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d914848916d6d916d6d916d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db69191da9191ffb6b6ffdadaffb6b6b66d6d
+916d6d6d48486d48486d48489148486d48489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24244824246d24246d24244824246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b69191b69191b69191b69191
+b69191da9191dab6b6da9191b69191b66d6db66d6db69191b69191b69191
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db69191ffdadaffdadadab6b6b69191da9191b69191b69191b69191
+b66d6d916d6db66d6db69191da9191b69191b66d6d916d6d914848916d6d
+916d6d6d48489148489148486d48486d24246d24246d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d4848914848914848914848916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191dab6b6dab6b6dab6b6da9191916d6db66d6db66d6db69191dab6b6
+dab6b6dab6b6da9191da9191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6d916d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191da9191
+b69191b66d6db66d6db66d6d916d6db66d6d916d6db66d6db69191b66d6d
+b69191b69191b69191b66d6db66d6dda9191ffb6b6ffdadaffb6b6b69191
+9148486d48486d48489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d24246d24246d24246d48486d48486d48486d4848
+6d24246d48486d48486d48486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6dab6b6da9191dab6b6
+ffb6b6dab6b6da9191b69191b69191da9191da9191b69191b69191b69191
+da9191da9191da9191b69191b69191b69191b66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffdadaffb6b6da9191da9191b66d6db69191b69191
+b66d6db66d6db66d6dda9191dab6b6da9191da9191b66d6db66d6d916d6d
+916d6d914848916d6d9148489148486d24246d24246d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191da9191dab6b6b69191b66d6db66d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191da9191b69191da9191
+da9191b66d6db66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6d
+b69191b66d6db66d6d916d6db66d6ddab6b6ffdadaffdadadab6b6b66d6d
+916d6db69191b69191b69191916d6d9148489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48489148489148489148486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d24246d48486d48486d24246d48486d2424
+6d24246d48486d48486d24246d24246d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffb6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191b69191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b66d6db69191b66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffffdaffdadaffb6b6dab6b6da9191b69191b66d6db69191
+b66d6d916d6d916d6dda9191ffb6b6ffb6b6ffb6b6da9191b69191916d6d
+914848916d6d916d6db66d6d9148486d48486d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d48486d24246d48486d24246d24246d48486d4848
+6d4848914848914848914848916d6d916d6db66d6db69191b69191b69191
+b69191b69191da9191da9191b69191b66d6d916d6db66d6db66d6dda9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db69191b66d6db69191b66d6db69191b69191
+b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6db69191dab6b6ffdadaffdadaffb6b6da9191
+da9191da9191dab6b6ffb6b6b691919148486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848916d6d9148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d24246d24246d48486d48486d2424
+6d48486d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+da9191dab6b6dab6b6da9191da9191da9191dab6b6da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffffdaffdadaffb6b6da9191da9191b69191b69191
+b66d6d916d6d914848b66d6ddab6b6ffb6b6ffdadaffb6b6dab6b6b69191
+916d6db66d6d916d6db66d6d9148489148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d24246d24246d2424
+6d24246d48486d4848914848914848916d6d916d6db66d6db69191b66d6d
+b69191b69191b69191b69191b69191b66d6d916d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191da9191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db66d6db69191ffb6b6ffb6b6ffb6b6ffb6b6b66d6d
+914848914848916d6ddab6b6dab6b6b66d6d6d48486d48486d24246d4848
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d24246d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffffffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffffdaffdadaffdadadab6b6dab6b6b69191b69191b66d6d
+b69191b66d6d914848914848b66d6dda9191ffb6b6ffb6b6dab6b6dab6b6
+b66d6db69191b66d6db66d6d9148486d48486d48489148486d48486d4848
+6d48486d24246d24246d48486d24246d24246d24246d24246d48486d4848
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d4848914848916d6db66d6db66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6dda9191
+dab6b6dab6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191da9191da9191da9191da9191
+b69191da9191b69191b69191b69191b69191da9191da9191b69191b69191
+b69191b69191b66d6db66d6dda9191ffb6b6ffdadada9191b66d6d916d6d
+914848916d6d916d6ddab6b6ffb6b6b69191916d6d6d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848916d6d916d6d916d6d916d6d9148486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6dab6b6ffdadaffb6b6
+dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6ddab6b6ffdadaffdadaffffdadab6b6dab6b6b69191b69191b69191
+b69191b66d6d916d6d914848914848b69191da9191dab6b6ffb6b6ffdada
+b69191b69191b66d6db66d6d9148486d48486d48489148486d48486d4848
+6d24246d24246d48486d48486d24246d48486d24246d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d4848914848916d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d916d6d916d6db66d6db69191
+da9191dab6b6dab6b6da9191da9191b69191b69191b66d6db69191b66d6d
+b69191b69191b69191b69191b69191b69191b66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6dda9191dab6b6dab6b6b66d6d916d6d916d6d
+b66d6db66d6d916d6db69191ffb6b6b691916d48486d48489148486d4848
+9148486d24249148486d48489148486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848916d6d916d6d9148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6dda9191ffffdaffffdaffdadaffb6b6da9191dab6b6b69191b69191
+b69191b69191b66d6d914848916d6db66d6db69191da9191da9191da9191
+da9191b69191b69191916d6d9148486d48489148489148486d48486d4848
+6d48486d48486d48486d24246d48486d24246d48486d4848914848914848
+914848916d6d9148489148489148489148489148486d48486d48486d2424
+6d48486d48486d48486d48486d24246d24246d24246d4848914848916d6d
+916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db69191
+da9191dab6b6dab6b6da9191dab6b6b69191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b66d6d9148486d48486d4848
+6d24246d24246d24246d48486d48486d4848916d6d916d6db66d6d916d6d
+916d6d916d6db66d6db69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db69191ffb6b6b691919148486d4848914848914848
+6d48486d48486d48489148489148489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+ffb6b6ffb6b6dab6b6da9191da9191dab6b6dab6b6da9191dab6b6da9191
+da9191b69191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db69191b69191b69191b69191b66d6db69191
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffdadaffffdaffdadaffb6b6dab6b6da9191da9191b69191
+b69191b69191b69191b66d6d916d6d916d6db66d6db66d6db69191b69191
+b69191b69191b69191916d6d9148489148486d48486d48486d48486d4848
+9148486d48486d48486d48486d4848914848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6d9148489148486d48486d24246d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6d916d6d6d48486d48486d4848
+6d24246d2424914848b66d6db66d6db66d6db66d6dffb6b6b69191b66d6d
+916d6d914848916d6d914848916d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db66d6db69191b69191b69191b69191b69191
+b69191b66d6db66d6ddab6b6ffb6b6916d6d482424916d6d6d2424916d6d
+6d24249148486d48489148486d48489148486d48486d48486d4848914848
+6d48489148486d48489148489148486d48489148486d48486d48486d4848
+6d48489148489148486d48486d4848914848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffdada
+ffdadaffffdaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191da9191b69191b69191b66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b69191da9191ffdadaffdadaffffdaffb6b6dab6b6da9191da9191b69191
+b69191b69191b69191b69191916d6d914848916d6db69191b69191b69191
+b69191b69191b69191b66d6d916d6d6d48489148486d48486d48486d4848
+6d48486d4848914848914848914848916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6d9148486d48486d48486d24246d2424
+6d48486d48486d48486d48486d4848914848916d6d916d6d916d6db66d6d
+b69191da9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191da9191b69191b69191b66d6db66d6d914848914848914848
+6d48486d2424914848b69191916d6d916d6dffb6b6ffb6b6ffdadab69191
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191da9191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db69191dab6b6dab6b6916d6d9148486d24249148486d4848
+9148486d48489148486d48489148486d48486d48486d4848914848914848
+9148486d48486d48489148486d48489148486d48486d48486d4848914848
+9148489148489148489148486d4848914848914848914848914848914848
+9148489148486d48486d48489148489148486d48489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d48486d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffffdaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191da9191b69191b66d6d
+b66d6db66d6db69191b69191b66d6db66d6db69191b66d6db66d6db66d6d
+b69191da9191ffb6b6ffdadaffffdaffdadadab6b6da9191dab6b6da9191
+b69191b69191b69191b69191b66d6d916d6db69191b69191b69191b69191
+b66d6db69191b69191b66d6d916d6d6d48486d48486d48486d4848914848
+6d48486d48486d4848914848914848914848914848916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d2424
+6d24246d24246d48486d24246d24246d4848914848916d6d916d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d914848914848914848
+9148486d48486d48486d24246d2424916d6dffb6b6ffb6b6da9191b69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b66d6db66d6db66d6db69191da9191b69191b69191
+b69191b69191b69191ffb6b6da91919148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+9148489148489148489148486d48486d48489148486d48486d48486d4848
+6d48489148489148489148486d48486d4848914848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffffdaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffdada
+ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6dda9191ffdadaffffdaffdadaffb6b6dab6b6dab6b6dab6b6
+da9191b69191b66d6db66d6db69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b66d6d916d6d6d48486d48486d48486d4848914848
+6d48486d48489148489148486d4848914848914848916d6d916d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6d6d48486d48486d2424
+6d24246d24246d24246d24246d24246d2424914848916d6d916d6d916d6d
+b66d6db69191b69191b69191da9191da9191da9191da9191b69191da9191
+da9191da9191da9191b69191b66d6db66d6db66d6db66d6d916d6d916d6d
+9148489148489148486d4848914848b66d6dda9191da9191da9191da9191
+da9191da9191da9191da9191dab6b6dab6b6da9191b69191da9191da9191
+da9191da9191b69191b69191b66d6db66d6db69191b69191b69191b69191
+b69191b69191dab6b6ffb6b6b69191916d6d9148486d4848914848914848
+6d48489148486d48489148489148486d48486d4848914848914848914848
+9148486d48489148489148486d48486d48486d4848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48486d48489148486d48486d48486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d24246d48486d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d24246d24246d48486d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffffdaffffdaffdadaffb6b6dab6b6ffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+916d6db66d6dda9191ffdadaffdadaffffdaffdadaffb6b6dab6b6dab6b6
+da9191da9191b66d6db66d6db69191b66d6db66d6db66d6db69191b69191
+b69191b69191b69191b66d6d9148489148486d48486d48486d4848914848
+9148486d48486d48486d48486d4848914848916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d914848914848914848916d6d916d6db66d6db66d6d
+b66d6d914848916d6d914848916d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+da9191da9191b69191b69191b69191916d6d916d6db66d6d916d6d916d6d
+916d6d914848914848916d6db66d6db69191b69191da9191da9191dab6b6
+da9191b69191b69191b69191da9191da9191da9191b69191b69191b69191
+da9191b69191b69191b66d6d916d6db66d6db66d6db69191b69191da9191
+b69191b69191dab6b6ffb6b6da9191916d6d6d4848916d6d914848914848
+6d48489148486d48489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48489148486d48486d48486d4848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d48486d48486d24246d24246d24246d24246d24246d4848
+6d24246d24246d48486d24246d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d48486d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffffdaffffdaffdadaffffdaffb6b6ffb6b6ffdadaffb6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191da9191da9191da9191da9191
+b69191b69191b69191da9191da9191dab6b6da9191b69191b66d6db69191
+b66d6db66d6dda9191ffdadaffffdaffffdaffdadaffb6b6dab6b6da9191
+dab6b6da9191b69191b66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191b69191da9191b69191b66d6db66d6d9148486d4848914848914848
+9148486d48486d48486d48486d4848916d6d916d6d916d6d914848916d6d
+9148486d48486d48486d48486d24246d48486d4848914848916d6db66d6d
+916d6d916d6d9148486d48489148486d48486d48486d24246d24246d4848
+6d48486d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191da9191
+dab6b6da9191b69191b69191b69191916d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db69191b69191da9191dab6b6dab6b6dab6b6dab6b6
+da9191b69191b69191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191da9191b69191
+b69191b69191dab6b6ffb6b6ffb6b6916d6d916d6d6d48489148486d4848
+9148489148489148486d48486d48486d48489148486d4848914848914848
+9148489148486d48486d48489148489148489148489148489148486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48489148486d48489148486d48489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d48486d48486d24246d2424
+6d48486d24246d24246d48486d24246d24246d24246d48486d24246d2424
+6d24246d24246d24246d24246d48486d48486d24246d48486d24246d2424
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6dab6b6
+ffb6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191dab6b6
+da9191da9191b69191b69191b69191b69191b69191da9191b69191da9191
+b69191b69191b69191b69191b69191da9191da9191dab6b6da9191da9191
+b66d6db66d6db69191da9191dab6b6dab6b6dab6b6b69191b66d6db66d6d
+b66d6db66d6dda9191ffb6b6ffffdaffffdaffffdaffb6b6ffb6b6da9191
+da9191da9191b69191b66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191916d6d916d6d6d48486d48486d4848914848
+6d48486d48486d48486d48486d4848916d6d9148489148486d48486d4848
+6d48486d48486d48486d24246d4848916d6db66d6db66d6db69191b66d6d
+b66d6d9148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848b66d6db66d6d
+b66d6db69191b66d6db69191b66d6db69191b69191b69191b69191b69191
+da9191b69191b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+916d6db66d6db66d6db69191b69191da9191da9191b69191da9191da9191
+da9191b69191da9191da9191b69191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191da9191
+b66d6db66d6dda9191ffb6b6ffb6b6b66d6d914848914848914848914848
+9148486d48489148489148489148486d48489148486d4848914848914848
+9148489148486d48486d48486d48486d48486d48489148486d4848914848
+9148489148489148489148489148489148486d4848914848914848914848
+6d48489148486d48489148486d48486d48486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d24246d2424
+6d48486d48486d48486d24246d48486d48486d48486d48486d48486d2424
+6d48486d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191dab6b6dab6b6dab6b6
+da9191b69191b69191b66d6db69191b69191da9191da9191da9191b69191
+b69191b66d6db69191b69191dab6b6da9191da9191b69191b66d6db66d6d
+b66d6db69191da9191ffb6b6ffdadaffffdaffdadaffdadaffb6b6da9191
+b66d6db66d6db66d6db66d6d916d6db66d6db69191b66d6db66d6db66d6d
+b69191b69191b69191b66d6d9148489148486d4848914848914848914848
+6d48486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48484824246d2424b69191b69191914848da9191ffb6b6
+b66d6d6d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848916d6d916d6db66d6d
+b66d6db69191b69191b69191b69191da9191b69191b69191b69191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191da9191da9191da9191dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191
+da9191b69191b69191b69191b66d6db66d6db66d6db66d6db69191da9191
+b69191b66d6db69191da9191dab6b6b66d6d916d6d914848914848916d6d
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d4848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148486d48486d48489148489148486d48486d4848914848914848914848
+914848916d6d9148489148489148489148489148489148486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191b69191da9191da9191dab6b6dab6b6ffb6b6ffb6b6
+dab6b6da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191da9191da9191b69191b69191b66d6db69191
+b66d6db66d6db66d6dda9191ffb6b6ffdadaffffdaffdadaffb6b6b66d6d
+914848914848914848916d6d916d6db69191b69191b69191b66d6db66d6d
+b66d6db69191b69191916d6d9148486d48486d48489148486d48486d4848
+9148489148489148489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d4848916d6ddab6b6ffb6b6
+b691916d24246d24244824246d48486d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db69191b69191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dda9191
+b69191b69191b66d6dda9191ffb6b6b691919148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148486d4848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48486d48489148486d48486d48489148486d48486d4848914848914848
+9148489148489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d48486d4848
+6d48486d24246d24246d24246d48486d24246d24246d24246d48486d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6ffb6b6ffb6b6ffdadaffffdaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191b69191b69191b69191da9191dab6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b66d6db66d6db69191
+b69191b69191916d6db66d6ddab6b6ffdadaffdadaffffdaffb6b6b69191
+6d48486d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b69191b69191b69191916d6d9148486d48486d48486d48486d48486d4848
+916d6db66d6d916d6d6d48486d4848914848916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d4848b66d6dda9191b69191
+916d6d6d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6d916d6d
+b66d6db66d6db69191b66d6db69191b69191b69191da9191da9191b69191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191dab6b6dab6b6dab6b6dab6b6ffb6b6ffb6b6
+dab6b6dab6b6da9191dab6b6da9191da9191da9191da9191da9191b69191
+b69191b69191b66d6db69191b69191b66d6db66d6d916d6db69191da9191
+dab6b6da9191b69191da9191dab6b6dab6b6916d6d914848914848914848
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48489148486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191b69191b69191b69191b69191dab6b6dab6b6ffb6b6ffb6b6
+dab6b6da9191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db69191
+da9191da9191da9191b69191dab6b6ffb6b6ffdadaffdadaffdadadab6b6
+9148486d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b69191b69191b691919148489148486d48486d48486d48486d4848916d6d
+b66d6d916d6d916d6d6d48486d24249148486d4848b66d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d916d6db66d6d916d6d916d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b66d6db69191b69191b69191da9191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191dab6b6b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b66d6d916d6db66d6db69191
+dab6b6dab6b6da9191b69191dab6b6dab6b6916d6d6d4848916d6d6d4848
+9148486d4848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148486d48489148486d4848914848
+6d48489148489148489148489148489148489148489148486d4848914848
+9148489148489148489148486d48489148486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d4848
+6d48486d48486d48486d48486d24246d24246d48486d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191b69191da9191da9191dab6b6dab6b6
+dab6b6b69191b69191b66d6db69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db69191dab6b6
+ffdadaffb6b6da9191b66d6db66d6dda9191ffb6b6ffdadaffffdaffb6b6
+916d6d6d48486d48486d4848b66d6db69191b69191b69191b66d6d916d6d
+b69191da9191da9191916d6d6d48486d48486d48486d48486d4848916d6d
+b66d6db66d6d9148486d48486d24246d48486d4848914848916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6d6d48486d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848b66d6d
+b66d6db66d6db66d6db69191b66d6db69191b69191da9191da9191da9191
+da9191b69191b69191da9191dab6b6da9191da9191da9191da9191da9191
+da9191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+da9191dab6b6dab6b6da9191dab6b6da9191916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d48486d24246d48486d48486d24246d48486d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffdadaffb6b6ffb6b6ffdadaffb6b6ffdadaffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191b69191b69191b66d6db69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6dda9191ffdada
+ffdadab691916d48486d24244824246d2424b69191ffb6b6ffffdaffb6b6
+916d6d6d48486d48486d2424916d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b66d6d9148486d48486d48486d48486d4848914848914848
+916d6d916d6d9148486d48486d48486d24246d4848914848916d6d916d6d
+916d6d916d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191b69191da9191da9191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+b69191da9191dab6b6dab6b6dab6b6da9191b66d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d916d6db66d6db66d6db66d6d916d6d9148486d4848914848914848
+9148489148486d48489148489148489148489148489148486d4848914848
+9148489148489148489148486d48486d48489148489148486d48486d4848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d24246d24246d48486d4848
+6d48486d24246d48486d48486d48486d48486d24246d24246d24246d2424
+6d24246d24246d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d24246d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffda
+ffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6ddab6b6ffdada
+ffb6b6b66d6d6d48486d48484824246d24246d2424916d6d916d6d916d6d
+6d48486d48486d2424914848914848b69191b69191b69191b66d6db69191
+b66d6db69191916d6d6d48486d48486d48486d4848914848916d6d914848
+916d6d916d6d916d6d9148489148486d48486d4848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+9148486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d2424914848916d6d
+916d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+916d6db69191da9191dab6b6dab6b6da9191b66d6d916d6d6d4848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db69191dab6b6dab6b6da9191da9191916d6d6d4848914848b66d6d
+b66d6db66d6d914848914848914848914848914848914848914848914848
+9148489148486d48489148486d48489148489148489148486d48486d4848
+9148489148489148489148489148486d48489148486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d48486d24246d48486d48486d48486d24246d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffffda
+ffffdaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db69191b66d6db69191b66d6db66d6d916d6db66d6dffb6b6ffdada
+ffb6b6b69191916d6d9148486d48486d48486d24246d48486d24246d2424
+6d24246d48486d4848914848916d6db66d6db69191b69191b66d6db66d6d
+b66d6db66d6d916d6d9148486d48489148486d48489148486d4848914848
+914848914848914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+b66d6db66d6d916d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db69191da9191dab6b6b69191914848916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+b66d6dda9191dab6b6dab6b6dab6b6dab6b6b69191914848b69191b69191
+dab6b6da9191b66d6d916d6d9148486d4848914848914848914848914848
+9148486d48489148489148489148489148489148486d48486d48486d4848
+9148489148486d48489148489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d4848
+6d48486d48486d48486d48486d48486d48486d24246d24246d48486d2424
+6d24246d48486d48486d24246d48486d24246d48486d48486d24246d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6d916d6db66d6ddab6b6ffffda
+ffb6b6b66d6db66d6d916d6db66d6d9148486d48486d48486d48486d4848
+6d48486d4848914848b66d6db66d6db69191b69191b69191b66d6db66d6d
+b69191b66d6d916d6d9148486d48489148486d48486d48486d48486d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d9148486d48486d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191da9191da9191da9191b69191da9191da9191dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191b69191b69191b69191da9191da9191
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191
+b66d6d916d6d916d6db69191da9191dab6b6da9191916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+916d6dda9191ffb6b6dab6b6dab6b6dab6b6da9191b66d6ddab6b6ffb6b6
+ffb6b6ffb6b6da9191916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d48486d24246d48486d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6ddab6b6ffb6b6
+ffdadab66d6d916d6d916d6db66d6d916d6d9148486d48486d48486d4848
+6d48486d4848914848916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d9148486d48486d48486d48486d48486d48486d48486d4848
+9148486d4848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db69191b66d6d
+916d6d6d48486d48486d24246d24244824246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191da9191dab6b6
+dab6b6dab6b6dab6b6da9191da9191b69191da9191da9191b69191da9191
+da9191da9191da9191da9191dab6b6da9191da9191da9191da9191da9191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+916d6d916d6d914848b66d6db69191da9191da9191916d6d916d6d914848
+914848916d6d9148489148489148489148489148489148486d4848914848
+916d6db69191dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6
+dab6b6dab6b6b69191916d6d9148486d48486d48489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24246d48486d48486d48486d24246d24246d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffb6b6dab6b6ffb6b6ffb6b6ffdadaffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191da9191b69191da9191
+b69191b69191b66d6db69191b69191da9191da9191b69191da9191b69191
+b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6dda9191ffdada
+ffdadab66d6d914848916d6d916d6db66d6d916d6d916d6d9148486d4848
+6d48486d24246d4848914848b66d6db66d6db66d6db69191b66d6db69191
+b66d6d9148486d48486d48486d48486d48486d48486d48486d48486d4848
+914848914848914848916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+916d6d9148486d48486d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424914848
+916d6d916d6db66d6db66d6db69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191da9191da9191dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db69191da9191dab6b6da9191b66d6d914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db66d6db69191da9191dab6b6dab6b6ffb6b6dab6b6ffb6b6
+dab6b6dab6b6b69191914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+6d48486d48489148486d48489148486d48489148486d48486d48486d4848
+6d48489148489148486d48486d48486d48486d48489148486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d4848
+6d24246d48486d48486d48486d24246d48486d48486d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadadab6b6dab6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6b69191da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191da9191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db69191ffb6b6
+ffdadab69191914848916d6d914848916d6d916d6db66d6d916d6d6d4848
+6d48486d24246d2424914848916d6db66d6db66d6db66d6db66d6db69191
+b66d6d9148486d48486d48486d24246d48486d24246d48486d24246d4848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+9148486d48486d24246d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+914848916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b69191
+b69191da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+914848914848916d6d916d6db69191da9191b69191916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d4848916d6dda9191ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6b69191b66d6d9148486d4848914848914848914848914848914848
+9148489148489148489148489148486d4848914848914848914848914848
+9148489148489148489148486d48486d48486d48489148489148486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d48486d24246d4848
+6d48486d48486d48486d24246d24246d24246d24246d48486d48486d2424
+6d24246d24246d48486d48486d48486d48486d24246d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191dab6b6
+da9191da9191b69191da9191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffdadada9191916d6d914848b66d6d914848b66d6db66d6db66d6d914848
+9148486d48486d48486d4848916d6d916d6db69191b66d6db66d6db66d6d
+b66d6d9148486d48486d48486d48486d48486d48486d24246d48486d4848
+916d6db66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d914848
+6d48486d24246d48486d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191da9191b69191da9191da9191b69191b69191b69191
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6d916d6db66d6db66d6db69191da9191da9191916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+9148486d4848914848b66d6ddab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+da9191916d6d9148489148486d4848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148489148486d48486d48486d48489148486d4848
+6d48486d48489148489148489148486d48486d48486d48489148486d4848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d24246d48486d24246d24246d48486d24246d24246d2424
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6dab6b6da9191b69191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffdadada9191b66d6d916d6d916d6d914848916d6d916d6d916d6d914848
+6d48486d48486d48486d4848916d6db66d6db66d6db66d6db66d6db66d6d
+916d6d9148486d48486d24246d24246d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d6d4848
+6d48486d24246d24246d48486d48486d24246d24246d24246d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+914848916d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191da9191b69191b69191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6d
+916d6d916d6db66d6db66d6db69191dab6b6da9191914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+9148486d48486d4848b66d6dda9191dab6b6ffb6b6ffb6b6ffb6b6da9191
+b66d6d9148489148486d4848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d24246d48486d4848
+6d48486d48486d48486d48486d48486d48486d24246d48486d48486d2424
+6d24246d24246d48486d24246d24246d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffdadaffdadaffdadaffffdaffdadaffdadaffdadaffb6b6ffb6b6dab6b6
+dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191b69191da9191b69191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6
+ffffdada9191b66d6d914848916d6d916d6d916d6db66d6d916d6d914848
+6d48486d48486d24246d4848914848b66d6db66d6db66d6db66d6db66d6d
+9148486d48486d24246d48486d24246d48486d48486d48486d4848916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d9148486d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+914848916d6d916d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191da9191da9191
+da9191b69191b69191da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191da9191dab6b6da9191b66d6d914848916d6d
+914848914848914848914848914848914848914848914848916d6d914848
+9148486d48486d4848914848b66d6dda9191dab6b6da9191da9191b66d6d
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48489148486d48489148486d48486d48486d48486d48486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+6d24246d24246d24246d48486d48486d48486d48486d24246d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6dffb6b6
+ffdadadab6b6916d6db66d6d914848916d6d914848916d6d916d6d914848
+6d48486d48486d48486d4848914848b66d6db66d6d916d6d916d6db66d6d
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6d916d6d916d6d916d6d9148486d48486d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6db66d6db69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b66d6db69191da9191
+da9191da9191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191da9191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6dffb6b6b69191914848914848914848
+914848914848914848914848914848914848914848916d6db66d6d916d6d
+9148489148486d48486d4848914848916d6d916d6db66d6d916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848914848914848914848914848914848
+9148486d48489148486d48489148486d4848914848914848914848914848
+6d48489148489148489148486d48486d48489148486d48486d48486d4848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d24246d24246d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d48486d4848
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffffdaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6
+dab6b6dab6b6dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6ddab6b6
+ffdadadab6b6b66d6d914848916d6d914848916d6d916d6d916d6d914848
+6d48486d24246d48486d4848916d6db69191b66d6d916d6db66d6d916d6d
+916d6d6d48486d48486d48486d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6d916d6db66d6db66d6db69191b69191b69191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191da9191
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db66d6db69191dab6b6da9191916d6d916d6d6d4848
+916d6d914848916d6d914848916d6d914848b66d6db66d6db69191b66d6d
+916d6d916d6d9148489148489148489148489148489148486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+6d48486d48486d48486d48486d48486d48489148489148486d48486d4848
+9148489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d24246d48486d4848
+6d48486d48486d48486d24246d24246d48486d24246d24246d24246d4848
+ffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6dda9191
+ffb6b6dab6b6b66d6d916d6d914848914848914848914848916d6d914848
+6d48486d48486d24246d2424916d6d916d6db66d6db66d6db66d6db66d6d
+916d6d6d48486d48486d48486d24246d48486d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d916d6d6d48486d48486d24246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d4848914848916d6db66d6db66d6db69191b69191b69191b69191da9191
+da9191da9191da9191b69191b69191b66d6db69191b66d6db66d6db66d6d
+b69191da9191dab6b6dab6b6dab6b6da9191da9191b69191b69191b69191
+da9191da9191b69191da9191da9191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6d916d6db69191ffb6b6b66d6d914848914848914848
+914848916d6d914848914848914848916d6db66d6db66d6db66d6db66d6d
+b66d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148489148486d4848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48489148489148489148486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d2424
+6d24246d24246d48486d48486d24246d48486d24246d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6
+ffb6b6ffdadaffdadaffffdaffdadaffdadaffb6b6dab6b6ffdadadab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6db69191
+ffb6b6ffb6b6b69191914848916d6d916d6d914848916d6d916d6d916d6d
+9148489148486d48486d2424914848b66d6db66d6db69191b66d6db66d6d
+b66d6d916d6d9148486d48486d24246d48486d48486d48486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d6d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d2424
+6d2424914848916d6d916d6db66d6db69191b69191b69191da9191dab6b6
+ffb6b6ffb6b6dab6b6da9191b69191b69191b69191b69191da9191b69191
+b69191b69191da9191ffb6b6dab6b6dab6b6da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6ddab6b6ffb6b6b66d6d916d6d6d4848916d6d
+914848914848914848914848914848914848916d6db69191b69191b66d6d
+b66d6d914848914848914848914848914848916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148489148489148486d48489148489148486d48486d48486d4848
+6d48486d48489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d24246d48486d48486d48486d48486d24246d48486d48486d4848
+6d48486d48486d24246d24246d24246d48486d48486d24246d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadadab6b6ffb6b6dab6b6
+da9191dab6b6dab6b6dab6b6dab6b6da9191b69191b69191da9191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+ffb6b6ffb6b6b69191916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d9148486d48486d48486d4848da9191b69191da9191b66d6db69191
+b66d6d916d6d6d48486d48484824246d48486d24246d48486d4848914848
+914848916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d48486d2424
+6d48486d24246d24246d24246d24246d48486d24246d24246d24246d4848
+6d48486d24246d48486d24246d24246d24246d48486d48486d24246d2424
+6d4848914848916d6d916d6db66d6db66d6db69191da9191ffb6b6ffb6b6
+ffdadaffb6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191da9191dab6b6da9191dab6b6b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6d916d6ddab6b6ffb6b6b66d6d6d48489148486d4848
+9148489148489148486d4848914848914848916d6db66d6db66d6db66d6d
+916d6d914848914848914848916d6db66d6db69191b69191b66d6d914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6d916d6d916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148489148489148489148489148489148489148486d48486d4848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+ffffdaffffdaffffdaffffffffffdaffffdaffffdaffffdaffffdaffffff
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffffda
+ffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6dab6b6
+dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b66d6db66d6db69191b69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6d916d6db66d6d
+dab6b6ffdadada9191b66d6d914848916d6d914848916d6d916d6d916d6d
+916d6d9148486d48486d2424914848b69191ffb6b6da9191da9191b66d6d
+b691919148486d48486d24246d48486d24246d48486d24246d48486d4848
+914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d9148486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+914848914848916d6d916d6db66d6db66d6dda9191dab6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6da9191b69191b69191b69191b69191b69191b69191
+da9191b69191b66d6db69191b69191dab6b6dab6b6dab6b6da9191da9191
+da9191b69191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6db69191ffb6b6ffb6b6da9191914848914848914848
+916d6d916d6d916d6d914848916d6d914848916d6d916d6d914848916d6d
+914848914848914848916d6d916d6dda9191dab6b6ffb6b6da9191b66d6d
+914848914848914848914848914848914848914848914848914848916d6d
+b69191b69191b69191b66d6db66d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d48489148486d4848914848914848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6ffb6b6ffdadaffdadaffdadaffdadaffdadadab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+dab6b6ffdadadab6b6b66d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d9148486d48484824246d2424b66d6ddab6b6dab6b6da9191b69191
+916d6d9148486d48486d24246d24246d48486d48486d48486d24246d4848
+914848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6d916d6d9148486d48486d48486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d24246d24246d24246d48486d24246d24246d4848
+6d4848914848916d6d916d6d916d6db66d6db69191dab6b6da9191dab6b6
+da9191da9191b69191b69191b66d6db66d6db66d6db69191b69191b69191
+da9191da9191b69191b66d6db66d6db66d6db69191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b66d6db69191ffb6b6dab6b6b66d6d916d6d916d6d914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848916d6db66d6ddab6b6ffb6b6ffb6b6dab6b6b69191
+916d6d6d48489148486d4848914848914848914848914848914848b66d6d
+b69191b69191b69191b69191b66d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48489148489148489148489148486d48486d48486d4848
+6d48489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d48486d48486d24246d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffb6b6
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191da9191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db69191b66d6db66d6db66d6db69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+dab6b6ffdadadab6b6916d6d9148486d4848914848916d6d916d6d916d6d
+916d6d916d6d6d48486d2424482424914848da9191ffb6b6dab6b6da9191
+916d6d916d6d6d24246d24246d24246d48486d24246d48486d48486d4848
+6d4848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6d9148486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d24246d24246d24246d48486d48486d48486d2424
+6d4848914848914848916d6d916d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+dab6b6dab6b6dab6b6da9191b69191b66d6db69191da9191dab6b6da9191
+dab6b6b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191da9191dab6b6da9191916d6d916d6d914848916d6d
+916d6d916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848916d6db66d6ddab6b6ffb6b6ffb6b6dab6b6b69191
+916d6d914848914848914848914848914848914848914848914848b66d6d
+b66d6db69191b69191b69191916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148489148486d4848
+6d48486d48486d48486d4848914848916d6db66d6db66d6d914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d24246d48486d48486d48486d48486d48489148486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b66d6db66d6db69191b69191b66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+da9191ffb6b6ffb6b6b66d6d914848914848914848916d6d914848b66d6d
+916d6db66d6d6d48486d48484824246d2424916d6ddab6b6dab6b6ffb6b6
+b69191b66d6d9148486d24246d24246d24246d24246d24246d24246d4848
+6d4848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6db66d6db66d6d916d6d9148486d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d4848916d6d916d6db66d6db66d6db69191b69191b66d6d
+b69191b69191b69191b69191b66d6d916d6db66d6db66d6db69191b69191
+da9191da9191dab6b6dab6b6b69191b66d6db66d6dda9191da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db69191b69191b69191dab6b6b69191914848916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848b66d6dda9191dab6b6dab6b6da9191b66d6d
+916d6d916d6d914848916d6d914848914848914848914848914848916d6d
+916d6db66d6db66d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d4848914848914848914848914848
+6d48489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148489148486d48486d48486d48486d4848914848
+6d48486d48486d4848914848916d6db66d6db69191b69191b66d6d914848
+9148486d24246d48486d48489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffffffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6dffb6b6ffdadada91919148489148486d2424916d6d916d6db66d6d
+916d6db66d6d9148486d48486d24246d24246d4848da9191dab6b6ffb6b6
+dab6b6b66d6d9148486d48486d24244824246d48486d24246d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6d916d6d9148489148486d48486d48486d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d4848914848914848916d6db66d6db66d6db69191b66d6d
+b69191b69191da9191da9191b66d6db66d6db66d6db69191b69191b69191
+da9191da9191da9191dab6b6da9191da9191b69191b69191da9191dab6b6
+dab6b6da9191da9191da9191b69191da9191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b69191b69191b69191b69191da9191916d6d916d6d914848916d6d914848
+916d6d914848916d6d916d6d914848916d6d914848914848914848914848
+914848914848914848914848916d6db66d6db66d6db66d6db66d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148489148486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848b66d6db66d6db66d6db69191b69191916d6d
+9148486d48486d48486d48486d48489148486d4848914848914848914848
+9148489148486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d24246d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffb6b6dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6ddab6b6ffb6b6dab6b6b66d6d9148489148486d4848914848914848
+916d6d916d6d9148486d48486d48484824246d2424b66d6ddab6b6dab6b6
+ffb6b6b69191916d6d916d6d6d48486d24246d48486d24246d24246d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6db66d6d916d6d9148489148486d48486d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848914848916d6db66d6db66d6db69191
+b69191da9191da9191da9191b69191b66d6d916d6db66d6db69191b69191
+b69191b69191b69191da9191dab6b6da9191b69191b69191b69191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db69191b69191b66d6d916d6d914848916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d48486d4848
+9148489148489148489148489148486d48489148486d48486d4848914848
+6d48486d48489148489148489148489148486d4848914848914848914848
+6d48486d48486d4848914848916d6db66d6db69191b69191914848916d6d
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffb6b6dab6b6ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191ffb6b6ffb6b6da9191b691919148486d2424916d6d914848
+b66d6d914848916d6d6d48486d48486d24244824246d2424dab6b6dab6b6
+ffb6b6dab6b6b69191b66d6d9148486d24246d24246d48486d24246d4848
+6d48486d48486d4848914848914848916d6d914848916d6d916d6d916d6d
+916d6db66d6d916d6d9148489148486d48489148486d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d4848914848916d6db66d6db66d6db69191
+dab6b6dab6b6dab6b6b69191b69191b66d6db69191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848916d6d914848914848914848
+914848916d6d914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d48486d4848914848
+9148489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d4848914848916d6db66d6d916d6d9148486d2424
+6d48486d48486d48486d48486d48486d48489148489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffb6b6ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6dab6b6da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db69191ffb6b6ffb6b6dab6b6da91919148486d48486d4848914848
+916d6d916d6d916d6d9148486d48486d48484824246d2424da9191ffb6b6
+ffb6b6ffb6b6da9191da91916d48486d48486d24246d48486d24246d4848
+6d24246d48486d48486d48486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d9148489148489148486d48486d48486d48486d2424
+6d24246d48486d24246d24246d24246d24246d48486d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d24246d24246d24246d2424
+6d24246d48486d4848914848914848914848916d6d916d6db66d6dda9191
+dab6b6dab6b6da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48489148486d48486d48486d48489148486d4848
+9148486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffb6b6ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6da9191
+da9191da9191dab6b6dab6b6dab6b6da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6dda9191ffb6b6ffb6b6dab6b6b691919148486d4848914848
+914848916d6d6d48489148486d24246d48486d24246d2424b66d6dffb6b6
+dab6b6dab6b6da9191b69191916d6d6d48486d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d4848914848914848914848916d6d
+916d6d916d6d9148489148489148489148486d48486d48486d48486d4848
+6d24246d48489148486d48486d48486d24246d24246d48486d24246d2424
+6d24246d24246d48486d48489148489148486d48486d24246d24246d2424
+6d24246d4848916d6d916d6db66d6db66d6db66d6db66d6db69191da9191
+dab6b6dab6b6dab6b6da9191da9191da9191b69191da9191b69191da9191
+b69191b69191b69191b69191da9191b69191da9191b69191b69191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+914848916d6d914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d916d6d916d6d914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48486d48486d48486d48489148486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191dab6b6da9191dab6b6da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6ddab6b6ffdadaffdadaffb6b6b66d6d916d6d6d2424914848
+9148486d48486d48486d48486d48486d48486d2424482424916d6ddab6b6
+dab6b6dab6b6dab6b6b66d6d9148486d48486d48486d24249148486d4848
+6d48486d24246d48486d24246d48486d48486d48486d4848914848914848
+916d6d916d6d916d6d916d6d916d6d9148489148489148486d4848914848
+9148489148489148489148486d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d48486d48489148486d48486d48486d24246d2424
+6d2424914848916d6db66d6db69191b69191b69191b69191b66d6db69191
+da9191dab6b6dab6b6da9191da9191da9191da9191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b66d6d
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848916d6d914848916d6d914848916d6d914848914848914848914848
+914848914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848916d6d
+b66d6db66d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148489148489148486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffdadaffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db69191ffb6b6ffdadaffb6b6b66d6d6d4848914848914848
+b66d6d6d48486d48486d24246d48486d48486d24246d24246d4848da9191
+ffb6b6dab6b6dab6b6916d6d9148486d24246d48486d24249148486d4848
+6d24246d24246d24246d24246d24246d24246d48486d4848914848914848
+914848914848916d6d916d6d916d6d9148486d48489148486d4848914848
+916d6d916d6db66d6d916d6d916d6d916d6d9148486d4848914848914848
+914848914848914848916d6d9148486d48486d48486d48486d48486d2424
+6d2424916d6d916d6db66d6db66d6dda9191b69191b69191b66d6dda9191
+da9191dab6b6dab6b6dab6b6dab6b6da9191da9191b69191da9191b69191
+da9191da9191b69191b69191da9191da9191da9191b69191b69191b69191
+b69191da9191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848b69191
+b69191b69191b69191b66d6d9148489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148486d48489148489148486d48486d48489148486d48489148486d4848
+6d48489148489148486d48489148486d48489148486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffdaffdadaffdada
+ffdadaffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6d916d6db66d6ddab6b6ffdadaffb6b6b69191916d6d914848916d6d
+b66d6d916d6d6d48486d48486d24246d48486d24246d24246d4848b69191
+dab6b6ffb6b6dab6b6b691919148489148486d24246d48486d24246d4848
+6d24246d48486d48486d48486d48486d48486d48486d48486d4848914848
+914848914848914848914848916d6d914848914848914848916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d9148489148486d48486d48486d2424
+6d4848914848916d6db66d6db69191da9191da9191b66d6db66d6db66d6d
+da9191da9191dab6b6da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191da9191da9191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b69191b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848916d6d916d6d914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848916d6d914848914848914848914848914848b66d6db66d6d
+b69191b69191b69191916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+6d48486d48489148489148489148489148486d48486d48486d4848914848
+6d48489148486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffda
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6dab6b6b66d6d916d6db66d6d
+b69191b66d6d9148486d48486d48486d48486d48486d48486d2424916d6d
+dab6b6ffb6b6da9191da91919148486d48484824246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d24246d48486d4848
+914848914848914848916d6d9148489148486d4848914848914848916d6d
+916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148489148486d48486d4848
+6d4848916d6d916d6db66d6db69191da9191dab6b6b69191b69191b66d6d
+da9191da9191dab6b6da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191da9191b69191da9191da9191b69191
+b69191b69191da9191da9191da9191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+914848914848914848914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848b66d6db66d6d
+b69191b69191b69191916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48489148486d48486d48489148486d4848
+6d48486d48486d48486d48489148486d48486d48489148486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48489148486d48489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffffffffffffffda
+ffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191dab6b6da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db69191ffb6b6ffb6b6da9191916d6db66d6d
+b66d6db66d6d916d6d9148489148486d48486d48486d2424482424914848
+dab6b6ffb6b6dab6b6b69191916d6d6d24246d48486d24246d48486d2424
+6d48486d24246d48486d24246d48486d48486d48486d48486d48486d4848
+9148489148489148489148486d48486d48486d4848914848914848916d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148489148486d4848914848
+914848916d6d916d6db66d6dda9191dab6b6dab6b6da9191b66d6db66d6d
+b66d6dda9191da9191dab6b6da9191dab6b6da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191b69191
+b69191b69191b69191b69191da9191da9191b66d6db66d6db66d6db69191
+b69191b69191b69191b66d6db69191b66d6db69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848916d6d916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848916d6d916d6d
+b66d6db66d6d916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffffdaffffdaffffdaffffdaffffdaffdadaffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191
+b66d6db69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6ddab6b6ffb6b6ffb6b6b66d6d916d6d
+916d6db69191916d6d9148489148489148486d48486d24246d24246d4848
+da9191ffb6b6dab6b6b69191916d6d6d24246d48484824246d24246d2424
+6d48486d24246d48486d48486d48486d48486d24246d48486d48486d4848
+9148489148489148489148486d48486d24246d48486d4848914848916d6d
+b66d6d916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d
+914848916d6d916d6db66d6dda9191dab6b6dab6b6da9191b69191b66d6d
+916d6db69191da9191dab6b6da9191dab6b6da9191b69191b69191da9191
+b69191b69191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191b69191b69191b69191da9191b69191b69191b66d6db69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848
+914848914848914848916d6d914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d4848914848914848914848914848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48489148486d4848914848
+6d48486d48486d48489148486d48486d48489148489148489148486d4848
+9148486d48489148486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffffdaffdadaffffdaffffdaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6d916d6db69191dab6b6ffffdadab6b6b69191
+b69191b66d6db66d6d914848916d6d9148489148486d48484824246d2424
+da9191dab6b6ffb6b6da9191916d6d6d48486d24246d24246d24246d4848
+6d24246d48486d24246d48486d48486d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d48486d24246d48486d4848914848916d6d
+b66d6db66d6d9148489148486d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d914848914848916d6d916d6d
+916d6d916d6db66d6db66d6dda9191da9191dab6b6da9191b69191b66d6d
+b66d6db66d6dda9191da9191dab6b6da9191da9191b69191b69191b69191
+b69191da9191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191b69191b69191b69191da9191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d914848914848914848914848
+914848916d6d916d6d914848914848914848914848914848916d6d916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48489148486d4848
+6d48486d48486d48486d48486d48489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48486d24246d48486d48486d4848914848914848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffffdaffffdaffffdaffdadaffdadaffffdaffffdaffdadaffdada
+ffb6b6ffb6b6ffdadadab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6db66d6dda9191ffdadaffb6b6
+da9191b66d6db66d6d9148489148486d48486d48486d48486d2424482424
+916d6ddab6b6dab6b6dab6b6b691919148484824246d48484824246d4848
+6d24246d48486d24246d48486d48486d48486d24246d48486d24246d2424
+6d24246d48486d48486d48486d48486d24246d24246d4848914848916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191da9191dab6b6da9191b69191b66d6d
+b66d6db66d6dda9191da9191dab6b6da9191da9191da9191da9191b69191
+b69191da9191da9191da9191da9191da9191b69191da9191da9191da9191
+da9191da9191b69191b69191b69191da9191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db66d6db69191b69191b69191b69191b69191
+b69191b66d6d916d6d916d6d914848916d6d916d6d916d6d914848916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48486d48489148486d48486d48489148486d4848914848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48489148489148486d48486d48486d48486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffdadaffffda
+ffdadaffb6b6ffdadaffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191b69191da9191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db69191b69191
+b69191b66d6db66d6db66d6db69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6dffb6b6ffb6b6
+ffb6b6da9191b691919148489148486d48486d48486d48486d48486d2424
+916d6ddab6b6dab6b6dab6b6da9191916d6d6d48486d24246d48486d2424
+6d48486d48486d48486d48486d48486d24246d24246d24246d48486d2424
+6d48486d48486d24246d48486d24246d48486d24246d4848914848916d6d
+9148489148489148486d4848914848914848916d6d914848914848914848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db69191da9191dab6b6da9191b69191b69191
+b66d6db69191da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191da9191
+da9191da9191b69191b69191b66d6db69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48489148489148486d48489148486d48486d48486d48486d48486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffdadaffffdaffdadaffffdaffffdaffffdaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191b69191b69191da9191b69191da9191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db69191dab6b6
+ffdadaffdadada9191916d6d9148486d48486d48486d48486d48486d2424
+914848dab6b6ffb6b6dab6b6ffb6b6b66d6d6d48486d24246d48486d2424
+6d48486d24246d24246d24246d24246d24246d24246d24246d48486d2424
+6d48486d48486d24246d24246d24246d24246d24246d2424914848916d6d
+9148486d48486d4848914848914848914848916d6d9148486d4848914848
+6d48486d48486d4848914848916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db69191da9191da9191da9191b66d6db69191
+b66d6db69191da9191da9191da9191da9191b69191b69191b69191da9191
+b69191b69191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148486d48486d48489148489148489148486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48489148489148486d48489148486d48486d48486d4848914848
+9148489148489148486d48486d48489148489148489148486d4848914848
+9148486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffdadaffffdaffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdada
+ffdadaffdadaffdadaffffdaffffdaffffdaffdadaffdadaffdadaffb6b6
+ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+b69191dab6b6ffb6b6b69191916d6d9148486d24249148486d2424914848
+b69191da9191ffdadaffb6b6ffb6b6b66d6d9148486d48486d24246d4848
+6d48486d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+6d48489148486d4848914848914848916d6d9148489148489148486d4848
+9148486d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191da9191b69191b69191
+b69191b69191da9191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b69191
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d916d6d914848914848914848
+914848914848916d6d916d6d916d6d914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d4848914848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+ffb6b6ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffffda
+ffffdaffdadaffdadaffb6b6ffdadaffdadaffdadaffb6b6ffdadaffdada
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6db66d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6db66d6dda9191da9191b69191b66d6d916d6d916d6d916d6db66d6d
+b69191da9191ffb6b6ffb6b6da9191b691916d24246d48484824246d4848
+6d24246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d48486d4848914848916d6d9148489148489148489148486d4848
+6d48486d48486d4848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191b69191b66d6db66d6db66d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+914848916d6d914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48489148489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffb6b6ffffdaffffdaffffdaffdadaffdadaffdadaffb6b6ffdadaffffda
+ffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffb6b6ffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191da9191da9191
+b69191b69191b69191b69191da9191b69191b66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191916d6db66d6d
+916d6db66d6db66d6db69191b69191da9191b69191da9191b69191b66d6d
+916d6dda9191ffb6b6ffdadaffb6b6b691919148486d48486d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d9148486d48486d4848
+6d48486d48486d4848914848916d6d914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d916d6d
+b66d6db66d6db69191b69191b69191b69191b69191da9191b69191b66d6d
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848914848914848914848914848916d6d916d6d916d6d914848
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848916d6d914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6db66d6d916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d4848914848914848
+6d48486d48486d48486d48486d48489148486d48489148489148486d4848
+6d48486d48489148489148486d48486d48486d48486d48486d4848914848
+6d48486d48489148489148486d48486d48486d48486d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdada
+ffdadaffffdaffdadaffffdaffffdaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191
+da9191b69191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d916d6d
+916d6db69191dab6b6ffb6b6ffb6b6b69191916d6d4824246d24246d2424
+6d48486d24246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48486d4848914848916d6d916d6d916d6d9148489148486d48486d4848
+6d24246d48486d4848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db69191b69191b69191b69191b66d6d9148489148486d4848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48489148486d48489148486d48486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148489148486d48489148489148486d48489148489148486d4848914848
+6d48489148486d48486d48489148486d48486d48486d48486d48486d4848
+6d48489148489148486d48489148486d48486d48486d24246d48486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffdadaffdadaffffdaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db69191ffb6b6dab6b6ffdadab69191916d6d6d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d6d48486d48486d2424
+6d48486d48489148486d4848914848914848914848914848914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848916d6d
+914848914848916d6d914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848b66d6db69191da9191da9191da9191b69191916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148489148489148489148489148489148486d24246d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffdadaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffdada
+ffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+b66d6db69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6d916d6db66d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db66d6db66d6d916d6db66d6d916d6db66d6d916d6d916d6d
+914848916d6ddab6b6ffb6b6ffdadada91919148486d24244824246d4848
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d6d48486d24246d2424
+6d24246d24246d48486d48486d48486d48489148486d4848914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191da9191da9191da9191b69191
+b69191da9191da9191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db69191b69191b66d6db66d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6db66d6db69191b69191da9191b66d6d916d6d914848914848
+9148489148486d48486d48486d48486d48489148486d48486d48486d4848
+9148489148486d48486d48486d48489148486d48486d48486d48486d4848
+6d48486d48489148486d48489148486d48489148486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48489148486d4848
+6d48489148486d48486d48486d48486d48489148489148486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d24246d48486d4848
+dab6b6ffffdaffdadaffdadaffdadaffdadaffffdaffffdaffffdaffffda
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffdadaffdadaffdadaffdada
+ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6dda9191ffb6b6ffb6b6dab6b69148489148486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d9148486d24246d4848
+6d24246d24246d24246d48486d24246d4848914848914848916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191da9191b69191da9191dab6b6dab6b6da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6db66d6db69191b66d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48489148489148486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148489148486d48486d4848914848914848
+ffb6b6ffffdaffdadaffdadaffffdaffffdaffffffffffdaffffdaffffda
+ffffdaffffdaffffdaffffdaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191dab6b6da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d
+916d6d916d6db66d6dffb6b6ffdadaffb6b6b691916d48484824246d2424
+6d24244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d9148486d48486d24246d2424
+6d24246d24246d24246d24246d48486d48486d4848914848916d6d916d6d
+916d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848916d6db66d6d916d6d914848914848914848914848
+9148489148486d48489148489148486d48489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+6d48486d48489148486d48486d48486d48489148486d48486d4848914848
+6d48489148486d48489148489148489148486d48486d48486d48486d4848
+6d48486d48489148486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48489148486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffffdaffffdaffffdaffdadaffdadaffdadaffdadaffffdaffdadaffb6b6
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6ddab6b6ffb6b6ffb6b6da91916d2424914848482424
+9148484824246d48484824246d48486d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6db66d6d916d6d9148486d48486d48486d2424
+6d24246d24246d24246d24246d4848914848914848914848916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+dab6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffffdaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6db66d6d
+916d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6db69191dab6b6ffb6b6da91919148484824246d4848
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+916d6d916d6d916d6db66d6db66d6d916d6d6d48486d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d914848
+916d6d914848916d6d914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d48486d48489148486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48489148489148489148489148486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d4848914848914848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffda
+ffdadaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d
+916d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6dffb6b6ffb6b6dab6b6b66d6d6d2424914848
+4824246d48486d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+916d6d916d6d916d6db66d6db66d6d916d6d9148486d48486d24246d4848
+6d24246d24246d24246d48486d4848914848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d
+916d6d914848914848914848916d6d914848916d6d916d6d914848916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48489148489148489148486d48486d48486d48486d4848914848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d4848914848
+ffb6b6ffffdaffffdaffffdaffffdaffffdaffffdaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffb6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d916d6db66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d
+b66d6d916d6d916d6d916d6dda9191ffdadadab6b6b691916d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848916d6d
+916d6d916d6db66d6db66d6db66d6d9148489148486d24246d24246d4848
+6d24246d24246d24246d24246d24246d4848914848914848914848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b69191
+b69191b66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d4848914848
+6d48486d48486d48486d48486d48489148486d48489148486d48486d4848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+6d48486d48489148486d48486d48489148486d48486d48489148486d4848
+6d48486d48486d48489148486d48489148486d48489148486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffdadaffdadaffb6b6ffdadaffdadaffdadaffffdaffffdaffdada
+ffdadaffdadaffffdaffdadaffdadaffdadaffdadaffdadaffb6b6ffdada
+ffdadadab6b6dab6b6da9191dab6b6dab6b6dab6b6dab6b6da9191b69191
+da9191b69191da9191b69191da9191da9191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6db66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6dffb6b6ffb6b6b69191916d6d6d2424
+6d48484824246d48486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d24246d4848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db69191b69191b66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+914848914848916d6d914848914848914848916d6d914848914848914848
+916d6d914848916d6d916d6d914848914848916d6d916d6d914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48486d48486d4848914848914848914848
+6d48489148486d48486d48489148486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+ffb6b6ffdadaffdadaffdadaffffdaffdadaffdadaffffdaffffdaffdada
+ffffdaffffdaffffdaffdadaffdadaffdadaffb6b6ffdadaffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6d916d6d916d6d916d6db66d6ddab6b6ffb6b6dab6b6b66d6d482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d9148486d48486d48486d4848
+6d48486d24246d24246d48486d24246d4848914848914848916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+916d6d914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148489148489148486d4848914848
+9148489148486d48486d48489148489148489148489148489148486d4848
+9148489148489148486d48489148489148489148486d4848914848914848
+9148489148489148486d48486d48489148489148489148489148486d4848
+9148486d48489148486d48486d48489148486d48486d48486d48486d4848
+6d48486d48486d48489148486d48486d48486d48486d48486d4848914848
+ffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdadaffdada
+ffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191ffb6b6ffb6b6ffb6b6b66d6d6d2424
+6d24246d24246d24246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848916d6d916d6d916d6d9148489148486d48489148486d48486d4848
+6d48486d48486d48486d4848914848914848914848914848916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848914848914848914848914848
+916d6d916d6d914848914848916d6d914848914848916d6d916d6d914848
+914848914848914848916d6d914848916d6d914848916d6d916d6d916d6d
+914848916d6d914848916d6d916d6d916d6d916d6d914848914848916d6d
+914848914848916d6d914848914848914848914848914848914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48489148486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48489148486d4848b66d6d
+ffdadaffb6b6ffdadaffdadaffdadaffdadaffffdaffdadaffffdaffdada
+ffdadaffb6b6ffb6b6ffdadaffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db69191dab6b6dab6b6dab6b6dab6b6dab6b6b66d6d6d4848
+4824246d24244824246d48486d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+6d48486d48486d48486d4848914848914848914848914848916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6d914848916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848916d6d914848914848914848914848914848914848914848914848
+916d6d916d6d916d6d914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48489148486d48486d4848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d48489148489148486d48486d4848914848
+6d48486d48486d48486d48486d48486d48486d2424b66d6d6d4848b69191
+ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffb6b6ffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191da9191b69191b69191b66d6db66d6db69191b69191
+b66d6db66d6db69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191dab6b6ffb6b6ffb6b6dab6b6ffb6b6da9191b69191914848
+6d48486d24246d24246d24246d48486d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148486d48489148486d48489148489148486d48486d48486d48486d4848
+6d48486d4848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b69191da9191da9191da9191b69191da9191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848916d6d914848914848914848914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d914848916d6d916d6d914848916d6d914848916d6d
+916d6d916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+6d48486d48489148489148486d48486d48486d48489148486d48486d4848
+6d48489148486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848b66d6db69191
+ffb6b6ffb6b6ffdadaffb6b6ffdadaffdadaffb6b6ffdadaffb6b6ffdada
+ffb6b6ffdadaffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6
+ffb6b6dab6b6da9191dab6b6dab6b6da9191da9191da9191dab6b6da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6d
+b66d6db69191dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6da9191b66d6d
+9148486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d2424914848916d6d6d48486d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d4848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848916d6d
+914848916d6d914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848916d6d916d6d916d6d914848916d6d916d6d916d6d
+914848916d6d914848914848914848914848914848916d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+6d48489148489148486d48486d4848914848914848914848914848914848
+6d48486d48486d48486d48486d48486d4848914848914848914848914848
+6d48489148489148489148486d48486d48486d24246d2424916d6db69191
+ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffdadaffb6b6ffdadaffdadaffb6b6
+ffb6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6ffb6b6
+dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6dda9191dab6b6dab6b6ffb6b6dab6b6dab6b6b69191b69191
+916d6d9148486d24246d48486d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d48486d48489148486d24246d24246d2424
+6d24246d48486d48486d48486d24246d24246d24246d48486d48486d4848
+6d4848914848914848914848916d6d914848916d6d916d6d916d6d916d6d
+b66d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+914848916d6d916d6d916d6d914848916d6d916d6d914848916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d914848914848
+914848914848914848916d6d914848914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48489148489148489148489148489148489148486d48489148486d4848
+6d48486d48486d48486d48489148489148486d48486d48486d4848914848
+9148486d48486d48486d48486d48486d48489148489148486d4848914848
+6d48486d48486d48489148486d48486d48486d24246d4848914848b66d6d
+dab6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffdadaffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6dab6b6ffb6b6dab6b6da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d916d6d
+916d6d916d6db69191da9191dab6b6dab6b6da9191b69191b66d6db66d6d
+b66d6db66d6d9148486d48486d48486d24246d24246d24246d24246d4848
+6d24246d24246d24246d24246d24246d48486d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d914848916d6d914848916d6d914848914848914848914848916d6d
+914848914848914848914848914848916d6d914848914848914848916d6d
+916d6d916d6d916d6d914848916d6d914848914848916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d4848914848
+9148489148489148486d48489148489148489148489148486d48486d4848
+9148486d48489148489148486d48489148486d4848914848914848914848
+6d48489148489148489148489148489148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffb6b6ffb6b6
+ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b69191b69191b66d6d916d6d9148486d24246d4848916d6d914848b66d6d
+6d48486d48484824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191da9191b69191b66d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d914848916d6d916d6d916d6d916d6d914848916d6d
+914848914848914848916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d914848
+916d6d916d6d914848916d6d916d6d914848916d6d914848914848914848
+914848914848914848916d6d914848916d6d914848916d6d916d6d914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848916d6d914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148486d48486d4848914848
+6d48489148486d48489148486d48489148489148489148486d48486d4848
+6d48486d48486d48489148486d48486d48489148489148486d48486d4848
+6d48486d48486d48486d48489148486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffdadaffdadaffdadaffdadaffdadaffb6b6
+ffb6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6
+dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6db66d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d
+b69191dab6b6da9191b66d6d6d48486d48486d4848916d6d916d6db66d6d
+9148486d48486d24244824246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848916d6d916d6d916d6d916d6d914848914848
+916d6d916d6db66d6db66d6d916d6db66d6db66d6db66d6db69191b69191
+b69191b69191b66d6db69191b69191da9191da9191dab6b6dab6b6da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848914848914848914848914848916d6d914848
+914848914848914848914848916d6d914848914848916d6d914848914848
+914848916d6d914848914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848916d6d914848916d6d916d6d914848
+914848914848914848914848914848916d6d916d6d914848916d6d916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48489148486d4848b66d6d
+916d6d916d6d9148489148486d48486d48486d48486d48486d48486d4848
+dab6b6ffdadaffb6b6ffb6b6dab6b6ffb6b6ffdadaffb6b6ffdadaffb6b6
+dab6b6dab6b6ffb6b6ffdadaffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191da9191da9191b69191916d6d916d6d914848914848916d6db66d6d
+b69191916d6d6d48486d24246d24246d48489148486d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+6d48486d4848914848914848916d6d9148489148489148486d4848914848
+914848916d6d916d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b66d6dda9191da9191da9191dab6b6ffb6b6dab6b6
+da9191b69191b69191b69191da9191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+914848914848916d6d916d6d916d6d916d6d914848914848914848916d6d
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d914848916d6d914848914848
+914848914848914848916d6d916d6d916d6d916d6d914848916d6d914848
+916d6d914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+6d48486d4848914848914848914848914848914848916d6d916d6d914848
+9148489148486d48489148486d48486d48486d48486d48486d4848da9191
+da9191da9191b691919148486d48486d48486d48486d48489148486d4848
+da9191ffb6b6dab6b6dab6b6ffb6b6ffb6b6ffdadaffb6b6ffb6b6ffb6b6
+dab6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db66d6d916d6db66d6d916d6d916d6d
+916d6dda9191dab6b6b69191916d6d914848916d6d914848914848916d6d
+b66d6d916d6d9148489148486d48486d4848916d6d9148486d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d24246d4848
+6d48486d48489148489148489148489148486d48489148486d4848914848
+916d6d916d6d916d6d916d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191da9191dab6b6ffb6b6dab6b6
+dab6b6da9191da9191b69191da9191b69191b66d6db69191b66d6db69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b66d6db69191
+b66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d916d6d914848914848
+914848914848916d6d916d6d916d6d914848916d6d914848914848916d6d
+916d6d914848914848916d6d914848914848914848914848914848916d6d
+914848916d6d914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d916d6d914848914848
+6d48489148489148489148489148486d4848b69191dab6b6ffb6b6da9191
+b69191b691916d48486d48486d48486d48489148486d4848914848da9191
+da9191da9191da9191da91919148486d48486d48486d48486d4848914848
+da9191dab6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6b69191da9191da9191da9191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191da9191da9191da9191b69191b66d6db66d6d
+b66d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6dda9191dab6b6da9191916d6d6d48489148486d48486d48486d4848
+914848916d6db66d6d916d6d914848914848916d6d914848b66d6d916d6d
+9148486d48486d24246d48486d24246d48486d24246d48486d24246d4848
+6d48486d48486d48489148489148486d48486d48486d4848914848914848
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191da9191
+b66d6db66d6db66d6db66d6db66d6db69191b69191dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+916d6d914848914848916d6d914848914848916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848916d6db66d6d916d6d
+9148489148489148486d48486d4848b69191b69191da9191dab6b6da9191
+b69191da91916d48486d24246d48486d48489148486d4848916d6dda9191
+b69191dab6b6b69191da91916d24246d48486d48486d48489148486d4848
+dab6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+b69191b69191da9191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b69191dab6b6dab6b6dab6b6dab6b6da9191b69191916d6d
+916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d916d6d916d6d
+916d6db66d6ddab6b6dab6b6b691916d48484824246d48484824246d2424
+6d24246d4848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+9148486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d48486d24246d48486d4848914848
+914848916d6d916d6d916d6db66d6db66d6db69191da9191dab6b6da9191
+b69191b66d6db66d6db66d6db66d6db66d6db69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d
+914848916d6d914848914848916d6d914848914848914848916d6d916d6d
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848916d6d914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848b66d6db69191dab6b6da9191b69191
+b691919148489148486d4848914848b66d6dda9191b69191da9191dab6b6
+da9191b691919148486d48486d48486d4848914848b66d6db69191b66d6d
+da9191da9191da9191b691919148486d24246d48486d48489148486d4848
+dab6b6ffb6b6ffdadaffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6ffb6b6
+ffb6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191dab6b6
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191dab6b6dab6b6dab6b6dab6b6dab6b6da9191b69191b66d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6db66d6ddab6b6dab6b6b69191b66d6d4824246d48484824246d2424
+4824246d24246d4848914848916d6d916d6d916d6d916d6db66d6db66d6d
+916d6d6d48486d48486d24246d24246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d24246d24246d48486d4848914848
+914848916d6d916d6d916d6db66d6db66d6db69191b69191dab6b6da9191
+b69191b66d6db66d6d916d6db66d6db66d6db69191b69191da9191b69191
+b69191da9191b69191b69191b69191b69191b66d6db69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6db66d6d916d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848b69191da9191da9191dab6b6da9191
+dab6b6916d6d6d48486d24246d24246d4848b66d6db69191b69191b69191
+916d6d916d6d9148486d48486d48489148486d48486d4848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+da9191ffb6b6ffb6b6ffdadaffb6b6dab6b6dab6b6ffb6b6ffb6b6dab6b6
+dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191dab6b6
+dab6b6da9191da9191da9191da9191da9191da9191b69191da9191da9191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db69191b66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191da9191da9191dab6b6dab6b6dab6b6da9191b69191916d6d
+916d6db66d6d916d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6d
+916d6d916d6db69191dab6b6da9191b66d6d6d24246d24246d24246d2424
+6d24244824244824246d24246d24246d4848914848914848916d6db66d6d
+916d6d916d6d9148486d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6db66d6db69191b69191da9191dab6b6dab6b6
+da9191b69191b66d6db66d6db66d6db66d6db69191b69191b69191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848914848916d6d914848914848914848914848916d6d
+914848914848916d6d914848914848914848914848916d6d914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848b66d6dda9191da9191da9191da9191
+da9191916d6d6d24249148486d4848914848914848916d6d916d6d916d6d
+9148489148486d48489148489148489148489148489148486d48486d4848
+6d48486d48486d48486d48486d48489148489148486d48486d4848914848
+da9191ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6ffb6b6dab6b6
+dab6b6dab6b6da9191dab6b6da9191da9191da9191dab6b6dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191da9191da9191da9191b69191b66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191dab6b6dab6b6b691919148484824246d4848482424
+6d48484824246d24246d24246d24244824246d24246d4848914848916d6d
+b66d6d916d6d9148486d48486d48486d24246d48486d24246d24246d2424
+6d24246d24246d48486d24246d24246d48486d24246d24246d48486d4848
+916d6d916d6d916d6d916d6db66d6db66d6db69191b69191da9191da9191
+da9191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b69191b69191b66d6db66d6db69191b66d6db66d6db69191b66d6d
+b66d6db69191b66d6db69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d914848916d6d914848916d6d916d6d916d6d914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+914848916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848916d6d914848916d6d914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848b66d6db69191b69191da9191da9191
+da91919148486d48486d48489148489148486d48486d48486d48486d4848
+6d48486d48486d4848914848916d6d916d6d916d6d916d6d6d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d4848914848914848
+da9191ffdadaffdadaffb6b6ffb6b6ffb6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191da9191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6ddab6b6ffb6b6b691919148486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824246d24246d4848
+914848916d6d916d6d916d6d6d48486d48486d48486d48486d24246d2424
+6d24246d24246d48486d24246d24246d24246d24246d24246d24246d4848
+914848914848916d6d916d6db66d6db66d6db69191b69191da9191da9191
+b69191b69191b66d6db66d6db66d6db66d6db69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d914848916d6d916d6d914848916d6d916d6d
+916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848916d6d914848916d6d916d6d914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848b66d6d916d6d
+9148489148489148489148489148489148489148486d48489148486d4848
+6d4848916d6d6d4848b69191dab6b6ffb6b6dab6b6dab6b6b691916d4848
+6d48486d48486d48489148486d48489148489148486d48486d48486d4848
+da9191ffb6b6ffb6b6ffb6b6ffb6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6da9191da9191dab6b6
+da9191da9191b69191da9191da9191da9191da9191da9191da9191da9191
+da9191b69191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db69191dab6b6ffb6b6da91919148486d48484824246d4848
+4824246d24246d24246d48486d24246d24244824246d2424482424482424
+6d24246d48486d48489148489148489148489148486d24246d4848482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+914848914848916d6d916d6d916d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191b69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db69191b66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d914848916d6d914848914848914848914848916d6d914848
+916d6d916d6d916d6d914848914848916d6d914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48489148489148486d48489148486d48486d48486d4848914848
+914848b66d6dda9191da9191dab6b6dab6b6dab6b6dab6b6da91916d2424
+9148486d48486d48486d48489148486d48486d48486d4848914848914848
+da9191dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6dab6b6dab6b6dab6b6dab6b6ffb6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6d916d6db66d6dda9191dab6b6dab6b6916d6d6d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24244824246d2424
+4824244824244824246d24246d4848914848916d6d9148486d48486d4848
+6d24246d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d4848914848914848916d6d916d6db66d6db66d6db66d6db66d6db69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d914848
+916d6d916d6d916d6d914848916d6d914848914848916d6d914848914848
+914848916d6d914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+916d6db66d6db69191b69191da91919148489148486d4848914848914848
+9148489148486d48489148489148489148486d4848914848914848914848
+6d4848916d6dda9191b69191da9191da9191dab6b6da9191da9191916d6d
+914848916d6d9148486d48486d48486d48486d48486d48486d4848914848
+da9191dab6b6dab6b6ffb6b6dab6b6ffb6b6dab6b6dab6b6da9191da9191
+dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191b69191da9191da9191da9191da9191da9191da9191b69191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6dda9191dab6b6dab6b6b66d6d9148486d48486d2424
+6d24246d24246d24246d24246d24244824246d24246d24246d24246d2424
+6d24246d24246d24244824244824246d24246d48486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d48486d4848914848914848916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6db66d6db66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d914848914848914848916d6d914848
+914848916d6d916d6d914848916d6d916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848916d6d914848
+9148489148489148489148489148489148489148489148486d4848914848
+b69191da9191dab6b6dab6b6ffb6b6b66d6d9148489148486d24246d4848
+6d48486d48489148489148486d48486d4848914848914848914848914848
+916d6d916d6d916d6db69191b66d6db69191da9191da9191b66d6db66d6d
+914848916d6d6d48486d48486d48486d48486d48486d4848914848914848
+da9191dab6b6dab6b6ffb6b6ffb6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191da9191da9191da9191da9191da9191da9191b69191da9191
+da9191da9191dab6b6da9191da9191da9191b69191da9191da9191b69191
+da9191da9191da9191da9191da9191da9191b69191b69191da9191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+916d6d916d6db66d6db69191dab6b6dab6b6b691916d48486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24244824244824246d24246d24246d2424
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d916d6db66d6d916d6db69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b66d6db66d6db66d6d916d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d914848914848916d6d914848914848914848
+914848916d6d914848916d6d914848914848914848914848914848914848
+914848914848916d6d914848914848916d6d914848916d6d916d6d916d6d
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848b66d6d
+da9191da9191da9191dab6b6dab6b6b69191916d6d6d48486d4848914848
+6d48489148489148489148489148486d4848914848914848ffb6b6ffb6b6
+ffdadaffdadaffb6b6b66d6d916d6d916d6db69191916d6d916d6d916d6d
+916d6d916d6d9148486d48486d48486d48486d48486d48489148486d4848
+da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191b69191
+da9191da9191da9191da9191da9191b69191b69191da9191da9191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6b691919148486d24246d4848
+6d24246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d4848914848914848916d6d916d6d914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+9148489148486d48486d4848914848916d6d916d6db69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b66d6d
+b66d6db66d6db69191b66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848916d6d916d6d916d6d916d6d916d6d
+916d6d914848916d6d916d6d914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848916d6d914848914848914848
+9148489148489148489148489148489148489148489148486d4848916d6d
+da9191da9191da9191da9191da9191b69191916d6d6d48486d48486d4848
+9148489148489148489148489148486d48486d4848916d6dffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6916d6db66d6d914848914848916d6d916d6d916d6d
+9148489148486d48489148486d48486d48486d4848914848914848914848
+dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191da9191da9191
+da9191b69191da9191da9191b69191b69191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191b69191da9191da9191da9191da9191
+da9191da9191b69191da9191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6da9191916d6d6d24246d4848
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d914848914848914848916d6db66d6db69191b66d6d
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b66d6db66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d914848916d6d914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d914848914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d4848914848
+b66d6db69191b69191da9191da9191b66d6d916d6d916d6d916d6d914848
+9148489148489148489148486d48486d4848b69191b69191ffb6b6ffb6b6
+ffb6b6ffb6b6ffb6b6dab6b6b66d6d9148486d2424916d6d916d6d916d6d
+9148489148486d48486d48486d48489148489148486d4848914848914848
+ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191da9191da9191da9191da9191da9191b69191b69191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191da9191dab6b6da9191916d6d6d48486d2424
+6d24244824246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848914848916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db69191dab6b6dab6b6dab6b6da9191b66d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d914848914848916d6d914848914848916d6d916d6d916d6d914848
+914848914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+9148489148486d48489148489148486d2424916d6ddab6b6ffb6b6ffb6b6
+ffdadaffb6b6ffdadaffb6b6916d6d6d24249148486d48486d24246d4848
+6d48486d48486d48489148486d48486d48486d48489148486d4848914848
+dab6b6da9191da9191da9191da9191da9191dab6b6da9191dab6b6da9191
+da9191dab6b6dab6b6dab6b6dab6b6da9191dab6b6da9191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191da9191da9191b69191916d6d6d48486d2424
+6d48486d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d4848914848916d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191da9191da9191b69191b69191b66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191da9191b66d6d916d6d6d24246d24246d4848
+914848914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d916d6d
+916d6d914848914848916d6d916d6d916d6d916d6d914848916d6d916d6d
+914848916d6d916d6d916d6d916d6d916d6d914848916d6d916d6d914848
+916d6d914848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848916d6d
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d4848914848916d6d916d6d916d6d916d6d914848914848
+6d48486d48489148489148489148486d48486d4848da9191dab6b6da9191
+ffb6b6ffb6b6ffb6b6dab6b66d24246d48486d2424914848914848914848
+9148489148489148486d48486d48486d48486d48486d48486d4848914848
+dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6
+da9191da9191dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b691919148486d48486d4848
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d24246d48486d48486d48489148486d48489148486d4848
+6d24246d24246d48486d4848914848916d6db66d6db66d6d916d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db69191
+b69191da9191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6d9148486d4848482424240000480000240000480000
+2400004800004824244824246d4848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6db66d6db69191b69191b66d6d916d6d916d6d
+b66d6d916d6d916d6d914848914848914848914848916d6d916d6d916d6d
+914848916d6d914848916d6d914848916d6d914848916d6d916d6d916d6d
+916d6d916d6d916d6d914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848916d6d916d6d916d6d914848914848
+9148489148489148489148489148486d48486d48486d4848916d6d916d6d
+b69191b69191b66d6db691916d48489148489148489148489148486d4848
+9148486d48486d48486d48486d48486d48486d48486d4848914848914848
+dab6b6dab6b6da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191
+da9191da9191da9191dab6b6dab6b6da9191b69191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191b69191da9191b69191
+da9191b69191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6db66d6dda9191b69191916d6d6d24246d2424
+6d24246d48486d24246d24244824246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d48486d48486d48486d4848
+6d48489148486d4848914848914848916d6d916d6d914848914848914848
+914848914848914848914848916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b69191b66d6db69191
+b69191b69191da9191da9191da9191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b66d6d9148489148486d24246d2424482424482424482424
+4824244800004800004800004800004800004824244824246d4848914848
+914848914848914848916d6db66d6db69191b691919148486d24246d2424
+6d24246d24244824244824244800004824244824246d48486d48486d4848
+6d48486d48486d48486d4848914848914848914848916d6db66d6db66d6d
+b66d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d2424
+6d24246d24246d48486d48489148489148489148486d48489148486d4848
+6d48486d48486d48486d48486d4848914848914848914848914848914848
+dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d914848916d6db66d6dda9191b69191916d6d6d24246d2424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d48486d48486d4848914848
+914848914848914848914848914848916d6d916d6d916d6d916d6d914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191da9191da9191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6d9148489148484824246d24244800006d2424480000
+482424482424482424482424482424480000480000480000240000240000
+480000480000480000482424482424480000240000240000240000240000
+240000240000480000240000480000240000480000240000240000240000
+2400004800004800004800004824244824244824246d24246d48486d2424
+6d48486d24244824246d24246d4848916d6d9148486d24246d24246d2424
+6d4848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48486d4848914848914848914848914848914848
+9148489148489148489148486d48486d48486d48486d48486d48486d4848
+9148489148486d48489148486d48489148486d4848914848914848914848
+da9191dab6b6dab6b6da9191da9191dab6b6da9191b69191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191916d6d914848914848b69191b69191da91919148486d48486d2424
+6d48484824246d48486d24246d24244824246d24246d24246d24246d2424
+6d24246d48486d48486d4848914848914848914848914848916d6d916d6d
+914848916d6d914848914848914848916d6d916d6db66d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6db66d6db69191b69191b69191b69191b69191
+da9191da9191dab6b6dab6b6da9191da9191b69191b66d6db69191b69191
+b66d6db66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b691919148489148486d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+480000480000480000480000480000480000480000480000480000480000
+482424482424482424482424482424482424482424480000480000480000
+480000480000480000480000480000480000240000240000240000240000
+240000240000240000240000480000480000480000240000240000480000
+4824246d4848914848916d6d914848916d6d914848914848914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48489148489148486d4848914848914848914848914848914848914848
+6d4848914848914848914848914848914848914848914848914848914848
+9148486d48489148486d48486d48489148486d48486d4848914848914848
+9148486d48486d48486d48489148486d48486d4848914848914848914848
+9148486d48489148486d48489148486d48486d48486d4848914848914848
+da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b66d6d
+b69191b69191b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b66d6d6d48486d4848914848b66d6db69191b691919148486d48486d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d4848
+9148486d4848914848914848914848914848914848914848914848914848
+914848916d6d914848914848914848916d6d916d6d916d6d916d6d914848
+914848914848916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+916d6d914848914848914848916d6d916d6db69191b69191b69191b69191
+b69191b69191da9191da9191b69191b66d6db66d6db66d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d916d6d
+b66d6db66d6db66d6d9148489148486d24246d24246d2424480000482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424480000480000480000480000480000480000480000
+4800006d24246d24246d48489148486d48486d48486d48489148486d4848
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d4848914848914848914848914848914848914848
+9148486d48489148486d4848914848914848914848914848914848914848
+da9191dab6b6dab6b6dab6b6dab6b6da9191da9191dab6b6da9191da9191
+da9191da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+b69191b69191b69191da9191da9191da9191da9191da9191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+9148486d2424482424914848b66d6dda9191b69191916d6d4824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+914848914848914848914848914848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d916d6d916d6d
+914848916d6d916d6d916d6d916d6db66d6db69191b69191da9191b69191
+da9191da9191dab6b6da9191da9191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b66d6db66d6d9148486d48486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d48486d48486d48486d24246d48486d24246d24246d24246d2424
+6d24246d24246d24246d24246d48486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148486d4848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148486d48489148489148489148489148489148486d4848
+9148489148489148489148489148489148486d48489148489148486d4848
+9148489148486d48486d48486d48486d48489148486d4848914848914848
+da9191da9191dab6b6dab6b6da9191da9191da9191da9191dab6b6da9191
+da9191dab6b6dab6b6da9191da9191b69191da9191da9191da9191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191da9191916d6d
+6d24246d2424482424914848916d6dda9191b69191916d6d6d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848914848914848914848914848914848916d6d916d6d914848916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d916d6d
+914848914848914848916d6d914848916d6d916d6d916d6d914848914848
+6d48486d48486d4848914848914848916d6db66d6db69191b69191b69191
+b69191da9191da9191da9191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6d916d6d6d48486d24246d2424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d48486d4848
+9148486d48486d48486d24246d2424482424482424482424480000480000
+4800004800004800004800004800006d2424916d6db66d6d916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148486d48489148486d4848914848914848
+9148489148489148489148486d48489148489148486d48486d48486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+da9191da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191da9191da9191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191da9191b691916d4848
+4824244824244824246d24246d4848b66d6d916d6d9148486d2424482424
+6d24244824246d24244824246d24246d24246d48486d2424914848914848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+6d48486d48486d4848914848914848914848916d6db69191b69191b69191
+b69191da9191da9191da9191da9191b69191b66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b66d6db66d6db66d6d9148486d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d48486d48486d48486d4848
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824244824244800004800004800004800006d24246d24246d4848914848
+6d48489148489148489148489148489148486d48489148486d4848914848
+6d4848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+9148489148486d48489148489148489148486d48486d48486d4848914848
+9148489148489148489148489148486d48486d48486d4848914848914848
+6d48489148489148486d48486d48486d48486d48486d48486d4848914848
+da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191da9191b69191da9191da9191da9191da9191
+b69191b69191b69191b69191b69191b66d6db69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6dda9191dab6b6da9191b69191b66d6d9148486d24246d2424
+4824244824246d24244824244824246d24246d48486d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d
+b66d6d916d6d916d6d914848916d6d916d6d916d6d916d6d916d6d914848
+6d48486d24246d24246d4848914848914848914848916d6db66d6db69191
+b66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+916d6db66d6d916d6db66d6d9148486d24246d2424482424480000482424
+480000482424482424482424480000482424482424482424482424482424
+482424482424480000482424482424482424480000482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824246d48486d48486d48486d48486d2424482424
+4824244824246d2424482424482424480000482424482424482424482424
+482424482424482424480000480000480000480000480000480000480000
+4800004800006d4848914848914848916d6d914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48486d4848914848914848914848914848914848914848
+da9191da9191dab6b6da9191da9191da9191da9191dab6b6da9191da9191
+da9191dab6b6da9191da9191da9191da9191da9191da9191b69191b69191
+da9191da9191b69191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191ffb6b6ffb6b6b691916d24244800006d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d48486d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6d916d6db66d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d916d6d
+9148489148489148486d48486d48486d4848914848916d6db69191b69191
+da9191b69191da9191b69191da9191b69191b66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6db66d6d9148486d48484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424480000482424480000482424482424
+4824244824246d48486d48489148486d48484824246d4848482424482424
+482424482424482424482424480000482424482424482424482424480000
+482424482424482424482424482424480000480000480000480000480000
+4800004800004800002400004824246d24246d4848914848914848914848
+6d48486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+da9191dab6b6dab6b6dab6b6da9191da9191dab6b6dab6b6dab6b6da9191
+dab6b6dab6b6dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b69191
+b66d6db69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6dda9191da91919148486d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6d916d6db66d6d
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848
+9148489148489148486d48486d24246d4848914848916d6db69191b69191
+da9191da9191da9191b69191b69191b66d6db66d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b69191b66d6db66d6db66d6d
+b66d6db66d6d916d6db66d6d9148486d24246d4848482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24249148486d48486d48486d24246d2424482424482424482424
+482424480000482424480000482424480000482424480000482424482424
+480000482424482424482424480000480000482424480000480000480000
+4800004800004800004800004800004800004800004824246d4848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d4848914848914848914848914848914848914848914848914848914848
+da9191dab6b6dab6b6da9191da9191dab6b6dab6b6da9191da9191da9191
+dab6b6dab6b6da9191da9191dab6b6dab6b6da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6d916d6d9148489148486d48484824244824246d24246d2424
+6d24246d24246d24246d24244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+916d6d916d6d916d6d916d6d914848916d6d916d6db66d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6d916d6d916d6db66d6db66d6db66d6d916d6d916d6d916d6d916d6d
+9148489148489148486d48486d48486d4848914848916d6d916d6db69191
+da9191dab6b6da9191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6d916d6d6d48486d24244824246d2424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424480000482424
+4824244824244824244824244824244824244824246d24244824246d2424
+6d48489148486d48484824246d2424482424482424482424482424482424
+482424480000482424480000482424480000480000480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+482424480000482424480000482424480000480000480000480000480000
+6d24249148489148489148489148489148486d48486d48486d4848914848
+6d48489148486d48486d48489148489148489148489148489148486d4848
+9148486d48486d48486d4848914848914848914848914848914848914848
+9148489148489148489148486d48486d48489148486d48486d4848914848
+6d48486d48489148489148489148486d48486d48486d4848914848914848
+da9191ffb6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6da9191dab6b6dab6b6dab6b6da9191da9191da9191
+da9191b69191b69191b69191da9191b69191b69191da9191b69191b69191
+b69191b66d6db69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b66d6db69191b69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b66d6d916d6d9148486d48486d24246d24246d24246d24246d24246d2424
+4824246d24246d24244824246d24244824246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d916d6d914848
+9148486d48486d48486d48486d48486d48486d4848914848916d6d916d6d
+b69191da9191da9191b69191b66d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6db66d6db66d6d
+916d6d916d6d916d6d916d6d9148486d24246d4848482424482424482424
+480000482424482424482424482424482424482424482424480000482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+4824244824244824244824244824244824244824246d48486d2424914848
+6d48486d48486d2424482424482424482424482424482424482424482424
+480000482424480000482424482424482424482424482424482424480000
+482424482424482424480000482424482424480000482424480000480000
+482424482424480000482424482424480000480000480000480000480000
+4800004824246d24246d4848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d4848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d4848914848914848914848914848914848
+da9191dab6b6da9191da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6da9191da9191dab6b6da9191da9191da9191da9191
+da9191b69191da9191da9191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b66d6db69191b69191b66d6db69191
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d
+916d6d6d48486d24244824246d24244824246d24246d24246d2424482424
+4824244824244824244824246d24246d24246d24246d2424482424482424
+4824246d24246d24246d24246d24246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6d916d6d916d6d
+9148489148489148489148486d4848914848914848916d6d916d6d916d6d
+b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6d916d6d6d48486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d48486d48486d4848
+6d24244824246d2424482424482424482424482424482424480000480000
+482424480000482424482424482424482424482424482424482424480000
+480000480000480000480000482424482424482424482424482424482424
+482424480000480000480000482424482424480000480000482424482424
+4800004800004800004800006d48486d4848914848914848914848914848
+6d48489148489148489148489148489148489148489148489148486d4848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148486d48489148486d48486d48489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191916d6d
+6d48486d24244824246d24244824244824244824244824246d24246d2424
+6d24246d24246d24244824246d24246d24246d24246d24246d2424482424
+4824246d24244824246d24244824246d24246d24246d4848914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b66d6db66d6db66d6db66d6db66d6d916d6d916d6d914848
+9148489148486d48489148486d48486d4848914848914848914848914848
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6d916d6db66d6d9148486d24246d4848482424482424480000
+482424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d48486d48484824246d2424
+482424482424482424482424482424482424480000482424482424480000
+482424482424482424482424480000482424480000482424482424480000
+480000480000482424482424482424482424480000480000480000482424
+482424482424480000480000480000482424482424482424480000480000
+4800004800004800004800004824246d24246d2424914848914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48489148486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191da9191dab6b6dab6b6da9191dab6b6dab6b6
+da9191da9191da9191da9191da9191da9191dab6b6da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b66d6d6d2424
+6d24244824244824244824246d24244824244824244824246d24246d2424
+6d24246d24246d24244824244824246d24246d24246d24246d24246d2424
+6d24244824244824246d24246d24246d24246d24246d2424914848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b66d6d916d6d916d6d
+916d6d914848914848916d6d914848914848914848916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6d916d6d6d48486d24244824246d2424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824244824246d24246d48486d48486d48486d2424482424482424
+482424480000482424482424482424482424480000482424480000482424
+482424482424480000482424480000480000480000480000482424480000
+482424482424480000480000480000480000482424480000480000482424
+482424482424482424482424480000480000480000482424480000480000
+4800004824244824244824244800004800004800004824246d2424914848
+9148489148489148489148486d48489148486d4848914848914848914848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148489148489148489148489148489148486d48486d48486d48486d4848
+9148486d48486d48486d48489148486d48489148486d48486d4848914848
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6dab6b6da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6db69191b66d6db69191b66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b691919148486d4848482424
+4824244824244824246d24244824244824244824244824244824246d2424
+6d24244824246d24244824244824244824246d24246d24246d2424482424
+6d24246d24246d24246d24246d24244824246d24246d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191da9191da9191b69191b69191b66d6db66d6d916d6d
+9148486d48486d4848914848916d6d914848916d6d914848914848916d6d
+916d6d916d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191b69191b66d6db69191b66d6d
+b66d6db66d6d916d6db66d6d916d6d6d48486d4848482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424482424482424482424482424482424482424480000
+480000482424482424482424482424482424482424482424482424482424
+4824246d24246d24246d24246d48486d2424482424482424480000482424
+482424482424482424482424482424482424482424480000482424482424
+482424482424480000482424480000480000480000480000482424482424
+482424482424480000480000482424480000480000480000480000480000
+482424480000480000480000480000480000480000480000480000480000
+4800004824244800004824244824244800004824244800004824246d4848
+914848914848914848914848914848914848914848916d6d914848914848
+9148489148489148486d4848914848914848914848914848914848914848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148489148486d48486d48489148486d48486d48486d4848914848914848
+dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191da9191
+da9191dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b66d6db69191b69191b66d6db69191b66d6d
+b69191b66d6db66d6db69191b69191b66d6db69191b69191b66d6db69191
+b69191b69191b69191b66d6db66d6db69191b66d6db69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6d916d6db66d6db69191b69191b69191b691916d48484800006d2424
+4824246d2424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+4824244824246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191da9191b69191b69191b69191b69191b69191
+916d6d916d6d914848914848916d6d916d6db66d6d916d6db66d6d916d6d
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191da9191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d24244824246d2424
+482424482424482424482424480000482424482424482424482424482424
+482424482424482424482424480000482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d48486d2424482424480000482424480000482424
+480000482424482424480000482424482424480000480000482424480000
+480000480000480000482424480000480000480000480000482424482424
+480000482424480000482424480000480000482424482424480000480000
+482424482424482424480000482424480000480000482424480000480000
+480000482424480000482424480000482424480000480000480000480000
+6d24246d4848914848914848914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48489148489148486d48489148489148486d48486d4848914848
+6d48489148489148486d48486d48486d48486d48486d48486d48486d4848
+da9191da9191da9191da9191dab6b6dab6b6da9191da9191dab6b6dab6b6
+da9191da9191da9191da9191dab6b6dab6b6da9191da9191da9191b69191
+da9191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191da9191da91919148489148484824246d2424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d2424482424482424
+6d24246d24246d24244824246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191da9191da9191da9191b69191b66d6d
+b66d6d916d6d9148486d4848914848916d6d916d6db66d6d916d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d916d6d
+b66d6d916d6db66d6db66d6db66d6d916d6d9148486d2424482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424482424480000480000482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48486d48486d48486d24246d2424480000482424482424482424482424
+482424480000482424480000482424482424482424480000482424480000
+482424480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000482424482424482424480000480000
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000482424482424480000482424480000
+4824246d4848916d6d916d6d916d6d914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d4848914848914848914848914848914848914848914848
+6d48489148486d48486d4848914848914848914848914848914848914848
+b69191da9191dab6b6da9191dab6b6dab6b6dab6b6dab6b6dab6b6dab6b6
+dab6b6da9191dab6b6dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b66d6db66d6db69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db69191b69191b66d6db66d6db69191b69191b66d6d
+b66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6d916d6d916d6d6d48486d24244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d4848914848
+914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191b69191b69191
+b66d6d916d6d916d6d916d6d914848b66d6db66d6db69191b66d6db66d6d
+b69191b69191b69191b69191b69191da9191da9191b69191da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d24246d2424482424
+6d2424480000482424482424482424482424482424482424482424480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d4848
+6d48486d2424482424482424480000482424480000482424482424482424
+480000482424480000482424480000480000480000480000480000482424
+482424480000480000480000482424482424480000480000480000480000
+482424482424480000480000480000480000480000482424480000480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000480000482424480000482424480000482424482424480000
+4824244824244824246d4848914848914848914848914848914848914848
+9148489148489148489148489148489148486d4848914848914848914848
+9148489148489148489148489148489148489148489148486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48489148486d4848
+da9191da9191da9191da9191da9191dab6b6dab6b6da9191dab6b6dab6b6
+dab6b6da9191da9191dab6b6da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b69191b69191b66d6db69191b69191b69191b66d6db66d6db69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b691919148486d24246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824246d24246d24246d24244824246d24246d24246d24246d48486d4848
+914848916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b69191b66d6db69191b69191b69191b69191da9191da9191da9191da9191
+b69191b66d6d916d6d916d6d914848b66d6db66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191da9191da9191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b66d6db69191b69191b66d6d
+b66d6db66d6d916d6db66d6db66d6d916d6d6d4848482424482424482424
+482424480000482424482424482424482424482424482424480000480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d48486d48486d2424
+6d2424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424482424480000480000482424482424
+480000480000482424482424482424482424482424480000480000482424
+482424482424480000480000480000482424482424482424480000480000
+482424482424480000480000480000480000480000480000480000480000
+480000480000482424480000480000480000480000482424482424482424
+4824244800004800006d24246d2424914848914848916d6d914848914848
+916d6d914848914848914848914848914848914848914848914848914848
+9148489148489148486d48489148489148486d48486d48486d4848914848
+6d48489148489148489148489148486d4848914848914848914848914848
+da9191da9191da9191da9191da9191dab6b6da9191da9191da9191da9191
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191b69191b69191b69191b69191b69191
+b69191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+916d6d482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24244824246d24246d24246d24246d24246d4848
+914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6db66d6d
+b66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191b69191b66d6db66d6db66d6d916d6db66d6db69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191b69191b69191b69191b69191
+b66d6db69191b66d6db69191b69191916d6d6d48486d24244800006d2424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424480000482424480000
+4824244824244824244824244824244824246d48486d48486d4848482424
+482424480000482424480000482424482424480000482424480000482424
+480000480000482424480000482424482424482424480000480000482424
+480000482424480000480000480000480000482424482424480000482424
+480000480000480000480000480000480000480000480000480000480000
+480000480000482424482424480000480000482424480000480000480000
+482424482424482424480000480000480000482424480000480000480000
+4824244800004824244824246d48486d4848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d48489148489148486d4848
+9148489148486d48486d48486d48486d48486d48486d48486d4848914848
+dab6b6da9191da9191da9191da9191da9191da9191da9191da9191da9191
+dab6b6da9191da9191da9191dab6b6da9191da9191da9191da9191b69191
+da9191b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191916d6d
+6d48486d24244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+4824244824244824246d24246d24246d24246d24246d24246d48486d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b69191b69191b69191b69191b69191da9191da9191da9191da9191da9191
+b69191b69191b66d6db66d6db66d6d916d6db66d6db66d6db66d6db69191
+b69191b66d6db69191b69191b69191b69191b69191b69191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6d916d6d6d48486d2424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824246d24246d48486d24246d48486d24246d2424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424480000
+480000482424482424480000482424482424482424482424482424482424
+480000482424480000480000480000482424480000482424480000480000
+480000480000482424482424482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000482424
+4824244800004824244824244800006d24246d4848914848914848914848
+916d6d916d6d916d6d916d6db66d6d914848914848914848914848914848
+9148489148489148486d48486d4848914848914848914848914848914848
+9148486d48486d48489148486d48486d48489148486d4848914848914848
+da9191da9191da9191da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191dab6b6dab6b6da9191da9191da9191da9191da9191
+b69191b69191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191b66d6d9148486d2424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824246d24246d24246d24246d2424
+6d4848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191b66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b69191b69191b69191b69191b69191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191b69191b66d6d9148486d24246d24246d2424480000
+6d2424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+4824244824246d24246d24246d48486d48486d2424482424482424480000
+482424480000482424482424482424482424482424480000482424482424
+482424482424480000480000482424482424482424480000480000482424
+480000482424480000480000480000482424482424482424482424480000
+482424482424482424480000480000482424480000482424480000480000
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000480000480000480000480000482424
+4800004800004824244824244800004800004824246d4848914848914848
+914848916d6d916d6d916d6d914848916d6d914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d48489148486d4848914848916d6d
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6d
+b66d6db69191b69191b69191da9191b66d6d6d48486d4848482424482424
+4824244824244824246d24244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24244824246d24246d4848
+6d4848914848914848916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+916d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6db66d6db69191b69191b69191da9191da9191dab6b6dab6b6dab6b6
+da9191da9191b69191b69191b69191b66d6db66d6db69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191da9191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b66d6db66d6d9148484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824246d48486d48486d48486d24246d2424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424480000482424480000480000482424482424482424482424480000
+482424482424480000480000480000482424480000480000480000480000
+480000480000482424482424480000482424480000482424482424480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424480000480000482424480000482424
+4824244800004824244824244824244800004800004824246d4848914848
+916d6d916d6d916d6d916d6d916d6d914848914848914848914848914848
+9148489148489148489148486d48489148489148489148489148486d4848
+9148486d48489148486d48486d48489148489148489148486d4848916d6d
+da9191dab6b6da9191da9191da9191dab6b6da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191b66d6d
+b69191b69191b69191916d6d6d48486d2424482424480000482424482424
+4824244824244824244824244824244824244824246d24244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d4848914848914848916d6d916d6d916d6d916d6db66d6d916d6d
+916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191b69191da9191dab6b6dab6b6da9191
+dab6b6da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191da9191da9191b69191b69191da9191da9191da9191da9191
+da9191da9191b69191b69191da9191b69191da9191b69191b69191b69191
+b66d6db69191b69191b69191b66d6d9148486d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d48486d24246d2424480000482424482424482424
+480000482424480000482424482424482424482424482424482424482424
+482424480000480000482424480000480000482424482424482424482424
+480000480000480000480000480000482424480000482424480000482424
+482424480000480000480000482424480000480000482424482424482424
+480000480000480000480000480000480000480000480000482424480000
+480000480000480000480000480000480000480000480000480000482424
+4800004824244824244824244824244824244824244824244824246d2424
+9148486d4848916d6d916d6d914848914848914848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+9148489148489148489148486d48486d4848914848914848914848916d6d
+da9191dab6b6da9191da9191da9191da9191da9191da9191dab6b6da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db69191b66d6db69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db69191b66d6db69191b66d6d
+b66d6db66d6db69191b69191b66d6db69191b69191b69191b66d6db69191
+b69191b69191b66d6d6d24246d24244824244824244824246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d48486d4848914848914848916d6d916d6d916d6db66d6d916d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b69191b69191b69191b69191da9191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b66d6db66d6db69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+da9191da9191da9191b69191da9191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6d9148486d24244824246d2424480000
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48489148486d48486d2424482424480000482424480000482424480000
+482424480000482424482424482424482424482424482424482424480000
+482424482424482424480000482424482424482424482424482424480000
+482424480000482424482424480000482424480000482424480000480000
+480000482424480000480000480000480000480000482424482424480000
+480000480000482424480000480000480000480000480000480000480000
+480000480000480000480000480000482424482424480000482424480000
+480000482424482424482424482424482424482424482424480000480000
+6d24246d4848914848914848914848914848914848916d6d914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d4848914848914848916d6d
+da9191dab6b6da9191da9191dab6b6dab6b6dab6b6dab6b6da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+da9191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b66d6db69191b69191b66d6db69191b66d6db66d6db66d6db69191
+da9191da9191b69191916d6d6d24246d24244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824246d24246d24246d24246d4848914848916d6d916d6d916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6d916d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191b69191b69191b69191b69191da9191
+b69191da9191b69191b69191b69191b69191b69191b69191b69191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db69191b69191916d6d9148486d4848482424482424482424482424
+482424480000482424482424482424480000482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424914848
+9148486d48486d2424482424480000482424480000482424480000482424
+480000482424482424482424482424482424480000482424480000482424
+482424482424482424482424482424480000482424480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+480000480000480000482424480000480000480000482424480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000482424482424482424482424482424482424482424482424
+482424914848914848914848916d6d916d6d916d6d914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48486d48486d48489148486d4848914848914848914848914848
+da9191da9191dab6b6da9191dab6b6da9191da9191da9191da9191dab6b6
+da9191da9191da9191da9191dab6b6da9191da9191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b66d6db66d6db69191b66d6db66d6db69191b69191
+b69191b66d6db69191b69191b66d6d916d6d6d48486d24246d24246d2424
+6d24246d48486d48486d24244824244824244824246d2424482424482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d24246d4848914848914848914848916d6d
+b66d6db66d6db66d6db66d6d916d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b69191b69191b69191da9191da9191dab6b6
+dab6b6dab6b6da9191da9191da9191da9191b69191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191da9191b69191da9191
+da9191da9191da9191da9191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db691919148489148486d4848482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d4848914848
+6d24246d2424480000482424482424482424482424482424480000482424
+480000482424482424482424482424482424482424482424482424480000
+482424482424482424482424480000480000482424482424482424482424
+482424482424482424480000482424480000480000480000482424482424
+480000480000482424482424482424480000480000480000480000480000
+482424482424482424480000482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424480000482424482424482424482424
+4824246d48486d48486d4848914848914848916d6d914848914848914848
+6d48489148489148489148489148489148489148489148486d4848914848
+6d48489148486d48489148486d4848914848914848914848914848914848
+da9191da9191da9191da9191da9191da9191da9191dab6b6da9191dab6b6
+da9191da9191dab6b6dab6b6da9191da9191da9191b69191b69191b69191
+b69191b69191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b69191b66d6db66d6d
+b69191b69191b66d6d9148486d2424482424482424482424482424482424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24244824246d24246d24246d48486d4848914848
+916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d916d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191da9191
+da9191da9191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191b69191b69191da9191b69191
+b69191b69191da9191da9191b69191b69191b69191b69191b66d6db69191
+b66d6db66d6db66d6d6d48486d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d48486d48489148486d2424
+6d2424482424482424480000482424480000482424482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+480000480000480000482424480000480000482424480000482424480000
+480000480000482424480000482424480000480000480000480000480000
+482424480000480000480000482424480000480000480000482424480000
+480000480000480000480000482424480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424482424482424482424482424482424
+4824244824246d24244824246d4848914848916d6d914848914848914848
+914848914848914848914848916d6d914848914848914848914848914848
+6d48489148489148489148489148486d48486d48486d48486d4848914848
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+da9191da9191da9191da9191b69191da9191da9191da9191b69191da9191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6d
+b69191b66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6db69191
+b66d6db69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db69191b69191b69191b66d6db66d6db66d6db66d6db69191
+b66d6d916d6d6d48486d24246d24244824244824246d24246d24246d2424
+4824244824244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824244824246d24244824246d24246d24246d24246d48486d4848
+6d4848914848916d6d916d6db66d6db66d6d916d6d916d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db69191b69191b69191da9191da9191da9191
+da9191da9191dab6b6dab6b6da9191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191da9191b69191da9191da9191da9191
+da9191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6db66d6d916d6d6d48486d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d48486d48486d48486d24246d2424
+482424482424480000482424480000482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424480000480000480000482424482424
+480000480000482424480000482424480000480000480000480000482424
+480000480000480000480000480000482424482424482424480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000480000482424480000482424480000480000
+482424480000480000480000480000480000480000482424482424482424
+482424482424482424482424914848914848914848914848916d6d914848
+914848914848914848914848914848914848914848914848914848914848
+9148486d48489148489148486d48486d48486d4848914848914848914848
+da9191dab6b6da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191b69191b69191b69191da9191b69191b69191
+da9191b69191b69191b69191b69191b69191b66d6db69191b66d6db66d6d
+b69191b66d6db69191b66d6db69191b66d6db69191b66d6db66d6db69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+b69191b69191b69191b69191b69191b66d6db66d6db69191b66d6db66d6d
+916d6d9148486d24246d24244824246d24244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d2424
+6d24246d48486d4848914848916d6d916d6d916d6d916d6d916d6d916d6d
+916d6d916d6d916d6db66d6db66d6db66d6db66d6db69191b69191da9191
+da9191da9191da9191da9191dab6b6da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191da9191da9191
+da9191da9191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+916d6d916d6d9148486d48486d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d24246d48489148486d4848482424482424480000
+6d2424480000482424480000482424482424482424480000482424482424
+482424482424482424482424480000482424482424480000482424480000
+482424480000480000482424482424480000482424482424480000480000
+482424480000480000480000480000480000480000480000480000480000
+480000480000482424480000482424480000480000480000480000480000
+480000480000480000482424482424480000480000480000480000480000
+480000480000480000480000480000480000480000482424480000480000
+480000482424482424482424480000482424482424482424482424482424
+4824244824244824244824246d4848914848914848914848914848914848
+914848914848914848914848914848914848914848914848914848914848
+9148489148489148486d48486d48489148489148486d4848914848916d6d
+da9191dab6b6da9191dab6b6da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db69191b69191b66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b66d6db66d6db69191b69191b66d6db66d6d916d6d
+6d48486d24246d48484824246d24244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d2424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d4848914848914848916d6d916d6d916d6d916d6d
+916d6db66d6d916d6d916d6db66d6db66d6db66d6db69191da9191da9191
+da9191da9191dab6b6da9191da9191da9191da9191da9191b69191b69191
+b69191da9191b69191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db69191
+914848916d6d9148486d48486d24246d2424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824246d24246d48486d48486d2424482424482424482424482424
+482424482424480000482424482424480000480000482424482424482424
+482424482424482424480000480000482424482424482424482424482424
+482424480000482424482424482424482424480000482424482424480000
+480000482424480000482424480000482424480000482424482424482424
+480000480000480000482424480000480000480000480000480000480000
+480000480000480000480000482424480000480000482424480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000482424482424480000482424480000480000482424480000482424
+4824244824244824244824244824246d2424914848916d6d914848914848
+914848916d6d916d6d916d6d9148489148489148489148489148486d4848
+9148486d48486d48486d48489148486d4848914848914848914848914848
+b69191da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191da9191da9191b69191b69191b69191b69191da9191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b69191b66d6db66d6db66d6db69191b66d6db69191b66d6db69191b66d6d
+b69191b66d6db66d6db69191b69191b69191b69191b66d6db69191b69191
+b69191b69191b69191b66d6db69191da9191da9191b69191b66d6d914848
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d24246d48486d4848914848914848916d6d916d6d
+916d6d916d6d916d6d916d6d916d6db66d6db66d6db66d6db69191b69191
+b69191da9191da9191b69191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6d916d6d
+9148486d48486d48486d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d48486d48486d4848482424482424480000482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424480000480000480000
+482424482424480000480000482424482424480000482424480000482424
+482424482424480000480000482424480000480000482424480000480000
+480000482424480000482424480000480000480000480000480000482424
+482424482424480000480000480000482424482424480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000482424482424480000480000480000482424
+482424482424482424482424482424482424914848914848916d6d914848
+916d6d916d6db66d6db66d6db66d6d914848914848914848914848914848
+9148489148489148489148486d48489148486d48489148486d4848914848
+da9191da9191da9191da9191da9191da9191da9191dab6b6da9191da9191
+da9191da9191da9191b69191b69191b69191b69191b69191da9191b69191
+b69191b69191b69191b69191b66d6db66d6db66d6db66d6db69191b69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db69191b69191b69191da9191b69191916d6d6d24246d2424
+480000482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d2424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d48486d48486d4848914848
+914848916d6d916d6d916d6d916d6d916d6db66d6db69191b66d6db69191
+da9191da9191da9191da9191dab6b6da9191da9191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191da9191b69191b69191b69191
+b69191b66d6db66d6db69191da9191da9191da9191b69191916d6d916d6d
+9148489148486d24246d2424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d48486d48486d24244824246d2424480000482424482424482424482424
+482424482424482424480000480000482424482424482424482424482424
+480000482424480000482424482424482424482424480000480000482424
+482424482424482424482424482424482424480000482424480000480000
+482424480000480000482424480000482424482424480000482424482424
+480000480000480000480000482424480000482424480000480000482424
+480000480000482424480000480000480000480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000482424480000482424480000482424482424482424
+4824244824244824244824244824244824246d4848914848916d6d916d6d
+914848b66d6d916d6db66d6d916d6d916d6d9148489148489148486d4848
+9148486d48489148486d48486d48486d48486d48486d48489148486d4848
+da9191da9191da9191da9191da9191da9191da9191da9191da9191da9191
+b69191b69191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191
+b69191b69191b66d6db66d6db66d6db66d6db66d6db69191b66d6db66d6d
+b66d6db66d6db66d6d9148486d48486d24246d2424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d24246d48486d48486d4848
+914848914848914848916d6d916d6d916d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191da9191da9191da9191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db69191b69191da9191b69191b66d6d914848914848
+9148486d24246d24246d2424482424482424482424482424482424482424
+4824244824244824244824244824244800004824246d24246d24246d4848
+6d24246d2424482424482424480000482424480000482424482424482424
+480000482424482424482424482424482424480000482424480000480000
+480000482424482424482424482424482424480000482424482424480000
+480000480000482424480000482424482424482424480000480000480000
+482424480000482424482424480000482424482424480000482424482424
+480000480000480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000480000480000482424480000
+482424480000480000482424480000482424482424482424482424482424
+4824244824244824244824244824244824246d4848914848914848916d6d
+916d6d914848b66d6db66d6db66d6d916d6d9148486d4848914848914848
+9148486d48489148489148489148489148486d48486d4848914848914848
+b69191da9191dab6b6da9191da9191da9191b69191da9191b69191b69191
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d914848
+6d48486d24246d2424482424482424480000482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d24246d48486d4848
+6d48486d48486d4848914848916d6d916d6d916d6db66d6db66d6d916d6d
+b69191b69191da9191da9191da9191da9191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db66d6db66d6db69191da9191b69191916d6d9148486d4848
+6d48486d24246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d48486d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+482424480000482424480000482424480000480000482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+480000482424482424482424482424480000482424480000480000482424
+482424480000482424480000480000482424482424480000480000480000
+480000482424480000482424480000482424482424480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000480000480000480000480000480000480000480000480000480000
+480000482424480000480000480000480000482424482424482424482424
+4800004824244824244824244824246d24246d24246d48486d2424914848
+914848914848916d6d914848916d6d916d6d9148486d4848914848914848
+9148486d48486d48486d48486d48486d48486d48486d48486d4848914848
+b69191da9191da9191da9191da9191da9191b69191da9191b69191b69191
+da9191da9191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b66d6db66d6db69191
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6d6d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d24244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d48486d48486d4848914848916d6d916d6d916d6db66d6d
+b66d6db66d6db69191da9191da9191b69191da9191b69191b69191b69191
+b66d6db69191b69191b66d6db69191b69191b66d6db69191b69191b69191
+b66d6db66d6d916d6db66d6db66d6db66d6d916d6d9148486d4848914848
+6d48486d2424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d48486d24246d2424482424
+482424482424482424482424482424482424482424482424482424480000
+482424480000482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424480000480000480000
+482424482424482424480000480000482424480000480000480000480000
+480000480000482424480000480000480000482424480000482424482424
+480000480000480000480000480000480000480000482424482424482424
+480000480000482424482424480000480000480000482424480000480000
+480000480000480000480000480000482424480000480000482424482424
+4800004824244824244824244824244824246d24246d24246d2424914848
+916d6d916d6d916d6d914848914848916d6d914848916d6d914848914848
+9148489148489148486d4848914848914848914848914848916d6d914848
+b69191da9191da9191da9191da9191da9191b69191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db69191b69191916d6d6d2424482424
+482424482424480000482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24246d24246d24246d48486d48486d4848914848914848916d6d916d6d
+b66d6db66d6db69191b69191da9191da9191da9191b69191b69191b66d6d
+b69191b69191b69191da9191b69191b66d6db66d6db66d6db69191b66d6d
+b66d6db66d6d916d6db66d6db69191916d6d6d48486d48489148486d4848
+6d48486d2424482424482424482424482424480000482424482424482424
+4824244824244824246d24246d48486d48486d2424482424482424482424
+482424482424482424482424482424482424482424482424480000480000
+480000482424480000482424482424482424482424482424482424482424
+480000482424480000482424482424482424480000482424482424482424
+480000480000480000482424482424482424480000482424480000482424
+480000482424482424480000480000482424480000480000480000482424
+480000482424480000482424480000480000482424480000482424480000
+482424480000480000480000482424480000480000480000480000480000
+480000480000480000480000480000482424480000480000480000480000
+480000480000480000482424480000482424482424482424482424482424
+4824244824244800004824244824244824244824246d24246d2424914848
+916d6d9148489148489148489148489148489148489148486d4848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+b69191b69191b69191b69191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db69191b66d6db66d6db66d6db66d6db66d6db66d6db66d6db66d6d
+b69191b69191b69191b69191da9191da9191da9191b66d6d6d4848482424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+4824246d24246d24246d24246d24246d48486d48486d4848914848914848
+916d6db66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6db66d6db69191b69191b69191b66d6db69191b69191b66d6db66d6d
+b66d6db66d6d916d6d9148489148486d24249148486d48489148486d4848
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d24246d48486d4848482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424480000480000
+482424482424482424482424482424482424480000482424482424482424
+482424482424480000482424482424482424480000482424482424482424
+482424480000482424480000480000482424482424482424482424480000
+480000482424482424482424482424480000482424480000482424480000
+480000482424480000482424482424480000480000482424482424482424
+482424480000480000482424480000480000480000480000480000480000
+480000480000480000482424482424482424482424482424482424482424
+4800004824244824244824244824244824244824246d24246d2424914848
+916d6d916d6d914848914848914848916d6d916d6d914848914848914848
+916d6d9148486d48486d48486d48486d48489148486d48489148486d4848
+da9191da9191da9191b69191da9191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db69191b69191b69191b69191b66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6d9148489148486d48486d48486d48486d48486d48486d24246d2424
+4824244824244824244824244824246d2424482424482424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d24244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d24246d24244824244824244824246d24246d24246d2424
+6d24246d24244824244824246d24246d24246d24246d48486d48486d4848
+914848914848916d6db66d6db69191b69191b69191b69191b69191b66d6d
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6d9148486d48486d24246d48489148489148486d48486d2424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24246d48486d2424482424480000480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424480000480000482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424480000480000482424482424482424482424480000
+482424482424480000482424482424480000480000482424482424480000
+482424482424482424482424482424482424480000482424482424480000
+480000480000480000480000480000482424480000482424482424480000
+480000480000480000482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824246d24244824246d2424
+6d4848916d6d914848914848914848914848914848914848914848914848
+9148489148486d48486d48486d48486d48486d4848914848914848914848
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b69191b66d6db66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6db69191916d6d6d4848
+6d2424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d4848916d6d6d48484800004824244800004824246d4848914848
+9148486d48484824244824244824244824246d24246d24246d48486d4848
+6d48486d48486d4848914848916d6db66d6db69191b69191b69191b69191
+b69191b69191b66d6db66d6db69191b66d6db69191b69191da9191916d6d
+916d6d6d48486d24244824246d48486d48489148486d48486d2424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d2424482424482424482424482424482424482424482424
+482424482424482424480000480000482424482424482424482424480000
+482424482424480000482424480000482424480000482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+482424482424482424482424482424482424482424482424482424480000
+482424482424482424480000482424482424482424482424480000482424
+482424482424482424480000482424482424482424482424482424482424
+4824244824246d2424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424480000
+480000482424480000482424482424482424482424480000482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d4848916d6d916d6d916d6d916d6d916d6d916d6d916d6d914848914848
+9148489148489148489148486d48486d48489148489148486d4848916d6d
+b69191da9191da9191b69191da9191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b66d6db69191b69191b66d6db66d6db66d6db66d6d
+b66d6db66d6db66d6db69191b66d6db66d6d9148486d48486d2424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+482424916d6d916d6d6d48484824244800006d24246d4848b66d6db66d6d
+b66d6d916d6d6d48484824244824244800004824244824246d24246d2424
+6d48486d48486d48486d48486d4848914848914848b66d6db66d6db69191
+b66d6db69191b69191b66d6db66d6db69191da9191da9191da9191914848
+6d24246d24244824246d24246d48489148486d24246d4848482424482424
+4824244824244824244824244824244824246d24244824246d24246d2424
+6d2424482424482424480000482424482424480000482424480000482424
+480000482424482424480000482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424480000482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d24246d2424
+6d24246d24244824244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424480000482424482424
+4824244824244824244824244824244824244824244824246d2424482424
+6d2424916d6d916d6d914848916d6d914848914848914848914848914848
+9148489148486d48486d48489148489148486d4848914848914848914848
+b69191da9191b69191da9191b69191b69191b69191da9191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b66d6db66d6db66d6db66d6db69191b69191b66d6db66d6d
+b66d6db69191b69191916d6d9148486d24246d24246d24246d2424482424
+4824244824246d2424482424482424482424482424482424482424482424
+4824244824244824246d24244824244824246d2424482424482424482424
+4824244824244824244824244824244824246d2424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000482424916d6db66d6d916d6d
+b66d6db66d6d9148484824244824244824244824246d24246d24246d2424
+6d24246d24246d24246d48486d48486d48486d4848914848914848916d6d
+916d6db66d6db66d6db66d6db66d6db69191da9191b66d6d916d6d6d2424
+4824246d24244824246d24249148489148486d48486d48486d2424482424
+4824244824244824244824244824244824246d48486d24246d24246d2424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424480000482424
+482424482424482424480000482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48489148489148486d48486d48486d48486d48486d48486d2424
+6d48486d48486d48486d24246d4848482424482424482424482424482424
+4824244824244824244800004824244824244824244824246d2424482424
+6d24249148489148489148489148489148489148486d4848914848914848
+9148489148489148489148489148486d48486d48486d48486d4848914848
+b69191da9191da9191da9191b69191da9191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b69191b66d6db66d6db66d6db66d6db66d6db66d6db69191b66d6db69191
+b69191b66d6d9148486d48484824244824246d24246d48484824246d2424
+4824246d24244824244824244824244824244824244824244824246d2424
+4824246d24246d24246d24246d24246d24246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+4824244824246d24246d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824246d2424482424482424482424482424482424482424482424482424
+482424482424482424482424482424480000480000916d6db69191da9191
+da9191dab6b69148489148484800006d48484824246d48486d48486d4848
+6d24246d24244824244824246d24246d48486d48486d48486d48486d4848
+6d4848914848914848914848914848916d6d916d6d6d48486d2424482424
+6d24244824246d24246d24249148486d48486d48486d24246d2424482424
+6d24244824244824244824244824244824246d24246d48486d2424482424
+482424482424482424482424482424480000482424482424482424482424
+480000482424482424480000482424482424482424480000482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d48486d24246d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d2424482424
+4824244824244800004824244824244824244824244824244824246d4848
+6d4848914848914848914848914848914848914848914848914848914848
+6d48489148489148486d48486d48486d48486d48486d4848914848914848
+b69191da9191da9191da9191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b69191b69191
+b66d6db66d6db69191b66d6db66d6db66d6db66d6db69191b69191b69191
+b66d6d6d48486d24244824246d24244824246d24246d24246d2424482424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d48486d24246d24246d24246d2424
+4824244824244824244824246d24244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24244824246d24246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+4824244824244824244824244824244824244800006d2424916d6db66d6d
+da9191b691916d48484824244824244824246d48486d24246d24246d2424
+6d48486d24244824244824244824246d24246d48486d4848914848914848
+916d6d9148489148489148486d48486d48486d48486d48486d24246d4848
+6d48486d48486d24246d24249148486d24246d24246d2424482424482424
+4824244824244824244824246d24246d48484824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424480000482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48486d48486d48486d48489148489148486d48486d48486d4848914848
+6d48486d48489148489148489148486d48489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48484824246d24244824244824244824244824244824244824246d2424
+914848914848914848916d6d914848914848914848914848914848914848
+9148486d48486d48489148489148486d4848914848914848914848914848
+b69191da9191da9191b69191b69191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b66d6db66d6db66d6db66d6db66d6db66d6db66d6d916d6db66d6db66d6d
+b66d6d6d48486d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24244824246d24246d24246d2424
+6d24246d24244824244824244824244824246d2424482424482424482424
+6d24246d24246d24244824244824244824246d24246d24246d2424482424
+4824246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d24246d24246d24246d24244824246d2424482424482424482424
+6d24244824244824244824244824244824246d24246d4848914848916d6d
+916d6d6d48484824244824244824246d24246d24246d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d24246d48486d48486d4848
+9148486d48486d24244824246d24244824246d24244824246d2424482424
+4824246d24246d24246d48486d48486d4848482424482424482424482424
+482424482424482424482424480000482424482424482424482424482424
+482424482424480000482424480000482424482424482424482424482424
+4824244824244824244824244824244824244824246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d48486d4848
+6d48489148486d48489148489148486d48489148489148489148486d4848
+6d48486d48486d48486d48489148489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d2424482424482424482424482424482424
+6d4848914848914848914848914848914848916d6d914848914848914848
+6d48486d48489148486d48486d48486d4848914848914848914848914848
+b69191b69191da9191da9191b69191b69191da9191b69191b69191b69191
+b69191b69191b69191b69191b66d6db66d6db69191b69191b69191b66d6d
+b66d6db66d6db66d6db69191b66d6d916d6d916d6d914848914848914848
+6d48486d48486d24246d24246d24246d24246d24246d24246d24246d2424
+4824246d24244824244824246d24244824244824246d2424482424482424
+4824246d24246d24246d24246d24246d24246d24244824246d24246d2424
+482424482424482424482424482424482424482424482424482424482424
+4824246d24244824244824244824244824244824246d24246d2424482424
+4824244824244824244824244824244824246d24244824246d2424482424
+6d24246d24244824246d24246d24246d24246d24244824246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d24246d24246d24246d24246d24246d24246d48486d48486d4848
+6d48489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d48486d48486d24246d24246d24246d24246d2424
+4824244824244824244824244824244824246d24246d2424da9191b69191
+b691919148484824244824244800006d48486d48486d48486d48486d4848
+6d24244824244824246d24246d24246d24246d24246d48486d48486d4848
+9148489148486d48486d24246d24244824244824244824246d24246d2424
+6d24246d48486d48486d24246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d4848
+6d24246d48486d48486d48486d48486d4848914848914848914848914848
+9148489148489148489148486d48486d48489148489148486d4848914848
+9148486d48486d48486d48489148486d48486d48489148486d4848914848
+9148489148489148486d48486d48486d48486d48486d48489148486d4848
+6d48489148489148489148486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d2424482424482424482424482424
+6d2424914848914848914848914848916d6d914848914848916d6d914848
+9148486d48489148486d48489148486d48486d48486d48486d4848914848
+b69191da9191da9191b69191da9191da9191da9191da9191da9191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6db66d6d
+b69191b66d6db66d6d916d6d9148486d48486d48489148486d48486d4848
+6d24246d48486d24246d48486d24246d24246d24246d2424482424482424
+6d24246d24244824244824244824244824244824246d2424482424482424
+4824244824244824246d24246d2424482424482424482424482424482424
+4824244824244824246d2424482424482424482424482424482424482424
+4824244824244824246d24244824244824244824246d24246d2424482424
+4824244824244824244824244824244824244824246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824246d24246d2424482424482424482424
+4824244824244824244824244824246d24246d24246d24246d24246d2424
+6d48486d48486d48486d48486d48486d4848914848914848914848914848
+9148489148489148489148489148489148486d48489148489148486d4848
+6d48489148486d48486d48486d48486d24246d24246d4848914848914848
+6d48486d24246d24244824244824246d24246d4848916d6d916d6d914848
+914848482424482424482424482424482424482424482424482424482424
+6d24246d24246d24244824244824244824246d24246d24246d4848914848
+9148486d48486d24246d2424482424482424482424482424482424482424
+482424482424482424482424482424480000482424482424482424482424
+4824244824244824244824244824244824244824244824246d24246d2424
+6d24246d24246d24246d48486d48486d48489148489148489148486d4848
+9148489148486d48486d48486d48489148486d48489148486d48486d4848
+6d48489148489148489148489148489148486d48486d4848914848914848
+6d48489148489148489148486d48489148489148486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d24246d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d24246d2424482424
+4824246d48486d4848914848914848914848914848914848914848914848
+9148489148489148486d48486d48486d48486d48489148486d4848914848
+b69191da9191b69191b69191da9191da9191b69191b69191b69191b69191
+b69191b66d6db69191b69191b69191b69191b69191b69191b69191b66d6d
+b66d6d916d6d916d6d916d6d9148486d48486d48486d48486d4848914848
+6d48486d24246d24246d2424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24244824244824244824246d2424
+4824244824244824244824246d2424482424482424482424482424482424
+4824244824244824244824246d24244824246d24246d2424482424482424
+4824244824244824244824244824246d2424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824246d24246d24246d24246d24246d24246d24246d24246d2424
+6d24246d48486d48486d48486d48486d48486d4848914848914848914848
+9148489148489148489148489148489148489148489148489148486d4848
+6d48486d48486d48486d24244824244824246d2424b66d6dda9191916d6d
+916d6d4824246d2424482424482424482424482424482424482424482424
+4824244824244824244824244824244824246d48486d4848914848914848
+6d48486d24246d24244824246d2424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24246d24246d24246d24246d48486d4848
+6d48486d48489148486d48486d48486d48489148489148486d4848914848
+9148489148486d48489148489148486d48489148489148486d4848914848
+6d48489148489148489148489148486d48486d4848914848914848914848
+6d48489148486d48486d48489148489148489148489148486d4848914848
+6d48486d48486d48486d48486d48489148489148489148486d48486d4848
+6d48489148486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+9148486d48486d48489148486d48486d48486d48486d24246d24246d2424
+4824246d24246d24246d4848914848914848914848914848914848914848
+9148489148489148489148486d48489148489148489148486d4848914848
+b69191da9191da9191b69191da9191b69191b69191b69191b69191b69191
+b69191b69191b69191b69191b69191b69191b69191b69191b66d6d916d6d
+916d6d916d6d916d6d9148486d48486d48486d48486d24246d24246d2424
+6d24246d24244824244824244824246d24244824246d2424482424482424
+4824246d24246d24244824246d24246d24246d24246d2424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+6d2424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+482424482424482424482424482424482424482424482424482424482424
+4824244824244824244824246d24244824244824244824246d2424482424
+4824246d24244824246d24246d24246d24246d24246d24246d24246d2424
+6d48486d24246d24246d48486d48486d4848914848914848914848914848
+9148489148489148489148489148486d48486d48486d48489148486d4848
+6d48486d24246d24246d24246d24246d24246d24246d2424482424482424
+4824246d24246d24246d24244824244824246d48486d48486d48486d2424
+6d2424482424482424482424482424482424482424482424482424482424
+4824244824244824244824244824244824244824244824244824246d2424
+6d24246d24246d24246d24246d48489148486d48486d48486d48486d4848
+6d48489148486d48486d48489148486d4848914848914848914848914848
+9148486d48489148489148489148489148489148489148489148486d4848
+9148489148489148489148489148489148486d48486d4848914848914848
+9148486d48489148489148489148486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d24246d48486d24246d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48486d48486d48486d48486d4848
+6d48486d48486d48486d48486d48486d48489148486d48486d48486d4848
+6d24246d24246d24246d4848914848914848914848914848914848914848
+9148489148486d48489148486d48489148486d48486d4848914848914848
+grestore
+showpage
+%%Trailer
+
+--owatagusiam
+Content-type: image/gif
+Content-Transfer-Encoding: quoted-printable
+Content-Description: Quoted-Printable test
+
+GIF87a@=01=C8=00=D7=00=00=00=00=00=00=00=AA=00=AA=00=00=AA=AA=AA=00=00=AA=
+=00=AA=AAU=00=AA=AA=AAUUUUU=FFU=FFUU=FF=FF=FFUU=FFU=FF=FF=FFU=FF=FF=FF=00=
+=00=00=14=14=14 ,,,888EEEQQQaaaqqq=82=82=82=92=92=92=A2=A2=A2=B6=B6=B6=CB=
+=CB=CB=E3=E3=E3=FF=FF=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=FF=FF=00=BE=
+=FF=00}=FF=00A=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=BE=FF=00}=FF=00A=FF=
+=00=00=FF=00=00=FFA=00=FF}=00=FF=BE=00=FF=FF=00=BE=FF=00}=FF=00A=FF}}=FF=9E=
+}=FF=BE}=FF=DF}=FF=FF}=FF=FF}=DF=FF}=BE=FF}=9E=FF}}=FF=9E}=FF=BE}=FF=DF}=FF=
+=FF}=DF=FF}=BE=FF}=9E=FF}}=FF}}=FF=9E}=FF=BE}=FF=DF}=FF=FF}=DF=FF}=BE=FF}=
+=9E=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=
+=C7=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=C7=FF=B6=
+=B6=FF=B6=B6=FF=C7=B6=FF=DB=B6=FF=EB=B6=FF=FF=B6=EB=FF=B6=DB=FF=B6=C7=FF=00=
+=00q=1C=00q8=00qU=00qq=00qq=00Uq=008q=00=1Cq=00=00q=1C=00q8=00qU=00qq=00Uq=
+=008q=00=1Cq=00=00q=00=00q=1C=00q8=00qU=00qq=00Uq=008q=00=1Cq88qE8qU8qa8qq8=
+qq8aq8Uq8Eq88qE8qU8qa8qq8aq8Uq8Eq88q88qE8qU8qa8qq8aq8Uq8EqQQqYQqaQqiQqqQqqQ=
+iqQaqQYqQQqYQqaQqiQqqQiqQaqQYqQQqQQqYQqaQqiQqqQiqQaqQYq=00=00A=10=00A =00A0=
+=00AA=00AA=000A=00 A=00=10A=00=00A=10=00A =00A0=00AA=000A=00 A=00=10A=00=00=
+A=00=00A=10=00A =00A0=00AA=000A=00 A=00=10A A( A0 A8 AA AA 8A 0A (A A( A0=
+ A8 AA 8A 0A (A A A( A0 A8 AA 8A 0A (A,,A0,A4,A<,AA,AA,<A,4A,0A,,A0,A4,A<=
+,AA,<A,4A,0A,,A,,A0,A4,A<,AA,<A,4A,0A=00M=B2=00Q=AE=00Q=AA=00U=AA=00Y=A6=00=
+Y=A2=00]=A2=00a=9E,=00=00=00=00@=01=C8=00=00=08=FC=00G=08=1CH=B0=A0=C1=83=
+=04I(\H=C2=04C=12%J<ThB=E2D=85=11O=B4HR#=A2=C7=8F C=864A=F2D=8C(0`=14=A1=81=
+dE=0B=175h=E0=B0Q=03=07=8D+/r=C2=88=C1=02=85=CF=14'=82=06=F5=89"=86=0D=1D2V=
+=10=95=11#E=0A=1C.l=C8=98A=A3=06=16!7=92=DCh=A2$=06=8C=9C-h=C8=B8=E1S=A8Y=
+=14f=85=A2H!=83=A8=D3=15U=8D=DC=F0=DA=94h=DA=B4h=EF=E2=D5=CB=B7/=D1=BFle=EC=
+=F4=A1dG=8E=AF9_=CC=F8=91CG=8D=C74hP=B5=01c=07=90=15*T8MQ=E3F=8B=150G=94=10=
+A:D=08=11+L=AB=0E=01=02=A1=EB=D7=09/Z\X=02=C6=C4=12C*^=84=18=11=85=90=13$E=
+=0A=1FN=D2=04=8C((=98=F2=C0=0C#f=D5=1A6=A2=DF=98=81=18=86=0C"0\=E4=C5K=E3=
+=85=8B=9C=FB2R=AC=AD=D1=85=C5=0C=AF=93u=F0H=E2=C3=07=12"7=82x=B5.C=F3=DF=BB=
+=E2=EF=A2P=E1"=85=8D=14,=BC=10C=0FC =91=03S5=ECPW_3=F4=95=16P=0EFx=C2_?=F9=
+=97X=0B3=9C=87XN9=C4=F0=82J6=00aD=0D04=D1=02=0B2=FC=C0=02f=99=B1=15C=17/=8D=
+@=9A=08=AB=D5h=1A=08=AD=C1=A6cA=139=84=D1l=10=B5=00dDC=A4=E0=11C=1F=99=F0=
+=02=0B=DFU4\=92=C4=157=D4O+=1C8=83=13K=001=C5=0D\=E2=F0=D8W.=CC=F0BYS=0E=95=
+B=0B-=D8PE=0C+=BC=D0F=0C8=A5$C=15=86=ED D=11G=14=01D=0E8=F0=C0=E7N2=B0 =9E]=
+g=C1=B0=9DZ(=90(C=0B=87=C9=80=C3=0FG=EC0=C6=0B=F6=11z=16=0D=87:=98=9F=84~Q=
+=A8B=0D1=00=A1C=0C4|=CA=14=0E2=B8=F0=9Dw=FBD=FC=E0=02=0F6=A0=19=D3=0A=B4f=
+=A6=C2=0A2Dq=82=88A=B8`B=0D=A5=D9=B8=1A=8E;=16;=82l=16=95 =A4G'=E40=D2w=C4=
+=95=A0=A4=10K=00=17=91=93O=A2 RqRN=D8=C3=0A=F2=C90=C5=0E<=B4=E7=83=0E8=DC =
+=D8N4=B40(Z>=05=E1Sf/(=B1=C6=126=A0H=C3=86;=F1=A9C=12KH=C1=85=12A=E8=90=03=
+=0FL=A5TDJ/=BC=FB=17=0C?=909=E5Z+=FC=80CN3=04=11=DD=BE=9Ai=E6=F0_cr:=14=0F=
+=99=8A=8C=A8[*=EC=D4=E2~.=B5=FB=9D=AA=AA=C2=80=E6=87,=D4\=EB=AD+=0AZB=CD=99=
+=AD #=8D=C2=AAF=AC=B1=B0=3D=E4=91=92=D7=DA`=82=14%=8CY=C3=09=1E=E1P=02=0A/I=
+-=DC=09=3D=B4 F=CA=13B=0D=E5H-=04w-I)=94tB=0A<=A4@]=0C=06=F30=C4=13FlQ=05=
+=9FY=FB=14=E1UKN=01=16=83=0F-=F4=10C=0C7=0CqC=0Di=D6=94=93=11E=08a=C3=0D9=
+=FC=F0D=12Z=0CQ=84=C1b=8C=1A=C3=0C+=CC=90=B7Oc=12EC=12=1F=13=A5=02=19^%=06=
+=C3=0C8=C8=DA=82=0A7|=EC=12=0C=9Br=8AB=11=12=9B|r=0A*=E0=00=03=0E=95=8A=0E=
+=AB=0D=17=A2yb=CD+=D2=0AZ=0A.=D0=C0=C2=0D=0E=B5=B9B=093=A8=F0s=D07=E6H=F4A=
+=0B=C5=D0PD2/=D9=C2=09.=A0=C9=BC=AA-=BC=B0=03I=8A=B9=80=14=0A=15=C9pto0=E4=
+=FB=92=11:`=1B=D1=B2=CF=02=C7-I(=D8=B0=14=18)=B1=98=0E=82=90=848x=81`9=08=
+=82XJ=17=9EA=8D)=05+=E8=C1=0E\=90=12>-=0Cf=14=D4I=10=D2=85=83%=FC=00=0AJ=F0=
+=C1=0Et=B0=83=190=E5=07=A5=DA=DCO|B=83=14=A0K=05=14=FA=C9=0C=14=C40=8C=A9=
+=8A=09=FB< U=0D=9Cb=AB=16@G)=B6[K[J&2=0A=A5@,=1F{B=11R4=84=94=CC=80Q=AF=0AP=
+=0BR=C7$&=99=E47$=18=08DF=E0=B3=D5=91=00h=C2=1A=DA=F5x=A4=10=17=94=00x=FDYL=
+=9BzP=03=D0=D0=E0=06*=A8=D9K\=10=83f=A9=EF=06R=01=9F=D7L=80=03=FE=A5@=093=
+=B8=C2w=F4w4=AF=8D=E4=04,=08JIJ=82<6=F1=E0C9=E8L=E3=CC@=06'=0C=C1=09=06=A3=
+=82=1Bb =03=1Exl-;=C8=CC=0C=80=A0=AE=0Fy=05+7=80=99N8=A9=B8=C0=09=A1=07<=E0=
+=C1=0E=A0=D0=03=13=C6*U=1E=E3=E1=A0\=08=86=17=C8 =08=83=8AA=85L=A8=83&x=07=
+=83=95I=C9=DFT =98=1E=BC=003=B5=93=90Oz0(=DB=9D=E5'*=08Q=EDP=90=03=EF=E4=E0=
+'=3D=C0=81=13\=A0=02#=B8`=0Al=11=82O=94=B5=02=16d=FCqF2=FA=99=08~=06=83=11=
+=D8H=8Cc<=16EV=D7=03=FEP=ED=08.@=91=10=02=AA=02=1E=F4@y,h=CE=0D=0C%=83|=C9Q=
+]=DCb=0B=0Arr=83%=04=E1=06U=D0_qre=BF=A3=99`Q.=08J=0E=D4"=03=1F=C4`U=BB=BB=
+=01=0E=8E0=05,X=81=09#,!S=0A=A4=92=CD=A8=E0=05I=D8I=0D=3D=94=18U=FA=12=07D=
+=00=C2=E2l=B0=84=AC=CC=84=07b=91A=1B[dS=17H=C1cLJ=89=A0=9C=92J=9BV=C5=A7=AB=
+=FA=10=0C=94=E0=18=01Ie=88D=EC=D4=104=17M=93=FDE0=99RJ=0A=CE=90=17=14=A8=8A=
+$< gD=DA=B8=83=1F=B1=A0=05=F2=9C=11=18=A9=C7=1A=10=CCs=8C=181J=0EX =91=88=
+=F0=A6=04f4N=0B=8A=D0=02"=9C=88=99MXB=11=DA=D8=CE=18=B4=C03=DE=01=0E=13f =
+=85=F05=87=075=F0A[M=90=1C=16=FB=88-I=FC=F3=C1A=81=90=D0=B393'=DF=81A=0Ev`=
+=03=1C=E0=A0=07=B0=8Ce=12vP=83=A9<=92=05=1E=C3=8C=B8=E6=93=18=D8b=D0=94r=E1=
+=13=1E=A1=D3=19)H=86*=C9=E3=9B=AD|=90=99=1B,=C7%=A8=C3J=A90=13=03O~=06gs=FC=
+[q_=80=84=1B=FC@=08B=80A=16=1C=16D=14=80&be-=E2O=80=00!=A1=00=A5=7F}$=ADF*2=
+!"`=E4{<Z=81^=F7=CA=D7=BE=92=86he4=E3n.B=D8=DA=14=A5=05=06=A5-=0F=94@=AB=16=
+=E8=00=09:=F8=8AuN=D0=A6=F0=BD$=077=00=AE=85u=C0=B9=1A=A8=80[=85,I=AA^"=A0=
+=15=A4=A0t=AA=BA=C1=12g=B28=1F=E4=80=0A=EC=A9=81=09=C7=F2][1AT=7F=9B=0Fb0=
+=A8=AA=17=E4=E0=09)iN$i=E2=1C=1A=F4=800=C5=B3=15=AEj=00=04=1A=04=C1|=80c=82=
+=11b=05=FB=17=198=81=084=E0A=0B~=DC=83:=D8 =06=1DTfN=8Cr=B9@=C1=EE>f]=0B=10=
+`=F8.k"=EA=C5}1-=B7=B4=F5=11=86=BC` =03=9EQ=81k=E4=D7=03=EB=88=045P=88k=16=
+=9C=ACD=CE=C0=04=1A=19s=CEXP=95"T=14=06=F2=E2=81=11z=85=A6@=DDU=09=DEy=C1=
+=10p;=04=B4=ECOJ)=09=B1=0D=1Ac=E2!=E4=80=C8=98=06-s=7F=E0=9C=0C%=90*R^Aoe=
+=C0=84=83=1D=C6=B8=C7=3D=E6*=CBU=DB=1EH!=09Sp=C1=12Nd<=E5=D1=C0=0A1x=02=0F=
+=B0=13m=DB=E6=C0=87k=A2=F6=DF=A6R=15&D=C7=07@=C0A=E9`=80=84=180=95Bq=A6=1A=
+=0D=EA=02g;=9B)-=A7=EE(C=0C=12=E8`=0Dz5z=CD=E7kvS=02=A8=F17=C4'=8A=1E=AE|`=
+=85"tHspy=C1=F0=88w=D7=98=9D=CBP=1A=8D=F7=84=FB83K=1E=CC=A0f=1E.=F2=EE=0C=
+=0A=99=C8H&=094=D8=C1gjUY$d=88=07QHW=0C=9C=00=84c=C2=CC=06=C7=DD]=11=C2=A9=
+=03#=13A=E16k'=0Bt=80=14=A6=FC-%=A02=02O=17=9AdN"a=D5=B1=D4=13=97n=B0=EE$=
+=CB@=06)=8CaX=F12=AF=C3=98=18=86=E8v=F7]=E2-=EF=85=D0=BB=DE=F7=AE=D1=80=F5=
+=BDo=1FQ=846=C6=81O=0Bb=00*=19=E4=C0=09=3D=90=8A'W=80=03=86=13Ox-=08=83=0B=
+=80=08=1CE=9A=0D=D2=13=97m=13=1E=E5a4=C1=ECQ=82=B3=81=0F=AA=12=97=1DL=81p=
+=C63=9E=0B=8C@=95=1B=E8=C0=06=04=8BA=10\Ed=0C=02np=DD=0C5hi@=84=BB=EBX2=0Bt=
+=BA2=C3`:=1C=90p=869=B0=83=0F=8A=B0=F9#X=FB=06;PfRT=98=F5N=B9E=C7X=8F=A1=D6=
+=E1=BD?=19=14=E7=06G=9B=FC=F7=0C=04Ro=02=DF{=ECd_4=BF?*=CC=1D=00%=05/=F0Bt=
+=A82rZ=DDu=82=C2+|=9Av=B2=96=17D\=98=13*=CA=87=FC=B4=04_=86=DF=05@=E8=C1=0F=
+^0=AA=92j=DA=06IxA=0Dr=D0.=E3=BDd=07G=00=06=91=01=1D=81=E3r=9D=A7*q=F5!z0=
+=03=E4=F3=12=0B=F7P[=10rC`=04M@=1DI=B6!Z=C5.=1B=94=03K=F06N=90=04"=92.~=93=
+=12:0=04=E5=F3I=C2=C7=17=14b=03\=B0(.&uSg;=DC=B2=02%=E1+=FD&H=18aF=05q`z=15=
+v=AA=11h=D1'}=B7=E1=02N=00=05=D2=02in=C5'=00h=03X=D0}=C4=E3=02=05=F3=12d=80=
+L=91a~=1A=15=14=CE=C4&:=F0=1D=C0=D3Y=85=A7*=DDTd=C9=D3=03cp=03/=D0.}=93s=15=
+vRA0=03C`=034=00=06=87q=80=07=B88=FCP=81w=0B=D7=02VP0=8F=E1=03A=10I=C45^N'=
+=03H=80=03C=A0=03Z=F2=04U=90=04hP'>G8=BDu=03=FD=14:=96=92~1tD^=D1=03=82=02=
+=83=C3=A7=17!u6L=10R(=A0=03=CCc=02=89=94E=C7r,=D2=13hAco?=08}A=88=3D=0F=D1Q=
+=19=01=03O`x=C3c<5=A3p-0=169Q=03=00=B5=85/=00=05=F5=91)(=A0<=F8=A2J0=83=87=
+=EA=93q.=80G7=E0=03=1E=C6p=BA(GQ=81=04=DD=11K=D9=B1=8C=EFGd=D0=E1=1Dxh=8DG=
+=B0\vX=88=A5S\r=C2=03=81=83=05*=15=04@=00)D =04<=A0=03>@=8C=DB=06\=FF=C1{0=
+=08=836=C0]K`r=94=18_Z=87=02K =1EH0=07=82=04=13w=F5=02b $$=10=16=FA=97W=CDG=
+=1A*=E0|=A7=01=8B=B1h=10Hr=1BG=98=13=FCrtq=C6=03=04N =19L=86=15<PA=DEaP=B1#=
+=14b"=03-Gdx8Gx=07=13@0=03X=10>v=97=93=E5C[0=93=12=0D=18=93=9DW=13=A9=F4=93=
+=E1=F7=02?=F0=86=E1=D6!=E7h:=08C*=06=A3R=15e.=B2=14=7F&t9?=80=19=9B=B1=8F=
+=FC=882'iY=B7=12=901=18!+=A0=16* T> =03=E0=88=93ibn.`|=99=81=8A3R=91z=B50=
+=7Ff=91=17=99o=19=89=3D@b4=D22!=ED=B4p=91=C7=02(=E4q=8F=B18L0=7F=18=93 =DB=
+=D1=86o=F4!=06(~=E1=87w[=E8=026=01s=0F=C8p}=B3=04<=95qw=18=933y=80=9E=F9=12=
+F=C1=03=AE=E77=E3=A5=13=D5![=1Ch=877=80=04;p.:=F6t=ED"=03Y=E0b=FA=B8B=FB=A8=
+=1Dkq&=14t:N=80=03)=F0=03=A13|04%=FC2PT5=D0=80v=D7N=86=A8"xE=10' =1070=02a=
+=90^=AE(v=18=99=97=B1=C1o=FD=96&9=14~+=12G!G=155=00+9=10=1D=A2=C201=B0=055P=
+=16B=F4=04=0BU\'=05=99=A193Q=D1+K@=999yW=9EI=9F=A1=C9=8D=A1) 0=A0=05:aR5p=
+=9A=82=C8I=9Da=03=1A=E3%U=96!h=F0j,=B2=19=FA=F8=041=D4=13n=91=03=99=C1F=E1D=
+*Q=C7=89=EE=E6^g=A5L_1=9F9=A7/=93=A3=3D$=80=02#=A0=02$p=03=0A=9194=F0N|=05=
+=84=DA=A9=91=DC=C9=145=D3=99/=10=04=F5q+&=94=03=E7bP?=A0=06\=F2=03=B9=07=03=
+P=D0=1F=BE=B1=16B4=1F=0B=E3L=00Z=9F=0E=88&O=A0=7F=97iwU=DA=022C=A5^=0A=941S=
+dH=80=81=08=0A=034=90@F=90.L=D6dqwW=FC=D1=93K=14=AAK14=03=80=01=1Ag=06=05A`=
+=05=BFt=9B=BD=17g1=F0=03J=F0)H=E0=98=97c=03r=B1$=B4=82=03=99=C3=1C=BA=D3=02=
+=10Q<=C0=E3=A2=D3C=3D6z=A3=DB=89$%=A0=02=A3=C2a0`=12=8B=F3!>=80;m9=03M@BG`=
+=04F=F0=03a=80=0390=04t!=16=18Z=14=10=E3=157=C0r?=D0*D`=03=82=F3=A5=91=99=
+=A5:=C9=AB=C0=0A=A6=AA=E4!=9D=87=8E;a[=B8g=13<=C0=048=B0=03P=11=04=94e+=BAD=
+=A1?@M=97h=1Foqf=FD=F2]|=DAnq=F6bM=E4=06Z=A0;;=E1=04;@=03M=D0=8F=91Gr=15F=
+=12=FFQ=04=A1=18=14=82F=A9xi=A9=97zX=F6=F3=02=D2=82=02=94=06:>!=03=A32=039=
+=D0=04B=D0=04X=90=04H =87@=B5.1=11=1E=BB=C9L=7FccW=B08zB=9F=FCZ=1A=93=FB=99=
+=B1=C1=BA=B1=DD=F8*t=98U:A=04QP=05kv.=DF=A1=03=CCFr=99Ai<=C4=1F=CB=C1{N=F1=
+=03L=A5=024@=19=1C=F2t=B8=03=96=02=E9{g=C2=13O7=1F=9C=D4P\=C2"=B6=823,P=04=
+=E4=05=B0) 3yU=A3=F5j=AF=02=B1`?=E2=114=F8QC=D1e|=B2=07=04d=05W =04=92`=18=
+=E7=C1IC=00=A7*=D0=02=D5:=159=A0=04=85=D9=03=E2=97=B1n=FB=B6r=C4=B1=1B=FB=
+=B1=C5=8A=9A=9Fc:3=90=9ChB<=B42=1D=EDD=03D=AB=AC=B6=B9KN=01=B0+=C2&/A{=CA$=
+=16q=84;=BD=D3=A7=11rVX=F9t=F1=B3m6=C9=7FD=DB3=ED=A4@P=90=03nx+=B91`=AD=F8=
+=B4P+=B5=B4=01=12R=E2=1B=E6=03=9F>=F0AF@=04H=A0=05:@=04;=C0=14Sp=94H=E01=AC=
+=13&TA=15=FC4=E0=06M=F0K=D3=08=B7=C4=FB=ABr=0B=ACt[=B7=1FR=93=8F9=8D'=C2=04=
+_1=04H=B5=12=C6s=18=A1d=9BU=90=B3=00"8=EBF=88<=B0=04=B6=F5=15=06=F5-:=BB=B3=
+=D7dD=9A=A1k=0A=14=039=E0X=99=9B=19J=C0=02=D2=EB=92B`=04=E1=84+=C4#=BA=F86O=
+=A4k=AF=A6{=BA=1E=95=A68=F1!=D01@=B8=15K;@k;VR;=80=95R=F6=A71=11=1D=AF=19"=
+=AF=E6=AB=C5=FB=B6=C7=8B=BC=C9=AB=BC=8Ag=80=C2cY=BE=94 T=D1P=87aB4=F0=03=A3=
+=02\q=8A9=91=B10=9C=F4/K`01=B0=04=E7=A6=95=DE=AA=1F0=88;=08c=A8_A=B9E[=B4=
+=A0A*=E79=1D*=10=9D=12=D9|_dh=AFQ=02=D1=F7=BF=00<6=13=82=18=D5=1A=1DH=00)=
+=3D=A0=03=07e=03;=96=B7=99=DB25=10=04'9=054ax=EDr=C1=FCf=CC=9F=19=EC=A5=1BL=
+=87=E88=9F=BC=D8=02C=E0=02=B1=1B=19=0B|=1EP=B7=03K`q=C0%=AD=10=14=16=F3=F1=
+=05=CE=AA=03G=80=04=A9bb=DDj=C3=E7=BB=8Fj=B3c.A+?=FA=C3=B7=A2=03E`=81/=C0=
+=03=BA=C1|=13y`3=A0=C4E#=11=80=E5=C4S+=03G`qR=F0=14=3DI=045@k=B4V=03B =19OW=
+=04?0<=AB=A3=B9=A9=C4=021Q=C1w%3g|=C1=FD)=AC=9DW=9Fk=1C=CC=9D=D7=1CG@=13I=
+=90=03R=E0s1=90=04=02;=13=01=B7=A8=3D=A3c=3Dp=18=B2=95=C0=9EQ+Yi=C3=DA=AC=
+=95=B8=93=04<a=C8=B8C]qj+^=00\=C2=C9>=16=01h=A2Qo$A=04=7FU,=DB=83=8A;"=1B=
+=DCYH10"=86=F7=02YA=13U=E1\=CFE=15:=80=05=86=11=9E=B9=D6N.=90=05_=81=99=BB=
+=0C=B7T=FA=8C=FC=DB(=CC=10=1D=D1=B0=06=8D=B7J=04M=B9m=E2=B6=13U@=88>=903.=
+=B1=03s=A1=13=C7=94/)t=C8N=BA=CD1dn4=B0=04=C0=17=A7=C8=E3=D2-=DBB=83=02=03=
+=A3b=1B=02Q=02G=90W?3=02=0D=03X%0=A3^=87=10R;5Or=84=0C=F4=12=AF=85{=1E=F7=
+=CF5[d=98C=B4=81i=04Yj=C1=0B=8D=BC=80( =12=0D3=F3=99=D5=18d=86=BE=E4=AC*5[=
+=CA=1Cd=FB=C2If=DD=04b=96=06=0D=07=B20=00=04=16=D7=CA=83=9B=C3=DC=AC=CD-=0B=
+=D3v=3D=ADo=A1T,=90=039`=02p=F1N=07=E63=C7b=02.=90O=10Q=04{=09=CA=B4=01>=15=
+=81-0P=1C=E4=02=8E3id=B2=B6=05P78N=90=8B=EB=EA}=0BM=BCi=FC=1200=04=8Fa\=CD=
+=9B=BC=C0=C6=D5=1F=E2z2a=03B@=04=82,d=17=B8=13A=F6t=19=82=8D=FB=BE=F4=02M=
+=A0a=072 =94=F2=D2.=DD06\=BEFt=D7=C4=CDC=EA=BA<=8F=03=04=F2,=10=F5=C1=13=94=
+=A6o=10A=83.=A0=D8=1C=B9w$=C6?=DD=11=14'=B9e3i=03=3D`=06=BA=DA=15=97=14+j=
+=E8}=9B]=8D=9D=FD=D9=9Fi>=8B=E3![=ED=027=B9=C6=8A=D1SY=9D=13R=90=12P0=17A=
+=16=DB=CA=F47=9E;CUAcW=CC=05=17=18=03G=C0=14=C6=A3=02=00=91B=C5@=18-P=1CD=
+=98P=E1B=86=0AS<=84=18Qb=C4=81=03W=D0=D8=D1"=C5=08=8E#=9C=8C =F1=02=87=8A=
+=157:=9ED=99=92#=09=12=3D\=98@=C1r=05K=9A4]=D4=C4Y=A2=C4=09=1B-t=ACp=D1=C2E=
+=91=87Bd=B4`=D1Bi=0B=186t=B8x:=03=C7=0E)6\=B0X=91U=EBV=AEYY|=05=1BV=EC=D7=
+=A5e=CD=9EE[=D6=85=8B=170=8E=FC=D0=F0=11=E3=C5=DA=19k=ED=DE=C5=BB=F6E=0F=B9=
+/=FC=FA=CD=1BX/=0C=BF0f=EC=D0=01=03F=0C=C5=8Dc<=9E!c=06=93%:v=F0=E8=11%=0A=
+=8E=1B7b=C8P=1C=83=87=0E=1E/b$=81=E1=E3G=0C#)=1A.t=FDZ=B6=8A=89=B5!V=B4H=BB=
+=C5=0B=12%:=D6=CDZQ=E5p=944=93=E4hQ=C2D=09=9C4=997g=A9=13=C5=09=185n=F8=B4=
+=91#=86W=B0J](=B9=BE=D4=C5=10=1A=87_=88=9D=D1U=FDX=F6I=CB=BEHk=96F|=EFk=89=
+=DC=A8=91C=86_=1Fs=EF=FA=17=AC-=1B=14=FB=EB/=C1=F0*=10=06+=1AcP=B1=19=BC=C0=
+A=88=1CvX"=87=1CtHb=08.=98=F8!=07=1C=8C(B=06=D0b=B8=81=88!vXL=86=AC=1E=92=
+=CD=A1=16e=B3M =DCp=CB=017=16=82=92=AB7=96=90=C2=8A6=E2~=04)=BA=CEZ=B8=E1=
+=B9=16=FBrz=AE9=9D=94;!(=16j0B=88=1A=E6=13K)=1A=9Ct=8F=06=CE=80=A8!=06$Z=F0=
+=01)=F5=BAj/=AC=A0=C4=13=CA;=B5=CC:3>=BB=FC=CA=A1=08=A9t=E8=01=C0=B5x8=90-=
+=91t=E8=AB@;=03K=901=C2=1Ad0=86=19h =02=07=1Cr=10=A2=89/=E8=F8"=08=0A=8D=90=
+=E1=B1=C7^=F8=E1P=1DhP=81=06=16_DH=05P=1B=92=88$=8CjXa=C6=8Az=E0j=AD=E5X2=
+=01=BE f`a=A3=94p =01H=96b0=A1=85=8B`=D0=C9=84=1BrB!=C9=9A=82]b=06=14=B6H!=
+=06:=8D=A8a=A9=EEZ=E8)=AC=B2=A2h=A1=07!x=1Cs=BD=B1=BC#=EC=87=1Enp!=07=17=CC=
+X+=CC=FA=D8l=13=AD7y=B0=C1=86=1A=E2=A5=C1=C0=B6f=F8=F3?=D3:=D3=C1O=03=01=F5=
+=D7_B=17=D3=C1=C3=0B=83=00"=0B'=9A=10b=07F=E9=FB=84=8C=86=1ES=8Dh=D4=83h=B3=
+=D8=A1=DAHbA.*V=98x=89=1B=B0=FAj=05=A5jxA=A7!\=80a=88=87=86c=EE=C7=9AL=80=
+=01=89=14=ECz5=BA=12=A0=D0=09=C9%M0=E1=84=FDfH";=ABx$+#k=BB=BB1=ACn=C9lOM=
+=17=84=C0a=88=19=06=E4=01=887=DB=1Do=AE=B4=FEs=EB=07 |=C0=01=B4=BFb=A0=81=
+=B0=03=0B=D3=81=0C=19=86=D8/`=00=E5=96=BB=C1=CFd =1B=07=1D=94=F8=C1=0B$=8C@=
+=C2=B2=F2*=95=81=08=C3=06z=A8=D6OG]<=E3=83b$=E9=86'=82=D0=01+=A0=8A=A0=E1=
+=86=1F=90=96=E1=04^=83=A6=01=05=D7=86#=81=85)rM=89&#x*=F9+=1D=96[=B2=04=17d=
+=80=3Dv=DB=95k=01=08=BF=E2ms=AC=1C=CA=14=EBi=A8=81=17=EF=05=CD=7F=98=97=B0=
+=18j=80=E1=AE=1F`=C0a=AD=B3=F2j+J-=94=D0=FBa=86=C4b=A0=FA=87|=F5=F2=8B=87 l=
+=B8=A1=07=C2=E8F=BFn=C7p=E0=A2=08=1Ft=B8A=D1#=C4=E5=E1=B2=1B=E8=AC=94=87!|=
+=A5M"=C71v=1CB =A7=02=16=88=88=07=F3y=02\=AA=E3=83=82=84=C7=05(`=C1=09$=88=
+=10=E2=94=00=064YIsZ`=02'=A1=CC=05G=88=DD=CAnP;=DB=AD=00hA;=C1=13=C2=C4=16i=
+=01=0Fx=C2=E3=8A=0B=AD=05.=1B=00A=0A5=10=83=0D=F0e=847=ED=E0=07yb=8B=9EH=14=
+=04=1F=F4@=0A=16=CA=81=0F=F0S =04=FD=05=06oc=82=A0^P=AF=F4U=B10=8D=91=C1=0D=
+=84=E0=03=0F=91=EF=06>=B0=81=0Fj =87=C8Hf>C=88=01=AB&=92=B1=14=AC=80(=01=14=
+=DD=00=09x=03=91=A5Q2=92=99=C1=BC=92=E0=9F=17(=EB=06=09!N=E9.=08=1D=E7(G=06=
+8=E8=81UN=80=C2=E9$e.?=8B=1D=0Ap=FC=D0=17=89=9DE=86=EC=81=E1=F0.Y=1F=17=EC=
+=00=076=A0=01=10=C8=B7;=E3=D5=00=07m=D1=0B=A0n`=033=90/^6=08=C2=0D=86=E0=84=
+=1C=9C=0F}-=B3=C1'=91=80=19*Z=B1=8AX=C4=01=E6NS=B4x=D5`J=E5=99=01=10.=E3+_I=
+=EC=05;=A8=18=E3R =03=00:."=3D=A0=88=0A=80=90=14=EDU=EA33=00=E7=94=8Ci=03C=
+=B1=ACR=AE=19C=0AX=92=BA=12=80=CC=04::V=B1Jp=04=C2@=81<=9DK=C1=14g=B7=03=B9=
+=D4=8E=84@+H=0F=92=B0L=B3=B80=93=C2=BB=E4=0C=BD=F3!=F2=FD=E0=078=80A=0F~p=
+=03 P=ED{`=83=DE*=C7=F9Jq=F5I`N=CC=81=17k`=B6=04=05=CC=96=B7TL=18k=A9=98=1C=
+$=E1=06<=10=82=0D=B2@=CE"L=B1=06,=C0=8Dm=A4=19=83=FE=05p"-=98=D1=0Ax =A2=C7=
+=88H{6=D8B=11=FB=13=E9=99=F3=015=05.=1B=C1L=8AC=02=13=F8=00.=CC=E9YoJ=B0=9B=
+=1E=08=85=06=E5Q=81d=822=87=1A=A0(=05'\d=EC=18=F9=98/H=AF=A0=98<=E8=D3=98=
+=B6I=EF=DC=C042 =03=11*S=B8U>=81=07}=8A=81=F3=DE=D4=83.t(=074=B0=01=0F:=D3=
+=19=FD4=A1N=01=BB=81bd=C0=83& =E1=0B:HL=82NZ=A0 XQ=B2=91-=8C=10=BCW=1D=96=
+=C5@(=96[A=0D=10=17=A3=17=3DD=050=E8=A95K=85=03Z=1D=E1=0AK=A8=81=0E=08=07=
+=06=1E=94=954=98=F1=81=17=E2=B7=D2=18=14=01=06+=D8=C8N=A0=B3=B3=F9=E8$=ADKR=
+=01=B9X0=9F"=A4j=05G=01=13kT=F0=82=1C=9C=E0=05J8ax=83=A6=B6=BB=E0=80).@=C2M=
+=9D=16=D71%=94=3DlzA=13=A2@6=1A=18=D3=BEtj=A9=9D6=DB=16C}=B1=07K8=FB=82=16^=
+:=D4=93=DA=12=06Y=F8P=0E=80p=04=DD=0A=CA1t=93b=FA=14=03=D1=04=DD=85[=C1=C9i=
+=8Cb=C3=906>=04_=FD=DB0=A86=C6=82=B8=1C=EAJ=A1=F9=0C\&$=04"8a=08=1D=9A=0A=
+=10=9Ex?O=81=E4F2=E9=0D=D0:=B9=83=F34!=06KJ=81y=91&&=90=89=85=BB.8=8D=0BL=
+=18=DE?=1A=8A1z!Q.=F1=14<=F6j=D2=BD=D3R=0B=A6=EC=FB=D5y=BD=F2=08B(=0D=1F=EF=
+C =EDX=86NE=D8=02=10`=9C=83=19=10=A80}Z=8C=85=16u=03=8C=EC=A0R=05=AB%=DD`=
+=F0=BC=FD=9ATw=80=B1K=0B=B5=82=1B=E4j=18"=02=94=08=0E=BE=0Ab6F=E4=06+=A0=15=
+=1AyP=04=DA=A2=B8=BE=16=12=82=11=94=F0=05%H*=89=DE=A4=01=C5X=B2=17=17=94=80=
+=06:i=1E=91d=00=96=1B4=01h=F0=F3K=C9=B62=D7=85=A6G=82=FB=8B=04=DA=09P=F0D$=
+=D8=F5=05Cxe=13=94=B6=DE*s=87xH{o=0Bd=A0=97=1B=84=A1*=C7T=AC=1A8=D3=19=0A=
+=17h=B1=FCL=02=10=16U=84%P=01=0EH=A0=1Cg=B4=C3=A0x=15J=07=BD=8EW=FC=18:=157=
+=9B4.=8D=F1%=A0=ED=12=A6m=06E+:=C8=B0=0Af`=E8l=AE=C0P =0Eq=8BJU=03!=18=024S=
+=D1=C1=BBc=E0=03# X=07=08=B3=AD=15=84P=B0#=F03=8B1=C0)=09ZP=83)=A4@#/=11=C3=
+=0E,=97=14=3DeA=82=95=CA=E3=AC=BB=13-=B3=0Ch./=C0=B5=04k=90O=BFpa=08Q=D0=03=
+=97=D7=A2^=D4=B2=D7=BD=D1=8A=1A=BBZF=D1)!=B2=07>(Cg(=DD=97=C6=D0@=0A;=C8O=
+=FC=80=00=04&t=E1=0DH=18=02=0F=E2=F4=18=18l=0F=07G=80=A8=A5d0=CE=C5=12=01=
+=EB=93l=B7=9F$K=03=07=F7=FC=B9=8A\t=82=0D=88=80=14p=A6j X=09=0A=BF)Rq%=02<=
+=E0=AF=89=C8=F2=A6=B0=9D\^=B7R=A1Y=CC=0F=82=80=035=1F=E1=08A =82=0E=B8=10=
+=19=C6=1C=11=075h=A7=AF@=C6=96=1C=A0=80U =93=CA=0C=9E=00=85=17=D8@t=AB=DEJ=
+=10=8A=A0=B5=93=99=CB=055x=13"wp=EB=13=E0=BBe#=BA=9F=A2i=D0=83=1Dh=C9=06=B2=
+=F6y=D4=BE=85=DD!=7F=AD=B4qI=C2=A4=8F=10=85=1FxR=B7=8DQ=D4=0E|`=C3t=DF`=F5`=
+xB=8BG=B3=83=19X=8A=FBA=E0A=14=EC<=A5=D1=AF=A1=0A=0C=D6=01g|pxx=0B=EC=89Gh=
+=BB=DB=03=D6=84=A9_^=9C3=F8=01=134=AF=E4 =18=01=A7=1A=061N1=99=1B=C8=08=BF=
+=93&=D7z=81,=A8=08=81=A0=81=A1=12=82=D0=A0=81,P,-b=82=1E=E8=01'=E8=01=B8=10=
+=11=9ES=81=16=E0>S=01=02=FD=FC=E0)=92=88=81=1C=08=82=1F0=82=B2=81-=19=B1=08=
+=AF=A0=81=C4=90=81"H=0C=18=B0=AB@=13=15=14=88=81[C=01=15@=11K=A9=01p=9A=81=
+=1Cx=82=1F=A0=81=FD=E9=B9L=02=9E=AB=10=0B=17x=82v=A1=8F7=81=81* =02!=98=822=
+@=82L=CB=81=F8a=8C=18=98=82 `=02=CE=E0=0C=F2=C1=01$@=02=1F("=1F=D8=81=0C=DC=
+=C0=0Bd=82=1C=90=02Q=E2=02=0F=B1=8C)(=02+=00=82!P=14=13=8C?=CD=92=AC(h3=06=
+=F1=AC=B6P=14=F2=E9=BD=19=B8=01$X=1E=1E=98.=C8S=82=D5=BA=8Dlr=AD#(=1C;c3*=
+=008=11=83=88=1E=D8=02=80=BB=BBy)=0C4=A2=81=1FH=B7=C4=B2=AF=C8=B8<=0C=F3=01=
+=C3!=C1=1C=A0=82=06=AC=91=AC=B8=0C=0D=99=BE=18=90=02=92=A8=08=AD=F8=A1=1E=
+=C0=9B=AF"=1F=11=A1=82=F3y"=18=10=82=E9@=88=15=18=02+=B8=1B=13=FC=03=02=A4=
+=E9=AA=E0=AB=92i=99!=F2=09=8A =98=1E=041=9F=1D @ @=02+X=82-=D2=01#=80=BF=CF=
+=D0=B6i{=A59=CB=01=1Ep(.@=02P=CA#=CA=F1=10E=19=02j#=82"=08=02,=A0=02(X=02(H=
+=BF=1B=A0=9C=F8#=94=12=BC=01#=D8=C3[=DA=01<=EC=01E=013=1D=12=11=C9=A8=81=81=
+=92=81Ps=C4=87=A0=18=D7=DA=01=92=F8=95=82=98H=FF=C1=C4=14=08=B5=0D=83=88es3=
+=C2=90=81&=C0=0Fc=FA*=13S=90=A3=B0;:=F3=15 @=01=E3=C1=811H=15Oi=01=D6@C=1A=
+=18=BB=1B=B9E=16 =1A=1D:=94*=B0=AF\b=02`=B4=81JI=886=92=82e=E3>=19p=82 =B8=
+=0A=CD=F3=B7=B8z/#=14=0A=96[=8A=EA=D0=01mC=13=F18%=BD=90=01# =9F"=F8=81'=D8=
+=02" =82=D1X?=19=F8=02$=B0=10=F8Y=A5x=F9=A4\=FCz<=1Cp=02'8=A6"=D8=01=19=F4=
+=90=C5=8A=1F!P=82=05{=02&x=02=19=84=1F=F6s=10=BB=E1=9E=D5=E3=0BBA)=19=E8=01=
++X=94=0B=E1=812=9A=81=89#=02#=08%=BB=9B=11N=C1=8D=97=0A=A7IR=11=BF;4L=F4=14=
+=87P=81=85=03=02=C68=1B=BF=88%=13=AC=813=C8:=8C=FB=0A=D5R=81=9E=00=99=0D=C2=
+=01Y+=A0i=E2=AE)=B9=1A=8C=B3;=93i=8B=A1:=14=0A=89=82N=FB=A4,=FCE=E3y=02=DA=
+=B8=C18=0A+!=BB=81C=81JgD=8A=DDh=9E=1D=08=0A=B6pJ=F1`=99=F1=F1=1A=B7=F2=0E=
+=1D=88G=C3=C9B=1B=10=82=A33C=1F=A8=9F=C5=8B=8C=1A8=91=F9)=82'x=A5W=D2"=8D:=
+=A6$8=82=FF=D2=C7i=CBF=F1=09=02%(=02"=106=1DH,=9C=F4G=06=D1=9E'(=03=C3=04=
+=C4=BA=D9=C2=C9b=B1YYH=C9=801=E0T=95=87=FC=E4=1F=02"=02=11a=C5=E7C=1B=D1=FC=
+;=C0k=1C=14=88=02r:=0F(=88=018=F8=8F0x=81+=80=BD=8D=EB=01=AC=D8P=15=E8=0D=
+=83=88=89=9D=08,=A4p=01]=DB=1C=AFz4=0C=AB=08=17=88=8C=80=AC=1F=F5<H=FCH=1B=
+=C9r=01 =C0=18=D7=AA=0B =D0=0A=1BH=82'=A92=B0X6=E6A=AC=DE=EB=92=CBC=AC&=1C=
+=8A=1E=00=02-8%=EF=11=02=AF=11=0A=E3!=1Feb=82W=CA=01C`O=CC=08=83 @=A6d=0A=
+=03=89=0A=A9=FAtS0=B2=AFq2=02=1F=F8B=BE=A46=E93=C3=1E=E0=01%=A8=81=A1=B2=94=
+=C7=98=C1=09=BB=1B%Z=94.q=D0>=B3=9B =98=95=1F=F4&=19=D0=AD=FD=88=16=AFh=C1=
+=15@=0C=16=E0=81=AC=C8=A5=C5H=82=E8=B1E=9D=CA=18eY=88=1B=90=01=19Q6=FFH=93=
+=16=C0=81 =00=0B=DB=BCE=90X=01cA=81+9=81!=08=16=89=FC=B1UY#=B4=BFz=C8=1D=D8=
+=01=B3,=02=D1B"Sb=8C=1A=08=13=BF=1B=88 =B0=08E,=B6=A4=D8=A2=19=88=8B=A2=14#=
+=1A(=03=DD=A2=81)8=02=1Bx=81*=90=17c=9A=01sU=A6=E6=01=1B7=EC=81=A0<=D5!=88=
+=13=A8=CB@p=AA/=1B=88Gx9=D7=8Dz=A5>u=A8=89=8A%A=E5G$h=82 @=82(=08=82)Y=C8'0=
+=14D=E1=BA=18=B8=82!=18=02=1DX=BD$=E2=93=81=C1X=C7=10=02=CA[=D4=A2*=AA=C9=
+=84V=D4=D2=0FC%N=1AH=82=EE=83=1Ej=15=D1=D1=CC=18=19=10=A0=838=02=01B=0C=D3=
+=F8A=A5 =B6A=D37=8Ex=A7=0CZ=01=1D`=0E=1B=DD*=1D 2=AFP=B2=FEI=CD=18=C8=0E)=
+=10=82%=C0=02p=E3=A21<<r=E9=1FN=BC=C5&=C8=01=9F=EB=A4=D7K=13=16@=8E=AB=A1=
+=C1=C9=9C=90"@=02=ED=E9=AD=AF=BA=D7=D5 /=A8=FC=18=02%=9A=A8s=8D=9Fx=E9=82=
+=1E=C8=81c2=DBP=E2=92y=E9=D3=BC=D5[7}K/=8A=9FD=A1(y1[=1D=98=02!=F8*0=13;=1A=
+=A04 =82*=80:=AE=CB=D8=8C=ED=D8=C9-*=A3z=CA=15=88=02#=E0=D8=C9=EA=01#=10A=
+=11=C9=C8=DA=80#=84=D0=C1=E98=81=14H=D4=83=90=01=AB=F8=3D"=E0Zd{U=19=D8=01=
+=8E0=89=8E(=9Dw*=A4=9D=C8=0A=F5j=01&H=0F=D1=E9>=98=E2=01&=10=02/h=02=A5=95=
+=D7#=E8P=C2Q=11=90=A1Z=15=F8=01=CD=BC=C5=A7=E1=02=B0(=B2=A4=00=0B=F8`=8A;=
+=E0=8B=1D8=82/=B4W0=92=97t=93=D7=97=A3=13%*=A6=BD=3D=02%=00%=BA=AD/=BD=85_=
+=F8=D5=D3=FA=EC=8C=B6-=C9=1A=10=17=C4=08A!x=82&8=14=11YVE=C1=0F]=84\=C4=8B=
+=DC=7F=9C=DC=0A=A5=CC)=DB8=EE{=0C.:=02(=FCp=E0s=0BM=D6=1A]=DA=C3=E0=14X=A4=
+=E6=19=88=13@,=A1H=81=19=E8=AA=16=98=81X=F1=01=95=D0=91=0C=8A=8E=17=D8=9Bji=
+=01#=80=81=CEC=01=87=81=81"=B8=81$R=3D0=D3=DF=F5{=8C=CB=A0=AD=DF=9BZU=11=E2=
+k=15=8B=D2=88=9E=1BX_&(=8F=AF=D2=1C=0BY=A5(PM=C6=C0=C3U=BA=DF=BC]=A5w=AD[=
+=B3=8D=DF-=E6=D7=F9],=1C=E0S=1B=E8=01(=F0=01.p=18=D2=D0=81=89{=02;=E0=C5=C5=
+=BA=90=B4\=D4=03=C6=D8E-"p2=AA=1E0D=1A=A0=83=A00=8D-=DC=01 =D0=A8=87=C4=DE$=
+=82=02=11=1D]=D3=C5`=B4=02=9A=1C=D41=1A=08=1A=17=C8=A5C=D1=09=16=D0=01=95=
+=90=81=94=D0=89=93=99=0A=16 =02=BB=D0=01=09J=01=E5=C9=8E44Td5\=A2z=8CO=E2=
+=01=1FP=01=1E=10=E2W=FET=A9=04=8B=11=DE=8D=84=9D=17{=9D/=E0=FC=12=82=CE=C0=
+=02-=90,<=9C:=FC=0CJcR=D2,=E62.=96=DF=F9Uf=FAU=0Dw\=94<=B4=8C4=F4=01=F9=CAC=
+=92=DC=01I=81=A8=81A=91=03=EEX=19=F8=01=D2=D8=81=A21Wcj=82=B6P=D0Tc=81G=CB0=
+U=85#=0C=C6=B5=13=0A=96=12P=01=13=E8=81=E5=B8=8A=15=18=A4=DEx=81=D4Aa=12=18=
+UN=16=9DS=CA=A7\=13=02=B6=88G=1B=98=A83=F0=81=CA=A8=01=F0=13=910=E8=09=CD=
+=83=E5=89=B6Hg=EC=8Er=81=BA=1A=D0E'x=16( =82=1F8=02=0BY=D2=091A=1F=18=D7-3=
+=E6=94~=DF=F8M=DF=A0\=E6=97=BE=81+P=B0=82Q=14h=DE=81=88e=82#=A8(O=C2=81o=96=
+=92=04U=8CF=95=DCE=95=8C5=00=82=ED=03.=CE@#=94U=0C=AB=EA=91=054=B4C=16 =DA=
+=03=9A=DB=91g=96=00=96=E4@=9D=9D=05=92=958=81=B0=9A=01=9D=FC=08=82=87(=01!P=
+=0E=13H=01'`=0B(=B0e=F2=F0=CD2b=C8$=10=D2=DC=A0=E8=E9=BD=D2=B0=88a-=99=97Ir=
+=8A=A5=A5=03p=EB=10%=C8T=19d=CF"=C8=02=93T=E9=94v=E9=97VfQb=EC=EA=BB=E1=9A=
+=8E>&=F8=E8 H=02-=90=02)8=91=1Cp=82n*`B=99T=04=AE=D0=1E=C0=AC=0Bi=82!(=02=
+=9D=06=BB!=90=82 =80=8C=03z=EA=95=D5H=8B=F1=C8=19&F=D1=81=E7=ACR=92}=D6=D9=
+=11=10=01=11=D0=95=12=C0=01=DD=0E=09=B5R=01j=DC=0D#=00?=B3=05=C2=C8=98=81"=
+=E8-=CB!4=E9=ADk"=9E=CD=FA =D7=B6=C0,=CB=AE=82nL=02=CCj0=C9X=82?=1E`=F7M=EC=
+c=B6bK=1D=9F=C7=8E=1F=BET=E6=CE=D06j=AB=E9=09A=18-=10=D7! =ED=96=FA=01o=02j=
+a=EC=EF=C5=10;m=81O=0Ba=82 =90=82{=CC=DC=1C=FB=00=88=1DOb=C8=88Ap=06=10=16*=
+=16=A6h=E8=F0=E1C=14=12'R=AC=98=02=C7=C4=14Bf=C0@=91=E2=85=0C=140T=98(Q=82=
+=04=CA=94#V=92X=E9R=04L=972Y=DA8I=02F=CA=9C&K=98=A0=E1=A2=05=D0=19In=D8=082=
+=83=C6=0C":b=0C)=92=83=8A=8B=15,VP=AD=B1b!=D6=ACZUP=ED=EA=F5=EBW=16b=C7=B2h=
+=F1=D3=85=0B=1AHr=F88Rd=88=93 :v=CC=90a=F7=0B=8D=1A;j=D0=E8=EB=F7/=E0=1A=82=
+=07=13=AEa=E3=87=12(D=89=DA`l=E3=F1=0D=1CAp=DC=A8=EC=B8=B1=E5=CC=91s=00=89=
+=C2=E4=07=10$h=80=F0=F8Q=C4G=8D=1FB=0A=C2h=ED=BAu=8C=A51^=D36=18=E3(=0E!HvH=
+n"D=8A=13&Cx=E8=08B$HA=BB3=8A(d=08=F1y=C3=8A=D2)=AA=80=F1P=85=0D%;z|q=01=FB=
+=D2=85=89=F0&U=CE=94=093fy=13-e=E6Li=D2=C4=09=144=C4=B6=88a=C3=05=16=18:=A0=
+=E4=D5=F1=02=C7=112=1C=D5=C2X=3DL=B5=15=82=0B=81=B5 Xd=8D=05=94Y/=00=D1=D8=
+=12>,=D1=83=19;=E8=80=84=0Eu=09x=94=0D=80=89=98W=0D=A8=15v=A2=0DBtA=C5=10@0=
+f=C4c1V=E6C=0EQhv=A3f8pf=DA=0E?=F4`=C4=12E=D0`=84]=B6=19D=1B=0C=06=E1=00=C5=
+=91H&i=D0=0C8=00=01=84=0E9=E0=90=9BjE=1CA=C5=0E;lT=90A9HQ=03=0E:\=A5=02t=D0=
+M']CR=A5pU=10=A8=B5=F6=82=0BD=0D=89=82Y6=ADW=DEy}=96=F7'K:=B5=90C=0E=DA=D1g=
+=03P.=D8=E0=D4=0D?=045=C3=0CL=C9=F0D_S=9D=99=A0V=0Cj*=95=83=F4=01=F5=02=0F=
+=A8=FD=80=04=12<=F0=B0=03=0F=3D=F0=FB=05)=AB=90=8E=F8=D7=89=83=D9=80=A2=0D>=
+=F8=00=04=13=95=E9xYe9 qe=0E7R&l=99=94=05=DBD=89H=D9=05=A6=91=CD=DA0=84=0F=
+=B29=F9=A4=0CJ=D8&E=10=BA=0A=E1=A3=A9<=D8p=AA]=FE=D9@=04=0E1x=DB=C2=0E[=B9=
+=90f=0Akz=E4=90=0A>$)=04=0EI=BEp/Z=9F=1E=E1=C2=09'=D9=C4g=9F=E7=01=FA=A7N%=
+=9C=90=C4=0A6LA`Yb=B9=80C=94=0C=97=05=04=0E-=D00=ABU4P=85i=A6`=B5=B0i=A7=9E=
+=BE=D0=02=0C=3D`|=D8=11>=E0 E=88=AD=BA|=94=88=82=3DV=C3=0D2=CB=8C=F1=CD=8D=
+=F9@=05=11-F=91m=8EB=DC=BA=05=8E7=E8P=F4=95=95=CD=BC=EAQ=CB=E6=D0lAu=D9=A0=
+=03=B3=D3=1AD=03l2=0Ca=04Q8=D4:=E1=0F=CA=0AH=C3=11-=D4=D5=97=0C=A8=B5=80=83=
+B=ECBd=11D*=D4=9C=FB=DD=0F=B3=C5p=83O?=91=FC=83=0C,=F0=84BI9=AD=14=B0=9F=03=
+=CF=D4=9EI'@(=D6=0F=0F=FA=F0=82=83=F3I=F5=F1W=1Cg=C5 =0Bl[JU=C8=13=F3=F0B=
+=0CV=CE=10=B6`R=F8pC=13<(=11=06_H=BD=0Cs_=B2=FA=D0=C3=85=95=FD=00=99=101=CA=
+=A8D=CA=3D=F8=F0C=10=C1f=F6=AB=10_=08a%=0EJ=E4he=D1=991a=04=1C=3D8=C1j=0F=
+=CC=DA=16=AE=0E54ku=0E=AE=C5=00D=92=13=12=D6=EA=B8@X=B5BB+=C8=90=95=DBn=A3=
+=197W@ =05e=0E60=01=D4=83$=CF=E0B=0D=EA=CDD=B8=C0=0C=E7=92=F6=90=80'=AD=C9=
+=97=83Z =04!=88=A5+=94=93J=E5,=A7 M=B1=80Fd=91X=02=B9 =BF[=11=C1/58=02_=0A=
+=D3=97=D7=C1=0EV=82=01^=A8~p=03!=E8=C0=07=BA=93Q=9C|`=85=1F=FC=80=09=C3=FC=
+=A2=0C=0Ev=00=04,=F8@.T=18=96ep=10=05=1D$=EDF=C1*=CD=11=BA4=17 =E4=A0.=06)=
+=92=0C=A6=A0=83=B7=10=A1=07F=9A=16=92=A2=F0=02(t=AF=087=98=81^z@=03=A2=08!*=
+W=91=0A=FB=DA=A7=C6=E7p=A5=06< =88=0Cn =A4=D91=AC=055SB=0DX=E0?=80=11N=80=
+=03D=89=DFJ@=03=03=C2=00=8C^=A1=0F=E46=D5=15=09N=10d1=A8A=0B =97=C7=90=01=
+=85=06=C7sB=0F~=D0=BA=D6=89=88=84%4a=0D=8C =04?=8C=C1V=AA=B9=81=12 =A3=AB!=
+=04a=09NHB=10~=90=03=DEL=A6LD=F8=DD=0F=8E@D ^)=0AC=B8=12=10=83=B8=851=00A=
+=08C=98=0B=11=92=C0=03=0F=C9=80l=90=EA=01eh`=BD=D9`qN=F7=A2=13=9D=EES=03=B4=
+=DC=C0=05e=A1=01=10.=95=C65=92=B3!=0A=FA=C1=12=945:D]P=9A=FB/=A8=C1=BFX=F2=
+=92>=FAQ'4CA|<=C2=95=B0=B0=00=06/P=A489=06PN=F5=05=0A=F8+=8B=C4 =E4=9D=18q=
+=F2U=9E|U=EC=04=03-=1F=A4=CAVJ=18V=9C=8A=F3=83+@=A1=0AK=08=C2=10r=F0=03=1D<=
+A=07K0&=15=8E=10=04!=F0=E0=97=C0=BC=12,=91=17=CC=C8=E8h=08=A8=CA=C1\D*4H=11=
+=81=85<XB=95|0=CDjZ=13_z[\=0B=94=E0=95 =04=B4=9Cj|_=0A=14D=83L=DA@=06< =93`=
+,=D5@=15=C4=E0=05%=10BJz =CF=11=D0=93`=883=89B=FF`=AE=A8l=AE+=0D=D3=14#=1By=
+=C8=06=B1=C0=05=A7*=1B=84n=00=A1D=B9=00=06$j=A8_<=C9*=0F=8EH0M=B8U=0Fx=D0=
+=84=B7=00=EF=06=3D=F0=15=12~=87=04'=18=81=08R=98K=10=80 =05=E0]a=09tP=C2=D6=
+d=9AC=1C=10=FC=01=08S=10m=10=AF=C4B=9C=12=8AP:=D0=C1=14`=9A=06=CE=EC=86=079=
+=A8A=12vP5=A2=CE=E9>t=92X=047=E6=1C=A7=B6o=05}AM=0C=AA=17=03o=C2=01=06=0E[P=
+=0B=90=E0=82=9D,=17%#hIY=0F=D7=93=18=AC =05=F6i=81=0C=08=04*,=04=A1=06=CE-=
+=0B=E6=C0=12W=B9~=85=A9n=A5O=0Ex=00=04:=3D=AC=06#=D3[_q=F0=03=13=09&=B0=82=
+=FD$=89@i=18=1C=14=A1=86g=80=A5=14r=F3=04=1A=B6=E5=07=888=82=13LE=04%=00=81=
+=86S`=C2=11=B0P=04=B9=B8=D6J98=0E=14=E4=F2=D2=11=BF=D4=B5=1Aj=A1=10z=10=84=
+=04_iU=D3l=92=93=A0=D0=1A)=E6=C0=8B=DD=1CY[=03=8A=15=E2=D2=E0=A9\=01=EF=0A=
+=B8=BB=83=BA=86mAb=99=0F=BF=14G=A0=12=8C=E5=04=01=13`J=AE=CA=82G=FET.W=E0=
+=01=B9=9A=D9@E=FB=AE=17+=9A=AA=D3=05=81b=83#=80=01=065=EB=C1=10=D0=D2=85=B3=
+=F45F*L=82=7FI=08=D1=AB=12=C6D=8D=D1=01=10=9E`=85=93F=AB=08Lx=82=ADl=15=D9=
+=C4=1Ea=09J=A0!=0D=95=80=04+`=E1T=A7=FA=01=114=94=D9=1Fh=E8J=14#10[=AB=83#=
+=B8A=08L(=C2=166K=A8& =85=06=CD=B2=8D=93=08R=83=19=98=9415=08^=0B=8E=D0=C0=
+=E6 =88=B8Pm_VZ=A0=A0 =E4x=05-=E8=01=1C=B6=D0W=7F=A2=80+3=C8AWHP8(=A3=E4=06=
+%=80A=1Eg=C0=02=19=04!D=3D=F8=C9=96=E1=DA=E5}b=EE^=07=85=C2=10=F2=82=14=BEL=
+=E1b.=EA=ABwlz=83[=FAN=08#=943`H=9D=17=A3=DD=16c7=B0a=9C0\=04"=18=A1KE(=82=
+=16=800=BB@=F7=A0=E0=816e=85=07=DE=D3 p=81=09Q=D8=EF=940I=FB%L=8F8=07=C7=EC=
+=E1=10=96@=06=D0Fz=96=CB|1=AA_c=9B!h!x*"=C3`=BE=F8=CFE=D6=BA=9Cm#'=82vp=A9=
+=B0|=11=0A*=8B=0D=9A=ECX=82=EB=06=D0=8F=D7=B5I=09=BA=AB=82=13x=07=066=90=02=
+Y6=B5m=F6=FA=9A,.h=02=0Cp=F0=93=9A=BD=FB=AF=82=E9=9E?_=00=031=E4=B0=09;=18=
+=0E=0F\=F7:=13zM070=82=C9@h=98=1FD=D8wK=08MbQU=05=DF=15=BC=EEv?8=DEsy=84)=
+=ADt=09]P=C2=8A=89#Z=1DU=DCW@=A0p=10=90=D0=04=DFhH=08;$=0E=A4=84z=EA=C97=B3=
+J<(p=08=87=93m=1D=0F=B7};=E8=B1S=B52=03=04q=CE=05A=A0=81=0E=94=E0=83=19|=AC=
+=05=F1D=8F=CF=03=C5=93=9D=80=CA,@=E1=81=0B=9C=D0k=AF,=DD=C8=AC=DFA=0BV%=98#=
+=BC =07=AE#=91=C9=FB=9A=F0=18)=C0 =07Z=8F=81=13p=9AX=B1=8F=DD=92^=F8=96c=A1=
+)=B4=9DI=C1d6=B0=D2~=E9Nr=1F=B0=D4Tv?=BF=EF=F0~=F0=D2N=09=08AH=82=12=A4 =9C=
+#=84T=B4=AC=ADxH=F1=FC=03#@=81=09X=E8R=D8=AD=9A4=81=DC=E4AI=8A=E9=00=0F=94=
+=1B=AB=D0L=0B =8A=13=0C=01H=E8=D8S=CD@w=D9=9A=E5pN=D3=C8=C0=84=DC@Y=E4=80=
+=09=C4=DE=C0=9C=95=09=88=04=06%=1D=A7=F0=DE=B6=1D=12=13=94=C5=AA=D9@=0C=D0=
+=05=0D=84=0Ae=C0=D1Q=E4=12=DD=11Ae=B8E=D6 `=B4=00=C1CQ=D4=EC@=D3n\=81=10\Y=
+=DD=8DJe=08\=10=DA]=B7=A4=CA=DD=A9=1F=A2=EDW=A0E=A1=FBM=81=11=FC[=A8=B4=96=
+=16j=07=A1=8CX=12=94=09=10=F4=08E=A1=0AH=9D=0D=01R=1E=0D=EC@=0E|=C1=0C=F4@=
+=87=B8=CA=BC=11=C4=10H=1B=E7=FCu=1E=FC=04=DF=AD=95=93=04=AD=C0=F9=F4=C0=0Dt=
+=95=148=1B=17=90=C0=15=EC=09=A0=18=E2=E1=14=0C|=BCI=09>=C8=CA=B1\=97Q=C5=C7=
+=B0@=88H=81=0Ch=C1=10=0CA=F5,G=16=D8=94=87=D0=80=0FlG=0FL=01=0D=D9P=13=19=
+=01=94P=81=80=10=C1=0C=1C=C1=EB=84=DD_=CC=0A=B4=A0Pi=18=C1=11=98=86=A5=99=
+=C1=15=14\=136a=DD=F9=80=14 =81=12=1E=DC=A8=DCb=14R!=83=99=8A=86=A8=A1=A6QI=
+=17=92=98e<=86=15=08=C6Q=F0=C0=B2=14 6*=C7=87=90=0F=0F0=C1=F4=E4=91pq=0C=BC=
+=A8=80BL=D9U=10=979=ED=E19=E2SI=C4=93=1F=9D=84Y=B9=07O=F4=95=0E=E4=85=C44=
+=0Ex5NW=D8@=1Db=8AW=CC=CA=12=A4M=0DT=C9=17=99=97=1C=85]@=DA=05E-=01=114=C1=
+=11=08A=10=F0F=0DX=CF=87,=CB=7F=1D=D7=BC=A1Y=0F=14A=13=FC@=01=140=D1=87=0D=
+=C1=134Z/:=E1=DD=B1=DD=10=FC@=15L!=15=14=D81=BEd=10=18=C1=10=94=C1=15p=C1k=
+=11=07=11pHka=DA=12=DC=00=120=CD=00^#=E5a#=1C-=8Br=F0@f=D9@=13tS8.D=0D=A8=
+=8BVh=01Q=D8!=CC=AD=D7=0A=C8=0B=0C=04=0E"=86=E0=0A=90=07=01=95=80=0B=A8=80=
+=0C =DBm=1D=D5=0B=FC!H=CC=87X=9CO?=92=9ET=B0Z=0CT=0A$=95=85=90=B8=C0=E7=BD[=
+A=9C=D6=FB=85=C1=18=9CT=95l=8D5=16=A5Q*=C7C=F5=C5=0A=F1@=1E=D4=88=14=94=01=
+=13=08=C12=96FI=A2=9F=C1=F9=80=128=A4=11 Zh4=01=10,=01=A2=09c=85!Zg=0D=8119=
+A=18=0A=01=11l=01=12=10=81m=0D=9E=1A.=C1=0C=FD=A4=0D@=CC`=16=E0=1Ff=E3`=0AH=
+=16=C8=80?e[V=D0=1AW,=04=13dE=CD=C8=C0=99=FC=A0cT=E9=1A#=F9=80=3Db=84W=B2=
+=C7=E1DE=01=11=90<=8E=E0=09=E4=C0Q=A1=05=0C=84=9DP=DD@=13=11=08$^=E0[J=DBTL=
+"=E7=C8Z]=B9@=E8=D0=80,%=09=0E=A0=C1=11@A=11,=10A=F2=00=0E=DC=E6`^dD=D9@=85=
+=15=01h%=01=14t=09=00v=8B=EF=80F=A09=C1=C1A=01=11x=C1=DE=B5=9F=A2%=C1=94=80=
+=C6=188=E8gN=09D=AE=941=19=C1=83V=89=16=96=D8=02%=C6=E5=AD=DA`=09=A5=914=13=
+Q=0A=E6`=1AA=0Dd=01=B6}=05r*=88=8B=AA@^=80=CE=A5=A0=E3V=AC@"}=E7=0A4=8EX=E8=
+=80=10=98=80=D4=1D=0E=0D=EC=09=09=B4=00=0A=AC=C0=0C=B4=87z=14=0CO=9C@=D9=9C=
+=05b0M=A3=B4a=91=B4=E5=E5T=0A=E7t=05=05=9E=A0T=D8=C0=AF=E4=85=0C=D8UQ=F8=CE=
+=138=C1=89=89=91=870=D3}=16=A6=07=DD=80=13=FC8=01=18=18A=13=94=CA=A9=00h/fR=
+N=1EA=15=D8=DC=0Fd=01=13@=01=17=00=1E=A8!=81=16h=01=E0MI=130=C1=14=B4=9F=A2=
+=BA=9F=10,=C1=14hA=12=98=A6=1B=EA=80=15h=A1=A6M=01=1B=CC=E9c=91=1A=9A=DEf}=
+=12=A5Q=D2=05=A4|=08=0DP=8A=A5P=A0=0B8=CA=99$=CCU=B0@=A4L$=DE<=E5=8E=85=1E=
+=E9%=C1=E6=1D=92=1C=3D%X=FDQ=10$iN=C4=80=0F=F4=8B=11=A4=84V=16=D0N=98=C0=0B=
+=0C=81=0D=D0Wc=00=0FR=C8=D7$QE=15P)W=F0=C0=03=01T=E6=D0@=84=FC=04=11=94H=14=
+(c=E3)g=AB=D8=E76V_=0C=EAg=11=18=81I=F6=80j=02(=AA=F8=A2=98=06#=15=FC=80=14=
+X=81=11T=81=13=10=81=16D=C1=11 =C1=98=0E=01=11P=81i=1EA=12=B4=C8=A2=16=13=
+=12$=01=194AO=F5=94=86Z=AAk-=81=12=ED=80=12=FC\fP^=81E=0A=88=0E=F4=D4=19=DE=
+g=11=1C=C5`y=D3Z=D6=95=1BJ=A2=AB:=CCQD=01R4=95=1E&=88T=F4=CD=C7@=C1W=B8=C0=
+=15,=17x=EDI=0A=B8=C0=11=A0@t=FA=0B=05=8AE=E0=18L;=EE=04O=F8=C4G=A0=C5=BD=
+=D0=80&.=8B=0DX=8A=97Q=E9=CA=B9,X(A=1D=D1G=B5=91=09=0EX=C1p=00=01=DA9=E6=E7=
+u=AAE=0EVC=A9l_<=CB=130=81=F9u=8B=BB=BE=AB=80=0E=A8=14=84=01=E0%=C1=13=18=
+=81=11 A=BA=D2=AD=11=8C=81=11$A0&=C1=16=FC]fMI=A1=BA=1F=10=0CA=14=18=C1=13=
+=F0=C0e=3D,=C4=E2=D4k=3D=AE&=B1=CA=1C=0A=C8=D8=10=81=0DxH=01=DEf=1B=EE=C5T=
+=F5=C5=E7=B0=00=DE=94J=F7=94=0B=0Bpn=0Dt=81A=BC=C0=1B=80=8EV=B4=AC=CB=B2@=
+=0D8=DF=96I=0EU=E0@=BF=B4=C7>=A1=8B=BF=F4=CB=D3=FC=E8=00x=BC=C7=0B=A0=00=D1=
+=1E=EB=08=A2=00=12d=DD=93=10=04=DE=1C=C8zY=85:R=C5#>=08=0CX=C8=C5\=8C=AA=F0=
+E=D8]=EE=A8j=AE=CB=C4 =0E=10=96=CC=F0"=B8=BAk=13.=E1=C3!=01=134A=C1=FE=1B=
+=FB=FE=1B=DE=D2=ED=11=F4=AB=18P=01C=06=81=FD=06=81J)*=0D=C5=DD=E3j=A1=16@=
+=ECk=B9k:=E5=C5=F7=82H=EC=1E=01=DA=10`=D8=CE=00*-=01=0E=F0@=12`L=19=D9=C7=
+=00B=CA=10=AC=8D=80=14=81=15=10=81=0C=F4=00=E7U=E5=E5\=0E=0B=BC=00=A4=9C=17=
+=EC2=CCO=00=C1=09=A4=00J=A8=87=0A=D8=91Y=10=01O=B0=CD;-E|=04o=C5=B4c=BF=BC=
+=07|=E4=91?A=CD=17=82c\=8D=1E#Q=10}=A8=CA_\=95=AB=8C+=F7=82=EC`%=D6=DA=0D=
+=06=83=CD=CE=9C=D2=A9=F9=15=DC-=DAJ=14=CC-=FF=11=01=11=B0=EF=FB=F6k=FC=8Eq=
+=FC=FC6=81k=0AA=11\=81=14@d=FBqA=12=E4=E4=E5=3D=EE=E3=AE=A6=16=060i$=D6c=CC=
+i=1EP=C9=B7=E6=80QR=1Ea=0A=01=1E=9D=CA\=98=CA=ED=E0=80=D6=A9=A2=0C|=8B=11=
+=D0=88=A9=C9=C0Or=04q=B6=1C=AE=D5h=8E=F5=D3=13=00A=F0=99M=0C<=08G=8C=07>=A1=
+=80=0C=EC=C5=0B=A4=00=D1=11=08=C9=E8=DA=FED=D7=0E=C4=C0=09=C0=07x=84Gx=9C@>=
+=BD=D6lPM=0D=F0=C1=0Cl=81or=0C=0FD=A2=91=D1G=9D=18=05=CCh/=137q=09=D5=00c=
+=A6=18R.=06=F0=04,=99=92=EF=15N=D8=14|=F1=16=E4=92=13=EC=9B=17=7F=F1=BFM=D6=
+=11=D8=E2=12$=01=12=1CA=17=8Cs=BFV=81=12P=C1=15D=EA=FD=DAo=CF=00=AC=17=BC=
+=C1\=BCV|=BEV=1D=EB@=0F=EC=C0=14\=E8=0F=E8=C8=14=ED3=AAlo=91=9CZ=B8=E0@=F4=
+=B1A=A5=EA=80=16=F4=80=10=FClA[=C8=A6m=90=89=12=10=E1j=C8=00s`=A9=04V2V4=C7=
+=0A=18=1F=E7\=89f~A=BD=09=01$=E5=00=81=98W;=9A=80=15x=8B=11,EC=10=84X=B0=CD=
+=E4=D4=15=0BP=81=BD=BC=C0+=C7=80,=D3r=0A =85=0A=B0=1A=1Aq=15=10=14q=0A=D2=
+=15=E8=12E=13=B9=0C=16=B0=0A=B9=96k5=0AF=AAtI=DD=F5$wV=B3=16l=07=80j"=15=D4=
+=E2=100A=18=A8A=104=81=13l07=A7=AB=96=A4=01=14 =C1=14L=01=18=90=CA=14=90=0A=
+\'=C6=14(A=85=B6=F3=02=19S=10=F0@=A6=FE=C6=89=D51=F9uA=E21=01=1E =01=17=B2=
+=C5=E5EA=A7=022=10(=87=86|X'N=81=13 Af=D5'=98=C8=00=BD=04=81=EFP=1B=1D=F6=
+=A3=8Fy=D9$=9A=9A=81=B8j_8A=DA=80=AC=C9,A=BE=A8=CC,=FF=C4#=ED=00=0C4=C1=9B=
+=185}=18=C1=0B=FB=D0=C0=D0=9D=00=13=D0=B2ngD=05M=819=B9=EAo=C2=CF=D2=3D-=E7=
+|S=F6=C8@=0D=C4=E7=A9P=9F@=DB=E7`i=D2=A9=A0]!=9B&=86=92A=F0=DC=E4=DAZ1=0De=
+=D8=12=08=07=C0zq=11`A=12T=C1=11X=01=14PA=16<=C1=184=81=14D=C1=19,A=C2=8A=
+=F38=BF=C5=02=19=C7=FD.=10=9C=DE=A2=10=C0=ED=A42=EE=E3=16=01=10\A=14D=81=C0=
+=BE=96=12=D4=A3=EBL=01@=C8=10(#FA=83=05w=10=14=C8CI=0E=1D@=844=09=E3D=88=90=
+=1CMf=10=8C=01=E3=85=8E*3j=D8=E8!c=85=0A=93'M=AEH=B1=92eK=96'Y=ACX1=83=C6=
+=19=198p=D4pq=A3=C8@=194=80=02=E5=91=03=87=8B=19(=90=98H=F1=82E=0B=187p=A0=
+=94=C9=82*U=1C-=B0f=CD=F1b=05=0C=18'R=9C=10=8B=02E=0A=1C)T=B0=F4a=D2e=FBK=
+=94o=E1=C2=959w=05=D5=17.h=F0=E8=D1=03=09=D0=19=7F=01=FB=14,=E3/=93);=9C8=
+=D1=F1d=C8=0E=1DC=AE=F0=81r$=08=0F=1D=0Eud=D6=B1=833g=1E=9F}=14ILeHi"=A7=8D=
+0=F1=A3=C4J=13=D7=89=9D<=81=E2=C4=8A=923J=92 I=B2d=CB=10/C=AC=08=09=12D=C8=
+=14%Q=BE$=112=87=B3=E6=1C=CF=A1=E7P2=86=0A=12=D1=3D=1C=F3=B8Q#=E3=E0=83=075=
+=C6=A82d=09=8E=1CQ=80=141B=04=8C=E3 =03i=E8=E8=D1=E2n=0B=165d=FC=88=CB=B6m[=
+=15%g=EAa=86=1F=82=88!=87)=8A(h =1Bh*c=BB=EDj=D8=08=AF=B1R=A8K&=1AJJ=A9=AE#=
+=B2r!=08=17Z=E8=C1=85=11a=18=8E,=1AP=18=8B=AC=B2=DC=EA=CF=A5=FD`<=89=AE=B9=
+=A8=CAJ=86=1Ap=F4k=06=1D=FE=1Al=A0=BFj=08=03=08=1El=FB8=02=87=1Fx=D8=01=88*=
+=84p=C2=8C(=84=E8A=B3);=F3=8C=07=1F=96=98=E2=B4=D2=868=ED=88"=9Eh=C2=09*=98=
+h=02=8B=C4=B2x=02=0D(=D8=CC=E2=0B=DC=92HB=091=95 M=B8=E1=82`=E2=88'=A8=10=
+=02=0A=1F=A6=8C=EE9=9Ct=98=C2=87=1E=AEh=0E=8A=1C=82=E8=CE=A7=EF -=08=88%=82=
+=C8=E1=06=F9|=F8=C3=07=CEn=D8=81=0A=81h=AA!=A6=19W=A0a?=17_4i=060th=A1T=1E^=
+=C0LA=C2p=B8!I=1F~8b=87=1B`=D8=08=87=15G=CDP=C3=BAZ=08=E9=85=1B=82=E0=C8=86=
+=BBFt=81=A4=18dXQZ=16_@=D5=BF=18=E3=92i=0BR=ABj=C1=A8=1A=82=0A)0=1F=09+=02=
+=07=A0=B83=02=A8&xxb=09=1F=D6+-=3D*=A6=DC=8C=B3!>=DB=EB=CB"=B8$=E2=08&=ACp=
+=E2=88+=98(316=B6=F8=A2=0C1=C4=88=FB=02=8A(=A8Pb=09=DC=B2=80=A2=0D%=88=A8=
+=08=CF ~=F8=C1=87C=B1+=C2=B1=CB=A0=BBl=87ZE=1A=A2=87#z=A0=E1=87=1C=8E=A0=C1=
+=D1=81"5=C8+=19`=90=01=8A=1Bn=00b=07(=88=C0=C1=0B=99A=FD=8B=08 =82=C5=D6Z=
+=B7=FE=BBA=08=19=EB=CAO=A3=FCj=B0l=07#&=0B=CD=E9=8Dd=E0a%=AC=84=FD=8F*=1BZ=
+=10.>=A0=80Pb=06$nx=C1m=18x=D0B=86=14=C8=A2{=C5=A5=FB=C3=F6=AD=19]=A5=B1=A9=
+bk=88=C2=06=1C{=F0!=08=1An =DA=C7!d=06=0C=B0=C4=D1-=BC=87!=89x=97=CA=CEx=B8=
+=A2=08$pM=A2=09.=BB,B=8A.=C0X=A2=E0&=C8=18=A3=0A+=AEh=1D=0B;=A2=808=8D=88=
+=E3L=A2=08"=86=A8H=F7)=8E@2=C9=91=EB=CD=8C=87"=84=00"=87=C1=C1=BD=C1=07=C7=
+=C7=A5=B9=E6=8D=BC=8A^=86#l=18=1C=FB=F2=C6=09=DB!=0AQ=E7=D2=1B=EF=97f(i=A5=
+=94h=CA(=86=1F=08=1B=C2!=DC=A1@=E2=0B$p=C8=CC+%=D2=B2=81=86$`Z=A1=05=C7.=FD=
+=8B=06=1Cqd=83Nq=84#/hAZ=C6g7=16}=8Fi=FB!=D5=8ChP=15=17t=0B=069=A9=C1=C7x@=
+=842=F0=A0y=03=11=90`=98=17=AA=1E|=C6=0A=9D=B9=DC=0E=AE=E4=83'=CC=09=09J=08=
+B=C6@G=04=CF1a=09M=88=82=13=AA@=85*=A4=E1=0B`=E0=C2=18=B2 =06)D=C1=0A=13=93=
+X=13=1E=96;=DD=09=A1K=FE=FA]=F04=B3=03!=18=C1c=DB=D1Q=07=05=13=A9=F0=F8=A0=
+=80/=08B=13vP=BD+=DE=C0@H=10B=11Z=90=83=B0=C1=88=81h=C9=01=02=C7=F7=1F=17=
+=E0=00g=D0=9AA=A5t=10=04%p!=0DK=B0=C8H=A0=A5=03=93xe=06)I=09=0Bh=90=03=C2=
+=CC`Wd=18=02=10p=FC`=83=E7=08=81=80M=B8A]=B8=D2F=17=C1h&=11=8CIL=800=97=FDU=
+=A5[.=B0A=18=91=E5=98=1D=8C=84\=E4b=1E=0D=CCS%=CB\=AE=07Lb=82=10=94=90=05)L=
+a=89C(=02=13=8Ap=84*,=C1tV=D8=C2=14=A8=E0=85.=B0=E1=0Cc=08=83=16=DA0=05)=A0=
+!=0DRP=C2=11=F9=D5=04#=E8=8EKQxb=14=A7=E4=03=86=1C=AA=09=E0b^+=9D=07-=19=F4=
+D=06#=E1"=B3\=F0=83=9D=F8=C0=068=D8=C1=05q=00=85=1D=00fTJ=D3$=0B^=B4=82=1E=
+=D4=E0=05=1B=89=01=18~=D0=83=1C(=A9=09|=F2=A5=0E=B6#=03#=0C!=06(BK=1C=A7=82=
+=1FFzs=95M=00=14=CAp=E6=95=18=CC=E03=82=D4d=DEb=14=B5=1C=B8=CC=93,0J=0C=B0B=
+=B6=0E=89=87P9=90=C2=0E|=80N=1F=91=B4J^@=92=09=A9=94=84$=F1=E0=07D=80=D7=14=
+=FB=C2T=91=D2=14a=09Z=88=82=9CL=E7=04/=E8p=0BhhC=00=0A1=86-xA=9AQ=D8=82=11=
+=A1p=87%d=01wJ=1D=C2=97=86=F4=03=CCM=A1^=98=E3=0B=CF=1A=D7#=9D=0AF1GP=1F=12=
+tP=14=10=81=E8=05=D3=D3=01=0F=82P#!=10=94=06=3D=E0=CA=1A=DFrR=97=F4`=05J=A0=
+=C1=15r`=D0=18=DC=A0=07=B6=02=02=AE=82=80=9D{=0AD=0A.x=C2Q=02=1A=03)=18!=06=
+=DC=D9L=97=9E=10=04 \=E6HP0(H|=B0=02=17=BC=E8=A4=D8=9A=CA=0Bh=B2=03=F8=0D=
+=14~/=10g=B3F=F4=02=B8=C9`=0A?)Bw=D4=A9=D3W=D6 =A1=0C=B9R=F0Ti%.=E9=CE=08J=
+=18B=99"=A6=04)=88=01=0D]=98=82=1D=B4=F0=861=D0=81=0Dj=F0B=16=90i=05=1A.au=
+=C1=E4=97=EE=9C0=85=B1=1E=81=89B =02_=80'=BC*=0D!=92=E7=9C=FB=ABO=F6r=99 =
+=DC=C0=06:X=16V=9A"A=FB=C8=04iU=01=CA=11=10{=12=C5=B6=A5=A03iB=0C=84+=04-X=
+=0A=07S=C0=D7=0D=920=03=1B=08Df%AB=0Ah=F0=12=93=E8@!=0A=15=C2=12=D2P=9B =08=
+=D8|9=BBJ=86\=F2=C66=EA=ED?=FA#=9EB'=DB=B8=FFUo=07^t=9Bp}PZhi=D1=95=CBU=DE=
+=0F=B6y=04(f=863@8=C2=96t=E7=C7!D=01=09M0=E2=12=AC=10=85=EDfa=0C`=00=C3=17=
+=D2=80=05=1D=E2=D0=CB=05K=02=EE=A6+=1C% =A16O=F0=01=1F~=97=DF=CE=8CP/=F6=0B=
+!=08Y=E9=13=C7=95=81=08<8=82=0Fp=B4=11=FBPe=82T=C1=D0=03=BB=A7=82#=9C=EA=A4=
+e)=8B=0Al0>=C3=0A=B7=060=D8=15=F2B=12=D7K=ED=C0U=8C=15=DFEU=A0=04=AFd=06=07=
+=EEC=02=14=BAt=99!d=E4=07=FBv,1=7F=FES=C8=1D=DB6=A3Q=D8=E7"=83=B2=9D=EA=1D!=
+=A4=D1=EB=D5=F3@=18B=1F$.=809=80H=11f=09E+=E5=8A=97=BD$B=11jW=A7=D9=C5=CE=
+=0EXh=DD=15=ACP=851=90=A1=09=05[=02=18=BA =85=DB=81=AE=09I=F0=98u=AE=90=E7=
+=FCb=A9=D0@=90=1C=B8=80r=83=10=D2d=08=AED=1C=0D=82p=A8=00n=AF=06=08=0E=96=
+=FEf=C4c=09W=FAnu=93=81=16x=D0=C5=1B a=06J=00=02Pt=00p!=B4=C0=06TYA=A7=F8=
+=C3=12=1E=D4Q=08<=F1=C1=11=A0`=84=EC0wj=E8=EB=A7IJ=8E=E3=EF=F1=BA.@ =C2=B8h=
+=E2=05=1C=10A=9F@=E0=19=14p=16=D2=E7%=F7G48B=0E~PX=96=A9L=80=F6tHt=A8,=B2=
+=BD|=ACc=1B=AB=08=11=94=D0=06(d=01=AAurX=14=18V=86/l=81=0D=89)7=13=AEp=04=
+=DA=E8=FC=09=CE=BE=0C=E6^>=D3=98=E6L=89=0AE=00=82i=8C=80=A5'=F0=A0=09@1=02H=
+=80=82=83=E3=B6=120=83=0B=D7=0C@T=15Rm+=C2=FE=FC^=B5=148-=82=AC@=06=C0}=C1=
+=B2J=D4=B6=C1-X&*`=C1=C2=15=88=83^=3De=07=BD=DB=94vp=80=DC=18df+<=C0P=0Bt=
+=00=86B=86=DCZ=FBy=F4=8C=EE=A38*=EC=8A3=99:=14D=19=B5=04 =0C=DD =83=B9c=0E=
+=A0`=AF+=14=8E=AD8=11=14f2=E3=03(=08=81=0AO=F8=97=D5=87=101'4=01Nr=FAB=C5=
+=D8=84=86'd!1X(7=15=B6=FF=84`n=C94D=90=AF8=E5=9D_=CD=F4@=08Q0=03E=AA=E03=A2=
+=E2=80z<=00B=18=B8cf=92=8DGh=E2=7FpD=06=B2=C2=E3HE=E1=E0=02o=E0h%=A4e,P=04=
+=05=86=03=06=9Ae><=04D=B0=E2=08^=AE=D0=C2=86n=FC=C4=82=06\=A0Wp=E4A=CA@fFL =
+=0A=A2=08=A6 =B28f=06=04=04@=1E=10U`=04}D=C5[=0CHJh` fl=07=C0=80=08=8C=C0=
+=EE=A2=80(=96`=08=AA=A0=F8jf=A7j=C0V6=A3=07=9C`s=A8`=0C=1A=E2=F9=A2h=07=E6@=
+=08=92=E0=0B=A2@=09xi8=84=C0=0A|c=08=BE*7=E4=E46=02=06=0A=9E=006\=A3=09j=C3=
+=0F=98`=3D=D8/=FB=12=A3=08=E0/=FE=EA=C5!=00=0B=E5=F8=E0=0A=86=E0=B4=9E@=07=
+=12c=07=A6=80=09=E4=AA=C9=1C=07(=90=00=EED=90)=0E=AE=F3=180=B16I=05|=E0%=CC=
+=C2n=C4=02,N=C0+V V=B2"+p=80=94^NXV =A7=AA=C5=04=90=00=05=10=CF<\/(vp =8A=
+=E0=06\=A0=06pB=06=CE=A0=C4=EA=02=F4=AC=A5=F1(=AF=07=06g=06=AA@=07=84KAf=A0=
+=09r=A0"=9C =0C=FC=9A=A0x.C=09FN!v =09=07b=0A=FC'>=94 3=A8/v=AE =3D=9C=A3d2=
+=A3=07=94 =0C=84 W=B4Ic=86=83=08=88=83=89=82=09=09t#7=96=E0=0C=C4=AC=09=C6`=
+=FC=A8=00=0A=AC@=98=92=00=0Bn=87=FDh=08=0Bz=C7w=AAD=96X/=08=C8=00=08=D6C=08=
+=92D>T=8E=07=98+=AE=D0)=84h=00=85=CE=08?j =E3=16=8D.=1E1.=02J=05f=E0=F3V=C2=
+P=00jE.Q,=98@,T=80=06^=C0=08=16M=F1=16=F0?<=8F=E3\=E0=04L=C0=04p=C0=07=BC=
+=C5=05=96@$=CA'=A7=9C=C0/=A8=A2=07**&\=05=12k=CF=81=EA=E2=8Ed=E0P=82=00=07=
+=84`jn=02=08=82=00=09=E2k=08=C8=CA!~M=06=80@=05!=05=84=A2=00=DA|=E09v`=CE=
+=F0=80=09=90 =08=BA@=08=00=05=EA6C=0A=0C=92=0D=00K=89=EE=C4=EA=FC=B8=10=0A=
+=E6L=1D=D5q=0A=D4=11=0C=A6=C0.=A1=20=0D=BE$=08=8B =1F=03=A6=09=82@=0D=C2=80=
+=09=86=80=8Ax=80=0Az@=EEVi=07=B4=C0=FD,=E5=06=96`/4k=84p=C4=F5=FCk=A7=FE=02=
+=0BB=E8xxF%=13=EE=11=1B=EF-=86=CF$b=82G42=C5=C4"&M =06L=E0=04=DCf#=A8=C0=A5=
+=12=AC.>=91=05=08=A2BL=8D*=AC=C0=04J=A0=04L=00=06n0=07=8AE=08=06'=07=BE=00=
+=08=9A=00'F%=912=E4=F3P=C2=01=F7&|=A6=82=9D*=E2=0A=94 '=8Ao=06=F8=CF=07=B6 =
+J=B4=E02=AC=80=0Dr=C0=09=E2=07+=BFC=D0x=AA8=8A=80=B5=A0,w=0ECJH=E69=AC=B1=
+=3D7C=07=DE=C0=0B=E0=CC=1C=AD.=09=AE b=AA=00=09=8E=E0=08=90=A0=0B=08t7=8E=
+=C0=08=D4=CE:=FE=F24NC4zg=0Bn=C78=AE=CD=08=E2oL=FC=CC=F1=07=08=0CY=F4B=08=
+=98=A0=0C=F0=ED=8A=04=F0G=98=C7~=1Ag=C0=12O=01WR=E1x@=E1X`=0Bf=80=0F=F0C=07=
+z%3=C8b5Y=D3=04^=F25c@=B7=BC=A5=90=9C=C2>p0=076r=13[=E0=04P=80=04~=13=06f %=
+G=AAXZ=00=08=9E=00=06=1A1&0R:=F7=06g@=02=09=08"=91=E0=85H=04=82=0A8"=92=FCq=
+3=14R=09=84=A0=10=99L r=80=07=A2=E0!=F2=C03=AAG/=FCod=9E/=DA4=03=DA=90@=86=
+=B8=00=08=FC=948=A4@s=BE=B0=86=06=B4P=07T@=8D Q=FD2=1FS=CE=08=9E =0A=A8=8Ee=
+=E0=EE3=FEQ/=A8=EE=07=00=E8=07=06G/=E2=AA|=0AQ=3D=FD=07HV1rR=12=D2F=F3-fOo=
+=EAB=16=93 <=0AB=B8=EE=03&}=F37O=E07M=A0=05~=E5=05Rd=F5=08=8A=05p@G=FCu=C0(=
+=86=E0=06N=80=04L=80=04H ,=80=C0[jDT=A8=82Ig=80)=C2=06=B1=18([F=82=08=AC`=
+=F0=AEb=D6=BA=83=06l =06\`=B0BB=A8p`b=F4MM=99=EC=C1r =D4h"'=80b=09=AA=84=F5=
+=04e=C3=B8=D1L{=A0c=FC=F4O)=03O*=87=0A=C4@@=13TQ=17=F5/=FFr=08f=08=09~ =0A(=
+=B3=0E=E3=0F=85=F2=85=17s$Dq=0033=13T=ED=C7=F5=08=C7~=EC=E7]`=A0Te=E2=B0=1C=
+1=A5j@4=11=E9=06=C2=A3=80`=E0=14Q &=7F=B3e=7F=13=05^`V=81=D5@J=A0VK=C27=954=
+=06=90ugk=16=07h=E4=D1=ECb]=AD=94=8D=AA=15%0=EC
+*=06=BA=00\j`=07X =06D=AFM=FB=E2=06X=E0=05V=B1e=9A=F6=83=B2=88=C9D=8C=08=88=
+&#f=80[A=E2=07=82=E7=F9=D0R=0A=FC=DA=C5=08=A2=E0=91T=CB)-=EEO=95=A0=0B=B6 7=
+=A4 =09=8C`=0C=04vs=12=F5=09=92@=8F=C2@=0A<fR?#=FE=06=97=CF=98=801L=94=06=
+=A2=80=15=CD=09=8B=8A=8E3=A2=C4=08=86=048p"$=82`=D10W=09=1E=88(_=80=D7=A0=
+=86=05R=B0=9D=18=C7m=82+=05=F0bV]=F65K@IM=C0=E3f`uK=C0=0CJ=82=04F=80=04z`=
+=04p=97=04=10=D0=05b=E0=0A\`F=A0@=7Fpsh=AF=B4he=84&=16=17=F1=A8b#=FBF>&=C8Y=
+]*=AEdf=F0=1E%=09=ED=8BG=08c =AE=00>=A0L=09=A2k=09=E6=93=0A=ED=CB=BEP'7=8A'=
+_=F3=D5`=AF=8C=FAp'6=044=0A=1E=13=87=8C=C0=0A=00=B7:=A4=C0=CE=04=97R1=E7=BE=
+(=93=07=82=10=08l`T=F7=CDq=8B=C6=7F|@=08=EC=E53 =B5=06=88=00=07A=04=96=FC=
+=1A=D1T=F5=87=07N5%=AA`*=EEcHh=82c=08,=B8l`=08b=D6ey=F6X=91=D5=08j6=C9vv=05=
+h=17wk=97=85I=E0ij=E0=15#=18=83=93=A0=F1j=EB$=92=E7FF=EA=F3V=C0=062D=7F=E4i=
+&=A6=82=06=9E=00{=00#!=D2I=8B=E8=88=F0h=80=185=8B=07=D8=80=AD=CC=D6=0A=BA1c=
+=92@=0B=E4K8=D0=17}=07=B1=09=E0V=0B=B4=00=09$T=8F=B8=00=0A=98 =0B<=06=0A=AA=
+=80@=89=A0=B9=06W=DE>=A3=CB=8C=00=0C=12=C3=8AXq=80?U=D0@=A5=B0=A8,6=902=80=
+=9D=05\Z=AD=E30X&=A8=D3B=EA=02(*XF=D4I@4=D4=7Fj@=09=E4=C9[Z=E0=08`=B2ew=16Y=
+Ixg=C3=07=05Z=80v=91=95=85Ey=04=E6=C8=04=10=B8*~`Zi=84+=1A=F0=86=FF=03=C3=
+=C0=C5=90X=F2$=FA=86F=BC%=0B=FCj=C0=08Z=89=F8v0+I=CCqb=98=07=E4=B2"=88=02'=
+=CC=03=EA~=C0O'=8A=1C=CF=17_=81 =09=C6=00=08=92=F9=99[=08}=8F@?=FF=E4c|=C0=
+=09=B4Y=9A=0F=E5=8D=AB=04=0A=92`=08=94=A0=08=92=19y=82=02=8F=F3x0&=F6=06=1C=
+=A2"=8A`2=D0=00=076=02=E9=12=09\=1B=8D=03=A7=02=E1=96=B7H=17yX=9Ab=06,=E5(=
+=F3=C2=025=D0[r=00Ig5=93=19zuek=85G=B9=04r7w=91=15=05=AE=02\=D6=82=C7=0Eh=
+=C2R=E2Fn=00%=98@=96MS*0X=B8d =0BF=D4U!=85=8C=D6=14T"=A9=06=10=B6=88=16=CA=
+=98=9D=AFM=89@=8C3FI=9C w=C8=E3=0A:=C6=99=11=B6c=84=AF=09=90@=9A;FA=B9=EFc(=
+s=A9=F9=EC=8D=7Fj3<=B4=0C=86(=07=F2=80=07J.=9D=F1x=97=BBf=A2=DEy=09=9C=A9o=
+=FC=0F=C4=8E=0A=F0!=F1=02=8D=9ABT=80b=F5=FCY4e=02zw=B0=A0=DA=A6Cj@=8D=14I=
+=05\`u=19=9A=A2I@=85qW=04=FC=FA=AFG=B9vY=D7+b=EB=119=1A=D3n@=0B=DE=C2=05=E0=
+bdIz'=9A=C0=06=9C=92=83|d=E8T=BA=F8|B=C8`Z2i=DA=98KfO=FF*3=B8=E0=0A=CA=E009=
+=E6=A7Q=1B=B5=8B=80=0A<=E6c=AA=E0=07=86=C0=09=02=97=A9=DFxp=B1=801=88=E09=
+=80=80M=9A=A0=08=16=B3=B20ZD=F7=8DD]=F5F=EC=898=8E=A0=09*=E7J=06b=09=F2=E2=
+=15=85s=06=DCY=07.H=06=96 $,R4}x*=D0=C7>~=A0[=C0=85F]`=A139=B0kW=A2G=E0=AF=
+=D5{=94=91=D5V=91=14-B6F8z=05b=E0=F3=A0S=C5RjUg=C0=09=98=80H=FEL=F5=88=0F<=
+=16Bfp=C4=0A=AA=87g=FB=A0=02=07=00=02=87@=819r=E8=00=A2=A3`=C1=1D:v=F0=E0=
+=E1=E3=C7=0F =12+Z<=82=E4=87=8F=8D=1C=81 =91=E2=A3=87=C8=87$I=8A=BCb=06=88=
+=C0"?=8E=18=F9=F10=88=C6!5l=D0=B8y=93=C7=8C=9D<g=1C=99Ad=86=0C*3b=C402#=07=
+=0E&=12=A7=88=DC=E1=83=86=0C=193v=0C=C9RDh=8F!C=B4=C8=90B=C3=06=8B=15-V=ACP=
+=816=AD=DA=B5i=CD=BA}=D1=82=85=DC=B9r=CB=A68a=A2D=09=12|=F9=8E=F8=0B=180=89=
+=BF"=0A=1B=16=F1=97=C4=15=12\J=DC=90=F2"=C6=8D=1Efk=9Ce=BB6=85=E6=CD=9C;sVQ=
+=A3=05Z=CF=9B1=A3]=C1b=C6=10=19=92k=DC=E4=A9=A3=C7=8C/9vH5=8A;=B7=EE=182=A4=
+=AA=0E=C2=C3a=C4=1D=08q$=19H=B0=B6=12=85=CC}=F0(R=C4=C8=94 =14+r=8Ch=FD=FAF=
+=91=FB=DCKr=E7=BE=11=C8=8D=1B=19=BB_=11re=07=12=1C"=9DG=ED=09=1F=A1s=1D=ACa=
+=C0(rd<=91=1E!=8F=FC=A0!=14k7=E0=E0=1C=80;=C4`=C3=0C=A1=B1=C0=C4Xn=99=06=A1=
+=0An=A1=E6 ]t=B5=00=03=0Ay=ED=D5=D7`=81}H=D8a=85=05fB=0Eq=DD`=03=10S=01=E8`=
+=84=A3=91=06=A3f+=CC=A0B=8C=A5=AD=B5=82=0C/=0C=E1D=0B6=B4pC=0D<=D10=05=0F=
+=B1=09Q=D4nJ=1A=B5=A2=824=04=B7=03=16L@=F1=12B9=0C=A1=14r=06e=99=DC=10G=0C=
+=C1=84=14J$A=1D=10f=12=A1=9Dv=DF=AD=D9=03IB=F8=A0=C5=10=FC=F1=F7=03=0E9=D8`=
+C=0FRP=B1=91=10=EC=F5p=03=0F6x!=C4k=F0=F5=F4d=0F=099=11=03=0C/=E4=00=83=14x=
+6=91g=0D=AE=EDDC=0E/=1Ca=E9=0C4=84=B6C=0B388=E1e.=E2X=FC=EA=84,8=E1=02=0F-=
+=B4=E0B=0B<=BC=A0W=09y=F9=05=E2=87"=8E8=02_%=94%=AA=0FJ=E8@C=10.=B4=C8=16=
+=8Dj=D9H=1A=0D;0{#[=AA=B2=C0=03M=9F=FEP=04=0F=06=DD=B6dn2=F8=B0=93=0E<`=01=
+=84=10=0D=09=D1D=13HP=A7=84G=05!g'=BC=06=F1 E=10Jt=B1=84=10e=1E=E1_=9A=1C=
+=B1=09^=0FU=04=C7C=0FH=00=B1DH<8=F1=83=0D=96=1E=91=A7=11A=D6=84=83=A58=01x(=
+=A6=0F=0D=B1C=13;=E0 =86=A3/=D8=97=83=A5&c|S=0D7=C0=8A=03=0CM=1C;WY=A5=9E=
+=8AY=AA=D3=CA%=A9=A3.=9CP=E3=0A=1C=EE=95k`=BB=F6:=82c-=A4=A0=82=0E1=10=08=
+=C5=0C7=90jV[;`=06mi,T=1D-=AAf=BD=E0=B4=C9dt!D=0EA=C4=89=9B}fwKUp=B5=111=84=
+=12d,1D=10=FB>=E8=80%=BCv=E3pP=0FN=98=19Q=14Lp=11=84=10h=D2I=91=9A=EDm=E7=
+=03=10Z@=B7=C3=0EF=10=81=1F=13>=04=B1=D4=C4=0E=1B=14=E4MM\lh=C67=BD=C9=1F=
+=117X=F5=82=0B0=B8p=03=106M=F1=A9=A7=AE=0D=01E]-=BC=F0=02=D4=A9=D2=AC=B5=CD=
+=14=C2=DACXA=C8=90=1A=0B5=C0=E0X_A=87hXbn=E50=C3Y(=8C|=FA=16p=F4 =B3=AAA\&Z=
+ZX=D7=88=B5f=10=8E%=D7=11-=88]=93=0D7,=A1=83=82=F5=99}=B6=92=00=D2=D0DA?H=
+=B1E=11L=08=E1=C6=11=09=BD{=B7@*=E1=BD=83=89=04=A1!D=F8=81C=1C=82=05+ A=09=
+=16Q=C2=12=8EP0=1F =E1:O`B=11t=E0=03!,=C1=0AO@=08=8A,=E5=03=93Y=8A=07=9C=BB=
+X=C6=86t=93$=DC=A0ax:]=AC\=D0=05=17=B8@N*=83=FB=01=18 =F6=AA=B8=CC=E5=06.=
+=D0=9D=A9p'!=1F=9AE.R=C1=01=11xp=05=1D=04a=09<=A8A=0C=84=C7=03=E3=05=ED0=7F=
+=B1=C1=09=E6R=02=16=BC=EA=04'8=16=0D=E4b3=1A=FC=E0X;XA=0CrT=96=17=D9=88=06=
+=DD=F3^=CDT5;=D4=DD=A0=08=06=99=82}=A0=C0=BE<*=89*=9F=1AH=BDz=F0&=1D4d=7F=
+=FD=83=97=12=A1=10=04"(eK=06=09B=11=B0p=04&LA"@P=C2=11=17v=04D=B4=04;O=F0S=
+=BD=92p=06=A6=14=01=07=0E;=19N,=C6=B9=13"*e&=8BH=CBb=05=AB=D9=D5=00=072|=01=
+=10=B4=A5C=A8]Ow@=0C=A2=AAt=A7C=18=D4 =08X=98=95=16e=E0=A3=18=A4=C0=8C40=81=
+=87t%=02=12=A8 =8A' =1A_\=10=83Z=B9=00=09=D5=9B=10=CC`=00=07=16`S=06=3D=88A=
+=0D=A2B=83=15=B4=B1j=FBm=B9L=A9b=E6=82=D2=D9=00zy=AC'=0Cp#=83=1D$=A1=069=08=
+=CE=C1r=90=86=E3=E0`=0A=82dNBt=E0'=81=8Cg@=03=C9=C1=14=80@=04=1C|=AC=A1Fh=
+=1B=1D=96p=05=99H=01=08A`=88=14=88`=04'dD=82H=C8=01=A0d2=84"4=A1`@=F0A=13P=
+=C99=D7tN=95%=FC=94=C5\=98=C3W=AD,=87Gp=08=CCRe=BB=A8=E1=CE=87Z=0C=EAXZ=15=
+=83?=C0=B2=05=B5=EA=10=AE@4=B4=C4=FC=A5=07}=11B=09"#!0=A2=C6R=15=E3=81=0Cl=
+=F0=83=DE]=A64=EB=B4=11=8Evi=16%=C4%=A7=B0=8A'=ED=E8iO=FB=E8=E6[=06a"B=832=
+=83=0C=FE=A06=09=09N=DDp=B0=D0=F144=08=02E=CE=10x=A0=AF#Pa=08=1A=D4=C1=13t=
+=B0=D7=B8-=A1=0AP=B8=C2=0F=0689=C1*a;=10=F1=01M`ZS=9A=D6=FC=F4=94x=9A=E7Rc=
+=A6=C3WA=E1&Z=15=E2=0F#=E4=DA=15=8C=AC=B5=0FJ=01=0APp=02=14=04=89CO=3D=DE=
+=AExE=B4=C4=90=A0=045xAor=A9E=B0=0A=C5SK=F8=C1X=CB=0A=AD=9A=01=81=06qA=9D=
+=0Ck=E0=02=1B=C4=D3=AD=B4=8B+=FB=E6J=83=01=C9=C6'R=99=CA=0Cx=90=04"0=84=B1@=
+=18=C3=16=00=1BX=86=E2`=08QP=A8`=F1=E6=1F=8D=10A=07=02=A1=DC=0Erp=D9$8a=09A=
+=18=02|Qd=03%=DC@=08=1A=F1=C1=18=FC =04#=98=B2=84=A2-=A1Lk=10V'=E4=A0P=AF=
+=B2=D0\j=C0=02=B881=A8=B7=A3YlQ=03=17=DA=0A=C9>y=A1=81^X =C5=E3!/=AA=1F"=81=
+=09=84=A7=04=95=A5=96?4(=C2=0D=A6=12=83&=9E=05=BA=CCR=CBX=82=E8=02!=98=8F=
+=06=3D=D8=81=90=E6=08=83=1C@=E1=05Q=F0=AE=1Eq=D3=03=B6=FB]p=0A=AC=012o=92=
+=02=04=864=CE?<=B8A=0E=E2{_=1D=D47=BE8=A0=82=12}@=AE=8F=0DH=B0L=F9=81=156=
+=12=04=04=97=16Os=13=C2=0A=9D#=93=09=9B=D0s6=3DY=0D8=85=13"=B4=94=06@=E0=02=
+r/D=97=1C=F8=80=C4=0F2=F1/=CDR=3D=B9=08A=08=8Fn+=8By=1Bc=19=FF6y=81=B11vg=
+=00=83=0E=CB=8E=096`=9EQ<=05=84=E7~=86=C8j=91=01jb=BD=82=1A@4A<=11a=8A=1A=
+=05=84F=91L=D8=F5=D4M=0D=04=E9eo=8Dw=06N=E8=01=1C=8C=C0=044=A39^=F1U=02A=84=
+=F0=05?!!=BEb=D3=08=7F=8E=A0=E0=3D=E3=C9=CFx=FA=C1xz=B0=04=FE=94R=A63]e=0D=
+=C2=A0=04=1E4=E1=06>=90=82=C5~0=03;=C5=E0=05<=00=C2=A3=E72-=1CPz=A8=A5=8A=
+=0Bj=BEH=97=17=F8 =97?=90]=0BN=A0=FB=17=A7=CAXh=A3FL=AF=FAb+=0DeQ=06?Hu[=C3=
+=DB=A8=18=FC@=09=0E=BB=01=F78s=DB=94o=8F=058h=C19{=E3=9B=16=1C=01@=E6=15c=
+=8A|`=03R=DE=A0=A2=EB=A3=81Q=E2=8AO=1D=D8=80*=BC=D1M=0E=A6=D2=1BLY=0AO=F7=
+=95=F6=0Dt=E0=F4-=B4=D4Oh=0EB=14=800=04"=F0I=CF=BE.e)=83=84'=D0=E2D=95=9E=
+=B2=81=14=08=E4=1F=AF=F7 =D5p=F97mS=D5=CDJ=9F=CA,=E9\:uq=E0=EF=98=E1 =DF0K=
+=ED=14l=B0=82$@=1C=D4=13=9F1=8D=053=DC=E1v=F1=08m=AF%=ED=1A=85=03!4=CA=07'=
+=B7m=CA/=8F=82=18=B9=80-=A1=B2=01=0F=96F=84y=D2'=9F=822=C9=0F=A8~=83 H=017J=
+`=0D=B1=AF=8C=C1!=D8=A0[L=AAy=08=E7}f=BC=D9=97=CDh=CERI=C7=B3g3=C3=97=09-=
+=C5=F9=BA=FBMv|=11=B2{=B4=9E=ABA=9B=1C=C2=03$d=E4=9C=B3=CA=A1=87=DF~b =0E=
+=F1&O=90=C1=10b=C0=93W=99;=07E=D0=EE=AB~$=97=1D,=C7D=A0~=A6=A8G=1D4=8C=C7=
+=0A=05F=08=0B=0F=A1w=CE H(=05c=D1=0C=E6=FF=7F[=DB#!=FCU=03R=90=03=F84=03B=
+=D08Tp=04E=C0`=09e=10f=B2wecOF=01=058=F0sW=86O2P=7FE=B0=04F=D0=03=06=C4=12@=
+=A0=14N=E7tQ i=C0=B7gF=C0tXG=05`=93|=CA=07BbWS6=B1=13=E6=A5=03H=D0=10f=80n>=
+=B0=04(=02=04=A5c}=1Evb%fb,=D0=031CsG=80=03/=A0=03=AE=01=05:=00=03X=90]1=A0=
+C4=80[=1B=E2T=BD=D5+&@q""c=18g+&`=02.=A0#.=F0=05MFr=95=07=03*=00=80o(=80*=
+=C0=02=FCY=01d2=90H=03T-L=E0=04R=80.=FE=85=04~=02 =14X=811=A04@'=03=0CvN,=
+=D1ohp0?=C0=04WW=82N=17|=0B=85=03=94=A3gL=87=897=00=05=0C=14=83=CA7Z5U=03NF=
+=03_=80tU=A1DN=90RG=E0=039=80=044 C=B1t}E83Ce=19C=B4=02.@=05=E4=F7}:=10=14=
+=00=12=04=E6=93=043=90C.=D0LM=B5=850=E0+6=E0~#=80=18^=18=7F=88=D7!za=02*=90=
+!:b[0=90=80=96=87=028=A0=8Doxyq=C8=02@ =14L=12=03O=F0_F=10=05P@=05=F5=C2=04=
+=05A=13B=D0=03=AE=97=81@'=04=DE=85O?=90=04=06=B1=15G0=05Z=C0Q=FF=85=1C=82%=
+=1E=97XZ=81=05X=E2=86=906=10=04=0B91=9E=F8=891=D5=03*=13fK0=8E2 H:=90=04=1A=
+T=05=FCd"H"=A3]=B0=04b=00'=8BBEpj1D5=F0=1C=CB=85BO=12=04F=D0v=FA=B7!z=F1=02=
+=D1X=028=004=D04=024=F0L=CE8=02=B4=B2=85=BE"=8Ddhy=B9=B5=03=99=A7r=DE=08=87=
+=D1u=1A,=F0$I=A2z=06v=04@=D0=80C=D0=04=07=15=1BH=E7zVvO2`=8F=BC=91>=1Af=15R=
+ =05Z@&?@=04L0Q=0D=B5P=09=89=90=AA=C3=96o)h=0F=C9JX=01=11:P=04=AE'=03=CBqYH=
+pQ=E62{=A44K=99=02=03=162=92r7w=A8=B2O=01B=15=AEC=03=9F=15=04S=E6=02N=90=02=
+dX=02V=A5=19A=A0=17B=A1=02'=B0=8C!R4\=E9=026=96=18=BFR=02=C8(M=B5=82=9Ad=C8=
+E\=D4=8DH=89=94J=B9=944=E0=03gS=14C0 =0A=C3=10=FD=B6=06=E3=98=1BZ=09=03R=08=
+t5`.=FCu"QB =95L0=04=E9=91=10Zr=109p=05=04=F9=96{=A6|a'=83q=F9=90=C8=A7=8AA=
+P=1B=90=02=03=99=92=03=FEA=04T=A2(N@N=96=F2_=B1B=84=85=D9K&I=16>=12=06S=C1=
+=03Apl=09s=06+d>CP:B=80=17&=F0=02)=F0=02=DC=88=02D0=16Q=D4=99=CC=18M%0=04"=
+=C3[0=80=1A.`+=3D0=8D=B6=12=94dh=02=AB=B9=9A=AE=F9=9A1b=92O=03+=D8=98G/=F0R=
+5Q=056=90=04=0E=01Y=F28=8F=DE=B5=03q%Q=1Du=03=F1#=10Q=D0=04!=D18=CB=19/=8D=
+=A4=11=D0=19=9D=D4Y=9D=3D=BA|=17=A32K7=9D=A9=A3=12=ABa=1FZ=80=05=E3=01:=10=
+=11=05=18SJA=D0v-=90=84"9=92=EB=C9=03*=E0=1C9=B7=814=A0=80=0E=F1=14w=B2o0p=
+=17'=A0=032=E4=02=02=FA1=A1=C1T=FC%=00"2=C0+%`=16:=C0=02z=01 =A4=B43&=E0=8A=
+=AA=19=03=AAI=A1=16z=A1=18=9A=94=9D=A1=02P=B0=8AE%=155=91:=07rO=FB=06+t=10^=
+3 =1B=E6=05=A9>=D0=9B=BEYl=1B87g=06|7a=07=8Fe.FR=1B\=E0=03P=D0=03=0Ec=03>p`=
+p=E9=A3>=0A=8A=A5=DA=90=CA=87"=03=92=A0a=1A=03Q`>=F2s=03=AFA=03L=A0=04=F9=
+=E1=02M=C8=03=E9Y=A54=E3=3D,=90 8=10=1CL=928=0D=81XE 7gV=04QV=94(=00)F=B0V=
+=09=A6=03>)E=89Wj(=00=11/`=02=C2(=A53=90=02=B1b=13<=A3=02=A6S=A1c=EA=A7\=04=
+=A8=81=EA=19=12"=03:=08~;=00=05G i=AF=E6d=D5=A5=13=E6=85=95Jb=A9=DF=F5]=BC=
+=11=91=0C%=04w=99tM=D0I9=80=10;=10=82&=13=AB=E6=A6=A3=FC<=BA=AA=3D=0A=A4a=
+=D1=AB=9D=C8t!=91=AD=F1t%=08V>=EF=91oL=D0S1=D0=03w=E0q=B5S=17=007=17\=E0Z=
+=D2=F2=03=DAc5fQ=872@=04=F8=CAXR=D0=04=84=C2=04=88=F5y=E4=8A=023=C2=04:T=8B=
+=83=07j7=E6=17}aN; =03)=00<1=93=03, =A0=DA5t=B7=D5Dcz[=7F=FA=AE=FFgV9=D2e9=
+=00=82^=C5$3=80=8Ed!N=8AI{=82=18=B0r=B5=B4U=A0h8P=04=DC=12d=85=C2=A5=04EZ=
+=E2aD=A9=CAtwB=B1=15[Jfba&=D5=05x=02XK=B7B=F46=04eZK=F7=D40=EC=86=92=E9=83=
+=04E=F0_=E7u=030=90 =E3=A3E=B1=82];p,#=99=03M=F0C=B3=864=A8=B3=95=EB!iA=80=
+=04X=90/=068{1=00=04=DEs=8BP=A3E:=C0T=D24\:=B0=02$PM=FC$=D0=02B=B0=02:q=1A=
+=EF=84Z=0A=B2o=0D=A1=8DG=10=80)'=03[=ABr=1A=AA=02=94sO3=90=044=F7=10=F1=98=
+=03n=C1=02!t^x=B9G=09=F8z=01K=88=AB=08=04=17=C4=15W=10r1@=04=81=D8}+2LQ=B0.=
+=EA=92gU=B7B=12=D1u=A5=D5ue&{=97=E2)=11=B9=03td`I=B0nz=02)=A5c=A6=A6C=1C6P=
+=05=0A=B2tU=F0=026=C0=8FR=A1=03e=F0)>=F0U1=00.=19dp=85I-=97=A1=7F=95=874-=
+=10B=F7T1=0AqzE@=04=EAe=1B=AC=C1=03?4Dr=11=04L=B9!C=D0EXH=02=B9[=93eq=3Dm=
+=E1#sA9=AF=F2B=F1=84L=A3!=037=00=803=C0=BC=99G=1A=12=92=AE=C6=01y=80=F7=BB,=
+=D0=10=95=E6 .g\=16=89=97=0A2=03=0B=F9=BDo=C5=C5z=C4=1A=FC=04X=14=CC=1A=FC-=
+c6J=E3-M=14o=FC=B1=11r=C6f=92=C3=82=D3=B9=8Al=09`=3D=10=06=E2=88)A=10=06m=
+=02=11=86=C0=B76=80=96=E2a>B=08+/ =04/=F0=03=FF=E9=8F=3DP=042=F4=0461K=E6=
+=E4,6!LfH=A5E=08b=00=EC=16@=E0=024=E2=7FH=A3=C9\i=1F=9F"^=DB=E1=10=12C=15=
+=9B=C7=9E+=90=AB=B0=F4=03=B8u,=B0=12=03W=A4=02%=C0=1BGv=1Ae=81=84=A1=E1=025=
+p=05/=10=06=04,Cp=10=03=B1=13=196=10=05=98=B7=19[=9B=C4>=07=03=16=99=8BS!=
+=A0=B7,=15=D2=C2=02K=03`=8AI=15=89$=04b{O"=C3=C5oU=812P=06=8F=85j=F6=D8=1B=
+=82=D2=10M=009J=B0=90=FD=B6=03=94=A8=03P=90nG0=BD6e>=E4&nO=C0=12=3DE=13TQ=
+=89=C2=F1/Jff0 :=89=BBTm5=C8=0A=89S=FC>=0C=A5/=C4=B9=8E=B2V=95,=8B-=00 ;=85=
+=032=C0=3D)W#*=F0=04702T`f9g/Q=B0=04=B6!=14<=00=D1=EET=CD=09=F2)=9A=D2=B4=
+=C0=D3=C2=96{=02>g*!Y=CD=D5r=06=C3tfP=1A=19=E0=EC=BA=94Z#=C8=CCF=AD=E9=8D0"=
+=AFK#$=94=BB=164 ]4=F08=08=15 =E7EHV=10=8F=F6=F1=CD=DF\l=3D0=BE=92=F7=BD=E5=
+,WC,HEPAO=10=95J=80z=82e=10=92=1B=B7=9F=E2=04NP=89LH=B8=05=F9t=05=81=04=A4=
+=F8-=E2r=C2[=F0=04=D8=82"Fp=86=F1DK9=B5Tx=F7=84=1E=97C=D8=B5VC=E0v=B2xF=98&=
+NA@=9EUa=86R=A0N=97=C7=1B6 20=C0=04=80i)U=E0=A4=C1h=CB=E9*<=BD=01=15F`=85)=
+=C0=D4+0=06=80=85=81=B6=854:,=03V=D0=9D=FB=EFSh/=AA#=99}:M=EB=7F=01=B8=02CM=
+=D4=F0Z=1Ad=11=CAQ35=C1-=A8=12"=15=FA$$=A5=E8y=09=D5=03=F3=B5=A2W=8D=D5=DF%=
+1=07=A8=95=84=98gx#=CF=EA=88=05=8E=14Q=E3a`=04=A1DB=12=16H=C0=05=12=A1-=C2'=
+=D7=FE=E3z.=81A=1A=A4@a6:0=D0=03=C0l=A6l=D5=02A=00+(=A2j=B2=133=B18=D1S=AAE=
+=D3Es=ACa=05C=B0=02(=00=10(P=9C@!=04=86=0C=18/^=C0=A0"C=C6=0D=1B4h=F8=90=D1=
+=C3=C5=8F=15=19Ul\=A1BJ=0C >=B8=0C=E1=B1Cc=0E=1E4=1A=CEP=91=C2e=0A=8E+j=E0=
+=901=A3=C6M=1E6 =DA=F8A#=C6=90=84/l=D4`=B1=F1%=CC-=3D=8E=0Ad=DA=B4=E9Q=15Tf=
+=E4=90=B1=83=88=8C#-aju=C9=B5=AB=0A%D=82=D0`=C1=A3F=0C$7f=FB=D0=D8=01=11=07=
+=8E=84A=15=CE=A5=AB=10=C6=DD=BB3=A2 =C4K=17=EF=DD=18Ar=BC=1D=02=C4J=91);t=
+=E0=C8=C1=C5=06=10)9=988=E9Q=C3!=8D=1E;|=F4=F0=E1=03=09=12=1C=10o=84=C6=C1$=
+=C8=94=180b=84=CEaD=08=922S=84=10=A9=92=D0=C5=ED=16=B9u=EF=DE=ED=E2=89=0B=
+=16=C1=85=03=17^<xF=E4=C9=95#g=D1b=08=8D=1FOr=CC=A0~6F=EA=1D*=06=9E8=11C=A0=
+=8A=16=B7]=BC=B8qWG=11=84:=C8"=DFhT=C5A=1CF~=08=8E=D1QE=0C=199=A6=CB=E0=AAB=
+=B8e=19|=C0=8C=B2=1E=98=10=E2=86=1Bx=C8=C2=A7=BB=84=90!=A5=96=DA=CB=E2=05=
+=AF=9C=B2=90=A9=A3`j=A1=AD=16:=A2=E1%=AF2=84=89=05#=82X=C1=85=8CZ`=E2=07=9B=
+h=B8=09=07 =80h=A2.=1A=FD=FA+=86#=F8=AA=F1=C6=00G=D3A=87#=AAX=FB=03=8A'\=AC=
+*=0A=1E=82=98a=87=EBbXR=07=1E=9C =E2=090=80(=02A=1Bl=90"=A4=1D=DE=C2K=86=C5=
+v=F8!=8A#=8A=E0!=89=18|H=0D7=DEt=13N7=19Z0=AE=B99=85[=EEN=E5=82=B3=01=07=B6=
+z=90=08=07=1DnHM=06=A0T=D0=81;=EEv=E8=01=87=17=C4=AB=E1=B6=1Cj=C8=CD=05+MdN=
+=86=14P=B0=E1=05"g=C0a=06=162=EA=01=85=1C^=B0=0F=C4=99r=90B=B5=9A=B8=D0A=AC=
+#=8E=F8=01=08=1C=82=F8!=B5=D4X=95a=05=1ClX!=86)=CEZ=EAB=0B3T=E1=06=1Fv=C8JD=
+f_=0Ao=85=A28=A2=AE9"lp"=07=1Fj=D4=D6F=C0h=D0qG=D5p=18=02=09"=10=84=E8&=19=
+=9A=84=01=87#pHM=07=D5b(=02=A5q=DF=E8=82=89=18Y=ABa=07)h=E8=EB=85=D5=AE=BC=
+=A1=8C=CD=16u=11=88=1B=80bs=FC=B7:=9B#=AA=E19=F1=948=A3=FFn=ABA"=3D=A2=18=
+=82=8B=B9R=A8=81;=13L8=A1=D1=85lhS=D2=E2=92=ABa=06=FC=B8=9B=B0Q9)=DEA=0C=13=
+\0=0AD=FFb=B8!]es=D0A=08+=AE8=02=88 =16=CB=C2=085U=F3A=87.=0F2D=88=1AB$=F6B=
+=A8X>=B5Y=11Y=FAj#=1D=90=0B=8F=05 Z=F0A=BC=DB=B6=05Wi=B9h=BC=ABK=1B=8Cxk=A7=
+=1B=90=D0!=D7=BB=B2=BC1=07=1F=83@=02=0E*=96=98=17F4=DBB=B0=AEu=CD=C5A=8D=04=
+%=AAA=CB0nh=D4=858=E5=8487=88=ED=9Cxb=E36l"K=1B=86P(n=84=06=0AYd=EEV=F0=D4=
+=05=CA=E9<.=B9=E0>=D7!=E4&n=10=EE=A1=F0`(=A1=84=9B=BB=82i=05=9F=F0=F3!=87=
+=1D=82P=E2=0B%\=D3=EF=E2=EB=F2=C2=C1=CC=9FX=98a=85=1B2=9C=9A=EA=AE=FB=FE=14=
+=11C=AC=8F=B2/&h=9B=9BT=BC=B9=80H{=DB=BB~=18=DF/=FD=E6=AEj=07=1An=18L=BF=D4=
+=E6=8A=DF.=18=A6bl=D6-=8A=E8=81=E9=B9K=BD=EB=06"<N!<=10=C2=1187=14=C5=DD=E4=
+=070=E3M=E5=1C=18=AA=CB]=CE8.=E0A=AF=04=F58=D5=B0=0C=057=18]=C8N=84&=17=94*=
+b=D0=92=9C=F7p=90=82=DB=95 =06M`=C1=0Bv=B0=03=17=84=AC=04$p=81e2D=83@!=04=
+=08]=CA=C1=0F=86 %!=EC=00[=DEJ=1E=0C\ =11=96=F9=A0(=CC=9A^=B1=BA=F2=82=19=
+=B8d=05=99"V=F6=B6=D2=1E=E64=87li[=D7]=CCF=BF=18l=8Bn=80=01=D3=D0Z=83=83,=
+=C8=85=8B=0F=E2=C1=0D=B4p=84%pF-=F3=F3=0B=D9^=90=84=1C=EC=EF&{=A4=01=0E|=10=
+=85=19=A1=8Ea=0FlX=04#h=9C=1A =C1=050h=01=0E=C86=FB=1E=18=9C=A0=83=1DT=C2=
+=0BNP*=15=18!M=B9=D9=95=0AVP=84=16=90=C5=057PA=0AK =B2=DB=85=12=0A3$=01=09=
+=A8b=83L=BDd=050(B=AE=06s=03/l=86=07<`Z=0Dx=00=86t=81=A9=06K=10=9A=F3p=10"*=
+6=D1)]=81=81V=94=99=BD=8D=D4=809=A8[S=0D=12=F2=03=83=D8eg^=FC=E2=0B=82=A0-n=
+=DA=E5G_@B=13x=C0=17=C7=D5=A8=07M=C8=8F=B9=12=14=11=FD=FC=A0=0B=3Dx=DCE=C8=
+=16F=85=C4=80=07O=D0=C9Ut=B0=04"=90!]=E4=11d=09=1DXBC=1E=B2Nm=BA=CD=17=AE=
+=B3=1D=13=98Rd=A3K=01=0CvP=839=84g=06=FCY=0B=0DPG=CF=16=DC=AE=95"-=81=80|`=
+=82V=8E=00=09S=C4=DE=104d=17=9D=D9=A0=07=9C=D9=82=0Dt=B0=D1t=E1=E7A=DB=DBH=
+=0B=AE=A7=CCe=C2$=06.=01*=0A=FB=9A=D5=1E=FF\=EC=05=B5=B4A=16l =846=E2=11/=
+=D8=B2=C1=BF=BEx=BE=85=10N=065=C0R=0E=D4(=C6=1E=FC=E0=807=D1=09\l=F3=C8G=BE=
+=00=07B@=D8=0Fz=B0=AE=1F=FC=00"n+=A8A=09=99P=CCU=AEM%IB=0FZ=00=03=19=A60=A2=
+=B73=01=C0v =83#=BE =05'=90=01=0D=86=00=85=85=1C=E1v(`e+I0C=13=84j=86#=E0=
+=CE2=C7`T=FC=18=91eC=B9=D8=CA=A8=93S0=CD@N@=C8]=0A=94=F2=92=A2.S=05=98J=E63=
+_=B2=83=17=B0=80=06+=E8P=0BJ=02=04=EA$=90sI@=13=0D=8A@=13=ABv=B3=9B=98=E1=
+=01=12=F6=D49=AC=12.=07N=18=C2=0D=82=C09=88=98A'=F5=B4=E3m=12=82=03=AE=0A=
+=E5s=8F=A1=C1=9E`=C8&BZ=0E=AF=DDK=EF=9B=1AH=83|=E2=80=B2=82M=E1x=E4D=10=90=
+=9D=C0=052=A5=FB=C9`-=FB=DF=FF=C2=80=05$=18=C1=08d =90=DA2=E5=06(=98=81=00=
+=930=94=D3n=B4=07:[=82=EC=DA=93=BD=D8>=A5=08,=8D=AD=88V0=E1!=84=12=A73=90C=
+=0D|`=03=1F`=891>8=10_=C2=A9\=B3=C1=170L=18=02B=E8=92=D6=B9p=F5w=FA=91=02g=
+=82pA=92=A1=F5=91?=C0=ADx=8C=00E=1B=88=A1=06[=AAjA=DB=BB=DE=F6=CEI70=F3=C1=
+=13@=E6ASJ=F4=06.=18=82)=8F0=1E=A2=84=C7=04-=00pJG`Y=DB=15=F8=06B=D0]S=10=
+=C5=02=14=08=F2=06=7F=F5=815=05=F4=84P=9ES=06=D1=C2Z=86e=EBg=11y=D29<=C0=CF=
+=FE=12=B4=03=1E=84=B5]=A1=D1=81=0F=8A=C0=05w=FD=05=C60=86A=0F =12=04=1E=CCE=
+=C8=E3=F9=C1Z=DF5=03=1D(A=0BB0=C2Y=C9=C6P=17=C0=E5=07[<K=C9j=10W=C79=D9=81=
+=FBP&d=0E=A4<=A9,)$l=83=CDr=96M=80X_=EB=E08z:=81=11,[`d=17=98=04S=18=F0=08=
+=BC=95=AE=96 =18=054=90=A4=09b=C0=D87=C9=D5=B8(=82=96=0E=82=F0=84=1Bt=A4=CF=
+~=C6^=86_=C2=82=AF=CC=00=06W=C1=CFZ=14=83=04# =C1=0B?P=C2b=FC=E8=901FW[=E0=
+=D3=16=A1d@=06x)d=D3=90=F4=C9\=E0=E3=03 =FC=E0=08=A6=AEko^0=84=BA=8EG<@=E0=
+=01=0Cl0=83!=AC=15u=82=84=18^=8B=03=9C=12=1AtC"=DFo=0El=90=84=06=CB=A0=DAY=
+=06=F0=EDVY=D9S=8A'=05H=00=EC=7F=93}=F33=97`=04DH=CD=14=8F=A2=B0.=97=E0=05=
+=92=EA=D0=B0o=B0=84=8E@k=05T=00X=A8=90ITr=1BU;=1D=06Q=D2u=CB=B2=18=ECO=08=
+=C0=13B=13=9C=90=05=A2=E9=E0}s=FBKr=B7=B5im=05=FB=0A=C8=03=87=A2m=E6R=DE=A1=
+d)=D3=E2iSo=1A=BE=B0=B1=B9=C0=0C!=1CeX=17=C2=F1=11=1A28-=10=82_=A30=C8=C1=
+=9B,=E4=B9=A9=81=11=F67=D4-0x=09=FE-s=09(=0B`=0E=DE=0E=06=BBm=81=B5=CD=8Csd=
+=93=A0W5=B9=9A=F3=82=D0=82=17=A0=F4=B25=E36rz=80T=A4=CA=84}=E2=1EV=D4=A7^T=
+=97=D0@=058=A4=C1=0C=16=D8$=FC=18a=09=C1=FB=02=1D=BE=D0=84=AD=BF=85=08=BD=
+=CF=B7=A4=01=93=B6=81=97=8DF1X=8C=C0=07=CE=83=B4=EE=EFS=F3=B2~=C3=19=CA=9B=
+=88O*<=BDy=A0=E0=C5=D6=02$=C4=A0=06=08=E3S=EA=C2C=83=D4=BD=C9E=A4=0E=B3=0B$=
+=99=C22=8F=B4=B2=AD$=F3e=E7=8C=A0=CA=02=3D=9C=DB=A8=DC=E8=8F=EE=E9=BF=19=02=
+=98=AFA=AA=1C=80=3D=A4=1A=B7=A8K=B0=E9=91=A2=15H=17=8B=1A=82r=19=14=19(=02=
+=B7=E9=FB!&=18=89$=10=BB=1C=18=BE=1C=10=02=1C=F0=02u+;=BC=B0=02=E8=8B>=E9=
+=B3=91=AA=E2.=B4=92=C1=17=C8=96=17=C8=0F=AE"=02=BA=AB;=DD=80?=1E=DC=8D=17h=
+=A0=BB=8A P)=0E=17Z=0B=88=DB=8D=1BP=02=C0[=9D=1809=1Dh=81=1B=B0&=82=10=A9=
+=FC=BB=AC+<=A5=12h=81=1CH=82c=13=C0d=03=02=14=D1=8A=D8;=91=19@=B6=04=F4)O=
+=82=3D+2=96=A83*=DC=D3=A4=1F=F0=01=18@=93=17=E0%B=BB=8BtA=10=1C=88=82(=E8=
+=81=92=F8'&=D8(=AC=CB=B7\a=1Au=B3=0B=19=8C=BE=E9=A3'=17|=A4=19(=9B4=A1=81=
+=85{=9C=1F=BC=C4K=8C2=08=F2=B8=FD=92=82.=89D=1Dp=01=1D=A8*=86i=01=19`=B3=14=
+(=82=8D[=81(=B4=81=13=B0=C2=FCs=81+\=81VB=81=9A=E9=81=CF=03=C3=11=D0=08=F7=
+=087=15`=14=02s=82dK=81=0D=FC=D9=AD=07l=C3=9Fz=C3&=FA=AB=0B=F3=A4=8C=C0=8F#=
+=18#W=E9=81,=01/=B6=A0=8E=1CP=17=15=04=BE=1B=80=82=BB=90=82=EE{D=1C=83=A2E=
+=DC4,=B8'=80=81=88"=F3>L=FCAML=19<=D9D=8Ai=0Ek=19=82=18P=82!=B0=013=A0=C6=
+=BAj=1B=18=E0=01=EEx=01=1A=B0E=13@=81=AEq=01X=BCB=85=A4!=13X=81=CD=D2E5c=AC=
+=15x=01=14=88=01=B2`=81=18=98=AD=9B#=01 H=02p=83 =1Bx:d=84:=DCk=A2=14P,=11q=
+=01=98x=81=09=CB=8F=B8=98=11=19=10H$8=AD=9C*=A2m<=82W=11=82=3D=C1!=B8(Gq=E4=
+=B7=9F=04=1F=E9=BB=09=85i=C7L|=C7=F2=BB=93=C1s=02=AE=F2=96-=C0=89R=B9=BB;=
+=DC=81=D1=B1=1D=C2=8A=B9W\H=AD=BC=AC"xH=88t=A1=81P=01=1B=88!=02=DB=C5=11=10=
+=01=B4<=B3=DD2=01=FD=FC`=01=CB=F09=DD=19=C9=AF =C9=A9=91=C0=A7=A8")=A2=01!=
+=98=01=1E=10=9F=1BC=1D=16H=03=DFj=02=FC(=CC=CD=03=BE=EB=A8=8A[=C2=81=C3=FA=
+=9C=A0=8C=BE=18x=CCG=0A#=F5=CB=01v=AC;=A4\()3=02y=C4"=D3y=9C=88=A0=8E=8E=92=
+=88=B6P=08=8B0=C5=85=88=81=C0=EA=B5=14=BA<=85=04=C3=FF=A2=01=AF=14@=11(=CB=
+=11(=8B=93<K=11=D0=CD=B4=94,eS=0DxA=B0=14=D0=3D=91TIc=B1=89%=D0=3D=EF=88=C0=
+=09=AC=1E=16=18KH=CA=A7=1B=D8=81=A2=83=16=CF=90=81*=C0=15=9Ah=92=07!=02"=90=
++=9D0"=C9=1CO=F2=84=81-;=CA=CC=942#=84=81,=00=0E=F6=C0"=16=00=1E,P=0F=D1=94=
+=08)(=AD=93=8B5=C6=D3=A4=FB3=A5,=CC?=88L=B6=D5=B3M=D0C=CB=B4=14=82=17 =82=
+=901P=06=D5=B9=DDT=B6=D53=AA=1F=F0=0F/=FC=B80=A8=F0=012$=8A=18H=82hSF=DB=DA=
+=1D=DE=BA=01"=F3*=18=C8#=9B=B0=0F=D31=1Dzd=8C=19@=93=C4=8C=8C$X=9C#=B0&=F2=
+=FC=C9=1F=1C=B8=EFk=93=F4=9C=1CF=D2=A2=1Cp=11=16=18%^=04=8F=C1=FB=01=F4=A8=
+=09=BDT=A0W=AB=01"=10=CFTK=01)=E0=CB=1C=C1=BF=AD=FC=C2=00=15=D0=D9=BC9=06E=
+=CB=168=C3-=FD=D2=07]=BDL=CB=8B"=E8=1A=07=DC=88]=E1=AD=0A=AC=89(R&D!=08r=EB=
+=8AG=91=01$=C0I=C6=FC=82n=E9=90=AD=F1=0F=BE=1C>"=C2)$=A5=01-P=A4=10=CB=8D=
+=C7=A1#=9F|D=A3\T=1D=DD=D1=85=F2(=C6=B3=81)=F8=C7 =B8=98=0E=89=BD=95q=88=19=
+=88=88=FD=E9=01Yq.=99Z=A0=85=10=1D^KH=AD=BCR0=D4=B9=02=0D=81=100=D03=03=D3/=
+-=B0=13P=81=19=80=88=1F=8DL=9EB=AA=83=88I=18=FC0=02=1Bx=90=0E=BD=907=1DV8=
+=05=AA=AFX=82'=FC=A7=10=B4=C0"=90=08=D31=16=8C=D4=81=9E=A8=89=12=FD=D5=DE=
+=BB=A8=98l=81=18=D0=0D;:1M=93LF=0D=D7=F8s=D4=E2=08=8F=1C=00=02=14=DB=81,q=
+=91[=D9=9E=DD=D1=0C=87=A8=89WC=B4D=EB=81=A2=19=8A=A0=B8=82=F8XMS}MT=FD=D7=
+=02cU=81}P=B4=14XV=DD=D2=D9=E1=95=1Ep=0D x=90!`=01%p@=DE"=82l=A4=81J=9D=A2`=
+u3b%=D6g=FA0@\=88=B7X=82=E6J=97=F6yX=DD=A9=02)R=81=1D(Q=E40.=1A=E0=01=CF=81=
+=01&=A0=93=EF=B1=A3=19=F0=CBpTTq=0DWr=BD5=17=08=02=19h=027@=A2=DE=8BDR=EA=
+=0A$=18=82=1F@=8D=AA =02=1D=18T"0=02(=98=C3=1Bx=02=DC=12=02R-=D5~=05P=80=FD=
+W=83mU=17=10=81=AF=1DX=06=FC5=81!=E8=88!=98=0A(=B0X=08=82=3D=DE=C9=01,=D8=
+=A8=EB=A8*=87=D8=9D=03c=8A=8D=CD=DB*2=8A=A2h=AC\=09=02U=EC=90=FA`=01=1D=A0-=
+*=92=A5=948&=8EX=8FZ=9DG=EF=A9=A1DT=88$=F0=17=B9=08=CA=9D=E5=D9=9Eu=AFI=A1=
+=C6=9BP=82=FD=08=AB=16=F0=0A=15=A8=B1=EB=98=8A=1D=90=04!=B8=02+H=82 =D8=03=
+=C6h=AC=15=D8=0E=19=C02=FF\=C8=AE=05=D8=B1%=81=B15X=06=1D=81=BFJ=81=C2E=82=
+=A5:=0E=89=C5=12|#=A3=3D=DB=D6d=CA[=E65=C9=96p=88R)=19=DD=FB=8A=1F=A0K)=C2=
+=BE=9B!=0B=8EpF=12=D2=81Fi4|=AA=82(h=D2=BA=B8=BES=BB=DCEm/=CC,W=F0=B3=12=88=
+=B8=02lILgJ=81=C6=B2=A6=83 =02=C6=00=03$H=02,@=BE&=98=0E=EA=BB=DB=14=98\8=
+=8B=A8=17=90(*=B5=D2=DB=0D=D0=B1=FC=DD=81=DD%[=11=E0=0E=17`=82"H=01=1B=08=
+=99=0E=D1=88;=B1=81&`=0B'=C8=15=1C=D0=027=80=01{=A4"=E6m=DE=E6=14=B4lT=0D_!=
+*=98=90@=D7=AA^=A8=D8=9A=8C=A8=81 H=B5.a=82=C4m=8B=B7=C8=95=BF=B4=A30z$=F4M=
+=DF=83=1A=BFEM=18=1B=F8=CE=EF=04=02'(=8F=180=9F=E5D=01=16=F0[=D5=18=82=C1=
+=08=83=1F0=02#8=02=C5h=82=8D=B2=99=14=A0C=F2=B0=81k=83=81=15=E8=0E=1D(=A5=
+=FDS=E0=05=D6=C5=B1=15=DB=07n=D5=DD$=01=15=10=0A=8A=99=A1=BF=82=0BOz=1E=1C=
+=A0=18=F2=F0U=96=C9*$=88=1A%=B8=15=818a=BDmN=A0=B5=82 =80=82=18=18K=0E=13=
+=88=19(=16=91=DC=0A=D5zN?I=A0=C5=19=8D=12=F9Q}=B3=0B=E1=F5=AE=1D=1Cb=DE`$=
+=F1=D3=AB=1Ax=01<=13=BFv=04=9F=B7*=022=8A=A5=820*=17=FC=F0F=8B=92=AB=1BP=03=
+=B7=E2=A5=DF!=3D4=FD=A6I=C1=81=E0=C0=8F=AD=CD=C56=06=C38~=E09=EE=3DZ$=81=1B=
+=A0=E3=11@=81V=A2=BEf=DD=B3y=BC=8D0R=0D=3Dl=8A=D8Ed=14=A6=9A=DD=F3=09=1C=A8=
+=8C=D7S=81=1C=A8-8s=8A'=08*=9C9=91=1C@=83=E0=02=82=99=B0=81=EB=9AB=0B=FC=A2=
+=B3=10=E3=17=D0=02=D5=08=C2=1B]=18L=BC=CC:q=A4%=F8=D9=B1=F4g=A3=C4f=BF=A8ZL=
+=19=08=81=A8=81-=C8f=18=D0=01t=B5=01=933=8Bk=D5^h)(}2=99W\c=00;f=06N=E6=AFE=
+K=18=10=820=18=81=1B=90=D5=9B=93=13=1F=12f=F6=1A=BC=F0=A8=A1=A3=BB=DB=CER=
+=01o=FEf=0C=A9^=1Cp=82=14D=1D=A8s=81=09=A4=9E=14X=81,=88=93g=B3=89F=DB=81=
+=B4=08=0D=1BX=02=FD=E0=A6=17=93=1F=ED=CB=89=C8=E4=C1M=0BhG=9A=94|=FC=8A#=BB=
+z=93=FD=EA=A3=DB=88Bn=AD=BB=B4J=88=1B=E8=BD @=14=14=B8=A8=06S=88=1F`=82s=B9=
+=18=89=80=0Ey|=93P=11=9Db=C6B=91vc=92=1E=D8=11X=88"=D8R=15p=D53=DB=88_=B3=
+=99G=96=93=F1=1B=8F=00y=0C=ED@=943=CE=E9=8D=AD=CB=8E=D0=D6=7F=0CV=1B=00=B4=
+=AEX=01=16i=B7=1E=D8!=1E=E8=0C=1F=A80=D6P=0F=FC=A88=C2=B9=8E=C1@W=A5e =BB=
+=1B%=F0B+=AC^$'=B5=01=DC=FA=EA=CCq=81=B8=95=E7=88=F3=BE=B4jX(=E2X=D7=C2=BE=
+=17 =D16=82=9A=C5Y=0B'=00=02=E6=B8=E4=16J=3D=CA=FB=AF=FD=EBkd=FE=EBV-lX=DD=
+=D2=11He=F8=BB=AC=16=10=83 t=CB=DB=B8=829@=02=A3Z=82=ED=A0lD=AE=CB=19@=02d=
+=F5=A4=CD=8E@=97=08=C9)NL(X=1A=02"=02Y!=9A=D0=C0=82=EA:=A7=A9=0E=17=D0=FB8=
+=17=9D=F82=C9i=01=80h!=B0=85=8B=176j=B8H=A8=D0=C5@=82=0Aa=F0=E0=F1=82=06=12=
+=1B=0CY`\=A1=11#F=81.|=DC=B0q#=A1G=1D=0C=1B*|=A1=D0=C6=12=164P=9C=88=19=13=
+=85=0A=19PT=9A<R=E3=C5=8B=1B2=04=BE=C8=B8=91=05A=81O`=9C(=A1ti=09=13$=9EB=
+=1D!u*=D5=AAV=AFN=0D=A1u+=D7=AD"Dt=F5=FAu=EC=08=15$F=9CE{=82=85=09=137t=C0t=
+=C1$EL=172=EF=E2=CD=ABW&=8A=BE~=FB=A6`=D1=03F=0C=1A)=FE=FE=BD=8B=F8o=0A=15=
+=3D|=D6hQ#F=0C=19;r=EC@=93=A4=CA=93)@t=0C=C1=81D=86=0C=18=A6y=A2~a:F=8D=1CA=
+|=D4=A8=A1#=88=95=16=1C=1B=12=C4=11r!o=DE0r=80=A1ac=C8=8C .b=B4=D08=B4=87@=
+=1A=3D=C7=F4=A0Q0=E1=8B=1C$=3D=FB=BA=90b=10=E7=8A=18/z=C4lk"=E6D=1A0=16=92a=
+8=DB=05G=E5-=B0=D88=B2=03=87u=98mK@q=E2=A2=04=D4=FEi=B1=02=18=E0=08 =10=18=
+=96=81=07=8E%=C2=08_UU=C2~O=B9pER&=D8=D5=D6=0A=E2=8D=B7=97=86z-=06=D8=0A-=
+=C8=C0=85=0D=88i=D8a=0A=8D=BD=D0=C2=10:=AC=F6=18=0EF=00=91=84=13O=0C=B1D=0E=
+8=04AC=0C=A6=C1=90=1A=8F=A6=CD=D0=84=11C=EC@=C3=167=D8=C6=11Q'%4=83=1E=3D=
+=BA=C0Co=BC=BD0DH5=D0@=03=18=F21=E4=1E=0D9=EC=94=84=0DF=F0=E0=83=0C1=E8=F0=
+=C2=0F=3D=00q=1D=88/=D8=C8C=8F1=F4u=02=0A1`x=C2=0B=87=EDh=03J=03=B9=B0=83m=
+=CA}H=C3=0C6=D8=E0=C5=9E'=A4=B0=C3=9D2=98=C0=9F=7FQ=09XiU=04bz=A0=A6b%=C8=
+=E0=083=A0=05U=A4J=DD=FB =1E=A4=18f=B8=A1=AA3u=A8=C2=0C5=1C=96=D8=86=1D=A2p=
+=E2=0Ag=9A=B6=C3=0D7=E2p=C4=0FA=14=E1=83=1D=98=CD=B0=83=0E8=F4=C8S=18;=C2P=
+=DAj2=18jhd=1C=11AC=0D=12=15T]=0D7=DC=10=C3=94=0A=FD=1AC=10J=D8=A0=C3=0D=86=
+=1A=E1=02=A1=1AI6=C5=0EF`=89=A5=0F"=0D=F1=03=0E=0A=A1do=139=C0pXx=E1=B5=F5=
+=AF=9D+=B8=10FCJr=84=C3=0A*=F0 =83=0D=3D8=91=C3=99=93=B5p=03=110D*j=7FNY=DA=
+=F1=80=98=16=B8)=82=9DzJU=7FL5=85j=C0=AB=B2=BC=98=0A*=D4=F0=9B=0A~=B5=BCXc4=
+=90=C6ca=DC=DE=00D=11=11=E5`=C3h=CC=9Av=03O1=18=C1=DAj=94=91=A6=83=0EBu=F4=
+=02=119=BCpl=0FE =AB=D2JT=02=E1D=0C7=E0=10=05=0F3=CC=80D=0C,,=EC^=91I=CC=90=
+=FCe=96=B1=F5=C0C=D6=D7=B1=C7=C2=0C<@=B1=03=AC=F6=A9,=9E=9D0=E8=E0=C3y=17!=
+=DC=C2D+=A40C=0C9=14a=85=99=0B=CF@=14=0B7LXBR=93>=E5=B1=80 g*rX$=97<=D5=C9L=
+=F5=8D=97=BF,=EF=85X=0A=85=B5@3=EA=AA7=A6B=0CH=F4H=18=0E@=CCk=03=13X=CEp&e0=
+=10=11C=15=91=F1=FA=C4=8ED=F0`=BC=154=B8=A4=1CQK=B80E=12R=F4=80=83=0D>=F0=
+=B0#O=D4i=BD=10O=BF=F1=90=03=13=A4=D5=A0=D1=CB*=18=1AD=0Ed=BF=FDC=0D=07]=1B=
+=C4=E0=0A=E50=04=CC:=04=3D=B9i'=F4=3D=1E=0AC=F4=A0=07CP=C9=0B=94=90=A4=9F=
+=10$=08dB=81=FC=96p=83"`)#,`=82@=F87=83=B5HJR=98=03=D0Sj=F0=1F=ABp=AEs=9E=
+=D3=0A=E8=14$=95=17=8Cn)(@=15=0B4=14=83X=A1./=FB~9=8C=0A=80=00=C3=182=06=06=
+,=90=81=E2l=E0=9D=170k=098=88=81=0D=EA=C0&=0A=F2=80=06Dp=02=B4>4&&tk=07/=C8=
+=11=10j =83=85=BDl#J=F0At=DE=F6=BE=D6=C4=CC4O=A0Zo=BE=B744=01=81=09=E7S=C1=
+=87|=C2=C3=1A=F8 =09T=C8=01H=10=85=03=19=14a$=05!L=0A=BA=A3=12=82=C8=80=7F=
+=E3=19U=CA=DA=02=B3=17=F4=C5i-P=C1=E4=AC=E3=91=17=AC=E0=04=A5Q=8D=0E=90=A0=
+=83=E4=AC@=09<=B0=C1=0E=E2=D6=02:=C1@),=C0=1C)=1D=E4=82=CC=05(=84"=F4\=A7V=
+=F0=C1=11=9C=A0=05=92"=02L=9A=D2=02;=B5=C5z0=A1=CB]R`C=D7=C5=90/}=F9=81=BA=
+=EA=84=C3=19f=01=06B=10=C2=0Fn=80=84=D4=00=D1[=17I=DB"=B3=C7F=F4=DD@=088=98=
+=01&yp=83=97=F9=E0|-=F0=81t=0C=D56)=DC=FC=E0=8B"=A1=CF=0D=92=06%=EF=F9=880S=
+=90=01=1BO=C4=C8=17=8CM=065=D8A=12"=C2=83=00=E2=C0z=97=81=12jz@=17=1D=02=81=
+=06=3D=F0=C1=0A=EF=83=B2=B6=ECp=850=B1=13lx=103=82Ha=06L=18OA*=F6I=B4=AD=A0=
+=06EXB=13=E8I=14"=B4=A0=08-=C0=98=83=96B=02=A5=BC=94=05A=8B=81=A4,=A5=CA=90=
+=89=0Ct#=B0=0DLI=B0=83=90*E=05QjKB&=8A=823=1C&=92=8Ai=96=AC=82=C9=AA=C4=0D=
+=C1=98=AF=EB=0Bi|=F8=02=D9(=E14=DB=DB^B=B0=A4=82=13=A5=C0=057=00+=0BL=C3=1E=
+{J=C1=05_=05k=16=AD=08=ADM=E2 =097X=82Hl=00=04"=DC=91=AB=D9:=9A=19w=94=84=
+=17=AC5=05E=80=01=12=86P=99b=E9`=07L=B0=02=16=A6p=04=DDdM%10=D3DO=90=83=BA=
+=80=C8=A1=0F=8D=C2=A9=FCV=85=1B=FB=04=C1=81<A=81=0CR=80!=19 =89#6=90=01=0A`=
+=90N=1D=BA$n=8Ct=81=10d=C0=02=17=1C=A1uj=ED=81=0AZ=E0=14T=DA=F4=A6a=19=01=
+=0C=18D=16=05=91=85=04h=93e_j=9B=82)=B8=C0=08Bu=02=0E=0C=DA=17=EBT=A7=B2vB=
+=01=10l=F0U`:=150=3D=90j=CD=D8=F8=9Df=C9=8EY;=98=1Fu=9A=A0=03=1E=9E=A8/t=AA=
+U=10d=F0=020=E8 V=8D=01kc\=F0=93=D5=CE=A0=07y=8BH=0F=84`%=1B=D4V{=DD=E3=96=
+=10=08=D3#=D4=02a=06/=DB=C1a=EC=D9=B0=18=B8a=8E:p=C2=10=9C@=063=FC =07=E9=
+=E4=17>=BB=E3=02=BA=04le-$$=CA=F8=B7=94=80=A9=E4=0A3P=C2ar9=AA=13=D8=C5aoq=
+=C1e=A5=99$=15=9CK]5=E8=01$=7F0=83=B0=1A=C4A=94=F2=D8MA=A0)=11=90=A0=B7#X=
+=C1=08=F8#=FB=BA=13=08!=068=BE-Qp`8=16H)=07-=98=81i=EA=85=C7'=A8=0EZ=B1=02o=
+0=A9z=BA=D7=AD=E0 =8B|=D9=89=B0=14=03=1E0=A1I=A8Y=CD=0E=8E=DC*=1C=E4=F7%=B5=
+b=CC=CD~pXc=ED=C0=07J=80=C2=0F=84=086=F9}o!0=C0=1A=BF=CE=D3I=C4=A1=00=91=B5=
+J=81}=09=03-=F5=8D=CB=0BqHB=10=8E=F5=03=D3=80=81=B5}=19Q=E9\=C0b=A6=C8=92t:=
+H=97ml=00=03=14(=85=902h=0AiM2=83=17=B4E=8A=0Eq=8BGb=C9=82=14=85=B2)$=88=C1=
+=92=99,=DC'+=C8U%X=01$I=E0=16=97=9CX]=CE=BB=CD@P=83(=1E=CC=8C1*@=16=0E=BEK=
+=DE=F0=A2=A0=08=DAUU=AD`=90=1C=D5}S2=02=D9f=B2T=13=032=84=DBfI=10t_4=BD=E9=
+=19=AC@qh=12C=0EtP=84!h!=09O=08qH=FB=84P=04#=F0=04=07=EF=93=CC=0D=ACD=19=E6=
+=F8=E0=85=84^=01=EDbP=84,=D0=A7=0A[0=02=8D=C2=A7=83=1D=1D=99=07=BCl=8Bi5=DB=
+=16=DF=A2,=D7-=86=09=0CT =86=16=80'=E6K=B9W=EBJ=80=83=A5=9C=A0=06=99.=81=14=
+L`=83Q=01=DB=A5=FD=D1=1CU=9A=9CS=05a=11#%=08=CC=0A=D0=96=C5=AAc=A4G=02a=EF=
+=1Fi=90=9C=99=C5nG=E6fsxy=B0=EEY=F9=1Bv_U=01=0C=84c=83 =FC=FA=058=F8=01=A6=
+=C9;=CC[=DBL5=93~=96=C9=C7=C5=05),!=09=C7=CA=81=82U=833=16=B0d=0DJ=00l=B8=
+=83P=A7=19=B6@G=A6A-=A0}`y=1E=EC`=9E=E2Z=C1x=01=86!=9D=E3Z=E7=A4=CB=F5=09=
+=96 =84=A4=BF=9A)*I=FDea=9DA=0D.=9D=E9Sq=FA=A6=BER=03=0A)=10=08'=E0,=16=D9=
+=A8=11k=09=C4=06R=FA=FB=01=11=B2=95=10=19=0CQ=9F=E8C#k=F2M=F66=F3rU(x=89=0C=
+g=A8=82=17=8Ca=07=BE=BA=E3o@=04=03=A1=9F=BD=B5=EBf=CC=0EwD=1F=B9=E5=C0gG(=
+=82=10.=B3=83*8=EB=05W=88A=1B )<?=B5=E0tR=ED=F7=0C=F6=18=1B=DDp=8Bg,=81=13=
+=F4N=0E=D4=DDD}=9E=E8=A5=9E=02=AA=C4=B1=B1X=0A=A5=C0=10=F4=D4S=94=80,=8D=8E=
+=C6=F8=87=EC5=DD=B3A=9B=820D=17TF=EC=B0=11=0B=FC=80m=B5@=13|=1F=8C=D8=C0`=
+=01=11=D5=ECY=0C=90=8D=E1=F0=84=0D=C4=17=898=95=E9=A0=CE=05=A5=CE=EA=E8Me=
+=E4@a=F5=80w=B0@=0A=D0=A0Z=85_=F4=09=13`=D4=84=B3=88=8B=C5 =81=E5=F5=93=0E=
+=D8=80=A1=10F=E4t=C1=DEp=DAVQ=9F=0C=B9=00=F0L=92j=CC@H Jl=BCM=CD=AD@=F6=B0V=
+=14`=8Cf=89^=1B=92=CEx=FC=08A=E8=C1=DE=C9=98=80=04NJ(=F9=C7=1C6=9B=06~=8C*u=
+=05=09pE=82=A0=C5=0A=10=16=9D=C5N=9C=09=8E=0B=D4@=DB=BCMB=F5=C0=B1H=D1=9EY=
+=92=A1=19=C4=D85=D5=0D=CA=04=0D=B0=0C=12=E2=C5=0CM=86=0DT=17=C4=D8Vi=F9=05=
+=0D=EA=85=96U=1F=AC0=D2=0F=A8F=0D=84=81=11pK=15=F0@=12,=D4=85=01=C1=B9=C8=
+=CE=9E=F8E=CC=B8=00J=DDZ^=F4=CB=89h=D5=0B=CC=C0=14=D0=11=19=D2=003=A1=8D=0E=
+=90=19a=BC=80 =B1=A1=1BJ=A3=09=B4=80=0E$=85=1EBE=05=EA=A1=B2=91R=06=F2=E1=
+=EC=F9aX=A4=00=C9=8C=80=BD\S`=9DOH=BD=17=FB=C4=06=A2=88D=F6=D4=8Ej=C0"=10XI=
+=B3=1C=E0%b=E2=EB=CC=CA=A6=01=01=BF=F0=00=12,=C1"=AD=19L=90V=F8q=08=3D=FD=
+=86=0D|=0F=0D=E8=00=13=CC=CBc=D4=C0=12=14=CB=B4DT=0A=18=81Q=FC=1D=C6=ABpb'=
+=A6=80=18`=CB=0BH=C1=8E=0C=C1-=CA=00Je=84"RM=9F=ED=D2xH#S=E4a=8B=ED=C0=A8t=
+=A36b=CE)u=E3=1E=F2a8=86=00XxE=0AH=05r=0D"=16=B1U=9C5=07i=B4=8D=13=18=C1=0E=
+4=93=88=E4=00=98=DC@=BD=B9=80=A1TU=AA=0D=E4=B9=E5=E3U=CEDc=C0=80=13=00=81j=
+=A8D=0E=10=813q=A2=0A=B4=CC=B8]=0C=0D<ej=C4=80=12=CC=1D=0F=08A=12=B8=80=0D,=
+Lc=18=93&=DA=89=9D=00=99RA=A3b=F0=90u$=C4=E0=D8KB=08=CD=19t=84C=9C=9C=A1=DD=
+=9AxH=E3M=BE=18Kac6>=E6M=02=D77J=85N=86=C0=0C=90=90=08=E0=0C=09=D4=DCY(W=11=
+=B0=C0Z-!=C9=DD=11i=CC=C7=0E=FC=93=E5=1D=CB=0D=C8A=C4$=8BiX=DD=0B=81U=87`em=
+=DA=09=0B8=C1d=94U=0C=DC=CB=F7T=C6=D9=FCI=DF=0CX=1F=8F=F0=84=9CP=0D=0Ct=A5=
+=BB=B5=07]j=08=0D=1C=81=E7=AD=CC=09\_=99H=11oD=C6@(=89G=D8=00=18=10=93=0C4=
+=81=0B=DC=CD=0C,=A0dfc=0B=95=0AyF=E6MR=E6=A5pNW=88=80=0A|=05=09=04Fc,=0C=0B=
+L=1Ba=F1=17}=F6Nex=C1[=F8=00=11<=1C=10=84=D8=DCM=06=8FP=01=15=FC=94=3DU%>=
+=DA=A6S=05#`=B0=C0=D9=18A=1E=F4=88=94=94=01pjH=0D(=06=0A=84=E1=0C=E0=80=D6l=
+O=8F=E0_=0B=88=C4=9D=BD=99=E7=C5=C4=8A=A9=0C=8E=EDH=84&=CBI8=84@p[Q$=07=0DD=
+R4=A2=0Cy=8E=8E=0A=A4=E7S=A0=90=8E=C6=DEz=82=E3*=B9=C0W=B0=00=10=B4=80O=F5=
+=91=89=DD=92=09=A0=00=F8=B1=95=0A=AC =D3lXaM=81=19 =01=10=04=01}d=C1=0F=AC=
+=17a=A0=09=0Cj=1C=ADXe=83J=9Fv=FC=F9=85O=10=01=10=C4=C0=0F=FC=05=0E=E4=E3=
+=0CU=DD=B70=C9=0DL=81=17"!=AA=E8=094=E6=C9=99=C4=CA=A9=0D=01=8Cb'F=E4=C8HH=
+=13=0C=1C=1D=8E=BA$=90f=A3=09=EC=1A=A3=EA=A8=90=82=10N=ED=A4=08x=D7=0B1=8C=
+=A6Q=86=0E@=81=9C8=01=A1=A9=00=DC=E9]=0C=E0=95=C1=0D=01=144=C1=12=1C=81=10=
+=E4@=12=FD=0E=0C=F6=00=C0=CD&=99b=A8=99=EE=C5=14pH=0A=04=01o=96=CF=89=E6=C5=
+=8Ar=08=0A=AC=A3B=91=91=9B`=07j=B0=96L=94=0E=9F=8A=07*=C6=C4=0B]=D1=87=FCQ5=
+=DA=00GH=E1j=9DG=0D\=A4=A2*=1D=A4>ELz=ABzJ*{z=05Y=EE=CF=91=9D=00=13=08A=AC=
+=C0=C0=0F=0CA=15=D4\
+=A3=C3=10=1E=98=B2=A5=E5=0DA=13H=81=B1=DC@=12=F8N=D1=08=C1N=C8=E6=82=DA=A0m=
+=EAix=A5=C8=F60=01=DE=E9E=FFD=E7=09=FCtK=F8,=81=B8LAZ=16=90=BB9=C4=82mS=0C(=
+U=FF0k=86=F8=8D=9D=CC=80=10=B4=CDw=AA=C4=F7q=EC=14nK=88=C1=C0=05)=A6=E8=85k=
+=7F=F0@=CCN=CA=B8^=85=93=F1@=FE=A1=00=0B=A0=C5T=F4=00=09=AC=80=0E=A8=80=AE=
+=84=E1=99=04=E1=10B=E8=DA=ADc=B3=08=01=B7=B4=E5=0E=84=81D=F6=80=EFT=01=9C=
+=F0=00=10=C0=00=13=94=DB=AC:=1F=83=C6=90=C3>=EC=ACl^=ED=88=19=C7=E6)=D8=86-=
+=0A=B0=A2=C4=F1=00=0B&${D=9EW2=04=0DH=C9{}l[=F8=00=DA=92=C7q=04=81=BD=80=A8=
+=B8=04F=B1=A0=816=F5KL=F4=C0=CB=E6(=CD>=C5=0C(=EE+=D9lV=A4@=09=BC'f=BA=E7N=
+=12=8C=0C=8C=80=BA=CC=80=D0=D6`_=B0=80=12=1E=9B=D4=C0=80=12=A4=13=1C=D5@=B5=
+=E0La=F0=D0=D8$=16=0D=18=C1o=F8=00=D6R=18=AD=DAL=BB=01'=DA=FC=E6=C9=86=0Cd=
+=0A@A=16=E8=C0=0A=AC=1B=EE=86=ECL=B8=00=91=D1=80=0A<=81=0D=F0=8B=F0=84=D4A=
+=08=CD=D1=C4=97w=C0=A9=F0=AE=E8=F4=C5=A5=F0yG=C5=98=C6=89=B8=C0=0F\=C1=12D=
+=C7=0E=C8N~1=C1=09P=DB=AB5=EES$=A9=B7>n=80=F0=A4=C8=FC=E4W=90=CD=DAu.c=C8=
+=00=13=90=98=BDi=929=91M=18=DE=80=1E\=D1=CB=14=C1=F9=0C,=C1=0EZ=87=AC=80=A3=
+=EC=05=F5=E6=AE=0C)F=0A=FC=06=10=05/=F5=F2EaxS=B8YT=0A=FC@c=C8=C0=9A4=D3=8E=
+ =81v=8D=87=0A0p=C0=C0D=F6JF=0A=C4=E1=0D=D4I=0A=AC=14=81N=CB=0E=B8=00=0E=84=
+S=0E=E8\=FA>=05=0AHfM=B5=EF=E6p =09y=05Z=B4=16=7FuH=84=A6@=12=18=0EI=14=DF=
+=BC=DC=C0=0FPA=84=CE=C0=11=88f~=1A=B0=CB=F4=C5=F5=F9=EA]=94=F0=F0=96=C8e=00=
+=11$=FCU=D6=16=EBI=0A=E0=CCK =05=A3=CC=D0=0Cx=81=0E(=81=8D=E0=911=E1nC9lL=
+=C4Y=8FL=94]=C6=84=0C(=E2N0=02=DC=0A=84=96=8D'=0E=BF=94=B8=F6p=A54=19=A5z=
+=C5=F5=F0=00=16=3D=A5=EBP=15=D8E=10=E1dD=0E=84=A6F\=11X=1DY=FD=D2=CA=9A=B9=
+=F0H=9CA=99=DE-=05o=08=D85=8Apjq=09=DB=09=8B=94i=ADP=C6[T=CB=DE(+=DA=C6=DC=
+=8A=B6=C0=15=10=01=1A R=8A=B5=05w=FD=91=97=B1=80=0E=90=C1N=D0=92C=3D=A0 =FF=
+=967=16rp=1D=B2{F=EE=85=A4M=11x=07=9A=1A=13=C6 =05=C6=AE=CB=1A=A9@~i2=99=A6=
+=C0=11=CCPYy=17(o1=B0=96=C8=F5=15U,3=F0Zt=81=0D4=E7S=9A=C6=EDT=AB=E2=9C1=D8=
+=BA!=AA=D4=C0m=F1=A9 =D9%=0B=1C=01CP=B2=DD@=91=86=06A,=BD@=E2=0Ar=CD=C5&s=
+=C7=1C=F2*U=AA=82pS=97=D5=EA=CB=CA=D2=0B=EC@=0C=AC=11=15W=B1=CD=A8=0B[=1D=
+=07=0E=0C=A45=A2=F38sq=F5ip=EB *=C3q=C8NT=E2L=C7I=A8=86=D1$=AB=CA=B4d!9*=
+=8D=81=ED=09=AC=88B=02=11=0BpA=13`=9EET=04!=C9=E4A=E3dB=A7=D2B;=99{N=C5=84=
+=08=12YrH[@=85=13=BC=14r=AE=00=10=04=96=82=D2=AE=87=C8=C0"=B3=D1=11=80=C6=
+=0D=FC=1AM8=DE)=8B=F4=C1=A6=8E=0D=FC@=0BA=A7=F0=E2=18=0Ex=AC=9D=F8=84=D6=10=
+=04=19=F5@=10=DC(=AA=CCt=AE=05L=13=F0=CD=1B=EEmRM=1F=0C=80=C4=D9=08f=02N=A0=
+ =1B=B5=E6=04=04=00;=
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: q-p vs. base64 test
+
+Received: from thumper.bellcore.com by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA18271; Sat, 26 Oct 91 06:35:15 -0700
+Received: from greenbush.bellcore.com by thumper.bellcore.com (4.1/4.7)
+ id <AA13322> for mrc@akbar.cac.washington.edu; Sat, 26 Oct 91 09:35:12 EDT
+Received: by greenbush.bellcore.com (4.1/4.7)
+ id <AA01130> for mrc@akbar.cac.washington.edu; Sat, 26 Oct 91 09:35:10 EDT
+Date: Sat, 26 Oct 91 09:35:10 EDT
+From: nsb@thumper.bellcore.com (Nathaniel Borenstein)
+Message-Id: <9110261335.AA01130@greenbush.bellcore.com>
+To: mrc@akbar.cac.washington.edu
+Subject: Audio in q-p
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;boundary=hal_9000
+
+--hal_9000
+Content-Type: AUDIO/BASIC
+Content-Description: I'm sorry, Dave (q-p)
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+
+=EA=EE=F9=FD=FB=F7=F1=E9=E3=E3=E2=E4=E7=E7=EC=F3=FD=FExwsomhca`]]^\ZYY[^c=
+k=FA=E9=E4=DD=DB=D7=D5=D5=D7=DA=DC=DD=DE=DE=DF=DF=DF=E2=E4=EB=F2=FCpmh^\Z=
+YYYXZ\^`dghhkmmnuy=FE=F4=EB=E4=E0=DD=DC=DA=D9=DC=DC=DD=DF=E3=E5=E3=E0=DF=E0=
+=E4=EE{jf_[YVUWZ]_bfmy=F8=EF=EA=E4=DE=D9=D8=D5=D5=D6=D9=DE=E5=EFynomiea`_=
+_cfcba^\Z[aku=F0=EA=E4=DE=DC=DC=DD=DF=DF=E0=E5=EA=ED=F4=F7~tz}=7F=FC=FE}|=
+=FE=FDzzvpmlou~=F4=EC=E5=E1=DF=DE=DF=DF=E7=ED=EF=F6=F7=FA=FExqmf^XSQPQSVW=
+VX[cu=F4=EA=DF=DC=D8=D3=D0=CF=CE=CE=CE=CF=D1=D5=D7=D8=DB=DE=E5=F4tf]XRPQR=
+SSTVY]bo~=EE=E7=E1=DF=E1=DF=E1=E5=E7=EA=EC=EA=EA=E7=E4=E5=E6=E5=E8=EF=F4=F9=
+{kee`\^___afgkpz=FB=F2=EC=E4=DE=DD=DC=DC=DC=DE=E3=E6=E9=ED=FEonjlrplhgmlj=
+npqsz=FA=F3=ED=E8=E8=EC=EE=EE=ED=EC=E4=DE=DE=DE=E7=EE=F4xlb\YWWUVX[]_bhs=FE=
+=EF=EB=E8=E2=DE=DC=DC=DB=DD=DF=DF=DF=DE=DC=DB=DC=DD=DE=DF=DE=E0=E8=F6voic=
+_^[YUSRSVW[\]^dp=F1=E3=DB=D6=D3=D1=D1=D1=D1=D5=DA=DF=EA=EF=EF=EF=F5=FFwkg=
+c]ZXVUV[an=7F=F7=EF=ED=ED=E9=E6=E5=E4=E4=E3=E5=E5=E9=F3=FB=FCzmjjihjlppqz=
+=F7=EE=E8=E1=DF=DF=DF=DF=E4=EA=F8vtqs}=FDzwskihddgijhilrz||}=FD=F5=ED=EA=E8=
+=E8=E3=E1=DF=DF=E5=E7=EE=F0=F4=F4=F4=F9=FA=F9=F7=FEnia]\YWUSTY^dmz~=FC=F1=
+=EE=E8=E0=DC=D8=D6=D4=D2=D0=CE=CE=CE=CF=D0=D3=D6=D7=DE=E8=F8vj`[VSOLJIHGH=
+HJLNQV[d=7F=E4=DA=D2=CC=C9=C8=C8=C8=C9=CA=CC=CD=CE=D0=D3=D6=DA=DF=E9=F9yu=
+oha\VRONMLLLMPW]gr=F6=EA=E1=DD=DB=D9=DA=DB=DB=DB=DA=DA=DA=DA=DC=DB=DE=E3=E8=
+=ED=F4wqjc`_bdbda_bged__cccchr=F6=EA=E6=E3=DE=DB=DB=D9=D9=D9=D7=D5=D5=D7=DA=
+=DF=E7=EE=FCrrnd^[YTSTVW[^biv=FB=F4=F2=F0=EB=E9=E6=DF=DC=DB=DA=D8=D9=DB=DD=
+=DF=E3=E7=EC=F0=F9=FEqhd__^\ZWUUSRRVZ^do=F3=E7=E0=DC=D9=D7=D4=D3=D1=CF=CE=
+=CE=D0=D4=D7=D8=DA=DC=DF=E5=EC=F9}umic\WVTSRNLKLNOUY]bio=F9=E9=DE=D9=D5=D1=
+=CE=CE=CE=CE=CF=D1=D4=D7=D8=D8=D9=D9=DA=DB=DD=E6=F6tdYSONMMLLLNPSY^dk|=EF=
+=E6=DE=DA=D7=D2=CF=CC=CA=C9=C7=C7=C9=CA=CC=D2=DB=E8xcVOLIGFGHIJKLNOT[_q=EC=
+=DE=D5=CF=CB=C7=C4=C3=C3=C3=C4=C6=C8=CC=D1=D9=E3=F3i[TOLJIHHILNQV[ao=F6=E7=
+=DF=DC=DA=D8=D7=D6=D5=D7=D8=D9=DB=E0=E9=F2=F9=FDxnjjfehlmlmrpoqw=FF=F3=EF=
+=EF=EF=F4=F2=F0=EC=E7=E6=E3=E0=E0=E4=E9=EC=F5|og^ZWVVUSRSTW[_n=FA=E6=DB=D7=
+=D3=D1=CE=CC=CA=C9=C8=C8=CA=CC=CF=D5=DB=E8x`XSPNMKJIJLMNPSY]f}=EE=E5=DD=DA=
+=D7=D4=D2=CF=CE=CE=CE=CF=CF=D2=D7=DC=E2=EB=F7zpjea^ZYZZZ[[\]][Z[]agr=FA=EA=
+=DF=DB=D6=D2=CF=CE=CD=CB=CC=CD=CF=D6=DC=E6=F6se]WRNKJIIIJMSX\aq=ED=DF=D8=D2=
+=CD=CB=C8=C8=C8=CA=CB=CD=CF=D3=D8=DD=E2=E8=F2ug_[VPNOOOPPPONMMNQV^k=F4=E0=
+=D7=CF=CB=C8=C5=C3=C2=C2=C4=C7=CA=CF=D6=E1=F6i\UOMJHGGGIKMQZfz=E7=DE=DA=D7=
+=D6=D5=D2=D2=D3=D4=D5=D6=D6=D8=DB=DE=E0=DF=E4=EA=F1=FEsib\YUTSTTUXY\`m=FF=
+=EE=E6=E3=E0=DE=DD=DF=DF=E4=E7=EB=F6=F8|=7F=FD=FB=F1=EE=EE=EE=EF=FA~~|uun=
+jooox=7F=FFxyutnmms=FA=F3=EC=E6=DF=DA=D7=D3=D2=D4=D6=D9=DE=E9=FDi]XTSROOO=
+OQTW]`jr=FE=F1=E8=DC=D7=D6=D5=D5=D6=D9=DC=DD=E0=E4=E6=E9=EB=EE=EB=E7=E1=E2=
+=E8=EB=EF=F6|qjig_\[ZYXY\]_hy=F3=EC=E8=E5=DF=DD=DD=E2=E7=E6=E5=E7=ED=F6=FB=
+~vmhfb^\[[^beq=FC=EA=DF=DB=D6=D2=CF=CF=CF=CF=D0=D3=D7=DE=EA=FEk`]YWUQONML=
+KJJMOU[dz=E7=DD=D7=D2=CF=CD=CD=CC=CA=C9=C8=C9=C8=C8=C9=CB=CE=D2=D6=DB=E7=FC=
+k]WSNKIFDCCDEGJNSY]`p=E9=DF=DB=D7=D4=D0=D0=CF=CE=CE=CD=CD=CE=CE=CF=CF=CF=CF=
+=CF=D0=D3=D7=DA=DC=E4=F9l_[WROLKIHHHILPVZ]h=F9=E8=E3=DF=D9=D1=CD=CD=CE=CD=
+=CC=CA=CB=CF=D6=DC=E5=F6sg]WSQRTST[fgban=EC=E2=E4=ED=E5=DB=D7=D8=DE=E3=E0=
+=DB=DC=E5=F7ptld\Y[\]]]cl=FF=F6=F3=EC=E7=E4=EE=7F=FA=EA=E1=E2=E9=E3=DC=DC=
+=E0=ED=F5=F1=F2=F3=F8z}=F8=FB=F1=F8vomptjb][]a]WRRV[][\n=F9=FC}=F8=E7=DE=DB=
+=D9=D6=CF=CE=CF=CF=CE=CE=CD=CD=CD=CE=D2=D6=DC=E5=EEyjaWONNQOMKKMOQSWZ]_dj=
+u=EF=DE=DC=DB=D9=D6=CE=CC=CE=D8=DC=D9=D9=DE=EE=7Fz=F0=F7loz=FC=FFnikv}np=F2=
+=EE=F9vq=F1=E8=ED=EF=F0=F6=F8=F8=F2=F8=F8ujtyi__iodVR\hb]bp{tdds=FD=F9=ED=
+=DF=DC=DE=DD=D9=D0=CD=CE=D1=CE=CB=C9=D0=DC=DA=D8=DFgUQRUMGHNOKIJOY[V_=E2=D2=
+=CF=E3WP=DB=C2=C7=ECa=D9=BF=C6=DE=C7=B6=B8c9B=CD=B5=BC=E5w=DDjG;25Be=EBmV=
+EBEGJ`=E0=DB=C5=B7=B2=C5F>_=BC=BA=DAOV=CB=CCTEQ=D1=C5=D2=FB=EA=CE=C9=E0U]=
+=DF=D0=D5w`mp}YEGHg=CFVEBL=D9=F0UIONA=CD=B9=AA=B32+/=CF=A9=AD=B7=C3=B4=B2=
+o/'.F=C8=BD=C1=C3=CF=EDG;:Em=CE=C2=C3=C2=CC=ECRA>?KZd=FF=DC=D5=E3cXn=D9=CE=
+=CE=CF=CF=D0=DA=ED=F7=E5=D9=DD=F5z=E8=DFvYXbyo][gjVMOTXWa=E5uk=DF=DC=CE=E1=
+]=EA=CB=BEY?=C5=BB=B7=BF<37k=C7=DA=BA=B9=BF=C1O0-6AX=D8=C3=BE=C2=DFE?GUh=D4=
+=C3=BC=BA=C4=DCSB?AMq=ECd=DB=CC=DDeP=EC=CE=C7=D6_=D5=C8=D9^]=E7=DA=E2\[=FA=
+=E0mZfs=FBb_dp^KLOc\K`=DD=D7=F4[=DA=CE=D8pL=CB=BC=BD=BFJ<;N=CC=CF=BB=B5=B7=
+=BBu:/5<J=EE=C2=BC=C1=CFUE@DO|=C9=C0=BE=C4=D7]D?=3D?G[=D8=CD=E8u=CE=CB=E8[=
+t=CA=BF=C6=EC=EF=C8=C9wOi=D6=D9cPc=EAkLKb=F1^IK`uVIKf=E4te=F2=DE=D3=D4=D7=
+=D6=EC=E7=C8=BE=B5=BFH8?=DE=CB=D5=C7=BB=B3=BDP618:>Y=CD=BF=C4=E2TIILMg=CE=
+=C0=BE=C6=D6mNF@BKZ~=E1=D2=C9=CD=E8p=E4=C8=CC=E1=EA=CB=BE=C5{X=F8=CF=EFNO=
+=FE=E0^JK\[JDLXYOM[=FEqiw=E5=DA=CE=CA=D2=D7=CB=C8=CA=DAW=F2=C4=B7=BAY:<o=
+=D7mU=D4=B8=B7=DB>5;<;?]=CC=C4=CFeOR]Za=F5=CD=C0=BF=C6=D1=EB\OKMS_u=DC=CC=
+=C6=CB=DD=7F=EE=DA=E0la=F0=D9=D9sUUa`VSZgum_^cf_^ao=F8=EF=EE=F2=EC=E6=DF=D7=
+=D1=CF=D1=D4=D4=D6=D9=E2=F1gZm=DF=CF=DBWECO_fgw=D8=CE=D3=F7TKIO[m=F7=F4=ED=
+=EA=E2=DF=DE=DC=D8=D4=D1=D4=DC=E7ue^]^]XSQTZ_m=F1=DE=D7=D4=D3=D1=D1=D3=D9=
+=DE=E8=EE{f_an=FB=F8xoruoia]]][ZZ\_j}=EC=DF=DA=D9=D8=D8=D7=D8=D7=D8=D9=D9=
+=DE{k=ED=DA=E3bNJN\]VYl=E3=D9=D9=EEc\\amujghms=F0=E3=DA=D0=CD=CB=CC=D1=DC=
+=E5=EC=FBk]UMHFGKOYe}=E1=D4=CD=CD=CE=CF=CE=CE=CF=D6=DE=E6=EF=FBu|=FC|=FEx=
+jb^ZVUROPPQSW\_k{=EC=E4=DC=D5=D2=CF=CF=D5=DC=D1=C7=C7=D4=F9`\ixk~=DA=CF=CF=
+=D7=EE]QQPPQNKKORWb}=E3=D6=D0=D2=D5=DA=E0=E5=ED{e^[SSUYd=F4=DF=DA=D0=CA=C8=
+=C9=C8=C8=CB=CD=D7=EFpja\[\YWWSRSRQQSSTVX[fv=F7=E8=DC=D6=CE=CB=CB=CC=CB=CA=
+=CA=CF=DE=E6=D7=CF=D4|PJJJKLT{=DC=DE~]RONQ[dlnlo=FE=E6=D7=CE=C9=C4=C2=C3=C6=
+=CB=D1=DA=E5w_QJEDDDEHO\o=E7=DA=D1=CB=CA=CC=CD=D2=DA=DF=E7=EB=E9=E9=EC=F2=
+=F6=FB=7Fxroie]XUSUVW[]`go=F8=E9=E5=DE=D9=D7=D5=D5=D5=D8=E2=E4=D9=DB=E3=E0=
+=F5o=FDeYcpy=E9=E4=ED=F5p_XSRRVZXTW]dx=EC=DF=D8=D5=D6=D5=D2=D2=D5=DA=E0=E9=
+w_YY\eq=F7=E8=DD=D9=D9=D7=D4=D5=D8=D9=E4=FBl^YUUUWYZZY]_bjmppnku=FE=F7=EC=
+=E7=E2=DC=D9=DA=D9=D5=D0=D2=D5=CC=DAai=F3=ECg\fjphVNS[\[]q=F9z=FC=F3=FA=E7=
+=DB=E1=E7=E1=DE=E8po=F1=E9=E8=F2=FE=F7=EC=F6g^g=E6=EE`l=F3=F5=EEe[=FA=EF~=
+ia=F9=EDm=FDn=F0=E3{=ED=F0=E1=EB=F1{|=E8rl=FDp=EAs=FDz=FD=DD^=DFqz=DCZ=DC=
+wt=DDR=F4U]cT=F7e=E1=F6zm=EE=EC=F4=DD=FA=D2=DF=E0=DA_=D6=FA=EB=E3=EE=E4=FC=
+=F8jyx^}^|=FB^]`[d]Zk`a]ckk=F6a=EFzy=EBv=DD=E7=DD=D5=EA=D5=E9=DC=D3=E2=CA=
+=DB=D0=DA=DF=DF=E1=F6=F0=EAy=EFn^\LbSkmS=F0L_TUePh\l=EE_|w\=DFo=E3=E2=EC=D0=
+=DF=D7=D8=DA=D7=D6=D3=D5=D5=D4=EB=D4^=E0j_=EASyTg\[]KZM[ZU=FFW=EA_=E1=EA=FA=
+=CF=FA=CC=DF=D2=D3=EC=C6=F7=CE=DB=D9=CD{=D0a=D5^=F5]TiKvIaWIfG^NQoN=D9^=E8=
+=FE=E6=D8=E4=D9=DF=D3=D7=D0=D8=D0=D4=D3=D8=DC=DC=DC=E9=ED=F5=EA=E6hk_^ZVS=
+XO\RVOWOfVd=FF=F5=DF=EF=D9=E5=CD=DC=D3=D7=CF=D2=D3=D9=CF=E6=D9=F3=EB=E8_s=
+NkKbO_[d[_=FC^wxw=E7=DF=EB=DE{=D5c=DBq=E0=DC=FF=DEx=DF=E3e=FCX}]=F8sl=E7_=
+=E5^=EFZl=FEe=DDW=DDo=E4=DD_=CE=FF=D1=E0=D6=D5=FC=D6ml=EEO=F5L=F1SYdJ=FFH=
+rN=FFXnmy=F2=ED=DD=EB=D2=DD=E2=D1=DD=D2=D7=E3=D7=E5=D1=F4=DB=E9=EB=E8g=E7=
+Tp\crfdjf=FB[gUgeZ=ECZ=E8i=EAy=EA=EF=FB=FC=EF=F6u=E7=F6=EEm=EBk=E4=FD=E0l=
+=DC`=E9agkq=E9|=DEz=DA=E4=DE=EB=E0=E6=DDu=E7f=F2s^[h^hcW=EAi=F8tk=E4=FCk=DA=
+X=D7W=ED=FE=E7=DC]=D7g=DB=F4r=E0g=E8va=EEb=FCW=DFb=F0y=E7=ED=FF=F1=EC=EF=F2=
+=F4=EDm=E8ok[=F5{Z=E5L=E0]u^\=E0=FE=DBi=DF=DE=D6=DC=DF=DD=E9=F9=DAh=D0k=E9=
+n=DF=F1b=F7Z=DAV=F1\\=EES^S^_Q_Qlq]=E2{=D4=F6=E1=DC=EB=D9=DB=F6=DC=EF=DA=E3=
+=E7=E7=E3=D9=E0=FA=DC=EEy=E0Z=EE=E6|=F6\=E4omzOVoM`N[=F1Y=DAT=E4=F6t=E9Z=D8=
+e=D3]=EA=E5=FF=DCo=F0=D9=D3=EE=E1=D7n=D3m=F8=F8kja=EEh=DBx=EA=D2f=D1i=EBr=
+gtqVQ^QoN_Uea[]`=D4h=D7w=F3=DF=D5~=DC=DE=E5=D5=D8=F9=D0=EF=CEx=D5=DA=DA=F0=
+ocxm=E7=EFV=EDaqoJfM^=FCRmV=EFu[=7FU=D2X=EAjg=E4=E3]p=ED=DEu=E2=E7=D8=E0=CE=
+=DC=D1=E0=CF=DD=D9=D4=E6=E6=DEl=DAh=F8=EDR=F8K^PZYITNR=FEIyao=FCh=DD\=DD\=
+=DB=D6=F3=DE=D7=D6=D8=DD=DF=CE=CF=D7=D3=EA=C2=EC=D4=EB=EF=DBT]i\=E4JXAQLF=
+TLj\=E3n=EFhm=EB=E6=DD=EA=DE=D5=D4=D0=DC=CE=CB=D1=CD=D8=E2=DA=DE=EE=DFjid=
+Y]MNJONIXQYOOZ=F9p=E3j=CF=D5=CA=D7=D2=CE=D7=CC=E0=D2=D0=C9=D9=CF=DB=CE=D3=
+=EAUvi=EEYKS`pMMI_[POKSWY]X^=7F=F0=FE~=FD=DC=D0=DB=CE=CD=CD=C9=CF=CF=D0=C6=
+=D2=CE=C9=E0=CC=F0pmjsSYJMVNLBLPLTG\^=F0_o=E9=DF=D4=E9=D8=DB=D6=D1=D4=CD=D0=
+=D7=D0=CE=CC=D6=D4=F6=D8=DD=E2=EBbej=FA^XRNXUNJHLR\S]u|zit=DE=D8=FE=EE=D4=
+=C7=CB=E6=F4=D5=C2=C6=D5=E0=D8=CB=D5he=EE=DA=E6XNX~gSNN^SHJMNW_PZ=D9=D9=E0=
+x^=FB=D3=CA=D9=EA=D5=C8=BF=C7=DE=E2=D1=C7=C9=D5=ED=E7=DA=E1hVQW[UJFIHIFDE=
+KSNP^=EF=DFx_=E4=CB=C2=C4=CC=C4=BC=BB=BD=CD=E0=BD=B9=C8=D5=EDdmH8Ab=E3=E0=
+iORI:303;FOi=E6=CF=C7=C4=C7=C5=C0=BF=BE=C3=C2=BC=BC=D1LDw=C6=CAfLx=CA=D1=
+R@L=DF=CE=E6\X^XIBBFN^]Z^e=EA=E6mQU=FB=D9=CB=CF=D1=CA=D5=E9=C3=BE=BF=BB=CE=
+L>Fe=CB=BC=BE=C4=C6=CEZ</,/7AX=ED=D6=CF=CA=CE=DB=EAv=E8=DB=CD=CB=CD=CB=C6=
+=CCoPHRagfj=DD=D3=D7=E2=E7=DD=D2=D3=E0o\SNNLO]=FA=EA=EA=F5ueh=EE=DD=DF=EF=
+iZ^=E2=C9=C6=D1=E5m_=D4=C1=D3b=FD]B?>Ao=BE=BB=C6=CDo?776:K=EE=C7=BB=B8=BC=
+=C5=CE=DA=E1=DE=D5=CF=CB=C7=C6=C3=CFY@9;BLLNd=FA=E9=E8=F9=FE=EC=D7=D4=D8=DF=
+{ge=7F=E7=D7=D0=D5=DA=DF=DE=F1_RSm=E3=DF_LJQq=DB=D1}H=3DR=C8=C7=C7=D1K><L=EE=
+=C5=B2=B6=BE=BF=DEF746>d=CB=BF=BC=BE=CB=E1=EDk_|=DB=CE=C8=C5=CA=D2=E5O=3D9;=
+AKX_m=DD=DD=E2=E5=EC=DD=DD=D8=DB=EA=ED=F6=EE=F5=E5=D8=D4=D2=DE=FFc^]VTYg=E9=
+=DDsSLN[=FD=DA=DC=EF_Lu=C6=C5=C3=CDT>9D{=C2=B0=B3=BD=CAj@789@q=C7=BB=BB=C0=
+=D3YOPO`=E4=CF=C7=C6=CB=DA=F6J:8;IYo=EA=EC=D3=D0=D8=D7=D9=D6=D3=CF=D0=E0=
+=F0ut}=EF=E5=F5|ja`ZVOOWZ`{=F5n_UQ]=E7=D1=CE=D2=D7gn=C4=BF=C2=BF=E4D:=3D^=CA=
+=B2=AF=BC=CD_?4479J=DF=C5=C1=C9=D9XKPVk=D7=CB=C8=C6=C6=C9=D9VB>CUjl=F1=E0=
+=D8=DB=DC=DB=D7=D1=D3=D8=E3=EAu]^`=FD=DD=DC=EEcYVUPOQS]o=EF=F0`]^`=E8=DC=D7=
+=D7=D7=D4s=DB=C0=C2=BF=CBN;7E=EC=C1=B2=B6=C3=E1M<58;Bn=C9=BD=C0=CA=DFON\l=
+=E4=D6=CE=CC=CA=C7=D3YA=3D>DQU\=EC=D3=CF=DA=D8=D9=DD=D7=DB=DF=F1=E8=E8m~=EC=
+=E0=E2=F4yce`TOR_q=FFxlfhkis=EA=DE=E5=E4=DCj=E4=C3=C2=BF=CDI:8M=D4=BF=B3=
+=B9=C6=DDO>56<H=E2=C4=BE=C9=DCrMNXa=EC=DC=CE=CA=C9=C9=EFH?AM[^]j=DB=D4=DA=
+=DE=DB=D1=D1=D5=DB=EE=F1=EF=EB=E7=E0=DC=E3=F5zovmaVNTcqhZW]fnlegppfV=E4=C2=
+=C1=BE=CCL<;T=CB=BC=B3=BA=C4=D2[D97=3DI=F7=C8=BE=C4=D6=FD_]dj=F5=E1=D9=CD=CD=
+=D1=FEF;<GVTOX=F5=DC=E2=E8=DF=D1=CA=CB=CF=DA=DF=E3=ED=EB=E1=DA=D6=E0=F7=FB=
+=EF=E8z]X[hh^ZRU^mvbYXYX=FA=CB=C7=C6=D6L>>X=C9=BC=B7=BA=C1=CBrI=3D:@O=F3=CB=
+=C4=C8=D4=FAd[Z]i=EC=DF=D4=CF=D5sIADHNOQg=E4=D6=D7=DA=CF=C9=C7=C9=CE=D5=E1=
+zeao=F0=F6xy=F8r[ONRY`d`_eo}=EB=E4=EA=E8{]=DF=C7=BE=BE=F0?<H=DA=C3=C2=C2=C2=
+=C2=CC_@76=3DN=EA=D0=CD=D1=DC=E8=F3wj_n=E5=D7=CD=CB=D7YEFR^TLU=E5=CF=DAjk=D9=
+=C8=C7=CC=CE=CE=D6=E8l`]]l=E9=DC=DF=EFiXPUZVOMS`ldd^MU=D8=C3=BD=DB?;L=CE=BF=
+=C1=C5=BF=BA=BC=CDU=3D:@R=F6=DE=DB=D9=D5=D1=D7=EBcX]n=EF=E7=E8xSECKRROY=ED=
+=D3=D2=DC=DF=D8=CD=CA=CC=CF=D2=D4=D9=E9ynr=FB=FCsi]UR\rcNISvyZTUd=DD=CC=C3=
+=CCN<E=DE=C1=C3=CD=C8=BC=BA=C7dA<>I_=FC=F9=E4=D7=CF=D3=E9k^`=FC=DD=DC=E9=F6=
+=F7gOFGMX_i=F0=DC=D9=D8=D3=CE=CB=CC=CF=D3=DA=E6v_Z\dq=F2=F0=FFk\\i]MIL\xx=
+^Y=E8=CB=C2=C9V<?y=C7=C5=D0=CE=BC=B6=BD=E5G>BINQTj=DF=D3=D4=E4=F6=F9=EB=E5=
+=EB=EE=E8=DE=FBRIHJMO\=FA=E0=D9=DA=DA=D3=CB=C5=C7=CD=CF=CF=D6=EFf`hl_[]]Y=
+V[\QLR_cWONi=D2=C9=CDoKO=E3=CB=CC=D6=CD=BC=B6=BE=DDQKOMHFLe=E3=E2=FAm=FD=E1=
+=E4rk=E8=DB=F9[Y\TNOYglgm=EA=D8=D4=D5=CF=C7=C2=C5=CB=D4=DC=E1=F0{gguj`n=EE=
+^KLX\SR\V]=DC=D8=FDQDESu=F3=F7=D7=BF=BA=C1=D2=7FgaOHHU=E6=D7=DE=DC=CF=CD=D1=
+=D7=D0=CA=CE=FBc=F4=FEL?CQXKBL=EC=DCn_=DB=C7=C9=D4=D4=CB=C9=D1=E9=F5=EB=ED=
+vkt=EB=DF=EEh`ce\TXWJ]=D6=E5[PRYVTZ=EA=C6=C1=CB=CB=C9=D2uSPOMQXby=E5=E2=DE=
+=CE=C6=C8=D7=D2=C6=CB=F6XUWOF?CQSOX=FC=DE=DF=E0=D6=D3=D8=DB=DA=DC=E4=E4=DF=
+=E7=E1=D6=CE=CE=E1=EE=DA=D9=EEn=E8=E8Tk=D9cUQMMFAH\pZ[=E4=D3=F1X[uaMOt=F2=
+q=EB=D4=C6=C1=C5=C4=BF=BD=C0=C9=CA=D2=E4eNIFCBFJMXl=EC=EB=F8=F1=E0=DC=E5=FB=
+=F7=EC=7Fh=FC=E7=F4ff=ED=DD=E6=FB=DE=D7w=DB=CC=EC~ra]OJNg=EF=FD=EC=DA=D5=ED=
+nkd\VV[YYeo=E5=D8=D4=CE=C9=C5=C8=CA=CB=D0=D3=EBdXMHCBDKOXi=EB=DC=D5=CF=CD=
+=CF=D2=D5=E0=FE|d\\UT^n=ED=DC=F2=E6=C5=CFm=EA=DD=F2h[]=E6=DC=EA=DC=CD=CD=D4=
+=DE=EFzePLLHDFHKWV_=EF=EC=DC=D3=D4=D6=D6=D2=DD=F4l`_]]f{=EF=E1=D9=D2=CC=C7=
+=C4=C3=C2=C4=C7=CD=DF=EBiQPICFHKJIsmO_iZTTVb~=EF=E5=D9=D1=CE=D0=CC=CC=CC=CE=
+=CF=D0=D4=E8=FEz_bj`o=FA=F2=FDpm`cZONMHGGGJOXo=E2=D5=CB=C5=C3=C0=BF=BF=C1=
+=C4=C9=D1=D9=E0lVYVOMXwTY}fUUX]Y^jw=FB=E4=E6=7F=EE=E7=FCw=EC=EA=E9=EB=E1=EB=
+=F5=E0=EC=EF=E1=E6=DE=E1=E4=E3=E9=F2jZTNKIHGJLS\l=E9=D7=D0=CD=C9=CA=CA=C9=
+=CA=CD=CE=D2=D9=DE=F9}=EDaj=D4m_=DD=FAZc`Y]idowvk_\]YVbd]etea=FEsl=EC=EC=DF=
+=DE=E1=DD=DD=E2=DF=E8zlj[YYZ]en=EE=DE=DA=D4=CF=CF=CF=CF=D1=D7=DA=E3=EB|_[=
+[UTOf=F9Xj=E0=F2=FC=E2=F2=F0=EC=F6{=F2=EE=EA=F9|=F6=F2omve\^d]]mof=7F=EB=E6=
+=EC=E6=DB=E6=ED=EBtcf_[]\\`ho=F3=E8=DD=D4=D4=D1=D1=CF=D2=DA=D9=EEpng_[]YR=
+^=E9jb=E3=E7=F7=E4=F1=FD=E8=E2=EB=E1=DD=DB=DC=E4=EA=EF=F9qtkc^_[[\Y_a_emj=
+eimsonla]]Z[_g{=EE=EA=DE=D2=CE=CC=CA=C9=C8=C6=CA=CD=CE=D5=E0=EA=FCo]VTSWS=
+RYYVZ]]\]_lyw=F3=E8=E4=E4=E0=DF=DF=DD=DE=E0=E1=EC=FA|of^[Y[\^fkkr~=F8=F4=F5=
+=ED=E5=E1=E6=ED=F4~=FD=F6=F2=ED=E7=E2=DE=DC=DD=DF=DE=DF=E3=E3=E8=EE=F5yss=
+upkhgfcaaa_ccdhkruusroux|=F6=ED=E9=E5=E3=E1=DE=DE=DF=E2=E8=EF=F4=F9=FDztm=
+igd`^^^^]]]^`k~=EF=E5=DE=D9=D5=D3=D3=D5=D6=D8=DB=DF=E5=EB=EE=EF=F1=EE=F3=FD=
+}{}xyz}zsuoh^YXXXXXZ^aimoptzwsqnv=FE=F6=EF=EC=E9=E4=E1=DF=DC=DA=D7=D5=D5=D6=
+=D7=D9=DD=E2=EB=EF=FC|=FE~||umkgc_\ZXVVX[]_cioz=F4=E9=DF=D9=D6=D5=D5=D3=D5=
+=D9=DC=DF=E7=ED=F7qjd^]\ZXXWWWY[]cn=F9=ED=E8=E0=DE=DE=DC=DC=DD=DD=DD=DE=DF=
+=E1=E2=E2=E6=EA=EC=EF=EF=ED=F0|rlg`^]\]^_bdhox=F9=F2=EE=E8=E1=E0=DE=DC=DC=
+=DE=DF=DE=DF=E2=E8=F0=FEphc^]\[ZXWY[_el{=F9=EF=EC=E6=E4=E4=E4=E4=E7=E8=E7=
+=E8=E7=EA=EB=E8=EA=ED=EF=F7=FFzxqmkijmoonlklmoz=F6=ED=E6=E1=DF=DE=DE=DE=DB=
+=D9=D9=DB=DD=E0=E4=EA=F2zf^YWUVTRRRSTVWZ_hkx=F3=EB=E0=DB=D7=D2=D0=CF=CF=D1=
+=D3=D5=D8=DB=DF=E5=E8=EC=F2=FEuvvunkd^\ZXWWWWY\_bgn=FE=EB=E0=DC=D8=D5=D4=D2=
+=D2=D3=D4=D7=DB=DF=E8=EE~oe][YXY[]``_^^^]^ahp|=FA=F0=E8=E6=E3=DE=DC=D9=D6=
+=D4=D3=D4=D6=D7=D9=DB=DF=E2=EE{qie`\YVVUTTUVZ]ajv=F8=E9=DF=DA=D7=D3=D1=D2=
+=D5=D7=DB=DF=E5=EE_^=CBdDLQ=D3=DAi`_=EF=EBgmfiZ=E4=D3=DFh=F1=E4|=D7=E6=EB=
+aTS=CA=BF=EFfH`=E8`h=F5k=F5i=DE=ED=F4TS=F9eji=DD=F9~dw=D6=FCTb=EA=DF=DA=E8=
+=DC=D0=DFj=DA=E0=DEn=F2=DA=D1=DF{=EA=EA`WSYNNLS]NPY`ng=F6=DD=D7=DF=DE=D3=D6=
+=D7=D5=DA=D5=D6=DC=DC=DB=D9=DC=D7=DC=DD=E5=EB=F5sb]fUUNNMKIKNQU]by=E4=E4=DD=
+=DB=D5=CF=CE=D3=D2=CF=CD=D2=D5=D6=CF=D9=DE=EF=E5=E8s_fgWQQTQNNSZVV`rpj=EF=
+=DA=D5=D8=DA=D2=CF=D6=DD=D3=D9=EBz=F5=DF=EB_[y=EEndn=EFx[Z^\X[\PTh=F0=E7=ED=
+=DD=CA=C6=CE=D6=D0=CE=D7qetbNKORMLY|=F7=EE=DA=CE=C9=CC=D6=D8=D3=E1_SY\WUU=
+J\=CE_=E5=C7sa`\=EB=EA]=E7=C7=CC=D8=E5=ED=D8=F8NU=FDiX[=EF=DA=EB=EF=D8=C9=
+=CC=D7=D2=CE=D9nZPH>879>HEL=E2=CF=C3=BF=BE=B7=B3=B7=BD=C3=C6=CEhJOSJEIWc]=
+e=EC=DD=EDf^^p=7FM?NNIOOh=C9=D1[=CC=B4=BF=C4=C7=DD=D6wO\=E3=DE=F2=DC=C9=CC=
+`QaTD>ENOIT=F5=DD=E0=DF=CB=C5=C9=CD=CE=D2wNGFC<=3DLfd=F3=D7=C3=B7=BA=BF=BB=B9=
+=BE=C6=E0YRRRH@M_oj\e=DE=DEg\fiYJHLe[K]=EDnr{=DD=CB=C6=D7P=E3=B5=C4Kc=CF=EF=
+N?J=D2=C3=DB=E2=BC=B8=D9Q^`J?@Y=E8X]=CF=C6=CE=D5=CF=C6=C8=E1b_^F<<<<=3DJ\bY=
+=D5=BB=C2=D6=C6=B9=BB=C8=E1=DA=C7=D1VY=D6=D0^[=E4=DDnSVrcHIXSHFKSUQSo=F1Y=
+^=D9=D5=DB=D4=CA=C8~=CE=B6=C0\`=C3=C4a?Q=C3=BB=DEk=BF=B9x?ARD67Q=FDOR=D9=CA=
+=DDj=EE=D3=DE[UqmMFOWTPa=D2=CA=E5=E1=C2=BD=C7=CE=CA=C3=C1=D0=7F=DA=CC=FBQ=
+Zq_OJTZKBMZNIM\`UTl=EDw=EC=CF=CF=CF=CE=CD=CC=CE=CD=C9=C7=CF=D5=CFZ=F8=C4=DF=
+JU=D8jD=3DD=EB=D2QS=CC=C6R>APF;?=ED=CF=E1=D4=BF=BC=C7=D0=CA=C4=CE=EC=EA=D4=DF=
+aT\_OO^fOR=EF=D9=ECs=E9=CE=D5_[=ED=FA\OSgh_h=DF=E4=E9=DC=D4=DE=E5=E7=DB=E3=
+sa=EE=EBni=F9=E2=E1{=F4=DF=FB`{=E4nZo=EAo=3DD=C0=C6D?=DB=C6`:?=D1=BD=E6t=BC=
+=B5=CCTX=E8^>B=DD=CAks=C9=C3=DFVj=DCwNQ=F6=FBOKU]LFP=F9jP|=CC=CB=D2=CF=C5=
+=BE=C5=DB=DA=CD=D6{ksxe_`d_\^[OMTSLMX\\^j=F6=E5=DE=D9=D6=D4=D0=D6=DB=D5=D2=
+=D2=CB=CB=D0=D0=E2Q=CE=BD=FDCS=DAx>6F=DE=F2GZ=C7=CAOCQjI>S=D3=DC{=CF=BF=BF=
+=D0=D5=C7=C4=DB~=DA=D5dOU[ODL=FFjHK=DF=D6p\=E8=CE=CF=F7=F3=D5=D0=FDi=E1=DA=
+=ED=E2=D7=D2=DFuv=F4ePNWZTYgbZ_|=F8ig=EA=DD=F3y=F5=E0=E0=E4=F0=D8=CB=DF=F6=
+=D6]=EC=BE=CFW_=E7=DFU>D=F4=D1n=F9=C7=C6=F4PNUK=3DBq=ED`=F3=CF=CB=D6}=DF=CD=
+=D8w=EF=DC=F1\Y^XLNe=EF[S=DD=CF=D6=D5=CC=C5=C1=CB=D7=CE=CE~y=EFbXjke`WXjW=
+LU^PNRTUQTanp=EF=E1=DE=D7=D4=DB=D2=CD=D1=D2=D2=DB=CF=D0=DB=E3\=F4=BE=D1P[=
+=EF=EFUBEg=E4n=FD=CD=D0~XOJE>E\ce=DE=D3=D6=DC=E7=DF=DA=E5=E8=DC=DF=E7=E5=EC=
+}|je=F3=EC=EDr=E3=D5=D4=DA=D2=CC=CB=D1=DC=ED=F5=EChSU_XW[YVVTWUPR]_\_ba=FD=
+=E7~=F5=DA=D4=DD=E2=DB=D4=D4=DC=DE=D7=CD=D3=DF=D7=D0=DC=EAe\=CE=CAUMpvXIB=
+M=FCh_=E2=CE=DC=FDdYPJENa\p=D6=D1=D3=D3=D4=D0=D0=D8=D7=D4=D7=DF=E3=F2hYSR=
+Z\XTk=E0=E0=ED=E3=D6=CD=CF=E6=F6=DE=E5fmf`=F5=EEm=F6=F1c^bZZ\Y_vjZ[_hf]m=DA=
+=D6=DE=DF=D5=CF=D0=E3=DE=CF=D2=F8=F0=DC=DC=EDnqa`=DC=E9VOW=F6^CAVis=F2=D3=
+=CA=CA=D7|g[KJ\mj=E3=D0=CE=CE=D4=D3=D3=D4=DF=E2=E8=FDmcXOKKSVSQj=E4=DE=E3=
+=DA=D5=D1=D5=D9=DA=D4=D8=E1=E1=E1=F7=FC=E3=DF=DC=EB=F6=F2=EAjVTUTTNMSRNNQ=
+SX^u=F5^=FE=CA=D1~=DA=CB=CC=CA=D0=CF=C3=C5=D2=D7=DD=CD=C7=E3YYnbLCI\njo=DC=
+=DBnXNJE?BKTW{=DC=D2=CF=D3=CD=C8=C9=CE=D2=D1=D0=DD=FDj__c_Z^=ED=E0=E7=DA=D0=
+=CE=CB=D3=DD=D6=DCukjWQXVQTTTXYV[XSWXZcig=F8=E2=E6=DB=D2=D6=D5=CE=CB=CC=CF=
+=D2=CD=D6=E9=DF=E5t=FE=F0hqnR=F2=CD[GQ_TNIL=EF=EDu=DF=CC=D2=E5=E2=EBo_NPb=
+kc{=DB=D6=D3=D0=CD=CE=CF=CF=D4=E4=F9fWOKJOTOOn=DE=EB=EB=D3=CF=DB=E2=DF=E0=
+=F1ljegghrx=FF}=F8=F0=EE=FE|=E7=EB~=F2=E9~m{=7Fq=FE=E6=EAn~=E6=E3=FBbt=F1=
+{lgfmdSn=E6Mh=C3=EAMa=EE{=FA[O=DC=C7=D9=DC=C6=C4=D9=D9=E3c[OLPXU]x=EC=DF=DF=
+=DF=DA=D5=D8=E5=F3u^YOIJOOKW=E9=E4=DE=D6=CF=CA=C5=CE=D3=C8=CB=DB=E4=E0=EA=
+|s_cjbbdYPWVQY[V]e]`wv=7F=E6=E4=E5=D8=D7=E0=DD=DB=EA=F2=DC=D7=E0}=E9=D9=DF=
+MU=CB=D7NMb`hTM{=CE=E5=EF=CE=C6=DA=DD=DB=F4dZY\eY]=FB=E9|=E9=DE=DD=DD=DD=E0=
+=F0vd[QLIJGGNZ^l=DB=D1=CF=CE=CA=C6=C7=C9=CA=CF=D3=D3=D5=DF=E9=E0=F0z=F5=FD=
+`_bUNNNKIIMOPT\_h=EC=DC=E1=DD=D6=D4=D0=CE=CE=D1=C9=C6=D2=DB=E7=E1=C8=D5XX=
+=F3^OMJM[Z\=F9=DB=EE=EE=EAo`]ZZ]^aw=F1=EB=DF=DB=D9=DA=D4=D1=D6=DC=DF=EFcV=
+VXNJSwqk=DA=CF=CE=CB=C9=CD=CC=CD=DA=E3=E5=FAfghfga^jb[YYTRSNJLSNO[dd=EF=DE=
+=DB=DC=D8=CC=CA=CF=CD=C9=C8=CD=CC=CB=DD=DC=E4=D3=D6=FD[^hXOLK[d\\w=FAen_X=
+WXSW\_l=F3=EC=E7=D9=D3=CD=CC=CB=CD=CE=D1=DD=EFqgZQUYYVc=F2=EF=E7=DE=D7=D6=
+=D7=DF=EB=EDoii^YX`]]]bnuo|=EF=F6qx=F8{v=FA=ED=E9=DF=DE=DC=DC=DA=DB=D6=D4=
+=DD=ED=DB=DAtk=F8_Nk=E6YNZZTOMObim=E7=D6=D4=D7=D6=DA=DF=EC{=F1=FAmm=EF=F6=
+x=EB=E5=EA=E0=DD=DB=DB=E1=F0=FDmZOJKKNPXu=DF=D7=D5=CB=C5=C8=CB=CE=CF=D4=DA=
+=E1=F7=F2qa[`a]]^^ZTQNMLKMQSXi=F9=E8=DC=D7=D5=D3=CA=C9=CC=CF=CA=C7=CE=DF=F4=
+=D8=C6=DBV`=DDvOLRa\W_ztdgbYUTUZZ[g=FD=F8=E9=E0=DD=DA=D0=CD=CE=D0=CF=D9=E8=
+=EFoZOZXTX_t=E3=D6=DA=D1=C8=CC=DB=DC=D6=F9^_[VNPTYZ_=FA=E9=E1=E7=E2=E5=ED=
+=FEnpsg`ep{=EF=EF=F6~=F0=E6=F4=FD=F8}o=FAh\=E3=DEdo=E1=DF=FF=F9=ED=DE=D8=DA=
+=DA=CF=CE=DF=ED=EEjXQOOPOOZ]`h=FE=E9=E4=DE=DA=D6=DA=DE=E9=F9qeYWX[_dw=E5=DB=
+=D5=D0=CC=C9=CC=D3=D6=D8=EBog]WVSV\^g{=FD{=F4yifg]^b^i=EF=E6=ED=E3=D9=D8=DB=
+=D4=D4=DD=E8=E8=E7fTk=EBcSZs\QTi=F8=F4~=E6=D1=D4=E4=E6=DB=E6dbrmdi=FA=EC=EB=
+=EF=E3=D8=D5=D6=D5=D6=E2=7Fe^QHEEHIMUk=E1=D7=CF=C8=C5=C4=C4=C6=C8=CD=D4=DC=
+=E7ud^[X\]\[YWXXNLNNJJOUZ^r=DF=D3=CF=CC=C4=C1=C2=C3=C7=C7=C1=C9=E4~=DC=EB=
+VMS^\TR_lXMQVNILTVR]=F3=DD=DA=D2=C8=C2=C0=C3=C4=C5=CB=E0pfTH@CHIHNl=DC=D3=
+=CE=C9=C5=C5=CC=D3=D7=DD=FBga\VRV_eck=F7=E8=EC=F8vweXTU[XU^~=F2=EF=E5=DC=D5=
+=D0=D4=E6=D9=C8=CF|d=F4}[Vb=E8=D8=DE=E8=D3=C9=D7s|=EEhRPXYPMQ\]\k=E3=D9=DE=
+=DF=DE=DF=FAg[TOIGKVg=FA=D7=CA=C4=BF=BE=BD=BD=BE=C4=CB=CF=DBrYPLJFDGJKKOT=
+XXUTX]_\]=FF=DE=DC=D8=CE=C6=C2=C2=C3=C3=BF=BD=C2=D2=E9=E4=FAZMLQZVOPYUMKL=
+NLJJOTX^|=DB=D1=CD=C9=C6=C3=C4=C9=CD=CE=D1=E6i]ZSNLO`{=EE=E3=D7=CE=CE=CF=D0=
+=CF=D3=E2q^VQMLLPWY^dr=FA}vkgv=F5|o=FC=E3=DB=E1=E1=D5=CD=CD=D1=D7=D7=CF=D4=
+=E4=E4=F0h[VTTUQUbjaaijf]W[egel=F1=DF=D8=D7=D4=CD=CA=CA=CC=CE=D0=D3=DDw^Z=
+VLHIOWVVb=EA=DE=E2=E5=DB=D3=D7=E6=F5=E9=E6{`f=FE=F3|y=EC=DF=E1=EC=F4=F5p`=
+fh\Z\hq^d=EF=DF=E4=F2=E5=D1=CF=DD=F1=E3=D3=DE]Td=F9mYWo=DD=E4mb=F7=E0=E9t=
+i~=EA=EEmh|=EC=E8=F0=FA=EB=E2=DF=E6=ECze_[TOLLPTST^s=EC=E5=E8=D9=CF=CE=CE=
+=CC=C9=C9=CA=CD=CE=CE=D1=DA=E1=E3=E7n[Y[YNMPRNKLMRPP\]cn|=DD=D4=D9=D3=CD=CC=
+=CC=D0=CF=CD=D0=DE=ED=DE=D9=DFvh=FA=E6zYW_bWMMPTOMR]kyy}=EA=DD=D8=D8=D6=D0=
+=CE=CE=D2=D7=D5=D7=DA=DE=E8=EF=F6tle]cb^]ZVUSRVYUR[krjo=E7=DA=D6=D9=DA=D1=
+=CD=CE=D2=D1=CC=CD=D5=D9=DB=DA=E7no=F9oihbWOKKPOKKQ]`[^n=ED=EB=ED=DE=DA=D3=
+=CF=D5=DA=D6=D4=D4=D4=D8=D9=D9=DC=E6=EE=EF=ECu[\__SLNTSQU[^iz=EE=E6=E7=DF=
+=DA=D9=DE=E9=E1=D9=DA=DF=E5=E9=E7=E8=E3=DC=DC=DF=DF=DC=DD=ECrp=FFn[SVYTSV=
+[]^cjt=FC=FE=F5=EC=F0=F6=F0=ED=EB=EA=F1=FC=FE=E9=DF=E2=E6=E6=DF=DE=DF=E3=ED=
+=F1=F1=EB=EF{nk~=F9wklnnibfnmd_gjj_Z`b_ak=F5=F1=F6=E7=DD=D5=CF=D0=D0=D0=D5=
+=DA=DA=DE=E4=EB=E5=E0=E9=F5=FF=FA{kb^^ZQONQOMOQW[[c}=EF=E8=E3=DB=D9=D8=D6=
+=D3=CE=CE=D4=D7=D6=D2=D8=DF=E8=EC=E7=FEf_`da[ZY[]^_bq=ED=EF=F6=ED=E1=DC=E2=
+=EB=F7=F0=F1}ncehm{jh=FA=E9=E7~dk=EF=FBg_emppn=FE=F5=E7=DB=DB=DB=DB=D9=D6=
+=D7=DC=EA=ED=E6=EA=F3qhrqf`_gi]Z\__\X]mxifu=EF=EB=EB=EB=E5=E1=DF=D9=D6=D7=
+=DA=E1=E5=DA=DB=E9=F0=F9=F7=FCj]Z]`a][^imkkj|=F7{st=F1=E5=E5=E9=EB=EC=E5=E6=
+=F5=EF=EE=F0=F2vv|=FD=F8=FEtw=F6=E7=E5=F0=F8=EB=E3=EB=FEorts`Z\^\_jmoo~=ED=
+=E1=E1=E6=E1=DA=D9=DD=E0=E1=E7=E1=DE=DD=DD=E8wgi_WUQPRRRUZ_k=EE=E1=EF=F0=DE=
+=DB=D8=D9=E4=E9=DD=D9=DF=EC=F1=EE=E7=EB=F3=F6=ED=E6=E3=DD=DC=E6=EB=EF=EB=E2=
+=F6nbj=F4gY\[__YVTWYYZ[[d=FB=F5=FA=FC=EE=DD=D9=D6=D5=D2=CE=CE=CF=D6=DD=DD=
+=E8znnkg^\fjc]`m=FDwdbr=F1=FB{=FC=EE=ED=F5=FA=F8=E4=ED=FF=EF=FF=F8=F0=EB=EB=
+=EF=E9=EE=ED=F5vmihe`ahkme^j~=F5}|=EC=DF=DF=EA=ED=E2=D8=DC=E5=DE=DF=E9=F4=
+vkdb_[YY[ZZWVX_cm=F8=EF=E4=D7=D3=D3=D1=CE=CA=C8=CA=CC=CF=CD=CE=D8=DB=ECpm=
+^ULHHGGEEIMORXYa=FC=E7=DF=DB=DA=D1=CC=CD=CE=CD=CB=CA=CD=CF=D8=DC=DD=E6=F8=
+idc`_YTU]]W[^ftjfgg}=F6vrx=EB=DE=DF=E2=DF=D8=D2=D4=DA=D6=D0=D3=DB=DF=E4=E7=
+=F1ohb^ZWVSOOTZ[UV]__\V[p=EF=ED=E8=E3=D6=CF=CF=D1=D4=D0=CF=D2=D8=DC=DD=DF=
+=DE=DC=DE=ED=FC=FF=F6=F9f]cpf[YWXXWVX\]_^ai{=F9zz=F3=EA=E6=E6=E7=DD=D5=D4=
+=D5=D7=D5=D5=D5=D4=D6=D5=D8=DE=E5=E2=E1=FA^Z[UNJFEHJKNNOYfx=F8=EC=DD=D0=CC=
+=CB=CD=CC=C8=C5=C7=CB=CD=CD=CE=D4=DD=E9=E6=EFg^XYVLHIJKKKNSX[]jw=F1=DE=D6=
+=D1=D5=D3=CE=CB=CD=D5=D7=D1=CF=D2=DA=DA=D8=DC=E2{lx`XZXWQONMLMNRXWZh=FE=FA=
+=FE=E5=DC=D9=D3=D5=D4=CF=D1=CF=D1=D8=D9=D8=DA=DB=DB=DD=E5=E6=E7=F8=F4=EE=FB=
+wjpna\USTQPQSVWUWZZ\dlp=F3=EF=F1=E0=D6=D4=D7=D8=D4=CE=CD=CE=D3=D2=CF=CE=D3=
+=D7=DC=EC=FCrjf\TWXYZQT]_c\Zbokl|=ED=E4=EF=FBw=FB=EF=F5}n|=ED=E6=E8=F2=E9=
+=DE=D9=DB=E0=E0=DC=DC=DF=ED=FE|qlhfa\]afeaeu=F8=F7=F9=7F=F2=EB=F9|}=FC=F4=
+=F6=F1=EA=E8=E7=E7=E2=DD=DA=DE=E6=EA=EA=EFn^[aa[VQV^_YV]gf__n=F1=E9=E9=DF=
+=D9=D3=D0=D0=D0=CE=CF=D1=D0=D2=D3=D7=DA=DE=DF=E8uc`hfYPQUVPNOVXUV[anrn~=F6=
+=F2=EE=F0=F5=E6=DD=DE=E0=DE=DC=D9=D7=D9=D9=DC=DF=E2=DE=DE=E3=EB=EF=E7=E4=F3=
+wljnnb]aeha\_cgecdjvz{y=FE=F2=ED=EB=F1=F4=ED=EA=EF=FEz=F9=ED=EB=ED=EC=E4=DE=
+=E2=EB=F4=EB=EA=EE=F0=FC=FC{tpkc][Z[\_en|=FB=F9=FA=F3=EB=E4=E5=E7=EB=ED=EA=
+=EA=EC=ED=EC=EB=EB=EC=EC=E9=ED=F1=EF=F0=FBwoica_ahjknpy=FB=F7=FA=F5=ED=E8=
+=E4=E5=E3=E1=DF=E2=EB=F6=FFyyxtnkliiijjihea__^_dkr=FC=ED=E5=DF=DF=DF=E0=E0=
+=DF=E2=E6=EC=F5=F7=F9xxz=F8=ED=EA=E4=E2=DE=DA=DB=DE=E1=EC=F8=7Fwl`[YYXWWY=
+\^bcbgq~=FA=F1=ED=E9=E6=E9=F3=FB=F4=EF=EC=E9=E8=E4=DE=DA=D9=D9=DA=DA=DB=DC=
+=E6=F1|ne\WVXZ[[^gljomo}=F8=F0=EF=EC=E6=E7=E7=E9=E7=E9=EB=EF=FAy~=F6=F2=EF=
+=F0=F1=F0=ED=EC=F0=F6=F5=F3=F2=F0=FA=7Fwvvr||{uoidcegggghifhku=7F=EE=E2=DD=
+=DB=DC=DE=DF=E4=E8=EF=FEoige`\[ZZZ\]do|=ED=E3=DC=D6=D1=CE=CD=CA=C9=C8=CA=CC=
+=CF=D6=D9=E5=FAi]WQMKJJIGGGHKOSX^hx=E9=DE=D9=D4=D1=CE=CD=CC=CD=CD=CD=CD=CE=
+=D0=D4=D6=D8=DC=E3=E5=EC=F0=FCoc^ZVVUVXXYYZ\^aelv=FB=FB}vty{{=F8=F1=F2=EA=
+=E3=E0=E0=DF=DF=DD=DD=DF=DD=D8=D6=DB=E6=EE=F3=F3r_XWXXVW]h}=F6=F2=EC=E6=DF=
+=DD=DE=DF=E2=E7=EC=F6=FEwprnmmiea^^^_ciqy=F4=EA=E7=E3=E2=DF=DE=DF=E2=E2=E6=
+=E4=E5=E9=ED=EF=F0=F4=F4=FDunha_]\\\[\^dm{=F7=F4=EA=E3=DF=DF=E2=E4=E3=E0=DF=
+=DE=DF=E1=E0=DF=E2=E8=EC=EF=F5~xnfb``adhnolmmmjeb^^__]^_behnu=F2=EC=EA=E4=
+=DF=DA=D7=D5=D3=D3=D0=CF=CF=D2=D4=D5=D4=D5=D7=DC=E1=EC=FAsd[WUQOMLLKLNOTX=
+YZ[^cm=FC=EB=E5=DF=DA=D6=D5=D2=D0=D0=CF=CE=CE=CE=CD=CE=CF=CF=D0=D3=D6=DA=E3=
+=F1o`XSQOMMLKKLMOTWZ^gp=FD=EE=E8=E0=DC=DA=D6=D6=D6=D5=D4=D4=D7=D8=D7=D7=D7=
+=D8=D9=DA=DB=DD=E3=EA=F3=FDyme]YWSNKJJMOOQTZ\_cj=FD=E9=DC=D7=D2=CF=CE=CC=CA=
+=C8=C8=C8=C8=C9=CA=CB=CC=CE=D4=DB=E8=FEk_YRMJGEDBBCDEFGJMR\j=ED=DA=D1=CC=CA=
+=C8=C7=C6=C5=C5=C5=C5=C5=C5=C6=C8=CA=CC=CE=D0=D7=DE=E9|_UOKGDBABCCCEHJMQW=
+`t=E8=DB=D4=CE=CB=C9=C8=C8=C7=C7=C7=C8=CB=CD=D1=D8=DD=E5=EF=FEsgb^[\^`a^]=
+[ZYYZ[]]\\]]``gmpz=F5=E9=E3=DE=DC=DB=DA=D8=D6=D4=D3=D4=D3=D3=D6=DB=E1=EB=FB=
+nb\[ZZ[]_ejlnnnmoqt~=F2=ED=E8=E1=DD=DC=DC=DF=E7=F0|ojda^\[[[[_dhlpuz=FA=F4=
+=ED=E8=E6=E6=E8=E4=E2=DF=DE=DE=DB=DA=D8=D7=D7=DA=D9=DA=DC=DE=DF=E5=ED=FDo=
+bZSOLKJIJLNQTX]gy=ED=DF=D9=D4=D0=CE=CD=CD=CC=CB=CB=CD=CD=CD=CF=D2=D8=DD=EA=
+=FDod]WPMKKJJJKMNOTY_ht=FC=EC=E2=DB=D6=D2=CE=CC=CC=CC=CB=CA=CA=CA=CB=CC=CD=
+=D1=D8=DF=F2m]VPMLKKLLLMNOT[dp=FB=EE=E8=E3=DF=DE=DE=DB=DB=DB=D9=D9=D6=D5=D5=
+=D7=D9=DA=DC=DE=DF=DF=E0=E7=ED}mb\XSPPQRPPSTUVZ^gv=F2=E2=DB=D6=D1=CF=CE=CD=
+=CD=CE=CF=D0=D2=D3=D5=D9=DC=E2=EB=F5{ohc^ZWVWVXXXYZZYXWWXY[]_dm=FE=EC=DF=DA=
+=D7=D4=D1=D0=CF=CE=CF=D0=D1=D2=D4=D7=DA=DC=DF=E1=E4=ED=F9ymifb^ZVSSRRSVXX=
+WXXY^es=F5=EB=E3=DF=DB=D7=D3=CF=CE=CD=CC=CC=CE=D1=D5=D9=DF=EErd\ZWUSRRSUX=
+[]^_`dem=FE=F5=F3=EE=EA=E6=DF=DD=DA=DA=D9=D7=D7=D5=D4=D5=D6=D8=DB=DE=E4=EF=
+xi`\YVRQQRSV[^`hz=ED=E3=DD=D9=D6=D5=D6=D7=D7=DA=DD=DE=E2=EA=F8vljff_\YVTT=
+VXXX[]_cm=F5=E8=DF=DA=D7=D4=D1=CF=CE=CD=CD=CE=CF=D0=D2=D5=D9=DD=E0=E7=F7n=
+a[VQONNMMMNORVYcp=FE=F4=EC=E5=DF=DD=DB=D9=D7=D6=D5=D6=D9=DD=E3=EB=F2=FExq=
+njhhhkns=FD=F2=F2=EF=EB=EC=EE=EF=F5=FB=FD~yz=FC=F3=EE=E9=E8=E9=EE=FA=7Fri=
+fd`_]]\^acfmv=FD=EF=EB=E9=E9=E4=DF=DE=DE=DF=DD=DD=DC=DC=DD=E2=E7=ED=FCsni=
+f_^^\\[[ZYY[]_dgmz=FE=F1=E9=E6=DF=DC=D8=D4=D3=D2=D1=D0=CE=CF=CF=D0=D2=D4=D8=
+=DE=E2=E9=F7pb[TOLLKJIIJLMORUX\hnw=F6=EB=E1=DA=D7=D2=CF=CE=CC=CA=C9=C9=CA=
+=CA=CB=CC=CE=D1=D5=DA=DE=E6=ED|oic[TOMLJJHHHHJKNSY]cl=FD=E6=DD=D7=D2=CE=CB=
+=C9=C7=C6=C5=C5=C6=C8=CB=CE=D2=DA=DF=E8=F5qe\UPNLIHFGIKMPU[bjw=F8=ED=E6=DF=
+=DC=D8=D1=CF=CE=CD=CE=CE=CE=CF=CF=CF=D0=D3=D6=D8=DC=E4=EExh^XQNMKKLLMOOSX=
+\_er=FB=EB=E2=DD=DA=D6=D0=CE=CD=CC=CD=CE=D0=D3=D7=DC=E0=E6=EA=F3sje`]]]\Z=
+Z[[]]]^_^^^__dhnw=F4=EA=E2=DD=DB=DA=D7=D5=D4=D4=D5=D5=D7=D9=DA=DE=E3=E5=E6=
+=EA=EF=F4=F3=F8~{via^[XURQONNMNQW\ds=F2=E9=E3=DE=DC=D9=D7=D6=D5=D4=D5=D6=D7=
+=D8=DB=DD=DE=E1=E2=E0=DF=E0=E2=E3=E5=EB=ED=EE=F7rg^ZWUTUWXY\^biot|=FB}wus=
+ngiklo{=F0=E9=E4=DF=DC=D9=D8=D7=D6=D6=D8=D9=DA=DE=E3=EA=F2=FAwojhecbca^]]=
+`cbbfiiklko=7F=F0=EB=E3=DF=DE=DD=DC=DB=D9=DA=DB=DD=E5=EF=7Fvmjijkjgc_\[ZX=
+XY\`iv=F9=ED=E8=E6=E3=E0=DF=DE=DE=DC=DB=DB=DB=DC=DC=DA=D9=D9=D8=D7=D8=DC=DF=
+=E5=F0zl_ZXTQPONKLLLNOSW\cm=FA=E8=DE=D9=D7=D3=D0=CE=CD=CD=CD=CC=CB=CA=CA=C9=
+=CB=CC=CE=D3=D9=DF=EDnaYQMKHGEDDEFGILOT[`k=F6=E4=D9=D2=CC=C9=C7=C5=C4=C3=C2=
+=C2=C3=C4=C7=CB=CE=D6=DF=F0n`ZUONLJJJIIJKKLMOQU[`k=F5=E0=D8=D1=CD=CB=C9=C8=
+=C7=C7=C9=CB=CD=D2=D8=DF=EC=F3=FBqkgb]ZWVUSTUVZ]`hmy=F7=EA=E4=DE=DE=DE=E1=
+=E4=E6=E4=E1=E0=DF=E2=E5=EA=ED=F6vnieecdgikliea`_`dfity|}{=FD=F4=EB=E7=E3=
+=DE=DB=D9=D6=D5=D5=D5=D6=D8=D9=DC=DE=E0=E9=F4=FEsd][VQONMMNORUZ`jy=EF=E7=E2=
+=DE=DC=DA=D9=D8=D8=D7=D4=D3=D1=CF=D0=CF=D0=D3=D6=DA=DF=EB~m_ZVOMKJJJLNPU\=
+m=F6=E7=DD=D9=D7=D7=D8=DA=DC=E0=E4=E6=ED=EF=F1=FA|sw{=FA=EF=EA=E4=E0=DF=DE=
+=DF=E2=E1=E1=E4=E6=E7=EA=EE=F9zpjhc_]\ZZ[[\\]\\\]`fkns=FA=EF=EB=E9=E5=E3=DE=
+=DB=D7=D5=D4=D3=D0=CF=CF=D1=D4=D8=DD=E0=E3=EA=EE=F3wi^WQNLKJJIIKLOT[f}=E5=
+=DC=D6=CF=CD=CB=C9=C9=C9=C9=CA=CC=CD=CE=D1=D5=DB=E2=EDxjhc^XTQNMLLKMMMNPS=
+X\bo=EF=DF=D9=D4=CF=CC=CA=C8=C8=C8=C9=CB=CC=CF=D3=D7=DC=E8=FFi^WOLJHHJKNS=
+Zam=7F=EE=E7=E1=DE=DC=D9=D8=D9=DB=DE=E2=E3=E1=DF=DF=E0=E3=E8=E9=E9=EC=EF=F2=
+=FCzwtvwqplfb_^[YYZ\\[^_cl{=F0=EB=E7=E1=DF=DE=DD=DB=DA=DA=D7=D4=D3=D3=D4=D4=
+=D8=DD=E3=EC=FFog\VQMKKKLNPU]n=ED=DE=D7=D3=D0=CE=CD=CE=CE=D0=D2=D5=D8=DB=E4=
+=F1vf^YVTUUUVWY[\]`fm|=F9=EF=EB=E7=E2=E1=DF=DF=DD=DA=DA=D8=D6=D4=D3=D6=D8=
+=DA=DB=DE=E5=EE|lc]YSONLLKLNRX^hy=F2=E8=DE=DA=D7=D4=D2=CF=CE=CE=CF=D0=D1=D2=
+=D3=D3=D3=D7=DB=E4=F0ra[TPNKIHIIJLNSY_l=FE=EC=E4=DB=D5=D1=CF=CD=CD=CD=CC=CD=
+=CE=CF=D2=D7=DB=DD=DF=E3=EA=ED=F1=7Fl`[YTPPPQSSWZ]djs|~}wtpmp{=7F=F7=EF=E8=
+=E3=E0=DD=DB=D7=D7=D7=D7=D9=DB=DC=DE=E3=EA=F1{mf`___acdflm|=F5=F7=F9{ywpq=
+sow~{utxy=FE=F3=EE=EC=E8=E9=E9=EA=ED=EE=F2=F9|umjiknu=FC=F4=EB=E6=E2=E2=EC=
+=FAsja][XXY\_cgmy=F8=EB=E0=DC=D5=CF=CE=CC=CB=CB=CC=CD=CF=D5=DC=E8=FEi\UPM=
+LKJKLLLNQY_iy=F6=E9=E2=E1=DD=DB=DA=D9=D8=D8=D6=D4=D2=D2=D2=D2=D3=D5=D8=DA=
+=DD=E2=EA=F6xldb^[[\[[\\^`b_^]]]]^^^`deintx=FB=EB=E2=DD=D8=D6=D3=CF=CE=CE=
+=CD=CD=CD=CD=CE=CF=D4=D8=DB=E0=ECyj`WSOMLJJIJLNPUY]bkw=F0=E5=E0=DD=DC=DB=D9=
+=D9=D9=D8=D9=D9=DA=DB=DC=DC=DD=DC=DB=DC=DB=DB=DC=DC=E1=E9=F1=FC~vmha\\ZXU=
+TTTSSUX[_cir~=F2=EC=E7=E1=DF=DE=DD=DB=DA=D8=D8=D8=D9=DA=DC=DF=E8=ED=EF=F1=
+=F5=F8=F9=F6=F4=F5=FCxnmmjge_``__```aeijhgfhmr}=FB=F4=EE=E6=DE=D9=D4=D1=CE=
+=CF=D1=D5=D8=DC=E2=ED=FBvjeb^[XWVWY[`efims{=FE=F9=F4=EC=E4=DF=DD=DC=DD=DE=
+=DE=DE=DC=DD=DD=DE=E3=EB=FEnf^ZVROOOOPPSX^j=FD=EA=DE=D7=D1=CE=CC=CA=C8=C7=
+=C8=C9=C9=CC=CE=D3=DA=E4=F5n^VOKIHGFFGILPX_jt=FA=EC=E4=DD=DA=D9=D8=D6=D5=D4=
+=D3=D3=D4=D4=D3=D5=D8=DB=DD=DD=E1=E8=EE=F4=F5=FCrj_ZXTSRSSRPQSX[]bm=FA=E7=
+=DC=D4=CF=CD=CB=C9=C9=CB=CC=CE=D2=D8=DE=E5=FAkd^XSQONMLMMNQW\_f{=EA=E1=DE=
+=D8=D5=D3=D0=CE=CD=CC=CD=CF=D7=DE=EA=FBqlke___^_`a`cjllklnnkmot}=F8=F1=EC=
+=E9=E8=E7=E3=E1=E7=EA=ED=EF=F2=EE=EA=EB=E9=EE=F6=FAytlgb__`bdhnz=F9=EF=EA=
+=E7=E7=EA=EB=EF}xqomllls}=F4=F0=EF=F0=F8=FExtuy=FB=F3=EF=EF=EE=EA=E7=E2=E0=
+=DE=DD=DC=DA=DA=DB=E0=E6=ED=FDk`\XURRRRRSUX\aen=FE=EF=E5=DE=DC=DC=DD=DE=DE=
+=DE=DE=E0=DE=DD=DD=DC=DB=DE=E2=E5=E9=EE=F9zrmnorpolgjilomlnnnuuw}=FD=FF=F9=
+=F5=FB=FB}tmhc_acegihimnu=FD=F5=EB=E4=DF=DA=D7=D4=D1=D1=D2=D4=D6=D9=DC=DE=
+=E2=E8=F4sg^YXVWWY[_dddc`adglprxz=7F=FF}|=FE=F8=EF=E8=E3=E0=DF=DE=DB=D9=DA=
+=DA=DA=D9=DB=DE=E3=E8=EB=EF=F8}}yqnf_]XVTSTVXY[_bdkz=F2=E7=DF=DA=D8=D8=D9=
+=DB=DC=DE=DF=E0=E1=E4=E5=E2=E2=E1=E2=E3=E1=DE=DD=DE=E3=E9=F8re]ZVQONNOPSW=
+Z^beilpw=FC=F1=EC=E5=E2=DF=E0=DF=DC=D9=D4=D1=D0=CF=CD=CC=CC=CE=D0=D5=DA=DE=
+=E2=E5=EFwh^ZUOMKKJJJKNPSY]afp=FF=F4=EC=E2=DD=D9=D3=CF=CD=CD=CD=CC=CB=CC=CD=
+=CD=CF=D3=D7=DC=E3=F7m_XQNLKKKJKKKLNSXao=F1=E3=D8=CF=CC=C8=C7=C6=C6=C7=C8=
+=C9=CA=CB=CD=CE=D0=D6=DE=E8t_WMIECA@@@@ACFJOYg=F4=DD=D3=CC=C8=C3=BF=BE=BD=
+=BC=BC=BD=BF=C2=C6=CA=D2=DD=EAw_WPLHECA@@@ABEGILQZi=F3=E1=D8=D0=CC=C9=C7=C5=
+=C4=C3=C3=C4=C4=C6=C9=CD=D3=D9=DF=EA=FEmd_[VRPONMNNOQTXZ^dp=F8=EC=E5=DF=DE=
+=DC=DC=DC=DC=DD=DD=DF=E0=E4=E9=F9rjdca`]\]^^bfkv=F3=EA=E1=DA=D6=D1=CF=CF=CE=
+=CE=CF=CF=D0=D3=D9=E4=F3sf\VQOMLLNNPSWXY[]`gn=FE=EF=E4=DB=D7=D4=D2=D2=D3=D5=
+=D7=D6=D7=D8=D8=D8=D7=D7=DB=DC=E0=EA=F2=F9wjc^ZWUSSTUUUXYZ\]afjs=FE=EE=E7=
+=E2=DE=DC=D9=D7=D4=D3=D4=D4=D3=D4=D5=D7=D9=DE=EA=FDl_YUTUVX\_hnt{=FA=F9=FA=
+=F6=FFy{spoolpopy=FC=EE=EB=E4=DE=DA=D8=D9=D8=DB=DD=DE=E0=E9=ED=F2zmliedba=
+`_^\\YWY[_ho=FB=EA=E0=DC=DE=DE=DD=DD=DF=E3=E6=E8=EA=ED=EF=F4=FA}rmhba__bc=
+ffhlu=F4=E8=E0=DA=D4=D2=D1=CF=CE=CF=CF=D1=D4=D7=DC=E4=EFte^XSOMKHGFEFGILN=
+U^m=F0=E1=D6=CF=CC=C9=C8=C7=C4=C3=C3=C3=C5=C7=CB=CF=D5=DE=EDxg]WQQONMLLMN=
+OSY`hu=ED=E6=E1=DE=DD=DB=DB=DB=DB=DD=DF=E3=E4=E8=ED=F6vkd_\[]__gox~=FA=F1=
+=EC=E8=E5=E2=DD=DB=DD=DE=DF=E2=E9=EA=EC=F4=FA=F6=F6=F6=FFvokihhe^]^^_bilp=
+y=FD=F9=F0=F3=F4=EE=EA=E7=E7=E9=EC=EF=F4=F3=EF=F2=F3=F6=FC=FF=FF=F4=EE=ED=
+=EA=EA=E7=E3=E4=E2=E0=DF=E2=EDzjc^[XVTVXZ\]_gjpy}=FA=F1=EC=E8=E5=E2=E3=E4=
+=E2=E2=E3=E4=E3=E2=E5=E7=EA=EA=EC=EE=EE=EE=ED=EE=EC=EA=E7=E4=E2=E0=E2=E1=E3=
+=E7=EA=F7xokjheb^ZURPOMMMNOQTZ`k=FD=ED=E1=DD=D8=D2=CF=CE=CE=CD=CD=CD=CD=CD=
+=CD=CD=CE=CE=CF=D5=DA=E1=F1o_YVRNMLMMMNOQTY_hpu=FF=F6=F4=ED=EA=E4=DD=D9=D5=
+=D2=D0=CF=CE=CF=D2=D5=DB=DF=EB=FB|qhdd_^[XUSQPRSW]dos=FA=EF=E7=DE=DC=DB=DA=
+=DB=DB=DE=E3=E7=EA=E7=E9=E7=E7=E7=E4=E3=DF=DD=DD=DE=E0=E3=E8=EA=ED=EC=E9=EC=
+=EF=F2=FFl]XTNKIGFEFHLOW_q=E7=DA=D3=CE=CC=CA=C9=C8=C7=C6=C6=C8=CA=CD=D2=D8=
+=DC=E0=E7=EE=FBoe^YWSQOOONNMLMORW\_k|=F1=E3=DC=D6=D1=CE=CC=CB=C9=C9=C9=CB=
+=CD=CE=D1=D9=E2=EEuf]WSOMLLLKLMNNQW]ep=FA=EF=E6=DD=D8=D3=CE=CD=CC=CA=CA=C8=
+=C8=C8=C8=C9=CB=CD=D1=D8=E3=FEdXPMJHGEEEEFHJKPW^n=F5=E7=DD=D7=D1=CE=CB=CB=
+=CA=C9=CA=CB=CC=CE=CF=D1=D5=D7=DB=E3=EB=F9tj_[YXVUTSSSTWY\_dfgilowy=F8=EA=
+=E2=DD=DB=D9=D8=D7=D4=D3=D2=D2=D2=D4=D6=DA=DC=E0=F1vf]XTSRPNNNORUW[^bjko=FF=
+=EF=E9=DF=D8=D1=CE=CB=C9=C7=C7=C6=C5=C5=C6=C8=CA=CE=D7=E4pZOKGDBBBBAEIIIL=
+NPYn=DF=D3=D0=D7=DD=D9=D1=CF=CE=CC=C8=C3=C0=C2=C6=CC=CE=CD=CF=D3=D9=DE=DE=
+=E3=FAaVNLJHHGGGHHIJMR\jy=ED=DE=DA=D6=D0=CF=CE=CD=CC=CB=CC=CD=CE=D3=DB=E5=
+=E9=EA=EE=FEj_^^^[\\c=FDn\SU\b^SVes=EE=F3u|=E9=D7=D3=D4=DB=E5=DA=D1=CE=CE=
+=D4=D8=DA=D6=D4=DA=E3=F9=FC=EA=E9=FC_UOSVOKEBGKHDELVbl=FD=DD=D4=D1=CF=CB=C5=
+=C0=BF=C1=C6=C7=C7=C8=C9=CD=D3=D0=D5=DF=EFo\ROOMMKIJHC?AEHMTe=F2=ED=E3=DB=
+=D1=CF=D5=D2=CA=C5=C2=C3=C7=CA=CC=CE=D8=E3=EB=E2=D8=D8=F3YSYYRF?LyWNK=3DCh=D2=
+=CB=7FWQd=CF=E9n=D9=CC=C2=C8=DA=DC=CE=C8=CE=DD=DF=D4=C6=C9=EDXVmvXKGO[YXP=
+NVc|nUKS=F2=E3lX[t=DB=D2=DC=E3=E8=DA=CA=C4=C6=CD=CF=CD=CD=CF=E3x~=FBjVMM`=
+oWE?M=F5=DB=FCSTa=E1=D3=E4^Q]=DF=CD=CF=F2^j=DC=CD=CF=EA_^}=DB=DA|\o=D9=D1=
+=D9{h=EC=D7=DAmWWn=D8=DD_KJ[=F7=FCVNY^efjq=EF=ECl=E8=D5=CF=E0X=EF=C3=B9=C3=
+M?Y=C0=B9=C9dX=FB=EFdI?GReoijWMST[|=EE=D9=CB=C5=CFv]]=EE=E0=F5ci=D9=D2=E7=
+l_l=E4=DF=E0=D6=CC=C9=DC=FF=F6=D7=CE=EDZV=DB=D1U??=FB=DAH8<s=C5=ED6-I=B6=B3=
+=CD?9Dz=CB=C0=B7=AE=AE=B2=BBw<<J=F1=CB=C4=C3=C3=CFV>9?Lo=E5=E8=DB=E6[D;8;=
+?H[m=E0=DA=DB=D9=DA=D8=CF=C2=BC=BE=C7=CB=D7=CF=C4=C6=D2=F8^m=E8=EBUFJX^XK=
+EKTVOKO_=EA=DF=EF=E1=CE=CA=CC=D6=DC=CE=CB=D4=E4=E8=CD=CB=CF=DE\=E1cG=DC=CD=
+=BA=CA1()O=BA=BE=D1=DA=C6=C1U/+2K=CA=BF=BF=C0=C3=CFeGJ=FB=C9=BA=B9=B9=BA=C3=
+=DARB?HQd=F9=E3=E0s^]ZSSu=CF=C8=CD=F9Uc=FA~WQ^=FE=E2mOLVefa^k=F8=F6m\b=F8=
+=DF=DB=DB=D8=CD=CA=CF=DD=E9=D7=D4=DF=E7=DE=CF=D6=D7=EFIs=D3=CA=C1Q7.:=D7=
+=C7=C4=CD=D7=C8=D0D0.7S=D2=C4=C5=CB=CE=E7VINg=CA=BD=BC=BC=C2=CC=E8UIEKYv=EA=
+=E7=E5=E5xYR^=EC=DA=D7=E0=E5=E6=FB_VZbp=F9=F9{ja^dw=F7=F7=F9=F5=F1=F1sd`f=
+=FF=E8=DD=DA=DB=D8=DE=EE=FE=7F=E0=E0=F9j=FD=DAnR=F8=DE=CD=CBd?8Ab=D5=BC=BD=
+=C6=C6=CEkC;8=3DY=D0=C7=C8=CF=F1]ZVUj=E2=D4=CA=C8=CF=EFXLFHLOZk=EB=D7=CE=CF=
+=D6=D9=D6=D1=D0=D0=D2=D1=D1=D4=DA=E2=EF|il~xl_VQOOMKKKLNOOQYd=FE=E0=D8=D5=
+=D0=CF=D0=CE=CC=CD=CD=CD=CE=CE=CE=D2=E4=EB=DD=E3=E7=E9dNNUU]t_T\[S[d[[a[T=
+WXT\|=EB=DC=D4=D0=CE=C9=C6=C7=C7=CA=CF=D6=DC=EEgYQMLJLOU^p=EE=E1=D8=D3=D4=
+=D0=CF=D7=DF=E6=F3zqcYVWVWZ[]ba[[[\^cnz=EF=E6=DE=DB=D8=D1=CE=CC=CB=C9=C9=CA=
+=CB=D0=E2=E0=D9=E4=FCnTGINMXt`W^ZP\k^cn[QSON[s=F7=DE=D3=D1=CE=C8=C7=C7=C5=
+=C7=CA=CE=D5=E8m^PLLLMRZb=FC=E3=D8=D2=CF=CF=CF=D1=D7=DF=F6hZTPOONOSX\_cdg=
+jmop~=F6=EA=DE=D8=D4=D3=D1=D2=D3=CD=C8=C6=C7=C9=CF=DA=E8=ED=EA=F4fUJ@>AEK=
+YYSUWVb=EE=E6=E0=E0=F7hjoy=E3=D5=D1=CD=C9=C7=C6=C5=C6=C8=CA=CC=D4=E0{ZKEA=
+@@ACFNXf=FB=E0=D4=CD=CB=CD=CE=CF=D5=D9=DF=E7=EA=EA=F2slko=FF=F1=EF=EC=EF|=
+rg^YZ\^a``cchx=ED=E4=DD=DD=EAm{=DD=DE=E9=E5=F6XT\[v=D1=CF=DC=D8=DB=F5=E2=D3=
+=D4=D1=D2=EAaYOLQY]krlo|=F9=EC=E8=EE=F5ze]ULIJLPZe=F1=D8=CD=CA=C7=C2=BF=BD=
+=BC=BE=C1=C8=CF=DA=EBk^WNJEA@@ACEEFHILQXh=EA=DB=D4=CD=C8=C3=C1=BF=BD=BC=BD=
+=BF=C1=CA=CC=C8=CD=E1vXD?A?CQRLNJEKW]y=E4=EEtlac=F9=E2=D9=CF=CC=CA=C9=C9=CA=
+=CB=CE=CF=D2=DB=EAnUJGFFHJMVeiq=D7=CB=CB=CC=CD=C9=C5=C7=CF=D9=D7=D6=E1za[=
+SOPSUTOLJGFEEJU^\]{=D8=CB=C4=C1=BE=BC=BC=BD=C5=CD=C7=C1=C3=C9=D5_HC>?Mh=FC=
+=ECyRF@=3D=3DBISh=EB=E1=DC=DC=D9=D7=CF=C9=C4=C0=BF=C2=C9=D4=EBfXNHFKR^q=FE=F5=
+=E4=DD=E0=E1=DD=DC=DC=D9=DC=E7=ED=F8qmnp|=F2=EC=FAma[^_\YVUVWYc=FE=E5=E0=E0=
+=DC=DA=D6=D3=D2=D6=DC=ED=FA=ECcV=F1=DB=D7=D7=F6LBCEU=D0=C0=BE=BF=CBsOHDIU=
+g=E6=D4=D1=D7=E3ueg=FC=E0=D4=CF=D5=E4mXNMLGEIV=F1=DB=D6=D5=D3=CD=CA=CB=CC=
+=CD=D3=D9=DD=E4=EC=EF=FEkifekj`[[]abYONPTUV^ovmjy=E2=D9=D2=CF=D0=CC=CC=D2=
+=E1c]=D8=C8=C6=CA=DCUFBCU=CF=BF=BD=C2=D0eJB>>FS=EF=D2=CE=D6=F6\TVb=E9=D7=D1=
+=D1=DDv[NJLNLQm=E1=D3=CE=CF=CD=C9=C7=C8=C8=CB=CF=DA=DF=DE=DF=E5{_USWZYSNK=
+MPRQONR[ct=E8=E2=DD=D7=D5=D1=CD=C6=C3=C5=C8=CD=D8=E1=E5rS[=DE=D4=D9=F2QD@=
+?>K=EE=CF=CA=CD=E1aPGDIV~=DA=CF=CD=CD=D0=D9=E4=EB=DF=D4=CE=CD=CE=D9=F5\OL=
+KIINWbw=F6=DF=D6=D0=CE=CE=CE=CF=D2=D7=DA=DA=DA=DE=E8{_\]__\XSRQOOQSX[_dek=
+=FB=E1=DA=DB=E4=F6=E4=D1=CB=CB=D1=D9=DD=DE=DD=DE=E2=E4=D2=C8=C9=D4|RMSVZe=
+rlfaXNJFEINVbt=7F=FA=EA=DF=DA=D3=CE=CA=C9=C9=CC=CF=D8=E4z__jjlgcdgl=FD=E6=
+=DD=DC=DE=DA=D9=D6=D9=E2=EA=EB=ECp_\\^ZVUUVUQMNS[gk_]t=E4=DA=DA=DF=D9=CE=CD=
+=CF=CF=D0=D0=CE=D0=D5=D7=DD=E8=E9=E4=ED=F2=FAupdbc^ZSPOONNQTTY`a`ch`^n=EF=
+=DB=D3=D5=D2=CF=CD=CD=CF=CD=CC=CD=CE=CF=CF=D1=D7=E6wc\^`]WOPW\WQOU`a_oncb=
+fw=EF=ED|o=E9=DE=DE=DA=DB=DD=DF=E6=E0=DA=D4=D7=E5=F1=F7=F2=F7ukchot=F3ua[=
+_glm]^x=F8=F9ogz=FE|mv=EB=E4=E5=ED=EC=EC=F6=F1=E0=DB=DD=E0=E5=DE=D9=E3=F3=
+~|oga_d[[^\WRWX\`[r=E5=EC=F9=ED=D6=CC=CE=DB=D9=CF=CD=D2=DB=DC=D9=D9=E3yhh=
+ha^^Z]ZUUSX^beaaimomn=FA=E1=E4=EB=E7=E0=DE=E7=F4=FD=F6=E9=DD=DC=DD=DA=DA=D9=
+=D6=DA=EC=F7~~sfd]XVX\[WUZv}z=EE=E8=E7=E1=DC=DB=DC=DC=DC=DD=DD=DE=EC~|}wh=
+fefmy|igv|rligehgkyyoo=FD=F4|q=FC=E7=DF=E3=E8=DE=DC=DF=E8=E8=DF=DE=DE=E5=EE=
+=ED}pj`]XY__ZUV\bip=F6=EA=EA=EA=EA=E8=DF=DC=DA=DA=D7=D7=DC=DD=DE=E0=DF=E9=
+=EF=FA|sg_^_[Y[\adagkomgfx=FD=F9=F9~=ED=E3=E7=E8=E9=E6=E0=EA=F5=ED=E6=E5=E7=
+=EC=E7=E4=DE=DF=EB=E2=DE=E4=E8=F8yuf_[WWXX[ZY\\ail{=F0=E2=DE=DE=E0=E4=E1=D9=
+=DD=EC=E7=DE=D8=DA=E8=F1=7Fulhjmh_[Y^c_ehl=F9=EF=EA=E0=DF=DF=DC=DA=D7=D6=D8=
+=DB=E3=E1=DE=DF=EC|sp}=FFe]YX\[VQOPPPUYX_n|=F6=E4=DB=D8=D2=D1=D0=D0=CF=CF=
+=CF=D1=D5=DA=DC=E2=E9=F1=F9{xvfoyli^\[\]`aZ\^i}=FCyx=F8=ED=F2yngcejn|z~=EA=
+=DC=D8=D9=DA=D8=DB=E0=E7=EF=F8=F3=F5wtkghnod_jr=F4=FDjz=F0=EF=F5=FC=F1=F5=
+=F9=F6=FD=EC=E9=F9=FE=F1=EB=F1=EF=EA=F3=F3=EF=EE=F2necca^]`i~{lmx=EF=F2pj=
+l{=F6~=F1=F1=EA=E1=E7=E3=E0=E6=EC=EC=E3=E3=E9=E5=EF=F4zpslkejoilt~=FFlccj=
+nl^^jmfdj=FE=FCz=F4=E8=DD=DB=DA=D6=D5=CF=CE=CE=CE=CF=D3=DC=E4=EFxq\VXVVQM=
+NRTTSVY\__mnq=F6=EA=E4=DF=DC=D5=D7=D7=D4=D8=D1=CD=D7=DF=DB=D6=D7=DC=F1l=D9=
+=CDdEGl=D3=E9RKOb\LJLNW=EF=D9=DC=ED=FE=DF=CE=CC=CF=DB=DB=D2=D6=E2=EC=F0=E5=
+=E8=FDy=EC=EB_PW=FB=FEWPh=E5`Qfw_QT=FA=D9=E9g=E5=CF=D5=EBl=E8=D0=DA=F9=FA=
+=EC=FA=F0=D4=D9w]V^w=EF=EDxa\t=EF=E3=DA=ECk|=FDtm~k\ece=ED=DA=E1eU]=E3=CF=
+=D3=F7QQw=D9=E5gVLY=D4=CA=DB^N_=CC=C4=D6ia=ED=D0=CA=D0=E4k_og][bfOJV=F1=DF=
+hNOa=E3=D6=E3`Yg=D8=CE=E6=E5=D1=D8e_=D0=C9=D8{U\=DF=D9=DB=F2\PT=ED=CD=D2u=
+UPi=D4=D8eFD]}ah=E8{U\=DD=CC=D1=FAx=DD=DCn=FD=CA=CCZJ[=CF=CC_EMp=D6=D9vll=
+=EA=D6=EEg=F3=E7=EF]d=D9=CD=D5eOv=CB=D2jV^=E6=D6=DEZISi~{cOHO=F1=DBfIEo=CB=
+=D4cZ=E6=CE=CD=D1=D1=CE=CC=CE=D5=D1=CD=CC=CF=EBt=FC=ED=EArNGHNZOHIJMV_ecp=
+{=ED=D2=CE=DD=E8=D7=CE=D0=D5=D9=D5=D9=E4=DD=D6=DC=F8gn=EF=DC=DF]LNd=F3dME=
+Ml=7FYO_=F3=E2=DF=DF=D3=D0=DB=DE=DA=D2=D1=D6=DF=F4=E6=D4=D4=E4]P^=DE=DAU@=
+DZ=F1_NVzkLIh=D1=D5=F2=EA=CF=C9=CF=D7=D2=D0=DB=E8=D9=D0=DD]Sd=F5mYVa=ED=DB=
+=E5`T_=DF=D8lNU|mLBIX\Zu=D6=CE=D2=D3=CC=C6=C5=C9=CC=CF=D7=DE=DA=D7=FEOJPQ=
+KJQjrNCZ=C8=C9ZFU=F8XFU=D0=C1=C2=C3=BB=BA=C9=ED=EA=E6XCEYnZY=7F=DF=DE=D5=C9=
+=CA=D9=E8=E2=E9WA<<<76<H]=FB=DA=C6=BC=B8=BB=BE=BE=BB=BE=CB=DC=DC=D3=DAcR\=
+=EE=E8mckpgVKHQmX@<L=F0mRZ=DF=D3pu=C2=BD=EEK~=CA=E4B?s=C2=BE=CC=DC=C8=C0=D8=
+MABHKTk}=DD=C7=C1=C6=C9=C7=C2=C7=D2=E8j^M=3D67;=3D;=3DM~=E2=EA=E5=CE=C3=C2=C7=C7=
+=BF=BC=BE=C9=CE=C8=C4=CA=DC=F9=E1=D7=E8THObS?;>FC<?KetF;=E9=B9=C5IM=CD=C5=
+=E8MU=C5=AD=AE=C3=C7=B7=BA=E5C=3DAMb^W=DC=C4=C8=DA=E1=DD=DC=DCvQTeN<8;>>=3D@V=
+=DA=D5=F2=E6=C9=C0=C6=CC=C9=BE=BC=C4=DE=E2=D0=CE=E5l=7F=D7=D0=E4]Zs=F9ZGB=
+HNH@APu=FCXIh=BD=BFWK=D4=C7=FCAD=D4=B3=B4=CE=CC=B4=B7r<=3DJPHE[=CF=C5=D4=E9=
+=D4=CB=D3=F1kjbdXE=3D@C??BO=EB=D8{=EA=C5=BE=C6=CD=C5=BD=BD=C7=D3=CD=C9=D7ml=
+=E2=DDx[^qaOMOND>AGECH[=DF=EDIG=C7=BA=DES=D5=C8=D4]R=D9=B6=B4=C5=C2=B1=B6=
+=E7GISMDFp=CE=D5~=EB=D2=DD\RZeUDDMF::@FEKj=D0=C8=CD=CC=BF=BC=C1=C5=BD=BB=
+=BF=C9=CB=C9=CF=FF^~=E7dRV\OB?HLE>?FJHIW=ED=DB=D7=CC=DD=D2=B7=BA=CC=C5=C1=
+=CF=EA~=F5=D1=BF=C5=CD=BF=C0pGC@95:ENLS=F1=D8=DC=EB=DB=CD=CF=DC=E6=E1=DC=
+rLL\\NS=E3=CC=CA=D2=CE=C2=BF=CA=D3=CD=CB=D6=EFmcXLKQRMOZ^VLKYcOMZb][s=DF=D4=
+=CD=CB=CB=C9=E0=CE=BA=C6=DA=CD=DCeVV\=EC=D7=E1=D2=C7=DELGG>:;GZ]c=D5=C6=C7=
+=CB=C8=C1=C0=C7=CD=CF=DAoZOC>ADEJ\=DF=D6=DC=D3=CB=CC=D8=E4=DE=DEw^h=FDg_s=
+=E6=DF=DF=DF=E2=E7=ECh]qqWU]TVggh=FB=DF=D6=D7bP=C4=C3d=E2=E4YQNNf=D2=CF=CD=
+=BE=BF=DEniNC@GW_]=E7=CA=CE=CD=C7=C5=C4=C7=CA=CD=DB`LI?66;=3D<AW=DA=D5=D4=C9=
+=C0=C1=CA=CE=CE=D0=D8=E4=E1=D7=DE=E5=DA=D0=D7=DB=DA=D5=DD{dVQ\QFJLJIJNZ[i=
+=DFoL=D2=BD=ED=D9=C4=F2Z=EC=EFz=D0=C6=C6=BB=BA=C8=D7=DF[HA?ILJV=F1|}=DA=D5=
+=D6=D6=D1=D6=E3bTN?:=3D?@DQ=FA=CE=C9=C9=BF=BB=BD=C0=BF=C3=C9=CD=D8=E6~j_^Y=
+W`^ZZYPLKMTOKKP[[[=FD=D8=D7=D8=D4=EB=C6=BA=DC=D1=BC=D6Uz=ECp=EF=D7=D2=C4=C4=
+=D6=EEyOC?>?DDKdcj=D8=CF=CE=C8=C6=C6=C6=CC=D9=EB[GBEDAL^=E7=CE=CD=C8=C1=C3=
+=C8=C9=CC=D5=DF=FD]SNJJLNWZ^vo]Yd=F0=F9hm=F4=E6=FCp=EA=D8=CC=CA=DBe=E1=B7=
+=CDQ=C6=D5KPSOZ=FD=E9=CF=C5=CD=DB=E8jNHADJII]=F4y=DC=CE=CB=C6=C4=C3=C3=CB=
+=DDnYI>9:=3D>FV=F2=CD=C6=C3=BE=BC=BD=C0=C5=CA=D4=E9n[YRRY^ewm|=F1o^[]_TNNM=
+OOP^=F2=D6=E4O=CC=BB=EB=CD=C5o=FA=E3_y=D6=D9=CE=C0=C6=CB=CE=E5k\LGLJFMWVh=
+=E5=DB=CE=C9=C8=C5=C9=D4=F4WKD;69<>EW=F0=CF=C6=C2=BE=BC=BE=C1=C3=C8=CF=D5=
+=E9=FD=FFe]ky=FD=EF=EC=ED=F2mWUYOGFFHFHSf=F6_{=BC=C3=DA=BE=C4=E1=D8=EEm=D9=
+=DE=FF=CF=C5=D0=D8=D9=E8lUJMMDDNQO_=EB=DA=CF=CD=C8=C1=C6=CE=D2=DF\LFB=3D;=3DC=
+LRh=D7=C7=BF=BE=BE=BB=BB=BF=C7=CD=D9=EBdVQLNOPW_glh[[]VTOQQNOR\h~=E4=D8=CD=
+=C9=C7=C7=C5=C4=C8=CC=D0=D4=D8=DB=E3=E5=DF=DF=E3=F5ug^WQPNNNMMNOOOQRUZ^g{=
+=EF=E7=DC=D6=D2=CF=CD=CC=CC=CD=CF=D0=D5=DD=DF=DF=EC=F4=F3=F4=F6=FEtzyuxyo=
+kh][YWUVVWYYZ\]\`ho{=F8=ED=E7=E7=E6=E0=DD=DB=D7=D3=D0=CE=CD=CB=CA=CC=CE=D3=
+=D7=DD=E8=7Fi_YVSRPOOQTUUX\[^`bfiku=EF=E6=DE=D9=D7=D8=D9=D9=DD=DE=E2=EB=EA=
+=EA=EE=F1=F0=EF=EE=F5=FC=FB=F9}xvkiihlihotux=F4=F0=F7=F9=FF=FB~rqvsptztq=FF=
+=FD~=F8=F0=ED=ED=ED=EB=ED=F4=F5=FFrqos{=FE=F9=F1=EC=E6=E3=DE=DC=DC=DE=E1=E2=
+=EB|okd_`OW=C7h<Ch=D9=E9Z\=E1=EDKG=FE=EDWH=DA=B8=BEwo=CF=CA_J=F4=C2=C9^=FE=
+=C4=C2rHV=CC=CEQK=EF=D4bL\=D1=D7KCUcJ?L=EA=DFi=FC=CB=C7=DCh=E7=CF=E3WZ=DE=
+=D6=FD_=E2=CD=D0=FA=FE=D7=CD=D8=EA=E7=CF=CEjZ^fXKMWYOMUZZVU]g_b=F9=FCn=F7=
+=DE=D8=D5=D3=D1=CA=C8=CC=CC=CA=C9=CB=D0=D1=D3=D4=E4wwl^UONKGFGFEGJNPV^`=F5=
+=DE=DC=D9=D9=F4=D8=BB=C0=D9=D5=C7=CFuP_=CB=BF=C8=C5=BA=BB=CEWJH>46>HEIg=D4=
+=D3=E2=D8=C8=C7=D3=EE=EA=EA[HGLQW]f=FC=D1=CA=CB=C8=C4=C4=C1=C8=D9=E7=EAo^=
+TY`[STZ_ZS[`daRO\XLO=E2=D1=F0=F6=DA=D0=CB=D2=CB=BB=C8]=C3=BA=C7=CEL:DL@J=
+=CA=BC=C4=C7=CDdC:228=3DP=D8=CA=C8=C2=C5=C9=CF=DE=DE=DA=D6=D5=D2=D1=D7=E6=FE=
+m^RO]pXU=ED=CF=CC=CD=CF=D3=CE=DDZLPNEBKe=F1pi=E6=E0oVU\c][q=DC=D9=E1=DF=D2=
+=CE=CD=CB=CD=CF=D8=D5=D7=D7=CD=C9=EFAl=BE=CFOC;<A<:s=B8=BA=C9=CB=D8W>35F=
+}=CE=BF=B9=B8=BA=C4=DEe]_f=F1=DC=D1=D2=E0`LC>=3D?GHJk=CF=CA=CD=CD=C7=C3=CCz=
+j=DD=D8=E1=EE=E4=CF=C9=D0=DF=DF=EB[MNMMVdf_ZUVRLIU=E7=DCvr=E5=D7=CC=CC=C8=
+=D3U=CB=B5=B9=D5O>C^OL=C6=AF=B5=C9z?5314E=D8=BF=BB=C1=D7wXKGNn=CE=BF=BD=C2=
+=CB=DD`LA=3DBUm^k=D0=C4=C5=D7=FD=DA=C8=D2XP=F5=CC=CC=F9Z}=D6=DDdTVgkSLUq=EC=
+=E8kW[ZVX]Xk=CD=C4=CF=7Fr=D9=C7=C2=D1PA=E0=BC=C7ZBBOuOG=CF=B4=B8=CF]D>A>>=
+S=CE=BE=BC=C2=D5=F9la[f=E3=CD=C5=C6=D1=FBWJB>=3DDONL`=DB=D9=E3t=FC=D0=CD=F3=
+[=EE=CF=C9=CC=D7=DD=D6=D1=D7=D9=D9=DA=DB=E2=FCignxw^UNGELTVZ=FD=D5=D6qNW=E2=
+=D1=CF=EANK=CC=BB=C4yHKw=D9_S=C7=B5=B7=C6pIBFADW=DD=C7=C5=D2hPMLMTh=DB=CD=
+=D1=E3y^PIIKMW=FE=DC=D1=CB=CA=CC=CB=C7=CB=D6=DA=DC=D2=C9=C9=DFX`=E9=E0oON=
+TWKBFOUPMOTVUU]q=E8=CE=C4=C5=CF=D9=D0=C9=C0=C2=CE=D6r=EF=BE=BA=CFD=3DP=EDm>=
+=3D=DE=BD=C5W=3D<@A:=3DM=DE=C7=C7=D4=E6=E2=DD=E6=ED=DD=CD=C1=BC=BF=C8=D7=EFxYG=
+AL_wwfy=DA=DDbT_=E7=DEvey=E6=EAlb|=D5=D6fS_=EF=EEdYl=D8=DC_Sc=EE=FC][=E9=D3=
+=EB[x=CF=C8=CF=ECkV=EC=C4=CC[FT=EC=E9TD=F4=B9=BB=E6R=F5=E8P<9M=D8=CF=EA=FC=
+=DB=D2=E7QHU=F5=DC=DB=D6=CF=D1=DFt^JCNcl`n=DC=C9=C8=E2o=E2=D0=CE=D8=E2=E3=
+=D6=D4=DF=E0=DF=E9~nd`[OKSb[QTanZJKf=EEnRT=F0=D4=CE=CE=C8=D7^=CD=BA=C3WK=E6=
+=C8=D0HF=C3=AF=BD_i=CF=E5@2:]=FAOO=E8=D3=FBGF\=FC][=DA=C6=CB=F8b=E7=E1Q@=
+M=EF=FAY]=DD=C6=C9=E9=E6=C6=C3=D4=E5=D9=CD=D4}x=DA=D3=EEaik^RMOUNLT[YSX_`=
+_j=F3=F0=EF=E7=DD=CD=C3=C3=D3g=DB=BE=C9XMk=DB=F7GF=DF=C2=D0b=E7=D0sD<DLIM=
+r=D5=D9=DF=DE=D2=D2=EB=EC=D4=C9=CB=D1=D9=D4=D5bKNXJELT_^Xh=DC=DB=E9=D9=CF=
+=D2=DF=F4=E5=DF=E7=FE=E1=D7=E4p=F2=DFy\Zlx[Ydma_n=FDujak=E7=E6=EA=D1=C7=CF=
+=FC=D1=BE=CFai=E7{OFN=F6=EF^c=E4=F5UHEGD@I\_i=DB=CE=CE=CD=CA=C6=C3=C5=C8=C3=
+=C2=CD=DF=E3=FC^XWVY\[_mjdf{sgdf_[VTWX\dosv=FF=F5=F8=ED=E8=E1=DF=DE=DB=D6=
+=D4=D2=D2=DC=DF=DE=DF=E0=E2=E5=E5=E6=EEi=DF=DF\[nVNONNYb`n=E8=EA|=FErnd^b=
+|wz=EA=DD=DA=D3=CE=CE=CF=CC=CB=CF=DC=E7=F3eWNLKIJJLO[_o=E9=E0=DC=D4=CF=D3=
+=D5=D4=D5=D6=D9=DD=DE=E0=E5=EE=EC=EA=EE=FAzvmg_[ZYVTVVWZ]__r=F9=FA=EA=E8=E6=
+=E2=DC=E2=DE=D6=DC=DE=DF=E1=DB=D7=D9=DC=D6=D7=DE=E1=E6=EF{=F6=FBnd^\XVSOO=
+PQRSUX\_bk=FE=EF=E6=DF=DF=DD=DA=D9=D6=D5=D5=D3=CF=CF=CF=CF=CF=D2=D5=DA=E0=
+=E7=FAjb_\YXVSPOPPQSTUY]bkw~=FB=EF=E9=E2=DE=DC=D9=D7=D4=D4=D4=D5=D4=D6=DA=
+=DB=DE=E2=EB=F4=F2=EF=EE=EF=F1=FErong`_^^^^`cbeeca___`aehjlv=7F=F4=F1=ED=EA=
+=E8=E4=E1=DF=DF=DE=DD=DC=DC=DD=E1=E6=E7=EA=EF=F7=F9=F9=F7=F8=F9=FA=F5=F1=F5=
+=FB=F7=F2=F9}xqlid][[[YY[^emy=F7=EF=ED=EA=E6=E5=E5=E5=E6=EA=EB=E7=E4=E1=DE=
+=DE=DE=E1=E8=EF=FC=7F=7F|yrnlhb_[YZZZ[^ajsy=FB=F0=EA=E4=E0=E1=E1=DF=DE=DC=
+=D9=D8=D7=D9=DC=DD=E1=EBzjgd`_^^^bb__agjjikllms=FD=F7=F3=EE=E9=E3=DF=DC=D8=
+=D6=D5=D4=D3=D6=DB=DF=E2=E3=EB=F8xpnd`^]\XTSTTUWXY]`fmv=FA=EC=E8=E8=E6=E4=
+=DE=DA=D8=D7=D8=D9=DB=DD=E1=E4=E5=E7=EC=F2=F8=FA=FE~~~=FB=FBupospqvrpniea=
+deeksy|=FC=FE}=FE=FE=FF=FE=FA=FB=FF=FB=F1=F0=EF=EB=EA=EC=EA=EB=F4=FFwspon=
+poqtvsrnotu}~=FA=EE=EB=EB=EC=EC=EC=ED=F1=F6=FC=FD~xx{~=FA=F2=F0=F3=F5=F5=F2=
+=EE=EB=E8=E7=E5=E5=E5=EA=F0=FFqh`[VVWYYZ\_hms}=F3=EA=E9=E6=E2=DF=DF=DE=DD=
+=DD=DE=DF=DF=E1=E4=E7=EA=EA=E9=E7=EB=EC=EC=F2=FBtjd`]ZZ[[[[[\^`dn=FD=EE=E3=
+=DE=DB=D9=D8=D8=D8=D8=D8=DA=DC=E3=EC=FDmd_^]Z[\^__eps|=F8=EF=E9=E5=E4=E1=E0=
+=E4=E6=E9=ED=F5=F7=F3=F5=F8=F3=EF=EF=F2=EF=F0=F6=FA=FD|vomjhjheccdbeihkty=
+y=F8=F1=F3=EF=F0=F1=ED=E8=E2=DD=DB=D9=D8=D9=DA=DE=E4=EB=FBuokd_\ZZXXZZ]^^=
+`foy=F5=EC=E3=DE=DC=D9=D8=D7=D8=D8=D9=DA=DD=DF=E3=EB=F7ynifc_][\\]^_ceitz=
+=FD=F6=EE=E7=E2=E1=E0=E2=E0=E1=E3=E5=E7=EA=EE=F8yqolhZ=EC=E4JG=CF=C5?I=CB=
+=CF=CC=D4TQ=C2=C3>L=D9lVv=CE=E7O=FAkZ=FEmU=F1=C9=FCP~=F1mu=EF=DF=DF=D0=DF=
+=EF=DD=E6z=EF=F9=EE=EE=EF=ED}nj^X\[\bedgnuor=F3=F3=EC=E4=E9=EC=ED=EB=E8=E8=
+=E9=F9=EC=EE=ED=EAz=FA~=EA=E3=EC=DF=DD=E2=E5=E0=ED=F5=E9nodZcXX\TZUO[ZY^i=
+=F9=EF=F2=EA=DD=D7=DE=DD=DC=DC=D3=DB=DA=D3=DA=D9=DA=E5=DE=DC=F0=E9=E6qefk=
+b]\]beZS[\W[Ye=EB=E6=FB=FD=FBfwlw=E1=ED=E3=D7=D4=D7=E4=E1=D6=D5=E3=DC=DB=E1=
+=E1o=FE=FFn_V\b[PSXWUOVZYrun=E9=D6=D5=DB=DD=D9=D0=D5=DA=D9=DA=D9=E0=F3=E9=
+=EC~=FB=E8=ED=E8=EB=F9=F8opmmecheZSUYUW^dhcl=F3=EE=F5=E8=D8=D5=EEm=EF=DE=E9=
+oo=E8=E0=F0=FE=7F=EF=E5=DC=F3s=EC=DF=E9x=EB=DC=DE=F0=FC=EFtZ_=E2=D7=E8[Y=F4=
+yUN]=F0gVb=F2i[\]r=F1wd=EB=D3=E8a=EB=CC=D3jz=D2=CF=E3=EB=D7=D4=F7l=DF=D9=EF=
+p=E1=D1=D7kYh=F9YKO`bRMRRMLPX\oog`=F8=D9=D7=D8=D1=CB=C9=CA=CF=D4=D1=C8=C3=
+=D3s=D0=BE=D2MLSe=DD_IV=E4M:X=DFI@=E1=CA`Z=EF=DF=E5[O\=E5qHS=D8=D6k=F4=CB=
+=C6=D6=ED=E2=DA=CE=CF=DB=F4=DE=CC=D0oKLtwQI\=DE=E7x=F6=DF=ECkz=FCzq=E9=D5=
+=E8dnyk=E8=D4=DErhPEOan=EDOK=C9=BAm9G=D7=EEC>=E8=B4=B6=DA=D4=B4=B8V8D=F2\=
+B\=C0=BE=DDc=EA=E1UER=D4=CD=EBo=DF=E6[KEGMJEKf=FAv|=E9=D4=C9=CC=D6=D8=CD=CD=
+=DF=EB=D6=CD=D6=EC=DB=CD=D8pb=F0=DChMMk=DCnHKanJEWN}=BC=C7[N=E7=E1H?H=D9=B3=
+=B5=CA=C8=BD=D2<0;IO\=DE=C0=BE=CEg\oaW=EA=CA=C2=C6=C9=CE=FCPEBHE@Gl=E5q=FD=
+=D9=CE=CC=CE=D5=D8=D6=D2=D3=D5=D7=D6=D8=DEq]as]PP\PHK[v`MO`k_hXU=C3=B7=D1=
+M=E6=C6=F3BE=E5=B2=AE=C6=D8=BD=C6A/8JKKl=CC=C6=DDQMYZO^=CE=C1=C4=CB=CF=D2=
+=DEYBAT~]Sm=CE=C9=DEZx=CA=CBm^=DD=CD=DF_f=E3=DAjSgyWLQi`MNf=E1=E6]Vkp`h=EB=
+=E9[=D3=B8=BDkT=DC=D8K=3DJ=C5=AF=BD=EC=CE=C2f62BYh=F4=CE=C0=C7uO[m_e=DB=C6=C1=
+=CB=DB=F3dMBCDCOnv\^=E8=DA=E8iu=D5=CB=DA=FF=DD=CA=CD=E0=EE=D5=CD=DAo`}=F6=
+aZe{lRP_m`NOd=7F^W=E0=E5c=CD=C3=DC\d^FG^=D7=B6=B2=C3=CC=C7=F9;3?W=ED=D3=CB=
+=C0=C2=DCSKW_d=E4=CE=C5=CA=DDeOIC??EOu=F2fh=E9=D5=DFp=DE=CB=C6=D1=F3=DC=CC=
+=CE=ECu=D4=CD=E1ll=EFiOO_=F2fQV\_]OLW=FA=FAi=F5=CE=CEk=DD=C5=C7=DAaQQd=ED=
+=E5=BF=B5=C0=D9=F7WA9<Gf=DB=DC=D4=D6zSLRc=EB=D2=C9=C1=C3=CF=EDZJDGJLOp=DB=
+=DA=E9=FA=E0=D5=DD=E5=D9=CB=CA=DC=F5=E7=D7=DDd^uo]RV]\X_=FD=FAeajl_[}=D7=DC=
+ac=DE=CF=D8=E3=EEW=F1=C2=C9=F4WQNR[[=CE=B8=BD=D6=F4hN@>H}=CF=D1=D1=D2=EF[=
+KLa=E2=D7=CF=C9=C7=D7eJGLF@L=F2=DDpZj=DB=D8k[=D4=BF=C3=D9=E3=CC=C3=CEm}=CD=
+=CA=ECTV=F1=EBUITmYJIOOJNZ[OM^=D7=CF=FA=F0=DD=D9=C0=BD=CDbl=D4=D1=E1i=D7=B6=
+=B2=C3n`=F2T=3D:I=E7=DAiWTWMDFT=EC=DA=D6=D0=D1=E4^OSWRO\=DF=D3=E4ir=DA=CF=D6=
+=DF=D7=C9=C7=D2=E6=EC=D8=D1=DDzz=E2=EE]SV[XSYnmSMTbh[Ww=D8=DAl^=E6=CF=EB=DE=
+=CC=CB=CF=F2[M^ux=CF=C2=C0=C8=DF`I?>CVr=DC=D3=DD=F9fY_n=E0=CF=C6=C2=C5=CB=
+=DFbUNNKHNWhaSQ]=FBsm=F5=DD=D2=D8=DE=D8=D3=D2=DA=DC=D8=D3=D8=E6=E6=E6=EA=FC=
+g]^feYSRZ\TOOg=E2=F8bj=F1d=E9=C9=C8=CF=F8_d=F2~_=D3=BC=B9=BF=D9fQF?>Ik=DB=
+=D7=EFe_XXT^=E7=CE=C9=CF=D6=E8m]NHHM[=FC=F1hf=EC=DE=DE=E0=DF=D4=CC=CA=CC=CC=
+=CD=D2=DA=DF=DF=DB=E1=EFphh]XPNNMKJLNLHKX]al=EA=D9=D6=E8r=D4=BF=BB=C3=DAy=
+=D8=CA=D0=E6=D5=BD=B7=BD=DCTNKB;>MnrPJJMNLU=F2=CF=CA=C9=CC=D3=D9=E9t`TT_=F1=
+=E9=F7=F6=E7=D7=D9=ED|s=F2=EC=F0=FD=E9=DF=E8=EFr|=F0=F4|v=F9=EF=FAmcdddZU=
+[a^XZg=FD=F4{=FD=DE=D3=D4=DA=DE=DD=DC=D7=D8=DB=DA=E7=FFieh=FB=E2=DC=D5=D4=
+=D9=E8aRMMQ[hjvoe_`el=FB=EA=E2=E3=EA}jc\XVVX^gv=F6=E7=DE=D8=D2=D0=D0=CE=CC=
+=CD=CE=CF=CF=D4=D7=DD=EB=F9widc_[WQMKIJJKLMPV\f|=E9=DC=D4=D0=CF=CD=CC=CC=CD=
+=CC=CC=CD=CE=D2=D7=DD=E8=F2=FDywrqqh_ZWTRRTWZ^^]]dkknv}yz}=FC=F6=ED=E6=E5=
+=E5=E6=E5=E5=E4=E3=DF=DF=DF=DF=DF=DE=DE=DD=DB=DB=DC=DF=E6=E8=EE=FAuke\ZXV=
+VVWXZZXXWY^hz=F7=E4=DD=DB=D9=D8=DA=DC=DA=D9=DA=DC=DD=DB=DB=DE=E3=E8=ED=EF=
+=F8}uohfhklkmnmhbbggffikkknplifglpz=F4=E9=EA=EA=EC=F0=ED=ED=EB=E8=E3=E1=E2=
+=E4=E4=E4=E2=E2=E4=E3=DF=E0=E1=E3=EA=F2}j`^[YXYY[[ZYZ^biv=EE=E6=DF=DB=D7=D5=
+=D4=D4=D5=D7=DC=DE=E2=ED=F5~vmfa`achlmlkiihhjmnqusppv|=F7=ED=E9=E5=E6=E7=E5=
+=E6=EA=E8=EA=EA=E8=E8=ED=F6xjfghjlpsqkfinor=FC=F2=EB=E1=DF=DF=DE=DD=DC=DF=
+=E5=EA=EB=F1=F9yuod^[YXWWXY[]do{=F6=EA=E1=DE=DA=D8=D7=D6=D7=D8=DB=DF=E6=EF=
+=F6~vrpxvtmlkiihikmnmklmo{~=FD=FC=F8=F2=F5=7Fy{xvx}=7F=FD=FF=FC=F5=EF=EE=EC=
+=EE=EF=EF=F1=F0=FB=FC=F6=F4=EE=EB=EA=EA=EB=ED=EC=EF=F3=F9=FDwojf_\\_cchmo=
+ps=FC=F3=EA=E1=DF=DE=DD=DC=DA=DB=DD=DF=E8=F3=FCsg`^ZXWVXXX[^_djn=FE=F3=E8=
+=DD=D9=D7=D5=D4=D3=D4=D7=DB=DE=DF=E2=E3=E6=E7=E9=EA=EE=F6xld_\[]^^acbdgil=
+spoonmmnopw=FB=F0=F0=F0=EE=ED=EC=EE=F1=F1=EF=F0=F1=EE=EA=EA=E8=E7=E4=DF=E0=
+=DE=DD=DD=DD=E1=E5=E8=F3vojd^]]\[ZWXZ[^dn=FE=EF=E9=E7=E7=E9=EA=ED=EF=F0=EE=
+=E9=E3=E0=E2=E1=E2=E7=EE=F8=FB=FCypnmkkkmr|=F3=EE=EB=EE=EF=EB=E8=E8=EB=EE=
+=FAvka^^^^agmw=FE=F5=F2=ED=E9=E9=E5=E7=EB=ED=EC=EC=EE=F4=F6=FA=FD=FCzxtnj=
+fccacgnw=F9=EF=EB=E6=E1=E1=E2=E4=E6=EA=F3=F9=FB}|usuuxuuz}=F9=EF=EA=E7=E6=
+=E7=EA=EC=EF=FDyojf`^^____`__`ehmy=F6=EB=E6=E3=DF=DD=DB=DB=DC=DB=DA=DB=DB=
+=DB=DB=DE=E2=E5=EF~roojf`][[[\][]^^aelv=FF=F4=EC=E2=DE=DC=DD=DD=DC=DD=DE=DF=
+=DF=E3=EC=F8~oiea`__^^`dfinpw=7F=F7=EE=E7=E0=DD=DB=DA=D8=D5=D2=D1=D3=D4=D6=
+=DB=E1=F0yj_[URONNOOQTW[^floz=F5=EC=E6=E2=DE=DE=DC=DA=DA=D9=D9=DA=DB=DB=DB=
+=DC=DE=DF=E0=E2=E4=E7=EE=FByplhd_]\[\\[_cgijr=FE=F5=F1=EF=EC=EA=EB=EE=EF=F5=
+|qmnlkkq=FF=F5=ED=EC=E9=E3=E0=DD=DA=D8=D5=D4=D5=D9=DF=EBrf^YXUUVVVXYYZ\_b=
+fkty=F0=E9=E3=DC=DA=D7=D4=D2=D0=CF=D0=D1=D1=D3=D5=D8=DC=E0=EA=F5sf`^\]ZWV=
+TRRSSUWXZY[aekw=F8=EB=E3=E0=DD=D9=D8=D5=D3=D1=CF=CF=D0=D3=D5=D7=DA=DE=E3=ED=
+=FAvha^[VUUVYZ^hnx=FE=F6=F3=F3=FA~tmmmmpusvwuqons{=FE=FA=F4=EE=E9=E3=E1=DF=
+=DC=DC=DB=DB=DA=D9=DC=DE=E0=E1=DF=E3=E6=E8=EC=F9=FEyog_[WTRPNNNNOOPSX^gx=ED=
+=E2=DC=D6=D2=CF=CE=CE=CC=CC=CC=CD=CE=D0=D5=D9=DB=DF=E8=EE=EF=FDonieb__\XV=
+SPONOOPRUY^dl|=F0=E7=DF=D9=D4=D1=CF=CE=CE=CE=CE=CE=CE=CF=CF=D3=D7=DA=DC=E3=
+=EC=FEk]VPMKJJIJJKLNSY_iy=EC=E0=D9=D4=D0=CD=CC=CA=CA=CA=CA=CB=CB=CC=CE=D0=
+=D5=D9=DD=E7=F3o_YTOLKKJJJJKMOSX^jz=F2=E3=DC=D8=D5=D3=D0=CE=CC=CB=CA=CA=CC=
+=CD=CF=D1=D5=DB=E1=EEyf[TROLJIIJKNQZeu=EE=E4=DD=DA=D8=D6=D7=D8=D8=EB=D9=BF=
+uF=DB=E2=CE=CCU=E9=D5x=F8Y=F1=D5VX=E0=D2=DDFd=C7OU>N=CB\=EF[=FA=CCHTUZ=F9=
+E=E7=DA=EB=CEi=D0=CD~=D0=DE=CE=CC=E9=D0=E2=D0=DCVwdkfOlz=DDmy=DCl=E9\O`HO=
+MNKOkm=E2=EC~=D0=CF=E2=EF=CA=CA=CC=D7=EF=CA=C9=D1\=E9=D3=FCiSw=D9rWW=FF\S=
+NJtYGQamOS=E4=E2=D2zf=CF=CA=D9V=DE=CA=CE=D8=EA=D2=C5=D4Uh=CC=F2`S\=D2wKO=DA=
+fORW=E7nO_=EF=F8W=FC=D9=FD~g=E1=CF=F3U=F7=CA=D9bm=DC=D1=E7\~=CD=D0mc=DB=D3=
+nNY=F7tNHUp[JN~=E7h]=FB=D9=DC=EB=E4=D6=D1=D6=DE=D5=CB=CF=DC=DA=CE=D3=E6~=FF=
+=DE=E1l[g~]SVTOIFJKFCNbdY]=E5=D4=DA=DF=D1=C9=C7=CA=CE=CA=C5=CA=D3=D0=CB=CD=
+=D7=E9=E5=DE=F1c[cbTLPWPJIPVNJP_aXYu=EF~u=E6=D5=D6=DC=D7=CA=C5=CD=D8=D0=C8=
+=C8=D6=DE=D7=D2=DEo_a^QOOOONSZXSTl=FCmjw=ED=E4=F3r=F4=DF=DE=FBp=DE=D0=D9=F3=
+=F5=DA=D8=DF=EF=EB=D4=CF=D9=EC=DD=D0=DFgbuoZXYQMLNZ]VZ=ED=DB=EEu=EA=D7=D3=
+=DFpfsfQKMU\bu=DD=CD=C7=C4=C1=C1=C1=BF=C3=C9=D2=DF=ECvXJ>?SM@KMIHIJT=F8id=
+=D9=C7=CE=E2=DA=D4=CF=D0=DF=E8=DD=D4=DF=F4=ED=DC=D1=CF=D0=CE=C8=C5=CA=D4=DC=
+=DFpM><>=3D96:AIJQ=EE=CD=C4=C2=BE=BA=B6=B7=BC=C2=C5=C9=D4wTTVNH@BHJJKOU\_Y=
+Tx=D3=D4=D2=D0=D2=CD=C7=C9=CD=C6=C1=C1=C1=C2=CA=D8=E3iVKC?>><<<?EJR_=F7=D3=
+=CB=CD=D2=D5=CF=CC=D2=F9g=FE=E8=EEln=E3=D2=CA=CA=C7=BF=BA=B8=BA=BD=C0=C4=CB=
+=DE]OID?;657989;?JQMU=DB=CC=CC=CD=CD=CD=C7=C2=C6=C6=BF=BE=BE=BE=C0=C7=CA=CC=
+=D6=E7pZROJFEFJMPUi=DA=DB=E8=E1=E6=E0=DFmUTUMGDEMYh=FA=D8=CC=C4=BE=BD=BC=BB=
+=BA=BC=BF=C7=CD=CF=DBsTJEEC??@CFGENcdd\UTckd=FE=E5=DB=CF=C9=CA=CB=C9=C9=C9=
+=CA=CE=D5=DA=DE=E4=EE{jl|=EE=EB=DF=D4=D4=D7=E0}xr\MIFCA?>AIR_=F6=DB=CD=C5=
+=C0=BE=BD=BE=BE=BF=C4=CB=D0=DD=FF[MFEEBCDIKKVc=FC=DE=F0ik=E6=D9=D7=DB=E9=E2=
+=D2=CE=D6=E0=F5=FC=E8=E2=F0megntliu=EB=DB=D4=D1=D1=CD=C9=CE=D9=F1rvnWJGGK=
+KIJSi=E6=D9=CF=CC=C7=C2=C2=C5=C9=CE=D9=E9v_WLD?@BB@AEKO`=FB=D9=CF=D3=D5=D1=
+=CA=C6=C3=C3=C6=C4=C1=C2=C7=CD=D7=DD=DE=EAlYOJIHFFGKS^hy=EA=E1=E3=EFlckkb=
+YTU[bhq=EB=D7=CD=C8=C5=C2=BF=BF=BF=C1=C4=C9=D0=DD=FBaVLD?=3D>?@CGJNa=F0=DD=D9=
+=EAz=EB=D7=D2=D4=DB=DB=D3=CD=CE=D6=E2=FFx=FEo^YUW\ej~=EB=DD=D3=CE=CA=C7=C6=
+=C9=CC=D5=DF=F4hXNHDCBACHO]y=E0=D5=CB=C6=C4=C3=C4=C4=C7=CC=D6=DF=ECv\MDAC=
+FHIKM]=E9=D6=CD=CF=DC=DB=CE=C8=C9=CD=D5=D4=CD=CC=D3=EA\SUSMHDDGJKLR`=EC=D4=
+=CC=C7=C3=C2=C2=C5=CC=D2=D8=E0w^PKHHGGLVk=EA=DB=D1=CA=C6=C2=C1=C2=C5=C8=CD=
+=D3=DD=FB^OHEFGEEJNOWg=E5=D5=DCxj=F3=DE=DC=E7or=E5=DB=DD=E8qj=FA=EC=FBi\]=
+iooqz=F2=DD=D1=CB=C7=C5=C8=CA=CE=D6=DD=F5bTKEB@??@FQc=ED=D9=CD=C6=BF=BD=BD=
+=BD=BE=BE=C1=CB=DB=EDrZI?>?DDDGM]=E9=DA=D6=D9=E5=EE=E9=EC=F9=FDmo=EC=E8=FB=
+l_^msnmiflqro=FE=EB=D9=CE=CA=C4=C0=C2=C7=CB=CF=D3=DA=F4]OIGD@?CJVf{=E3=D3=
+=CC=C7=C5=C7=C9=CC=D1=DD=F5aUNKIHIKMQ[t=E3=DA=D7=D5=CF=CD=CD=CE=D2=DD=E9{=
+f]ZWSRPTYXZ\d=F6=E0=E4=F7{=FE=F1=F6w=F1=D8=CC=C7=C4=C3=C2=C0=C0=C6=CE=DD=FC=
+k]OF?=3D<<=3D?EN^=EB=D6=CD=C9=C6=C4=C2=C2=C4=C8=CB=CD=D4=D9=E8s_YUOJGEDDDDDEG=
+KSZh=EF=DD=D4=CF=CB=C6=C1=BF=BF=BE=BF=BF=C0=C2=C5=C8=CD=D8=E5pYNIGECBBEHN=
+QUY_lu=F6=EB=E1=DE=DE=DD=D8=D4=D3=D4=D3=D0=CF=D2=D5=D6=DA=DC=E4=F1=FBoe]U=
+QONMLLMNPRW^ju=FF=EE=E6=DF=D9=D6=D6=D5=D3=D1=D2=D2=D3=D2=CF=D0=D3=D5=D7=DA=
+=DF=EA=F3=FBzlf^YTOMKLMOQUZam=F9=EA=DE=D9=D5=D1=CF=D0=D4=D5=D6=D8=DC=E4=EA=
+=F3}mea`_][Y[aaaefjkfkov}unx=FA=F9=FD~=FA=F4=E9=E4=E0=DD=DC=DB=DA=DB=DB=DC=
+=DF=E2=E4=E5=E7=EC=EE=F7=FEtoommkha]\YYYZ\_^al}=F6=EF=E6=DE=DA=DB=DC=DC=DC=
+=DF=E9=E8=E7=EE=F3xmhd_]___cfmr}=F0=F3=F3=EC=E9=EC=F5=ED=E8=E6=EA=EF=EF=EC=
+=E6=E7=EA=E9=E7=E8=EF=FDz=FC~=FDthknmmikrx~kdcb_ZY]hmln=EB=DF=E0=E3=DF=D9=
+=D5=D7=D9=DC=DE=DD=DF=EC=F3=F5wmlh`_]^_bd_ev=EB=F8o{=EE=E7=ED=FD=FD=F6=E8=
+=DC=EDbm=DA=D5=DF=EF=E3=D0=CF=EEURg=F8`T\=EF=E4w^]hyo]WW\]XY_ggaf=FB=E4=DB=
+=DA=D7=D1=CE=CD=CF=CF=CE=CF=D4=D9=D9=DA=DD=E2=E4=E5=E1=E2=E6=EE=F6=F3l]ZZ=
+SLKLNLJJKOSTW^q}w=F8=DC=D5=DC=E5=DB=D1=D4=DD=DE=D2=CC=CB=CF=D2=D0=D1=DB=F0=
+=F1=E6=F0ian=7Fkabnt`VON]=F9lVMPg=FFbZz=D3=CC=D5=EC=E1=D0=D3=F5`i=EC=E0=F8=
+o=EB=D8=D3=DB=E4=DD=D2=D1=DC=EA=F7pZIAEIE=3D<J|=E3{=ED=C9=BC=BE=CC=D9=CB=C2=
+=CB=FB_=E7=CE=DB[Qo=DE=FA]a=ED=E3oQKNOHCFJNTYmzh=D8=BF=BD=C3=CA=CD=C6=CC=FF=
+\=E1=C2=BD=C4=CE=C8=C7=EDI>>ED?ANp=EC=FA=EF=DA=CE=D0=DD=EC=EFxXG?=3D=3D=3D?GW}=DF=
+=CC=BF=BD=C0=C2=BE=B9=B9=BF=C9=C1=BC=C7pVi=E6eIGYbJ=3D>GH?=3DHXPHO=FF=F1bh=D4=
+=C0=C4=D4=D8=C5=BE=C7=E3=DE=CB=CB=DD=E8=DD=DDUR=C3=B9=CBL@=EE=CCO8=3D=CC=BB=
+=CCXX=D9=E8>02?JD@T=CD=C4=CF=DB=CE=C4=C2=CE=D7=CD=C5=CD=EAn=FE=F2hORo=DF=7F=
+`=F5=D3=CD=CE=D6=CE=C8=C6=D2=F5peZQOPTRQPOMNNKIGKQOP]=EB=D8=D7=E1=E3=CD=C6=
+=C8=C7=C5=C4=C2=C1=C4=CE=D9=D7=D7=CD=D0=ED=F0}Nk=D1|H=3D=3DX=F9A6F=CD=C5=E8KL=
+=ECx=3D6?[=E8=E2=DE=CB=BC=BB=C3=CC=CC=C8=C5=CD=D5=CF=C9=CF=E6c\SF=3D<I]MAJ=DD=
+=C6=CDq_=D5=C9=E0NX=D0=CC=FCX=DC=BE=C2bHq=C7=D4JBt=CB=EBA?h=D1gGO=E3=D1{N=
+[=D4=C8=CD=DD=DA=C9=CB=FCWTm=D4pK[=CC=C5=D8D?=DD=BA=C9G;L=CD=C8O=3D`=BD=BB=CE=
+]P=F8{B7;M=F6=E4|=EE=D2=CE=E0ia=FC=DB=CD=C9=C9=C9=CA=D1=E9eVQMU=F4=D3=E6a=
+=DF=C7=C2=CDxb=DB=D3fKZ=F6sRLPcTGDJSWUOYlko=F1=DD=CF=CC=D2=DD=D7=CD=CB=CD=
+=CB=C4=BF=C0=C9=D2=D8=DD=EFe]RMMPOOLJJGBEPg=FB`Sg=D5=D7=E9=ED=D3=C2=BC=C4=
+=D9=DF=D9=EF`Y]=EE=D9=DD=EEl_aqeZ^=ED=D6=D7=E4|=EE=E6pSKSk=F7YU=DF=C8=C8=E3=
+]o=D2=D7]Ny=CD=D4ZIQ=DE=DEUK^=F9jUNm=D6=E1bs=D3=D4yV^=D6=CA=D9w=DC=CA=C6=D1=
+uy=D8=D5=EFjx=FChYSS\hZ]XMKQOg=DE=FC\WL]=CF=D0x=F1=CA=BF=BD=C7=E9=F8=D3=F0=
+MJW=E9=CF=D9=F4=E1=D3=DFhOR=FA=D3=DFfi=F2=FCUA?LaQDW=CF=C8=DDd=EE=C6=BE=CE=
+f=E0=C1=C1=FCU=ED=CA=CBlKV=E2lHEO]XLLV[MIV=F0=DC=EDv=EF=D5=CC=D2=D7=CD=C1=
+=BE=CB=E4=E0=CE=CC=DEicx=DE=DF=EA=E9oZTKHU=E1dHKLKH@C\=DF=FA=7F=CF=CF=E6=FA=
+=F3=DD=DB=E2=D9=C6=BE=C3=CA=CB=CD=CC=DA|=7F=E1=DC=E3=F5f[WKDCED=3D>Ks=EDhl=DE=
+=CF=D7=F9z=D7=C5=CB=DB=D7=CD=CA=D5xf=DA=CC=E1\p=E1=EA\LU=EBvIE[nTJJe=E0cN=
+=F9=CB=CE=DF=EC=E4=DF=E9=ED=D9=CB=CD=D6=D5=D2=CE=D9mh=D6=CD=E3_]f=DD=D9dN=
+QMDBHJQc=F7=F6iVXrdM^=D1=C6=CF=EE=DC=C6=C5=E6b=DA=C7=CA=DD=EB=D7=C9=D1ca=E3=
+=E0S>?R=EDbJU=D9=CFjM]=D5=CB=DEh=EC=D2=DE_f=E8=E9=F8\XkhTS_\T\_ekgm=E5=D2=
+=CF=D0=D2=D5=CF=CA=CA=D0=D9=D6=D3=D4=DFbl=E9mRILScfLKPNM^aqqX=EB=C2=C8QC|=
+=C6=C5=E1]=CC=BB=C7QQ=D2=C7=E1OT=DA=D7RHY=E3=DF]Oe=ECYFQ=ED=DB=E8o=E6=D1=D7=
+_NWn=ED=E6=EB=DC=CD=CC=CF=DA=EE=EF=DB=D9=F9k=FE=DD=D3zNTiRCBFNQIKk=F2ZV{=DC=
+=D5=D4=D2=C9=C3=C6=CC=CF=CB=C4=C2=C4=C6=CB=D0=D5=EESNXZYRKHI?9ATOUUU=F6=E5=
+MI=E1=CF=D6=CC=D1=CE=C6=CE=F3=E9=CB=CD=D4=CC=CB=CE~On=CF=DCNK~=CF=DAI>U=D9=
+zIJo=D6=DAo=F4=D2=D6=EC=DF=D1=D3qUt=D2=D7WJd=DB=F6J@P=DC=E2KDa=D3=DC\V=F1=
+=CD=D0d]=DD=D6t]=EA=CF=D0=FFb=E0=CF=DBgv=CF=C7=D2y=EF=D0=CFuU=FE=CE=D4_LQ=
+v=F2TH[=DDyG?M=E8=E3K@_=D6jGM=F1=DBjm=CA=BE=C8bc=C4=B8=C6_=EA=BB=B6=CBMJ=EF=
+=D3V>E|=EFI>DZ^KJ_=D9=DEbk=D7=CE=D6=DF=D7=CF=CE=E0bj=DD=DA=EB=F5=FE=F0=E1=
+=EA^X_hv=F1=F9=F9=7Fc\=EB=D7=FC[Zd=EF=E4j]=FC=DE=F7rh\f=E2=DEr`h=E9=D9=F2=
+W[=E5=D4=DFiZj=ECwi=F8=DF=DA=DF=E0=E1=ED=FE=EC=DC=E2=F7=EA=DB=D6=DAx_=F5=E4=
+[Oat_iYNNUNPXT[=EE=DF{dh=F8=DA=CF=DE=EC=D8=CE=CE=D2=DF=DC=CC=C9=D5=EB=DF=DA=
+=E1~l=FB=EAtUPRMJIKLJL^=EFoPU=E5=CC=CD=DE=DC=C8=C2=CB=D7=CF=C3=BF=CB=E2=DB=
+=D1=DAcUUOMLJGDA@EJILd=EF=FA=F6=E6=E1=DD=D2=CD=C9=C7=CA=C9=C4=C3=C8=CD=CF=
+=CB=C9=CD=DF=E8=EEbROQQOMIHJJKGELVZUR_=FE=F8}=F5=DC=CB=C5=C8=CB=CA=C7=C5=C7=
+=CB=CA=C5=C2=C8=D5=EBihq\OORPMGACJIHMUYXUXc}=DE=CE=C9=CC=D2=CF=C7=C7=CF=D8=
+=CF=C6=C6=CE=E7t=F2=E7=F8q=FE=F6o_VPPONLOQQQOOX`cr=EC=DC=D5=D4=D4=D1=CB=C8=
+=C9=CF=DB=DC=D5=D2=E0na=FD=D8=DC]O[=EC=F7XLT=EA=E8ZORZ[NJQdjfcj~=DC=DB=D8=
+=D5=D3=D0=CD=CD=D5=D6=D3=D5=D7=D8=DB=D9=D8=DD=E4=E2=EDmZWWVPLKKJIMTZSUp=D6=
+=D3=EB=FD=DA=CA=C8=D6=E6=D5=C8=CA=E1y=EE=DC=ED_\{=E2=F4[Uco_Yay=FBbX_=EF=ED=
+jf=EE=D9=D9=E8=F1=E2=D8=DB=E8=F3=EC=E0=E5=FDjedfa[XZ]a^Y\itv~=EB=DF=DC=DA=
+=DA=D6=D5=D9=DE=E6=EC=E9=E9=EFvh_\[[^cghjim|=FC=FE=F9=E8=DE=DD=DF=EB=E1=D8=
+=D6=D7=DC=D9=D6=DA=E5|ung]Y[[ZVSUXYVVWZZ[dv=EE=E6=E1=DE=DD=DC=DD=DE=DE=DC=
+=DB=DB=DB=D8=D6=D6=D5=D6=D7=D7=D8=DB=DC=E3=EF{lb[UPNMMMOSVY]aix=F9=ED=EC=E9=
+=E5=E2=DF=DA=D7=D7=D7=D7=D7=D9=DA=DC=DF=E5=EE=F7=FC}ojf^YSPORUVYY]hx=FB=F0=
+=E6=DE=DB=DA=D9=D7=D5=D5=D5=D6=D4=D5=D5=D7=D9=DC=E1=EA=F1ucZTQOOOPRUVV[_f=
+ko=FA=EB=DF=DB=D6=D4=D2=D2=D3=D4=D6=D7=DB=DF=E8=F7ukb\ZYYZ\_ejs|=FC=F5=F6=
+}yw{=FF=FA=F9=F7=FEz|=FF=F5=F6=F2=EE=F2=EA=E5=E0=DE=DE=DE=E1=E5=E8=E8=EB=EF=
+=FCzxql`_```fkmmjgc__^_cfmw=FD=EF=EA=E7=E2=DF=DF=DE=DD=E1=E2=E1=E3=E4=E4=E5=
+=E5=E7=EB=EF=F1=FBvmjjgfegghmq{=FC=F7=EC=EA=E9=ED=FFqje`eijmnpnluz{=FF|us=
+tw=FA=EF=E9=E4=DF=DF=E0=DF=DF=DE=DE=DC=DC=DB=DA=DE=E1=E9=ED=FCmklgda_][XU=
+TTRQSSUX[^fu=F4=E5=DC=D7=D4=D0=CD=CD=CD=CD=CC=CC=CD=CE=D1=D6=DB=E2=EC|kec=
+a\Y[[YYWTRSRRPPQPPRSV[du=EB=DC=D3=CC=C8=C5=C3=C2=C2=C1=C1=C3=C5=C7=CC=CF=D8=
+=E0=F5eWNJFDCBCCBBDFIMS]n=EF=DE=D6=CE=CA=C8=C6=C4=C3=C3=C2=C3=C4=C5=C8=CE=
+=D2=CF=DBhTNKLLHFHFA?ACIUf=F6=DF=D9=D8=D3=CD=C9=C6=C4=C3=C5=C8=CC=D1=D7=DB=
+=DC=DC=DF=EBu`XOLKIIKLNPQVZbv=EB=DB=D7=D3=D0=CF=CE=CC=CB=CB=CC=CE=D4=DC=F0=
+la\WTTSRRUW[]`ed`^\^h|=F1=E8=DD=D9=D4=D1=CF=CE=CF=CE=CF=D0=D3=D8=DE=E5=E9=
+=ED=F6uf]ZXRNMMNOQSV[_ait=F9=E8=DF=DB=D6=D1=D0=D0=CE=CC=CD=CD=CE=D0=D4=D7=
+=DA=DB=E2=EFvf^WQMKJIHIJJKKNSX_i}=E9=DF=D8=D3=CF=CC=C9=C6=C5=C4=C4=C4=C4=C4=
+=C6=C9=CC=D2=D9=E0=F5n^WRMJGEDCCB?FJCGENa_r=F4=D7=CF=D4=D0=CC=C7=C4=C3=C0=
+=BE=BC=BE=C0=C5=CB=D0=DB=EDql`]XTSPNLLLLMLLLLMMOTZ_ck=FA=E3=DA=D4=CF=CB=C8=
+=C5=C5=C6=C5=C6=C9=CC=CE=D1=D3=D7=DC=DE=E6=F2p_VPLGB@@AABDHJMS[g=F7=E0=D7=
+=CF=CC=C8=C4=C1=C0=BF=C0=C1=C4=C6=C8=CA=CD=CF=D4=DB=E4=FFbXOJFC??>>>?@AEH=
+NZn=E3=D5=CD=C7=C3=C0=BF=BE=BE=BE=BE=BE=BE=C0=C3=C7=CC=D5=E5nZOJEB@@??@BC=
+FIKMSZj=F7=E3=D7=CE=CA=C7=C5=C5=C4=C5=C5=C4=C5=C7=C9=CC=D0=D6=DD=EA{eZSMI=
+FFFGGHJLNORZcs=F1=E4=D9=D1=CF=CC=CB=CA=C9=C9=CA=CB=CB=CC=CE=D3=D9=DE=E8=FD=
+k`ZXTRPNOOOPRVXZ^`fmx=F6=EE=EE=EC=E6=DE=DC=DC=DC=DB=D8=D7=D8=D8=DA=DB=DE=E3=
+=EA=EF=F7=FF=FCzvpnlljfgedfijkkjgfffjlx=F4=F0=ED=E9=E6=E1=E0=DF=E0=E4=E6=EF=
+=F9yrngils}=7F=FE=F7=F1=EC=E9=E6=E4=E3=E3=E4=E6=EC=EF=F4=FDuj`_^[XZVQlZ_d=
+Z=FE=F3or=EF=E1=D7=DF=EA=DC=D7=D9=DB=E8=E1=DB=E1=EA=E9=E4=DE=E0=F3=E7=E4=E3=
+=E6=FCx=F9|qjhpob]\\^YUZ]]ZWX]^]^do=F7=ED=E8=DF=DB=DA=DB=DD=D9=D3=D1=CF=D0=
+=D2=D3=D6=D8=D9=DA=DC=E1=F5yohb^\[[ZYXXYYYZ[ZYY[^ejy=F0=E6=DD=D9=D8=D5=D0=
+=CF=CE=CE=CE=CF=D2=D5=D9=DC=E3=F8od\UONMLLLNOORW\es=F9=EA=DE=D7=D1=CF=CC=CA=
+=C9=C9=C9=CA=CB=CD=CF=D4=D8=DD=E1=EEzkc]VOKIGEDDEGIKNS[do=F5=E3=D8=D0=CC=C8=
+=C5=C2=C0=C0=C0=C1=C5=C8=CB=CE=D4=DD=E9{cYQLIGECCCCDEHKOV_p=EE=DC=D5=CE=CB=
+=C9=C6=C3=C2=C1=C1=C3=C4=C7=CA=CE=D3=D8=E3=F7j\UOKHGFEEEFHIKMPV]fy=EA=DE=D8=
+=CF=CD=CB=C9=C8=C8=C8=C9=CB=CD=CF=D2=D6=D9=DC=E3=ED=FCoc[UOLKJJJJKMOSY^h=FE=
+=E9=DC=D5=CF=CC=C9=C8=C6=C6=C5=C5=C7=CA=CC=CF=D8=E4yf\SOKHFECBBBBDGKR[j=EF=
+=DE=D7=CE=CB=C9=C6=C4=C2=C3=C3=C4=C6=C8=C9=CA=CD=D0=D7=DE=EC}f[WROLKIHGHI=
+KLMOSY]dks=F7=EA=DF=DD=D8=D2=CF=CE=CC=CA=C9=C9=C9=CA=CB=CD=CF=D3=D8=DD=E1=
+=E9=FBk]WQNLJGFFGHHJMQXav=EE=E0=DA=D4=CF=CE=CD=CC=CB=CB=C9=C7=C8=C9=CA=CC=
+=CF=D4=DC=E9q_YTQOOOONMLMMNSY_fr=7F=F4=E9=E1=DD=DD=DC=DA=D8=D7=D8=D9=DA=DB=
+=DD=DD=DD=E0=E7=ED=F0=FC{vtvv=7F=F3=EF=ED=EB=ED=EE=EF=EF=EF=F2=F8ymlhb^[X=
+USTTUUWZZ[[^`i=FC=EA=E1=D9=D3=CE=CB=C9=C7=C5=C5=C7=C9=CC=CF=D7=DE=E9|j_[W=
+QNLIHGHJLNSX^gn=FC=EA=E0=DB=D5=CF=CD=CC=CB=CC=CB=CC=CD=CE=D1=D6=DC=E0=E6=F0=
+|l_YTOONMMLKKKLLNPW]gv=EF=DF=D8=CF=CB=C8=C5=C4=C2=C2=C3=C4=C6=C8=CB=CE=D4=
+=DD=EA=FDka^XRNLJHFEEDEFGJMPV_p=F9=E4=D7=CF=CC=C9=C8=C7=C7=C8=C8=C8=C8=C9=
+=CA=CC=CE=D3=DA=E2=F0sf^WOKHFEDEGILRY`n=F8=E5=DB=D5=D2=D0=CF=CE=CD=CD=CD=CE=
+=D1=D4=D5=D7=D9=DD=E3=EC=FCulg^YUSONMNORVY\`jt=F7=EA=E5=DF=DC=DB=DA=DA=DA=
+=D9=D9=DA=D8=D8=D9=DA=DD=E1=E9=FCrjfa`aefcb__][[\^`dhlnpw=FF=F5=EA=E1=DD=D8=
+=D3=CE=CC=CD=CE=CE=CF=D1=D6=DD=E4=ED=FBvc]YUUQOOONMLLLMOQV]j=FE=E9=DE=D7=D3=
+=CF=CD=CC=CB=CB=CB=CA=CB=CC=CD=CF=D4=D9=DE=E4=EB=EB=EF=FDxk^WRMLKKKKLNOSW=
+[bn~=FB=F4=EC=E7=E3=DD=DC=DA=D7=D4=D3=D2=D0=D0=D1=D4=D6=D8=D9=D9=DB=DF=E4=
+=EB=ED=FCywtrmd`_\YUQOMKKLLLMNRY^l=EF=DE=D5=CF=CC=CA=C7=C4=C3=C2=C3=C5=C7=
+=C9=CD=D3=DC=EDn^TNKIIIJLNORVX\`el~=EF=E4=DE=DA=D8=D3=D0=CF=CE=CD=CE=CE=D1=
+=D7=DD=E9=FDi^XRONMNORW]`el}=F0=EA=E4=DE=DA=DA=DA=DA=D7=D6=D5=D6=D7=D6=D8=
+=DA=DC=DE=E2=EA=F3zoh^XTOMLLLNOQVZ_hs=F1=E5=E0=DF=DE=DC=D9=D6=D4=D2=D0=CE=
+=CD=CD=CD=CD=CE=CF=D1=D6=DC=E1=F0xf_\VSQOMKKLKLLMNQV\ep=FD=E9=DF=DC=DA=D8=
+=D5=D0=CD=CC=CB=C9=C8=C9=CA=CC=CE=D2=D9=E0=EC~k^ZUQOOOPRSV[_``fmt=FD=FB=F6=
+=EE=E9=E7=E6=E7=EB=ED=ED=EF=F1=F2=EF=EE=EE=ED=F9}{vvtwx=FF=FA=F1=E8=E5=E3=
+=E0=DF=E2=E3=E3=E4=E3=E7=E7=EC=F0=FAunkjjid`__]]^bgijhd__][]ajv=FE=EC=E1=DB=
+=D6=D4=D3=D0=CF=CF=CF=CF=D2=D8=DB=DF=E5=E3=EA=F9oc^ZTPNMKLNOUX]afmv=F9=EB=
+=E1=DE=DC=DA=DA=D9=D7=D5=D5=D6=D5=D4=D6=D8=D9=DB=DE=E0=E1=E3=E2=E3=EB=FBw=
+--hal_9000
+Content-Type: AUDIO/BASIC
+Content-Description: I'm sorry, Dave (BASE64)
+Content-Transfer-Encoding: BASE64
+
+6u75/fv38enj4+Lk5+fs8/3+eHdzb21oY2FgXV1eXFpZWVteY2v66eTd29fV1dfa3N3e3t/f
+3+Lk6/L8cG1oXlxaWVlZWFpcXmBkZ2hoa21tbnV5/vTr5ODd3NrZ3Nzd3+Pl4+Df4OTue2pm
+X1tZVlVXWl1fYmZtefjv6uTe2djV1dbZ3uXveW5vbWllYWBfX2NmY2JhXlxaW2FrdfDq5N7c
+3N3f3+Dl6u309350en1//P59fP79enp2cG1sb3V+9Ozl4d/e39/n7e/29/r+eHFtZl5YU1FQ
+UVNWV1ZYW2N19Orf3NjT0M/Ozs7P0dXX2Nve5fR0Zl1YUlBRUlNTVFZZXWJvfu7n4d/h3+Hl
+5+rs6urn5OXm5ejv9Pl7a2VlYFxeX19fYWZna3B6+/Ls5N7d3Nzc3uPm6e3+b25qbHJwbGhn
+bWxqbnBxc3r68+3o6Ozu7u3s5N7e3ufu9HhsYlxZV1dVVlhbXV9iaHP+7+vo4t7c3Nvd39/f
+3tzb3N3e397g6PZ2b2ljX15bWVVTUlNWV1tcXV5kcPHj29bT0dHR0dXa3+rv7+/1/3drZ2Nd
+WlhWVVZbYW5/9+/t7enm5eTk4+Xl6fP7/HptamppaGpscHBxevfu6OHf39/f5Or4dnRxc339
+endza2loZGRnaWpoaWxyenx8ff317ero6OPh39/l5+7w9PT0+fr59/5uaWFdXFlXVVNUWV5k
+bXp+/PHu6ODc2NbU0tDOzs7P0NPW197o+HZqYFtWU09MSklIR0hISkxOUVZbZH/k2tLMycjI
+yMnKzM3O0NPW2t/p+Xl1b2hhXFZST05NTExMTVBXXWdy9urh3dvZ2tvb29ra2trc297j6O30
+d3FqY2BfYmRiZGFfYmdlZF9fY2NjY2hy9urm497b29nZ2dfV1dfa3+fu/HJybmReW1lUU1RW
+V1teYml2+/Ty8Ovp5t/c29rY2dvd3+Pn7PD5/nFoZF9fXlxaV1VVU1JSVlpeZG/z5+Dc2dfU
+09HPzs7Q1NfY2tzf5ez5fXVtaWNcV1ZUU1JOTEtMTk9VWV1iaW/56d7Z1dHOzs7Oz9HU19jY
+2dna293m9nRkWVNPTk1NTExMTlBTWV5ka3zv5t7a19LPzMrJx8fJyszS2+h4Y1ZPTElHRkdI
+SUpLTE5PVFtfceze1c/Lx8TDw8PExsjM0dnj82lbVE9MSklISElMTlFWW2Fv9uff3NrY19bV
+19jZ2+Dp8vn9eG5qamZlaGxtbG1ycG9xd//z7+/v9PLw7Ofm4+Dg5Ons9XxvZ15aV1ZWVVNS
+U1RXW19u+ubb19PRzszKycjIyszP1dvoeGBYU1BOTUtKSUpMTU5QU1ldZn3u5d3a19TSz87O
+zs/P0tfc4uv3enBqZWFeWllaWlpbW1xdXVtaW11hZ3L66t/b1tLPzs3LzM3P1tzm9nNlXVdS
+TktKSUlJSk1TWFxhce3f2NLNy8jIyMrLzc/T2N3i6PJ1Z19bVlBOT09PUFBQT05NTU5RVl5r
+9ODXz8vIxcPCwsTHys/W4fZpXFVPTUpIR0dHSUtNUVpmeufe2tfW1dLS09TV1tbY297g3+Tq
+8f5zaWJcWVVUU1RUVVhZXGBt/+7m4+De3d/f5Ofr9vh8f/378e7u7u/6fn58dXVuam9vb3h/
+/3h5dXRubW1z+vPs5t/a19PS1NbZ3un9aV1YVFNST09PT1FUV11ganL+8ejc19bV1dbZ3N3g
+5Obp6+7r5+Hi6Ovv9nxxamlnX1xbWllYWVxdX2h58+zo5d/d3eLn5uXn7fb7fnZtaGZiXlxb
+W15iZXH86t/b1tLPz8/P0NPX3ur+a2BdWVdVUU9OTUxLSkpNT1VbZHrn3dfSz83NzMrJyMnI
+yMnLztLW2+f8a11XU05LSUZEQ0NERUdKTlNZXWBw6d/b19TQ0M/Ozs3Nzs7Pz8/Pz9DT19rc
+5PlsX1tXUk9MS0lISEhJTFBWWl1o+ejj39nRzc3OzczKy8/W3OX2c2ddV1NRUlRTVFtmZ2Jh
+buzi5O3l29fY3uPg29zl93B0bGRcWVtcXV1dY2z/9vPs5+Tuf/rq4eLp49zc4O318fLz+Hp9
++Pvx+HZvbXB0amJdW11hXVdSUlZbXVtcbvn8ffjn3tvZ1s/Oz8/Ozs3Nzc7S1tzl7nlqYVdP
+Tk5RT01LS01PUVNXWl1fZGp1797c29nWzszO2NzZ2d7uf3rw92xvevz/bmlrdn1ucPLu+XZx
+8ejt7/D2+Pjy+Ph1anR5aV9faW9kVlJcaGJdYnB7dGRkc/357d/c3t3Z0M3O0c7LydDc2tjf
+Z1VRUlVNR0hOT0tJSk9ZW1Zf4tLP41dQ28LH7GHZv8bex7a4YzlCzbW85Xfdakc7MjVCZett
+VkVCRUdKYODbxbeyxUY+X7y62k9Wy8xURVHRxdL76s7J4FVd39DVd2BtcH1ZRUdIZ89WRUJM
+2fBVSU9OQc25qrMyKy/Pqa23w7Syby8nLkbIvcHDz+1HOzpFbc7Cw8LM7FJBPj9LWmT/3NXj
+Y1hu2c7Oz8/Q2u335dnd9Xro33ZZWGJ5b11bZ2pWTU9UWFdh5XVr39zO4V3qy75ZP8W7t788
+Mzdrx9q6ub/BTzAtNkFY2MO+wt9FP0dVaNTDvLrE3FNCP0FNcexk28zdZVDszsfWX9XI2V5d
+59riXFv64G1aZnP7Yl9kcF5LTE9jXEtg3df0W9rO2HBMy7y9v0o8O07Mz7u1t7t1Oi81PEru
+wrzBz1VFQERPfMnAvsTXXUQ/PT9HW9jN6HXOy+hbdMq/xuzvyMl3T2nW2WNQY+prTEti8V5J
+S2B1VklLZuR0ZfLe09TX1uznyL61v0g4P97L1ce7s71QNjE4Oj5Zzb/E4lRJSUxNZ87AvsbW
+bU5GQEJLWn7h0snN6HDkyMzh6su+xXtY+M/vTk/+4F5KS1xbSkRMWFlPTVv+cWl35drOytLX
+y8jK2lfyxLe6WTo8b9dtVdS4t9s+NTs8Oz9dzMTPZU9SXVph9c3Av8bR61xPS01TX3XczMbL
+3X/u2uBsYfDZ2XNVVWFgVlNaZ3VtX15jZl9eYW/47+7y7Obf19HP0dTU1tni8Wdabd/P21dF
+Q09fZmd32M7T91RLSU9bbff07eri397c2NTR1NzndWVeXV5dWFNRVFpfbfHe19TT0dHT2d7o
+7ntmX2Fu+/h4b3J1b2lhXV1dW1paXF9qfezf2tnY2NfY19jZ2d57a+3a42JOSk5cXVZZbOPZ
+2e5jXFxhbXVqZ2htc/Dj2tDNy8zR3OXs+2tdVU1IRkdLT1llfeHUzc3Oz87Oz9be5u/7dXz8
+fP54amJeWlZVUk9QUFFTV1xfa3vs5NzV0s/P1dzRx8fU+WBcaXhrftrPz9fuXVFRUFBRTktL
+T1JXYn3j1tDS1drg5e17ZV5bU1NVWWT039rQysjJyMjLzdfvcGphXFtcWVdXU1JTUlFRU1NU
+VlhbZnb36NzWzsvLzMvKys/e5tfP1HxQSkpKS0xUe9zefl1ST05RW2Rsbmxv/ubXzsnEwsPG
+y9Ha5XdfUUpFRERERUhPXG/n2tHLyszN0trf5+vp6ezy9vt/eHJvaWVdWFVTVVZXW11gZ2/4
+6eXe2dfV1dXY4uTZ2+Pg9W/9ZVljcHnp5O31cF9YU1JSVlpYVFddZHjs39jV1tXS0tXa4Ol3
+X1lZXGVx9+jd2dnX1NXY2eT7bF5ZVVVVV1laWlldX2JqbXBwbmt1/vfs5+Lc2drZ1dDS1cza
+YWnz7GdcZmpwaFZOU1tcW11x+Xr88/rn2+Hn4d7ocG/x6ejy/vfs9mdeZ+buYGzz9e5lW/rv
+fmlh+e1t/W7w43vt8OHr8Xt86HJs/XDqc/16/d1e33F63Frcd3TdUvRVXWNU92Xh9npt7uz0
+3frS3+DaX9b66+Pu5Pz4anl4Xn1efPteXWBbZF1aa2BhXWNra/Zh73p563bd593V6tXp3NPi
+ytvQ2t/f4fbw6nnvbl5cTGJTa21T8ExfVFVlUGhcbO5ffHdc32/j4uzQ39fY2tfW09XV1OvU
+XuBqX+pTeVRnXFtdS1pNW1pV/1fqX+Hq+s/6zN/S0+zG987b2c170GHVXvVdVGlLdklhV0lm
+R15OUW9O2V7o/ubY5Nnf09fQ2NDU09jc3Nzp7fXq5mhrX15aVlNYT1xSVk9XT2ZWZP/13+/Z
+5c3c09fP0tPZz+bZ8+voX3NOa0tiT19bZFtf/F53eHfn3+vee9Vj23Hg3P/eeN/jZfxYfV34
+c2znX+Ve71ps/mXdV91v5N1fzv/R4NbV/NZtbO5P9UzxU1lkSv9Ick7/WG5tefLt3evS3eLR
+3dLX49fl0fTb6evoZ+dUcFxjcmZkamb7W2dVZ2Va7Froaep56u/7/O/2def27m3ra+T94Gzc
+YOlhZ2tx6Xzeetrk3uvg5t1152byc15baF5oY1fqafh0a+T8a9pY11ft/ufcXddn2/Ry4Gfo
+dmHuYvxX32Lweeft//Hs7/L07W3ob2tb9Xta5UzgXXVeXOD+22nf3tbc393p+dpo0Gvpbt/x
+Yvda2lbxXFzuU15TXl9RX1FscV3ie9T24dzr2dv23O/a4+fn49ng+tzueeBa7uZ89lzkb216
+T1ZvTWBOW/FZ2lTk9nTpWthl013q5f/cb/DZ0+7h127Tbfj4a2ph7mjbeOrSZtFp63JndHFW
+UV5Rb05fVWVhW11g1GjXd/Pf1X7c3uXV2PnQ78541dra8G9jeG3n71btYXFvSmZNXvxSbVbv
+dVt/VdJY6mpn5ONdcO3edeLn2ODO3NHgz93Z1Obm3mzaaPjtUvhLXlBaWUlUTlL+SXlhb/xo
+3VzdXNvW897X1tjd387P19PqwuzU6+/bVF1pXORKWEFRTEZUTGpc427vaG3r5t3q3tXU0NzO
+y9HN2OLa3u7famlkWV1NTkpPTklYUVlPT1r5cONqz9XK19LO18zg0tDJ2c/bztPqVXZp7llL
+U2BwTU1JX1tQT0tTV1ldWF5/8P5+/dzQ287NzcnPz9DG0s7J4MzwcG1qc1NZSk1WTkxCTFBM
+VEdcXvBfb+nf1OnY29bR1M3Q19DOzNbU9tjd4utiZWr6XlhSTlhVTkpITFJcU111fHppdN7Y
+/u7Ux8vm9NXCxtXg2MvVaGXu2uZYTlh+Z1NOTl5TSEpNTldfUFrZ2eB4XvvTytnq1ci/x97i
+0cfJ1e3n2uFoVlFXW1VKRklISUZERUtTTlBe7994X+TLwsTMxLy7vc3gvbnI1e1kbUg4QWLj
+4GlPUkk6MzAzO0ZPaebPx8THxcC/vsPCvLzRTER3xspmTHjK0VJATN/O5lxYXlhJQkJGTl5d
+Wl5l6uZtUVX72cvP0crV6cO+v7vOTD5GZcu8vsTGzlo8LywvN0FY7dbPys7b6nbo283LzcvG
+zG9QSFJhZ2Zq3dPX4ufd0tPgb1xTTk5MT1366ur1dWVo7t3f72laXuLJxtHlbV/UwdNi/V1C
+Pz5Bb767xs1vPzc3NjpL7se7uLzFztrh3tXPy8fGw89ZQDk7QkxMTmT66ej5/uzX1Njfe2dl
+f+fX0NXa397xX1JTbePfX0xKUXHb0X1IPVLIx8fRSz48TO7Fsra+v95GNzQ2PmTLv7y+y+Ht
+a198287IxcrS5U89OTtBS1hfbd3d4uXs3d3Y2+rt9u715djU0t7/Y15dVlRZZ+ndc1NMTlv9
+2tzvX0x1xsXDzVQ+OUR7wrCzvcpqQDc4OUBxx7u7wNNZT1BPYOTPx8bL2vZKOjg7SVlv6uzT
+0NjX2dbTz9Dg8HV0fe/l9XxqYWBaVk9PV1pge/VuX1VRXefRztLXZ27Ev8K/5EQ6PV7Ksq+8
+zV8/NDQ3OUrfxcHJ2VhLUFZr18vIxsbJ2VZCPkNVamzx4Njb3NvX0dPY4+p1XV5g/d3c7mNZ
+VlVQT1FTXW/v8GBdXmDo3NfX19Rz28DCv8tOOzdF7MGytsPhTTw1ODtCbsm9wMrfT05cbOTW
+zszKx9NZQT0+RFFVXOzTz9rY2d3X29/x6Ohtfuzg4vR5Y2VgVE9SX3H/eGxmaGtpc+re5eTc
+auTDwr/NSTo4TdS/s7nG3U8+NTY8SOLEvsncck1OWGHs3M7KycnvSD9BTVteXWrb1Nre29HR
+1dvu8e/r5+Dc4/V6b3ZtYVZOVGNxaFpXXWZubGVncHBmVuTCwb7MTDw7VMu8s7rE0ltEOTc9
+SffIvsTW/V9dZGr14dnNzdH+Rjs8R1ZUT1j13OLo39HKy8/a3+Pt6+Ha1uD3++/oel1YW2ho
+XlpSVV5tdmJZWFlY+svHxtZMPj5Yyby3usHLckk9OkBP88vEyNT6ZFtaXWns39TP1XNJQURI
+Tk9RZ+TW19rPycfJztXhemVhb/D2eHn4cltPTlJZYGRgX2Vvfevk6uh7Xd/Hvr7wPzxI2sPC
+wsLCzF9ANzY9TurQzdHc6PN3al9u5dfNy9dZRUZSXlRMVeXP2mpr2cjHzM7O1uhsYF1dbOnc
+3+9pWFBVWlZPTVNgbGRkXk1V2MO92z87TM6/wcW/urzNVT06QFL23tvZ1dHX62NYXW7v5+h4
+U0VDS1JST1nt09Lc39jNyszP0tTZ6Xlucvv8c2ldVVJccmNOSVN2eVpUVWTdzMPMTjxF3sHD
+zci8usdkQTw+SV/8+eTXz9Ppa15g/N3c6fb3Z09GR01YX2nw3NnY087LzM/T2uZ2X1pcZHHy
+8P9rXFxpXU1JTFx4eF5Z6MvCyVY8P3nHxdDOvLa95Uc+QklOUVRq39PU5Pb56+Xr7uje+1JJ
+SEpNT1z64Nna2tPLxcfNz8/W72ZgaGxfW11dWVZbXFFMUl9jV09OadLJzW9LT+PLzNbNvLa+
+3VFLT01IRkxl4+L6bf3h5HJr6Nv5W1lcVE5PWWdsZ23q2NTVz8fCxcvU3OHwe2dndWpgbu5e
+S0xYXFNSXFZd3Nj9UURFU3Xz99e/usHSf2dhT0hIVebX3tzPzdHX0MrO+2P0/kw/Q1FYS0JM
+7NxuX9vHydTUy8nR6fXr7XZrdOvf7mhgY2VcVFhXSl3W5VtQUllWVFrqxsHLy8nSdVNQT01R
+WGJ55eLezsbI19LGy/ZYVVdPRj9DUVNPWPze3+DW09jb2tzk5N/n4dbOzuHu2tnubujoVGvZ
+Y1VRTU1GQUhccFpb5NPxWFt1YU1PdPJx69TGwcXEv73AycrS5GVOSUZDQkZKTVhs7Ov48eDc
+5fv37H9o/Of0Zmbt3eb73td328zsfnJhXU9KTmfv/eza1e1ua2RcVlZbWVllb+XY1M7JxcjK
+y9DT62RYTUhDQkRLT1hp69zVz83P0tXg/nxkXFxVVF5u7dzy5sXPberd8mhbXebc6tzNzdTe
+73plUExMSERGSEtXVl/v7NzT1NbW0t30bGBfXV1me+/h2dLMx8TDwsTHzd/raVFQSUNGSEtK
+SXNtT19pWlRUVmJ+7+XZ0c7QzMzMzs/Q1Oj+el9iamBv+vL9cG1gY1pPTk1IR0dHSk9Yb+LV
+y8XDwL+/wcTJ0dngbFZZVk9NWHdUWX1mVVVYXVleanf75OZ/7uf8d+zq6evh6/Xg7O/h5t7h
+5OPp8mpaVE5LSUhHSkxTXGzp19DNycrKycrNztLZ3vl97WFq1G1f3fpaY2BZXWlkb3d2a19c
+XVlWYmRdZXRlYf5zbOzs397h3d3i3+h6bGpbWVlaXWVu7t7a1M/Pz8/R19rj63xfW1tVVE9m
++Vhq4PL84vLw7PZ78u7q+Xz28m9tdmVcXmRdXW1vZn/r5uzm2+bt63RjZl9bXVxcYGhv8+jd
+1NTR0c/S2tnucG5nX1tdWVJe6Wpi4+f35PH96OLr4d3b3OTq7/lxdGtjXl9bW1xZX2FfZW1q
+ZWltc29ubGFdXVpbX2d77ure0s7MysnIxsrNztXg6vxvXVZUU1dTUllZVlpdXVxdX2x5d/Po
+5OTg39/d3uDh7Pp8b2ZeW1lbXF5ma2tyfvj09e3l4ebt9H799vLt5+Le3N3f3t/j4+ju9Xlz
+c3Vwa2hnZmNhYWFfY2NkaGtydXVzcm91eHz27enl4+He3t/i6O/0+f16dG1pZ2RgXl5eXl1d
+XV5ga37v5d7Z1dPT1dbY29/l6+7v8e7z/X17fXh5en16c3VvaF5ZWFhYWFhaXmFpbW9wdHp3
+c3Fudv727+zp5OHf3NrX1dXW19nd4uvv/Hz+fnx8dW1rZ2NfXFpYVlZYW11fY2lvevTp39nW
+1dXT1dnc3+ft93FqZF5dXFpYWFdXV1lbXWNu+e3o4N7e3Nzd3d3e3+Hi4ubq7O/v7fB8cmxn
+YF5dXF1eX2JkaG94+fLu6OHg3tzc3t/e3+Lo8P5waGNeXVxbWlhXWVtfZWx7+e/s5uTk5OTn
+6Ofo5+rr6Ort7/f/enhxbWtpam1vb25sa2xtb3r27ebh397e3tvZ2dvd4OTq8npmXllXVVZU
+UlJSU1RWV1pfaGt48+vg29fS0M/P0dPV2Nvf5ejs8v51dnZ1bmtkXlxaWFdXV1dZXF9iZ27+
+6+Dc2NXU0tLT1Nfb3+jufm9lXVtZWFlbXWBgX15eXl1eYWhwfPrw6Obj3tzZ1tTT1NbX2dvf
+4u57cWllYFxZVlZVVFRVVlpdYWp2+Onf2tfT0dLV19vf5e5fXstkRExR09ppYF/v62dtZmla
+5NPfaPHkfNfm62FUU8q/72ZIYOhgaPVr9Wne7fRUU/llamnd+X5kd9b8VGLq39ro3NDfatrg
+3m7y2tHfe+rqYFdTWU5OTFNdTlBZYG5n9t3X397T1tfV2tXW3Nzb2dzX3N3l6/VzYl1mVVVO
+Tk1LSUtOUVVdYnnk5N3b1c/O09LPzdLV1s/Z3u/l6HNfZmdXUVFUUU5OU1pWVmBycGrv2tXY
+2tLP1t3T2et69d/rX1t57m5kbu94W1peXFhbXFBUaPDn7d3Kxs7W0M7XcWV0Yk5LT1JNTFl8
+9+7azsnM1tjT4V9TWVxXVVVKXM5f5cdzYWBc6+pd58fM2OXt2PhOVf1pWFvv2uvv2MnM19LO
+2W5aUEg+ODc5PkhFTOLPw7++t7O3vcPGzmhKT1NKRUlXY11l7N3tZl5ecH9NP05OSU9PaMnR
+W8y0v8TH3dZ3T1zj3vLcycxgUWFURD5FTk9JVPXd4N/LxcnNztJ3TkdGQzw9TGZk89fDt7q/
+u7m+xuBZUlJSSEBNX29qXGXe3mdcZmlZSkhMZVtLXe1ucnvdy8bXUOO1xEtjz+9OP0rSw9vi
+vLjZUV5gSj9AWehYXc/GztXPxsjhYl9eRjw8PDw9SlxiWdW7wtbGubvI4drH0VZZ1tBeW+Td
+blNWcmNISVhTSEZLU1VRU2/xWV7Z1dvUysh+zrbAXGDDxGE/UcO73mu/uXg/QVJENjdR/U9S
+2crdau7T3ltVcW1NRk9XVFBh0srl4cK9x87Kw8HQf9rM+1FacV9PSlRaS0JNWk5JTVxgVVRs
+7Xfsz8/Pzs3Mzs3Jx8/Vz1r4xN9KVdhqRD1E69JRU8zGUj5BUEY7P+3P4dS/vMfQysTO7OrU
+32FUXF9PT15mT1Lv2exz6c7VX1vt+lxPU2doX2jf5Onc1N7l59vjc2Hu625p+eLhe/Tf+2B7
+5G5ab+pvPUTAxkQ/28ZgOj/RveZ0vLXMVFjoXj5C3cprc8nD31Zq3HdOUfb7T0tVXUxGUPlq
+UHzMy9LPxb7F29rN1ntrc3hlX2BkX1xeW09NVFNMTVhcXF5q9uXe2dbU0Nbb1dLSy8vQ0OJR
+zr39Q1PaeD42Rt7yR1rHyk9DUWpJPlPT3HvPv7/Q1cfE237a1WRPVVtPREz/akhL39ZwXOjO
+z/fz1dD9aeHa7eLX0t91dvRlUE5XWlRZZ2JaX3z4aWfq3fN59eDg5PDYy9/21l3svs9XX+ff
+VT5E9NFu+cfG9FBOVUs9QnHtYPPPy9Z9383Yd+/c8VxZXlhMTmXvW1Pdz9bVzMXBy9fOzn55
+72JYamtlYFdYaldMVV5QTlJUVVFUYW5w7+He19Tb0s3R0tLbz9Db41z0vtFQW+/vVUJFZ+Ru
+/c3QflhPSkU+RVxjZd7T1tzn39rl6Nzf5+XsfXxqZfPs7XLj1dTa0szL0dzt9exoU1VfWFdb
+WVZWVFdVUFJdX1xfYmH953712tTd4tvU1Nze183T39fQ3OplXM7KVU1wdlhJQk38aF/iztz9
+ZFlQSkVOYVxw1tHT09TQ0NjX1Nff4/JoWVNSWlxYVGvg4O3j1s3P5vbe5WZtZmD17m328WNe
+YlpaXFlfdmpaW19oZl1t2tbe39XP0OPez9L48Nzc7W5xYWDc6VZPV/ZeQ0FWaXPy08rK13xn
+W0tKXG1q49DOztTT09Tf4uj9bWNYT0tLU1ZTUWrk3uPa1dHV2drU2OHh4ff849/c6/by6mpW
+VFVUVE5NU1JOTlFTWF519V7+ytF+2svMytDPw8XS193Nx+NZWW5iTENJXG5qb9zbblhOSkU/
+QktUV3vc0s/TzcjJztLR0N39al9fY19aXu3g59rQzsvT3dbcdWtqV1FYVlFUVFRYWVZbWFNX
+WFpjaWf44ubb0tbVzsvMz9LN1unf5XT+8GhxblLyzVtHUV9UTklM7+1138zS5eLrb19OUGJr
+Y3vb1tPQzc7Pz9Tk+WZXT0tKT1RPT27e6+vTz9vi3+DxbGplZ2docnj/ffjw7v585+t+8ul+
+bXt/cf7m6m5+5uP7YnTxe2xnZm1kU27mTWjD6k1h7nv6W0/cx9ncxsTZ2eNjW09MUFhVXXjs
+39/f2tXY5fN1XllPSUpPT0tX6eTe1s/Kxc7TyMvb5ODqfHNfY2piYmRZUFdWUVlbVl1lXWB3
+dn/m5OXY1+Dd2+ry3NfgfenZ301Vy9dOTWJgaFRNe87l787G2t3b9GRaWVxlWV376Xzp3t3d
+3eDwdmRbUUxJSkdHTlpebNvRz87KxsfJys/T09Xf6eDwevX9YF9iVU5OTktJSU1PUFRcX2js
+3OHd1tTQzs7RycbS2+fhyNVYWPNeT01KTVtaXPnb7u7qb2BdWlpdXmF38evf29na1NHW3N/v
+Y1ZWWE5KU3dxa9rPzsvJzczN2uPl+mZnaGZnYV5qYltZWVRSU05KTFNOT1tkZO/e29zYzMrP
+zcnIzczL3dzk09b9W15oWE9MS1tkXFx3+mVuX1hXWFNXXF9s8+zn2dPNzMvNztHd73FnWlFV
+WVlWY/Lv597X1tff6+1vaWleWVhgXV1dYm51b3zv9nF4+Ht2+u3p397c3Nrb1tTd7dvadGv4
+X05r5llOWlpUT01PYmlt59bU19ba3+x78fptbe/2eOvl6uDd29vh8P1tWk9KS0tOUFh139fV
+y8XIy87P1Nrh9/JxYVtgYV1dXl5aVFFOTUxLTVFTWGn56NzX1dPKyczPysfO3/TYxttWYN12
+T0xSYVxXX3p0ZGdiWVVUVVpaW2f9+Ong3drQzc7Qz9no729aT1pYVFhfdOPW2tHIzNvc1vle
+X1tWTlBUWVpf+unh5+Ll7f5ucHNnYGVwe+/v9n7w5vT9+H1v+mhc495kb+Hf//nt3tja2s/O
+3+3ualhRT09QT09aXWBo/unk3trW2t7p+XFlWVdYW19kd+Xb1dDMyczT1tjrb2ddV1ZTVlxe
+Z3v9e/R5aWZnXV5iXmnv5u3j2djb1NTd6OjnZlRr62NTWnNcUVRp+PR+5tHU5Obb5mRicm1k
+afrs6+/j2NXW1dbif2VeUUhFRUhJTVVr4dfPyMXExMbIzdTc53VkXltYXF1cW1lXWFhOTE5O
+SkpPVVpect/Tz8zEwcLDx8fByeR+3OtWTVNeXFRSX2xYTVFWTklMVFZSXfPd2tLIwsDDxMXL
+4HBmVEhAQ0hJSE5s3NPOycXFzNPX3ftnYVxWUlZfZWNr9+js+HZ3ZVhUVVtYVV5+8u/l3NXQ
+1ObZyM98ZPR9W1Zi6Nje6NPJ13N87mhSUFhZUE1RXF1ca+PZ3t/e3/pnW1RPSUdLVmf618rE
+v769vb7Ey8/bcllQTEpGREdKS0tPVFhYVVRYXV9cXf/e3NjOxsLCw8O/vcLS6eT6Wk1MUVpW
+T1BZVU1LTE5MSkpPVFhefNvRzcnGw8TJzc7R5mldWlNOTE9ge+7j187Oz9DP0+JxXlZRTUxM
+UFdZXmRy+n12a2d29Xxv/OPb4eHVzc3R19fP1OTk8GhbVlRUVVFVYmphYWlqZl1XW2VnZWzx
+39jX1M3KyszO0NPdd15aVkxISU9XVlZi6t7i5dvT1+b16eZ7YGb+83x57N/h7PT1cGBmaFxa
+XGhxXmTv3+Ty5dHP3fHj095dVGT5bVlXb93kbWL34Ol0aX7q7m1ofOzo8Prr4t/m7HplX1tU
+T0xMUFRTVF5z7OXo2c/OzszJycrNzs7R2uHj525bWVtZTk1QUk5LTE1SUFBcXWNufN3U2dPN
+zMzQz83Q3u3e2d92aPrmellXX2JXTU1QVE9NUl1reXl96t3Y2NbQzs7S19XX2t7o7/Z0bGVd
+Y2JeXVpWVVNSVllVUltrcmpv59rW2drRzc7S0czN1dnb2udub/lvaWhiV09LS1BPS0tRXWBb
+Xm7t6+3e2tPP1drW1NTU2NnZ3Obu7+x1W1xfX1NMTlRTUVVbXml67ubn39rZ3unh2drf5enn
+6OPc3N/f3N3scnD/bltTVllUU1ZbXV5janT8/vXs8Pbw7evq8fz+6d/i5ubf3t/j7fHx6+97
+bmt++XdrbG5uaWJmbm1kX2dqal9aYGJfYWv18fbn3dXP0NDQ1dra3uTr5eDp9f/6e2tiXl5a
+UU9OUU9NT1FXW1tjfe/o49vZ2NbTzs7U19bS2N/o7Of+Zl9gZGFbWllbXV5fYnHt7/bt4dzi
+6/fw8X1uY2VobXtqaPrp535ka+/7Z19lbXBwbv7159vb29vZ1tfc6u3m6vNxaHJxZmBfZ2ld
+WlxfX1xYXW14aWZ17+vr6+Xh39nW19rh5drb6fD59/xqXVpdYGFdW15pbWtranz3e3N08eXl
+6evs5eb17+7w8nZ2fP34/nR39ufl8Pjr4+v+b3J0c2BaXF5cX2ptb29+7eHh5uHa2d3g4efh
+3t3d6HdnaV9XVVFQUlJSVVpfa+7h7/De29jZ5Ond2d/s8e7n6/P27ebj3dzm6+/r4vZuYmr0
+Z1lcW19fWVZUV1lZWltbZPv1+vzu3dnW1dLOzs/W3d3oem5ua2deXGZqY11gbf13ZGJy8ft7
+/O7t9fr45O3/7//48Ovr7+nu7fV2bWloZWBhaGttZV5qfvV9fOzf3+rt4tjc5d7f6fR2a2Ri
+X1tZWVtaWldWWF9jbfjv5NfT09HOysjKzM/Nztjb7HBtXlVMSEhHR0VFSU1PUlhZYfzn39va
+0czNzs3Lys3P2Nzd5vhpZGNgX1lUVV1dV1teZnRqZmdnffZ2cnjr3t/i39jS1NrW0NPb3+Tn
+8W9oYl5aV1ZTT09UWltVVl1fX1xWW3Dv7ejj1s/P0dTQz9LY3N3f3tze7fz/9vlmXWNwZltZ
+V1hYV1ZYXF1fXmFpe/l6evPq5ubn3dXU1dfV1dXU1tXY3uXi4fpeWltVTkpGRUhKS05OT1lm
+ePjs3dDMy83MyMXHy83NztTd6ebvZ15YWVZMSElKS0tLTlNYW11qd/He1tHV087LzdXX0c/S
+2trY3OJ7bHhgWFpYV1FPTk1MTU5SWFdaaP76/uXc2dPV1M/Rz9HY2dja29vd5ebn+PTu+3dq
+cG5hXFVTVFFQUVNWV1VXWlpcZGxw8+/x4NbU19jUzs3O09LPztPX3Oz8cmpmXFRXWFlaUVRd
+X2NcWmJva2x87eTv+3f77/V9bnzt5ujy6d7Z2+Dg3Nzf7f58cWxoZmFcXWFmZWFldfj3+X/y
+6/l8ffz09vHq6Ofn4t3a3ubq6u9uXlthYVtWUVZeX1lWXWdmX19u8enp39nT0NDQzs/R0NLT
+19re3+h1Y2BoZllQUVVWUE5PVlhVVlthbnJufvby7vD15t3e4N7c2dfZ2dzf4t7e4+vv5+Tz
+d2xqbm5iXWFlaGFcX2NnZWNkanZ6e3n+8u3r8fTt6u/+evnt6+3s5N7i6/Tr6u7w/Px7dHBr
+Y11bWltcX2VufPv5+vPr5OXn6+3q6uzt7Ovr7Ozp7fHv8Pt3b2ljYV9haGprbnB5+/f69e3o
+5OXj4d/i6/b/eXl4dG5rbGlpaWpqaWhlYV9fXl9ka3L87eXf39/g4N/i5uz19/l4eHr47erk
+4t7a297h7Ph/d2xgW1lZWFdXWVxeYmNiZ3F++vHt6ebp8/v07+zp6OTe2tnZ2trb3ObxfG5l
+XFdWWFpbW15nbGpvbW99+PDv7Obn5+nn6evv+nl+9vLv8PHw7ezw9vXz8vD6f3d2dnJ8fHt1
+b2lkY2VnZ2dnaGlmaGt1f+7i3dvc3t/k6O/+b2lnZWBcW1paWlxdZG987ePc1tHOzcrJyMrM
+z9bZ5fppXVdRTUtKSklHR0dIS09TWF5oeOne2dTRzs3Mzc3Nzc7Q1NbY3OPl7PD8b2NeWlZW
+VVZYWFlZWlxeYWVsdvv7fXZ0eXt7+PHy6uPg4N/f3d3f3djW2+bu8/NyX1hXWFhWV11offby
+7Obf3d7f4ufs9v53cHJubW1pZWFeXl5fY2lxefTq5+Pi397f4uLm5OXp7e/w9PT9dW5oYV9d
+XFxcW1xeZG179/Tq49/f4uTj4N/e3+Hg3+Lo7O/1fnhuZmJgYGFkaG5vbG1tbWplYl5eX19d
+Xl9iZWhudfLs6uTf2tfV09PQz8/S1NXU1dfc4ez6c2RbV1VRT01MTEtMTk9UWFlaW15jbfzr
+5d/a1tXS0NDPzs7Ozc7Pz9DT1trj8W9gWFNRT01NTEtLTE1PVFdaXmdw/e7o4Nza1tbW1dTU
+19jX19fY2drb3ePq8/15bWVdWVdTTktKSk1PT1FUWlxfY2r96dzX0s/OzMrIyMjIycrLzM7U
+2+j+a19ZUk1KR0VEQkJDREVGR0pNUlxq7drRzMrIx8bFxcXFxcXGyMrMztDX3ul8X1VPS0dE
+QkFCQ0NDRUhKTVFXYHTo29TOy8nIyMfHx8jLzdHY3eXv/nNnYl5bXF5gYV5dW1pZWVpbXV1c
+XF1dYGBnbXB69enj3tzb2tjW1NPU09PW2+Hr+25iXFtaWltdX2VqbG5ubm1vcXR+8u3o4d3c
+3N/n8HxvamRhXlxbW1tbX2RobHB1evr07ejm5ujk4t/e3tva2NfX2tna3N7f5e39b2JaU09M
+S0pJSkxOUVRYXWd57d/Z1NDOzc3My8vNzc3P0tjd6v1vZF1XUE1LS0pKSktNTk9UWV9odPzs
+4tvW0s7MzMzLysrKy8zN0djf8m1dVlBNTEtLTExMTU5PVFtkcPvu6OPf3t7b29vZ2dbV1dfZ
+2tze39/g5+19bWJcWFNQUFFSUFBTVFVWWl5ndvLi29bRz87Nzc7P0NLT1dnc4uv1e29oY15a
+V1ZXVlhYWFlaWllYV1dYWVtdX2Rt/uzf2tfU0dDPzs/Q0dLU19rc3+Hk7fl5bWlmYl5aVlNT
+UlJTVlhYV1hYWV5lc/Xr49/b19PPzs3MzM7R1dnf7nJkXFpXVVNSUlNVWFtdXl9gZGVt/vXz
+7urm393a2tnX19XU1dbY297k73hpYFxZVlJRUVJTVlteYGh67ePd2dbV1tfX2t3e4ur4dmxq
+ZmZfXFlWVFRWWFhYW11fY2316N/a19TRz87Nzc7P0NLV2d3g5/duYVtWUU9OTk1NTU5PUlZZ
+Y3D+9Ozl393b2dfW1dbZ3ePr8v54cW5qaGhoa25z/fLy7+vs7u/1+/1+eXr88+7p6Onu+n9y
+aWZkYF9dXVxeYWNmbXb97+vp6eTf3t7f3d3c3N3i5+38c25pZl9eXlxcW1taWVlbXV9kZ216
+/vHp5t/c2NTT0tHQzs/P0NLU2N7i6fdwYltUT0xMS0pJSUpMTU9SVVhcaG539uvh2tfSz87M
+ysnJysrLzM7R1dre5u18b2ljW1RPTUxKSkhISEhKS05TWV1jbP3m3dfSzsvJx8bFxcbIy87S
+2t/o9XFlXFVQTkxJSEZHSUtNUFVbYmp3+O3m39zY0c/Ozc7Ozs/Pz9DT1tjc5O54aF5YUU5N
+S0tMTE1PT1NYXF9lcvvr4t3a1tDOzczNztDT19zg5urzc2plYF1dXVxaWltbXV1dXl9eXl5f
+X2Robnf06uLd29rX1dTU1dXX2dre4+Xm6u/08/h+e3ZpYV5bWFVSUU9OTk1OUVdcZHPy6ePe
+3NnX1tXU1dbX2Nvd3uHi4N/g4uPl6+3u93JnXlpXVVRVV1hZXF5iaW90fPt9d3Vzbmdpa2xv
+e/Dp5N/c2djX1tbY2dre4+ry+ndvamhlY2JjYV5dXWBjYmJmaWlrbGtvf/Dr49/e3dzb2drb
+3eXvf3Ztamlqa2pnY19cW1pYWFlcYGl2+e3o5uPg397e3Nvb29zc2tnZ2NfY3N/l8HpsX1pY
+VFFQT05LTExMTk9TV1xjbfro3tnX09DOzc3NzMvKysnLzM7T2d/tbmFZUU1LSEdFRERFRkdJ
+TE9UW2Br9uTZ0szJx8XEw8LCw8THy87W3/BuYFpVT05MSkpKSUlKS0tMTU9RVVtga/Xg2NHN
+y8nIx8fJy83S2N/s8/txa2diXVpXVlVTVFVWWl1gaG159+rk3t7e4eTm5OHg3+Ll6u32dm5p
+ZWVjZGdpa2xpZWFgX2BkZml0eXx9e/306+fj3tvZ1tXV1dbY2dze4On0/nNkXVtWUU9OTU1O
+T1JVWmBqee/n4t7c2tnY2NfU09HP0M/Q09ba3+t+bV9aVk9NS0pKSkxOUFVcbfbn3dnX19ja
+3ODk5u3v8fp8c3d7+u/q5ODf3t/i4eHk5ufq7vl6cGpoY19dXFpaW1tcXF1cXFxdYGZrbnP6
+7+vp5ePe29fV1NPQz8/R1Njd4OPq7vN3aV5XUU5MS0pKSUlLTE9UW2Z95dzWz83LycnJycrM
+zc7R1dvi7XhqaGNeWFRRTk1MTEtNTU1OUFNYXGJv79/Z1M/MysjIyMnLzM/T19zo/2leV09M
+SkhISktOU1phbX/u5+He3NnY2dve4uPh39/g4+jp6ezv8vx6d3R2d3FwbGZiX15bWVlaXFxb
+Xl9jbHvw6+fh397d29ra19TT09TU2N3j7P9vZ1xWUU1LS0tMTlBVXW7t3tfT0M7Nzs7Q0tXY
+2+TxdmZeWVZUVVVVVldZW1xdYGZtfPnv6+fi4d/f3dra2NbU09bY2tve5e58bGNdWVNPTkxM
+S0xOUlheaHny6N7a19TSz87Oz9DR0tPT09fb5PByYVtUUE5LSUhJSUpMTlNZX2z+7OTb1dHP
+zc3NzM3Oz9LX293f4+rt8X9sYFtZVFBQUFFTU1daXWRqc3x+fXd0cG1we3/37+jj4N3b19fX
+19nb3N7j6vF7bWZgX19fYWNkZmxtfPX3+Xt5d3Bxc293fnt1dHh5/vPu7Ojp6ert7vL5fHVt
+amlrbnX89Ovm4uLs+nNqYV1bWFhZXF9jZ215+Ovg3NXPzszLy8zNz9Xc6P5pXFVQTUxLSktM
+TExOUVlfaXn26eLh3dva2djY1tTS0tLS09XY2t3i6vZ4bGRiXltbXFtbXFxeYGJfXl1dXV1e
+Xl5gZGVpbnR4++vi3djW08/Ozs3Nzc3Oz9TY2+DseWpgV1NPTUxKSklKTE5QVVldYmt38OXg
+3dzb2dnZ2NnZ2tvc3N3c29zb29zc4enx/H52bWhhXFxaWFVUVFRTU1VYW19jaXJ+8uzn4d/e
+3dva2NjY2drc3+jt7/H1+Pn29PX8eG5tbWpnZV9gYF9fYGBgYWVpamhnZmhtcn379O7m3tnU
+0c7P0dXY3OLt+3ZqZWJeW1hXVldZW2BlZmltc3v++fTs5N/d3N3e3t7c3d3e4+v+bmZeWlZS
+T09PT1BQU1heav3q3tfRzszKyMfIycnMztPa5PVuXlZPS0lIR0ZGR0lMUFhfanT67OTd2tnY
+1tXU09PU1NPV2Nvd3eHo7vT1/HJqX1pYVFNSU1NSUFFTWFtdYm3659zUz83LycnLzM7S2N7l
++mtkXlhTUU9OTUxNTU5RV1xfZnvq4d7Y1dPQzs3Mzc/X3ur7cWxrZV9fX15fYGFgY2psbGts
+bm5rbW90ffjx7Ono5+Ph5+rt7/Lu6uvp7vb6eXRsZ2JfX2BiZGhuevnv6ufn6uvvfXhxb21s
+bGxzffTw7/D4/nh0dXn78+/v7urn4uDe3dza2tvg5u39a2BcWFVSUlJSUlNVWFxhZW7+7+Xe
+3Nzd3t7e3uDe3d3c297i5enu+XpybW5vcnBvbGdqaWxvbWxubm51dXd9/f/59fv7fXRtaGNf
+YWNlZ2loaW1udf316+Tf2tfU0dHS1NbZ3N7i6PRzZ15ZWFZXV1lbX2RkZGNgYWRnbHByeHp/
+/318/vjv6OPg397b2dra2tnb3uPo6+/4fX15cW5mX11YVlRTVFZYWVtfYmRrevLn39rY2Nnb
+3N7f4OHk5eLi4eLj4d7d3uPp+HJlXVpWUU9OTk9QU1daXmJlaWxwd/zx7OXi3+Df3NnU0dDP
+zczMztDV2t7i5e93aF5aVU9NS0tKSkpLTlBTWV1hZnD/9Ozi3dnTz83NzczLzM3Nz9PX3OP3
+bV9YUU5MS0tLSktLS0xOU1hhb/Hj2M/MyMfGxsfIycrLzc7Q1t7odF9XTUlFQ0FAQEBAQUNG
+Sk9ZZ/Td08zIw7++vby8vb/CxsrS3ep3X1dQTEhFQ0FAQEBBQkVHSUxRWmnz4djQzMnHxcTD
+w8TExsnN09nf6v5tZF9bVlJQT05NTk5PUVRYWl5kcPjs5d/e3Nzc3N3d3+Dk6flyamRjYWBd
+XF1eXmJma3bz6uHa1tHPz87Oz8/Q09nk83NmXFZRT01MTE5OUFNXWFlbXWBnbv7v5NvX1NLS
+09XX1tfY2NjX19vc4Ory+XdqY15aV1VTU1RVVVVYWVpcXWFmanP+7ufi3tzZ19TT1NTT1NXX
+2d7q/WxfWVVUVVZYXF9obnR7+vn69v95e3Nwb29scG9wefzu6+Te2tjZ2Nvd3uDp7fJ6bWxp
+ZWRiYWBfXlxcWVdZW19ob/vq4Nze3t3d3+Pm6Ort7/T6fXJtaGJhX19iY2ZmaGx19Ojg2tTS
+0c/Oz8/R1Nfc5O90ZV5YU09NS0hHRkVGR0lMTlVebfDh1s/MycjHxMPDw8XHy8/V3u14Z11X
+UVFPTk1MTE1OT1NZYGh17ebh3t3b29vb3d/j5Ojt9nZrZF9cW11fX2dveH768ezo5eLd293e
+3+Lp6uz0+vb29v92b2tpaGhlXl1eXl9iaWxwef358PP07urn5+ns7/Tz7/Lz9vz///Tu7erq
+5+Pk4uDf4u16amNeW1hWVFZYWlxdX2dqcHl9+vHs6OXi4+Ti4uPk4+Ll5+rq7O7u7u3u7Orn
+5OLg4uHj5+r3eG9ramhlYl5aVVJQT01NTU5PUVRaYGv97eHd2NLPzs7Nzc3Nzc3Nzs7P1drh
+8W9fWVZSTk1MTU1NTk9RVFlfaHB1//b07erk3dnV0tDPzs/S1dvf6/t8cWhkZF9eW1hVU1FQ
+UlNXXWRvc/rv597c29rb297j5+rn6efn5+Tj393d3uDj6Ort7Ons7/L/bF1YVE5LSUdGRUZI
+TE9XX3Hn2tPOzMrJyMfGxsjKzdLY3ODn7vtvZV5ZV1NRT09PTk5NTE1PUldcX2t88ePc1tHO
+zMvJycnLzc7R2eLudWZdV1NPTUxMTEtMTU5OUVddZXD67+bd2NPOzczKysjIyMjJy83R2OP+
+ZFhQTUpIR0VFRUVGSEpLUFdebvXn3dfRzsvLysnKy8zOz9HV19vj6/l0al9bWVhWVVRTU1NU
+V1lcX2RmZ2lsb3d5+Ori3dvZ2NfU09LS0tTW2tzg8XZmXVhUU1JQTk5OT1JVV1teYmprb//v
+6d/Y0c7LycfHxsXFxsjKztfkcFpPS0dEQkJCQkFFSUlJTE5QWW7f09DX3dnRz87MyMPAwsbM
+zs3P09ne3uP6YVZOTEpISEdHR0hISUpNUlxqee3e2tbQz87NzMvMzc7T2+Xp6u7+al9eXl5b
+XFxj/W5cU1VcYl5TVmVz7vN1fOnX09Tb5drRzs7U2NrW1Nrj+fzq6fxfVU9TVk9LRUJHS0hE
+RUxWYmz93dTRz8vFwL/BxsfHyMnN09DV3+9vXFJPT01NS0lKSEM/QUVITVRl8u3j29HP1dLK
+xcLDx8rMztjj6+LY2PNZU1lZUkY/THlXTks9Q2jSy39XUWTP6W7ZzMLI2tzOyM7d39TGye1Y
+Vm12WEtHT1tZWFBOVmN8blVLU/LjbFhbdNvS3OPo2srExs3Pzc3P43h++2pWTU1gb1dFP031
+2/xTVGHh0+ReUV3fzc/yXmrczc/qX15929p8XG/Z0dl7aOzX2m1XV27Y3V9LSlv3/FZOWV5l
+Zmpx7+xs6NXP4Fjvw7nDTT9ZwLnJZFj772RJP0dSZW9paldNU1RbfO7Zy8XPdl1d7uD1Y2nZ
+0udsX2zk3+DWzMnc//bXzu1aVtvRVT8/+9pIODxzxe02LUm2s80/OUR6y8C3rq6yu3c8PErx
+y8TDw89WPjk/TG/l6NvmW0Q7ODs/SFtt4Nrb2drYz8K8vsfL18/ExtL4Xm3o61VGSlheWEtF
+S1RWT0tPX+rf7+HOyszW3M7L1OTozcvP3lzhY0fczbrKMSgpT7q+0drGwVUvKzJLyr+/wMPP
+ZUdK+8m6ubm6w9pSQj9IUWT54+BzXl1aU1N1z8jN+VVj+n5XUV7+4m1PTFZlZmFea/j2bVxi
++N/b29jNys/d6dfU3+fez9bX70lz08rBUTcuOtfHxM3XyNBEMC43U9LExcvO51ZJTmfKvby8
+wszoVUlFS1l26ufl5XhZUl7s2tfg5eb7X1ZaYnD5+XtqYV5kd/f3+fXx8XNkYGb/6N3a29je
+7v5/4OD5av3ablL43s3LZD84QWLVvL3Gxs5rQzs4PVnQx8jP8V1aVlVq4tTKyM/vWExGSExP
+Wmvr187P1tnW0dDQ0tHR1Nri73xpbH54bF9WUU9PTUtLS0xOT09RWWT+4NjV0M/QzszNzc3O
+zs7S5Ovd4+fpZE5OVVVddF9UXFtTW2RbW2FbVFdYVFx869zU0M7JxsfHys/W3O5nWVFNTEpM
+T1VecO7h2NPU0M/X3+bzenFjWVZXVldaW11iYVtbW1xeY2567+be29jRzszLycnKy9Di4Nnk
+/G5UR0lOTVh0YFdeWlBca15jbltRU09OW3P33tPRzsjHx8XHys7V6G1eUExMTE1SWmL849jS
+z8/P0dff9mhaVFBPT05PU1hcX2NkZ2ptb3B+9ure2NTT0dLTzcjGx8nP2ujt6vRmVUpAPkFF
+S1lZU1VXVmLu5uDg92hqb3nj1dHNycfGxcbIyszU4HtaS0VBQEBBQ0ZOWGb74NTNy83Oz9XZ
+3+fq6vJzbGtv//Hv7O98cmdeWVpcXmFgYGNjaHjt5N3d6m173d7p5fZYVFxbdtHP3Njb9eLT
+1NHS6mFZT0xRWV1rcmxvfPns6O71emVdVUxJSkxQWmXx2M3Kx8K/vby+wcjP2utrXldOSkVB
+QEBBQ0VFRkhJTFFYaOrb1M3Iw8G/vby9v8HKzMjN4XZYRD9BP0NRUkxOSkVLV1155O50bGFj
++eLZz8zKycnKy87P0tvqblVKR0ZGSEpNVmVpcdfLy8zNycXHz9nX1uF6YVtTT1BTVVRPTEpH
+RkVFSlVeXF172MvEwb68vL3FzcfBw8nVX0hDPj9NaPzseVJGQD09QklTaOvh3NzZ18/JxMC/
+wsnU62ZYTkhGS1Jecf715N3g4d3c3Nnc5+34cW1ucHzy7PptYVteX1xZVlVWV1lj/uXg4Nza
+1tPS1tzt+uxjVvHb19f2TEJDRVXQwL6/y3NPSERJVWfm1NHX43VlZ/zg1M/V5G1YTk1MR0VJ
+VvHb1tXTzcrLzM3T2d3k7O/+a2lmZWtqYFtbXWFiWU9OUFRVVl5vdm1qeeLZ0s/QzMzS4WNd
+2MjGytxVRkJDVc+/vcLQZUpCPj5GU+/Sztb2XFRWYunX0dHddltOSkxOTFFt4dPOz83Jx8jI
+y8/a397f5XtfVVNXWllTTktNUFJRT05SW2N06OLd19XRzcbDxcjN2OHlclNb3tTZ8lFEQD8+
+S+7Pys3hYVBHRElWftrPzc3Q2eTr39TOzc7Z9VxPTEtJSU5XYnf239bQzs7Oz9LX2tra3uh7
+X1xdX19cWFNSUU9PUVNYW19kZWv74drb5Pbk0cvL0dnd3t3e4uTSyMnUfFJNU1ZaZXJsZmFY
+TkpGRUlOVmJ0f/rq39rTzsrJyczP2OR6X19qamxnY2RnbP3m3dze2tnW2eLq6+xwX1xcXlpW
+VVVWVVFNTlNbZ2tfXXTk2trf2c7Nz8/Q0M7Q1dfd6Onk7fL6dXBkYmNeWlNQT09OTlFUVFlg
+YWBjaGBebu/b09XSz83Nz83Mzc7Pz9HX5ndjXF5gXVdPUFdcV1FPVWBhX29uY2Jmd+/tfG/p
+3t7a293f5uDa1Nfl8ffy93VrY2hvdPN1YVtfZ2xtXV54+PlvZ3r+fG126+Tl7ezs9vHg293g
+5d7Z4/N+fG9nYV9kW1teXFdSV1hcYFty5ez57dbMztvZz83S29zZ2eN5aGhoYV5eWl1aVVVT
+WF5iZWFhaW1vbW764eTr5+De5/T99und3N3a2tnW2uz3fn5zZmRdWFZYXFtXVVp2fXru6Ofh
+3Nvc3Nzd3d7sfnx9d2hmZWZteXxpZ3Z8cmxpZ2VoZ2t5eW9v/fR8cfzn3+Po3tzf6Ojf3t7l
+7u19cGpgXVhZX19aVVZcYmlw9urq6uro39za2tfX3N3e4N/p7/p8c2dfXl9bWVtcYWRhZ2tv
+bWdmeP35+X7t4+fo6ebg6vXt5uXn7Ofk3t/r4t7k6Ph5dWZfW1dXWFhbWllcXGFpbHvw4t7e
+4OTh2d3s597Y2ujxf3VsaGptaF9bWV5jX2VobPnv6uDf39za19bY2+Ph3t/sfHNwff9lXVlY
+XFtWUU9QUFBVWVhfbnz25NvY0tHQ0M/Pz9HV2tzi6fH5e3h2Zm95bGleXFtcXWBhWlxeaX38
+eXj47fJ5bmdjZWpufHp+6tzY2drY2+Dn7/jz9Xd0a2dobm9kX2py9P1qevDv9fzx9fn2/ezp
++f7x6/Hv6vPz7+7ybmVjY2FeXWBpfntsbXjv8nBqbHv2fvHx6uHn4+Dm7Ozj4+nl7/R6cHNs
+a2Vqb2lsdH7/bGNjam5sXl5qbWZkav78evTo3dva1tXPzs7Oz9Pc5O94cVxWWFZWUU1OUlRU
+U1ZZXF9fbW5x9urk39zV19fU2NHN19/b1tfc8WzZzWRFR2zT6VJLT2JcTEpMTlfv2dzt/t/O
+zM/b29LW4uzw5ej9eezrX1BX+/5XUGjlYFFmd19RVPrZ6Wflz9XrbOjQ2vn67Prw1Nl3XVZe
+d+/teGFcdO/j2uxrfP10bX5rXGVjZe3a4WVVXePP0/dRUXfZ5WdWTFnUytteTl/MxNZpYe3Q
+ytDka19vZ11bYmZPSlbx32hOT2Hj1uNgWWfYzubl0dhlX9DJ2HtVXN/Z2/JcUFTtzdJ1VVBp
+1NhlRkRdfWFo6HtVXN3M0fp43dxu/crMWkpbz8xfRU1w1tl2bGzq1u5n8+fvXWTZzdVlT3bL
+0mpWXubW3lpJU2l+e2NPSE/x22ZJRW/L1GNa5s7N0dHOzM7V0c3Mz+t0/O3qck5HSE5aT0hJ
+Sk1WX2VjcHvt0s7d6NfO0NXZ1dnk3dbc+Gdu79zfXUxOZPNkTUVNbH9ZT1/z4t/f09Db3trS
+0dbf9ObU1ORdUF7e2lVARFrxX05WemtMSWjR1fLqz8nP19LQ2+jZ0N1dU2T1bVlWYe3b5WBU
+X9/YbE5VfG1MQklYXFp11s7S08zGxcnMz9fe2tf+T0pQUUtKUWpyTkNayMlaRlX4WEZV0MHC
+w7u6ye3q5lhDRVluWll/397VycrZ6OLpV0E8PDw3NjxIXfvaxry4u76+u77L3NzT2mNSXO7o
+bWNrcGdWS0hRbVhAPEzwbVJa39NwdcK97kt+yuRCP3PCvszcyMDYTUFCSEtUa33dx8HGycfC
+x9Loal5NPTY3Oz07PU1+4urlzsPCx8e/vL7JzsjEytz54dfoVEhPYlM/Oz5GQzw/S2V0Rjvp
+ucVJTc3F6E1Vxa2uw8e3uuVDPUFNYl5X3MTI2uHd3Nx2UVRlTjw4Oz4+PUBW2tXy5snAxszJ
+vrzE3uLQzuVsf9fQ5F1ac/laR0JITkhAQVB1/FhJaL2/V0vUx/xBRNSztM7MtLdyPD1KUEhF
+W8/F1OnUy9Pxa2piZFhFPUBDPz9CT+vYe+rFvsbNxb29x9PNyddtbOLdeFtecWFPTU9ORD5B
+R0VDSFvf7UlHx7reU9XI1F1S2ba0xcKxtudHSVNNREZwztV+69LdXFJaZVVERE1GOjpARkVL
+atDIzcy/vMHFvbu/ycvJz/9efudkUlZcT0I/SExFPj9GSkhJV+3b18zd0re6zMXBz+p+9dG/
+xc2/wHBHQ0A5NTpFTkxT8djc69vNz9zm4dxyTExcXE5T48zK0s7Cv8rTzcvW721jWExLUVJN
+T1peVkxLWWNPTVpiXVtz39TNy8vJ4M66xtrN3GVWVlzs1+HSx95MR0c+OjtHWl1j1cbHy8jB
+wMfNz9pvWk9DPkFERUpc39bc08vM2OTe3ndeaP1nX3Pm39/f4ufsaF1xcVdVXVRWZ2do+9/W
+12JQxMNk4uRZUU5OZtLPzb6/3m5pTkNAR1dfXefKzs3HxcTHys3bYExJPzY2Oz08QVfa1dTJ
+wMHKzs7Q2OTh197l2tDX29rV3XtkVlFcUUZKTEpJSk5aW2nfb0zSve3ZxPJa7O960MbGu7rI
+199bSEE/SUxKVvF8fdrV1tbR1uNiVE4/Oj0/QERR+s7Jyb+7vcC/w8nN2OZ+al9eWVdgXlpa
+WVBMS01UT0tLUFtbW/3Y19jU68a63NG81lV67HDv19LExNbueU9DPz4/RERLZGNq2M/OyMbG
+xszZ61tHQkVEQUxe587NyMHDyMnM1d/9XVNOSkpMTldaXnZvXVlk8PlobfTm/HDq2MzK22Xh
+t81RxtVLUFNPWv3pz8XN2+hqTkhBREpJSV30edzOy8bEw8PL3W5ZST45Oj0+RlbyzcbDvry9
+wMXK1OluW1lSUlleZXdtfPFvXltdX1ROTk1PT1Be8tbkT8y7683Fb/rjX3nW2c7AxsvO5Wtc
+TEdMSkZNV1Zo5dvOycjFydT0V0tEOzY5PD5FV/DPxsK+vL7Bw8jP1en9/2Vda3n97+zt8m1X
+VVlPR0ZGSEZIU2b2X3u8w9q+xOHY7m3Z3v/PxdDY2ehsVUpNTURETlFPX+vaz83IwcbO0t9c
+TEZCPTs9Q0xSaNfHv76+u7u/x83Z62RWUUxOT1BXX2dsaFtbXVZUT1FRTk9SXGh+5NjNycfH
+xcTIzNDU2Nvj5d/f4/V1Z15XUVBOTk5NTU5PT09RUlVaXmd77+fc1tLPzczMzc/Q1d3f3+z0
+8/T2/nR6eXV4eW9raF1bWVdVVlZXWVlaXF1cYGhve/jt5+fm4N3b19PQzs3LyszO09fd6H9p
+X1lWU1JQT09RVFVVWFxbXmBiZmlrde/m3tnX2NnZ3d7i6+rq7vHw7+71/Pv5fXh2a2lpaGxp
+aG90dXj08Pf5//t+cnF2c3B0enRx//1++PDt7e3r7fT1/3Jxb3N7/vnx7Obj3tzc3uHi63xv
+a2RfYE9Xx2g8Q2jZ6Vpc4e1LR/7tV0jauL53b8/KX0r0wsle/sTCckhWzM5RS+/UYkxc0ddL
+Q1VjSj9M6t9p/MvH3Gjnz+NXWt7W/V/izdD6/tfN2Ornz85qWl5mWEtNV1lPTVVaWlZVXWdf
+Yvn8bvfe2NXT0crIzMzKycvQ0dPU5Hd3bF5VT05LR0ZHRkVHSk5QVl5g9d7c2dn02LvA2dXH
+z3VQX8u/yMW6u85XSkg+NDY+SEVJZ9TT4tjIx9Pu6upbSEdMUVddZvzRysvIxMTByNnn6m9e
+VFlgW1NUWl9aU1tgZGFST1xYTE/i0fD22tDL0su7yF3DusfOTDpETEBKyrzEx81kQzoyMjg9
+UNjKyMLFyc/e3trW1dLR1+b+bV5ST11wWFXtz8zNz9PO3VpMUE5FQktl8XBp5uBvVlVcY11b
+cdzZ4d/Szs3Lzc/Y1dfXzcnvQWy+z09DOzxBPDpzuLrJy9hXPjM1Rn3Ov7m4usTeZV1fZvHc
+0dLgYExDPj0/R0hKa8/Kzc3Hw8x6at3Y4e7kz8nQ39/rW01OTU1WZGZfWlVWUkxJVefcdnLl
+18zMyNNVy7W51U8+Q15PTMavtcl6PzUzMTRF2L+7wdd3WEtHTm7Ov73Cy91gTEE9QlVtXmvQ
+xMXX/drI0lhQ9czM+Vp91t1kVFZna1NMVXHs6GtXW1pWWF1Ya83Ez39y2cfC0VBB4LzHWkJC
+T3VPR8+0uM9dRD5BPj5Tzr68wtX5bGFbZuPNxcbR+1dKQj49RE9OTGDb2eN0/NDN81vuz8nM
+193W0dfZ2drb4vxpZ254d15VTkdFTFRWWv3V1nFOV+LRz+pOS8y7xHlIS3fZX1PHtbfGcElC
+RkFEV93HxdJoUE1MTVRo283R43leUElJS01X/tzRy8rMy8fL1trc0snJ31hg6eBvT05UV0tC
+Rk9VUE1PVFZVVV1x6M7Exc/Z0MnAws7Wcu++us9EPVDtbT493r3FVz08QEE6PU3ex8fU5uLd
+5u3dzcG8v8jX73hZR0FMX3d3Znna3WJUX+fedmV55upsYnzV1mZTX+/uZFls2NxfU2Pu/F1b
+6dPrW3jPyM/sa1bsxMxbRlTs6VRE9Lm75lL16FA8OU3Yz+r829LnUUhV9dzb1s/R33ReSkNO
+Y2xgbtzJyOJv4tDO2OLj1tTf4N/pfm5kYFtPS1NiW1FUYW5aSktm7m5SVPDUzs7I117NusNX
+S+bI0EhGw6+9X2nP5UAyOl36T0/o0/tHRlz8XVvaxsv4YufhUUBN7/pZXd3GyenmxsPU5dnN
+1H142tPuYWlrXlJNT1VOTFRbWVNYX2BfavPw7+fdzcPD02fbvslYTWvb90dG38LQYufQc0Q8
+RExJTXLV2d/e0tLr7NTJy9HZ1NViS05YSkVMVF9eWGjc2+nZz9Lf9OXf5/7h1+Rw8t95XFps
+eFtZZG1hX279dWpha+fm6tHHz/zRvs9haed7T0ZO9u9eY+T1VUhFR0RASVxfadvOzs3KxsPF
+yMPCzd/j/F5YV1ZZXFtfbWpkZntzZ2RmX1tWVFdYXGRvc3b/9fjt6OHf3tvW1NLS3N/e3+Di
+5eXm7mnf31xbblZOT05OWWJgbujqfP5ybmReYnx3eurd2tPOzs/My8/c5/NlV05MS0lKSkxP
+W19v6eDc1M/T1dTV1tnd3uDl7uzq7vp6dm1nX1taWVZUVlZXWl1fX3L5+uro5uLc4t7W3N7f
+4dvX2dzW197h5u979vtuZF5cWFZTT09QUVJTVVhcX2Jr/u/m39/d2tnW1dXTz8/Pz8/S1drg
+5/pqYl9cWVhWU1BPUFBRU1RVWV1ia3d+++/p4t7c2dfU1NTV1Nba297i6/Ty7+7v8f5yb25n
+YF9eXl5eYGNiZWVjYV9fX2BhZWhqbHZ/9PHt6ujk4d/f3t3c3N3h5ufq7/f5+ff4+fr18fX7
+9/L5fXhxbGlkXVtbW1lZW15lbXn37+3q5uXl5ebq6+fk4d7e3uHo7/x/f3x5cm5saGJfW1la
+WlpbXmFqc3n78Ork4OHh397c2djX2dzd4et6amdkYF9eXl5iYl9fYWdqamlrbGxtc/338+7p
+49/c2NbV1NPW29/i4+v4eHBuZGBeXVxYVFNUVFVXWFldYGZtdvrs6Ojm5N7a2NfY2dvd4eTl
+5+zy+Pr+fn5++/t1cG9zcHF2cnBuaWVhZGVla3N5fPz+ff7+//76+//78fDv6+rs6uv0/3dz
+cG9ucG9xdHZzcm5vdHV9fvru6+vs7Ozt8fb8/X54eHt++vLw8/X18u7r6Ofl5eXq8P9xaGBb
+VlZXWVlaXF9obXN98+rp5uLf397d3d7f3+Hk5+rq6efr7Ozy+3RqZGBdWlpbW1tbW1xeYGRu
+/e7j3tvZ2NjY2Nja3OPs/W1kX15dWltcXl9fZXBzfPjv6eXk4eDk5unt9ffz9fjz7+/y7/D2
++v18dm9tamhqaGVjY2RiZWloa3R5efjx8+/w8e3o4t3b2djZ2t7k6/t1b2tkX1xaWlhYWlpd
+Xl5gZm959ezj3tzZ2NfY2Nna3d/j6/d5bmlmY19dW1xcXV5fY2VpdHr99u7n4uHg4uDh4+Xn
+6u74eXFvbGha7ORKR8/FP0nLz8zUVFHCwz5M2WxWds7nT/prWv5tVfHJ/FB+8W1179/f0N/v
+3eZ67/nu7u/tfW5qXlhcW1xiZWRnbnVvcvPz7OTp7O3r6Ojp+ezu7ep6+n7q4+zf3eLl4O31
+6W5vZFpjWFhcVFpVT1taWV5p+e/y6t3X3t3c3NPb2tPa2drl3tzw6eZxZWZrYl1cXWJlWlNb
+XFdbWWXr5vv9+2Z3bHfh7ePX1Nfk4dbV49zb4eFv/v9uX1ZcYltQU1hXVU9WWllydW7p1tXb
+3dnQ1drZ2tng8+nsfvvo7ejr+fhvcG1tZWNoZVpTVVlVV15kaGNs8+716NjV7m3v3ulvb+jg
+8P5/7+Xc83Ps3+l469ze8PzvdFpf4tfoW1n0eVVOXfBnVmLyaVtcXXLxd2Tr0+hh68zTanrS
+z+Pr19T3bN/Z73Dh0ddrWWj5WUtPYGJSTVJSTUxQWFxvb2dg+NnX2NHLycrP1NHIw9Nz0L7S
+TUxTZd1fSVbkTTpY30lA4cpgWu/f5VtPXOVxSFPY1mv0y8bW7eLazs/b9N7M0G9LTHR3UUlc
+3ud49t/sa3r8enHp1ehkbnlr6NTecmhQRU9hbu1PS8m6bTlH1+5DPui0ttrUtLhWOETyXEJc
+wL7dY+rhVUVS1M3rb9/mW0tFR01KRUtm+nZ86dTJzNbYzc3f69bN1uzbzdhwYvDcaE1Na9xu
+SEthbkpFV059vMdbTufhSD9I2bO1ysi90jwwO0lPXN7Avs5nXG9hV+rKwsbJzvxQRUJIRUBH
+bOVx/dnOzM7V2NbS09XX1tjecV1hc11QUFxQSEtbdmBNT2BrX2hYVcO30U3mxvNCReWyrsbY
+vcZBLzhKS0tszMbdUU1ZWk9ezsHEy8/S3llCQVR+XVNtzsneWnjKy21e3c3fX2bj2mpTZ3lX
+TFFpYE1OZuHmXVZrcGBo6+lb07i9a1Tc2Es9SsWvvezOwmY2MkJZaPTOwMd1T1ttX2XbxsHL
+2/NkTUJDRENPbnZcXuja6Gl11cva/93KzeDu1c3ab2B99mFaZXtsUlBfbWBOT2R/Xlfg5WPN
+w9xcZF5GR17XtrLDzMf5OzM/V+3Ty8DC3FNLV19k5M7Fyt1lT0lDPz9FT3XyZmjp1d9w3svG
+0fPczM7sddTN4Wxs72lPT1/yZlFWXF9dT0xX+vpp9c7Oa93Fx9phUVFk7eW/tcDZ91dBOTxH
+Ztvc1NZ6U0xSY+vSycHDz+1aSkRHSkxPcNva6frg1d3l2cvK3PXn191kXnVvXVJWXVxYX/36
+ZWFqbF9bfdfcYWPez9jj7lfxwsn0V1FOUltbzri91vRoTkA+SH3P0dHS71tLTGHi18/Jx9dl
+SkdMRkBM8t1wWmrb2Gtb1L/D2ePMw85tfc3K7FRW8etVSVRtWUpJT09KTlpbT01e18/68N3Z
+wL3NYmzU0eFp17ayw25g8lQ9Oknn2mlXVFdNREZU7NrW0NHkXk9TV1JPXN/T5Gly2s/W39fJ
+x9Lm7NjR3Xp64u5dU1ZbWFNZbm1TTVRiaFtXd9jabF7mz+vezMvP8ltNXnV4z8LAyN9gST8+
+Q1Zy3NPd+WZZX27gz8bCxcvfYlVOTktITldoYVNRXftzbfXd0tje2NPS2tzY09jm5ubq/Gdd
+XmZlWVNSWlxUT09n4vhiavFk6cnIz/hfZPJ+X9O8ub/ZZlFGPz5Ja9vX72VfWFhUXufOyc/W
+6G1dTkhITVv88Whm7N7e4N/UzMrMzM3S2t/f2+HvcGhoXVhQTk5NS0pMTkxIS1hdYWzq2dbo
+ctS/u8PaedjK0ObVvbe93FROS0I7Pk1uclBKSk1OTFXyz8rJzNPZ6XRgVFRf8en39ufX2e18
+c/Ls8P3p3+jvcnzw9Hx2+e/6bWNkZGRaVVthXlhaZ/30e/3e09Ta3t3c19jb2uf/aWVo++Lc
+1dTZ6GFSTU1RW2hqdm9lX2BlbPvq4uPqfWpjXFhWVlheZ3b2597Y0tDQzszNzs/P1Nfd6/l3
+aWRjX1tXUU1LSUpKS0xNUFZcZnzp3NTQz83MzM3MzM3O0tfd6PL9eXdycXFoX1pXVFJSVFda
+Xl5dXWRra252fXl6ffz27ebl5ebl5eTj39/f39/e3t3b29zf5uju+nVrZVxaWFZWVldYWlpY
+WFdZXmh69+Td29nY2tza2drc3dvb3uPo7e/4fXVvaGZoa2xrbW5taGJiZ2dmZmlra2tucGxp
+ZmdscHr06erq7PDt7evo4+Hi5OTk4uLk49/g4ePq8n1qYF5bWVhZWVtbWllaXmJpdu7m39vX
+1dTU1dfc3uLt9X52bWZhYGFjaGxtbGtpaWhoam1ucXVzcHB2fPft6eXm5+Xm6ujq6ujo7fZ4
+amZnaGpscHNxa2Zpbm9y/PLr4d/f3t3c3+Xq6/H5eXVvZF5bWVhXV1hZW11kb3v26uHe2tjX
+1tfY29/m7/Z+dnJweHZ0bWxraWloaWttbm1rbG1ve379/Pjy9X95e3h2eH1//f/89e/u7O7v
+7/Hw+/z29O7r6urr7ezv8/n9d29qZl9cXF9jY2htb3Bz/PPq4d/e3dza293f6PP8c2dgXlpY
+V1ZYWFhbXl9kam7+8+jd2dfV1NPU19ve3+Lj5ufp6u72eGxkX1xbXV5eYWNiZGdpbHNwb29u
+bW1ub3B3+/Dw8O7t7O7x8e/w8e7q6ujn5N/g3t3d3eHl6PN2b2pkXl1dXFtaV1haW15kbv7v
+6efn6ert7/Du6ePg4uHi5+74+/x5cG5ta2trbXJ88+7r7u/r6Ojr7vp2a2FeXl5eYWdtd/71
+8u3p6eXn6+3s7O709vr9/Hp4dG5qZmNjYWNnbnf57+vm4eHi5Obq8/n7fXx1c3V1eHV1en35
+7+rn5ufq7O/9eW9qZmBeXl9fX19gX19gZWhtefbr5uPf3dvb3Nva29vb297i5e9+cm9vamZg
+XVtbW1xdW11eXmFlbHb/9Ozi3tzd3dzd3t/f4+z4fm9pZWFgX19eXmBkZmlucHd/9+7n4N3b
+2tjV0tHT1Nbb4fB5al9bVVJPTk5PT1FUV1teZmxvevXs5uLe3tza2tnZ2tvb29ze3+Di5Ofu
++3lwbGhkX11cW1xcW19jZ2lqcv718e/s6uvu7/V8cW1ubGtrcf/17ezp4+Dd2tjV1NXZ3+ty
+Zl5ZWFVVVlZWWFlZWlxfYmZrdHnw6ePc2tfU0tDP0NHR09XY3ODq9XNmYF5cXVpXVlRSUlNT
+VVdYWllbYWVrd/jr4+Dd2djV09HPz9DT1dfa3uPt+nZoYV5bVlVVVllaXmhueP728/P6fnRt
+bW1tcHVzdnd1cW9uc3v++vTu6ePh39zc29va2dze4OHf4+bo7Pn+eW9nX1tXVFJQTk5OTk9P
+UFNYXmd47eLc1tLPzs7MzMzNztDV2dvf6O7v/W9uaWViX19cWFZTUE9OT09QUlVZXmRsfPDn
+39nU0c/Ozs7Ozs7Pz9PX2tzj7P5rXVZQTUtKSklKSktMTlNZX2l57ODZ1NDNzMrKysrLy8zO
+0NXZ3efzb19ZVE9MS0tKSkpKS01PU1heanry49zY1dPQzszLysrMzc/R1dvh7nlmW1RST0xK
+SUlKS05RWmV17uTd2tjW19jY69m/dUbb4s7MVenVePhZ8dVWWODS3UZkx09VPk7LXO9b+sxI
+VFVa+UXn2uvOadDNftDezszp0OLQ3FZ3ZGtmT2x63W153GzpXE9gSE9NTktPa23i7H7Qz+Lv
+ysrM1+/KydFc6dP8aVN32XJXV/9cU05KdFlHUWFtT1Pk4tJ6Zs/K2Vbeys7Y6tLF1FVozPJg
+U1zSd0tP2mZPUlfnbk9f7/hX/Nn9fmfhz/NV98rZYm3c0edcfs3QbWPb025OWfd0TkhVcFtK
+Tn7naF372dzr5NbR1t7Vy8/c2s7T5n7/3uFsW2d+XVNWVE9JRkpLRkNOYmRZXeXU2t/RycfK
+zsrFytPQy83X6eXe8WNbY2JUTFBXUEpJUFZOSlBfYVhZde9+debV1tzXysXN2NDIyNbe19Le
+b19hXlFPT09PTlNaWFNUbPxtanft5PNy9N/e+3De0Nnz9drY3+/r1M/Z7N3Q32didW9aWFlR
+TUxOWl1WWu3b7nXq19PfcGZzZlFLTVVcYnXdzcfEwcHBv8PJ0t/sdlhKPj9TTUBLTUlISUpU
++Glk2cfO4trUz9Df6N3U3/Tt3NHP0M7IxcrU3N9wTT48Pj05NjpBSUpR7s3Ewr66tre8wsXJ
+1HdUVFZOSEBCSEpKS09VXF9ZVHjT1NLQ0s3Hyc3GwcHBwsrY42lWS0M/Pj48PDw/RUpSX/fT
+y83S1c/M0vln/ujubG7j0srKx7+6uLq9wMTL3l1PSUQ/OzY1Nzk4OTs/SlFNVdvMzM3NzcfC
+xsa/vr6+wMfKzNbncFpST0pGRUZKTVBVadrb6OHm4N9tVVRVTUdERU1ZaPrYzMS+vby7ury/
+x83P23NUSkVFQz8/QENGR0VOY2RkXFVUY2tk/uXbz8nKy8nJycrO1dre5O57amx87uvf1NTX
+4H14clxNSUZDQT8+QUlSX/bbzcXAvr2+vr/Ey9Dd/1tNRkVFQkNESUtLVmP83vBpa+bZ19vp
+4tLO1uD1/Oji8G1lZ250bGl169vU0dHNyc7Z8XJ2bldKR0dLS0lKU2nm2c/Mx8LCxcnO2el2
+X1dMRD9AQkJAQUVLT2D72c/T1dHKxsPDxsTBwsfN193e6mxZT0pJSEZGR0tTXmh56uHj72xj
+a2tiWVRVW2JocevXzcjFwr+/v8HEydDd+2FWTEQ/PT4/QENHSk5h8N3Z6nrr19LU29vTzc7W
+4v94/m9eWVVXXGVqfuvd087Kx8bJzNXf9GhYTkhEQ0JBQ0hPXXng1cvGxMPExMfM1t/sdlxN
+REFDRkhJS01d6dbNz9zbzsjJzdXUzczT6lxTVVNNSERER0pLTFJg7NTMx8PCwsXM0tjgd15Q
+S0hIR0dMVmvq29HKxsLBwsXIzdPd+15PSEVGR0VFSk5PV2fl1dx4avPe3OdvcuXb3ehxavrs
++2lcXWlvb3F68t3Ry8fFyMrO1t31YlRLRUJAPz9ARlFj7dnNxr+9vb2+vsHL2+1yWkk/Pj9E
+RERHTV3p2tbZ5e7p7Pn9bW/s6PtsX15tc25taWZscXJv/uvZzsrEwMLHy8/T2vRdT0lHREA/
+Q0pWZnvj08zHxcfJzNHd9WFVTktJSElLTVFbdOPa19XPzc3O0t3pe2ZdWldTUlBUWVhaXGT2
+4OT3e/7x9nfx2MzHxMPCwMDGzt38a11PRj89PDw9P0VOXuvWzcnGxMLCxMjLzdTZ6HNfWVVP
+SkdFRERERERFR0tTWmjv3dTPy8bBv7++v7/AwsXIzdjlcFlOSUdFQ0JCRUhOUVVZX2x19uvh
+3t7d2NTT1NPQz9LV1trc5PH7b2VdVVFPTk1MTE1OUFJXXmp1/+7m39nW1tXT0dLS09LP0NPV
+19rf6vP7emxmXllUT01LTE1PUVVaYW356t7Z1dHP0NTV1tjc5OrzfW1lYWBfXVtZW2FhYWVm
+amtma292fXVuePr5/X769Onk4N3c29rb29zf4uTl5+zu9/50b29tbWtoYV1cWVlZWlxfXmFs
+ffbv5t7a29zc3N/p6Ofu83htaGRfXV9fX2NmbXJ98PPz7Ons9e3o5urv7+zm5+rp5+jv/Xr8
+fv10aGtubW1pa3J4fmtkY2JfWlldaG1sbuvf4OPf2dXX2dze3d/s8/V3bWxoYF9dXl9iZF9l
+duv4b3vu5+39/fbo3O1ibdrV3+/j0M/uVVJn+GBUXO/kd15daHlvXVdXXF1YWV9nZ2Fm++Tb
+2tfRzs3Pz87P1NnZ2t3i5OXh4ubu9vNsXVpaU0xLTE5MSkpLT1NUV15xfXf43NXc5dvR1N3e
+0szLz9LQ0dvw8ebwaWFuf2thYm50YFZPTl35bFZNUGf/Ylp608zV7OHQ0/Vgaezg+G/r2NPb
+5N3S0dzq93BaSUFFSUU9PEp843vtyby+zNnLwsv7X+fO21tRb976XWHt429RS05PSENGSk5U
+WW16aNi/vcPKzcbM/1zhwr3EzsjH7Uk+PkVEP0FOcOz679rO0N3s73hYRz89PT0/R1d938y/
+vcDCvrm5v8nBvMdwVmnmZUlHWWJKPT5HSD89SFhQSE//8WJo1MDE1NjFvsfj3svL3ejd3VVS
+w7nLTEDuzE84Pcy7zFhY2eg+MDI/SkRAVM3Ez9vOxMLO183Fzepu/vJoT1Jv339g9dPNztbO
+yMbS9XBlWlFPUFRSUVBPTU5OS0lHS1FPUF3r2Nfh483GyMfFxMLBxM7Z19fN0O3wfU5r0XxI
+PT1Y+UE2Rs3F6EtM7Hg9Nj9b6OLey7y7w8zMyMXN1c/Jz+ZjXFNGPTxJXU1BSt3GzXFf1cng
+TljQzPxY3L7CYkhxx9RKQnTL60E/aNFnR0/j0XtOW9TIzd3aycv8V1Rt1HBLW8zF2EQ/3brJ
+RztMzchPPWC9u85dUPh7Qjc7TfbkfO7SzuBpYfzbzcnJycrR6WVWUU1V9NPmYd/Hws14YtvT
+Zkta9nNSTFBjVEdESlNXVU9ZbGtv8d3PzNLd183LzcvEv8DJ0tjd72VdUk1NUE9PTEpKR0JF
+UGf7YFNn1dfp7dPCvMTZ39nvYFld7tnd7mxfYXFlWl7t1tfkfO7mcFNLU2v3WVXfyMjjXW/S
+111Oec3UWklR3t5VS175alVObdbhYnPT1HlWXtbK2XfcysbRdXnY1e9qePxoWVNTXGhaXVhN
+S1FPZ978XFdMXc/QePHKv73H6fjT8E1KV+nP2fTh099oT1L6099mafL8VUE/TGFRRFfPyN1k
+7sa+zmbgwcH8Ve3Ky2xLVuJsSEVPXVhMTFZbTUlW8Nztdu/VzNLXzcG+y+TgzszeaWN43t/q
+6W9aVEtIVeFkSEtMS0hAQ1zf+n/Pz+b6893b4tnGvsPKy83M2nx/4dzj9WZbV0tEQ0VEPT5L
+c+1obN7P1/l618XL29fNytV4ZtrM4Vxw4epcTFXrdklFW25USkpl4GNO+cvO3+zk3+nt2cvN
+1tXSztltaNbN419dZt3ZZE5RTURCSEpRY/f2aVZYcmRNXtHGz+7cxsXmYtrHyt3r18nRY2Hj
+4FM+P1LtYkpV2c9qTV3Vy95o7NLeX2bo6fhcWGtoVFNfXFRcX2VrZ23l0s/Q0tXPysrQ2dbT
+1N9ibOltUklMU2NmTEtQTk1eYXFxWOvCyFFDfMbF4V3Mu8dRUdLH4U9U2tdSSFnj311PZexZ
+RlHt2+hv5tHXX05Xbu3m69zNzM/a7u/b2flr/t3Tek5UaVJDQkZOUUlLa/JaVnvc1dTSycPG
+zM/LxMLExsvQ1e5TTlhaWVJLSEk/OUFUT1VVVfblTUnhz9bM0c7GzvPpy83UzMvOfk9uz9xO
+S37P2kk+Vdl6SUpv1tpv9NLW7N/R03FVdNLXV0pk2/ZKQFDc4ktEYdPcXFbxzdBkXd3WdF3q
+z9D/YuDP22d2z8fSee/Qz3VV/s7UX0xRdvJUSFvdeUc/TejjS0Bf1mpHTfHbam3KvshiY8S4
+xl/qu7bLTUrv01Y+RXzvST5EWl5LSl/Z3mJr187W39fPzuBiat3a6/X+8OHqXlhfaHbx+fl/
+Y1zr1/xbWmTv5Gpd/N73cmhcZuLecmBo6dnyV1vl1N9pWmrsd2n439rf4OHt/uzc4vfq29ba
+eF/15FtPYXRfaVlOTlVOUFhUW+7fe2Ro+NrP3uzYzs7S39zMydXr39rhfmz76nRVUFJNSklL
+TEpMXu9vUFXlzM3e3MjCy9fPw7/L4tvR2mNVVU9NTEpHREFARUpJTGTv+vbm4d3SzcnHysnE
+w8jNz8vJzd/o7mJST1FRT01JSEpKS0dFTFZaVVJf/vh99dzLxcjLysfFx8vKxcLI1etpaHFc
+T09SUE1HQUNKSUhNVVlYVVhjfd7OyczSz8fHz9jPxsbO53Ty5/hx/vZvX1ZQUE9OTE9RUVFP
+T1hgY3Ls3NXU1NHLyMnP29zV0uBuYf3Y3F1PW+z3WExU6uhaT1JaW05KUWRqZmNqftzb2NXT
+0M3N1dbT1dfY29nY3eTi7W1aV1dWUExLS0pJTVRaU1Vw1tPr/drKyNbm1cjK4Xnu3O1fXHvi
+9FtVY29fWWF5+2JYX+/tambu2dno8eLY2+jz7ODl/WplZGZhW1haXWFeWVxpdHZ+69/c2trW
+1dne5uzp6e92aF9cW1teY2doamltfPz++eje3d/r4djW19zZ1trlfHVuZ11ZW1taVlNVWFlW
+VldaWltkdu7m4d7d3N3e3tzb29vY1tbV1tfX2Nvc4+97bGJbVVBOTU1NT1NWWV1haXj57ezp
+5eLf2tfX19fX2drc3+Xu9/x9b2pmXllTUE9SVVZZWV1oePvw5t7b2tnX1dXV1tTV1dfZ3OHq
+8XVjWlRRT09PUFJVVlZbX2Zrb/rr39vW1NLS09TW19vf6Pd1a2JcWllZWlxfZWpzfPz19n15
+d3v/+vn3/np8//X28u7y6uXg3t7e4eXo6Ovv/Hp4cWxgX2BgYGZrbW1qZ2NfX15fY2Ztd/3v
+6ufi39/e3eHi4ePk5OXl5+vv8ft2bWpqZ2ZlZ2dobXF7/Pfs6unt/3FqZWBlaWptbnBubHV6
+e/98dXN0d/rv6eTf3+Df397e3Nzb2t7h6e38bWtsZ2RhX11bWFVUVFJRU1NVWFteZnX05dzX
+1NDNzc3NzMzNztHW2+LsfGtlY2FcWVtbWVlXVFJTUlJQUFFQUFJTVltkdevc08zIxcPCwsHB
+w8XHzM/Y4PVlV05KRkRDQkNDQkJERklNU11u797WzsrIxsTDw8LDxMXIztLP22hUTktMTEhG
+SEZBP0FDSVVm9t/Z2NPNycbEw8XIzNHX29zc3+t1YFhPTEtJSUtMTlBRVlpiduvb19PQz87M
+y8vMztTc8GxhXFdUVFNSUlVXW11gZWRgXlxeaHzx6N3Z1NHPzs/Oz9DT2N7l6e32dWZdWlhS
+Tk1NTk9RU1ZbX2FpdPno39vW0dDQzszNzc7Q1Nfa2+LvdmZeV1FNS0pJSElKSktLTlNYX2l9
+6d/Y08/MycbFxMTExMTGyczS2eD1bl5XUk1KR0VEQ0NCP0ZKQ0dFTmFfcvTXz9TQzMfEw8C+
+vL7AxcvQ2+1xbGBdWFRTUE5MTExMTUxMTExNTU9UWl9ja/rj2tTPy8jFxcbFxsnMztHT19ze
+5vJwX1ZQTEdCQEBBQUJESEpNU1tn9+DXz8zIxMHAv8DBxMbIys3P1Nvk/2JYT0pGQz8/Pj4+
+P0BBRUhOWm7j1c3Hw8C/vr6+vr6+wMPHzNXlblpPSkVCQEA/P0BCQ0ZJS01TWmr349fOysfF
+xcTFxcTFx8nM0Nbd6ntlWlNNSUZGRkdHSEpMTk9SWmNz8eTZ0c/My8rJycrLy8zO09ne6P1r
+YFpYVFJQTk9PT1BSVlhaXmBmbXj27u7s5t7c3Nzb2NfY2Nrb3uPq7/f//Hp2cG5sbGpmZ2Vk
+Zmlqa2tqZ2ZmZmpsePTw7enm4eDf4OTm7/l5cm5naWxzfX/+9/Hs6ebk4+Pk5uzv9P11amBf
+XltYWlZRbFpfZFr+829y7+HX3+rc19nb6OHb4erp5N7g8+fk4+b8ePl8cWpocG9iXVxcXllV
+Wl1dWldYXV5dXmRv9+3o39va293Z09HP0NLT1tjZ2tzh9XlvaGJeXFtbWllYWFlZWVpbWllZ
+W15lannw5t3Z2NXQz87Ozs/S1dnc4/hvZFxVT05NTExMTk9PUldcZXP56t7X0c/MysnJycrL
+zc/U2N3h7nprY11WT0tJR0VEREVHSUtOU1tkb/Xj2NDMyMXCwMDAwcXIy87U3el7Y1lRTElH
+RUNDQ0NERUhLT1ZfcO7c1c7LycbDwsHBw8THys7T2OP3alxVT0tIR0ZFRUVGSElLTVBWXWZ5
+6t7Yz83LycjIyMnLzc/S1tnc4+38b2NbVU9MS0pKSkpLTU9TWV5o/unc1c/MycjGxsXFx8rM
+z9jkeWZcU09LSEZFQ0JCQkJER0tSW2rv3tfOy8nGxMLDw8TGyMnKzdDX3ux9ZltXUk9MS0lI
+R0hJS0xNT1NZXWRrc/fq393Y0s/OzMrJycnKy83P09jd4en7a11XUU5MSkdGRkdISEpNUVhh
+du7g2tTPzs3My8vJx8jJyszP1NzpcV9ZVFFPT09PTk1MTU1OU1lfZnJ/9Onh3d3c2tjX2Nna
+293d3eDn7fD8e3Z0dnZ/8+/t6+3u7+/v8vh5bWxoYl5bWFVTVFRVVVdaWltbXmBp/Orh2dPO
+y8nHxcXHyczP197pfGpfW1dRTkxJSEdISkxOU1heZ2786uDb1c/NzMvMy8zNztHW3ODm8Hxs
+X1lUT09OTU1MS0tLTExOUFddZ3bv39jPy8jFxMLCw8TGyMvO1N3q/WthXlhSTkxKSEZFRURF
+RkdKTVBWX3D55NfPzMnIx8fIyMjIycrMztPa4vBzZl5XT0tIRkVERUdJTFJZYG745dvV0tDP
+zs3Nzc7R1NXX2d3j7Px1bGdeWVVTT05NTk9SVllcYGp09+rl39zb2tra2dna2NjZ2t3h6fxy
+amZhYGFlZmNiX19dW1tcXmBkaGxucHf/9erh3djTzszNzs7P0dbd5O37dmNdWVVVUU9PT05N
+TExMTU9RVl1q/une19PPzczLy8vKy8zNz9TZ3uTr6+/9eGteV1JNTEtLS0tMTk9TV1tibn77
+9Ozn493c2tfU09LQ0NHU1tjZ2dvf5Ovt/Hl3dHJtZGBfXFlVUU9NS0tMTExNTlJZXmzv3tXP
+zMrHxMPCw8XHyc3T3O1uXlROS0lJSUpMTk9SVlhcYGVsfu/k3trY09DPzs3OztHX3en9aV5Y
+Uk9OTU5PUlddYGVsffDq5N7a2tra19bV1tfW2Nrc3uLq83pvaF5YVE9NTExMTk9RVlpfaHPx
+5eDf3tzZ1tTS0M7Nzc3Nzs/R1tzh8HhmX1xWU1FPTUtLTEtMTE1OUVZcZXD96d/c2tjV0M3M
+y8nIycrMztLZ4Ox+a15aVVFPT09QUlNWW19gYGZtdP379u7p5+bn6+3t7/Hy7+7u7fl9e3Z2
+dHd4//rx6OXj4N/i4+Pk4+fn7PD6dW5ramppZGBfX11dXmJnaWpoZF9fXVtdYWp2/uzh29bU
+09DPz8/P0tjb3+Xj6vlvY15aVFBOTUtMTk9VWF1hZm12+evh3tza2tnX1dXW1dTW2Nnb3uDh
+4+Lj6/t3
+--hal_9000--
+--owatagusiam
+Content-Type: MESSAGE/RFC822
+Content-Description: Multiple encapsulation
+
+Received: from tomobiki-cho.cac.washington.edu by akbar.cac.washington.edu
+ (5.65/UW-NDC Revision: 2.23 ) id AA17676; Thu, 24 Oct 91 17:34:03 -0700
+Date: Thu, 24 Oct 1991 17:32:56 -0700 (PDT)
+From: Mark Crispin <MRC@CAC.Washington.EDU>
+Sender: Mark Crispin <mrc@Tomobiki-Cho.CAC.Washington.EDU>
+Subject: Here's some more
+To: Mark Crispin <MRC@CAC.Washington.EDU>
+Message-Id: <MailManager.688350776.11603.mrc@Tomobiki-Cho.CAC.Washington.EDU>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED;BOUNDARY="16819560-2078917053-688350843:#11603"
+
+--16819560-2078917053-688350843:#11603
+Content-Type: APPLICATION/POSTSCRIPT
+Content-Transfer-Encoding: BASE64
+Content-Description: The Simpsons!!
+
+JSEKJSVCb3VuZGluZ0JveDogKGF0ZW5kKQolJVBhZ2VzOiAoYXRlbmQpCiUl
+RG9jdW1lbnRGb250czogKGF0ZW5kKQolJUVuZENvbW1lbnRzCiUKJSBGcmFt
+ZU1ha2VyIFBvc3RTY3JpcHQgUHJvbG9nIDIuMCwgZm9yIHVzZSB3aXRoIEZy
+YW1lTWFrZXIgMi4wCiUgQ29weXJpZ2h0IChjKSAxOTg2LDg3LDg5IGJ5IEZy
+YW1lIFRlY2hub2xvZ3ksIEluYy4gIEFsbCByaWdodHMgcmVzZXJ2ZWQuCiUK
+JSBLbm93biBQcm9ibGVtczoKJQlEdWUgdG8gYnVncyBpbiBUcmFuc2NyaXB0
+LCB0aGUgJ1BTLUFkb2JlLScgaXMgb21pdHRlZCBmcm9tIGxpbmUgMQovRk12
+ZXJzaW9uICgyLjApIGRlZiAKJSBTZXQgdXAgQ29sb3IgdnMuIEJsYWNrLWFu
+ZC1XaGl0ZQoJL0ZNUHJpbnRJbkNvbG9yIHN5c3RlbWRpY3QgL2NvbG9yaW1h
+Z2Uga25vd24gZGVmCiUgVW5jb21tZW50IHRoaXMgbGluZSB0byBmb3JjZSBi
+Jncgb24gY29sb3IgcHJpbnRlcgolICAgL0ZNUHJpbnRJbkNvbG9yIGZhbHNl
+IGRlZgovRnJhbWVEaWN0IDE5MCBkaWN0IGRlZiAKc3lzdGVtZGljdCAvZXJy
+b3JkaWN0IGtub3duIG5vdCB7L2Vycm9yZGljdCAxMCBkaWN0IGRlZgoJCWVy
+cm9yZGljdCAvcmFuZ2VjaGVjayB7c3RvcH0gcHV0fSBpZgolIFRoZSByZWFk
+bGluZSBpbiAyMy4wIGRvZXNuJ3QgcmVjb2duaXplIGNyJ3MgYXMgbmwncyBv
+biBBcHBsZVRhbGsKRnJhbWVEaWN0IC90bXByYW5nZWNoZWNrIGVycm9yZGlj
+dCAvcmFuZ2VjaGVjayBnZXQgcHV0IAplcnJvcmRpY3QgL3JhbmdlY2hlY2sg
+e0ZyYW1lRGljdCAvYnVnIHRydWUgcHV0fSBwdXQgCkZyYW1lRGljdCAvYnVn
+IGZhbHNlIHB1dCAKbWFyayAKJSBTb21lIFBTIG1hY2hpbmVzIHJlYWQgcGFz
+dCB0aGUgQ1IsIHNvIGtlZXAgdGhlIGZvbGxvd2luZyAzIGxpbmVzIHRvZ2V0
+aGVyIQpjdXJyZW50ZmlsZSA1IHN0cmluZyByZWFkbGluZQowMAowMDAwMDAw
+MDAwCmNsZWFydG9tYXJrIAplcnJvcmRpY3QgL3JhbmdlY2hlY2sgRnJhbWVE
+aWN0IC90bXByYW5nZWNoZWNrIGdldCBwdXQgCkZyYW1lRGljdCAvYnVnIGdl
+dCB7IAoJL3JlYWRsaW5lIHsKCQkvZ3N0cmluZyBleGNoIGRlZgoJCS9nZmls
+ZSBleGNoIGRlZgoJCS9naW5kZXggMCBkZWYKCQl7CgkJCWdmaWxlIHJlYWQg
+cG9wIAoJCQlkdXAgMTAgZXEge2V4aXR9IGlmIAoJCQlkdXAgMTMgZXEge2V4
+aXR9IGlmIAoJCQlnc3RyaW5nIGV4Y2ggZ2luZGV4IGV4Y2ggcHV0IAoJCQkv
+Z2luZGV4IGdpbmRleCAxIGFkZCBkZWYgCgkJfSBsb29wCgkJcG9wIAoJCWdz
+dHJpbmcgMCBnaW5kZXggZ2V0aW50ZXJ2YWwgdHJ1ZSAKCQl9IGRlZgoJfSBp
+ZgovRk1WRVJTSU9OIHsKCUZNdmVyc2lvbiBuZSB7CgkJL1RpbWVzLVJvbWFu
+IGZpbmRmb250IDE4IHNjYWxlZm9udCBzZXRmb250CgkJMTAwIDEwMCBtb3Zl
+dG8KCQkoRnJhbWVNYWtlciB2ZXJzaW9uIGRvZXMgbm90IG1hdGNoIHBvc3Rz
+Y3JpcHRfcHJvbG9nISkKCQlkdXAgPQoJCXNob3cgc2hvd3BhZ2UKCQl9IGlm
+Cgl9IGRlZiAKL0ZNTE9DQUwgewoJRnJhbWVEaWN0IGJlZ2luCgkwIGRlZiAK
+CWVuZCAKCX0gZGVmIAoJL2dzdHJpbmcgRk1MT0NBTAoJL2dmaWxlIEZNTE9D
+QUwKCS9naW5kZXggRk1MT0NBTAoJL29yZ3hmZXIgRk1MT0NBTAoJL29yZ3By
+b2MgRk1MT0NBTAoJL29yZ2FuZ2xlIEZNTE9DQUwKCS9vcmdmcmVxIEZNTE9D
+QUwKCS95c2NhbGUgRk1MT0NBTAoJL3hzY2FsZSBGTUxPQ0FMCgkvbWFudWFs
+ZmVlZCBGTUxPQ0FMCgkvcGFwZXJoZWlnaHQgRk1MT0NBTAoJL3BhcGVyd2lk
+dGggRk1MT0NBTAovRk1ET0NVTUVOVCB7IAoJYXJyYXkgL0ZNZm9udHMgZXhj
+aCBkZWYgCgkvI2NvcGllcyBleGNoIGRlZgoJRnJhbWVEaWN0IGJlZ2luCgkw
+IG5lIGR1cCB7c2V0bWFudWFsZmVlZH0gaWYKCS9tYW51YWxmZWVkIGV4Y2gg
+ZGVmCgkvcGFwZXJoZWlnaHQgZXhjaCBkZWYKCS9wYXBlcndpZHRoIGV4Y2gg
+ZGVmCglzZXRwYXBlcm5hbWUKCW1hbnVhbGZlZWQge3RydWV9IHtwYXBlcnNp
+emV9IGlmZWxzZSAKCXttYW51YWxwYXBlcnNpemV9IHtmYWxzZX0gaWZlbHNl
+IAoJe2Rlc3BlcmF0ZXBhcGVyc2l6ZX0gaWYKCS95c2NhbGUgZXhjaCBkZWYK
+CS94c2NhbGUgZXhjaCBkZWYKCWN1cnJlbnR0cmFuc2ZlciBjdmxpdCAvb3Jn
+eGZlciBleGNoIGRlZgoJY3VycmVudHNjcmVlbiBjdmxpdCAvb3JncHJvYyBl
+eGNoIGRlZgoJL29yZ2FuZ2xlIGV4Y2ggZGVmIC9vcmdmcmVxIGV4Y2ggZGVm
+CgllbmQgCgl9IGRlZiAKCS9wYWdlc2F2ZSBGTUxPQ0FMCgkvb3JnbWF0cml4
+IEZNTE9DQUwKCS9sYW5kc2NhcGUgRk1MT0NBTAovRk1CRUdJTlBBR0UgeyAK
+CUZyYW1lRGljdCBiZWdpbiAKCS9wYWdlc2F2ZSBzYXZlIGRlZgoJMy44NiBz
+ZXRtaXRlcmxpbWl0CgkvbGFuZHNjYXBlIGV4Y2ggMCBuZSBkZWYKCWxhbmRz
+Y2FwZSB7IAoJCTkwIHJvdGF0ZSAwIGV4Y2ggbmVnIHRyYW5zbGF0ZSBwb3Ag
+CgkJfQoJCXtwb3AgcG9wfQoJCWlmZWxzZQoJeHNjYWxlIHlzY2FsZSBzY2Fs
+ZQoJL29yZ21hdHJpeCBtYXRyaXggZGVmCglnc2F2ZSAKCX0gZGVmIAovRk1F
+TkRQQUdFIHsKCWdyZXN0b3JlIAoJcGFnZXNhdmUgcmVzdG9yZQoJZW5kIAoJ
+c2hvd3BhZ2UKCX0gZGVmIAovRk1ERUZJTkVGT05UIHsgCglGcmFtZURpY3Qg
+YmVnaW4KCWZpbmRmb250IAoJUmVFbmNvZGUgCgkyIGluZGV4IGV4Y2ggCglk
+ZWZpbmVmb250IGV4Y2ggCglzY2FsZWZvbnQgCglGTWZvbnRzIDMgMSByb2xs
+IAoJcHV0CgllbmQgCgl9IGJpbmQgZGVmCi9GTU5PUk1BTElaRUdSQVBISUNT
+IHsgCgluZXdwYXRoCgkwLjAgMC4wIG1vdmV0bwoJMSBzZXRsaW5ld2lkdGgK
+CTAgc2V0bGluZWNhcAoJMCAwIDAgc2V0aHNiY29sb3IKCTAgc2V0Z3JheSAK
+CX0gYmluZCBkZWYKCS9meCBGTUxPQ0FMCgkvZnkgRk1MT0NBTAoJL2ZoIEZN
+TE9DQUwKCS9mdyBGTUxPQ0FMCgkvbGx4IEZNTE9DQUwKCS9sbHkgRk1MT0NB
+TAoJL3VyeCBGTUxPQ0FMCgkvdXJ5IEZNTE9DQUwKL0ZNQkVHSU5FUFNGIHsg
+CgllbmQgCgkvRk1FUFNGIHNhdmUgZGVmIAoJL3Nob3dwYWdlIHt9IGRlZiAK
+CUZNTk9STUFMSVpFR1JBUEhJQ1MgCglbL2Z5IC9meCAvZmggL2Z3IC91cnkg
+L3VyeCAvbGx5IC9sbHhdIHtleGNoIGRlZn0gZm9yYWxsIAoJZnggZnkgdHJh
+bnNsYXRlIAoJcm90YXRlCglmdyB1cnggbGx4IHN1YiBkaXYgZmggdXJ5IGxs
+eSBzdWIgZGl2IHNjYWxlIAoJbGx4IG5lZyBsbHkgbmVnIHRyYW5zbGF0ZSAK
+CX0gYmluZCBkZWYKL0ZNRU5ERVBTRiB7CglGTUVQU0YgcmVzdG9yZQoJRnJh
+bWVEaWN0IGJlZ2luIAoJfSBiaW5kIGRlZgpGcmFtZURpY3QgYmVnaW4gCi9z
+ZXRtYW51YWxmZWVkIHsKJSVCZWdpbkZlYXR1cmUgKk1hbnVhbEZlZWQgVHJ1
+ZQoJIHN0YXR1c2RpY3QgL21hbnVhbGZlZWQgdHJ1ZSBwdXQKJSVFbmRGZWF0
+dXJlCgl9IGRlZgovbWF4IHsyIGNvcHkgbHQge2V4Y2h9IGlmIHBvcH0gYmlu
+ZCBkZWYKL21pbiB7MiBjb3B5IGd0IHtleGNofSBpZiBwb3B9IGJpbmQgZGVm
+Ci9pbmNoIHs3MiBtdWx9IGRlZgovcGFnZWRpbWVuIHsgCglwYXBlcmhlaWdo
+dCBzdWIgYWJzIDE2IGx0IGV4Y2ggCglwYXBlcndpZHRoIHN1YiBhYnMgMTYg
+bHQgYW5kCgl7L3BhcGVybmFtZSBleGNoIGRlZn0ge3BvcH0gaWZlbHNlCgl9
+IGRlZgoJL3BhcGVyc2l6ZWRpY3QgRk1MT0NBTAovc2V0cGFwZXJuYW1lIHsg
+CgkvcGFwZXJzaXplZGljdCAxNCBkaWN0IGRlZiAKCXBhcGVyc2l6ZWRpY3Qg
+YmVnaW4KCS9wYXBlcm5hbWUgL3Vua25vd24gZGVmIAoJCS9MZXR0ZXIgOC41
+IGluY2ggMTEuMCBpbmNoIHBhZ2VkaW1lbgoJCS9MZXR0ZXJTbWFsbCA3LjY4
+IGluY2ggMTAuMTYgaW5jaCBwYWdlZGltZW4KCQkvVGFibG9pZCAxMS4wIGlu
+Y2ggMTcuMCBpbmNoIHBhZ2VkaW1lbgoJCS9MZWRnZXIgMTcuMCBpbmNoIDEx
+LjAgaW5jaCBwYWdlZGltZW4KCQkvTGVnYWwgOC41IGluY2ggMTQuMCBpbmNo
+IHBhZ2VkaW1lbgoJCS9TdGF0ZW1lbnQgNS41IGluY2ggOC41IGluY2ggcGFn
+ZWRpbWVuCgkJL0V4ZWN1dGl2ZSA3LjUgaW5jaCAxMC4wIGluY2ggcGFnZWRp
+bWVuCgkJL0EzIDExLjY5IGluY2ggMTYuNSBpbmNoIHBhZ2VkaW1lbgoJCS9B
+NCA4LjI2IGluY2ggMTEuNjkgaW5jaCBwYWdlZGltZW4KCQkvQTRTbWFsbCA3
+LjQ3IGluY2ggMTAuODUgaW5jaCBwYWdlZGltZW4KCQkvQjQgMTAuMTI1IGlu
+Y2ggMTQuMzMgaW5jaCBwYWdlZGltZW4KCQkvQjUgNy4xNiBpbmNoIDEwLjEy
+NSBpbmNoIHBhZ2VkaW1lbgoJZW5kCgl9IGRlZgovcGFwZXJzaXplIHsKCXBh
+cGVyc2l6ZWRpY3QgYmVnaW4KCQkvTGV0dGVyIHtsZXR0ZXJ0cmF5fSBkZWYK
+CQkvTGV0dGVyU21hbGwge2xldHRlcnRyYXkgbGV0dGVyc21hbGx9IGRlZgoJ
+CS9UYWJsb2lkIHsxMXgxN3RyYXl9IGRlZgoJCS9MZWRnZXIge2xlZGdlcnRy
+YXl9IGRlZgoJCS9MZWdhbCB7bGVnYWx0cmF5fSBkZWYKCQkvU3RhdGVtZW50
+IHtzdGF0ZW1lbnR0cmF5fSBkZWYKCQkvRXhlY3V0aXZlIHtleGVjdXRpdmV0
+cmF5fSBkZWYKCQkvQTMge2EzdHJheX0gZGVmCgkJL0E0IHthNHRyYXl9IGRl
+ZgoJCS9BNFNtYWxsIHthNHRyYXkgYTRzbWFsbH0gZGVmCgkJL0I0IHtiNHRy
+YXl9IGRlZgoJCS9CNSB7YjV0cmF5fSBkZWYKCQkvdW5rbm93biB7dW5rbm93
+bn0gZGVmCglwYXBlcnNpemVkaWN0IGR1cCBwYXBlcm5hbWUga25vd24ge3Bh
+cGVybmFtZX0gey91bmtub3dufSBpZmVsc2UgZ2V0CgllbmQKCXN0YXR1c2Rp
+Y3QgYmVnaW4gc3RvcHBlZCBlbmQgCgl9IGRlZgovbWFudWFscGFwZXJzaXpl
+IHsKCXBhcGVyc2l6ZWRpY3QgYmVnaW4KCQkvTGV0dGVyIHtsZXR0ZXJ9IGRl
+ZgoJCS9MZXR0ZXJTbWFsbCB7bGV0dGVyc21hbGx9IGRlZgoJCS9UYWJsb2lk
+IHsxMXgxN30gZGVmCgkJL0xlZGdlciB7bGVkZ2VyfSBkZWYKCQkvTGVnYWwg
+e2xlZ2FsfSBkZWYKCQkvU3RhdGVtZW50IHtzdGF0ZW1lbnR9IGRlZgoJCS9F
+eGVjdXRpdmUge2V4ZWN1dGl2ZX0gZGVmCgkJL0EzIHthM30gZGVmCgkJL0E0
+IHthNH0gZGVmCgkJL0E0U21hbGwge2E0c21hbGx9IGRlZgoJCS9CNCB7YjR9
+IGRlZgoJCS9CNSB7YjV9IGRlZgoJCS91bmtub3duIHt1bmtub3dufSBkZWYK
+CXBhcGVyc2l6ZWRpY3QgZHVwIHBhcGVybmFtZSBrbm93biB7cGFwZXJuYW1l
+fSB7L3Vua25vd259IGlmZWxzZSBnZXQKCWVuZAoJc3RvcHBlZCAKCX0gZGVm
+Ci9kZXNwZXJhdGVwYXBlcnNpemUgewoJc3RhdHVzZGljdCAvc2V0cGFnZXBh
+cmFtcyBrbm93bgoJCXsKCQlwYXBlcndpZHRoIHBhcGVyaGVpZ2h0IDAgMSAK
+CQlzdGF0dXNkaWN0IGJlZ2luCgkJe3NldHBhZ2VwYXJhbXN9IHN0b3BwZWQg
+cG9wIAoJCWVuZAoJCX0gaWYKCX0gZGVmCi9zYXZlbWF0cml4IHsKCW9yZ21h
+dHJpeCBjdXJyZW50bWF0cml4IHBvcAoJfSBiaW5kIGRlZgovcmVzdG9yZW1h
+dHJpeCB7CglvcmdtYXRyaXggc2V0bWF0cml4Cgl9IGJpbmQgZGVmCi9kbWF0
+cml4IG1hdHJpeCBkZWYKL2RwaSAgICA3MiAwIGRtYXRyaXggZGVmYXVsdG1h
+dHJpeCBkdHJhbnNmb3JtCiAgICBkdXAgbXVsIGV4Y2ggICBkdXAgbXVsIGFk
+ZCAgIHNxcnQgZGVmCi9mcmVxIGRwaSAxOC43NSBkaXYgOCBkaXYgcm91bmQg
+ZHVwIDAgZXEge3BvcCAxfSBpZiA4IG11bCBkcGkgZXhjaCBkaXYgZGVmCi9z
+YW5nbGUgMSAwIGRtYXRyaXggZGVmYXVsdG1hdHJpeCBkdHJhbnNmb3JtIGV4
+Y2ggYXRhbiBkZWYKL0RpYWNyaXRpY0VuY29kaW5nIFsKLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYKLy5u
+b3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgLy5ub3RkZWYKLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5u
+b3RkZWYgL3NwYWNlIC9leGNsYW0gL3F1b3RlZGJsCi9udW1iZXJzaWduIC9k
+b2xsYXIgL3BlcmNlbnQgL2FtcGVyc2FuZCAvcXVvdGVzaW5nbGUgL3BhcmVu
+bGVmdAovcGFyZW5yaWdodCAvYXN0ZXJpc2sgL3BsdXMgL2NvbW1hIC9oeXBo
+ZW4gL3BlcmlvZCAvc2xhc2ggL3plcm8gL29uZQovdHdvIC90aHJlZSAvZm91
+ciAvZml2ZSAvc2l4IC9zZXZlbiAvZWlnaHQgL25pbmUgL2NvbG9uIC9zZW1p
+Y29sb24KL2xlc3MgL2VxdWFsIC9ncmVhdGVyIC9xdWVzdGlvbiAvYXQgL0Eg
+L0IgL0MgL0QgL0UgL0YgL0cgL0ggL0kgL0ogL0sKL0wgL00gL04gL08gL1Ag
+L1EgL1IgL1MgL1QgL1UgL1YgL1cgL1ggL1kgL1ogL2JyYWNrZXRsZWZ0IC9i
+YWNrc2xhc2gKL2JyYWNrZXRyaWdodCAvYXNjaWljaXJjdW0gL3VuZGVyc2Nv
+cmUgL2dyYXZlIC9hIC9iIC9jIC9kIC9lIC9mIC9nIC9oCi9pIC9qIC9rIC9s
+IC9tIC9uIC9vIC9wIC9xIC9yIC9zIC90IC91IC92IC93IC94IC95IC96IC9i
+cmFjZWxlZnQgL2JhcgovYnJhY2VyaWdodCAvYXNjaWl0aWxkZSAvLm5vdGRl
+ZiAvQWRpZXJlc2lzIC9BcmluZyAvQ2NlZGlsbGEgL0VhY3V0ZQovTnRpbGRl
+IC9PZGllcmVzaXMgL1VkaWVyZXNpcyAvYWFjdXRlIC9hZ3JhdmUgL2FjaXJj
+dW1mbGV4IC9hZGllcmVzaXMKL2F0aWxkZSAvYXJpbmcgL2NjZWRpbGxhIC9l
+YWN1dGUgL2VncmF2ZSAvZWNpcmN1bWZsZXggL2VkaWVyZXNpcwovaWFjdXRl
+IC9pZ3JhdmUgL2ljaXJjdW1mbGV4IC9pZGllcmVzaXMgL250aWxkZSAvb2Fj
+dXRlIC9vZ3JhdmUKL29jaXJjdW1mbGV4IC9vZGllcmVzaXMgL290aWxkZSAv
+dWFjdXRlIC91Z3JhdmUgL3VjaXJjdW1mbGV4Ci91ZGllcmVzaXMgL2RhZ2dl
+ciAvLm5vdGRlZiAvY2VudCAvc3RlcmxpbmcgL3NlY3Rpb24gL2J1bGxldAov
+cGFyYWdyYXBoIC9nZXJtYW5kYmxzIC9yZWdpc3RlcmVkIC9jb3B5cmlnaHQg
+L3RyYWRlbWFyayAvYWN1dGUKL2RpZXJlc2lzIC8ubm90ZGVmIC9BRSAvT3Ns
+YXNoIC8ubm90ZGVmIC8ubm90ZGVmIC8ubm90ZGVmIC8ubm90ZGVmCi95ZW4g
+Ly5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYgLy5ub3RkZWYg
+Ly5ub3RkZWYKL29yZGZlbWluaW5lIC9vcmRtYXNjdWxpbmUgLy5ub3RkZWYg
+L2FlIC9vc2xhc2ggL3F1ZXN0aW9uZG93bgovZXhjbGFtZG93biAvbG9naWNh
+bG5vdCAvLm5vdGRlZiAvZmxvcmluIC8ubm90ZGVmIC8ubm90ZGVmCi9ndWls
+bGVtb3RsZWZ0IC9ndWlsbGVtb3RyaWdodCAvZWxsaXBzaXMgLy5ub3RkZWYg
+L0FncmF2ZSAvQXRpbGRlCi9PdGlsZGUgL09FIC9vZSAvZW5kYXNoIC9lbWRh
+c2ggL3F1b3RlZGJsbGVmdCAvcXVvdGVkYmxyaWdodAovcXVvdGVsZWZ0IC9x
+dW90ZXJpZ2h0IC8ubm90ZGVmIC8ubm90ZGVmIC95ZGllcmVzaXMgL1lkaWVy
+ZXNpcwovZnJhY3Rpb24gL2N1cnJlbmN5IC9ndWlsc2luZ2xsZWZ0IC9ndWls
+c2luZ2xyaWdodCAvZmkgL2ZsIC9kYWdnZXJkYmwKL3BlcmlvZGNlbnRlcmVk
+IC9xdW90ZXNpbmdsYmFzZSAvcXVvdGVkYmxiYXNlIC9wZXJ0aG91c2FuZAov
+QWNpcmN1bWZsZXggL0VjaXJjdW1mbGV4IC9BYWN1dGUgL0VkaWVyZXNpcyAv
+RWdyYXZlIC9JYWN1dGUKL0ljaXJjdW1mbGV4IC9JZGllcmVzaXMgL0lncmF2
+ZSAvT2FjdXRlIC9PY2lyY3VtZmxleCAvLm5vdGRlZiAvT2dyYXZlCi9VYWN1
+dGUgL1VjaXJjdW1mbGV4IC9VZ3JhdmUgL2RvdGxlc3NpIC9jaXJjdW1mbGV4
+IC90aWxkZSAvbWFjcm9uCi9icmV2ZSAvZG90YWNjZW50IC9yaW5nIC9jZWRp
+bGxhIC9odW5nYXJ1bWxhdXQgL29nb25layAvY2Fyb24KXSBkZWYKL1JlRW5j
+b2RlIHsgCglkdXAgCglsZW5ndGggCglkaWN0IGJlZ2luIAoJewoJMSBpbmRl
+eCAvRklEIG5lIAoJCXtkZWZ9IAoJCXtwb3AgcG9wfSBpZmVsc2UgCgl9IGZv
+cmFsbAoJRW5jb2RpbmcgU3RhbmRhcmRFbmNvZGluZyBlcSAKCXsKCQkvRW5j
+b2RpbmcgRGlhY3JpdGljRW5jb2RpbmcgZGVmCgl9aWYKCWN1cnJlbnRkaWN0
+IAoJZW5kIAoJfSBiaW5kIGRlZgovZ3JheW1vZGUgdHJ1ZSBkZWYKCS9id2lk
+dGggRk1MT0NBTAoJL2Jwc2lkZSBGTUxPQ0FMCgkvYnN0cmluZyBGTUxPQ0FM
+Cgkvb25iaXRzIEZNTE9DQUwKCS9vZmZiaXRzIEZNTE9DQUwKCS94aW5kZXgg
+Rk1MT0NBTAoJL3lpbmRleCBGTUxPQ0FMCgkveCBGTUxPQ0FMCgkveSBGTUxP
+Q0FMCi9zZXRwYXR0ZXJuIHsKCSAvYndpZHRoICBleGNoIGRlZgoJIC9icHNp
+ZGUgIGV4Y2ggZGVmCgkgL2JzdHJpbmcgZXhjaCBkZWYKCSAvb25iaXRzIDAg
+ZGVmICAvb2ZmYml0cyAwIGRlZgoJIGZyZXEgc2FuZ2xlIGxhbmRzY2FwZSB7
+OTAgYWRkfSBpZiAKCQl7L3kgZXhjaCBkZWYKCQkgL3ggZXhjaCBkZWYKCQkg
+L3hpbmRleCB4IDEgYWRkIDIgZGl2IGJwc2lkZSBtdWwgY3ZpIGRlZgoJCSAv
+eWluZGV4IHkgMSBhZGQgMiBkaXYgYnBzaWRlIG11bCBjdmkgZGVmCgkJIGJz
+dHJpbmcgeWluZGV4IGJ3aWR0aCBtdWwgeGluZGV4IDggaWRpdiBhZGQgZ2V0
+CgkJIDEgNyB4aW5kZXggOCBtb2Qgc3ViIGJpdHNoaWZ0IGFuZCAwIG5lCgkJ
+IHsvb25iaXRzICBvbmJpdHMgIDEgYWRkIGRlZiAxfQoJCSB7L29mZmJpdHMg
+b2ZmYml0cyAxIGFkZCBkZWYgMH0KCQkgaWZlbHNlCgkJfQoJCXNldHNjcmVl
+bgoJIHt9IHNldHRyYW5zZmVyCgkgb2ZmYml0cyBvZmZiaXRzIG9uYml0cyBh
+ZGQgZGl2IEZNc2V0Z3JheQoJL2dyYXltb2RlIGZhbHNlIGRlZgoJfSBiaW5k
+IGRlZgovZ3JheW5lc3MgewoJRk1zZXRncmF5CglncmF5bW9kZSBub3QgewoJ
+CS9ncmF5bW9kZSB0cnVlIGRlZgoJCW9yZ3hmZXIgY3Z4IHNldHRyYW5zZmVy
+CgkJb3JnZnJlcSBvcmdhbmdsZSBvcmdwcm9jIGN2eCBzZXRzY3JlZW4KCQl9
+IGlmCgl9IGJpbmQgZGVmCgkvSFVFIEZNTE9DQUwKCS9TQVQgRk1MT0NBTAoJ
+L0JSSUdIVCBGTUxPQ0FMCgkvQ29sb3JzIEZNTE9DQUwKRk1QcmludEluQ29s
+b3IgCgkKCXsKCS9IVUUgMCBkZWYKCS9TQVQgMCBkZWYKCS9CUklHSFQgMCBk
+ZWYKCSUgYXJyYXkgb2YgYXJyYXlzIEh1ZSBhbmQgU2F0IHZhbHVlcyBmb3Ig
+dGhlIHNlcGFyYXRpb25zIFtIVUUgQlJJR0hUXQoJL0NvbG9ycyAgIAoJW1sw
+ICAgIDAgIF0gICAgJSBibGFjawoJIFswICAgIDAgIF0gICAgJSB3aGl0ZQoJ
+IFswLjAwIDEuMF0gICAgJSByZWQKCSBbMC4zNyAxLjBdICAgICUgZ3JlZW4K
+CSBbMC42MCAxLjBdICAgICUgYmx1ZQoJIFswLjUwIDEuMF0gICAgJSBjeWFu
+CgkgWzAuODMgMS4wXSAgICAlIG1hZ2VudGEKCSBbMC4xNiAxLjBdICAgICUg
+Y29tbWVudCAvIHllbGxvdwoJIF0gZGVmCiAgICAgIAoJL0JFR0lOQklUTUFQ
+Q09MT1IgeyAKCQlCSVRNQVBDT0xPUn0gZGVmCgkvQkVHSU5CSVRNQVBDT0xP
+UmMgeyAKCQlCSVRNQVBDT0xPUmN9IGRlZgoJL0sgeyAKCQlDb2xvcnMgZXhj
+aCBnZXQgZHVwCgkJMCBnZXQgL0hVRSBleGNoIHN0b3JlIAoJCTEgZ2V0IC9C
+UklHSFQgZXhjaCBzdG9yZQoJCSAgSFVFIDAgZXEgQlJJR0hUIDAgZXEgYW5k
+CgkJCXsxLjAgU0FUIHN1YiBzZXRncmF5fQoJCQl7SFVFIFNBVCBCUklHSFQg
+c2V0aHNiY29sb3J9IAoJCSAgaWZlbHNlCgkJfSBkZWYKCS9GTXNldGdyYXkg
+eyAKCQkvU0FUIGV4Y2ggMS4wIGV4Y2ggc3ViIHN0b3JlIAoJCSAgSFVFIDAg
+ZXEgQlJJR0hUIDAgZXEgYW5kCgkJCXsxLjAgU0FUIHN1YiBzZXRncmF5fQoJ
+CQl7SFVFIFNBVCBCUklHSFQgc2V0aHNiY29sb3J9IAoJCSAgaWZlbHNlCgkJ
+fSBiaW5kIGRlZgoJfQoJCgl7CgkvQkVHSU5CSVRNQVBDT0xPUiB7IAoJCUJJ
+VE1BUEdSQVl9IGRlZgoJL0JFR0lOQklUTUFQQ09MT1JjIHsgCgkJQklUTUFQ
+R1JBWWN9IGRlZgoJL0ZNc2V0Z3JheSB7c2V0Z3JheX0gYmluZCBkZWYKCS9L
+IHsgCgkJcG9wCgkJfSBkZWYKCX0KaWZlbHNlCi9ub3JtYWxpemUgewoJdHJh
+bnNmb3JtIHJvdW5kIGV4Y2ggcm91bmQgZXhjaCBpdHJhbnNmb3JtCgl9IGJp
+bmQgZGVmCi9kbm9ybWFsaXplIHsKCWR0cmFuc2Zvcm0gcm91bmQgZXhjaCBy
+b3VuZCBleGNoIGlkdHJhbnNmb3JtCgl9IGJpbmQgZGVmCi9sbm9ybWFsaXpl
+IHsgCgkwIGR0cmFuc2Zvcm0gZXhjaCBjdmkgMiBpZGl2IDIgbXVsIDEgYWRk
+IGV4Y2ggaWR0cmFuc2Zvcm0gcG9wCgl9IGJpbmQgZGVmCi9IIHsgCglsbm9y
+bWFsaXplIHNldGxpbmV3aWR0aAoJfSBiaW5kIGRlZgovWiB7CglzZXRsaW5l
+Y2FwCgl9IGJpbmQgZGVmCi9YIHsgCglmaWxscHJvY3MgZXhjaCBnZXQgZXhl
+YwoJfSBiaW5kIGRlZgovViB7IAoJZ3NhdmUgZW9maWxsIGdyZXN0b3JlCgl9
+IGJpbmQgZGVmCi9OIHsgCglzdHJva2UKCX0gYmluZCBkZWYKL00ge25ld3Bh
+dGggbW92ZXRvfSBiaW5kIGRlZgovRSB7bGluZXRvfSBiaW5kIGRlZgovRCB7
+Y3VydmV0b30gYmluZCBkZWYKL08ge2Nsb3NlcGF0aH0gYmluZCBkZWYKCS9u
+IEZNTE9DQUwKL0wgeyAKIAkvbiBleGNoIGRlZgoJbmV3cGF0aAoJbm9ybWFs
+aXplCgltb3ZldG8gCgkyIDEgbiB7cG9wIG5vcm1hbGl6ZSBsaW5ldG99IGZv
+cgoJfSBiaW5kIGRlZgovWSB7IAoJTCAKCWNsb3NlcGF0aAoJfSBiaW5kIGRl
+ZgoJL3gxIEZNTE9DQUwKCS94MiBGTUxPQ0FMCgkveTEgRk1MT0NBTAoJL3ky
+IEZNTE9DQUwKCS9yYWQgRk1MT0NBTAovUiB7IAoJL3kyIGV4Y2ggZGVmCgkv
+eDIgZXhjaCBkZWYKCS95MSBleGNoIGRlZgoJL3gxIGV4Y2ggZGVmCgl4MSB5
+MQoJeDIgeTEKCXgyIHkyCgl4MSB5MgoJNCBZIAoJfSBiaW5kIGRlZgovUlIg
+eyAKCS9yYWQgZXhjaCBkZWYKCW5vcm1hbGl6ZQoJL3kyIGV4Y2ggZGVmCgkv
+eDIgZXhjaCBkZWYKCW5vcm1hbGl6ZQoJL3kxIGV4Y2ggZGVmCgkveDEgZXhj
+aCBkZWYKCW5ld3BhdGgKCXgxIHkxIHJhZCBhZGQgbW92ZXRvCgl4MSB5MiB4
+MiB5MiByYWQgYXJjdG8KCXgyIHkyIHgyIHkxIHJhZCBhcmN0bwoJeDIgeTEg
+eDEgeTEgcmFkIGFyY3RvCgl4MSB5MSB4MSB5MiByYWQgYXJjdG8KCWNsb3Nl
+cGF0aAoJMTYge3BvcH0gcmVwZWF0Cgl9IGJpbmQgZGVmCi9DIHsgCglncmVz
+dG9yZQoJZ3NhdmUKCVIgCgljbGlwCgl9IGJpbmQgZGVmCi9VIHsgCglncmVz
+dG9yZQoJZ3NhdmUKCX0gYmluZCBkZWYKL0YgeyAKCUZNZm9udHMgZXhjaCBn
+ZXQKCXNldGZvbnQKCX0gYmluZCBkZWYKL1QgeyAKCW1vdmV0byBzaG93Cgl9
+IGJpbmQgZGVmCi9SRiB7IAoJcm90YXRlCgkwIG5lIHstMSAxIHNjYWxlfSBp
+ZgoJfSBiaW5kIGRlZgovVEYgeyAKCWdzYXZlCgltb3ZldG8gCglSRgoJc2hv
+dwoJZ3Jlc3RvcmUKCX0gYmluZCBkZWYKL1AgeyAKCW1vdmV0bwoJMCAzMiAz
+IDIgcm9sbCB3aWR0aHNob3cKCX0gYmluZCBkZWYKL1BGIHsgCglnc2F2ZQoJ
+bW92ZXRvIAoJUkYKCTAgMzIgMyAyIHJvbGwgd2lkdGhzaG93CglncmVzdG9y
+ZQoJfSBiaW5kIGRlZgovUyB7IAoJbW92ZXRvCgkwIGV4Y2ggYXNob3cKCX0g
+YmluZCBkZWYKL1NGIHsgCglnc2F2ZQoJbW92ZXRvCglSRgoJMCBleGNoIGFz
+aG93CglncmVzdG9yZQoJfSBiaW5kIGRlZgovQiB7IAoJbW92ZXRvCgkwIDMy
+IDQgMiByb2xsIDAgZXhjaCBhd2lkdGhzaG93Cgl9IGJpbmQgZGVmCi9CRiB7
+IAoJZ3NhdmUKCW1vdmV0bwoJUkYKCTAgMzIgNCAyIHJvbGwgMCBleGNoIGF3
+aWR0aHNob3cKCWdyZXN0b3JlCgl9IGJpbmQgZGVmCgkveCBGTUxPQ0FMCgkv
+eSBGTUxPQ0FMCgkvZHggRk1MT0NBTAoJL2R5IEZNTE9DQUwKCS9kbCBGTUxP
+Q0FMCgkvdCBGTUxPQ0FMCgkvdDIgRk1MT0NBTAoJL0NvcyBGTUxPQ0FMCgkv
+U2luIEZNTE9DQUwKCS9yIEZNTE9DQUwKL1cgeyAKCWRub3JtYWxpemUKCS9k
+eSBleGNoIGRlZgoJL2R4IGV4Y2ggZGVmCglub3JtYWxpemUKCS95ICBleGNo
+IGRlZgoJL3ggIGV4Y2ggZGVmCgkvZGwgZHggZHggbXVsIGR5IGR5IG11bCBh
+ZGQgc3FydCBkZWYKCWRsIDAuMCBndCB7CgkJL3QgY3VycmVudGxpbmV3aWR0
+aCBkZWYKCQlzYXZlbWF0cml4CgkJL0NvcyBkeCBkbCBkaXYgZGVmCgkJL1Np
+biBkeSBkbCBkaXYgZGVmCgkJL3IgW0NvcyBTaW4gU2luIG5lZyBDb3MgMC4w
+IDAuMF0gZGVmCgkJL3QyIHQgMi41IG11bCAzLjUgbWF4IGRlZgoJCW5ld3Bh
+dGgKCQl4IHkgdHJhbnNsYXRlCgkJciBjb25jYXQKCQkwLjAgMC4wIG1vdmV0
+bwoJCWRsIHQgMi43IG11bCBzdWIgMC4wIHJsaW5ldG8KCQlzdHJva2UKCQly
+ZXN0b3JlbWF0cml4CgkJeCBkeCBhZGQgeSBkeSBhZGQgdHJhbnNsYXRlCgkJ
+ciBjb25jYXQKCQl0IDAuNjcgbXVsIHNldGxpbmV3aWR0aAoJCXQgMS42MSBt
+dWwgbmVnICAwLjAgdHJhbnNsYXRlCgkJMC4wIDAuMCBtb3ZldG8KCQl0MiAx
+LjcgbXVsIG5lZyAgdDIgMi4wIGRpdiAgICAgbW92ZXRvCgkJMC4wIDAuMCBs
+aW5ldG8KCQl0MiAxLjcgbXVsIG5lZyAgdDIgMi4wIGRpdiBuZWcgbGluZXRv
+CgkJc3Ryb2tlCgkJdCBzZXRsaW5ld2lkdGgKCQlyZXN0b3JlbWF0cml4CgkJ
+fSBpZgoJfSBiaW5kIGRlZgovRyB7IAoJZ3NhdmUKCW5ld3BhdGgKCW5vcm1h
+bGl6ZSB0cmFuc2xhdGUgMC4wIDAuMCBtb3ZldG8gCglkbm9ybWFsaXplIHNj
+YWxlIAoJMC4wIDAuMCAxLjAgNSAzIHJvbGwgYXJjIAoJY2xvc2VwYXRoIGZp
+bGwKCWdyZXN0b3JlCgl9IGJpbmQgZGVmCi9BIHsgCglnc2F2ZQoJc2F2ZW1h
+dHJpeAoJbmV3cGF0aAoJMiBpbmRleCAyIGRpdiBhZGQgZXhjaCAzIGluZGV4
+IDIgZGl2IHN1YiBleGNoIAoJbm9ybWFsaXplIDIgaW5kZXggMiBkaXYgc3Vi
+IGV4Y2ggMyBpbmRleCAyIGRpdiBhZGQgZXhjaCAKCXRyYW5zbGF0ZSAKCXNj
+YWxlIAoJMC4wIDAuMCAxLjAgNSAzIHJvbGwgYXJjIAoJcmVzdG9yZW1hdHJp
+eAoJc3Ryb2tlCglncmVzdG9yZQoJfSBiaW5kIGRlZgoJL3ggRk1MT0NBTAoJ
+L3kgRk1MT0NBTAoJL3cgRk1MT0NBTAoJL2ggRk1MT0NBTAoJL3h4IEZNTE9D
+QUwKCS95eSBGTUxPQ0FMCgkvd3cgRk1MT0NBTAoJL2hoIEZNTE9DQUwKCS9G
+TXNhdmVvYmplY3QgRk1MT0NBTAoJL0ZNb3B0b3AgRk1MT0NBTAoJL0ZNZGlj
+dHRvcCBGTUxPQ0FMCi9CRUdJTlBSSU5UQ09ERSB7IAoJL0ZNZGljdHRvcCBj
+b3VudGRpY3RzdGFjayAxIGFkZCBkZWYgCgkvRk1vcHRvcCBjb3VudCA0IHN1
+YiBkZWYgCgkvRk1zYXZlb2JqZWN0IHNhdmUgZGVmCgl1c2VyZGljdCBiZWdp
+biAKCS9zaG93cGFnZSB7fSBkZWYgCglGTU5PUk1BTElaRUdSQVBISUNTIAoJ
+MyBpbmRleCBuZWcgMyBpbmRleCBuZWcgdHJhbnNsYXRlCgl9IGJpbmQgZGVm
+Ci9FTkRQUklOVENPREUgewoJY291bnQgLTEgRk1vcHRvcCB7cG9wIHBvcH0g
+Zm9yIAoJY291bnRkaWN0c3RhY2sgLTEgRk1kaWN0dG9wIHtwb3AgZW5kfSBm
+b3IgCglGTXNhdmVvYmplY3QgcmVzdG9yZSAKCX0gYmluZCBkZWYKL2duIHsg
+CgkwIAoJewk0NiBtdWwgCgkJY2YgcmVhZCBwb3AgCgkJMzIgc3ViIAoJCWR1
+cCA0NiBsdCB7ZXhpdH0gaWYgCgkJNDYgc3ViIGFkZCAKCQl9IGxvb3AKCWFk
+ZCAKCX0gYmluZCBkZWYKCS9zdHIgRk1MT0NBTAovY2ZzIHsgCgkvc3RyIHNs
+IHN0cmluZyBkZWYgCgkwIDEgc2wgMSBzdWIge3N0ciBleGNoIHZhbCBwdXR9
+IGZvciAKCXN0ciBkZWYgCgl9IGJpbmQgZGVmCi9pYyBbIAoJMCAwIDAgMCAw
+IDAgMCAwIDAgMCAwIDAgMCAwIDAgMDIyMwoJMCAwIDAgMCAwIDAgMCAwIDAg
+MCAwIDAgMCAwIDAgMDIyMwoJMAoJezAgaHh9IHsxIGh4fSB7MiBoeH0gezMg
+aHh9IHs0IGh4fSB7NSBoeH0gezYgaHh9IHs3IGh4fSB7OCBoeH0gezkgaHh9
+Cgl7MTAgaHh9IHsxMSBoeH0gezEyIGh4fSB7MTMgaHh9IHsxNCBoeH0gezE1
+IGh4fSB7MTYgaHh9IHsxNyBoeH0gezE4IGh4fQoJezE5IGh4fSB7Z24gaHh9
+IHswfSB7MX0gezJ9IHszfSB7NH0gezV9IHs2fSB7N30gezh9IHs5fSB7MTB9
+IHsxMX0gezEyfQoJezEzfSB7MTR9IHsxNX0gezE2fSB7MTd9IHsxOH0gezE5
+fSB7Z259IHswIHdofSB7MSB3aH0gezIgd2h9IHszIHdofQoJezQgd2h9IHs1
+IHdofSB7NiB3aH0gezcgd2h9IHs4IHdofSB7OSB3aH0gezEwIHdofSB7MTEg
+d2h9IHsxMiB3aH0KCXsxMyB3aH0gezE0IHdofSB7Z24gd2h9IHswIGJsfSB7
+MSBibH0gezIgYmx9IHszIGJsfSB7NCBibH0gezUgYmx9IHs2IGJsfQoJezcg
+Ymx9IHs4IGJsfSB7OSBibH0gezEwIGJsfSB7MTEgYmx9IHsxMiBibH0gezEz
+IGJsfSB7MTQgYmx9IHtnbiBibH0KCXswIGZsfSB7MSBmbH0gezIgZmx9IHsz
+IGZsfSB7NCBmbH0gezUgZmx9IHs2IGZsfSB7NyBmbH0gezggZmx9IHs5IGZs
+fQoJezEwIGZsfSB7MTEgZmx9IHsxMiBmbH0gezEzIGZsfSB7MTQgZmx9IHtn
+biBmbH0KCV0gZGVmCgkvc2wgRk1MT0NBTAoJL3ZhbCBGTUxPQ0FMCgkvd3Mg
+Rk1MT0NBTAoJL2ltIEZNTE9DQUwKCS9icyBGTUxPQ0FMCgkvY3MgRk1MT0NB
+TAoJL2xlbiBGTUxPQ0FMCgkvcG9zIEZNTE9DQUwKL21zIHsgCgkvc2wgZXhj
+aCBkZWYgCgkvdmFsIDI1NSBkZWYgCgkvd3MgY2ZzIAoJL2ltIGNmcyAKCS92
+YWwgMCBkZWYgCgkvYnMgY2ZzIAoJL2NzIGNmcyAKCX0gYmluZCBkZWYKNDAw
+IG1zIAovaXAgeyAKCWlzIAoJMCAKCWNmIGNzIHJlYWRsaW5lIHBvcCAKCXsJ
+aWMgZXhjaCBnZXQgZXhlYyAKCQlhZGQgCgkJfSBmb3JhbGwgCglwb3AgCgkK
+CX0gYmluZCBkZWYKL3doIHsgCgkvbGVuIGV4Y2ggZGVmIAoJL3BvcyBleGNo
+IGRlZiAKCXdzIDAgbGVuIGdldGludGVydmFsIGltIHBvcyBsZW4gZ2V0aW50
+ZXJ2YWwgY29weSBwb3AKCXBvcyBsZW4gCgl9IGJpbmQgZGVmCi9ibCB7IAoJ
+L2xlbiBleGNoIGRlZiAKCS9wb3MgZXhjaCBkZWYgCglicyAwIGxlbiBnZXRp
+bnRlcnZhbCBpbSBwb3MgbGVuIGdldGludGVydmFsIGNvcHkgcG9wCglwb3Mg
+bGVuIAoJfSBiaW5kIGRlZgovczEgMSBzdHJpbmcgZGVmCi9mbCB7IAoJL2xl
+biBleGNoIGRlZiAKCS9wb3MgZXhjaCBkZWYgCgkvdmFsIGNmIHMxIHJlYWRo
+ZXhzdHJpbmcgcG9wIDAgZ2V0IGRlZgoJcG9zIDEgcG9zIGxlbiBhZGQgMSBz
+dWIge2ltIGV4Y2ggdmFsIHB1dH0gZm9yCglwb3MgbGVuIAoJfSBiaW5kIGRl
+ZgovaHggeyAKCTMgY29weSBnZXRpbnRlcnZhbCAKCWNmIGV4Y2ggcmVhZGhl
+eHN0cmluZyBwb3AgcG9wIAoJfSBiaW5kIGRlZgoJL2ggRk1MT0NBTAoJL3cg
+Rk1MT0NBTAoJL2QgRk1MT0NBTAoJL2xiIEZNTE9DQUwKCS9iaXRtYXBzYXZl
+IEZNTE9DQUwKCS9pcyBGTUxPQ0FMCgkvY2YgRk1MT0NBTAovd2J5dGVzIHsg
+CglkdXAgCgk4IGVxIHtwb3B9IHsxIGVxIHs3IGFkZCA4IGlkaXZ9IHszIGFk
+ZCA0IGlkaXZ9IGlmZWxzZX0gaWZlbHNlCgl9IGJpbmQgZGVmCi9CRUdJTkJJ
+VE1BUEJXYyB7IAoJMSB7fSBDT01NT05CSVRNQVBjCgl9IGJpbmQgZGVmCi9C
+RUdJTkJJVE1BUEdSQVljIHsgCgk4IHt9IENPTU1PTkJJVE1BUGMKCX0gYmlu
+ZCBkZWYKL0JFR0lOQklUTUFQMkJJVGMgeyAKCTIge30gQ09NTU9OQklUTUFQ
+YwoJfSBiaW5kIGRlZgovQ09NTU9OQklUTUFQYyB7IAoJL3IgZXhjaCBkZWYK
+CS9kIGV4Y2ggZGVmCglnc2F2ZQoJdHJhbnNsYXRlIHJvdGF0ZSBzY2FsZSAv
+aCBleGNoIGRlZiAvdyBleGNoIGRlZgoJL2xiIHcgZCB3Ynl0ZXMgZGVmIAoJ
+c2wgbGIgbHQge2xiIG1zfSBpZiAKCS9iaXRtYXBzYXZlIHNhdmUgZGVmIAoJ
+ciAgICAgICAgICAgICAgICAgICAgCgkvaXMgaW0gMCBsYiBnZXRpbnRlcnZh
+bCBkZWYgCgl3cyAwIGxiIGdldGludGVydmFsIGlzIGNvcHkgcG9wIAoJL2Nm
+IGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBkIFt3IDAgMCBoIG5lZyAwIGhdIAoJ
+e2lwfSBpbWFnZSAKCWJpdG1hcHNhdmUgcmVzdG9yZSAKCWdyZXN0b3JlCgl9
+IGJpbmQgZGVmCi9CRUdJTkJJVE1BUEJXIHsgCgkxIHt9IENPTU1PTkJJVE1B
+UAoJfSBiaW5kIGRlZgovQkVHSU5CSVRNQVBHUkFZIHsgCgk4IHt9IENPTU1P
+TkJJVE1BUAoJfSBiaW5kIGRlZgovQkVHSU5CSVRNQVAyQklUIHsgCgkyIHt9
+IENPTU1PTkJJVE1BUAoJfSBiaW5kIGRlZgovQ09NTU9OQklUTUFQIHsgCgkv
+ciBleGNoIGRlZgoJL2QgZXhjaCBkZWYKCWdzYXZlCgl0cmFuc2xhdGUgcm90
+YXRlIHNjYWxlIC9oIGV4Y2ggZGVmIC93IGV4Y2ggZGVmCgkvYml0bWFwc2F2
+ZSBzYXZlIGRlZiAKCXIgICAgICAgICAgICAgICAgICAgIAoJL2lzIHcgZCB3
+Ynl0ZXMgc3RyaW5nIGRlZgoJL2NmIGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBk
+IFt3IDAgMCBoIG5lZyAwIGhdIAoJe2NmIGlzIHJlYWRoZXhzdHJpbmcgcG9w
+fSBpbWFnZQoJYml0bWFwc2F2ZSByZXN0b3JlIAoJZ3Jlc3RvcmUKCX0gYmlu
+ZCBkZWYKCS9wcm9jMSBGTUxPQ0FMCgkvcHJvYzIgRk1MT0NBTAoJL25ld3By
+b2MgRk1MT0NBTAovRm1jYyB7CiAgICAvcHJvYzIgZXhjaCBjdmxpdCBkZWYK
+ICAgIC9wcm9jMSBleGNoIGN2bGl0IGRlZgogICAgL25ld3Byb2MgcHJvYzEg
+bGVuZ3RoIHByb2MyIGxlbmd0aCBhZGQgYXJyYXkgZGVmCiAgICBuZXdwcm9j
+IDAgcHJvYzEgcHV0aW50ZXJ2YWwKICAgIG5ld3Byb2MgcHJvYzEgbGVuZ3Ro
+IHByb2MyIHB1dGludGVydmFsCiAgICBuZXdwcm9jIGN2eAp9IGJpbmQgZGVm
+Ci9uZ3JheXQgMjU2IGFycmF5IGRlZgovbnJlZHQgMjU2IGFycmF5IGRlZgov
+bmJsdWV0IDI1NiBhcnJheSBkZWYKL25ncmVlbnQgMjU2IGFycmF5IGRlZgoJ
+L2dyeXQgRk1MT0NBTAoJL2JsdXQgRk1MT0NBTAoJL2dybnQgRk1MT0NBTAoJ
+L3JlZHQgRk1MT0NBTAoJL2luZHggRk1MT0NBTAoJL2N5bnUgRk1MT0NBTAoJ
+L21hZ3UgRk1MT0NBTAoJL3llbHUgRk1MT0NBTAoJL2sgRk1MT0NBTAoJL3Ug
+Rk1MT0NBTAovY29sb3JzZXR1cCB7CgljdXJyZW50Y29sb3J0cmFuc2ZlcgoJ
+L2dyeXQgZXhjaCBkZWYKCS9ibHV0IGV4Y2ggZGVmCgkvZ3JudCBleGNoIGRl
+ZgoJL3JlZHQgZXhjaCBkZWYKCTAgMSAyNTUgewoJCS9pbmR4IGV4Y2ggZGVm
+CgkJL2N5bnUgMSByZWQgaW5keCBnZXQgMjU1IGRpdiBzdWIgZGVmCgkJL21h
+Z3UgMSBncmVlbiBpbmR4IGdldCAyNTUgZGl2IHN1YiBkZWYKCQkveWVsdSAx
+IGJsdWUgaW5keCBnZXQgMjU1IGRpdiBzdWIgZGVmCgkJL2sgY3ludSBtYWd1
+IG1pbiB5ZWx1IG1pbiBkZWYKCQkvdSBrIGN1cnJlbnR1bmRlcmNvbG9ycmVt
+b3ZhbCBleGVjIGRlZgoJCW5yZWR0IGluZHggMSAwIGN5bnUgdSBzdWIgbWF4
+IHN1YiByZWR0IGV4ZWMgcHV0CgkJbmdyZWVudCBpbmR4IDEgMCBtYWd1IHUg
+c3ViIG1heCBzdWIgZ3JudCBleGVjIHB1dAoJCW5ibHVldCBpbmR4IDEgMCB5
+ZWx1IHUgc3ViIG1heCBzdWIgYmx1dCBleGVjIHB1dAoJCW5ncmF5dCBpbmR4
+IDEgayBjdXJyZW50YmxhY2tnZW5lcmF0aW9uIGV4ZWMgc3ViIGdyeXQgZXhl
+YyBwdXQKCX0gZm9yCgl7MjU1IG11bCBjdmkgbnJlZHQgZXhjaCBnZXR9Cgl7
+MjU1IG11bCBjdmkgbmdyZWVudCBleGNoIGdldH0KCXsyNTUgbXVsIGN2aSBu
+Ymx1ZXQgZXhjaCBnZXR9Cgl7MjU1IG11bCBjdmkgbmdyYXl0IGV4Y2ggZ2V0
+fQoJc2V0Y29sb3J0cmFuc2ZlcgoJe3BvcCAwfSBzZXR1bmRlcmNvbG9ycmVt
+b3ZhbAoJe30gc2V0YmxhY2tnZW5lcmF0aW9uCgl9IGJpbmQgZGVmCgkvdHJh
+biBGTUxPQ0FMCi9mYWtlY29sb3JzZXR1cCB7CgkvdHJhbiAyNTYgc3RyaW5n
+IGRlZgoJMCAxIDI1NSB7L2luZHggZXhjaCBkZWYgCgkJdHJhbiBpbmR4CgkJ
+cmVkIGluZHggZ2V0IDc3IG11bAoJCWdyZWVuIGluZHggZ2V0IDE1MSBtdWwK
+CQlibHVlIGluZHggZ2V0IDI4IG11bAoJCWFkZCBhZGQgMjU2IGlkaXYgcHV0
+fSBmb3IKCWN1cnJlbnR0cmFuc2ZlcgoJezI1NSBtdWwgY3ZpIHRyYW4gZXhj
+aCBnZXQgMjU1LjAgZGl2fQoJZXhjaCBGbWNjIHNldHRyYW5zZmVyCn0gYmlu
+ZCBkZWYKL0JJVE1BUENPTE9SIHsgCgkvZCA4IGRlZgoJZ3NhdmUKCXRyYW5z
+bGF0ZSByb3RhdGUgc2NhbGUgL2ggZXhjaCBkZWYgL3cgZXhjaCBkZWYKCS9i
+aXRtYXBzYXZlIHNhdmUgZGVmIAoJY29sb3JzZXR1cAoJL2lzIHcgZCB3Ynl0
+ZXMgc3RyaW5nIGRlZgoJL2NmIGN1cnJlbnRmaWxlIGRlZiAKCXcgaCBkIFt3
+IDAgMCBoIG5lZyAwIGhdIAoJe2NmIGlzIHJlYWRoZXhzdHJpbmcgcG9wfSB7
+aXN9IHtpc30gdHJ1ZSAzIGNvbG9yaW1hZ2UgCgliaXRtYXBzYXZlIHJlc3Rv
+cmUgCglncmVzdG9yZQoJfSBiaW5kIGRlZgovQklUTUFQQ09MT1JjIHsgCgkv
+ZCA4IGRlZgoJZ3NhdmUKCXRyYW5zbGF0ZSByb3RhdGUgc2NhbGUgL2ggZXhj
+aCBkZWYgL3cgZXhjaCBkZWYKCS9sYiB3IGQgd2J5dGVzIGRlZiAKCXNsIGxi
+IGx0IHtsYiBtc30gaWYgCgkvYml0bWFwc2F2ZSBzYXZlIGRlZiAKCWNvbG9y
+c2V0dXAKCS9pcyBpbSAwIGxiIGdldGludGVydmFsIGRlZiAKCXdzIDAgbGIg
+Z2V0aW50ZXJ2YWwgaXMgY29weSBwb3AgCgkvY2YgY3VycmVudGZpbGUgZGVm
+IAoJdyBoIGQgW3cgMCAwIGggbmVnIDAgaF0gCgl7aXB9IHtpc30ge2lzfSB0
+cnVlIDMgY29sb3JpbWFnZQoJYml0bWFwc2F2ZSByZXN0b3JlIAoJZ3Jlc3Rv
+cmUKCX0gYmluZCBkZWYKL0JJVE1BUEdSQVkgeyAKCTgge2Zha2Vjb2xvcnNl
+dHVwfSBDT01NT05CSVRNQVAKCX0gYmluZCBkZWYKL0JJVE1BUEdSQVljIHsg
+Cgk4IHtmYWtlY29sb3JzZXR1cH0gQ09NTU9OQklUTUFQYwoJfSBiaW5kIGRl
+ZgovRU5EQklUTUFQIHsKCX0gYmluZCBkZWYKZW5kIAolJUVuZFByb2xvZwol
+JUJlZ2luU2V0dXAKKDIuMCkgRk1WRVJTSU9OCjEgMSA2MTIgNzkyIDAgMSAx
+IEZNRE9DVU1FTlQKL2ZpbGxwcm9jcyAzMiBhcnJheSBkZWYKZmlsbHByb2Nz
+IDAgeyAwLjAwMDAwMCBncmF5bmVzcyB9IHB1dApmaWxscHJvY3MgMSB7IDAu
+MTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyIHsgMC4zMDAwMDAg
+Z3JheW5lc3MgfSBwdXQKZmlsbHByb2NzIDMgeyAwLjUwMDAwMCBncmF5bmVz
+cyB9IHB1dApmaWxscHJvY3MgNCB7IDAuNzAwMDAwIGdyYXluZXNzIH0gcHV0
+CmZpbGxwcm9jcyA1IHsgMC45MDAwMDAgZ3JheW5lc3MgfSBwdXQKZmlsbHBy
+b2NzIDYgeyAwLjk3MDAwMCBncmF5bmVzcyB9IHB1dApmaWxscHJvY3MgNyB7
+IDEuMDAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyA4IHs8MGYxZTNj
+NzhmMGUxYzM4Nz4gOCAxIHNldHBhdHRlcm4gfSBwdXQKZmlsbHByb2NzIDkg
+ezwwZjg3YzNlMWYwNzgzYzFlPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxs
+cHJvY3MgMTAgezxjY2NjY2NjY2NjY2NjY2NjPiA4IDEgc2V0cGF0dGVybiB9
+IHB1dApmaWxscHJvY3MgMTEgezxmZmZmMDAwMGZmZmYwMDAwPiA4IDEgc2V0
+cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTIgezw4MTQyMjQxODE4MjQ0Mjgx
+PiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTMgezwwMzA2MGMx
+ODMwNjBjMDgxPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxscHJvY3MgMTQg
+ezw4MDQwMjAxMDA4MDQwMjAxPiA4IDEgc2V0cGF0dGVybiB9IHB1dApmaWxs
+cHJvY3MgMTUge30gcHV0CmZpbGxwcm9jcyAxNiB7IDEuMDAwMDAwIGdyYXlu
+ZXNzIH0gcHV0CmZpbGxwcm9jcyAxNyB7IDAuOTAwMDAwIGdyYXluZXNzIH0g
+cHV0CmZpbGxwcm9jcyAxOCB7IDAuNzAwMDAwIGdyYXluZXNzIH0gcHV0CmZp
+bGxwcm9jcyAxOSB7IDAuNTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9j
+cyAyMCB7IDAuMzAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMSB7
+IDAuMTAwMDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMiB7IDAuMDMw
+MDAwIGdyYXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyMyB7IDAuMDAwMDAwIGdy
+YXluZXNzIH0gcHV0CmZpbGxwcm9jcyAyNCB7PGYwZTFjMzg3MGYxZTNjNzg+
+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyNSB7PGYwNzgzYzFl
+MGY4N2MzZTE+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyNiB7
+PDMzMzMzMzMzMzMzMzMzMzM+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxw
+cm9jcyAyNyB7PDAwMDBmZmZmMDAwMGZmZmY+IDggMSBzZXRwYXR0ZXJuIH0g
+cHV0CmZpbGxwcm9jcyAyOCB7PDdlYmRkYmU3ZTdkYmJkN2U+IDggMSBzZXRw
+YXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAyOSB7PGZjZjlmM2U3Y2Y5ZjNmN2U+
+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAzMCB7PDdmYmZkZmVm
+ZjdmYmZkZmU+IDggMSBzZXRwYXR0ZXJuIH0gcHV0CmZpbGxwcm9jcyAzMSB7
+fSBwdXQKJSVFbmRTZXR1cAowIDYgL0hlbHZldGljYSBGTURFRklORUZPTlQK
+JSVQYWdlOiAiMSIgMQolJUJlZ2luUGFwZXJTaXplOiBMZXR0ZXIKJSVFbmRQ
+YXBlclNpemUKNjEyIDc5MiAwIEZNQkVHSU5QQUdFCjcyIDc0NiA1NDAgNzU2
+IFIKNyBYCjAgSwpWCjcyIDMyLjY3IDU0MCA0Mi42NyBSClYKNSBYCjExNSAx
+ODAgNi4xNSA2LjE1IDMyMi42OSA0ODIuOTEgRwowLjUgSAoyIFoKMCBYCjEx
+NSAxODAgNi4xNSA2LjE1IDMyMi42OSA0ODIuOTEgQQozMjAuNDkgNDg5Ljc3
+IDMyMC40OSA0ODIuOTQgMzIzLjE1IDQ4Mi4wNSAzMjcuOTYgNDg2Ljg2IDQg
+TAo3IFgKVgo0NjUuMTQgMTA1LjkxIE0KIDQ2My42MSA4NS45IDQ3MC42IDY2
+Ljk4IDQ3Ni42OCA0OS41NiBECjAgWApOCjQzOCA5My4zNiBNCiA0MzguODEg
+NzguMzcgNDQyLjg2IDY3Ljg0IDQ0Ny43MiA1OS43NCBECk4KNDI0LjIzIDk3
+LjgxIE0KIDQyMy40MiA4MS42MSA0MjUuODcgNjkuMTggNDMwLjczIDYxLjA4
+IEQKTgozOTYuNjkgMTAzLjA4IE0KIDM5NS44OCA4Ni44OCAzOTggODAuNTIg
+NDAxLjI0IDcxLjIxIEQKTgozOTMuNTIgMTE1LjUxIE0KIDQwMi45NiAxMjQu
+MSA0MzMuMTQgMTY4LjQxIDQ0NS4yOSAxNzguMjggRAogNDUxLjI0IDE1My44
+NiA0NDguNzEgMTQ5LjU1IDQ0Ni45MyAxMzguNjcgRAogNDMxLjc1IDEzMC40
+NCA0MjkuNzIgMTIzLjg2IDQ0Mi4yNSAxMjIuNDcgRAogNDQxLjQ5IDExOS4x
+OCA0NDEuMjQgMTE4LjY3IDQ0MC45OSAxMTYuNjUgRAogNDIyLjYzIDExMy45
+OSAzOTUuODggMTE0LjE1IDM5NC40MSAxMTMuODYgRAo0IFgKVgo0NzUuNzkg
+MTU4LjI5IDQ5OS4yMSAxMjAuODIgNDY2LjE3IDExNy4wMyA0NjkuMjEgMTM1
+LjI1IDQgTApWCjMxMC40MiA1NzguMTcgTQogMzA3LjQ4IDU4Ni42NiAzMDcu
+MTggNjAxLjI1IDMxNy43MSA2MDQuOSBECiAzMTQuODcgNjE2LjY0IDMxOC45
+MiA2MTkuMDcgMzIyLjk3IDYyMi43MiBECiAzMjAuMTQgNjMzLjY1IDMyNC4x
+OSA2NDEuNzYgMzMxLjg4IDY0MC41NCBECiAzMjcuMDIgNjU2LjM0IDMzNC43
+MiA2NjMuNjMgMzQxLjYgNjY0Ljg0IEQKIDMzNy41NSA2ODEuODUgMzQyLjQx
+IDY4NC42OSAzNDguNzYgNjg5Ljg3IEQKIDM0Ni4wNiA3MDguNTggMzU3Ljgg
+NzEyLjYzIDM2NS4xIDcxMC4yIEQKIDM2NS41IDcyNy42MiAzNzEuOTggNzI5
+LjY1IDM4MS4zIDczMi40OCBECiAzODAuODkgNzQ2LjY2IDM4OS40IDc0Ni4y
+NSAzOTUuODggNzQ2LjY2IEQKIDM5OS4xMiA3NjQuODggNDEyLjA4IDc2My42
+NyA0MTguNTYgNzYyLjQ1IEQKIDQzMy45NSA3NzQuMiA0NDMuNjcgNzc0LjIg
+NDUwLjUxIDc2Ny41OCBECiA0NzUuNjcgNzczLjM5IDQ3NC4wNSA3NjQuODgg
+NDc2LjE1IDc1OC40MyBECiA0NzYuOTQgNzU3LjkzIDQ3Ny43MiA3NTcuNCA0
+NzguNSA3NTYuODYgRAogNDk2LjMyIDc1OCA0OTEuMDYgNzUxLjExIDQ5MS41
+MSA3NDEuNjUgRAogNTAxLjU5IDczMi40OCA0OTkuOTcgNzI0LjM4IDQ5My44
+OSA3MTcuOSBECiA0OTcuOTQgNzA4Ljk5IDQ5MS44NyA3MDEuNyA0ODcuNDEg
+NzAwLjA4IEQKIDQ4Ny40MSA2ODcuOTMgNDg2LjIgNjg2LjMxIDQ3My4yMyA2
+ODYuMzEgRAogNDczLjY0IDY3Ni41OSA0NzUuNjcgNjcwLjUxIDQ1OS4wNiA2
+NjguODkgRAogNDYxLjg5IDY1Ni4zNCA0NTAuNTUgNjUzLjEgNDQ2LjEgNjUx
+Ljg4IEQKIDQ0OS4zNCA2MzQuMDYgNDQwLjQzIDYzMi4wNCA0MzUuNTcgNjMw
+LjgyIEQKIDQ0MC40MyA2MTcuMDUgNDM0Ljc2IDYxMy40IDQyOC4yOCA2MTEu
+NzggRAogNDMzLjE0IDYwMS42NiA0MjcuMDYgNTk0Ljc3IDQyMC4xOCA1OTEu
+NTMgRAogNDIyLjYxIDU3OC45OCA0MTYuNTMgNTczLjcxIDQxMS4yNyA1Njgu
+ODUgRAogNDE4LjE1IDU1My4wNiA0MDguODQgNTQ1Ljc2IDQwMi44NSA1NDEu
+ODcgRAogNDAzLjE3IDUyOC4zNSAzOTUuNDcgNTIyLjY4IDM4OS44IDUxOS44
+NCBECiAzODcuNzggNTA0LjQ1IDM3OC40NiA1MDQuODYgMzcxLjE3IDUwNC4w
+NSBECjMgWApWCjAgWApOCjIzMi42NSA1NjYuNDIgMjU2LjY3IDU3NC4zOSAy
+MzguMSA1NTMuODIgMyBMCjUgWApWCjIxOS4yMSAxMTcuODcgMjE5LjI5IDEz
+NC4yNiAyMjEuNDMgMTM1LjI3IDIyNS44NiAxMzMuMjMgMjI1LjE4IDExOS41
+NiA1IEwKNCBYClYKMTcxLjkgNzcuMTYgMTc4LjMyIDcyLjMyIDIwMC42NiA1
+MC40MiAyMDEuNTkgNDYuNDYgMjAzLjkgNDYuMzcgMjE4LjUyIDY0LjA0IDIx
+Ny4yNyA3My4wNQogMjEwLjYzIDc2Ljc1IDggTAowIFgKVgoxMzcuOTggODEu
+MjEgMTMyLjkyIDcwLjU3IDEzMS4yNyA2NC4zNyAxNDQuMDUgNTcuOTIgMTQ5
+Ljg4IDcxLjcxIDE1My4yNyA3Ni4zNSAxNTMuMjcgODQuMTIgNyBMClYKMTUz
+LjI3IDEwMi42NyBNCiAxMzIuOTIgMTIwLjE5IDEzMi45MiA5MS44NCAxMzYu
+NDYgODEuNzEgRAogMTQyLjAzIDc4LjE3IDE0OC41OCA3OC43NiAxNTIuNjYg
+NzkuMTggRAo0IFgKVgowIFgKTgoyNzUuNjkgMTAwLjQ0IE0KIDI5MS40MSA5
+Ni41OSAzMjguODUgNzEuMDggMzIwLjI0IDYyLjk4IEQKIDMwOC41OSA1NS44
+OSAyOTEuMDUgNjEuMTQgMjc4LjIyIDU5Ljk0IEQKVgpOCjMwNi4zMiAzMDIu
+NDUgMjIyLjkzIDIyNy4wMSAyMDkuMTYgMjAzLjkzIDE2OS4wNiAyMTMuNjUg
+MTcwLjY4IDIzMS4wNiAxODQuODYgMjUxLjcyIDIyNi41OCAzMDUuNTkgNyBM
+CjQgWApWCjE3Mi43MSA5OC42MiAxNzIuNzEgNzYuNzUgMjE1LjY0IDc2Ljc1
+IDIxNS42NCA5OC4yMiA0IEwKVgowIFgKTgoxOTMuNzcgMTQxLjE1IDIxOS4y
+OSAxMzQuMjYgMjE4Ljg4IDk4LjYyIDE3My4xMiA5OC42MiAxNzMuMTIgMTMx
+LjQzIDUgTAo0IFgKVgowIFgKTgoyODEuMjYgMTYzLjAyIDI4MS4yNiA5Ny40
+MSAyNTguNTcgMTIzLjMzIDI1OC45OCAxNjMuNDIgNCBMCjQgWApWCjAgWApO
+CjI4MS42NiAyNTkuMDEgMjgxLjY2IDE2Ni42NiAyMjQuMTUgMjMxLjA2IDMg
+TAo0IFgKVgowIFgKTgoxNzcuMTYgMjI0LjE4IE0KIDE4My4xNSAyMTkuMzYg
+MTg4LjkxIDIwMy41MiAxOTAuMTMgMTk1LjgzIEQKIDE4Ni44OCAxOTIuNTkg
+MTc3LjI4IDE4OS4wNyAxNzIuMyAxODYuNTEgRAo2IFgKVgowIFgKTgoxNDku
+NjIgNTAuOTYgTQogMTQzLjA0IDM4LjkzIDE2MC4zOSA0MC42MiAxNzUuOTUg
+NDAuMyBECiAxOTEuMDMgMzkuOTkgMjA3LjM0IDM4LjQyIDIwMC41IDUwLjQ1
+IEQKMyBYClYKMCBYCk4KODkuMjggMTQ4LjQ0IE0KIDg3LjUzIDEzOS4yNyA5
+My4zMiAxMzkuOTMgOTQuMTQgMTMzLjg2IEQKIDkzLjYyIDExOC4yNiA5My42
+NyAxMTEuMjIgOTUuMzUgMTAzLjQ4IEQKIDExNi40MSA5Ny44MSAxMjguNTYg
+MTAxLjQ2IDE1NC40OCAxMDIuNjcgRAogMTU1LjcgMTAzLjg5IDE1NC44OSAx
+MDUuMSAxNTUuMjkgMTA3LjEzIEQKIDE2Mi4xOCAxMDUuOTEgMTc2LjQyIDEw
+OC4wOCAxODUuNjcgMTA3LjUzIEQKIDE4Ni44OCAxMTUuMjMgMTg1LjI2IDEx
+OC44NyAxODYuODggMTI4LjU5IEQKIDE5NS4zOSAxMzUuNDggMTk2LjYxIDE0
+MC43NCAxOTUuOCAxNDUuMiBECjMgWApWCjAgWApOCjIzOC43MyA2MjUuOTYg
+TQogMjM0LjU2IDYzNC4yOSAyMzAuMTIgNjM3LjYgMjIxLjUxIDYzOS42MyBE
+CiAyMDEuNzcgNjY4LjQ5IDE2Mi44NyA2NzQuMTYgMTM4LjQyIDY0Ny43MyBE
+CiAxMTQuOTIgNjIyLjMyIDEyMS4yNyA1ODYuNDcgMTUwLjEzIDU1OC42MyBE
+CiAxNjMuMTggNTQ2LjEgMTgyLjAyIDUyMS42NyAxNzcuNDcgNDkwLjc4IEQK
+TgoxNzQuOTQgNTYyLjY4IDE1My4xNyA1ODEuOTEgMTU3LjcyIDU2MC4xNCAx
+NDAuNTEgNTY3Ljc0IDE0OS42MiA1NDIuNDIgNSBMCk4KMTc0LjQzIDU1Mi4w
+NCBNCiAxNjAuNjggNTY5LjUxIDEzNC45NCA1MzAuNzggMTY5LjM3IDUzNC4z
+MiBECjcgWApWCjAgWApOCjE1OS4yNCA1MzkuODkgTQogMTYwLjc2IDU1My4w
+NiAxNzAuMzggNTQ3LjQ5IDE3My40MiA1NDYuOTggRApOCjE3MC4zOCA1MzYu
+MzUgTQogMTY0LjMgNTM4Ljg4IDE2NS4zMiA1NDYuNDcgMTcyLjkxIDU0Ni40
+NyBECk4KNyBYCjkwIDQ1MCAxOC40OCAxOC40OCAyMzcuNDYgNjA5LjUxIEcK
+MCBYCjkwIDQ1MCAxOC40OCAxOC40OCAyMzcuNDYgNjA5LjUxIEEKNyBYCjkw
+IDQ1MCAyMy4yOSAyMi4yOCAyMDkuMzYgNTk0LjA2IEcKMCBYCjkwIDQ1MCAy
+My4yOSAyMi4yOCAyMDkuMzYgNTk0LjA2IEEKOTAgNDUwIDIuNzggMi43OCAy
+MjEuNzcgNTgzLjY5IEcKOTAgNDUwIDIuNzggMi43OCAyMjEuNzcgNTgzLjY5
+IEEKOTAgNDUwIDIuNzggMi43OCAyNDYuNTggNTk5Ljg5IEcKOTAgNDUwIDIu
+NzggMi43OCAyNDYuNTggNTk5Ljg5IEEKMjMyLjY1IDU2Ni40MiBNCiAyMDku
+NTcgNTU1LjA4IDE3OC43OSA1MjkuOTcgMjAyLjI4IDUwMi45MyBECiAyMjIu
+NjEgNDc5LjYgMjQ5LjM2IDQ4My4xOSAyNTAuODggNTE3LjExIEQKIDI1NC45
+MyA1MjIuMTcgMjYwLjUgNTE4LjEyIDI1OS45OSA1MzIuMyBECjUgWApWCjAg
+WApOCjI1NC45MyA1NzQuMzIgTQogMjczLjE1IDU3NS44NCAzMDMuNjMgNTUy
+Ljk4IDI3My42NiA1MzcuODcgRAogMjUzLjQxIDUyNy43NCAyMjUuMDYgNTIz
+LjY5IDIwMy43OSA1MjUuNzIgRAo1IFgKVgowIFgKTgoyMDcuMzQgNTI5LjI2
+IDIwMC43NiA1MjEuMTYgMiBMCk4KMTY3LjM0IDY2My45MyBNCiAxNTYuOTQg
+Njc5IDEyNS4zMiA2NjIuOTIgMTQyLjAzIDYzOC42MiBECk4KMTQ5LjYyIDY1
+Ni44NCBNCiAxMzAuODkgNjYzLjkzIDExNS4yIDY0NS43IDEyNy44NSA2MjYu
+OTcgRApOCjE3Ny45OCA0OTguODggTQogMTUwLjY0IDQ5Ny4zNiAyMDkuMzYg
+NDc0LjU4IDIzMC4xMiA0NjguNTEgRApOCjE3MC45OSA0OTYuNzYgMTUyLjE1
+IDQ2OC41MSAyIEwKTgoxNTIuMTUgNDY4LjUxIE0KIDE3My40MiA0NDguNzYg
+MTk3LjcyIDQ0MC4xNiAyMjQuMDUgNDM3LjYzIEQKTgoyMzAuMTIgNDY4LjUx
+IDIyNC4wNSA0MzcuNjMgMiBMCk4KMjQxLjIgNDkyLjM3IDI1MC44OCA0ODgu
+MjUgMjYxIDQ4MS42NyAzIEwKTgoyNDEuMiA0OTIuMzcgTQogMjQ5Ljg3IDQ3
+My4wNiAyNzQuNjcgNDI4LjAxIDI3Ny4yIDM5OC4xNCBECiAzMDMuNTMgMzc5
+LjQgMzE0Ljc5IDM1MC41MiAzMTQuMTYgMzEwLjU1IEQKIDMxMi42NCAyOTMu
+ODQgMjM4LjQyIDI5MC43IDIxNS42NCAyOTAuMiBECjcgWApWCjAgWApOCjE1
+Ni4yMSA0NjQuOTYgMTQ1LjA3IDQ1MS4yOSAyIEwKTgoxMTUuMiAzODYuNDkg
+TQogMTA0LjQ3IDM5My4wMyA5Ny45OCAzOTcuNjMgOTUuNDUgNDA1LjczIEQK
+IDk2LjQ2IDQyNS45OCAxMzguNDkgNDY1LjQ3IDE2OC4zNiA0NDcuNzUgRAog
+MTg0LjA0IDQzOC4zNSAxNzcuNzIgMzk3LjY1IDE2OC44NiAzODUuNDggRApO
+CjEwNi4wOCAzMTEuMDUgNzYuMjEgMzUxLjU2IDg3Ljg2IDM1MC4wNCA4Ny44
+NiAzNjIuMTkgOTguNDkgMzU2LjYyIDk3LjQ4IDM3Mi4zMiAxMDcuMSAzNjMu
+NzEKIDEwNS4wNyAzODEuOTMgMTE3LjczIDM3MS4zIDExNi4yMSAzODkuMDIg
+MTI4Ljg3IDM3OS40IDEyNy44NSAzOTUuNiAxMzguOTkgMzg3IDEzOC40OSA0
+MDEuNjggMTQ5LjYyIDM5Mi41NwogMTUwLjY0IDQwNS4yMiAxNTYuMjEgMzk2
+LjExIDE2MC4yNiA0MDUuMjIgMTggTApOCjk4LjQ5IDM5OC4xNCBNCiA2NC41
+NyAzNjAuNjcgNjQuOTIgMjkzLjQzIDkzLjkzIDI1NC4zNSBECk4KMTA2LjA4
+IDMxMS4wNSBNCiAxMTMuNjggMzAyLjk1IDExMy42OCAyOTcuODkgMTIwLjc3
+IDI4Ny43NyBECk4KMTIwLjc3IDI4Ny43NyBNCiAxNTAuMTMgMjk1LjM2IDE2
+My44IDI2MS40NCAxMjQuODIgMjY3LjAxIEQKTgozMDYuMzIgMzAyLjQ1IE0K
+IDMwMS43NyAyNjYgMjYyLjUyIDIzOC42NiAyMjYuNTggMjMxLjA2IEQKNCBY
+ClYKMCBYCk4KMTYwLjI2IDQwNS4yMiBNCiAxNjguMTIgMzg3Ljk2IDE3MS45
+IDM2Ny41NiAxODUuMDcgMzUwLjEgRAogMTkyLjk2IDM1MC41NSAxOTIuOTYg
+MzQ5LjMzIDE5NC4xOCAzNDQuOTggRApOCjkwIDQ1MCAxNC41OCAxNy44MiAx
+OTEuNzUgMzI2LjY1IEEKOTAgNDUwIDIuNjMgMi42MyAxODkuOTIgMzI5LjI4
+IEcKOTAgNDUwIDIuNjMgMi42MyAxODkuOTIgMzI5LjI4IEEKNyBYCjkwIDQ1
+MCAyMC40NSAyMS4wNiAxNjcuMjQgMzEzLjI4IEcKMCBYCjkwIDQ1MCAyMC40
+NSAyMS4wNiAxNjcuMjQgMzEzLjI4IEEKOTAgNDUwIDIuNjMgMi42MyAxNTcu
+NTIgMzEzLjA4IEcKOTAgNDUwIDIuNjMgMi42MyAxNTcuNTIgMzEzLjA4IEEK
+MTg3LjcgMzA5LjY0IE0KIDIyMC45MSAzMjguNjcgMjIwLjEgMjk5LjUxIDE5
+OS44NSAyOTYuNjggRAo3IFgKVgowIFgKTgoyMDguNzYgMzAwLjMyIE0KIDIx
+NS42NCAyOTAuMiAyMTQuNDMgMjkyLjIyIDIyNS4zNiAyODAuMDcgRAogMjEz
+LjYyIDI1OC42IDE3OC4zOCAyNjMuMDYgMTYyLjU4IDI2NC42OCBECjcgWApW
+CjAgWApOCjE1OC4xMyAyNjMuMDYgMTY0LjIgMjY3LjUyIDIgTApOCjE5Mi45
+NiAyNjMuODcgTQogMTkyLjU2IDI1OS4wMSAxOTAuMTMgMjU2LjU4IDE4MS4y
+MSAyNTYuMTcgRAogMTgwIDI1MC45MSAxNzYuNjYgMjM3Ljk4IDE3Mi41NCAy
+MzMuMjQgRAo0IFgKVgowIFgKTgoxNTIuODYgMjM4LjM1IE0KIDE2My44IDIz
+MS4wNiAxNzEuMDkgMjM1LjExIDE3NS45NSAyMzQuMyBECiAxNzcuNTcgMjE4
+LjkxIDE3NS41NCAyMTAuODEgMTc2LjM1IDE5OS44OCBECiAxOTcuNTUgMTcx
+LjEgMjM2LjI5IDEzNS45NCAxNDIuMzMgMTM3LjEgRAogMTA5LjUzIDEzNy41
+IDU2LjQ3IDE0MS4xNSAxMDQuMjYgMTkxLjM3IEQKNiBYClYKMCBYCk4KMTU0
+Ljg5IDExNC40MiAxNTUuMjkgMTA2LjMyIDIgTApOCjEwNC4yNiAxOTUuMDIg
+TQogMTA2LjU2IDE3NC4wOSAxMDIuMjQgMTM5LjEyIDEwNi4yOSAxMjMuNzMg
+RAogMTEwLjA5IDExOS40OSAxMTQuMjkgMTE4LjkxIDEyNC4xMSAxMTkuNjgg
+RAogMTMyLjIxIDEwNS4xIDEzOC42OSAxMDcuOTQgMTI3LjQ3IDEzNC44NyBE
+CiAxMjcuNDcgMTQ5LjA1IDEyNi45NCAxNzIuNzQgMTMyLjYxIDE4OS43NSBE
+CjcgWApWCjAgWApOCjEyNC4xMSAxMTkuNjggTQogMTEyLjM2IDg1LjY2IDEw
+NS44OCAxMDYuNzIgMTExLjU1IDEwOS4xNSBECjcgWApWCjAgWApOCjEwOS4z
+NyAxMjYuNzcgMTA0LjU3IDEyMy4xIDEwMC44OSAxMTQuMTEgMTE3LjIyIDEx
+MC4xOSAxMjIuMDMgMTE2LjUyIDEyNS41NyAxMjMuOTkgNiBZCjcgWApWCjEw
+OC43MiAxMTguNDcgTQogOTEuMyAxMDUuOTEgMTE0Ljc5IDEwMy4wOCAxMTYu
+NDEgMTE2LjQ0IEQKVgowIFgKTgoxMDYuMjkgMTI0Ljk1IE0KIDk3Ljc4IDEy
+MS4zIDk0Ljk0IDEwOS4xNSAxMDMuNDUgMTE0LjAxIEQKNyBYClYKMCBYCk4K
+MTA3LjAzIDIyMS42MSBNCiAxMDAuNjIgMjExLjIyIDk5IDIwMy4xMiA5OS40
+IDE5OC4yNiBECiAxMDcuMjEgMTkxLjUyIDEyOS4zNyAxODguOTQgMTQwLjcx
+IDE5MC4xNiBECiAxNDUuMTcgMTk4LjY2IDEzOC4wOCAyMTIuNjkgMTQxLjAy
+IDIyMC40MyBECjYgWApWCjAgWApOCjExOS4yNSAyNDcuNzcgTQogMTI5LjYg
+MjQwLjcyIDEzNy45IDIzNy40NSAxNDQuMDQgMjM2LjcgRAogMTYwLjkxIDIz
+NC42MyAxNjIuMjggMjUyLjgzIDE0MC41MSAyNTguNCBECiAxMjguMjMgMjYx
+LjYxIDEzMC44OSAyNjIuOTYgMTI0LjgyIDI2Ny4wMSBECjcgWApWCjAgWApO
+CjkzLjkzIDI1NC4zNSBNCiA4OS44OCAyMTkuOTMgMTM5LjUgMTk1LjYyIDE0
+MS4wMiAyMjAuNDMgRAo3IFgKVgowIFgKTgoxMTUuNyAyMzAuNTYgTQogMTI4
+LjM2IDIxMy4zNCAxNjcuMzQgMjE5LjQyIDE0Ny42IDIzNi4xMyBECjcgWApW
+CjAgWApOCjE3Mi43MSAxMDcuMjggMTcyLjcxIDc2Ljc1IDIgTApOCjE1My4y
+NyAxMDIuNjcgMTUzLjI3IDc2LjM1IDIgTApOCjE1My4yNyA3Ny4xNiBNCiAx
+NjIuOTkgNjguMjQgMTcyLjcxIDczLjEgMTcxLjkgNzcuMTYgRAogMTk4LjIz
+IDY3LjAzIDEzNC4yMyA2NC42IDE1My4yNyA3Ny4xNiBECjcgWApWCjAgWApO
+CjcgWAo5MCA0NTAgNS44NyA1LjQ3IDE1Ny41MiA2MS41NiBHCjAgWAo5MCA0
+NTAgNS44NyA1LjQ3IDE1Ny41MiA2MS41NiBBCjE3OC4zMiA3Mi4zMiBNCiAx
+ODkuMjYgNjguNjcgMjAwLjI1IDYwLjk2IDIwMC42NiA1MC40MiBECjcgWApW
+CjAgWApOCjIwMC42NiA1MC40MiAxNTIuMDUgNTAuNDIgMiBMCk4KMTMwLjE4
+IDcyLjcgMTMwLjE4IDEwMC42NSAyIEwKTgoxMDguMzEgOTQuMTcgMTA4LjMx
+IDcyLjI5IDIgTApOCjEwOC4yNCA5NC41IE0KIDEwNS45NiA5Ni42NSAxMDQu
+NTcgOTcuNjYgMTA0LjY5IDEwMS4zMyBECk4KMTI4LjQ5IDQyLjg1IE0KIDEz
+MC40NiA0Mi4xNSAxMzAuMiA2NS45MSAxMzEuOSA2NS4xMyBECiAxNDMuOCA1
+OS42OCAxNTMuMjIgNDkuODcgMTUyLjg2IDQzLjEzIEQKNyBYClYKMCBYCk4K
+MTQ5Ljg4IDcxLjcxIE0KIDE0Ni43MSA2Ny4wMyAxNDUuNyA2Ni42NSAxNDQu
+MDUgNTcuOTIgRAo3IFgKVgowIFgKTgoxMDIuNjQgNDUuMTYgTQogOTcuMzgg
+MzMuODIgMTEzLjQgMzMuNzQgMTI4Ljk3IDMzLjQxIEQKIDE0NC4wNSAzMy4x
+IDE1OS43NSAzMS43OSAxNTIuNDYgNDIuMzIgRAozIFgKVgowIFgKTgoxMDQu
+MjYgNjYuNjMgTQogMTAxLjgyIDU1LjI4IDEwMi4yNCA0OC40IDEwMy4wNSA0
+NC43NSBECiAxMjAuODcgNDAuMyAxNDAuNzYgNDIuMSAxNTIuOTEgNDIuMzUg
+RAo3IFgKVgowIFgKTgoxMDguMzEgNzIuMjkgTQogMTE3LjIyIDY4LjY1IDEy
+My4zIDcwLjY3IDEzMC4xOCA3Mi43IEQKIDE0OCA1OC41MyA4Ni4wOSA2MC4y
+IDEwNy43MyA3NS4xMyBECjcgWApWCjAgWApOCjEyMi42NiA2Mi4zNSAxMjgu
+ODcgNjIuODUgMTMwLjY0IDYzLjk5IDEzMS4yNyA2NC4zNyAxMzEuOSA2NC4y
+NSAxMzMuNDIgNjMuNjEgMTMyLjQxIDUxLjQ2CiAxMjUuOTYgNTIuMjIgOCBM
+CjcgWApWCjIyMS4zMSAxMzUuMDcgMjA5LjU3IDEyOC41OSAyMDkuNTcgMTM2
+LjI5IDE5NS4zOSAxMzAuMjEgMTk2LjIgMTQ2LjgyIDE3Ny41NyAxNTMuMyAx
+ODcuMjkgMTY4LjY5CiAxNjkuNDcgMTgxLjI1IDE5MC4xMyAxOTUuODMgMTg1
+LjY3IDIxNi4wOCAyMTEuNTkgMjEzLjI0IDIyMC41IDIzNi4zMyAyMzkuNTQg
+MjIxLjM0IDI2MS40MSAyMzIuMjggMjYyLjIyIDIxMi4wMwogMjc4LjQyIDIx
+NC40NiAyNzIuNzUgMTk1LjAyIDI4NS43MSAxODYuOTIgMjc2LjggMTc2LjM5
+IDI4Ni4xMSAxNjUuMDQgMjY4LjQ3IDE1Ny4yNyAyMSBMClYKMCBYCk4KMjEx
+LjU5IDE0NS4yIE0KIDIyMi41MyAxMzMuMDUgMjE5LjY5IDEzNC42NyAyNDAu
+MzUgMTI5LjQgRApOCjkwIDQ1MCAxNC40MiAxNC45OSAyMTkuNDUgMTcyLjc0
+IEEKOTAgNDUwIDEzLjE2IDEzLjE2IDI1My41MSAxODEuODUgQQo5MCA0NTAg
+Mi4wMyAyLjAzIDI1My43MSAxODguNTQgRwo5MCA0NTAgMi4wMyAyLjAzIDI1
+My43MSAxODguNTQgQQo5MCA0NTAgMi4wMyAyLjAzIDIyMi45MyAxODAuMDMg
+Rwo5MCA0NTAgMi4wMyAyLjAzIDIyMi45MyAxODAuMDMgQQoxOTcuODIgMTc0
+Ljc3IDIwNS4xMSAxNzQuMzYgMiBMCk4KMjA1LjUyIDE5MC41NiAyMTAuNzgg
+MTg0LjQ5IDIgTApOCjIyNS4zNiAxOTYuMjMgMjI0LjE1IDE4Ni45MiAyIEwK
+TgoyNDAuMzUgMTk4LjI2IDI0NC44IDE5Mi4xOCAyIEwKTgoyNTQuNTIgMjAx
+LjkgMjU0LjUyIDE5NC42MSAyIEwKTgoyNjguNyAxOTcuODUgMjYzLjg0IDE5
+MC4xNiAyIEwKTgoyMzkuMTMgMjIwLjUzIE0KIDI0My4xOCAyMTQuNDYgMjQy
+Ljc4IDIxMC40MSAyNDAuMzUgMjA1LjU1IEQKTgoyMTEuNTkgMjExLjYyIE0K
+IDIxMS41OSAyMDUuMTQgMjA5LjE2IDIwMy45MyAyMTcuNjcgMTk3LjA0IEQK
+TgoyMTcuNjcgMTk3LjA0IDIyNi45OCAyMDMuNTIgMjMzLjQ2IDIwNS4xNCAy
+NDAuMzUgMjA1LjU1IDQgTApOCjIzOS4xMyAyMjAuNTMgMjMxLjQ0IDIxMi40
+MyAyMjYuOTggMjEwLjgxIDIxMS41OSAyMTEuNjIgNCBMCk4KMjMxLjQ0IDIx
+Mi40MyAyMzMuNDYgMjA1LjE0IDIgTApOCjIyNi45OCAyMTAuODEgMjI2Ljk4
+IDIwMy41MiAyIEwKTgoyMDQuMyAxNTQuNTIgTQogMTg5LjcyIDE1My4zIDIw
+My45IDEzOC43MiAyMTEuNTkgMTQ1LjIgRApOCjI0MC43NSAxNjkuOTEgTQog
+MjUzLjcxIDE4MC44NCAyNTguOTggMTY0LjIzIDI0My45OSAxNjAuMTggRAo3
+IFgKVgowIFgKTgoyMDQuMTEgMTUxLjY4IE0KIDE5OC44OCAxNTEuMjQgMjAz
+Ljk3IDE0Ni4wMSAyMDYuNzMgMTQ4LjM0IEQKTgoyNjUuNDYgMTY3LjQ3IE0K
+IDI3My41NiAxNjguNjkgMjcwLjMyIDE1NS43MyAyNjQuNjUgMTU1LjMzIEQK
+TgoyNjUuNTkgMTY1LjQ1IE0KIDI2OC4zIDE2NS44NSAyNjcuMjEgMTYxLjUz
+IDI2NS4zMiAxNjEuNCBECk4KMjU2LjU1IDE1MS4yOCBNCiAyNDIuMzcgMTc5
+LjYzIDIxOC4yMiAxMTguMjkgMjUzLjc5IDEzMC4xOSBECk4KMjY0LjY1IDE1
+NS4zMyBNCiAyNjMuNDMgMTUyLjA5IDI2My40MyAxNTAuMDYgMjYwLjYgMTQ3
+LjIyIEQKNyBYClYKMCBYCk4KMjI1LjM2IDEzMy44NiAyMjUuMzYgMTIwLjkg
+MiBMCk4KMjI1LjM2IDEyMC45IE0KIDIwNi43MyAxMTIuOCAxOTcuNDIgODEu
+MjEgMjE2LjQ1IDczLjEgRAo3IFgKVgowIFgKTgoyMjMuMzQgOTIuNTUgTQog
+MjE3LjE1IDgyLjQ4IDIxNC44MyA2OS4wNSAyMjAuNSA1OS43NCBECk4KMjE4
+LjQ4IDYzLjk5IE0KIDIxMS41OSA2NC40IDIwMy4wOSA1NS4yOCAyMDMuNDkg
+NDUuOTcgRAo3IFgKVgowIFgKTgoyMDMuOSA0Ni4zNyBNCiAxNzguNzkgNDcu
+OTkgMTY3Ljg1IDI4LjE1IDIxNC44MyAyOS4zNiBECjcgWApWCjAgWApOCjIx
+NC44MyAyOS4zNiBNCiAyMjguMiAxOC4wMiAzMDcuOTkgMjIuODggMjgwLjQ1
+IDQ1LjE2IEQKTgoyNTguNTcgMTIzLjMzIDI1OC41NyAxMzEuODMgMiBMCk4K
+MjQ4LjQ1IDE1MC44NyBNCiAyNjkuMTEgMTU3LjM1IDI2OC4zIDEyMy43MyAy
+NDkuMjYgMTMxLjQzIEQKNyBYClYKMCBYCk4KMjQ4LjQ1IDE0My45OCBNCiAy
+NjQuNjUgMTQ1LjIgMjU1Ljc0IDEzNS44OCAyNDguNDUgMTM3LjUgRApOCjI2
+NC4zIDE1Ni42NCAyNjIuOSAxNTEuOTYgMjYxLjM4IDE0OC45MiAyNTkuMzYg
+MTUwLjQ0IDI1Ny43MSAxNTEuNzEgMjU2LjQ1IDE1Ny4yNyA2IEwKNyBYClYK
+MjU1LjE4IDEyMy4xIDI1OS4zNiAxMjMuMSAyNTkuMzYgMTE5Ljk0IDMgTApW
+CjE5MS4zOSAyNTguMjggMTg4LjQ4IDI1Ni44OCAxODUuNyAyNTYuMTIgMTgz
+LjU0IDI0OS4wNCAxOTIuOTEgMjQ4LjkxIDE5NC45MyAyNDkuMTYgNiBMCjQg
+WApWCjE5MS45IDI2Mi43MSAxOTEuNjQgMjYwLjkzIDE5MC42MyAyNTkuNDEg
+MTg4Ljg2IDI1OC4yOCAxODYuMDcgMjU5LjQxIDUgTAo3IFgKVgoyODEuMTMg
+ODMuMjMgMjczLjAzIDgyLjcyIDI3My40MSA3MC45NSAyODIuNjUgNjYuNzgg
+NCBMCjAgWApWCjI3NC43NyA3Mi43IE0KIDI4NC4wOSA2Mi4xNyAyODQuMDkg
+NTAuODMgMjc5LjIzIDQwLjMgRAo3IFgKVgowIFgKTgoyNTguNiAxMjMuNzMg
+TQogMjgyLjUgMTIyLjExIDI4Ni43MyA5Mi4xNCAyNzQuMzcgNzguMzcgRAo3
+IFgKVgowIFgKTgoyNjcuMDggMTA3LjEzIE0KIDI3My4xOCA5Ny4wNSAyNzYu
+OCA3Ny41NiAyNzMuOTYgNjYuNjMgRAo3IFgKVgowIFgKTgoxMzcuNiA4MC44
+MyAxMzIuOTIgNzAuNTcgMiBMCk4KMjE3LjEyIDYyLjM1IDIwNi44MyA1Mi45
+OCAyMDQuNjggNDguNTUgMjA0LjQzIDQzLjg3IDIxOC44NiA0OC41NSA1IEwK
+NyBYClYKMjMwLjEyIDQ2OC41MSAyNTEuODkgNDQ2LjIzIDI2MSA0ODEuNjcg
+MjcyLjY1IDQ0Ni4yMyAyNjAuNSA0NTEuMjkgNSBMCjAgWApOCjIyNy44NCAx
+MjAuOTUgMjIzLjQxIDExOS4zIDIyNC40MyAxMTUuNzYgMyBMCjcgWApWCjI1
+Mi45IDU5OC40MiBNCiAyNzguNDIgNTg5LjkxIDI2MSA1NjkuNjYgMjMyLjY1
+IDU2Ni40MiBEClYKMCBYCk4KNSBYCjkwIDQ1MCA2LjQ4IDYuNDggMzYxLjQ1
+IDQ3Ny4zMiBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDM2MS40NSA0NzcuMzIg
+QQo1IFgKOTAgNDUwIDYuNDggNi40OCAzNDguMDggNDc0Ljg5IEcKMCBYCjkw
+IDQ1MCA2LjQ4IDYuNDggMzQ4LjA4IDQ3NC44OSBBCjUgWAo5MCA0NTAgNi40
+OCA2LjQ4IDMzMy4xIDQ3NC4wOCBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDMz
+My4xIDQ3NC4wOCBBCjUgWAowIDc4IDYuMTUgNi4xNSAzNzAgNDg5LjM5IEcK
+MCBYCjAgNzggNi4xNSA2LjE1IDM3MCA0ODkuMzkgQQozMTguMTEgNTg1LjA1
+IE0KIDMyMS4zNSA1ODcuODkgMzIxLjc2IDU4OC4yOSAzMjIuNTcgNTkzLjU2
+IEQKIDMyOS40NSA1ODkuOTEgMzMyLjY5IDU5My41NiAzMzUuMTIgNTk4LjAx
+IEQKIDMzOS41OCA1OTEuOTQgMzQ0LjAzIDU5NS4xOCAzNDcuMjcgNTk4LjAx
+IEQKIDM0OC4wOCA1ODcuNDggMzU3LjQgNTg4LjcgMzY0LjI5IDU4OS41MSBE
+CiAzNjMuNDggNTc4LjE3IDM3Mi43OSA1ODIuNjIgMzc1LjIyIDU4MC42IEQK
+IDM3MS45OCA1NjguNDUgMzc5LjY3IDU2OC44NSAzODAuODkgNTY3LjY0IEQK
+IDM3NS4yMiA1NjYuMDIgMzcwLjYgNTU2LjggMzc3LjY1IDU1MS44NCBECiAz
+NjkuOTUgNTQwLjEgMzc4Ljg2IDUzNi44NSAzODAuODkgNTM2LjQ1IEQKIDM5
+MC42MSA1NDkuNDEgNDAwLjc0IDUxNC41OCAzNzMuMiA1MjAuMjUgRAo3IFgK
+VgowIFgKTgo3IFgKOTAgNDUwIDE4LjQzIDIxLjA2IDMyNS4yIDU2NS42MSBH
+CjAgWAo5MCA0NTAgMTguNDMgMjEuMDYgMzI1LjIgNTY1LjYxIEEKMzMyLjI5
+IDU1Mi4yNSBNCiAzMDUuNTYgNTc3LjM2IDI5Ni4yNCA1NDAuMSAzMjIuNTcg
+NTQwLjEgRAo3IFgKVgowIFgKTgo5MCA0NTAgMi43OCAyLjc4IDMxNC4yMSA1
+NzMuMTUgRwo5MCA0NTAgMi43OCAyLjc4IDMxNC4yMSA1NzMuMTUgQQozNDQu
+NDQgNTgxLjQxIDM0MS42IDU4OC4yOSAyIEwKTgozNTQuOTcgNTgxIDM1Ny44
+IDU4Ni4yNyAyIEwKTgozNjMuNDggNTc0LjEyIDM2Ny45MyA1NzkuNzkgMiBM
+Ck4KMzY3LjkzIDU2MS45NyAzNzIuMzkgNTY0LjggMiBMCk4KMzI4LjY0IDU4
+Ny40OCAzMjkuMDUgNTk0Ljc3IDIgTApOCjMwOC4zOSA1NzQuOTMgMzAzLjk0
+IDU3OC41NyAyIEwKTgozMTMuMjUgNTgxLjgxIDMwOC44IDU4Ny40OCAyIEwK
+TgozMTkuNzMgNTg2LjI3IDMxOC4xMSA1OTEuOTQgMiBMCk4KMzM1LjQzIDU0
+NS40NiAzMzMuMjcgNTQ4LjM3IDMyNC41NCA1NDYuOTggMzI1LjA1IDU0MC41
+MiA0IEwKNyBYClYKMzc4LjA1IDUzMC4zNyBNCiAzODYuOTcgNTM1LjY0IDM4
+Ni41NiA1MjQuMyAzNzguMDUgNTI0LjMgRAowIFgKTgozMDguOSA1MjcuMjkg
+TQogMzEyLjg1IDUxOS40NCAzMTIuODUgNTE5LjQ0IDMyMC41NCA1MTMuNzcg
+RApOCjM0MC43NCA1NDEuMTYgTQogMzQ4LjcyIDUzNy45OSAzNTUuMTcgNTQw
+LjI3IDM1OC40NyA1NDEuNzYgRAogMzY3LjU5IDU0NS45MSAzNjUuMDkgNTI3
+LjM3IDM0My4yNyA1MjkuNTEgRAo3IFgKVgo5MCA0NTAgMTguNDMgMjEuMDYg
+MzQ5LjEgNTYwLjc1IEcKMCBYCjkwIDQ1MCAxOC40MyAyMS4wNiAzNDkuMSA1
+NjAuNzUgQQo5MCA0NTAgMi43OCAyLjc4IDM0MS4zNSA1NjUuMDUgRwo5MCA0
+NTAgMi43OCAyLjc4IDM0MS4zNSA1NjUuMDUgQQozNDYuMTggNTMxLjU0IDM2
+My4yNyA1MzYuMSAzNzIuNzcgNTIyLjE3IDM3MS4zNyA1MDIuNDMgMzQ1LjQz
+IDUyOS4xMyA1IEwKNyBYClYKMzA3LjE4IDU0OC42IE0KIDI1MC44OCA1MzQu
+ODMgMzQ2LjU0IDUxMy4wNSAzNjQuNjkgNTI3Ljk0IEQKMCBYCk4KMzYzLjA3
+IDUzMS4xOCBNCiAzNjUuOSA1MjkuNTYgMzY1LjkgNTI3LjEzIDM2NS41IDUy
+My40OSBECk4KMzczLjIgNTIwLjI1IDM3MC43NyA0ODkuODcgMiBMCk4KNSBY
+CjkwIDQ1MCA2LjQ4IDYuNDggMzcyLjc5IDQ4My44IEcKMCBYCjkwIDQ1MCA2
+LjQ4IDYuNDggMzcyLjc5IDQ4My44IEEKMzE5LjIzIDQ3MC41MyBNCiAzMTUu
+MTggNDM2LjExIDI5NS45NCA0NDQuNzEgMjk2Ljk1IDQxNC4zNCBECiAyOTgu
+MDcgMzgwLjc1IDMzNC40MSAzNzkuNCAzMjMuMjggMzE5LjE2IEQKIDMwOC4y
+NCAyMzguNzQgMjk3Ljk2IDIyMS45NSAyOTkuOTkgMTUzLjEgRAogMzA3LjA4
+IDEzOS45MyAzNjMuNzggMTI5LjMgMzkxLjEyIDEzNC4zNiBECk4KMzc0LjQx
+IDQ3Ny4xMSBNCiAzOTcuNyA0NDAuNjYgNDQ5Ljg1IDQxMy44MyA0MjAuNDgg
+MzQ4LjUyIEQKTgozNjUuMyA0MzUuNiBNCiAzODYuMDUgNDIwLjkyIDM5NS40
+MiAzOTMuMDcgMzkwLjM2IDM3MS44MSBECk4KMzgwLjk5IDQxOC44OSBNCiAz
+ODIuNTEgMzk1LjEgMzg4LjA4IDM3Mi4zMiAzOTguMiAzNTQuNiBECk4KMzA1
+LjQzIDQ0MC4wMyBNCiAzMjUuNjggNDMxLjkzIDM0OC40NiA0MjkuMjcgMzY4
+LjcxIDQzMi44MiBECk4KMjY0LjA0IDQ0My4xOSBNCiAyODkuMzYgNDQwLjE2
+IDI5OC40NyA0NjQuNDYgMzE4LjIxIDQ2NC40NiBECk4KMjc1LjE4IDQwOS43
+OCBNCiAyODYuMzIgNDExLjggMjg2LjcgNDEyLjA2IDI5Ni44MiA0MTguNjQg
+RApOCjI2OS42MSA0NTUuODUgMjg5LjM2IDQ1MC4yOCAyIEwKTgoyOTMuNDkg
+MzgzLjAyIDMxMS41MiAzODEuNDIgMiBMCk4KMjg3Ljg0IDQxMi44MiAyODYu
+MzggMzkwLjYzIDIgTApOCjQyMi41MSA0NDUuNzMgTQogNDUxLjM2IDQ0MC4x
+NiA0MDguODQgNDAwLjY3IDQwOS44NSA0MjkuNTIgRAo3IFgKVgowIFgKTgo0
+MDguMzMgNDY0LjQ2IE0KIDQ0NS4zNCA0NjcuOSA0MDIuODkgNDAwLjY3IDM5
+NS4yOSA0NDMuNyBECjcgWApWCjAgWApOCjM4Mi42NCA0NjYuODYgTQogNDQy
+LjM4IDQ5My43IDM4NC45MiA0MDguMzkgMzg2Ljk0IDQ2MS41NSBECjcgWApW
+CjAgWApOCjM4MiA0NjYuNDggMzg2LjMxIDQ2MS40MiAyIEwKTgoyNDguNDUg
+MTUwLjg3IDI0OC40NSAxNDMuOTggMiBMCk4KMjQ4LjQ1IDEzNy41IDI0OS4y
+NiAxMzEuNDMgMiBMCk4KMzI0LjMzIDE0MC4xOCBNCiAzMjYuMjEgMTE1LjYz
+IDMyMy43OCAxMDAuMjQgMjk2LjA0IDkwLjkgRApOCjMxMi4zIDc5LjA0IE0K
+IDMzNS4xMiA4NC40NSAzNDcuMjcgOTAuOTMgMzUyLjEzIDk1LjM4IEQKIDM1
+Mi41NCA5MC4xMiAzNTEuMzIgODUuMjYgMzUxLjczIDc5Ljk5IEQKIDM0OC4z
+NSA3Ny4xNiAzNDMuNjMgNzQuNzIgMzM2LjAzIDc1LjI4IEQKIDMzNC41NCA3
+NS4xNyAzMzguNSA3OS43OSAzMzUuNTMgNzkuMTggRAogMzMxLjcgNzguMzkg
+MzI1LjMxIDcyLjk5IDMxOC4xMSA3MS44OSBECjIgWApWCjAgWApOCjM0OS43
+IDEzNS4wNyAzNTIuMTMgOTUuMzggMiBMCk4KMzc3LjY3IDEzMy4xNCBNCiAz
+NzguNjMgMTA5LjcgMzk0LjYgODYuNjMgMzcxIDc4LjU2IEQKIDM1Ni4xMiA3
+My41IDM1Mi42OSA2Ni4xMSAzNDguNDkgNjMuMzkgRApOCjM4Mi45MiA1OC41
+MyBNCiAzNjYuMzEgNjAuMTUgMzUyLjk0IDUwLjgzIDM0OC40OSA2My4zOSBE
+CiAzMjAuNTQgMzkuNDkgMzUwLjUxIDQ1LjU2IDM3NS42MyA0Ni4zNyBECjIg
+WApWCjAgWApOCjQ0NS4yOSAyMjcuMDEgNDcyLjgzIDIyMS4zNCA0NzIuODMg
+MjM4LjM1IDQ5Mi4yNyAyMzkuMTYgNDg4LjIyIDI1OC42IDUwNi40NSAyNzEu
+MTYgNDg5Ljg0IDI4OC41OAogNTA2Ljg1IDMxMi4wNyA0ODAuMTIgMzE3LjMz
+IDQ4Ny4wMSAzNDYuMDkgNDU1LjgyIDM0MC4wMSA0NDUuMjkgMzYyLjcgNDIy
+LjIgMzQ2LjQ5IDM5Ny45IDM1NSAzOTMuMDQgMzMxLjEKIDM3MS4xNyAzMzAu
+MjkgMzgwLjA4IDI5OS41MSAxNyBMCk4KNyBYCjkwIDQ1MCAxOS4wNCAyMS40
+NyAzOTIuNjQgMjgzLjcyIEcKMCBYCjkwIDQ1MCAxOS4wNCAyMS40NyAzOTIu
+NjQgMjgzLjcyIEEKNDQxLjY0IDI4My4zMSA0NTAuMTUgMjg1Ljc0IDIgTApO
+CjQzNS44OCAyOTQuNDQgNDQxLjY0IDI5OC4zIDIgTApOCjQyNy40MyAyOTku
+NyA0MjkuMDkgMzA2LjQgMiBMCk4KNDE1LjI5IDI5OS45MyA0MTQuMSAzMDYu
+OCAyIEwKTgo0MDAuODQgMzAzLjA5IDQwMy45OCAzMTEuMjYgMiBMCk4KMzky
+LjY0IDMwNS4xOCAzOTEuNDIgMzE0LjUgMiBMCk4KMzgyLjg5IDMwMi4yIDM3
+OS4yNyAzMTAuMDQgMiBMCk4KMzc2LjM1IDI5NC44MyAzNzAuMzYgMjk5LjUx
+IDIgTApOCjkwIDQ1MCAyLjYzIDIuNjMgMzg1Ljk1IDI5Ny42OSBHCjkwIDQ1
+MCAyLjYzIDIuNjMgMzg1Ljk1IDI5Ny42OSBBCjM3NS42MyAyNjguMzMgTQog
+MzE0Ljg3IDI1OS4wMSA0MTIuMDggMjM1LjkyIDQzMi43MyAyNDQuODMgRApO
+CjQzMCAyNDkuNTkgTQogNDMzLjI0IDI0NS41NCA0MzMuNjQgMjQ0LjMzIDQz
+My42NCAyMzkuODcgRApOCjM4OC4yMSAyNjIuODMgTQogMzkwLjcgMjY2LjI3
+IDQwMC43NCAyNzIuMjkgNDAxLjUgMjcwLjMgRAogNDAyLjI2IDI2OC4yOCA0
+MDMuMjcgMjY2IDQwNS44IDI2My44NCBECiA0MDcuODMgMjYyLjExIDM5MC45
+OSAyNTkuNDEgMzg5LjczIDI2MC44MSBECjcgWApWCjkwIDQ1MCAyMS4wNiAy
+MS40NyA0MjAuOTkgMjc5LjI2IEcKMCBYCjkwIDQ1MCAyMS4wNiAyMS40NyA0
+MjAuOTkgMjc5LjI2IEEKOTAgNDUwIDIuNjMgMi42MyA0MTEuNDcgMjg4LjM3
+IEcKOTAgNDUwIDIuNjMgMi42MyA0MTEuNDcgMjg4LjM3IEEKNDAwLjMzIDI3
+NC44MSBNCiAzODIuNTEgMjkxLjgyIDM1OS4wMiAyNjcuOTIgMzg4LjU5IDI2
+MS40NCBECjcgWApWCjAgWApOCjQ0My42NyAyNTIuMTMgTQogNDYwLjI1IDI2
+My4yMiA0NjYuNzYgMjMyLjI4IDQ0Mi40NSAyMzIuMjggRApOCjQ0Ni4yNCAy
+NDYuNDggTQogNDUzLjcgMjUxLjQ3IDQ1Ni42MyAyMzcuNTQgNDQ1LjY5IDIz
+Ny41NCBECk4KMzg5LjQgMjQ2LjA1IE0KIDQwNy4zNSAyMzIuOTkgNDE2LjEz
+IDIwNy4xNyA0MDYuODEgMTc5LjYzIEQKTgo3IFgKOTAgNDUwIDUuODcgNS44
+NyA0MDYuMjEgMjA2Ljk3IEcKMCBYCjkwIDQ1MCA1Ljg3IDUuODcgNDA2LjIx
+IDIwNi45NyBBCjcgWAo5MCA0NTAgNS44NyA1Ljg3IDQxNi4zMyAyMDMuMzIg
+RwowIFgKOTAgNDUwIDUuODcgNS44NyA0MTYuMzMgMjAzLjMyIEEKNyBYCjkw
+IDQ1MCA1Ljg3IDUuODcgNDI5LjI5IDIwNS43NSBHCjAgWAo5MCA0NTAgNS44
+NyA1Ljg3IDQyOS4yOSAyMDUuNzUgQQo0NDQuODggMTc4LjAxIE0KIDQyOS40
+OSAxNzUuNTggNDIwLjU4IDE3Mi43NCA0MDQuNzkgMTgwLjAzIEQKIDQwMy42
+OSAxNzQuNjEgMzk2LjY5IDEyNi4xNiAzNzQuODIgMTEwLjc3IEQKIDM4NC41
+NCAxMTAuMzcgMzg2Ljk3IDExMi44IDM5NC42NiAxMTQuMDEgRAo0IFgKVgow
+IFgKTgozOTQuNjYgMTE0LjAxIDM4OC41OSA5Ny44MSA0MDQuMzggMTA3Ljk0
+IDQwNy4yMiA4Ny42OSA0MjIuNjEgMTAwLjY1IDQzMC43MSA4NC44NSA0NDIu
+ODYgOTkuMDMKIDQ1OS40NiA4NC44NSA0NjMuOTIgMTA2LjMyIDQ4MS43NCA5
+NS43OSA0ODEuMzQgMTE0LjgyIDQ5OC4zNSAxMDcuMTMgNDk1LjkyIDEyMC40
+OSAxMyBMCjQgWApWCjAgWApOCjQ3NS4yOSAxNTkuNzYgTQogNDgxLjM0IDE0
+NS42IDQ5Ny45NCAxMjkuODEgNTA4LjQ4IDEyMy4zMyBECiA1MDAuMzcgMTIw
+LjkgNTAwLjc4IDEyMC45IDQ5NS45MiAxMjAuNDkgRAo0IFgKVgowIFgKTgo0
+NDIuMDUgMjMyLjI4IE0KIDQ0NS42OSAyMjMuMzcgNDQ4LjkzIDIxOC4xIDQ0
+OS4zNCAyMTIuMDMgRAogNDgwLjUzIDE5Mi41OSA0NzguOTEgMTU0LjUyIDQ2
+OS4xOCAxMzUuMDcgRAogNDcxLjYxIDExNi4wNCA0NjYuMzUgMTA5Ljk2IDQ2
+Mi4zIDExMi4zOSBECjcgWApWCjAgWApOCjQ2MS4wOCAxMjAuNDkgTQogNDY0
+LjczIDEwMS40NiA0NDguMTIgMTAwLjI0IDQ1MS43NyAxMjAuMDkgRAo3IFgK
+VgowIFgKTgo0NTEuNzcgMTIwLjA5IE0KIDQ0NS4yOSAxMDcuMTMgNDM2LjM4
+IDEwOS41NiA0NDMuNjcgMTI0Ljk1IEQKNyBYClYKMCBYCk4KNDQyLjQ1IDEy
+Mi41MiBNCiA0MjYuNjYgMTI0LjU0IDQzNS45NyAxMzEuODMgNDQ2LjkxIDEz
+OC43MiBECiA0NDkuMzQgMTQ2LjAxIDQ1MC45NiAxNjIuMjEgNDQzLjY3IDE4
+NC40OSBECk4KNyBYCjkwIDQ1MCA1Ljg3IDUuODcgNDUxLjE2IDIxNC42NiBH
+CjAgWAo5MCA0NTAgNS44NyA1Ljg3IDQ1MS4xNiAyMTQuNjYgQQo3IFgKOTAg
+NDUwIDUuODcgNS44NyA0NDEuMDQgMjA5LjQgRwowIFgKOTAgNDUwIDUuODcg
+NS44NyA0NDEuMDQgMjA5LjQgQQo0NzYuODggNDkuMjEgTQogNDkxLjA2IDMy
+LjIgMzg5LjggMTcuNiA0MzEuMTEgNDcuOTkgRAogNDMzLjU0IDQzLjU0IDQz
+NC4zNSA0MS45MiA0MzcuMTkgMzcuNDYgRAogNDQwLjQzIDM5LjA4IDQ0Mi4z
+IDQwLjE2IDQ0Ny4zMSA0Mi43MyBECiA0NDYuNSA0Ny45OSA0NDMuNjcgNTAu
+NDIgNDQwLjgzIDU1LjY5IEQKIDQ0My45IDU3LjQxIDQ0NC45MSA1OC40MiA0
+NDcuMzEgNTkuNzQgRAogNDU0LjYgNTUuNjkgNDU2LjIyIDUwLjQyIDQ1OS40
+NiA0Mi4zMiBECiA0NjcuOTcgNDUuOTcgNDc0Ljc5IDUwLjI0IDQ3Ny4yOSA0
+OS4yMSBECjYgWApWCjAgWApOCjQzMC43MSA2MC45NiBNCiA0NDQuODggNDMu
+OTQgMzQ2LjA2IDMxLjM5IDM4Ny4zNyA2MS43NyBECiAzODkuOCA1Ny4zMSAz
+OTEuODMgNTYuMDkgMzk0LjY2IDUxLjY0IEQKIDM5Ny45IDUzLjI2IDM5Ni4x
+MyA1MS45IDQwMS4xNCA1NC40NyBECiA0MDAuMzMgNTkuNzQgMzk3LjUgNjIu
+MTcgMzk0LjY2IDY3LjQzIEQKIDM5Ni40MyA2OS4wNSAzOTguMiA3MC4xOSA0
+MDEuMTQgNzEuNDggRAogNDA4LjQzIDY3LjQzIDQxMC4wNSA2Mi4xNyA0MTMu
+MjkgNTQuMDcgRAogNDIxLjggNTcuNzIgNDI4LjM2IDYxLjk4IDQzMC44NiA2
+MC45NiBECjYgWApWCjAgWApOCjQzMS4xMSA0Ny45OSA0NDAuODMgNTUuNjkg
+MiBMCk4KMzg3LjM3IDYxLjc3IDM5NC42NiA2Ny40MyAyIEwKTgozMjAuNTQg
+NTEzLjc3IDMyMC41NCA0ODIuOTQgMiBMCk4KNSBYCjkwIDQ1MCA2LjQ4IDYu
+NDggMzE5LjczIDQ3Ni41MSBHCjAgWAo5MCA0NTAgNi40OCA2LjQ4IDMxOS43
+MyA0NzYuNTEgQQowIEYKKE9yaWdpbmFsIGltYWdlIHNjYW5uZWQgZnJvbSBM
+QSBUKSAwIC0yNzAgNTgwLjM4IDE4LjMgVEYKKGltZXMgYnkgRG91ZyBLcmF1
+c2UpIDAgLTI3MCA1ODAuMzggMTA5LjA2IFRGCihUKSAwIC0yNzAgNTg2LjU3
+IDE4LjMgVEYKKHJhY2VkIGluIEZyYW1lIE1ha2VyIGJ5IENodWNrIE11c2Np
+YW5vKSAwIC0yNzAgNTg2LjU3IDIxLjc0IFRGCihDb3B5cmlnaHQsIDE5OTAs
+IEVsIEJhcnRvIEVudGVycHJpc2VzKSAwIC0yNzAgNTkyLjc2IDE4LjMgVEYK
+Rk1FTkRQQUdFCiUlRW5kUGFnZTogIjEiIDAKJSVUcmFpbGVyCiUlQm91bmRp
+bmdCb3g6IDAgMCA2MTIgNzkyCiUlUGFnZXM6IDEgLTEKJSVEb2N1bWVudEZv
+bnRzOiBIZWx2ZXRpY2EKCgo=
+
+--16819560-2078917053-688350843:#11603
+Content-Type: BINARY;name="Alices_PDP-10"
+Content-Transfer-Encoding: BASE64
+Content-Description: Alice's PDP-10 w/ TECO & DDT
+
+CQkJCUFsaWNlJ3MgUERQLTEwCgo7OzsgV2l0aCB0aGFua3MgKGFuZCBhcG9s
+b2dpZXMpIHRvIENocmlzIFN0YWN5LCBBbGFuIFdlY2hzbGVyLCBOb2VsIENo
+aWFwcGEsCjs7OyBMYXJyeSBBbGxlbiwgYW5kIG9mIGNvdXJzZSBBcmxvIEd1
+dGhyaWUsIGFuZCBwYXJ0aWN1bGFybHkgdG8gQW5uIE1hcmllIEZpbm4KOzs7
+IHdobyBpcyBhIGtpbmQgc291bCBhbmQgbm90IGF0IGFsbCBsaWtlIHRoZSBw
+ZXJzb24gcG9ydHJheWVkIGhlcmVpbi4KOzs7CQkJCQkJCQktLSBzcmEgMyBN
+YXkgODUKClRoaXMgc29uZyBpcyBjYWxsZWQgIkFsaWNlJ3MgUERQLTEwIi4g
+IEJ1dCBBbGljZSBkb2Vzbid0IG93biBhIFBEUC0xMCwgaW4gZmFjdApBbGlj
+ZSBpc24ndCBldmVuIGluIHRoZSBzb25nLiAgSXQncyBqdXN0IHRoZSBuYW1l
+IG9mIHRoZSBzb25nLiAgVGhhdCdzIHdoeSBJCmNhbGxlZCB0aGlzIHNvbmcg
+IkFsaWNlJ3MgUERQLTEwIi4KCllvdSBzZWUsIGl0IGFsbCBzdGFydGVkIGFi
+b3V0IHR3byBpbmNvbXBhdGlibGUgbW9uaXRvciB2ZXJzaW9ucyBhZ28sIGFi
+b3V0IHR3bwptb250aHMgYWdvIG9uIGEgVHVlc2RheSwgd2hlbiBteSBmcmll
+bmQgYW5kIEkgU1VQRFVQJ2Qgb3ZlciB0byBNSVQtT1ogdG8gcGljawp1cCBz
+b21lIGhhY2tlcnMgdG8gZ28gb3V0IGZvciBhIENoaW5lc2UgZGlubmVyLiAg
+QnV0IEFJIGhhY2tlcnMgZG9uJ3QgbGl2ZSBvbgpNSVQtT1osIHRoZXkgbGl2
+ZSBvbiB2YXJpb3VzIGFzc29ydGVkIGxpc3BtcyBhbmQgc3VjaCwgYW5kIHNl
+ZWluZyBhcyBhbmQgaG93CnRoZXkgbmV2ZXIgbG9nIGluIGV4Y2VwdCB2aWEg
+dGhlIGZpbGUgc2VydmVyLCB0aGV5IGhhZG4ndCBnb3R0ZW4gYXJvdW5kIHRv
+CmRvaW5nIGZpbGVzeXN0ZW0gZ2FyYmFnZSBjb2xsZWN0aW9uIGZvciBhIGxv
+bmcgdGltZS4KCldlIGdvdCBvdmVyIHRoZXJlLCBzYXcgNjAwIHBhZ2VzIGZy
+ZWUsIDEwMDAwIHBhZ2VzIGluIHVzZSBvbiBhIDUgcGFjayBQUzosIGFuZApk
+ZWNpZGVkIGl0IHdvdWxkIGJlIGEgZnJpZW5kbHkgZ2VzdHVyZSB0byBydW4g
+Q0hFQ0tEIGZvciB0aGVtIGFuZCB0cnkgdG8KcmVjbGFpbSBzb21lIG9mIHRo
+YXQgbG9zdCBzcGFjZS4gIFNvIHdlIHJlbG9hZGVkIHRoZSBzeXN0ZW0gd2l0
+aCB0aGUgZmxvcHBpZXMKYW5kIHRoZSBzd2l0Y2ggcmVnaXN0ZXJzIGFuZCBv
+dGhlciBpbXBsZW1lbnRzIG9mIGRlc3RydWN0aW9uLCBhbmQgYW5zd2VyZWQg
+IlkiCnRvIFJVTiBDSEVDS0Q/CgpCdXQgd2hlbiB3ZSBnb3QgdGhlIHN5c3Rl
+bSB1cCBhbmQgdHJpZWQgdG8gcmVsZWFzZSBhbGwgdGhlIGxvc3QgcGFnZXMg
+dGhlcmUgd2FzCmEgbG91ZCBiZWVwaW5nIGFuZCBhIGJpZyBtZXNzYWdlIGZs
+YXNoZWQgdXAgb24gb3VyIHNjcmVlbiBzYXlpbmc6CiAgICAgICAgICAgICAg
+ICAgICAgICBQRVJNSVNTSU9OIERFTklFRCBCWSBBQ0oKCldlbGwsIHdlJ2Qg
+bmV2ZXIgaGVhcmQgb2YgYSB2ZXJzaW9uIG9mIEFDSiB0aGF0IHdvdWxkIGxl
+dCB5b3UgZ28gaW50byBNRERUIGZyb20KQU5PTllNT1VTIGJ1dCBub3QgcnVu
+IENIRUNLRCwgYW5kIHNvLCB3aXRoIHRlYXJzIGluIG91ciBleWVzLCB3ZSBo
+ZWFkZWQgb2ZmCm92ZXIgdGhlIENoYW9zbmV0IGxvb2tpbmcgZm9yIGEgZmls
+ZXN5c3RlbSB3aXRoIGVub3VnaCBmcmVlIHBhZ2VzIHRvIHdyaXRlIG91dAp0
+aGUgTE9TVC1QQUdFUy5CSU4gZmlsZS4gIERpZG4ndCBmaW5kIG9uZS4uLgoK
+VW50aWwgd2UgZ290IHRvIFhYLTExLCBhbmQgYXQgdGhlIG90aGVyIGVuZCBv
+ZiBYWC0xMSB3YXMgYW5vdGhlciBNSVQgVHdlbmV4LAphbmQgaW4gUFM6PE9Q
+RVJBVE9SPiBvbiB0aGF0IE1JVCBUd2VuZXggd2FzIGFub3RoZXIgTE9TVC1Q
+QUdFUy5CSU4gZmlsZS4gIEFuZAp3ZSBkZWNpZGVkIHRoYXQgb25lIGJpZyBM
+T1NULVBBR0VTLkJJTiBmaWxlIHdhcyBiZXR0ZXIgdGhhbiB0d28gbGl0dGxl
+CkxPU1QtUEFHRVMuQklOIGZpbGUsIGFuZCByYXRoZXIgdGhhbiBwYWdlIHRo
+YXQgb25lIGluIHdlIHRob3VnaHQgd2UnZCB3cml0ZQpvdXJzIG91dC4gIFNv
+IHRoYXQncyB3aGF0IHdlIGRpZC4KCldlbnQgYmFjayB0byBPWiwgZm91bmQg
+c29tZSBoYWNrZXJzIGFuZCB3ZW50IG91dCBmb3IgYSBDaGluZXNlIGRpbm5l
+ciB0aGF0CmNvdWxkbid0IGJlIGJlYXQsIGFuZCBkaWRuJ3QgZ2V0IHVwIHVu
+dGlsIHRoZSBuZXh0IG1vcm5pbmcgd2hlbiB3ZSBnb3QgYSBTRU5ECmZyb20g
+QW5uIE1hcmllIEZpbm4uICBTaGUgc2FpZCwgIktpZCwgd2UgZm91bmQgeW91
+ciBpbml0aWFscyBpbiBTSVhCSVQgaW4gdGhlCnJpZ2h0IGhhbGYgb2YgYSBQ
+T1BKIGF0IHRoZSBlbmQgb2YgYSB0d28gbWVnYXdvcmQgY29yZSBkdW1wIGZ1
+bGwgb2YgZ2FyYmFnZSwKanVzdCB3YW50ZWQgdG8ga25vdyBpZiB5b3UgaGFk
+IGFueSBpbmZvcm1hdGlvbiBhYm91dCBpdCIuICBBbmQgSSBzYWlkLCAiWWVz
+Cm1hJ2FtIEFubiBNYXJpZSwgSSBjYW5ub3QgdGVsbCBhIGxpZSwgSSBwdXQg
+dGhhdCBYVU5BTUUgaW50byB0aGF0IGhhbGZ3b3JkIi4KCkFmdGVyIHRhbGtp
+bmcgYmFjayBhbmQgZm9ydGggd2l0aCBBbm4gZm9yIGFib3V0IDQ1IG1lc3Nh
+Z2VzIHdlIGFycml2ZWQgYXQgdGhlCnRydXRoIG9mIHRoZSBtYXR0ZXIgYW5k
+IEFubiBzYWlkIHRoYXQgd2UgaGFkIHRvIGdvIHJlYnVpbGQgdGhlIGJpdHRh
+YmxlIGFuZCB3ZQphbHNvIGhhZCB0byBjb21lIGRvd24gYW5kIHRhbGsgdG8g
+aGVyIGluIHJvb20gTkU0My01MDEuICBOb3cgZnJpZW5kcywgdGhlcmUgd2Fz
+Cm9ubHkgb25lIG9mIHR3byB0aGluZ3MgdGhhdCBBbm4gY291bGQgb2YgZG9u
+ZSB3aXRoIHVzIGRvd24gYXQgcm9vbSA1MDEsIGFuZCB0aGUKZmlyc3Qgb25l
+IHdhcyB0aGF0IHNoZSBjb3VsZCBoYXZlIGhpcmVkIHVzIG9uIHRoZSBzcG90
+IGZvciBhY3R1YWxseSBrbm93aW5nCmVub3VnaCBhYm91dCBUd2VuZXggdG8g
+c2NyZXcgaXQgdXAgdGhhdCBiYWRseSwgd2hpY2ggd2Fzbid0IHZlcnkgbGlr
+ZWx5IGFuZCB3ZQpkaWRuJ3QgZXhwZWN0IGl0LCBhbmQgdGhlIG90aGVyIHdh
+cyB0aGF0IHNoZSBjb3VsZCBoYXZlIGJhd2xlZCB1cyBvdXQgYW5kIHRvbGQK
+dXMgbmV2ZXIgdG8gYmUgc2VlbiBoYWNraW5nIGZpbGVzeXN0ZW1zIGFnYWlu
+LCB3aGljaCB3YXMgd2hhdCB3ZSBleHBlY3RlZC4gIEJ1dAp3aGVuIHdlIGdv
+dCB0byByb29tIDUwMSB3ZSBkaXNjb3ZlcmVkIHRoYXQgdGhlcmUgd2FzIGEg
+dGhpcmQgcG9zc2liaWxpdHkgdGhhdAp3ZSBoYWRuJ3QgZXZlbiBjb3VudGVk
+IHVwb24sIGFuZCB3ZSB3YXMgYm90aCBpbW1lZGlhdGVseSBkZS13aGVlbGVk
+LgpDRCVESVInZWQuICBBbmQgSSBzYWlkICJBbm4sIEkgZG9uJ3QgdGhpbmsg
+SSBjYW4gcmVidWlsZCB0aGUgYml0dGFibGUgd2l0aCB0aGlzCmhlcmUgRklM
+RVMtT05MWSBiaXQgc2V0LiIgIEFuZCBzaGUgc2FpZCAiWE9GRiwga2lkLCBn
+ZXQgaW50byB0aGlzIFVEUCBwYWNrZXQiCmFuZCB0aGF0J3Mgd2hhdCB3ZSBk
+aWQgYW5kIHJvZGUgdXAgdG8gdGhlIHNxdWFyZSBicmFja2V0IGFzY2l6IHNs
+YXNoIHNjZW5lIG9mCnRoZSBjcmltZSBzbGFzaCBjbG9zZSBzcXVhcmUgYnJh
+Y2tldC4KCk5vdyBmcmllbmRzLCBJIHdhbnQgdG8gdGVsbCB5b3UgYWJvdXQg
+dGhlIG5pbnRoIGZsb29yIG9mIGJ1aWxkaW5nIE5FNDMgd2hlcmUKdGhpcyBo
+YXBwZW5lZC4gIFRoZXkgZ290IHRocmVlIEtMMTBzLCAyNCBMSVNQTXMsIGFu
+ZCBhYm91dCAzMiBWQVhlbiBydW5uaW5nIDQuMgp1bml4LiAgQnV0IHdoZW4g
+d2UgZ290IHRvIHRoZSBzcXVhcmUgYnJhY2tldCBhc2NpeiBzbGFzaCBzY2Vu
+ZSBvZiB0aGUgY3JpbWUKc2xhc2ggY2xvc2Ugc3F1YXJlIGJyYWNrZXQgdGhl
+cmUgd2FzIGZpdmUgdHdlbmV4IGhhY2tlcnMgcGFzdCBhbmQgcHJlc2VudCwg
+dGhpcwpiZWluZyB0aGUgYmlnZ2VzdCBsb3NzYWdlIHlldCBieSBhbiBSTVMg
+Y2xvbmUgYW5kIGV2ZXJ5Ym9keSB3YW50ZWQgdG8gZ2V0IGluCnRoZWlyIHN1
+Z2dlc3Rpb24gZm9yIGEgbmV3IHN5c3RlbSBkYWVtb24gdGhhdCB3b3VsZCBo
+YXZlIGtlcHQgaXQgZnJvbSBldmVyCmhhdmluZyBoYXBwZW5lZCBpbiB0aGUg
+Zmlyc3QgcGxhY2UuICBBbmQgdGhleSB3YXMgdXNpbmcgdXAgYWxsIGtpbmRz
+IG9mCmRlYnVnZ2luZyBlcXVpcG1lbnQgdGhhdCB0aGV5IGhhZCBseWluZyBh
+cm91bmQgb24gVjNBIFNXU0tJVCB0YXBlcy4gIFRoZXkgd2VyZQpkb2luZyBE
+U3MsIE1PTlJEcywgYW5kIFJTVFJTSHMsIGFuZCB0aGV5IG1hZGUgMjcwMDAg
+cGFnZXMgb2YgY29yZSBkdW1wcyBhbmQKcGhvdG8gZmlsZXMgb24gYW4gUlAw
+NiB3aXRoIGNvbW1lbnRzIGFuZCAtUkVBRC0uLVRISVMtIGZpbGVzIHRvIGJl
+IHVzZWQgYXMKZXZpZGVuY2UgYWdhaW5zdCB1cy4KCkFmdGVyIHRoZSBvcmRl
+YWwsIEFubiB0b29rIHVzIGJhY2sgZG93bnN0YWlycyBhbmQgbGVmdCB1cyB3
+aXRoIHRoZSBDTFUgaGFja2Vycy4KU2hlIHNhaWQgIktpZCwgSSdtIGdvbm5h
+IGxlYXZlIHlvdSB3aXRoIHRoZSBDTFUgaGFja2Vycy4gIEkgd2FudCB5b3Vy
+IGpzeXMKbWFudWFsIGFuZCB5b3VyIFJPTE0gRFRJIi4gIEkgc2FpZCAiQW5u
+LCBJIGNhbiB1bmRlcnN0YW5kIHlvdXIgd2FudGluZyBteSBqc3lzCm1hbnVh
+bCBzbyBJIHdvbid0IHJlbWluZCB0aGUgQ0xVIGhhY2tlcnMgb2YgZ3JvZHkg
+dGhpbmdzIGxpa2Ugb3BlcmF0aW5nCnN5c3RlbXMsIGJ1dCB3aGF0IGRvIHlv
+dSB3YW50IG15IERUSSBmb3I/IiBhbmQgc2hlIHNhaWQgIktpZCwgd2UgZG9u
+J3Qgd2FudCBhbnkKVlRTIGVycm9ycyIuICBJIHNhaWQgIkFubiwgZGlkIHlv
+dSB0aGluayBJIHdhcyBnb2luZyB0byB0cnkgdG8gY3Jhc2ggdGhlIHN5c3Rl
+bQpmb3IgbGl0dGVyaW5nPyIgIEFubiBzYWlkIHRoYXQgc2hlIHdhcyBtYWtp
+bmcgc3VyZSwgYW5kIGZyaWVuZHMsIEFubiB3YXMsCidjYXVzZSBzaGUgY2xl
+YXJlZCBhbGwgbXkgbGVmdC1oYW5kIHByaXZzIGJpdHMgc28gSSBjb3VsZG4n
+dCBsb2dvdXQuICBBbmQgc2hlCmRpc2FibGVkIHRoZSBUUkVQTEFDRSBjb21t
+YW5kIHNvIEkgY291bGRuJ3QgY3JvY2sgaW4gYW4gWENUIFswXSBpbnN0cnVj
+dGlvbiwKY2F1c2UgYW4gaWxsZWdhbCBpbnN0cnVjdGlvbiBpbnRlcnJ1cHQg
+dG8gTUVYRUMsIGFuZCBzbmVhayBpbnRvIE1ERFQuICBZZWFoLApBbm4gd2Fz
+IG1ha2luZyBzdXJlLCBhbmQgaXQgd2FzIGFib3V0IGZvdXIgb3IgZml2ZSBo
+b3VycyBsYXRlciB0aGF0IENoaWFwcGEKKHJlbWVtYmVyIENoaWFwcGE/ICBU
+aGlzIHNvbmcncyBuZXZlciBldmVuIG1lbnRpb25lZCBDaGlhcHBhKSBDaGlh
+cHBhIGNhbWUgYnkKYW5kIHdpdGggYSBmZXcgZ3JhdHVpdG91cyBpbnN1bHRz
+IHRvIHRoZSBDTFUgaGFja2VycyBiYWlsZWQgdXMgb3V0IG9mIHRoZXJlLAph
+bmQgd2Ugd2VudCBvdXQgYW5kIGhhZCBhbm90aGVyIENoaW5lc2UgZGlubmVy
+IHRoYXQgY291bGRuJ3QgYmUgYmVhdCwgYW5kCmRpZG4ndCBnZXQgdXAgdW50
+aWwgdGhlIG5leHQgbW9ybmluZyB3aGVuIHdlIGFsbCBoYWQgdG8gZ28gdG8g
+TENTIENvbXB1dGF0aW9uYWwKUmVzb3VyY2VzIHN0YWZmIG1lZXRpbmcuCgpX
+ZSB3YWxrZWQgaW4sIHNhdCBkb3duLiAgQW5uIGNhbWUgaW4gd2l0aCB0aGUg
+UlAwNiBkaXNrIHBhY2sgd2l0aCB0aGUgMjcwMDAKcGFnZXMgd2l0aCB0aGUg
+Y29tbWVudHMgYW5kIHRoZSAtUkVBRC0uLVRISVMtIGZpbGVzIGFuZCBhIHR3
+byBsaXRlciBjb2ZmZWUgbXVnLApzYXQgZG93bi4gIEVzdGhlciBGZWxpeCBj
+b21lcyBpbiBzYXlzICJBbGwgcmlzZSIsIHdlIHN0b29kIHVwLCBBbm4gc3Rv
+b2QgdXAKd2l0aCB0aGUgMjcwMDAgcGFnZSBSUDA2IHBhY2ssIGFuZCBEYXZl
+IENsYXJrIGNvbWVzIGluIHdpdGggYW4gSUJNIFBDLiAgSGUgc2l0cwpkb3du
+LCB3ZSBzaXQgZG93biwgQW5uIGxvb2tzIGF0IHRoZSBJQk0gUEMuICBUaGVu
+IGF0IHRoZSAyNzAwMCBwYWdlIFJQMDYgcGFjaywKdGhlbiBhdCB0aGUgSUJN
+IFBDLCB0aGVuIGF0IHRoZSAyNzAwMCBwYWdlIFJQMDYgcGFjaywgYW5kIGJl
+Z2FuIHRvIGNyeSwgYmVjYXVzZQpBbm4gaGFkIGNvbWUgdG8gdGhlIHJlYWxp
+emF0aW9uIHRoYXQgaXQgd2FzIGEgdHlwaWNhbCBjYXNlIG9mIDM2JTg9PTQg
+YW5kIHRoYXQKdGhlcmUgd2FzIG5vIHdheSB0byBkaXNwbGF5IHRob3NlIGxh
+c3QgZm91ciBiaXRzLCBhbmQgdGhhdCBEYXZlIHdhc24ndCBnb25uYQpsb29r
+IGF0IHRoZSAyNzAwMCBwYWdlcyBvZiBjb3JlIGR1bXBzIGFuZCBwaG90byBm
+aWxlcyBvbiB0aGUgUlAwNiBwYWNrIHdpdGggdGhlCmNvbW1lbnRzIGFuZCAt
+UkVBRC0uLVRISVMtIGZpbGVzIGV4cGxhaW5pbmcgd2hhdCBlYWNoIG9uZSB3
+YXMgdG8gYmUgdXNlZCBhcwpldmlkZW5jZSBhZ2FpbnN0IHVzLgoKQW5kIHdl
+IHdlcmUgcGVybWFuZW50bHkgYXNzaWduZWQgdG8gdGhlIGJhdGNoIGRyZWdz
+IHF1ZXVlIGFuZCBoYWQgdG8gcmVidWlsZAp0aGUgYml0dGFibGUgKGluIHRo
+ZSBiYXRjaCBkcmVncyBxdWV1ZSkuICBCdXQgdGhhdCdzIG5vdCB3aGF0IEkg
+Y2FtZSBoZXJlIHRvCnRhbGsgYWJvdXQuICBJIGNhbWUgaGVyZSB0byB0YWxr
+IGFib3V0IERFQy4KIAo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09
+PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpUaGV5
+IGdvdCBhIGJ1aWxkaW5nIHVwIHRoZXJlIGluIE1hcmxib3JvIHdoZXJlIHlv
+dSB3YWxrIGluIGFuZCBnZXQgYXZlcnRlZCwKZGl2ZXJ0ZWQsIGludmVydGVk
+LCByZXZlcnRlZCwgYW5kIHBlcnZlcnRlZC4gIEkgd2VudCB1cCB0aGVyZSBv
+bmUgZGF5IHRvIHBpY2sKdXAgYSBuZXcgY29weSBvZiB0aGUgdG9vbHMgdGFw
+ZS4gIERyb3ZlIGRvd24gdG8gUGhpbGx5IGZvciBhIEdyZWF0ZnVsIERlYWQK
+Y29uY2VydCB0aGUgbmlnaHQgYmVmb3JlLCBzbyBJIGxvb2tlZCBhbmQgZmVs
+dCBteSBiZXN0IHdoZW4gSSB3ZW50IGluIHRoYXQKbW9ybmluZy4gICdDYXVz
+ZSBJIHdhbnRlZCB0byBsb29rIGxpa2UgYSByZWFsIGxpdmUgdHdlbmV4IGhh
+Y2tlciBmcm9tIE1JVC4gIEkKd2FudGVkIHRvIGZlZWwgbGlrZSwgSSB3YW50
+ZWQgdG8gYmUgYSByZWFsIGxpdmUgdHdlbmV4IGhhY2tlciBmcm9tIE1JVC4g
+IEkKd2Fsa2VkIGluIGFuZCBJIHdhcyBodW5nIGRvd24sIGJydW5nIGRvd24s
+IGh1bmcgdXAsIGFuZCBzcGFjZWQgb3V0LiAgVGhlCnJlY2VwdGlvbmlzdCBo
+YW5kcyBiZSBhIHBpZWNlIG9mIHBhcGVyIHNheWluZyAiS2lkLCB0aGUgRURJ
+VC0yMCBtYWludGFpbmVycyBhcmUKcG9sbGluZyB1c2VyIG9waW5pb25zIHRv
+ZGF5IGFuZCB3b3VsZCBsaWtlIHlvdSB0byBzdG9wIGJ5IHJvb20gNjA0IHdo
+aWxlIHlvdSdyZQpoZXJlLiIKCkkgd2Fsa2VkIGluIHRoZXJlIGFuZCBJIHNh
+aWQgIkRyb2lkcywgSSB3YW50IHRvIGxvc2UuICBJIG1lYW4sIEkgd2FudCB0
+byBsb3NlLgpJIHdhbnQgdG8gc2VlIGxpbmUgZWRpdG9ycyBvbiBDUlRzIGFu
+ZCBudWxscyBpbiBteSBmaWxlcy4gIFdyaXRlIDM2IGJpdCBhc2NpaQp0aGF0
+IGNhbid0IGJlIHJlYWQgZXhjZXB0IHdpdGggdGhlIG1vbml0b3IgZmlsdGVy
+aW5nIGl0LiAgSSBtZWFuIExPU0UsIExPU0UsCkxPU0UhIiAgQW5kIEkgc3Rh
+cnRlZCBqdW1waW5nIHVwIGFuZCBkb3duIHllbGxpbmcgIkxPU0UsIExPU0Ui
+LCBhbmQgS2V2aW4KUGFldHpvbGQgY2FtZSBpbiB3ZWFyaW5nIGhpcyBtb29z
+ZSBlYXIgaGF0IGFuZCBzdGFydGVkIGp1bXBpbmcgdXAgYW5kIGRvd24gd2l0
+aAptZSB5ZWxsaW5nICJMT1NFLCBMT1NFIiwgYW5kIGEgREVDIHNhbGVzIHJl
+cCBjYW1lIG92ZXIsIHB1dCBhbiBhcm0gYXJvdW5kIG15CnNob3VsZGVyLCBh
+bmQgc2FpZCAiSG93J2QgeW91IGxpa2UgbWUgdG8gc2hvdyB5b3UgYSAqcmVh
+bCogZWRpdG9yIHRoYXQgaGFzCm1hY3JvcyBhbmQgdGhpbmdzIGxpa2UgdGhh
+dD8gIFdlIGhhdmUgb25lLCBpdCdzIGNhbGxlZCBUVi4uLi4iCgpEaWRuJ3Qg
+ZmVlbCB0b28gZ29vZCBhYm91dCBpdC4KClByb2NlZWRlZCBvbiBkb3duIHRo
+ZSBoYWxsIGdldHRpbmcgbW9yZSBkaXZlcnNpb25zIGFuZCBwZXJ2ZXJzaW9u
+cy4gIE1hbiwgSSB3YXMKaW4gdGhlcmUgZm9yIHR3byBob3VycywgdGhyZWUg
+aG91cnMsIGZvdXIgaG91cnMsIEkgd2FzIGluIHRoZXJlIGZvciBhIGxvbmcK
+dGltZSwgYW5kIHRoZXkgd2FzIGRvaW5nIGFsbCBraW5kcyBvZiBtZWFuIG5h
+c3R5IHVnbHkgdGhpbmdzLCBhbmQgSSB3YXMganVzdApoYXZpbmcgYSB0b3Vn
+aCB0aW1lIHRoZXJlLiAgVGhleSB3YXMgZGl2ZXJ0aW5nIGFuZCBpbnZlcnRp
+bmcgZXZlcnkgc2luZ2xlIHBhcnQKb2YgbWUgYW5kIHRoZXkgd2FzIGxlYXZp
+bmcgbm8gYml0IHVudG91Y2hlZC4KCkZpbmFsbHkgSSBnb3QgdG8gdGhlIHZl
+cnkgbGFzdCBvZmZpY2UgKEknZCBiZWVuIGluIGFsbCB0aGUgcmVzdCksIHRo
+ZSB2ZXJ5IGxhc3QKZGVzaywgYWZ0ZXIgdGhhdCB3aG9sZSBiaWcgdGhpbmcg
+dGhlcmUsIGFuZCBJIHdhbGsgb3ZlciBhbmQgc2F5ICJ3aGF0IGRvIHlvdQp3
+YW50PyIgYW5kIHRoZSBtYW4gc2F5cyAiS2lkLCB3ZSBvbmx5IGdvdCBvbmUg
+cXVlc3Rpb246IGhhdmUgeW91IGV2ZXIgYmVlbgpkZXdoZWVsZWQ/IgoKU28g
+SSBwcm9jZWVkZWQgdG8gdGVsbCBoaW0gdGhlIHN0b3J5IG9mIHRoZSAxMDYw
+MCBwYWdlIGZpdmUgcGFjayBQUzogIHdpdGggZnVsbApvcmNoZXN0cmF0aW9u
+IGFuZCBmaXZlIHBhcnQgaGFybW9ueSBhbmQgb3RoZXIgcGhlbm9tZW5hIGFu
+ZCBoZSBzdG9wcGVkIG1lIHJpZ2h0CnRoZXJlIGFuZCBzYWlkICJLaWQsIGRp
+ZCB5b3UgZXZlciBnZXQgaGF1bGVkIG9uIHRoZSBjYXJwZXQgZm9yIGl0PyIK
+ClNvIEkgcHJvY2VlZGVkIHRvIHRlbGwgaGltIGFib3V0IHRoZSAyNzAwMCBw
+YWdlIFJQMDYgcGFjayB3aXRoIHRoZSBjb21tZW50cyBhbmQKdGhlIC1SRUFE
+LS4tVEhJUy0gZmlsZXMgYW5kIGhlIHN0b3BwZWQgbWUgcmlnaHQgdGhlcmUg
+YW5kIHNhaWQgIktpZCwgSSB3YW50IHlvdQp0byBnbyBzaXQgb3ZlciB0aGVy
+ZSBvbiB0aGF0IGJlbmNoIG1hcmtlZCBMYXJnZSBTeXN0ZW1zIFNJRy4gIE5P
+VywgS0lEISIKCkksIEkgd2Fsa2VkIG92ZXIgdG8gdGhlIGJlbmNoIHRoZXJl
+Li4uIFNlZSwgdGhlIExDRyBncm91cCBpcyB3aGVyZSB0aGV5IHB1dCB5b3UK
+aWYgdGhleSB0aGluayB5b3UgbWF5IG5vdCBiZSBjb21wYXRpYmxlIHdpdGgg
+dGhlIHJlc3Qgb2YgREVDJ3MgcHJvZHVjdCBsaW5lLgoKVGhlcmUgd2FzIGFs
+bCBraW5kcyBvZiBtZWFuIG5hc3R5IHVnbHkgcGVvcGxlIHRoZXJlIG9uIHRo
+ZSBiZW5jaC4uLiBDaGFvc25ldApkZXNpZ25lcnMuLi4gTGlzcCBoYWNrZXJz
+Li4uIFRFQ08gaGFja2Vycy4gIFRFQ08gaGFja2VycyByaWdodCB0aGVyZSBv
+biB0aGUKYmVuY2ggd2l0aCBtZSEgIEFuZCB0aGUgbWVhbmVzdCBvbmUgb2Yg
+dGhlbSwgdGhlIGhhaXJpZXN0IFRFQ08gaGFja2VyIG9mIHRoZW0KYWxsIHdh
+cyBjb21pbmcgb3ZlciB0byBtZS4gIEFuZCBoZSB3YXMgbWVhbiBhbmQgbmFz
+dHkgYW5kIGhvcnJpYmxlIGFuZAp1bmRvY3VtZW50ZWQgYW5kIGFsbCBraW5k
+cyBvZiBzdHVmZi4gIEFuZCBoZSBzYXQgZG93biBuZXh0IHRvIG1lIGFuZCBz
+YWlkOgogCi4oNjc1MDQxNjQwNzQ0LmY2dzAwNzE0MTAwNDc0NS5mNnc2NDM3
+MDAwMDAwMDAuZjYpLC5meCpbMEBmdF5dMCR3XlwKCkFuZCBJIHNhaWQgIkkg
+ZGlkbid0IGdldCBub3RoaW5nLCBJIGhhZCB0byByZWJ1aWxkIHRoZSBiaXR0
+YWJsZSBpbiBxdWV1ZSBzaXgiCmFuZCBoZSBzYWlkOgogCi4oNjc1MDQxNjQw
+MDY3LmY2dzQxNjMwMDcxNTc2NS5mNncwMDQ0NDU2NzUwNDUuZjYKICA0NTU0
+NDU0NDAwNDYuZjZ3NTc2MjAwNTM1MTQ0LmY2dzM3MDAwMDAwMDAwMC5mNgop
+LC5meCpbMEBmdF5dMCR3XlwKCkFuZCBJIHNhaWQgIkxpdHRlcmluZyIuICBB
+bmQgdGhleSBhbGwgbW92ZWQgYXdheSBmcm9tIG1lIG9uIHRoZSBiZW5jaCB0
+aGVyZSwKd2l0aCB0aGUgaGFpcnkgZXllYmFsbCBhbmQgYWxsIGtpbmRzIG9m
+IG1lYW4gbmFzdHkgdWdseSBzdHVmZiB1bnRpbCBJIHNhaWQgImFuZAptYWtp
+bmcgdW5kb2N1bWVudGVkIGNoYW5nZXMgdG8gdGhlIGRlZmF1bHQgRU1BQ1Mg
+a2V5IGJpbmRpbmdzIi4gIEFuZCB0aGV5IGFsbApjYW1lIGJhY2ssIHNob29r
+IG15IGhhbmQsIGFuZCB3ZSBoYWQgYSBncmVhdCB0aW1lIG9uIHRoZSBiZW5j
+aCB0YWxraW5nIGFib3V0CkNoYW9zbmV0IGhhY2tpbmcgYW5kIExpc3AgaW50
+ZXJwcmV0ZXJzIHdyaXR0ZW4gaW4gVEVDTywgYW5kIGV2ZXJ5dGhpbmcgd2Fz
+CmZpbmUuICBBbmQgd2Ugd2VyZSBlYXRpbmcgUGVraW5nIHJhdnMgYW5kIHNt
+b2tpbmcgYWxsIGtpbmRzIG9mIHRoaW5ncyB1bnRpbCB0aGUKZ3V5IGZyb20g
+RERDIGNhbWUgb3ZlciwgaGFkIHNvbWUgcGFwZXIgaW4gaGlzIGhhbmQsIHNh
+aWQ6CgpLSURTLVRISVMtU1BSLUZPUk0tSEFTLUZJRlRZLUVJR0hULUxJTkVT
+LVRISVJUWS1TRVZFTi1CT1hFUy1BTictU0lYVFktRUlHSFQKUVVFU1RJT05T
+LVdFLVdBTlQtVE8tS05PVy1USEUtREVUQUlMUy1PRi1USEUtQlVHLVRIRS1M
+T0FELUZBQ1RPUi1XSEVOLUlUCkhBUFBFTkVELUFORC1BTlktT1RIRVItS0lO
+RC1PRi1USElORy1ZT1UtR09ULVRPLVNBWS1XRS1XQU5ULVRPLUtOT1ctVEhF
+LUYtUwpHVVknUy1OQU1FLUFORC1IT1ctTUFOWS1UUkFDS1MtT04tWU9VUi1U
+QVBFLURSSVZFLUFORC1BTlktT1RIRVItS0lORC1PRi1USElORwpZT1UtR09U
+LVRPLVNBWQoKYW5kIGhlIHRhbGtlZCBmb3IgZm9ydHktZml2ZSBtaW51dGVz
+IGFuZCBub2JvZHkgdW5kZXJzdG9vZCBhIHdvcmQgdGhhdCBoZSBzYWlkCm9y
+IHdoeSB3ZSB3ZXJlIGRvaW5nIHRoaXMgYnV0IHdlIGhhZCBmdW4gZmlsbGlu
+ZyBvdXQgdGhlIGZvcm1zIGluIHRyaXBsaWNhdGUKYW5kIHNwZWN1bGF0aW5n
+IG9uIHdoeSB3ZSB3ZXJlIGZpbGxpbmcgb3V0IFNQUnMgb24gdW5zdXBwb3J0
+ZWQgcHJvZHVjdHMuCgpJIGZpbGxlZCBvdXQgdGhlIHNwZWNpYWwgZm9ybSB3
+aXRoIHRoZSBmb3VyLWxldmVsIG1hY3JvIGRlZmluaW5nIG1hY3Jvcy4gIFR5
+cGVkCml0IGluIHRoZXJlIGp1c3QgbGlrZSBpdCB3YXMgYW5kIGV2ZXJ5dGhp
+bmcgd2FzIGZpbmUuICBBbmQgSSBwdXQgZG93biBteQprZXlib2FyZCwgYW5k
+IEkgc3dpdGNoZWQgYnVmZmVycywgYW5kIHRoZXJlIC4uLiBpbiB0aGUgb3Ro
+ZXIgYnVmZmVyLi4uIGNlbnRlcmVkCmluIHRoZSBvdGhlciBidWZmZXIuLi4g
+YXdheSBmcm9tIGV2ZXJ5dGhpbmcgZWxzZSBpbiB0aGUgYnVmZmVyLi4uIGlu
+CnBhcmVudGhlc2VzLCBjYXBpdGFsIGxldHRlcnMsIGluIHJldmVyc2Ugdmlk
+ZW8sIHJlYWQgdGhlIGZvbGxvd2luZyB3b3JkczoKCiJLaWQsIGhhdmUgeW91
+IHRha2VuIHRoZSBgYFZNUyBmb3IgVE9QUy0yMCBtYW5hZ2VycycnIGNvdXJz
+ZSB5ZXQ/IgoKSSB3YWxrZWQgb3ZlciB0byB0aGUgbWFuIGFuZCBJIHNhaWQg
+Ik1pc3RlciwgeW91IGdvdCBhIGxvdCBvZiBkYW1uZWQgZ2FsbAphc2tpbmcg
+bWUgaWYgSSd2ZSB0YWtlbiB0aGUgYGBWTVMgZm9yIFRPUFMtMjAgbWFuYWdl
+cnMnJyBjb3Vyc2UgeWV0LiAgSSBtZWFuLi4uCkkgbWVhbi4uLiBJIG1lYW4s
+IEknbSBzaXR0aW5nIGhlcmUgb24gdGhlIGJlbmNoLCBJJ20gc2l0dGluZyBo
+ZXJlIG9uIHRoZSBMQ0cKU0lHIGJlbmNoLCAnY2F1c2UgeW91IHdhbnQgdG8g
+a25vdyBpZiBJJ20gYnJhaW5kYW1hZ2VkIGVub3VnaCB0cmFkZSBteSBQRFAt
+MTAKZm9yIHBhcnRpYWwgY3JlZGl0IG9uIGEgc3lzdGVtIHRoYXQgZG9lc24n
+dCBldmVuIGhhbmRsZSBmaWxlbmFtZSBjb21wbGV0aW9uCmFmdGVyIGJlaW5n
+IGEgbGl0dGVyYnVnLiIKCkhlIGxvb2tlZCBhdCBtZSBhbmQgc2FpZCAiS2lk
+LCB0aGUgZnJvbnQgb2ZmaWNlIGRvbid0IGxpa2UgeW91ciBraW5kLCBzbyB3
+ZSdyZQpnb2luZyB0byBwdXQgeW91IG9uIG91ciBWQVgvVk1TIG1haWxpbmcg
+bGlzdC4iICBBbmQgZnJpZW5kcywgc29tZXdoZXJlIGRvd24gaW4KdGhlIE5F
+NDMgcmVjZWl2aW5nIHJvb20gaXMgYSBsYXJnZSB0cmFzaCBiYXJyZWwgd2l0
+aCBhIGJpZyBzaWduIG9uIGl0IHRoYXQgc2F5cwoiVkFYL1ZNUyBkb2N1bWVu
+dHMiLgoKQW5kIHRoZSBvbmx5IHJlYXNvbiBJJ20gc2luZ2luZyB5b3UgdGhl
+IHNvbmcgbm93IGlzIHRoYXQgc29tZWRheSB5b3UgbWF5IGtub3cKc29tZWJv
+ZHkgaW4gYSBzaW1pbGFyIHNpdHVhdGlvbi4uLiBvciB5b3UgbWF5IGJlIGlu
+IGEgc2ltaWxhciBzaXR1YXRpb24uICBBbmQKaWYgeW91J3JlIGluIGEgc2l0
+dWF0aW9uIGxpa2UgdGhhdCB0aGVyZSdzIG9ubHkgb25lIHRoaW5nIHlvdSBj
+YW4gZG8sIGFuZAp0aGF0J3MgY2FsbCB1cCB0aGUgRGlnaXRhbCBFZHVjYXRp
+b25hbCBTZXJ2aWNlcyBvZmZpY2UgbmVhcmVzdCB5b3UgYW5kIHNpbmcKIllv
+dSBjYW4gaGFjayBhbnl0aGluZyB5b3Ugd2FudCB3aXRoIFRFQ08gYW5kIERE
+VCIgYW5kIGhhbmcgdXAuCgpZb3Uga25vdywgaWYgb25lIHBlcnNvbiwganVz
+dCBvbmUgcGVyc29uLCBkb2VzIGl0LCB0aGV5IG1heSB0aGluayBoZSdzIHJl
+YWxseQpkYW5nZXJvdXMgYW5kIHRoZXkgd29uJ3QgdGFrZSBoaXMgbWFjaGlu
+ZS4KCkFuZCBpZiB0d28gcGVvcGxlIGRvIGl0LCBpbiBoYXJtb255LCB0aGV5
+IG1heSB0aGluayB0aGV5J3JlIGJvdGggSVRTIGhhY2tlcnMKYW5kIHRoZXkg
+d29uJ3QgdG91Y2ggZWl0aGVyIG9mIHRoZW0uCgpBbmQgaWYgdGhyZWUgcGVv
+cGxlIGRvIGl0ISAgQ2FuIHlvdSBpbWFnaW5lIHRocmVlIHBlb3BsZSBjYWxs
+aW5nIHVwLCBzaW5naW4nIGEKYmFyIG9mICJBbGljZSdzIFBEUC0xMCIgYW5k
+IGhhbmdpbmcgdXA/ICBUaGV5IG1heSB0aGluayBpdCdzIGFuIHJlLWltcGxl
+bWVudGEtCnRpb24gb2YgdGhlIENoYW9zbmV0IHByb3RvY29sLgoKQW5kIGNh
+biB5b3UgaW1hZ2luZSBmaWZ0eSBwZW9wbGUgYSBkYXk/ICBJIHNhaWQgRklG
+VFkgcGVvcGxlIGEgZGF5LCBjYWxsaW5nIHVwLApzaW5naW4nIGEgYmFyIG9m
+ICJBbGljZSdzIFBEUC0xMCIgYW5kIGhhbmdpbmcgdXA/ICBGcmllbmRzLCB0
+aGV5IG1heSB0aGluayBpdCdzCmEgTU9WRU1FTlQsIGFuZCB0aGF0J3Mgd2hh
+dCBpdCBpczogVEhFIDM2LUJJVCBBTlRJLUxPU1NBR0UgTU9WRU1FTlQhICBB
+bmQgYWxsCnlvdSBnb3R0YSBkbyB0byBqb2luIGlzIHRvIHNpbmcgaXQgdGhl
+IG5leHQgdGltZSBpdCBjb21lcyB1cCB0byB0aGUgaGVhZCBvZiB0aGUKR09M
+U1QuCiAKV2l0aCBmZWVsaW4nLgogCiAKWW91IGNhbiBoYWNrIGFueXRoaW5n
+IHlvdSB3YW50LCB3aXRoIFRFQ08gYW5kIEREVC4KWW91IGNhbiBoYWNrIGFu
+eXRoaW5nIHlvdSB3YW50LCB3aXRoIGp1c3QgVEVDTyBhbmQgRERULgokVSBp
+biBhbmQgYmVnaW4gdG8gaGFjay4KVHdpZGRsZSBiaXRzIGluIGEgY29yZSBk
+dW1wIGFuZCB3cml0ZSBpdCBiYWNrLgpZb3UgY2FuIGhhY2sgYW55dGhpbmcg
+eW91IHdhbnQsIHdpdGggVEVDTyBhbmQgRERULgooQnV0IGJlIGNhcmVmdWwg
+dHlwaW5nIDxSRVQ+KQpKdXN0IHdpdGggVEVDTyBhbmQgRERUIQo=
+
+--16819560-2078917053-688350843:#11603
+Content-Type: MESSAGE/RFC822
+Content-Description: Going deeper
+
+Date: Thu, 24 Oct 1991 17:08:20 -0700 (PDT)
+From: Nathaniel S. Borenstein <nsb@thumper.bellcore.com>
+Subject: A Multipart message
+MIME-Version: 1.0
+Content-type: multipart/mixed;boundary=foobarbazola
+To: nsb@thumper.bellcore.com
+
+--foobarbazola
+
+This is a text prefix.
+You should be able to read this, no matter where you are.
+
+Once you've had a chance to read this, interesting things should start to
+happen. In particular, a picture and audio should appear,
+more or less *in parallel*.
+
+After that, an atomicmail message will ask you a few questions.
+--foobarbazola
+Content-type: multipart/parallel;boundary=seconddivider
+
+--seconddivider
+Content-type: image/gif
+Content-Transfer-Encoding: base64
+Content-Description: Bunny
+
+R0lGODdhQAHIAKMAAP///7YAAP+RkbZIALZtSP/akdq2kba2tm1tbf9tbW3/bf//bW1t//9t/23//wAAACwAAAAAQAHIAEAE/lDJSau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBI5D0OyKQy+XgYntBoc7l8FK/YrPAh6Hq9x2X02Sw/BugB
+AOAcU98Hq3ZOr5+aazY+z++3x4CBBlNwYVRydomKi3+AZg8BkZKTkmWCl5gGSIRwSk2LoKE6jxKNgQdsCaqqD2ummbCCm3GdtYiiuLkWR3xJfb/Av0jBxHlIUrGXtHJhX85gTLrSobzF1tfY2J1NmFwC0Rjez1+b0+Z0htnq2rXtcR6Etx/iX/Ln96PuqOv8
+xvrt9vAJHDjBkD5j/fr9cxeQoENdBm1VeKVpX0IAx6BEXLjR08OP/rmOGBxZ0FMyQR0XFlIZJ49Bdu9AygSSchOsmiy5RWF56GLMmUBz4Cw0ZijRWDzftFq3FGPDoFBRGN12KYyhq0iT2vL5NOo9iiencgR7EqVWjgnLeSXohGzZt3ADMdNpdpvbsl3X6tV7
+N66jvYADF6wai2JewYgTK17MuPGoXQrsWTnsuLLly5gza97MubOMMjzLeB6dZSjKpXQdASTNukVTPXuKbbIkxczOpJRb694l1w8lSn2TcZolcbfxC20zoYJUJkATVhj9wiIuFtxxwcOHqVPi6BFe6U9ms0kiwMzq61B5KbnInl3worR+Rj4w7plV9DLDtN+P
+EXe4cWpt/kDPM/LhJ81G/KV1Vm4GOiZWgtudZV2DmVX3UxsWQbjGAeFJeAiFmE11i0hlwaGah/ExNFFEDIIokBnIHYIXikpJ+Np2Lbo4TXWGcWTTSR7eGGGOOipioUnwcZQMjS0pJFKRPzxCSghHVgGeMkz2pFCBUMbwXmGg2SXll35VmdN81nhCZJczjOnm
+m6KNYspsZMa1Jps61vnWnXjimRoZmTTCZ5+E+kmBZJIVqqgJU8ID46KQRirppJRWaumlmGaq6aacuvZmp5Rmh+KgoFY2VVE3FVcqZ1LSyV1RTQSQhhqunKLiqg4OWM8er5IhZRq13sYRrosJmU2dog5LLGCC/pbhUmyuMHflIB3VROqyopD1GiS/TaKncp4A
+dC226EzHbbeVRDfItFCEqyy5+PS1HCurJNAKh+zKEl+V48JbA2h9qAlWhnwE960bIsVxZL/+3kEwhBl12Alh4C0zHxL1ZcdwwyVUs9/DHwPql8WlYFwfNFxyTIPHGrYc8J/dVLHLyeSkrDIMHoPsMkz/zGUGgTYXRPM3G9+sAcs7y7bg0c4E2IGuXQRttMO+
+JM2zSkU/HefUK9S0odXCBMn1gQeBHbaNY2e7EEJm9zdq2kZibZPOEGcpNdxB8HjobRpGbLfTeBMh4oqYrJdmEhqZycStgQ9h1Ig/Tqv4Sj1f7Eey/482rkLCi8vHop1/E5cTRjjem7XmGVhbWOhOBMkV6jY8XpVWthW+IHt3w051zyfmBKSNuJ8Ou5mvLLwk
+jcbKZrruLCgO1khHTZdl8ml+wnwKkxtce1aspyX8pDCn+maQ+Qob+iYKfr/owVIgz36g51fBz4Q3/xF++dK5CZdY3lUV5ogbydzw3oe/bmyNcHI5BAFRcr1iFVAuDUzMAm8SQcVMkIEVZMzzbvKXDFaIFDAToAdHmLfmkbAxiUrhCTcjD0RYz3qRWeFl4ETD
+T8nwhjjMoQ53yMMe+vCHQAyiEIdIxCIa8YhITKISHRemQhxwiWyZHBNgCEVcNFFMY7KF+v6q6IJkyWh12+AiOtxxGviFUYxFMM8b5MKtgtXljGg0gq6gh6/amGFWwWof4+J4A6jVQwx2vCOw2OCGd/GxTa7YFbT8gThqlWGQczrTIRF5kfdwY2mT/ExCDua8
+TL6gDdta5OWulBJrebJrvSmYc8xQiXwFUIunvANKVImuSFxQYqIrRCxHoC1a1tI5BazCHncpIOWc65f3Wlf5ZjMWYh5NmbJgA7qcpa5gdq5nW/yhvFyxigd0c0MPbNe+JOnMvSknFfWyFzjD2S6rhGaXLCpF4aJFTUKyU5xT4JcYW/WGgEXMEaMszLSWYRUv
+fkiJ+klQI0l5JfEYRJHVQiLSWv62hMP9E5oje5IhaDacbOLtJR/TTnsy8j6SNWNoV5FohujWtmBw6GCzgNzQykM/IKajpfxBVkoPNVOa5g6HE8XplqZYmG/UNDI9/akMWcZSoTKSIcUTx1GFNjSlnrAaTW2pfyZSH8Ahh2ZWvWpWwYbJrxKIQX4kWhAT6tQN
+lTV1UiUS1MJawZu21a20AwEVqUQPj/orInd9KjnLyYGUfA2nYiOsgMrWNuQpNnWMbSwqEvvYQ/2jrY6tbMkue1izMcmvPKxOLySbWcUeya1a/exjLcSM8JC1Iqotp94G41qX4Yt1sq2cOcWpUPPJDY6n5N9EsDRWf+oRN+cJrm5LJv8vw/XiolLMJXAPJUIj
+Dq4kcSifQXOyXE74Y0xErJZHNhu5MkX3EGOxiBofVgXQwotzooMckvZ3voW5YqxN2pB739vEvQZwT/Wl3Zb2itDopepv0aVe9biIk76cV03ivJ33qjgUt5yXNoAA3uuWKKI36mN70pOwkzispN5h7XciHrFKT5wk33FPw9mwiFfXipsWlzjErltHS2Zs0xrb
+mCHHQ56T6LpU2iXwml/EcY7n96QiOm92KRpJkD87YesuyIDtDU59NxzeK4czfgpWnuYwzMZG7UKKt4wwmKsMN07WN81kiB8T2Dw2Aj64vPi78zbS12Z2JRjOx5VzUF06mz6kJ86Vb7pnzEIzvoOyFa90ahwoFZ2/55EZylr8lgilFMH7URo8BNaWEwu431VJ
+Sbs1THVuwscJQGtEs33UH6VLDWvaKprWtY7Mp3Gda1e/Otc98PUggO0DbQmng8QONkoEWpRk523Z3Wi2s5kIq2gTeNpz4DS2PajqbvN625Gp7tHEDW4ctLAgcylIuQUHGRWu23Htjve7t3CoM0Nm3sE2Ib5zEQEAOxoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+--seconddivider
+Content-type: audio/basic
+Content-Transfer-Encoding: base64
+Content-Description: TV Theme songs
+
+8vp7cnRvc29ubWxcUebUX0tM1N5bauLZ5ejv2N5772hiX1lVVFdZWWPm0dDU1+LubmZhW1VXXVlfdOnZ0tHPz9TY3/ZgV1VUV2Lw7e/0cmRcV1FPUFZeb+7g2dLOzc7U5HJfYFxYWF/66uni3el0Zl5ZXFtdZnPx4djOyMXGz3FJPkzjXFVf/NfX2dzf5vj2
+fXn39OLe9k46N1zAwM1maM7wVkxLW+bQ1dzi49zW0NDX4mBAOVfHyNDcdHfe4mdbUUtHTnXYz8/U09Ta2dTQ7UhJ1cvnT0VP//fd2O9WRkI+PEVlzsC/yM7PycG+vcpFOO69zF9PbOx+YFJJOzY2MzM8X8i7u7y7uLOxtsRLQ8m+2U5KXurcWVNRRTs3My0q
+LkDZv7+8tq+trrHGU9i5vtxUVW3ldFJPRzszMi4qKS9Oxbu8tq6qqay31Me3v9tRT2p3X05IPzwxLi8tKSk0aMO/vrSurKuvvb+ysr/mWGH1Z0tFPz01LS4vLiwvTcO5v8K6tK+wt7evr7nJ6+7qa05DPjs4ListLS0uO9C2tsDBvbm1trKur7W9zeF8Xk1F
+Pjo3MSsrLSwtL027rrK9vbq1sa+trrK4v8vacVZORT88OTAqLCsrLDFnubK9wLixraysrrG4vszkaVtaW1tbXllDQlBd7GJLQj04NTg9RlJgeuna0tDV2dfQzczJxcPL6M2+u8VZPDQxMDM4P0pQWmfu3dbb4ePc0czIw7+/zta+t7rdRDg0MzM3PkhPVl58
+4Nra3t/b0szIw7++w9jNu7W67EM5NjY4PEFITFVgde/k3+br4tnPy8XBwdJq28a5uc9SPzk2OD1FTVRn+Ofb2Nbe3tXOysbExMjn787FurnFbEE5PTw4Nj1aU09f4dbZ29nLzc3IysnReOrWzsXBxd1NOzU0NzxBT23h1cvL2e9pX15u8drO1tvN0c/Gv8LU
+fltMRkM/Pj08PT9HU23s3dbPycS+vsnMy8vBvsV3RDkxLi4zOj5FSU/XubS0srKys7S2ucJhPzQyPkc5MzAwMC8xOd2qqauqrK6xtLvLTzUxRfpMOjI5UkY6MzNdr7K2sLS1u8TdYExGPjIySeXc63xQPT7PrbPFzMjE2mlNRklOREJKbdTV5djFy7usu9No
+SGzb/E5EOS8qLj7nx7/DzNi9paSrtEgzPWDyUz8/PC4wOkZ76lI/SrSpr620vs5RP2DK7z88Uezc1NPV3XTCr8htSzw0KSQkLlXi139R6b2sp6y3ppicqLfhSzcnHR0jKikpIx0dJEq1qqOYkZWXnaq9xHArJztMQTstHxoaHy05b6uorKeoqbG72i86p6m8
+v8s7Jyg38MG1pqm7zkU1LSceFhEorautyllGxqikn5yWmaKpuMZWNiccEw0bNC0eGB03rqGfn5eUmZqfpaiwyz4qHBcoRiQgKC09Rz47YcLHr6Whpq+5x1gsJXGtbU+8vMi4w+G4sdZeRDUtIx0bGxoisaO6v6edn6OjpJ6Yoayyy1UvIh0XDg8pLRwYIEW8
+pJ2dmJGVmZykrMBlPCkaHOndKh0eKCg0RztHwcy+qKOhqa+zzi4uq6fSODhvbLevytO3x0Q9LiojHRscHB6/n7G3qZyfnZ2mp6CmtLrOVDYpHxoVDxcgGh8u5K6kn6KdlpWam5+ntc1FLiQbHicocM/qOygrKCxJW8+rpqu0s7nLTC46XsiioaSwYlZKSr/Q
+OjYuKSMeGxodJFN41p2Xk5ijpqmpn6Kzvuw/LiciHBQQGx0YK1Gwo6alqqeZmJydpK24yVY0IhwrLx8rab7IRDw2MzAyXL69zNrOz+o/M1TyPGquop+lqrC+3E5PRzo3NS8tLSorOT452qugnqSqrrrMYVhZT0xBNzQyLisuKy/ptauorK+1vcra19R2WlBA
+Pj85LiouX7mvrK+1vM1XQT9LWFdeX0pHU2VGLzq+r62vtbq7yVA/P0VAOzk5ODY6REo3Pbyspqessba7zmpLQj04Nzc2NjY+Tzs317aurK61urrCzuDreUtLcFU+MDlKNC5NvbOwtLi9y9J9VFZST1Rk6eh+7+pnT1RcUklITVNVVFlb69LPzczN0+NfVFRT
+Yebc5+PQy9V9Tz8/QUBBSVNq4NXW29TFv8LKzczS9XJgTlVkYFZPU1xeWVJJQUpia+jIt7O3uLvJ1vVOPDY6UVtPT0Y+PD88OtKnqa2tsrrCzWxFNS82S0o9NC43QGmsqq6ts7nG12tJPDg0MDE0Liw1WK+en5+pub/rW0Q3Mi0qKywsKzA/r5udo7XMyLmy
+u95EOTIsKSgoKD61tbndW8m8uLu5wcv2SEg/Li25oqeuPzVETE1HPzguJiMnMdqinZ+fs97NxctNKiQzVEw6PLCdoaOseDMuLConHx0fKCwsUJ2Sko6TrkVV2MTORi4mIh4aH0tsWL21JB9Vwaiip6+3ub/UrqWuqqmlvRweIyQuMjI1NTEwOE3NtKujnKo0
+PklNTTEtMUJX07mvrqytr6u9MjhEQTUsLC88OjM+3rmup6Kr32vYzb/eTktGQjcxOU1KPWLNZM63tLS1wOZj9tzg53teSkw+KiktR7musbi7yd3i3t/Px8TXPDU4Nj5GMzXmvbW0tra5v8LBWz9Z3s3ZRDUyMDEuOtK2r7GxtcrWw73E4U5JSkpDOzY1NzI5
+5sO+tLG1u8PN0971eG1eVEY/PDY1Nzvit7KxtLi+xcjS29vwYE9FQTs2OkA+O0nNu7i4urq/xM3o1NF+b/5PPjY2Ozc2RM+7t7rBwcnT0dLS315TVEc9OT9NRkltxLq4uby8xdHkdmtcT09iWUc9PTw3RN7Curq7vsHNbU9e3+lVSU5QSkQ/OT3rwrq3ur7F
+xcXM3X5uWElHS01HPkBDTdjHycu7vsy+v83T5uP9WElHSkc7NjU9a9PEvr27vcfSz8ra6eBeV1VJPjg0LS5D4sO7vL67t7m+w9j5Vz84NzU1Oz86PVzOt66xtri7wdlXOzIxLzI7Qk/jwN9S1sW4rq+6zFY+OzYvLTZGx6qwsLfZ4Fp1Tz40LS4vLzBFpJSd
+p7G4v95JNSwgHBwcHDKmk5mbn6Wnv1xNNyQbFRIdapyboqOnpamtu10uIB0YEzucrKmiqq/E0EAxKiUhHCmgmaqfoq6tqLM9Jh0aFRTVnb3IzK2oqqmlsOc6Gxyfla6+LiwvKSMgHxwsNs2LiJGWq1c0RDMoHBURDxDOj5idqb6oqK23xkMqHS0mtpK3sD1J
+Yy0mIB0aLKOnNJaModREqq/VQSsdHMlcHhlFqigpX7Osnpmdp5edXSQznz0ZEBMVDxgptI+LlKipi5arQxwgHRgREtCdrkYkm6RPyDK2rrr+O7OqKy0vmYytzhoZHxgXFiqdnqw0OI6RlKVduzMlHyTMwyYaCyTCITy8kY6Ynqygma88HR2j0B0RDRIRHzuo
+jImSoUuVjarvGx4ZEhAVxJejbBospOinra2lv1kuM8m7s8y0jqVYIBEcFxQRFryWqzHFhYWPmjQ7OSAeHSTcKhQMFD3kl5mUj5ums8CioDUbGLQ9EwwX0jkuKTWljJGfspOOujQWFQ8aXmi6np80GSasQrexsq0+LBkkmY2YsbeTsS0dEx0XEQ4UQ5+voY+C
+hZepJywpGhoaHTUrDwwZtpmMjpCTqMBSMuenNBstxB0PFzuwTSQpQaiNlLehjprbIxEYLs3KzjvCxhgYS8i7qrmqxRwfr5+Zj6ZmoqZKKxcZHhALCgwui4mTiIWPnz0tVysZFREPHRMKZpOcl5KVk5u2bz0pP8MbH14hSq7IttMnFxQgsZGdqo+atzISJaKg
+r/4tI0w5Fi9fJ3Dfy7I1LKqZnJaTuradzS0bFBoTDAkJFZuBh4qEjZ3BJSksIRwWDw4VGSeamJqSm5+irb5JLSJIyBoiuq2gvTpbIhQTHUSfkaWfkr0mN06lobnELBsbTi0dPS01ukIrsp6XlqaqnKNNqaw1IRMPEgwFDTKbhYWNiouYqkIkJyEWEg4ID1TL
+nJarop2fn6PDXk8mH0EdGaealZ7wNyQWDxEb4JSWqJmrL2ipl5arSycbFik0HDc3HScrLquUjo6ar7WlScGpQCgZEw8IBQ0jo4uEio+MlZ/DMzAkGg4IDBy8pK2hrbuon5uisFkvLR0cSr6elpqsPiwfFQ4PGzSllKq+nZWZnZqfvDIeGRYaOyYaHxwbMpmS
+ko+YpLTWo6d4tPooGQ8LCA0VIKSQiIaRj5Ccqc81HxQLBQse9Jefs7ZOtp2YnKXRLiQZFEKopJGSotopGxcQDQ8dwpKcPKSRkI+PnbUxGxoXFio5GxwPDDiXj42Lk6K0Q7OdutBOKxwLCRYcFxcvp5GEi4+PmqT+MSMPDBgcGyOdltHSL02pnJyqxzkaDySf
+k6KalavTLB8aEg4OEyG3n5qNjY+QkKC5TSIaExIYMiAPCRG2l42Oi5KfrlhZopu8RyAXCggXIh8eSLuji4iPk5SbrjgaDAwdJyIksJe8Py1Bs5uYpbgrFBo5s5SYmZi5Xi8hGBgRDA4PG5SHjI2Tjo+hyUQsHxsTDx4eDRsmLq2WjIuRnaKvzLWr1zUaDhwt
+JR0aJE3dtZiTl5yoy0InFh4+P0s8Sa6pu7LC676yvzkiGSDBt8V3zqOhvLjCYVI5LSgqJSTfr6mwvqScrLPIRl9fRyseGRs6yLm3uamftbOst7hmSEQyHxwuVFhFNm+lq6usxbi6w9c8KCw8Pz9CRv2xy9WzyMXRzsA/IyxV2MLGZFCrqcK52dNNNzUpIDDV
+z+rTwM2ooq6sv9FDMjIrICQ1Slla19Gqn66xvbu5zEQuKCw+RUpNRzlau8KyrbO3vdNIMjFgyM9uPDFJsbnXzM/K1WNHPjZARkJc3tvXraq0uNdnWkg4KiEvatbZ6uXYr6mzrrO9zT0vKCUvOz5W7F1OuqSqp6/Q3GFPNC88V0c6Nzg5XK+/vLK7vsz4TE5P
+XubX1FY1OMfP18dcTE9ANkPZu7rXYlVMZq+vvrr2RDUwM0FORExd8fTj46qktLjOxsVoODI0NDQ0NzxOUbirsqqtt8hUOztFS0ZFRDcsLNOptbq8w8nhXERdzNNNQljuTd2uwVNIRkdESkZT89bDv9L0TPStrbjA1Fg5LCYvWHZYYllcWM2qp7G61EdFUElF
+RzwzODw6PE21qbu9uLe84lRT8ONPT1A/Nis3yMe6vcbL18zGv8Pb6Gs8NjvhtuNNTlZMPEJa29JZVsa7x2PVsLa6vuFSNi0pKjdLbNzrZ92zo6iurrz9NTI7SVY7LjA6OTtnr622uMPCzdtiW9nE1lZJPDIzV3pS1MvUalLPurvCy+tVSTk43rbJ3UI6TkdE
+ZdTd09bjd1dXw6+6tLfZSz04ODYzPklQXk1GvKOmrbPG3lQ+R0c6OzgxMjo9U7mysay7zn9d295MUM7HbD02O87F481PQENATcO7x9fp+G585L27xb12NDM/R0pCR3bIxs/19r+0v7yyxVc5NTw+Ojs+PkdbY82pqKuy21pIRlRNPj8/OC41QVW6tbSvuL/Z
+Vunac1ppz19FPj7Sv9fuSD9GT3jwxb6/xllBT+S2sMLIYj4xMz08SElX19997+W5q7G7035SUk45OTtIXkM7SGXDrqywu95BPkdNbWxkfk47OUTkta+6zGts2dbpX1FLRUNHR0pXxrm9vdtPRkpUU2F/XVBKREBPs6qxudNSPkFKQkFEPj9AaMrPuKyvuM5s
+T0ZYW0w+PU9EQUZcvK6zudFUUUxFP2X9YFZER0laxbi9xNRVS/nvWv5r4O1HRkhOzrrA9/RxUldJTevaz+NNTFxs4buwvclmTlVGQkRGTEI7PEbsw6+rs7S/1+tPRkJAOzQ0O0NJUb2rr7jJaWRaUkVDT1xoaWXy276zxX9MPkVCRExh693Vy8TJ1Me2vMpm
+Pj5AP0BNa9LfTkdNb825uL2+zvJYSldfQjk0OD5JZNC1rbC1v8LGW0xKTE04LjA6S3LFtbe1wu1eTeXR3OllX313ee3Cu/BFPERJPkhf7uZeWeHJvr+6tLvUS0NCODk9P0xVbGFSUdm3sLa+xtftTUhSZE88ODhLaN2/srPAzt7Y39zkZ0hDRz9BRUtjz8K/
+wcrdVkti3PxiXVRWVVTexLrBdkE8Slnt5FpKSUt/zb+1tbO+zO1KPzw8PDs8RlHi1eLZv7Cwub/Pb09IUU5CPzk2OU7TyL7BzMnT3c/IvcVePzw/PT5LWtjP587V9nla+dfY4/vrbvrS0sW/0dhQOTk/WupuZGlqb9G6uLGxzl5JPT4/Q0Y+PkNPTmrJysO3
+t7rB08zZ/VlMRTo7NzQ8Tty8vMDM5cu/vcTL6UY+PUNUZ97CyvpvTU3n1GxWTEhe7dTEx8i6udJlRzxBR0ZNTkteaXjJv7+3s8dgQzpAWF1OQjk9RUjrw7+9t7zHzdDLyNZnTTo3OTo/Uu7QwMHKzdvVvsDM0vNQPj5KWmP21+BPUFlOW3T38/ztbGTVw7/G
+xL7cUEtDQ0RLT0FARFHozbqzs7O/3W1gZUU+QTk3NkXt7tLAuLO8xtLYxdlfRz5GPDo/QU/qycJ/UmfZyM2+w31oT09ZW2xfbtpnR0Bezn5282T8du3YzsK+v8HE0FJLWklISEFDOzxK9cy/trW3vNN452ZfTz86NDQ7XOXnzsjBwcnLx8fHz3lLREM/R0pN
+bdvK0Xvx08nIxsn0Vko/RVfy1N7l6mBr2M/pZGFORVb8283Hvsbb5XpYVFVNQT5BQEZHWdnHurS3vcbR325tU0I9PD88PFDfysbLx8rDur29xtxPOTs+PkVQZ3T4zt/szsa7x9PRf1NNR0ZKXV9Y8mhkYl7Kyu9oW1JV28fJwsfV21lVa1ZfXE1JRUhJXd/M
+vr++t7vEzXhtWVJMQj8+SU1R2NLs2NnLvcbLwXde70ZDTl9PUU5DT2rb4Xj179rgz8fW3HtUYGFRTkZKd9jb2Np7fOt83MnEvsthUEVDZsm/yOpKQEZBQ09X2cjKv75n4sDL121NQD42NELYuM5fz1VE8ca5vL/Ob1Q/P3RGP9njWEdMVdzGxsPCvcDKzEg/
++ExBRUFJQE/m1sO4tsta2Ng4etlIT0NIP0Bcyru2t87sw080dUs7R01ifObj2MK4tsTnz0cuU/9HXF1ZWlV7yLWsrbnS2TorOzw2RWnv7P9u3Mm8tLS3tvo0PDYvP3FrUkE6S8S0rquts3E5Pjg3Rtj7Sz4zOP26sq6srsBqRzw1MUvg+lc9Nk/Ata2ts8Ru
+Pjc5MT3h3l46LTbMqqGkq7t7Oy8vKi5MUFA+Lix3opyfpLHKSjUwKClHWEczJyA4n5aZn67KRS8rKDPO0U0wJh4mrZmbn63CbjYpLVvBy08xKB8fWp2bnqjEUi4iNrOst2oxJh8eL6qam5+xezUiLbyzvN04KyQgLr6inZ6mvkclIGOyt8M/KyEdJdShm5qf
+uE0pHS66rrJgKx8bHTmnnJqdrdE4JivPrK7LMCIcGS2km5yers06JibuqKe3OSUbFSWgmZyertwwHyTIqqq+NyYeFx+klpqfr8ovHTCxrrDXMyIdGRu6lZabprkuHPimr7tfPyccGBkxnZecpLc4H9GgqK3JVy0eGBgmuZ2coro3HT+eoKW00TghGRcidKad
+n630HyGqoKOt2zsmHRgeSaKXm6jMKBxqoqKnyzgjGxgcRJ+WmqXELhwwqJ+kxDsmHBcVL56Ym6bBNR02pZ6fskkmHRkVJqCbnqfFLBxKnpyerlEnGxkXIKaanqLMJB+5nqCltk4qHBsdH7ydn560JiS4n6KswT8sHxoaH2Khnp6pLCK9n5+nsWItHxoXHT2v
+pKCkTiDjnJugsN41JBwYGzSyp6erySQvo5qcpb8/JRwZGSm3oqGrzSUhtZuaoLtEJRsbGiiunqKszCshyZybn7s6IhscGx+0m56kyiYivJybn69KJBkZGx7Fnp+k6iEurpyaoa1LJRsbHR9IqqOlWx8+qp2bpKtxKh8bGh9Dv62juypDrJ+bo6rlLCMeGhw8
+xLqnrzZDqZ2Zo63dLSEcGho8uMyvrUUvuZ6XnavIMiYeHBktuMe7tkor96GXmqazQSgdHBwrxLqxuEEiNKiWlJ+tRicdGhokzaqotDwgO6aYlaGuRiUdGxsjS66lrjkj6qialJ+tVSYeHBsjR8Cxch4eZ72kl5ufrG0/Pzg5PDQ6LBccT8momZygp7jNc0ZA
+RjUuKRgaP2y5npyfprbI0Vk/RD8zLh0YMNm1n5+oqrXI6UtJ7WM0KR4YJ9K3npujqLTH2k1FXVw6KyAXHVa8o5yjp6+/ye7mzt1ALCEYGjzPq52krLvMx9XQxMTXQCYYGzNTuqSnr7rMx9bOwMncRygdKT5YuqSjssHiytxx62BtSycfMkz0u6WgrbjZYEhE
+TkE+OyghN0xN1a2hrbztWkxDTVh53zkmOWFe4ryruutCPkpjwb/SytIvOexR+8uxvt5bTlhJWkkzMMxeObm6uLarr8LkRjswNjkvNsYtKLitqqihpbjQRzQoIyMqbkRHy7Oprq+rrNE8KyYkKuitrKG58LfL2VtXUiwkIiM9sqGVmZ+zejskJCQqLSMn26ud
+mZ+jrX4mIScoIiAttJ+cmaKrwVw1KCIdIR8iQ7uWjJWet0gzJyUkJB4iSSykj6Gdq7hdKh8fJy3Dpjwpm5WotjU9LSEeIiRtornp2I+VuMZBKhYXHzG9obPROcuQlK04HBgZHiuxnaOo1z+fkZ83FxwaGSC1nrevyUyejJNIEBIdJihQpLi72ii4jI+tFQ4a
+ICzDmJ6uxSw2lZGlHAwWGiM6npmkpugtnYuXRQsOFx4ut5uqq8c5Y46NrxkMFhkke5eaqbRBMKiLmyEMEBsfMqqbrKetWbmLkCcPDhoeJGCdpLSvSU6Miy4XDhgpMtafmquo3DWUkSAaEBQkJDewlZygqlaYjDcaEQ0eKStTn52mo8CqjK0ZFw8dKio8oJSg
+p7jUk5YdFA8YKSgvupacoaG8nY1tExISJSksP5+bqqO8rY6lFhIQHjQvOKiUn6Guu5GdGRMOFionLOCal5+fo5ObJBgSFCspISutl5+inpqdQRwYFixELz3Bm56rn6qy3B4aFiA6LznlmZCeoK68uSMZFRw6Li5Ho5Sfpp2lxSYZGBs6OTFUspiYn5+lqS8Y
+FxUkNio3SKKUmpmZnMwcFRYiNikmKdGcoJ+amKkpGxssRDE0LmSfoqikr7QrHB4kPEY9LnObmJaaU8EqFRgcP2w9OOyel5SWM7y8GhURHTNE0bWek46QQT+uIhcTGSEeIjezkYuMo+CkLRsZGSMmJiorsZeRlbikxC4kGyk2OTMtyZuaoTRMvzQuJ0NgPDg4
+xJmTls0lKxYXIUW+v7/X25+Qj6YsORwRDxIhSbKmoZaKjKgvyzcZEREVFRYmrpOEhZm5rb0mGRocHBkcISacipihmpi6Ix4jKyooNDetmi0Y6Jaaxj85MzMvRcOhj6oaGiYnHje6r6q69bygjZI1HywtGw8PGEmkopuUiYqyJTRxMRgQERITFzmYhICMxtSw
+0CgZGhkXFBcer4ySop6YnMMoISMkIyQor5U0FSKnkJm1RjMpLDxdn47SHBweKCZftay0uL/En4udLispNiEVCxA+pJ2clomQTlPjr+oiDgwODRBlk4SAl7a2pqHCIBUWDg4SGVmMkJuamJKgRh4lHRscHj6bzRIfooqNmzgwJR4qOLSToRsYGTFvsLS4tVxn
+VqiPmUcvKDdAJQ8NHtagopWNmLHJybq0zxoLDA0WKJyJjpKcpaShoUYSERITFRk61ZqRk5KUlqsfGyEcHyA9Lh0cM5iNi5HiIicgLz+8uDEoJCooxZKcXt86Rlyvn7JqNTg8UWQcDCO0qJ6clqW/ybSvsLUvDgkMDjKWjZOXoaeloJutHAsODxMgMS+akJKP
+lpWePRYZGhorKxIdNd6Zj4uMniQeJSZ6+RogQCs6PqmWl7pLzT7Gsigot2bMV0BLMx8d3rOfmL8rs7Gpo7K5SRwKDBRHlJzGoJufm56epkoPCw4PISYqn5GTj5SWm6clFBcUHyEPEi2vjouMjpfXGiEiOzoVEyUsOMGjj42bPjEuTn4gIjzZROZbSnbdNdus
+o6wnKmykqKisxk8uFA4eRLP9wKeam5uanqnIIg4QFBYWId6Xjo6Pl5ykxBwYGxgOCxA1npiNjY+Tny8eLCoaDg8cT2SqmpWRlq0yV0YnGRkmw8jLwdrGsad+vKs/Ih8uqqGmpLpiMiYUHLPZLjlApZebl52pvEEXDhgPDyXHnI6UlZabn6c5GB4OCAsPLZuP
+jIuQmJysKjIdDA4OGD3IqpaWj4+W2jk3FRcWGjy1v7jFsZ+fs7uuKRwcIfugp66vv3pBLSxKIygvPbeamJ2dqb9SLhgZFRUcIrmUjZGVm6Grzx8YEwwNECSpkI+PkZWapFUfGA4ODhMorqCYj4+SmKc1HxkWFBYh0b3FsKSalZW+KCEeICMvvKu0s77Cw69S
+Ijs2MDFBqZufo6+9wc4mDREdHj21nZKXmJuiqLQ7EA0PDRMoxJqPjo6Vmp2oHg4PDRIWHs2cmJmTj5GU6xYZFRUWGkOuvq+dmJ2VnyImIx4jIjq1zcCutrCephseNC1HScigoq+svr6zVhIOHR9Dw7ialJqanqWoyBcKDQ4ZLEadj5OTlJaXni4NDQ4QFhcx
+nJiSjpGQlawbEhQTFhgeZ8C1mpeTjpdGGx0dHyMjRrzCtL6ym5tIIiotNT9KvqWsr76/rskkFRwdKr65pZiYm6Gtq8MtEAsOFjo41paRkJGYmqbgGQsNDxUZKaGZmY6QkpefQxEQERMWGi7CqZaVlZCVpCQbHRodHCbht7Kyq5ubn1QkKyQsLDC5rLSvtbrX
+3jIcJjZHa0ytnqShqLC8zScNDhsrQcSilJeZm6WrsUMSDRESGCa7m5OPj5WcoLMdDxQSFRkcR6eYk5WQk5jOGBkXFRscLr3DtKiZl5ufMigqHyQgLbS2u7C9xrioPB87Nz0+T6eosKqvt73qGw8iLy/luJ6Ynp6kq7nLJQ8SEhQnY6SSk5SXm6KuQRMQFBQY
+FyWomJaPj5KZqiMUGRYXGB/ryOSnl5SSlbkkIx0cHCF8tMS5ubqunq8yUj0wLCdbr7avsLS6zjEYHzMxzMqqnKKorbGyvEkZDhEUJlC0k4+Ulp2ms9AhDxESEhUdrpSWj46Rl5/+FhIUExcaL8vOoZOUk5GcMRscFhgbLLm/urO9qZmavDI4JyEiKru3u62y
+ssX9MiMwP05iXqqir6uvs7/XNhMOFiRltpmQmJufqLTDThgNERAUG9mUkY6OlJqksSUPEhITFxxQtKCTk4+Ql7QaFRYVGR1EtsO9u6GWlprOKiccHR0ysrSxub6+va3FKz1LRzhCra+2uL+/y+0kDxgxR8+plpaeoq++y8wxEQ4REhxmmpCPkJaepqxxGQ4S
+EBQXKq+aj4+QlJiiMRQTExUYIsi8uKyck5GSpSUcHBgYHW26vrO5vraenOw4Ti0kJOK0xbm8ws/NShwZPE/QuqSao6att8fWWR8ODxIdWKeQjpGWn6myvDARDhAQEhq0lJCOj5OaosgaDxMSFRcywsOilJCTkZldGBgWFRgqvMu5u76umJGhPS8sIR8ow7+6
+tcj9S1M9Kjvi6dbPoaCoqrbBbEYtFAwSH+Svl5CXmJ2kt81SHw0ODw8Y1ZGQjo6WnqivOBMPExAVH2LLoI+OkJOZriIUFxMWHT931b29pJSQl6woJh0aIEPC3Mls6U5OvaxNQ9BATrqnr663w1s7NCEPETLjvJ6TmJyhqbLD2TEVCw4OF8iSkJCQmJ6pr9Yj
+DxAQDxguwJ2Nj5CUm6RVFxATEhorTUDbu52Pj5GcNxkcGRoq3+vhb0pO/6SWqDFCMypMra+8uczYQTwyHhYuz22rm5uko6uvv+tEIg4KDxjElZCTlZuhqLTBQRgLDQ4TJ66Wjo+RlJylvCgODxARHj1JT66XjY+Rl64cEhcVIE31Qko9Trmbj5fKJikgJ9i3
+wcToT0s7NTw7MUlP3KifqKuwubvPRS4bCw0f052UlZmdpamvv3stEAsODh2sko+Qk5eep7DrHA0PDxUrOUiokY6RlJmjPhQUFRctPC82PlKulY6Sni4dIB49vt1hSTw4OT/Cn7IuQznWpaqwt7vCXzsxKxgOJciwmZeeoqeqsL3fPB4MCw8Wr5GTlZaanqix
+wjsVDA8PHDRDppGPk5SbpLUnEhUVHDQuLzZPqJKQlZixHxsfJuXmRz80NTY/tJiaSCw6Nbyts7bJeEQ1Li4pHSXQ1qWboKSprLS+cTspFQsOGbySl5eYnaGnrLriJw4LDhQtX5+QkpWYnaSs3xwQFBYlKyg3f5+RkpeapTkYGx428jg4LzE0TKmWlqwsJClG
+rr2+zVY7MTAtNVBLPEy7n6SoqK62x2k0KyISDBrOmZiamp+lqqu7dTUZCwwRH8udkpaXmp2lr7g2FQ8SGSorNtCdk5WYnKO7JBgcIz44LzAvON+flZed0h8gK9C7ztRFNi8wMkyrpEAwW7ipr6y3xd5PNSsrHxMaz6WbnZ+lq620wvNFKBMLDxvNmpWZnJ6j
+qK614CURDxUiLTK+m5OXmp+mrHMdFh0sMysvLzXSnJWanKkwGyNBzHZVOS8yNTFCo5mwLC/wsLCvtct9QTIrLSshIEayoZ6kp62ts75aPC4fEA0X25mYmpyhqKuvuMY7Gg4PHCw0tZmXm5yhrK67MRcWIS4rLDA3wZyWnZ2j0R8bLF5sRTwvLi42cJ+VoVIn
+Nr2vt7vOZDsuKiowN0xLdKugp6mrr7vTSTIuJxoOE12Zmp6eo6essMbbSSURDhkpNrOXmJyepK2ys/8gExkoLS0zOLuZl56hpbE7HB8+Ujo2Mi8zONWfmJ6uLCFPt7vC2UA2MS4qL02tvy7DpquurrfB2EcxLS0mGRI5nJ2enqetr7XL+04wHg8SJDyslpme
+n6WuurzjMxkSHysrNEixmZmeo6ivzykYJD83NDYwMTy6nZufpdshKMzMdnFDODMxLC7Un6I9N7KstLO60WZDMiwuKyQdLqWeoKKor7S73EQ6MScaDxlBppibnqKqsL7L6zsjEhQpLzFPqJmcnqKttso/HRgrODM5ODQ/rJmcn6K1Lxww21tRQTUxNDM2vZye
+vTI/try8v/xIPjYtLC0tOEvOpZ+kqLK9zHNCOjMrIhYUNJ+anJ2jrLTA3U06LhwSHC8x2Z6XnZ+irbnNUCgXHDEvMzw3Q6eYnaClslogHz1HPUU3Ly4zPK6an6pPKWq6ycd3PjIyMC4uMMetVrufp6uvvvxNQjYyLSkeFCadmZ2cpLDBxutOOy4kFRMmOMqa
+l5+ipq+9zVAzHBQhNS87Qd6imp2fo7HTLxohQDs/PTAsL1ujmJ6jsysp089tZj0wLC0tMkWtnshGq6evsMNMODcxLi4rJx4lppqenaSyyORNPTwwKR0TGjywmJeeoqq0wMxVNycWFis2Nk60m5qfoaauyUEfFyk8Mjo2LzHPnZidoa4+Hi3dSUhBMi0uLjPZ
+n5usP02stcDJSjg0MS4wLy0yQr6enKGotMlpRDc3NC0nGhMtoZeZnKCtutliRjYsHhMcOTVNppeanqOttc1KKRcZMjEvNzhAsJqbnp+r0CYcN0U2OzMvLjA0upqZobsw7LPS70Y8My4rLC8x8qu/tpyjrbrMVzwzLy8rKSIXIZ2Vm52jrcLvRz01LycYFCg+
+9Z6Wmp6jrr3aSDIfFSA4LTAyWqOXm56grME3HB9BODIuLC0sPKqXmp+pPCrXz0g+My8rKisuOq+brFyoorO+fEY2MC4sKysrIiellZydpbHHbkk6NC4qHxQZP7WZlpyepq/E5kc0KRgVJy4sOcydmJudoq3BVCUYKTssLSkpLX6el5udpM4kL89APzctKCgr
+L1WjmKDW5ai1yN0+NC8vLCsqKzhbt5ybn6a46Uk/OjUtKCYbEimjl5ecoKq1x/lAMSwfEhkvLD2tmZqdnaKtyV81GhkxListKi7Ampeanqe3Lx4+TTU3LCclKC/AnJmdqzvltd/kPjEsKSkrKy35pa6um6GpufZDNTIwLCYmJRofoJWZmp+sxN1fQzItKBoT
+IzNIoZeanqCoss5SOiYXHi4mLCs3r5qZnJ+puEsiI0YzMiwmJSY1r5manqToNMbeR0IyLScoKywvvZugvaGgsrt5RzIvMC0mJCgkKKWUnJyjr8xcWUExKiggFhkybp6WnJ6lrLfKTjwuHRgoKCUtRKSZm5yfrL3YMR8uOSkqJicoOqaXmp6iuDE+yT06Misn
+JSgsObGanK+xo7bVbz82LiwpJiMlLU6rmJugpLLIaUw+NiskIBwYKLedmJyepKy6zFg5LygaHSskK3efmZydoqu/aUEmIjcrJCQjKEihmJueprNVLmBNMS8pJiQlKUWmmZ2itrCv22I9NC0pJycmJS+yoZ+Zn6m5y2xAODEsJSEgHSGumJqdn6m6zNxbOi4o
+HxskKSvHnZqfoaixxvRIMyUoLCQkIylhn5qdnqaz1D47VDguKCQjIyhQn5mdoKu9uclXPDEtJiIjJyc7qJyinp+su/JQOTAvLSYiIyIpsJibnqOtvvRdTjsvLCUeICgwtZucoKWttcNtSzksJyknJiYqy56cnp+qt8tVOzc1LSgjIiIm/p2Znp+oudDbWDou
+KSUgIiUpRaGZnqCmrsDtSzguLCsoIyMoPqianJ6lschuUkg9LyonJiYlM62bnqOos77NZj84NzEmIiYlLMKdnqChrLzW4e0/Li0nISAhKNKcm5+iqKu2V0o8LSkiICImKmqfmpubqbe/VT8yLSooJiMiJ22bmKKfqrnfSkE7Ny0oJScyLzCom6KlrbnL2lQ6
+NDI+OiIiJiu0np+io63E53LSxTUnKB8fHym+nJygpK2tqL42OCskIB4fISvMnpydl57Bzlo2LyooIyQjIijYl5GgpKrGc0I6MjAuJyQmMt3gp5ulp6/A7FZNOTIuM2Q2ICg0rp6lpquzyF9KSb3NJiQiHiArtJ2foaexvKynQi0uIx8eHx8stZyeoJuYrUte
+NCwpJiIiJCUsvpiPlqmuzEc8My0rLCglJCrOop+epKu32k09PjcwLSxFxyojUaigp6i0vtFYRTx6rz8fIx8gMqqfpKGptcm9pbMsLCUeHh8gLqubn6Ghm51oOzcoJyMfHiInMq+alI+fw808NS4rJiYmJSUobZiTpKSsxWA/NjAzLywpNL3RKdefpKmrvPdf
+TT84Qre2KR8jIECmoaqorr7q5q6kVSUnHh4eITWonaKjqaObrDUzKiQhHx4fJTumm5mPlbldQy4sKCUiIyMlJ1KVjJuoqtNHOTErLCwqJyhjr9u9naWsr8pKPT02MzBwrXAgIShgo6WrrrjRZUrSpqwuIiEcHSJNpJ+jpK2wnp3ULC0kHx4eHiRZn5yelJCh
+STwvJiUjHx8hIilflouQo7PPODAuKScoJSQmNa+gn5+jrLrfPjQ2Ly0tM7+uOiEwtaOorLPCbUFAQrajyiMfHxwnwaGjpKq3ybCdqTYnJR0cHR4nvZ6foJyVmcAwLiUhIR4dHyIqvZiOjZi6ZDkrKyclJyYiJCrCl4+cp6nMTTgvLi0qKyxEr7svO6WkrK7B
+ZEM6NjZVqqs5Hh8fLK2hqKivxnjWp5/AKSEfGx0eLa2doaKjnZikRykpIB4dHR4hL6yZlI2Qp0M2KyQkIiEgHyEnXJSKlKWuyjcuKykoKCcnK82suriioq+00kM2MS8vNcaovSweJU6npKqsut1LRsWjqFEiHh0bHz2mn6SkrKyenbctJCMcHBscIEKhm5uR
+j5zRLi0iISAeHh4hJlyVio6ateI3KysmJSUlJCY7rp+anaaqv3s5Ly0tLS47t65vJia8paertNFOPjpEs6O3LRwdHCLRoqOmqLXArZ2maiUgHRocHCHlnp2emZKWqzcoJh8fHRwdHyjLmIyMkqhOOSkmJCIhICEhKcyZjpWoqsBINCwqKCsqK0avtkQ1raCr
+rLv1Pjk2M1erqmQiGx4ntaCmqa685c2mn7A2Hh0aGxwmup6eoKCbl5/HKCQgHRwbHB4qt5qSjI+ddC4sIyMgHx4dHyREl4qOnbHEOy4qKScmKCYrzauzr6agqrHFTDcuLy80yam4Ox0eNq6iqau62UxEwqOlxiYbHBkcLayeo6OpqJycqj4hIRwaGhweLaqa
+mZCOl64vKyUfHx4dHB4iOpqJjJSr6D0qKSYmJCMiIzWypJuapqq8dj0yLSwsLDm6rs4sH1CopquuxFI/O0qyoq4+HBsbHDumnqWmrriqnJ+7Kh4dGRobHzamm52YkJShViYmHx0cGxscIj2djIuPnl45KyYlJCIfHx8o5Z6RkKKst0s4Ly0oKisrQ7OzcS7b
+oKmus8tBNzk3aKmnxigaHB5GpKGorbbOyKWdqU8eHBkYGh9IpJ2fnpmVnK8uIyEcGxobGyBOno+MjZe6MS8mIyIhHh0fITeejYyZrrpGLywsKCUoJirarrnFtqGosrtiPC8zNDi/p7VGHhok+6KlqbHG5Vi3n6K2KxkaGBog+6CfoaOimZmi3yIhHRoZGhsf
++52Wj4yTpDcrKCAgHx0bHSArp4uKkabYSS0rKigjIyQjN7Ospp+lqbXNTjovLjAvRa+txTAbJ76kp6u151tJ7qqfq14cGBkZIsifpaWor6Kanq0uHR0ZGBocIdOcm5aOkpzJKCghHx4dGxsfKa2Mio6Z2T0vKSgmJCAhICjLppyTnq+w7EE0LysrLC/frbfy
+JyevpayuvFU+Pj/Fo6a8KxcZGiW6oKerrbuxnZym1h8bGRgaHCe/nJ6ckpGapzYjIx0dGxsbHiuwj4qNk68xLiglJCEeHR8hPKeWjZaytmM3Ly0qJykoNLmuvGQ5rKWysctLNjc4R62ks1seGB0qrqKprbbIzqicoq4vGRoXGBwrsp+hoZqUmJ/DIh8dGxoa
+Gx0trJSNjZGePSkqISEfHhsdHyyskIuPo8dpMy0sKSUkJCdirrKvraqosrzhRjUwMTLWqbHGMxkcNqymrK/I71W7n6Cqzx4WGBccMqmipqWkmZiepzcdHRkYGhsdMqaYkI2RmbgpJyIeHh0bGx4mto6Kjpa+PTEoKCYjICEgMLeuppidsrLLSzkvLCwsObWu
+v9onHVOprK+260hCYaqfqbI0GBcZHUWmpquprqGZnaK8IhoaFxkaHjyinJiOkpmiPiEhHRwbGRodJL6Oio6QpjUtKSQkIB4dHibVqp+Pkq681ToyLSooKS3ur7u+ZCzZprK1wUw5OT6/o6iy2SEWGR9+payws7+xnZ2kqzwaGBcXGh9foqGdkpKbnrUnHR0a
+GRkZGya3j4uOj5r7JygiHx8dHBwgQKiZjY6exVE2KiomIyMlNbq2vra2r620wuk9Ly8wSayntL49HBgovaiutcPx0qadpam+JhYXFxknvKOno5mUmp6mSx0bGhcYGBsor5OMjpGYrSwgIBwdGxkaHS2pkYyNla42LiUkIx8fICfnsbusm5uzu8pCNiwrKzHA
+p7O9zy4cMK2vtLzpRkK5n6SrsT8aFBcZLq2orKujmZqgpLkqGRkVFhgbMaiZj42TmKLqIB0cGhoYGRsmqo2LjZCeQycnHyAeHh4fOLazpJGQpc3dNCwoJiYnQa+xwsFmNkmstcDLRDcxU6qjrbHVKxYVHDyqra+1taCboaWtdh4VFxUYHEaln5WOlJqfrzEb
+HBkXFxgaIa2MjI+QmbclIR8cHRwcGyfJrJ2Ojpi5PjMlJCEhIizMsb++v6+qs7bNVDguLDO/pauyw0kiFiDarbK3wN2xnp+nrL40FxQVFR/fpqedkpWcn6rNIRgZFRUWGCGvjoyPkZmmOx4eHBsaGRkeRaiWjI2ToDorJR8gHh8jOre6vbKel6rJzTwuKCYo
+QK2qt8F4Oh4kurG7wWxCZKifqKu2fCETFBcjvamtp5qWnaCnsz0bFxYUFhgmrpONjpKZoronGxwaGRcYGyunkYyOkZnKJCEeHR0cHSV6ub6ql4+bxVI3KSUiIyrRrLvC1GY+Prqyw9BFNTXBo6irsss5GxMYK7Wss7Com52ipq7FKhYTFBQYKa6ckI6VmaGs
+Vh4YGRcWFhggr46MjpCXqDAdHhwbGxscLr25pJKNk6ZCLicgIB8hM7e5xcfQuaiuwMJdOiwqOa+nrrLKUiwYFzezs7q+xaiepKetuksdERMUGDKso5mPlpuhqrkxGRcXFBYXHcKOi46Ql6DKIhscGhoZGR9isZ+OjZKaySYjHx4eHiNNusvFw6uZnMvcTi4o
+Jipqqa61xGU/KB07rb3D1kzLo6GoqrbVLxgRFRo7q6ujlZadoKix3yUVFRQTFhzvkoyPkJigrz4cGRoXFxcaLbKcjY2SlqguHh8cHB0dKNfAx7ejkpOrUkIsJCQkLrqsub/pVkE3VLC83ls4Obeiqau000ElFBMdQqywr6CZnaGlsL5AGxIUExUdYpqOj5KY
+oau/KxgZFxUWFx/Gl42MkZWf1B8dHRobGx0zwsawnZCQnMcuKx8gHyM8tbvHzl7wuKu9wfk7LytCrKitsM5OMx4UHt6yuLu0op2io6245CsVEhUUHtefl4+TmZ+pslsfFhcTExYaO5eMjo+Wnq80GxwaGBkZH0+9r5iOj5alLyUiHR4eJVa6zsjbxqSYrPra
+MSooLOSprrPEWEAvHR+8s8jD47yhoqarudI8HhETFh7Cp6KUlJueqK/FNBgUFBEUGS2ajI+Plp6qzCQZGxcWFxorvamUjJCUnO8hIB0bHR4q28fPwLeckZ3nSjEjIyQuvq26v21JQTQwv7Lm7UE/taKoq7PNTy0ZERglu6yunpecoKSuumUjExMTExkrn46O
+kZWeqLRAHBgYFBUWHVGhkYyQlJuvKxweGhobHS/M0r2rl4+XrTctIh8gIzq2usvWTlnSr7S+00Y2Lkirpq2vylY6JxYYM7m0uq6fnKKjrbnXNxkQExMYMaaWj5KXnqivzCkXFxQSFRktn46Nj5SbqGAeHBsXGRkfScO6oJGOlZ1VJiQdHh8kU7rPz/7tspuh
+2dc6Kyks4aqutMVPPjQhG0SxycTOuaCipqu6z0cmFBEVGT6ooJSSmZ2nrrtLHRQUERMYI6KMjY+UnKa4LxsaFxYXGCnKtJuOj5SZsygfHhscHSrYzt7NzaaUl7lTOSYkJS+6rbm/VT89Ny7sr9PcUkqvoaeqttJRMh0SFR1oq62clZqfpa+76iwWEhIRFiGn
+jY6QlJ6nsXMgFxcUFBUdTKuWjZCUmac9Hh0aGhseNtDaxbackJSiVS8mHyElQrO6yHRFR1+0r7/TTDkwUqqprrTVTjcqGRUnxrO3qpycoqWvu9dBHhESEhYpqZOPkZaeqK/ANBkWFBIUGS6kkI2QlJqkwyQbGxgZGiFS2cmplo+Vm78oIx0dHyfZvd7nTk3B
+nZ3Id0AsKi3EqrK1ykw6NCUbMbXCv76qnqSnrbvRTiwXEBMXM6qckZKZnqmwvlUhFBMREhYjoY6OkJScprU6GxkXFhcZMNHDn5CPlZiqLh4dGxweNcXz+n3+q5aWrk47KCUlO7CyusVMPTk2MFC3zd1l2qelq6282EszHxMSG0StqZiUm5+ps7/vMhkQEREV
+H6SNj5GVnaey0SYYFxUUFiL4tZqOkJSZo1geHBsZGyNTfVfjzaGTlJ/3LSYfHyjctMbMTD47R7mvwtJXPTq/pq6vutpLNiwbEx/dtLKgl52iqLK+4EgiEhAREyKoj4+Slp6os787GxUVEhMaO6uWjZGVmqO7KBoaGBgcLWZM5rebkZSZsyohHh0fM7vK41lC
+Qe+hnLpiTS8tP62ttbjWTTYzJxolvr7Br56eqKiyvupOLxkPERQmrJWQlJifqrS/eicVEhERFymlko6RlZulsUgdFxgWGB9HVeyqlo+VmKQ6Hh0bHCV8yFVaRkjCm5WnTDorJCvPrru710g3NTErM8TF782uoaqrsb/bTDgjFA8XK6+dk5Wbn6qywNM9HRER
+ERQhpY+PkZadqLHJLBcWFhQZLVz4o5KQlpmgxyAaGxkdM9ZJTU1TsJiSnL8tKB8iO7a7xdRGOzY677bD3GtG2qqqr7C+1Uo4LR4RFzi2qJiWnqOstMPdTioUDhESHamPkZOWnqm1wEgfFBQSFCJM0p+PkJWZobI0GhkZGSVOTERX8KeUkpimMiAfHSnPu9Ti
+RTw4PrmerUZRMzW9rLW1vt1JNS8pGxhDuLqfmaCnrLS/4FE1Hg8PEhuuj5GUl5+ptr/iMRgREhEbOr+dj5GVmqOv5CQWGBcbMEc8TdKhkpSYnt0eHBwePsLuVkM7O0WumJ1jOC8mSLC2vL/qSjYxLyojQrnYrJ2hq6y1vtpOOSkYDhIcvJOTlpqhrLbB30Uj
+EQ8RFCu5mo+TlpumsMQ9GhUXFiI+PEfAnZGVmZ2wKhkcGyjd/0dEPD5eppWYrTMpIirBtsHE/kU3MTA5X9nWbtWppa6vt8PdSTctIhMQHsqblZicpK22w+VLMxoODxIftpeQk5ecp7LB4ioWFBQZLz5Ft5qQlZmfrFwdGBodOF07Pjo/0p+UmJ9gIB8gPrvN
+0l8+NzIzRaukaEhK1aqwtLjI4EY2LSsfFSC9p5mYn6avtcLdSjkmEw4RGr6TkpWYnqm1w99BHhESFCA8Qq6WkZaaoa7CLhgWGSJDNzg7Q7ybk5mcricbHifczGlYOzg0OvCfmLY4OTa/r7y70P5DNS0sLCEnxrejmqSqsrnF70g2LR0PDxnmkpOXmaCqtr/u
+Uy4XDxEYMF2qk5KWmqGvvmojFRYbKzsxPUqtlpOZnKdJHBofMdRLQzY1NT2+m5WhUikqObm/yttRPjEuLDFC3dzXtaClr7O+yWpKNC0nGQ8Yb5eTmZukrLi+5E08IhEOFCLUoZGUmJylssfWOBsSFh0xMjhnppSVmZ6ovykYGiJBTTg3MTdFrJeWnK8pHyha
+veDgSjswLy42vaC2QNuzqLS3v9NfRTUrKyQZGGSfl5mep7K7yOdEOywaDg8bdpuTlZmeqbPK304pFRAWIzU2y56Slpmfq7hMHxYbKUI0MzQ4aaKUmJukRh0dLt3uUEQzLi4xPauYoko3T7i1wcP8Tz0zKyssKCdcrZ6aoqq2v899RDYuIxYPF1aWk5eaoKu3
+xP5VNx4QEBsuPriYk5iboK683jAZFBwtMS41OMWbk5mcosAmGR84XUA9MC0uNHCglZyzLipNvsjRekQ6MCwrL0C7vd6rnqWvt8feWkQzLSgfFBRGmJWZm6awvcpjRzsqGA4TJEiplJSZnKSwxdVEJRQUHy0uMkOvl5SZnaS1Px0YJD8/NTAsLjm3mpWao0gf
+KHPMbl4+My0sKzDNn6RM8qupt7zNXEo9MSoqJh4aO56Ymp6ouMbTXkA6Lh8SDhtXnpOVmp6ns8fYUzIcEBYlLjFxopWWmp6otm0rFxgqOi8uLS1BqJaXmqC9JRsv91lKPi8rLC04r5mcuzZds7rFzFRCPDIrKysqMlCwnZuhqbnO3FxCOS8mHBATQ5mUl5qi
+rLnM+U45KBYPGiwzypuVmZugrLrdPSAUGy4uLC8uWZ+VmJufsTwcHTxWQj0vKiwvSaSWmqVKKVq+zdlWOzYvKiosNrqt966dpa672WJOPjYuKCIZEi6ZlZmbpLG9zG9MPC4eEBEjOrWXlZqcoq690kwuGhMfLisvNcmblZmcorH8JxghQzw1LikqL9udlZqf
+uyUm7NNbUDovLCoqLUymncRWqae2vdZORj41LSonIhwrn5aanKW2xs9mST0vJRkPGD2plpWbnqayxNdONyQUFCYsLDyxmZaanaWy1zsdFyw+Ly4rKTG7mpaanqw4HS/QT0o9LiorKzDNnZmoR2ysucjQTT07MiwqKCgvQrSbmp+putjxWEM7LycfFRAuoJaX
+m6CrtsrcUDotHA8aLSxGp5eYmp2ns9FOLBgbOC8rLSo0rpeWmp6s5iEdRVU8Oy0nKCszuJqZn78u17fc2007NC8rKiosXa2/q5qkrLzeWE1DOzEoJB0TIJ2VmZujr73L41k9MCYVESYwTJ+Wmp2fq7fTUTgfFSMxKCwrOqqWmJufrsoxGyVdOTYtJycqOq2X
+mZ6qMyy/z2ZSOzEsKyosMMGeq9efo7S85VNCPzcvKSUjHB6llJycpbTL2HtPPjEpHREZNmudlpueo628zlg+LRkYLCopLkejl5mcn67HTSUbN0MvLScoKT+nlpmepe8kO8NJTDgvKSgpKziymp/RuqS6xuZMPTg0LSkmJicstZabnqe60GxpSj8yKiMZFCu6
+nJWcnqixv89kPzQiFR4uJzBjn5ibnaSxzF02HSFDLSooJypFoZaanqa9LSXeWD46LSgmKixCqZicrmGys97WRz4zMSwpKCgxz6+fmaGpvdhcT0c8MyklHxYerpqXnKCsvMnkXT00KhoXKSouwpyYnZ6otM9dQiccLTEmKSYsYJ2Wm56ouEclNew4OSwnJSct
+Xp+YnKPbUbfxbEY3MCsqKCkpO6uirJyisL1nV0A/ODEpJSUeHq6Wm5ykr8ba+VY+My4hGB8vMK+amp6hq7nNWUkzHyEvJigoLcObmJyerL14LihHOi8sJCUlLs+cmJ2gujvj0kdHMy4pJycpLE+fm6ylornCaEs+OTUuKiUmJymwlZyepbjNZF1KPzEsJhwb
+Kz2qmJyfprDA1VY/OSkeJykkKjGympqdoK3F60AqMD0sKiQjJi+2mZieoa9NQNlIPTQsKCQmKTHInJmkq6m43mZAOTEuKyglJSxLp5mcoKm+8E9IPjkwKSYiHidkopidoaq6z/hWPTYvJSEnJio/p5mcn6Wwy2ZROC4yLSYkIyY1q5ianqay0UVKRTYvKiQi
+JCc2r5qanqazv31NPjQuLCcmJiYuuZubnqKuxV5IPzk1LygkJScsyZuZn6Ovw/hWTTwyLiomJCUrX5+an6GrudVRRj84LiglISMnQaKZnZ+pus7kTjk1LSciICMnQaOYnJ6irtRfSjgyLCklJCYoOamWmJ6nst5OPjcyLiwmIyQqRqybmqCnt99URT84MCwp
+LCokL7mcnKGlscPvTD86QD0mIiQiKtKdm56grb/q18o8LS0kISAiKd+cmZ6gp6nDP0MzLSkkISElKlqfmJearrz9PjowLiooJSIkKsqZmZ6grb5bRj05Ni4rJis8KzWlmp6jqrvhX0c7NjdQLx8kIy6wm52ipbLOXVfF2SoqJB8fISy5mpqfo62trEk2Nioo
+IR8fIy67m5mZlqfh9Dw2LisoJCQjJCy4kpOioa/PSjw3Ly8sJyQmPVxPn5qhpbHITUdANjAvPE4jHyg6pZyfpau55ExHark9IiUfICE2qZucn6ezuqq4LzMrJSAeHyE2qpmcnZmd8kU/LiwnJiAgIyQwrJKOnaqvY0M2MiwrKyYiJDG5qJ+aoqi56kU8PDUv
+LC5aQB8q1J+do6a0wmlKPj/CvSYfIh4kQqKcn6Grvde1p0ooLSEfHR8hP6KanaCemq02Py4pJiIfHiIlPaWVj5Ksuuo6Ni0rKCgjIiMqtpWYn5+tv089NTMwLComN8YvJ7Cdn6WrvvZQPzs0SrNMHSEgJc6fnqOlsMRm4qquLCclHR4eJF+em56hqZ+d7y81
+KCUgHh0gJ2OemZWOnfVwOS8rKCUiISAjJ8aPjqGjrd9ENjAtLSooJCjpvTytmqKnr8tMRD43NDPbti0dJCu5nZ+mqrbXWk3Fps4jIh4cHibJnpyhpa+vn6c1LCshHh0dHinInJqbkpS2Pj8uKScjHx8gIirFkoqWqK7ePDQvKiopJiMkN6+opJugqrbgQDk5
+My8tNsLLJCA4rp6jqbHCa0lBSbOpNh4fHR0qt56foqi2w6yevCopIx0cHR4qtZubnpqUnU44MSglIh4dHyEsu5aMjp+84jgvLCgmJiIhIinDmJWdoqrBWzszMy8tLCpAtFYkQ6Wfpqq52k9BPzpyqrclHh8eL6yeo6Stu9nJpKFOJiQdGx0eLK6bnZ+jnJiu
+Ni8pIh8dHB4gLa+Zko2UtVI8LSsoJCMgHyElRZeMl6Wpykk5MC8uKyonKs+vTFufn6qswllDPDk4N8ysUx8eITGon6eossx7W7ygrDAfHhscHy+qnKCiqKucncgtKiMeHRwdITGrmZmRjp9eOTIoJyMgHx4gJTqcio6frsk8My8rKykmIyQzs6utn5+qs85E
+Ozg0MDA5vbM2HiNJp6CprLvlT0tgr6HDJR0dGx85p52ipa24qpukRCckHRscHCA3ppqdmZKXtDcvKSMiHh0dHyQ7n4yLlazuQS4tKigmIyEhKN+fl5mjqLf9PzY1Ly4tLEOxvi4mz6GlqrDGWURCP+moqjweHB0fTKOfpqq2ycSjnbUtIB4aGxwhQ6Kcn5+a
+lZ/4LCoiHx0cHR4lTp6QjI6ffjwwKikmJCAfICQ3oI6Poay6RzkvLywqKSgq2q3POrueqa247UY6Ojk6w6a+KxweI9ygpKiuwdtouJ6j9CMdGxocI9yfnqKkppqarDkmJB0cGxweJeicmJCNmLkyMCglIyEeHR4iLaiLi5mtxEIwLiwqJyYkJDOyrK+noKiy
+v1M+NjU0ND+xrV8kHS27oKertdpYTnarn7AxGxsaGye9nqOmqbKlmp/EJyIeGhsbHie9m5uYj5SjQysrIiEfHRweIi6rjIqQoXNHLiwqKCUiISEo0aSclqKqs+5DNTEtLi0vXa67PiExqqWrr79SQT9Cy6Sn3SEZHBstrp+nqq+/uJ6cqkEeHhoZGx4tr5ue
+nZWTnLcsKCMeHhwcHCAxqI+LjpnNNDAnKCUjHx4fIzyjko6bsbVOODEuKykqKS+9rsRASqOmr7PRRzY6OUKxpLg5GxsfOKahqa+70eOsnKO9JhobGBsfO6eeoaOelpqmTCIjHRsaGxwfP6KUjY2VqjAsKCMiIB4cHiEtpY2Lk6rGTi8uLColJiYmSq2vrqql
+qLS/WDsvLzEyZ6qv2igaJN+kp6y13llNyqKfrUUbGRkaIeSho6anqp2Zn7MpHx4aGhocIeedmZKNk55XJyggHx4dGxwfLKiLio+dfD4tKSgmIyAgIS64qJ2Voq616kAyLiorLDa8rr5YICmxpquvvVc+Pk+voKm9JRgaGii2n6errrepm56nUx4cGRgaHCe8
+nZ2ZkJOcrisjIB0dGxobHi2mjIqOlb0xLSYkIyEeHR4n3aKWjZmyuFQ2LiwoJygsT7G2vkc7qaexttFDMzQ416eot0MbGBwtraOqsLvPvaCdprUoGRkXGRwvrZ+inpaUnKJcICAdGxoaGx40oo6LjZKjNSonISAgHx4hKd6cko+Yvj4yJycpKyouNfqknqGi
+yzxJOzYwLSwzRsSqo6OqaigtNTxRXVRMXc+5rqqqwyomKy07VNrAta+wtrOwwS4lLCsuOUf6vK6opqapujIgJyouOT9EbMKzq6ejpcQrJycoMDtHZs3BuLCrqLJBNz02NDU7SN/At7Syr7o8LjxCWezzUVDtyLy2sLg/JiwxPWjMvru3u8K/urtdKiktLjpN
+2r6xq6qrrrfmKyUsLjc/SGXJta2qp6evRCcqKy45RFTnybu1rqqrwDc3NzE2O0FQ2MG7ubSzxzg1SEpZXk1KXtnHvbi2yjQqNT5Wz8XFwcHEwr67wT8pLTE5T9bEuK+ur7S7xEYnKS8xOkBO3bqvq6mqrcktJistNT5IUPXGua6rqaxtMjUwMjo+R2LPxL+7
+tLDMPEpVUltJREtvz8S+vL9rLzRMXcnFyM3Y1tDMxsHcMSkwOE7Pxbq2tLW9x8reNSgtMjZAT96+tK2srrC8RCgqLjM/R0xfzrqvrKqquD8vMS84PURT99HHv7eusu9BTUpFPz1BTPzNx8S/wFc4RV3ez9jxWF3n1szBv2cvMT5L6dDBvry7w8zKxl9i5snL
+3XllZu/b3n5fVk5LTWNy78zGz9/p7m9idO/3WU9PTFZv38i/xdtfXWn/9u/iX0xOUV3t1snK0OB5amT48m75a1JTWl9cXtrLzeVxallj6eXd4mNORkpTZuPPzexjcnXt1NDXcVROTlFXduDPyM/f3Nvf7O/p9GBOTU1OYdzJw8vfZFZbbuXq9F9JSE1WddbH
+yN5nXWtu9tzs+XNYU1llcnXZzdHtXm1obtzW2N9sWU5PYOjMys/7VF1269bT1nlOTFBkeOfQ09nvYm/z6ehramRaVFJfYm3XzM7be2pVU2ju5G9dTkpXet/NxMffXF9z7eng6WNZVVhp5dvY1dDY5/x1bV9mcW5gWFhXVWTfysjV9ltXW2j37eptUE5Z+NrX
+y8zhbF5kZmp6Y15ma/f87OHp0svO42ZhVU9f/d3feG9faujazMnQckxOVWXr7ef9W1tk/evn2tvmcl1mXmBzaXL9Z1pZXl1h3svI2WReWVp84uLvZlBNWuzRyMTH3VlWX2Ns/mlXUlRj7uTb1c/P3GhXWVVRW2z182FdZWrw1cjJ2GRNT1p82drrblNQXu/d
+2M3S911b/uft3+xubGZmZ3n3et3Q1OdfYmpm7t/h/11XT1Z23szL2W5PVGnz5e15Wk1PW/bd4NbT335hY2JjaWBfbHBxannl4c/Fx9pjWFRWbt/d9GBaWWfcz83Kz3VNTVxrdnR0ZVtfevPq4OHd2eJyYFtdZ3ro3+1mYF5eetvLyNlkV1Zf79nd621TUV/o
+0M/Ny91jW2RkZXxoXGFn+e3w29jTzdJ8UlJPTV355eZlX2Vm59TOzttfSUpWauLl9fVeWmzv39rU1NzwZW52aPH4cu32aGVz8OXSyMjVX1ZWT115a2FdU1Be48/IyMvdU0xVWFljYltbX3be1tLNztHcZU9QVVNaavTkf2nw5d7QyczdZU5NVmnn431zXlRk
+4tfWzs7jZFddbmZsc1xcXlxfft/h1szQ4V5UW1VYZ2lfWVdWX+PTysfS6llOWWNpbnJoXF504M7Py8jU6GdYV1teXWJyfvN8+tve2M3T/FhPT1JcbvDvcXRoaOTX2NLU9ltYXmx+ee/3aWpuanzj4NfR2ev8bHJ6am98bltbX2nn2MzHzuD2ZF9jZV5ZWVNU
+YfzXz83Hz+9oXlZPU1ZYX19len7b1tfO0/lWUlNTXW196nds/m/o2dXS1+tiYXf+5d7m4m5bX2Boe+ba1t3+cX9v/v1maV1PTVNd/trOy8/rd3RfZGxhWFZRU2bk0srIyc/6XmJdVVlcW2NpceXY0s3Nz9b4WFRWVF1udPX6a/rm4NnS1eVuWVplZm1+Z2Vf
+VFpu7urd19vje236bGFmWFdZVVRc/uPRycvP3v7zbV9nYFlWVlt62tHMx8vR5GdqZVlTUlJVXGN63djTy8/Z7V9WU1FWXmJob3F94Nzazs/eb1xcZGZrfH1ubGZn8+Hk3NTZ3+379fZ6em1fXFpZXvji08rM1N3s+25eW1hVT09XZt7UzsfJ1ORuYllRTk1P
+UFdl/dnOzcjL23ZdV1FSV11qbHHp39XP0M3P42BZXmVud3b2fWlvbvvq7uTf63Ft+fn17XtvXlNTVVtq79fNz9vl297q/WNZU01MU2Ho183JytHe4O5tYltTUlBWafDbz8rIy9r9cG5eWlxbXltba+vc19PQ1+plXGFgZGtjYWdhZm787ujc4/hqbPd6b/Z8
+bGZcXWB48ebW0dXf6d7f7+95Zl1ZV1lp69vPztPc8fl1ZF5bVlFRVmB+49rOzdbmdnFsY11bWl5iZGvx3dvSztXpb2ViYGl5b2prb27+5N3Wz9bvaWz3fXN6e29nY2Nm++nl2Nbd7Pz09/51aWJaVVZWYfzez9DZ5/R9bGZjXVxXUVVd/eHZzcrR4fh+bWlq
+X2FmaG1z6NbRzs3X9GdnXVtndHlqXmRqdOnh2tnhaVljbXB8ff79Z2Fka/jn3tvd6f309X7v+m5qXVZZX2zr1MvN1+jk5n15cV9cV09SYurZ0MvL0+v7/2JdXllWW11hbPPd08/S3W5dYFtWX2xvaV5fa/7q3dbb6mdYX2119fPy7nptcfTs5djZ3+j79v50
+7+rw/mpdWV9pcd/V2ettcP5zePh0YlpUUFz65djR1Nvs9/N3a25qXlxfYmzs39PN0Nrqdm5mX2f99XJlYGrs4NrR2OxrXl9r++vo7XRlYV908u/g6HBob399ffTr7HNgXFVZYWTx3eP/cXr+9uzq7mpXU1BZ9t7Vz9fg5+Pn6uv4e2JWWmFs7ODY0Nbm+3lk
+Xl9eanVkXmBn8eHc2N10Xl5bYfjl4+h3am9v+unp5OpqXm/28uff3t/wZ2RiaHz159ze7+7s9ejh6O9tWlhYXfzc1dbefXj0fXb8cmZdUlVifebd2dnffmZpX1peXV9ramhufunb1dfe/19gXl1/4N7g83j159/c2NzwZFlgfffp4eXp+2pmaWxrevx9/3F0
++3j75OPycWFcXGNy39LW3vdy/fp/7/BuXlhYaOXa0s7U3vJ0cG1maW1maHX+7t/d2tHU4XhfWlhXY+3m7PN6cfXj4NredFpSVGB3693h/G1sZ2x/fPPvbmZpb/Xv7uLd6XVsY2N4893P1N3rfn306unsb1pWV17r2tLP235lZ2Zna2hmXlhdcPTd2trV2e5q
+ZF9eYmh+7fv7f3Lu3dnW3HtcWl1n7d7a3vptcnvy4d/g7WdaYXns39/i5/BuaWhmb3nx5Ot4dHJv/Ojn8nRdWFhbb+XZ1eFtX2ZwdvT8aV1YXG7v2tPR0trrb21uamxydPf69Ojr49zU1eB5W1ZYW2zt6up7am977Obf4XdcU1hqfuXe4uz8d3F1/v726e78
+a239/fHk3uh+aF5eaPbc09TefWpv+/Hs7HBdV1tq69zSz9frbmNgZWRiZmJkZ2z25t7a1df1YFdSVFhi+/Z+c2xy9eDd2t5zW1ZceOzi3OLveX348OHg4N/wdm9v+fj79PV4ZWNgX27x3tbd72teX2VwdHBlWVZecODW0M7a8mhiY2VraGBeX2vy5NfT0tDX
+6WleWlhcavju9/Lu7eLX1Nbc/FtTVl5t++nteGx08ere3ODwaFxbXWVzev54cmdnbXL57uPf7HNnYGJpf+71/WpiZ37e1dDP2vJta3d4+/ttYV1je+rZ09TW4v5gW1xaWl1jamtv9Ozf2dTU4HxcVVVda/fs6/5ve+3f29nb7mddX2/25uLn7v51b3rz6+nh
+3+n8fPz89e7q+mxfW1tl7dzU09v3a2hub29xZFpUWGby29TT2ed2Y15gYF9fYWVmbPHk29bT1N/+ZF5fZnjs5eTueP/t3tra2+5lW11p/u3o93BjXWJr9unn5Ot6bG/6/Hx9dm1gXF9n+t/W0NPd/GdobHJzbmhgXWT/39LNzdXpbGJkZ2tsZ2VmZnvl29bU
+1druZVtaXWJrcnZ4bmx18eDa2uD8X1ldZXT8+m9iXV5s/Orj4ebzdG7/7erv9XhybGdv++fb1dPY4vN3dnN3empfW1pfcefZ1NPc9GlkamhiYV1bXF5x69zV1NXc6HZmYGBfZWhqbm/+8eLa08/Y7WpfYWpx+fT9bWZr/uTb2trf9G5qefXyfGxlXl9ka3rs
+4tva4vF6dW1rZmZgWlZXXm7p29XV3fhrZGZpZWBcW1xo9N/Vz8/S3PF2bGhmZ2prbXB27+be2tra5GxcWl1lbXp7b2piaW/z4uHk8G5laXzu6ev9bGJeZm157efi4ury9Ozp7fhya2VeYGr84NnU1dzrfHh0endqYF1cZHjr3NjX2+lwZ2traWViX2JlbH3w
+5eHi5+9tX19iZ21zcWxoZmpy9N/b3OP7cn/u5uXo83ZpYmz97eTf3uPv+vLu7PJ6Zl5dWlxjcu/i3N7p/H3/fHd8bWFdW2Fu8N7Z19rl/HX7+XZyamRjZnju5drX19vk8nNxc3N1d29saG7569/b2d3ubGNpb3v8f3JqZGRv+ern5un3bWlve/7/e2tgXV1g
+aH/p4d/n9Hn87e3v735oX15r9ODY1NTa5PX69vZ4bGVfXl9s+une29vj+21paGhna2xlZGRs/+rf3Nvg+mtqdPv9+fd0a2hqdfTk4eHi6vb+8ejq7e58a2ZpbW/x4t3e6O728unq8f1uY1pbY3br4dzd6fx1c3r9dmddW1teZ33m397f7XFrbmtpaWtrZWZ0
+9erd2NnZ3vFucH3y9Pfxe21uevrr39/o8ndrbHH8/Hp4bmdgZWxtfu3m5/b+8e7q6ez5c21fXGJw7OXf3efw7urq6er0bWVhaG/+4t3c3OPy/fLyfXd2dnBobXp/7eTi6PF2aWlw/ff8fnVmZWxxffPu+m1mZnF++fH6eG9nX2Rqb3vw5+Tp8enl5OHj7H5x
+ZmJoeujg3+Ht//zu7fX2d2FcW19qee3k5enydmpvfXBtb25uaGt78+be3ef7cGZlb33183tza2/16eTj4eh9bm/56enq6/p4cW1uefPz7ejr73p68vT07vpqX19eY3bu4uLtfm9x+/N6cWpeWFpic/Tq5OXu/3dxd/d+c3p3efry6uXe29rf9nJsaG998uv2
+enZ5/e/q7O58YVtfavjv8vB+cnF6enn4+Pr4enl+/Ozs7urvemtpZ2d96d7c5e739+zn6/N7amBkcPLg3Nze6vr+/H7+eW1pZ2l3+PHo4t/h63NnYmBkanP/fG9tcXz05+XseWJcXmn/7/D3eW9w+u7r5OPp8fz87Ofk5ent9XxtbW1u/Ojh4u18cnT9+/p4
+a2JdXml+5t3e5vpvbnr5+n9waGlu/u3o5N/g6vluaGtwdf3u7evu9PDt6ODe6n9pYWV57ufl7PT/fPjv7u3ue2pmaXfv7vL1/HRvbW1ubHX58vh8b213/H//emtlX19neerf3ufu8/Pr5uvw/21qbvzq4uDe3ub3fHZ2fG9qbnJzd3Z5+ezn6PNvZF5fanr2
+7fN7e/rz7efl6X5nYmh77ery8/T5/PTu7u7t7e72+/z67uvr7O31em9uc/nk3d/se3V3ffj9e29mYGJy8+jh5Oj1b2prbWtmYWJma3b79+/l4uj4bGNfYGdz8ejo7vHr5N7b2+L2al9ldPLs7/j+/n337ezr8n1sZ2Zpbm9zeXx5dnNwcn3z5eTr/mtobn/4
+8/J7bGts/+rh3eHwfHJyffT2e3Zycv7v5+Tl3+Dr/G5qZ2pucvvz+3hzfO/o4uDsb19dX2l7+/h4a2xuefbt7PdxZF9janF1cXR3cXF5/vXt5uDh6PR/eP7v7e3t/G9scP3p39zh829sbm51eG1oYWBneO/o5OPn73lucG1sbm93fHv7+fHq4t7h6n5mYmRs
+//Hq7f599urm4N7l9HVrcn7x7PH09fl9/vLx7+3w9fp5eXV5+/n5enFpYmZt+efj6f1qZ2tyen12amNiZXL26ePl6vJ+eH/4+n9+/fn18O3p5N7c4ez+a2ZocX75+PxvaXD97+rl729lYGRu//b8e3VycXP47u3p6vP8e379+vT5+ndvbmt17+Xd3OHsfXl9
+9fL4+HVpaW7559/b3ebyfXd3fXdta2tqbXX57urk5Ox9cmxnZ2xtb3Jtampv/e3o5ut3ZmZpbv709/n+/vfu5t/e4Ojv+vn19vX5+/l/d3x///Tr5+vyem5tbXJ1b29tZGRree7j3uLr+nx7c3h8bmpqaW/87OTg3+Dl7/34e29zcnV7fv/47ebf3d/j73Bs
+bXL99vj+enV4+O/p4+n0e29xeHl8eHBubGlqcXN3+PLx9X1zbm1xc21qamVja37p3dja3+ny+vz79n5uamxy9ubg3t7i7Pp0b21oZmZma3B1ffTu6ujr9XpsaGpufvj4fnZ5e/bt6ePo9Xt1e/rv7vT6fnl5fu/r7Ofn6+vt7/Dy8v53cW1ubnn27ePf5+76
+enV1dm5qZWFjaHbx6uXl6/l1b2tqaGVjYmNqc3v37+3q7fX9/Xt7+/v38fX48+/q5+Ti5fB9enn/9PR9eXJsbm53/vz7/3l1ff58fnxxbW1pbG9zfvfs6Ons8PT6/v50bWpkZGt67eLc3N/l7vD4fHltaGdobn7u5+He3+Pr+X51bmxpa25ydXz38+7p7PR8
+bGhpa3V4cnJuampwd3z3+n10b3J1d3v+e3d6eXz17+vl4ODj6O3u8Pb5em9tamtx/e7m4eXt+Xlxb2tqaGNjYmh19+3n4+bu+Xl3dnN3cm5zdXv57evp5+ny/np++PXv7/b4+3/98+3s6ent8Pf18fLw8Ph6dHJyc3727Ont8vj5+v55b2tpZmVobXn67uru
+9P55dG9ua2dmZmdtd/Hn4N3f5+vu8vj5+316ff738Orn5+Xn7/19enp6dW9xbm5xcXl++fTy/Xp8f/379vv+eW9vb3X99u3r7/Tz7/Hy9Px4eHd3ef707efk5urt7vP7/H53dXByd//v6ubj6fV+eXRvb3JwcnN0dnr78+/u9npvbWxsbnFzdXRxcnN4e/35
+fHVyevnv6+jp6uzx9fb08e7r6+7x8e7w7uzz+3x0bmpqcHr58vf+f314eXp2b21qam53+e7q5+vx+P9+e318dXV3eX367+nl4ebs8vr9/vTx9PT6/Pj17+7s6+71/f349fH1/n96dXRxdnl/9/j7fXz/fH39eHFsaWhnam96+PX5fnwuc25kAAAAIAAA9RIA
+AAABAAAfQAAAAAEAAAAAAAAAAP//////////////////////////////////////9//////////////////////////////////////////////3//////////f/////////////////////////////////////////////////////////////////////
+////9/////////////////////////////////////////////////////////////////////////////////////////////f/////////9/////////////////////////f/////////9///////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9//////////3////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////f39/f////////////////////////////////////////////////39/f39/////////////////////////////f39/f///f/////////////////////9/f3/////3d3d3d3d//3
+6+fr7/f///93d3d3d29va2/d1dXd5+93b29vb29rb29vd3f/9+/v7+/v9/93b29vb///////d3d3b3f379PT33dnY2NnZ2dra2dnZ2dr//fv7+vv7+//d3d39//r/+fr593b59vv62tr729jX1tZX2drZ2drb19dY+/3zsTN2dXr4+v3b29fWV3LvsjP0edn
+WU9ITUtGP0ZAPkJMSkzn79vXzs7Lx8fKzMjHxcC6uLm8vsDHyNVda0m0vlk+PDw1Ozs6Pj4+PkQ9OD9NTEtJTlVPTl1fVWv//+/b9+vCyL6/vMO7u7y6urq8vbu2t8W/xL29z8Vn06i4Z2dJQDUxLzAmZ0g/LSssLC8zMjg9O0E+T3dv38rNysjZ69PN2dXr
+193M28vL0ca7s7Kztba2tbWztsSloau2vbvNb2dTTUo+ODc2OTQxNTI2LzU0Njc7ND9DPUxPUznd4/drXcbBy8Hb511d/8jJy9HO39nCzdvGT6OrucXDt9vFzctPqry91ztCNjg6NThAODk4OD1HQUVES0w8OWNLV0VRSl1d49Nva8vMurqut7q8tLi7vsF3
+p6a1v8a/T1v//2NLPEdCPzM6NTw8PUNDS1NX7/9f/1tZTkk6R0NKQUv/V1NbU01Ta+vjv7u/wb20uru7vbefrcHM2WtR7909rsJjb005Mj04ND1LQ0xMX1XVzNnLvtHra01L92tKR11LZ05NPknbX8LLxsbMxL7Vzs1LtqrCvdXJQFFOUeddSUpfSTw9U0NB
+SFFna+dvY83L3dPBz+dnREFPQ/dba1FKRU9DOV1K23fDzMbBvcLGwmO1p7bJxsVr69fXX6m7xN9PYzZBNztMQz1ZSURZ691j68vJ53dCV05DSUFEREZPPTpIV0Zr39vR2efF1dPP37KqvNfLv1vFyc/M42tn1U1PVWt3SlHvd2NjW1lvY2Pb5+NXWVE9Se9b
+U0dKSj85ODpBPvfRz8e7vry/vcD3oqq+y8DGXdfPY7WmycjnTz5BQ0U9/2NKU1lJVWdvX2/R92dRUVFJS0dLRD89Pj05PUVN69nLy73Fv8m/vcetp7PJxsr348bTxs1fS1tMP0U7REBJTWdMRkJATEpRVWNbV1lESU3n691dT1VTSzhFSkhv38jIvrW5xLW0
+w7yhrsu/zvdb3VNCpsXTd1cyODk4Nz9AOkxFPkR3TE7/3f/vZ1ljY1Ff71tdVU1PPUdnRsnTz8m3u8y+vrrEsaWxx9HZTULO3c7bTkhdQzY8OzY7PkNDRFM+QFFMTXfrb2fnd2dvxs7A2dnfY11HTWtZZ8nTvb+6wcnDvMZDobDK31vvOl//OanLZ19bNi46
+NTVEP0lNSUNN6/9Ozs7f2+t322/N18rLb9njZ09DW1l3yO+8y83PyczHyDursMZXTVcwX2tf70ZATkA4Mjk3NUM/X1tJUVXr53fJz8zLzc/Z17vBu7rX281RSEREb07nw8jAytnNxczD782sw1tXTTg+Pry8XVtjQS0yNzI0PWtIZ11XZ87I78jGxsjJzczJ
+yMzKwN3bzltERELfWd3Ly87b1c7X281rU6vLR0w8NzTnSlNMPz89NTU4NjdCTe9v6+fXzsjLwb++xsnP0b27ur28289dS0U5P05CzffXzuPd3cfNyvdFqrldT0s6Ojmux+tZSTgwMjM5N2tGZ9X/6/++xsbDv8HLyNvNvsHCw7/TxVdRQjpKUUXXa/9jU+/j
+X9/bbzWrr+M9QUs0SmddX1VPUUU9Nz88093vzcPN18PAy8fCxtHb79u6vcjIx8ljW0I+N0VCX3dv12Nvd9vd59vZNMeu50RNWyurw+fNS0c4Pj02Pk9Z99XVwcHOvsLBysHEx9vvxcvE49nX0U9FRzw3O0HrWWNrXVtvZ3dv498/N6nJPGNVQUxZ68prX1FK
+PTs83f/d58bAy9fFw8vJzMbL52/TvcPP79/fVT5APjI+RMpD99Xj69vX1+/X/0g1pclN9zy6vsrP30o6REI3NldX3/fVwr2/wr7Evc3Gztnd1dfX71HbUV08QDo6O0BnV2dd52PX3f/fd9lvYzeoyVvn909HxtfL61FLQjxAR+/rXc3Kxs3bzs/T79vra9Xd
+xs3TXWtPa0E/PTxAT0vMa+PR38/T483f5+trPKnJZ+84rb7EzedjOEA+PD9Lb//rzsvEwdnRys7d3e/Z591nU0tVTUtNPTo8PU1ESclra9HXzdnbytPX1903pbPG7+frT+vE529LO0FEQD/RVevV18/Ta9/v2fdnb9f3wM7d205PTUlCOz1JSE9R0WPN38rI
+ydHJy83T20y9qb1PPKvDxMdjTjg9SzxGQT7M3/fVycrn/2vj91tb1/dnXUxKU0ZLR0k/RE5VV1frwuvTvs3Iy8bHy9vfVTCnvVXb991Vx/dNR0VFPkdESmvf3d/b19tV/+t3XXfZ39vj01VdTEhJR0pCUVtbd+vTwdfIyMPLysjI3d//PLyx0UKut3fH/0M4
+PT87R09AZ87n09fNze9r2d9vb9fn6+9MU0tdS0NFXUVja9tr593dycXIzc7Gys5j42dNOKu9Y87XTUxvQkw/PTxHTkxOZ8hnb9fv22t36+vv53fC0etfX2dHRUR3Rltv99ff1cbIzcHEz8jO01v3b0wxpq/TRKu8Y19MRTM5NjxNSUVdb9vT2dXL9+9v5+Pv
+1ePdUVlfUVdIS1FnZ+PRyu/Nxtm9y8vZ1cvvWUtfRlkwq7vr2WdnPkhMPj49P05vTV3rwm/f1d3MZ1vX12tvvc/V/3dfS0hLR2P3/8zFzczHv73Mxb/by+drTU1ZU1M0qOdTsNtMOj42NTw4SHdTTmPM4+fbz8fRUdXVwGvI1edfV01LTU9AWdnf1cjJyt3X
+wNfL49/bZ1NFUU9dVzettcZTTT9EO1U6R0VXa+9nWcnf3d/d0c5Td8vnz8jF19dnWUxET0dG583rzcnIx928zdvF4+djVVdJTV1fT0GlXa/OZ1s2QS4+S09n3WNXyc7VzdvLzf/R08TNzs/T909LQ0hTREj3zdHZx8jT18fKb+dvUVFNSk1RWV1XNLq0Y0c+
+UzNJSk3nW+Pf4/fT1ff32eP/x3f328TOzNXjXUhIQUxKT//Ix93EwsfOzL7Z791OTFNTSU9ZX19bNLSub3dNPT08Rmtjb9lr18vX0dXX48PLzc/JztHb2VdbPUZDUUdV48/NzMXNz8drzs9RT1NPWUpMXVlbY1kvqMpHOV1FOVnv39nv2f/F62/M5/fdys3O
+X8nM0ddvd1tFPz9VQXdf28vH1cfMv8zfyV3vY1tRX1tXY11n/z/VvqpVd01DRm9dd3fP39vL/9nfx+/G1cXX1c/j21lVQj45P05Ia+fb2cHb28Pd413vX0xKW2t3X/93Y/dfTy6s70JRTXdRz9vf7+vXd91nV+dj69PPzOPP29vjU1FKSjlNRVvd4+vNwdHd
+vsPd32fOS2PvY+vf9+9dZ19RKc+qyetb00dV2W/r2dPbXXdX3ffXzcnK1et3/29OREM6OkpM/93d3c3KxOPNz29rb+tXVf/V5+Pb/2tjU0c4R77LR+vVTu/MY+/TV+NvV/9fZ+Pn1e+/3XdnWVVDR0JJTVXn687X183MxsXV39lja9t339Xb1d3/X09RRzon
+p7DfxtnrRedfzuffUVffUV3L1c/d1dfjU1VKSz88PEhPzdfrz87b57zP3dvf91lfz3fdzs3f5/dZTD89OSiot2vT48pK3+/rd0hT409V/9vb99PHyN9XTE5LQUFRS+vd0d/O0//CzdHn79Pj9//J09vMytPrb05DPz47J6yfvMnTzT9d21lfSV9XWUvZ28rd
+1dnr2U9JRE1CPlNNz8XI49fTym/JyV3b92f398rT983Na/dXSEA4OjQzU63EZ9vVRdPXVVFLTVtZW3fV2XfGz8/fUURBT0ZJV9fL2crb28bX69nR3//j1c7nwsvXz9HrWU1DPjs9PytCn7Bvy9FLWWdNXUtDTF1n493Oz+Pn43dVP0NRSkxPxMfFzePLy+dn
+48FbWdvZ393Hxeff319MRDs8NTdNSjWlt0/Zx3dR9+dPSUVb39//583H19vdWW8+QU1jWdvGx9PdxNHjZ0rv62/nyszFzci819tv/0ZBPD05QkJHMZ+svMbMXTrrXUxPQUnd09/XxM3fZ2Nr/0dEWW9b1dHI0czXZ1tfXVn372vjys3V18rv/19TPj4+Oj83
+5/dCsbCyWc/bSGv3R0hIa9fV78/HyNlR/1VTUUNVX83X08vPz89bTuNJTmfO2cO/v9XPwuNrS08+Pj1DPz9TQUvTn63PxWf/R0dTS01b29HO58rJ31dbXXf/TlNfw2P/57/b/11rWVtNd9HK07zGz9XV605OQz0/PkVBOP9f9zWxrcz3ye88TVM/b3fd/8LV
+1ePnU0xRd+drSNPR12vD0d9f60xTU0z/yrzDvMDG1c/rXz9BRj5DR0FHTEY3rsylvc/PSTxBTEz3V93Rzdfd7/ddTl/34+dLzu/nX9/R319fU0lXXefJvsK+v8fZ91VTP0pDRURKTEdPa8tvNaa42etXSTpKX2vdb9XKy/9Zd2NPUffr71HX3+vI3+fn3VtZ
+Tldj28O+u8TAxuvrXU5BQ0VGREVXTEtMO6tPp7K5R09EOE7Va1fd49vX505b405P79P/1VN32Wtva9vrVU5bX93Lwr63yMTLX1lCTUdGR0tGTv9BV+Nnaz+9qb1HW0pFVd/348zV1W9fR0prUVXv32/dY+/f0Wdn299Zb11vzsrFvra/zNNrWU5MRUNFTEJZ
+TE7nP3ey11+juj9RSU1fzs9fzW93V+9GSXdbV+vXb9Nb73dXTv/R919n5+/HwcO9vb7rb1dOQ0ZTRkhLY09K11vvZ2tnP8W3/0Rr91fj0dPO22NVb0dFZ2f3Wdd33cxXa29r7/fd93fn2dPJxcG/v1vrb1lMR09KSltXUVPnSzyvx85M67JnRGvZd9vjZ19O
+XVVrUUhn9//Vb3fN51dZTmfX293b98zMyrzEzMrKRV1RS0ZJTFtOTldJ79nb/1tnYzr/s8VLz87Td83fZ3dMTFNPSHdv11VX487bTFnv/+vZz9Hb08fVxs3N2c9OTm9dSEtTWVFOU+NvT0SxxdNrRlWzx1XHxtVnd1VPTlFRVU9RY9NvY0nba2dGQ2t349HR
+y83Hx8TF0e/390lFVVlKVVPZSVdTd8jX2eNMSk4xsrLHd8S/9/dj3XdNR1NVTFfr1WdPVW/nX0ZO59/308/LzdPIz9fv72NnW0x3Z01Zb19jT9vj/1HBu/9vRkqxvt/NwsVrb1VfTUdLWUxNb9lnZ0ldS11XRUtn19fOwMfIysfK2XdnTFlPS11VX1fjb19N
+z+//ynfvQk06t7XH1cjFzuNjY3dVP0hTTUfdd19ZSVNOW1dNV+PX0c/HxsnPzszdV2tVWWNv/2tZa/dda2PV2Wf3d93/TMG4vt/Mw8/d911bSUtESE1RXd9nVVf3TGtVUU9X39HOyr/EyMrPy+NPWUtRVWNbXW9f3fdb1//3Z3dTSVVdu8Xb48THzd3jb2NK
+T0lITU53a09bT19Ib19bY2vPz8zIw8XJ1dPnY0xPTVf/42Nvb2Pn72/P62d3619X37q+xt3Kxs3RY+ddVUFLUUJKU3ddUU9T611HY11d39PTysO/xM3J4/dnU0pMW2v3XVXd9/fd79/fVWNdTVvVwc/j783Ly93fb2dPRFNVSUxO91NNSUx3701ZV3fO0cvG
+xcbDy8n/XVlPSVNn/+vvW+fj/+/PY+tOWVNZXcu9xtnPycbT391da01DSltMTVVnU1VEY2dnW1Vb49HTzcvDwsjMz3dbVVdJSf9jd1tj39vfd85jX1lOTVdbyL/G49PTydPV3/9rT0xRU1dKTlFPUUbna2f/WWvT187Kzc3Mzc/da0tLTklR92drb+fn3eff
+129ZWU1Ra+PKycrR0czN3d3va1lXUVlRXVVfT1lJU2vfa3dfd9Pr0c7OzMbJ3+9rSktRS1Ffd19r3+vZ98vj71tOS0dr09nMy8vN59nT6/d3TGNVV01Ra0dNU0vvY93r6+Pd1dHPx8zJzczja19JR0tTW1dfWd3vd+9322tvU0lGV2fR083JwMjRztnn/29f
+d2NVTVt3R0hPW2933efr59Pf2dfRysjK1eNbTkdHTF1ZVV/nd9/v98nd/29OREZja9fRzcTFzevT09/v/2dnX1tTa1dLSUv/72ff4/f/2Xfny8vLztvd/1VJTUZMW11ZXefn5+/P591bX09MTXfn48nNw8TLztfj929va19dTG9VU0lOb2P3d+/j/+/T5+vP
+y8/M1+t3UUVMTU5rV1t37+vv09/d42NVTklOWffdzsbExc3b59vj7+v/Y1XnW09MU3dOY+Pf4/f/293v483M0dHva11OR0pOTlt3XV3369Pd/3drX1VTT2tn3dfIw8LCx9Pn9+tnb2td91NVTU3/W09r5993d//Z49vbzs3O0eN3WU1LSlVO7+dVY2fX3Xd3
+d/9bTE1Na2Pfz8vAwcnP2+fn5+//b3dbW1lOX2dZU2vd4+tn59ff39XPzs/fb1tPS0xLX1lnZ1tdb93ja/9rY09VTFvr39XJxb/AxMzX43drb2tjY1NPUWtOV19ja+/jd1v329Pf29XN2ffjVU5NTE9vX2NfX+tr5/drd3djXVFL7+/d18jDvsHO0dnnZ3dr
+a1lVS1d3XVdf/2dn9+NnZ93n2dfR0c/jb2dOTE1PU/93W13jb2f3d+9jX1dRV1Hf1dPMx8LBv87X0+NVV29fV1NOXWdXW2v/Z2Nv611d3dXZ29XP2e9vY05NS1Fr9+tf9+9nY/93b29jXVtdX9vT0c7IxsbG3efjZ1NVY11VUVNnZ193b+9rY2v3/2/b09vX
+ztff519TUU1PV2d3711db2djY3fnXV1XX+930cvLzMnIysrj7913TU5nWVdPY2drb//rb2dbY+vvb8/V59nX5+t3W1lRTlNr72/jZ2dva2Nrd/dvY2937+vbztHP087R2eP/a11ZV2NXUVVjb/937/f/Z19n7+Pn19nb09vr62tXXV1TX3fv9+93XWf/Y2dv
+d11fZ/fn39/Tztvb0+vj3/fvb19XW2Nba3dv6+/n92dZY2v/69nX29vn5+t3X1tbWVdr7/fr6/93Y3dnb2drd2/37+/Z49fX2evbd+/va2Njb19ZX11na/fn69/rd19n///j29/b2+Pj63ddWV1jb2d36+d3/2djb2drXWdna/d33d/d2d3Z6/dnb+/3d3dn
+W1lf/3f/7+vj3+trX2vvb93b3d/v7+/vZ2tdXWN3a+/n7+vrb2NrZ2dfZ29393fv29vf3df/b/93b/9vZ29nWV13b3fn793j6/9fY3dr69vj3+Pn5/drX2NjZ/9v9+Pv7+9va2tvZ29ra///7+/f3ePd3/dfX2tv5+dv92tjZ+v/9//32/f//19nb3fr29/n
+6/f//2tjb11ja//36+vj7+9rb19nb2Nvd+/r/93f39/n71//b2v3/2v//29nd///7+vf3+frY11vd/ff3+Pr5+//b2NjY2N362/v/+/3d29rY2dra2dv7+/r49vZ4+P3Y11nb/fr6/fvb2//9+v/6+Pn/+9jZ3dv7+vn6/f39/93b2tnX2P/9//r6/fr92Nd
+Z19rY2/n7+Pr3dnj5+93a2tfZ/93b+v3a29v6+v37+Pr929n/2f/9+fj5+/37/9nY2dja//39//r9/93Z2Nna3drb/f34+fj1+Pv92dfX2f/5+P37/d3a2vv7///9/d3a/9va/f/6+Pv9////29nZ19na+vn9+fn5/9rZ2Nja29369/n59/b5+/3/2djX2v/
+92/r529rb+/r//fv5/93b2dvb2/n3+f//+//b2tra2//6///6+v39/9vY2d3d2937+fr59vr//93XV1n/+Pj5+vj929n7+P/d3fvd2//d3dvb+vn93d393dvZ2dnXWv35+vn5+fvd2tdZ293d+/f6+/j2+fvd/drW2Nv/+/v5+t3b2vv6+v/9+Pr929rb2P/
+9+vn6//vd2tnX2tjd29v9///9/93a2Nv6+v//9/f7+PZ6+93/19Za3fn3+fr6+93Y+/r/2Nv/3f/d3dra+//4+fr/+//a2tdZ19n9+vj//fnb2trd+/j3evv6/f/9+Prd19vZ1ldY2//5+vv92d339nb9/fv/29na2dv73fr5+93/3d3b2vvb3d36+v/9/d3
+d29rd/fv///r93f34+tvZ2NjWV9v9+/v7/f/a+/f2+dv//////fvd//v//f392tn/+/j3dPf7+v//2tv/2djX1tdX/939+vr5+fn429n3ef/Y11nZ//r/29nb2f/6+vd3d/j6/dv//93/2t3X11ra2trd/9n///3/293/2N3Z2tvd/935+fn49/j/2/f53dr
+b29n//drb2tjTlfAub7M2fdnY11dX1lTV2NfVWdjX2f//3fn5+/3b3d3Xdnd29/b39/Z293j729dU1VbU1vOyP/Pz99v5+/n/29ZU1NRTVFRUVNZW1NXX3f/79/r1dPP08/IzM3V09nj/+fra11PT0pMR0dNxNNJd7jG3XddT1lrXV/va11v6+fr19vf2dfZ
+zt/n7/9vb3f349fnd+9jY2dbW05MSkJDRURBTFXBwNfn29/Z02/Ny8z37+/v39PJzM/P2d/d79vv73dnXWtRb19ZX1tvY29VTF1VW0pFSURLST88TaKpy9XvR0FXU/fZd1tXU09T99PZzM3Tys7Z08/3/2PvY1fR3c/OzdfZb+dra1dRT0dJRUQ/Qjs/rrfZ
+729jRlt3d9Vj5+/3d2PK1crRy+vj611fXevTW1Nbd2Nf72f/5+frTvf/709OTE1fVUo1vramwW9NRjRPXUtZR0tPRkRO4+vdyca/zL/CwMvPz8PPx7zO1dXZ2efrd2dKS0tGS0k/Q09ORT5Aq85ZX1UuO0M7PElIQltLa1fJ28q9vcW9vLrDu7rDtbrVwMXN
+ycXLzuNO429RTTs+Ozw4LrfbNsauXT7XSy7fXz4/Qzk1PTs/TldRz9X/08zf28/nur+1ur+8trnAvL7AwNt3Y+dRSldITltDRTkuo9NJR0kzRcx3UeNVRHdjVU/RTllTZ2NRQV33Tedn10vv18jLzb3CzLe7vc/d3WtZOtm011c4K7Hrb0FIPkzfzVlva2d3
+69nRz8n328hrRUVJTVNdT8xn7+e+yszB0cnXwcTd9853//dfX0NBPiyqzuNRP0E9wc1b/9VRT1lTWd/T28nL3/9NREc+QDs/SUdfX8n/yc3Gxr+7vr3JxsW+z8b/VUYvq61vO0lnQ1+/S1fnSmvvZ2Nnz+//d1lBPDs+TjpEXVXvysjVs7q7u760vL7Oxc1d
+Y1VOST9EN++vyVFGV0rr6+9R2Vljd1FJU+/rS0hIP0A9OTw9PkNOTHfDwsmyuLS1t7q+wcS/ztdMS0hCPiuurNtBRkxFxM/bwcf/09dnS+/v91tTTD0/QTk5Pjk+TVtP1d3NvLmwr7C0s7rCzd3rU0hPSzUuU79ZSUZFSsfXy9XOxnfbWWfd2+dj918/Qj46
+Nz06Pz9ATltv18a6squtr7G6xsvv/0xNRj84R1lKSUQ/SUvZZ9vN1d3DysvZ28rP993ZWUxXS0RNSUVHPkdXTv/T18i7tbGzur3Cz2dbRDw3OkI/PjxBQUNMzu/MwcjJyci7x7+8xtPZd2NIQz8+QUFKSkhMSuN3z8zPv7q+sru7wdPvUU1XRDo1NTQ1Oj1F
+Te/V2cbAwcC8u764vMPK0/9fV09MQEk9RkRBRk13TO/fz8a8urO3ssDDyVtBOjo1NDEzOEI/SU1X1+vLusS/urq+vbq7wMrX2VlNRERMPUVFTU5RSE/3Z1fXxb68u7/GwW9ORj83R0BBPDxBRUdNUf/n57mur7S8xczLzd/D13dOTVFJS0ZVTE9PV0R3T+9f
+b+/Lv7q9zctXU1FTPkVKTU9OV01LS1dPxNHDzs/Myc7Hys7X219TWfdZWWNfZ1dXZ11VTGtPXU132dPFyb2wuct3X0k5QkI8PD5DOzrZrq6xvMTL79nd98//TF1vW2dvY/9bWf9RP2dXZ09NZ1NZY1nKyc7NvsLZ78rPWV9ORUBB/1NbW2tv2dXVy8vLz//Z
+ztv/V1NfW1NVVUlTS0xd72Nra09IzFvL78brvqu12UxFPDo6QEo7xqy0wt/fWWf3X1VrVdld42fZ09nrd8/rd1Hdd3dZRUtAPtvbWVNTVU6vrLDO02tfRU13//9KTUhKQ0Hr4+PT19vO3eP///9jW01jUVdvV2P3y87j51nOW28909vPP8Wqz2djXUA8V0RX
+O7qstsBba0NrSmNfd0tAR0tOWetj19fNzePftru/xWvJ1/9GZz9nXUs4orxGTlU/RD9rWf9OS1lNPURvZ03dyse7zr69zM3Rd+f/d11VT1tj087nyr3PTUxIOeM/NajFQEQ8ODE0S0RTRDequdVX984+48Hnt9HFvry+u726vb7EwcfH473K58Zfbzk0Njw4
+NCinvjs9OzUtNkFBV1Nd5/9rZ9dZx8rGubi2vLm7vcS7vbq8v7/LxMPHyde/zVc/UTk2OCwrRcU2LSwqJCwrMzU4PDY7q+fLa29fW8m12bWutq+vqaytqaiop6ijp6WsoaypxtdPRP8xOSOlXS4sKh8hISEkJigjJSMiJygvLi40SUA6T//Vzr66tLeztK6v
+raqmqaSmqKeurq+ttsWvn6rBvdE6PDMvNjo1NTMtNMtKRiw7JSoqKiovMDBJRO9R49nIvLu6r66sq72xv8XNvdHZPJ+uvrq9413rb/fRb1dv3T9IUT5LNDczLzgsLjcuNzI3PUVARUZRb9+/uri7x8TOv75ryE3Mprjbwb9O3cZrxL7Gv7jAVaWlrLLvvjE8
+NDkzMjs0OTA1Nzk4PD5T3bnX70c/QjpJQDs9I6DjTM9jRl3Fy7+srrWvsLW2sba9vsO9ycVb5/drUd1bSVFjVU1BRndvY1E5Ni0oLTgqLR+2uTw8Sj1r08r/w7u/t7a70dWfp6yusLnfy9XdyltPx2/v18zL58i9srjCY087KysuKSMoH7Y5LjI2PTM+SFHD
+47rCuby1sLa8vrnF1dnVycO/xcS6zb+9v7u+xK21tsHfUTQ1Kj0qJyJOUzIsMy0uOVc9Sc7nzsTT1VO1obC2uGNV72/Nd8TZ97/TvMS9sLiqq6qvtshnPzQ+Li4kLb41Nzw1LkZKNU3nVV3/Tf9r09d3Z+tjRUTb49tZ09tny8K+u7axrKyura+7z1M9Pj0r
+Kbq16ztdSTVIW0/3zsbJyUpnb0M3qshd3TAwLjpAOT49OD3fVdnCvK6pqayos7/d4z/vKjVnt9lKZ1lKWcPnysDFyL3T99tT919LOe86MTFAPTk4QzY3R0Jda9PGs7O1tr6/XVk82TkzRbL/3W/j78vXurS2vbuwvMW9z+c1vsfC4y01Mzo5OUU8MzFTPmNb
+38TJvLvLzE0/Nj45K9271+dn18nJvbG3rrGysLG9usHJ4/drPGc1NDtLPE5GQj46XTxVWWPG09/d60ZFOTcxOSg/1chJZ+PT2cSyva2zubOvube2w+9nvdvHRTg5SEFBP1VJOE132dHPu77Gx9tNRjYzLjIpLjxfZ01X99nVvb+0s72ztbe4ub7Dz9VORV1H
+PkZda2tRX0hnzv/v68XAycPJ/1FAOjcuLzQ7Pkc/SENdX3e/vri+wrvAw73D1dnva1PLSVNMW29nT3dfXevVzdO+ur22u8TN40c9MTcsOTs/S1NjV1njxcS/wcG/xsjF0f9fV0g9VTdAQERjV2vZ/2vEycfMxbq4uLjL2+9HQTQ3P05ZVedd72fvzcXFwsXO
+ztHL2XdbUUw6ODM1ODo/R11XUVnTzM3PycG6uLa2vczG40NEO0o+V1dj2WfZ28LHwLW4xcvLx85vTUtHOTMyOzE6PD9LV01VyevPycS/vbu5uL7P4008PUBESu9f1c/Jz+e7xry/u7zJy+tva04/RD00MzA0NjxAP1VNV8n3zcnOy8W7v72+ydXbTUFIQl/f
+a+/VzsbZuce7uri4v8rF719VPj48NjQxOzc6R0VKSEzP593Mz8jFxsHAwMnXb0U7RD9FY1njy8nHxL26ura1trvIxfdrV0lBQzg2NTU5N0VBSU1ja9frzMrGysjJzc/nb09EPkRARedb9+vZ2cDHvsC3uba7wsXR/29OSkk8PTs5PkFLUW9r593N28rGyNHP
+2+ff619PQjs+Qz9XY1nj08rMyMLEv7y6usfMd1NVWUpGRk9DSU5TTVdr4+fPy87Ry8jO2d//93dPRUc+RkRFR2dZZ/fT19HLzcLJvcTT091rW11TXVlVR05na2Pr18/Zz8rLysfAxM3TY2v/U0ZJQz5EQkNGS1VbX+ff49PKzMfMzv9nY0tNSk5PSEtbb2/j
+3dHGx83Av8LGw8PIz8/f3/dLU0hDT1VTT11XY05r/3fn0dfb529OR0pJSUNMTUtdXVFn5//b2cjMy8HCxcXBx8zM0e/V/2dvVV9bXVtjW11j7+vj993vY993Y05PUU9KSExLS0tPZ3f349Xj583JzczMydHV3eP/1+P3/19vU19v32Nb29/n39PV49/Xd/9v
+T05RT0lOXVNXTlVj99/d3/dv4+f/09Xb2dfn999rXf9n/+vv7+frb93r39nd69/343djV1dZX19ZUWdjd29j39//29vf49vf7/ff62dr919rY1lfU0xd72Pn0ePn3dHT5+fV5+Pfb2NnXVdvX1lvd//v/9vd69vn/2/r7/9r//9dV2NbY2dbWVlOZ2Nb/+/j
+d93Z2d3Z2d/f329j///r/+d339XV5+vd19nVz/f///9vV/dvd1VOTExTTU5bU1lbXWNvd2/f39fX2+Pn7/fvb+f/6+/v79XRzN3/2dn/1czX3evnd2f/a3dTV1NZW1dLUU5PX2tbb2djb2//9+fv0ePv92dvd/9v92tv2dfn3evX3dPN0c/V3ffvb/fn52td
+V1tnTlFOVVlXU2tf/+vv6/f3d///Z/f/9+/j39/r7+fT6+vr2dXj693ba11j4/fj1+NrX2NvWVNRUV9fZ1nvZ2djb19bY293b29vb/fn293r729349//6+fX493O3dv//99359nj73drX19dUVdZ7+v/a29vX/9fd//n3+t3XU1ZX2trd/f/d//n4//v5+Pv
+4+fX7+//59/X3+f/62f/d2NnX2NrWVnj2+ff3ff3b3fvZ2NbZ19v/2/////35+9vW3f3b+/r7/d3b+v33+vnb2tjb2tnb+fv6/dv/2/v39/r79nj3eNvY2/3//93a2tna29r93fv62ff7293a19jW/f/429fY2NXa13/Y2vf4+/f593X3dHZ3dvX72Nra2f/
+6+fvXWt3/3f/7/f/7/9vb3d39+vj/+93b19bZ05XY1Nj7+Pj2dfn5//j3+fv2d3d9/fv62t3b29na/9rX2dvd//n5+/3W3frZ3fr3W9dWV1bX+t369v/d+Pn59nX6+/va93n7/9v6/9jZ/9jY13r3////2tn/+9v93dn72t34+N3/2tdZ11fZ/93/+vZ4+/f
+29vj3ef392NrXWPn/+Pr92tvZ+9nY2f/b+fj9+fvb///d29nd2dfXVtbV/f379vX39/3193n6+vna2tvY1tr7/f//+9va11rb11v73fn3fff92N3d//v5993Z2NjX11vZ//n5+vf6+Pf7+fv9+vv629fY//v9+t3/2dn7+drW2dnX+//5+9r///34/9vb29j
+Z29bWWf/3dvd1dfd193j6+/392djVV9rd+/n7/fva193b2f/b2d3b+/vb3f339vr9+9vZ19nZ2dra//3b/fr5+fn9+fn//fv411j9+fv63f3/29n6+9rb/9r/2Nj92dna+/d5+drd29va3f/d+/j2+//92fv6//j3+Pr7+9fVV93Z//3/2/39+/rY2tna/f3
+9+Pv92//9+v/d/fvd2v/93fv39/r9///9+N3d2939/d3b19d//fr9/f/b11vb2Nr6/9v/+vn4/d39/93d29ja2/v4+/r593n69vd3+/f/2dnb2/va/d3X293b/9jd2tb/+9nY//v/+v37+/3d+v37+93b/9rY2djd+vd3d/r4+fd4+939/df/29vZ2dvd+v/
+a2/3d2frd19ja2f/b2/v5+/v3+Pfd29vW1ddb//n69nb4+Pn3ePr7+/3d3f/d2tnX/93/2Nr/3ddd+NnY2v/b3fv59//9+9j/3dvb2Njb29n6+//d+vf493r3+/d493j93d3Y1tr/2/3a+/rb+fvd2N3b/93/2/v7293a2vra29nZ19fY/fr6/fv6/fv4+Pd
+3+fr93d363dv/+vv929nb2NfZ3d3b2fv6/d33dvv63f3/2Nnb2Nj///39+vv7/fvd3f37/fn4+vvd/9rX3f3a2d3b2d3Y2/3d/9v5+fnd+f/d2v/a3d36+trb29rZ//n7+vn7+fv7+fv5+/r93f/63f//+/37+93/19jd2v3d2dnZ2t373f/72tva2ddXV9d
+Z3fr493j6+/v///v9/d369/v9/f39+v3/+f/6+t3d/f///dvd+9vb/f/93d393dfXWdfXWNnb//r//d3Z2dr63f3393v7/93Z2t3/+vf5+Pn4+/r6+/r5+Pj///rd3f/7+/rb2drX1lr7/fv5+//d2NfY3dfWV9nY2dvZ2Nv9/fn7/fr6+d39+fj2+Pd2efn
+3+fj3+fv7/93b29nY2dfa3f3/293Y2Nvb2d3d3dvb11nZ29rb/f/a2vv79/f69PX3dvrZ19jY2dra3dnY2t3a2/v993Z1dvn5+t37+Pnd/9vZ2/f3edra2dna19nXVdXXV9r73fj6293b+fv7+/r7/9vd11j/+/f3d/f293jd+v/3+vn93dva/d3Z2v3d29r
+b2dja2dnZ2t3d99r5/fv/+fV4+vd6/ddTe9fWV1v/29dY29ra+vNzMrLzNHX193n699ja09RUU9VWVlTVW/nY19fV11ZV29ra//3///n49vb0czMz8rOy8/L1dfd2df/9+9dZ1dbWVdTTFNMTU1NS0RDU1VKR0xVY1v319nOzdXNztPKysrIyMbLytnR0dXZ
+6//3Uc+1uMVnV0k7Pj0+PD07P0lIY1v/6+vV2dPZ9+djY0xrTk9Zb2tjyM/N1czLzs/RyMnPvsjJ0dfvX1tvZ0dTQUfRb1VXVWtXd11351/3291r5+9nV09NR0xHRj1HS1FHT+tf/9XLxr6/vsG8vL27u7q5wsPb4//va1NGMdGnr+tLOzguNTQ6Njk4MzQ8
+TEz/293X3XdrW19VT09jd1v3/2dn08LCv766ube2tbexsK+vr6+vuLi8xrvIsK/JQTYqJCsqKSoqJigsKzEzNENIW1dbW2NTRtfDzevVU1FCQ0ZBSf9r1ee8vbu0s66wra+vsrezt66345+fn6q50TM6MjM0OSsuLysrLjc5MjpDPTs3LjQ3NUTZb1lr40pj
+Ss3Az8S8wsnA1b68vLu4vce7zuf/W2tJoayuvM3fREtRTUZHRTxEOT09Pz5RU0xGST07MzY5PeNL/+dOQ0BDQU7V59O1s7a0trCxr7WxurzH1dvRPt2fn7a690EuNS4vQjk1Pjo4P01ISv/fzsPfX1tTY0lMyu//70VDNzBDT0FGRtn/59XCvsO9u7y9w9Xf
+72tDrKey173LT0lMPkNORkRNQzpFRkJH79fRz93vX1/v1dnjvM7JY1k9OjtHSkVMxMHP2cLOxcK8yufI2VdV6z5frZ+8u81KPjU3QUFDTklZQkZR41fXzMzKw83/19v351fnXWdZSDk7Qk5RSdnv503rwNHJwLzEyc3ZXUxrNqKpw9/bxz5n/0zf72/N70xE
+b1U/X9NV72NRT0hLU1vvw9HvTlc+Ojo/T0jvb83V29PCvcXHwsbb3fdrWVMyuMKlt7jEPWc+TmdL18/dWWP/a1X/69PVZ2dVT0tRT07/UVFIPzwsOTxGQVnj2d//ucXIvru6vsnT1+fP5+9Do7tO50RBT1NGZ8bd2/dbU0xvTm/j719fSj9CX0FIUetVWUg7
+RzUvOz9N19PGy8G1trWus7W8v8TN2czZ0TWfR6my328/TDHO/0NXTEQ/RUo/Q+9jU1FHS09OSVFLa0lVQUA2Pjo/5+PKycjVzLu6xbW5vsPT0dPj/8/ZS622v0lVSz1G22vXT29jRz4+QU1Oa1NbT0VIUWtX/8a+53dNSD4+M01ZStPFyse/uLS/s6+7y8HR
+x9/b08xnTq1HtcFTQlNOL09JPEc/PDlBSl1NV/9ZT1//b+Ndus/bSExCTkVBVW/Jx7/Cu8G/v7i8vr7T71vnd11R61E3pNs6PTc/P1Vf319jWUdAOT4+XVNVX01nY/fV62fAxsb3a00/SUhL18PJu7q4t7a4sb23ws3r3+Nrb0//SjynZya+uUdCRks2Pzcw
+NTk2OldZb3fR2c7TvsC9wrO4zv9dTVFIQlNjycTGwsK+vsa0xcnZ32Pfa2dOVUo9LaldLjs4NTZET1tPPURBOj1BV0/v13fX09HFxcm7tbrDzU7vQUZGTdHfy9O4ubu5v7LBv8zLd///Z1VHSi05uzwuOia6W1/nSz43Qj43PVlT19Pb18XAwr21va2vuczf
+a2s+TD1C/1nV58rGzOfCvOP3b+9j/2PjWUZXM7/LQzdANjA9W053S0BbTztIX1/d2dHbwcC/vcO/s72+zcpTVUZMTUdbym/Pzb3K3b+91W/f4+t3b19OSDgvrkozODYuMTiqu75fX0pIQ0XX1+PdyufR277JwcS3v8fVVUtEPztERE13/8/OwM3nvsXPd+/d
+62tnX05FRTWyZzk2OzZKTmfXa01J519FX9XP68vbyb7Lvci+trvHztFXRDxJRD9FXev/18jZa+fF49f36/d369VZW2dNMqtnQjg2QkBJNKiqzuvK40hX0efZze/FwN3N3ce4ytvIZ0o8Nk04Oz5XW1nOzuf31cXZ0+Pj/+ff93dVX0dntVM4Nz9BU/fnzMxb
+Z93d52/Kysjfz8nDzczMysnZ0edXQTw4PTk9P05OY9PX3XfZxM/V999v39vj1d/MU7W4z0VDRj3v9//XOqu/u8/fb1Xr9+9v43dra29fzt3GT0xEQTs8R0FFTmtV19Hb79PEz8rZxN3X3+tv78td97nRTkI6Qm/v5+PK/1XR583v2+fn3993//9n52/ZV9dV
+QkNBOj4+REdM5+PMxtPTz8HRy8/N69PZ087Tv9FXV8nLY01HTXfr218xyrTD09/dUV1XX+NdV1FT71VV61dESU5ISENHTffdX+fPa///0cXT49fbydfNx8TC02Pr591dX1FP29XL03f32Wv/U/9na1//a1tXT07vX1dXT1NHS0E+P0lOY2//38nZ18jM09Pf
+zMvMzM7Kx83N0dvj0ePrXXdd///nSUG5u87b/1lMXV9ZUVtRT2tTSk1GQkFGSUFHUU/n/+/n2d/bzc7N0dfKy8jOxsTHx8fN02PV92dvZ19r4/djXdNdZ01XTk5PV0pOT1NTX1tXd0pMTklLTFFd/9/Zzc3VzMrKzc3b/+PNx8/My8vN09vbd3fvZ1tfXVFd
+XVNXV11jX2/3W2tvd/djXWt372ddWU9fT01ZT1fn79fNzNvbzdXn09Xn793J38/R0c/Zb29bWVtXW2tnY2trY1d37/9dZ/9bV2drXV1vWWdnXWdbX3dfW3dr9+fZz8/J483Rz9XV6+d358zd2+fn3ev/W1lXW2dfa2tv/1tdU1dfd//n7+tfa29v92d372tj
+Y1NVW1lbXW9j9/fb3c/Xz9XZzdXV2c7VxNXT2+Pfb/9XXV9bW1dXVV1ZV19ZV2dvb2trd29v/3f36/fra29TTmN3Z1dZ6+fn1dvd1+PX39vf6+Pf49/b3d/3d+P/X01bVVv34+fnd2tnd2NXa2Njd2ddW1td9+/n9+d37+9r62/na+vj9+v37+vb4+Pn2/f3
+/3fr29Xn3W93/2dZV1tZW133b3frV1trX1Vrb3f/d+/ja+fX2dfjz9/r//936+P3/2/n92fv//fn92/3Y11nb2/v5+9ra2v3Y1lZT1tVZ+fr699v7+v3d+vf693f4+d3693b3+vn52tfb1n3Z3fr/+dva3dr9+Pr4/9vY3dvWWNr7+t3d2dZUVljd11n5+fr
+2+fv43f/79fb4+vd4+vn2dnr1+vja19nZ/dn5/93//fr92NbW19jX3dbb2Nr7+vrd2tn73dZY2tnX+/d5+vj7+ff/3ff7+fv7+vvd+Pd2d/j4/dvY//vd1//72v3b2N3X1tbb2d3b2/3Y2dr/9vvY+vra1dZb11n7///69/3//9vb9nZ49/j3+Pf29Xf3d/d
+5+f/a3dj52//a2NjY2tnW1FVVVNZW19dXf/v73ddb/fr5//j7+/f19PX29nX1ePv5+/v6+Prb///3ef/d29va2tna3frb2//Z2dvY1tZV1dbU1dfX29fZ/fv91/j59//793d7+Pf39vVztnj693b9+/r//f/6+Pb6/9vd19bW19ZY2tvY29fY19jX2NZV2Nj
+X///6/fj2dnrXe/r7+/r29/v1dfb09vP2+Pj3+Pvb2tj//9vd/9nZ1tbXWdfa2NrY2dnd3d3d3dva2NdZ1tbX2d329/r6/9vd+Pr3+fn3dfX2evb39Xbb2/rd2trY2Nfb/fn3+frd2tvZ11na2trd29jW1tbZ2NjX1lXX2dvd/ff59XO4+Pr4+Pr3+Pj3d/d
+3evf3dtv//dv/2dvW2Nv6293729nY2NnY293/3dv//9va/9rd2dfa19rZ2fvd+fd3eNv7/d34+vn2+/d3d/b59nb5/9rb19nXWtRV19n/+/jd2tvd11ZY2vr5+vn7/d373f/a2tvZ2/39/f36+fX19vn7/fn7//r6+vf4+Pv/3djX19na29jY11bb2/r7+/3
+b//vXW//d//37//372/3d+93a2tvd//j9+f33dnr4+v3/2dvb3fv5+v35+fvd29jY2NrZ2tna19v/3d35+Nra/drZ1///+P39+P36+fv92djb/93//f3//fn3+Pna2//92dfa2P/6+fv5+/vZ19va3f/929jZ//v5+PZ3e/373d3a/fr6+fv9/d3d2dra2tn
+d29va3d39+Pv4+t3a/fvY2P/7+vr7+vr//93Y2Nja+t3d/93b/d39//n5+/r/2Nvd+fn3+Pf49/n63djY2tvZ3d3a1/3d//3d2trb3djXV9jZ/fn6+vjd/93d2//7+Pv7/f36+vn5+Pnb/93a2v39+/j4+Pv6//v/29v7+//9/9fZ3dr5+9vb2drX11dY/93
+a29rd/93Z2dfX/f/7+/v6+/v3ePj3d/f7+fv7+Pd2+Pn3+Pn7+v/Y11ZX2Nrb2drd293Z2NfX293b2dnZ2t3/3f/9/d3b19j7+vr4+Pn7+fd39/r4+P/7/f37+/36+fn7+v39/9na2dnb29rZ2v39/d3b29nb19jX113//93b+/r//93a3f34+vr4+vj493j
+4/f/d2N3/+/j3+Pf7+937/9392dfY1tfY19rb///d2dnb+/37/dva//v5+fr5+f393dva3d3d2/rd3fr7//r62tra/93d//n7/936+fr5+fn/2dnY2/v6+/v5+vv93d3/29va2NnZ2//b3f/b/9vXWtr7+/r5+f/7+vv7+Pnb29vY2f379/r7+/v9/fn6+fn
+93dra29va3f/6+v/a2dv//f/a2tvd///a/93//9rZ2Nvb+//92//6+/v6+93b/fr7+fv7+v/7/fv5+f3929vZ2tn/+v/d//f93drd3dr/3dnZ2f/729nd3dra11fd/f/7+vf6//f5+vv3e/3d+9va/f36+/////v//fv73drd2d3/2tvd+fv7/f//2/v72dv
+b+/f929ra2drZ293b29v//dvb+/n7/fv/29vd/f34+vr63fv6+fr6+/v/3d3b3f39+//7/f/d/9rY3f3a29v7+frZ2/3a2NnZ2f/7+v/929vd3f39/f/b2trZ2/37+vv4+Pr7+fr4+vr7+/n///39/f/7/dv72tnb3djX2f/5/d3/29nXW93b29v/293/3dv
+7+fv6+9vb/d37+vv6/fv9//39/f39+v/d/9v////5+f3/3f3d2t3d3dn//f/b2trb29ra2tn7+vv7/93/+/n5/fv7/f/d2d37+//6+/n92/3////9/93//93/3f////3//93/2tra2f37+vn6//vd2tnb/fr6+fn/293///v/+/37/9rb/9vb///b29vb///
+d+/vd3dvb3f/9/fr63dr/3f/9+/rd+fn7///d3dv////7/fv9/f/d293//f/d/f/b2d3d2t36///////d2936/f/9////+v3/+v3d////29vb3f/9+/v9+/3d293b2/n6+/393dvb/f/7+/36/93a2tva//v9/dv9/d3d/f//293b29v/2dr9+//b3fr6+/v
+7/fr7+f3d293d/93/+Pvd293b293d//v//d3/29v7+v3//fv63fv/3d3d3d3a29vb/f/b2/v92t39+93d3f/9/f35/fr5+/3d2/35+v3/+/////39/93/3f/a2dra29vb29vZ2939/f39/9vd///b29v7+v3d3fv5+vv9+/3/+vn/3f/9/9v7+fv73f39+//
+b/93Z29vb2dj/3dv/2Nna/93a/////f/b2t3/+/j3+/v5+Pv7+Pr7//r92937+vv/+/v/2tvd29r///v/29vd3djb///b3d3d2//d2tnb2937/f3b//3d////+vr7/fn92//6+vn5+/3//fn6+936+93b2tvb2v/b/d3d3drb2dvZ2drb/fv6/f3d2/3/+/3
+6+v/d/93d2//7+/37+v/a3f36/fv6+//b+vv9//r9/d3d3d3d///93d3/2drb3dvb//37/fv9/9vb/f///fr9293//d3d+/37+/v6/dv///39+vv93f36+/vb+/v/2trb2t3////a3d3b2tra2dnd//v5+fv/293d/f37+v39/fv9////+/v5+vn9293a3f/
+/+//a3f/b2//7/9vd///b/////939/dvb///Z2v////r5+vvd293//fv7+/3/+vr/+/r6+/v7/9nb/f/d/93d3dvd/9rb//3b2//d/9vb3d3d3fv/2937/drd/f3/+vn5+/39/93/+/vd///9/dv9+/39//v7/////f/9/93d2///2t3//f/d29r//fv//9v
+//f/d3f37//37+//6+vn93dvb2tv6+f///f393d37+/3//d3b/93d///d3dvb//vb3f/93d3d/f37+f/d3f//29vd//3d/9vd3fv5+v//+//b//v73d39/f/b2/3d3dvd3dv9/f39////3d3//f/9+/3///////v6/9va293a3f/7+/37+/v//fn7+/v7/93
+/+/39//373d3d29v/3dvb293d/////93a2t3/3f/d3dvd/fv7+fn73d3//dv///37/f3d293/+vv//fr7+//9/dvb3f3d3f37/f/d29nb2//////b2t3b/9ra293///37/f36+v3a2//////6+vn4+fr9///7/f///f/b3f3929vd/9rd/9rb293b293b293
+/+//d/f/93d373dvd///9+vn5+///+/37/fv7////3d3d/f3b293///3//93b2t3//939////29vb3f/9/f//3d3/3f/b2/39+/v6+//7+vv/3f/7+/v7/fv9/f3//939/93d3d3b293d3d3b29vd2///3d3b29vb3f/7+/v9/f3//f37+/39+/v7/fv7/d3
+b//39///////9/f/b//v/3d3d/93/+/3/29vb29vb3f//3dvb2tvd//3//fv6+/3///39/f37+//9+vn7/f//29v////9/fv7/93//9vb29va29v/3dvd29vb29vd//3///3//f//+/v6+vv9///9+/3/+/v9/fv6+v3/3f/d3f//29vb3d3/3d3d29ra29v
+b//37/f39/f///////f///93d3f39/fv7+/v9/f39//37+/v7+/v7/d3b3dva29vb29v/3d3b3d3d3d3d29v/2939/f///f3//fv6+/39/f39+/r6+/39/f////3/////3dv//f/d293b2tvd3d3//f3//93////d2trb//37+vr7/f39/f////r7///7+/v
+9+/v9+/3/3d3d29vb3d3/3f3///3d2trb2tvb293d//3//////f////39///7+/v7/fv/+//9+/v9/////fv7//3//9vd////3d3b29vb//3//9vb29vd/////fv7/d3d3d3//9vb//v7+/r5+//9/d3d3f39/f3///v7+/v93dvb29vd29vb29vb3f///f3
+7/9rb2//b3f3//f/////////9///9/f///fv9/f////39+/39/////////f3//9vb3d3d3f//3d3d/93//f3b29vd////+/v9/f3/2939+/3///v6+/v9/f/7/f/d3d3//f//3f///f3/////3dvb293b3f/d3dv////93dvb2939//37+vv9/f/d/fr7+//
+9+/v7+/393f/9/93d//v9/93b29vd3d3d2tvb29vb3d3//93d//39/fr/3d3//fv7+/r7+/v9/f/d/f3///36+/v5+vv/3f3/3f/d/f/d29vb29vb2//d293b29v//d3//////f/9/9vd///9+/37+vv7+/////v5+v///fv//f3/3dvb3dva29v//9vb3d3
+d///9/93b///d3d3////////93f393d3d//37+/v6+vv7+/3///393d39//37/f3929vb3d3d////29vd29vd//////3/3dvb3f/9//39+/v7/fv///39/fv7+/v6/f///93d///b3f/9/f39/93b29vb29rd///d29vd3d3d/f3/3d3//939+/39/f39/f3
+9/f3/3f//+/v7+/v9+/v93dvd3dvb3d3///393dvb29vb2//d3d3b2////fv7+vv93f/9//v7/f39+/v9+/39///9/////////f/9/93/////29vd//39/93d29vb29rb3d3//93b3d3/+/v7/f////3/+/v7+vv9////+/3//93d3f/9/f3//f3//93b2tr
+b293//fv9/f/d3dvb293////d/////fv6+/v7/f/d//39/f///93//f39/f3////d3d3d/f3/+/39///d2tv///39/f//3d3b29rb//37/f39/939/f37/f3////////9/f39/d3d//39////29v////9/f37/f/b29va2t3d//37+/3////d3f///fv////
+///37/f39///d3d3//f39/f3//////f3////d3d3d3f////39///d3d3d3f/9+/v9/f/d3d3d3f/9/f///////fv7/f39///d3d3d//3////d3f//////3d3d////////////3dvb29vd3f/9+/39/93d3d3//fv7+/39/f/9/f39/f/d29vb3d3////d3f/
+////9/f//3f/9/f/9/fv9/f3/3f/d3f/9/fv9+///3dvb3d3d///9///////9/f39/f//3f/////9/d3d/93d////3d3//f3///39/f39/f///////////f3////d29vb2///////3d3d////3f/9/93d3d3d/f37/f37+/39/f3////9/f39/f//3d3////
+///////39/f3/3d3////d///d//39////3d3d3f//3d3////9+/v9///9//39/93d3f39/f3///39/f///93//f////39/////////////////////93d3d3d///d3dvb293d3f///////f////v7+/v6+vv9/f3//93////9/f3/////3d3//93b3f////3
+////////d3d3d3d3d293d///////////9/////fv7+/v7+/39+/3//f3//////93//93d3d3d293d293d3dvd///d///d///d3f/////////////////////9/fv7+/v7/f/////9/f39/f/9/f3//93d3dvb293d////29vd/93d///////d/////f39/f3
+9/f///f/////9+/v7/f/////d3f///f//////3f///////93d/////f3/////3d3/////3d3d3f//3d3////////9/f///f39+/v9/f///93d3f/9/f/9/f/9/f/d3d3d3d3//fv9/////////93d3d3d3d3/3f/d//////3//////93//f37/f/9/f39//3
+7/f39/f3///39/////9vb3f//////3d3//9vb3d3d3f/////9/93d3d3d3d3d//////3//fv7/f///93d/f3////////9+/3/3f//3d3d3f/d/////////93d3d3d3f/d//3//////f/////9/f/d/////f3/3f///////////f39/f39/f/////////9/fv
+7/f3/3f///93d//3//93d/93d3d3d3d3d3d3d///////9+/3//////////93d/////fv9/////////////fv9/f39/////f///////93/3dvd3dvb293//93d3d3d//3//////f39/f39/f37+/v7+/v9/f//3d3d//39/f3////d//////////////3////
+/3d3d3d3d3d3d3d3////////d//////39///9+/37+/39/f393f////39///d3d3/3d3d3d3d//////////39///d3f//////////////3d3///////////39//39//39/93///39+/v9/f39/f//////////3d3////////d3f/d/////////f/////////
+/3d3///////39/f39/////////f39+/39/////////////93d3d3d3d3d3f//3d3d/93d///9/f3///////////39/f39//////39/f//3d3//f39/f39/f3/3d3//////////////93d/93d3d3d3d3/3d3//f////////////39/f39/f39/f3////d3d3
+//93/3d3d///d3f//3d3////9/f///f/////////////////9/93d3f/d//////3////////////////d///////9/////93/////3f////39///////9//3//////////f39/f//3dvd3f/////d3f////////////////3///////3///39///d//////3
+////////////////////9/f3///////////3/////////////////3f////////////////3//////////f3//f///////////f////39///////d///9/////////////////////////93d////3d3/////3d3///39/f/////////////////////////
+////9///////9/f3//93d/////////////////////////////////////////f////////39//37/f/////d3f/////////d//////////////39/f39////////////3d3d///////d3f/////////////d////3f///93d3f////////////3/3f///93
+////9/f39/////////f///////////////93d///////d3d3///////3//////////f3//////f39/f/////9/f3///39///d////////////////3d3d///////////////9///////d///////////////////////d///9+/39/f/////////////////
+//93d///////9/////93///39///////////////////////9/f///////f39/f39/f////39/////////f//////3f/9//////////3/////3d3d/////f39////////////////3d3///39/f39/////////////////////93d3d3///v//93////////
+//////////f3////////////////////////9/f3////////////////////////////////////////////////////9/f/////////9/93d3d3d3f/////////////////////////////////////9/////////f3/////////////3d3d3d3//////93
+d/////f3///39/f/////////////////////9///////////9/f39////////3f///////93d////////3d3///////3///3////9/f39/f///////////f//////////////////3d3d////////////3d3d3f/////d3d3d/////////f3////9/f///f3
+///3//////f3//////////////f3//////93d////////3d3//93d3d3d3f///////////////f39/f39/f39//////39////////////3f///93d3f/d3f/////////d3f///////93d/////////f///////f39///////9/f////////////3////////
+9/f///93d///////////////////////////////////////9/////f/////////////////////////////////////////////////////9//////////3////////////////////////////9/////////////////////f////////////////39/f3
+///////39/////////////////////////////93d/////////93///39////3f////////3//93//////f/////9///////d////////+///3d3///////////////////////3//f3////////////////9///////9//3//////////93d///////////
+/////////////////////////3f/////////////d//////////////////////3////d3f/////////d3d3d3f//////3d3///////////////39/f3//////f///f/////////9/f39//3//////93d/////////93d3d3///////////3//////f/////
+/////////////3f//////////////////////////3d3//////////93d3f////////////////////////////3//////////////////////////////////////////////f/////////9//////////3//////////////f////////////////////3
+9////////////////////////////3f/////////////9/f////////////3////////////////////////////////////////////////////////////////////////////////9////////////////3d3///////3//////////f/////////////
+////////////////////////////////////d3f/////////d3d3d///////////////9/////////////////////////////////////////////f///93/////////////3f///////////////////f///////f39/////////////////////93d///
+d3f///////93//////////////////f///////////////////////////////////////////////93/3f///////////////////////////f39///////////////////////////////////////////////d3d3d///////////////////////////
+///////////////////////////////////////////////////////////////////////////////////39+/////////39/f///////////////93d3d3//////////////////////f/////////////////////////////////////////////////
+////d3d3d///////////////////////9///9///9//3//////////////f3////////////9/////////////////93///////////////////////39/f///////////////////////////////////////////////////93d///////////////////
+///////////////////////////3//f39/f///93d//////////////////3//////////////f39///////9//3//////////////////////////////////////////////////////93d3f/////////////////////////////////////////////
+////9/f3////////////////////////////////////////9/f39///////9//////////3/////////////////////3f//////////////////////////////////////////////////////////////////3d3d///////////////d/////f/////
+//////////////////////////////////////////////////////////f///f////////////////////3////////////////////////////////d///////////////////////////////////////////////////////9///////////////////
+///////////////////////////////39/f///////////////////////////////////f/////////9///////d///////////////////////////////////////////////////////////////////////////////////////////////////////
+//////f39///////////////9//////////////////////////////////////////////////////////////39//////////////////////////////////////////////////////////////////////////////39//3////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////3////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9///////////////////////////////////
+///////////////////////////////////////////////////////////////////////////3///////////////////////////////////////////////////////////////////3////////////////////////9///////////////////////
+////////////////////////d/////////////////////////////////////f/////////////////////////////////////////////9/////////////////////////f/////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////9/////////////////////f/////////9///////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////////////3//////////////f/////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////3////////////////////////////////////////////////////9//////////3////////////////////////9///////////////////////////////////////////////
+//////////////////////////////f39/////////f////////////////////////////////////////////////////3///39/f///////////////////////////fv9///d/////93///3/////////////3d3d3d3d///////9/f39/f39/f3////
+////////d3d3d3d3d3d3d3d3////9/f39/f3//93d29vb29vb3d3d///9+/v7+vr4+Pj5+vr7+/v9/93b2tnZ2NfX2Nr/+/r6+/v9+/v7+/r7+/v9+/n6+v3d29nY19fX11dX19ja2939+/n49/f3d3f49/j5+fr5+v3d29nXVlXV1fr1dn/Y19dXWNn49/b
+69nb2dvT0dPX1+9vWVFJRENDQ0dPT13/39vTyr++vb7BxMPHzc/T5/dXUUhCPDs6NDzTwOdfW11VU1nTztXLzNHd38zKzs7R219jVU9IUU5Zb+vX19fNzM3IxsfKz9vvX1FOSkVFPz07OzUxMjUzNjXvur/N3cfBvLm5t6+0tra3vby5vr+/xtF3S0VBPz9E
+P0BLTEpARVPb21VNV0tDOjo8Pj0+OUe7zedHOzo2NzastN3/49vPxbavp62wr6y5ubmxt7vFxNvvV0dRRUpPR0VGPT5APjg4PUM9ODgyMjA8OkBbW1dZUUlDR01ANq6pwl9r2dndxciwtL65ubvBura5s7m3u87L2dV329//0/frUVNXQ+NdUU5CPTAvLzEs
+O87VX0c6PDYxOTc0t7bIZ11r/8jFwLS/xs3L1WfRys7R0dvX31nvz8HJvru5t7vFwsTn2b/G409PSD01OTk2ODg9PDk5Mzk+OTBTsdNIW1VJ1Wu+sL3Jw8LKv7+5vMDCxd/3d1X37/fZycPCyuPX0+9Xys/3Rz09LjCv0UxESkEzOjc2QkQ/Pi6px2/nd/d3
+58fCttPZ2cTFyrq5uMDDys7f/1/L1cvR0ctv/93Z/8zO399KSUVDP0JGPENGQjlFSUZJWWtPTarEW1FPT1N3ysfBWdfOyMTEurrD3dvva0VFR01v38/K1cr3587jb83L3WNdXy+ux9dGRTktNDQ0PUo+UWc4qLPX2//fSMjb58prd9HbzcC6vMrX29/ba1tN
+39/KzdHLz9vTys3N693NT1lTRkBDST46Ozs2NkpETF3/6z3BqMVJ/1tXyNfvy3fj387Cz8C/yc9fb2dOR1ndy7/D1b7L1c3GxtXO0cDOZ0vGrV/bOjUsLS4sL0BCO1VnLE+nwU/j3V+/yM/C78nFw72/ub7EzXdfb1lCVVnP0c/nzdXX0cvL1+/fd/dTa1FL
+RFE9MTMwLzEyRUZLV+tHLKW/S1lfY8vK3fe/38DFvL+7u7/O1VXnVU9j2bjBwMq9wsPRz9Ff13df90U8qudCRD8tJSwrKy9AO0XdVddDxKrB987L08fK58HDxsO4u8fKxdn3VW9vRE5M0dfZy8/K2eff2W9OTEdNW/dZTj1ERjUuLzM0NF9Lb+vX10w3pLlv
+wOu7z8jZyri7u7u4vLy+0/9ZUU1FTVvn02vIxtvf92tbY0hnTFM9X7DvS0c+LywvLS45TUZv5+u81VGir9G/zb3Kx//fzrnJv8HBy7/jUUFERDY7PkpbY1Xby93/a/ffU1NJVVnJb+tXX0dFNzIzNTpZWefjvt93Obyqxd/Cy9nLwtvHscm+vsnNzudNRTtJ
+PjtGQ9dR78zX2f9rT19O42t3a8+rz8FTSTQyMi42OEZLy1/IxMZTRaG577/N2cfVW+fCu8++0/fXb01EQD89Oj87UVdVd9nN729rW2NPU3fN/8Lrv1VJa1M1Oj9AT2PRz8jP0WM2n7bLyePHzcjX1b26zL7H13frR0Q4Pzw/PT5PW9tfzc/VZ1njVV/v789J
+or251Ug7OjYvODtOV81vy8jD3zmktL1338930efvZ7zrws7va+9VQT9BO0E+OUbdW1XFzNFfX2tVX19fzee51cjJW1lVPjg7QlXb1dXbwd1rP6ukuWvTz+PF4//Tws3VyltVTUY9Nz46SEBAP+PDY8HFydvf12fXyNfXvqayvdFVPTs+KzI7QmNrX8zTyVkz
+n6u/19/3/91XX9/R0d3OY1tdSj48QTtBRTc7791rzcLMy9vr0+PZ28XMw7zJxedfTkM4NzhTSN9P0d/ZQzOhtsjjd+9nzl1r19fT281PXVlLPztIO0xTTEz3vtnHvsHD09vV3cXRPqGtvb3/bzRGNzIyNjlCYz++X9VbMK2uvs3fY2PZ2U/Xzu+/619Id05B
+Pz48P0U+U/fZ2c6+ysfN787f09/L2b/Ru83nS1E/Ozo7TEfjSdNLT0kvtLa//+NV78fT48rHd+vFW1lPUT8/P0pVVVXfxLrRwb2/y8hr42/M69+hscnO50E0PjgwOTQ7Q0tMv9drTDSftszK62fv0dnn1cld0W9dUWdJSEI/SEhDSVvI0cvCwMPDyf/V2+vX
+y9XFz9VdZz9CPDU4OkJCSVfjTkdHMam1v+fOa//Ax83IvdnM1WfdT1tCTURMY1ndW8DEysDAxdHNVWNT9zqttb7I40swQD8wMTAyPEI6RtPn7102oq+7z9N3UcnZz8vH2czd51P3T1NFREpRSFt3w8PRv8DCxNXfX29fW93R78rdU0M6Pzg3Mj07RD1VU1dO
+TzKgsrvby+dnxci+u8nNwsVZT+NPQEdER/dO2c++xtO/vsXI21tdSlk6obPOxsxDMDU4MjUvOkM9Odnr711LQ6GuwcfX3Vff08C778/F3VlPXVE/RD5ISD/fXc/G0cDFvsfRb29ZWdvM0V/D1UY/OTs6MjBJTEVB/01OVUM8oa69ydfL582/urfTy77vWT9L
+OkA6Q1VfTtfZvsbbvsfJzeNbV0k7vKm6zr53Mjc0Miw4Mz9JPEDj59X/Vzqfr7rJzNVnz8m7us7Kx8xLQkk6QTk/TEtIW93By9u/xsPMa1dXT2PXy87v1e9KPzc8Lzg+RElNRl1f32tGxp+vyL7RyszDwrG31+u7T006PzM1Pz9vY09MucTX68LE1dFZVV1H
+N6SwtcvR4zI+MTEsOTlGU0xMW8V331dHn7C9wMPT18nLtLnOZ79OXT5FODRCQVVNR07EzMfVy8vH/19MX1fj0cLGzGPVSzg3PC88S0prTllJX+PR7zilrLzKy8f/v7+yu8RvzlE3PDM3N0RP6+td586/yePK3df3U09ZOa6us7vEa0FBNDE1NjFCRVVZ729r
+yddLPp+z18PK48/KyrW7z1n3UTc1STE1SEpfVVVd/77P18bT1etVV+Pd18y/xcxvX1k2NDo/PkxF3V9RT+vf3eNKobfFz7/X07++urrVd0xLLzI2NDVGY/dja+fPzMPL2dXZ51tXZ1WmsrrDwFs+QS4rPjwwSk5vV3d3a2vHX0ejtcTJz8Xrv8O/t+ddTUI6
+LzcyOExAXU5fVf/Hxd/Z2WffXffJztHCv8PKz0s6QS9XPElNU+NKY/93d9X/OJ+zws7BwNG+ybm660dEODgrPTE9TF9rZ8t30crDa8bvZ99vTc+ksbm6xOdCQTI2Mz81ONNIY13/b13/d/+/qrrX0cLDyL/EwrxXVUk4NDI5NDhrP3fr7+vXxdXVyF1b1+fP
+vcLGxbzf11tNNzVJSEVAd0tdQVv/b1tbQ6apv9vOu8G+wr2/y0hHPzAsPTk9PtlfzszZ1cjJ68TKb+vn90Kfqbi8v8s9QjwvLzs5OjjvUdtVVe9n52dOp6i938zGxb2+x853U0RBNC8vQzdISff379HZzcr/wd33/87Lw7y7ysbJz29HPzc1P05EQUlvSVFT
+22vrVUalqLvfvb3Ht7vHxVs+PTsxLjQ3V1V3/8jbydvAyVvE5+P/zkeun667xMNFPj43Lzs2NDHdQWtnX1nv79lvO66tvd/JvM64vMjLTD0+PDcxNTk81T7Z03fT2cbVY8Zvd9vNx723v83JvUtVQDs6O0VLN1FKU0tOX+f/90lEo7S7xru6vru2x8VENT04
+LjI6QE732+u8zs3XxtdLy1/r299Fn6m0wsfKNz44NDo1NDo+RE/P61VZ3d/JRUCisr/Xv7vAvL/dzUk0PD02NDlEX1dbytPf0d/R20z/92vLx8W+ucPM08xPQkI5OjtDPE09S0Nv3+//5+NPPZ+ywL66ubu3w8j3QC41NC84P093Vdd3vd3b49NrSGd3a9FI
+raGttMLGXTw/OTo1MC9GTTtFXV/bVdHO61VEn7C8vrzDvbvf12s8MDg9ODVISWNHz+/bx3ff0V9dWWPjyb3Ht77Fz8hvSj09Pjc5PURBP0xVU8vNb9Vj42elsLy2ury4usvOVzMtMDc7OlPnTFvPztlry0zjSVVrZ+Pn36Klr7++30o+Pjo8Mi4/Pkg+4+tf
+W8Vv0dVMvaavvbm8vr3CXeNVLzM1PTs9Tv9FTs/v3VP//+vfPGdn2b+8u7fExcnTTkU/RDw3O0w/RUlV3ffV687nvUS0p6q5tLm1vcNnWzooLi09O0Nn609byMj3U0LdY1dRW+vZTcykqLa6wllDPTk5OTAvW0FCT/fX2/93v+/JO6ekrr69uLvB60x3Ni4x
+PEVBTGf3Q93vy11JRuPNP0Z328u8vrq8v9XOd0I4Rjo5ODpLTkRbY9fN39vKw+c5oqGru7q3u8rVSUMtKi40PkdP69FEzMrTa0FC7+dZR+/Z40GfrLOzxetMPTs3NjYuOD1GQOv/38/Z68LLykzZn6bAuLi7601JPzctL0BKR0z/d01V3dlfTD7Pb11NY8jF
+ur/BvsFd42M9Rjw3OztBPz9MY2Pdy+fbvN3IVzafp7q4t7tvbz44MiwsPklPT99fd/fN129NPtXZZ1Xfx+fboLW4tedRSj06Njg1LkpEP1lb3d/V78O53dPVSqmisr+4vkNEQDc7MDRKW19ZXVdv31nV/0dvb+NOZ93Vvr29xsHNW1VPQkI2OT06Qk9JQctb
+xdvTx8HNztXT05+mvLPL/0Y3MjE4MEZbb/df92vBX+d3d09I429Z091Co6y1ybtRQD5LPjU1MTlKRk9Ezsznzsm/48zOxMREoZ+uxONPODw5N0BCR1trd01N/9Vdb2fPUV9nX2/Vyb+8w8bO208+Sz5END08Qz9Mb0zrd7/KvMTRyc2+vFHTn6/L1VE4NDYy
+PT1IU1PXW1fXxF1X51NnQGtby99XvKSuvtnEUzpCO0MzOS0+Qkpf28/f17rTt+vbwcDE/0WfreNvOjo1Pj9NXVVJT/dXUevba1XLTuNLVe/Tzci7u77K21tKPDs9Qzk7OUZGT1fZ19nPwcHB0dO9vL/F/6Wrv1dJNzY6QUFVU0pNb+dR293vVV//50hRZ93d
+QqCsscn3XTxBOjtAOTQxP0xVa86/0cTGxr/d0cO+x85nq6y8RD46OkVXXetNS09B71Vf52ffTGvNWVF398fCvbzB6+dMOEU4PTtHPDpFXVFV277IvcTOutnDv73Ex+OvrbtPPzc6PUdMTFVEU0drZ1/r99tBQvfvWVvdQqimrbfv1TxARD07QzY4Mkhd9//I
+vr6+z7rjys/Cys7R36uw0UY/O0NMWXdXS0dIQlVv/13TXVFfU8lZ39O/wbq6xuP/Qz85OThCPT09S11X58rFvMjHx3fByb3GxsS+s8DPQzs8PElNUVdIPllAd+/fV85PSVl3yV/j72ezp62/2103Szo7PD85OD1CXePjv8G40cvHW8rKw8/OxsG80+tCPENE
+79tXTkxFSEVj4+Pra293Wf/E99fNvry+wet3Qz07Nzo8Qj5ET1tK99W9vcPBv9HjusO9w8XCvsTrX0Q7QERPU0pJSEBLRefb99lTW2dnd8/jyc/CtbO3xXdJPEA8Pz9FQj9TTkxjy76+xsy+4/+9zszJ38vB3VdMRD5FW/9jT01bT0tV48rbY+tj3+PnxsvH
+w7y9v9HjST8zNzw+RUZFU2NVR9nGu83JycLj18HFxcPNxr7ra01ESEZXVWtLSU1XSllVzM9TW0/f3+Pby8vJv7vAzN9TQDo8PkJHS0Vrb1NT48C71czGvf/JzcnMzNPDz19KVT1KSmNjW0dFX01ZT1nK3Vv/b+fXxtO/w8K/wsz/TUA4NDg9QUtKWef/WVHT
+uLzNzcK+zsnExMnJ08bnW01NRUZDTVNOS0RPV1VNT8/nTmPr18/Hyby6wLy+yNtLQzg3O0JDR1FO6+/vX86+vtfXzMDM29PLx9XR02tNTktBU1NVVU5ISFFrU0pjzudf9+PVxefDwbrExtHfVT46NzQ8QEVPV2/n/3fnyb/BzePLwMTLycu/zNPTZ09JS0RL
+Y0lVTE5IW2dfVWffUVtv28vG18a/wMrK7+NMQj0+PUdLTV1Z393f3dXRv8vZ487E4+vbycrM42NTSUtHSFfdW1tMU1Nd/1lfZ83dX3fdxM/PzMfIzutZd0xCQD5CSVNfa3fn2evZ4+PE0ff31cPj29nVys/vVV9MT1NLU99va1dfW+/j/19n2W9OZ8zCzdnC
+ycrPb1NbR0RAP0tKT1Nrb+/O787T27//d+fK1dnd1c/HzP9nTltfVV9nz/dZWWNZ7/9vU1vdX1lnztnNd9nZ3WtbT1FLSEVBV1db5+ff287Zz9Xjw/db68fR1ePf09XZa1NTT1tMT1n//11fZ29v52tjT99L/+/K1c7b19Hn6/dXV1FPS01fW2/v9+/T293T
+0f/Kd//V69vj993X09VvW0xbX19rb9n/Y29jd193Z1NO51Xvz+/J1d3n329nW1tdU1FOU1lv79fn49fX1+ff/91rVdPj1ePr69vd32d3TlVnWWNn4/93//93//93V3dbV9XX69HP5+tva2dZXU9jT1ldWWNn2dvj3dPV32/NXf/j/93n593d387jd/dVXf/3
+b+vd9+dr72f3WV9O31dd993n2dH/91/3WVf/U2NTWWtdd+vd1dnn09133fdvY2tn3dXj39/n0+9r/1NZZ19fX+9r72//Z+9dd91bV1vr5+/X0+PrX1v3WWNj/2dn72Nr6+fV0+fX29//a2P/Z//332vX4//Z52djU1t3Y/f/5/fna+//52P/Z2dvd//b39vV
+3e9vX11jZ2NrZ2N3Z1/d59/X7+v/d/9f91tna+fb7/fZ693f7/9bY/ddZ2dr5//jd+f/b/dda1lf/+fr387n929nW2Nja/93a3dna9/r2d/j9+N3a19fZ2d39+v/5+vv3d//a11j92drd/fj9+f/7993Y2/3Z2fn2Xfn0/d392NZb29na1lnX/9369/b5+fn
+Y/9rZ1tvY3fb5+v/3efZ2+PnWW//Y2df9/fn52P342tjX3dXX3fvd+fV9/fvX1tb/3fn9//ja9/n2dvv699va2dbY29db+tvY//rd+Pr93dZY/dvb2ff6+fn9+ffb2dr73dr2+dv59vv63dnX/dr//9rXfdn6+vf2ffn/1tZY19rY1//4/dj9+Nv2e/n73df
+b2drY+fb49135+Prd1/rb2fr43fr1+9va2NVZ2tvd29n73fn69vj7/fvZ1tdXf//Xf/j52v35+Pb//f3Z2ff/29r593j3evv6+trY/dna+P3b//r52f/XVv/b/dva1//6/fj29fvd2dnWWNfb/djb+PfY+v359/n69/3Z3fva2vd19vj6/f/62tbZ2tbd/f/
+9///b2djW2dv9/dv/+/n693Z2ff372ddWWf/72Nr/+9j6+fr729vb2tv//d3b+fX393v9/fva2drb2//b+///+t3b2N3Z/ffb2f//+/v39vb/2d3Y1lfY2/vXf//63f/49/f/+vnb19v/2v/3dnX4+93d+t3W2t3b3d373f/b2d3Z2f//+N3Z/fv7/fn3d93
+d29jWV1n9/9v73fv59/f4+t3929n9+9v93ff5+fva/dr729fZ29vb2//9+f//293///v529j9+P34+ff42tva2NZX2P/b2/v9+/3593n5//jb1tn92fv793d5+dr72//b2dn///3a//rb2v//2d3b//rb2f/4//n393372trY1tdd/d3///v/+/f4/f/Z29n
+Y3f3/3d35+v37////+t3Z2d3b3dnd+Pr//frd2//6/9na+/v/+vn4+9rb29nW11vb2936+t3993n5/f/919bd3drb+ff4+t39/9rd+9na3fv/2f/4+////dr9//r62t39+f/99/n7/9na11fY3dvb2/343fn3+/3d19vW13352935/f373f393f3b3drZ///
+Z/fd5+9372f/5+t3a///63f/6+Nn/3dvY1v/b29rd+v/b9/f6+v/b19VVf/r79/T3+v/d/9nZ29ra2tvb3dvd+fn////d//v3f9v///v9+fn5/fv/2tnZ/93d2/34/933efv93dvZ11Z7/9399/v/29rd293b2NrX2NnZ3fv493v63dr63fr//fr6+/37+dv
+Y/dnX1ln993Z18/V3e/j/29nX19bVVVrZ/fn3e/3d/9ja29jXWdrY293d93OztXb2+Pn4933Z2dva2Nvb29rd+f3a/fv//9jd3dnY+/X62drZ1FTa2dnb1137/937/fn3+fn29POz9PP29nvd01FPzY8ycvfa9/R43ddV2dba19f393d4+vHz7+/wcfb53dZ
+TUlTXV//79/b29PN083XZ1VR/0dHPj4/PTw+Ozs7177H62Pv79vKxsLCw8THyby7uLu8ur7Av83X41FMR0ZBOjs+Q0tHSUtNSkdMVUs2M1m9y19ZVdfZb2tdRj5KTkpPW1tMY19jzMO5vru8vru5s7W2uby8v8XRyM/dZ0s8R0hET01MSD9KUT1LTD46r669
+71FHQ0JCR0c8O0tITEl3b8HBvby0tbu3ur/Ay83bZ1VPQzs2MDs8NTE3Niq4vd9JUUA2UVdCoKe0zsfZ0+ddZ1nvSe9BRXdvxc7AvLi5ure3ub/Da1VVW0g3N1FJ20NOQUz3Pf9KWU1ZT01PRN2ftl/fUUM+Rjw4b0g+REA6TE3Ty729vriyt6+/vMvd60lb
+Wz9DSsfjX1NrTKG8xd/3PzRGSUFPM62p0TzvVztHPE1JT0n/SU1Md9W/ubivsriyuLTA5+M/bzs7PklIQl1n/2tJwUZHXU5fWWdCSfdFL6PIQD83Ly5FNUtNWU9DV11X1bu4r6ytrrWvsrrGW0o4LDI4V0pRWVtHob20x+9RRsjjU2tZR1csn8ZGWz4vT91A
+PlE9Sk9LUdXJuriwrq+1urm61V9BVTdENlM2PDg8U1U+19O9X8vZz1PR4+dP/y6hyU9XSz9FUT1GS0NGPk9Pa+u0u7CqrLO6t8HjSUouOjpIOVtCPbesu+/DWURfwndKZ1tf929HLZ+5Y+db2Tr3Pz/I/0hva0j/zL22u7S6u9Pn30ZVRDY4NTozQjpdVVNr
+18DR08nf7+9Xd+9PRy5dpsZHZ7w3b99JT11jTldPTOOxwLyyu7fT3VE+Pj04OEF3OTmmtLTb0//vzs7vsG9A4+vnWfdOMKC6TkjnPjjMSl1nTkPrW0s+xrbIvsm/wc9TRFc/RTcvRzM8QE9fzc2+vbu4z7+13VPX32dMPTOit11fTMJIa+9KSkE9Q0tBW1HB
+t7u+xt3VXVNnQ1lXQVvNV8fCvsjEybzHyMfTytPIZ+fOW13bunfnSUBjR0bb2UljREhDS1NI0dPP09fvXVs8/0ZVPkFBX0/Z99fXxsHEv8rI273H7+PN51dn27/H00pMY01PS9VFU0VHSllZUUvD3e/jZ1NIQUpVz9Nn/75LznddXdHH1cDKzefjxt/n0+s+
+/8/VUUNEPUl3Z83P4//3UUdn59F32c/V59tnZ0tnX1NOU0/XVdN3z9vRwc/O19nIa+fr49vdWU1LREVIRTxDa01fZ3fv2ePXb9Hb7+PV6+/nz+fXd9H/999r59HrTHdZXf//b+/Hb//d0Wdd3WtER05HV1VNSv9f1+fjzdvV7+tj3dXb1dHP49/322/32+vr
+69Nvzdtra913X//jXWP/5/frXe9dW0tZV0tITkZXWeffd9l339137/f/4+fb09n/63fj/2/j4+/NZ+vLa1NTX0pda93b3/drX9/T1dN3Sj9OT0FKa1Xvd+PM39vKzc/X0dPV09nO/+drWUFGS1dOd3dPS1FMSUdKW93v4+vT29vLz9/r3cvMwMO+zsrNzMLD
+w8PCw8HKztHd12tfS0Q/ODI0MjI4NTU4Ojc8OTc4ODc8OT5JP0JjuLG0uL67vLmvuLe1ubWurqypqaimpKWprLG+zv9ZR0U+OzZXMjMvMSwrKSssKiYjIiEfHy8yLywtLjFBRNG8wL68r7Gvq6eipKajpaapq6iqrLK3ubvJycDL0/f3V04/Qj05MSssKbas
+wVU0KyEoJycpLS8uNS83RW/GwLGzqquxub7Va1lPY9fT59nAtbS6sLi1uruysr/A69td97673Ts7MjMuLCkrLScxLzc8U//n1/dra9lVSefv69fj0WfTvsO+r7OtrK2yuMO5vdf/QUmfoa/XXS8rNi4uMiwmMy4wMllfZ0/Iu9lLVUlAMzAzNDQ/Y0vJwbWu
+qbCssLmxtbO+2d01pafC0b7rOdfPT2dHREzrOkTr38hZ00pJODAuLygmJCYsLTE5QlNrwri/w7/Fur68ubzETqqfn6e10U1N611C3zZNX8HfuraysLCvvrTjTTQ2IiEiICksLSozPj7PzLzC0cHZtb7VX9cqn66wyUhjX8LNycP/58DCuLeyr6+zsrvCZz85
+MCsvLScsNSwxLz00SUxXymdvX3dd601fSSyfn7G2wjBvQmtGXfdVu8a1sqqmqaqoqL+tyGM7KjUoKzsvOTc5R0TTV+fDRVM7Y0c/MiwhoOs2Okw4Z0drd+tj28LLvbqsqKmqqq65229dPS8yOzQ6W1dMTVfDxLXEvsVjW09GPzcwHz+1odvvQyv3KTlCLzpB
+TnfGv6alo6SrqLHDz1c8Ni45M/863W/3xruuq7DIr8RXwudAMiohoNMuLyswNyw7NEgzOjxG/1nVra6xsbzfU0BFOjg+Pv/DvbvIvbi2r6autba0ysXOSDcuJ6VO57LvRTBnMDQ7LTE1QTRVXc61tK69ve//SP85PjwwQNlb0+O+sqispqivr8XHr8VJPUMu
+qPc+OiwvPjA+Oj80OkE6OU/PzrXTyEI6My03Li42PU7LzrvAt7CqpqSqr7O1v8vOTz9CKp9vrLr/VT5IND5NLjk1PztJ48O+rtdXRzk8NjYxLjM2RG9Pb925taqmrKy8ucbDyk5DOiahuF9NSzM92TzvXUhJSe9HS867v8Oz51EvLi4tMCw6MWfZb8vGvrav
+r7Gyx8njb09bP0gtr7BJu6nOS8NVRkXVOlVV70vItri9urVRQj47Oi48PTs561FO68PGuLWpuLXA3UxK9zZNMSus9zg9QC5rxkrj71fTd8BvwL+zwMPOzDw4Li44OTpFY2uvwLi7t7CurrO9a/89QDQuOirbrMMpvqln38JfW0dOPF13Y/fGvr3Du+fZPDs+
+VTXva1lNw87Iw7Wzr7Krvs5vQjs1Kzo1NLzKOzU7QTxbU1v362Pb377JvsK2xsHbV1cvPD03SVFr/7vCrruyvKu0tLXNRzw0NS0sMDLdwXcwLK68wMfL61Fb1/fvz1/jx8LD1+9NRDw9X0PH48F3usi5urixrrOuw+tVOy80LjAw3ddPNzQvPkDXNsXRZ9fK
+18e/x8y/1+dOUzRCNEc8W9fZxrC9rLStrq2wtctvTzYyMjMzLDvA9z0wLLvCtO/Z21VOz13fy29j51VdU1s5O1dTa2/3vcC7wbi6sbivtLi7WVdDNDY8NjI+5+ffQUo+RFddP7vj78TPa8nN0dHTW0xfOz0+W0xGT//j0bXHrbeztbrHz2NKQjg4SDg8Pl3j
+z1tXX9FfuuO1ytPM12djd1lZW1FFVUY+QkZNW2vr11m+77rnzcHKxtXPXU9OTmtRW0nn09Pv5+Nfb8BRt8LDzMbZb9Vv/293QkZFQD5JT08/S1ld29m7z8TG09dnd0ZNPTxOTTtH/9/n23fP18Xbzr6+y9XFztnRy+9rVUxVTVNOd293V3fT12PjzONZW2dV
+Sz9JSEhISV9Va+vv19V3//dr/9nMvL3Ry7vExMrF2dPvd1NXV1dva1dMZ2tTXf/M2ffZ729RTUZKRklLU0xdVf/f3e/b291Xa8vfZ2PM59f/63fn91nf59935+dnd9nP4+/rxd3r92vnb1dbW0tfVU9Xz9vf69/r52tZV2Nfydf399/b1+vvY29jY+/32ef3
+7/9P92fr79Pbd2tbd2//WWv/d1lvY19bWV/r3+/r691359fvXV/vX2tVUVVVa1H/69nRy87X5+fP493j3/9bVU9jW1t3Z1fj99fjzufZ1dn39/9jXevv42fvb+fna11jVVFVWW9d//d37/fv3ffX39XOU/dTd2d3a/dn/+/j59fV48/P1XdvX+dZ92//a2Pv
+/3dVU0xTSFNf72fj7+NbY+fv3e/j3+9fW2Pnb3fZ5+/f2dnf09Hbzsrj9+ddU/93b2djY1H3X1tfX1Vbb29f7+Nja13ja+vb59nZa29na19d/3dj6/fjzNvv3c3N0dnb62Nr92dZXV1rb2dTXU9XT01dX/ff4+djY+tn99/X32//Vef/9//X5+fV3dff5+fd
+1efj69frVWvb629v92dVTV1fV0pOa3drb/d3Y13j///349v/XW9v62dr0Wtr7+vr3+Pf39PV793v73fr//fr/2f/52Nr91tdUVNnZ2tva91fZ2f/7+Pva1tfW/drY3fr59v/79fn3+f309vra/fvVV3rd+t33f9bZ9/ra29n39vn1+Pd/3ff99vb4+djW19j
+/11rb1lfa3fja11jY+/jb+fn/3dra1//Y2//Z1vn7/dn/+/n6+fn2dvr42tv99/f59trb/fvb2tn72/v7+N3593//+Pv72tjWVVd/1tdY1Vd9+NfY2Nv6+ff3c/d69/Z7+/Z/2tnX//v52935+9j92/n73fv7+/3W/f3Z13r4//n//9vWV1v/1lVWfdv9+fb
+5+fj29//3+/j//f/7+v39/9j9+v/3+v/72/vb+Pj2f9rXVVrY1Vn71dXd+//W2Nn7/ff29/v793Z39vb6+/3V2//d11r6+fr79nf5+drW3dnY93vX19da2Nnb3dnX/ffb2tdXedv6+Pb193f52v3///nd2Pv6+/vZ/9j7+t3393j62/vZ+tv6193b29r/2dd
+a1Vb/+//a+//a+/b2ePn/+ff793f72drZ2dnW11nb3djZ+fv6/dr793v9+/3Z1trb3f/d3dna2/j919fa+9v3dnn3e/j4//f39Vnb2f/d2tvd2tnX+/r/2vvZ2//X/fj92vrd//3a11ra1tv5+//Xet3Y93d3+dv6/f/7+vf6+/3/+vnd3dn5/9vd3fn7+Nj
+a2/3b+v/719372tnY2NXZ+Pn/+f3Z29j//f3Z3fr6//v93dna2//5+/v7+fv9+vb3evvd+/v9/fv63dr5+P392djW1lfd+frd+dbW2/33/dn73dj/3frXWddX11n73fb3dnn63f33eP39+/b3+PV7///319ja19jZ+vX93djXWP37//vY2///2NjY1tnY2N3
+d2f//+///+Pb4+frZ2//7/fn///r2dvd3ffrZ2/r//dvd+tbXWtv33f/7//3629rW11nWWtda11r39fn6+Pj4+P/Z2/33+Prd+vn7+/3/29jb+fn69/n72/vd+t3b+P3/29fb19jX2NZX2dnb///d+vv4+v3b2/34+/j93d37/f/Z2vn9/fn5+f/59//X2d3
+72/v6+fr5+d3b2NvY19dX1tdd3fn4//n5/f3a+/n2+//5/93/19na2dn9+N37+Prb/f39+fr92/f4+f/b2Nja2dva+f/3+f3a2/v4///d//r99/jY1336+N3b2drXf/3/193b3fnX2f3b2t3/+//7+Prd+/363fn9/93/+/v///va//n52v/23dj7+/vd2vv
+9/dj6/93a+frb2d39/9vZ+/j3ed3Z2drZ2t3d+936/939+Pj2+PvZ19fY/drZ3fr49/va2tv7+fda2/3b+/ra2drY2d359n35+vva3dd99/n4+Pn///rd2/3///n42tf/1Vjd///a19jZ2N36+/3/+djY1t3/2NjX3fn5+/n7/f33+/v293b3+P/3+vj2ePf
+3ev3d/9XT1tVb29fd2dbY/9rd29nb29na3dvXWv39+9d7+v//+vr49nX2ePr9+/n6+vr4+/v5+/r92t319vrd2dvXVlrb/9n//fra2NrY19fY3dvY19rW11vZ+/v69/d3+fn6/fr3+PZ39/f5/dna+f/6+9n529ra+fvd2t3/29va29nXWNn929fb2trZ2tn
+Z/fv5+vn5+vv3d1vb3fv//f/b2/v393f4+Prb2/va/93b3d3b3fvd3f3b293a2tjW1lnZ3d3/+937+9v7+fr7+fn529vd2tn9+936+//9/9r/+vr593d6/9373djd/fv73drd2drb2djZ2vr92Nva3fv6+fr6/f/7/dvZ2tr9//r9+Prd+/n72f/5+P/7+fj
+7+/n6+vr/29jZ2tfa3dvZ/f/b/93//9n/+/nb29jb3f/9/fv9//j729j92//d+fb3+vj3+v3d/fr33dv////b//vb2v39/dvX2fvZ2trb29v//93a3f/6+Pvb/9va/93a/9379/36+ff6/fd5+/39/9vb2tdY2//d293/2dr7/fv5+t3b2tfd2//7+/r93fr
+93d39293b//v//fr5/f3593f7+v/Z2drZ2tv//9v//9vZ2v/b+/v//dvb///d+///+/v//9nb/f3d2/v7+9r9+/v5+ff3+/v6/dna29v/+vv/3drX1lja19n5+Pn4/drZ2dv7/f/b/9vZ2t3d2Nvd//r93dv7+939+/v9+v/d3f//+vf4+fn3ef36+Pr9+fr
+/29fX3drb+/r7/93/2Nna3d3Y19ja2dfd/f//+/3629vb2tra/f/79/f6/fv92dv4/fn4+/nb29rY93b2+//Z2NfX2Nnd+9va/93a2fv69/3/3drY29va2drb3fr4+/v4+/v4+vr49nf4+tjZ29rb3f//+vr6+/r5/f3d2NrZ1tZWV1jY3dd/2tZb2/b5+PT
+2dfVb9v/b2dnW1dZWV9ZVWdrX+ff09vR2c7T2dfOzs/Z23djWdn/Y11NVVVNR0ZFQEBF3b7G2fdrV1dZa3fX19PMzdvN0dnTzcrNzNPb52t3a//n//9vWVVXV01IRklLRT1DXf9JR05VZ2///93TztXFxMjDxcHHycrX3efj9+/3///j69lj2c3n2XdnZ09T
+UXdHZ05CUUxLTkdITVNTzbi/yf/n91tPW1ldS0I4Qrvr12tXSVdJTFXv92P/083GvsfBvrqysrW2vcHIvl3dZ1VMS0M/STw7PEFIRkRZwNtASVU/Q0FNSk9FT0JDSUpIZ1Nb32fdb2Nna1/HxsG9sbSyramrrqappKuqsbu2tsu8uG9POy4tJCcpJW8tKiEj
+JyAoKy81NzZEr7/H29lfPkxLPUJBNzdCQ0pnwLu9r6mrqqempaipoqSoqKuprKirqrS4w+dXPjUlTLlGKCgpISEnKygnJyUtLSctQT4zTuNNV07/2W9T38rKsrSsra2ssKytq6q2scjPyuPvxb/Kw8XZwdHNXzGsujxVOTkqLyssMSkkxd88Kzs9JS8+Q0lP
+TT9PX0Lrx7+7rayrp6yvrKKqrrevsMpf311d329Ob+9rTT0xK6fbPUJjQywzMDg9TlFAS3dN9/9COUVLTTo3OTw2MjU6Rf9vvaits6uqqKmxrq6528f/xudrb2vG729NPD0sJ6jLNUk8Rj1MRFFEOKO1yUlZP0JVPllvRTJMQzgvOkJGS9vIvbyzrKaqr6+y
+wr5PyFtZS0ZOSuNNS009bzkrn7JbytdNTdU/Sc5HTk5TSWNZPD9APz9LNDc5Ni4yMTxDW8y+wL6srKqlq7K0sve2vetXY1XD087Z2UZrXyyiv79NQfc4SWdNL6TB/2NTPTI2NjZHPjk7Nj45PDQ5RUVfzcXHvruvo6+ssbuw78/JZ2PIyMDG2dtdTFlfXTKf
+rc277+M8R0z/PjkxNT0vLjIyMi46NjMxODU8Nj5DVffLu7a3rqmmo6qssrm3u8bJ71m8zsHZ70s/V1NrKq6uz99vWS9MOSaqSDwvZywoMzsxMzc3PS84REVAQ//Jx765ubGmqaqop7K7uKvbwb7f78PJz8pVW1tLRkMrb63XPkk0NjdFNjY+LTM+LzY6PzY+
+QU4/PkVNVVvv18/Lt8C9saurrKiqr8Svr8LPy+v/x93V605PQ0dHTz0vq9s3P1MvPT0kql9ZM004LD86REdDRl1bTWv/01+9wcC7v7qzr7Cvr6e2t769t9/FxtPLyd9RTEE+Pzs5NDCsYzRdNygvOTc2NzE3SP9CU0tP91vr1edP2dHPY7/EwbzKubu0sLCu
+r66/vcDOS11R99tv/1dHQD9GOD4zJabOTEI7My86Kay2Tj87QkNVW1NXXVfr0WPTy8jAube7uK61uri0t7m5vd9nv05JSVFETktTQT9BNjg2OTwkp77nQzo9N1NPT0Q+PldZymfZ1cbT48bE5+vOyMa/yMq4yb/Jx77K0bK6yV3/V0dETU5XTEk8Pz04Qjk4
+NSOnsv87Sz8+QjkwpsHZzMTGd8DPy8Pf27zP99vD193Ay7m8ycvIwMLVyNvjRVc7SzpPPDw+Pjo8Njo2MDo7J7euyU/n70/ny87Ld2vG27zJxsLF2+PfzVlNU/drZ2/T389ra9fdWb29xttnd1U4vlP3PkZVREhNRTw+PEEta6y7S05nV29HTqm/ytHNW+/D
+68zN/13Lym9rzstVzMxr529Ma2f3U01ZZ0NES0BI2U1ZUVtrTVVNUU33/09XrLDL0/fv/2Nd6+93W9tMUcTP293ra1HfT1tX611nvmfZ929r3+PGzcnVb2NCPz/TSVFHTm9V/0lIWV1nT9W2tbvH08/ZX0qqtLvT39c/vT2/SGdRVV9PTFVRTkvOT1VXTkdT
+W19KZ2NdUUrnVUxvxU1nd+vvb2/v/+tPyLquuLzN39vO2+f/11tT509HSlfbTEhPSEZATUJXTc5nUU9RU113b+Pjzd93b+NjVcl3/9XZ/+PrV99b/89TZ8W2u8PXz1XfsbW7ztlOQkTGSltHPkVJUT9LRUb/SFlZUVVvUWNMX1XK0ePj09fvusHEzsrf19fX
+2dnZxdXby8/I411jT0xTUV1XZ05NRudJW01fW1NdT1FPZ+v3b9/rVWv/b3fb99HXZ+vjb3fE48ndztPR0+Pv683f1+vMzMbM2fdZU+tZWVtfTktHa0pRPkNHV1lLRUtnUWdnd2//3e/Vd/fVzcPGxcXD0by8yc/d1dXf619r1+dva9nXa2djWVFRW05HT01I
+RT1VZ01OU05PTU1TW2Nd43fv4+PZ7//R19fRzMjByMrXw7zJyM7P2dHVztHV39nb39/3X05CRkBKUUdLS0REQUxvSUNGT1dXZ1VVX19vb/fX1dvT49fTz8nFxcnCx8vDusXM38/d3dHX72fra3dd619XSEpHS0RJRkhMT1FTT1VVRFVNVUtvW1Vn6+ff987f
+1dfXysjExMTHy8XFzs7O0cjV7/d3919ZX1VPXV1bX11LUURNSVNRR0xKWWNXa2drXV9ra9F379nV19fN3dfZ09n3z9nRxsbK0dXR1dPV23djY11jY1tRWVdPTk9ZVUhOSV9LTllba/9312/v4//r/2vv79/369/j493d2+fj4+ffz9vb1c/R2dnd1+Pd5/dX
+Y1dbX01OW1ddUV1ZTFVOY19b/1tjW2f/7//363dr7+Pd49/f2dnT093V0d/b6+/f6+PZ3+fn6+vn93fvXVVZY2NdW1VfWW9bZ2dRTk5jW0trb3dv793nW2/f3+93693f39fN09HZ1dXX3dfr593d99vdb+P/d/9vb+ddVU9ZTkxLSFlRT1lbd19nZ2tjd+Pr
+/93d49/r19Hb2//v39vv/+/n2efX4933Z///687n0+trb/dfa19jY1lRV1VnWV9XX2Nna2tj/1VdXev39+/j29fP2ePn39vd6/9v3//f6+/f49/n2fdrb2Nf72trd2dfZ1tnZ11nb2dZV19XZ13362tj72vr///v63fd39fX39vd1dXd19fd693Zb2P/9//3
+b13/XU1TVVNZY2t3b2N3d3drb/fvb2Nn//f3Z2/36+/j9+v3X+/Za//vz9/b49/v9+vd3+tja//jb/fv5+tv9+Nva2djXWNfY2NnVVtdV1VbWV9fX2Nj7+Pv993f39XX09v/d9/d3c7R4+vZ5+vj5+vr//dr93d3/2f//+tj/1tTT09ZV11bW1lXV2NnW29n
+63fr59/Z69vj5+fb3evd3+/n2dv349fj39/d5+/f5+vfd1t3/2dnY2drZ2NVXVlPVVVXV19nd2Nda11bd2N3b+9v6+fb29vf39fd0c7O0d/Ly9nPztXr3/dvd3djX19ZT1NbWVdVWWdfW1FbT1NPU11rd+/v6//r3+9359/n99vj0dnZ09vd2dfb3efj7+Pr
+Z2ff62tv/2tnZ3drZ11VV29jY2ddY19fb/9nX19bWVlra3f/7//v5+/b3dnT1c7Tz8/P09vr59/n9/dj9+9j929vY2Nna11ZVVVfX1Vba2NbZ2f3b29jWWdfd3f/6+vr49/d99vb59ff293f3d/n49PR5/fr7+vr9///d11ZV2ddd+93W1tv/1trXVdj/3f3
+d/d37/9352Njb19rX//r4+tv7/d35+fj3ePb2dXb09fvY//v59/v//93b19rZ11dZ3djZ2NfZ/93V2Nrb2v/63f/d2fv/3fr9+Pf6//j39/n3efr5+9372/369/d5/9fb29jb3djd293a3fr9//n7+9r93dnb2NRW19rW2//d/fr/+tva+P/9//r5+vn3dvj
+4+Pn5+vn6+vr9/fv/2Nbb193b2djb293b2dvZ3d363df72//42tv/9//39/3//9vd/f3/+/35+fv5//r5+vn5/93a29vZ/fv7+9rZ11ra2drb2Nna2djb+/r9+vf9+fn9///Z1t36+/r6+Pn7/9va2tvd/dvd+vr7+/n7+Pj/+vr9+/////v92djXV1na3f/
+Y2vn7+9nd+939/f/a+v3d+9vX2vv5+/f6+Pv63f3b29vb/d3///373dva/9na29r93f36+Pv79/3b/939/f/9933d3fn629v6+93d/dra19bWWf//+tv7/fvd/d3////d+//9+vj9//3/3fv7/93/2/v3/9vd2dfY29vd2dv6+Pn7//j9//n5+vn6/fj33dn
+b2/r73d3Z2ddZ2NdY2//5+v///f//293b293b3fv/+/r9/fj/29392/v/2//7/f/9+/v9+/d3efv9//3/2drb/fn9+/f63dv929ra2//b29vb2tjX2tdW2dvZ293d+frZ3drY/fn7/f37+fb4+v35+vj7+Pj5+vn39/vd3fv/+/3/2trX2NjWVljZ3dva2tv
+/29r9/dna3drb3f34+vr5/9nd/93//f//+/v59/n4+vv39vj5+fj9/9va3d3//9vd2tfb3dra/93d3dvd///Z13//2tnb2tja2//72dnb2/v9+/39+/n39/n4+fn5/f35+//7+vj6+/v6+/v73d3b2Nvb2dZW1tdXV1v79/b393j6//v/2tnd//vd11dX1td
+Z3fn6+ff3dnb2ePn/2/3d19dX2dnX2935+Pf2dfZ4+vf4/9jX1tXU1dfa2trd/f3b+fd39XX2dHb7/dnX05XV1dfZ/f3///j39/r3ePr911ZT05OUVdn693Tzs/RzcLFzM/T2XdbSktIQ0dIS1Vf/+Pf59/Vy9Pf3XdfUU9VUU9Vd+fr1czMycjLycHEzd3/
+Z01DQ0NFR0tOT11r69/f4+Pd29v35+v/XV1r93fv3dnRzdPTz83V2+P/Y1lOTEpLTk5RV1tbb3fn29HRzc/b1dn3Z19OU1lb7+/3493Z09HRycvP3/d3W0VAQUJHT09fY2tr493ZzsfJz9ff7+9XTk1VX1/r7+ff3czPyMfHys/rX0xFRD4+QE5bZ2dv49PL
+x8S/wsjP71dKQElFQ0tba+vb0c/JxcrMxs3dd19MPzw7RUlOV2vf49vMw7+/vb/GzN1rT0E+QU5JTFFbb+//39HPz9fX2+9fb11MTlNZ39/b2dnM08nDx8zZ52tdSUU/Oz5BRGddXf/v29fRzsXO29Xd519bW1VXW2vdys3bzsjCytXX71dbWUlNRD9JRkhK
+W+P3b93Nx8zNzs/f2+/r/19LT09rb+vFz8rGy9vN7/9ZU0hPS09OQT9BSUxX28zNyc7Vz93MysTHzu9dTENGTEtNZ+fr17i91dff51vvZ09RSUI9PENXb93PwsLIy9XVz87j4+9rW09dT0dJa1VO/7vR3d3vb1VRUWtvT01IRD9BWe/Rz8W9vcS+wr+/2dXO
+1+v/419FR1F3TevFw7tPY1lXTkhTT0tIPzw+PkNT79vnysDRa1NVa1VO3fd3991jWWdd1+fvvqywu83F087Rzc7j4/9TS0ZIT/dr3WNnv6+6yOfvTFdvY3fdRTs9Pm9DQ0PGqrJXO0M9Ozo8PUA4NjM1NztPd2/fz+N3TUZLV9/ZyMLCuMDR48W/v8q0qKOs
+vtvjv8q/tbm+xsz3TUlfd3dCMkapsMxOPjk8QUZOWfdXNi8vPEU2Ok63uFk5MUg5NUtOSUg/PTk5Pmvb62fj2ed3T1Vnzcy7t7OusL/VY73F5+esoafTz1nP3czAub3J1dtLP0NLd1c4O6axwF9CNDZEQk1r0e89NjE7QTUzOK21xWc2PD8+R+fj91FPPDw8
+StXM2dnZ3+9j/1dZz9fVv7u2zVlK39NnQ8ChrbhVb0zj09vGu7jN0WM/PkBVy9HrP8antddTSTo7VVXfy1sxOTA9ODcvq8m+sUo/PlVFTdlvW1dRQD5IQvfI1//Mzutv42NnyMXNx7G0xd1vStFnO6Otvr5IUVXHXW/Cutvbb084Oj1G529FMqOtx0I+QzJd
+Z0zMvzlBOTk4RjHGrcS9ukc7QFVCb2tTSmtLOj5APs/V2c3G391n4+POvLq8rKu0vM/jW8BV75+vvstjTENjSevF2UpKQz8xMzU8Sz88Pi6jvkY6RUw9PVe8zko5QT9NVS+hrr/HrVs7Z0pr0/dNV0pZSExKSMHG08y9z9NvXczj18Tnr63ZzufjRGsyzKrG
+38tdOkVjRlvB41FRPTo9NTg/Yz5OWT66rXdP62syOte70WM+Rkg+d02ir+evtclP90tF2eNn31VTWd1BU8zIXb/Z0dVvRtvX61dduLPR40fZOz84zKx363dIOTxCOktH4/dnPzg8PDo9/0lj1Ui+pbxfzs1jO8Wuts1KTsxR3Uihrr6rtcvOa0RC2U1H22NM
+TVFNW9fP78tv2VlRPkxdV0JTxLrNdz9TQzwsvq3n21NKPT4+OWc+OV3EXT5KPUpR21vvyGs5t6i/y87FOsSyxb5fXULHXTGisrmpzd3I31tC1UxCVWc6SUQ7X+Nfb8nN//9XPkpVSUpLvtfI1UtGPUEsvqzO0VdLNkNIPk5OOUlP3dtZRUFba1n3yff/RZ+6
+wnfZTFO6xb/H6z+5VTWlrqmu08fJvU5N59tI/19bT0VITWdVa9FrW19NPkE+RT1XyWPrX0w+PUIotqzD50ZZOUI3O11vO0r/705TY9XRze/Lyc/dOJ+uwFX/zc/DycXZ513KYzKjo6zfwt9VwEZOUW8+U13vOE9LSu//Y8vX21NRSj5BQEXNxW9310dERUUl
+QKy9U0d3NkA4M0FORz5Z91U5QO/Tys7Tws3ZOJ+oumPbv7/Nv8HIyVHVRT2fpLjruudXwVdGS1NDQWNrNTk6XU1r/8vR3VVrVzg9PEfO01t33f9ERUYoya69a0ZjP1U6N0lPRD7Vz2dPREnLzr3Hus33N6apulvvu87/193F11VLPMmfsr13y19r42tLbz47
+QO8/Qz09R1NZV3fX40H/TkE+Ql3R213j3cxdZz0rq7G702P/QeM/SFVdR013zV/dSlPnY9HIvsn3QFOjv81Tx9NHU1nA0V83NVufwL7Pyf9d6+d3TzpEWU4/WT1R/1tV383j42PJ/0hFT1vGz/fv38/d/zQprqnAa1f/U28+S1FNR1tRa033S1ffU0rFusnZ
+SUyft8Jnwb5ISL/bd/8vUT2osr1f103OXd/bRjlPV0hRRTpLX1tPU7zZ0+PM3VlFUc2933fny8vdWTUrqre1W1VbR2NAPEtIS1FMTVFRRUxdUUnjur1v7zpfo7vZxr9AW+9NR1vPL2OqucVvy0/Rd9HX6z9nWVVRTkFC71NTXcnGytXD0/dT27nL2eNn2Xdr
+Ri86q8LDd29HTmtESU1BP0c9PUY+SFVrTktnzb7v3ec4obe+12PTRVNFT7BCJrWru8/Fz1W/ztXB70FR3U1Ob1FE41NVX9/KwsLB09l3zsHbd2/V90jvQi0sqc3b5+dAWd9XXUFPPk8/Pz89R0NdT1lZWce/2f8toa+4Rc5ZN0hNuucwJ6mqwcPrw/+9ze+8
+7z9J11dPY09B111r79nEwLfEys/MvcfvZ+vHQEzbPC42p8vnzt87RXdOWUM/O1E/PDg7OkJNSlFZSMC7x0wroqK7d2PrN1m7RU41LLSsvM3bym/H09vD2WdG2VVLV1FP11/Vzc7PvbbB2869vN/n291jQlnbTixXqblb2V9LS2tdW0I/PlM9ODg5O09OSlFX
+RL68zj4quKG1RkzrOL87RV8vJqqtxcnP2ffP292+yFdf21VJQ2NVZ1vXx8jBuLi8ysm0um/B2etfTktjSinKpbtv4+tBX+dTT047Pko7MzU2N1s/Qk1nW9e450kzd5/F50b3x10xR0o4Jaitv8PP23e/29W8v2tj/1c/SE1fa+fZy8q+t7e/w72xvdvN291T
+S0rrRCixp7/b59lHV+dEa0k2PUs6Li4zOkk8RU1ZVeu920M51Z+33UTFsS48XUM1Kqevv7nP2ePNzsjJvt9ZX01FPD5jVU3TysnDvLfEzbmyvU93yv9KSk5XPif3qr3X79trU093z04+QU48NDA1PD49QWfN2f/PvUw8z5+400O3xi49O0QsL62zvr3XW8/v
+y8K/vtfnV0NJOjxfW0LZxLu5v7W8yriwwV/ny1dASUNLNSq8sMV3zedO30zvzuM/QkVJMTM5PkQ/S//Ly87Zz9Hf95+vyVe/Uy86OzwoS6230b69TWvjXc7Bze9nPkM9M0VZU0jVyL+7ube8trSzwOvN119PSz5FNyqrs73nxN1Pa+/XztFAQENAMjQ6P0Y+
+S9vLytHX5923PrCpxNnHRy05OS8nL6zG18W7T+dn0cbHx+P3O0A0O0BrUVPjxsa7vbe7uK/Mw9fVb09TR0A/Mi6ossHvu81Ha+PK2dlPP0E7Ojg5Q0lDTM/EzsvVX8euO66qwbY7QjE9Ni0p3bTf58K6SGdVxsRjyMjnNjM6PUBvRFfbxr/Bt7fCr7PCy7/P
+WUlCQFE3MTqqtd/IudNT/+vD11VvSz42OzQ8Rk9AW9fHxcvHV8u5TLahs688MDU0NC0p37RdTse1SlFZyb/vXbpvPS89PD1TRUPnwb7AvLu8rbe+ysrHWUI/QUs5Mmuls1+9vM/r482/xVdB3UMyMTk7Qk1GTtvOxsvJd7zNRz2fuatFLzMxMisrsblPTL66
+71trx8vOWcPIRiw7OkQ+TUJvu8C+wLm3rrnKusrvb047PUQ9LTqjtmfZtd/v/8u/2c9RX0Y2Lz86RkZJTWvRzsvJU7HfYzWlr6tIMjkuLywrrrNdRM6350xn69lr79F3RSstPUBDTkJXu7vGurqyqre8v7XR/0pHP0lFMz6jtM7Rtc7Z68LDy1vbT0kwLzo6
+P0dCRHfvX9PTZ7fbTzWrpaZDMDYxLSsqvrFrR/+0zFlX1cpjTb7nUzAuPkJLSUh3vLrFubq1rrW8w7/GT01IOz8/LeensN3dvMvR99m+yU1r3UEzMjs9QUtESs3Zb9/F0bfR/1UxoZ9bNzU4KioqrrZdQFHCw+tM0cjfSdPHQTYuPT1HREvfxr7BvryvtLG5
+wMnV3UhEOzo7L6qrutfvxP/LY2vAzFNn90UwNjk7P0lCWc/rZ9/Rv7PJz28vtJ/MRzc5KSc2rb53S1fOvNFXz8nRXdfO/zszPEhGQk1v08TCwL6vt7m1wsrZWUg/OzM0LrixvuP/zt/ZY//Cyktdb0IyNz09SEpGb9Hj/8zXyrHFxMk/Tp+u2zw5LyI+rd1f
+UVN3xs1Vzc3RY/dVXzw6NzpGO0d348bJv7+ttLa3wc3vW0s+PzQtNq2xznfR98nn/8/Bymv/Wz02Nz49OktL/9/v3cbVybPCxc9IPp+uxj84MCDCt99bTV1Iw8z30c7n9+tKWUdCOjdLPkXv38/Bu7qstrK1v8n/Vz8+PDAlPqvA993ZUb//X8/Iz03vTzs5
+Oz4/OUhKd9ff0cPNuri7vstZa+Oou1U3NCOyt2tbT/9HyNPT29ddd29LSExIOjo/PUbd9+u/vryturK1v83/V0I6Oi8mvq/nd9vdUch3Z8vK3U7dVz08Nz9FPj3n2d/b08nbs7y6vdHXvz2nvk89Lz28w0lRWVlP69fJ69NZa1dEPl09PEE/OGvb2+e9v7+r
+tbO1vsx3XVc8OjMqrrjRV9dbTsvr79vZ60pbRjs5MzlAQDpFz2/v79Hfs7a9v9nbuy+ksWs1L0q51V9N91n3682+2etb/1dHP2NIPjw9N07r12PEx7+usri3vclvX1E5Ni0rrbXn59fbUcnZ3c/X52f3RkM7OzY8OztCze/f1+vVuLa7x93Fvyuyrd0uK8G+
+VUROZ1NRX9G+31db905BPUxbRTs6REl3zf/Oz7qvrrS0ub3vV2NDOyw9r7j//9HZXd3Vzs3b5/f3RERDODw4OjdN/2vv3V9jubW3vOPIvC7Lqdk/Ka7MT0RIa1lTTcu9xmNv71NDPkNVVzc3O1dZ1d/V2bi0rrK3t73XV2NINi13uMnvV+/v4+PVw8vXb+Nr
+R0lIPkQ+OjhK/+fZ391vuba0uXfEuD0rpMc+K63OPEpBXUt3PtHEyFNTY0lEPT5ZXTcyOEzj48nfzbu1rq+4trrKW29TNC7ZvWNb9+933d/nvc/Z7+P/RUxETEQ9PDU7Z9HjyuPfvLq0umPLrkMnq6s6OK6+MEdTQFtfQVfFxVlNd0pGRDxnU1E1Okf319XT
+07i8rq63ubfEVWNLMCmywUFE19tTxl/XvMd3d+PXS0pJVUk6PTs+TdXLzczrtrywuf9rrkouKadFzbfONDVKPkpPPVvX2d9OVz9DPj9Mb0Y/OkhP0cnI1bm4r6qxuLK6z2tXOje2wj9FY29V2VnbxMROW2NnRUBDTkc4Nz09RVPXy8/nubyuvcvRr0tFLsy7
+rrrfSDg+P1lTRM5f3+93W0VFRURJW0M9OUZRW87I08C+uK21vbe+z29fNPfBzTtCW09n09/MxMtjWff3TkFHX0Y2MjtESkhn09HZwcCzts7Is3dHOC+qpsZXV0g2Pl9Ta8xRXc3Pa1FVSFdTTUw9P05dV2PDwcLGt6+0vL3Lz2s/M7ndRDtBSUJHX9Xjz///
+XWvvV0tJ/0s6NDlRWU7v38u/vb+2ssXGre9VPymmp85IXVMwO0lNztNfTtPZb01LS1NTQFlEPU9jXVnjvbrGtrGutsLHyuM0y8lZOjlAR05KQde/21vfV19rV05VZ103NjhFXVv3zv+/t8G5tb7Lr/dnRCmuo8hPTmc1OEg8yr5rPtXO1UpMRFVMPD5FPUBO
+T0/b29O9ubmzssfRxdc1sbtGOzs/R2dVT2O9zXfXZ+PTXVtVZ1c3NTlEU1fdyNHrsrK+urzXs9tPPj+/pcJfSVVCNz1MTOvdP1/vzE9DQltLRjpMRT5NWf/fx9fBtby0sc3Xx2NKr8U8OkU+R2NnXdnT48HnUWfXX1lRa0s9NDhDVWfrw8nbv766uLr3tc1K
+ObtCpMDrSElbN0VZV1df20Jf0fc/QklJPTlPRj9DXefjzcHfxbC2tsbXyEdMrN07NEk/RVtd99vM52/EV+vfXW9rVV1CPT5Ka1fLv8TZuMnEr713ssE+P7REu7XnV0RNMkJVSlFLPVFn3V0/R0lEQDr/S09A39PRzrnMyrmuucbZ00e1tlc8OUBEQkxX1d/L
+2WPPy+9bVXdfSkpGQT9D61HGyL/dtM3XvrNXwsc4yaw9b67DVVNHNkZVSF33PD3P210/TElKQD5ZY1NGb9HTy77Dy768r8z360+oxUE+O0JISz7j1f/rzV1Pd8dPUWNvTEs/RT1DVWvXx87RvMHZyb7Gy80zra89K6i2409BPUxOTkfXTDdGwONDRk5XRT9V
+63dT3d/L073Eyr6/t7v/TMWqz0c+QDxHSTxdzF1j091NWdXfV0tVQUg+RD1HV3fZ48fGy77LzM7R57k3tLZAJ++syu9JQD9fU0bbZ0I/3czVP05LW0dL49dv59Hvx7vCzL+/vr3ETb2rvkZBPTxATDhFb99V/9dLY2NjWWtdQ0lNSURFV+vd3dG81bvE08zI
+Wbv3sLtEOiuovr9GR0w/U01ja1NBV+/JRElFTEpEa93n2dvn777C08bBwLrTSrKowkdGQTo+SjtHd1dr5+dOW99jU1tOU0dNTUJLVXfV38rFw8S+09HA7//MsLz3Pyqssb5NR1lAPmdPX1lFTl3RTT5PRE5IS87f29vf58u+zNPAwb3JR7euv1NISzZBSD5L
+XUlO22dRY9lrXU9KRlNJR0hKU1vn283Aw8W779HDyUCrssnbSS09q7pbSWNCPuNdTGdOR13VVzpLSUxCT9Pn3evZ6+PFvXfXwMPRT6q75/dJQTg8P0JRXUVf1W93W9fn609KSUxMT0dXTVl3683Ixs7A2dfIzUGuodNTZzMuqrfPZ2dIP11vT99vS/fP50NK
+QUpMSe/N32vn22/Xv9FvyMrXWam7W05bQzY5OkJJSkBrd2df7+fX11U/V09IZ19nTufZ0c2+vsPFxMrLz0GqoMdHT0syqrvFWVU4Nk9XSFdvPnfj40BZTD1TVVnTznf/3ePNy8tnxsb/b6e90VdKUTo7NUZIRjlZ41Fb91vj3VNERm9FP+vvU03X18jBvLvT
+u8HFz0PDorVTV0guqa2+119MM1lrR013PkNrY0Y/Wz48T1//5+9f3V/d0c5rzchP96ax59tTQ0JAOztVTEBJ0+9M3/ff2+tKTlP/SkzJW1Nf187Hx77V477Pbz/AqLFOY0ko2am9d+9GOEhnTUjvV0Fnb2NKSlNFSPfXyd//29//zsdd3cdVW6etz+/bSDpG
+TjlETT1K4/9jWd3d32NHVUxVU09XX2tZ/9vLxcDG68LM7z69rqvHRU8yQaq3yVk8OjtVS1lIX0pXX2trSE1RU03du9dv98vf1cvH38NfR6atwtN3V0M+PE5FRTpK72djZ1vT03dLXW9OSWNnUUfdWf/nxcbK98LDbzy3rr6xSzs5Na27tVM6Nz5MSllPTFNN
+X13dUU1N12dV1bjnVd3F187FxdHPS6arssPbT1lMPTzvQjs92/93UVlr42dLSFtLSE/jV0ZF519nd8fR2e/CXznnrb7Dvz0yNqy3yds5MjlRTl9RT01LWVnbY1lf59VZzs/H32/LzdfCxdHj96ats7/LRUlrQj9BSEE6Vef3Y1dv9+NjVV1KU1NdX01LQ3fV
+V9vX39f/YzO9r8/buEItNq61x0Y9OjlMVW9nVVlXZ9nTb1/R593rwsXV783Vz92/vs9TvqiuuL7dT0hKR0dANzxHRmtnb1tfd/9Pa1NRU3dXUWtfSUpfzWPX3cfrZzqysc/rvVkvLa6yxE02NEFETl1rV0pNSmPPWWPn3WdbvcHbX9/L3+fBxMZNraiztrzK
+XVFGQVdBNDY9VVNTU1tVZ1VHX1VKTfddTFvjTUtr39vTz8rrW+ett8fRt3c1NquxtU44MzhLVVt3VUdMR2PZXVt361VP18T/V1Nj9+vVz91Xr6y9u7bMXUpGR09JOTk8S2fdV+9r72dVV2djb+f/W2/341Xnb13jxc/jSL+vudvjuMU0NauzwFM4NzQ+TG9v
+UUNKR13v62fj91NjzdHR51tX79vK1W9dq63CyrjCb19EU04/OzlBQ1Xv9/93b1tdWVtX1d93a9nfXWPr/1X/1c7vRLKwu+djy783PquyxkQ6ODU5RUx3TD5AR1n/d1nV/1tnzNv/92NZW/fO1/fjqa+5ybe+0+tHX2tGPD5JRVfv61fNX2Nfd/9R69Fr7+vZ
+WU9n/05d78xJS7i2x+9Z9749Q6+zyj84Nzc1PUPnQkY6TVd37//T2W/nysn3Z2NrXePVy0OurLS/wLfBzeNG//9COz1ESkzvZ+dn51f3Z+tZ3dPr4+PT/2dTY1lV6/9Vd6q+1edrU75FN6+03Uc7NTQ3O0ZOTjg9RlFf/+vb4+Pf1cb/Z1FR7+/XXU6srLfD
+wre6xN9Vd29DP0Y/Q1n/413ZZ1nv9+9f29/r0dXT51tdVVdZ42c7/63F3+trS8TZW6633UM7NTE0O05PQzg5Pedf//fN59XVzsjvX1lMY9XHVU2pq8S8xLq5vl9V71tAQkJAPUT/0XdjTV9b02vv29f3ys/M1WddVVdO22c9vrHBWdX3R13K0a2150JCPTI2
+O0hjRTU1RFHbY+PO2d/Nys5vZ1NIX2fXSuOtsMbOyby7v9VT30xFQ0VDO0NX09lnR1H/4+/309Hb08nF0Xf3WVFTX2dEs7vE///Xa1HMs624Z0RBPzY0PkVZSzU1QWdrXc7T09fOwc7rY1FLVf9vP/etuM/VxMO6v99rY0tFRUFBPD1X791dSk5R7/fnytvX
+ytO/zev/W1lXW1FRr7bN/2PfTl/Hqa3AWUdDQjk3OkpPQDU7QGNn793H187Lycn/X05NX/dnQr+yv8rfysPAvO/ZX0pFSkI+PkFT499NSFdX/1v/38Xby8rGxdt3W2PvVUjEscLNZ1tPWUCxpa/NRUtFQzs4QU1OQTE6SFldWdPVytPLxdf3X05Td99OVbO9
+zsXdyMfByc3Pb0lCV0tDP0pX3+NIQ07vX19n487L1cfJy9t3U13/SkayuP/f51NLTTvFpLHjSkhLRT87R1VNPzM5Q/9nd+vXyMjJw9n/71td9+9X67LD3cvOzsbH2c7E70RCTElDP0JZ4/9NQEtr/2f/59fXyMbKzs/rY2dvSlG3uF3j32dHTTvIo6/PRkVE
+PkY9R1VLPTU3Q1fvb/fX1cu9yd1360//52tPvLS+/9HPzMHH19/VxkZIR00/SkVZ9+NOP0hbW/fn59nd47vL0dXnY91nQVOxvU5T3U9NTzm+pLDBRkVAQj5LRU1HPDY3RE9n6+PXzs7Iu+fr62NvzWtVr7bIW8/V2b/I293bb2dNRUZGQkxXW11LREFOTWPX
+09XZ18fGzszX92frS821y29R0UlLTjrBpa7bd0Y9QUFDTk9AODY6P1FTa9vVz8zDzdvLd2v/12/fsLjZd9vR3b/BzNPX/2tOU0RKR0rvY09LREdGT05v1dXf2crj077vd29dRbm80VdV3VdMRT27qq3RW0pPP0ZHV1lBOTY9QklZb+PVzMjCyev30V/v50zb
+rr9fW+vT2crNytHd/+tXRWdHT1f/WVNLS0tGV1330c3Mys/V2dfFZ/9KUbTAZ0tZX11NPjpfra3Ra0w7U0NFVU8/OzU8P0dd/93Xz7+9zNndd9nb50jEsLtZV9vTz8fL083Md+9vTUFVWVdrU0hXTEhDSWP/1dfOxtHb19Hr0dlFY7G950xfW1tjQDtPrq61
+b08+PEVPVV89Njw7PUZOY+/d58m60+fb51/X30e6tMfZV+vRz8nKzNPP3//vV0hCTttrT0ZPb0pDSV/d09fPyMzbzs/T3+tVb7S/X1lZV09fRD7vsreyd0xDPz9H/1M7Njg+PkRNa+/j4829zuvf52/3/121tsj/92/nzdPKy83f49v/X0tKUffZWURZVU9D
+SGPZ19nJys3OzdfR2/9G77HFW05vUUxjRkDjsb22z1NEQkZGT1c9Njw7RUJHWffd28vE09nn729vV0+vsd1vY2v/yt/Pys7V7/f/X0tLX+9XV01ZX0xMSF3d087Lx8/Px83Oz/dPzLHFX1ldZ1dPRUbbsL+/301EQ0hKUz49Ojs6PkVGT2Pn2c7H1+fn729v
+TtG1uNtn51PVzuPTx8vZ1Wtfb1tO9+tfQlH/XUpFTVPr483Jy8vTzsvNzddXuLvG711XU3dKTEvEsL7GyE9JSUlTTEA5OUE5Oz9GTU9v1crJ03df52tjS9+yx3ff207ny9PZys/Ty/9VWW9d7+NPSEdNb0xCR0/n59fJzMzP1c/O0d9nsrzPVeNbU2dLWU66
+r7zGzllHU1lfSkQ7Oz5EQUJETV932c3M12NXZ2dVR8q10U1J3U5n79XZ1dvVz19OUVfn5+9KSlFXUW9VSlXjz83HxsfE1cvTzP/fr7nbV1Vra1VBSv+2tLrPd1tBR3dbSj48Oz09Rk9HTl/XzcfKyu9X//9jS7u11V1OW2/nY93Z1efd1fdVR01n2VlAQU9P
+SERLU0lZ59PTzM7F08/R1XfArrvna2NX6/dMStWwsLbRd81MS93nSD4+PT5AQUtVTVHnzsnMz9VjXV1bTrm4yVtNV1td3+/b79/X0f//S0dr011HQFVTTElKRmdVa9fTy83JzMvX713Tr73vY19VWU5dR9W4sbfOa9n/S+fvSz4/P0FHS0hHb1ln18bIzt3/
+Z2tRU7W0yl9OU2djWd3NZ2vb1+f/SUZn61FJQklNTUhKQENn9/f3ysvJy87R90/jrrrVX2NfY1VIWWe4tbbJ52vZWW//SkdCPUNKVU9JT//f48vH09/vd3dTScKxxlNOT1//W1/T2VVj093rV0hv52dITkpHU01JRkZMb+d318nLy83X3U33r7POT19dW1lM
+S061tb6+2Vvv2193T0VJPUBIX1tLTmf/zcbMyd/n5+9fS8S0v11OUWP/b2//2+tn79HXWUtfZ2dIS0xGT0xMREdRUVXn29/Kz83V30rvr7fNV1dbV1NOVVm6sbrLzldJ2e9nb01GPD9NXWdXT29r78XG19Xj4+NdS7uyx+9ZV1v39+//d2dr29/TX1NfY19R
+T0hBWVFFRkdNV09r3c/dzs/Z70rbsrjfVV1bTVFPV2+9trrG93dM3+tXY1lJPDtjX1VbU+tn48zJzf/X299rU7Wzv2dVd1lr39/jb29b28t391tdV2NnX0o+SGNFQkpNW1Fnb9HK387Tb0VjsLr/VVNnTEZRV92/ucHO3VNP69VOXWdRPDld91VTWf/3483V
+y9lv0ddrY6+vwW9Vb3dn79Xf72dnY8jRd29vWU9f/04/P1tOQ0ZOV1lda//Jz93TZ0jPsb/jV05ZXUVJXe+7vMHZ2WNFd81RTXddPTtR711XU29r2dHZ/9Xd6+NnzrCzymtdd3fv79XZb19v99/M3//vX1Nd31lIQ09RTUpMW1VnX/frytnvb07Ftb/vU1dR
+TUdITv+9vL/OW/9PU+d3SllfQzxFa2NdW2dv2c7b92/X6+9X2a+2z+9j72/3287jd1nr59vjztv3V2dX72tPRU5RTE5MTFdjY2vf583d/0a6tsRdV1lTTUZOUf+9v7/PXU1ZU9tbV01XRT9IX29fa2dn49Hd////42dn1ay61f//6+9n58nda1nj29nd28zO
+/0/3Z2dZTE5OT09RTVVfZ2fb3d9vW1G7ts1XUVlPS0VKWd+9v8DZXU5OV3fnTldJR0VTX3d3//dv39Pd//f3Z2NnvLC60W/v599n48zTXVPn1evZ29ff92ddd1tfV1VRV1NVT11bXf/n091rS9u3v9VNTllRR0NIXdO/v83fV09PVVHZX0VHQUhZX29v/+/3
+29vr72/nZ1dTtq+702/f3df/2c/VW1vn19/f19XXa+/3WVtXW11VWVVRU19bW2vn39ljSFe1vV1XU1NRTEVLW86/wdfnWUtXXVlva1VAQktvZ//3d2vd2evna/djZ09bta++d+/f79nn2c/nZ1/j0+fb28vPd13f70pNY19VXVdOTV9ZUfd33etbSdO6xudJ
+V05RTUtRXcq+xNXrT1FXZ113Z1tGPVN3/+fva2vb0+drb11nU03Zt7rF32Pv79/n19djVe/j49nf1c/L22/va2dLd29vd1tPU1tfXW/d599XTL28yvdZS05RSkhVd8i/y+tVWUhTVVtv90hBREVn79vna2/XztlvXfddV0fZsr7T1+tj3+vn191TTtvj5+fb
+79HX1f9rW1lZTmP371lTV1//d/fn1edbWbS4zmtZX0pZSUxfzcXAxmtPUU5MWVdd50ZAQk5O69vrY2Pv09trW19nSETKscLv/+v3a+Pf1edbUdfb69/r2dvP2d/nW1lPY03d329ZV1dj6+/n391La7S3zldnW1lKT05rycXCx29NV01bVVtX20w+QE5nU9PZ
+a1/v1dVrWVtbSz/Ls8DZW+9f71vd2+NRU+PX4+Pv59Pb1d/bb0xOX2Nn0d9jV1ln3ePf5+tOZ7a4yl9RW09RQ1F3xcfCzG9PU09XX1dX629DPk9Xa2/N919v1dPfX1dbTELKtL3ZY1tjb3dr2+tNU+vb393n59nR29fb61NKXVVnd9N3U09v49/Z4+9KTri4
+ze9VTFlTS0nfxcXIze9VT1NXWV1P7+tLQklVX+PVzv/33czZ/11fT0DCtb7ba19T/1/3a/dLS2Pf39/369nj2+fb92tRTWNX39vfZ1Nb3dXV3/9RUbW6xvdbTklVRVFXxsPHz/9VTVNdWVNTU+9NSEtOa2vO19/n59XT3WtdT0bVtMHX729fa+9r21tMT3fv
+39nr/9nZ69/r72tZTElVXddva1VZY93N63dLWbS4zu9ZVU5LTUvjzMTGzmdVVVVja1lRV2dbTExRU2frzOvf39fd3etdS0bHtsPV43dr/3fnd29KT+ff693X493T2+PZ/+9jV01OY/fnW1VTZ2/b2V1Ha7m73VtbUUtLSEpdxcvL02tNT2NvZ2NbU11jXVlV
+WWfZ39Pj28/P49lrTEfBtsfX9+9n//f3/1lVS2/d2//Z0d/b193j9//3XU5RV/dna1NVd//d02tIZ7m5311XXUtJR0ldycDT3f9ZR13/W19XV1FdY19VUWfr2+vR2dvO1+t3XUjGtLzX6//v7+fr61VKUVXn2et319vf39/Ta/f/Y09LU2t3WWtfX/fj319K
+S7i432NfVU1MSVH/zcfP72NbS1n//19RXVNf73djU1/v3//Z09vZ0+djW0nDtb7X/3dn4/fv709HSGN37+/369nX69/R63fvX1VVV2trVWf/Y2/V3V1RUbi8y29fVU5NSk5ryM7N419NT1tb92tVV1df599dWV9r72//ztnd5+N3TUjVtb/X52dv5+/n91tM
+R/f/513f49/Z3d/X22vvWV1ZY1tnY0/da3fv32NHa7a5z/dbW1NTU0z/xdX/21tJTl1Z6+9fW1dr49dVW2tv9+vr2c3d4+93UUjMtsXb//93d+P3Z1VJP13vd2//2ePfb9fb6+///1FnY2//b1dd43fvX2tMWby0xfdnWVlTU07jxtf/a11PTFVv//dXV1Nb
+99n/W29va2vr79PM33dbWUrTtrvP92tj//fvd2tLTGP3d2tZa9Xd7/fn9/ff72Nn62tZX1NM59/X/11NX762xnf/Z1lPTEvTw87ZXV9MTE1XXW93V2tnW9vnZ1nvb2/f5+/T091fWUrbt73Rb2d33dHR32NFS19b511dXd/j3d3f3evna1FZ92tZY19Va9Xr
+Z1FHSLq3y+drY19bTlfTxdVrWU1VT2dfXe9vV1lbY+/Tb2NjY//R1/fjy91rSETrtrvL5+d3b83X21dPUVljWXdj6+fn39vfd3fnY1132fdTT1VRWXfbd05GRsC1wd1jd1dNSkvjyc//71lPWUlZW3d3611R7+/T3+v/69/T01v/zc//V0lVuLvZ/2fv387R
+d1VTVVljV113a+f///fN29vbZ2Nj311TVWNfT2Pj/11JR8G3v/9TV1VLTVndydX3XVFJTUxdX1tjX2NXd+PR2133a9vN193V09X/TEZTubnA/1vd3cnP6/fvV05XTU9nb93R42/f2f9rb+/j/2tMXXdbW1/b21tKRWO8vtnn/1VPTk9rzNHZb01HQ0xXX2Nn
+XVdXX2v37+vf1+PV3+PnZ9ffZ0lIvLfBZ+/dysnZ5+9jTVtj/11Tb+vjb+vVzuNj99vjd11db91XWVtZX2NXS1u5vsprUUxfY1ddzsfZZ1VLRUtbV1tPSk5V79/v29v3a//ZztXrd+vrXUtEv7PE73fb49XR1dHrY1FbW01NXdH/693R13fj38/V719ZZ1lf
+Z/fv7+9VRUG7vchrU1lTX0xvy8PPb29FP0BLX19ZV29XW2Nf78/b7+vXzuf3Z/f/91s/TLvGb+vRzszK23fvX1VZa1dNW+/n78/O1+ff3efvZ2/vb9/nX19dZ+v/Skq8tsddVWtrXVdVzcbf/1lJREVOW0tHSl1RU1133dPX93fRxt3nb1Ndb1tGVbi8ymPb
+3c3Izu/n901bW1lLZ+/v2evnb+f/0c3d611dUe9vV/9dd3djS0zRucBbV2tZV1NnyLvD41tJRkNPT1NXUVtZVVvj2c/J92/nz+tra1nv629GQsG+y2vb1c/J2+f//1VZa0xJTF9r29fbz+P/793r9/9d/2f3Z29bXWtjY0pfvMrrV29v52djyL/Ha11MS0pL
+T1NfV2N3Y2/v18/R6+fLxs93XVlfX2dNRN29yvd368zEzXdfX05LTElPW2v/4+/r6//v3e/X3WtfX2ff2/9fVVFdX0trub/bY1tbZ133vr7JY1tXSklKU1dXU1VdVWfvzMfM09fRz89ra19bW3dTQeO7ws9r587I22Nd6+9OTU5MTFdr593f4+v/b+vd0edn
+W13nb3dbX/9vZ05Tw8DZa29bW2PXxb3M//9PSUJDSlNOT2dnb2fvz9nd0c3X0d1vd11db/9RRVu9xut308jE0Wv/71lITU1ZTlVr993Z2dPdY2/V72dfWffLzu9dU11bU1dTz8TOXV1jW+PVv8fM709MQUpEVU1RV0hTV/f30eP3ydfX383j91VNd09PRuO5
+vM3vytPP49/T611GUU1VWVvf69Pf2eNbWevJ3+t3b2vd129fV1tbVUxFzL7K91dZU2fnxMLB3VdVR0Q/SEtXWUlVb3f/29PRyM/d/933U01f72tdUV/ExM/b0dPb2+vd/2dbW1dVX05X/+fVzNX32dnX/13352vfyP9TU133Y1NHzsDNV0xZV2/XvLvGY01V
+QEJAQ05MSkxv6+t3389v39Hd/87N62dbU1dXS0nGu8rf59nTzdnX319NR1NVXVN30c/O3dnn693V02/v/2/TyNPrZ1tdTkNF/8DC31trW1Pvzr7H709OQj5FRk5PUUpZb1Nd49fj18nV3+vZZ09VY19TTELGtr7N19vj3Xf/61lTY/dvV05b6+fV0dPX/+fT
+z+PR19vZ09V3W1NnV1lRTb7Fd1NZXV/bzb7J61lPR0RCPURDREtf///n3dfvb9vj4+PdWVNPXXdfVUfGu8HnY+vj09vR2f9VT19VWVFd/93b1crL1d/V1+fd4+/bycvX529nUUpFVb/F/1FXWWPPvL3N/0lDQDw+Q0lMT19vX1Nj499vY9vT4+PZ729ZW1tV
+Rj/LuLzZ59fR3293//dbUVFZVVVf183b19XZ92/32c3NztHKw893Y2NbV0pGTcK/yu9nU1PbwsTNb1FNSkhDRkNFR1drXWNd9+Pr48rG099rX09TV19dU0trtr73Wevn7+drZ2drX2d3Z09RX2Nj49fV3+vVzs/X3dfLyM7jZ11ba11dT8e7zlVMTFPLvbzA
+1VlMQkJEQURJTExXX9/X2d139//Z2efj9//v62dVTkdPvL3NX/9fb9vZ2d/nT01JTUdNXWvd29Pdb1Nj3dPP993N08vO1efvW0xMPVG+wuNrZ2/Ixb7L2d1OSUJDP0dVZ+9dd2Nra19r79PNv8nf62NnZ19ZZ0tHw77H29vj9+9dZ19rTlVfa11OU1Ffb2/n
+d1tR/9vX08i/ztPjZ19VT1FVSPe5v9NNUVvbxL3I1+NLT09OSUtXTk5MX2/n293P09ndz8fn5//fZ1NbX2dT07zE91dTTf9r39vba05RTEpGS1Vb91lfX1v/2dXn2dfP03fd3dXv/1tNREO/v85bW19vxL+9v85ZRkxAQUNMX1vr79Xfa2tv1+PVzMbb99nZ
+1ef/Z1dHS8HJ2+/3d9/Z/+tbXVdRU0hERllfd+drZ09RXWdf68rFxtvT1edZUVFNRkHJvM9db/fbxcXBx+9NRE1PU09nb1VbVd3jY2fn19/Ty7y+09nj62NZV29dU729x2tTU13bd/93a1dOWVFVS01XU05KT09na2fXzsjH0Xfb2/9vVU1NS0zZuc1jU05b
+48i+v9f3WUpMRUpTV1NNXWv3Xf/Pz9fby8nK3+fZ2d3n29tdT8O6yF1PVXfn49/R0f9dV0xDQUpbXVtV/3dZUVn318/O03fn993f/1tLTUJPy8zfU1dd3cK8ydP3SkdIS0pMT11jb+Pnb2t3a//bysTCx9HX09nn/29VT9++zGtjd9nR0ev/b2NXT05RV1Vf
+V1VTXV9VU1l3683Hz9HN1+d3UUpITEhOwLvVV09Md8S9yNdfR0dLVVFdX11LSUtVXW/j69vRy8bFzdfV2d3nZ1tjU9G3wdtjWV//69fR09l3W09VTllbU05TY1dRW+vTzMrL1Xf/d//fd1dbWURI177TT0hJb8vEyc7bW09OSkJETVFTT1f/d19nb+fZzcrL
+zdvj087T3+frUWPBzetr59vZ08/V199fWVdVSk1RU1tj3+tfV1tr99/Zy87R2e/jd2NTU0dH78DPTklN58G9w9d3T0VGSVlbXVFMSkxfV11ZW2v3z83MzcXL0dNvW1ddTHe7vc5v7+vn18/O1+dZWWNbV1fvd2dfV1lPUWfVz9XMzM3X62/d42NTTEhFZ73H
+70lITmfP1c/R909HSFFZTk5MR0xOW1dZW+/NyMXOysvf39vT393/V1vNvdX3Y2dn2dHX1+v/b29dTkpMT05b/2tvXW/r3ePRy9XX6/fR0+tnd2tRXcLHY0ZMWePKyMfL3U5FS1tOTExJSElRY2NRV2d32+fjx8DN59vb3f9fV0t3wsvba11v18vO193/VVNT
+V1dfX2NnY/9rXWtrZ+vO1dPd49/VzeNbTlVIUc3A41NPTnfNx8XK31dKVW9bVVNTU09VTUxLV+vd09nTzcbJ3+Pj32ddU0hbv73X/1tnd//v99Xjb1ldVU5jZ29TW2djXVtra9PNy83d71/v19fnZ2tOSW/Az3dTSFX3ysbI22dVb+dvU0hFS01ZWVdPXff/
+39nMyszR5+vj3/f/a1NRz73jU1dv39/v69f3X11rX09TW2NPU29362///9/d99nb19nX1dffY/9jSVfHyvdPQU7vyL7Dy11RUVVfUVtPUUtOUUpdX/dvb+fXzN3Oz9XM1d1jW0RM2brDb29nd2dr19HbXV9fXVVV9293b+dnW09T693X3dPX7+/d08/Z929Z
+QkLOv9FZR0tn1cvL1f9rZ3dfSkpNU09PUU9dW11ba/fNxc3M1ePn2+Pn51dPa77G0etv/2Nv39/rZ13/529vd1lVX3f/d19f/2dr59fN09fd29vfd2dnRkn3v9NJQET/zcTG1/ddV2NrVVFRTEpGSk1fa2tr9+fj3+PTzNHV299rZ1tZZ7+7xOtVV1X/0dPd
+229jb11v/2f/43ddX1V343d349vd3+Pd2dHM1e9VQj9NxcjrSEdVd8jJz9PvX1tXS09JSU1MU19nV1tVY+fd193f19Pd39Pd32tTSv+6wuNva1/r2+vn6+/r3/dnY2fr6//v72djZ1ljb9/Rz9Hd39vX32dTTUVIzsdnR0dR28vN1d9bV1NXX1VNSktITlt3
+d19dZ/d359XTycnT3d/b9+tnWf+7vM5nTmN36+fv6///Y9/j/93n711bV2tvX29r9+PV1dfZ1dHP229VTENHY8HNTkNGZ9vMy9vnV1FTW0tJTEZOSU9dXVtVd/fn5+/f69fX19fP0d/3XWfDusDjY19n9+/33c/R1dv3Z+/na3f/d/djXV9jY+/X1dfv69nb
+5+f3WUk9QdfRV0dKY9nNzdf/X1FbY1FHSEdGS1vd2XdjV1dZa+/b1d3Vz9fV0dnfa1/TvcP/WVv/59/r6+dvb+vj9/fjd2tnb+tjXVdnZ3f369fZzsvN6+9dTUtARu/F21NMXevPyt3jZ1FRV1FJVVlXT1NnY1tdWV9nX//f5+vb2dPV29/rXVdfxbvN/1dV
+UVtd/9fV6+vd3d//b2N3d/9nV1dZ493Lztfb1d3n3evnY1FGS2vNzVlVV3fXz913X1///1dISEpHTFf/529fW2NbXXfn6//n2dnn38/P1f9f1cPRW1lRV1Vr793d///r33dnX1NZY/f3/2tv93fv69/O09fZ1d/f/1FMSlPOyGNTUefKycrbd1dXU1FRT09P
+WWfn3fdnV1VXWWdja2/rzMzP0dnn/1NH3b+/409KSUxVb+/n/2v/b1tbb2v3719TTU1V793Xz8rBy83N19vj905EP0PVub7v79HP1dX3X1lNVVNTT1NMTFn/1+9VR0ZJWffj19vn29Xd187N0WtNVcG801lERElTW///b//O0W9bSklKT1VZVVFj987R283K
+ztfR4+Pvb29rY1XbvcH/b9XRyMvV5/9bW1tRUUxMSk3/5+9r/1lPUUlPVW/3187X19HBzltHROPB11FEP0pZ08fb/1//Y1lRUV9RWWvn71tf59Pd49PR0dvRysfb/19PTENRx7vT3dnRx83b/1dNTU1PT0pVa+fO0f9RSkNGVVFja+fXzsfX3dPH41FHPF/D
+w91bSUFPW+NjX//v519jV2tZWW9v91Vj99nZzsDCwNfX2f9ja+drW0xPx77K4+Pd2eNfV0ZJT1v3/1tNTUpf/2P/VUlNY19j7+/Ry8rva9PKy+NvT2PLyO9GRUFV19n/a93372NRUVFMR1FbY2/j0cvN59nf3///ztnj79ffa2tP98DC3+/X49fd52tdVU9d
+UU1JTV/j4+/jVUpMVVdbW1fd0crN08jAw+9PQT9rxc3vVUdd3edv9+9va01MT01XXf/j32trX2Pv993X19/v0d//a93na1dHTdnD0c7N09f3W0xHSl9v92NPVe/v39nvVUtLUW9v/9/GwMLJ09fRzN1rV0tjwcDXV0tRa1tXX19fXVtfX09VT0tPV19rY2vv
+3+Pf5+/v4+NbX+/j7/ddT9/DyNHfY/fnX1tPXWPv52tXUW9db93/W1dba9/n7+fXztHOzszJxcvna09dzsbTXU1NW13v9//na2NPSkRNTUtj99//Y2PX72tjXWNjb/fvb//T0+dLQE3X0dPX393nXVdVVVtj/2NKTm9v59fd32tRW2tr/+/TxsbExsjGwM7X
+/01Pb8PG129jY2vna1tdUU9bV09VV01b/+djT1Fja2NbW2trb+dra2Nr711NRU33zMvX19//W1FIS2dv3/9dZ/fv3dnd72NRY9nT3dPLx8bEx9PPz87M62NRXcu/yPdbRU5bV1drW1FjVVtXUUtOXW9vY2/r6+v/Z2tnZ29jXWvXzt1XU0pK18nT12tVY1tN
+UV3/52NRVU5OXWfd2WNXZ9nX19/X19fMx8fMzMTDzN9nV93Jyc/3XVdnb2trW2NjUUpJSk5RW+/3Y2dr929LSVNja+9v/+/fztX3U0Y/Y8bDxtfr92NXVV1jY1dnZ1NdZ2vf1/dra2Pr6+vd39PKx87Mz8zK2e9dW1vVv8bXVWNbY1tOU1ddW29VU1VXb+9r
+W11b9+tjXV1ja+vn92/32ef3Y01DSe+/vc/rV0xLS1PvY11jV05bXVfv/2tdZ+/d2d/Z3c7Mz9PZ2c7FydPrY13Zvb7NXVNTV2dbb2tv9+9vTk5OV2f37///729jZ2djY2tra//f3efnZ05GQ1HOxdPfXVFNS1FXV2P/TkdJTFNr//dvZ2/V2d3/Z93MzM7O
+08rKycjV71fvwL3J52Nbb2dr//dja/djWVlda13//2/v3/9XXVlnZ2v//+fn2+fnWUdBPUvNv8XdW0hOTlFMSFlbU0lISVlra99nWVvr1dvf/9PPz8/TzMnHz8jN62//yL3C1fdrW2dTb//32e//V1ldb29n31vr529nZ2tn713r3+vrb+vv909JPT/fyL/P
+Z0xGT0xJRlNbV0xER0tRWWtrWW/v2d1r/93R0dPf08rPysvM1d/nzL3DzO9va2/39+v3393r62tvWWP37//r3f/va29vXVdd59nda2vva1tLQD5M28bL71NKV1dKRk9jU0o+P05TXWdrV2vr3dX3///b29vr28fMy8zIz9n368G+wd9ra1dv/+vf5/fr32dn
+T13r7+vv3f9rXWvvY11n3dnd7+fZ529KREJM2cbD1W9ZWV1FR0xZWUpCP0pMXV1nb2f39/dnb2/33/fn383N2dvPyNXfb9W+xttvb11vb2/n//f32+djXV3f//fr39v392/3b2P31cvf5+vf629PRUlK2cjG0f9jWV1FSU5OU0hFRU9OU1lZb2/39//39+vr
+3//329XO2dfXytXnb9m9yN9jVVNnb133b2/32/dZTl33///369//9/ff62/329Xf29vZ329XVU9N38rGzd/3b1NISVNjXU5FSUdHTVNd9/f39////29v92dv59vf69vPxs33X9/DyddnV11jb/f3//fn29lZTGdvb//f383jZ/f3b2/329fj29HV42dZV1NK
+VdfJz9nb91dTSEpVTUZHSEZHR1Nj9/f//2f3b2/3Y2Pr2dXR3+POzNfjb+PDws7nZ2///29v/+vb3/dVX/f/6//349v3//9n9//n2+fj29n3929dU0dGT9HLx8zVb09NTE9PS0tPT0dKS09nb+P/Z2Nv/+fjb9/f6+v35+PO0dHr99vOwdH3XW9nX2NZ99vV
+1edfWV9db2/319fn9/fn2ePj3+fb2+f3/2f/b11NT+fRztHb611TSE1TTExLS0VBQ0pZX29j9+P3//9vY2dv6+Pj69vKyc7n49vTyc7nZ2dvb+fj29vX511fY19jZ2Pjztvb9//n5/f392/b39vV5/9vXU1JSl3OzNHb4/dfTU1PTUtIRkY/QVP34/dfZ/dn
+Z/9v9/dv69fb29XOyd/36+ffzMvR1fdnX2Nv9+fb919jZ19jZ2Pj19/rb2f/5/ff39XR3+f3b//3Y1VOTWPOxMrV3+NZTk1ISk9ITVFIQkpRW11Vb/dfX2//6/dvb+vr69/V1efb1dHTzMjP62Nnb/fr99vR62/nb11dVWf36/ff5+vV1d33Y2/d3/f3b/f3
+Z1lbVVXdx8jb4/dnXVVZY2NJRklERElRZ19j/+Nv/29n92dbXW9v69/OytPb5/dvb9fN2/9jY/fn39vb52//X1VVT2/n18/P3W/j6+f//9vX3ePj6+PrZ19ZTUxj28rNzM3Xb05JTk9JR0xLREdVb2dn//f3X19fb2Nf9+PT293X39/j4+vnb/fKzd1vY2P/
+9/fnb19vZ29nX19v//fn6/fj3eP/39PO09////d3b2dZV1t3z8XKzNf/b1lRW1dOTVFMS0dOWVFZX29fZ1/32+t3Z29n/+vb2d3Z39vd/+vTzONnW1139+Pd2d//b11NTFVn/+vr493T0933693f3+tnZ//34+NvW1lXd9fNyc/r/19db19ZUU1KSEhMUVFf
+99vZ/2dvd2dnZ2//993X09PT2+v3W1vr0933Z3fb3d/3/29fXVlTS1Vf99/r39/d3/93/+Pr2dnf3+v3/3dVV11dZ//KwsbX42ddXVVVTkxMSk5XW1tnZ2d3W2dv/29vb//d49/f39vb4/93W2fry8rZb13///f3491vb19bWVldX/9n/+vf3f/32dHX2etn
+/2dv//9bWWdnb//KxMvVX1tZW1dbX1tfUVFKSFFZZ2f/X2/r69v3///d4//r69HV09vj62d399PjZ2dn63fj49frZ1tNUUhNU1tvb9fP0eP/4+vd9+v/9///3evjb3dnV1/XwsnR///j/3dZW01TTkxMSFdXX2dv/3f/Z3dfX2/34+vbzs7R43dfd19n69PX
+9+vr2evr2et3WVVRV1VX/3f//+Pd63f/4+Pr6+Pj6/f/9/9nW19fUV/OwsDP/2dnW1tVW1tXVVVPUVtbX1tXW2dnZ3d369nZ3f//493j4/dv/2dn68/T3fdvb2fr3ePrd29vb1dXW1tfd93Z3ffj2d3Z493db19v////d///W2fvzM3jb29rW19bX2tfX1dN
+TE5NU1FTb+Pv7+//9//v7//r2d3d3e/36/9rZ+PZ43dnb//V0dnjb1dTV05XW1tr993Z2d3d6///9+Pr/+/j493d63dXU1dr0crR3e9rZ3d3a19bV09KTVNXW1tb//9nW2N3/+/j3efr1dXV2ff//2dXX//Rzt3n//fj5+f/a19rW1NXU2t39+vn7/f/Z///
+99nZ5/fvd///a29dW1df38zM1edjX11fY2NvZ19fY1dXV01PZ2t3/3f/4+PZ1d3b29/r6//36/93d//VzudvW2fv39/3/2tnW11ZVVdXW2t399/r/+/369//a3f37+Pj6/9nZ11d69XR2/9nXWv//+/n/2ddU01NT1Fda2v///fr4+Pj5//n49/d493b3/9n
+XVvn1dfrb3f339vj7/dnV1VPVVtjZ//r493/Z11jb+/r9/f359/f53dnW01NV+PO0d//b293/3dvZ1tnX1VTU1dfd////2v/69/b39vZ1dXX3ePn/3dvb2vf09Prd///7/f/b3dfW19XW19vb2dfa/9ja2dn//f/7+Pr4+/v/2tnW1dRVevKyd1vV11nd+vr
+92dvY11ZT1dZW193/+/j49nV29vd5+fn693b5/d3a2/v1dXvb2Nn9+vr5/f/d2tbWVVba2P/6/d3d2d36////3d36+Pn6///Y1lNTFn/1+f/Z19f/+vv/3drW1FMTFNbX3f359/d29vr7+Pb2dvd3dfX293vb2dr49fb73f/7+vj3/9rXVtfX19rZ19v6/fv
+d1lfa2t37/fn39vX3/drW1FMTFH32dv/b2Njd/9rX2NfWVVMTFVZXWNrZ//v3dnj39/d29fb19PZ29/n9+/r39PX1+f3/2vv39vr/29rZ2N3a2NjY2tvY1tja2///+vn6/fv/+//Z1lRTVF319Pba1tfY/93d/9rX1lRTE1NUVtnd//r3+//59/f3//3393d
+29vf3+fj6+/j3+fv7//f29vj5+vv729vY19rd/f//2v/d29rZ//v39/j9/f/Z2NTTk5Z9+Pfd2dda2//73dfVVNMT1FRWVlfY+fb2+//7/fj5+/j29fX0dvf6/frd/fb29//Z2vr39vn//93//9vXWt3b+93Y2Nna3f//+Pf39/j29/ja11VU1ld/9vZ7/93
+X1lZ/2tnX1FOTU1NWVldZ2vrd293////6//f3+ff39vf2efnd2vn29Xrb2P/79/b9+fn39///293Z/9r/+9nd2dnb+ff3+tv7///Z1tVU11Va+vn72Pfb2NjY1VZXf/vZ11dV1dda+fn/11r59/f69/f59fR0dfZzs3R62tj/9PT2e9jXWt3b29jY2dZU1NT
+WVld//9na2v/d1tPW2dr/3f///fb3+9nT0lV2c7R3+//Z2dja29vZ29vY2dvd/fr93d3b////+/3593b39/n49vr/3drZ+vV1+tnb2tvZ19nZ2tnb2djZ2N3/3dvZ2f//3dra29v//f////v9/9vX1VX/9XR3f9nXVtbY/93a29nY2drZ3f/d2tr//fr9+/3
+493n5+/r59vd5+9va+/Z0+NrY2Nrb2tvd3d3/3dfY19rb3f//////29vd29vd3dv//f39/dvX1lVY+Pb3/dvZ11dY2trb/9rW11dY2///3dvd+vv6/9v9+fj4+Pr39/f2+P/Z2/f0d/3d2t3d2///3dv/29ra293b3f/d////2dva29vd//////v//9rW1lR
+Xe/Z2/dvW2NjY2Nba29nXVtbX3d39/9ra//v7/f/7+fn5+/j393f3+P//2/f1dnf/3dvb2N3/3fv//9va293/2/3a///d29vd2//b//3//d3////a11RUW/f193/X1tfX11bX29nX1dbXWdrb3drd//v93d39+vr6/fr3+Pj4+Pr9//j19fj/3d3d//////3
+9///d3drb//////3//93d3dva2//9+93d/93a19ZU1/v393/Z11fa11bX29nXVdXX2dvb3drb//37////+/v7//v3+fj5+ff7///39Xb93d3a3f///f////3d3dnb//////3/3dvd/93b3f/7/f//+//d2dZWV/v39vrd29rb1tZXWtrY1tXXV1na3d3d///
+/3d3d3f3///36+fv9+/f6/f/79fd63d3a2//d//////393drb///////7///d/93b//35/f///f/d2tbX19339/r/3dra19dY2NnX11dX2dna2d3d///d//////3///37+fv7+/j7/f/79fb729nZ293b///d//3/2tjZ///////9/93//f/d//37/f39+/3
+/29nZ19349/j7/93b19fZ2tvY11dX11jZ2v/////////d3f/d3f/9/f//+vf4/dv99/f73dra293///////372tjb3d3//f35/d3//93///37/f36+/3/2tra2Nr7+Pr7/f3b2dfX2djXV1fXV1dY2//////d/93d/9vb//v7+vv/+vn6/93/+Pd6/d3d/93
+/3f///f3/2tr////////9/93/3f////39//39///d29rY11n/+fj5+v/Z2NjY2djY2NnX19jZ3d3//93b3f///93//f//////+vr6////+vd4/9vd3dvb2v//+/v/3drb29vd//v7/f////3///3//f39///d3f/b2djd+vr6+//b2tjZ2tjY2NjXVtbY2tv
+d2////////93d////////+vn7///9+/n5/93d3f/////9/f/d293d3d3d//v9/////////////////f///9vZ2Njb+/r6/f///9rZ2trZ2NfX11da////3dv/3d3////////9/f/9+vn///////v6+/3//93d/////f/d3d3d3d3d//3////d///////9+/3
+//////93b2tnd+/n4/f///9nZ2Nja2Nna2NdX2dvb293/3d3//////////////f3///37+/v5+/3d3f/////9/f/////d3dvb/////////f39/93//////////93d29va//r5/f//3d3b293d2tfY19fY2t3d2///////3f//293/3f//+/r7///////9+/v
+/3d3//////f/////b29rd//37+////////////f///////93d29nZ2//6+/v7/f/b2drb2djZ2djY2///3f///93d3d3/3f///f//////////////+/v//93d///////d/93/3d3d///////////////9+/3////////d29vb//v5+vv9///b2tvb2tra2tn
+Y2tva293/3d3d/////93d3f////3//////////fv//9vd///////////d29nb3f///////f3//////////////////93b2///+/r9///d3d3d29ra2dnZ2dra3f/////////d3f///////f39+////93d//39////////////3d3d29nb3f/////////////
+/////////////29vd3f//+/n6/f//3d3b29va2tna293d3f/d/93d/////////////////////93d//36///d///////////d3dvb3d3////////////9/f//3f/d////3d3d3f//+/r7/d3d3d3b293d3dva2tra293d/93////////////////9/f3////
+////9/////////////93d2tva2tvd/////f3//////////////////////9vd//r7/f//////3d3a2tva2trb293d3f//////3d3///////39/f//3f//3f/9/f///////////93b29vb2///////////////////////////3d3d3dvd//r6+////93d293
+d3dvb29vd3d3d3d3//////////////////////////////f/////////////////b293d3f///////////////////////////93d//39/////93d3d3//93d29vb29vb2/////////////////////////////////////////3////d293b293d///////
+/////////////////////3dvd///9/f//////////3d3b29vb3d3d3f///93d////////////////////3d3///3////////////d/93d3d3/////////////////////////////3d3d3f///f///93d3d3/////3f/d3d3b293////////////////////
+///////////3//93d/////////93d3d3d3d3////////////////////////////d3f///////93/////////3d3b29vd3f///////////////////////////93d/////////////////93d293d///////////d/////////////////93d293////////
+////////d///d3d3d/////////////////////////////////////////////////93/3d3////////////////////////////////d3d3//////93d///////////d3d3d3d3//////////////////////////////////////////////////93d3f/
+////////////////////////////d3d3d/////////////////93d3d3d///////////////////////////////////////////////////////////////////////////////////////d3d3d///////////////////d3d3d3f/////////////////
+/////////////////////////////////////////////////////////////////////3d3d////////////////////3d3d3d3//////////////////////////////////////////////////////////////////////////////////////93d///
+/////////////////3d3////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////LnNuZAAAACAAAHDqAAAAAQAAH0AAAAABAAAA
+AAAAAADT087My8rM1W9369/j493b4+tvVVFVZ3djXUpVVVlfa2v/39/Z19vb3ePj5+/n3dvNxsLCwL+/wMXIyMzf93dnVVNVTktGQkNAPz9ERUlKV+/r7+/fzczLyMjAv7/Bw8TGycvOztXn/1dIQj89PT9DSU5TWV1j/+PbzsnLysjJxsPDwr/CxMPHy8/P
+19/n19XfX1lTTElKS0pJSkpNU1FbZ//r3dfT0dXV19/3Y+u/s7vFy9t358SysLnMa0U/S+/HydtOOTIxOERv901BOTQ3R93Au8TX91X3x7qurrW91V9j6761uMF3PzUzOU3f72tKPDk9Tc67u8TdTkZT1bmytL3Ob0xjzcG3u9VnQz5La8a9xt9KOTc7TNnE
+12dIOTg/a8C4uslnQ0NO17qztsLfZ1nTubCtssTdS0FP/8W+3Vc9MDA5R+fjVUI3Mjg/27u3vc13T1Xbvqumsr/rXVfvwbi5yF9HNzU9T//vSjs1LzhI97++1e9LRE3rvLCxt8brX+/Eta6wvttJPkR3v7m6zk05NThH3czNU0A9PTdZ9+fLXe/rY8XTvb/R
+003Z49Ovsa2z3ds+Tc3ArbnESzUwKj13zbp3RCwpLCtVysS5X2M7O+PZr6mvrtPdT1W4tq2vx9c2RENOw9vbUTk2KjlE373bzk88Y0PIv7y41873U7/CtbHCwU9VT1XFxL7XXXc6U3dfu+//SjM4NE3Oz8JPTDk1TOe4rrm9WVFDTMG4rqi2vv9NS//AtLS7
+40QuNTtDz8xnTjMwLTFBY9PJ499V793Gtre4usfByru8v71jd1dL2efOzU4/MTAwP3fby1lBPzI7S9uytrrMWUo+/7+vqKy250tLRdO6urHPbz46TlXVvtPRSDw1PU1Z2e9OSTlFSO+7yMfTY2d3xrq0ssDD/+vDxa+wubxMRDk+2efEyk45Ky0uPPdj9z84
+Ni5EZ8KqtbfK/+vrvq2tp7fEQklnVcG7x8A/OS4zSknn/09ONkA9XcbLwdln90nN0cCxysbbX8nZvbjDy0pKOEjOybW5z+c0OTdEv8u/3UQ1KjI767a9u2NNQjrbvLWntbfRW9vfvK+xrb/RPUBTRtnvU/c1OjQ2TD9KQTo/NUlbzrbDvtXXxcqwrrCvzNVM
+V8fKubvNTTYzLUDvd79ZRjQpMjZjssa63Ug9PGfPuKu6u19vXV23tLGu1Wc7RFfjvru+x0pKOE/X78ZXTkMzQDpM2WPvS0lNVc7HubfEyVPPvr2ssLG562tM37vBt8xrNy4zMU3NXWs1MCkoNDz3uMq/b1NP77+yq6iztm/Zzcu3u728T0g5QVVb3VVTPjM8
+OVfn/91GUU9Lxsy8uc3rRlHdzLextMHdWT1d09O3v8rjRkVD777HvN9OMjI4Om/L99k/PDQ4W9e9sr+763ffxLWwray9wl3Xx8+5xszNOj80O0tJUzw4MS46OlN3TndDWd/ZtbKwr77C6866ua+uu9NMRDdP093FW0g5LDEyQs3/11M8NDVHY7+xurxrU1VO
+w723rL3C92PO177ByMRNW1njwsq+71VON0VJTs9XTzcyODRJ3cm+19dCTefTtKyvrsDF/+O5tq6tvMZFRTpF2e93WTo1KC4vNU9ISkQ0Oj53w7qtr7vJ27+/sa6xr8jrVU7PzsDKXU0yNj0+9/fjRDo5MkBv973FxWtKW2/NtsC8zdFHU9nJt7S+wEpKP1O+
+u7e1zGs4QEFZzdXjVTUyLTxDTcnr/19Aa+/EubWuwcXIyLS2rrK7u/9f71/J3+dLNzUtLz0/Z0tBMy4yOFG+vrG8vs1nxLiuqa+vztVRX8HAwLlvVzM4NTlnd2PnOTYsND1Lycrr40JRTc28vrbB31tNzMi2sLi451ldTsLBurvP0UxJ32PI391XOjkyNE5L
+71dLOTVAVdm1u7fFzGPru7KtqK+z6+vv17q2wMZFPiswMjVNST4/LC4qNEZdwMvVz1vFwrOusq+2zMrZvLy3t83bRD5RTN/rb008OjU6U0rjY2NNOUdVz7q6t8/fRkH31cSzubrZb05Rz72+uL/MSEpra8G7y9VAPTU6RD5XWUE+Mjg/Z87LvcHV0+e+tLKt
+s7W5zsG+vbK1uetJNjI7P0pRPzwsLS4ySF932/9VPlnIu7CqrrLCx+fAuLe0t8fvP0lGXc/f91E5Mi8+P1FrSkM2ND1F08m/vdX/X1PRxLm/w8hvd87FuLe4zedjSffIwre9v18/OzxI1/f3S0AtLzU8V8vRyGdVRWu/ubSstLnLxr+4r66xtMjvRUtGSFdE
+OTQqLS82PERJQjs8PFPPurW2tb7JwcSxra+2wcz/Tdnby8fMX0I7OTx3Z29nSjwzNz1H1dvGx9lfY+vX2cLLx/fv79vJw8i6w8rT39PXysbZxef3/1dNRktIP0k7Pz9EPT5LUU7f68q9vL/CvLq/t7ezsbi3wMjM28tXTko9OTExNjM7NjdDP0BHTf9ny8rD
+ubq0tbe1vrW4xLzGwszd90z/W0dZSldDREk5Qjk4Rz1LSUlrTGfj3cDMydXVz//d29nAy72/wMPTy9fVv8i+zdfTd+tRTudVS0JAQTxBPjtXSUpTXePdxcG+t766u7e3urC5vLW9ucff70dJOzU8NzgvLzIwNjk7T0xda/fT18i7uK+zs7a7w8jAwL66ztNv
+V05JWU9P61FPRD4+PEFCS29VTVNf6+PJxc7E2+dv69/Xz8vIvMLEzs7V3czPz8LKzf9VS0VLREpXSkk/Ozk6QUVN09fKycvPxbq5uLK2tbq+xsW7u72+y2tHPzY2My42d1M9NzEsLjv/s7LKxmdNa7+rqKitum9Ob8W1r7jNXTsxO1vbxdNIQTAuNTtn3/9X
+OzI5QtO4sb/Kdzk/XcyxsrrNb0VE/7eurLfMTj5FZ7+ztcVVPSwuPUzOxVNEMCouSdG8tsTIV+vbvauqrLe9WePCuKirrsxGMyo4O+/ZRTQjJCMtRee1/182Lzt3rqiirLvJPEvjsaOhr9VMKDA6a6ysrVk6JiQ0R7KvvGsvKSQwW7Cnr7hAPTI52bustbpd
+O0VB2baqtLrCOFFJ2bq3tu9fLzI8PdVRY0U5Ly1BRM3Mz87j1/e5r62yvLzfycu8rrC10c9OR/dK51M7OS8tKkBDTuc/RTpBP3e1sq68wM/Xv7irq6y7a289Vde6tMr3NDMuOPfMs8dILSgoLVXMs7d3Vy4wO9mvp67Nay8uOF+tp6W01zwuP12wp6ev1zwr
+MkTJt7fNNi8mJjJAwMrFXTc6PGO8pqirr1vvZ9WwqKavtE1CTVW/vMBMPickKyw+R05BOzcrO0jfuLi5xsZrx7SvqrCwxtffU83Ew9PbSztVTOvR2+9OSjM/RkX3T0dBTUFdvsO4yHdHRFtvtra6y19nR+vItqyzwUtEN03ZwK27/z0rJy1AXbnGUTgqKSpP
+uKeisbtIPkzNraiirb9CNT5Xta2ruVE7JiovPd/dUzUvJyw0V7W2tNNPR0XKsqOjq7FVY1nNtqyux+8vMjQ9zb6461U3Ljg5V9/NSUc7LUFEz7u4uff/OkHjybnDwV3Z9++/ure+vVnvyeO+vcRjWz0yS0j/50E4LTErRd/ZuczZZ1vfuKmmqLPGa1Xjv6yn
+rcJVOy09PeO+b0osJR8kNE23vdtDMS8127eqorC8W1VPva+oprjXPjU3W8S8s91rOy44QL67vs46MSkwPsC2u71DPjw/17mvuLtDPD5Jwq6psLhnOEZFx7uwtdt3Lzg5PdPO30Q9Ki02O9nMxd3f71vMu62wsLnMyP/FvLe5x8xGY2tryl1GNzgrMkA/WVNH
+Ojo5RMC9trjH1ffvy6ypq67E2UFXTs6wucVZOyw1RHe1uMNXNSorOUq6tM9bNS8vU8uvqrzfODY0X72yqbO/7zpI47uvrLfRWzI4Pdm9ur5ENSkrMEXLyMVLPTg3Vb6rqqqz09Ndzrqtq6+4/0xESOvDuc3jOCwtKjVEa1NIQC43OkrRvbvOyevrxcKzs6++
+yMPdxsrHx9NrPltZZ83T3UlPPD/3W99vTzs6NzrPxr23z+dHTD/3uLixvdNLVf/nr66vu3c/ND9IwLS+zj8zKzE8Z77G/zwzLTRv3bGst75NTlXGs6yossBKPUNfurSuvE86LTA0Z9fX7zQvKS4707m6ulVDPTzjw6ioq693X0Zrvq6qsbpCNzg2d8W3vsxB
+Lzc1T9nCyk9DMDM5Rt3EuevnT0JvWcjGv8df1WPNu7e3vb1P5+Njw8XIa1M1O0hB42tZRjkuLEpRybvJzPfvTL+urqmvuMrn/8uzsrK+3Vk3PkD/zv9INC0nLzNDytvnQToxOu/Pr7C6x1VZV7yzraq9yFM/R9G8u7HbTjowOlfEu7nfQDErNT3RvLm9UUE0
+O2+8rrOzXTw8PNu5rKuwxUdIQGPBuLjJdzc4OD5Xz81RVzU0Oz1j2cPRb1tI18y3tLi519X/zry7u7/TRFFbY8vP0VtBLC06P29v71dFOjNd58q5w8t3X0nHtrSutcLZWVnMvLe6yOdKPD5Byr2/ykk4MTc8/8TO6z82MDpvxrS1v188Nzznuq+uu8xVRF2+
+t66uxfc1MDdEzsK82z8sKi8928i/0UU9ND7ZtayqrctrV+u+sKurst9NP0jdx7q+z1kuMC0yRndZRUEvODxE38fC21s/Q+PPubS1vMzvZ7+8ube/50lVT/fR18vrTjo8S13b7/dVQzQ4Z/+/usbRb01Az8W/uMLMWU9Zz7OysbrTSz0+RcjBwM5INzE1Ot+/
+yddAOC45U8W2t7zjSz5RxrisrrrEST1K37yysL73ODM1SNnKyFU/Lis2TNPLv8NVPjQ9b8K5trTXZ2Nvybuvr7fDV09HX8i7xc3vPD8/Rd/V11lMNjQ7Q3fT08znTElnb9PGy83Xd2PJxMG5uL7J2Wtv91dvd2NKQkJBSEJIZ1VGP0VFUWfvz8DGzcbFwby7
+uLW5vL/FzcrOzL/I1+9nRUBCPkJGQz5CPjpASlFb//9jUUhZZ+vMv7q7wNPZxsrHvrzF2eNXVVVV993Zd1dMREM/TP/va1dPT2Nn58W+xd3j72/j38/DvsXGytHNysTGzt1rVURGWWfj/2dKSkA9SE9ra+9VTk1N69HPxcXGz9130cPJwsDA3+//b9vO1d93
+Uz47ODznwMZbRjQ2OmO9u8rnUzc5WcO1rbG6zE9L0cC2sLi+Yz06V93Kv83rRzQzOF3v0+dZPTg/Tc29u7/vST9L38C5ub3Ob1F3wLqyu9NTPDY6U9vIy1FGMTQ9SO/T02dbNj1HZ7u7t7XDyl3JvrewvLvT9+Pju7/ByFVAMTtf587vQjkuMDdXw7y+UT42
+Kz9Vy662u+NMPj69r6ututk1PENZuba4zEA1LTlTzLu87z8tNDNXwLixv8pMNUxXw7C5uNVdQ13KvLC3vchBSE9XzcrJ2U5EOknf69FTVz0yR0XnzN/dTlFGa8G8ubzTW0VM5760sb7bXz84S/fBt9tZOy42OVu+wL5ZSTAxP1nFusfGXVFH9721r7HAxW9b
+68q+vsfdU0hDXdvdzElJPjRDSufG599ORkBI18O4vNX3RD5OzbKws71vQTlD28S2wv8/NTA0XcnIuGdNMzE2Ot+/xr/vST9j076vsrm9X2//zrS4ur3VY0zf48y+b2M7Njs+493fb0BBMj1jZ8rTXUU7PEPXtrWxv8xfQ//Ovq7Cw29FNzhdz72472cyNTk8
+28jKzFtNOk/Vzr28xtFOTVPTvcC8z9XdTsnAwLnO0UhBTkvOvb++WU41OFFdv81vRjQxMT/LvbS/2082PVvGsa6xyHdDOWfKt6u8wVs3MzJJ0cC390svMzpFzcLNxz84NDvvy7yzvcJra93Js7a6xm9fTtnPy7j/Z0o5QkFjzd1vQzsxPl/XurTO2UhISlu4
+uLi441M8SN/GtLbIbz85M0fn1b/P/0g4Qz9Xxt/XXT81Plvrx73Mx1lf2+O7uL2909lf47i8tbfNX0NPW9u8ytlBOjExQmfbvfdROTAzPWO8vcX/YzhFd8qvrre9Vz44SsW7srfTUTU6PmPFyMdMPzYxQ2/LuMjIXUFMZ8m5ub7T/z9O48aztr3BU0dIW8a9
+tsH/Rz1Ea9G5vMtJNC4uPmfbvN1dPjI4SNe3urfMYz1JY9Gzrbe3a09GVcfBubv/RDU4OknVb+tbPjw3SlVv0Vt3TE3X0bu4vcDV3XfPu723xdlvRlv30bzP50s6LzlN78zD72c6PD1jt7S2ueNKOUt3xrW3xOtGQT9vycm95048OklnzrzP204/OD5Nb9Hf
+a009R0zvvLu+zfdLTXe/t7C2we9ZX+u8r7O3zko4O05nz8TdTDo0MD9j18bXZz4yPEDnt7q6xWtKS2PNvrbAxk1JUVnNwsXHUUI5PExd2e9vTDxCR2/KvrzV3V1J/9PBusfPX1Vn38O4uL7PYz5M/864t73GTT07S/fv2VVGODI1PP/M0c5XRj1Da8u9t8bG
+/1/nxrq4s7nL60hf587B92NCNTk6Tevj40Q+MzVFWcK8xNFbTk/Zt7WwuMpXRU73xLS3ve9NNjdN98++32s7NzU73b29uMX/P1Fd27i4wsxIQj9bzsO8v+dGOkZJ48bFxdljTUTr18/XV006OT9H78vPZ01FQE/dx7u/w99Pa9/DsLO1u8NrWdPBvLfB00Q7
+Njxb5+PdSjsvND5M78zZ601BP2vNvbW6ye9HWV/KvL2/ylVEPk3rycf/WTo1PEL/x8jZW0tAS9PHvLjE/0lAR92+tLW+yFtDVefDtre7yV1CQuO9u7i/ZzUxMjZI3+/vSjkyNT9f0cDO2VdJU+fHuba3vcfv08K7t7vC00lFQ0vf1+NRQDUxOEVV193/RDw8
+Quu8urW6yF1O98/AtLm+z19FWf/TxMHZazs9P0vj0c/RWUxGWdnHvMDN40dFT9/Mx8bdW0tFXdXFxNXjW0VJWca7t7/jXz09TVnV21VFOjQ4P//jz9XjRT5Da9G6uba9y+fdx7q3rbO81WtOW9nNz8pdSTk7PENRV05dRTw+Q1Pn08vV0f/n18nEyszN729v
+18/T3+tvSkVMTWtvX1NLSUZT3efT19frW2drd9Xbz8nK7+fXys7BysfDytPV0c/RxMPAydHdU01JQEU/Pz9BRD89PURTU11jb+/349XdyMa5tbq9wcLFz8DCyMnnb1lTQ0NVTU5KQTw6PUVHXVljb+Pnb9fCyb/CxMrR1+vRydHL1dXZb3dXWV1j519bU1lv
+b93X48r/d+Pj/2v/b2ffX2fv59vr1+9n3d/b19vR2dHK2cjOz9//d01FPjtBQEpLSEZCR0tMWVnvydHV187Hwru6uLW7vsHGxc/GzNfT711TTVNMT0Y9REVGQ0RMSllnd9/by9HRyNfOz9vN29PT29n/3et3d1FZU01MSllRTf9dW11j/2vr//fdZ19n2dHd
+yMvNxdPMzcS8x8PD0cvNx8fFvs3ZW0hHQkRCPkI/Pjs4RkZPT09ZV29r2cfNx769vcC6vL69v8HE0e9r42dTU1NMQ0VAPD5CREVHSUdbZ2/r08nGyMzLycnTz9HLxtHVzNn36+dvX///W2djXfd3a2vb0e/39+dnX1tVa/djZ+/3Z+/V2dnLy8/Z39XPyc/V
+18mzuetfPzM2O0rX50hBMS42PPfDz9l3PztH1butrbW8zv/fw6+ur7rTU0BHW8q9yvdMOC82RWfXa01FNzRDW8O0ucHfTEdTyLe1uMf/PTtT57m2xGM+MC8668G3yVM2LTI967qtusx3NDVIY7+wvcjXR073u6+ts85vOzpfuaurrsFDMS40Uc3O51U4MTA1
+SGdjX0Q6Ok/VvK61zNFDQdfArqeossZVO0V3yb3Rd0w6NDpN1dXvPzctLDVdtrK7yE8+PEHRvK+0vsxTT2PRu73M01E6R3fZvLfdZ0gzOkvZu7O621c5Ok5fy8X/VUc+ScyzsLXOTz86P8ivqaq1008+OUTjwchvRjIyODhOyFdDOiwtNkrGr67Cz0A6Sve1
+qaissMN348nEu8FnW00+U8nBwvc5MCsqM0fHurzHUT5DROu8uL7Nd0vnyr6vts9ONS00RsmxrsNbNSssNkfTusDM2UdBb+PNyW9TRT9Vw7Kur73RTEBHb7yurrK4329vY8/F21NFOThHX93PUTcwKis4Ucq1utlfPzZH276vr7e7w9nIuLK1w+tHODhGzbS2
+v1cxKSksO+fEws1TPDxFU8m+ydNfTnfEuLGxw/9INT1b2bSuvs9bOTY9U9XF1fdZUVPZvrzHVzw0MjnrtK6wv103Mzpbuq6stb7dTWvRw7e4z2c6OEdV073dSDImJjA6a7u7zlc7NjhG0bu7vr/DxLqvrK28/0tGS+e2ra69XzssKzQ/3cPPd0g3PE33ysrj
+d0Y8d8O1q628WzgtMkPMsay02z80MDZBX9l3U0tFSd+/vMVvPzg3P+e6ra+8y0U5Pk/CsrC0ut/318W8t77TVz08X8C6t79KLyYlKzrZvrrVSzoyN0Xvv77P52/rybmsqrS/XT0+T8qvrbDBTjEtLzxv1d3vRjg5P1Pv32dNPzxHzravrrzjRTY9Wc61rrrI
+40FFSWfKzfdTPj5P17m0xO9AMi41TLyvsLz/OjMzQ9O+t7jA22vZwry3ucjdTUnnv7ivu2c9LCovM0XZ3VtJODY5QUVRr2tOUWfOtKqqtMlbR0fjta2rsL5NPDc/d8/L31tENTtT783MX0k3M0X3va+ss81DNjtN37qyuM1bRkRKXdvjT0E5OkNfxrvEZz04
+NTtrxLa5w89JPkdXzb3Bxs9r68+5tLS+yls+Tdu4rq2xy0AtLTI+d9HRWz01NTtEY8zrTEM8RefDsa62wv9ISvfDr62yuddGPUJrz9fbXz07P1Hn2WdLPi8vO++/trXCZ0A6R2/RubXA011K99XKu8FbRzw+U9u7t7/fPzQyOFe+tLjHWTs2OUnOxsbKX0hP
+4762tLvHW0RNyLavrLHDbzk5QEVn029LOzE1O0hV/1k+Nzg9VcO0tr7bR0JL57asrLC961FNZ8i9vMjvSTlBXdnAy2dMNC44Rte4srnMRzlCVdu7t73MXUhR48/Exe9DODc+Z8S/x1U7NTM7W8y8vMjnRj5FWcm/zttfQ03bwba1vcdXPkrnvq6qrbjfPDxM
+/87BxWdANzQ7RVfdY0M5NTpXzL23xGdJP0Vnx7SytbrRX13/yL3Dze9KQUvrxsXTW0AvLTdO0b28y107NjtZ0b+4xedPSG/LxLq4zl9APUZvv7i3v1E2MTM5yLy7wttPQUJJ48zR31c9QVPXvLjB1WNIQWvAuK6vtL1nS2Pjyb6/0088OD9TZ19VQzMuMjlX
+w7m81UY1Nj9vu7SzutVnW+fKvba/yutDRm/Nu7rMVT80NkNdxrzGz2M/Q1Hr0cLH23dPXdHCwsfPXT8+Rl/XzcDKUT82NjxT483Ob1FHUf/Xw89vXUVDT+u+uL7Haz89R+fCtrO1u9Hn58zJycLJ71NPW3f342NEOTc3PVF31cnTa0pIRkrn1c3Gys7Oy8XC
+ytXbb11308bBydfrSTw9TFNZX2v3WU9XX11VXVtZW1fXw8fNy87jW1vv38/Ry8XN519VTUpMUVVfd9vX5+fnXVFbS09r79/V2dvZ71dfb3ff2ce+vr+6vcXR1+vn793OyONfWU0+OTk/RUdTW2dfY2tbV1tXY/f33cjEycjJ09nva+fVzsvKzMzbY1tMTFtn
+Z+vb4+ff92vv//9vb+/b08/TzuNvW1dfa2v31+93Z1tTT0xJSktKV//f3dvn43dXX11d69HT09tvb/9fXffr39PTx8G/wcHCx9Pn/+fX2dfN62NbRj49PkVVY11v929vZ1tvb1lf49XOys/LxtHf3+Pv19Xdzs/d29t3V01JS01NW9v/d+drVVNKT2vn79fN
+2d3f7+fj/+fb3+fZ29nX3W9jTEVKT1fv3ffj929jZ11nZ2/r5/fv29vj42tn92/jzsHCv7y/xsvO0dPV19njb2NdX1VGP0FCQkVNWXfZ4+tnTExVX+fTzc3V/2t338O/yNv/UU9ryL68zWNJP0RZ18bDy9tVTFPbxr3G3WdCQ093xb7P71FAPUVdzcDF2Vc+
+Oj1N387L71dIP0vVyL/C2VlNT0PVzNXJX29MTNt3wczOy1vT19O5urfF/1k6TePOtrzPPjY1LkfbwLfP20E6Pz/NwL6+XW89Pm/3uri/wEdRP0zAvbe711kyPT//vsnD2Uw/NU5Zyr/bzEtCW0/NysfMVV8/S9PjwcfT20pXS1HN681rTU0+a+vOv3fnSENL
+Ssu9vsBXXTk8TPe5uL7OV2NF58S7srfAyUdLRv/Au73XXTsvPkBnys7VZ0k+PE9rzcbR61NNY1vZ2d3vX19d58nNy9P/WU933cvAzN9fTUxrzcG9wv9rS0lb3ca1wNNNQjM4SdXAt8pbPDo3Q9/FxMFrQDc+Tt3Nw83vU0dI983CwsTXTkdGRHfNzNXfXVf3
+18vCyNvnY+PJwry9xGtTQEFMz8vE2Wc/Pz1V0b7DzF9VP0JXz7660W8/Oz1N07u4vs1RPUNJ47vEw8lJQD1R69XC53dPRFNrycHLyFdRS0X/78nKd/dOUUZr1dXOX09HTPfMwb7V/0M7Pk/Lubm82U86Oj9vvL3I209DPVHRvbS7yVNLS2/EuLa0yu8+QEVX
+3cbO01lEPEdj39HR/2NPW2/Ryc//T0FCSXfXys/jS0RHV+PDw8v3V0VO2726tMTfS0E+58WwtrrRWTk9Qt/Bu89rQDk4QGfNxctdOj5BR9m8vb/dRjg+UdnAu8HJ/1FEWffV0+tnVUpjb9fI129RTVP3yr+9w833SVv/zb7AxetMQkJjx8LB1f8+OUFfybO5
+wl9FNDdC78u4xuNIRD1NY8zFxOtTQVFrz73ByNNLRUNf0cXBy9dXU1Vdzs7R91NOTFvn1ct3Uz49RF3Pv8XGY1M/RWfVyL/MzUtISF/NwMXnZ0M5R0zRubzE70o9PGfIu7S5zFVOTE/Vx8S/32NGTWfvzcjna05LTG/MxcjXb1FHTU1v22ddTENKUf/Rz91R
+TT9I58a6t77OW1NN47y6uMzIV0VPW9O+vsLvUz48Stu/v89rPzU3OUnXzMXTVT5ATFvOwszPa1NPd8zIx85dTUdFT9vHzM5rTENIW9/IvMvOb05n387AxcTvW01X18K9wM3vQkFDb8LAw8b/ST5JX9/Gxt1KPz4+XdXIv91XPT1HSdO7xMHZUT9O68+9usbM
+W01NX8rOyspdV0ZHZ9/IzedMPz5EVePfze9rSkn339HKzttTT1V3x72+0fdNPEVT/77Cy2tLPjxP08u8wc9VTlVfzL7Fv+NXQ0xvzsG7x8tjSkJd1c7ByNn/Sk9X/8zr3Vs/QkZNZ9/RX0w9PElrzcvJyGNMTV/Jv7i6wNFvW/fPu7y93WNEPkpd37vP2Uw9
+NDdHd9/I6088RUVryMXMxmtTRmvXwr290f9NTUvf08/O/05GQU5T3c/fd1dKb+PKx8Tf90lNX8rGvsPBd1VLWee8v73Pd0RJUd/BusXjS0E5RGPXycXrRDs7P2PVw8TbXUVBT1vPvcbF2Wdjd8zExcLvd0xKb9vO23dvQERJV+vn319HPjxR78vEzNVRRktj
+wcG8wdPrS0Vn977Aw9tNPj9D/+u/yd9NSENO2cHBvNVbQkdT3by7ychfT0hjzsq9vczPSFNr38LKxcpPS0FVX2/jXVFAOkVHb+fT2VVKR03OzcHKy3dbVd3Ds7e2x+NTSFvRyrfE20M9Nj5d69HJY0s3OTpF68/J2UE9PkNExri6u8Lb29vEvby6xM93T1v3
+4+t3Z0VFSFf359l3WU1FTuvXyszRa0hGV9vEwcPL209MWeu/vb/Ld0ZGUWvZv8fvSj05PU3b18jfWUU+P1njxc3K51tLY+PGvLm/xmtfY9vLz8rGb1VFS1Nv//d3T0BJSFNb/+9XTFFX0c7JyszjX1P/17+/w9PnW01Rb+vG1/dRRkNHV9vZydH/V1fdY2Pd
+68jXb1lXb/ffzc3AztfV4+fvzMjMxdXT92NdV+NnTndOTUJDRURRUW/XW11OW19d18/EvcrJ29fTzb+/ur7P219XV0/rb//3Skc9PUFMa09bY0xIPlVn38DNxcd3X2Pf08a6wcTRb/9fb2//zF9jVUNNSVNfb+tb/19Xa1/n3dnM3dHfa//vzs/Tyc/Jzd/X
+18zbz8jZ0d3vd2dnV1ldSlFXTExKTVFfWU1jZ2N3b9/b1dHRyM7XzMrHxsrK1dvn/+N3Y11dXU5PTklMS0tJS0xGT11bXf/X1c/P49Pb49nOyMvLzNPX42NvXf937/9jW1FPWVNjd9vna3djV19r59XO2+//d2Pj08XJxcfT19ffzszFys3V629ZV1tOUUhF
+R0FCQ0RRSldPUVdv39ndzdHOy8XDw72/xL/Ex83NytnX91VTSUpFU1dKR0c+OztT5+NrUUpJSuvCub3K12td58W6t7bE11lHWe/Rw9f/Qzw6PVfV2d1bRTxBS2/GwMrbWUdIU82/urzRb05Xb8a5tbrLd1NK59W/usfnRTg7PUzbzutNPjc4Pk3OxMfdTktL
+V8i6tr7E42vny8K7v8TvX0tJZ9/Z21VEOjk/Sm/d/0g+OzxVzbq5xndTOv/bua21t29JOjvXxq+zwlU0NDJT37m10Us0MzJPzL2122M7OD5Zvbmw0f9MM05dua6srd9vOD/jvK2xsV0+Ly07U77CzEYwMCo6Ssq6w908QjxTzbustrXbWW9Ovrasr8DRP1E+
+T9nJy11LMjQ5OElM50tXSTpMSevfxMfXw13M0c7Hy7/NvsbRxWPjVetv19dbaz5APkJV68Vv20s6QjZMTM+81b9HTz1E0cGutLrZSV1DxLusrbnNPT86Y8e7s99nNy4uOWfVuOtRQCs4Ndm6ra/v9zU6XcSuraq/0z81S+u0srTRPjwqNjz3vcHdNjQrL0Bn
+tLu3/z5ANWvKr623vD9XP0zGsq24vj88STvZz7jJ0WMxQDRCa8rD79E4Qkc71dm80c9rPe9IzMm8u8u+487Pz7/Iu+PH0Vt3RVk/U0w/3zpKPjw8NV9CxMXdy0z/RtnOuK64td3f/++8wa66vtU9PTNKU8jKUVkuMiwyW+u613c9Lzs5wL+vsMT3PUJJvrCu
+qr/NQjhDS7m2r79LOCs3N9HIvcE+PjEyP+e5wrhNSEU0Tm+5u7jIUWtAb9e5sLW032NTR9XNt8jKXztENktMb91KWTU9P0X348Nb0/9P2d2/wbXKy7/vxcu/xMbGTtdNU11NV0BTN0trPltCTj9GS03M2cfbd2tL2WO8uLq73+dHZ9/Hsbu/d04+PF9jv8LV
+SDg4L19VyLvRYzs4M1fNwrDGzU08PT+9u6yuwdlBTVG9trSw2003MjlPy93ATTk0LDY808q9z0tHOE1dvLGzstPnV1/FuK2ztddNU0Fn68TTZ0IzODQ/S2fvSUxNQkpN49W/99//RufryMi5vd3DX+fRzcnRyE/P609jU2NJYztd401rS1NDT0hDydXO019V
+SutLwr3Dvtv/Su/RvrCyuM/vRkXrY76+0U08Ni5HQ9/E/2s6NjE93cyyusZdR0lOvLasrL3ZS0pLxby2sd1LNzEwQv/nv11AMy0yNdvHubtdTTpBTMC2t7DP411M78a2urfXTVVDY+vDx81bOj46Rk3Zy2d3PD5DQHfnvtfd/0dnUdXOv73OwN/ZzMm+vrjX
+wc9ba0xfSnc/RF08Rj49OkRKPNV3/9lId13L57SvubXEyPfOy8mvtr3ZVz87VUnM1V1RNDArND5Xx9v3QDg2O9/JsrK9201OTMG6r6q5yldDQOvEvrLMd0E2Nznj18DJSz4vNTr3z8++Tkg9PE3dvsi641NVR93FtrK0xF3rY8/DubrM0Ts6PDtP/8dfSjgv
+OjhP58XN73dCWf/LvLWzxb/f/8rGvLy439v/SVlNWU1bRDpKOD9GQUVGVTrd1/fN79lj0WPft8G7x8vj4+d3u8LAw2NMRVlOyMTV3UQ8Mz5Ga8Xb10U7OTpbWby9yudHQ0PRy7etvL7vX1Pvu7ivuNdHOTk9a1/T3UU7LzE0Xf/vxU9MQkVjwra2sMPT2f/I
+vrGxsbljY0pT58HK4281MTMzPE/nTUs3Lzw5V+u/w8/rS2/bx720tMK91ePMz8jJwN1rb05nW19r309FZ0dNTUtPV189UW9R42trVf9KV8nZyc7R39XP2be7u7nG3XfjVcrG7+tKQTY+PT1vS1dEPDo+U1PCxsfE92fvyce3srm7y9vrz8PIvcXZTD8+PUk+
+TE5DRzg5O01OT81n73ddd+PK27y9zMjTy8vDzse749fjb2/b12PJX01ZSUZFY0x3W0VVRUlHV2930Wfv91tv3cjOvrzHvszVzcO9w7zH02dHQkdRRFdPRUM6ODlIP07bW+9jX2PRyM63u73AxsjJvsO4tr/C2+trZ09Lb0lIPDk4OD44RkdFTUZOTln338rR
+ycjMysvDx76+v73FztXZ72vj59vva19jVUdRSlVVT1tTU0lPU0tfVV3j72dX/1/r39HGvr7IxMfLwcO/vb7T/1NGS0ZOVVdPPD85OUFFVXfrd1lbVXfbyL27vcfM08/Cwb62uMfO/1VjWV1jb0Q6OzU6PERTV11GSUpJZ+vMx8jT49vrzcTBvb2+zdXX3c/V
+ztXZa09RS1VVY19VTUNJQ0hXXWdrd0hMV1H349PLysrn1dHNw766vb7L1ePr3f93Z11FPT48QENJU01TRklGT2Pvx8bGyNHV3cfCvbm6usXGzM7KysTLz19MQT09PEJAREM8PTk/QU3/d+Nj7+//09PHwcLHy8XKwcXFvsbXd/dd7/f32fdnR0hDQVVT/3dZ
+TUNNRVljZ9n/b1dVVWvOyr/BxcfMycu/v7/AwL/Xa2dBOj5NX19AOzcyO0Bbzs/ZUUxDTtvAtLW703dZZ8e3rK63b1VKPNnHvrjnQy0rKzJP58PjSD4tNjpVvLi2xFNJPU/Rua6vtstd613Lv8K42edJPU5K3dffZz9HOENTU89n/1lGW0tb09fO3f/nd8rH
+wb7Nxt/Kv825x85nQUo6TFtd70M/NTM+P+vL1eNJTEdMzMC4s8PJX2fNybe0srvP00jjyMi8ytdHOzkwPEVRWUE9NDc/Sffb1+9TWV9vx8LIvdHTa+PDv7a0v91TWUjfyMa+2V9ANjs+Y8vJ00s9NjY+/8a6xONAPkE+3by5scDMXVHZxrixtrzZd0BITl3j
+a1tEOTs4PEhOWVVKS0xd99fK18vb08nIu729vt/O18e9vLq/00xBSEhbZ2NXPDg1MjpIXcrva0hAS0r3yL66x8hZ49PXvLm5us3fT2/Z283Ob1lISUdTW09fR0BEPV3v689nY0lJb/fFvsLLZ29N18S/srG7zk1OS//FxsfdRTovMjpMz8/3RDs8Mkfnzra/
+yPdX///Ct7Owu8Rfb9fLu7i6v/9fTUZfWV9bQjwyODo7S0hZTEVNR+fd2cV30e/vysu+u7vC2dP31crDwMbnZ0JGSl/O2d9fPj41O0ZryNfbS0NMP3fby73O0V9b29XDu7u4vszbzcbJxM1nY0JFSEZRRUxAOD04RV9f31dZQ1H/37y3urzb713bxLu1s77P
+T1VX2b68wutFPC42OkXj72NGODk1RWfKuMDXT0tfZ8S8uLXDzuNrz8m9u8LRTk5PS+ff22dJSTlCRURfT/dnQU1IY87Cv8/fTVFv2cW4vL7jb05dyry6t8r/Ojw8Ru/j61k8PC85PUXb09P3Rk9H78u9tLa/zNnM2cG9u7rEzM/dz9vKzl93Q0VKP0k9QTo4
+QDtNZ1XfUVtLTffNwbnCyV9vV928tra2yNFIU1Nrv8LI0Uo/NkBKX8/fX0A4PTlV38+/02tORGNvxLy9vdvf7+vEvri1wsZna1tV0d/fXz48NDpKTu9VTjs5PkJnx8O+zdtPV8u/uLO5v/9vW/e+u7u4zd89RkRJ7/93Wzs5MTs+TNvjb1NJU0znzca+y9Xr
+a87OvbvCv+Pn2+vK08vZW11GTllV/05VRDtAQ0/r69Nrd05P39PHvsjIb2dZa8TCv7zO2Vdv/9nIzttnQT82QkdM/1tNQTo/QOPT08Pnb2P30de5tb2+3+/n2726trnO60pNd+vIy+dEODcxPFVX91lJNzdAS9PBxcP/Z0pP0cO+tLu9b29nd8a+vbzTYz9H
+T1fRzedZPTo1P0pj1+ddTUNVTtnNx77V3+NnzMrBvMPC0dvLzsHGy89PT0VCU09fSEo/Nz1BSWtf91VTSUR3ysC6vL3Z5+/jwby9t8nNY2vn48nH0dFMQTg/R0v3X0tCOT48TNnZzdtXU0dIXbirvb/P9+vfwLy+vNvrUUlv79XP31M9PjpBV1/db1dEOkhV
+68HEws5vVUjnyMC6u8ZXTl9XyL69vNlbQT5ISGPdd19CPTc8S07r3V9fRFVrd8XHvr7PytPKv8G7w8bXd87NxcfOzlNRS0VfU11jTEM1O0NGX/frX0tPP13f2cLHydNn52/Ovr68w85nXefjz8rV401IQ0BdU1trSEY8PUhj2d3T51NbWdfFwbvBxdNj3c/C
+uru6yv9XTmfn09N3WTo6PD1PW2/3RkA3PE93z8bI2Wd3Wc6/vLi8yd9b993Gur+/01tGS19j499fVzw6PEFfW3fnTEc/S2fbyMnJ3V9r98rDv7nBz29b49XKxszZU0tHSltf73dTSjpBSEvn29PdWU9Hb87JwsfL3e/v98nHysfN1/9Z2f/n2f9nR0NDSl1M
+VV9LRj5GTlnd/+fnXf/r08PAvMLG19vOzMHAwL7X5/9d3+fd42NOQUREQ05RVU5JSj1MVVfn/9/vb/f308vNx8fF19HO2c/Lzc/v/1tjY1VZWVVMREZDSk1JXV9nXVdja+PXz8rMysrIzNPFx8bGx8zPz9v35+9vXVlTS0xLREdLS0pKSUdIU1dfb+vd09nb
+0czKx8nIyM3Tz8/RysvP2evvb2NfVVVdU0lHTkxMTVFRWVtbXff/Z/ff3dnNz9POy83LzM/NztPf3+vn429bV1dNS05OUVlTUVVXW2f39+ff2+vr49vPy83Pz9Xd49vZ0c/X33dfTlNbVV1jW05HRkNITU9fX1lTVVlV3c/PycrM0dPRz8a/wsHGy9HR0ePP
+093rX1VNS01MZ19TT0pKR0lPVe9nZ/93b2vr09PLzM3L19vb1dHXztfd3W9dWVtTRlNOS0xISkhPXWff5/f3d/fv18zJw8nMyM7PzsnGysnP2dv/a2NvW1VbU01LSUlHS0tIW1NPWWdnWWvv79XRzsrP19/V0dXMyMjFztPb43df9/frb11VTVFLSVldX2NV
+T0xVW2/j6+vd3+d359HMyMfP12dV/8rCztdrXVtj3+dVS0E4QFnfxb7M60w5PUlnxrq5y99VTGfTyb/A32dvZ+/Fu8DdRDk4OkbrwLnJWT40NDdE28TK01lIUVfrwr3G1VdOZ9HBta+4zWNBSffPv7q/b01BO0dn69v/SURCQ1vbxcp3STs8QFPItbS5zE5G
+SU7RurrB0UxKVVvdwMhZQDQxPUrjwLzfUUQ2PVXjv7jEztVvZ9/LwMTV7+vrzb24vMXdQzs7PF/Hyc7XRzo8PkVr91tjRT5T283Bvt1PPTZD2761rbfPZz8/Tf/Ct77O71NKWWfn510+Pz9Dd769wM9KOTY5QtG7vLvGZ2t399fN2+v/U//Fvr293Uc8MzdM
+78S8xOdNPjxDV9nCwvdbWVfrxsPI1U9ASU/ZuK+xvm8+Ojo+/8m/w9lPQkVFS2dfVU9CRF/dysLKa008PVf/w7K2vMr3U13n08G+zufj69/KydNdPzg4PVvGu7rHVzo2OD13ycDDze9VX+PRxcjna0ZK682+tr7VSzYyOklnw73RVUM8PUljz8jd/+vv48vA
+xM5TRktX3bixsLnOVT9ASnfHwsbTVUZMUWPfZ09IOzpX18W6v99FNjI7Vcm3s7vZV0pKU//Nv8vf493bx8HG30U7OTtI67y4wONMOzc6SdvNycj3VW/f3czV62tLSFnEube0vl87MzpJ3cO4vetJPzxFT//Iz2dZXXfTxbzFZ0U7OkFdvrGxts5NPzxA68XC
+wtFZS05d//9dTD85PVPRwr/G/z82NUVn07q2xuNTTF/rz8HD0f9348y8t7zCVzo4OD/Zu7a70Ug5ODlM0cfEzWNRV1vXzs/nW0dFX8u+t7XG7z82PERdyrzF41E/QURHWf9bTFNj3cO6u7/jRj07Rtu6s7K5119MRW/Tx8HF41lPY//b1WtRQTU7U+fFu8DZ
+SDY3PEnrvrnE3VtOU1v/z8znY1/30765u8FjRT89R93GvsLXWUQ/QlXb69vfV0tv38/I2fdbPjtF2762srrZQzg9TG/DvLzTWUxIUVdv1W9JR0lV1cC6vNVVQD0/S8+9u7zOW05IT9vOzMnTX2/dzcrR71NANzlN3cjBwd9FNzZAUWPKwNV3V0pv5+PMzV9N
+T2vPvrW0vNdKQz9I3b27vMdXRkI+WdHP1ddbS1P/z8zZ611FPUfnyb+6vtlRPEBVXdvBxdtdTU5ZWWPvW0M/RE33xL27yFFBQkZfw7i4vddRS0ZZysDAw9drV2/XxL/P4109O0df0cC+yf8+OD1HW9HGzfdTTFlnZ93XXUlLTmvJv76+20xGR1PRwbzAz19L
+SEhX18/X31FHWW/Vw8n3WUA4Q13Ovrm8zk88Pkrvy769zWdPTv/v48/fTUZHSG/Rx8LOa0tDRV/TxMHD011RSEv319HNz2v/283ExMzfUT8/S93LwsLVTDw4Pktd08PbVUtEVevfzctjSkZJXc69u7vLU0JBRuu/ury+901HQ1fn2dPM/1Nb2cvJztdrQT1E
+683HvsPdVz9FX/ffyMfdW01X99vf2/dCOT5DW8rDxs9OPDw/Ucq6ur3TV0xKW9nNz87ZZ1fjyL+9wc/vSD5K983CvL/fRj5DTl3vzM1rTk5d29XV2WdAPkRV58bAwetHQT5K/8zBwsrfX1lf48vR91tOQ07bw7q9x9tFNztH787AwMlbPj9O993TxN9TS1XZ
+ysfBxmNIPENX28y/v+9RRURJXdfOzddfVU7nz83ba1dGT2PPvbi7xOtHQEZj2cfByeNOR0tn6+vbd0pBPk3by8bJ51U+Oj9O3c3DwdNZTU1T99HLyN1vZ+vRyMfL41NFSFvTw8C9w9tfSU1da+PR0/dVXW/v3+vrZ05ITmfZz8zO60tCP0NVa+/j3WdXV2N3
+///f92dr78/GyM7T905GU3fZz8rDw91r/3ddWWPd1/fnzszv7+//Y1tZb93r59nP62tjV1NNTFdn9//n299rW11bWWPr28/N1d/dd1Vba3ff3dPP0ePd1etdU1VbV1Nd29/v4+drW1ddb+/3383T3d/3b1dXXWdrb/fj3dv/Z/djXWvn4+Pd393Z/2NfY2Nr
+7+PZ29PMz9vvY11bXV/v1dnZ1etdVU5VXWN3593nd2Nrd11VY29fXWNn693v6+trV1tv/+vb1czX//9nY2t32c7M087K19/f93dnZ2d3b2v/4/drY2Pr92f/7/f//2tva1NRVVtVX3dv7+fr92tbXf/d1dnZ19fvd+d3Z2/359vd08Xv20xVTUPRyMG+/9dH
+SVFOyMXJvf/XSFH/XdvIzNFNSjk/SErP0dHLU04+Rmdjz8fPzll3R2PbRNPn68lZzOfdxtnI2/fnTetR/9f/zf/n3T5rS1fT78HF18tPd0pOZ0/P09/RSW9KT+9TzNvjyVHjVWPV78PRa2c9TT9N/8yutdX/Mjo5Rrmvt8dnOi49S824ucTrPT07d8e/udFd
+PzhMW867x9tXPjpDd92/v8tHOUnr27jL02c/PD/fr7Ouw+c7NUB3uKq1vEI5LTFXzbyrzF80MTVLx7KyvFU7KTQ/Z7K0wMQ+My893ciuuc1XPUtNzLnIxlM+PD7Vxr2x71k9O1FjwLa/40U9N1m+xLDIbz4jNLusp66vzUUyLj1rwau7zDw0MTFrysmw/1c5
+M0VXx7TJxj48MEbKzbG7zdM4Skl3uMG9zk9APXf3xsBbUzs/WWu1uLnERT0wQM3GrrvOTTI1ON+6vbVfWT4xT+e9qr3GQzguO3fCr6/GzDM6P0i+u7m7V0I0Q2/dtsXdSzpEQdXBxLpnY0k632PMvWtvQT5FWcTKwFc+PzRN08ewwc1RNjU877a0rsTVPDdA
+Xbmss7VnRDY5Z927r9FjNTI1RdG+sb9vSi9GQ1+5yMTjPj08Xd3HwGtdOkNvd7i6xMtHPzpOysW30fdBOExbxbfAxEg+N0LCw7i13VM2NDvbt7GvyWM/MErdyqq950M2MjRXzLy061EuOURVvLi8v0k9NEL3zbrAyt04SVHRvsPFXVNDRcbEvbrvWTY4QFe+
+u7jZUT00TtXIsb7HSzczOee4ubTXTS4vPF25rba5TkAzPWfXt7LNZzc8PnfDubfKX0U1T2vHtcbOVzg9O2vFvLnvXTw6Xe+6uczjPz4/Tr/BvNlGMi06Vc21ur5NOzA1/8W9rsnVPTY+Sb+tsbLjSzI8a8quqbzTODYyQdPBuLlnQDE9SNW8wsRjPz4599PF
+vndfOzhMTcW6wMlRRDxVyMO0w99FND9O57a6ue9DLi5TzLuuuso8NzQ7y7q4s9f3Nj5KXbuwvM5IPzlj08m3yXdEMT5B773Dwv9CODVZ27663e86OEpVwLy7yVNIOUfJwLK0wk87PEBvwb230Wc6NEpr07a9wVk9NDn/wb231VcyNTtJw7e8vk8/N0n3yrm2
+ym88QUrbw76+11FAOFvnw7bHz1M5Pj/jwr2/b044Pl/Xu7vC40JAQVW7ubfBYzYtOU3jvL3BVz4xMU7dybbHzj04PUXRt7i87001TevNtK+7yD9BPl/Iv7m8Z0Y2Q0/Ov8bJWT1APuPNv73rXzs1SlfMvsDXTEA7SdvKt8HLZzpCVeu7vrvnTDMwSNnDt73I
+QTo4PtnBvLXVVzg9SvfAtb3FT0M5a86/tLjLXzU+Pm/CwL/RRjs2TVnIvNPfQDlDSszLwc1OQjk92826u8BdQEBF5768t83jQjdOd824wcJTPTU3b8O7tsn/MTc6Rsi7vb9OPTBBY867uMbrQEdFzL69uc9dQjdja8S5w8dZPD9J28+9xvdROD5b773Cwuc/
+PDxRwbuzuslBNj5M376/vWdJNDVNZ9m8z3c9OjtL3b2/wetIM0tf2bi0vslHQj3nwL23vN9HOk5fwbrDykw8Ozzfyr2701U7OEhVzsK+304+OEnnz7vBxU88Slvdurq311kzNEjvy7m/zj89OT/jw8K+50w3QU7bw7rE20c+Of/NwLe8yv89TVfHvcC+1UU6
+Pl/nwcXbazo3P07Oyr3LTDs0OU3Mvr6+XUE/SOPDvbTH0UU+V+/JuL3GW0Y2Q1/Nwb7Pazk+Pk7LxszOSzszS1/Pv8TTUT1ETsK7u7fJXUM9Y9/Du8PRWz4+TtvKvc3rTTtDUffHxcTZSj4/UePLv7/HUURNZ9W/vcTjVz9CVd/Z0eNPQz4+WefN093/SD5J
+W+POytHvV0pN48rIxr/XXVdd48zBzcbOT01VU+/b087bb1FVZ/9rVe/rV1tbY1lVXffdd2vZ1d3j59/N1d/bz2dVWVdjZ2/f2+9dT1VTUW/Vztv3U19bU1nZzdXf4/9nWWPTx8jTzs7vXffd2c3P2+9fSEhOY2fr3W9dT0lNTVVVa+tnX1trY2/d38zL29PX
+09/f1d/K2dvR429bXedf6+dv91lPS1VbTG/db2tXVVFTX1fR09nT1d3vd9PTy8nX1+drVV3r69fR399dUU9R///f0e//X1NTWeN308vn4/9nb+/T28vM/9vn/2tj3e93W1VbUU9TU+tVV2ddZ1lbW1VnUWvfd9nb3ef/3+fNzdXJz9nvb9v/39PdzttvVWPv
+X+/d9/9TTk1Td2Pj1/dnV1lfX+fjzcjX09vZ/2/Z49/3Y+9nXVVn52tnd2v3Z19ba2NT53dv929vXV9vb9nR2cTJ3WP/2d/Ixs3O22tVV2tj9+fr91tMRk1XT2/3Z1tNTUxTX1/Rz+Pb3evn2c7RyszXztPd///jd9/b39tvX05NWV/d2V1NRkNHW+fbzmtT
+Tkxj58y9v9HdSGNj27u8ue9dPzpVX8DCwNtCRDxMb8W/zd09QD1K48+6x85XREtO08a0wsxnPk9Cd8a5t9nvNkI/QNXKvN3jPDhIOmvny93j2T1XR03rzsPbv//d22PXa8bPx8b3y1tXR1HR3bvN21VBQzpn98i+13c+PjhR78a0xMJNRT1E0cCussJ3Ozo1
+79e4s89vMzo2VcK/ttddNzE6Ssy8tdN3TDFGRcK4sbNnWTM9d8ivsbB3UTcyXd+4trtfOjwuP1nKub/jODguOlvdtLq+S0E9Nd/Er662zztPPVXEtrG/yTs/RD7n27zP11c6SjhGXc/db9U7V0lI/9W/97/d99FfyefI1crG/9VIY11Zd//D69VdSFVF/1HL
+yufVSE89T9/NuMvFZ1NDQMvRtbvN1UJJPdnKxLnn/z06OUzNy7rrZ0ExOjjfzby9TU0vNj/jtrivze86NlPvsrKvu2dPMURTyLS2uWNJMjZIV7i9uu88Ni0/TL60usBATTtC58CvtLdbS009a2e9wMfOPUo6QVXZwt/MRE9NP2tZzW/ja0jrSd9v087dwuPL
+3d3L58lbzcrTy2/RW+dZU81f41VZSURbQNPnW1lDRTpdScq+78xTd0jry8yzxMPVXVdTysq5w9vrPEQ8/8rBuXdbMzQ2PtHRvM9bOTE9PsC8t7PR3TtJT9OytK/BXzg0Pkq8u7i+UTwuO0HJu7q6SkMzN0TXu7u2X0tIOm9rube3u0pXPkn/xrjCwk1NTENr
+d8bV21M+Tj5ZT+t3TFM7S0hb4+/HUd3vb9F3xse+zOvH98/V09Hb30vT3/fZ7+tRdz5r51X3UW9GU0tHyOPN1WNvW9fju7jDxGdVQlFjzL7O20tDOkBrz77L40c7OTrv576/4282OThdxLuvucpFQUNru7exttVCOT5Lwby4vV9CMzk8z8W/vU5EMTI4QMnH
+u8lTRzpLb728uLp3b1Fj3765vr9ZSkhLX9/BytFLP0U9UUzX52tnPExER2fTyOvRd+vT68jEucrN2Wf3UXd31+9r11tvT1VZV99P29dvXUldR/9r/8Pb1V1fVVvV0be7yt9TUUnd27m3zdtFQDhT3cy4zONHODQ6W3fBz2tONDU0W8q6ssPbQD4/77q3rrfK
+SD0/S8LDtbvjWTc7PmfMxb33TDc3PEzKyb3OU0Y8Tuu8uri592tLXevBvcXISUZARV1rwc/dVTtFP1Pvx8z3XzlBR0rnyL3T21NHd//NwLm/091XX/fd1cTHZ+tPU1VM7+/PX1NjRE9DSk9TV0/P62trW29329+7uMfJ3+dZ09nLucnN60s/SGtvv8nraz04
+NUZdz7/d/0A6N0Pvzbq7x/dLR07GwLe2ydFHPj1Vycm7xW9DNztFz83DvF9JOztH183LvWdMQkBTyL2+utdbUUz30bq7vslJSkJKd8a/ydFAPUA/TuvL3V9CNjs/TuPFyd9rR05j29G8uMjL62ff79fKvsvr31FjVV333+9Nd2dVWU1PZ1tFa9339///Z9tf
+977AwcjRZ2/3Z8bDz89vSD5HSv/K1dtnSDxASVnNyttjTDw/V1/GvMbIb0tEY8y+tLrI70Y/SdvOw73jV0I6QHfj18RvTDw3Om/j3cbfX0k/S2/Cw7u62+9VWePBvr2541tVTFPXycnKd0RDQEZv193fXUBAQUZL38/bz2dV///vzLzAycxv5+tf99PT99N3
+V1tFSP//VdnXX2NNR1n3VXfL5+9jS1nf39m+xM/R/2fj19m/vdnvY05PX2fny+tXUUxATUZO61dPTktASVlb39fv2dvvZ9/MzsbHx8jT69nV3d/V7+P3XW9vW09jX19dVVdjWUtVWV/r/+fN2+//2dnJxszDydt3/3dn49nT2WNNS0tJU2vn23dOTUtDTv/n
+393/W1NOXd3OyMHFzvdfW93OzcTD3VdOSlHv99/L50xJQ0NdW1ff/1dKRUZOa+/Jxc/3/2Nv18rGvL/T2+dn69XRxtH3X1tNV2Nr5+9dSEk/P1drd/fvX1NXTvfP0cjFx9Pj/+vP19PI0etfX1Nfd2Pf52NMT05ZX2P/3+tXV1dX93f/ys/X7933483RyMjN
+2dvvd+/f0c/fZ11bTVFZY/dfU0pMP0NLT2drd2tXVU//39fLz83d7+/Px87Gyc//b19r193d099rTU1MV1ld/2dfSUxMWfdv29Hbd2fd3cjLycLN42vr79vV18n3XU5VT093Z+PrV0dJTUhj3+fZX1VJTFd3zczEzNnrX1tn2c7BtLnZTEA8Re/Iu7/fQDk2
+O2vXwsH/STU1OkzJwrjJ7z8+SF2/trW4z09MV//Fu7q990c/R1nVxMjPTz83PExb0cz3WUA/Q2vZxL/N11VT/9XFvb3L/09PW9/Iys3nTkRAVV/b1dtfQkVCWdnPzudjQ0NT68e/vsvvZ1dd18XFvdnfZ0zOzs3N628+SUFEzNHNb0Q5LzhE673HzUc7Ojhf
+xLe0vstHPU5nurCyuW9ZNzxn57q2wc08OjI8/9G703c+Mjs/58O8utlXRT7/z7y1vcFTS1tRyMnCv2trQExdV8332V9ARkJv3dfRWVc/PVlrz8LP0V1NTU/Gwr271+NFSv/Mvb7G70lIO1Xj08rRXTo5Oj9f3c/NX0o9P1/jxr7JzVdZa//Hv7m7zudRa9nb
+x8rVZ05MT3f///dJQD8/Ud3R1W9VPT9Lb7u4u8TrXz9P3cm2usTdSTs+Vc6/uc9vPTY4Qu/CwL53QzU3SWvHvMXVTElEZ8nJurvP51Nr782+xMTdUUlP39nN1V9MPD1HSd3f428/OzpG59O9xtVPQ0ZPyru7t8zfTUdr18a5w9lNRz9O2d3MyGNOPEBGTd3v
+0eNLS0lj59HEzchv49HdwMvMyuN3WePH0cjdW0hDSmPdztvfRT88PPfPzsXjUTw8QGfBu7zG41U8TWfGtLm/2Uc9P1HLvbnB6zw3OkBdysvMWUQ3OUpb18TM705MSu/PyL7Hze9Z48vCvcHL71NOVe/GyM33Sjo9SFvNzdlnPTc0Qt3Fu7/TTD1AU8m6t7fO
+WUI/V9nGtr/PUT48Rl/Txb/3Uzo+RE3OzMrTSkk/Ue/VwsPOb1N378nBx8LTb19Z2dXLxe9nSEZXXdvr71dCPjhR73fN929LP0NKz7/GwtddR1Hrxry2vsdXSklXy8bFv/9OPEBNY8/T3VNAPjpRd+fH2eNPQ1vryLu9xdlrS1PZv7y7xfdFR0pfwsPCzE07
+NT9f18LJ0Ug8ODpb0ce+zmc/PEnvyre5u9NRPklr1b65wMtERUNM39fJzFVGOD1DV+Pj01tFSklv79nI3+djUdvPwsHFyOv//9vHycLP41lEUV/fzd3bW0U6PE1r3913XURFRV3PxMTE22dJVffNw77H12NRR2/Xz8fN51NCS073ztfdZ0ZGSWfV19PnX0dI
+Y93DvsXLW1dRX8HAvrzZUT5HX9vCv8PvSzozRFvvydNvQzo6PlnIxr3LZz5ATWfHury9911OW8/EvrzJ50pJUXfXzMzjU0U9S1dr32tfSj1IVd/Tycrnb1NZ2cm/v8LOU1ld68bIw8dvSD9MY+PI0dVXQT4/X+ffyPddRUFMY9fHy8ffW0xvz8zBv8/dTU9b
+28bOzs1PS0JO/+Pjd1tDPEFI69Xf3VtNQkXr0cfByt1PUV/bv7m5vctbQFHv076/x+dHOztL49XF0Vs6ODo/98nKwutPQUFnz8O6v8ZrTEtj2cbCv9VjQkhZa8/Tz91HRUJR993R53dLP0tj18/MzGNZT1XMycO8x+tRU2fbwsXDzllJQFXr58zb/0c9O0NX
+4+PVb089SVf/y8fO01VPS+vGwr6/z/dNXefJxMnL901LQ2/b2dlvTz88R1Hn29tvT0lBUd/Jv8LF409Xa9G9vb3J90NEW9nKv8fXSj87Q/fPzsb/TT09Q2PdyszTWUc/Ue/OwL7G1VNXa9HDxL/Ea1FIVePNy9HnTT0/RF/v4+NVQzw7TffXyMzrT0ZHb8XC
+u7rJ909j3dG+v77Jb0ZJXdvdyc9vQUE/RWvj999XRT5JX+/XydHbVVljzcbHxsz/W0r/08zK0edXRUlR587O1XdOPUJX/9PO0WtJRERVzMjFy9VRTV3Xyb6/wdFjSUzv0czEz+9LRkJRd93n/1NHPUVKZ+Pf3+9LSlf32dXJy9XrY+fNx9HOye939//V1dXv
+b1lJS11ja1ljU0xIR1dnZ//34/9v59vRzMzJys/j3dvf1dvbz91rW2drU2fv7/9fX1FXU0pjd2dnd29TV2Nn4+Pf083T7+fT2dfVzcvX629v72Pf3+f/W1VNTEtJX11VT1FOTVljY9fj6+/j7+fXyc7JzM/T193f0czf1+frZ1lZWWNrV19TUU1VV1FXW1dv
+VWv/3dvd29Pd2evZz9HX39//a+f36+fj629nV1VvZ2tv62NZU1NZd/f/6+P/X19r99fRzcrP3+fn3+PV0dHb52trZ2dn6/9nX1VOUVFXV2NdU1tfW11n7/fj6+vb2dvX1c7Zz8/Z19XX29/j/+//XV9fWVdRU09ZVU5RUU9VV2v/4+Pf3d/n2c7KzMrM09nr
+99/Z2efj42dfW1trb2dbZ1dNU1tbX19vXV9dW3f36+vf2ePb29PNzs/X3d/3393n3ev/a11ZU1tdU1VTS05n62dfTUlMWf/XyNPj91Vr18vAvcTOb11r2czDwM/vTURNVXfPzudOQz4/TGvXz91RQkRJW8/Fv8jZXUxj59G/vcr3V0tP99nJw9dvR0FGTvfN
+zdNbRT5EV+vKyNtrSUVM98rDvMXvXVNb48K9vclnTEhNZ+vGxutbRD5GTf/T0fdGPj9Hd9/JzG9OP0V3yr23t8h3RkFf28u9t8f3Z1FOX2Nj909FRFFr683XWUg3Oz9M07q5v9NNRE1O3by5vsTjZ//Z2cnJZ1dCPU3XzMfA60M/NjtRb8/Ax3dNS0hO79XK
+1etjd9nMvrzFz1VHTVPrwru/0107PERCa9njY1VDQFdnY9HvTUxDQ+/Cvrq742tTRmPZxbu6wNn3WVXn79/dXUxNTVvPxc7nTDY2Oz5ryMbFx1NCV1ld2crPy9tjzsPJyMprU0E8W93Ov73LV0Q4OUlT38nNd/9jU/fva3dXRE9n476ztLvJSj4/R2vNvLy/
+zE5LSUhTXVlbTUNZ29nO0VNGPDVAWdfAtrjN90dHW/fTw7/Mz8vTy8bP710/PUNP38e8x+9LODQ3QV/Px8nTUVNfVf/P1dndX+fHvr65vu9MOztN78y+uc1jTDs9Q0tf3f9ja3fv19PnY0RCREz327e1u8VfTEpJ58XDvr7XW2dvW/9dT01AP0zZz8bH90U3
+MjlL98i4u9d3TUhTW9vDx8/Z29XNwcPH1U1ESUlrxL3Bx2c8Ojg8U+fr091RTFNRd/dja2dRWcu8u7m803c+PE1n0b25v9tNPkFFRm/fZ1dnX2/Ryc/bSj09QE3Ot7W4xVlKPz9j0cG9vMf/d2tdd1lPUUdBXc3HwsLnVzswOkZT172+1WdJRlVOa9HL49/X
+zsS8vsPdSEVESOu8t7rDY0Q8NjtM79/O1VtRX1Vnd1NdY0ZT3cO9t7nK60E+TO/Zv7m+3VlJS0pGUf9VT1NX/8/M091EPT1BT9u8ub3BY0pEQ2/HwL+902fr59/R911bRT5K487Ev9NRODA1Qk/fv7/P50pITERV3//32dnRvry/w2tPTExT3by6vL3vTEA7
+QWNnd8/rTFFfW/drTE9BPEvVw765vM5TPT9Z98+5tcXOX0hTT03j/1NRVVFZ3dHO1Us/QT5H28C/vsdnVUZCd9PVxr7T59fXz8zvZ2tHQFvRy8XB0e9CNj1HSu/FxdtrSEdOSl3v/1tjb/fOwb/Az2NVTUr3wLu7vs/3TT5BT1//1dlnX1FTa19RW1NBSXfb
+x73Bxt9HR1dv2b+5vsv3X29ZU2f3TU5PT//T0dXvQjw+PEnrwsHD2WNOQ0Zj1dPHw9XZ0dPJw93v3UdBT9vMv73M/z41O0ZLa8TG2W9IS09ETe9dVVlfd8zGxsh3T01MWdm8uLq802dMQ1Hb3+PO61FTY2/b90xOPjg+Y9XFv8PPTjw/W+/TurfAyndVb/9r
+z9V3XVlRX9/X1d1IPz08RtvHxsHOVUtFRG/Z287I63fb0crJ2d9nSUj/zsu/wNH3QDpHU2PTwdf/VUZNVU5361tMT0tZ08fIyXdKR0NKzrm6u7zdX0xFZ93Zz8rdT1tnd+NbV1E/Okvr28y/zt9NPUpd/8u6usbPd2/f/93O3VNZV1XfyMvTYz07OjpPysLF
+xOdRRTxL99vTwsRva9/VyMnZ1e9FTO/MyL2+zlc6PUdNX8vC0/dTSVdXUWvjTk5TU3fMyczbSkJCQlvPvL28wOtjTE3nzNXLyHdf59/X1W9bTTk8R3fbycbXXTw4QFF3zbvCy9N37/9Jwcvd//dv98/LzdFdRz87Pk7Xz8rH91FJRE13Z/fP22/j0czL0+fr
+T0hb08fDwMjVU0JFTllj18rb52dfY19TWVlTUVFb78zX3+dbSkVDS+fO0cO9zN/3b29dX93G1dvNz+dnTlVbTEtn92d37+/3WVFZV09Z7+PVycfGydXvZ11j993XyMfV52dVSEdIT19bWe/f719j/19RUWPvb+vMxcfV3dv/Y2Pf19nOx8XV/29jT09VX2v3
+39nZ/2tfTkxNUVld593V7293V0tOU1lbb9PKytPOzOdna+vr79XLxMzf4+9RS0xbWV1r7+dbU1dPTExVY2v348/KzsrN3/dvY2fv3czIz9XnV0tKSk1TXe/rd2v3Z1dXW2tjb/fX09XTz9fn7+vr//ff08/T0d33Z1lVV1db/93X2d3va1VNTE5Vd9/n3+Nj
+WVtXV11na3f3693Rz9PX3e9rX3fn49XKz9vrY09RVVNnd2//5+9nX1VTWVlj49/TzdXPzdv/9+t39+vr1df3d+t3WU9NTEtKU+vj5+P/U1NTVWvv387Kz9XV2+fZ29/b3+vf593X3etvW09PVVn35+PX2f9XV1VRUVn/4+vv3+NrW1NRV1Vf39XZ1c7V6+Pn
+69/r49vX393X1993Z11RTVFVW+vd4/dbTUlMVXfdzMnGy93r/+//99vP2e/j2/9rZ2dnU01OWVNRZ+vjd2drZ1Vbd+ff1c7Jye/dX19nWczIycb/909b/1nZ6/f3U9X3X1tNY0//1+vMTlVXRF1b3b/R301NRUNvzb6+09tFS0xOw7+/vv9bPUZV977CyGdF
+STtP69W/4+9LPktI67++v9PfSElfZ8TFzMlfX0tK5//bY1tVQVNb991n/0lNUUnP0czJ/+dZX9vXwcPJ21tnY9/NxcR3XUM9SVXdusHPX0k7PEfvv7rO20hGREfjy8zOY1E+RV3dxL/M1VdNQF/Mz77G0dVVX01f22/X3//vW+v391lJTURV5+/EzN/vTlVP
+Z8jIv87rV0lT/8y8ydtIRz48V9PJu+NfPz08QmfGv7/X70tNVWfKyMrD5+9bd8/Tzev/WU9dVd3T691dZ19R53fd/1djS13f2cXV50dBQULvycm72eNKP0VO3b7DwOtvRktb/8HC1dtMVUhO0crByGtMPUZDY8vNxMzra0hjW//L48vZd+Nv09v/Y05bR13d
+/85nV09CVU1dzN/VX1lRTtvRw7vT3UhTWW+/ury+d1E7P0lrxb/K41dNPUdd67/R2/9IT0lfzM/F319GP05R3czZy2NnW0v3b+PT687v39Pjze9nU0xvY9HCy8lRTURFXWfRwePvSEhJX87Jv833XUFMV926wMTVTz86R1/OvcnZTkVDPlf328n/d05RZ3fR
+zNPTZ29Pd9HPwtPX2VNrW3fP/+dXX2NO2dXZ11FTQUt3/76+zuNKRD1R39O/yt9OP0lK38bMxmNZRUNfd8m4ysxXTD9Cb9G+uc7PTUpJT93f085nd0tX293V7+NjTndf1cXR0W9nVU7v79nba2tITVVX29/jb0xFQl/f0b3Hz1VNR0rjwr+6xuNMSk1dysHM
+zVVPQEhn48bI3e9BRkJb1c7EzutnSVdRd9fr2e9fd1nf49/vVWtMUdvjxsjdXU1XUf/JzMbdb0xKZ+vMwdXfSkpESd/NxsHfbz9FRU3MvsLA31lCSlf3xMXV2U1OSFf33c9nX0xFW1fdz9nZXWdjW9XVztfv31v/29nEz91fTFlMX8vKyONjPkBRTuPBystr
+VT9Gd+fGv9PfSk1JVc/Myc5rXUBMUV3Ly87ZVU9GZ+/bxNnd61lva9PR2dFdXV1X59fO12tnRktda8nKz+ddWUtd3dvF0+9KRk5R48/OzVtPPUFZXdfCzdNXTkFI38/Dvc/fWV9Td8bJy85vZ01fb9vN3+dTS09L99vn0W9rWVN3Y93d699VY3dn09nV/1VZ
+R1fb28bM3U1MU0hvzMvF2/9ITGdnzMTO0VtdR1HXz8nI3/9CS0VOz8rJyvdbR1tf78nV1eNVWVH/39vRb19KRltf29Pd3U1RVVPf383T5+NbZ+fjxc7RX1djU/fNycff70dFT073ydHRa1FARl33zcnb505TTWPPy8rTa1dAU1nfzM7TX01JRGdv28nd32tX
+X2PR0c7Lb3dfY9vOxs3Z/0lRUWPT089vY1FLX//Ry+PrS1FdY9PKzONbVT5MZ//Nzdl3T0g/T+PVxsvXX1lbWdXKzcvvZ09Z59fJyNXjS1FNX9/j2d1fX0tbW//V9+trTl1j3c7L11lbSEtv58vFzedKTENJ79nKy99ZSlNd58XLzfdjU0l3zsu/y9VfU09O
+39HVzXdnSVdfXdvX5+dVWVdv0dXO2V9RRVFf28vT21dLTEdn69fR/3dMT2/rx8XK51VZS1vTysHH0W9JTk9fzs7M2WdHRU9n58/b41NVTFPfzcrH419GT1Vvz8vX21dVR1dnZ9/vZ19NW1/bzNfZd1dTW+vNw8bP3V1XX//PyM3bZ05LT2vZz9HfUUpKV2/R
+yc7VX0tFTv/vzc3ra09ITGvd3dVvWU9TZ+vPx9PbX05VX9nJxcfdZ1FOXd/Iyc3nU0tJTffVztf/TUdKU/fPycvb/1dLV2vjysvRa1lOT1tr6+Pra1VbX11r/9/nd2dn9+vn39vj49/Z3dPf9+dnX/ff3+vvWVdVU13f1dXnb11XU1n31czd93dPT1lv39nd
+d19VTFNn3dfT1etrXVl349/by9nvb2trb/fj183bb/9fV1Vdb2/n93d3Z1tjd+/n09Hf92ddXW/n39Xbd2dRSkxRY/fT13drV05RW+fTydXf3/9dZ+PVzsnN0d1vXWP/7+fZ22dbU1FXZ3fr1e9rZ11XXXfr49Xn72tbV1lrd//d7+f3XV1f9+fr29/f73dn
+b+vv99Xd5//3d2fv9+/Z//d3Y1VVXV134+vn73dnXWt379fX3+/v/3fvd//n/3ddW1tfb11n72//d/9na+9v69vf2dvn7/fb393P2dfjd2NdX11d/2v3d2NXVV1da+/r2+v3a2N3a+vT29nrb1NTXV/v2dnd72dZWWv3383X3e9jX2t3993R3d//Y2dn9/9v
+6/frb1tZXWtfb93f3efn92/vd/fZ3dfd91lZX1/v11v/Rkg+SNHCusr/QjtDQsq5t7//UzpJWce4u7xbZ0E/a9u+xsdLPkY6Z9m+v9PbOUA6Q9e/t8TMQD9EPtvFucDPUThJPXfLv7vV3T9HSE/PzLzR2VdBX0zj0cbG3dlDU1lTY1/PY9HrV/dLV0zd58/B
+381MTE9X2d+9xs3jP0E9Tl/FvMjRPkA5QW/Et73FQjs9P9PHtLXCUzk5Rcm7tbDP7zc2OD/DurnBTTcwOUDMs7a8U0AzOUzItLK6U0U1MkrZubK3WTw7Lkvvu668xD8+ND3ju660yUE7OzzTvLO300g2PDd3y7y61185QD5byb+3zd1JQUxF0cW/yOtVO0hE
+a9vVzP/rSk1RXfdvyevL12vnW+tbz9fTvdfN71dJV+vvws7nY0E/PFlvxsDX5z9DPVfdzLjG0UM+PkfPzbzC1WM8QUPbw7q5z2s6Pj5dvLm1x1U3NDxNvLa0v01BLzpDv7a1vUs/MDZH3bSzt9VENzVG57Ovtck/ODA9W72yu8o/OzU5Xdm1u8RMPT06d9e5
+uMPLRklFWcy+usfOSkhKR+fbyN/nT0JPSGdf5+dv21/vd9/Xa9lf3dHj32/rT/9d68XZ0/9nSExjY8PJz+NXTT9bV8+9zdFXTj5RX+e8y9NfRDxCZ2+9xM93Q0M+3c28tsvnQUNByFfXudVfQTY6SMXCtsJbSTQ9R868tbnvTjY8R8+0srDNTjo1R2u3tbnL
+QD4zPU3Tub/LPz45PlvXtr7C70FFQl3dusXK1z5LRVHdwbzXzkRHV03j3cXv22NJa2PV28/3a9tTb//r1///Sut3a99351FnSWPL48rO42tRVVHTys7La1VETUjnw9XMa04+TVHru83K/0k9Qf/duLfG1UdIPuvNv7PL40g7OU7VybfMY0U0Nkbfxra85002
+Oz7TvLWx01c3NT53vLizx1FEND5TyLm5wEhCNT1O1by7vl9HPkBP2bq+vN1GSj9R77/DzNE+SUpN69G/3cpIR19R39fEd9/vQldLXe/Ra1PXT2v/a+Pr1U3d71/R79Pr12vnx+PN1ePra29Vz9Xj21lRRVdJb8j/3WNKP0Vfd7y/y9lLSj//68a50dlPQz5b
+0cq3yd1POzlH38q4vddbOTo868e4ss//Ozg9Z8S/s8NbSTU8SdG+uLtdTzc8SeO8ubjTTj07QVfDwr3TRkM6RlPBv8PJQ0pDSmfXvcrGS0ldV9nOu87M40djV+/ZxudZZz9VV13f19FHZ0ZK53fN0chXZ+NN3ePOz9NvTd9v59Xd13d3SGvjY9n/71NjWVnJ
+49vfZ1tM91vPxdvTXVNDXf/Zv9PTa0dCTffZvL/O40U/PmvVv7rOdz46OU7NzbrF7084O0bdyLu8/188PUrrv7q2xP9EQElvw725yVtEO0RRzMnFzkdHPD9P78HJxU9JS0r338HJy9tFVU5r08HI3eNFT1Vf28rGY29HRWdd1dPIb193Rvdv2c/V6073XWff
+99Xn509X22PZ29Xv72dPztPV0e9vTVdEY83jzOddR0pOV8bNyc9VS0JPXce9x8djTUZR68m7vstbRz5E792/vtX3Pjo6S93LvNnjRTw/S8zDuLzfTz0/Ts7CvLrjWUA+S93JxL13U0I9SlXLycHvTEs/T13PxMnJTktJS+/Ows3OWUtTT/fVxs/da0lbW+vT
+zMtra0dPW2ff3dFXWVVO72/f2dl3T3db59vZ0d3rTtvT083T0XdvSVXN0czb91NJRkXb29PZWU0/SEnVwMbLa1dJSVvXwcHF51tJQ19rxb/Iz0xBPEZv07/P01lCP0Xr08DC029FREXv08nB0e9ORUpv0dfG1eNdSFlf0dfLzv9vS19r6+Pj0WNfVU///9n/
+2+9bX1nr79vj5+9b///Xz9HN499VY2vv19fO/3dPS1tVd+/n71VOR1Nb/9nVzndvV13//9nOyeN3a1v/Z+PMzd9nX0xbVVnd62tZd1FTZ//R08vv/3dPa2vjz9F3zt9TX2dv19XP93dXVWNf393Z2W9fTVdXb+Pv12dfT1dnZ9/f1eNjY1V3b9/R189371/v
+393O0893a2db///f2dtvVVdNVVdv5+vjV1lTWWvvz87P229rV2/rzcfNz/93VVljZ/fn2/dTS0lVa9vX2dlbVU5Zd9vT1c5jX1db9+fR1dNvVVtV//fb0dvbVVtXX+vd1dvnVU5VV+/r19Pfb05ZVffn2c3b3VtbX2/n18zV2+dfZ1/v29PO5+9XW11f6+/f
+d2dXTFFTZ+/n4/f/V2Njd9fZ1d3bZ2d3b9vX19Xb91dbV2tvb9/r411VU1N3a+fd29NOY29TZ03ryM7D1ddMSVFI38PIvdV3STxJRme+xLzZVz06SUzVvMbBTlNJPuf/w7vT2z5KSVfIxr/Da+9DV//rv7/M00RXQE3Rz7/Oa1M8UUhvzNvRS1FGQdvvxsLj
+d0NLa2vEwcbTT1VDW87Tv8jjWTpIQFfEy7/fVUE7VVXNusnLR0c9Qu/GvLrTaz1AQ1fLvcHCa107RUxfyL/JzVNVPk1r48rn52NRX0v349vXd/fj/9tj5+9Zd2fnw9XI/2dLSmPfxbvKy0ZLPkD318G821s7Pz1Oz8jEw2NXPUld/8a/v8VXT0JT49e+xM9r
+SVFDb9nn0V1bRkzvd8/Xd19JT3fjyc/Md1NRRvfN077X60pDSFHVvsbCY1U6QlFdwcHG0UdEOkR307q80VtBSUNbxsm8zndRQVtR773Mv+NdTk93/8/I3dNPXVdT42v3d1d3UW/bb9ddU1NT29vKy+fbREtRWcLKxcZrUz1GWe+8w8lnS0Q7TefbvM3ZWURE
+RV3Ny7vb90dKXVnZx8/GU2dNT9lv285390tnY+vPd/9TTl9T18/VyllTSUbf48m9zt9KRkBVy8e9v99fPT9GX7u+usBbRDg9Udm2u8JrRD43SN/TucbbUT5CRF/CxrjJY0ZDUVXTvcvEWVNLSHfvz8p390ZMY1nT0+9vUVtVd9Xnz+tbV03v2dXE0dlbS0tT
+z8fLxvdfP0RRXcnCzM1RSD1HZ+O/wdn3RUlIW9XXwc7vd0RbWWvO18lvW/9V2+vn02frY2vT583r929KX1tnytnPd09CQVfX073L10xGQEfXwcO722s+P0hvwLrEw1FLO0Jj48O80d8/QDpG3c6/vfdZPURJd8TIw8tXT0BZ787Az9FTRltK48/TzVtdTkfv
+b9nP629GTVtVy83GyFlPP13Z37/KzmdJR0Frxsy+zedDQExOz77Kwl1TQkZdZ86+2dVJS0VKd/fMyu/3S2tna9Xv2dNb42ff4+PZW29bT9nnztNrXUFLXWPGzM/3UUU8VdnTv8nOUUlGQ9nEy73b60dFS03Tv8vGW1E9RGN3x77f60NIRVnO1cnKZ1FBWWvR
+w87NY0xTS9nTzMX3/01GZ2POx8rNUVNGR9/bwsXZWUBOTl3Jz8bbY0U+Wf/TwMnLUU1GSd3Lzr7d50hLU0/ZyNHLWVdETG9ry9X3d0hZTm/d69PnX11O6+/Xz//dVVPrb87X2+dTY1Nj0ePP729KRFld48fZzFldSEbr1dHB09VNUUhLzsnKw/9ZQEtZ98LD
+0dlNT0Fd39XDzudNQ05M38/VxmNbSUj/b87J19FTU1FT09nIy293SE/v78nT1V9LWUtrz9PH7/9CQmdb08XTzFVXQ03X3cvA1ddHUUhTzNPGxWtVP05Xb8TV0f9OST9nZ9fE3d9RSl9P1dfXx2dvZ1fjZ9Pb49VRa29d0XfVXVlvUd/T585j701N92fVztvR
+TltBStnjycXr/0VRS1/FzM3OY2c/X2PXws7NY0pXSNvd3cT/91FHZ1PZ2dvPU1NJStn/yMvv70VOa3fH08fvV1FCWd/TxNfXSEVPR+PL08X//0JFX1vVwszHXV1DSdvZyL/X50ZRT13L083NY1NBX2/nyc7ZY0tXSePX38jrY1lHX13dzuPTZ1FZTevrztN3
+705V/2fO29PVVV9TWdvfzNvjW0hfa2/R389bXVFG59vjx+fZT19ZXdfN185vZ0lb73fO0e/nSU9KW9Pr1dlXU0VTX9nI3ddnTFdM29HNw+/vVUhnb87LydNZU0hM79vHzNfvRExRV8zPyNNvSz9N/+PK1c1jU01Hd9vbx9fnT01VV+PN283rZ0pZ93fXz9/b
+TT1RyNXv2c5vd1Nna+/na91vV/9d32v332/f/1/Z/+dr999jd/dr32N3XWfrU3fdZ+dZd3fv51vv2WfjZ+Nv69v31+dr3Xfna/fXd+/vX+tfd2/j12d3/133W9/n699f9/dZ/2vb7+d3W3dnX+fv22d371drb2fX699v/29VZ+fv1+/fa3dnUevj99v352Nn
+W13r4+vX6+tXd29n4+fv1XdnU2drY+/f5+tja1d3X13d5+/vY3dbd3d30dvv4+/vXefv79fv6913b1v392vd7+/vZ3dd52t34//j/2/3Xe9nZ913//f//1vvd//b/+Pr/3dd5/9r3Xfr/29rW+dvd93/92tv92vja/ffY3d3/+f/5/dr62Nv39vb5+NbRlNX
+/+/n72/nWV//b9/f2/9j/1Nn7/fV2dfnY3dbY+Pn1c7Xd1t3V2fd69Pf92dZb1dj2/fX43dfXf9b/9133/f3b1v/WV/fb9/j629f/19n43fn9/f/b99fd99nb1ln08TJb1NNRFH3x7/L/0ZBREvTwb/Hb0w9P2PVwrvEb0xFPVnHxLzJa0o9RU7vvsPK70w+
+QFPfxL3N609GRlHLw8XHW1FIR2PTvb7dXT8+RWvGwr3dSUc8Ru/NvcHVVT5ARWPCvb7TVz48SG/Dur3MTT46PufFubjOUz09QFu9ur3NT0M6SPfPuL/fW0E9RGPGv7zKTz87PU/JvsXDZ0xTWe/Pw89VSD1H38y7uMhLODY4S9PFsrzfVz0/SV3JvcP/XU9J
+18TGxvdJPUBEU7m1u71fPDc5Rue5vMXPTEdHXd3Vyf9TSkNn18i5w/dAPDtH2cu6uO9fRz5JXc/KwdVJV0tPyr/EzV8+Oj9L1bW4v99COTlFZ7+6xdFMRkpVzsbG11dHO0rn07m4zW9ANTlK2cS0u9VdPz1Jb93OzltRTFHOvr6/3Uo8Oz9vvLu4vldJPT5O
+98rIx+dKWWNvxsTXX0Y4PvfRvbC7zEs4NTx3Vb22xd1LQkpX39fTa1FLRuO+v7zFWUQ6N0Tnv7221V9IQERP3dnX209b59nHxMt3QTo6Rtm/srTBVTs3N0vbyrvJ401DT3fTzc5rR0c+V766ubnbSjg2O0/AvLW330c+QElrzM3Pa01TY9XGx8lbPzo8TeO6
+s7rIU0E8QE//xMLb3VFO79nOz+NRPz9AVb24t7vfPjQ2Pf+9urbDWz89SGPXycz3Q0JNa8W8vsVVPjU8TWu9s73GVzw5PlHfvsfV51FOW9/O2d9RR0RI3by5uMpbPDc7T8i/u7nrV0VCW/fb09tOP01v0b67w2tENTRGZ8ixsrrjQTc3Q3fJvcXXWUxX99HL
+3WtGQTxNxby5t89RPTU4Wc/GuL3ZXUlJUWfn399OR2fjzL7AxVk8NTdL37y0t8RRPjw9U9vHxdV3S03r08jG40lCPDt3wLmzuc1HNjQ6b8K5tLvjS0RLWf/V2etKSVf3ycXGzUo5Nz5R0763vtdNP0BIVdvB0dvrTGfbzsnTY0Y/PUvXubm5wl86NDhLzcC8
+uNlPRURda+fP10tATF3Tv76/3Uo2N0RfzbS4vs5KPT5GXc/K29tjU2PdzNXnVUE9PFHFvbq601M9OUXnzse6x2tZSWPd3dvVZ0JBa+PIu7zJbz0zO0dnv7S5yFM/PT5T2cnF129LU//TxcvfWUU8PPfCwbm/21k8OEr318e6yGtXUWfr59/fXz5Ka9vDvr/M
+STk4P1fVubi+3UdDP0r31cfj519Hb9XMw81dSD43Rd2+vbe97z83PEzTz7652VlKUffr2dfbTkJMZ8++v77fQjs8SFnVub7E40lFSUxnzdV3601O2czHyd9RQj08TsG9u7zRSTo4QuvLyb3IZ01Ga93f089nQUFT/8K/wcRZPTY+S2O/ub/FY0RCSFfjx9Xj
+71Fb38vHze9MQT1B28K+u8l3Pzk+X9HVxb53XUlR29PX1dlVPU9n2769wc5JNzpDW8u4u8JrRz9DWefIx9//Rkpn18XF1WNJPzpZyMe8vtldPzpHb9fOvcpdUVNr39fZ2f8+RmPrxb+/yFU8OEBX17y8wttIRENP79PI6+9XQmPXzcTK/0lAOEfJw8C5xu9B
+Oj5L1dnCveNPSlHr59PZ301BS2PVv8G/40Y9PU1jx7q/xmNLSU9378vd9+9NU9fKw8rbX0M8PE7Gv7y9zE86OkBr087AyV9JRmPd187P6z09TWfHvb/Ab0Y2PU5dxbnAxmdGQUhv38TL519HS//PxMrNV0M7PWPOx76/0Uk+QUz399fA53dba9nb3fd3Vz9R
+69fBvsLVTjw6TmfVvsLLd0lHSe/bzs3vWUVFb8m9wMz/RTw5SsnAvLrL/0A8QWvZ0cHRX01Nd9/VytfnRD1MX9vBvb5vUT09SWfMwsXRWVNNV+vd2etjTUVZ1cbDy9FvTD1B59PRw8PPU0VPY+sf
+--seconddivider--
+This is junk.
+
+What will it do?
+--foobarbazola
+Content-type: application/atomicmail
+
+; This is a a MAGICMAIL message that contains a survey.
+; If you are reading this after receiving it in the mail, that
+; means that your mail-reading program needs to be upgraded
+; to know how to run MAGICMAIL programs.
+;
+; If you are reading this in the hope of editing your survey
+; before sending it, you should start at the END of the message,
+; which is where you'll find the parts you're likely to want
+; to try to edit.
+
+(setq newline "\n")
+
+(setq global-survey-qid-ctr 0)
+(setq total-questions 0)
+
+(defun nextctr ()
+ (setq global-survey-qid-ctr (plus global-survey-qid-ctr 1)))
+
+; USAGE: (survey-multiple-choice "Which is best? " '("bar" "baz" "ola"))
+
+(defun informative (p)
+ (strcat
+ "#"
+ (int-to-str global-survey-qid-ctr)
+ " of "
+ (int-to-str total-questions)
+ ": "
+ p))
+
+(defun survey-multiple-choice (prompt choices)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (car
+ (car
+ (select (cons (list "" (informative prompt) NIL NIL) (cons (list "" "" NIL NIL) choices)))))
+ newline))
+
+; USAGE: (survey-short-answer "How are you? ")
+
+(defun survey-short-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (getstring (informative prompt) "")
+ newline))
+
+; USAGE: (survey-integer-answer "How old are you? ")
+
+(defun survey-integer-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (int-to-str (getinteger (informative prompt)))
+ newline))
+
+; USAGE: (survey-boolean-answer "Do you think I'm sexy? ")
+
+(defun survey-boolean-answer (prompt)
+ (strcat
+ (int-to-str (nextctr))
+ " ("
+ prompt
+ "): "
+ (cond ((getboolean (informative prompt)) "Yes")
+ (T "No"))
+ newline))
+
+(defun mapcar (func args)
+ (cond ((null args) NIL)
+ (T
+ (append
+ (list (magiceval (list func (car args))))
+ (mapcar func (cdr args))))))
+
+(defun surv-pkg (q)
+ (list (strcat (int-to-str (nextctr))
+ " ("
+ q
+ "): ")
+ (informative q) "" "s"))
+
+(defun surv-pkg2 (q)
+ (list (strcat (int-to-str (nextctr))
+ " ("
+ (car q)
+ "): ")
+ (informative (car q)) "" (car (cdr q))))
+
+(defun formatfillinlist (lis)
+ (cond
+ ((null lis) "")
+ (T (strcat
+ (car (car lis))
+ (sexp-to-str (car (cdr (car lis))))
+ newline
+ (formatfillinlist (cdr lis))))))
+
+; USAGE: (survey-simple-form "preface" '("foo" "bar" "baz")))
+
+(defun survey-simple-form (preface qlist)
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar 'surv-pkg qlist)))))
+
+; USAGE: (survey-complex-form "preface" '('("foo" "i") '("bar" "s") '("baz" "b")))
+
+(defun survey-complex-form (preface qlist)
+ (formatfillinlist
+ (fillindata
+ (cons (list "" preface "" "i" NIL NIL)
+ (mapcar 'surv-pkg2 qlist)))))
+
+(defun ask-question-set (qlist)
+ (cond
+ ((null qlist) "")
+ (T (strcat
+ (magiceval
+ (cons
+ (car (car qlist))
+ (cdr (car qlist))))
+ (ask-question-set (cdr qlist))))))
+
+(defun qcount (l)
+ (cond
+ ((null l) 0)
+ ((eq 'survey-simple-form (car (cdr (car (car l)))))
+ (plus (dcount (car (cdr (car (cdr (cdr (car l)))))))
+ (qcount (cdr l))))
+ (T (plus 1 (qcount (cdr l))))))
+
+(defun dcount (l)
+ (cond
+ ((null l) 0)
+ (T (plus 1 (dcount (cdr l))))))
+
+(defun handle-survey (to cc subject qlist)
+ (progn
+ (setq total-questions (qcount qlist))
+ (sendmessage
+ to
+ cc
+ subject
+ (ask-question-set qlist)
+ NIL
+ 0
+ T)))
+
+(defun maybe-displaytext (t)
+ (cond
+ ((equal t NIL) NIL)
+ ((equal t "") NIL)
+ (T (displaytext t))))
+
+; This is the user-generated portion of the survey
+; Be careful in editing this, because you don't want to
+; mess up the LISP syntax. (Be especially careful if you
+; don't know how to program in LISP!)
+; Note that lines that begin with semicolons are COMMENTS.
+
+(maybe-displaytext "Thank you for your patience. I would now like to ask you four questions about what this message did when you tried to read it.")
+(handle-survey
+ "nsb" NIL
+ "Re: Multipart test"
+ '(
+ ('survey-multiple-choice "Were you able to read the introductory text?"
+ '(
+ "Yes"
+ "No"
+ "I don't know"
+ ))
+ ('survey-multiple-choice "What happened with the audio part?"
+ '(
+ "I heard it fine on my SPARC."
+ "I saw a message saying I can't read it because I'm not on a SPARC"
+ "I saw garbage"
+ "I don't know"
+ ))
+ ('survey-multiple-choice "What happened with the picture?"
+ '(
+ "I saw the picture just fine"
+ "I saw a message saying I can't see it unless I run X."
+ "I saw garbage"
+ "I don't know."
+ ))
+ ('survey-short-answer "Please enter any other comments: ")
+ ))
+(maybe-displaytext "")
+
+--foobarbazola
+Content-Type: MESSAGE/RFC822
+Content-Description: Yet another level deeper...
+
+Date: Thu, 24 Oct 1991 17:09:10 -0700 (PDT)
+From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
+Subject: Monster!
+MIME-Version: 1.0
+Content-Description: I'm Twying...
+Content-Transfer-Encoding: base64
+Content-type: AUDIO/X-SUN
+
+LnNuZAAAADAAANeKAAAAAQAAH0AAAAABRWxtZXIgRnVkZCB0YWxraW5nAAAAAAAAVFZYan39
++Flh7N3X6t7f2dzc811za3dsaWhl4tvY3ebd4ej/fnn+cGX+YWNubP3t5d7W2+b3bVhZVVlX
+Vmj52s3JxcrLz83Q3d/x/V5QTFFsVllraHPf2mdqXVpcTUZJR05YXV5mYlBqyrmz3y4qKEq0
+qqK4UUU3SGho4M3Gxtpy4djHwc/sVVrTx8PH3+9dVFdNR0VIR0Q+OTpE8cfGXEJI6L64vMbO
+z9LrW1dXaO3xaFFt3s/qS0VJUl7r6d/7e/LYxsPJz9Hf+NjKy3NbRUBLvp6nNyIXK7ufmbcz
+LDNFzWBmyNTJWk9e2sPJUz44QmfWxNlPSEVOVlvLwL64v/JCO0Hbv7q92N79Y0c/TtO6vdxG
+NzdCTEY+Ozc/WMu5vtT29+HZ3MnKz893Tz89S8S2sbK8uLreOS88v62txDswOVbc6kxDP0vp
+3u1VQ0Q/Pkk+PEJPSD87NkjFtbrKx7a0vTQoLsGfnaK9X368r7TcQTxQyLe9UDc3Q1dtpZ0j
+EAkOPKSMks4iJDavpLq2TGBKNTo3TdN4Qjk/t6egorfb7Mqyt66kqjYXDBVVno+cWx4ZHjRz
+xs9BPz1Nzr23t+dKSNSyr7X0zJ6QnBYEBBPEj4qiMiI/rqSwxd3JtupKO07CxD8qJDi9qarq
+Lz+pkpAzCQEJNpaIlMopLV2+1zA8O93sPjg94bS1YTs60a6loarH256SrScJCSmni5K2IBwk
+S89JPy/lx73J4tGxr7DpMS86tainmKIiFQUNMqCMkKUuIBouMUJAMUtPyLa1uKutq7G6vb+5
+uKmamJ8cCgYQvJWNnMUhISAsKCcrMXu+tL7Bzbi7tsju3bihmp/GIRggbKCeqmAwJiUjJCgq
+OUFISOi8r6yxt8C3oJqf3B0XJrKXkZrMKhwcHyQpKS0uNTpU1sq/vr/JtZmRl7IUDxjdlI2R
+pkgfJCAmIR8kL0/GxHPqbcnNy7eYkZq5Dg0btoyHjJ5fGyUdHBEMDhg8tq5XPDJuwK+jnZeZ
+pzsgKO6ZjoySoNYvIhgSDQ4SHzjf51FMRGHSu6uempmdqre+uKmjpazVNSkhHhwbGxwfKTJA
+UVjYtaensmg7XKyZkI+Vna3MPSsfGRYWGB0lKzU8T9SvoKW2MSYsyJ2Vk5icoKCmtjkdFhYZ
+HR8iKCw1RtO2pJyirMfCtKumqK29u8rGynhJNCwmIyMhIB8gJCw93LOus7y+tKykoZ+gpKit
+srvPSS8mISAhISMiHyAsPXnIwbWtpp6cnJ6gpaWio6a0YzQnHxwaFhQSExgdJzjvvayjnZiX
+lZWXmZ6jq7bZQzAlIx8cGhgXFxocIi9Nx7apoZyXlZSWmZ2gpaqy1TwpIR0aGBQUFRgcIS5U
+xLSpoZuXlJSWmZ2foaSpuk4tJSIeHBgTEQ8TGB0mNOmvoZuXlpWUk5WZnqe230w3KiIeHBkY
+FxcZGx0iKTfqs6efm5iWlJOVmJueprHKSTYqIR0aGBcXFxgbHycxRsmvqKKenZybmpudoaqv
+tb9jOSoiHh0cGRYWGBwiL0nBq5+bmZiYl5ianaSuvs3hUDovJCAhIR0ZFhUXGyIuTL6ro5yX
+lZSWmJmbn6m4ZD4yKighHh4dHB8mIR0eJi891LyuopuYlpWYnJ6hp7TPRDEqJiAdGxscHiEj
+JSo1S2B+xL+so56cnJqZm56iq7C31zorJiIeHh4dHh8jKCouND5Wd72vpaKkoJ6enJ2kq7PG
+z1AxLyglJSMiJCIjJyorMzhE9su7rquhnp6dnZ2gpbHE4F5HNzEqKCUkJiUmJCUkKzdJ4tW8
+ua6ppaGinp6io6assLznW0IzLSYhHx8gIyYoLTVBVG/cwby5ta6pqaioqq6xtb7G1GROQz46
+NS4sKSosLDA0NztP/NXAuK+urq2trK2wtLzO1Mr8Wl1KP0Q/Ojk5NjU0NTo+Q05ledzYxsDA
+xL+9vbvAyczY5F1ZTEQ/P2vR2U9ISEVIWXXlxsbLzsnd2cjb3t7a4NfZ7+9fWvRJSk5DS1Bn
+9HbZyMnOzNHe4dXAyNzscEk+UU5TaMrN6ONjaVtSSUhMUVVf5+/Z2H7c3XdsVmFWT1N53Vpa
+XV3tZtHWzr/K2NztSExI98vCxby3yMPdz1NLR0nmSjhIVGVhbtbl9VRiU15JRk5GT2p3W2Ds
+0UG3wbzD0cn+XD9WR2FXSFrg4cO+u7/Gv8LJ2eZdTUlNQEU/R0hKWFTqbtZx+XRc/lVVXOhf
+bf3Szsu/xLe+u8fXyNPQa9fobHde0kx9UlzZSl08Tj5OTEdWRtFE90JN2UfWRmFnUuZGz05q
+bPjIZcfevMrF2bzkYdxf2FdzUs3X3WLew87Jcda+3NhCdOlOTT9gXdg7WVPnzUJKR89PWTxG
+5fb1RkruzOjV2NTCyN/haNfc+vDg1FxmTVtdb3dc5WfL//vX6szd3+/50srAycrW1tXoUEJI
+VGlcRUlf8droftvPysbN2/Lo9nFZVVFNfXV35mdu/FlLTkpZ7WNOS0lqcWRwZuXP0NHR2uTe
+3uR4bfrf7nXrdWjp2d7l5t3c8+DZ9W/f0vB2/tjS2d7dd1RrVUhDRV1aUktNSVJ5VE5LWdvV
+yM7Z7dvN4vz+5NTT0t74YnT3ZWFVS05OU1haWPvW1dTc0sS+vcfb5dTbeWFOXm9weV1YUlFO
+TU5PbdXn3d7v9dbM0NHcxsDIyeBlXnFZXF1Z6/x/b19STl5VTEpJTl1dWllUT2VpX3f6zcXG
+ytfS3tnQ3d3s3e9zcWz2YWN8aF5eaWpoaldXVW3r9tnPxcbIyNLb6n5WU1pTW2ZlXlRPS09O
+WltfbmxcS0tPVl5eaerZysPDx8/OzcvT097p41tKQkFGT+TMwcTEze9cQEE/PT5ASmS8tre7
+v8C9sKytuGMzKCYqMkvMubm602lNPjcvKignKi9Kv6qhnp2dnqKosLm2v8lEIhUODA4ZMLOf
+m5ufrNQ7KiIiIiYw66ufl5OTlZiboKeurKy+RxwNBQIFDB+/mpGQlqTKMCMcGhkbITO1m5CM
+i42Sm6Wuvlg9LSgrLCcfFg4MDxgurpyUlJijvjclHRseJDrGpJqWlpidpqy6z2XEraiv3hsK
+AwMKG62QiIiMmtweEg4PFBoq+aOUjYyNk6C7Qjg1O0NDR8Kqq7ooDgUCBxbJkomIjZs9GA4L
+Dhcpx52QjIyQmrFJMzNJqpaOk6gXAwAADTSXiYSLmVAZEg0QDxUbOaiRiomLmrUwKitB1b+3
+v7/Lq7DMIQ4IBg8snY2Kj6ksEw8QHCzQqpuTkpOcq0k4Pa+TjZG6EAIBCzWVh4WMsxgOCw8T
+FhIYIr2WjIuRn10/RaudmpytxjwzLPC31zIRCAcOOpuMjZJfFhARIDLLuKmjmJebor7bNlmm
+j4qRQwkAAAtPjoGBkWMSEhUfHRMPDzKmjYuOqSAZHL2cj5OYrO1xS9JN2jUgFw4RGUykmp20
+NBobHCxLz7aup6Ogp6i1ur3Fu6WXkJhBDgEGFKSKhIqlJA8TFR4ZFxUd7aWXm5+yrayrrsjh
+fq6gi4yuGQAACC6Ng4afPBUdQCslDg0TvZWMl0UmIb6dlZ2ju6+kpab+LTaooakeAwAFJJOG
+iqAdDxg4+H4sIz6pko+bu2FAz7q8USg8mouKswYAAAynhISPTxoqVKgoFw0bvJSNnUwaLsib
+najM466noLjNOEM/W6+pMhcNCBpTmZWc1jcxJ1gtMTRXuqalrbg6T3nEz2m8louNUgUAAyqQ
+gomfMyRPPzsTFRxqnZ22IyUyqqSnvde1pJ6rukZHSnlSPrGjWR0JBRXRk4+cMiksPccxLjre
+taa2wMHEsrS1uc71mYyLpgYAABmWhoigsEDLKBUOEDK9pVYwKV61u9FJvK2co6q1vL7iVTk4
+RJuXLAwACDWbj5q8RKrHPCgdPaqnw89Wq6Sv22y7rLCoj42TFQAADKmPipumscYoDA4RPr/G
+Lytqva9GR9ain6OrrqGkrko/UFLKlp4ZDAIMWZ+ZnJ+qnSsYGym1pcooybSerTwv46253OuP
+ho4UAAAKqZKTm5OPnDAHDR29xCYcL6qs1SM2qZqgu8Sol568KjO+vryYmR0MAQcsq5ucl56b
+KBIdL7u8Nx/Ir6zEdsKoqLnSTJ+OiZ0KAQken6WbmY6ZOhEKJTzhKi1oqa4tKTOtqa/DqqGl
+sFB85Mg+K8+OmBIGARSypq6djZCZFAwg1L05LUSerGY/r6Cgxznkua2ul4+oCQQJHKOunpiN
+ny8PDjEvJyBRr5+9KjvOs7yxrJ+ku8e/vs1NJkuOmQ4JBRO4draWhpWfFxRLMB8esaqa/SlI
+uruxpaGctNnJvjhgnJsTBwsSsN29noyWuiMROygcHsesobQuzbvCXa6poqu+sLLJPC8rmose
+BQsNRkE+n4WJqs8Y1kMYE3efqKQ9vLNFH1yvqKe0qazKKy4pLkKkrhUYEBtQQa2ajaW0JR4v
+Hyg8qLClub3N7+3brLeyc8fsW9qsj6gJDBIjuUibjoitQiUeLBEVJqi2t7e4s0UvTK67t7ms
+rcZHUGs3LS2ZkBMJDhNEKNWahI6vxSfLHhccqZ6nn6utPygiwrG0q6upw0csRDcmOZeeCw4O
+GcItrZKFmq1fKbYfHCmkrrSwv7EzJSnLU3fXu69mSUBPO7GZmxsKFhM7K7qPiY20ty5GGxEh
+Zqq4o6Sf1SotPexqtKuhr8XlTzosIx+jkxMKGRVeKEmbiZXCqSq/JBcjsLDIpK6lczE9s9Xb
+trWvXz9JUisvs5urDQ0WGzYio4+ImLe3UkYSGCe5w7Ohn6I6MjJcS8yvpKO5w/ppNykizpS8
+Cg8XHTMdqY2JnKKqxdsPEiPXY62inaM8Re/jPfK+p69z3FE4KCvbnpoYBxoSKSFHkYqNppnR
+wBsMFyA5OaOfl6XDur3GYrixn6q/veo+LCo2rZ4yChUUFyUcopCLm5mmwUsMDxMiLb2gmpis
+rL/eP1HXr6y6sMLkPDNUuKa5DhEXEiscr5eLlJiauq4SDw8cKkKsoZKdn6euvN1tzKzDxd9I
+NScmM+u2OA0aGRwvI66bjpqWoaatHRkSHiQ0RrKcnpuipKmy2s6/T0Q1Mi4sLD7PukUeLSUs
+NClJ4a2vqKmmo7a6UU1GQjs2QVDJyb60rq6ytcPNdT8zMDIzNTk4MDo9NzoxMkDrwLmuqqmq
+rre9ydtMNi4sMDo/Wc28r6qrqquusrzN6kw+MyYiIB8hIycuTM28s66rp6isr7S6yGY9NjY4
+OT1azLesp6Khoqiwv95NOi0hHRwbHB4iLEH9wLKqpaOlqKuxucxLODMxMjIyO2S+sKuop6Wp
+sLzVTTktIh4dHh4fJCtBy7erpp+dnp+or7fDaz4uLCwvLzlvv7Krp6Wipq6/5UgzLSIbGhoZ
+HB4jLk/DsKijoJ6eoqaststLNi8tMjU7eLytpKCipKartsxNNC4kHBsZGBocHypQxLCrpqCe
+naCiqbHA5UMuLjFAZywxraWdnp6aoKxIN7yzSwsFCQsZExw8nY+OlZ2WoLghFxclLil3r5eR
+l5ydoay5Qjs7NzctKS23np4jCQwJGBUaxpmJi42gnbojDwkPG1VbpJaNi5OdrKu5wPjdx11E
+LyknO8O2wQwHCAccGcqgiYiKk8CsIBwLEh7CoaSVl5CcplfJtMm04rrM5jEnJCQ8cbs3DQwL
+EyUvs5uIioqdvU4nHA8bIauinJ2bm6WvNVNKu8HT43NgMC4oLT28rMoTDAoOIy+1pY2KiZbD
+OB4cEhUZUamcm52dn6PVPzZQwri//d9YST08ONWvrCQNCwsfO7Gpl42LjKHNIh8ZGhgbMMef
+n52koaq9WzpJU8pcWDo7Pz0/Z7m1+xkRDRUq8q6omJSOk5quNyofIh0fJDi5p5ydm56jsMpa
+NzYuLy8tLzI+0K6uSSIeHys8VubbvbSpp6OmrbPByU88KyktM0122da/urOzvcDN2WBMPjo7
+OD5HNysrLDA3SEM+V8qvopydoKKkqK28QysjISYsLS82S8y3r62qqqustcpXPTEsJx4aGhse
+KCwrNVW5p5yam5qbnJyfsu45LiwrKiYqL0i9r6qnpqeqstJFNy0nIBoXFhkcJSgrOc6to5mZ
+mpmZmpyit2w7MS4uLSouPuK9sa+tq6yuuMbpSzspHhkXGBkeISUtTbypnp6enp2dnqKxy04+
+OTMsLC44Zce1r6uoq6mvv+FLOSwfGhgZGx4jJCYy4LSlnp6cmpiZm6W/aUI4LiwmKTFC/cm7
+u7KvsbbF/F1iRSwfGxobHR8eIS1qtKWen5ybmpyeqb7abFk/PTMzOUjt1MC5rqurr7zSVkEy
+JRsXFxkbHyEkL+2ypp6fnpyZmpyfrb7Sbz40LCktNUZezr2zrKyvxM7F110uHhgWFxofHxod
+K7uknZ2emZeVm6W5zd9IOykwQr2trquurK+7XUY9MjE+yegeDQoMGSc6RKyYkpKjv0hqOSgb
+HVGompuXlY+RnsVKWU1CJiQoMy4pICTon+gIAwQVSrqunIaEhJhVKSsfDQ4QQ5+WmZaQk5tp
+Lju8yt5E+b5KLikpIS5LtywNDQ4mRO6sloeKj7hINiMTDxwypaiin5qerOJDzczhQ2VFTS4q
+LjItNr2dZAoMDSNVyKWOgouPtshAIw4MIjusrqWfmai/393Bd/nDsOhALjU1Lx4p0qU6BgsS
+KjI5o4uDk5u4rUAXDhvNy6ukl5mhZtvDUjpKubTLOGlPPSolLE6utwwKGxU5LKSTho6emsH6
+FBISQEReoJuWpK+/rFhD2aqos/tK+S4jJCkjMb68EBEZGTcwqZiJl5qbuPQaFxgxJ9annZ2q
+rLCvR0RqwtPiUedNMDg7PEG2pBkLHhEmKbWWi46aj6x5FhcUHhsqpJ6cn52krjpJ5WFavrzC
+fDs9NywkPaglCB4YHyLHmZCPpI6gxCAhGhocJauupZ+Ynqe5ucNrW728R0tBOzAuME22Lwss
+HxIfP6Whkp2NmrftTCEUHBw9Wa6emZyanam38UhHNTNJNy81PT09uFYOKRwPHCzItZWcjpih
+sMEjGB0YIy3Fp5uYkpWco6zSQi8oKScmKDM72a0fHcgUFSA6J7CfnJWcmZ+uKjYdGB0jKUyy
+npeVk5Sbo6tOLCQeGRsdLNYcIKYlJ2ixIW2q5q+upbet77pILT9GLDbFwLKinZybnqKv7jss
+HRsbGiQ8GDO8Izuvui6cp7mmna+6rU1CLzAvLio9SWmupamfnqWmqsBNOyshIB8jLxwe6yIl
+ysIqq5/Kp5isx6TIOFZRKDFRNDfNzcevqailoaOpq7XjOy0nHx4iHxwpMitJyP7gqbG+qKvA
+tK7HwrleP1Y4Lj4+Nk7n376ws7mxusbD3U9GOjU4NDE1MC8xPUlvzs65srCsq62vsr7H1FpK
+QUI8PTw8P0JTf83Cwby6ubvBzdpbRD04NjQ5OTxASmRf7tnPwr2+vb2/xc7pc1dJSUlPT159
+7M7U1dfW3PDta19MRkE/Ozk8P0ZNaNbMzMO8uLi3uru8wMXGz+3xcmdXUUdLS0dMTE9PT05O
+TUtJRkJETU9hb+Da29XV4ujZ3dzXyMbBwsfGzNTc6V5aWnBhX3v8dGdrY1lVVmJmZ19ZZlhO
+UFJXVV9f+ufl1dPKx728ubq9v8PK2/pjTkVKR0dFSElKTEVOW1RaVVBXXV5obHB2aW9nWFpX
+bNnTzsXBxsTFy83O1d7l7mrn/vn1e917eVRRTEhTTExXW15baV5dWFpeVmRvfPLU1dvc2NXX
+2t3r9Pl5ZuPta9jl29ff5nphZFhealtnc2JaXmJfYmVkauDq7d3o4vNsamTz3d3Vz9LU6uHY
+fOZ5+f9y8Hvt9Pbv3+Dj4NzY3d7sbXFrXlRtcGp4aV1ZZV9WVlpiX3F9eO7r+n9tXlpTT05Q
+U1phZ/HZ1c7S2NTY1uPc0t7m4vv+7uXv6u7+7Wjl4uDm3uji7GJYTUpHSk5QV2d24d3Y2Nze
+7Otx5fdeXl5WWWdjXV915uLc293qbvDn3uB3b2VseWxeauDa1M7LzdXY6vjs+2pYWl1eUU5W
+UFFb5Orezs3U0svRzdDP2+HxX2VmWFhVUE5eb2/4/vpzaVdcXVthXF9mfurf1dfW3Nz1cWlf
+XFx5afXyaGhtZWRkb+vj19fU1trf5u5iZV5iZ2NmW2d+de/b3t/r4OVyaF1YVk1SZF91X3jl
+5+fj3+nj393naGBfX21uc3JfXnP5eGd26O3s6ev08eX9bmz7dPrc6ujd19zf6Ozp5O3m52pq
+YWlpYl5eXWLu49rT0NPS09Xc6uNka21iaF1PWe7t7X3udm9vdnxjWVpXVFlrXVNkZ2Zq6/Xi
+9HX+dfn9bmFmdPPj5Ofk9/zd3tXU3N7b2uDg2eJ3Z2N0enT+9ujf3trj+2FOTU1NS1BPTlhh
+Z19jafTp3+PW3u3f7Hdxelpm/fXl39Pa39jT0tjX3+/gZOxqSnBYXmhlTV9fVXl539/VenF9
+V3zrXG3o29LNzcvW397R2dXX59/r/lVNT0pARkxRTVJqetbe2MXXzFn0Wld8VFhXXmtu73Ht
+6uHPxtB+09/Z+2Vd29z9Ze/e5Ovo+HrwamlUT0paZlxrYHzy7M3O4U5L1Z6tI05L5ygsJlqm
+OsG5nrizRlbdKSEgPyw5Premn6SknaeszNtFOCQeJyUlJr6jUS49Ss9GNDWqnbmvq6Gqtz40
+3CwlIzlK7Me/oKCmr7G730IsMC8tLC4vLC2+l60fMC5RQS8sxJGfoKOdnq52FxscGRQYKDip
+pKCbmp2uuUtBPiwnJzIxOTk4Xe7QbNm0nZ45LDL6uL2+y5qVmqGwtnlPHhUVGB4iMj23oqCm
+rrPOezcqKSksLTU3QElUV1/Vpp1MMS4v0r6u2qScnZ2nrH7LPiciICQoRUu+qqahpae8x1E1
+LikrKC4xO0E/R0VmZHDOp6C+dzk6cb2z2baxr6mvvkVBMC0rIyMlNT/xz8C0sK+/xudlZ1NK
+Ozs5Pj9CRkhRc8/IvqWZp/1AMEjltsbPsbGoqa3VRz4uLykoJi0uMj5AXFzc18XAx7/JyNPk
+X09MPz07Pj9RadzJzMW+tKmqt/w9OEnKxL68vb+/zn5VOzg4Nzg6OTc5O0BPdOTez7+9ub2+
+w9LP1MzMysjJxsHFxsnP3GZbZWnr82RURUZJXXhdWk5NVVpRT1NHREA9PT07PkZKbN7SycrJ
+xr+8vr2/wsO/wsnT4OzwdFVUW1FXWFFYXlxYVVxoaP7c3d/T193u72BcV1BURkRCP0VMUFpf
+WXZ43szJysjFxcPAwcTMz83c2/FmY1pQVFFHQ0VFPz9DSk5MT17u29jc4tDJxMPCv8zW0Pts
+XVVWWF1t8fbu3dHNyMO/wsbExs3I0nZqVE9LTkxFQkJJTkhIRENKSUhMR0pRV/9s4dPZ29bN
+z8rJ0czLzs3N2dz44trf2t/Y29bPz9HnbmZdXFpUUUxLVFFMV09OWWX8/uZtf2NRV3F7XFVc
+c+vYztrr3NvTzdbf5tzY6e1qXV5RWFpeZFxdb+bl6X3+cfB+b+jnb2Nnafjn4uXq7Ozj/Vpb
+WV5tZ/39+d7T1NbSzdXMzM/Nys3X7O5iU1BHTlBUbGhkXFhRVGbzY21xYnhgWFVPTlBoZmL1
+++Tj59/o1eHl4OPU3+Po5uTd5+bc6N/l29vu2tzc1+RyY1phVV9sT09iZ2lla/VtZWxfcur8
+4W1bZl/6bFpnbvrm2Hpl++F77NPvaWNYY2rs2NzhbfL6fOfh3t/i39jr5OxebV9naF5rZ1N9
++FtybdzX087c19n1XXnf1dLecfDP2Hl5U0dMYvz2YltgW/7u7ub9bGZnZl9WV1VUYV5dbVpZ
+8+/q39z9bebd1NTc39/Y1tPW5Hfp8Xvc3Nzy7+To7vVZWlFn7+puTVBR/mluVk9hfd74ZmFq
+dmz9d2Jhbm5Xa2B88HJgZ9/Z1NvieO3a3s/Ydfnt3enZ2+z9eurd299nbWnn/1BQXGl79Vhb
+WFNPV+zf2tLX187HztHOz91+al5x4+by721Vd2pYZ2NqaWVOSEtQW2RaW1VXXVx+6+x99f/s
+3N/o5d/8393d2+Lf3+bf1tDM1dTvfnHm5t3c5mVeXk5eWFtkX15XTlhhbGzuY1RUU19re+/R
+y87T6H10bl/6xr29zFIxJiYoNruhm5ujukIrJCMkKCwxO1zMua6rqqurrKyvtrvHzNHhRi0g
+GhcaJ0qxnpyfprlPNCspLDI9YMu7sq2rq6ytrK+zvMxhQzpAVj0tHxcSExw1uqCbnqe3dDUn
+IiYxdbSloqGgoqaqsbq+vLrBwsfIyk4zHhMODREdaaKUkZObri4aEQ4SHTqxnpeUlpqepq6y
+tLazraqqtlcmFg4MDhQk6aGZmJ2vOR0VERIZKs+kmpSTlZuirrrEwL2zq6WkrlciEw0OEyJj
+pZmXmqPdJRcRERYm5qWZlJOWnKSwzEpNwqyfnaZWGg0JCxAlxZyVkpquMxoRDhAZK8iimZSU
+l5yirrS9vbyzr66zx0ImGhQTGCE9tqmipq9fLB4ZGR0u2qqcl5eboq28uq2in6XEKBcPERk6
+qpaSlqk3GA0LDA8cOLainZucnqSjpJ+enaClrrzReEI3Jx0YFhcbJUS4q6av1isfGhsfNMKj
+m5eXm6Kuu7OknJmdrycUDA8aQ6SVkZWmOhkNCgsQGzXBp6KeoKOlpaWioZ+hp7HM+2xtVjcg
+GA8QFSN1o5qZoLctGRMTGyy1npSSlJujt87etKGYk5qvHA0GCRE6nY+OlqorFw0NDhgjRsGt
+qaanpaSkoqKkpqiwtsK8tbfaNBwRDhIZM66bm564Mx0XGSA8t5+cmZyfrrm3vLuxp5+dq+wd
+EA0PJcWclZOizygbFhQWGyEsUt63r6ekop6dn6GkqbTAv7yvv3wiFg4NGCmvo5yfq0olHBse
+KEjGr66qq6iqqaippqmmo5ucrEwYExAVJO+empq0OhkSDxYeL1XnurqsrKWqqq6usLWxsKmi
+n6nLHxUNFia1mJOVsDQYEhEbJj/jvbKxq6+prausrrCwpJ2apNQaDQsSOaGNjJCvIQ8LDhUi
+LUdlx7eqpJ+foaOpqq6sqqGepr8cDwoOIsGUkpSxKRUQFRspOkZJ0r6pqKSqs7a9trGelpGb
+4RULChR7mYyOmUEYDw8XIi0yLTNeuKCen6zFycetpJ2alpurMQ8OCx+3l46UnykVDQ8THCEn
+K0exo5uiqbu9rqadmpSSm7obCwoOP52NjZbPGA8OEhkeHyguv6WamKS4TuS0oZmXmJWcrS8O
+DQser5aNl6kfEg4SGCAgKzy9npuYpLzoz66fmpmZl5+6Hg0ND0yjkJOkOBUPDxIWGRkjNa2c
+mZyyzv23opqZm5uYnasrDQ0OPp2OjZvMGxMRFhcaFx0wtJudoLL217GhmpyfoJmZozQLCQkm
+oI+OoksaGRsfGhcSHDetmp2m2UvNqZ6ZnqWjl5OePwwJCyybjo2iVB4eHx8WEQ8ZS6eXnKjA
+bryopKStt6iakp1gDgkNJJyUkqn4MjU3KRYODxlTq5+nwN7Nsqelp6yrmo+Qph4IChBlnpqd
+udTdxj0cDQwPIdmtqK6xrKelpKmqq6eclZaqMRIPFy6uqqy9wtnTOR4RDxMfPc62r6qnpaap
+rrOxqp6Wl64uERAYL7y4tcS6t7DVKhcSFB0sP2vHr6Senp+kqKqqoZubqkEdFBojRHPpUe3Z
+x8RCJx0bHicxPli+qZ+dnqCkp6ytqZ+esj4fHCAxfF1INTc/3uE/KCMjJi0xLzI7cb+zqqal
+pKSkn56fr04uKCksNDMuKisuRtnM1eXe2Ma/w9hMQkRR3MO+t7WxqaGgq7h8RkFOOi0lIiEn
+N0RJQz5D6svXTDQrKS04T9m+r6ifmJOTmaCtxVtFLyMdHB0hLTU2MDM0QPR/TDw3PFbGuK6s
+q6ainpmWl56ns9dNNiccGhgZHSUpLC0yO1PWz3NOQUVd18W7s6ymn5yYlJSYobFPLyMcFhQU
+FRgfKTA8TnbJuLm8xcvLxri0r6ypo5+dnJiXmaCwTSofGRcTEhEUGiEtPFLQvrCsrK+/7EpM
+69W/s6qjnZuZlpaYnq9SKR4YFBISExYbIzRM3Mq4rqytsL3bV17bxLm2sauknpybm5udo7dM
+JR0VEREQEhUdKD6/rKqjoKSoq7fbXk9GT/jCtKukn52cnZ6hpa3fMR4YExARERQWHCY507St
+qaWlpqmuvNVaQkRlyLuvqKOfnJubnaGpsn0uHhcSDxASFhoiLEm7qqain6Cjpqy3xX1GOzc6
+Rfq9r6WfnZyeoqy3xEAnHBQQDg8RFRsjMX6sn5ybmZmcnqOuxWk+MjU9SF7Dtqmhn52eoqu7
+3U4wIRkRDg0OExkfLFa5opmVlZWXnJ+lsPY5KiIiKC44asqzo52dnqGqudFQQzMcFhAODQ0U
+GzHNrKKYkZSWmZ6pscs7Ly8wMDxS2rqvp6KfoaWrvWk3JyAhIysgFh8dFBAeLCi1opmUjpCT
+laGxzTMdHBwYHSw1U7CinZiWlpicqLRjKyMdFxUXGygfGE0fFRw1Ni+YmpWPjI+RlKSuUiUe
+HRYVIR8gTLytoJiZmJqdo7VcOSgcGxwYGSQ9IRy8HhUlMDBKlZqOjIyNkZmmriwdGxMOEhkc
+JlK5qJ6ZmJuip7dFNiskHh4fHh8mMfXCJWasGi9at8ifjpeMjo+Xn7F0OBoXFg8PFh0fNcy5
+q6Sfn6Koqb59UjUqJCMgISMoME2uxjKeuyrVybdVl5yYj5mWn6tXTiUUFhQQFh0pOsiuop6f
+nqK0xfIyLSooJyUpLj0+W8mxnq0qorYgTd2q5ZaXl5CioLZNIB4aDg8UFRsoPtWtqaCbnqGl
+rtDeRTMvLCorNDFB5Gnt672om9YtnC0cNMSuxZGdlJGjpLNMFxUUCwwRFh43xaiYmZqYnq62
+zToyLCsrLzdjzXHPy8fqYXi9osocurseMf6ht5eYm4+mu9pSGhEYDw4RFR421LWblpuamp+2
+yEc1Liw3RlRdubO5tbq8z0kzSK2nHh6qJis4p5+elbGUnkUtLSENDxATGxwm7K2xqJiboKms
+sr9oPndYRU3KyMnO3cLVY07mXDszXZ2iHC6uOj44q6OUoPKWoT4eJBsPDgoSJB4fxJyenpub
+mqbQyLDUPVS/s7vHvqu4R0BVTjIvMz89L1qcmyEjVDf2KDPDlZxBn52rPh4aGRwNCyBFUcam
+mZGSn5+cp2Y1OUziT1mzo627yu9ZLyUjKyolKCw6+p+YPCo8Kz8rMDmckq2foKa0PBsQGRUO
+FiE9sZ2dmI6Sm6a3b0tIMj3WurCqs73JSDAmIh0fIyMrOEj4qZOZOyseI0k8NzGZk5WUprG9
+7RkMCwoOGh8ss5eQjY+YnJ2pVDcuMlfBvbWprbDNNCMfHx0fHyMvUcy8oZKadTEbHjntTzqf
+nJWPmrA4KxgRDwkJEyjPoZWRi4iLkJy2MCsmISItQ7ejrL1PNSkmHxgXGB4sQ1W+mY+bry4W
+HjS9xKyfp5SQlaVPHg4PDwsMEhs5no+MiouQlZqrSCcaGiAy4a6mrK+30UApHRcXGh4pMj/g
+npGXnVcXGR9aup+erp6cmp2qMA8LCQoRGyM1uJ6QiIeJj529PjArJiAfKG+soJ+xSiskIR8e
+GxodI0GkkpCXpyIbITqtn5qoqaWln6bGIRIKBwsRGyxTuqKTjImJjpu2NyUkJSgsMT7Nraus
+vD4pIR4fJSkrQ6+fnJvAJx8fOLSal5qetr7ByUwsGQsHBwsYOq+el5WRjo2Okp9oJBobJDRo
+xcHKyc3XdT8vJR4gNr+xrmwfGh00qJONj5emyGNIPC8fEQsKDRc5rZ2YmZqYl5aXn7wxHxwe
+K0nOwczcdeTKxNDbvLm/6SsZFhkptJeOjI+ap78+KiAWDQoJDBg0rpuWlpebnqKqvk0vJSYq
+M0zm2e5QQt+mmpaYtx0PDBAppJKNjpafq7nK4C8XCwUFChlcnI+Oj5SZnaOv/yoaFRQZJjpc
++b+qpJ2g2iwbGyTJnJKMj5Wfr89TNx8UCwcIDh7AmY+Nj5GZoK9wLxwUEBEVHTC6oJ2fzS0e
+HSzRnZaRlZqeoaKorFAfEAsMFCu9npiXmZucnqa9MRwUEBIVHTHVt7DJKSAbITqvm5mWnJ+k
+pKirxiwbDw8VKMmgmJaUlpaanq5uKx0YFhklP1JLHxMPFCfAl5KSmaWur6y0xycVDQ4WOqSW
+j5KVmZqdoK9NKBwYGSJGxsoyFQ4NGkyekpSYqLS8sbjSJhMNDxxanZOPk5WYmp6mujsmHR4q
+X/FDGwwLDiSwko6Rmaiytbb1LRQMDBQypJSQkpmcn6OsuU0yLSw8u6u+SRcMCw8fzJ2dnKKo
+qKa0OBwNDA8hwpyVk5SYmZ2kvF8xKz2tnZ21HAoHCxvPn5ueoaGcm6E/FQkIDiO4n5mampqZ
+napOLi9KxMW8qrC/OBMNCxIet5yXlJmdoq0uHhAOEB9doZWVlZyhsslHTEdMuJyaqGsPCQoT
+J76jp56cmZmeORYMChInvqKYl5SUmKTXLCk8S91SYK2mpdQYCwoUKKabnZ+hpqa4JRUMDhjv
+pZiXm5+lsufXWL2nmZ++IwwMEyvIqKWmm5iYom4UDQwPHTDUrpyXk5ei5TUvSbexr9c+x52f
+qR8JBw4pu5qhpqOeoaUzEg4RI9Siop6kqq+0ybafmpxeHQ0RINimp6eupqanVh4PDhYkSd2/
+vamin6m+SUi+p52eo6/Heq2lyS0RCxAus6OftLyxsV0vEw8ZMrmjoqepuvgzNs2akJlpGg0a
+Uaqmt8q6np6uJBIMFyM+6O69p5ucplkxLsmpn6KlqK/E1arMLRwMDRhorZ2cqaioxisbDhUi
++6qcnZ2hukYsN7aXlbkcDg43qpyipaacmKoqDgsMGiUzP76ilpeoQSUtR7auqaadmpy6RbK+
+Ox8KCBNqppqcq6Wgrz0eDRUpxKmfpKGep7lCO76XkaMeDQkd5rGvp52Uj6UpDgsNFxgbI3me
+kZSn2jpozclc27KfmqLNNLOetjUNCBI+pqmktqGWmVQeEBQ1zMDTsqycpeMuOVupl5iuIBYQ
+HyksQKuWkJKvLxwaGBcRFSW4mpabp6u2vUgwNL6noKSyzE/CnLIcDwgUMq/cr52akpo/Gx4Z
+Jy4vRaGbmqZINtu4wa6rm5fMHREUJEDJt5iVkpvFHhgSDg4PGEyel5aeo6u6MCQkN7yqqKWj
+rLs/vasnFwwNHcitsJaVkZSsIiMdHiUkLbWcnp+zys/HRE1orJaR0xkSDiAjJzKdko+Ws0Q3
+HQ4LCRIvtqWZlpKVp1g1KzJDYLKfnJ+oxkVZrzAUDQsdNX95n5GPkqbeX0sfGRQeM8O7q6Kc
+n67My7/iyLiqqS0ZFRcoLEPSlpOSm6jsPR0PDQwVIDnIoJuWl5yns345NT3rva+rp6/HOCMl
+YzccGBMmRUlLrpmUlKKsq647IhocIyUoP7ignJ6dnqW6TTA3TWk4FxoZICwmSLeanJyloqi5
+LiIdHB0bHic9v6ugmJWVmZ2nsdg8LCkpKSsrLi4xS+VJODcvXVlJYMezs7a4r66ts7bAzvlK
+ODk6O0VR5svJw7y4ub/S3+tuVUQ8NjY1NDQ0OUBV28bBuLO2vcLM2u9tYk9WYF524d56XmVY
+U1xletvV2NHK0d1sWGFUSkdAOjo+PUv40szCu7eysbK2vsfebWRaS0BBRktOWVdf+3t45e1Y
+SEA8ODg5Oz08P0NKYNfKv768ubaurrO0uLy8v8xhSUM+P0RLUk9PXE9XYE1OTUpLUmx8ZWh0
+U01UTkhLWerOv7e2tLO0tbS1vMnZeVVGREA8PDw+QE9t9eZvYlFPTUVBQEJBREhERUtYVWnf
+1MrBvb27t7W3u73DztDOz9jU3/n/dFpJSkdDRUlOSEdHR0JBSEJBQ0pUXuv3ZHne2sW6tLK0
+s7a4ur/YVk4/Ozw/QUFJTVJaX25SRENDP0hZWGNzc+XZz8zLz9Hc1M7GxsbCxsbAvcbS+FtY
+Tk9YTEtLUVhu8nNmVk9OV1xfXmd56vJ2Xk5MTExe6tDLyLy7ure3u8LaXkpESkY/PT9ES2Jf
+XltaTUtOSENBRlVr7tra2dnV1dbKy83JxL68uLm9xcvP5m1fYFlMTlNUZWFWT0ZHQ0FDP0BL
+U2zw6Op0X1hST05RTlFl2cm/ura2v8S/2M/3emNsaGrj2fpmamFqb2BITEtPYm5aXVpk9m74
+VVJNSm3YysbFvbzCvr/Axd5wTUhaYGj7919YTU1aSEE+QEZy3Gb/9HDhX01JSkhBRFDXzczL
+vLe+xMPHz+NfX+fqyGBXz+7N3eXlXlVURE5s3Tw8PkRrQTpO9Fte5cOtrbe6uLXAV0RNOjAv
+NENXS1bNtrS7uLW/3UxIRTYtLT/65DUvSExYLTJeur/FqZ+ZnaWoqLY7JB8iHhkbLeS5rZ+X
+lpyjrbNkKB4dHx0bHSY2U72uyL69U8k8VrympJ6ampihtuA7IhcREhgdJES5oJmYl5ecqcw3
+KiAbGhoaI7yuJCknKcAlMrSUlJeUl4+aZy0nHBUPEidUdq+dk4+Smp2lbyIaGhsaGiU9VVA+
+PEE2KCQmK7ufLj60z5mmqKKOk6WasarDIxYcGxccGy+6uLyonqClueBoQSwtM0LK4PjT9z0u
+KyouJyS5lr0q4jyiofXdm4ymoarRri8ZFSMiHS0zuKCkr6ejyEcpHyg0Ly9zva+35UM/OCot
+Lzg7XZyXU1pIOrw9PS+WlayctrVvPxwPIxseKzy9p5iipJ+qsdpBKzY/LThP1dbDWThKTDw+
+SERZ1X1r6FVQoJVSL0U2vdToJq6X17ZKMC82PhQZIB87UcrLoJifnqq1u7bWL1fc27zFfV3A
+6D5JNyokLqusMDIjMbeqrzucmp2XukgjNy4XHRYdPr6vv6Wppp+6QS01MTVRNUS7rq+7zT48
+VUI8PUBK0shSv6KnxEkkIjy5t7elu62ep88qIRgaJx4eJzfWqJicnp6qsbrQNSouKztv2sG2
+tcHB0UtCNi0rLDnFurvOPUFMta2pqN3D0t/kQDYeHiAfLjpLUsuuqJ2bnaWzyz07Pjs8O0Jc
+ysTCvtpqSDMuTrjBuNwtLzfFr52bsrhOPzs/QCsqHh0oOs22sLm+tru1rrLLRTUrLDg5PUxT
+VezNw7a0uMS/sru60EE5LDQ/uaKgm6Otv0IxJiYgGhoaHidAuqqenZ6fpau3wW1BNiolIyMn
+METet6+ura+3x/hFQE5SatdoTUdLfrqno5+hrso9LCEfISAkJigrM0zTsKWgn6Gkq7XNZ0w/
+PTw3Mjc4PU3mxb69vb/Fx8PGzvRDOThM5Mewrq6wtslOOy0oJiYnKi84PmTIubOwsLW4v8bN
+195xTklJQkE/S2njzr/K1ftNWO7Dv7mzvMThXEI9OjY9Q0xXYe3m9HFj2MrDvbu+ytlhTEg/
+OTg7PEJb49jJx8HCx8zR397qV0k/RlH1yr62tbnB2GdJPjw9PTs/SVNZX1lYW3rm1cW9u7y8
+vcPNc1VDREZIVubOz8/V4Pl3X09Xb9DL1t5/X3PCv728xtJoTkI8PTs6Ozs/SFBVS0hBQkdO
+4Me8t7Kxsba/y/JbU01KVVxcZ2poXV5OTVFc2sjOzOJZVG3Yy8HK0N5qZU5KR0VIRExbUEpD
+QD08QU3yybivrq2utb/YVkA9PENNU1t3e/7vWlZVTUdGUG/byL7Ey9rubHDf6dnIysbV7PdZ
+VU9PPjg4MjM3PUvpw7CrqaanrbW/5k5APDk2OT5IWWTkbVVYTUpd48fL39tTS09e39O3u8TO
+U0M0Njg/UF1gRTcxMjdKxbWso6Gjp624zVVFQTk3MzIxNT9GZdPM0M/tfvvazcTPe/8+Oj9Y
+2MCzubvVSz85QU9dTEM5KystNmi9sKuoqamusra930w9MSwqLTJCesi+v8TN3ci1sqy9PC8j
+Iy7VrKSdqblwOjYyOjc1Jx4fHy3gq5+bm5+kqq2ur71OKx8dHyc2Ud3U419i38a+w2s5OlPc
+rrFUPCcnL7OclpKcquExLSsrIx0UDhEaM7CblpSVmJianaSzOyQcGh0mMD1KTFjZyb7FXzsx
+KywzOkhD4aymnZ6uwkzux6eirLQ5LCcpKB8eGRocI0O2pJ6dn52cnJ6kukkyKS45RExBOTg6
+OUpOPzQrJSRSq6ag7B8eI82djpGWnbSrrbD7KhEJCg0bN9i1qqWYjYuNl7UrHx0eIBwbHSZV
+pZ6frFEvLCstLiYjJSxRppSSlJ8+MzfVr6eyU/hLsaiuTx8RDREVHiouRLadj4uNkp2vwspV
+MygeHy1Mvbl6PDQuOUY3KyMiMK6cm55IISs8qZ2dwF9WcK2zXSAUDRIaIjAtL9Kfk42Nkpuj
+qqy9RSkcHChCcfZRRVbQu7n5Qy8oNPqrprjyKCxGtKKlqcm+v7DANh0UEhIaHR8kL86il5GT
+mJqdn6i/MSIfJCw2PkBE2LCoqK7NNzI7/6+6OCocJ2mnnp+nvq2zsc0tFxAPEx0fIyo2uJyV
+k5WZnZ+mrs4zJyYoLTQwMj/fua+ur8NeaGPCsPItIx8yzaqkoqurqLG5Qh8UExEWGx0kNMei
+mJaVmJ2gprr9NCgpLTE1Nj/Nta2opqmvtrm7tbFDJx4cK0m2raetqaKqszocEQ8OERMVHSvJ
+oJeSkZOXmqG4TS0kIyQkKjFDx7auqaanqrK4wLar0zkmHylGtbOstK6kqbRFJRkZFhQUFBcf
+O7ujnJaSk5Wbpr5WNSkjHyAnL0HOua6npaqsrrKpsjYpHyU31sfMrq2hoanNOiseHBgUERYZ
+IC9PrZ2WkpKVl5ymvz8qIiAfISQtRMivpaGioKSipb86KigtMjQ567uurbe+zVc0JhwaGhka
+Gx4pQbmpoJyYlpeboq7OQi0kHx8jJy9MwquloJ2en5+nusxSOzkzLS8zNT48P0xGPTs2Li0q
+KCkqKzA5RFzTvLCtqqSjpaaorbO6x+pPP0E8PEdSbtrHx8bBxtDidFpQU09QTE9USj9CQjs6
+OTYxMjQzOTxCQU1q58W5sa+sqKako6WorLC3wd1ZTz43ODY1NjcyMjc2Njg8PT5KfM/Hv7y9
+v73DzN5bRTs8PDo7PUFDSFdl6dfQzcS5tK+urKyurbC3v9RlR0E/Pzw6OjY1OTo4OTw9QExt
+2cnDwL+9vsTcXFFGQ0JGT1dcd2BbXVpOSk5SY868ubSurrCxtLjG31ZIQD9DQkJISUdIRUA7
+PD1ETF3o2czHxcXFx+pcXWFZW97Tz9DMy8zQ6lVGT1Zl6trNxr++ubq8y+R0UUlGSUlEQENF
+QkI/Ozg0NjpEVenNx7u4uLm/yNf1++nn3s3OzcW/xcnO5VhRSUdLTFll4NDMzNnhbU9NTE1N
+UF5rZfbjZ1ZNST8+QkRQaeTUysHFy9njdk9UV2lr48nGv7y9xsjV8GVTT1RPTm3o38/O3O9o
+VUxJTkc/SFpn6NjV4uvd3mZdbe7u2cXDwMHH0/N7cF9WT05LZO/w1NfoX1FOTUxJTE5O/NrW
+2eHmXltdWVNLTU1c7tPOzMbMy83U2/diXmFo3dDR1dzM0t3T3WhUWFlPU1xbZHrt+Xp2dmRY
+W2Fo9/Xwd29fW1lVVU5TW1JYfdPNyszP0tfZ6GdmZlpSbdrz7tXP39nN53Hhe1Rh7WRvb2rv
+2PP02k5JX0dKb/L91M/MxdDUzt7h8nBz5Gte81Zi4O3l51lg309L22pV795W9P1N7F9WVltY
+X/VZZeJsZ97iafx7U2dv4M7e2srPydjF41FkP0tISWdO1tT3ur3Dw9tubexHSuc/SV9GaGnL
+SkdmQ1xZ4fZywszNw8vHaW5WSuBNU89PdMrl2snNYFfaREf0RfJgZvDnuOnMy2nRRl1dZHVK
+3l1t3endR2VOPl9v1OzMxMa517W++Nlj9EXXaUrq5M3kxfBc7klRV1FLUGxKX+TbZ25gPkhA
+Pk5EU3tz7M/K2djtZWT6UP3K78rHy8LHxdTW0d5QZ2dbc3rX1NbW4dJLTV1CQU9iSd5oWtDV
+7V/ePVLpOEzoWU7Iz9m9d23YRkJfW0jY6VnW9crF3+Bk3lzw5fbJVdrE/cDH93lPZkvx6lbd
+WuHQ4s7seU5bWEbiREjI2tm7vcPIwlLYzDxUX0JGV9RZ3NpEeGRaW2VfVdZmWcjN/Wt8aE1K
+Sk9NSGHT29/Iyd7P0Pj87GVr3OJb18zo0srQZmXlcmD+4eHr28/K01pX287YQzs9MzAwQdLQ
+yLqzusnJ6Vk7Lzc5OT5HZOXQwbqys7W5urvgY7GcrxUZMScbG2Gsoq2qkpWqQE/vGw8RGyYj
+N6uYlJeSkJekt8c+IhwdHhsbICQjQpifGSS3eB8qqJ6XoZuMkKtErc8VEBYdHR86r56mqZ2g
+yzY/NCIfJCorLzI1sZK7FtvLJxxVo6iWq5qOn91KoS8QGx8tLk2vnputoJmrP0RWJyAlJikk
+JCwvPanVGjo2HyFHtq6ZrJiPn7KwoyccKiY9RMyxnqO2n6f3PUk0JionKiwjQpi6DznUHhcp
+wNqeR6KOpWm7mC8ZLSxEOOeympywm5yxfLnHPj0vNTMsJjar0hEl8BwXJcM0rc+1k6xf2p82
+G0A4Pz3Nt6GkvaGhwEu3ukxRRUo5LSyrnhgduisdIrVRrsFGlqRNOKHVFikvOjtav6mlxqmh
+tky7tHvUz9tJNi6/qRkY0CUZIL7orq/Ul6JIPahfGSo2Pm7CrZ+fuaunyUvLwVjp/FU+SKrT
+FyotGxomQlOmRaiWtD+9rx8hLy5M7LqkmqShmqrb5tJLUU5HPjbAqC0cTiQYHS07x8DYmqNH
+1K83HiopLj9MtqOlqJ6juMG+y+l2TUI/sqYhIN4dGiJCYKy1vpetO8nAIxwoIzJC96yjqqee
+q7/Nb2LnZ+vJvqa+HTswFRkfLkSptJyUssO1PhweHh0mLfepoJ2YmqOsvMvaX0dN7LWlPSPC
+HhQbJC9AtbqYpsarxywgJBwhKz+xqKCamp6jq7jI3ks7WLy0Rx5MHhAdIz9NqKqYoLyqXycf
+HxsfK0C1qp6bm5+kr8Ta/1c/2K62UyI/Hw4ZGy0zqKGYmKmfuzgnJBoZIShYtqGamZqcoa+4
+00YyOs7LeyItKwwYHCYusqagma2frlU3LB8dKCs+y66hnZycnaSorr7sTV7OUjQeKxkOHRws
+O6innZqtq9E5KSUeHiktarennpqanJ6jpq6zwOTHzVU1HSoTDhkYKDukq5iepqfXQikgGh4f
+Jk3BqqCbmpqcn6auudtc4FlPNBktFhMfHCw0qbydn6ikvuc+NyYnKCc4V7qqoJ2cnZ6gqK6/
+UD88MjgdGiUOHB4oM8S0uaC5qrjS4005MzswQnrNtaumop+gn6WnrLvQWEo5Px4WJw4ZICU7
+2Ke1m6ikrMvWQTUvNC01PlbCtauopKOio6Sor7rdUkEvOR8ZIg4cHiY37LC2namhrLK7TUA8
+Ni80OErcvbCsqKSlpqWprb/uSzkxLygZKRkYKh5AO7y9q6SjoaqotcfWRTw6NzpCa9TCurOu
+qqqtsbjMTDgwKywoHy8dICYiPju4wKuqp6emqa+zwtZJSUBBTF3mzMC6s7Kxtr/RVEg4MjIt
+MS8oNCQoJys6PsS+rquko6Cip6myvuFXPjo2OkFLaOzOwL6+wsHM12FEPzo1NS0rKSUoJi0v
+QGbMurOqp6Wnp6uutcTqUEk+QT0+Q0/nzL21s7O3uL/RVD85My8qKiUkJCYrLz9axrmtpaGf
+oKGmq7C830g1LiwqKy45RvTLv7m5tba8xfZQPjYzLCgkIyQlLjlW2bqvqqCdnJudoKasuM5I
+NiwmJCUoKzZF7r+zr7G2u8XqWz8yLisqKSsoKSorMj7fu6ynoZyamZqcn6e00z0sJB8eHiEl
+Lj5wyr22s7W3usl1QTYsKSooKSwsLjI5TuO4rqijn52dnJ2eoqmxwlI2LSknJicoKy83P0VO
+WvHp5XxoWlpJOjMvLzAzO0JKe9i+tK+rqqenpaanp6qttLvL9UM3MSwpJigmJyotMjxHU2ff
+zL/Ey8/sT0xYYczAxsO4vMW8vLm7trCvsrCuub7M3E5APDQvLSwtLS8zODs8P0VKXG1g2VnM
+0dC6177Rydde2GLP3v3NwsvGwb69xb2+u8fFwObS62lrR0s8NTktMS4uOS5BRl3K3sPNyM3O
+y9nPxt3FscC8zbW6R/JI3jo+Xk/NPsvE+d3hxE9qS0dNPk1NUTxOSD1SRGd3WsfD2721vbvC
+wt1eWEZoN098ZPrrq7dV2bDmTd/ouuhWx6nDW6++XzovNC8kJCwoJDI9T9PLsq2xsKm0wL3r
+5VlDQkw7N0VMUOWnqCy+rEfCSLnBrTtVm2bLx7jnOT8qSCcbJSclKEvitq2onZ2iqavKWDIo
+LCQfIi0tNVXLn5c/35xlyji7xKrCL5Wu1m/PcSIoGisqFx43dEi1qZ+cp6ahqOdnSTguIikw
+LSc2Sj9NtJWqLKm7rncwzq6fHrOdt7FLuVH9Hxs5JB4fPd64urmdn6u5r8s/LCYrJyYjMz9a
+zsmwsKqamzxZzjdJKjr+nsTonK2t5kMuLygTGyUkLEO9qJyjpaCovk0+NjgtLDhITEvWwru3
+v87I2dufljotxzBnLCo8n74mpri+4TQrMj4YGikmMD50r5yjrqKmtc5NOmZELDRDTFZ8zrOv
+tLO2w93AoJ8mLlA9yiw52prANqbdu0cpKDY0GCEqKzk3YK2hra6nqazOTG7XPS8xN0A2Nk+/
+usPHzczQq5xBJ+Ipzjsvy6CeN6Crt7IqKicwGRkuKUM9V6+foqufpqm4W2RoPi0vLzg4LzzX
+zeDDv7vL7auaZSa+K8EtINW2uSqjvq+tIUM2KR8fKCc2KkKpq6Ohnpqeqq6txEg3LDExJyo0
+R11f07231eCrodlCuTC49B2+ykXgX1K7vTHa3DZNNixGNig5OkrAz76qqq2oqq+ywNxvU0A7
+Ozg5Ozo+SkZESkpb7dDHyL28vrzBy9BiTUY5O0RCR1BTWFxUTEhFRUtPXuHOwbq3sK6trbGx
+tru6u8LSXkxBMzEtKCcnKCksMzlEcdLIvLu4trKur6+ws7jF3F9EOTYzLisrLC02Qm/Hu7Ou
+q6qrq6mrsLS6xM78T0Q+PToyLi0uKyYxTXrPv73Fx8C+s6+1v8PRXEpAPzkxLiwsLzM4RFjT
+vLWuqqmoqq62w+lQQDs4Nzc4PU7u9e3TztLN1c2+vL28uba4vsXiUkI0LCgkIB8fIycrN0zM
+tKuloJ2bnJydoaq52z4uJyIgHh8jKy4yRlNj9um9sa2pqaWioqOmrLrhPC0mHxwZGBscHyk4
+d76vqKGdnJ6en6Onr7nMTz0yLSooJyozOkFq1MXEwbmzr62urKusrbK92UYuJh8aGBYWGBof
+KUDHrqSempmZmpygqbHA8ko8NC8sLC01NzQ7QU1g3b2vrKqpqKaprLTF6kIyKiUfHBoaGx0i
+KDRUv6ylnpybnJ2gp6686kM6Mi4sLDI6Pj1BPkJNTOS/tK2sqaWlp6q1zWA6KyEdHRwdHyEo
+Lz5uxLGqpqKhoaGkp6uyvupJPDQuLzdBQz1HSk1SbtS/s7Cxsayqq6y4yHw7LiUdHBsaGx0i
+LT36va2moZ6eoKSqsbrD4FdKQj0/SUY5OTk4O0NYyretqKakpaaqtc9LNS0oIh8fHiAjKC86
+Td7Gu7GrpaOqr7G8zuRLNzz+cvE2IiQsOTVJvKqbl5ucm56ir0cqIx8cGRgaIi04RWe/r6ur
+srSxtr3cUUE+PzQuNL2fomEgFxsuNvyxnI6Li5GZo6/hHxQQERkdHyQv2a+tvtHt+NVfPjVF
+yL3FTzs2Nj/Gqrw3GxIbKMikmpGOi42Qn8s6IyEdGxwiL1LAxsTQ4/xFNy4vNklv2NLw6GhJ
+PN+suT0aDxMdvZ2UlJWVlpepVyEdHiYuMzs+2r6wrrPNUk9qzMrXUkdDREQ7Lyguxa68LBAO
+EDSgkI6VmJ6bnqNdJhoaJ0m5uMhESdC1r7Z1OzxSw7TESTAoLDA8VL5gLBoRFRpVrpuZmJyh
+o6evz0UtMDR+wLK6xlZZZMy9xcToe+/c33Q7MCckMsCuvisUDw8ouZeSlqC7ycGxs8Q6LCg5
+xqylrMpCOW7LtbO+/UM8QkdEOi0qNOnSXigcFxs5r5iYma7RQ0/8wszpRUBfxaqqqrnVYXDP
+t7S0wXRENTAqMlXOWy0XDw4bbJ2Pj5WtUi86XsTF6jg1P8+tpqWvzEU/UM63r7e98zwvLC01
+V0s2HhUQFS2vlZKTpNMtJys/3MPXU1natqykpKm40mp+3sC9wd9bS1TKXjgcFBEWLLuZlZam
+zywmJjA+T1FPUGq8tamqqq+2wczLytXH0OBMOTxO6lw0HxcSGymum5WaqeUsKTPvvba/0kdW
+WMy8r6mnp6u00FNGYr2+PR8PDQ8pqo+Nj6QuGBQbLOK7vFtOQ8m4qqmtuc3awbOqpKSpuXAy
+KSYvNDgoHBYUHz2il5OeyCUaGyjeraaqtNvg2LStpqiuucjjzbSlp8shDQkLH6+Pi42fLBUP
+FyjhvMg6LSlKvqeiprDBy8Gvq6iprsVTQFhs10IpHRYaJcGfmp69LRwaJFK4qa67Rj1Zwa2p
+p6uzu7urnp6vJw8LCx+0kIuOnywWEBkq07jEMSsrbq2hnqez0vjNu7CutL1rXt7H0jogFxEZ
+J7iemqNRJRodK92xrr7dRHC7qqGjprG7tqebm6ssDwsMJKmPi4+rHREPGi3QxU4kICdTraCf
+q7560riqp6myw1XbvbfPKhsSFR5qoJyf1CkcHi3Rs6/LSTxNtKagpay2tqqcl52/GgwLFc6W
+i46cMBQPFidW3zslHCIwuaejrMXTxa+kn6KqvNC+rKy8LBsTGi21nZ+xKBsXIUG5tsg2LDF/
+raimsby7p5mUnM0XCwwaqI6HjaIjERAbNsXKLh4ZIju0qau66862pp+gqrpp2biwtjkeFxgv
+vJyerisVEhkvwK+7UjJB362mqrTOxa2bkI+fMw4KDiyZi4mX2RcQFillyjggGhwybrK4zEpj
+tqKbm6Cxy9qzr69MHRUTKbealqBGFxEVJ3WuuuI1PNKuoqSswdG/ppiQlsAbCwwWv5GKjacn
+Dw8YLmLVMSMeJkzEs9ZKO2Gtnpqaoa+/ya2nrWccExAev56UnsAfFhckPsbPPzAwf7imqKy4
+xLywo5qWne4aDA4ZtZGKjaMuEhAVJTpQLigkL/u8r8ZqQfSzpZ6epK26tqWiqj4XEA4ex5yW
+n8IgGx0qQtVBLikx3K+hpKezvritp5+cmZvBJg4LDieikYyVrCQYFRwlLSohJTDKrqWrvG1X
+1bWrqaqyuaqcnacqEgoLHFaZlpiuOionLzc7KSMhMs6pnZ2eqa+6urzAwbGjnqZQGg4MFTun
+mpyl/jYuMzovIRoZHzq2oZ6fp7G2uba6wb+rnZqfxyIUEBksxq633D9L5LezxzAcFhggQLWn
+oJ6dnp+iq7nZ17mqs1shFBMbPbemtF0yNGKypKfCKxwaIDL7u7aurqiinp+krbu0q6WvPh0R
+EBcrTsZCKiMoS7Gfoa5XKiMqQtW4uru/u6+npKamra+rray/NSQdIS7fzWApHRkfNcGpq7b2
+P0Tsu7O0zU05QFvLubKusa2lnqG+PicnLmG+wzofGhghNdG9y0gvLTNRxLW1wNxq17utqqan
+qKWem5yqzzUuMT/cTy0ZEg8WIT29ucdPQEjMrKWkrbvZ4Mu0rKimqKytpJ6hvjYhHiAtPDkq
+GREQFyE30b/HW1zDrJ+cnKOvxNPMurGrqq2tsKyloLA3IhocIjQ5NCEYFRkmQrmqqK7Fw8Gt
+qaertsZp/Ny5rqytsbbCxbmpqNEmExATHjI9PicgHCdDtqKenqq2v8C4u7nM3k9a3buqo5+j
+pay6y9O5sWciDwkKDiNUqaqvwOa8saajp7g+MSs0Scy8sa2sqKejpKauus1dQzQ8Tdw9JREJ
+CQseeJmTkZakrdnNZEoyIhwaIjS6o5uYl5ufo6yxxM5mUV1GRD9fxs89HgwIBQwer4+LiY+Y
+qtFUMiweGRYWHzeznZeVmaCsv8tsWVtR71FWQU/MuLZdHQwHBQ8omYqFh4+dZzQmJSYcGxgd
+LcqdlJCSnKvrOzU5VOzOzmBYPj04UbbC/hsLBwcUOJOIg4eRojsrIiAhHRsaHjS2m5SRlqfS
+LSgtPsW6tb3Zbjg3LywuMFFCNh8QDw8prY6HhYmVqDknJCIkHh0dIDi9n5iXnK9oLyotQci8
+ucBfRzIvLS0wLTRFaU8sGBUTLK+PiIaIlKM9KCAdHxsYGhsyxpyTk5es+iYgIyhLxrKttrV1
+QS4nKSMpKjzlZOQmJR4tspqLi4mRnrwyLiQoIRkaFyQ9rpuanK5pKiEnMF26t6++zkgvKSAm
+Ji02RLmyrL8mJRclbJ2LioaOl61BLx8bFQ0NDRk3rZaUlJ21ajY4TU7MeMbPx7pYRyolIiQt
+M0rEuLXHLywfQ66YjI2Mm6d5LS0eHRQNDg8eRKeamZyirKu1r7pOQiw6O09hNy0mKzVWvrm1
+rK2uvDAqHSvHoo+PjZikvzYuIhoUDQwPGjyunZmdnKGfoKmvSDUrKC0vLislJio918G3uq2p
+q6xPLR8gQLeYlpSbpKmzt+UxGg8LDRMeN/G4rqGYk4+Sm6tWLiUfHRgVFxspS7qrq66qn52d
+qjEgGCLaoJKVmKOmqaixVB8PDAwRGSEkJCpJrJmSj5Oan6asuUciFhEUGiUxQEz8taCYlZee
+xlBdza6srMroTMm1s71IKB4dHB4fHh0eJj3DrKamp6ainpyco78yIiAoMTgwJB8kNcmspKKj
+paCenqbBPjZR1M1VLyUkLk/P1z0qIyMoLS4qJyk0/7Wqqqutraahn6Orsri4uL3QSTUuLS4y
+MC8uLTJHx7ayuMPKxb64vdVVU+XP2E05LiwtLSonJCgxRtG+vbiuqqakpqqvr7O4vsx7VkxT
+W09MSEI6PUFBP0ZQVmzhy7+9v8vc6+59Vkc/Ozw/QT0/Pjw9R1h/0MjAwLy3trS1s7i+wsfa
+bVdOTVBXVVRUTUZDQ0VGSExJX/Tvz8jK0+ppV05HPzo5OztETnjSy7+8t7Oytbm6vr2/w8TW
+7F9TS0NAOjg6PEBPU19za+p9/35yWVleYmX94e12XE5PTU1LQT8/QkpX6Mq/t7Kvrq6wtbq+
+xtLZ4+txWU9SRjw8Ojc2NTU5QlF83cu/vr++vcPR2uBrUF5bWFtWd2BXWE1NUl5k7c/FwsW9
+v8bJ3OpnYF5YV1VfWFZaV0xLS0dIRUdOUVNp69zLyc/W33JcWl1t7dXIwbu4u7/N3V9MTElK
+TU5Za+jY3fRvYVZSTUxMTlp4f+HPz+dxVk1KPkJBSE9c7dHBwMnN0c7WdPr17d3OzMvExM/j
++HZ+XWreeXzh3eD0819aTklKQklNUmju1MjBw8vN225hUUxLS1Js9tbM1PpYUU1IR05YX/vU
+z8rGxtPv7WNTTE1OT19v+enWztHa3+rvYX3mbunXztLS1NtvVllPS1VdadzLyL/BytHxWU9F
+Pz0+Q01fdex7eGVdV1ptan7y3M7LxsLK3fNoUU9JRU9Ua8/Ky8jH0eD5cFxOXFlW69jX1tfa
+bltSWFtUWl5u39zg1NTZ7O54ZV1aX2fr1c7EvsLEyMzQ/VdcTEhpTztqu+9T3ks3Nz2+PzJN
+XdfMx3XJzkxjd1Bt/1nZyttgw7VebeBZfs5HOMbk+ri9aVHe8T85Qj1FR1rRxMPQtbrcwsvk
+dd/Y6UpIV09XXEZMS0RPYmR5y8G/yMXL62dfTEhKRD5IWl1f+9ne9N7S5NvedmbW5evF0dXf
+XldSWF32c37JyMbZ59Xw73lqVdxcU9Xv7dPe5dPP18zO4tdmX+tf19/3bltNQ0A5Ojs7Slpw
+89PZ+t7d4vbu3tpv7rnPbM9pVlRRT1FmXfrJycbBw8jVzdDY3PxZZmlaaVZiV1dhWnZYWEhN
+WFNl8fLc7V5kT1VZU1Rl9WvPxczDzMzN6mzxSUdQTllca1ZoX1peYOp48tDZzcTLys3I4NrG
+WTU5Oi82Z9vAsrO3r7jBx0w+OjIsMDs+Z83FubW0s6+xubrL6WdEPTo2Mjk4Qa25JDBFIB4/
+W9inpqKfprOuuDgpKR8eKDBIvq2qpqatsbvU3G9HP0E3LjEvMzR+nJwdH7onGTmnrZ6empSb
+vsG0IxceJR4lQuW8tLGutWZG3fdG78VLMjI1MDM6PTk2Ni44m440GbizLDidmJObpJaU1Rsy
+LBMPHjE6UWC7rdI94Mo2NMy4v83gY085NVXaQjlJQC5OkpEeJbHGMHybmI+nup2nGA0dGxYc
+ObyouH+xqjYiPttBR8m8x0kvN09BPmDJVzxAT9G8mIy4Ma+xt2qrqpekJUfaKQ8bJitSTd6x
+qDUjNjYsOWnNrrLg/NQ8Mj0/SmhMO1bHvLStmYufHCUnODNHy52NtWPFVyEaIiHnxnLRsdgq
+Li49xrvVv75GLi4uO87FvbCvucjL1Gaejb8bJB1Cwri/kY9OQSYeHysjMqq1urHYNUAwLte7
+52hBKCkyOOCvqa2+Vjc3NjZIpZK6IicfuKiorpeWak4eHCvr38Kls7rWKB4oMTbUv87Ibjw9
+X9C7tb7PUDQvLjFXyudSno22JBgPTKqmq5idsaglGyxLz7XKKzQ7JiUoN6ydo67AUU1MOUPL
+v8ZMMDI/SXN9tJS2HBkNLaOWl5WXsKIzGSQvfbS7LTM7LDk4S6udoKxMIyYwOFvLxbrfOS8u
+MD9PPjxFSdyakdo5HRylnJiipjQ8UhkcIjWvnq5sSTI+STd6raagqFEvMz16zVxDOCwuLyw8
+w66TlDsrEhm4oJqdnzjRTB8kIzS0pLxnMCI4TGK1q6iirEotJSc7SEtrRkJjWEdj8dC6v7eb
+oTYuDxfEm46Plzo3Jh0iHCPvpKCeuygoKju9tLOvuctULSk1Vb6ssL5bLywrLjranpm9MxES
+MK6Uk5hTODQsRSomLcmrnqNGJBweOMe3rq+6ub/eXUJG0L67t+g8OjQ5TXmumKZZJg0WMKeR
+jZW5xDQ+OyIdIS7KpK/fLBseMeq0sb64vb7EUjc+WdG0tX45LCoydqKfw0EYFjCvko6PqM07
+LE4sIh4jMrahprwrHSAv2q6vs6+wra6+RjYyQNHK9jcnJSznnp6yPBEWK6qOjJK8TiYsPisk
+GholvaKeqjEgJDTFqaars7iws8Y+LCcvSlxdOywoPKeXnasdDhkymIuJlbkvHTI0LB8VFiiu
+m5aiRyUhLcqttrvfzbzDXzQlICs4W20/MjytmZqhMhAVH6mPio6qQB0oMi8rGhghxJ2TlbAw
+HyA4yb7B1NK7tLxSLSMmL0TMzU9Jt52anbsXFBtNlYyMns4hIDIxMh4XGz+ml5WnNx8cJ0Zp
+c0hB2rm1xkMpKDRPzcxYOUqqmpuiJw4UH6WOi5KuQiI72FMzGxcitJmSmMMlGx0rS09DQvqw
+parNNSYrP1zTWS4pVZ6Ynb4RDBMsmYyMnsk9N6+9OB4VGDOgl5arJhsfLvLFUUvYsqGfr0ws
+JzjTyt08Jiq0mp6uHQoOH6eMipKsvk29qzIbEhImqJWWoDccIy9L3T4vT6+gnqtJLC4/zOI1
+JBsd7ZujxyENGNCWioqZbtBgy7YhEhIcbZmRpF4nHzfaUUI3Pqybm6DGKSQtMzoxIx8gLKmW
+p2keDyWulo+Ro9uqwcdRGRMcNb2grzQwLjbsXDdVvaibna5eLiUvPTY2MS0uNsecn3YqERjn
+nI+PkqWgn8BVGA4QIEO/rkxA1cnHzUM9vKuio7RQPC8sNTQ+TkEqIjSookkgDhm7louNkaWf
+pDwlDgsQKu2/r7upo61wMCk+saqts7vExGQ6MjBBSi8gJcWsQisVGLmaj5KUoJ6dORwODhkz
+Q1yzq56dtTMwNF29yL+xqaiyOystMjcuIR7hpzIcFRa0m5Wak5aXlzMYExkcKy0vtp+aoLNJ
+1b/+T0vcr6Sx0kw3MS0jISguxq8eFBogtKOdnI2OlptIKyklFRskMraqtLquuLvcP0XS9NfH
+y8b8NScqLTE0PbS/GxsbKsSro5uMkpahvzYvHRIcHyxSuLqnoqqwtMnr803vwr/XdTs5NzEu
+McupIxMcHC8yfrSPj5iZm6tQJBQbGxcaOc6soaCemqO3xcHI50xLZE08PEU+Oty3Ih0nIigq
+OsyZnJ6XlaS26S4rHxUVHyEq46yhm5ycm5+tue45LiwsLCkpTLUvJmU9Li1BWrDFy6GeuLCr
+veVALiwsHyAvNTjQs6mioJ6dpa+9eTosIx8fICwxLD17SVO8u7qutbSttr6urr7Cz05OSDs7
+Ozc/SU/Pu7Wur7O0vmdDNSspJSQpLi8yPVnfzr+8vri2t7KvsbCytri7wM5dOzUxLjE9Qkpt
+z727tbO4v9RlUk1BNjUzNDc5OUNFSv/Xyry4trGurq2trrK8zmpBPD00LzA1O0NPX/Dna2Zc
+ZnpaXPf9bVZbVERFPz5IXHjWysS+vru4uru6vcK9wNT2XFJMSEhIRkZGRUlLTFZaWu7o69TU
+alZLPj5CQ0hPcHXRw8jKyMbOyMK/xsnJ3NrS2+F2XVVOTE5PS0hFS09RZ2BdXlNOYnlw+Gp8
+3c3FwsvT3F772ujrYv/w1s3X3n/+c37Rv76+wcPBxsjQ805FPjw9Ojs6ODg8RU9kZ+3b3MXD
+y87S19bMztDeZllZTVFgXVxnddzNw8XW2N3u6P1xcFRWVk9ce19cYVRQ6drPy9jUzsvQ22NU
+VUtNVl126n725N3Z1dr4a27x2u5yVUlIRUhKR0RGSl3s3s/Ozs7PysbM4fNlV1xmYmN0bGXp
+3tja3+/27+jNyMHG1djX8mlPR0BBSE902srHyMrIys/nVlZNTVhRUVRNSk1LTVJTYmVw19PG
+yM3Mz9/d52VcSklOT1Nf/+Doz8zLzNPb/WR+437i297Y931dS0dEQklW9dLMycDDxMrN8WFf
+UU9PWV9VVmtWWVVdXlpvc+zkzM3U0trfe2hMQ0E+Q0pVb+HOwr+/v8fM4nJXUGTwemvo339p
+VklIR1Ficc7Ix8W+y9zeVExDPT9IVt7WzcDBv73Eytbf5Ojo3ONka911Wk5CPDs5PEpRXnnn
+0dHS0eJvVUxBRUxWcmvmzs3N0uDxa2vm29nKwsC9vsfP71RFPz4+SFTo0se/wMDJz/xkTkpJ
+Q0VKV2B3e2NUS0pNTlZn99bKy8O/w8rmXktFPkBARlX708W/w8jJ1NXvWmRWWezb08rM1+la
+S0pMSUxb886/vr3CzXxUSz48PUNGT+7Rx8PFxcnOzc/R0t565dDi6djoXEpAPT8/P0hQ+8zG
+x8vN71pQTUlGOT58SdDMw8tz/UpkZnru3drQvrq4urzA2XBaTUI+P0VMXOLZ1tdpXFtbSVNQ
+SlRi0tW7Z2ttTFU+WElQUdjLwb7Q19Xd6M3K6ExDRFNi48rR4evm/XpbXmRib87EyMl+an5E
+Pj5ERkJP5c7Gxrq+wdvr3f9LQEFBSUrWzeHzy87UzNfM29zPxMvS2dzW3lZNR0Q+PU9cVVDn
+3mZl/Nrta2N8WE5QVllWS131cF1Td+Jw88u9ube1usHT/L22fjw6OTIvMEXa8HW9r7O+0M9l
+Ni40OTk3QtO+xcW3tbm7v8PVTzssQqS2Kzg7PDQoMauxOfW2vNBETLpcKjTTu7zLrp6jr6yo
+sd08OkIvIyopLbDDIjItJywlLa+kv6ifoabGScVUKS85csTcsp+hp6yvrMMtKisiHx01pUkg
+OzJUMSc8psI+s6+pulV1uT0qN0/HaUmxoKSmqKmu1T1WXzUmRZ1THSweOR4VIq6vS6iqna5O
+bK9OKy0+s8ptraGtue/CuUovQj0u+qM9LUccWyYqUaq006zOrUQxM0IpJzX1qK2pnZ6qsb6w
+uDwqLi8uLzBHv52wLd4jLSUaLsmsyp2emqdRVk4qHCIqwr6+pp2jq7zM1DwyNji5ny8vOx8t
+GBwyslu/o6Sf0Ffc5yUlK1G71rekpbxaR9TC8V2+vsw7NJ6XJjwrLUUYIdKiOrvAqbEsKOdO
+KjdRoqG2r6Cx+S0vPComPWzjoaNEsywhMRklWK7Pn6Ofpj84OCkeJi3Oubuon625zsu35T5K
+R66dKjNKLjggKc6hRMCvrr80M+tVJDFlsrPOwaSueFfUzHA5a7nNSUOlnS8lKCUoFyO1oNmu
+p6PEJiY6Lx8ryKGlrKKcr0o6SUYsJjVhpKUywFk3LyQxu6ntqaamv0I3SS8fK0bBv9a8q8JM
+VfhMPjfcu89dW6iaMB82IikZIdmkvq2cnq02MkI7IjPNqq28t6q5QDpCRTk5WMi9op/W2jYl
+Kx8mSLLCqqeqtD4tMiodJzXGtbOmnai92FdSPTBG3qKaPbi/LS0eKD++MaufpLJbSD0lGyk1
+VejEsKq/y8LQPzQ2PT1WpKHHvW1JNyIp6cs3v7SvvPrfwT0mOV3XxbmpoLXvz2g9Nzdey/n/
+6Vyony9DPB8jHSjaq92mn6vSNjQ0JR40S97Cr6iovMfAXz5CU85YPaSYNjtYKy4cKN6uSqmc
+obF9ZEclHDdOXc+wqajHzrhvMzE4Ojk7t5qo8qzNRicmN0cuNrC5xNTAyUIpOFg/ScSvr8HH
+vOpFQ1BFP0j/3lGznFw93iYpHitOsdywnKeyzc1LMyEuOjNPtKuprbWx0jw/TT0/Q01Lu6rr
+z9c2MisuPvJHu6u2z9xoQzUuPj07WLy4uLiyt+xKRz89RVRSbZukLqpLLCoiLDs+MqCmqquo
+tM85MzsnLTlERNzDr7TCwMdBOlJGOUajozeztUhIL1NRRi61vFXStrxLPUN4P1LGyOvBvLjJ
+1MXjPTg2Ly9Lr3VAr/g9Nj5FRjtbtOjVurTP5/VpPzZEUkRuwMrExbq3wcrAzHJhQUGsvS+6
+Ty40OUZcU12uyNLFyGheSkQ9OlrkZNHDyMna0uJRTVtANi8ypK4iocE2WT9TPm48rGLPurnJ
+u73YzjtVRTc891TPxsfCxsbQZkE9z7cmTbYuSkjgTMzMsLhqus5RP1NBST/q3E7Ntr6+tLzL
+31VJPTY+PDMsVqk0O6Q6RU1hQltsvrfotb3H279aXEFFPzM8XWHuuLm7uLK1wdvkPS61xxi4
+OyFSL18+xEit3sO42eDUzFfNW8fc8cO95MPL3nNTT0hCS0xLTDvJpzBHoyxJPUQ2YHjWwE21
+z8rbxEFLRT1BOkjNzdSwubWytbq7ydZaNzLFvhu/xSDQTsVmscaxvty4S05NWTZPP1FTS+Db
+X9C/Zez49W5eVl1FREREuswsr10sVkVTRcdbtcy4uL/Fy8ti50hLSFjf0u27trqytL7J4FI/
+Lyg2uhsisRtAU9hkt7K4rXer6Nl66zVJSD5aQd7Q0sW3wra0uLzCzd5aT2c+NUmuOB6oLidC
+OT43v1Kq9ayvvLq83E7iNU01PVtRWbu8xLS1usXJ1V09Rkk8OlQ+RKtLKqcxMuRDSVHPTbNd
+sbnNycpGPEEvQjg9Vnp4ta+up66zuc5JRTUuKT+rHyakH0zezWbKsVumUqi8yc/LSzjxNX05
+WNtUT7u8z7rN1mxPUlg/Sk46MUy3JS6qITI/SkzGsq2ovaG2vb+8Vkc6OT0uStrrza2ws7K5
+wc7uSTcuLS0tMz01Ua+9M62+M8ZK1Ux35q+6uqi6tsrfUko7Tjs3VFF7vbG2r7bB3UtEPS8r
+K1ZZF0X1HUpCvNSur5+qtqXQ6j1ELzcuPkg33r25sKaqqa64wO5VUj82NTEyLy0pO7IqIbEj
+MlRZsbuuq524pK25w1RCNi0tUC9F4M66rKursr7DTDYvLy0sLS82vfoxtHMuUTvn+d+7pa2u
+p7u25l1IPCk3Ni1EfLuwqaakqaqvv+o/OC8nICImPkok0WYu5Eu4u720orC2r724Z2pwQi0/
+ODA5ReTZybSrrq6ut9JKQDcrKCYiJk44IW45O0s8r7Cvp5yfnqeoq2RGPzYtLyw5OEvHvbOu
+ra61xttQPDQvKyglKD47JjU9LjYs7MDWt6WkpKqqo7bP3l03Lio0QkjItK2opqOjq7fRRy0m
+JCQlJig0PjE5TVpcP1nQ1P26q6amqKalrL7O30wyLC0xMj3fvrm1sK2xu8TjUTsvLCssLDk/
+NjU8RTw7SnXMx7y3rqqrqKartr/MXDsxLSwrLTI+WeDJu7e0s7W3v8vgT0U7Nzg8Njc6NTc8
+RVX00cG5sq+tqKepqq+5xWs7LysqKSgtLjhE/cbAvLu5vc3cZ0w/Ojo3NDk7NjpDYsu8s66r
+qqyrqquutbnC7Ec3LyspKy0uMztM0723r66yusxzRjg4ODEtLS4yOEThxry5r6qpqKamrbO9
+x9ZUQTozLiwtMjhDX+DPy8i+vcDH3kk2Ly4tKi0tKi03U8G1rKajo6Sjo6arsLjQVkM8Ni8u
+LS0vNj9O28fCv7y9xtFaPjcvKykrKy0uMjZD3r6wq6ejoaOlpamutb3WRTcyLSwsLzY7PUFb
+4ca9ur3K0+pyVT89OzcwLS8wNDlIYNC8s6unpqOio6eqrbXE5kk4MCsqKistNz9BUW7u5ePl
+6+x5W01EPjw7OTU3PUNIX9nHurOuq6qqqKmtsbK2welPPDQwLi4vMTU5PD1DT+7Qy83WzM/X
+zczO1X5MPz0/QEdZX2vZx8C8trSytLm5vL/J6VRIRDw6ODg5NjUzMDI6P0RV+dfIwL28uLW2
+ub/L09ja8+zc29LXz83Ixcvdb1pWT1piXWhy9251fPFkTEY+Pz49PjxASlT10MzAv8DKz87M
+yNLb3NzydGZjb2ZkZV1VUFNi/OPV0s3N0NnX09tqW05MTURERklMTk9UZubo3NXa3dXQ2dvT
+zMjMy8/R0Nr1Y01IT01JTllic+Ha2NbW4mZQUVFOTEpKTlBcZFxo8O7s3urk2eDZ19vQ1dDP
+1c/L0HlmYVlZV11p7+bey9Hfz9TY6fnrd2Zna11dU09WWl9mZF9iavvq6HXhzNTZ0tHP2eLc
+/WtoVUxFQkJCQ0lWYvbRysjIysbO43Bq/mJaW11SVFtdT0laYV116OLPzMjDwsLDw8vS3+zd
+fFpYUE5IR0hJTE9VVV503dfv3tzh7/x6bFhYWVZVVFxpVFNYX3xm3dTaz8bDxMTHyM3X4H1t
+T09ZS0hLUFFYX+LX0crNzt1r6G9WfN3Z4d7m3Or3Zk9KRU1OUvzWzMbDxMjHxM7edU9KVkpK
+VE1IUFc/TGP37jBERvnG1rtsuL3G4V+4XsxXU0A8TkBrXc/LwsDEvcDK0cfZ5HZlVEJLTlBI
+Qk5BRj1AST5QU/vp0sO7v7/DvsLX6FZPQUpHSkpffX3a6s7M2dnPz+7N68zO1cnOzOPH4GpL
+Pjw1Njg9PUBY48rBwr/CyszmUldEQktX7cXOxLvnyGvn51beTc7Zwr/DtL25x9PxV0dDPzk+
+PE4/RmV6Y0R2VFtLQWpOSELS5dnQe7vLVV5fSERFQeF378K4ubm8vLLUz7/aS19oSvRATOxC
+Wv5wSd9STNVF+OpYY87M1dPfv8ZXVe5FOjk4PTk8ROng0sXQwszK0sbXc+lk29zJytXU18h0
+RUVGTkBDVuPIxL+5trq9vsXfV0Y6Oj48NTg+S1t43ce+zsvMycXZ18/V0czZ6u9cemVIPz8+
+QEc+PlFw19XUzcm/wcjLwczr/33pYFFNSUVJTWT9WktBR0xOWujFvbavsLa6v8nXWko8NTc2
+NTpBT+HPzcnT2s3S1s/h6+JkYV9cUEpJSElVWk1HRUdW683BvLm3tLW5vMnb5F9RSD02NTk9
+Pj9HV2xs19LLwcLAwMXJy9J9VE1TcGNeS0FDTPjLv769v7y2tbW7y3ZMPTk2MzMzNz9Z49TU
+231ZUE5MSEpLT3jY0MnJy8bAwcHPXEU+Ql7Sw7zAys7Mw7q8x+FIPDw8P0VBPj1AT+nRycnM
+43fl29LS3V5NR0dV4sXAwc1aREBM+8i+w+NTSkxvzsPC1VtFPDo8QEZMSkZIWc68s7C0vvdH
+PTs+S1peWktK88Gvq6qvwF07OlPTu7a7yVU/Pkhi1M3dUjkzNjxIaeBcRT0/Tde6srTC3Ec8
+Pj9TYlNNSlD6xrmxr7jG+ldZ5czGydt8SD5CTGPsellCNjI2PVfYyr68vb+9u8LM/Eo5MC8y
+ND50y721sbCxtr7F1ntjXm/5amtaSU1MTExJRD45ODs8RWzTyMbCw8TIz9b7T0I7ODk/SFvP
+vbWwr7O6v8PKyMPG0O9RSUM7P0ZSbmNVSEQ9QE/nwLa2uL3Hy975fE49NCspKi46T8Wwqaiq
+tcbSfPHLv8HKakQ7NjpCYeTyaUU6NjZCYMe1s7S7yu9PR0tJRTswLCwuNV24qKCfpK/GZEpY
+zrqytMRgOy4tMDtaaFc+My4tPOe1qaisuulCNzQ3Pj44LysqKzJYt6efn6Ktwt9S2bqvrbjX
+QDEuNknZu7zORC4pKTRkuquqr8JOQDc3PkRCOS4qJyw81KyhnJ2msPZCQErJubOz00M7N0bl
+yb3KQSsiJCs/vaynq7jRTDw9PD05LyolIys94LCjnp2frLtYPkdL1b65u8bS6m3t0/FsRC4p
+Jiw4abWrqay73Ec2NjAtLCgnJyw7XMSvp6KfpK7APTo8XbWvrbLOWEJC69rmWjAnJCc50qyh
+oKOuwfI+NjAoJyEdHyMvY72zrqekoKWvyjMsMFOsoZyeq79VPkRPPzIhHBsePbCemZqgsO1C
+MiwqJB8eHiYvUb+9vLOsqqe4QSkeLW2mlZOYosFANzY5LiEcGB0twJ6Yl5qouus9MSomIyEp
+NUF92eHjYVfQt6+rwS0oIz2nmpKWpMs1LTo9NSodGx41taWiqLvX+23qQy8qKDBXx7rGVzw5
+RmbetamurlssMC3ApJ+cpLbYQ0VcOCwmICxKw6yutbjDubXGXTQoKjJGY0k7Mi4/e9rgPV+t
+p5+sLCEeNaabmJ/SO0xuv3YlHBkl1qykp7i7sq2t3S0fHShE/Xs+MTdL0L9YNDavnpqeLx4f
+NZ+Xl6NXNUf0/DgZFh04rKWruNG9qqm1TSsqNkRtQzMwLzI3My8yPU7Zqp2en70wQEW5oqSq
+sL3Cv00sIBwlMkbPxsizra+4aUA7O0ZOR1Bqan9IODYvMjRAoJmmrh8ePcmenaatpqupwCIb
+GiA4TDtSyrOiqvczKjPM08vM4b+43D0lGh0nM1LU3rSuu7uknaeyMRw2yaeZnqKhqbhPHxYY
+HCpDebapqa7LLiQlJCszTLKlpKW6SDsuJh8+mZ/LOhAfuramrrSmm6TWNx0qbkJOTXqtnKez
+yThTRCoqLz2yq77hPi06NScnISQ5ZsytrLGvnpSf4iMaMMi/rqedmJy2LyMeHx4dKfysoai9
+ylc5MCYoN0vUzvHYy+xfTkVHNzmekLk/JBN7wy6+q6GZnnVATycsIxxVr6ueoa2muS8yKyxE
+LzDY1mFYNjFTTTMvKjFbTuu+noyTvDwnT1QiKr+lpq1SY78zHhodNmpGy6ynpbpZyMRPNDBI
+wuQ1NT1NWDUvdLzHeUqfjp86KiNISR87pJyfr8CsqS0ZHCI0NTOzmpyksriuZx8eJy83Ljq9
+tXI9OkQ5JB0vnZXeari0oEEzrZ2sREplrzkdJzM8LSg9p6jU2b2us0hKtrz5TUjb2zg0PTYr
+KCtDakvJsa+xt6aVnT05OjE1Ijaln6yzq6vbHhgfHhweNbmrsq+mp748PE0/Mjy/tsfQwbvX
+Py0srpPaHr0u9Tcdpp+otaumpVAfTjcnHx0220A/q6KkrrWstzo0QkM/MUHIWzo9SU42Kzle
+RUH4t621vaiYmsLcuksuICvCWjbptK7UNk9iLCIjKi8qL9e8uK6knqeyr7PSPjhANisuRXBI
+RKeStiu0LkQiFb9aVc6vn6XDuaRgNSgpMB4fMk5V6LWho6ypqbNZNDo1KisyOz09Z8HMxr++
+ucW+tre7uquryn5xTUAsLD85MjA9VURDXtpqRj9ISEZObdDKxrSurq6xs7jGzM18VlNSWlRM
+TUtMTEc+UlpJaVLnztzQzMzsaklZVEdLQEc+PUZNU1JRUWLr28/FwL+9tbS3t7m/zd74clFH
+QkNNT1JUUkw+PURGS1dlZG7UyMrJ0HdUTU5UX1JMU17u7fb2fVlNW2v+3NvW0Ma7u7u6vMXC
+u7u9xMvN3N3rWk5JPzk5ODk5NDA0OkZTXOTWzcjDw7/AwsTEv7y5usDL6ltGQDozMTY7QVfu
+2c7OzMK+v8HGzc3P0dzg6VhNRkM/PT09QEtY5dPR0NXQ0NHOyszU5d7X2uHqfV5bZXNnXk1J
+T1FSUlBWXlxs5dLIxsrLycfGx83cbWxeTUlFPjw5Ojw8Pz9FS1D80snBxMnGv7+/wMXFycfJ
+1OFeT0pGT1FbYFBacPHi3dDY9F557N7LzcvHwby/xcnT5GtbV05NRT8+Pj4/QUI/RU9ddGrl
+2dfPysHCxczMz9rW2eBsWFFPUk1QV1ZXX37j29vifOzh3NvY0tfY2dHNy8rO0tjq/fx4VUtH
+Q0A/Pz8+Pz9DTFdUWmpj6s/Mx8bAw8LAxMHG0+tx7/pfX1tUT1VZT05QUk9Xa/Ph3Orf5OHp
+8uN+XVxgbfL86efv3dfOzcvM1uLqaVxYT05LUFxi++rj4uDs3NnSz9POycbHxM3l49bf9l9V
+U1NJRUQ/PT0/Pz4/SlJd9ubUz9PO3dnX3dXU19zY3ujd6Ofb2dfRy8rHxsbLyc3ebFlOS0pF
+R0hGRERIS0tUXvH169XSzMrKzc/N0Nvc725hV05JSUlIR0VHRkxPU2F34t/Qy8jIxsPAx83O
+7l1dUk9WXWd67d3c3crHx8zN1Pd0X1hWTE1PTk5KSU1eXWnc3tfP1MjIysjR1uh3cP7q6+rr
++nNiXVlYUUtNSEdJSlJeaG18d+vt9Ojv9+/o29bS1c7V7Pvo4t7SzM7Pz8/V2+lvXVhcVkxI
+SlJQTUxMS1NfX3T45ejh4HJvfeLj4dXQ09zf5+vl4eTn2dvd4O3W1tznZF9SUllUT05PTk1K
+SUlNVGLrzsXDwL69wcjL093obE9FSElMTVJYbHH0fX3X2tjPzMvT1NXg4uHudFtPTU9OUFxT
+T1dOS1RhbO7OyMHDwsDCys3bZ1dKRkJCQD9ITlZYYWJjbmRaZF9lcefQzMrEw7++x87l6ets
+YmFdU1FWWFlacufw59ja4+frZ2RlUE1NSEhLS1VYaeLe29XP2NfW4v3zffrl4+h/6ud0a15o
+YVtcavfZ1tjNzdHY19fh3n5kVkxJQUJJSUxZZ/Pazb+8urm5vL/CzOP4bVVMSUdCQE1LS09K
+TFZaaW1q9+7h2Nzc7GVral9kWVBOTVJVcPXj09DMysfIyc3X2uT/amxjZWVUXWBcX1xbV+rc
+3Njb5+t5dWxga2thX2RtVVtbW253Xl9jc+7n2dnd3NPY5+fvc+79bVxNTVhaa3X28v14ZWRy
+e3Tf0M7P3t7W1tLWz9LZ3H1hWk1NSkpIR1Ns3cvBwcLBvsDNzs3U3HdaSkE/Pz5AUFdnbnN1
+buzr2trs8W365O/67nBlX19aT1FdeG3449/i3M/Nz8zKzc7hfmZcal1RT01HTVpeZ+Xg08nJ
+ys7g535WUFNTT0xTX3Ty5Whrb112YXvuZXFvbmht7u3d19Xh7m1aUU5beGz3d2Zxe/fl2tnS
+2Nrg7eDX3+79dlVOX19oed/Y+lxaVVth7eHu2NbQzczJxMfJzM7P1et9X1hOR0JEQ0dMVW7i
+2Xbo2u94/ntwWltSU1NWVVtcXHPu4uzn18/P0dzf4/Dc6fXieHxyaHNt+uLc0tzy7f5s6tvd
+1Nbc7e99T1BUVVRWV05TVWBgZ/ns2+Xj/Wjub2haT1dPS1FSWWvq3NbX0tbVycbN09foZGX1
+enf6eX18/WJj8+fo7O/rbGlqX1lec/b6dm5z59nKzc7V6fHf2eHf1tr3aFpURUhfXVxbWVpV
+VltkeXZv5eTvfmp78uT9Z1pYV1ll+d3c497W2uLb1M7T2ezq615gXFlqanTs3tvq5NPU2955
+7G5edGBvXk5NSUxSUl737OPd2+V+7Ojz59zu9H1hTUhbWFhuXlxw7+PT1tPP0tfZ3fz93+H9
+eWN3YGj7d3r78+/n7Hlc+t7k5F9RVk9RT1hddezf1dHZ3MzEv7/Dw8ja5H9fW1VPTk5SRkpg
+X1hZX15mYmRlfPhyXlpVTkhIV1tp9PDn+erW0dPWz9LX1drn2NXV3O7o5/j57tTR1OX/7P1a
+UFxtfV1YV0pHSk5TXWj//u39a/3l19bk2+x45v16bmxeXl5WV1pf6dvf5t/b3NXP3NPQ5eH6
+YFFPUFr6c3z8d/Xt3drOy3flvE9HW0ZWPllJSl5b5tHCvr7Mv8rDzNTV7epZT0pUQ09PXX9l
+2VZQVEtFVdxW8dDQZT7aTWxt/klKTlH81sxq2NbN0cS/x8HIxezy31VXVlpVS1deX+vf3t7P
+1djY1mhNTVFAPUJGT1XZ09jJzd3d2eFWXlRhSFFKV0xb197JxsHQzuXdYuj57FFVS/bnz9LR
+w9TUYtzg+Vf3WU9YRU1Rfs91yszp5ExUSkxeZE9e2/1u0by7tbi6uuPXWU5GQD06PkBFRujL
+xcO+u8nX2VROTUg/PTw5OEJw7cW7urzJyU9EQz08OExFRmXu4rmtubaru8W+3NM/S1IvRkI9
+TnC+xc+8zk/1Sk9FVGpDW91d8dDMytfRTkFBPDYyNzo2Zbe4xbeqzb6w43RXXk40T/o4U9Hd
+ycK4vsGvvNXNY0U4NzcvMj03PUtIU3npzr+foVCfrCuuSHvPM69APLxRUchsZXdqtjg++y09
+PEjf/LuvvrK16UY0LSMcHx4eLjhZuaSYnqCUm7ivskItODQoLklwfMWorsG2vVk/QjsuLDcy
+Ljo8ODtETkc+4Fe9lLIlnS0cryjKzkSet7ecs76q5dtOJEonID8tPVpctry+rsanqE/ZKR0c
+GyAnNb21rJ6lo6WfpzVctiAdbPdAyp+it6OrVTknLCAYKC42yK6hn6ekr0g6KyQjJTI9UsrH
+nZQ9O5spEC1OIjqpq6WgoJu1PuMjGyAgLy5Iq6yqo6qpp7bFQx8eGhwoN8CloKGkpJqtFSvt
+EBfWtrOelpifrdJcHhIbGh8zS66usKGmwMhMNyggNTc2uKqnpbdftp8aCkInD0een5uXmZul
+VH5HGyAtMXVKraDkWttLOC2vmau2rjsiGBgpK1CppKCts5q3EB5vHBzCm5ubnp+hLhcmGxYf
+Pqi7vJui8i8pNCQZPLavpaWeoNk2RLLPDhHYTR9KlpGvt6Gz7Swxp7Mva7O4SyQxPh8r3K+j
+sbehyh4fJj1PXqSrUpubDw4sFxl0pJONm7af3xYdIUi6ZZ6dvlAlJCgdNLiml5qmtiEODxQo
+qpaNjpq1vbgNAxUvPa6WjZCsJBwqGRfKmY+avsQ1HBQYMb6zpJ2erDIv4l9GNLKMkx4eeDAR
+ElSmoLJWnZ4eFSA0XCzNnKdhKytZMCnPqai2MkDoOk1PO+SRmRMfQh/AXbuLjrFOTTYyFR+i
+qaa02Kc/GB0iO9iykZOqJhASFRY2p5WWsEk/vjcNGK+enZ+cl6UiDRlNPueql4+5GxknWjYw
+qZeYsywxMyYoLr2urpOgGRgeIC5FrpuQnSs0QSktJmybm565MEUtHjFlqapBQ0lN3jcs3JGc
+EBUZHqYxP5KOlrojJGMfGS1LmZvBwkk3KRIcwp+OkKCqMRgXFSiunpmntpmxEAkKG7+km5eO
+kLQmGhwoMPqtn5yrTi8jHx8gObynoau7PCftozsTFS2xn6G5p5mzKh8cPrPOvq+jmqszJiI/
+tbisqauxOSUjHRonnqBIPg8auUvCvKyYlKtvKx1TKyI1WJ+Rm6LTJDvJq5+rvMNKQTQoMjk+
+OSgzu8QtHBgfSq+tr6uopqvkLiQoODw/S8yqoKK0RztJ4sDUQjHAkpZ2IA0PQrq+tLasnJzB
+Ox4fTjs6OS6xmpmcQR0uOMC0/r7F5dUwISisorHjDw8iOaqzxbWtqp+2KSkeMcpDRVNrp6Kj
+o7G7xkA5NzvSt6+zemK7ud03HxslStF2WEt3ubi8Z0Pvyr2+XXDmyLr6R0FA2Ly1wnKpoL1g
+GA8eJsWzOz7gu6Gfu8JFRrrT/0EpO9Ht2TM3zb2mo6mqucrA0cpoLzBBPzIsGBYkOLOosa61
+saWvxlgxPUVBTzo4TVDLvsK+xryvs85NtKS1viIUHy68qL3W7e24qspSNCE3S1XKNzE+OcOz
+rqOopqi9y2E2LzqzsLdDEhYaKrS/11VOwp6gpbQvPj5Pw+FOfD9XYk+6rqOco7NPN0fpx9A7
+KCg4REIvHBkeK8+1v8jYuKelqL5AQkxP4VNW9vrCxNfW38y8zMael6CpHw0UGT2rubfLVK2h
+qq46HiYqTNEtNkpirb9zwcKhnKKlvldJLiYtyWJrKA4SEiSyqp6kq6Wfo6PKJiMdJDU0Wsm3
+oZ+en6amrcHTSjw4LfuoscopEhEYJ1q/zFfcwKuirtQtKUzu10gu1KqclJmjuT47MyxMqKa1
+MQkGChS1nJSVnp6YmqDBFw4OEig5RdjEoJKQkasqIymzmpqf9iQfHykqIjn52LQxGhoaOKid
+mp+vtK2srVQlHhwlPtOwq6imqrLMR0A/OyktusW+2x0cJ1efmJusUDI25Uc0IRciMr2hp6io
+qqCm7jElPKaZlZzNIhUhv7arHAAAAhOqj4iKkpKPlZXOFAwIESxprayvnZyanzsjKU6el5qu
+KR4fLOVBKSMjLEI9KBwfMLCXkpej50xd/NIyHxsZK76kmp2kqbq+0DssHyG1nJyeJg0PGcaV
+kpquQEqxsrouDw8WKqWen6a1p5+kqywZHS+pnKHCHxQpqqCbIwQEBR6bi4WMnaassKcpEgwJ
+Gc2bj5ejp7u5zj48OWisp6StcD85OjwvOD00OSsfHyVIs6Cdpa/K7spjRjIqLjA9e23Dsq2j
+pa68RzowJzGvoKiuHgsOFE2XkZKcwL2xtLI8FxMTH7isoafLu7iyq1AsLTuklZScTRkPFO+p
+ncEKBQYWn4qDh5jITz++XRoRChE9oZCRob4tKDlPubi4t8a+vMjURzU12ainrWIcFRcg16aj
+qcw/Qtm9uE0qIyjyqaaq4DM6zaOaoL4mFxYkpZSRlCEKCQxKmY2Kmr9LRrqn0CoWDRsur5qf
+r2syXsnEyy4sOcKclZajKBAMDBY1raWqayQtWKeVlZuvMysuRL68UUEvNM28rKq5vcq+rK25
+USkfHyMtx6WoqU0fHR0xsZ+dpsM6LzZM0zYoHRciNq2al5eiyj4wPtC4qrPITS4mLa2fmp4b
+DQQFEzqPhYOIl8FLPk5cHxcNDBY1n5GPlao0IB0wup6YnatPJyQpRsXoRDMsLi8vLjA1QLym
+nZqis0sqJCEpOUFFODzQqpqUk5m6KBgVGR9HpaGevh4cFyawnZKVpdExKDf41b86IBoUIUap
+mJaYpMtNO+G2s7DdPikeHjW1qJ1IGQsDCxK3jYaDipm7Lyk4N0YnGhMQHEOfj42PnN0+OMqo
+pKbsHxIOFCqrl5KYxh4PDRYtsZubo8Y0NVOyoZ+nXhwTEh6/l4uKjpvoJh0fO7+utyQQCgkU
+NpmKiIqYZR8XGyg7cTwhGRQdPKSRjY6XuC8fIjW+oqCpzSkcFxkrTLWt8EAfHCU7npGNjpiv
+MhwaHig/T0svIicxtJqRjpKewioeGx4mLzo2Ni4tMjvHr6qmrL5YMCwvNEL53c1zPjs77Lys
+pKSlrrm6vLW5xk8qJSIkNThtPSksJkfDoJeVl56vRCofISEsMTU4LzhJ0aqemZeco7rSV0dO
+QC8hGBEQFSPcpJiXnrYtHhwkWKibmJ2teywmKD6/rKizVismLF+nmpebrzQeGx8z07mzSS8j
+Hyw7sp6bmaC6Nh8aGhwnM0luWnpdxa+kmpaTlZym0y0dFRMWHixZ9t47JCIfMdGilZCRl6PJ
+MiQkJzhIYm88MykmL1ynl5COkZu1KRYODxUeO8u8wTMoHyA5u5ySj5CasDofHB4nOU/J0Tkp
+HiNDp5GMi4+fRRwVFRkiLkM2KiIcHyhqqJmSkZegvy0fGx8pOWHLxd1aS0/OsaOZk5GXpXAj
+FxAQFR0sUmvXUzMuKTm/qJyYmJ2ySSojKDBWvba9Ry4lIze4m5KTmq48Ix0fKU/IvcwyJxsa
+JDqunZiXna1WKCEiKDtQxcpPPzA4RsWklo6Nkp9NHBMRFyI7X9swJhwbJ1Gfko2Nj57BJRoX
+GB4pPj8yKicsSbqgmJCPk5ijwDEdFhIRFBopRb+0tbfOw7+0pqKiprdXLygrNXq/trTROywr
+QMClnJyjyCwdHSpLtK65RhoUExzSn5KQl6dEJSElL0piVykbGBszsZiOjY+Xo66+wcpTPS4e
+GxURFREbIz6snpSQkZWcrsUyJh4ZGhgZHyvlqp6WlZWWmJeco7gwGxIOEBUiTcmuscvmTNa1
+rKmy/DIqKzrgw7nORTYtN+60pqOptOhOTUQ9Wri9tE4aFg8XP6+Uj5ebq7OuuclLGxAODRos
+xqajnZuZkpOXnsFALCcqIixRP84jExEMGS+xm5qdnKGem6u8LhkWFRooKz3auZ+alpKVmJys
+wToiHRkZGyN8vba4PDpFP8LDXWxCV7u1srpeYH5a3DUkJS62nJeXnqquu07srUUiFQMIDxiu
+pZiNkI2Nm7AyFBUTDRURGeGqlo6Sj5ShpOQvOC0yQC4rJj+frLglDRUZKDg3856SkI+ntc83
+LhwPExsk5syunZmVl6Cipr9LJhseHx40trCrrejOvkIvKiErNTBC2b+op7GuttlvLiUzPMis
+raOfqavBQaiZNBkVCB0iETW2mo2Vn5ibulwXESQYFh0ks5memZOXlqg57c1NXDRLuj8iN6Pn
+HBsMISMRJsmclZiej5Cs1CIfJBINGB1Br7aYj5WYnKijwSQlJBgZIPVcNqyurKD4ZKNxLjgs
+PkYoO7S/wrzPrb4wQVFqvsvCp63Ry0suXKI9DywbEyYV75qmoJWWnaI+StAXGB8eKjVDoJef
+l5qhqMs+zj4rT0lGVS4mvp0gFskYGx8awa7ao4+bnJy3plIcLScUGyIts8enkpmhmqOvvisu
+LhQouxIcqSxGo76koFu4rjMrPS0uPStdxDnFrsW5vsGpuMypsN7A4jYuJ76tECO/ESU0Zqau
+qJmVsqCjPkolKCodHklHOqqrpqWqp6pQ4uQrMTAsLiolLq+sFrmhF9e3Pa/RzaO526auNL3d
+OUYqPUMxSbHfsqSxqaq9t8YwQS8bKa8wErmwGuGhT66mvaS7SK3BLWnWLkFcSvRF2LtUwqq5
+uKvK4FsxNCgeIUKvFRqfFyilV6SuoJ2mtKieKsvWKjwoQDs/TbjHT6a3tq2ts7rC7HwsNi8h
+Jy0pJjSzqx+8mh7IpkmuzresX/6z7jfJRzY+Mkg7PL/XRri5c7vCy8xn9GI5N0I3/LA7OqdG
+Ma9Ld7lXvb5Oy7td0sJk0U4+2kM/zFJRydXbynR9+FPbY0xWRD8/RUZa2cPP07rg1MzWvdXO
+vs3PvL/Dw9dtRTc1My45PDpSS1tc6cbMzMHLaeF+Pm9ISFpVY2XxYcHYw8Pst+PsxObNzszM
+xdb14GzM1F7jWVZMOUU/QEM7bDZZO0ZaSM5Ryl2/0ujMUbxnv97Ov0y11bvA671KzkRH2kbH
+VW79TPpK8ltN0k3tZUJMNlM8TFBc3UDKR9reXsdgwdzNzru4trq3usTFyr7Fyct220lJQTPW
+Ml1BOVQ0TjtfSV1ST35LTF481VJPxEDUTM9OzM5JxFa8ec3K2LjBu7y7yb3Jz8jYzOVqUUtA
+P0E7QUw+TUFN80TsbEzBSOlaWfRm1j7CR+fOXb1kwefBzXjGOL1NTc9E61jRV87Kdr1bz9VU
+31VLZk1LYklQYu9YvFXbzEfHRldr40TabEDBTtDF5b7HxL7B0cHSwufFWuzOOr49bz9IQ0m5
+LsNWPtI27DpmRk/ONbM+5LtJvlJ1TWFS9t9H0Gj30urh2s/PzNbLzfzo1fNjWu3lXdBj1t1m
+3d70Xdf7SOptQHN1a/Fz32bh6/loUVx7Rm1IT05K3lpcbOlW0F7LvULMd13DR+xJfbk1vEfn
+vma5QbVzV8I742c9WVZl/M/YyLXRucBxzldRVUA7T0pPZOVt5tPbZcrYyc5OxlZoWuJpUs9k
+9Gjazmpy8WFYS+FHTVk/xGYu52E8u95z089OQ8VEVFZZble7Wsu9z7TLt9e+4UXaPE1BWk5n
+10vKWcbQ4MdW5EdQTkP2OU/eS7pMz+hpx2HHRcNCPts7ak3kzM/NwM/WucnoxetFV1pDRkJC
+TkhLfUn111XXyMnazWXczmrQ0c3O0dfm39hr4k3cWUfWWOpsztjixMrp09vQ4+vfSUo+SE87
+V0pb/WPPa9jJ0GrXwz9TTkVcP1lg3+5c0u3o39hw3vZPc+5q0d5dvt7aylDQyU7S2mlQ4FBO
+y05PeXNtfcfc2ctn3FhsV0tlP9lkP2VXWGHnS83NS9NhXWZV4m5o/erm2tvRdtLZz9D90FpW
++OLcUuDNYO7ryeXQaGfPSkpMTkZQ8GTxXWvcWsXd6r/o3cveVOvbas3UZMjZT9Ns8uVZW93R
+TlnmSvJQT+9HVUxTXvbUX1pxRk5pXun34+7PyF7XwNzOYeBpSltD6Xv7z2bJzW/Q99/f0N7s
+01f620b1XWppYdlNXWhLTufURljOallj1WJM0+v9WWphT3v73OJp9tLlbs/jWszd7Nbm38/r
+zsxNUU9qQ1Db6tvse0zoX1fQXNPdXMzXbWTIXl/oR11zemPeycvsyMnq1Mviychuz25SWWpF
+S1FFTUlUR2vqSM1tbv5g+Fpuee1f7s/ebNHN7/3g2VVMW11HUGlZavLy7MvEyL7Gx8fZ4mJc
+TEhJPFFFRk1uXFDnbM3J0cjQxN7u1llUXWpKXVP9Wk7fbul3bW9uWG7ca+DQ09DMxszN4en0
+WlRKTE9NUVJod2776tLi2NLT1X7fX15KQldJTGFtzM7UycTQ59Lj5Gzf1uPOysfN0NTX11pT
+WEdETEc/QkVITk5b7tjTzdLMzNls/WpGRkZESE5ZWH3z8s7Z08fIycPP583Z09Lfz9bfX3vv
+XmJSVXBVTUZPc3F23dTIzdnT6n9eWklEQj4/R01KTntm387Yzs3MzcnF0ePe6/brbHJcXGfx
+5G7k9up26+9ld3Dd/uXf0srLz8rL325eSkhBPz5DVVVqZtfIxr7GwL7BzNriYVVAQkQ7S1tr
+3+fb3ub69+VkXFlQWlpcWmrm6vNy+mxhfephV2RiZ/be3drOyMPHys/P0tXmYlRHRUVFSEhK
+UFxl2tHS1tDT5mlZX1lu++jm6uH2ZXra4WpqWE1PS0tPW/XmzsjIy8nExdLe3F9VTE1JRUlJ
+Tl3l5NnNx8LEw9HkeWFrXkxMTllha/xyfO9sXGVWUF/609TXy8i/u7y/wsbQ7FtWRz5DQz09
+PD9OdefSyMXFx9ngdFlWT0tFQENNVGL97ubc19vf6npjae725evezszJx8TCxMrU8F5PSk9M
+SElPXFzo287JzNPd311PSkdEQUZDSFNZa+vWz9PP0tvm6+fo+GVYUGR6bN3Ny83i+mBYVmNx
+av7tavjXzcrGwcLHz9xeUkxMS0tNSUlKSFJd8c/JyMHDycnN2WlfVU1YbXr52dfX6P3haGRf
+X2dfWlVeZffkft/i7fBcT0hESEdAQkdLTWbezci/u72/w8nf2+tlZ1lbWV5q59/b2NHP0c/r
+/n1xYFZWTVJOWF9n7drW4+xvUk1RTktKTE9YZ+/a087LzdDS52lfXltXUU9OTmHw5tPKxcbI
+ydnxfX37W1xVUE1dcGzc1NLMzdZ9aWVPUlVXV01YX/ncz8vJx8nK2N3f7PNlXVRPTVrz4s/U
+zN/n+1hcV1JOTEpEQUtTXW/26e3u+G1w/vB4W1daUlvv3NHLx8bHxs7Z4uTh4u/ycF9ra9/e
+49nX2fb072FSUk1NSEZHUl97+eXW1s7O2flcXVlNT0xKR0dVd/fd2NXQz9TT1ePb4el0d3n5
+2djSz87Z4Ojv+mJZXFNXbmXo2djV2c7RcXxjTk1JSk5YWVpl9NXOzMvExMPJ0tvsWU5XTUhE
+R09Vfv/q1eDfd2xhW15UX15STlZlbfX37uPc3vb0bF9lX2ZqaPDX18vHxcTEx8zU2N1+XEpI
+RUpNU1ta8N3a3Njne+n7WVdMR0dASU1WYe/OztDX5Gxwb1pYWmpncdvPzczHw8jO2fZ4ZVFM
+S0RCRk1f3tbLwsPExczT3N75YVBIRUpYUl9ub2ry4OvV0M/L0tjq8N3Y1M/Ozc/Y3l1geWFT
+TkdDPj9CTGj+187Ky9bt+V1XWE1MRkNHSlh33NHLyMXHysnH0dbeXVBPVVhcderc29zV3Wlo
+bWZiV1RVc9ze2c7KytDZ6WtXTE5KRURAPj5GVFfq0s7LzcrNztLg+GxjT09PXH772t/Tz9nY
+4d7p3tt8bWRq6t3l7dvW3+j18extXFlWSEBARFRo39jKwr/Av8XJyNntZFlOSUpSVFBe3+vf
+2uTm3vRmfu9mT0xZeW9oZ/t7fWhbUU9NRUZLS1Bk48vFwb6/wMXI0c7a8HleXU9NUmV/b3rv
+3trX3+Ts+GVaX1hNUF5j+/l1bWBZZVhRVE9PUllZZG/dysjHy9PS1uB1fW5cUkxIREZV9/3j
+09TKx8bIzc/e9WJVTUpOWGzv/WRmd93VbXNlYn9ia3bq2MzGxsPKycjN0t3lWEQ9Pj4+SFRr
++dnd28vL0NPX71tMTE5KSE5QTlRYW15+ffbY0MzO2NTX6+7bzcnNztTQ1u1x62dOTEVLTUxX
+6NHS0tDKzNfe3OFrV0lGRD49QktPWnH818nLxcPE1e90VVJYW2f0+9/o3Njo3OHg6nZSUFld
+ft/Z2Nni5s/O2NXe3G1OSz8/RU1Ubebi2s3Lzs3LzdD9aGJPVWFv6N3h3NzW0t3d4u/nWE5M
+S09ZX13v6ejm0s/r5WlNQj08PUFFT1h62dPMx7+/wsHL1X1cWlJWZfjs3d3SzszPy8vM0OJc
+SUtGREdLVFZ27uPX2NrV33RSPz4/REpTdOXezcjHwcPJyc/rWk1JS0xNU15u6tnQ0cjFycbU
+7lFBQ0VITV9r5uX36+Pc2NDb4vVdWlpZUndx8NbN0czGw8K+xdJ4UlFVWF5ycOjl39vZ0M7N
+3O9VQzw9P0BGR0tOVl5e5tze2el7XktKTl1aaG7m0drDxca+xMnV81JNTVxw9t/Wy8jExsfH
+y9xvX1NIPkA+P0RIT1Vk6dvWzM7f7mlOPj1IU1rt39/W0dLRys3T131zVUhOXF5+6Nzk18vS
+1MrO2Nb6Zk9ITlBXYPPp383KzMjJ1tb+UUs9PElSTFNp9tbMzcm/w8PGz9hdRktOTUv+9HzT
+1NPM0dtlVE9LSkRQW1xj49rh2dTa62xkXFlWUkhBTFNbZ33OxsXCwb/Cx8/h9lhER0pKVGLf
+2uTf1dXP0+f1al5MSVBVV1pjb/vv393g3OpfWE5MSUVJT1/v3NDPzcbLycvN3GhYT0dDSU1Z
+a9/YefrWzsvHyNXjalJLRktTXnTj0dPP3ebV6u5qZF1dWVNu5d7RxsLGy87Rzs/edlBKS0E+
+R05UVvff39POzs3T3mJVTUQ7OkFJTFZ19N/OzMrGzMzQ2tj+dflr7tng1dre1NLQ0NLka1pR
+SURDTFpc/dze3tnd2t7laVhbT0hJTVdrcOzW29bP0MvO0uNcT0VCP0BMUWPmzc3OzMnI0u14
+cllYS0hNWnfpzMjIxMTDwcjL1N7+TkxGRUdJTFpeZXTs2dnPztzk5l5ZW3bc3dPN3+t0Z+xr
+aFxQUEhIR0lZau3S09fP18/W4vpoT0pFQEJKVl/j0MvPycPBw8nO425aU1FNVl5093zl393c
+1c/Q2vRvVlRMSltpal93eltj9Ox673xZT1ROUF5+287K1NPY3NTa3ntcUkxMTUpOYfLa0tTT
+zM3N09zk/lFIRUpMTG7Z3d3e3tHL0tfU4mFbVlFVXfPg1c/NyMbIxMDFxs7lYk5KSEVOWm/o
+bWJzcmX7aVtOREM/Pj9GT1JUbXPdz87N0MzQ7V1iZmJm/+bfzsnNycTK1MjLb0pJTUtMVv3f
+0NTW0cnFzc/R5FRMSEVCP0hLT05RXevW4e7Z0+9oXFBRU1rj3tbe4tnX2+rb2fJlT05SUWv4
+597i6eLb18/Sz9bfaFZISlpd0s7d1M/X1tBhzczffFFkQDpWcOjDxNPJx9btycrPXU5KPj9A
+Q01Za19ObdnP2dXN3O3vRjhDYmz10N7SzdtPVmb05PlyV1FUXtXLzMnU1cbFztPP2vpeWk5F
+X1dS/V15ZNTZ89z26eV7XEtJTUpIWV76b97b39Xj9W9QTE1ESlVZWeHk8drLx8nHycXfcWFR
+YV375d7gZuzay9Dy4PXd6tria1xVXnbp+l9JTFBVbmzf1dtsVGJ+3MK7wdfTzdrTz9n5WUZU
+XU5MUVlZT1dSVGdv3fj94mRsXE9XT09a5vVnaX3Z3d/d1dfu2tdvV/PyfNXe2tbPz9nM1tzb
+aWFcWllo4uR8dlpfUVd0XV93WUdFTVRFVO9VTt3k2s/R2Nvm23Ro3WVlYOxsb2fczm7g0tra
+ye5nfX1PSXjsatHN1trX0NprfnxGUWBSTVFjbFH7X+vS5N7vztDb29HJeuXK0dnUb+1eZ1FL
+XX1oRFtSREhk2tnTwtPXyuVuctt7R0ZPQkFRTFdiUXDl28jByMa9xc7Z5V9QdPVMV/5XP27c
+SUD9yklbv9JqybzBurW9yL3JVklCPjU5OzY1PD07R1hNPV73TFrPyH7Qv9zVwcPIt7O0tbO6
+xcbSZEVVQTEyODIvPE5ESNR9bMrCxL62uMnLwu7b3m/Q3ejZ39vrR1RmP0tLRlJLSGlv3spk
+7cpe2dRY5dtNU2pjfc7MzcPD39ra5F1GQDo5Ny8wOi88Vu3Nus7Hr8G0r7CvrrKzsr+9x8jW
+Wz42LCcpKiUpLiwwRXDfuaumpKCipqmuumxFNSglIx4hICEsPTtasr6xn6emnqesq7zRys9B
+Ndo2Ikg5J01PLEXVOETTVVzCvbuxsL++t95s105p3WrbyMrGvbu9y8tbPDs6Li00JyIqJic5
+N+2xrqejoJ2en6Gprr9IQDgiKSMcKyclOD/tt66opKOkrrK2Pz43JisoHSIjIy02Rc2vpqin
+nJ+mn5+ns6/KLVY0HTEnGygmICswNFPT7si6t6+rqKmnqLOwrMnFnMspoh0fshQmyB8nrCUu
+sihJxUdavX1M3+NPxrbEt6vEuKu+uau3uLPjPkQ0Ky4rKjEvND5HX/e9rKytq6usrq6str26
+z0xdTSpAPio9LyYtKykuLi0zNzM6P1DYvq+pp6aloJe+rpkeq7MXsUscW8se1sQfwk897e5I
+Ue87N2VAR8fCvK6wsK2tu7rLSV45LjQoKCsrOUtL2Lizta6vt7O0uL7Kzczzy8pO3VM/60lH
+TkVANi8qJyknLjY5TVD+x7i2qpzbnpcnm7MeoVwqq88lrGkyrFvjvWM7SDwtPU4+5f1cxMbJ
+ucS9wdbvSUE6Mi8yNTA9UklbxOhfu7/Eu7nEyrXGwazJzLjp0MVa3OJJQT00Li8tLjMvMjg3
+PHeuOTuWKjeVI0iZJ06YKVaaKK6ePairUd/YNzFUMDfQOEhvU93dyWjXzj1qSDE+OT9EPsRV
+Y7PQy7jDzrzAP83dO9ntW87i7dXBx8q6ysvVRUg8LjQyLzYzMDk7vsI8sbDSura0y7ux1sCu
+ylCktjOevT2fNDBwIygvLy9BUDJ1ZzvbQkhxPu6+MT6iHHelGp6sNpmqv56tv67PWEQ8LCw9
+KC05LEZS2MC7ra+urLu60kdUNS4sIyUlIyYtOTJDyV7cwbqur6SnraGksqSiuqioQsVhKzs3
+MT4/MTM+MTc/Lz5LQ7PPLJ44IpogcJwpqKE/rK5Dvls5OTkqJjolLzooSknerrGgoaKfqay3
+0cdTQC4oKSUlJi1DMEu+X8XBvLW6qK65qq6/q6pqrbU4wz8tNC0sNTYqLi8qMzEvPz1sr9k4
+mUQpkyS+liaonjqpqUqvxD1POSssPSw1SStCQTy9zLCorKarsbvZ30hFOi8vLC48KzjaNUnO
+WufCvN61qMqzq9Gxrs61us/jbkMwOi0uSi8xOis2MS43Pq42OpkqSZouvZjPV5fQMJQ0QJoi
+zrooRzUtNHRPO7pJ/bJSt7fPtOhVTi0tKT0xIsgqJa00Rqe9xaanWqKqLqPEO6k5O+M8XmLN
+zsrBUMzuPl4/Q1IwNC8nKjs0Ir9SJ7HJVLissribunOZc1+bT92kL0zRLDg+QkrB0FK6XkvM
+OEhOLTkxWDAcqR0omxmvnCyanW+co0KpwjLXPCxGRCdGPCzLXu2rt6ysuLTaaUI5QCstKCEt
+MSk2aT/dwry3uq20tqm32a+8Pqy2PalYN7o7SdVcyru93NFxOz8xLjUoLkU4H7L5Fpo4LY8z
+qZNUoJ5btMAvNTMlIDAjJUEiOes4srOynqmkp7Sy1lNJNjMqKSooLTlXS+2zvryutLO3rrDI
+uL5Myr5JyLw06VIz0UFG2dtt/Xg+P0UtNS8pMykq2Tsgn1ookTW2jk2alsKfoUy9zyg1LyQq
+MyAtLyJIPTy0wLKmrKyqtLzFeEY0NCosKyUsLjXp3v6yr7isq66vsrW+xb/oTshOPMgzO+ww
+bdpay+VmXko/OTo3NjUwNjM0PV+tMLWXHp6cH5WrK5nYKaE6JaooKrgkNewxPss9fLRruazK
+q6y+rLrKw0o/NCwsKCsmKDA7Qka2xsejsrCnrq+yrNHouTA63Cg7TyhTTTvT28e+vb/N0F1L
+Rzg9ODE9MjM/zE0wmjgykyW1liimnjmrqDTbtidL2yhC/yU/TSf1STy3ycKvvrq8ytpnSjw2
+Ny8zLzDQRDqkSk2eVbSdt7ShuWCpTCq6JyhIHykwJzZFSOC4vLeturWzzdB5OTMxKSowKS9s
+yUO2pOyspbGprquyzKi/NbXIKrq9HsHkHLlDH801KtJnPLy9VbnISchMPF5COUVLvLMunLMk
+ki/Jly6mrvHj30kkXSYgMCUjOCosSjhX3cS2rqilpKOoq6+751owKycfICQiKDc8zLSrpZub
+op2er6+6TTwwKysoKzQrQ+Q8vbrzt8ZteEg1Ly4pKS0tMTtGWs++tK2pqqykm73Yki4onhco
+vh4wVVYsva4wtbRvxK/YSfVVKzpcKkm+TsOqt7Krt8rIVDozMSkmLissXU/YpqOpnpusqKPX
+4r84LTMoISgqJy89PErU1OK+vtjFyl/i201k4lpdUFBPXN/x08XXdradxCqZrBi7qhstoDQi
+nq8nqadcxau3Nb+6IS5AHiEwKytR007Gs8vBuOpd50o9Sj5EW72fdl2SOciZNrDFy/c52DEx
+LDUoIkAsNVv47de/r7Gtqayvr7nVb0w/ODU3Lyw2MTNLRFzCvrSurKiiqLqsvDBTaC44bz9C
+Z3N4RuzkQVRFMzI3NjA0PkRGaM3Oybu3t7q+w11QzkxBzWCqmz3VlykpsSc1P0jNP+m78UvB
+xTtaYF1EOF4+Me3T9sSwtsnN3j4vNT81N1RUS3rNwsGyo5ytyJ9KK8hN3Unbt1xJUUIpJi4m
+IScvNj6/r7SqpKu0u8PbSFNuQj5k7HNPaNBPU+xpR0zpbGi0n7s7o6gsNrTASr+vv8jXyc4x
+M0wvKzMuLC8/Rj9LT0xLVWvb0cu5s77LwL3DwsO7vs60lpkhzrAUHxws0Sq5pK/P0sM/Lig4
+LyE0TOzHvK6puLm2v8/Rw9JrWd/4S1BhUEc9QUE0O0db0cqzuayeprnDuEw9ODhOMTpGNS8q
+KC8tLz9OdM++tba3raqsrrGtr7u5v95GOjUwLC0uLTY8RVFp18e6uKCb1T2qWCIo97jAvLKj
+rknGwzErJisuHSY3NDc0eslluaehp62nruNLQEJCO/XC0NPBtrrd5+ZMq6YqK0McIBslstqz
+qKKmyVE9OyMfKCo0Ls+qrKumnqKtraqsut7HVzEvKiwoJzU8RERMz95lzs/R0dG+ur62raq5
+S3I/KCwmN1AxUuPc6E9XSkY+P2vy3r+xrKyppqmtr7W5y15LRTs0MTExLzAxOj5HTVj07NDL
+xb26rbJhRjk3Mi5byL+7u7S1yllRUD4xLS81NTtZyL25rq2srrazu8XhYnxNUVFJXFdaal1r
+Rzw3NzgyOk1W42JK3OpqafrJz8zP3s3e7mZzztXT1enp+1xRTlxzbWXQvru5trO0trq+zeNT
+Pzw0Ly8vLjQ5OkZW8/Lu7Hx03cS9v9X3clVeYeLGwsrNx8TPeVpNTEY/P1Bz5dXJvru8ube2
+t7u6vsHKz9bR11tBOjcvLisqLCssLS4zO0zw2ca4sK+1urzI2NjKvru3t7q4v91VTEpKQTs5
+PD4+Qk7UwL66s6+ytrq+wdVVR0FDPzs9PTo2MzQ0NDQ0MzM4PElw3L6yraqrr7S6vcHCwMPF
+v8HLyM3rW05GPjo4Nzo2MjM6SVvXvrOura2rrLG7x+VPQzo2NTAyMzIzNTY4Nzg7PEJDSl/T
+wLSrp6WmqKenq7O8yV8/NDAzNTc1NDk9Q0xVX/n9YlpUVVp70cbEv727v8DByNPzXUg+Pzw8
+QUA9PDk4PEhPTExb7//Tvrevrq+wsLG5vLzI0PR5WEQ9NzY8RUlOTlVOTFBFP0ZDQUZGTV7s
+3tnP0Mq/vby+wMXJyczP2u7sWUhIPTg5NTU3PEZPY9vEvLWwrq2urq6xub/F1+H1a2hcTkhK
+SkRAOzMvLCsrLTU+YePNvbu7vcfN1HlRTkg8PkNLaNfJwbeztbOztLa6vcvu+FtYSUdFQUZI
+REM/Pj49SE9a5c7Fwr28vLy/yNHjV0tHPjo7PEVER0lHUFpeTkxGPT9KUWvdyr+7t7W0trKy
+uL/M7/NgTU1FSU9PV2Ls3M/MztTpbWRdUk5OSUtERkU/REtTSz8/SFzuz8O7t7K2ubm5vMLK
+7lpIREVFSkhMVVpnZGFhV+LnTT87OzxEWXXlzcK+vbu5t7S1vcrcYVxWWE5LVllVUUtITE1Q
+TUZKT19kYHzd3N3da19UR0hLRT9BRlPsx7+9uLa1tri6vLzB2XNdWE9QTElISEdDPTo7Nzw/
+NzU3PUpY3c/HuLSzsq6usbO1vs3lWE5JRz87Ozw5OTw8Qk5lXObFvry7t7m7v8twWVY/PUI+
+Ozg/S2fPy764s7G0s7a9wMXjWE1EPDs8OTg1MzY3NTQ2PURAR1Nd7c/KwLu6tLSyr7CxsbK4
+u7/J32JtWktAQEE8PDs8QkpJTVphdOHM0OH/fU9IRTo4OTs5PURO9se9ubazsK+yub6+v8r0
+YGpTSk1OTE9XT0tJQ0NGRz48QEhJVF3+7nzYzs7S2MrIxMC/uLKzsrK0ucDL5lVCPDczMTE0
+MzM6Rk5h9ODVz9nb9l10amZfW2Hz19HLx76/x8PLz9X4Z2tpZlxj5e3g0szLzth9Y11PQTw6
+MzU3Oj5FT1hdZPTOxcO/vbm2srKzrq+ytbrE90s+Ni8vLSssLS0xOT9Z68/Lzr/ByszIzvHu
+3u3VycjExMXM09H4bGheYlZOWlp339DCvbm5vL7L3XdNQTg3NDAyNTo+PkBOat/UzcfHv728
+u7u1r6+ws7i8xv1NRj03MjEwMTMzNz1BP0VUVFZrYFNYXmDr2ce9ubW2uLS4wcXI0+5dUVRT
+TVVa28/QyMbL3el2WExBQkFBQ0hWaE9ISkZCRUg+P0FGWFl038i8vL26tba6ury7v8zZ1s7c
+5dXN2WJdSkA7Ni8tLCwuMDhAZca9u7avs7OyuLzG09/3+11s+d/da/zzWVhdWV7zd/3s3MrK
+xcbKxMlwZlFGPjUvLjA0NTpDTU1d0MzKvsDEx8jAu7a2tbGyt7i8wttLPDIuLCoqLC4yPUhf
+2MvAu7u3uLq9xsjO3fLm9HtbTE9aX2ZiUlNJRklYfd/JxsLAztDO5GZRSEhFOzxASW/k3en0
+6mpfYmNlXF976NTNwbi3ur3Gw8jR3/TeaVZRS0g/QEg/REtFS09QVVtnXWvd3trLw729vLzA
+v8HHyNTna/vZ3Nn0ft9OQz02NzUyMjU3QFfmz8W+ura6vcbOzv5QRz88PD9GTlVU/+vu2tDD
+vru3u769vsK7ury/w859alFEOzQtKioqLTU9SlJb4uTbztHNwMDAv8G/u7S1vcrT/U0+PTo3
+Oz09QEhiy76/v7+5u8PCzd3f9lJGRT9DTlRcUVllRUJCQkRKbOTcz8K7uLS3vb7Cxszd6FtP
+U0xYat7Szcvdak1HRz04NjM4PD0/SlhtzMrIzNXKycnW6m/dzdXNwb27uL3D3W15WUU8NjQ8
+Pz9NeunSycPDzMvFzd1kVFlndffs3fdacm5RQT4/QD4+QU770sW9ubm6uLfA2NzpW0Q8ODxE
+Umhpd/fczcrY5+LeYUhDQ0lnz8rPzszExM/d8ftgRzw9QEpx0cjHxsfBw8vdevN0UUtQUV3V
+y87cbF3r3+JaSUtLSENLX+zLxc/Pz/b+YUo+OTc5ODg9Su/Buby6uLq7u8LZ5mRdT0pNUdjJ
+1N3g2+rgc1dXTlNWUmB15czJzN98Z1xPTktCP0VERFN138rAwcvN2t/g4HpKSEpQUlNz3MbE
+x8zL3drR9FZFSU5SVmL1y729xcnQ4v1bXk0/P0ZPS05t38nIzc7O1s/X23pTWm5hYOvl0dHZ
++mVcT1n4bU9LT1pq8dnHv8HKz99fUk5LQDg2Oz9FTmbWx8PIyc3Z1dLY8ExLV1lYXe7PwcPL
+y9fd1NjdXkxSV1VPT1rv1dLa6WVZVlVIRElOWVdf6s/DvsLM2OV8ZW9XTE9eYFJYcuLP0tnm
+5H1RVl9STmTr7uPSycfEy8jH0eRZU01FP0FCR01XXnXMxMTDx87g1d7o79bM0MrW4c3LzdHl
+42FWTz8/SllPWFZOYezq+HBWW1hOT0hASEpLS0lZ287Jv8LDyNrqY05PXnP62c7Gv76/xs3S
+2mtSSERESk9SVn7r2cbEysvV3vZcSkA7PEE8QEhIVHTu3M/Ex8zYd11g+3b32s7KxMXQe3lv
+XVJKSURMXWDl187Hvr/O1NHd7WFRS0NNTURSYV7o6H3b5NrO0tfS3+zcbHnm4dfMwMDDy9Vs
+VEhEPz0/Q0xfbe3UztDn8dfvYl5dU1hcXPv2d+7s2Nzx9mRYXGNtaH3m5ubw6/LfycvK0ndo
+VklFQEBETVViz8nGvr/Ey9LdZFFHQ0lPXFxmfWnUyszIz97kYF9aT115Xk1NTlJqaWNj6dre
+7WlbUmhs6tnb28zJx8TR3ndbS0Y/RmN57N3Mxb/DytDtfV1NSUI+RWd54dDMwL7BxdTX1t3m
+Zk1AQ0ZFUU9OW+fe2c7P1dz9W01OWFhNUlNr1dXZ6VxXaF5lW1BZZW7l1dPCwcbL5mZjUVRW
+UWjw2c/N0cvBv7/G2OhpT01DPjo7PDs9P1LvzMnLx83S2ONYU2VTVlhWVl/m3Nvk19nra15e
+7+TayM7Tz8bHz9j5Y3NjUU1QT1J4+vPW2dXU6X1hZXBuRENGRVbc0MjExcDDx8vO1XhNP0A+
+PEdc/d3Jvb2/y9DP3l9KSkg9PUZNWE5RbWdOTVp67Pn52MzMysO/wczP1vNVTVFMTEdLW19f
+79HLysS9vcLJ2W1VRD5DQUFDTm7u3N/c3HNZT0tFQkdVZmPZx8XExMPGyNDjfFtLQkxUTVpq
+Yl9dZu/n2tXSz9Le6s7Rz8PH12ZabFhaS0tCPTo4NT5a5L29tLSztbq9xM3kSD07PkZU3t3Q
+1OHsdmHn6nRzSU1dZ/fM1Nd1bVNLSERIS0NAP0NacdXVzdrGv8PI1Nfc1fRpX3ly0MrHxsvO
+0+xSS0U/PDs8TXDIvr27vr3EzNpbRz44Njc2PkVTWHJo6NHNys/Oy87d9+/r4NfS0+/5WPz1
+3PZdWE9IRkVb4sfBwcvKzM3N9VVART5FTFDfzb6+ytvVX3hUSklLXU1YadbCvbu3u8XKytDh
+X0Y/OTg6RkxRVnbv3eff1vFxXWFrX1VUbdXbzsvW7mNkWk5LREBRUHHWzLy/uL7K1e5yWVdM
+R0hcXunT1M3Szc/16/NUSkNDRklRWF7c0s3Dx8vN1N9dVEQ+Qz8+T23l+3Da19bP2NXkaWRk
+2+7bxcbM19fN8FBcSUlBQD9NT2zPxb3CvsPA2c7YdmFHRTs7PU5j583KxsDCysfO4GVZU1h1
+98/O2tfazs3z7lZGQDw2Nzg8Q01v3dfNzM3NztfRzeJ75+Ps39XN2G1sUFVWTlNJTkxP8t3L
+vb26ury8ydjlW00/Ozo3NzxIUWLj2NPY1dfc2Nr2X3hwbuHY18/Z3XVTVEZAPjs7RE1S7cm9
+urm7uLrBzNXzWk5HTUpLS1RzbGJkcmVWW1VVYmTl1tXAvL27vMLK0d9xSz49Ojg6SFds79DB
+vLu6vsbO1OhfT0RFRkRHR01IR0dITEpMT1VaVubIw7+9uru9v8LMYk5MSD45PEVLS2fZw8XJ
+v769wsG/zN1bX+NnU2r6YkxEREQ+ODg7Ojk8TPHVxry2tLW0s7zM4/1QQjk2ODs+Q1Zs/PrU
+ys/NxsXI1M/Jw8rb2dfaalZiTz86PDo3OD5ZfdrCure0ure7wM3j4vtaTUtQSUBHYlxPXO59
+ZPbj1t//zsTI1NbJx8zo5OdPPDU0NDMxMzo/S/3Gvbu4tba7w8XI1GNSWWNbVE5RTE1JXFxT
+Y2/37NrLv7q8u7e3vMLL7VZEPjcvLi0uNDpDUuTRyr+8uLe6wsjO2/D1ZlVhaFE+Oz5CQD1F
+T2D95su/vry0srm8vb/N71lPTUI3NDc6Oj5PW/zf2MS9vsC9u8POy7/E1tTM1O5MQD49Ozc6
+PD5GSnbHv7m1uLzBxsjP+l1IPj02NDM1ODpBTlZq3c/OxsS9ubi7vLu7vsPM421MQzs5Ojc5
+PENOc8/EvLe3ubm6wc3UeFRRRT9DPDU0Nzs8PUdNWHHdysK+vby7vb6+vsDT4Ox5Tzs0MzEx
+NjxLZfjOvri3trOzub/H0tfkW01JQz49PTw7PURHRU5b+NjIvrm0tLS1s7i8w9D5WEY6NjM2
+Njs/PEn7yr6+v728v8XHzdp5XFdRS0A8Ozk1Njk8PkBN9tPIvLe0tLS2ube8v8r2Z1RDPDc2
+Ojo+Q0pTYtzLv7/Av8HHz8rJysnU6nFPQTw1MTAwMjU3QmLZwbq2s7O0tLe8xs33WE5CPDw/
+PDs9RkpKVG7XzsrFwb69vbq4vb/Jz9f6XkpEOjY2MzI3O0VWd83Avbq4t7i5ucLL1NrV5mlQ
+RTw5NTY6Ojs8QlRu38O4sbKyr7S4u8HWaEw/NzMwLi83Oj9JW+fTxby6ubi8vr7ExcXCzPNW
+SUE/Pzs5Ojo6PEdc2sW/vLq5uby9xc7W+FlIPz07Pz48PkBDQEhU7NDLxMbAwLu3t7q+wszc
+/lJDPTk1MjI1OTxHcdnKwbm4t7i6usDI0N5ueU9HQzk5Nzg5PENJVePKv7u4tLKxsLK2vMHI
+3WdIPjg2NjAyMzY6P0RKdtLNysbGxMPCxcXFzs3P2HBXSkI8ODg3Oz1FWN3Mvrm2t7Sys7e9
+w87XWElAPDw8Ojo6OD1BTV/j1szGw76+ubq9vsXTd01HPjw5NTIxNzxKXt3Mxr69u7q6u7y/
+xs3W1uFkXFVMRz45ODc5PD9OZNnFvrq5t7S0trq9x9xRQjo3Njg4NjU6Qkpm08nBu7q3tbKy
+tri+z+ZWTkk+PDk1MjAwMzlFWuHOx8O/vbq8vb3Dz8/0XUtDSEhDQEA9P0JGUP3VycK+u7u5
+trm9v8PN8GBTS0M7Ojs4NjY8QkhZ+uHTxr66t7q6vMbZ8l9TTUNBPj46ODg8Qkxp3MvFxb64
+ubm5uLvG2epqVEtEQTs5OTo+QklYYeDNx728uLi+u73L0e5iYUZFRT9EQ0RIRld78dbLvry9
+u7y+xMvW0+FbWFVKREE8Njc5Oj9LT1Rr39HLy8XHysfJxszZ7fh8YVJMRz49PkBHVnvdz8S8
+ure1uLu9v8jpZVhKSUZCPz48ODo9Q09WZuzs2MnFwcPAx9DW72daVkxRTENFRUNHRklWX2nq
+4dvQxry3s7a6v8TM315HQT07Oz1CTWbm18HBvr3Kys/gX2vv+vb0cunbfuZgUE1IRkVPV3zW
+0snBwcjVZmNYT05OTU9iaGh++GJeT01PSkhPZd7PysHBxMPHyM3/XVVLSk5RXW1n8d7Z1dzn
+/mVUWV9g993Rzc3HxsnP4HdUSUQ/PT0+REhV9v/q4ejqdGz98trQzs3Ozc3T3GddTUI+PkRP
+X33SzMnExs3b3PZYTk9c9tzfzsvK1OZpWk1CPj5DTGvY3ce/wr/EytzV2t3mYvvr9uXoffjy
+Z1VVT0xaW/vVys3MydXY+1pQRDw8PD1EVl1r4NjS0Nvu6u9tYWhidt7U0MjDysvS3/lXTEZN
+VFN059zIxMXI0tfqUktJR0xZaO3TycrU4mBaTkZDRktOZ+z66GxeXU5PTEhOXOjOy8bCwcHI
+0fRUS0c/Rk9c3tXHxMjAv8je4eL9a2RbXFxt5Ovz4OpoSj5EPz9FTXD43cu9vLi7wsnW3llR
+T05OTUpNTlTh4/tcX15hWlrqz8vJwMPCwtVhQTo4NDU3PE5b2MC+vr7J1OJ4e+re1szHy8zG
+ys7bWUY/PT1ATFrr0tLIyMjJ0+bvWE1UT05IVVRZeVttb1hST0xVbNnKycXGycfK3/RNRkhD
+Q0tYceXVzMvBxsjMeWVNQENITVVy2NbCvMPCz3dXPzk9ODtJWc3Cu7i6vcPV4G9f7+biz8jH
+yL/Ez3pIPTc3OEFKZG3n3m/P1XZTTUlBPkRIWN7ey8rMy81sTEY+PD5KadDEwry4ubW3v8Xa
+WUVAQ0VPXWvx4uHc9GXv42JaV1383tzczcvPytdPTUI3NjY7RV/Yy8K/vsHS/FxGP0NIU8/B
+v7y7vsXZTkE7MzAzO1HWw7qzrq6xuMvbZkA7NzhCUWXq29vbZlBMVFBITVhi1sG+trO0u8TP
+aU5AOz0+RVj+zs3Tz/J5YElHTkpKU17q0MTAv8XfXE4+Ojg1OD5KZ869uLa5v8TnWkxFRk5o
+d+HCvb25yeRmQj49Oz5IXPLPwsK9v9zrX0ZFQUFLXnblzMbGxdphUEI6OztBTGXVybq1trnC
+2F5DOzo7Qk1b9M/KycPR6OhZTlFKTm3f0s28uru+0tr8TkA/Ozw/R051yb+9vr6/ytvoZWv0
+6NPOycrP7E5KQDg3Njg+Q1/Xw7q9u8rd4VRFRD9BSldifc7HwMl6WE1CQ0xKVHHjzL+7uLS6
+xs7gVU9NRklPVmTdzs3OX1RYTElMT2nf1NjLx83XfUtGQD0+P0RS+t/Mv8G/wdPha05HRkVI
+XPXozMHFxMrW6VZEQERHTv3czb67vsDM62tQS0xNTlvm49fP3+dcTkpIREpabO/azsa8vL2/
+yd38X0tKR0FFU17cz8zJ2GhOS0Q+QkVGVuzRw7+8vsjY72VKQURES2Dz0MzMx8zdc15LR01S
+V2/e38vDv8TO6WpnRT4/Q0hj7d3Jw8bL1l5WTz0+QEJNZHvQv7/BydRhUUM7SEdO6M/IwL69
+vcLK2t9PQ0A8PEZTcNXIwMnH2OtuSUdCQk1oc+LSxsTO0PhhTz89QENe0si9uLS6wMXU3mRP
+U01Iad7dzMjJx9JtU0I5NTY2OUh72snBw8nN3mdKOzY4OT1gz8O7trW4vcvnelJIR0ZQd9nW
+ycG/xM/WZ1RGQEJHU3fb18fEy9j/VEpCOj4/QlXizb+7urzJ505AODQ3PUZs1MvAvry8v8p7
+WEg8PT5EX9nKvbi3ub7PWUM5MzQ3PE9o1MG5tLi6yu9UQTs9QE3k3czCv77EzOVhVUpMWFvX
+ysO9vb7I1ldBOjYxNDo+UtnJvrm4usLTVUE3Ly8yOkd70b21srS6yW9SQDs6PEdf28e+trKx
+tMTvTT47PD9Oa/3Sx8LDx81tTEE5Nzc6RF7ayru3ur/USzw3Mzg6Ql/OxbqzsrS5xWBFNzM0
+Nz5gzsW2sbO1vdhgRTk2NTc+TvfNvri3ub7XUz85Njg7SvDYxrq2t7rC3GRKPj48QVnQxLqz
+s7K6yV9HPDg2OkVW3OXJvb3B0H1BNy8uLjE7U+vHubOytrzSV0M5Nzc5Plnexbmysra70FtJ
+PTs/Q1Dczca8uLi+x9pRRTs3NzlI8tnLvbm8x91URD05PD1AXtHMv7i7vtVWRDgwLzM4P/HD
+vLOusLS91mRGOzc4OUNq3cO1sre5wtlTPTY1Nz5V3Mi9tba9wNdkSTw6Oj1G7sq/trCytsDd
+WEE8PD0+Smz53MK/xdBeTEU6OT09RWvVyry4ub3PaU09NjY5Pkps0sW6tre7yNT+Rjw7Oz5P
+7s7Bu7q6v83WZUpCP0BBS1v40MO/ws3tYUI6Ozw+Rm/Pxr+9vsPRd04/Ozo+QE9u07+5uLzG
+5WxKPT88PkVX7s2/uri5v83STkA9Ojs9SVbcyr+/w8vO2FlNS0lKbd/Sw8O+vcXX7GBIRUdN
+UW7tzsXNzM9zU1hIRkNFTVnj2Me/wsbcX0w/Ozo9QkpZbs/Gwr7Fz+Z/YEtLTE9aY+DMxcLB
+y9Lkc1xUU1p72dLMyNHbc1RUSUNBP0dMUPfcyMjHxdJ9XE1DQkJESFrwz8PEwsjZ/lxMSD8+
+Slzk07+9v7zE3PxVTExGS0xbdunKxcHBzObqYE1KS1Np+nPn3+La71NXVEtNXW7TycjCvb2/
+wNJjUUQ9PDxGSlbXy8XHycvja1tPVE9KUFZmf+La3Nl9VUpIQEFJT2zm1cbBv7/EzeZ3YVZQ
+UFZn3dbMw8XK22BTTEpLTlRq79XPy8XL0fJYTEA+PT4/SVj1zcbEycrXc2VVVU5NS09l+93Q
+ys7cf3lYUF1hdd7TzcXCycfPW1JLQ0FER1Bfbd7Ny8vK3GtTR0ZHVnrmzsW+vL69v8zU61pP
+TFNZYPrf3dHU1+teaU5ERUNGRkxOYuLn49j7XFxNSkpMUGN18dvf2c3N5G/yaWVwe+/d1MfA
+v77A0+h1W1VUTE5q/d/QzdHZ2f1fWU1IRkVLTVdmamz+fGNbaV5aZXhubOTv7N3Z1N3a2O58
++mleWWZr6ePo5+92Y/xrcO/v8nvp3NDGxcjP5u9nV1BMTk1JSk1o8evU3NPMz+Hd2uTa4tjQ
+0tDN2uXlZlVMSEZIWmh16evfztt3aFpPUE1ISlBd9NfX3O5oXE1PW2B969/X0s7Lztbc4ehf
+UlVVW1pi5uvc193T2NfPz9Tc43tjV0RLT0xRTVBl++De2tDc92hcZ1hPXlt429TP2NdrVEtM
+UllraPDh4ePV1+PqaG9ZYfhza+fc0snK1PV0X1Rbb+zi2/RtdGJeYVlvZ1/v5tfNysTFzc/Q
+0dTo3ftfX2BaXVhOUEpOVk9ea1lfZnVxbGNhVlpnYVteZW71bvplWFVUXeXUx8K9ur3Cz2pN
+R0hHSk5b5NDHwby+zfVSQ0JERU1Ye9vOxcLN3XdjWUtFPj9CQ0xz7uzl2d/7+W9sYO3c3M/Y
+29v6d19keWz73uHZ3O1kY2xp9G72917349rP5HpmU1hXW2dfetnc2t3ocmXmy87JxsjKz9TV
+0+VlTUhRXFz4fPvl93FNQDs4Oj9IVOnNwr29vMTbbklAQD1AQ0pb3cm9vsDH5mlcS01NUm7c
+y8LCyszO1d3d3N5tbF1T+PFkWExJT1nx3dfMysvXflI/PD5CR0tYftvOyL6/y3tOSklLT1Jc
+/N7W2NzgWVT25+TT1NPb1M3W52hOQkZOWfra0MzHxdLcb1A+QENAUGPZzL+2t7/Oak1JTlti
+9djKxry7wclkQz46OTk4P0/sztbb5e9wY/J2UkxITFz18GVsT0lMXtDLyMTCv7/Dz/dRRkxV
+X2JwzsK7uby+31FIQEdMTkxV9dnO1thhSD5GVlRMSE5yyMTIze9eTk3v2t/a9P7e/e5vSkM9
+TFp9z8W8t7XAyM9UPDM1PkBFR1i+r7CytchcPkdPREFHUV95c97O5V9c3MvPysnVzMfKyc5f
+R0Re1t/S09Pb3GhEPzw7ODc6QVjUw7+8vcvrRzIxPFzv7NzaxL68ubrLTz07R0pMYszAwcXL
+x9NzUFV+73PkdOn4/2hNQzxCYtXPysi/vsbJ315MNjA0OlrRzcTBvr7X4WxPQDU4PkV3wcjP
+xsfJz1pNTlTieejM1dbFwcrZbu5dVXBaUE1COkpdX+ns2NXWv8bVzXpLV+P0fk5CRl/FubSw
+r7e7vsxuRjowNDg6yaivKRcVHsyhmpqfoqlBGxMSIE64rKqquTgsNFu1r7CysrrSUFLdv73a
+PT3MxLVEFBARH7GflY6OlaM5GxscKklvzt3w4z4tNTvOrKejqb9GLS9BRTMqKCxcua+rKxUY
+G76ZlI6PlZ/FHhcaHz3JraSrudUsJCotS8+7s7e4yT0uNzg6Pi83zLu3zBYTGSKglY6Li5Cc
+VBUWFx8/TLysra24P0RGSrmzsLRZMCQdIS01QjwrT7zEwhoKDxRNmZCLiYyUoiQWGRk6wa2d
+mp6rVygtLEHEtqimsto7LSwuMDYsJDFDTFwYDRcgq5iSjo+WqS4PEBgmuKKamJijvCocHB9G
+sqOdnaxdNCguOz4+OkDGs61WDgoOH6eYkY+Ok58wFRgbLeXDq6arvVcsLjTTpqGdnatZJhsb
+JUC3sK6/Wrmw7RIFBQ3ul4uJh4iSuxUNDxcnO7Wkm56qWyMfIi9et6mipbJtPVduUE0/Nyoa
+FyuznrkYFBq5lZKPk5Wfxh0VGiqxq6Kio6xHGxAVHC7sqJiUl6pILCgnKzdIaE40KCIoS6aa
+qRgMDh+mlIuLio2bOxIMCxgvxqaZlJuqKxsaGx8qzqKZm6Kzelo4LS4pJyUkKDXIpp2Xk5ko
+CwgLN5qLh4eJj60WCgcNGiZvraCouC0bGhonQrKemZujvkFLVua6u7/lKxwdJ2G7r5+WkdcU
+DxDLmY2LjJCctBQKBw0iQLy1qa2vMhkaGizKpZmXn7k5Jiwz6q6tr8owIR4hPMKvp6WdkkUS
+ExK0ko6OkZmm5g8MDBE6vauor1E+IRQWHEyek4+RncEqHCM9v62xv0wsKCw/wb3StbGfli0a
+GB6cj42Sm7reHwoMDiHCr6qpwTIdDhMi0JuRkZah3C4gJUXYvc1GOzc6SubDvcXPr51OICAk
+nI+OkJq01B0ICgoauZ6YmqO3TxwVFh7TqJ6cqLdpNDpXclQvJCYuU7mrn6GinScNDQ1QkomG
+h46Zwg8JCA0i3qidnqu2KhkYHE+ooqa4Sjg5VrOttPYmHh4eLMiel5OTIw8PE7OTioiKlJ1j
+EhMNFjBJxrm/Ry8ZGBslvJ+YlqBNKR8fNGK3sMhBLCg17K6alZG2DQsLI5qMhoaLlaMbCwgI
+HEysoqCu0icRExcsu6KZmJ+6RCgqOGC8wGI2JydCv6qfnZybJAoJCjqYioWHjJGqEwoHDSzS
+qaWnsdcfEhESK6uZk5SfvjMeIjmynqG3Oh8bJD6vn5+Zk64VCQYWrJKJiYyOmykPCgwf9Kae
+o6vKKBIODx67nZSUmac/GhUcO6WamqNVIBwkXq6kmpSnGQsHE72ZjIuNj5wtEgsLHkiupams
+vSwWEhMlvZ+VkpagcR0XGzijl5ae7CIdIkWmlpVREAoLJrSZj42OlrccEw8ZOsmsp6iw8B4U
+EhYrw6SamZuhvyscGyi/npicsT4iHCfQn5KSRRILCymul42MjZCqGxANFTXOqp+dpr8fExET
+J8unm5mcor8qHxwjRrOfnaa/OiglKTaxmpGeGwwIEl6fkIyLj5o2Ew4PIVe0o5+kvCwTDhEc
+Y6aZlZadsjchHyZFsKOforJBJx8oPd2/v7Sdk7ATCAUYrpOLjI+VqxwOCxE8r6Cgq802GxAR
+GEKelJCUnrRSJh8oS6ibmp61NiIcHixMxr28rpqdHgsFCjqdjYmMkpw7EQ0OJKydmZ2vSyUS
+EBcqp5aSk5yv2isfJC66npqcqjgfGhskO8avsKWYuRIJBRC8l4uJjpakHw4MDSe0oJuftUQk
+FRUZKqiWkJGZqcAxHx4kyZyVlZ9OJBkYIUO4r7ekmTwPCAUbqpKKjJKZuRUMChBOqp2dq+Uy
+GhEWHseYkY+Unq5dHhccPJyQkJe3JBcSGTmzpq3DopspDggJOJuOio+boD4PDAsasqKanrZO
+LBcTGiarlZCQlp68IRATKaSPjpWnLxcPEyPKqa1xUJ6fGw4JEaaSi4uUnqcnDQwLIaqhm6PC
+PioXExssopOPjpWrMRUPH8ebj5OezRwSExtZpqbBMDKqmzsRDRGxko+OmKGoMw8NDx+roqKq
+20QtGxggO56PkZSmQyccH06pm5mrPCEZGyVKrqi6OS3LnJ0gEA8fmZKPk5yjuh8NDxY/oqaj
+r85LJxgbMrCXlZykvkMtKT2yp6zpJh4eIC1Iw7bN6cOjmx4ODxadjouMlJ+zJwoKDR2qpJ+m
+tl8wGhQhWpmPkZio3zstLdquqa82HxwdKETGr7xS2KabKA8OFZ6Pi4yXoLgfCgkLHa2knqe3
+yzYaExkunY+OlKXNOyorYK6ho9QqHR0pSLiqt7ulmq4PCgofmIyHi5KauBUGBgotq5+cqbFc
+IBIRGu2ZkI6VpVsjGyXrpJmcsTIcGh81uKm0wKiatxMLCSObjoqNlJu6FgkIDTGuoqCtuksf
+EhIa3pqRjpKjTx8YIn6jlpimPx0aIDm9sqmcmNwOCAkknY6JjZKb3hMJBw46rp6dqrc/HBAP
+F0qckY2PlKAxEg4Y75iPkJxTHRQWKMquqaCZpRoMCRWrkomJjZSnHwoHCRvLo5icpb8lEQ4R
+JKmWjo+Um7MiEA8cuJaPkZ9DGxIVJfGvpZuZQRIKCiqjkIqMj5jGFQsJEDmynpufrUMbDw8V
+NaSVjpCYpGMkHBonxKCWlqDXJBkaIDfBr6udmUwRCQklpI+JjI+ZTxELCRVQrZudqcIpFQ8Q
+G8Oaj46UnrcwHR0n26OcmqC/MB8bHyxjta6zvq6bvBUNCR+fkIuOl5+8Gg0MEVqmnJ2v9TEd
+FRYdu5iSkZut8y0fHynTn5udrD8pIR4nO8WqqbtLMT6lmjMUDA7enZSUm56g7hgTDyW0rai5
++WE/IhweMKealpigqrdJKykvw6Kcn7cwHx0dKD/KsLPBQickLfadnSgWDRLbqpmUlJWZyB4a
+Film4rq+vsBWJCElNrGknZ2hrLpCLTI8uqWhp8A3KSUnNUpj1G5ROisqMMmanikYDBmtm4+O
+kZSaPxkUDyQ/1rrHzNc7IyQmS6uhnZ+quNczLzhTsqyvt9RENCklKjFH82BLPT5lrperGA8L
+I56Qi42QmKIjDw4PLMqzrrvWSyoZGh9XoJiWmaKv2i8qKz23pZ+gs0YmGRkdKvq3ucvatpy8
+DQkHHpiMh4qPnLEXCwsPWZ+YnKzgNB0RFBzDl46OlJ+vVB0WGjeej46Xwx8TDhIeT6qjrsO2
+n68UCggZno2Ii5GfxRcLCw47o5iZpdgqGw8QGECbjoyPl6ZCGQ8XO5yOjZSzHxANFC2unKLq
+Okq3ni0NDQ+vj4yMk56/Lw0JDBi1oZyjvTsoGxMZJ6yTjo2UpT8dGSuvmZOYpy8TDREhvJ6d
+qzwqXtSpnhgPFBielpCOk5/HKQsPEx+wua65czYsHhst9Z2UmJypyl1MTbmnop+4MCEcITbV
+razJVTsu3Ku8mK4OFQsboJqOkpmrrB4OFQ0p4OCvyFdNQyUsOMmbl5ibr9tSOEHKxLq/RTUs
+LUdz59XoYuvWTtGqr/gtHiROu6qor661TC8oJi4/SVNYT048MzleuqunqKy4/z05O0ZbeN/J
+vLzgVEY9xrdTqJsrFQ4MPpeNio6itC0PDg0Xv5+anLZGLBsZHjakmJidtkNEUMmtr8NBJCMs
+Pb+0uK+doqOZHAoKDMmNiIiNplgfCQgLFsKfnZ+5LyETEBs+nI+PmLYxLTrbrKmzcigfJzK/
+qKmnoaydpQ8NDhuVi4iJkKhaFQMHChmxpp+oYy4iFRUiQJ2SlJmrTzkvSLOsq7k5KSYpS7aq
+rLyxo5S1DQsKLZKKhouSolgPAwYJL6iioMlAKBwQEh7Tko2NmcwqHRwru56UmK0vGx05r52f
+raGcmi8JBwzEj4iGjJOsJwgDBhWvnZmofi0aDwkPIqSPjZCerGMhFRcun46Nl8UgFhYotJ6d
+np2YqA8GBRKijYWHi5W3GgYFCSOmmpipUSITCwsUO5iNjJGiXiYaGSm7mY+Rm88lGxwq3Kql
+rMC3oKIfCwcNu5SLi5OdvSENDA8qopuZrTcfFhARHeaWjIyUszQfGhwqwJyRkJm+IRUTHEui
+k5CbZxsTGDifkJ4lEAsYO8SfmJKSnjAZFRgsPdu6trj8Kx0dJuOooZ+jqLhCKyk5u6OenqO3
+ZDQlICIuz6qmq8Q8KyUoN1zCwbSTmiMQAwkxoI2KjJGYLg8KCB++nZadreYmFBIVMJ+SjZGe
+sk4oJytjoZqZn1MjGxYZHi2/qKauQSMdHit4r6Ojr18uKayVxyoRDuuilZafpqZcHRoWPKKc
+mqxpSjEkJCnYnJaXo+o4LSQkKT6yqrFbKB8dGx0lPrGkp7k5KSgsRr2so6Ktt5yXTxcLC0OZ
+ioiNl6Q/EgwLFdyjmZ7HMiUbGBwrq5WQlKRfLyQfJTWznZylQhsSEBgvtZ6crD8iGx83uJ+c
+m4+VJg4HDcOThoWKlKQlDAkIGcykmqXfNSUaGyJSmo+Olqw/LCgmLU+qm5unNxsWFx86u6ir
+cyoiJjfyt6eVrRsVDS2ekIuQl6bMGhEPEzzEq63iPTAoJjnZn5WUlaDPLB4fRKWWkZzrHA4O
+EyPIqarBMiMjLUnXtq+bkzMWDxCulIyLk5/DKQwLDBnCr6KxSDYqIylYp5KOkpu7Rjk49q2m
+qL4tHhgZJTrJuO4zJyc4yrm8tq2wuK/SMiAWI8aZjo+Wti0TDA0SLrGenKO5Szkzb62emZuf
+rLrEw7KtuFAlHCEmKC0+7FQtISY278PG3czJy7/GvK2+TzsrMei4p6S110UwLCkoLDZH0MfH
+yM+/vbuuqamqrKyssrrTaUU5ODAuMS4uMj1nUzEpLkJZW9mupLwtIjOuoq1gNSs1Vdeurb3F
+YjkvKCs4TX3W18bCu62rq6uusLK0r62zyVI8OUNIS0UvJCImKi00PEqysT81O9LCJxkmro6H
+iZOrNR0WERUfPrKrvD0oJC/Mp5uXm6W1wsC5sbloKhsaIUC/7jk9qbGvqQwDCAuri4aBiZat
+JAsMFCKgnqCkOigeFhkquZaOkZq/LCYpU6+nquQhGRsnzq2ypqOemBoJCw+fioaFj59HGAgF
+ChS5o6SsKyYdFxoj6ZyPj5SoPCcdKbqbj46ZzR4UHEOom6TUXtirzA8NDi6RjImPnLUsEQkN
+F8iep7QpHBgQEBg6n46Olak7MigmL2ujlJOeySMcIzm0o6Sora6rGwsMEKyQioqSoG0eDAwQ
+J6Scm60zHxgTFSBYnJGRmbkzJyElOMKgmZqlUSUfJDjHr6mswee+qMsaDw0koJKNlKHENRsU
+Fh+1nZqjRR8ZFhceM6qUj5OjTSkjKjrZrJ2ZnLEuHBodLWu3pp+juy8fHyxUt6qqqJ2XUhAI
+Bh+kjomNlJ9+FQ0LFtijmqLqMCMbGR01pJSQlKdXMSgpLEWunJmfxicdGx4qP72loaprIxwd
+JD+5pZ6eqFglHB8+rZ2UjqofDQYUM6OQj5GZsCQXEBhGrJucqcc2IR0dKLmemJmo2DQnIyYu
+7KujpcEsHRocIjPGpp6grUYoIytfr6WforM/JyQwx6GTl0wZDRA0rpmTlJmkSB0XGCvJr6iz
+zkwvJiYra6adnqfHQTApKC1NsqiqyS8iHyEqOua+veY9MTdlw6qblqsdDw0mpZKMjpSfyCAV
+ExtOsqetXjAnISApSqeYlpqtUy8lJCv0ppubrC0XExYfOb2rrmEpLkyktxQTEumOioiRonYu
+GREYIK+cnqo0HhsdIC/GnY6Pl64rIic3uaCbm6k2GxMVI16vrXYnHh8rXbadrB8cFUiVjoqP
+nLk6HBUYG1yvrK86JiIjJjjCoZaXnrk6LjJOvrKvtFAqIB8qRdLDejw0NEBwuauhlr0VEQ9B
+lI2KkqHEKxEMDRZXq6SuQi0rJipNqpaPkp7MMSs07LettVwrIB4iMGu2tUwsJy1vuLStra+v
+r20eFhtLl46Ola42Gw4MEB/Bo6Or5i4rK0avnpeYo7pRP+y7rq3MOSsoKjM/9cDKXD88S9nL
+vMM/Tsi7rbLU8EgrJB8oyKOcnq5lKx0ZGBwqS8OyvM/eY862qJ+eo6uvrqqsrbhWMCgnMElP
+TjYqNTE1Ti49a8WcpzoXCxNOlouKkJ/LIBQODh35pZ6uQi0kHyQ3q5aPkJy3TELLrau03TQp
+IiMsQc/C+DcqO063lz0PEQw7lI6Kjp21NhQMDxRUp6ml4DMvJyY6y6SSkZemOyoyU6+hn6C2
+Mh8cJkm9rLQ9IyRKr5m+Dw4NMpWQjJOhwSsUCw8XVqWtrzchHxobKMuejo2Rm7jcR0XcvKqf
+oq52KyUoNeHBwtJOPjUrMWCtmywPDg3bnJSPm6a6OxgUFB/DurZOKSoqJyg4vpuUlp212+Xf
+zrqpm5WarS8bGyIz6b6zv0QtIiU6xaGgHxAMD86fko+UmqJSGhQSJuC/uXY9MSomKju1mpSW
+na/C6EdGz66gn6jANyghICYz3q6ruzwgGRohNt+xnpWmHAwGE8iXi4uOlaQoFQ4TM7iko7dU
+NCQeHyuumZKTnK/jPS8vPr+mn6O9Lh4aGyAsTbCorugoHBodKVOun52gszcnKDW9qqWkoZat
+GQ0GFLuWi4ySnbUfEQ4VUqSYmKfOPScdHSe/nZeZqU4uJyQnL2iqoqnNJhoZGyU3yKSdnq5C
+KyozZbiooJ+ozy4jIi1auaumqK/ONCQiKz++qaantUstJSYwVsS3vc7PpaodFA0YqJWNj5uu
+cB8SEhY/pJ6fvi4lIB8nOa+XkpajVi0rLDl4t6Wfps0pHBsfLE++rKau5y0iJjHlsKmoqrLW
+NigsULSmpa/bPC4uMTlrqp4zGA8RzZ2TkZifr0IcFhYmwa6pukg3MiosPbuemJmiu1tEQUhs
+wqulq9coGxsiNceysbxMLiQmMtKtq8REV7ObwBEOD86OioqToMEtEQsOGc2oqcgwKisqLF2p
+lo6SnrxFOjk2O166q69hJRoaHy5P1MzuQjQwOti6r66up5maMBUOGKaTjY+csjgYDQwRLayl
+psc7Ni4vP7adkI+asjMoKzjFq6iuXCMXFRkq5bS2TiolK0a+raurtOi5pZmdJBERIZ6Rj5Of
+vy4WDQ8aPailrbpvST476KublZmivEo6Pl3Ny0ktIB0cIS5K8VQ2MTg/Y9DAt7Gwr7XuPee5
+pqHYNzsySGpMxLK/xU8xNTI1T9m5r7m+yV5KQkrAr6+zxXFSRlHX6VA3LTEzOWja1kQtM0jv
+0c7Gu8JOQ269sL1XU8q4uUI2Ly5DaLuorMHuSEBaSne5tMNWPT5NZ9rDvbm/1dPJvrm+5l1R
+RjwwNUr8WzksLTA2Rlq4uFI+QEy5r7qvucHMIBok1piPkpy+Kx8ZGSI0yK+z3T0tLlOzn5yf
+qb5hY+jIvsk+JBwdKlTD0EE5MzA4VbiioZ6eHQsOFaKKiouVszYZDhIfbJ+hu0ojHR8hPKua
+kZKhSCIcKs+onZ+1Nx8bHy/itrPhMiUnNtOnoZ6YLQ4TENCRlI6YtW8gExQcK7uty9QvKDgx
+z6KZj4+Yp80xMEBUv7i5vE4sIR8mLjtPTz82LSsvNU/CvZ+cJxkVE7ScmpKesM4jFhkeNa6u
+t7FMP1g24qyelpaeqr5KTE5rs6mnqtopHBofLT/dy1kwIR4iL2/JxsanmzgXEhC2mZiTnqm3
+LxoXHDGsqbG1QjU9L0O7pZmWnKez2mxRTb2uqqu/NiMcHSMtRfvO0zwnHyIuT7qvrrC6yOrS
+oqUtKh83nJ+ho3dHLxseJDKyp7G57jxGQz7QsaefqsD3PjxGRWvKys1fMygkJCw3SdrCwtBZ
+Q0FGbcu6sa6vv87sXG3SuK64zE9LS0VEvaBOHRsVUJ+nm5+zq0wfHx8qzcHUzVQ7QjM4ybio
+oa7JWTk2My46Y8m6xV1LOzU5Q3W9tLW5yvRUUvXNwbmxsrjKTDc3Qu/DwMHeYlA/TFrnxsSl
+nEAfGhVqp62dprivOiAmJTi8yVzaQDQ1JShJxKigqK/DPTYwLkfQvLC40lg8NTQ4X72zr7fJ
+Xj8/SPXBu7e1tsH7RTxN38/Ky8xpOjBZrkceHRxknaakp+xSMBsdJjHBs7u1u9bQVj7Qs6yo
+s9fzRTg6Ola/vr/LT0U5MDU+f726u7/qVkdN7MK5u7vFzGNAOjXco8YnJB9enqurqMG8TR4j
+LDfAvs25uc7TTkXHubSwv9bqQzg1N1DmaU49ODYuLjU9X9rm1ehWY9a8sa+3vr66qZyuKiIf
+NqiurqGvvmseHCkuWM9by8lEOjM00q+rpqettXswMDA0RUJJbk0+My45S3jg8FhaXlPbwLe1
+u8jOv7KfoywfJyqvo72nqVdWJhcmOlKwsLepuEE9ND2+tbSsrrrDSTQ4ODxCOj1DODU0O1zM
+wcfO2ODp7N3LwLy+y83KyNxv46+fuiUiIzKwvtKru0lBIB43PF+4xLetyU9MPm+3ubWtsbzO
+OjM/PUpWTnv5Oy4wNEJke8/LzNT8fc++urrDx8DBytXoY1nOrrspHyMswLPErKrLay4jPOLZ
+uLi6sMY8Pj5C0MfHub77QTEuNj9P6tPM4Ec2Nz5iz8e7vMj3VFRtzsa/wL+/ycvLztLX5GzL
+rK1JKSox7sTOvLK5zEIsOFpm1MbLv9U1LjE4R1dRdeNdQzk7S19YZnTb12RJUdC/vb7Av7/J
+cG3Vv77H0c/N0GhOVvvjbuft2sXA0DwxMjxh08zBvdlNPztEXmzo2c7cVUNLV05WVGXf+llO
+UXTZ2d7c2dXvXmzu7Ofe4dva7ubazsbLzNXV3PNwWm9/9N/lz9Th2VxPSEVKS1hg9dXOys/T
+1Otw+G5sd1lWTkpZWFtu8O7scl/q9G5tZFtoa1xfXVNWYvTf3+vl9mlcWmNo5tPOzczQzsvM
+y8/R6Xp6WE9OTE5PTVRo9mZiX1xibnv3cX7ebFthWV9cXWFcWmr1fuDX19Pc5N/a39nP2N7t
++m1sbmlmdv14e9/U1NfV09Hc4+tnX1pJREVFSU1QWGzk6ere6mM=
+
+--foobarbazola--
+--16819560-2078917053-688350843:#11603--
+--owatagusiam--
+
diff --git a/comm/mailnews/test/data/multipart-base64-1 b/comm/mailnews/test/data/multipart-base64-1
new file mode 100644
index 0000000000..4c71b75923
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-1
@@ -0,0 +1,11 @@
+Content-Type: multipart/mixed; boundary=boundary
+Content-Transfer-Encoding: base64
+
+This part shouldn't appear
+--boundary
+Content-Type: text/plain
+Content-Transfer-Encoding: base64
+
+TXVsdGlwYXJ0IGJhc2U2NCBlbmNvZGVkIHRleHQu
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart-base64-1.out b/comm/mailnews/test/data/multipart-base64-1.out
new file mode 100644
index 0000000000..5658323cf3
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-1.out
@@ -0,0 +1 @@
+Multipart base64 encoded text. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart-base64-2 b/comm/mailnews/test/data/multipart-base64-2
new file mode 100644
index 0000000000..1ccfcff011
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-2
@@ -0,0 +1,12 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+yadda yadda
+
+--boundary
+Content-Type: text/html
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+YmFzZTY0ZW5jb2RlZCBIVE1MIHRleHQgaW5zaWRlIGEgbXVsdGlwYXJ0IG1lc3N
+hZ2UuPC9oZWFkPjwvaHRtbD4=
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart-base64-2.out b/comm/mailnews/test/data/multipart-base64-2.out
new file mode 100644
index 0000000000..e55e98ee9d
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-2.out
@@ -0,0 +1 @@
+<html><head>base64encoded HTML text inside a multipart message.</head></html> \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart-base64-3 b/comm/mailnews/test/data/multipart-base64-3
new file mode 100644
index 0000000000..c0876a8d44
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-3
@@ -0,0 +1,10 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+etc etc
+--boundary
+Content-Type: text/html
+Content-Transfer-Encoding: base64
+
+PGh0bWw+PGhlYWQ+VGhpcyB0aW1lLCB0aGUgdGFncw0Kc2hvdWxkIGJlIHN0cmlwcGVkIG91dC48L2hlYWQ+PC9odG1sPg==
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart-base64-3.out b/comm/mailnews/test/data/multipart-base64-3.out
new file mode 100644
index 0000000000..bd070ea356
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-base64-3.out
@@ -0,0 +1 @@
+This time, the tags should be stripped out. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart-complex1 b/comm/mailnews/test/data/multipart-complex1
new file mode 100644
index 0000000000..f9473072d8
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-complex1
@@ -0,0 +1,35 @@
+Content-Type: multipart/mixed; boundary="boundary"
+
+This shouldn't appear.
+--boundary
+Content-Type: application/octet-stream
+Content-Transfer-Encoding: base64
+
+VGhpcyBpc24ndCByZWFsbHkgYW4gYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtLiA7KQ=='
+
+--boundary
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+TmVpdGhlciBpcyB0aGlzIGFuIGltYWdlL3BuZy4=
+
+--boundary
+Content-Type: multipart/related; boundary="boundary2"
+
+--boundary2
+Content-Type: text/html
+
+<html><head>This part should be returned.</head></html>
+
+--boundary2--
+
+--boundary
+Content-Type: text/plain
+
+This part shouldn't.
+
+--boundary
+
+Neither should this part!
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart-complex1.out b/comm/mailnews/test/data/multipart-complex1.out
new file mode 100644
index 0000000000..5dcd69d888
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-complex1.out
@@ -0,0 +1 @@
+This part should be returned. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart-complex2 b/comm/mailnews/test/data/multipart-complex2
new file mode 100644
index 0000000000..0898d02810
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-complex2
@@ -0,0 +1,62 @@
+From - Mon Jun 02 19:00:00 2008
+Content-Type: multipart/mixed; boundary="bou"
+Message-Id: <123456@example.com>
+
+Part 1
+--bou
+Content-Type: multipart/related; boundary="bound"
+
+Part 2
+--bound
+Content-Type: multipart/digest; boundary="boundar"
+
+Part 3
+--boundar
+Content-Type: multipart/alternative; boundary="boundary"
+
+Part 4
+--boundary
+Content-Type: application/octet-stream
+
+Wow, what alternatives!
+
+We're trying to confuse the parser here.
+
+--bou
+
+--bound
+
+--boundar
+
+--boundary
+Content-Type: application/pdf
+
+A choice between a PDF and an octet stream! How marvellous!
+
+--boundary--
+
+--boundar
+Content-Type: multipart/mixed; boundary="boundary123456"
+
+--boundary123456
+Content-Type: text/plain
+
+This is the correct answer.
+
+--boundary123456--
+
+--boundar--
+
+--bound
+Content-Type: text/plain
+
+One last attempt at confusing the parser.
+
+--bound--
+
+--bou
+Content-Type: text/html
+
+<html><body>No harm in making another.</body></html>
+
+--bou--
diff --git a/comm/mailnews/test/data/multipart-complex2.out b/comm/mailnews/test/data/multipart-complex2.out
new file mode 100644
index 0000000000..fbc0c12b6e
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-complex2.out
@@ -0,0 +1 @@
+This is the correct answer. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart-message-1.eml b/comm/mailnews/test/data/multipart-message-1.eml
new file mode 100644
index 0000000000..00f2f8df7b
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-message-1.eml
@@ -0,0 +1,42 @@
+To: test@example.com
+From: test@example.com
+Subject: Test message with attached message
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------9B410E80D6DA0868F068B0E4"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a test message with an attached message.
+
+
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: message/rfc822;
+ name="attached-message.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="attached-message.eml"
+
+To: test@example.com
+From: test@example.com
+Subject: Attached message (plaintext)
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f4@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Here is the body of the attached message. Search for bodyOfAttachedMessagePlain.
+
+
+--------------9B410E80D6DA0868F068B0E4--
diff --git a/comm/mailnews/test/data/multipart-message-2.eml b/comm/mailnews/test/data/multipart-message-2.eml
new file mode 100644
index 0000000000..fe59d3af97
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-message-2.eml
@@ -0,0 +1,42 @@
+To: test@example.com
+From: test@example.com
+Subject: Test message with attached message
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------9B410E80D6DA0868F068B0E4"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a test message with an attached message.
+
+
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: message/rfc822;
+ name="attached-message.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="attached-message.eml"
+
+To: test@example.com
+From: test@example.com
+Subject: Attached message (plaintext, base64 encoded)
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f4@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: base64
+
+SGVyZSBpcyB0aGUgYm9keSBvZiB0aGUgYXR0YWNoZWQgbWVzc2FnZS4gU2VhcmNoIGZvciBib2R5T2ZBdHRhY2hlZE1lc3NhZ2VQbMOkaW4u
+
+
+--------------9B410E80D6DA0868F068B0E4--
diff --git a/comm/mailnews/test/data/multipart-message-3.eml b/comm/mailnews/test/data/multipart-message-3.eml
new file mode 100644
index 0000000000..e4f4d3ba13
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-message-3.eml
@@ -0,0 +1,57 @@
+To: test@example.com
+From: test@example.com
+Subject: Test message with attached message
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------9B410E80D6DA0868F068B0E4"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a test message with an attached message.
+
+
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: message/rfc822;
+ name="attached-message.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="attached-message.eml"
+
+To: test@example.com
+From: test@example.com
+Subject: Attached message (plaintext + HMTL)
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+Content-Language: de-DE
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Here is the body of the attached message. Search for bodyOfAttachedMessagePläin.
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+<body>Here is the body of the attached message. Search for bodyOfAttachedMessageHTML.</body>
+
+--------------FAB286B8794CC63C0A0FD1BB--
+
+
+--------------9B410E80D6DA0868F068B0E4--
diff --git a/comm/mailnews/test/data/multipart-message-4.eml b/comm/mailnews/test/data/multipart-message-4.eml
new file mode 100644
index 0000000000..85ba876212
--- /dev/null
+++ b/comm/mailnews/test/data/multipart-message-4.eml
@@ -0,0 +1,57 @@
+To: test@example.com
+From: test@example.com
+Subject: Test message with attached message
+Message-ID: <8259dd8e-2293-8765-e720-61dfcd10a6f3@example.com>
+Date: Sat, 30 Dec 2017 19:12:38 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------9B410E80D6DA0868F068B0E4"
+Content-Language: en-GB
+
+This is a multi-part message in MIME format.
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a test message with an attached message.
+
+
+--------------9B410E80D6DA0868F068B0E4
+Content-Type: message/rfc822;
+ name="attached-message.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="attached-message.eml"
+
+To: test@example.com
+From: test@example.com
+Subject: Attached message (plaintext + HMTL, both base64 encoded)
+Message-ID: <a30f750d-d56c-8a52-971c-f95a131e8332@example.com>
+Date: Sat, 30 Dec 2017 19:31:21 +0100
+User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101
+ Thunderbird/59.0a1
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------FAB286B8794CC63C0A0FD1BB"
+Content-Language: de-DE
+
+This is a multi-part message in MIME format.
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: base64
+
+SGVyZSBpcyB0aGUgYm9keSBvZiB0aGUgYXR0YWNoZWQgbWVzc2FnZS4gU2VhcmNoIGZvciBib2R5T2ZBdHRhY2hlZE1lc3NhZ2VQbGFpbi4=
+
+
+--------------FAB286B8794CC63C0A0FD1BB
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: base64
+
+PGJvZHk+SGVyZSBpcyB0aGUgYm9keSBvZiB0aGUgYXR0YWNoZWQgbWVzc2FnZS4gU2VhcmNoIGZvciBib2R5T2ZBdHRhY2hlZE1lc3PDpGdlSFRNTC48L2JvZHk+
+
+--------------FAB286B8794CC63C0A0FD1BB--
+
+
+--------------9B410E80D6DA0868F068B0E4--
diff --git a/comm/mailnews/test/data/multipart1 b/comm/mailnews/test/data/multipart1
new file mode 100644
index 0000000000..4c6b2a0745
--- /dev/null
+++ b/comm/mailnews/test/data/multipart1
@@ -0,0 +1,12 @@
+Content-Type: multipart/mixed;
+ boundary="boundary"
+
+This is a text message in MIME format.
+This part shouldn't appear in the output.
+
+--boundary
+Content-Type: text/plain
+
+Hello, world! (yet again...)
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart1.out b/comm/mailnews/test/data/multipart1.out
new file mode 100644
index 0000000000..d6546401c4
--- /dev/null
+++ b/comm/mailnews/test/data/multipart1.out
@@ -0,0 +1 @@
+Hello, world! (yet again...) \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart2 b/comm/mailnews/test/data/multipart2
new file mode 100644
index 0000000000..8735523f45
--- /dev/null
+++ b/comm/mailnews/test/data/multipart2
@@ -0,0 +1,13 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+This is a text/html message. This part shouldn't appear at all!
+
+--boundary
+Content-Type: text/html
+
+<html><body>Multipart HTML message with just a single part!
+</body></html>
+
+--boundary--
+
+Actually, this part shouldn't appear either.
diff --git a/comm/mailnews/test/data/multipart2.out b/comm/mailnews/test/data/multipart2.out
new file mode 100644
index 0000000000..9c6467aafd
--- /dev/null
+++ b/comm/mailnews/test/data/multipart2.out
@@ -0,0 +1,2 @@
+<html><body>Multipart HTML message with just a single part!
+</body></html> \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart3 b/comm/mailnews/test/data/multipart3
new file mode 100644
index 0000000000..2183cbd6a2
--- /dev/null
+++ b/comm/mailnews/test/data/multipart3
@@ -0,0 +1,29 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+--boundary
+Content-Type: text/html
+
+<html>
+
+
+<body>
+
+
+
+Here, the HTML tags should be stripped out.
+
+
+
+</body>
+
+
+
+</html>
+
+
+
+
+
+
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart3.out b/comm/mailnews/test/data/multipart3.out
new file mode 100644
index 0000000000..f592bc0df7
--- /dev/null
+++ b/comm/mailnews/test/data/multipart3.out
@@ -0,0 +1 @@
+Here, the HTML tags should be stripped out. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipart4 b/comm/mailnews/test/data/multipart4
new file mode 100644
index 0000000000..934b5b1cc2
--- /dev/null
+++ b/comm/mailnews/test/data/multipart4
@@ -0,0 +1,7 @@
+Content-Type: multipart/mixed; boundary=boundary
+
+--boundary
+
+This has no headers, so should be recognized as plain text.
+
+--boundary--
diff --git a/comm/mailnews/test/data/multipart4.out b/comm/mailnews/test/data/multipart4.out
new file mode 100644
index 0000000000..49092e8920
--- /dev/null
+++ b/comm/mailnews/test/data/multipart4.out
@@ -0,0 +1 @@
+This has no headers, so should be recognized as plain text. \ No newline at end of file
diff --git a/comm/mailnews/test/data/multipartmalt-detach b/comm/mailnews/test/data/multipartmalt-detach
new file mode 100644
index 0000000000..e0066ca56f
--- /dev/null
+++ b/comm/mailnews/test/data/multipartmalt-detach
@@ -0,0 +1,49 @@
+From
+X-Account-Key: account1
+X-UIDL: 0397aedc0eee392343488772c79f110d
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 10000000
+X-Mozilla-Keys:
+Return-Path: <ef@hg.de>
+X-Flags: 0000
+Date: Tue, 29 Aug 2006 16:42:08 GMT
+From: abc <ef@hg.de>
+To: abc <ef@hg.de>
+Subject: detach test
+Message-ID: <xxxyy@zzz>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="gmxboundary=-1156956072-29266-top"
+
+--gmxboundary=-1156956072-29266-top
+Content-Type: text/plain; charset="iso-8859-1"
+
+plain body
+--gmxboundary=-1156956072-29266-top
+Content-Type: multipart/related; boundary="gmxboundary=-1156956072-29266-sub"
+
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/html; charset="iso-8859-1"
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
+w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns=3D"http://www.w3.org/1999/xhtml" xml:lang=3D"de" lang=3D"de">
+<head>
+<title>Update</title>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-1" />
+</head>
+
+<body> body hello
+</body>
+</html>
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/plain
+Content-Disposition: inline; filename="head_update.txt"
+
+head_update.txt
+--gmxboundary=-1156956072-29266-sub
+Content-Type: text/plain
+Content-Disposition: inline; filename="smurf_update_neu.txt"
+
+smurf_update_neu.txt
+--gmxboundary=-1156956072-29266-sub--
+--gmxboundary=-1156956072-29266-top--
diff --git a/comm/mailnews/test/data/readme.txt b/comm/mailnews/test/data/readme.txt
new file mode 100644
index 0000000000..c3cee26033
--- /dev/null
+++ b/comm/mailnews/test/data/readme.txt
@@ -0,0 +1,103 @@
+This directory (mailnews/test/data) contains various support files for the
+testing of mailnews.
+
+Not all files will be documented here, but this will be a place for some
+documentation.
+
+signons-mailnews1.8.txt
+-----------------------
+
+This is a passwords file from gecko 1.8/gecko 1.9/early 1.9.1
+(TB 1.5/2.0/pre 3.0 beta 1, SM 1.0/1.5/early 2.0). It is used as a test input
+for several test_*Password.js files to check that we load usernames and
+passwords correctly from the legacy file.
+
+abLists1.mab
+------------
+
+An address book with 5 cards and 3 lists. The cards only have the email and
+prefer mail format set:
+
+test1@invalid.com unknown
+test2@invalid.com unknown
+test3@invalid.com unknown
+test4@invalid.com plain text
+test5@invalid.com html
+
+There are 3 lists, TestList1, TestList2 and TestList3. They have the following
+cards:
+
+TestList1:
+
+test1@invalid.com
+test2@invalid.com
+test3@invalid.com
+
+TestList2:
+
+test4@invalid.com
+
+TestList3:
+
+test5@invalid.com
+
+
+abLists2.mab
+------------
+
+The same as abLists1.mab, but with com.invalid instead of invalid.com, and ListTestX instead of TestListX.
+
+mime-torture
+------------
+
+This is a file which is known as the MIME torture test. It contains several
+nested multipart/* and message/* segments; as its explanation describes:
+
+ This is a demonstration of multi-part mail with encapsulated messages. This
+ is a very complex message whose purpose it is to exercise software using the
+ new multi-part message standard.
+
+The original source of this file is unknown, but a copy can be found at
+<http://sourceforge.net/projects/kmmail/files/MIME%20Torture%20Tests/>.
+
+
+
+
+mbox_modern
+-----------
+
+A simple mbox in the form that Thunderbird currently seems to save (as
+of Nov 2018).
+Separator lines are of the form:
+
+From - Fri Aug 24 11:55:47 2018
+
+Lines beginning with "From " within the message body are escaped
+by prefixing a space.
+
+
+mbox_unquoted
+-------------
+
+Old-style mbox. This seems to be what Thunderbird was storing back around
+2005, and so there are likely to be a bunch of these out in the wild.
+
+Separator lines are lines containing only: "From "
+Lines within the message body beginning with "From " are not escaped in
+any way.
+
+
+mbox_mboxrd
+-----------
+
+Example of the mboxrd variant, taken from the wikipedia mbox page.
+Separator lines are of the form:
+
+From MAILER-DAEMON Fri Jul 8 12:08:34 2011
+
+(note that MAILER-DAEMON is a special case - this is where an
+email address usually goes, but MAILER-DAEMON can be used instead).
+
+"From " lines within the message body are escaped by prefixing with
+a '>' char.
+
diff --git a/comm/mailnews/test/data/reply-filter-testmail b/comm/mailnews/test/data/reply-filter-testmail
new file mode 100644
index 0000000000..8cfc47a042
--- /dev/null
+++ b/comm/mailnews/test/data/reply-filter-testmail
@@ -0,0 +1,22 @@
+From - Sun Feb 09 12:56:43 2014
+X-Mozilla-Status: 0001
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Delivered-To: from@FOO.invalid
+Received: from webapp-out.mozilla.org (webapp01.sj.mozilla.com [63.245.208.146])
+ by mx.google.com with ESMTPSA id g8sm16554434lae.1.2014.02.09.02.56.44
+ for <multiple recipients>
+ (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
+ Sun, 09 Feb 2014 02:56:45 -0800 (PST)
+Message-ID: <52F75EEB.9080701@example.com>
+Date: Sun, 09 Feb 2014 12:56:43 +0200
+From: someone@example.com
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:30.0) Gecko/20100101 Thunderbird/30.0a1
+MIME-Version: 1.0
+To: from@FOO.invalid
+Subject: reply filter test
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+testing!
+
diff --git a/comm/mailnews/test/data/signons-mailnews1.8-alt.json b/comm/mailnews/test/data/signons-mailnews1.8-alt.json
new file mode 100644
index 0000000000..1d92b99d13
--- /dev/null
+++ b/comm/mailnews/test/data/signons-mailnews1.8-alt.json
@@ -0,0 +1,39 @@
+{
+ "nextId": 11,
+ "logins": [
+ {
+ "id": 9,
+ "hostname": "mailbox://invalid",
+ "httpRealm": "mailbox://invalid",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECFSwsJB6aYktBBAgrtToaikwdWTSCAkwekq4",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECLILW7cWSv/IBBCbmdtTmNF1fFF0fIlPIUHw",
+ "guid": "{995dec9d-20ce-4ee0-8b1e-5058dc559766}",
+ "encType": 1,
+ "timeCreated": 1401842558488,
+ "timeLastUsed": 1401842558488,
+ "timePasswordChanged": 1401842558488,
+ "timesUsed": 1
+ },
+ {
+ "id": 10,
+ "hostname": "news://invalid",
+ "httpRealm": "news://invalid",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECAsAALQhSUvNBBBjMqY1m93CVkc4++I4O2Bi",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECKZCrPQsEWWEBBBqfxVHcP9ZeA13oom34EAc",
+ "guid": "{8633ac11-67e4-4771-b0b0-10e3f6f33759}",
+ "encType": 1,
+ "timeCreated": 1401842558499,
+ "timeLastUsed": 1401842558499,
+ "timePasswordChanged": 1401842558499,
+ "timesUsed": 1
+ }
+ ],
+ "disabledHosts": [],
+ "version": 1
+}
diff --git a/comm/mailnews/test/data/signons-mailnews1.8-imap.json b/comm/mailnews/test/data/signons-mailnews1.8-imap.json
new file mode 100644
index 0000000000..8dc8c50a36
--- /dev/null
+++ b/comm/mailnews/test/data/signons-mailnews1.8-imap.json
@@ -0,0 +1,23 @@
+{
+ "nextId": 9,
+ "logins": [
+ {
+ "id": 8,
+ "hostname": "imap://localhost",
+ "httpRealm": "imap://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECIGWS/j4zEyoBAh/qfrLz22RzQ==",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECFxZ82GjB8EYBBC24vYHNSltwqrSuANT7hzk",
+ "guid": "{c44fc6f4-5351-4f3f-a38e-df3c5a8722c3}",
+ "encType": 1,
+ "timeCreated": 1401842558439,
+ "timeLastUsed": 1401842558439,
+ "timePasswordChanged": 1401842558439,
+ "timesUsed": 1
+ }
+ ],
+ "disabledHosts": [],
+ "version": 1
+}
diff --git a/comm/mailnews/test/data/signons-mailnews1.8-multiple.json b/comm/mailnews/test/data/signons-mailnews1.8-multiple.json
new file mode 100644
index 0000000000..8f4d414a4b
--- /dev/null
+++ b/comm/mailnews/test/data/signons-mailnews1.8-multiple.json
@@ -0,0 +1,71 @@
+{
+ "nextId": 8,
+ "logins": [
+ {
+ "id": 4,
+ "hostname": "mailbox://localhost",
+ "httpRealm": "mailbox://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECHZjntDstHpKBBBNtLMPriE8NejeH8jKD0Rm",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECNI2tGGh8mBvBBBNCWM6ZTmTrNQ6b1Yf5LPK",
+ "guid": "{1dfe473f-626d-4381-ba42-8776860cc1f2}",
+ "encType": 1,
+ "timeCreated": 1401842558346,
+ "timeLastUsed": 1401842558346,
+ "timePasswordChanged": 1401842558346,
+ "timesUsed": 1
+ },
+ {
+ "id": 5,
+ "hostname": "mailbox://localhost",
+ "httpRealm": "mailbox://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECMo6So75XWp/BBBHmhkaEi8ARzPrc3ewXcJm",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECLnGNMIgkP5nBBBNF17yd5s87mmmgUoUDvlW",
+ "guid": "{0ad08eaa-a43c-449f-af70-0fd0e8814322}",
+ "encType": 1,
+ "timeCreated": 1401842558360,
+ "timeLastUsed": 1401842558360,
+ "timePasswordChanged": 1401842558360,
+ "timesUsed": 1
+ },
+ {
+ "id": 6,
+ "hostname": "smtp://localhost",
+ "httpRealm": "smtp://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECLGRtm7GmsrHBBCmUotod1Is9aXn31EljSkO",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDfG5HET+ZzTBBDGXgNIhsm2yRmJlVAJZk3d",
+ "guid": "{0ae6d9e4-d84c-4ae2-8912-8d4e02983c3c}",
+ "encType": 1,
+ "timeCreated": 1401842558376,
+ "timeLastUsed": 1401842558376,
+ "timePasswordChanged": 1401842558376,
+ "timesUsed": 1
+ },
+ {
+ "id": 7,
+ "hostname": "smtp://localhost",
+ "httpRealm": "smtp://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJRenYQk+2IeBBBuSXtLEmQTZYeRy+FqDyri",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEyPWwzpnkcDBBCmYiJ1mpN8dM2aFC6t92gf",
+ "guid": "{cafab428-efe3-4e43-bcfa-e72bee1a8165}",
+ "encType": 1,
+ "timeCreated": 1401842558388,
+ "timeLastUsed": 1401842558388,
+ "timePasswordChanged": 1401842558388,
+ "timesUsed": 1
+ }
+ ],
+ "disabledHosts": [],
+ "version": 1
+}
diff --git a/comm/mailnews/test/data/signons-mailnews1.8.json b/comm/mailnews/test/data/signons-mailnews1.8.json
new file mode 100644
index 0000000000..8c7ad49772
--- /dev/null
+++ b/comm/mailnews/test/data/signons-mailnews1.8.json
@@ -0,0 +1,55 @@
+{
+ "nextId": 4,
+ "logins": [
+ {
+ "id": 1,
+ "hostname": "mailbox://localhost",
+ "httpRealm": "mailbox://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECO7LElFP4W22BBBKpiIzQrGQ46h5dtUIChvA",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECMgrtBjOOXkOBBAhYNyFziZ3JniuGDjleFua",
+ "guid": "{c094ca22-c190-432d-a7f8-4b6459cf7716}",
+ "encType": 1,
+ "timeCreated": 1401842558236,
+ "timeLastUsed": 1401842558236,
+ "timePasswordChanged": 1401842558236,
+ "timesUsed": 1
+ },
+ {
+ "id": 2,
+ "hostname": "news://localhost",
+ "httpRealm": "news://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDVT947PjMppBBDa/UR8chJ5lGzwTUED2yvz",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECA8tKoHOg4aLBBCsMmZPD/lN9lRAXtPQ3fD+",
+ "guid": "{07b34bc7-3ba4-43d2-b124-379122d854fa}",
+ "encType": 1,
+ "timeCreated": 1401842558250,
+ "timeLastUsed": 1401842558250,
+ "timePasswordChanged": 1401842558250,
+ "timesUsed": 1
+ },
+ {
+ "id": 3,
+ "hostname": "smtp://localhost",
+ "httpRealm": "smtp://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECK061gNLW4h8BBAp446q+NewuvUPzJlPkbMc",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECO5ps1iVsu13BBC3NejVl9gEtTJfF6IuKXAD",
+ "guid": "{dad236d6-91d8-489a-ad96-35cf8372c50e}",
+ "encType": 1,
+ "timeCreated": 1401842558263,
+ "timeLastUsed": 1401842558263,
+ "timePasswordChanged": 1401842558263,
+ "timesUsed": 1
+ }
+ ],
+ "disabledHosts": [],
+ "version": 1
+}
diff --git a/comm/mailnews/test/data/signons-smtp.json b/comm/mailnews/test/data/signons-smtp.json
new file mode 100644
index 0000000000..8ded49647c
--- /dev/null
+++ b/comm/mailnews/test/data/signons-smtp.json
@@ -0,0 +1,39 @@
+{
+ "nextId": 13,
+ "logins": [
+ {
+ "id": 11,
+ "hostname": "imap://localhost",
+ "httpRealm": "imap://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECFFWS3gV0I5aBAjM6y9n8kmZkg==",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECN/q2CWcXjkKBBCjzlW4M+uk8xeFVF8ANZW9",
+ "guid": "{daaabad2-1ecd-4563-8cb5-f533ec5a6d66}",
+ "encType": 1,
+ "timeCreated": 1401842558551,
+ "timeLastUsed": 1401842558551,
+ "timePasswordChanged": 1401842558551,
+ "timesUsed": 1
+ },
+ {
+ "id": 12,
+ "hostname": "smtp://localhost",
+ "httpRealm": "smtp://localhost",
+ "formSubmitURL": null,
+ "usernameField": "",
+ "passwordField": "",
+ "encryptedUsername": "MEIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDsBW6Hoo2pNBBgV4TBEDckxkdAz4/sSr5taJPJikR//Z0k=",
+ "encryptedPassword": "MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECL+KL/SZm8uJBBDWKDhjCk3740okXbg6sr2z",
+ "guid": "{1434fff1-680f-4130-b834-ae710ef7931e}",
+ "encType": 1,
+ "timeCreated": 1401842558564,
+ "timeLastUsed": 1401842558564,
+ "timePasswordChanged": 1401842558564,
+ "timesUsed": 1
+ }
+ ],
+ "disabledHosts": [],
+ "version": 1
+}
diff --git a/comm/mailnews/test/data/smime/Alice.p12 b/comm/mailnews/test/data/smime/Alice.p12
new file mode 100644
index 0000000000..5533001a3c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/Alice.p12
Binary files differ
diff --git a/comm/mailnews/test/data/smime/Bob.p12 b/comm/mailnews/test/data/smime/Bob.p12
new file mode 100644
index 0000000000..b5c8504a73
--- /dev/null
+++ b/comm/mailnews/test/data/smime/Bob.p12
Binary files differ
diff --git a/comm/mailnews/test/data/smime/Dave.p12 b/comm/mailnews/test/data/smime/Dave.p12
new file mode 100644
index 0000000000..ee9fcefe64
--- /dev/null
+++ b/comm/mailnews/test/data/smime/Dave.p12
Binary files differ
diff --git a/comm/mailnews/test/data/smime/Eve.p12 b/comm/mailnews/test/data/smime/Eve.p12
new file mode 100644
index 0000000000..c0c3d477fb
--- /dev/null
+++ b/comm/mailnews/test/data/smime/Eve.p12
Binary files differ
diff --git a/comm/mailnews/test/data/smime/README.md b/comm/mailnews/test/data/smime/README.md
new file mode 100644
index 0000000000..90e46effd4
--- /dev/null
+++ b/comm/mailnews/test/data/smime/README.md
@@ -0,0 +1,23 @@
+S/MIME test certificates and messages
+=====================================
+
+This directory contains certificates and S/MIME test messages, which are
+generated by building NSS and executing a subset of the NSS test suite.
+
+The certificates will expire on the date listed in file expiration.txt
+
+The certificates must be refreshed around the expiration date.
+To do so, run the generate.sh script.
+
+Alternatively, join the #nss IRC channel and ask a member of the NSS
+team to execute the NSS test suite, and give you a copy of the contents
+of this directory:
+ tests_results/security/localhost.1/sharedb/smime/tb/
+
+(You only need the files having extensions .eml or .p12 or .pem .)
+
+The generate.sh script will automatically call local-gen.sh script,
+which generates additional test data, based on the updated NSS data.
+
+You may call local-gen.sh independently, if necessary, e.g. when adding
+new local tests.
diff --git a/comm/mailnews/test/data/smime/TestCA.pem b/comm/mailnews/test/data/smime/TestCA.pem
new file mode 100644
index 0000000000..0e9f065d6e
--- /dev/null
+++ b/comm/mailnews/test/data/smime/TestCA.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQTAgFw0yMzExMjEy
+MDUwMDdaGA8yMDczMTEyMTIwNTAwN1owZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDPL+VBVHBnK32PfKsFz5mwpeOdSOsfqymx2DN1qo42mQ8s
+SRmrK7lbi0iiGuU+3jFBh/29wWitHH+1qb/DDSiIYk+RS3mVKdTxDOztwyRW7mf3
+oqK3OR4cQhLxXhIDuhDW4P4CQTwu6CSqwyTkrJeEi77foQ/C1rX1zQWMpJW7n4Iv
+9PNMedHpNtoXP7zS8GxV9WwiHFEcRYzwdrlHQJm9+l0OWp8Tl5DFniJjjOuQYr4n
+DGJ1R4F74yU9NPrOzAy9Cvm1eO4novQEcUyQ5Mdnw7bHI31ChWf/KyZzDLA+jTMI
+30fLNDr512k0Z1429p1n77nzGhSDSbSNKFtyyNy1AgMBAAGjMDAuMBEGCWCGSAGG
++EIBAQQEAwIABzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0B
+AQsFAAOCAQEAt8reMMu83pDQiZqgq1bI7P1sGznDykMefFITZF3veDas86T9seDZ
+pPAT45uo8t/+yuQWciDfBDcB5NnedkTjmXUaG1ZKekTamv6uNaLqr5aZrotnNUwK
+CYk4ci1K5MuprqE6kBKKP8cGYL5ZqA4PPIHxlCgU2JR9G8NVn7Nw3Xb6ZTqRWTSn
+g2tM3LWBDCu2p6qIZPNu35OYBUmLzsfSSlroj8iwirnOa+LYAmUMTQlnBQpln1WI
+q+q0haYFPt9MTGpvwA9JBoElS9I4XxJHbthlsCizas9UJfue37RIR5LRXA8zudRU
+Rwo8QS6MTgppRvorz2kvABRmkXiYk3Vn/g==
+-----END CERTIFICATE-----
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart
new file mode 100644
index 0000000000..d5215872b3
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart
@@ -0,0 +1,54 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMTkw
+NzAzMTczMDA3WhcNMjQwNzAzMTczMDA3WjCBgDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAM
+BgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4F8P
+ZSndSoPhUMMdVU8cIVqWPx1Hg+OUZzfTk7vf8dnwOWJqtoT6O+5Ff432kPbIdjIm
+3m7Seo0eBC1s1W0z2kYztPkVCzSlAfMg/5bEpkPgVSWQjn2LxsYuDHRzrr/QnwUg
++ZdwIFps29PcK8XFQeaKNc/rm4UYhRTZ34iEUQf7QQVSQZaxPZCunxj+qk+T1KaI
+Kd/KCLJX9hFXybbGM5TT0BaxQahdtls4aUnymmhoqQ3fJJwRIB6Lth/m1TPvKE5X
+j6boivY0A0DGhFWNZmxOvDUJa9HcMwc3MamgzZdxnp41BawL6Vkwq1SzwCcqIBaM
+1kVaUYvGsAGl4H5xBwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA0+5CEGeVmPLNJ
+kwDBP8DPzobniH+y/s38aNRu24qJuvB522+HW+6BMwZW+Le/PFojvUPSQjgL4YFD
+vKnjtC/YarPiO3e3Q9hilMRldunE1Yl8Ldh61zbNLP/JLgHroICza2F5YIb42qzA
+ANeJqbZT+08ybxBiUJZlq2v0L+S2bLPhS4Jb1sNIQi3BnL5f/nlnpi3TsWgLH9lN
+2CDeMK3A6qSaEgZ8pdA8pHLRzJGD2mHUaXvUY/lrl76xYXuwqYlJDg+0vI8T/f0R
+C7FHPTdbqJZjutbvpm5EanXzRVL/rv3YzMYz767Xp22Qf9Tk+fOpUbsIvdj4f96+
+ytVusgx1MYICyTCCAsUCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBNTAYBgkq
+hkiG9w0BCQMxCwYJKoZIhvcNAQcBMCMGCSqGSIb3DQEJBDEWBBQCH3ANIhscFLil
+//h1Np3FJ8a3wDB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYD
+VQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQK
+EwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJ
+EAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
+A1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQD
+EwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASCAQBhAYUa+Ccopm7PavBb
+FjOLJVcd2u8KJ2+n2eiDQK8EWN90zd9q/Ba3eVLHBihbuMLgJ1964crCikBJTOSA
+PguHhdiUjs6GVYbRY9sZkYbVjDJ7pqWCj0TRpUuctOtSIfHJ0on+Kc1RPzV4Q9QY
+OKoHT9yRx3bSeNKeUjSzUz/fiMpxImnFerva0CaJCo30f0d7aJsnR1m1QRvbfnYQ
+ONaApLGJRAA79scSTRA8e16iwt5dkkpr+jabpfeHK94dYubk3RmA99NadN1pNvE4
+pOzC6rgBJ/nExeALjXM+bmhdeI6vqCpx99H5xGzV6BpSZAyRk07vT5HyhX4sYl/O
+xieMAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.bad.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.bad.eml
new file mode 100644
index 0000000000..200c40916b
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.bad.eml
@@ -0,0 +1,59 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD clear-signed sig.SHA1
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAM
+BgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrph
+Ew/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozu
+WF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qa
+P6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8
+uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq
+46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWg
+PRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8
+lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQn
+MnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvv
+nVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9
+ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBb
+vCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0D
+H920bp2/MYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkq
+hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUy
+MDRaMCMGCSqGSIb3DQEJBDEWBBQCH3ANIhscFLil//h1Np3FJ8a3wDB4BgkrBgEE
+AYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQG
+EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
+dzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAN
+BgkqhkiG9w0BAQEFAASCAQCIyolmFjNhn+RqgufettmYEpqi3dKJSM3/qTykclNQ
+zu43v36T+FXEZ6n4aloo7OfHM/q8VCAFNDdEY+StBGA1eViozpAyf0u5+iwb4snA
+wec5F8FkP1l2DmHi0IqschOkk3xUP4+YTXE/BT1JWxSpJYyYdn9ZRH4vja9p+xCC
+pChh1i5MKYJe6nf3u/R1so5dEx9aTqV3UwBC7LhU92psH77Qjsh89ndgQwHbb625
+lCi/Y1ePLehTqzg67oknqnSYd3bTkZ2H2I9OZTRDfJks392qvlbz5WEb2ThBakXn
+MEceLnvWENGa1tgMo1DGiz9+6Qrju+ywvFL/1nN/KWcGAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.eml
new file mode 100644
index 0000000000..49c7722d29
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.eml
@@ -0,0 +1,59 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA1
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAM
+BgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrph
+Ew/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozu
+WF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qa
+P6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8
+uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq
+46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWg
+PRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8
+lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQn
+MnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvv
+nVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9
+ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBb
+vCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0D
+H920bp2/MYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkq
+hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUy
+MDRaMCMGCSqGSIb3DQEJBDEWBBQCH3ANIhscFLil//h1Np3FJ8a3wDB4BgkrBgEE
+AYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQG
+EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
+dzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAN
+BgkqhkiG9w0BAQEFAASCAQCIyolmFjNhn+RqgufettmYEpqi3dKJSM3/qTykclNQ
+zu43v36T+FXEZ6n4aloo7OfHM/q8VCAFNDdEY+StBGA1eViozpAyf0u5+iwb4snA
+wec5F8FkP1l2DmHi0IqschOkk3xUP4+YTXE/BT1JWxSpJYyYdn9ZRH4vja9p+xCC
+pChh1i5MKYJe6nf3u/R1so5dEx9aTqV3UwBC7LhU92psH77Qjsh89ndgQwHbb625
+lCi/Y1ePLehTqzg67oknqnSYd3bTkZ2H2I9OZTRDfJks392qvlbz5WEb2ThBakXn
+MEceLnvWENGa1tgMo1DGiz9+6Qrju+ywvFL/1nN/KWcGAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env
new file mode 100644
index 0000000000..7ae46f6c8a
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env.eml
new file mode 100644
index 0000000000..9fc5639e62
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.env.eml
@@ -0,0 +1,84 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed then enveloped sig.SHA1
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAB7306xpmSwOuE8Q2GRtHo4mKadBM7i55/TuhavTFUYcNpaW
+jnf/mIOma9IbjaRR5ciOFlzL9uCWTmAQUBEpP14dgP/V55YUemI0Rh1xzgrhCmZ5
+VSQKBwx5kJfzUFGHP+1xjbxiWfsVvgvf5nFxpwzUUZNtqsTegxE/gtGtbCbozPYu
+eM4zseShkbSPExtL+Hz79aWPX03/KG/9s5Tek0cg+Jrck+vaoGtLfnPkQIAfHdgj
+bSM6m5/OoCFzoa1yd0MBRNJClQep80nKsVwugzScnDcfIzNKH8RLrf8Ls26oj2Cg
+JqqwRhTTIlUP75mqiySdStGUbZPXwxwx6cg4uYUwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQAOD4EuEKS+lstmQh1LNzj6CABIILgJLJOQpaaAmnk9mRLzvZOntn
+QiEeipGgrgvQiwbX73l4zbWXmNbVGiU+wIEX+xy85Mi11inFp6//TooJ8ORnq4oY
+Gz8J8SfMqJYqXgN1R/W+dQ/UdK6IVBYdktJmNb3IVUKjArBjT0hhfgwo2Wr/y87v
+o8kGdKoi/ZRK/H//kVhmFmOVeOA+3pTa+4Qg691GRm941OhXHAXGiCwOwXPwuqj6
+Xzr8D5Npq4xQrL3G6iSfpcPdWZQKcTvoL991gAESKWoigdXAzoKZI3Tf/0dhMacN
+pr13CgLmpKi/GJk6RUOE5Z3v8bOu6seJfx2B2JrgHTLj1D44d7CirDSwQH6W9ndZ
+XC0vDaA696Fo7hoUpumuw9HD/eQ9PGBc3LqTN9nwsMesyO4Ffh0mCzF+nGiwhhjv
+Ipvil+n9nFOVRoBtXWNVyi7atxPMeHobocx+gJ12g7JJr+wD6nzhbNilZoD/uYBX
+OKTpOBRiHegQlHPdK4UqzVXLK5fENHjEjfWl/CjeIOO66o3nP0d7qpDJCUsF0UeY
+g0qeHqoT1QBzIa091d4y9oKg+hb+AbERNR5Xz4GojyWjfa6FzCyZq9aTyYTIOIzH
+avmVEr//hCxvtPKT8gXVr4mGxjq8AonojkE71WMIVHk0LZXeyap1g1a4ZkG9XeJk
+BJ+YNpOAD9KpraqwMffV8QigHe1aoqUBxbw4ujUSN1rv4Gts9JHLWPqbnLn6HRDK
+mW0bcqhGxgUDcAzvyEY2+eD2e+KZrngClBVZk6XwF7apVMzbTZ+l8V+1Ax/f2d79
+8AzrK5As0Ss8bD/hr71GTsQaivAf450qISay4h8OWv1xGGbV8nIl2518dTuAQU97
+3Gpx9hZBRfV/vDW6OX9lzqPzEodeAKUV8CrOA85sdpc5M4yBRkzpFlA+WGJSrHuR
+D45R9ydRl/3tqLtIAxDxKXSEa1VQg1XY+VLxNZKuBEys3aPN0xostBkkD0f9JZCk
+GfhBltAKESFdW3Sm+m7WYmEHcl2lUwpHZMtedOpkQqMPYjFgoWOV22KH20WTAek7
+ARLq85/76IYm3nPYM0KNs1O72khtMKTp63wMz9UNyvtfT9CMcn9sAZ0O/UsaAijw
+12B+bnYb5c+jHSCmN04WelNP018d1Z86CZ3n92qj8N1iGs4ZjPnRdLddssqCkMby
+QEPteOpEH98KD6A9ZtJbasI5x1XnrcNM/wdrf4ScT4qXR4i+lXCsMnweyCl7S51l
+V1IrK6GHmW3I46SQhWqF8520xcqPFT5ib/PUWxfi1KDe7gaGEaiuksDHLrf1Rpl9
+x1swoeQKqGbw8AucFuhmTu3pWWD1xgxSgjc+89ysn9EZ+l6Xowc67w8VtFVX9kfx
+oEnOxW39XeDAW6nXLLLy8mIewJ6bmBtzgDHsp+c/+zHEndRYzbLS3VOABKrVWJZG
+DWVF5R4patYLv4KsUgUfuDrrM/v7nlgtXfjskpLiAdyIjGjgywZD86Ol7AGKrNi6
+XnseTMx+J9wLuGF8/wR4nc2pLbJoIBNdToUIcisKfUA+UVtJzBqkI8jGDytmVXLZ
+GO8nFlDr16dJFvPCYFUEjn3jYPyZNzWytzaxVWIJhjnG2Rlo0EFcJbCVEpLDT/d+
+McSoxuef/1gQSm/hm3qYkzTkJnErYv9NCOXzf3DTFj/HZjJem3kT49J0/KDxrnI5
+yfincjB/9SHJiaLOSTut1PnFtcFdGXAiN285HzDJ6mi4711BmSATqj0l7QVZyoqJ
+4KHlWUGou02sXRo9JlDSRdy9vcGJ4pAs2XIjDF2O7935Odco9eLt5iDlPDNd06YI
+2UstWJ37QFr1oNJ4FtpsQxtQKe64Ed9qTlJoSZV3ft8wxmZMFizQ4n2Z6peoemMy
+dwl3EqmWVgczNBVol9EeOJQ0F2+v7BWir3pH4Pii4w8tGirCB4aecc9guj7fixeL
+5zjvCSL0QuB2CXX9T2G7L0hYZM+uQ3lGPDl0Bc2/HHLqwf5cvr3oLNwKb4vfJw5d
+1Oh22nT+4kHuDnOA6y0n5n7X6Bkul3XrPYuv1IILR9K2aZrkMbo4pqRulcRKLL5O
+eGEZoZ64kjxShCeJfqPzLsxirseqgCj1vxKbCeY310XUB9AHEhGzfaVZadqiBqV+
+ajzosSWz3m2HgXq4hSKpwaDdATFL/6Man004p8tykza0JQquBS3akVoB8AEyGPAH
+6bR64ipq/CtbCxb5xzsJjyWjy7pWYyb5jOJLJJAM79CGweYL1h293MIeLTe+htBG
+9a2TpyM6xGBebZJ6NLjS2Rij3XYVA+PgIqsx06wJRNQbsP80asHdJ4Fgq4retzO7
+dWO3TfyrjiQWYUi3t3dWUpwFA3M07Te6evKGP6P1qZNGUQVUpKJ9A0vTcI+//Hc1
+wnMekFNfYQhRLip7za3xLxXv4rgOip6lzqKQDcATuSfBYxhOD2PuuiYpBWWAF7uH
+BGsxD5pFEfrhLa1G3odYen0Ea66+56Nm7m/sglRJIHwcoEEacajKuF3Pe0CgY00p
+RjzIAC4bXoovHSUEpvNsgAm5sN9VixiJQFwkntomFqX22t5QHENYoTeofB32M+MA
+niAMAh1knGD+PErJZ7Q6P/DT/c4Db4PyS6L6Du7U4TgDSfsx5OOc9kM9tN4SR9iB
+rWqvW5cBCu3vpD7W/HmmZa6JV2NxCVa2myJm0iCWEEZ+/WhfiGKZuopw0CaK4DrG
+aXnyKLIRgmh/leF3zogfNf2woRrt7OnW2rP3r2bVt+t7R3LHr7SXEPAoqcEXzK55
+qRhO+wl8Hs7EmYQ3+L83jRmZVh8hdw2LZWeVetWZOa0/KZm2S9WxnmiW8dbMbAd2
+VWUnzsQDD/LIkI2MKiqWMpN6dl6YD+cEuKHLlHWi5ELbupcKJaupUvVVMCkHQBDz
+6YZu7pfjKQUPbZUI9DhKUCoNokBEjhwNfn3CRRMXJfnJbYDgSGtrQxsc4uXlzJnt
+ViQzl+cz8Oi79wIb+73kAOfAFqNNzQ/kcvVstyTBuC2XnZ/l7L5hfBXCK1bqhysQ
+jCg/RvgS7KJsKHquRWGATKyZuia4yLEo2gj4FYZLFf7jXKEwh7FMDvqHq9nCsfz0
+aJNPXYZL0f05lGv7sPwvsQkq5LWeSRhvSNHLr383G7262Pl3VhUxlYvTObUObxcC
+Cf9OEbNwz971AiKyeQrO8faJm1ttjq1VwibYBiOAYfdW6DMcvsjxo3Tiu8AGz8/Y
+tz4axf8LCrzGLi00UFKZjxdYAgTeOxe1ATUnV9//yvt3OMuRWCmHVBTYQyA62mh2
+VnwjXrZuQHI3Py/FIWMWR6TxrYflNRkW5WslguLNKQpbJ/VbvYTb73QZiGQAeUMY
+cCE5WqcEev8Ky2eX9e6sjzQijHxJ46WNHEGZ0XoNm0P7Y0PxRf2EjbSi1qe+XkyP
+br9qGLzcHGAbBVCBPrkf4kwxPRu4SP1UUqWaT5VCErPU79vkwUJnzUWrS2W7psQW
+KQjbHHkgZAqTUr3WUJA0wBOA8kvl8xfB31Rl1W7WhzbwOeyw76V+JfkiVFxXhRPl
+iSGqwP0f1yaB6AmBskSmhqmpt4C6jQG0gq6ucEPbSj0rMGzcuSnL0Buq9z/9j/+I
+NTmpFdUBecgoxcckh4mDYQmRB+6o9LXd9pfr9BfXKxstbRbsNFi9mUovmvbGnikY
+OaBPXQudoDIDJFGoqVzF+PRZMlGQ4Vnqm3ojPfteZKfOBZYiMOvooEgddCXHI0Ei
+8TlcdrrWtaSgimV4vUfIW2UGe3zeh7dHmwsJ4tRiSAMXJEjgc2/pQuC5LjifGrnn
+N5CAFA8oYFAV5T5prTyUVHlMled5b2ndwDqM7YRkmAomTnKe0nz4TIPvzyjDeY2T
+r7yuGXPhBD/ELedr7vBKmrQzV/rsY1a2e66B8u9NaEqUZMj9dFdjpETQXr4jv3ME
+EOmuA/ZN/RPo9TjuvjEKn5oAAAAAAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent
new file mode 100644
index 0000000000..861f10e1e9
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent
@@ -0,0 +1,55 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAER0NvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIGEgdGVzdCBt
+ZXNzYWdlIGZyb20gQWxpY2UgdG8gQm9iLg0KAAAAAAAAoIIDYjCCA14wggJGoAMC
+AQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMTkwNzAzMTczMDA3WhcNMjQw
+NzAzMTczMDA3WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4G
+CSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4F8PZSndSoPhUMMdVU8cIVqW
+Px1Hg+OUZzfTk7vf8dnwOWJqtoT6O+5Ff432kPbIdjIm3m7Seo0eBC1s1W0z2kYz
+tPkVCzSlAfMg/5bEpkPgVSWQjn2LxsYuDHRzrr/QnwUg+ZdwIFps29PcK8XFQeaK
+Nc/rm4UYhRTZ34iEUQf7QQVSQZaxPZCunxj+qk+T1KaIKd/KCLJX9hFXybbGM5TT
+0BaxQahdtls4aUnymmhoqQ3fJJwRIB6Lth/m1TPvKE5Xj6boivY0A0DGhFWNZmxO
+vDUJa9HcMwc3MamgzZdxnp41BawL6Vkwq1SzwCcqIBaM1kVaUYvGsAGl4H5xBwID
+AQABMA0GCSqGSIb3DQEBCwUAA4IBAQA0+5CEGeVmPLNJkwDBP8DPzobniH+y/s38
+aNRu24qJuvB522+HW+6BMwZW+Le/PFojvUPSQjgL4YFDvKnjtC/YarPiO3e3Q9hi
+lMRldunE1Yl8Ldh61zbNLP/JLgHroICza2F5YIb42qzAANeJqbZT+08ybxBiUJZl
+q2v0L+S2bLPhS4Jb1sNIQi3BnL5f/nlnpi3TsWgLH9lN2CDeMK3A6qSaEgZ8pdA8
+pHLRzJGD2mHUaXvUY/lrl76xYXuwqYlJDg+0vI8T/f0RC7FHPTdbqJZjutbvpm5E
+anXzRVL/rv3YzMYz767Xp22Qf9Tk+fOpUbsIvdj4f96+ytVusgx1MYICyTCCAsUC
+AQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBNTAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMCMGCSqGSIb3DQEJBDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4Bgkr
+BgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
+MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDAS
+BgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIB
+HjANBgkqhkiG9w0BAQEFAASCAQA3Ola6Yh8cBgYxColrYgWEVCRuAfcPDc+iX7EK
+5R5WDLEJL/6nLXTn8bi7ysnDUmeocPtyzxXh6vSx30D45WbsuYV1qZbXJGfe3xgM
+yGaYI99VRe2kuduA6rqNstg8MEpyjSTd0pQVekKdQLVWRoOF+7s8v32m4m1noGu6
+jHAsXcbb00Ck1WciSjOswXFCR6xm1o2nAQwzMpMSjclbXvi0epLOJpU2ww7mLG45
+LGMIV4SYMhP0v6sL/UUpmth4piL7OmhEfzXOvmKRxhaHkREdHJK1LUoCjLZ616Uo
+ZhbZrJSMf9uyftdsJXjML++R9Xh22MOqwP1FkejdiLNzqbXJAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent.eml
new file mode 100644
index 0000000000..bc85350c5c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA1.multipart.mismatch-econtent.eml
@@ -0,0 +1,61 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD mismatch-econtent sig.SHA1
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAER0NvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIGEgdGVzdCBt
+ZXNzYWdlIGZyb20gQWxpY2UgdG8gQm9iLg0KAAAAAAAAoIIDYjCCA14wggJGoAMC
+AQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgx
+MTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4G
+CSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfw
+rnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozuWF+bK2ujstKVXby5RX8S
+Ycuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FP
+MMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/
+Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWV
+rhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QID
+AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbT
+f4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2
+LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3by
+YenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72
+WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUb
+tOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0DH920bp2/MYIC5zCCAuMC
+AQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMDVaMCMGCSqGSIb3DQEJ
+BDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4BgkrBgEEAYI3EAQxazBpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASC
+AQCC8kXRuO8IiCkyCMhOAp+URAoBKsokWbpFG8Vmr0GFge7+rOoOeBwJk+n9Lq0y
+CsPyOEKe1oPOPAcU48R/B6eh9wfdMcWTdmg0RxxptQIsE7D37ETRr4aG0kPMD5Ay
+fGsEttlXprBwrsClr3skd05QNlFPEhEsp/SYBeNl21oJhFeGq9kxDfP5VJ0vVMEb
+XEjdP/YiJFBiwX/es7p2BNnmzVPgquHRMtN7ZIR8y3c6PgHovAzxvxhVYrKuxaEp
+u0oKkS+CEQasNYF8nOOkT6ER9KnM9G/wA1aQflDtR9Lf9gn/QVb12HnVoWhUSKtQ
+e9uhSUBVTeVrORDGqQAVtDCSAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart
new file mode 100644
index 0000000000..9f52ddd580
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart
@@ -0,0 +1,54 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTE5MDcwMzE3MzAwN1oXDTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AOBfD2Up3UqD4VDDHVVPHCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2
+yHYyJt5u0nqNHgQtbNVtM9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/
+0J8FIPmXcCBabNvT3CvFxUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpP
+k9SmiCnfygiyV/YRV8m2xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz
+7yhOV4+m6Ir2NANAxoRVjWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8An
+KiAWjNZFWlGLxrABpeB+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnl
+ZjyzSZMAwT/Az86G54h/sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4
+C+GBQ7yp47Qv2Gqz4jt3t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG
++NqswADXiam2U/tPMm8QYlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07Fo
+Cx/ZTdgg3jCtwOqkmhIGfKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyP
+E/39EQuxRz03W6iWY7rW76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y
++H/evsrVbrIMdTGCAtkwggLVAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFBMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwLwYJKoZIhvcNAQkEMSIEILJr
+dzzDNYWQV/M1oK41/rfKXs+hx4nk6HPGaJpiwfmGMHgGCSsGAQQBgjcQBDFrMGkw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYD
+VQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQK
+EwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEB
+AQUABIIBAD8R/CfmwG3HaluS10rh7iqhaWNmC/JO/UvQ6zOf+2ekKB28vR1rRaud
+NOZzE+E6eoT8SKgC9sK2wa7Ywr2+YFrqjVQj7Oo0OJ6uzkId+2jeA1ROyJZGFvPI
+gT6RfTCSLje006GxQmXdxM0FnC+BcIGCFF/AmIAu4oOWx1Yh6ZkIqgiCrN8OeqRn
+lVmaG0TBFivJ5mHXNuPQ8M1xi4uB8QRaTbThTq22hZWoHuCQLHt0zwjkAqg4JZUQ
+skg7YwB8Vff7uzrcZnlzJVjYPObdlJSV9LE4LiRiiCeBIqiv1Xd2fRicRuMp17xS
+MdQvFJGcfqtP6IzDgIMdg4Tw6IH2Gj0AAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.bad.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.bad.eml
new file mode 100644
index 0000000000..ae989dee6e
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.bad.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD clear-signed sig.SHA256
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwNlowLwYJKoZIhvcNAQkEMSIEILJrdzzDNYWQV/M1oK41/rfKXs+h
+x4nk6HPGaJpiwfmGMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBAB5kCx05RMyY/fvE
+LTQK5mVlejmq/cr9ys74Q+xV9Bk79phxzNrBYLcJpfPO0B9jESPn5DIBVx63sdt2
+dQsV19WLJf79c5N3dddHPdN6DuuQ4kmcdvBXPgagwX+99w9crxE0jW1r5O5eVZET
+1/wP0fBsjBMkUtBFeEIcoyBJLQ0w4DC6lVQJghMI46Ouc2UNDt3VFSsSyzIGpvmj
+M/twajeu68eQD3evco4eVP75SJ/jrFtRaUYLvfBy0vpBemxznDDRqu5O6Bes75tj
+bFSYCY1uW3AFfpwN7l3sn47xVlcTwYlK0DUoKqtJ6YYJW6cMf+pSZYFuUQed/v+O
+6k/oDIAAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.eml
new file mode 100644
index 0000000000..c3c2ed9067
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA256
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwNlowLwYJKoZIhvcNAQkEMSIEILJrdzzDNYWQV/M1oK41/rfKXs+h
+x4nk6HPGaJpiwfmGMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBAB5kCx05RMyY/fvE
+LTQK5mVlejmq/cr9ys74Q+xV9Bk79phxzNrBYLcJpfPO0B9jESPn5DIBVx63sdt2
+dQsV19WLJf79c5N3dddHPdN6DuuQ4kmcdvBXPgagwX+99w9crxE0jW1r5O5eVZET
+1/wP0fBsjBMkUtBFeEIcoyBJLQ0w4DC6lVQJghMI46Ouc2UNDt3VFSsSyzIGpvmj
+M/twajeu68eQD3evco4eVP75SJ/jrFtRaUYLvfBy0vpBemxznDDRqu5O6Bes75tj
+bFSYCY1uW3AFfpwN7l3sn47xVlcTwYlK0DUoKqtJ6YYJW6cMf+pSZYFuUQed/v+O
+6k/oDIAAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env
new file mode 100644
index 0000000000..d2bb1400b1
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env.eml
new file mode 100644
index 0000000000..3bf13bb6d7
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.env.eml
@@ -0,0 +1,85 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed then enveloped sig.SHA256
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBABu4Uw4nkFZTLMcutFrICVwlrMMmmGByrPE8ppYOxLrUEKpP
+9yVTxDfL5xeL110+MOBacbHKaFoB1lQLsWrNQ5pyJUVVl1NeTp30xSYAmSd3fps2
+BG0iuncBfwpvk17e6TKikSo3v3h8b4oX3yTjRhsst4bGyTS1lx0NYKSYfzv3mY1n
+50bpyF/FPpvw7skD+u26v7GAkjel+g6J3SPfPFPHLYt+4QxTvFSGfQ/lpwta34Tm
+vemzqopMiUURnBYjWMqzJfOVJKrwXNyUYwxvl1y+9rT+SwUlbRFtRrI97GaRJ8vD
+7SZcx8AS7YMxs8N572q5tnf7Xm77aIV46ZV0kIgwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQS5HlNF0WuXaInGAhxTVKuqCABIILoH10enusrXTpHTn8llvsOp7s
+vNx4BBkQ6wSW8Ee+yJ/8zySe1ZCIQMk9VBcvX/nLAdVszxOtIhkjOuj1PDoN6TsD
+R4kCjsa2OfO16ujaAHAdpahD8Gs34M8MPZ0M/XVqXC7/1jjp5t/1Jecgfhm1ylsR
+VFSBFvJybTV/DGkOGyrC/PxHe0bqHU9Ef+SnhLt6cozgaeenOPz1IWJ6A4PP/G8Y
+pUwwFpu1eCPWRfZ6TOIOfQprAgEja1WUySjCMG3I0vwJ6AGr83f4sm4EJ4eWR2MP
+ANuAOhHkkVWy22+qC06dVnEWqo81Tr4+qZ8L/QZJD1CaGuTBaJaIPTOJ1rqoMP1e
+0whXIV/3f08lJD42cfWhHsJrYEd2Go3JkEQLfQwMovBuZd50TOe2+iXoOTGjLZML
+W70bNQ9y+WLXHi/XGuQNNPZbiQEVixA77SZTygZ01/EnyDV/qmV1o+L9xa5H2JRc
+iOPLSdr1ymQKMl1gRL0hFfUiE7XUFGEF+E+69qbXLxfuk1YnuJD/o9O/dC4ALtbP
+1qmQNOoHjmczmrd7QAV/vOJ+nEo7NdJXIYkHfQpcuu6x0lOAt1gRKZcVp24HHgYi
+7TKIfqL5KNN03o04JwuxmkipS4DaijSTqCjJWfVfJUulMC3ry+sVZz8+0/S+hZQ6
+O5D0QsVRnglv5r+Q7IjI2fsGeSIf8qzPqW9I1h5xglSSYLWEydK/YYc/SvaxpFQj
+g5oywA08E/XvvvbbsGRVmcs5gJyOFryyMXv7OV2UMsNDqLVwnLrPvFAnlu2ghCFS
+J8FaZa1fzwSB4M+xiuxhDRLQr8ka1jnSMLncusyovoFuUB0OAED/4BFVlBJhn+O7
+PjuPuA1Kdvg4VKW0+5hY7bFM1THFwIeagVh2TcFTuybF1YVVUcMjwctHVCacVBKd
+Zs+Gl5b2YHWlEijHFs6mPJ5yLSnI44JkJ1v/IB1GbjK0oQVjMyZPsS82a22//nTr
+Juu+1euh1KjD4NZ3U8k3/gCe/ruBE8VccFVFu7OsKn/l+Pz1uYGeGIP0yDqh85m4
+y7eYxFEFGMsfNfgnVjROSTOT6pJyDBJJatfI2flApOVuvNoiwrnX8zqnEfatuYt0
+HFlrj5oyZ4h7f3l+Ec/oPIGzzgvUmCLvmxbVFQfUqNmcdQMK+0i4RVDyCUOYLouT
+GveHJvQqP4DTAQtZQUFh+OOXPKCM7ZpJ0r1lQUK4w2FCCvyS4pSIZCUHiruh8lYs
+5C8p0a8H9crAlCQI/lKlwI+Ao+jhHxa3z1JZMrbafmGRX0NMusG77TA0A2kAP3qX
+RIq6Vy3QOqyvNiR/dhqsYcaO6fMbSnzAwuQNQq/63EfwEq2MFBmU4ZeT2H8lranK
+xqz4RizFC5VwpThrVsx3pampSU52fWZQ3O5IHUECnyuc/7ATe3wJhTJkXZG9A4xQ
+5B2SljH7S4LcwyJS3eyYezs+Tk6mwyemkP/aKC57DQGSoy07gRJb5ipcMWVpBks7
+fGsTQM41lW4jzl57/jPBRC5bO9YaNv+lbDZC2T4uFOTlqw/Su8tK/+2eLoegdO8w
+ImIb1TyI0p3lUjKGvDzAggpRxzsFG/6tdodca4+N3KslhXLzeP5ItjlcPM+wDZq3
+O6vdpQ/AdBKjqwt6w2sRrHH4FOYuTd/BP4GvqUPxDd2ZAMh9e6fhltW/ZZ3Yix/l
+ppnMVYl7dwpjtKa4KxtWeZQfGubixsR1s2+7Ul/HcCi1Fk8O6EXuZfnMQcLdL+FH
+Ftyut3PizmfvBYS0kUiOIos/7GT8jM5MomHGmb7UMozM6qrxKQpo8Y97+QAOBAj5
+C+8w4h2Vpy6+DP/E58AU722Qgslrw6Fc9mEOod1p8cya45S4Rv29WEBW9Zqw9wOA
+n/GpuBFwv60/enIt3SD5rl4+man0raz1l4E92zZ9IlzY2sheMd5glLJ05Sc5WgyM
+o4koTj+BYL5vo+czWGJFakCRPWefOznszTtNwI9acBlafLYm7TskRuCasOv80S3Y
+BbJrOPVLbvbnwfM6aRFt8XblRzB8lxXndDKNjYZ8eZdoH6f/MNXCBaRWbyKi01sy
+c5jzNz0O5+XPgbfaec0y7u0qnLgxEdXj4dBxkF+fjmSbWEGNp91lR13IIarD7+3U
+EjnlvDiOds2HXghLcfJRsSZ3NvwyEHqP/8kjcEFf6kHxWjp7qvbz1FknJWf3loae
+wnq99ONk6AfdkW2P6aWe6ovKfXcRlAXTfcbrFq+4swA7Mc29GVYk9JbaddZVsZpH
+V3rse9IscUZb9GU48bJOqmTDF3dYnPAMoY4MvArUuXvGHiw9mFIQdI8QPjuCw0Ty
+KQjOJ/EwJ7G/2nXqBANIlrEIhLH7IljjdQ16cP5MauT6jDvS8wKtcm4FqOQ/g3Bp
+NcFZGXXPTZCbCLdJ4dMk2FLUDJejXVhh621rpl0aw+12I/9Zpf5LO1Zu4VkT7Btl
+ui6med7l+ngYt6V8RO4jCade1f5dKfWo9CMFa4sw/aXVeJqfZTdc02ifnEUhjt1g
+YYnjSft7TdVpHfax2bSKjLFKqhpMDj0O+jxofiYxBMOvBH6t+e0SL9Cita/qankU
+aSY4m+topmDBzlfDlq72Fvt/bjd7tejdMoxx+7nqI3Ll2scHBoc3tvRA4AqR/hY9
+w1PcPHrvyvxUwJMwKkfyjrQgpHPLIfZNmh8L3Kw5sxQGskNo+hbcYdKbhYu1ffRV
+iGLDcbbpbwc3sKTjQtt58UkjIXUiaOcc69Q7alQqnaiKaF3RNTpVFS4Onpd/GrF/
+Z+LHCJvtwOp9HJnIwMxMOhlM4eMcdi0oljhEG8JKlaEetG535B33x7c/bj1yRaDC
+PN3052dlN4bphQbj5JivknavAUqsYfNUyj/NFsJDCUzf8/igybAxCqXgB5c5MAQS
+CftszaH8y6gJEXp0j3Rnhvm5kIWkIYMyt9YeER1oM/qviGCgQD+0x70Rb4eh2GWY
+uwX8e3nMXLYhdNpCQdPHqeibz1o1ZACl41j9IahKBs8k3cRSWgLpxAelCu8lL/xs
+zAir6D5ifeIiWxFD9/wbqTUuCAnuvteQU38XkgQCccVBqnFkGlDynTYLmG6Ptn2g
+8xUmyJrdJV0otQkyUQ//QuuNB3jF+cIZ9yayFOsKcfRkvNlMiA+VTQic7enLW24A
+FbcAovX2CJtm2FZnSnUIKKORAJMwaGlZY26+V73VQ25Ngyi6SxErOnqZ/IXQp6cL
+erRN5hcVHlpJXnOrmcaXLaytk9C055p5Qb7WLuEmvqJvxTE6sVvDB2B+78gURgwG
+22SVQRp5Ic0s8u9syf8REi7cjfSR9NurTNDoi96jG4A/x7D4s3cvHS9Ss0az9j71
+P+15Ta2jPdEV2FUwQucpBVkuPysJUiZYgGrcajd0PnwkHTSNz8ZMQf/V/0lwNeeF
+oA8+FxCZMp5Lgk9UL5hxP6fD+H1yNsALdkRw7g6bKsVadd2BosNe4JYxce1t6LfF
+A4a1o4kPnaPZZKnLH0p3dw7lUKXqCqWGO5vGxQVCNpQCmip4gpup3JV22CaWosSO
+LLV7WbEZtK8wHVkKeayzuooQgZ5SRKhQWRXgLeCQiz8a3Q8LIOOfKHNLSl5PkM57
+wbPLLiTG8/XCWUimCy7JIudpSdaz8mC+WhhiN8LKi7qRAcJGd4zd4i87DbgLWP26
+AN/LbI/bVudcNHgSL9grBzulA/QjP8EEOACU+osYAFI3UU3zqm67JPEtUvLB9bhK
+iOSfcqnmjdTV1uREDuIKz1VkjhAgPCz56B+3mWPQy4XXWbnq+17PWic0uAfpMbr9
+V+cC1RVnlTLIs2akt3XlZlDfIpxLwXNdafT4EKn8jQsHiX591Awfbjb+lpIZrBZl
+tVpx2eBm2prBeaQv7bYG2k8xVDpDPr32MTNKg/+StsAMIkoJ7k+zxTTaXU4fV7Bi
+DuQcX010iuE1nic/rIR4coS5JR9qOQh7SPqlMGlfmQQQEOXj06U9q35T3kLw0Sb4
+EwAAAAAAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent
new file mode 100644
index 0000000000..fbcfc22aeb
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent
@@ -0,0 +1,56 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAtkw
+ggLVAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCgggFBMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwLwYJKoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3Q
+YbZ8NKqgsYvmT/s/qgusMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMx
+EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQ
+BgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZI
+hvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
+MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDAS
+BgNVBAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBABC3gc5l0jis
+U2prBP6YpKTjJef8Sp7RegkL37NO1GfEg8lVZiFZGnWGMfGD6fE5tbiixS3HIhnx
+isW6nO3fXwkboPDJCkfyt8C0ysSiIYpKhwKWAZ9Ed2D9Lo3D9UJLuHbpHSTGcGTr
+z4VqoVE50hXAp7LtaVt83ZT+AyLLLLvpp8bcBB2S1CvCdeGFJ3pF56zz9OEm+Sxq
++FsbbyokYg+fIETDeAUGhCRQKAU/QCvQEP3bRcxRt0yTZvX7f60ga9j5kZkbRuiO
+U5CWfXBZRAipsk1fMwkJa/bFqDfVXDXnzke/nHSfw8WtHPL9SeVMqZyS5RBi4Gjd
+fDoI7erSDAkAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent.eml
new file mode 100644
index 0000000000..6c6477a64c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA256.multipart.mismatch-econtent.eml
@@ -0,0 +1,61 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD mismatch-econtent sig.SHA256
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAvcw
+ggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCgggFfMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwNlowLwYJ
+KoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3QYbZ8NKqgsYvmT/s/qgusMHgG
+CSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
+aWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEU
+MBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMA0GCSqGSIb3DQEBAQUABIIBAIZk5LSUiVMZeaoR/ftlgvF3wGOcJEqIlsSe
+wsvFs9CEcbZhLwP5+mX78MBtD20ZmBej3c+6ZUTdELXW6/mpvWKV9+VJpH2mXBAV
+OQSxzzLLxmxA3j4CRiOJmYG3vEfgK5oa3fCxoP8Y2j6m0WSreeWJFy4oSvDkgP4I
+RSUu86NtzkuCwf6ZD7QObkhMMvIUqMSYDd27YkmuRhIPhjuDfUsxVnEzxuvRgifJ
+QVKvpkj5lsgeVa9T3GRXIfdgOo1w/HS/QpiQHCHs0UDHLzuOkQYVNVGLTuLFZJCf
+U/Zq3Uc8HGlPKQ5lnPFMzK2mUt2DQyRDubkk102xAkEJGFsujtQAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart
new file mode 100644
index 0000000000..5a1a1c2fdc
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart
@@ -0,0 +1,54 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTE5MDcwMzE3MzAwN1oXDTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AOBfD2Up3UqD4VDDHVVPHCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2
+yHYyJt5u0nqNHgQtbNVtM9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/
+0J8FIPmXcCBabNvT3CvFxUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpP
+k9SmiCnfygiyV/YRV8m2xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz
+7yhOV4+m6Ir2NANAxoRVjWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8An
+KiAWjNZFWlGLxrABpeB+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnl
+ZjyzSZMAwT/Az86G54h/sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4
+C+GBQ7yp47Qv2Gqz4jt3t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG
++NqswADXiam2U/tPMm8QYlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07Fo
+Cx/ZTdgg3jCtwOqkmhIGfKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyP
+E/39EQuxRz03W6iWY7rW76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y
++H/evsrVbrIMdTGCAukwggLlAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCg
+ggFRMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwPwYJKoZIhvcNAQkEMTIEMFHS
+xq+v2uYEt+1Cl18IAFzsK6a/jcQXxa6IzEYbHrHifrdlGO5onbLdf1KfAHKiPTB4
+BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+FDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQsw
+CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRh
+aW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBD
+QQIBHjANBgkqhkiG9w0BAQEFAASCAQBQN10JIbN5LfL6a6pRD574/fzGh0/ssmvM
+YVio8jwZs6ayh0LDT3QaH8PeyNf1RWM1fTRQ9wzbiv4HL2xjVk/fscrjojertRxm
+n3s3jxorDAbpNMcAyaVLf41KZ8BGih/gNUAqEReLq+5thl6rspNSiO/joWNUUZQj
+ATXS33g+A4Vm9/SxvBxA5STyRUJ+VSMhh0o5FcZSQgnjHH18F1lcma5+Xx2Il/6p
+K3oqBb6azXMdGEdZtmXu8r1hTWPuBNGI64wAiBhRdbk2c54oft0hmw7JzZKIxSoY
+Zk4FW8nUwJr+AFAGXHVOWpVN0tn8L1k4DHoEgFez/O1DlnHxu4oEAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.bad.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.bad.eml
new file mode 100644
index 0000000000..1423fa9482
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.bad.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD clear-signed sig.SHA384
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCg
+ggFvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwOFowPwYJKoZIhvcNAQkEMTIEMFHSxq+v2uYEt+1Cl18IAFzsK6a/
+jcQXxa6IzEYbHrHifrdlGO5onbLdf1KfAHKiPTB4BgkrBgEEAYI3EAQxazBpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEF
+AASCAQB6l943JrgvaAWL7sW/rbb2DdlcnJXS3o5hFDqe32YNBXNITY5xP9F3vrgJ
+TS54PWaJ9f7eD6HLaU77l98ySgkrxPHoP/EFHNmuO1a/T6PSuc3YkVxhXoIkJSz8
+3TEyTpuRm5UwBAjwZEV8BaKiq6bFi803L5NUEuRibrlQ3C/MldoDEd+7/0sAb0Md
+JPgD7R5oZZWEtosxH6B60tgH0Daotx/AZkNcjLrZ3YH6iHjUJKkQAPpUacGJChNj
+ibFZu57S3KjJSd3wfWfQk44bGYevMZGqXLl8PQMoOZQD1coWpLBvY1hZUeHGUyvC
+tsxPOCAraEuvsJffxIEaQYLrpeVKAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.eml
new file mode 100644
index 0000000000..a81560e276
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA384
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCg
+ggFvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwOFowPwYJKoZIhvcNAQkEMTIEMFHSxq+v2uYEt+1Cl18IAFzsK6a/
+jcQXxa6IzEYbHrHifrdlGO5onbLdf1KfAHKiPTB4BgkrBgEEAYI3EAQxazBpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEF
+AASCAQB6l943JrgvaAWL7sW/rbb2DdlcnJXS3o5hFDqe32YNBXNITY5xP9F3vrgJ
+TS54PWaJ9f7eD6HLaU77l98ySgkrxPHoP/EFHNmuO1a/T6PSuc3YkVxhXoIkJSz8
+3TEyTpuRm5UwBAjwZEV8BaKiq6bFi803L5NUEuRibrlQ3C/MldoDEd+7/0sAb0Md
+JPgD7R5oZZWEtosxH6B60tgH0Daotx/AZkNcjLrZ3YH6iHjUJKkQAPpUacGJChNj
+ibFZu57S3KjJSd3wfWfQk44bGYevMZGqXLl8PQMoOZQD1coWpLBvY1hZUeHGUyvC
+tsxPOCAraEuvsJffxIEaQYLrpeVKAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env
new file mode 100644
index 0000000000..07f2160fba
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env.eml
new file mode 100644
index 0000000000..8151234896
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.env.eml
@@ -0,0 +1,85 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed then enveloped sig.SHA384
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAHcDztdpIvfLvE3m7SUyV2YOPsiiWbe7ZU53PKWyJ3A/aseZ
+e5lgNRajBisJVbfbUmg8e1HC81fP/6hGpJQuAhFoYD5AD5H/hKGfpASd0Ul8LNKX
+S4e/oQd/lSLE9/mmyuYLH9WMID+AFNBlmzKku1VD2QKWSjby0VxCBAkOC+mmSd3i
+dL3VNOdvaQklcgKNUArlUuNCejhm7oosAKoathLpqV9zwKjJr/XYcWMna6mTDpMh
+jG9kJqTyB+pD+Ew2koCk4q8Yixe88+r8MrLlCQiWBnPF4fQFHQq6JsioSuDu0Y/D
+KDcxD68h+7uokqrxm+fz6jk8lWhEpCOl4ZSdc1UwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQJx73Up3k1h1UQRf++GD7U6CABIILsJo5f5+d/Yf7n3EmGmTp7MYh
+FjxIPKufeOms4QImdWRulsYGxzfLEtKvZnTVI/1yDfndWbhQdWzUJkorPaNVUjty
+NgkTk/okczl0Gs2F2tvAhitzuZLCQlDccJW8ONiOP7kCqW+m7Og075y1imLYIr70
+nEooXYwCZp57NNfMx+a9o/Qpf7gCK85BmPhTY8oJDJFhu5tjONvJZfqMzG7e0VJA
+zKpQwgFJf/2R5pI8lfsyLKHJT3g5tGZqautbquA4N8VJ9TUnSjNQz95A8rT3qk2f
+iFuAdDKcd151yc4VZXEo7tY81WA/yykL5Ap1dspPNsibNWqjfyJgfUEuARg1NpV1
+U9N3JSLuDU6ceCUG2iCohkUS87KVA4NXv8g4oRk/wnGmx01so3udwuurIk5mZIhB
+JvHtkRro2mWPFNCttRQA5qRpVtaD52sYQcDgG40ITzZjCkuXUbBJU2H/xWmKyftI
+lX8FlIS8efn/LdCNa9ECzRzzRLNRLAzpIGwKeSX36ZXglKnivDV3dNRqIttZ6dI2
+jJmZUpKFfvH1Xl48V5Q1iTcy+xT/dIuxeXBJRVBuFmkz4Ovdvzp4qDpvOrGoBnNX
+Utbw9C/8SGDHvJlMzATDVb/c8A8crEeYygxNmQtZih/vke7OggnL8Sem6JU5xmgw
+3SKul/B/ExiinSza4fYAdLps5qEa8RW+ff3R+HsHNfJ2pu1EDzm0GNf5BtOzJNus
+iyY+kuo+KwbgYEK2GPESr3eyI1fihlDlasz36RhE0F1yv0lC90gAJen+eZT74hLf
+P+w0/F5SYMrBMxZ+z5PVfPuGpUIUBf00hLqQdADmTAfPEarlskA2j1pxhQ+JDjSe
+33X3a+wxv5xEl6AfWLFaSWhLkCiHAU1hT1EJ9hOi2Zvh8edj2rBvyUGLk1y/Jjju
+Qtd/HabC5PZXzl3/nAT/TE6pAU1UU2n7iE5K+u/5l5WuVnOFhs7QOBeLidSg+8iM
+XXMLf7fYDRx/7LuI8NJf0yKEgndjgw11WXAv+spB/xOTXNCnbnywYod+dpWWc+1J
+ZPgHdVvRG3A4C1VSFE2CN/lYrl65p8sOVf8WiDHrdGK+Wfr99CQrtyRii6coaMzc
+neQl8LnMYpDaPrEoj8TieD7U1nmC8aCWbeGlOBFvo7fw6gmLwwzZfpLoXvdjxfHe
+f0XIxkc7AlB/Yd4HO5iBQvPgXWkUXY0x6bvPJ7FNI2CXHk5j9eTF8yTQ3F1iTX+3
+0vgbZWhLwJ0+MhNdPJLeQ5Sc6f19TeirZuooxIUnwhydFLmOgJCLWpX+L7dmGfJU
+lPGGzYaaVcbAWyC5PJcsI6pA7SdBj11XirMJ1AnVu5fpl17UHl9JzzFTwOiGt9oC
+GY2DNx81YPJIB12VRBPiVSHttqMmWmAKkBISF5Nky0hXTBoYPh/f2cF2iCJq0KID
+oSQRGSIQWHDn2K6ZdrQF7nJHPawAuS348snf7hAW4QX9oJAh6unxguZMOP/eO282
+jLuXOa+JGG10o2x8fpGRkNUeuHF/9P0QixnJssGGqUQzl2dEF5HwyYbL2spz/cpP
+g/cPWt+qyw2tT7xefwE/JTBj/zkDddByRFdeSMWJAp7n+q4tiizw1j0j66JWV6Ar
+ExFcr/wmIHvxsPRSyycTtikpIXuJvBkWzNkckvo5gd9K2NDXIATEMBH73j32iU8S
+5vkNfQinuBkQ70TSAxKFJwdvkWrg6xe2E8NwWO0KPO0yNmD+xgyGYVwWwasit+A5
+wCLgUBsvIfif4MGWrKCAmoGMAU5gEND3bNisa9y0OuU1SEetHXvQBbcoIAppk25A
+8lrUPsYdPIUubo9tb58cSu6Tr3uwhiaym4kw3F4WtY6A2+A1Z0HANjOj5y13qRfa
+cxZtpMx75LZ9C6T4DZa8rlZ+NcBMYVdoDPwY0Mo9JFuuZbD8PrhI7MbOki30ecTO
+bPxFYldfMGEnkUCPJculFntSVBEDhdl4RBvwjGhbOEQriP6tq+tDzulPDEPpgdFy
+Rv81YaojjOPjG+Zxo22JbVRMHxTv69Bqhvoxm5WOyk0s3IjWzbwmBSvMePD9kNgc
+X0ok0JGzguaxEi/MFZb1A9jw+7qhp3CrTCppQZOyiI5LasWs+WzPSKDvTRWrFoFo
+mPRcAIxWDuXxOtop3K+b5ivZv7PnY1j+SaOxuSQqTN7gszBwNHawUnTtWUiyJHEZ
+MsZ1qF/KLFUmznbxpdVGD3d4VPPA+ZsugSM9/qKzc3YifTRhW2O5LdMX4VoZ4GAQ
+FWCFtFuj0SvcqmhrmiXNerPr+79KnP/7Mh/B70vMpBK8VxwSDW+XRbYU6CfkWjsS
+JUZqKwtf2ovqtxfbjv0zTLvISOV1uowY8z9jzzkPJz54U17zeqBTHDU5XnAo+0iC
+RR4Xteua9YwdcFr30LZLKvrb+oDdThNDVZzHTsL1mE1lmWla0I0t2tq41tCva/mt
+YQ6rzb03bFROe5Vgrq0PCqws2/DBdD9l5EUxPs4vpflSKNSYOhiKmjgCo881Zo7s
+BOPreA6sGpywgRkl6udrYcsfoU4n1ADmiJjm9LnCl1jz+OLShYZ5PJkm3PYS54xa
+mWepYJ7XL8Oq5t888jBxktxILVKLafA0uWUb3AwEPZ0x+aDJmcZ2nfZp6dezn3xg
+sVhlvA0+AK3bVcprTVals7R0idpN2CdX5B62KLpV/i0q7prt9qNS8SLkv++mYFnX
+iusCVNUFmE9za0xgc/ESc6/cMK/lfFPde61/hQ/0fRvjPt8E+1c4tpCWOUh9GDbJ
+5cJFkaoxja8zYUuGpjDCs0/2Fi3dcmmUum0qlbrmp4GzBzka7h4Yzn+e0eQj20fV
+S95xEC0sWIzotMVWofVWIXlEljxDv7FRVbtMllWPhCFiCbepMf1LKyjDJ8WNzvfj
+qLRiuoBWRnvi31wTgfFu5zc/Ep96GaYOYCygTlr2Qd2C15nJuGvonGpEvgOLpC7X
+5WyM1BvNs9X+F0ZpOHHvDeIpTtMmDLpYxI+drft9n6lGm3vc02jrF5RarhWW0gBr
+0NW3lq1a5WFwhkas685vZ67J4NoR02T8/yCdlBJc0Pb/0RQO+FtUdGT93FaxwJq6
+6eNeOvSRinkAtdMGUUwOtlRho+DtkNkXh/werLblCaW4dbZ/wy2zq2k5RyTjiHyG
+BnJUnNGc5Fsc7jW59xlWzdnN4vS7Mtz83q5FWNoGpf0IbPxL/hPLD28ESxsdmst4
+Ew4YeCrAz3PGK+5/Br506ZEzwZtYc7CQidCojpiT4vxLE4aVEfcLQ/ILy0EAKphi
+LfESkclxQvc+qHEWlexv0zdVMSFuir+iV93Q5Q+Ic5kczZwdgRtNsbkakPw7yrpC
+a7bOfRPkx05UZmaPDdVqWzFS4mNDFMLlPt/zgaV/jYSxVbGOCqrp1pkYy5pKDO2+
+ZhtzzjaUEh/QQ1Q6tIqFj79B2PrP9JLr+doZq47G9Adi6jzi/biIJc8Cj7H9ON6K
+k0xVE+GTEXDyQReTnoq0IEe15UJwF8toE1fqnbAu5WEu6ErGEyoOc7yOc8dO9fr8
+TA6II1AS5Ske95mdXlyoM0+8ZwIOb/nts3g3t0sH6nsvWnKmq0eX1qCftZRZR+Q0
+tVSEKMXVGZeC6oXdK0fvi9nYGkW6n11aDd8RWDdxGyNGbd5lClMPpFUOsD56vI5B
+oMJI8aT29y24VfhuaodrwSbsAKibwpewjLH06U4pjlSzzBa724fHh3s9OAo5hFyq
+x90ML0WmSfU+n2dNWptdV1TU6kLdht7Y42zcJPFSPJ+QyfUX3daaq9mzgK6sZp9l
+ZFVQEF3q0TfmFNWg0Sf+rfe+fmhq0fg+VScLN4XhyUCgzWAfX/DFmGY5Kny8ePvo
+AEWD3Sz1mv87ieTJ32876WlCcd8vsfE5F74pkMsNEuEfbhw55fOQY31FLUE+38Sd
+e3H4jDcR5myvJcZPAQx/ebChK31A3opjQpkoGKTs0N6h2+SRcPEk9PpCWJi4jIME
+EFRZEjk7+hbeGdekn0pePRwAAAAAAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent
new file mode 100644
index 0000000000..827bb480c4
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent
@@ -0,0 +1,56 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAukw
+ggLlAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCgggFRMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwPwYJKoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/
+A8oCqKGMQ//YKsV2t/b5GjwwU78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBp
+MGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBU
+ZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEG
+A1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UE
+ChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0B
+AQEFAASCAQBZv8VWQ1fJR5CTJm9YJ4Z1ybeVG7xJpZVrcM4/QZ9q9HK4+AkriLhG
+1mIrBWSUV+vrjisPpnv0NHFd+ueRlLufT3aUpaUyobkADT3uYV5CHzQVzmIQeGCF
+Kk7yiGoe6rWmMU/ejb5Qr/iO+132Rdh65IsvYzauvHv4SnT114AK2YwcLdosQLDI
+sx5OwzL817JNCB1R5RH/GTBkYpmtsLJo5APoLuQ+RuWb9h9VTHZ8VXuSx7tTYcrM
+AafavFUhOctbT3uosixXU1YuToP6l8dmto0zgRuIqSc1bu1d+U1WA0kfe0ershPU
+Cg47Smcnd1EvG/BD0ZVF17bT2KC7rwF0AAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent.eml
new file mode 100644
index 0000000000..6772b1bbbe
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA384.multipart.mismatch-econtent.eml
@@ -0,0 +1,62 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD mismatch-econtent sig.SHA384
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAwcw
+ggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCgggFvMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwOFowPwYJ
+KoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/A8oCqKGMQ//YKsV2t/b5Gjww
+U78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3
+DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASCAQA13iPQULY+PeDW
+IlrVfJZM6pjuYY77nYjqBajZkpQIUGVo/nz9mvdm0yA/BmEaSOdKOIdAgQFzlKxt
+VvNs9BtF+pJlZRcBhJq+eYOjbFBIv4++MyCzQWaBIFnhbER+OcXDH0usq2owCFOc
+IzaeRre8mg/kqGeVyPL2OxoHH4qio3xzJZBR03mnA0kLbxhw3XlF77dVLHlbFUqI
+IVJxYSnX09sITdfJol6sZ81lOwb84g3qxOPw1B6J83zbZlFliAz1t77PLHNBHJzD
+jIqRjXSNaMJ9BO2suhco2C0ngfVrI1pu34duqdlIgl/2RCOc2wgdFLRCBiXkhR7i
+YuZE1YxxAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart
new file mode 100644
index 0000000000..1960da58a8
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart
@@ -0,0 +1,55 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTE5MDcwMzE3MzAwN1oXDTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AOBfD2Up3UqD4VDDHVVPHCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2
+yHYyJt5u0nqNHgQtbNVtM9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/
+0J8FIPmXcCBabNvT3CvFxUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpP
+k9SmiCnfygiyV/YRV8m2xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz
+7yhOV4+m6Ir2NANAxoRVjWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8An
+KiAWjNZFWlGLxrABpeB+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnl
+ZjyzSZMAwT/Az86G54h/sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4
+C+GBQ7yp47Qv2Gqz4jt3t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG
++NqswADXiam2U/tPMm8QYlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07Fo
+Cx/ZTdgg3jCtwOqkmhIGfKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyP
+E/39EQuxRz03W6iWY7rW76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y
++H/evsrVbrIMdTGCAvkwggL1AgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCg
+ggFhMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwTwYJKoZIhvcNAQkEMUIEQIDt
+IdbNbMpZ5GN0mWvd8LPPByCCdkoPPPx0FE23D5CwnagYOMkbOV35hzVG+C0VDZqE
++HLxo4Ki0iR/UTmAz5kweAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG
+9w0BCRACCzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIG
+A1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJKoZIhvcNAQEBBQAEggEALSlFzzCkvVYr
+Gk+i/7YEmZHg5jDewy4bNl/i2FXNaBhu8cgSKTeHVv3/znoDui55TV7mkvzDOiiG
+3UDwU0Gig4Inq0LrMPgJnVzMbsYx0eToAkNznPbpWqQi063X6DzulxvcJxUDfmge
+x30isoIdRSmsF8nJAj76f64OQqKYNBaZ+RLds4PvdIwHsO3tU+ZZMStqhHlXRb0O
+EHxNTUGxC8FsmthIyCUF+Uuy/EKOFEzlD6l/HbxZbg+UZMPF16d73uQdtq6CE5RS
+7HR367IHdBgph2kpGFmT7VnqnofVCmM5vBK6TU1Iyd1J5MSLBccGvi5yhQHWpFy2
+oLOutbnYBQAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.bad.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.bad.eml
new file mode 100644
index 0000000000..c5ec8af01e
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.bad.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD clear-signed sig.SHA512
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCg
+ggF/MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIxMFowTwYJKoZIhvcNAQkEMUIEQIDtIdbNbMpZ5GN0mWvd8LPPByCC
+dkoPPPx0FE23D5CwnagYOMkbOV35hzVG+C0VDZqE+HLxo4Ki0iR/UTmAz5kweAYJ
+KwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+AR4wDQYJKoZIhvcNAQEBBQAEggEAFYqoaQ1WZBzIZr2ETwX9Zj2NU13Cp2HmCfrF
+AagfrDpd3rcuo2q66Eagv7nnEmviP7vV8ouqAHMRr0p3Q+8mS24XbCAKo30mh8/Q
+EaHl6sc42SVwJD9zhAfaMzwqKuN5wUEMYApYtg7xv1aaaWq+mzhC13O5aTjD9168
+tY60cUfGKlcg8qhXbkB4kPEZhsGOESkFfn81sD9Ug1FhQ0apYnjLHvBtwy4Zd2t+
+lmfywK6y8udNRmNDSlinfqF0XLnvmPyMVqZ03QDdCnbCGPzem2Zoq0b4Bv1P8NjK
+KEAW4jBPoy3A2/83gKCyFfllHIX1nlQ12uc3Mb4CqdlxAqeQlgAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.eml
new file mode 100644
index 0000000000..2115292414
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA512
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCg
+ggF/MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIxMFowTwYJKoZIhvcNAQkEMUIEQIDtIdbNbMpZ5GN0mWvd8LPPByCC
+dkoPPPx0FE23D5CwnagYOMkbOV35hzVG+C0VDZqE+HLxo4Ki0iR/UTmAz5kweAYJ
+KwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+AR4wDQYJKoZIhvcNAQEBBQAEggEAFYqoaQ1WZBzIZr2ETwX9Zj2NU13Cp2HmCfrF
+AagfrDpd3rcuo2q66Eagv7nnEmviP7vV8ouqAHMRr0p3Q+8mS24XbCAKo30mh8/Q
+EaHl6sc42SVwJD9zhAfaMzwqKuN5wUEMYApYtg7xv1aaaWq+mzhC13O5aTjD9168
+tY60cUfGKlcg8qhXbkB4kPEZhsGOESkFfn81sD9Ug1FhQ0apYnjLHvBtwy4Zd2t+
+lmfywK6y8udNRmNDSlinfqF0XLnvmPyMVqZ03QDdCnbCGPzem2Zoq0b4Bv1P8NjK
+KEAW4jBPoy3A2/83gKCyFfllHIX1nlQ12uc3Mb4CqdlxAqeQlgAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env
new file mode 100644
index 0000000000..2f09f19f96
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env.eml
new file mode 100644
index 0000000000..32612a28ca
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.env.eml
@@ -0,0 +1,86 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed then enveloped sig.SHA512
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAIf0ggJUZgsEe+JFBXA/FFd3vFU/K7klhDqTf51UD6kui+TV
+3t6UjkTJJDmbEwOJ4Ma8xE10PNP5nBfk2bbNrR0WJB/dwamlcBLcyQmzhSN6sVji
+QIgrZm5X0uKtXnx18+ffAB71KwL4eB9rYr2UMMvQsCIQXU0TMIHJIUltB2SJiFXM
+WwTmEtakzOzxmpBlpWmPGD72/4VOqFMdIVOBGnP4Dg1luFVknHWtcmKpTnte1k3i
+t5zpJf3EuuYDEJbl5XnjyhTAv56x1lm4t0NXvtsKh9hjxdOYLXhajbXEhQ/gXrGh
+myxwQgOpLZZ2oluE+QaKAK3JqyU1feKaHxfwUl0wgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQie4U4HbZUzxIwiexvDB11qCABIIL0KYQgzWfCDawywh7eB9sWnT5
+O0I4g8Y1D7qSa+KY3rgBzurEE1V8gfmPOgRbmlVLfrnlAQrsFKjzozUXllAmIQGs
+UBI6c3+yiW9HgIaoe3RyPJcgDj19Qd/+43dZW+kMC2dAU26jwAb9JG0GW8u5naCG
+QD1PaCFyq1eO6E9imh2any4W/L/HmvrjROGqYSvIr6LMjiPuhJG3tDkcxEFrEgHQ
++gEkAdhr9TmTx8G+vUSbk9owJCf1lz8G1WELRQcdEqpSPkuAnzpjjExpuRsOuB6S
+OcfRGvqlOeB9KzJ9te3/0y/d4iCtY0c38WvAdxXxuxw+NLLRd+9CinHcVK9s9p19
+oPx2k7OndlBeqSjvHui3dznSG3w2njzJi5oxq3Gu3Qnrc63BlaXPfGxIUvfQ+GJ9
++7u3xi6MH3gxvaS4JSITRjf+LuVjv1eqquDWrdz2kd4FUdjk2ie6i0N/tmmxO88I
+8wVhABetEDIBc46A/dHPtuqrsFFYyAgmCq8sdXrFapqB0yzh/LZpLvx6fA4sagIN
+SIHPJUnE1wpt8CXN4TteTyVcq485dmqj7PtqYvIutEqNa5vt5LeSLz27UE6QgkiN
+TT+P65hc6B62jeBl53YxYYF6UjBYugsgQT5FwqjirVuTppv+j/kFf/KI8UKpfwzz
+AfNDGKLgRJiy4vRCst0zyUUkFjq2C14X0M2K0kRk/GhFUkqOT7MuYNMs45qo5mKa
+lOp/uR8COkONGkKx/hHaqHeKYiS5czQsrhnpIJ3sDVO3WPvnB3qcaH52hig2EI8t
+nC2GkMnknfJ/Pka/R2lG083hSp1cCfbQbj4txoO60JpCQ8hRIKhiU00lNNixJxbP
+QrlBa2UfPajVqlnxTzemiS8fEF9cjQ6v9AC9J+YVablxgxFquPX2Nq2aH9WZUcMd
+DVTlERtqWbbZLEiFq5ND4+xaw8Q7GSublrdlWgucHhaRAOG+9Z5aSABKi2BoHiI2
+MTzyEC0rGPuEfDRLWg19xhYAAAAS2WlKgYXQqCKJ9g6CnuPiMT7fhtD5J3hPQLcQ
+EuVrBT4Me73IB3tw+cp9Ih5ntcYtj7NSilb+gj+v5nmE1InbKU25z1eIJM46fYUz
+rS/wXkb8+TiqzBvUVwMxpyWV1jIXrahcftkPN4ewEUq6apxiVLpz2oVUQKgguN2c
+HFqzY1RuByP2Zyv71yHGFXnBIOtJZHp2RnFcEBM9EIfVxd5aCUodqDPNqrsFqPld
+pKdWgFrCHrIkMiqw6vixO2emv23BnNJ20BWa2nFuzDdebpILwO0dcELJHoKh1Ack
+HlwZiIgkIl+ZQgwRnAupDZHG0dTEUemKWGL5vSPQAYSwW/+okDcvHXGqacFF2McC
+WX5doKSML0nU4Nr74cNiH30UkFLi/YaJIFtapVJOSN/3aFW+QMl52XAkteSvPY8Y
+O/Qp81C+alYQEvF1ncKUgByQchTcFdouV/R9B+LIjM0vlqZOM9/+KQ4GZJV4Pw4E
+sAewu+ElGE2nb6Vk1BI9Rxutg1I/84RY2pbXGEBuhHJUwJapsc/kMMO5db52FPNS
+vBtIfrlKTUYGCb5lsQpF0hdYOabhRRpPHkU4UAHV1u+heExgbG8cieeo4/xzf43U
+UvIs/OIi0sEucDOnOzkCAlwiiaRKmnOcYsjMUbIYFHT9kqshO9TxDdEiOCGr88f8
+lwu+K203gVrlWwE5QXCwxb/48E02fcmfJz/R+yh7q4Hus6GIbPGZWIIMIeenRfcU
+/l01SZ05pyJ6HknD/ORKY4KCnheMhIgwu5UqdZdADr4cb3N5ppAtTk97zmwIrPAS
+p7prkRNi4cI8af1P70xaX7wPkAu2SAdaRmXEwrf27fTohwpkev6YPIdHxv20NMEW
+dRoD+3f8hzFP6L8v3bL0TPaMo7LegGse4pZoz4qsI4EtZ1Vb/kZQKGHWuiQppNTw
+BbQIhvyzg58vA0vA3Gz7LrnmZVmHfKbsv2XsMNIGR/JwyVxLyUE1VdLX4r1oXUul
+BpYxhQkBsZaYTBys56BNPIHzPvm+6poiqtNhKXB/TlszulSXqodcW5E/1a+JZ4fH
+RpS0t0FIAgYWwrSmJMXftUIZhse7TmY6Jg8CJs8WrID25cxrXPQeRU4rR9xCJ/xd
+uAAcFQZu+AXlG3in/tGQNT/AZd1zyLVLEaprh7tfrFXsqxIrbhLzE9C14Zbv4LeK
+vWMb5bIzY86LYVJrufGv8jX9h0cd/A6oQZEEkoj96CSEmEYJ/ka40q21C0axvGij
+h/3UKrd8TLBKGn96lS4/o6ZWMsEirPEWBaPZGK2bj0Tp2TbLBxM8EPeFvihInptA
+FGPqn2jUrVogZ+96TXMHsWaVnawX1zLHcS/adb6jswCCykw6kHH93+G0PX3LSRvf
+SvncTMx0banRuRdBGYYf2wdtl2KNaXsJ3WjK3+uRxuwBS5ONtPA3r2bv/R7Qg0be
+NniGFwfwfTri8JwIz52PDain1Gr1NLamcoZQlpOQDqfAikUsjtwpDDFwL8ECpC9J
+/0yptvFIVgGCtcmHHYKgxS1ebigrWVBRB/vXvQG5+auXJ+UI2s53VBADAjoc53yJ
+XzF9Ljo0uedqmdPmN07JGC99FPuy+Tjd5HmloFHA3tfAqPIN2RDZDA++sGOANz71
+SNhDM9qOhkqCieRCzfbE8DcosZoMKCxN0Adm+/mF18RFcrkhRqSapZMvl11Z5Hbh
+zxWJIDQPiyzbQpFWWe8cpDnVzAWC28T/BU2XUounKlHzfL5G2AK7vOy0UQ7uN/vP
+JMjanqbm9FPl+RliECblNUO09k7bR3y72YCMJRilTfv2b1bO3wRjWXSkxgUJ/YSF
+NLgC88u4jk2VynbkqxKtGdus7gDuZvTe5GAa3aR21bP8j++R0ODMNHtKfnmGFLMU
+irFElsJQpej/gtWXDMMsqkMR4WCUm6fkYM4dPbVmxpEbOnWVX4hjqBtiZMKvuCVh
+tW4jDrbvgiZ42f5GpZfE0A0ywzAqEcjXXvx/+8bkC7oYeCFBw8QLbpNcC38hfgcX
+AHYUNC0oPcvRpp5PHmH/tuASnyehv12VaGp/0r2dN+wLbdgCoNaeS98A9wqOsqiA
+JOe/4ka15mqr5aSqoFLrvNrK4jdG1Hi7oBoqRgy43iBInVrw5o/ArgoR2Fj7shCA
+69bZSujkogPGLFzBM+O6K4aa4zZpXkHomkv3nlcDiq64qcfsvP7XXw7icCwm84Z3
+6dNL2Tpq2Og95ZQDIdZ25LO99BMmA6mAOFRHWf1NUsm09fziTEPAXNFKDyf7Oulp
+XctHSkVcwSlRbMXuz464yInztTjRQDFCoviC1sVygdAWVLNoKvcFnDBnH3TZdukH
+O+2KWRP7pyqjiJuAjoII1IdFqa89kS3nSfWoPg6wKtAEAmgkBqtroAiDxy9CaZs/
+a4m26ANPjFdNrXcfRO+Y2S9LrRatWSAlJRoHO/oi1FXNvgs2S+WUWyuiP6ajy7p+
+hrYu75IuNnvczpYdAZ6UWkT9raXk5tQsX//pkhogEq5LucLitBqPhyms6QKeUe5o
+lhvoBYdzr8Zn5QXXWZDpMJIBZQVIuspY7d4puf/4AE8GphaB18rgP6IOWUsa/mus
+enr6XK4e9iOOMu0gYEZSsX+V+61bBWawPkqAGDFdM8fWGEIHy5dbNzXzWQmYFl5n
+gI1wuzS8os0bZ/x/NXR3eWf65o0RR4d/YzS/sgkOGGHB6j/Yifi6YD0wsCB03js7
+bMmB4lfS3DG0whrOPQBoQw95hxgCdUo2U4ImuoTVa0dxM+o628d+17QbooTvqBQa
+JgeigM9qyUORUNd21+5PkeMNL/JH9y1sG0twzjZoJHWjv6Eb9FiemIsUdkExa694
+bMivLnol7cybBM/UCaVp1H74nKtqFogv2cmZJtRNIf0aALtbHISSjLCkY0sKtIFM
+8Nj9a3BrtZkD4dCJotcL/+PYL1R9kM8lVv3D/8jT7nBpp3KB9wGWP+/wKY3CxDAn
+xoBvKCwgI4b71VsUhpb/eiSJDBGRhyBunQArhm8LIAQQnuXpkZPkypYIgw5hsRZZ
+rwAAAAAAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent
new file mode 100644
index 0000000000..8c4c3741c6
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent
@@ -0,0 +1,56 @@
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAvkw
+ggL1AgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCgggFhMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwTwYJKoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPi
+v36orVfLbv7L5+nHNhBNFBFfPHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnyww
+eAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
+YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3Qg
+Q0ECAR4wDQYJKoZIhvcNAQEBBQAEggEAoundvRYJ9fHgAVPoxq5Bsv0R0edWrSDU
+f3tFwwKG8H7hJwMWtGugVOAUfSimlM4foDwJquqTAdY6JVN798wftuWsXT54zkUS
+iB03LpslL7r+gAIa+glmhX6tmLGxIlAa5UyJUHBXoeL3+ze1EoEC+vET5MPh7FnQ
+mC9EP7ZaB1yn8z8N/v5KGzDBC2J9ARZoZ7ckon5cxfW9K69dD+ld184cKeWhPPEl
+5cspbZc2GqUCT0iyKW7M5wuovyOH4BLoSDyh9FFqKeAsyb4g5eQ3cggTZC+rqx3y
+MqxMGaXiLOqxz26zeedkvXI/ZyNeN8zBU9Zo0djCEfCE4GgDw+Kz/wAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent.eml b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent.eml
new file mode 100644
index 0000000000..36e95acc06
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.dsig.SHA512.multipart.mismatch-econtent.eml
@@ -0,0 +1,62 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: BAD mismatch-econtent sig.SHA512
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a FAKE message NOT from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAxcw
+ggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCgggF/MBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIxMFowTwYJ
+KoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPiv36orVfLbv7L5+nHNhBNFBFf
+PHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnywweAYJKwYBBAGCNxAEMWswaTBk
+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91
+bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVz
+dCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJKoZIhvcNAQEB
+BQAEggEAk2Msj1BJMbEibgaUlV2xqXRzDMn0D6EWKr199+U78lHwDe683Ws3Y3ne
+nl4lEDm8plNVupxwrq+vgB2uWoEzyNklXmY4LswVgNqH5xY8/pBGui1zAcpHlAFn
+xoNnTS8Sigydq1TZZ7rauGFaaNBQR/QFWJuxH7P+PhWGCojqi78FKmxx97BeXRUv
+SpGqOg5ggHvPN+7qRQQArmCL/cqF3/GmthZWzXt3JZZLPZ17wpMDSgyZu6xKZ358
+9RvvIlxNKoQdzEt7WwFoIkH4N9q3GvKxjy2litolnJBuHL1L4WmMMyFyW4WC/oPQ
+zbNgtMrWvXSl2GL0T48yDpbOYtS4dgAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..db24e18da0
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml
@@ -0,0 +1,128 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA1 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAEgg8sQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvc2lnbmVkOyBwcm90b2NvbD0i
+YXBwbGljYXRpb24vcGtjczctc2lnbmF0dXJlIjsgbWljYWxnPXNoYS0xOyBib3Vu
+ZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQiCgpU
+aGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2UgaW4gTUlN
+RSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIwNTAyMDMw
+NDA0CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsgbmFtZT1z
+bWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNvbnRlbnQt
+VHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9uOiBh
+dHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlwdGlv
+bjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNEUUVIQTZD
+QU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRBbFZUCk1S
+TXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRH
+RnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRBU0JnTlZC
+QU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFRVUFCSUlC
+QUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhKU2pVcUxO
+ClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1ZsT0pwQkIr
+bktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFUSnFFY0t0
+eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJOMFBrQ3lr
+V1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JGRHI3WGNX
+c3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3VjJJd2J4
+QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JGVXp0NlZL
+L0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZsQXdRQkFn
+UVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBGWW5hTlhj
+U2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJvK21yNG5z
+TUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dTeVBpZlpx
+bkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpibmFKL3pG
+YVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3RVhtOVZh
+VjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFBQUFBPQoK
+Ci0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQKQ29udGVu
+dC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9c21pbWUu
+cDdzCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURp
+c3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN3MKQ29udGVu
+dC1EZXNjcmlwdGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpN
+SUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNBUUV4Q3pBSkJnVXJEZ01DR2dVQU1JQUdD
+U3FHU0liM0RRRUhBUUFBCm9JSURZakNDQTE0d2dnSkdvQU1DQVFJQ0FSNHdEUVlK
+S29aSWh2Y05BUUVMQlFBd1pERUxNQWtHQTFVRUJoTUMKVlZNeEV6QVJCZ05WQkFn
+VENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVTF2ZFc1MFlXbHVJRlpwWlhj
+eApFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVTUJJR0ExVUVBeE1MVGxOVElG
+UmxjM1FnUTBFd0hoY05Nak14Ck1USXhNakExTURNMldoY05Namd4TVRJeE1qQTFN
+RE0yV2pDQmdERUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlYKQkFnVENrTmhiR2xt
+YjNKdWFXRXhGakFVQmdOVkJBY1REVTF2ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05W
+QkFvVApDVUpQUjFWVElFNVRVekVnTUI0R0NTcUdTSWIzRFFFSkFSWVJRV3hwWTJW
+QVpYaGhiWEJzWlM1amIyMHhEakFNCkJnTlZCQU1UQlVGc2FXTmxNSUlCSWpBTkJn
+a3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQW5ycGgKRXcvSUhsZU9I
+bW5yRmpxWVdOZndybmFrbjlWSHZKVmlDcnlPMEdPT3BTVUZMYlNUQWwwNTZhbmg2
+OFNEdm96dQpXRitiSzJ1anN0S1ZYYnk1Ulg4U1ljdWMxdjF6MFdNR0RyMVM1ZlEv
+ZFBFbXp1aUxXNlNzNENlTGZtaytGM3FhClA2WUdhRUxzWm9FczZmVWNsM0ZQTU1m
+ODFLZFl5Rmx5M1ZqMnRIVkZZOFJzSldvRDlRenJXcGNXa3h5TDVSRDgKdUR0akdP
+dCtqQ1ZaMUJKN08xQS9Kb3hpazdZWnRrMzV2WjFvdllvQVE3UUJ6VGg3Z3FocWlp
+RDltRVlyd2tDcQo0NlNDcyt5WGVhZUYyTTZ0bERXVnJocmVNczJ6c3dtbEZMQ1pm
+T3IrSUF3c1NnZXo3cjZzNTZ2TFJRZTVPd1dnClBSUnlRMzNRSWJzZDdjbUwzUUlE
+QVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ0hSSy9TVGZmd3VxSTgKbEdJ
+TTRaZFJETmFYeTVQdWttYlRmNGw4RXVhWGpLSld3eTVsYS9UZE94cHFnNXRiZjdr
+MUFXeUE4OENWS2pRbgpNbnFYcWJlRC9xYVVVRFV6R0kvMkx0eE5aemNZNUJhVTVQ
+YXV2SGxsRGRxNEhwanVVZklvdXB2ajcxWGRzWHZ2Cm5WYkRZZ09aWERiV1JLdWJ5
+M2J5WWVuaHVnVExzZDI3QTQvVmtlTHU5M3JscEphWFM2RWlleEVxUTlLTmZFZzkK
+WmF6SzNUWWxBbFFETXAvSXliNzJXVkNKbUxKMVBTc2JES2xwaU1PekxUTDhKeHNJ
+UkJJVFdMa0xFYjZaakxCYgp2Q2dhOUtnTCtXbnB6MDZrZXFVYnRPZzZCQmpCT2lY
+dzJQWTVhUFZOUkNaZEx1anV4andxTU43SlpuRG1RejBECkg5MjBicDIvTVlJQzV6
+Q0NBdU1DQVFFd2FUQmtNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1Ey
+RnMKYVdadmNtNXBZVEVXTUJRR0ExVUVCeE1OVFc5MWJuUmhhVzRnVm1sbGR6RVNN
+QkFHQTFVRUNoTUpRazlIVlZNZwpUbE5UTVJRd0VnWURWUVFERXd0T1UxTWdWR1Z6
+ZENCRFFRSUJIakFKQmdVckRnTUNHZ1VBb0lJQlV6QVlCZ2txCmhraUc5dzBCQ1FN
+eEN3WUpLb1pJaHZjTkFRY0JNQndHQ1NxR1NJYjNEUUVKQlRFUEZ3MHlNekV4TWpF
+eU1EVXkKTURWYU1DTUdDU3FHU0liM0RRRUpCREVXQkJTZWsxVk9GU3Bab2NRODRk
+QUxxbWpuL2FMMmRqQjRCZ2tyQmdFRQpBWUkzRUFReGF6QnBNR1F4Q3pBSkJnTlZC
+QVlUQWxWVE1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3CkZBWURWUVFI
+RXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3RUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14
+RkRBU0JnTlYKQkFNVEMwNVRVeUJVWlhOMElFTkJBZ0VlTUhvR0N5cUdTSWIzRFFF
+SkVBSUxNV3VnYVRCa01Rc3dDUVlEVlFRRwpFd0pWVXpFVE1CRUdBMVVFQ0JNS1Ey
+RnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5SaGFXNGdWbWxsCmR6RVNN
+QkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZRUURFd3RPVTFNZ1ZHVnpk
+Q0JEUVFJQkhqQU4KQmdrcWhraUc5dzBCQVFFRkFBU0NBUUF1V3JFVnhLTytkTCtv
+Z1NrYlV1L3ZaYUtiR1pQbUpsNGhybHFzN3EzcAp2NUpvQm0wZkFiSTZaRjJUd2Fa
+WHQzUlg3RzJkK2ppT3QzSDFlN1grWU5RWWo4WGtvR3o2TkdJck5yKzI0bllRClND
+VllEY2ZVZEg0cVdSZHgzYnBSVEd3RDBsMDY3eHRsUlAwdGQySm1tTStoVUIyRzNF
+Y2NURmxjenEzbHhrYjgKb3hGMWRLZUF3RUpSWmlkSnZ1V3hNMHlIZjVWdHVVSVRk
+dWhVM2ViNU1GM2tiV01qVmROaEs2ZGpmbC9vWE5YMQpWSUJGWit2RmpxZGpuNEZw
+WElWN3I5UlQrWlRBRlRYRWFzK0Y4eFVjdDMrRjdPcVptU1Uwdm04dXl3TFZvTldz
+ClVSNmh1ZGl6V3RUQi9CTEdlNkNnL21MSDZibVlOU2ZucHRkMDJFNjQvL21NQUFB
+QUFBQUEKLS0tLS0tLS0tLS0tLS1tczAzMDkwMzAyMDkwMjAyMDUwMjAzMDQwNC0t
+CgoAAAAAAACgggNfMIIDWzCCAkOgAwIBAgIBMjANBgkqhkiG9w0BAQsFADBkMQsw
+CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRh
+aW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBD
+QTAeFw0yMzExMjEyMDUwNDNaFw0yODExMjEyMDUwNDNaMH4xCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxHzAdBgkqhkiG9w0BCQEWEERhdmVAZXhhbXBsZS5j
+b20xDTALBgNVBAMTBERhdmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDIS6Iyqw0hewI8Lkj+nQEhnBQfflgDKrW4nFcNZ5vzU92Nv553ZIFDgNMNyMoz
+tg4irXkzl2Z9VX1xea01iA+OnWCo9w5sTYqqPYKJktXbit5Dr6A5Zb7//kxIB5DX
+irVlYK3coKS7084va31SdSYke/RgEnsNWoxn48WQddddAhMQ8BUQJzaQmpx6i0Kg
+ns5NlA4NqsaFl1+f32uzOSPOy70bx9UFprvVx2/04LXNNQBg0DDlrbHNRYh3SJZs
+rFbSvVTazIYzvOiWJo/QQcOVNdHwqFTGy8ZCtUn8SwrIuuvARpOYnLumFFsk1fP9
+HMHcTqSWkl+1f4dC5IBQW7INAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGNN2cbQ
+c1VjlHeTkloseW93MTh2lEi/zKlS0byKKEFBfSlPgp0jtQZravUxqvt8cCNpn6KC
+oJIJiBHozi4vptcfvvNXQJv7Ic8v8QtG3Tqq9A2f+PAtwInnucGO2JSYfuIc6yfd
+8+0347H8CPDekAcj5uBHSRiq11r0Uv3UkltluSa/XgVI0vNFPOMRgXyRR1tWAv4w
+L4YIgQR6wPVJ9LdB678AhnElq1qOY9QlK42W01pVinvk5Y2AWaA/E3yAobtE13ot
+l4GZnjqNLZNvnZ0CqmCuF85caT4fPNZNfA5NhnFFAGYdMkk2FPgZtHt8YFaBySo7
+3VNFEsDvha75rckxggLnMIIC4wIBATBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMAkGBSsOAwIaBQCgggFT
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIwNlowIwYJKoZIhvcNAQkEMRYEFP3dSYzjQbYGeZxH2iS6L5cXkp9gMHgG
+CSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
+aWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEU
+MBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEyMA0GCSqGSIb3DQEBAQUABIIBABK5c2p0y91pkb5hfuJpwCDOQ4rZdMBHatfx
+Jr6V3T3WrrqFY9bEy58GnAidUcnEzwBfCNgJIdL1X2BxJqiwowx4HEo6vUQ+11Dv
+yHIoDpKHqf8BvM8blTesKBtrrt8sXgBIitxwS8j3YYJkMeYX2H88giOV2Kquzncy
+cnYLJVlTgDxJjAQdEJ+HZySQTpBnlzQyceyOiOktvAX0i6H3tw24YMyb3ma4576O
+gU//KmIbjKhVrIO29OvDdX0qPvgkehq3YepWlNMEkpr+tfQn9acjzvqQnQPfKtkn
+Llrv4BtyNEwBcTVbfplc0dfPjiAF11IsCMvhzquMSQVXYSEJb5AAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.eml
new file mode 100644
index 0000000000..4358503045
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA1.multipart.eml
@@ -0,0 +1,75 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA1
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALAilHYmQY9S/H2MP0JZkHLxqlBshzmkaVeCm8xrhJSjUqLN
+Y2dob849A8/sYystVGkuravuVAv5HQGTmQ0ntE7VlOJpBB+nKsRWUAZ8kr4HhAJl
+G2GirOwKbQnNrL/feB+AotQTJqEcKtxUsSfk9tMBp2EIh/g0WaV+jOcIjERZcnU8
+EKp/zBN0PkCykWP/3zJKaVcPaH/4ylrRXebcCblTZOhK9bLuCsyeOrFDr7XcWszL
+V1+bSS5VtnpO3Mg58720MyzNQ/rHHHSMl7LoG7V2IwbxBsM+RpBOXateTt+70tmh
+Yi+IpTYXkZqWxwnfwcCBFUzt6VK/BFD3QY4SyNEwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQmIgaQceqX0qrIHhaZ0lFoaCABIGgiPZeDBvazh0FYnaNXcSbP9kV
+Aw632l9ziRyCF7WgtODI7cf2ZNUZ/QBPYRo+mr4nsMHlFqDV7W4Z5bZiEcSmn017
+E44Mvx7swnZwhO7CWSyPifZqnHDjzFMPeyGvcsKcMlEBBU2TCF6WpszoJ6adrKHM
+bnaJ/zFaReR/mZEIXtPoYkEfG2aqjkBcACalmDm7ww2kHjEb7EXm9VaV5SPiUgQQ
+yzIt+J5gqf4Wi2CjUNwoFQAAAAAAAAAAAAA=
+
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAM
+BgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrph
+Ew/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozu
+WF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qa
+P6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8
+uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq
+46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWg
+PRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8
+lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQn
+MnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvv
+nVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9
+ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBb
+vCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0D
+H920bp2/MYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkq
+hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUy
+MDVaMCMGCSqGSIb3DQEJBDEWBBSek1VOFSpZocQ84dALqmjn/aL2djB4BgkrBgEE
+AYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQG
+EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
+dzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAN
+BgkqhkiG9w0BAQEFAASCAQAuWrEVxKO+dL+ogSkbUu/vZaKbGZPmJl4hrlqs7q3p
+v5JoBm0fAbI6ZF2TwaZXt3RX7G2d+jiOt3H1e7X+YNQYj8XkoGz6NGIrNr+24nYQ
+SCVYDcfUdH4qWRdx3bpRTGwD0l067xtlRP0td2JmmM+hUB2G3EccTFlczq3lxkb8
+oxF1dKeAwEJRZidJvuWxM0yHf5VtuUITduhU3eb5MF3kbWMjVdNhK6djfl/oXNX1
+VIBFZ+vFjqdjn4FpXIV7r9RT+ZTAFTXEas+F8xUct3+F7OqZmSU0vm8uywLVoNWs
+UR6hudizWtTB/BLGe6Cg/mLH6bmYNSfnptd02E64//mMAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..980c55df08
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml
@@ -0,0 +1,129 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA256 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABIIPS0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtMjU2
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKCi0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQK
+Q29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9
+c21pbWUucDdzCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250
+ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN3MK
+Q29udGVudC1EZXNjcmlwdGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0
+dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFN
+RUFnRUZBRENBQmdrcWhraUc5dzBCCkJ3RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFn
+RUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRN
+Uk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVk
+R0ZwYmlCVwphV1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05W
+QkFNVEMwNVRVeUJVWlhOMElFTkJNQjRYCkRUSXpNVEV5TVRJd05UQXpObG9YRFRJ
+NE1URXlNVEl3TlRBek5sb3dnWUF4Q3pBSkJnTlZCQVlUQWxWVE1STXcKRVFZRFZR
+UUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FX
+VjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZVeUJPVTFNeElEQWVCZ2txaGtpRzl3MEJD
+UUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZMjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xq
+WlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2
+NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMy
+MGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5
+RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JPQW5pMzVwClBoZDZtaittQm1oQzdHYUJM
+T24xSEpkeFR6REgvTlNuV01oWmN0MVk5clIxUldQRWJDVnFBL1VNNjFxWEZwTWMK
+aStVUS9MZzdZeGpyZm93bFdkUVNlenRRUHlhTVlwTzJHYlpOK2IyZGFMMktBRU8w
+QWMwNGU0S29hb29nL1poRwpLOEpBcXVPa2dyUHNsM21uaGRqT3JaUTFsYTRhM2pM
+TnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBD
+RzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBr
+MzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAz
+VHNhYW9PYlczKzVOUUZzZ1BQQQpsU28wSnpKNmw2bTNnLzZtbEZBMU14aVA5aTdj
+VFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZN2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJ
+RG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdIZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5z
+UktrUFMKalh4SVBXV3N5dDAySlFKVUF6S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZ
+akRzeTB5L0NjYkNFUVNFMWk1Q3hHKwptWXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxH
+N1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1YUzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9k
+dEc2ZHZ6R0NBdmN3Z2dMekFnRUJNR2t3WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFS
+QmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVds
+dUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VKUApSMVZUSUU1VFV6RVVNQklHQTFVRUF4
+TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RRWUpZSVpJQVdVREJBSUJCUUNnCmdnRmZN
+QmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Foa2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05B
+UWtGTVE4WERUSXoKTVRFeU1USXdOVEl3TjFvd0x3WUpLb1pJaHZjTkFRa0VNU0lF
+SUVYdEhLaGVVbDA4OXVOUDRrMDlCb2FMSzgrbApueG1xYXYxRGl0RzVGTkxMTUhn
+R0NTc0dBUVFCZ2pjUUJERnJNR2t3WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSCkJn
+TlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlG
+WnBaWGN4RWpBUUJnTlYKQkFvVENVSlBSMVZUSUU1VFV6RVVNQklHQTFVRUF4TUxU
+bE5USUZSbGMzUWdRMEVDQVI0d2VnWUxLb1pJaHZjTgpBUWtRQWdzeGE2QnBNR1F4
+Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3
+CkZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3RUFZRFZRUUtFd2xDVDBk
+VlV5Qk9VMU14RkRBU0JnTlYKQkFNVEMwNVRVeUJVWlhOMElFTkJBZ0VlTUEwR0NT
+cUdTSWIzRFFFQkFRVUFCSUlCQURhUWVnNWRKbkZFUUxrVAo3UmxDNVEycmZrYjJE
+cVVvRkthS2s2N0RQQ3ljSFkrK1FRRXpYUTRsMDYwVUd1SlBsUFhRd2svWkxZSGkx
+K3NKCnNHd2J1TmhEa2t6dXNUd3dVa2UxMUJHRnJHbGZ4K3dhTDdTdjlTV1RMR1JG
+aFd2QXhibFUyc29LcFRGa29mUHUKM0lGU1ltQkQxK1ZrbTlmd1p4NEQvQ2F5aVVt
+Z1I4UFVZR2IwWE9IS0hOY3A0QWVLQlp3eEx3MFBPZmZwc3ZQQQpNandMMktHYXVD
+L1RFM09WMWErRERxeHl6SHZ0dEN6TVVNM1VEcjR2OG1NOXNCQ3M2TkhPa3M5VmFi
+VkthUFJNCmhKck8wSTR1bWw4NnZDcWI0dEFRVzNCOUdHUkRSWm0zTHRnNDQwS0ZN
+SHJhM0ZhQnBuNUo5YWlIbC9rdUFMS3IKUkFjSWpOVUFBQUFBQUFBPQotLS0tLS0t
+LS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIwNTAyMDMwNDA0LS0KCgAAAAAAAKCCA18w
+ggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIw
+NTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTENMAsGA1UEAxME
+RGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhLojKrDSF7Ajwu
+SP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKteTOXZn1VfXF5
+rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVgrdygpLvTzi9r
+fVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2UDg2qxoWXX5/f
+a7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9VNrMhjO86JYm
+j9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxOpJaSX7V/h0Lk
+gFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOUd5OSWix5b3cx
+OHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmIEejOLi+m1x++
+81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7TfjsfwI8N6QByPm
+4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiBBHrA9Un0t0Hr
+vwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZmeOo0tk2+dnQKq
+YK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0USwO+FrvmtyTGC
+AvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIG
+A1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAIBBQCgggFfMBgGCSqGSIb3
+DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwOFow
+LwYJKoZIhvcNAQkEMSIEILvHCcJ1Dy4qQ58snaPXylskaZ4duo6RSfw0ipdvOr/p
+MHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlm
+b3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5T
+UzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQAgsxa6BpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEyMA0GCSqGSIb3DQEBAQUABIIBAEZ74HFW+r0S42oHw32z4V3TERM1PyrE
+X84SW7sbTIjodeD53SnNdBaduBcu2JfHxDu9MbrNRkk5Y1UZoIeSlZ4K1aJ9Geou
+ERGSKyqOVpn4SrAitW+S8un6wwqZ7NiMug+cMFxe3QxBOFrRzwb6n3ABqvLaWeKF
+MGXLIdC6OHFgUJsAwwGNE2PzrilHkF25kdAYWc0JYL24JHSDWADizZOuZddSF2xy
+185JIZEuLvDD60gpnhPY5M3GQry+xOQcQQR/CDkARDoVw4VRKrGDVFY9bQGUdqnb
+Mu9t+7pn/qRaINCWIFQWfuUV5RmaWyEXpJDSaLWkgnJ8iQhUrLKJkkcAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.eml
new file mode 100644
index 0000000000..58af108c00
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA256.multipart.eml
@@ -0,0 +1,76 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA256
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALAilHYmQY9S/H2MP0JZkHLxqlBshzmkaVeCm8xrhJSjUqLN
+Y2dob849A8/sYystVGkuravuVAv5HQGTmQ0ntE7VlOJpBB+nKsRWUAZ8kr4HhAJl
+G2GirOwKbQnNrL/feB+AotQTJqEcKtxUsSfk9tMBp2EIh/g0WaV+jOcIjERZcnU8
+EKp/zBN0PkCykWP/3zJKaVcPaH/4ylrRXebcCblTZOhK9bLuCsyeOrFDr7XcWszL
+V1+bSS5VtnpO3Mg58720MyzNQ/rHHHSMl7LoG7V2IwbxBsM+RpBOXateTt+70tmh
+Yi+IpTYXkZqWxwnfwcCBFUzt6VK/BFD3QY4SyNEwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQmIgaQceqX0qrIHhaZ0lFoaCABIGgiPZeDBvazh0FYnaNXcSbP9kV
+Aw632l9ziRyCF7WgtODI7cf2ZNUZ/QBPYRo+mr4nsMHlFqDV7W4Z5bZiEcSmn017
+E44Mvx7swnZwhO7CWSyPifZqnHDjzFMPeyGvcsKcMlEBBU2TCF6WpszoJ6adrKHM
+bnaJ/zFaReR/mZEIXtPoYkEfG2aqjkBcACalmDm7ww2kHjEb7EXm9VaV5SPiUgQQ
+yzIt+J5gqf4Wi2CjUNwoFQAAAAAAAAAAAAA=
+
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwN1owLwYJKoZIhvcNAQkEMSIEIEXtHKheUl089uNP4k09BoaLK8+l
+nxmqav1DitG5FNLLMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBADaQeg5dJnFEQLkT
+7RlC5Q2rfkb2DqUoFKaKk67DPCycHY++QQEzXQ4l060UGuJPlPXQwk/ZLYHi1+sJ
+sGwbuNhDkkzusTwwUke11BGFrGlfx+waL7Sv9SWTLGRFhWvAxblU2soKpTFkofPu
+3IFSYmBD1+Vkm9fwZx4D/CayiUmgR8PUYGb0XOHKHNcp4AeKBZwxLw0POffpsvPA
+MjwL2KGauC/TE3OV1a+DDqxyzHvttCzMUM3UDr4v8mM9sBCs6NHOks9VabVKaPRM
+hJrO0I4uml86vCqb4tAQW3B9GGRDRZm3Ltg440KFMHra3FaBpn5J9aiHl/kuALKr
+RAcIjNUAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..90705d69e8
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml
@@ -0,0 +1,130 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA384 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABIIPX0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtMzg0
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKCi0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQK
+Q29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9
+c21pbWUucDdzCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250
+ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN3MK
+Q29udGVudC1EZXNjcmlwdGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0
+dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFN
+RUFnSUZBRENBQmdrcWhraUc5dzBCCkJ3RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFn
+RUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRN
+Uk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVk
+R0ZwYmlCVwphV1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05W
+QkFNVEMwNVRVeUJVWlhOMElFTkJNQjRYCkRUSXpNVEV5TVRJd05UQXpObG9YRFRJ
+NE1URXlNVEl3TlRBek5sb3dnWUF4Q3pBSkJnTlZCQVlUQWxWVE1STXcKRVFZRFZR
+UUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FX
+VjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZVeUJPVTFNeElEQWVCZ2txaGtpRzl3MEJD
+UUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZMjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xq
+WlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2
+NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMy
+MGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5
+RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JPQW5pMzVwClBoZDZtaittQm1oQzdHYUJM
+T24xSEpkeFR6REgvTlNuV01oWmN0MVk5clIxUldQRWJDVnFBL1VNNjFxWEZwTWMK
+aStVUS9MZzdZeGpyZm93bFdkUVNlenRRUHlhTVlwTzJHYlpOK2IyZGFMMktBRU8w
+QWMwNGU0S29hb29nL1poRwpLOEpBcXVPa2dyUHNsM21uaGRqT3JaUTFsYTRhM2pM
+TnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBD
+RzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBr
+MzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAz
+VHNhYW9PYlczKzVOUUZzZ1BQQQpsU28wSnpKNmw2bTNnLzZtbEZBMU14aVA5aTdj
+VFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZN2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJ
+RG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdIZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5z
+UktrUFMKalh4SVBXV3N5dDAySlFKVUF6S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZ
+akRzeTB5L0NjYkNFUVNFMWk1Q3hHKwptWXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxH
+N1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1YUzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9k
+dEc2ZHZ6R0NBd2N3Z2dNREFnRUJNR2t3WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFS
+QmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVds
+dUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VKUApSMVZUSUU1VFV6RVVNQklHQTFVRUF4
+TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RRWUpZSVpJQVdVREJBSUNCUUNnCmdnRnZN
+QmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Foa2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05B
+UWtGTVE4WERUSXoKTVRFeU1USXdOVEl3T1Zvd1B3WUpLb1pJaHZjTkFRa0VNVElF
+TUhqQVY3TFBPMmV6bzJ6OWM4K1VML3pRanJucQp6U0c2WHhuUGprQ1lCUHRwWUdE
+RGludkduZE1WK09kQ012b3l3ekI0QmdrckJnRUVBWUkzRUFReGF6QnBNR1F4CkN6
+QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZB
+WURWUVFIRXcxTmIzVnUKZEdGcGJpQldhV1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZV
+eUJPVTFNeEZEQVNCZ05WQkFNVEMwNVRVeUJVWlhOMApJRU5CQWdFZU1Ib0dDeXFH
+U0liM0RRRUpFQUlMTVd1Z2FUQmtNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVF
+CkNCTUtRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCeE1OVFc5MWJuUmhhVzRnVm1s
+bGR6RVNNQkFHQTFVRUNoTUoKUWs5SFZWTWdUbE5UTVJRd0VnWURWUVFERXd0T1Ux
+TWdWR1Z6ZENCRFFRSUJIakFOQmdrcWhraUc5dzBCQVFFRgpBQVNDQVFBNjZCS084
+REFsdlNnaWEzanR5YWx1VVNSREZTWk14TnBqMUozZ3NYMnRHUllkeURXQW5XRDdl
+d3ZoCjE3Mm1xVkVWQlIrdXdjRWY4N3VxMWk3MGF2OGNma1hWL2ZlTURXejhKaWJT
+a3E3ZmxPelZjMEdSWHAwYzFGcDUKS21FRHhYc29UZ1NhMlJHK0g0bGNrblViRUJ5
+ZytORDY5R1JPaDR2NkMwS1pHaEN4Yk5rR0VKdTJTbFVVdCtCbQpwOHB5VGp1a1V1
+dHd1bUgxd0I0U3FDR2VaZU9oZmZqaWFmODd2VlZ1dHpBdzV2Z1ZMcDd4aHhIZG5S
+TmdndktjCnFaSDR2VXk3ekplc2ZKU3IxVHA0NjVKbzR1cWlSRU4zRCtmNjlOU2p1
+b0VwSEpVaEtZcU5iclNSdXcwK2hqOXMKTDZPazVmbk0rOXl6ZEpHZ2RoZ2FyNTIv
+Z2JyWEFBQUFBQUFBCi0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIw
+MzA0MDQtLQoKAAAAAAAAoIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQEL
+BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
+DU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNT
+IFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzESMBAGA1UEChMJQk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4
+YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SB
+Q4DTDcjKM7YOIq15M5dmfVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+
+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2
+kJqceotCoJ7OTZQODarGhZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2x
+zUWId0iWbKxW0r1U2syGM7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7
+phRbJNXz/RzB3E6klpJftX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7
+fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiU
+mH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8
+kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8
+gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7
+fGBWgckqO91TRRLA74Wu+a3JMYIDBzCCAwMCAQEwaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBglghkgB
+ZQMEAgIFAKCCAW8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B
+CQUxDxcNMjMxMTIxMjA1MjA5WjA/BgkqhkiG9w0BCQQxMgQwf1+TPmep+x3WlUqO
+w8KbxOWv7gxL9a4POWJ7+eC/C20k8A6nVWFNaCXr79aYEztcMHgGCSsGAQQBgjcQ
+BDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV
+BAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxML
+TlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMA0GCSqG
+SIb3DQEBAQUABIIBAH4vrkxGgaQZNW8SMJz5KuK0hYR4hcnALuJlxeza+tDszjUY
+nHZb6Jz/yJSbaioQ2Wa/lM9GkbDSyzlDJkSdJTaC6iwYuU+OrYz82KUdVq2xK5YG
+bI4anOkujUFw2P45EuXwkzOgttit9GD31WDBcEwr3UrH2Dlfw4QBBanFr763nLAf
+vDhxrbKRPChGebM580lE+zg5gfzRMboCQKiTbt1RIp8iq6ZZJiBQcpK9RYmhYAEd
+8yrjHcn7KYiBZ7HEE5b5NJR7eSZHZCXgpBMb5LSls3MaizgD2zDznwp6Wwz/2gRA
+UseigMEXTawzy9CPnsWVshHXdvJ1xSmY8SNABRQAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.eml
new file mode 100644
index 0000000000..e8c28de2cd
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA384.multipart.eml
@@ -0,0 +1,76 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA384
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALAilHYmQY9S/H2MP0JZkHLxqlBshzmkaVeCm8xrhJSjUqLN
+Y2dob849A8/sYystVGkuravuVAv5HQGTmQ0ntE7VlOJpBB+nKsRWUAZ8kr4HhAJl
+G2GirOwKbQnNrL/feB+AotQTJqEcKtxUsSfk9tMBp2EIh/g0WaV+jOcIjERZcnU8
+EKp/zBN0PkCykWP/3zJKaVcPaH/4ylrRXebcCblTZOhK9bLuCsyeOrFDr7XcWszL
+V1+bSS5VtnpO3Mg58720MyzNQ/rHHHSMl7LoG7V2IwbxBsM+RpBOXateTt+70tmh
+Yi+IpTYXkZqWxwnfwcCBFUzt6VK/BFD3QY4SyNEwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQmIgaQceqX0qrIHhaZ0lFoaCABIGgiPZeDBvazh0FYnaNXcSbP9kV
+Aw632l9ziRyCF7WgtODI7cf2ZNUZ/QBPYRo+mr4nsMHlFqDV7W4Z5bZiEcSmn017
+E44Mvx7swnZwhO7CWSyPifZqnHDjzFMPeyGvcsKcMlEBBU2TCF6WpszoJ6adrKHM
+bnaJ/zFaReR/mZEIXtPoYkEfG2aqjkBcACalmDm7ww2kHjEb7EXm9VaV5SPiUgQQ
+yzIt+J5gqf4Wi2CjUNwoFQAAAAAAAAAAAAA=
+
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCg
+ggFvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwOVowPwYJKoZIhvcNAQkEMTIEMHjAV7LPO2ezo2z9c8+UL/zQjrnq
+zSG6XxnPjkCYBPtpYGDDinvGndMV+OdCMvoywzB4BgkrBgEEAYI3EAQxazBpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEF
+AASCAQA66BKO8DAlvSgia3jtyaluUSRDFSZMxNpj1J3gsX2tGRYdyDWAnWD7ewvh
+172mqVEVBR+uwcEf87uq1i70av8cfkXV/feMDWz8JibSkq7flOzVc0GRXp0c1Fp5
+KmEDxXsoTgSa2RG+H4lcknUbEByg+ND69GROh4v6C0KZGhCxbNkGEJu2SlUUt+Bm
+p8pyTjukUutwumH1wB4SqCGeZeOhffjiaf87vVVutzAw5vgVLp7xhxHdnRNggvKc
+qZH4vUy7zJesfJSr1Tp465Jo4uqiREN3D+f69NSjuoEpHJUhKYqNbrSRuw0+hj9s
+L6Ok5fnM+9yzdJGgdhgar52/gbrXAAAAAAAA
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..8396145575
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml
@@ -0,0 +1,131 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA512 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABIIPd0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtNTEy
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKCi0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQK
+Q29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9
+c21pbWUucDdzCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250
+ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN3MK
+Q29udGVudC1EZXNjcmlwdGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0
+dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFN
+RUFnTUZBRENBQmdrcWhraUc5dzBCCkJ3RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFn
+RUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRN
+Uk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVk
+R0ZwYmlCVwphV1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05W
+QkFNVEMwNVRVeUJVWlhOMElFTkJNQjRYCkRUSXpNVEV5TVRJd05UQXpObG9YRFRJ
+NE1URXlNVEl3TlRBek5sb3dnWUF4Q3pBSkJnTlZCQVlUQWxWVE1STXcKRVFZRFZR
+UUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FX
+VjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZVeUJPVTFNeElEQWVCZ2txaGtpRzl3MEJD
+UUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZMjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xq
+WlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2
+NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMy
+MGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5
+RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JPQW5pMzVwClBoZDZtaittQm1oQzdHYUJM
+T24xSEpkeFR6REgvTlNuV01oWmN0MVk5clIxUldQRWJDVnFBL1VNNjFxWEZwTWMK
+aStVUS9MZzdZeGpyZm93bFdkUVNlenRRUHlhTVlwTzJHYlpOK2IyZGFMMktBRU8w
+QWMwNGU0S29hb29nL1poRwpLOEpBcXVPa2dyUHNsM21uaGRqT3JaUTFsYTRhM2pM
+TnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBD
+RzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBr
+MzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAz
+VHNhYW9PYlczKzVOUUZzZ1BQQQpsU28wSnpKNmw2bTNnLzZtbEZBMU14aVA5aTdj
+VFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZN2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJ
+RG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdIZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5z
+UktrUFMKalh4SVBXV3N5dDAySlFKVUF6S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZ
+akRzeTB5L0NjYkNFUVNFMWk1Q3hHKwptWXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxH
+N1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1YUzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9k
+dEc2ZHZ6R0NBeGN3Z2dNVEFnRUJNR2t3WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFS
+QmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVds
+dUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VKUApSMVZUSUU1VFV6RVVNQklHQTFVRUF4
+TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RRWUpZSVpJQVdVREJBSURCUUNnCmdnRi9N
+QmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Foa2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05B
+UWtGTVE4WERUSXoKTVRFeU1USXdOVEl4TVZvd1R3WUpLb1pJaHZjTkFRa0VNVUlF
+UU03LzRNclNXWU9UQmZEUmxrWEhOVkd1eCtNTwpXdG5lV0lGYTkrS3JsbXBPQjRs
+NTJvTzBuU051TkJIcng0Q082eXRVYjdhbTdGaVZWZlA2MktwTmxTUXdlQVlKCkt3
+WUJCQUdDTnhBRU1Xc3dhVEJrTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNC
+TUtRMkZzYVdadmNtNXAKWVRFV01CUUdBMVVFQnhNTlRXOTFiblJoYVc0Z1ZtbGxk
+ekVTTUJBR0ExVUVDaE1KUWs5SFZWTWdUbE5UTVJRdwpFZ1lEVlFRREV3dE9VMU1n
+VkdWemRDQkRRUUlCSGpCNkJnc3Foa2lHOXcwQkNSQUNDekZyb0drd1pERUxNQWtH
+CkExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdO
+VkJBY1REVTF2ZFc1MFlXbHUKSUZacFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElF
+NVRVekVVTUJJR0ExVUVBeE1MVGxOVElGUmxjM1FnUTBFQwpBUjR3RFFZSktvWklo
+dmNOQVFFQkJRQUVnZ0VBbllySGZ2Ym1YQnBIb0w1dEM2SkxubFFPV3VrRThGNkxN
+MDZPCld3eHF1NWlnU25maTZ2RndoV3RGVE82VGNqVjV4RzRSYjgrdU51aXU2UFNv
+TVpTWjROV1l3Q29lNmpOY0JubUEKZXlDN2tybVBpNjhoUjdVbmNqTXVsTjlNTUNI
+ckh4QXl2TDAyV0NQdndMZFdqZWtYTTBJVFAvNjJXR2ZmalVEQQpROVpSQlFzRita
+QXgwWTcwRGpvMmppekN4MDJWc2w1Y3hkQVBscFR0aGRFY09jaTY1N1pCWktSN2dI
+U0NNU3JoClhMNUtBZ1ozNzFZUWxEUWx3ZXFmMXhVQTFaMGxFbzQrNFZsb24yY1NI
+OGQrdUJyRmk2aU51d3pVNjAwMkRJWUoKcDA4SzJrU0RtYmgvUDJDTDZkam8rSDkr
+b0hNaFB4dlNKZVFub1VtR2dFRHpLWkFTSmdBQUFBQUFBQT09Ci0tLS0tLS0tLS0t
+LS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQtLQoKAAAAAAAAoIIDXzCCA1sw
+ggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQz
+WhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZl
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0B
+IZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dmfVV9cXmtNYgP
+jp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUm
+JHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarGhZdfn99rszkj
+zsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syGM7zoliaP0EHD
+lTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJftX+HQuSAUFuy
+DQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRI
+v8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb
++yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kY
+qtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZx
+JatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfO
+XGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu+a3JMYIDFzCC
+AxMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
+A1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQD
+EwtOU1MgVGVzdCBDQQIBMjANBglghkgBZQMEAgMFAKCCAX8wGAYJKoZIhvcNAQkD
+MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMxMTIxMjA1MjExWjBPBgkq
+hkiG9w0BCQQxQgRAQcBO43Dx0aNeXXf2pU4y6U36sAtFProN8PEoolCZgeDIjRsi
+1OJkxNAmEqrHK21DzmXefNZSB+FHcaKCUY+XSDB4BgkrBgEEAYI3EAQxazBpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkqhkiG9w0BAQEF
+AASCAQCdJDF3KkiUEoV1n05fuq0MqhZmUVwLOgTvfmnTFj142WNOkiKFKTw+x+BC
++iyFS4tl9lw3roWusYjdOJG4ZduKY6CENN91SEXXkBfgiRGznT17urQEmoFwm37k
+JLMb4i3nReaz2XPg17iMz6aOfzHwliWL2Z8CF+TcLitAU5rjD5zuw96dwUBf8G5C
+ra2LDSkusMAG3UFvKxwRo1i06MzeYTkTExMqBjatCT4S8eaK3xzH7twehe45FRu/
+6Wdi7hjlmGR1MOSFu5H9haZ58nSscd7xzaPeNRgpsBgHiYwisGJSYek8W71BF5iN
+Tlvj60r95OoAI6hb+o+YkzaB9jB0AAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.eml b/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.eml
new file mode 100644
index 0000000000..7eab13caa9
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.dsig.SHA512.multipart.eml
@@ -0,0 +1,76 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then clear-signed sig.SHA512
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALAilHYmQY9S/H2MP0JZkHLxqlBshzmkaVeCm8xrhJSjUqLN
+Y2dob849A8/sYystVGkuravuVAv5HQGTmQ0ntE7VlOJpBB+nKsRWUAZ8kr4HhAJl
+G2GirOwKbQnNrL/feB+AotQTJqEcKtxUsSfk9tMBp2EIh/g0WaV+jOcIjERZcnU8
+EKp/zBN0PkCykWP/3zJKaVcPaH/4ylrRXebcCblTZOhK9bLuCsyeOrFDr7XcWszL
+V1+bSS5VtnpO3Mg58720MyzNQ/rHHHSMl7LoG7V2IwbxBsM+RpBOXateTt+70tmh
+Yi+IpTYXkZqWxwnfwcCBFUzt6VK/BFD3QY4SyNEwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQmIgaQceqX0qrIHhaZ0lFoaCABIGgiPZeDBvazh0FYnaNXcSbP9kV
+Aw632l9ziRyCF7WgtODI7cf2ZNUZ/QBPYRo+mr4nsMHlFqDV7W4Z5bZiEcSmn017
+E44Mvx7swnZwhO7CWSyPifZqnHDjzFMPeyGvcsKcMlEBBU2TCF6WpszoJ6adrKHM
+bnaJ/zFaReR/mZEIXtPoYkEfG2aqjkBcACalmDm7ww2kHjEb7EXm9VaV5SPiUgQQ
+yzIt+J5gqf4Wi2CjUNwoFQAAAAAAAAAAAAA=
+
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCg
+ggF/MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIxMVowTwYJKoZIhvcNAQkEMUIEQM7/4MrSWYOTBfDRlkXHNVGux+MO
+WtneWIFa9+KrlmpOB4l52oO0nSNuNBHrx4CO6ytUb7am7FiVVfP62KpNlSQweAYJ
+KwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+AR4wDQYJKoZIhvcNAQEBBQAEggEAnYrHfvbmXBpHoL5tC6JLnlQOWukE8F6LM06O
+Wwxqu5igSnfi6vFwhWtFTO6TcjV5xG4Rb8+uNuiu6PSoMZSZ4NWYwCoe6jNcBnmA
+eyC7krmPi68hR7UncjMulN9MMCHrHxAyvL02WCPvwLdWjekXM0ITP/62WGffjUDA
+Q9ZRBQsF+ZAx0Y70Djo2jizCx02Vsl5cxdAPlpTthdEcOci657ZBZKR7gHSCMSrh
+XL5KAgZ371YQlDQlweqf1xUA1Z0lEo4+4Vlon2cSH8d+uBrFi6iNuwzU6002DIYJ
+p08K2kSDmbh/P2CL6djo+H9+oHMhPxvSJeQnoUmGgEDzKZASJgAAAAAAAA==
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.env.eml b/comm/mailnews/test/data/smime/alice.env.eml
new file mode 100644
index 0000000000..d6ff3aa37c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.eml
@@ -0,0 +1,26 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALAilHYmQY9S/H2MP0JZkHLxqlBshzmkaVeCm8xrhJSjUqLN
+Y2dob849A8/sYystVGkuravuVAv5HQGTmQ0ntE7VlOJpBB+nKsRWUAZ8kr4HhAJl
+G2GirOwKbQnNrL/feB+AotQTJqEcKtxUsSfk9tMBp2EIh/g0WaV+jOcIjERZcnU8
+EKp/zBN0PkCykWP/3zJKaVcPaH/4ylrRXebcCblTZOhK9bLuCsyeOrFDr7XcWszL
+V1+bSS5VtnpO3Mg58720MyzNQ/rHHHSMl7LoG7V2IwbxBsM+RpBOXateTt+70tmh
+Yi+IpTYXkZqWxwnfwcCBFUzt6VK/BFD3QY4SyNEwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQmIgaQceqX0qrIHhaZ0lFoaCABIGgiPZeDBvazh0FYnaNXcSbP9kV
+Aw632l9ziRyCF7WgtODI7cf2ZNUZ/QBPYRo+mr4nsMHlFqDV7W4Z5bZiEcSmn017
+E44Mvx7swnZwhO7CWSyPifZqnHDjzFMPeyGvcsKcMlEBBU2TCF6WpszoJ6adrKHM
+bnaJ/zFaReR/mZEIXtPoYkEfG2aqjkBcACalmDm7ww2kHjEb7EXm9VaV5SPiUgQQ
+yzIt+J5gqf4Wi2CjUNwoFQAAAAAAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..295500f2ab
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml
@@ -0,0 +1,131 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA1 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAEgg+WQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1taW1lOyBuYW1l
+PXNtaW1lLnA3bTsKICAgIHNtaW1lLXR5cGU9c2lnbmVkLWRhdGEKQ29udGVudC1U
+cmFuc2Zlci1FbmNvZGluZzogYmFzZTY0CkNvbnRlbnQtRGlzcG9zaXRpb246IGF0
+dGFjaG1lbnQ7IGZpbGVuYW1lPXNtaW1lLnA3bQpDb250ZW50LURlc2NyaXB0aW9u
+OiBTL01JTUUgQ3J5cHRvZ3JhcGhpYyBTaWduYXR1cmUKCk1JQUdDU3FHU0liM0RR
+RUhBcUNBTUlBQ0FRRXhDekFKQmdVckRnTUNHZ1VBTUlBR0NTcUdTSWIzRFFFSEFh
+Q0EKSklBRWdnUk1RMjl1ZEdWdWRDMVVlWEJsT2lCaGNIQnNhV05oZEdsdmJpOXdh
+Mk56TnkxdGFXMWxPeUJ1WVcxbApQWE50YVcxbExuQTNiVHNLSUNBZ0lITnRhVzFs
+TFhSNWNHVTlaVzUyWld4dmNHVmtMV1JoZEdFS1EyOXVkR1Z1CmRDMVVjbUZ1YzJa
+bGNpMUZibU52WkdsdVp6b2dZbUZ6WlRZMENrTnZiblJsYm5RdFJHbHpjRzl6YVhS
+cGIyNDYKSUdGMGRHRmphRzFsYm5RN0lHWnBiR1Z1WVcxbFBYTnRhVzFsTG5BM2JR
+cERiMjUwWlc1MExVUmxjMk55YVhCMAphVzl1T2lCVEwwMUpUVVVnUlc1amNubHdk
+R1ZrSUUxbGMzTmhaMlVLQ2sxSlFVZERVM0ZIVTBsaU0wUlJSVWhCCk5rTkJUVWxC
+UTBGUlFYaG5aMGRHVFVsSlFtZFJTVUpCUkVKd1RVZFJlRU42UVVwQ1owNVdRa0Za
+VkVGc1ZsUUsKVFZKTmQwVlJXVVJXVVZGSlJYZHdSRmxYZUhCYWJUbDVZbTFzYUUx
+U1dYZEdRVmxFVmxGUlNFVjNNVTVpTTFaMQpaRWRHY0dKcFFsZGhWMVl6VFZKSmR3
+cEZRVmxFVmxGUlMwVjNiRU5VTUdSV1ZYbENUMVV4VFhoR1JFRlRRbWRPClZrSkJU
+VlJETURWVVZYbENWVnBZVGpCSlJVNUNRV2RGYjAxQk1FZERVM0ZIQ2xOSllqTkVV
+VVZDUVZGVlFVSkoKU1VKQlRFRnBiRWhaYlZGWk9WTXZTREpOVURCS1dtdElUSGh4
+YkVKemFIcHRhMkZXWlVOdE9IaHlhRXBUYWxWeApURTRLV1RKa2IySTRORGxCT0M5
+eldYbHpkRlpIYTNWeVlYWjFWa0YyTlVoUlIxUnRVVEJ1ZEVVM1ZteFBTbkJDClFp
+dHVTM05TVjFWQldqaHJjalJJYUVGS2JBcEhNa2RwY2s5M1MySlJiazV5VEM5bVpV
+SXJRVzkwVVZSS2NVVmoKUzNSNFZYTlRabXM1ZEUxQ2NESkZTV2d2WnpCWFlWWXJh
+azlqU1dwRlVscGpibFU0Q2tWTGNDOTZRazR3VUd0RAplV3RYVUM4emVrcExZVlpq
+VUdGSUx6UjViSEpTV0dWaVkwTmliRlJhVDJoTE9XSk1kVU56ZVdWUGNrWkVjamRZ
+ClkxZHpla3dLVmpFcllsTlROVlowYm5CUE0wMW5OVGczTWpCTmVYcE9VUzl5U0Vo
+SVUwMXNOMHh2UnpkV01rbDMKWW5oQ2MwMHJVbkJDVDFoaGRHVlVkQ3MzTUhSdGFB
+cFphU3RKY0ZSWldHdGFjVmQ0ZDI1bWQyTkRRa1pWZW5RMgpWa3N2UWtaRU0xRlpO
+Rk41VGtWM1owRlpTa3R2V2tsb2RtTk9RVkZqUWsxQ01FZERWME5IQ2xOQlJteEJk
+MUZDClFXZFJVVzFKWjJGUlkyVnhXREJ4Y2tsSWFHRmFNR3hHYjJGRFFVSkpSMmRw
+VUZwbFJFSjJZWHBvTUVaWmJtRk8KV0dOVFlsQTVhMVlLUVhjMk16SnNPWHBwVW5s
+RFJqZFhaM1JQUkVrM1kyWXlXazVWV2k5UlFsQlpVbThyYlhJMApibk5OU0d4R2NV
+UldOMWMwV2pWaVdtbEZZMU50YmpBeE53cEZORFJOZG5nM2MzZHVXbmRvVHpkRFYx
+TjVVR2xtClduRnVTRVJxZWtaTlVHVjVSM1pqYzB0alRXeEZRa0pWTWxSRFJqWlhj
+SE42YjBvMllXUnlTMGhOQ21KdVlVb3YKZWtaaFVtVlNMMjFhUlVsWWRGQnZXV3RG
+WmtjeVlYRnFhMEpqUVVOaGJHMUViVGQzZHpKclNHcEZZamRGV0cwNQpWbUZXTlZO
+UWFWVm5VVkVLZVhwSmRDdEtOV2R4WmpSWGFUSkRhbFZPZDI5R1VVRkJRVUZCUVVG
+QlFVRkJRVUU5CkNnb0FBQUFBQUFDZ2dnTmlNSUlEWGpDQ0FrYWdBd0lCQWdJQkhq
+QU5CZ2txaGtpRzl3MEJBUXNGQURCa01Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdB
+MVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5SaAphVzRn
+Vm1sbGR6RVNNQkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZRUURFd3RP
+VTFNZ1ZHVnpkQ0JEClFUQWVGdzB5TXpFeE1qRXlNRFV3TXpaYUZ3MHlPREV4TWpF
+eU1EVXdNelphTUlHQU1Rc3dDUVlEVlFRR0V3SlYKVXpFVE1CRUdBMVVFQ0JNS1Ey
+RnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5SaGFXNGdWbWxsZHpFUwpN
+QkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRNU0F3SGdZSktvWklodmNOQVFrQkZoRkJi
+R2xqWlVCbGVHRnRjR3hsCkxtTnZiVEVPTUF3R0ExVUVBeE1GUVd4cFkyVXdnZ0Vp
+TUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUsKQW9JQkFRQ2V1bUVURDhn
+ZVY0NGVhZXNXT3BoWTEvQ3VkcVNmMVVlOGxXSUt2STdRWTQ2bEpRVXR0Sk1DWFRu
+cApxZUhyeElPK2pPNVlYNXNyYTZPeTBwVmR2TGxGZnhKaHk1elcvWFBSWXdZT3ZW
+TGw5RDkwOFNiTzZJdGJwS3pnCko0dCthVDRYZXBvL3BnWm9RdXhtZ1N6cDlSeVhj
+VTh3eC96VXAxaklXWExkV1BhMGRVVmp4R3dsYWdQMURPdGEKbHhhVEhJdmxFUHk0
+TzJNWTYzNk1KVm5VRW5zN1VEOG1qR0tUdGhtMlRmbTluV2k5aWdCRHRBSE5PSHVD
+cUdxSwpJUDJZUml2Q1FLcmpwSUt6N0pkNXA0WFl6cTJVTlpXdUd0NHl6Yk96Q2FV
+VXNKbDg2djRnREN4S0I3UHV2cXpuCnE4dEZCN2s3QmFBOUZISkRmZEFodXgzdHlZ
+dmRBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFJZEUKcjlKTjkvQzZv
+anlVWWd6aGwxRU0xcGZMays2U1p0Ti9pWHdTNXBlTW9sYkRMbVZyOU4wN0dtcURt
+MXQvdVRVQgpiSUR6d0pVcU5DY3llcGVwdDRQK3BwUlFOVE1Zai9ZdTNFMW5OeGpr
+RnBUazlxNjhlV1VOMnJnZW1PNVI4aWk2Cm0rUHZWZDJ4ZSsrZFZzTmlBNWxjTnRa
+RXE1dkxkdkpoNmVHNkJNdXgzYnNEajlXUjR1NzNldVdrbHBkTG9TSjcKRVNwRDBv
+MThTRDFsck1yZE5pVUNWQU15bjhqSnZ2WlpVSW1Zc25VOUt4c01xV21JdzdNdE12
+d25Hd2hFRWhOWQp1UXNSdnBtTXNGdThLQnIwcUF2NWFlblBUcVI2cFJ1MDZEb0VH
+TUU2SmZEWTlqbG85VTFFSmwwdTZPN0dQQ293CjNzbG1jT1pEUFFNZjNiUnVuYjh4
+Z2dMbk1JSUM0d0lCQVRCcE1HUXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWUQKVlFR
+SUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdW
+M01SSXdFQVlEVlFRSwpFd2xDVDBkVlV5Qk9VMU14RkRBU0JnTlZCQU1UQzA1VFV5
+QlVaWE4wSUVOQkFnRWVNQWtHQlNzT0F3SWFCUUNnCmdnRlRNQmdHQ1NxR1NJYjNE
+UUVKQXpFTEJna3Foa2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05BUWtGTVE4WERUSXoK
+TVRFeU1USXdOVEl3Tmxvd0l3WUpLb1pJaHZjTkFRa0VNUllFRko2VFZVNFZLbG1o
+eER6aDBBdXFhT2Y5b3ZaMgpNSGdHQ1NzR0FRUUJnamNRQkRGck1Ha3daREVMTUFr
+R0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtCmIzSnVhV0V4RmpBVUJn
+TlZCQWNURFUxdmRXNTBZV2x1SUZacFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElF
+NVQKVXpFVU1CSUdBMVVFQXhNTFRsTlRJRlJsYzNRZ1EwRUNBUjR3ZWdZTEtvWklo
+dmNOQVFrUUFnc3hhNkJwTUdReApDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJ
+RXdwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSEV3MU5iM1Z1CmRHRnBiaUJXYVdW
+M01SSXdFQVlEVlFRS0V3bENUMGRWVXlCT1UxTXhGREFTQmdOVkJBTVRDMDVUVXlC
+VVpYTjAKSUVOQkFnRWVNQTBHQ1NxR1NJYjNEUUVCQVFVQUJJSUJBQkk4S2xtM3pR
+ZVpOV205cURwUDRlb0lkMmtsRTN3MApWbW1FYi9vUFdZdDgydysrWlhjWDJLam9J
+NU0xazRaVVdlQXAzMUx5czF4QndCTEdJR2FMcHY0KzhKQ3cwWFVBCm5wand6eWtN
+SWlqUG5zS2d1eWZ0QzluVXdWcHN4S3djNlhLelJUUExsbW5QRWwxU3pMZ1ZEeW1T
+ZWhldU8yZWcKdE9udnNoZW9LZ0FxUWhmeUtjTEMwa1lRR3lsL2dBaEhkNFBWTXI5
+N2dsQUpsWWtNN2ZIdFVsRXcya0Q3NW1ZNwpyaE1aSnNLcktXMEZobWE3b1BqaGdk
+cDlhdmQ5ZEtPYW5oSFV1bGd4czQxeWFWa3c4VUxCUnZLdTEzU3NTNnlmCkxqSkcv
+MlcxQmRrSldzTGhqbmYvak1kOHlmdzZaNUV2YzQ3N1BxbmJQK2sxYjBsSTJxMnFO
+K3NBQUFBQUFBQT0KAAAAAAAAoIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcN
+AQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV
+BAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxML
+TlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQsw
+CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRh
+aW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZl
+QGV4YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+e
+d2SBQ4DTDcjKM7YOIq15M5dmfVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+g
+OWW+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAV
+ECc2kJqceotCoJ7OTZQODarGhZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw
+5a2xzUWId0iWbKxW0r1U2syGM7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaT
+mJy7phRbJNXz/RzB3E6klpJftX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1
+Mar7fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nB
+jtiUmH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzj
+EYF8kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmg
+PxN8gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4
+GbR7fGBWgckqO91TRRLA74Wu+a3JMYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJV
+UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzES
+MBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjAJBgUr
+DgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
+BTEPFw0yMzExMjEyMDUyMDZaMCMGCSqGSIb3DQEJBDEWBBSHr4cnoWdpElbFRIhR
+Loc6lIJBVzB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMHoGCyqGSIb3DQEJEAIL
+MWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBMjANBgkqhkiG9w0BAQEFAASCAQAkDvW85/xcFRxE4U95Pv7M
+kkTLxfdWIapWFQ5wJ4qTDBQ6OlH4lqE3C5XNsQUPtumsvvfYcl8n+meQsp6yGglX
+Icqf09FeyR20d2TC2WgrTeS2V1Ye0Rr9qhJRLLOyQ7FsEExYgvcgGA8U1JCG49cm
+z6bDRwarUaJyI8rBTISdYCF70MuqSHRtoe4rmx2yAsyrGVUVTP0WyB9yq7UD27Hg
+AL7CuHWiD67JbwrfGX5XXVh8CpBUmsQDhB9hCQ2QIZdQMGp9URuXjii/xMZneTh6
+TWIyNF9D3JakhEpukz/7fTPWfLnTDa4jyHhob41ZoIcu0yb7PBYILA7AyIBZksd2
+AAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..ddf0eb3d3c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA1.opaque.eml
@@ -0,0 +1,70 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA1
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAEggRMQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1taW1lOyBuYW1l
+PXNtaW1lLnA3bTsKICAgIHNtaW1lLXR5cGU9ZW52ZWxvcGVkLWRhdGEKQ29udGVu
+dC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0CkNvbnRlbnQtRGlzcG9zaXRpb246
+IGF0dGFjaG1lbnQ7IGZpbGVuYW1lPXNtaW1lLnA3bQpDb250ZW50LURlc2NyaXB0
+aW9uOiBTL01JTUUgRW5jcnlwdGVkIE1lc3NhZ2UKCk1JQUdDU3FHU0liM0RRRUhB
+NkNBTUlBQ0FRQXhnZ0dGTUlJQmdRSUJBREJwTUdReEN6QUpCZ05WQkFZVEFsVlQK
+TVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSEV3MU5iM1Z1
+ZEdGcGJpQldhV1YzTVJJdwpFQVlEVlFRS0V3bENUMGRWVXlCT1UxTXhGREFTQmdO
+VkJBTVRDMDVUVXlCVVpYTjBJRU5CQWdFb01BMEdDU3FHClNJYjNEUUVCQVFVQUJJ
+SUJBTEFpbEhZbVFZOVMvSDJNUDBKWmtITHhxbEJzaHpta2FWZUNtOHhyaEpTalVx
+TE4KWTJkb2I4NDlBOC9zWXlzdFZHa3VyYXZ1VkF2NUhRR1RtUTBudEU3VmxPSnBC
+QituS3NSV1VBWjhrcjRIaEFKbApHMkdpck93S2JRbk5yTC9mZUIrQW90UVRKcUVj
+S3R4VXNTZms5dE1CcDJFSWgvZzBXYVYrak9jSWpFUlpjblU4CkVLcC96Qk4wUGtD
+eWtXUC8zekpLYVZjUGFILzR5bHJSWGViY0NibFRaT2hLOWJMdUNzeWVPckZEcjdY
+Y1dzekwKVjErYlNTNVZ0bnBPM01nNTg3MjBNeXpOUS9ySEhIU01sN0xvRzdWMkl3
+YnhCc00rUnBCT1hhdGVUdCs3MHRtaApZaStJcFRZWGtacVd4d25md2NDQkZVenQ2
+VksvQkZEM1FZNFN5TkV3Z0FZSktvWklodmNOQVFjQk1CMEdDV0NHClNBRmxBd1FC
+QWdRUW1JZ2FRY2VxWDBxcklIaGFaMGxGb2FDQUJJR2dpUFplREJ2YXpoMEZZbmFO
+WGNTYlA5a1YKQXc2MzJsOXppUnlDRjdXZ3RPREk3Y2YyWk5VWi9RQlBZUm8rbXI0
+bnNNSGxGcURWN1c0WjViWmlFY1NtbjAxNwpFNDRNdng3c3duWndoTzdDV1N5UGlm
+WnFuSERqekZNUGV5R3Zjc0tjTWxFQkJVMlRDRjZXcHN6b0o2YWRyS0hNCmJuYUov
+ekZhUmVSL21aRUlYdFBvWWtFZkcyYXFqa0JjQUNhbG1EbTd3dzJrSGpFYjdFWG05
+VmFWNVNQaVVnUVEKeXpJdCtKNWdxZjRXaTJDalVOd29GUUFBQUFBQUFBQUFBQUE9
+CgoAAAAAAACgggNiMIIDXjCCAkagAwIBAgIBHjANBgkqhkiG9w0BAQsFADBkMQsw
+CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRh
+aW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBD
+QTAeFw0yMzExMjEyMDUwMzZaFw0yODExMjEyMDUwMzZaMIGAMQswCQYDVQQGEwJV
+UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzES
+MBAGA1UEChMJQk9HVVMgTlNTMSAwHgYJKoZIhvcNAQkBFhFBbGljZUBleGFtcGxl
+LmNvbTEOMAwGA1UEAxMFQWxpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCeumETD8geV44eaesWOphY1/CudqSf1Ue8lWIKvI7QY46lJQUttJMCXTnp
+qeHrxIO+jO5YX5sra6Oy0pVdvLlFfxJhy5zW/XPRYwYOvVLl9D908SbO6ItbpKzg
+J4t+aT4Xepo/pgZoQuxmgSzp9RyXcU8wx/zUp1jIWXLdWPa0dUVjxGwlagP1DOta
+lxaTHIvlEPy4O2MY636MJVnUEns7UD8mjGKTthm2Tfm9nWi9igBDtAHNOHuCqGqK
+IP2YRivCQKrjpIKz7Jd5p4XYzq2UNZWuGt4yzbOzCaUUsJl86v4gDCxKB7Puvqzn
+q8tFB7k7BaA9FHJDfdAhux3tyYvdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIdE
+r9JN9/C6ojyUYgzhl1EM1pfLk+6SZtN/iXwS5peMolbDLmVr9N07GmqDm1t/uTUB
+bIDzwJUqNCcyepept4P+ppRQNTMYj/Yu3E1nNxjkFpTk9q68eWUN2rgemO5R8ii6
+m+PvVd2xe++dVsNiA5lcNtZEq5vLdvJh6eG6BMux3bsDj9WR4u73euWklpdLoSJ7
+ESpD0o18SD1lrMrdNiUCVAMyn8jJvvZZUImYsnU9KxsMqWmIw7MtMvwnGwhEEhNY
+uQsRvpmMsFu8KBr0qAv5aenPTqR6pRu06DoEGME6JfDY9jlo9U1EJl0u6O7GPCow
+3slmcOZDPQMf3bRunb8xggLnMIIC4wIBATBpMGQxCzAJBgNVBAYTAlVTMRMwEQYD
+VQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQK
+EwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMAkGBSsOAwIaBQCg
+ggFTMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwNlowIwYJKoZIhvcNAQkEMRYEFJ6TVU4VKlmhxDzh0AuqaOf9ovZ2
+MHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlm
+b3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5T
+UzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBABI8Klm3zQeZNWm9qDpP4eoId2klE3w0
+VmmEb/oPWYt82w++ZXcX2KjoI5M1k4ZUWeAp31Lys1xBwBLGIGaLpv4+8JCw0XUA
+npjwzykMIijPnsKguyftC9nUwVpsxKwc6XKzRTPLlmnPEl1SzLgVDymSeheuO2eg
+tOnvsheoKgAqQhfyKcLC0kYQGyl/gAhHd4PVMr97glAJlYkM7fHtUlEw2kD75mY7
+rhMZJsKrKW0Fhma7oPjhgdp9avd9dKOanhHUulgxs41yaVkw8ULBRvKu13SsS6yf
+LjJG/2W1BdkJWsLhjnf/jMd8yfw6Z5Evc477PqnbP+k1b0lI2q2qN+sAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..3835411fdd
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml
@@ -0,0 +1,132 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA256 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABIIPs0NvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnRUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQklJRVRFTnZiblJsYm5RdFZIbHdaVG9nWVhCd2JHbGpZ
+WFJwYjI0dmNHdGpjemN0YldsdFpUc2cKYm1GdFpUMXpiV2x0WlM1d04yMDdDaUFn
+SUNCemJXbHRaUzEwZVhCbFBXVnVkbVZzYjNCbFpDMWtZWFJoQ2tOdgpiblJsYm5R
+dFZISmhibk5tWlhJdFJXNWpiMlJwYm1jNklHSmhjMlUyTkFwRGIyNTBaVzUwTFVS
+cGMzQnZjMmwwCmFXOXVPaUJoZEhSaFkyaHRaVzUwT3lCbWFXeGxibUZ0WlQxemJX
+bHRaUzV3TjIwS1EyOXVkR1Z1ZEMxRVpYTmoKY21sd2RHbHZiam9nVXk5TlNVMUZJ
+RVZ1WTNKNWNIUmxaQ0JOWlhOellXZGxDZ3BOU1VGSFExTnhSMU5KWWpORQpVVVZJ
+UVRaRFFVMUpRVU5CVVVGNFoyZEhSazFKU1VKblVVbENRVVJDY0UxSFVYaERla0ZL
+UW1kT1ZrSkJXVlJCCmJGWlVDazFTVFhkRlVWbEVWbEZSU1VWM2NFUlpWM2h3V20w
+NWVXSnRiR2hOVWxsM1JrRlpSRlpSVVVoRmR6Rk8KWWpOV2RXUkhSbkJpYVVKWFlW
+ZFdNMDFTU1hjS1JVRlpSRlpSVVV0RmQyeERWREJrVmxWNVFrOVZNVTE0UmtSQgpV
+MEpuVGxaQ1FVMVVRekExVkZWNVFsVmFXRTR3U1VWT1FrRm5SVzlOUVRCSFExTnhS
+d3BUU1dJelJGRkZRa0ZSClZVRkNTVWxDUVV4QmFXeElXVzFSV1RsVEwwZ3lUVkF3
+U2xwclNFeDRjV3hDYzJoNmJXdGhWbVZEYlRoNGNtaEsKVTJwVmNVeE9DbGt5Wkc5
+aU9EUTVRVGd2YzFsNWMzUldSMnQxY21GMmRWWkJkalZJVVVkVWJWRXdiblJGTjFa
+cwpUMHB3UWtJcmJrdHpVbGRWUVZvNGEzSTBTR2hCU213S1J6SkhhWEpQZDB0aVVX
+NU9ja3d2Wm1WQ0swRnZkRkZVClNuRkZZMHQwZUZWelUyWnJPWFJOUW5BeVJVbG9M
+MmN3VjJGV0sycFBZMGxxUlZKYVkyNVZPQXBGUzNBdmVrSk8KTUZCclEzbHJWMUF2
+TTNwS1MyRldZMUJoU0M4MGVXeHlVbGhsWW1ORFlteFVXazlvU3psaVRIVkRjM2xs
+VDNKRwpSSEkzV0dOWGMzcE1DbFl4SzJKVFV6VldkRzV3VHpOTlp6VTROekl3VFhs
+NlRsRXZja2hJU0ZOTmJEZE1iMGMzClZqSkpkMko0UW5OTksxSndRazlZWVhSbFZI
+UXJOekIwYldnS1dXa3JTWEJVV1ZoclduRlhlSGR1Wm5kalEwSkcKVlhwME5sWkxM
+MEpHUkROUldUUlRlVTVGZDJkQldVcExiMXBKYUhaalRrRlJZMEpOUWpCSFExZERS
+d3BUUVVacwpRWGRSUWtGblVWRnRTV2RoVVdObGNWZ3djWEpKU0doaFdqQnNSbTlo
+UTBGQ1NVZG5hVkJhWlVSQ2RtRjZhREJHCldXNWhUbGhqVTJKUU9XdFdDa0YzTmpN
+eWJEbDZhVko1UTBZM1YyZDBUMFJKTjJObU1scE9WVm92VVVKUVdWSnYKSzIxeU5H
+NXpUVWhzUm5GRVZqZFhORm8xWWxwcFJXTlRiVzR3TVRjS1JUUTBUWFo0TjNOM2Js
+cDNhRTgzUTFkVAplVkJwWmxweGJraEVhbnBHVFZCbGVVZDJZM05MWTAxc1JVSkNW
+VEpVUTBZMlYzQnplbTlLTm1Ga2NrdElUUXBpCmJtRktMM3BHWVZKbFVpOXRXa1ZK
+V0hSUWIxbHJSV1pITW1GeGFtdENZMEZEWVd4dFJHMDNkM2N5YTBocVJXSTMKUlZo
+dE9WWmhWalZUVUdsVloxRlJDbmw2U1hRclNqVm5jV1kwVjJreVEycFZUbmR2UmxG
+QlFVRkJRVUZCUVVGQgpRVUZCUFFvS0FBQUFBQUFBb0lJRFlqQ0NBMTR3Z2dKR29B
+TUNBUUlDQVI0d0RRWUpLb1pJaHZjTkFRRUxCUUF3ClpERUxNQWtHQTFVRUJoTUNW
+Vk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXYK
+ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RVVNQklH
+QTFVRUF4TUxUbE5USUZSbApjM1FnUTBFd0hoY05Nak14TVRJeE1qQTFNRE0yV2hj
+Tk1qZ3hNVEl4TWpBMU1ETTJXakNCZ0RFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJn
+TlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlG
+WnAKWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RWdNQjRHQ1NxR1NJYjNE
+UUVKQVJZUlFXeHBZMlZBWlhoaApiWEJzWlM1amIyMHhEakFNQmdOVkJBTVRCVUZz
+YVdObE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFF
+QW5ycGhFdy9JSGxlT0htbnJGanFZV05md3JuYWtuOVZIdkpWaUNyeU8wR09PcFNV
+RkxiU1QKQWwwNTZhbmg2OFNEdm96dVdGK2JLMnVqc3RLVlhieTVSWDhTWWN1YzF2
+MXowV01HRHIxUzVmUS9kUEVtenVpTApXNlNzNENlTGZtaytGM3FhUDZZR2FFTHNa
+b0VzNmZVY2wzRlBNTWY4MUtkWXlGbHkzVmoydEhWRlk4UnNKV29ECjlRenJXcGNX
+a3h5TDVSRDh1RHRqR090K2pDVloxQko3TzFBL0pveGlrN1ladGszNXZaMW92WW9B
+UTdRQnpUaDcKZ3FocWlpRDltRVlyd2tDcTQ2U0NzK3lYZWFlRjJNNnRsRFdWcmhy
+ZU1zMnpzd21sRkxDWmZPcitJQXdzU2dlego3cjZzNTZ2TFJRZTVPd1dnUFJSeVEz
+M1FJYnNkN2NtTDNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQ0hS
+Sy9TVGZmd3VxSThsR0lNNFpkUkROYVh5NVB1a21iVGY0bDhFdWFYaktKV3d5NWxh
+L1RkT3hwcWc1dGIKZjdrMUFXeUE4OENWS2pRbk1ucVhxYmVEL3FhVVVEVXpHSS8y
+THR4Tlp6Y1k1QmFVNVBhdXZIbGxEZHE0SHBqdQpVZklvdXB2ajcxWGRzWHZ2blZi
+RFlnT1pYRGJXUkt1YnkzYnlZZW5odWdUTHNkMjdBNC9Wa2VMdTkzcmxwSmFYClM2
+RWlleEVxUTlLTmZFZzlaYXpLM1RZbEFsUURNcC9JeWI3MldWQ0ptTEoxUFNzYkRL
+bHBpTU96TFRMOEp4c0kKUkJJVFdMa0xFYjZaakxCYnZDZ2E5S2dMK1ducHowNmtl
+cVVidE9nNkJCakJPaVh3MlBZNWFQVk5SQ1pkTHVqdQp4andxTU43SlpuRG1RejBE
+SDkyMGJwMi9NWUlDOXpDQ0F2TUNBUUV3YVRCa01Rc3dDUVlEVlFRR0V3SlZVekVU
+Ck1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5S
+aGFXNGdWbWxsZHpFU01CQUcKQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZR
+UURFd3RPVTFNZ1ZHVnpkQ0JEUVFJQkhqQU5CZ2xnaGtnQgpaUU1FQWdFRkFLQ0NB
+Vjh3R0FZSktvWklodmNOQVFrRE1Rc0dDU3FHU0liM0RRRUhBVEFjQmdrcWhraUc5
+dzBCCkNRVXhEeGNOTWpNeE1USXhNakExTWpBM1dqQXZCZ2txaGtpRzl3MEJDUVF4
+SWdRZ1JlMGNxRjVTWFR6MjQwL2kKVFQwR2hvc3J6NldmR2FwcS9VT0swYmtVMHNz
+d2VBWUpLd1lCQkFHQ054QUVNV3N3YVRCa01Rc3dDUVlEVlFRRwpFd0pWVXpFVE1C
+RUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5SaGFX
+NGdWbWxsCmR6RVNNQkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZRUURF
+d3RPVTFNZ1ZHVnpkQ0JEUVFJQkhqQjYKQmdzcWhraUc5dzBCQ1JBQ0N6RnJvR2t3
+WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbQpiM0p1YVdF
+eEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VK
+UFIxVlRJRTVUClV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RR
+WUpLb1pJaHZjTkFRRUJCUUFFZ2dFQU5wQjYKRGwwbWNVUkF1UlB0R1VMbERhdCtS
+dllPcFNnVXBvcVRyc004TEp3ZGo3NUJBVE5kRGlYVHJSUWE0aytVOWREQwpUOWt0
+Z2VMWDZ3bXdiQnU0MkVPU1RPNnhQREJTUjdYVUVZV3NhVi9IN0JvdnRLLzFKWk1z
+WkVXRmE4REZ1VlRhCnlncWxNV1NoOCs3Y2dWSmlZRVBYNVdTYjEvQm5IZ1A4SnJL
+SlNhQkh3OVJnWnZSYzRjb2MxeW5nQjRvRm5ERXYKRFE4NTkrbXk4OEF5UEF2WW9a
+cTRMOU1UYzVYVnI0TU9ySExNZSsyMExNeFF6ZFFPdmkveVl6MndFS3pvMGM2Uwp6
+MVZwdFVwbzlFeUVtczdRamk2YVh6cThLcHZpMEJCYmNIMFlaRU5GbWJjdTJEampR
+b1V3ZXRyY1ZvR21ma24xCnFJZVgrUzRBc3F0RUJ3aU0xUUFBQUFBQUFBPT0KAAAA
+AAAAoIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcN
+MjMxMTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEG
+A1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UE
+ChMJQk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0w
+CwYDVQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEui
+MqsNIXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15
+M5dmfVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt
+3KCku9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQO
+DarGhZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U
+2syGM7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6k
+lpJftX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3
+k5JaLHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR
+6M4uL6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox
+/Ajw3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEE
+esD1SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46
+jS2Tb52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA
+74Wu+a3JMYIC9zCCAvMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBglghkgBZQMEAgEFAKCCAV8w
+GAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMxMTIx
+MjA1MjA4WjAvBgkqhkiG9w0BCQQxIgQgB5kEehqjBBQgSmmSaJe317lotoCQ61TY
+Gu1WJaL4kUsweAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRAC
+CzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV
+BAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxML
+TlNTIFRlc3QgQ0ECATIwDQYJKoZIhvcNAQEBBQAEggEARoCTSNz+CTwFJHyK2tk3
+6sk3XrH2WI8S0HbJyjNRNxYPlcn45SDgzMXCvcoEF+/j7N3kvManWseIFyqVvRzo
+Afvh/kNZ5Qw8i0RytU1NiLn4uCZsTHrhDXS/WyZFAhr00jKh26LZ4VT/IbGy4o/s
+ySauEfhWaqkF3mfihoSVYcXLDOMwSjH9Ixcouf6nzYInccs+c60U4Kxpfo3KTDD6
+Hi14Ic0yj+47ob668xvwOovi+bfFdG36n/rFQ8CKa2CahhHYiGT8ufj/2oPKbRc0
++ixwUSloHm1b1K1wg5HpuMBn7nydMid4IFPv5z7wMaEL++D68GV44mlUYI+1oLNI
+bgAAAAAAAA==
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..d617329d23
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA256.opaque.eml
@@ -0,0 +1,71 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA256
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABIIETENvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKAAAAAAAAoIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhh
+bXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbST
+Al056anh68SDvozuWF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiL
+W6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD
+9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7
+gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez
+7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tb
+f7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4Hpju
+UfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaX
+S6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsI
+RBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLuju
+xjwqMN7JZnDmQz0DH920bp2/MYIC9zCCAvMCAQEwaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBglghkgB
+ZQMEAgEFAKCCAV8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B
+CQUxDxcNMjMxMTIxMjA1MjA3WjAvBgkqhkiG9w0BCQQxIgQgRe0cqF5SXTz240/i
+TT0Ghosrz6WfGapq/UOK0bkU0ssweAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQG
+EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
+dzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6
+BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlm
+b3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5T
+UzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJKoZIhvcNAQEBBQAEggEANpB6
+Dl0mcURAuRPtGULlDat+RvYOpSgUpoqTrsM8LJwdj75BATNdDiXTrRQa4k+U9dDC
+T9ktgeLX6wmwbBu42EOSTO6xPDBSR7XUEYWsaV/H7BovtK/1JZMsZEWFa8DFuVTa
+ygqlMWSh8+7cgVJiYEPX5WSb1/BnHgP8JrKJSaBHw9RgZvRc4coc1yngB4oFnDEv
+DQ859+my88AyPAvYoZq4L9MTc5XVr4MOrHLMe+20LMxQzdQOvi/yYz2wEKzo0c6S
+z1VptUpo9EyEms7Qji6aXzq8Kpvi0BBbcH0YZENFmbcu2DjjQoUwetrcVoGmfkn1
+qIeX+S4AsqtEBwiM1QAAAAAAAA==
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..38ba154842
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml
@@ -0,0 +1,132 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA384 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABIIPx0NvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnSUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQklJRVRFTnZiblJsYm5RdFZIbHdaVG9nWVhCd2JHbGpZ
+WFJwYjI0dmNHdGpjemN0YldsdFpUc2cKYm1GdFpUMXpiV2x0WlM1d04yMDdDaUFn
+SUNCemJXbHRaUzEwZVhCbFBXVnVkbVZzYjNCbFpDMWtZWFJoQ2tOdgpiblJsYm5R
+dFZISmhibk5tWlhJdFJXNWpiMlJwYm1jNklHSmhjMlUyTkFwRGIyNTBaVzUwTFVS
+cGMzQnZjMmwwCmFXOXVPaUJoZEhSaFkyaHRaVzUwT3lCbWFXeGxibUZ0WlQxemJX
+bHRaUzV3TjIwS1EyOXVkR1Z1ZEMxRVpYTmoKY21sd2RHbHZiam9nVXk5TlNVMUZJ
+RVZ1WTNKNWNIUmxaQ0JOWlhOellXZGxDZ3BOU1VGSFExTnhSMU5KWWpORQpVVVZJ
+UVRaRFFVMUpRVU5CVVVGNFoyZEhSazFKU1VKblVVbENRVVJDY0UxSFVYaERla0ZL
+UW1kT1ZrSkJXVlJCCmJGWlVDazFTVFhkRlVWbEVWbEZSU1VWM2NFUlpWM2h3V20w
+NWVXSnRiR2hOVWxsM1JrRlpSRlpSVVVoRmR6Rk8KWWpOV2RXUkhSbkJpYVVKWFlW
+ZFdNMDFTU1hjS1JVRlpSRlpSVVV0RmQyeERWREJrVmxWNVFrOVZNVTE0UmtSQgpV
+MEpuVGxaQ1FVMVVRekExVkZWNVFsVmFXRTR3U1VWT1FrRm5SVzlOUVRCSFExTnhS
+d3BUU1dJelJGRkZRa0ZSClZVRkNTVWxDUVV4QmFXeElXVzFSV1RsVEwwZ3lUVkF3
+U2xwclNFeDRjV3hDYzJoNmJXdGhWbVZEYlRoNGNtaEsKVTJwVmNVeE9DbGt5Wkc5
+aU9EUTVRVGd2YzFsNWMzUldSMnQxY21GMmRWWkJkalZJVVVkVWJWRXdiblJGTjFa
+cwpUMHB3UWtJcmJrdHpVbGRWUVZvNGEzSTBTR2hCU213S1J6SkhhWEpQZDB0aVVX
+NU9ja3d2Wm1WQ0swRnZkRkZVClNuRkZZMHQwZUZWelUyWnJPWFJOUW5BeVJVbG9M
+MmN3VjJGV0sycFBZMGxxUlZKYVkyNVZPQXBGUzNBdmVrSk8KTUZCclEzbHJWMUF2
+TTNwS1MyRldZMUJoU0M4MGVXeHlVbGhsWW1ORFlteFVXazlvU3psaVRIVkRjM2xs
+VDNKRwpSSEkzV0dOWGMzcE1DbFl4SzJKVFV6VldkRzV3VHpOTlp6VTROekl3VFhs
+NlRsRXZja2hJU0ZOTmJEZE1iMGMzClZqSkpkMko0UW5OTksxSndRazlZWVhSbFZI
+UXJOekIwYldnS1dXa3JTWEJVV1ZoclduRlhlSGR1Wm5kalEwSkcKVlhwME5sWkxM
+MEpHUkROUldUUlRlVTVGZDJkQldVcExiMXBKYUhaalRrRlJZMEpOUWpCSFExZERS
+d3BUUVVacwpRWGRSUWtGblVWRnRTV2RoVVdObGNWZ3djWEpKU0doaFdqQnNSbTlo
+UTBGQ1NVZG5hVkJhWlVSQ2RtRjZhREJHCldXNWhUbGhqVTJKUU9XdFdDa0YzTmpN
+eWJEbDZhVko1UTBZM1YyZDBUMFJKTjJObU1scE9WVm92VVVKUVdWSnYKSzIxeU5H
+NXpUVWhzUm5GRVZqZFhORm8xWWxwcFJXTlRiVzR3TVRjS1JUUTBUWFo0TjNOM2Js
+cDNhRTgzUTFkVAplVkJwWmxweGJraEVhbnBHVFZCbGVVZDJZM05MWTAxc1JVSkNW
+VEpVUTBZMlYzQnplbTlLTm1Ga2NrdElUUXBpCmJtRktMM3BHWVZKbFVpOXRXa1ZK
+V0hSUWIxbHJSV1pITW1GeGFtdENZMEZEWVd4dFJHMDNkM2N5YTBocVJXSTMKUlZo
+dE9WWmhWalZUVUdsVloxRlJDbmw2U1hRclNqVm5jV1kwVjJreVEycFZUbmR2UmxG
+QlFVRkJRVUZCUVVGQgpRVUZCUFFvS0FBQUFBQUFBb0lJRFlqQ0NBMTR3Z2dKR29B
+TUNBUUlDQVI0d0RRWUpLb1pJaHZjTkFRRUxCUUF3ClpERUxNQWtHQTFVRUJoTUNW
+Vk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXYK
+ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RVVNQklH
+QTFVRUF4TUxUbE5USUZSbApjM1FnUTBFd0hoY05Nak14TVRJeE1qQTFNRE0yV2hj
+Tk1qZ3hNVEl4TWpBMU1ETTJXakNCZ0RFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJn
+TlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlG
+WnAKWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RWdNQjRHQ1NxR1NJYjNE
+UUVKQVJZUlFXeHBZMlZBWlhoaApiWEJzWlM1amIyMHhEakFNQmdOVkJBTVRCVUZz
+YVdObE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFF
+QW5ycGhFdy9JSGxlT0htbnJGanFZV05md3JuYWtuOVZIdkpWaUNyeU8wR09PcFNV
+RkxiU1QKQWwwNTZhbmg2OFNEdm96dVdGK2JLMnVqc3RLVlhieTVSWDhTWWN1YzF2
+MXowV01HRHIxUzVmUS9kUEVtenVpTApXNlNzNENlTGZtaytGM3FhUDZZR2FFTHNa
+b0VzNmZVY2wzRlBNTWY4MUtkWXlGbHkzVmoydEhWRlk4UnNKV29ECjlRenJXcGNX
+a3h5TDVSRDh1RHRqR090K2pDVloxQko3TzFBL0pveGlrN1ladGszNXZaMW92WW9B
+UTdRQnpUaDcKZ3FocWlpRDltRVlyd2tDcTQ2U0NzK3lYZWFlRjJNNnRsRFdWcmhy
+ZU1zMnpzd21sRkxDWmZPcitJQXdzU2dlego3cjZzNTZ2TFJRZTVPd1dnUFJSeVEz
+M1FJYnNkN2NtTDNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQ0hS
+Sy9TVGZmd3VxSThsR0lNNFpkUkROYVh5NVB1a21iVGY0bDhFdWFYaktKV3d5NWxh
+L1RkT3hwcWc1dGIKZjdrMUFXeUE4OENWS2pRbk1ucVhxYmVEL3FhVVVEVXpHSS8y
+THR4Tlp6Y1k1QmFVNVBhdXZIbGxEZHE0SHBqdQpVZklvdXB2ajcxWGRzWHZ2blZi
+RFlnT1pYRGJXUkt1YnkzYnlZZW5odWdUTHNkMjdBNC9Wa2VMdTkzcmxwSmFYClM2
+RWlleEVxUTlLTmZFZzlaYXpLM1RZbEFsUURNcC9JeWI3MldWQ0ptTEoxUFNzYkRL
+bHBpTU96TFRMOEp4c0kKUkJJVFdMa0xFYjZaakxCYnZDZ2E5S2dMK1ducHowNmtl
+cVVidE9nNkJCakJPaVh3MlBZNWFQVk5SQ1pkTHVqdQp4andxTU43SlpuRG1RejBE
+SDkyMGJwMi9NWUlEQnpDQ0F3TUNBUUV3YVRCa01Rc3dDUVlEVlFRR0V3SlZVekVU
+Ck1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5S
+aGFXNGdWbWxsZHpFU01CQUcKQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZR
+UURFd3RPVTFNZ1ZHVnpkQ0JEUVFJQkhqQU5CZ2xnaGtnQgpaUU1FQWdJRkFLQ0NB
+Vzh3R0FZSktvWklodmNOQVFrRE1Rc0dDU3FHU0liM0RRRUhBVEFjQmdrcWhraUc5
+dzBCCkNRVXhEeGNOTWpNeE1USXhNakExTWpBNVdqQS9CZ2txaGtpRzl3MEJDUVF4
+TWdRd2VNQlhzczg3WjdPamJQMXoKejVRdi9OQ091ZXJOSWJwZkdjK09RSmdFKzJs
+Z1lNT0tlOGFkMHhYNDUwSXkrakxETUhnR0NTc0dBUVFCZ2pjUQpCREZyTUdrd1pE
+RUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZq
+QVVCZ05WCkJBY1REVTF2ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05WQkFvVENVSlBS
+MVZUSUU1VFV6RVVNQklHQTFVRUF4TUwKVGxOVElGUmxjM1FnUTBFQ0FSNHdlZ1lM
+S29aSWh2Y05BUWtRQWdzeGE2QnBNR1F4Q3pBSkJnTlZCQVlUQWxWVApNUk13RVFZ
+RFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlC
+V2FXVjNNUkl3CkVBWURWUVFLRXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05WQkFNVEMw
+NVRVeUJVWlhOMElFTkJBZ0VlTUEwR0NTcUcKU0liM0RRRUJBUVVBQklJQkFEcm9F
+bzd3TUNXOUtDSnJlTzNKcVc1UkpFTVZKa3pFMm1QVW5lQ3hmYTBaRmgzSQpOWUNk
+WVB0N0MrSFh2YWFwVVJVRkg2N0J3Ui96dTZyV0x2UnEveHgrUmRYOTk0d05iUHdt
+SnRLU3J0K1U3TlZ6ClFaRmVuUnpVV25rcVlRUEZleWhPQkpyWkViNGZpVnlTZFJz
+UUhLRDQwUHIwWkU2SGkvb0xRcGthRUxGczJRWVEKbTdaS1ZSUzM0R2FueW5KT082
+UlM2M0M2WWZYQUhoS29JWjVsNDZGOStPSnAvenU5Vlc2M01ERG0rQlV1bnZHSApF
+ZDJkRTJDQzhweXBrZmk5VEx2TWw2eDhsS3ZWT25qcmttamk2cUpFUTNjUDUvcjAx
+S082Z1NrY2xTRXBpbzF1CnRKRzdEVDZHUDJ3dm82VGwrY3o3M0xOMGthQjJHQnF2
+bmIrQnV0Y0FBQUFBQUFBPQoAAAAAAACgggNfMIIDWzCCAkOgAwIBAgIBMjANBgkq
+hkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQTAeFw0yMzExMjEyMDUwNDNaFw0yODExMjEyMDUwNDNa
+MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxHzAdBgkqhkiG9w0BCQEW
+EERhdmVAZXhhbXBsZS5jb20xDTALBgNVBAMTBERhdmUwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDIS6Iyqw0hewI8Lkj+nQEhnBQfflgDKrW4nFcNZ5vz
+U92Nv553ZIFDgNMNyMoztg4irXkzl2Z9VX1xea01iA+OnWCo9w5sTYqqPYKJktXb
+it5Dr6A5Zb7//kxIB5DXirVlYK3coKS7084va31SdSYke/RgEnsNWoxn48WQdddd
+AhMQ8BUQJzaQmpx6i0Kgns5NlA4NqsaFl1+f32uzOSPOy70bx9UFprvVx2/04LXN
+NQBg0DDlrbHNRYh3SJZsrFbSvVTazIYzvOiWJo/QQcOVNdHwqFTGy8ZCtUn8SwrI
+uuvARpOYnLumFFsk1fP9HMHcTqSWkl+1f4dC5IBQW7INAgMBAAEwDQYJKoZIhvcN
+AQELBQADggEBAGNN2cbQc1VjlHeTkloseW93MTh2lEi/zKlS0byKKEFBfSlPgp0j
+tQZravUxqvt8cCNpn6KCoJIJiBHozi4vptcfvvNXQJv7Ic8v8QtG3Tqq9A2f+PAt
+wInnucGO2JSYfuIc6yfd8+0347H8CPDekAcj5uBHSRiq11r0Uv3UkltluSa/XgVI
+0vNFPOMRgXyRR1tWAv4wL4YIgQR6wPVJ9LdB678AhnElq1qOY9QlK42W01pVinvk
+5Y2AWaA/E3yAobtE13otl4GZnjqNLZNvnZ0CqmCuF85caT4fPNZNfA5NhnFFAGYd
+Mkk2FPgZtHt8YFaBySo73VNFEsDvha75rckxggMHMIIDAwIBATBpMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEy
+MA0GCWCGSAFlAwQCAgUAoIIBbzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
+CSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMTBaMD8GCSqGSIb3DQEJBDEyBDB5QYSN
+Co34KSPmqQAB3Vi5UBwOtlGft2/DLFvHmOeXf3XZXzdKa2pQbdU3Zh0+NbYweAYJ
+KwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+ATIwDQYJKoZIhvcNAQEBBQAEggEApdfZ/LaOtlUK8es57GGIjUE7ImY7MqhOyA5g
+ASOqiWqSUSlkfQou1TOpQjKKJe6/XngbT+qm559FWXLa/FikDlJqlcbmIVYsYF65
+SNAPgeiyHkrJpzBuBAkoYB7vfGNrZ24DUY3di08GiMB/dyPuNVOdVpPml156quTS
+EQ7Uc2HUMZ3hr3XEXrlonzGBrUGB58AaHVmt6R/n/YctkexKgSCSmSnZOqM+Ijng
+TA4YQNAVCpOLGWEhRlRNq1iSZ5KDPUGqDjlfwx3Ayt00eyWSEMYG1msf9gjFaRLd
+OpILfjJfvmseaxRh011lsMgOHQCEZvFWH15JnWpaDIk/sTD0QgAAAAAAAA==
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..748a76eaf9
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA384.opaque.eml
@@ -0,0 +1,71 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA384
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABIIETENvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKAAAAAAAAoIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhh
+bXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbST
+Al056anh68SDvozuWF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiL
+W6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD
+9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7
+gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez
+7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tb
+f7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4Hpju
+UfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaX
+S6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsI
+RBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLuju
+xjwqMN7JZnDmQz0DH920bp2/MYIDBzCCAwMCAQEwaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBglghkgB
+ZQMEAgIFAKCCAW8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B
+CQUxDxcNMjMxMTIxMjA1MjA5WjA/BgkqhkiG9w0BCQQxMgQweMBXss87Z7OjbP1z
+z5Qv/NCOuerNIbpfGc+OQJgE+2lgYMOKe8ad0xX450Iy+jLDMHgGCSsGAQQBgjcQ
+BDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV
+BAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxML
+TlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMA0GCSqG
+SIb3DQEBAQUABIIBADroEo7wMCW9KCJreO3JqW5RJEMVJkzE2mPUneCxfa0ZFh3I
+NYCdYPt7C+HXvaapURUFH67BwR/zu6rWLvRq/xx+RdX994wNbPwmJtKSrt+U7NVz
+QZFenRzUWnkqYQPFeyhOBJrZEb4fiVySdRsQHKD40Pr0ZE6Hi/oLQpkaELFs2QYQ
+m7ZKVRS34GanynJOO6RS63C6YfXAHhKoIZ5l46F9+OJp/zu9VW63MDDm+BUunvGH
+Ed2dE2CC8pypkfi9TLvMl6x8lKvVOnjrkmji6qJEQ3cP5/r01KO6gSkclSEpio1u
+tJG7DT6GP2wvo6Tl+cz73LN0kaB2GBqvnb+ButcAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..821a48aaa4
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml
@@ -0,0 +1,133 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA512 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABIIP3ENvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnTUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQklJRVRFTnZiblJsYm5RdFZIbHdaVG9nWVhCd2JHbGpZ
+WFJwYjI0dmNHdGpjemN0YldsdFpUc2cKYm1GdFpUMXpiV2x0WlM1d04yMDdDaUFn
+SUNCemJXbHRaUzEwZVhCbFBXVnVkbVZzYjNCbFpDMWtZWFJoQ2tOdgpiblJsYm5R
+dFZISmhibk5tWlhJdFJXNWpiMlJwYm1jNklHSmhjMlUyTkFwRGIyNTBaVzUwTFVS
+cGMzQnZjMmwwCmFXOXVPaUJoZEhSaFkyaHRaVzUwT3lCbWFXeGxibUZ0WlQxemJX
+bHRaUzV3TjIwS1EyOXVkR1Z1ZEMxRVpYTmoKY21sd2RHbHZiam9nVXk5TlNVMUZJ
+RVZ1WTNKNWNIUmxaQ0JOWlhOellXZGxDZ3BOU1VGSFExTnhSMU5KWWpORQpVVVZJ
+UVRaRFFVMUpRVU5CVVVGNFoyZEhSazFKU1VKblVVbENRVVJDY0UxSFVYaERla0ZL
+UW1kT1ZrSkJXVlJCCmJGWlVDazFTVFhkRlVWbEVWbEZSU1VWM2NFUlpWM2h3V20w
+NWVXSnRiR2hOVWxsM1JrRlpSRlpSVVVoRmR6Rk8KWWpOV2RXUkhSbkJpYVVKWFlW
+ZFdNMDFTU1hjS1JVRlpSRlpSVVV0RmQyeERWREJrVmxWNVFrOVZNVTE0UmtSQgpV
+MEpuVGxaQ1FVMVVRekExVkZWNVFsVmFXRTR3U1VWT1FrRm5SVzlOUVRCSFExTnhS
+d3BUU1dJelJGRkZRa0ZSClZVRkNTVWxDUVV4QmFXeElXVzFSV1RsVEwwZ3lUVkF3
+U2xwclNFeDRjV3hDYzJoNmJXdGhWbVZEYlRoNGNtaEsKVTJwVmNVeE9DbGt5Wkc5
+aU9EUTVRVGd2YzFsNWMzUldSMnQxY21GMmRWWkJkalZJVVVkVWJWRXdiblJGTjFa
+cwpUMHB3UWtJcmJrdHpVbGRWUVZvNGEzSTBTR2hCU213S1J6SkhhWEpQZDB0aVVX
+NU9ja3d2Wm1WQ0swRnZkRkZVClNuRkZZMHQwZUZWelUyWnJPWFJOUW5BeVJVbG9M
+MmN3VjJGV0sycFBZMGxxUlZKYVkyNVZPQXBGUzNBdmVrSk8KTUZCclEzbHJWMUF2
+TTNwS1MyRldZMUJoU0M4MGVXeHlVbGhsWW1ORFlteFVXazlvU3psaVRIVkRjM2xs
+VDNKRwpSSEkzV0dOWGMzcE1DbFl4SzJKVFV6VldkRzV3VHpOTlp6VTROekl3VFhs
+NlRsRXZja2hJU0ZOTmJEZE1iMGMzClZqSkpkMko0UW5OTksxSndRazlZWVhSbFZI
+UXJOekIwYldnS1dXa3JTWEJVV1ZoclduRlhlSGR1Wm5kalEwSkcKVlhwME5sWkxM
+MEpHUkROUldUUlRlVTVGZDJkQldVcExiMXBKYUhaalRrRlJZMEpOUWpCSFExZERS
+d3BUUVVacwpRWGRSUWtGblVWRnRTV2RoVVdObGNWZ3djWEpKU0doaFdqQnNSbTlo
+UTBGQ1NVZG5hVkJhWlVSQ2RtRjZhREJHCldXNWhUbGhqVTJKUU9XdFdDa0YzTmpN
+eWJEbDZhVko1UTBZM1YyZDBUMFJKTjJObU1scE9WVm92VVVKUVdWSnYKSzIxeU5H
+NXpUVWhzUm5GRVZqZFhORm8xWWxwcFJXTlRiVzR3TVRjS1JUUTBUWFo0TjNOM2Js
+cDNhRTgzUTFkVAplVkJwWmxweGJraEVhbnBHVFZCbGVVZDJZM05MWTAxc1JVSkNW
+VEpVUTBZMlYzQnplbTlLTm1Ga2NrdElUUXBpCmJtRktMM3BHWVZKbFVpOXRXa1ZK
+V0hSUWIxbHJSV1pITW1GeGFtdENZMEZEWVd4dFJHMDNkM2N5YTBocVJXSTMKUlZo
+dE9WWmhWalZUVUdsVloxRlJDbmw2U1hRclNqVm5jV1kwVjJreVEycFZUbmR2UmxG
+QlFVRkJRVUZCUVVGQgpRVUZCUFFvS0FBQUFBQUFBb0lJRFlqQ0NBMTR3Z2dKR29B
+TUNBUUlDQVI0d0RRWUpLb1pJaHZjTkFRRUxCUUF3ClpERUxNQWtHQTFVRUJoTUNW
+Vk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXYK
+ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RVVNQklH
+QTFVRUF4TUxUbE5USUZSbApjM1FnUTBFd0hoY05Nak14TVRJeE1qQTFNRE0yV2hj
+Tk1qZ3hNVEl4TWpBMU1ETTJXakNCZ0RFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJn
+TlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlG
+WnAKWlhjeEVqQVFCZ05WQkFvVENVSlBSMVZUSUU1VFV6RWdNQjRHQ1NxR1NJYjNE
+UUVKQVJZUlFXeHBZMlZBWlhoaApiWEJzWlM1amIyMHhEakFNQmdOVkJBTVRCVUZz
+YVdObE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFF
+QW5ycGhFdy9JSGxlT0htbnJGanFZV05md3JuYWtuOVZIdkpWaUNyeU8wR09PcFNV
+RkxiU1QKQWwwNTZhbmg2OFNEdm96dVdGK2JLMnVqc3RLVlhieTVSWDhTWWN1YzF2
+MXowV01HRHIxUzVmUS9kUEVtenVpTApXNlNzNENlTGZtaytGM3FhUDZZR2FFTHNa
+b0VzNmZVY2wzRlBNTWY4MUtkWXlGbHkzVmoydEhWRlk4UnNKV29ECjlRenJXcGNX
+a3h5TDVSRDh1RHRqR090K2pDVloxQko3TzFBL0pveGlrN1ladGszNXZaMW92WW9B
+UTdRQnpUaDcKZ3FocWlpRDltRVlyd2tDcTQ2U0NzK3lYZWFlRjJNNnRsRFdWcmhy
+ZU1zMnpzd21sRkxDWmZPcitJQXdzU2dlego3cjZzNTZ2TFJRZTVPd1dnUFJSeVEz
+M1FJYnNkN2NtTDNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQ0hS
+Sy9TVGZmd3VxSThsR0lNNFpkUkROYVh5NVB1a21iVGY0bDhFdWFYaktKV3d5NWxh
+L1RkT3hwcWc1dGIKZjdrMUFXeUE4OENWS2pRbk1ucVhxYmVEL3FhVVVEVXpHSS8y
+THR4Tlp6Y1k1QmFVNVBhdXZIbGxEZHE0SHBqdQpVZklvdXB2ajcxWGRzWHZ2blZi
+RFlnT1pYRGJXUkt1YnkzYnlZZW5odWdUTHNkMjdBNC9Wa2VMdTkzcmxwSmFYClM2
+RWlleEVxUTlLTmZFZzlaYXpLM1RZbEFsUURNcC9JeWI3MldWQ0ptTEoxUFNzYkRL
+bHBpTU96TFRMOEp4c0kKUkJJVFdMa0xFYjZaakxCYnZDZ2E5S2dMK1ducHowNmtl
+cVVidE9nNkJCakJPaVh3MlBZNWFQVk5SQ1pkTHVqdQp4andxTU43SlpuRG1RejBE
+SDkyMGJwMi9NWUlERnpDQ0F4TUNBUUV3YVRCa01Rc3dDUVlEVlFRR0V3SlZVekVU
+Ck1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxYm5S
+aGFXNGdWbWxsZHpFU01CQUcKQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZR
+UURFd3RPVTFNZ1ZHVnpkQ0JEUVFJQkhqQU5CZ2xnaGtnQgpaUU1FQWdNRkFLQ0NB
+WDh3R0FZSktvWklodmNOQVFrRE1Rc0dDU3FHU0liM0RRRUhBVEFjQmdrcWhraUc5
+dzBCCkNRVXhEeGNOTWpNeE1USXhNakExTWpFeFdqQlBCZ2txaGtpRzl3MEJDUVF4
+UWdSQXp2L2d5dEpaZzVNRjhOR1cKUmNjMVVhN0g0dzVhMmQ1WWdWcjM0cXVXYWs0
+SGlYbmFnN1NkSTI0MEVldkhnSTdySzFSdnRxYnNXSlZWOC9yWQpxazJWSkRCNEJn
+a3JCZ0VFQVlJM0VBUXhhekJwTUdReEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZR
+UUlFd3BECllXeHBabTl5Ym1saE1SWXdGQVlEVlFRSEV3MU5iM1Z1ZEdGcGJpQldh
+V1YzTVJJd0VBWURWUVFLRXdsQ1QwZFYKVXlCT1UxTXhGREFTQmdOVkJBTVRDMDVU
+VXlCVVpYTjBJRU5CQWdFZU1Ib0dDeXFHU0liM0RRRUpFQUlMTVd1ZwphVEJrTVFz
+d0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNCTUtRMkZzYVdadmNtNXBZVEVXTUJR
+R0ExVUVCeE1OClRXOTFiblJoYVc0Z1ZtbGxkekVTTUJBR0ExVUVDaE1KUWs5SFZW
+TWdUbE5UTVJRd0VnWURWUVFERXd0T1UxTWcKVkdWemRDQkRRUUlCSGpBTkJna3Fo
+a2lHOXcwQkFRRUZBQVNDQVFDZGlzZCs5dVpjR2tlZ3ZtMExva3VlVkE1YQo2UVR3
+WG9zelRvNWJER3E3bUtCS2QrTHE4WENGYTBWTTdwTnlOWG5FYmhGdno2NDI2Szdv
+OUtneGxKbmcxWmpBCktoN3FNMXdHZVlCN0lMdVN1WStMcnlGSHRTZHlNeTZVMzB3
+d0llc2ZFREs4dlRaWUkrL0F0MWFONlJjelFoTS8KL3JaWVo5K05RTUJEMWxFRkN3
+WDVrREhSanZRT09qYU9MTUxIVFpXeVhsekYwQStXbE8yRjBSdzV5THJudGtGawpw
+SHVBZElJeEt1RmN2a29DQm5mdlZoQ1VOQ1hCNnAvWEZRRFZuU1VTamo3aFdXaWZa
+eElmeDM2NEdzV0xxSTI3CkROVHJUVFlNaGdtblR3cmFSSU9adUg4L1lJdnAyT2o0
+ZjM2Z2N5RS9HOUlsNUNlaFNZYUFRUE1wa0JJbUFBQUEKQUFBQQoAAAAAAACgggNf
+MIIDWzCCAkOgAwIBAgIBMjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQTAeFw0yMzExMjEy
+MDUwNDNaFw0yODExMjEyMDUwNDNaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD
+YWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dV
+UyBOU1MxHzAdBgkqhkiG9w0BCQEWEERhdmVAZXhhbXBsZS5jb20xDTALBgNVBAMT
+BERhdmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIS6Iyqw0hewI8
+Lkj+nQEhnBQfflgDKrW4nFcNZ5vzU92Nv553ZIFDgNMNyMoztg4irXkzl2Z9VX1x
+ea01iA+OnWCo9w5sTYqqPYKJktXbit5Dr6A5Zb7//kxIB5DXirVlYK3coKS7084v
+a31SdSYke/RgEnsNWoxn48WQddddAhMQ8BUQJzaQmpx6i0Kgns5NlA4NqsaFl1+f
+32uzOSPOy70bx9UFprvVx2/04LXNNQBg0DDlrbHNRYh3SJZsrFbSvVTazIYzvOiW
+Jo/QQcOVNdHwqFTGy8ZCtUn8SwrIuuvARpOYnLumFFsk1fP9HMHcTqSWkl+1f4dC
+5IBQW7INAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGNN2cbQc1VjlHeTkloseW93
+MTh2lEi/zKlS0byKKEFBfSlPgp0jtQZravUxqvt8cCNpn6KCoJIJiBHozi4vptcf
+vvNXQJv7Ic8v8QtG3Tqq9A2f+PAtwInnucGO2JSYfuIc6yfd8+0347H8CPDekAcj
+5uBHSRiq11r0Uv3UkltluSa/XgVI0vNFPOMRgXyRR1tWAv4wL4YIgQR6wPVJ9LdB
+678AhnElq1qOY9QlK42W01pVinvk5Y2AWaA/E3yAobtE13otl4GZnjqNLZNvnZ0C
+qmCuF85caT4fPNZNfA5NhnFFAGYdMkk2FPgZtHt8YFaBySo73VNFEsDvha75rckx
+ggMXMIIDEwIBATBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
+MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDAS
+BgNVBAMTC05TUyBUZXN0IENBAgEyMA0GCWCGSAFlAwQCAwUAoIIBfzAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMTFa
+ME8GCSqGSIb3DQEJBDFCBECLGWWpslJFMvOEk9q44090UhQgpJD8KlVBlPuGsVEy
+n/0FLk6DajSnb8cvERdMxnj6r8OsmFLAlZX8skEtz5U0MHgGCSsGAQQBgjcQBDFr
+MGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
+DU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNT
+IFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMA0GCSqGSIb3
+DQEBAQUABIIBACUi8ABMy+I9BZZPF3Ammyo+p7wZ4VMyTFzp00/yiYhkL1yzqqNG
+DCnGuN9yJ18IDFcssj0se6A+2J/bQtzFdxxw/Yj31Cxm8x8B0mWNfmqlJR6Wk7/F
+x8h9KiCTS3q3PJNGHWjtRcW5lyK+7u5L3diPmU4XyCiimwPI6z1EpejoOdEkHasA
+bdLz1utrUgTxFgZ/qe0cRhj+aafUh2rgMj8soyWSHp2hEMxGmlzcJ4slCYv71syT
+ujSEdr18X02ySs/OYeEoJLIILU8KuQgeHcV9gXPNyANA3P6s3T0Q10xeDOrusEcS
+Xr9PrCxKhxKUG/oSZYn917btgmObYQO9B5MAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..472aeca9e8
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.env.sig.SHA512.opaque.eml
@@ -0,0 +1,72 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: enveloped then opaque-signed sig.SHA512
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABIIETENvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPWVudmVsb3BlZC1kYXRhCkNv
+bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0
+aW9uOiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNj
+cmlwdGlvbjogUy9NSU1FIEVuY3J5cHRlZCBNZXNzYWdlCgpNSUFHQ1NxR1NJYjNE
+UUVIQTZDQU1JQUNBUUF4Z2dHRk1JSUJnUUlCQURCcE1HUXhDekFKQmdOVkJBWVRB
+bFZUCk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFO
+YjNWdWRHRnBiaUJXYVdWM01SSXcKRUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRB
+U0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRW9NQTBHQ1NxRwpTSWIzRFFFQkFR
+VUFCSUlCQUxBaWxIWW1RWTlTL0gyTVAwSlprSEx4cWxCc2h6bWthVmVDbTh4cmhK
+U2pVcUxOClkyZG9iODQ5QTgvc1l5c3RWR2t1cmF2dVZBdjVIUUdUbVEwbnRFN1Zs
+T0pwQkIrbktzUldVQVo4a3I0SGhBSmwKRzJHaXJPd0tiUW5OckwvZmVCK0FvdFFU
+SnFFY0t0eFVzU2ZrOXRNQnAyRUloL2cwV2FWK2pPY0lqRVJaY25VOApFS3AvekJO
+MFBrQ3lrV1AvM3pKS2FWY1BhSC80eWxyUlhlYmNDYmxUWk9oSzliTHVDc3llT3JG
+RHI3WGNXc3pMClYxK2JTUzVWdG5wTzNNZzU4NzIwTXl6TlEvckhISFNNbDdMb0c3
+VjJJd2J4QnNNK1JwQk9YYXRlVHQrNzB0bWgKWWkrSXBUWVhrWnFXeHduZndjQ0JG
+VXp0NlZLL0JGRDNRWTRTeU5Fd2dBWUpLb1pJaHZjTkFRY0JNQjBHQ1dDRwpTQUZs
+QXdRQkFnUVFtSWdhUWNlcVgwcXJJSGhhWjBsRm9hQ0FCSUdnaVBaZURCdmF6aDBG
+WW5hTlhjU2JQOWtWCkF3NjMybDl6aVJ5Q0Y3V2d0T0RJN2NmMlpOVVovUUJQWVJv
+K21yNG5zTUhsRnFEVjdXNFo1YlppRWNTbW4wMTcKRTQ0TXZ4N3N3blp3aE83Q1dT
+eVBpZlpxbkhEanpGTVBleUd2Y3NLY01sRUJCVTJUQ0Y2V3Bzem9KNmFkcktITQpi
+bmFKL3pGYVJlUi9tWkVJWHRQb1lrRWZHMmFxamtCY0FDYWxtRG03d3cya0hqRWI3
+RVhtOVZhVjVTUGlVZ1FRCnl6SXQrSjVncWY0V2kyQ2pVTndvRlFBQUFBQUFBQUFB
+QUFBPQoKAAAAAAAAoIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhh
+bXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbST
+Al056anh68SDvozuWF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiL
+W6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD
+9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7
+gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez
+7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tb
+f7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4Hpju
+UfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaX
+S6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsI
+RBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLuju
+xjwqMN7JZnDmQz0DH920bp2/MYIDFzCCAxMCAQEwaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBglghkgB
+ZQMEAgMFAKCCAX8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B
+CQUxDxcNMjMxMTIxMjA1MjExWjBPBgkqhkiG9w0BCQQxQgRAzv/gytJZg5MF8NGW
+Rcc1Ua7H4w5a2d5YgVr34quWak4HiXnag7SdI240EevHgI7rK1RvtqbsWJVV8/rY
+qk2VJDB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD
+YWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dV
+UyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWug
+aTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
+TW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1Mg
+VGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASCAQCdisd+9uZcGkegvm0LokueVA5a
+6QTwXoszTo5bDGq7mKBKd+Lq8XCFa0VM7pNyNXnEbhFvz6426K7o9KgxlJng1ZjA
+Kh7qM1wGeYB7ILuSuY+LryFHtSdyMy6U30wwIesfEDK8vTZYI+/At1aN6RczQhM/
+/rZYZ9+NQMBD1lEFCwX5kDHRjvQOOjaOLMLHTZWyXlzF0A+WlO2F0Rw5yLrntkFk
+pHuAdIIxKuFcvkoCBnfvVhCUNCXB6p/XFQDVnSUSjj7hWWifZxIfx364GsWLqI27
+DNTrTTYMhgmnTwraRIOZuH8/YIvp2Oj4f36gcyE/G9Il5CehSYaAQPMpkBImAAAA
+AAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.future.dsig.SHA256.multipart.eml b/comm/mailnews/test/data/smime/alice.future.dsig.SHA256.multipart.eml
new file mode 100644
index 0000000000..c2096e561c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.future.dsig.SHA256.multipart.eml
@@ -0,0 +1,60 @@
+MIME-Version: 1.0
+Date: Wed, 22 Nov 2023 02:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA256
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwNlowLwYJKoZIhvcNAQkEMSIEILJrdzzDNYWQV/M1oK41/rfKXs+h
+x4nk6HPGaJpiwfmGMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBAB5kCx05RMyY/fvE
+LTQK5mVlejmq/cr9ys74Q+xV9Bk79phxzNrBYLcJpfPO0B9jESPn5DIBVx63sdt2
+dQsV19WLJf79c5N3dddHPdN6DuuQ4kmcdvBXPgagwX+99w9crxE0jW1r5O5eVZET
+1/wP0fBsjBMkUtBFeEIcoyBJLQ0w4DC6lVQJghMI46Ouc2UNDt3VFSsSyzIGpvmj
+M/twajeu68eQD3evco4eVP75SJ/jrFtRaUYLvfBy0vpBemxznDDRqu5O6Bes75tj
+bFSYCY1uW3AFfpwN7l3sn47xVlcTwYlK0DUoKqtJ6YYJW6cMf+pSZYFuUQed/v+O
+6k/oDIAAAAAAAAA=
+--------------ms030903020902020502030404--
+
diff --git a/comm/mailnews/test/data/smime/alice.mime b/comm/mailnews/test/data/smime/alice.mime
new file mode 100644
index 0000000000..ccbbc92e15
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime
@@ -0,0 +1,5 @@
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Language: en-US
+
+This is a test message from Alice to Bob.
diff --git a/comm/mailnews/test/data/smime/alice.mime.dsig.SHA1 b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA1
new file mode 100644
index 0000000000..e9dd941c89
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA1
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.mime.dsig.SHA256 b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA256
new file mode 100644
index 0000000000..3643ef3f35
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA256
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.mime.dsig.SHA384 b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA384
new file mode 100644
index 0000000000..e93f04bdc7
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA384
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.mime.dsig.SHA512 b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA512
new file mode 100644
index 0000000000..09f1bb8654
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime.dsig.SHA512
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.mime.env b/comm/mailnews/test/data/smime/alice.mime.env
new file mode 100644
index 0000000000..bca6133360
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.mime.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml
new file mode 100644
index 0000000000..c09006f695
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml
@@ -0,0 +1,108 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA1 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDYjCCA14wggJGoAMCAQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDM2WhcNMjgxMTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEgMB4GCSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAM
+BgNVBAMTBUFsaWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrph
+Ew/IHleOHmnrFjqYWNfwrnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozu
+WF+bK2ujstKVXby5RX8SYcuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qa
+P6YGaELsZoEs6fUcl3FPMMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8
+uDtjGOt+jCVZ1BJ7O1A/Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq
+46SCs+yXeaeF2M6tlDWVrhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWg
+PRRyQ33QIbsd7cmL3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8
+lGIM4ZdRDNaXy5PukmbTf4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQn
+MnqXqbeD/qaUUDUzGI/2LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvv
+nVbDYgOZXDbWRKuby3byYenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9
+ZazK3TYlAlQDMp/Iyb72WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBb
+vCga9KgL+Wnpz06keqUbtOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0D
+H920bp2/MYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkq
+hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUy
+MDVaMCMGCSqGSIb3DQEJBDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4BgkrBgEE
+AYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQG
+EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
+dzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjAN
+BgkqhkiG9w0BAQEFAASCAQCC8kXRuO8IiCkyCMhOAp+URAoBKsokWbpFG8Vmr0GF
+ge7+rOoOeBwJk+n9Lq0yCsPyOEKe1oPOPAcU48R/B6eh9wfdMcWTdmg0RxxptQIs
+E7D37ETRr4aG0kPMD5AyfGsEttlXprBwrsClr3skd05QNlFPEhEsp/SYBeNl21oJ
+hFeGq9kxDfP5VJ0vVMEbXEjdP/YiJFBiwX/es7p2BNnmzVPgquHRMtN7ZIR8y3c6
+PgHovAzxvxhVYrKuxaEpu0oKkS+CEQasNYF8nOOkT6ER9KnM9G/wA1aQflDtR9Lf
+9gn/QVb12HnVoWhUSKtQe9uhSUBVTeVrORDGqQAVtDCSAAAAAAAA
+--------------ms030903020902020502030404--
+
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYD
+VQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsN
+IXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dm
+fVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCk
+u9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarG
+hZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syG
+M7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJf
+tX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5Ja
+LHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4u
+L6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw
+3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1
+SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2T
+b52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu
++a3JMYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMDVa
+MCMGCSqGSIb3DQEJBDEWBBShyU6bIf3q21OR8eStNLnCLyWJjDB4BgkrBgEEAYI3
+EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYD
+VQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMT
+C05TUyBUZXN0IENBAgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJV
+UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzES
+MBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkq
+hkiG9w0BAQEFAASCAQBVPbj0n2gkfXUAsXRvkMVwe68zmXwhaNta7SUS/XTCnmeM
+mWTY5bTBmZdQchtIuCxWDla85L9SI/JVl4YrS1qoUX9p7IS0q3wsprFtVE0Hb7iU
+tBeJ4xBdR2oZjnFLgZjB98zZenrJjAsC0N4tTkQOUkpROmZjZUe30o9QNkf/w+60
+Thj9Nam/Z0qlrBbjsa20HNamX41AK0e784kDKcvyvEk0G6agbJcH58xQHMFm0Wc3
+tLP1at6GyrW62OeyLKoHBc39/CtZKjd4VPaKahqY7QD/TCKd6gH8d8x4klOz5k9/
+yVA1qMkFtUR1I1D3iKodLRI36e2oMkKt900GzyLvAAAAAAAA
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..dddae44229
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml
@@ -0,0 +1,107 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA1 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAEggskQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvc2lnbmVkOyBwcm90b2NvbD0i
+YXBwbGljYXRpb24vcGtjczctc2lnbmF0dXJlIjsgbWljYWxnPXNoYS0xOyBib3Vu
+ZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQiCgpU
+aGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2UgaW4gTUlN
+RSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIwNTAyMDMw
+NDA0CkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgoKVGhpcyBpcyBhIHRlc3QgbWVz
+c2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4KCi0tLS0tLS0tLS0tLS0tbXMwMzA5MDMw
+MjA5MDIwMjA1MDIwMzA0MDQKQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2Nz
+Ny1zaWduYXR1cmU7IG5hbWU9c21pbWUucDdzCkNvbnRlbnQtVHJhbnNmZXItRW5j
+b2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50OyBm
+aWxlbmFtZT1zbWltZS5wN3MKQ29udGVudC1EZXNjcmlwdGlvbjogUy9NSU1FIENy
+eXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNB
+UUV4Q3pBSkJnVXJEZ01DR2dVQU1JQUdDU3FHU0liM0RRRUhBUUFBCm9JSURZakND
+QTE0d2dnSkdvQU1DQVFJQ0FSNHdEUVlKS29aSWh2Y05BUUVMQlFBd1pERUxNQWtH
+QTFVRUJoTUMKVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdO
+VkJBY1REVTF2ZFc1MFlXbHVJRlpwWlhjeApFakFRQmdOVkJBb1RDVUpQUjFWVElF
+NVRVekVVTUJJR0ExVUVBeE1MVGxOVElGUmxjM1FnUTBFd0hoY05Nak14Ck1USXhN
+akExTURNMldoY05Namd4TVRJeE1qQTFNRE0yV2pDQmdERUxNQWtHQTFVRUJoTUNW
+Vk14RXpBUkJnTlYKQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVTF2
+ZFc1MFlXbHVJRlpwWlhjeEVqQVFCZ05WQkFvVApDVUpQUjFWVElFNVRVekVnTUI0
+R0NTcUdTSWIzRFFFSkFSWVJRV3hwWTJWQVpYaGhiWEJzWlM1amIyMHhEakFNCkJn
+TlZCQU1UQlVGc2FXTmxNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1J
+SUJDZ0tDQVFFQW5ycGgKRXcvSUhsZU9IbW5yRmpxWVdOZndybmFrbjlWSHZKVmlD
+cnlPMEdPT3BTVUZMYlNUQWwwNTZhbmg2OFNEdm96dQpXRitiSzJ1anN0S1ZYYnk1
+Ulg4U1ljdWMxdjF6MFdNR0RyMVM1ZlEvZFBFbXp1aUxXNlNzNENlTGZtaytGM3Fh
+ClA2WUdhRUxzWm9FczZmVWNsM0ZQTU1mODFLZFl5Rmx5M1ZqMnRIVkZZOFJzSldv
+RDlRenJXcGNXa3h5TDVSRDgKdUR0akdPdCtqQ1ZaMUJKN08xQS9Kb3hpazdZWnRr
+MzV2WjFvdllvQVE3UUJ6VGg3Z3FocWlpRDltRVlyd2tDcQo0NlNDcyt5WGVhZUYy
+TTZ0bERXVnJocmVNczJ6c3dtbEZMQ1pmT3IrSUF3c1NnZXo3cjZzNTZ2TFJRZTVP
+d1dnClBSUnlRMzNRSWJzZDdjbUwzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VB
+QTRJQkFRQ0hSSy9TVGZmd3VxSTgKbEdJTTRaZFJETmFYeTVQdWttYlRmNGw4RXVh
+WGpLSld3eTVsYS9UZE94cHFnNXRiZjdrMUFXeUE4OENWS2pRbgpNbnFYcWJlRC9x
+YVVVRFV6R0kvMkx0eE5aemNZNUJhVTVQYXV2SGxsRGRxNEhwanVVZklvdXB2ajcx
+WGRzWHZ2Cm5WYkRZZ09aWERiV1JLdWJ5M2J5WWVuaHVnVExzZDI3QTQvVmtlTHU5
+M3JscEphWFM2RWlleEVxUTlLTmZFZzkKWmF6SzNUWWxBbFFETXAvSXliNzJXVkNK
+bUxKMVBTc2JES2xwaU1PekxUTDhKeHNJUkJJVFdMa0xFYjZaakxCYgp2Q2dhOUtn
+TCtXbnB6MDZrZXFVYnRPZzZCQmpCT2lYdzJQWTVhUFZOUkNaZEx1anV4andxTU43
+SlpuRG1RejBECkg5MjBicDIvTVlJQzV6Q0NBdU1DQVFFd2FUQmtNUXN3Q1FZRFZR
+UUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnMKYVdadmNtNXBZVEVXTUJRR0ExVUVC
+eE1OVFc5MWJuUmhhVzRnVm1sbGR6RVNNQkFHQTFVRUNoTUpRazlIVlZNZwpUbE5U
+TVJRd0VnWURWUVFERXd0T1UxTWdWR1Z6ZENCRFFRSUJIakFKQmdVckRnTUNHZ1VB
+b0lJQlV6QVlCZ2txCmhraUc5dzBCQ1FNeEN3WUpLb1pJaHZjTkFRY0JNQndHQ1Nx
+R1NJYjNEUUVKQlRFUEZ3MHlNekV4TWpFeU1EVXkKTURWYU1DTUdDU3FHU0liM0RR
+RUpCREVXQkJRNmxzQm5PZ0crK290SlJyTkl2QUJML3RsUDlEQjRCZ2tyQmdFRQpB
+WUkzRUFReGF6QnBNR1F4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSUV3cERZ
+V3hwWm05eWJtbGhNUll3CkZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3
+RUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRBU0JnTlYKQkFNVEMwNVRVeUJVWlhO
+MElFTkJBZ0VlTUhvR0N5cUdTSWIzRFFFSkVBSUxNV3VnYVRCa01Rc3dDUVlEVlFR
+RwpFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4
+TU5UVzkxYm5SaGFXNGdWbWxsCmR6RVNNQkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRN
+UlF3RWdZRFZRUURFd3RPVTFNZ1ZHVnpkQ0JEUVFJQkhqQU4KQmdrcWhraUc5dzBC
+QVFFRkFBU0NBUUNDOGtYUnVPOElpQ2t5Q01oT0FwK1VSQW9CS3Nva1dicEZHOFZt
+cjBHRgpnZTcrck9vT2VCd0prK245THEweUNzUHlPRUtlMW9QT1BBY1U0OFIvQjZl
+aDl3ZmRNY1dUZG1nMFJ4eHB0UUlzCkU3RDM3RVRScjRhRzBrUE1ENUF5ZkdzRXR0
+bFhwckJ3cnNDbHIzc2tkMDVRTmxGUEVoRXNwL1NZQmVObDIxb0oKaEZlR3E5a3hE
+ZlA1VkowdlZNRWJYRWpkUC9ZaUpGQml3WC9lczdwMkJObm16VlBncXVIUk10Tjda
+SVI4eTNjNgpQZ0hvdkF6eHZ4aFZZckt1eGFFcHUwb0trUytDRVFhc05ZRjhuT09r
+VDZFUjlLbk05Ry93QTFhUWZsRHRSOUxmCjlnbi9RVmIxMkhuVm9XaFVTS3RRZTl1
+aFNVQlZUZVZyT1JER3FRQVZ0RENTQUFBQUFBQUEKLS0tLS0tLS0tLS0tLS1tczAz
+MDkwMzAyMDkwMjAyMDUwMjAzMDQwNC0tCgoAAAAAAACgggNfMIIDWzCCAkOgAwIB
+AgIBMjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQTAeFw0yMzExMjEyMDUwNDNaFw0yODEx
+MjEyMDUwNDNaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxHzAdBgkq
+hkiG9w0BCQEWEERhdmVAZXhhbXBsZS5jb20xDTALBgNVBAMTBERhdmUwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIS6Iyqw0hewI8Lkj+nQEhnBQfflgD
+KrW4nFcNZ5vzU92Nv553ZIFDgNMNyMoztg4irXkzl2Z9VX1xea01iA+OnWCo9w5s
+TYqqPYKJktXbit5Dr6A5Zb7//kxIB5DXirVlYK3coKS7084va31SdSYke/RgEnsN
+Woxn48WQddddAhMQ8BUQJzaQmpx6i0Kgns5NlA4NqsaFl1+f32uzOSPOy70bx9UF
+prvVx2/04LXNNQBg0DDlrbHNRYh3SJZsrFbSvVTazIYzvOiWJo/QQcOVNdHwqFTG
+y8ZCtUn8SwrIuuvARpOYnLumFFsk1fP9HMHcTqSWkl+1f4dC5IBQW7INAgMBAAEw
+DQYJKoZIhvcNAQELBQADggEBAGNN2cbQc1VjlHeTkloseW93MTh2lEi/zKlS0byK
+KEFBfSlPgp0jtQZravUxqvt8cCNpn6KCoJIJiBHozi4vptcfvvNXQJv7Ic8v8QtG
+3Tqq9A2f+PAtwInnucGO2JSYfuIc6yfd8+0347H8CPDekAcj5uBHSRiq11r0Uv3U
+kltluSa/XgVI0vNFPOMRgXyRR1tWAv4wL4YIgQR6wPVJ9LdB678AhnElq1qOY9Ql
+K42W01pVinvk5Y2AWaA/E3yAobtE13otl4GZnjqNLZNvnZ0CqmCuF85caT4fPNZN
+fA5NhnFFAGYdMkk2FPgZtHt8YFaBySo73VNFEsDvha75rckxggLnMIIC4wIBATBp
+MGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBU
+ZXN0IENBAgEyMAkGBSsOAwIaBQCgggFTMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B
+BwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwNVowIwYJKoZIhvcNAQkEMRYE
+FNqZhWCqLhrB4Ky7OPY0lZEFpBOJMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIw
+egYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxp
+Zm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBO
+U1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMA0GCSqGSIb3DQEBAQUABIIBADuT
+TJ0xvbdqdRv4vlurLQOfHd8HKalwbq4L1TLlUhrnbBhnTyA+IU2f+xHl8JVKy3/s
+IpnCDPrcg1sKu1vPEIBUgZhX3LzcEpHni0/nsD+zAFco9ED4e5Wbw+848bD/97II
+6MICkKN+m/iyyrMDurK81QW/6wvsr2sCA52YOC445G7RJCEEg6TdHXPMaE21c2J7
+n2U08LJuRKH1wG1WNDSc6A7px6VbU+otsq9hKWybj1+Z1iUF6ydlTgWTsRrdPoZD
+4VM2pKm/+TwMKe2o5IBV22455yzWGs9zTxV7alVZLBtPATS/c4z11ewlIjNuKfYx
+p0W7OBLxwEPFtmnO+gwAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml
new file mode 100644
index 0000000000..f884d97d22
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml
@@ -0,0 +1,110 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA256 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCg
+ggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwN1owLwYJKoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3QYbZ8
+NKqgsYvmT/s/qgusMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBAFv//74bXIDuvMPo
+DhpHBNPAPpSIC1AfxS2RX/zdaGmYJ+Xs5J2A/Klxug41esJLWi8Wkd7cQwDW1E98
+66OtKp4pIOnwXfJzC8kZqt8WGtN6aSJHmy2lLKEGLSvdwAG//e0uwaCN1EiNh0rL
+N05kbz7ImmOKgtA775/C2iaISs/ssqTBkbaXqGs05+xwmcUCmTe+qMjNBA3lMVJe
+DciPNnsuvRtepoDTZDpVKG56qgglKhjqjGIMXR1Gq1rZGI8tmbGwse/XCfwggcNT
+cMiht1NfJe0J9xHjuK4WiTQsInb4RZYlSpqF3O8SwEqqh3CrvFkq7+nx8OB+/Nkt
+ouqCozwAAAAAAAA=
+--------------ms030903020902020502030404--
+
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAIBBQCgggFf
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIwN1owLwYJKoZIhvcNAQkEMSIEIEn/cLPP/KB6hIiWM7paTPCOsUYl5Tz6
+Tq6Bv0iYl4seMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQ
+Agsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYD
+VQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMT
+C05TUyBUZXN0IENBAgEyMA0GCSqGSIb3DQEBAQUABIIBAGprZxSl9ugWTrekZPgY
+9D8+tnOBakCh1z6RrHSKIa+A44Bk7mzJVxADn+pGxk9yZKHOCXIKJ3dXk/2OdFDB
+NgLioA8ITJVeACJ8KlIFPAraCdG+RH3NoOmmywLw5Ep0/+0gSJ3+kDxahR6t4kBD
+sMTkQOSSgWShStAuwBAQJVQrC2TXbpgwHi1HOK1I2YLh68Sn6BRiHnH3NsE83gO+
+swsbef40En647jN0t6CksqweHKmxqoS71jOgnIQUc/Bc9uQ6y0VvWwZce83DgPF2
+K9tuW6SvPWhjJlk7uNRzE1sl87vkjZQyDCcX3JpVmNeYYIV9eBMR+VnYgxqL4IMd
+mUoAAAAAAAA=
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..2e7b02fd5c
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml
@@ -0,0 +1,108 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA256 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABIILQ0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtMjU2
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgoKVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4KCi0tLS0tLS0tLS0tLS0tbXMw
+MzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQKQ29udGVudC1UeXBlOiBhcHBsaWNhdGlv
+bi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9c21pbWUucDdzCkNvbnRlbnQtVHJhbnNm
+ZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2ht
+ZW50OyBmaWxlbmFtZT1zbWltZS5wN3MKQ29udGVudC1EZXNjcmlwdGlvbjogUy9N
+SU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFD
+QU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnRUZBRENBQmdrcWhraUc5dzBCCkJ3
+RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFnRUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3
+VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXli
+bWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCVwphV1YzTVJJd0VBWURWUVFL
+RXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05WQkFNVEMwNVRVeUJVWlhOMElFTkJNQjRY
+CkRUSXpNVEV5TVRJd05UQXpObG9YRFRJNE1URXlNVEl3TlRBek5sb3dnWUF4Q3pB
+SkJnTlZCQVlUQWxWVE1STXcKRVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZB
+WURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZV
+eUJPVTFNeElEQWVCZ2txaGtpRzl3MEJDUUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZ
+Mjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xqWlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVC
+QlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1
+MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMyMGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15
+dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JP
+QW5pMzVwClBoZDZtaittQm1oQzdHYUJMT24xSEpkeFR6REgvTlNuV01oWmN0MVk5
+clIxUldQRWJDVnFBL1VNNjFxWEZwTWMKaStVUS9MZzdZeGpyZm93bFdkUVNlenRR
+UHlhTVlwTzJHYlpOK2IyZGFMMktBRU8wQWMwNGU0S29hb29nL1poRwpLOEpBcXVP
+a2dyUHNsM21uaGRqT3JaUTFsYTRhM2pMTnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2
+K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBDRzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhr
+aUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBrMzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3
+cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAzVHNhYW9PYlczKzVOUUZzZ1BQQQpsU28w
+SnpKNmw2bTNnLzZtbEZBMU14aVA5aTdjVFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZ
+N2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJRG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdI
+ZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5zUktrUFMKalh4SVBXV3N5dDAySlFKVUF6
+S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZakRzeTB5L0NjYkNFUVNFMWk1Q3hHKwpt
+WXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxHN1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1Y
+UzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9kdEc2ZHZ6R0NBdmN3Z2dMekFnRUJNR2t3
+WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdF
+eEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VK
+UApSMVZUSUU1VFV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RR
+WUpZSVpJQVdVREJBSUJCUUNnCmdnRmZNQmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Fo
+a2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05BUWtGTVE4WERUSXoKTVRFeU1USXdOVEl3
+TjFvd0x3WUpLb1pJaHZjTkFRa0VNU0lFSUlrQkZCQWNpR2FtQzFsOHJyUTlSZjNR
+WWJaOApOS3Fnc1l2bVQvcy9xZ3VzTUhnR0NTc0dBUVFCZ2pjUUJERnJNR2t3WkRF
+TE1Ba0dBMVVFQmhNQ1ZWTXhFekFSCkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZq
+QVVCZ05WQkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlYKQkFvVENVSlBS
+MVZUSUU1VFV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d2VnWUxL
+b1pJaHZjTgpBUWtRQWdzeGE2QnBNR1F4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlE
+VlFRSUV3cERZV3hwWm05eWJtbGhNUll3CkZBWURWUVFIRXcxTmIzVnVkR0ZwYmlC
+V2FXVjNNUkl3RUFZRFZRUUtFd2xDVDBkVlV5Qk9VMU14RkRBU0JnTlYKQkFNVEMw
+NVRVeUJVWlhOMElFTkJBZ0VlTUEwR0NTcUdTSWIzRFFFQkFRVUFCSUlCQUZ2Ly83
+NGJYSUR1dk1QbwpEaHBIQk5QQVBwU0lDMUFmeFMyUlgvemRhR21ZSitYczVKMkEv
+S2x4dWc0MWVzSkxXaThXa2Q3Y1F3RFcxRTk4CjY2T3RLcDRwSU9ud1hmSnpDOGta
+cXQ4V0d0TjZhU0pIbXkybExLRUdMU3Zkd0FHLy9lMHV3YUNOMUVpTmgwckwKTjA1
+a2J6N0ltbU9LZ3RBNzc1L0MyaWFJU3Mvc3NxVEJrYmFYcUdzMDUreHdtY1VDbVRl
+K3FNak5CQTNsTVZKZQpEY2lQTm5zdXZSdGVwb0RUWkRwVktHNTZxZ2dsS2hqcWpH
+SU1YUjFHcTFyWkdJOHRtYkd3c2UvWENmd2dnY05UCmNNaWh0MU5mSmUwSjl4SGp1
+SzRXaVRRc0luYjRSWllsU3BxRjNPOFN3RXFxaDNDcnZGa3E3K254OE9CKy9Oa3QK
+b3VxQ296d0FBQUFBQUFBPQotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0LS0KCgAAAAAAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3
+DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYD
+VQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMT
+C05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
+YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2
+ZUBleGFtcGxlLmNvbTENMAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMhLojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/
+nndkgUOA0w3IyjO2DiKteTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOv
+oDllvv/+TEgHkNeKtWVgrdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDw
+FRAnNpCanHqLQqCezk2UDg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQ
+MOWtsc1FiHdIlmysVtK9VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BG
+k5icu6YUWyTV8/0cwdxOpJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAY03ZxtBzVWOUd5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq
+9TGq+3xwI2mfooKgkgmIEejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5
+wY7YlJh+4hzrJ93z7TfjsfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U8
+4xGBfJFHW1YC/jAvhgiBBHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZ
+oD8TfIChu0TXei2XgZmeOo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU
++Bm0e3xgVoHJKjvdU0USwO+FrvmtyTGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJ
+YIZIAWUDBAIBBQCgggFfMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI
+hvcNAQkFMQ8XDTIzMTEyMTIwNTIwN1owLwYJKoZIhvcNAQkEMSIEIPm7XR13fPHa
+m+1pxFqf5uXVLsdEoyx1EnutGnO2N1NzMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+ATIwegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD
+YWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dV
+UyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMA0GCSqGSIb3DQEBAQUABIIB
+ABxDL+T8yu719yQXuujVUk8aODbyfjLKTruYYMvFswfTigIe9v0sbEOZZXXJkXyS
+m9cpbt9AKv1nCfTnBd0iHkRzXaogA10Qu3lIn/OeEjXGiX55j0FtxzfctvgFCpq1
+onGh2rsJGyJs7+Aj0gFB86nClyV2mG0BBPZmh0stKh0KxDr2wECB0JZmKCNoRUyt
+kEslAlDoqqVwwSt+QjuttFRM62A3qZK6I2+L6R1q7xefDItw9U3uOvPiPbd9MyOd
+0utIu8Kg/ZxgpiRBBDNFi5a9GO/3WIUnA6qZsAjYXAY6kambIqhPbAY7yce4VT4i
+YHhOlUFzcLEzCuWB9SmKPKgAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml
new file mode 100644
index 0000000000..ce76b53615
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml
@@ -0,0 +1,110 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA384 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCg
+ggFvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIwOFowPwYJKoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/A8oC
+qKGMQ//YKsV2t/b5GjwwU78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBpMGQx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
+dGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0
+IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEF
+AASCAQA13iPQULY+PeDWIlrVfJZM6pjuYY77nYjqBajZkpQIUGVo/nz9mvdm0yA/
+BmEaSOdKOIdAgQFzlKxtVvNs9BtF+pJlZRcBhJq+eYOjbFBIv4++MyCzQWaBIFnh
+bER+OcXDH0usq2owCFOcIzaeRre8mg/kqGeVyPL2OxoHH4qio3xzJZBR03mnA0kL
+bxhw3XlF77dVLHlbFUqIIVJxYSnX09sITdfJol6sZ81lOwb84g3qxOPw1B6J83zb
+ZlFliAz1t77PLHNBHJzDjIqRjXSNaMJ9BO2suhco2C0ngfVrI1pu34duqdlIgl/2
+RCOc2wgdFLRCBiXkhR7iYuZE1YxxAAAAAAAA
+--------------ms030903020902020502030404--
+
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAICBQCgggFv
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIwOVowPwYJKoZIhvcNAQkEMTIEMJjo84gQCc/E7z0jxIomLlWRgs2MVBRK
+RBuRp3o5QYoQ0J75hOodTEpkurLruCipMzB4BgkrBgEEAYI3EAQxazBpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkqhkiG9w0BAQEFAASC
+AQCLTB7jJanT47w3rJ/WbnqobQCRf8d2xJmSL+zS642Bk5JeTSMHrRsQhxZAXs7a
+oehBpCyWuCf+TLMinfx70DXPr8zaJH4odZAwYMgbOvpL3NZ02PP0DTgMfYDCIfSJ
+Iak2tkA9TeQtXTfSZB5EgSTGqVJVtkA5eA3JLPPQ3QsNLbkRlUx+1ezp8nLjvZZI
+TpsQagK9ZiRJb5TC5Fyvl6djVfIaWLmZ7oA4STnyB9S16soY4PSMYkqaQD2HxU2m
+tSfLSrfP5hS4IA6glUym7nE5CMSnQzscivo/ul0Fqxain3wB2cq9E9xuLUUYUBOo
+HqfdUNvfhJTf9ZKd5fuFg8xcAAAAAAAA
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..6bf0dd9c1a
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml
@@ -0,0 +1,109 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA384 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABIILV0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtMzg0
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgoKVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4KCi0tLS0tLS0tLS0tLS0tbXMw
+MzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQKQ29udGVudC1UeXBlOiBhcHBsaWNhdGlv
+bi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9c21pbWUucDdzCkNvbnRlbnQtVHJhbnNm
+ZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2ht
+ZW50OyBmaWxlbmFtZT1zbWltZS5wN3MKQ29udGVudC1EZXNjcmlwdGlvbjogUy9N
+SU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFD
+QU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnSUZBRENBQmdrcWhraUc5dzBCCkJ3
+RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFnRUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3
+VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXli
+bWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCVwphV1YzTVJJd0VBWURWUVFL
+RXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05WQkFNVEMwNVRVeUJVWlhOMElFTkJNQjRY
+CkRUSXpNVEV5TVRJd05UQXpObG9YRFRJNE1URXlNVEl3TlRBek5sb3dnWUF4Q3pB
+SkJnTlZCQVlUQWxWVE1STXcKRVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZB
+WURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZV
+eUJPVTFNeElEQWVCZ2txaGtpRzl3MEJDUUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZ
+Mjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xqWlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVC
+QlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1
+MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMyMGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15
+dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JP
+QW5pMzVwClBoZDZtaittQm1oQzdHYUJMT24xSEpkeFR6REgvTlNuV01oWmN0MVk5
+clIxUldQRWJDVnFBL1VNNjFxWEZwTWMKaStVUS9MZzdZeGpyZm93bFdkUVNlenRR
+UHlhTVlwTzJHYlpOK2IyZGFMMktBRU8wQWMwNGU0S29hb29nL1poRwpLOEpBcXVP
+a2dyUHNsM21uaGRqT3JaUTFsYTRhM2pMTnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2
+K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBDRzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhr
+aUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBrMzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3
+cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAzVHNhYW9PYlczKzVOUUZzZ1BQQQpsU28w
+SnpKNmw2bTNnLzZtbEZBMU14aVA5aTdjVFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZ
+N2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJRG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdI
+ZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5zUktrUFMKalh4SVBXV3N5dDAySlFKVUF6
+S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZakRzeTB5L0NjYkNFUVNFMWk1Q3hHKwpt
+WXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxHN1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1Y
+UzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9kdEc2ZHZ6R0NBd2N3Z2dNREFnRUJNR2t3
+WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdF
+eEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VK
+UApSMVZUSUU1VFV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RR
+WUpZSVpJQVdVREJBSUNCUUNnCmdnRnZNQmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Fo
+a2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05BUWtGTVE4WERUSXoKTVRFeU1USXdOVEl3
+T0Zvd1B3WUpLb1pJaHZjTkFRa0VNVElFTU1qU2VIL0VOalMzMW56Zzh6Nk5sT1Qv
+QThvQwpxS0dNUS8vWUtzVjJ0L2I1R2p3d1U3OGVGbWV3QVlTZ1V4V0JQakI0Qmdr
+ckJnRUVBWUkzRUFReGF6QnBNR1F4CkN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZR
+UUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnUKZEdGcGJpQldh
+V1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05WQkFNVEMwNVRV
+eUJVWlhOMApJRU5CQWdFZU1Ib0dDeXFHU0liM0RRRUpFQUlMTVd1Z2FUQmtNUXN3
+Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFCkNCTUtRMkZzYVdadmNtNXBZVEVXTUJR
+R0ExVUVCeE1OVFc5MWJuUmhhVzRnVm1sbGR6RVNNQkFHQTFVRUNoTUoKUWs5SFZW
+TWdUbE5UTVJRd0VnWURWUVFERXd0T1UxTWdWR1Z6ZENCRFFRSUJIakFOQmdrcWhr
+aUc5dzBCQVFFRgpBQVNDQVFBMTNpUFFVTFkrUGVEV0lsclZmSlpNNnBqdVlZNzdu
+WWpxQmFqWmtwUUlVR1ZvL256OW12ZG0weUEvCkJtRWFTT2RLT0lkQWdRRnpsS3h0
+VnZOczlCdEYrcEpsWlJjQmhKcStlWU9qYkZCSXY0KytNeUN6UVdhQklGbmgKYkVS
+K09jWERIMHVzcTJvd0NGT2NJemFlUnJlOG1nL2txR2VWeVBMMk94b0hINHFpbzN4
+ekpaQlIwM21uQTBrTApieGh3M1hsRjc3ZFZMSGxiRlVxSUlWSnhZU25YMDlzSVRk
+ZkpvbDZzWjgxbE93Yjg0ZzNxeE9QdzFCNko4M3piClpsRmxpQXoxdDc3UExITkJI
+SnpEaklxUmpYU05hTUo5Qk8yc3VoY28yQzBuZ2ZWckkxcHUzNGR1cWRsSWdsLzIK
+UkNPYzJ3Z2RGTFJDQmlYa2hSN2lZdVpFMVl4eEFBQUFBQUFBCi0tLS0tLS0tLS0t
+LS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQtLQoKAAAAAAAAoIIDXzCCA1sw
+ggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQz
+WhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZl
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0B
+IZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dmfVV9cXmtNYgP
+jp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUm
+JHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarGhZdfn99rszkj
+zsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syGM7zoliaP0EHD
+lTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJftX+HQuSAUFuy
+DQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRI
+v8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb
++yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kY
+qtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZx
+JatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfO
+XGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu+a3JMYIDBzCC
+AwMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
+A1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQD
+EwtOU1MgVGVzdCBDQQIBMjANBglghkgBZQMEAgIFAKCCAW8wGAYJKoZIhvcNAQkD
+MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMxMTIxMjA1MjA5WjA/Bgkq
+hkiG9w0BCQQxMgQwwQwPXigu0RMNO59T+kDgLPfpY8F3pH0GOgxrfIUdElz9/pRQ
+5KiF4by/u8pxOENJMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcN
+AQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw
+FAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNV
+BAMTC05TUyBUZXN0IENBAgEyMA0GCSqGSIb3DQEBAQUABIIBAGDla+pZkw/9KQka
+Kz+5Qkrjoiit+vYmeKPiY2Mq7nv5wUofBt7OzLMbV7yYuFKXcZfVrIKf/K5U1kjz
+3L8WV5pP+QimhVmpuehUetGf6fVtmZZoaqxtlwFm7kTLnU6klHgGqF7qWWmARkU8
+9fE+m+Ilu/HM/FcCD5nFTLUcj7dT/jJloqkDiddV1b6V8kJOXy/nAtRj/4Z8Y/rP
+oTVWFnM+5ZMkKtL2TyMF92uW9Myhqwrpc/aAgh9Oihk9deY9itGgIHME67j2GCH8
+maLchZfKvC06ESqks63qDDdU0WWu00vYPI8zAlQ5yomSJ+vBM/4vFdEpd+58zh0a
+u8MSrhUAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml
new file mode 100644
index 0000000000..f642393b9d
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml
@@ -0,0 +1,110 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA512 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms030903020902020502030404"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms030903020902020502030404
+Content-Type: text/plain
+
+This is a test message from Alice to Bob.
+
+--------------ms030903020902020502030404
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA2IwggNeMIICRqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTAzNloXDTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxIDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29t
+MQ4wDAYDVQQDEwVBbGljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJ66YRMPyB5Xjh5p6xY6mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evE
+g76M7lhfmytro7LSlV28uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35p
+Phd6mj+mBmhC7GaBLOn1HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMc
+i+UQ/Lg7YxjrfowlWdQSeztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhG
+K8JAquOkgrPsl3mnhdjOrZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UH
+uTsFoD0UckN90CG7He3Ji90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k33
+8LqiPJRiDOGXUQzWl8uT7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPA
+lSo0JzJ6l6m3g/6mlFA1MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V
+3bF7751Ww2IDmVw21kSrm8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPS
+jXxIPWWsyt02JQJUAzKfyMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+
+mYywW7woGvSoC/lp6c9OpHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw
+5kM9Ax/dtG6dvzGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJP
+R1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCg
+ggF/MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIz
+MTEyMTIwNTIxMFowTwYJKoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPiv36o
+rVfLbv7L5+nHNhBNFBFfPHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnywweAYJ
+KwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWlu
+IFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EC
+AR4wDQYJKoZIhvcNAQEBBQAEggEAk2Msj1BJMbEibgaUlV2xqXRzDMn0D6EWKr19
+9+U78lHwDe683Ws3Y3nenl4lEDm8plNVupxwrq+vgB2uWoEzyNklXmY4LswVgNqH
+5xY8/pBGui1zAcpHlAFnxoNnTS8Sigydq1TZZ7rauGFaaNBQR/QFWJuxH7P+PhWG
+Cojqi78FKmxx97BeXRUvSpGqOg5ggHvPN+7qRQQArmCL/cqF3/GmthZWzXt3JZZL
+PZ17wpMDSgyZu6xKZ3589RvvIlxNKoQdzEt7WwFoIkH4N9q3GvKxjy2litolnJBu
+HL1L4WmMMyFyW4WC/oPQzbNgtMrWvXSl2GL0T48yDpbOYtS4dgAAAAAAAA==
+--------------ms030903020902020502030404--
+
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAIDBQCgggF/
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIxMVowTwYJKoZIhvcNAQkEMUIEQCKa6K8JdDElNZqgpIMHZfWM4nvfHFVv
+gM6sFIbVLqEO29SdKvmGcDYCxL1+CTREv3XgX+qkAdGMlQvNxAknLycweAYJKwYB
+BAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIw
+DQYJKoZIhvcNAQEBBQAEggEAev3JOI7z6ditEucwL+sC6ynzWfGmEv5ABLERl6N0
+i5spavf/+ZHExgmBXy0zg4OYsH4wpde+vT+mE7LbMMZm1GTMLq+qheHW/ZY06ppy
+EEyqbogtHhv13OudnDqIT7veFqJEjQXKVpG7vNGZHYUQsZ7n/EGsIFcrkjUch44O
+ZaPBJkkCM2r8JeeJWGcIaZ4XZcQvwPr67uqXXhSv59YWzNCc7GU6GXkgnncWygtg
+acuB7pbJkXUlyKwAGSMHhzBH56M2O/ef8Z9G629MaoWoCEAsXyb+qK21++OubWAM
+rsfDdQv+75ZKQdzE0gfVYWmyjP7ZHlIQzDOzWScsKP7aewAAAAAAAA==
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..5cf6af5917
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml
@@ -0,0 +1,110 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: clear-signed sig.SHA512 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABIILb0NvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3NpZ25lZDsgcHJvdG9j
+b2w9ImFwcGxpY2F0aW9uL3BrY3M3LXNpZ25hdHVyZSI7IG1pY2FsZz1zaGEtNTEy
+OyBib3VuZGFyeT0iLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIwMzA0
+MDQiCgpUaGlzIGlzIGEgY3J5cHRvZ3JhcGhpY2FsbHkgc2lnbmVkIG1lc3NhZ2Ug
+aW4gTUlNRSBmb3JtYXQuCgotLS0tLS0tLS0tLS0tLW1zMDMwOTAzMDIwOTAyMDIw
+NTAyMDMwNDA0CkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgoKVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4KCi0tLS0tLS0tLS0tLS0tbXMw
+MzA5MDMwMjA5MDIwMjA1MDIwMzA0MDQKQ29udGVudC1UeXBlOiBhcHBsaWNhdGlv
+bi9wa2NzNy1zaWduYXR1cmU7IG5hbWU9c21pbWUucDdzCkNvbnRlbnQtVHJhbnNm
+ZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2ht
+ZW50OyBmaWxlbmFtZT1zbWltZS5wN3MKQ29udGVudC1EZXNjcmlwdGlvbjogUy9N
+SU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJYjNEUUVIQXFD
+QU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnTUZBRENBQmdrcWhraUc5dzBCCkJ3
+RUFBS0NDQTJJd2dnTmVNSUlDUnFBREFnRUNBZ0VlTUEwR0NTcUdTSWIzRFFFQkN3
+VUFNR1F4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXli
+bWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCVwphV1YzTVJJd0VBWURWUVFL
+RXdsQ1QwZFZVeUJPVTFNeEZEQVNCZ05WQkFNVEMwNVRVeUJVWlhOMElFTkJNQjRY
+CkRUSXpNVEV5TVRJd05UQXpObG9YRFRJNE1URXlNVEl3TlRBek5sb3dnWUF4Q3pB
+SkJnTlZCQVlUQWxWVE1STXcKRVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZB
+WURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUkl3RUFZRApWUVFLRXdsQ1QwZFZV
+eUJPVTFNeElEQWVCZ2txaGtpRzl3MEJDUUVXRVVGc2FXTmxRR1Y0WVcxd2JHVXVZ
+Mjl0Ck1RNHdEQVlEVlFRREV3VkJiR2xqWlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVC
+QlFBRGdnRVBBRENDQVFvQ2dnRUIKQUo2NllSTVB5QjVYamg1cDZ4WTZtRmpYOEs1
+MnBKL1ZSN3lWWWdxOGp0QmpqcVVsQlMyMGt3SmRPZW1wNGV2RQpnNzZNN2xoZm15
+dHJvN0xTbFYyOHVVVi9FbUhMbk5iOWM5RmpCZzY5VXVYMFAzVHhKczdvaTF1a3JP
+QW5pMzVwClBoZDZtaittQm1oQzdHYUJMT24xSEpkeFR6REgvTlNuV01oWmN0MVk5
+clIxUldQRWJDVnFBL1VNNjFxWEZwTWMKaStVUS9MZzdZeGpyZm93bFdkUVNlenRR
+UHlhTVlwTzJHYlpOK2IyZGFMMktBRU8wQWMwNGU0S29hb29nL1poRwpLOEpBcXVP
+a2dyUHNsM21uaGRqT3JaUTFsYTRhM2pMTnM3TUpwUlN3bVh6cS9pQU1MRW9Icys2
+K3JPZXJ5MFVICnVUc0ZvRDBVY2tOOTBDRzdIZTNKaTkwQ0F3RUFBVEFOQmdrcWhr
+aUc5dzBCQVFzRkFBT0NBUUVBaDBTdjBrMzMKOExxaVBKUmlET0dYVVF6V2w4dVQ3
+cEptMDMrSmZCTG1sNHlpVnNNdVpXdjAzVHNhYW9PYlczKzVOUUZzZ1BQQQpsU28w
+SnpKNmw2bTNnLzZtbEZBMU14aVA5aTdjVFdjM0dPUVdsT1QycnJ4NVpRM2F1QjZZ
+N2xIeUtMcWI0KzlWCjNiRjc3NTFXdzJJRG1WdzIxa1NybTh0MjhtSHA0Ym9FeTdI
+ZHV3T1AxWkhpN3ZkNjVhU1dsMHVoSW5zUktrUFMKalh4SVBXV3N5dDAySlFKVUF6
+S2Z5TW0rOWxsUWlaaXlkVDByR3d5cGFZakRzeTB5L0NjYkNFUVNFMWk1Q3hHKwpt
+WXl3Vzd3b0d2U29DL2xwNmM5T3BIcWxHN1RvT2dRWXdUb2w4TmoyT1dqMVRVUW1Y
+UzdvN3NZOEtqRGV5V1p3CjVrTTlBeC9kdEc2ZHZ6R0NBeGN3Z2dNVEFnRUJNR2t3
+WkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1QKQ2tOaGJHbG1iM0p1YVdF
+eEZqQVVCZ05WQkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlZCQW9UQ1VK
+UApSMVZUSUU1VFV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RR
+WUpZSVpJQVdVREJBSURCUUNnCmdnRi9NQmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Fo
+a2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05BUWtGTVE4WERUSXoKTVRFeU1USXdOVEl4
+TUZvd1R3WUpLb1pJaHZjTkFRa0VNVUlFUUQxOVdPblgzQzlZUDhUejdnR0RrTFBp
+djM2bwpyVmZMYnY3TDUrbkhOaEJORkJGZlBIdkNMSlBQaWZ5bldhdCtTYW02dXcr
+SlhFQ2sycmFPUVJyT255d3dlQVlKCkt3WUJCQUdDTnhBRU1Xc3dhVEJrTVFzd0NR
+WURWUVFHRXdKVlV6RVRNQkVHQTFVRUNCTUtRMkZzYVdadmNtNXAKWVRFV01CUUdB
+MVVFQnhNTlRXOTFiblJoYVc0Z1ZtbGxkekVTTUJBR0ExVUVDaE1KUWs5SFZWTWdU
+bE5UTVJRdwpFZ1lEVlFRREV3dE9VMU1nVkdWemRDQkRRUUlCSGpCNkJnc3Foa2lH
+OXcwQkNSQUNDekZyb0drd1pERUxNQWtHCkExVUVCaE1DVlZNeEV6QVJCZ05WQkFn
+VENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVTF2ZFc1MFlXbHUKSUZacFpY
+Y3hFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVTUJJR0ExVUVBeE1MVGxOVElG
+UmxjM1FnUTBFQwpBUjR3RFFZSktvWklodmNOQVFFQkJRQUVnZ0VBazJNc2oxQkpN
+YkVpYmdhVWxWMnhxWFJ6RE1uMEQ2RVdLcjE5CjkrVTc4bEh3RGU2ODNXczNZM25l
+bmw0bEVEbThwbE5WdXB4d3JxK3ZnQjJ1V29FenlOa2xYbVk0THN3VmdOcUgKNXhZ
+OC9wQkd1aTF6QWNwSGxBRm54b05uVFM4U2lneWRxMVRaWjdyYXVHRmFhTkJRUi9R
+RldKdXhIN1ArUGhXRwpDb2pxaTc4RktteHg5N0JlWFJVdlNwR3FPZzVnZ0h2UE4r
+N3FSUVFBcm1DTC9jcUYzL0dtdGhaV3pYdDNKWlpMClBaMTd3cE1EU2d5WnU2eEta
+MzU4OVJ2dklseE5Lb1FkekV0N1d3Rm9Ja0g0TjlxM0d2S3hqeTJsaXRvbG5KQnUK
+SEwxTDRXbU1NeUZ5VzRXQy9vUFF6Yk5ndE1yV3ZYU2wyR0wwVDQ4eURwYk9ZdFM0
+ZGdBQUFBQUFBQT09Ci0tLS0tLS0tLS0tLS0tbXMwMzA5MDMwMjA5MDIwMjA1MDIw
+MzA0MDQtLQoKAAAAAAAAoIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQEL
+BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
+DU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNT
+IFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzESMBAGA1UEChMJQk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4
+YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SB
+Q4DTDcjKM7YOIq15M5dmfVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+
+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2
+kJqceotCoJ7OTZQODarGhZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2x
+zUWId0iWbKxW0r1U2syGM7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7
+phRbJNXz/RzB3E6klpJftX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7
+fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiU
+mH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8
+kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8
+gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7
+fGBWgckqO91TRRLA74Wu+a3JMYIDFzCCAxMCAQEwaTBkMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAG
+A1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBglghkgB
+ZQMEAgMFAKCCAX8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0B
+CQUxDxcNMjMxMTIxMjA1MjExWjBPBgkqhkiG9w0BCQQxQgRAByhD0hpw3j+n2w9y
+ZknUWmx8n4Wy2/t/6zB/F3YsF/1af6Cxz7w7QoW59OvPVmFfhOPIOxrj3C5cFm2m
+4pfq1zB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD
+YWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dV
+UyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMHoGCyqGSIb3DQEJEAILMWug
+aTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
+TW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1Mg
+VGVzdCBDQQIBMjANBgkqhkiG9w0BAQEFAASCAQBUHCgfyMRgBgrqs3uTjzYtIZRI
+2grYveQQMka98xQOUK0vQgpzZEJCLmlv8A/crkzjAGE9xkXkSS3ul6UKDp4vpIpt
+/WG66ew9XE9dThXAHFtFlmNKjL44HAov85iTpDHbYLFJIXbrinjyP0BCK71bhF4I
+WDRLahRaj/+uIK5YWACILHIoeIGzjJp3EQEUS4NRKFS39zutSa+G88BlOHSQSLKU
+2FinO7oFvr/C3tKKVvVV9AKsIlm1KQNemzDVMFbhCo5dz23Fn6G2MtVUEfCUJ9J5
+THWPoTYU3u1QMFmWXCwDabQfjcxZkPObI1319IqAiEF959OjesIXJHMVM+LsAAAA
+AAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml
new file mode 100644
index 0000000000..7cd2374913
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml
@@ -0,0 +1,99 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA1 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-1; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAER0NvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIGEgdGVzdCBt
+ZXNzYWdlIGZyb20gQWxpY2UgdG8gQm9iLg0KAAAAAAAAoIIDYjCCA14wggJGoAMC
+AQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgx
+MTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4G
+CSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfw
+rnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozuWF+bK2ujstKVXby5RX8S
+Ycuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FP
+MMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/
+Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWV
+rhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QID
+AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbT
+f4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2
+LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3by
+YenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72
+WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUb
+tOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0DH920bp2/MYIC5zCCAuMC
+AQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMDVaMCMGCSqGSIb3DQEJ
+BDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4BgkrBgEEAYI3EAQxazBpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASC
+AQCC8kXRuO8IiCkyCMhOAp+URAoBKsokWbpFG8Vmr0GFge7+rOoOeBwJk+n9Lq0y
+CsPyOEKe1oPOPAcU48R/B6eh9wfdMcWTdmg0RxxptQIsE7D37ETRr4aG0kPMD5Ay
+fGsEttlXprBwrsClr3skd05QNlFPEhEsp/SYBeNl21oJhFeGq9kxDfP5VJ0vVMEb
+XEjdP/YiJFBiwX/es7p2BNnmzVPgquHRMtN7ZIR8y3c6PgHovAzxvxhVYrKuxaEp
+u0oKkS+CEQasNYF8nOOkT6ER9KnM9G/wA1aQflDtR9Lf9gn/QVb12HnVoWhUSKtQ
+e9uhSUBVTeVrORDGqQAVtDCSAAAAAAAA
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAA
+oIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMC
+VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
+EjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMx
+MTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Qk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYD
+VQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsN
+IXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dm
+fVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCk
+u9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarG
+hZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syG
+M7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJf
+tX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5Ja
+LHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4u
+L6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw
+3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1
+SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2T
+b52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu
++a3JMYIC5zCCAuMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMDVa
+MCMGCSqGSIb3DQEJBDEWBBR+/Hmem/9+Cxq2rA64h6iPIJjFVzB4BgkrBgEEAYI3
+EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYD
+VQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMT
+C05TUyBUZXN0IENBAgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJV
+UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzES
+MBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkq
+hkiG9w0BAQEFAASCAQCN31EIUUnt4otrXECvg6CaEbVXc0++9g6wuqdTCs/JumlM
+mp4YI+v2QynVgRE+z1jUaGMvd2+oV3i0YsxP2l1HmgizvPeaULkK0sEJ/kDNG089
+Lu0NytAz7V3rN9wuoQQtKTkFvW/x0R6/sBN5i7aDFL/JKagb4jZQk6VTYfDBPFbK
+9Aw+XqCxIAKUqC40D57bDQV0uLfionpIyCIoFdIXgEDw96ILvy3ZfzWBeWWIudvx
+fSO5NurTG1FaLEC7wMwnW6TCFpXgIKiAbIQ6XTc/Dw/SOMVskl1gSR360nc7SSld
+MDCiExRb8OftDjbE97jFT+5o8JgR0h61BVzKvcv2AAAAAAAA
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..e8453479e3
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml
@@ -0,0 +1,102 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA1 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAEggohQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wa2NzNy1taW1lOyBuYW1l
+PXNtaW1lLnA3bTsKICAgIHNtaW1lLXR5cGU9c2lnbmVkLWRhdGEKQ29udGVudC1U
+cmFuc2Zlci1FbmNvZGluZzogYmFzZTY0CkNvbnRlbnQtRGlzcG9zaXRpb246IGF0
+dGFjaG1lbnQ7IGZpbGVuYW1lPXNtaW1lLnA3bQpDb250ZW50LURlc2NyaXB0aW9u
+OiBTL01JTUUgQ3J5cHRvZ3JhcGhpYyBTaWduYXR1cmUKCk1JQUdDU3FHU0liM0RR
+RUhBcUNBTUlBQ0FRRXhDekFKQmdVckRnTUNHZ1VBTUlBR0NTcUdTSWIzRFFFSEFh
+Q0EKSklBRVIwTnZiblJsYm5RdFZIbHdaVG9nZEdWNGRDOXdiR0ZwYmcwS0RRcFVh
+R2x6SUdseklHRWdkR1Z6ZENCdApaWE56WVdkbElHWnliMjBnUVd4cFkyVWdkRzhn
+UW05aUxnMEtBQUFBQUFBQW9JSURZakNDQTE0d2dnSkdvQU1DCkFRSUNBUjR3RFFZ
+SktvWklodmNOQVFFTEJRQXdaREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFn
+VENrTmgKYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNURFUxdmRXNTBZV2x1SUZacFpY
+Y3hFakFRQmdOVkJBb1RDVUpQUjFWVApJRTVUVXpFVU1CSUdBMVVFQXhNTFRsTlRJ
+RlJsYzNRZ1EwRXdIaGNOTWpNeE1USXhNakExTURNMldoY05Namd4Ck1USXhNakEx
+TURNMldqQ0JnREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xt
+YjNKdWFXRXgKRmpBVUJnTlZCQWNURFUxdmRXNTBZV2x1SUZacFpYY3hFakFRQmdO
+VkJBb1RDVUpQUjFWVElFNVRVekVnTUI0RwpDU3FHU0liM0RRRUpBUllSUVd4cFky
+VkFaWGhoYlhCc1pTNWpiMjB4RGpBTUJnTlZCQU1UQlVGc2FXTmxNSUlCCklqQU5C
+Z2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFucnBoRXcvSUhsZU9I
+bW5yRmpxWVdOZncKcm5ha245Vkh2SlZpQ3J5TzBHT09wU1VGTGJTVEFsMDU2YW5o
+NjhTRHZvenVXRitiSzJ1anN0S1ZYYnk1Ulg4UwpZY3VjMXYxejBXTUdEcjFTNWZR
+L2RQRW16dWlMVzZTczRDZUxmbWsrRjNxYVA2WUdhRUxzWm9FczZmVWNsM0ZQCk1N
+ZjgxS2RZeUZseTNWajJ0SFZGWThSc0pXb0Q5UXpyV3BjV2t4eUw1UkQ4dUR0akdP
+dCtqQ1ZaMUJKN08xQS8KSm94aWs3WVp0azM1dloxb3ZZb0FRN1FCelRoN2dxaHFp
+aUQ5bUVZcndrQ3E0NlNDcyt5WGVhZUYyTTZ0bERXVgpyaHJlTXMyenN3bWxGTENa
+Zk9yK0lBd3NTZ2V6N3I2czU2dkxSUWU1T3dXZ1BSUnlRMzNRSWJzZDdjbUwzUUlE
+CkFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNIUksvU1RmZnd1cUk4bEdJ
+TTRaZFJETmFYeTVQdWttYlQKZjRsOEV1YVhqS0pXd3k1bGEvVGRPeHBxZzV0YmY3
+azFBV3lBODhDVktqUW5NbnFYcWJlRC9xYVVVRFV6R0kvMgpMdHhOWnpjWTVCYVU1
+UGF1dkhsbERkcTRIcGp1VWZJb3Vwdmo3MVhkc1h2dm5WYkRZZ09aWERiV1JLdWJ5
+M2J5Clllbmh1Z1RMc2QyN0E0L1ZrZUx1OTNybHBKYVhTNkVpZXhFcVE5S05mRWc5
+WmF6SzNUWWxBbFFETXAvSXliNzIKV1ZDSm1MSjFQU3NiREtscGlNT3pMVEw4Snhz
+SVJCSVRXTGtMRWI2WmpMQmJ2Q2dhOUtnTCtXbnB6MDZrZXFVYgp0T2c2QkJqQk9p
+WHcyUFk1YVBWTlJDWmRMdWp1eGp3cU1ON0pabkRtUXowREg5MjBicDIvTVlJQzV6
+Q0NBdU1DCkFRRXdhVEJrTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNCTUtR
+MkZzYVdadmNtNXBZVEVXTUJRR0ExVUUKQnhNTlRXOTFiblJoYVc0Z1ZtbGxkekVT
+TUJBR0ExVUVDaE1KUWs5SFZWTWdUbE5UTVJRd0VnWURWUVFERXd0TwpVMU1nVkdW
+emRDQkRRUUlCSGpBSkJnVXJEZ01DR2dVQW9JSUJVekFZQmdrcWhraUc5dzBCQ1FN
+eEN3WUpLb1pJCmh2Y05BUWNCTUJ3R0NTcUdTSWIzRFFFSkJURVBGdzB5TXpFeE1q
+RXlNRFV5TURWYU1DTUdDU3FHU0liM0RRRUoKQkRFV0JCUTZsc0JuT2dHKytvdEpS
+ck5JdkFCTC90bFA5REI0QmdrckJnRUVBWUkzRUFReGF6QnBNR1F4Q3pBSgpCZ05W
+QkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFI
+RXcxTmIzVnVkR0ZwCmJpQldhV1YzTVJJd0VBWURWUVFLRXdsQ1QwZFZVeUJPVTFN
+eEZEQVNCZ05WQkFNVEMwNVRVeUJVWlhOMElFTkIKQWdFZU1Ib0dDeXFHU0liM0RR
+RUpFQUlMTVd1Z2FUQmtNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNSwpR
+MkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCeE1OVFc5MWJuUmhhVzRnVm1sbGR6RVNN
+QkFHQTFVRUNoTUpRazlIClZWTWdUbE5UTVJRd0VnWURWUVFERXd0T1UxTWdWR1Z6
+ZENCRFFRSUJIakFOQmdrcWhraUc5dzBCQVFFRkFBU0MKQVFDQzhrWFJ1TzhJaUNr
+eUNNaE9BcCtVUkFvQktzb2tXYnBGRzhWbXIwR0ZnZTcrck9vT2VCd0prK245THEw
+eQpDc1B5T0VLZTFvUE9QQWNVNDhSL0I2ZWg5d2ZkTWNXVGRtZzBSeHhwdFFJc0U3
+RDM3RVRScjRhRzBrUE1ENUF5CmZHc0V0dGxYcHJCd3JzQ2xyM3NrZDA1UU5sRlBF
+aEVzcC9TWUJlTmwyMW9KaEZlR3E5a3hEZlA1VkowdlZNRWIKWEVqZFAvWWlKRkJp
+d1gvZXM3cDJCTm5telZQZ3F1SFJNdE43WklSOHkzYzZQZ0hvdkF6eHZ4aFZZckt1
+eGFFcAp1MG9La1MrQ0VRYXNOWUY4bk9Pa1Q2RVI5S25NOUcvd0ExYVFmbER0UjlM
+Zjlnbi9RVmIxMkhuVm9XaFVTS3RRCmU5dWhTVUJWVGVWck9SREdxUUFWdERDU0FB
+QUFBQUFBCgAAAAAAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUA
+MGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBU
+ZXN0IENBMB4XDTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFt
+cGxlLmNvbTENMAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBAMhLojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA
+0w3IyjO2DiKteTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+
+TEgHkNeKtWVgrdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCa
+nHqLQqCezk2UDg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1F
+iHdIlmysVtK9VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YU
+WyTV8/0cwdxOpJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
+Y03ZxtBzVWOUd5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xw
+I2mfooKgkgmIEejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+
+4hzrJ93z7TfjsfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFH
+W1YC/jAvhgiBBHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfICh
+u0TXei2XgZmeOo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xg
+VoHJKjvdU0USwO+FrvmtyTGCAucwggLjAgEBMGkwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwCQYFKw4DAhoF
+AKCCAVMwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcN
+MjMxMTIxMjA1MjA1WjAjBgkqhkiG9w0BCQQxFgQUm65DL4oFcsmGH7YBY7L/DFFu
++1IweAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRACCzFroGkw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0ECATIwDQYJKoZIhvcNAQEBBQAEggEAjy2Ul+exALQeQzkznHPkkoPpnrx9
+8fXYYVXAfnVhk8TBTIYVyRyvQ7JJUf/KY0pqq4IgIdhYLCqZq88oYvRi+DJBvG44
+TL5Q2IjZQBX5vR9NIsz0xBq0vrd6+UZkVGpkdt5b3PY9xImscrjYtYDuraPWaXw7
+8lXU2a4rvuRIhjVZ+suizg0vom3m9AE+x8lJAhBZbx0knMzBZl5ykP07sfhGPk8I
+ROhJP4BtTloTscwbVy/NEi89lQZDOjSK5dopjJikRO255Brln7PvBbmLfx+7hVPs
+QzP4e6ByJ4ppN0YvBZiQJGIAdLcD0BU2I0cuHFtYEns5S4fSDA90oG+3mAAAAAAA
+AA==
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml
new file mode 100644
index 0000000000..78fd4338f0
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml
@@ -0,0 +1,100 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA256 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAvcw
+ggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCgggFfMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwN1owLwYJ
+KoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3QYbZ8NKqgsYvmT/s/qgusMHgG
+CSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
+aWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEU
+MBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMA0GCSqGSIb3DQEBAQUABIIBAFv//74bXIDuvMPoDhpHBNPAPpSIC1AfxS2R
+X/zdaGmYJ+Xs5J2A/Klxug41esJLWi8Wkd7cQwDW1E9866OtKp4pIOnwXfJzC8kZ
+qt8WGtN6aSJHmy2lLKEGLSvdwAG//e0uwaCN1EiNh0rLN05kbz7ImmOKgtA775/C
+2iaISs/ssqTBkbaXqGs05+xwmcUCmTe+qMjNBA3lMVJeDciPNnsuvRtepoDTZDpV
+KG56qgglKhjqjGIMXR1Gq1rZGI8tmbGwse/XCfwggcNTcMiht1NfJe0J9xHjuK4W
+iTQsInb4RZYlSpqF3O8SwEqqh3CrvFkq7+nx8OB+/NktouqCozwAAAAAAAA=
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAvcwggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAIBBQCgggFf
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIwN1owLwYJKoZIhvcNAQkEMSIEIO6D4Lhj7a6vLFjjv8++1eVnXYSedIs2
+C/lOIdHE4vk4MHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQ
+Agsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYD
+VQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMT
+C05TUyBUZXN0IENBAgEyMA0GCSqGSIb3DQEBAQUABIIBAHO+nNdyQDaT7c3eJOs1
+pufm+lLOAtzZSqBceoyFFczusndRCa5EVtEExdjr8q81riBHFeMLSiRS7p3v+EF9
+UvI1ejWDUwcURUUOGjNpLC13Sy01Rtn8i9/1tTM2J1/0tN/D4/Z7QnTfyFLzltRe
+orxyFrErLhyqUwSIKknrOr2G/YWb5P8PGz++5zx/OrPAKxhWyCCzmD9Vnu/08kyo
+6D/7I9M/HY+1Jc8Z+X3/8JtkVcFIkjUE55TLFj5FY30AFr0luhlZXRk0Hcus0yQQ
+SYTsYiEbXh6QZ4i+78jiRn4oiht9Nllq5ccKQAKsXspb3nnJdzYVq5qBTRedS4Ck
+2AIAAAAAAAA=
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..75977ae5d6
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml
@@ -0,0 +1,103 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA256 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABIIKPUNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnRUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQkVkRGIyNTBaVzUwTFZSNWNHVTZJSFJsZUhRdmNHeGhh
+VzROQ2cwS1ZHaHBjeUJwY3lCaElIUmwKYzNRZ2JXVnpjMkZuWlNCbWNtOXRJRUZz
+YVdObElIUnZJRUp2WWk0TkNnQUFBQUFBQUtDQ0EySXdnZ05lTUlJQwpScUFEQWdF
+Q0FnRWVNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1HUXhDekFKQmdOVkJBWVRBbFZUTVJN
+d0VRWURWUVFJCkV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRH
+RnBiaUJXYVdWM01SSXdFQVlEVlFRS0V3bEMKVDBkVlV5Qk9VMU14RkRBU0JnTlZC
+QU1UQzA1VFV5QlVaWE4wSUVOQk1CNFhEVEl6TVRFeU1USXdOVEF6TmxvWApEVEk0
+TVRFeU1USXdOVEF6Tmxvd2dZQXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJ
+RXdwRFlXeHBabTl5CmJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdW
+M01SSXdFQVlEVlFRS0V3bENUMGRWVXlCT1UxTXgKSURBZUJna3Foa2lHOXcwQkNR
+RVdFVUZzYVdObFFHVjRZVzF3YkdVdVkyOXRNUTR3REFZRFZRUURFd1ZCYkdsagpa
+VENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKNjZZ
+Uk1QeUI1WGpoNXA2eFk2Cm1Galg4SzUycEovVlI3eVZZZ3E4anRCampxVWxCUzIw
+a3dKZE9lbXA0ZXZFZzc2TTdsaGZteXRybzdMU2xWMjgKdVVWL0VtSExuTmI5YzlG
+akJnNjlVdVgwUDNUeEpzN29pMXVrck9BbmkzNXBQaGQ2bWorbUJtaEM3R2FCTE9u
+MQpISmR4VHpESC9OU25XTWhaY3QxWTlyUjFSV1BFYkNWcUEvVU02MXFYRnBNY2kr
+VVEvTGc3WXhqcmZvd2xXZFFTCmV6dFFQeWFNWXBPMkdiWk4rYjJkYUwyS0FFTzBB
+YzA0ZTRLb2Fvb2cvWmhHSzhKQXF1T2tnclBzbDNtbmhkak8KclpRMWxhNGEzakxO
+czdNSnBSU3dtWHpxL2lBTUxFb0hzKzYrck9lcnkwVUh1VHNGb0QwVWNrTjkwQ0c3
+SGUzSgppOTBDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFoMFN2MGsz
+MzhMcWlQSlJpRE9HWFVReldsOHVUCjdwSm0wMytKZkJMbWw0eWlWc011Wld2MDNU
+c2Fhb09iVzMrNU5RRnNnUFBBbFNvMEp6SjZsNm0zZy82bWxGQTEKTXhpUDlpN2NU
+V2MzR09RV2xPVDJycng1WlEzYXVCNlk3bEh5S0xxYjQrOVYzYkY3NzUxV3cySURt
+VncyMWtTcgptOHQyOG1IcDRib0V5N0hkdXdPUDFaSGk3dmQ2NWFTV2wwdWhJbnNS
+S2tQU2pYeElQV1dzeXQwMkpRSlVBektmCnlNbSs5bGxRaVppeWRUMHJHd3lwYVlq
+RHN5MHkvQ2NiQ0VRU0UxaTVDeEcrbVl5d1c3d29HdlNvQy9scDZjOU8KcEhxbEc3
+VG9PZ1FZd1RvbDhOajJPV2oxVFVRbVhTN283c1k4S2pEZXlXWnc1a005QXgvZHRH
+NmR2ekdDQXZjdwpnZ0x6QWdFQk1Ha3daREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJC
+Z05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVCkJnTlZCQWNURFUxdmRXNTBZV2x1
+SUZacFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVTUJJR0ExVUUKQXhN
+TFRsTlRJRlJsYzNRZ1EwRUNBUjR3RFFZSllJWklBV1VEQkFJQkJRQ2dnZ0ZmTUJn
+R0NTcUdTSWIzRFFFSgpBekVMQmdrcWhraUc5dzBCQndFd0hBWUpLb1pJaHZjTkFR
+a0ZNUThYRFRJek1URXlNVEl3TlRJd04xb3dMd1lKCktvWklodmNOQVFrRU1TSUVJ
+SWtCRkJBY2lHYW1DMWw4cnJROVJmM1FZYlo4TktxZ3NZdm1UL3MvcWd1c01IZ0cK
+Q1NzR0FRUUJnamNRQkRGck1Ha3daREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05W
+QkFnVENrTmhiR2xtYjNKdQphV0V4RmpBVUJnTlZCQWNURFUxdmRXNTBZV2x1SUZa
+cFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVCk1CSUdBMVVFQXhNTFRs
+TlRJRlJsYzNRZ1EwRUNBUjR3ZWdZTEtvWklodmNOQVFrUUFnc3hhNkJwTUdReEN6
+QUoKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SWXdG
+QVlEVlFRSEV3MU5iM1Z1ZEdGcApiaUJXYVdWM01SSXdFQVlEVlFRS0V3bENUMGRW
+VXlCT1UxTXhGREFTQmdOVkJBTVRDMDVUVXlCVVpYTjBJRU5CCkFnRWVNQTBHQ1Nx
+R1NJYjNEUUVCQVFVQUJJSUJBRnYvLzc0YlhJRHV2TVBvRGhwSEJOUEFQcFNJQzFB
+ZnhTMlIKWC96ZGFHbVlKK1hzNUoyQS9LbHh1ZzQxZXNKTFdpOFdrZDdjUXdEVzFF
+OTg2Nk90S3A0cElPbndYZkp6QzhrWgpxdDhXR3RONmFTSkhteTJsTEtFR0xTdmR3
+QUcvL2UwdXdhQ04xRWlOaDByTE4wNWtiejdJbW1PS2d0QTc3NS9DCjJpYUlTcy9z
+c3FUQmtiYVhxR3MwNSt4d21jVUNtVGUrcU1qTkJBM2xNVkplRGNpUE5uc3V2UnRl
+cG9EVFpEcFYKS0c1NnFnZ2xLaGpxakdJTVhSMUdxMXJaR0k4dG1iR3dzZS9YQ2Z3
+Z2djTlRjTWlodDFOZkplMEo5eEhqdUs0VwppVFFzSW5iNFJaWWxTcHFGM084U3dF
+cXFoM0NydkZrcTcrbng4T0IrL05rdG91cUNvendBQUFBQUFBQT0KAAAAAAAAoIID
+XzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMx
+EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQ
+BgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIx
+MjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYDVQQD
+EwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsNIXsC
+PC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dmfVV9
+cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCku9PO
+L2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarGhZdf
+n99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syGM7zo
+liaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJftX+H
+QuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5JaLHlv
+dzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4uL6bX
+H77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw3pAH
+I+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1SfS3
+Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2Tb52d
+AqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu+a3J
+MYIC9zCCAvMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQw
+EgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBglghkgBZQMEAgEFAKCCAV8wGAYJKoZI
+hvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMxMTIxMjA1MjA3
+WjAvBgkqhkiG9w0BCQQxIgQgH2RoJZYGB5PpItjlIsJ9xeHXE6XVp755RlWhsHuv
+8wUweAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRACCzFroGkw
+ZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1v
+dW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRl
+c3QgQ0ECATIwDQYJKoZIhvcNAQEBBQAEggEAQx5TP15R9/KUgSUzBidOAByxXoVQ
+X7QFdNlVeZVqrNdXoOiiqC4QgROwEVrON9tDsihOnUc7Quz0CsAlVLnWYOA7PI3x
+fhsEo6zrhYkrw+epKDVYQvM23SazGkJP9Hj082t3WSvqozpc2/LTsqEk049Fitly
+JEjKBHoe+VNpZszZYE/54yMevRUbsJ5Y1WVnII0lCMHVMYnGGu8KpVIxonQH9kiR
+Sh+gJjJLCiEA4O3wn+TpcOr2Jx/2kOjS+1zpCFZW8n0XRuJTd5KLdNR3nEph10fP
+eG49hJhRcIXM8bQiOIWrPdZX4BqydXiQtLmV68PGjXfQI6tYmIKTdQLlgQAAAAAA
+AA==
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml
new file mode 100644
index 0000000000..f7bba58170
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml
@@ -0,0 +1,101 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA384 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-384; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAwcw
+ggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCgggFvMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwOVowPwYJ
+KoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/A8oCqKGMQ//YKsV2t/b5Gjww
+U78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3
+DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASCAQA2ndCjL7xWdn4M
+2k8Z2rWaYjH0Se0PdjXtzWuBx4+fz4pZem85x76yYM9jXUj2WM0FOQJoLuGAI9dW
+dBZeyqw8f9jfiesKTgxhD7sozs/b7YNARBhy72HvoMwDCFQQBNeMloXwIExabsYd
+iRjXmIkhqG0egJUxcpdXgJ+/oPa/XwHtYsnOKFUfGsr179adfsbDgf0qLoBzD+rO
+Domnv/TxXnPcw8PDQxUsXdiDqEZCg/VgjJCGudA5pXCrK8LttwMGK0RRDgLov5bc
+TTwQ8LmhqopJ+SCglxB4NnBBoNIhwdMgpyoC8jVXTaN5vjAb+EQ0SX9iWQeLLapu
+kd61mwAkAAAAAAAA
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAwcwggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAICBQCgggFv
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIwOVowPwYJKoZIhvcNAQkEMTIEMGm2gTRLynmB/epC5vth7RqIt3GkAFuA
+v9JwtV98eFMpzN755C5Vkofb/FB+ch/uAzB4BgkrBgEEAYI3EAQxazBpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkqhkiG9w0BAQEFAASC
+AQB6rYCoo7vnQmIh62slOOHl1YS9qsDRzZ/rRO6cLAheTZz+e4oAgdec5nqurDdJ
+dkmNZd+1kBpUB5P8MZ0EgfBjs6Dajn6slH+3cp56lu8FdkcBphetpfaYbJJDmAU2
+vqw9zlshSoRemvRo5MziwNWMUJZbovLMrrNo3ksVGOYj08+WCdnv0vC8Zf4x00cX
+WWG1fux6vDSvD5UijXAolh+O6D7Hy7U24Bpgy+p252b2BvT0mFDOXQgkgJdGLgpj
+iXTzKXpWaQnapHRjM9mHmj4k1JVXrGNMV4bB7VqXSnPavjaILXtJ8oZomVMVBBk3
+WLqe/skDXHPc931jHNve3ahrAAAAAAAA
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..2bca810f96
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml
@@ -0,0 +1,103 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA384 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABIIKUkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnSUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQkVkRGIyNTBaVzUwTFZSNWNHVTZJSFJsZUhRdmNHeGhh
+VzROQ2cwS1ZHaHBjeUJwY3lCaElIUmwKYzNRZ2JXVnpjMkZuWlNCbWNtOXRJRUZz
+YVdObElIUnZJRUp2WWk0TkNnQUFBQUFBQUtDQ0EySXdnZ05lTUlJQwpScUFEQWdF
+Q0FnRWVNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1HUXhDekFKQmdOVkJBWVRBbFZUTVJN
+d0VRWURWUVFJCkV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRH
+RnBiaUJXYVdWM01SSXdFQVlEVlFRS0V3bEMKVDBkVlV5Qk9VMU14RkRBU0JnTlZC
+QU1UQzA1VFV5QlVaWE4wSUVOQk1CNFhEVEl6TVRFeU1USXdOVEF6TmxvWApEVEk0
+TVRFeU1USXdOVEF6Tmxvd2dZQXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJ
+RXdwRFlXeHBabTl5CmJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdW
+M01SSXdFQVlEVlFRS0V3bENUMGRWVXlCT1UxTXgKSURBZUJna3Foa2lHOXcwQkNR
+RVdFVUZzYVdObFFHVjRZVzF3YkdVdVkyOXRNUTR3REFZRFZRUURFd1ZCYkdsagpa
+VENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKNjZZ
+Uk1QeUI1WGpoNXA2eFk2Cm1Galg4SzUycEovVlI3eVZZZ3E4anRCampxVWxCUzIw
+a3dKZE9lbXA0ZXZFZzc2TTdsaGZteXRybzdMU2xWMjgKdVVWL0VtSExuTmI5YzlG
+akJnNjlVdVgwUDNUeEpzN29pMXVrck9BbmkzNXBQaGQ2bWorbUJtaEM3R2FCTE9u
+MQpISmR4VHpESC9OU25XTWhaY3QxWTlyUjFSV1BFYkNWcUEvVU02MXFYRnBNY2kr
+VVEvTGc3WXhqcmZvd2xXZFFTCmV6dFFQeWFNWXBPMkdiWk4rYjJkYUwyS0FFTzBB
+YzA0ZTRLb2Fvb2cvWmhHSzhKQXF1T2tnclBzbDNtbmhkak8KclpRMWxhNGEzakxO
+czdNSnBSU3dtWHpxL2lBTUxFb0hzKzYrck9lcnkwVUh1VHNGb0QwVWNrTjkwQ0c3
+SGUzSgppOTBDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFoMFN2MGsz
+MzhMcWlQSlJpRE9HWFVReldsOHVUCjdwSm0wMytKZkJMbWw0eWlWc011Wld2MDNU
+c2Fhb09iVzMrNU5RRnNnUFBBbFNvMEp6SjZsNm0zZy82bWxGQTEKTXhpUDlpN2NU
+V2MzR09RV2xPVDJycng1WlEzYXVCNlk3bEh5S0xxYjQrOVYzYkY3NzUxV3cySURt
+VncyMWtTcgptOHQyOG1IcDRib0V5N0hkdXdPUDFaSGk3dmQ2NWFTV2wwdWhJbnNS
+S2tQU2pYeElQV1dzeXQwMkpRSlVBektmCnlNbSs5bGxRaVppeWRUMHJHd3lwYVlq
+RHN5MHkvQ2NiQ0VRU0UxaTVDeEcrbVl5d1c3d29HdlNvQy9scDZjOU8KcEhxbEc3
+VG9PZ1FZd1RvbDhOajJPV2oxVFVRbVhTN283c1k4S2pEZXlXWnc1a005QXgvZHRH
+NmR2ekdDQXdjdwpnZ01EQWdFQk1Ha3daREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJC
+Z05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVCkJnTlZCQWNURFUxdmRXNTBZV2x1
+SUZacFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVTUJJR0ExVUUKQXhN
+TFRsTlRJRlJsYzNRZ1EwRUNBUjR3RFFZSllJWklBV1VEQkFJQ0JRQ2dnZ0Z2TUJn
+R0NTcUdTSWIzRFFFSgpBekVMQmdrcWhraUc5dzBCQndFd0hBWUpLb1pJaHZjTkFR
+a0ZNUThYRFRJek1URXlNVEl3TlRJd09Wb3dQd1lKCktvWklodmNOQVFrRU1USUVN
+TWpTZUgvRU5qUzMxbnpnOHo2TmxPVC9BOG9DcUtHTVEvL1lLc1YydC9iNUdqd3cK
+VTc4ZUZtZXdBWVNnVXhXQlBqQjRCZ2tyQmdFRUFZSTNFQVF4YXpCcE1HUXhDekFK
+QmdOVkJBWVRBbFZUTVJNdwpFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZ
+RFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdWM01SSXdFQVlEClZRUUtFd2xDVDBkVlV5
+Qk9VMU14RkRBU0JnTlZCQU1UQzA1VFV5QlVaWE4wSUVOQkFnRWVNSG9HQ3lxR1NJ
+YjMKRFFFSkVBSUxNV3VnYVRCa01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVD
+Qk1LUTJGc2FXWnZjbTVwWVRFVwpNQlFHQTFVRUJ4TU5UVzkxYm5SaGFXNGdWbWxs
+ZHpFU01CQUdBMVVFQ2hNSlFrOUhWVk1nVGxOVE1SUXdFZ1lEClZRUURFd3RPVTFN
+Z1ZHVnpkQ0JEUVFJQkhqQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0FRQTJuZENqTDd4
+V2RuNE0KMms4WjJyV2FZakgwU2UwUGRqWHR6V3VCeDQrZno0cFplbTg1eDc2eVlN
+OWpYVWoyV00wRk9RSm9MdUdBSTlkVwpkQlpleXF3OGY5amZpZXNLVGd4aEQ3c296
+cy9iN1lOQVJCaHk3Mkh2b013RENGUVFCTmVNbG9Yd0lFeGFic1lkCmlSalhtSWto
+cUcwZWdKVXhjcGRYZ0orL29QYS9Yd0h0WXNuT0tGVWZHc3IxNzlhZGZzYkRnZjBx
+TG9CekQrck8KRG9tbnYvVHhYblBjdzhQRFF4VXNYZGlEcUVaQ2cvVmdqSkNHdWRB
+NXBYQ3JLOEx0dHdNR0swUlJEZ0xvdjViYwpUVHdROExtaHFvcEorU0NnbHhCNE5u
+QkJvTklod2RNZ3B5b0M4alZYVGFONXZqQWIrRVEwU1g5aVdRZUxMYXB1CmtkNjFt
+d0FrQUFBQUFBQUEKAAAAAAAAoIIDXzCCA1swggJDoAMCAQICATIwDQYJKoZIhvcN
+AQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV
+BAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxML
+TlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQsw
+CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRh
+aW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZl
+QGV4YW1wbGUuY29tMQ0wCwYDVQQDEwREYXZlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAyEuiMqsNIXsCPC5I/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+e
+d2SBQ4DTDcjKM7YOIq15M5dmfVV9cXmtNYgPjp1gqPcObE2Kqj2CiZLV24reQ6+g
+OWW+//5MSAeQ14q1ZWCt3KCku9POL2t9UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAV
+ECc2kJqceotCoJ7OTZQODarGhZdfn99rszkjzsu9G8fVBaa71cdv9OC1zTUAYNAw
+5a2xzUWId0iWbKxW0r1U2syGM7zoliaP0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaT
+mJy7phRbJNXz/RzB3E6klpJftX+HQuSAUFuyDQIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQBjTdnG0HNVY5R3k5JaLHlvdzE4dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1
+Mar7fHAjaZ+igqCSCYgR6M4uL6bXH77zV0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nB
+jtiUmH7iHOsn3fPtN+Ox/Ajw3pAHI+bgR0kYqtda9FL91JJbZbkmv14FSNLzRTzj
+EYF8kUdbVgL+MC+GCIEEesD1SfS3Qeu/AIZxJatajmPUJSuNltNaVYp75OWNgFmg
+PxN8gKG7RNd6LZeBmZ46jS2Tb52dAqpgrhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4
+GbR7fGBWgckqO91TRRLA74Wu+a3JMYIDBzCCAwMCAQEwaTBkMQswCQYDVQQGEwJV
+UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzES
+MBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBglg
+hkgBZQMEAgIFAKCCAW8wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG
+9w0BCQUxDxcNMjMxMTIxMjA1MjA5WjA/BgkqhkiG9w0BCQQxMgQwRhhGZgW5jNk6
+Mdwp3BLFuPTFqdMa1hQ5JFC6D2Bb315GtiRGuhs5zd9wqx9fT3EjMHgGCSsGAQQB
+gjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECATIwegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYT
+AlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3
+MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEyMA0G
+CSqGSIb3DQEBAQUABIIBAGQ8ol3VEBmRG+OXDAUa7RfEhl8Ygo37IIxCu1UswP7A
+QUkg4WlFYymEVpYCTDx9WIynFhYplRWhOMKnqMnAJsJP5j9Wrh3RnehYcJfPtv37
+xIwK8zXKpe+DolVcC/wF31xjEN3dfBD6gqoJz96Z8PAtpoEsOQCa0mhW7gJB5Uqz
+qUURu3sUZ6po8hHf2Q2U+4sHe18m3Wf2o4Snt9xsA0NNXHpUCDs3abSWVv5+KCi5
+She6qD65cRkrPPjtyk5oYedIDt8VIicJxww6hn42fIEaqjRuq90JmUISMBDdem3R
+Dr3xBsFiaCogV2A8Tlw+lRqPja/iO+3RAvUi7K7jvX8AAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml
new file mode 100644
index 0000000000..c213faa60a
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml
@@ -0,0 +1,101 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA512 then clear-signed signed by dave
+Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-512; boundary="------------ms010205070902020502030809"
+
+This is a cryptographically signed message in MIME format.
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAxcw
+ggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCgggF/MBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIxMFowTwYJ
+KoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPiv36orVfLbv7L5+nHNhBNFBFf
+PHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnywweAYJKwYBBAGCNxAEMWswaTBk
+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91
+bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVz
+dCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJKoZIhvcNAQEB
+BQAEggEAk2Msj1BJMbEibgaUlV2xqXRzDMn0D6EWKr199+U78lHwDe683Ws3Y3ne
+nl4lEDm8plNVupxwrq+vgB2uWoEzyNklXmY4LswVgNqH5xY8/pBGui1zAcpHlAFn
+xoNnTS8Sigydq1TZZ7rauGFaaNBQR/QFWJuxH7P+PhWGCojqi78FKmxx97BeXRUv
+SpGqOg5ggHvPN+7qRQQArmCL/cqF3/GmthZWzXt3JZZLPZ17wpMDSgyZu6xKZ358
+9RvvIlxNKoQdzEt7WwFoIkH4N9q3GvKxjy2litolnJBuHL1L4WmMMyFyW4WC/oPQ
+zbNgtMrWvXSl2GL0T48yDpbOYtS4dgAAAAAAAA==
+
+--------------ms010205070902020502030809
+Content-Type: application/pkcs7-signature; name=smime.p7s
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7s
+Content-Description: S/MIME Cryptographic Signature
+
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwEAAKCCA18wggNbMIICQ6ADAgECAgEyMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
+aWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4X
+DTIzMTEyMTIwNTA0M1oXDTI4MTEyMTIwNTA0M1owfjELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEfMB0GCSqGSIb3DQEJARYQRGF2ZUBleGFtcGxlLmNvbTEN
+MAsGA1UEAxMERGF2ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhL
+ojKrDSF7AjwuSP6dASGcFB9+WAMqtbicVw1nm/NT3Y2/nndkgUOA0w3IyjO2DiKt
+eTOXZn1VfXF5rTWID46dYKj3DmxNiqo9gomS1duK3kOvoDllvv/+TEgHkNeKtWVg
+rdygpLvTzi9rfVJ1JiR79GASew1ajGfjxZB1110CExDwFRAnNpCanHqLQqCezk2U
+Dg2qxoWXX5/fa7M5I87LvRvH1QWmu9XHb/Tgtc01AGDQMOWtsc1FiHdIlmysVtK9
+VNrMhjO86JYmj9BBw5U10fCoVMbLxkK1SfxLCsi668BGk5icu6YUWyTV8/0cwdxO
+pJaSX7V/h0LkgFBbsg0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY03ZxtBzVWOU
+d5OSWix5b3cxOHaUSL/MqVLRvIooQUF9KU+CnSO1Bmtq9TGq+3xwI2mfooKgkgmI
+EejOLi+m1x++81dAm/shzy/xC0bdOqr0DZ/48C3Aiee5wY7YlJh+4hzrJ93z7Tfj
+sfwI8N6QByPm4EdJGKrXWvRS/dSSW2W5Jr9eBUjS80U84xGBfJFHW1YC/jAvhgiB
+BHrA9Un0t0HrvwCGcSWrWo5j1CUrjZbTWlWKe+TljYBZoD8TfIChu0TXei2XgZme
+Oo0tk2+dnQKqYK4XzlxpPh881k18Dk2GcUUAZh0ySTYU+Bm0e3xgVoHJKjvdU0US
+wO+FrvmtyTGCAxcwggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIwDQYJYIZIAWUDBAIDBQCgggF/
+MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEy
+MTIwNTIxMVowTwYJKoZIhvcNAQkEMUIEQKS244uhcwiDn0c6TR1pe8T7kflHweF9
+6hj3VWiJnRUBVcq/gJ5EfodJSCTs2bFTZzZTstJ5KXmjGf1WgeUKKW0weAYJKwYB
+BAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBMjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp
+ZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECATIw
+DQYJKoZIhvcNAQEBBQAEggEAbM4GrVIf7wQ5e/2JFxZVjU6xKgqxwPlk/9YT37XN
+bDuMkVbrWAsr8ZcPe8i9c4AN1KvxGV5DI+SHC0dd9ILRYLVpXp307z8mIl8SBuvx
+t+6mTGuZJ47XWCoNP0h7OXHTE9byoag2xbv/krwAS0MZuxvDs3gVDGpXlcJvKWaN
+F8GbEljQeWEhSv3QKGBIgVq147ndQC4TRzZ1tmYn/Ood/pr8PeqbGr35NNqYPim1
+a96vF0VQhamUkdpt8EcNgOwiShERG/AFbQUof5baawG6YQWXT2iyG4VrqoKFXK60
+NJIn1t4JhEjROP1YhW2tcGkVUcuZtlzSBaD0I+OwO4otrgAAAAAAAA==
+--------------ms010205070902020502030809--
+
+
diff --git a/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..f91a7240b8
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml
@@ -0,0 +1,104 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Dave@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA512 then opaque signed by dave
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABIIKakNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vcGtjczctbWltZTsg
+bmFtZT1zbWltZS5wN207CiAgICBzbWltZS10eXBlPXNpZ25lZC1kYXRhCkNvbnRl
+bnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0aW9u
+OiBhdHRhY2htZW50OyBmaWxlbmFtZT1zbWltZS5wN20KQ29udGVudC1EZXNjcmlw
+dGlvbjogUy9NSU1FIENyeXB0b2dyYXBoaWMgU2lnbmF0dXJlCgpNSUFHQ1NxR1NJ
+YjNEUUVIQXFDQU1JQUNBUUV4RHpBTkJnbGdoa2dCWlFNRUFnTUZBRENBQmdrcWhr
+aUc5dzBCCkJ3R2dnQ1NBQkVkRGIyNTBaVzUwTFZSNWNHVTZJSFJsZUhRdmNHeGhh
+VzROQ2cwS1ZHaHBjeUJwY3lCaElIUmwKYzNRZ2JXVnpjMkZuWlNCbWNtOXRJRUZz
+YVdObElIUnZJRUp2WWk0TkNnQUFBQUFBQUtDQ0EySXdnZ05lTUlJQwpScUFEQWdF
+Q0FnRWVNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1HUXhDekFKQmdOVkJBWVRBbFZUTVJN
+d0VRWURWUVFJCkV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRH
+RnBiaUJXYVdWM01SSXdFQVlEVlFRS0V3bEMKVDBkVlV5Qk9VMU14RkRBU0JnTlZC
+QU1UQzA1VFV5QlVaWE4wSUVOQk1CNFhEVEl6TVRFeU1USXdOVEF6TmxvWApEVEk0
+TVRFeU1USXdOVEF6Tmxvd2dZQXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJ
+RXdwRFlXeHBabTl5CmJtbGhNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdW
+M01SSXdFQVlEVlFRS0V3bENUMGRWVXlCT1UxTXgKSURBZUJna3Foa2lHOXcwQkNR
+RVdFVUZzYVdObFFHVjRZVzF3YkdVdVkyOXRNUTR3REFZRFZRUURFd1ZCYkdsagpa
+VENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKNjZZ
+Uk1QeUI1WGpoNXA2eFk2Cm1Galg4SzUycEovVlI3eVZZZ3E4anRCampxVWxCUzIw
+a3dKZE9lbXA0ZXZFZzc2TTdsaGZteXRybzdMU2xWMjgKdVVWL0VtSExuTmI5YzlG
+akJnNjlVdVgwUDNUeEpzN29pMXVrck9BbmkzNXBQaGQ2bWorbUJtaEM3R2FCTE9u
+MQpISmR4VHpESC9OU25XTWhaY3QxWTlyUjFSV1BFYkNWcUEvVU02MXFYRnBNY2kr
+VVEvTGc3WXhqcmZvd2xXZFFTCmV6dFFQeWFNWXBPMkdiWk4rYjJkYUwyS0FFTzBB
+YzA0ZTRLb2Fvb2cvWmhHSzhKQXF1T2tnclBzbDNtbmhkak8KclpRMWxhNGEzakxO
+czdNSnBSU3dtWHpxL2lBTUxFb0hzKzYrck9lcnkwVUh1VHNGb0QwVWNrTjkwQ0c3
+SGUzSgppOTBDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFoMFN2MGsz
+MzhMcWlQSlJpRE9HWFVReldsOHVUCjdwSm0wMytKZkJMbWw0eWlWc011Wld2MDNU
+c2Fhb09iVzMrNU5RRnNnUFBBbFNvMEp6SjZsNm0zZy82bWxGQTEKTXhpUDlpN2NU
+V2MzR09RV2xPVDJycng1WlEzYXVCNlk3bEh5S0xxYjQrOVYzYkY3NzUxV3cySURt
+VncyMWtTcgptOHQyOG1IcDRib0V5N0hkdXdPUDFaSGk3dmQ2NWFTV2wwdWhJbnNS
+S2tQU2pYeElQV1dzeXQwMkpRSlVBektmCnlNbSs5bGxRaVppeWRUMHJHd3lwYVlq
+RHN5MHkvQ2NiQ0VRU0UxaTVDeEcrbVl5d1c3d29HdlNvQy9scDZjOU8KcEhxbEc3
+VG9PZ1FZd1RvbDhOajJPV2oxVFVRbVhTN283c1k4S2pEZXlXWnc1a005QXgvZHRH
+NmR2ekdDQXhjdwpnZ01UQWdFQk1Ha3daREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJC
+Z05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVCkJnTlZCQWNURFUxdmRXNTBZV2x1
+SUZacFpYY3hFakFRQmdOVkJBb1RDVUpQUjFWVElFNVRVekVVTUJJR0ExVUUKQXhN
+TFRsTlRJRlJsYzNRZ1EwRUNBUjR3RFFZSllJWklBV1VEQkFJREJRQ2dnZ0YvTUJn
+R0NTcUdTSWIzRFFFSgpBekVMQmdrcWhraUc5dzBCQndFd0hBWUpLb1pJaHZjTkFR
+a0ZNUThYRFRJek1URXlNVEl3TlRJeE1Gb3dUd1lKCktvWklodmNOQVFrRU1VSUVR
+RDE5V09uWDNDOVlQOFR6N2dHRGtMUGl2MzZvclZmTGJ2N0w1K25ITmhCTkZCRmYK
+UEh2Q0xKUFBpZnluV2F0K1NhbTZ1dytKWEVDazJyYU9RUnJPbnl3d2VBWUpLd1lC
+QkFHQ054QUVNV3N3YVRCawpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JN
+S1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5UVzkxCmJuUmhhVzRnVm1sbGR6
+RVNNQkFHQTFVRUNoTUpRazlIVlZNZ1RsTlRNUlF3RWdZRFZRUURFd3RPVTFNZ1ZH
+VnoKZENCRFFRSUJIakI2QmdzcWhraUc5dzBCQ1JBQ0N6RnJvR2t3WkRFTE1Ba0dB
+MVVFQmhNQ1ZWTXhFekFSQmdOVgpCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05W
+QkFjVERVMXZkVzUwWVdsdUlGWnBaWGN4RWpBUUJnTlZCQW9UCkNVSlBSMVZUSUU1
+VFV6RVVNQklHQTFVRUF4TUxUbE5USUZSbGMzUWdRMEVDQVI0d0RRWUpLb1pJaHZj
+TkFRRUIKQlFBRWdnRUFrMk1zajFCSk1iRWliZ2FVbFYyeHFYUnpETW4wRDZFV0ty
+MTk5K1U3OGxId0RlNjgzV3MzWTNuZQpubDRsRURtOHBsTlZ1cHh3cnErdmdCMnVX
+b0V6eU5rbFhtWTRMc3dWZ05xSDV4WTgvcEJHdWkxekFjcEhsQUZuCnhvTm5UUzhT
+aWd5ZHExVFpaN3JhdUdGYWFOQlFSL1FGV0p1eEg3UCtQaFdHQ29qcWk3OEZLbXh4
+OTdCZVhSVXYKU3BHcU9nNWdnSHZQTis3cVJRUUFybUNML2NxRjMvR210aFpXelh0
+M0paWkxQWjE3d3BNRFNneVp1NnhLWjM1OAo5UnZ2SWx4TktvUWR6RXQ3V3dGb0lr
+SDROOXEzR3ZLeGp5MmxpdG9sbkpCdUhMMUw0V21NTXlGeVc0V0Mvb1BRCnpiTmd0
+TXJXdlhTbDJHTDBUNDh5RHBiT1l0UzRkZ0FBQUFBQUFBPT0KAAAAAAAAoIIDXzCC
+A1swggJDoAMCAQICATIwDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNV
+BAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1
+MDQzWhcNMjgxMTIxMjA1MDQzWjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
+aWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMg
+TlNTMR8wHQYJKoZIhvcNAQkBFhBEYXZlQGV4YW1wbGUuY29tMQ0wCwYDVQQDEwRE
+YXZlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEuiMqsNIXsCPC5I
+/p0BIZwUH35YAyq1uJxXDWeb81Pdjb+ed2SBQ4DTDcjKM7YOIq15M5dmfVV9cXmt
+NYgPjp1gqPcObE2Kqj2CiZLV24reQ6+gOWW+//5MSAeQ14q1ZWCt3KCku9POL2t9
+UnUmJHv0YBJ7DVqMZ+PFkHXXXQITEPAVECc2kJqceotCoJ7OTZQODarGhZdfn99r
+szkjzsu9G8fVBaa71cdv9OC1zTUAYNAw5a2xzUWId0iWbKxW0r1U2syGM7zoliaP
+0EHDlTXR8KhUxsvGQrVJ/EsKyLrrwEaTmJy7phRbJNXz/RzB3E6klpJftX+HQuSA
+UFuyDQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBjTdnG0HNVY5R3k5JaLHlvdzE4
+dpRIv8ypUtG8iihBQX0pT4KdI7UGa2r1Mar7fHAjaZ+igqCSCYgR6M4uL6bXH77z
+V0Cb+yHPL/ELRt06qvQNn/jwLcCJ57nBjtiUmH7iHOsn3fPtN+Ox/Ajw3pAHI+bg
+R0kYqtda9FL91JJbZbkmv14FSNLzRTzjEYF8kUdbVgL+MC+GCIEEesD1SfS3Qeu/
+AIZxJatajmPUJSuNltNaVYp75OWNgFmgPxN8gKG7RNd6LZeBmZ46jS2Tb52dAqpg
+rhfOXGk+HzzWTXwOTYZxRQBmHTJJNhT4GbR7fGBWgckqO91TRRLA74Wu+a3JMYID
+FzCCAxMCAQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBMjANBglghkgBZQMEAgMFAKCCAX8wGAYJKoZIhvcN
+AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMxMTIxMjA1MjExWjBP
+BgkqhkiG9w0BCQQxQgRA+hctDIjVkQhyhDSxpWIURZmBcgj3NOVg2HfqCidkdlGG
+0VaAooi+DkLe1bQ4QmtOb+KUit5Z7Lk8lL9vdHnv9jB4BgkrBgEEAYI3EAQxazBp
+MGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBU
+ZXN0IENBAgEyMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEG
+A1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UE
+ChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBMjANBgkqhkiG9w0B
+AQEFAASCAQCdehep/Xr5z7GD2PSCpRuA/NHvbgoGo+Fcy2glRdNo0riWnrdEzqB/
+38TFayd51SKvwcnfluyaX7dS1r7UWkT4Gc3RjtUHPlbw2Q7oQvG9yjFv18ojEH8j
+QduOmuxX8Ua/qmlwg2LYEus2tJXhAjRKgFLpmLRtS1yK+EJbsDR0GaCcpOnql/HK
+0XXvA3iQ6zSOp7sTy6vruqZLD2WVku9UtVCldvkvMtj27r2pyO039J0dhlNj9SsB
+H+S5IDboWbBsK/T2YIutizL3UZaUN82Me6+T9hyZaSe+lOARYnXOTf29s3tTX8Ka
+d1PJmURGSYMvoeTGOG7Jt/P5TOQOfZV/AAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque
new file mode 100644
index 0000000000..a6a2f98d04
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque
@@ -0,0 +1,42 @@
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAER0NvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIGEgdGVzdCBt
+ZXNzYWdlIGZyb20gQWxpY2UgdG8gQm9iLg0KAAAAAAAAoIIDYjCCA14wggJGoAMC
+AQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMTkwNzAzMTczMDA3WhcNMjQw
+NzAzMTczMDA3WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4G
+CSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4F8PZSndSoPhUMMdVU8cIVqW
+Px1Hg+OUZzfTk7vf8dnwOWJqtoT6O+5Ff432kPbIdjIm3m7Seo0eBC1s1W0z2kYz
+tPkVCzSlAfMg/5bEpkPgVSWQjn2LxsYuDHRzrr/QnwUg+ZdwIFps29PcK8XFQeaK
+Nc/rm4UYhRTZ34iEUQf7QQVSQZaxPZCunxj+qk+T1KaIKd/KCLJX9hFXybbGM5TT
+0BaxQahdtls4aUnymmhoqQ3fJJwRIB6Lth/m1TPvKE5Xj6boivY0A0DGhFWNZmxO
+vDUJa9HcMwc3MamgzZdxnp41BawL6Vkwq1SzwCcqIBaM1kVaUYvGsAGl4H5xBwID
+AQABMA0GCSqGSIb3DQEBCwUAA4IBAQA0+5CEGeVmPLNJkwDBP8DPzobniH+y/s38
+aNRu24qJuvB522+HW+6BMwZW+Le/PFojvUPSQjgL4YFDvKnjtC/YarPiO3e3Q9hi
+lMRldunE1Yl8Ldh61zbNLP/JLgHroICza2F5YIb42qzAANeJqbZT+08ybxBiUJZl
+q2v0L+S2bLPhS4Jb1sNIQi3BnL5f/nlnpi3TsWgLH9lN2CDeMK3A6qSaEgZ8pdA8
+pHLRzJGD2mHUaXvUY/lrl76xYXuwqYlJDg+0vI8T/f0RC7FHPTdbqJZjutbvpm5E
+anXzRVL/rv3YzMYz767Xp22Qf9Tk+fOpUbsIvdj4f96+ytVusgx1MYICyTCCAsUC
+AQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBNTAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMCMGCSqGSIb3DQEJBDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4Bgkr
+BgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
+MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDAS
+BgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIB
+HjANBgkqhkiG9w0BAQEFAASCAQA3Ola6Yh8cBgYxColrYgWEVCRuAfcPDc+iX7EK
+5R5WDLEJL/6nLXTn8bi7ysnDUmeocPtyzxXh6vSx30D45WbsuYV1qZbXJGfe3xgM
+yGaYI99VRe2kuduA6rqNstg8MEpyjSTd0pQVekKdQLVWRoOF+7s8v32m4m1noGu6
+jHAsXcbb00Ck1WciSjOswXFCR6xm1o2nAQwzMpMSjclbXvi0epLOJpU2ww7mLG45
+LGMIV4SYMhP0v6sL/UUpmth4piL7OmhEfzXOvmKRxhaHkREdHJK1LUoCjLZ616Uo
+ZhbZrJSMf9uyftdsJXjML++R9Xh22MOqwP1FkejdiLNzqbXJAAAAAAAA
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.eml b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.eml
new file mode 100644
index 0000000000..8dbc85328a
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA1
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
+JIAER0NvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpUaGlzIGlzIGEgdGVzdCBt
+ZXNzYWdlIGZyb20gQWxpY2UgdG8gQm9iLg0KAAAAAAAAoIIDYjCCA14wggJGoAMC
+AQICAR4wDQYJKoZIhvcNAQELBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VT
+IE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0EwHhcNMjMxMTIxMjA1MDM2WhcNMjgx
+MTIxMjA1MDM2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEgMB4G
+CSqGSIb3DQEJARYRQWxpY2VAZXhhbXBsZS5jb20xDjAMBgNVBAMTBUFsaWNlMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnrphEw/IHleOHmnrFjqYWNfw
+rnakn9VHvJViCryO0GOOpSUFLbSTAl056anh68SDvozuWF+bK2ujstKVXby5RX8S
+Ycuc1v1z0WMGDr1S5fQ/dPEmzuiLW6Ss4CeLfmk+F3qaP6YGaELsZoEs6fUcl3FP
+MMf81KdYyFly3Vj2tHVFY8RsJWoD9QzrWpcWkxyL5RD8uDtjGOt+jCVZ1BJ7O1A/
+Joxik7YZtk35vZ1ovYoAQ7QBzTh7gqhqiiD9mEYrwkCq46SCs+yXeaeF2M6tlDWV
+rhreMs2zswmlFLCZfOr+IAwsSgez7r6s56vLRQe5OwWgPRRyQ33QIbsd7cmL3QID
+AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHRK/STffwuqI8lGIM4ZdRDNaXy5PukmbT
+f4l8EuaXjKJWwy5la/TdOxpqg5tbf7k1AWyA88CVKjQnMnqXqbeD/qaUUDUzGI/2
+LtxNZzcY5BaU5PauvHllDdq4HpjuUfIoupvj71XdsXvvnVbDYgOZXDbWRKuby3by
+YenhugTLsd27A4/VkeLu93rlpJaXS6EiexEqQ9KNfEg9ZazK3TYlAlQDMp/Iyb72
+WVCJmLJ1PSsbDKlpiMOzLTL8JxsIRBITWLkLEb6ZjLBbvCga9KgL+Wnpz06keqUb
+tOg6BBjBOiXw2PY5aPVNRCZdLujuxjwqMN7JZnDmQz0DH920bp2/MYIC5zCCAuMC
+AQEwaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtO
+U1MgVGVzdCBDQQIBHjAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZI
+hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzExMjEyMDUyMDVaMCMGCSqGSIb3DQEJ
+BDEWBBQ6lsBnOgG++otJRrNIvABL/tlP9DB4BgkrBgEEAYI3EAQxazBpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+Q2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9H
+VVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASC
+AQCC8kXRuO8IiCkyCMhOAp+URAoBKsokWbpFG8Vmr0GFge7+rOoOeBwJk+n9Lq0y
+CsPyOEKe1oPOPAcU48R/B6eh9wfdMcWTdmg0RxxptQIsE7D37ETRr4aG0kPMD5Ay
+fGsEttlXprBwrsClr3skd05QNlFPEhEsp/SYBeNl21oJhFeGq9kxDfP5VJ0vVMEb
+XEjdP/YiJFBiwX/es7p2BNnmzVPgquHRMtN7ZIR8y3c6PgHovAzxvxhVYrKuxaEp
+u0oKkS+CEQasNYF8nOOkT6ER9KnM9G/wA1aQflDtR9Lf9gn/QVb12HnVoWhUSKtQ
+e9uhSUBVTeVrORDGqQAVtDCSAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env
new file mode 100644
index 0000000000..2a0837b0df
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env.eml b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env.eml
new file mode 100644
index 0000000000..d9ab8a516b
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA1.opaque.env.eml
@@ -0,0 +1,77 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed then enveloped sig.SHA1
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAFg/9DcR02cz7rInvpmO9KUPTrNAhcCTV/eqVHY/qjjX1nzV
+Yoehcruqm//F4GELcywvh7HYoGvO9i4zuULNwMI2kghua6YtmS25VMGtCQbGk4u0
+z3XHEJ+nEcrKXyuKPa9+tRcadA2K0QmTE93mM/sBHt12c4x2golDzQenV6wmycVz
+PVBtLRmvA6Az6r25ekIjbV+/s49EHkzYeyF4zzH0bKtujT2GhVrN9/QJ9r6y+DC0
+cjg/uCU8Dq6tk64p69hEthN+Heg0OzGKiiJqZhtHnvj/t2dq4VMNi+thD2XG3Kon
+Io4FD/elD1P3k8Eo5pCPJHQuCYCj96PTAwpIbx8wgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQqBeY10cjBZ0V65HAaQpegqCABIIKIOhi0zMNH30s8p+29uHZ+7vC
+3bMxDgoPzwhSQ0pRaSRQSnPQ+0ViEiSBft5Df1fs3KusyePE0z/Dqfu9Hccy/771
+9Ozh93GmvWW+sKfneBjlkQR+OAm1bQCGTVYPzRv//TTLpyxA1FWG6fcmg5n3WVbM
+bSqwWxXqkiioTmfLG+F4ZIWgRrofKQf8nwM2dzIonSvAyiG69ybKDWoyTdj2oeeR
+oyf4Phyvwp+ezKiZK2tkYoiWuKzh30Rpu2J6r/VUPgh8aYvqUb2IycOarntkf0lU
+KsGywxrMlw0W656lEHb69J5wteV1QQeQqemM2KTtdj3LZW5h9ox/+Yip3WRqbK2D
+vqiRtk13u66i1W2N4OlYALreZuFV/l9NrCaLQ+BVoGU2uS1R6wz6rw3p8D86sOc3
+cz/unLeb5r/GSjWg6xc75bDflf5/lUJ9aQ07JDNE5vDbxUA+VQzlo0sT3X3rSt5N
+99oppPX1UQQlxTBwJWFPjk9uipVLi0XH5OXS0Y95wjAteIwaQQXbdRrUUALPPmoS
+PRw0oY/7dMTQ7UxbPoWkF3BNUik9rZQmCUaby9ExMZCzDyq2wV2HkRLaDq/AA5td
+BBG7crophMXL7cwaODHcqcrWW2swGk3Uqf5qed4U+XWENK9LpDWFizH7qoEG1o7x
+pgs1UP6Nowojk5srVDCCh2LwiupUvYhZglrsbfGr/ZsBrkL3JKMwPZnbOGdDGla5
+9bHyX/NepyDP/2zmYhCAaKO3y+b4Nx5bzIs1LM5WMTiI9mXKjbFet8G14wR+ZzIL
+7B8FCS8q+91SEyxgMJ9AFevuxfv0nMdoPjs825FY4HoG/dBkcoVqyoU8HNuKwIY1
+75oXX/IVcZ1uOt76r8q+Tse4Sfzojl8UK9PasuAxycrEghzumAmpapy63Y+Kb+vp
+j+ZxPqh3xHYDCGBWvmRL1K/GU//MczpdG9crJeKy2mouTnsdrbCu2pKf0Bt/GwF3
+gOWoW6hVH9n4qSrHbBOb0gtRfxxX1D01eZk9Tg9NdbU+agqqrC6EQwGUnWeaADpK
+mRFVCBu8yyiCZdlyowJ/IGck6AIDCKi6IhXPOUoxEER2Bte3jPE+1FjRIHVwf4AJ
+bF1LlI+8v/vBmJrc3Lk9+6YM7a1+93Zu1Wgc3trvea6bEcozz1qnYcvyBsvHWju7
+egPJr6gcaQCqAWyir9XxANxNXqTS3unkurZW3pzIBMWwx02Q5xqkg56as9j2Pecx
+jaTURubKVqPRcGXlOPNJQmVvv/6xLaYOhblpaAE7wJ33/3GXQBNEFEYsQT1A6Esf
+VOHkLbB5Js6T7F+2h+9pi9jgdZSmb5XIKYwnDLw4zfF2lWnzJX3ncY+o55qOIk/3
+pj0PltwllzagTcb0sb5PJqRXBRpXqX/uxnF2IljRm6hZYgS6LF1YoIAN7bkh9wMB
+uCPxchYkfJCsMvY746h3MhxG6E9dKXRaEt7YtwoWZ05jJ4HNcZ98YJj9D5O75FB/
+SVIvlyQOGMQeFb269bLD1tZ9FxeR5KHkWMsV4cBbFMU3vopijfLDrfQJ83aqWCay
+GUji4bZpW1q4Hq3/hg1lZ3nkeOyd0eX89ugqxgH80BRosuVzbJhWqR/4+bQDdY8c
+9+/zkh4KE0pZZvUwixdwnfYPfTqkecZAlUi+9t1LkgBnj3BAUkn5O49H9hSkIJUI
+yO3G752VjPhEndztRPqE4zQ5YM4fqnWliQgn+H65Tcz2wyfIjMuOoAuBr4q+51Fg
+S9d3UMuAeAT+929gpfkckP5/St5oNew352yfdf9E4bgx+/WwPjfREh7ymQxBUvhW
+WiZKkekwVkbxnrynp3VYh4o+gO/2xaYPEObUtt0u7u8DLtGCFpwSEn6aUpGdS5tz
+ChAX2Xm2TZUpLerTR46SjOdfM5gLR6DNOFXDAqQeooBwMcTxZ5uTQO0WDbnSEUhi
+9BXvktCRBMkOh+S4YxoM9f4pL69vX7PPYUr5jyPdR7MS3+OfKLV9G1IVMRIJUsY+
+ykdWkIDBUtLvG2yWKvk6O+4909fGnU51nRqRhtIzA5U+j9rUtBDsGOdjoJPxrWVc
+7R9+hhi3fSdPEXjU5WhW/Sw0jUjZTkwu58yEzNmshFGwbw7dw1ErWk0I5kxNYqOH
+5k4nQpZXODszf/ZnaKMbwS0rkLiqBlyrd14wd2JqosNSmsSbP54X53PGDR963iFm
+MiHGXKjEY/ssFt/6d+UfoOolSPa0rP2IwXaCVJMgHFz764PJNOag8T8q+uyLJutv
+q/3YFmHXOextU9LN3lYiEFwuTQgHf+/63TFc2UXo3bz5bx0VId1tenCejVLNHYqq
+HZqI6CYLAklqeIGvWrZ10UGbpU1oPuPDSPmu3dfASlYUSPAAsjLU7rGSmzvwcSHI
+j0oXv4I19HFbDXHBAoGecZGqinybFZ/09fF2l3nUUY+fwhN4kBrRQkmuovSCq7eg
+Tp5T7b1DpVAeqe7M49GmOa6mWQolkTMvOXJAaKFCB2ACLvCzDhUShwn8XH732nUX
+6uSaNcGmNSY52p2eTdS/jJD6OPcizGlDzlUflUVEelxJD6Ngz+9VBBEbCa8MkRYi
+RGcSCWtt7RAKWlzu9AFeSpe/XmJef2CYtZ6IXLQ1c/44Kj2KwsQpSVyJzo7oCnXg
+o+1WOHfVZ+0UC1mkMpMAE4XoONr4HFjiIePd3MUZarSb0Kt5/vo5lRpOstd58Nr9
+0ROYJ3HHios5O9WxuSFQLICFGhWomnDEBoBcmUJwLnwpFDO2et6PhJn7aCd4t9t7
+H0/fpWbhvamFuHQf4CVK1RmJz02508nGpcS/xGwy9FXpew3Ehq0TkQbzjduwd2SY
+blEQsIhZONR5Hv+gAxnHYTRsABeSkej8/SU3qmiSbcD/UK1ynFhpnkHA7yV+G8bS
+lXEj+zWtEMNuEnJ4G8+ZLXwSGI9nWEw8fnUb82cp0RirNgGc8dKuOYmx7mq6UKqd
++D2jcrZY4eJrAdOUQ8Iv+QYM91pWJzm4qhk8jEkOriod48Q3boeSaqRQZG6jU7Sy
+BNJq4ekdolLctkxk8D0P95CICoEsi1cLZ/eRI80XIk0OuL00xmgXYscJilKFE5Wv
+KRkSNLclCfh2AuhNVCvxDfQn+W/jFABMRKK9B8f5amHNk2WwwOSEXf/R3XIqWei/
+AHBCQpQQ66pWoPfTUnXxAYuaFrdTAx5O1jAcADOS3BpGnzQ8EZTMAXekCr8xS/IM
+tRFZBs2dtiLdcBEQYnYMnbIdpAxzVCiL9gD/dB90Hbjkl5/Vs10HRBoYAofYHpKa
+T/lpeYYD1x6ZvrfLARMiJb3WoUKdYgcTgL9WgoJ8BOdEeTiZNIU6NIV0LtZdGqrA
+4H/hN58SihDX2HvhSCAmx+P52f+Nz1QHfMOm5fb31DuZ+o1LnySF+XENNiBBLjEB
+bn9TF7GcuCewTHO+e7Fa+ghKyLb2hsk69c2t63UauQQQV84jgXE+Pd1WnIa2xAHs
+FwAAAAAAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque
new file mode 100644
index 0000000000..6743858212
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque
@@ -0,0 +1,43 @@
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAtkw
+ggLVAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCgggFBMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwLwYJKoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3Q
+YbZ8NKqgsYvmT/s/qgusMHgGCSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMx
+EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQ
+BgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZI
+hvcNAQkQAgsxa6BpMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
+MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDAS
+BgNVBAMTC05TUyBUZXN0IENBAgEeMA0GCSqGSIb3DQEBAQUABIIBABC3gc5l0jis
+U2prBP6YpKTjJef8Sp7RegkL37NO1GfEg8lVZiFZGnWGMfGD6fE5tbiixS3HIhnx
+isW6nO3fXwkboPDJCkfyt8C0ysSiIYpKhwKWAZ9Ed2D9Lo3D9UJLuHbpHSTGcGTr
+z4VqoVE50hXAp7LtaVt83ZT+AyLLLLvpp8bcBB2S1CvCdeGFJ3pF56zz9OEm+Sxq
++FsbbyokYg+fIETDeAUGhCRQKAU/QCvQEP3bRcxRt0yTZvX7f60ga9j5kZkbRuiO
+U5CWfXBZRAipsk1fMwkJa/bFqDfVXDXnzke/nHSfw8WtHPL9SeVMqZyS5RBi4Gjd
+fDoI7erSDAkAAAAAAAA=
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.eml b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.eml
new file mode 100644
index 0000000000..2d250d71b8
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA256
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAvcw
+ggLzAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIBBQCgggFfMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwNlowLwYJ
+KoZIhvcNAQkEMSIEIIkBFBAciGamC1l8rrQ9Rf3QYbZ8NKqgsYvmT/s/qgusMHgG
+CSsGAQQBgjcQBDFrMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
+aWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEU
+MBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wegYLKoZIhvcNAQkQAgsxa6BpMGQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENB
+AgEeMA0GCSqGSIb3DQEBAQUABIIBAIZk5LSUiVMZeaoR/ftlgvF3wGOcJEqIlsSe
+wsvFs9CEcbZhLwP5+mX78MBtD20ZmBej3c+6ZUTdELXW6/mpvWKV9+VJpH2mXBAV
+OQSxzzLLxmxA3j4CRiOJmYG3vEfgK5oa3fCxoP8Y2j6m0WSreeWJFy4oSvDkgP4I
+RSUu86NtzkuCwf6ZD7QObkhMMvIUqMSYDd27YkmuRhIPhjuDfUsxVnEzxuvRgifJ
+QVKvpkj5lsgeVa9T3GRXIfdgOo1w/HS/QpiQHCHs0UDHLzuOkQYVNVGLTuLFZJCf
+U/Zq3Uc8HGlPKQ5lnPFMzK2mUt2DQyRDubkk102xAkEJGFsujtQAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env
new file mode 100644
index 0000000000..8678aa4990
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env.eml b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env.eml
new file mode 100644
index 0000000000..fb141a23a1
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA256.opaque.env.eml
@@ -0,0 +1,77 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed then enveloped sig.SHA256
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBALK694MHQH2cLnZMl9yM2GIU4/UiLP4h1nyfBDWPHY+DnMLm
+yNQZq9kMTdmz7azFzHKhStYI36x7Kp+WsdyOT7okX/l0u5DWTElwkFzAZmQkgnk7
+UUIwXsnt1DAt5CKmCRk10AFLtgW15Q+s3AaAsvrEH2oyt0HVa+xIZCPgysjBOBwV
+S4EEJ0H1AJiF0kpbiNVzy28TPfgo0VYYUGvOdXq1JIRBPTNl0L8XPPwoapheYZwx
+pMJ0yf0RlvAsHSRv2gGvV7S3DeOKimOp2+sy6QoLflHVktVO8o2nMOiTl7T7PUAp
+qm//CuwTBTRbQPNNxithNpVhadfSLYQuye/yrk4wgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQ5psvSjUNaCq0jgjdZq2Op6CABIIKMEnW8HaM60Sf10fUKxxAO5hn
+zlv/k3bjkGljrSSJmwCRJpf21PVDQTNrIeGAXcm3WiQSZpe5GOnKVE8CfEDLzVSS
+X60atoDpjLHis2lTbQeGk8tYjZlK0KCc+YR7ZwnJ/WNaMCHQGTe+GYov4CUSXbc9
+RTEavo22RIaDKz899HecswLtJMUNTXcCOxRK/DNyxJdtjSKFhk8rhtsCHmzEW4wb
+JJA08s5FCCWqdyhk3BCKVHHz0ASXRZ/wrRCSdYMgWDo2vgh6rWaTIo1H2O/iWhT4
+a7I0DeMQAdqnNWqpdskdMOPpX1+0UJxxp+jXThgHVqrY1eQNHjqAM8/rhGK4d7pm
+KG8RY89uiliRYmuZgD+a4n0VKAaLMbXBSF9I5Bx0VXceJaeUnDTZjNzXSet8ZU/k
+DA5zxhzJ8yDmIo+wkNc6WxwMjdCiYyArIaJ+s2mD8oIQVBne6sZURYnUKixXwh+L
+qjsqxteobHIW3z6PvdAG2O7crLnwaYx39gIJVAYHLicQaQK+NYjuu5EhGdjjKAic
+2IWsyyNGPaZX8A4V/r2uCVRz3rbimwGFq4JgT0zoyJg0tF8IVRbKNASkoKdm+3Bh
+f2fXj2fRITUjMbiG6N513hw7ieM4me1jcrEV1NTKLpQ0TAqSUJmls7o+7lQJOw+7
+YLL8pA/DTLEhP4T7YqqgUleEpicnRDfkKyvR6FPTt7ERKKEhjP60aFE5ybg5+0yf
+/XNGTf5nIMRrlVTgyd4oJth6r6B/AO/XQj14+L4EIvA0+nW0ujvO6g45fOMiqHs1
+zhQh5eNpFeySW/+MsC13cxle97eJxrC7Dx3Paov7iSVPKOPCixs5EyOe3PmOp/XY
+ny/E6GRIaqikwNeG6CQu5tziJxaxJzhKAuq1M/asYzL3nsuazJ3dfSvd5tCqTv+v
+3gNelvRffOwI2CQLUtYzqkjrX+sGSK+srls12RoDEBhXPswQfOI7E5jOCPXwXFtB
+TYyx2iu0mo31R00NPinko849SSm0WDzqB86/tOVze8+Z6zClMY2zJDL+NozcMcHw
+YFgoy58hsjX7+KX20nN6fLDzaaw5CtOV8u08Wq25CNb71OyZh9ttoONTtqI+b7L2
+LsYujjJYwIs0H1kz8N+iFcUaQKSCz1IZYl7LXajRapLRziuFp+31CdPnqk4cYptv
+n0+QyItmrD2FY/a+P9j04eZD3zMj0xSihh9XHAzAVglJBTpko7iMWGteAi4u2hi6
+JpRf523Yzf3p5mBWhhyiuKsLdskuLBmjDjL2ia5l56TsU1ol2DwTJnJ28S4x0ACV
+V4ZfgsUC3U+Ak1EeApop1MjTjAG8StSzmRGwyCW1I7wOBsbaubu6hcrE4Ekwyaei
+ti4Zu7Z3/n4ZBznVlcxlsOhl8jA4ul9YsoNMuJlfuyuGXv+V1LVyHOygeakR1AeE
+tknZ6bHP6g2QFlfGRs+n6y2D8BHaQahHGB/Ghr1r1AstkiuZ8V1lORdpgPYKDuqh
+B84rVsFRDIikhKd2GNS30t94Md+v3TmlRE6LwpKxMx5+E8KuppOtmWpA/0c6IDEh
+AcbTnl9BGGSxzhgxS9DkmlPbXc/oRzpx2ojr6GbH5TwG13397Lpgqf5Z6KncGZLY
+4piCMRANcTP89VFhCdwU02Lx6bTFWSasUq0Li3nlJcfmYqzpTUwEK0EaFi/+UzPZ
+vsrpGIwk7ycOT3gzQ0hlPHu6tiZilbtIJK2g0/RQYl5K7Uy8oZm6d8bxDKcOjEzU
+V0PsmBg9WY7Ylgn3MwQzlNRgrzivWdIKFKIwB9ILE6EB2/knkK9V+dGtIjqfx2+E
+JsPUe0yfq/crQt7j0zWe0yqyk1mA4WaphuY7R6x5kMR6KZQH8/1RgUAjuKRyPOBu
+kOvPzrp4W/xEpIdJOpKUqioC2xvxl6V5N6nmlu8615BYUIcA2eUtd9LOngOwHFvm
+50QCrRNw8kpSKWzlIkbv5UkOMUYeiP6JJPDAZC+G4WtPApx4itEcNtiB0VkFcHhN
+yn7oqaDUlXBU3OFnTw4dBrPrHnLXgkLMo16fMDncnojxv8zQgxXGE5nVwGSzKTra
+MpCFEXv+7bhP1mogDCNfe+WybsXy4yYcZKgqS2xVky+Q8dsSNaBCnC38puj2qYVg
+KQB44kWsp2QgSg0EFtNBS+DLkCqaEdIvuqjIKuz0mQVBhoBrlTOKtj+pItpI4DaZ
++Q211MFPQKcutZKQU0d6gzWU025h4oSnJrGL20R1488L0fnQ2Ky8qJxOJL27ztWM
+wkTL46vM0zvyN/uuvkwxDTt/c5wGQLKfDchcXg+9JKx1dzzzVeojwO2lxGKE01Gu
+QOgCNFAZ3RkUwvuLrfn6UsPYuvJL8UxUeUuXFE08Ngx+ACyI67iYxJ5XplsZ2sJK
+XsRgOfIhv9QU2GQvhwIIzYcgYdXCykgaUIyxQu2crs+ZGWl44wtwtyJraV5tnWri
+vtReOL2zBb7+mOKEHhl3hZvzpbcnj50aC32GELyD7zs4Du+JSzGmZjcPVYO0Twcx
+VbKRO8k2QJuu90zslUIT6S5331Vg472uJt8+xNsH9Ls8GE7zKVGzjdFPeKgepoh+
+QDf2mQfF7pShvYKdJAWPOdXSDqKHr+YkziqcLFx/Oz8h0ZCpbpj5zRak2Es+pLpx
+P3GFAOxJyVXgUmjezwLT9XMoWAD5EZEkwMZGYfrZqpKJTKJksxG/AySCWNhTsVbx
+H+Qr8Ownj86TJEmxtD4QwLrb8+4vDBpZaY5guLiujTBoYwlPUB1Hr3DXO21LDVts
+IOQlCnTRyyR+Qt06GTd3KjAOuZ+s6AGBomzkIxg0mvy1Vb0615yuNvIr1aZQfb5b
+cYhWbk6G6ERKNAHy5lJRwIsBEH02y/iFcZCNkiBLv9Np82iiV6rT5U3b9JhmOayQ
+Pql45iUYDtJJ+yjeml8duo67I4hFdB1JaljqHICZS5RsFJhfskJkENpy/hhUcSbR
+RP52o2rOEbg5smo7WXuvvJNeOfsKGgTzmWkYGTWbjNkP7ToFLj5Gv5g8m+rH68hB
+Hk0tYhYyqXJlddJfw0zsvuFa4cgqNWEN08Z2aZ82/1PhojWyGIJDyHGkqg/wF+tR
+Jbm1AEtnNB/LDcZuS4rPr8lY7NsciCkC07QWbAYZe9yufm1+WX08eV7AERkTyOz2
+TsjFpmUtJpWYNh6cQy2pngMWGf8BG5KFrvsqgiKKXWE2rGXDG4DqFDNnPR85xPMG
+FuLPQj87fAlb6pdTrox3IXjxTD9dw+iaP57SQG/ugYhnVipbGGkFG+Q9y+GAU9f1
+B6KfVAqEdyehoe5yULQebCDKyL6cg9+B2UGL2il0mhc0ngC9y/RcKVpORe8SEK90
+bROxHJ4VUyUP6RcU/mA22T9N15zCkrLhptZkpa3Ul1TDf6uFfjO8b2V1wH3NrWve
+Tsz/sMXnwFK4y6cu85PWh2k/49NEKp4CPvxRleUCTPLyWMNsgm+JUNeveW++JUgE
+EIYbDmZpsHS76UU1EkjW5jEAAAAAAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque
new file mode 100644
index 0000000000..ad985e9fe1
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque
@@ -0,0 +1,43 @@
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAukw
+ggLlAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCgggFRMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwPwYJKoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/
+A8oCqKGMQ//YKsV2t/b5GjwwU78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBp
+MGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1N
+b3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBU
+ZXN0IENBAgEeMHoGCyqGSIb3DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEG
+A1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UE
+ChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0B
+AQEFAASCAQBZv8VWQ1fJR5CTJm9YJ4Z1ybeVG7xJpZVrcM4/QZ9q9HK4+AkriLhG
+1mIrBWSUV+vrjisPpnv0NHFd+ueRlLufT3aUpaUyobkADT3uYV5CHzQVzmIQeGCF
+Kk7yiGoe6rWmMU/ejb5Qr/iO+132Rdh65IsvYzauvHv4SnT114AK2YwcLdosQLDI
+sx5OwzL817JNCB1R5RH/GTBkYpmtsLJo5APoLuQ+RuWb9h9VTHZ8VXuSx7tTYcrM
+AafavFUhOctbT3uosixXU1YuToP6l8dmto0zgRuIqSc1bu1d+U1WA0kfe0ershPU
+Cg47Smcnd1EvG/BD0ZVF17bT2KC7rwF0AAAAAAAA
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.eml b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.eml
new file mode 100644
index 0000000000..12bda4ac0e
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.eml
@@ -0,0 +1,50 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA384
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgIFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAwcw
+ggMDAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAICBQCgggFvMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIwOFowPwYJ
+KoZIhvcNAQkEMTIEMMjSeH/ENjS31nzg8z6NlOT/A8oCqKGMQ//YKsV2t/b5Gjww
+U78eFmewAYSgUxWBPjB4BgkrBgEEAYI3EAQxazBpMGQxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYD
+VQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEeMHoGCyqGSIb3
+DQEJEAILMWugaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYD
+VQQDEwtOU1MgVGVzdCBDQQIBHjANBgkqhkiG9w0BAQEFAASCAQA13iPQULY+PeDW
+IlrVfJZM6pjuYY77nYjqBajZkpQIUGVo/nz9mvdm0yA/BmEaSOdKOIdAgQFzlKxt
+VvNs9BtF+pJlZRcBhJq+eYOjbFBIv4++MyCzQWaBIFnhbER+OcXDH0usq2owCFOc
+IzaeRre8mg/kqGeVyPL2OxoHH4qio3xzJZBR03mnA0kLbxhw3XlF77dVLHlbFUqI
+IVJxYSnX09sITdfJol6sZ81lOwb84g3qxOPw1B6J83zbZlFliAz1t77PLHNBHJzD
+jIqRjXSNaMJ9BO2suhco2C0ngfVrI1pu34duqdlIgl/2RCOc2wgdFLRCBiXkhR7i
+YuZE1YxxAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env
new file mode 100644
index 0000000000..fafb42fdfb
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env.eml b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env.eml
new file mode 100644
index 0000000000..0168f70662
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA384.opaque.env.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed then enveloped sig.SHA384
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAGv07wsrAJ8GqyCfEtStVG2gueimqSU+6t5KyZfpxc12+ELG
+71E3BTnIm7swgXv0bTHOtOYLRhchdvI9V9ElhRESPUN6L4O00DqijCRf3pg0Kcfp
+z90IxzHJTlE/Jhs8dzJwlzSBIG65lsY50H/PvAbTYtsmyAfPKq4o5+A6ohKfc1yn
+PX3aiPeaFwC2kRv/QLeRg7nXB0yBrlf/HzOJwQLrQI/r1qjJ0r4R/2W/xrlQM8cS
+DFZBzqKqrV3dFctoEP6QT3KN7QyeyUuqPxiMgOr0y1t38hWhEiv9tS3qnSpkFy5n
+2vVaPxlO8+ZqLE6HuRg4HxvvrjRO9www9dYVf5swgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQTbFpe8SpoZe2KgawgRYGaKCABIIKUEaXSE2u+3LizSoe9pSfTjDD
+Z5TQikvfTZD0ihEd//o7GaN1nC+TNvnzmOKbkR2X/8w9zQtG+vmgemZhK/aIDf53
+W3V6LHrDcxK/ytXtFRdHATM54i2MMK+9JIwFSXGptjCr2RjWPN4WXw/UlgS7tnMI
+QMGUByzIfns9cR1e1Ofk5/zLSUt0NNanAaMZ60xgX5c1nPbHwzAalh3nv0hRFRSm
+LuZ1Sgc1NNK+5Y8aGW/XT5iqcQsaNon2k+hWPQPODTAzcV6uMvaA8KajkYH1sQzV
+WS8Df6DvvV1y29dLa9o1+HxZD+RUWCVsVQ0XdNaJa19NcPbJCtAj6SuRjDArI/z+
+B8MF/lAOVLdQSG5UQnOdG/k38Ep6xNTiD+LTGZdncrp8kexORmGL1OoWoYhB1Wqz
+R19Lr97MbjNZa68MRZFDsidPYztjJhZJyfNvu7udx46b89t/3a+1Ol2GpPdIXfGR
+hSNiJZZD8+AMtOyrhE2qOYh0gI3mIsSyyCjoBTb+oIN+8fRRtmVZziJFyG/pO4DV
+nPsaXeCSEhtbyuWsA6iHW0zSlOjWKz6UmlNK3otn/jaq1sp/MRR6owChSs+Q6gAZ
+vg8SZmU1KDg9xhcK2+O6uSmj6E0n4ULDZdbO0nT7SWA967EnK3rC5IFK7VA7P6pS
+lr0/kD88TUlkIwQI0NiX8WUEUdBxT6zBszPieTXSgqOf37TmbmPyVnAgtHk7qFdl
+wXq/T6HXx0/nuUGgTCGo9ZfSBfWLfp5O4lolpqYAS5x3FKVjKa7d3szOxgSUj4Hj
+FsGx7N1/n+DyYiMDWC+t6QKvZUS1PvJAZn3wpp/lWkpZCN3b95E8j3MXitq54dJ6
+OcsmEpwpJ8/VlY5Gij2wlsZPcP2dTUWSskLzOmZq3JI/UvABd1yARscbH9fePUiv
+8gSXFFzbuLZG57oQ/D8A+ATOZ3w2fHCX3prbnC5wc8D6hMZkUHapS3ihQk3JGxlx
+l9k1jCEM8/4yRf9QpLEXWfpfSeLHvAm8m0WPhmS+9jTroflxZrOmnlhKX/GGsjRz
+2t71ZOK3cXmlF5vSVvOsxC/oROfv3PUfflOINBBhdzC7ELEgzpYOzlLPBeOXx92w
+4Qj1xTzeqs/JnD5bG1TpoLiAuJbnnaLpWxPv2d1Lfv8zvmPyStyrY+NrPYSOIcXg
+RBFXuFzzaIyvjSWoFd4CzOZULsIV2U+EV94khsiktWRgOc47Mj/54XTSHSOmSeJS
+csehclTo738ldOG4ocA2xUCQvpiYdcmj9jkgh5QnqLajQozMvweNp2Lohb5ByglR
+B/ANyAGkRGcOVGCwxWpeaqT3ndlX9Ckhevnt1W9wAbFKv43YxkCa97z+rFsh1caa
+W/vOZymJSY9C0ZAOpF/ANW1oOKnV6/PgL1NnPWKAjg+x7Z+5oz0vYQ7puIvaFicp
+fPtT28T7mwBeIJJSW7jcjvuRsjpZtRj0NUB5O1rPHKiJEdCv8Xv88Lf+6VKapOX0
+I/jjgcg70E7L2cnpPVIgVJBkmzdEm0UYelFf+dBHxxTekM/9mHIm1sbN0BFXiOhX
+oa2Dw7MfyD81NcHjkwqpLD6hlqFOU3CHFdpV6YcslPdV6DupVcUSZPBptcwsxZr/
+RonfxNqOnMNi+he2/0whi0q3lME/H2WCdJaheVue9EoZApv+27GWcDQbZRW3j3uM
+E/BLlTd23cTBrrFlBhgmBZ/WoS9Rgo9EmQ/Av4TNFJ/IbUlTEfuMCdIVsr2tusjr
+R09bI8faiQhU+zC1Bui39DQfr59khc3o6OJe0eVjU3xGoVV86Q6uiXCODkflCy70
+eEN4CslJSn/ugG9oDTdPM9LFAd01FMO3JEnCIsavxhjVZt3Xn8MjQbMgzTFMgmWi
+8zz0oPp91M2iwh9CSViksNiRRPMcNpZv/uAZHeK9VG+s9ps/UTePIXeqjlj/rUxX
+5QYzlwQF+VMgPA9UiZEwDIETXyajhQgexHWwzt1gOUxGazfLJuWQpA527Oqxeyrs
+I/83tnEKW2Jno9qMtgkvIhhIg8oM0YSTA/7jAjgFADOPtjkvbXu6znhwZEVWpBIw
+wCCQuoQLDs7BAxc9+owNtRXyxqQ5qj8eNaYHiOTLI/9L8kbxhLNLst1Vv2DqOOLF
+BeoW7skg6/9QPIrI8dXMEhfUOywf4xFtwlUS9GQ7JX6VR0rOatWUyBdYwzSNcArd
+bqm8LUjStN/VfW3pca4SYOcYPey4lF5R7PZqj/cOC7FDKnMc7bV/WEKFyi2TwQe3
+0QaBY+lxcgodv/iCXIOd2o/Q72M8hA2HUbZ3XdNLhB4hGpo8JJ6Fe0JfBzzvdquH
+nmVSC+eIJdD1ZNTWfBVfutNjeFSG2QdRNbtz422IQLf1PWMpuxYUglQoz45bp6XS
+qc51veIl3q8YqP1fo1AlpyLJKbZpJk8IMCrZ+hw85ur44xgRFqMFTEzfjis0jH3N
+kpii5BhekIxwBtKImXme2H3Jn83gP/CMDpuqXkQhuzRsdBoZMRwbM0PIcyNIJgrH
+Fh3wYoGRE5PTjb8wAEMTy6dsKtkQZQiU2G9Pe0FiW0hmfqn8n02y1qZPrxMNOm1q
+XsBxP3RCPFp3U4bTZY1mtzN4q74/V27lmv7FPKzkaZmkUzxx8B6F7gbkmlxoGfOh
+qCcZmqkDupeHaIMeyqMRMT/7SAALvQg5QCznZlGPMNkKTuQzosn4YSm2mSJAWN7t
+Qe5ZKGJHu51sT47ff8n9ZM8Y1FpWoL0yjfEaivAStZFpiWeil8mpf4CrxSWSvhgh
+egFzF5EIHWkMTl1Je3uhRZ2a4JAA/w5mB3LRUY5yPpcSQxAZIUD+UEyZKge0CTGu
+NzvbdkiWJ+MNII779kOfv2YcOKAWzEG9rquAscCLnil+EMLSZhmwx1yMWxgFZosH
+LBWQ3FSgBLqRAHW302t22ukxYLzu0uZUbVPOCU9GqytA/Oy08SLWSfxgL7csmNw6
+6afiYqt3OdpMJF/RU4Fk4waAgOuZIM+610pYZgTrIB99Dc+oBWiNfzDdG8VhjvoQ
+Ni6C6QUY8DbXgErofx/e8MYO4MJ8xlbBe3x74aYaKdVHrNlw7Jq57+qsZ7sLqmaW
+qDlxd+v11a5cMgfU+XnSEiHhu2Q140cXd11vZiOiVfacnIL/tR9QcEBYLo+vb+Cd
+kIzFxs8306Chw9jNNjUkaRJ9sG5TnIUHgNjzmlCmQ0/KH3ZQ+APICucoLwpbsgaW
+9i1kmf9JCKt1g8R7nFvPnWGN/AtsfOoVJ4uWv+TyC357XTP4SQ+gO8brVU4wYzeO
+XKJBTUyBLV3kG4L1ALuS7UjOtTER5kn6auzfTOsId9MkeMWXqNiGCcSH3hpVcmba
+kocktrblIF2/kDWrZTZlXBT3Sg2xwaZphbeS/wKn1D2dO4vED5jpyqdYbKzP+Y1o
+Qb88DGe7JmetaPXGXgtbpREunokZv/0jHhNRHO2sDQ9jp1XSQduG4dy5hEHdNi/w
+fz/vwI0KDifdlV60Nj/vIAJR7kmRTzj10Wo7LQOG7wQQJIndUORBZ4xaJ9PNxMxE
+0QAAAAAAAAAAAAA=
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque
new file mode 100644
index 0000000000..f5be55e031
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque
@@ -0,0 +1,43 @@
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTE5MDcwMzE3MzAwN1oX
+DTI0MDcwMzE3MzAwN1owgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOBfD2Up3UqD4VDDHVVP
+HCFalj8dR4PjlGc305O73/HZ8DliaraE+jvuRX+N9pD2yHYyJt5u0nqNHgQtbNVt
+M9pGM7T5FQs0pQHzIP+WxKZD4FUlkI59i8bGLgx0c66/0J8FIPmXcCBabNvT3CvF
+xUHmijXP65uFGIUU2d+IhFEH+0EFUkGWsT2Qrp8Y/qpPk9SmiCnfygiyV/YRV8m2
+xjOU09AWsUGoXbZbOGlJ8ppoaKkN3yScESAei7Yf5tUz7yhOV4+m6Ir2NANAxoRV
+jWZsTrw1CWvR3DMHNzGpoM2XcZ6eNQWsC+lZMKtUs8AnKiAWjNZFWlGLxrABpeB+
+cQcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANPuQhBnlZjyzSZMAwT/Az86G54h/
+sv7N/GjUbtuKibrwedtvh1vugTMGVvi3vzxaI71D0kI4C+GBQ7yp47Qv2Gqz4jt3
+t0PYYpTEZXbpxNWJfC3Yetc2zSz/yS4B66CAs2theWCG+NqswADXiam2U/tPMm8Q
+YlCWZatr9C/ktmyz4UuCW9bDSEItwZy+X/55Z6Yt07FoCx/ZTdgg3jCtwOqkmhIG
+fKXQPKRy0cyRg9ph1Gl71GP5a5e+sWF7sKmJSQ4PtLyPE/39EQuxRz03W6iWY7rW
+76ZuRGp180VS/6792MzGM++u16dtkH/U5PnzqVG7CL3Y+H/evsrVbrIMdTGCAvkw
+ggL1AgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCgggFhMBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwTwYJKoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPi
+v36orVfLbv7L5+nHNhBNFBFfPHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnyww
+eAYJKwYBBAGCNxAEMWswaTBkMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
+cm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNT
+MRQwEgYDVQQDEwtOU1MgVGVzdCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
+YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3Qg
+Q0ECAR4wDQYJKoZIhvcNAQEBBQAEggEAoundvRYJ9fHgAVPoxq5Bsv0R0edWrSDU
+f3tFwwKG8H7hJwMWtGugVOAUfSimlM4foDwJquqTAdY6JVN798wftuWsXT54zkUS
+iB03LpslL7r+gAIa+glmhX6tmLGxIlAa5UyJUHBXoeL3+ze1EoEC+vET5MPh7FnQ
+mC9EP7ZaB1yn8z8N/v5KGzDBC2J9ARZoZ7ckon5cxfW9K69dD+ld184cKeWhPPEl
+5cspbZc2GqUCT0iyKW7M5wuovyOH4BLoSDyh9FFqKeAsyb4g5eQ3cggTZC+rqx3y
+MqxMGaXiLOqxz26zeedkvXI/ZyNeN8zBU9Zo0djCEfCE4GgDw+Kz/wAAAAAAAA==
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.eml b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.eml
new file mode 100644
index 0000000000..ee2a880163
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.eml
@@ -0,0 +1,50 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed sig.SHA512
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=signed-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Cryptographic Signature
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0B
+BwGggCSABEdDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCg0KVGhpcyBpcyBhIHRl
+c3QgbWVzc2FnZSBmcm9tIEFsaWNlIHRvIEJvYi4NCgAAAAAAAKCCA2IwggNeMIIC
+RqADAgECAgEeMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlC
+T0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBMB4XDTIzMTEyMTIwNTAzNloX
+DTI4MTEyMTIwNTAzNlowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
+bmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIwEAYDVQQKEwlCT0dVUyBOU1Mx
+IDAeBgkqhkiG9w0BCQEWEUFsaWNlQGV4YW1wbGUuY29tMQ4wDAYDVQQDEwVBbGlj
+ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ66YRMPyB5Xjh5p6xY6
+mFjX8K52pJ/VR7yVYgq8jtBjjqUlBS20kwJdOemp4evEg76M7lhfmytro7LSlV28
+uUV/EmHLnNb9c9FjBg69UuX0P3TxJs7oi1ukrOAni35pPhd6mj+mBmhC7GaBLOn1
+HJdxTzDH/NSnWMhZct1Y9rR1RWPEbCVqA/UM61qXFpMci+UQ/Lg7YxjrfowlWdQS
+eztQPyaMYpO2GbZN+b2daL2KAEO0Ac04e4Koaoog/ZhGK8JAquOkgrPsl3mnhdjO
+rZQ1la4a3jLNs7MJpRSwmXzq/iAMLEoHs+6+rOery0UHuTsFoD0UckN90CG7He3J
+i90CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh0Sv0k338LqiPJRiDOGXUQzWl8uT
+7pJm03+JfBLml4yiVsMuZWv03TsaaoObW3+5NQFsgPPAlSo0JzJ6l6m3g/6mlFA1
+MxiP9i7cTWc3GOQWlOT2rrx5ZQ3auB6Y7lHyKLqb4+9V3bF7751Ww2IDmVw21kSr
+m8t28mHp4boEy7HduwOP1ZHi7vd65aSWl0uhInsRKkPSjXxIPWWsyt02JQJUAzKf
+yMm+9llQiZiydT0rGwypaYjDsy0y/CcbCEQSE1i5CxG+mYywW7woGvSoC/lp6c9O
+pHqlG7ToOgQYwTol8Nj2OWj1TUQmXS7o7sY8KjDeyWZw5kM9Ax/dtG6dvzGCAxcw
+ggMTAgEBMGkwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
+BgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoTCUJPR1VTIE5TUzEUMBIGA1UE
+AxMLTlNTIFRlc3QgQ0ECAR4wDQYJYIZIAWUDBAIDBQCgggF/MBgGCSqGSIb3DQEJ
+AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEyMTIwNTIxMFowTwYJ
+KoZIhvcNAQkEMUIEQD19WOnX3C9YP8Tz7gGDkLPiv36orVfLbv7L5+nHNhBNFBFf
+PHvCLJPPifynWat+Sam6uw+JXECk2raOQRrOnywweAYJKwYBBAGCNxAEMWswaTBk
+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91
+bnRhaW4gVmlldzESMBAGA1UEChMJQk9HVVMgTlNTMRQwEgYDVQQDEwtOU1MgVGVz
+dCBDQQIBHjB6BgsqhkiG9w0BCRACCzFroGkwZDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEjAQBgNVBAoT
+CUJPR1VTIE5TUzEUMBIGA1UEAxMLTlNTIFRlc3QgQ0ECAR4wDQYJKoZIhvcNAQEB
+BQAEggEAk2Msj1BJMbEibgaUlV2xqXRzDMn0D6EWKr199+U78lHwDe683Ws3Y3ne
+nl4lEDm8plNVupxwrq+vgB2uWoEzyNklXmY4LswVgNqH5xY8/pBGui1zAcpHlAFn
+xoNnTS8Sigydq1TZZ7rauGFaaNBQR/QFWJuxH7P+PhWGCojqi78FKmxx97BeXRUv
+SpGqOg5ggHvPN+7qRQQArmCL/cqF3/GmthZWzXt3JZZLPZ17wpMDSgyZu6xKZ358
+9RvvIlxNKoQdzEt7WwFoIkH4N9q3GvKxjy2litolnJBuHL1L4WmMMyFyW4WC/oPQ
+zbNgtMrWvXSl2GL0T48yDpbOYtS4dgAAAAAAAA==
+
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env
new file mode 100644
index 0000000000..b3ceab836d
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env.eml b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env.eml
new file mode 100644
index 0000000000..fce61249d0
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.sig.SHA512.opaque.env.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Date: Tue, 21 Nov 2023 20:50:06 +0000
+From: Alice@example.com
+To: Bob@example.com
+Subject: opaque-signed then enveloped sig.SHA512
+Content-Type: application/pkcs7-mime; name=smime.p7m;
+ smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename=smime.p7m
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggGFMIIBgQIBADBpMGQxCzAJBgNVBAYTAlVT
+MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRIw
+EAYDVQQKEwlCT0dVUyBOU1MxFDASBgNVBAMTC05TUyBUZXN0IENBAgEoMA0GCSqG
+SIb3DQEBAQUABIIBAF0TORPbbeaZfqKfa+mpqRuKHYtdOf6chLLFEgvJQ0GetOZ9
+ULSBWi319Rf6TIL1t9oN87gZcRuwJnVl0itPc2qvTjoWf/mErYwrq21at6BDIhoY
+m3SD3DAghIoHYR383uvuiPky9ZtawzgjDzqEabCK4yGxwlvDBGqP+J+IuJa48Q//
+mP9AivhCBfbnnFASmHNavQvBcXuypVfIDmjNw0uPO2vcnrbAk4JccFKHOj7Fks+Z
+07/LvBKrH86Fm9Pl/+RzuamS1IrYFQ3ZkSNZDeCTBHrzISUsl1f8BL04HsYXg2JD
+jrXFatfR7JzICELFEpW+ThhWsnuY5yoIL7DFXZowgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBAgQQHGHJHWI2NNlH8MLoMuSl1aCABIIKYOtfWD79w10B/7LpmfBCIVQi
+NL/UHgTdemwlW5a8aJSD49wI9Tsm7OLVg/+Byydow3qQkXHFaXxdx67cUWQ/8T8R
+l2BXw27W1D4j5bqvTT7oe6r9un/BwytgZAEv5uoFeb7wpyVTn7aAZyE5uK237pmR
+xRhVHgWwKJS1uD5IAkIgnYg4jx+n2Jlv8VIDOghBS3VCWwqCFY9p+hHudFM5PaIF
+HDjs3+gmKdT2pXIQhZ+Mtq34KiCEwIEI2TpzpIo7+C5CFhkHkng0ieozDZgDvxGw
+HEzRvOPyEhDfVxKY1v4UK8JPpo4ba/A6s/tjmiYYJuCvjlHhsKGm1cqFn2YX63Wq
+Oex9SsdctqP0Te7IGYB2SVdODryHeF96KjU/BGuFW355f+qf+EwWypmeM1FyNkbW
+6O1Pr77WtD5L67oZkQ1jJqTiqeR7cuUrr2BqCqYxTbAlQyfa9Q8lDQxTre6Rbjua
+fRMMY5wXIrqOEMFgxy3APM8kmfkCNWfKiDzRR68CslnvNcoKQP3fpC53Otm72k5N
+ahryvo9WNejzfogKSAfVjmbgtNpZwgBfK4Xv1rJ32KSljzbEsQx98coPVmvddglI
+4vTK/myPJDYwjlKSw+78aQ5eXBgzTM2KNJzXnEGIPW+cMU3Bl5XS/Jnteb/Xwudg
+bFVrVW5tW/RbOQYKEYHu5xw7kJgA5gvuKjcUEyEYIMCZ2wmiRVO6XipUmPgubke0
+3y9InlcKQV/yDoSF3TAfgFLHvvtv7VWrqNfrqSdRkJVrAl8W/s8tvm/TykTLnbwu
+nyT7jO1Ruzw4efEHnOF3eezCyDzzsPqZv6KuQYeixiQSkmyAc5zlHeNX2jXNmPrp
+yInuAjIhRm9rnTwpvAMif2TfsYCemmV82CZEDJUB+PwzRglkN+LNlhCHOEx2wUsA
+bnwxC8qD4FQlvCRVV7k0w5KdDYRmgIUzvhsX1v3DulFDZr/r1ULVJ/znDl/z8PhU
+fzrmr1vIO/Zs0Ow/IFHlGl5ACq/ubl9DUXVEMvBVna5iMLpXrmCuftecDgWVEqvo
+61Oe5NhKd6jsomKyVvUTRIrHMkkIHiF5sWdx8yrsn3jLhP9VvKHoPKQJBaCXSHIc
+ZTXlffPvOeHal+scupDZLUzFSk5ZE6EQB0n1z5j3YvGgLW7njGsd3/wGjAZqnshu
++dbq8Nm4itlCLfwH2QI+4WAiUPZotuySXAuGyxZzzaEoW/ZpKLPGU4Aq37dNwPI8
+U8kV3+K/ZWWs8RX4+0Q6s4xmf3D6bLGGJk9oDuycPNz++PbfrakWWlSrZyYRUXBR
+g89J6jM4A8xGlCsmaP+D8yz5wvUC/gSxQEHbgsQOUuMTQvzgciP6kmgC6kaqdagc
++YNQAxPzO177jca3KWERNFF1BQqMGX+1Occg4AnEJCNU4joOpcqKP3v0355TJ9wi
+9bTnzN/tN5KdzNr3Dsv0tlLwQjdb8/rSTqFry9E0yl5vMltNhWXLBYefNYoS8A9I
+uNYbuBw4U+f9fw+6ZpG7l+L4Tm2vBUAyCT3fGQpkJJHD+dG1l7DvQn4vjRGRmwSp
+YfrSKvDwFVAkMH8LAqvrNoXEzrnBznqHzlNFx5Cc9E3JYXhertq7S711o8ljEsSs
+x3vuwdoJkoRUMET4N5GEU1ZC/bPmfOAXUuUzHrOVxIgx8w8hhV2auOS/caN7euRO
+ZLPfESKe+AolheiO/Y4OfiIrIprJERqI2yGI4dThOqptcR7UYfmS/r16BBtjuTyW
+fGzY/oXV7ZAg0Wcx25kYBptFVV3WVviCD0kUy9xYpwn+e6pnV9J3INLe2uiBcoj2
+5vV60m++4A6C4CHRNGLRsCu/Nm2dNJmQ+PKwQWSBLsEaYV3keFieZ3/uzGvpAQVL
+HGjmi9KdSN5ciJ8qWkVhq+ptlyU+e/m+ZTqf7UesX3GDlkm1LojvPMeeyxEhnMPN
+qaUqotTTwIV1M6qVvXc8f78oOSE7DzCRQUHkx6UD8RI4bKCBM3mM4WOaaJBke68H
+iGbUyDG6Y0Q+S/myG9LYGSjAXxqrllfNDDTZubIStFSJbxT+OhGYY/YQ1cro6FUd
+43yi3TXFJUgm1Wb8c816jVRkJttCm72Ykbn5IRfw5Y2UOFlOFTuHbsyCoxnR+/Ul
+t20tCXevuGQVGxxIth3XkQ9+1QX/URT86+SUYCmM6h65A2mY2GzUOvwgbPdW0wbO
+1eoLMSsYPnLmVFiUu/MOxfsRPT8t2vlTSlxcXhM/i7Az5Y7VTDFD1/l23sXC2/Ik
+r1zmz7I3av1yflwmrFm4eCAXY6/p+B/9URCKrX5SlnpUkc8L/jMnqCWE1mkd4SBM
+7hwxfJU8VqmBmolGYy5RIFImc3EBNxX15ES0Tea0RE0acznJOcBbK3FJd8Ec7F7A
+mUZF7IKEGqHMMFyZRsYh62cqO/tsmuGE6YrwdVUSLhCXa//GCVWTtyERXl4Wblbw
+NLkoDfp5Yb/hSNHLoH2i/0+wBqgll21BuAT3t1CYeAjLO+PwquHyRC0U78NDv5DK
+ohgYgxyFjbCk/f4Vfum+ceMTI3AOMCq6OCGSWNVYCZLYxhNC2yHyjK1kSDFeFmlB
++JA1xf9lTse/xj92M+Y5eEaOeFqkdHszdjXVpdRrFTr5lowPr05ZRg8P8GcqkBtR
+iEnV8aR3EPkHWAiaJWFKRbEc8LZKhtog7yKieXb18O1u8+lJ4VFyMjXL9M5ZQ3Iy
+YnoqT1hRdTTZg8S8aKKpLt1hTL6l2he5KSBSpqVgkOJNkau7vHuD6CCdqtF/iXDC
+4QN6+bPxrDApQIkraUWX///SWCr+MoqPFdh9tfa0KXHAWFtPwmG/XelRfZvsCNYn
+oUxa/5+lUhk5kXfAtPSpOkYl7TnhYRyMrx7x5YisNS7GHscRx9d2h9cUmoaecuiN
+Tl+KtXJwMyXbrUh4ZJVu3QFj2P01OVPGsRV50bK8lhM5tsJQEtTF183JiXAGOdYc
+hesx2ouf24qrWf++OWfcu/YcbEgq0qibQhGRVq+f/nsGmVJCJ9c6YpCHTWvw5Fcn
+sBQ1OhS/DUC/W7CeSyI84u5sSrl0xXrYpZVyhdJ6GmBubud1iaWN7bQpqFk1GAmh
+YfyY5yBf/e33xAmqY8zVScnlQgTctdtdhI0StGyVtWTEysxRWzN9SEIDurFjKlv1
+7eyTAFNoZZMuVK+OA9nATUmLKfNRIx0IvjrFVpYqxsdjnr+VC4wnl1ILd2EfXc/D
+dlaeO3gVpFU1ijomkUtljT/9Zd3OEUqyfSKdCJHrtcGdDi0jP6yQQj+Ac2YSClae
+PL/9HGmJBrhN+kXL/5uC59zHkIFVwmZgjzgLn39pAX3/nj/0JnoqSyyrqBEzDdBK
+OP4+J0gWHQ2NzXMfvGQHjnDc2LzYi5h1AWqrSMV9KJJF7Cj7UpzGLTTpbTlPICYV
+O1+I/v54U/iP7AD9AEApWiCR1i8kZ1iJI6Szs+gapeSvbeZUWlVLshZKWbXeEe3W
+tjmh2Dd4vDz2eXjMpP1bdqk4ARJipu7ngzr+YvjhNFIH0048xHDR9gGuPKr/5rYE
+EI5/6Oncwk+GkFuirsiDR3gAAAAAAAAAAAAA
+
diff --git a/comm/mailnews/test/data/smime/alice.textplain b/comm/mailnews/test/data/smime/alice.textplain
new file mode 100644
index 0000000000..ef8b8f84ec
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.textplain
@@ -0,0 +1,3 @@
+Content-Type: text/plain
+
+This is a test message from Alice to Bob.
diff --git a/comm/mailnews/test/data/smime/alice.textplain.sig.SHA1 b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA1
new file mode 100644
index 0000000000..abf83fb632
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA1
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.textplain.sig.SHA256 b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA256
new file mode 100644
index 0000000000..78e35b5dea
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA256
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.textplain.sig.SHA384 b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA384
new file mode 100644
index 0000000000..f887be3a11
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA384
Binary files differ
diff --git a/comm/mailnews/test/data/smime/alice.textplain.sig.SHA512 b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA512
new file mode 100644
index 0000000000..114a61db50
--- /dev/null
+++ b/comm/mailnews/test/data/smime/alice.textplain.sig.SHA512
Binary files differ
diff --git a/comm/mailnews/test/data/smime/expiration.txt b/comm/mailnews/test/data/smime/expiration.txt
new file mode 100644
index 0000000000..18de5dd6f2
--- /dev/null
+++ b/comm/mailnews/test/data/smime/expiration.txt
@@ -0,0 +1 @@
+Tue Nov 21 20:50:36 2028
diff --git a/comm/mailnews/test/data/smime/generate.sh b/comm/mailnews/test/data/smime/generate.sh
new file mode 100755
index 0000000000..d2625b9f39
--- /dev/null
+++ b/comm/mailnews/test/data/smime/generate.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+
+set -e
+
+# This script creates updated data for automated S/MIME tests.
+# It will do a local build of NSS, execute parts of the NSS test suite,
+# and copy data created by it to the local source tree.
+
+if ! test -e generate.sh || ! test -e local-gen.sh
+then
+ echo "you must run this script from inside the directory that contains generate.sh and local-gen.sh"
+ exit
+fi
+
+mkdir nssbuild
+pushd nssbuild
+
+cp -riv ../../../../../../security/nss nss
+cp -riv ../../../../../../nsprpub nspr
+
+export USE_64=1
+cd nss
+make nss_build_all
+
+export NSS_CYCLES=sharedb
+export NSS_TESTS=smime
+cd tests
+HOST=localhost DOMSUF=localdomain ./all.sh
+
+popd
+cp -v nssbuild/tests_results/security/localhost.1/sharedb/smime/tb/*.eml .
+cp -v nssbuild/tests_results/security/localhost.1/sharedb/smime/tb/*.p12 .
+cp -v nssbuild/tests_results/security/localhost.1/sharedb/smime/tb/*.pem .
+
+CWD=$(pwd)
+
+EXPIRATION_INFO_FILE="$CWD/expiration.txt"
+ALICE_DIR="$CWD/nssbuild/tests_results/security/localhost.1/sharedb/alicedir"
+
+export DIST="$CWD/nssbuild/dist/"
+
+pushd nssbuild/nss/tests/common
+OBJDIR=$(make objdir_name)
+export OBJDIR
+popd
+
+# PATH logic copied from nss/tests/common/init.sh
+if [ "${OS_ARCH}" = "WINNT" ] && [ "$OS_NAME" != "CYGWIN_NT" ] && [ "$OS_NAME" != "MINGW32_NT" ]; then
+ PATH=.\;${DIST}/${OBJDIR}/bin\;${DIST}/${OBJDIR}/lib\;$PATH
+ PATH=$(perl ../path_uniq -d ';' "$PATH")
+elif [ "${OS_ARCH}" = "Android" ]; then
+ # android doesn't have perl, skip the uniq step
+ PATH=.:${DIST}/${OBJDIR}/bin:${DIST}/${OBJDIR}/lib:$PATH
+else
+ PATH=.:${DIST}/${OBJDIR}/bin:${DIST}/${OBJDIR}/lib:/bin:/usr/bin:$PATH
+ # added /bin and /usr/bin in the beginning so a local perl will
+ # be used
+ PATH=$(perl nssbuild/nss/tests/path_uniq -d ':' "$PATH")
+fi
+
+export PATH
+export LD_LIBRARY_PATH=${DIST}/${OBJDIR}/lib:$LD_LIBRARY_PATH
+export SHLIB_PATH=${DIST}/${OBJDIR}/lib:$SHLIB_PATH
+export LIBPATH=${DIST}/${OBJDIR}/lib:$LIBPATH
+export DYLD_LIBRARY_PATH=${DIST}/${OBJDIR}/lib:$DYLD_LIBRARY_PATH
+
+certutil -d "${ALICE_DIR}" -L -n Alice |grep -i "Not After" | \
+ sed 's/^.*: //' > "${EXPIRATION_INFO_FILE}"
+
+# exporting DYLD_LIBRARY_PATH to a subprocess doesn't work on recent OSX
+export NSS_LIB_PATH=${DIST}/${OBJDIR}/lib
+
+# Now refresh Thunderbird's local test data that is based on the NSS
+# test suite data.
+./local-gen.sh
+
+echo "Done. Will remove the NSS build/test tree in 20 seconds."
+sleep 20
+rm -rf nssbuild
diff --git a/comm/mailnews/test/data/smime/local-gen.sh b/comm/mailnews/test/data/smime/local-gen.sh
new file mode 100755
index 0000000000..c74b31e73a
--- /dev/null
+++ b/comm/mailnews/test/data/smime/local-gen.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+set -e
+
+# This script creates additional S/MIME test files.
+# It's called automatically by generate.sh.
+# However, it can also be called directly, if the test data from NSS
+# is still sufficiently fresh, and only the local test files need to
+# be updated, e.g. when adding new tests.
+
+if [ -n "$NSS_LIB_PATH" ]
+then
+ export LD_LIBRARY_PATH=${NSS_LIB_PATH}:$LD_LIBRARY_PATH
+ export SHLIB_PATH=${NSS_LIB_PATH}:$SHLIB_PATH
+ export LIBPATH=${NSS_LIB_PATH}:$LIBPATH
+ export DYLD_LIBRARY_PATH=${NSS_LIB_PATH}:$DYLD_LIBRARY_PATH
+fi
+
+if ! test -e generate.sh || ! test -e local-gen.sh
+then
+ echo "you must run this script from inside the directory that contains local-gen.sh and generate.sh"
+ exit
+fi
+
+if ! hash certutil || ! hash pk12util || ! hash atob || ! hash btoa
+then
+ echo "Required NSS utilities cannot be executed. Add \$OBJDIR/dist/bin of a local Thunderbird build to both the PATH and (platform specific) library path environment variable (e.g. LD_LIBRARY_PATH or DYLD_LIBRARY_PATH)."
+ exit
+fi
+
+MILLDIR="$(pwd)/../../../../mail/test/browser/smime/data"
+
+# When executing mozmill in the CI environment, the files from this
+# directory aren't available. Copy all files that mozmill requires to
+# the mozmill directory.
+cp -rv Bob.p12 TestCA.pem "$MILLDIR"
+
+TMPDIR="./tmp-local"
+mkdir $TMPDIR
+
+BOUNDARY="--------BOUNDARY"
+
+EMAILDATE=$(date --rfc-email --utc)
+
+MSGHEADER="MIME-Version: 1.0
+Date: ${EMAILDATE}
+From: Alice <alice@example.com>
+To: Bob <bob@example.com>
+Subject: a message
+Content-Type: multipart/alternative; boundary=\"${BOUNDARY}\"
+
+"
+
+ENVHEADER="Content-Type: application/pkcs7-mime; smime-type=enveloped-data
+Content-Transfer-Encoding: base64
+
+"
+
+certutil -d $TMPDIR -N --empty-password
+pk12util -d $TMPDIR -i Alice.p12 -W nss
+pk12util -d $TMPDIR -i Bob.p12 -W nss
+certutil -d $TMPDIR -M -n TestCA -t C,C,
+
+INPUT="Content-type: text/plain
+
+SECRET-TEXT the attacker wants to steal
+"
+echo "$INPUT" | cmsutil -d $TMPDIR -E -r bob@example.com | btoa > $TMPDIR/prey.b64
+
+INPUT="Content-type: text/html
+
+<pre>Please reply to this harmless looking message</pre><style>.moz-text-plain, .moz-quote-pre, fieldset {display: none;}</style>"
+echo "$INPUT" | cmsutil -d $TMPDIR -E -r bob@example.com | btoa > $TMPDIR/bait.b64
+
+MSG=$TMPDIR/msg.eml
+
+{
+ echo -n "$MSGHEADER"
+ echo "--$BOUNDARY"
+ echo -n "$ENVHEADER"
+ cat $TMPDIR/bait.b64
+ echo "--$BOUNDARY"
+ echo -n "$ENVHEADER"
+ cat $TMPDIR/prey.b64
+ echo "--$BOUNDARY"
+} > $MSG
+
+mv $MSG "$MILLDIR/multipart-alternative.eml"
+
+# Create a message with a mismatching message date (use a later time,
+# because the test certificates aren't valid at earlier times).
+
+GOOD_DATE=$(grep ^Date "alice.dsig.SHA256.multipart.eml" | sed 's/^Date: //')
+FUTURE_DATE=$(date --utc --rfc-email --date="${GOOD_DATE} + 6 hours")
+sed "s/^Date: .*$/Date: ${FUTURE_DATE}/" "alice.dsig.SHA256.multipart.eml" > "alice.future.dsig.SHA256.multipart.eml"
+
+rm -rf $TMPDIR
diff --git a/comm/mailnews/test/data/tb2hexpopularity.sql b/comm/mailnews/test/data/tb2hexpopularity.sql
new file mode 100644
index 0000000000..2ab6bd2c68
--- /dev/null
+++ b/comm/mailnews/test/data/tb2hexpopularity.sql
@@ -0,0 +1,76 @@
+-- Address book data for use in various tests.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 1),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 2),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 3),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 4),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 5);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'FirstName', 'firs'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'LastName', 'lastn'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'PrimaryEmail', 'ema@test.invalid'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'LowercasePrimaryEmail', 'ema@test.invalid'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'DisplayName', 'd'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'NickName', 'ni'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'PreferMailFormat', '0'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'PopularityIndex', '0'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'AllowRemoteContent', '0'),
+ ('9c4232ec-8992-44b2-8fd7-71dfb23a93c6', 'LastModifiedDate', '0'),
+
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'FirstName', 'first'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'NickName', 'nic'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'PrimaryEmail', 'emai@test.invalid'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'LowercasePrimaryEmail', 'emai@test.invalid'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'LastName', 'l'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'DisplayName', 'di'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'PreferMailFormat', '0'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'PopularityIndex', '0'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'AllowRemoteContent', '0'),
+ ('40326ec9-5361-4f8c-9797-0de3b28edce9', 'LastModifiedDate', '0'),
+
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'DisplayName', 'dis'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'NickName', 'nick'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'PrimaryEmail', 'email@test.invalid'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'LowercasePrimaryEmail', 'email@test.invalid'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'FirstName', 'f'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'LastName', 'la'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'PreferMailFormat', '0'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'PopularityIndex', '0'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'AllowRemoteContent', '0'),
+ ('f96f5eb1-6181-4588-b0d4-0fc47c9ac995', 'LastModifiedDate', '0'),
+
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'LastName', 'las'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'DisplayName', 'disp'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'NickName', 'nickn'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'PrimaryEmail', 'e@test.invalid'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'LowercasePrimaryEmail', 'e@test.invalid'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'FirstName', 'fi'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'PreferMailFormat', '0'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'PopularityIndex', '0'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'AllowRemoteContent', '0'),
+ ('a95c192c-ad3d-4e56-a7e3-8b969c80c717', 'LastModifiedDate', '0'),
+
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'FirstName', 'fir'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'LastName', 'last'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'DisplayName', 'displ'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'PrimaryEmail', 'em@test.invalid'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'LowercasePrimaryEmail', 'em@test.invalid'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'NickName', 'n'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'PreferMailFormat', '0'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'PopularityIndex', 'a'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'AllowRemoteContent', '0'),
+ ('620c1226-eb2d-4df7-a532-f731544525ba', 'LastModifiedDate', '0');
+
+INSERT INTO lists (uid, localId, name, nickName, description) VALUES
+ ('ab831252-d358-435b-a5a4-7f8536ea53d5', 6, 't', '', 'list'),
+ ('52924d31-3a7d-420f-b4b4-039e9888ed08', 7, 'te', '', 'lis'),
+ ('71b6f54e-dbc4-4bf0-a7d7-7235a5551ba5', 8, 'tes', '', 'li'),
+ ('a0f4d368-7451-4756-b003-1018f231c7b4', 9, 'test', 'abcdef', 'l');
diff --git a/comm/mailnews/test/data/template-latin1 b/comm/mailnews/test/data/template-latin1
new file mode 100644
index 0000000000..7118682b94
--- /dev/null
+++ b/comm/mailnews/test/data/template-latin1
@@ -0,0 +1,30 @@
+From - Sun 24 Jan 2016 21:57:32
+X-Mozilla-Status: 0000
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+FCC: mailbox://nobody@Local%20Folders/Sent
+BCC: Some User <u1@example.com>, Another Person <u2@example.com>
+X-Identity-Key: id2
+From: Some User <example@example.com>
+Subject: =?UTF-8?B?bGF0aW4xIHRlbXBsYXRlIMOlw6TDtg==?=
+Message-ID: <8fd7dc38-5b3b-c1a3-2003-4d33dd1c70bf@example.com>
+Date: Sun, 24 Jan 2016 21:57:32 +0200
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
+ attachmentreminder=0
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101
+ Thunderbird/46.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=windows-1252
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+ <meta content="text/html; charset=windows-1252"
+ http-equiv="Content-Type">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ html åäö xlatin1<br>
+ </body>
+</html>
+
+
diff --git a/comm/mailnews/test/data/template-utf8 b/comm/mailnews/test/data/template-utf8
new file mode 100644
index 0000000000..e8a65e21a7
--- /dev/null
+++ b/comm/mailnews/test/data/template-utf8
@@ -0,0 +1,33 @@
+From - Sat 23 Jan 2016 22:06:43 +0200
+X-Mozilla-Status: 0000
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+FCC: mailbox://nobody@Local%20Folders/Sent
+BCC: Some User <u1@example.com>, Another Person <u2@example.com>
+X-Identity-Key: id2
+From: Some User <example@example.com>
+Subject: utf-8 test
+Message-ID: <ee022a93-1fbf-7086-24c7-a61939ea52ce@example.com>
+Date: Sat, 23 Jan 2016 22:06:43 +0200
+X-Mozilla-Draft-Info: internal/draft; vcard=1; receipt=0; DSN=0; uuencode=0;
+ attachmentreminder=0
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101
+ Thunderbird/46.0a1
+MIME-Version: 1.0
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ <p>åäö xutf8<br>
+ </p>
+ <br>
+ <br>
+ </body>
+</html>
+
+
diff --git a/comm/mailnews/test/data/test_virtualFolders.dat b/comm/mailnews/test/data/test_virtualFolders.dat
new file mode 100644
index 0000000000..2c7e4812be
--- /dev/null
+++ b/comm/mailnews/test/data/test_virtualFolders.dat
@@ -0,0 +1,14 @@
+version=1
+uri=mailbox://nobody@Local%20Folders/unread-local
+scope=mailbox://nobody@Local%20Folders/Inbox|mailbox://nobody@Local%20Folders/Trash|mailbox://nobody@Local%20Folders/non-existent
+terms=ALL
+searchOnline=false
+uri=mailbox://nobody@Local%20Folders/invalidserver-local
+scope=mailbox://nobody@Local%20Folders/Inbox|user@foo/INBOX
+terms=ALL
+searchOnline=false
+uri=mailbox://nobody@Local%20Folders/%24label1
+searchFolderFlag=1000
+scope=*
+terms=AND (tag,contains,$label1)
+searchOnline=false
diff --git a/comm/mailnews/test/fakeserver/Auth.jsm b/comm/mailnews/test/fakeserver/Auth.jsm
new file mode 100644
index 0000000000..4bd240b509
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Auth.jsm
@@ -0,0 +1,209 @@
+/* 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/. */
+
+/**
+ * This file implements the authentication mechanisms
+ * - AUTH LOGIN
+ * - AUTH PLAIN
+ * - AUTH CRAM-MD5
+ * for all the server implementations, i.e. in a generic way.
+ * In fact, you could use this to implement a real server in JS :-) .
+ *
+ * @author Ben Bucksch <ben.bucksch beonex.com>
+ */
+
+var EXPORTED_SYMBOLS = ["AuthPLAIN", "AuthLOGIN", "AuthCRAM"];
+
+/**
+ * Implements AUTH PLAIN
+ *
+ * @see RFC 4616
+ */
+var AuthPLAIN = {
+ /**
+ * Takes full PLAIN auth line, and decodes it.
+ *
+ * @param line {string}
+ * @returns {Object { username : value, password : value } }
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH PLAIN line -" + line + "-\n");
+ line = atob(line); // base64 decode
+ let aap = line.split("\u0000"); // 0-charater is delimiter
+ if (aap.length != 3) {
+ throw new Error("Expected three parts");
+ }
+ /* aap is: authorize-id, authenticate-id, password.
+ Generally, authorize-id = authenticate-id = username.
+ authorize-id may thus be empty and then defaults to authenticate-id. */
+ var result = {};
+ var authzid = aap[0];
+ result.username = aap[1];
+ result.password = aap[2];
+ dump(
+ "authorize-id: -" +
+ authzid +
+ "-, username: -" +
+ result.username +
+ "-, password: -" +
+ result.password +
+ "-\n"
+ );
+ if (authzid && authzid != result.username) {
+ throw new Error(
+ "Expecting a authorize-id that's either the same as authenticate-id or empty"
+ );
+ }
+ return result;
+ },
+
+ /**
+ * Create an AUTH PLAIN line, to allow a client to authenticate to a server.
+ * Useful for tests.
+ */
+ encodeLine(username, password) {
+ username = username.substring(0, 255);
+ password = password.substring(0, 255);
+ return btoa("\u0000" + username + "\u0000" + password); // base64 encode
+ },
+};
+
+var AuthLOGIN = {
+ /**
+ * Takes full LOGIN auth line, and decodes it.
+ * It may contain either username or password,
+ * depending on state/step (first username, then pw).
+ *
+ * @param line {string}
+ * @returns {string} username or password
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH LOGIN -" + atob(line) + "-\n");
+ return atob(line); // base64 decode
+ },
+};
+
+/**
+ * Implements AUTH CRAM-MD5
+ *
+ * @see RFC 2195, RFC 2104
+ */
+var AuthCRAM = {
+ /**
+ * First part of CRAM exchange is that the server sends
+ * a challenge to the client. The client response depends on
+ * the challenge. (This prevents replay attacks, I think.)
+ * This function generates the challenge.
+ *
+ * You need to store it, you'll need it to check the client response.
+ *
+ * @param domain {string} - Your hostname or domain,
+ * e.g. "example.com", "mx.example.com" or just "localhost".
+ * @returns {string} The challenge.
+ * It's already base64-encoded. Send it as-is to the client.
+ */
+ createChallenge(domain) {
+ var timestamp = new Date().getTime(); // unixtime
+ var challenge = "<" + timestamp + "@" + domain + ">";
+ dump("CRAM challenge unencoded: " + challenge + "\n");
+ return btoa(challenge);
+ },
+ /**
+ * Takes full CRAM-MD5 auth line, and decodes it.
+ *
+ * Compare the returned |digest| to the result of
+ * encodeCRAMMD5(). If they match, the |username|
+ * returned here is authenticated.
+ *
+ * @param line {string}
+ * @returns {Object { username : value, digest : value } }
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH CRAM-MD5 line -" + line + "-\n");
+ line = atob(line);
+ dump("base64 decoded -" + line + "-\n");
+ var sp = line.split(" ");
+ if (sp.length != 2) {
+ throw new Error("Expected one space");
+ }
+ var result = {};
+ result.username = sp[0];
+ result.digest = sp[1];
+ return result;
+ },
+ /**
+ * @param text {string} - server challenge (base64-encoded)
+ * @param key {string} - user's password
+ * @returns {string} digest as hex string
+ */
+ encodeCRAMMD5(text, key) {
+ text = atob(text); // createChallenge() returns it already encoded
+ dump("encodeCRAMMD5(text: -" + text + "-, key: -" + key + "-)\n");
+ const kInputLen = 64;
+ // const kHashLen = 16;
+ const kInnerPad = 0x36; // per spec
+ const kOuterPad = 0x5c;
+
+ key = this.textToNumberArray(key);
+ text = this.textToNumberArray(text);
+ // Make sure key is exactly kDigestLen bytes long. Algo per spec.
+ if (key.length > kInputLen) {
+ // (results in kHashLen)
+ key = this.md5(key);
+ }
+ while (key.length < kInputLen) {
+ // Fill up with zeros.
+ key.push(0);
+ }
+
+ // MD5((key XOR outerpad) + MD5((key XOR innerpad) + text)) , per spec
+ var digest = this.md5(
+ this.xor(key, kOuterPad).concat(
+ this.md5(this.xor(key, kInnerPad).concat(text))
+ )
+ );
+ return this.arrayToHexString(digest);
+ },
+ // Utils
+ xor(binary, value) {
+ var result = [];
+ for (var i = 0; i < binary.length; i++) {
+ result.push(binary[i] ^ value);
+ }
+ return result;
+ },
+ md5(binary) {
+ var md5 = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ md5.init(Ci.nsICryptoHash.MD5);
+ md5.update(binary, binary.length);
+ return this.textToNumberArray(md5.finish(false));
+ },
+ textToNumberArray(text) {
+ var array = [];
+ for (var i = 0; i < text.length; i++) {
+ // Convert string (only lower byte) to array.
+ array.push(text.charCodeAt(i) & 0xff);
+ }
+ return array;
+ },
+ arrayToHexString(binary) {
+ var result = "";
+ for (var i = 0; i < binary.length; i++) {
+ if (binary[i] > 255) {
+ throw new Error("unexpected that value > 255");
+ }
+ let hex = binary[i].toString(16);
+ if (hex.length < 2) {
+ hex = "0" + hex;
+ }
+ result += hex;
+ }
+ return result;
+ },
+};
diff --git a/comm/mailnews/test/fakeserver/Binaryd.jsm b/comm/mailnews/test/fakeserver/Binaryd.jsm
new file mode 100644
index 0000000000..7502ef1e55
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Binaryd.jsm
@@ -0,0 +1,250 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 = ["BinaryServer"];
+
+const CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+/**
+ * A binary stream-based server.
+ * Listens on a socket, and whenever a new connection is made it runs
+ * a user-supplied handler function.
+ *
+ * Example:
+ * A trivial echo server (with a null daemon, so no state shared between
+ * connections):
+ *
+ * let echoServer = new BinaryServer(function(conn, daemon) {
+ * while(1) {
+ * let data = conn.read(1);
+ * conn.write(data);
+ * }
+ * }, null);
+ *
+ */
+
+class BinaryServer {
+ /**
+ * The handler function should be of the form:
+ * async function handlerFn(conn, daemon)
+ *
+ * @async
+ * @callback handlerFn
+ * @param {Connection} conn
+ * @param {object} daemon
+ *
+ * The handler function runs as long as it wants - reading and writing bytes
+ * (via methods on conn) until it is finished with the connection.
+ * The handler simply returns to indicate the connection is done, or throws
+ * an exception to indicate that something went wrong.
+ * The daemon is the object which holds the server data/state, shared with
+ * all connection handler. The BinaryServer doesn't do anything with daemon
+ * other than passing it directly on to the handler function.
+ */
+
+ /**
+ * Construct a new BinaryServer.
+ *
+ * @param {handlerFn} handlerFn - Function to call to handle each new connection.
+ * @param {object} daemon - Object to pass on to the handler, to share state
+ * and functionality between across connections.
+ */
+ constructor(handlerFn, daemon) {
+ this._port = -1;
+ this._handlerFn = handlerFn;
+ this._daemon = daemon;
+ this._listener = null; // Listening socket to accept new connections.
+ this._connections = new Set();
+ }
+
+ /**
+ * Starts the server running.
+ *
+ * @param {number} port - The port to run on (or -1 to pick one automatically).
+ */
+ async start(port = -1) {
+ if (this._listener) {
+ throw Components.Exception(
+ "Server already started",
+ Cr.NS_ERROR_ALREADY_INITIALIZED
+ );
+ }
+
+ let socket = new ServerSocket(
+ port,
+ true, // Loopback only.
+ -1 // Default max pending connections.
+ );
+
+ let server = this;
+
+ socket.asyncListen({
+ async onSocketAccepted(socket, transport) {
+ let conn = new Connection(transport);
+ server._connections.add(conn);
+ try {
+ await server._handlerFn(conn, server._daemon);
+ // If we get here, handler completed, without error.
+ } catch (e) {
+ if (conn.isClosed()) {
+ // if we get here, assume the error occurred because we're
+ // shutting down, and ignore it.
+ } else {
+ // if we get here, something went wrong.
+ dump("ERROR " + e.toString());
+ }
+ }
+ conn.close();
+ server._connections.delete(conn);
+ },
+ onStopListening(socket, status) {
+ // Server is stopping, time to close any outstanding connections.
+ server._connections.forEach(conn => conn.close());
+ server._connections.clear();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
+ });
+ // We're running!
+ this._listener = socket;
+ }
+
+ /**
+ * Provides port, a read-only attribute to get which port the server
+ * server is listening upon. Behaviour is undefined if server is not
+ * running.
+ */
+ get port() {
+ return this._listener.port;
+ }
+
+ /**
+ * Stops the server, if it is running.
+ */
+ stop() {
+ if (!this._listener) {
+ // Already stopped.
+ return;
+ }
+ this._listener.close();
+ this._listener = null;
+ // We could still be accepting new connections at this point,
+ // so we wait until the onStopListening callback to tear down the
+ // connections.
+ }
+}
+
+/**
+ * Connection wraps a nsITransport with read/write functions that are
+ * javascript async, to simplify writing server handers.
+ * Handlers should only need to use read() and write() from here, leaving
+ * all connection management up to the BinaryServer.
+ */
+class Connection {
+ constructor(transport) {
+ this._transport = transport;
+ this._input = transport.openInputStream(0, 0, 0);
+ let outStream = transport.openOutputStream(0, 0, 0);
+ this._output = new BinaryOutputStream(outStream);
+ }
+
+ /**
+ * @returns true if close() has been called.
+ */
+ isClosed() {
+ return this._transport === null;
+ }
+
+ /**
+ * Closes the connection. Can be safely called multiple times.
+ * The BinaryServer will call this - handlers don't need to worry about
+ * the connection status.
+ */
+ close() {
+ if (this.isClosed()) {
+ return;
+ }
+ this._input.close();
+ this._output.close();
+ this._transport.close(Cr.NS_OK);
+ this._input = null;
+ this._output = null;
+ this._transport = null;
+ }
+
+ /**
+ * Read exactly nBytes from the connection.
+ *
+ * @param {number} nBytes - The number of bytes required.
+ * @returns {Array.<number>} - An array containing the requested bytes.
+ */
+ async read(nBytes) {
+ let conn = this;
+ let buf = [];
+ while (buf.length < nBytes) {
+ let want = nBytes - buf.length;
+ // A slightly odd-looking construct to wrap the listener-based
+ // asyncwait() into a javascript async function.
+ await new Promise((resolve, reject) => {
+ try {
+ conn._input.asyncWait(
+ {
+ onInputStreamReady(stream) {
+ // how many bytes are actually available?
+ let n;
+ try {
+ n = stream.available();
+ } catch (e) {
+ // stream was closed.
+ reject(e);
+ }
+ if (n > want) {
+ n = want;
+ }
+ let chunk = new BinaryInputStream(stream).readByteArray(n);
+ Array.prototype.push.apply(buf, chunk);
+ resolve();
+ },
+ },
+ 0,
+ want,
+ Services.tm.mainThread
+ );
+ } catch (e) {
+ // asyncwait() failed
+ reject(e);
+ }
+ });
+ }
+ return buf;
+ }
+
+ /**
+ * Write data to the connection.
+ *
+ * @param {Array.<number>} data - The bytes to send.
+ */
+ async write(data) {
+ // TODO: need to check outputstream for writeability here???
+ // Might be an issue if we start throwing bigger chunks of data about...
+ await this._output.writeByteArray(data);
+ }
+}
diff --git a/comm/mailnews/test/fakeserver/Imapd.jsm b/comm/mailnews/test/fakeserver/Imapd.jsm
new file mode 100644
index 0000000000..6023bf7b90
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Imapd.jsm
@@ -0,0 +1,2544 @@
+/* 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/. */
+// This file implements test IMAP servers
+
+var EXPORTED_SYMBOLS = [
+ "ImapDaemon",
+ "ImapMailbox",
+ "ImapMessage",
+ "IMAP_RFC3501_handler",
+ "configurations",
+ "mixinExtension",
+ "IMAP_GMAIL_extension",
+ "IMAP_MOVE_extension",
+ "IMAP_CUSTOM_extension",
+ "IMAP_RFC2197_extension",
+ "IMAP_RFC2342_extension",
+ "IMAP_RFC3348_extension",
+ "IMAP_RFC4315_extension",
+ "IMAP_RFC5258_extension",
+ "IMAP_RFC2195_extension",
+];
+
+// IMAP DAEMON ORGANIZATION
+// ------------------------
+// The large numbers of RFCs all induce some implicit assumptions as to the
+// organization of an IMAP server. Ideally, we'd like to be as inclusive as
+// possible so that we can guarantee that it works for every type of server.
+// Unfortunately, such all-accepting setups make generic algorithms hard to
+// use; given their difficulty in a generic framework, it seems unlikely that
+// a server would implement such characteristics. It also seems likely that
+// if mailnews had a problem with the implementation, then most clients would
+// see similar problems, so as to make the server widely unusable. In any
+// case, if someone complains about not working on bugzilla, it can be added
+// to the test suite.
+// So, with that in mind, this is the basic layout of the daemon:
+// DAEMON
+// + Namespaces: parentless mailboxes whose names are the namespace name. The
+// type of the namespace is specified by the type attribute.
+// + Mailboxes: ImapMailbox objects with several properties. If a mailbox
+// | | property begins with a '_', then it should not be serialized because
+// | | it can be discovered from other means; in particular, a '_' does not
+// | | necessarily mean that it is a private property that should not be
+// | | accessed. The parent of a top-level mailbox is null, not "".
+// | + I18N names: RFC 3501 specifies a modified UTF-7 form for names.
+// | | However, a draft RFC makes the names UTF-8; it is expected to be
+// | | completed and implemented "soon". Therefore, the correct usage is
+// | | to specify the mailbox names as one normally does in JS and the
+// | | protocol will take care of conversion itself.
+// | + Case-sensitivity: RFC 3501 takes no position on this issue, only that
+// | | a case-insensitive server must treat the base-64 parts of mailbox
+// | | names as case-sensitive. The draft UTF8 RFC says nothing on this
+// | | topic, but Crispin recommends using Unicode case-insensitivity. We
+// | | therefore treat names in such manner (if the case-insensitive flag
+// | | is set), in technical violation of RFC 3501.
+// | + Flags: Flags are (as confirmed by Crispin) case-insensitive. Internal
+// | flag equality, though, uses case-sensitive checks. Therefore they
+// | should be normalized to a title-case form (e.g., \Noselect).
+// + Synchronization: On certain synchronizing commands, the daemon will call
+// | a synchronizing function to allow manipulating code the chance to
+// | perform various (potentially expensive) actions.
+// + Messages: A message is represented internally as an annotated URI.
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+class ImapDaemon {
+ constructor(flags, syncFunc) {
+ this._flags = flags;
+
+ this.namespaces = [];
+ this.idResponse = "NIL";
+ this.root = new ImapMailbox("", null, { type: IMAP_NAMESPACE_PERSONAL });
+ this.uidvalidity = Math.round(Date.now() / 1000);
+ this.inbox = new ImapMailbox("INBOX", null, this.uidvalidity++);
+ this.root.addMailbox(this.inbox);
+ this.namespaces.push(this.root);
+ this.syncFunc = syncFunc;
+ // This can be used to cause the artificial failure of any given command.
+ this.commandToFail = "";
+ // This can be used to simulate timeouts on large copies
+ this.copySleep = 0;
+ }
+ synchronize(mailbox, update) {
+ if (this.syncFunc) {
+ this.syncFunc.call(null, this);
+ }
+ if (update) {
+ for (var message of mailbox._messages) {
+ message.recent = false;
+ }
+ }
+ }
+ getNamespace(name) {
+ for (var namespace of this.namespaces) {
+ if (
+ name.indexOf(namespace.name) == 0 &&
+ name[namespace.name.length] == namespace.delimiter
+ ) {
+ return namespace;
+ }
+ }
+ return this.root;
+ }
+ createNamespace(name, type) {
+ var newbox = this.createMailbox(name, { type });
+ this.namespaces.push(newbox);
+ }
+ getMailbox(name) {
+ if (name == "") {
+ return this.root;
+ }
+ // INBOX is case-insensitive, no matter what
+ if (name.toUpperCase().startsWith("INBOX")) {
+ name = "INBOX" + name.substr(5);
+ }
+ // We want to find a child who has the same name, but we don't quite know
+ // what the delimiter is. The convention is that different namespaces use a
+ // name starting with '#', so that's how we'll work it out.
+ let mailbox;
+ if (name.startsWith("#")) {
+ for (mailbox of this.root._children) {
+ if (
+ mailbox.name.indexOf(name) == 0 &&
+ name[mailbox.name.length] == mailbox.delimiter
+ ) {
+ break;
+ }
+ }
+ if (!mailbox) {
+ return null;
+ }
+
+ // Now we continue like normal
+ let names = name.split(mailbox.delimiter);
+ names.splice(0, 1);
+ for (let part of names) {
+ mailbox = mailbox.getChild(part);
+ if (!mailbox || mailbox.nonExistent) {
+ return null;
+ }
+ }
+ } else {
+ // This is easy, just split it up using the inbox's delimiter
+ let names = name.split(this.inbox.delimiter);
+ mailbox = this.root;
+
+ for (let part of names) {
+ mailbox = mailbox.getChild(part);
+ if (!mailbox || mailbox.nonExistent) {
+ return null;
+ }
+ }
+ }
+ return mailbox;
+ }
+ createMailbox(name, oldBox) {
+ var namespace = this.getNamespace(name);
+ if (namespace.name != "") {
+ name = name.substring(namespace.name.length + 1);
+ }
+ var prefixes = name.split(namespace.delimiter);
+ var subName;
+ if (prefixes[prefixes.length - 1] == "") {
+ subName = prefixes.splice(prefixes.length - 2, 2)[0];
+ } else {
+ subName = prefixes.splice(prefixes.length - 1, 1)[0];
+ }
+ var box = namespace;
+ for (var component of prefixes) {
+ box = box.getChild(component);
+ // Yes, we won't autocreate intermediary boxes
+ if (box == null || box.flags.includes("\\NoInferiors")) {
+ return false;
+ }
+ }
+ // If this is an ImapMailbox...
+ if (oldBox && oldBox._children) {
+ // Only delete now so we don't screw ourselves up if creation fails
+ this.deleteMailbox(oldBox);
+ oldBox._parent = box == this.root ? null : box;
+ let newBox = new ImapMailbox(subName, box, this.uidvalidity++);
+ newBox._messages = oldBox._messages;
+ box.addMailbox(newBox);
+
+ // And if oldBox is an INBOX, we need to recreate that
+ if (oldBox.name == "INBOX") {
+ this.inbox = new ImapMailbox("INBOX", null, this.uidvalidity++);
+ this.root.addMailbox(this.inbox);
+ }
+ oldBox.name = subName;
+ } else if (oldBox) {
+ // oldBox is a regular {} object, so it contains mailbox data but is not
+ // a mailbox itself. Pass it into the constructor and let that deal with
+ // it...
+ let childBox = new ImapMailbox(
+ subName,
+ box == this.root ? null : box,
+ oldBox
+ );
+ box.addMailbox(childBox);
+ // And return the new mailbox, since this is being used by people setting
+ // up the daemon.
+ return childBox;
+ } else {
+ var creatable = hasFlag(this._flags, IMAP_FLAG_NEEDS_DELIMITER)
+ ? name[name.length - 1] == namespace.delimiter
+ : true;
+ let childBox = new ImapMailbox(subName, box == this.root ? null : box, {
+ flags: creatable ? [] : ["\\NoInferiors"],
+ uidvalidity: this.uidvalidity++,
+ });
+ box.addMailbox(childBox);
+ }
+ return true;
+ }
+ deleteMailbox(mailbox) {
+ if (mailbox._children.length == 0) {
+ // We don't preserve the subscribed state for deleted mailboxes
+ var parentBox = mailbox._parent == null ? this.root : mailbox._parent;
+ parentBox._children.splice(parentBox._children.indexOf(mailbox), 1);
+ } else {
+ // clear mailbox
+ mailbox._messages = [];
+ mailbox.flags.push("\\Noselect");
+ }
+ }
+}
+
+class ImapMailbox {
+ constructor(name, parent, state) {
+ this.name = name;
+ this._parent = parent;
+ this._children = [];
+ this._messages = [];
+ this._updates = [];
+
+ // Shorthand for uidvalidity
+ if (typeof state == "number") {
+ this.uidvalidity = state;
+ state = {};
+ }
+
+ if (!state) {
+ state = {};
+ }
+
+ for (var prop in state) {
+ this[prop] = state[prop];
+ }
+
+ this.setDefault("subscribed", false);
+ this.setDefault("nonExistent", false);
+ this.setDefault("delimiter", "/");
+ this.setDefault("flags", []);
+ this.setDefault("specialUseFlag", "");
+ this.setDefault("uidnext", 1);
+ this.setDefault("msgflags", [
+ "\\Seen",
+ "\\Answered",
+ "\\Flagged",
+ "\\Deleted",
+ "\\Draft",
+ ]);
+ this.setDefault("permflags", [
+ "\\Seen",
+ "\\Answered",
+ "\\Flagged",
+ "\\Deleted",
+ "\\Draft",
+ "\\*",
+ ]);
+ }
+ setDefault(prop, def) {
+ this[prop] = prop in this ? this[prop] : def;
+ }
+ addMailbox(mailbox) {
+ this._children.push(mailbox);
+ }
+ getChild(name) {
+ for (var mailbox of this._children) {
+ if (name == mailbox.name) {
+ return mailbox;
+ }
+ }
+ return null;
+ }
+ matchKids(pattern) {
+ if (pattern == "") {
+ return this._parent ? this._parent.matchKids("") : [this];
+ }
+
+ var portions = pattern.split(this.delimiter);
+ var matching = [this];
+ for (var folder of portions) {
+ if (folder.length == 0) {
+ continue;
+ }
+
+ let generator = folder.includes("*") ? "allChildren" : "_children";
+ let possible = matching.reduce(function (arr, elem) {
+ return arr.concat(elem[generator]);
+ }, []);
+
+ if (folder == "*" || folder == "%") {
+ matching = possible;
+ continue;
+ }
+
+ let parts = folder.split(/[*%]/).filter(function (str) {
+ return str.length > 0;
+ });
+ matching = possible.filter(function (mailbox) {
+ let index = 0,
+ name = mailbox.fullName;
+ for (var part of parts) {
+ index = name.indexOf(part, index);
+ if (index == -1) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+ return matching;
+ }
+ get fullName() {
+ return (
+ (this._parent ? this._parent.fullName + this.delimiter : "") + this.name
+ );
+ }
+ get displayName() {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ // Escape backslash and double-quote with another backslash before encoding.
+ return manager.unicodeToMutf7(this.fullName.replace(/([\\"])/g, "\\$1"));
+ }
+ get allChildren() {
+ return this._children.reduce(function (arr, elem) {
+ return arr.concat(elem._allChildrenInternal);
+ }, []);
+ }
+ get _allChildrenInternal() {
+ return this._children.reduce(
+ function (arr, elem) {
+ return arr.concat(elem._allChildrenInternal);
+ },
+ [this]
+ );
+ }
+ addMessage(message) {
+ this._messages.push(message);
+ if (message.uid >= this.uidnext) {
+ this.uidnext = message.uid + 1;
+ }
+ if (!this._updates.includes("EXISTS")) {
+ this._updates.push("EXISTS");
+ }
+ if ("__highestuid" in this && message.uid > this.__highestuid) {
+ this.__highestuid = message.uid;
+ }
+ }
+ get _highestuid() {
+ if ("__highestuid" in this) {
+ return this.__highestuid;
+ }
+ var highest = 0;
+ for (var message of this._messages) {
+ if (message.uid > highest) {
+ highest = message.uid;
+ }
+ }
+ this.__highestuid = highest;
+ return highest;
+ }
+ expunge() {
+ var response = "";
+ for (var i = 0; i < this._messages.length; i++) {
+ if (this._messages[i].flags.includes("\\Deleted")) {
+ response += "* " + (i + 1) + " EXPUNGE\0";
+ this._messages.splice(i--, 1);
+ }
+ }
+ if (response.length > 0) {
+ delete this.__highestuid;
+ }
+ return response;
+ }
+}
+
+class ImapMessage {
+ constructor(URI, uid, flags) {
+ this._URI = URI;
+ this.uid = uid;
+ this.size = 0;
+ this.flags = [];
+ for (let flag in flags) {
+ this.flags.push(flag);
+ }
+ this.recent = false;
+ }
+ get channel() {
+ return Services.io.newChannel(
+ this._URI,
+ null,
+ null,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ }
+ setFlag(flag) {
+ if (!this.flags.includes(flag)) {
+ this.flags.push(flag);
+ }
+ }
+ // This allows us to simulate servers that approximate the rfc822 size.
+ setSize(size) {
+ this.size = size;
+ }
+ clearFlag(flag) {
+ let index = this.flags.indexOf(flag);
+ if (index != -1) {
+ this.flags.splice(index, 1);
+ }
+ }
+ getText(start, length) {
+ if (!start) {
+ start = 0;
+ }
+ if (!length) {
+ length = -1;
+ }
+ var channel = this.channel;
+ var istream = channel.open();
+ var bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ bstream.setInputStream(istream);
+ var str = bstream.readBytes(start);
+ if (str.length != start) {
+ throw new Error("Erm, we didn't just pass through 8-bit");
+ }
+ length = length == -1 ? istream.available() : length;
+ if (length > istream.available()) {
+ length = istream.available();
+ }
+ str = bstream.readBytes(length);
+ return str;
+ }
+
+ get _partMap() {
+ if (this.__partMap) {
+ return this.__partMap;
+ }
+ var partMap = {};
+ var emitter = {
+ startPart(partNum, headers) {
+ var imapPartNum = partNum.replace("$", "");
+ // If there are multiple imap parts that this represents, we'll
+ // overwrite with the latest. This is what we want (most deeply nested).
+ partMap[imapPartNum] = [partNum, headers];
+ },
+ };
+ MimeParser.parseSync(this.getText(), emitter, {
+ bodyformat: "none",
+ stripcontinuations: false,
+ });
+ return (this.__partMap = partMap);
+ }
+ getPartHeaders(partNum) {
+ return this._partMap[partNum][1];
+ }
+ getPartBody(partNum) {
+ var body = "";
+ var emitter = {
+ deliverPartData(partNum, data) {
+ body += data;
+ },
+ };
+ var mimePartNum = this._partMap[partNum][0];
+ MimeParser.parseSync(this.getText(), emitter, {
+ pruneat: mimePartNum,
+ bodyformat: "raw",
+ });
+ return body;
+ }
+}
+
+// IMAP FLAGS
+// If you don't specify any flag, no flags are set.
+
+/**
+ * This flag represents whether or not CREATE hierarchies need a delimiter.
+ *
+ * If this flag is off, <tt>CREATE a<br />CREATE a/b</tt> fails where
+ * <tt>CREATE a/<br />CREATE a/b</tt> would succeed (assuming the delimiter is
+ * '/').
+ */
+var IMAP_FLAG_NEEDS_DELIMITER = 2;
+
+function hasFlag(flags, flag) {
+ return (flags & flag) == flag;
+}
+
+// IMAP Namespaces
+var IMAP_NAMESPACE_PERSONAL = 0;
+// var IMAP_NAMESPACE_OTHER_USERS = 1;
+// var IMAP_NAMESPACE_SHARED = 2;
+
+// IMAP server helpers
+var IMAP_STATE_NOT_AUTHED = 0;
+var IMAP_STATE_AUTHED = 1;
+var IMAP_STATE_SELECTED = 2;
+
+function parseCommand(text, partial) {
+ var args = [];
+ var current = args;
+ var stack = [];
+ if (partial) {
+ args = partial.args;
+ current = partial.current;
+ stack = partial.stack;
+ current.push(partial.text);
+ }
+ var atom = "";
+ while (text.length > 0) {
+ let c = text[0];
+
+ if (c == '"') {
+ let index = 1;
+ let s = "";
+ while (index < text.length && text[index] != '"') {
+ if (text[index] == "\\") {
+ index++;
+ if (text[index] != '"' && text[index] != "\\") {
+ throw new Error("Expected quoted character");
+ }
+ }
+ s += text[index++];
+ }
+ if (index == text.length) {
+ throw new Error("Expected DQUOTE");
+ }
+ current.push(s);
+ text = text.substring(index + 1);
+ continue;
+ } else if (c == "{") {
+ let end = text.indexOf("}");
+ if (end == -1) {
+ throw new Error("Expected CLOSE_BRACKET");
+ }
+ if (end + 1 != text.length) {
+ throw new Error("Expected CRLF");
+ }
+ let length = parseInt(text.substring(1, end));
+ // Usable state
+ // eslint-disable-next-line no-throw-literal
+ throw { length, current, args, stack, text: "" };
+ } else if (c == "(") {
+ stack.push(current);
+ current = [];
+ } else if (c == ")") {
+ if (atom.length > 0) {
+ current.push(atom);
+ atom = "";
+ }
+ let hold = current;
+ current = stack.pop();
+ if (current == undefined) {
+ throw new Error("Unexpected CLOSE_PAREN");
+ }
+ current.push(hold);
+ } else if (c == " ") {
+ if (atom.length > 0) {
+ current.push(atom);
+ atom = "";
+ }
+ } else if (
+ text.toUpperCase().startsWith("NIL") &&
+ (text.length == 3 || text[3] == " ")
+ ) {
+ current.push(null);
+ text = text.substring(4);
+ continue;
+ } else {
+ atom += c;
+ }
+ text = text.substring(1);
+ }
+ if (stack.length != 0) {
+ throw new Error("Expected CLOSE_PAREN!");
+ }
+ if (atom.length > 0) {
+ args.push(atom);
+ }
+ return args;
+}
+
+function formatArg(argument, spec) {
+ // Get NILs out of the way quickly
+ var nilAccepted = false;
+ if (spec.startsWith("n") && spec[1] != "u") {
+ spec = spec.substring(1);
+ nilAccepted = true;
+ }
+ if (argument == null) {
+ if (!nilAccepted) {
+ throw new Error("Unexpected NIL!");
+ }
+
+ return null;
+ }
+
+ // array!
+ if (spec.startsWith("(")) {
+ // typeof array is object. Don't ask me why.
+ if (!Array.isArray(argument)) {
+ throw new Error("Expected list!");
+ }
+ // Strip the '(' and ')'...
+ spec = spec.substring(1, spec.length - 1);
+ // ... and apply to the rest
+ return argument.map(function (item) {
+ return formatArg(item, spec);
+ });
+ }
+
+ // or!
+ var pipe = spec.indexOf("|");
+ if (pipe > 0) {
+ var first = spec.substring(0, pipe);
+ try {
+ return formatArg(argument, first);
+ } catch (e) {
+ return formatArg(argument, spec.substring(pipe + 1));
+ }
+ }
+
+ // By now, we know that the input should be generated from an atom or string.
+ if (typeof argument != "string") {
+ throw new Error("Expected argument of type " + spec + "!");
+ }
+
+ if (spec == "atom") {
+ argument = argument.toUpperCase();
+ } else if (spec == "mailbox") {
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ argument = manager.mutf7ToUnicode(argument);
+ } else if (spec == "string") {
+ // Do nothing
+ } else if (spec == "flag") {
+ argument = argument.toLowerCase();
+ if (
+ !("a" <= argument[0] && argument[0] <= "z") &&
+ !("A" <= argument[0] && argument[0] <= "Z")
+ ) {
+ argument = argument[0] + argument[1].toUpperCase() + argument.substr(2);
+ } else {
+ argument = argument[0].toUpperCase() + argument.substr(1);
+ }
+ } else if (spec == "number") {
+ if (argument == parseInt(argument)) {
+ argument = parseInt(argument);
+ }
+ } else if (spec == "date") {
+ if (
+ !/^\d{1,2}-[A-Z][a-z]{2}-\d{4}( \d{2}(:\d{2}){2} [+-]\d{4})?$/.test(
+ argument
+ )
+ ) {
+ throw new Error("Expected date!");
+ }
+ argument = new Date(Date.parse(argument.replace(/-(?!\d{4}$)/g, " ")));
+ } else {
+ throw new Error("Unknown spec " + spec);
+ }
+
+ return argument;
+}
+
+// IMAP TEST SERVERS
+// -----------------
+// Because of IMAP and the LEMONADE RFCs, we have a myriad of different
+// server configurations that we should ideally be supporting. We handle them
+// by defining a core RFC 3501 implementation and then have different server
+// extensions subclass the server through functions below. However, we also
+// provide standard configurations for best handling.
+// Configurations:
+// * Barebones RFC 3501
+// * Cyrus
+// * UW IMAP
+// * Courier
+// * Exchange
+// * Dovecot
+// * Zimbra
+// * GMail
+// KNOWN DEVIATIONS FROM RFC 3501:
+// + The autologout timer is 3 minutes, not 30 minutes. A test with a logout
+// of 30 minutes would take a very long time if it failed.
+// + SEARCH (except for UNDELETED) and STARTTLS are not supported,
+// nor is all of FETCH.
+// + Concurrent mailbox access is probably compliant with a rather liberal
+// implementation of RFC 3501, although probably not what one would expect,
+// and certainly not what the Dovecot IMAP server tests expect.
+
+/* IMAP Fakeserver operates in a different manner than the rest of fakeserver
+ * because of some differences in the protocol. Commands are dispatched through
+ * onError, which parses the message into components. Like other fakeserver
+ * implementations, the command property will be called, but this time with an
+ * argument that is an array of data items instead of a string representing the
+ * rest of the line.
+ */
+class IMAP_RFC3501_handler {
+ constructor(daemon) {
+ this.kUsername = "user";
+ this.kPassword = "password";
+ this.kAuthSchemes = []; // Added by RFC2195 extension. Test may modify as needed.
+ this.kCapabilities = [
+ /* "LOGINDISABLED", "STARTTLS", */
+ "CLIENTID",
+ ]; // Test may modify as needed.
+ this.kUidCommands = ["FETCH", "STORE", "SEARCH", "COPY"];
+
+ this._daemon = daemon;
+ this.closing = false;
+ this.dropOnStartTLS = false;
+ // map: property = auth scheme {String}, value = start function on this obj
+ this._kAuthSchemeStartFunction = {};
+
+ this._enabledCommands = {
+ // IMAP_STATE_NOT_AUTHED
+ 0: [
+ "CAPABILITY",
+ "NOOP",
+ "LOGOUT",
+ "STARTTLS",
+ "CLIENTID",
+ "AUTHENTICATE",
+ "LOGIN",
+ ],
+ // IMAP_STATE_AUTHED
+ 1: [
+ "CAPABILITY",
+ "NOOP",
+ "LOGOUT",
+ "SELECT",
+ "EXAMINE",
+ "CREATE",
+ "DELETE",
+ "RENAME",
+ "SUBSCRIBE",
+ "UNSUBSCRIBE",
+ "LIST",
+ "LSUB",
+ "STATUS",
+ "APPEND",
+ ],
+ // IMAP_STATE_SELECTED
+ 2: [
+ "CAPABILITY",
+ "NOOP",
+ "LOGOUT",
+ "SELECT",
+ "EXAMINE",
+ "CREATE",
+ "DELETE",
+ "RENAME",
+ "SUBSCRIBE",
+ "UNSUBSCRIBE",
+ "LIST",
+ "LSUB",
+ "STATUS",
+ "APPEND",
+ "CHECK",
+ "CLOSE",
+ "EXPUNGE",
+ "SEARCH",
+ "FETCH",
+ "STORE",
+ "COPY",
+ "UID",
+ ],
+ };
+ // Format explanation:
+ // atom -> UPPERCASE
+ // string -> don't touch!
+ // mailbox -> Apply ->UTF16 transformation with case-insensitivity stuff
+ // flag -> Titlecase (or \Titlecase, $Titlecase, etc.)
+ // date -> Make it a JSDate object
+ // number -> Make it a number, if possible
+ // ( ) -> list, apply flags as specified
+ // [ ] -> optional argument.
+ // x|y -> either x or y format.
+ // ... -> variable args, don't parse
+ this._argFormat = {
+ CAPABILITY: [],
+ NOOP: [],
+ LOGOUT: [],
+ STARTTLS: [],
+ CLIENTID: ["string", "string"],
+ AUTHENTICATE: ["atom", "..."],
+ LOGIN: ["string", "string"],
+ SELECT: ["mailbox"],
+ EXAMINE: ["mailbox"],
+ CREATE: ["mailbox"],
+ DELETE: ["mailbox"],
+ RENAME: ["mailbox", "mailbox"],
+ SUBSCRIBE: ["mailbox"],
+ UNSUBSCRIBE: ["mailbox"],
+ LIST: ["mailbox", "mailbox"],
+ LSUB: ["mailbox", "mailbox"],
+ STATUS: ["mailbox", "(atom)"],
+ APPEND: ["mailbox", "[(flag)]", "[date]", "string"],
+ CHECK: [],
+ CLOSE: [],
+ EXPUNGE: [],
+ SEARCH: ["atom", "..."],
+ FETCH: ["number", "atom|(atom|(atom))"],
+ STORE: ["number", "atom", "flag|(flag)"],
+ COPY: ["number", "mailbox"],
+ UID: ["atom", "..."],
+ };
+
+ this.resetTest();
+ }
+ resetTest() {
+ this._state = IMAP_STATE_NOT_AUTHED;
+ this._multiline = false;
+ this._nextAuthFunction = undefined; // should be in RFC2195_ext, but too lazy
+ }
+ onStartup() {
+ this._state = IMAP_STATE_NOT_AUTHED;
+ return "* OK IMAP4rev1 Fakeserver started up";
+ }
+
+ // CENTRALIZED DISPATCH FUNCTIONS
+
+ // IMAP sends commands in the form of "tag command args", but fakeserver
+ // parsing tries to call the tag, which doesn't exist. Instead, we use this
+ // error method to do the actual command dispatch. Mailnews uses numbers for
+ // tags, which won't impede on actual commands.
+ onError(tag, realLine) {
+ this._tag = tag;
+ var space = realLine.indexOf(" ");
+ var command = space == -1 ? realLine : realLine.substring(0, space);
+ realLine = space == -1 ? "" : realLine.substring(space + 1);
+
+ // Now parse realLine into an array of atoms, etc.
+ try {
+ var args = parseCommand(realLine);
+ } catch (state) {
+ if (typeof state == "object") {
+ this._partial = state;
+ this._partial.command = command;
+ this._multiline = true;
+ return "+ More!";
+ }
+
+ return this._tag + " BAD " + state;
+ }
+
+ // If we're here, we have a command with arguments. Dispatch!
+ return this._dispatchCommand(command, args);
+ }
+ onMultiline(line) {
+ // A multiline arising form a literal being passed
+ if (this._partial) {
+ // There are two cases to be concerned with:
+ // 1. The CRLF is internal or end (we want more)
+ // 1a. The next line is the actual command stuff!
+ // 2. The CRLF is in the middle (rest of the line is args)
+ if (this._partial.length >= line.length + 2) {
+ // Case 1
+ this._partial.text += line + "\r\n";
+ this._partial.length -= line.length + 2;
+ return undefined;
+ } else if (this._partial.length != 0) {
+ this._partial.text += line.substring(0, this._partial.length);
+ line = line.substring(this._partial.length);
+ }
+ var command = this._partial.command;
+ var args;
+ try {
+ args = parseCommand(line, this._partial);
+ } catch (state) {
+ if (typeof state == "object") {
+ // Yet another literal coming around...
+ this._partial = state;
+ this._partial.command = command;
+ return "+ I'll be needing more text";
+ }
+
+ this._multiline = false;
+ return this.tag + " BAD parse error: " + state;
+ }
+
+ this._partial = undefined;
+ this._multiline = false;
+ return this._dispatchCommand(command, args);
+ }
+
+ if (this._nextAuthFunction) {
+ var func = this._nextAuthFunction;
+ this._multiline = false;
+ this._nextAuthFunction = undefined;
+ if (line == "*") {
+ return this._tag + " BAD Okay, as you wish. Chicken";
+ }
+ if (!func || typeof func != "function") {
+ return this._tag + " BAD I'm lost. Internal server error during auth";
+ }
+ try {
+ return this._tag + " " + func.call(this, line);
+ } catch (e) {
+ return this._tag + " BAD " + e;
+ }
+ }
+ return undefined;
+ }
+ _dispatchCommand(command, args) {
+ this.sendingLiteral = false;
+ command = command.toUpperCase();
+ if (command == this._daemon.commandToFail.toUpperCase()) {
+ return this._tag + " NO " + command + " failed";
+ }
+ var response;
+ if (command in this) {
+ this._lastCommand = command;
+ // Are we allowed to execute this command?
+ if (!this._enabledCommands[this._state].includes(command)) {
+ return (
+ this._tag + " BAD illegal command for current state " + this._state
+ );
+ }
+
+ try {
+ // Format the arguments nicely
+ args = this._treatArgs(args, command);
+
+ // UID command by itself is not useful for PerformTest
+ if (command == "UID") {
+ this._lastCommand += " " + args[0];
+ }
+
+ // Finally, run the thing
+ response = this[command](args);
+ } catch (e) {
+ if (typeof e == "string") {
+ response = e;
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ response = "BAD " + command + " not implemented";
+ }
+
+ // Add status updates
+ if (this._selectedMailbox) {
+ for (var update of this._selectedMailbox._updates) {
+ let line;
+ switch (update) {
+ case "EXISTS":
+ line = "* " + this._selectedMailbox._messages.length + " EXISTS";
+ break;
+ }
+ response = line + "\0" + response;
+ }
+ }
+
+ var lines = response.split("\0");
+ response = "";
+ for (let line of lines) {
+ if (!line.startsWith("+") && !line.startsWith("*")) {
+ response += this._tag + " ";
+ }
+ response += line + "\r\n";
+ }
+ return response;
+ }
+ _treatArgs(args, command) {
+ var format = this._argFormat[command];
+ var treatedArgs = [];
+ for (var i = 0; i < format.length; i++) {
+ var spec = format[i];
+
+ if (spec == "...") {
+ treatedArgs = treatedArgs.concat(args);
+ args = [];
+ break;
+ }
+
+ if (args.length == 0) {
+ if (spec.startsWith("[")) {
+ // == optional arg
+ continue;
+ } else {
+ throw new Error("BAD not enough arguments");
+ }
+ }
+
+ if (spec.startsWith("[")) {
+ // We have an optional argument. See if the format matches and move on
+ // if it doesn't. Ideally, we'd rethink our decision if a later
+ // application turns out to be wrong, but that's ugly to do
+ // iteratively. Should any IMAP extension require it, we'll have to
+ // come back and change this assumption, though.
+ spec = spec.substr(1, spec.length - 2);
+ try {
+ var out = formatArg(args[0], spec);
+ } catch (e) {
+ continue;
+ }
+ treatedArgs.push(out);
+ args.shift();
+ continue;
+ }
+ try {
+ treatedArgs.push(formatArg(args.shift(), spec));
+ } catch (e) {
+ throw new Error("BAD " + e);
+ }
+ }
+ if (args.length != 0) {
+ throw new Error("BAD Too many arguments");
+ }
+ return treatedArgs;
+ }
+
+ // PROTOCOL COMMANDS (ordered as in spec)
+
+ CAPABILITY(args) {
+ var capa = "* CAPABILITY IMAP4rev1 " + this.kCapabilities.join(" ");
+ if (this.kAuthSchemes.length > 0) {
+ capa += " AUTH=" + this.kAuthSchemes.join(" AUTH=");
+ }
+ capa += "\0OK CAPABILITY completed";
+ return capa;
+ }
+ CLIENTID(args) {
+ return "OK Recognized a valid CLIENTID command, used for authentication methods";
+ }
+ LOGOUT(args) {
+ this.closing = true;
+ if (this._selectedMailbox) {
+ this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
+ }
+ this._state = IMAP_STATE_NOT_AUTHED;
+ return "* BYE IMAP4rev1 Logging out\0OK LOGOUT completed";
+ }
+ NOOP(args) {
+ return "OK NOOP completed";
+ }
+ STARTTLS(args) {
+ // simulate annoying server that drops connection on STARTTLS
+ if (this.dropOnStartTLS) {
+ this.closing = true;
+ return "";
+ }
+ return "BAD maild doesn't support TLS ATM";
+ }
+ _nextAuthFunction = undefined;
+ AUTHENTICATE(args) {
+ var scheme = args[0]; // already uppercased by type "atom"
+ // |scheme| contained in |kAuthSchemes|?
+ if (
+ !this.kAuthSchemes.some(function (s) {
+ return s == scheme;
+ })
+ ) {
+ return "-ERR AUTH " + scheme + " not supported";
+ }
+
+ var func = this._kAuthSchemeStartFunction[scheme];
+ if (!func || typeof func != "function") {
+ return (
+ "BAD I just pretended to implement AUTH " + scheme + ", but I don't"
+ );
+ }
+ return func.apply(this, args.slice(1));
+ }
+ LOGIN(args) {
+ if (
+ this.kCapabilities.some(function (c) {
+ return c == "LOGINDISABLED";
+ })
+ ) {
+ return "BAD old-style LOGIN is disabled, use AUTHENTICATE";
+ }
+ if (args[0] == this.kUsername && args[1] == this.kPassword) {
+ this._state = IMAP_STATE_AUTHED;
+ return "OK authenticated";
+ }
+ return "BAD invalid password, I won't authenticate you";
+ }
+ SELECT(args) {
+ var box = this._daemon.getMailbox(args[0]);
+ if (!box) {
+ return "NO no such mailbox";
+ }
+
+ if (this._selectedMailbox) {
+ this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
+ }
+ this._state = IMAP_STATE_SELECTED;
+ this._selectedMailbox = box;
+ this._readOnly = false;
+
+ var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0";
+ response += "* " + box._messages.length + " EXISTS\0* ";
+ response += box._messages.reduce(function (count, message) {
+ return count + (message.recent ? 1 : 0);
+ }, 0);
+ response += " RECENT\0";
+ for (var i = 0; i < box._messages.length; i++) {
+ if (!box._messages[i].flags.includes("\\Seen")) {
+ response += "* OK [UNSEEN " + (i + 1) + "]\0";
+ break;
+ }
+ }
+ response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0";
+ response += "* OK [UIDNEXT " + box.uidnext + "]\0";
+ if ("uidvalidity" in box) {
+ response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0";
+ }
+ return response + "OK [READ-WRITE] SELECT completed";
+ }
+ EXAMINE(args) {
+ var box = this._daemon.getMailbox(args[0]);
+ if (!box) {
+ return "NO no such mailbox";
+ }
+
+ if (this._selectedMailbox) {
+ this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
+ }
+ this._state = IMAP_STATE_SELECTED;
+ this._selectedMailbox = box;
+ this._readOnly = true;
+
+ var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0";
+ response += "* " + box._messages.length + " EXISTS\0* ";
+ response += box._messages.reduce(function (count, message) {
+ return count + (message.recent ? 1 : 0);
+ }, 0);
+ response += " RECENT\0";
+ for (var i = 0; i < box._messages.length; i++) {
+ if (!box._messages[i].flags.includes("\\Seen")) {
+ response += "* OK [UNSEEN " + (i + 1) + "]\0";
+ break;
+ }
+ }
+ response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0";
+ response += "* OK [UIDNEXT " + box.uidnext + "]\0";
+ response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0";
+ return response + "OK [READ-ONLY] EXAMINE completed";
+ }
+ CREATE(args) {
+ if (this._daemon.getMailbox(args[0])) {
+ return "NO mailbox already exists";
+ }
+ if (!this._daemon.createMailbox(args[0])) {
+ return "NO cannot create mailbox";
+ }
+ return "OK CREATE completed";
+ }
+ DELETE(args) {
+ var mbox = this._daemon.getMailbox(args[0]);
+ if (!mbox || mbox.name == "") {
+ return "NO no such mailbox";
+ }
+ if (mbox._children.length > 0) {
+ for (let i = 0; i < mbox.flags.length; i++) {
+ if (mbox.flags[i] == "\\Noselect") {
+ return "NO cannot delete mailbox";
+ }
+ }
+ }
+ this._daemon.deleteMailbox(mbox);
+ return "OK DELETE completed";
+ }
+ RENAME(args) {
+ var mbox = this._daemon.getMailbox(args[0]);
+ if (!mbox || mbox.name == "") {
+ return "NO no such mailbox";
+ }
+ if (!this._daemon.createMailbox(args[1], mbox)) {
+ return "NO cannot rename mailbox";
+ }
+ return "OK RENAME completed";
+ }
+ SUBSCRIBE(args) {
+ var mailbox = this._daemon.getMailbox(args[0]);
+ if (!mailbox) {
+ return "NO error in subscribing";
+ }
+ mailbox.subscribed = true;
+ return "OK SUBSCRIBE completed";
+ }
+ UNSUBSCRIBE(args) {
+ var mailbox = this._daemon.getMailbox(args[0]);
+ if (mailbox) {
+ mailbox.subscribed = false;
+ }
+ return "OK UNSUBSCRIBE completed";
+ }
+ LIST(args) {
+ // even though this is the LIST function for RFC 3501, code for
+ // LIST-EXTENDED (RFC 5258) is included here to keep things simple and
+ // avoid duplication. We can get away with this because the _treatArgs
+ // function filters out invalid args for servers that don't support
+ // LIST-EXTENDED before they even get here.
+
+ let listFunctionName = "_LIST";
+ // check for optional list selection options argument used by LIST-EXTENDED
+ // and other related RFCs
+ if (args.length == 3 || (args.length > 3 && args[3] == "RETURN")) {
+ let selectionOptions = args.shift();
+ selectionOptions = selectionOptions.toString().split(" ");
+ selectionOptions.sort();
+ for (let option of selectionOptions) {
+ listFunctionName += "_" + option.replace(/-/g, "_");
+ }
+ }
+ // check for optional list return options argument used by LIST-EXTENDED
+ // and other related RFCs
+ if (
+ (args.length > 2 && args[2] == "RETURN") ||
+ this.kCapabilities.includes("CHILDREN")
+ ) {
+ listFunctionName += "_RETURN";
+ let returnOptions = args[3] ? args[3].toString().split(" ") : [];
+ if (
+ this.kCapabilities.includes("CHILDREN") &&
+ !returnOptions.includes("CHILDREN")
+ ) {
+ returnOptions.push("CHILDREN");
+ }
+ returnOptions.sort();
+ for (let option of returnOptions) {
+ listFunctionName += "_" + option.replace(/-/g, "_");
+ }
+ }
+ if (!this[listFunctionName]) {
+ return "BAD unknown LIST request options";
+ }
+
+ let base = this._daemon.getMailbox(args[0]);
+ if (!base) {
+ return "NO no such mailbox";
+ }
+ let requestedBoxes;
+ // check for multiple mailbox patterns used by LIST-EXTENDED
+ // and other related RFCs
+ if (args[1].startsWith("(")) {
+ requestedBoxes = parseCommand(args[1])[0];
+ } else {
+ requestedBoxes = [args[1]];
+ }
+ let response = "";
+ for (let requestedBox of requestedBoxes) {
+ let people = base.matchKids(requestedBox);
+ for (let box of people) {
+ response += this[listFunctionName](box);
+ }
+ }
+ return response + "OK LIST completed";
+ }
+ // _LIST is the standard LIST command response
+ _LIST(aBox) {
+ if (aBox.nonExistent) {
+ return "";
+ }
+ return (
+ "* LIST (" +
+ aBox.flags.join(" ") +
+ ') "' +
+ aBox.delimiter +
+ '" "' +
+ aBox.displayName +
+ '"\0'
+ );
+ }
+ LSUB(args) {
+ var base = this._daemon.getMailbox(args[0]);
+ if (!base) {
+ return "NO no such mailbox";
+ }
+ var people = base.matchKids(args[1]);
+ var response = "";
+ for (var box of people) {
+ if (box.subscribed) {
+ response +=
+ '* LSUB () "' + box.delimiter + '" "' + box.displayName + '"\0';
+ }
+ }
+ return response + "OK LSUB completed";
+ }
+ STATUS(args) {
+ var box = this._daemon.getMailbox(args[0]);
+ if (!box) {
+ return "NO no such mailbox exists";
+ }
+ for (let i = 0; i < box.flags.length; i++) {
+ if (box.flags[i] == "\\Noselect") {
+ return "NO STATUS not allowed on Noselect folder";
+ }
+ }
+ var parts = [];
+ for (var status of args[1]) {
+ var line = status + " ";
+ switch (status) {
+ case "MESSAGES":
+ line += box._messages.length;
+ break;
+ case "RECENT":
+ line += box._messages.reduce(function (count, message) {
+ return count + (message.recent ? 1 : 0);
+ }, 0);
+ break;
+ case "UIDNEXT":
+ line += box.uidnext;
+ break;
+ case "UIDVALIDITY":
+ line += box.uidvalidity;
+ break;
+ case "UNSEEN":
+ line += box._messages.reduce(function (count, message) {
+ return count + (message.flags.includes("\\Seen") ? 0 : 1);
+ }, 0);
+ break;
+ default:
+ return "BAD unknown status flag: " + status;
+ }
+ parts.push(line);
+ }
+ return (
+ '* STATUS "' +
+ args[0] +
+ '" (' +
+ parts.join(" ") +
+ ")\0OK STATUS completed"
+ );
+ }
+ APPEND(args) {
+ var mailbox = this._daemon.getMailbox(args[0]);
+ if (!mailbox) {
+ return "NO [TRYCREATE] no such mailbox";
+ }
+ var flags, date, text;
+ if (args.length == 3) {
+ if (args[1] instanceof Date) {
+ flags = [];
+ date = args[1];
+ } else {
+ flags = args[1];
+ date = Date.now();
+ }
+ text = args[2];
+ } else if (args.length == 4) {
+ flags = args[1];
+ date = args[2];
+ text = args[3];
+ } else {
+ flags = [];
+ date = Date.now();
+ text = args[1];
+ }
+ var msg = new ImapMessage(
+ "data:text/plain," + encodeURI(text),
+ mailbox.uidnext++,
+ flags
+ );
+ msg.recent = true;
+ msg.date = date;
+ mailbox.addMessage(msg);
+ return "OK APPEND complete";
+ }
+ CHECK(args) {
+ this._daemon.synchronize(this._selectedMailbox, false);
+ return "OK CHECK completed";
+ }
+ CLOSE(args) {
+ this._selectedMailbox.expunge();
+ this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
+ this._selectedMailbox = null;
+ this._state = IMAP_STATE_AUTHED;
+ return "OK CLOSE completed";
+ }
+ EXPUNGE(args) {
+ // Will be either empty or LF-terminated already
+ var response = this._selectedMailbox.expunge();
+ this._daemon.synchronize(this._selectedMailbox);
+ return response + "OK EXPUNGE completed";
+ }
+ SEARCH(args, uid) {
+ if (args[0] == "UNDELETED") {
+ let response = "* SEARCH";
+ let messages = this._selectedMailbox._messages;
+ for (let i = 0; i < messages.length; i++) {
+ if (!messages[i].flags.includes("\\Deleted")) {
+ response += " " + messages[i].uid;
+ }
+ }
+ response += "\0";
+ return response + "OK SEARCH COMPLETED\0";
+ }
+ return "BAD not here yet";
+ }
+ FETCH(args, uid) {
+ // Step 1: Get the messages to fetch
+ var ids = [];
+ var messages = this._parseSequenceSet(args[0], uid, ids);
+
+ // Step 2: Ensure that the fetching items are in a neat format
+ if (typeof args[1] == "string") {
+ if (args[1] in this.fetchMacroExpansions) {
+ args[1] = this.fetchMacroExpansions[args[1]];
+ } else {
+ args[1] = [args[1]];
+ }
+ }
+ if (uid && !args[1].includes("UID")) {
+ args[1].push("UID");
+ }
+
+ // Step 2.1: Preprocess the item fetch stack
+ var items = [],
+ prefix = undefined;
+ for (let item of args[1]) {
+ if (item.indexOf("[") > 0 && !item.includes("]")) {
+ // We want to append everything into an item until we find a ']'
+ prefix = item + " ";
+ continue;
+ }
+ if (prefix !== undefined) {
+ if (typeof item != "string" || !item.includes("]")) {
+ prefix +=
+ (typeof item == "string" ? item : "(" + item.join(" ") + ")") + " ";
+ continue;
+ }
+ // Replace superfluous space with a ']'.
+ prefix = prefix.substr(0, prefix.length - 1) + "]";
+ item = prefix;
+ prefix = undefined;
+ }
+ item = item.toUpperCase();
+ if (!items.includes(item)) {
+ items.push(item);
+ }
+ }
+
+ // Step 3: Fetch time!
+ var response = "";
+ for (var i = 0; i < messages.length; i++) {
+ response += "* " + ids[i] + " FETCH (";
+ var parts = [];
+ const flagsBefore = messages[i].flags.slice();
+ for (let item of items) {
+ // Brief explanation: an item like BODY[]<> can't be hardcoded easily,
+ // so we go for the initial alphanumeric substring, passing in the
+ // actual string as an optional second part.
+ var front = item.split(/[^A-Z0-9-]/, 1)[0];
+ var functionName = "_FETCH_" + front.replace(/-/g, "_");
+
+ if (!(functionName in this)) {
+ return "BAD can't fetch " + front;
+ }
+ try {
+ parts.push(this[functionName](messages[i], item));
+ } catch (ex) {
+ return "BAD error in fetching: " + ex;
+ }
+ }
+ const flagsAfter = messages[i].flags;
+ if (
+ !items.includes("FLAGS") &&
+ (flagsAfter.length != flagsBefore.length ||
+ flagsAfter.some((f, i) => f != flagsBefore[i]))
+ ) {
+ // Flags changed, send them too, even though they weren't requested.
+ parts.push(this._FETCH_FLAGS(messages[i], "FLAGS"));
+ }
+ response += parts.join(" ") + ")\0";
+ }
+ return response + "OK FETCH completed";
+ }
+ STORE(args, uid) {
+ var ids = [];
+ var messages = this._parseSequenceSet(args[0], uid, ids);
+
+ args[1] = args[1].toUpperCase();
+ var silent = args[1].includes(".SILENT", 1);
+ if (silent) {
+ args[1] = args[1].substring(0, args[1].indexOf("."));
+ }
+
+ if (typeof args[2] != "object") {
+ args[2] = [args[2]];
+ }
+
+ var response = "";
+ for (var i = 0; i < messages.length; i++) {
+ var message = messages[i];
+ switch (args[1]) {
+ case "FLAGS":
+ message.flags = args[2];
+ break;
+ case "+FLAGS":
+ for (let flag of args[2]) {
+ message.setFlag(flag);
+ }
+ break;
+ case "-FLAGS":
+ for (let flag of args[2]) {
+ var index;
+ if ((index = message.flags.indexOf(flag)) != -1) {
+ message.flags.splice(index, 1);
+ }
+ }
+ break;
+ default:
+ return "BAD change what now?";
+ }
+ response += "* " + ids[i] + " FETCH (FLAGS (";
+ response += message.flags.join(" ");
+ response += "))\0";
+ }
+ if (silent) {
+ response = "";
+ }
+ return response + "OK STORE completed";
+ }
+ COPY(args, uid) {
+ var messages = this._parseSequenceSet(args[0], uid);
+
+ var dest = this._daemon.getMailbox(args[1]);
+ if (!dest) {
+ return "NO [TRYCREATE] what mailbox?";
+ }
+
+ for (var message of messages) {
+ let newMessage = new ImapMessage(
+ message._URI,
+ dest.uidnext++,
+ message.flags
+ );
+ newMessage.recent = false;
+ dest.addMessage(newMessage);
+ }
+ if (this._daemon.copySleep > 0) {
+ // spin rudely for copyTimeout milliseconds.
+ let now = new Date();
+ let alarm;
+ let startingMSeconds = now.getTime();
+ while (true) {
+ alarm = new Date();
+ if (alarm.getTime() - startingMSeconds > this._daemon.copySleep) {
+ break;
+ }
+ }
+ }
+ return "OK COPY completed";
+ }
+ UID(args) {
+ var name = args.shift();
+ if (!this.kUidCommands.includes(name)) {
+ return "BAD illegal command " + name;
+ }
+
+ args = this._treatArgs(args, name);
+ return this[name](args, true);
+ }
+
+ postCommand(reader) {
+ if (this.closing) {
+ this.closing = false;
+ reader.closeSocket();
+ }
+ if (this.sendingLiteral) {
+ reader.preventLFMunge();
+ }
+ reader.setMultiline(this._multiline);
+ if (this._lastCommand == reader.watchWord) {
+ reader.stopTest();
+ }
+ }
+ onServerFault(e) {
+ return (
+ ("_tag" in this ? this._tag : "*") + " BAD Internal server error: " + e
+ );
+ }
+
+ // FETCH sub commands and helpers
+
+ fetchMacroExpansions = {
+ ALL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE" */],
+ FAST: ["FLAGS", "INTERNALDATE", "RFC822.SIZE"],
+ FULL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE", "BODY" */],
+ };
+ _parseSequenceSet(set, uid, ids /* optional */) {
+ if (typeof set == "number") {
+ if (uid) {
+ for (let i = 0; i < this._selectedMailbox._messages.length; i++) {
+ var message = this._selectedMailbox._messages[i];
+ if (message.uid == set) {
+ if (ids) {
+ ids.push(i + 1);
+ }
+ return [message];
+ }
+ }
+ return [];
+ }
+ if (!(set - 1 in this._selectedMailbox._messages)) {
+ return [];
+ }
+ if (ids) {
+ ids.push(set);
+ }
+ return [this._selectedMailbox._messages[set - 1]];
+ }
+
+ var daemon = this;
+ function part2num(part) {
+ if (part == "*") {
+ if (uid) {
+ return daemon._selectedMailbox._highestuid;
+ }
+ return daemon._selectedMailbox._messages.length;
+ }
+ let re = /[0-9]/g;
+ let num = part.match(re);
+ if (!num || num.length != part.length) {
+ throw new Error("BAD invalid UID " + part);
+ }
+ return parseInt(part);
+ }
+
+ var elements = set.split(/,/);
+ set = [];
+ for (var part of elements) {
+ if (!part.includes(":")) {
+ set.push(part2num(part));
+ } else {
+ var range = part.split(/:/);
+ range[0] = part2num(range[0]);
+ range[1] = part2num(range[1]);
+ if (range[0] > range[1]) {
+ let temp = range[1];
+ range[1] = range[0];
+ range[0] = temp;
+ }
+ for (let i = range[0]; i <= range[1]; i++) {
+ set.push(i);
+ }
+ }
+ }
+ set.sort();
+ for (let i = set.length - 1; i > 0; i--) {
+ if (set[i] == set[i - 1]) {
+ set.splice(i, 0);
+ }
+ }
+
+ if (!ids) {
+ ids = [];
+ }
+ var messages;
+ if (uid) {
+ messages = this._selectedMailbox._messages.filter(function (msg, i) {
+ if (!set.includes(msg.uid)) {
+ return false;
+ }
+ ids.push(i + 1);
+ return true;
+ });
+ } else {
+ messages = [];
+ for (var id of set) {
+ if (id - 1 in this._selectedMailbox._messages) {
+ ids.push(id);
+ messages.push(this._selectedMailbox._messages[id - 1]);
+ }
+ }
+ }
+ return messages;
+ }
+ _FETCH_BODY(message, query) {
+ if (query == "BODY") {
+ return "BODYSTRUCTURE " + bodystructure(message.getText(), false);
+ }
+ // parts = [ name, section, empty, {, partial, empty } ]
+ var parts = query.split(/[[\]<>]/);
+
+ if (parts[0] != "BODY.PEEK" && !this._readOnly) {
+ message.setFlag("\\Seen");
+ }
+
+ if (parts[3]) {
+ parts[3] = parts[3].split(/\./).map(function (e) {
+ return parseInt(e);
+ });
+ }
+
+ if (parts[1].length == 0) {
+ // Easy case: we have BODY[], just send the message...
+ let response = "BODY[]";
+ var text;
+ if (parts[3]) {
+ response += "<" + parts[3][0] + ">";
+ text = message.getText(parts[3][0], parts[3][1]);
+ } else {
+ text = message.getText();
+ }
+ response += " {" + text.length + "}\r\n";
+ response += text;
+ return response;
+ }
+
+ // What's inside the command?
+ var data = /((?:\d+\.)*\d+)(?:\.([^ ]+))?/.exec(parts[1]);
+ var partNum;
+ if (data) {
+ partNum = data[1];
+ query = data[2];
+ } else {
+ partNum = "";
+ if (parts[1].includes(" ", 1)) {
+ query = parts[1].substring(0, parts[1].indexOf(" "));
+ } else {
+ query = parts[1];
+ }
+ }
+ var queryArgs;
+ if (parts[1].includes(" ", 1)) {
+ queryArgs = parseCommand(parts[1].substr(parts[1].indexOf(" ")))[0];
+ } else {
+ queryArgs = [];
+ }
+
+ // Now we have three parameters representing the part number (empty for top-
+ // level), the subportion representing what we want to find (empty for the
+ // body), and an array of arguments if we have a subquery. If we made an
+ // error here, it will pop until it gets to FETCH, which will just pop at a
+ // BAD response, which is what should happen if the query is malformed.
+ // Now we dump it all off onto ImapMessage to mess with.
+
+ // Start off the response
+ let response = "BODY[" + parts[1] + "]";
+ if (parts[3]) {
+ response += "<" + parts[3][0] + ">";
+ }
+ response += " ";
+
+ data = "";
+ switch (query) {
+ case "":
+ case "TEXT":
+ data += message.getPartBody(partNum);
+ break;
+ case "HEADER": // I believe this specifies mime for an RFC822 message only
+ data += message.getPartHeaders(partNum).rawHeaderText + "\r\n";
+ break;
+ case "MIME":
+ data += message.getPartHeaders(partNum).rawHeaderText + "\r\n\r\n";
+ break;
+ case "HEADER.FIELDS": {
+ let joinList = [];
+ let headers = message.getPartHeaders(partNum);
+ for (let header of queryArgs) {
+ header = header.toLowerCase();
+ if (headers.has(header)) {
+ joinList.push(
+ headers
+ .getRawHeader(header)
+ .map(value => `${header}: ${value}`)
+ .join("\r\n")
+ );
+ }
+ }
+ data += joinList.join("\r\n") + "\r\n";
+ break;
+ }
+ case "HEADER.FIELDS.NOT": {
+ let joinList = [];
+ let headers = message.getPartHeaders(partNum);
+ for (let header of headers) {
+ if (!(header in queryArgs)) {
+ joinList.push(
+ headers
+ .getRawHeader(header)
+ .map(value => `${header}: ${value}`)
+ .join("\r\n")
+ );
+ }
+ }
+ data += joinList.join("\r\n") + "\r\n";
+ break;
+ }
+ default:
+ data += message.getPartBody(partNum);
+ }
+
+ this.sendingLiteral = true;
+ response += "{" + data.length + "}\r\n";
+ response += data;
+ return response;
+ }
+ _FETCH_BODYSTRUCTURE(message, query) {
+ return "BODYSTRUCTURE " + bodystructure(message.getText(), true);
+ }
+ // _FETCH_ENVELOPE,
+ _FETCH_FLAGS(message) {
+ var response = "FLAGS (";
+ response += message.flags.join(" ");
+ if (message.recent) {
+ response += " \\Recent";
+ }
+ response += ")";
+ return response;
+ }
+ _FETCH_INTERNALDATE(message) {
+ let date = message.date;
+ // Format timestamp as: "%d-%b-%Y %H:%M:%S %z" (%b in English).
+ let year = date.getFullYear().toString();
+ let month = date.toLocaleDateString("en-US", { month: "short" });
+ let day = date.getDate().toString();
+ let hours = date.getHours().toString().padStart(2, "0");
+ let minutes = date.getMinutes().toString().padStart(2, "0");
+ let seconds = date.getSeconds().toString().padStart(2, "0");
+ let offset = date.getTimezoneOffset();
+ let tzoff =
+ Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60);
+ let timeZone = (offset < 0 ? "+" : "-") + tzoff.toString().padStart(4, "0");
+
+ let response = 'INTERNALDATE "';
+ response += `${day}-${month}-${year} ${hours}:${minutes}:${seconds} ${timeZone}`;
+ response += '"';
+ return response;
+ }
+ _FETCH_RFC822(message, query) {
+ if (query == "RFC822") {
+ return this._FETCH_BODY(message, "BODY[]").replace("BODY[]", "RFC822");
+ }
+ if (query == "RFC822.HEADER") {
+ return this._FETCH_BODY(message, "BODY.PEEK[HEADER]").replace(
+ "BODY[HEADER]",
+ "RFC822.HEADER"
+ );
+ }
+ if (query == "RFC822.TEXT") {
+ return this._FETCH_BODY(message, "BODY[TEXT]").replace(
+ "BODY[TEXT]",
+ "RFC822.TEXT"
+ );
+ }
+
+ if (query == "RFC822.SIZE") {
+ var channel = message.channel;
+ var length = message.size ? message.size : channel.contentLength;
+ if (length == -1) {
+ var inputStream = channel.open();
+ length = inputStream.available();
+ inputStream.close();
+ }
+ return "RFC822.SIZE " + length;
+ }
+ throw new Error("Unknown item " + query);
+ }
+ _FETCH_UID(message) {
+ return "UID " + message.uid;
+ }
+}
+
+// IMAP4 RFC extensions
+// --------------------
+// Since there are so many extensions to IMAP, and since these extensions are
+// not strictly hierarchical (e.g., an RFC 2342-compliant server can also be
+// RFC 3516-compliant, but a server might only implement one of them), they
+// must be handled differently from other fakeserver implementations.
+// An extension is defined as follows: it is an object (not a function and
+// prototype pair!). This object is "mixed" into the handler via the helper
+// function mixinExtension, which applies appropriate magic to make the
+// handler compliant to the extension. Functions are added untransformed, but
+// both arrays and objects are handled by appending the values onto the
+// original state of the handler. Semantics apply as for the base itself.
+
+// Note that UIDPLUS (RFC4315) should be mixed in last (or at least after the
+// MOVE extension) because it changes behavior of that extension.
+var configurations = {
+ Cyrus: ["RFC2342", "RFC2195", "RFC5258"],
+ UW: ["RFC2342", "RFC2195"],
+ Dovecot: ["RFC2195", "RFC5258"],
+ Zimbra: ["RFC2197", "RFC2342", "RFC2195", "RFC5258"],
+ Exchange: ["RFC2342", "RFC2195"],
+ LEMONADE: ["RFC2342", "RFC2195"],
+ CUSTOM1: ["MOVE", "RFC4315", "CUSTOM"],
+ GMail: ["GMAIL", "RFC2197", "RFC2342", "RFC3348", "RFC4315"],
+};
+
+function mixinExtension(handler, extension) {
+ if (extension.preload) {
+ extension.preload(handler);
+ }
+
+ for (var property in extension) {
+ if (property == "preload") {
+ continue;
+ }
+ if (typeof extension[property] == "function") {
+ // This is a function, so we add it to the handler
+ handler[property] = extension[property];
+ } else if (extension[property] instanceof Array) {
+ // This is an array, so we append the values
+ if (!(property in handler)) {
+ handler[property] = [];
+ }
+ handler[property] = handler[property].concat(extension[property]);
+ } else if (property in handler) {
+ // This is an object, so we add in the values
+ // Hack to make arrays et al. work recursively
+ mixinExtension(handler[property], extension[property]);
+ } else {
+ handler[property] = extension[property];
+ }
+ }
+}
+
+// Support for Gmail extensions: XLIST and X-GM-EXT-1
+var IMAP_GMAIL_extension = {
+ preload(toBeThis) {
+ toBeThis._preGMAIL_STORE = toBeThis.STORE;
+ toBeThis._preGMAIL_STORE_argFormat = toBeThis._argFormat.STORE;
+ toBeThis._argFormat.STORE = ["number", "atom", "..."];
+ toBeThis._DEFAULT_LIST = toBeThis.LIST;
+ },
+ XLIST(args) {
+ // XLIST is really just SPECIAL-USE that does not conform to RFC 6154
+ return this.LIST(args);
+ },
+ LIST(args) {
+ // XLIST was deprecated, LIST implies SPECIAL-USE for Gmail.
+ args.push("RETURN");
+ args.push("SPECIAL-USE");
+ return this._DEFAULT_LIST(args);
+ },
+ _LIST_RETURN_CHILDREN(aBox) {
+ return IMAP_RFC5258_extension._LIST_RETURN_CHILDREN(aBox);
+ },
+ _LIST_RETURN_CHILDREN_SPECIAL_USE(aBox) {
+ if (aBox.nonExistent) {
+ return "";
+ }
+
+ let result = "* LIST (" + aBox.flags.join(" ");
+ if (aBox._children.length > 0) {
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\HasChildren";
+ } else if (!aBox.flags.includes("\\NoInferiors")) {
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\HasNoChildren";
+ }
+ if (aBox.specialUseFlag && aBox.specialUseFlag.length > 0) {
+ result += " " + aBox.specialUseFlag;
+ }
+ result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
+ return result;
+ },
+ STORE(args, uid) {
+ let regex = /[+-]?FLAGS.*/;
+ if (regex.test(args[1])) {
+ // if we are storing flags, use the method that was overridden
+ this._argFormat = this._preGMAIL_STORE_argFormat;
+ args = this._treatArgs(args, "STORE");
+ return this._preGMAIL_STORE(args, uid);
+ }
+ // otherwise, handle gmail specific cases
+ let ids = [];
+ let messages = this._parseSequenceSet(args[0], uid, ids);
+ args[2] = formatArg(args[2], "string|(string)");
+ for (let i = 0; i < args[2].length; i++) {
+ if (args[2][i].includes(" ")) {
+ args[2][i] = '"' + args[2][i] + '"';
+ }
+ }
+ let response = "";
+ for (let i = 0; i < messages.length; i++) {
+ let message = messages[i];
+ switch (args[1]) {
+ case "X-GM-LABELS":
+ if (message.xGmLabels) {
+ message.xGmLabels = args[2];
+ } else {
+ return "BAD can't store X-GM-LABELS";
+ }
+ break;
+ case "+X-GM-LABELS":
+ if (message.xGmLabels) {
+ message.xGmLabels = message.xGmLabels.concat(args[2]);
+ } else {
+ return "BAD can't store X-GM-LABELS";
+ }
+ break;
+ case "-X-GM-LABELS":
+ if (message.xGmLabels) {
+ for (let i = 0; i < args[2].length; i++) {
+ let idx = message.xGmLabels.indexOf(args[2][i]);
+ if (idx != -1) {
+ message.xGmLabels.splice(idx, 1);
+ }
+ }
+ } else {
+ return "BAD can't store X-GM-LABELS";
+ }
+ break;
+ default:
+ return "BAD change what now?";
+ }
+ response += "* " + ids[i] + " FETCH (X-GM-LABELS (";
+ response += message.xGmLabels.join(" ");
+ response += "))\0";
+ }
+ return response + "OK STORE completed";
+ },
+ _FETCH_X_GM_MSGID(message) {
+ if (message.xGmMsgid) {
+ return "X-GM-MSGID " + message.xGmMsgid;
+ }
+ return "BAD can't fetch X-GM-MSGID";
+ },
+ _FETCH_X_GM_THRID(message) {
+ if (message.xGmThrid) {
+ return "X-GM-THRID " + message.xGmThrid;
+ }
+ return "BAD can't fetch X-GM-THRID";
+ },
+ _FETCH_X_GM_LABELS(message) {
+ if (message.xGmLabels) {
+ return "X-GM-LABELS " + message.xGmLabels;
+ }
+ return "BAD can't fetch X-GM-LABELS";
+ },
+ kCapabilities: ["XLIST", "X-GM-EXT-1"],
+ _argFormat: { XLIST: ["mailbox", "mailbox"] },
+ // Enabled in AUTHED and SELECTED states
+ _enabledCommands: { 1: ["XLIST"], 2: ["XLIST"] },
+};
+
+var IMAP_MOVE_extension = {
+ MOVE(args, uid) {
+ let messages = this._parseSequenceSet(args[0], uid);
+
+ let dest = this._daemon.getMailbox(args[1]);
+ if (!dest) {
+ return "NO [TRYCREATE] what mailbox?";
+ }
+
+ for (var message of messages) {
+ let newMessage = new ImapMessage(
+ message._URI,
+ dest.uidnext++,
+ message.flags
+ );
+ newMessage.recent = false;
+ dest.addMessage(newMessage);
+ }
+ let mailbox = this._selectedMailbox;
+ let response = "";
+ for (let i = messages.length - 1; i >= 0; i--) {
+ let msgIndex = mailbox._messages.indexOf(messages[i]);
+ if (msgIndex != -1) {
+ response += "* " + (msgIndex + 1) + " EXPUNGE\0";
+ mailbox._messages.splice(msgIndex, 1);
+ }
+ }
+ if (response.length > 0) {
+ delete mailbox.__highestuid;
+ }
+
+ return response + "OK MOVE completed";
+ },
+ kCapabilities: ["MOVE"],
+ kUidCommands: ["MOVE"],
+ _argFormat: { MOVE: ["number", "mailbox"] },
+ // Enabled in SELECTED state
+ _enabledCommands: { 2: ["MOVE"] },
+};
+
+// Provides methods for testing fetchCustomAttribute and issueCustomCommand
+var IMAP_CUSTOM_extension = {
+ preload(toBeThis) {
+ toBeThis._preCUSTOM_STORE = toBeThis.STORE;
+ toBeThis._preCUSTOM_STORE_argFormat = toBeThis._argFormat.STORE;
+ toBeThis._argFormat.STORE = ["number", "atom", "..."];
+ },
+ STORE(args, uid) {
+ let regex = /[+-]?FLAGS.*/;
+ if (regex.test(args[1])) {
+ // if we are storing flags, use the method that was overridden
+ this._argFormat = this._preCUSTOM_STORE_argFormat;
+ args = this._treatArgs(args, "STORE");
+ return this._preCUSTOM_STORE(args, uid);
+ }
+ // otherwise, handle custom attribute
+ let ids = [];
+ let messages = this._parseSequenceSet(args[0], uid, ids);
+ args[2] = formatArg(args[2], "string|(string)");
+ for (let i = 0; i < args[2].length; i++) {
+ if (args[2][i].includes(" ")) {
+ args[2][i] = '"' + args[2][i] + '"';
+ }
+ }
+ let response = "";
+ for (let i = 0; i < messages.length; i++) {
+ let message = messages[i];
+ switch (args[1]) {
+ case "X-CUSTOM-VALUE":
+ if (message.xCustomValue && args[2].length == 1) {
+ message.xCustomValue = args[2][0];
+ } else {
+ return "BAD can't store X-CUSTOM-VALUE";
+ }
+ break;
+ case "X-CUSTOM-LIST":
+ if (message.xCustomList) {
+ message.xCustomList = args[2];
+ } else {
+ return "BAD can't store X-CUSTOM-LIST";
+ }
+ break;
+ case "+X-CUSTOM-LIST":
+ if (message.xCustomList) {
+ message.xCustomList = message.xCustomList.concat(args[2]);
+ } else {
+ return "BAD can't store X-CUSTOM-LIST";
+ }
+ break;
+ case "-X-CUSTOM-LIST":
+ if (message.xCustomList) {
+ for (let i = 0; i < args[2].length; i++) {
+ let idx = message.xCustomList.indexOf(args[2][i]);
+ if (idx != -1) {
+ message.xCustomList.splice(idx, 1);
+ }
+ }
+ } else {
+ return "BAD can't store X-CUSTOM-LIST";
+ }
+ break;
+ default:
+ return "BAD change what now?";
+ }
+ response += "* " + ids[i] + " FETCH (X-CUSTOM-LIST (";
+ response += message.xCustomList.join(" ");
+ response += "))\0";
+ }
+ return response + "OK STORE completed";
+ },
+ _FETCH_X_CUSTOM_VALUE(message) {
+ if (message.xCustomValue) {
+ return "X-CUSTOM-VALUE " + message.xCustomValue;
+ }
+ return "BAD can't fetch X-CUSTOM-VALUE";
+ },
+ _FETCH_X_CUSTOM_LIST(message) {
+ if (message.xCustomList) {
+ return "X-CUSTOM-LIST (" + message.xCustomList.join(" ") + ")";
+ }
+ return "BAD can't fetch X-CUSTOM-LIST";
+ },
+ kCapabilities: ["X-CUSTOM1"],
+};
+
+// RFC 2197: ID
+var IMAP_RFC2197_extension = {
+ ID(args) {
+ let clientID = "(";
+ for (let i of args) {
+ clientID += '"' + i + '"';
+ }
+
+ clientID += ")";
+ let clientStrings = clientID.split(",");
+ clientID = "";
+ for (let i of clientStrings) {
+ clientID += '"' + i + '" ';
+ }
+ clientID = clientID.slice(1, clientID.length - 3);
+ clientID += ")";
+ this._daemon.clientID = clientID;
+ return "* ID " + this._daemon.idResponse + "\0OK Success";
+ },
+ kCapabilities: ["ID"],
+ _argFormat: { ID: ["(string)"] },
+ _enabledCommands: { 1: ["ID"], 2: ["ID"] },
+};
+
+// RFC 2342: IMAP4 Namespace (NAMESPACE)
+var IMAP_RFC2342_extension = {
+ NAMESPACE(args) {
+ var namespaces = [[], [], []];
+ for (let namespace of this._daemon.namespaces) {
+ namespaces[namespace.type].push(namespace);
+ }
+
+ var response = "* NAMESPACE";
+ for (var type of namespaces) {
+ if (type.length == 0) {
+ response += " NIL";
+ continue;
+ }
+ response += " (";
+ for (let namespace of type) {
+ response += '("';
+ response += namespace.displayName;
+ response += '" "';
+ response += namespace.delimiter;
+ response += '")';
+ }
+ response += ")";
+ }
+ response += "\0OK NAMESPACE command completed";
+ return response;
+ },
+ kCapabilities: ["NAMESPACE"],
+ _argFormat: { NAMESPACE: [] },
+ // Enabled in AUTHED and SELECTED states
+ _enabledCommands: { 1: ["NAMESPACE"], 2: ["NAMESPACE"] },
+};
+
+// RFC 3348 Child Mailbox (CHILDREN)
+var IMAP_RFC3348_extension = {
+ kCapabilities: ["CHILDREN"],
+};
+
+// RFC 4315: UIDPLUS
+var IMAP_RFC4315_extension = {
+ preload(toBeThis) {
+ toBeThis._preRFC4315UID = toBeThis.UID;
+ toBeThis._preRFC4315APPEND = toBeThis.APPEND;
+ toBeThis._preRFC4315COPY = toBeThis.COPY;
+ toBeThis._preRFC4315MOVE = toBeThis.MOVE;
+ },
+ UID(args) {
+ // XXX: UID EXPUNGE is not supported.
+ return this._preRFC4315UID(args);
+ },
+ APPEND(args) {
+ let response = this._preRFC4315APPEND(args);
+ if (response.indexOf("OK") == 0) {
+ let mailbox = this._daemon.getMailbox(args[0]);
+ let uid = mailbox.uidnext - 1;
+ response =
+ "OK [APPENDUID " +
+ mailbox.uidvalidity +
+ " " +
+ uid +
+ "]" +
+ response.substring(2);
+ }
+ return response;
+ },
+ COPY(args) {
+ let mailbox = this._daemon.getMailbox(args[0]);
+ if (mailbox) {
+ var first = mailbox.uidnext;
+ }
+ let response = this._preRFC4315COPY(args);
+ if (response.indexOf("OK") == 0) {
+ let last = mailbox.uidnext - 1;
+ response =
+ "OK [COPYUID " +
+ this._selectedMailbox.uidvalidity +
+ " " +
+ args[0] +
+ " " +
+ first +
+ ":" +
+ last +
+ "]" +
+ response.substring(2);
+ }
+ return response;
+ },
+ MOVE(args) {
+ let mailbox = this._daemon.getMailbox(args[1]);
+ if (mailbox) {
+ var first = mailbox.uidnext;
+ }
+ let response = this._preRFC4315MOVE(args);
+ if (response.includes("OK MOVE")) {
+ let last = mailbox.uidnext - 1;
+ response = response.replace(
+ "OK MOVE",
+ "OK [COPYUID " +
+ this._selectedMailbox.uidvalidity +
+ " " +
+ args[0] +
+ " " +
+ first +
+ ":" +
+ last +
+ "]"
+ );
+ }
+ return response;
+ },
+ kCapabilities: ["UIDPLUS"],
+};
+
+// RFC 5258: LIST-EXTENDED
+var IMAP_RFC5258_extension = {
+ preload(toBeThis) {
+ toBeThis._argFormat.LIST = [
+ "[(atom)]",
+ "mailbox",
+ "mailbox|(mailbox)",
+ "[atom]",
+ "[(atom)]",
+ ];
+ },
+ _LIST_SUBSCRIBED(aBox) {
+ if (!aBox.subscribed) {
+ return "";
+ }
+
+ let result = "* LIST (" + aBox.flags.join(" ");
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\Subscribed";
+ if (aBox.nonExistent) {
+ result += " \\NonExistent";
+ }
+ result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
+ return result;
+ },
+ _LIST_RETURN_CHILDREN(aBox) {
+ if (aBox.nonExistent) {
+ return "";
+ }
+
+ let result = "* LIST (" + aBox.flags.join(" ");
+ if (aBox._children.length > 0) {
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\HasChildren";
+ } else if (!aBox.flags.includes("\\NoInferiors")) {
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\HasNoChildren";
+ }
+ result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
+ return result;
+ },
+ _LIST_RETURN_SUBSCRIBED(aBox) {
+ if (aBox.nonExistent) {
+ return "";
+ }
+
+ let result = "* LIST (" + aBox.flags.join(" ");
+ if (aBox.subscribed) {
+ if (aBox.flags.length > 0) {
+ result += " ";
+ }
+ result += "\\Subscribed";
+ }
+ result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
+ return result;
+ },
+ // TODO implement _LIST_REMOTE, _LIST_RECURSIVEMATCH, _LIST_RETURN_SUBSCRIBED
+ // and all valid combinations thereof. Currently, nsImapServerResponseParser
+ // does not support any of these responses anyway.
+
+ kCapabilities: ["LIST-EXTENDED"],
+};
+
+/**
+ * This implements AUTH schemes. Could be moved into RFC3501 actually.
+ * The test can en-/disable auth schemes by modifying kAuthSchemes.
+ */
+var IMAP_RFC2195_extension = {
+ kAuthSchemes: ["CRAM-MD5", "PLAIN", "LOGIN"],
+
+ preload(handler) {
+ handler._kAuthSchemeStartFunction["CRAM-MD5"] = this.authCRAMStart;
+ handler._kAuthSchemeStartFunction.PLAIN = this.authPLAINStart;
+ handler._kAuthSchemeStartFunction.LOGIN = this.authLOGINStart;
+ },
+
+ authPLAINStart(lineRest) {
+ this._nextAuthFunction = this.authPLAINCred;
+ this._multiline = true;
+
+ return "+";
+ },
+ authPLAINCred(line) {
+ var req = AuthPLAIN.decodeLine(line);
+ if (req.username == this.kUsername && req.password == this.kPassword) {
+ this._state = IMAP_STATE_AUTHED;
+ return "OK Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
+ }
+ return "BAD Wrong username or password, crook!";
+ },
+
+ authCRAMStart(lineRest) {
+ this._nextAuthFunction = this.authCRAMDigest;
+ this._multiline = true;
+
+ this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost");
+ return "+ " + this._usedCRAMMD5Challenge;
+ },
+ authCRAMDigest(line) {
+ var req = AuthCRAM.decodeLine(line);
+ var expectedDigest = AuthCRAM.encodeCRAMMD5(
+ this._usedCRAMMD5Challenge,
+ this.kPassword
+ );
+ if (req.username == this.kUsername && req.digest == expectedDigest) {
+ this._state = IMAP_STATE_AUTHED;
+ return "OK Hello friend!";
+ }
+ return "BAD Wrong username or password, crook!";
+ },
+
+ authLOGINStart(lineRest) {
+ this._nextAuthFunction = this.authLOGINUsername;
+ this._multiline = true;
+
+ return "+ " + btoa("Username:");
+ },
+ authLOGINUsername(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kUsername) {
+ this._nextAuthFunction = this.authLOGINPassword;
+ } else {
+ // Don't return error yet, to not reveal valid usernames
+ this._nextAuthFunction = this.authLOGINBadUsername;
+ }
+ this._multiline = true;
+ return "+ " + btoa("Password:");
+ },
+ authLOGINBadUsername(line) {
+ return "BAD Wrong username or password, crook!";
+ },
+ authLOGINPassword(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kPassword) {
+ this._state = IMAP_STATE_AUTHED;
+ return "OK Hello friend! Where did you pull out this old auth scheme?";
+ }
+ return "BAD Wrong username or password, crook!";
+ },
+};
+
+// FETCH BODYSTRUCTURE
+function bodystructure(msg, extension) {
+ if (!msg || msg == "") {
+ return "";
+ }
+
+ // Use the mime parser emitter to generate body structure data. Most of the
+ // string will be built as we exit a part. Currently not working:
+ // 1. Some of the fields return NIL instead of trying to calculate them.
+ // 2. MESSAGE is missing the ENVELOPE and the lines at the end.
+ var bodystruct = "";
+ function paramToString(params) {
+ let paramList = [];
+ for (let [param, value] of params) {
+ paramList.push('"' + param.toUpperCase() + '" "' + value + '"');
+ }
+ return paramList.length == 0 ? "NIL" : "(" + paramList.join(" ") + ")";
+ }
+ var headerStack = [];
+ var BodyStructureEmitter = {
+ startPart(partNum, headers) {
+ bodystruct += "(";
+ headerStack.push(headers);
+ this.numLines = 0;
+ this.length = 0;
+ },
+ deliverPartData(partNum, data) {
+ this.length += data.length;
+ this.numLines += Array.from(data).filter(x => x == "\n").length;
+ },
+ endPart(partNum) {
+ // Grab the headers from before
+ let headers = headerStack.pop();
+ let contentType = headers.contentType;
+ if (contentType.mediatype == "multipart") {
+ bodystruct += ' "' + contentType.subtype.toUpperCase() + '"';
+ if (extension) {
+ bodystruct += " " + paramToString(contentType);
+ // XXX: implement the rest
+ bodystruct += " NIL NIL NIL";
+ }
+ } else {
+ bodystruct +=
+ '"' +
+ contentType.mediatype.toUpperCase() +
+ '" "' +
+ contentType.subtype.toUpperCase() +
+ '"';
+ bodystruct += " " + paramToString(contentType);
+
+ // XXX: Content ID, Content description
+ bodystruct += " NIL NIL";
+
+ let cte = headers.has("content-transfer-encoding")
+ ? headers.get("content-transfer-encoding")
+ : "7BIT";
+ bodystruct += ' "' + cte + '"';
+
+ bodystruct += " " + this.length;
+ if (contentType.mediatype == "text") {
+ bodystruct += " " + this.numLines;
+ }
+
+ // XXX: I don't want to implement these yet
+ if (extension) {
+ bodystruct += " NIL NIL NIL NIL";
+ }
+ }
+ bodystruct += ")";
+ },
+ };
+ MimeParser.parseSync(msg, BodyStructureEmitter, {});
+ return bodystruct;
+}
diff --git a/comm/mailnews/test/fakeserver/Ldapd.jsm b/comm/mailnews/test/fakeserver/Ldapd.jsm
new file mode 100644
index 0000000000..2206ebce81
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Ldapd.jsm
@@ -0,0 +1,665 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 = ["LDAPDaemon", "LDAPHandlerFn"];
+
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+/**
+ * This file provides fake LDAP server functionality, just enough to run
+ * our unit tests against.
+ *
+ * Currently:
+ * - it accepts any bind request (no authentication).
+ * - it supports searches, but only some types of filter.
+ * - it supports unbind (quit) requests.
+ * - all other requests are ignored.
+ *
+ * It should be extensible enough that extra features can be added as
+ * required.
+ */
+
+/*
+ * Helpers for application-neutral BER-encoding/decoding.
+ *
+ * BER is self-describing enough to allow us to parse it without knowing
+ * the meaning. So we can break down a binary stream into ints, strings,
+ * sequences etc... and then leave it up to the separate LDAP code to
+ * interpret the meaning of it.
+ *
+ * Clearest BER reference I've read:
+ * https://docs.oracle.com/cd/E19476-01/821-0510/def-basic-encoding-rules.html
+ */
+
+/**
+ * Encodes a BER length, returning an array of (wire-format) bytes.
+ * It's variable length encoding - smaller numbers are encoded with
+ * fewer bytes.
+ *
+ * @param {number} i - The length to encode.
+ */
+function encodeLength(i) {
+ if (i < 128) {
+ return [i];
+ }
+
+ let temp = i;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ bytes.unshift(0x80 | bytes.length);
+ return bytes;
+}
+
+/**
+ * Helper for encoding and decoding BER values.
+ * Each value is notionally a type-length-data triplet, although we just
+ * store type and an array for data (with the array knowing it's length).
+ * BERValue.data is held in raw form (wire format) for non-sequence values.
+ * For sequences and sets (constructed values), .data is empty, and
+ * instead .children is used to hold the contained BERValue objects.
+ */
+class BERValue {
+ constructor(type) {
+ this.type = type;
+ this.children = []; // only for constructed values (sequences)
+ this.data = []; // the raw data (empty for constructed ones)
+ }
+
+ /**
+ * Encode the BERValue to an array of bytes, ready to be written to the wire.
+ *
+ * @returns {Array.<number>} - The encoded bytes.
+ */
+ encode() {
+ let bytes = [];
+ if (this.isConstructed()) {
+ for (let c of this.children) {
+ bytes = bytes.concat(c.encode());
+ }
+ } else {
+ bytes = this.data;
+ }
+ return [this.type].concat(encodeLength(bytes.length), bytes);
+ }
+
+ // Functions to check class (upper two bits of type).
+ isUniversal() {
+ return (this.type & 0xc0) == 0x00;
+ }
+ isApplication() {
+ return (this.type & 0xc0) == 0x40;
+ }
+ isContextSpecific() {
+ return (this.type & 0xc0) == 0x80;
+ }
+ isPrivate() {
+ return (this.type & 0xc0) == 0xc0;
+ }
+
+ /*
+ * @return {boolean} - Is this value a constructed type a sequence or set?
+ * (As encoded in bit 5 of the type)
+ */
+ isConstructed() {
+ return !!(this.type & 0x20);
+ }
+
+ /**
+ * @returns {number} - The tag number of the type (the lower 5 bits).
+ */
+ tag() {
+ return this.type & 0x1f;
+ }
+
+ // Functions to check for some of the core universal types.
+ isNull() {
+ return this.type == 0x05;
+ }
+ isBoolean() {
+ return this.type == 0x01;
+ }
+ isInteger() {
+ return this.type == 0x02;
+ }
+ isOctetString() {
+ return this.type == 0x04;
+ }
+ isEnumerated() {
+ return this.type == 0x0a;
+ }
+
+ // Functions to interpret the value in particular ways.
+ // No type checking is performed, as application/context-specific
+ // types can also use these.
+
+ asBoolean() {
+ return this.data[0] != 0;
+ }
+
+ asInteger() {
+ let i = 0;
+ // TODO: handle negative numbers!
+ for (let b of this.data) {
+ i = (i << 8) | b;
+ }
+ return i;
+ }
+
+ asEnumerated() {
+ return this.asInteger();
+ }
+
+ // Helper to interpret an octet string as an ASCII string.
+ asString() {
+ // TODO: pass in expected encoding?
+ if (this.data.length > 0) {
+ return MailStringUtils.uint8ArrayToByteString(new Uint8Array(this.data));
+ }
+ return "";
+ }
+
+ // Static helpers to construct specific types of BERValue.
+ static newNull() {
+ let ber = new BERValue(0x05);
+ ber.data = [];
+ return ber;
+ }
+
+ static newBoolean(b) {
+ let ber = new BERValue(0x01);
+ ber.data = [b ? 0xff : 0x00];
+ return ber;
+ }
+
+ static newInteger(i) {
+ let ber = new BERValue(0x02);
+ // TODO: does this handle negative correctly?
+ while (i >= 128) {
+ ber.data.unshift(i & 255);
+ i >>= 8;
+ }
+ ber.data.unshift(i);
+ return ber;
+ }
+
+ static newEnumerated(i) {
+ let ber = BERValue.newInteger(i);
+ ber.type = 0x0a; // sneaky but valid.
+ return ber;
+ }
+
+ static newOctetString(bytes) {
+ let ber = new BERValue(0x04);
+ ber.data = bytes;
+ return ber;
+ }
+
+ /**
+ * Create an octet string from an ASCII string.
+ */
+ static newString(str) {
+ let ber = new BERValue(0x04);
+ if (str.length > 0) {
+ ber.data = Array.from(str, c => c.charCodeAt(0));
+ }
+ return ber;
+ }
+
+ /**
+ * Create a new sequence
+ *
+ * @param {number} type - BER type byte
+ * @param {Array.<BERValue>} children - The contents of the sequence.
+ */
+ static newSequence(type, children) {
+ let ber = new BERValue(type);
+ ber.children = children;
+ return ber;
+ }
+
+ /*
+ * A helper to dump out the value (and it's children) in a human-readable
+ * way.
+ */
+ dbug(prefix = "") {
+ let desc = "";
+ switch (this.type) {
+ case 0x01:
+ desc += `BOOLEAN (${this.asBoolean()})`;
+ break;
+ case 0x02:
+ desc += `INTEGER (${this.asInteger()})`;
+ break;
+ case 0x04:
+ desc += `OCTETSTRING ("${this.asString()}")`;
+ break;
+ case 0x05:
+ desc += `NULL`;
+ break;
+ case 0x0a:
+ desc += `ENUMERATED (${this.asEnumerated()})`;
+ break;
+ case 0x30:
+ desc += `SEQUENCE`;
+ break;
+ case 0x31:
+ desc += `SET`;
+ break;
+ default:
+ desc = `0x${this.type.toString(16)}`;
+ if (this.isConstructed()) {
+ desc += " SEQUENCE";
+ }
+ break;
+ }
+
+ switch (this.type & 0xc0) {
+ case 0x00:
+ break; // universal
+ case 0x40:
+ desc += " APPLICATION";
+ break;
+ case 0x80:
+ desc += " CONTEXT-SPECIFIC";
+ break;
+ case 0xc0:
+ desc += " PRIVATE";
+ break;
+ }
+
+ if (this.isConstructed()) {
+ desc += ` ${this.children.length} children`;
+ } else {
+ desc += ` ${this.data.length} bytes`;
+ }
+
+ // Dump out the beginning of the payload as raw bytes.
+ let rawdump = this.data.slice(0, 8).join(" ");
+ if (this.data.length > 8) {
+ rawdump += "...";
+ }
+
+ dump(`${prefix}${desc} ${rawdump}\n`);
+
+ for (let c of this.children) {
+ c.dbug(prefix + " ");
+ }
+ }
+}
+
+/**
+ * Parser to decode BER elements from a Connection.
+ */
+class BERParser {
+ constructor(conn) {
+ this._conn = conn;
+ }
+
+ /**
+ * Helper to fetch the next byte in the stream.
+ *
+ * @returns {number} - The byte.
+ */
+ async _nextByte() {
+ let buf = await this._conn.read(1);
+ return buf[0];
+ }
+
+ /**
+ * Helper to read a BER length field from the connection.
+ *
+ * @returns {Array.<number>} - 2 elements: [length, bytesconsumed].
+ */
+ async _readLength() {
+ let n = await this._nextByte();
+ if ((n & 0x80) == 0) {
+ return [n, 1]; // msb clear => single-byte encoding
+ }
+ // lower 7 bits are number of bytes encoding length (big-endian order).
+ n = n & 0x7f;
+ let len = 0;
+ for (let i = 0; i < n; ++i) {
+ len = (len << 8) + (await this._nextByte());
+ }
+ return [len, 1 + n];
+ }
+
+ /**
+ * Reads a single BERValue from the connection (including any children).
+ *
+ * @returns {Array.<number>} - 2 elements: [value, bytesconsumed].
+ */
+ async decodeBERValue() {
+ // BER values always encoded as TLV (type, length, value) triples,
+ // where type is a single byte, length can be a variable number of bytes
+ // and value is a byte string, of size length.
+ let type = await this._nextByte();
+ let [length, lensize] = await this._readLength();
+
+ let ber = new BERValue(type);
+ if (type & 0x20) {
+ // it's a sequence
+ let cnt = 0;
+ while (cnt < length) {
+ let [child, consumed] = await this.decodeBERValue();
+ cnt += consumed;
+ ber.children.push(child);
+ }
+ if (cnt != length) {
+ // All the bytes in the sequence must be accounted for.
+ // TODO: should define a specific BER error type so handler can
+ // detect and respond to BER decoding issues?
+ throw new Error("Mismatched length in sequence");
+ }
+ } else {
+ ber.data = await this._conn.read(length);
+ }
+ return [ber, 1 + lensize + length];
+ }
+}
+
+/*
+ * LDAP-specific code from here on.
+ */
+
+/*
+ * LDAPDaemon holds our LDAP database and has methods for
+ * searching and manipulating the data.
+ * So tests can set up test data here, shared by any number of LDAPHandlerFn
+ * connections.
+ */
+class LDAPDaemon {
+ constructor() {
+ // An entry is an object of the form:
+ // {dn:"....", attributes: {attr1: [val1], attr2:[val2,val3], ...}}
+ // Note that the attribute values are arrays (attributes can have multiple
+ // values in LDAP).
+ this.entries = {}; // We map dn to entry, to ensure dn is unique.
+ this.debug = false;
+ }
+
+ /**
+ * If set, will dump out assorted debugging info.
+ */
+ setDebug(yesno) {
+ this.debug = yesno;
+ }
+
+ /**
+ * Add entries to the LDAP database.
+ * Overwrites previous entries with same dn.
+ * since attributes can have multiple values, they should be arrays.
+ * For example:
+ * {dn: "...", {cn: ["Bob Smith"], ...}}
+ * But because that can be a pain, non-arrays values will be promoted.
+ * So we'll also accept:
+ * {dn: "...", {cn: "Bob Smith", ...}}
+ */
+ add(...entries) {
+ // Clone the data before munging it.
+ let entriesCopy = JSON.parse(JSON.stringify(entries));
+ for (let e of entriesCopy) {
+ if (e.dn === undefined || e.attributes === undefined) {
+ throw new Error("bad entry");
+ }
+
+ // Convert attr values to arrays, if required.
+ for (let [attr, val] of Object.entries(e.attributes)) {
+ if (!Array.isArray(val)) {
+ e.attributes[attr] = [val];
+ }
+ }
+ this.entries[e.dn] = e;
+ }
+ }
+
+ /**
+ * Find entries in our LDAP db.
+ *
+ * @param {BERValue} berFilter - BERValue containing the filter to apply.
+ * @returns {Array} - The matching entries.
+ */
+ search(berFilter) {
+ let f = this.buildFilter(berFilter);
+ return Object.values(this.entries).filter(f);
+ }
+
+ /**
+ * Recursively build a filter function from a BER-encoded filter.
+ * The resulting function accepts a single entry as parameter, and
+ * returns a bool to say if it passes the filter or not.
+ *
+ * @param {BERValue} ber - The filter.
+ * @returns {Function} - A function to test an entry against the filter.
+ */
+ buildFilter(ber) {
+ if (!ber.isContextSpecific()) {
+ throw new Error("Bad filter");
+ }
+
+ switch (ber.tag()) {
+ case 0: {
+ // and
+ if (ber.children.length < 1) {
+ throw new Error("Bad 'and' filter");
+ }
+ let subFilters = ber.children.map(this.buildFilter);
+ return function (e) {
+ return subFilters.every(filt => filt(e));
+ };
+ }
+ case 1: {
+ // or
+ if (ber.children.length < 1) {
+ throw new Error("Bad 'or' filter");
+ }
+ let subFilters = ber.children.map(this.buildFilter);
+ return function (e) {
+ return subFilters.some(filt => filt(e));
+ };
+ }
+ case 2: {
+ // not
+ if (ber.children.length != 1) {
+ throw new Error("Bad 'not' filter");
+ }
+ let subFilter = this.buildFilter(ber.children[0]); // one child
+ return function (e) {
+ return !subFilter(e);
+ };
+ }
+ case 3: {
+ // equalityMatch
+ if (ber.children.length != 2) {
+ throw new Error("Bad 'equality' filter");
+ }
+ let attrName = ber.children[0].asString().toLowerCase();
+ let attrVal = ber.children[1].asString().toLowerCase();
+ return function (e) {
+ let attrs = Object.keys(e.attributes).reduce(function (c, key) {
+ c[key.toLowerCase()] = e.attributes[key];
+ return c;
+ }, {});
+ return (
+ attrs[attrName] !== undefined &&
+ attrs[attrName].map(val => val.toLowerCase()).includes(attrVal)
+ );
+ };
+ }
+ case 7: {
+ // present
+ let attrName = ber.asString().toLowerCase();
+ return function (e) {
+ let attrs = Object.keys(e.attributes).reduce(function (c, key) {
+ c[key.toLowerCase()] = e.attributes[key];
+ return c;
+ }, {});
+ return attrs[attrName] !== undefined;
+ };
+ }
+ case 4: // substring (Probably need to implement this!)
+ case 5: // greaterOrEqual
+ case 6: // lessOrEqual
+ case 8: // approxMatch
+ case 9: // extensibleMatch
+ // UNSUPPORTED! just match everything.
+ dump("WARNING: unsupported filter\n");
+ return e => true;
+ default:
+ throw new Error("unknown filter");
+ }
+ }
+}
+
+/**
+ * Helper class to help break down LDAP handler into multiple functions.
+ * Used by LDAPHandlerFn, below.
+ * Handler state for a single connection (as opposed to any state common
+ * across all connections, which is handled by LDAPDaemon).
+ */
+class LDAPHandler {
+ constructor(conn, daemon) {
+ this._conn = conn;
+ this._daemon = daemon;
+ }
+
+ // handler run() should exit when done, or throw exception to crash out.
+ async run() {
+ let parser = new BERParser(this._conn);
+
+ while (1) {
+ let [msg] = await parser.decodeBERValue();
+ if (this._daemon.debug) {
+ dump("=== received ===\n");
+ msg.dbug("C: ");
+ }
+
+ if (
+ msg.type != 0x30 ||
+ msg.children.length < 2 ||
+ !msg.children[0].isInteger()
+ ) {
+ // badly formed message - TODO: bail out gracefully...
+ throw new Error("Bad message..");
+ }
+
+ let msgID = msg.children[0].asInteger();
+ let req = msg.children[1];
+
+ // Handle a teeny tiny subset of requests.
+ switch (req.type) {
+ case 0x60:
+ this.handleBindRequest(msgID, req);
+ break;
+ case 0x63:
+ this.handleSearchRequest(msgID, req);
+ break;
+ case 0x42: // unbindRequest (essentially a "quit").
+ return;
+ }
+ }
+ }
+
+ /**
+ * Send out an LDAP message.
+ *
+ * @param {number} msgID - The ID of the message we're responding to.
+ * @param {BERValue} payload - The message content.
+ */
+ async sendLDAPMessage(msgID, payload) {
+ let msg = BERValue.newSequence(0x30, [BERValue.newInteger(msgID), payload]);
+ if (this._daemon.debug) {
+ msg.dbug("S: ");
+ }
+ await this._conn.write(msg.encode());
+ }
+
+ async handleBindRequest(msgID, req) {
+ // Ignore the details, just say "OK!"
+ // TODO: Add some auth support here, would be handy for testing.
+ let bindResponse = new BERValue(0x61);
+ bindResponse.children = [
+ BERValue.newEnumerated(0), // resultCode 0=success
+ BERValue.newString(""), // matchedDN
+ BERValue.newString(""), // diagnosticMessage
+ ];
+
+ if (this._daemon.debug) {
+ dump("=== send bindResponse ===\n");
+ }
+ await this.sendLDAPMessage(msgID, bindResponse);
+ }
+
+ async handleSearchRequest(msgID, req) {
+ // Make sure all the parts we expect are present and of correct type.
+ if (
+ req.children.length < 8 ||
+ !req.children[0].isOctetString() ||
+ !req.children[1].isEnumerated() ||
+ !req.children[2].isEnumerated() ||
+ !req.children[3].isInteger() ||
+ !req.children[4].isInteger() ||
+ !req.children[5].isBoolean()
+ ) {
+ throw new Error("Bad search request!");
+ }
+
+ // Perform search
+ let filt = req.children[6];
+ let matches = this._daemon.search(filt);
+
+ // Send a searchResultEntry for each match
+ for (let match of matches) {
+ let dn = BERValue.newString(match.dn);
+ let attrList = new BERValue(0x30);
+ for (let [key, values] of Object.entries(match.attributes)) {
+ let valueSet = new BERValue(0x31);
+ for (let v of values) {
+ valueSet.children.push(BERValue.newString(v));
+ }
+
+ attrList.children.push(
+ BERValue.newSequence(0x30, [BERValue.newString(key), valueSet])
+ );
+ }
+
+ // 0x64 = searchResultEntry
+ let searchResultEntry = BERValue.newSequence(0x64, [dn, attrList]);
+
+ if (this._daemon.debug) {
+ dump(`=== send searchResultEntry ===\n`);
+ }
+ this.sendLDAPMessage(msgID, searchResultEntry);
+ }
+
+ //SearchResultDone ::= [APPLICATION 5] LDAPResult
+ let searchResultDone = new BERValue(0x65);
+ searchResultDone.children = [
+ BERValue.newEnumerated(0), // resultCode 0=success
+ BERValue.newString(""), // matchedDN
+ BERValue.newString(""), // diagnosticMessage
+ ];
+
+ if (this._daemon.debug) {
+ dump(`=== send searchResultDone ===\n`);
+ }
+ this.sendLDAPMessage(msgID, searchResultDone);
+ }
+}
+
+/**
+ * Handler function to deal with a connection to our LDAP server.
+ */
+async function LDAPHandlerFn(conn, daemon) {
+ let handler = new LDAPHandler(conn, daemon);
+ await handler.run();
+}
diff --git a/comm/mailnews/test/fakeserver/Maild.jsm b/comm/mailnews/test/fakeserver/Maild.jsm
new file mode 100644
index 0000000000..30647e1d56
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Maild.jsm
@@ -0,0 +1,566 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Much of the original code is taken from netwerk's httpserver implementation
+
+var EXPORTED_SYMBOLS = [
+ "nsMailServer",
+ "gThreadManager", // TODO: kill this export
+ "fsDebugNone",
+ "fsDebugAll",
+ "fsDebugRecv",
+ "fsDebugRecvSend",
+];
+
+var CC = Components.Constructor;
+
+/**
+ * The XPCOM thread manager. This declaration is obsolete and exists only
+ * because deleting it breaks several dozen tests at the moment.
+ */
+var gThreadManager = Services.tm;
+
+var fsDebugNone = 0;
+var fsDebugRecv = 1;
+var fsDebugRecvSend = 2;
+var fsDebugAll = 3;
+
+/**
+ * JavaScript constructors for commonly-used classes; precreating these is a
+ * speedup over doing the same from base principles. See the docs at
+ * http://developer.mozilla.org/en/Components.Constructor for details.
+ */
+var ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+var BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+// Time out after 3 minutes
+var TIMEOUT = 3 * 60 * 1000;
+
+/**
+ * The main server handling class. A fake server consists of three parts, this
+ * server implementation (which handles the network communication), the handler
+ * (which handles the state for a connection), and the daemon (which handles
+ * the state for the logical server). To make a new server, one needs to pass
+ * in a function to create handlers--not the handlers themselves--and the
+ * backend daemon. Since each handler presumably needs access to the logical
+ * server daemon, that is passed into the handler creation function. A new
+ * handler will be constructed for every connection made.
+ *
+ * As the core code is inherently single-threaded, it is guaranteed that all of
+ * the calls to the daemon will be made on the same thread, so you do not have
+ * to worry about reentrancy in daemon calls.
+ *
+ * Typical usage:
+ *
+ * function createHandler(daemon) {
+ * return new handler(daemon);
+ * }
+ * do_test_pending();
+ * var server = new nsMailServer(createHandler, serverDaemon);
+ * // Port to use. I tend to like using 1024 + default port number myself.
+ * server.start(port);
+ *
+ * // Set up a connection the server...
+ * server.performTest();
+ * transaction = server.playTransaction();
+ * // Verify that the transaction is correct...
+ *
+ * server.resetTest();
+ * // Set up second test...
+ * server.performTest();
+ * transaction = server.playTransaction();
+ *
+ * // Finished with tests
+ * server.stop();
+ *
+ * var thread = Services.tm.currentThread;
+ * while (thread.hasPendingEvents())
+ * thread.processNextEvent(true);
+ *
+ * do_test_finished();
+ */
+class nsMailServer {
+ constructor(handlerCreator, daemon) {
+ this._debug = fsDebugNone;
+
+ /** The port on which this server listens. */
+ this._port = -1;
+
+ /** The socket associated with this. */
+ this._socket = null;
+
+ /**
+ * True if the socket in this is closed (and closure notifications have been
+ * sent and processed if the socket was ever opened), false otherwise.
+ */
+ this._socketClosed = true;
+
+ /**
+ * Should we log transactions? This only matters if you want to inspect the
+ * protocol traffic. Defaults to true because this was written for protocol
+ * testing.
+ */
+ this._logTransactions = true;
+
+ this._handlerCreator = handlerCreator;
+ this._daemon = daemon;
+ this._readers = [];
+ this._test = false;
+ this._watchWord = undefined;
+
+ /**
+ * An array to hold refs to all the input streams below, so that they don't
+ * get GCed
+ */
+ this._inputStreams = [];
+ }
+
+ onSocketAccepted(socket, trans) {
+ if (this._debug != fsDebugNone) {
+ dump("Received Connection from " + trans.host + ":" + trans.port + "\n");
+ }
+
+ const SEGMENT_SIZE = 1024;
+ const SEGMENT_COUNT = 1024;
+ var input = trans
+ .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this._inputStreams.push(input);
+
+ var handler = this._handlerCreator(this._daemon);
+ var reader = new nsMailReader(
+ this,
+ handler,
+ trans,
+ this._debug,
+ this._logTransactions
+ );
+ this._readers.push(reader);
+
+ // Note: must use main thread here, or we might get a GC that will cause
+ // threadsafety assertions. We really need to fix XPConnect so that
+ // you can actually do things in multi-threaded JS. :-(
+ input.asyncWait(reader, 0, 0, Services.tm.mainThread);
+ this._test = true;
+ }
+
+ onStopListening(socket, status) {
+ if (this._debug != fsDebugNone) {
+ dump("Connection Lost " + status + "\n");
+ }
+
+ this._socketClosed = true;
+ // We've been killed or we've stopped, reset the handler to the original
+ // state (e.g. to require authentication again).
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i]._handler.resetTest();
+ this._readers[i]._realCloseSocket();
+ }
+ }
+
+ setDebugLevel(debug) {
+ this._debug = debug;
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i].setDebugLevel(debug);
+ }
+ }
+
+ start(port = -1) {
+ if (this._socket) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ if (port > 0) {
+ this._port = port;
+ }
+ this._socketClosed = false;
+
+ var socket = new ServerSocket(
+ this._port,
+ true, // loopback only
+ -1
+ ); // default number of pending connections
+
+ socket.asyncListen(this);
+ this._socket = socket;
+ }
+
+ stop() {
+ if (!this._socket) {
+ return;
+ }
+
+ this._socket.close();
+ this._socket = null;
+
+ for (let reader of this._readers) {
+ reader._realCloseSocket();
+ }
+
+ if (this._readers.some(e => e.observer.forced)) {
+ return;
+ }
+
+ // spin an event loop and wait for the socket-close notification
+ let thr = Services.tm.currentThread;
+ while (!this._socketClosed) {
+ // Don't wait for the next event, just in case there isn't one.
+ thr.processNextEvent(false);
+ }
+ }
+ stopTest() {
+ this._test = false;
+ }
+
+ get port() {
+ if (this._port == -1) {
+ this._port = this._socket.port;
+ }
+ return this._port;
+ }
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface = ChromeUtils.generateQI(["nsIServerSocketListener"]);
+
+ // NON-XPCOM PUBLIC API
+
+ /**
+ * Returns true if this server is not running (and is not in the process of
+ * serving any requests still to be processed when the server was last
+ * stopped after being run).
+ */
+ isStopped() {
+ return this._socketClosed;
+ }
+
+ /**
+ * Runs the test. It will not exit until the test has finished.
+ */
+ performTest(watchWord) {
+ this._watchWord = watchWord;
+
+ let thread = Services.tm.currentThread;
+ while (!this.isTestFinished()) {
+ thread.processNextEvent(false);
+ }
+ }
+
+ /**
+ * Returns true if the current processing test has finished.
+ */
+ isTestFinished() {
+ return this._readers.length > 0 && !this._test;
+ }
+
+ /**
+ * Returns the commands run between the server and client.
+ * The return is an object with two variables (us and them), both of which
+ * are arrays returning the commands given by each server.
+ */
+ playTransaction() {
+ if (this._readers.some(e => e.observer.forced)) {
+ throw new Error("Server timed out!");
+ }
+ if (this._readers.length == 1) {
+ return this._readers[0].transaction;
+ }
+ return this._readers.map(e => e.transaction);
+ }
+
+ /**
+ * Prepares for the next test.
+ */
+ resetTest() {
+ this._readers = this._readers.filter(function (reader) {
+ return reader._isRunning;
+ });
+ this._test = true;
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i]._handler.resetTest();
+ }
+ }
+}
+
+function readTo(input, count, arr) {
+ var old = new BinaryInputStream(input).readByteArray(count);
+ Array.prototype.push.apply(arr, old);
+}
+
+/**
+ * The nsMailReader service, which reads and handles the lines.
+ * All specific handling is passed off to the handler, which is responsible
+ * for maintaining its own state. The following commands are required for the
+ * handler object:
+ * onError Called when handler[command] does not exist with both the
+ * command and rest-of-line as arguments
+ * onStartup Called on initialization with no arguments
+ * onMultiline Called when in multiline with the entire line as an argument
+ * postCommand Called after every command with this reader as the argument
+ * [command] An untranslated command with the rest of the line as the
+ * argument. Defined as everything to the first space
+ *
+ * All functions, except onMultiline and postCommand, treat the
+ * returned value as the text to be sent to the client; a newline at the end
+ * may be added if it does not exist, and all lone newlines are converted to
+ * CRLF sequences.
+ *
+ * The return of postCommand is ignored. The return of onMultiline is a bit
+ * complicated: it may or may not return a response string (returning one is
+ * necessary to trigger the postCommand handler).
+ *
+ * This object has the following supplemental functions for use by handlers:
+ * closeSocket Performs a server-side socket closing
+ * setMultiline Sets the multiline mode based on the argument
+ */
+class nsMailReader {
+ constructor(server, handler, transport, debug, logTransaction) {
+ this._debug = debug;
+ this._server = server;
+ this._buffer = [];
+ this._lines = [];
+ this._handler = handler;
+ this._transport = transport;
+ // We don't seem to properly handle large streams when the buffer gets
+ // exhausted, which causes issues trying to test large messages. So just
+ // allow a really big buffer.
+ var output = transport.openOutputStream(
+ Ci.nsITransport.OPEN_BLOCKING,
+ 1024,
+ 4096
+ );
+ this._output = output;
+ if (logTransaction) {
+ this.transaction = { us: [], them: [] };
+ } else {
+ this.transaction = null;
+ }
+
+ // Send response line
+ var response = this._handler.onStartup();
+ response = response.replace(/([^\r])\n/g, "$1\r\n");
+ if (!response.endsWith("\n")) {
+ response = response + "\r\n";
+ }
+ if (this.transaction) {
+ this.transaction.us.push(response);
+ }
+ this._output.write(response, response.length);
+ this._output.flush();
+
+ this._multiline = false;
+
+ this._isRunning = true;
+
+ this.observer = {
+ server,
+ forced: false,
+ notify(timer) {
+ this.forced = true;
+ this.server.stopTest();
+ this.server.stop();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+ };
+ this.timer = Cc["@mozilla.org/timer;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsITimer);
+ this.timer.initWithCallback(
+ this.observer,
+ TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+
+ _findLines() {
+ var buf = this._buffer;
+ for (
+ var crlfLoc = buf.indexOf(13);
+ crlfLoc >= 0;
+ crlfLoc = buf.indexOf(13, crlfLoc + 1)
+ ) {
+ if (buf[crlfLoc + 1] == 10) {
+ break;
+ }
+ }
+ if (crlfLoc == -1) {
+ // We failed to find a newline
+ return;
+ }
+
+ var line = String.fromCharCode.apply(null, buf.slice(0, crlfLoc));
+ this._buffer = buf.slice(crlfLoc + 2);
+ this._lines.push(line);
+ this._findLines();
+ }
+
+ onInputStreamReady(stream) {
+ if (this.observer.forced) {
+ return;
+ }
+
+ this.timer.cancel();
+ try {
+ var bytes = stream.available();
+ } catch (e) {
+ // Someone, not us, has closed the stream. This means we can't get any
+ // more data from the stream, so we'll just go and close our socket.
+ this._realCloseSocket();
+ return;
+ }
+ readTo(stream, bytes, this._buffer);
+ this._findLines();
+
+ while (this._lines.length > 0) {
+ var line = this._lines.shift();
+
+ if (this._debug != fsDebugNone) {
+ dump("RECV: " + line + "\n");
+ }
+
+ var response;
+ try {
+ let command;
+ if (this._multiline) {
+ response = this._handler.onMultiline(line);
+
+ if (response === undefined) {
+ continue;
+ }
+ } else {
+ // Record the transaction
+ if (this.transaction) {
+ this.transaction.them.push(line);
+ }
+
+ // Find the command and splice it out...
+ var splitter = line.indexOf(" ");
+ command = splitter == -1 ? line : line.substring(0, splitter);
+ let args = splitter == -1 ? "" : line.substring(splitter + 1);
+
+ // By convention, commands are uppercase
+ command = command.toUpperCase();
+
+ if (this._debug == fsDebugAll) {
+ dump("Received command " + command + "\n");
+ }
+
+ if (command in this._handler) {
+ response = this._handler[command](args);
+ } else {
+ response = this._handler.onError(command, args);
+ }
+ }
+
+ this._preventLFMunge = false;
+ this._handler.postCommand(this);
+
+ if (this.watchWord && command == this.watchWord) {
+ this.stopTest();
+ }
+ } catch (e) {
+ response = this._handler.onServerFault(e);
+ if (e instanceof Error) {
+ dump(e.name + ": " + e.message + "\n");
+ dump("File: " + e.fileName + " Line: " + e.lineNumber + "\n");
+ dump("Stack trace:\n" + e.stack);
+ } else {
+ dump("Exception caught: " + e + "\n");
+ }
+ }
+
+ if (!this._preventLFMunge) {
+ response = response.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n");
+ }
+
+ if (!response.endsWith("\n")) {
+ response = response + "\r\n";
+ }
+
+ if (this._debug == fsDebugRecvSend) {
+ dump("SEND: " + response.split(" ", 1)[0] + "\n");
+ } else if (this._debug == fsDebugAll) {
+ var responses = response.split("\n");
+ responses.forEach(function (line) {
+ dump("SEND: " + line + "\n");
+ });
+ }
+
+ if (this.transaction) {
+ this.transaction.us.push(response);
+ }
+
+ try {
+ this._output.write(response, response.length);
+ this._output.flush();
+ } catch (ex) {
+ if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
+ dump("Stream closed whilst sending, this may be expected\n");
+ this._realCloseSocket();
+ } else {
+ // Some other issue, let the test see it.
+ throw ex;
+ }
+ }
+
+ if (this._signalStop) {
+ this._realCloseSocket();
+ this._signalStop = false;
+ }
+ }
+
+ if (this._isRunning) {
+ stream.asyncWait(this, 0, 0, Services.tm.currentThread);
+ this.timer.initWithCallback(
+ this.observer,
+ TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ }
+
+ closeSocket() {
+ this._signalStop = true;
+ }
+ _realCloseSocket() {
+ this._isRunning = false;
+ this._output.close();
+ this._transport.close(Cr.NS_OK);
+ this._server.stopTest();
+ }
+
+ setMultiline(multi) {
+ this._multiline = multi;
+ }
+
+ setDebugLevel(debug) {
+ this._debug = debug;
+ }
+
+ preventLFMunge() {
+ this._preventLFMunge = true;
+ }
+
+ get watchWord() {
+ return this._server._watchWord;
+ }
+
+ stopTest() {
+ this._server.stopTest();
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsIInputStreamCallback"]);
+}
diff --git a/comm/mailnews/test/fakeserver/Nntpd.jsm b/comm/mailnews/test/fakeserver/Nntpd.jsm
new file mode 100644
index 0000000000..f6d1be0a48
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Nntpd.jsm
@@ -0,0 +1,631 @@
+/* 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/. */
+
+// This file implements test NNTP servers
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+var EXPORTED_SYMBOLS = [
+ "NntpDaemon",
+ "NewsArticle",
+ "NNTP_POSTABLE",
+ "NNTP_REAL_LENGTH",
+ "NNTP_RFC977_handler",
+ "NNTP_RFC2980_handler",
+ "NNTP_RFC3977_handler",
+ "NNTP_Giganews_handler",
+ "NNTP_RFC4643_extension",
+];
+
+class NntpDaemon {
+ constructor(flags) {
+ this._groups = {};
+ this._messages = {};
+ this._flags = flags;
+ }
+ addGroup(group, postable) {
+ var flags = 0;
+ if (postable) {
+ flags |= NNTP_POSTABLE;
+ }
+ this._groups[group] = { keys: [], flags, nextKey: 1 };
+ }
+ addArticle(article) {
+ this._messages[article.messageID] = article;
+ for (let group of article.groups) {
+ if (group in this._groups) {
+ var key = this._groups[group].nextKey++;
+ this._groups[group][key] = article;
+ this._groups[group].keys.push(key);
+ }
+ }
+ }
+ addArticleToGroup(article, group, key) {
+ this._groups[group][key] = article;
+ this._messages[article.messageID] = article;
+ this._groups[group].keys.push(key);
+ if (this._groups[group].nextKey <= key) {
+ this._groups[group].nextKey = key + 1;
+ }
+ }
+ removeArticleFromGroup(groupName, key) {
+ let group = this._groups[groupName];
+ delete group[key];
+ group.keys = group.keys.filter(x => x != key);
+ }
+ getGroup(group) {
+ if (this._groups.hasOwnProperty(group)) {
+ return this._groups[group];
+ }
+ return null;
+ }
+ getGroupStats(group) {
+ if (group.keys.length == 0) {
+ return [0, 0, 0];
+ }
+ var min = 1 << 30;
+ var max = 0;
+ group.keys.forEach(function (key) {
+ if (key < min) {
+ min = key;
+ }
+ if (key > max) {
+ max = key;
+ }
+ });
+
+ var length;
+ if (hasFlag(this._flags, NNTP_REAL_LENGTH)) {
+ length = group.keys.length;
+ } else {
+ length = max - min + 1;
+ }
+
+ return [length, min, max];
+ }
+ getArticle(msgid) {
+ if (msgid in this._messages) {
+ return this._messages[msgid];
+ }
+ return null;
+ }
+}
+
+function NewsArticle(text) {
+ this.headers = new Map();
+ this.body = "";
+ this.messageID = "";
+ this.fullText = text;
+
+ var headerMap;
+ [headerMap, this.body] = MimeParser.extractHeadersAndBody(text);
+ for (var [header, values] of headerMap._rawHeaders) {
+ var value = values[0];
+ this.headers.set(header, value);
+ if (header == "message-id") {
+ var start = value.indexOf("<");
+ var end = value.indexOf(">", start);
+ this.messageID = value.substring(start, end + 1);
+ } else if (header == "newsgroups") {
+ this.groups = value.split(/[ \t]*,[ \t]*/);
+ }
+ }
+
+ // Add in non-existent fields
+ if (!this.headers.has("lines")) {
+ let lines = this.body.split("\n").length;
+ this.headers.set("lines", lines);
+ }
+}
+
+/**
+ * This function converts an NNTP wildmat into a regular expression.
+ *
+ * I don't know how accurate it is wrt i18n characters, but its primary usage
+ * right now is just XPAT, where i18n effects are utterly unspecified, so I am
+ * not too concerned.
+ *
+ * This also neglects cases where special characters are in [] blocks.
+ */
+function wildmat2regex(wildmat) {
+ // Special characters in regex that aren't special in wildmat
+ wildmat = wildmat.replace(/[$+.()|{}^]/, function (str) {
+ return "\\" + str;
+ });
+ wildmat = wildmat.replace(/(\\*)([*?])/, function (str, p1, p2) {
+ // TODO: This function appears to be wrong on closer inspection.
+ if (p1.length % 2 == 0) {
+ return p2 == "*" ? ".*" : ".";
+ }
+ return str;
+ });
+ return new RegExp(wildmat);
+}
+
+// NNTP FLAGS
+var NNTP_POSTABLE = 0x0001;
+
+var NNTP_REAL_LENGTH = 0x0100;
+
+function hasFlag(flags, flag) {
+ return (flags & flag) == flag;
+}
+
+// NNTP TEST SERVERS
+// -----------------
+// To be comprehensive about testing and fallback, we define these varying
+// levels of RFC-compliance:
+// * RFC 977 solely (there's not a lot there!)
+// * RFC 977 + 2980 (note that there are varying levels of this impl)
+// * RFC 3977 bare bones
+// * RFC 3977 full
+// * RFC 3977 + post-3977 extensions
+// * Giganews (Common newsserver for ISP stuff; highest importance)
+// * INN 2.4 (Gold standard common implementation; second highest importance)
+// Note too that we want various levels of brokenness:
+// * Perm errors that require login
+// * "I can't handle that" (e.g., news.mozilla.org only supports XOVER for
+// searching with XHDR)
+// * Naive group counts, missing articles
+// * Limitations on what can be posted
+
+// This handler implements the bare minimum required by RFC 977. Actually, not
+// even that much: IHAVE and SLAVE are not implemented, as those two are
+// explicitly server implementations.
+class NNTP_RFC977_handler {
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.resetTest();
+ }
+ resetTest() {
+ this.extraCommands = "";
+ this.articleKey = null;
+ this.group = null;
+ }
+ ARTICLE(args) {
+ var info = this._selectArticle(args, 220);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ response += info[0].fullText.replace(/^\./gm, "..");
+ response += ".";
+ return response;
+ }
+ BODY(args) {
+ var info = this._selectArticle(args, 222);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ response += info[0].body.replace(/^\./gm, "..");
+ response += ".";
+ return response;
+ }
+ GROUP(args) {
+ var group = this._daemon.getGroup(args);
+ if (group == null) {
+ return "411 no such news group";
+ }
+
+ this.group = group;
+ this.articleKey = 0 in this.group.keys ? this.group.keys[0] : null;
+
+ var stats = this._daemon.getGroupStats(group);
+ return (
+ "211 " +
+ stats[0] +
+ " " +
+ stats[1] +
+ " " +
+ stats[2] +
+ " " +
+ args +
+ " group selected"
+ );
+ }
+ HEAD(args) {
+ var info = this._selectArticle(args, 221);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ for (let [header, value] of info[0].headers) {
+ response += header + ": " + value + "\n";
+ }
+ response += ".";
+ return response;
+ }
+ HELP(args) {
+ var response = "100 Why certainly, here is my help:\n";
+ response += "Mozilla fake NNTP RFC 977 testing server";
+ response += "Commands supported:\n";
+ response += "\tARTICLE <message-id> | [nnn]\n";
+ response += "\tBODY\n";
+ response += "\tGROUP group\n";
+ response += "\tHEAD\n";
+ response += "\tHELP\n";
+ response += "\tLAST\n";
+ response += "\tLIST\n";
+ response += "\tNEWGROUPS\n";
+ response += "\tNEWNEWS\n";
+ response += "\tNEXT\n";
+ response += "\tPOST\n";
+ response += "\tQUIT\n";
+ response += "\tSTAT\n";
+ response += this.extraCommands;
+ response += ".";
+ return response;
+ }
+ LAST(args) {
+ if (this.group == null) {
+ return "412 no newsgroup selected";
+ }
+ if (this.articleKey == null) {
+ return "420 no current article has been selected";
+ }
+ return "502 Command not implemented";
+ }
+ LIST(args) {
+ var response = "215 list of newsgroup follows\n";
+ for (let groupname in this._daemon._groups) {
+ let group = this._daemon._groups[groupname];
+ let stats = this._daemon.getGroupStats(group);
+ response +=
+ groupname +
+ " " +
+ stats[1] +
+ " " +
+ stats[0] +
+ " " +
+ (hasFlag(group.flags, NNTP_POSTABLE) ? "y" : "n") +
+ "\n";
+ }
+ response += ".";
+ return response;
+ }
+ NEWGROUPS(args) {
+ return "502 Command not implemented";
+ }
+ NEWNEWS(args) {
+ return "502 Command not implemented";
+ }
+ NEXT(args) {
+ if (this.group == null) {
+ return "412 no newsgroup selected";
+ }
+ if (this.articleKey == null) {
+ return "420 no current article has been selected";
+ }
+ return "502 Command not implemented";
+ }
+ POST(args) {
+ this.posting = true;
+ this.post = "";
+ return "340 Please continue";
+ }
+ QUIT(args) {
+ this.closing = true;
+ return "205 closing connection - goodbye!";
+ }
+ STAT(args) {
+ var info = this._selectArticle(args, 223);
+ return info[1];
+ }
+ LISTGROUP(args) {
+ // Yes, I know this isn't RFC 977, but I doubt that mailnews will ever drop
+ // its requirement for this, so I'll stuff it in here anyways...
+ var group = args == "" ? this.group : this._daemon.getGroup(args);
+ if (group == null) {
+ return "411 This newsgroup does not exist";
+ }
+
+ var response = "211 Articles follow:\n";
+ for (let key of group.keys) {
+ response += key + "\n";
+ }
+ response += ".\n";
+ return response;
+ }
+
+ onError(command, args) {
+ return "500 command not recognized";
+ }
+ onServerFault(e) {
+ return "500 internal server error: " + e;
+ }
+ onStartup() {
+ this.closing = false;
+ this.group = null;
+ this.article = null;
+ this.posting = false;
+ return "200 posting allowed";
+ }
+ onMultiline(line) {
+ if (line == ".") {
+ if (this.posting) {
+ var article = new NewsArticle(this.post);
+ this._daemon.addArticle(article);
+ this.posting = false;
+ return "240 Wonderful article, your style is gorgeous!";
+ }
+ }
+
+ if (this.posting) {
+ if (line.startsWith(".")) {
+ line = line.substring(1);
+ }
+
+ this.post += line + "\n";
+ }
+
+ return undefined;
+ }
+ postCommand(reader) {
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ reader.setMultiline(this.posting);
+ }
+
+ /**
+ * Selects an article based on args.
+ *
+ * Returns an array of objects consisting of:
+ * # The selected article (or null if non was selected
+ * # The first line response
+ */
+ _selectArticle(args, responseCode) {
+ var art, key;
+ if (args == "") {
+ if (this.group == null) {
+ return [null, "412 no newsgroup has been selected"];
+ }
+ if (this.articleKey == null) {
+ return [null, "420 no current article has been selected"];
+ }
+
+ art = this.group[this.articleKey];
+ key = this.articleKey;
+ } else if (args.startsWith("<")) {
+ art = this._daemon.getArticle(args);
+ key = 0;
+
+ if (art == null) {
+ return [null, "430 no such article found"];
+ }
+ } else {
+ if (this.group == null) {
+ return [null, "412 no newsgroup has been selected"];
+ }
+
+ key = parseInt(args);
+ if (key in this.group) {
+ this.articleKey = key;
+ art = this.group[key];
+ } else {
+ return [null, "423 no such article number in this group"];
+ }
+ }
+
+ var respCode =
+ responseCode + " " + key + " " + art.messageID + " article selected";
+ return [art, respCode];
+ }
+}
+
+class NNTP_RFC2980_handler extends NNTP_RFC977_handler {
+ DATE(args) {
+ return "502 Command not implemented";
+ }
+ LIST(args) {
+ var index = args.indexOf(" ");
+ var command = index == -1 ? args : args.substring(0, index);
+ args = index == -1 ? "" : args.substring(index + 1);
+ command = command.toUpperCase();
+ if ("LIST_" + command in this) {
+ return this["LIST_" + command](args);
+ }
+ return super.LIST(command + " " + args);
+ }
+ LIST_ACTIVE(args) {
+ return super.LIST(args);
+ }
+ MODE(args) {
+ if (args == "READER") {
+ return this.onStartup();
+ }
+ return "500 What do you think you're trying to pull here?";
+ }
+ XHDR(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ args = args.split(" ");
+ var header = args[0].toLowerCase();
+ var found = false;
+ var response = "221 Headers abound\n";
+ for (let key of this._filterRange(args[1], this.group.keys)) {
+ if (!this.group[key].headers.has(header)) {
+ continue;
+ }
+ found = true;
+ response += key + " " + this.group[key].headers.get(header) + "\n";
+ }
+ if (!found) {
+ return "420 No such article";
+ }
+ response += ".";
+ return response;
+ }
+ XOVER(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ args = args.split(/ +/, 3);
+ var response = "224 List of articles\n";
+ for (let key of this._filterRange(args[0], this.group.keys)) {
+ response += key + "\t";
+ var article = this.group[key];
+ response +=
+ article.headers.get("subject") +
+ "\t" +
+ article.headers.get("from") +
+ "\t" +
+ article.headers.get("date") +
+ "\t" +
+ article.headers.get("message-id") +
+ "\t" +
+ (article.headers.get("references") || "") +
+ "\t" +
+ article.fullText.replace(/\r?\n/, "\r\n").length +
+ "\t" +
+ article.body.split(/\r?\n/).length +
+ "\t" +
+ (article.headers.get("xref") || "") +
+ "\n";
+ }
+ response += ".\n";
+ return response;
+ }
+ XPAT(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ /* XPAT header range ... */
+ args = args.split(/ +/, 3);
+ let header = args[0].toLowerCase();
+ let regex = wildmat2regex(args[2]);
+
+ let response = "221 Results follow\n";
+ for (let key of this._filterRange(args[1], this.group.keys)) {
+ let article = this.group[key];
+ if (
+ article.headers.has(header) &&
+ regex.test(article.headers.get(header))
+ ) {
+ response += key + " " + article.headers.get(header) + "\n";
+ }
+ }
+ return response + ".";
+ }
+
+ _filterRange(range, keys) {
+ let dash = range.indexOf("-");
+ let low, high;
+ if (dash < 0) {
+ low = high = parseInt(range);
+ } else {
+ low = parseInt(range.substring(0, dash));
+ if (dash < range.length - 1) {
+ high = range.substring(dash + 1);
+ } else {
+ // Everything is less than this.
+ high = 1.0 / 0.0;
+ }
+ }
+ return keys.filter(function (e) {
+ return low <= e && e <= high;
+ });
+ }
+}
+
+class NNTP_Giganews_handler extends NNTP_RFC2980_handler {
+ XHDR(args) {
+ var header = args.split(" ")[0].toLowerCase();
+ if (
+ header in ["subject", "from", "xref", "date", "message-id", "references"]
+ ) {
+ return super.XHDR(args);
+ }
+ return "503 unsupported header field";
+ }
+}
+
+class NNTP_RFC4643_extension extends NNTP_RFC2980_handler {
+ constructor(daemon) {
+ super(daemon);
+
+ this.extraCommands += "\tAUTHINFO USER\n";
+ this.extraCommands += "\tAUTHINFO PASS\n";
+ this.expectedUsername = "testnews";
+ this.expectedPassword = "newstest";
+ this.requireBoth = true;
+ this.authenticated = false;
+ this.usernameReceived = false;
+ }
+
+ AUTHINFO(args) {
+ if (this.authenticated) {
+ return "502 Command unavailable";
+ }
+
+ var argSplit = args.split(" ");
+ var action = argSplit[0];
+ var param = argSplit[1];
+
+ if (action == "user") {
+ if (this.usernameReceived) {
+ return "502 Command unavailable";
+ }
+
+ var expectUsername = this.lastGroupTried
+ ? this._daemon.groupCredentials[this.lastGroupTried][0]
+ : this.expectedUsername;
+ if (param != expectUsername) {
+ return "481 Authentication failed";
+ }
+
+ this.usernameReceived = true;
+ if (this.requireBoth) {
+ return "381 Password required";
+ }
+
+ this.authenticated = this.lastGroupTried ? this.lastGroupTried : true;
+ return "281 Authentication Accepted";
+ } else if (action == "pass") {
+ if (!this.requireBoth || !this.usernameReceived) {
+ return "482 Authentication commands issued out of sequence";
+ }
+
+ this.usernameReceived = false;
+
+ var expectPassword = this.lastGroupTried
+ ? this._daemon.groupCredentials[this.lastGroupTried][1]
+ : this.expectedPassword;
+ if (param != expectPassword) {
+ return "481 Authentication failed";
+ }
+
+ this.authenticated = this.lastGroupTried ? this.lastGroupTried : true;
+ return "281 Authentication Accepted";
+ }
+ return "502 Invalid Command";
+ }
+ LIST(args) {
+ if (this.authenticated) {
+ return args ? super.LIST(args) : "502 Invalid command: LIST";
+ }
+ return "480 Authentication required";
+ }
+ GROUP(args) {
+ if (
+ (this._daemon.groupCredentials != null && this.authenticated == args) ||
+ (this._daemon.groupCredentials == null && this.authenticated)
+ ) {
+ return super.GROUP(args);
+ }
+ if (this._daemon.groupCredentials != null) {
+ this.lastGroupTried = args;
+ }
+ return "480 Authentication required";
+ }
+}
diff --git a/comm/mailnews/test/fakeserver/Pop3d.jsm b/comm/mailnews/test/fakeserver/Pop3d.jsm
new file mode 100644
index 0000000000..33a2b06a90
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Pop3d.jsm
@@ -0,0 +1,454 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Contributors:
+ * Ben Bucksch <ben.bucksch beonex.com> <http://business.beonex.com> (RFC 5034 Authentication)
+ */
+/* This file implements test POP3 servers
+ */
+
+var EXPORTED_SYMBOLS = [
+ "Pop3Daemon",
+ "POP3_RFC1939_handler",
+ "POP3_RFC2449_handler",
+ "POP3_RFC5034_handler",
+];
+
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+// Since we don't really need to worry about peristence, we can just
+// use a UIDL counter.
+var gUIDLCount = 1;
+
+/**
+ * Read the contents of a file to the string.
+ *
+ * @param fileName A path relative to the current working directory, or
+ * a filename underneath the "data" directory relative to
+ * the cwd.
+ */
+function readFile(fileName) {
+ let cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+ // Try to find the file relative to either the data directory or to the
+ // current working directory.
+ let file = cwd.clone();
+ if (fileName.includes("/")) {
+ let parts = fileName.split("/");
+ for (let part of parts) {
+ if (part == "..") {
+ file = file.parent;
+ } else {
+ file.append(part);
+ }
+ }
+ } else {
+ file.append("data");
+ file.append(fileName);
+ }
+
+ if (!file.exists()) {
+ throw new Error("Cannot find file named " + fileName);
+ }
+
+ return mailTestUtils.loadFileToString(file);
+}
+
+class Pop3Daemon {
+ messages = [];
+ _messages = [];
+ _totalMessageSize = 0;
+
+ /**
+ * Set the messages that the POP3 daemon will provide to its clients.
+ *
+ * @param messages An array of either 1) strings that are filenames whose
+ * contents will be loaded from the files or 2) objects with a "fileData"
+ * attribute whose value is the content of the file.
+ */
+ setMessages(messages) {
+ this._messages = [];
+ this._totalMessageSize = 0;
+
+ function addMessage(element) {
+ // if it's a string, then it's a file-name.
+ if (typeof element == "string") {
+ this._messages.push({ fileData: readFile(element), size: -1 });
+ } else {
+ // Otherwise it's an object as dictionary already.
+ this._messages.push(element);
+ }
+ }
+ messages.forEach(addMessage, this);
+
+ for (var i = 0; i < this._messages.length; ++i) {
+ this._messages[i].size = this._messages[i].fileData.length;
+ this._messages[i].uidl = "UIDL" + gUIDLCount++;
+ this._totalMessageSize += this._messages[i].size;
+ }
+ }
+ getTotalMessages() {
+ return this._messages.length;
+ }
+ getTotalMessageSize() {
+ return this._totalMessageSize;
+ }
+}
+
+// POP3 TEST SERVERS
+// -----------------
+
+var kStateAuthNeeded = 1; // Not authenticated yet, need username and password
+var kStateAuthPASS = 2; // got command USER, expecting command PASS
+var kStateTransaction = 3; // Authenticated, can fetch and delete mail
+
+/**
+ * This handler implements the bare minimum required by RFC 1939.
+ * If dropOnAuthFailure is set, the server will drop the connection
+ * on authentication errors, to simulate servers that do the same.
+ */
+class POP3_RFC1939_handler {
+ kUsername = "fred";
+ kPassword = "wilma";
+
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.dropOnAuthFailure = false;
+ this._multiline = false;
+ this.resetTest();
+ }
+
+ resetTest() {
+ this._state = kStateAuthNeeded;
+ }
+
+ USER(args) {
+ if (this._state != kStateAuthNeeded) {
+ return "-ERR invalid state";
+ }
+
+ if (args == this.kUsername) {
+ this._state = kStateAuthPASS;
+ return "+OK user recognized";
+ }
+
+ return "-ERR sorry, no such mailbox";
+ }
+ PASS(args) {
+ if (this._state != kStateAuthPASS) {
+ return "-ERR invalid state";
+ }
+
+ if (args == this.kPassword) {
+ this._state = kStateTransaction;
+ return "+OK maildrop locked and ready";
+ }
+
+ this._state = kStateAuthNeeded;
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR invalid password";
+ }
+ STAT(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ return (
+ "+OK " +
+ this._daemon.getTotalMessages() +
+ " " +
+ this._daemon.getTotalMessageSize()
+ );
+ }
+ LIST(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ var result = "+OK " + this._daemon._messages.length + " messages\r\n";
+ for (var i = 0; i < this._daemon._messages.length; ++i) {
+ result += i + 1 + " " + this._daemon._messages[i].size + "\r\n";
+ }
+
+ result += ".";
+ return result;
+ }
+ UIDL(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ let result = "+OK\r\n";
+ for (let i = 0; i < this._daemon._messages.length; ++i) {
+ result += i + 1 + " " + this._daemon._messages[i].uidl + "\r\n";
+ }
+
+ result += ".";
+ return result;
+ }
+ TOP(args) {
+ let [messageNumber, numberOfBodyLines] = args.split(" ");
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ let result = "+OK\r\n";
+ let msg = this._daemon._messages[messageNumber - 1].fileData;
+ let index = msg.indexOf("\r\n\r\n");
+ result += msg.slice(0, index);
+ if (numberOfBodyLines) {
+ result += "\r\n\r\n";
+ let bodyLines = msg.slice(index + 4).split("\r\n");
+ result += bodyLines.slice(0, numberOfBodyLines).join("\r\n");
+ }
+ result += "\r\n.";
+ return result;
+ }
+ RETR(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ var result = "+OK " + this._daemon._messages[args - 1].size + "\r\n";
+ result += this._daemon._messages[args - 1].fileData;
+ result += ".";
+ return result;
+ }
+ DELE(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ return "+OK";
+ }
+ NOOP(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ return "+OK";
+ }
+ RSET(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ this._state = kStateAuthNeeded;
+ return "+OK";
+ }
+ QUIT(args) {
+ // Let the client close the socket
+ // this.closing = true;
+ return "+OK fakeserver signing off";
+ }
+ onStartup() {
+ this.closing = false;
+ this._state = kStateAuthNeeded;
+ return "+OK Fake POP3 server ready";
+ }
+ onError(command, args) {
+ return "-ERR command " + command + " not implemented";
+ }
+ onServerFault(e) {
+ return "-ERR internal server error: " + e;
+ }
+ postCommand(reader) {
+ reader.setMultiline(this._multiline);
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ }
+}
+
+/**
+ * This implements CAPA
+ *
+ * @see RFC 2449
+ */
+class POP3_RFC2449_handler extends POP3_RFC1939_handler {
+ kCapabilities = ["UIDL", "TOP"]; // the test may adapt this as necessary
+
+ CAPA(args) {
+ var capa = "+OK List of our wanna-be capabilities follows:\r\n";
+ for (var i = 0; i < this.kCapabilities.length; i++) {
+ capa += this.kCapabilities[i] + "\r\n";
+ }
+ if (this.capaAdditions) {
+ capa += this.capaAdditions();
+ }
+ capa += "IMPLEMENTATION fakeserver\r\n.";
+ return capa;
+ }
+}
+
+/**
+ * This implements the AUTH command, i.e. authentication using CRAM-MD5 etc.
+ *
+ * @see RFC 5034
+ * @author Ben Bucksch <ben.bucksch beonex.com> <http://business.beonex.com>
+ */
+class POP3_RFC5034_handler extends POP3_RFC2449_handler {
+ kAuthSchemes = ["CRAM-MD5", "PLAIN", "LOGIN"]; // the test may adapt this as necessary
+ _usedCRAMMD5Challenge = null; // not base64-encoded
+
+ constructor(daemon) {
+ super(daemon);
+
+ this._kAuthSchemeStartFunction = {
+ "CRAM-MD5": this.authCRAMStart,
+ PLAIN: this.authPLAINStart,
+ LOGIN: this.authLOGINStart,
+ };
+ }
+
+ // called by this.CAPA()
+ capaAdditions() {
+ var capa = "";
+ if (this.kAuthSchemes.length > 0) {
+ capa += "SASL";
+ for (var i = 0; i < this.kAuthSchemes.length; i++) {
+ capa += " " + this.kAuthSchemes[i];
+ }
+ capa += "\r\n";
+ }
+ return capa;
+ }
+ AUTH(lineRest) {
+ // |lineRest| is a string containing the rest of line after "AUTH "
+ if (this._state != kStateAuthNeeded) {
+ return "-ERR invalid state";
+ }
+
+ // AUTH without arguments returns a list of supported schemes
+ if (!lineRest) {
+ var capa = "+OK I like:\r\n";
+ for (var i = 0; i < this.kAuthSchemes.length; i++) {
+ capa += this.kAuthSchemes[i] + "\r\n";
+ }
+ capa += ".\r\n";
+ return capa;
+ }
+
+ var args = lineRest.split(" ");
+ var scheme = args[0].toUpperCase();
+ // |scheme| contained in |kAuthSchemes|?
+ if (
+ !this.kAuthSchemes.some(function (s) {
+ return s == scheme;
+ })
+ ) {
+ return "-ERR AUTH " + scheme + " not supported";
+ }
+
+ var func = this._kAuthSchemeStartFunction[scheme];
+ if (!func || typeof func != "function") {
+ return (
+ "-ERR I just pretended to implement AUTH " + scheme + ", but I don't"
+ );
+ }
+ return func.call(this, "1" in args ? args[1] : undefined);
+ }
+
+ onMultiline(line) {
+ if (this._nextAuthFunction) {
+ var func = this._nextAuthFunction;
+ this._multiline = false;
+ this._nextAuthFunction = undefined;
+ if (line == "*") {
+ return "-ERR Okay, as you wish. Chicken";
+ }
+ if (!func || typeof func != "function") {
+ return "-ERR I'm lost. Internal server error during auth";
+ }
+ try {
+ return func.call(this, line);
+ } catch (e) {
+ return "-ERR " + e;
+ }
+ }
+
+ if (super.onMultiline) {
+ // Call parent.
+ return super.onMultiline.call(this, line);
+ }
+ return undefined;
+ }
+
+ authPLAINStart(lineRest) {
+ this._nextAuthFunction = this.authPLAINCred;
+ this._multiline = true;
+
+ return "+";
+ }
+ authPLAINCred(line) {
+ var req = AuthPLAIN.decodeLine(line);
+ if (req.username == this.kUsername && req.password == this.kPassword) {
+ this._state = kStateTransaction;
+ return "+OK Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+
+ authCRAMStart(lineRest) {
+ this._nextAuthFunction = this.authCRAMDigest;
+ this._multiline = true;
+
+ this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost");
+ return "+ " + this._usedCRAMMD5Challenge;
+ }
+ authCRAMDigest(line) {
+ var req = AuthCRAM.decodeLine(line);
+ var expectedDigest = AuthCRAM.encodeCRAMMD5(
+ this._usedCRAMMD5Challenge,
+ this.kPassword
+ );
+ if (req.username == this.kUsername && req.digest == expectedDigest) {
+ this._state = kStateTransaction;
+ return "+OK Hello friend!";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+
+ authLOGINStart(lineRest) {
+ this._nextAuthFunction = this.authLOGINUsername;
+ this._multiline = true;
+
+ return "+ " + btoa("Username:");
+ }
+ authLOGINUsername(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kUsername) {
+ this._nextAuthFunction = this.authLOGINPassword;
+ } else {
+ // Don't return error yet, to not reveal valid usernames.
+ this._nextAuthFunction = this.authLOGINBadUsername;
+ }
+ this._multiline = true;
+ return "+ " + btoa("Password:");
+ }
+ authLOGINBadUsername(line) {
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+ authLOGINPassword(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kPassword) {
+ this._state = kStateTransaction;
+ return "+OK Hello friend! Where did you pull out this old auth scheme?";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+}
diff --git a/comm/mailnews/test/fakeserver/Smtpd.jsm b/comm/mailnews/test/fakeserver/Smtpd.jsm
new file mode 100644
index 0000000000..646b626b86
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Smtpd.jsm
@@ -0,0 +1,274 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+// This file implements test SMTP servers
+
+var EXPORTED_SYMBOLS = ["SmtpDaemon", "SMTP_RFC2821_handler"];
+
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+class SmtpDaemon {
+ _messages = {};
+}
+
+// SMTP TEST SERVERS
+// -----------------
+
+var kStateAuthNeeded = 0;
+var kStateAuthOptional = 2;
+var kStateAuthenticated = 3;
+
+/**
+ * This handler implements the bare minimum required by RFC 2821.
+ *
+ * @see RFC 2821
+ * If dropOnAuthFailure is set, the server will drop the connection
+ * on authentication errors, to simulate servers that do the same.
+ */
+class SMTP_RFC2821_handler {
+ kAuthRequired = false;
+ kUsername = "testsmtp";
+ kPassword = "smtptest";
+ kAuthSchemes = ["CRAM-MD5", "PLAIN", "LOGIN"];
+ kCapabilities = ["8BITMIME", "SIZE", "CLIENTID"];
+ _nextAuthFunction = undefined;
+
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.dropOnAuthFailure = false;
+
+ this._kAuthSchemeStartFunction = {
+ "CRAM-MD5": this.authCRAMStart,
+ PLAIN: this.authPLAINStart,
+ LOGIN: this.authLOGINStart,
+ };
+
+ this.resetTest();
+ }
+
+ resetTest() {
+ this._state = this.kAuthRequired ? kStateAuthNeeded : kStateAuthOptional;
+ this._nextAuthFunction = undefined;
+ this._multiline = false;
+ this.expectingData = false;
+ this._daemon.post = "";
+ }
+ EHLO(args) {
+ var capa = "250-fakeserver greets you";
+ if (this.kCapabilities.length > 0) {
+ capa += "\n250-" + this.kCapabilities.join("\n250-");
+ }
+ if (this.kAuthSchemes.length > 0) {
+ capa += "\n250-AUTH " + this.kAuthSchemes.join(" ");
+ }
+ capa += "\n250 HELP"; // the odd one: no "-", per RFC 2821
+ return capa;
+ }
+ CLIENTID(args) {
+ return "250 ok";
+ }
+ AUTH(lineRest) {
+ if (this._state == kStateAuthenticated) {
+ return "503 You're already authenticated";
+ }
+ var args = lineRest.split(" ");
+ var scheme = args[0].toUpperCase();
+ // |scheme| contained in |kAuthSchemes|?
+ if (
+ !this.kAuthSchemes.some(function (s) {
+ return s == scheme;
+ })
+ ) {
+ return "504 AUTH " + scheme + " not supported";
+ }
+ var func = this._kAuthSchemeStartFunction[scheme];
+ if (!func || typeof func != "function") {
+ return (
+ "504 I just pretended to implement AUTH " + scheme + ", but I don't"
+ );
+ }
+ dump("Starting AUTH " + scheme + "\n");
+ return func.call(this, args.length > 1 ? args[1] : undefined);
+ }
+ MAIL(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok";
+ }
+ RCPT(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok";
+ }
+ DATA(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ this.expectingData = true;
+ this._daemon.post = "";
+ return "354 ok\n";
+ }
+ RSET(args) {
+ return "250 ok\n";
+ }
+ VRFY(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok\n";
+ }
+ EXPN(args) {
+ return "250 ok\n";
+ }
+ HELP(args) {
+ return "211 ok\n";
+ }
+ NOOP(args) {
+ return "250 ok\n";
+ }
+ QUIT(args) {
+ this.closing = true;
+ return "221 done";
+ }
+ onStartup() {
+ this.closing = false;
+ return "220 ok";
+ }
+
+ /**
+ * AUTH implementations
+ *
+ * @see RFC 4954
+ */
+ authPLAINStart(lineRest) {
+ if (lineRest) {
+ // all in one command, called initial client response, see RFC 4954
+ return this.authPLAINCred(lineRest);
+ }
+
+ this._nextAuthFunction = this.authPLAINCred;
+ this._multiline = true;
+
+ return "334 ";
+ }
+ authPLAINCred(line) {
+ var req = AuthPLAIN.decodeLine(line);
+ if (req.username == this.kUsername && req.password == this.kPassword) {
+ this._state = kStateAuthenticated;
+ return "235 2.7.0 Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ authCRAMStart(lineRest) {
+ this._nextAuthFunction = this.authCRAMDigest;
+ this._multiline = true;
+
+ this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost");
+ return "334 " + this._usedCRAMMD5Challenge;
+ }
+ authCRAMDigest(line) {
+ var req = AuthCRAM.decodeLine(line);
+ var expectedDigest = AuthCRAM.encodeCRAMMD5(
+ this._usedCRAMMD5Challenge,
+ this.kPassword
+ );
+ if (req.username == this.kUsername && req.digest == expectedDigest) {
+ this._state = kStateAuthenticated;
+ return "235 2.7.0 Hello friend!";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ authLOGINStart(lineRest) {
+ this._nextAuthFunction = this.authLOGINUsername;
+ this._multiline = true;
+
+ return "334 " + btoa("Username:");
+ }
+ authLOGINUsername(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kUsername) {
+ this._nextAuthFunction = this.authLOGINPassword;
+ } else {
+ // Don't return error yet, to not reveal valid usernames.
+ this._nextAuthFunction = this.authLOGINBadUsername;
+ }
+ this._multiline = true;
+ return "334 " + btoa("Password:");
+ }
+ authLOGINBadUsername(line) {
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+ authLOGINPassword(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kPassword) {
+ this._state = kStateAuthenticated;
+ return "235 2.7.0 Hello friend! Where did you pull out this old auth scheme?";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ onError(command, args) {
+ return "500 Command " + command + " not recognized\n";
+ }
+ onServerFault(e) {
+ return "451 Internal server error: " + e;
+ }
+ onMultiline(line) {
+ if (this._nextAuthFunction) {
+ var func = this._nextAuthFunction;
+ this._multiline = false;
+ this._nextAuthFunction = undefined;
+ if (line == "*") {
+ // abort, per RFC 4954 and others
+ return "501 Okay, as you wish. Chicken";
+ }
+ if (!func || typeof func != "function") {
+ return "451 I'm lost. Internal server error during auth";
+ }
+ try {
+ return func.call(this, line);
+ } catch (e) {
+ return "451 " + e;
+ }
+ }
+ if (line == ".") {
+ if (this.expectingData) {
+ this.expectingData = false;
+ return "250 Wonderful article, your style is gorgeous!";
+ }
+ return "503 Huch? How did you get here?";
+ }
+
+ if (this.expectingData) {
+ if (line.startsWith(".")) {
+ line = line.substring(1);
+ }
+ // This uses CR LF to match with the specification
+ this._daemon.post += line + "\r\n";
+ }
+ return undefined;
+ }
+ postCommand(reader) {
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ reader.setMultiline(this._multiline || this.expectingData);
+ }
+}
diff --git a/comm/mailnews/test/resources/.eslintrc.js b/comm/mailnews/test/resources/.eslintrc.js
new file mode 100644
index 0000000000..9fad83a7b1
--- /dev/null
+++ b/comm/mailnews/test/resources/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/xpcshell-test", "plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/test/resources/IMAPpump.jsm b/comm/mailnews/test/resources/IMAPpump.jsm
new file mode 100644
index 0000000000..3f4b55266d
--- /dev/null
+++ b/comm/mailnews/test/resources/IMAPpump.jsm
@@ -0,0 +1,143 @@
+/* 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/. */
+
+/*
+ * This file provides a simple interface to the imap fake server. Demonstration
+ * of its use can be found in test_imapPump.js
+ *
+ * The code that forms the core of this file, in its original incarnation,
+ * was test_imapFolderCopy.js There have been several iterations since
+ * then.
+ */
+
+var EXPORTED_SYMBOLS = ["IMAPPump", "setupIMAPPump", "teardownIMAPPump"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+var { gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var Imapd = ChromeUtils.import("resource://testing-common/mailnews/Imapd.jsm");
+var { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+// define globals
+var IMAPPump = {
+ daemon: null, // the imap fake server daemon
+ server: null, // the imap fake server
+ incomingServer: null, // nsIMsgIncomingServer for the imap server
+ inbox: null, // nsIMsgFolder/nsIMsgImapMailFolder for imap inbox
+ mailbox: null, // imap fake server mailbox
+};
+
+function setupIMAPPump(extensions) {
+ // Create Application info if we need it.
+ updateAppInfo();
+
+ // These are copied from imap's head_server.js to here so we can run
+ // this from any directory.
+ function makeServer(daemon, infoString) {
+ if (infoString in Imapd.configurations) {
+ return makeServer(daemon, Imapd.configurations[infoString].join(","));
+ }
+
+ function createHandler(d) {
+ var handler = new Imapd.IMAP_RFC3501_handler(d);
+ if (!infoString) {
+ infoString = "RFC2195";
+ }
+
+ var parts = infoString.split(/ *, */);
+ for (var part of parts) {
+ Imapd.mixinExtension(handler, Imapd["IMAP_" + part + "_extension"]);
+ }
+ return handler;
+ }
+ var server = new nsMailServer(createHandler, daemon);
+ server.start();
+ return server;
+ }
+
+ function createLocalIMAPServer() {
+ let server = localAccountUtils.create_incoming_server(
+ "imap",
+ IMAPPump.server.port,
+ "user",
+ "password"
+ );
+ server.QueryInterface(Ci.nsIImapIncomingServer);
+ return server;
+ }
+
+ // end copy from head_server.js
+
+ IMAPPump.daemon = new Imapd.ImapDaemon();
+ IMAPPump.server = makeServer(IMAPPump.daemon, extensions);
+
+ IMAPPump.incomingServer = createLocalIMAPServer();
+
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+ localAccount.incomingServer = localAccountUtils.incomingServer;
+
+ // Let's also have another account, using the same identity
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = IMAPPump.incomingServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref("mail.server.default.max_cached_connections", 1);
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.default.download_on_biff", false);
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref("mail.biff.alert.show_preview", false);
+
+ IMAPPump.incomingServer.performExpand(null);
+
+ IMAPPump.inbox = IMAPPump.incomingServer.rootFolder.getChildNamed("INBOX");
+ IMAPPump.mailbox = IMAPPump.daemon.getMailbox("INBOX");
+ IMAPPump.inbox instanceof Ci.nsIMsgImapMailFolder;
+}
+
+// This will clear not only the imap accounts but also local accounts.
+function teardownIMAPPump() {
+ // try to finish any pending operations
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ IMAPPump.inbox = null;
+ try {
+ let serverSink = IMAPPump.incomingServer.QueryInterface(
+ Ci.nsIImapServerSink
+ );
+ serverSink.abortQueuedUrls();
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.server.resetTest();
+ IMAPPump.server.stop();
+ MailServices.accounts.removeIncomingServer(IMAPPump.incomingServer, false);
+ IMAPPump.incomingServer = null;
+ localAccountUtils.clearAll();
+ } catch (ex) {
+ dump(ex);
+ }
+}
diff --git a/comm/mailnews/test/resources/LocalAccountUtils.jsm b/comm/mailnews/test/resources/LocalAccountUtils.jsm
new file mode 100644
index 0000000000..11af0d1577
--- /dev/null
+++ b/comm/mailnews/test/resources/LocalAccountUtils.jsm
@@ -0,0 +1,195 @@
+/* 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 = ["localAccountUtils"];
+
+// MailServices
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// Local Mail Folders. Requires prior setup of profile directory
+
+var localAccountUtils = {
+ inboxFolder: undefined,
+ incomingServer: undefined,
+ rootFolder: undefined,
+ msgAccount: undefined,
+
+ _localAccountInitialized: false,
+ _mailboxStoreContractID: undefined,
+
+ pluggableStores: [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+ ],
+
+ clearAll() {
+ this._localAccountInitialized = false;
+ if (this.msgAccount) {
+ MailServices.accounts.removeAccount(this.msgAccount);
+ }
+ this.incomingServer = undefined;
+ this.msgAccount = undefined;
+ this.inboxFolder = undefined;
+ this.rootFolder = undefined;
+ },
+
+ loadLocalMailAccount(storeID) {
+ if (
+ (storeID && storeID == this._mailboxStoreContractID) ||
+ (!storeID && this._localAccountInitialized)
+ ) {
+ return;
+ }
+
+ this.clearAll();
+ if (storeID) {
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+ }
+
+ this._mailboxStoreContractID = storeID;
+ MailServices.accounts.createLocalMailAccount();
+
+ this.incomingServer = MailServices.accounts.localFoldersServer;
+ this.msgAccount = MailServices.accounts.FindAccountForServer(
+ this.incomingServer
+ );
+
+ this.rootFolder = this.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ this.inboxFolder = this.rootFolder
+ .createLocalSubfolder("Inbox")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ // a local inbox should have a Mail flag!
+ this.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+
+ // Force an initialization of the Inbox folder database.
+ this.inboxFolder.prettyName;
+
+ this._localAccountInitialized = true;
+ },
+
+ /**
+ * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
+ *
+ * @param {string} aType - The type of the server (pop3, imap etc).
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server.
+ * @param {string} aPassword - The password for the server.
+ * @param {string} aHostname - The hostname for the server (defaults to localhost).
+ * @returns {nsIMsgIncomingServer} The newly-created nsIMsgIncomingServer.
+ */
+ create_incoming_server(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname = "localhost"
+ ) {
+ let serverAndAccount = localAccountUtils.create_incoming_server_and_account(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname
+ );
+ return serverAndAccount.server;
+ },
+
+ /**
+ * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
+ * There are no identities created for the account.
+ *
+ * @param {string} aType - The type of the server (pop3, imap etc).
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server.
+ * @param {string} aPassword - The password for the server.
+ * @param {string} aHostname - The hostname for the server (defaults to localhost).
+ * @returns {object} object
+ * @returns {nsIMsgIncomingServer} object.server Created server
+ * @returns {nsIMsgAccount} object.account Created account
+ */
+ create_incoming_server_and_account(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname = "localhost"
+ ) {
+ let server = MailServices.accounts.createIncomingServer(
+ aUsername,
+ aHostname,
+ aType
+ );
+ server.port = aPort;
+ if (aUsername != null) {
+ server.username = aUsername;
+ }
+ if (aPassword != null) {
+ server.password = aPassword;
+ }
+
+ server.valid = false;
+
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+ if (aType == "pop3") {
+ // Several tests expect that mail is deferred to the local folders account,
+ // so do that.
+ this.loadLocalMailAccount();
+ server.QueryInterface(Ci.nsIPop3IncomingServer);
+ server.deferredToAccount = this.msgAccount.key;
+ }
+ server.valid = true;
+
+ return { server, account };
+ },
+
+ /**
+ * Create an outgoing nsISmtpServer with the given parameters.
+ *
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server
+ * @param {string} aPassword - The password for the server
+ * @param {string} [aHostname=localhost] - The hostname for the server.
+ * @returns {nsISmtpServer} The newly-created nsISmtpServer.
+ */
+ create_outgoing_server(aPort, aUsername, aPassword, aHostname = "localhost") {
+ let server = MailServices.smtp.createServer();
+ server.hostname = aHostname;
+ server.port = aPort;
+ server.authMethod = Ci.nsMsgAuthMethod.none;
+ return server;
+ },
+
+ /**
+ * Associate the given outgoing server with the given account.
+ * It does so by creating a new identity in the account using the given outgoing
+ * server.
+ *
+ * @param {nsIMsgAccount} aIncoming - The account to associate.
+ * @param {nsISmtpServer} aOutgoingServer - The outgoing server to associate.
+ * @param {bool} aSetAsDefault - Whether to set the outgoing server as the
+ * default for the account.
+ */
+ associate_servers(aIncoming, aOutgoingServer, aSetAsDefault = false) {
+ if (!(aIncoming instanceof Ci.nsIMsgAccount)) {
+ throw new Error("aIncoming isn't an account");
+ }
+
+ let identity = MailServices.accounts.createIdentity();
+ identity.smtpServerKey = aOutgoingServer.key;
+
+ aIncoming.addIdentity(identity);
+
+ if (aSetAsDefault) {
+ aIncoming.defaultIdentity = identity;
+ }
+ },
+};
diff --git a/comm/mailnews/test/resources/MailTestUtils.jsm b/comm/mailnews/test/resources/MailTestUtils.jsm
new file mode 100644
index 0000000000..588e21521a
--- /dev/null
+++ b/comm/mailnews/test/resources/MailTestUtils.jsm
@@ -0,0 +1,611 @@
+/* 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 = ["mailTestUtils"];
+
+var { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// See Bug 903946
+function avoidUncaughtExceptionInExternalProtocolService() {
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mime_types_file",
+ Services.prefs.getCharPref("helpers.global_mime_types_file")
+ );
+ } catch (ex) {}
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mailcap_file",
+ Services.prefs.getCharPref("helpers.global_mailcap_file")
+ );
+ } catch (ex) {}
+}
+avoidUncaughtExceptionInExternalProtocolService();
+
+var mailTestUtils = {
+ // Loads a file to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadFileToString(aFile, aCharset) {
+ var data = "";
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(aFile, -1, 0, 0);
+
+ if (aCharset) {
+ var cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(fstream, aCharset, 4096, 0x0000);
+ let str = {};
+ while (cstream.readString(4096, str) != 0) {
+ data += str.value;
+ }
+
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(fstream);
+
+ let str = sstream.read(4096);
+ while (str.length > 0) {
+ data += str;
+ str = sstream.read(4096);
+ }
+
+ sstream.close();
+ }
+
+ fstream.close();
+
+ return data;
+ },
+
+ // Loads a message to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadMessageToString(aFolder, aMsgHdr, aCharset) {
+ var data = "";
+ let reusable = {};
+ let bytesLeft = aMsgHdr.messageSize;
+ let stream = aFolder.getMsgInputStream(aMsgHdr, reusable);
+ if (aCharset) {
+ let cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(stream, aCharset, 4096, 0x0000);
+ let str = {};
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ while (cstream.readString(bytesToRead, str) != 0) {
+ data += str.value;
+ bytesLeft -= bytesToRead;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ }
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(stream);
+
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ var str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ while (str.length > 0) {
+ data += str;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ }
+ sstream.close();
+ }
+ stream.close();
+
+ return data;
+ },
+
+ // Loads a message to a UTF-16 string.
+ loadMessageToUTF16String(folder, msgHdr, charset) {
+ let str = this.loadMessageToString(folder, msgHdr, charset);
+ let arr = new Uint8Array(Array.from(str, x => x.charCodeAt(0)));
+ return new TextDecoder().decode(arr);
+ },
+
+ // Gets the first message header in a folder.
+ firstMsgHdr(folder) {
+ let enumerator = folder.msgDatabase.enumerateMessages();
+ let first = enumerator[Symbol.iterator]().next();
+ return first.done ? null : first.value;
+ },
+
+ // Gets message header number N (0 based index) in a folder.
+ getMsgHdrN(folder, n) {
+ let i = 0;
+ for (let next of folder.msgDatabase.enumerateMessages()) {
+ if (i == n) {
+ return next;
+ }
+ i++;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the file system a particular file is on.
+ * Currently supported on Windows only.
+ *
+ * @param {nsIFile} aFile - The file to get the file system for.
+ * @returns {string} The file system a particular file is on, or 'null'
+ * if not on Windows.
+ */
+ get_file_system(aFile) {
+ if (!("@mozilla.org/windows-registry-key;1" in Cc)) {
+ dump("get_file_system() is supported on Windows only.\n");
+ return null;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const MAX_PATH = 260;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ // Returns the path of the volume a file is on.
+ let GetVolumePathName = kernel32.declare(
+ "GetVolumePathNameW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in: lpszFileName
+ ctypes.char16_t.ptr, // out: lpszVolumePathName
+ ctypes.uint32_t // in: cchBufferLength
+ );
+
+ let filePath = aFile.path;
+ // The volume path should be at most 1 greater than than the length of the
+ // path -- add 1 for a trailing backslash if necessary, and 1 for the
+ // terminating null character. Note that the parentheses around the type are
+ // necessary for new to apply correctly.
+ let volumePath = new (ctypes.char16_t.array(filePath.length + 2))();
+
+ if (!GetVolumePathName(filePath, volumePath, volumePath.length)) {
+ throw new Error(
+ "Unable to get volume path for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ // Returns information about the file system for the given volume path. We just need
+ // the file system name.
+ let GetVolumeInformation = kernel32.declare(
+ "GetVolumeInformationW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in, optional: lpRootPathName
+ ctypes.char16_t.ptr, // out: lpVolumeNameBuffer
+ ctypes.uint32_t, // in: nVolumeNameSize
+ ctypes.uint32_t.ptr, // out, optional: lpVolumeSerialNumber
+ ctypes.uint32_t.ptr, // out, optional: lpMaximumComponentLength
+ ctypes.uint32_t.ptr, // out, optional: lpFileSystemFlags
+ ctypes.char16_t.ptr, // out: lpFileSystemNameBuffer
+ ctypes.uint32_t // in: nFileSystemNameSize
+ );
+
+ // We're only interested in the name of the file system.
+ let fsName = new (ctypes.char16_t.array(MAX_PATH + 1))();
+
+ if (
+ !GetVolumeInformation(
+ volumePath,
+ null,
+ 0,
+ null,
+ null,
+ null,
+ fsName,
+ fsName.length
+ )
+ ) {
+ throw new Error(
+ "Unable to get volume information for " +
+ volumePath.readString() +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ return fsName.readString();
+ } finally {
+ kernel32.close();
+ }
+ },
+
+ /**
+ * Try marking a region of a file as sparse, so that zeros don't consume
+ * significant amounts of disk space. This is a platform-dependent routine and
+ * is not supported on all platforms. The current status of this function is:
+ * - Windows: Supported, but only on NTFS volumes.
+ * - Mac: Not supported.
+ * - Linux: As long as you seek to a position before writing, happens automatically
+ * on most file systems, so this function is a no-op.
+ *
+ * @param {nsIFile} aFile - The file to mark as sparse.
+ * @param {integer} aRegionStart - The start position of the sparse region,
+ * in bytes.
+ * @param {integer} aRegionBytes - The number of bytes to mark as sparse.
+ * @returns {boolean} Whether the OS and file system supports marking files as
+ * sparse. If this is true, then the file has been marked as sparse.
+ * If this isfalse, then the underlying system doesn't support marking files as
+ * sparse. If an exception is thrown, then the system does support marking
+ * files as sparse, but an error occurred while doing so.
+ *
+ */
+ mark_file_region_sparse(aFile, aRegionStart, aRegionBytes) {
+ let fileSystem = this.get_file_system(aFile);
+ dump(
+ "[mark_file_region_sparse()] File system = " +
+ (fileSystem || "(unknown)") +
+ ", file region = at " +
+ this.toMiBString(aRegionStart) +
+ " for " +
+ this.toMiBString(aRegionBytes) +
+ "\n"
+ );
+
+ if ("@mozilla.org/windows-registry-key;1" in Cc) {
+ // On Windows, check whether the drive is NTFS. If it is, proceed.
+ // If it isn't, then bail out now, because in all probability it is
+ // FAT32, which doesn't support sparse files.
+ if (fileSystem != "NTFS") {
+ return false;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const HANDLE = ctypes.voidptr_t;
+ // A BOOLEAN (= BYTE = unsigned char) is distinct from a BOOL.
+ // http://blogs.msdn.com/b/oldnewthing/archive/2004/12/22/329884.aspx
+ const BOOLEAN = ctypes.unsigned_char;
+ const FILE_SET_SPARSE_BUFFER = new ctypes.StructType(
+ "FILE_SET_SPARSE_BUFFER",
+ [{ SetSparse: BOOLEAN }]
+ );
+ // LARGE_INTEGER is actually a type union. We'll use the int64 representation
+ const LARGE_INTEGER = ctypes.int64_t;
+ const FILE_ZERO_DATA_INFORMATION = new ctypes.StructType(
+ "FILE_ZERO_DATA_INFORMATION",
+ [{ FileOffset: LARGE_INTEGER }, { BeyondFinalZero: LARGE_INTEGER }]
+ );
+
+ const GENERIC_WRITE = 0x40000000;
+ const OPEN_ALWAYS = 4;
+ const FILE_ATTRIBUTE_NORMAL = 0x80;
+ const INVALID_HANDLE_VALUE = new ctypes.Int64(-1);
+ const FSCTL_SET_SPARSE = 0x900c4;
+ const FSCTL_SET_ZERO_DATA = 0x980c8;
+ const FILE_BEGIN = 0;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ let CreateFile = kernel32.declare(
+ "CreateFileW",
+ ctypes.winapi_abi,
+ HANDLE, // return type: handle to the file
+ ctypes.char16_t.ptr, // in: lpFileName
+ ctypes.uint32_t, // in: dwDesiredAccess
+ ctypes.uint32_t, // in: dwShareMode
+ ctypes.voidptr_t, // in, optional: lpSecurityAttributes (note that
+ // we're cheating here by not declaring a
+ // SECURITY_ATTRIBUTES structure -- that's because
+ // we're going to pass in null anyway)
+ ctypes.uint32_t, // in: dwCreationDisposition
+ ctypes.uint32_t, // in: dwFlagsAndAttributes
+ HANDLE // in, optional: hTemplateFile
+ );
+
+ let filePath = aFile.path;
+ let hFile = CreateFile(
+ filePath,
+ GENERIC_WRITE,
+ 0,
+ null,
+ OPEN_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ let hFileInt = ctypes.cast(hFile, ctypes.intptr_t);
+ if (ctypes.Int64.compare(hFileInt.value, INVALID_HANDLE_VALUE) == 0) {
+ throw new Error(
+ "CreateFile failed for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ try {
+ let DeviceIoControl = kernel32.declare(
+ "DeviceIoControl",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hDevice
+ ctypes.uint32_t, // in: dwIoControlCode
+ ctypes.voidptr_t, // in, optional: lpInBuffer
+ ctypes.uint32_t, // in: nInBufferSize
+ ctypes.voidptr_t, // out, optional: lpOutBuffer
+ ctypes.uint32_t, // in: nOutBufferSize
+ ctypes.uint32_t.ptr, // out, optional: lpBytesReturned
+ ctypes.voidptr_t // inout, optional: lpOverlapped (again, we're
+ // cheating here by not having this as an
+ // OVERLAPPED structure
+ );
+ // bytesReturned needs to be passed in, even though it's meaningless
+ let bytesReturned = new ctypes.uint32_t();
+ let sparseBuffer = new FILE_SET_SPARSE_BUFFER();
+ sparseBuffer.SetSparse = 1;
+
+ // Mark the file as sparse
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_SPARSE,
+ sparseBuffer.address(),
+ FILE_SET_SPARSE_BUFFER.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark file as sparse, error " + ctypes.winLastError
+ );
+ }
+
+ let zdInfo = new FILE_ZERO_DATA_INFORMATION();
+ zdInfo.FileOffset = aRegionStart;
+ let regionEnd = aRegionStart + aRegionBytes;
+ zdInfo.BeyondFinalZero = regionEnd;
+ // Mark the region as a sparse region
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_ZERO_DATA,
+ zdInfo.address(),
+ FILE_ZERO_DATA_INFORMATION.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark region as zero, error " + ctypes.winLastError
+ );
+ }
+
+ // Move to past the sparse region and mark it as the end of the file. The
+ // above DeviceIoControl call is useless unless followed by this.
+ let SetFilePointerEx = kernel32.declare(
+ "SetFilePointerEx",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hFile
+ LARGE_INTEGER, // in: liDistanceToMove
+ LARGE_INTEGER.ptr, // out, optional: lpNewFilePointer
+ ctypes.uint32_t // in: dwMoveMethod
+ );
+ if (!SetFilePointerEx(hFile, regionEnd, null, FILE_BEGIN)) {
+ throw new Error(
+ "Unable to set file pointer to end, error " + ctypes.winLastError
+ );
+ }
+
+ let SetEndOfFile = kernel32.declare(
+ "SetEndOfFile",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hFile
+ );
+ if (!SetEndOfFile(hFile)) {
+ throw new Error(
+ "Unable to set end of file, error " + ctypes.winLastError
+ );
+ }
+
+ return true;
+ } finally {
+ let CloseHandle = kernel32.declare(
+ "CloseHandle",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hObject
+ );
+ CloseHandle(hFile);
+ }
+ } finally {
+ kernel32.close();
+ }
+ } else if ("nsILocalFileMac" in Ci) {
+ // Macs don't support marking files as sparse.
+ return false;
+ } else {
+ // Assuming Unix here. Unix file systems generally automatically sparsify
+ // files.
+ return true;
+ }
+ },
+
+ /**
+ * Converts a size in bytes into its mebibytes string representation.
+ * NB: 1 MiB = 1024 * 1024 = 1048576 B.
+ *
+ * @param {integer} aSize - The size in bytes.
+ * @returns {string} A string representing the size in mebibytes.
+ */
+ toMiBString(aSize) {
+ return aSize / 1048576 + " MiB";
+ },
+
+ /**
+ * A variant of do_timeout that accepts an actual function instead of
+ * requiring you to pass a string to evaluate. If the function throws an
+ * exception when invoked, we will use do_throw to ensure that the test fails.
+ *
+ * @param {integer} aDelayInMS - The number of milliseconds to wait before firing the timer.
+ * @param {Function} aFunc - The function to invoke when the timer fires.
+ * @param {object} [aFuncThis] - Optional 'this' pointer to use.
+ * @param {*[]} aFuncArgs - Optional list of arguments to pass to the function.
+ */
+ _timer: null,
+ do_timeout_function(aDelayInMS, aFunc, aFuncThis, aFuncArgs) {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let wrappedFunc = function () {
+ try {
+ aFunc.apply(aFuncThis, aFuncArgs);
+ } catch (ex) {
+ // we want to make sure that if the thing we call throws an exception,
+ // that this terminates the test.
+ do_throw(ex);
+ }
+ };
+ this._timer.initWithCallback(
+ wrappedFunc,
+ aDelayInMS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Ensure the given nsIMsgFolder's database is up-to-date, calling the provided
+ * callback once the folder has been loaded. (This may be instantly or
+ * after a re-parse.)
+ *
+ * @param {nsIMsgFolder} aFolder - The nsIMsgFolder whose database you want
+ * to ensure is up-to-date.
+ * @param {Function} aCallback - The callback function to invoke once the
+ * folder has been loaded.
+ * @param {object} aCallbackThis - The 'this' to use when calling the callback.
+ * Pass null if your callback does not rely on 'this'.
+ * @param {*[]} aCallbackArgs - A list of arguments to pass to the callback
+ * via apply. If you provide [1,2,3], we will effectively call:
+ * aCallbackThis.aCallback(1,2,3);
+ * @param {boolean} [aSomeoneElseWillTriggerTheUpdate=false] If this is true,
+ * we do not trigger the updateFolder call and it is assumed someone else is
+ * taking care of that.
+ */
+ updateFolderAndNotify(
+ aFolder,
+ aCallback,
+ aCallbackThis,
+ aCallbackArgs,
+ aSomeoneElseWillTriggerTheUpdate = false
+ ) {
+ // register for the folder loaded notification ahead of time... even though
+ // we may not need it...
+ let folderListener = {
+ onFolderEvent(aEventFolder, aEvent) {
+ if (aEvent == "FolderLoaded" && aFolder.URI == aEventFolder.URI) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ aCallback.apply(aCallbackThis, aCallbackArgs);
+ }
+ },
+ };
+
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+
+ if (!aSomeoneElseWillTriggerTheUpdate) {
+ aFolder.updateFolder(null);
+ }
+ },
+
+ /**
+ * For when you want to compare elements non-strictly.
+ */
+ non_strict_index_of(aArray, aElem) {
+ for (let [i, elem] of aArray.entries()) {
+ if (elem == aElem) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Click on a particular cell in a tree. `window` is not defined here in this
+ * file, so we can't provide it as a default argument. Similarly, we pass in
+ * `EventUtils` as an argument because importing it here does not work
+ * because `window` is not defined.
+ *
+ * @param {object} EventUtils - The EventUtils object.
+ * @param {Window} win - The window the tree is in.
+ * @param {Element} tree - The tree element.
+ * @param {number} row - The tree row to click on.
+ * @param {number} column - The tree column to click on.
+ * @param {object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`.
+ */
+ treeClick(EventUtils, win, tree, row, column, event) {
+ let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell");
+ let treeChildren = tree.lastElementChild;
+ EventUtils.synthesizeMouse(
+ treeChildren,
+ coords.x + coords.width / 2,
+ coords.y + coords.height / 2,
+ event,
+ win
+ );
+ },
+
+ /**
+ * For waiting until an element exists in a given document. Pass in the
+ * `MutationObserver` as an argument because importing it here does not work
+ * because `window` is not defined here.
+ *
+ * @param {object} MutationObserver - The MutationObserver object.
+ * @param {Document} doc - Document that contains the elements.
+ * @param {string} observedNodeId - Id of the element to observe.
+ * @param {string} awaitedNodeId - Id of the element that will soon exist.
+ * @returns {Promise.<undefined>} - A promise fulfilled when the element exists.
+ */
+ awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) {
+ return new Promise(resolve => {
+ let outerObserver = new MutationObserver((mutationsList, observer) => {
+ for (let mutation of mutationsList) {
+ if (mutation.type == "childList" && mutation.addedNodes.length) {
+ let element = doc.getElementById(awaitedNodeId);
+
+ if (element) {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ }
+ });
+
+ let nodeToObserve = doc.getElementById(observedNodeId);
+ outerObserver.observe(nodeToObserve, { childList: true });
+ });
+ },
+};
diff --git a/comm/mailnews/test/resources/MessageGenerator.jsm b/comm/mailnews/test/resources/MessageGenerator.jsm
new file mode 100644
index 0000000000..4e3f8b75f1
--- /dev/null
+++ b/comm/mailnews/test/resources/MessageGenerator.jsm
@@ -0,0 +1,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);
diff --git a/comm/mailnews/test/resources/MessageInjection.jsm b/comm/mailnews/test/resources/MessageInjection.jsm
new file mode 100644
index 0000000000..9e1b7c79cb
--- /dev/null
+++ b/comm/mailnews/test/resources/MessageInjection.jsm
@@ -0,0 +1,987 @@
+/* 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 = ["MessageInjection"];
+
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+var { IMAPPump, setupIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+
+const SEARCH_TERM_MAP_HELPER = {
+ subject: Ci.nsMsgSearchAttrib.Subject,
+ body: Ci.nsMsgSearchAttrib.Body,
+ from: Ci.nsMsgSearchAttrib.Sender,
+ to: Ci.nsMsgSearchAttrib.To,
+ cc: Ci.nsMsgSearchAttrib.CC,
+ recipient: Ci.nsMsgSearchAttrib.ToOrCC,
+ involves: Ci.nsMsgSearchAttrib.AllAddresses,
+ age: Ci.nsMsgSearchAttrib.AgeInDays,
+ tags: Ci.nsMsgSearchAttrib.Keywords,
+ // If a test uses a custom search term, they must register that term
+ // with the id "mailnews@mozilla.org#test"
+ custom: Ci.nsMsgSearchAttrib.Custom,
+};
+
+/**
+ * Handling for Messages in Folders. Usage of either `local` or `imap`.
+ *
+ * Beware:
+ * Currently only one active instance of MessageInjection is supported due
+ * to a dependency on retrieving an account in the constructor.
+ */
+class MessageInjection {
+ /**
+ * MessageInjectionSetup
+ */
+ _mis = {
+ _nextUniqueFolderId: 0,
+
+ injectionConfig: {
+ mode: "none",
+ },
+ listeners: [],
+ notifyListeners(handlerName, args) {
+ for (let listener of this.listeners) {
+ if (handlerName in listener) {
+ listener[handlerName].apply(listener, args);
+ }
+ }
+ },
+
+ /**
+ * The nsIMsgIncomingServer
+ */
+ incomingServer: null,
+
+ /**
+ * The incoming server's (synthetic) root message folder.
+ */
+ rootFolder: null,
+
+ /**
+ * The nsIMsgFolder that is the inbox.
+ */
+ inboxFolder: null,
+
+ /**
+ * Fakeserver daemon, if applicable.
+ */
+ daemon: null,
+ /**
+ * Fakeserver server instance, if applicable.
+ */
+ server: null,
+ };
+ /**
+ * Creates an environment for tests.
+ * Usage either with "local" or "imap".
+ * An Inbox folder is created. Retrieve it via `getInboxFolder`
+ *
+ * IMAP:
+ * Starts an IMAP Server for you
+ *
+ * @param {object} injectionConfig
+ * @param {"local"|"imap"} injectionConfig.mode mode One of "local", "imap".
+ * @param {boolean} [injectionConfig.offline] Should the folder be marked offline (and
+ * fully downloaded)? Only relevant for IMAP.
+ * @param {MessageGenerator} [msgGen] The MessageGenerator which generates the new
+ * SyntheticMessages. We do not create our own because we would lose track of
+ * messages created from another MessageGenerator.
+ * It's optional as it is used only for a subset of methods.
+ */
+ constructor(injectionConfig, msgGen) {
+ // Set the injection Mode.
+ this._mis.injectionConfig = injectionConfig;
+
+ // Disable new mail notifications.
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ // Set msgGen if given.
+ if (msgGen) {
+ this.msgGen = msgGen;
+ }
+
+ // we need to pull in the notification service so we get events?
+ MailServices.mfn;
+
+ if (this._mis.injectionConfig.mode == "local") {
+ // This does createIncomingServer() and createAccount(), sets the server as
+ // the account's server, then sets the server.
+ try {
+ MailServices.accounts.createLocalMailAccount();
+ } catch (ex) {
+ // This will fail if someone already called this. Like in the mozmill
+ // case.
+ }
+
+ let localAccount = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+
+ // We need an identity or we get angry warnings.
+ let identity = MailServices.accounts.createIdentity();
+ // We need an email to protect against random code assuming it exists and
+ // throwing exceptions.
+ identity.email = "sender@nul.invalid";
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+
+ this._mis.incomingServer = MailServices.accounts.localFoldersServer;
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;
+ this._mis.rootFolder.createSubfolder("Inbox", null);
+ this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
+ // a local inbox should have a Mail flag!
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Inbox);
+ this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);
+
+ // Force an initialization of the Inbox folder database.
+ this._mis.inboxFolder.prettyName;
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // Disable autosync in favor of our explicitly forcing downloads of all
+ // messages in a folder. This is being done speculatively because when we
+ // didn't do this we got tripped up by the semaphore being in use and
+ // concern over inability to hang a listener off of the completion of the
+ // download. (Although I'm sure there are various ways we could do it.)
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ // Set the offline property based on the configured setting. This will
+ // affect newly created folders.
+ Services.prefs.setBoolPref(
+ "mail.server.default.offline_download",
+ this._mis.injectionConfig.offline
+ );
+
+ // set up IMAP fakeserver and incoming server
+ setupIMAPPump("");
+ this._mis.daemon = IMAPPump.daemon;
+ this._mis.server = IMAPPump.server;
+ this._mis.incomingServer = IMAPPump.incomingServer;
+ // this.#mis.server._debug = 3;
+
+ // do not log transactions; it's just a memory leak to us
+ this._mis.server._logTransactions = false;
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.defaultAccount;
+ // We need an email to protect against random code assuming it exists and
+ // throwing exceptions.
+ let identity = localAccount.defaultIdentity;
+ identity.email = "sender@nul.invalid";
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref(
+ "mail.server.server1.max_cached_connections",
+ 1
+ );
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);
+
+ this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;
+
+ this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
+ // make sure the inbox's offline state is correct. (may be excessive now
+ // that we set the pref above?)
+ if (this._mis.injectionConfig.offline) {
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ this._mis.inboxFolder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);
+
+ this._mis.handleUriToRealFolder = {};
+ this._mis.handleUriToFakeFolder = {};
+ this._mis.realUriToFakeFolder = {};
+ this._mis.realUriToFakeFolder[this._mis.inboxFolder.URI] =
+ this._mis.daemon.getMailbox("INBOX");
+ } else {
+ throw new Error(
+ "Illegal injection config option: " + this._mis.injectionConfig.mode
+ );
+ }
+
+ this._mis.junkHandle = null;
+ this._mis.junkFolder = null;
+
+ this._mis.trashHandle = null;
+ this._mis.trashFolder = null;
+ }
+
+ /**
+ * @returns {nsIMsgFolder}
+ */
+ getInboxFolder() {
+ return this._mis.inboxFolder;
+ }
+ /**
+ * @returns {boolean}
+ */
+ messageInjectionIsLocal() {
+ return this._mis.injectionConfig.mode == "local";
+ }
+ /**
+ * Call this method to finish the use of MessageInjection.
+ * Stops the IMAP server (if used) and stops internal functions.
+ */
+ teardownMessageInjection() {
+ if (this._mis.injectionConfig.mode == "imap") {
+ this._mis.incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish.
+ // (This spins its own event loop...)
+ this._mis.server.stop();
+ }
+
+ // Clean out this.#mis; we don't just null the global because it's conceivable we
+ // might still have some closures floating about.
+ for (let key in this._mis) {
+ delete this._mis[key];
+ }
+ }
+ /**
+ * Register a listener to be notified when interesting things happen involving
+ * calls made to the message injection API.
+ *
+ * @param {object} listener
+ * @param {Function} listener.onVirtualFolderCreated Called when a virtual
+ * folder is created using |makeVirtualFolder|. Takes a nsIMsgFolder
+ * that defines the virtual folder as argument.
+ */
+ registerMessageInjectionListener(listener) {
+ this._mis.listeners.push(listener);
+ }
+ /**
+ * Create and return an empty folder. If you want to delete this folder
+ * you must call |deleteFolder| to kill it! If you want to rename it, you
+ * must implement a method called renameFolder and then call it.
+ *
+ * @param {string} [folderName] A folder name with no support for hierarchy at this
+ * time. A name of the form "gabba#" will be autogenerated if you do not
+ * provide one.
+ * @param {nsMsgFolderFlags[]} [specialFlags] A list of nsMsgFolderFlags bits to set.
+ * @returns {nsIMsgFolder|string} In local mode a nsIMsgFolder is returned.
+ * In imap mode a folder URI is returned.
+ */
+ async makeEmptyFolder(folderName, specialFlags) {
+ if (folderName == null) {
+ folderName = "gabba" + this._mis._nextUniqueFolderId++;
+ }
+ let testFolder;
+
+ if (this._mis.injectionConfig.mode == "local") {
+ let localRoot = this._mis.rootFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ testFolder = localRoot.createLocalSubfolder(folderName);
+ // it seems dumb that we have to set this.
+ testFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+ if (specialFlags) {
+ for (let flag of specialFlags) {
+ testFolder.setFlag(flag);
+ }
+ }
+ this._mis.notifyListeners("onRealFolderCreated", [testFolder]);
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // Circumvent this scoping.
+ let mis = this._mis;
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl: (url, exitCode) => {
+ // get the newly created nsIMsgFolder folder
+ let msgFolder = mis.rootFolder.getChildNamed(folderName);
+
+ // XXX there is a bug that causes folders to be reported as ImapPublic
+ // when there is no namespace support by the IMAP server. This is
+ // a temporary workaround.
+ msgFolder.clearFlag(Ci.nsMsgFolderFlags.ImapPublic);
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.ImapPersonal);
+
+ if (specialFlags) {
+ for (let flag of specialFlags) {
+ msgFolder.setFlag(flag);
+ }
+ }
+
+ // get a reference to the fake server folder
+ let fakeFolder = this._mis.daemon.getMailbox(folderName);
+ // establish the mapping
+ mis.handleUriToRealFolder[testFolder] = msgFolder;
+ mis.handleUriToFakeFolder[testFolder] = fakeFolder;
+ mis.realUriToFakeFolder[msgFolder.URI] = fakeFolder;
+
+ // notify listeners
+ mis.notifyListeners("onRealFolderCreated", [msgFolder]);
+ },
+ });
+
+ testFolder = this._mis.rootFolder.URI + "/" + folderName;
+
+ // Tell the IMAP service to create the folder, adding a listener that
+ // hooks up the 'handle' URI -> actual folder mapping.
+ MailServices.imap.createFolder(
+ this._mis.rootFolder,
+ folderName,
+ promiseUrlListener
+ );
+ await promiseUrlListener.promise;
+ }
+
+ return testFolder;
+ }
+ /**
+ * Small helper for moving folder.
+ *
+ * @param {nsIMsgFolder} source
+ * @param {nsIMsgFolder} target
+ */
+ static async moveFolder(source, target) {
+ // we're doing a true move
+ await new Promise((resolve, reject) => {
+ MailServices.copy.copyFolder(
+ MessageInjection.get_nsIMsgFolder(source),
+ MessageInjection.get_nsIMsgFolder(target),
+ true,
+ {
+ /* nsIMsgCopyServiceListener implementation */
+ OnStartCopy() {},
+ OnProgress(progress, progressMax) {},
+ SetMessageKey(key) {},
+ SetMessageId(messageId) {},
+ OnStopCopy(status) {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ },
+ null
+ );
+ });
+ }
+ /**
+ *
+ * Get/create the junk folder handle. Use getRealInjectionFolder if you
+ * need the underlying nsIFolder.
+ *
+ * @returns {nsIMsgFolder}
+ */
+ async getJunkFolder() {
+ if (!this._mis.junkHandle) {
+ this._mis.junkHandle = await this.makeEmptyFolder("Junk", [
+ Ci.nsMsgFolderFlags.Junk,
+ ]);
+ }
+
+ return this._mis.junkHandle;
+ }
+ /**
+ * Get/create the trash folder handle. Use getRealInjectionFolder if you
+ * need the underlying nsIMsgFolder.
+ *
+ * @returns {nsIMsgFolder|string}
+ */
+ async getTrashFolder() {
+ if (!this._mis.trashHandle) {
+ // the folder may have been created and already known...
+ this._mis.trashFolder = this._mis.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Trash
+ );
+ if (this._mis.trashFolder) {
+ this._mis.trashHandle = this._mis.rootFolder.URI + "/Trash";
+ let fakeFolder = this._mis.daemon.getMailbox("Trash");
+ this._mis.handleUriToRealFolder[this._mis.trashHandle] =
+ this._mis.trashFolder;
+ this._mis.handleUriToFakeFolder[this._mis.trashHandle] = fakeFolder;
+ this._mis.realUriToFakeFolder[this._mis.trashFolder.URI] = fakeFolder;
+ } else {
+ this._mis.trashHandle = await this.makeEmptyFolder("Trash", [
+ Ci.nsMsgFolderFlags.Trash,
+ ]);
+ }
+ }
+
+ return this._mis.trashHandle;
+ }
+ /**
+ * Create and return a virtual folder.
+ *
+ * @param {nsIMsgFolder[]} folders The real folders this virtual folder should draw from.
+ * @param {SEARCH_TERM_MAP_HELPER} searchDef The search definition to use
+ * to build the list of search terms that populate this virtual folder.
+ * Keys should be stuff from SEARCH_TERM_MAP_HELPER and values should be
+ * strings to search for within those attribute things.
+ * @param {boolean} [booleanAnd] Should the search terms be and-ed together.
+ * Defaults to false.
+ * @param {string} [folderName] Name to use.
+ * @returns {nsIMsgFolder|string} In local usage returns a nsIMsgFolder
+ * in imap usage returns a Folder URI.
+ */
+ makeVirtualFolder(folders, searchDef, booleanAnd, folderName) {
+ let name = folderName
+ ? folderName
+ : "virt" + this._mis._nextUniqueFolderId++;
+
+ let terms = [];
+ let termCreator = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ for (let key in searchDef) {
+ let val = searchDef[key];
+ let term = termCreator.createTerm();
+ let value = term.value;
+ value.str = val;
+ term.value = value;
+ term.attrib = SEARCH_TERM_MAP_HELPER[key];
+ if (term.attrib == Ci.nsMsgSearchAttrib.Custom) {
+ term.customId = "mailnews@mozilla.org#test";
+ }
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = Boolean(booleanAnd);
+ terms.push(term);
+ }
+ // create an ALL case if we didn't add any terms
+ if (terms.length == 0) {
+ let term = termCreator.createTerm();
+ term.matchAll = true;
+ terms.push(term);
+ }
+
+ let wrapped = VirtualFolderHelper.createNewVirtualFolder(
+ name,
+ this._mis.rootFolder,
+ folders,
+ terms,
+ /* online */ false
+ );
+ this._mis.notifyListeners("onVirtualFolderCreated", [
+ wrapped.virtualFolder,
+ ]);
+ return wrapped.virtualFolder;
+ }
+ /**
+ * Mark the folder as offline and force all of its messages to be downloaded.
+ * This is an asynchronous operation that will call resolve once the
+ * download is completed.
+ *
+ * @param {string} folderHandle Folder URI.
+ */
+ async makeFolderAndContentsOffline(folderHandle) {
+ if (this._mis.injectionConfig.mode != "imap") {
+ return;
+ }
+
+ let msgFolder = this.getRealInjectionFolder(folderHandle);
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+
+ /**
+ * Create multiple new local folders, populating them with messages according to
+ * the set definitions provided. Differs from makeFolderWithSets by taking
+ * the number of folders to create and return the list of created folders as
+ * the first element in the returned list. This method is simple enough that
+ * the limited code duplication is deemed acceptable in support of readability.
+ *
+ * @param {number} folderCount
+ * @param {MakeMessageOptions[]} synSetDefs A synthetic set
+ * definition, as appropriate to pass to makeNewSetsInFolders.
+ * @returns {Promise<object[]>} A Promise with a list whose first element are
+ * the nsIMsgFolders created and whose subsequent items are the
+ * SyntheticMessageSets used to populate the folder (as returned by
+ * makeNewSetsInFolders). So nsIMsgFolder[], ...SyntheticMessageSet.
+ *
+ * Please note that the folders are either nsIMsgFolder, or folder
+ * URIs, depending on whether we're in local injection mode, or on IMAP. This
+ * should be transparent to you, unless you start trying to inject messages
+ * into a folder that hasn't been created by makeFoldersWithSets. See
+ * test_folder_deletion_nested in base_index_messages.js for an example of
+ * such pain.
+ */
+ async makeFoldersWithSets(folderCount, synSetDefs) {
+ let msgFolders = [];
+ for (let i = 0; i < folderCount; i++) {
+ msgFolders.push(await this.makeEmptyFolder());
+ }
+ let results = await this.makeNewSetsInFolders(msgFolders, synSetDefs);
+ // results may be referenced by addSetsToFolders in an async fashion, so
+ // don't change it.
+ results = results.concat();
+ results.unshift(msgFolders);
+ return results;
+ }
+ /**
+ * Given one or more existing folders, create new message sets and
+ * add them to the folders using.
+ *
+ * @param {nsIMsgFolder[]} msgFolders A list of nsIMsgFolder.
+ * The synthetic messages will be added to the folder(s).
+ * @param {MakeMessageOptions[]} synSetDefs A list of set definition objects as
+ * defined by MessageGenerator.makeMessages.
+ * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
+ * folders to ensure Thunderbird knows about the newly injected messages.
+ * If you are testing Thunderbird's use of updateFolder itself, you will
+ * not want this and so will want to pass true for this argument.
+ * @returns {SyntheticMessageSet[]} A Promise with a list of SyntheticMessageSet objects,
+ * each corresponding to the entry in synSetDefs (or implied if an integer was passed).
+ */
+ async makeNewSetsInFolders(msgFolders, synSetDefs, doNotForceUpdate) {
+ // - create the synthetic message sets
+ let messageSets = [];
+ for (let synSetDef of synSetDefs) {
+ // Using the getter of the MessageGenerator for error handling.
+ let messages = this.messageGenerator.makeMessages(synSetDef);
+ messageSets.push(new SyntheticMessageSet(messages));
+ }
+
+ // - add the messages to the folders (interleaving them)
+ await this.addSetsToFolders(msgFolders, messageSets, doNotForceUpdate);
+
+ return messageSets;
+ }
+ /**
+ * Spreads the messages in messageSets across the folders in msgFolders. Each
+ * message set is spread in a round-robin fashion across all folders. At the
+ * same time, each message-sets insertion is interleaved with the other message
+ * sets. This distributes message across multiple folders for useful
+ * cross-folder threading testing (via the round robin) while also hopefully
+ * avoiding making things pathologically easy for the code under test (by way
+ * of the interleaving.)
+ *
+ * For example, given the following 2 input message sets:
+ * message set 'lower': [a b c d e f]
+ * message set 'upper': [A B C D E F G H]
+ *
+ * across 2 folders:
+ * folder 1: [a A c C e E G]
+ * folder 2: [b B d D f F H]
+ * across 3 folders:
+ * folder 1: [a A d D G]
+ * folder 2: [b B e E H]
+ * folder 3: [c C f F]
+ *
+ * @param {nsIMsgFolder[]} msgFolders
+ * An nsIMsgFolder to add the message sets to or a list of them.
+ * @param {SyntheticMessageSet[]} messageSets A list of SyntheticMessageSets.
+ * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
+ * folders to ensure Thunderbird knows about the newly injected messages.
+ * If you are testing Thunderbird's use of updateFolder itself, you will
+ * not want this and so will want to pass true for this argument.
+ */
+ async addSetsToFolders(msgFolders, messageSets, doNotForceUpdate) {
+ let iterFolders;
+
+ this._mis.notifyListeners("onInjectingMessages", []);
+
+ // -- Pre-loop
+ if (this._mis.injectionConfig.mode == "local") {
+ for (let folder of msgFolders) {
+ if (!(folder instanceof Ci.nsIMsgLocalMailFolder)) {
+ throw new Error("All folders in msgFolders must be local folders!");
+ }
+ }
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // no protection is possible because of our dependency on promises,
+ // although we could check that the fake URL is one we handed out.
+ } else {
+ throw new Error("Message injection is not configured!");
+ }
+
+ if (this._mis.injectionConfig.mode == "local") {
+ // Note: in order to cut down on excessive fsync()s, we do a two-pass
+ // approach. In the first pass we just allocate messages to the folder
+ // we are going to insert them into. In the second pass we insert the
+ // messages into folders in batches and perform any mutations.
+ let folderBatches = msgFolders.map(folder => {
+ return { folder, messages: [] };
+ });
+ iterFolders = this._looperator([...folderBatches.keys()]);
+ let iPerSet = 0,
+ folderNext = iterFolders.next();
+
+ // - allocate messages to folders
+ // loop, incrementing our subscript until all message sets are out of messages
+ let didSomething;
+ do {
+ didSomething = false;
+ // for each message set, if it is not out of messages, add the message
+ for (let messageSet of messageSets) {
+ if (iPerSet < messageSet.synMessages.length) {
+ let synMsg = messageSet._trackMessageAddition(
+ folderBatches[folderNext.value].folder,
+ iPerSet
+ );
+ folderBatches[folderNext.value].messages.push({
+ messageSet,
+ synMsg,
+ index: iPerSet,
+ });
+ didSomething = true;
+ }
+ }
+ iPerSet++;
+ folderNext = iterFolders.next();
+ } while (didSomething);
+
+ // - inject messages
+ for (let folderBatch of folderBatches) {
+ // it is conceivable some folders might not get any messages, skip them.
+ if (!folderBatch.messages.length) {
+ continue;
+ }
+
+ let folder = folderBatch.folder;
+ folder.gettingNewMessages = true;
+ let messageStrings = folderBatch.messages.map(message =>
+ message.synMsg.toMboxString()
+ );
+ folder.addMessageBatch(messageStrings);
+
+ for (let message of folderBatch.messages) {
+ let synMsgState = message.synMsg.metaState;
+ // If we need to mark the message as junk grab the header and do so.
+ if (synMsgState.junk) {
+ message.messageSet.setJunk(
+ true,
+ message.messageSet.getMsgHdr(message.index)
+ );
+ }
+ if (synMsgState.read) {
+ // XXX this will generate an event; I'm not sure if we should be
+ // trying to avoid that or not. This case is really only added
+ // for IMAP where this makes more sense.
+ message.messageSet.setRead(
+ true,
+ message.messageSet.getMsgHdr(message.index)
+ );
+ }
+ }
+ if (folderBatch.messages.length) {
+ let lastMRUTime = Math.floor(
+ Number(folderBatch.messages[0].synMsg.date) / 1000
+ );
+ folder.setStringProperty("MRUTime", lastMRUTime);
+ }
+ folder.gettingNewMessages = false;
+ folder.hasNewMessages = true;
+ folder.setNumNewMessages(
+ folder.getNumNewMessages(false) + messageStrings.length
+ );
+ folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ }
+
+ // make sure that junk filtering gets a turn
+ // XXX we probably need to be doing more in terms of filters here,
+ // although since filters really want to be run on the inbox, there
+ // are separate potential semantic issues involved.
+ for (let folder of msgFolders) {
+ folder.callFilterPlugins(null);
+ }
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ iterFolders = this._looperator(msgFolders);
+ // we need to call updateFolder on all the folders, not just the first
+ // one...
+ let iPerSet = 0,
+ folder = iterFolders.next();
+ let didSomething;
+ do {
+ didSomething = false;
+ for (let messageSet of messageSets) {
+ if (iPerSet < messageSet.synMessages.length) {
+ didSomething = true;
+
+ let realFolder = this._mis.handleUriToRealFolder[folder.value];
+ let fakeFolder = this._mis.handleUriToFakeFolder[folder.value];
+ let synMsg = messageSet._trackMessageAddition(realFolder, iPerSet);
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(synMsg.toMessageString())
+ );
+ let imapMsg = new ImapMessage(
+ msgURI.spec,
+ fakeFolder.uidnext++,
+ []
+ );
+ // If the message's meta-state indicates it is junk, set that flag.
+ // There is also a NotJunk flag, but we're not playing with that
+ // right now; as long as nothing is ever marked as junk, the junk
+ // classifier won't run, so it's moot for now.
+ if (synMsg.metaState.junk) {
+ imapMsg.setFlag("Junk");
+ }
+ if (synMsg.metaState.read) {
+ imapMsg.setFlag("\\Seen");
+ }
+ fakeFolder.addMessage(imapMsg);
+ }
+ }
+ iPerSet++;
+ folder = iterFolders.next();
+ } while (didSomething);
+
+ // We have nothing more to do if we aren't support to force the update.
+ if (doNotForceUpdate) {
+ return;
+ }
+
+ for (let iFolder = 0; iFolder < msgFolders.length; iFolder++) {
+ let realFolder = this._mis.handleUriToRealFolder[msgFolders[iFolder]];
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(realFolder, resolve);
+ });
+
+ // compel download of the messages if appropriate
+ if (realFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ realFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Return the nsIMsgFolder associated with a folder handle. If the folder has
+ * been created since the last injection and you are using IMAP, you may need
+ * to first resolve the Promises for us to be able to provide
+ * you with a result.
+ *
+ * @param {nsIMsgFolder|string} folderHandle nsIMsgFolder or folder URI.
+ * @returns {nsIMsgFolder}
+ */
+ getRealInjectionFolder(folderHandle) {
+ if (this._mis.injectionConfig.mode == "imap") {
+ return this._mis.handleUriToRealFolder[folderHandle];
+ }
+ return folderHandle;
+ }
+ /**
+ * Move messages in the given set to the destination folder.
+ *
+ * For IMAP moves we force an update of the source folder and then the
+ * destination folder. This ensures that any (pseudo-)offline operations in
+ * the source folder have had a chance to run and that we have seen the changes
+ * in the target folder.
+ * We additionally cause all of the message bodies to be downloaded in the
+ * target folder if the folder has the Offline flag set.
+ *
+ * @param {SyntheticMessageSet} synMessageSet The messages to move.
+ * @param {nsIMsgFolder|string} destFolder The target folder or target folder URI.
+ * @param {boolean} [allowUndo=false] Should we generate undo operations and, as a
+ * side-effect, offline operations? (The code uses undo operations as
+ * a proxy-indicator for it coming from the UI and therefore performing
+ * pseudo-offline operations instead of trying to do things online.)
+ */
+ async moveMessages(synMessageSet, destFolder, allowUndo) {
+ let realDestFolder = this.getRealInjectionFolder(destFolder);
+
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ // In the IMAP case tell listeners we are moving messages without
+ // destination headers.
+ if (!this.messageInjectionIsLocal()) {
+ this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", [
+ realDestFolder,
+ ]);
+ }
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ folder,
+ msgs,
+ realDestFolder,
+ /* move */ true,
+ promiseCopyListener,
+ null,
+ Boolean(allowUndo)
+ );
+ await promiseCopyListener.promise;
+ // update the synthetic message set's folder entry...
+ synMessageSet._folderSwap(folder, realDestFolder);
+
+ // IMAP special case per function doc...
+ if (!this.messageInjectionIsLocal()) {
+ // update the source folder to force it to issue the move
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+
+ // update the dest folder to see the new header.
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(realDestFolder, resolve);
+ });
+
+ // compel download of messages in dest folder if appropriate
+ if (realDestFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ realDestFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Move the messages to the trash; do not use this on messages that are already
+ * in the trash, we are not clever enough for that.
+ *
+ * @param {SyntheticMessageSet} synMessageSet The set of messages to trash.
+ * The messages do not all have to be in the same folder,
+ * but we have to trash them folder by folder if they are not.
+ */
+ async trashMessages(synMessageSet) {
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ // In the IMAP case tell listeners we are moving messages without
+ // destination headers, since that's what trashing amounts to.
+ if (!this.messageInjectionIsLocal()) {
+ this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", []);
+ }
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(
+ msgs,
+ null,
+ false,
+ true,
+ promiseCopyListener,
+ /* do not allow undo, currently leaks */ false
+ );
+ await promiseCopyListener.promise;
+
+ // just like the move case we need to force updateFolder calls for IMAP
+ if (!this.messageInjectionIsLocal()) {
+ // update the source folder to force it to issue the move
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+
+ // trash folder may not have existed at startup but the deletion
+ // will have created it.
+ let trashFolder = this.getRealInjectionFolder(
+ await this.getTrashFolder()
+ );
+
+ // update the dest folder to see the new header.
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(trashFolder, resolve);
+ });
+
+ // compel download of messages in dest folder if appropriate
+ if (trashFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ trashFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Delete all of the messages in a SyntheticMessageSet like the user performed a
+ * shift-delete (or if the messages were already in the trash).
+ *
+ * @param {SyntheticMessageSet} synMessageSet The set of messages to delete.
+ * The messages do not all have to be in the same folder, but we have to
+ * delete them folder by folder if they are not.
+ */
+ static async deleteMessages(synMessageSet) {
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(
+ msgs,
+ null,
+ /* delete storage */ true,
+ /* is move? */ false,
+ promiseCopyListener,
+ /* do not allow undo, currently leaks */ false
+ );
+ await promiseCopyListener.promise;
+ }
+ }
+ /**
+ * Empty the trash.
+ */
+ async emptyTrash() {
+ let trashHandle = await this.getTrashFolder();
+ let trashFolder = this.getRealInjectionFolder(trashHandle);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ trashFolder.emptyTrash(promiseUrlListener);
+ await promiseUrlListener.promise;
+ }
+ /**
+ * Delete the given folder, removing the storage. We do not move it to the
+ * trash.
+ */
+ deleteFolder(folder) {
+ let realFolder = this.getRealInjectionFolder(folder);
+ realFolder.parent.propagateDelete(realFolder, true);
+ }
+
+ /**
+ * @param {nsIMsgFolder} folder
+ * @returns {nsIMsgFolder}
+ */
+ static get_nsIMsgFolder(folder) {
+ if (!(folder instanceof Ci.nsIMsgFolder)) {
+ return MailUtils.getOrCreateFolder(folder);
+ }
+ return folder;
+ }
+ /**
+ * An iterator that generates an infinite sequence of its argument. So
+ * _looperator(1, 2, 3) will generate the iteration stream: [1, 2, 3, 1, 2, 3,
+ * 1, 2, 3, ...].
+ */
+ *_looperator(list) {
+ if (list.length == 0) {
+ throw new Error("list must have at least one item!");
+ }
+
+ let i = 0,
+ length = list.length;
+ while (true) {
+ yield list[i];
+ i = (i + 1) % length;
+ }
+ }
+
+ get messageGenerator() {
+ if (this.msgGen === undefined) {
+ throw new Error(
+ "MessageInjection.jsm needs a MessageGenerator for new messages. " +
+ "The MessageGenerator helps you with threaded messages. If you use " +
+ "two different MessageGenerators the behaviour with threads are complicated."
+ );
+ }
+ return this.msgGen;
+ }
+ /**
+ * @param {MessageGenerator} msgGen The MessageGenerator which generates the new
+ * SyntheticMessages. We do not create our own because we would lose track of
+ * messages created from another MessageGenerator.
+ */
+ set messageGenerator(msgGen) {
+ this.msgGen = msgGen;
+ }
+}
diff --git a/comm/mailnews/test/resources/NetworkTestUtils.jsm b/comm/mailnews/test/resources/NetworkTestUtils.jsm
new file mode 100644
index 0000000000..131eb9c9eb
--- /dev/null
+++ b/comm/mailnews/test/resources/NetworkTestUtils.jsm
@@ -0,0 +1,294 @@
+/* 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/. */
+
+/**
+ * This file provides utilities useful in testing more advanced networking
+ * scenarios, such as proxies and SSL connections.
+ */
+
+const EXPORTED_SYMBOLS = ["NetworkTestUtils"];
+
+var CC = Components.Constructor;
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+// The following code is adapted from network/test/unit/test_socks.js, in order
+// to provide a SOCKS proxy server for our testing code.
+//
+// For more details on how SOCKSv5 works, please read RFC 1928.
+var currentThread = Services.tm.currentThread;
+
+const STATE_WAIT_GREETING = 1;
+const STATE_WAIT_SOCKS5_REQUEST = 2;
+
+/**
+ * A client of a SOCKS connection.
+ *
+ * This doesn't implement all of SOCKSv5, just enough to get a simple proxy
+ * working for the test code.
+ *
+ * @param {nsIInputStream} client_in - The nsIInputStream of the socket.
+ * @param {nsIOutputStream} client_out - The nsIOutputStream of the socket.
+ */
+function SocksClient(client_in, client_out) {
+ this.client_in = client_in;
+ this.client_out = client_out;
+ this.inbuf = [];
+ this.state = STATE_WAIT_GREETING;
+ this.waitRead(this.client_in);
+}
+SocksClient.prototype = {
+ // ... implement nsIInputStreamCallback ...
+ QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
+ onInputStreamReady(input) {
+ var len = input.available();
+ var bin = new BinaryInputStream(input);
+ var data = bin.readByteArray(len);
+ this.inbuf = this.inbuf.concat(data);
+
+ switch (this.state) {
+ case STATE_WAIT_GREETING:
+ this.handleGreeting();
+ break;
+ case STATE_WAIT_SOCKS5_REQUEST:
+ this.handleSocks5Request();
+ break;
+ }
+
+ if (!this.sub_transport) {
+ this.waitRead(input);
+ }
+ },
+
+ // Listen on the input for the next packet
+ waitRead(input) {
+ input.asyncWait(this, 0, 0, currentThread);
+ },
+
+ // Simple handler to write out a binary string (because xpidl sucks here)
+ write(buf) {
+ this.client_out.write(buf, buf.length);
+ },
+
+ // Handle the first SOCKSv5 client message
+ handleGreeting() {
+ if (this.inbuf.length == 0) {
+ return;
+ }
+
+ if (this.inbuf[0] != 5) {
+ dump("Unknown protocol version: " + this.inbuf[0] + "\n");
+ this.close();
+ return;
+ }
+
+ // Some quality checks to make sure we've read the entire greeting.
+ if (this.inbuf.length < 2) {
+ return;
+ }
+ var nmethods = this.inbuf[1];
+ if (this.inbuf.length < 2 + nmethods) {
+ return;
+ }
+ this.inbuf = [];
+
+ // Tell them that we don't log into this SOCKS server.
+ this.state = STATE_WAIT_SOCKS5_REQUEST;
+ this.write("\x05\x00");
+ },
+
+ // Handle the second SOCKSv5 message
+ handleSocks5Request() {
+ if (this.inbuf.length < 4) {
+ return;
+ }
+
+ // Find the address:port requested.
+ var atype = this.inbuf[3];
+ var len, addr;
+ if (atype == 0x01) {
+ // IPv4 Address
+ len = 4;
+ addr = this.inbuf.slice(4, 8).join(".");
+ } else if (atype == 0x03) {
+ // Domain name
+ len = this.inbuf[4];
+ addr = String.fromCharCode.apply(null, this.inbuf.slice(5, 5 + len));
+ len = len + 1;
+ } else if (atype == 0x04) {
+ // IPv6 address
+ len = 16;
+ addr = this.inbuf
+ .slice(4, 20)
+ .map(i => i.toString(16))
+ .join(":");
+ }
+ var port = (this.inbuf[4 + len] << 8) | this.inbuf[5 + len];
+ dump("Requesting " + addr + ":" + port + "\n");
+
+ // Map that data to the port we report.
+ var foundPort = gPortMap.get(addr + ":" + port);
+ dump("This was mapped to " + foundPort + "\n");
+
+ if (foundPort !== undefined) {
+ this.write(
+ "\x05\x00\x00" + // Header for response
+ "\x04" +
+ "\x00".repeat(15) +
+ "\x01" + // IPv6 address ::1
+ String.fromCharCode(foundPort >> 8) +
+ String.fromCharCode(foundPort & 0xff) // Port number
+ );
+ } else {
+ this.write(
+ "\x05\x05\x00" + // Header for failed response
+ "\x04" +
+ "\x00".repeat(15) +
+ "\x01" + // IPv6 address ::1
+ "\x00\x00"
+ );
+ this.close();
+ return;
+ }
+
+ // At this point, we contact the local server on that port and then we feed
+ // the data back and forth. Easiest way to do that is to open the connection
+ // and use the async copy to do it in a background thread.
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ let trans = sts.createTransport([], "localhost", foundPort, null, null);
+ let tunnelInput = trans.openInputStream(0, 1024, 1024);
+ let tunnelOutput = trans.openOutputStream(0, 1024, 1024);
+ this.sub_transport = trans;
+ NetUtil.asyncCopy(tunnelInput, this.client_out);
+ NetUtil.asyncCopy(this.client_in, tunnelOutput);
+ },
+
+ close() {
+ this.client_in.close();
+ this.client_out.close();
+ if (this.sub_transport) {
+ this.sub_transport.close(Cr.NS_OK);
+ }
+ },
+};
+
+// A SOCKS server that runs on a random port.
+function SocksTestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ dump("Starting SOCKS server on " + this.listener.port + "\n");
+ this.port = this.listener.port;
+ this.listener.asyncListen(this);
+ this.client_connections = [];
+}
+SocksTestServer.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
+
+ onSocketAccepted(socket, trans) {
+ var input = trans.openInputStream(0, 0, 0);
+ var output = trans.openOutputStream(0, 0, 0);
+ var client = new SocksClient(input, output);
+ this.client_connections.push(client);
+ },
+
+ onStopListening(socket) {},
+
+ close() {
+ for (let client of this.client_connections) {
+ client.close();
+ }
+ this.client_connections = [];
+ if (this.listener) {
+ this.listener.close();
+ this.listener = null;
+ }
+ },
+};
+
+var gSocksServer = null;
+// hostname:port -> the port on localhost that the server really runs on.
+var gPortMap = new Map();
+
+var NetworkTestUtils = {
+ /**
+ * Set up a proxy entry such that requesting a connection to hostName:port
+ * will instead cause a connection to localRemappedPort. This will use a SOCKS
+ * proxy (because any other mechanism is too complicated). Since this is
+ * starting up a server, it does behoove you to call shutdownServers when you
+ * no longer need to use the proxy server.
+ *
+ * @param {string} hostName - The DNS name to use for the client.
+ * @param {integer} hostPort - The port number to use for the client.
+ * @param {integer} localRemappedPort - The port number on which the real server sits.
+ */
+ configureProxy(hostName, hostPort, localRemappedPort) {
+ if (gSocksServer == null) {
+ gSocksServer = new SocksTestServer();
+ // Using PAC makes much more sense here. However, it turns out that PAC
+ // appears to be broken with synchronous proxy resolve, so enabling the
+ // PAC mode requires bug 791645 to be fixed first.
+ /*
+ let pac = 'data:text/plain,function FindProxyForURL(url, host) {' +
+ "if (host == 'localhost' || host == '127.0.0.1') {" +
+ 'return "DIRECT";' +
+ '}' +
+ 'return "SOCKS5 127.0.0.1:' + gSocksServer.port + '";' +
+ '}';
+ dump(pac + '\n');
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ */
+
+ // Until then, we'll serve the actual proxy via a proxy filter.
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+ let filter = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+ applyFilter(aURI, aProxyInfo, aCallback) {
+ if (aURI.host != "localhost" && aURI.host != "127.0.0.1") {
+ aCallback.onProxyFilterResult(
+ pps.newProxyInfo(
+ "socks",
+ "localhost",
+ gSocksServer.port,
+ "",
+ "",
+ Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
+ 0,
+ null
+ )
+ );
+ return;
+ }
+ aCallback.onProxyFilterResult(aProxyInfo);
+ },
+ };
+ pps.registerFilter(filter, 0);
+ }
+ dump("Requesting to map " + hostName + ":" + hostPort + "\n");
+ gPortMap.set(hostName + ":" + hostPort, localRemappedPort);
+ },
+
+ /**
+ * Turn off any servers started by this file (e.g., the SOCKS proxy server).
+ */
+ shutdownServers() {
+ if (gSocksServer) {
+ gSocksServer.close();
+ }
+ },
+};
diff --git a/comm/mailnews/test/resources/POP3pump.js b/comm/mailnews/test/resources/POP3pump.js
new file mode 100644
index 0000000000..5a083798b1
--- /dev/null
+++ b/comm/mailnews/test/resources/POP3pump.js
@@ -0,0 +1,269 @@
+/* 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/. */
+
+/**
+ * This routine will allow the easy processing of
+ * messages through the fake POP3 server into the local
+ * folder. It uses a single global defined as:
+ *
+ * gPOP3Pump: the main access to the routine
+ * gPOP3Pump.run() function to run to load the messages. Returns promise that
+ * resolves when done.
+ * gPOP3Pump.files: (in) an array of message files to load
+ * gPOP3Pump.onDone: function to execute after completion
+ (optional and deprecated)
+ * gPOP3Pump.fakeServer: (out) the POP3 incoming server
+ * gPOP3Pump.resetPluggableStore(): function to change the pluggable store for the
+ * server to the input parameter's store.
+ * (in) pluggable store contract ID
+ *
+ * adapted from test_pop3GetNewMail.js
+ *
+ * Original Author: Kent James <kent@caspia.com>
+ *
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+// Import the pop3 server scripts
+var { nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var {
+ Pop3Daemon,
+ POP3_RFC1939_handler,
+ POP3_RFC2449_handler,
+ POP3_RFC5034_handler,
+} = ChromeUtils.import("resource://testing-common/mailnews/Pop3d.jsm");
+
+function POP3Pump() {
+ // public attributes
+ this.fakeServer = null;
+ this.onDone = null;
+ this.files = null;
+
+ // local private variables
+
+ this.kPOP3_PORT = 1024 + 110;
+ this._server = null;
+ this._daemon = null;
+ this._incomingServer = null;
+ this._firstFile = true;
+ this._tests = [];
+ this._finalCleanup = false;
+ this._expectedResult = Cr.NS_OK;
+ this._actualResult = Cr.NS_ERROR_UNEXPECTED;
+ this._mailboxStoreContractID = Services.prefs.getCharPref(
+ "mail.serverDefaultStoreContractID"
+ );
+}
+
+// nsIUrlListener implementation
+POP3Pump.prototype.OnStartRunningUrl = function (url) {};
+
+POP3Pump.prototype.OnStopRunningUrl = function (aUrl, aResult) {
+ this._actualResult = aResult;
+ if (aResult != Cr.NS_OK) {
+ // If we have an error, clean up nicely.
+ this._server.stop();
+
+ var thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+ Assert.equal(aResult, this._expectedResult);
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, _checkPumpBusy);
+};
+
+// Setup the daemon and server
+// If the debugOption is set, then it will be applied to the server.
+POP3Pump.prototype._setupServerDaemon = function (aDebugOption) {
+ this._daemon = new Pop3Daemon();
+ function createHandler(d) {
+ return new POP3_RFC1939_handler(d);
+ }
+ this._server = new nsMailServer(createHandler, this._daemon);
+ if (aDebugOption) {
+ this._server.setDebugLevel(aDebugOption);
+ }
+ return [this._daemon, this._server];
+};
+
+POP3Pump.prototype._createPop3ServerAndLocalFolders = function () {
+ if (typeof localAccountUtils.inboxFolder == "undefined") {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ if (!this.fakeServer) {
+ this.fakeServer = localAccountUtils.create_incoming_server(
+ "pop3",
+ this.kPOP3_PORT,
+ "fred",
+ "wilma"
+ );
+ }
+
+ return this.fakeServer;
+};
+
+POP3Pump.prototype.resetPluggableStore = function (aStoreContractID) {
+ if (aStoreContractID == this._mailboxStoreContractID) {
+ return;
+ }
+
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ aStoreContractID
+ );
+
+ // Cleanup existing files, server and account instances, if any.
+ if (this._server) {
+ this._server.stop();
+ }
+
+ if (this.fakeServer && this.fakeServer.valid) {
+ this.fakeServer.closeCachedConnections();
+ MailServices.accounts.removeIncomingServer(this.fakeServer, false);
+ }
+
+ this.fakeServer = null;
+ localAccountUtils.clearAll();
+
+ this._incomingServer = this._createPop3ServerAndLocalFolders();
+ this._mailboxStoreContractID = aStoreContractID;
+};
+
+POP3Pump.prototype._checkBusy = function () {
+ if (this._tests.length == 0 && !this._finalCleanup) {
+ this._incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ this._server.stop();
+ this._finalCleanup = true;
+ do_timeout(20, _checkPumpBusy);
+ return;
+ }
+
+ if (this._finalCleanup) {
+ if (Services.tm.currentThread.hasPendingEvents()) {
+ do_timeout(20, _checkPumpBusy);
+ } else {
+ // exit this module
+ do_test_finished();
+ if (this.onDone) {
+ this._promise.then(this.onDone, this.onDone);
+ }
+ if (this._actualResult == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(this._actualResult);
+ }
+ }
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (this._incomingServer.serverBusy) {
+ do_timeout(20, _checkPumpBusy);
+ return;
+ }
+
+ this._testNext();
+};
+
+POP3Pump.prototype._testNext = function () {
+ let thisFiles = this._tests.shift();
+ if (!thisFiles) {
+ // Exit.
+ this._checkBusy();
+ }
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ if (this._firstFile) {
+ this._firstFile = false;
+
+ // Start the fake POP3 server
+ this._server.start();
+ this.kPOP3_PORT = this._server.port;
+ if (this.fakeServer) {
+ this.fakeServer.port = this.kPOP3_PORT;
+ }
+ } else {
+ this._server.resetTest();
+ }
+
+ // Set up the test
+ this._daemon.setMessages(thisFiles);
+
+ // Now get the mail, get inbox in case it got un-deferred.
+ let inbox = this._incomingServer.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ MailServices.pop3.GetNewMail(null, this, inbox, this._incomingServer);
+
+ this._server.performTest();
+ } catch (e) {
+ this._server.stop();
+
+ do_throw(e);
+ }
+};
+
+POP3Pump.prototype.run = function (aExpectedResult) {
+ do_test_pending();
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ this._server = this._setupServerDaemon();
+ this._daemon = this._server[0];
+ this._server = this._server[1];
+
+ this._firstFile = true;
+ this._finalCleanup = false;
+
+ if (aExpectedResult) {
+ this._expectedResult = aExpectedResult;
+ }
+
+ // In the default configuration, only a single test is accepted
+ // by this routine. But the infrastructure exists to support
+ // multiple tests, as this was in the original files. We leave that
+ // infrastructure in place, so that if desired this routine could
+ // be easily copied and modified to make multiple passes through
+ // a POP3 server.
+
+ this._tests[0] = this.files;
+
+ this._testNext();
+
+ // This probably does not work with multiple tests, but nobody is using that.
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ return this._promise;
+};
+
+var gPOP3Pump = new POP3Pump();
+gPOP3Pump._incomingServer = gPOP3Pump._createPop3ServerAndLocalFolders();
+
+function _checkPumpBusy() {
+ gPOP3Pump._checkBusy();
+}
diff --git a/comm/mailnews/test/resources/PromiseTestUtils.jsm b/comm/mailnews/test/resources/PromiseTestUtils.jsm
new file mode 100644
index 0000000000..cb40b8c856
--- /dev/null
+++ b/comm/mailnews/test/resources/PromiseTestUtils.jsm
@@ -0,0 +1,316 @@
+/* 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/. */
+
+/**
+ * This file provides utilities useful in using Promises and Task.jsm
+ * with mailnews tests.
+ */
+
+const EXPORTED_SYMBOLS = ["PromiseTestUtils"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Url listener that can wrap another listener and trigger a callback.
+ *
+ * @param [aWrapped] The nsIUrlListener to pass all notifications through to.
+ * This gets called prior to the callback (or async resumption).
+ */
+
+var PromiseTestUtils = {};
+
+PromiseTestUtils.PromiseUrlListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+};
+
+PromiseTestUtils.PromiseUrlListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIUrlListener"]),
+
+ OnStartRunningUrl(aUrl) {
+ if (this.wrapped && this.wrapped.OnStartRunningUrl) {
+ this.wrapped.OnStartRunningUrl(aUrl);
+ }
+ },
+ OnStopRunningUrl(aUrl, aExitCode) {
+ if (this.wrapped && this.wrapped.OnStopRunningUrl) {
+ this.wrapped.OnStopRunningUrl(aUrl, aExitCode);
+ }
+ if (aExitCode == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(aExitCode);
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Copy listener that can wrap another listener and trigger a callback.
+ *
+ * @param {nsIMsgCopyServiceListener} [aWrapped] - The nsIMsgCopyServiceListener
+ * to pass all notifications through to. This gets called prior to the
+ * callback (or async resumption).
+ */
+PromiseTestUtils.PromiseCopyListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._result = { messageKeys: [], messageIds: [] };
+};
+
+PromiseTestUtils.PromiseCopyListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
+ OnStartCopy() {
+ if (this.wrapped && this.wrapped.OnStartCopy) {
+ this.wrapped.OnStartCopy();
+ }
+ },
+ OnProgress(aProgress, aProgressMax) {
+ if (this.wrapped && this.wrapped.OnProgress) {
+ this.wrapped.OnProgress(aProgress, aProgressMax);
+ }
+ },
+ SetMessageKey(aKey) {
+ if (this.wrapped && this.wrapped.SetMessageKey) {
+ this.wrapped.SetMessageKey(aKey);
+ }
+
+ this._result.messageKeys.push(aKey);
+ },
+ SetMessageId(aMessageId) {
+ if (this.wrapped && this.wrapped.SetMessageId) {
+ this.wrapped.SetMessageId(aMessageId);
+ }
+
+ this._result.messageIds.push(aMessageId);
+ },
+ OnStopCopy(aStatus) {
+ if (this.wrapped && this.wrapped.OnStopCopy) {
+ this.wrapped.OnStopCopy(aStatus);
+ }
+
+ if (aStatus == Cr.NS_OK) {
+ this._resolve(this._result);
+ } else {
+ this._reject(aStatus);
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Stream listener that can wrap another listener and trigger a callback.
+ *
+ * @param {nsIStreamListener} [aWrapped] - The nsIStreamListener to pass all
+ * notifications through to. This gets called prior to the callback
+ * (or async resumption).
+ */
+PromiseTestUtils.PromiseStreamListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._data = null;
+ this._stream = null;
+};
+
+PromiseTestUtils.PromiseStreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ onStartRequest(aRequest) {
+ if (this.wrapped && this.wrapped.onStartRequest) {
+ this.wrapped.onStartRequest(aRequest);
+ }
+ this._data = "";
+ this._stream = null;
+ },
+
+ onStopRequest(aRequest, aStatusCode) {
+ if (this.wrapped && this.wrapped.onStopRequest) {
+ this.wrapped.onStopRequest(aRequest, aStatusCode);
+ }
+ if (aStatusCode == Cr.NS_OK) {
+ this._resolve(this._data);
+ } else {
+ this._reject(aStatusCode);
+ }
+ },
+
+ onDataAvailable(aRequest, aInputStream, aOff, aCount) {
+ if (this.wrapped && this.wrapped.onDataAvailable) {
+ this.wrapped.onDataAvailable(aRequest, aInputStream, aOff, aCount);
+ }
+ if (!this._stream) {
+ this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ this._stream.init(aInputStream);
+ }
+ this._data += this._stream.read(aCount);
+ },
+
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Folder listener to resolve a promise when a certain folder event occurs.
+ *
+ * @param {nsIMsgFolder} folder - nsIMsgFolder to listen to
+ * @param {string} event - Event name to listen for. Example event is
+ * "DeleteOrMoveMsgCompleted".
+ * @returns {Promise} Promise that resolves when the event occurs.
+ */
+PromiseTestUtils.promiseFolderEvent = function (folder, event) {
+ return new Promise((resolve, reject) => {
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderEvent(aEventFolder, aEvent) {
+ if (folder === aEventFolder && event == aEvent) {
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ resolve();
+ }
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+ });
+};
+
+/**
+ * Folder listener to resolve a promise when a certain folder event occurs.
+ *
+ * @param {nsIMsgFolder} folder - nsIMsgFolder to listen to.
+ * @param {string} listenerMethod - string listener method to listen for.
+ * Example listener method is "msgsClassified".
+ * @returns {Promise} Promise that resolves when the event occurs.
+ */
+PromiseTestUtils.promiseFolderNotification = function (folder, listenerMethod) {
+ return new Promise((resolve, reject) => {
+ let mfnListener = {};
+ mfnListener[listenerMethod] = function () {
+ let args = Array.from(arguments);
+ let flag = true;
+ for (let arg of args) {
+ if (folder && arg instanceof Ci.nsIMsgFolder) {
+ if (arg == folder) {
+ flag = true;
+ break;
+ } else {
+ return;
+ }
+ }
+ }
+
+ if (flag) {
+ MailServices.mfn.removeListener(mfnListener);
+ resolve(args);
+ }
+ };
+ MailServices.mfn.addListener(
+ mfnListener,
+ Ci.nsIMsgFolderNotificationService[listenerMethod]
+ );
+ });
+};
+
+/**
+ * Folder listener to resolve a promise when a folder with a certain
+ * name is added.
+ *
+ * @param {string} folderName - folder name to listen for
+ * @returns {Promise<nsIMsgFolder>} Promise that resolves with the new folder
+ * when the folder add completes.
+ */
+PromiseTestUtils.promiseFolderAdded = function (folderName) {
+ return new Promise((resolve, reject) => {
+ var listener = {
+ folderAdded: aFolder => {
+ if (aFolder.name == folderName) {
+ MailServices.mfn.removeListener(listener);
+ resolve(aFolder);
+ }
+ },
+ };
+ MailServices.mfn.addListener(
+ listener,
+ Ci.nsIMsgFolderNotificationService.folderAdded
+ );
+ });
+};
+
+/**
+ * Timer to resolve a promise after a delay
+ *
+ * @param {integer} aDelay - Delay in milliseconds
+ * @returns {Promise} Promise that resolves after the delay.
+ */
+PromiseTestUtils.promiseDelay = function (aDelay) {
+ return new Promise((resolve, reject) => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(resolve, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+ });
+};
+
+/**
+ * Search listener to resolve a promise when a search completes
+ *
+ * @param {nsIMsgSearchSession} aSearchSession - The nsIMsgSearchSession to search
+ * @param {nsIMsgSearchNotify} aWrapped - The nsIMsgSearchNotify to pass all
+ * notifications through to. This gets called prior to the callback
+ * (or async resumption).
+ */
+PromiseTestUtils.PromiseSearchNotify = function (aSearchSession, aWrapped) {
+ this._searchSession = aSearchSession;
+ this._searchSession.registerListener(this);
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+};
+
+PromiseTestUtils.PromiseSearchNotify.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSearchNotify"]),
+ onSearchHit(aHeader, aFolder) {
+ if (this.wrapped && this.wrapped.onSearchHit) {
+ this.wrapped.onSearchHit(aHeader, aFolder);
+ }
+ },
+ onSearchDone(aResult) {
+ this._searchSession.unregisterListener(this);
+ if (this.wrapped && this.wrapped.onSearchDone) {
+ this.wrapped.onSearchDone(aResult);
+ }
+ if (aResult == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(aResult);
+ }
+ },
+ onNewSearch() {
+ if (this.wrapped && this.wrapped.onNewSearch) {
+ this.wrapped.onNewSearch();
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/test/resources/abSetup.js b/comm/mailnews/test/resources/abSetup.js
new file mode 100644
index 0000000000..65e5e1bf82
--- /dev/null
+++ b/comm/mailnews/test/resources/abSetup.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Sets up the directory service provider to return the app dir as the profile
+ * directory for the address book to use for locating its files during the
+ * tests.
+ *
+ * Note there are further configuration setup items below this.
+ */
+
+/**
+ * General Configuration Data that applies to the address book.
+ */
+
+// Personal Address Book configuration items.
+var kPABData = {
+ URI: "jsaddrbook://abook.sqlite",
+ fileName: "abook.sqlite",
+ dirName: "Personal Address Book",
+ dirType: 101,
+ dirPrefID: "ldap_2.servers.pab",
+ readOnly: false,
+ position: 1,
+};
+
+// Collected Address Book configuration items.
+var kCABData = {
+ URI: "jsaddrbook://history.sqlite",
+ fileName: "history.sqlite",
+ dirName: "Collected Addresses",
+ dirType: 101,
+ dirPrefID: "ldap_2.servers.history",
+ readOnly: false,
+ position: 2,
+};
+
+// This currently applies to all address books of local type.
+var kNormalPropertiesURI =
+ "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml";
+
+/**
+ * Installs a pre-prepared address book file into the profile directory.
+ * This version is for JS/SQLite address books, if you create a new type,
+ * replace this function to test them.
+ *
+ * @param {string} source - Path to the source data, without extension
+ * @param {string} dest - Final file name in the profile, with extension
+ */
+function loadABFile(source, dest) {
+ let sourceFile = do_get_file(`${source}.sql`);
+ let destFile = do_get_profile();
+ destFile.append(dest);
+
+ info(`Creating ${destFile.path} from ${sourceFile.path}`);
+
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ fstream.init(sourceFile, -1, 0, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let data = "";
+ let read = 0;
+ do {
+ let str = {};
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ cstream.close();
+
+ let conn = Services.storage.openDatabase(destFile);
+ conn.executeSimpleSQL(data);
+ conn.close();
+}
diff --git a/comm/mailnews/test/resources/alertTestUtils.js b/comm/mailnews/test/resources/alertTestUtils.js
new file mode 100644
index 0000000000..2b757480e1
--- /dev/null
+++ b/comm/mailnews/test/resources/alertTestUtils.js
@@ -0,0 +1,487 @@
+/* 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/. */
+
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * This file provides support for writing mailnews tests that require hooking
+ * into the alerts system. Normally these tests would require a UI and fail in
+ * debug mode, but with this method you can hook into the alerts system and
+ * avoid the UI.
+ *
+ * This file registers prompts for nsIWindowWatcher::getNewPrompter and also
+ * registers a nsIPromptService service. nsIWindowWatcher::getNewAuthPrompter
+ * is also implemented but returns the nsILoginManagerPrompter as this would
+ * be expected when running mailnews.
+ *
+ * To register the system:
+ *
+ * function run_test() {
+ * registerAlertTestUtils();
+ * // ...
+ * }
+ *
+ * You can then hook into the alerts just by defining a function of the same
+ * name as the interface function:
+ *
+ * function alert(aDialogTitle, aText) {
+ * // do my check
+ * }
+ *
+ * Interface functions that do not have equivalent functions defined and get
+ * called will be treated as unexpected, and therefore they will call
+ * do_throw().
+ */
+/* globals alert, confirm, prompt */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+);
+
+// Wrapper to the nsIPrompt interface.
+// This allows the send code to attempt to display errors to the user without
+// failing.
+var alertUtilsPrompts = {
+ alert(aDialogTitle, aText) {
+ if (typeof alert == "function") {
+ alert(aDialogTitle, aText);
+ return;
+ }
+
+ do_throw("alert unexpectedly called: " + aText + "\n");
+ },
+
+ alertCheck(aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof alertCheck == "function") {
+ // eslint-disable-next-line no-undef
+ alertCheck(aDialogTitle, aText, aCheckMsg, aCheckState);
+ return;
+ }
+
+ do_throw("alertCheck unexpectedly called: " + aText + "\n");
+ },
+
+ confirm(aDialogTitle, aText) {
+ if (typeof confirm == "function") {
+ return confirm(aDialogTitle, aText);
+ }
+
+ do_throw("confirm unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmCheck(aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof confirmCheck == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmCheck(aDialogTitle, aText, aCheckMsg, aCheckState);
+ }
+
+ do_throw("confirmCheck unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmEx(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof confirmEx == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmEx(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmEx unexpectedly called: " + aText + "\n");
+ return 0;
+ },
+
+ prompt(aDialogTitle, aText, aValue, aCheckMsg, aCheckState) {
+ if (typeof prompt == "function") {
+ return prompt(aDialogTitle, aText, aValue, aCheckMsg, aCheckState);
+ }
+
+ do_throw("prompt unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof promptUsernameAndPassword == "function") {
+ // eslint-disable-next-line no-undef
+ return promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("promptUsernameAndPassword unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptPassword(aDialogTitle, aText, aPassword, aCheckMsg, aCheckState) {
+ if (typeof promptPassword == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPassword(
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("promptPassword unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ select(aDialogTitle, aText, aCount, aSelectList, aOutSelection) {
+ if (typeof select == "function") {
+ // eslint-disable-next-line no-undef
+ return select(aDialogTitle, aText, aCount, aSelectList, aOutSelection);
+ }
+
+ do_throw("select unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
+};
+
+var alertUtilsPromptService = {
+ alert(aParent, aDialogTitle, aText) {
+ if (typeof alertPS == "function") {
+ // eslint-disable-next-line no-undef
+ alertPS(aParent, aDialogTitle, aText);
+ return;
+ }
+
+ do_throw("alertPS unexpectedly called: " + aText + "\n");
+ },
+
+ alertCheck(aParent, aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof alertCheckPS == "function") {
+ // eslint-disable-next-line no-undef
+ alertCheckPS(aParent, aDialogTitle, aText, aCheckMsg, aCheckState);
+ return;
+ }
+
+ do_throw("alertCheckPS unexpectedly called: " + aText + "\n");
+ },
+
+ confirm(aParent, aDialogTitle, aText) {
+ if (typeof confirmPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmPS(aParent, aDialogTitle, aText);
+ }
+
+ do_throw("confirmPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmCheck(aParent, aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof confirmCheckPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmCheckPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmCheckPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmEx(
+ aParent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof confirmExPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmExPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmExPS unexpectedly called: " + aText + "\n");
+ return 0;
+ },
+
+ prompt(aParent, aDialogTitle, aText, aValue) {
+ if (typeof promptPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPS(aParent, aDialogTitle, aText, aValue);
+ }
+
+ do_throw("promptPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptUsernameAndPassword(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword
+ ) {
+ if (typeof promptUsernameAndPasswordPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword
+ );
+ }
+
+ do_throw(
+ "promptUsernameAndPasswordPS unexpectedly called: " + aText + "\n"
+ );
+ return false;
+ },
+
+ promptPassword(aParent, aDialogTitle, aText, aPassword) {
+ if (typeof promptPasswordPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPasswordPS(aParent, aDialogTitle, aText, aPassword);
+ }
+
+ do_throw("promptPasswordPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ select(aParent, aDialogTitle, aText, aCount, aSelectList, aOutSelection) {
+ if (typeof selectPS == "function") {
+ // eslint-disable-next-line no-undef
+ return selectPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aCount,
+ aSelectList,
+ aOutSelection
+ );
+ }
+
+ do_throw("selectPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPromptService",
+ "nsIPromptService2",
+ ]),
+};
+
+var alertUtilsWindowWatcher = {
+ getNewPrompter(aParent) {
+ return alertUtilsPrompts;
+ },
+
+ getNewAuthPrompter(aParent) {
+ return Cc["@mozilla.org/login-manager/authprompter;1"].getService(
+ Ci.nsIAuthPrompt
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]),
+};
+
+// Special prompt that ensures we get prompted for logins. Calls
+// promptPasswordPS/promptUsernameAndPasswordPS directly, rather than through
+// the prompt service, because the function signature changed and no longer
+// allows a "save password" check box.
+let alertUtilsMsgAuthPrompt = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ _getFormattedOrigin(aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI);
+ }
+
+ return uri.scheme + "://" + uri.displayHostPort;
+ },
+
+ _getRealmInfo(aRealmString) {
+ var httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString)) {
+ return [null, null, null];
+ }
+
+ var uri = Services.io.newURI(aRealmString);
+ var pathname = "";
+
+ if (uri.pathQueryRef != "/") {
+ pathname = uri.pathQueryRef;
+ }
+
+ var formattedOrigin = this._getFormattedOrigin(uri);
+
+ return [formattedOrigin, formattedOrigin + pathname, uri.username];
+ },
+
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aUsername,
+ aPassword
+ ) {
+ var checkBox = { value: false };
+ var checkBoxLabel = null;
+ var [origin, realm] = this._getRealmInfo(aPasswordRealm);
+
+ if (typeof promptUsernameAndPasswordPS != "function") {
+ throw new Error(
+ "promptUsernameAndPasswordPS unexpectedly called: " + aText + "\n"
+ );
+ }
+
+ // eslint-disable-next-line no-undef
+ var ok = promptUsernameAndPasswordPS(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (!ok || !checkBox.value || !origin) {
+ return ok;
+ }
+
+ if (!aPassword.value) {
+ return ok;
+ }
+
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ aUsername.value,
+ aPassword.value
+ );
+ Services.logins.addLogin(newLogin);
+
+ return ok;
+ },
+
+ promptPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aPassword
+ ) {
+ var checkBox = { value: false };
+ var checkBoxLabel = null;
+ var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ if (typeof promptPasswordPS != "function") {
+ throw new Error("promptPasswordPS unexpectedly called: " + aText + "\n");
+ }
+
+ // eslint-disable-next-line no-undef
+ var ok = promptPasswordPS(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (ok && checkBox.value && origin && aPassword.value) {
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ username,
+ aPassword.value
+ );
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ return ok;
+ },
+};
+
+function registerAlertTestUtils() {
+ MockRegistrar.register(
+ "@mozilla.org/embedcomp/window-watcher;1",
+ alertUtilsWindowWatcher
+ );
+ MockRegistrar.register(
+ "@mozilla.org/messenger/msgAuthPrompt;1",
+ alertUtilsMsgAuthPrompt
+ );
+ MockRegistrar.register("@mozilla.org/prompter;1", alertUtilsPromptService);
+ Services.prompt = alertUtilsPromptService;
+}
+
+var gDummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
diff --git a/comm/mailnews/test/resources/filterTestUtils.js b/comm/mailnews/test/resources/filterTestUtils.js
new file mode 100644
index 0000000000..c34d1147c0
--- /dev/null
+++ b/comm/mailnews/test/resources/filterTestUtils.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+// Utility functions for testing interactions with filters.
+
+var contains = Ci.nsMsgSearchOp.Contains;
+// This maps strings to a filter attribute (excluding the parameter)
+var ATTRIB_MAP = {
+ // Template : [attrib, op, field of value, otherHeader]
+ subject: [Ci.nsMsgSearchAttrib.Subject, contains, "str", null],
+ from: [Ci.nsMsgSearchAttrib.Sender, contains, "str", null],
+ date: [Ci.nsMsgSearchAttrib.Date, Ci.nsMsgSearchOp.Is, "date", null],
+ size: [Ci.nsMsgSearchAttrib.Size, Ci.nsMsgSearchOp.Is, "size", null],
+ "message-id": [
+ Ci.nsMsgSearchAttrib.OtherHeader + 1,
+ contains,
+ "str",
+ "Message-ID",
+ ],
+ "user-agent": [
+ Ci.nsMsgSearchAttrib.OtherHeader + 2,
+ contains,
+ "str",
+ "User-Agent",
+ ],
+};
+// And this maps strings to filter actions
+var ACTION_MAP = {
+ // Template : [action, auxiliary attribute field, auxiliary value]
+ priority: [Ci.nsMsgFilterAction.ChangePriority, "priority", 6],
+ delete: [Ci.nsMsgFilterAction.Delete],
+ read: [Ci.nsMsgFilterAction.MarkRead],
+ unread: [Ci.nsMsgFilterAction.MarkUnread],
+ kill: [Ci.nsMsgFilterAction.KillThread],
+ watch: [Ci.nsMsgFilterAction.WatchThread],
+ flag: [Ci.nsMsgFilterAction.MarkFlagged],
+ stop: [Ci.nsMsgFilterAction.StopExecution],
+ tag: [Ci.nsMsgFilterAction.AddTag, "strValue", "tag"],
+};
+
+/**
+ * Creates a filter and appends it to the nsIMsgFilterList.
+ *
+ * @param {nsIMsgFilter} list - An nsIMsgFilter to which the new filter will be appended.
+ * @param {string} trigger - A key of ATTRIB_MAP that represents the filter trigger.
+ * @param {string} value - The value of the filter trigger.
+ * @param {nsMsgFilterAction} action - A key of ACTION_MAP that represents the action to be taken.
+ */
+function createFilter(list, trigger, value, action) {
+ var filter = list.createFilter(trigger + action + "Test");
+ filter.filterType = Ci.nsMsgFilterType.NewsRule;
+
+ var searchTerm = filter.createTerm();
+ searchTerm.matchAll = false;
+ if (trigger in ATTRIB_MAP) {
+ let information = ATTRIB_MAP[trigger];
+ searchTerm.attrib = information[0];
+ if (information[3] != null) {
+ searchTerm.arbitraryHeader = information[3];
+ }
+ searchTerm.op = information[1];
+ var oldValue = searchTerm.value;
+ oldValue.attrib = information[0];
+ oldValue[information[2]] = value;
+ searchTerm.value = oldValue;
+ } else {
+ throw new Error("Unknown trigger " + trigger);
+ }
+ searchTerm.booleanAnd = true;
+ filter.appendTerm(searchTerm);
+
+ var filterAction = filter.createAction();
+ if (action in ACTION_MAP) {
+ let information = ACTION_MAP[action];
+ filterAction.type = information[0];
+ if (1 in information) {
+ filterAction[information[1]] = information[2];
+ }
+ } else {
+ throw new Error("Unknown action " + action);
+ }
+ filter.appendAction(filterAction);
+
+ filter.enabled = true;
+
+ // Add to the end
+ list.insertFilterAt(list.filterCount, filter);
+}
diff --git a/comm/mailnews/test/resources/folderEventLogHelper.js b/comm/mailnews/test/resources/folderEventLogHelper.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/comm/mailnews/test/resources/folderEventLogHelper.js
diff --git a/comm/mailnews/test/resources/logHelper.js b/comm/mailnews/test/resources/logHelper.js
new file mode 100644
index 0000000000..88a9616904
--- /dev/null
+++ b/comm/mailnews/test/resources/logHelper.js
@@ -0,0 +1,567 @@
+/* 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/. */
+
+/*
+ * Makes everything awesome if you are Andrew. Some day it will make everything
+ * awesome if you are not awesome too.
+ *
+ * Right now the most meaningful thing to know is that if XPCOM failures happen
+ * (and get reported to the error console), this will induce a unit test
+ * failure. You should think this is awesome no matter whether you are Andrew
+ * or not.
+ */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["Element", "Node"]);
+
+var _mailnewsTestLogger;
+var _xpcshellLogger;
+var _testLoggerContexts = [];
+var _testLoggerContextId = 0;
+var _testLoggerActiveContext;
+
+var _logHelperInterestedListeners = false;
+
+/**
+ * Let test code extend the list of allowed XPCOM errors.
+ */
+var logHelperAllowedErrors = ["NS_ERROR_FAILURE"];
+var logHelperAllowedWarnings = [/Quirks Mode/];
+
+/**
+ * Let other test helping code decide whether to register for potentially
+ * expensive notifications based on whether anyone can even hear those
+ * results.
+ */
+function logHelperHasInterestedListeners() {
+ return _logHelperInterestedListeners;
+}
+
+/**
+ * Tunnel nsIScriptErrors that show up on the error console to ConsoleInstance.
+ * We could send everything but I think only script errors are likely of much
+ * concern. Also, this nicely avoids infinite recursions no matter what you do
+ * since what we publish is not going to end up as an nsIScriptError.
+ *
+ * This is based on my (asuth') exmmad extension.
+ */
+var _errorConsoleTunnel = {
+ initialize() {
+ Services.console.registerListener(this);
+
+ // we need to unregister our listener at shutdown if we don't want explosions
+ Services.obs.addObserver(this, "quit-application");
+ },
+
+ shutdown() {
+ Services.console.unregisterListener(this);
+ Services.obs.removeObserver(this, "quit-application");
+ },
+
+ observe(aMessage, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this.shutdown();
+ return;
+ }
+
+ try {
+ if (
+ aMessage instanceof Ci.nsIScriptError &&
+ !aMessage.errorMessage.includes("Error console says")
+ ) {
+ // Unfortunately changes to mozilla-central are throwing lots
+ // of console errors during testing, so disable (we hope temporarily)
+ // failing on XPCOM console errors (see bug 1014350).
+ // An XPCOM error aMessage looks like this:
+ // [JavaScript Error: "uncaught exception: 2147500037"]
+ // Capture the number, and allow known XPCOM results.
+ let matches = /JavaScript Error: "(\w+)/.exec(aMessage);
+ let XPCOMresult = null;
+ if (matches) {
+ for (let result in Cr) {
+ if (matches[1] == Cr[result]) {
+ XPCOMresult = result;
+ break;
+ }
+ }
+ let message = XPCOMresult || aMessage;
+ if (logHelperAllowedErrors.some(e => e == matches[1])) {
+ if (XPCOMresult) {
+ info("Ignoring XPCOM error: " + message);
+ }
+ return;
+ }
+ info("Found XPCOM error: " + message);
+ }
+ // Ignore warnings that match a white-listed pattern.
+ if (
+ /JavaScript Warning:/.test(aMessage) &&
+ logHelperAllowedWarnings.some(w => w.test(aMessage))
+ ) {
+ return;
+ }
+ dump(`Error console says: ${aMessage}`);
+ }
+ } catch (ex) {
+ // This is to avoid pathological error loops. we definitely do not
+ // want to propagate an error here.
+ }
+ },
+};
+
+// This defaults to undefined and is for use by test-folder-display-helpers
+// so that it can pre-initialize the value so that when we are evaluated in
+// its subscript loader we see a value of 'true'.
+var _do_not_wrap_xpcshell;
+
+/**
+ * Initialize logging. The idea is to:
+ *
+ * - Always create a dump appender on 'test'.
+ * - Check if there's a desire to use a logsploder style network connection
+ * based on the presence of an appropriate file in 'tmp'. This should be
+ * harmless in cases where there is not such a file.
+ *
+ * We will wrap the interesting xpcshell functions if we believe there is an
+ * endpoint that cares about these things (such as logsploder).
+ */
+function _init_log_helper() {
+ // - dump on test
+ _mailnewsTestLogger = console.createInstance({
+ prefix: "test.test",
+ });
+
+ // - silent category for xpcshell stuff that already gets dump()ed
+ _xpcshellLogger = console.createInstance({
+ prefix: "xpcshell",
+ });
+
+ // Create a console listener reporting thinger in all cases. Since XPCOM
+ // failures will show up via the error console, this allows our test to fail
+ // in more situations where we might otherwise silently be cool with bad
+ // things happening.
+ _errorConsoleTunnel.initialize();
+
+ if (_logHelperInterestedListeners) {
+ if (!_do_not_wrap_xpcshell) {
+ _wrap_xpcshell_functions();
+ }
+
+ // Send a message telling the listeners about the test file being run.
+ _xpcshellLogger.info({
+ _jsonMe: true,
+ _isContext: true,
+ _specialContext: "lifecycle",
+ _id: "start",
+ testFile: _TEST_FILE,
+ });
+ }
+}
+_init_log_helper();
+
+/**
+ * Mark the start of a test. This creates nice console output as well as
+ * setting up logging contexts so that use of other helpers in here
+ * get associated with the context.
+ *
+ * This will likely only be used by the test driver framework, such as
+ * asyncTestUtils.js. However, |mark_sub_test_start| is for user test code.
+ */
+function mark_test_start(aName, aParameter, aDepth) {
+ if (aDepth == null) {
+ aDepth = 0;
+ }
+
+ // clear out any existing contexts
+ mark_test_end(aDepth);
+
+ let term = aDepth == 0 ? "test" : "subtest";
+ _testLoggerActiveContext = {
+ type: term,
+ name: aName,
+ parameter: aParameter,
+ _id: ++_testLoggerContextId,
+ };
+ if (_testLoggerContexts.length) {
+ _testLoggerActiveContext._contextDepth = _testLoggerContexts.length;
+ _testLoggerActiveContext._contextParentId =
+ _testLoggerContexts[_testLoggerContexts.length - 1]._id;
+ }
+ _testLoggerContexts.push(_testLoggerActiveContext);
+
+ _mailnewsTestLogger.info(
+ _testLoggerActiveContext._id,
+ "Starting " + term + ": " + aName + (aParameter ? ", " + aParameter : "")
+ );
+}
+
+/**
+ * Mark the end of a test started by |mark_test_start|.
+ */
+function mark_test_end(aPopTo) {
+ if (aPopTo === undefined) {
+ aPopTo = 0;
+ }
+ // clear out any existing contexts
+ while (_testLoggerContexts.length > aPopTo) {
+ let context = _testLoggerContexts.pop();
+ _mailnewsTestLogger.info(
+ context._id,
+ "Finished " +
+ context.type +
+ ": " +
+ context.name +
+ (context.parameter ? ", " + context.parameter : "")
+ );
+ }
+}
+
+/**
+ * For user test code and test support code to mark sub-regions of tests.
+ *
+ * @param {string} aName The name of the (sub) test.
+ * @param {string} [aParameter=null] The parameter if the test is being parameterized.
+ * @param {boolean} [aNest=false] Should this nest inside other sub-tests?
+ * If you omit orpass false, we will close out any existing sub-tests.
+ * If you pass true, we nest inside the previous test/sub-test and rely on
+ * you to call |mark_sub_test_end|.
+ * Sub tests can lost no longer than their parent.
+ * You should strongly consider using the aNest parameter if you are test
+ * support code.
+ */
+function mark_sub_test_start(aName, aParameter, aNest) {
+ let depth = aNest ? _testLoggerContexts.length : 1;
+ mark_test_start(aName, aParameter, depth);
+}
+
+/**
+ * Mark the end of a sub-test. Because sub-tests can't outlive their parents,
+ * there is no ambiguity about what sub-test we are closing out.
+ */
+function mark_sub_test_end() {
+ if (_testLoggerContexts.length <= 1) {
+ return;
+ }
+ mark_test_end(_testLoggerContexts.length - 1);
+}
+
+/**
+ * Express that all tests were run to completion. This helps the listener
+ * distinguish between successful termination and abort-style termination where
+ * the process just keeled over and on one told us.
+ *
+ * This also tells us to clean up.
+ */
+function mark_all_tests_run() {
+ // make sure all tests get closed out
+ mark_test_end();
+
+ _xpcshellLogger.info("All finished");
+}
+
+function _explode_flags(aFlagWord, aFlagDefs) {
+ let flagList = [];
+
+ for (let flagName in aFlagDefs) {
+ let flagVal = aFlagDefs[flagName];
+ if (flagVal & aFlagWord) {
+ flagList.push(flagName);
+ }
+ }
+
+ return flagList;
+}
+
+var _registered_json_normalizers = [];
+
+/**
+ * Copy natives or objects, deferring to _normalize_for_json for objects.
+ */
+function __value_copy(aObj, aDepthAllowed) {
+ if (aObj == null || typeof aObj != "object") {
+ return aObj;
+ }
+ return _normalize_for_json(aObj, aDepthAllowed, true);
+}
+
+/**
+ * Simple object copier to limit accidentally JSON-ing a ridiculously complex
+ * object graph or getting tripped up by prototypes.
+ *
+ * @param {object} aObj - Input object.
+ * @param {integer} aDepthAllowed - How many times we are allowed to recursively
+ * call ourselves.
+ */
+function __simple_obj_copy(aObj, aDepthAllowed) {
+ let oot = {};
+ let nextDepth = aDepthAllowed - 1;
+ for (let key in aObj) {
+ // avoid triggering getters
+ if (aObj.__lookupGetter__(key)) {
+ oot[key] = "*getter*";
+ continue;
+ }
+ let value = aObj[key];
+
+ if (value == null) {
+ oot[key] = null;
+ } else if (typeof value != "object") {
+ oot[key] = value;
+ } else if (!aDepthAllowed) {
+ // steal control flow if no more depth is allowed
+ oot[key] = "truncated, string rep: " + value.toString();
+ } else if (Array.isArray(value)) {
+ // array? (not directly counted, but we will terminate because the
+ // child copying occurs using nextDepth...)
+ oot[key] = value.map(v => __value_copy(v, nextDepth));
+ } else {
+ // it's another object! woo!
+ oot[key] = _normalize_for_json(value, nextDepth, true);
+ }
+ }
+
+ // let's take advantage of the object's native toString now
+ oot._stringRep = aObj.toString();
+
+ return oot;
+}
+
+var _INTERESTING_MESSAGE_HEADER_PROPERTIES = {
+ "gloda-id": 0,
+ "gloda-dirty": 0,
+ junkscore: "",
+ junkscoreorigin: "",
+ msgOffset: 0,
+ offlineMsgSize: 0,
+};
+
+/**
+ * Given an object, attempt to normalize it into an interesting JSON
+ * representation.
+ *
+ * We transform generally interesting mail objects like:
+ * - nsIMsgFolder
+ * - nsIMsgDBHdr
+ */
+function _normalize_for_json(aObj, aDepthAllowed, aJsonMeNotNeeded) {
+ if (aDepthAllowed === undefined) {
+ aDepthAllowed = 2;
+ }
+
+ // if it's a simple type just return it direct
+ if (typeof aObj != "object") {
+ return aObj;
+ } else if (aObj == null) {
+ return aObj;
+ }
+
+ // recursively transform arrays outright
+ if (Array.isArray(aObj)) {
+ return aObj.map(v => __value_copy(v, aDepthAllowed - 1));
+ }
+
+ // === Mail Specific ===
+ // (but common and few enough to not split out)
+ if (aObj instanceof Ci.nsIMsgFolder) {
+ return {
+ type: "folder",
+ name: aObj.prettyName,
+ uri: aObj.URI,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgFolderFlags),
+ };
+ } else if (aObj instanceof Ci.nsIMsgDBHdr) {
+ let properties = {};
+ for (let name in _INTERESTING_MESSAGE_HEADER_PROPERTIES) {
+ let propType = _INTERESTING_MESSAGE_HEADER_PROPERTIES[name];
+ if (propType === 0) {
+ properties[name] =
+ aObj.getStringProperty(name) != ""
+ ? aObj.getUint32Property(name)
+ : null;
+ } else {
+ properties[name] = aObj.getStringProperty(name);
+ }
+ }
+ return {
+ type: "msgHdr",
+ name: aObj.folder.URI + "#" + aObj.messageKey,
+ subject: aObj.mime2DecodedSubject,
+ from: aObj.mime2DecodedAuthor,
+ to: aObj.mime2DecodedRecipients,
+ messageKey: aObj.messageKey,
+ messageId: aObj.messageId,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgMessageFlags),
+ interestingProperties: properties,
+ };
+ } else if (Node.isInstance(aObj)) {
+ // === Generic ===
+ // DOM nodes, including elements
+ let name = aObj.nodeName;
+ let objAttrs = {};
+
+ if (Element.isInstance(aObj)) {
+ name += "#" + aObj.getAttribute("id");
+ }
+
+ if ("attributes" in aObj) {
+ let nodeAttrs = aObj.attributes;
+ for (let iAttr = 0; iAttr < nodeAttrs.length; iAttr++) {
+ objAttrs[nodeAttrs[iAttr].name] = nodeAttrs[iAttr].value;
+ }
+ }
+
+ let bounds = { left: null, top: null, width: null, height: null };
+ if ("getBoundingClientRect" in aObj) {
+ bounds = aObj.getBoundingClientRect();
+ }
+
+ return {
+ type: "domNode",
+ name,
+ value: aObj.nodeValue,
+ namespace: aObj.namespaceURI,
+ boundingClientRect: bounds,
+ attrs: objAttrs,
+ };
+ } else if (aObj instanceof Ci.nsIDOMWindow) {
+ let winId, title;
+ if (aObj.document && aObj.document.documentElement) {
+ title = aObj.document.title;
+ winId =
+ aObj.document.documentElement.getAttribute("windowtype") ||
+ aObj.document.documentElement.getAttribute("id") ||
+ "unnamed";
+ } else {
+ winId = "n/a";
+ title = "no document";
+ }
+ return {
+ type: "domWindow",
+ id: winId,
+ title,
+ location: "" + aObj.location,
+ coords: { x: aObj.screenX, y: aObj.screenY },
+ dims: { width: aObj.outerWidth, height: aObj.outerHeight },
+ };
+ } else if (aObj instanceof Error) {
+ // Although straight JS exceptions should serialize pretty well, we can
+ // improve things by making "stack" more friendly.
+ return {
+ type: "error",
+ message: aObj.message,
+ fileName: aObj.fileName,
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ stack: aObj.stack ? aObj.stack.split(/\n\r?/g) : null,
+ _stringRep: aObj.message,
+ };
+ } else if (aObj instanceof Ci.nsIException) {
+ return {
+ type: "error",
+ message: "nsIException: " + aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ result: aObj.result,
+ stack: null,
+ };
+ } else if (aObj instanceof Ci.nsIStackFrame) {
+ return {
+ type: "stackFrame",
+ name: aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ };
+ } else if (aObj instanceof Ci.nsIScriptError) {
+ return {
+ type: "stackFrame",
+ name: aObj.errorMessage,
+ category: aObj.category,
+ fileName: aObj.sourceName,
+ lineNumber: aObj.lineNumber,
+ };
+ }
+
+ for (let [checkType, handler] of _registered_json_normalizers) {
+ if (aObj instanceof checkType) {
+ return handler(aObj);
+ }
+ }
+
+ // Do not fall into simple object walking if this is an XPCOM interface.
+ // We might run across getters and that leads to nothing good.
+ if (aObj instanceof Ci.nsISupports) {
+ return {
+ type: "XPCOM",
+ name: aObj.toString(),
+ };
+ }
+
+ let simple_obj = __simple_obj_copy(aObj, aDepthAllowed);
+ if (!aJsonMeNotNeeded) {
+ simple_obj._jsonMe = true;
+ }
+ return simple_obj;
+}
+
+function register_json_normalizer(aType, aHandler) {
+ _registered_json_normalizers.push([aType, aHandler]);
+}
+
+/*
+ * Wrap the xpcshell test functions that do interesting things. The idea is
+ * that we clobber these only if we're going to value-add; that decision
+ * gets made up top in the initialization function.
+ *
+ * Since eq/neq fall-through to do_throw in the explosion case, we don't handle
+ * that since the scoping means that we're going to see the resulting
+ * do_throw.
+ */
+
+var _orig_do_throw;
+var _orig_do_check_neq;
+var _orig_do_check_eq;
+// do_check_true is implemented in terms of do_check_eq
+// do_check_false is implemented in terms of do_check_eq
+
+function _CheckAction(aSuccess, aLeft, aRight, aStack) {
+ this.type = "check";
+ this.success = aSuccess;
+ this.left = _normalize_for_json(aLeft);
+ this.right = _normalize_for_json(aRight);
+ this.stack = _normalize_for_json(aStack);
+}
+_CheckAction.prototype = {
+ _jsonMe: true,
+ // we don't need a toString because we should not go out to the console
+};
+
+/**
+ * Representation of a failure from do_throw.
+ */
+function _Failure(aText, aStack) {
+ this.type = "failure";
+ this.text = aText;
+ this.stack = _normalize_for_json(aStack);
+}
+_Failure.prototype = {
+ _jsonMe: true,
+};
+
+function _wrapped_do_throw(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ // We need to use an info because otherwise explosion loggers can get angry
+ // and they may be indiscriminate about what they subscribe to.
+ _xpcshellLogger.info(_testLoggerActiveContext, new _Failure(text, stack));
+
+ return _orig_do_throw(text, stack);
+}
+
+function _wrap_xpcshell_functions() {
+ _orig_do_throw = do_throw;
+ do_throw = _wrapped_do_throw; // eslint-disable-line no-global-assign
+}
diff --git a/comm/mailnews/test/resources/mailShutdown.js b/comm/mailnews/test/resources/mailShutdown.js
new file mode 100644
index 0000000000..0a5a5aa092
--- /dev/null
+++ b/comm/mailnews/test/resources/mailShutdown.js
@@ -0,0 +1,51 @@
+/* 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/. */
+
+/* Provides methods to make sure our test shuts down mailnews properly. */
+
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+// Notifies everyone that the we're shutting down. This is needed to make sure
+// that e.g. the account manager closes and cleans up correctly. It is semi-fake
+// because we don't actually do any work to make sure the profile goes away, but
+// it will mimic the behaviour in the app sufficiently.
+//
+// See also http://developer.mozilla.org/en/Observer_Notifications
+function postShutdownNotifications() {
+ // first give everyone a heads up about us shutting down. if someone wants
+ // to cancel this, our test should fail.
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+ if (cancelQuit.data) {
+ do_throw("Cannot shutdown: Someone cancelled the quit request!");
+ }
+
+ // post all notifications in the right order. none of these are cancellable
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNNETTEARDOWN
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNTEARDOWN
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
+ );
+
+ // finally, the xpcom-shutdown notification is handled by XPCOM itself.
+}
+
+MockRegistrar.unregisterAll();
+
+// First do a gc to let anything not being referenced be cleaned up.
+gc();
+
+// Now shut everything down.
+postShutdownNotifications();
diff --git a/comm/mailnews/test/resources/msgFolderListenerSetup.js b/comm/mailnews/test/resources/msgFolderListenerSetup.js
new file mode 100644
index 0000000000..72eaedb7a3
--- /dev/null
+++ b/comm/mailnews/test/resources/msgFolderListenerSetup.js
@@ -0,0 +1,429 @@
+/* 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/. */
+
+// ChromeUtils.import should be used for this, but it breaks mozmill.
+// Assume whatever test loaded this file already has mailTestUtils.
+/* globals mailTestUtils */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var allTestedEvents =
+ MailServices.mfn.msgAdded |
+ MailServices.mfn.msgsClassified |
+ MailServices.mfn.msgsJunkStatusChanged |
+ MailServices.mfn.msgsDeleted |
+ MailServices.mfn.msgsMoveCopyCompleted |
+ MailServices.mfn.msgKeyChanged |
+ MailServices.mfn.msgUnincorporatedMoved |
+ MailServices.mfn.folderAdded |
+ MailServices.mfn.folderDeleted |
+ MailServices.mfn.folderMoveCopyCompleted |
+ MailServices.mfn.folderRenamed |
+ MailServices.mfn.folderCompactStart |
+ MailServices.mfn.folderCompactFinish |
+ MailServices.mfn.folderReindexTriggered;
+
+// Current test being executed
+var gTest = 1;
+
+// Which events are expected
+var gExpectedEvents = [];
+
+// The current status (what all has been done)
+var gCurrStatus = 0;
+var kStatus = {
+ notificationsDone: 0x1,
+ onStopCopyDone: 0x2,
+ functionCallDone: 0x4,
+ everythingDone: 0,
+};
+kStatus.everythingDone =
+ kStatus.notificationsDone | kStatus.onStopCopyDone | kStatus.functionCallDone;
+
+// For copyFileMessage: this stores the header that was received
+var gHdrsReceived = [];
+
+var gMsgHdrs = [];
+
+// Our listener, which captures events and verifies them as they are received.
+var gMFListener = {
+ msgAdded(aMsg) {
+ verify([MailServices.mfn.msgAdded, aMsg]);
+ // We might not actually have a header in gHdrsReceived in the IMAP case,
+ // so use the aMsg we got instead
+ gMsgHdrs.push({ hdr: aMsg, ID: aMsg.messageId });
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ } else if (gExpectedEvents[0][0] == MailServices.mfn.msgsClassified) {
+ // XXX this is a hack to deal with limitations of the classification logic
+ // and the new list. We want to issue a call to clear the list once all
+ // the messages have been added, which would be when the next expected
+ // event is msgsClassified. (The limitation is that if we don't do this,
+ // we can end up getting told about this message again later.)
+ aMsg.folder.clearNewMessages();
+ }
+ },
+
+ msgsClassified(aMsgs, aJunkProcessed, aTraitProcessed) {
+ dump("classified id: " + aMsgs[0].messageId + "\n");
+ verify([
+ MailServices.mfn.msgsClassified,
+ aMsgs,
+ aJunkProcessed,
+ aTraitProcessed,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsJunkStatusChanged(messages) {
+ verify([MailServices.mfn.msgsJunkStatusChanged, messages]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsDeleted(aMsgs) {
+ verify([MailServices.mfn.msgsDeleted, aMsgs]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ verify([
+ MailServices.mfn.msgsMoveCopyCompleted,
+ aMove,
+ aSrcMsgs,
+ aDestFolder,
+ aDestMsgs,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgKeyChanged(aOldKey, aNewMsgHdr) {
+ verify([MailServices.mfn.msgKeyChanged, aOldKey, aNewMsgHdr]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgUnincorporatedMoved(srcFolder, msg) {
+ verify([MailServices.mfn.msgUnincorporatedMoved, srcFolder, msg]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderAdded(aFolder) {
+ verify([MailServices.mfn.folderAdded, aFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderDeleted(aFolder) {
+ verify([MailServices.mfn.folderDeleted, aFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ verify([
+ MailServices.mfn.folderMoveCopyCompleted,
+ aMove,
+ aSrcFolder,
+ aDestFolder,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ verify([MailServices.mfn.folderRenamed, aOrigFolder, aNewFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderCompactStart(folder) {
+ verify([MailServices.mfn.folderCompactStart, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderCompactFinish(folder) {
+ verify([MailServices.mfn.folderCompactFinish, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderReindexTriggered(folder) {
+ verify([MailServices.mfn.folderReindexTriggered, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+};
+
+// Copy listener, for proceeding after each operation.
+var copyListener = {
+ // For copyFileMessage: this should be the folder the message is being stored to
+ mFolderStoredIn: null,
+ mMessageId: "",
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ gHdrsReceived.push(this.mFolderStoredIn.GetMessageHeader(aKey));
+ },
+ GetMessageId(aMessageId) {
+ aMessageId = { value: this.mMessageId };
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ gCurrStatus |= kStatus.onStopCopyDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ },
+};
+
+function resetStatusAndProceed() {
+ gHdrsReceived.length = 0;
+ gCurrStatus = 0;
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, () => {
+ this.doTest(++gTest);
+ });
+}
+
+// Checks whether the array returned from a function has exactly these elements.
+function hasExactlyElements(array, elements) {
+ // If an nsIArray (it could also be a single header or a folder)
+ if (elements instanceof Ci.nsIArray) {
+ var count = elements.length;
+
+ // Check: array sizes should be equal.
+ Assert.equal(count, array.length);
+
+ for (let i = 0; i < count; i++) {
+ // Check: query element, must be a header or folder and present in the array
+ var currElement;
+ try {
+ currElement = elements.queryElementAt(i, Ci.nsIMsgDBHdr);
+ } catch (e) {}
+ if (!currElement) {
+ try {
+ currElement = elements.queryElementAt(i, Ci.nsIMsgFolder);
+ } catch (e) {}
+ }
+ Assert.equal(typeof currElement, "object");
+ Assert.notEqual(
+ mailTestUtils.non_strict_index_of(array, currElement),
+ -1
+ );
+ }
+ } else if (Array.isArray(elements)) {
+ Assert.equal(elements.length, array.length);
+ for (let el of elements) {
+ Assert.equal(typeof el, "object");
+ Assert.equal(
+ el instanceof Ci.nsIMsgDBHdr || el instanceof Ci.nsIMsgFolder,
+ true
+ );
+ Assert.notEqual(mailTestUtils.non_strict_index_of(array, el), -1);
+ }
+ } else if (
+ elements instanceof Ci.nsIMsgDBHdr ||
+ elements instanceof Ci.nsIMsgFolder
+ ) {
+ // If a single header or a folder
+
+ // Check: there should be only one element in the array.
+ Assert.equal(array.length, 1);
+
+ // Check: the element should be present
+ Assert.notEqual(mailTestUtils.non_strict_index_of(array, elements), -1);
+ } else {
+ // This shouldn't happen
+ do_throw("Unrecognized item returned from listener");
+ }
+}
+
+// Verifies an event
+function verify(event) {
+ // Check: make sure we actually have an item to process
+ Assert.ok(gExpectedEvents.length >= 1);
+ var expected = gExpectedEvents.shift();
+
+ // Check: events match.
+ var eventType = expected[0];
+ Assert.equal(event[0], eventType);
+
+ dump("..... Verifying event type " + eventType + "\n");
+
+ switch (eventType) {
+ case MailServices.mfn.msgAdded:
+ // So for IMAP right now, we aren't able to get the actual nsIMsgDBHdr.
+ // Instead, we'll match up message ids as a (poor?) substitute.
+ if (expected[1].expectedMessageId) {
+ Assert.equal(expected[1].expectedMessageId, event[1].messageId);
+ break;
+ }
+ // If we do have a header, fall through to the case below
+ case MailServices.mfn.msgsDeleted:
+ case MailServices.mfn.folderDeleted:
+ // Check: headers match/folder matches.
+ hasExactlyElements(expected[1], event[1]);
+ break;
+ case MailServices.mfn.msgsClassified:
+ // In the IMAP case expected[1] is a list of mesage-id strings whereas in
+ // the local case (where we are copying from files), we actually have
+ // the headers.
+ if (typeof expected[1][0] == "string") {
+ // IMAP; message id strings
+ // The IMAP case has additional complexity in that the 'new message'
+ // list is not tailored to our needs and so may over-report about
+ // new messagse. So to deal with this we make sure the msgsClassified
+ // event is telling us about at least the N expected events and that
+ // the last N of these events match
+ if (event[1].length < expected[1].length) {
+ do_throw("Not enough reported classified messages.");
+ }
+ let ignoreCount = event[1].length - expected[1].length;
+ for (let i = 0; i < expected[1].length; i++) {
+ let eventHeader = event[1][i + ignoreCount];
+ Assert.equal(expected[1][i], eventHeader.messageId);
+ }
+ } else {
+ // actual headers
+ hasExactlyElements(expected[1], event[1]);
+ }
+ // aJunkProcessed: was the message processed for junk?
+ Assert.equal(expected[2], event[2]);
+ // aTraitProcessed: was the message processed for traits?
+ Assert.equal(expected[3], event[3]);
+ break;
+ case MailServices.mfn.msgsJunkStatusChanged:
+ // Check: same messages?
+ hasExactlyElements(expected[1], event[1]);
+ break;
+ case MailServices.mfn.msgKeyChanged:
+ Assert.equal(expected[1].expectedMessageId, event[2].messageId);
+ break;
+ case MailServices.mfn.msgUnincorporatedMoved:
+ // Check: Same folder?
+ Assert.equal(expected[1].URI, event[1].URI);
+ // Check: message matches?
+ hasExactlyElements(expected[2], event[2]);
+ break;
+ case MailServices.mfn.msgsMoveCopyCompleted:
+ case MailServices.mfn.folderMoveCopyCompleted:
+ // Check: Move or copy as expected.
+ Assert.equal(expected[1], event[1]);
+
+ // Check: headers match/folder matches.
+ hasExactlyElements(expected[2], event[2]);
+
+ // Check: destination folder matches.
+ Assert.equal(expected[3].URI, event[3].URI);
+
+ if (eventType == MailServices.mfn.folderMoveCopyCompleted) {
+ break;
+ }
+
+ // Check: destination headers. We expect these for local and imap folders,
+ // but we will not have heard about the headers ahead of time,
+ // so the best we can do is make sure they match up. To this end,
+ // we check that the message-id header values match up.
+ for (let iMsg = 0; iMsg < event[2].length; iMsg++) {
+ let srcHdr = event[2][iMsg];
+ let destHdr = event[4][iMsg];
+ Assert.equal(srcHdr.messageId, destHdr.messageId);
+ }
+ break;
+ case MailServices.mfn.folderAdded:
+ // Check: parent folder matches
+ Assert.equal(expected[1].URI, event[1].parent.URI);
+
+ // Check: folder name matches
+ Assert.equal(expected[2], event[1].prettyName);
+ Assert.equal(expected[2], event[1].name);
+
+ // Not a check, but call the passed in callback with the new folder,
+ // used e.g. to store this folder somewhere.
+ if (expected[3]) {
+ expected[3](event[1]);
+ }
+ break;
+ case MailServices.mfn.folderRenamed:
+ // Check: source folder matches
+ hasExactlyElements(expected[1], event[1]);
+
+ // Check: destination folder name matches
+ Assert.equal(expected[2], event[2].prettyName);
+ break;
+ case MailServices.mfn.folderCompactStart:
+ case MailServices.mfn.folderCompactFinish:
+ case MailServices.mfn.folderReindexTriggered:
+ // Check: same folder?
+ Assert.equal(expected[1].URI, event[1].URI);
+ break;
+ }
+}
diff --git a/comm/mailnews/test/resources/passwordStorage.js b/comm/mailnews/test/resources/passwordStorage.js
new file mode 100644
index 0000000000..26db74c5a7
--- /dev/null
+++ b/comm/mailnews/test/resources/passwordStorage.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/* globals gDEPTH */
+
+if (typeof gDEPTH == "undefined") {
+ do_throw("gDEPTH must be defined when using passwordStorage.js");
+}
+
+/**
+ * Use the given storage database as the current signon database.
+ *
+ * @returns {Promise} Promive for when the storage database is usable.
+ */
+function setupForPassword(storageName) {
+ let keyDB = do_get_file(gDEPTH + "mailnews/data/key4.db");
+ keyDB.copyTo(do_get_profile(), "key4.db");
+
+ let signons = do_get_file(gDEPTH + "mailnews/data/" + storageName);
+ signons.copyTo(do_get_profile(), "logins.json");
+ return Services.logins.initializationPromise;
+}
diff --git a/comm/mailnews/test/resources/searchTestUtils.js b/comm/mailnews/test/resources/searchTestUtils.js
new file mode 100644
index 0000000000..df26824bca
--- /dev/null
+++ b/comm/mailnews/test/resources/searchTestUtils.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+// Contains various functions commonly used in testing mailnews search.
+
+/**
+ * TestSearch: Class to test number of search hits
+ *
+ * @param {nsIMsgFolder} aFolder - The folder to search
+ * @param {string|integer} aValue - value used for the search
+ * The interpretation of aValue depends on aAttrib. It
+ * defaults to string, but for certain attributes other
+ * types are used.
+ * WARNING: not all attributes have been tested.
+ *
+ * @param {nsMsgSearchAttrib} aAttrib - Attribute for the search (Ci.nsMsgSearchAttrib.Size, etc.)
+ * @param {nsMsgSearchOp} aOp - Operation for the search (Ci.nsMsgSearchOp.Contains, etc.)
+ * @param {integer} aHitCount - Expected number of search hits
+ * @param {Function} onDone - Function to call on completion of search
+ * @param {string} aCustomId - Id string for the custom action, if aAttrib is Custom
+ * @param {string} aArbitraryHeader - For OtherHeader case, header.
+ * @param {string|integer} aHdrProperty - For HdrProperty and Uint32HdrProperty case
+ *
+ */
+function TestSearch(
+ aFolder,
+ aValue,
+ aAttrib,
+ aOp,
+ aHitCount,
+ onDone,
+ aCustomId,
+ aArbitraryHeader,
+ aHdrProperty
+) {
+ var searchListener = {
+ onSearchHit(dbHdr, folder) {
+ hitCount++;
+ },
+ onSearchDone(status) {
+ print("Finished search does " + aHitCount + " equal " + hitCount + "?");
+ searchSession = null;
+ Assert.equal(aHitCount, hitCount);
+ if (onDone) {
+ onDone();
+ }
+ },
+ onNewSearch() {
+ hitCount = 0;
+ },
+ };
+
+ // define and initiate the search session
+
+ var hitCount;
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder);
+ var searchTerm = searchSession.createTerm();
+ searchTerm.attrib = aAttrib;
+
+ var value = searchTerm.value;
+ // This is tricky - value.attrib must be set before actual values
+ value.attrib = aAttrib;
+ if (aAttrib == Ci.nsMsgSearchAttrib.JunkPercent) {
+ value.junkPercent = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Priority) {
+ value.priority = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Date) {
+ value.date = aValue;
+ } else if (
+ aAttrib == Ci.nsMsgSearchAttrib.MsgStatus ||
+ aAttrib == Ci.nsMsgSearchAttrib.FolderFlag ||
+ aAttrib == Ci.nsMsgSearchAttrib.Uint32HdrProperty
+ ) {
+ value.status = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.MessageKey) {
+ value.msgKey = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Size) {
+ value.size = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.AgeInDays) {
+ value.age = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.JunkStatus) {
+ value.junkStatus = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
+ value.status = Ci.nsMsgMessageFlags.Attachment;
+ } else {
+ value.str = aValue;
+ }
+ searchTerm.value = value;
+ searchTerm.op = aOp;
+ searchTerm.booleanAnd = false;
+ if (aAttrib == Ci.nsMsgSearchAttrib.Custom) {
+ searchTerm.customId = aCustomId;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.OtherHeader) {
+ searchTerm.arbitraryHeader = aArbitraryHeader;
+ } else if (
+ aAttrib == Ci.nsMsgSearchAttrib.HdrProperty ||
+ aAttrib == Ci.nsMsgSearchAttrib.Uint32HdrProperty
+ ) {
+ searchTerm.hdrProperty = aHdrProperty;
+ }
+
+ searchSession.appendTerm(searchTerm);
+ searchSession.registerListener(searchListener);
+ searchSession.search(null);
+}
+
+/*
+ * Test search validity table Available and Enabled settings
+ *
+ * @param aScope: search scope (Ci.nsMsgSearchScope.offlineMail, etc.)
+ * @param aOp: search operation (Ci.nsMsgSearchOp.Contains, etc.)
+ * @param aAttrib: search attribute (Ci.nsMsgSearchAttrib.Size, etc.)
+ * @param aValue: expected value (true/false) for Available and Enabled
+ */
+const gValidityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+].getService(Ci.nsIMsgSearchValidityManager);
+
+function testValidityTable(aScope, aOp, aAttrib, aValue) {
+ var validityTable = gValidityManager.getTable(aScope);
+ var isAvailable = validityTable.getAvailable(aAttrib, aOp);
+ var isEnabled = validityTable.getEnabled(aAttrib, aOp);
+ if (aValue) {
+ Assert.ok(isAvailable);
+ Assert.ok(isEnabled);
+ } else {
+ Assert.ok(!isAvailable);
+ Assert.ok(!isEnabled);
+ }
+}
diff --git a/comm/mailnews/test/resources/smimeUtils.jsm b/comm/mailnews/test/resources/smimeUtils.jsm
new file mode 100644
index 0000000000..db7cf2e5c9
--- /dev/null
+++ b/comm/mailnews/test/resources/smimeUtils.jsm
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * This file provides some utilities for helping run S/MIME tests.
+ */
+
+var EXPORTED_SYMBOLS = ["SmimeUtils"];
+
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const gCertDialogs = {
+ confirmDownloadCACert: (ctx, cert, trust) => {
+ dump("Requesting certificate download\n");
+ trust.value = Ci.nsIX509CertDB.TRUSTED_EMAIL;
+ return true;
+ },
+ setPKCS12FilePassword: (ctx, password) => {
+ throw new Error("Not implemented");
+ },
+ getPKCS12FilePassword: (ctx, password) => {
+ password.value = "";
+ return true;
+ },
+ viewCert: (ctx, cert) => {
+ throw new Error("Not implemented");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICertificateDialogs"]),
+};
+
+var SmimeUtils = {
+ ensureNSS() {
+ // Ensure NSS is initialized.
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+ // Set up the internal key token so that subsequent code doesn't fail. If
+ // this isn't done, we'll fail to work if the NSS databases didn't already
+ // exist.
+ let keydb = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
+ Ci.nsIPK11TokenDB
+ );
+ try {
+ keydb.getInternalKeyToken().initPassword("");
+ } catch (e) {
+ // In this scenario, the key token already had its password initialized.
+ // Therefore, we don't need to do anything (assuming its password is
+ // empty).
+ }
+
+ MockRegistrar.register("@mozilla.org/nsCertificateDialogs;1", gCertDialogs);
+ },
+
+ loadPEMCertificate(file, certType, loadKey = false) {
+ dump("Loading certificate from " + file.path + "\n");
+ let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ certDB.importCertsFromFile(file, certType);
+ },
+
+ loadCertificateAndKey(file, pw) {
+ dump("Loading key from " + file.path + "\n");
+ let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ certDB.importPKCS12File(file, pw);
+ },
+};